aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2023-08-14 15:38:30 -0700
committerXin Li <delphij@google.com>2023-08-14 15:38:30 -0700
commitbddf63953e111d742b591c1c0c7c34bcda8a51c7 (patch)
tree3a93128bff4b737b24b0c9581922c0b20410f0f4
parentee890da55c82b95deca3518d5f3777e3d8ca9f0e (diff)
parentfbb9890f8922aa55fde183655a0017e69127ea4b (diff)
downloadpigweed-tmp_amf_298295554.tar.gz
Merge Android U (ab/10368041)tmp_amf_298295554
Bug: 291102124 Merged-In: I10c41adb8fe3e126cfa4ff2f49b15863fff379de Change-Id: I66f7a6cccaafc173d3924dae62a736c6c53520c7
-rw-r--r--.allstar/binary_artifacts.yaml4
-rw-r--r--.bazelignore7
-rw-r--r--.bazelrc22
-rw-r--r--.black.toml20
-rw-r--r--.clang-tidy5
-rw-r--r--.git-blame-ignore-revs64
-rw-r--r--.gitignore16
-rw-r--r--.gn5
-rw-r--r--.mypy.ini2
-rw-r--r--.pw_ide.yaml3
-rw-r--r--.pylintrc38
-rw-r--r--BUILD.bazel41
-rw-r--r--BUILD.gn307
-rw-r--r--CMakeLists.txt43
-rw-r--r--Kconfig.zephyr4
-rw-r--r--OWNERS2
-rw-r--r--PIGWEED_MODULES15
-rw-r--r--PW_PLUGINS8
-rw-r--r--README.md8
-rw-r--r--WORKSPACE296
-rw-r--r--bootstrap.bat7
-rw-r--r--bootstrap.sh8
-rw-r--r--docker/BUILD.gn4
-rw-r--r--docker/Dockerfile.cache1
-rw-r--r--docs/BUILD.gn110
-rw-r--r--docs/Doxyfile2796
-rw-r--r--docs/_static/css/pigweed.css63
-rw-r--r--docs/_static/pw_logo.icobin0 -> 4286 bytes
-rw-r--r--docs/_static/pw_logo.svg97
-rw-r--r--docs/automated_analysis.rst14
-rw-r--r--docs/build_system.rst23
-rw-r--r--docs/concepts/index.rst18
-rw-r--r--docs/conf.py104
-rw-r--r--docs/contributing.rst288
-rw-r--r--docs/embedded_cpp_guide.rst15
-rw-r--r--docs/faq.rst21
-rw-r--r--docs/getting_started.rst52
-rw-r--r--docs/images/pw_watch_test_demo2.gifbin0 -> 864841 bytes
-rw-r--r--docs/index.rst77
-rw-r--r--docs/module_structure.rst33
-rw-r--r--docs/os_abstraction_layers.rst12
-rw-r--r--docs/python_build.rst664
-rw-r--r--docs/run_doxygen.py95
-rw-r--r--docs/size_optimizations.rst79
-rw-r--r--docs/style_guide.rst870
-rw-r--r--docs/targets.rst2
-rw-r--r--docs/templates/docs/api.rst13
-rw-r--r--docs/templates/docs/cli.rst11
-rw-r--r--docs/templates/docs/concepts.rst9
-rw-r--r--docs/templates/docs/design.rst14
-rw-r--r--docs/templates/docs/docs.rst117
-rw-r--r--docs/templates/docs/gui.rst9
-rw-r--r--docs/templates/docs/guides.rst15
-rw-r--r--docs/templates/docs/tutorials/index.rst13
-rw-r--r--jest.config.ts32
-rw-r--r--package-lock.json17115
-rw-r--r--package.json88
-rw-r--r--pigweed.json9
-rw-r--r--pw_alignment/BUILD.bazel26
-rw-r--r--pw_alignment/BUILD.gn36
-rw-r--r--pw_alignment/docs.rst42
-rw-r--r--pw_alignment/public/pw_alignment/alignment.h84
-rw-r--r--pw_allocator/BUILD.gn24
-rw-r--r--pw_allocator/CMakeLists.txt90
-rw-r--r--pw_allocator/block.cc35
-rw-r--r--pw_allocator/block_test.cc81
-rw-r--r--pw_allocator/freelist.cc28
-rw-r--r--pw_allocator/freelist_heap.cc24
-rw-r--r--pw_allocator/freelist_heap_test.cc3
-rw-r--r--pw_allocator/freelist_test.cc48
-rw-r--r--pw_allocator/public/pw_allocator/block.h5
-rw-r--r--pw_allocator/public/pw_allocator/freelist.h14
-rw-r--r--pw_allocator/public/pw_allocator/freelist_heap.h14
-rw-r--r--pw_allocator/py/BUILD.gn1
-rw-r--r--pw_allocator/py/pw_allocator/heap_viewer.py198
-rw-r--r--pw_allocator/py/setup.cfg2
-rw-r--r--pw_analog/public/pw_analog/microvolt_input.h2
-rw-r--r--pw_android_toolchain/BUILD.gn4
-rw-r--r--pw_android_toolchain/docs.rst77
-rw-r--r--pw_arduino_build/BUILD.bazel10
-rw-r--r--pw_arduino_build/BUILD.gn4
-rw-r--r--pw_arduino_build/arduino.gni2
-rw-r--r--pw_arduino_build/py/BUILD.bazel58
-rw-r--r--pw_arduino_build/py/BUILD.gn1
-rw-r--r--pw_arduino_build/py/builder_test.py40
-rw-r--r--pw_arduino_build/py/file_operations_test.py93
-rw-r--r--pw_arduino_build/py/pw_arduino_build/__main__.py290
-rwxr-xr-xpw_arduino_build/py/pw_arduino_build/builder.py440
-rw-r--r--pw_arduino_build/py/pw_arduino_build/core_installer.py116
-rw-r--r--pw_arduino_build/py/pw_arduino_build/file_operations.py79
-rw-r--r--pw_arduino_build/py/pw_arduino_build/log.py7
-rw-r--r--pw_arduino_build/py/pw_arduino_build/teensy_detector.py48
-rwxr-xr-xpw_arduino_build/py/pw_arduino_build/unit_test_client.py18
-rwxr-xr-xpw_arduino_build/py/pw_arduino_build/unit_test_runner.py208
-rw-r--r--pw_arduino_build/py/pw_arduino_build/unit_test_server.py99
-rw-r--r--pw_arduino_build/py/setup.cfg1
-rw-r--r--pw_assert/Android.bp25
-rw-r--r--pw_assert/BUILD.bazel64
-rw-r--r--pw_assert/BUILD.gn98
-rw-r--r--pw_assert/CMakeLists.txt130
-rw-r--r--pw_assert/assert_backend_compile_test.cc3
-rw-r--r--pw_assert/assert_compatibility_public_overrides/pw_assert_backend/assert_backend.h (renamed from pw_assert/assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h)0
-rw-r--r--pw_assert/assert_facade_test.cc285
-rw-r--r--pw_assert/assert_test.cc10
-rw-r--r--pw_assert/backend.cmake22
-rw-r--r--pw_assert/backend.gni4
-rw-r--r--pw_assert/docs.rst25
-rw-r--r--pw_assert/fake_backend.cc2
-rw-r--r--pw_assert/libc_assert_public_overrides/assert.h16
-rw-r--r--pw_assert/libc_assert_public_overrides/cassert16
-rw-r--r--pw_assert/print_and_abort_assert_public_overrides/pw_assert_backend/assert_backend.h (renamed from pw_assert/print_and_abort_public_overrides/pw_assert_backend/assert_lite_backend.h)0
-rw-r--r--pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h52
-rw-r--r--pw_assert/public/pw_assert/assert.h57
-rw-r--r--pw_assert/public/pw_assert/check.h2
-rw-r--r--pw_assert/public/pw_assert/config.h28
-rw-r--r--pw_assert/public/pw_assert/internal/check_impl.h21
-rw-r--r--pw_assert/public/pw_assert/internal/libc_assert.h42
-rw-r--r--pw_assert/public/pw_assert/internal/print_and_abort.h47
-rw-r--r--pw_assert_basic/BUILD.bazel5
-rw-r--r--pw_assert_basic/BUILD.gn27
-rw-r--r--pw_assert_basic/CMakeLists.txt41
-rw-r--r--pw_assert_basic/backend.cmake29
-rw-r--r--pw_assert_basic/basic_handler.cc49
-rw-r--r--pw_assert_basic/docs.rst31
-rw-r--r--pw_assert_basic/public/pw_assert_basic/assert_basic.h32
-rw-r--r--pw_assert_basic/public/pw_assert_basic/handler.h2
-rw-r--r--pw_assert_basic/public_overrides/pw_assert_backend/check_backend.h (renamed from pw_assert_basic/public_overrides/pw_assert_backend/assert_backend.h)0
-rw-r--r--pw_assert_log/Android.bp29
-rw-r--r--pw_assert_log/BUILD.bazel17
-rw-r--r--pw_assert_log/BUILD.gn60
-rw-r--r--pw_assert_log/CMakeLists.txt30
-rw-r--r--pw_assert_log/assert_backend_public_overrides/pw_assert_backend/assert_backend.h (renamed from pw_assert_log/public_overrides/pw_assert_backend/assert_backend.h)2
-rw-r--r--pw_assert_log/assert_log.cc8
-rw-r--r--pw_assert_log/check_backend_public_overrides/pw_assert_backend/check_backend.h (renamed from pw_assert_log/assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h)4
-rw-r--r--pw_assert_log/public/pw_assert_log/assert_log.h60
-rw-r--r--pw_assert_log/public/pw_assert_log/check_log.h75
-rw-r--r--pw_assert_tokenized/BUILD.bazel8
-rw-r--r--pw_assert_tokenized/BUILD.gn11
-rw-r--r--pw_assert_tokenized/CMakeLists.txt57
-rw-r--r--pw_assert_tokenized/assert_public_overrides/pw_assert_backend/assert_backend.h (renamed from pw_assert_tokenized/assert_public_overrides/pw_assert_backend/assert_lite_backend.h)0
-rw-r--r--pw_assert_tokenized/check_public_overrides/pw_assert_backend/check_backend.h (renamed from pw_assert_tokenized/check_public_overrides/pw_assert_backend/assert_backend.h)0
-rw-r--r--pw_assert_tokenized/docs.rst14
-rw-r--r--pw_assert_tokenized/log_handler.cc34
-rw-r--r--pw_assert_tokenized/public/pw_assert_tokenized/check_tokenized.h11
-rw-r--r--pw_assert_zephyr/BUILD.gn4
-rw-r--r--pw_assert_zephyr/CMakeLists.txt29
-rw-r--r--pw_assert_zephyr/docs.rst8
-rw-r--r--pw_assert_zephyr/public/pw_assert_zephyr/assert_zephyr.h36
-rw-r--r--pw_assert_zephyr/public/pw_assert_zephyr/check_zephyr.h48
-rw-r--r--pw_assert_zephyr/public_overrides/pw_assert_backend/assert_backend.h6
-rw-r--r--pw_assert_zephyr/public_overrides/pw_assert_backend/check_backend.h16
-rw-r--r--pw_async/BUILD.bazel25
-rw-r--r--pw_async/BUILD.gn94
-rw-r--r--pw_async/README.md (renamed from pw_web_ui/OWNERS)0
-rw-r--r--pw_async/async.gni22
-rw-r--r--pw_async/backend.gni23
-rw-r--r--pw_async/docs.rst174
-rw-r--r--pw_async/fake_dispatcher_fixture.gni38
-rw-r--r--pw_async/fake_dispatcher_test.cc216
-rw-r--r--pw_async/fake_dispatcher_test.gni43
-rw-r--r--pw_async/public/pw_async/dispatcher.h58
-rw-r--r--pw_async/public/pw_async/fake_dispatcher.h87
-rw-r--r--pw_async/public/pw_async/fake_dispatcher_fixture.h66
-rw-r--r--pw_async/public/pw_async/internal/types.h45
-rw-r--r--pw_async/public/pw_async/task.h62
-rw-r--r--pw_async_basic/BUILD.bazel30
-rw-r--r--pw_async_basic/BUILD.gn182
-rw-r--r--pw_async_basic/README.md0
-rw-r--r--pw_async_basic/dispatcher.cc169
-rw-r--r--pw_async_basic/dispatcher_test.cc200
-rw-r--r--pw_async_basic/docs.rst60
-rw-r--r--pw_async_basic/fake_dispatcher.cc134
-rw-r--r--pw_async_basic/fake_dispatcher_fixture_test.cc36
-rw-r--r--pw_async_basic/public/pw_async_basic/dispatcher.h96
-rw-r--r--pw_async_basic/public/pw_async_basic/fake_dispatcher.h73
-rw-r--r--pw_async_basic/public/pw_async_basic/task.h74
-rw-r--r--pw_async_basic/public_overrides/pw_async_backend/fake_dispatcher.h15
-rw-r--r--pw_async_basic/public_overrides/pw_async_backend/task.h15
-rw-r--r--pw_async_basic/size_report/BUILD.gn33
-rw-r--r--pw_async_basic/size_report/post_1_task.cc27
-rw-r--r--pw_async_basic/size_report/task.cc23
-rw-r--r--pw_base64/BUILD.bazel2
-rw-r--r--pw_base64/BUILD.gn4
-rw-r--r--pw_base64/CMakeLists.txt20
-rw-r--r--pw_base64/base64.cc19
-rw-r--r--pw_base64/base64_test.cc67
-rw-r--r--pw_base64/base64_test_c.c2
-rw-r--r--pw_base64/public/pw_base64/base64.h31
-rw-r--r--pw_bloat/BUILD.gn4
-rw-r--r--pw_bloat/bloat.cmake27
-rw-r--r--pw_bloat/bloat.gni329
-rw-r--r--pw_bloat/bloat_macros.ld73
-rw-r--r--pw_bloat/bloat_this_binary.cc2
-rw-r--r--pw_bloat/docs.rst337
-rw-r--r--pw_bloat/examples/BUILD.gn47
-rw-r--r--pw_bloat/py/BUILD.gn19
-rw-r--r--pw_bloat/py/bloaty_config_test.py103
-rw-r--r--pw_bloat/py/label_test.py242
-rw-r--r--pw_bloat/py/pw_bloat/__main__.py133
-rw-r--r--pw_bloat/py/pw_bloat/binary_diff.py85
-rw-r--r--pw_bloat/py/pw_bloat/binary_size_aggregator.py75
-rwxr-xr-xpw_bloat/py/pw_bloat/bloat.py383
-rw-r--r--pw_bloat/py/pw_bloat/bloat_output.py212
-rw-r--r--pw_bloat/py/pw_bloat/bloaty_config.py193
-rw-r--r--pw_bloat/py/pw_bloat/label.py290
-rw-r--r--pw_bloat/py/pw_bloat/label_output.py625
-rw-r--r--pw_bloat/py/setup.cfg1
-rw-r--r--pw_bloat/test_label.csv205
-rw-r--r--pw_blob_store/BUILD.bazel1
-rw-r--r--pw_blob_store/BUILD.gn7
-rw-r--r--pw_blob_store/CMakeLists.txt10
-rw-r--r--pw_blob_store/blob_store.cc23
-rw-r--r--pw_blob_store/blob_store_chunk_write_test.cc16
-rw-r--r--pw_blob_store/blob_store_deferred_write_test.cc12
-rw-r--r--pw_blob_store/blob_store_test.cc40
-rw-r--r--pw_blob_store/flat_file_system_entry.cc19
-rw-r--r--pw_blob_store/flat_file_system_entry_test.cc8
-rw-r--r--pw_blob_store/public/pw_blob_store/blob_store.h18
-rw-r--r--pw_blob_store/public/pw_blob_store/flat_file_system_entry.h4
-rw-r--r--pw_blob_store/public/pw_blob_store/internal/metadata_format.h1
-rw-r--r--pw_blob_store/size_report/base.cc4
-rw-r--r--pw_blob_store/size_report/basic_blob.cc4
-rw-r--r--pw_blob_store/size_report/deferred_write_blob.cc4
-rw-r--r--pw_bluetooth/BUILD.bazel112
-rw-r--r--pw_bluetooth/BUILD.gn186
-rw-r--r--pw_bluetooth/CMakeLists.txt87
-rw-r--r--pw_bluetooth/README.md1
-rw-r--r--pw_bluetooth/address_test.cc55
-rw-r--r--pw_bluetooth/api_test.cc25
-rw-r--r--pw_bluetooth/docs.rst152
-rw-r--r--pw_bluetooth/emboss_test.cc28
-rw-r--r--pw_bluetooth/public/pw_bluetooth/address.h100
-rw-r--r--pw_bluetooth/public/pw_bluetooth/assigned_uuids.h692
-rw-r--r--pw_bluetooth/public/pw_bluetooth/constants.h22
-rw-r--r--pw_bluetooth/public/pw_bluetooth/controller.h164
-rw-r--r--pw_bluetooth/public/pw_bluetooth/gatt/client.h307
-rw-r--r--pw_bluetooth/public/pw_bluetooth/gatt/constants.h23
-rw-r--r--pw_bluetooth/public/pw_bluetooth/gatt/error.h136
-rw-r--r--pw_bluetooth/public/pw_bluetooth/gatt/server.h267
-rw-r--r--pw_bluetooth/public/pw_bluetooth/gatt/types.h140
-rw-r--r--pw_bluetooth/public/pw_bluetooth/hci.emb2570
-rw-r--r--pw_bluetooth/public/pw_bluetooth/host.h194
-rw-r--r--pw_bluetooth/public/pw_bluetooth/internal/hex.h46
-rw-r--r--pw_bluetooth/public/pw_bluetooth/internal/raii_ptr.h67
-rw-r--r--pw_bluetooth/public/pw_bluetooth/low_energy/advertising_data.h65
-rw-r--r--pw_bluetooth/public/pw_bluetooth/low_energy/bond_data.h78
-rw-r--r--pw_bluetooth/public/pw_bluetooth/low_energy/central.h255
-rw-r--r--pw_bluetooth/public/pw_bluetooth/low_energy/connection.h173
-rw-r--r--pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h160
-rw-r--r--pw_bluetooth/public/pw_bluetooth/low_energy/security_mode.h41
-rw-r--r--pw_bluetooth/public/pw_bluetooth/pairing_delegate.h119
-rw-r--r--pw_bluetooth/public/pw_bluetooth/peer.h41
-rw-r--r--pw_bluetooth/public/pw_bluetooth/result.h98
-rw-r--r--pw_bluetooth/public/pw_bluetooth/types.h101
-rw-r--r--pw_bluetooth/public/pw_bluetooth/uuid.h200
-rw-r--r--pw_bluetooth/public/pw_bluetooth/vendor.emb112
-rw-r--r--pw_bluetooth/public/pw_bluetooth/vendor.h40
-rw-r--r--pw_bluetooth/result_test.cc70
-rw-r--r--pw_bluetooth/size_report/BUILD.gn36
-rw-r--r--pw_bluetooth/size_report/make_2_views_and_write.cc33
-rw-r--r--pw_bluetooth/size_report/make_view_and_write.cc28
-rw-r--r--pw_bluetooth/uuid_test.cc110
-rw-r--r--pw_bluetooth_hci/BUILD.bazel3
-rw-r--r--pw_bluetooth_hci/BUILD.gn11
-rw-r--r--pw_bluetooth_hci/packet.cc39
-rw-r--r--pw_bluetooth_hci/packet_test.cc41
-rw-r--r--pw_bluetooth_hci/public/pw_bluetooth_hci/packet.h20
-rw-r--r--pw_bluetooth_hci/public/pw_bluetooth_hci/uart_transport.h3
-rw-r--r--pw_bluetooth_hci/uart_transport.cc6
-rw-r--r--pw_bluetooth_hci/uart_transport_fuzzer.cc47
-rw-r--r--pw_bluetooth_profiles/BUILD.bazel51
-rw-r--r--pw_bluetooth_profiles/BUILD.gn50
-rw-r--r--pw_bluetooth_profiles/CMakeLists.txt40
-rw-r--r--pw_bluetooth_profiles/device_info_service.cc67
-rw-r--r--pw_bluetooth_profiles/device_info_service_test.cc171
-rw-r--r--pw_bluetooth_profiles/docs.rst77
-rw-r--r--pw_bluetooth_profiles/public/pw_bluetooth_profiles/device_info_service.h291
-rw-r--r--pw_boot/BUILD.bazel11
-rw-r--r--pw_boot/BUILD.gn3
-rw-r--r--pw_boot/docs.rst108
-rw-r--r--pw_boot_cortex_m/BUILD.bazel4
-rw-r--r--pw_boot_cortex_m/BUILD.gn4
-rw-r--r--pw_boot_cortex_m/basic_cortex_m.ld119
-rw-r--r--pw_boot_cortex_m/bloaty_config.bloaty7
-rw-r--r--pw_boot_cortex_m/core_init.c2
-rw-r--r--pw_boot_cortex_m/docs.rst158
-rw-r--r--pw_build/BUILD.bazel7
-rw-r--r--pw_build/BUILD.gn136
-rw-r--r--pw_build/CMakeLists.txt98
-rw-r--r--pw_build/bazel_internal/linker_script.ld2
-rw-r--r--pw_build/bazel_internal/pigweed_internal.bzl82
-rw-r--r--pw_build/bazel_internal/py_proto_library.bzl30
-rw-r--r--pw_build/cc_blob_library.cmake171
-rw-r--r--pw_build/cc_blob_library.gni51
-rw-r--r--pw_build/cc_blob_library_test.cc50
-rw-r--r--pw_build/cc_executable.gni125
-rw-r--r--pw_build/cc_library.gni242
-rw-r--r--pw_build/constraints/board/BUILD.bazel5
-rw-r--r--pw_build/constraints/chipset/BUILD.bazel5
-rw-r--r--pw_build/constraints/rtos/BUILD.bazel8
-rw-r--r--pw_build/copy_from_cipd.gni6
-rw-r--r--pw_build/defaults.gni18
-rw-r--r--pw_build/docs.rst314
-rw-r--r--pw_build/evaluate_path_expressions.gni102
-rw-r--r--pw_build/exec.gni3
-rw-r--r--pw_build/generated_pigweed_modules_lists.gni125
-rw-r--r--pw_build/gn_internal/build_target.gni170
-rw-r--r--pw_build/hil.gni38
-rw-r--r--pw_build/host_tool.gni1
-rw-r--r--pw_build/linker_script.gni25
-rw-r--r--pw_build/pigweed.bzl83
-rw-r--r--pw_build/pigweed.cmake993
-rw-r--r--pw_build/pigweed_toolchain_upstream.bzl78
-rw-r--r--pw_build/platforms/BUILD.bazel39
-rw-r--r--pw_build/py/BUILD.gn13
-rw-r--r--pw_build/py/build_recipe_test.py114
-rw-r--r--pw_build/py/create_python_tree_test.py440
-rw-r--r--pw_build/py/file_prefix_map_test.py61
-rw-r--r--pw_build/py/generate_cc_blob_library_test.py270
-rw-r--r--pw_build/py/project_builder_prefs_test.py129
-rw-r--r--pw_build/py/pw_build/build_recipe.py530
-rw-r--r--pw_build/py/pw_build/collect_wheels.py26
-rwxr-xr-xpw_build/py/pw_build/copy_from_cipd.py88
-rw-r--r--pw_build/py/pw_build/create_gn_venv.py38
-rw-r--r--pw_build/py/pw_build/create_python_tree.py338
-rw-r--r--pw_build/py/pw_build/error.py20
-rw-r--r--pw_build/py/pw_build/exec.py27
-rw-r--r--pw_build/py/pw_build/file_prefix_map.py39
-rw-r--r--pw_build/py/pw_build/generate_cc_blob_library.py217
-rw-r--r--pw_build/py/pw_build/generate_modules_lists.py173
-rw-r--r--pw_build/py/pw_build/generate_python_package.py145
-rwxr-xr-xpw_build/py/pw_build/generate_python_package_gn.py8
-rw-r--r--pw_build/py/pw_build/generate_python_requirements.py135
-rw-r--r--pw_build/py/pw_build/generated_tests.py98
-rw-r--r--pw_build/py/pw_build/gn_resolver.py488
-rw-r--r--pw_build/py/pw_build/host_tool.py34
-rw-r--r--pw_build/py/pw_build/mirror_tree.py55
-rw-r--r--pw_build/py/pw_build/pip_install_python_deps.py122
-rw-r--r--pw_build/py/pw_build/project_builder.py953
-rw-r--r--pw_build/py/pw_build/project_builder_argparse.py166
-rw-r--r--pw_build/py/pw_build/project_builder_context.py466
-rw-r--r--pw_build/py/pw_build/project_builder_prefs.py190
-rw-r--r--pw_build/py/pw_build/python_package.py238
-rwxr-xr-xpw_build/py/pw_build/python_runner.py596
-rw-r--r--pw_build/py/pw_build/python_wheels.py8
-rw-r--r--pw_build/py/pw_build/wrap_ninja.py600
-rw-r--r--pw_build/py/pw_build/zip.py46
-rwxr-xr-xpw_build/py/python_runner_test.py245
-rw-r--r--pw_build/py/setup.cfg11
-rw-r--r--pw_build/py/zip_test.py101
-rw-r--r--pw_build/python.gni214
-rw-r--r--pw_build/python.rst336
-rw-r--r--pw_build/python_action.gni144
-rw-r--r--pw_build/python_dist.gni305
-rwxr-xr-xpw_build/python_dist/setup.sh21
-rw-r--r--pw_build/python_gn_args.gni24
-rw-r--r--pw_build/python_venv.gni251
-rw-r--r--pw_build/rust_executable.gni56
-rw-r--r--pw_build/rust_library.gni65
-rw-r--r--pw_build/selects.bzl7
-rw-r--r--pw_build/target_types.gni2
-rw-r--r--pw_build/test_blob_0123.binbin0 -> 4 bytes
-rw-r--r--pw_build/update_bundle.gni1
-rw-r--r--pw_build_info/BUILD.bazel21
-rw-r--r--pw_build_info/BUILD.gn32
-rw-r--r--pw_build_info/CMakeLists.txt20
-rw-r--r--pw_build_info/build_id.cc14
-rw-r--r--pw_build_info/build_id_test.cc30
-rw-r--r--pw_build_info/docs.rst97
-rw-r--r--pw_build_info/public/pw_build_info/build_id.h5
-rw-r--r--pw_build_info/py/BUILD.bazel41
-rw-r--r--pw_build_info/py/BUILD.gn16
-rw-r--r--pw_build_info/py/build_id_test.py48
-rw-r--r--pw_build_info/py/pw_build_info/build_id.py33
-rw-r--r--pw_build_info/py/pyproject.toml16
-rw-r--r--pw_build_info/py/setup.cfg30
-rw-r--r--pw_build_info/py/setup.py18
-rw-r--r--pw_build_mcuxpresso/BUILD.gn4
-rw-r--r--pw_build_mcuxpresso/py/BUILD.gn1
-rw-r--r--pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py25
-rw-r--r--pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py161
-rw-r--r--pw_build_mcuxpresso/py/tests/components_test.py200
-rw-r--r--pw_bytes/Android.bp36
-rw-r--r--pw_bytes/BUILD.bazel21
-rw-r--r--pw_bytes/BUILD.gn21
-rw-r--r--pw_bytes/CMakeLists.txt32
-rw-r--r--pw_bytes/bit_test.cc50
-rw-r--r--pw_bytes/byte_builder.cc4
-rw-r--r--pw_bytes/byte_builder_test.cc57
-rw-r--r--pw_bytes/docs.rst49
-rw-r--r--pw_bytes/endian_test.cc210
-rw-r--r--pw_bytes/public/pw_bytes/bit.h (renamed from pw_polyfill/standard_library_public/pw_polyfill/standard_library/bit.h)25
-rw-r--r--pw_bytes/public/pw_bytes/byte_builder.h85
-rw-r--r--pw_bytes/public/pw_bytes/endian.h50
-rw-r--r--pw_bytes/public/pw_bytes/span.h7
-rw-r--r--pw_bytes/public/pw_bytes/units.h24
-rw-r--r--pw_bytes/size_report/BUILD.bazel13
-rw-r--r--pw_bytes/size_report/byte_builder_size_report.cc10
-rw-r--r--pw_checksum/BUILD.bazel20
-rw-r--r--pw_checksum/BUILD.gn95
-rw-r--r--pw_checksum/CMakeLists.txt9
-rw-r--r--pw_checksum/config.gni17
-rw-r--r--pw_checksum/crc16_ccitt.cc3
-rw-r--r--pw_checksum/crc16_ccitt_perf_test.cc37
-rw-r--r--pw_checksum/crc16_ccitt_test.cc12
-rw-r--r--pw_checksum/crc32.cc117
-rw-r--r--pw_checksum/crc32_perf_test.cc58
-rw-r--r--pw_checksum/crc32_test.cc72
-rw-r--r--pw_checksum/docs.rst81
-rw-r--r--pw_checksum/public/pw_checksum/crc16_ccitt.h9
-rw-r--r--pw_checksum/public/pw_checksum/crc32.h46
-rw-r--r--pw_checksum/public/pw_checksum/internal/config.h29
-rw-r--r--pw_checksum/size_report/BUILD.bazel109
-rw-r--r--pw_checksum/size_report/BUILD.gn107
-rw-r--r--pw_checksum/size_report/run_checksum.cc119
-rw-r--r--pw_chrono/BUILD.bazel21
-rw-r--r--pw_chrono/BUILD.gn6
-rw-r--r--pw_chrono/CMakeLists.txt61
-rw-r--r--pw_chrono/backend.cmake22
-rw-r--r--pw_chrono/chrono.proto101
-rw-r--r--pw_chrono/docs.rst453
-rw-r--r--pw_chrono/public/pw_chrono/internal/system_clock_macros.h7
-rw-r--r--pw_chrono/public/pw_chrono/simulated_system_clock.h2
-rw-r--r--pw_chrono/public/pw_chrono/system_clock.h177
-rw-r--r--pw_chrono/public/pw_chrono/system_timer.h122
-rw-r--r--pw_chrono/py/BUILD.bazel37
-rw-r--r--pw_chrono/py/BUILD.gn36
-rw-r--r--pw_chrono/py/pw_chrono/__init__.py0
-rw-r--r--pw_chrono/py/pw_chrono/py.typed0
-rw-r--r--pw_chrono/py/pw_chrono/timestamp_analyzer.py66
-rw-r--r--pw_chrono/py/timestamp_analyzer_test.py110
-rw-r--r--pw_chrono/system_clock_facade_test.cc41
-rw-r--r--pw_chrono/system_clock_facade_test_c.c48
-rw-r--r--pw_chrono/system_timer_facade_test.cc14
-rw-r--r--pw_chrono_embos/BUILD.bazel4
-rw-r--r--pw_chrono_embos/BUILD.gn4
-rw-r--r--pw_chrono_freertos/BUILD.bazel10
-rw-r--r--pw_chrono_freertos/BUILD.gn4
-rw-r--r--pw_chrono_freertos/CMakeLists.txt14
-rw-r--r--pw_chrono_stl/BUILD.gn4
-rw-r--r--pw_chrono_stl/CMakeLists.txt10
-rw-r--r--pw_chrono_stl/public/pw_chrono_stl/system_clock_config.h2
-rw-r--r--pw_chrono_threadx/BUILD.bazel4
-rw-r--r--pw_chrono_threadx/BUILD.gn4
-rw-r--r--pw_chrono_zephyr/BUILD.gn4
-rw-r--r--pw_chrono_zephyr/CMakeLists.txt27
-rw-r--r--pw_chrono_zephyr/Kconfig9
-rw-r--r--pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_config.h2
-rw-r--r--pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h2
-rw-r--r--pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_inline.h2
-rw-r--r--pw_cli/BUILD.gn4
-rw-r--r--pw_cli/docs.rst15
-rw-r--r--pw_cli/py/BUILD.bazel10
-rw-r--r--pw_cli/py/BUILD.gn15
-rw-r--r--pw_cli/py/envparse_test.py44
-rw-r--r--pw_cli/py/plugins_test.py57
-rw-r--r--pw_cli/py/process_integration_test.py73
-rw-r--r--pw_cli/py/pw_cli/__main__.py2
-rw-r--r--pw_cli/py/pw_cli/argument_types.py3
-rw-r--r--pw_cli/py/pw_cli/arguments.py43
-rw-r--r--pw_cli/py/pw_cli/branding.py10
-rw-r--r--pw_cli/py/pw_cli/color.py9
-rw-r--r--pw_cli/py/pw_cli/env.py50
-rw-r--r--pw_cli/py/pw_cli/envparse.py87
-rw-r--r--pw_cli/py/pw_cli/log.py87
-rw-r--r--pw_cli/py/pw_cli/plugins.py187
-rw-r--r--pw_cli/py/pw_cli/process.py129
-rw-r--r--pw_cli/py/pw_cli/pw_command_plugins.py22
-rwxr-xr-xpw_cli/py/pw_cli/requires.py28
-rw-r--r--pw_cli/py/pw_cli/toml_config_loader_mixin.py50
-rw-r--r--pw_cli/py/pw_cli/yaml_config_loader_mixin.py (renamed from pw_console/py/pw_console/yaml_config_loader_mixin.py)36
-rw-r--r--pw_cli/py/setup.cfg4
-rw-r--r--pw_compilation_testing/BUILD.bazel28
-rw-r--r--pw_compilation_testing/BUILD.gn45
-rw-r--r--pw_compilation_testing/CMakeLists.txt23
-rw-r--r--pw_compilation_testing/OWNERS1
-rw-r--r--pw_compilation_testing/docs.rst195
-rw-r--r--pw_compilation_testing/negative_compilation_test.gni90
-rw-r--r--pw_compilation_testing/public/pw_compilation_testing/negative_compilation.h49
-rw-r--r--pw_compilation_testing/py/BUILD.gn34
-rw-r--r--pw_compilation_testing/py/generator_test.py122
-rw-r--r--pw_compilation_testing/py/pw_compilation_testing/__init__.py0
-rw-r--r--pw_compilation_testing/py/pw_compilation_testing/generator.py435
-rw-r--r--pw_compilation_testing/py/pw_compilation_testing/py.typed0
-rw-r--r--pw_compilation_testing/py/pw_compilation_testing/runner.py288
-rw-r--r--pw_compilation_testing/py/pyproject.toml16
-rw-r--r--pw_compilation_testing/py/setup.cfg26
-rw-r--r--pw_compilation_testing/py/setup.py18
-rw-r--r--pw_console/BUILD.gn12
-rw-r--r--pw_console/embedding.rst6
-rw-r--r--pw_console/images/2048_plugin1.svg1213
-rw-r--r--pw_console/images/calculator_plugin.pngbin9818 -> 0 bytes
-rw-r--r--pw_console/images/calculator_plugin.svg1385
-rw-r--r--pw_console/images/clock_plugin1.pngbin4694 -> 0 bytes
-rw-r--r--pw_console/images/clock_plugin1.svg378
-rw-r--r--pw_console/images/clock_plugin2.pngbin35290 -> 0 bytes
-rw-r--r--pw_console/images/clock_plugin2.svg1413
-rw-r--r--pw_console/plugins.rst48
-rw-r--r--pw_console/py/BUILD.bazel236
-rw-r--r--pw_console/py/BUILD.gn13
-rw-r--r--pw_console/py/command_runner_test.py150
-rw-r--r--pw_console/py/console_app_test.py39
-rw-r--r--pw_console/py/console_prefs_test.py69
-rw-r--r--pw_console/py/help_window_test.py46
-rw-r--r--pw_console/py/log_filter_test.py265
-rw-r--r--pw_console/py/log_store_test.py28
-rw-r--r--pw_console/py/log_view_test.py305
-rw-r--r--pw_console/py/pw_console/__main__.py130
-rw-r--r--pw_console/py/pw_console/command_runner.py126
-rw-r--r--pw_console/py/pw_console/console_app.py863
-rw-r--r--pw_console/py/pw_console/console_log_server.py78
-rw-r--r--pw_console/py/pw_console/console_prefs.py124
-rw-r--r--pw_console/py/pw_console/docs/__init__.py0
-rw-r--r--pw_console/py/pw_console/docs/user_guide.rst32
-rw-r--r--pw_console/py/pw_console/embed.py118
-rw-r--r--pw_console/py/pw_console/filter_toolbar.py64
-rw-r--r--pw_console/py/pw_console/help_window.py167
-rw-r--r--pw_console/py/pw_console/html/__init__.py0
-rw-r--r--pw_console/py/pw_console/html/index.html37
-rw-r--r--pw_console/py/pw_console/html/main.js261
-rw-r--r--pw_console/py/pw_console/html/style.css74
-rw-r--r--pw_console/py/pw_console/key_bindings.py74
-rw-r--r--pw_console/py/pw_console/log_filter.py38
-rw-r--r--pw_console/py/pw_console/log_line.py19
-rw-r--r--pw_console/py/pw_console/log_pane.py426
-rw-r--r--pw_console/py/pw_console/log_pane_saveas_dialog.py119
-rw-r--r--pw_console/py/pw_console/log_pane_selection_dialog.py111
-rw-r--r--pw_console/py/pw_console/log_pane_toolbars.py53
-rw-r--r--pw_console/py/pw_console/log_screen.py90
-rw-r--r--pw_console/py/pw_console/log_store.py83
-rw-r--r--pw_console/py/pw_console/log_view.py224
-rw-r--r--pw_console/py/pw_console/pigweed_code_style.py115
-rw-r--r--pw_console/py/pw_console/plugin_mixin.py4
-rw-r--r--pw_console/py/pw_console/plugins/bandwidth_toolbar.py48
-rw-r--r--pw_console/py/pw_console/plugins/calc_pane.py22
-rw-r--r--pw_console/py/pw_console/plugins/clock_pane.py336
-rw-r--r--pw_console/py/pw_console/plugins/twenty48_pane.py561
-rw-r--r--pw_console/py/pw_console/progress_bar/__init__.py23
-rw-r--r--pw_console/py/pw_console/progress_bar/progress_bar_impl.py30
-rw-r--r--pw_console/py/pw_console/progress_bar/progress_bar_state.py38
-rw-r--r--pw_console/py/pw_console/progress_bar/progress_bar_task_counter.py1
-rw-r--r--pw_console/py/pw_console/pw_ptpython_repl.py152
-rw-r--r--pw_console/py/pw_console/pyserial_wrapper.py153
-rw-r--r--pw_console/py/pw_console/python_logging.py122
-rw-r--r--pw_console/py/pw_console/quit_dialog.py50
-rw-r--r--pw_console/py/pw_console/repl_pane.py202
-rw-r--r--pw_console/py/pw_console/search_toolbar.py210
-rw-r--r--pw_console/py/pw_console/style.py169
-rw-r--r--pw_console/py/pw_console/templates/__init__.py0
-rw-r--r--pw_console/py/pw_console/templates/repl_output.jinja4
-rw-r--r--pw_console/py/pw_console/test_mode.py86
-rw-r--r--pw_console/py/pw_console/text_formatting.py32
-rw-r--r--pw_console/py/pw_console/widgets/__init__.py7
-rw-r--r--pw_console/py/pw_console/widgets/border.py73
-rw-r--r--pw_console/py/pw_console/widgets/checkbox.py33
-rw-r--r--pw_console/py/pw_console/widgets/event_count_history.py16
-rw-r--r--pw_console/py/pw_console/widgets/mouse_handlers.py1
-rw-r--r--pw_console/py/pw_console/widgets/table.py85
-rw-r--r--pw_console/py/pw_console/widgets/window_pane.py90
-rw-r--r--pw_console/py/pw_console/widgets/window_pane_toolbar.py107
-rw-r--r--pw_console/py/pw_console/window_list.py164
-rw-r--r--pw_console/py/pw_console/window_manager.py302
-rw-r--r--pw_console/py/repl_pane_test.py114
-rw-r--r--pw_console/py/setup.cfg17
-rw-r--r--pw_console/py/table_test.py303
-rw-r--r--pw_console/py/text_formatting_test.py392
-rw-r--r--pw_console/py/window_manager_test.py220
-rw-r--r--pw_console/testing.rst149
-rw-r--r--pw_containers/Android.bp35
-rw-r--r--pw_containers/BUILD.bazel39
-rw-r--r--pw_containers/BUILD.gn75
-rw-r--r--pw_containers/CMakeLists.txt62
-rw-r--r--pw_containers/algorithm_test.cc294
-rw-r--r--pw_containers/docs.rst274
-rw-r--r--pw_containers/filtered_view_test.cc6
-rw-r--r--pw_containers/flat_map_test.cc9
-rw-r--r--pw_containers/intrusive_list_test.cc34
-rw-r--r--pw_containers/public/pw_containers/algorithm.h366
-rw-r--r--pw_containers/public/pw_containers/filtered_view.h2
-rw-r--r--pw_containers/public/pw_containers/flat_map.h28
-rw-r--r--pw_containers/public/pw_containers/internal/algorithm_internal.h38
-rw-r--r--pw_containers/public/pw_containers/iterator.h34
-rw-r--r--pw_containers/public/pw_containers/to_array.h14
-rw-r--r--pw_containers/public/pw_containers/vector.h145
-rw-r--r--pw_containers/size_report/BUILD.bazel93
-rw-r--r--pw_containers/size_report/BUILD.gn80
-rw-r--r--pw_containers/size_report/base.cc19
-rw-r--r--pw_containers/size_report/base.h62
-rw-r--r--pw_containers/size_report/intrusive_list.cc38
-rw-r--r--pw_containers/size_report/linked_list.cc45
-rw-r--r--pw_containers/to_array_test.cc10
-rw-r--r--pw_containers/vector_test.cc289
-rw-r--r--pw_cpu_exception/BUILD.gn9
-rw-r--r--pw_cpu_exception/CMakeLists.txt15
-rw-r--r--pw_cpu_exception/backend.cmake21
-rw-r--r--pw_cpu_exception/backend.gni2
-rw-r--r--pw_cpu_exception/public/pw_cpu_exception/support.h7
-rw-r--r--pw_cpu_exception_cortex_m/BUILD.bazel42
-rw-r--r--pw_cpu_exception_cortex_m/BUILD.gn5
-rw-r--r--pw_cpu_exception_cortex_m/CMakeLists.txt44
-rw-r--r--pw_cpu_exception_cortex_m/exception_entry_test.cc28
-rw-r--r--pw_cpu_exception_cortex_m/public/pw_cpu_exception_cortex_m/snapshot.h6
-rw-r--r--pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h6
-rw-r--r--pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h2
-rw-r--r--pw_cpu_exception_cortex_m/py/BUILD.bazel51
-rw-r--r--pw_cpu_exception_cortex_m/py/BUILD.gn1
-rw-r--r--pw_cpu_exception_cortex_m/py/exception_analyzer_test.py220
-rw-r--r--pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/__init__.py4
-rw-r--r--pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cfsr_decoder.py9
-rw-r--r--pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cortex_m_constants.py357
-rw-r--r--pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py157
-rw-r--r--pw_cpu_exception_cortex_m/py/setup.cfg4
-rw-r--r--pw_cpu_exception_cortex_m/snapshot.cc19
-rw-r--r--pw_cpu_exception_cortex_m/support.cc10
-rw-r--r--pw_crypto/BUILD.bazel60
-rw-r--r--pw_crypto/BUILD.gn24
-rw-r--r--pw_crypto/docs.rst35
-rw-r--r--pw_crypto/ecdsa_boringssl.cc8
-rw-r--r--pw_crypto/ecdsa_test.cc2
-rw-r--r--pw_crypto/ecdsa_uecc.cc65
-rw-r--r--pw_crypto/public/pw_crypto/sha256_boringssl.h10
-rw-r--r--pw_crypto/sha256_mock.cc2
-rw-r--r--pw_crypto/sha256_mock_test.cc2
-rw-r--r--pw_crypto/sha256_test.cc2
-rw-r--r--pw_crypto/size_report/BUILD.bazel8
-rw-r--r--pw_crypto/size_report/ecdsa_p256_verify.cc2
-rw-r--r--pw_crypto/size_report/sha256_simple.cc2
-rw-r--r--pw_digital_io/BUILD.bazel48
-rw-r--r--pw_digital_io/BUILD.gn53
-rw-r--r--pw_digital_io/CMakeLists.txt (renamed from pw_web_ui/web_bundle.bzl)45
-rw-r--r--pw_digital_io/OWNERS1
-rw-r--r--pw_digital_io/README.md7
-rw-r--r--pw_digital_io/digital_io.cc60
-rw-r--r--pw_digital_io/digital_io_test.cc350
-rw-r--r--pw_digital_io/docs.rst284
-rw-r--r--pw_digital_io/public/pw_digital_io/digital_io.h541
-rw-r--r--pw_digital_io/public/pw_digital_io/internal/conversions.h148
-rw-r--r--pw_docgen/BUILD.gn4
-rw-r--r--pw_docgen/docs.gni21
-rw-r--r--pw_docgen/docs.rst48
-rw-r--r--pw_docgen/py/BUILD.gn2
-rw-r--r--pw_docgen/py/pw_docgen/docgen.py77
-rw-r--r--pw_docgen/py/pw_docgen/sphinx/google_analytics.py11
-rw-r--r--pw_docgen/py/pw_docgen/sphinx/module_metadata.py226
-rw-r--r--pw_docgen/py/setup.cfg7
-rw-r--r--pw_doctor/BUILD.gn4
-rw-r--r--pw_doctor/docs.rst9
-rw-r--r--pw_doctor/py/BUILD.gn6
-rwxr-xr-xpw_doctor/py/pw_doctor/doctor.py197
-rw-r--r--pw_env_setup/BUILD.gn222
-rw-r--r--pw_env_setup/bazel/cipd_setup/cipd_rules.bzl3
-rw-r--r--pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl4
-rw-r--r--pw_env_setup/bazel_only.json2
-rw-r--r--pw_env_setup/compatibility.json2
-rw-r--r--pw_env_setup/config.json9
-rw-r--r--pw_env_setup/docs.rst414
-rwxr-xr-xpw_env_setup/get_pw_env_setup.sh7
-rwxr-xr-xpw_env_setup/post-checkout-hook-helper.sh1
-rw-r--r--pw_env_setup/py/BUILD.gn2
-rw-r--r--pw_env_setup/py/environment_test.py106
-rw-r--r--pw_env_setup/py/json_visitor_test.py15
-rw-r--r--pw_env_setup/py/pw_env_setup/apply_visitor.py7
-rw-r--r--pw_env_setup/py/pw_env_setup/batch_visitor.py28
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version2
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests34
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/arm.json7
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json7
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/black.json16
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json17
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json20
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json17
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/default.json7
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/doxygen.json15
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/go.json28
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json16
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/kythe.json4
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/luci.json14
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json113
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/python.json15
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/rbe.json16
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/riscv.json15
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/testing.json14
-rwxr-xr-xpw_env_setup/py/pw_env_setup/cipd_setup/update.py138
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json15
-rw-r--r--pw_env_setup/py/pw_env_setup/cipd_setup/web.json17
-rwxr-xr-xpw_env_setup/py/pw_env_setup/cipd_setup/wrapper.py136
-rw-r--r--pw_env_setup/py/pw_env_setup/colors.py1
-rw-r--r--pw_env_setup/py/pw_env_setup/config_file.py43
-rwxr-xr-xpw_env_setup/py/pw_env_setup/env_setup.py317
-rw-r--r--pw_env_setup/py/pw_env_setup/environment.py60
-rw-r--r--pw_env_setup/py/pw_env_setup/gni_visitor.py52
-rw-r--r--pw_env_setup/py/pw_env_setup/json_visitor.py1
-rw-r--r--pw_env_setup/py/pw_env_setup/python_packages.py302
-rw-r--r--pw_env_setup/py/pw_env_setup/shell_visitor.py74
-rw-r--r--pw_env_setup/py/pw_env_setup/spinner.py1
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/__init__.py1
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/__main__.py77
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list73
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py121
-rw-r--r--pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt23
-rw-r--r--pw_env_setup/py/pw_env_setup/windows_env_start.py25
-rwxr-xr-xpw_env_setup/py/python_packages_test.py94
-rw-r--r--pw_env_setup/pypi_common_setup.cfg11
-rw-r--r--pw_env_setup/util.sh158
-rw-r--r--pw_file/BUILD.bazel30
-rw-r--r--pw_file/BUILD.gn1
-rw-r--r--pw_file/CMakeLists.txt6
-rw-r--r--pw_file/flat_file_system.cc26
-rw-r--r--pw_file/flat_file_system_test.cc20
-rw-r--r--pw_file/public/pw_file/flat_file_system.h34
-rw-r--r--pw_function/Android.bp27
-rw-r--r--pw_function/BUILD.bazel38
-rw-r--r--pw_function/BUILD.gn41
-rw-r--r--pw_function/CMakeLists.txt46
-rw-r--r--pw_function/docs.rst188
-rw-r--r--pw_function/function_test.cc131
-rw-r--r--pw_function/pointer_test.cc126
-rw-r--r--pw_function/public/pw_function/config.h8
-rw-r--r--pw_function/public/pw_function/function.h208
-rw-r--r--pw_function/public/pw_function/internal/function.h259
-rw-r--r--pw_function/public/pw_function/internal/static_invoker.h43
-rw-r--r--pw_function/public/pw_function/pointer.h97
-rw-r--r--pw_function/public/pw_function/scope_guard.h87
-rw-r--r--pw_function/scope_guard_test.cc88
-rw-r--r--pw_function/size_report/BUILD.bazel1
-rw-r--r--pw_fuzzer/BUILD.gn51
-rw-r--r--pw_fuzzer/docs.rst57
-rw-r--r--pw_fuzzer/examples/toy_fuzzer.cc66
-rw-r--r--pw_fuzzer/fuzzer.bzl11
-rw-r--r--pw_fuzzer/fuzzer.gni108
-rw-r--r--pw_fuzzer/pw_fuzzer_disabled.cc2
-rw-r--r--pw_hdlc/BUILD.bazel45
-rw-r--r--pw_hdlc/BUILD.gn64
-rw-r--r--pw_hdlc/CMakeLists.txt149
-rw-r--r--pw_hdlc/Kconfig6
-rw-r--r--pw_hdlc/decoder.cc4
-rw-r--r--pw_hdlc/decoder_test.cc2
-rw-r--r--pw_hdlc/docs.rst151
-rw-r--r--pw_hdlc/encoded_size_test.cc313
-rw-r--r--pw_hdlc/encoder.cc23
-rw-r--r--pw_hdlc/encoder_test.cc42
-rw-r--r--pw_hdlc/hdlc_sys_io_system_server.cc (renamed from targets/mimxrt595_evk/system_rpc_server.cc)24
-rw-r--r--pw_hdlc/java/main/dev/pigweed/pw_hdlc/BUILD.bazel32
-rw-r--r--pw_hdlc/java/main/dev/pigweed/pw_hdlc/CustomVarInt.java73
-rw-r--r--pw_hdlc/java/main/dev/pigweed/pw_hdlc/Decoder.java172
-rw-r--r--pw_hdlc/java/main/dev/pigweed/pw_hdlc/Encoder.java156
-rw-r--r--pw_hdlc/java/main/dev/pigweed/pw_hdlc/Frame.java69
-rw-r--r--pw_hdlc/java/main/dev/pigweed/pw_hdlc/Protocol.java27
-rw-r--r--pw_hdlc/java/test/dev/pigweed/pw_hdlc/BUILD.bazel72
-rw-r--r--pw_hdlc/java/test/dev/pigweed/pw_hdlc/DecoderTest.java88
-rw-r--r--pw_hdlc/java/test/dev/pigweed/pw_hdlc/EncoderTest.java328
-rw-r--r--pw_hdlc/java/test/dev/pigweed/pw_hdlc/FrameTest.java53
-rw-r--r--pw_hdlc/public/pw_hdlc/decoder.h28
-rw-r--r--pw_hdlc/public/pw_hdlc/encoded_size.h92
-rw-r--r--pw_hdlc/public/pw_hdlc/encoder.h4
-rw-r--r--pw_hdlc/public/pw_hdlc/internal/encoder.h9
-rw-r--r--pw_hdlc/public/pw_hdlc/internal/protocol.h31
-rw-r--r--pw_hdlc/public/pw_hdlc/rpc_channel.h35
-rw-r--r--pw_hdlc/public/pw_hdlc/rpc_packets.h3
-rw-r--r--pw_hdlc/py/BUILD.gn8
-rwxr-xr-xpw_hdlc/py/decode_test.py557
-rwxr-xr-xpw_hdlc/py/encode_test.py42
-rw-r--r--pw_hdlc/py/pw_hdlc/decode.py188
-rw-r--r--pw_hdlc/py/pw_hdlc/encode.py7
-rw-r--r--pw_hdlc/py/pw_hdlc/protocol.py2
-rw-r--r--pw_hdlc/py/pw_hdlc/rpc.py154
-rw-r--r--pw_hdlc/py/pw_hdlc/rpc_console.py285
-rw-r--r--pw_hdlc/py/setup.cfg6
-rw-r--r--pw_hdlc/rpc_channel_test.cc26
-rw-r--r--pw_hdlc/rpc_example/BUILD.bazel12
-rw-r--r--pw_hdlc/rpc_example/BUILD.gn8
-rw-r--r--pw_hdlc/rpc_example/CMakeLists.txt1
-rw-r--r--pw_hdlc/rpc_example/docs.rst6
-rwxr-xr-xpw_hdlc/rpc_example/example_script.py29
-rw-r--r--pw_hdlc/rpc_example/hdlc_rpc_server.cc2
-rw-r--r--pw_hdlc/rpc_packets.cc7
-rw-r--r--pw_hdlc/size_report/BUILD.bazel67
-rw-r--r--pw_hdlc/size_report/BUILD.gn57
-rw-r--r--pw_hdlc/size_report/hdlc_size_report.cc105
-rw-r--r--pw_hdlc/ts/BUILD.bazel68
-rw-r--r--pw_hdlc/ts/decoder_test.ts5
-rw-r--r--pw_hdlc/ts/encoder_test.ts7
-rw-r--r--pw_hdlc/ts/package.json15
-rw-r--r--pw_hdlc/ts/tsconfig.json34
-rw-r--r--pw_hdlc/ts/yarn.lock512
-rw-r--r--pw_hdlc/wire_packet_parser.cc2
-rw-r--r--pw_hex_dump/BUILD.gn1
-rw-r--r--pw_hex_dump/hex_dump.cc12
-rw-r--r--pw_hex_dump/hex_dump_test.cc61
-rw-r--r--pw_hex_dump/public/pw_hex_dump/hex_dump.h20
-rw-r--r--pw_i2c/BUILD.bazel4
-rw-r--r--pw_i2c/BUILD.gn13
-rw-r--r--pw_i2c/address_test.cc6
-rw-r--r--pw_i2c/device_test.cc93
-rw-r--r--pw_i2c/docs.rst7
-rw-r--r--pw_i2c/initiator_mock.cc6
-rw-r--r--pw_i2c/initiator_mock_test.cc29
-rw-r--r--pw_i2c/public/pw_i2c/initiator.h8
-rw-r--r--pw_i2c/public/pw_i2c/initiator_mock.h27
-rw-r--r--pw_i2c/public/pw_i2c/register_device.h62
-rw-r--r--pw_i2c/register_device.cc8
-rw-r--r--pw_i2c/register_device_test.cc114
-rw-r--r--pw_i2c_mcuxpresso/BUILD.bazel3
-rw-r--r--pw_i2c_mcuxpresso/BUILD.gn4
-rw-r--r--pw_i2c_mcuxpresso/OWNERS2
-rw-r--r--pw_ide/BUILD.gn25
-rw-r--r--pw_ide/README.md2
-rw-r--r--pw_ide/docs.rst195
-rw-r--r--pw_ide/py/BUILD.gn49
-rw-r--r--pw_ide/py/commands_test.py53
-rw-r--r--pw_ide/py/cpp_test.py983
-rw-r--r--pw_ide/py/editors_test.py342
-rw-r--r--pw_ide/py/pw_ide/__init__.py14
-rw-r--r--pw_ide/py/pw_ide/__main__.py27
-rw-r--r--pw_ide/py/pw_ide/activate.py489
-rw-r--r--pw_ide/py/pw_ide/cli.py435
-rw-r--r--pw_ide/py/pw_ide/commands.py845
-rw-r--r--pw_ide/py/pw_ide/cpp.py1082
-rw-r--r--pw_ide/py/pw_ide/editors.py626
-rw-r--r--pw_ide/py/pw_ide/exceptions.py34
-rw-r--r--pw_ide/py/pw_ide/py.typed0
-rw-r--r--pw_ide/py/pw_ide/python.py53
-rw-r--r--pw_ide/py/pw_ide/settings.py323
-rw-r--r--pw_ide/py/pw_ide/symlinks.py24
-rw-r--r--pw_ide/py/pw_ide/vscode.py337
-rw-r--r--pw_ide/py/pyproject.toml16
-rw-r--r--pw_ide/py/setup.cfg30
-rw-r--r--pw_ide/py/setup.py18
-rw-r--r--pw_ide/py/test_cases.py175
-rw-r--r--pw_ide/py/vscode_test.py79
-rw-r--r--pw_interrupt/BUILD.gn4
-rw-r--r--pw_interrupt/CMakeLists.txt5
-rw-r--r--pw_interrupt/backend.cmake19
-rw-r--r--pw_interrupt/backend.gni2
-rw-r--r--pw_interrupt_cortex_m/BUILD.gn4
-rw-r--r--pw_interrupt_cortex_m/CMakeLists.txt4
-rw-r--r--pw_interrupt_zephyr/BUILD.gn4
-rw-r--r--pw_interrupt_zephyr/CMakeLists.txt19
-rw-r--r--pw_interrupt_zephyr/Kconfig4
-rw-r--r--pw_interrupt_zephyr/public/pw_interrupt_zephyr/context_inline.h2
-rw-r--r--pw_intrusive_ptr/BUILD.bazel61
-rw-r--r--pw_intrusive_ptr/BUILD.gn64
-rw-r--r--pw_intrusive_ptr/CMakeLists.txt37
-rw-r--r--pw_intrusive_ptr/OWNERS1
-rw-r--r--pw_intrusive_ptr/docs.rst83
-rw-r--r--pw_intrusive_ptr/intrusive_ptr_test.cc459
-rw-r--r--pw_intrusive_ptr/public/pw_intrusive_ptr/internal/ref_counted_base.h50
-rw-r--r--pw_intrusive_ptr/public/pw_intrusive_ptr/intrusive_ptr.h209
-rw-r--r--pw_intrusive_ptr/public/pw_intrusive_ptr/recyclable.h135
-rw-r--r--pw_intrusive_ptr/recyclable_test.cc89
-rw-r--r--pw_intrusive_ptr/ref_counted_base.cc61
-rw-r--r--pw_kvs/BUILD.bazel32
-rw-r--r--pw_kvs/BUILD.gn18
-rw-r--r--pw_kvs/CMakeLists.txt610
-rw-r--r--pw_kvs/alignment.cc2
-rw-r--r--pw_kvs/alignment_test.cc40
-rw-r--r--pw_kvs/checksum.cc2
-rw-r--r--pw_kvs/checksum_test.cc21
-rw-r--r--pw_kvs/converts_to_span_test.cc43
-rw-r--r--pw_kvs/docs.rst61
-rw-r--r--pw_kvs/entry.cc36
-rw-r--r--pw_kvs/entry_cache.cc44
-rw-r--r--pw_kvs/entry_test.cc35
-rw-r--r--pw_kvs/fake_flash_memory.cc7
-rw-r--r--pw_kvs/fake_flash_test_key_value_store.cc2
-rw-r--r--pw_kvs/flash_memory.cc21
-rw-r--r--pw_kvs/flash_partition_stream_test.cc28
-rw-r--r--pw_kvs/flash_partition_test.cc14
-rw-r--r--pw_kvs/key_value_store.cc181
-rw-r--r--pw_kvs/key_value_store_binary_format_test.cc111
-rw-r--r--pw_kvs/key_value_store_fuzz_test.cc121
-rw-r--r--pw_kvs/key_value_store_initialized_test.cc87
-rw-r--r--pw_kvs/key_value_store_map_test.cc31
-rw-r--r--pw_kvs/key_value_store_put_test.cc13
-rw-r--r--pw_kvs/key_value_store_test.cc83
-rw-r--r--pw_kvs/key_value_store_wear_test.cc11
-rw-r--r--pw_kvs/public/pw_kvs/alignment.h32
-rw-r--r--pw_kvs/public/pw_kvs/checksum.h37
-rw-r--r--pw_kvs/public/pw_kvs/crc16_checksum.h8
-rw-r--r--pw_kvs/public/pw_kvs/fake_flash_memory.h17
-rw-r--r--pw_kvs/public/pw_kvs/flash_memory.h72
-rw-r--r--pw_kvs/public/pw_kvs/flash_partition_with_stats.h20
-rw-r--r--pw_kvs/public/pw_kvs/format.h61
-rw-r--r--pw_kvs/public/pw_kvs/internal/entry.h24
-rw-r--r--pw_kvs/public/pw_kvs/internal/entry_cache.h34
-rw-r--r--pw_kvs/public/pw_kvs/internal/key_descriptor.h2
-rw-r--r--pw_kvs/public/pw_kvs/internal/sectors.h20
-rw-r--r--pw_kvs/public/pw_kvs/internal/span_traits.h17
-rw-r--r--pw_kvs/public/pw_kvs/io.h27
-rw-r--r--pw_kvs/public/pw_kvs/key.h18
-rw-r--r--pw_kvs/public/pw_kvs/key_value_store.h78
-rw-r--r--pw_kvs/pw_kvs_private/config.h8
-rw-r--r--pw_kvs/sectors.cc16
-rw-r--r--pw_kvs/sectors_test.cc2
-rw-r--r--pw_kvs/size_report/BUILD.gn8
-rw-r--r--pw_kvs/size_report/base_with_only_flash.cc12
-rw-r--r--pw_kvs/size_report/with_kvs.cc6
-rw-r--r--pw_libc/BUILD.bazel1
-rw-r--r--pw_libc/BUILD.gn1
-rw-r--r--pw_libc/memset_test.cc7
-rw-r--r--pw_log/Android.bp16
-rw-r--r--pw_log/BUILD.bazel18
-rw-r--r--pw_log/BUILD.gn2
-rw-r--r--pw_log/CMakeLists.txt22
-rw-r--r--pw_log/backend.cmake19
-rw-r--r--pw_log/basic_log_test.cc93
-rw-r--r--pw_log/basic_log_test_plain_c.c72
-rw-r--r--pw_log/docs.rst23
-rw-r--r--pw_log/glog_adapter_test.cc2
-rw-r--r--pw_log/log.proto23
-rw-r--r--pw_log/proto_utils.cc21
-rw-r--r--pw_log/proto_utils_test.cc61
-rw-r--r--pw_log/protobuf.rst9
-rw-r--r--pw_log/public/pw_log/config.h2
-rw-r--r--pw_log/public/pw_log/internal/glog_adapter.h17
-rw-r--r--pw_log/public/pw_log/log.h24
-rw-r--r--pw_log/public/pw_log/options.h5
-rw-r--r--pw_log/public/pw_log/proto_utils.h19
-rw-r--r--pw_log_android/Android.bp13
-rw-r--r--pw_log_android/BUILD.gn11
-rw-r--r--pw_log_android/OWNERS (renamed from third_party/embos/OWNERS)0
-rw-r--r--pw_log_android/docs.rst9
-rw-r--r--pw_log_android/public/pw_log_android/log_android.h6
-rw-r--r--pw_log_basic/BUILD.gn4
-rw-r--r--pw_log_basic/CMakeLists.txt16
-rw-r--r--pw_log_basic/docs.rst8
-rw-r--r--pw_log_basic/log_basic.cc4
-rw-r--r--pw_log_basic/public/pw_log_basic/log_basic.h21
-rw-r--r--pw_log_basic/pw_log_basic_private/config.h7
-rw-r--r--pw_log_null/Android.bp28
-rw-r--r--pw_log_null/BUILD.bazel13
-rw-r--r--pw_log_null/CMakeLists.txt21
-rw-r--r--pw_log_null/public/pw_log_null/log_null.h13
-rw-r--r--pw_log_null/test.cc27
-rw-r--r--pw_log_null/test_c.c19
-rw-r--r--pw_log_rpc/BUILD.bazel8
-rw-r--r--pw_log_rpc/BUILD.gn19
-rw-r--r--pw_log_rpc/CMakeLists.txt233
-rw-r--r--pw_log_rpc/docs.rst17
-rw-r--r--pw_log_rpc/log_filter.cc63
-rw-r--r--pw_log_rpc/log_filter_service.cc25
-rw-r--r--pw_log_rpc/log_filter_service_test.cc141
-rw-r--r--pw_log_rpc/log_filter_test.cc311
-rw-r--r--pw_log_rpc/log_service_test.cc252
-rw-r--r--pw_log_rpc/public/pw_log_rpc/internal/config.h13
-rw-r--r--pw_log_rpc/public/pw_log_rpc/log_filter.h15
-rw-r--r--pw_log_rpc/public/pw_log_rpc/log_filter_map.h8
-rw-r--r--pw_log_rpc/public/pw_log_rpc/log_filter_service.h27
-rw-r--r--pw_log_rpc/public/pw_log_rpc/rpc_log_drain.h52
-rw-r--r--pw_log_rpc/public/pw_log_rpc/rpc_log_drain_map.h9
-rw-r--r--pw_log_rpc/public/pw_log_rpc/rpc_log_drain_thread.h6
-rw-r--r--pw_log_rpc/rpc_log_drain.cc27
-rw-r--r--pw_log_rpc/rpc_log_drain_test.cc30
-rw-r--r--pw_log_rpc/test_utils.cc46
-rw-r--r--pw_log_string/BUILD.bazel8
-rw-r--r--pw_log_string/BUILD.gn4
-rw-r--r--pw_log_string/CMakeLists.txt9
-rw-r--r--pw_log_string/backend.cmake20
-rw-r--r--pw_log_string/public/pw_log_string/log_string.h4
-rw-r--r--pw_log_tokenized/BUILD.bazel41
-rw-r--r--pw_log_tokenized/BUILD.gn95
-rw-r--r--pw_log_tokenized/CMakeLists.txt61
-rw-r--r--pw_log_tokenized/backend.cmake19
-rw-r--r--pw_log_tokenized/backend.gni18
-rw-r--r--pw_log_tokenized/base64_over_hdlc.cc13
-rw-r--r--pw_log_tokenized/compatibility.cc60
-rw-r--r--pw_log_tokenized/docs.rst77
-rw-r--r--pw_log_tokenized/log_tokenized.cc (renamed from pw_tokenizer/tokenize_to_global_handler_with_payload.cc)23
-rw-r--r--pw_log_tokenized/log_tokenized_test.cc45
-rw-r--r--pw_log_tokenized/log_tokenized_test_c.c16
-rw-r--r--pw_log_tokenized/metadata_test.cc49
-rw-r--r--pw_log_tokenized/public/pw_log_tokenized/config.h12
-rw-r--r--pw_log_tokenized/public/pw_log_tokenized/handler.h31
-rw-r--r--pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h53
-rw-r--r--pw_log_tokenized/public/pw_log_tokenized/metadata.h76
-rw-r--r--pw_log_tokenized/py/BUILD.bazel34
-rw-r--r--pw_log_tokenized/py/BUILD.gn1
-rw-r--r--pw_log_tokenized/py/format_string_test.py4
-rw-r--r--pw_log_tokenized/py/metadata_test.py21
-rw-r--r--pw_log_tokenized/py/pw_log_tokenized/__init__.py49
-rw-r--r--pw_log_zephyr/BUILD.gn11
-rw-r--r--pw_log_zephyr/CMakeLists.txt41
-rw-r--r--pw_log_zephyr/Kconfig34
-rw-r--r--pw_log_zephyr/docs.rst30
-rw-r--r--pw_log_zephyr/log_zephyr.cc2
-rw-r--r--pw_log_zephyr/public/pw_log_zephyr/log_zephyr.h52
-rw-r--r--pw_log_zephyr/public_overrides/zephyr_custom_log.h35
-rw-r--r--pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc31
-rw-r--r--pw_malloc/BUILD.gn4
-rw-r--r--pw_malloc_freelist/BUILD.bazel14
-rw-r--r--pw_malloc_freelist/BUILD.gn2
-rw-r--r--pw_malloc_freelist/freelist_malloc.cc9
-rw-r--r--pw_malloc_freelist/freelist_malloc_test.cc2
-rw-r--r--pw_metric/BUILD.bazel79
-rw-r--r--pw_metric/BUILD.gn58
-rw-r--r--pw_metric/CMakeLists.txt64
-rw-r--r--pw_metric/docs.rst24
-rw-r--r--pw_metric/metric.cc8
-rw-r--r--pw_metric/metric_service_nanopb.cc92
-rw-r--r--pw_metric/metric_service_nanopb_test.cc2
-rw-r--r--pw_metric/metric_service_pwpb.cc123
-rw-r--r--pw_metric/metric_service_pwpb_test.cc218
-rw-r--r--pw_metric/metric_test.cc2
-rw-r--r--pw_metric/public/pw_metric/metric.h4
-rw-r--r--pw_metric/public/pw_metric/metric_service_nanopb.h8
-rw-r--r--pw_metric/public/pw_metric/metric_service_pwpb.h50
-rw-r--r--pw_metric/pw_metric_private/metric_walker.h75
-rw-r--r--pw_metric/pw_metric_proto/metric_service.options4
-rw-r--r--pw_metric/pw_metric_proto/metric_service.proto2
-rw-r--r--pw_metric/py/BUILD.bazel (renamed from pw_status/ts/BUILD.bazel)36
-rw-r--r--pw_metric/py/BUILD.gn38
-rw-r--r--pw_metric/py/metric_parser_test.py297
-rw-r--r--pw_metric/py/pw_metric/__init__.py13
-rw-r--r--pw_metric/py/pw_metric/metric_parser.py80
-rw-r--r--pw_minimal_cpp_stdlib/BUILD.bazel6
-rw-r--r--pw_minimal_cpp_stdlib/BUILD.gn18
-rw-r--r--pw_minimal_cpp_stdlib/docs.rst30
-rw-r--r--pw_minimal_cpp_stdlib/isolated_test.cc70
-rw-r--r--pw_minimal_cpp_stdlib/public/internal/iterator.h10
-rw-r--r--pw_minimal_cpp_stdlib/public/internal/type_traits.h101
-rw-r--r--pw_minimal_cpp_stdlib/public/internal/utility.h22
-rw-r--r--pw_module/BUILD.gn4
-rw-r--r--pw_module/docs.rst23
-rw-r--r--pw_module/py/BUILD.gn4
-rw-r--r--pw_module/py/check_test.py7
-rw-r--r--pw_module/py/pw_module/__main__.py44
-rw-r--r--pw_module/py/pw_module/check.py61
-rw-r--r--pw_module/py/pw_module/create.py940
-rw-r--r--pw_multisink/CMakeLists.txt23
-rw-r--r--pw_multisink/multisink.cc33
-rw-r--r--pw_multisink/multisink_test.cc43
-rw-r--r--pw_multisink/multisink_threaded_test.cc13
-rw-r--r--pw_multisink/public/pw_multisink/multisink.h22
-rw-r--r--pw_multisink/util.cc6
-rw-r--r--pw_package/BUILD.gn4
-rw-r--r--pw_package/docs.rst6
-rw-r--r--pw_package/py/BUILD.gn2
-rw-r--r--pw_package/py/pw_package/git_repo.py50
-rw-r--r--pw_package/py/pw_package/package_manager.py55
-rw-r--r--pw_package/py/pw_package/packages/arduino_core.py47
-rw-r--r--pw_package/py/pw_package/packages/boringssl.py15
-rw-r--r--pw_package/py/pw_package/packages/chromium_verifier.py50
-rw-r--r--pw_package/py/pw_package/packages/crlset.py15
-rw-r--r--pw_package/py/pw_package/packages/emboss.py48
-rw-r--r--pw_package/py/pw_package/packages/freertos.py13
-rw-r--r--pw_package/py/pw_package/packages/googletest.py13
-rw-r--r--pw_package/py/pw_package/packages/mbedtls.py21
-rw-r--r--pw_package/py/pw_package/packages/micro_ecc.py21
-rw-r--r--pw_package/py/pw_package/packages/nanopb.py9
-rw-r--r--pw_package/py/pw_package/packages/pico_sdk.py40
-rw-r--r--pw_package/py/pw_package/packages/protobuf.py13
-rw-r--r--pw_package/py/pw_package/packages/smartfusion_mss.py13
-rw-r--r--pw_package/py/pw_package/packages/stm32cube.py81
-rw-r--r--pw_package/py/pw_package/pigweed_packages.py29
-rw-r--r--pw_perf_test/BUILD.bazel212
-rw-r--r--pw_perf_test/BUILD.gn187
-rw-r--r--pw_perf_test/CMakeLists.txt151
-rw-r--r--pw_perf_test/arm_cortex_cyccnt_public_overrides/pw_perf_test_timer_backend/timer.h16
-rw-r--r--pw_perf_test/backend.cmake18
-rw-r--r--pw_perf_test/chrono_public_overrides/pw_perf_test_timer_backend/timer.h16
-rw-r--r--pw_perf_test/chrono_test.cc37
-rw-r--r--pw_perf_test/docs.rst325
-rw-r--r--pw_perf_test/log_perf_handler.cc60
-rw-r--r--pw_perf_test/log_perf_handler_main.cc22
-rw-r--r--pw_perf_test/perf_test.cc108
-rw-r--r--pw_perf_test/perf_test.gni84
-rw-r--r--pw_perf_test/perf_test_test.cc44
-rw-r--r--pw_perf_test/performance_test_generic.cc50
-rw-r--r--pw_perf_test/public/pw_perf_test/event_handler.h67
-rw-r--r--pw_perf_test/public/pw_perf_test/googletest_style_event_handler.h27
-rw-r--r--pw_perf_test/public/pw_perf_test/internal/chrono_timer_interface.h39
-rw-r--r--pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h62
-rw-r--r--pw_perf_test/public/pw_perf_test/internal/duration_unit.h24
-rw-r--r--pw_perf_test/public/pw_perf_test/internal/timer.h53
-rw-r--r--pw_perf_test/public/pw_perf_test/log_perf_handler.h33
-rw-r--r--pw_perf_test/public/pw_perf_test/perf_test.h178
-rw-r--r--pw_perf_test/state_test.cc61
-rw-r--r--pw_perf_test/timer_test.cc32
-rw-r--r--pw_persistent_ram/BUILD.bazel4
-rw-r--r--pw_persistent_ram/BUILD.gn7
-rw-r--r--pw_persistent_ram/CMakeLists.txt9
-rw-r--r--pw_persistent_ram/persistent_buffer.cc2
-rw-r--r--pw_persistent_ram/persistent_buffer_test.cc26
-rw-r--r--pw_persistent_ram/persistent_test.cc6
-rw-r--r--pw_persistent_ram/public/pw_persistent_ram/persistent.h6
-rw-r--r--pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h2
-rw-r--r--pw_persistent_ram/size_report/BUILD.bazel22
-rw-r--r--pw_persistent_ram/size_report/persistent_base_with_crc16.cc2
-rw-r--r--pw_polyfill/Android.bp24
-rw-r--r--pw_polyfill/BUILD.bazel101
-rw-r--r--pw_polyfill/BUILD.gn89
-rw-r--r--pw_polyfill/CMakeLists.txt72
-rw-r--r--pw_polyfill/cstddef_public_overrides/cstddef22
-rw-r--r--pw_polyfill/docs.rst156
-rw-r--r--pw_polyfill/iterator_public_overrides/iterator22
-rw-r--r--pw_polyfill/public/pw_polyfill/language_feature_macros.h2
-rw-r--r--pw_polyfill/public/pw_polyfill/standard.h1
-rw-r--r--pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h4
-rw-r--r--pw_polyfill/standard_library_public/pw_polyfill/standard_library/type_traits.h51
-rw-r--r--pw_polyfill/test.cc81
-rw-r--r--pw_preprocessor/Android.bp24
-rw-r--r--pw_preprocessor/CMakeLists.txt8
-rw-r--r--pw_preprocessor/arguments_test.cc2
-rw-r--r--pw_preprocessor/boolean_test.cc2
-rw-r--r--pw_preprocessor/concat_test.cc2
-rw-r--r--pw_preprocessor/public/pw_preprocessor/arch.h4
-rw-r--r--pw_preprocessor/public/pw_preprocessor/compiler.h2
-rw-r--r--pw_preprocessor/util_test.cc2
-rw-r--r--pw_presubmit/BUILD.gn4
-rw-r--r--pw_presubmit/docs.rst343
-rw-r--r--pw_presubmit/py/BUILD.gn21
-rw-r--r--pw_presubmit/py/bazel_parser_test.py117
-rwxr-xr-xpw_presubmit/py/git_repo_test.py102
-rw-r--r--pw_presubmit/py/gitmodules_test.py190
-rw-r--r--pw_presubmit/py/keep_sorted_test.py357
-rw-r--r--pw_presubmit/py/ninja_parser_test.py102
-rw-r--r--pw_presubmit/py/owners_checks_test.py448
-rwxr-xr-xpw_presubmit/py/presubmit_test.py128
-rw-r--r--pw_presubmit/py/pw_presubmit/bazel_parser.py63
-rw-r--r--pw_presubmit/py/pw_presubmit/build.py558
-rw-r--r--pw_presubmit/py/pw_presubmit/cli.py231
-rw-r--r--pw_presubmit/py/pw_presubmit/cpp_checks.py39
-rwxr-xr-xpw_presubmit/py/pw_presubmit/format_code.py677
-rw-r--r--pw_presubmit/py/pw_presubmit/git_repo.py286
-rw-r--r--pw_presubmit/py/pw_presubmit/gitmodules.py192
-rw-r--r--pw_presubmit/py/pw_presubmit/inclusive_language.py45
-rwxr-xr-xpw_presubmit/py/pw_presubmit/install_hook.py107
-rw-r--r--pw_presubmit/py/pw_presubmit/keep_sorted.py497
-rw-r--r--pw_presubmit/py/pw_presubmit/module_owners.py82
-rw-r--r--pw_presubmit/py/pw_presubmit/ninja_parser.py64
-rw-r--r--pw_presubmit/py/pw_presubmit/npm_presubmit.py22
-rw-r--r--pw_presubmit/py/pw_presubmit/owners_checks.py461
-rwxr-xr-xpw_presubmit/py/pw_presubmit/pigweed_presubmit.py1256
-rw-r--r--pw_presubmit/py/pw_presubmit/presubmit.py1227
-rw-r--r--pw_presubmit/py/pw_presubmit/python_checks.py75
-rw-r--r--pw_presubmit/py/pw_presubmit/shell_checks.py42
-rw-r--r--pw_presubmit/py/pw_presubmit/source_in_build.py168
-rw-r--r--pw_presubmit/py/pw_presubmit/todo_check.py108
-rw-r--r--pw_presubmit/py/pw_presubmit/tools.py105
-rw-r--r--pw_presubmit/py/setup.cfg6
-rw-r--r--pw_presubmit/py/todo_check_test.py156
-rwxr-xr-xpw_presubmit/py/tools_test.py37
-rw-r--r--pw_protobuf/Android.bp107
-rw-r--r--pw_protobuf/BUILD.bazel119
-rw-r--r--pw_protobuf/BUILD.gn110
-rw-r--r--pw_protobuf/CMakeLists.txt79
-rw-r--r--pw_protobuf/codegen_decoder_test.cc245
-rw-r--r--pw_protobuf/codegen_encoder_test.cc277
-rw-r--r--pw_protobuf/codegen_message_test.cc2027
-rw-r--r--pw_protobuf/decoder.cc8
-rw-r--r--pw_protobuf/decoder_fuzzer.cc219
-rw-r--r--pw_protobuf/decoder_test.cc43
-rw-r--r--pw_protobuf/docs.rst2154
-rw-r--r--pw_protobuf/encoder.cc353
-rw-r--r--pw_protobuf/encoder_fuzzer.cc173
-rw-r--r--pw_protobuf/encoder_perf_test.cc38
-rw-r--r--pw_protobuf/encoder_test.cc79
-rw-r--r--pw_protobuf/find.cc2
-rw-r--r--pw_protobuf/find_test.cc12
-rw-r--r--pw_protobuf/fuzz.h54
-rw-r--r--pw_protobuf/map_utils_test.cc12
-rw-r--r--pw_protobuf/message.cc2
-rw-r--r--pw_protobuf/message_test.cc30
-rw-r--r--pw_protobuf/public/pw_protobuf/bytes_utils.h4
-rw-r--r--pw_protobuf/public/pw_protobuf/decoder.h26
-rw-r--r--pw_protobuf/public/pw_protobuf/encoder.h319
-rw-r--r--pw_protobuf/public/pw_protobuf/internal/codegen.h245
-rw-r--r--pw_protobuf/public/pw_protobuf/internal/proto_integer_base.h2
-rw-r--r--pw_protobuf/public/pw_protobuf/message.h8
-rw-r--r--pw_protobuf/public/pw_protobuf/serialized_size.h28
-rw-r--r--pw_protobuf/public/pw_protobuf/stream_decoder.h188
-rw-r--r--pw_protobuf/pw_protobuf_codegen_protos/codegen_options.proto34
-rw-r--r--pw_protobuf/pw_protobuf_protos/field_options.proto37
-rw-r--r--pw_protobuf/pw_protobuf_test_deps_protos/imported.options15
-rw-r--r--pw_protobuf/pw_protobuf_test_deps_protos/imported.proto20
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/full_test.options26
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/full_test.proto86
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/imported.options15
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/imported.proto4
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/importer.proto8
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/optional.options16
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/optional.proto25
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/repeated.options19
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/repeated.proto8
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/size_report.options15
-rw-r--r--pw_protobuf/pw_protobuf_test_protos/size_report.proto57
-rw-r--r--pw_protobuf/py/Android.bp44
-rw-r--r--pw_protobuf/py/BUILD.bazel8
-rw-r--r--pw_protobuf/py/BUILD.gn9
-rw-r--r--pw_protobuf/py/pw_protobuf/codegen_pwpb.py1638
-rw-r--r--pw_protobuf/py/pw_protobuf/options.py114
-rw-r--r--pw_protobuf/py/pw_protobuf/output_file.py1
-rwxr-xr-xpw_protobuf/py/pw_protobuf/plugin.py70
-rw-r--r--pw_protobuf/py/pw_protobuf/proto_tree.py407
-rwxr-xr-xpw_protobuf/py/pw_protobuf/symbol_name_mapping.py613
-rw-r--r--pw_protobuf/py/setup.cfg5
-rw-r--r--pw_protobuf/size_report.rst62
-rw-r--r--pw_protobuf/size_report/BUILD.bazel161
-rw-r--r--pw_protobuf/size_report/BUILD.gn188
-rw-r--r--pw_protobuf/size_report/decoder_incremental.cc2
-rw-r--r--pw_protobuf/size_report/decoder_partial.cc (renamed from pw_protobuf/size_report/decoder_full.cc)2
-rw-r--r--pw_protobuf/size_report/encode_decode_core.cc25
-rw-r--r--pw_protobuf/size_report/message_core.cc27
-rw-r--r--pw_protobuf/size_report/oneof_codegen_comparison.cc390
-rw-r--r--pw_protobuf/size_report/proto_base.cc (renamed from pw_assert_zephyr/assert_zephyr.cc)15
-rw-r--r--pw_protobuf/size_report/proto_bloat.cc350
-rw-r--r--pw_protobuf/size_report/proto_bloat.h45
-rw-r--r--pw_protobuf/size_report/simple_codegen_comparison.cc181
-rw-r--r--pw_protobuf/stream_decoder.cc270
-rw-r--r--pw_protobuf/stream_decoder_test.cc364
-rw-r--r--pw_protobuf_compiler/BUILD.bazel24
-rw-r--r--pw_protobuf_compiler/BUILD.gn28
-rw-r--r--pw_protobuf_compiler/CMakeLists.txt18
-rw-r--r--pw_protobuf_compiler/docs.rst141
-rw-r--r--pw_protobuf_compiler/nested_packages_test.cc72
-rw-r--r--pw_protobuf_compiler/proto.bzl10
-rw-r--r--pw_protobuf_compiler/proto.cmake435
-rw-r--r--pw_protobuf_compiler/proto.gni156
-rw-r--r--pw_protobuf_compiler/pw_nanopb_cc_library.bzl2
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/BUILD.bazel56
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/BUILD.gn39
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/aggregate.proto30
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/aggregate_wrapper.proto28
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel35
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.gn (renamed from pw_fuzzer/oss_fuzz.gni)20
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/id/id.proto24
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/id/internal/id_internal.proto (renamed from pw_polyfill/public_overrides/iterator)10
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel50
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.gn33
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/thing/thing.proto (renamed from pw_web_ui/src/frontend/index.tsx)16
-rw-r--r--pw_protobuf_compiler/pw_nested_packages/data_type/thing/type_of_thing.proto (renamed from pw_polyfill/public_overrides/bit)15
-rw-r--r--pw_protobuf_compiler/pw_proto_library.bzl357
-rw-r--r--pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel50
-rw-r--r--pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/pwpb_test.options21
-rw-r--r--pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/pwpb_test.proto37
-rw-r--r--pw_protobuf_compiler/pwpb_test.cc61
-rw-r--r--pw_protobuf_compiler/py/Android.bp32
-rw-r--r--pw_protobuf_compiler/py/BUILD.gn1
-rw-r--r--pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py201
-rw-r--r--pw_protobuf_compiler/py/pw_protobuf_compiler/proto_target_invalid.py30
-rw-r--r--pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py135
-rwxr-xr-xpw_protobuf_compiler/py/python_protos_test.py170
-rw-r--r--pw_protobuf_compiler/py/setup.cfg14
-rw-r--r--pw_protobuf_compiler/toolchain.gni6
-rw-r--r--pw_protobuf_compiler/ts/BUILD.bazel70
-rw-r--r--pw_protobuf_compiler/ts/build.ts98
-rw-r--r--pw_protobuf_compiler/ts/codegen/template_replacement.bzl61
-rw-r--r--pw_protobuf_compiler/ts/codegen/template_replacement.ts90
-rw-r--r--pw_protobuf_compiler/ts/package.json18
-rw-r--r--pw_protobuf_compiler/ts/proto_collection.ts27
-rw-r--r--pw_protobuf_compiler/ts/ts_proto_collection.bzl59
-rw-r--r--pw_protobuf_compiler/ts/ts_proto_collection.template.ts2
-rw-r--r--pw_protobuf_compiler/ts/ts_proto_collection_test.ts25
-rw-r--r--pw_protobuf_compiler/ts/tsconfig.json34
-rw-r--r--pw_protobuf_compiler/ts/yarn.lock497
-rw-r--r--pw_random/BUILD.bazel4
-rw-r--r--pw_random/BUILD.gn33
-rw-r--r--pw_random/CMakeLists.txt10
-rw-r--r--pw_random/docs.rst15
-rw-r--r--pw_random/get_int_bounded_fuzzer.cc59
-rw-r--r--pw_random/public/pw_random/fuzzer.h37
-rw-r--r--pw_random/public/pw_random/random.h71
-rw-r--r--pw_random/public/pw_random/xor_shift.h11
-rw-r--r--pw_random/xor_shift_test.cc193
-rw-r--r--pw_result/Android.bp27
-rw-r--r--pw_result/BUILD.gn17
-rw-r--r--pw_result/CMakeLists.txt4
-rw-r--r--pw_result/docs.rst140
-rw-r--r--pw_result/public/pw_result/internal/result_internal.h15
-rw-r--r--pw_result/public/pw_result/result.h158
-rw-r--r--pw_result/result_test.cc375
-rw-r--r--pw_result/size_report/BUILD.bazel36
-rw-r--r--pw_result/size_report/BUILD.gn30
-rw-r--r--pw_result/size_report/ladder_and_then.cc37
-rw-r--r--pw_result/size_report/ladder_or_else.cc36
-rw-r--r--pw_result/size_report/ladder_transform.cc33
-rw-r--r--pw_result/size_report/monadic_and_then.cc31
-rw-r--r--pw_result/size_report/monadic_or_else.cc36
-rw-r--r--pw_result/size_report/monadic_transform.cc32
-rw-r--r--pw_result/size_report/pointer_read.cc8
-rw-r--r--pw_result/size_report/result_read.cc8
-rw-r--r--pw_result/statusor_test.cc14
-rw-r--r--pw_ring_buffer/BUILD.gn3
-rw-r--r--pw_ring_buffer/CMakeLists.txt7
-rw-r--r--pw_ring_buffer/docs.rst9
-rw-r--r--pw_ring_buffer/prefixed_entry_ring_buffer.cc37
-rw-r--r--pw_ring_buffer/prefixed_entry_ring_buffer_test.cc64
-rw-r--r--pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h60
-rw-r--r--pw_ring_buffer/size_report/BUILD.bazel8
-rw-r--r--pw_ring_buffer/size_report/ring_buffer_multi.cc4
-rw-r--r--pw_router/BUILD.bazel1
-rw-r--r--pw_router/BUILD.gn10
-rw-r--r--pw_router/CMakeLists.txt40
-rw-r--r--pw_router/Kconfig7
-rw-r--r--pw_router/public/pw_router/egress.h2
-rw-r--r--pw_router/public/pw_router/egress_function.h3
-rw-r--r--pw_router/public/pw_router/packet_parser.h3
-rw-r--r--pw_router/public/pw_router/static_router.h7
-rw-r--r--pw_router/size_report/BUILD.gn2
-rw-r--r--pw_router/static_router_test.cc4
-rw-r--r--pw_rpc/Android.bp230
-rw-r--r--pw_rpc/BUILD.bazel167
-rw-r--r--pw_rpc/BUILD.gn155
-rw-r--r--pw_rpc/CMakeLists.txt376
-rw-r--r--pw_rpc/Kconfig8
-rw-r--r--pw_rpc/benchmark.cc4
-rw-r--r--pw_rpc/benchmark.rst20
-rw-r--r--pw_rpc/call.cc212
-rw-r--r--pw_rpc/call_test.cc370
-rw-r--r--pw_rpc/callback_test.cc248
-rw-r--r--pw_rpc/channel.cc34
-rw-r--r--pw_rpc/channel_test.cc67
-rw-r--r--pw_rpc/client.cc12
-rw-r--r--pw_rpc/client_call.cc45
-rw-r--r--pw_rpc/client_integration_test.cc31
-rw-r--r--pw_rpc/client_server.cc5
-rw-r--r--pw_rpc/client_server_test.cc14
-rw-r--r--pw_rpc/docs.rst809
-rw-r--r--pw_rpc/endpoint.cc182
-rw-r--r--pw_rpc/fake_channel_output.cc25
-rw-r--r--pw_rpc/fake_channel_output_test.cc28
-rw-r--r--pw_rpc/fuzz/BUILD.gn140
-rw-r--r--pw_rpc/fuzz/alarm_timer_test.cc64
-rw-r--r--pw_rpc/fuzz/argparse.cc259
-rw-r--r--pw_rpc/fuzz/argparse_test.cc196
-rw-r--r--pw_rpc/fuzz/client_fuzzer.cc111
-rw-r--r--pw_rpc/fuzz/engine.cc553
-rw-r--r--pw_rpc/fuzz/engine_test.cc264
-rw-r--r--pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h56
-rw-r--r--pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h230
-rw-r--r--pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h339
-rw-r--r--pw_rpc/integration_testing.cc32
-rw-r--r--pw_rpc/internal/integration_test_ports.gni3
-rw-r--r--pw_rpc/internal/packet.proto13
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/AbstractCall.java145
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/BUILD.bazel9
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Call.java54
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Channel.java7
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java194
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Endpoint.java311
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/FutureCall.java163
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcChannelException.java29
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcServiceException.java21
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcServiceMethodException.java27
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcStateException.java22
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java2
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java112
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/PendingRpc.java18
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/RpcKey.java41
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/RpcManager.java112
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java2
-rw-r--r--pw_rpc/java/main/dev/pigweed/pw_rpc/StreamObserverCall.java235
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel47
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java104
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java209
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java217
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/IdsTest.java1
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/PacketsTest.java3
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/RpcManagerTest.java149
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java148
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java164
-rw-r--r--pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java98
-rw-r--r--pw_rpc/method_test.cc2
-rw-r--r--pw_rpc/nanopb/Android.bp84
-rw-r--r--pw_rpc/nanopb/BUILD.bazel78
-rw-r--r--pw_rpc/nanopb/BUILD.gn68
-rw-r--r--pw_rpc/nanopb/CMakeLists.txt336
-rw-r--r--pw_rpc/nanopb/client_call_test.cc43
-rw-r--r--pw_rpc/nanopb/client_integration_test.cc2
-rw-r--r--pw_rpc/nanopb/client_server_context_test.cc116
-rw-r--r--pw_rpc/nanopb/client_server_context_threaded_test.cc117
-rw-r--r--pw_rpc/nanopb/codegen_test.cc3
-rw-r--r--pw_rpc/nanopb/common.cc29
-rw-r--r--pw_rpc/nanopb/docs.rst32
-rw-r--r--pw_rpc/nanopb/fake_channel_output_test.cc16
-rw-r--r--pw_rpc/nanopb/method.cc6
-rw-r--r--pw_rpc/nanopb/method_test.cc21
-rw-r--r--pw_rpc/nanopb/method_union_test.cc6
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h134
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h110
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h115
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h10
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h45
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/internal/common.h31
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method.h10
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method_union.h2
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h114
-rw-r--r--pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h14
-rw-r--r--pw_rpc/nanopb/pw_rpc_nanopb_private/internal_test_utils.h22
-rw-r--r--pw_rpc/nanopb/serde_test.cc2
-rw-r--r--pw_rpc/nanopb/server_reader_writer.cc16
-rw-r--r--pw_rpc/nanopb/server_reader_writer_test.cc30
-rw-r--r--pw_rpc/nanopb/synchronous_call_test.cc235
-rw-r--r--pw_rpc/packet.cc24
-rw-r--r--pw_rpc/packet_meta.cc39
-rw-r--r--pw_rpc/packet_meta_test.cc57
-rw-r--r--pw_rpc/packet_test.cc3
-rw-r--r--pw_rpc/public/pw_rpc/channel.h19
-rw-r--r--pw_rpc/public/pw_rpc/client.h14
-rw-r--r--pw_rpc/public/pw_rpc/client_server.h21
-rw-r--r--pw_rpc/public/pw_rpc/integration_test_socket_client.h105
-rw-r--r--pw_rpc/public/pw_rpc/integration_testing.h72
-rw-r--r--pw_rpc/public/pw_rpc/internal/call.h374
-rw-r--r--pw_rpc/public/pw_rpc/internal/call_context.h36
-rw-r--r--pw_rpc/public/pw_rpc/internal/channel.h8
-rw-r--r--pw_rpc/public/pw_rpc/internal/channel_list.h41
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_call.h99
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_server_testing.h114
-rw-r--r--pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h137
-rw-r--r--pw_rpc/public/pw_rpc/internal/config.h213
-rw-r--r--pw_rpc/public/pw_rpc/internal/encoding_buffer.h146
-rw-r--r--pw_rpc/public/pw_rpc/internal/endpoint.h180
-rw-r--r--pw_rpc/public/pw_rpc/internal/fake_channel_output.h85
-rw-r--r--pw_rpc/public/pw_rpc/internal/lock.h25
-rw-r--r--pw_rpc/public/pw_rpc/internal/method_info_tester.h25
-rw-r--r--pw_rpc/public/pw_rpc/internal/method_lookup.h7
-rw-r--r--pw_rpc/public/pw_rpc/internal/packet.h39
-rw-r--r--pw_rpc/public/pw_rpc/internal/server_call.h30
-rw-r--r--pw_rpc/public/pw_rpc/internal/test_method.h14
-rw-r--r--pw_rpc/public/pw_rpc/internal/test_method_context.h37
-rw-r--r--pw_rpc/public/pw_rpc/internal/test_utils.h49
-rw-r--r--pw_rpc/public/pw_rpc/method_id.h75
-rw-r--r--pw_rpc/public/pw_rpc/method_info.h76
-rw-r--r--pw_rpc/public/pw_rpc/packet_meta.h61
-rw-r--r--pw_rpc/public/pw_rpc/payloads_view.h15
-rw-r--r--pw_rpc/public/pw_rpc/server.h115
-rw-r--r--pw_rpc/public/pw_rpc/service.h12
-rw-r--r--pw_rpc/public/pw_rpc/service_id.h80
-rw-r--r--pw_rpc/public/pw_rpc/synchronous_call.h295
-rw-r--r--pw_rpc/public/pw_rpc/synchronous_call_result.h271
-rw-r--r--pw_rpc/public/pw_rpc/test_helpers.h104
-rw-r--r--pw_rpc/public/pw_rpc/thread_testing.h29
-rw-r--r--pw_rpc/pw_rpc_private/fake_server_reader_writer.h22
-rw-r--r--pw_rpc/pw_rpc_test_protos/no_package.proto44
-rw-r--r--pw_rpc/pwpb/BUILD.bazel333
-rw-r--r--pw_rpc/pwpb/BUILD.gn329
-rw-r--r--pw_rpc/pwpb/CMakeLists.txt354
-rw-r--r--pw_rpc/pwpb/client_call_test.cc376
-rw-r--r--pw_rpc/pwpb/client_integration_test.cc160
-rw-r--r--pw_rpc/pwpb/client_reader_writer_test.cc239
-rw-r--r--pw_rpc/pwpb/client_server_context_test.cc123
-rw-r--r--pw_rpc/pwpb/client_server_context_threaded_test.cc123
-rw-r--r--pw_rpc/pwpb/codegen_test.cc399
-rw-r--r--pw_rpc/pwpb/docs.rst261
-rw-r--r--pw_rpc/pwpb/echo_service_test.cc41
-rw-r--r--pw_rpc/pwpb/fake_channel_output_test.cc98
-rw-r--r--pw_rpc/pwpb/method_info_test.cc62
-rw-r--r--pw_rpc/pwpb/method_lookup_test.cc161
-rw-r--r--pw_rpc/pwpb/method_test.cc425
-rw-r--r--pw_rpc/pwpb/method_union_test.cc175
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/echo_service_pwpb.h30
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h473
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h110
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h115
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h115
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h211
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/internal/common.h74
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h461
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method_union.h57
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h119
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h469
-rw-r--r--pw_rpc/pwpb/public/pw_rpc/pwpb/test_method_context.h397
-rw-r--r--pw_rpc/pwpb/pw_rpc_pwpb_private/internal_test_utils.h69
-rw-r--r--pw_rpc/pwpb/serde_test.cc56
-rw-r--r--pw_rpc/pwpb/server_callback_test.cc95
-rw-r--r--pw_rpc/pwpb/server_reader_writer.cc26
-rw-r--r--pw_rpc/pwpb/server_reader_writer_test.cc318
-rw-r--r--pw_rpc/pwpb/stub_generation_test.cc29
-rw-r--r--pw_rpc/pwpb/synchronous_call_test.cc238
-rw-r--r--pw_rpc/py/Android.bp47
-rw-r--r--pw_rpc/py/BUILD.bazel114
-rw-r--r--pw_rpc/py/BUILD.gn15
-rw-r--r--pw_rpc/py/pw_rpc/__init__.py4
-rw-r--r--pw_rpc/py/pw_rpc/callback_client/call.py161
-rw-r--r--pw_rpc/py/pw_rpc/callback_client/errors.py3
-rw-r--r--pw_rpc/py/pw_rpc/callback_client/impl.py463
-rw-r--r--pw_rpc/py/pw_rpc/client.py310
-rw-r--r--pw_rpc/py/pw_rpc/codegen.py225
-rw-r--r--pw_rpc/py/pw_rpc/codegen_nanopb.py141
-rw-r--r--pw_rpc/py/pw_rpc/codegen_pwpb.py277
-rw-r--r--pw_rpc/py/pw_rpc/codegen_raw.py116
-rw-r--r--pw_rpc/py/pw_rpc/console_tools/__init__.py10
-rw-r--r--pw_rpc/py/pw_rpc/console_tools/console.py169
-rw-r--r--pw_rpc/py/pw_rpc/console_tools/functions.py22
-rw-r--r--pw_rpc/py/pw_rpc/console_tools/watchdog.py20
-rw-r--r--pw_rpc/py/pw_rpc/descriptors.py95
-rw-r--r--pw_rpc/py/pw_rpc/lossy_channel.py253
-rw-r--r--pw_rpc/py/pw_rpc/packets.py52
-rw-r--r--pw_rpc/py/pw_rpc/plugin.py15
-rwxr-xr-xpw_rpc/py/pw_rpc/plugin_pwpb.py27
-rw-r--r--pw_rpc/py/pw_rpc/testing.py87
-rwxr-xr-xpw_rpc/py/tests/callback_client_test.py536
-rwxr-xr-xpw_rpc/py/tests/client_test.py169
-rwxr-xr-xpw_rpc/py/tests/console_tools/console_tools_test.py113
-rw-r--r--pw_rpc/py/tests/console_tools/functions_test.py10
-rw-r--r--pw_rpc/py/tests/console_tools/watchdog_test.py6
-rw-r--r--pw_rpc/py/tests/descriptors_test.py20
-rwxr-xr-xpw_rpc/py/tests/ids_test.py31
-rwxr-xr-xpw_rpc/py/tests/packets_test.py70
-rwxr-xr-xpw_rpc/py/tests/python_client_cpp_server_test.py32
-rw-r--r--pw_rpc/raw/Android.bp74
-rw-r--r--pw_rpc/raw/BUILD.bazel9
-rw-r--r--pw_rpc/raw/BUILD.gn8
-rw-r--r--pw_rpc/raw/CMakeLists.txt134
-rw-r--r--pw_rpc/raw/client_reader_writer_test.cc166
-rw-r--r--pw_rpc/raw/client_test.cc173
-rw-r--r--pw_rpc/raw/client_testing.cc43
-rw-r--r--pw_rpc/raw/codegen_test.cc98
-rw-r--r--pw_rpc/raw/method.cc35
-rw-r--r--pw_rpc/raw/method_test.cc37
-rw-r--r--pw_rpc/raw/method_union_test.cc28
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h51
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/client_testing.h46
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/internal/method.h4
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/internal/method_union.h2
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h87
-rw-r--r--pw_rpc/raw/public/pw_rpc/raw/test_method_context.h7
-rw-r--r--pw_rpc/raw/server_reader_writer_test.cc29
-rw-r--r--pw_rpc/raw/service_nc_test.cc38
-rw-r--r--pw_rpc/server.cc72
-rw-r--r--pw_rpc/server_call.cc2
-rw-r--r--pw_rpc/server_test.cc415
-rw-r--r--pw_rpc/size_report/base.cc4
-rw-r--r--pw_rpc/size_report/server_only.cc10
-rw-r--r--pw_rpc/size_report/server_with_echo_service.cc4
-rw-r--r--pw_rpc/system_server/BUILD.bazel4
-rw-r--r--pw_rpc/system_server/BUILD.gn7
-rw-r--r--pw_rpc/system_server/CMakeLists.txt20
-rw-r--r--pw_rpc/system_server/backend.cmake19
-rw-r--r--pw_rpc/system_server/public/pw_rpc_system_server/rpc_server.h6
-rw-r--r--pw_rpc/system_server/public/pw_rpc_system_server/socket.h4
-rw-r--r--pw_rpc/test_helpers_test.cc199
-rw-r--r--pw_rpc/ts/BUILD.bazel106
-rw-r--r--pw_rpc/ts/call.ts4
-rw-r--r--pw_rpc/ts/call_test.ts33
-rw-r--r--pw_rpc/ts/client.ts16
-rw-r--r--pw_rpc/ts/client_test.ts170
-rw-r--r--pw_rpc/ts/descriptors.ts4
-rw-r--r--pw_rpc/ts/descriptors_test.ts20
-rw-r--r--pw_rpc/ts/docs.rst47
-rw-r--r--pw_rpc/ts/method.ts2
-rw-r--r--pw_rpc/ts/package.json17
-rw-r--r--pw_rpc/ts/packets.ts6
-rw-r--r--pw_rpc/ts/packets_test.ts16
-rw-r--r--pw_rpc/ts/queue.ts59
-rw-r--r--pw_rpc/ts/rpc_classes.ts9
-rw-r--r--pw_rpc/ts/tsconfig.json34
-rw-r--r--pw_rpc/ts/yarn.lock510
-rw-r--r--pw_rust/BUILD.bazel13
-rw-r--r--pw_rust/BUILD.gn25
-rw-r--r--pw_rust/docs.rst63
-rw-r--r--pw_rust/examples/embedded_hello/BUILD.bazel39
-rw-r--r--pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld271
-rw-r--r--pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld271
-rw-r--r--pw_rust/examples/embedded_hello/src/main.rs32
-rw-r--r--pw_rust/examples/host_executable/BUILD.gn48
-rw-r--r--pw_rust/examples/host_executable/a/lib.rs22
-rw-r--r--pw_rust/examples/host_executable/b/lib.rs (renamed from pw_polyfill/public_overrides/type_traits)10
-rw-r--r--pw_rust/examples/host_executable/c/lib.rs19
-rw-r--r--pw_rust/examples/host_executable/main.rs42
-rw-r--r--pw_rust/examples/host_executable/other.rs18
-rw-r--r--pw_rust/rust.gni20
-rw-r--r--pw_snapshot/BUILD.bazel48
-rw-r--r--pw_snapshot/BUILD.gn2
-rw-r--r--pw_snapshot/CMakeLists.txt23
-rw-r--r--pw_snapshot/cpp_compile_test.cc26
-rw-r--r--pw_snapshot/docs.rst2
-rw-r--r--pw_snapshot/module_usage.rst7
-rw-r--r--pw_snapshot/public/pw_snapshot/uuid.h6
-rw-r--r--pw_snapshot/pw_snapshot_protos/snapshot.proto10
-rw-r--r--pw_snapshot/pw_snapshot_protos/snapshot_metadata.proto7
-rw-r--r--pw_snapshot/py/BUILD.bazel65
-rw-r--r--pw_snapshot/py/BUILD.gn10
-rw-r--r--pw_snapshot/py/generate_example_snapshot.py36
-rw-r--r--pw_snapshot/py/metadata_test.py142
-rw-r--r--pw_snapshot/py/pw_snapshot/processor.py122
-rw-r--r--pw_snapshot/py/pw_snapshot_metadata/metadata.py76
-rw-r--r--pw_snapshot/setup.rst2
-rw-r--r--pw_snapshot/uuid.cc7
-rw-r--r--pw_snapshot/uuid_test.cc18
-rw-r--r--pw_software_update/BUILD.bazel139
-rw-r--r--pw_software_update/BUILD.gn111
-rw-r--r--pw_software_update/bundled_update.proto7
-rw-r--r--pw_software_update/bundled_update_service.cc20
-rw-r--r--pw_software_update/bundled_update_service_pwpb.cc521
-rw-r--r--pw_software_update/bundled_update_service_pwpb_test.cc19
-rw-r--r--pw_software_update/docs.rst441
-rw-r--r--pw_software_update/manifest_accessor.cc25
-rw-r--r--pw_software_update/public/pw_software_update/blob_store_openable_reader.h38
-rw-r--r--pw_software_update/public/pw_software_update/bundled_update_backend.h29
-rw-r--r--pw_software_update/public/pw_software_update/bundled_update_service.h18
-rw-r--r--pw_software_update/public/pw_software_update/bundled_update_service_pwpb.h112
-rw-r--r--pw_software_update/public/pw_software_update/config.h18
-rw-r--r--pw_software_update/public/pw_software_update/manifest_accessor.h2
-rw-r--r--pw_software_update/public/pw_software_update/openable_reader.h35
-rw-r--r--pw_software_update/public/pw_software_update/update_bundle_accessor.h50
-rw-r--r--pw_software_update/py/BUILD.gn14
-rw-r--r--pw_software_update/py/cli_test.py104
-rw-r--r--pw_software_update/py/dev_sign_test.py40
-rw-r--r--pw_software_update/py/keys_test.py117
-rw-r--r--pw_software_update/py/metadata_test.py16
-rw-r--r--pw_software_update/py/pw_software_update/cli.py554
-rw-r--r--pw_software_update/py/pw_software_update/dev_sign.py60
-rw-r--r--pw_software_update/py/pw_software_update/generate_test_bundle.py336
-rw-r--r--pw_software_update/py/pw_software_update/keys.py97
-rw-r--r--pw_software_update/py/pw_software_update/metadata.py59
-rw-r--r--pw_software_update/py/pw_software_update/remote_sign.py428
-rw-r--r--pw_software_update/py/pw_software_update/root_metadata.py106
-rw-r--r--pw_software_update/py/pw_software_update/update_bundle.py168
-rw-r--r--pw_software_update/py/pw_software_update/verify.py144
-rw-r--r--pw_software_update/py/remote_sign_test.py129
-rw-r--r--pw_software_update/py/root_metadata_test.py41
-rw-r--r--pw_software_update/py/update_bundle_test.py52
-rw-r--r--pw_software_update/py/verify_test.py84
-rw-r--r--pw_software_update/tuf.proto4
-rw-r--r--pw_software_update/update_bundle_accessor.cc202
-rw-r--r--pw_software_update/update_bundle_test.cc71
-rw-r--r--pw_span/Android.bp26
-rw-r--r--pw_span/BUILD.bazel25
-rw-r--r--pw_span/BUILD.gn95
-rw-r--r--pw_span/CMakeLists.txt23
-rw-r--r--pw_span/compatibility_test.cc77
-rw-r--r--pw_span/docs.rst88
-rw-r--r--pw_span/public/pw_span/internal/config.h26
-rw-r--r--pw_span/public/pw_span/internal/span_impl.h (renamed from pw_span/public/pw_span/internal/span.h)32
-rw-r--r--pw_span/public/pw_span/span.h46
-rw-r--r--pw_span/public_overrides/span20
-rw-r--r--pw_span/span_test.cc45
-rw-r--r--pw_spi/BUILD.bazel41
-rw-r--r--pw_spi/BUILD.gn35
-rw-r--r--pw_spi/docs.rst65
-rw-r--r--pw_spi/initiator_mock.cc49
-rw-r--r--pw_spi/initiator_mock_test.cc92
-rw-r--r--pw_spi/linux_spi.cc18
-rw-r--r--pw_spi/linux_spi_test.cc101
-rw-r--r--pw_spi/public/pw_spi/chip_selector.h2
-rw-r--r--pw_spi/public/pw_spi/chip_selector_mock.h36
-rw-r--r--pw_spi/public/pw_spi/device.h14
-rw-r--r--pw_spi/public/pw_spi/initiator.h4
-rw-r--r--pw_spi/public/pw_spi/initiator_mock.h123
-rw-r--r--pw_spi/public/pw_spi/linux_spi.h11
-rw-r--r--pw_spi/spi_test.cc4
-rw-r--r--pw_status/Android.bp27
-rw-r--r--pw_status/CMakeLists.txt12
-rw-r--r--pw_status/py/Android.bp24
-rw-r--r--pw_status/py/BUILD.gn1
-rw-r--r--pw_status/status.cc1
-rw-r--r--pw_status/ts/package.json8
-rw-r--r--pw_status/ts/tsconfig.json34
-rw-r--r--pw_status/ts/yarn.lock4
-rw-r--r--pw_stm32cube_build/BUILD.gn4
-rw-r--r--pw_stm32cube_build/py/BUILD.gn1
-rw-r--r--pw_stm32cube_build/py/pw_stm32cube_build/__main__.py58
-rw-r--r--pw_stm32cube_build/py/pw_stm32cube_build/find_files.py112
-rw-r--r--pw_stm32cube_build/py/pw_stm32cube_build/gen_file_list.py5
-rw-r--r--pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py69
-rw-r--r--pw_stm32cube_build/py/pw_stm32cube_build/inject_init.py10
-rw-r--r--pw_stm32cube_build/py/tests/find_files_test.py128
-rw-r--r--pw_stm32cube_build/py/tests/icf_to_ld_test.py73
-rw-r--r--pw_stm32cube_build/py/tests/inject_init_test.py107
-rw-r--r--pw_stream/Android.bp41
-rw-r--r--pw_stream/BUILD.bazel41
-rw-r--r--pw_stream/BUILD.gn44
-rw-r--r--pw_stream/CMakeLists.txt31
-rw-r--r--pw_stream/Kconfig1
-rw-r--r--pw_stream/docs.rst32
-rw-r--r--pw_stream/interval_reader.cc2
-rw-r--r--pw_stream/interval_reader_test.cc4
-rw-r--r--pw_stream/memory_stream_test.cc27
-rw-r--r--pw_stream/null_stream_test.cc84
-rw-r--r--pw_stream/public/pw_stream/interval_reader.h13
-rw-r--r--pw_stream/public/pw_stream/memory_stream.h10
-rw-r--r--pw_stream/public/pw_stream/null_stream.h21
-rw-r--r--pw_stream/public/pw_stream/socket_stream.h82
-rw-r--r--pw_stream/public/pw_stream/std_file_stream.h3
-rw-r--r--pw_stream/public/pw_stream/stream.h11
-rw-r--r--pw_stream/public/pw_stream/sys_io_stream.h4
-rw-r--r--pw_stream/socket_stream.cc165
-rw-r--r--pw_stream/socket_stream_test.cc194
-rw-r--r--pw_stream/std_file_stream.cc41
-rw-r--r--pw_stream/std_file_stream_test.cc182
-rw-r--r--pw_stream/stream_test.cc22
-rw-r--r--pw_string/Android.bp42
-rw-r--r--pw_string/BUILD.bazel99
-rw-r--r--pw_string/BUILD.gn132
-rw-r--r--pw_string/CMakeLists.txt112
-rw-r--r--pw_string/api.rst91
-rw-r--r--pw_string/design.rst75
-rw-r--r--pw_string/docs.rst332
-rw-r--r--pw_string/format.cc4
-rw-r--r--pw_string/format_test.cc8
-rw-r--r--pw_string/guide.rst248
-rw-r--r--pw_string/public/pw_string/format.h35
-rw-r--r--pw_string/public/pw_string/internal/config.h35
-rw-r--r--pw_string/public/pw_string/internal/string_common_functions.inc324
-rw-r--r--pw_string/public/pw_string/internal/string_impl.h179
-rw-r--r--pw_string/public/pw_string/string.h681
-rw-r--r--pw_string/public/pw_string/string_builder.h307
-rw-r--r--pw_string/public/pw_string/to_string.h23
-rw-r--r--pw_string/public/pw_string/type_to_string.h30
-rw-r--r--pw_string/public/pw_string/util.h169
-rw-r--r--pw_string/size_report/format_many_without_error_handling.cc4
-rw-r--r--pw_string/size_report/format_multiple.cc2
-rw-r--r--pw_string/size_report/format_single.cc6
-rw-r--r--pw_string/size_report/string_builder_size_report_incremental.cc2
-rw-r--r--pw_string/string_builder.cc36
-rw-r--r--pw_string/string_builder_test.cc45
-rw-r--r--pw_string/string_test.cc1874
-rw-r--r--pw_string/to_string_test.cc39
-rw-r--r--pw_string/type_to_string.cc23
-rw-r--r--pw_string/type_to_string_test.cc85
-rw-r--r--pw_string/util_test.cc148
-rw-r--r--pw_symbolizer/BUILD.bazel8
-rw-r--r--pw_symbolizer/BUILD.gn4
-rw-r--r--pw_symbolizer/py/BUILD.bazel45
-rw-r--r--pw_symbolizer/py/BUILD.gn1
-rw-r--r--pw_symbolizer/py/llvm_symbolizer_test.py25
-rw-r--r--pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py32
-rw-r--r--pw_symbolizer/py/pw_symbolizer/symbolizer.py13
-rw-r--r--pw_symbolizer/py/symbolizer_test.py57
-rw-r--r--pw_sync/Android.bp24
-rw-r--r--pw_sync/BUILD.bazel119
-rw-r--r--pw_sync/BUILD.gn77
-rw-r--r--pw_sync/CMakeLists.txt142
-rw-r--r--pw_sync/backend.cmake43
-rw-r--r--pw_sync/backend.gni6
-rw-r--r--pw_sync/binary_semaphore_facade_test.cc2
-rw-r--r--pw_sync/condition_variable_test.cc356
-rw-r--r--pw_sync/counting_semaphore_facade_test.cc2
-rw-r--r--pw_sync/docs.rst2297
-rw-r--r--pw_sync/inline_borrowable_test.cc224
-rw-r--r--pw_sync/interrupt_spin_lock_facade_test.cc12
-rw-r--r--pw_sync/mutex_facade_test.cc14
-rw-r--r--pw_sync/public/pw_sync/binary_semaphore.h85
-rw-r--r--pw_sync/public/pw_sync/borrow.h84
-rw-r--r--pw_sync/public/pw_sync/condition_variable.h91
-rw-r--r--pw_sync/public/pw_sync/counting_semaphore.h82
-rw-r--r--pw_sync/public/pw_sync/inline_borrowable.h151
-rw-r--r--pw_sync/public/pw_sync/internal/borrowable_storage.h89
-rw-r--r--pw_sync/public/pw_sync/interrupt_spin_lock.h72
-rw-r--r--pw_sync/public/pw_sync/lock_annotations.h297
-rw-r--r--pw_sync/public/pw_sync/mutex.h67
-rw-r--r--pw_sync/public/pw_sync/recursive_mutex.h95
-rw-r--r--pw_sync/public/pw_sync/thread_notification.h79
-rw-r--r--pw_sync/public/pw_sync/timed_mutex.h63
-rw-r--r--pw_sync/public/pw_sync/timed_thread_notification.h71
-rw-r--r--pw_sync/public/pw_sync/virtual_basic_lockable.h32
-rw-r--r--pw_sync/recursive_mutex.cc27
-rw-r--r--pw_sync/recursive_mutex_facade_test.cc96
-rw-r--r--pw_sync/recursive_mutex_facade_test_c.c32
-rw-r--r--pw_sync/thread_notification_facade_test.cc2
-rw-r--r--pw_sync/timed_mutex_facade_test.cc43
-rw-r--r--pw_sync/timed_thread_notification_facade_test.cc2
-rw-r--r--pw_sync_baremetal/Android.bp27
-rw-r--r--pw_sync_baremetal/BUILD.bazel20
-rw-r--r--pw_sync_baremetal/BUILD.gn26
-rw-r--r--pw_sync_baremetal/CMakeLists.txt29
-rw-r--r--pw_sync_baremetal/docs.rst37
-rw-r--r--pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h2
-rw-r--r--pw_sync_baremetal/public/pw_sync_baremetal/mutex_inline.h2
-rw-r--r--pw_sync_baremetal/public/pw_sync_baremetal/recursive_mutex_inline.h46
-rw-r--r--pw_sync_baremetal/public/pw_sync_baremetal/recursive_mutex_native.h23
-rw-r--r--pw_sync_baremetal/public_overrides/pw_sync_backend/recursive_mutex_inline.h16
-rw-r--r--pw_sync_baremetal/public_overrides/pw_sync_backend/recursive_mutex_native.h16
-rw-r--r--pw_sync_embos/BUILD.bazel10
-rw-r--r--pw_sync_embos/BUILD.gn4
-rw-r--r--pw_sync_freertos/BUILD.bazel141
-rw-r--r--pw_sync_freertos/BUILD.gn72
-rw-r--r--pw_sync_freertos/CMakeLists.txt43
-rw-r--r--pw_sync_freertos/docs.rst121
-rw-r--r--pw_sync_freertos/interrupt_spin_lock.cc19
-rw-r--r--pw_sync_freertos/public/pw_sync_freertos/thread_notification_inline.h5
-rw-r--r--pw_sync_freertos/public/pw_sync_freertos/thread_notification_native.h12
-rw-r--r--pw_sync_freertos/public/pw_sync_freertos/timed_thread_notification_inline.h11
-rw-r--r--pw_sync_freertos/thread_notification.cc38
-rw-r--r--pw_sync_freertos/thread_notification_test.cc123
-rw-r--r--pw_sync_freertos/timed_thread_notification.cc82
-rw-r--r--pw_sync_freertos/timed_thread_notification_test.cc125
-rw-r--r--pw_sync_stl/BUILD.bazel58
-rw-r--r--pw_sync_stl/BUILD.gn51
-rw-r--r--pw_sync_stl/CMakeLists.txt41
-rw-r--r--pw_sync_stl/public/pw_sync_stl/condition_variable_inline.h50
-rw-r--r--pw_sync_stl/public/pw_sync_stl/condition_variable_native.h25
-rw-r--r--pw_sync_stl/public/pw_sync_stl/recursive_mutex_inline.h52
-rw-r--r--pw_sync_stl/public/pw_sync_stl/recursive_mutex_native.h31
-rw-r--r--pw_sync_stl/public_overrides/pw_sync_backend/condition_variable_inline.h16
-rw-r--r--pw_sync_stl/public_overrides/pw_sync_backend/condition_variable_native.h16
-rw-r--r--pw_sync_stl/public_overrides/pw_sync_backend/recursive_mutex_inline.h16
-rw-r--r--pw_sync_stl/public_overrides/pw_sync_backend/recursive_mutex_native.h16
-rw-r--r--pw_sync_threadx/BUILD.bazel10
-rw-r--r--pw_sync_threadx/BUILD.gn4
-rw-r--r--pw_sync_zephyr/BUILD.gn4
-rw-r--r--pw_sync_zephyr/CMakeLists.txt31
-rw-r--r--pw_sync_zephyr/Kconfig17
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/mutex_inline.h2
-rw-r--r--pw_sync_zephyr/public/pw_sync_zephyr/mutex_native.h2
-rw-r--r--pw_sys_io/BUILD.bazel5
-rw-r--r--pw_sys_io/BUILD.gn9
-rw-r--r--pw_sys_io/CMakeLists.txt18
-rw-r--r--pw_sys_io/backend.cmake19
-rw-r--r--pw_sys_io/public/pw_sys_io/sys_io.h10
-rw-r--r--pw_sys_io/sys_io.cc4
-rw-r--r--pw_sys_io_arduino/BUILD.bazel10
-rw-r--r--pw_sys_io_arduino/BUILD.gn4
-rw-r--r--pw_sys_io_arduino/sys_io_arduino.cc17
-rw-r--r--pw_sys_io_baremetal_lm3s6965evb/BUILD.gn8
-rw-r--r--pw_sys_io_baremetal_lm3s6965evb/docs.rst9
-rw-r--r--pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc4
-rw-r--r--pw_sys_io_baremetal_stm32f429/BUILD.bazel5
-rw-r--r--pw_sys_io_baremetal_stm32f429/BUILD.gn4
-rw-r--r--pw_sys_io_baremetal_stm32f429/public/pw_sys_io_baremetal_stm32f429/init.h2
-rw-r--r--pw_sys_io_baremetal_stm32f429/sys_io_baremetal.cc34
-rw-r--r--pw_sys_io_emcraft_sf2/BUILD.gn4
-rw-r--r--pw_sys_io_emcraft_sf2/OWNERS1
-rw-r--r--pw_sys_io_emcraft_sf2/sys_io_emcraft_sf2.cc4
-rw-r--r--pw_sys_io_mcuxpresso/BUILD.bazel2
-rw-r--r--pw_sys_io_mcuxpresso/BUILD.gn4
-rw-r--r--pw_sys_io_mcuxpresso/sys_io.cc4
-rw-r--r--pw_sys_io_pico/BUILD.bazel35
-rw-r--r--pw_sys_io_pico/BUILD.gn38
-rw-r--r--pw_sys_io_pico/OWNERS1
-rw-r--r--pw_sys_io_pico/docs.rst8
-rw-r--r--pw_sys_io_pico/sys_io.cc89
-rw-r--r--pw_sys_io_stdio/BUILD.gn4
-rw-r--r--pw_sys_io_stdio/CMakeLists.txt9
-rw-r--r--pw_sys_io_stdio/sys_io.cc2
-rw-r--r--pw_sys_io_stm32cube/BUILD.bazel4
-rw-r--r--pw_sys_io_stm32cube/BUILD.gn4
-rw-r--r--pw_sys_io_stm32cube/docs.rst20
-rw-r--r--pw_sys_io_stm32cube/pw_sys_io_stm32cube_private/config.h19
-rw-r--r--pw_sys_io_stm32cube/sys_io.cc52
-rw-r--r--pw_sys_io_zephyr/BUILD.gn4
-rw-r--r--pw_sys_io_zephyr/CMakeLists.txt11
-rw-r--r--pw_sys_io_zephyr/sys_io.cc10
-rw-r--r--pw_system/BUILD.bazel81
-rw-r--r--pw_system/BUILD.gn90
-rw-r--r--pw_system/CMakeLists.txt79
-rw-r--r--pw_system/backend.cmake19
-rw-r--r--pw_system/docs.rst13
-rw-r--r--pw_system/example_user_app_init.cc11
-rw-r--r--pw_system/freertos_backends.gni6
-rw-r--r--pw_system/freertos_target_hooks.cc13
-rw-r--r--pw_system/hdlc_rpc_server.cc15
-rw-r--r--pw_system/init.cc17
-rw-r--r--pw_system/log.cc10
-rw-r--r--pw_system/log_backend.cc17
-rw-r--r--pw_system/public/pw_system/config.h17
-rw-r--r--pw_system/public/pw_system/rpc_server.h1
-rw-r--r--pw_system/public/pw_system/thread_snapshot_service.h22
-rw-r--r--pw_system/py/BUILD.bazel60
-rw-r--r--pw_system/py/BUILD.gn8
-rw-r--r--pw_system/py/pw_system/console.py391
-rw-r--r--pw_system/py/pw_system/device.py174
-rw-r--r--pw_system/py/setup.cfg8
-rw-r--r--pw_system/socket_target_io.cc6
-rw-r--r--pw_system/stl_backends.gni5
-rw-r--r--pw_system/stl_target_hooks.cc18
-rw-r--r--pw_system/system_target.gni120
-rw-r--r--pw_system/thread_snapshot_service.cc30
-rw-r--r--pw_system/work_queue.cc2
-rw-r--r--pw_target_runner/BUILD.gn4
-rw-r--r--pw_target_runner/go/src/pigweed.dev/pw_target_runner_client/main.go2
-rw-r--r--pw_target_runner/go/src/pigweed.dev/pw_target_runner_server/main.go2
-rw-r--r--pw_thread/BUILD.bazel151
-rw-r--r--pw_thread/BUILD.gn75
-rw-r--r--pw_thread/CMakeLists.txt43
-rw-r--r--pw_thread/backend.cmake28
-rw-r--r--pw_thread/backend.gni3
-rw-r--r--pw_thread/docs.rst112
-rw-r--r--pw_thread/public/pw_thread/config.h10
-rw-r--r--pw_thread/public/pw_thread/snapshot.h5
-rw-r--r--pw_thread/public/pw_thread/thread.h14
-rw-r--r--pw_thread/public/pw_thread/thread_info.h117
-rw-r--r--pw_thread/public/pw_thread/thread_iteration.h37
-rw-r--r--pw_thread/public/pw_thread/thread_snapshot_service.h85
-rw-r--r--pw_thread/pw_thread_private/thread_snapshot_service.h27
-rw-r--r--pw_thread/pw_thread_protos/thread.proto4
-rw-r--r--pw_thread/pw_thread_protos/thread_snapshot_service.proto32
-rw-r--r--pw_thread/py/BUILD.bazel47
-rw-r--r--pw_thread/py/BUILD.gn1
-rw-r--r--pw_thread/py/pw_thread/thread_analyzer.py137
-rw-r--r--pw_thread/py/thread_analyzer_test.py262
-rw-r--r--pw_thread/snapshot.cc22
-rw-r--r--pw_thread/thread_info_test.cc99
-rw-r--r--pw_thread/thread_snapshot_service.cc197
-rw-r--r--pw_thread/thread_snapshot_service_test.cc206
-rw-r--r--pw_thread_embos/BUILD.bazel49
-rw-r--r--pw_thread_embos/BUILD.gn40
-rw-r--r--pw_thread_embos/docs.rst11
-rw-r--r--pw_thread_embos/id_public_overrides/pw_thread_backend/id_inline.h (renamed from pw_thread_embos/public_overrides/pw_thread_backend/id_inline.h)0
-rw-r--r--pw_thread_embos/id_public_overrides/pw_thread_backend/id_native.h (renamed from pw_thread_embos/public_overrides/pw_thread_backend/id_native.h)0
-rw-r--r--pw_thread_embos/public/pw_thread_embos/context.h8
-rw-r--r--pw_thread_embos/public/pw_thread_embos/options.h4
-rw-r--r--pw_thread_embos/public/pw_thread_embos/snapshot.h6
-rw-r--r--pw_thread_embos/sleep_public_overrides/pw_thread_backend/sleep_inline.h (renamed from pw_thread_embos/public_overrides/pw_thread_backend/sleep_inline.h)0
-rw-r--r--pw_thread_embos/snapshot.cc22
-rw-r--r--pw_thread_embos/thread_public_overrides/pw_thread_backend/thread_inline.h (renamed from pw_thread_embos/public_overrides/pw_thread_backend/thread_inline.h)0
-rw-r--r--pw_thread_embos/thread_public_overrides/pw_thread_backend/thread_native.h (renamed from pw_thread_embos/public_overrides/pw_thread_backend/thread_native.h)0
-rw-r--r--pw_thread_embos/yield_public_overrides/pw_thread_backend/yield_inline.h (renamed from pw_thread_embos/public_overrides/pw_thread_backend/yield_inline.h)0
-rw-r--r--pw_thread_freertos/BUILD.bazel152
-rw-r--r--pw_thread_freertos/BUILD.gn129
-rw-r--r--pw_thread_freertos/CMakeLists.txt57
-rw-r--r--pw_thread_freertos/backend.cmake26
-rw-r--r--pw_thread_freertos/backend.gni13
-rw-r--r--pw_thread_freertos/docs.rst40
-rw-r--r--pw_thread_freertos/id_public_overrides/pw_thread_backend/id_inline.h (renamed from pw_thread_freertos/public_overrides/pw_thread_backend/id_inline.h)0
-rw-r--r--pw_thread_freertos/id_public_overrides/pw_thread_backend/id_native.h (renamed from pw_thread_freertos/public_overrides/pw_thread_backend/id_native.h)0
-rw-r--r--pw_thread_freertos/public/pw_thread_freertos/context.h8
-rw-r--r--pw_thread_freertos/public/pw_thread_freertos/options.h12
-rw-r--r--pw_thread_freertos/public/pw_thread_freertos/sleep_inline.h1
-rw-r--r--pw_thread_freertos/public/pw_thread_freertos/snapshot.h9
-rw-r--r--pw_thread_freertos/public/pw_thread_freertos/thread_inline.h1
-rw-r--r--pw_thread_freertos/public/pw_thread_freertos/util.h3
-rw-r--r--pw_thread_freertos/public/pw_thread_freertos/yield_inline.h1
-rw-r--r--pw_thread_freertos/pw_thread_freertos_private/thread_iteration.h25
-rw-r--r--pw_thread_freertos/py/BUILD.bazel21
-rw-r--r--pw_thread_freertos/py/BUILD.gn32
-rw-r--r--pw_thread_freertos/py/pw_thread_freertos/__init__.py0
-rw-r--r--pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py90
-rw-r--r--pw_thread_freertos/py/py.typed0
-rw-r--r--pw_thread_freertos/py/pyproject.toml16
-rw-r--r--pw_thread_freertos/py/setup.cfg27
-rw-r--r--pw_thread_freertos/py/setup.py18
-rw-r--r--pw_thread_freertos/sleep_public_overrides/pw_thread_backend/sleep_inline.h (renamed from pw_thread_freertos/public_overrides/pw_thread_backend/sleep_inline.h)0
-rw-r--r--pw_thread_freertos/snapshot.cc38
-rw-r--r--pw_thread_freertos/thread.cc23
-rw-r--r--pw_thread_freertos/thread_iteration.cc81
-rw-r--r--pw_thread_freertos/thread_iteration_test.cc160
-rw-r--r--pw_thread_freertos/thread_public_overrides/pw_thread_backend/thread_inline.h (renamed from pw_thread_freertos/public_overrides/pw_thread_backend/thread_inline.h)0
-rw-r--r--pw_thread_freertos/thread_public_overrides/pw_thread_backend/thread_native.h (renamed from pw_thread_freertos/public_overrides/pw_thread_backend/thread_native.h)0
-rw-r--r--pw_thread_freertos/yield_public_overrides/pw_thread_backend/yield_inline.h (renamed from pw_thread_freertos/public_overrides/pw_thread_backend/yield_inline.h)0
-rw-r--r--pw_thread_stl/BUILD.bazel32
-rw-r--r--pw_thread_stl/BUILD.gn50
-rw-r--r--pw_thread_stl/CMakeLists.txt49
-rw-r--r--pw_thread_stl/id_public_overrides/pw_thread_backend/id_inline.h (renamed from pw_thread_stl/public_overrides/pw_thread_backend/id_inline.h)0
-rw-r--r--pw_thread_stl/id_public_overrides/pw_thread_backend/id_native.h (renamed from pw_thread_stl/public_overrides/pw_thread_backend/id_native.h)0
-rw-r--r--pw_thread_stl/public/pw_thread_stl/options.h5
-rw-r--r--pw_thread_stl/public/pw_thread_stl/sleep_inline.h1
-rw-r--r--pw_thread_stl/public/pw_thread_stl/thread_inline.h2
-rw-r--r--pw_thread_stl/public/pw_thread_stl/yield_inline.h2
-rw-r--r--pw_thread_stl/sleep_public_overrides/pw_thread_backend/sleep_inline.h (renamed from pw_thread_stl/public_overrides/pw_thread_backend/sleep_inline.h)0
-rw-r--r--pw_thread_stl/thread_iteration.cc27
-rw-r--r--pw_thread_stl/thread_public_overrides/pw_thread_backend/thread_inline.h (renamed from pw_thread_stl/public_overrides/pw_thread_backend/thread_inline.h)0
-rw-r--r--pw_thread_stl/thread_public_overrides/pw_thread_backend/thread_native.h (renamed from pw_thread_stl/public_overrides/pw_thread_backend/thread_native.h)0
-rw-r--r--pw_thread_stl/yield_public_overrides/pw_thread_backend/yield_inline.h (renamed from pw_thread_stl/public_overrides/pw_thread_backend/yield_inline.h)0
-rw-r--r--pw_thread_threadx/BUILD.bazel55
-rw-r--r--pw_thread_threadx/BUILD.gn41
-rw-r--r--pw_thread_threadx/docs.rst11
-rw-r--r--pw_thread_threadx/id_public_overrides/pw_thread_backend/id_inline.h (renamed from pw_thread_threadx/public_overrides/pw_thread_backend/id_inline.h)0
-rw-r--r--pw_thread_threadx/id_public_overrides/pw_thread_backend/id_native.h (renamed from pw_thread_threadx/public_overrides/pw_thread_backend/id_native.h)0
-rw-r--r--pw_thread_threadx/public/pw_thread_threadx/context.h9
-rw-r--r--pw_thread_threadx/public/pw_thread_threadx/id_inline.h2
-rw-r--r--pw_thread_threadx/public/pw_thread_threadx/options.h8
-rw-r--r--pw_thread_threadx/public/pw_thread_threadx/snapshot.h6
-rw-r--r--pw_thread_threadx/sleep_public_overrides/pw_thread_backend/sleep_inline.h (renamed from pw_thread_threadx/public_overrides/pw_thread_backend/sleep_inline.h)0
-rw-r--r--pw_thread_threadx/snapshot.cc26
-rw-r--r--pw_thread_threadx/thread_public_overrides/pw_thread_backend/thread_inline.h (renamed from pw_thread_threadx/public_overrides/pw_thread_backend/thread_inline.h)0
-rw-r--r--pw_thread_threadx/thread_public_overrides/pw_thread_backend/thread_native.h (renamed from pw_thread_threadx/public_overrides/pw_thread_backend/thread_native.h)0
-rw-r--r--pw_thread_threadx/yield_public_overrides/pw_thread_backend/yield_inline.h (renamed from pw_thread_threadx/public_overrides/pw_thread_backend/yield_inline.h)0
-rw-r--r--pw_thread_zephyr/BUILD.gn25
-rw-r--r--pw_thread_zephyr/CMakeLists.txt30
-rw-r--r--pw_thread_zephyr/Kconfig18
-rw-r--r--pw_thread_zephyr/docs.rst8
-rw-r--r--pw_thread_zephyr/public/pw_thread_zephyr/sleep_inline.h28
-rw-r--r--pw_thread_zephyr/sleep.cc51
-rw-r--r--pw_thread_zephyr/sleep_public_overrides/pw_thread_backend/sleep_inline.h16
-rw-r--r--pw_tls_client/BUILD.bazel14
-rw-r--r--pw_tls_client/BUILD.gn20
-rw-r--r--pw_tls_client/docs.rst8
-rw-r--r--pw_tls_client/generate_build_time_header.py15
-rw-r--r--pw_tls_client/public/pw_tls_client/entropy.h4
-rw-r--r--pw_tls_client/public/pw_tls_client/session.h2
-rw-r--r--pw_tls_client/public/pw_tls_client/test/test_server.h11
-rw-r--r--pw_tls_client/py/BUILD.gn1
-rw-r--r--pw_tls_client/py/pw_tls_client/generate_test_data.py148
-rw-r--r--pw_tls_client/py/setup.cfg3
-rw-r--r--pw_tls_client/test_server.cc14
-rw-r--r--pw_tls_client/test_server_test.cc2
-rw-r--r--pw_tls_client_boringssl/BUILD.bazel2
-rw-r--r--pw_tls_client_boringssl/tls_client_boringssl.cc14
-rw-r--r--pw_tls_client_mbedtls/BUILD.bazel9
-rw-r--r--pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h6
-rw-r--r--pw_tls_client_mbedtls/tls_client_mbedtls.cc13
-rw-r--r--pw_tokenizer/Android.bp37
-rw-r--r--pw_tokenizer/BUILD.bazel62
-rw-r--r--pw_tokenizer/BUILD.gn166
-rw-r--r--pw_tokenizer/CMakeLists.txt81
-rw-r--r--pw_tokenizer/Kconfig33
-rw-r--r--pw_tokenizer/argument_types_test.cc1
-rw-r--r--pw_tokenizer/argument_types_test_c.c11
-rw-r--r--pw_tokenizer/backend.gni7
-rw-r--r--pw_tokenizer/base64.cc14
-rw-r--r--pw_tokenizer/base64_test.cc53
-rw-r--r--pw_tokenizer/database.gni13
-rw-r--r--pw_tokenizer/decode.cc24
-rw-r--r--pw_tokenizer/detokenize.cc26
-rw-r--r--pw_tokenizer/detokenize_fuzzer.cc2
-rw-r--r--pw_tokenizer/detokenize_test.cc31
-rw-r--r--pw_tokenizer/docs.rst1398
-rw-r--r--pw_tokenizer/encode_args.cc24
-rw-r--r--pw_tokenizer/encode_args_test.cc42
-rw-r--r--pw_tokenizer/generate_decoding_test_data.cc13
-rw-r--r--pw_tokenizer/global_handlers_test.cc299
-rw-r--r--pw_tokenizer/global_handlers_test_c.c64
-rw-r--r--pw_tokenizer/hash.cc10
-rw-r--r--pw_tokenizer/java/dev/pigweed/tokenizer/detokenizer.cc4
-rw-r--r--pw_tokenizer/options.proto2
-rw-r--r--pw_tokenizer/public/pw_tokenizer/base64.h74
-rw-r--r--pw_tokenizer/public/pw_tokenizer/config.h10
-rw-r--r--pw_tokenizer/public/pw_tokenizer/detokenize.h13
-rw-r--r--pw_tokenizer/public/pw_tokenizer/encode_args.h158
-rw-r--r--pw_tokenizer/public/pw_tokenizer/hash.h10
-rw-r--r--pw_tokenizer/public/pw_tokenizer/internal/argument_types.h10
-rw-r--r--pw_tokenizer/public/pw_tokenizer/internal/decode.h18
-rw-r--r--pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h2
-rw-r--r--pw_tokenizer/public/pw_tokenizer/tokenize.h213
-rw-r--r--pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler.h77
-rw-r--r--pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h61
-rw-r--r--pw_tokenizer/pw_tokenizer_linker_rules.ld55
-rw-r--r--pw_tokenizer/pw_tokenizer_linker_sections.ld34
-rw-r--r--pw_tokenizer/pw_tokenizer_private/tokenize_test.h4
-rw-r--r--pw_tokenizer/py/BUILD.bazel156
-rw-r--r--pw_tokenizer/py/BUILD.gn7
-rwxr-xr-xpw_tokenizer/py/database_test.py444
-rwxr-xr-xpw_tokenizer/py/decode_test.py1735
-rw-r--r--pw_tokenizer/py/detokenize_proto_test.py67
-rwxr-xr-xpw_tokenizer/py/detokenize_test.py367
-rwxr-xr-xpw_tokenizer/py/elf_reader_test.py218
-rwxr-xr-xpw_tokenizer/py/encode_test.py72
-rwxr-xr-xpw_tokenizer/py/example_legacy_binary_with_tokenized_strings.elfbin1184 -> 0 bytes
-rwxr-xr-xpw_tokenizer/py/generate_argument_types_macro.py24
-rwxr-xr-xpw_tokenizer/py/generate_hash_macro.py62
-rwxr-xr-xpw_tokenizer/py/generate_hash_test_data.py41
-rwxr-xr-xpw_tokenizer/py/pw_tokenizer/database.py406
-rw-r--r--pw_tokenizer/py/pw_tokenizer/decode.py624
-rwxr-xr-xpw_tokenizer/py/pw_tokenizer/detokenize.py291
-rwxr-xr-xpw_tokenizer/py/pw_tokenizer/elf_reader.py127
-rw-r--r--pw_tokenizer/py/pw_tokenizer/encode.py108
-rw-r--r--pw_tokenizer/py/pw_tokenizer/parse_message.py114
-rw-r--r--pw_tokenizer/py/pw_tokenizer/proto/__init__.py29
-rw-r--r--pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py73
-rw-r--r--pw_tokenizer/py/pw_tokenizer/tokens.py444
-rw-r--r--pw_tokenizer/py/setup.cfg3
-rw-r--r--pw_tokenizer/py/tokenized_string_decoding_test_data.py3
-rwxr-xr-xpw_tokenizer/py/tokens_test.py668
-rw-r--r--pw_tokenizer/py/varint_test_data.py3
-rw-r--r--pw_tokenizer/simple_tokenize_test.cc129
-rw-r--r--pw_tokenizer/size_report/BUILD.bazel67
-rw-r--r--pw_tokenizer/size_report/BUILD.gn61
-rw-r--r--pw_tokenizer/size_report/base.h57
-rw-r--r--pw_tokenizer/size_report/tokenize_string.cc31
-rw-r--r--pw_tokenizer/size_report/tokenize_string_expr.cc31
-rw-r--r--pw_tokenizer/token_database_fuzzer.cc8
-rw-r--r--pw_tokenizer/tokenize.cc26
-rw-r--r--pw_tokenizer/tokenize_test.cc180
-rw-r--r--pw_tokenizer/tokenize_test_c.c20
-rw-r--r--pw_tokenizer/tokenize_to_global_handler.cc34
-rw-r--r--pw_tokenizer/ts/detokenizer.ts139
-rw-r--r--pw_tokenizer/ts/detokenizer_test.ts80
-rw-r--r--pw_tokenizer/ts/index.ts (renamed from pw_polyfill/public_overrides/cstddef)8
-rw-r--r--pw_tokenizer/ts/int_testdata.ts52
-rw-r--r--pw_tokenizer/ts/printf_decoder.ts170
-rw-r--r--pw_tokenizer/ts/printf_decoder_test.ts122
-rw-r--r--pw_tokenizer/ts/token_database.ts57
-rw-r--r--pw_tool/BUILD.gn10
-rw-r--r--pw_tool/docs.rst9
-rw-r--r--pw_tool/main.cc8
-rw-r--r--pw_toolchain/Android.bp27
-rw-r--r--pw_toolchain/BUILD.bazel50
-rw-r--r--pw_toolchain/BUILD.gn33
-rw-r--r--pw_toolchain/CMakeLists.txt43
-rw-r--r--pw_toolchain/arm_clang/BUILD.gn58
-rw-r--r--pw_toolchain/arm_clang/toolchains.gni148
-rw-r--r--pw_toolchain/arm_gcc/BUILD.bazel42
-rw-r--r--pw_toolchain/arm_gcc/BUILD.gn30
-rw-r--r--pw_toolchain/arm_gcc/CMakeLists.txt33
-rw-r--r--pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc103
-rw-r--r--pw_toolchain/arm_gcc/toolchains.gni30
-rw-r--r--pw_toolchain/c_optimization.gni20
-rw-r--r--pw_toolchain/clang_tools.gni50
-rw-r--r--pw_toolchain/docs.rst289
-rw-r--r--pw_toolchain/generate_toolchain.gni75
-rw-r--r--pw_toolchain/host_clang/BUILD.gn13
-rw-r--r--pw_toolchain/host_clang/toolchain.cmake45
-rw-r--r--pw_toolchain/host_clang/toolchains.gni89
-rw-r--r--pw_toolchain/host_gcc/toolchain.cmake22
-rw-r--r--pw_toolchain/no_destructor_test.cc87
-rw-r--r--pw_toolchain/public/pw_toolchain/no_destructor.h122
-rw-r--r--pw_toolchain/py/BUILD.gn1
-rw-r--r--pw_toolchain/py/clang_tidy_test.py37
-rw-r--r--pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py94
-rw-r--r--pw_toolchain/py/pw_toolchain/clang_tidy.py135
-rw-r--r--pw_toolchain/rbe.gni31
-rw-r--r--pw_toolchain/rust_toolchain.bzl48
-rw-r--r--pw_toolchain/static_analysis_toolchain.gni73
-rw-r--r--pw_toolchain/traits.gni57
-rw-r--r--pw_toolchain/wrap_abort.cc26
-rw-r--r--pw_trace/BUILD.bazel59
-rw-r--r--pw_trace/BUILD.gn31
-rw-r--r--pw_trace/CMakeLists.txt15
-rw-r--r--pw_trace/backend.cmake19
-rw-r--r--pw_trace/docs.rst54
-rw-r--r--pw_trace/example/sample_app.cc11
-rw-r--r--pw_trace/public/pw_trace/internal/trace_internal.h332
-rw-r--r--pw_trace/pw_trace_zero/public_overrides/pw_trace_backend/trace_backend.h20
-rw-r--r--pw_trace/py/BUILD.gn1
-rwxr-xr-xpw_trace/py/pw_trace/trace.py82
-rwxr-xr-xpw_trace/py/trace_test.py352
-rw-r--r--pw_trace/trace_backend_compile_test.cc180
-rw-r--r--pw_trace/trace_backend_compile_test_c.c94
-rw-r--r--pw_trace/trace_facade_test.cc50
-rw-r--r--pw_trace_tokenized/BUILD.bazel100
-rw-r--r--pw_trace_tokenized/BUILD.gn33
-rw-r--r--pw_trace_tokenized/CMakeLists.txt37
-rw-r--r--pw_trace_tokenized/OWNERS2
-rw-r--r--pw_trace_tokenized/docs.rst40
-rw-r--r--pw_trace_tokenized/example/filter.cc12
-rw-r--r--pw_trace_tokenized/example/linux_group_by_tid.cc100
-rw-r--r--pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h5
-rw-r--r--pw_trace_tokenized/example/trigger.cc15
-rw-r--r--pw_trace_tokenized/linux_config_overrides.h30
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/internal/trace_tokenized_internal.h13
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h27
-rw-r--r--pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h2
-rw-r--r--pw_trace_tokenized/py/BUILD.gn2
-rwxr-xr-xpw_trace_tokenized/py/pw_trace_tokenized/get_trace.py116
-rwxr-xr-xpw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py86
-rw-r--r--pw_trace_tokenized/py/setup.cfg5
-rw-r--r--pw_trace_tokenized/trace.cc120
-rw-r--r--pw_trace_tokenized/trace_buffer.cc17
-rw-r--r--pw_trace_tokenized/trace_buffer_log.cc14
-rw-r--r--pw_trace_tokenized/trace_buffer_log_test.cc16
-rw-r--r--pw_trace_tokenized/trace_buffer_test.cc4
-rw-r--r--pw_trace_tokenized/trace_rpc_service_nanopb.cc9
-rw-r--r--pw_trace_tokenized/trace_test.cc60
-rw-r--r--pw_transfer/BUILD.bazel72
-rw-r--r--pw_transfer/BUILD.gn154
-rw-r--r--pw_transfer/CMakeLists.txt14
-rw-r--r--pw_transfer/atomic_file_transfer_handler.cc130
-rw-r--r--pw_transfer/atomic_file_transfer_handler_test.cc215
-rw-r--r--pw_transfer/chunk.cc314
-rw-r--r--pw_transfer/chunk_test.cc66
-rw-r--r--pw_transfer/client.cc35
-rw-r--r--pw_transfer/client_test.cc1855
-rw-r--r--pw_transfer/context.cc787
-rw-r--r--pw_transfer/docs.rst508
-rw-r--r--pw_transfer/integration_test.cc258
-rw-r--r--pw_transfer/integration_test/BUILD.bazel263
-rw-r--r--pw_transfer/integration_test/JavaClient.java291
-rw-r--r--pw_transfer/integration_test/client.cc221
-rw-r--r--pw_transfer/integration_test/config.proto225
-rw-r--r--pw_transfer/integration_test/cross_language_large_read_test.py128
-rw-r--r--pw_transfer/integration_test/cross_language_large_write_test.py128
-rw-r--r--pw_transfer/integration_test/cross_language_medium_read_test.py156
-rw-r--r--pw_transfer/integration_test/cross_language_medium_write_test.py150
-rw-r--r--pw_transfer/integration_test/cross_language_small_test.py140
-rw-r--r--pw_transfer/integration_test/expected_errors_test.py336
-rw-r--r--pw_transfer/integration_test/legacy_binaries_test.py279
-rw-r--r--pw_transfer/integration_test/multi_transfer_test.py125
-rw-r--r--pw_transfer/integration_test/proxy.py637
-rw-r--r--pw_transfer/integration_test/proxy_test.py261
-rw-r--r--pw_transfer/integration_test/python_client.py156
-rw-r--r--pw_transfer/integration_test/server.cc237
-rw-r--r--pw_transfer/integration_test/test_fixture.py625
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel44
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/ProtocolVersion.java34
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java206
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java533
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java159
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferError.java46
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java321
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferParameters.java41
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferProgress.java65
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferService.java (renamed from pw_assert_log/public/pw_assert_log/assert_lite_log.h)28
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/TransferTimeoutSettings.java70
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java208
-rw-r--r--pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java189
-rw-r--r--pw_transfer/java/test/dev/pigweed/pw_transfer/BUILD.bazel45
-rw-r--r--pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java2511
-rw-r--r--pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h58
-rw-r--r--pw_transfer/public/pw_transfer/client.h49
-rw-r--r--pw_transfer/public/pw_transfer/handler.h73
-rw-r--r--pw_transfer/public/pw_transfer/internal/chunk.h280
-rw-r--r--pw_transfer/public/pw_transfer/internal/client_context.h9
-rw-r--r--pw_transfer/public/pw_transfer/internal/config.h20
-rw-r--r--pw_transfer/public/pw_transfer/internal/context.h130
-rw-r--r--pw_transfer/public/pw_transfer/internal/event.h49
-rw-r--r--pw_transfer/public/pw_transfer/internal/protocol.h46
-rw-r--r--pw_transfer/public/pw_transfer/internal/server_context.h6
-rw-r--r--pw_transfer/public/pw_transfer/transfer.h25
-rw-r--r--pw_transfer/public/pw_transfer/transfer_thread.h129
-rw-r--r--pw_transfer/pw_transfer_private/chunk_testing.h10
-rw-r--r--pw_transfer/pw_transfer_private/filename_generator.h32
-rw-r--r--pw_transfer/py/BUILD.bazel (renamed from pw_protobuf_compiler/ts/codegen/BUILD.bazel)39
-rw-r--r--pw_transfer/py/BUILD.gn26
-rw-r--r--pw_transfer/py/pw_transfer/__init__.py8
-rw-r--r--pw_transfer/py/pw_transfer/chunk.py236
-rw-r--r--pw_transfer/py/pw_transfer/client.py245
-rw-r--r--pw_transfer/py/pw_transfer/transfer.py649
-rwxr-xr-xpw_transfer/py/tests/python_cpp_transfer_test.py212
-rw-r--r--pw_transfer/py/tests/transfer_test.py1186
-rw-r--r--pw_transfer/read.svg20
-rw-r--r--pw_transfer/test_rpc_server.cc70
-rw-r--r--pw_transfer/transfer.cc28
-rw-r--r--pw_transfer/transfer.proto59
-rw-r--r--pw_transfer/transfer_test.cc2180
-rw-r--r--pw_transfer/transfer_thread.cc243
-rw-r--r--pw_transfer/transfer_thread_test.cc233
-rw-r--r--pw_transfer/ts/BUILD.bazel74
-rw-r--r--pw_transfer/ts/client.ts24
-rw-r--r--pw_transfer/ts/package.json14
-rw-r--r--pw_transfer/ts/transfer.ts19
-rw-r--r--pw_transfer/ts/transfer_test.ts62
-rw-r--r--pw_transfer/ts/tsconfig.json35
-rw-r--r--pw_transfer/write.svg24
-rw-r--r--pw_unit_test/BUILD.bazel133
-rw-r--r--pw_unit_test/BUILD.gn187
-rw-r--r--pw_unit_test/CMakeLists.txt75
-rw-r--r--pw_unit_test/README.md2
-rw-r--r--pw_unit_test/docs.rst645
-rw-r--r--pw_unit_test/facade_test.gni6
-rw-r--r--pw_unit_test/framework.cc11
-rw-r--r--pw_unit_test/framework_test.cc11
-rw-r--r--pw_unit_test/googletest_handler_adapter.cc79
-rw-r--r--pw_unit_test/googletest_style_event_handler.cc141
-rw-r--r--pw_unit_test/logging_event_handler.cc97
-rw-r--r--pw_unit_test/logging_main.cc2
-rw-r--r--pw_unit_test/printf_main.cc22
-rw-r--r--pw_unit_test/public/pw_unit_test/event_handler.h41
-rw-r--r--pw_unit_test/public/pw_unit_test/googletest_handler_adapter.h41
-rw-r--r--pw_unit_test/public/pw_unit_test/googletest_style_event_handler.h90
-rw-r--r--pw_unit_test/public/pw_unit_test/internal/framework.h (renamed from pw_unit_test/public/pw_unit_test/framework.h)108
-rw-r--r--pw_unit_test/public/pw_unit_test/logging_event_handler.h6
-rw-r--r--pw_unit_test/public/pw_unit_test/printf_event_handler.h44
-rw-r--r--pw_unit_test/public/pw_unit_test/simple_printing_event_handler.h23
-rw-r--r--pw_unit_test/public/pw_unit_test/static_library_support.h57
-rw-r--r--pw_unit_test/public/pw_unit_test/unit_test_service.h5
-rw-r--r--pw_unit_test/public_overrides/gtest/gtest.h2
-rw-r--r--pw_unit_test/py/BUILD.bazel31
-rw-r--r--pw_unit_test/py/BUILD.gn2
-rw-r--r--pw_unit_test/py/pw_unit_test/__init__.py8
-rw-r--r--pw_unit_test/py/pw_unit_test/rpc.py88
-rw-r--r--pw_unit_test/py/pw_unit_test/test_runner.py262
-rwxr-xr-xpw_unit_test/py/rpc_service_test.py58
-rw-r--r--pw_unit_test/py/setup.cfg4
-rw-r--r--pw_unit_test/py/test_group_metadata_test.py88
-rw-r--r--pw_unit_test/rpc_gtest_event_handler.cc106
-rw-r--r--pw_unit_test/rpc_gtest_public/pw_unit_test/internal/rpc_event_handler.h44
-rw-r--r--pw_unit_test/rpc_light_event_handler.cc (renamed from pw_unit_test/rpc_event_handler.cc)17
-rw-r--r--pw_unit_test/rpc_light_public/pw_unit_test/internal/rpc_event_handler.h (renamed from pw_unit_test/public/pw_unit_test/internal/rpc_event_handler.h)13
-rw-r--r--pw_unit_test/simple_printing_event_handler.cc60
-rw-r--r--pw_unit_test/simple_printing_main.cc8
-rw-r--r--pw_unit_test/static_library_archived_tests.cc29
-rw-r--r--pw_unit_test/static_library_missing_archived_tests.cc35
-rw-r--r--pw_unit_test/static_library_support.cc23
-rw-r--r--pw_unit_test/static_library_support_test.cc57
-rw-r--r--pw_unit_test/test.cmake345
-rw-r--r--pw_unit_test/test.gni257
-rw-r--r--pw_unit_test/unit_test_service.cc56
-rw-r--r--pw_varint/Android.bp31
-rw-r--r--pw_varint/BUILD.gn12
-rw-r--r--pw_varint/CMakeLists.txt12
-rw-r--r--pw_varint/public/pw_varint/stream.h12
-rw-r--r--pw_varint/public/pw_varint/varint.h25
-rw-r--r--pw_varint/stream.cc30
-rw-r--r--pw_varint/stream_test.cc59
-rw-r--r--pw_varint/varint.cc1
-rw-r--r--pw_varint/varint_test.cc81
-rw-r--r--pw_watch/BUILD.gn9
l---------pw_watch/doc_resources/pw_watch_test_demo2.gif1
-rw-r--r--pw_watch/docs.rst143
-rw-r--r--pw_watch/py/BUILD.gn8
-rw-r--r--pw_watch/py/pw_watch/argparser.py124
-rw-r--r--pw_watch/py/pw_watch/debounce.py50
-rwxr-xr-xpw_watch/py/pw_watch/watch.py1120
-rw-r--r--pw_watch/py/pw_watch/watch_app.py830
-rw-r--r--pw_watch/py/setup.cfg3
-rwxr-xr-xpw_watch/py/watch_test.py57
-rw-r--r--pw_web/BUILD.gn (renamed from pw_web_ui/BUILD.gn)4
-rw-r--r--pw_web/README.md1
-rw-r--r--pw_web/docs.rst199
-rw-r--r--pw_web/webconsole/.eslintrc.json3
-rw-r--r--pw_web/webconsole/.gitignore36
-rw-r--r--pw_web/webconsole/common/device.ts30
-rw-r--r--pw_web/webconsole/common/logService.ts26
-rw-r--r--pw_web/webconsole/common/protos.ts20
-rw-r--r--pw_web/webconsole/components/connect.tsx35
-rw-r--r--pw_web/webconsole/components/log.tsx161
-rw-r--r--pw_web/webconsole/components/repl/autocomplete.ts107
-rw-r--r--pw_web/webconsole/components/repl/basicSetup.ts69
-rw-r--r--pw_web/webconsole/components/repl/index.tsx202
-rw-r--r--pw_web/webconsole/components/repl/localStorageArray.ts40
-rw-r--r--pw_web/webconsole/components/uploadDb.tsx79
-rw-r--r--pw_web/webconsole/next.config.js21
-rw-r--r--pw_web/webconsole/package.json42
-rw-r--r--pw_web/webconsole/pages/_app.tsx35
-rw-r--r--pw_web/webconsole/pages/_document.tsx39
-rw-r--r--pw_web/webconsole/pages/index.tsx61
-rw-r--r--pw_web/webconsole/public/favicon.pngbin0 -> 502 bytes
-rw-r--r--pw_web/webconsole/styles/Home.module.scss75
-rw-r--r--pw_web/webconsole/styles/globals.css41
-rw-r--r--pw_web/webconsole/styles/log.module.css18
-rw-r--r--pw_web/webconsole/styles/repl.module.css92
-rw-r--r--pw_web/webconsole/tsconfig.json20
-rw-r--r--pw_web_ui/BUILD.bazel120
-rw-r--r--pw_web_ui/README.md1
-rw-r--r--pw_web_ui/docs.rst11
-rw-r--r--pw_web_ui/index.html24
-rw-r--r--pw_web_ui/index.ts16
-rw-r--r--pw_web_ui/package.json14
-rw-r--r--pw_web_ui/rollup.config.js32
-rw-r--r--pw_web_ui/src/frontend/app.tsx190
-rw-r--r--pw_web_ui/src/frontend/log.tsx52
-rw-r--r--pw_web_ui/src/frontend/rpc.tsx51
-rw-r--r--pw_web_ui/src/frontend/serial_log.tsx64
-rw-r--r--pw_web_ui/tsconfig.json34
-rw-r--r--pw_web_ui/yarn.lock359
-rw-r--r--pw_work_queue/BUILD.bazel4
-rw-r--r--pw_work_queue/BUILD.gn1
-rw-r--r--pw_work_queue/CMakeLists.txt77
-rw-r--r--pw_work_queue/public/pw_work_queue/internal/circular_buffer.h8
-rw-r--r--pw_work_queue/public/pw_work_queue/work_queue.h4
-rw-r--r--pw_work_queue/work_queue_test.cc57
-rw-r--r--rollup.config.js116
-rw-r--r--seed/0000-index.rst14
-rw-r--r--seed/0001-the-seed-process.rst412
-rw-r--r--seed/0001-the-seed-process/seed-index-gerrit.pngbin0 -> 63567 bytes
-rw-r--r--seed/0002-template.rst117
-rw-r--r--seed/0101-pigweed.json.rst210
-rw-r--r--seed/0102-module-docs.rst203
-rw-r--r--seed/BUILD.gn44
-rw-r--r--targets/arduino/BUILD.bazel15
-rw-r--r--targets/arduino/BUILD.gn11
-rw-r--r--targets/arduino/system_rpc_server.cc71
-rw-r--r--targets/arduino/target_toolchains.gni4
-rw-r--r--targets/default_config.BUILD54
-rw-r--r--targets/docs/BUILD.bazel24
-rw-r--r--targets/docs/BUILD.gn25
-rw-r--r--targets/docs/target_docs.rst3
-rw-r--r--targets/docs/tokenized_log_handler.cc24
-rw-r--r--targets/emcraft_sf2_som/BUILD.bazel9
-rw-r--r--targets/emcraft_sf2_som/BUILD.gn122
-rw-r--r--targets/emcraft_sf2_som/OWNERS1
-rw-r--r--targets/emcraft_sf2_som/boot.cc9
-rw-r--r--targets/emcraft_sf2_som/config/sf2_mss_hal_conf.h4
-rw-r--r--targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld302
-rw-r--r--targets/emcraft_sf2_som/target_docs.rst19
-rw-r--r--targets/emcraft_sf2_som/vector_table.c3
-rw-r--r--targets/host/CMakeLists.txt11
-rw-r--r--targets/host/pw_add_test_executable.cmake54
-rw-r--r--targets/host/system_rpc_server.cc60
-rw-r--r--targets/host/target_docs.rst14
-rw-r--r--targets/host/target_toolchains.gni109
-rw-r--r--targets/host_device_simulator/BUILD.bazel34
-rw-r--r--targets/host_device_simulator/BUILD.gn12
-rw-r--r--targets/host_device_simulator/OWNERS1
-rw-r--r--targets/host_device_simulator/boot.cc35
-rw-r--r--targets/host_device_simulator/target_docs.rst102
-rw-r--r--targets/lm3s6965evb_qemu/BUILD.gn6
-rw-r--r--targets/lm3s6965evb_qemu/py/BUILD.gn1
-rwxr-xr-xtargets/lm3s6965evb_qemu/py/lm3s6965evb_qemu_utils/unit_test_runner.py11
-rw-r--r--targets/lm3s6965evb_qemu/py/setup.cfg3
-rw-r--r--targets/lm3s6965evb_qemu/target_toolchains.gni10
-rw-r--r--targets/mimxrt595_evk/BUILD.bazel9
-rw-r--r--targets/mimxrt595_evk/BUILD.gn11
-rw-r--r--targets/mimxrt595_evk/OWNERS1
-rw-r--r--targets/mimxrt595_evk/mimxrt595_flash.ld103
-rw-r--r--targets/mimxrt595_evk/target_toolchains.gni4
-rw-r--r--targets/rp2040/BUILD.bazel3
-rw-r--r--targets/rp2040/BUILD.gn12
-rw-r--r--targets/rp2040/OWNERS2
-rw-r--r--targets/rp2040/pico_executable.gni37
-rw-r--r--targets/rp2040/pico_logging_test_main.cc16
-rw-r--r--targets/rp2040/target_docs.rst102
-rw-r--r--targets/rp2040_pw_system/BUILD.bazel29
-rw-r--r--targets/rp2040_pw_system/BUILD.gn87
-rw-r--r--targets/rp2040_pw_system/OWNERS2
-rw-r--r--targets/rp2040_pw_system/boot.cc77
-rw-r--r--targets/rp2040_pw_system/config/FreeRTOSConfig.h93
-rw-r--r--targets/rp2040_pw_system/config/rp2040_hal_config.h16
-rw-r--r--targets/rp2040_pw_system/target_docs.rst170
-rw-r--r--targets/stm32f429i_disc1/BUILD.bazel38
-rw-r--r--targets/stm32f429i_disc1/BUILD.gn11
-rw-r--r--targets/stm32f429i_disc1/boot.cc5
-rw-r--r--targets/stm32f429i_disc1/py/BUILD.gn1
-rw-r--r--targets/stm32f429i_disc1/py/setup.cfg5
-rw-r--r--targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/BUILD.bazel18
-rw-r--r--targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py30
-rwxr-xr-xtargets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_client.py6
-rwxr-xr-xtargets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py160
-rw-r--r--targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_server.py46
-rw-r--r--targets/stm32f429i_disc1/system_rpc_server.cc71
-rw-r--r--targets/stm32f429i_disc1/target_toolchains.gni16
-rw-r--r--targets/stm32f429i_disc1_stm32cube/BUILD.bazel42
-rw-r--r--targets/stm32f429i_disc1_stm32cube/BUILD.gn5
-rw-r--r--targets/stm32f429i_disc1_stm32cube/OWNERS1
-rw-r--r--targets/stm32f429i_disc1_stm32cube/config/FreeRTOSConfig.h1
-rw-r--r--targets/stm32f429i_disc1_stm32cube/target_docs.rst94
-rw-r--r--third_party/Android.bp38
-rw-r--r--third_party/boringssl/BUILD.bazel4
-rw-r--r--third_party/boringssl/BUILD.gn27
-rw-r--r--third_party/boringssl/README.md10
-rw-r--r--third_party/boringssl/boringssl.gni6
-rw-r--r--third_party/boringssl/docs.rst78
-rw-r--r--third_party/emboss/BUILD.gn71
-rw-r--r--third_party/emboss/OWNERS2
-rw-r--r--third_party/emboss/build_defs.gni167
-rw-r--r--third_party/emboss/docs.rst63
-rw-r--r--third_party/emboss/emboss.gni25
-rw-r--r--third_party/emboss/embossc_runner.py23
-rw-r--r--third_party/freertos/BUILD.bazel152
-rw-r--r--third_party/freertos/BUILD.gn18
-rw-r--r--third_party/freertos/CMakeLists.txt30
-rw-r--r--third_party/freertos/docs.rst36
-rw-r--r--third_party/fuchsia/BUILD.bazel71
-rw-r--r--third_party/fuchsia/BUILD.gn90
-rw-r--r--third_party/fuchsia/CMakeLists.txt51
-rw-r--r--third_party/fuchsia/OWNERS2
-rw-r--r--third_party/fuchsia/copy.bara.sky72
-rw-r--r--third_party/fuchsia/docs.rst65
-rwxr-xr-xthird_party/fuchsia/generate_fuchsia_patch.py173
-rw-r--r--third_party/fuchsia/pigweed_adaptations.patch276
-rw-r--r--third_party/fuchsia/repo/.clang-format33
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h540
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h21
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h567
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h445
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h138
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h252
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h800
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h181
-rw-r--r--third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc1092
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h185
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h50
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h230
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h100
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h56
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h847
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h112
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h137
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h66
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h475
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h509
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h118
-rw-r--r--third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h79
-rw-r--r--third_party/google_auto/OWNERS1
-rw-r--r--third_party/googletest/BUILD.gn66
-rw-r--r--third_party/googletest/CMakeLists.txt76
-rw-r--r--third_party/googletest/docs.rst44
-rw-r--r--third_party/mbedtls/BUILD.gn13
-rw-r--r--third_party/mbedtls/README.md4
-rw-r--r--third_party/micro_ecc/BUILD.gn40
-rw-r--r--third_party/micro_ecc/BUILD.micro_ecc28
-rw-r--r--third_party/nanopb/BUILD.gn1
-rw-r--r--third_party/nanopb/CMakeLists.txt5
-rw-r--r--third_party/nanopb/generate_nanopb_proto.py3
-rw-r--r--third_party/pico_sdk/OWNERS2
-rw-r--r--third_party/pico_sdk/gn/BUILD.gn6
-rw-r--r--third_party/pico_sdk/gn/generate_config_header.gni6
-rw-r--r--third_party/pico_sdk/src/BUILD.gn22
-rw-r--r--third_party/pico_sdk/src/boards/BUILD.gn22
-rw-r--r--third_party/pico_sdk/src/common/BUILD.gn2
-rw-r--r--third_party/pico_sdk/src/common/boot_picoboot/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/common/boot_uf2/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/common/pico_base/BUILD.gn2
-rw-r--r--third_party/pico_sdk/src/common/pico_base/generate_version_header.gni6
-rw-r--r--third_party/pico_sdk/src/common/pico_binary_info/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/common/pico_bit_ops/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/common/pico_divider/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/common/pico_stdlib/BUILD.gn18
-rw-r--r--third_party/pico_sdk/src/common/pico_stdlib/pico_stdio.gni2
-rw-r--r--third_party/pico_sdk/src/common/pico_sync/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/common/pico_time/BUILD.gn8
-rw-r--r--third_party/pico_sdk/src/common/pico_usb_reset_interface/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/common/pico_util/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2040/hardware_regs/BUILD.gn18
-rw-r--r--third_party/pico_sdk/src/rp2040/hardware_structs/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/BUILD.gn2
-rw-r--r--third_party/pico_sdk/src/rp2_common/boot_stage2/BUILD.gn25
-rw-r--r--third_party/pico_sdk/src/rp2_common/cmsis/BUILD.gn14
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_adc/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_base/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_claim/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_clocks/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_divider/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_dma/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_exception/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_flash/BUILD.gn14
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_gpio/BUILD.gn8
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_i2c/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_interp/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_irq/BUILD.gn7
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_pio/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_pll/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_pwm/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_resets/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_rtc/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_spi/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_sync/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_timer/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_uart/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_vreg/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_watchdog/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/hardware_xosc/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_bootrom/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_bootsel_via_double_reset/BUILD.gn2
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_double/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.gn10
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_float/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_int64_ops/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_malloc/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_mem_ops/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_platform/BUILD.gn8
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_printf/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_runtime/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_standard_link/BUILD.gn2
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_stdio/BUILD.gn12
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_stdio_semihosting/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_stdio_uart/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn27
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_stdlib/BUILD.gn2
-rw-r--r--third_party/pico_sdk/src/rp2_common/pico_unique_id/BUILD.gn6
-rw-r--r--third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn96
-rw-r--r--third_party/rules_proto_grpc/OWNERS1
-rw-r--r--third_party/rules_proto_grpc/internal_proto.bzl309
-rw-r--r--third_party/smartfusion_mss/OWNERS1
-rw-r--r--third_party/smartfusion_mss/README.md3
-rw-r--r--third_party/stm32cube/BUILD.gn14
-rw-r--r--third_party/stm32cube/stm32cube.gni2
-rw-r--r--ts/buildprotos.ts81
-rw-r--r--ts/device/index.ts197
-rw-r--r--ts/device/index_test.ts126
-rw-r--r--ts/index.ts22
-rw-r--r--ts/index_test.ts76
-rw-r--r--ts/transport/device_transport.ts (renamed from pw_web_ui/src/transport/device_transport.ts)0
-rw-r--r--ts/transport/serial_mock.ts (renamed from pw_web_ui/src/transport/serial_mock.ts)12
-rw-r--r--ts/transport/web_serial_transport.ts (renamed from pw_web_ui/src/transport/web_serial_transport.ts)8
-rw-r--r--ts/transport/web_serial_transport_test.ts (renamed from pw_web_ui/src/transport/web_serial_transport_test.ts)25
-rw-r--r--ts/types/serial.d.ts (renamed from pw_web_ui/types/serial.d.ts)4
-rw-r--r--tsconfig.json41
-rw-r--r--yarn.lock5136
2542 files changed, 202642 insertions, 44980 deletions
diff --git a/.allstar/binary_artifacts.yaml b/.allstar/binary_artifacts.yaml
new file mode 100644
index 000000000..676e75023
--- /dev/null
+++ b/.allstar/binary_artifacts.yaml
@@ -0,0 +1,4 @@
+# Ignore reason: Empty binaries used to test ELF parsing
+ignorePaths:
+- pw_tokenizer/py/elf_reader_test_binary.elf
+- pw_tokenizer/py/example_binary_with_tokenized_strings.elf
diff --git a/.bazelignore b/.bazelignore
index db791e1ce..5b3b096ae 100644
--- a/.bazelignore
+++ b/.bazelignore
@@ -2,3 +2,10 @@
.presubmit
.environment
environment
+node_modules
+out
+bazel-bin
+bazel-out
+bazel-pigweed
+bazel-testlogs
+outbazel
diff --git a/.bazelrc b/.bazelrc
index e70ca8c7b..832402187 100644
--- a/.bazelrc
+++ b/.bazelrc
@@ -19,11 +19,30 @@ build --incompatible_enable_cc_toolchain_resolution
coverage --experimental_generate_llvm_lcov
coverage --combined_report=lcov
+# Suppress the DEBUG: log messages from bazel. We get lots of spammy DEBUG:
+# messages from our third-party dependencies.
+build --ui_event_filters=-debug
+
# Enforces consistent action environment variables. This is important to
# address Protobuf's rebuild sensitivity on changes to the environment
# variables.
build --incompatible_strict_action_env
+# Silence compiler warnings from external repositories.
+#
+# This is supported by Bazel's default C++ toolchain, but not yet by
+# rules_cc_toolchain
+# (https://github.com/bazelembedded/rules_cc_toolchain/issues/46).
+build --features=external_include_paths
+
+# TODO(b/269204725): Move the following flags to the toolchain configuration.
+# By default build with C++17.
+build --cxxopt='-std=c++17'
+build --cxxopt="-fno-rtti"
+build --cxxopt="-Wnon-virtual-dtor"
+# Allow uses of the register keyword, which may appear in C headers.
+build --cxxopt="-Wno-register"
+
# This leaks the PATH variable into the Bazel build environment, which
# enables the Python pw_protobuf plugins to find the Python version
# via CIPD and pw_env_setup. This is a partial fix for pwbug/437,
@@ -40,9 +59,6 @@ build:asan-libfuzzer \
--@rules_fuzzing//fuzzing:cc_engine_instrumentation=libfuzzer
build:asan-libfuzzer --@rules_fuzzing//fuzzing:cc_engine_sanitizer=asan
-# Configures toolchain with polyfill headers.
-build --@rules_cc_toolchain_config//:user_defined=//pw_polyfill:toolchain_polyfill_overrides
-
# Specifies desired output mode for running tests.
# Valid values are
# 'summary' to output only test status summary
diff --git a/.black.toml b/.black.toml
new file mode 100644
index 000000000..c9c0c31b3
--- /dev/null
+++ b/.black.toml
@@ -0,0 +1,20 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+[tool.black]
+line-length = 80
+skip_string_normalization = true
+target-version = ['py310']
+include = '\.pyi?$'
+exclude = '^/(out|\.?environment)'
diff --git a/.clang-tidy b/.clang-tidy
index 10f27f397..1e43e0768 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -38,10 +38,12 @@ Checks: >
modernize-replace-disallow-copy-and-assign-macro,
modernize-replace-random-shuffle,
modernize-shrink-to-fit,
+ modernize-unary-static-assert,
modernize-use-bool-literals,
modernize-use-equals-delete,
modernize-use-noexcept,
modernize-use-nullptr,
+ modernize-use-override,
modernize-use-transparent-functors,
modernize-use-uncaught-exceptions,
performance-faster-string-find,
@@ -87,9 +89,6 @@ HeaderFilterRegex: '.*'
# Note: added in c++11
# @ modernize-redundant-void-arg:
# modernize-return-braced-init-list:
-# @ modernize-unary-static-assert:
-# Note: added in c++17
-# The message argument can be omitted when it is empty
# @ modernize-use-auto:
# Advises to use auto when initializing with a cast to
# avoid duplicating the type name
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 000000000..1941b2738
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,64 @@
+# This is the list of commits that should be ignored by 'git blame'. To
+# use it, run the following:
+#
+# git blame --ignore-revs-file .git-blame-ignore-revs
+#
+# To automatically use it when running 'git blame' on this repository, run the
+# following:
+#
+# git config blame.ignoreRevsFile .git-blame-ignore-revs
+#
+# To automatically use it for all repositories, run the following:
+#
+# git config --global blame.ignoreRevsFile .git-blame-ignore-revs
+#
+# If concerned about other repositories using a different name, this is the
+# community standard, and is the file used by GitHub to ignore revisions in
+# their blame view.
+# https://github.blog/changelog/2022-03-24-ignore-commits-in-the-blame-view-beta/
+
+# Change formatting from yapf to black.
+02732d9506b4f66854f23ee064907ef6405b83f3
+1bda37930ca8f8ce3e5e6b79982f3244de51d0f2
+21cfe286308b6a465fcaa90db433bd2f382f683a
+31c492a6119fd0a36e3d233f9354c014f2652b89
+33b390814422db286fdf240bea27e6c09881b911
+3c8ba26bc89ae6762598655fd2133c0c150651df
+3cb4e229a05f720ad1c1c3405597e214680416ec
+44ee7fb1f63d8a9c0a8528857d4eda7e3e117267
+47398dfbbf3ed28a9751d521966e9b75b7897da7
+4d2f06133b301776f8c9a10c8363878a58fb56f0
+503e192e4ae138f623e5463f3435c2afeb9c808b
+5235eda12307db99d5ca0c24830cc1bcaf672145
+58ddd05093ecfb4558a7980f3b707011fd37e06e
+5b01d4fab136839d3d25062ec137ab931e92a5f3
+5b9ec8d5cae24541589ebfe5025b3c7daa6dcf52
+5e52cc3f9a12845bb29798fabf99cfc574692010
+5ffe355fd89b3dbdb11ec77034de9db54950fa2a
+622eb812cc8e73519f5732852e5d977cdcb50a53
+6eafe56722c702adae2996d6404fa3e08f074ffc
+6fb07f7b33f504e51049f0ea62b4700527fc34cf
+7220f7ebed024ee22f79043351709a561c1e393e
+85794e3642c659b6c7e1c9a90b65f5089a01a657
+85b160144ce9165883732974bf0fad4097869def
+873707e66bbac4ef8ac738e4ef45a44cdfbea9ac
+88529bdaf1fee07fae9105ecd845f77fb9597723
+916aaa30bd672d53a73ce650e5e4dd595c87cd12
+91e057b168cfa90797edcd62ac3da5d63bdfb873
+94138bba6b7fc2d7990379523e469494dae1d88f
+972b56c34486dcba43dd399a1cca88dc8dbca132
+9770972320c7b0e0c60f6ed51d88d63d38925d1b
+aa145977c92667b3766ad83c875b4718e77ed4ee
+b2ce4c9c6896d58ce4b4c5d07d798ab98c090a9c
+b94f2276bb7464532d477602a4a22f20fe51084f
+c2006043567754f95c49dcf3d1964904c923a1e2
+d5210ba6685e3676eb7377e930947d677541056d
+d64ba160189672976fbae008f0c332cebfd3b935
+db7d262156172ad6f66d896a3e7107bdd399c1c9
+e91ac5e74a012c71e8fc66162fe2eca9b7b45abd
+ec5b5a196977780aa93a8bd8be2ab5cd39664d70
+efdc28498ee4b6f6495178373227b58e883739b2
+f2097137dc6e9654e10815539d80e84e40e38855
+f84f8325ad5bb84847d2370ba58625c1a74086a7
+fbd772c3ad784ec2c434cfbb0ac9dea91d8648f9
+fdbe9ec4f04ae85e66a4245d3876cb871eff96c0
diff --git a/.gitignore b/.gitignore
index f66dbc962..ed3ae360b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,14 +2,19 @@
compile_commands.json
out/
bazel-*
+outbazel/
.presubmit/
docs/_build
# Editors
+.pw_ide/
+.vscode/*
+!.vscode/OWNERS
+!.vscode/pw_project_*.json
.idea/
+.ijwb/
.project
.cproject
-.vscode
# Clangd directories
.clangd/
/.cache/clangd/
@@ -56,15 +61,6 @@ pw_env_setup/py/oxidizer/build
environment
.environment
build_overrides/pigweed_environment.gni
-# TODO(pwbug/216) Remove following lines in this section.
-# Maybe find a way to delete these files before these lines are removed.
-python*-env/
-.python*-env/
-pw_env_setup/.env_setup.sh
-pw_env_setup/.env_setup.bat
-.cipd
-.cargo
-.bootstrap/
# Web Tools
node_modules
diff --git a/.gn b/.gn
index fc3984a3d..3fe130bfc 100644
--- a/.gn
+++ b/.gn
@@ -15,9 +15,14 @@
buildconfig = "//BUILDCONFIG.gn"
default_args = {
+ # Default all upstream Pigweed toolchains to enable pw::span asserts.
+ pw_span_ENABLE_ASSERTS = true
+
pw_build_PIP_CONSTRAINTS =
[ "//pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list" ]
+ pw_build_PIP_REQUIREMENTS = [ "//pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt" ]
+
# Exclude third-party headers from static analysis.
pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES = [
"third_party/.*",
diff --git a/.mypy.ini b/.mypy.ini
new file mode 100644
index 000000000..6b4d1672a
--- /dev/null
+++ b/.mypy.ini
@@ -0,0 +1,2 @@
+[mypy]
+no_implicit_optional = True
diff --git a/.pw_ide.yaml b/.pw_ide.yaml
new file mode 100644
index 000000000..b06ae6a67
--- /dev/null
+++ b/.pw_ide.yaml
@@ -0,0 +1,3 @@
+config_title: pw_ide
+
+default_target: pw_strict_host_clang_debug
diff --git a/.pylintrc b/.pylintrc
index 1d46aeef9..60a4f9116 100644
--- a/.pylintrc
+++ b/.pylintrc
@@ -1,10 +1,5 @@
[MASTER] # inclusive-language: ignore
-# A comma-separated list of package or module names from where C extensions may
-# be loaded. Extensions are loading into the active Python interpreter and may
-# run arbitrary code.
-extension-pkg-allowlist=mypy
-
# Add files or directories to the blocklist. They should be base names, not
# paths.
ignore=CVS
@@ -28,7 +23,7 @@ limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
-load-plugins=
+load-plugins=pylint.extensions.no_self_use
# Pickle collected data for later comparisons.
persistent=yes
@@ -60,11 +55,23 @@ confidence=
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
-disable=bad-continuation, # Rely on yapf for formatting
+disable=broad-exception-raised,
+ consider-iterating-dictionary,
+ consider-using-f-string,
+ consider-using-generator,
+ consider-using-in,
consider-using-with,
fixme,
- subprocess-run-check,
+ implicit-str-concat,
raise-missing-from,
+ redundant-u-string-prefix,
+ subprocess-run-check,
+ superfluous-parens,
+ unnecessary-lambda-assignment,
+ unspecified-encoding,
+ use-dict-literal,
+ use-list-literal,
+
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
@@ -131,13 +138,6 @@ max-line-length=80
# Maximum number of lines in a module.
max-module-lines=9999
-# List of optional constructs for which whitespace checking is disabled. `dict-
-# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
-# `trailing-comma` allows a space between comma and closing bracket: (a, ).
-# `empty-line` allows space-only lines.
-no-space-check=trailing-comma,
- dict-separator
-
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
@@ -161,7 +161,7 @@ allow-global-unused-variables=yes
callbacks=cb_,
_cb
-# A regular expression matching the name of placeholder variables (i.e.
+# A regular expression matching the name of placeholder variables (i.e.
# expected to not be used). # inclusive-language: ignore
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
@@ -309,7 +309,7 @@ good-names=i,
_
# Include a hint for the correct naming format with invalid-name.
-include-naming-hint=no
+include-naming-hint=yes
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
@@ -511,5 +511,5 @@ preferred-modules=
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
-overgeneral-exceptions=BaseException,
- Exception
+overgeneral-exceptions=builtins.BaseException,
+ builtins.Exception
diff --git a/BUILD.bazel b/BUILD.bazel
index 9d4cb4fce..a7ef535e1 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -12,50 +12,9 @@
# License for the specific language governing permissions and limitations under
# the License.
-load(
- "@com_github_bazelbuild_buildtools//buildifier:def.bzl",
- "buildifier",
- "buildifier_test",
-)
-
licenses(["notice"])
exports_files(
["tsconfig.json"],
visibility = [":__subpackages__"],
)
-
-# Fix all Bazel relevant files.
-buildifier(
- name = "buildifier",
- # Ignore gn and CIPD outputs in formatting.
- # NOTE: These globs are not Bazel native and are passed directly
- # through to the buildifier binary.
- # TODO: Remove these globs when
- # https://github.com/bazelbuild/buildtools/issues/623 is addressed.
- exclude_patterns = [
- "./.environment/**/*",
- "./.presubmit/**/*",
- "./.out/**/*",
- ],
-)
-
-# Test to ensure all Bazel build files are properly formatted.
-buildifier_test(
- name = "buildifier_test",
- srcs = glob(
- [
- "**/*.bazel",
- "**/*.bzl",
- "**/*.oss",
- "**/*.sky",
- "**/BUILD",
- ],
- # Node modules do not play nice with buildifier. Exclude these
- # generated Bazel files from format testing.
- exclude = ["**/node_modules/**/*"],
- ) + ["WORKSPACE"],
- diff_command = "diff -u",
- mode = "diff",
- verbose = True,
-)
diff --git a/BUILD.gn b/BUILD.gn
index 4f11d5bdc..aa97568b9 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -19,36 +19,73 @@ import("$dir_pw_arduino_build/arduino.gni")
import("$dir_pw_build/host_tool.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_perf_test/perf_test.gni")
import("$dir_pw_rpc/config.gni")
+import("$dir_pw_rust/rust.gni")
import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+import("$dir_pw_toolchain/c_optimization.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
+import("$dir_pw_toolchain/non_c_toolchain.gni")
import("$dir_pw_unit_test/test.gni")
# Main build file for upstream Pigweed.
declare_args() {
- # The default optimization level for building upstream Pigweed targets.
+ # The default C++ optimization level for building upstream Pigweed targets.
#
- # Choices:
- # debug
- # size_optimized
- # speed_optimized
- pw_default_optimization_level = "debug"
+ # Must be one of "debug", "size_optimized", or "speed_optimized".
+ pw_DEFAULT_C_OPTIMIZATION_LEVEL = "debug"
+
+ # The C++ optimization levels for which to generate targets.
+ #
+ # Supported levels are "debug", "size_optimized", or "speed_optimized".
+ pw_C_OPTIMIZATION_LEVELS = [
+ "debug",
+ "size_optimized",
+ ]
# List of application image GN targets specific to the Pigweed target.
pw_TARGET_APPLICATIONS = []
+}
- # Whether to run integration tests, which may involve multiple processes
- # communicating over a socket. Integration tests require RPC synchronization.
- pw_RUN_INTEGRATION_TESTS = host_os == "linux"
+# This toolchain is used to force some dependencies to not be parsed by the
+# default toolchain. This is desirable because the default toolchain generates
+# build steps for all parsed targets, not just desired dependencies.
+if (current_toolchain == default_toolchain) {
+ pw_non_c_toolchain("non_default_toolchain") {
+ }
}
+# List any optimization levels in pw_C_OPTIMIZATION_LEVELS that are not in
+# pw_toolchain_SUPPORTED_C_OPTIMIZATION_LEVELS. This is accomplished by adding
+# all supported levels to the selected levels, then removing all supported
+# levels. The remaining list contains only the unsupported levels.
+_unknown_optimization_levels =
+ pw_C_OPTIMIZATION_LEVELS + pw_toolchain_SUPPORTED_C_OPTIMIZATION_LEVELS -
+ pw_toolchain_SUPPORTED_C_OPTIMIZATION_LEVELS
+
+assert(_unknown_optimization_levels == [],
+ "pw_C_OPTIMIZATION_LEVELS includes unsupported optimization levels: " +
+ "$_unknown_optimization_levels")
+
+# Assert that the selected pw_DEFAULT_C_OPTIMIZATION_LEVEL is in the
+# pw_toolchain_SUPPORTED_C_OPTIMIZATION_LEVELS list. This is done by adding then
+# removing all instances of the selected levels from the supported levels list.
+# If the resulting list did not change, then no supported levels were removed,
+# indicating that pw_DEFAULT_C_OPTIMIZATION_LEVEL is not a supported value.
+assert(pw_toolchain_SUPPORTED_C_OPTIMIZATION_LEVELS +
+ [ pw_DEFAULT_C_OPTIMIZATION_LEVEL ] -
+ [ pw_DEFAULT_C_OPTIMIZATION_LEVEL ] !=
+ pw_toolchain_SUPPORTED_C_OPTIMIZATION_LEVELS,
+ "pw_DEFAULT_C_OPTIMIZATION_LEVEL (\"$pw_DEFAULT_C_OPTIMIZATION_LEVEL" +
+ "\") must be one of the supported values: " +
+ "$pw_toolchain_SUPPORTED_C_OPTIMIZATION_LEVELS")
+
# Enumerate all of the different targets that upstream Pigweed will build by
# default. Downstream projects should not depend on this target; this target is
# exclusively to facilitate easy upstream development and testing.
group("default") {
deps = [
- ":check_modules",
":docs",
":host",
":pi_pico",
@@ -56,12 +93,8 @@ group("default") {
":python.tests",
":static_analysis",
":stm32f429i",
- "pw_rpc:test_protos.python.install",
+ ":warn_if_modules_out_of_date",
]
-
- if (pw_RUN_INTEGRATION_TESTS) {
- deps += [ ":integration_tests" ]
- }
}
# Verify that this BUILD.gn file is only used by Pigweed itself.
@@ -69,7 +102,7 @@ assert(get_path_info("//", "abspath") == get_path_info(".", "abspath"),
"Pigweed's top-level BUILD.gn may only be used when building upstream " +
"Pigweed. To pull all Pigweed code into your build, import " +
"\$dir_pigweed/modules.gni and create a top-level pw_test_group " +
- "that depends on the tests in pw_modules_tests. See " +
+ "that depends on the tests in pw_module_tests. See " +
"https://pigweed.dev/build_system.html for details.")
_update_or_check_modules_lists = {
@@ -86,11 +119,30 @@ _update_or_check_modules_lists = {
]
}
-# Check that PIGWEED_MODULES is up-to-date and sorted.
-action("check_modules") {
+# There are races if the module check and module file update are run at the same
+# time. Force them to be serialized to prevent races. As long as the generated
+# module file is up to date, they'll pass.
+pool("module_check_pool") {
+ depth = 1
+}
+
+# Warns if PIGWEED_MODULES is not up-to-date and sorted.
+action("warn_if_modules_out_of_date") {
forward_variables_from(_update_or_check_modules_lists, "*")
outputs = [ "$target_gen_dir/$target_name.passed" ]
- args += [ "--warn-only" ] + rebase_path(outputs, root_build_dir)
+ args += [
+ "--mode=WARN",
+ "--stamp",
+ ] + rebase_path(outputs, root_build_dir)
+ pool = ":module_check_pool"
+}
+
+# Fails if PIGWEED_MODULES is not up-to-date and sorted.
+action("check_modules") {
+ forward_variables_from(_update_or_check_modules_lists, "*")
+ outputs = [ "$target_gen_dir/$target_name.ALWAYS_RERUN" ] # Never created
+ args += [ "--mode=CHECK" ]
+ pool = ":module_check_pool"
}
# Run this command after adding an item to PIGWEED_MODULES to update the
@@ -98,10 +150,12 @@ action("check_modules") {
action("update_modules") {
forward_variables_from(_update_or_check_modules_lists, "*")
outputs = [ "$target_gen_dir/$target_name.ALWAYS_RERUN" ] # Never created
+ args += [ "--mode=UPDATE" ]
+ pool = ":module_check_pool"
}
group("pw_system_demo") {
- deps = [ "$dir_pw_system:system_examples" ]
+ deps = [ "$dir_pw_system:system_examples(:non_default_toolchain)" ]
}
group("pi_pico") {
@@ -119,16 +173,11 @@ template("_build_pigweed_default_at_all_optimization_levels") {
group(target_name) {
deps = [
- ":pigweed_default(${_toolchain_prefix}$pw_default_optimization_level)",
+ ":pigweed_default(${_toolchain_prefix}$pw_DEFAULT_C_OPTIMIZATION_LEVEL)",
]
}
- foreach(optimization,
- [
- "debug",
- "size_optimized",
- "speed_optimized",
- ]) {
+ foreach(optimization, pw_C_OPTIMIZATION_LEVELS) {
group(target_name + "_$optimization") {
deps = [ ":pigweed_default($_toolchain_prefix$optimization)" ]
}
@@ -156,8 +205,13 @@ _build_pigweed_default_at_all_optimization_levels("host_clang") {
toolchain_prefix = "$_internal_toolchains:pw_strict_host_clang_"
}
-_build_pigweed_default_at_all_optimization_levels("host_gcc") {
- toolchain_prefix = "$_internal_toolchains:pw_strict_host_gcc_"
+# GCC is only supported for Windows. Pigweed doesn't yet provide a Windows
+# clang toolchain, and Pigweed does not provide gcc toolchains for macOS and
+# Linux.
+if (host_os == "win") {
+ _build_pigweed_default_at_all_optimization_levels("host_gcc") {
+ toolchain_prefix = "$_internal_toolchains:pw_strict_host_gcc_"
+ }
}
if (pw_third_party_mcuxpresso_SDK != "") {
@@ -181,41 +235,76 @@ _build_pigweed_default_at_all_optimization_levels("qemu_gcc") {
"$dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_gcc_"
}
-_build_pigweed_default_at_all_optimization_levels("qemu_clang") {
- toolchain_prefix =
- "$dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_clang_"
+# TODO(b/244604080): Inline string tests are too big to fit in the QEMU firmware
+# except under a size-optimized build. For now, only build size-optimized.
+#
+# _build_pigweed_default_at_all_optimization_levels("qemu_clang") {
+# toolchain_prefix =
+# "$dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_clang_"
+# }
+group("qemu_clang_size_optimized") {
+ deps = [ ":pigweed_default($dir_pigweed/targets/lm3s6965evb_qemu:lm3s6965evb_qemu_clang_size_optimized)" ]
+}
+group("qemu_clang") {
+ deps = [ ":qemu_clang_size_optimized" ]
}
# Run clang-tidy on pigweed_default with pw_strict_host_clang_debug toolchain options.
# Make sure to invoke gn clean out when any relevant .clang-tidy
# file is updated.
group("static_analysis") {
- _toolchain = "$_internal_toolchains:pw_strict_host_clang_debug"
- deps = [ ":pigweed_default($_toolchain.static_analysis)" ]
+ # Static analysis is only supported on Linux and macOS using clang-tidy.
+ if (host_os != "win") {
+ _toolchain = "$_internal_toolchains:pw_strict_host_clang_debug"
+ deps = [ ":pigweed_default($_toolchain.static_analysis)" ]
+ }
}
group("docs") {
deps = [ "$dir_pigweed/docs($dir_pigweed/targets/docs)" ]
}
-# Tests larger than unit tests, typically run in a specific configuration. Only
-# added if pw_RUN_INTEGRATION_TESTS is true.
+# Tests that are run as host actions, such as tests of the build system.
+#
+# These are distinguished from `integration_tests` in that they are short
+# unit tests of specific functionality that should be tested in the default
+# build.
+group("action_tests") {
+ _default_tc = _default_toolchain_prefix + pw_DEFAULT_C_OPTIMIZATION_LEVEL
+ deps = [ "$dir_pw_unit_test:test_group_metadata_test.action($_default_tc)" ]
+}
+
+# Tests larger than unit tests, typically run in a specific configuration.
group("integration_tests") {
- _default_tc = _default_toolchain_prefix + pw_default_optimization_level
+ _default_tc = _default_toolchain_prefix + pw_DEFAULT_C_OPTIMIZATION_LEVEL
deps = [
+ "$dir_pw_cli/py:process_integration_test.action($_default_tc)",
"$dir_pw_rpc:cpp_client_server_integration_test($_default_tc)",
"$dir_pw_rpc/py:python_client_cpp_server_test.action($_default_tc)",
- "$dir_pw_transfer:cpp_client_integration_test($_default_tc)",
- "$dir_pw_transfer/py:python_cpp_transfer_test.action($_default_tc)",
"$dir_pw_unit_test/py:rpc_service_test.action($_default_tc)",
]
}
-# OSS-Fuzz uses this target to build fuzzers alone.
+# Build-only target for fuzzers.
group("fuzzers") {
- # Fuzzing is only supported on Linux and MacOS using clang.
+ deps = []
+
+ # TODO(b/274437709): The client_fuzzer encounters build errors on macos. Limit
+ # it to Linux hosts for now.
+ if (host_os == "linux") {
+ _default_tc = _default_toolchain_prefix + pw_DEFAULT_C_OPTIMIZATION_LEVEL
+ deps += [ "$dir_pw_rpc/fuzz:client_fuzzer($_default_tc)" ]
+ }
+
if (host_os != "win") {
- deps = [ ":pw_module_tests($dir_pigweed/targets/host:host_clang_fuzz)" ]
+ # Coverage-guided fuzzing is only supported on Linux and MacOS using clang.
+ deps += [
+ "$dir_pw_bluetooth_hci:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ "$dir_pw_fuzzer:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ "$dir_pw_protobuf:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ "$dir_pw_random:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ "$dir_pw_tokenizer:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ ]
}
}
@@ -225,8 +314,11 @@ group("asan") {
}
}
+# TODO(b/234876100): msan will not work until the C++ standard library included
+# in the sysroot has a variant built with msan.
group("msan") {
- if (host_os != "win") {
+ # TODO(b/259695498): msan doesn't work on macOS yet.
+ if (host_os != "win" && host_os != "mac" && host_os != "linux") {
deps = [ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_msan)" ]
}
}
@@ -238,14 +330,16 @@ group("tsan") {
}
group("ubsan") {
- if (host_os != "win") {
+ # TODO(b/259695498): ubsan doesn't work on macOS yet.
+ if (host_os != "win" && host_os != "mac") {
deps =
[ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_ubsan)" ]
}
}
group("ubsan_heuristic") {
- if (host_os != "win") {
+ # TODO(b/259695498): ubsan_heuristic doesn't work on macOS yet.
+ if (host_os != "win" && host_os != "mac") {
deps = [ ":pw_module_tests.run($dir_pigweed/targets/host:host_clang_ubsan_heuristic)" ]
}
}
@@ -254,10 +348,13 @@ group("runtime_sanitizers") {
if (host_os != "win") {
deps = [
":asan",
- ":msan",
":tsan",
":ubsan",
+ # TODO(b/234876100): msan will not work until the C++ standard library
+ # included in the sysroot has a variant built with msan.
+ # ":msan",
+
# No ubsan_heuristic, which may have false positives.
]
}
@@ -265,18 +362,25 @@ group("runtime_sanitizers") {
pw_python_group("python") {
python_deps = [
- "$dir_pigweed/docs:sphinx_themes",
- "$dir_pw_env_setup:python",
- "$dir_pw_env_setup:target_support_packages",
+ "$dir_pw_env_setup:python($pw_build_PYTHON_TOOLCHAIN)",
+ "$dir_pw_env_setup:target_support_packages($pw_build_PYTHON_TOOLCHAIN)",
]
}
+group("pigweed_pypi_distribution") {
+ deps = [ "$dir_pw_env_setup:pypi_pigweed_python_source_tree($pw_build_PYTHON_TOOLCHAIN)" ]
+}
+
# Build host-only tooling.
group("host_tools") {
deps = [
- "$dir_pw_target_runner/go:simple_client",
- "$dir_pw_target_runner/go:simple_server",
+ "$dir_pw_target_runner/go:simple_client(:non_default_toolchain)",
+ "$dir_pw_target_runner/go:simple_server(:non_default_toolchain)",
]
+
+ if (pw_rust_ENABLE_EXPERIMENTAL_BUILD) {
+ deps += [ "$dir_pw_rust/examples/host_executable:hello($dir_pigweed/targets/host:host_clang_debug)" ]
+ }
}
# By default, Pigweed will build this target when invoking ninja.
@@ -294,21 +398,53 @@ group("pigweed_default") {
# build.
deps += [ ":pw_module_tests.run" ]
}
- }
- # Trace examples currently only support running on non-windows host
- if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
- pw_toolchain_SCOPE.is_host_toolchain && host_os != "win") {
- deps += [
- "$dir_pw_trace:trace_example_basic",
- "$dir_pw_trace_tokenized:trace_tokenized_example_basic",
- "$dir_pw_trace_tokenized:trace_tokenized_example_filter",
- "$dir_pw_trace_tokenized:trace_tokenized_example_rpc",
- "$dir_pw_trace_tokenized:trace_tokenized_example_trigger",
- ]
+ # Add performance tests to the automatic build
+ deps += [ ":pw_perf_tests" ]
+
+ # Add action tests to the automatic build
+ deps += [ ":action_tests" ]
+
+ # Trace examples currently only support running on non-windows host
+ if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
+ pw_toolchain_SCOPE.is_host_toolchain && host_os != "win") {
+ deps += [
+ "$dir_pw_trace:trace_example_basic",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_basic",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_filter",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_rpc",
+ "$dir_pw_trace_tokenized:trace_tokenized_example_trigger",
+ ]
+ }
}
}
+group("cpp14_compatibility") {
+ _cpp14_toolchain = "$_internal_toolchains:pw_strict_host_clang_debug_cpp14"
+ deps = [
+ ":cpp14_modules($_cpp14_toolchain)",
+ ":cpp14_tests.run($_cpp14_toolchain)",
+ ]
+}
+
+# Build Pigweed with -std=c++20 to ensure compatibility. Compile with
+# optimizations since the compiler tends to catch more errors with optimizations
+# enabled than without.
+group("cpp20_compatibility") {
+ _cpp20_tc = "$_internal_toolchains:pw_strict_host_clang_size_optimized_cpp20"
+ deps = [ ":pigweed_default($_cpp20_tc)" ]
+}
+
+group("build_with_pw_minimal_cpp_stdlib") {
+ _toolchain = "$_internal_toolchains:pw_strict_host_clang_size_optimized_minimal_cpp_stdlib"
+
+ # This list of supported modules is incomplete.
+ deps = [
+ "$dir_pw_status($_toolchain)",
+ "$dir_pw_tokenizer($_toolchain)",
+ ]
+}
+
# The default toolchain is not used for compiling C/C++ code.
if (current_toolchain != default_toolchain) {
group("apps") {
@@ -318,14 +454,25 @@ if (current_toolchain != default_toolchain) {
# Add target-specific images.
deps += pw_TARGET_APPLICATIONS
- # Add the pw_tool target to be built on host.
+ # Add host-only targets to the build.
if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
pw_toolchain_SCOPE.is_host_toolchain) {
deps += [ "$dir_pw_tool" ]
+
+ # TODO(b/240982565): Build integration tests on Windows and macOS when
+ # SocketStream supports those platforms.
+ if (host_os == "linux") {
+ # Build the integration test binaries, but don't run them by default.
+ deps += [
+ "$dir_pw_rpc:client_integration_test",
+ "$dir_pw_rpc:test_rpc_server",
+ "$dir_pw_unit_test:test_rpc_server",
+ ]
+ }
}
}
- # All Pigweed modules that can be built using gn. This is not built by default.
+ # All Pigweed modules that can be built using gn. Not built by default.
group("pw_modules") {
deps = pw_modules
}
@@ -334,4 +481,36 @@ if (current_toolchain != default_toolchain) {
pw_test_group("pw_module_tests") {
group_deps = pw_module_tests
}
+
+ group("pw_perf_tests") {
+ deps = [
+ "$dir_pw_checksum:perf_tests",
+ "$dir_pw_perf_test:perf_test_tests_test",
+ "$dir_pw_protobuf:perf_tests",
+ ]
+ }
+
+ # Modules that support C++14.
+ # TODO(hepler): pw_kvs is supposed to compile as C++14, but does not.
+ group("cpp14_modules") {
+ public_deps = [
+ dir_pw_polyfill,
+ dir_pw_preprocessor,
+ dir_pw_tokenizer,
+ dir_pw_varint,
+ ]
+ }
+
+ # Tests that support C++14.
+ pw_test_group("cpp14_tests") {
+ group_deps = [
+ "$dir_pw_polyfill:tests",
+ "$dir_pw_span:tests",
+ ]
+ tests = [
+ "$dir_pw_tokenizer:simple_tokenize_test",
+ "$dir_pw_containers:to_array_test",
+ "$dir_pw_string:string_test",
+ ]
+ }
}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a6c922cb0..4e463d29a 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,12 +20,43 @@ cmake_minimum_required(VERSION 3.16)
# Regardless of whether it's set or not the following include will ensure it is.
include(pw_build/pigweed.cmake)
+if(CONFIG_ZEPHYR_PIGWEED_MODULE)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_ASSERT
+ pw_assert.assert pw_assert_zephyr.assert pw_assert/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_ASSERT
+ pw_assert.check pw_assert_zephyr.check pw_assert/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK
+ pw_chrono.system_clock pw_chrono_zephyr.system_clock pw_chrono/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_INTERRUPT_CONTEXT
+ pw_interrupt.context pw_interrupt_zephyr.context pw_interrupt/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_LOG_ZEPHYR
+ pw_log pw_log_zephyr pw_log/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_LOG_TOKENIZED
+ pw_log_tokenized.handler pw_log_zephyr.tokenized_handler pw_log_tokenized/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_LOG_TOKENIZED
+ pw_log pw_log_tokenized pw_log/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_THREAD_SLEEP
+ pw_thread.sleep pw_thread_zephyr.sleep pw_thread/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_MUTEX
+ pw_sync.mutex pw_sync_zephyr.mutex_backend pw_sync/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYNC_BINARY_SEMAPHORE
+ pw_sync.mutex pw_sync_zephyr.binary_semaphore_backend pw_sync/backend.cmake)
+ pw_set_zephyr_backend_ifdef(CONFIG_PIGWEED_SYS_IO
+ pw_sys_io pw_sys_io_zephyr pw_sys_io/backend.cmake)
+
+ if(CONFIG_NANOPB AND "${dir_pw_third_party_nanopb}" STREQUAL "")
+ set(dir_pw_third_party_nanopb "${ZEPHYR_NANOPB_MODULE_DIR}" CACHE PATH "" FORCE)
+ endif()
+endif()
+
+add_subdirectory(pw_allocator EXCLUDE_FROM_ALL)
add_subdirectory(pw_assert EXCLUDE_FROM_ALL)
add_subdirectory(pw_assert_basic EXCLUDE_FROM_ALL)
add_subdirectory(pw_assert_log EXCLUDE_FROM_ALL)
add_subdirectory(pw_assert_zephyr EXCLUDE_FROM_ALL)
add_subdirectory(pw_base64 EXCLUDE_FROM_ALL)
add_subdirectory(pw_blob_store EXCLUDE_FROM_ALL)
+add_subdirectory(pw_bluetooth EXCLUDE_FROM_ALL)
add_subdirectory(pw_build EXCLUDE_FROM_ALL)
add_subdirectory(pw_build_info EXCLUDE_FROM_ALL)
add_subdirectory(pw_bytes EXCLUDE_FROM_ALL)
@@ -34,25 +65,31 @@ add_subdirectory(pw_chrono EXCLUDE_FROM_ALL)
add_subdirectory(pw_chrono_freertos EXCLUDE_FROM_ALL)
add_subdirectory(pw_chrono_stl EXCLUDE_FROM_ALL)
add_subdirectory(pw_chrono_zephyr EXCLUDE_FROM_ALL)
+add_subdirectory(pw_compilation_testing EXCLUDE_FROM_ALL)
add_subdirectory(pw_containers EXCLUDE_FROM_ALL)
add_subdirectory(pw_cpu_exception EXCLUDE_FROM_ALL)
add_subdirectory(pw_cpu_exception_cortex_m EXCLUDE_FROM_ALL)
+add_subdirectory(pw_digital_io EXCLUDE_FROM_ALL)
add_subdirectory(pw_file EXCLUDE_FROM_ALL)
add_subdirectory(pw_function EXCLUDE_FROM_ALL)
add_subdirectory(pw_hdlc EXCLUDE_FROM_ALL)
add_subdirectory(pw_interrupt EXCLUDE_FROM_ALL)
add_subdirectory(pw_interrupt_cortex_m EXCLUDE_FROM_ALL)
add_subdirectory(pw_interrupt_zephyr EXCLUDE_FROM_ALL)
+add_subdirectory(pw_intrusive_ptr EXCLUDE_FROM_ALL)
add_subdirectory(pw_kvs EXCLUDE_FROM_ALL)
add_subdirectory(pw_log EXCLUDE_FROM_ALL)
add_subdirectory(pw_log_basic EXCLUDE_FROM_ALL)
add_subdirectory(pw_log_null EXCLUDE_FROM_ALL)
add_subdirectory(pw_log_string EXCLUDE_FROM_ALL)
add_subdirectory(pw_log_tokenized EXCLUDE_FROM_ALL)
+add_subdirectory(pw_log_rpc EXCLUDE_FROM_ALL)
add_subdirectory(pw_log_zephyr EXCLUDE_FROM_ALL)
add_subdirectory(pw_minimal_cpp_stdlib EXCLUDE_FROM_ALL)
+add_subdirectory(pw_metric EXCLUDE_FROM_ALL)
add_subdirectory(pw_multisink EXCLUDE_FROM_ALL)
add_subdirectory(pw_persistent_ram EXCLUDE_FROM_ALL)
+add_subdirectory(pw_perf_test EXCLUDE_FROM_ALL)
add_subdirectory(pw_polyfill EXCLUDE_FROM_ALL)
add_subdirectory(pw_preprocessor EXCLUDE_FROM_ALL)
add_subdirectory(pw_protobuf EXCLUDE_FROM_ALL)
@@ -68,6 +105,7 @@ add_subdirectory(pw_status EXCLUDE_FROM_ALL)
add_subdirectory(pw_stream EXCLUDE_FROM_ALL)
add_subdirectory(pw_string EXCLUDE_FROM_ALL)
add_subdirectory(pw_sync EXCLUDE_FROM_ALL)
+add_subdirectory(pw_sync_baremetal EXCLUDE_FROM_ALL)
add_subdirectory(pw_sync_freertos EXCLUDE_FROM_ALL)
add_subdirectory(pw_sync_stl EXCLUDE_FROM_ALL)
add_subdirectory(pw_sync_zephyr EXCLUDE_FROM_ALL)
@@ -78,15 +116,20 @@ add_subdirectory(pw_system EXCLUDE_FROM_ALL)
add_subdirectory(pw_thread EXCLUDE_FROM_ALL)
add_subdirectory(pw_thread_freertos EXCLUDE_FROM_ALL)
add_subdirectory(pw_thread_stl EXCLUDE_FROM_ALL)
+add_subdirectory(pw_thread_zephyr EXCLUDE_FROM_ALL)
add_subdirectory(pw_tokenizer EXCLUDE_FROM_ALL)
+add_subdirectory(pw_toolchain EXCLUDE_FROM_ALL)
add_subdirectory(pw_trace EXCLUDE_FROM_ALL)
add_subdirectory(pw_trace_tokenized EXCLUDE_FROM_ALL)
add_subdirectory(pw_transfer EXCLUDE_FROM_ALL)
add_subdirectory(pw_unit_test EXCLUDE_FROM_ALL)
add_subdirectory(pw_varint EXCLUDE_FROM_ALL)
+add_subdirectory(pw_work_queue EXCLUDE_FROM_ALL)
add_subdirectory(third_party/nanopb EXCLUDE_FROM_ALL)
add_subdirectory(third_party/freertos EXCLUDE_FROM_ALL)
+add_subdirectory(third_party/fuchsia EXCLUDE_FROM_ALL)
+add_subdirectory(third_party/googletest EXCLUDE_FROM_ALL)
if(NOT ZEPHYR_PIGWEED_MODULE_DIR)
add_subdirectory(targets/host EXCLUDE_FROM_ALL)
diff --git a/Kconfig.zephyr b/Kconfig.zephyr
index d0c43d020..9e07bf0ae 100644
--- a/Kconfig.zephyr
+++ b/Kconfig.zephyr
@@ -13,7 +13,7 @@
# the License.
config ZEPHYR_PIGWEED_MODULE
- select LIB_CPLUSPLUS
+ select REQUIRES_FULL_LIBCPP
depends on STD_CPP17 || STD_CPP2A || STD_CPP20 || STD_CPP2B
if ZEPHYR_PIGWEED_MODULE
@@ -38,6 +38,8 @@ rsource "pw_stream/Kconfig"
rsource "pw_string/Kconfig"
rsource "pw_sync_zephyr/Kconfig"
rsource "pw_sys_io_zephyr/Kconfig"
+rsource "pw_thread_zephyr/Kconfig"
+rsource "pw_tokenizer/Kconfig"
rsource "pw_varint/Kconfig"
endif # ZEPHYR_PIGWEED_MODULE
diff --git a/OWNERS b/OWNERS
index c0c23f8b0..cc577aeed 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,4 +1,4 @@
# CHRE team maintainers in AOSP
bduddie@google.com
-karthikmb@google.com
+berchet@google.com
stange@google.com
diff --git a/PIGWEED_MODULES b/PIGWEED_MODULES
index dfb7f2725..ef740fcf3 100644
--- a/PIGWEED_MODULES
+++ b/PIGWEED_MODULES
@@ -1,4 +1,5 @@
docker
+pw_alignment
pw_allocator
pw_analog
pw_android_toolchain
@@ -8,10 +9,14 @@ pw_assert_basic
pw_assert_log
pw_assert_tokenized
pw_assert_zephyr
+pw_async
+pw_async_basic
pw_base64
pw_bloat
pw_blob_store
+pw_bluetooth
pw_bluetooth_hci
+pw_bluetooth_profiles
pw_boot
pw_boot_cortex_m
pw_build
@@ -26,11 +31,13 @@ pw_chrono_stl
pw_chrono_threadx
pw_chrono_zephyr
pw_cli
+pw_compilation_testing
pw_console
pw_containers
pw_cpu_exception
pw_cpu_exception_cortex_m
pw_crypto
+pw_digital_io
pw_docgen
pw_doctor
pw_env_setup
@@ -41,9 +48,11 @@ pw_hdlc
pw_hex_dump
pw_i2c
pw_i2c_mcuxpresso
+pw_ide
pw_interrupt
pw_interrupt_cortex_m
pw_interrupt_zephyr
+pw_intrusive_ptr
pw_kvs
pw_libc
pw_log
@@ -61,6 +70,7 @@ pw_minimal_cpp_stdlib
pw_module
pw_multisink
pw_package
+pw_perf_test
pw_persistent_ram
pw_polyfill
pw_preprocessor
@@ -72,6 +82,7 @@ pw_result
pw_ring_buffer
pw_router
pw_rpc
+pw_rust
pw_snapshot
pw_software_update
pw_span
@@ -94,6 +105,7 @@ pw_sys_io_baremetal_lm3s6965evb
pw_sys_io_baremetal_stm32f429
pw_sys_io_emcraft_sf2
pw_sys_io_mcuxpresso
+pw_sys_io_pico
pw_sys_io_stdio
pw_sys_io_stm32cube
pw_sys_io_zephyr
@@ -104,6 +116,7 @@ pw_thread_embos
pw_thread_freertos
pw_thread_stl
pw_thread_threadx
+pw_thread_zephyr
pw_tls_client
pw_tls_client_boringssl
pw_tls_client_mbedtls
@@ -116,5 +129,5 @@ pw_transfer
pw_unit_test
pw_varint
pw_watch
-pw_web_ui
+pw_web
pw_work_queue
diff --git a/PW_PLUGINS b/PW_PLUGINS
index a3047aef4..b78b0b3af 100644
--- a/PW_PLUGINS
+++ b/PW_PLUGINS
@@ -10,10 +10,14 @@
# virtual environment. The function must have no required arguments and should
# return an int to use as the exit code.
-# Pigweed's presubmit check script
+# keep-sorted: start
+console pw_console.__main__ main
+format pw_presubmit.format_code _pigweed_upstream_main
heap-viewer pw_allocator.heap_viewer main
+ide pw_ide.__main__ main
package pw_package.pigweed_packages main
presubmit pw_presubmit.pigweed_presubmit main
requires pw_cli.requires main
rpc pw_hdlc.rpc_console main
-console pw_console.__main__ main
+update pw_software_update.cli main
+# keep-sorted: end
diff --git a/README.md b/README.md
index 8548ab451..af69b6a3e 100644
--- a/README.md
+++ b/README.md
@@ -6,14 +6,16 @@ that enable faster and more reliable development on small-footprint MMU-less
32-bit microcontrollers like the STMicroelectronics STM32L452 or the Nordic
nRF52832.
-For more information please see our website: https://pigweed.dev/
+For more information please see our website: https://pigweed.dev/.
## Links
+<!-- TODO(b/256680603) Remove query string from issue tracker link. -->
+
- [Documentation](https://pigweed.dev/)
-- [Source Code](https://cs.opensource.google/pigweed/pigweed)
+- [Source Code](https://cs.pigweed.dev/pigweed)
- [Code Reviews](https://pigweed-review.googlesource.com)
- [Mailing List](https://groups.google.com/forum/#!forum/pigweed)
- [Chat Room](https://discord.gg/M9NSeTA)
-- [Issue Tracker](https://bugs.pigweed.dev/)
+- [Issue Tracker](https://issues.pigweed.dev/issues?q=status:open)
diff --git a/WORKSPACE b/WORKSPACE
index e95aa1b97..8dcbd4781 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -14,12 +14,28 @@
workspace(
name = "pigweed",
- managed_directories = {"@npm": ["node_modules"]},
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
-load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
-load("//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl", "pigweed_deps")
+load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository", "new_git_repository")
+load(
+ "//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl",
+ "cipd_client_repository",
+ "cipd_repository",
+ "pigweed_deps",
+)
+
+# Set up Bazel platforms.
+# Required by: pigweed.
+# Used in modules: //pw_build, (Assorted modules via select statements).
+http_archive(
+ name = "platforms",
+ sha256 = "5308fc1d8865406a49427ba24a9ab53087f17f5266a7aabbfc28823f3916e1ca",
+ urls = [
+ "https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.6/platforms-0.0.6.tar.gz",
+ "https://github.com/bazelbuild/platforms/releases/download/0.0.6/platforms-0.0.6.tar.gz",
+ ],
+)
# Setup CIPD client and packages.
# Required by: pigweed.
@@ -30,6 +46,14 @@ load("@cipd_deps//:cipd_init.bzl", "cipd_init")
cipd_init()
+cipd_client_repository()
+
+cipd_repository(
+ name = "pw_transfer_test_binaries",
+ path = "pigweed/pw_transfer_test_binaries/${os=linux}-${arch=amd64}",
+ tag = "version:pw_transfer_test_binaries_528098d588f307881af83f769207b8e6e1b57520-linux-amd64-cipd.cipd",
+)
+
# Set up Python support.
# Required by: rules_fuzzing, com_github_nanopb_nanopb.
# Used in modules: None.
@@ -47,12 +71,10 @@ http_archive(
# in an older version of bazel_skylib. However io_bazel_rules_go requires a
# newer version.
http_archive(
- name = "bazel_skylib",
- sha256 = "1c531376ac7e5a180e0237938a2536de0c54d93f5c278634818e0efc952dd56c",
- urls = [
- "https://github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz",
- "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.0.3/bazel-skylib-1.0.3.tar.gz",
- ],
+ name = "bazel_skylib", # 2022-09-01
+ sha256 = "4756ab3ec46d94d99e5ed685d2d24aece484015e45af303eb3a11cab3cdc2e71",
+ strip_prefix = "bazel-skylib-1.3.0",
+ urls = ["https://github.com/bazelbuild/bazel-skylib/archive/refs/tags/1.3.0.zip"],
)
load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace")
@@ -64,11 +86,10 @@ bazel_skylib_workspace()
# Used in modules: //pw_analog, //pw_i2c.
http_archive(
name = "com_google_googletest",
- sha256 = "9dc9157a9a1551ec7a7e43daea9a694a0bb5fb8bec81235d8a1e6ef64c716dcb",
- strip_prefix = "googletest-release-1.10.0",
+ sha256 = "ad7fdba11ea011c1d925b3289cf4af2c66a352e18d4c7264392fead75e919363",
+ strip_prefix = "googletest-1.13.0",
urls = [
- "https://mirror.bazel.build/github.com/google/googletest/archive/release-1.10.0.tar.gz",
- "https://github.com/google/googletest/archive/release-1.10.0.tar.gz",
+ "https://github.com/google/googletest/archive/refs/tags/v1.13.0.tar.gz",
],
)
@@ -77,8 +98,9 @@ http_archive(
# Used in modules: All cc targets.
git_repository(
name = "rules_cc_toolchain",
- commit = "80b51ba81f14eebe06684efa25261f6dc46e9b29",
- remote = "https://github.com/bazelembedded/rules_cc_toolchain.git",
+ commit = "9f209fda87414285bc66accd3612575b29760fba",
+ remote = "https://github.com/bazelembedded/rules_cc_toolchain",
+ shallow_since = "1675385535 -0800",
)
load("@rules_cc_toolchain//:rules_cc_toolchain_deps.bzl", "rules_cc_toolchain_deps")
@@ -89,28 +111,27 @@ load("@rules_cc_toolchain//cc_toolchain:cc_toolchain.bzl", "register_cc_toolchai
register_cc_toolchains()
-# Set up Protobuf rules.
-# Required by: pigweed, com_github_bazelbuild_buildtools.
-# Used in modules: //pw_protobuf.
-http_archive(
- name = "com_google_protobuf",
- sha256 = "c6003e1d2e7fefa78a3039f19f383b4f3a61e81be8c19356f85b6461998ad3db",
- strip_prefix = "protobuf-3.17.3",
- url = "https://github.com/protocolbuffers/protobuf/archive/v3.17.3.tar.gz",
+# Sets up Bazels documentation generator.
+# Required by: rules_cc_toolchain.
+# Required by modules: All
+git_repository(
+ name = "io_bazel_stardoc",
+ commit = "2b801dc9b93f73812948ee4e505805511b0f55dc",
+ remote = "https://github.com/bazelbuild/stardoc.git",
+ shallow_since = "1651081130 -0400",
)
-load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
-
-protobuf_deps()
-
# Set up tools to build custom GRPC rules.
+#
+# We use a fork that silences some zlib compilation warnings.
+#
# Required by: pigweed.
# Used in modules: //pw_protobuf.
-http_archive(
+git_repository(
name = "rules_proto_grpc",
- sha256 = "7954abbb6898830cd10ac9714fbcacf092299fda00ed2baf781172f545120419",
- strip_prefix = "rules_proto_grpc-3.1.1",
- urls = ["https://github.com/rules-proto-grpc/rules_proto_grpc/archive/3.1.1.tar.gz"],
+ commit = "2fbf774a5553b773372f7b91f9b1dc06ee0da2d3",
+ remote = "https://github.com/tpudlik/rules_proto_grpc.git",
+ shallow_since = "1675375991 -0800",
)
load(
@@ -124,7 +145,7 @@ rules_proto_grpc_toolchains()
rules_proto_grpc_repos()
# Set up Protobuf rules.
-# Required by: pigweed, com_github_bazelbuild_buildtools.
+# Required by: pigweed.
# Used in modules: //pw_protobuf.
http_archive(
name = "com_google_protobuf",
@@ -144,6 +165,7 @@ git_repository(
name = "com_github_nanopb_nanopb",
commit = "e601fca6d9ed7fb5c09e2732452753b2989f128b",
remote = "https://github.com/nanopb/nanopb.git",
+ shallow_since = "1641373017 +0800",
)
load("@com_github_nanopb_nanopb//:nanopb_deps.bzl", "nanopb_deps")
@@ -158,80 +180,16 @@ load("@com_github_nanopb_nanopb//:nanopb_workspace.bzl", "nanopb_workspace")
nanopb_workspace()
-# Set up Bazel platforms.
-# Required by: pigweed.
-# Used in modules: //pw_build, (Assorted modules via select statements).
-git_repository(
- name = "platforms",
- commit = "d4c9d7f51a7c403814b60f66d20eeb425fbaaacb",
- remote = "https://github.com/bazelbuild/platforms.git",
-)
-
-# Set up NodeJs rules.
-# Required by: pigweed.
-# Used in modules: //pw_web_ui.
-http_archive(
- name = "build_bazel_rules_nodejs",
- sha256 = "b32a4713b45095e9e1921a7fcb1adf584bc05959f3336e7351bcf77f015a2d7c",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/4.1.0/rules_nodejs-4.1.0.tar.gz"],
-)
-
-# Get the latest LTS version of Node.
-load(
- "@build_bazel_rules_nodejs//:index.bzl",
- "node_repositories",
- "yarn_install",
-)
-
-node_repositories(package_json = ["//:package.json"])
-
-yarn_install(
- name = "npm",
- package_json = "//:package.json",
- yarn_lock = "//:yarn.lock",
-)
-
-# Set up web-testing rules.
-# Required by: pigweed.
-# Used in modules: //pw_web_ui.
-http_archive(
- name = "io_bazel_rules_webtesting",
- sha256 = "9bb461d5ef08e850025480bab185fd269242d4e533bca75bfb748001ceb343c3",
- urls = ["https://github.com/bazelbuild/rules_webtesting/releases/download/0.3.3/rules_webtesting.tar.gz"],
-)
-
-load("@io_bazel_rules_webtesting//web:repositories.bzl", "web_test_repositories")
-
-web_test_repositories()
-
-load(
- "@io_bazel_rules_webtesting//web/versioned:browsers-0.3.2.bzl",
- "browser_repositories",
-)
-
-browser_repositories(
- chromium = True,
- firefox = True,
-)
-
# Set up embedded C/C++ toolchains.
# Required by: pigweed.
# Used in modules: //pw_polyfill, //pw_build (all pw_cc* targets).
git_repository(
name = "bazel_embedded",
- commit = "17c93d5fa52c4c78860b8bbd327325fba6c85555",
+ commit = "91dcc13ebe5df755ca2fe896ff6f7884a971d05b",
remote = "https://github.com/bazelembedded/bazel-embedded.git",
+ shallow_since = "1631751909 +0800",
)
-# Instantiate Pigweed configuration for embedded toolchain,
-# this must be called before bazel_embedded_deps.
-load(
- "//pw_build:pigweed_toolchain_upstream.bzl",
- "toolchain_upstream_deps",
-)
-
-toolchain_upstream_deps()
-
# Configure bazel_embedded toolchains and platforms.
load(
"@bazel_embedded//:bazel_embedded_deps.bzl",
@@ -263,57 +221,75 @@ load(
register_gcc_arm_none_toolchain()
-# Registers platforms for use with toolchain resolution
-register_execution_platforms("//pw_build/platforms:all")
-
-# Set up Golang toolchain rules.
-# Required by: bazel_gazelle, com_github_bazelbuild_buildtools.
-# Used in modules: None.
-http_archive(
- name = "io_bazel_rules_go",
- sha256 = "d1ffd055969c8f8d431e2d439813e42326961d0942bdf734d2c95dc30c369566",
- urls = [
- "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.24.5/rules_go-v0.24.5.tar.gz",
- "https://github.com/bazelbuild/rules_go/releases/download/v0.24.5/rules_go-v0.24.5.tar.gz",
- ],
-)
+# Rust Support
+#
-load(
- "@io_bazel_rules_go//go:deps.bzl",
- "go_register_toolchains",
- "go_rules_dependencies",
+git_repository(
+ name = "rules_rust",
+ # Pulls in the main branch with https://github.com/bazelbuild/rules_rust/pull/1803
+ # merged. Once a release is cut with that commit, we should switch to
+ # using a release tarbal.
+ commit = "a5853fd37053b65ee30ba4f8064b9db67c90d53f",
+ remote = "https://github.com/bazelbuild/rules_rust",
+ shallow_since = "1675302817 -0800",
)
-go_rules_dependencies()
+load("@rules_rust//rust:repositories.bzl", "rules_rust_dependencies", "rust_analyzer_toolchain_repository", "rust_repository_set")
-go_register_toolchains()
+rules_rust_dependencies()
-# Set up bazel package manager for golang.
-# Required by: com_github_bazelbuild_buildtools.
-# Used in modules: None.
-http_archive(
- name = "bazel_gazelle",
- sha256 = "b85f48fa105c4403326e9525ad2b2cc437babaa6e15a3fc0b1dbab0ab064bc7c",
- urls = [
- "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.2/bazel-gazelle-v0.22.2.tar.gz",
- "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.22.2/bazel-gazelle-v0.22.2.tar.gz",
+# Here we pull in a specific toolchain. Unfortunately `rust_repository_set`
+# does not provide a way to add `target_compatible_with` options which are
+# needed to be compatible with `@bazel_embedded` (specifically
+# `@bazel_embedded//constraints/fpu:none` which is specified in
+# `//platforms`)
+#
+# See `//toolchain:rust_linux_x86_64` for how this is used.
+#
+# Note: This statement creates name mangled remotes of the form:
+# `@{name}__{triplet}_tools`
+# (example: `@rust_linux_x86_64__thumbv7m-none-eabi_tools/`)
+rust_repository_set(
+ name = "rust_linux_x86_64",
+ edition = "2021",
+ exec_triple = "x86_64-unknown-linux-gnu",
+ extra_target_triples = [
+ "thumbv7m-none-eabi",
+ "thumbv6m-none-eabi",
],
+ versions = ["1.67.0"],
)
-load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
+# Registers our Rust toolchains that are compatable with `@bazel_embedded`.
+register_toolchains(
+ "//pw_toolchain:thumbv7m_rust_linux_x86_64",
+ "//pw_toolchain:thumbv6m_rust_linux_x86_64",
+)
-gazelle_dependencies()
+# Allows creation of a `rust-project.json` file to allow rust analyzer to work.
+load("@rules_rust//tools/rust_analyzer:deps.bzl", "rust_analyzer_dependencies")
-# Set up bazel buildtools (bazel linter and formatter).
-# Required by: pigweed.
-# Used in modules: //:all (bazel specific tools).
-http_archive(
- name = "com_github_bazelbuild_buildtools",
- sha256 = "c28eef4d30ba1a195c6837acf6c75a4034981f5b4002dda3c5aa6e48ce023cf1",
- strip_prefix = "buildtools-4.0.1",
- url = "https://github.com/bazelbuild/buildtools/archive/4.0.1.tar.gz",
+# Since we do not use rust_register_toolchains, we need to define a
+# rust_analyzer_toolchain.
+register_toolchains(rust_analyzer_toolchain_repository(
+ name = "rust_analyzer_toolchain",
+ # This should match the currently registered toolchain.
+ version = "1.67.0",
+))
+
+rust_analyzer_dependencies()
+
+# Vendored third party rust crates.
+git_repository(
+ name = "rust_crates",
+ commit = "c39c1d1d4e4bdf2d8145beb8882af6f6e4e6dbbc",
+ remote = "https://pigweed.googlesource.com/third_party/rust_crates",
+ shallow_since = "1675359057 +0000",
)
+# Registers platforms for use with toolchain resolution
+register_execution_platforms("//pw_build/platforms:all")
+
load("//pw_build:target_config.bzl", "pigweed_config")
# Configure Pigweeds backend.
@@ -322,14 +298,26 @@ pigweed_config(
build_file = "//targets:default_config.BUILD",
)
+# Required by: rules_fuzzing.
+#
+# Provided here explicitly to override an old version of absl that
+# rules_fuzzing_dependencies attempts to pull in. That version has
+# many compiler warnings on newer clang versions.
+http_archive(
+ name = "com_google_absl",
+ sha256 = "3ea49a7d97421b88a8c48a0de16c16048e17725c7ec0f1d3ea2683a2a75adc21",
+ strip_prefix = "abseil-cpp-20230125.0",
+ urls = ["https://github.com/abseil/abseil-cpp/archive/refs/tags/20230125.0.tar.gz"],
+)
+
# Set up rules for fuzz testing.
# Required by: pigweed.
# Used in modules: //pw_protobuf, //pw_tokenizer, //pw_fuzzer.
http_archive(
name = "rules_fuzzing",
- sha256 = "94f25c7a18db0502ace26a3ef7d0a25fd7c195c4e9770ddd1b1ec718e8936091",
- strip_prefix = "rules_fuzzing-0.1.3",
- urls = ["https://github.com/bazelbuild/rules_fuzzing/archive/v0.1.3.zip"],
+ sha256 = "d9002dd3cd6437017f08593124fdd1b13b3473c7b929ceb0e60d317cb9346118",
+ strip_prefix = "rules_fuzzing-0.3.2",
+ urls = ["https://github.com/bazelbuild/rules_fuzzing/archive/v0.3.2.zip"],
)
load("@rules_fuzzing//fuzzing:repositories.bzl", "rules_fuzzing_dependencies")
@@ -340,11 +328,6 @@ load("@rules_fuzzing//fuzzing:init.bzl", "rules_fuzzing_init")
rules_fuzzing_init()
-# esbuild setup
-load("@build_bazel_rules_nodejs//toolchains/esbuild:esbuild_repositories.bzl", "esbuild_repositories")
-
-esbuild_repositories(npm_repository = "npm") # Note, npm is the default value for npm_repository
-
RULES_JVM_EXTERNAL_TAG = "2.8"
RULES_JVM_EXTERNAL_SHA = "79c9850690d7614ecdb72d68394f994fef7534b292c4867ce5e7dec0aa7bdfad"
@@ -376,3 +359,26 @@ maven_install(
"https://repo1.maven.org/maven2",
],
)
+
+new_git_repository(
+ name = "micro_ecc",
+ build_file = "//:third_party/micro_ecc/BUILD.micro_ecc",
+ commit = "b335ee812bfcca4cd3fb0e2a436aab39553a555a",
+ remote = "https://github.com/kmackay/micro-ecc.git",
+ shallow_since = "1648504566 -0700",
+)
+
+git_repository(
+ name = "boringssl",
+ commit = "0fd67c76fc4bfb05a665c087ebfead77a3267f6d",
+ remote = "https://boringssl.googlesource.com/boringssl",
+ shallow_since = "1637714942 +0000",
+)
+
+http_archive(
+ name = "freertos",
+ build_file = "//:third_party/freertos/BUILD.bazel",
+ sha256 = "89af32b7568c504624f712c21fe97f7311c55fccb7ae6163cda7adde1cde7f62",
+ strip_prefix = "FreeRTOS-Kernel-10.5.1",
+ urls = ["https://github.com/FreeRTOS/FreeRTOS-Kernel/archive/refs/tags/V10.5.1.tar.gz"],
+)
diff --git a/bootstrap.bat b/bootstrap.bat
index 5c46a29c9..a61d2233e 100644
--- a/bootstrap.bat
+++ b/bootstrap.bat
@@ -88,9 +88,10 @@ goto finish
:: came from the developer and not from a previous bootstrap possibly from
:: another workspace.
-:: Not prefixing environment with "." since that doesn't hide it anyway.
+if "%PW_PROJECT_ROOT%"=="" set "PW_PROJECT_ROOT=%PW_ROOT%"
+
if "%PW_ENVIRONMENT_ROOT%"=="" (
- set "_PW_ACTUAL_ENVIRONMENT_ROOT=%PW_ROOT%\environment"
+ set "_PW_ACTUAL_ENVIRONMENT_ROOT=%PW_PROJECT_ROOT%\environment"
) else (
set "_PW_ACTUAL_ENVIRONMENT_ROOT=%PW_ENVIRONMENT_ROOT%"
)
@@ -99,8 +100,6 @@ set "shell_file=%_PW_ACTUAL_ENVIRONMENT_ROOT%\activate.bat"
set "_pw_start_script=%PW_ROOT%\pw_env_setup\py\pw_env_setup\windows_env_start.py"
-if "%PW_PROJECT_ROOT%"=="" set "PW_PROJECT_ROOT=%PW_ROOT%"
-
:: If PW_SKIP_BOOTSTRAP is set, only run the activation stage instead of the
:: complete env_setup.
if not "%PW_SKIP_BOOTSTRAP%" == "" goto skip_bootstrap
diff --git a/bootstrap.sh b/bootstrap.sh
index 75da8d972..a615fc501 100644
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -63,9 +63,13 @@ export PW_PROJECT_ROOT
. "$PW_ROOT/pw_env_setup/util.sh"
+# Check environment properties
pw_deactivate
pw_eval_sourced "$_pw_sourced" "$_PW_BOOTSTRAP_PATH"
-pw_check_root "$PW_ROOT"
+if ! pw_check_root "$PW_ROOT"; then
+ return
+fi
+
_PW_ACTUAL_ENVIRONMENT_ROOT="$(pw_get_env_root)"
export _PW_ACTUAL_ENVIRONMENT_ROOT
SETUP_SH="$_PW_ACTUAL_ENVIRONMENT_ROOT/activate.sh"
@@ -90,3 +94,5 @@ unset SETUP_SH
unset _bootstrap_abspath
pw_cleanup
+
+git -C "$PW_ROOT" config blame.ignoreRevsFile .git-blame-ignore-revs
diff --git a/docker/BUILD.gn b/docker/BUILD.gn
index dd021e82f..a4d5ebe10 100644
--- a/docker/BUILD.gn
+++ b/docker/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/docker/Dockerfile.cache b/docker/Dockerfile.cache
index 8d6c18da8..ccedbc0ab 100644
--- a/docker/Dockerfile.cache
+++ b/docker/Dockerfile.cache
@@ -25,7 +25,6 @@ ENV CIPD_CACHE_DIR /pigweed-cache/cipd-cache-dir
# PW_ENVIRONMENT_ROOT alone.
ENV PW_ROOT /cache/pigweed
ENV PW_ENVIRONMENT_ROOT /cache/environment
-ENV PW_CIPD_PACKAGE_FILES "$PW_ROOT/pw_env_setup/py/pw_env_setup/cipd_setup/*.json"
WORKDIR $PW_ROOT
# env_setup requires .git for determining top-level directory with git rev-parse
diff --git a/docs/BUILD.gn b/docs/BUILD.gn
index bd299067a..bbfc362db 100644
--- a/docs/BUILD.gn
+++ b/docs/BUILD.gn
@@ -15,21 +15,30 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
+import("$dir_pw_build/python_action.gni")
import("$dir_pw_docgen/docs.gni")
+pw_doc_group("static_assets") {
+ inputs = [
+ "_static/css/pigweed.css",
+ "_static/pw_logo.ico",
+ "_static/pw_logo.svg",
+ ]
+}
+
# Note: These may be useful for downstream projects, which is why they are
# split out from the overall docgen target below.
pw_doc_group("core_docs") {
inputs = [
+ "$dir_pw_build/python.gni",
"images/pw_env_setup_demo.gif",
"images/pw_status_test.png",
"images/pw_watch_build_demo.gif",
"images/pw_watch_on_device_demo.gif",
"images/pw_watch_test_demo.gif",
+ "images/pw_watch_test_demo2.gif",
"images/stm32f429i-disc1_connected.jpg",
-
- # TODO(pwbug/368): This should be in the pw_doc_gen target instead of here.
- "_static/css/pigweed.css",
+ "run_doxygen.py",
]
sources = [
"code_of_conduct.rst",
@@ -52,6 +61,19 @@ pw_doc_group("release_notes") {
]
}
+pw_doc_group("templates") {
+ sources = [
+ "templates/docs/api.rst",
+ "templates/docs/cli.rst",
+ "templates/docs/concepts.rst",
+ "templates/docs/design.rst",
+ "templates/docs/docs.rst",
+ "templates/docs/gui.rst",
+ "templates/docs/guides.rst",
+ "templates/docs/tutorials/index.rst",
+ ]
+}
+
# Documentation for upstream Pigweed targets.
group("target_docs") {
deps = [
@@ -64,6 +86,7 @@ group("target_docs") {
"$dir_pigweed/targets/lm3s6965evb_qemu:target_docs",
"$dir_pigweed/targets/mimxrt595_evk:target_docs",
"$dir_pigweed/targets/rp2040:target_docs",
+ "$dir_pigweed/targets/rp2040_pw_system:target_docs",
"$dir_pigweed/targets/stm32f429i_disc1:target_docs",
"$dir_pigweed/targets/stm32f429i_disc1_stm32cube:target_docs",
]
@@ -75,12 +98,78 @@ group("module_docs") {
group("third_party_docs") {
deps = [
+ "$dir_pigweed/third_party/boringssl:docs",
+ "$dir_pigweed/third_party/emboss:docs",
"$dir_pigweed/third_party/freertos:docs",
+ "$dir_pigweed/third_party/fuchsia:docs",
+ "$dir_pigweed/third_party/googletest:docs",
"$dir_pigweed/third_party/nanopb:docs",
"$dir_pigweed/third_party/tinyusb:docs",
]
}
+_doxygen_input_files = [
+ # All sources with doxygen comment blocks.
+ "$dir_pw_async/public/pw_async/dispatcher.h",
+ "$dir_pw_async/public/pw_async/fake_dispatcher_fixture.h",
+ "$dir_pw_async/public/pw_async/task.h",
+ "$dir_pw_async_basic/public/pw_async_basic/dispatcher.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/gatt/client.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/gatt/server.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/host.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/low_energy/central.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/low_energy/connection.h",
+ "$dir_pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h",
+ "$dir_pw_chrono/public/pw_chrono/system_clock.h",
+ "$dir_pw_chrono/public/pw_chrono/system_timer.h",
+ "$dir_pw_function/public/pw_function/scope_guard.h",
+ "$dir_pw_log_tokenized/public/pw_log_tokenized/handler.h",
+ "$dir_pw_log_tokenized/public/pw_log_tokenized/metadata.h",
+ "$dir_pw_rpc/public/pw_rpc/internal/config.h",
+ "$dir_pw_string/public/pw_string/format.h",
+ "$dir_pw_string/public/pw_string/string.h",
+ "$dir_pw_function/public/pw_function/function.h",
+ "$dir_pw_function/public/pw_function/pointer.h",
+ "$dir_pw_string/public/pw_string/string_builder.h",
+ "$dir_pw_string/public/pw_string/util.h",
+ "$dir_pw_sync/public/pw_sync/binary_semaphore.h",
+ "$dir_pw_sync/public/pw_sync/borrow.h",
+ "$dir_pw_sync/public/pw_sync/counting_semaphore.h",
+ "$dir_pw_sync/public/pw_sync/inline_borrowable.h",
+ "$dir_pw_sync/public/pw_sync/interrupt_spin_lock.h",
+ "$dir_pw_sync/public/pw_sync/lock_annotations.h",
+ "$dir_pw_sync/public/pw_sync/mutex.h",
+ "$dir_pw_sync/public/pw_sync/thread_notification.h",
+ "$dir_pw_sync/public/pw_sync/timed_mutex.h",
+ "$dir_pw_sync/public/pw_sync/timed_thread_notification.h",
+ "$dir_pw_sync/public/pw_sync/virtual_basic_lockable.h",
+ "$dir_pw_tokenizer/public/pw_tokenizer/encode_args.h",
+ "$dir_pw_tokenizer/public/pw_tokenizer/tokenize.h",
+]
+
+pw_python_action("generate_doxygen") {
+ _output_dir = "docs/doxygen"
+ script = "run_doxygen.py"
+ inputs = [
+ "//PIGWEED_MODULES",
+ "Doxyfile",
+ ]
+ inputs += _doxygen_input_files
+ args = [
+ "--gn-root",
+ rebase_path("//", root_build_dir),
+ "--pigweed-modules-file",
+ rebase_path("//PIGWEED_MODULES", root_build_dir),
+ "--output-dir",
+ _output_dir,
+ "--doxygen-config",
+ rebase_path("Doxyfile", root_build_dir),
+ "--include-paths",
+ ]
+ args += rebase_path(_doxygen_input_files, root_build_dir)
+ outputs = [ "$root_build_dir/$_output_dir/xml/index.xml" ]
+}
+
pw_doc_gen("docs") {
conf = "conf.py"
sources = [
@@ -97,19 +186,18 @@ pw_doc_gen("docs") {
output_directory = target_gen_dir
deps = [
":core_docs",
+ ":generate_doxygen",
":module_docs",
":release_notes",
- ":sphinx_themes.install",
+ ":static_assets",
":target_docs",
+ ":templates",
":third_party_docs",
+ "$dir_pigweed/seed:docs",
"$dir_pw_env_setup:python.install",
]
-}
-# Install Pigweed specific sphinx themes.
-pw_python_requirements("sphinx_themes") {
- requirements = [
- "furo",
- "sphinx_design",
- ]
+ # Required to set the PYTHONPATH so automodule, autoclass or autofunction RST
+ # directives work.
+ python_metadata_deps = [ "$dir_pw_env_setup:core_pigweed_python_packages" ]
}
diff --git a/docs/Doxyfile b/docs/Doxyfile
new file mode 100644
index 000000000..28f2b8f22
--- /dev/null
+++ b/docs/Doxyfile
@@ -0,0 +1,2796 @@
+# Doxyfile 1.9.6
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+#
+# Note:
+#
+# Use doxygen to compare the used configuration file with the template
+# configuration file:
+# doxygen -x [configFile]
+# Use doxygen to compare the used configuration file with the template
+# configuration file without replacing the environment variables or CMake type
+# replacement variables:
+# doxygen -x_noenv [configFile]
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the configuration
+# file that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME = $(PW_DOXYGEN_PROJECT_NAME)
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF =
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO =
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY = $(PW_DOXYGEN_OUTPUT_DIRECTORY)
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
+# sub-directories (in 2 levels) under the output directory of each output format
+# and will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system. Adapt CREATE_SUBDIRS_LEVEL to
+# control the number of sub-directories.
+# The default value is: NO.
+
+CREATE_SUBDIRS = NO
+
+# Controls the number of sub-directories that will be created when
+# CREATE_SUBDIRS tag is set to YES. Level 0 represents 16 directories, and every
+# level increment doubles the number of directories, resulting in 4096
+# directories at level 8 which is the default and also the maximum value. The
+# sub-directories are organized in 2 levels, the first level always has a fixed
+# number of 16 directories.
+# Minimum value: 0, maximum value: 8, default value: 8.
+# This tag requires that the tag CREATE_SUBDIRS is set to YES.
+
+CREATE_SUBDIRS_LEVEL = 8
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES = NO
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Bulgarian,
+# Catalan, Chinese, Chinese-Traditional, Croatian, Czech, Danish, Dutch, English
+# (United States), Esperanto, Farsi (Persian), Finnish, French, German, Greek,
+# Hindi, Hungarian, Indonesian, Italian, Japanese, Japanese-en (Japanese with
+# English messages), Korean, Korean-en (Korean with English messages), Latvian,
+# Lithuanian, Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese,
+# Romanian, Russian, Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish,
+# Swedish, Turkish, Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE = English
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF = "The $name class" \
+ "The $name widget" \
+ "The $name file" \
+ is \
+ provides \
+ specifies \
+ contains \
+ represents \
+ a \
+ an \
+ the
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF = NO
+
+# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
+# such as
+# /***************
+# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
+# Javadoc-style will behave just like regular comments and it will not be
+# interpreted by doxygen.
+# The default value is: NO.
+
+JAVADOC_BANNER = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# By default Python docstrings are displayed as preformatted text and doxygen's
+# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the
+# doxygen's special commands can be used and the contents of the docstring
+# documentation blocks is shown as doxygen documentation.
+# The default value is: YES.
+
+PYTHON_DOCSTRING = YES
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE = 4
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:^^"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". Note that you cannot put \n's in the value part of an alias
+# to insert newlines (in the resulting output). You can put ^^ in the value part
+# of an alias to insert a newline as if a physical newline was in the original
+# file. When you need a literal { or } or , in the value part of an alias you
+# have to escape them by means of a backslash (\), this can lead to conflicts
+# with the commands \{ and \} for these it is advised to use the version @{ and
+# @} or use a double escape (\\{ and \\})
+
+ALIASES = "rst=^^\verbatim embed:rst:leading-asterisk^^" \
+ "endrst=\endverbatim" \
+ "rstref{1}=\verbatim embed:rst:inline :ref:`\1` \endverbatim" \
+ "crossref{3}=\verbatim embed:rst:inline :\1:\2:`\3` \endverbatim" \
+ "c_macro{1}=@crossref{c,macro,\1}" \
+ "cpp_class{1}=@crossref{cpp,class,\1}" \
+ "cpp_func{1}=@crossref{cpp,func,\1}" \
+ "cpp_type{1}=@crossref{cpp,type,\1}"
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL = NO
+
+# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
+# sources only. Doxygen will then generate output that is more tailored for that
+# language. For instance, namespaces will be presented as modules, types will be
+# separated into more groups, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_SLICE = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
+# Csharp (C#), C, C++, Lex, D, PHP, md (Markdown), Objective-C, Python, Slice,
+# VHDL, Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
+# tries to guess whether the code is fixed or free formatted code, this is the
+# default for Fortran type files). For instance to make doxygen treat .inc files
+# as Fortran files (default is PHP), and .f files as C (default is Fortran),
+# use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen. When specifying no_extension you should add
+# * to the FILE_PATTERNS.
+#
+# Note see also the list of default file extension mappings.
+
+EXTENSION_MAPPING =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See https://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 5.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS = 5
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE = 0
+
+# The NUM_PROC_THREADS specifies the number of threads doxygen is allowed to use
+# during processing. When set to 0 doxygen will based this on the number of
+# cores available in the system. You can set it explicitly to a value larger
+# than 0 to get more control over the balance between CPU load and processing
+# speed. At this moment only the input processing can be done using multiple
+# threads. Since this is still an experimental feature the default is set to 1,
+# which effectively disables parallel processing. Please report any issues you
+# encounter. Generating dot graphs in parallel is controlled by the
+# DOT_NUM_THREADS setting.
+# Minimum value: 0, maximum value: 32, default value: 1.
+
+NUM_PROC_THREADS = 0
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE = NO
+
+# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
+# methods of a class will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIV_VIRTUAL = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE = YES
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC = YES
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES = NO
+
+# If this flag is set to YES, the name of an unnamed parameter in a declaration
+# will be determined by the corresponding definition. By default unnamed
+# parameters remain unnamed in the output.
+# The default value is: YES.
+
+RESOLVE_UNNAMED_PARAMS = YES
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# will also hide undocumented C++ concepts if enabled. This option has no effect
+# if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# declarations. If set to NO, these declarations will be included in the
+# documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS = NO
+
+# With the correct setting of option CASE_SENSE_NAMES doxygen will better be
+# able to match the capabilities of the underlying filesystem. In case the
+# filesystem is case sensitive (i.e. it supports files in the same directory
+# whose names only differ in casing), the option must be set to YES to properly
+# deal with such files in case they appear in the input. For filesystems that
+# are not case sensitive the option should be set to NO to properly deal with
+# output files written for symbols that only differ in casing, such as for two
+# classes, one named CLASS and the other named Class, and to also support
+# references to files without having to specify the exact matching casing. On
+# Windows (including Cygwin) and MacOS, users should typically set this option
+# to NO, whereas on Linux or other Unix flavors it should typically be set to
+# YES.
+# Possible values are: SYSTEM, NO and YES.
+# The default value is: SYSTEM.
+
+CASE_SENSE_NAMES = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_HEADERFILE tag is set to YES then the documentation for a class
+# will show which file needs to be included to use the class.
+# The default value is: YES.
+
+SHOW_HEADERFILE = YES
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file. See also section "Changing the
+# layout of pages" for information.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as documenting some parameters in
+# a documented function twice, or documenting parameters that don't exist or
+# using markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR = YES
+
+# If WARN_IF_INCOMPLETE_DOC is set to YES, doxygen will warn about incomplete
+# function parameter documentation. If set to NO, doxygen will accept that some
+# parameters have no documentation without warning.
+# The default value is: YES.
+
+WARN_IF_INCOMPLETE_DOC = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong parameter
+# documentation, but not about the absence of documentation. If EXTRACT_ALL is
+# set to YES then this flag will automatically be disabled. See also
+# WARN_IF_INCOMPLETE_DOC
+# The default value is: NO.
+
+WARN_NO_PARAMDOC = NO
+
+# If WARN_IF_UNDOC_ENUM_VAL option is set to YES, doxygen will warn about
+# undocumented enumeration values. If set to NO, doxygen will accept
+# undocumented enumeration values. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: NO.
+
+WARN_IF_UNDOC_ENUM_VAL = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
+# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
+# at the end of the doxygen process doxygen will return with a non-zero status.
+# Possible values are: NO, YES and FAIL_ON_WARNINGS.
+# The default value is: NO.
+
+WARN_AS_ERROR = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# See also: WARN_LINE_FORMAT
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT = "$file:$line: $text"
+
+# In the $text part of the WARN_FORMAT command it is possible that a reference
+# to a more specific place is given. To make it easier to jump to this place
+# (outside of doxygen) the user can define a custom "cut" / "paste" string.
+# Example:
+# WARN_LINE_FORMAT = "'vi $file +$line'"
+# See also: WARN_FORMAT
+# The default value is: at line $line of file $file.
+
+WARN_LINE_FORMAT = "at line $line of file $file"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr). In case the file specified cannot be opened for writing the
+# warning and error messages are written to standard error. When as file - is
+# specified the warning and error messages are written to standard output
+# (stdout).
+
+WARN_LOGFILE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT = $(PW_DOXYGEN_INPUT)
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see:
+# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
+# See also: INPUT_FILE_ENCODING
+# The default value is: UTF-8.
+
+INPUT_ENCODING = UTF-8
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses The INPUT_FILE_ENCODING tag can be used to specify
+# character encoding on a per file pattern basis. Doxygen will compare the file
+# name with each pattern and apply the encoding instead of the default
+# INPUT_ENCODING) if there is a match. The character encodings are a list of the
+# form: pattern=encoding (like *.php=ISO-8859-1). See cfg_input_encoding
+# "INPUT_ENCODING" for further information on supported encodings.
+
+INPUT_FILE_ENCODING =
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# Note the list of default checked file patterns might differ from the list of
+# default file extension mappings.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml,
+# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C
+# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd,
+# *.vhdl, *.ucf, *.qsf and *.ice.
+
+FILE_PATTERNS = *.c \
+ *.cc \
+ *.cxx \
+ *.cpp \
+ *.c++ \
+ *.java \
+ *.ii \
+ *.ixx \
+ *.ipp \
+ *.i++ \
+ *.inl \
+ *.idl \
+ *.ddl \
+ *.odl \
+ *.h \
+ *.hh \
+ *.hxx \
+ *.hpp \
+ *.h++ \
+ *.l \
+ *.cs \
+ *.d \
+ *.php \
+ *.php4 \
+ *.php5 \
+ *.phtml \
+ *.inc \
+ *.m \
+ *.markdown \
+ *.md \
+ *.mm \
+ *.dox \
+ *.f90 \
+ *.f95 \
+ *.f03 \
+ *.f08 \
+ *.f18 \
+ *.f \
+ *.for \
+ *.vhd \
+ *.vhdl \
+ *.ucf \
+ *.qsf \
+ *.ice
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS = *out/* \
+ *node_modules/* \
+ *third_party/* \
+ *_test.cc
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# ANamespace::AClass, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS = *
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that doxygen will use the data processed and written to standard output
+# for further processing, therefore nothing else, like debug statements or used
+# commands (so in case of a Windows batch file always use @echo OFF), should be
+# written to standard output.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
+# The Fortran standard specifies that for fixed formatted Fortran code all
+# characters from position 72 are to be considered as comment. A common
+# extension is to allow longer lines before the automatic comment starts. The
+# setting FORTRAN_COMMENT_AFTER will also make it possible that longer lines can
+# be processed before the automatic comment starts.
+# Minimum value: 7, maximum value: 10000, default value: 72.
+
+FORTRAN_COMMENT_AFTER = 72
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# entity all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS = YES
+
+# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
+# clang parser (see:
+# http://clang.llvm.org/) for more accurate parsing at the cost of reduced
+# performance. This can be particularly helpful with template rich C++ code for
+# which doxygen's built-in parser lacks the necessary type information.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+# The default value is: NO.
+
+CLANG_ASSISTED_PARSING = NO
+
+# If the CLANG_ASSISTED_PARSING tag is set to YES and the CLANG_ADD_INC_PATHS
+# tag is set to YES then doxygen will add the directory of each input to the
+# include path.
+# The default value is: YES.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_ADD_INC_PATHS = YES
+
+# If clang assisted parsing is enabled you can provide the compiler with command
+# line options that you would normally use when invoking the compiler. Note that
+# the include paths will already be set by doxygen for the files and directories
+# specified with INPUT and INCLUDE_PATH.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_OPTIONS =
+
+# If clang assisted parsing is enabled you can provide the clang parser with the
+# path to the directory containing a file called compile_commands.json. This
+# file is the compilation database (see:
+# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the
+# options used when the source files were built. This is equivalent to
+# specifying the -p option to a clang tool, such as clang-check. These options
+# will then be passed to the parser. Any options specified with CLANG_OPTIONS
+# will be added as well.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+
+CLANG_DATABASE_PATH =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX = YES
+
+# The IGNORE_PREFIX tag can be used to specify a prefix (or a list of prefixes)
+# that should be ignored while generating the index headers. The IGNORE_PREFIX
+# tag works for classes, function and member names. The entity will be placed in
+# the alphabetical list under the first letter of the entity name that remains
+# after removing the prefix.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML = NO
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER =
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER =
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# Note: Since the styling of scrollbars can currently not be overruled in
+# Webkit/Chromium, the styling will be left out of the default doxygen.css if
+# one or more extra stylesheets have been specified. So if scrollbar
+# customization is desired it has to be added explicitly. For an example see the
+# documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET =
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES =
+
+# The HTML_COLORSTYLE tag can be used to specify if the generated HTML output
+# should be rendered with a dark or light theme.
+# Possible values are: LIGHT always generate light mode output, DARK always
+# generate dark mode output, AUTO_LIGHT automatically set the mode according to
+# the user preference, use light mode if no preference is set (the default),
+# AUTO_DARK automatically set the mode according to the user preference, use
+# dark mode if no preference is set and TOGGLE allow to user to switch between
+# light and dark mode via a button.
+# The default value is: AUTO_LIGHT.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE = AUTO_LIGHT
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a color-wheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use gray-scales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via JavaScript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have JavaScript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_MENUS = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see:
+# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To
+# create a documentation set, doxygen will generate a Makefile in the HTML
+# output directory. Running make will produce the docset in that directory and
+# running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
+# genXcode/_index.html for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME = "Doxygen generated docs"
+
+# This tag determines the URL of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDURL =
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# on Windows. In the beginning of 2021 Microsoft took the original page, with
+# a.o. the download links, offline the HTML help workshop was already many years
+# in maintenance mode). You can download the HTML help workshop from the web
+# archives at Installation executable (see:
+# http://web.archive.org/web/20160201063255/http://download.microsoft.com/downlo
+# ad/0/A/9/0A939EF6-E31C-430F-A3DF-DFAE7960D564/htmlhelp.exe).
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the main .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS =
+
+# The QHG_LOCATION tag can be used to specify the location (absolute path
+# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to
+# run qhelpgenerator on the generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine tune the look of the index (see "Fine-tuning the output"). As an
+# example, the default style sheet generated by doxygen has an example that
+# shows how to put an image at the root of the tree instead of the PROJECT_NAME.
+# Since the tree basically has the same information as the tab index, you could
+# consider setting DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW = NO
+
+# When both GENERATE_TREEVIEW and DISABLE_INDEX are set to YES, then the
+# FULL_SIDEBAR option determines if the side bar is limited to only the treeview
+# area (value NO) or if it should extend to the full height of the window (value
+# YES). Setting this to YES gives a layout similar to
+# https://docs.readthedocs.io with more room for contents, but less room for the
+# project logo, title, and description. If either GENERATE_TREEVIEW or
+# DISABLE_INDEX is set to NO, this option has no effect.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FULL_SIDEBAR = NO
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW = NO
+
+# If the OBFUSCATE_EMAILS tag is set to YES, doxygen will obfuscate email
+# addresses.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+OBFUSCATE_EMAILS = YES
+
+# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
+# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
+# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
+# the HTML output. These images will generally look nicer at scaled resolutions.
+# Possible values are: png (the default) and svg (looks nicer but requires the
+# pdf2svg or inkscape tool).
+# The default value is: png.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FORMULA_FORMAT = png
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE = 10
+
+# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
+# to create new LaTeX commands to be used in formulas as building blocks. See
+# the section "Including formulas" for details.
+
+FORMULA_MACROFILE =
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side JavaScript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX = NO
+
+# With MATHJAX_VERSION it is possible to specify the MathJax version to be used.
+# Note that the different versions of MathJax have different requirements with
+# regards to the different settings, so it is possible that also other MathJax
+# settings have to be changed when switching between the different MathJax
+# versions.
+# Possible values are: MathJax_2 and MathJax_3.
+# The default value is: MathJax_2.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_VERSION = MathJax_2
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. For more details about the output format see MathJax
+# version 2 (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) and MathJax version 3
+# (see:
+# http://docs.mathjax.org/en/latest/web/components/output.html).
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility. This is the name for Mathjax version 2, for MathJax version 3
+# this will be translated into chtml), NativeMML (i.e. MathML. Only supported
+# for NathJax 2. For MathJax version 3 chtml will be used instead.), chtml (This
+# is the name for Mathjax version 3, for MathJax version 2 this will be
+# translated into HTML-CSS) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment. The default value is:
+# - in case of MathJax version 2: https://cdn.jsdelivr.net/npm/mathjax@2
+# - in case of MathJax version 3: https://cdn.jsdelivr.net/npm/mathjax@3
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH =
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# for MathJax version 2 (see https://docs.mathjax.org/en/v2.7-latest/tex.html
+# #tex-and-latex-extensions):
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# For example for MathJax version 3 (see
+# http://docs.mathjax.org/en/latest/input/tex/extensions/index.html):
+# MATHJAX_EXTENSIONS = ams
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using JavaScript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see:
+# https://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see:
+# https://xapian.org/). See the section "External Indexing and Searching" for
+# details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when not enabling USE_PDFLATEX the default is latex when enabling
+# USE_PDFLATEX the default is pdflatex and when in the later case latex is
+# chosen this is overwritten by pdflatex. For specific output languages the
+# default can have been set differently, this depends on the implementation of
+# the output language.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME =
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# Note: This tag is used in the Makefile / make.bat.
+# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file
+# (.tex).
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME = makeindex
+
+# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to
+# generate index for LaTeX. In case there is no backslash (\) as first character
+# it will be automatically added in the LaTeX code.
+# Note: This tag is used in the generated output file (.tex).
+# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.
+# The default value is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_MAKEINDEX_CMD = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE = a4
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES =
+
+# The LATEX_HEADER tag can be used to specify a user-defined LaTeX header for
+# the generated LaTeX document. The header should contain everything until the
+# first chapter. If it is left blank doxygen will generate a standard header. It
+# is highly recommended to start with a default header using
+# doxygen -w latex new_header.tex new_footer.tex new_stylesheet.sty
+# and then modify the file new_header.tex. See also section "Doxygen usage" for
+# information on how to generate the default header that doxygen normally uses.
+#
+# Note: Only use a user-defined header if you know what you are doing!
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. The following
+# commands have a special meaning inside the header (and footer): For a
+# description of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER =
+
+# The LATEX_FOOTER tag can be used to specify a user-defined LaTeX footer for
+# the generated LaTeX document. The footer should contain everything after the
+# last chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer. See also section "Doxygen
+# usage" for information on how to generate the default footer that doxygen
+# normally uses. Note: Only use a user-defined footer if you know what you are
+# doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as
+# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX
+# files. Set this option to YES, to get a higher quality PDF documentation.
+#
+# See also section LATEX_CMD_NAME for selecting the engine.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP = NO
+
+# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
+# path from which the emoji images will be read. If a relative path is entered,
+# it will be relative to the LATEX_OUTPUT directory. If left blank the
+# LATEX_OUTPUT directory will be used.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EMOJI_DIRECTORY =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's
+# configuration file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's configuration file. A template extensions file can be
+# generated using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML = YES
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING = YES
+
+# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include
+# namespace members in file scope as well, matching the HTML output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_NS_MEMB_FILE_SCOPE = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT = docbook
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# the structure of the code including all documentation. Note that this feature
+# is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION = YES
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor. Note that the INCLUDE_PATH is not recursive, so the setting of
+# RECURSIVE has no effect here.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED = __cplusplus \
+ PW_LOCKABLE= \
+ PW_PRINTF_FORMAT(...)= \
+ PW_CONSTEXPR_CPP20= \
+ PW_EXCLUSIVE_LOCK_FUNCTION(...)= \
+ PW_EXCLUSIVE_TRYLOCK_FUNCTION(...)= \
+ PW_UNLOCK_FUNCTION(...)= \
+ PW_NO_LOCK_SAFETY_ANALYSIS= \
+ PW_CXX_STANDARD_IS_SUPPORTED(...)=1 \
+ PW_EXTERN_C_START= \
+ PW_LOCKS_EXCLUDED(...)= \
+ PW_EXCLUSIVE_LOCKS_REQUIRED(...)= \
+ PW_GUARDED_BY(...)= \
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT = NO
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS = 0
+
+# DOT_COMMON_ATTR is common attributes for nodes, edges and labels of
+# subgraphs. When you want a differently looking font in the dot files that
+# doxygen generates you can specify fontname, fontcolor and fontsize attributes.
+# For details please see <a href=https://graphviz.org/doc/info/attrs.html>Node,
+# Edge and Graph Attributes specification</a> You need to make sure dot is able
+# to find the font, which can be done by putting it in a standard location or by
+# setting the DOTFONTPATH environment variable or by setting DOT_FONTPATH to the
+# directory containing the font. Default graphviz fontsize is 14.
+# The default value is: fontname=Helvetica,fontsize=10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_COMMON_ATTR = "fontname=Helvetica,fontsize=10"
+
+# DOT_EDGE_ATTR is concatenated with DOT_COMMON_ATTR. For elegant style you can
+# add 'arrowhead=open, arrowtail=open, arrowsize=0.5'. <a
+# href=https://graphviz.org/doc/info/arrows.html>Complete documentation about
+# arrows shapes.</a>
+# The default value is: labelfontname=Helvetica,labelfontsize=10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_EDGE_ATTR = "labelfontname=Helvetica,labelfontsize=10"
+
+# DOT_NODE_ATTR is concatenated with DOT_COMMON_ATTR. For view without boxes
+# around nodes set 'shape=plain' or 'shape=plaintext' <a
+# href=https://www.graphviz.org/doc/info/shapes.html>Shapes specification</a>
+# The default value is: shape=box,height=0.2,width=0.4.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4"
+
+# You can set the path where dot can find font specified with fontname in
+# DOT_COMMON_ATTR and others dot attributes.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH =
+
+# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a
+# graph for each documented class showing the direct and indirect inheritance
+# relations. In case HAVE_DOT is set as well dot will be used to draw the graph,
+# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set
+# to TEXT the direct and indirect inheritance relations will be shown as texts /
+# links.
+# Possible values are: NO, YES, TEXT and GRAPH.
+# The default value is: YES.
+
+CLASS_GRAPH = NO
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH = NO
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies. See also the chapter Grouping
+# in the manual.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag UML_LOOK is set to YES.
+
+UML_LIMIT_NUM_FIELDS = 10
+
+# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and
+# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS
+# tag is set to YES, doxygen will add type and arguments for attributes and
+# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen
+# will not generate fields with class member information in the UML graphs. The
+# class diagrams will look similar to the default class diagrams but using UML
+# notation for the relationships.
+# Possible values are: NO, YES and NONE.
+# The default value is: NO.
+# This tag requires that the tag UML_LOOK is set to YES.
+
+DOT_UML_DETAILS = NO
+
+# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters
+# to display on a single line. If the actual line length exceeds this threshold
+# significantly it will wrapped across multiple lines. Some heuristics are apply
+# to avoid ugly line breaks.
+# Minimum value: 0, maximum value: 1000, default value: 17.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_WRAP_THRESHOLD = 17
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH = NO
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH = YES
+
+# The DIR_GRAPH_MAX_DEPTH tag can be used to limit the maximum number of levels
+# of child directories generated in directory dependency graphs by dot.
+# Minimum value: 1, maximum value: 25, default value: 1.
+# This tag requires that the tag DIRECTORY_GRAPH is set to YES.
+
+DIR_GRAPH_MAX_DEPTH = 1
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT = png
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG = NO
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file or to the filename of jar file
+# to be used. If left blank, it is assumed PlantUML is not used or called during
+# a preprocessing step. Doxygen will generate a warning when it encounters a
+# \startuml command in this case and will not generate output for the diagram.
+
+PLANTUML_JAR_PATH =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH = 0
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# Note: This tag requires that UML_LOOK isn't set, i.e. the doxygen internal
+# graphical representation for inheritance and collaboration diagrams is used.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate
+# files that are used to generate the various graphs.
+#
+# Note: This setting is not only used for dot files but also for msc temporary
+# files.
+# The default value is: YES.
+
+DOT_CLEANUP = YES
diff --git a/docs/_static/css/pigweed.css b/docs/_static/css/pigweed.css
index 5e87cafbc..bea0fee45 100644
--- a/docs/_static/css/pigweed.css
+++ b/docs/_static/css/pigweed.css
@@ -25,7 +25,7 @@
text-transform: uppercase;
}
.sidebar-brand-text {
- font-size: 2.5rem;
+ font-size: 2.5rem;
}
/********** General document coloring ***********/
@@ -93,6 +93,16 @@ code.literal {
background: var(--color-text-selection-background);
}
+
+/* Make dark mode 'code-block:: :emphasize-lines:' background color readable. */
+body[data-theme="dark"] .highlight .hll {
+ background-color: var(--color-code-hll-background);
+}
+
+body[data-theme="light"] .highlight .hll {
+ background-color: var(--color-code-hll-background);
+}
+
/* Use normal mixed-case for h4, h5, and h6 */
h4, h5, h6 {
text-transform: none;
@@ -115,3 +125,54 @@ h4, h5, h6 {
-webkit-mask-image: var(--icon--check-circle-fill);
mask-image: var(--icon--check-circle-fill);
}
+
+/* Sub sections in breathe doxygen output.
+ E.g 'Public Functions' or 'Public Static Functions' in a class. */
+.breathe-sectiondef > p.breathe-sectiondef-title {
+ font-weight: bold;
+ font-size: var(--font-size--normal);
+}
+
+/* Support taglines inline with page titles */
+section.with-subtitle > h1 {
+ display: inline;
+}
+
+/* Restore the padding to the top of the page that was removed by making the
+ h1 element inline */
+section.with-subtitle {
+ padding-top: 1.5em;
+}
+
+.section-subtitle {
+ display: inline;
+ font-size: larger;
+ font-weight: bold;
+}
+
+/* Styling for module doc section buttons */
+ul.pw-module-section-buttons {
+ display: flex;
+ justify-content: center;
+ padding: 0;
+}
+
+li.pw-module-section-button {
+ display: inline;
+ list-style-type: none;
+ padding: 0 4px;
+}
+
+li.pw-module-section-button p {
+ display: inline;
+}
+
+li.pw-module-section-button p a {
+ background-color: var(--color-section-button) !important;
+ border-color: var(--color-section-button) !important;
+}
+
+li.pw-module-section-button p a:hover {
+ background-color: var(--color-section-button-hover) !important;
+ border-color: var(--color-section-button-hover) !important;
+}
diff --git a/docs/_static/pw_logo.ico b/docs/_static/pw_logo.ico
new file mode 100644
index 000000000..545efa685
--- /dev/null
+++ b/docs/_static/pw_logo.ico
Binary files differ
diff --git a/docs/_static/pw_logo.svg b/docs/_static/pw_logo.svg
new file mode 100644
index 000000000..4ebaf2a48
--- /dev/null
+++ b/docs/_static/pw_logo.svg
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ width="82.519623"
+ height="96"
+ viewBox="0 0 21.833318 25.4"
+ version="1.1"
+ id="svg5"
+ inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+ sodipodi:docname="pw-logo.svg"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview7"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ inkscape:document-units="px"
+ showgrid="true"
+ inkscape:zoom="5.1754899"
+ inkscape:cx="34.29627"
+ inkscape:cy="64.824781"
+ inkscape:window-width="2131"
+ inkscape:window-height="1334"
+ inkscape:window-x="1307"
+ inkscape:window-y="72"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="layer1"
+ inkscape:pageshadow="2"
+ fit-margin-top="0"
+ fit-margin-left="0.25981"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ width="96px">
+ <inkscape:grid
+ type="xygrid"
+ id="grid899"
+ originx="-0.29938562"
+ originy="0.007612793" />
+ </sodipodi:namedview>
+ <defs
+ id="defs2" />
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(-0.29938553,0.00761281)">
+ <g
+ id="g934"
+ transform="matrix(6.0036869,0,0,6.0036869,-1.8419922,0.03809209)">
+ <path
+ style="fill:#f100f7;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.1616449,3.4336061 C 1.9091824,3.5326629 1.5875433,1.5193339 1.8309272,0.85077532 1.9385896,0.55503414 2.031071,0.2463175 2.4114224,-0.00401171 2.8544419,-0.05203268 2.6565454,0.38084903 2.5067113,1.256888 2.3568774,2.1329272 2.1616453,3.4336062 2.1616449,3.4336061 Z"
+ id="path2515"
+ sodipodi:nodetypes="cscsc" />
+ <path
+ style="fill:#b700d7;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.2191106,3.4109307 C 2.0678012,3.6359974 2.0167095,3.0174321 1.7528477,2.3566876 1.4889859,1.6959431 0.94922328,2.1927191 1.0001916,1.7425692 1.1506181,1.5574268 1.8838262,1.5874003 2.2588512,2.3931613 2.6338765,3.1989223 2.219111,3.4109305 2.2191106,3.4109307 Z"
+ id="path2413"
+ sodipodi:nodetypes="cscsc" />
+ <path
+ style="fill:#951798;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.1237722,3.5148295 C 2.3150351,3.7071003 2.3599863,3.0564818 2.6763817,2.4192218 2.9059426,1.9568575 3.3825927,1.9923461 3.1850672,1.6011417 2.9975756,1.4621979 2.4269145,1.8007494 2.2115779,2.6630282 1.9962408,3.525307 2.1237717,3.5148295 2.1237722,3.5148295 Z"
+ id="path2513"
+ sodipodi:nodetypes="cscsc"
+ inkscape:transform-center-x="-0.045186222"
+ inkscape:transform-center-y="-0.11748418" />
+ <path
+ style="fill:#b700d7;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.16657,3.1630056 C 1.935386,3.3047925 2.3927779,2.3796051 2.1943537,1.717073 2.0826164,1.3439858 1.8924319,1.040618 2.0299315,0.81735074 2.252724,0.72377787 2.5506558,1.2084909 2.5567396,2.0972302 2.5628236,2.9859696 2.1665704,3.1630056 2.16657,3.1630056 Z"
+ id="path2517"
+ sodipodi:nodetypes="cscsc" />
+ <path
+ style="fill:#fb71fe;fill-opacity:1;stroke-width:0.216574"
+ d="M 2.1694107,3.3555634 C 1.972559,3.5421082 1.8165702,2.9074538 1.703537,2.2050082 1.5905038,1.5025626 1.0219277,1.2858513 1.2514214,1.001359 1.637517,0.63319073 1.9169663,1.6332865 2.1067716,2.5015425 c 0.1898056,0.8682561 0.062639,0.8540209 0.062639,0.8540209 z"
+ id="path2519"
+ sodipodi:nodetypes="cscsc" />
+ <path
+ style="fill:#00a100;fill-opacity:1;stroke-width:0.264583"
+ d="M 2.1055809,4.223121 C 2.0551089,3.1942573 2.0383098,2.9291347 2.6324934,2.5000643 3.2266767,2.070994 3.6923741,2.4674508 3.9933288,2.5699226 3.718952,2.7370717 3.4647904,2.6395555 3.1496058,2.781203 2.7064121,2.9803792 2.738338,3.0037867 2.6187891,3.2423382 2.2503826,3.9774682 2.4762183,4.1592185 2.1055809,4.223121 Z"
+ id="path2415"
+ sodipodi:nodetypes="cscssc" />
+ <path
+ style="fill:#00cf00;fill-opacity:1;stroke-width:0.272503"
+ d="M 2.0975886,4.2112405 C 1.7892123,4.1687886 2.0455122,3.5896312 1.4981867,2.9104616 1.0529822,2.3580126 0.74373118,3.1368263 0.36812698,2.8108117 0.69766349,1.9826211 1.4696657,1.6318979 1.8391765,2.643645 c 0.097896,0.2680453 0.1875581,0.3804525 0.2399067,0.5618191 0.1452432,0.5032081 0.2932885,1.0610147 0.018505,1.0057764 z"
+ id="path2411"
+ sodipodi:nodetypes="cscssc" />
+ </g>
+ </g>
+</svg>
diff --git a/docs/automated_analysis.rst b/docs/automated_analysis.rst
index 23a31780d..8612cda0b 100644
--- a/docs/automated_analysis.rst
+++ b/docs/automated_analysis.rst
@@ -45,7 +45,7 @@ like ``static_analysis`` and ``python_checks.gn_python_check``. See the
your Pigweed-based project.
.. _PyLint: https://pylint.org/
-.. _.pylintrc: https://cs.opensource.google/pigweed/pigweed/+/main:.pylintrc
+.. _.pylintrc: https://cs.pigweed.dev/pigweed/+/main:.pylintrc
Mypy
----
@@ -88,7 +88,7 @@ produce any runnable binaries: it simply analyzes the source files.
.. _clang-tidy: https://clang.llvm.org/extra/clang-tidy/
.. _Abseil: https://abseil.io/
-.. _.clang-tidy: https://cs.opensource.google/pigweed/pigweed/+/main:.clang-tidy
+.. _.clang-tidy: https://cs.pigweed.dev/pigweed/+/main:.clang-tidy
.. _Clang Static Analyzers: https://clang-analyzer.llvm.org/available_checks.html
@@ -106,11 +106,11 @@ described in this section. For more detail about these sanitizers, see the
We use the default ``-fsanitize=undefined`` option.
.. note::
- Pigweed does not currently support msan. See https://bugs.pigweed.dev/560
- for details.
+ Pigweed does not currently support msan. See
+ https://issuetracker.google.com/234876100 for details.
The exact configurations we use for these sanitizers are in
-`pw_toolchain/host_clang/BUILD.gn <https://cs.opensource.google/pigweed/pigweed/+/main:pw_toolchain/host_clang/BUILD.gn>`_.
+`pw_toolchain/host_clang/BUILD.gn <https://cs.pigweed.dev/pigweed/+/main:pw_toolchain/host_clang/BUILD.gn>`_.
You can see the current status of the sanitizer builds in the `Pigweed CI
console`_, as ``pigweed-linux-san-*``.
@@ -151,7 +151,7 @@ these can be added to individual presubmit steps (`examples`_). You can also
directly include the `python_checks.gn_python_lint`_ presubmit step.
.. _examples: https://cs.opensource.google/search?q=file:pigweed_presubmit.py%20%22python.lint%22&sq=&ss=pigweed%2Fpigweed
-.. _python_checks.gn_python_lint: https://cs.opensource.google/pigweed/pigweed/+/main:pw_presubmit/py/pw_presubmit/python_checks.py?q=file:python_checks.py%20gn_python_lint&ss=pigweed%2Fpigweed
+.. _python_checks.gn_python_lint: https://cs.pigweed.dev/pigweed/+/main:pw_presubmit/py/pw_presubmit/python_checks.py?q=file:python_checks.py%20gn_python_lint&ss=pigweed%2Fpigweed
clang-tidy
==========
@@ -166,7 +166,7 @@ which checks are executed. See the `clang documentation`_ for a discussion of ho
the tool chooses which ``.clang-tidy`` files to apply when run on a particular
source file.
-.. _pw_toolchain/static_analysis_toolchain.gni: https://cs.opensource.google/pigweed/pigweed/+/main:pw_toolchain/static_analysis_toolchain.gni
+.. _pw_toolchain/static_analysis_toolchain.gni: https://cs.pigweed.dev/pigweed/+/main:pw_toolchain/static_analysis_toolchain.gni
.. _clang documentation: https://clang.llvm.org/extra/clang-tidy/
Clang sanitizers
diff --git a/docs/build_system.rst b/docs/build_system.rst
index 6cd32a54a..a7a5c8e8e 100644
--- a/docs/build_system.rst
+++ b/docs/build_system.rst
@@ -306,10 +306,10 @@ present, Ninja will build this group when invoked without arguments.
Optimization levels
^^^^^^^^^^^^^^^^^^^
-Pigweed's ``//BUILD.gn`` defines the ``pw_default_optimization_level`` build
+Pigweed's ``//BUILD.gn`` defines the ``pw_DEFAULT_C_OPTIMIZATION_LEVEL`` build
arg, which specifies the optimization level to use for the default groups
(``host``, ``stm32f429i``, etc.). The supported values for
-``pw_default_optimization_level`` are:
+``pw_DEFAULT_C_OPTIMIZATION_LEVEL`` are:
* ``debug`` -- create debugging-friendly binaries (``-Og``)
* ``size_optimized`` -- optimize for size (``-Os``)
@@ -317,10 +317,11 @@ arg, which specifies the optimization level to use for the default groups
(``-O2``)
Pigweed defines versions of its groups in ``//BUILD.gn`` for each optimization
-level. Rather than relying on ``pw_default_optimization_level``, you may
-directly build a group at the desired optimization level:
-``<group>_<optimization>``. Examples include ``host_clang_debug``,
-``host_gcc_size_optimized``, and ``stm32f429i_speed_optimized``.
+level specified in the ``pw_C_OPTIMIZATION_LEVELS`` list. Rather than relying
+on ``pw_DEFAULT_C_OPTIMIZATION_LEVEL``, you may directly build a group at the
+desired optimization level: ``<group>_<optimization>``. Examples include
+``host_clang_debug``, ``host_gcc_size_optimized``, and
+``stm32f429i_speed_optimized``.
Upstream GN target groups
^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -360,7 +361,7 @@ Next runtime sanitizers supported:
* ``integer``: Checks for undefined or suspicious integer behavior.
* ``float-divide-by-zero``: Checks for floating point division by zero.
- * ``implicit-conversion"``: Checks for suspicious behavior of implicit conversions.
+ * ``implicit-conversion``: Checks for suspicious behavior of implicit conversions.
* ``nullability``: Checks for null as function arg, lvalue and return type.
These additional checks are heuristic and may not correspond to undefined
@@ -454,7 +455,7 @@ build. For information on Pigweed's target system, refer to
The empty toolchain
-------------------
-Pigweed's ``BUILDCONFIG.gn`` sets the project's default toolchain to a "empty"
+Pigweed's ``BUILDCONFIG.gn`` sets the project's default toolchain to an "empty"
toolchain which does not specify any compilers or override any build arguments.
Downstream projects are recommended to do the same, following the steps
described in :ref:`top-level-build` to configure builds for each of their
@@ -693,6 +694,7 @@ compatible with only a host os;
target_compatible_with = select({
"@platforms//os:windows": [],
"@platforms//os:linux": [],
+ "@platforms//os:ios": [],
"@platforms//os:macos": [],
"//conditions:default": ["@platforms//:incompatible"],
}),
@@ -764,9 +766,10 @@ however it is possible to override this from the command line. e.g.
.. _Bazel config reference: https://docs.bazel.build/versions/main/skylark/config.html
+.. _docs-build_system-bazel_configuration:
-Pigweeds configuration
-^^^^^^^^^^^^^^^^^^^^^^
+Pigweed's Bazel configuration
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Pigweeds Bazel configuration API is designed to be distributed across the
Pigweed repository and/or your downstream repository. If you are coming from
GN's centralized configuration API it might be useful to think about
diff --git a/docs/concepts/index.rst b/docs/concepts/index.rst
index 1caf7b597..a08bbf81d 100644
--- a/docs/concepts/index.rst
+++ b/docs/concepts/index.rst
@@ -109,3 +109,21 @@ to keep in mind:
* Setting up new projects to use Pigweed is currently not very easy, but we are
working to address that. In the meantime, join the Pigweed community on
`Discord <https://discord.gg/M9NSeTA>`_ to get help.
+
+Supported language versions
+===========================
+
+C++
+---
+Most Pigweed code requires C++17, but a few modules, such as
+:ref:`module-pw_kvs` and :ref:`module-pw_tokenizer`, work with C++14. All
+Pigweed code is compatible with C++20. Pigweed defines GN toolchains for
+building with C++14 and C++20; see :ref:`target-host` target documentation for
+more information. For Bazel, the C++ standard version can be configured using
+the `--cxxopt flag <https://bazel.build/docs/user-manual#cxxopt>`_.
+
+.. _docs-concepts-python-version:
+
+Python
+------
+Pigweed officially supports Python 3.8, 3.9, and 3.10.
diff --git a/docs/conf.py b/docs/conf.py
index 519b87bc9..f99f920e5 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -17,7 +17,7 @@ from datetime import date
import sphinx
# The suffix of source filenames.
-source_suffix = ['.rst']
+source_suffix = ['.rst', '.md']
# The master toctree document. # inclusive-language: ignore
master_doc = 'index'
@@ -35,18 +35,41 @@ version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1.0'
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'pigweed-code-light'
-pygments_dark_style = 'pigweed-code'
+# The class of the Pygments (syntax highlighting) style to use.
+pygments_style = 'pw_console.pigweed_code_style.PigweedCodeLightStyle'
+pygments_dark_style = 'pw_console.pigweed_code_style.PigweedCodeStyle'
extensions = [
'pw_docgen.sphinx.google_analytics', # Enables optional Google Analytics
+ 'pw_docgen.sphinx.module_metadata',
'sphinx.ext.autodoc', # Automatic documentation for Python code
'sphinx.ext.napoleon', # Parses Google-style docstrings
+ 'sphinxarg.ext', # Automatic documentation of Python argparse
'sphinxcontrib.mermaid',
'sphinx_design',
+ 'myst_parser',
+ 'breathe',
+ 'sphinx_copybutton', # Copy-to-clipboard button on code blocks
]
+myst_enable_extensions = [
+ # "amsmath",
+ "colon_fence",
+ # "deflist",
+ "dollarmath",
+ # "html_admonition",
+ # "html_image",
+ # "linkify",
+ # "replacements",
+ # "smartquotes",
+ # "substitution",
+ # "tasklist",
+]
+
+# When a user clicks the copy-to-clipboard button the `$ ` prompt should not be
+# copied: https://sphinx-copybutton.readthedocs.io/en/latest/use.html
+copybutton_prompt_text = "$ "
+
_DIAG_HTML_IMAGE_FORMAT = 'SVG'
blockdiag_html_image_format = _DIAG_HTML_IMAGE_FORMAT
nwdiag_html_image_format = _DIAG_HTML_IMAGE_FORMAT
@@ -73,6 +96,9 @@ html_use_smartypants = True
# If false, no module index is generated.
html_domain_indices = True
+html_favicon = 'docs/_static/pw_logo.ico'
+html_logo = 'docs/_static/pw_logo.svg'
+
# If false, no index is generated.
html_use_index = True
@@ -92,9 +118,10 @@ html_static_path = ['docs/_static']
# or fully qualified paths (eg. https://...)
html_css_files = [
'css/pigweed.css',
-
# Needed for Inconsolata font.
'https://fonts.googleapis.com/css2?family=Inconsolata&display=swap',
+ # FontAwesome for mermaid and sphinx-design
+ "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css",
]
html_theme_options = {
@@ -121,9 +148,15 @@ html_theme_options = {
'color-inline-code-border': '#cccccc',
'color-text-selection-background': '#1d5fad',
'color-text-selection-foreground': '#ffffff',
+ # Background color for focused headings.
+ 'color-highlight-on-target': '#ffffcc',
+ # Background color emphasized code lines.
+ 'color-code-hll-background': '#ffffcc',
+ 'color-section-button': '#b529aa',
+ 'color-section-button-hover': '#fb71fe',
},
'dark_css_variables': {
- 'color-sidebar-brand-text': '#e815a5',
+ 'color-sidebar-brand-text': '#fb71fe',
'color-sidebar-search-border': '#e815a5',
'color-sidebar-link-text--top-level': '#ff79c6',
'color-sidebar-link-text': '#8be9fd',
@@ -144,9 +177,23 @@ html_theme_options = {
'color-inline-code-border': '#575757',
'color-text-selection-background': '#2674bf',
'color-text-selection-foreground': '#ffffff',
+ # Background color for focused headings.
+ 'color-highlight-on-target': '#ffc55140',
+ # Background color emphasized code lines.
+ 'color-code-hll-background': '#ffc55140',
+ 'color-section-button': '#fb71fe',
+ 'color-section-button-hover': '#b529aa',
},
}
+mermaid_version = '9.4.0'
+# TODO(tonymd): Investigate if ESM only v10 Mermaid can be used.
+# This does not work:
+# mermaid_init_js = '''
+# import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
+# mermaid.initialize({ startOnLoad: true });
+# '''
+
# Output file base name for HTML help builder.
htmlhelp_basename = 'Pigweeddoc'
@@ -158,10 +205,51 @@ man_pages = [('index', 'pigweed', 'Pigweed', ['Google'], 1)]
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- ('index', 'Pigweed', 'Pigweed', 'Google', 'Pigweed', 'Firmware framework',
- 'Miscellaneous'),
+ (
+ 'index',
+ 'Pigweed',
+ 'Pigweed',
+ 'Google',
+ 'Pigweed',
+ 'Firmware framework',
+ 'Miscellaneous',
+ ),
]
+exclude_patterns = ['docs/templates/**']
+
+breathe_projects = {
+ # Assuming doxygen output is at out/docs/doxygen/
+ # This dir should be relative to out/docs/gen/docs/pw_docgen_tree/
+ "Pigweed": "./../../../doxygen/xml/",
+}
+
+breathe_default_project = "Pigweed"
+
+breathe_debug_trace_directives = True
+
+# Treat these as valid attributes in function signatures.
+cpp_id_attributes = [
+ "PW_EXTERN_C_START",
+ "PW_NO_LOCK_SAFETY_ANALYSIS",
+]
+# This allows directives like this to work:
+# .. cpp:function:: inline bool try_lock_for(
+# chrono::SystemClock::duration timeout) PW_EXCLUSIVE_TRYLOCK_FUNCTION(true)
+cpp_paren_attributes = [
+ "PW_EXCLUSIVE_TRYLOCK_FUNCTION",
+ "PW_EXCLUSIVE_LOCK_FUNCTION",
+ "PW_UNLOCK_FUNCTION",
+ "PW_NO_SANITIZE",
+]
+# inclusive-language: disable
+# Info on cpp_id_attributes and cpp_paren_attributes
+# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-cpp_id_attributes
+# inclusive-language: enable
+
+# Disable Python type hints
+# autodoc_typehints = 'none'
+
def do_not_skip_init(app, what, name, obj, would_skip, options):
if name == "__init__":
diff --git a/docs/contributing.rst b/docs/contributing.rst
index beb43f1de..13ab4a9dc 100644
--- a/docs/contributing.rst
+++ b/docs/contributing.rst
@@ -16,36 +16,42 @@ to respect these guidelines.
Pigweed contribution overview
-----------------------------
-
-#. One-time contributor setup:
-
- - Sign the `Contributor License Agreement <https://cla.developers.google.com/>`_.
- - Verify that your Git user email (git config user.email) is either Google
- Account email or an Alternate email for the Google account used to sign
- the CLA (Manage Google account → Personal Info → email)
- - Sign in to `Gerrit <https://pigweed-review.googlesource.com/>`_ to create
- an account using the same Google account you used above.
- - Obtain a login cookie from Gerrit's `new-password <https://pigweed-review.googlesource.com/new-password>`_ page
- - Install the Gerrit commit hook to automatically add a ``Change-Id: ...``
- line to your commit
- - Install the Pigweed presubmit check hook with ``pw presubmit --install``
-
-#. Ensure all files include the correct copyright and license headers
-#. Include any necessary changes to the documentation
-#. Run :ref:`module-pw_presubmit` to detect style or compilation issues before
- uploading
-#. Upload the change with ``git push origin HEAD:refs/for/main``
-#. Address any reviewer feedback by amending the commit (``git commit --amend``)
-#. Submit change to CI builders to merge. If you are not part of Pigweed's
- core team, you can ask the reviewer to add the `+2 CQ` vote, which will
- trigger a rebase and submit once the builders pass
-
.. note::
If you have any trouble with this flow, reach out in our `chat room
<https://discord.gg/M9NSeTA>`_ or on the `mailing list
<https://groups.google.com/forum/#!forum/pigweed>`_ for help.
+One-time contributor setup
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+#. Sign the
+ `Contributor License Agreement <https://cla.developers.google.com/>`_.
+#. Verify that your Git user email (git config user.email) is either Google
+ Account email or an Alternate email for the Google account used to sign
+ the CLA (Manage Google account → Personal Info → email).
+#. Obtain a login cookie from Gerrit's
+ `new-password <https://pigweed.googlesource.com/new-password>`_ page.
+#. Install the :ref:`gerrit-commit-hook` to automatically add a
+ ``Change-Id: ...`` line to your commit.
+#. Install the Pigweed presubmit check hook with ``pw presubmit --install``.
+ Remember to :ref:`activate-pigweed-environment` first!
+
+Change submission process
+^^^^^^^^^^^^^^^^^^^^^^^^^
+#. Ensure all files include the correct copyright and license headers.
+#. Include any necessary changes to the documentation.
+#. Run :ref:`module-pw_presubmit` to detect style or compilation issues before
+ uploading.
+#. Upload the change with ``git push origin HEAD:refs/for/main``.
+#. Add ``gwsq-pigweed@pigweed.google.com.iam.gserviceaccount.com`` as a
+ reviewer. This will automatically choose an appropriate person to review the
+ change.
+#. Address any reviewer feedback by amending the commit
+ (``git commit --amend``).
+#. Submit change to CI builders to merge. If you are not part of Pigweed's
+ core team, you can ask the reviewer to add the `+2 CQ` vote, which will
+ trigger a rebase and submit once the builders pass.
+
Contributor License Agreement
-----------------------------
Contributions to this project must be accompanied by a Contributor License
@@ -58,6 +64,8 @@ You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.
+.. _gerrit-commit-hook:
+
Gerrit Commit Hook
------------------
Gerrit requires all changes to have a ``Change-Id`` tag at the bottom of each
@@ -66,6 +74,9 @@ instructions below.
**Linux/macOS**
+The command below assumes that your current working directory is the root
+of your Pigweed repository.
+
.. code:: bash
$ f=`git rev-parse --git-dir`/hooks/commit-msg ; mkdir -p $(dirname $f) ; curl -Lo $f https://gerrit-review.googlesource.com/tools/hooks/commit-msg ; chmod +x $f
@@ -80,167 +91,10 @@ it to the ``.git\hooks`` directory in the Pigweed repository.
copy %HOMEPATH%\Downloads\commit-msg %HOMEPATH%\pigweed\.git\hooks\commit-msg
-Commit message
+Commit Message
--------------
-Consider the following when writing a commit message:
-
-#. **Documentation and comments are better** - Consider whether the commit
- message contents would be better expressed in the documentation or code
- comments. Docs and code comments are durable and readable later; commit
- messages are rarely read after the change lands.
-#. **Include why the change is made, not just what the change is** - It is
- important to include a "why" component in most commits. Sometimes, why is
- evident - for example, reducing memory usage, or optimizing. But it is often
- not. Err on the side of over-explaining why, not under-explaining why.
-
-Pigweed commit messages should conform to the following style:
-
-**Yes:**
-
-.. code:: none
-
- pw_some_module: Short capitalized description
-
- Details about the change here. Include a summary of the what, and a clear
- description of why the change is needed for future maintainers.
-
- Consider what parts of the commit message are better suited for
- documentation.
-
-**Yes**: Small number of modules affected; use {} syntax.
-
-.. code:: none
-
- pw_{foo, bar, baz}: Change something in a few places
-
- When changes cross a few modules, include them with the syntax shown above.
-
-
-**Yes**: targets are effectively modules, even though they're nested, so they get a
-``/`` character.
-
-.. code:: none
-
- targets/xyz123: Tweak support for XYZ's PQR
-
-**Yes**: Uses imperative style for subject and text.
-
-.. code:: none
-
- pw_something: Add foo and bar functions
-
- This commit correctly uses imperative present-tense style.
-
-**No**: Uses non-imperative style for subject and text.
-
-.. code:: none
-
- pw_something: Adds more things
-
- Use present tense imperative style for subjects and commit. The above
- subject has a plural "Adds" which is incorrect; should be "Add".
-
-**Yes**: Use bulleted lists when multiple changes are in a single CL. Prefer
-smaller CLs, but larger CLs are a practical reality.
-
-.. code:: none
-
- pw_complicated_module: Pre-work for refactor
-
- Prepare for a bigger refactor by reworking some arguments before the larger
- change. This change must land in downstream projects before the refactor to
- enable a smooth transition to the new API.
-
- - Add arguments to MyImportantClass::MyFunction
- - Update MyImportantClass to handle precondition Y
- - Add stub functions to be used during the transition
-
-**No**: Run on paragraph instead of bulleted list
-
-.. code:: none
-
- pw_foo: Many things in a giant BWOT
-
- This CL does A, B, and C. The commit message is a Big Wall Of Text (BWOT),
- which we try to discourage in Pigweed. Also changes X and Y, because Z and
- Q. Furthermore, in some cases, adds a new Foo (with Bar, because we want
- to). Also refactors qux and quz.
-
-**No**: Doesn't capitalize the subject
-
-.. code:: none
-
- pw_foo: do a thing
-
- Above subject is incorrect, since it is a sentence style subject.
-
-**Yes**: Doesn't capitalize the subject when subject's first word is a
-lowercase identifier.
-
-.. code:: none
-
- pw_foo: std::unique_lock cleanup
-
- This commit message demonstrates the subject when the subject has an
- identifier for the first word. In that case, follow the identifier casing
- instead of capitalizing.
-
- However, imperative style subjects often have the identifier elsewhere in
- the subject; for example:
-
- pw_foo: Improve use of std::unique_lock
-
-**No**: Uses a non-standard ``[]`` to indicate moduule:
-
-.. code:: none
-
- [pw_foo]: Do a thing
-
-**No**: Has a period at the end of the subject
-
-.. code:: none
-
- pw_bar: Do somehthing great.
-
-**No**: Puts extra stuff after the module which isn't a module.
-
-.. code:: none
-
- pw_bar/byte_builder: Add more stuff to builder
-
-Footer
-^^^^^^
-We support a number of `git footers`_ in the commit message, such as ``Bug:
-123`` in the message below:
-
-.. code:: none
-
- pw_something: Add foo and bar functions
-
- Bug: 123
-
-You are encouraged to use the following footers when appropriate:
-
-* ``Bug``: Associates this commit with a bug (issue in our `bug tracker`_). The
- bug will be automatically updated when the change is submitted. When a change
- is relevant to more than one bug, include multiple ``Bug`` lines, like so:
-
- .. code:: none
-
- pw_something: Add foo and bar functions
-
- Bug: 123
- Bug: 456
-
-* ``Fixed``: Like ``Bug``, but automatically closes the bug when submitted.
-
-In addition, we support all of the `Chromium CQ footers`_, but those are
-relatively rarely useful.
-
-.. _bug tracker: https://bugs.chromium.org/p/pigweed/issues/list
-.. _Chromium CQ footers: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/infra/cq.md#options
-.. _git footers: https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/git-footers.html
-
+See the :ref:`commit message section of the style guide<commit-style>` for how
+commit messages should look.
Documentation
-------------
@@ -266,10 +120,38 @@ development workflow <http://ceres-solver.org/contributing.html>`_. Consult the
<https://gerrit-documentation.storage.googleapis.com/Documentation/2.12.3/intro-user.html>`_
for more information on using Gerrit.
+You may add the special address
+``gwsq-pigweed@pigweed.google.com.iam.gserviceaccount.com`` as a reviewer to
+have Gerrit automatically choose an appropriate person to review your change.
+
In the future we may support GitHub pull requests, but until that time we will
close GitHub pull requests and ask that the changes be uploaded to Gerrit
instead.
+Instructions for reviewers
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+#. Get the `Gerrit Monitor extension
+ <https://chrome.google.com/webstore/detail/gerrit-monitor/leakcdjcdifiihdgalplgkghidmfafoh?hl=en>`_.
+#. When added to the attention set for a change, respond within 1 business day:
+
+ * Review the change if possible, OR
+ * If you will not be able to review the change within 1 business day (e.g.
+ due to handling P0s), comment on the change stating so, and reassign to
+ another reviewer if possible.
+ * The response time expectation only applies to Googlers working full-time
+ on Pigweed.
+#. Remove yourself from the `attention set
+ <https://gerrit-review.googlesource.com/Documentation/user-attention-set.html>`_
+ for changes where another person (author or reviewer) must take action
+ before you can continue to review. You are encouraged, but not required, to
+ leave a comment when doing so, especially for changes by external
+ contributors who may not be familiar with our process.
+
+SLO
+^^^
+90% of changes on which a Googler working on Pigweed full-time is added to the
+attention set as a reviewer get triaged within 1 business day.
+
Community Guidelines
--------------------
This project follows `Google's Open Source Community Guidelines
@@ -410,6 +292,44 @@ track, e.g.
When tracking an upstream branch, ``pw presubmit`` will only run checks on the
modified files, rather than the entire repository.
+Presubmit flags
+^^^^^^^^^^^^^^^
+``pw presubmit`` can accept a number of flags
+
+``-b commit, --base commit``
+ Git revision against which to diff for changed files. Default is the tracking
+ branch of the current branch. Set commit to "HEAD" to check files added or
+ modified but not yet commited. Cannot be used with --full.
+
+``--full``
+ Run presubmit on all files, not just changed files. Cannot be used with
+ --base.
+
+``-e regular_expression, --exclude regular_expression``
+ Exclude paths matching any of these regular expressions, which are interpreted
+ relative to each Git repository's root.
+
+``-k, --keep-going``
+ Continue instead of aborting when errors occur.
+
+``--output-directory OUTPUT_DIRECTORY``
+ Output directory (default: <repo root>/out/presubmit)
+
+``--package-root PACKAGE_ROOT``
+ Package root directory (default: <output directory>/packages)
+
+``--clear, --clean``
+ Delete the presubmit output directory and exit.
+
+``-p, --program PROGRAM``
+ Which presubmit program to run
+
+``--step STEP``
+ Provide explicit steps instead of running a predefined program.
+
+``--install``
+ Install the presubmit as a Git pre-push hook and exit.
+
.. _Sphinx: https://www.sphinx-doc.org/
.. inclusive-language: disable
diff --git a/docs/embedded_cpp_guide.rst b/docs/embedded_cpp_guide.rst
index 4417d065d..d5925b044 100644
--- a/docs/embedded_cpp_guide.rst
+++ b/docs/embedded_cpp_guide.rst
@@ -173,3 +173,18 @@ must be silenced. This is done in one of the following ways:
In C, silencing warnings on unused functions may require compiler-specific
attributes (``__attribute__((unused))``). Avoid this by removing the
functions or compiling with C++ and using ``[[maybe_unused]]``.
+
+Dealing with ``nodiscard`` return values
+----------------------------------------
+There are rare circumstances where a ``nodiscard`` return value from a function
+call needs to be discarded. For ``pw::Status`` value ``.IgnoreError()`` can be
+appended to the the function call. For other instances, ``std::ignore`` can be
+used.
+
+.. code-block:: cpp
+
+ // <tuple> defines std::ignore.
+ #include <tuple>
+
+ DoThingWithStatus().IgnoreError();
+ std::ignore = DoThingWithReturnValue(); \ No newline at end of file
diff --git a/docs/faq.rst b/docs/faq.rst
index 7a6f248e1..ab73f90ed 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -51,8 +51,8 @@ the Pigweed integrated environment and build, or just use individual modules?
A la carte: Individual modules only
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-This is best option if you have an existing project, with pre-existing build in
-place.
+This is the best option if you have an existing project, with pre-existing build
+in place.
To use the libraries, submodule or copy the relevant Pigweed modules into your
project, and use them like any other C++ library. You can reference the
@@ -90,6 +90,16 @@ The summary is:
the mailing list. We know this part of Pigweed is incomplete and will help
those who are interested in giving Pigweed a try.
+Why doesn't Pigweed allow shell scripting?
+------------------------------------------
+Pigweed supports multiple platforms. The native shells on these differ and
+additionally "compatible" shells often have sububle differences in behavior.
+Pigweed uses Python instead shell wherever practical and changes to Pigweed that
+include shell scripting will likely be rejected. Users of Pigweed may use shell
+scripts in their own code and we have included support for
+`Shellcheck <https://www.shellcheck.net/>`_ during presubmit checks that is
+automatically enabled if ``shellcheck`` found in the path.
+
What development hosts are supported?
-------------------------------------
We support the following platforms:
@@ -144,7 +154,12 @@ to provide equivalent binaries, which is some effort.
Host platforms that we are likely to support in the future
..........................................................
-- **Mac on ARM (M1)** - This is currently supported through Rosetta.
+- **Mac on ARM (M1)** - This is currently experimentally supported through
+ Rosetta, and this support is enabled by default. To explicitly choose to use
+ or not use Rosetta set add ``"rosetta": "force"`` to your environment setup
+ config file. Other possible values are ``"never"`` and ``"allow"``. For now,
+ ``"allow"`` means ``"force"`` but at some point in the future it will change
+ to ``"never"``.
- **Linux on ARM** - At time of writing (mid 2020), we do not support ARM-based
host platforms. However, we would like to support this eventually.
- **Windows on WSL2 x86-64** - There are some minor issues preventing WSL2 on
diff --git a/docs/getting_started.rst b/docs/getting_started.rst
index dc9af5567..54d503637 100644
--- a/docs/getting_started.rst
+++ b/docs/getting_started.rst
@@ -104,6 +104,15 @@ Most Linux installations should work out of box, and not require any manual
installation of prerequisites beyond basics like ``git`` and
``build-essential`` (or the equivalent for your distro).
+.. inclusive-language: disable
+
+To flash devices using OpenOCD, you may need to extend your system udev rules
+at ``/etc/udev/rules.d/``. The OpenOCD repository has a good
+`example udev rules file <https://github.com/openocd-org/openocd/blob/master/contrib/60-openocd.rules>`_
+that includes many popular hardware debuggers.
+
+.. inclusive-language: enable
+
**macOS**
To start using Pigweed on MacOS, you'll need to install XCode. Download it
@@ -127,7 +136,7 @@ following commands to ensure Python knows how to use OpenSSL.
To flash firmware to a STM32 Discovery development board (and run ``pw test``)
from macOS, you will need to install OpenOCD. Install
-[Homebrew](https://brew.sh), then install OpenOCD with `brew install openocd`.
+`Homebrew <https://brew.sh>`_, then install OpenOCD with ``brew install openocd``.
**Windows**
@@ -176,8 +185,10 @@ Below is a real-time demo with roughly what you should expect to see as output:
Congratulations, you are now set up to start using Pigweed!
-Pigweed Environment
-===================
+.. _activate-pigweed-environment:
+
+Activate your Pigweed Environment
+=================================
After going through the initial setup process, your current terminal will be in
the Pigweed development environment that provides all the tools you should need
to develop on Pigweed. If you leave that session, you can activate the
@@ -254,8 +265,8 @@ the host automatically build and run the unit tests. Unit tests err on the side
of being quiet in the success case, and only output test results when there's a
failure.
-To see the a test failure, modify ``pw_status/status_test.cc`` to fail by
-changing one of the strings in the "KnownString" test.
+To see a test failure, modify ``pw_status/status_test.cc`` to fail by changing
+one of the strings in the "KnownString" test.
.. image:: images/pw_watch_test_demo.gif
:width: 800
@@ -270,7 +281,7 @@ Try running the ``pw_status`` test manually:
.. code:: bash
- $ ./out/host_{clang,gcc}_debug/obj/pw_status/test/status_test
+ $ ./out/pw_strict_host_{clang,gcc}_debug/obj/pw_status/test/status_test
Depending on your host OS, the compiler will default to either ``clang`` or
``gcc``.
@@ -376,6 +387,35 @@ You can explicitly build just the documentation with the command below.
This concludes the introduction to developing for upstream Pigweed.
+Building Tests Individually
+===========================
+Sometimes it's faster to incrementally build a single test target rather than
+waiting for the whole world to build and all tests to run. GN has a built-in
+tool, ``gn outputs``, that will translate a GN build step into a Ninja build
+step. In order to build and run the right test, it's important to explicitly
+specify which target to build the test under (e.g. host, SM32F529I-DISC1).
+This can be done by appending the GN path to the target toolchain in parenthesis
+after the desired GN build step label as seen in the example below.
+
+.. code:: none
+
+ $ gn outputs out "//pw_status:status_test.run(//targets/host/pigweed_internal:pw_strict_host_clang_debug)"
+ pw_strict_host_clang_debug/obj/pw_status/status_test.run.pw_pystamp
+
+ $ ninja -C out pw_strict_host_clang_debug/obj/pw_status/status_test.run.pw_pystamp
+ ninja: Entering directory `out'
+ [4/4] ACTION //pw_status:status_test.run(//targets/host/pigweed_internal:pw_strict_host_clang_debug)
+
+The ``.run`` following the test target name is a sub-target created as part of
+the ``pw_test`` GN template. If you remove ``.run``, the test will build but
+not attempt to run.
+
+In macOS and Linux, ``xargs`` can be used to turn this into a single command:
+
+.. code:: bash
+
+ $ gn outputs out "//pw_status:status_test.run(//targets/host/pigweed_internal:pw_strict_host_clang_debug)" | xargs ninja -C out
+
Next steps
==========
diff --git a/docs/images/pw_watch_test_demo2.gif b/docs/images/pw_watch_test_demo2.gif
new file mode 100644
index 000000000..e9e9c65bc
--- /dev/null
+++ b/docs/images/pw_watch_test_demo2.gif
Binary files differ
diff --git a/docs/index.rst b/docs/index.rst
index 0b5d9015d..98e7805e8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,6 +1,8 @@
.. _docs-root:
.. highlight:: sh
+.. TODO(b/256680603) Remove query string from issue tracker link.
+
.. toctree::
:maxdepth: 1
:hidden:
@@ -8,25 +10,26 @@
Home <self>
docs/getting_started
docs/concepts/index
+ module_guides
docs/release_notes/index
- Source Code <https://cs.opensource.google/pigweed/pigweed>
- Code Reviews <https://pigweed-review.googlesource.com>
Mailing List <https://groups.google.com/forum/#!forum/pigweed>
Chat Room <https://discord.gg/M9NSeTA>
- Issue Tracker <https://bugs.pigweed.dev/>
+ docs/os_abstraction_layers
+ docs/size_optimizations
+ FAQ <docs/faq>
+ third_party_support
+ Source Code <https://cs.pigweed.dev/pigweed>
+ Code Reviews <https://pigweed-review.googlesource.com>
+ Issue Tracker <https://issues.pigweed.dev/issues?q=status:open>
docs/contributing
docs/code_of_conduct
docs/embedded_cpp_guide
Style Guide <docs/style_guide>
Automated Analysis <automated_analysis>
- docs/os_abstraction_layers
targets
Build System <build_system>
- docs/size_optimizations
- FAQ <docs/faq>
+ SEEDs <seed/0000-index>
docs/module_structure
- module_guides
- third_party_support
=======
Pigweed
@@ -39,15 +42,19 @@ STM32L452 or the Nordic nRF52832.
.. attention::
- Pigweed is in **early access**; though many modules are shipping in
- production already. If you're interested in using Pigweed, please reach out
- in our `chat room <https://discord.gg/M9NSeTA>`_ or on the `mailing list
- <https://groups.google.com/forum/#!forum/pigweed>`_.
+ Pigweed is in **early access**; though many modules are shipping in
+ production already. If you're interested in using Pigweed, please reach out
+ in our `chat room <https://discord.gg/M9NSeTA>`_ or on the `mailing list
+ <https://groups.google.com/forum/#!forum/pigweed>`_.
Getting Started
---------------
-If you'd like to get set up with Pigweed, please visit the
-:ref:`docs-getting-started` guide.
+Check out `Pigweed Sample Project <https://pigweed.googlesource.com/pigweed/sample_project/+/main/README.md>`_
+to see how to use Pigweed as a library in your broader project.
+
+Visit the :ref:`docs-getting-started` guide to learn how to bootstrap and
+activate a Pigweed environment, build Pigweed for a specific host or device,
+run tests, and more.
What does Pigweed offer?
------------------------
@@ -121,8 +128,8 @@ workspace, and makes no changes to your system. This tooling is designed to be
reused by any project.
.. image:: docs/images/pw_env_setup_demo.gif
- :width: 800
- :alt: pw environment setup demo
+ :width: 800
+ :alt: pw environment setup demo
``pw_unit_test`` - Embedded testing for MCUs
--------------------------------------------
@@ -131,7 +138,7 @@ compatible with `Google Test <https://github.com/google/googletest>`_. Unlike
Google Test, :ref:`module-pw_unit_test` is built on top of embedded friendly
primitives; for example, it does not use dynamic memory allocation.
Additionally, it is easy to port to new target platforms by implementing the
-`test event handler interface <https://cs.opensource.google/pigweed/pigweed/+/main:pw_unit_test/public/pw_unit_test/event_handler.h>`_.
+`test event handler interface <https://cs.pigweed.dev/pigweed/+/main:pw_unit_test/public/pw_unit_test/event_handler.h>`_.
Like other modules in Pigweed, ``pw_unit_test`` is designed for use in
established codebases with their own build system, without the rest of Pigweed
@@ -141,33 +148,33 @@ developing code on your desktop (with tests), then running the same tests
on-device.
.. image:: docs/images/pw_status_test.png
- :width: 800
- :alt: pw_status test run natively on host
+ :width: 800
+ :alt: pw_status test run natively on host
And more!
---------
Here is a selection of interesting modules:
- - :ref:`module-pw_cpu_exception_cortex_m` - Robust low level hardware fault
- handler for ARM Cortex-M; the handler even has unit tests written in
- assembly to verify nested-hardware-fault handling!
+- :ref:`module-pw_cpu_exception_cortex_m` - Robust low level hardware fault
+ handler for ARM Cortex-M; the handler even has unit tests written in assembly
+ to verify nested-hardware-fault handling!
- - :ref:`module-pw_polyfill` - Similar to JavaScript “polyfill” libraries, this
- module provides selected C++17 standard library components that are
- compatible with C++14.
+- :ref:`module-pw_polyfill` - Similar to JavaScript “polyfill” libraries, this
+ module provides selected C++17 standard library components that are compatible
+ with C++14.
- - :ref:`module-pw_tokenizer` - Replace string literals from log statements
- with 32-bit tokens, to reduce flash use, reduce logging bandwidth, and save
- formatting cycles from log statements at runtime.
+- :ref:`module-pw_tokenizer` - Replace string literals from log statements with
+ 32-bit tokens, to reduce flash use, reduce logging bandwidth, and save
+ formatting cycles from log statements at runtime.
- - :ref:`module-pw_kvs` - A key-value-store implementation for flash-backed
- persistent storage with integrated wear levelling. This is a lightweight
- alternative to a file system for embedded devices.
+- :ref:`module-pw_kvs` - A key-value-store implementation for flash-backed
+ persistent storage with integrated wear levelling. This is a lightweight
+ alternative to a file system for embedded devices.
- - :ref:`module-pw_protobuf` - An early preview of our wire-format-oriented
- protocol buffer implementation. This protobuf compiler makes a different set
- of implementation tradeoffs than the most popular protocol buffer library in
- this space, nanopb.
+- :ref:`module-pw_protobuf` - An early preview of our wire-format-oriented
+ protocol buffer implementation. This protobuf compiler makes a different set
+ of implementation tradeoffs than the most popular protocol buffer library in
+ this space, nanopb.
See the :ref:`docs-module-guides` for the complete list of modules and their
documentation.
diff --git a/docs/module_structure.rst b/docs/module_structure.rst
index f97654f9e..03b6fa924 100644
--- a/docs/module_structure.rst
+++ b/docs/module_structure.rst
@@ -103,9 +103,8 @@ C++ module structure
C++ public headers
~~~~~~~~~~~~~~~~~~
-Located ``{pw_module_dir}/public/<module>``. These are headers that must be
-exposed due to C++ limitations (i.e. are included from the public interface,
-but are not intended for public use).
+Located ``{pw_module_dir}/public/<module>``. These headers are the public
+interface for the module.
**Public headers** should take the form:
@@ -166,9 +165,9 @@ For example, the ``pw_unit_test`` module provides a header override for
public_overrides/gtest/gtest.h
public/pw_unit_test
- public/pw_unit_test/framework.h
public/pw_unit_test/simple_printing_event_handler.h
public/pw_unit_test/event_handler.h
+ public/pw_unit_test/internal/framework.h
Note that the overrides are in a separate directory ``public_overrides``.
@@ -387,6 +386,28 @@ The GN build system provides the
:ref:`pw_facade template<module-pw_build-facade>` as a convenient way to declare
facades.
+Multiple Facades
+~~~~~~~~~~~~~~~~
+A module may contain multiple facades. Each facade's public override headers
+must be contained in separate folders in the backend implementation, so that
+it's possible to use multiple backends for a module.
+
+.. code-block::
+
+ # pw_foo contains 2 facades, foo and bar
+ pw_foo/...
+ # Public headers
+ # public/pw_foo/foo.h #includes pw_foo_backend/foo.h
+ # public/pw_foo/bar.h #includes pw_foo_backend/bar.h
+ public/pw_foo/foo.h
+ public/pw_foo/bar.h
+
+ pw_foo_backend/...
+
+ # Public override headers for facade1 and facade2 go in separate folders
+ foo_public_overrides/pw_foo_backend/foo.h
+ bar_public_overrides/pw_foo_backend/bar.h
+
Documentation
-------------
Documentation should go in the root module folder, typically in the
@@ -442,6 +463,8 @@ To create a new Pigweed module, follow the below steps.
- Declare tests in ``pw_test_group("tests")``
- Declare docs in ``pw_docs_group("docs")``
+ Both ``tests`` and ``docs`` are required, even if the module is empty!
+
6. Add Bazel build support in ``{pw_module_dir}/BUILD.bazel``
7. Add CMake build support in ``{pw_module_dir}/CMakeLists.txt``
@@ -463,6 +486,6 @@ To create a new Pigweed module, follow the below steps.
11. Run :ref:`module-pw_module-module-check`
- - ``$ pw module-check {pw_module_dir}``
+ - ``$ pw module check {pw_module_dir}``
12. Contribute your module to upstream Pigweed (optional but encouraged!)
diff --git a/docs/os_abstraction_layers.rst b/docs/os_abstraction_layers.rst
index 94969d596..d2be7df76 100644
--- a/docs/os_abstraction_layers.rst
+++ b/docs/os_abstraction_layers.rst
@@ -253,10 +253,10 @@ portability efficiency tradeoff does not have to be made up front.
Thread Notification
-------------------
Pigweed intends to provide the ``pw::sync::ThreadNotification`` and
-``pw::sync::TimedThreadNotification`` facades which permit a singler consumer to
+``pw::sync::TimedThreadNotification`` facades which permit a single consumer to
block until an event occurs. This should be backed by the most efficient native
primitive for a target, regardless of whether that is a semaphore, event flag
-group, condition variable, or direct task notification with a critical section
+group, condition variable, direct task notification with a critical section, or
something else.
Counting Semaphore
@@ -379,7 +379,7 @@ Execution Contexts
------------------
Code runs in *execution contexts*. Common examples of execution contexts on
microcontrollers are **thread context** and **interrupt context**, though there
-are others. Since OS abstactions deal with concurrency, it's important to
+are others. Since OS abstractions deal with concurrency, it's important to
understand what API primitives are safe to call in what contexts. Since the
number of execution contexts is too large for Pigweed to cover exhaustively,
Pigweed has the following classes of APIs:
@@ -413,13 +413,13 @@ Pigweed has a single API which validates the context requirements through
a few reasons:
#. **Too many contexts** - Since there are contexts beyond just thread,
- interrupt, and NMI, having context-specefic APIs would be a hard to
+ interrupt, and NMI, having context-specific APIs would be a hard to
maintain. The proliferation of postfixed APIs (``...FromISR``,
``...FromNMI``, ``...FromThreadCriticalSection``, and so on) would also be
confusing for users.
-#. **Must verify context anyway** - Backends are requried to enforce context
- requirements with ``DHCECK`` or related calls, so we chose a simple API
+#. **Must verify context anyway** - Backends are required to enforce context
+ requirements with ``DCHECK`` or related calls, so we chose a simple API
which happens to match both the C++'s STL and Google's Abseil.
#. **Multi-context code** - Code running in multiple contexts would need to be
diff --git a/docs/python_build.rst b/docs/python_build.rst
index d0326133f..8c8848ae5 100644
--- a/docs/python_build.rst
+++ b/docs/python_build.rst
@@ -1,10 +1,19 @@
.. _docs-python-build:
-======================
-Pigweed's Python build
-======================
+=========================
+Pigweed's GN Python Build
+=========================
+
+.. seealso::
+
+ - :bdg-ref-primary-line:`module-pw_build-python` for detailed template usage.
+ - :bdg-ref-primary-line:`module-pw_build` for other GN templates available
+ within Pigweed.
+ - :bdg-ref-primary-line:`docs-build-system` for a high level guide and
+ background information on Pigweed's build system as a whole.
+
Pigweed uses a custom GN-based build system to manage its Python code. The
-Pigweed Python build supports packaging, installation, and distribution of
+Pigweed Python build supports packaging, installation and distribution of
interdependent local Python packages. It also provides for fast, incremental
static analysis and test running suitable for live use during development (e.g.
with :ref:`module-pw_watch`) or in continuous integration.
@@ -15,168 +24,348 @@ setup uses GN to set up the initial Python environment, regardless of the final
build system. As needed, non-GN projects can declare just their Python packages
in GN.
-Background
-==========
-Developing software involves much more than writing source code. Software needs
-to be compiled, executed, tested, analyzed, packaged, and deployed. As projects
-grow beyond a few files, these tasks become impractical to manage manually.
-Build systems automate these auxiliary tasks of software development, making it
-possible to build larger, more complex systems quickly and robustly.
+How it Works
+============
+In addition to compiler commands a Pigweed GN build will execute Python scripts
+for various reasons including running tests, linting code, generating protos and
+more. All these scripts are run as part of a
+:ref:`module-pw_build-pw_python_action` GN template which will ultimately run
+``python``. Running Python on it's own by default will make any Python packages
+installed on the users system available for importing. This is not good and can
+lead to flaky builds when different packages are installed on each developer
+workstation. To get around this the Python community uses `virtual environments
+<https://docs.python.org/3/library/venv.html>`_ (venvs) that expose a specific
+set of Python packages separate from the host system.
+
+When a Pigweed GN build starts a single venv is created for use by all
+:ref:`pw_python_actions <module-pw_build-pw_python_action>` throughout the build
+graph. Once created, all required third-party Python packages needed for the
+project are installed. At that point no further modifications are made to
+the venv. Of course if a new third-party package dependency is added it will be
+installed too. Beyond that all venvs remain static. More venvs can be created
+with the :ref:`module-pw_build-pw_python_venv` template if desired, but only one
+is used by default.
+
+.. card::
+
+ **Every pw_python_action is run inside a venv**
+ ^^^
+ .. mermaid::
+ :caption:
+
+ flowchart LR
+ out[GN Build Dir<br/>fa:fa-folder out]
+
+ out -->|ninja -C out| createvenvs
+
+ createvenvs(Create venvs)
+ createvenvs --> pyactions1
+ createvenvs --> pyactions2
+
+ subgraph pyactions1[Python venv 1]
+ direction TB
+ venv1(fa:fa-folder out/python-venv &nbsp)
+ a1["pw_python_action('one')"]
+ a2["pw_python_action('two')"]
+ venv1 --> a1
+ venv1 --> a2
+ end
+
+ subgraph pyactions2[Python venv 2]
+ direction TB
+ venv2(fa:fa-folder out/another-venv &nbsp)
+ a3["pw_python_action('three')"]
+ a4["pw_python_action('four')"]
+ venv2 --> a3
+ venv2 --> a4
+ end
+
+.. note::
+
+ Pigweed uses `this venv target
+ <https://cs.opensource.google/pigweed/pigweed/+/main:pw_env_setup/BUILD.gn?q=pigweed_build_venv>`_
+ if a project does not specify it's own build venv. See
+ :bdg-ref-primary-line:`docs-python-build-python-gn-venv` on how to define
+ your own default venv.
+
+Having a static venv containing only third-party dependencies opens the flood
+gates for python scripts to run. If the venv only contains third-party
+dependencies you may be wondering how you can import your own in-tree Python
+packages. Python code run in the build may still import any in-tree Python
+packages created with :ref:`module-pw_build-pw_python_package`
+templates. However this only works if a correct ``python_deps`` arg is
+provided. Having that Python dependency defined in GN allows the
+:ref:`module-pw_build-pw_python_action`
+to set `PYTHONPATH
+<https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH>`_ so that given
+package can be imported. This has the benefit of the build failing if a
+dependency for any Python action or package is missing.
+
+.. admonition:: Benefits of Python ``venvs`` in GN
+ :class: important
+
+ - Using venvs to execute Python in GN provides reproducible builds with fixed
+ third-party dependencies.
+ - Using ``PYTHONPATH`` coupled with ``python_deps`` to import in-tree Python
+ packages enforces dependency correctness.
+
+
+Managing Python Requirements
+============================
+
+.. _docs-python-build-python-gn-venv:
+
+Build Time Python Virtualenv
+----------------------------
+Pigweed's GN Python build infrastructure relies on a single build-only venv for
+executing Python code. This provides an isolated environment with a reproducible
+set of third party Python constraints where all Python tests and linting can
+run. All :ref:`module-pw_build-pw_python_action` targets are executed within
+this build venv.
+
+The default build venv is specified via a GN arg and is best set in the root
+``.gn`` or ``BUILD.gn`` file. For example:
-Python is an interpreted language, but it shares most build automation concerns
-with other languages. Pigweed uses Python extensively and must to address these
-needs for itself and its users.
+.. code-block::
-Existing solutions
-==================
-The Python programming langauge does not have an official build automation
-system. However, there are numerous Python-focused build automation tools with
-varying degrees of adoption. See the `Python Wiki
-<https://wiki.python.org/moin/ConfigurationAndBuildTools>`_ for examples.
+ pw_build_PYTHON_BUILD_VENV = "//:project_build_venv"
-A few Python tools have become defacto standards, including `setuptools
-<https://pypi.org/project/setuptools/>`_, `wheel
-<https://pypi.org/project/wheel/>`_, and `pip <https://pypi.org/project/pip/>`_.
-These essential tools address key aspects of Python packaging and distribution,
-but are not intended for general build automation. Tools like `PyBuilder
-<https://pybuilder.io/>`_ and `tox <https://tox.readthedocs.io/en/latest/>`_
-provide more general build automation for Python.
+.. _docs-python-build-python-gn-requirements-files:
-The `Bazel <http://bazel.build/>`_ build system has first class support for
-Python and other languages used by Pigweed, including protocol buffers.
+Third-party Python Requirements and Constraints
+-----------------------------------------------
+Your project may have third party Python dependencies you wish to install into
+the bootstrapped environment and in the GN build venv. There are two main ways
+to add Python package dependencies:
-Challenges
-==========
-Pigweed's use of Python is different from many other projects. Pigweed is a
-multi-language, modular project. It serves both as a library or middleware and
-as a development environment.
+**Adding Requirements Files**
-This section describes Python build automation challenges encountered by
-Pigweed.
+1. Add a ``install_requires`` entry to a ``setup.cfg`` file defined in a
+ :ref:`module-pw_build-pw_python_package` template. This is the best option
+ if your in-tree Python package requires an external Python package.
-Dependencies
-------------
-Pigweed is organized into distinct modules. In Python, each module is a separate
-package, potentially with dependencies on other local or `PyPI
-<https://pypi.org/>`_ packages.
+2. Create a standard Python ``requirements.txt`` file in your project and add it
+ to the ``pw_build_PIP_REQUIREMENTS`` GN arg list.
-The basic Python packaging tools lack dependency tracking for local packages.
-For example, a package's ``setup.py`` or ``setup.cfg`` lists all of
-its dependencies, but ``pip`` is not aware of local packages until they are
-installed. Packages must be installed with their dependencies taken into
-account, in topological sorted order.
+ Requirements files support a wide range of install locations including
+ packages from pypi.org, the local file system and git repos. See `pip's
+ Requirements File documentation
+ <https://pip.pypa.io/en/stable/user_guide/#requirements-files>`_ for more
+ info.
-To work around this, one could set up a private `PyPI server
-<https://pypi.org/project/pypiserver/>`_ instance, but this is too cumbersome
-for daily development and incompatible with editable package installation.
+ The GN arg can be set in your project's root ``.gn`` or ``BUILD.gn`` file.
-Testing
--------
-Tests are crucial to having a healthy, maintainable codebase. While they take
-some initial work to write, the time investment pays for itself many times over
-by contributing to the long-term resilience of a codebase. Despite their
-benefit, developers don't always take the time to write tests. Any barriers to
-writing and running tests result in fewer tests and consequently more fragile,
-bug-prone codebases.
+ .. code-block::
-There are lots of great Python libraries for testing, such as
-`unittest <https://docs.python.org/3/library/unittest.html>`_ and
-`pytest <https://docs.pytest.org/en/stable/>`_. These tools make it easy to
-write and execute individual Python tests, but are not well suited for managing
-suites of interdependent tests in a large project. Writing a test with these
-utilities does not automatically run them or keep running them as the codebase
-changes.
+ pw_build_PIP_REQUIREMENTS = [
+ # Project specific requirements
+ "//tools/requirements.txt",
+ ]
-Static analysis
----------------
-Various static analysis tools exist for Python. Two widely used, powerful tools
-are `Pylint <https://www.pylint.org/>`_ and `Mypy <http://mypy-lang.org/>`_.
-Using these tools improves code quality, as they catch bugs, encourage good
-design practices, and enforce a consistent coding style. As with testing,
-barriers to running static analysis tools cause many developers to skip them.
-Some developers may not even be aware of these tools.
+ See the :ref:`docs-python-build-python-gn-structure` section below for a full
+ code listing.
-Deploying static analysis tools to a codebase like Pigweed has some challenges.
-Mypy and Pylint are simple to run, but they are extremely slow. Ideally, these
-tools would be run constantly during development, but only on files that change.
-These tools do not have built-in support for incremental runs or dependency
-tracking.
+**Adding Constraints Files**
-Another challenge is configuration. Mypy and Pylint support using configuration
-files to select which checks to run and how to apply them. Both tools only
-support using a single configuration file for an entire run, which poses a
-challenge to modular middleware systems where different parts of a project may
-require different configurations.
+Every project should ideally inherit Pigweed's third party Python package
+version. This is accomplished via `Python constraints files
+<https://pip.pypa.io/en/stable/user_guide/#constraints-files>`_. Constraints
+control which versions of packages get installed by ``pip`` if that package is
+installed. To inherit Pigweed's Python constraints include ``constraint.list``
+from the ``pw_env_setup`` module from in your top level ``.gn`` file. Additonal
+project specific constraints can be appended to this list.
-Protocol buffers
-----------------
-`Protocol buffers <https://developers.google.com/protocol-buffers>`_ are an
-efficient system for serializing structured data. They are widely used by Google
-and other companies.
+.. code-block::
-The protobuf compiler ``protoc`` generates Python modules from ``.proto`` files.
-``protoc`` strictly generates protobuf modules according to their directory
-structure. This works well in a monorepo, but poses a challenge to a middleware
-system like Pigweed. Generating protobufs by path also makes integrating
-protobufs with existing packages awkward.
+ pw_build_PIP_CONSTRAINTS = [
+ "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list",
+ "//tools/constraints.txt",
+ ]
-Requirements
-============
-Pigweed aims to provide high quality software components and a fast, effective,
-flexible development experience for its customers. Pigweed's high-level goals
-and the `challenges`_ described above inform these requirements for the Pigweed
-Python build.
+.. _docs-python-build-python-gn-structure:
-- Integrate seamlessly with the other Pigweed build tools.
-- Easy to use independently, even if primarily using a different build system.
-- Support standard packaging and distribution with setuptools, wheel, and pip.
-- Correctly manage interdependent local Python packages.
-- Out-of-the-box support for writing and running tests.
-- Preconfigured, trivial-to-run static analysis integration for Pylint and Mypy.
-- Fast, dependency-aware incremental rebuilds and test execution, suitable for
- use with :ref:`module-pw_watch`.
-- Seamless protocol buffer support.
+GN Structure for Python Code
+============================
+Here is a full example of what is required to build Python packages using
+Pigweed's GN build system. A brief file hierarchy is shown here with file
+content following. See also :ref:`docs-python-build-structure` below for details
+on the structure of Python packages.
+
+.. code-block::
+ :caption: :octicon:`file-directory;1em` Top level GN file hierarchy
+ :name: gn-python-file-tree
+
+ project_root/
+ ├── .gn
+ ├── BUILDCONFIG.gn
+ ├── build_overrides/
+ │ └── pigweed.gni
+ ├── BUILD.gn
+ │
+ ├── python_package1/
+ │ ├── BUILD.gn
+ │ ├── setup.cfg
+ │ ├── setup.py
+ │ ├── pyproject.toml
+ │ │
+ │ ├── package_name/
+ │ │ ├── module_a.py
+ │ │ ├── module_b.py
+ │ │ ├── py.typed
+ │ │ └── nested_package/
+ │ │ ├── py.typed
+ │ │ └── module_c.py
+ │ │
+ │ ├── module_a_test.py
+ │ └── module_c_test.py
+ │
+ ├── third_party/
+ │ └── pigweed/
+ │
+ └── ...
-Detailed design
-===============
+- :octicon:`file-directory;1em` project_root/
-Build automation tool
----------------------
-Existing Python tools may be effective for Python codebases, but their utility
-is more limited in a multi-language project like Pigweed. The cost of bringing
-up and maintaining an additional build automation system for a single language
-is high.
+ - :octicon:`file;1em` .gn
-Pigweed uses GN as its primary build system for all languages. While GN does not
-natively support Python, adding support is straightforward with GN templates.
+ .. code-block::
-GN has strong multi-toolchain and multi-language capabilities. In GN, it is
-straightforward to share targets and artifacts between different languages. For
-example, C++, Go, and Python targets can depend on the same protobuf
-declaration. When using GN for multiple languages, Ninja schedules build steps
-for all languages together, resulting in faster total build times.
+ buildconfig = "//BUILDCONFIG.gn"
+ import("//build_overrides/pigweed.gni")
-Not all Pigweed users build with GN. Of Pigweed's three supported build systems,
-GN is the fastest, lightest weight, and easiest to run. It also has simple,
-clean syntax. This makes it feasible to use GN only for Python while building
-primarily with a different system.
+ default_args = {
+ pw_build_PIP_CONSTRAINTS = [
+ # Inherit Pigweed Python constraints
+ "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list",
-Given these considerations, GN is an ideal choice for Pigweed's Python build.
+ # Project specific constraints file
+ "//tools/constraint.txt",
+ ]
+
+ pw_build_PIP_REQUIREMENTS = [
+ # Project specific requirements
+ "//tools/requirements.txt",
+ ]
+
+ # Default gn build virtualenv target.
+ pw_build_PYTHON_BUILD_VENV = "//:project_build_venv"
+ }
+
+ - :octicon:`file;1em` BUILDCONFIG.gn
+
+ .. code-block::
+
+ _pigweed_directory = {
+ import("//build_overrides/pigweed.gni")
+ }
+
+ set_default_toolchain("${_pigweed_directory.dir_pw_toolchain}/default")
+
+ - :octicon:`file-directory;1em` build_overrides / :octicon:`file;1em` pigweed.gni
+
+ .. code-block::
+
+ declare_args() {
+ # Location of the Pigweed repository.
+ dir_pigweed = "//third_party/pigweed/"
+ }
+
+ # Upstream Pigweed modules.
+ import("$dir_pigweed/modules.gni")
+
+ - :octicon:`file;1em` BUILD.gn
+
+ .. code-block::
+
+ import("//build_overrides/pigweed.gni")
+
+ import("$dir_pw_build/python.gni")
+ import("$dir_pw_build/python_dist.gni")
+ import("$dir_pw_build/python_venv.gni")
+ import("$dir_pw_unit_test/test.gni")
+
+ # Lists all the targets build by default with e.g. `ninja -C out`.
+ group("default") {
+ deps = [
+ ":python.lint",
+ ":python.tests",
+ ]
+ }
+
+ # This group is built during bootstrap to setup the interactive Python
+ # environment.
+ pw_python_group("python") {
+ python_deps = [
+ # Generate and pip install _all_python_packages
+ ":pip_install_project_tools",
+ ]
+ }
+
+ # In-tree Python packages
+ _project_python_packages = [
+ "//python_package1",
+ ]
+
+ # Pigweed Python packages to include
+ _pigweed_python_packages = [
+ "$dir_pw_env_setup:core_pigweed_python_packages",
+ "$dir_pigweed/targets/lm3s6965evb_qemu/py",
+ "$dir_pigweed/targets/stm32f429i_disc1/py",
+ ]
+
+ _all_python_packages =
+ _project_python_packages + _pigweed_python_packages
+
+ # The default venv for Python actions in GN
+ # Set this gn arg in a declare_args block in this file 'BUILD.gn' or in '.gn' to
+ # use this venv.
+ #
+ # pw_build_PYTHON_BUILD_VENV = "//:project_build_venv"
+ #
+ pw_python_venv("project_build_venv") {
+ path = "$root_build_dir/python-venv"
+ constraints = pw_build_PIP_CONSTRAINTS
+ requirements = pw_build_PIP_REQUIREMENTS
+
+ # Ensure all third party Python dependencies are installed into this venv.
+ # This works by checking the setup.cfg files for all packages listed here and
+ # installing the packages listed in the [options].install_requires field.
+ source_packages = _all_python_packages
+ }
+
+ # This template collects all python packages and their dependencies into a
+ # single super Python package for installation into the bootstrapped virtual
+ # environment.
+ pw_python_distribution("generate_project_python_distribution") {
+ packages = _all_python_packages
+ generate_setup_cfg = {
+ name = "project-tools"
+ version = "0.0.1"
+ append_date_to_version = true
+ include_default_pyproject_file = true
+ }
+ }
+
+ # Install the project-tools super Python package into the bootstrapped
+ # Python venv.
+ pw_python_pip_install("pip_install_project_tools") {
+ packages = [ ":generate_project_python_distribution" ]
+ }
.. _docs-python-build-structure:
-Module structure
-----------------
+Pigweed Module Structure for Python Code
+========================================
Pigweed Python code is structured into standard Python packages. This makes it
simple to package and distribute Pigweed Python packages with common Python
tools.
Like all Pigweed source code, Python packages are organized into Pigweed
modules. A module's Python package is nested under a ``py/`` directory (see
-:ref:`docs-module-structure`).
+:ref:`Pigweed Module Stucture <docs-module-structure>`).
.. code-block::
- :caption: Example layout of a Pigweed Python package.
+ :caption: :octicon:`file-directory;1em` Example layout of a Pigweed Python package.
:name: python-file-tree
module_name/
@@ -204,14 +393,14 @@ above file tree ``setup.py`` and ``pyproject.toml`` files are stubs with the
following content:
.. code-block::
- :caption: setup.py
+ :caption: :octicon:`file;1em` setup.py
:name: setup-py-stub
import setuptools # type: ignore
setuptools.setup() # Package definition in setup.cfg
.. code-block::
- :caption: pyproject.toml
+ :caption: :octicon:`file;1em` pyproject.toml
:name: pyproject-toml-stub
[build-system]
@@ -271,8 +460,8 @@ Testing
^^^^^^^
Tests for a Python package are listed in its ``pw_python_package`` target.
Adding a new test is simple: write the test file and list it in its accompanying
-Python package. The build will run the it when the test, the package, or one
-of its dependencies is updated.
+Python package. The build will run it when the test, the package, or one of its
+dependencies is updated.
Static analysis
^^^^^^^^^^^^^^^
@@ -285,38 +474,25 @@ tests, static analysis is only run when files or their dependencies change.
Packages may opt out of static analysis as necessary.
-Installing packages in a virtual environment
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Python packages declared in the Python build may be installed in a specified
-`virtual environment <https://docs.python.org/3/tutorial/venv.html>`_. The
-default venv to use may be specified using a GN build arg. The venv may be
-overridden for individual targets. The Python build tracks installation status
-of packages based on which venv is in use.
-
-The Python build examines the ``VIRTUAL_ENV`` environment variable to determine
-the current venv. If the selected virtual environment is active, packages are
-installed directly into it. If the venv is not active, it is activated before
-installing the packages.
+In addition to user specified ``mypy.ini`` files some arguments are always
+passed to ``mypy`` by default. They can be seen in this excerpt of
+``//pw_build/python.gni`` below:
-.. admonition:: Under construction
-
- Pigweed Python targets are not yet aware of the virtual environment.
- Currently, all actions occur in the active venv without consulting
- ``VIRTUAL_ENV``.
-
-Python packages defined entirely in tree are installed with the ``--editable``
-option. Partially or fully generated packages are installed without that option.
+.. literalinclude:: pw_build/python.gni
+ :start-after: [default-mypy-args]
+ :end-before: [default-mypy-args]
Building Python wheels
^^^^^^^^^^^^^^^^^^^^^^
-Wheels are the standard format for distributing Python packages. The Pigweed
-Python build supports creating wheels for individual packages and groups of
-packages. Building the ``.wheel`` subtarget creates a ``.whl`` file for the
-package using the PyPA's `build <https://pypa-build.readthedocs.io/en/stable/>`_
-tool.
-
-The ``.wheel`` subtarget of ``pw_python_package`` records the location of
-the generated wheel with `GN metadata
+`Wheels <https://wheel.readthedocs.io/en/stable/>`_ are the standard format for
+distributing Python packages. The Pigweed Python build supports creating wheels
+for individual packages and groups of packages. Building the ``.wheel``
+subtarget creates a ``.whl`` file for the package using the PyPA's `build
+<https://pypa-build.readthedocs.io/en/stable/>`_ tool.
+
+The ``.wheel`` subtarget of any ``pw_python_package`` or
+:ref:`module-pw_build-pw_python_distribution` records the location of the
+generated wheel with `GN metadata
<https://gn.googlesource.com/gn/+/HEAD/docs/reference.md#var_metadata>`_.
Wheels for a Python package and its transitive dependencies can be collected
from the ``pw_python_package_wheels`` key. See
@@ -332,7 +508,7 @@ source tree is incomplete; the final Python package, including protobufs, is
generated in the output directory.
Generating setup.py
--------------------
+^^^^^^^^^^^^^^^^^^^
The ``pw_python_package`` target in the ``BUILD.gn`` duplicates much of the
information in the ``setup.py`` or ``setup.cfg`` file. In many cases, it would
be possible to generate a ``setup.py`` file rather than including it in the
@@ -340,15 +516,163 @@ source tree. However, removing the ``setup.py`` would preclude using a direct,
editable installation from the source tree.
Pigweed packages containing protobufs are generated in full or in part. These
-packages may use generated setup files, since they are always be packaged or
+packages may use generated setup files, since they are always packaged or
installed from the build output directory.
-See also
-========
- - :ref:`module-pw_build-python`
- - :ref:`module-pw_build`
- - :ref:`docs-build-system`
+Rationale
+=========
+
+Background
+----------
+Developing software involves much more than writing source code. Software needs
+to be compiled, executed, tested, analyzed, packaged, and deployed. As projects
+grow beyond a few files, these tasks become impractical to manage manually.
+Build systems automate these auxiliary tasks of software development, making it
+possible to build larger, more complex systems quickly and robustly.
+
+Python is an interpreted language, but it shares most build automation concerns
+with other languages. Pigweed uses Python extensively and must address these
+needs for itself and its users.
+
+Existing solutions
+------------------
+The Python programming langauge does not have an official build automation
+system. However, there are numerous Python-focused build automation tools with
+varying degrees of adoption. See the `Python Wiki
+<https://wiki.python.org/moin/ConfigurationAndBuildTools>`_ for examples.
+
+A few Python tools have become defacto standards, including `setuptools
+<https://pypi.org/project/setuptools/>`_, `wheel
+<https://pypi.org/project/wheel/>`_, and `pip <https://pypi.org/project/pip/>`_.
+These essential tools address key aspects of Python packaging and distribution,
+but are not intended for general build automation. Tools like `PyBuilder
+<https://pybuilder.io/>`_ and `tox <https://tox.readthedocs.io/en/latest/>`_
+provide more general build automation for Python.
+
+The `Bazel <http://bazel.build/>`_ build system has first class support for
+Python and other languages used by Pigweed, including protocol buffers.
+
+Challenges
+----------
+Pigweed's use of Python is different from many other projects. Pigweed is a
+multi-language, modular project. It serves both as a library or middleware and
+as a development environment.
+
+This section describes Python build automation challenges encountered by
+Pigweed.
+
+Dependencies
+^^^^^^^^^^^^
+Pigweed is organized into distinct modules. In Python, each module is a separate
+package, potentially with dependencies on other local or `PyPI
+<https://pypi.org/>`_ packages.
+
+The basic Python packaging tools lack dependency tracking for local packages.
+For example, a package's ``setup.py`` or ``setup.cfg`` lists all of
+its dependencies, but ``pip`` is not aware of local packages until they are
+installed. Packages must be installed with their dependencies taken into
+account, in topological sorted order.
+
+To work around this, one could set up a private `PyPI server
+<https://pypi.org/project/pypiserver/>`_ instance, but this is too cumbersome
+for daily development and incompatible with editable package installation.
+
+Testing
+^^^^^^^
+Tests are crucial to having a healthy, maintainable codebase. While they take
+some initial work to write, the time investment pays for itself many times over
+by contributing to the long-term resilience of a codebase. Despite their
+benefit, developers don't always take the time to write tests. Any barriers to
+writing and running tests result in fewer tests and consequently more fragile,
+bug-prone codebases.
+
+There are lots of great Python libraries for testing, such as
+`unittest <https://docs.python.org/3/library/unittest.html>`_ and
+`pytest <https://docs.pytest.org/en/stable/>`_. These tools make it easy to
+write and execute individual Python tests, but are not well suited for managing
+suites of interdependent tests in a large project. Writing a test with these
+utilities does not automatically run them or keep running them as the codebase
+changes.
+
+Static analysis
+^^^^^^^^^^^^^^^
+
+.. seealso::
+
+ :bdg-ref-primary-line:`docs-automated-analysis` for info on other static
+ analysis tools used in Pigweed.
+
+Various static analysis tools exist for Python. Two widely used, powerful tools
+are `Pylint <https://www.pylint.org/>`_ and `Mypy <http://mypy-lang.org/>`_.
+Using these tools improves code quality, as they catch bugs, encourage good
+design practices, and enforce a consistent coding style. As with testing,
+barriers to running static analysis tools cause many developers to skip them.
+Some developers may not even be aware of these tools.
+
+Deploying static analysis tools to a codebase like Pigweed has some challenges.
+Mypy and Pylint are simple to run, but they are extremely slow. Ideally, these
+tools would be run constantly during development, but only on files that change.
+These tools do not have built-in support for incremental runs or dependency
+tracking.
+
+Another challenge is configuration. Mypy and Pylint support using configuration
+files to select which checks to run and how to apply them. Both tools only
+support using a single configuration file for an entire run, which poses a
+challenge to modular middleware systems where different parts of a project may
+require different configurations.
+
+Protocol buffers
+^^^^^^^^^^^^^^^^
+`Protocol buffers <https://developers.google.com/protocol-buffers>`_ are an
+efficient system for serializing structured data. They are widely used by Google
+and other companies.
+
+The protobuf compiler ``protoc`` generates Python modules from ``.proto`` files.
+``protoc`` strictly generates protobuf modules according to their directory
+structure. This works well in a monorepo, but poses a challenge to a middleware
+system like Pigweed. Generating protobufs by path also makes integrating
+protobufs with existing packages awkward.
+
+Requirements
+------------
+Pigweed aims to provide high quality software components and a fast, effective,
+flexible development experience for its customers. Pigweed's high-level goals
+and the `challenges`_ described above inform these requirements for the Pigweed
+Python build.
+
+- Integrate seamlessly with the other Pigweed build tools.
+- Easy to use independently, even if primarily using a different build system.
+- Support standard packaging and distribution with setuptools, wheel, and pip.
+- Correctly manage interdependent local Python packages.
+- Out-of-the-box support for writing and running tests.
+- Preconfigured, trivial-to-run static analysis integration for Pylint and Mypy.
+- Fast, dependency-aware incremental rebuilds and test execution, suitable for
+ use with :ref:`module-pw_watch`.
+- Seamless protocol buffer support.
+
+Design Decision
+---------------
+Existing Python tools may be effective for Python codebases, but their utility
+is more limited in a multi-language project like Pigweed. The cost of bringing
+up and maintaining an additional build automation system for a single language
+is high.
+
+Pigweed uses GN as its primary build system for all languages. While GN does not
+natively support Python, adding support is straightforward with GN templates.
+
+GN has strong multi-toolchain and multi-language capabilities. In GN, it is
+straightforward to share targets and artifacts between different languages. For
+example, C++, Go, and Python targets can depend on the same protobuf
+declaration. When using GN for multiple languages, Ninja schedules build steps
+for all languages together, resulting in faster total build times.
+
+Not all Pigweed users build with GN. Of Pigweed's three supported build systems,
+GN is the fastest, lightest weight, and easiest to run. It also has simple,
+clean syntax. This makes it feasible to use GN only for Python while building
+primarily with a different system.
+
+Given these considerations, GN is an ideal choice for Pigweed's Python build.
.. _Configuring setup() using setup.cfg files: https://ipython.readthedocs.io/en/stable/interactive/reference.html#embedding
.. _Build System Support - How to use it?: https://setuptools.readthedocs.io/en/latest/build_meta.html?highlight=pyproject.toml#how-to-use-it
diff --git a/docs/run_doxygen.py b/docs/run_doxygen.py
new file mode 100644
index 000000000..b0b921838
--- /dev/null
+++ b/docs/run_doxygen.py
@@ -0,0 +1,95 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Run doxygen on all Pigweed modules."""
+
+import argparse
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import sys
+from typing import List
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--gn-root',
+ type=Path,
+ required=True,
+ help='Root of the GN tree.',
+ )
+ parser.add_argument(
+ '--pigweed-modules-file',
+ type=Path,
+ required=True,
+ help='Pigweed modules list file',
+ )
+ parser.add_argument(
+ '--output-dir',
+ type=Path,
+ required=True,
+ help='Location to write output to',
+ )
+ parser.add_argument(
+ '--doxygen-config',
+ type=Path,
+ required=True,
+ help='Location to write output to',
+ )
+ parser.add_argument(
+ '--include-paths',
+ nargs='+',
+ type=Path,
+ )
+ return parser.parse_args()
+
+
+def main(
+ gn_root: Path,
+ pigweed_modules_file: Path,
+ output_dir: Path,
+ doxygen_config: Path,
+ include_paths: List[Path],
+) -> None:
+ root_build_dir = Path.cwd()
+
+ # Pigweed top level module list.
+ pw_module_list = [
+ str((gn_root / path).resolve())
+ for path in pigweed_modules_file.read_text().splitlines()
+ ]
+
+ # Use selected modules only if provided
+ if include_paths:
+ pw_module_list = [
+ str((root_build_dir / path).resolve()) for path in include_paths
+ ]
+
+ env = os.environ.copy()
+ env['PW_DOXYGEN_OUTPUT_DIRECTORY'] = str(output_dir.resolve())
+ env['PW_DOXYGEN_INPUT'] = ' '.join(pw_module_list)
+ env['PW_DOXYGEN_PROJECT_NAME'] = 'Pigweed'
+
+ # Clean out old xmls
+ shutil.rmtree(output_dir, ignore_errors=True)
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ command = ['doxygen', str(doxygen_config.resolve())]
+ process = subprocess.run(command, env=env, check=True)
+ sys.exit(process.returncode)
+
+
+if __name__ == '__main__':
+ main(**vars(_parse_args()))
diff --git a/docs/size_optimizations.rst b/docs/size_optimizations.rst
index 5c409fecc..ffe1f7412 100644
--- a/docs/size_optimizations.rst
+++ b/docs/size_optimizations.rst
@@ -44,7 +44,7 @@ Consider splitting templated interfaces into multiple layers so that more of the
implementation can be shared between different instantiations. A more advanced
form is to share common logic internally by using default sentinel template
argument value and ergo instantation such as ``pw::Vector``'s
-``size_t kMaxSize = vector_impl::kGeneric`` or ``std::span``'s
+``size_t kMaxSize = vector_impl::kGeneric`` or ``pw::span``'s
``size_t Extent = dynamic_extent``.
-----------------
@@ -88,7 +88,7 @@ all zeros for ``.bss`` section placement.
Static Destructors And Finalizers
=================================
-For many embedded projects, cleaning up after the program is not a requiement,
+For many embedded projects, cleaning up after the program is not a requirement,
meaning the exit functions including any finalizers registered through
``atexit``, ``at_quick_exit``, and static destructors can all be removed to
reduce the size.
@@ -237,18 +237,18 @@ Buffer Sizing
=============
We'd be remiss not to mention the sizing of the various buffers that may exist
in your application. You could consider watermarking them with
-:ref:`module-pw_metric`. You may also be able toadjust their servicing interval
+:ref:`module-pw_metric`. You may also be able to adjust their servicing interval
and priority, but do not forget to keep the ingress burst sizes and scheduling
jitter into account.
----------------------------
Standard C and C++ libraries
----------------------------
-Toolchains are typically distrubted with their preferred standard C library and
+Toolchains are typically distributed with their preferred standard C library and
standard C++ library of choice for the target platform.
Although you do not always have a choice in what standard C library and what
-what standard C++ library is used or even how it's compiled, we recommend always
+standard C++ library is used or even how it's compiled, we recommend always
keeping an eye out for common sources of bloat.
Assert
@@ -560,7 +560,7 @@ Compiler Optimization Options
=============================
Don't forget to configure your compiler to optimize for size if needed. With
Clang this is ``-Oz`` and with GCC this can be done via ``-Os``. The GN
-toolchains provided through :ref:`module-pw_toolchain` which are optimize for
+toolchains provided through :ref:`module-pw_toolchain` which are optimized for
size are suffixed with ``*_size_optimized``.
Garbage collect function and data sections
@@ -592,16 +592,63 @@ to recommend considering this for simple functions which are 10 lines or less.
Link Time Optimization (LTO)
============================
-Consider using LTO to further reduce the size of the binary if needed. This
-tends to be very effective at devirtualization.
-
-Unfortunately, the aggressive inlining done by LTO can make it much more
-difficult to triage bugs. In addition, it can significantly increase the total
-build time.
-
-On GCC and Clang this can be enabled by passing ``-flto`` to both the compiler
-and the linker. In addition, on GCC ``-fdevirtualize-at-ltrans`` can be
-optionally used to devirtualize more aggressively.
+**Summary: LTO can decrase your binary size, at a cost: LTO makes debugging
+harder, interacts poorly with linker scripts, and makes crash reports less
+informative. We advise only enabling LTO when absolutely necessary.**
+
+Link time optimization (LTO) moves some optimizations from the individual
+compile steps to the final link step, to enable optimizing across translation
+unit boundaries.
+
+LTO can both increase performance and reduce binary size for embedded projects.
+This appears to be a strict improvement; and one might think enabling LTO at
+all times is the best approach. However, this is not the case; in practice, LTO
+is a trade-off.
+
+**LTO benefits**
+
+* **Reduces binary size** - When compiling with size-shrinking flags like
+ ``-Oz``, some function call overhead can be eliminated, and code paths might
+ be eliminated by the optimizer after inlining. This can include critical
+ abstraction removal like devirtualization.
+* **Improves performance** - When code is inlined, the optimizer can better
+ reduce the number of instructions. When code is smaller, the instruction
+ cache has better hit ratio leading to better performance. In some cases,
+ entire function calls are eliminated.
+
+**LTO costs**
+
+* **LTO interacts poorly with linker scripts** - Production embedded projects
+ often have complicated linker scripts to control the physical layout of code
+ and data on the device. For example, you may want to put performance critical
+ audio codec functions into the fast tightly coupled (TCM) memory region.
+ However, LTO can interact with linker script requirements in strange ways,
+ like inappropriately inlining code that was manually placed into other
+ functions in the wrong region; leading to hard-to-understand bugs.
+* **Debugging LTO binaries is harder** - LTO increases the differences between
+ the machine code and the source code. This makes stepping through source code
+ in a debugger confusing, since the instruction pointer can hop around in
+ confusing ways.
+* **Crash reports for LTO binaries can be misleading** - Just as with
+ debugging, LTO'd binaries can produce confusing stacks in crash reports.
+* **LTO significantly increases build times** - The compilation model is
+ different when LTO is enabled, since individual translation unit compilations
+ (`.cc` --> `.o`) files now produce LLVM- or GCC- IR instead of native machine
+ code; machine code is only generated at the link phase. This makes the final
+ link step take significantly longer. Since any source changes will result in
+ a link step, developer velocity is reduced due to the slow compile time.
+
+How to enable LTO
+-----------------
+On GCC and Clang LTO is enabled by passing ``-flto`` to both the compiler
+and the linker. On GCC ``-fdevirtualize-at-ltrans`` enables more aggressive
+devirtualization.
+
+Our recommendation
+------------------
+* Disable LTO unless absolutely necessary; e.g. due to lack of space.
+* When enabling LTO, carefully and thoroughly test the resulting binary.
+* Check that crash reports are still useful under LTO for your product.
Disabling Scoped Static Initialization Locks
============================================
diff --git a/docs/style_guide.rst b/docs/style_guide.rst
index 0cb1aa9ac..956ac2713 100644
--- a/docs/style_guide.rst
+++ b/docs/style_guide.rst
@@ -3,11 +3,19 @@
===========
Style Guide
===========
+- :ref:`cpp-style`
+- :ref:`owners-style`
+- :ref:`python-style`
+- :ref:`documentation-style`
+- :ref:`commit-style`
+
.. tip::
- Pigweed runs ``pw format`` as part of ``pw presubmit`` to perform some code
- formatting checks. To speed up the review process, consider adding ``pw
- presubmit`` as a git push hook using the following command:
- ``pw presubmit --install``
+ Pigweed runs ``pw format`` as part of ``pw presubmit`` to perform some code
+ formatting checks. To speed up the review process, consider adding ``pw
+ presubmit`` as a git push hook using the following command:
+ ``pw presubmit --install``
+
+.. _cpp-style:
---------
C++ style
@@ -28,10 +36,7 @@ embedded development beyond just C++ style.
C++ standard
============
-Pigweed primarily uses the C++17 standard. A few modules maintain support for
-C++14, however (e.g. :ref:`module-pw_kvs` and its dependencies).
-
-All Pigweed C++ code must compile with ``-std=C++17`` in Clang and GCC. C++20
+All Pigweed C++ code must compile with ``-std=c++17`` in Clang and GCC. C++20
features may be used as long as the code still compiles unmodified with C++17.
See ``pw_polyfill/language_feature_macros.h`` for macros that provide C++20
features when supported.
@@ -113,7 +118,6 @@ Permitted Headers
* ``<optional>``
* ``<random>``
* ``<ratio>``
- * ``<span>``
* ``<string_view>``
* ``<tuple>``
* ``<type_traits>``
@@ -127,7 +131,10 @@ Permitted Headers
* ``<algorithm>`` -- be wary of potential memory allocation
* ``<atomic>`` -- not all MCUs natively support atomic operations
* ``<bitset>`` -- conversions to or from strings are disallowed
- * ``<functional>`` -- do **not** use ``std::function``
+ * ``<functional>`` -- do **not** use ``std::function``; use
+ :ref:`module-pw_function`
+ * ``<mutex>`` -- can use ``std::lock_guard``, use :ref:`module-pw_sync` for
+ mutexes
* ``<new>`` -- for placement new
* ``<numeric>`` -- be wary of code size with multiple template instantiations
@@ -135,15 +142,17 @@ Permitted Headers
:class: error
* Dynamic containers (``<list>``, ``<map>``, ``<set>``, ``<vector>``, etc.)
- * Streams (``<iostream>``, ``<ostream>``, ``<fstream>``, etc.)
- * ``<exception>``
- * ``<future>``, ``<mutex>``, ``<thread>``
- * ``<memory>``
+ * Streams (``<iostream>``, ``<ostream>``, ``<fstream>``, ``<sstream>`` etc.)
+ -- in some cases :ref:`module-pw_stream` can work instead
+ * ``<span>`` -- use :ref:`module-pw_span` instead. Downstream projects may
+ want to directly use ``std::span`` if it is available, but upstream must
+ use the ``pw::span`` version for compatability
+ * ``<string>`` -- can use :ref:`module-pw_string`
+ * ``<thread>`` -- can use :ref:`module-pw_thread`
+ * ``<future>`` -- eventually :ref:`module-pw_async` will offer this
+ * ``<exception>``, ``<stdexcept>`` -- no exceptions
+ * ``<memory>``, ``<scoped_allocator>`` -- no allocations
* ``<regex>``
- * ``<scoped_allocator>``
- * ``<sstream>``
- * ``<stdexcept>``
- * ``<string>``
* ``<valarray>``
Headers not listed here should be carefully evaluated before they are used.
@@ -208,10 +217,250 @@ comment line, even if the blank comment line is the last line in the block.
Control statements
==================
-All loops and conditional statements must use braces.
+
+Loops and conditionals
+----------------------
+All loops and conditional statements must use braces, and be on their own line.
+
+.. admonition:: **Yes**: Always use braces for line conditionals and loops:
+ :class: checkmark
+
+ .. code:: cpp
+
+ while (SomeCondition()) {
+ x += 2;
+ }
+ if (OtherCondition()) {
+ DoTheThing();
+ }
+
+
+.. admonition:: **No**: Missing braces
+ :class: error
+
+ .. code:: cpp
+
+ while (SomeCondition())
+ x += 2;
+ if (OtherCondition())
+ DoTheThing();
+
+.. admonition:: **No**: Statement on same line as condition
+ :class: error
+
+ .. code:: cpp
+
+ while (SomeCondition()) { x += 2; }
+ if (OtherCondition()) { DoTheThing(); }
+
The syntax ``while (true)`` is preferred over ``for (;;)`` for infinite loops.
+.. admonition:: **Yes**:
+ :class: checkmark
+
+ .. code:: cpp
+
+ while (true) {
+ DoSomethingForever();
+ }
+
+.. admonition:: **No**:
+ :class: error
+
+ .. code:: cpp
+
+ for (;;) {
+ DoSomethingForever();
+ }
+
+
+Prefer early exit with ``return`` and ``continue``
+--------------------------------------------------
+Prefer to exit early from functions and loops to simplify code. This is the
+same same conventions as `LLVM
+<https://llvm.org/docs/CodingStandards.html#use-early-exits-and-continue-to-simplify-code>`_.
+We find this approach is superior to the "one return per function" style for a
+multitude of reasons:
+
+* **Visually**, the code is easier to follow, and takes less horizontal screen
+ space.
+* It makes it clear what part of the code is the **"main business" versus "edge
+ case handling"**.
+* For **functions**, parameter checking is in its own section at the top of the
+ function, rather than scattered around in the fuction body.
+* For **loops**, element checking is in its own section at the top of the loop,
+ rather than scattered around in the loop body.
+* Commit **deltas are simpler to follow** in code reviews; since adding a new
+ parameter check or loop element condition doesn't cause an indentation change
+ in the rest of the function.
+
+The guidance applies in two cases:
+
+* **Function early exit** - Early exits are for function parameter checking
+ and edge case checking at the top. The main functionality follows.
+* **Loop early exit** - Early exits in loops are for skipping an iteration
+ due to some edge case with an item getting iterated over. Loops may also
+ contain function exits, which should be structured the same way (see example
+ below).
+
+.. admonition:: **Yes**: Exit early from functions; keeping the main handling
+ at the bottom and de-dentend.
+ :class: checkmark
+
+ .. code:: cpp
+
+ Status DoSomething(Parameter parameter) {
+ // Parameter validation first; detecting incoming use errors.
+ PW_CHECK_INT_EQ(parameter.property(), 3, "Programmer error: frobnitz");
+
+ // Error case: Not in correct state.
+ if (parameter.other() == MyEnum::kBrokenState) {
+ LOG_ERROR("Device in strange state: %s", parametr.state_str());
+ return Status::InvalidPrecondition();
+ }
+
+ // Error case: Not in low power mode; shouldn't do anything.
+ if (parameter.power() != MyEnum::kLowPower) {
+ LOG_ERROR("Not in low power mode");
+ return Status::InvalidPrecondition();
+ }
+
+ // Main business for the function here.
+ MainBody();
+ MoreMainBodyStuff();
+ }
+
+.. admonition:: **No**: Main body of function is buried and right creeping.
+ Even though this is shorter than the version preferred by Pigweed due to
+ factoring the return statement, the logical structure is less obvious. A
+ function in Pigweed containing **nested conditionals indicates that
+ something complicated is happening with the flow**; otherwise it would have
+ the early bail structure; so pay close attention.
+ :class: error
+
+ .. code:: cpp
+
+ Status DoSomething(Parameter parameter) {
+ // Parameter validation first; detecting incoming use errors.
+ PW_CHECK_INT_EQ(parameter.property(), 3, "Programmer error: frobnitz");
+
+ // Error case: Not in correct state.
+ if (parameter.other() != MyEnum::kBrokenState) {
+ // Error case: Not in low power mode; shouldn't do anything.
+ if (parameter.power() == MyEnum::kLowPower) {
+ // Main business for the function here.
+ MainBody();
+ MoreMainBodyStuff();
+ } else {
+ LOG_ERROR("Not in low power mode");
+ }
+ } else {
+ LOG_ERROR("Device in strange state: %s", parametr.state_str());
+ }
+ return Status::InvalidPrecondition();
+ }
+
+.. admonition:: **Yes**: Bail early from loops; keeping the main handling at
+ the bottom and de-dentend.
+ :class: checkmark
+
+ .. code:: cpp
+
+ for (int i = 0; i < LoopSize(); ++i) {
+ // Early skip of item based on edge condition.
+ if (!CommonCase()) {
+ continue;
+ }
+ // Early exit of function based on error case.
+ int my_measurement = GetSomeMeasurement();
+ if (my_measurement < 10) {
+ LOG_ERROR("Found something strange; bailing");
+ return Status::Unknown();
+ }
+
+ // Main body of the loop.
+ ProcessItem(my_items[i], my_measurement);
+ ProcessItemMore(my_items[i], my_measurement, other_details);
+ ...
+ }
+
+.. admonition:: **No**: Right-creeping code with the main body buried inside
+ some nested conditional. This makes it harder to understand what is the
+ main purpose of the loop versus what is edge case handling.
+ :class: error
+
+ .. code:: cpp
+
+ for (int i = 0; i < LoopSize(); ++i) {
+ if (CommonCase()) {
+ int my_measurement = GetSomeMeasurement();
+ if (my_measurement >= 10) {
+ // Main body of the loop.
+ ProcessItem(my_items[i], my_measurement);
+ ProcessItemMore(my_items[i], my_measurement, other_details);
+ ...
+ } else {
+ LOG_ERROR("Found something strange; bailing");
+ return Status::Unknown();
+ }
+ }
+ }
+
+There are cases where this structure doesn't work, and in those cases, it is
+fine to structure the code differently.
+
+No ``else`` after ``return`` or ``continue``
+--------------------------------------------
+Do not put unnecessary ``} else {`` blocks after blocks that terminate with a
+return, since this causes unnecessary rightward indentation creep. This
+guidance pairs with the preference for early exits to reduce code duplication
+and standardize loop/function structure.
+
+.. admonition:: **Yes**: No else after return or continue
+ :class: checkmark
+
+ .. code:: cpp
+
+ // Note lack of else block due to return.
+ if (Failure()) {
+ DoTheThing();
+ return Status::ResourceExausted();
+ }
+
+ // Note lack of else block due to continue.
+ while (MyCondition()) {
+ if (SomeEarlyBail()) {
+ continue;
+ }
+ // Main handling of item
+ ...
+ }
+
+ DoOtherThing();
+ return OkStatus();
+
+.. admonition:: **No**: Else after return needlessly creeps right
+ :class: error
+
+ .. code:: cpp
+
+ if (Failure()) {
+ DoTheThing();
+ return Status::ResourceExausted();
+ } else {
+ while (MyCondition()) {
+ if (SomeEarlyBail()) {
+ continue;
+ } else {
+ // Main handling of item
+ ...
+ }
+ }
+ DoOtherThing();
+ return OkStatus();
+ }
+
Include guards
==============
The first non-comment line of every header file must be ``#pragma once``. Do
@@ -275,6 +524,16 @@ C++ code
``foo_bar`` style naming. For consistency with other variables whose value is
always fixed for the duration of the program, the naming convention is
``kCamelCase``, and so that is the style we use in Pigweed.
+* Trivial membor accessors should be named with ``snake_case()``. The Google
+ C++ style allows either ``snake_case()`` or ``CapsCase()``, but Pigweed
+ always uses ``snake_case()``.
+* Abstract base classes should be named generically, with derived types named
+ specifically. For example, ``Stream`` is an abstract base, and
+ ``SocketStream`` and ``StdioStream`` are an implementations of that
+ interface. Any prefix or postfix indicating whether something is abstract or
+ concrete is not permitted; for example, ``IStream`` or ``SocketStreamImpl``
+ are both not permitted. These pre-/post-fixes add additional visual noise and
+ are irrelevant to consumers of these interfaces.
C code
------
@@ -297,7 +556,7 @@ length of the prefix.
* C functions (``int pw_foo_FunctionName(void);``),
* variables used by C code (``int pw_foo_variable_name;``),
- * constant variables used by C code (``int pw_foo_kConstantName;``),
+ * constant variables used by C code (``const int pw_foo_kConstantName;``),
* structs used by C code (``typedef struct {} pw_foo_StructName;``), and
* all of the above for ``extern "C"`` names in C++ code.
@@ -308,7 +567,8 @@ Preprocessor macros
* Public Pigweed macros must be prefixed with the module name (e.g.
``PW_MY_MODULE_*``).
* Private Pigweed macros must be prefixed with an underscore followed by the
- module name (e.g. ``_PW_MY_MODULE_*``).
+ module name (e.g. ``_PW_MY_MODULE_*``). (This style may change, see
+ `b/234886184 <https://issuetracker.google.com/issues/234886184>`_).
**Example**
@@ -363,6 +623,8 @@ Preprocessor macros
} // namespace nested_namespace
} // namespace pw::my_module
+See :ref:`docs-pw-style-macros` for details about macro usage.
+
Namespace scope formatting
==========================
All non-indented blocks (namespaces, ``extern "C"`` blocks, and preprocessor
@@ -407,6 +669,28 @@ them.
} // namespace
} // namespace pw::nested
+Using directives for literals
+=============================
+`Using-directives
+<https://en.cppreference.com/w/cpp/language/namespace#Using-directives>`_ (e.g.
+``using namespace ...``) are permitted in implementation files only for the
+purposes of importing literals such as ``std::chrono_literals`` or
+``pw::bytes::unit_literals``. Namespaces that contain any symbols other than
+literals are not permitted in a using-directive. This guidance also has no
+impact on `using-declarations
+<https://en.cppreference.com/w/cpp/language/namespace#Using-declarations>`_
+(e.g. ``using foo::Bar;``).
+
+Rationale: Literals improve code readability, making units clearer at the point
+of definition.
+
+.. code-block:: cpp
+
+ using namespace std::chrono; // Not allowed
+ using namespace std::literals::chrono_literals; // Allowed
+
+ constexpr std::chrono::duration delay = 250ms;
+
Pointers and references
=======================
For pointer and reference types, place the asterisk or ampersand next to the
@@ -423,6 +707,8 @@ Prefer storing references over storing pointers. Pointers are required when the
pointer can change its target or may be ``nullptr``. Otherwise, a reference or
const reference should be used.
+.. _docs-pw-style-macros:
+
Preprocessor macros
===================
Macros should only be used when they significantly improve upon the C++ code
@@ -438,8 +724,9 @@ to ensure the macros are hard to use wrong.
Stand-alone statement macros
----------------------------
Macros that are standalone statements must require the caller to terminate the
-macro invocation with a semicolon. For example, the following does *not* conform
-to Pigweed's macro style:
+macro invocation with a semicolon (see `Swalling the Semicolon
+<https://gcc.gnu.org/onlinedocs/cpp/Swallowing-the-Semicolon.html>`_). For
+example, the following does *not* conform to Pigweed's macro style:
.. code-block:: cpp
@@ -474,8 +761,7 @@ contents can be placed in a ``do { ... } while (0)`` loop.
} while (0)
Standalone macros at global scope that do not already require a semicolon can
-add a ``static_assert`` or throwaway struct declaration statement as their
-last line.
+add a ``static_assert`` declaration statement as their last line.
.. code-block:: cpp
@@ -499,7 +785,7 @@ example:
Macros in private implementation files (.cc)
--------------------------------------------
-Macros within .cc files that should only used within one file should be
+Macros within .cc files that should only be used within one file should be
undefined after their last use; for example:
.. code-block:: cpp
@@ -556,17 +842,46 @@ interacting with a POSIX API in intentionally non-portable code. Never use
POSIX functions with suitable standard or Pigweed alternatives, such as
``strnlen`` (use ``pw::string::NullTerminatedLength`` instead).
+.. _owners-style:
+
+--------------------
+Code Owners (OWNERS)
+--------------------
+If you use Gerrit for source code hosting and have the
+`code-owners <https://android-review.googlesource.com/plugins/code-owners/Documentation/backend-find-owners.html>`__
+plug-in enabled Pigweed can help you enforce consistent styling on those files
+and also detects some errors.
+
+The styling follows these rules.
+
+#. Content is grouped by type of line (Access grant, include, etc).
+#. Each grouping is sorted alphabetically.
+#. Groups are placed the following order with a blank line separating each
+ grouping.
+
+ * "set noparent" line
+ * "include" lines
+ * "file:" lines
+ * user grants (some examples: "*", "foo@example.com")
+ * "per-file:" lines
+
+This plugin will, by default, act upon any file named "OWNERS".
+
+.. _python-style:
+
------------
Python style
------------
Pigweed uses the standard Python style: PEP8, which is available on the web at
https://www.python.org/dev/peps/pep-0008/. All Pigweed Python code should pass
-``yapf`` when configured for PEP8 style.
+``pw format``, which invokes ``black`` with a couple options.
-Python 3
-========
-Pigweed uses Python 3. Some modules may offer limited support for Python 2, but
-Python 3.6 or newer is required for most Pigweed code.
+Python versions
+===============
+Pigweed officially supports :ref:`a few Python versions
+<docs-concepts-python-version>`. Upstream Pigweed code must support those Python
+versions. The only exception is :ref:`module-pw_env_setup`, which must also
+support Python 2 and 3.6.
---------------
Build files: GN
@@ -635,6 +950,8 @@ ambiguity with other build systems or tooling.
Pigweed's Bazel files follow the `Bazel style guide
<https://docs.bazel.build/versions/main/skylark/build-style.html>`_.
+.. _documentation-style:
+
-------------
Documentation
-------------
@@ -678,13 +995,13 @@ for the ReST heading syntax.
Document Title: Two Bars of Equals
==================================
Document titles use equals ("====="), above and below. Capitalize the words
- in the title, except for 'of' and 'the'.
+ in the title, except for 'a', 'of', and 'the'.
---------------------------
Major Sections Within a Doc
---------------------------
- Major sections use hypens ("----"), above and below. Capitalize the words in
- the title, except for 'of' and 'the'.
+ Major sections use hyphens ("----"), above and below. Capitalize the words in
+ the title, except for 'a', 'of', and 'the'.
Heading 1 - For Sections Within a Doc
=====================================
@@ -692,7 +1009,7 @@ for the ReST heading syntax.
Heading 2 - for subsections
---------------------------
- Subsections use hypens ("----"). In many cases, these headings may be
+ Subsections use hyphens ("----"). In many cases, these headings may be
sentence-like. In those cases, only the first letter should be capitalized.
For example, FAQ subsections would have a title with "Why does the X do the
Y?"; note the sentence capitalization (but not title capitalization).
@@ -817,10 +1134,493 @@ Consider using ``.. list-table::`` syntax, which is more maintainable and
easier to edit for complex tables (`details
<https://docutils.sourceforge.io/docs/ref/rst/directives.html#list-table>`_).
+Code Blocks
+===========
+Use code blocks from actual source code files wherever possible. This helps keep
+documentation fresh and removes duplicate code examples. There are a few ways to
+do this with Sphinx.
+
+Snippets
+--------
+The `literalinclude`_ directive creates a code blocks from source files. Entire
+files can be included or a just a subsection. The best way to do this is with
+the ``:start-after:`` and ``:end-before:`` options.
+
+Example:
+
+.. card::
+
+ Documentation Source (``.rst`` file)
+ ^^^
+
+ .. code-block:: rst
+
+ .. literalinclude:: run_doxygen.py
+ :start-after: [doxygen-environment-variables]
+ :end-before: [doxygen-environment-variables]
+
+.. card::
+
+ Source File
+ ^^^
+
+ .. code-block::
+
+ # DOCSTAG: [doxygen-environment-variables]
+ env = os.environ.copy()
+ env['PW_DOXYGEN_OUTPUT_DIRECTORY'] = str(output_dir.resolve())
+ env['PW_DOXYGEN_INPUT'] = ' '.join(pw_module_list)
+ env['PW_DOXYGEN_PROJECT_NAME'] = 'Pigweed'
+ # DOCSTAG: [doxygen-environment-variables]
+
+.. card::
+
+ Rendered Output
+ ^^^
+
+ .. code-block::
+
+ env = os.environ.copy()
+ env['PW_DOXYGEN_OUTPUT_DIRECTORY'] = str(output_dir.resolve())
+ env['PW_DOXYGEN_INPUT'] = ' '.join(pw_module_list)
+ env['PW_DOXYGEN_PROJECT_NAME'] = 'Pigweed'
+
+Python
+------
+Include Python API documentation from docstrings with `autodoc directives`_.
+Example:
+
+.. code-block:: rst
+
+ .. automodule:: pw_cli.log
+ :members:
+
+ .. automodule:: pw_console.embed
+ :members: PwConsoleEmbed
+ :undoc-members:
+ :show-inheritance:
+
+ .. autoclass:: pw_console.log_store.LogStore
+ :members: __init__
+ :undoc-members:
+ :show-inheritance:
+
+Include argparse command line help with the `argparse
+<https://sphinx-argparse.readthedocs.io/en/latest/usage.html>`_
+directive. Example:
+
+.. code-block:: rst
+
+ .. argparse::
+ :module: pw_watch.watch
+ :func: get_parser
+ :prog: pw watch
+ :nodefaultconst:
+ :nodescription:
+ :noepilog:
+
+
+Doxygen
+-------
+Doxygen comments in C, C++, and Java are surfaced in Sphinx using `Breathe
+<https://breathe.readthedocs.io/en/latest/index.html>`_.
+
+.. note::
+
+ Sources with doxygen comment blocks must be added to the
+ ``_doxygen_input_files`` list in ``//docs/BUILD.gn`` to be processed.
+
+Breathe provides various `directives
+<https://breathe.readthedocs.io/en/latest/directives.html>`_ for bringing
+Doxygen comments into Sphinx. These include the following:
+
+- `doxygenfile
+ <https://breathe.readthedocs.io/en/latest/directives.html#doxygenfile>`_ --
+ Documents everything in a source file.
+- `doxygenclass
+ <https://breathe.readthedocs.io/en/latest/directives.html#doxygenclass>`_ --
+ Documents a class and its members.
+
+ .. code-block:: rst
+
+ .. doxygenclass:: pw::sync::BinarySemaphore
+ :members:
+
+.. admonition:: See also
+
+ `Breathe directives to use in RST files <https://breathe.readthedocs.io/en/latest/directives.html>`_
+
+Example Doxygen Comment Block
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Start a Doxygen comment block using ``///`` (three forward slashes).
+
+.. code-block:: cpp
+
+ /// This is the documentation comment for the `PW_LOCK_RETURNED()` macro. It
+ /// describes how to use the macro.
+ ///
+ /// Doxygen comments can refer to other symbols using Sphinx cross
+ /// references. For example, @cpp_class{pw::InlineBasicString}, which is
+ /// shorthand for @crossref{cpp,class,pw::InlineBasicString}, links to a C++
+ /// class. @crossref{py,func,pw_tokenizer.proto.detokenize_fields} links to a
+ /// Python function.
+ ///
+ /// @param[out] dest The memory area to copy to.
+ /// @param[in] src The memory area to copy from.
+ /// @param[in] n The number of bytes to copy
+ ///
+ /// @retval OK KVS successfully initialized.
+ /// @retval DATA_LOSS KVS initialized and is usable, but contains corrupt data.
+ /// @retval UNKNOWN Unknown error. KVS is not initialized.
+ ///
+ /// @rst
+ /// The ``@rst`` and ``@endrst`` commands form a block block of
+ /// reStructuredText that is rendered in Sphinx.
+ ///
+ /// .. warning::
+ /// this is a warning admonition
+ ///
+ /// .. code-block:: cpp
+ ///
+ /// void release(ptrdiff_t update = 1);
+ /// @endrst
+ ///
+ /// Example code block using Doxygen markup below. To override the language
+ /// use `@code{.cpp}`
+ ///
+ /// @code
+ /// class Foo {
+ /// public:
+ /// Mutex* mu() PW_LOCK_RETURNED(mu) { return &mu; }
+ ///
+ /// private:
+ /// Mutex mu;
+ /// };
+ /// @endcode
+ ///
+ /// @b The first word in this sentence is bold (The).
+ ///
+ #define PW_LOCK_RETURNED(x) __attribute__((lock_returned(x)))
+
+Doxygen Syntax
+^^^^^^^^^^^^^^
+Pigweed prefers to use RST wherever possible, but there are a few Doxygen
+syntatic elements that may be needed.
+
+Common Doxygen commands for use within a comment block:
+
+- ``@rst`` To start a reStructuredText block. This is a custom alias for
+ ``\verbatim embed:rst:leading-asterisk``.
+- `@code <https://www.doxygen.nl/manual/commands.html#cmdcode>`_ Start a code
+ block.
+- `@param <https://www.doxygen.nl/manual/commands.html#cmdparam>`_ Document
+ parameters, this may be repeated.
+- `@pre <https://www.doxygen.nl/manual/commands.html#cmdpre>`_ Starts a
+ paragraph where the precondition of an entity can be described.
+- `@post <https://www.doxygen.nl/manual/commands.html#cmdpost>`_ Starts a
+ paragraph where the postcondition of an entity can be described.
+- `@return <https://www.doxygen.nl/manual/commands.html#cmdreturn>`_ Single
+ paragraph to describe return value(s).
+- `@retval <https://www.doxygen.nl/manual/commands.html#cmdretval>`_ Document
+ return values by name. This is rendered as a definition list.
+- `@note <https://www.doxygen.nl/manual/commands.html#cmdnote>`_ Add a note
+ admonition to the end of documentation.
+- `@b <https://www.doxygen.nl/manual/commands.html#cmdb>`_ To bold one word.
+
+Doxygen provides `structural commands
+<https://doxygen.nl/manual/docblocks.html#structuralcommands>`_ that associate a
+comment block with a particular symbol. Example of these include ``@class``,
+``@struct``, ``@def``, ``@fn``, and ``@file``. Do not use these unless
+necessary, since they are redundant with the declarations themselves.
+
+One case where structural commands are necessary is when a single comment block
+describes multiple symbols. To group multiple symbols into a single comment
+block, include a structural commands for each symbol on its own line. For
+example, the following comment documents two macros:
+
+.. code-block:: cpp
+
+ /// @def PW_ASSERT_EXCLUSIVE_LOCK
+ /// @def PW_ASSERT_SHARED_LOCK
+ ///
+ /// Documents functions that dynamically check to see if a lock is held, and
+ /// fail if it is not held.
+
+.. seealso:: `All Doxygen commands <https://www.doxygen.nl/manual/commands.html>`_
+
+Cross-references
+^^^^^^^^^^^^^^^^
+Pigweed provides Doxygen aliases for creating Sphinx cross references within
+Doxygen comments. These should be used when referring to other symbols, such as
+functions, classes, or macros.
+
+.. inclusive-language: disable
+
+The basic alias is ``@crossref``, which supports any `Sphinx domain
+<https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html>`_.
+For readability, aliases for commonnly used types are provided.
+
+.. inclusive-language: enable
+
+- ``@crossref{domain,type,identifier}`` Inserts a cross reference of any type in
+ any Sphinx domain. For example, ``@crossref{c,func,foo}`` is equivalent to
+ ``:c:func:`foo``` and links to the documentation for the C function ``foo``,
+ if it exists.
+- ``@c_macro{identifier}`` Equivalent to ``:c:macro:`identifier```.
+- ``@cpp_func{identifier}`` Equivalent to ``:cpp:func:`identifier```.
+- ``@cpp_class{identifier}`` Equivalent to ``:cpp:class:`identifier```.
+- ``@cpp_type{identifier}`` Equivalent to ``:cpp:type:`identifier```.
+
+.. note::
+
+ Use the `@` aliases described above for all cross references. Doxygen
+ provides other methods for cross references, but Sphinx cross references
+ offer several advantages:
+
+ - Sphinx cross references work for all identifiers known to Sphinx, including
+ those documented with e.g. ``.. cpp:class::`` or extracted from Python.
+ Doxygen references can only refer to identifiers known to Doxygen.
+ - Sphinx cross references always use consistent formatting. Doxygen cross
+ references sometimes render as plain text instead of code-style monospace,
+ or include ``()`` in macros that shouldn't have them.
+ - Sphinx cross references can refer to symbols that have not yet been
+ documented. They will be formatted correctly and become links once the
+ symbols are documented.
+ - Using Sphinx cross references in Doxygen comments makes cross reference
+ syntax more consistent within Doxygen comments and between RST and
+ Doxygen.
+
+Create cross reference links elsewhere in the document to symbols documented
+with Doxygen using standard Sphinx cross references, such as ``:cpp:class:`` and
+``:cpp:func:``.
+
+.. code-block:: rst
+
+ - :cpp:class:`pw::sync::BinarySemaphore::BinarySemaphore`
+ - :cpp:func:`pw::sync::BinarySemaphore::try_acquire`
+
+.. seealso::
+ - `C++ cross reference link syntax`_
+ - `C cross reference link syntax`_
+ - `Python cross reference link syntax`_
+
.. _Sphinx: https://www.sphinx-doc.org/
.. inclusive-language: disable
.. _reStructuredText Primer: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html
+.. _literalinclude: https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-literalinclude
+.. _C++ cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing
+.. _C cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing-c-constructs
+.. _Python cross reference link syntax: https://www.sphinx-doc.org/en/master/usage/restructuredtext/domains.html#cross-referencing-python-objects
+.. _autodoc directives: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directives
.. inclusive-language: enable
+
+.. _commit-style:
+
+--------------
+Commit message
+--------------
+Pigweed commit message bodies and summaries are limited to 72 characters wide
+to improve readability. Commit summaries should also be prefixed with the name
+of the module that the commit is affecting. :ref:`Examples
+<docs-contributing-commit-message-examples>` of well and ill-formed commit
+messages are provided below.
+
+Consider the following when writing a commit message:
+
+#. **Documentation and comments are better** - Consider whether the commit
+ message contents would be better expressed in the documentation or code
+ comments. Docs and code comments are durable and readable later; commit
+ messages are rarely read after the change lands.
+#. **Include why the change is made, not just what the change is** - It is
+ important to include a "why" component in most commits. Sometimes, why is
+ evident - for example, reducing memory usage, or optimizing. But it is often
+ not. Err on the side of over-explaining why, not under-explaining why.
+
+.. _docs-contributing-commit-message-examples:
+
+Pigweed commit messages should conform to the following style:
+
+.. admonition:: **Yes**:
+ :class: checkmark
+
+ .. code:: none
+
+ pw_some_module: Short capitalized description
+
+ Details about the change here. Include a summary of the what, and a clear
+ description of why the change is needed for future maintainers.
+
+ Consider what parts of the commit message are better suited for
+ documentation.
+
+.. admonition:: **Yes**: Small number of modules affected; use {} syntax.
+ :class: checkmark
+
+ .. code:: none
+
+ pw_{foo, bar, baz}: Change something in a few places
+
+ When changes cross a few modules, include them with the syntax shown
+ above.
+
+.. admonition:: **Yes**: Targets are effectively modules, even though they're
+ nested, so they get a ``/`` character.
+ :class: checkmark
+
+ .. code:: none
+
+ targets/xyz123: Tweak support for XYZ's PQR
+
+.. admonition:: **Yes**: Uses imperative style for subject and text.
+ :class: checkmark
+
+ .. code:: none
+
+ pw_something: Add foo and bar functions
+
+ This commit correctly uses imperative present-tense style.
+
+.. admonition:: **No**: Uses non-imperative style for subject and text.
+ :class: error
+
+ .. code:: none
+
+ pw_something: Adds more things
+
+ Use present tense imperative style for subjects and commit. The above
+ subject has a plural "Adds" which is incorrect; should be "Add".
+
+.. admonition:: **Yes**: Use bulleted lists when multiple changes are in a
+ single CL. Prefer smaller CLs, but larger CLs are a practical reality.
+ :class: checkmark
+
+ .. code:: none
+
+ pw_complicated_module: Pre-work for refactor
+
+ Prepare for a bigger refactor by reworking some arguments before the larger
+ change. This change must land in downstream projects before the refactor to
+ enable a smooth transition to the new API.
+
+ - Add arguments to MyImportantClass::MyFunction
+ - Update MyImportantClass to handle precondition Y
+ - Add stub functions to be used during the transition
+
+.. admonition:: **No**: Run on paragraph instead of bulleted list
+ :class: error
+
+ .. code:: none
+
+ pw_foo: Many things in a giant BWOT
+
+ This CL does A, B, and C. The commit message is a Big Wall Of Text
+ (BWOT), which we try to discourage in Pigweed. Also changes X and Y,
+ because Z and Q. Furthermore, in some cases, adds a new Foo (with Bar,
+ because we want to). Also refactors qux and quz.
+
+.. admonition:: **No**: Doesn't capitalize the subject
+ :class: error
+
+ .. code:: none
+
+ pw_foo: do a thing
+
+ Above subject is incorrect, since it is a sentence style subject.
+
+.. admonition:: **Yes**: Doesn't capitalize the subject when subject's first
+ word is a lowercase identifier.
+ :class: checkmark
+
+ .. code:: none
+
+ pw_foo: std::unique_lock cleanup
+
+ This commit message demonstrates the subject when the subject has an
+ identifier for the first word. In that case, follow the identifier casing
+ instead of capitalizing.
+
+ However, imperative style subjects often have the identifier elsewhere in
+ the subject; for example:
+
+ .. code:: none
+
+ pw_foo: Improve use of std::unique_lock
+
+.. admonition:: **No**: Uses a non-standard ``[]`` to indicate module:
+ :class: error
+
+ .. code:: none
+
+ [pw_foo]: Do a thing
+
+.. admonition:: **No**: Has a period at the end of the subject
+ :class: error
+
+ .. code:: none
+
+ pw_bar: Do something great.
+
+.. admonition:: **No**: Puts extra stuff after the module which isn't a module.
+ :class: error
+
+ .. code:: none
+
+ pw_bar/byte_builder: Add more stuff to builder
+
+Footer
+======
+We support a number of `git footers`_ in the commit message, such as ``Bug:
+123`` in the message below:
+
+.. code:: none
+
+ pw_something: Add foo and bar functions
+
+ Bug: 123
+
+You are encouraged to use the following footers when appropriate:
+
+* ``Bug``: Associates this commit with a bug (issue in our `bug tracker`_). The
+ bug will be automatically updated when the change is submitted. When a change
+ is relevant to more than one bug, include multiple ``Bug`` lines, like so:
+
+ .. code:: none
+
+ pw_something: Add foo and bar functions
+
+ Bug: 123
+ Bug: 456
+
+* ``Fixed`` or ``Fixes``: Like ``Bug``, but automatically closes the bug when
+ submitted.
+
+ .. code:: none
+
+ pw_something: Fix incorrect use of foo
+
+ Fixes: 123
+
+In addition, we support all of the `Chromium CQ footers`_, but those are
+relatively rarely useful.
+
+.. _bug tracker: https://bugs.chromium.org/p/pigweed/issues/list
+.. _Chromium CQ footers: https://chromium.googlesource.com/chromium/src/+/refs/heads/main/docs/infra/cq.md#options
+.. _git footers: https://commondatastorage.googleapis.com/chrome-infra-docs/flat/depot_tools/docs/html/git-footers.html
+
+Copy-to-clipboard feature on code blocks
+========================================
+
+.. _sphinx-copybutton: https://sphinx-copybutton.readthedocs.io/en/latest/
+.. _Remove copybuttons using a CSS selector: https://sphinx-copybutton.readthedocs.io/en/latest/use.html#remove-copybuttons-using-a-css-selector
+
+The copy-to-clipboard feature on code blocks is powered by `sphinx-copybutton`_.
+
+``sphinx-copybutton`` recognizes ``$`` as an input prompt and automatically
+removes it.
+
+There is a workflow for manually removing the copy-to-clipboard button for a
+particular code block but it has not been implemented yet. See
+`Remove copybuttons using a CSS selector`_.
diff --git a/docs/targets.rst b/docs/targets.rst
index 7c13097dc..111718774 100644
--- a/docs/targets.rst
+++ b/docs/targets.rst
@@ -41,7 +41,7 @@ the variables ``ar``, ``cc``, and ``cxx`` to the appropriate compilers. Pigweed
provides many commonly used compiler configurations in the ``pw_toolchain``
module.
-The rest of the a Pigweed target's configuration is listed within a ``defaults``
+The rest of a Pigweed target's configuration is listed within a ``defaults``
scope in its toolchain. Every variable in this scope is an override of a GN
build argument defined in Pigweed. Some notable arguments include:
diff --git a/docs/templates/docs/api.rst b/docs/templates/docs/api.rst
new file mode 100644
index 000000000..7ce13f745
--- /dev/null
+++ b/docs/templates/docs/api.rst
@@ -0,0 +1,13 @@
+.. _module-MODULE_NAME-api:
+
+=============
+API Reference
+=============
+..
+ If a module includes a developer-facing API, this is the place to include API
+ documentation. This doc may be omitted for experimental modules that are not
+ yet ready for public use.
+
+ A module may have APIs for multiple languages. If so, replace this file with
+ an `api` directory and an `index.rst` file that links to separate docs for
+ each supported language.
diff --git a/docs/templates/docs/cli.rst b/docs/templates/docs/cli.rst
new file mode 100644
index 000000000..a3afc0185
--- /dev/null
+++ b/docs/templates/docs/cli.rst
@@ -0,0 +1,11 @@
+.. _module-MODULE_NAME-cli:
+
+=============
+CLI Reference
+=============
+..
+ If the module has a command-line interface, document that here. For Python
+ CLIs, the `sphinx-argparse package
+ <https://sphinx-argparse.readthedocs.io/en/stable/usage.html>`_ makes this
+ very simple and is already included in Pigweed. This doc may be omitted for
+ experimental modules that are not yet ready for public use.
diff --git a/docs/templates/docs/concepts.rst b/docs/templates/docs/concepts.rst
new file mode 100644
index 000000000..9dde7063b
--- /dev/null
+++ b/docs/templates/docs/concepts.rst
@@ -0,0 +1,9 @@
+.. _module-MODULE_NAME-concepts:
+
+========
+Concepts
+========
+..
+ This doc describes fundamental concepts related to the problem the module
+ solves which are independent of the module's solution. Topics related to this
+ module's solution and specific design should be in ``design.rst``.
diff --git a/docs/templates/docs/design.rst b/docs/templates/docs/design.rst
new file mode 100644
index 000000000..b214229c3
--- /dev/null
+++ b/docs/templates/docs/design.rst
@@ -0,0 +1,14 @@
+.. _module-MODULE_NAME-design:
+
+======
+Design
+======
+..
+ This doc provides background on how a module works internally, the assumptions
+ inherent in its design, why this particular design was chosen over others, and
+ other topics of that nature.
+
+ This doc is only required when the "Design considerations" section in the
+ index becomes large enough that a separate doc is needed to maintain
+ readability. If *this* doc becomes too large, it can be replaced with a
+ ``design`` subdirectory.
diff --git a/docs/templates/docs/docs.rst b/docs/templates/docs/docs.rst
new file mode 100644
index 000000000..f223d3fef
--- /dev/null
+++ b/docs/templates/docs/docs.rst
@@ -0,0 +1,117 @@
+.. _module-MODULE_NAME:
+
+===========
+MODULE_NAME
+===========
+..
+ A short description of the module. Aim for three sentences at most that
+ clearly convey what the module does.
+
+.. card::
+
+ :octicon:`comment-discussion` Status:
+ :bdg-primary:`Experimental`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Unstable`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Stable`
+ :octicon:`kebab-horizontal`
+ :bdg-primary:`Current`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Deprecated`
+
+ :octicon:`multi-select` Backends: :ref:`module-pw_rpc`
+
+ :octicon:`copy` Facade: :ref:`module-pw_rpc`
+
+----------
+Background
+----------
+..
+ Describe *what* problem this module solves. You can make references to other
+ existing solutions to highlight why their approaches were not suitable, but
+ avoid describing *how* this module solves the problem in this section.
+
+ For some modules, this section may need to include fundamental concepts about
+ the problem being solved. Those should be mentioned briefly here, but covered
+ more extensively in ``concepts.rst``.
+
+------------
+Our solution
+------------
+..
+ Describe *how* this module addresses the problems highlighted in section
+ above. This is a great place to include artifacts other than text to more
+ effectively deliver the message. For example, consider including images or
+ animations for UI-related modules, or code samples illustrating a superior API
+ in libraries.
+
+Design considerations
+---------------------
+..
+ Briefly explain any pertinent design considerations and trade-offs. For
+ example, perhaps this module optimizes for very small RAM and Flash usage at
+ the expense of additional CPU time, where other solutions might make a
+ different choice.
+
+ Many modules will have more extensive documentation on design and
+ implementation details. If so, that additional content should be in
+ ``design.rst`` and linked to from this section.
+
+Size report
+-----------
+..
+ If this module includes code that runs on target, include the size report(s)
+ here.
+
+Roadmap
+-------
+..
+ What are the broad future plans for this module? What remains to be built, and
+ what are some known issues? Are there opportunities for contribution?
+
+---------------
+Who this is for
+---------------
+..
+ Highlight the use cases that are appropriate for this module. This is the
+ place to make the sales pitch-why should developers use this module, and what
+ makes this module stand out from alternative solutions that try to address
+ this problem.
+
+--------------------
+Is it right for you?
+--------------------
+..
+ This module may not solve all problems or be appropriate for all
+ circumstances. This is the place to add those caveats. Highly-experimental
+ modules that are under very active development might be a poor fit for any
+ developer right now. If so, mention that here.
+
+---------------
+Getting started
+---------------
+..
+ Explain how developers use this module in their project, including any
+ prerequisites and conditions. This section should cover the fastest and
+ simplest ways to get started (for example, "add this dependency in GN",
+ "include this header"). More complex and specific use cases should be covered
+ in guides that are linked in this section.
+
+--------
+Contents
+--------
+.. toctree::
+ :maxdepth: 1
+
+ concepts
+ design
+ guides
+ api
+ cli
+ gui
+
+.. toctree::
+ :maxdepth: 2
+
+ tutorials/index
diff --git a/docs/templates/docs/gui.rst b/docs/templates/docs/gui.rst
new file mode 100644
index 000000000..fa63ff7d6
--- /dev/null
+++ b/docs/templates/docs/gui.rst
@@ -0,0 +1,9 @@
+.. _module-MODULE_NAME-gui:
+
+=============
+GUI Reference
+=============
+..
+ If the module has a graphic interface of any kind (including text-mode
+ interfaces and web applications), document its usage here. This doc may be
+ omitted for experimental modules that are not yet ready for public use.
diff --git a/docs/templates/docs/guides.rst b/docs/templates/docs/guides.rst
new file mode 100644
index 000000000..50348dd3f
--- /dev/null
+++ b/docs/templates/docs/guides.rst
@@ -0,0 +1,15 @@
+.. _module-MODULE_NAME-guides:
+
+======
+Guides
+======
+..
+ Guides are task-oriented. They are essentially steps or recipe a user should
+ follow to complete a particular goal.
+
+ Each module should have a minimum of two guides: one guide on how to integrate
+ the module with the user's project, and one guide on the most common use case
+ for the module. Most modules ought to have many more guides than this.
+
+ If this document becomes to large and difficult to navigate, replace it with a
+ ``guides`` subdirectory.
diff --git a/docs/templates/docs/tutorials/index.rst b/docs/templates/docs/tutorials/index.rst
new file mode 100644
index 000000000..037bec8bb
--- /dev/null
+++ b/docs/templates/docs/tutorials/index.rst
@@ -0,0 +1,13 @@
+.. _module-MODULE_NAME-codelabs:
+
+=========
+Tutorials
+=========
+..
+ Tutorials are primarily learning-oriented; they are designed to teach the user
+ about a concept or feature so that they can achieve basic competence.
+
+ If your module has tutorials, this index should link to each of them so that
+ they're populated in the table of contents. Also consider adding narrative
+ instead of simply adding a bulleted list. Guide the user through the
+ curriculum they should follow to become knowledgable about the module.
diff --git a/jest.config.ts b/jest.config.ts
new file mode 100644
index 000000000..0e04b80cf
--- /dev/null
+++ b/jest.config.ts
@@ -0,0 +1,32 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {pathsToModuleNameMapper} from "ts-jest";
+import type {InitialOptionsTsJest} from 'ts-jest/dist/types';
+
+const paths = {
+ "pigweedjs/pw_*": [
+ "./pw_*/ts"
+ ],
+ "pigweedjs/protos/*": [
+ "./dist/protos/*"
+ ]
+}
+const config: InitialOptionsTsJest = {
+ preset: 'ts-jest/presets/js-with-ts',
+ testRegex: "(/__tests__/.*|(\\_|/)(test|spec))\\.[jt]sx?$",
+ moduleNameMapper: pathsToModuleNameMapper(paths, {prefix: '<rootDir>/'})
+}
+
+export default config
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 000000000..d00b93c9e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,17115 @@
+{
+ "name": "pigweedjs",
+ "version": "0.0.7",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "pigweedjs",
+ "version": "0.0.7",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@protobuf-ts/protoc": "^2.7.0",
+ "google-protobuf": "^3.17.3",
+ "long": "^5.2.1",
+ "object-path": "^0.11.8",
+ "ts-protoc-gen": "^0.15.0"
+ },
+ "bin": {
+ "pw_protobuf_compiler": "dist/bin/pw_protobuf_compiler.js"
+ },
+ "devDependencies": {
+ "@grpc/grpc-js": "^1.3.7",
+ "@material-ui/core": "^4.12.1",
+ "@material-ui/lab": "^4.0.0-alpha.60",
+ "@rollup/plugin-commonjs": "^19.0.0",
+ "@rollup/plugin-node-resolve": "^13.3.0",
+ "@rollup/plugin-typescript": "^8.3.3",
+ "@types/crc": "^3.4.0",
+ "@types/google-protobuf": "^3.15.5",
+ "@types/jest": "^28.1.4",
+ "@types/node": "^16.0.1",
+ "@types/react": "^17.0.14",
+ "@types/react-dom": "^17.0.9",
+ "ansi_up": "^5.1.0",
+ "arg": "^5.0.2",
+ "base64-js": "^1.5.1",
+ "buffer": "^6.0.3",
+ "crc": "^4.1.1",
+ "debug": "^4.3.2",
+ "eslint": "^7.30.0",
+ "eslint-plugin-react": "^7.24.0",
+ "grpc-tools": "^1.11.2",
+ "grpc-web": "^1.2.1",
+ "gts": "^3.1.0",
+ "html-react-parser": "^1.4.0",
+ "http-server": "^13.0.2",
+ "install-peers": "^1.0.3",
+ "jest-environment-jsdom": "^28.1.3",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
+ "requirejs": "^2.3.6",
+ "rimraf": "^3.0.2",
+ "rollup": "^2.52.8",
+ "rollup-plugin-dts": "^4.2.2",
+ "rollup-plugin-node-builtins": "^2.1.2",
+ "rollup-plugin-node-globals": "^1.4.0",
+ "rollup-plugin-node-polyfills": "^0.2.1",
+ "rollup-plugin-sourcemaps": "^0.6.3",
+ "rxjs": "^7.2.0",
+ "tmp": "0.2.1",
+ "ts-jest": "^28.0.5",
+ "ts-node": "^10.8.1",
+ "tsc-watch": "^5.0.3",
+ "tslib": "^2.4.0",
+ "typescript": "^4.3.5"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
+ "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.1.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.12.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz",
+ "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.6.tgz",
+ "integrity": "sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@ampproject/remapping": "^2.1.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.18.6",
+ "@babel/helper-compilation-targets": "^7.18.6",
+ "@babel/helper-module-transforms": "^7.18.6",
+ "@babel/helpers": "^7.18.6",
+ "@babel/parser": "^7.18.6",
+ "@babel/template": "^7.18.6",
+ "@babel/traverse": "^7.18.6",
+ "@babel/types": "^7.18.6",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.1",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/highlight": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.18.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.7.tgz",
+ "integrity": "sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.18.7",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "jsesc": "^2.5.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+ "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.6.tgz",
+ "integrity": "sha512-vFjbfhNCzqdeAtZflUFrG5YIFqGTqsctrtkZ1D/NB0mDW9TwW3GmmUepYY4G9wCET5rY5ugz4OGTcLd614IzQg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.18.6",
+ "@babel/helper-validator-option": "^7.18.6",
+ "browserslist": "^4.20.2",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-environment-visitor": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz",
+ "integrity": "sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-function-name": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz",
+ "integrity": "sha512-0mWMxV1aC97dhjCah5U5Ua7668r5ZmSC2DLfH2EZnf9c3/dHZKiFa5pRLMH5tjSl471tY6496ZWk/kjNONBxhw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/template": "^7.18.6",
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-hoist-variables": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
+ "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+ "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.8.tgz",
+ "integrity": "sha512-che3jvZwIcZxrwh63VfnFTUzcAM9v/lznYkkRxIBGMPt1SudOKHAEec0SIRCfiuIzTcF7VGj/CaTT6gY4eWxvA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-environment-visitor": "^7.18.6",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-simple-access": "^7.18.6",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "@babel/template": "^7.18.6",
+ "@babel/traverse": "^7.18.8",
+ "@babel/types": "^7.18.8"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz",
+ "integrity": "sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-simple-access": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz",
+ "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-split-export-declaration": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
+ "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
+ "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
+ "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.6.tgz",
+ "integrity": "sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/template": "^7.18.6",
+ "@babel/traverse": "^7.18.6",
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
+ "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-convert": {
+ "version": "1.9.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.8.tgz",
+ "integrity": "sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz",
+ "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.15.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "regenerator-runtime": "^0.13.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz",
+ "integrity": "sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/parser": "^7.18.6",
+ "@babel/types": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template/node_modules/@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/highlight": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.8.tgz",
+ "integrity": "sha512-UNg/AcSySJYR/+mIcJQDCv00T+AqRO7j/ZEJLzpaYtgM48rMg5MnkJgyNqkzo88+p4tfRvZJCEiwwfG6h4jkRg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.18.7",
+ "@babel/helper-environment-visitor": "^7.18.6",
+ "@babel/helper-function-name": "^7.18.6",
+ "@babel/helper-hoist-variables": "^7.18.6",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/parser": "^7.18.8",
+ "@babel/types": "^7.18.8",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/highlight": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.8.tgz",
+ "integrity": "sha512-qwpdsmraq0aJ3osLJRApsc2ouSJCdnMeZwB0DhbtHAtRpZNZCdlbRnHIgcRKzdE1g0iOGg644fzjOBcdOz9cPw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "to-fast-properties": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.8.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "0.4.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^13.9.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ignore": {
+ "version": "4.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@grpc/grpc-js": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@grpc/proto-loader": "^0.6.4",
+ "@types/node": ">=12.12.47"
+ },
+ "engines": {
+ "node": "^8.13.0 || >=10.10.0"
+ }
+ },
+ "node_modules/@grpc/proto-loader": {
+ "version": "0.6.6",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/long": "^4.0.1",
+ "lodash.camelcase": "^4.3.0",
+ "long": "^4.0.0",
+ "protobufjs": "^6.10.0",
+ "yargs": "^16.1.1"
+ },
+ "bin": {
+ "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@grpc/proto-loader/node_modules/long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+ "dev": true
+ },
+ "node_modules/@grpc/proto-loader/node_modules/protobufjs": {
+ "version": "6.11.2",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/long": "^4.0.1",
+ "@types/node": ">=13.7.0",
+ "long": "^4.0.0"
+ },
+ "bin": {
+ "pbjs": "bin/pbjs",
+ "pbts": "bin/pbts"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.5.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "node_modules/@humanwhocodes/object-schema": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jest/console": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.1.tgz",
+ "integrity": "sha512-0RiUocPVFEm3WRMOStIHbRWllG6iW6E3/gUPnf4lkrVFyXIIDeCe+vlKeYyFOMhB2EPE6FLFCNADSOOQMaqvyA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^28.1.1",
+ "jest-util": "^28.1.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/core": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.2.tgz",
+ "integrity": "sha512-Xo4E+Sb/nZODMGOPt2G3cMmCBqL4/W2Ijwr7/mrXlq4jdJwcFQ/9KrrJZT2adQRk2otVBXXOz1GRQ4Z5iOgvRQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/console": "^28.1.1",
+ "@jest/reporters": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^28.0.2",
+ "jest-config": "^28.1.2",
+ "jest-haste-map": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.1",
+ "jest-resolve-dependencies": "^28.1.2",
+ "jest-runner": "^28.1.2",
+ "jest-runtime": "^28.1.2",
+ "jest-snapshot": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "jest-validate": "^28.1.1",
+ "jest-watcher": "^28.1.1",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^28.1.1",
+ "rimraf": "^3.0.0",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/core/node_modules/ci-info": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
+ "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/@jest/environment": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz",
+ "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "jest-mock": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/expect": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.2.tgz",
+ "integrity": "sha512-HBzyZBeFBiOelNbBKN0pilWbbrGvwDUwAqMC46NVJmWm8AVkuE58NbG1s7DR4cxFt4U5cVLxofAoHxgvC5MyOw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "expect": "^28.1.1",
+ "jest-snapshot": "^28.1.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/expect-utils": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.1.tgz",
+ "integrity": "sha512-n/ghlvdhCdMI/hTcnn4qV57kQuV9OTsZzH1TTCVARANKhl6hXJqLKUkwX69ftMGpsbpt96SsDD8n8LD2d9+FRw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "jest-get-type": "^28.0.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/fake-timers": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz",
+ "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@sinonjs/fake-timers": "^9.1.2",
+ "@types/node": "*",
+ "jest-message-util": "^28.1.3",
+ "jest-mock": "^28.1.3",
+ "jest-util": "^28.1.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/globals": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.2.tgz",
+ "integrity": "sha512-cz0lkJVDOtDaYhvT3Fv2U1B6FtBnV+OpEyJCzTHM1fdoTsU4QNLAt/H4RkiwEUU+dL4g/MFsoTuHeT2pvbo4Hg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.2",
+ "@jest/expect": "^28.1.2",
+ "@jest/types": "^28.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/reporters": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.2.tgz",
+ "integrity": "sha512-/whGLhiwAqeCTmQEouSigUZJPVl7sW8V26EiboImL+UyXznnr1a03/YZ2BX8OlFw0n+Zlwu+EZAITZtaeRTxyA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^28.1.1",
+ "@jest/test-result": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^5.1.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "^28.1.1",
+ "jest-util": "^28.1.1",
+ "jest-worker": "^28.1.1",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "terminal-link": "^2.0.0",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
+ "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@jest/reporters/node_modules/v8-to-istanbul": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz",
+ "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/@jest/schemas": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz",
+ "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==",
+ "dev": true,
+ "dependencies": {
+ "@sinclair/typebox": "^0.24.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/source-map": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz",
+ "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
+ "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@jest/test-result": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.1.tgz",
+ "integrity": "sha512-hPmkugBktqL6rRzwWAtp1JtYT4VHwv8OQ+9lE5Gymj6dHzubI/oJHMUpPOt8NrdVWSrz9S7bHjJUmv2ggFoUNQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/console": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/test-sequencer": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.1.tgz",
+ "integrity": "sha512-nuL+dNSVMcWB7OOtgb0EGH5AjO4UBCt68SLP08rwmC+iRhyuJWS9MtZ/MpipxFwKAlHFftbMsydXqWre8B0+XA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/test-result": "^28.1.1",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/transform": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz",
+ "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^28.1.3",
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^1.4.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "jest-regex-util": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
+ "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@jest/transform/node_modules/write-file-atomic": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz",
+ "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || >=16"
+ }
+ },
+ "node_modules/@jest/types": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz",
+ "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^28.1.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
+ "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.0.0",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.0.8",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "detect-libc": "^1.0.3",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.5",
+ "nopt": "^5.0.0",
+ "npmlog": "^5.0.1",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "node-pre-gyp": "bin/node-pre-gyp"
+ }
+ },
+ "node_modules/@material-ui/core": {
+ "version": "4.12.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.4.4",
+ "@material-ui/styles": "^4.11.4",
+ "@material-ui/system": "^4.12.1",
+ "@material-ui/types": "5.1.0",
+ "@material-ui/utils": "^4.11.2",
+ "@types/react-transition-group": "^4.2.0",
+ "clsx": "^1.0.4",
+ "hoist-non-react-statics": "^3.3.2",
+ "popper.js": "1.16.1-lts",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.8.0 || ^17.0.0",
+ "react-transition-group": "^4.4.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/material-ui"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.6 || ^17.0.0",
+ "react": "^16.8.0 || ^17.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@material-ui/lab": {
+ "version": "4.0.0-alpha.60",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.4.4",
+ "@material-ui/utils": "^4.11.2",
+ "clsx": "^1.0.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.8.0 || ^17.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "peerDependencies": {
+ "@material-ui/core": "^4.12.1",
+ "@types/react": "^16.8.6 || ^17.0.0",
+ "react": "^16.8.0 || ^17.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@material-ui/styles": {
+ "version": "4.11.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.4.4",
+ "@emotion/hash": "^0.8.0",
+ "@material-ui/types": "5.1.0",
+ "@material-ui/utils": "^4.11.2",
+ "clsx": "^1.0.4",
+ "csstype": "^2.5.2",
+ "hoist-non-react-statics": "^3.3.2",
+ "jss": "^10.5.1",
+ "jss-plugin-camel-case": "^10.5.1",
+ "jss-plugin-default-unit": "^10.5.1",
+ "jss-plugin-global": "^10.5.1",
+ "jss-plugin-nested": "^10.5.1",
+ "jss-plugin-props-sort": "^10.5.1",
+ "jss-plugin-rule-value-function": "^10.5.1",
+ "jss-plugin-vendor-prefixer": "^10.5.1",
+ "prop-types": "^15.7.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/material-ui"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.6 || ^17.0.0",
+ "react": "^16.8.0 || ^17.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@material-ui/styles/node_modules/csstype": {
+ "version": "2.6.18",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@material-ui/system": {
+ "version": "4.12.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.4.4",
+ "@material-ui/utils": "^4.11.2",
+ "csstype": "^2.5.2",
+ "prop-types": "^15.7.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/material-ui"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.6 || ^17.0.0",
+ "react": "^16.8.0 || ^17.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@material-ui/system/node_modules/csstype": {
+ "version": "2.6.18",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@material-ui/types": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@material-ui/utils": {
+ "version": "4.11.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.4.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.8.0 || ^17.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@protobuf-ts/protoc": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.7.0.tgz",
+ "integrity": "sha512-YW61nbX9d3mWF44193S+bmWth5eFHxvrePQMUfdY8eEa3PTmhAUwgVCUBeCXEVUWgz1H/E0CnwdjlJgW4vQtOg==",
+ "bin": {
+ "protoc": "protoc.js"
+ }
+ },
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@rollup/plugin-commonjs": {
+ "version": "19.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^3.1.0",
+ "commondir": "^1.0.1",
+ "estree-walker": "^2.0.1",
+ "glob": "^7.1.6",
+ "is-reference": "^1.2.1",
+ "magic-string": "^0.25.7",
+ "resolve": "^1.17.0"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.38.3"
+ }
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/plugin-commonjs/node_modules/magic-string": {
+ "version": "0.25.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ },
+ "node_modules/@rollup/plugin-node-resolve": {
+ "version": "13.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^3.1.0",
+ "@types/resolve": "1.17.1",
+ "deepmerge": "^4.2.2",
+ "is-builtin-module": "^3.1.0",
+ "is-module": "^1.0.0",
+ "resolve": "^1.19.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.42.0"
+ }
+ },
+ "node_modules/@rollup/plugin-typescript": {
+ "version": "8.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^3.1.0",
+ "resolve": "^1.17.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^2.14.0",
+ "tslib": "*",
+ "typescript": ">=3.7.0"
+ },
+ "peerDependenciesMeta": {
+ "tslib": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "0.0.39",
+ "estree-walker": "^1.0.1",
+ "picomatch": "^2.2.2"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0"
+ }
+ },
+ "node_modules/@rollup/pluginutils/node_modules/@types/estree": {
+ "version": "0.0.39",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinclair/typebox": {
+ "version": "0.24.19",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.19.tgz",
+ "integrity": "sha512-gHJu8cdYTD5p4UqmQHrxaWrtb/jkH5imLXzuBypWhKzNkW0qfmgz+w1xaJccWVuJta1YYUdlDiPHXRTR4Ku0MQ==",
+ "dev": true
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "0.14.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
+ "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz",
+ "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^1.7.0"
+ }
+ },
+ "node_modules/@szmarczak/http-timer": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defer-to-connect": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/@tsconfig/node10": {
+ "version": "1.0.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node12": {
+ "version": "1.0.11",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node14": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/node16": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.1.19",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz",
+ "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
+ "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
+ "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.17.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.17.1.tgz",
+ "integrity": "sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.3.0"
+ }
+ },
+ "node_modules/@types/crc": {
+ "version": "3.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "0.0.50",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/google-protobuf": {
+ "version": "3.15.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/graceful-fs": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
+ "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "node_modules/@types/istanbul-reports": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
+ "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
+ "dev": true,
+ "dependencies": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "node_modules/@types/jest": {
+ "version": "28.1.4",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.4.tgz",
+ "integrity": "sha512-telv6G5N7zRJiLcI3Rs3o+ipZ28EnE+7EvF0pSrt2pZOMnAVI/f+6/LucDxOvcBcTeTL3JMF744BbVQAVBUQRA==",
+ "dev": true,
+ "dependencies": {
+ "jest-matcher-utils": "^28.0.0",
+ "pretty-format": "^28.0.0"
+ }
+ },
+ "node_modules/@types/jsdom": {
+ "version": "16.2.14",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.14.tgz",
+ "integrity": "sha512-6BAy1xXEmMuHeAJ4Fv4yXKwBDTGTOseExKE3OaHiNycdHdZw59KfYzrt0DkDluvwmik1HRt6QS7bImxUmpSy+w==",
+ "dev": true,
+ "dependencies": {
+ "@types/node": "*",
+ "@types/parse5": "*",
+ "@types/tough-cookie": "*"
+ }
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/long": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/minimist": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "16.11.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/normalize-package-data": {
+ "version": "2.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/parse5": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz",
+ "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==",
+ "dev": true
+ },
+ "node_modules/@types/prettier": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
+ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "17.0.31",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "17.0.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/react-transition-group": {
+ "version": "4.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/resolve": {
+ "version": "1.17.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/scheduler": {
+ "version": "0.16.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/stack-utils": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
+ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
+ "dev": true
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
+ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
+ "dev": true
+ },
+ "node_modules/@types/yargs": {
+ "version": "17.0.10",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz",
+ "integrity": "sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==",
+ "dev": true,
+ "dependencies": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "node_modules/@types/yargs-parser": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
+ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "4.33.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/experimental-utils": "4.33.0",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "debug": "^4.3.1",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.1.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^4.0.0",
+ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils": {
+ "version": "4.33.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/json-schema": "^7.0.7",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "*"
+ }
+ },
+ "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "4.33.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "4.33.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0"
+ },
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "4.33.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "4.33.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "4.33.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "4.33.0",
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^8.10.0 || ^10.13.0 || >=11.10.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "dev": true
+ },
+ "node_modules/abbrev": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/abstract-leveldown": {
+ "version": "0.12.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "~3.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "7.4.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-globals": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
+ "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^7.1.1",
+ "acorn-walk": "^7.1.1"
+ }
+ },
+ "node_modules/acorn-globals/node_modules/acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi_up": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ansi-align": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.1.0"
+ }
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-escapes": {
+ "version": "4.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.21.3"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/aproba": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/are-we-there-yet": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/are-we-there-yet/node_modules/readable-stream": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/are-we-there-yet/node_modules/string_decoder": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arrify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/asn1.js": {
+ "version": "5.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/async": {
+ "version": "2.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "atob": "bin/atob.js"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
+ "node_modules/babel-jest": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz",
+ "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/transform": "^28.1.3",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^28.1.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.8.0"
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-jest-hoist": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz",
+ "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/babel-preset-current-node-syntax": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
+ "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.8.3",
+ "@babel/plugin-syntax-import-meta": "^7.8.3",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.8.3",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-top-level-await": "^7.8.3"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/babel-preset-jest": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz",
+ "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "babel-plugin-jest-hoist": "^28.1.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/basic-auth": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/bl": {
+ "version": "0.8.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "~1.0.26"
+ }
+ },
+ "node_modules/bn.js": {
+ "version": "4.12.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/boxen": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-align": "^3.0.0",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.1.0",
+ "cli-boxes": "^2.2.1",
+ "string-width": "^4.2.2",
+ "type-fest": "^0.20.2",
+ "widest-line": "^3.1.0",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/boxen/node_modules/camelcase": {
+ "version": "6.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brorand": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/browser-process-hrtime": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
+ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
+ "dev": true
+ },
+ "node_modules/browserify-aes": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/browserify-cipher": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "node_modules/browserify-des": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/browserify-fs": {
+ "version": "1.0.0",
+ "dev": true,
+ "dependencies": {
+ "level-filesystem": "^1.0.1",
+ "level-js": "^2.1.3",
+ "levelup": "^0.18.2"
+ }
+ },
+ "node_modules/browserify-rsa": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^5.0.0",
+ "randombytes": "^2.0.1"
+ }
+ },
+ "node_modules/browserify-rsa/node_modules/bn.js": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/browserify-sign": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "bn.js": "^5.1.1",
+ "browserify-rsa": "^4.0.1",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.3",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.5",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/bn.js": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/browserify-sign/node_modules/readable-stream": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/string_decoder": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.1.tgz",
+ "integrity": "sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ }
+ ],
+ "peer": true,
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001359",
+ "electron-to-chromium": "^1.4.172",
+ "node-releases": "^2.0.5",
+ "update-browserslist-db": "^1.0.4"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/bs-logger": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+ "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+ "dev": true,
+ "dependencies": {
+ "fast-json-stable-stringify": "2.x"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/buffer-es6": {
+ "version": "4.9.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/buffer-xor": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/builtin-modules": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cacheable-request": {
+ "version": "6.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^3.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^4.1.0",
+ "responselike": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cacheable-request/node_modules/get-stream": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cacheable-request/node_modules/lowercase-keys": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-keys": {
+ "version": "6.2.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "map-obj": "^4.0.0",
+ "quick-lru": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/camelcase-keys/node_modules/map-obj": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001365",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001365.tgz",
+ "integrity": "sha512-VDQZ8OtpuIPMBA4YYvZXECtXbddMCUFJk1qu8Mqxfm/SZJNSr1cy4IuLCOL7RJ/YASrvJcYg1Zh+UEUQ5m6z8Q==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ }
+ ],
+ "peer": true
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/chardet": {
+ "version": "0.7.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/chownr": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ci-info": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cipher-base": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
+ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/cli-boxes": {
+ "version": "2.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cli-width": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/clone": {
+ "version": "0.1.19",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/clone-response": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "iojs": ">= 1.0.0",
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/collect-v8-coverage": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
+ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-support": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "color-support": "bin.js"
+ }
+ },
+ "node_modules/colors": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.1.90"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-stream": {
+ "version": "1.6.2",
+ "dev": true,
+ "engines": [
+ "node >= 0.8"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/concat-stream/node_modules/isarray": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-stream/node_modules/readable-stream": {
+ "version": "2.3.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/concat-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-stream/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/configstore": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dot-prop": "^5.2.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^3.0.0",
+ "unique-string": "^2.0.0",
+ "write-file-atomic": "^3.0.0",
+ "xdg-basedir": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/console-control-strings": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.8.0",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.1"
+ }
+ },
+ "node_modules/convert-source-map/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/corser": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/crc": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "buffer": ">=6.0.3"
+ }
+ },
+ "node_modules/create-ecdh": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ }
+ },
+ "node_modules/create-hash": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "node_modules/create-hmac": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crypto-browserify": {
+ "version": "3.12.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/crypto-random-string": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/css-vendor": {
+ "version": "2.0.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.3",
+ "is-in-browser": "^1.0.2"
+ }
+ },
+ "node_modules/cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true
+ },
+ "node_modules/cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dev": true,
+ "dependencies": {
+ "cssom": "~0.3.6"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cssstyle/node_modules/cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true
+ },
+ "node_modules/csstype": {
+ "version": "3.0.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+ "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "dev": true,
+ "dependencies": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls/node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/data-urls/node_modules/whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decamelize-keys": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "decamelize": "^1.1.0",
+ "map-obj": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz",
+ "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==",
+ "dev": true
+ },
+ "node_modules/decode-uri-component": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/decompress-response": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-response": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/dedent": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
+ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/deep-extend": {
+ "version": "0.6.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deepmerge": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/defer-to-connect": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deferred-leveldown": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "abstract-leveldown": "~0.12.1"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "object-keys": "^1.0.12"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/delegates": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/des.js": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "detect-libc": "bin/detect-libc.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/diff-sequences": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz",
+ "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==",
+ "dev": true,
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/diffie-hellman": {
+ "version": "5.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/dom-serializer": {
+ "version": "1.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.0",
+ "entities": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "dev": true,
+ "dependencies": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/domexception/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/domhandler": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.2.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "2.8.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^1.0.1",
+ "domelementtype": "^2.2.0",
+ "domhandler": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/dot-prop": {
+ "version": "5.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-obj": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/duplexer": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/duplexer3": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.4.186",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.186.tgz",
+ "integrity": "sha512-YoVeFrGd/7ROjz4R9uPoND1K/hSRC/xADy9639ZmIZeJSaBnKdYx3I6LMPsY7CXLpK7JFgKQVzeZ/dk2br6Eaw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/elliptic": {
+ "version": "6.5.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.11.9",
+ "brorand": "^1.1.0",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.1",
+ "inherits": "^2.0.4",
+ "minimalistic-assert": "^1.0.1",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/emittery": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz",
+ "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/enquirer": {
+ "version": "2.3.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/entities": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/errno": {
+ "version": "0.1.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prr": "~1.0.1"
+ },
+ "bin": {
+ "errno": "cli.js"
+ }
+ },
+ "node_modules/errno/node_modules/prr": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.19.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-goat": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/escodegen": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
+ "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
+ "dev": true,
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/escodegen/node_modules/levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/escodegen/node_modules/optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/escodegen/node_modules/prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/escodegen/node_modules/type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "7.32.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "7.12.11",
+ "@eslint/eslintrc": "^0.4.3",
+ "@humanwhocodes/config-array": "^0.5.0",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.1.2",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
+ "table": "^6.0.9",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-es": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-utils": "^2.0.0",
+ "regexpp": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=4.19.1"
+ }
+ },
+ "node_modules/eslint-plugin-node": {
+ "version": "11.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-plugin-es": "^3.0.0",
+ "eslint-utils": "^2.0.0",
+ "ignore": "^5.1.1",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.10.1",
+ "semver": "^6.1.0"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=5.16.0"
+ }
+ },
+ "node_modules/eslint-plugin-node/node_modules/semver": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-plugin-prettier": {
+ "version": "3.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prettier-linter-helpers": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=5.0.0",
+ "prettier": ">=1.13.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint-config-prettier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.26.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.3",
+ "array.prototype.flatmap": "^1.2.4",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.2.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.0.4",
+ "object.entries": "^1.1.4",
+ "object.fromentries": "^2.0.4",
+ "object.hasown": "^1.0.0",
+ "object.values": "^1.1.4",
+ "prop-types": "^15.7.2",
+ "resolve": "^2.0.0-next.3",
+ "semver": "^6.3.0",
+ "string.prototype.matchall": "^4.0.5"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/eslint-scope/node_modules/estraverse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint/node_modules/doctrine": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "4.0.6",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/espree": {
+ "version": "7.3.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "0.5.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/event-stream": {
+ "version": "3.3.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "~0.1.1",
+ "from": "~0",
+ "map-stream": "~0.1.0",
+ "pause-stream": "0.0.11",
+ "split": "0.3",
+ "stream-combiner": "~0.0.4",
+ "through": "~2.3.1"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/evp_bytestokey": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/execa": {
+ "version": "5.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/execa?sponsor=1"
+ }
+ },
+ "node_modules/executioner": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mixly": "^1.0.0"
+ }
+ },
+ "node_modules/exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/expect": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.1.tgz",
+ "integrity": "sha512-/AANEwGL0tWBwzLNOvO0yUdy2D52jVdNXppOqswC49sxMN2cPWsGCQdzuIf9tj6hHoBQzNvx75JUYuQAckPo3w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/expect-utils": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "jest-matcher-utils": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-util": "^28.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/external-editor": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/external-editor/node_modules/tmp": {
+ "version": "0.0.33",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "os-tmpdir": "~1.0.2"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-diff": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fastq": {
+ "version": "1.13.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fb-watchman": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz",
+ "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "bser": "2.1.1"
+ }
+ },
+ "node_modules/figures": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^1.0.5"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flat-cache/node_modules/flatted": {
+ "version": "3.2.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.14.4",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreach": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/from": {
+ "version": "0.1.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/fulcon": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/functional-red-black-tree": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fwd-stream": {
+ "version": "1.0.4",
+ "dev": true,
+ "dependencies": {
+ "readable-stream": "~1.0.26-4"
+ }
+ },
+ "node_modules/gauge": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1 || ^2.0.0",
+ "strip-ansi": "^3.0.1 || ^4.0.0",
+ "wide-align": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/gauge/node_modules/ansi-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gauge/node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gauge/node_modules/string-width": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gauge/node_modules/strip-ansi": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/global-dirs": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ini": "2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.11.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.1.1",
+ "ignore": "^5.1.4",
+ "merge2": "^1.3.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/google-protobuf": {
+ "version": "3.19.0",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/got": {
+ "version": "9.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/is": "^0.14.0",
+ "@szmarczak/http-timer": "^1.1.2",
+ "cacheable-request": "^6.0.0",
+ "decompress-response": "^3.3.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^4.1.0",
+ "lowercase-keys": "^1.0.1",
+ "mimic-response": "^1.0.1",
+ "p-cancelable": "^1.0.0",
+ "to-readable-stream": "^1.0.0",
+ "url-parse-lax": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/got/node_modules/get-stream": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.10",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
+ "dev": true
+ },
+ "node_modules/grpc-tools": {
+ "version": "1.11.2",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "@mapbox/node-pre-gyp": "^1.0.5"
+ },
+ "bin": {
+ "grpc_tools_node_protoc": "bin/protoc.js",
+ "grpc_tools_node_protoc_plugin": "bin/protoc_plugin.js"
+ }
+ },
+ "node_modules/grpc-web": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/gts": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "^4.2.0",
+ "@typescript-eslint/parser": "^4.2.0",
+ "chalk": "^4.1.0",
+ "eslint": "^7.10.0",
+ "eslint-config-prettier": "^7.0.0",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-prettier": "^3.1.4",
+ "execa": "^5.0.0",
+ "inquirer": "^7.3.3",
+ "json5": "^2.1.3",
+ "meow": "^9.0.0",
+ "ncp": "^2.0.0",
+ "prettier": "^2.1.2",
+ "rimraf": "^3.0.2",
+ "update-notifier": "^5.0.0",
+ "write-file-atomic": "^3.0.3"
+ },
+ "bin": {
+ "gts": "build/src/cli.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "typescript": ">=3"
+ }
+ },
+ "node_modules/hard-rejection": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/has": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/has-bigints": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-unicode": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-yarn": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hash-base": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/hash-base/node_modules/readable-stream": {
+ "version": "3.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/hash-base/node_modules/string_decoder": {
+ "version": "1.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/hash.js": {
+ "version": "1.1.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hmac-drbg": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hosted-git-info": {
+ "version": "4.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/html-dom-parser": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "4.2.2",
+ "htmlparser2": "6.1.0"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/html-react-parser": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "domhandler": "4.2.2",
+ "html-dom-parser": "1.0.2",
+ "react-property": "2.0.0",
+ "style-to-js": "1.1.0"
+ },
+ "peerDependencies": {
+ "react": "0.14 || 15 || 16 || 17"
+ }
+ },
+ "node_modules/htmlparser2": {
+ "version": "6.1.0",
+ "dev": true,
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.0.0",
+ "domutils": "^2.5.2",
+ "entities": "^2.0.0"
+ }
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "dependencies": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/http-server": {
+ "version": "13.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "^1.0.3",
+ "colors": "^1.4.0",
+ "corser": "^2.0.1",
+ "he": "^1.1.0",
+ "http-proxy": "^1.18.0",
+ "mime": "^1.6.0",
+ "minimist": "^1.2.5",
+ "opener": "^1.5.1",
+ "portfinder": "^1.0.25",
+ "secure-compare": "3.0.1",
+ "union": "~0.5.0",
+ "url-join": "^2.0.5"
+ },
+ "bin": {
+ "http-server": "bin/http-server"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/human-signals": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=10.17.0"
+ }
+ },
+ "node_modules/hyphenate-style-name": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/idb-wrapper": {
+ "version": "1.7.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/ignore": {
+ "version": "5.1.8",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-lazy": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-local": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
+ "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ },
+ "bin": {
+ "import-local-fixture": "fixtures/cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/indexof": {
+ "version": "0.0.1",
+ "dev": true
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/inline-style-parser": {
+ "version": "0.1.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/inquirer": {
+ "version": "7.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.19",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.4.0",
+ "rxjs": "^6.6.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/rxjs": {
+ "version": "6.6.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^1.9.0"
+ },
+ "engines": {
+ "npm": ">=2.0.0"
+ }
+ },
+ "node_modules/inquirer/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/install-peers": {
+ "version": "1.0.3",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "executioner": "^2.0.1"
+ }
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is": {
+ "version": "0.2.7",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-builtin-module": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "builtin-modules": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-ci": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ci-info": "^2.0.0"
+ },
+ "bin": {
+ "is-ci": "bin.js"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.8.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-in-browser": {
+ "version": "1.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-installed-globally": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "global-dirs": "^3.0.0",
+ "is-path-inside": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-module": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-npm": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-obj": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-object": {
+ "version": "0.1.2",
+ "dev": true
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true
+ },
+ "node_modules/is-reference": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "*"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-yarn-global": {
+ "version": "0.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isarray": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isbuffer": {
+ "version": "0.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz",
+ "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument/node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "peer": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^3.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz",
+ "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jest": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.2.tgz",
+ "integrity": "sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/core": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "import-local": "^3.0.2",
+ "jest-cli": "^28.1.2"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-changed-files": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.0.2.tgz",
+ "integrity": "sha512-QX9u+5I2s54ZnGoMEjiM2WeBvJR2J7w/8ZUmH2um/WLAuGAYFQcsVXY9+1YL6k0H/AGUdH8pXUAv6erDqEsvIA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "execa": "^5.0.0",
+ "throat": "^6.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-circus": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.2.tgz",
+ "integrity": "sha512-E2vdPIJG5/69EMpslFhaA46WkcrN74LI5V/cSJ59L7uS8UNoXbzTxmwhpi9XrIL3zqvMt5T0pl5k2l2u2GwBNQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.2",
+ "@jest/expect": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^0.7.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^28.1.1",
+ "jest-matcher-utils": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-runtime": "^28.1.2",
+ "jest-snapshot": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "pretty-format": "^28.1.1",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3",
+ "throat": "^6.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-cli": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.2.tgz",
+ "integrity": "sha512-l6eoi5Do/IJUXAFL9qRmDiFpBeEJAnjJb1dcd9i/VWfVWbp3mJhuH50dNtX67Ali4Ecvt4eBkWb4hXhPHkAZTw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/core": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "import-local": "^3.0.2",
+ "jest-config": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "jest-validate": "^28.1.1",
+ "prompts": "^2.0.1",
+ "yargs": "^17.3.1"
+ },
+ "bin": {
+ "jest": "bin/jest.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "node-notifier": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-cli/node_modules/yargs": {
+ "version": "17.5.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
+ "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jest-cli/node_modules/yargs-parser": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
+ "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jest-config": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.2.tgz",
+ "integrity": "sha512-g6EfeRqddVbjPVBVY4JWpUY4IvQoFRIZcv4V36QkqzE0IGhEC/VkugFeBMAeUE7PRgC8KJF0yvJNDeQRbamEVA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@jest/test-sequencer": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "babel-jest": "^28.1.2",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^28.1.2",
+ "jest-environment-node": "^28.1.2",
+ "jest-get-type": "^28.0.2",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.1",
+ "jest-runner": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "jest-validate": "^28.1.1",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^28.1.1",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": "*",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-config/node_modules/ci-info": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
+ "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/jest-diff": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.1.tgz",
+ "integrity": "sha512-/MUUxeR2fHbqHoMMiffe/Afm+U8U4olFRJ0hiVG2lZatPJcnGxx292ustVu7bULhjV65IYMxRdploAKLbcrsyg==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-docblock": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz",
+ "integrity": "sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "detect-newline": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-each": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.1.tgz",
+ "integrity": "sha512-A042rqh17ZvEhRceDMi784ppoXR7MWGDEKTXEZXb4svt0eShMZvijGxzKsx+yIjeE8QYmHPrnHiTSQVhN4nqaw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/types": "^28.1.1",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^28.0.2",
+ "jest-util": "^28.1.1",
+ "pretty-format": "^28.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-environment-jsdom": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-28.1.3.tgz",
+ "integrity": "sha512-HnlGUmZRdxfCByd3GM2F100DgQOajUBzEitjGqIREcb45kGjZvRrKUdlaF6escXBdcXNl0OBh+1ZrfeZT3GnAg==",
+ "dev": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.3",
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/jsdom": "^16.2.4",
+ "@types/node": "*",
+ "jest-mock": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "jsdom": "^19.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-environment-node": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.2.tgz",
+ "integrity": "sha512-oYsZz9Qw27XKmOgTtnl0jW7VplJkN2oeof+SwAwKFQacq3CLlG9u4kTGuuLWfvu3J7bVutWlrbEQMOCL/jughw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.2",
+ "@jest/fake-timers": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "jest-mock": "^28.1.1",
+ "jest-util": "^28.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-get-type": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz",
+ "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-haste-map": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz",
+ "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "jest-worker": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "^2.3.2"
+ }
+ },
+ "node_modules/jest-leak-detector": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.1.tgz",
+ "integrity": "sha512-4jvs8V8kLbAaotE+wFR7vfUGf603cwYtFf1/PYEsyX2BAjSzj8hQSVTP6OWzseTl0xL6dyHuKs2JAks7Pfubmw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-matcher-utils": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.1.tgz",
+ "integrity": "sha512-NPJPRWrbmR2nAJ+1nmnfcKKzSwgfaciCCrYZzVnNoxVoyusYWIjkBMNvu0RHJe7dNj4hH3uZOPZsQA+xAYWqsw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-message-util": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz",
+ "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^28.1.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^28.1.3",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-message-util/node_modules/@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.18.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/jest-mock": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz",
+ "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-pnp-resolver": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
+ "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "peerDependencies": {
+ "jest-resolve": "*"
+ },
+ "peerDependenciesMeta": {
+ "jest-resolve": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jest-regex-util": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz",
+ "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-resolve": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.1.tgz",
+ "integrity": "sha512-/d1UbyUkf9nvsgdBildLe6LAD4DalgkgZcKd0nZ8XUGPyA/7fsnaQIlKVnDiuUXv/IeZhPEDrRJubVSulxrShA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.1",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^28.1.1",
+ "jest-validate": "^28.1.1",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^1.1.0",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-resolve-dependencies": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.2.tgz",
+ "integrity": "sha512-OXw4vbOZuyRTBi3tapWBqdyodU+T33ww5cPZORuTWkg+Y8lmsxQlVu3MWtJh6NMlKRTHQetF96yGPv01Ye7Mbg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "jest-regex-util": "^28.0.2",
+ "jest-snapshot": "^28.1.2"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-runner": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.2.tgz",
+ "integrity": "sha512-6/k3DlAsAEr5VcptCMdhtRhOoYClZQmxnVMZvZ/quvPGRpN7OBQYPIC32tWSgOnbgqLXNs5RAniC+nkdFZpD4A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/console": "^28.1.1",
+ "@jest/environment": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.10.2",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^28.1.1",
+ "jest-environment-node": "^28.1.2",
+ "jest-haste-map": "^28.1.1",
+ "jest-leak-detector": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-resolve": "^28.1.1",
+ "jest-runtime": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "jest-watcher": "^28.1.1",
+ "jest-worker": "^28.1.1",
+ "source-map-support": "0.5.13",
+ "throat": "^6.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-runner/node_modules/source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/jest-runtime": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.2.tgz",
+ "integrity": "sha512-i4w93OsWzLOeMXSi9epmakb2+3z0AchZtUQVF1hesBmcQQy4vtaql5YdVe9KexdJaVRyPDw8DoBR0j3lYsZVYw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/environment": "^28.1.2",
+ "@jest/fake-timers": "^28.1.2",
+ "@jest/globals": "^28.1.2",
+ "@jest/source-map": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "execa": "^5.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-mock": "^28.1.1",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.1",
+ "jest-snapshot": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-snapshot": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.2.tgz",
+ "integrity": "sha512-wzrieFttZYfLvrCVRJxX+jwML2YTArOUqFpCoSVy1QUapx+LlV9uLbV/mMEhYj4t7aMeE9aSQFHSvV/oNoDAMA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/traverse": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@types/babel__traverse": "^7.0.6",
+ "@types/prettier": "^2.1.5",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^28.1.1",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "jest-haste-map": "^28.1.1",
+ "jest-matcher-utils": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-util": "^28.1.1",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^28.1.1",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-util": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz",
+ "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==",
+ "dev": true,
+ "dependencies": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-util/node_modules/ci-info": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
+ "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==",
+ "dev": true
+ },
+ "node_modules/jest-validate": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.1.tgz",
+ "integrity": "sha512-Kpf6gcClqFCIZ4ti5++XemYJWUPCFUW+N2gknn+KgnDf549iLul3cBuKVe1YcWRlaF8tZV8eJCap0eECOEE3Ug==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/types": "^28.1.1",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^28.0.2",
+ "leven": "^3.1.0",
+ "pretty-format": "^28.1.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-validate/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/jest-watcher": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.1.tgz",
+ "integrity": "sha512-RQIpeZ8EIJMxbQrXpJQYIIlubBnB9imEHsxxE41f54ZwcqWLysL/A0ZcdMirf+XsMn3xfphVQVV4EW0/p7i7Ug==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@jest/test-result": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.10.2",
+ "jest-util": "^28.1.1",
+ "string-length": "^4.0.1"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-worker": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
+ "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/jest-worker/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "3.14.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/js-yaml/node_modules/argparse": {
+ "version": "1.0.10",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz",
+ "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==",
+ "dev": true,
+ "dependencies": {
+ "abab": "^2.0.5",
+ "acorn": "^8.5.0",
+ "acorn-globals": "^6.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.1",
+ "decimal.js": "^10.3.1",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.0",
+ "parse5": "6.0.1",
+ "saxes": "^5.0.1",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.0.0",
+ "w3c-hr-time": "^1.0.2",
+ "w3c-xmlserializer": "^3.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^10.0.0",
+ "ws": "^8.2.3",
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "canvas": "^2.5.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/acorn": {
+ "version": "8.7.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
+ "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dev": true,
+ "dependencies": {
+ "punycode": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-url": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz",
+ "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true,
+ "peer": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jss": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "csstype": "^3.0.2",
+ "is-in-browser": "^1.1.3",
+ "tiny-warning": "^1.0.2"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/jss"
+ }
+ },
+ "node_modules/jss-plugin-camel-case": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "hyphenate-style-name": "^1.0.3",
+ "jss": "10.8.1"
+ }
+ },
+ "node_modules/jss-plugin-default-unit": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1"
+ }
+ },
+ "node_modules/jss-plugin-global": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1"
+ }
+ },
+ "node_modules/jss-plugin-nested": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "node_modules/jss-plugin-props-sort": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1"
+ }
+ },
+ "node_modules/jss-plugin-rule-value-function": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "node_modules/jss-plugin-vendor-prefixer": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.3.1",
+ "css-vendor": "^2.0.8",
+ "jss": "10.8.1"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-includes": "^3.1.3",
+ "object.assign": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.0"
+ }
+ },
+ "node_modules/kind-of": {
+ "version": "6.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/latest-version": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "package-json": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/level-blobs": {
+ "version": "0.1.7",
+ "dev": true,
+ "dependencies": {
+ "level-peek": "1.0.6",
+ "once": "^1.3.0",
+ "readable-stream": "^1.0.26-4"
+ }
+ },
+ "node_modules/level-blobs/node_modules/readable-stream": {
+ "version": "1.1.14",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "node_modules/level-filesystem": {
+ "version": "1.2.0",
+ "dev": true,
+ "dependencies": {
+ "concat-stream": "^1.4.4",
+ "errno": "^0.1.1",
+ "fwd-stream": "^1.0.4",
+ "level-blobs": "^0.1.7",
+ "level-peek": "^1.0.6",
+ "level-sublevel": "^5.2.0",
+ "octal": "^1.0.0",
+ "once": "^1.3.0",
+ "xtend": "^2.2.0"
+ }
+ },
+ "node_modules/level-filesystem/node_modules/xtend": {
+ "version": "2.2.0",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/level-fix-range": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "clone": "~0.1.9"
+ }
+ },
+ "node_modules/level-hooks": {
+ "version": "4.5.0",
+ "dev": true,
+ "dependencies": {
+ "string-range": "~1.2"
+ }
+ },
+ "node_modules/level-js": {
+ "version": "2.2.4",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "abstract-leveldown": "~0.12.0",
+ "idb-wrapper": "^1.5.0",
+ "isbuffer": "~0.0.0",
+ "ltgt": "^2.1.2",
+ "typedarray-to-buffer": "~1.0.0",
+ "xtend": "~2.1.2"
+ }
+ },
+ "node_modules/level-js/node_modules/object-keys": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/level-js/node_modules/typedarray-to-buffer": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/level-js/node_modules/xtend": {
+ "version": "2.1.2",
+ "dev": true,
+ "dependencies": {
+ "object-keys": "~0.4.0"
+ },
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/level-peek": {
+ "version": "1.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "level-fix-range": "~1.0.2"
+ }
+ },
+ "node_modules/level-peek/node_modules/level-fix-range": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/level-sublevel": {
+ "version": "5.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "level-fix-range": "2.0",
+ "level-hooks": ">=4.4.0 <5",
+ "string-range": "~1.2.1",
+ "xtend": "~2.0.4"
+ }
+ },
+ "node_modules/level-sublevel/node_modules/object-keys": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "foreach": "~2.0.1",
+ "indexof": "~0.0.1",
+ "is": "~0.2.6"
+ }
+ },
+ "node_modules/level-sublevel/node_modules/xtend": {
+ "version": "2.0.6",
+ "dev": true,
+ "dependencies": {
+ "is-object": "~0.1.2",
+ "object-keys": "~0.2.0"
+ },
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/levelup": {
+ "version": "0.18.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bl": "~0.8.1",
+ "deferred-leveldown": "~0.2.0",
+ "errno": "~0.1.1",
+ "prr": "~0.0.0",
+ "readable-stream": "~1.0.26",
+ "semver": "~2.3.1",
+ "xtend": "~3.0.0"
+ }
+ },
+ "node_modules/levelup/node_modules/semver": {
+ "version": "2.3.2",
+ "dev": true,
+ "license": "BSD",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.1.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.camelcase": {
+ "version": "4.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+ "dev": true
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
+ "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lowercase-keys": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ltgt": {
+ "version": "2.2.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.22.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "vlq": "^0.2.2"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/make-error": {
+ "version": "1.3.6",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "node_modules/map-obj": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/map-stream": {
+ "version": "0.1.0",
+ "dev": true
+ },
+ "node_modules/md5.js": {
+ "version": "1.3.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/meow": {
+ "version": "9.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/minimist": "^1.2.0",
+ "camelcase-keys": "^6.2.2",
+ "decamelize": "^1.2.0",
+ "decamelize-keys": "^1.1.0",
+ "hard-rejection": "^2.1.0",
+ "minimist-options": "4.1.0",
+ "normalize-package-data": "^3.0.0",
+ "read-pkg-up": "^7.0.1",
+ "redent": "^3.0.0",
+ "trim-newlines": "^3.0.0",
+ "type-fest": "^0.18.0",
+ "yargs-parser": "^20.2.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/meow/node_modules/type-fest": {
+ "version": "0.18.1",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge-stream": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/miller-rabin": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ },
+ "bin": {
+ "miller-rabin": "bin/miller-rabin"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mimic-fn": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mimic-response": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimatch": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimist-options": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "arrify": "^1.0.1",
+ "is-plain-obj": "^1.1.0",
+ "kind-of": "^6.0.3"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/mixly": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fulcon": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minimist": "^1.2.5"
+ },
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mute-stream": {
+ "version": "0.0.8",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ncp": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "ncp": "bin/ncp"
+ }
+ },
+ "node_modules/node-cleanup": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-fetch": {
+ "version": "2.6.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ }
+ },
+ "node_modules/node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
+ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/nopt": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "1"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^4.0.1",
+ "is-core-module": "^2.5.0",
+ "semver": "^7.3.4",
+ "validate-npm-package-license": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-url": {
+ "version": "4.5.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npm-run-path": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/npmlog": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
+ }
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz",
+ "integrity": "sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==",
+ "dev": true
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.11.0",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object-path": {
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
+ "integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==",
+ "engines": {
+ "node": ">= 10.12.0"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.hasown": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/octal": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-fn": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/opener": {
+ "version": "1.5.2",
+ "dev": true,
+ "license": "(WTFPL OR MIT)",
+ "bin": {
+ "opener": "bin/opener-bin.js"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/os-tmpdir": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/p-cancelable": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json": {
+ "version": "6.5.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "got": "^9.6.0",
+ "registry-auth-token": "^4.0.0",
+ "registry-url": "^5.0.0",
+ "semver": "^6.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/package-json/node_modules/semver": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-asn1": {
+ "version": "5.1.6",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "asn1.js": "^5.2.0",
+ "browserify-aes": "^1.0.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-json/node_modules/@babel/code-frame": {
+ "version": "7.15.8",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/highlight": "^7.14.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+ "dev": true
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pause-stream": {
+ "version": "0.0.11",
+ "dev": true,
+ "license": [
+ "MIT",
+ "Apache2"
+ ],
+ "dependencies": {
+ "through": "~2.3"
+ }
+ },
+ "node_modules/pbkdf2": {
+ "version": "3.1.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pirates": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
+ "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/popper.js": {
+ "version": "1.16.1-lts",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/portfinder": {
+ "version": "1.0.28",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^2.6.2",
+ "debug": "^3.1.1",
+ "mkdirp": "^0.5.5"
+ },
+ "engines": {
+ "node": ">= 0.12.0"
+ }
+ },
+ "node_modules/portfinder/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/portfinder/node_modules/ms": {
+ "version": "2.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prepend-http": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "2.4.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/prettier-linter-helpers": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz",
+ "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==",
+ "dev": true,
+ "dependencies": {
+ "@jest/schemas": "^28.1.3",
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format/node_modules/react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
+ "dev": true
+ },
+ "node_modules/process-es6": {
+ "version": "0.11.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.7.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/prr": {
+ "version": "0.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ps-tree": {
+ "version": "1.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "event-stream": "=3.3.4"
+ },
+ "bin": {
+ "ps-tree": "bin/ps-tree.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/psl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
+ "dev": true
+ },
+ "node_modules/public-encrypt": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/pump": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pupa": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "escape-goat": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/quick-lru": {
+ "version": "4.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/randomfill": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/rc": {
+ "version": "1.2.8",
+ "dev": true,
+ "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "dependencies": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "bin": {
+ "rc": "cli.js"
+ }
+ },
+ "node_modules/rc/node_modules/ini": {
+ "version": "1.3.8",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/rc/node_modules/strip-json-comments": {
+ "version": "2.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "17.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "17.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "scheduler": "^0.20.2"
+ },
+ "peerDependencies": {
+ "react": "17.0.2"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "17.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-property": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/react-transition-group": {
+ "version": "4.4.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up": {
+ "version": "7.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.1.0",
+ "read-pkg": "^5.2.0",
+ "type-fest": "^0.8.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/find-up": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/locate-path": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-limit": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/p-locate": {
+ "version": "4.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg-up/node_modules/type-fest": {
+ "version": "0.8.1",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/read-pkg/node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/read-pkg/node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/read-pkg/node_modules/semver": {
+ "version": "5.7.1",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/read-pkg/node_modules/type-fest": {
+ "version": "0.6.0",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "1.0.34",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.9",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.3.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexpp": {
+ "version": "3.2.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ }
+ },
+ "node_modules/registry-auth-token": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rc": "^1.2.8"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/registry-url": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rc": "^1.2.8"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/requirejs": {
+ "version": "2.3.6",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "r_js": "bin/r.js",
+ "r.js": "bin/r.js"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/resolve": {
+ "version": "1.20.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-cwd/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve.exports": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz",
+ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/responselike": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lowercase-keys": "^1.0.0"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ripemd160": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "2.58.0",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rollup-plugin-dts": {
+ "version": "4.2.2",
+ "dev": true,
+ "license": "LGPL-3.0",
+ "dependencies": {
+ "magic-string": "^0.26.1"
+ },
+ "engines": {
+ "node": ">=v12.22.11"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/Swatinem"
+ },
+ "optionalDependencies": {
+ "@babel/code-frame": "^7.16.7"
+ },
+ "peerDependencies": {
+ "rollup": "^2.55",
+ "typescript": "^4.1"
+ }
+ },
+ "node_modules/rollup-plugin-dts/node_modules/@babel/code-frame": {
+ "version": "7.16.7",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@babel/highlight": "^7.16.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/rollup-plugin-dts/node_modules/magic-string": {
+ "version": "0.26.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/rollup-plugin-inject": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
+ "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==",
+ "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.",
+ "dev": true,
+ "dependencies": {
+ "estree-walker": "^0.6.1",
+ "magic-string": "^0.25.3",
+ "rollup-pluginutils": "^2.8.1"
+ }
+ },
+ "node_modules/rollup-plugin-inject/node_modules/estree-walker": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+ "dev": true
+ },
+ "node_modules/rollup-plugin-inject/node_modules/magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "dev": true,
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ },
+ "node_modules/rollup-plugin-node-builtins": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "browserify-fs": "^1.0.0",
+ "buffer-es6": "^4.9.2",
+ "crypto-browserify": "^3.11.0",
+ "process-es6": "^0.11.2"
+ }
+ },
+ "node_modules/rollup-plugin-node-globals": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "acorn": "^5.7.3",
+ "buffer-es6": "^4.9.3",
+ "estree-walker": "^0.5.2",
+ "magic-string": "^0.22.5",
+ "process-es6": "^0.11.6",
+ "rollup-pluginutils": "^2.3.1"
+ }
+ },
+ "node_modules/rollup-plugin-node-globals/node_modules/acorn": {
+ "version": "5.7.4",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/rollup-plugin-node-polyfills": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz",
+ "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==",
+ "dev": true,
+ "dependencies": {
+ "rollup-plugin-inject": "^3.0.0"
+ }
+ },
+ "node_modules/rollup-plugin-sourcemaps": {
+ "version": "0.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^3.0.9",
+ "source-map-resolve": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">=10.0.0",
+ "rollup": ">=0.31.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/rollup-pluginutils": {
+ "version": "2.8.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "estree-walker": "^0.6.1"
+ }
+ },
+ "node_modules/rollup-pluginutils/node_modules/estree-walker": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/run-async": {
+ "version": "2.4.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.4.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "~2.1.0"
+ }
+ },
+ "node_modules/rxjs/node_modules/tslib": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "dev": true,
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.20.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "node_modules/secure-compare": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.3.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver-diff": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/semver-diff/node_modules/semver": {
+ "version": "6.3.0",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/sha.js": {
+ "version": "2.4.11",
+ "dev": true,
+ "license": "(MIT AND BSD-3-Clause)",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-resolve": {
+ "version": "0.6.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0"
+ }
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.10",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/split": {
+ "version": "0.3.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "through": "2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz",
+ "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stream-combiner": {
+ "version": "0.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "duplexer": "~0.1.1"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "0.10.31",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-argv": {
+ "version": "0.1.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6.19"
+ }
+ },
+ "node_modules/string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/string-range": {
+ "version": "1.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.6",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "regexp.prototype.flags": "^1.3.1",
+ "side-channel": "^1.0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-final-newline": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/style-to-js": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "style-to-object": "0.3.0"
+ }
+ },
+ "node_modules/style-to-object": {
+ "version": "0.3.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inline-style-parser": "0.1.1"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz",
+ "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
+ "node_modules/table": {
+ "version": "6.7.2",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ajv": {
+ "version": "8.6.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/table/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tar": {
+ "version": "6.1.11",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "1.0.4",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/terminal-link": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
+ "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "ansi-escapes": "^4.2.1",
+ "supports-hyperlinks": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "dev": true,
+ "license": "ISC",
+ "peer": true,
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/text-table": {
+ "version": "0.2.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/throat": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz",
+ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tiny-warning": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tmp": {
+ "version": "0.2.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rimraf": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8.17.0"
+ }
+ },
+ "node_modules/tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-readable-stream": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tough-cookie": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
+ "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
+ "dev": true,
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.1.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/trim-newlines": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ts-jest": {
+ "version": "28.0.5",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.5.tgz",
+ "integrity": "sha512-Sx9FyP9pCY7pUzQpy4FgRZf2bhHY3za576HMKJFs+OnQ9jS96Du5vNsDKkyedQkik+sEabbKAnCliv9BEsHZgQ==",
+ "dev": true,
+ "dependencies": {
+ "bs-logger": "0.x",
+ "fast-json-stable-stringify": "2.x",
+ "jest-util": "^28.0.0",
+ "json5": "^2.2.1",
+ "lodash.memoize": "4.x",
+ "make-error": "1.x",
+ "semver": "7.x",
+ "yargs-parser": "^21.0.1"
+ },
+ "bin": {
+ "ts-jest": "cli.js"
+ },
+ "engines": {
+ "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0"
+ },
+ "peerDependencies": {
+ "@babel/core": ">=7.0.0-beta.0 <8",
+ "babel-jest": "^28.0.0",
+ "jest": "^28.0.0",
+ "typescript": ">=4.3"
+ },
+ "peerDependenciesMeta": {
+ "@babel/core": {
+ "optional": true
+ },
+ "babel-jest": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-jest/node_modules/yargs-parser": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
+ "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ts-node": {
+ "version": "10.8.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "bin": {
+ "ts-node": "dist/bin.js",
+ "ts-node-cwd": "dist/bin-cwd.js",
+ "ts-node-esm": "dist/bin-esm.js",
+ "ts-node-script": "dist/bin-script.js",
+ "ts-node-transpile-only": "dist/bin-transpile.js",
+ "ts-script": "dist/bin-script-deprecated.js"
+ },
+ "peerDependencies": {
+ "@swc/core": ">=1.2.50",
+ "@swc/wasm": ">=1.2.50",
+ "@types/node": "*",
+ "typescript": ">=2.7"
+ },
+ "peerDependenciesMeta": {
+ "@swc/core": {
+ "optional": true
+ },
+ "@swc/wasm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ts-node/node_modules/acorn": {
+ "version": "8.7.1",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ts-node/node_modules/arg": {
+ "version": "4.1.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ts-protoc-gen": {
+ "version": "0.15.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "google-protobuf": "^3.15.5"
+ },
+ "bin": {
+ "protoc-gen-ts": "bin/protoc-gen-ts"
+ }
+ },
+ "node_modules/tsc-watch": {
+ "version": "5.0.3",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "node-cleanup": "^2.1.2",
+ "ps-tree": "^1.2.0",
+ "string-argv": "^0.1.1",
+ "strip-ansi": "^6.0.0"
+ },
+ "bin": {
+ "tsc-watch": "index.js"
+ },
+ "engines": {
+ "node": ">=8.17.0"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.4.0",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/tsutils": {
+ "version": "3.21.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^1.8.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ },
+ "peerDependencies": {
+ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+ }
+ },
+ "node_modules/tsutils/node_modules/tslib": {
+ "version": "1.14.1",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.20.2",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "4.4.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=4.2.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/union": {
+ "version": "0.5.0",
+ "dev": true,
+ "dependencies": {
+ "qs": "^6.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/union/node_modules/qs": {
+ "version": "6.10.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unique-string": {
+ "version": "2.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "crypto-random-string": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz",
+ "integrity": "sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ }
+ ],
+ "peer": true,
+ "dependencies": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ },
+ "bin": {
+ "browserslist-lint": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/update-notifier": {
+ "version": "5.1.0",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boxen": "^5.0.0",
+ "chalk": "^4.1.0",
+ "configstore": "^5.0.1",
+ "has-yarn": "^2.1.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^2.0.0",
+ "is-installed-globally": "^0.4.0",
+ "is-npm": "^5.0.0",
+ "is-yarn-global": "^0.3.0",
+ "latest-version": "^5.1.0",
+ "pupa": "^2.1.1",
+ "semver": "^7.3.4",
+ "semver-diff": "^3.1.1",
+ "xdg-basedir": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/yeoman/update-notifier?sponsor=1"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/url-join": {
+ "version": "2.0.5",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/url-parse-lax": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prepend-http": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-compile-cache": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/vlq": {
+ "version": "0.2.3",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/w3c-hr-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+ "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+ "dev": true,
+ "dependencies": {
+ "browser-process-hrtime": "^1.0.0"
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz",
+ "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==",
+ "dev": true,
+ "dependencies": {
+ "xml-name-validator": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "node_modules/widest-line": {
+ "version": "3.1.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "string-width": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/write-file-atomic": {
+ "version": "3.0.3",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz",
+ "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xdg-basedir": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ },
+ "node_modules/xtend": {
+ "version": "3.0.0",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "4.0.0",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yn": {
+ "version": "3.1.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ },
+ "dependencies": {
+ "@ampproject/remapping": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz",
+ "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jridgewell/gen-mapping": "^0.1.0",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ },
+ "@babel/code-frame": {
+ "version": "7.12.11",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.10.4"
+ }
+ },
+ "@babel/compat-data": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz",
+ "integrity": "sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==",
+ "dev": true,
+ "peer": true
+ },
+ "@babel/core": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.6.tgz",
+ "integrity": "sha512-cQbWBpxcbbs/IUredIPkHiAGULLV8iwgNRMFzvbhEXISp4f3rUUXE5+TIw6KwUWUR3DwyI6gmBRnmAtYaWehwQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@ampproject/remapping": "^2.1.0",
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.18.6",
+ "@babel/helper-compilation-targets": "^7.18.6",
+ "@babel/helper-module-transforms": "^7.18.6",
+ "@babel/helpers": "^7.18.6",
+ "@babel/parser": "^7.18.6",
+ "@babel/template": "^7.18.6",
+ "@babel/traverse": "^7.18.6",
+ "@babel/types": "^7.18.6",
+ "convert-source-map": "^1.7.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.1",
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/highlight": "^7.18.6"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "@babel/generator": {
+ "version": "7.18.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.7.tgz",
+ "integrity": "sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/types": "^7.18.7",
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "jsesc": "^2.5.1"
+ },
+ "dependencies": {
+ "@jridgewell/gen-mapping": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
+ "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jridgewell/set-array": "^1.0.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.9"
+ }
+ }
+ }
+ },
+ "@babel/helper-compilation-targets": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.6.tgz",
+ "integrity": "sha512-vFjbfhNCzqdeAtZflUFrG5YIFqGTqsctrtkZ1D/NB0mDW9TwW3GmmUepYY4G9wCET5rY5ugz4OGTcLd614IzQg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/compat-data": "^7.18.6",
+ "@babel/helper-validator-option": "^7.18.6",
+ "browserslist": "^4.20.2",
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "@babel/helper-environment-visitor": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz",
+ "integrity": "sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q==",
+ "dev": true,
+ "peer": true
+ },
+ "@babel/helper-function-name": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz",
+ "integrity": "sha512-0mWMxV1aC97dhjCah5U5Ua7668r5ZmSC2DLfH2EZnf9c3/dHZKiFa5pRLMH5tjSl471tY6496ZWk/kjNONBxhw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/template": "^7.18.6",
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-hoist-variables": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz",
+ "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-module-imports": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
+ "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-module-transforms": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.8.tgz",
+ "integrity": "sha512-che3jvZwIcZxrwh63VfnFTUzcAM9v/lznYkkRxIBGMPt1SudOKHAEec0SIRCfiuIzTcF7VGj/CaTT6gY4eWxvA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-environment-visitor": "^7.18.6",
+ "@babel/helper-module-imports": "^7.18.6",
+ "@babel/helper-simple-access": "^7.18.6",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "@babel/template": "^7.18.6",
+ "@babel/traverse": "^7.18.8",
+ "@babel/types": "^7.18.8"
+ }
+ },
+ "@babel/helper-plugin-utils": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.6.tgz",
+ "integrity": "sha512-gvZnm1YAAxh13eJdkb9EWHBnF3eAub3XTLCZEehHT2kWxiKVRL64+ae5Y6Ivne0mVHmMYKT+xWgZO+gQhuLUBg==",
+ "dev": true,
+ "peer": true
+ },
+ "@babel/helper-simple-access": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz",
+ "integrity": "sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-split-export-declaration": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz",
+ "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/helper-validator-identifier": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz",
+ "integrity": "sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==",
+ "dev": true
+ },
+ "@babel/helper-validator-option": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz",
+ "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==",
+ "dev": true,
+ "peer": true
+ },
+ "@babel/helpers": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.6.tgz",
+ "integrity": "sha512-vzSiiqbQOghPngUYt/zWGvK3LAsPhz55vc9XNN0xAl2gV4ieShI2OQli5duxWHD+72PZPTKAcfcZDE1Cwc5zsQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/template": "^7.18.6",
+ "@babel/traverse": "^7.18.6",
+ "@babel/types": "^7.18.6"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz",
+ "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==",
+ "dev": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "chalk": "^2.0.0",
+ "js-tokens": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "@babel/parser": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.8.tgz",
+ "integrity": "sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA==",
+ "dev": true,
+ "peer": true
+ },
+ "@babel/plugin-syntax-async-generators": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz",
+ "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-bigint": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz",
+ "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-class-properties": {
+ "version": "7.12.13",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz",
+ "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.12.13"
+ }
+ },
+ "@babel/plugin-syntax-import-meta": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz",
+ "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-syntax-json-strings": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz",
+ "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-logical-assignment-operators": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz",
+ "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-syntax-nullish-coalescing-operator": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz",
+ "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-numeric-separator": {
+ "version": "7.10.4",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz",
+ "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.10.4"
+ }
+ },
+ "@babel/plugin-syntax-object-rest-spread": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz",
+ "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-optional-catch-binding": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz",
+ "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-optional-chaining": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz",
+ "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.8.0"
+ }
+ },
+ "@babel/plugin-syntax-top-level-await": {
+ "version": "7.14.5",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz",
+ "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.14.5"
+ }
+ },
+ "@babel/plugin-syntax-typescript": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz",
+ "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.18.6"
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.15.4",
+ "dev": true,
+ "requires": {
+ "regenerator-runtime": "^0.13.4"
+ }
+ },
+ "@babel/template": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.6.tgz",
+ "integrity": "sha512-JoDWzPe+wgBsTTgdnIma3iHNFC7YVJoPssVBDjiHfNlyt4YcunDtcDOUmfVDfCK5MfdsaIoX9PkijPhjH3nYUw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/parser": "^7.18.6",
+ "@babel/types": "^7.18.6"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/highlight": "^7.18.6"
+ }
+ }
+ }
+ },
+ "@babel/traverse": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.8.tgz",
+ "integrity": "sha512-UNg/AcSySJYR/+mIcJQDCv00T+AqRO7j/ZEJLzpaYtgM48rMg5MnkJgyNqkzo88+p4tfRvZJCEiwwfG6h4jkRg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/code-frame": "^7.18.6",
+ "@babel/generator": "^7.18.7",
+ "@babel/helper-environment-visitor": "^7.18.6",
+ "@babel/helper-function-name": "^7.18.6",
+ "@babel/helper-hoist-variables": "^7.18.6",
+ "@babel/helper-split-export-declaration": "^7.18.6",
+ "@babel/parser": "^7.18.8",
+ "@babel/types": "^7.18.8",
+ "debug": "^4.1.0",
+ "globals": "^11.1.0"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/highlight": "^7.18.6"
+ }
+ },
+ "globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "@babel/types": {
+ "version": "7.18.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.8.tgz",
+ "integrity": "sha512-qwpdsmraq0aJ3osLJRApsc2ouSJCdnMeZwB0DhbtHAtRpZNZCdlbRnHIgcRKzdE1g0iOGg644fzjOBcdOz9cPw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-validator-identifier": "^7.18.6",
+ "to-fast-properties": "^2.0.0"
+ }
+ },
+ "@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "dev": true,
+ "peer": true
+ },
+ "@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "dev": true,
+ "requires": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ }
+ },
+ "@emotion/hash": {
+ "version": "0.8.0",
+ "dev": true
+ },
+ "@eslint/eslintrc": {
+ "version": "0.4.3",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.1.1",
+ "espree": "^7.3.0",
+ "globals": "^13.9.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "strip-json-comments": "^3.1.1"
+ },
+ "dependencies": {
+ "ignore": {
+ "version": "4.0.6",
+ "dev": true
+ }
+ }
+ },
+ "@grpc/grpc-js": {
+ "version": "1.4.1",
+ "dev": true,
+ "requires": {
+ "@grpc/proto-loader": "^0.6.4",
+ "@types/node": ">=12.12.47"
+ }
+ },
+ "@grpc/proto-loader": {
+ "version": "0.6.6",
+ "dev": true,
+ "requires": {
+ "@types/long": "^4.0.1",
+ "lodash.camelcase": "^4.3.0",
+ "long": "^4.0.0",
+ "protobufjs": "^6.10.0",
+ "yargs": "^16.1.1"
+ },
+ "dependencies": {
+ "long": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
+ "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
+ "dev": true
+ },
+ "protobufjs": {
+ "version": "6.11.2",
+ "dev": true,
+ "requires": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.4",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.0",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.0",
+ "@types/long": "^4.0.1",
+ "@types/node": ">=13.7.0",
+ "long": "^4.0.0"
+ }
+ }
+ }
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.5.0",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "@humanwhocodes/object-schema": {
+ "version": "1.2.0",
+ "dev": true
+ },
+ "@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "@istanbuljs/schema": {
+ "version": "0.1.3",
+ "dev": true,
+ "peer": true
+ },
+ "@jest/console": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/@jest/console/-/console-28.1.1.tgz",
+ "integrity": "sha512-0RiUocPVFEm3WRMOStIHbRWllG6iW6E3/gUPnf4lkrVFyXIIDeCe+vlKeYyFOMhB2EPE6FLFCNADSOOQMaqvyA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "jest-message-util": "^28.1.1",
+ "jest-util": "^28.1.1",
+ "slash": "^3.0.0"
+ }
+ },
+ "@jest/core": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/core/-/core-28.1.2.tgz",
+ "integrity": "sha512-Xo4E+Sb/nZODMGOPt2G3cMmCBqL4/W2Ijwr7/mrXlq4jdJwcFQ/9KrrJZT2adQRk2otVBXXOz1GRQ4Z5iOgvRQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/console": "^28.1.1",
+ "@jest/reporters": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "jest-changed-files": "^28.0.2",
+ "jest-config": "^28.1.2",
+ "jest-haste-map": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.1",
+ "jest-resolve-dependencies": "^28.1.2",
+ "jest-runner": "^28.1.2",
+ "jest-runtime": "^28.1.2",
+ "jest-snapshot": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "jest-validate": "^28.1.1",
+ "jest-watcher": "^28.1.1",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^28.1.1",
+ "rimraf": "^3.0.0",
+ "slash": "^3.0.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ci-info": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
+ "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "@jest/environment": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz",
+ "integrity": "sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA==",
+ "dev": true,
+ "requires": {
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "jest-mock": "^28.1.3"
+ }
+ },
+ "@jest/expect": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-28.1.2.tgz",
+ "integrity": "sha512-HBzyZBeFBiOelNbBKN0pilWbbrGvwDUwAqMC46NVJmWm8AVkuE58NbG1s7DR4cxFt4U5cVLxofAoHxgvC5MyOw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "expect": "^28.1.1",
+ "jest-snapshot": "^28.1.2"
+ }
+ },
+ "@jest/expect-utils": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.1.tgz",
+ "integrity": "sha512-n/ghlvdhCdMI/hTcnn4qV57kQuV9OTsZzH1TTCVARANKhl6hXJqLKUkwX69ftMGpsbpt96SsDD8n8LD2d9+FRw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "jest-get-type": "^28.0.2"
+ }
+ },
+ "@jest/fake-timers": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz",
+ "integrity": "sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw==",
+ "dev": true,
+ "requires": {
+ "@jest/types": "^28.1.3",
+ "@sinonjs/fake-timers": "^9.1.2",
+ "@types/node": "*",
+ "jest-message-util": "^28.1.3",
+ "jest-mock": "^28.1.3",
+ "jest-util": "^28.1.3"
+ }
+ },
+ "@jest/globals": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-28.1.2.tgz",
+ "integrity": "sha512-cz0lkJVDOtDaYhvT3Fv2U1B6FtBnV+OpEyJCzTHM1fdoTsU4QNLAt/H4RkiwEUU+dL4g/MFsoTuHeT2pvbo4Hg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/environment": "^28.1.2",
+ "@jest/expect": "^28.1.2",
+ "@jest/types": "^28.1.1"
+ }
+ },
+ "@jest/reporters": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.2.tgz",
+ "integrity": "sha512-/whGLhiwAqeCTmQEouSigUZJPVl7sW8V26EiboImL+UyXznnr1a03/YZ2BX8OlFw0n+Zlwu+EZAITZtaeRTxyA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@jest/console": "^28.1.1",
+ "@jest/test-result": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "exit": "^0.1.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-instrument": "^5.1.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.1.3",
+ "jest-message-util": "^28.1.1",
+ "jest-util": "^28.1.1",
+ "jest-worker": "^28.1.1",
+ "slash": "^3.0.0",
+ "string-length": "^4.0.1",
+ "strip-ansi": "^6.0.0",
+ "terminal-link": "^2.0.0",
+ "v8-to-istanbul": "^9.0.1"
+ },
+ "dependencies": {
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
+ "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "v8-to-istanbul": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz",
+ "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^1.6.0"
+ }
+ }
+ }
+ },
+ "@jest/schemas": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz",
+ "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==",
+ "dev": true,
+ "requires": {
+ "@sinclair/typebox": "^0.24.1"
+ }
+ },
+ "@jest/source-map": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz",
+ "integrity": "sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "callsites": "^3.0.0",
+ "graceful-fs": "^4.2.9"
+ },
+ "dependencies": {
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
+ "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ }
+ }
+ },
+ "@jest/test-result": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.1.tgz",
+ "integrity": "sha512-hPmkugBktqL6rRzwWAtp1JtYT4VHwv8OQ+9lE5Gymj6dHzubI/oJHMUpPOt8NrdVWSrz9S7bHjJUmv2ggFoUNQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/console": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "collect-v8-coverage": "^1.0.0"
+ }
+ },
+ "@jest/test-sequencer": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.1.tgz",
+ "integrity": "sha512-nuL+dNSVMcWB7OOtgb0EGH5AjO4UBCt68SLP08rwmC+iRhyuJWS9MtZ/MpipxFwKAlHFftbMsydXqWre8B0+XA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/test-result": "^28.1.1",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.1",
+ "slash": "^3.0.0"
+ }
+ },
+ "@jest/transform": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz",
+ "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/core": "^7.11.6",
+ "@jest/types": "^28.1.3",
+ "@jridgewell/trace-mapping": "^0.3.13",
+ "babel-plugin-istanbul": "^6.1.1",
+ "chalk": "^4.0.0",
+ "convert-source-map": "^1.4.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.3",
+ "jest-regex-util": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "pirates": "^4.0.4",
+ "slash": "^3.0.0",
+ "write-file-atomic": "^4.0.1"
+ },
+ "dependencies": {
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.14",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz",
+ "integrity": "sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "write-file-atomic": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz",
+ "integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.7"
+ }
+ }
+ }
+ },
+ "@jest/types": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz",
+ "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==",
+ "dev": true,
+ "requires": {
+ "@jest/schemas": "^28.1.3",
+ "@types/istanbul-lib-coverage": "^2.0.0",
+ "@types/istanbul-reports": "^3.0.0",
+ "@types/node": "*",
+ "@types/yargs": "^17.0.8",
+ "chalk": "^4.0.0"
+ }
+ },
+ "@jridgewell/gen-mapping": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz",
+ "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jridgewell/set-array": "^1.0.0",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "@jridgewell/resolve-uri": {
+ "version": "3.0.8",
+ "dev": true
+ },
+ "@jridgewell/set-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
+ "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
+ "dev": true,
+ "peer": true
+ },
+ "@jridgewell/sourcemap-codec": {
+ "version": "1.4.14",
+ "dev": true
+ },
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "dev": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "@mapbox/node-pre-gyp": {
+ "version": "1.0.6",
+ "dev": true,
+ "requires": {
+ "detect-libc": "^1.0.3",
+ "https-proxy-agent": "^5.0.0",
+ "make-dir": "^3.1.0",
+ "node-fetch": "^2.6.5",
+ "nopt": "^5.0.0",
+ "npmlog": "^5.0.1",
+ "rimraf": "^3.0.2",
+ "semver": "^7.3.5",
+ "tar": "^6.1.11"
+ }
+ },
+ "@material-ui/core": {
+ "version": "4.12.3",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.4.4",
+ "@material-ui/styles": "^4.11.4",
+ "@material-ui/system": "^4.12.1",
+ "@material-ui/types": "5.1.0",
+ "@material-ui/utils": "^4.11.2",
+ "@types/react-transition-group": "^4.2.0",
+ "clsx": "^1.0.4",
+ "hoist-non-react-statics": "^3.3.2",
+ "popper.js": "1.16.1-lts",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.8.0 || ^17.0.0",
+ "react-transition-group": "^4.4.0"
+ }
+ },
+ "@material-ui/lab": {
+ "version": "4.0.0-alpha.60",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.4.4",
+ "@material-ui/utils": "^4.11.2",
+ "clsx": "^1.0.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.8.0 || ^17.0.0"
+ }
+ },
+ "@material-ui/styles": {
+ "version": "4.11.4",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.4.4",
+ "@emotion/hash": "^0.8.0",
+ "@material-ui/types": "5.1.0",
+ "@material-ui/utils": "^4.11.2",
+ "clsx": "^1.0.4",
+ "csstype": "^2.5.2",
+ "hoist-non-react-statics": "^3.3.2",
+ "jss": "^10.5.1",
+ "jss-plugin-camel-case": "^10.5.1",
+ "jss-plugin-default-unit": "^10.5.1",
+ "jss-plugin-global": "^10.5.1",
+ "jss-plugin-nested": "^10.5.1",
+ "jss-plugin-props-sort": "^10.5.1",
+ "jss-plugin-rule-value-function": "^10.5.1",
+ "jss-plugin-vendor-prefixer": "^10.5.1",
+ "prop-types": "^15.7.2"
+ },
+ "dependencies": {
+ "csstype": {
+ "version": "2.6.18",
+ "dev": true
+ }
+ }
+ },
+ "@material-ui/system": {
+ "version": "4.12.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.4.4",
+ "@material-ui/utils": "^4.11.2",
+ "csstype": "^2.5.2",
+ "prop-types": "^15.7.2"
+ },
+ "dependencies": {
+ "csstype": {
+ "version": "2.6.18",
+ "dev": true
+ }
+ }
+ },
+ "@material-ui/types": {
+ "version": "5.1.0",
+ "dev": true,
+ "requires": {}
+ },
+ "@material-ui/utils": {
+ "version": "4.11.2",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.4.4",
+ "prop-types": "^15.7.2",
+ "react-is": "^16.8.0 || ^17.0.0"
+ }
+ },
+ "@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ }
+ },
+ "@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "dev": true
+ },
+ "@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ }
+ },
+ "@protobuf-ts/protoc": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@protobuf-ts/protoc/-/protoc-2.7.0.tgz",
+ "integrity": "sha512-YW61nbX9d3mWF44193S+bmWth5eFHxvrePQMUfdY8eEa3PTmhAUwgVCUBeCXEVUWgz1H/E0CnwdjlJgW4vQtOg=="
+ },
+ "@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "dev": true
+ },
+ "@protobufjs/base64": {
+ "version": "1.1.2",
+ "dev": true
+ },
+ "@protobufjs/codegen": {
+ "version": "2.0.4",
+ "dev": true
+ },
+ "@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "@protobufjs/fetch": {
+ "version": "1.1.0",
+ "dev": true,
+ "requires": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "@protobufjs/float": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "@protobufjs/inquire": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "@protobufjs/path": {
+ "version": "1.1.2",
+ "dev": true
+ },
+ "@protobufjs/pool": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "@protobufjs/utf8": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "@rollup/plugin-commonjs": {
+ "version": "19.0.2",
+ "dev": true,
+ "requires": {
+ "@rollup/pluginutils": "^3.1.0",
+ "commondir": "^1.0.1",
+ "estree-walker": "^2.0.1",
+ "glob": "^7.1.6",
+ "is-reference": "^1.2.1",
+ "magic-string": "^0.25.7",
+ "resolve": "^1.17.0"
+ },
+ "dependencies": {
+ "estree-walker": {
+ "version": "2.0.2",
+ "dev": true
+ },
+ "magic-string": {
+ "version": "0.25.7",
+ "dev": true,
+ "requires": {
+ "sourcemap-codec": "^1.4.4"
+ }
+ }
+ }
+ },
+ "@rollup/plugin-node-resolve": {
+ "version": "13.3.0",
+ "dev": true,
+ "requires": {
+ "@rollup/pluginutils": "^3.1.0",
+ "@types/resolve": "1.17.1",
+ "deepmerge": "^4.2.2",
+ "is-builtin-module": "^3.1.0",
+ "is-module": "^1.0.0",
+ "resolve": "^1.19.0"
+ }
+ },
+ "@rollup/plugin-typescript": {
+ "version": "8.3.3",
+ "dev": true,
+ "requires": {
+ "@rollup/pluginutils": "^3.1.0",
+ "resolve": "^1.17.0"
+ }
+ },
+ "@rollup/pluginutils": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "@types/estree": "0.0.39",
+ "estree-walker": "^1.0.1",
+ "picomatch": "^2.2.2"
+ },
+ "dependencies": {
+ "@types/estree": {
+ "version": "0.0.39",
+ "dev": true
+ },
+ "estree-walker": {
+ "version": "1.0.1",
+ "dev": true
+ }
+ }
+ },
+ "@sinclair/typebox": {
+ "version": "0.24.19",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.19.tgz",
+ "integrity": "sha512-gHJu8cdYTD5p4UqmQHrxaWrtb/jkH5imLXzuBypWhKzNkW0qfmgz+w1xaJccWVuJta1YYUdlDiPHXRTR4Ku0MQ==",
+ "dev": true
+ },
+ "@sindresorhus/is": {
+ "version": "0.14.0",
+ "dev": true
+ },
+ "@sinonjs/commons": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz",
+ "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==",
+ "dev": true,
+ "requires": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "@sinonjs/fake-timers": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz",
+ "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==",
+ "dev": true,
+ "requires": {
+ "@sinonjs/commons": "^1.7.0"
+ }
+ },
+ "@szmarczak/http-timer": {
+ "version": "1.1.2",
+ "dev": true,
+ "requires": {
+ "defer-to-connect": "^1.0.1"
+ }
+ },
+ "@tootallnate/once": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
+ "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==",
+ "dev": true
+ },
+ "@tsconfig/node10": {
+ "version": "1.0.9",
+ "dev": true
+ },
+ "@tsconfig/node12": {
+ "version": "1.0.11",
+ "dev": true
+ },
+ "@tsconfig/node14": {
+ "version": "1.0.3",
+ "dev": true
+ },
+ "@tsconfig/node16": {
+ "version": "1.0.3",
+ "dev": true
+ },
+ "@types/babel__core": {
+ "version": "7.1.19",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz",
+ "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "@types/babel__generator": {
+ "version": "7.6.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
+ "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "@types/babel__template": {
+ "version": "7.4.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
+ "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "@types/babel__traverse": {
+ "version": "7.17.1",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.17.1.tgz",
+ "integrity": "sha512-kVzjari1s2YVi77D3w1yuvohV2idweYXMCDzqBiVNN63TcDWrIlTVOYpqVrvbbyOE/IyzBoTKF0fdnLPEORFxA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/types": "^7.3.0"
+ }
+ },
+ "@types/crc": {
+ "version": "3.4.0",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/estree": {
+ "version": "0.0.50",
+ "dev": true
+ },
+ "@types/google-protobuf": {
+ "version": "3.15.5",
+ "dev": true
+ },
+ "@types/graceful-fs": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
+ "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/istanbul-lib-coverage": {
+ "version": "2.0.3",
+ "dev": true
+ },
+ "@types/istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==",
+ "dev": true,
+ "requires": {
+ "@types/istanbul-lib-coverage": "*"
+ }
+ },
+ "@types/istanbul-reports": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz",
+ "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==",
+ "dev": true,
+ "requires": {
+ "@types/istanbul-lib-report": "*"
+ }
+ },
+ "@types/jest": {
+ "version": "28.1.4",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.4.tgz",
+ "integrity": "sha512-telv6G5N7zRJiLcI3Rs3o+ipZ28EnE+7EvF0pSrt2pZOMnAVI/f+6/LucDxOvcBcTeTL3JMF744BbVQAVBUQRA==",
+ "dev": true,
+ "requires": {
+ "jest-matcher-utils": "^28.0.0",
+ "pretty-format": "^28.0.0"
+ }
+ },
+ "@types/jsdom": {
+ "version": "16.2.14",
+ "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-16.2.14.tgz",
+ "integrity": "sha512-6BAy1xXEmMuHeAJ4Fv4yXKwBDTGTOseExKE3OaHiNycdHdZw59KfYzrt0DkDluvwmik1HRt6QS7bImxUmpSy+w==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/parse5": "*",
+ "@types/tough-cookie": "*"
+ }
+ },
+ "@types/json-schema": {
+ "version": "7.0.9",
+ "dev": true
+ },
+ "@types/long": {
+ "version": "4.0.1",
+ "dev": true
+ },
+ "@types/minimist": {
+ "version": "1.2.2",
+ "dev": true
+ },
+ "@types/node": {
+ "version": "16.11.4",
+ "dev": true
+ },
+ "@types/normalize-package-data": {
+ "version": "2.4.1",
+ "dev": true
+ },
+ "@types/parse5": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz",
+ "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==",
+ "dev": true
+ },
+ "@types/prettier": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.6.3.tgz",
+ "integrity": "sha512-ymZk3LEC/fsut+/Q5qejp6R9O1rMxz3XaRHDV6kX8MrGAhOSPqVARbDi+EZvInBpw+BnCX3TD240byVkOfQsHg==",
+ "dev": true,
+ "peer": true
+ },
+ "@types/prop-types": {
+ "version": "15.7.4",
+ "dev": true
+ },
+ "@types/react": {
+ "version": "17.0.31",
+ "dev": true,
+ "requires": {
+ "@types/prop-types": "*",
+ "@types/scheduler": "*",
+ "csstype": "^3.0.2"
+ }
+ },
+ "@types/react-dom": {
+ "version": "17.0.10",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/react-transition-group": {
+ "version": "4.4.4",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
+ "@types/resolve": {
+ "version": "1.17.1",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/scheduler": {
+ "version": "0.16.2",
+ "dev": true
+ },
+ "@types/stack-utils": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
+ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
+ "dev": true
+ },
+ "@types/tough-cookie": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz",
+ "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
+ "dev": true
+ },
+ "@types/yargs": {
+ "version": "17.0.10",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.10.tgz",
+ "integrity": "sha512-gmEaFwpj/7f/ROdtIlci1R1VYU1J4j95m8T+Tj3iBgiBFKg1foE/PSl93bBd5T9LDXNPo8UlNN6W0qwD8O5OaA==",
+ "dev": true,
+ "requires": {
+ "@types/yargs-parser": "*"
+ }
+ },
+ "@types/yargs-parser": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz",
+ "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
+ "dev": true
+ },
+ "@typescript-eslint/eslint-plugin": {
+ "version": "4.33.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/experimental-utils": "4.33.0",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "debug": "^4.3.1",
+ "functional-red-black-tree": "^1.0.1",
+ "ignore": "^5.1.8",
+ "regexpp": "^3.1.0",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/experimental-utils": {
+ "version": "4.33.0",
+ "dev": true,
+ "requires": {
+ "@types/json-schema": "^7.0.7",
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^3.0.0"
+ },
+ "dependencies": {
+ "eslint-utils": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ }
+ }
+ },
+ "@typescript-eslint/parser": {
+ "version": "4.33.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/scope-manager": "4.33.0",
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/typescript-estree": "4.33.0",
+ "debug": "^4.3.1"
+ }
+ },
+ "@typescript-eslint/scope-manager": {
+ "version": "4.33.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0"
+ }
+ },
+ "@typescript-eslint/types": {
+ "version": "4.33.0",
+ "dev": true
+ },
+ "@typescript-eslint/typescript-estree": {
+ "version": "4.33.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "@typescript-eslint/visitor-keys": "4.33.0",
+ "debug": "^4.3.1",
+ "globby": "^11.0.3",
+ "is-glob": "^4.0.1",
+ "semver": "^7.3.5",
+ "tsutils": "^3.21.0"
+ }
+ },
+ "@typescript-eslint/visitor-keys": {
+ "version": "4.33.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/types": "4.33.0",
+ "eslint-visitor-keys": "^2.0.0"
+ }
+ },
+ "abab": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
+ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+ "dev": true
+ },
+ "abbrev": {
+ "version": "1.1.1",
+ "dev": true
+ },
+ "abstract-leveldown": {
+ "version": "0.12.4",
+ "dev": true,
+ "requires": {
+ "xtend": "~3.0.0"
+ }
+ },
+ "acorn": {
+ "version": "7.4.1",
+ "dev": true
+ },
+ "acorn-globals": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz",
+ "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.1.1",
+ "acorn-walk": "^7.1.1"
+ },
+ "dependencies": {
+ "acorn-walk": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
+ "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
+ "dev": true
+ }
+ }
+ },
+ "acorn-jsx": {
+ "version": "5.3.2",
+ "dev": true,
+ "requires": {}
+ },
+ "acorn-walk": {
+ "version": "8.2.0",
+ "dev": true
+ },
+ "agent-base": {
+ "version": "6.0.2",
+ "dev": true,
+ "requires": {
+ "debug": "4"
+ }
+ },
+ "ajv": {
+ "version": "6.12.6",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "ansi_up": {
+ "version": "5.1.0",
+ "dev": true
+ },
+ "ansi-align": {
+ "version": "3.0.1",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.1.0"
+ }
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "dev": true
+ },
+ "ansi-escapes": {
+ "version": "4.3.2",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.21.3"
+ },
+ "dependencies": {
+ "type-fest": {
+ "version": "0.21.3",
+ "dev": true
+ }
+ }
+ },
+ "ansi-regex": {
+ "version": "5.0.1",
+ "dev": true
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "dev": true,
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "aproba": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "are-we-there-yet": {
+ "version": "2.0.0",
+ "dev": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^3.6.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ }
+ }
+ },
+ "arg": {
+ "version": "5.0.2",
+ "dev": true
+ },
+ "array-includes": {
+ "version": "3.1.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "is-string": "^1.0.7"
+ }
+ },
+ "array-union": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "array.prototype.flatmap": {
+ "version": "1.2.5",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.0"
+ }
+ },
+ "arrify": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "asn1.js": {
+ "version": "5.4.1",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "astral-regex": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "async": {
+ "version": "2.6.3",
+ "dev": true,
+ "requires": {
+ "lodash": "^4.17.14"
+ }
+ },
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "atob": {
+ "version": "2.1.2",
+ "dev": true
+ },
+ "babel-jest": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz",
+ "integrity": "sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/transform": "^28.1.3",
+ "@types/babel__core": "^7.1.14",
+ "babel-plugin-istanbul": "^6.1.1",
+ "babel-preset-jest": "^28.1.3",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "slash": "^3.0.0"
+ }
+ },
+ "babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ }
+ },
+ "babel-plugin-jest-hoist": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz",
+ "integrity": "sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/template": "^7.3.3",
+ "@babel/types": "^7.3.3",
+ "@types/babel__core": "^7.1.14",
+ "@types/babel__traverse": "^7.0.6"
+ }
+ },
+ "babel-preset-current-node-syntax": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz",
+ "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/plugin-syntax-async-generators": "^7.8.4",
+ "@babel/plugin-syntax-bigint": "^7.8.3",
+ "@babel/plugin-syntax-class-properties": "^7.8.3",
+ "@babel/plugin-syntax-import-meta": "^7.8.3",
+ "@babel/plugin-syntax-json-strings": "^7.8.3",
+ "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3",
+ "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3",
+ "@babel/plugin-syntax-numeric-separator": "^7.8.3",
+ "@babel/plugin-syntax-object-rest-spread": "^7.8.3",
+ "@babel/plugin-syntax-optional-catch-binding": "^7.8.3",
+ "@babel/plugin-syntax-optional-chaining": "^7.8.3",
+ "@babel/plugin-syntax-top-level-await": "^7.8.3"
+ }
+ },
+ "babel-preset-jest": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz",
+ "integrity": "sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "babel-plugin-jest-hoist": "^28.1.3",
+ "babel-preset-current-node-syntax": "^1.0.0"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "base64-js": {
+ "version": "1.5.1",
+ "dev": true
+ },
+ "basic-auth": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "bl": {
+ "version": "0.8.2",
+ "dev": true,
+ "requires": {
+ "readable-stream": "~1.0.26"
+ }
+ },
+ "bn.js": {
+ "version": "4.12.0",
+ "dev": true
+ },
+ "boxen": {
+ "version": "5.1.2",
+ "dev": true,
+ "requires": {
+ "ansi-align": "^3.0.0",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.1.0",
+ "cli-boxes": "^2.2.1",
+ "string-width": "^4.2.2",
+ "type-fest": "^0.20.2",
+ "widest-line": "^3.1.0",
+ "wrap-ansi": "^7.0.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "6.2.0",
+ "dev": true
+ }
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "dev": true,
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "brorand": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "browser-process-hrtime": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
+ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
+ "dev": true
+ },
+ "browserify-aes": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "browserify-cipher": {
+ "version": "1.0.1",
+ "dev": true,
+ "requires": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "browserify-des": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "browserify-fs": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "level-filesystem": "^1.0.1",
+ "level-js": "^2.1.3",
+ "levelup": "^0.18.2"
+ }
+ },
+ "browserify-rsa": {
+ "version": "4.1.0",
+ "dev": true,
+ "requires": {
+ "bn.js": "^5.0.0",
+ "randombytes": "^2.0.1"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "5.2.0",
+ "dev": true
+ }
+ }
+ },
+ "browserify-sign": {
+ "version": "4.2.1",
+ "dev": true,
+ "requires": {
+ "bn.js": "^5.1.1",
+ "browserify-rsa": "^4.0.1",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.5.3",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.5",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "5.2.0",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "3.6.0",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ }
+ }
+ },
+ "browserslist": {
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.1.tgz",
+ "integrity": "sha512-Nq8MFCSrnJXSc88yliwlzQe3qNe3VntIjhsArW9IJOEPSHNx23FalwApUVbzAWABLhYJJ7y8AynWI/XM8OdfjQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "caniuse-lite": "^1.0.30001359",
+ "electron-to-chromium": "^1.4.172",
+ "node-releases": "^2.0.5",
+ "update-browserslist-db": "^1.0.4"
+ }
+ },
+ "bs-logger": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
+ "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
+ "dev": true,
+ "requires": {
+ "fast-json-stable-stringify": "2.x"
+ }
+ },
+ "bser": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
+ "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "node-int64": "^0.4.0"
+ }
+ },
+ "buffer": {
+ "version": "6.0.3",
+ "dev": true,
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "buffer-es6": {
+ "version": "4.9.3",
+ "dev": true
+ },
+ "buffer-from": {
+ "version": "1.1.2",
+ "dev": true
+ },
+ "buffer-xor": {
+ "version": "1.0.3",
+ "dev": true
+ },
+ "builtin-modules": {
+ "version": "3.3.0",
+ "dev": true
+ },
+ "cacheable-request": {
+ "version": "6.1.0",
+ "dev": true,
+ "requires": {
+ "clone-response": "^1.0.2",
+ "get-stream": "^5.1.0",
+ "http-cache-semantics": "^4.0.0",
+ "keyv": "^3.0.0",
+ "lowercase-keys": "^2.0.0",
+ "normalize-url": "^4.1.0",
+ "responselike": "^1.0.2"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "5.2.0",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "2.0.0",
+ "dev": true
+ }
+ }
+ },
+ "call-bind": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.0.2"
+ }
+ },
+ "callsites": {
+ "version": "3.1.0",
+ "dev": true
+ },
+ "camelcase": {
+ "version": "5.3.1",
+ "dev": true
+ },
+ "camelcase-keys": {
+ "version": "6.2.2",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.3.1",
+ "map-obj": "^4.0.0",
+ "quick-lru": "^4.0.1"
+ },
+ "dependencies": {
+ "map-obj": {
+ "version": "4.3.0",
+ "dev": true
+ }
+ }
+ },
+ "caniuse-lite": {
+ "version": "1.0.30001365",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001365.tgz",
+ "integrity": "sha512-VDQZ8OtpuIPMBA4YYvZXECtXbddMCUFJk1qu8Mqxfm/SZJNSr1cy4IuLCOL7RJ/YASrvJcYg1Zh+UEUQ5m6z8Q==",
+ "dev": true,
+ "peer": true
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "char-regex": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz",
+ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==",
+ "dev": true,
+ "peer": true
+ },
+ "chardet": {
+ "version": "0.7.0",
+ "dev": true
+ },
+ "chownr": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "ci-info": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "cipher-base": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "cjs-module-lexer": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz",
+ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
+ "dev": true,
+ "peer": true
+ },
+ "cli-boxes": {
+ "version": "2.2.1",
+ "dev": true
+ },
+ "cli-cursor": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "restore-cursor": "^3.1.0"
+ }
+ },
+ "cli-width": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "cliui": {
+ "version": "7.0.4",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "clone": {
+ "version": "0.1.19",
+ "dev": true
+ },
+ "clone-response": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "clsx": {
+ "version": "1.1.1",
+ "dev": true
+ },
+ "co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==",
+ "dev": true,
+ "peer": true
+ },
+ "collect-v8-coverage": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz",
+ "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==",
+ "dev": true,
+ "peer": true
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "dev": true,
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "dev": true
+ },
+ "color-support": {
+ "version": "1.1.3",
+ "dev": true
+ },
+ "colors": {
+ "version": "1.4.0",
+ "dev": true
+ },
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
+ "commondir": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "dev": true
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "dev": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "2.3.7",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "dev": true
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "configstore": {
+ "version": "5.0.1",
+ "dev": true,
+ "requires": {
+ "dot-prop": "^5.2.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^3.0.0",
+ "unique-string": "^2.0.0",
+ "write-file-atomic": "^3.0.0",
+ "xdg-basedir": "^4.0.0"
+ }
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "convert-source-map": {
+ "version": "1.8.0",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "safe-buffer": "~5.1.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.3",
+ "dev": true
+ },
+ "corser": {
+ "version": "2.0.1",
+ "dev": true
+ },
+ "crc": {
+ "version": "4.1.1",
+ "dev": true,
+ "requires": {}
+ },
+ "create-ecdh": {
+ "version": "4.0.4",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ }
+ },
+ "create-hash": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "create-hmac": {
+ "version": "1.1.7",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "create-require": {
+ "version": "1.1.1",
+ "dev": true
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "crypto-browserify": {
+ "version": "3.12.0",
+ "dev": true,
+ "requires": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ }
+ },
+ "crypto-random-string": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "css-vendor": {
+ "version": "2.0.8",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.8.3",
+ "is-in-browser": "^1.0.2"
+ }
+ },
+ "cssom": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
+ "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==",
+ "dev": true
+ },
+ "cssstyle": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz",
+ "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==",
+ "dev": true,
+ "requires": {
+ "cssom": "~0.3.6"
+ },
+ "dependencies": {
+ "cssom": {
+ "version": "0.3.8",
+ "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
+ "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
+ "dev": true
+ }
+ }
+ },
+ "csstype": {
+ "version": "3.0.9",
+ "dev": true
+ },
+ "data-urls": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
+ "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==",
+ "dev": true,
+ "requires": {
+ "abab": "^2.0.6",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^11.0.0"
+ },
+ "dependencies": {
+ "tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.1"
+ }
+ },
+ "webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
+ "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
+ "dev": true,
+ "requires": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ }
+ }
+ }
+ },
+ "debug": {
+ "version": "4.3.2",
+ "dev": true,
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "dev": true
+ },
+ "decamelize-keys": {
+ "version": "1.1.0",
+ "dev": true,
+ "requires": {
+ "decamelize": "^1.1.0",
+ "map-obj": "^1.0.0"
+ }
+ },
+ "decimal.js": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz",
+ "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==",
+ "dev": true
+ },
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "dev": true
+ },
+ "decompress-response": {
+ "version": "3.3.0",
+ "dev": true,
+ "requires": {
+ "mimic-response": "^1.0.0"
+ }
+ },
+ "dedent": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
+ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
+ "dev": true,
+ "peer": true
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "dev": true
+ },
+ "deep-is": {
+ "version": "0.1.4",
+ "dev": true
+ },
+ "deepmerge": {
+ "version": "4.2.2",
+ "dev": true
+ },
+ "defer-to-connect": {
+ "version": "1.1.3",
+ "dev": true
+ },
+ "deferred-leveldown": {
+ "version": "0.2.0",
+ "dev": true,
+ "requires": {
+ "abstract-leveldown": "~0.12.1"
+ }
+ },
+ "define-properties": {
+ "version": "1.1.3",
+ "dev": true,
+ "requires": {
+ "object-keys": "^1.0.12"
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "des.js": {
+ "version": "1.0.1",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "dev": true
+ },
+ "detect-newline": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
+ "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
+ "dev": true,
+ "peer": true
+ },
+ "diff": {
+ "version": "4.0.2",
+ "dev": true
+ },
+ "diff-sequences": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz",
+ "integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==",
+ "dev": true
+ },
+ "diffie-hellman": {
+ "version": "5.0.3",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "dir-glob": {
+ "version": "3.0.1",
+ "dev": true,
+ "requires": {
+ "path-type": "^4.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "dom-helpers": {
+ "version": "5.2.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
+ "dom-serializer": {
+ "version": "1.3.2",
+ "dev": true,
+ "requires": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.2.0",
+ "entities": "^2.0.0"
+ }
+ },
+ "domelementtype": {
+ "version": "2.2.0",
+ "dev": true
+ },
+ "domexception": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
+ "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==",
+ "dev": true,
+ "requires": {
+ "webidl-conversions": "^7.0.0"
+ },
+ "dependencies": {
+ "webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true
+ }
+ }
+ },
+ "domhandler": {
+ "version": "4.2.2",
+ "dev": true,
+ "requires": {
+ "domelementtype": "^2.2.0"
+ }
+ },
+ "domutils": {
+ "version": "2.8.0",
+ "dev": true,
+ "requires": {
+ "dom-serializer": "^1.0.1",
+ "domelementtype": "^2.2.0",
+ "domhandler": "^4.2.0"
+ }
+ },
+ "dot-prop": {
+ "version": "5.3.0",
+ "dev": true,
+ "requires": {
+ "is-obj": "^2.0.0"
+ }
+ },
+ "duplexer": {
+ "version": "0.1.2",
+ "dev": true
+ },
+ "duplexer3": {
+ "version": "0.1.4",
+ "dev": true
+ },
+ "electron-to-chromium": {
+ "version": "1.4.186",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.186.tgz",
+ "integrity": "sha512-YoVeFrGd/7ROjz4R9uPoND1K/hSRC/xADy9639ZmIZeJSaBnKdYx3I6LMPsY7CXLpK7JFgKQVzeZ/dk2br6Eaw==",
+ "dev": true,
+ "peer": true
+ },
+ "elliptic": {
+ "version": "6.5.4",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.11.9",
+ "brorand": "^1.1.0",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.1",
+ "inherits": "^2.0.4",
+ "minimalistic-assert": "^1.0.1",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "emittery": {
+ "version": "0.10.2",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz",
+ "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==",
+ "dev": true,
+ "peer": true
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "dev": true
+ },
+ "end-of-stream": {
+ "version": "1.4.4",
+ "dev": true,
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "enquirer": {
+ "version": "2.3.6",
+ "dev": true,
+ "requires": {
+ "ansi-colors": "^4.1.1"
+ }
+ },
+ "entities": {
+ "version": "2.2.0",
+ "dev": true
+ },
+ "errno": {
+ "version": "0.1.8",
+ "dev": true,
+ "requires": {
+ "prr": "~1.0.1"
+ },
+ "dependencies": {
+ "prr": {
+ "version": "1.0.1",
+ "dev": true
+ }
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "es-abstract": {
+ "version": "1.19.1",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "es-to-primitive": "^1.2.1",
+ "function-bind": "^1.1.1",
+ "get-intrinsic": "^1.1.1",
+ "get-symbol-description": "^1.0.0",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "is-callable": "^1.2.4",
+ "is-negative-zero": "^2.0.1",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.1",
+ "is-string": "^1.0.7",
+ "is-weakref": "^1.0.1",
+ "object-inspect": "^1.11.0",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.2",
+ "string.prototype.trimend": "^1.0.4",
+ "string.prototype.trimstart": "^1.0.4",
+ "unbox-primitive": "^1.0.1"
+ }
+ },
+ "es-to-primitive": {
+ "version": "1.2.1",
+ "dev": true,
+ "requires": {
+ "is-callable": "^1.1.4",
+ "is-date-object": "^1.0.1",
+ "is-symbol": "^1.0.2"
+ }
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "dev": true
+ },
+ "escape-goat": {
+ "version": "2.1.1",
+ "dev": true
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "dev": true
+ },
+ "escodegen": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
+ "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
+ "dev": true,
+ "requires": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "optionator": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+ "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+ "dev": true,
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.6",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "word-wrap": "~1.2.3"
+ }
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
+ "dev": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ }
+ }
+ },
+ "eslint": {
+ "version": "7.32.0",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "7.12.11",
+ "@eslint/eslintrc": "^0.4.3",
+ "@humanwhocodes/config-array": "^0.5.0",
+ "ajv": "^6.10.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.0.1",
+ "doctrine": "^3.0.0",
+ "enquirer": "^2.3.5",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^5.1.1",
+ "eslint-utils": "^2.1.0",
+ "eslint-visitor-keys": "^2.0.0",
+ "espree": "^7.3.1",
+ "esquery": "^1.4.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "functional-red-black-tree": "^1.0.1",
+ "glob-parent": "^5.1.2",
+ "globals": "^13.6.0",
+ "ignore": "^4.0.6",
+ "import-fresh": "^3.0.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "js-yaml": "^3.13.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.0.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.1",
+ "progress": "^2.0.0",
+ "regexpp": "^3.1.0",
+ "semver": "^7.2.1",
+ "strip-ansi": "^6.0.0",
+ "strip-json-comments": "^3.1.0",
+ "table": "^6.0.9",
+ "text-table": "^0.2.0",
+ "v8-compile-cache": "^2.0.3"
+ },
+ "dependencies": {
+ "doctrine": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "ignore": {
+ "version": "4.0.6",
+ "dev": true
+ }
+ }
+ },
+ "eslint-config-prettier": {
+ "version": "7.2.0",
+ "dev": true,
+ "requires": {}
+ },
+ "eslint-plugin-es": {
+ "version": "3.0.1",
+ "dev": true,
+ "requires": {
+ "eslint-utils": "^2.0.0",
+ "regexpp": "^3.0.0"
+ }
+ },
+ "eslint-plugin-node": {
+ "version": "11.1.0",
+ "dev": true,
+ "requires": {
+ "eslint-plugin-es": "^3.0.0",
+ "eslint-utils": "^2.0.0",
+ "ignore": "^5.1.1",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.10.1",
+ "semver": "^6.1.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "dev": true
+ }
+ }
+ },
+ "eslint-plugin-prettier": {
+ "version": "3.4.1",
+ "dev": true,
+ "requires": {
+ "prettier-linter-helpers": "^1.0.0"
+ }
+ },
+ "eslint-plugin-react": {
+ "version": "7.26.1",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.3",
+ "array.prototype.flatmap": "^1.2.4",
+ "doctrine": "^2.1.0",
+ "estraverse": "^5.2.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.0.4",
+ "object.entries": "^1.1.4",
+ "object.fromentries": "^2.0.4",
+ "object.hasown": "^1.0.0",
+ "object.values": "^1.1.4",
+ "prop-types": "^15.7.2",
+ "resolve": "^2.0.0-next.3",
+ "semver": "^6.3.0",
+ "string.prototype.matchall": "^4.0.5"
+ },
+ "dependencies": {
+ "resolve": {
+ "version": "2.0.0-next.3",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ }
+ },
+ "semver": {
+ "version": "6.3.0",
+ "dev": true
+ }
+ }
+ },
+ "eslint-scope": {
+ "version": "5.1.1",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^4.1.1"
+ },
+ "dependencies": {
+ "estraverse": {
+ "version": "4.3.0",
+ "dev": true
+ }
+ }
+ },
+ "eslint-utils": {
+ "version": "2.1.0",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^1.1.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "dev": true
+ }
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "espree": {
+ "version": "7.3.1",
+ "dev": true,
+ "requires": {
+ "acorn": "^7.4.0",
+ "acorn-jsx": "^5.3.1",
+ "eslint-visitor-keys": "^1.3.0"
+ },
+ "dependencies": {
+ "eslint-visitor-keys": {
+ "version": "1.3.0",
+ "dev": true
+ }
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "dev": true
+ },
+ "esquery": {
+ "version": "1.4.0",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.1.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.3.0",
+ "dev": true,
+ "requires": {
+ "estraverse": "^5.2.0"
+ }
+ },
+ "estraverse": {
+ "version": "5.2.0",
+ "dev": true
+ },
+ "estree-walker": {
+ "version": "0.5.2",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "dev": true
+ },
+ "event-stream": {
+ "version": "3.3.4",
+ "dev": true,
+ "requires": {
+ "duplexer": "~0.1.1",
+ "from": "~0",
+ "map-stream": "~0.1.0",
+ "pause-stream": "0.0.11",
+ "split": "0.3",
+ "stream-combiner": "~0.0.4",
+ "through": "~2.3.1"
+ }
+ },
+ "eventemitter3": {
+ "version": "4.0.7",
+ "dev": true
+ },
+ "evp_bytestokey": {
+ "version": "1.0.3",
+ "dev": true,
+ "requires": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "execa": {
+ "version": "5.1.1",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "get-stream": "^6.0.0",
+ "human-signals": "^2.1.0",
+ "is-stream": "^2.0.0",
+ "merge-stream": "^2.0.0",
+ "npm-run-path": "^4.0.1",
+ "onetime": "^5.1.2",
+ "signal-exit": "^3.0.3",
+ "strip-final-newline": "^2.0.0"
+ }
+ },
+ "executioner": {
+ "version": "2.0.1",
+ "dev": true,
+ "requires": {
+ "mixly": "^1.0.0"
+ }
+ },
+ "exit": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
+ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==",
+ "dev": true,
+ "peer": true
+ },
+ "expect": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.1.tgz",
+ "integrity": "sha512-/AANEwGL0tWBwzLNOvO0yUdy2D52jVdNXppOqswC49sxMN2cPWsGCQdzuIf9tj6hHoBQzNvx75JUYuQAckPo3w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/expect-utils": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "jest-matcher-utils": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-util": "^28.1.1"
+ }
+ },
+ "external-editor": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "chardet": "^0.7.0",
+ "iconv-lite": "^0.4.24",
+ "tmp": "^0.0.33"
+ },
+ "dependencies": {
+ "tmp": {
+ "version": "0.0.33",
+ "dev": true,
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ }
+ }
+ },
+ "fast-deep-equal": {
+ "version": "3.1.3",
+ "dev": true
+ },
+ "fast-diff": {
+ "version": "1.2.0",
+ "dev": true
+ },
+ "fast-glob": {
+ "version": "3.2.7",
+ "dev": true,
+ "requires": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ }
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "dev": true
+ },
+ "fastq": {
+ "version": "1.13.0",
+ "dev": true,
+ "requires": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "fb-watchman": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz",
+ "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "bser": "2.1.1"
+ }
+ },
+ "figures": {
+ "version": "3.2.0",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-entry-cache": {
+ "version": "6.0.1",
+ "dev": true,
+ "requires": {
+ "flat-cache": "^3.0.4"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "dev": true,
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "flat-cache": {
+ "version": "3.0.4",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.1.0",
+ "rimraf": "^3.0.2"
+ },
+ "dependencies": {
+ "flatted": {
+ "version": "3.2.2",
+ "dev": true
+ }
+ }
+ },
+ "follow-redirects": {
+ "version": "1.14.4",
+ "dev": true
+ },
+ "foreach": {
+ "version": "2.0.5",
+ "dev": true
+ },
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
+ "from": {
+ "version": "0.1.7",
+ "dev": true
+ },
+ "fs-minipass": {
+ "version": "2.1.0",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "dev": true,
+ "optional": true
+ },
+ "fulcon": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "dev": true
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "fwd-stream": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "readable-stream": "~1.0.26-4"
+ }
+ },
+ "gauge": {
+ "version": "3.0.1",
+ "dev": true,
+ "requires": {
+ "aproba": "^1.0.3 || ^2.0.0",
+ "color-support": "^1.1.2",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.1",
+ "object-assign": "^4.1.1",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1 || ^2.0.0",
+ "strip-ansi": "^3.0.1 || ^4.0.0",
+ "wide-align": "^1.1.2"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "dev": true,
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
+ "gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "peer": true
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "dev": true
+ },
+ "get-intrinsic": {
+ "version": "1.1.1",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has": "^1.0.3",
+ "has-symbols": "^1.0.1"
+ }
+ },
+ "get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "dev": true,
+ "peer": true
+ },
+ "get-stream": {
+ "version": "6.0.1",
+ "dev": true
+ },
+ "get-symbol-description": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.1.1"
+ }
+ },
+ "glob": {
+ "version": "7.2.0",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "global-dirs": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "ini": "2.0.0"
+ }
+ },
+ "globals": {
+ "version": "13.11.0",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "globby": {
+ "version": "11.0.4",
+ "dev": true,
+ "requires": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.1.1",
+ "ignore": "^5.1.4",
+ "merge2": "^1.3.0",
+ "slash": "^3.0.0"
+ }
+ },
+ "google-protobuf": {
+ "version": "3.19.0"
+ },
+ "got": {
+ "version": "9.6.0",
+ "dev": true,
+ "requires": {
+ "@sindresorhus/is": "^0.14.0",
+ "@szmarczak/http-timer": "^1.1.2",
+ "cacheable-request": "^6.0.0",
+ "decompress-response": "^3.3.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^4.1.0",
+ "lowercase-keys": "^1.0.1",
+ "mimic-response": "^1.0.1",
+ "p-cancelable": "^1.0.0",
+ "to-readable-stream": "^1.0.0",
+ "url-parse-lax": "^3.0.0"
+ },
+ "dependencies": {
+ "get-stream": {
+ "version": "4.1.0",
+ "dev": true,
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ }
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.10",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
+ "dev": true
+ },
+ "grpc-tools": {
+ "version": "1.11.2",
+ "dev": true,
+ "requires": {
+ "@mapbox/node-pre-gyp": "^1.0.5"
+ }
+ },
+ "grpc-web": {
+ "version": "1.3.0",
+ "dev": true
+ },
+ "gts": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "@typescript-eslint/eslint-plugin": "^4.2.0",
+ "@typescript-eslint/parser": "^4.2.0",
+ "chalk": "^4.1.0",
+ "eslint": "^7.10.0",
+ "eslint-config-prettier": "^7.0.0",
+ "eslint-plugin-node": "^11.1.0",
+ "eslint-plugin-prettier": "^3.1.4",
+ "execa": "^5.0.0",
+ "inquirer": "^7.3.3",
+ "json5": "^2.1.3",
+ "meow": "^9.0.0",
+ "ncp": "^2.0.0",
+ "prettier": "^2.1.2",
+ "rimraf": "^3.0.2",
+ "update-notifier": "^5.0.0",
+ "write-file-atomic": "^3.0.3"
+ }
+ },
+ "hard-rejection": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "has": {
+ "version": "1.0.3",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-bigints": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "has-symbols": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "has-tostringtag": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "dev": true
+ },
+ "has-yarn": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "hash-base": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "safe-buffer": "^5.2.0"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "3.6.0",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "1.3.0",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.2.0"
+ }
+ }
+ }
+ },
+ "hash.js": {
+ "version": "1.1.7",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "he": {
+ "version": "1.2.0",
+ "dev": true
+ },
+ "hmac-drbg": {
+ "version": "1.0.1",
+ "dev": true,
+ "requires": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "hoist-non-react-statics": {
+ "version": "3.3.2",
+ "dev": true,
+ "requires": {
+ "react-is": "^16.7.0"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "16.13.1",
+ "dev": true
+ }
+ }
+ },
+ "hosted-git-info": {
+ "version": "4.0.2",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "html-dom-parser": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "domhandler": "4.2.2",
+ "htmlparser2": "6.1.0"
+ }
+ },
+ "html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "requires": {
+ "whatwg-encoding": "^2.0.0"
+ }
+ },
+ "html-escaper": {
+ "version": "2.0.2",
+ "dev": true,
+ "peer": true
+ },
+ "html-react-parser": {
+ "version": "1.4.0",
+ "dev": true,
+ "requires": {
+ "domhandler": "4.2.2",
+ "html-dom-parser": "1.0.2",
+ "react-property": "2.0.0",
+ "style-to-js": "1.1.0"
+ }
+ },
+ "htmlparser2": {
+ "version": "6.1.0",
+ "dev": true,
+ "requires": {
+ "domelementtype": "^2.0.1",
+ "domhandler": "^4.0.0",
+ "domutils": "^2.5.2",
+ "entities": "^2.0.0"
+ }
+ },
+ "http-cache-semantics": {
+ "version": "4.1.0",
+ "dev": true
+ },
+ "http-proxy": {
+ "version": "1.18.1",
+ "dev": true,
+ "requires": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ }
+ },
+ "http-proxy-agent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz",
+ "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==",
+ "dev": true,
+ "requires": {
+ "@tootallnate/once": "2",
+ "agent-base": "6",
+ "debug": "4"
+ }
+ },
+ "http-server": {
+ "version": "13.0.2",
+ "dev": true,
+ "requires": {
+ "basic-auth": "^1.0.3",
+ "colors": "^1.4.0",
+ "corser": "^2.0.1",
+ "he": "^1.1.0",
+ "http-proxy": "^1.18.0",
+ "mime": "^1.6.0",
+ "minimist": "^1.2.5",
+ "opener": "^1.5.1",
+ "portfinder": "^1.0.25",
+ "secure-compare": "3.0.1",
+ "union": "~0.5.0",
+ "url-join": "^2.0.5"
+ }
+ },
+ "https-proxy-agent": {
+ "version": "5.0.0",
+ "dev": true,
+ "requires": {
+ "agent-base": "6",
+ "debug": "4"
+ }
+ },
+ "human-signals": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "hyphenate-style-name": {
+ "version": "1.0.4",
+ "dev": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "idb-wrapper": {
+ "version": "1.7.2",
+ "dev": true
+ },
+ "ieee754": {
+ "version": "1.2.1",
+ "dev": true
+ },
+ "ignore": {
+ "version": "5.1.8",
+ "dev": true
+ },
+ "import-fresh": {
+ "version": "3.3.0",
+ "dev": true,
+ "requires": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ }
+ },
+ "import-lazy": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "import-local": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz",
+ "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "pkg-dir": "^4.2.0",
+ "resolve-cwd": "^3.0.0"
+ }
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "dev": true
+ },
+ "indent-string": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "indexof": {
+ "version": "0.0.1",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "dev": true
+ },
+ "ini": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "inline-style-parser": {
+ "version": "0.1.1",
+ "dev": true
+ },
+ "inquirer": {
+ "version": "7.3.3",
+ "dev": true,
+ "requires": {
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.1.0",
+ "cli-cursor": "^3.1.0",
+ "cli-width": "^3.0.0",
+ "external-editor": "^3.0.3",
+ "figures": "^3.0.0",
+ "lodash": "^4.17.19",
+ "mute-stream": "0.0.8",
+ "run-async": "^2.4.0",
+ "rxjs": "^6.6.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "through": "^2.3.6"
+ },
+ "dependencies": {
+ "rxjs": {
+ "version": "6.6.7",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.9.0"
+ }
+ },
+ "tslib": {
+ "version": "1.14.1",
+ "dev": true
+ }
+ }
+ },
+ "install-peers": {
+ "version": "1.0.3",
+ "dev": true,
+ "requires": {
+ "executioner": "^2.0.1"
+ }
+ },
+ "internal-slot": {
+ "version": "1.0.3",
+ "dev": true,
+ "requires": {
+ "get-intrinsic": "^1.1.0",
+ "has": "^1.0.3",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "is": {
+ "version": "0.2.7",
+ "dev": true
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "dev": true
+ },
+ "is-bigint": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "has-bigints": "^1.0.1"
+ }
+ },
+ "is-boolean-object": {
+ "version": "1.1.2",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-builtin-module": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "builtin-modules": "^3.0.0"
+ }
+ },
+ "is-callable": {
+ "version": "1.2.4",
+ "dev": true
+ },
+ "is-ci": {
+ "version": "2.0.0",
+ "dev": true,
+ "requires": {
+ "ci-info": "^2.0.0"
+ }
+ },
+ "is-core-module": {
+ "version": "2.8.0",
+ "dev": true,
+ "requires": {
+ "has": "^1.0.3"
+ }
+ },
+ "is-date-object": {
+ "version": "1.0.5",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "dev": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "is-generator-fn": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz",
+ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==",
+ "dev": true,
+ "peer": true
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-in-browser": {
+ "version": "1.1.3",
+ "dev": true
+ },
+ "is-installed-globally": {
+ "version": "0.4.0",
+ "dev": true,
+ "requires": {
+ "global-dirs": "^3.0.0",
+ "is-path-inside": "^3.0.2"
+ }
+ },
+ "is-module": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "is-negative-zero": {
+ "version": "2.0.1",
+ "dev": true
+ },
+ "is-npm": {
+ "version": "5.0.0",
+ "dev": true
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "dev": true
+ },
+ "is-number-object": {
+ "version": "1.0.6",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-obj": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "is-object": {
+ "version": "0.1.2",
+ "dev": true
+ },
+ "is-path-inside": {
+ "version": "3.0.3",
+ "dev": true
+ },
+ "is-plain-obj": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true
+ },
+ "is-reference": {
+ "version": "1.2.1",
+ "dev": true,
+ "requires": {
+ "@types/estree": "*"
+ }
+ },
+ "is-regex": {
+ "version": "1.1.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-shared-array-buffer": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "is-stream": {
+ "version": "2.0.1",
+ "dev": true
+ },
+ "is-string": {
+ "version": "1.0.7",
+ "dev": true,
+ "requires": {
+ "has-tostringtag": "^1.0.0"
+ }
+ },
+ "is-symbol": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "has-symbols": "^1.0.2"
+ }
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "is-weakref": {
+ "version": "1.0.1",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0"
+ }
+ },
+ "is-yarn-global": {
+ "version": "0.3.0",
+ "dev": true
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "dev": true
+ },
+ "isbuffer": {
+ "version": "0.0.0",
+ "dev": true
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "istanbul-lib-coverage": {
+ "version": "3.2.0",
+ "dev": true,
+ "peer": true
+ },
+ "istanbul-lib-instrument": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz",
+ "integrity": "sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "istanbul-lib-report": {
+ "version": "3.0.0",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^3.0.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ }
+ },
+ "istanbul-reports": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz",
+ "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ }
+ },
+ "jest": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest/-/jest-28.1.2.tgz",
+ "integrity": "sha512-Tuf05DwLeCh2cfWCQbcz9UxldoDyiR1E9Igaei5khjonKncYdc6LDfynKCEWozK0oLE3GD+xKAo2u8x/0s6GOg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/core": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "import-local": "^3.0.2",
+ "jest-cli": "^28.1.2"
+ }
+ },
+ "jest-changed-files": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.0.2.tgz",
+ "integrity": "sha512-QX9u+5I2s54ZnGoMEjiM2WeBvJR2J7w/8ZUmH2um/WLAuGAYFQcsVXY9+1YL6k0H/AGUdH8pXUAv6erDqEsvIA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "execa": "^5.0.0",
+ "throat": "^6.0.1"
+ }
+ },
+ "jest-circus": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.2.tgz",
+ "integrity": "sha512-E2vdPIJG5/69EMpslFhaA46WkcrN74LI5V/cSJ59L7uS8UNoXbzTxmwhpi9XrIL3zqvMt5T0pl5k2l2u2GwBNQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/environment": "^28.1.2",
+ "@jest/expect": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "co": "^4.6.0",
+ "dedent": "^0.7.0",
+ "is-generator-fn": "^2.0.0",
+ "jest-each": "^28.1.1",
+ "jest-matcher-utils": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-runtime": "^28.1.2",
+ "jest-snapshot": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "pretty-format": "^28.1.1",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3",
+ "throat": "^6.0.1"
+ }
+ },
+ "jest-cli": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.2.tgz",
+ "integrity": "sha512-l6eoi5Do/IJUXAFL9qRmDiFpBeEJAnjJb1dcd9i/VWfVWbp3mJhuH50dNtX67Ali4Ecvt4eBkWb4hXhPHkAZTw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/core": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "chalk": "^4.0.0",
+ "exit": "^0.1.2",
+ "graceful-fs": "^4.2.9",
+ "import-local": "^3.0.2",
+ "jest-config": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "jest-validate": "^28.1.1",
+ "prompts": "^2.0.1",
+ "yargs": "^17.3.1"
+ },
+ "dependencies": {
+ "yargs": {
+ "version": "17.5.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
+ "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.0.0"
+ }
+ },
+ "yargs-parser": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
+ "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "jest-config": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-28.1.2.tgz",
+ "integrity": "sha512-g6EfeRqddVbjPVBVY4JWpUY4IvQoFRIZcv4V36QkqzE0IGhEC/VkugFeBMAeUE7PRgC8KJF0yvJNDeQRbamEVA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/core": "^7.11.6",
+ "@jest/test-sequencer": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "babel-jest": "^28.1.2",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "deepmerge": "^4.2.2",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-circus": "^28.1.2",
+ "jest-environment-node": "^28.1.2",
+ "jest-get-type": "^28.0.2",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.1",
+ "jest-runner": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "jest-validate": "^28.1.1",
+ "micromatch": "^4.0.4",
+ "parse-json": "^5.2.0",
+ "pretty-format": "^28.1.1",
+ "slash": "^3.0.0",
+ "strip-json-comments": "^3.1.1"
+ },
+ "dependencies": {
+ "ci-info": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
+ "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "jest-diff": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.1.tgz",
+ "integrity": "sha512-/MUUxeR2fHbqHoMMiffe/Afm+U8U4olFRJ0hiVG2lZatPJcnGxx292ustVu7bULhjV65IYMxRdploAKLbcrsyg==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.0.0",
+ "diff-sequences": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.1"
+ }
+ },
+ "jest-docblock": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz",
+ "integrity": "sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "detect-newline": "^3.0.0"
+ }
+ },
+ "jest-each": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-28.1.1.tgz",
+ "integrity": "sha512-A042rqh17ZvEhRceDMi784ppoXR7MWGDEKTXEZXb4svt0eShMZvijGxzKsx+yIjeE8QYmHPrnHiTSQVhN4nqaw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/types": "^28.1.1",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^28.0.2",
+ "jest-util": "^28.1.1",
+ "pretty-format": "^28.1.1"
+ }
+ },
+ "jest-environment-jsdom": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-28.1.3.tgz",
+ "integrity": "sha512-HnlGUmZRdxfCByd3GM2F100DgQOajUBzEitjGqIREcb45kGjZvRrKUdlaF6escXBdcXNl0OBh+1ZrfeZT3GnAg==",
+ "dev": true,
+ "requires": {
+ "@jest/environment": "^28.1.3",
+ "@jest/fake-timers": "^28.1.3",
+ "@jest/types": "^28.1.3",
+ "@types/jsdom": "^16.2.4",
+ "@types/node": "*",
+ "jest-mock": "^28.1.3",
+ "jest-util": "^28.1.3",
+ "jsdom": "^19.0.0"
+ }
+ },
+ "jest-environment-node": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.2.tgz",
+ "integrity": "sha512-oYsZz9Qw27XKmOgTtnl0jW7VplJkN2oeof+SwAwKFQacq3CLlG9u4kTGuuLWfvu3J7bVutWlrbEQMOCL/jughw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/environment": "^28.1.2",
+ "@jest/fake-timers": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "jest-mock": "^28.1.1",
+ "jest-util": "^28.1.1"
+ }
+ },
+ "jest-get-type": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz",
+ "integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==",
+ "dev": true
+ },
+ "jest-haste-map": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz",
+ "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/types": "^28.1.3",
+ "@types/graceful-fs": "^4.1.3",
+ "@types/node": "*",
+ "anymatch": "^3.0.3",
+ "fb-watchman": "^2.0.0",
+ "fsevents": "^2.3.2",
+ "graceful-fs": "^4.2.9",
+ "jest-regex-util": "^28.0.2",
+ "jest-util": "^28.1.3",
+ "jest-worker": "^28.1.3",
+ "micromatch": "^4.0.4",
+ "walker": "^1.0.8"
+ }
+ },
+ "jest-leak-detector": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.1.tgz",
+ "integrity": "sha512-4jvs8V8kLbAaotE+wFR7vfUGf603cwYtFf1/PYEsyX2BAjSzj8hQSVTP6OWzseTl0xL6dyHuKs2JAks7Pfubmw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.1"
+ }
+ },
+ "jest-matcher-utils": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.1.tgz",
+ "integrity": "sha512-NPJPRWrbmR2nAJ+1nmnfcKKzSwgfaciCCrYZzVnNoxVoyusYWIjkBMNvu0RHJe7dNj4hH3uZOPZsQA+xAYWqsw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^4.0.0",
+ "jest-diff": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "pretty-format": "^28.1.1"
+ }
+ },
+ "jest-message-util": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz",
+ "integrity": "sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.12.13",
+ "@jest/types": "^28.1.3",
+ "@types/stack-utils": "^2.0.0",
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "micromatch": "^4.0.4",
+ "pretty-format": "^28.1.3",
+ "slash": "^3.0.0",
+ "stack-utils": "^2.0.3"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.18.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz",
+ "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.18.6"
+ }
+ }
+ }
+ },
+ "jest-mock": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz",
+ "integrity": "sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA==",
+ "dev": true,
+ "requires": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*"
+ }
+ },
+ "jest-pnp-resolver": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz",
+ "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==",
+ "dev": true,
+ "peer": true,
+ "requires": {}
+ },
+ "jest-regex-util": {
+ "version": "28.0.2",
+ "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz",
+ "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==",
+ "dev": true,
+ "peer": true
+ },
+ "jest-resolve": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.1.tgz",
+ "integrity": "sha512-/d1UbyUkf9nvsgdBildLe6LAD4DalgkgZcKd0nZ8XUGPyA/7fsnaQIlKVnDiuUXv/IeZhPEDrRJubVSulxrShA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "chalk": "^4.0.0",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.1",
+ "jest-pnp-resolver": "^1.2.2",
+ "jest-util": "^28.1.1",
+ "jest-validate": "^28.1.1",
+ "resolve": "^1.20.0",
+ "resolve.exports": "^1.1.0",
+ "slash": "^3.0.0"
+ }
+ },
+ "jest-resolve-dependencies": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.2.tgz",
+ "integrity": "sha512-OXw4vbOZuyRTBi3tapWBqdyodU+T33ww5cPZORuTWkg+Y8lmsxQlVu3MWtJh6NMlKRTHQetF96yGPv01Ye7Mbg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "jest-regex-util": "^28.0.2",
+ "jest-snapshot": "^28.1.2"
+ }
+ },
+ "jest-runner": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.2.tgz",
+ "integrity": "sha512-6/k3DlAsAEr5VcptCMdhtRhOoYClZQmxnVMZvZ/quvPGRpN7OBQYPIC32tWSgOnbgqLXNs5RAniC+nkdFZpD4A==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/console": "^28.1.1",
+ "@jest/environment": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "emittery": "^0.10.2",
+ "graceful-fs": "^4.2.9",
+ "jest-docblock": "^28.1.1",
+ "jest-environment-node": "^28.1.2",
+ "jest-haste-map": "^28.1.1",
+ "jest-leak-detector": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-resolve": "^28.1.1",
+ "jest-runtime": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "jest-watcher": "^28.1.1",
+ "jest-worker": "^28.1.1",
+ "source-map-support": "0.5.13",
+ "throat": "^6.0.1"
+ },
+ "dependencies": {
+ "source-map-support": {
+ "version": "0.5.13",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
+ "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ }
+ }
+ },
+ "jest-runtime": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.2.tgz",
+ "integrity": "sha512-i4w93OsWzLOeMXSi9epmakb2+3z0AchZtUQVF1hesBmcQQy4vtaql5YdVe9KexdJaVRyPDw8DoBR0j3lYsZVYw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/environment": "^28.1.2",
+ "@jest/fake-timers": "^28.1.2",
+ "@jest/globals": "^28.1.2",
+ "@jest/source-map": "^28.1.2",
+ "@jest/test-result": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "chalk": "^4.0.0",
+ "cjs-module-lexer": "^1.0.0",
+ "collect-v8-coverage": "^1.0.0",
+ "execa": "^5.0.0",
+ "glob": "^7.1.3",
+ "graceful-fs": "^4.2.9",
+ "jest-haste-map": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-mock": "^28.1.1",
+ "jest-regex-util": "^28.0.2",
+ "jest-resolve": "^28.1.1",
+ "jest-snapshot": "^28.1.2",
+ "jest-util": "^28.1.1",
+ "slash": "^3.0.0",
+ "strip-bom": "^4.0.0"
+ }
+ },
+ "jest-snapshot": {
+ "version": "28.1.2",
+ "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.2.tgz",
+ "integrity": "sha512-wzrieFttZYfLvrCVRJxX+jwML2YTArOUqFpCoSVy1QUapx+LlV9uLbV/mMEhYj4t7aMeE9aSQFHSvV/oNoDAMA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@babel/core": "^7.11.6",
+ "@babel/generator": "^7.7.2",
+ "@babel/plugin-syntax-typescript": "^7.7.2",
+ "@babel/traverse": "^7.7.2",
+ "@babel/types": "^7.3.3",
+ "@jest/expect-utils": "^28.1.1",
+ "@jest/transform": "^28.1.2",
+ "@jest/types": "^28.1.1",
+ "@types/babel__traverse": "^7.0.6",
+ "@types/prettier": "^2.1.5",
+ "babel-preset-current-node-syntax": "^1.0.0",
+ "chalk": "^4.0.0",
+ "expect": "^28.1.1",
+ "graceful-fs": "^4.2.9",
+ "jest-diff": "^28.1.1",
+ "jest-get-type": "^28.0.2",
+ "jest-haste-map": "^28.1.1",
+ "jest-matcher-utils": "^28.1.1",
+ "jest-message-util": "^28.1.1",
+ "jest-util": "^28.1.1",
+ "natural-compare": "^1.4.0",
+ "pretty-format": "^28.1.1",
+ "semver": "^7.3.5"
+ }
+ },
+ "jest-util": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz",
+ "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==",
+ "dev": true,
+ "requires": {
+ "@jest/types": "^28.1.3",
+ "@types/node": "*",
+ "chalk": "^4.0.0",
+ "ci-info": "^3.2.0",
+ "graceful-fs": "^4.2.9",
+ "picomatch": "^2.2.3"
+ },
+ "dependencies": {
+ "ci-info": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz",
+ "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==",
+ "dev": true
+ }
+ }
+ },
+ "jest-validate": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.1.tgz",
+ "integrity": "sha512-Kpf6gcClqFCIZ4ti5++XemYJWUPCFUW+N2gknn+KgnDf549iLul3cBuKVe1YcWRlaF8tZV8eJCap0eECOEE3Ug==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/types": "^28.1.1",
+ "camelcase": "^6.2.0",
+ "chalk": "^4.0.0",
+ "jest-get-type": "^28.0.2",
+ "leven": "^3.1.0",
+ "pretty-format": "^28.1.1"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "jest-watcher": {
+ "version": "28.1.1",
+ "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.1.tgz",
+ "integrity": "sha512-RQIpeZ8EIJMxbQrXpJQYIIlubBnB9imEHsxxE41f54ZwcqWLysL/A0ZcdMirf+XsMn3xfphVQVV4EW0/p7i7Ug==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@jest/test-result": "^28.1.1",
+ "@jest/types": "^28.1.1",
+ "@types/node": "*",
+ "ansi-escapes": "^4.2.1",
+ "chalk": "^4.0.0",
+ "emittery": "^0.10.2",
+ "jest-util": "^28.1.1",
+ "string-length": "^4.0.1"
+ }
+ },
+ "jest-worker": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
+ "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@types/node": "*",
+ "merge-stream": "^2.0.0",
+ "supports-color": "^8.0.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.14.1",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "dependencies": {
+ "argparse": {
+ "version": "1.0.10",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ }
+ }
+ },
+ "jsdom": {
+ "version": "19.0.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz",
+ "integrity": "sha512-RYAyjCbxy/vri/CfnjUWJQQtZ3LKlLnDqj+9XLNnJPgEGeirZs3hllKR20re8LUZ6o1b1X4Jat+Qd26zmP41+A==",
+ "dev": true,
+ "requires": {
+ "abab": "^2.0.5",
+ "acorn": "^8.5.0",
+ "acorn-globals": "^6.0.0",
+ "cssom": "^0.5.0",
+ "cssstyle": "^2.3.0",
+ "data-urls": "^3.0.1",
+ "decimal.js": "^10.3.1",
+ "domexception": "^4.0.0",
+ "escodegen": "^2.0.0",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy-agent": "^5.0.0",
+ "https-proxy-agent": "^5.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.0",
+ "parse5": "6.0.1",
+ "saxes": "^5.0.1",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^4.0.0",
+ "w3c-hr-time": "^1.0.2",
+ "w3c-xmlserializer": "^3.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^2.0.0",
+ "whatwg-mimetype": "^3.0.0",
+ "whatwg-url": "^10.0.0",
+ "ws": "^8.2.3",
+ "xml-name-validator": "^4.0.0"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "8.7.1",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
+ "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
+ "dev": true
+ },
+ "tr46": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
+ "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.1"
+ }
+ },
+ "webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-10.0.0.tgz",
+ "integrity": "sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==",
+ "dev": true,
+ "requires": {
+ "tr46": "^3.0.0",
+ "webidl-conversions": "^7.0.0"
+ }
+ }
+ }
+ },
+ "jsesc": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
+ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
+ "dev": true,
+ "peer": true
+ },
+ "json-buffer": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "dev": true
+ },
+ "json-schema-traverse": {
+ "version": "0.4.1",
+ "dev": true
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "json5": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
+ "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==",
+ "dev": true
+ },
+ "jss": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.3.1",
+ "csstype": "^3.0.2",
+ "is-in-browser": "^1.1.3",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "jss-plugin-camel-case": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.3.1",
+ "hyphenate-style-name": "^1.0.3",
+ "jss": "10.8.1"
+ }
+ },
+ "jss-plugin-default-unit": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1"
+ }
+ },
+ "jss-plugin-global": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1"
+ }
+ },
+ "jss-plugin-nested": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "jss-plugin-props-sort": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1"
+ }
+ },
+ "jss-plugin-rule-value-function": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.3.1",
+ "jss": "10.8.1",
+ "tiny-warning": "^1.0.2"
+ }
+ },
+ "jss-plugin-vendor-prefixer": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.3.1",
+ "css-vendor": "^2.0.8",
+ "jss": "10.8.1"
+ }
+ },
+ "jsx-ast-utils": {
+ "version": "3.2.1",
+ "dev": true,
+ "requires": {
+ "array-includes": "^3.1.3",
+ "object.assign": "^4.1.2"
+ }
+ },
+ "keyv": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "json-buffer": "3.0.0"
+ }
+ },
+ "kind-of": {
+ "version": "6.0.3",
+ "dev": true
+ },
+ "kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "peer": true
+ },
+ "latest-version": {
+ "version": "5.1.0",
+ "dev": true,
+ "requires": {
+ "package-json": "^6.3.0"
+ }
+ },
+ "level-blobs": {
+ "version": "0.1.7",
+ "dev": true,
+ "requires": {
+ "level-peek": "1.0.6",
+ "once": "^1.3.0",
+ "readable-stream": "^1.0.26-4"
+ },
+ "dependencies": {
+ "readable-stream": {
+ "version": "1.1.14",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ }
+ }
+ },
+ "level-filesystem": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "concat-stream": "^1.4.4",
+ "errno": "^0.1.1",
+ "fwd-stream": "^1.0.4",
+ "level-blobs": "^0.1.7",
+ "level-peek": "^1.0.6",
+ "level-sublevel": "^5.2.0",
+ "octal": "^1.0.0",
+ "once": "^1.3.0",
+ "xtend": "^2.2.0"
+ },
+ "dependencies": {
+ "xtend": {
+ "version": "2.2.0",
+ "dev": true
+ }
+ }
+ },
+ "level-fix-range": {
+ "version": "2.0.0",
+ "dev": true,
+ "requires": {
+ "clone": "~0.1.9"
+ }
+ },
+ "level-hooks": {
+ "version": "4.5.0",
+ "dev": true,
+ "requires": {
+ "string-range": "~1.2"
+ }
+ },
+ "level-js": {
+ "version": "2.2.4",
+ "dev": true,
+ "requires": {
+ "abstract-leveldown": "~0.12.0",
+ "idb-wrapper": "^1.5.0",
+ "isbuffer": "~0.0.0",
+ "ltgt": "^2.1.2",
+ "typedarray-to-buffer": "~1.0.0",
+ "xtend": "~2.1.2"
+ },
+ "dependencies": {
+ "object-keys": {
+ "version": "0.4.0",
+ "dev": true
+ },
+ "typedarray-to-buffer": {
+ "version": "1.0.4",
+ "dev": true
+ },
+ "xtend": {
+ "version": "2.1.2",
+ "dev": true,
+ "requires": {
+ "object-keys": "~0.4.0"
+ }
+ }
+ }
+ },
+ "level-peek": {
+ "version": "1.0.6",
+ "dev": true,
+ "requires": {
+ "level-fix-range": "~1.0.2"
+ },
+ "dependencies": {
+ "level-fix-range": {
+ "version": "1.0.2",
+ "dev": true
+ }
+ }
+ },
+ "level-sublevel": {
+ "version": "5.2.3",
+ "dev": true,
+ "requires": {
+ "level-fix-range": "2.0",
+ "level-hooks": ">=4.4.0 <5",
+ "string-range": "~1.2.1",
+ "xtend": "~2.0.4"
+ },
+ "dependencies": {
+ "object-keys": {
+ "version": "0.2.0",
+ "dev": true,
+ "requires": {
+ "foreach": "~2.0.1",
+ "indexof": "~0.0.1",
+ "is": "~0.2.6"
+ }
+ },
+ "xtend": {
+ "version": "2.0.6",
+ "dev": true,
+ "requires": {
+ "is-object": "~0.1.2",
+ "object-keys": "~0.2.0"
+ }
+ }
+ }
+ },
+ "levelup": {
+ "version": "0.18.6",
+ "dev": true,
+ "requires": {
+ "bl": "~0.8.1",
+ "deferred-leveldown": "~0.2.0",
+ "errno": "~0.1.1",
+ "prr": "~0.0.0",
+ "readable-stream": "~1.0.26",
+ "semver": "~2.3.1",
+ "xtend": "~3.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "2.3.2",
+ "dev": true
+ }
+ }
+ },
+ "leven": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
+ "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==",
+ "dev": true,
+ "peer": true
+ },
+ "levn": {
+ "version": "0.4.1",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ }
+ },
+ "lines-and-columns": {
+ "version": "1.1.6",
+ "dev": true
+ },
+ "lodash": {
+ "version": "4.17.21",
+ "dev": true
+ },
+ "lodash.camelcase": {
+ "version": "4.3.0",
+ "dev": true
+ },
+ "lodash.clonedeep": {
+ "version": "4.5.0",
+ "dev": true
+ },
+ "lodash.memoize": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
+ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==",
+ "dev": true
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "dev": true
+ },
+ "lodash.truncate": {
+ "version": "4.4.2",
+ "dev": true
+ },
+ "long": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.2.1.tgz",
+ "integrity": "sha512-GKSNGeNAtw8IryjjkhZxuKB3JzlcLTwjtiQCHKvqQet81I93kXslhDQruGI/QsddO83mcDToBVy7GqGS/zYf/A=="
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "dev": true,
+ "requires": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "lru-cache": {
+ "version": "6.0.0",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "ltgt": {
+ "version": "2.2.1",
+ "dev": true
+ },
+ "magic-string": {
+ "version": "0.22.5",
+ "dev": true,
+ "requires": {
+ "vlq": "^0.2.2"
+ }
+ },
+ "make-dir": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "semver": "^6.0.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "dev": true
+ }
+ }
+ },
+ "make-error": {
+ "version": "1.3.6",
+ "dev": true
+ },
+ "makeerror": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz",
+ "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "tmpl": "1.0.5"
+ }
+ },
+ "map-obj": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "map-stream": {
+ "version": "0.1.0",
+ "dev": true
+ },
+ "md5.js": {
+ "version": "1.3.5",
+ "dev": true,
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "meow": {
+ "version": "9.0.0",
+ "dev": true,
+ "requires": {
+ "@types/minimist": "^1.2.0",
+ "camelcase-keys": "^6.2.2",
+ "decamelize": "^1.2.0",
+ "decamelize-keys": "^1.1.0",
+ "hard-rejection": "^2.1.0",
+ "minimist-options": "4.1.0",
+ "normalize-package-data": "^3.0.0",
+ "read-pkg-up": "^7.0.1",
+ "redent": "^3.0.0",
+ "trim-newlines": "^3.0.0",
+ "type-fest": "^0.18.0",
+ "yargs-parser": "^20.2.3"
+ },
+ "dependencies": {
+ "type-fest": {
+ "version": "0.18.1",
+ "dev": true
+ }
+ }
+ },
+ "merge-stream": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "merge2": {
+ "version": "1.4.1",
+ "dev": true
+ },
+ "micromatch": {
+ "version": "4.0.4",
+ "dev": true,
+ "requires": {
+ "braces": "^3.0.1",
+ "picomatch": "^2.2.3"
+ }
+ },
+ "miller-rabin": {
+ "version": "4.0.1",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ }
+ },
+ "mime": {
+ "version": "1.6.0",
+ "dev": true
+ },
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "2.1.0",
+ "dev": true
+ },
+ "mimic-response": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "min-indent": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "minimalistic-assert": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.5",
+ "dev": true
+ },
+ "minimist-options": {
+ "version": "4.1.0",
+ "dev": true,
+ "requires": {
+ "arrify": "^1.0.1",
+ "is-plain-obj": "^1.1.0",
+ "kind-of": "^6.0.3"
+ }
+ },
+ "minipass": {
+ "version": "3.1.5",
+ "dev": true,
+ "requires": {
+ "yallist": "^4.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "2.1.2",
+ "dev": true,
+ "requires": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ }
+ },
+ "mixly": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "fulcon": "^1.0.1"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.5",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.5"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "dev": true
+ },
+ "mute-stream": {
+ "version": "0.0.8",
+ "dev": true
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "dev": true
+ },
+ "ncp": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "node-cleanup": {
+ "version": "2.1.2",
+ "dev": true
+ },
+ "node-fetch": {
+ "version": "2.6.5",
+ "dev": true,
+ "requires": {
+ "whatwg-url": "^5.0.0"
+ }
+ },
+ "node-int64": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
+ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==",
+ "dev": true,
+ "peer": true
+ },
+ "node-releases": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz",
+ "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
+ "dev": true,
+ "peer": true
+ },
+ "nopt": {
+ "version": "5.0.0",
+ "dev": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "normalize-package-data": {
+ "version": "3.0.3",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^4.0.1",
+ "is-core-module": "^2.5.0",
+ "semver": "^7.3.4",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "dev": true,
+ "peer": true
+ },
+ "normalize-url": {
+ "version": "4.5.1",
+ "dev": true
+ },
+ "npm-run-path": {
+ "version": "4.0.1",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.0.0"
+ }
+ },
+ "npmlog": {
+ "version": "5.0.1",
+ "dev": true,
+ "requires": {
+ "are-we-there-yet": "^2.0.0",
+ "console-control-strings": "^1.1.0",
+ "gauge": "^3.0.0",
+ "set-blocking": "^2.0.0"
+ }
+ },
+ "nwsapi": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz",
+ "integrity": "sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==",
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "dev": true
+ },
+ "object-inspect": {
+ "version": "1.11.0",
+ "dev": true
+ },
+ "object-keys": {
+ "version": "1.1.1",
+ "dev": true
+ },
+ "object-path": {
+ "version": "0.11.8",
+ "resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
+ "integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA=="
+ },
+ "object.assign": {
+ "version": "4.1.2",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3",
+ "has-symbols": "^1.0.1",
+ "object-keys": "^1.1.1"
+ }
+ },
+ "object.entries": {
+ "version": "1.1.5",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.fromentries": {
+ "version": "2.0.5",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.hasown": {
+ "version": "1.1.0",
+ "dev": true,
+ "requires": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "object.values": {
+ "version": "1.1.5",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1"
+ }
+ },
+ "octal": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "5.1.2",
+ "dev": true,
+ "requires": {
+ "mimic-fn": "^2.1.0"
+ }
+ },
+ "opener": {
+ "version": "1.5.2",
+ "dev": true
+ },
+ "optionator": {
+ "version": "0.9.1",
+ "dev": true,
+ "requires": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.3"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "p-cancelable": {
+ "version": "1.1.0",
+ "dev": true
+ },
+ "p-try": {
+ "version": "2.2.0",
+ "dev": true
+ },
+ "package-json": {
+ "version": "6.5.0",
+ "dev": true,
+ "requires": {
+ "got": "^9.6.0",
+ "registry-auth-token": "^4.0.0",
+ "registry-url": "^5.0.0",
+ "semver": "^6.2.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "dev": true
+ }
+ }
+ },
+ "parent-module": {
+ "version": "1.0.1",
+ "dev": true,
+ "requires": {
+ "callsites": "^3.0.0"
+ }
+ },
+ "parse-asn1": {
+ "version": "5.1.6",
+ "dev": true,
+ "requires": {
+ "asn1.js": "^5.2.0",
+ "browserify-aes": "^1.0.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "parse-json": {
+ "version": "5.2.0",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.15.8",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.14.5"
+ }
+ }
+ }
+ },
+ "parse5": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
+ "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
+ "dev": true
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "dev": true
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "dev": true
+ },
+ "path-parse": {
+ "version": "1.0.7",
+ "dev": true
+ },
+ "path-type": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "pause-stream": {
+ "version": "0.0.11",
+ "dev": true,
+ "requires": {
+ "through": "~2.3"
+ }
+ },
+ "pbkdf2": {
+ "version": "3.1.2",
+ "dev": true,
+ "requires": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true,
+ "peer": true
+ },
+ "picomatch": {
+ "version": "2.3.0",
+ "dev": true
+ },
+ "pirates": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
+ "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==",
+ "dev": true,
+ "peer": true
+ },
+ "pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "find-up": "^4.0.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ }
+ }
+ },
+ "popper.js": {
+ "version": "1.16.1-lts",
+ "dev": true
+ },
+ "portfinder": {
+ "version": "1.0.28",
+ "dev": true,
+ "requires": {
+ "async": "^2.6.2",
+ "debug": "^3.1.1",
+ "mkdirp": "^0.5.5"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.3",
+ "dev": true
+ }
+ }
+ },
+ "prelude-ls": {
+ "version": "1.2.1",
+ "dev": true
+ },
+ "prepend-http": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "prettier": {
+ "version": "2.4.1",
+ "dev": true
+ },
+ "prettier-linter-helpers": {
+ "version": "1.0.0",
+ "dev": true,
+ "requires": {
+ "fast-diff": "^1.1.2"
+ }
+ },
+ "pretty-format": {
+ "version": "28.1.3",
+ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz",
+ "integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==",
+ "dev": true,
+ "requires": {
+ "@jest/schemas": "^28.1.3",
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^18.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true
+ },
+ "react-is": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
+ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
+ "dev": true
+ }
+ }
+ },
+ "process-es6": {
+ "version": "0.11.6",
+ "dev": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.1",
+ "dev": true
+ },
+ "progress": {
+ "version": "2.0.3",
+ "dev": true
+ },
+ "prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ }
+ },
+ "prop-types": {
+ "version": "15.7.2",
+ "dev": true,
+ "requires": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.8.1"
+ },
+ "dependencies": {
+ "react-is": {
+ "version": "16.13.1",
+ "dev": true
+ }
+ }
+ },
+ "prr": {
+ "version": "0.0.0",
+ "dev": true
+ },
+ "ps-tree": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "event-stream": "=3.3.4"
+ }
+ },
+ "psl": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
+ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==",
+ "dev": true
+ },
+ "public-encrypt": {
+ "version": "4.0.3",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "pump": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "punycode": {
+ "version": "2.1.1",
+ "dev": true
+ },
+ "pupa": {
+ "version": "2.1.1",
+ "dev": true,
+ "requires": {
+ "escape-goat": "^2.0.0"
+ }
+ },
+ "queue-microtask": {
+ "version": "1.2.3",
+ "dev": true
+ },
+ "quick-lru": {
+ "version": "4.0.1",
+ "dev": true
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "randomfill": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "rc": {
+ "version": "1.2.8",
+ "dev": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "dependencies": {
+ "ini": {
+ "version": "1.3.8",
+ "dev": true
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "dev": true
+ }
+ }
+ },
+ "react": {
+ "version": "17.0.2",
+ "dev": true,
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "react-dom": {
+ "version": "17.0.2",
+ "dev": true,
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1",
+ "scheduler": "^0.20.2"
+ }
+ },
+ "react-is": {
+ "version": "17.0.2",
+ "dev": true
+ },
+ "react-property": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "react-transition-group": {
+ "version": "4.4.2",
+ "dev": true,
+ "requires": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ }
+ },
+ "read-pkg": {
+ "version": "5.2.0",
+ "dev": true,
+ "requires": {
+ "@types/normalize-package-data": "^2.4.0",
+ "normalize-package-data": "^2.5.0",
+ "parse-json": "^5.0.0",
+ "type-fest": "^0.6.0"
+ },
+ "dependencies": {
+ "hosted-git-info": {
+ "version": "2.8.9",
+ "dev": true
+ },
+ "normalize-package-data": {
+ "version": "2.5.0",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "semver": {
+ "version": "5.7.1",
+ "dev": true
+ },
+ "type-fest": {
+ "version": "0.6.0",
+ "dev": true
+ }
+ }
+ },
+ "read-pkg-up": {
+ "version": "7.0.1",
+ "dev": true,
+ "requires": {
+ "find-up": "^4.1.0",
+ "read-pkg": "^5.2.0",
+ "type-fest": "^0.8.1"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "4.1.0",
+ "dev": true,
+ "requires": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "locate-path": {
+ "version": "5.0.0",
+ "dev": true,
+ "requires": {
+ "p-locate": "^4.1.0"
+ }
+ },
+ "p-limit": {
+ "version": "2.3.0",
+ "dev": true,
+ "requires": {
+ "p-try": "^2.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "4.1.0",
+ "dev": true,
+ "requires": {
+ "p-limit": "^2.2.0"
+ }
+ },
+ "type-fest": {
+ "version": "0.8.1",
+ "dev": true
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "redent": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.13.9",
+ "dev": true
+ },
+ "regexp.prototype.flags": {
+ "version": "1.3.1",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "regexpp": {
+ "version": "3.2.0",
+ "dev": true
+ },
+ "registry-auth-token": {
+ "version": "4.2.1",
+ "dev": true,
+ "requires": {
+ "rc": "^1.2.8"
+ }
+ },
+ "registry-url": {
+ "version": "5.1.0",
+ "dev": true,
+ "requires": {
+ "rc": "^1.2.8"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "dev": true
+ },
+ "require-from-string": {
+ "version": "2.0.2",
+ "dev": true
+ },
+ "requirejs": {
+ "version": "2.3.6",
+ "dev": true
+ },
+ "requires-port": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "resolve": {
+ "version": "1.20.0",
+ "dev": true,
+ "requires": {
+ "is-core-module": "^2.2.0",
+ "path-parse": "^1.0.6"
+ }
+ },
+ "resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "resolve-from": "^5.0.0"
+ },
+ "dependencies": {
+ "resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "peer": true
+ }
+ }
+ },
+ "resolve-from": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "resolve.exports": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz",
+ "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==",
+ "dev": true,
+ "peer": true
+ },
+ "responselike": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "lowercase-keys": "^1.0.0"
+ }
+ },
+ "restore-cursor": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "onetime": "^5.1.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "reusify": {
+ "version": "1.0.4",
+ "dev": true
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "ripemd160": {
+ "version": "2.0.2",
+ "dev": true,
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "rollup": {
+ "version": "2.58.0",
+ "dev": true,
+ "requires": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "rollup-plugin-dts": {
+ "version": "4.2.2",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.16.7",
+ "magic-string": "^0.26.1"
+ },
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.16.7",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "@babel/highlight": "^7.16.7"
+ }
+ },
+ "magic-string": {
+ "version": "0.26.2",
+ "dev": true,
+ "requires": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ }
+ }
+ },
+ "rollup-plugin-inject": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
+ "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==",
+ "dev": true,
+ "requires": {
+ "estree-walker": "^0.6.1",
+ "magic-string": "^0.25.3",
+ "rollup-pluginutils": "^2.8.1"
+ },
+ "dependencies": {
+ "estree-walker": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+ "dev": true
+ },
+ "magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "dev": true,
+ "requires": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ }
+ }
+ },
+ "rollup-plugin-node-builtins": {
+ "version": "2.1.2",
+ "dev": true,
+ "requires": {
+ "browserify-fs": "^1.0.0",
+ "buffer-es6": "^4.9.2",
+ "crypto-browserify": "^3.11.0",
+ "process-es6": "^0.11.2"
+ }
+ },
+ "rollup-plugin-node-globals": {
+ "version": "1.4.0",
+ "dev": true,
+ "requires": {
+ "acorn": "^5.7.3",
+ "buffer-es6": "^4.9.3",
+ "estree-walker": "^0.5.2",
+ "magic-string": "^0.22.5",
+ "process-es6": "^0.11.6",
+ "rollup-pluginutils": "^2.3.1"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "5.7.4",
+ "dev": true
+ }
+ }
+ },
+ "rollup-plugin-node-polyfills": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz",
+ "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==",
+ "dev": true,
+ "requires": {
+ "rollup-plugin-inject": "^3.0.0"
+ }
+ },
+ "rollup-plugin-sourcemaps": {
+ "version": "0.6.3",
+ "dev": true,
+ "requires": {
+ "@rollup/pluginutils": "^3.0.9",
+ "source-map-resolve": "^0.6.0"
+ }
+ },
+ "rollup-pluginutils": {
+ "version": "2.8.2",
+ "dev": true,
+ "requires": {
+ "estree-walker": "^0.6.1"
+ },
+ "dependencies": {
+ "estree-walker": {
+ "version": "0.6.1",
+ "dev": true
+ }
+ }
+ },
+ "run-async": {
+ "version": "2.4.1",
+ "dev": true
+ },
+ "run-parallel": {
+ "version": "1.2.0",
+ "dev": true,
+ "requires": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "rxjs": {
+ "version": "7.4.0",
+ "dev": true,
+ "requires": {
+ "tslib": "~2.1.0"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "2.1.0",
+ "dev": true
+ }
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "dev": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "dev": true
+ },
+ "saxes": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
+ "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
+ "dev": true,
+ "requires": {
+ "xmlchars": "^2.2.0"
+ }
+ },
+ "scheduler": {
+ "version": "0.20.2",
+ "dev": true,
+ "requires": {
+ "loose-envify": "^1.1.0",
+ "object-assign": "^4.1.1"
+ }
+ },
+ "secure-compare": {
+ "version": "3.0.1",
+ "dev": true
+ },
+ "semver": {
+ "version": "7.3.5",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^6.0.0"
+ }
+ },
+ "semver-diff": {
+ "version": "3.1.1",
+ "dev": true,
+ "requires": {
+ "semver": "^6.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "6.3.0",
+ "dev": true
+ }
+ }
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "sha.js": {
+ "version": "2.4.11",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "side-channel": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.0",
+ "get-intrinsic": "^1.0.2",
+ "object-inspect": "^1.9.0"
+ }
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true,
+ "peer": true
+ },
+ "slash": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "slice-ansi": {
+ "version": "4.0.0",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "dev": true
+ },
+ "source-map-resolve": {
+ "version": "0.6.0",
+ "dev": true,
+ "requires": {
+ "atob": "^2.1.2",
+ "decode-uri-component": "^0.2.0"
+ }
+ },
+ "sourcemap-codec": {
+ "version": "1.4.8",
+ "dev": true
+ },
+ "spdx-correct": {
+ "version": "3.1.1",
+ "dev": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.3.0",
+ "dev": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.1",
+ "dev": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.10",
+ "dev": true
+ },
+ "split": {
+ "version": "0.3.3",
+ "dev": true,
+ "requires": {
+ "through": "2"
+ }
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "dev": true
+ },
+ "stack-utils": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz",
+ "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==",
+ "dev": true,
+ "requires": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "dependencies": {
+ "escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true
+ }
+ }
+ },
+ "stream-combiner": {
+ "version": "0.0.4",
+ "dev": true,
+ "requires": {
+ "duplexer": "~0.1.1"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "dev": true
+ },
+ "string-argv": {
+ "version": "0.1.2",
+ "dev": true
+ },
+ "string-length": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
+ "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "char-regex": "^1.0.2",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "string-range": {
+ "version": "1.2.2",
+ "dev": true
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "dev": true,
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "string.prototype.matchall": {
+ "version": "4.0.6",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.19.1",
+ "get-intrinsic": "^1.1.1",
+ "has-symbols": "^1.0.2",
+ "internal-slot": "^1.0.3",
+ "regexp.prototype.flags": "^1.3.1",
+ "side-channel": "^1.0.4"
+ }
+ },
+ "string.prototype.trimend": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "string.prototype.trimstart": {
+ "version": "1.0.4",
+ "dev": true,
+ "requires": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.1.3"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "dev": true,
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ },
+ "strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "peer": true
+ },
+ "strip-final-newline": {
+ "version": "2.0.0",
+ "dev": true
+ },
+ "strip-indent": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "min-indent": "^1.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "dev": true
+ },
+ "style-to-js": {
+ "version": "1.1.0",
+ "dev": true,
+ "requires": {
+ "style-to-object": "0.3.0"
+ }
+ },
+ "style-to-object": {
+ "version": "0.3.0",
+ "dev": true,
+ "requires": {
+ "inline-style-parser": "0.1.1"
+ }
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "supports-hyperlinks": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz",
+ "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ }
+ },
+ "symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true
+ },
+ "table": {
+ "version": "6.7.2",
+ "dev": true,
+ "requires": {
+ "ajv": "^8.0.1",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "dependencies": {
+ "ajv": {
+ "version": "8.6.3",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ }
+ },
+ "json-schema-traverse": {
+ "version": "1.0.0",
+ "dev": true
+ }
+ }
+ },
+ "tar": {
+ "version": "6.1.11",
+ "dev": true,
+ "requires": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^3.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "dependencies": {
+ "mkdirp": {
+ "version": "1.0.4",
+ "dev": true
+ }
+ }
+ },
+ "terminal-link": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz",
+ "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "ansi-escapes": "^4.2.1",
+ "supports-hyperlinks": "^2.0.0"
+ }
+ },
+ "test-exclude": {
+ "version": "6.0.0",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "dev": true
+ },
+ "throat": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz",
+ "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==",
+ "dev": true,
+ "peer": true
+ },
+ "through": {
+ "version": "2.3.8",
+ "dev": true
+ },
+ "tiny-warning": {
+ "version": "1.0.3",
+ "dev": true
+ },
+ "tmp": {
+ "version": "0.2.1",
+ "dev": true,
+ "requires": {
+ "rimraf": "^3.0.0"
+ }
+ },
+ "tmpl": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
+ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
+ "dev": true,
+ "peer": true
+ },
+ "to-fast-properties": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
+ "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
+ "dev": true,
+ "peer": true
+ },
+ "to-readable-stream": {
+ "version": "1.0.0",
+ "dev": true
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "dev": true,
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "tough-cookie": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
+ "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
+ "dev": true,
+ "requires": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.1.2"
+ }
+ },
+ "tr46": {
+ "version": "0.0.3",
+ "dev": true
+ },
+ "trim-newlines": {
+ "version": "3.0.1",
+ "dev": true
+ },
+ "ts-jest": {
+ "version": "28.0.5",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.5.tgz",
+ "integrity": "sha512-Sx9FyP9pCY7pUzQpy4FgRZf2bhHY3za576HMKJFs+OnQ9jS96Du5vNsDKkyedQkik+sEabbKAnCliv9BEsHZgQ==",
+ "dev": true,
+ "requires": {
+ "bs-logger": "0.x",
+ "fast-json-stable-stringify": "2.x",
+ "jest-util": "^28.0.0",
+ "json5": "^2.2.1",
+ "lodash.memoize": "4.x",
+ "make-error": "1.x",
+ "semver": "7.x",
+ "yargs-parser": "^21.0.1"
+ },
+ "dependencies": {
+ "yargs-parser": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz",
+ "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==",
+ "dev": true
+ }
+ }
+ },
+ "ts-node": {
+ "version": "10.8.1",
+ "dev": true,
+ "requires": {
+ "@cspotcode/source-map-support": "^0.8.0",
+ "@tsconfig/node10": "^1.0.7",
+ "@tsconfig/node12": "^1.0.7",
+ "@tsconfig/node14": "^1.0.0",
+ "@tsconfig/node16": "^1.0.2",
+ "acorn": "^8.4.1",
+ "acorn-walk": "^8.1.1",
+ "arg": "^4.1.0",
+ "create-require": "^1.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "v8-compile-cache-lib": "^3.0.1",
+ "yn": "3.1.1"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "8.7.1",
+ "dev": true
+ },
+ "arg": {
+ "version": "4.1.3",
+ "dev": true
+ }
+ }
+ },
+ "ts-protoc-gen": {
+ "version": "0.15.0",
+ "requires": {
+ "google-protobuf": "^3.15.5"
+ }
+ },
+ "tsc-watch": {
+ "version": "5.0.3",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.3",
+ "node-cleanup": "^2.1.2",
+ "ps-tree": "^1.2.0",
+ "string-argv": "^0.1.1",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "tslib": {
+ "version": "2.4.0",
+ "dev": true
+ },
+ "tsutils": {
+ "version": "3.21.0",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ },
+ "dependencies": {
+ "tslib": {
+ "version": "1.14.1",
+ "dev": true
+ }
+ }
+ },
+ "type-check": {
+ "version": "0.4.0",
+ "dev": true,
+ "requires": {
+ "prelude-ls": "^1.2.1"
+ }
+ },
+ "type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true
+ },
+ "type-fest": {
+ "version": "0.20.2",
+ "dev": true
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "dev": true
+ },
+ "typedarray-to-buffer": {
+ "version": "3.1.5",
+ "dev": true,
+ "requires": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "typescript": {
+ "version": "4.4.4",
+ "dev": true
+ },
+ "unbox-primitive": {
+ "version": "1.0.1",
+ "dev": true,
+ "requires": {
+ "function-bind": "^1.1.1",
+ "has-bigints": "^1.0.1",
+ "has-symbols": "^1.0.2",
+ "which-boxed-primitive": "^1.0.2"
+ }
+ },
+ "union": {
+ "version": "0.5.0",
+ "dev": true,
+ "requires": {
+ "qs": "^6.4.0"
+ },
+ "dependencies": {
+ "qs": {
+ "version": "6.10.1",
+ "dev": true,
+ "requires": {
+ "side-channel": "^1.0.4"
+ }
+ }
+ }
+ },
+ "unique-string": {
+ "version": "2.0.0",
+ "dev": true,
+ "requires": {
+ "crypto-random-string": "^2.0.0"
+ }
+ },
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
+ "dev": true
+ },
+ "update-browserslist-db": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.4.tgz",
+ "integrity": "sha512-jnmO2BEGUjsMOe/Fg9u0oczOe/ppIDZPebzccl1yDWGLFP16Pa1/RM5wEoKYPG2zstNcDuAStejyxsOuKINdGA==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "escalade": "^3.1.1",
+ "picocolors": "^1.0.0"
+ }
+ },
+ "update-notifier": {
+ "version": "5.1.0",
+ "dev": true,
+ "requires": {
+ "boxen": "^5.0.0",
+ "chalk": "^4.1.0",
+ "configstore": "^5.0.1",
+ "has-yarn": "^2.1.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^2.0.0",
+ "is-installed-globally": "^0.4.0",
+ "is-npm": "^5.0.0",
+ "is-yarn-global": "^0.3.0",
+ "latest-version": "^5.1.0",
+ "pupa": "^2.1.1",
+ "semver": "^7.3.4",
+ "semver-diff": "^3.1.1",
+ "xdg-basedir": "^4.0.0"
+ }
+ },
+ "uri-js": {
+ "version": "4.4.1",
+ "dev": true,
+ "requires": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "url-join": {
+ "version": "2.0.5",
+ "dev": true
+ },
+ "url-parse-lax": {
+ "version": "3.0.0",
+ "dev": true,
+ "requires": {
+ "prepend-http": "^2.0.0"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "v8-compile-cache": {
+ "version": "2.3.0",
+ "dev": true
+ },
+ "v8-compile-cache-lib": {
+ "version": "3.0.1",
+ "dev": true
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "dev": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "vlq": {
+ "version": "0.2.3",
+ "dev": true
+ },
+ "w3c-hr-time": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
+ "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+ "dev": true,
+ "requires": {
+ "browser-process-hrtime": "^1.0.0"
+ }
+ },
+ "w3c-xmlserializer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz",
+ "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==",
+ "dev": true,
+ "requires": {
+ "xml-name-validator": "^4.0.0"
+ }
+ },
+ "walker": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+ "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==",
+ "dev": true,
+ "peer": true,
+ "requires": {
+ "makeerror": "1.0.12"
+ }
+ },
+ "webidl-conversions": {
+ "version": "3.0.1",
+ "dev": true
+ },
+ "whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "dev": true,
+ "requires": {
+ "iconv-lite": "0.6.3"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ }
+ }
+ }
+ },
+ "whatwg-mimetype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
+ "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
+ "dev": true
+ },
+ "whatwg-url": {
+ "version": "5.0.0",
+ "dev": true,
+ "requires": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "which": {
+ "version": "2.0.2",
+ "dev": true,
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "which-boxed-primitive": {
+ "version": "1.0.2",
+ "dev": true,
+ "requires": {
+ "is-bigint": "^1.0.1",
+ "is-boolean-object": "^1.1.0",
+ "is-number-object": "^1.0.4",
+ "is-string": "^1.0.5",
+ "is-symbol": "^1.0.3"
+ }
+ },
+ "wide-align": {
+ "version": "1.1.5",
+ "dev": true,
+ "requires": {
+ "string-width": "^1.0.2 || 2 || 3 || 4"
+ }
+ },
+ "widest-line": {
+ "version": "3.1.0",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.0.0"
+ }
+ },
+ "word-wrap": {
+ "version": "1.2.3",
+ "dev": true
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "dev": true
+ },
+ "write-file-atomic": {
+ "version": "3.0.3",
+ "dev": true,
+ "requires": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "ws": {
+ "version": "8.8.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz",
+ "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==",
+ "dev": true,
+ "requires": {}
+ },
+ "xdg-basedir": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "xml-name-validator": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
+ "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
+ "dev": true
+ },
+ "xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true
+ },
+ "xtend": {
+ "version": "3.0.0",
+ "dev": true
+ },
+ "y18n": {
+ "version": "5.0.8",
+ "dev": true
+ },
+ "yallist": {
+ "version": "4.0.0",
+ "dev": true
+ },
+ "yargs": {
+ "version": "16.2.0",
+ "dev": true,
+ "requires": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ }
+ },
+ "yargs-parser": {
+ "version": "20.2.9",
+ "dev": true
+ },
+ "yn": {
+ "version": "3.1.1",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
index f3a9852b2..b3eebb567 100644
--- a/package.json
+++ b/package.json
@@ -1,69 +1,89 @@
{
- "name": "pigweed",
- "version": "0.0.1",
+ "name": "pigweedjs",
+ "version": "0.0.8",
"description": "An open source collection of embedded-targeted libraries",
- "author": "",
+ "author": "The Pigweed Authors",
"license": "Apache-2.0",
+ "types": "./dist/types/ts/index.d.ts",
+ "exports": {
+ ".": "./dist/index.mjs",
+ "./protos": "./dist/protos/collection.umd.js",
+ "./protos/*": "./dist/protos/*"
+ },
+ "bin": {
+ "pw_protobuf_compiler": "./dist/bin/pw_protobuf_compiler.js"
+ },
+ "scripts": {
+ "prebuild": "rimraf dist && npm run build-protos",
+ "build-protos": "ts-node -P tsconfig.json ts/buildprotos.ts",
+ "build": "npm run rollup",
+ "dev": "rollup -c -w",
+ "rollup": "rollup -c && npm run postbuild",
+ "postbuild": "rimraf dist/protos/types && chmod +x ./dist/bin/pw_protobuf_compiler.js",
+ "start": "tsc-watch --onSuccess \"rollup -c\"",
+ "check": "gts check",
+ "fix": "gts fix",
+ "test": "npm run build && jest --silent",
+ "jest": "jest"
+ },
"devDependencies": {
- "@bazel/concatjs": "4.1.0",
- "@bazel/esbuild": "4.1.0",
- "@bazel/ibazel": "^0.15.10",
- "@bazel/jasmine": "4.1.0",
- "@bazel/rollup": "4.1.0",
- "@bazel/typescript": "4.1.0",
"@grpc/grpc-js": "^1.3.7",
"@material-ui/core": "^4.12.1",
"@material-ui/lab": "^4.0.0-alpha.60",
"@rollup/plugin-commonjs": "^19.0.0",
- "@rollup/plugin-node-resolve": "^13.0.0",
- "@types/argparse": "^2.0.10",
+ "@rollup/plugin-node-resolve": "^13.3.0",
+ "@rollup/plugin-typescript": "^8.3.3",
"@types/crc": "^3.4.0",
"@types/google-protobuf": "^3.15.5",
- "@types/jasmine": "^3.7.8",
+ "@types/jest": "^28.1.4",
"@types/node": "^16.0.1",
"@types/react": "^17.0.14",
"@types/react-dom": "^17.0.9",
- "argparse": "^2.0.1",
+ "ansi_up": "^5.1.0",
+ "arg": "^5.0.2",
"base64-js": "^1.5.1",
"buffer": "^6.0.3",
- "crc": "^3.8.0",
+ "crc": "^4.1.1",
"debug": "^4.3.2",
"eslint": "^7.30.0",
"eslint-plugin-react": "^7.24.0",
- "google-protobuf": "^3.17.3",
"grpc-tools": "^1.11.2",
"grpc-web": "^1.2.1",
"gts": "^3.1.0",
+ "html-react-parser": "^1.4.0",
"http-server": "^13.0.2",
"install-peers": "^1.0.3",
- "jasmine": "^3.8.0",
- "jasmine-core": "^3.8.0",
- "karma": "6.3.4",
- "karma-chrome-launcher": "^3.1.0",
- "karma-firefox-launcher": "^2.1.1",
- "karma-jasmine": "^4.0.1",
- "karma-junit-reporter": "^2.0.1",
- "karma-requirejs": "^1.1.0",
- "karma-sourcemap-loader": "^0.3.8",
+ "jest-environment-jsdom": "^28.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"requirejs": "^2.3.6",
+ "rimraf": "^3.0.2",
"rollup": "^2.52.8",
+ "rollup-plugin-dts": "^4.2.2",
"rollup-plugin-node-builtins": "^2.1.2",
"rollup-plugin-node-globals": "^1.4.0",
+ "rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-sourcemaps": "^0.6.3",
"rxjs": "^7.2.0",
"tmp": "0.2.1",
- "ts-protoc-gen": "^0.15.0",
- "typescript": "^4.3.5",
- "wait-queue": "^1.1.4"
- },
- "scripts": {
- "check": "gts check",
- "fix": "gts fix"
+ "ts-jest": "^28.0.5",
+ "ts-node": "^10.8.1",
+ "tsc-watch": "^5.0.3",
+ "tslib": "^2.4.0",
+ "typescript": "^4.3.5"
},
"dependencies": {
- "ansi_up": "^5.1.0",
- "html-react-parser": "^1.4.0"
- }
+ "@protobuf-ts/protoc": "^2.7.0",
+ "google-protobuf": "^3.17.3",
+ "long": "^5.2.1",
+ "object-path": "^0.11.8",
+ "ts-protoc-gen": "^0.15.0"
+ },
+ "config": {
+ "protocVersion": "3.17.3"
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ]
}
diff --git a/pigweed.json b/pigweed.json
new file mode 100644
index 000000000..7a4623e5d
--- /dev/null
+++ b/pigweed.json
@@ -0,0 +1,9 @@
+{
+ "pw": {
+ "pw_presubmit": {
+ "format": {
+ "python_formatter": "black"
+ }
+ }
+ }
+}
diff --git a/pw_alignment/BUILD.bazel b/pw_alignment/BUILD.bazel
new file mode 100644
index 000000000..298605fc9
--- /dev/null
+++ b/pw_alignment/BUILD.bazel
@@ -0,0 +1,26 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+pw_cc_library(
+ name = "pw_alignment",
+ hdrs = ["public/pw_alignment/alignment.h"],
+ includes = ["public"],
+)
diff --git a/pw_alignment/BUILD.gn b/pw_alignment/BUILD.gn
new file mode 100644
index 000000000..3ea208fce
--- /dev/null
+++ b/pw_alignment/BUILD.gn
@@ -0,0 +1,36 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("pw_alignment") {
+ public = [ "public/pw_alignment/alignment.h" ]
+ public_configs = [ ":public_include_path" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_alignment/docs.rst b/pw_alignment/docs.rst
new file mode 100644
index 000000000..8d4958db1
--- /dev/null
+++ b/pw_alignment/docs.rst
@@ -0,0 +1,42 @@
+.. _module-pw_alignment:
+
+============
+pw_alignment
+============
+This module defines an interface for ensuring natural alignment of objects.
+
+Avoiding Atomic Libcalls
+========================
+The main motivation is to provide a portable way for
+preventing the compiler from emitting libcalls to builtin atomics
+functions and instead use native atomic instructions. This is especially
+useful for any pigweed user that uses ``std::atomic``.
+
+Take for example `std::atomic<std::optional<bool>>`. Accessing the underlying object
+would normally involve a call to a builtin `__atomic_*` function provided by a builtins
+library. However, if the compiler can determine that the size of the object is the same
+as its alignment, then it can replace a libcall to `__atomic_*` with native instructions.
+
+There can be certain situations where a compiler might not be able to assert this.
+Depending on the implementation of `std::optional<bool>`, this object could
+have a size of 2 bytes but an alignment of 1 byte which wouldn't satisfy the constraint.
+
+Basic usage
+-----------
+`pw_alignment` provides a wrapper class `pw::NaturallyAligned` for enforcing natural alignment without any
+changes to how the object is used. Since this is commonly used with atomics, an
+aditional class `pw::AlignedAtomic` is provided for simplifying things.
+
+.. code-block:: c++
+
+ // Passing a `std::optional<bool>` to `__atomic_exchange` might not replace the call
+ // with native instructions if the compiler cannot determine all instances of this object
+ // will happen to be aligned.
+ std::atomic<std::optional<bool>> maybe_nat_aligned_obj;
+
+ // `pw::NaturallyAligned<...>` forces the object to be aligned to its size, so passing
+ // it to `__atomic_exchange` will result in a replacement with native instructions.
+ std::atomic<pw::NaturallyAligned<std::optional<bool>>> nat_aligned_obj;
+
+ // Shorter spelling for the same as above.
+ std::AlignedAtomic<std::optional<bool>> also_nat_aligned_obj;
diff --git a/pw_alignment/public/pw_alignment/alignment.h b/pw_alignment/public/pw_alignment/alignment.h
new file mode 100644
index 000000000..e295c5d0c
--- /dev/null
+++ b/pw_alignment/public/pw_alignment/alignment.h
@@ -0,0 +1,84 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// todo-check: ignore
+// TODO(fxbug.dev/120998): Once this bug is addressed, this module can likely
+// be removed and we could just inline the using statements.
+
+#include <atomic>
+#include <limits>
+#include <type_traits>
+
+namespace pw {
+
+#if __cplusplus >= 202002L
+using bit_ceil = std::bit_ceil;
+#else
+constexpr size_t countl_zero(size_t x) noexcept {
+ size_t size_digits = std::numeric_limits<size_t>::digits;
+
+ if (sizeof(x) <= sizeof(unsigned int))
+ return __builtin_clz(static_cast<unsigned int>(x)) -
+ (std::numeric_limits<unsigned int>::digits - size_digits);
+
+ if (sizeof(x) <= sizeof(unsigned long))
+ return __builtin_clzl(static_cast<unsigned long>(x)) -
+ (std::numeric_limits<unsigned long>::digits - size_digits);
+
+ static_assert(sizeof(x) <= sizeof(unsigned long long));
+ return __builtin_clzll(static_cast<unsigned long long>(x)) -
+ (std::numeric_limits<unsigned long long>::digits - size_digits);
+}
+
+constexpr size_t bit_width(size_t x) noexcept {
+ return std::numeric_limits<size_t>::digits - countl_zero(x);
+}
+
+constexpr size_t bit_ceil(size_t x) noexcept {
+ if (x == 0)
+ return 1;
+ return size_t{1} << bit_width(size_t{x - 1});
+}
+#endif
+
+// The NaturallyAligned class is a wrapper class for ensuring the object is
+// aligned to a power of 2 bytes greater than or equal to its size.
+template <typename T>
+struct [[gnu::aligned(bit_ceil(sizeof(T)))]] NaturallyAligned
+ : public T{NaturallyAligned() : T(){} NaturallyAligned(const T& t) :
+ T(t){} template <class U>
+ NaturallyAligned(const U& u) : T(u){} NaturallyAligned
+ operator=(T other){return T::operator=(other);
+} // namespace pw
+}
+;
+
+// This is a convenience wrapper for ensuring the object held by std::atomic is
+// naturally aligned. Ensuring the underlying objects's alignment is natural
+// allows clang to replace libcalls to atomic functions
+// (__atomic_load/store/exchange/etc) with native instructions when appropriate.
+//
+// Example usage:
+//
+// // Here std::optional<bool> has a size of 2 but alignment of 1, which would
+// // normally lower to an __atomic_* libcall, but pw::NaturallyAligned in
+// // std::atomic tells the compiler to align the object to 2 bytes, which
+// // satisfies the requirements for replacing __atomic_* with instructions.
+// pw::AlignedAtomic<std::optional<bool>> mute_enable{};
+//
+template <typename T>
+using AlignedAtomic = std::atomic<NaturallyAligned<T>>;
+
+} // namespace pw
diff --git a/pw_allocator/BUILD.gn b/pw_allocator/BUILD.gn
index c25bb4c12..f335199a4 100644
--- a/pw_allocator/BUILD.gn
+++ b/pw_allocator/BUILD.gn
@@ -45,8 +45,9 @@ pw_source_set("block") {
configs = [ ":enable_heap_poison" ]
public = [ "public/pw_allocator/block.h" ]
public_deps = [
- "$dir_pw_assert",
- "$dir_pw_status",
+ dir_pw_assert,
+ dir_pw_span,
+ dir_pw_status,
]
sources = [ "block.cc" ]
}
@@ -57,7 +58,8 @@ pw_source_set("freelist") {
public = [ "public/pw_allocator/freelist.h" ]
public_deps = [
"$dir_pw_containers:vector",
- "$dir_pw_status",
+ dir_pw_span,
+ dir_pw_status,
]
sources = [ "freelist.cc" ]
}
@@ -71,8 +73,9 @@ pw_source_set("freelist_heap") {
":freelist",
]
deps = [
- "$dir_pw_assert",
- "$dir_pw_log",
+ dir_pw_assert,
+ dir_pw_log,
+ dir_pw_span,
]
sources = [ "freelist_heap.cc" ]
}
@@ -87,13 +90,20 @@ pw_test_group("tests") {
pw_test("block_test") {
configs = [ ":enable_heap_poison" ]
- deps = [ ":block" ]
+ deps = [
+ ":block",
+ dir_pw_span,
+ ]
sources = [ "block_test.cc" ]
}
pw_test("freelist_test") {
configs = [ ":enable_heap_poison" ]
- deps = [ ":freelist" ]
+ deps = [
+ ":freelist",
+ dir_pw_span,
+ dir_pw_status,
+ ]
sources = [ "freelist_test.cc" ]
}
diff --git a/pw_allocator/CMakeLists.txt b/pw_allocator/CMakeLists.txt
new file mode 100644
index 000000000..8a4d7b491
--- /dev/null
+++ b/pw_allocator/CMakeLists.txt
@@ -0,0 +1,90 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_allocator.block STATIC
+ HEADERS
+ public/pw_allocator/block.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_span
+ pw_status
+ SOURCES
+ block.cc
+)
+
+pw_add_library(pw_allocator.freelist STATIC
+ HEADERS
+ public/pw_allocator/freelist.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_containers.vector
+ pw_span
+ pw_status
+ SOURCES
+ freelist.cc
+)
+
+pw_add_library(pw_allocator.freelist_heap STATIC
+ HEADERS
+ public/pw_allocator/freelist_heap.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_allocator.block
+ pw_allocator.freelist
+ PRIVATE_DEPS
+ pw_assert
+ pw_log
+ pw_span
+ SOURCES
+ freelist_heap.cc
+)
+
+pw_add_test(pw_allocator.block_test
+ SOURCES
+ block_test.cc
+ PRIVATE_DEPS
+ pw_allocator.block
+ pw_span
+ GROUPS
+ modules
+ pw_allocator
+)
+
+pw_add_test(pw_allocator.freelist_test
+ SOURCES
+ freelist_test.cc
+ PRIVATE_DEPS
+ pw_allocator.freelist
+ pw_span
+ pw_status
+ GROUPS
+ modules
+ pw_allocator
+)
+
+pw_add_test(pw_allocator.freelist_heap_test
+ SOURCES
+ freelist_heap_test.cc
+ PRIVATE_DEPS
+ pw_allocator.freelist_heap
+ GROUPS
+ modules
+ pw_allocator
+)
diff --git a/pw_allocator/block.cc b/pw_allocator/block.cc
index c673eb55c..ec0afc22a 100644
--- a/pw_allocator/block.cc
+++ b/pw_allocator/block.cc
@@ -17,10 +17,11 @@
#include <cstring>
#include "pw_assert/check.h"
+#include "pw_span/span.h"
namespace pw::allocator {
-Status Block::Init(const std::span<std::byte> region, Block** block) {
+Status Block::Init(const span<std::byte> region, Block** block) {
// Ensure the region we're given is aligned and sized accordingly
if (reinterpret_cast<uintptr_t>(region.data()) % alignof(Block) != 0) {
return Status::InvalidArgument();
@@ -40,7 +41,8 @@ Status Block::Init(const std::span<std::byte> region, Block** block) {
// with the following storage. Since the space between this block and the
// next are implicitly part of the raw data, size can be computed by
// subtracting the pointers.
- aliased.block->next_ = reinterpret_cast<Block*>(region.end());
+ aliased.block->next_ =
+ reinterpret_cast<Block*>(region.data() + region.size_bytes());
aliased.block->MarkLast();
aliased.block->prev_ = nullptr;
@@ -57,7 +59,7 @@ Status Block::Split(size_t head_block_inner_size, Block** new_block) {
}
// Don't split used blocks.
- // TODO: Relax this restriction? Flag to enable/disable this check?
+ // TODO(jgarside): Relax this restriction? Flag to enable/disable this check?
if (Used()) {
return Status::FailedPrecondition();
}
@@ -80,7 +82,7 @@ Status Block::Split(size_t head_block_inner_size, Block** new_block) {
}
// (2) Does the resulting block have enough space to store the header?
- // TODO: What to do if the returned section is empty (i.e. remaining
+ // TODO(jgarside): What to do if the returned section is empty (i.e. remaining
// size == sizeof(Block))?
if (InnerSize() - aligned_head_block_inner_size <
sizeof(Block) + 2 * PW_ALLOCATOR_POISON_OFFSET) {
@@ -160,34 +162,37 @@ Status Block::MergePrev() {
return prev_->MergeNext();
}
-// TODO(pwbug/234): Add stack tracing to locate which call to the heap operation
-// caused the corruption.
-// TODO: Add detailed information to log report and leave succinct messages
-// in the crash message.
+// TODO(b/234875269): Add stack tracing to locate which call to the heap
+// operation caused the corruption.
+// TODO(jgarside): Add detailed information to log report and leave succinct
+// messages in the crash message.
void Block::CrashIfInvalid() {
switch (CheckStatus()) {
case VALID:
break;
case MISALIGNED:
- PW_DCHECK(false, "The block at address %p is not aligned.", this);
+ PW_DCHECK(false,
+ "The block at address %p is not aligned.",
+ static_cast<void*>(this));
break;
case NEXT_MISMATCHED:
PW_DCHECK(false,
"The 'prev' field in the next block (%p) does not match the "
"address of the current block (%p).",
- Next()->Prev(),
- this);
+ static_cast<void*>(Next()->Prev()),
+ static_cast<void*>(this));
break;
case PREV_MISMATCHED:
PW_DCHECK(false,
"The 'next' field in the previous block (%p) does not match "
"the address of the current block (%p).",
- Prev()->Next(),
- this);
+ static_cast<void*>(Prev()->Next()),
+ static_cast<void*>(this));
break;
case POISON_CORRUPTED:
- PW_DCHECK(
- false, "The poisoned pattern in the block at %p is corrupted.", this);
+ PW_DCHECK(false,
+ "The poisoned pattern in the block at %p is corrupted.",
+ static_cast<void*>(this));
break;
}
}
diff --git a/pw_allocator/block_test.cc b/pw_allocator/block_test.cc
index e397314e9..06772bd99 100644
--- a/pw_allocator/block_test.cc
+++ b/pw_allocator/block_test.cc
@@ -15,9 +15,9 @@
#include "pw_allocator/block.h"
#include <cstring>
-#include <span>
#include "gtest/gtest.h"
+#include "pw_span/span.h"
using std::byte;
@@ -28,7 +28,7 @@ TEST(Block, CanCreateSingleBlock) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- auto status = Block::Init(std::span(bytes, kN), &block);
+ auto status = Block::Init(span(bytes, kN), &block);
ASSERT_EQ(status, OkStatus());
EXPECT_EQ(block->OuterSize(), kN);
@@ -48,7 +48,7 @@ TEST(Block, CannotCreateUnalignedSingleBlock) {
byte* byte_ptr = bytes;
Block* block = nullptr;
- auto status = Block::Init(std::span(byte_ptr + 1, kN - 1), &block);
+ auto status = Block::Init(span(byte_ptr + 1, kN - 1), &block);
EXPECT_EQ(status, Status::InvalidArgument());
}
@@ -57,7 +57,7 @@ TEST(Block, CannotCreateTooSmallBlock) {
constexpr size_t kN = 2;
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- auto status = Block::Init(std::span(bytes, kN), &block);
+ auto status = Block::Init(span(bytes, kN), &block);
EXPECT_EQ(status, Status::InvalidArgument());
}
@@ -68,7 +68,7 @@ TEST(Block, CanSplitBlock) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
auto status = block->Split(kSplitN, &next_block);
@@ -101,7 +101,7 @@ TEST(Block, CanSplitBlockUnaligned) {
uintptr_t split_len = split_addr - (uintptr_t)&bytes;
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
auto status = block->Split(kSplitN, &next_block);
@@ -133,15 +133,13 @@ TEST(Block, CanSplitMidBlock) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* block2 = nullptr;
- block->Split(kSplit1, &block2)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), block->Split(kSplit1, &block2));
Block* block3 = nullptr;
- block->Split(kSplit2, &block3)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), block->Split(kSplit2, &block3));
EXPECT_EQ(block->Next(), block3);
EXPECT_EQ(block3->Next(), block2);
@@ -156,7 +154,7 @@ TEST(Block, CannotSplitBlockWithoutHeaderSpace) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
auto status = block->Split(kSplitN, &next_block);
@@ -171,7 +169,7 @@ TEST(Block, MustProvideNextBlockPointer) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
auto status = block->Split(kSplitN, nullptr);
EXPECT_EQ(status, Status::InvalidArgument());
@@ -183,7 +181,7 @@ TEST(Block, CannotMakeBlockLargerInSplit) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
auto status = block->Split(block->InnerSize() + 1, &next_block);
@@ -197,7 +195,7 @@ TEST(Block, CannotMakeSecondBlockLargerInSplit) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
auto status = block->Split(
@@ -214,7 +212,7 @@ TEST(Block, CanMakeZeroSizeFirstBlock) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
auto status = block->Split(0, &next_block);
@@ -229,7 +227,7 @@ TEST(Block, CanMakeZeroSizeSecondBlock) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
auto status = block->Split(
@@ -245,7 +243,7 @@ TEST(Block, CanMarkBlockUsed) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
block->MarkUsed();
EXPECT_EQ(block->Used(), true);
@@ -263,7 +261,7 @@ TEST(Block, CannotSplitUsedBlock) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
block->MarkUsed();
@@ -281,15 +279,13 @@ TEST(Block, CanMergeWithNextBlock) {
alignas(Block*) byte bytes[kN];
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* block2 = nullptr;
- block->Split(kSplit1, &block2)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), block->Split(kSplit1, &block2));
Block* block3 = nullptr;
- block->Split(kSplit2, &block3)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), block->Split(kSplit2, &block3));
EXPECT_EQ(block3->MergeNext(), OkStatus());
@@ -311,11 +307,10 @@ TEST(Block, CannotMergeWithFirstOrLastBlock) {
// Do a split, just to check that the checks on Next/Prev are
// different...
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
- block->Split(512, &next_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), block->Split(512, &next_block));
EXPECT_EQ(next_block->MergeNext(), Status::OutOfRange());
EXPECT_EQ(block->MergePrev(), Status::OutOfRange());
@@ -328,11 +323,10 @@ TEST(Block, CannotMergeUsedBlock) {
// Do a split, just to check that the checks on Next/Prev are
// different...
Block* block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &block), OkStatus());
Block* next_block = nullptr;
- block->Split(512, &next_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), block->Split(512, &next_block));
block->MarkUsed();
EXPECT_EQ(block->MergeNext(), Status::FailedPrecondition());
@@ -344,15 +338,13 @@ TEST(Block, CanCheckValidBlock) {
alignas(Block*) byte bytes[kN];
Block* first_block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &first_block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &first_block), OkStatus());
Block* second_block = nullptr;
- first_block->Split(512, &second_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), first_block->Split(512, &second_block));
Block* third_block = nullptr;
- second_block->Split(256, &third_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), second_block->Split(256, &third_block));
EXPECT_EQ(first_block->IsValid(), true);
EXPECT_EQ(second_block->IsValid(), true);
@@ -364,19 +356,16 @@ TEST(Block, CanCheckInalidBlock) {
alignas(Block*) byte bytes[kN];
Block* first_block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &first_block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &first_block), OkStatus());
Block* second_block = nullptr;
- first_block->Split(512, &second_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), first_block->Split(512, &second_block));
Block* third_block = nullptr;
- second_block->Split(256, &third_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), second_block->Split(256, &third_block));
Block* fourth_block = nullptr;
- third_block->Split(128, &fourth_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), third_block->Split(128, &fourth_block));
std::byte* next_ptr = reinterpret_cast<std::byte*>(first_block);
memcpy(next_ptr, second_block, sizeof(void*));
@@ -405,15 +394,13 @@ TEST(Block, CanPoisonBlock) {
alignas(Block*) byte bytes[kN];
Block* first_block = nullptr;
- EXPECT_EQ(Block::Init(std::span(bytes, kN), &first_block), OkStatus());
+ EXPECT_EQ(Block::Init(span(bytes, kN), &first_block), OkStatus());
Block* second_block = nullptr;
- first_block->Split(512, &second_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), first_block->Split(512, &second_block));
Block* third_block = nullptr;
- second_block->Split(256, &third_block)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), second_block->Split(256, &third_block));
EXPECT_EQ(first_block->IsValid(), true);
EXPECT_EQ(second_block->IsValid(), true);
diff --git a/pw_allocator/freelist.cc b/pw_allocator/freelist.cc
index 03b514da6..d14d3bd27 100644
--- a/pw_allocator/freelist.cc
+++ b/pw_allocator/freelist.cc
@@ -16,7 +16,7 @@
namespace pw::allocator {
-Status FreeList::AddChunk(std::span<std::byte> chunk) {
+Status FreeList::AddChunk(span<std::byte> chunk) {
// Check that the size is enough to actually store what we need
if (chunk.size() < sizeof(FreeListNode)) {
return Status::OutOfRange();
@@ -29,7 +29,7 @@ Status FreeList::AddChunk(std::span<std::byte> chunk) {
aliased.bytes = chunk.data();
- size_t chunk_ptr = FindChunkPtrForSize(chunk.size(), false);
+ unsigned short chunk_ptr = FindChunkPtrForSize(chunk.size(), false);
// Add it to the correct list.
aliased.node->size = chunk.size();
@@ -39,17 +39,17 @@ Status FreeList::AddChunk(std::span<std::byte> chunk) {
return OkStatus();
}
-std::span<std::byte> FreeList::FindChunk(size_t size) const {
+span<std::byte> FreeList::FindChunk(size_t size) const {
if (size == 0) {
- return std::span<std::byte>();
+ return span<std::byte>();
}
- size_t chunk_ptr = FindChunkPtrForSize(size, true);
+ unsigned short chunk_ptr = FindChunkPtrForSize(size, true);
// Check that there's data. This catches the case where we run off the
// end of the array
if (chunks_[chunk_ptr] == nullptr) {
- return std::span<std::byte>();
+ return span<std::byte>();
}
// Now iterate up the buckets, walking each list to find a good candidate
@@ -58,11 +58,11 @@ std::span<std::byte> FreeList::FindChunk(size_t size) const {
FreeListNode* node;
std::byte* data;
} aliased;
- aliased.node = chunks_[i];
+ aliased.node = chunks_[static_cast<unsigned short>(i)];
while (aliased.node != nullptr) {
if (aliased.node->size >= size) {
- return std::span<std::byte>(aliased.data, aliased.node->size);
+ return span<std::byte>(aliased.data, aliased.node->size);
}
aliased.node = aliased.node->next;
@@ -71,11 +71,11 @@ std::span<std::byte> FreeList::FindChunk(size_t size) const {
// If we get here, we've checked every block in every bucket. There's
// nothing that can support this allocation.
- return std::span<std::byte>();
+ return span<std::byte>();
}
-Status FreeList::RemoveChunk(std::span<std::byte> chunk) {
- size_t chunk_ptr = FindChunkPtrForSize(chunk.size(), true);
+Status FreeList::RemoveChunk(span<std::byte> chunk) {
+ unsigned short chunk_ptr = FindChunkPtrForSize(chunk.size(), true);
// Walk that list, finding the chunk.
union {
@@ -112,9 +112,9 @@ Status FreeList::RemoveChunk(std::span<std::byte> chunk) {
return Status::NotFound();
}
-size_t FreeList::FindChunkPtrForSize(size_t size, bool non_null) const {
- size_t chunk_ptr = 0;
- for (chunk_ptr = 0; chunk_ptr < sizes_.size(); chunk_ptr++) {
+unsigned short FreeList::FindChunkPtrForSize(size_t size, bool non_null) const {
+ unsigned short chunk_ptr = 0;
+ for (chunk_ptr = 0u; chunk_ptr < sizes_.size(); chunk_ptr++) {
if (sizes_[chunk_ptr] >= size &&
(!non_null || chunks_[chunk_ptr] != nullptr)) {
break;
diff --git a/pw_allocator/freelist_heap.cc b/pw_allocator/freelist_heap.cc
index 1dacf2d5d..fcb8f3c50 100644
--- a/pw_allocator/freelist_heap.cc
+++ b/pw_allocator/freelist_heap.cc
@@ -21,7 +21,7 @@
namespace pw::allocator {
-FreeListHeap::FreeListHeap(std::span<std::byte> region, FreeList& freelist)
+FreeListHeap::FreeListHeap(span<std::byte> region, FreeList& freelist)
: freelist_(freelist), heap_stats_() {
Block* block;
PW_CHECK_OK(
@@ -29,7 +29,7 @@ FreeListHeap::FreeListHeap(std::span<std::byte> region, FreeList& freelist)
"Failed to initialize FreeListHeap region; misaligned or too small");
freelist_.AddChunk(BlockToSpan(block))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
region_ = region;
heap_stats_.total_bytes = region.size();
@@ -44,7 +44,7 @@ void* FreeListHeap::Allocate(size_t size) {
return nullptr;
}
freelist_.RemoveChunk(chunk)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
Block* chunk_block = Block::FromUsableSpace(chunk.data());
@@ -55,7 +55,7 @@ void* FreeListHeap::Allocate(size_t size) {
auto status = chunk_block->Split(size, &leftover);
if (status == PW_STATUS_OK) {
freelist_.AddChunk(BlockToSpan(leftover))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
chunk_block->MarkUsed();
@@ -96,9 +96,9 @@ void FreeListHeap::Free(void* ptr) {
if (prev != nullptr && !prev->Used()) {
// Remove from freelist and merge
freelist_.RemoveChunk(BlockToSpan(prev))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
chunk_block->MergePrev()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
// chunk_block is now invalid; prev now encompasses it.
chunk_block = prev;
@@ -106,13 +106,13 @@ void FreeListHeap::Free(void* ptr) {
if (next != nullptr && !next->Used()) {
freelist_.RemoveChunk(BlockToSpan(next))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
chunk_block->MergeNext()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
// Add back to the freelist
freelist_.AddChunk(BlockToSpan(chunk_block))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
heap_stats_.bytes_allocated -= size_freed;
heap_stats_.cumulative_freed += size_freed;
@@ -147,7 +147,7 @@ void* FreeListHeap::Realloc(void* ptr, size_t size) {
// Do nothing and return ptr if the required memory size is smaller than
// the current size.
- // TODO: Currently do not support shrink of memory chunk.
+ // TODO(keir): Currently do not support shrink of memory chunk.
if (old_size >= size) {
return ptr;
}
@@ -192,8 +192,8 @@ void FreeListHeap::LogHeapStats() {
PW_LOG_INFO(" ");
}
-// TODO: Add stack tracing to locate which call to the heap operation caused
-// the corruption.
+// TODO(keir): Add stack tracing to locate which call to the heap operation
+// caused the corruption.
void FreeListHeap::InvalidFreeCrash() {
PW_DCHECK(false, "You tried to free an invalid pointer!");
}
diff --git a/pw_allocator/freelist_heap_test.cc b/pw_allocator/freelist_heap_test.cc
index 469e7d974..0d3353c3b 100644
--- a/pw_allocator/freelist_heap_test.cc
+++ b/pw_allocator/freelist_heap_test.cc
@@ -14,9 +14,8 @@
#include "pw_allocator/freelist_heap.h"
-#include <span>
-
#include "gtest/gtest.h"
+#include "pw_span/span.h"
namespace pw::allocator {
diff --git a/pw_allocator/freelist_test.cc b/pw_allocator/freelist_test.cc
index 45d1d353f..eea832a2b 100644
--- a/pw_allocator/freelist_test.cc
+++ b/pw_allocator/freelist_test.cc
@@ -14,9 +14,8 @@
#include "pw_allocator/freelist.h"
-#include <span>
-
#include "gtest/gtest.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
using std::byte;
@@ -42,7 +41,7 @@ TEST(FreeList, CanRetrieveAddedMember) {
byte data[kN] = {std::byte(0)};
- auto status = list.AddChunk(std::span(data, kN));
+ auto status = list.AddChunk(span(data, kN));
EXPECT_EQ(status, OkStatus());
auto item = list.FindChunk(kN);
@@ -56,8 +55,7 @@ TEST(FreeList, CanRetrieveAddedMemberForSmallerSize) {
byte data[kN] = {std::byte(0)};
- list.AddChunk(std::span(data, kN))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data, kN)));
auto item = list.FindChunk(kN / 2);
EXPECT_EQ(item.size(), kN);
EXPECT_EQ(item.data(), data);
@@ -69,9 +67,8 @@ TEST(FreeList, CanRemoveItem) {
byte data[kN] = {std::byte(0)};
- list.AddChunk(std::span(data, kN))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- auto status = list.RemoveChunk(std::span(data, kN));
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data, kN)));
+ auto status = list.RemoveChunk(span(data, kN));
EXPECT_EQ(status, OkStatus());
auto item = list.FindChunk(kN);
@@ -86,10 +83,8 @@ TEST(FreeList, FindReturnsSmallestChunk) {
byte data1[kN1] = {std::byte(0)};
byte data2[kN2] = {std::byte(0)};
- list.AddChunk(std::span(data1, kN1))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- list.AddChunk(std::span(data2, kN2))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data1, kN1)));
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data2, kN2)));
auto chunk = list.FindChunk(kN1 / 2);
EXPECT_EQ(chunk.size(), kN1);
@@ -115,10 +110,8 @@ TEST(FreeList, FindReturnsCorrectChunkInSameBucket) {
byte data2[kN2] = {std::byte(0)};
// List should now be 257 -> 512 -> NULL
- list.AddChunk(std::span(data1, kN1))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- list.AddChunk(std::span(data2, kN2))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data1, kN1)));
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data2, kN2)));
auto chunk = list.FindChunk(kN2 + 1);
EXPECT_EQ(chunk.size(), kN1);
@@ -137,10 +130,8 @@ TEST(FreeList, FindCanMoveUpThroughBuckets) {
// List should now be:
// bkt[3] (257 bytes up to 512 bytes) -> 257 -> NULL
// bkt[4] (513 bytes up to 1024 bytes) -> 513 -> NULL
- list.AddChunk(std::span(data1, kN1))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- list.AddChunk(std::span(data2, kN2))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data1, kN1)));
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data2, kN2)));
// Request a 300 byte chunk. This should return the 513 byte one
auto chunk = list.FindChunk(kN1 + 1);
@@ -154,9 +145,8 @@ TEST(FreeList, RemoveUnknownChunkReturnsNotFound) {
byte data[kN] = {std::byte(0)};
byte data2[kN] = {std::byte(0)};
- list.AddChunk(std::span(data, kN))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- auto status = list.RemoveChunk(std::span(data2, kN));
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data, kN)));
+ auto status = list.RemoveChunk(span(data2, kN));
EXPECT_EQ(status, Status::NotFound());
}
@@ -167,17 +157,13 @@ TEST(FreeList, CanStoreMultipleChunksPerBucket) {
byte data1[kN] = {std::byte(0)};
byte data2[kN] = {std::byte(0)};
- list.AddChunk(std::span(data1, kN))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- list.AddChunk(std::span(data2, kN))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data1, kN)));
+ ASSERT_EQ(OkStatus(), list.AddChunk(span(data2, kN)));
auto chunk1 = list.FindChunk(kN);
- list.RemoveChunk(chunk1)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), list.RemoveChunk(chunk1));
auto chunk2 = list.FindChunk(kN);
- list.RemoveChunk(chunk2)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), list.RemoveChunk(chunk2));
// Ordering of the chunks doesn't matter
EXPECT_TRUE(chunk1.data() != chunk2.data());
diff --git a/pw_allocator/public/pw_allocator/block.h b/pw_allocator/public/pw_allocator/block.h
index 3edc92c3b..09b08b74f 100644
--- a/pw_allocator/public/pw_allocator/block.h
+++ b/pw_allocator/public/pw_allocator/block.h
@@ -16,8 +16,9 @@
// usable.
#pragma once
-#include <span>
+#include <cstdint>
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::allocator {
@@ -91,7 +92,7 @@ class Block final {
// Returns:
// INVALID_ARGUMENT if the given region is unaligned to too small, or
// OK otherwise.
- static Status Init(const std::span<std::byte> region, Block** block);
+ static Status Init(const span<std::byte> region, Block** block);
// Returns a pointer to a Block, given a pointer to the start of the usable
// space inside the block (i.e. the opposite operation to UsableSpace()). In
diff --git a/pw_allocator/public/pw_allocator/freelist.h b/pw_allocator/public/pw_allocator/freelist.h
index e10692aac..a5a50416e 100644
--- a/pw_allocator/public/pw_allocator/freelist.h
+++ b/pw_allocator/public/pw_allocator/freelist.h
@@ -14,9 +14,9 @@
#pragma once
#include <array>
-#include <span>
#include "pw_containers/vector.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::allocator {
@@ -64,31 +64,31 @@ class FreeList {
// OK: The chunk was added successfully
// OUT_OF_RANGE: The chunk could not be added for size reasons (e.g. if
// the chunk is too small to store the FreeListNode).
- Status AddChunk(std::span<std::byte> chunk);
+ Status AddChunk(span<std::byte> chunk);
// Finds an eligible chunk for an allocation of size `size`. Note that this
// will return the first allocation possible within a given bucket, it does
- // not currently optimize for finding the smallest chunk. Returns a std::span
+ // not currently optimize for finding the smallest chunk. Returns a span
// representing the chunk. This will be "valid" on success, and will have size
// = 0 on failure (if there were no chunks available for that allocation).
- std::span<std::byte> FindChunk(size_t size) const;
+ span<std::byte> FindChunk(size_t size) const;
// Remove a chunk from this freelist. Returns:
// OK: The chunk was removed successfully
// NOT_FOUND: The chunk could not be found in this freelist.
- Status RemoveChunk(std::span<std::byte> chunk);
+ Status RemoveChunk(span<std::byte> chunk);
private:
// For a given size, find which index into chunks_ the node should be written
// to.
- size_t FindChunkPtrForSize(size_t size, bool non_null) const;
+ unsigned short FindChunkPtrForSize(size_t size, bool non_null) const;
private:
template <size_t kNumBuckets>
friend class FreeListBuffer;
struct FreeListNode {
- // TODO: Double-link this? It'll make removal easier/quicker.
+ // TODO(jgarside): Double-link this? It'll make removal easier/quicker.
FreeListNode* next;
size_t size;
};
diff --git a/pw_allocator/public/pw_allocator/freelist_heap.h b/pw_allocator/public/pw_allocator/freelist_heap.h
index 8073ec398..673302627 100644
--- a/pw_allocator/public/pw_allocator/freelist_heap.h
+++ b/pw_allocator/public/pw_allocator/freelist_heap.h
@@ -15,10 +15,10 @@
#pragma once
#include <cstddef>
-#include <span>
#include "pw_allocator/block.h"
#include "pw_allocator/freelist.h"
+#include "pw_span/span.h"
namespace pw::allocator {
@@ -34,7 +34,7 @@ class FreeListHeap {
size_t total_allocate_calls;
size_t total_free_calls;
};
- FreeListHeap(std::span<std::byte> region, FreeList& freelist);
+ FreeListHeap(span<std::byte> region, FreeList& freelist);
void* Allocate(size_t size);
void Free(void* ptr);
@@ -44,13 +44,13 @@ class FreeListHeap {
void LogHeapStats();
private:
- std::span<std::byte> BlockToSpan(Block* block) {
- return std::span<std::byte>(block->UsableSpace(), block->InnerSize());
+ span<std::byte> BlockToSpan(Block* block) {
+ return span<std::byte>(block->UsableSpace(), block->InnerSize());
}
void InvalidFreeCrash();
- std::span<std::byte> region_;
+ span<std::byte> region_;
FreeList& freelist_;
HeapStats heap_stats_;
};
@@ -61,7 +61,7 @@ class FreeListHeapBuffer {
static constexpr std::array<size_t, kNumBuckets> defaultBuckets{
16, 32, 64, 128, 256, 512};
- FreeListHeapBuffer(std::span<std::byte> region)
+ FreeListHeapBuffer(span<std::byte> region)
: freelist_(defaultBuckets), heap_(region, freelist_) {}
void* Allocate(size_t size) { return heap_.Allocate(size); }
@@ -71,7 +71,7 @@ class FreeListHeapBuffer {
const FreeListHeap::HeapStats& heap_stats() const {
return heap_.heap_stats_;
- };
+ }
void LogHeapStats() { heap_.LogHeapStats(); }
diff --git a/pw_allocator/py/BUILD.gn b/pw_allocator/py/BUILD.gn
index 8b4696adc..afd151d00 100644
--- a/pw_allocator/py/BUILD.gn
+++ b/pw_allocator/py/BUILD.gn
@@ -28,4 +28,5 @@ pw_python_package("py") {
]
python_deps = [ "$dir_pw_cli/py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_allocator/py/pw_allocator/heap_viewer.py b/pw_allocator/py/pw_allocator/heap_viewer.py
index 72da6ff1c..074fb2bc9 100644
--- a/pw_allocator/py/pw_allocator/heap_viewer.py
+++ b/pw_allocator/py/pw_allocator/heap_viewer.py
@@ -18,13 +18,14 @@ import sys
import math
import logging
from typing import Optional
-from dataclasses import dataclass
+from dataclasses import dataclass, field
import coloredlogs # type: ignore
@dataclass
class HeapBlock:
"""Building blocks for memory chunk allocated at heap."""
+
size: int
mem_offset: int
next: Optional['HeapBlock'] = None
@@ -33,7 +34,10 @@ class HeapBlock:
@dataclass
class HeapUsage:
"""Contains a linked list of allocated HeapBlocks."""
- begin: HeapBlock = HeapBlock(0, 0)
+
+ # Use a default_factory to avoid mutable default value. See
+ # https://docs.python.org/3/library/dataclasses.html#mutable-default-values
+ begin: HeapBlock = field(default_factory=lambda: HeapBlock(0, 0))
def add_block(self, block):
cur_block = self.begin.next
@@ -65,33 +69,46 @@ class HeapUsage:
def add_parser_arguments(parser):
- parser.add_argument('--dump-file',
- help=('dump file that contains a list of malloc and '
- 'free instructions. The format should be as '
- 'follows: "m <size> <address>" on a line for '
- 'each malloc called and "f <address>" on a line '
- 'for each free called.'),
- required=True)
-
- parser.add_argument('--heap-low-address',
- help=('lower address of the heap.'),
- type=lambda x: int(x, 0),
- required=True)
-
- parser.add_argument('--heap-high-address',
- help=('higher address of the heap.'),
- type=lambda x: int(x, 0),
- required=True)
-
- parser.add_argument('--poison-enabled',
- help=('if heap poison is enabled or not.'),
- default=False,
- action='store_true')
-
- parser.add_argument('--pointer-size',
- help=('size of pointer on the machine.'),
- default=4,
- type=lambda x: int(x, 0))
+ """Parse args."""
+ parser.add_argument(
+ '--dump-file',
+ help=(
+ 'dump file that contains a list of malloc and '
+ 'free instructions. The format should be as '
+ 'follows: "m <size> <address>" on a line for '
+ 'each malloc called and "f <address>" on a line '
+ 'for each free called.'
+ ),
+ required=True,
+ )
+
+ parser.add_argument(
+ '--heap-low-address',
+ help=('lower address of the heap.'),
+ type=lambda x: int(x, 0),
+ required=True,
+ )
+
+ parser.add_argument(
+ '--heap-high-address',
+ help=('higher address of the heap.'),
+ type=lambda x: int(x, 0),
+ required=True,
+ )
+
+ parser.add_argument(
+ '--poison-enabled',
+ help=('if heap poison is enabled or not.'),
+ default=False,
+ action='store_true',
+ )
+
+ parser.add_argument(
+ '--pointer-size',
+ help=('size of pointer on the machine.'),
+ default=4,
+ type=lambda x: int(x, 0),
+ )
_LEFT_HEADER_CHAR = '['
@@ -104,26 +121,31 @@ _LOG = logging.getLogger(__name__)
def _exit_due_to_file_not_found():
- _LOG.critical('Dump file location is not provided or dump file is not '
- 'found. Please specify a valid file in the argument.')
+ _LOG.critical(
+ 'Dump file location is not provided or dump file is not '
+ 'found. Please specify a valid file in the argument.'
+ )
sys.exit(1)
def _exit_due_to_bad_heap_info():
_LOG.critical(
'Heap low/high address is missing or invalid. Please put valid '
- 'addresses in the argument.')
+ 'addresses in the argument.'
+ )
sys.exit(1)
-def visualize(dump_file=None,
- heap_low_address=None,
- heap_high_address=None,
- poison_enabled=False,
- pointer_size=4):
+def visualize(
+ dump_file=None,
+ heap_low_address=None,
+ heap_high_address=None,
+ poison_enabled=False,
+ pointer_size=4,
+):
"""Visualization of heap usage."""
- # TODO(pwbug/236): Add standarized mechanisms to produce dump file and read
- # heap information from dump file.
+ # TODO(b/235282507): Add standarized mechanisms to produce dump file and
+ # read heap information from dump file.
aligned_bytes = pointer_size
header_size = pointer_size * 2
@@ -152,7 +174,8 @@ def visualize(dump_file=None,
# Add a HeapBlock when malloc is called
block = HeapBlock(
int(math.ceil(float(info[1]) / aligned_bytes)) * aligned_bytes,
- int(info[2], 0) - heap_low_address)
+ int(info[2], 0) - heap_low_address,
+ )
heap_visualizer.add_block(block)
elif info[0] == 'f':
# Remove the HeapBlock when free is called
@@ -174,38 +197,55 @@ def visualize(dump_file=None,
is_used = False
# Print overall heap information
- _LOG.info('%-40s%-40s', f'The heap starts at {hex(heap_low_address)}.',
- f'The heap ends at {hex(heap_high_address)}.')
- _LOG.info('%-40s%-40s', f'Heap size is {heap_size // 1024}k bytes.',
- f'Heap is aligned by {aligned_bytes} bytes.')
+ _LOG.info(
+ '%-40s%-40s',
+ f'The heap starts at {hex(heap_low_address)}.',
+ f'The heap ends at {hex(heap_high_address)}.',
+ )
+ _LOG.info(
+ '%-40s%-40s',
+ f'Heap size is {heap_size // 1024}k bytes.',
+ f'Heap is aligned by {aligned_bytes} bytes.',
+ )
if poison_offset != 0:
_LOG.info(
'Poison is enabled %d bytes before and after the usable '
- 'space of each block.', poison_offset)
+ 'space of each block.',
+ poison_offset,
+ )
else:
_LOG.info('%-40s', 'Poison is disabled.')
_LOG.info(
- '%-40s', 'Below is the visualization of the heap. '
- 'Each character represents 4 bytes.')
+ '%-40s',
+ 'Below is the visualization of the heap. '
+ 'Each character represents 4 bytes.',
+ )
_LOG.info('%-40s', f" '{_FREE_CHAR}' indicates free space.")
_LOG.info('%-40s', f" '{_USED_CHAR}' indicates used space.")
_LOG.info(
- '%-40s', f" '{_LEFT_HEADER_CHAR}' indicates header or "
- 'poisoned space before the block.')
- _LOG.info('%-40s', f" '{_RIGHT_HEADER_CHAR}' poisoned space after "
- 'the block.')
+ '%-40s',
+ f" '{_LEFT_HEADER_CHAR}' indicates header or "
+ 'poisoned space before the block.',
+ )
+ _LOG.info(
+ '%-40s',
+ f" '{_RIGHT_HEADER_CHAR}' poisoned space after " 'the block.',
+ )
print()
# Go over the heap space where there will be 64 characters each line.
- for line_base_address in range(0, heap_size, _CHARACTERS_PER_LINE *
- _BYTES_PER_CHARACTER):
+ for line_base_address in range(
+ 0, heap_size, _CHARACTERS_PER_LINE * _BYTES_PER_CHARACTER
+ ):
# Print the heap address of the current line.
- sys.stdout.write(f"{' ': <13}"
- f'{hex(heap_low_address + line_base_address)}'
- f"{f' (+{line_base_address}):': <12}")
- for line_offset in range(0,
- _CHARACTERS_PER_LINE * _BYTES_PER_CHARACTER,
- _BYTES_PER_CHARACTER):
+ sys.stdout.write(
+ f"{' ': <13}"
+ f'{hex(heap_low_address + line_base_address)}'
+ f"{f' (+{line_base_address}):': <12}"
+ )
+ for line_offset in range(
+ 0, _CHARACTERS_PER_LINE * _BYTES_PER_CHARACTER, _BYTES_PER_CHARACTER
+ ):
# Determine if the current 4 bytes is used, unused, or is a
# header.
# The case that we have went over the previous block and will
@@ -218,26 +258,34 @@ def visualize(dump_file=None,
# be printed out as unused.
# Otherwise set the next HeapBlock allocated.
if next_block.next is None:
- next_mem_offset = (heap_size + header_size +
- poison_offset + 1)
+ next_mem_offset = (
+ heap_size + header_size + poison_offset + 1
+ )
next_size = 0
else:
next_mem_offset = next_block.next.mem_offset
next_size = next_block.next.size
# Determine the status of the current 4 bytes.
- if (next_mem_offset - header_size - poison_offset <=
- current_address < next_mem_offset):
+ if (
+ next_mem_offset - header_size - poison_offset
+ <= current_address
+ < next_mem_offset
+ ):
is_left_header = True
is_right_header = False
is_used = False
- elif (next_mem_offset <= current_address <
- next_mem_offset + next_size):
+ elif (
+ next_mem_offset <= current_address < next_mem_offset + next_size
+ ):
is_left_header = False
is_right_header = False
is_used = True
- elif (next_mem_offset + next_size <= current_address <
- next_mem_offset + next_size + poison_offset):
+ elif (
+ next_mem_offset + next_size
+ <= current_address
+ < next_mem_offset + next_size + poison_offset
+ ):
is_left_header = False
is_right_header = True
is_used = False
@@ -266,18 +314,14 @@ def main():
# Try to use pw_cli logs, else default to something reasonable.
try:
import pw_cli.log # pylint: disable=import-outside-toplevel
+
pw_cli.log.install()
except ImportError:
- coloredlogs.install(level='INFO',
- level_styles={
- 'debug': {
- 'color': 244
- },
- 'error': {
- 'color': 'red'
- }
- },
- fmt='%(asctime)s %(levelname)s | %(message)s')
+ coloredlogs.install(
+ level='INFO',
+ level_styles={'debug': {'color': 244}, 'error': {'color': 'red'}},
+ fmt='%(asctime)s %(levelname)s | %(message)s',
+ )
visualize(**vars(parser.parse_args()))
diff --git a/pw_allocator/py/setup.cfg b/pw_allocator/py/setup.cfg
index 4f1a6fdb3..3aa3264a5 100644
--- a/pw_allocator/py/setup.cfg
+++ b/pw_allocator/py/setup.cfg
@@ -21,7 +21,7 @@ description = Pigweed heap allocator
[options]
packages = find:
zip_safe = False
-install_requires = pw_cli
+install_requires =
[options.package_data]
pw_allocator = py.typed
diff --git a/pw_analog/public/pw_analog/microvolt_input.h b/pw_analog/public/pw_analog/microvolt_input.h
index 6a3a562ef..a40e17ba0 100644
--- a/pw_analog/public/pw_analog/microvolt_input.h
+++ b/pw_analog/public/pw_analog/microvolt_input.h
@@ -38,7 +38,7 @@ class MicrovoltInput : public AnalogInput {
int32_t min_voltage_uv; // Microvolts at AnalogInput::Limits::min.
};
- virtual ~MicrovoltInput() = default;
+ ~MicrovoltInput() override = default;
// Blocks until the specified timeout duration has elapsed or the voltage
// sample has been returned, whichever comes first.
diff --git a/pw_android_toolchain/BUILD.gn b/pw_android_toolchain/BUILD.gn
index 8cd271930..cf6b65bd5 100644
--- a/pw_android_toolchain/BUILD.gn
+++ b/pw_android_toolchain/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("static_libstdc") {
# Compile against the static libstdc++ and libc++, since we don't have an
@@ -25,3 +26,6 @@ config("static_libstdc") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_android_toolchain/docs.rst b/pw_android_toolchain/docs.rst
index e810fb6c4..8dbc661dd 100644
--- a/pw_android_toolchain/docs.rst
+++ b/pw_android_toolchain/docs.rst
@@ -24,21 +24,22 @@ Toolchains
``pw_android_toolchain`` provides GN toolchains that may be used to build
Pigweed against an Android NDK. The following toolchains are defined:
- - arm_android_debug
- - arm_android_size_optimized
- - arm_android_speed_optimized
- - arm64_android_debug
- - arm64_android_size_optimized
- - arm64_android_speed_optimized
- - x64_android_debug
- - x64_android_size_optimized
- - x64_android_speed_optimized
- - x86_android_debug
- - x86_android_size_optimized
- - x86_android_speed_optimized
+- arm_android_debug
+- arm_android_size_optimized
+- arm_android_speed_optimized
+- arm64_android_debug
+- arm64_android_size_optimized
+- arm64_android_speed_optimized
+- x64_android_debug
+- x64_android_size_optimized
+- x64_android_speed_optimized
+- x86_android_debug
+- x86_android_size_optimized
+- x86_android_speed_optimized
.. note::
- The documentation for this module is currently incomplete.
+
+ The documentation for this module is currently incomplete.
Defining Toolchains
===================
@@ -50,34 +51,34 @@ For example:
.. code::
- import("//build_overrides/pigweed.gni")
+ import("//build_overrides/pigweed.gni")
- import("$dir_pw_android_toolchain/toolchains.gni")
- import("$dir_pw_android_toolchain/generate_toolchain.gni")
+ import("$dir_pw_android_toolchain/toolchains.gni")
+ import("$dir_pw_android_toolchain/generate_toolchain.gni")
- my_target_scope = {
- # Use Pigweed's Android toolchain as a base.
- _toolchain_base = pw_toolchain_android.debug
+ my_target_scope = {
+ # Use Pigweed's Android toolchain as a base.
+ _toolchain_base = pw_toolchain_android.debug
- # Forward everything except the defaults scope from that toolchain.
- forward_variables_from(_toolchain_base, "*", [ "defaults" ])
+ # Forward everything except the defaults scope from that toolchain.
+ forward_variables_from(_toolchain_base, "*", [ "defaults" ])
- defaults = {
- # Forward everything from the base toolchain's defaults.
- forward_variables_from(_toolchain_base.defaults, "*")
+ defaults = {
+ # Forward everything from the base toolchain's defaults.
+ forward_variables_from(_toolchain_base.defaults, "*")
- # Build for 64-bit AArch64 Android devices.
- current_cpu = "arm64"
+ # Build for 64-bit AArch64 Android devices.
+ current_cpu = "arm64"
- # Extend with custom build arguments for the target.
- pw_log_BACKEND = dir_pw_log_tokenized
- }
- }
+ # Extend with custom build arguments for the target.
+ pw_log_BACKEND = dir_pw_log_tokenized
+ }
+ }
- # Create the actual GN toolchain from the scope.
- pw_generate_android_toolchain("my_target") {
- forward_variables_from(my_target_scope, "*")
- }
+ # Create the actual GN toolchain from the scope.
+ pw_generate_android_toolchain("my_target") {
+ forward_variables_from(my_target_scope, "*")
+ }
Since Android NDKs contain toolchains for all supported targets, as a
convenience, ``pw_generate_android_toolchains`` does not require that
@@ -86,10 +87,10 @@ toolchain for each supported target will be generated.
.. code::
- # Generate arm_*, arm64_*, x64_*, and x86_* for each scope in the list.
- pw_generate_android_toolchains("target_toolchains) {
- toolchains = pw_toolchain_android_list
- }
+ # Generate arm_*, arm64_*, x64_*, and x86_* for each scope in the list.
+ pw_generate_android_toolchains("target_toolchains) {
+ toolchains = pw_toolchain_android_list
+ }
Customization
-------------
diff --git a/pw_arduino_build/BUILD.bazel b/pw_arduino_build/BUILD.bazel
index 29e210a2e..95146e47c 100644
--- a/pw_arduino_build/BUILD.bazel
+++ b/pw_arduino_build/BUILD.bazel
@@ -25,7 +25,17 @@ pw_cc_library(
name = "pw_arduino_build",
srcs = ["arduino_main_wrapper.cc"],
hdrs = ["public/pw_arduino_build/init.h"],
+ # pw_arduino_build/arduino_main_wrapper.cc can't find 'Arduino.h'
+ tags = ["manual"],
deps = [
"//pw_sys_io",
],
)
+
+# Used in targets/arduino
+pw_cc_library(
+ name = "pw_arduino_build_header",
+ hdrs = ["public/pw_arduino_build/init.h"],
+ includes = ["public"],
+ visibility = ["//visibility:public"],
+)
diff --git a/pw_arduino_build/BUILD.gn b/pw_arduino_build/BUILD.gn
index bbe200308..e853386a6 100644
--- a/pw_arduino_build/BUILD.gn
+++ b/pw_arduino_build/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_arduino_build/arduino.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# Backend for the pw_arduino_init module.
@@ -51,3 +52,6 @@ if (pw_arduino_build_CORE_PATH != "") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_arduino_build/arduino.gni b/pw_arduino_build/arduino.gni
index 3ba0000e9..6ac859f7a 100644
--- a/pw_arduino_build/arduino.gni
+++ b/pw_arduino_build/arduino.gni
@@ -62,7 +62,7 @@ if (pw_arduino_build_CORE_PATH != "") {
pw_arduino_build_PACKAGE_NAME + " list-boards")
_compiler_path_override =
- rebase_path(dir_cipd_pigweed + "/bin", root_build_dir)
+ rebase_path(pw_env_setup_CIPD_PIGWEED + "/bin", root_build_dir)
arduino_core_library_path = "$_arduino_selected_core_path/hardware/" +
"$pw_arduino_build_PACKAGE_NAME/libraries"
diff --git a/pw_arduino_build/py/BUILD.bazel b/pw_arduino_build/py/BUILD.bazel
new file mode 100644
index 000000000..d69fbb0e0
--- /dev/null
+++ b/pw_arduino_build/py/BUILD.bazel
@@ -0,0 +1,58 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_python//python:defs.bzl", "py_library", "py_test")
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+py_library(
+ name = "pw_arduino_build",
+ srcs = [
+ "pw_arduino_build/__init__.py",
+ "pw_arduino_build/__main__.py",
+ "pw_arduino_build/builder.py",
+ "pw_arduino_build/core_installer.py",
+ "pw_arduino_build/file_operations.py",
+ "pw_arduino_build/log.py",
+ "pw_arduino_build/teensy_detector.py",
+ "pw_arduino_build/unit_test_client.py",
+ "pw_arduino_build/unit_test_runner.py",
+ "pw_arduino_build/unit_test_server.py",
+ ],
+ imports = ["."],
+)
+
+py_test(
+ name = "builder_test",
+ size = "small",
+ srcs = [
+ "builder_test.py",
+ ],
+ deps = [
+ ":pw_arduino_build",
+ ],
+)
+
+py_test(
+ name = "file_operations_test",
+ size = "small",
+ srcs = [
+ "file_operations_test.py",
+ ],
+ deps = [
+ ":pw_arduino_build",
+ ],
+)
diff --git a/pw_arduino_build/py/BUILD.gn b/pw_arduino_build/py/BUILD.gn
index 731b345e8..bdcce99de 100644
--- a/pw_arduino_build/py/BUILD.gn
+++ b/pw_arduino_build/py/BUILD.gn
@@ -39,5 +39,6 @@ pw_python_package("py") {
"file_operations_test.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
python_deps = [ "$dir_pw_cli/py" ]
}
diff --git a/pw_arduino_build/py/builder_test.py b/pw_arduino_build/py/builder_test.py
index c911e89ce..0c7df224b 100644
--- a/pw_arduino_build/py/builder_test.py
+++ b/pw_arduino_build/py/builder_test.py
@@ -21,25 +21,27 @@ from parameterized import parameterized # type: ignore
class TestShellArgumentSplitting(unittest.TestCase):
"""Tests to ensure shlex.split handles expected use cases."""
- @parameterized.expand([
- (
- "remove-both-quotes",
- """ -DUSB_CONFIG_POWER=100 """
- """ '-DUSB_MANUFACTURER="Adafruit LLC"' """
- """ '-DUSB_PRODUCT="Adafruit PyGamer Advance M4"' """
- """ "-I$HOME/samd/1.6.2/cores/arduino/TinyUSB" """,
- [
- """ -DUSB_CONFIG_POWER=100 """.strip(),
- """ -DUSB_MANUFACTURER="Adafruit LLC" """.strip(),
- """ -DUSB_PRODUCT="Adafruit PyGamer Advance M4" """.strip(),
- """ -I$HOME/samd/1.6.2/cores/arduino/TinyUSB """.strip(),
- ]
- )
- ]) # yapf: disable
- def test_split_arguments_and_remove_quotes(self, unused_test_name,
- input_string, expected):
- """Test splitting a string into a list of arguments with quotes removed.
- """
+ @parameterized.expand(
+ [
+ (
+ "remove-both-quotes",
+ """ -DUSB_CONFIG_POWER=100 """
+ """ '-DUSB_MANUFACTURER="Adafruit LLC"' """
+ """ '-DUSB_PRODUCT="Adafruit PyGamer Advance M4"' """
+ """ "-I$HOME/samd/1.6.2/cores/arduino/TinyUSB" """,
+ [
+ """ -DUSB_CONFIG_POWER=100 """.strip(),
+ """ -DUSB_MANUFACTURER="Adafruit LLC" """.strip(),
+ """ -DUSB_PRODUCT="Adafruit PyGamer Advance M4" """.strip(),
+ """ -I$HOME/samd/1.6.2/cores/arduino/TinyUSB """.strip(),
+ ],
+ )
+ ]
+ )
+ def test_split_arguments_and_remove_quotes(
+ self, unused_test_name, input_string, expected
+ ):
+ """Test splitting a str into a list of arguments with quotes removed."""
result = shlex.split(input_string)
self.assertEqual(result, expected)
diff --git a/pw_arduino_build/py/file_operations_test.py b/pw_arduino_build/py/file_operations_test.py
index 9d350e605..5f36c0ba9 100644
--- a/pw_arduino_build/py/file_operations_test.py
+++ b/pw_arduino_build/py/file_operations_test.py
@@ -47,54 +47,67 @@ def create_files(root_dir, file_names):
class TestFileOperations(unittest.TestCase):
"""Tests to ensure arduino core library source files can be found."""
+
def setUp(self):
self.test_dir = tempfile.mkdtemp()
def tearDown(self):
shutil.rmtree(self.test_dir)
- @parameterized.expand([
- (
- "sources recursive", file_set(), ["**/*.ino", "**/*.h", "**/*.cpp"],
- [
- "app.ino",
- os.path.join("core", "pwm", "pulse.h"),
- os.path.join("libraries", "b.cpp"),
- os.path.join("libraries", "c.h"),
- ]
- ),
- (
- "directories recursive", file_set(), ["**"],
- [
- "core",
- os.path.join("core", "pwm"),
- "libraries",
- ]
- ),
- (
- "directories one level deep", file_set(), ["*"],
- [
- "core",
- "libraries",
- ]
- ),
- (
- "items one level deep", file_set(), ["*"],
- [
- "app.ino",
- "core",
- "libraries",
- ]
- )
- ]) # yapf: disable
- def test_find_files(self, test_case, base_fileset, patterns,
- expected_results):
+ @parameterized.expand(
+ [
+ (
+ "sources recursive",
+ file_set(),
+ ["**/*.ino", "**/*.h", "**/*.cpp"],
+ [
+ "app.ino",
+ os.path.join("core", "pwm", "pulse.h"),
+ os.path.join("libraries", "b.cpp"),
+ os.path.join("libraries", "c.h"),
+ ],
+ ),
+ (
+ "directories recursive",
+ file_set(),
+ ["**"],
+ [
+ "core",
+ os.path.join("core", "pwm"),
+ "libraries",
+ ],
+ ),
+ (
+ "directories one level deep",
+ file_set(),
+ ["*"],
+ [
+ "core",
+ "libraries",
+ ],
+ ),
+ (
+ "items one level deep",
+ file_set(),
+ ["*"],
+ [
+ "app.ino",
+ "core",
+ "libraries",
+ ],
+ ),
+ ]
+ )
+ def test_find_files(
+ self, test_case, base_fileset, patterns, expected_results
+ ):
"""Test find_files on source files and directories."""
create_files(self.test_dir, base_fileset)
- result = file_operations.find_files(self.test_dir,
- patterns,
- directories_only=("directories"
- in test_case))
+ result = file_operations.find_files(
+ self.test_dir,
+ patterns,
+ directories_only=("directories" in test_case),
+ )
self.assertSequenceEqual(expected_results, result)
diff --git a/pw_arduino_build/py/pw_arduino_build/__main__.py b/pw_arduino_build/py/pw_arduino_build/__main__.py
index e6fa77d93..0811bef48 100644
--- a/pw_arduino_build/py/pw_arduino_build/__main__.py
+++ b/pw_arduino_build/py/pw_arduino_build/__main__.py
@@ -91,8 +91,7 @@ def show_command_print_flag_string(args, flag_string):
print(flag_string)
-def subtract_flags(flag_list_a: List[str],
- flag_list_b: List[str]) -> List[str]:
+def subtract_flags(flag_list_a: List[str], flag_list_b: List[str]) -> List[str]:
"""Given two sets of flags return flags in a that are not in b."""
flag_counts = OrderedDict() # type: OrderedDict[str, int]
for flag in flag_list_a + flag_list_b:
@@ -106,9 +105,9 @@ def run_command_lines(args, command_lines: List[str]):
print(command_line)
# TODO(tonymd): Exit with sub command exit code.
command_line_args = shlex.split(command_line)
- process = subprocess.run(command_line_args,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ process = subprocess.run(
+ command_line_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
if process.returncode != 0:
_LOG.error('Command failed with exit code %d.', process.returncode)
_LOG.error('Full command:')
@@ -145,8 +144,9 @@ def run_command(args, builder):
run_command_lines(args, builder.get_postbuild_steps())
if args.run_upload_command:
- command = builder.get_upload_line(args.run_upload_command,
- args.serial_port)
+ command = builder.get_upload_line(
+ args.run_upload_command, args.serial_port
+ )
run_command_lines(args, [command])
@@ -196,8 +196,10 @@ def show_command(args, builder):
show_command_print_flag_string(args, sflags)
elif args.s_only_flags:
- s_only_flags = subtract_flags(shlex.split(builder.get_s_flags()),
- shlex.split(builder.get_c_flags()))
+ s_only_flags = subtract_flags(
+ shlex.split(builder.get_s_flags()),
+ shlex.split(builder.get_c_flags()),
+ )
show_command_print_flag_string(args, " ".join(s_only_flags))
elif args.cpp_flags:
@@ -205,8 +207,10 @@ def show_command(args, builder):
show_command_print_flag_string(args, cppflags)
elif args.cpp_only_flags:
- cpp_only_flags = subtract_flags(shlex.split(builder.get_cpp_flags()),
- shlex.split(builder.get_c_flags()))
+ cpp_only_flags = subtract_flags(
+ shlex.split(builder.get_cpp_flags()),
+ shlex.split(builder.get_c_flags()),
+ )
show_command_print_flag_string(args, " ".join(cpp_only_flags))
elif args.ld_flags:
@@ -217,8 +221,9 @@ def show_command(args, builder):
show_command_print_flag_string(args, builder.get_ld_libs())
elif args.ld_lib_names:
- show_command_print_flag_string(args,
- builder.get_ld_libs(name_only=True))
+ show_command_print_flag_string(
+ args, builder.get_ld_libs(name_only=True)
+ )
elif args.ar_flags:
ar_flags = builder.get_ar_flags()
@@ -281,37 +286,50 @@ def show_command(args, builder):
show_command_print_string_list(args, vfiles)
-def add_common_parser_args(parser, serial_port, build_path, build_project_name,
- project_path, project_source_path):
+def add_common_parser_args(
+ parser,
+ serial_port,
+ build_path,
+ build_project_name,
+ project_path,
+ project_source_path,
+):
"""Add command line options common to the run and show commands."""
parser.add_argument(
"--serial-port",
default=serial_port,
- help="Serial port for flashing. Default: '{}'".format(serial_port))
+ help="Serial port for flashing. Default: '{}'".format(serial_port),
+ )
parser.add_argument(
"--build-path",
default=build_path,
- help="Build directory. Default: '{}'".format(build_path))
+ help="Build directory. Default: '{}'".format(build_path),
+ )
parser.add_argument(
"--project-path",
default=project_path,
- help="Project directory. Default: '{}'".format(project_path))
+ help="Project directory. Default: '{}'".format(project_path),
+ )
parser.add_argument(
"--project-source-path",
default=project_source_path,
- help="Project directory. Default: '{}'".format(project_source_path))
- parser.add_argument("--library-path",
- default=[],
- nargs="+",
- type=str,
- help="Path to Arduino Library directory.")
+ help="Project directory. Default: '{}'".format(project_source_path),
+ )
+ parser.add_argument(
+ "--library-path",
+ default=[],
+ nargs="+",
+ type=str,
+ help="Path to Arduino Library directory.",
+ )
parser.add_argument(
"--build-project-name",
default=build_project_name,
- help="Project name. Default: '{}'".format(build_project_name))
- parser.add_argument("--board",
- required=True,
- help="Name of the board to use.")
+ help="Project name. Default: '{}'".format(build_project_name),
+ )
+ parser.add_argument(
+ "--board", required=True, help="Name of the board to use."
+ )
# nargs="+" is one or more args, e.g:
# --menu-options menu.usb.serialhid menu.speed.150
parser.add_argument(
@@ -320,21 +338,25 @@ def add_common_parser_args(parser, serial_port, build_path, build_project_name,
type=str,
metavar="menu.usb.serial",
help="Desired Arduino menu options. See the "
- "'list-menu-options' subcommand for available options.")
- parser.add_argument("--set-variable",
- action="append",
- metavar='some.variable=NEW_VALUE',
- help="Override an Arduino recipe variable. May be "
- "specified multiple times. For example: "
- "--set-variable 'serial.port.label=/dev/ttyACM0' "
- "--set-variable 'serial.port.protocol=Teensy'")
+ "'list-menu-options' subcommand for available options.",
+ )
+ parser.add_argument(
+ "--set-variable",
+ action="append",
+ metavar='some.variable=NEW_VALUE',
+ help="Override an Arduino recipe variable. May be "
+ "specified multiple times. For example: "
+ "--set-variable 'serial.port.label=/dev/ttyACM0' "
+ "--set-variable 'serial.port.protocol=Teensy'",
+ )
def check_for_missing_args(args):
if args.arduino_package_path is None:
raise MissingArduinoCore(
"Please specify the location of an Arduino core using "
- "'--arduino-package-path' and '--arduino-package-name'.")
+ "'--arduino-package-path' and '--arduino-package-name'."
+ )
# TODO(tonymd): These defaults don't make sense anymore and should be removed.
@@ -342,11 +364,15 @@ def get_default_options():
defaults = {}
defaults["build_path"] = os.path.realpath(
os.path.expanduser(
- os.path.expandvars(os.path.join(os.getcwd(), "build"))))
+ os.path.expandvars(os.path.join(os.getcwd(), "build"))
+ )
+ )
defaults["project_path"] = os.path.realpath(
- os.path.expanduser(os.path.expandvars(os.getcwd())))
- defaults["project_source_path"] = os.path.join(defaults["project_path"],
- "src")
+ os.path.expanduser(os.path.expandvars(os.getcwd()))
+ )
+ defaults["project_source_path"] = os.path.join(
+ defaults["project_path"], "src"
+ )
defaults["build_project_name"] = os.path.basename(defaults["project_path"])
defaults["serial_port"] = "UNKNOWN"
return defaults
@@ -359,7 +385,8 @@ def load_config_file(args):
if args.save_config and not args.config_file:
raise FileNotFoundError(
- "'--save-config' requires the '--config-file' option")
+ "'--save-config' requires the '--config-file' option"
+ )
if not args.config_file:
return
@@ -412,95 +439,118 @@ def load_config_file(args):
def _parse_args() -> argparse.Namespace:
"""Setup argparse and parse command line args."""
+
def log_level(arg: str) -> int:
try:
return getattr(logging, arg.upper())
except AttributeError:
raise argparse.ArgumentTypeError(
- f'{arg.upper()} is not a valid log level')
+ f'{arg.upper()} is not a valid log level'
+ )
def existing_directory(input_string: str):
"""Argparse type that resolves to an absolute path."""
input_path = Path(os.path.expandvars(input_string)).absolute()
if not input_path.exists():
raise argparse.ArgumentTypeError(
- "'{}' is not a valid directory.".format(str(input_path)))
+ "'{}' is not a valid directory.".format(str(input_path))
+ )
return input_path.as_posix()
parser = argparse.ArgumentParser()
- parser.add_argument("-q",
- "--quiet",
- help="hide run command output",
- action="store_true")
- parser.add_argument('-l',
- '--loglevel',
- type=log_level,
- default=logging.INFO,
- help='Set the log level '
- '(debug, info, warning, error, critical)')
+ parser.add_argument(
+ "-q", "--quiet", help="hide run command output", action="store_true"
+ )
+ parser.add_argument(
+ '-l',
+ '--loglevel',
+ type=log_level,
+ default=logging.INFO,
+ help='Set the log level ' '(debug, info, warning, error, critical)',
+ )
default_options = get_default_options()
# Global command line options
- parser.add_argument("--arduino-package-path",
- type=existing_directory,
- help="Path to the arduino IDE install location.")
- parser.add_argument("--arduino-package-name",
- help="Name of the Arduino board package to use.")
- parser.add_argument("--compiler-path-override",
- type=existing_directory,
- help="Path to arm-none-eabi-gcc bin folder. "
- "Default: Arduino core specified gcc")
+ parser.add_argument(
+ "--arduino-package-path",
+ type=existing_directory,
+ help="Path to the arduino IDE install location.",
+ )
+ parser.add_argument(
+ "--arduino-package-name",
+ help="Name of the Arduino board package to use.",
+ )
+ parser.add_argument(
+ "--compiler-path-override",
+ type=existing_directory,
+ help="Path to arm-none-eabi-gcc bin folder. "
+ "Default: Arduino core specified gcc",
+ )
parser.add_argument("-c", "--config-file", help="Path to a config file.")
- parser.add_argument("--save-config",
- action="store_true",
- help="Save command line arguments to the config file.")
+ parser.add_argument(
+ "--save-config",
+ action="store_true",
+ help="Save command line arguments to the config file.",
+ )
# Subcommands
- subparsers = parser.add_subparsers(title="subcommand",
- description="valid subcommands",
- help="sub-command help",
- dest="subcommand",
- required=True)
+ subparsers = parser.add_subparsers(
+ title="subcommand",
+ description="valid subcommands",
+ help="sub-command help",
+ dest="subcommand",
+ required=True,
+ )
# install-core command
install_core_parser = subparsers.add_parser(
- "install-core", help="Download and install arduino cores")
+ "install-core", help="Download and install arduino cores"
+ )
install_core_parser.set_defaults(func=core_installer.install_core_command)
- install_core_parser.add_argument("--prefix",
- required=True,
- help="Path to install core files.")
+ install_core_parser.add_argument(
+ "--prefix", required=True, help="Path to install core files."
+ )
install_core_parser.add_argument(
"--core-name",
required=True,
choices=core_installer.supported_cores(),
- help="Name of the arduino core to install.")
+ help="Name of the arduino core to install.",
+ )
# list-boards command
- list_boards_parser = subparsers.add_parser("list-boards",
- help="show supported boards")
+ list_boards_parser = subparsers.add_parser(
+ "list-boards", help="show supported boards"
+ )
list_boards_parser.set_defaults(func=list_boards_command)
# list-menu-options command
list_menu_options_parser = subparsers.add_parser(
"list-menu-options",
- help="show available menu options for selected board")
+ help="show available menu options for selected board",
+ )
list_menu_options_parser.set_defaults(func=list_menu_options_command)
- list_menu_options_parser.add_argument("--board",
- required=True,
- help="Name of the board to use.")
+ list_menu_options_parser.add_argument(
+ "--board", required=True, help="Name of the board to use."
+ )
# show command
- show_parser = subparsers.add_parser("show",
- help="Return compiler information.")
- add_common_parser_args(show_parser, default_options["serial_port"],
- default_options["build_path"],
- default_options["build_project_name"],
- default_options["project_path"],
- default_options["project_source_path"])
- show_parser.add_argument("--delimit-with-newlines",
- help="Separate flag output with newlines.",
- action="store_true")
+ show_parser = subparsers.add_parser(
+ "show", help="Return compiler information."
+ )
+ add_common_parser_args(
+ show_parser,
+ default_options["serial_port"],
+ default_options["build_path"],
+ default_options["build_project_name"],
+ default_options["project_path"],
+ default_options["project_source_path"],
+ )
+ show_parser.add_argument(
+ "--delimit-with-newlines",
+ help="Separate flag output with newlines.",
+ action="store_true",
+ )
show_parser.add_argument("--library-names", nargs="+", type=str)
output_group = show_parser.add_mutually_exclusive_group(required=True)
@@ -517,20 +567,23 @@ def _parse_args() -> argparse.Namespace:
output_group.add_argument("--ld-libs", action="store_true")
output_group.add_argument("--ld-lib-names", action="store_true")
output_group.add_argument("--objcopy", help="objcopy step for SUFFIX")
- output_group.add_argument("--objcopy-flags",
- help="objcopy flags for SUFFIX")
+ output_group.add_argument(
+ "--objcopy-flags", help="objcopy flags for SUFFIX"
+ )
output_group.add_argument("--core-path", action="store_true")
output_group.add_argument("--cc-binary", action="store_true")
output_group.add_argument("--cxx-binary", action="store_true")
output_group.add_argument("--ar-binary", action="store_true")
output_group.add_argument("--objcopy-binary", action="store_true")
output_group.add_argument("--size-binary", action="store_true")
- output_group.add_argument("--prebuilds",
- action="store_true",
- help="Show prebuild step commands.")
- output_group.add_argument("--postbuilds",
- action="store_true",
- help="Show postbuild step commands.")
+ output_group.add_argument(
+ "--prebuilds", action="store_true", help="Show prebuild step commands."
+ )
+ output_group.add_argument(
+ "--postbuilds",
+ action="store_true",
+ help="Show postbuild step commands.",
+ )
output_group.add_argument("--upload-tools", action="store_true")
output_group.add_argument("--upload-command")
output_group.add_argument("--library-includes", action="store_true")
@@ -549,16 +602,21 @@ def _parse_args() -> argparse.Namespace:
# run command
run_parser = subparsers.add_parser("run", help="Run Arduino recipes.")
- add_common_parser_args(run_parser, default_options["serial_port"],
- default_options["build_path"],
- default_options["build_project_name"],
- default_options["project_path"],
- default_options["project_source_path"])
- run_parser.add_argument("--run-link",
- nargs="+",
- type=str,
- help="Run the link command. Expected arguments: "
- "the archive file followed by all obj files.")
+ add_common_parser_args(
+ run_parser,
+ default_options["serial_port"],
+ default_options["build_path"],
+ default_options["build_project_name"],
+ default_options["project_path"],
+ default_options["project_source_path"],
+ )
+ run_parser.add_argument(
+ "--run-link",
+ nargs="+",
+ type=str,
+ help="Run the link command. Expected arguments: "
+ "the archive file followed by all obj files.",
+ )
run_parser.add_argument("--run-objcopy", action="store_true")
run_parser.add_argument("--run-prebuilds", action="store_true")
run_parser.add_argument("--run-postbuilds", action="store_true")
@@ -584,8 +642,8 @@ def main():
if args.compiler_path_override:
# Get absolute path
compiler_path_override = os.path.realpath(
- os.path.expanduser(os.path.expandvars(
- args.compiler_path_override)))
+ os.path.expanduser(os.path.expandvars(args.compiler_path_override))
+ )
args.compiler_path_override = compiler_path_override
load_config_file(args)
@@ -594,8 +652,9 @@ def main():
args.func(args)
elif args.subcommand in ["list-boards", "list-menu-options"]:
check_for_missing_args(args)
- builder = ArduinoBuilder(args.arduino_package_path,
- args.arduino_package_name)
+ builder = ArduinoBuilder(
+ args.arduino_package_path, args.arduino_package_name
+ )
builder.load_board_definitions()
args.func(args, builder)
else: # args.subcommand in ["run", "show"]
@@ -609,7 +668,8 @@ def main():
project_source_path=args.project_source_path,
library_path=getattr(args, 'library_path', None),
library_names=getattr(args, 'library_names', None),
- compiler_path_override=args.compiler_path_override)
+ compiler_path_override=args.compiler_path_override,
+ )
builder.load_board_definitions()
builder.select_board(args.board, args.menu_options)
if args.set_variable:
diff --git a/pw_arduino_build/py/pw_arduino_build/builder.py b/pw_arduino_build/py/pw_arduino_build/builder.py
index 2a9dcc3fa..6d268b40b 100755
--- a/pw_arduino_build/py/pw_arduino_build/builder.py
+++ b/pw_arduino_build/py/pw_arduino_build/builder.py
@@ -38,47 +38,54 @@ def arduino_runtime_os_string():
arduno_platform = {
"Linux": "linux",
"Windows": "windows",
- "Darwin": "macosx"
+ "Darwin": "macosx",
}
return arduno_platform[platform.system()]
class ArduinoBuilder:
"""Used to interpret arduino boards.txt and platform.txt files."""
+
# pylint: disable=too-many-instance-attributes,too-many-public-methods
BOARD_MENU_REGEX = re.compile(
- r"^(?P<name>menu\.[^#=]+)=(?P<description>.*)$", re.MULTILINE)
+ r"^(?P<name>menu\.[^#=]+)=(?P<description>.*)$", re.MULTILINE
+ )
BOARD_NAME_REGEX = re.compile(
- r"^(?P<name>[^\s#\.]+)\.name=(?P<description>.*)$", re.MULTILINE)
+ r"^(?P<name>[^\s#\.]+)\.name=(?P<description>.*)$", re.MULTILINE
+ )
- VARIABLE_REGEX = re.compile(r"^(?P<name>[^\s#=]+)=(?P<value>.*)$",
- re.MULTILINE)
+ VARIABLE_REGEX = re.compile(
+ r"^(?P<name>[^\s#=]+)=(?P<value>.*)$", re.MULTILINE
+ )
MENU_OPTION_REGEX = re.compile(
r"^menu\." # starts with "menu"
r"(?P<menu_option_name>[^.]+)\." # first token after .
- r"(?P<menu_option_value>[^.]+)$") # second (final) token after .
+ r"(?P<menu_option_value>[^.]+)$"
+ ) # second (final) token after .
TOOL_NAME_REGEX = re.compile(
- r"^tools\." # starts with "tools"
- r"(?P<tool_name>[^.]+)\.") # first token after .
+ r"^tools\." r"(?P<tool_name>[^.]+)\." # starts with "tools"
+ ) # first token after .
INTERPOLATED_VARIABLE_REGEX = re.compile(r"{[^}]+}", re.MULTILINE)
OBJCOPY_STEP_NAME_REGEX = re.compile(r"^recipe.objcopy.([^.]+).pattern$")
- def __init__(self,
- arduino_path,
- package_name,
- build_path=None,
- project_path=None,
- project_source_path=None,
- library_path=None,
- library_names=None,
- build_project_name=None,
- compiler_path_override=False):
+ def __init__(
+ self,
+ arduino_path,
+ package_name,
+ build_path=None,
+ project_path=None,
+ project_source_path=None,
+ library_path=None,
+ library_names=None,
+ build_project_name=None,
+ compiler_path_override=False,
+ ):
self.arduino_path = arduino_path
self.arduino_package_name = package_name
self.selected_board = None
@@ -95,16 +102,15 @@ class ArduinoBuilder:
self.compiler_path_override_binaries = []
if self.compiler_path_override:
self.compiler_path_override_binaries = file_operations.find_files(
- self.compiler_path_override, "*")
+ self.compiler_path_override, "*"
+ )
# Container dicts for boards.txt and platform.txt file data.
self.board = OrderedDict()
self.platform = OrderedDict()
- self.menu_options = OrderedDict({
- "global_options": {},
- "default_board_values": {},
- "selected": {}
- })
+ self.menu_options = OrderedDict(
+ {"global_options": {}, "default_board_values": {}, "selected": {}}
+ )
self.tools_variables = {}
# Set and check for valid hardware folder.
@@ -113,17 +119,21 @@ class ArduinoBuilder:
if not os.path.exists(self.hardware_path):
raise FileNotFoundError(
"Arduino package path '{}' does not exist.".format(
- self.arduino_path))
+ self.hardware_path
+ )
+ )
# Set and check for valid package name
- self.package_path = os.path.join(self.arduino_path, "hardware",
- package_name)
+ self.package_path = os.path.join(
+ self.arduino_path, "hardware", package_name
+ )
# {build.arch} is the first folder name of the package (upcased)
self.build_arch = os.path.split(package_name)[0].upper()
if not os.path.exists(self.package_path):
- _LOG.error("Error: Arduino package name '%s' does not exist.",
- package_name)
+ _LOG.error(
+ "Error: Arduino package name '%s' does not exist.", package_name
+ )
_LOG.error("Did you mean:\n")
# TODO(tonymd): On Windows concatenating "/" may not work
possible_alternatives = [
@@ -142,14 +152,15 @@ class ArduinoBuilder:
self.library_path.append(Path(self.package_path) / "libraries")
if library_path:
self.library_path = [
- os.path.realpath(os.path.expanduser(
- os.path.expandvars(l_path))) for l_path in library_path
+ os.path.realpath(os.path.expanduser(os.path.expandvars(l_path)))
+ for l_path in library_path
]
# Grab all folder names in the cores directory. These are typically
# sub-core source files.
self.sub_core_folders = os.listdir(
- os.path.join(self.package_path, "cores"))
+ os.path.join(self.package_path, "cores")
+ )
self._find_tools_variables()
@@ -185,8 +196,7 @@ class ArduinoBuilder:
# Replace variables in platform
for var, value in self.platform.items():
- self.platform[var] = self._replace_variables(
- value, variable_source)
+ self.platform[var] = self._replace_variables(value, variable_source)
def _apply_recipe_overrides(self):
# Override link recipes with per-core exceptions
@@ -196,25 +206,30 @@ class ArduinoBuilder:
# To {archive_file_path} (which should contain the core.a file)
new_link_line = self.platform["recipe.c.combine.pattern"].replace(
"{object_files} \"{build.path}/{archive_file}\"",
- "{object_files} {archive_file_path}", 1)
+ "{object_files} {archive_file_path}",
+ 1,
+ )
# Add the teensy provided toolchain lib folder for link access to
# libarm_cortexM*_math.a
new_link_line = new_link_line.replace(
"\"-L{build.path}\"",
"\"-L{build.path}\" -L{compiler.path}/arm/arm-none-eabi/lib",
- 1)
+ 1,
+ )
self.platform["recipe.c.combine.pattern"] = new_link_line
# Remove the pre-compiled header include
self.platform["recipe.cpp.o.pattern"] = self.platform[
- "recipe.cpp.o.pattern"].replace("\"-I{build.path}/pch\"", "",
- 1)
+ "recipe.cpp.o.pattern"
+ ].replace("\"-I{build.path}/pch\"", "", 1)
# Adafruit-samd core
# TODO(tonymd): This build_arch may clash with Arduino-SAMD core
elif self.build_arch == "SAMD":
new_link_line = self.platform["recipe.c.combine.pattern"].replace(
"\"{build.path}/{archive_file}\" -Wl,--end-group",
- "{archive_file_path} -Wl,--end-group", 1)
+ "{archive_file_path} -Wl,--end-group",
+ 1,
+ )
self.platform["recipe.c.combine.pattern"] = new_link_line
# STM32L4 Core:
@@ -224,7 +239,8 @@ class ArduinoBuilder:
# seems to be core.a (except STM32 core)
line_to_delete = "-Wl,--start-group \"{build.path}/{archive_file}\""
new_link_line = self.platform["recipe.c.combine.pattern"].replace(
- line_to_delete, "-Wl,--start-group {archive_file_path}", 1)
+ line_to_delete, "-Wl,--start-group {archive_file_path}", 1
+ )
self.platform["recipe.c.combine.pattern"] = new_link_line
# stm32duino core
@@ -239,7 +255,8 @@ class ArduinoBuilder:
self.menu_options["selected"] = {}
# Set default menu options for selected board
for menu_key, menu_dict in self.menu_options["default_board_values"][
- self.selected_board].items():
+ self.selected_board
+ ].items():
for name, var in self.board[self.selected_board].items():
starting_key = "{}.{}.".format(menu_key, menu_dict["name"])
if name.startswith(starting_key):
@@ -257,8 +274,9 @@ class ArduinoBuilder:
menu_match = menu_match_result.groupdict()
menu_value = menu_match["menu_option_value"]
menu_key = "menu.{}".format(menu_match["menu_option_name"])
- self.menu_options["default_board_values"][
- self.selected_board][menu_key]["name"] = menu_value
+ self.menu_options["default_board_values"][self.selected_board][
+ menu_key
+ ]["name"] = menu_value
# Update build variables
self._copy_default_menu_options_to_build_variables()
@@ -285,7 +303,8 @@ class ArduinoBuilder:
# {archive_file_path} is the final core.a archive
if self.build_path:
current_board["archive_file_path"] = os.path.join(
- self.build_path, "core.a")
+ self.build_path, "core.a"
+ )
if self.project_source_path:
current_board["build.source.path"] = self.project_source_path
@@ -301,10 +320,12 @@ class ArduinoBuilder:
# Teensyduino is installed into the arduino IDE folder
# rather than ~/.arduino15/packages/
current_board["runtime.hardware.path"] = os.path.join(
- self.hardware_path, "teensy")
+ self.hardware_path, "teensy"
+ )
current_board["build.system.path"] = os.path.join(
- self.package_path, "system")
+ self.package_path, "system"
+ )
# Set the {build.core.path} variable that pointing to a sub-core
# folder. For Teensys this is:
@@ -312,15 +333,17 @@ class ArduinoBuilder:
# it's typically just the 'arduino' folder. For example:
# 'arduino-samd/hardware/samd/1.8.8/cores/arduino'
core_path = Path(self.package_path) / "cores"
- core_path /= current_board.get("build.core",
- self.sub_core_folders[0])
+ core_path /= current_board.get(
+ "build.core", self.sub_core_folders[0]
+ )
current_board["build.core.path"] = core_path.as_posix()
current_board["build.arch"] = self.build_arch
for name, var in current_board.items():
- current_board[name] = var.replace("{build.core.path}",
- core_path.as_posix())
+ current_board[name] = var.replace(
+ "{build.core.path}", core_path.as_posix()
+ )
def load_board_definitions(self):
"""Loads Arduino boards.txt and platform.txt files into dictionaries.
@@ -352,21 +375,27 @@ class ArduinoBuilder:
for b_match in [m.groupdict() for m in board_name_matches]:
self.board[b_match["name"]] = OrderedDict()
self.menu_options["default_board_values"][
- b_match["name"]] = OrderedDict()
+ b_match["name"]
+ ] = OrderedDict()
# Get all board variables, e.g. teensy40.*
for current_board_name, current_board in self.board.items():
board_line_matches = re.finditer(
fr"^\s*{current_board_name}\."
- fr"(?P<key>[^#=]+)=(?P<value>.*)$", board_file,
- re.MULTILINE)
+ fr"(?P<key>[^#=]+)=(?P<value>.*)$",
+ board_file,
+ re.MULTILINE,
+ )
for b_match in [m.groupdict() for m in board_line_matches]:
# Check if this line is a menu option
# (e.g. 'menu.usb.serial') and save as default if it's the
# first one seen.
ArduinoBuilder.save_default_menu_option(
- current_board_name, b_match["key"], b_match["value"],
- self.menu_options)
+ current_board_name,
+ b_match["key"],
+ b_match["value"],
+ self.menu_options,
+ )
current_board[b_match["key"]] = b_match["value"].strip()
self._set_global_arduino_variables()
@@ -387,19 +416,23 @@ class ArduinoBuilder:
r'(?P<menu_option_name>[^.]+)\.' # first token after .
r'(?P<menu_option_value>[^.]+)' # second token after .
r'(\.(?P<rest>.+))?', # optionally any trailing tokens after a .
- key)
+ key,
+ )
if menu_match_result:
menu_match = menu_match_result.groupdict()
current_menu_key = "menu.{}".format(menu_match["menu_option_name"])
# If this is the first menu option seen for current_board_name, save
# as the default.
- if current_menu_key not in menu_options["default_board_values"][
- current_board_name]:
+ if (
+ current_menu_key
+ not in menu_options["default_board_values"][current_board_name]
+ ):
menu_options["default_board_values"][current_board_name][
- current_menu_key] = {
- "name": menu_match["menu_option_value"],
- "description": value
- }
+ current_menu_key
+ ] = {
+ "name": menu_match["menu_option_value"],
+ "description": value,
+ }
def _replace_variables(self, line, variable_lookup_source):
"""Replace {variables} from loaded boards.txt or platform.txt.
@@ -408,8 +441,7 @@ class ArduinoBuilder:
definitions from variable_lookup_source.
"""
new_line = line
- for current_var_match in self.INTERPOLATED_VARIABLE_REGEX.findall(
- line):
+ for current_var_match in self.INTERPOLATED_VARIABLE_REGEX.findall(line):
# {build.flags.c} --> build.flags.c
current_var = current_var_match.strip("{}")
@@ -428,33 +460,49 @@ class ArduinoBuilder:
runtime_tool_paths += glob.glob(
os.path.join(
os.path.realpath(os.path.expanduser(os.path.expandvars("~"))),
- ".arduino15", "packages", "arduino", "tools", "*"))
+ ".arduino15",
+ "packages",
+ "arduino",
+ "tools",
+ "*",
+ )
+ )
# <ARDUINO_PATH>/tools/<OS_STRING>/<TOOL_NAMES>
runtime_tool_paths += glob.glob(
- os.path.join(self.arduino_path, "tools",
- arduino_runtime_os_string(), "*"))
+ os.path.join(
+ self.arduino_path, "tools", arduino_runtime_os_string(), "*"
+ )
+ )
# <ARDUINO_PATH>/tools/<TOOL_NAMES>
# This will grab linux/windows/macosx/share as <TOOL_NAMES>.
runtime_tool_paths += glob.glob(
- os.path.join(self.arduino_path, "tools", "*"))
+ os.path.join(self.arduino_path, "tools", "*")
+ )
# Process package tools after arduino tools.
# They should overwrite vars & take precedence.
# <PACKAGE_PATH>/tools/<OS_STRING>/<TOOL_NAMES>
runtime_tool_paths += glob.glob(
- os.path.join(self.package_path, "tools",
- arduino_runtime_os_string(), "*"))
+ os.path.join(
+ self.package_path, "tools", arduino_runtime_os_string(), "*"
+ )
+ )
# <PACKAGE_PATH>/tools/<TOOL_NAMES>
# This will grab linux/windows/macosx/share as <TOOL_NAMES>.
runtime_tool_paths += glob.glob(
- os.path.join(self.package_path, "tools", "*"))
+ os.path.join(self.package_path, "tools", "*")
+ )
for path in runtime_tool_paths:
# Make sure TOOL_NAME is not an OS string
- if not (path.endswith("linux") or path.endswith("windows")
- or path.endswith("macosx") or path.endswith("share")):
+ if not (
+ path.endswith("linux")
+ or path.endswith("windows")
+ or path.endswith("macosx")
+ or path.endswith("share")
+ ):
# TODO(tonymd): Check if a file & do nothing?
# Check if it's a directory with subdir(s) as a version string
@@ -469,17 +517,21 @@ class ArduinoBuilder:
version_paths = sorted(glob.glob(os.path.join(path, "*")))
# Check if all sub folders start with a version string.
if len(version_paths) == sum(
- bool(re.match(r"^[0-9.]+", os.path.basename(vp)))
- for vp in version_paths):
+ bool(re.match(r"^[0-9.]+", os.path.basename(vp)))
+ for vp in version_paths
+ ):
for version_path in version_paths:
version_string = os.path.basename(version_path)
var_name = "runtime.tools.{}-{}.path".format(
- tool_folder, version_string)
+ tool_folder, version_string
+ )
self.tools_variables[var_name] = os.path.join(
- path, version_string)
+ path, version_string
+ )
var_name = "runtime.tools.{}.path".format(tool_folder)
self.tools_variables[var_name] = os.path.join(
- path, os.path.basename(version_paths[-1]))
+ path, os.path.basename(version_paths[-1])
+ )
# Else set toolpath to path.
else:
var_name = "runtime.tools.{}.path".format(tool_folder)
@@ -505,12 +557,14 @@ class ArduinoBuilder:
# (and from copied in menu options).
for var, value in self.board[self.selected_board].items():
self.board[self.selected_board][var] = self._replace_variables(
- value, self.board[self.selected_board])
+ value, self.board[self.selected_board]
+ )
# Check for build.variant variable
# This will be set in selected board after menu options substitution
build_variant = self.board[self.selected_board].get(
- "build.variant", None)
+ "build.variant", None
+ )
if build_variant:
# Set build.variant.path
bvp = os.path.join(self.package_path, "variants", build_variant)
@@ -526,7 +580,8 @@ class ArduinoBuilder:
# Replace {vars} in platform from the selected board definition
for var, value in self.platform.items():
self.platform[var] = self._replace_variables(
- value, self.board[self.selected_board])
+ value, self.board[self.selected_board]
+ )
# platform -> platform
# Replace any remaining {vars} in platform from platform
@@ -539,8 +594,10 @@ class ArduinoBuilder:
self.platform[var] = self._replace_variables(value, self.platform)
_LOG.debug("MENU_OPTIONS: %s", _pretty_format(self.menu_options))
- _LOG.debug("SELECTED_BOARD: %s",
- _pretty_format(self.board[self.selected_board]))
+ _LOG.debug(
+ "SELECTED_BOARD: %s",
+ _pretty_format(self.board[self.selected_board]),
+ )
_LOG.debug("PLATFORM: %s", _pretty_format(self.platform))
def selected_board_spec(self):
@@ -554,8 +611,10 @@ class ArduinoBuilder:
menu_match_result = self.MENU_OPTION_REGEX.match(key_name)
if menu_match_result:
menu_match = menu_match_result.groupdict()
- name = "menu.{}.{}".format(menu_match["menu_option_name"],
- menu_match["menu_option_value"])
+ name = "menu.{}.{}".format(
+ menu_match["menu_option_name"],
+ menu_match["menu_option_value"],
+ )
if len(name) > max_string_length[0]:
max_string_length[0] = len(name)
if len(description) > max_string_length[1]:
@@ -569,7 +628,8 @@ class ArduinoBuilder:
max_string_length = [0, 0]
for key_name, value in self.menu_options["default_board_values"][
- self.selected_board].items():
+ self.selected_board
+ ].items():
full_key = key_name + "." + value["name"]
if len(full_key) > max_string_length[0]:
max_string_length[0] = len(full_key)
@@ -596,10 +656,13 @@ class ArduinoBuilder:
if self.variant_includes:
line = compile_line.replace(
"{includes} \"{source_file}\" -o \"{object_file}\"",
- self.variant_includes, 1)
+ self.variant_includes,
+ 1,
+ )
else:
line = compile_line.replace(
- "{includes} \"{source_file}\" -o \"{object_file}\"", "", 1)
+ "{includes} \"{source_file}\" -o \"{object_file}\"", "", 1
+ )
return line
def _get_tool_name(self, line):
@@ -610,7 +673,8 @@ class ArduinoBuilder:
def get_upload_tool_names(self):
return [
- self._get_tool_name(t) for t in self.platform.keys()
+ self._get_tool_name(t)
+ for t in self.platform.keys()
if self.TOOL_NAME_REGEX.match(t) and 'upload.pattern' in t
]
@@ -626,7 +690,8 @@ class ArduinoBuilder:
line = self.platform.get(variable, False)
# Get all unique variables used in this line in line.
unique_vars = sorted(
- set(self.INTERPOLATED_VARIABLE_REGEX.findall(line)))
+ set(self.INTERPOLATED_VARIABLE_REGEX.findall(line))
+ )
# Search for each unique_vars in namespace and global.
for var in unique_vars:
v_raw_name = var.strip("{}")
@@ -639,8 +704,9 @@ class ArduinoBuilder:
# eg:
# ('tools.stm32CubeProg.cmd', 'stm32CubeProg.sh'),
# ('tools.stm32CubeProg.cmd.windows', 'stm32CubeProg.bat'),
- possible_var_name = "{}.{}.{}".format(namespace, v_raw_name,
- arduino_runtime_os_string())
+ possible_var_name = "{}.{}.{}".format(
+ namespace, v_raw_name, arduino_runtime_os_string()
+ )
os_override_result = self._get_platform_variable(possible_var_name)
if os_override_result:
@@ -654,6 +720,7 @@ class ArduinoBuilder:
return line
def get_upload_line(self, tool_name, serial_port=False):
+ """TODO(tonymd) Add docstring."""
# TODO(tonymd): Error if tool_name does not exist
tool_namespace = "tools.{}".format(tool_name)
pattern = "tools.{}.upload.pattern".format(tool_name)
@@ -665,7 +732,8 @@ class ArduinoBuilder:
return sys.exit(1)
line = self._get_platform_variable_with_substitutions(
- pattern, tool_namespace)
+ pattern, tool_namespace
+ )
# TODO(tonymd): Teensy specific tool overrides.
if tool_name == "teensyloader":
@@ -673,8 +741,9 @@ class ArduinoBuilder:
# {serial.port.label} and {serial.port.protocol} are returned by
# the teensy_ports binary.
line = line.replace("\"-portlabel={serial.port.label}\"", "", 1)
- line = line.replace("\"-portprotocol={serial.port.protocol}\"", "",
- 1)
+ line = line.replace(
+ "\"-portprotocol={serial.port.protocol}\"", "", 1
+ )
if serial_port == "UNKNOWN" or not serial_port:
line = line.replace('"-port={serial.port}"', "", 1)
@@ -685,9 +754,11 @@ class ArduinoBuilder:
def _get_binary_path(self, variable_pattern):
compile_line = self.replace_compile_binary_with_override_path(
- self._get_platform_variable(variable_pattern))
+ self._get_platform_variable(variable_pattern)
+ )
compile_binary, _ = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
+ compile_line
+ )
return compile_binary
def get_cc_binary(self):
@@ -715,10 +786,12 @@ class ArduinoBuilder:
for arg in replacement_line_args:
compile_binary_basename = os.path.basename(arg.strip("\""))
if compile_binary_basename in self.compiler_path_override_binaries:
- new_compiler = os.path.join(self.compiler_path_override,
- compile_binary_basename)
+ new_compiler = os.path.join(
+ self.compiler_path_override, compile_binary_basename
+ )
replacement_line = replacement_line.replace(
- arg, new_compiler, 1)
+ arg, new_compiler, 1
+ )
return replacement_line
def replace_compile_binary_with_override_path(self, compile_line):
@@ -727,94 +800,123 @@ class ArduinoBuilder:
# Change the compiler path if there's an override path set
if self.compiler_path_override:
compile_binary, line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
+ compile_line
+ )
compile_binary_basename = os.path.basename(
- compile_binary.strip("\""))
- new_compiler = os.path.join(self.compiler_path_override,
- compile_binary_basename)
+ compile_binary.strip("\"")
+ )
+ new_compiler = os.path.join(
+ self.compiler_path_override, compile_binary_basename
+ )
if platform.system() == "Windows" and not re.match(
- r".*\.exe$", new_compiler, flags=re.IGNORECASE):
+ r".*\.exe$", new_compiler, flags=re.IGNORECASE
+ ):
new_compiler += ".exe"
if os.path.isfile(new_compiler):
replacement_compile_line = "\"{}\" {}".format(
- new_compiler, line)
+ new_compiler, line
+ )
return replacement_compile_line
def get_c_compile_line(self):
- _LOG.debug("ARDUINO_C_COMPILE: %s",
- _pretty_format(self.platform["recipe.c.o.pattern"]))
+ _LOG.debug(
+ "ARDUINO_C_COMPILE: %s",
+ _pretty_format(self.platform["recipe.c.o.pattern"]),
+ )
compile_line = self.platform["recipe.c.o.pattern"]
compile_line = self._strip_includes_source_file_object_file_vars(
- compile_line)
+ compile_line
+ )
compile_line += " -I{}".format(
- self.board[self.selected_board]["build.core.path"])
+ self.board[self.selected_board]["build.core.path"]
+ )
compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
+ compile_line
+ )
return compile_line
def get_s_compile_line(self):
- _LOG.debug("ARDUINO_S_COMPILE %s",
- _pretty_format(self.platform["recipe.S.o.pattern"]))
+ _LOG.debug(
+ "ARDUINO_S_COMPILE %s",
+ _pretty_format(self.platform["recipe.S.o.pattern"]),
+ )
compile_line = self.platform["recipe.S.o.pattern"]
compile_line = self._strip_includes_source_file_object_file_vars(
- compile_line)
+ compile_line
+ )
compile_line += " -I{}".format(
- self.board[self.selected_board]["build.core.path"])
+ self.board[self.selected_board]["build.core.path"]
+ )
compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
+ compile_line
+ )
return compile_line
def get_ar_compile_line(self):
- _LOG.debug("ARDUINO_AR_COMPILE: %s",
- _pretty_format(self.platform["recipe.ar.pattern"]))
+ _LOG.debug(
+ "ARDUINO_AR_COMPILE: %s",
+ _pretty_format(self.platform["recipe.ar.pattern"]),
+ )
compile_line = self.platform["recipe.ar.pattern"].replace(
- "\"{object_file}\"", "", 1)
+ "\"{object_file}\"", "", 1
+ )
compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
+ compile_line
+ )
return compile_line
def get_cpp_compile_line(self):
- _LOG.debug("ARDUINO_CPP_COMPILE: %s",
- _pretty_format(self.platform["recipe.cpp.o.pattern"]))
+ _LOG.debug(
+ "ARDUINO_CPP_COMPILE: %s",
+ _pretty_format(self.platform["recipe.cpp.o.pattern"]),
+ )
compile_line = self.platform["recipe.cpp.o.pattern"]
compile_line = self._strip_includes_source_file_object_file_vars(
- compile_line)
+ compile_line
+ )
compile_line += " -I{}".format(
- self.board[self.selected_board]["build.core.path"])
+ self.board[self.selected_board]["build.core.path"]
+ )
compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
+ compile_line
+ )
return compile_line
def get_link_line(self):
- _LOG.debug("ARDUINO_LINK: %s",
- _pretty_format(self.platform["recipe.c.combine.pattern"]))
+ _LOG.debug(
+ "ARDUINO_LINK: %s",
+ _pretty_format(self.platform["recipe.c.combine.pattern"]),
+ )
compile_line = self.platform["recipe.c.combine.pattern"]
compile_line = self.replace_compile_binary_with_override_path(
- compile_line)
+ compile_line
+ )
return compile_line
def get_objcopy_step_names(self):
names = [
- name for name, line in self.platform.items()
+ name
+ for name, line in self.platform.items()
if self.OBJCOPY_STEP_NAME_REGEX.match(name)
]
return names
def get_objcopy_steps(self) -> List[str]:
lines = [
- line for name, line in self.platform.items()
+ line
+ for name, line in self.platform.items()
if self.OBJCOPY_STEP_NAME_REGEX.match(name)
]
lines = [
@@ -834,10 +936,12 @@ class ArduinoBuilder:
objcopy_step_names = self.get_objcopy_step_names()
objcopy_suffixes = [
- m[1] for m in [
+ m[1]
+ for m in [
self.OBJCOPY_STEP_NAME_REGEX.match(line)
for line in objcopy_step_names
- ] if m
+ ]
+ if m
]
if pattern not in objcopy_step_names:
_LOG.error("Error: objcopy suffix '%s' does not exist.", suffix)
@@ -868,8 +972,9 @@ class ArduinoBuilder:
# TODO(tonymd): Rename this to get_hooks(hook_name, step).
# TODO(tonymd): Add a list-hooks and or run-hooks command
def get_postbuild_line(self, step_number):
- line = self.platform["recipe.hooks.postbuild.{}.pattern".format(
- step_number)]
+ line = self.platform[
+ "recipe.hooks.postbuild.{}.pattern".format(step_number)
+ ]
line = self.replace_command_args_with_compiler_override_path(line)
return line
@@ -879,8 +984,11 @@ class ArduinoBuilder:
# TODO(tonymd): STM32 core uses recipe.hooks.prebuild.1.pattern.windows
# (should override non-windows key)
lines = [
- line for name, line in self.platform.items() if re.match(
- r"^recipe.hooks.(?:sketch.)?prebuild.[^.]+.pattern$", name)
+ line
+ for name, line in self.platform.items()
+ if re.match(
+ r"^recipe.hooks.(?:sketch.)?prebuild.[^.]+.pattern$", name
+ )
]
# TODO(tonymd): Write a function to fetch/replace OS specific patterns
# (ending in an OS string)
@@ -892,7 +1000,8 @@ class ArduinoBuilder:
def get_postbuild_steps(self) -> List[str]:
lines = [
- line for name, line in self.platform.items()
+ line
+ for name, line in self.platform.items()
if re.match(r"^recipe.hooks.postbuild.[^.]+.pattern$", name)
]
@@ -905,44 +1014,53 @@ class ArduinoBuilder:
def get_s_flags(self):
compile_line = self.get_s_compile_line()
_, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
+ compile_line
+ )
compile_line = compile_line.replace("-c", "", 1)
return compile_line.strip()
def get_c_flags(self):
compile_line = self.get_c_compile_line()
_, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
+ compile_line
+ )
compile_line = compile_line.replace("-c", "", 1)
return compile_line.strip()
def get_cpp_flags(self):
compile_line = self.get_cpp_compile_line()
_, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
+ compile_line
+ )
compile_line = compile_line.replace("-c", "", 1)
return compile_line.strip()
def get_ar_flags(self):
compile_line = self.get_ar_compile_line()
_, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
+ compile_line
+ )
return compile_line.strip()
def get_ld_flags(self):
compile_line = self.get_link_line()
_, compile_line = ArduinoBuilder.split_binary_from_arguments(
- compile_line)
+ compile_line
+ )
# TODO(tonymd): This is teensy specific
- line_to_delete = "-o \"{build.path}/{build.project_name}.elf\" " \
+ line_to_delete = (
+ "-o \"{build.path}/{build.project_name}.elf\" "
"{object_files} \"-L{build.path}\""
+ )
if self.build_path:
- line_to_delete = line_to_delete.replace("{build.path}",
- self.build_path)
+ line_to_delete = line_to_delete.replace(
+ "{build.path}", self.build_path
+ )
if self.build_project_name:
- line_to_delete = line_to_delete.replace("{build.project_name}",
- self.build_project_name)
+ line_to_delete = line_to_delete.replace(
+ "{build.project_name}", self.build_project_name
+ )
compile_line = compile_line.replace(line_to_delete, "", 1)
libs = re.findall(r'(-l[^ ]+ ?)', compile_line)
@@ -962,6 +1080,7 @@ class ArduinoBuilder:
return " ".join(libs)
def library_folders(self):
+ """TODO(tonymd) Add docstring."""
# Arduino library format documentation:
# https://arduino.github.io/arduino-cli/library-specification/#layout-of-folders-and-files
# - If src folder exists,
@@ -979,9 +1098,11 @@ class ArduinoBuilder:
library_folders = OrderedDict()
for library_dir in self.library_path:
found_library_names = file_operations.find_files(
- library_dir, folder_patterns, directories_only=True)
- _LOG.debug("Found Libraries %s: %s", library_dir,
- found_library_names)
+ library_dir, folder_patterns, directories_only=True
+ )
+ _LOG.debug(
+ "Found Libraries %s: %s", library_dir, found_library_names
+ )
for lib_name in found_library_names:
lib_dir = os.path.join(library_dir, lib_name)
src_dir = os.path.join(lib_dir, "src")
@@ -1029,8 +1150,9 @@ class ArduinoBuilder:
def core_files(self, pattern):
sources = []
- for file_path in file_operations.find_files(self.get_core_path(),
- [pattern]):
+ for file_path in file_operations.find_files(
+ self.get_core_path(), [pattern]
+ ):
sources.append(os.path.join(self.get_core_path(), file_path))
return sources
@@ -1050,9 +1172,9 @@ class ArduinoBuilder:
sources = []
if self.build_variant_path:
for file_path in file_operations.find_files(
- self.get_variant_path(), [pattern]):
- sources.append(os.path.join(self.get_variant_path(),
- file_path))
+ self.get_variant_path(), [pattern]
+ ):
+ sources.append(os.path.join(self.get_variant_path(), file_path))
return sources
def variant_c_files(self):
@@ -1066,10 +1188,12 @@ class ArduinoBuilder:
def project_files(self, pattern):
sources = []
- for file_path in file_operations.find_files(self.project_path,
- [pattern]):
+ for file_path in file_operations.find_files(
+ self.project_path, [pattern]
+ ):
if not file_path.startswith(
- "examples") and not file_path.startswith("libraries"):
+ "examples"
+ ) and not file_path.startswith("libraries"):
sources.append(file_path)
return sources
diff --git a/pw_arduino_build/py/pw_arduino_build/core_installer.py b/pw_arduino_build/py/pw_arduino_build/core_installer.py
index 662f90ee1..dcb1fe186 100644
--- a/pw_arduino_build/py/pw_arduino_build/core_installer.py
+++ b/pw_arduino_build/py/pw_arduino_build/core_installer.py
@@ -36,7 +36,6 @@ class ArduinoCoreNotSupported(Exception):
"""Exception raised when a given core can not be installed."""
-# yapf: disable
_ARDUINO_CORE_ARTIFACTS: Dict[str, Dict] = {
# pylint: disable=line-too-long
"teensy": {
@@ -111,7 +110,7 @@ _ARDUINO_CORE_ARTIFACTS: Dict[str, Dict] = {
"file_name": "Teensyduino.exe",
"sha256": "88f58681e5c4772c54e462bc88280320e4276e5b316dcab592fe38d96db990a1",
},
- }
+ },
},
"adafruit-samd": {
"all": {
@@ -153,7 +152,6 @@ _ARDUINO_CORE_ARTIFACTS: Dict[str, Dict] = {
"Windows": {},
},
}
-# yapf: enable
def install_core_command(args: argparse.Namespace):
@@ -162,7 +160,8 @@ def install_core_command(args: argparse.Namespace):
def install_core(prefix, core_name):
install_prefix = os.path.realpath(
- os.path.expanduser(os.path.expandvars(prefix)))
+ os.path.expanduser(os.path.expandvars(prefix))
+ )
install_dir = os.path.join(install_prefix, core_name)
cache_dir = os.path.join(install_prefix, ".cache", core_name)
@@ -188,7 +187,9 @@ def install_core(prefix, core_name):
else:
raise ArduinoCoreNotSupported(
"Invalid core '{}'. Supported cores: {}".format(
- core_name, ", ".join(supported_cores())))
+ core_name, ", ".join(supported_cores())
+ )
+ )
def supported_cores():
@@ -196,8 +197,7 @@ def supported_cores():
def get_windows_process_names() -> List[str]:
- result = subprocess.run("wmic process get description",
- capture_output=True)
+ result = subprocess.run("wmic process get description", capture_output=True)
output = result.stdout.decode().splitlines()
return [line.strip() for line in output if line]
@@ -211,14 +211,16 @@ def install_teensy_core_windows(install_prefix, install_dir, cache_dir):
url=arduino_artifact["url"],
expected_sha256sum=arduino_artifact["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=arduino_artifact["file_name"])
+ downloaded_file_name=arduino_artifact["file_name"],
+ )
teensyduino_artifact = teensy_artifacts["teensyduino"]
teensyduino_installer = file_operations.download_to_cache(
url=teensyduino_artifact["url"],
expected_sha256sum=teensyduino_artifact["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=teensyduino_artifact["file_name"])
+ downloaded_file_name=teensyduino_artifact["file_name"],
+ )
file_operations.extract_archive(arduino_zipfile, install_dir, cache_dir)
@@ -231,13 +233,14 @@ def install_teensy_core_windows(install_prefix, install_dir, cache_dir):
install_command = [teensyduino_installer, "--dir=teensy"]
_LOG.info(" Running: %s", " ".join(install_command))
- _LOG.info(" Please click yes on the Windows 'User Account Control' "
- "dialog.")
+ _LOG.info(
+ " Please click yes on the Windows 'User Account Control' " "dialog."
+ )
_LOG.info(" You should see: 'Verified publisher: PRJC.COM LLC'")
- def wait_for_process(process_name,
- timeout=30,
- result_operator=operator.truth):
+ def wait_for_process(
+ process_name, timeout=30, result_operator=operator.truth
+ ):
start_time = time.time()
while result_operator(process_name in get_windows_process_names()):
time.sleep(1)
@@ -245,19 +248,22 @@ def install_teensy_core_windows(install_prefix, install_dir, cache_dir):
_LOG.error(
"Error: Installation Failed.\n"
"Please click yes on the Windows 'User Account Control' "
- "dialog.")
+ "dialog."
+ )
sys.exit(1)
# Run Teensyduino installer with admin rights (non-blocking)
# User Account Control (UAC) will prompt the user for consent
import ctypes # pylint: disable=import-outside-toplevel
+
ctypes.windll.shell32.ShellExecuteW(
None, # parent window handle
"runas", # operation
teensyduino_installer, # file to run
subprocess.list2cmdline(install_command), # command parameters
install_prefix, # working directory
- 1) # Display mode (SW_SHOWNORMAL: Activates and displays a window)
+ 1,
+ ) # Display mode (SW_SHOWNORMAL: Activates and displays a window)
# Wait for teensyduino_installer to start running
wait_for_process("TeensyduinoInstall.exe", result_operator=operator.not_)
@@ -271,7 +277,9 @@ def install_teensy_core_windows(install_prefix, install_dir, cache_dir):
"Error: Installation Failed.\n"
"Please try again and ensure Teensyduino is installed in "
"the folder:\n"
- "%s", teensy_core_dir)
+ "%s",
+ teensy_core_dir,
+ )
sys.exit(1)
else:
_LOG.info("Install complete!")
@@ -289,17 +297,21 @@ def install_teensy_core_mac(unused_install_prefix, install_dir, cache_dir):
url=teensyduino_artifact["url"],
expected_sha256sum=teensyduino_artifact["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=teensyduino_artifact["file_name"])
+ downloaded_file_name=teensyduino_artifact["file_name"],
+ )
extracted_files = file_operations.extract_archive(
teensyduino_zip,
install_dir,
cache_dir,
- remove_single_toplevel_folder=False)
+ remove_single_toplevel_folder=False,
+ )
toplevel_folder = sorted(extracted_files)[0]
- os.symlink(os.path.join(toplevel_folder, "Contents", "Java", "hardware"),
- os.path.join(install_dir, "hardware"),
- target_is_directory=True)
+ os.symlink(
+ os.path.join(toplevel_folder, "Contents", "Java", "hardware"),
+ os.path.join(install_dir, "hardware"),
+ target_is_directory=True,
+ )
def install_teensy_core_linux(install_prefix, install_dir, cache_dir):
@@ -311,18 +323,22 @@ def install_teensy_core_linux(install_prefix, install_dir, cache_dir):
url=arduino_artifact["url"],
expected_sha256sum=arduino_artifact["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=arduino_artifact["file_name"])
+ downloaded_file_name=arduino_artifact["file_name"],
+ )
teensyduino_artifact = teensy_artifacts["teensyduino"]
teensyduino_installer = file_operations.download_to_cache(
url=teensyduino_artifact["url"],
expected_sha256sum=teensyduino_artifact["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=teensyduino_artifact["file_name"])
+ downloaded_file_name=teensyduino_artifact["file_name"],
+ )
file_operations.extract_archive(arduino_tarfile, install_dir, cache_dir)
- os.chmod(teensyduino_installer,
- os.stat(teensyduino_installer).st_mode | stat.S_IEXEC)
+ os.chmod(
+ teensyduino_installer,
+ os.stat(teensyduino_installer).st_mode | stat.S_IEXEC,
+ )
original_working_dir = os.getcwd()
os.chdir(install_prefix)
@@ -340,31 +356,36 @@ def apply_teensy_patches(install_dir):
# hardware -> Teensyduino.app/Contents/Java/hardware
# Resolve paths since `git apply` doesn't work if a path is beyond a
# symbolic link.
- patch_root_path = (Path(install_dir) /
- "hardware/teensy/avr/cores").resolve()
+ patch_root_path = (
+ Path(install_dir) / "hardware/teensy/avr/cores"
+ ).resolve()
# Get all *.diff files relative to this python file's parent directory.
patch_file_paths = sorted(
- (Path(__file__).parent / "core_patches/teensy").glob("*.diff"))
+ (Path(__file__).parent / "core_patches/teensy").glob("*.diff")
+ )
# Apply each patch file.
for diff_path in patch_file_paths:
- file_operations.git_apply_patch(patch_root_path.as_posix(),
- diff_path.as_posix(),
- unsafe_paths=True)
+ file_operations.git_apply_patch(
+ patch_root_path.as_posix(), diff_path.as_posix(), unsafe_paths=True
+ )
-def install_arduino_samd_core(install_prefix: str, install_dir: str,
- cache_dir: str):
+def install_arduino_samd_core(
+ install_prefix: str, install_dir: str, cache_dir: str
+):
artifacts = _ARDUINO_CORE_ARTIFACTS["arduino-samd"]["all"]["core"]
core_tarfile = file_operations.download_to_cache(
url=artifacts["url"],
expected_sha256sum=artifacts["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=artifacts["file_name"])
+ downloaded_file_name=artifacts["file_name"],
+ )
- package_path = os.path.join(install_dir, "hardware", "samd",
- artifacts["version"])
+ package_path = os.path.join(
+ install_dir, "hardware", "samd", artifacts["version"]
+ )
os.makedirs(package_path, exist_ok=True)
file_operations.extract_archive(core_tarfile, package_path, cache_dir)
original_working_dir = os.getcwd()
@@ -375,17 +396,20 @@ def install_arduino_samd_core(install_prefix: str, install_dir: str,
return True
-def install_adafruit_samd_core(install_prefix: str, install_dir: str,
- cache_dir: str):
+def install_adafruit_samd_core(
+ install_prefix: str, install_dir: str, cache_dir: str
+):
artifacts = _ARDUINO_CORE_ARTIFACTS["adafruit-samd"]["all"]["core"]
core_tarfile = file_operations.download_to_cache(
url=artifacts["url"],
expected_sha256sum=artifacts["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=artifacts["file_name"])
+ downloaded_file_name=artifacts["file_name"],
+ )
- package_path = os.path.join(install_dir, "hardware", "samd",
- artifacts["version"])
+ package_path = os.path.join(
+ install_dir, "hardware", "samd", artifacts["version"]
+ )
os.makedirs(package_path, exist_ok=True)
file_operations.extract_archive(core_tarfile, package_path, cache_dir)
@@ -405,10 +429,12 @@ def install_stm32duino_core(install_prefix, install_dir, cache_dir):
url=artifacts["url"],
expected_sha256sum=artifacts["sha256"],
cache_directory=cache_dir,
- downloaded_file_name=artifacts["file_name"])
+ downloaded_file_name=artifacts["file_name"],
+ )
- package_path = os.path.join(install_dir, "hardware", "stm32",
- artifacts["version"])
+ package_path = os.path.join(
+ install_dir, "hardware", "stm32", artifacts["version"]
+ )
os.makedirs(package_path, exist_ok=True)
file_operations.extract_archive(core_tarfile, package_path, cache_dir)
original_working_dir = os.getcwd()
diff --git a/pw_arduino_build/py/pw_arduino_build/file_operations.py b/pw_arduino_build/py/pw_arduino_build/file_operations.py
index eb5fa41ab..d3178231e 100644
--- a/pw_arduino_build/py/pw_arduino_build/file_operations.py
+++ b/pw_arduino_build/py/pw_arduino_build/file_operations.py
@@ -35,20 +35,22 @@ class InvalidChecksumError(Exception):
pass
-def find_files(starting_dir: str,
- patterns: List[str],
- directories_only=False) -> List[str]:
+def find_files(
+ starting_dir: str, patterns: List[str], directories_only=False
+) -> List[str]:
original_working_dir = os.getcwd()
if not (os.path.exists(starting_dir) and os.path.isdir(starting_dir)):
raise FileNotFoundError(
- "Directory '{}' does not exist.".format(starting_dir))
+ "Directory '{}' does not exist.".format(starting_dir)
+ )
os.chdir(starting_dir)
files = []
for pattern in patterns:
for file_path in glob.glob(pattern, recursive=True):
- if not directories_only or (directories_only
- and os.path.isdir(file_path)):
+ if not directories_only or (
+ directories_only and os.path.isdir(file_path)
+ ):
files.append(file_path)
os.chdir(original_working_dir)
return sorted(files)
@@ -70,9 +72,7 @@ def md5_sum(file_name):
return hash_md5.hexdigest()
-def verify_file_checksum(file_path,
- expected_checksum,
- sum_function=sha256_sum):
+def verify_file_checksum(file_path, expected_checksum, sum_function=sha256_sum):
downloaded_checksum = sum_function(file_path)
if downloaded_checksum != expected_checksum:
raise InvalidChecksumError(
@@ -80,7 +80,8 @@ def verify_file_checksum(file_path,
f"{downloaded_checksum} {os.path.basename(file_path)}\n"
f"{expected_checksum} (expected)\n\n"
"Please delete this file and try again:\n"
- f"{file_path}")
+ f"{file_path}"
+ )
_LOG.debug(" %s:", sum_function.__name__)
_LOG.debug(" %s %s", downloaded_checksum, os.path.basename(file_path))
@@ -96,14 +97,18 @@ def relative_or_absolute_path(file_string: str):
return file_path.resolve()
-def download_to_cache(url: str,
- expected_md5sum=None,
- expected_sha256sum=None,
- cache_directory=".cache",
- downloaded_file_name=None) -> str:
+def download_to_cache(
+ url: str,
+ expected_md5sum=None,
+ expected_sha256sum=None,
+ cache_directory=".cache",
+ downloaded_file_name=None,
+) -> str:
+ """TODO(tonymd) Add docstring."""
cache_dir = os.path.realpath(
- os.path.expanduser(os.path.expandvars(cache_directory)))
+ os.path.expanduser(os.path.expandvars(cache_directory))
+ )
if not downloaded_file_name:
# Use the last part of the URL as the file name.
downloaded_file_name = url.split("/")[-1]
@@ -117,13 +122,13 @@ def download_to_cache(url: str,
if os.path.exists(downloaded_file):
_LOG.info("Downloaded: %s", relative_or_absolute_path(downloaded_file))
if expected_sha256sum:
- verify_file_checksum(downloaded_file,
- expected_sha256sum,
- sum_function=sha256_sum)
+ verify_file_checksum(
+ downloaded_file, expected_sha256sum, sum_function=sha256_sum
+ )
elif expected_md5sum:
- verify_file_checksum(downloaded_file,
- expected_md5sum,
- sum_function=md5_sum)
+ verify_file_checksum(
+ downloaded_file, expected_md5sum, sum_function=md5_sum
+ )
return downloaded_file
@@ -144,10 +149,12 @@ def extract_tarfile(archive_file: str, dest_dir: str):
archive.extractall(path=dest_dir)
-def extract_archive(archive_file: str,
- dest_dir: str,
- cache_dir: str,
- remove_single_toplevel_folder=True):
+def extract_archive(
+ archive_file: str,
+ dest_dir: str,
+ cache_dir: str,
+ remove_single_toplevel_folder=True,
+):
"""Extract a tar or zip file.
Args:
@@ -159,8 +166,9 @@ def extract_archive(archive_file: str,
directory.
"""
# Make a temporary directory to extract files into
- temp_extract_dir = os.path.join(cache_dir,
- "." + os.path.basename(archive_file))
+ temp_extract_dir = os.path.join(
+ cache_dir, "." + os.path.basename(archive_file)
+ )
os.makedirs(temp_extract_dir, exist_ok=True)
_LOG.info("Extracting: %s", relative_or_absolute_path(archive_file))
@@ -179,8 +187,9 @@ def extract_archive(archive_file: str,
# Check if tarfile has only one folder
# If yes, make that the new path_to_extracted_files
if remove_single_toplevel_folder and len(extracted_top_level_files) == 1:
- path_to_extracted_files = os.path.join(temp_extract_dir,
- extracted_top_level_files[0])
+ path_to_extracted_files = os.path.join(
+ temp_extract_dir, extracted_top_level_files[0]
+ )
# Move extracted files to dest_dir
extracted_files = os.listdir(path_to_extracted_files)
@@ -215,7 +224,8 @@ def decode_file_json(file_name):
# Get absolute path to the file.
file_path = os.path.realpath(
- os.path.expanduser(os.path.expandvars(file_name)))
+ os.path.expanduser(os.path.expandvars(file_name))
+ )
json_file_options = {}
try:
@@ -227,10 +237,9 @@ def decode_file_json(file_name):
return json_file_options, file_path
-def git_apply_patch(root_directory,
- patch_file,
- ignore_whitespace=True,
- unsafe_paths=False):
+def git_apply_patch(
+ root_directory, patch_file, ignore_whitespace=True, unsafe_paths=False
+):
"""Use `git apply` to apply a diff file."""
_LOG.info("Applying Patch: %s", patch_file)
diff --git a/pw_arduino_build/py/pw_arduino_build/log.py b/pw_arduino_build/py/pw_arduino_build/log.py
index d5afb3035..285583134 100644
--- a/pw_arduino_build/py/pw_arduino_build/log.py
+++ b/pw_arduino_build/py/pw_arduino_build/log.py
@@ -24,6 +24,7 @@ def install(level: int = logging.INFO) -> None:
try:
import pw_cli.log # pylint: disable=import-outside-toplevel
+
pw_cli.log.install(level=level)
except ImportError:
# Set log level on root logger to debug, otherwise any higher levels
@@ -33,8 +34,10 @@ def install(level: int = logging.INFO) -> None:
_STDERR_HANDLER.setLevel(level)
_STDERR_HANDLER.setFormatter(
- logging.Formatter("[%(asctime)s] "
- "%(levelname)s %(message)s", "%Y%m%d %H:%M:%S"))
+ logging.Formatter(
+ "[%(asctime)s] " "%(levelname)s %(message)s", "%Y%m%d %H:%M:%S"
+ )
+ )
root.addHandler(_STDERR_HANDLER)
diff --git a/pw_arduino_build/py/pw_arduino_build/teensy_detector.py b/pw_arduino_build/py/pw_arduino_build/teensy_detector.py
index 74e9892eb..d00e68ec4 100644
--- a/pw_arduino_build/py/pw_arduino_build/teensy_detector.py
+++ b/pw_arduino_build/py/pw_arduino_build/teensy_detector.py
@@ -41,6 +41,7 @@ def log_subprocess_output(level, output):
class BoardInfo(typing.NamedTuple):
"""Information about a connected dev board."""
+
dev_name: str
usb_device_path: str
protocol: str
@@ -49,9 +50,12 @@ class BoardInfo(typing.NamedTuple):
def test_runner_args(self) -> List[str]:
return [
- "--set-variable", f"serial.port.protocol={self.protocol}",
- "--set-variable", f"serial.port={self.usb_device_path}",
- "--set-variable", f"serial.port.label={self.dev_name}"
+ "--set-variable",
+ f"serial.port.protocol={self.protocol}",
+ "--set-variable",
+ f"serial.port={self.usb_device_path}",
+ "--set-variable",
+ f"serial.port.label={self.dev_name}",
]
@@ -65,18 +69,24 @@ def detect_boards(arduino_package_path=False) -> list:
teensy_core = Path("third_party/arduino/cores/teensy")
if not teensy_core.exists():
teensy_core = Path(
- "third_party/pigweed/third_party/arduino/cores/teensy")
+ "third_party/pigweed/third_party/arduino/cores/teensy"
+ )
if not teensy_core.exists():
raise UnknownArduinoCore
teensy_device_line_regex = re.compile(
r"^(?P<address>[^ ]+) (?P<dev_name>[^ ]+) "
- r"\((?P<label>[^)]+)\) ?(?P<rest>.*)$")
+ r"\((?P<label>[^)]+)\) ?(?P<rest>.*)$"
+ )
boards = []
- detect_command = [(teensy_core / "hardware" / "tools" /
- "teensy_ports").absolute().as_posix(), "-L"]
+ detect_command = [
+ (teensy_core / "hardware" / "tools" / "teensy_ports")
+ .absolute()
+ .as_posix(),
+ "-L",
+ ]
# TODO(tonymd): teensy_ports -L on windows does not return the right port
# string Example:
@@ -87,9 +97,9 @@ def detect_boards(arduino_package_path=False) -> list:
# So we get "-port=Port_#0001.Hub_#0003"
# But it should be "-port=usb:0/140000/0/1"
- process = subprocess.run(detect_command,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ process = subprocess.run(
+ detect_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
if process.returncode != 0:
_LOG.error("Command failed with exit code %d.", process.returncode)
_LOG.error("Full command:")
@@ -104,11 +114,14 @@ def detect_boards(arduino_package_path=False) -> list:
if device_match_result:
teensy_device = device_match_result.groupdict()
boards.append(
- BoardInfo(dev_name=teensy_device["dev_name"],
- usb_device_path=teensy_device["address"],
- protocol="Teensy",
- label=teensy_device["label"],
- arduino_upload_tool_name="teensyloader"))
+ BoardInfo(
+ dev_name=teensy_device["dev_name"],
+ usb_device_path=teensy_device["address"],
+ protocol="Teensy",
+ label=teensy_device["label"],
+ arduino_upload_tool_name="teensyloader",
+ )
+ )
return boards
@@ -125,8 +138,9 @@ def main():
_LOG.info(" - Name: %s", board.label)
_LOG.info(" - Port: %s", board.dev_name)
_LOG.info(" - Address: %s", board.usb_device_path)
- _LOG.info(" - Test runner args: %s",
- " ".join(board.test_runner_args()))
+ _LOG.info(
+ " - Test runner args: %s", " ".join(board.test_runner_args())
+ )
if __name__ == "__main__":
diff --git a/pw_arduino_build/py/pw_arduino_build/unit_test_client.py b/pw_arduino_build/py/pw_arduino_build/unit_test_client.py
index 38227f56a..ae3b4a1f8 100755
--- a/pw_arduino_build/py/pw_arduino_build/unit_test_client.py
+++ b/pw_arduino_build/py/pw_arduino_build/unit_test_client.py
@@ -27,13 +27,17 @@ def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('binary', help='The target test binary to run')
- parser.add_argument('--server-port',
- type=int,
- default=8081,
- help='Port the test server is located on')
- parser.add_argument('runner_args',
- nargs=argparse.REMAINDER,
- help='Arguments to forward to the test runner')
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ default=8081,
+ help='Port the test server is located on',
+ )
+ parser.add_argument(
+ 'runner_args',
+ nargs=argparse.REMAINDER,
+ help='Arguments to forward to the test runner',
+ )
return parser.parse_args()
diff --git a/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py b/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py
index 4fa601162..05678f751 100755
--- a/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py
+++ b/pw_arduino_build/py/pw_arduino_build/unit_test_runner.py
@@ -25,8 +25,8 @@ import time
from pathlib import Path
from typing import List
-import serial # type: ignore
-import serial.tools.list_ports # type: ignore
+import serial
+import serial.tools.list_ports
import pw_arduino_build.log
from pw_arduino_build import teensy_detector
from pw_arduino_build.file_operations import decode_file_json
@@ -69,56 +69,74 @@ def parse_args():
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('binary',
- help='The target test binary to run',
- type=valid_file_name)
- parser.add_argument('--port',
- help='The name of the serial port to connect to when '
- 'running tests')
- parser.add_argument('--baud',
- type=int,
- default=115200,
- help='Target baud rate to use for serial communication'
- ' with target device')
- parser.add_argument('--test-timeout',
- type=float,
- default=5.0,
- help='Maximum communication delay in seconds before a '
- 'test is considered unresponsive and aborted')
- parser.add_argument('--verbose',
- '-v',
- dest='verbose',
- action='store_true',
- help='Output additional logs as the script runs')
-
- parser.add_argument('--flash-only',
- action='store_true',
- help="Don't check for test output after flashing.")
+ parser.add_argument(
+ 'binary', help='The target test binary to run', type=valid_file_name
+ )
+ parser.add_argument(
+ '--port',
+ help='The name of the serial port to connect to when ' 'running tests',
+ )
+ parser.add_argument(
+ '--baud',
+ type=int,
+ default=115200,
+ help='Target baud rate to use for serial communication'
+ ' with target device',
+ )
+ parser.add_argument(
+ '--test-timeout',
+ type=float,
+ default=5.0,
+ help='Maximum communication delay in seconds before a '
+ 'test is considered unresponsive and aborted',
+ )
+ parser.add_argument(
+ '--verbose',
+ '-v',
+ dest='verbose',
+ action='store_true',
+ help='Output additional logs as the script runs',
+ )
+
+ parser.add_argument(
+ '--flash-only',
+ action='store_true',
+ help="Don't check for test output after flashing.",
+ )
# arduino_builder arguments
# TODO(tonymd): Get these args from __main__.py or elsewhere.
- parser.add_argument("-c",
- "--config-file",
- required=True,
- help="Path to a config file.")
- parser.add_argument("--arduino-package-path",
- help="Path to the arduino IDE install location.")
- parser.add_argument("--arduino-package-name",
- help="Name of the Arduino board package to use.")
- parser.add_argument("--compiler-path-override",
- help="Path to arm-none-eabi-gcc bin folder. "
- "Default: Arduino core specified gcc")
+ parser.add_argument(
+ "-c", "--config-file", required=True, help="Path to a config file."
+ )
+ parser.add_argument(
+ "--arduino-package-path",
+ help="Path to the arduino IDE install location.",
+ )
+ parser.add_argument(
+ "--arduino-package-name",
+ help="Name of the Arduino board package to use.",
+ )
+ parser.add_argument(
+ "--compiler-path-override",
+ help="Path to arm-none-eabi-gcc bin folder. "
+ "Default: Arduino core specified gcc",
+ )
parser.add_argument("--board", help="Name of the Arduino board to use.")
- parser.add_argument("--upload-tool",
- required=True,
- help="Name of the Arduino upload tool to use.")
- parser.add_argument("--set-variable",
- action="append",
- metavar='some.variable=NEW_VALUE',
- help="Override an Arduino recipe variable. May be "
- "specified multiple times. For example: "
- "--set-variable 'serial.port.label=/dev/ttyACM0' "
- "--set-variable 'serial.port.protocol=Teensy'")
+ parser.add_argument(
+ "--upload-tool",
+ required=True,
+ help="Name of the Arduino upload tool to use.",
+ )
+ parser.add_argument(
+ "--set-variable",
+ action="append",
+ metavar='some.variable=NEW_VALUE',
+ help="Override an Arduino recipe variable. May be "
+ "specified multiple times. For example: "
+ "--set-variable 'serial.port.label=/dev/ttyACM0' "
+ "--set-variable 'serial.port.protocol=Teensy'",
+ )
return parser.parse_args()
@@ -137,9 +155,9 @@ def read_serial(port, baud_rate, test_timeout) -> bytes:
"""
serial_data = bytearray()
- device = serial.Serial(baudrate=baud_rate,
- port=port,
- timeout=_FLASH_TIMEOUT)
+ device = serial.Serial(
+ baudrate=baud_rate, port=port, timeout=_FLASH_TIMEOUT
+ )
if not device.is_open:
raise TestingFailure('Failed to open device')
@@ -170,8 +188,11 @@ def read_serial(port, baud_rate, test_timeout) -> bytes:
# Try to trim captured results to only contain most recent test run.
test_start_index = serial_data.rfind(_TESTS_STARTING_STRING)
- return serial_data if test_start_index == -1 else serial_data[
- test_start_index:]
+ return (
+ serial_data
+ if test_start_index == -1
+ else serial_data[test_start_index:]
+ )
def wait_for_port(port):
@@ -186,17 +207,18 @@ def flash_device(test_runner_args, upload_tool):
# TODO(tonymd): Create a library function to call rather than launching
# the arduino_builder script.
flash_tool = 'arduino_builder'
- cmd = [flash_tool, "--quiet"] + test_runner_args + [
- "--run-objcopy", "--run-postbuilds", "--run-upload", upload_tool
- ]
+ cmd = (
+ [flash_tool, "--quiet"]
+ + test_runner_args
+ + ["--run-objcopy", "--run-postbuilds", "--run-upload", upload_tool]
+ )
_LOG.info('Flashing firmware to device')
_LOG.debug('Running: %s', " ".join(cmd))
env = os.environ.copy()
- process = subprocess.run(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- env=env)
+ process = subprocess.run(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
+ )
if process.returncode:
log_subprocess_output(logging.ERROR, process.stdout)
raise TestingFailure('Failed to flash target device')
@@ -225,8 +247,16 @@ def handle_test_results(test_output):
_LOG.info('Test passed!')
-def run_device_test(binary, flash_only, port, baud, test_timeout, upload_tool,
- arduino_package_path, test_runner_args) -> bool:
+def run_device_test(
+ binary,
+ flash_only,
+ port,
+ baud,
+ test_timeout,
+ upload_tool,
+ arduino_package_path,
+ test_runner_args,
+) -> bool:
"""Flashes, runs, and checks an on-device test binary.
Returns true on test pass.
@@ -253,7 +283,8 @@ def run_device_test(binary, flash_only, port, baud, test_timeout, upload_tool,
if platform.system() == "Windows":
# Delete the incorrect serial port.
index_of_port = [
- i for i, l in enumerate(test_runner_args)
+ i
+ for i, l in enumerate(test_runner_args)
if l.startswith('serial.port=')
]
if index_of_port:
@@ -290,8 +321,10 @@ def get_option(key, config_file_values, args, required=False):
# Print a similar error message to argparse
executable = os.path.basename(sys.argv[0])
option = "--" + key.replace("_", "-")
- print(f"{executable}: error: the following arguments are required: "
- f"{option}")
+ print(
+ f"{executable}: error: the following arguments are required: "
+ f"{option}"
+ )
sys.exit(1)
return final_option
@@ -306,33 +339,34 @@ def main():
pw_arduino_build.log.install(log_level)
# Construct arduino_builder flash arguments for a given .elf binary.
- arduino_package_path = get_option("arduino_package_path",
- json_file_options,
- args,
- required=True)
+ arduino_package_path = get_option(
+ "arduino_package_path", json_file_options, args, required=True
+ )
# Arduino core args.
arduino_builder_args = [
"--arduino-package-path",
arduino_package_path,
"--arduino-package-name",
- get_option("arduino_package_name",
- json_file_options,
- args,
- required=True),
+ get_option(
+ "arduino_package_name", json_file_options, args, required=True
+ ),
]
# Use CIPD installed compilers.
- compiler_path_override = get_option("compiler_path_override",
- json_file_options, args)
+ compiler_path_override = get_option(
+ "compiler_path_override", json_file_options, args
+ )
if compiler_path_override:
arduino_builder_args += [
- "--compiler-path-override", compiler_path_override
+ "--compiler-path-override",
+ compiler_path_override,
]
# Run subcommand with board selection arg.
arduino_builder_args += [
- "run", "--board",
- get_option("board", json_file_options, args, required=True)
+ "run",
+ "--board",
+ get_option("board", json_file_options, args, required=True),
]
# .elf file location args.
@@ -351,14 +385,16 @@ def main():
for var in args.set_variable:
arduino_builder_args += ["--set-variable", var]
- if run_device_test(binary.as_posix(),
- args.flash_only,
- args.port,
- args.baud,
- args.test_timeout,
- args.upload_tool,
- arduino_package_path,
- test_runner_args=arduino_builder_args):
+ if run_device_test(
+ binary.as_posix(),
+ args.flash_only,
+ args.port,
+ args.baud,
+ args.test_timeout,
+ args.upload_tool,
+ arduino_package_path,
+ test_runner_args=arduino_builder_args,
+ ):
sys.exit(0)
else:
sys.exit(1)
diff --git a/pw_arduino_build/py/pw_arduino_build/unit_test_server.py b/pw_arduino_build/py/pw_arduino_build/unit_test_server.py
index 947b2ae8e..6291c2f38 100644
--- a/pw_arduino_build/py/pw_arduino_build/unit_test_server.py
+++ b/pw_arduino_build/py/pw_arduino_build/unit_test_server.py
@@ -42,27 +42,37 @@ def parse_args():
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--server-port',
- type=int,
- default=8081,
- help='Port to launch the pw_target_runner_server on')
- parser.add_argument('--server-config',
- type=argparse.FileType('r'),
- help='Path to server config file')
- parser.add_argument('--verbose',
- '-v',
- dest='verbose',
- action="store_true",
- help='Output additional logs as the script runs')
- parser.add_argument("-c",
- "--config-file",
- required=True,
- help="Path to an arduino_builder config file.")
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ default=8081,
+ help='Port to launch the pw_target_runner_server on',
+ )
+ parser.add_argument(
+ '--server-config',
+ type=argparse.FileType('r'),
+ help='Path to server config file',
+ )
+ parser.add_argument(
+ '--verbose',
+ '-v',
+ dest='verbose',
+ action="store_true",
+ help='Output additional logs as the script runs',
+ )
+ parser.add_argument(
+ "-c",
+ "--config-file",
+ required=True,
+ help="Path to an arduino_builder config file.",
+ )
# TODO(tonymd): Explicitly split args using "--". See example in:
# //pw_unit_test/py/pw_unit_test/test_runner.py:326
- parser.add_argument('runner_args',
- nargs=argparse.REMAINDER,
- help='Arguments to forward to the test runner')
+ parser.add_argument(
+ 'runner_args',
+ nargs=argparse.REMAINDER,
+ help='Arguments to forward to the test runner',
+ )
return parser.parse_args()
@@ -79,8 +89,9 @@ def generate_runner(command: str, arguments: List[str]) -> str:
return '\n'.join(runner)
-def generate_server_config(runner_args: Optional[List[str]],
- arduino_package_path: str) -> IO[bytes]:
+def generate_server_config(
+ runner_args: Optional[List[str]], arduino_package_path: str
+) -> IO[bytes]:
"""Returns a temporary generated file for use as the server config."""
if "teensy" not in arduino_package_path:
@@ -101,20 +112,26 @@ def generate_server_config(runner_args: Optional[List[str]],
test_runner_args += ["--port", board.dev_name]
test_runner_args += ["--upload-tool", board.arduino_upload_tool_name]
config_file.write(
- generate_runner(_TEST_RUNNER_COMMAND,
- test_runner_args).encode('utf-8'))
+ generate_runner(_TEST_RUNNER_COMMAND, test_runner_args).encode(
+ 'utf-8'
+ )
+ )
config_file.flush()
return config_file
-def launch_server(server_config: Optional[IO[bytes]],
- server_port: Optional[int], runner_args: Optional[List[str]],
- arduino_package_path: str) -> int:
+def launch_server(
+ server_config: Optional[IO[bytes]],
+ server_port: Optional[int],
+ runner_args: Optional[List[str]],
+ arduino_package_path: str,
+) -> int:
"""Launch a device test server with the provided arguments."""
if server_config is None:
# Auto-detect attached boards if no config is provided.
- server_config = generate_server_config(runner_args,
- arduino_package_path)
+ server_config = generate_server_config(
+ runner_args, arduino_package_path
+ )
cmd = [_TEST_SERVER_COMMAND, '-config', server_config.name]
@@ -138,9 +155,11 @@ def main():
arduino_package_path = None
if args.config_file:
json_file_options, unused_config_path = decode_file_json(
- args.config_file)
- arduino_package_path = json_file_options.get("arduino_package_path",
- None)
+ args.config_file
+ )
+ arduino_package_path = json_file_options.get(
+ "arduino_package_path", None
+ )
# Must pass --config-file option in the runner_args.
if "--config-file" not in args.runner_args:
args.runner_args.append("--config-file")
@@ -149,15 +168,21 @@ def main():
# Check for arduino_package_path in the runner_args
try:
arduino_package_path = args.runner_args[
- args.runner_args.index("--arduino-package-path") + 1]
+ args.runner_args.index("--arduino-package-path") + 1
+ ]
except (ValueError, IndexError):
# Only raise an error if arduino_package_path not set from the json.
if arduino_package_path is None:
- raise UnknownArduinoCore("Test runner arguments: '{}'".format(
- " ".join(args.runner_args)))
-
- exit_code = launch_server(args.server_config, args.server_port,
- args.runner_args, arduino_package_path)
+ raise UnknownArduinoCore(
+ "Test runner arguments: '{}'".format(" ".join(args.runner_args))
+ )
+
+ exit_code = launch_server(
+ args.server_config,
+ args.server_port,
+ args.runner_args,
+ arduino_package_path,
+ )
sys.exit(exit_code)
diff --git a/pw_arduino_build/py/setup.cfg b/pw_arduino_build/py/setup.cfg
index 05f6d2a00..f59cba7f4 100644
--- a/pw_arduino_build/py/setup.cfg
+++ b/pw_arduino_build/py/setup.cfg
@@ -23,6 +23,7 @@ packages = find:
zip_safe = False
install_requires =
pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
coloredlogs
parameterized
diff --git a/pw_assert/Android.bp b/pw_assert/Android.bp
new file mode 100644
index 000000000..8350f15b3
--- /dev/null
+++ b/pw_assert/Android.bp
@@ -0,0 +1,25 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_assert_headers",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ host_supported: true,
+}
diff --git a/pw_assert/BUILD.bazel b/pw_assert/BUILD.bazel
index 080fc99d8..f6d1f8616 100644
--- a/pw_assert/BUILD.bazel
+++ b/pw_assert/BUILD.bazel
@@ -26,20 +26,18 @@ licenses(["notice"])
pw_cc_facade(
name = "facade",
hdrs = [
- "assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h",
- "print_and_abort_public_overrides/pw_assert_backend/assert_lite_backend.h",
+ "assert_compatibility_public_overrides/pw_assert_backend/assert_backend.h",
"public/pw_assert/assert.h",
"public/pw_assert/check.h",
- "public/pw_assert/config.h",
"public/pw_assert/internal/check_impl.h",
- "public/pw_assert/internal/print_and_abort.h",
"public/pw_assert/short.h",
],
includes = [
- "assert_lite_public_overrides",
+ "assert_compatibility_public_overrides",
"public",
],
deps = [
+ ":config",
"//pw_preprocessor",
],
)
@@ -53,9 +51,60 @@ pw_cc_library(
)
pw_cc_library(
+ name = "config",
+ hdrs = ["public/pw_assert/config.h"],
+ includes = ["public"],
+)
+
+pw_cc_library(
+ name = "libc_assert",
+ hdrs = [
+ "libc_assert_public_overrides/assert.h",
+ "libc_assert_public_overrides/cassert",
+ "public/pw_assert/internal/libc_assert.h",
+ ],
+ includes = [
+ "libc_assert_public_overrides",
+ "public",
+ ],
+ deps = [
+ "//pw_assert",
+ "//pw_preprocessor",
+ ],
+)
+
+pw_cc_library(
+ name = "print_and_abort",
+ hdrs = ["public/pw_assert/internal/print_and_abort.h"],
+ includes = ["public"],
+ visibility = ["//visibility:private"],
+ deps = [":config"],
+)
+
+pw_cc_library(
+ name = "print_and_abort_assert_backend",
+ hdrs = ["print_and_abort_assert_public_overrides/pw_assert_backend/assert_backend.h"],
+ includes = ["print_and_abort_assert_public_overrides"],
+ deps = [
+ ":config",
+ ":print_and_abort",
+ ],
+)
+
+pw_cc_library(
+ name = "print_and_abort_check_backend",
+ hdrs =
+ ["print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h"],
+ includes = ["print_and_abort_public_overrides"],
+ deps = [":print_and_abort"],
+)
+
+pw_cc_library(
name = "backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
- deps = ["@pigweed//pw_assert_basic"],
+ deps = [
+ "@pigweed//pw_assert_basic",
+ ],
)
pw_cc_test(
@@ -70,8 +119,10 @@ pw_cc_test(
deps = [
":facade",
"//pw_assert",
+ "//pw_compilation_testing:negative_compilation_testing",
"//pw_preprocessor",
"//pw_span",
+ "//pw_status",
"//pw_string",
"//pw_unit_test",
],
@@ -85,6 +136,7 @@ pw_cc_test(
],
deps = [
"//pw_assert",
+ "//pw_status",
"//pw_unit_test",
],
)
diff --git a/pw_assert/BUILD.gn b/pw_assert/BUILD.gn
index 15882105f..28552dc0f 100644
--- a/pw_assert/BUILD.gn
+++ b/pw_assert/BUILD.gn
@@ -32,13 +32,23 @@ config("public_include_path") {
visibility = [ ":*" ]
}
-config("lite_backend_overrides") {
- include_dirs = [ "assert_lite_public_overrides" ]
+config("assert_backend_overrides") {
+ include_dirs = [ "assert_compatibility_public_overrides" ]
visibility = [ ":*" ]
}
-config("print_and_abort_backend_overrides") {
- include_dirs = [ "print_and_abort_public_overrides" ]
+config("libc_assert_overrides") {
+ include_dirs = [ "libc_assert_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
+config("print_and_abort_check_backend_overrides") {
+ include_dirs = [ "print_and_abort_check_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
+config("print_and_abort_assert_backend_overrides") {
+ include_dirs = [ "print_and_abort_assert_public_overrides" ]
visibility = [ ":*" ]
}
@@ -99,40 +109,66 @@ pw_facade("assert") {
# This backend to pw_assert's PW_ASSERT()/PW_DASSERT() macros provides backwards
# compatibility with pw_assert's previous C-symbol based API.
#
-# Warning: The "lite" naming is transitional. assert_lite_backend.h headers
-# will be renamed as the pw_assert API is reassessed. (pwbug/246)
-pw_source_set("lite_compatibility_backend") {
- public_configs = [ ":lite_backend_overrides" ]
+# Warning: The assert facade is in a transitional state, and this target is
+# likely to be removed as the pw_assert API is reassessed. (b/235149326)
+pw_source_set("assert_compatibility_backend") {
+ public_configs = [ ":assert_backend_overrides" ]
public_deps = [ dir_pw_preprocessor ]
- public =
- [ "assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h" ]
+ public = [
+ "assert_compatibility_public_overrides/pw_assert_backend/assert_backend.h",
+ ]
}
-group("lite_compatibility_backend.impl") {
+group("assert_compatibility_backend.impl") {
}
-# This backend to pw_assert's PW_ASSERT()/PW_DASSERT() macros prints the assert
-# expression, file/line number, and function with printf, then aborts. It is
-# intended for use with host builds.
-#
-# Warning: The "lite" naming is transitional. assert_lite_backend.h headers
-# will be renamed as the pw_assert API is reassessed. (pwbug/246)
-pw_source_set("print_and_abort") {
- public_configs = [
- ":print_and_abort_backend_overrides",
- ":public_include_path",
+pw_source_set("libc_assert") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "libc_assert_public_overrides/assert.h",
+ "libc_assert_public_overrides/cassert",
+ "public/pw_assert/internal/libc_assert.h",
]
public_deps = [
- ":config",
+ ":assert",
dir_pw_preprocessor,
]
+}
+
+pw_source_set("print_and_abort") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ ":config" ]
+ public = [ "public/pw_assert/internal/print_and_abort.h" ]
+ visibility = [ ":*" ]
+}
+
+# This backend to pw_assert's PW_CHECK()/PW_DCHECK() macros prints the assert
+# expression, evaluated expression, file/line number, function, and user message
+# with printf, then aborts. It is intended for use with host builds.
+pw_source_set("print_and_abort_check_backend") {
+ public_configs = [ ":print_and_abort_check_backend_overrides" ]
+ public_deps = [ ":print_and_abort" ]
public = [
- "print_and_abort_public_overrides/pw_assert_backend/assert_lite_backend.h",
+ "print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h",
+ ]
+}
+
+group("print_and_abort_check_backend.impl") {
+}
+
+# This backend to pw_assert's PW_ASSERT()/PW_DASSERT() macros prints the assert
+# expression, file/line number, and function with printf, then aborts. It is
+# intended for use with host builds.
+pw_source_set("print_and_abort_assert_backend") {
+ public_configs = [ ":print_and_abort_assert_backend_overrides" ]
+ public_deps = [
+ ":config",
+ ":print_and_abort",
]
- sources = [ "public/pw_assert/internal/print_and_abort.h" ]
+ public = [ "print_and_abort_assert_public_overrides/pw_assert_backend/assert_backend.h" ]
}
-group("print_and_abort.impl") {
+group("print_and_abort_assert_backend.impl") {
}
# pw_assert is low-level and ubiquitous. Because of this, it can often cause
@@ -163,7 +199,10 @@ group("impl") {
pw_test("assert_test") {
configs = [ ":public_include_path" ]
sources = [ "assert_test.cc" ]
- deps = [ ":pw_assert" ]
+ deps = [
+ ":pw_assert",
+ dir_pw_status,
+ ]
}
pw_test_group("tests") {
@@ -178,7 +217,7 @@ pw_test_group("tests") {
# provided. However, since this doesn't depend on the backend it re-includes
# the facade headers.
pw_test("assert_facade_test") {
- configs = [ ":public_include_path" ] # For internal/assert_impl.h
+ configs = [ ":public_include_path" ] # For internal/check_impl.h
sources = [
"assert_facade_test.cc",
"fake_backend.cc",
@@ -186,9 +225,12 @@ pw_test("assert_facade_test") {
"pw_assert_test/fake_backend.h",
]
deps = [
- ":assert",
+ ":pw_assert",
+ dir_pw_span,
dir_pw_status,
+ dir_pw_string,
]
+ negative_compilation_tests = true
# TODO(frolv): Fix this test on the QEMU target.
enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "lm3s6965evb_executable"
diff --git a/pw_assert/CMakeLists.txt b/pw_assert/CMakeLists.txt
index b58bb2700..623e3efea 100644
--- a/pw_assert/CMakeLists.txt
+++ b/pw_assert/CMakeLists.txt
@@ -13,12 +13,136 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_assert/backend.cmake)
pw_add_module_config(pw_assert_CONFIG)
-pw_add_facade(pw_assert
+pw_add_library(pw_assert INTERFACE
+ PUBLIC_DEPS
+ pw_assert.check
+ pw_assert.assert
+ pw_assert.config
+)
+
+pw_add_library(pw_assert.config INTERFACE
+ HEADERS
+ public/pw_assert/config.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
- pw_preprocessor
${pw_assert_CONFIG}
)
-target_include_directories(pw_assert INTERFACE assert_lite_public_overrides)
+
+pw_add_facade(pw_assert.assert INTERFACE
+ BACKEND
+ pw_assert.assert_BACKEND
+ HEADERS
+ public/pw_assert/assert.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert.config
+)
+
+pw_add_facade(pw_assert.check INTERFACE
+ BACKEND
+ pw_assert.check_BACKEND
+ HEADERS
+ public/pw_assert/check.h
+ public/pw_assert/internal/check_impl.h
+ public/pw_assert/short.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert.config
+ pw_preprocessor
+)
+
+# Warning: The assert facade is in a transitional state, and this target is
+# likely to be removed as the pw_assert API is reassessed. (b/235149326)
+pw_add_library(pw_assert.assert_compatibility_backend INTERFACE
+ HEADERS
+ assert_compatibility_public_overrides/pw_assert_backend/assert_backend.h
+ PUBLIC_INCLUDES
+ assert_compatibility_public_overrides
+ PUBLIC_DEPS
+ pw_preprocessor
+)
+
+pw_add_library(pw_assert.libc_assert INTERFACE
+ HEADERS
+ libc_assert_public_overrides/assert.h
+ libc_assert_public_overrides/cassert
+ public/pw_assert/internal/libc_assert.h
+ PUBLIC_INCLUDES
+ public
+ libc_assert_public_overrides
+ PUBLIC_DEPS
+ pw_assert.assert
+ pw_preprocessor
+)
+
+pw_add_library(pw_assert.print_and_abort INTERFACE
+ HEADERS
+ public/pw_assert/internal/print_and_abort.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert.config
+)
+
+# This backend to pw_assert's PW_CHECK()/PW_DCHECK() macros prints the assert
+# expression, evaluated expression, file/line number, function, and user message
+# with printf, then aborts. It is intended for use with host builds.
+pw_add_library(pw_assert.print_and_abort_check_backend INTERFACE
+ HEADERS
+ print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h
+ PUBLIC_INCLUDES
+ print_and_abort_check_public_overrides
+ PUBLIC_DEPS
+ pw_assert.print_and_abort
+)
+
+# This backend to pw_assert's PW_ASSERT()/PW_DASSERT() macros prints the assert
+# expression, file/line number, and function with printf, then aborts. It is
+# intended for use with host builds.
+pw_add_library(pw_assert.print_and_abort_assert_backend INTERFACE
+ HEADERS
+ print_and_abort_assert_public_overrides/pw_assert_backend/assert_backend.h
+ PUBLIC_INCLUDES
+ print_and_abort_assert_public_overrides
+ PUBLIC_DEPS
+ pw_assert.config
+ pw_assert.print_and_abort
+)
+
+pw_add_test(pw_assert.assert_facade_test
+ SOURCES
+ assert_facade_test.cc
+ fake_backend.cc
+ public/pw_assert/internal/check_impl.h
+ pw_assert_test/fake_backend.h
+ PRIVATE_DEPS
+ pw_assert
+ pw_compilation_testing._pigweed_only_negative_compilation
+ pw_status
+ pw_string
+ GROUPS
+ modules
+ pw_assert
+)
+
+if((NOT "${pw_assert.assert_BACKEND}" STREQUAL "") AND
+ (NOT "${pw_assert.check_BACKEND}" STREQUAL ""))
+ pw_add_test(pw_assert.assert_backend_compile_test
+ SOURCES
+ assert_backend_compile_test.cc
+ assert_backend_compile_test_c.c
+ PRIVATE_DEPS
+ pw_assert
+ pw_status
+ GROUPS
+ modules
+ pw_assert
+ )
+endif()
diff --git a/pw_assert/assert_backend_compile_test.cc b/pw_assert/assert_backend_compile_test.cc
index 96f17e432..888499984 100644
--- a/pw_assert/assert_backend_compile_test.cc
+++ b/pw_assert/assert_backend_compile_test.cc
@@ -33,7 +33,8 @@
// the test has failed. Obviously manually verifying these is a pain
// and so this is not a suitable test for production.
//
-// TODO(pwbug/88): Add verification of the actually recorded asserts statements.
+// TODO(b/235289499): Add verification of the actually recorded asserts
+// statements.
#include "gtest/gtest.h"
#include "pw_assert/short.h"
diff --git a/pw_assert/assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h b/pw_assert/assert_compatibility_public_overrides/pw_assert_backend/assert_backend.h
index 367c39dd6..367c39dd6 100644
--- a/pw_assert/assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h
+++ b/pw_assert/assert_compatibility_public_overrides/pw_assert_backend/assert_backend.h
diff --git a/pw_assert/assert_facade_test.cc b/pw_assert/assert_facade_test.cc
index a2a6fdd50..3b91756ee 100644
--- a/pw_assert/assert_facade_test.cc
+++ b/pw_assert/assert_facade_test.cc
@@ -26,6 +26,7 @@
// clang-format on
#include "gtest/gtest.h"
+#include "pw_compilation_testing/negative_compilation.h"
#include "pw_status/status.h"
namespace {
@@ -35,48 +36,50 @@ namespace {
EXPECT_STREQ(pw_captured_assert.message, expected_assert_message); \
} while (0)
-class AssertFail : public ::testing::Test {
+class AssertFailTest : public ::testing::Test {
protected:
void SetUp() override { pw_captured_assert.triggered = 0; }
void TearDown() override { EXPECT_EQ(pw_captured_assert.triggered, 1); }
};
-class AssertPass : public ::testing::Test {
+class AssertPassTest : public ::testing::Test {
protected:
void SetUp() override { pw_captured_assert.triggered = 0; }
void TearDown() override { EXPECT_EQ(pw_captured_assert.triggered, 0); }
};
// PW_CRASH(...)
-TEST_F(AssertFail, CrashMessageNoArguments) {
+TEST_F(AssertFailTest, CrashMessageNoArguments) {
PW_CRASH("Goodbye");
EXPECT_MESSAGE("Goodbye");
}
-TEST_F(AssertFail, CrashMessageWithArguments) {
+TEST_F(AssertFailTest, CrashMessageWithArguments) {
PW_CRASH("Goodbye cruel %s", "world");
EXPECT_MESSAGE("Goodbye cruel world");
}
// PW_CHECK(...) - No message
-TEST_F(AssertPass, CheckNoMessage) { PW_CHECK(true); }
-TEST_F(AssertFail, CheckNoMessage) {
+TEST_F(AssertPassTest, CheckNoMessage) { PW_CHECK(true); }
+TEST_F(AssertFailTest, CheckNoMessage) {
PW_CHECK(false);
EXPECT_MESSAGE("Check failed: false. ");
}
-TEST_F(AssertPass, CheckNoMessageComplexExpression) { PW_CHECK(2 == 2); }
-TEST_F(AssertFail, CheckNoMessageComplexExpression) {
+TEST_F(AssertPassTest, CheckNoMessageComplexExpression) { PW_CHECK(2 == 2); }
+TEST_F(AssertFailTest, CheckNoMessageComplexExpression) {
PW_CHECK(1 == 2);
EXPECT_MESSAGE("Check failed: 1 == 2. ");
}
// PW_CHECK(..., msg) - With message; with and without arguments.
-TEST_F(AssertPass, CheckMessageNoArguments) { PW_CHECK(true, "Hello"); }
-TEST_F(AssertFail, CheckMessageNoArguments) {
+TEST_F(AssertPassTest, CheckMessageNoArguments) { PW_CHECK(true, "Hello"); }
+TEST_F(AssertFailTest, CheckMessageNoArguments) {
PW_CHECK(false, "Hello");
EXPECT_MESSAGE("Check failed: false. Hello");
}
-TEST_F(AssertPass, CheckMessageWithArguments) { PW_CHECK(true, "Hello %d", 5); }
-TEST_F(AssertFail, CheckMessageWithArguments) {
+TEST_F(AssertPassTest, CheckMessageWithArguments) {
+ PW_CHECK(true, "Hello %d", 5);
+}
+TEST_F(AssertFailTest, CheckMessageWithArguments) {
PW_CHECK(false, "Hello %d", 5);
EXPECT_MESSAGE("Check failed: false. Hello 5");
}
@@ -86,15 +89,15 @@ TEST_F(AssertFail, CheckMessageWithArguments) {
// Test message formatting separate from the triggering.
// Only test formatting for the type once.
-TEST_F(AssertFail, IntLessThanNoMessageNoArguments) {
+TEST_F(AssertFailTest, IntLessThanNoMessageNoArguments) {
PW_CHECK_INT_LT(5, -2);
EXPECT_MESSAGE("Check failed: 5 (=5) < -2 (=-2). ");
}
-TEST_F(AssertFail, IntLessThanMessageNoArguments) {
+TEST_F(AssertFailTest, IntLessThanMessageNoArguments) {
PW_CHECK_INT_LT(5, -2, "msg");
EXPECT_MESSAGE("Check failed: 5 (=5) < -2 (=-2). msg");
}
-TEST_F(AssertFail, IntLessThanMessageArguments) {
+TEST_F(AssertFailTest, IntLessThanMessageArguments) {
PW_CHECK_INT_LT(5, -2, "msg: %d", 6);
EXPECT_MESSAGE("Check failed: 5 (=5) < -2 (=-2). msg: 6");
}
@@ -102,55 +105,55 @@ TEST_F(AssertFail, IntLessThanMessageArguments) {
// Test comparison boundaries.
// INT <
-TEST_F(AssertPass, IntLt1) { PW_CHECK_INT_LT(-1, 2); }
-TEST_F(AssertPass, IntLt2) { PW_CHECK_INT_LT(1, 2); }
-TEST_F(AssertFail, IntLt3) { PW_CHECK_INT_LT(-1, -2); }
-TEST_F(AssertFail, IntLt4) { PW_CHECK_INT_LT(1, 1); }
+TEST_F(AssertPassTest, IntLt1) { PW_CHECK_INT_LT(-1, 2); }
+TEST_F(AssertPassTest, IntLt2) { PW_CHECK_INT_LT(1, 2); }
+TEST_F(AssertFailTest, IntLt3) { PW_CHECK_INT_LT(-1, -2); }
+TEST_F(AssertFailTest, IntLt4) { PW_CHECK_INT_LT(1, 1); }
// INT <=
-TEST_F(AssertPass, IntLe1) { PW_CHECK_INT_LE(-1, 2); }
-TEST_F(AssertPass, IntLe2) { PW_CHECK_INT_LE(1, 2); }
-TEST_F(AssertFail, IntLe3) { PW_CHECK_INT_LE(-1, -2); }
-TEST_F(AssertPass, IntLe4) { PW_CHECK_INT_LE(1, 1); }
+TEST_F(AssertPassTest, IntLe1) { PW_CHECK_INT_LE(-1, 2); }
+TEST_F(AssertPassTest, IntLe2) { PW_CHECK_INT_LE(1, 2); }
+TEST_F(AssertFailTest, IntLe3) { PW_CHECK_INT_LE(-1, -2); }
+TEST_F(AssertPassTest, IntLe4) { PW_CHECK_INT_LE(1, 1); }
// INT ==
-TEST_F(AssertFail, IntEq1) { PW_CHECK_INT_EQ(-1, 2); }
-TEST_F(AssertFail, IntEq2) { PW_CHECK_INT_EQ(1, 2); }
-TEST_F(AssertFail, IntEq3) { PW_CHECK_INT_EQ(-1, -2); }
-TEST_F(AssertPass, IntEq4) { PW_CHECK_INT_EQ(1, 1); }
+TEST_F(AssertFailTest, IntEq1) { PW_CHECK_INT_EQ(-1, 2); }
+TEST_F(AssertFailTest, IntEq2) { PW_CHECK_INT_EQ(1, 2); }
+TEST_F(AssertFailTest, IntEq3) { PW_CHECK_INT_EQ(-1, -2); }
+TEST_F(AssertPassTest, IntEq4) { PW_CHECK_INT_EQ(1, 1); }
// INT !=
-TEST_F(AssertPass, IntNe1) { PW_CHECK_INT_NE(-1, 2); }
-TEST_F(AssertPass, IntNe2) { PW_CHECK_INT_NE(1, 2); }
-TEST_F(AssertPass, IntNe3) { PW_CHECK_INT_NE(-1, -2); }
-TEST_F(AssertFail, IntNe4) { PW_CHECK_INT_NE(1, 1); }
+TEST_F(AssertPassTest, IntNe1) { PW_CHECK_INT_NE(-1, 2); }
+TEST_F(AssertPassTest, IntNe2) { PW_CHECK_INT_NE(1, 2); }
+TEST_F(AssertPassTest, IntNe3) { PW_CHECK_INT_NE(-1, -2); }
+TEST_F(AssertFailTest, IntNe4) { PW_CHECK_INT_NE(1, 1); }
// INT >
-TEST_F(AssertFail, IntGt1) { PW_CHECK_INT_GT(-1, 2); }
-TEST_F(AssertFail, IntGt2) { PW_CHECK_INT_GT(1, 2); }
-TEST_F(AssertPass, IntGt3) { PW_CHECK_INT_GT(-1, -2); }
-TEST_F(AssertFail, IntGt4) { PW_CHECK_INT_GT(1, 1); }
+TEST_F(AssertFailTest, IntGt1) { PW_CHECK_INT_GT(-1, 2); }
+TEST_F(AssertFailTest, IntGt2) { PW_CHECK_INT_GT(1, 2); }
+TEST_F(AssertPassTest, IntGt3) { PW_CHECK_INT_GT(-1, -2); }
+TEST_F(AssertFailTest, IntGt4) { PW_CHECK_INT_GT(1, 1); }
// INT >=
-TEST_F(AssertFail, IntGe1) { PW_CHECK_INT_GE(-1, 2); }
-TEST_F(AssertFail, IntGe2) { PW_CHECK_INT_GE(1, 2); }
-TEST_F(AssertPass, IntGe3) { PW_CHECK_INT_GE(-1, -2); }
-TEST_F(AssertPass, IntGe4) { PW_CHECK_INT_GE(1, 1); }
+TEST_F(AssertFailTest, IntGe1) { PW_CHECK_INT_GE(-1, 2); }
+TEST_F(AssertFailTest, IntGe2) { PW_CHECK_INT_GE(1, 2); }
+TEST_F(AssertPassTest, IntGe3) { PW_CHECK_INT_GE(-1, -2); }
+TEST_F(AssertPassTest, IntGe4) { PW_CHECK_INT_GE(1, 1); }
// PW_CHECK_UINT_*(...)
// Binary checks with uints, comparisons: <, <=, =, !=, >, >=.
// Test message formatting separate from the triggering.
// Only test formatting for the type once.
-TEST_F(AssertFail, UintLessThanNoMessageNoArguments) {
+TEST_F(AssertFailTest, UintLessThanNoMessageNoArguments) {
PW_CHECK_UINT_LT(5, 2);
EXPECT_MESSAGE("Check failed: 5 (=5) < 2 (=2). ");
}
-TEST_F(AssertFail, UintLessThanMessageNoArguments) {
+TEST_F(AssertFailTest, UintLessThanMessageNoArguments) {
PW_CHECK_UINT_LT(5, 2, "msg");
EXPECT_MESSAGE("Check failed: 5 (=5) < 2 (=2). msg");
}
-TEST_F(AssertFail, UintLessThanMessageArguments) {
+TEST_F(AssertFailTest, UintLessThanMessageArguments) {
PW_CHECK_UINT_LT(5, 2, "msg: %d", 6);
EXPECT_MESSAGE("Check failed: 5 (=5) < 2 (=2). msg: 6");
}
@@ -158,34 +161,34 @@ TEST_F(AssertFail, UintLessThanMessageArguments) {
// Test comparison boundaries.
// UINT <
-TEST_F(AssertPass, UintLt1) { PW_CHECK_UINT_LT(1, 2); }
-TEST_F(AssertFail, UintLt2) { PW_CHECK_UINT_LT(2, 2); }
-TEST_F(AssertFail, UintLt3) { PW_CHECK_UINT_LT(2, 1); }
+TEST_F(AssertPassTest, UintLt1) { PW_CHECK_UINT_LT(1, 2); }
+TEST_F(AssertFailTest, UintLt2) { PW_CHECK_UINT_LT(2, 2); }
+TEST_F(AssertFailTest, UintLt3) { PW_CHECK_UINT_LT(2, 1); }
// UINT <=
-TEST_F(AssertPass, UintLe1) { PW_CHECK_UINT_LE(1, 2); }
-TEST_F(AssertPass, UintLe2) { PW_CHECK_UINT_LE(2, 2); }
-TEST_F(AssertFail, UintLe3) { PW_CHECK_UINT_LE(2, 1); }
+TEST_F(AssertPassTest, UintLe1) { PW_CHECK_UINT_LE(1, 2); }
+TEST_F(AssertPassTest, UintLe2) { PW_CHECK_UINT_LE(2, 2); }
+TEST_F(AssertFailTest, UintLe3) { PW_CHECK_UINT_LE(2, 1); }
// UINT ==
-TEST_F(AssertFail, UintEq1) { PW_CHECK_UINT_EQ(1, 2); }
-TEST_F(AssertPass, UintEq2) { PW_CHECK_UINT_EQ(2, 2); }
-TEST_F(AssertFail, UintEq3) { PW_CHECK_UINT_EQ(2, 1); }
+TEST_F(AssertFailTest, UintEq1) { PW_CHECK_UINT_EQ(1, 2); }
+TEST_F(AssertPassTest, UintEq2) { PW_CHECK_UINT_EQ(2, 2); }
+TEST_F(AssertFailTest, UintEq3) { PW_CHECK_UINT_EQ(2, 1); }
// UINT !=
-TEST_F(AssertPass, UintNe1) { PW_CHECK_UINT_NE(1, 2); }
-TEST_F(AssertFail, UintNe2) { PW_CHECK_UINT_NE(2, 2); }
-TEST_F(AssertPass, UintNe3) { PW_CHECK_UINT_NE(2, 1); }
+TEST_F(AssertPassTest, UintNe1) { PW_CHECK_UINT_NE(1, 2); }
+TEST_F(AssertFailTest, UintNe2) { PW_CHECK_UINT_NE(2, 2); }
+TEST_F(AssertPassTest, UintNe3) { PW_CHECK_UINT_NE(2, 1); }
// UINT >
-TEST_F(AssertFail, UintGt1) { PW_CHECK_UINT_GT(1, 2); }
-TEST_F(AssertFail, UintGt2) { PW_CHECK_UINT_GT(2, 2); }
-TEST_F(AssertPass, UintGt3) { PW_CHECK_UINT_GT(2, 1); }
+TEST_F(AssertFailTest, UintGt1) { PW_CHECK_UINT_GT(1, 2); }
+TEST_F(AssertFailTest, UintGt2) { PW_CHECK_UINT_GT(2, 2); }
+TEST_F(AssertPassTest, UintGt3) { PW_CHECK_UINT_GT(2, 1); }
// UINT >=
-TEST_F(AssertFail, UintGe1) { PW_CHECK_UINT_GE(1, 2); }
-TEST_F(AssertPass, UintGe2) { PW_CHECK_UINT_GE(2, 2); }
-TEST_F(AssertPass, UintGe3) { PW_CHECK_UINT_GE(2, 1); }
+TEST_F(AssertFailTest, UintGe1) { PW_CHECK_UINT_GE(1, 2); }
+TEST_F(AssertPassTest, UintGe2) { PW_CHECK_UINT_GE(2, 2); }
+TEST_F(AssertPassTest, UintGe3) { PW_CHECK_UINT_GE(2, 1); }
// PW_CHECK_PTR_*(...)
// Binary checks with uints, comparisons: <, <=, =, !=, >, >=.
@@ -194,76 +197,76 @@ TEST_F(AssertPass, UintGe3) { PW_CHECK_UINT_GE(2, 1); }
// Test comparison boundaries.
// PTR <
-TEST_F(AssertPass, PtrLt1) {
+TEST_F(AssertPassTest, PtrLt1) {
PW_CHECK_PTR_LT(reinterpret_cast<void*>(0xa), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertFail, PtrLt2) {
+TEST_F(AssertFailTest, PtrLt2) {
PW_CHECK_PTR_LT(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertFail, PtrLt3) {
+TEST_F(AssertFailTest, PtrLt3) {
PW_CHECK_PTR_LT(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xa));
}
// PTR <=
-TEST_F(AssertPass, PtrLe1) {
+TEST_F(AssertPassTest, PtrLe1) {
PW_CHECK_PTR_LE(reinterpret_cast<void*>(0xa), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertPass, PtrLe2) {
+TEST_F(AssertPassTest, PtrLe2) {
PW_CHECK_PTR_LE(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertFail, PtrLe3) {
+TEST_F(AssertFailTest, PtrLe3) {
PW_CHECK_PTR_LE(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xa));
}
// PTR ==
-TEST_F(AssertFail, PtrEq1) {
+TEST_F(AssertFailTest, PtrEq1) {
PW_CHECK_PTR_EQ(reinterpret_cast<void*>(0xa), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertPass, PtrEq2) {
+TEST_F(AssertPassTest, PtrEq2) {
PW_CHECK_PTR_EQ(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertFail, PtrEq3) {
+TEST_F(AssertFailTest, PtrEq3) {
PW_CHECK_PTR_EQ(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xa));
}
// PTR !=
-TEST_F(AssertPass, PtrNe1) {
+TEST_F(AssertPassTest, PtrNe1) {
PW_CHECK_PTR_NE(reinterpret_cast<void*>(0xa), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertFail, PtrNe2) {
+TEST_F(AssertFailTest, PtrNe2) {
PW_CHECK_PTR_NE(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertPass, PtrNe3) {
+TEST_F(AssertPassTest, PtrNe3) {
PW_CHECK_PTR_NE(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xa));
}
// PTR >
-TEST_F(AssertFail, PtrGt1) {
+TEST_F(AssertFailTest, PtrGt1) {
PW_CHECK_PTR_GT(reinterpret_cast<void*>(0xa), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertFail, PtrGt2) {
+TEST_F(AssertFailTest, PtrGt2) {
PW_CHECK_PTR_GT(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertPass, PtrGt3) {
+TEST_F(AssertPassTest, PtrGt3) {
PW_CHECK_PTR_GT(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xa));
}
// PTR >=
-TEST_F(AssertFail, PtrGe1) {
+TEST_F(AssertFailTest, PtrGe1) {
PW_CHECK_PTR_GE(reinterpret_cast<void*>(0xa), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertPass, PtrGe2) {
+TEST_F(AssertPassTest, PtrGe2) {
PW_CHECK_PTR_GE(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xb));
}
-TEST_F(AssertPass, PtrGe3) {
+TEST_F(AssertPassTest, PtrGe3) {
PW_CHECK_PTR_GE(reinterpret_cast<void*>(0xb), reinterpret_cast<void*>(0xa));
}
// NOTNULL
-TEST_F(AssertPass, PtrNotNull) {
+TEST_F(AssertPassTest, PtrNotNull) {
PW_CHECK_NOTNULL(reinterpret_cast<void*>(0xa));
}
-TEST_F(AssertFail, PtrNotNull) {
+TEST_F(AssertFailTest, PtrNotNull) {
PW_CHECK_NOTNULL(reinterpret_cast<void*>(0x0));
}
@@ -271,20 +274,28 @@ TEST_F(AssertFail, PtrNotNull) {
[[maybe_unused]] bool Function2(int) { return false; }
// NOTNULL for function poionters
-TEST_F(AssertPass, FunctionPtrNotNull) {
+TEST_F(AssertPassTest, FunctionPtrNotNull) {
PW_CHECK_NOTNULL(&Function1);
PW_CHECK_NOTNULL(&Function2);
}
-TEST_F(AssertFail, FunctionPtrNotNull) {
+TEST_F(AssertFailTest, FunctionPtrNotNull) {
void (*const function)() = nullptr;
PW_CHECK_NOTNULL(function);
}
+[[maybe_unused]] void CompareIntWithString() {
+#if PW_NC_TEST(CompareIntWithString)
+ PW_NC_EXPECT("cannot initialize|invalid conversion");
+
+ PW_CHECK_INT_EQ(123l, "This check message is accidentally compared to 123!");
+#endif // PW_NC_TEST
+}
+
// Note: Due to platform inconsistencies, the below test for the NOTNULL
// message doesn't work. Some platforms print NULL formatted as %p as "(nil)",
// others "0x0". Leaving this here for reference.
//
-// TEST_F(AssertFail, PtrNotNullDescription) {
+// TEST_F(AssertFailTest, PtrNotNullDescription) {
// intptr_t intptr = 0;
// PW_CHECK_NOTNULL(intptr);
// EXPECT_MESSAGE("Check failed: intptr (=0x0) != nullptr (=0x0). ");
@@ -296,46 +307,46 @@ TEST_F(AssertFail, FunctionPtrNotNull) {
// Test message formatting separate from the triggering.
// Only test formatting for the type once.
-TEST_F(AssertFail, FloatLessThanNoMessageNoArguments) {
+TEST_F(AssertFailTest, FloatLessThanNoMessageNoArguments) {
PW_CHECK_FLOAT_EXACT_LT(5.2, 2.3);
EXPECT_MESSAGE("Check failed: 5.2 (=5.200000) < 2.3 (=2.300000). ");
}
-TEST_F(AssertFail, FloatLessThanMessageNoArguments) {
+TEST_F(AssertFailTest, FloatLessThanMessageNoArguments) {
PW_CHECK_FLOAT_EXACT_LT(5.2, 2.3, "msg");
EXPECT_MESSAGE("Check failed: 5.2 (=5.200000) < 2.3 (=2.300000). msg");
}
-TEST_F(AssertFail, FloatLessThanMessageArguments) {
+TEST_F(AssertFailTest, FloatLessThanMessageArguments) {
PW_CHECK_FLOAT_EXACT_LT(5.2, 2.3, "msg: %d", 6);
EXPECT_MESSAGE("Check failed: 5.2 (=5.200000) < 2.3 (=2.300000). msg: 6");
}
// Check float NEAR both above and below the permitted range.
-TEST_F(AssertFail, FloatNearAboveNoMessageNoArguments) {
+TEST_F(AssertFailTest, FloatNearAboveNoMessageNoArguments) {
PW_CHECK_FLOAT_NEAR(5.2, 2.3, 0.1);
EXPECT_MESSAGE(
"Check failed: 5.2 (=5.200000) <= 2.3 + abs_tolerance (=2.400000). ");
}
-TEST_F(AssertFail, FloatNearAboveMessageNoArguments) {
+TEST_F(AssertFailTest, FloatNearAboveMessageNoArguments) {
PW_CHECK_FLOAT_NEAR(5.2, 2.3, 0.1, "msg");
EXPECT_MESSAGE(
"Check failed: 5.2 (=5.200000) <= 2.3 + abs_tolerance (=2.400000). msg");
}
-TEST_F(AssertFail, FloatNearAboveMessageArguments) {
+TEST_F(AssertFailTest, FloatNearAboveMessageArguments) {
PW_CHECK_FLOAT_NEAR(5.2, 2.3, 0.1, "msg: %d", 6);
EXPECT_MESSAGE(
"Check failed: 5.2 (=5.200000) <= 2.3 + abs_tolerance (=2.400000). msg: "
"6");
}
-TEST_F(AssertFail, FloatNearBelowNoMessageNoArguments) {
+TEST_F(AssertFailTest, FloatNearBelowNoMessageNoArguments) {
PW_CHECK_FLOAT_NEAR(1.2, 2.3, 0.1);
EXPECT_MESSAGE(
"Check failed: 1.2 (=1.200000) >= 2.3 - abs_tolerance (=2.200000). ");
}
-TEST_F(AssertFail, FloatNearBelowMessageNoArguments) {
+TEST_F(AssertFailTest, FloatNearBelowMessageNoArguments) {
PW_CHECK_FLOAT_NEAR(1.2, 2.3, 0.1, "msg");
EXPECT_MESSAGE(
"Check failed: 1.2 (=1.200000) >= 2.3 - abs_tolerance (=2.200000). msg");
}
-TEST_F(AssertFail, FloatNearBelowMessageArguments) {
+TEST_F(AssertFailTest, FloatNearBelowMessageArguments) {
PW_CHECK_FLOAT_NEAR(1.2, 2.3, 0.1, "msg: %d", 6);
EXPECT_MESSAGE(
"Check failed: 1.2 (=1.200000) >= 2.3 - abs_tolerance (=2.200000). msg: "
@@ -346,53 +357,53 @@ TEST_F(AssertFail, FloatNearBelowMessageArguments) {
// integer conversions in the asserts.
// FLOAT <
-TEST_F(AssertPass, FloatLt1) { PW_CHECK_FLOAT_EXACT_LT(1.1, 1.2); }
-TEST_F(AssertFail, FloatLt2) { PW_CHECK_FLOAT_EXACT_LT(1.2, 1.2); }
-TEST_F(AssertFail, FloatLt3) { PW_CHECK_FLOAT_EXACT_LT(1.2, 1.1); }
+TEST_F(AssertPassTest, FloatLt1) { PW_CHECK_FLOAT_EXACT_LT(1.1, 1.2); }
+TEST_F(AssertFailTest, FloatLt2) { PW_CHECK_FLOAT_EXACT_LT(1.2, 1.2); }
+TEST_F(AssertFailTest, FloatLt3) { PW_CHECK_FLOAT_EXACT_LT(1.2, 1.1); }
// FLOAT <=
-TEST_F(AssertPass, FloatLe1) { PW_CHECK_FLOAT_EXACT_LE(1.1, 1.2); }
-TEST_F(AssertPass, FloatLe2) { PW_CHECK_FLOAT_EXACT_LE(1.2, 1.2); }
-TEST_F(AssertFail, FloatLe3) { PW_CHECK_FLOAT_EXACT_LE(1.2, 1.1); }
+TEST_F(AssertPassTest, FloatLe1) { PW_CHECK_FLOAT_EXACT_LE(1.1, 1.2); }
+TEST_F(AssertPassTest, FloatLe2) { PW_CHECK_FLOAT_EXACT_LE(1.2, 1.2); }
+TEST_F(AssertFailTest, FloatLe3) { PW_CHECK_FLOAT_EXACT_LE(1.2, 1.1); }
// FLOAT ~= based on absolute error.
-TEST_F(AssertFail, FloatNearAbs1) { PW_CHECK_FLOAT_NEAR(1.09, 1.2, 0.1); }
-TEST_F(AssertPass, FloatNearAbs2) { PW_CHECK_FLOAT_NEAR(1.1, 1.2, 0.1); }
-TEST_F(AssertPass, FloatNearAbs3) { PW_CHECK_FLOAT_NEAR(1.2, 1.2, 0.1); }
-TEST_F(AssertPass, FloatNearAbs4) { PW_CHECK_FLOAT_NEAR(1.2, 1.1, 0.1); }
-TEST_F(AssertFail, FloatNearAbs5) { PW_CHECK_FLOAT_NEAR(1.21, 1.1, 0.1); }
+TEST_F(AssertFailTest, FloatNearAbs1) { PW_CHECK_FLOAT_NEAR(1.09, 1.2, 0.1); }
+TEST_F(AssertPassTest, FloatNearAbs2) { PW_CHECK_FLOAT_NEAR(1.1, 1.2, 0.1); }
+TEST_F(AssertPassTest, FloatNearAbs3) { PW_CHECK_FLOAT_NEAR(1.2, 1.2, 0.1); }
+TEST_F(AssertPassTest, FloatNearAbs4) { PW_CHECK_FLOAT_NEAR(1.2, 1.1, 0.1); }
+TEST_F(AssertFailTest, FloatNearAbs5) { PW_CHECK_FLOAT_NEAR(1.21, 1.1, 0.1); }
// Make sure the abs_tolerance is asserted to be >= 0.
-TEST_F(AssertFail, FloatNearAbs6) { PW_CHECK_FLOAT_NEAR(1.2, 1.2, -0.1); }
-TEST_F(AssertPass, FloatNearAbs7) { PW_CHECK_FLOAT_NEAR(1.2, 1.2, 0.0); }
+TEST_F(AssertFailTest, FloatNearAbs6) { PW_CHECK_FLOAT_NEAR(1.2, 1.2, -0.1); }
+TEST_F(AssertPassTest, FloatNearAbs7) { PW_CHECK_FLOAT_NEAR(1.2, 1.2, 0.0); }
// FLOAT ==
-TEST_F(AssertFail, FloatEq1) { PW_CHECK_FLOAT_EXACT_EQ(1.1, 1.2); }
-TEST_F(AssertPass, FloatEq2) { PW_CHECK_FLOAT_EXACT_EQ(1.2, 1.2); }
-TEST_F(AssertFail, FloatEq3) { PW_CHECK_FLOAT_EXACT_EQ(1.2, 1.1); }
+TEST_F(AssertFailTest, FloatEq1) { PW_CHECK_FLOAT_EXACT_EQ(1.1, 1.2); }
+TEST_F(AssertPassTest, FloatEq2) { PW_CHECK_FLOAT_EXACT_EQ(1.2, 1.2); }
+TEST_F(AssertFailTest, FloatEq3) { PW_CHECK_FLOAT_EXACT_EQ(1.2, 1.1); }
// FLOAT !=
-TEST_F(AssertPass, FloatNe1) { PW_CHECK_FLOAT_EXACT_NE(1.1, 1.2); }
-TEST_F(AssertFail, FloatNe2) { PW_CHECK_FLOAT_EXACT_NE(1.2, 1.2); }
-TEST_F(AssertPass, FloatNe3) { PW_CHECK_FLOAT_EXACT_NE(1.2, 1.1); }
+TEST_F(AssertPassTest, FloatNe1) { PW_CHECK_FLOAT_EXACT_NE(1.1, 1.2); }
+TEST_F(AssertFailTest, FloatNe2) { PW_CHECK_FLOAT_EXACT_NE(1.2, 1.2); }
+TEST_F(AssertPassTest, FloatNe3) { PW_CHECK_FLOAT_EXACT_NE(1.2, 1.1); }
// FLOAT >
-TEST_F(AssertFail, FloatGt1) { PW_CHECK_FLOAT_EXACT_GT(1.1, 1.2); }
-TEST_F(AssertFail, FloatGt2) { PW_CHECK_FLOAT_EXACT_GT(1.2, 1.2); }
-TEST_F(AssertPass, FloatGt3) { PW_CHECK_FLOAT_EXACT_GT(1.2, 1.1); }
+TEST_F(AssertFailTest, FloatGt1) { PW_CHECK_FLOAT_EXACT_GT(1.1, 1.2); }
+TEST_F(AssertFailTest, FloatGt2) { PW_CHECK_FLOAT_EXACT_GT(1.2, 1.2); }
+TEST_F(AssertPassTest, FloatGt3) { PW_CHECK_FLOAT_EXACT_GT(1.2, 1.1); }
// FLOAT >=
-TEST_F(AssertFail, FloatGe1) { PW_CHECK_FLOAT_EXACT_GE(1.1, 1.2); }
-TEST_F(AssertPass, FloatGe2) { PW_CHECK_FLOAT_EXACT_GE(1.2, 1.2); }
-TEST_F(AssertPass, FloatGe3) { PW_CHECK_FLOAT_EXACT_GE(1.2, 1.1); }
+TEST_F(AssertFailTest, FloatGe1) { PW_CHECK_FLOAT_EXACT_GE(1.1, 1.2); }
+TEST_F(AssertPassTest, FloatGe2) { PW_CHECK_FLOAT_EXACT_GE(1.2, 1.2); }
+TEST_F(AssertPassTest, FloatGe3) { PW_CHECK_FLOAT_EXACT_GE(1.2, 1.1); }
// Nested comma handling.
static int Add3(int a, int b, int c) { return a + b + c; }
-TEST_F(AssertFail, CommaHandlingLeftSide) {
+TEST_F(AssertFailTest, CommaHandlingLeftSide) {
PW_CHECK_INT_EQ(Add3(1, 2, 3), 4);
EXPECT_MESSAGE("Check failed: Add3(1, 2, 3) (=6) == 4 (=4). ");
}
-TEST_F(AssertFail, CommaHandlingRightSide) {
+TEST_F(AssertFailTest, CommaHandlingRightSide) {
PW_CHECK_INT_EQ(4, Add3(1, 2, 3));
EXPECT_MESSAGE("Check failed: 4 (=4) == Add3(1, 2, 3) (=6). ");
}
@@ -501,19 +512,19 @@ TEST(AssertFail, DCheckOkSingleSideEffectingCall) {
}
// Verify PW_CHECK_OK, including message handling.
-TEST_F(AssertFail, StatusNotOK) {
+TEST_F(AssertFailTest, StatusNotOK) {
pw::Status status = pw::Status::Unknown();
PW_CHECK_OK(status);
EXPECT_MESSAGE("Check failed: status (=UNKNOWN) == OkStatus() (=OK). ");
}
-TEST_F(AssertFail, StatusNotOKMessageNoArguments) {
+TEST_F(AssertFailTest, StatusNotOKMessageNoArguments) {
pw::Status status = pw::Status::Unknown();
PW_CHECK_OK(status, "msg");
EXPECT_MESSAGE("Check failed: status (=UNKNOWN) == OkStatus() (=OK). msg");
}
-TEST_F(AssertFail, StatusNotOKMessageArguments) {
+TEST_F(AssertFailTest, StatusNotOKMessageArguments) {
pw::Status status = pw::Status::Unknown();
PW_CHECK_OK(status, "msg: %d", 5);
EXPECT_MESSAGE("Check failed: status (=UNKNOWN) == OkStatus() (=OK). msg: 5");
@@ -522,7 +533,7 @@ TEST_F(AssertFail, StatusNotOKMessageArguments) {
// Example expression for the test below.
pw::Status DoTheThing() { return pw::Status::ResourceExhausted(); }
-TEST_F(AssertFail, NonTrivialExpression) {
+TEST_F(AssertFailTest, NonTrivialExpression) {
PW_CHECK_OK(DoTheThing());
EXPECT_MESSAGE(
"Check failed: DoTheThing() (=RESOURCE_EXHAUSTED) == OkStatus() (=OK). ");
@@ -531,27 +542,29 @@ TEST_F(AssertFail, NonTrivialExpression) {
// Note: This function seems pointless but it is not, since pw::Status::FOO
// constants are not actually status objects, but code objects. This way we can
// ensure the macros work with both real status objects and literals.
-TEST_F(AssertPass, Function) { PW_CHECK_OK(pw::OkStatus()); }
-TEST_F(AssertPass, Enum) { PW_CHECK_OK(PW_STATUS_OK); }
-TEST_F(AssertFail, Function) { PW_CHECK_OK(pw::Status::Unknown()); }
-TEST_F(AssertFail, Enum) { PW_CHECK_OK(PW_STATUS_UNKNOWN); }
+TEST_F(AssertPassTest, Function) { PW_CHECK_OK(pw::OkStatus()); }
+TEST_F(AssertPassTest, Enum) { PW_CHECK_OK(PW_STATUS_OK); }
+TEST_F(AssertFailTest, Function) { PW_CHECK_OK(pw::Status::Unknown()); }
+TEST_F(AssertFailTest, Enum) { PW_CHECK_OK(PW_STATUS_UNKNOWN); }
#if PW_ASSERT_ENABLE_DEBUG
// In debug mode, the asserts should check their arguments.
-TEST_F(AssertPass, DCheckFunction) { PW_DCHECK_OK(pw::OkStatus()); }
-TEST_F(AssertPass, DCheckEnum) { PW_DCHECK_OK(PW_STATUS_OK); }
-TEST_F(AssertFail, DCheckFunction) { PW_DCHECK_OK(pw::Status::Unknown()); }
-TEST_F(AssertFail, DCheckEnum) { PW_DCHECK_OK(PW_STATUS_UNKNOWN); }
+TEST_F(AssertPassTest, DCheckFunction) { PW_DCHECK_OK(pw::OkStatus()); }
+TEST_F(AssertPassTest, DCheckEnum) { PW_DCHECK_OK(PW_STATUS_OK); }
+TEST_F(AssertFailTest, DCheckFunction) { PW_DCHECK_OK(pw::Status::Unknown()); }
+TEST_F(AssertFailTest, DCheckEnum) { PW_DCHECK_OK(PW_STATUS_UNKNOWN); }
#else // PW_ASSERT_ENABLE_DEBUG
// In release mode, all the asserts should pass.
-TEST_F(AssertPass, DCheckFunction_Ok) { PW_DCHECK_OK(pw::OkStatus()); }
-TEST_F(AssertPass, DCheckEnum_Ok) { PW_DCHECK_OK(PW_STATUS_OK); }
-TEST_F(AssertPass, DCheckFunction_Err) { PW_DCHECK_OK(pw::Status::Unknown()); }
-TEST_F(AssertPass, DCheckEnum_Err) { PW_DCHECK_OK(PW_STATUS_UNKNOWN); }
+TEST_F(AssertPassTest, DCheckFunction_Ok) { PW_DCHECK_OK(pw::OkStatus()); }
+TEST_F(AssertPassTest, DCheckEnum_Ok) { PW_DCHECK_OK(PW_STATUS_OK); }
+TEST_F(AssertPassTest, DCheckFunction_Err) {
+ PW_DCHECK_OK(pw::Status::Unknown());
+}
+TEST_F(AssertPassTest, DCheckEnum_Err) { PW_DCHECK_OK(PW_STATUS_UNKNOWN); }
#endif // PW_ASSERT_ENABLE_DEBUG
-// TODO: Figure out how to run some of these tests is C.
+// TODO(keir): Figure out how to run some of these tests is C.
} // namespace
diff --git a/pw_assert/assert_test.cc b/pw_assert/assert_test.cc
index ecbb0c1fb..1d7db5ad0 100644
--- a/pw_assert/assert_test.cc
+++ b/pw_assert/assert_test.cc
@@ -15,6 +15,7 @@
#include "pw_assert/assert.h"
#include "gtest/gtest.h"
+#include "pw_status/status.h"
// PW_ASSERT() should always be enabled, and always evaluate the expression.
TEST(Assert, AssertTrue) {
@@ -34,6 +35,15 @@ TEST(Assert, DebugAssertTrue) {
}
}
+TEST(Assert, AssertOkEvaluatesExpressionAndDoesNotCrashOnOk) {
+ int evaluated = 1;
+ PW_ASSERT_OK(([&]() {
+ ++evaluated;
+ return pw::OkStatus();
+ })());
+ EXPECT_EQ(evaluated, 2);
+}
+
// Unfortunately, we don't have the infrastructure to test failure handling
// automatically, since the harness crashes in the process of running this
// test. The unsatisfying alternative is to test the functionality manually,
diff --git a/pw_assert/backend.cmake b/pw_assert/backend.cmake
new file mode 100644
index 000000000..04d3b5c20
--- /dev/null
+++ b/pw_assert/backend.cmake
@@ -0,0 +1,22 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_assert module's CHECK facade.
+pw_add_backend_variable(pw_assert.assert_BACKEND)
+
+# Backend for the pw_assert module's ASSERT facade.
+pw_add_backend_variable(pw_assert.check_BACKEND)
diff --git a/pw_assert/backend.gni b/pw_assert/backend.gni
index c4876cfac..27b57d686 100644
--- a/pw_assert/backend.gni
+++ b/pw_assert/backend.gni
@@ -21,6 +21,6 @@ declare_args() {
# Backend for the pw_assert module's ASSERT facade.
#
# Warning: This naming is transitional. Modifying this build argument WILL
- # result in future breakages. (pwbug/246)
- pw_assert_LITE_BACKEND = "${dir_pw_assert}:lite_compatibility_backend"
+ # result in future breakages. (b/235149326)
+ pw_assert_LITE_BACKEND = "${dir_pw_assert}:assert_compatibility_backend"
}
diff --git a/pw_assert/docs.rst b/pw_assert/docs.rst
index 03c4fdad2..f411e8184 100644
--- a/pw_assert/docs.rst
+++ b/pw_assert/docs.rst
@@ -458,6 +458,13 @@ PW_ASSERT API Reference
Same as ``PW_ASSERT()``, except that if ``PW_ASSERT_ENABLE_DEBUG == 0``, the
assert is disabled and condition is not evaluated.
+.. cpp:function:: PW_ASSERT_OK(expression)
+
+ A header- and constexpr-safe version of ``PW_CHECK_OK()``.
+
+ If the given expression is not `OK`, crash the system. Otherwise, do nothing.
+ The condition is guarenteed to be evaluated.
+
.. attention::
Unlike the ``PW_CHECK_*()`` suite of macros, ``PW_ASSERT()`` and
@@ -522,7 +529,7 @@ This facade module (``pw_assert``) does not provide a backend. See
The backend must provide the header
-``pw_assert_backend/backend.h``
+``pw_assert_backend/check_backend.h``
and that header must define the following macros:
@@ -609,7 +616,7 @@ is providing a macro-based backend API for the ``PW_ASSERT()`` and
are extremely confusingly similar and are NOT interchangeable.
A macro-based backend for the ``PW_ASSERT()`` macros must provide the following
-macro in a header at ``pw_assert_backend/assert_lite_backend.h``.
+macro in a header at ``pw_assert_backend/assert_backend.h``.
.. cpp:function:: PW_ASSERT_HANDLE_FAILURE(expression)
@@ -762,6 +769,15 @@ Compatibility
-------------
The facade is compatible with both C and C++.
+---------------------------------------
+C Standard Library `assert` Replacement
+---------------------------------------
+An optional replacement of the C standard Library's `assert` macro is provided
+through the `libc_assert` target which fully implements replacement `assert.h`
+and `cassert` headers using `PW_ASSERT`. While this is effective for porting
+external code to microcontrollers, we do not advise embedded projects use the
+`assert` macro unless absolutely necessary.
+
----------------
Roadmap & Status
----------------
@@ -780,6 +796,11 @@ Available Assert Backends
the functionality. There are (a) tests for the facade macro processing logic,
using a fake assert backend; and (b) compile tests to verify that the
selected backend compiles with all supported assert constructions and types.
+- ``pw_assert:print_and_abort_backend`` - **Stable** - Uses the ``printf`` and
+ ``abort`` standard library functions to implement the assert facade. Prints
+ the assert expression, evaluated arguments if any, file/line, function name,
+ and user message, then aborts. Only suitable for targets that support these
+ standard library functions. Compatible with C++14.
- ``pw_assert_basic`` - **Stable** - The assert basic module is a simple assert
handler that displays the failed assert line and the values of captured
arguments. Output is directed to ``pw_sys_io``. This module is a great
diff --git a/pw_assert/fake_backend.cc b/pw_assert/fake_backend.cc
index 8ab2279d2..b81c106bc 100644
--- a/pw_assert/fake_backend.cc
+++ b/pw_assert/fake_backend.cc
@@ -15,8 +15,8 @@
#include "pw_assert_test/fake_backend.h"
#include <cstring>
-#include <span>
+#include "pw_span/span.h"
#include "pw_string/string_builder.h"
// Global that's publicly accessible to read captured assert contents.
diff --git a/pw_assert/libc_assert_public_overrides/assert.h b/pw_assert/libc_assert_public_overrides/assert.h
new file mode 100644
index 000000000..3d574a1f4
--- /dev/null
+++ b/pw_assert/libc_assert_public_overrides/assert.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert/internal/libc_assert.h"
diff --git a/pw_assert/libc_assert_public_overrides/cassert b/pw_assert/libc_assert_public_overrides/cassert
new file mode 100644
index 000000000..3d574a1f4
--- /dev/null
+++ b/pw_assert/libc_assert_public_overrides/cassert
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert/internal/libc_assert.h"
diff --git a/pw_assert/print_and_abort_public_overrides/pw_assert_backend/assert_lite_backend.h b/pw_assert/print_and_abort_assert_public_overrides/pw_assert_backend/assert_backend.h
index 02b81df94..02b81df94 100644
--- a/pw_assert/print_and_abort_public_overrides/pw_assert_backend/assert_lite_backend.h
+++ b/pw_assert/print_and_abort_assert_public_overrides/pw_assert_backend/assert_backend.h
diff --git a/pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h b/pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h
new file mode 100644
index 000000000..b62a59585
--- /dev/null
+++ b/pw_assert/print_and_abort_check_public_overrides/pw_assert_backend/check_backend.h
@@ -0,0 +1,52 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <stdio.h>
+
+#include "pw_assert/internal/print_and_abort.h"
+
+// Implement the pw_assert backend by forwarding to the print_and_abort.h
+// macros, which use printf and abort.
+#define PW_HANDLE_CRASH(...) \
+ PW_ASSERT_PRINT_EXPRESSION("CRASH", "PW_CRASH()"); \
+ _PW_ASSERT_PRINT_MESSAGE_AND_ABORT(__VA_ARGS__)
+
+#define PW_HANDLE_ASSERT_FAILURE(condition_string, ...) \
+ PW_ASSERT_PRINT_EXPRESSION("CHECK", condition_string); \
+ _PW_ASSERT_PRINT_MESSAGE_AND_ABORT(__VA_ARGS__)
+
+#define PW_HANDLE_ASSERT_BINARY_COMPARE_FAILURE(arg_a_str, \
+ arg_a_val, \
+ comparison_op_str, \
+ arg_b_str, \
+ arg_b_val, \
+ type_fmt, \
+ ...) \
+ PW_ASSERT_PRINT_EXPRESSION("CHECK", \
+ arg_a_str " " comparison_op_str " " arg_b_str); \
+ fprintf(stderr, \
+ " EVALUATED CONDITION\n\n " arg_a_str " (=" type_fmt \
+ ") " comparison_op_str " " arg_b_str " (=" type_fmt \
+ ")" \
+ ".\n\n", \
+ arg_a_val, \
+ arg_b_val); \
+ _PW_ASSERT_PRINT_MESSAGE_AND_ABORT(__VA_ARGS__)
+
+#define _PW_ASSERT_PRINT_MESSAGE_AND_ABORT(...) \
+ fprintf(stderr, " MESSAGE\n\n " __VA_ARGS__); \
+ fprintf(stderr, "\n\n"); \
+ fflush(stderr); \
+ abort()
diff --git a/pw_assert/public/pw_assert/assert.h b/pw_assert/public/pw_assert/assert.h
index ec4995f80..47ec8b7bb 100644
--- a/pw_assert/public/pw_assert/assert.h
+++ b/pw_assert/public/pw_assert/assert.h
@@ -14,19 +14,19 @@
#pragma once
#include "pw_assert/config.h" // For PW_ASSERT_ENABLE_DEBUG
-#include "pw_assert_backend/assert_lite_backend.h"
+#include "pw_assert_backend/assert_backend.h"
// A header- and constexpr-safe version of PW_CHECK().
//
// If the given condition is false, crash the system. Otherwise, do nothing.
-// The condition is guaranteed to be evaluated. This assert implementation is
-// guaranteed to be constexpr-safe.
-//
-// IMPORTANT: Unlike the PW_CHECK_*() suite of macros, this API captures no
-// rich information like line numbers, the file, expression arguments, or the
-// stringified expression. Use these macros only when absolutely necessary --
-// in headers, constexr contexts, or in rare cases where the call site overhead
-// of a full PW_CHECK must be avoided. Use PW_CHECK_*() whenever possible.
+// The condition is guaranteed to be evaluated.
+//
+// IMPORTANT: Unlike the PW_CHECK_*() suite of macros, not all backends for
+// this API capture rich information like line numbers, the file, expression
+// arguments, or the stringified expression. Use these macros only when
+// absolutely necessary -- in headers, constexpr contexts, or in rare cases
+// where the call site overhead of a full PW_CHECK must be avoided. Use
+// PW_CHECK_*() whenever possible.
#define PW_ASSERT(condition) \
do { \
if (!(condition)) { \
@@ -39,14 +39,43 @@
// Same as PW_ASSERT(), except that if PW_ASSERT_ENABLE_DEBUG == 1, the assert
// is disabled and condition is not evaluated.
//
-// IMPORTANT: Unlike the PW_CHECK_*() suite of macros, this API captures no
-// rich information like line numbers, the file, expression arguments, or the
-// stringified expression. Use these macros only when absolutely necessary --
-// in headers, constexr contexts, or in rare cases where the call site overhead
-// of a full PW_CHECK must be avoided. Use PW_DCHECK_*() whenever possible.
+// IMPORTANT: Unlike the PW_CHECK_*() suite of macros, not all backends for
+// this API capture rich information like line numbers, the file, expression
+// arguments, or the stringified expression. Use these macros only when
+// absolutely necessary -- in headers, constexpr contexts, or in rare cases
+// where the call site overhead of a full PW_CHECK must be avoided. Use
+// PW_CHECK_*() whenever possible.
#define PW_DASSERT(condition) \
do { \
if ((PW_ASSERT_ENABLE_DEBUG == 1) && !(condition)) { \
PW_ASSERT_HANDLE_FAILURE(#condition); \
} \
} while (0)
+
+// A header- and constexpr-safe version of PW_CHECK_OK().
+//
+// If the condition does not evaluate to PW_STATUS_OK, crash.
+// Otherwise, do nothing. The expression is guaranteed to be evaluated.
+//
+// Unlike `PW_CHECK_OK`, this macro does not currently log the failed status
+// kind.
+//
+// IMPORTANT: Unlike the PW_CHECK_*() suite of macros, not all backends for
+// this API capture rich information like line numbers, the file, expression
+// arguments, or the stringified expression. Use these macros only when
+// absolutely necessary -- in headers, constexpr contexts, or in rare cases
+// where the call site overhead of a full PW_CHECK must be avoided. Use
+// PW_CHECK_*() whenever possible.
+#define PW_ASSERT_OK(expression, ...) \
+ do { \
+ const _PW_ASSERT_OK_STATUS _pw_assert_ok_status = (expression); \
+ if (_pw_assert_ok_status != PW_STATUS_OK) { \
+ PW_ASSERT_HANDLE_FAILURE(#expression); \
+ } \
+ } while (0)
+
+#ifdef __cplusplus
+#define _PW_ASSERT_OK_STATUS ::pw::Status
+#else
+#define _PW_ASSERT_OK_STATUS pw_Status
+#endif // __cplusplus
diff --git a/pw_assert/public/pw_assert/check.h b/pw_assert/public/pw_assert/check.h
index 31ffca74d..57b443790 100644
--- a/pw_assert/public/pw_assert/check.h
+++ b/pw_assert/public/pw_assert/check.h
@@ -111,4 +111,4 @@
// Note that for the assert failures, the handler should assume the assert
// has already failed (the facade checks the condition before delegating).
//
-#include "pw_assert_backend/assert_backend.h"
+#include "pw_assert_backend/check_backend.h"
diff --git a/pw_assert/public/pw_assert/config.h b/pw_assert/public/pw_assert/config.h
index 8bcb0ee97..6b39cecfc 100644
--- a/pw_assert/public/pw_assert/config.h
+++ b/pw_assert/public/pw_assert/config.h
@@ -33,3 +33,31 @@
#if !defined(PW_ASSERT_CAPTURE_VALUES)
#define PW_ASSERT_CAPTURE_VALUES 1
#endif // !defined(PW_ASSERT_CAPTURE_VALUES)
+
+// Modes available for how to end an assert failure for pw_assert_basic.
+#define PW_ASSERT_BASIC_ACTION_ABORT 100
+#define PW_ASSERT_BASIC_ACTION_EXIT 101
+#define PW_ASSERT_BASIC_ACTION_LOOP 102
+
+#ifdef PW_ASSERT_BASIC_ABORT
+#error PW_ASSERT_BASIC_ABORT is deprecated! Use PW_ASSERT_BASIC_ACTION instead.
+#endif // PW_ASSERT_BASIC_ABORT
+
+// Set to one of the following to define how pw_basic_assert should act after an
+// assert failure:
+// - PW_ASSERT_BASIC_ACTION_ABORT: Call std::abort()
+// - PW_ASSERT_BASIC_ACTION_EXIT: Call std::_Exit(-1)
+// - PW_ASSERT_BASIC_ACTION_LOOP: Loop forever
+#ifndef PW_ASSERT_BASIC_ACTION
+#define PW_ASSERT_BASIC_ACTION PW_ASSERT_BASIC_ACTION_ABORT
+#endif // PW_ASSERT_BASIC_ACTION
+
+// Whether to show the CRASH ASCII art banner.
+#ifndef PW_ASSERT_BASIC_SHOW_BANNER
+#define PW_ASSERT_BASIC_SHOW_BANNER 1
+#endif // PW_ASSERT_BASIC_SHOW_BANNER
+
+// Whether to use ANSI colors.
+#ifndef PW_ASSERT_BASIC_USE_COLORS
+#define PW_ASSERT_BASIC_USE_COLORS 1
+#endif // PW_ASSERT_BASIC_USE_COLORS
diff --git a/pw_assert/public/pw_assert/internal/check_impl.h b/pw_assert/public/pw_assert/internal/check_impl.h
index bb7c7f349..3755f75dd 100644
--- a/pw_assert/public/pw_assert/internal/check_impl.h
+++ b/pw_assert/public/pw_assert/internal/check_impl.h
@@ -220,7 +220,6 @@ constexpr T ConvertToType(const U& value) {
arg_b_str, \
arg_b_val, \
type_fmt, \
- message, \
...) \
PW_HANDLE_ASSERT_BINARY_COMPARE_FAILURE(arg_a_str, \
arg_a_val, \
@@ -228,19 +227,17 @@ constexpr T ConvertToType(const U& value) {
arg_b_str, \
arg_b_val, \
type_fmt, \
- message, \
__VA_ARGS__)
#else
-#define _PW_CHECK_BINARY_ARG_HANDLER(arg_a_str, \
- arg_a_val, \
- comparison_op_str, \
- arg_b_str, \
- arg_b_val, \
- type_fmt, \
- message, \
- ...) \
- PW_HANDLE_ASSERT_FAILURE( \
- arg_a_str " " comparison_op_str " " arg_b_str, message, __VA_ARGS__)
+#define _PW_CHECK_BINARY_ARG_HANDLER(arg_a_str, \
+ arg_a_val, \
+ comparison_op_str, \
+ arg_b_str, \
+ arg_b_val, \
+ type_fmt, \
+ ...) \
+ PW_HANDLE_ASSERT_FAILURE(arg_a_str " " comparison_op_str " " arg_b_str, \
+ __VA_ARGS__)
#endif // PW_ASSERT_CAPTURE_VALUES
// Custom implementation for FLOAT_NEAR which is implemented through two
diff --git a/pw_assert/public/pw_assert/internal/libc_assert.h b/pw_assert/public/pw_assert/internal/libc_assert.h
new file mode 100644
index 000000000..865650ed1
--- /dev/null
+++ b/pw_assert/public/pw_assert/internal/libc_assert.h
@@ -0,0 +1,42 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// Include these headers as C++ code, in case assert.h is included within an
+// extern "C" block.
+#ifdef __cplusplus
+extern "C++" {
+#endif // __cplusplus
+
+#include "pw_assert/assert.h"
+#include "pw_preprocessor/util.h"
+
+#ifdef __cplusplus
+} // extern "C++"
+#endif // __cplusplus
+
+// Provide static_assert() on >=C11
+#if (defined(__USE_ISOC11) || \
+ defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) && \
+ !defined(__cplusplus)
+#define static_assert _Static_assert
+#endif // C11 or newer
+
+// Provide assert()
+#undef assert
+#if defined(NDEBUG) // Required by ANSI C standard.
+#define assert(condition) ((void)0)
+#else
+#define assert(condition) PW_ASSERT(condition)
+#endif // defined(NDEBUG)
diff --git a/pw_assert/public/pw_assert/internal/print_and_abort.h b/pw_assert/public/pw_assert/internal/print_and_abort.h
index 3efdf0980..be231f166 100644
--- a/pw_assert/public/pw_assert/internal/print_and_abort.h
+++ b/pw_assert/public/pw_assert/internal/print_and_abort.h
@@ -19,9 +19,9 @@
#include "pw_assert/config.h"
#if PW_ASSERT_ENABLE_DEBUG
-#define _PW_ASSERT_DEBUG_MACRO "or PW_DASSERT() "
+#define _PW_ASSERT_MACRO(type) "PW_" type "() or PW_D" type "()"
#else
-#define _PW_ASSERT_DEBUG_MACRO
+#define _PW_ASSERT_MACRO(type) "PW_" type "()"
#endif // PW_ASSERT_ENABLE_DEBUG
#ifdef __GNUC__
@@ -34,23 +34,28 @@
// expression using printf. Uses ANSI escape codes for colors.
//
// This is done with single printf to work better in multithreaded enironments.
-#define PW_ASSERT_HANDLE_FAILURE(expression) \
- fflush(stdout); \
- fprintf(stderr, \
- "\033[41m\033[37m\033[1m%s:%d:\033[0m " \
- "\033[1mPW_ASSERT() " _PW_ASSERT_DEBUG_MACRO \
- "\033[31mFAILED!\033[0m\n\n" \
- " FAILED ASSERTION\n\n" \
- " %s\n\n" \
- " FILE & LINE\n\n" \
- " %s:%d\n\n" \
- " FUNCTION\n\n" \
- " %s\n\n", \
- __FILE__, \
- __LINE__, \
- expression, \
- __FILE__, \
- __LINE__, \
- _PW_ASSERT_ABORT_FUNCTION); \
- fflush(stderr); \
+#define PW_ASSERT_HANDLE_FAILURE(expression) \
+ PW_ASSERT_PRINT_EXPRESSION("ASSERT", expression); \
+ fflush(stderr); \
abort()
+
+#define PW_ASSERT_PRINT_EXPRESSION(macro, expression) \
+ fflush(stdout); \
+ fprintf(stderr, \
+ "\033[41m\033[37m\033[1m%s:%d:\033[0m " \
+ "\033[1m" \
+ _PW_ASSERT_MACRO(macro) \
+ " " \
+ "\033[31mFAILED!\033[0m\n\n" \
+ " FAILED ASSERTION\n\n" \
+ " %s\n\n" \
+ " FILE & LINE\n\n" \
+ " %s:%d\n\n" \
+ " FUNCTION\n\n" \
+ " %s\n\n", \
+ __FILE__, \
+ __LINE__, \
+ expression, \
+ __FILE__, \
+ __LINE__, \
+ _PW_ASSERT_ABORT_FUNCTION)
diff --git a/pw_assert_basic/BUILD.bazel b/pw_assert_basic/BUILD.bazel
index 95f5a5b54..7308a580d 100644
--- a/pw_assert_basic/BUILD.bazel
+++ b/pw_assert_basic/BUILD.bazel
@@ -25,7 +25,7 @@ pw_cc_library(
name = "headers",
hdrs = [
"public/pw_assert_basic/assert_basic.h",
- "public_overrides/pw_assert_backend/assert_backend.h",
+ "public_overrides/pw_assert_backend/check_backend.h",
],
includes = [
"public",
@@ -67,9 +67,8 @@ pw_cc_library(
deps = [
":handler_facade",
":headers",
- "//pw_assert:facade",
"//pw_preprocessor",
- "//pw_string",
+ "//pw_string:builder",
"//pw_sys_io",
],
)
diff --git a/pw_assert_basic/BUILD.gn b/pw_assert_basic/BUILD.gn
index 72c22f176..4c5244317 100644
--- a/pw_assert_basic/BUILD.gn
+++ b/pw_assert_basic/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
import("backend.gni")
config("public_include_path") {
@@ -36,6 +37,9 @@ pw_facade("handler") {
public = [ "public/pw_assert_basic/handler.h" ]
}
+# TODO(b/235149326): This backend implements pw_assert's check backend and the
+# temporary compatibility C ABI (pw_assert_HandleFailure()).
+#
# pw_assert_basic only provides the backend's interface. The implementation is
# pulled in through pw_build_LINK_DEPS.
pw_source_set("pw_assert_basic") {
@@ -45,26 +49,19 @@ pw_source_set("pw_assert_basic") {
]
public = [
"public/pw_assert_basic/assert_basic.h",
- "public/pw_assert_basic/handler.h",
- "public_overrides/pw_assert_backend/assert_backend.h",
+ "public_overrides/pw_assert_backend/check_backend.h",
]
+ public_deps = [ ":handler.facade" ]
}
# The assert backend deps that might cause circular dependencies, since
# pw_assert is so ubiquitous. These deps are kept separate so they can be
# depended on from elsewhere.
pw_source_set("pw_assert_basic.impl") {
- public_deps = [
- dir_pw_result,
- dir_pw_string,
- dir_pw_sys_io,
- ]
deps = [
- ":pw_assert_basic",
+ ":handler",
+ "$dir_pw_assert:assert_compatibility_backend",
"$dir_pw_assert:config",
- "$dir_pw_assert:facade",
- "$dir_pw_preprocessor",
- pw_assert_basic_HANDLER_BACKEND,
]
sources = [ "assert_basic.cc" ]
}
@@ -83,10 +80,9 @@ pw_source_set("basic_handler") {
deps = [
":handler.facade",
"$dir_pw_assert:config",
- "$dir_pw_assert:facade",
"$dir_pw_preprocessor",
- "$dir_pw_sys_io:facade", # Only pull in the facade to avoid circular deps
- dir_pw_status,
+ "$dir_pw_string:builder",
+ "$dir_pw_sys_io",
]
sources = [ "basic_handler.cc" ]
}
@@ -94,3 +90,6 @@ pw_source_set("basic_handler") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_assert_basic/CMakeLists.txt b/pw_assert_basic/CMakeLists.txt
index ec2121ed4..64c93d0a7 100644
--- a/pw_assert_basic/CMakeLists.txt
+++ b/pw_assert_basic/CMakeLists.txt
@@ -13,12 +13,45 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_assert_basic/backend.cmake)
-pw_auto_add_simple_module(pw_assert_basic
- IMPLEMENTS_FACADE
- pw_assert
+pw_add_facade(pw_assert_basic.handler INTERFACE
+ BACKEND
+ pw_assert_basic.handler_BACKEND
+ HEADERS
+ public/pw_assert_basic/handler.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_preprocessor
+)
+
+# TODO(b/235149326): This backend implements pw_assert's check backend and the
+# temporary compatibility C ABI (pw_assert_HandleFailure()).
+pw_add_library(pw_assert_basic.check_backend STATIC
+ HEADERS
+ public_overrides/pw_assert_backend/check_backend.h
+ public/pw_assert_basic/assert_basic.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_assert_basic.handler
+ SOURCES
+ assert_basic.cc
+ PRIVATE_DEPS
+ pw_assert_basic.handler
+ pw_assert.config
+ pw_assert.assert_compatibility_backend
+)
+
+pw_add_library(pw_assert_basic.basic_handler STATIC
+ SOURCES
+ basic_handler.cc
PRIVATE_DEPS
+ pw_assert_basic.handler.facade
+ pw_assert.config
pw_preprocessor
- pw_string
+ pw_string.builder
pw_sys_io
)
diff --git a/pw_assert_basic/backend.cmake b/pw_assert_basic/backend.cmake
new file mode 100644
index 000000000..2e8755c2a
--- /dev/null
+++ b/pw_assert_basic/backend.cmake
@@ -0,0 +1,29 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Handler backend for the pw_assert_basic module which implements
+# pw_assert_basic_HandleFailure, this defaults to the basic_handler.
+#
+# Note: Don't confuse pw_assert.check_BACKEND and
+# pw_assert_basic.handler_BACKEND:
+# 1) pw_assert.check_BACKEND must be set to pw_assert_basic.check_backend in
+# order to use this module which ensures that asserts always invoke
+# pw_assert_basic_HandleFailure.
+# 2) pw_assert_basic.handler_BACKEND allows you to switch out the
+# implementation of the handler which is invoked (i.e.
+# pw_assert_basic_HandleFailure).
+pw_add_backend_variable(pw_assert_basic.handler_BACKEND)
diff --git a/pw_assert_basic/basic_handler.cc b/pw_assert_basic/basic_handler.cc
index 6a6e73414..046b10087 100644
--- a/pw_assert_basic/basic_handler.cc
+++ b/pw_assert_basic/basic_handler.cc
@@ -14,8 +14,8 @@
// This is a very basic direct output log implementation with no buffering.
-//#define PW_LOG_MODULE_NAME "ASRT"
-//#include "pw_log/log.h"
+// #define PW_LOG_MODULE_NAME "ASRT"
+// #include "pw_log/log.h"
#include <cstdio>
#include <cstring>
@@ -26,15 +26,6 @@
#include "pw_string/string_builder.h"
#include "pw_sys_io/sys_io.h"
-// If 1, call C's standard abort() function on assert failure.
-#ifndef PW_ASSERT_BASIC_ABORT
-#define PW_ASSERT_BASIC_ABORT 1
-#endif // PW_ASSERT_BASIC_ABORT
-
-// TODO(pwbug/17): Expose these through the config system.
-#define PW_ASSERT_BASIC_SHOW_BANNER 1
-#define PW_ASSERT_BASIC_USE_COLORS 1
-
// ANSI color constants to control the terminal. Not Windows compatible.
// clang-format off
#if PW_ASSERT_BASIC_USE_COLORS
@@ -81,7 +72,7 @@ static const char* kCrashBanner[] = {
static void WriteLine(const std::string_view& s) {
pw::sys_io::WriteLine(s)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
typedef pw::StringBuffer<150> Buffer;
@@ -141,21 +132,27 @@ extern "C" void pw_assert_basic_HandleFailure(const char* file_name,
// device. At some point we'll have a reboot BSP function or similar, but for
// now this is acceptable since no one is using this basic backend.
if (!PW_ASSERT_BASIC_DISABLE_NORETURN) {
- if (PW_ASSERT_BASIC_ABORT) {
- // abort() doesn't flush stderr/stdout, so manually flush them before
- // aborting. abort() is preferred to exit(1) because debuggers catch it.
- std::fflush(stderr);
- std::fflush(stdout);
- std::abort();
- } else {
- WriteLine("");
- WriteLine(MAGENTA " HANG TIME" RESET);
- WriteLine("");
- WriteLine(
- " ... until a debugger joins. System is waiting in a while(1)");
- while (true) {
- }
+#if (PW_ASSERT_BASIC_ACTION == PW_ASSERT_BASIC_ACTION_ABORT)
+ // abort() doesn't flush stderr/stdout, so manually flush them before
+ // aborting. abort() is preferred to exit(1) because debuggers catch it.
+ std::fflush(stderr);
+ std::fflush(stdout);
+ std::abort();
+#elif (PW_ASSERT_BASIC_ACTION == PW_ASSERT_BASIC_ACTION_EXIT)
+ // Use _Exit to not run destructors or atexit hooks in case they cause
+ // further crashes.
+ std::_Exit(-1);
+#elif (PW_ASSERT_BASIC_ACTION == PW_ASSERT_BASIC_ACTION_LOOP)
+ WriteLine("");
+ WriteLine(MAGENTA " HANG TIME" RESET);
+ WriteLine("");
+ WriteLine(
+ " ... until a debugger joins. System is waiting in a while(1)");
+ while (true) {
}
+#else
+#error PW_ASSERT_BASIC_ACTION Must be set to valid option.
+#endif
PW_UNREACHABLE;
} else {
WriteLine("");
diff --git a/pw_assert_basic/docs.rst b/pw_assert_basic/docs.rst
index a06627f9b..1f8885925 100644
--- a/pw_assert_basic/docs.rst
+++ b/pw_assert_basic/docs.rst
@@ -20,6 +20,37 @@ behavior.
intended mostly for ease of initial bringup. We encourage teams to use
tokenized asserts since they are much smaller both in terms of ROM and RAM.
+----------------------------
+Module Configuration Options
+----------------------------
+The following configurations can be adjusted via compile-time configuration of
+this module, see the
+:ref:`module documentation <module-structure-compile-time-configuration>` for
+more details.
+
+.. c:macro:: PW_ASSERT_BASIC_ACTION
+
+ Controls what happens after an assert failure. Should be set to one of the
+ following options:
+
+ - PW_ASSERT_BASIC_ACTION_ABORT: Call std::abort()
+ - PW_ASSERT_BASIC_ACTION_EXIT: Call std::_Exit(-1)
+ - PW_ASSERT_BASIC_ACTION_LOOP: Loop forever
+
+ Defaults to abort.
+
+.. c:macro:: PW_ASSERT_BASIC_SHOW_BANNER
+
+ Controls whether ASCII art banner is printed on assert failure.
+
+ This defaults to enabled.
+
+.. c:macro:: PW_ASSERT_BASIC_USE_COLORS
+
+ Controls whether colors are used in assert message printed to console.
+
+ This defaults to enabled.
+
.. _module-pw_assert_basic-custom_handler:
Custom handler backend example
diff --git a/pw_assert_basic/public/pw_assert_basic/assert_basic.h b/pw_assert_basic/public/pw_assert_basic/assert_basic.h
index 88405f88a..6d8bd6cf8 100644
--- a/pw_assert_basic/public/pw_assert_basic/assert_basic.h
+++ b/pw_assert_basic/public/pw_assert_basic/assert_basic.h
@@ -18,12 +18,22 @@
#include "pw_preprocessor/compiler.h"
#include "pw_preprocessor/util.h"
+// Use __PRETTY_FUNCTION__, a GNU extension, in place of the __func__ macro when
+// supported. __PRETTY_FUNCTION__ expands to the full C++ function name.
+#ifdef __GNUC__
+#define _PW_ASSERT_BASIC_FUNCTION_NAME __PRETTY_FUNCTION__
+#else
+#define _PW_ASSERT_BASIC_FUNCTION_NAME __func__
+#endif // __GNUC__
+
// Die with a message with many attributes included. This is the crash macro
// frontend that funnels everything into the C handler provided by the user,
// pw_assert_basic_HandleFailure().
#define PW_HANDLE_CRASH(...) \
pw_assert_basic_HandleFailure( \
- __FILE__, __LINE__, __func__ PW_COMMA_ARGS(__VA_ARGS__))
+ __FILE__, \
+ __LINE__, \
+ _PW_ASSERT_BASIC_FUNCTION_NAME PW_COMMA_ARGS(__VA_ARGS__))
// Die with a message with many attributes included. This is the crash macro
// frontend that funnels everything into the C handler provided by the user,
@@ -31,7 +41,7 @@
#define PW_HANDLE_ASSERT_FAILURE(condition_string, message, ...) \
pw_assert_basic_HandleFailure(__FILE__, \
__LINE__, \
- __func__, \
+ _PW_ASSERT_BASIC_FUNCTION_NAME, \
"Check failed: " condition_string \
". " message PW_COMMA_ARGS(__VA_ARGS__))
@@ -51,13 +61,13 @@
type_fmt, \
message, ...) \
pw_assert_basic_HandleFailure( \
- __FILE__, \
- __LINE__, \
- __func__, \
- "Check failed: " \
- arg_a_str " (=" type_fmt ") " \
- comparison_op_str " " \
- arg_b_str " (=" type_fmt ")" \
- ". " message, \
- arg_a_val, arg_b_val PW_COMMA_ARGS(__VA_ARGS__))
+ __FILE__, \
+ __LINE__, \
+ _PW_ASSERT_BASIC_FUNCTION_NAME, \
+ "Check failed: " \
+ arg_a_str " (=" type_fmt ") " \
+ comparison_op_str " " \
+ arg_b_str " (=" type_fmt ")" \
+ ". " message, \
+ arg_a_val, arg_b_val PW_COMMA_ARGS(__VA_ARGS__))
// clang-format on
diff --git a/pw_assert_basic/public/pw_assert_basic/handler.h b/pw_assert_basic/public/pw_assert_basic/handler.h
index 33dd3541a..3d1c4f5ab 100644
--- a/pw_assert_basic/public/pw_assert_basic/handler.h
+++ b/pw_assert_basic/public/pw_assert_basic/handler.h
@@ -17,8 +17,8 @@
#include "pw_preprocessor/util.h"
// This is needed for testing the basic crash handler.
-// TODO(pwbug/17): Replace when Pigweed config system is added.
#define PW_ASSERT_BASIC_DISABLE_NORETURN 0
+
#if PW_ASSERT_BASIC_DISABLE_NORETURN
#define PW_ASSERT_NORETURN
#else
diff --git a/pw_assert_basic/public_overrides/pw_assert_backend/assert_backend.h b/pw_assert_basic/public_overrides/pw_assert_backend/check_backend.h
index 5cdee4f62..5cdee4f62 100644
--- a/pw_assert_basic/public_overrides/pw_assert_backend/assert_backend.h
+++ b/pw_assert_basic/public_overrides/pw_assert_backend/check_backend.h
diff --git a/pw_assert_log/Android.bp b/pw_assert_log/Android.bp
new file mode 100644
index 000000000..d245f25b0
--- /dev/null
+++ b/pw_assert_log/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_assert_log_headers",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: [
+ "assert_backend_public_overrides",
+ "check_backend_public_overrides",
+ "public",
+ ],
+ host_supported: true,
+}
diff --git a/pw_assert_log/BUILD.bazel b/pw_assert_log/BUILD.bazel
index 3f21931ca..e5b794737 100644
--- a/pw_assert_log/BUILD.bazel
+++ b/pw_assert_log/BUILD.bazel
@@ -22,17 +22,17 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
- name = "pw_assert_log",
+ name = "check_backend",
srcs = [
"assert_log.cc",
],
hdrs = [
- "public/pw_assert_log/assert_log.h",
- "public_overrides/pw_assert_backend/assert_backend.h",
+ "check_backend_public_overrides/pw_assert_backend/check_backend.h",
+ "public/pw_assert_log/check_log.h",
],
includes = [
+ "check_backend_public_overrides",
"public",
- "public_overrides",
],
deps = [
"//pw_assert:facade",
@@ -42,16 +42,17 @@ pw_cc_library(
)
pw_cc_library(
- name = "lite_backend",
+ name = "assert_backend",
hdrs = [
- "assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h",
- "public/pw_assert_log/assert_lite_log.h",
+ "assert_backend_public_overrides/pw_assert_backend/assert_backend.h",
+ "public/pw_assert_log/assert_log.h",
],
includes = [
- "assert_lite_public_overrides",
+ "assert_backend_public_overrides",
"public",
],
deps = [
+ "//pw_log",
"//pw_preprocessor",
],
)
diff --git a/pw_assert_log/BUILD.gn b/pw_assert_log/BUILD.gn
index addc8882c..b62387315 100644
--- a/pw_assert_log/BUILD.gn
+++ b/pw_assert_log/BUILD.gn
@@ -16,56 +16,71 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
-config("default_config") {
+config("public_include_path") {
include_dirs = [ "public" ]
}
-config("backend_config") {
- include_dirs = [ "public_overrides" ]
+config("check_backend_overrides") {
+ include_dirs = [ "check_backend_public_overrides" ]
}
-config("lite_backend_overrides") {
- include_dirs = [ "assert_lite_public_overrides" ]
+config("assert_backend_overrides") {
+ include_dirs = [ "assert_backend_public_overrides" ]
}
-# This backend to pw_assert's PW_CHECK()/PW_CRASH() macros via PW_LOG.
-pw_source_set("pw_assert_log") {
+# This target provides a backend to pw_assert's check facade
+# (PW_CHECK()/PW_CRASH() macros) and assert compatibility C ABI
+# (the pw_assert_HandleFailure() function) via PW_LOG.
+pw_source_set("check_backend") {
public_configs = [
- ":backend_config",
- ":default_config",
+ ":check_backend_overrides",
+ ":public_include_path",
]
public_deps = [ "$dir_pw_log" ]
- public = [ "public_overrides/pw_assert_backend/assert_backend.h" ]
+ public =
+ [ "check_backend_public_overrides/pw_assert_backend/check_backend.h" ]
deps = [
+ "$dir_pw_assert:assert_compatibility_backend",
"$dir_pw_assert:config",
"$dir_pw_assert:facade",
"$dir_pw_preprocessor",
]
sources = [
+ # TODO(b/235149326): assert_log.cc implements the assert compatibility
+ # backend, but nothing for check_backend.
"assert_log.cc",
- "public/pw_assert_log/assert_log.h",
+ "public/pw_assert_log/check_log.h",
]
}
+# TODO(b/235149326): Remove this deprecated alias.
+group("pw_assert_log") {
+ public_deps = [ ":check_backend" ]
+}
+
# This backend to pw_assert's PW_ASSERT() macros via PW_LOG. It is intended only
# for use with PW_LOG backends which are constexpr compatible such as
# pw_log_android.
-#
-# Warning: The "lite" naming is transitional. assert_lite_backend.h headers
-# will be renamed as the pw_assert API is reassessed. (pwbug/246)
-pw_source_set("lite_backend") {
+pw_source_set("assert_backend") {
public_configs = [
- ":lite_backend_overrides",
- ":default_config",
+ ":assert_backend_overrides",
+ ":public_include_path",
+ ]
+ public_deps = [
+ "$dir_pw_log",
+ "$dir_pw_preprocessor",
]
- public_deps = [ dir_pw_preprocessor ]
public =
- [ "assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h" ]
- sources = [ "public/pw_assert_log/assert_lite_log.h" ]
+ [ "assert_backend_public_overrides/pw_assert_backend/assert_backend.h" ]
+ sources = [ "public/pw_assert_log/assert_log.h" ]
+}
+
+group("assert_backend.impl") {
}
-group("lite_compatibility_backend.impl") {
+group("check_backend.impl") {
}
# pw_assert_log doesn't have deps with potential circular dependencies, so this
@@ -76,3 +91,6 @@ group("pw_assert_log.impl") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_assert_log/CMakeLists.txt b/pw_assert_log/CMakeLists.txt
index 96776e00d..dda9ae154 100644
--- a/pw_assert_log/CMakeLists.txt
+++ b/pw_assert_log/CMakeLists.txt
@@ -14,11 +14,35 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_auto_add_simple_module(pw_assert_log
- IMPLEMENTS_FACADE
- pw_assert
+# This backend to pw_assert's PW_CHECK()/PW_CRASH() macros via PW_LOG.
+pw_add_library(pw_assert_log.check_backend STATIC
+ HEADERS
+ check_backend_public_overrides/pw_assert_backend/check_backend.h
+ public/pw_assert_log/check_log.h
+ PUBLIC_INCLUDES
+ check_backend_public_overrides
+ public
PUBLIC_DEPS
pw_log
+ pw_preprocessor
+ SOURCES
+ assert_log.cc
PRIVATE_DEPS
+ pw_assert.config
+ pw_assert.assert_compatibility_backend
+)
+
+# This backend to pw_assert's PW_ASSERT() macros via PW_LOG. It is intended only
+# for use with PW_LOG backends which are constexpr compatible such as
+# pw_log_android.
+pw_add_library(pw_assert_log.assert_backend INTERFACE
+ HEADERS
+ assert_backend_public_overrides/pw_assert_backend/assert_backend.h
+ public/pw_assert_log/assert_log.h
+ PUBLIC_INCLUDES
+ assert_backend_public_overrides
+ public
+ PUBLIC_DEPS
+ pw_log
pw_preprocessor
)
diff --git a/pw_assert_log/public_overrides/pw_assert_backend/assert_backend.h b/pw_assert_log/assert_backend_public_overrides/pw_assert_backend/assert_backend.h
index 8522926b6..76e16b6a8 100644
--- a/pw_assert_log/public_overrides/pw_assert_backend/assert_backend.h
+++ b/pw_assert_log/assert_backend_public_overrides/pw_assert_backend/assert_backend.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
diff --git a/pw_assert_log/assert_log.cc b/pw_assert_log/assert_log.cc
index 0e8be1852..00212e01b 100644
--- a/pw_assert_log/assert_log.cc
+++ b/pw_assert_log/assert_log.cc
@@ -12,17 +12,21 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pw_assert_log/assert_log.h"
-
#include "pw_assert/config.h"
+#include "pw_log/levels.h"
+#include "pw_log/log.h"
+#include "pw_log/options.h"
+#include "pw_preprocessor/compiler.h"
extern "C" void pw_assert_HandleFailure(void) {
#if PW_ASSERT_ENABLE_DEBUG
PW_LOG(PW_LOG_LEVEL_FATAL,
+ "pw_assert_log",
PW_LOG_FLAGS,
"Crash: PW_ASSERT() or PW_DASSERT() failure");
#else
PW_LOG(PW_LOG_LEVEL_FATAL,
+ "pw_assert_log",
PW_LOG_FLAGS,
"Crash: PW_ASSERT() failure. Note: PW_DASSERT disabled");
#endif // PW_ASSERT_ENABLE_DEBUG
diff --git a/pw_assert_log/assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h b/pw_assert_log/check_backend_public_overrides/pw_assert_backend/check_backend.h
index 91a6ff6ef..03d85161a 100644
--- a/pw_assert_log/assert_lite_public_overrides/pw_assert_backend/assert_lite_backend.h
+++ b/pw_assert_log/check_backend_public_overrides/pw_assert_backend/check_backend.h
@@ -1,4 +1,4 @@
-// Copyright 2022 The Pigweed Authors
+// Copyright 2020 The Pigweed Authors
//
// 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
@@ -17,4 +17,4 @@
// (though only pw_assert/check.h can only point to 1 backend).
#pragma once
-#include "pw_assert_log/assert_lite_log.h"
+#include "pw_assert_log/check_log.h"
diff --git a/pw_assert_log/public/pw_assert_log/assert_log.h b/pw_assert_log/public/pw_assert_log/assert_log.h
index d238e57ec..20e92db92 100644
--- a/pw_assert_log/public/pw_assert_log/assert_log.h
+++ b/pw_assert_log/public/pw_assert_log/assert_log.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -17,53 +17,15 @@
#include "pw_log/log.h"
#include "pw_log/options.h"
#include "pw_preprocessor/compiler.h"
-#include "pw_preprocessor/util.h"
-// Die with a message with several attributes included. This crash frontend
-// funnels everything into the logger, which must then handle the true crash
-// behaviour.
-#define PW_HANDLE_CRASH(message, ...) \
- do { \
- PW_LOG(PW_LOG_LEVEL_FATAL, PW_LOG_FLAGS, "Crash: " message, __VA_ARGS__); \
- PW_UNREACHABLE; \
+// Forward directly to the backend PW_HANDLE_LOG() macro rather than PW_LOG().
+// PW_LOG() checks the user-overridable PW_LOG_LEVEL macro, which may not work
+// in headers without additional handling.
+#define PW_ASSERT_HANDLE_FAILURE(condition_string) \
+ do { \
+ PW_HANDLE_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_MODULE_NAME, \
+ PW_LOG_FLAGS, \
+ "Assert failed: " condition_string); \
+ PW_UNREACHABLE; \
} while (0)
-
-// Die with a message with several attributes included. This assert frontend
-// funnels everything into the logger, which is responsible for displaying the
-// log, then crashing/rebooting the device.
-#define PW_HANDLE_ASSERT_FAILURE(condition_string, message, ...) \
- do { \
- PW_LOG(PW_LOG_LEVEL_FATAL, \
- PW_LOG_FLAGS, \
- "Check failed: " condition_string ". " message, \
- __VA_ARGS__); \
- PW_UNREACHABLE; \
- } while (0)
-
-// Sample assert failure message produced by the below implementation:
-//
-// Check failed: old_x (=610) < new_x (=50). Details: foo=10, bar.
-//
-// Putting the value next to the operand makes the string easier to read.
-
-// clang-format off
-// This is too hairy for clang format to handle and retain readability.
-#define PW_HANDLE_ASSERT_BINARY_COMPARE_FAILURE(arg_a_str, \
- arg_a_val, \
- comparison_op_str, \
- arg_b_str, \
- arg_b_val, \
- type_fmt, \
- message, ...) \
- do { \
- PW_LOG(PW_LOG_LEVEL_FATAL, \
- PW_LOG_FLAGS, \
- "Check failed: " \
- arg_a_str " (=" type_fmt ") " \
- comparison_op_str " " \
- arg_b_str " (=" type_fmt ")" \
- ". " message, \
- arg_a_val, arg_b_val, __VA_ARGS__); \
- PW_UNREACHABLE; \
- } while(0)
-// clang-format on
diff --git a/pw_assert_log/public/pw_assert_log/check_log.h b/pw_assert_log/public/pw_assert_log/check_log.h
new file mode 100644
index 000000000..158cf6af6
--- /dev/null
+++ b/pw_assert_log/public/pw_assert_log/check_log.h
@@ -0,0 +1,75 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_log/levels.h"
+#include "pw_log/log.h"
+#include "pw_log/options.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_preprocessor/util.h"
+
+// Die with a message with several attributes included. This crash frontend
+// funnels everything into the logger, which must then handle the true crash
+// behaviour.
+#define PW_HANDLE_CRASH(message, ...) \
+ do { \
+ PW_HANDLE_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_MODULE_NAME, \
+ PW_LOG_FLAGS, \
+ "Crash: " message, \
+ __VA_ARGS__); \
+ PW_UNREACHABLE; \
+ } while (0)
+
+// Die with a message with several attributes included. This assert frontend
+// funnels everything into the logger, which is responsible for displaying the
+// log, then crashing/rebooting the device.
+#define PW_HANDLE_ASSERT_FAILURE(condition_string, message, ...) \
+ do { \
+ PW_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_MODULE_NAME, \
+ PW_LOG_FLAGS, \
+ "Check failed: " condition_string ". " message, \
+ __VA_ARGS__); \
+ PW_UNREACHABLE; \
+ } while (0)
+
+// Sample assert failure message produced by the below implementation:
+//
+// Check failed: old_x (=610) < new_x (=50). Details: foo=10, bar.
+//
+// Putting the value next to the operand makes the string easier to read.
+
+// clang-format off
+// This is too hairy for clang format to handle and retain readability.
+#define PW_HANDLE_ASSERT_BINARY_COMPARE_FAILURE(arg_a_str, \
+ arg_a_val, \
+ comparison_op_str, \
+ arg_b_str, \
+ arg_b_val, \
+ type_fmt, \
+ message, ...) \
+ do { \
+ PW_LOG(PW_LOG_LEVEL_FATAL, \
+ PW_LOG_MODULE_NAME, \
+ PW_LOG_FLAGS, \
+ "Check failed: " \
+ arg_a_str " (=" type_fmt ") " \
+ comparison_op_str " " \
+ arg_b_str " (=" type_fmt ")" \
+ ". " message, \
+ arg_a_val, arg_b_val, __VA_ARGS__); \
+ PW_UNREACHABLE; \
+ } while(0)
+// clang-format on
diff --git a/pw_assert_tokenized/BUILD.bazel b/pw_assert_tokenized/BUILD.bazel
index bd2fe861f..c3c49141f 100644
--- a/pw_assert_tokenized/BUILD.bazel
+++ b/pw_assert_tokenized/BUILD.bazel
@@ -27,8 +27,8 @@ pw_cc_library(
"log_handler.cc",
],
hdrs = [
- "assert_public_overrides/pw_assert_backend/assert_lite_backend.h",
- "check_public_overrides/pw_assert_backend/assert_backend.h",
+ "assert_public_overrides/pw_assert_backend/assert_backend.h",
+ "check_public_overrides/pw_assert_backend/check_backend.h",
"public/pw_assert_tokenized/assert_tokenized.h",
"public/pw_assert_tokenized/check_tokenized.h",
"public/pw_assert_tokenized/handler.h",
@@ -40,7 +40,9 @@ pw_cc_library(
],
deps = [
"//pw_assert",
- "//pw_log_tokenized,",
+ "//pw_base64",
+ "//pw_bytes",
+ "//pw_log_tokenized",
"//pw_preprocessor",
"//pw_tokenizer",
],
diff --git a/pw_assert_tokenized/BUILD.gn b/pw_assert_tokenized/BUILD.gn
index 42561a45d..bcc9eec4c 100644
--- a/pw_assert_tokenized/BUILD.gn
+++ b/pw_assert_tokenized/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
pw_assert_tokenized_HANDLER_BACKEND = "$dir_pw_assert_tokenized:log_handler"
@@ -52,7 +53,7 @@ pw_source_set("assert_backend") {
"$dir_pw_tokenizer",
]
public = [
- "assert_public_overrides/pw_assert_backend/assert_lite_backend.h",
+ "assert_public_overrides/pw_assert_backend/assert_backend.h",
"public/pw_assert_tokenized/assert_tokenized.h",
]
}
@@ -68,10 +69,11 @@ pw_source_set("check_backend") {
]
public_deps = [
":handler",
+ "$dir_pw_log_tokenized",
"$dir_pw_tokenizer",
]
public = [
- "check_public_overrides/pw_assert_backend/assert_backend.h",
+ "check_public_overrides/pw_assert_backend/check_backend.h",
"public/pw_assert_tokenized/check_tokenized.h",
]
}
@@ -99,8 +101,10 @@ pw_source_set("log_handler") {
":handler",
"$dir_pw_assert:config",
"$dir_pw_base64",
+ "$dir_pw_bytes",
"$dir_pw_log",
"$dir_pw_log_tokenized",
+ "$dir_pw_span",
]
sources = [ "log_handler.cc" ]
}
@@ -108,3 +112,6 @@ pw_source_set("log_handler") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_assert_tokenized/CMakeLists.txt b/pw_assert_tokenized/CMakeLists.txt
new file mode 100644
index 000000000..0728a6248
--- /dev/null
+++ b/pw_assert_tokenized/CMakeLists.txt
@@ -0,0 +1,57 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_assert_tokenized.handler STATIC
+ HEADERS
+ public/pw_assert_tokenized/handler.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_preprocessor
+ SOURCES
+ log_handler.cc
+ PRIVATE_DEPS
+ pw_assert.config
+ pw_base64
+ pw_bytes
+ pw_log
+ pw_log_tokenized
+)
+
+pw_add_library(pw_assert_tokenized.assert_backend INTERFACE
+ HEADERS
+ assert_public_overrides/pw_assert_backend/assert_backend.h
+ public/pw_assert_tokenized/assert_tokenized.h
+ PUBLIC_INCLUDES
+ assert_public_overrides
+ public
+ PUBLIC_DEPS
+ pw_assert_tokenized.handler
+ pw_tokenizer
+)
+
+pw_add_library(pw_assert_tokenized.check_backend INTERFACE
+ HEADERS
+ check_public_overrides/pw_assert_backend/check_backend.h
+ public/pw_assert_tokenized/check_tokenized.h
+ PUBLIC_INCLUDES
+ check_public_overrides
+ public
+ PUBLIC_DEPS
+ pw_assert_tokenized.handler
+ pw_log_tokenized
+ pw_tokenizer
+)
diff --git a/pw_assert_tokenized/assert_public_overrides/pw_assert_backend/assert_lite_backend.h b/pw_assert_tokenized/assert_public_overrides/pw_assert_backend/assert_backend.h
index 561f5216f..561f5216f 100644
--- a/pw_assert_tokenized/assert_public_overrides/pw_assert_backend/assert_lite_backend.h
+++ b/pw_assert_tokenized/assert_public_overrides/pw_assert_backend/assert_backend.h
diff --git a/pw_assert_tokenized/check_public_overrides/pw_assert_backend/assert_backend.h b/pw_assert_tokenized/check_public_overrides/pw_assert_backend/check_backend.h
index 4a1e3aeb4..4a1e3aeb4 100644
--- a/pw_assert_tokenized/check_public_overrides/pw_assert_backend/assert_backend.h
+++ b/pw_assert_tokenized/check_public_overrides/pw_assert_backend/check_backend.h
diff --git a/pw_assert_tokenized/docs.rst b/pw_assert_tokenized/docs.rst
index 0a0aeebf2..2458b5ad3 100644
--- a/pw_assert_tokenized/docs.rst
+++ b/pw_assert_tokenized/docs.rst
@@ -24,10 +24,10 @@ the reported tokens.
* **PW_CHECK_\*()**: The ``PW_CHECK_*()`` macros work in contexts where
tokenization is fully supported, so they are able to capture the CHECK
statement expression and any provided string literal in addition to the file
- name:
+ name in the pw_log_tokenized key/value format:
- Check failure in pw_metric/size_report/base.cc: \*unoptimizable >= 0,
- Ensure this CHECK logic stays.
+ "■msg♦Check failure: \*unoptimizable >= 0, Ensure this CHECK logic
+ stays■module♦KVS■file♦pw_kvs/size_report/base.cc"
Evaluated values of ``PW_CHECK_*()`` statements are not captured, and any
string formatting arguments are also not captured. This minimizes call-site
@@ -49,10 +49,10 @@ Setup
#. Set ``pw_assert_BACKEND = "$dir_pw_assert_tokenized:check_backend"`` and
``pw_assert_LITE_BACKEND = "$dir_pw_assert_tokenized:assert_backend"`` in
your target configuration.
-#. Ensure your target provides ``pw_tokenizer_GLOBAL_HANDLER_BACKEND``. By
- default, pw_assert_tokenized will forward assert failures to the tokenizer
- handler as logs. The tokenizer handler should check for ``LOG_LEVEL_FATAL``
- and properly divert to a crash handler.
+#. Ensure your target provides
+ ``pw_log_tokenized_HANDLER_BACKEND``. By default, pw_assert_tokenized will
+ forward assert failures to the log system. The tokenizer handler should check
+ for ``LOG_LEVEL_FATAL`` and properly divert to a crash handler.
#. Add file name tokens to your token database. pw_assert_tokenized can't create
file name tokens that can be parsed out of the final compiled binary. The
``pw_relative_source_file_names``
diff --git a/pw_assert_tokenized/log_handler.cc b/pw_assert_tokenized/log_handler.cc
index ea559eabb..c415efb2b 100644
--- a/pw_assert_tokenized/log_handler.cc
+++ b/pw_assert_tokenized/log_handler.cc
@@ -15,13 +15,17 @@
#include <cstdint>
#include <cstring>
#include <memory>
-#include <span>
#include "pw_assert/config.h"
#include "pw_assert_tokenized/handler.h"
#include "pw_base64/base64.h"
+#include "pw_bytes/endian.h"
#include "pw_log/log.h"
+#include "pw_log_tokenized/config.h"
+#include "pw_log_tokenized/handler.h"
#include "pw_log_tokenized/log_tokenized.h"
+#include "pw_log_tokenized/metadata.h"
+#include "pw_span/span.h"
extern "C" void pw_assert_tokenized_HandleAssertFailure(
uint32_t tokenized_file_name, int line_number) {
@@ -32,17 +36,19 @@ extern "C" void pw_assert_tokenized_HandleAssertFailure(
char base64_buffer[kBufferSize];
size_t len =
- pw::base64::Encode(std::span(hash_buffer, sizeof(tokenized_file_name)),
- std::span(base64_buffer));
+ pw::base64::Encode(pw::span(hash_buffer, sizeof(tokenized_file_name)),
+ pw::span(base64_buffer));
base64_buffer[len] = '\0';
#if PW_ASSERT_ENABLE_DEBUG
PW_LOG(PW_LOG_LEVEL_FATAL,
+ "pw_assert_tokenized",
PW_LOG_FLAGS,
"PW_ASSERT() or PW_DASSERT() failure at $%s:%d",
base64_buffer,
line_number);
#else
PW_LOG(PW_LOG_LEVEL_FATAL,
+ "pw_assert_tokenized",
PW_LOG_FLAGS,
"PW_ASSERT() failure. Note: PW_DASSERT disabled $%s:%d",
base64_buffer,
@@ -53,14 +59,20 @@ extern "C" void pw_assert_tokenized_HandleAssertFailure(
extern "C" void pw_assert_tokenized_HandleCheckFailure(
uint32_t tokenized_message, int line_number) {
- // TODO(amontanez): There should be a less-hacky way to assemble this.
- const uint32_t payload = _PW_LOG_TOKENIZED_LEVEL(PW_LOG_LEVEL_FATAL) |
- _PW_LOG_TOKENIZED_FLAGS(PW_LOG_FLAGS) |
- _PW_LOG_TOKENIZED_LINE(line_number);
- uint8_t token_buffer[sizeof(tokenized_message)];
- memcpy(token_buffer, &tokenized_message, sizeof(tokenized_message));
+ // If line_number is too large to fit in the packed payload, the Metadata
+ // class will properly set it to 0, which is the expected value for line
+ // number values that would cause the bit field to overflow.
+ // See https://pigweed.dev/pw_log_tokenized/#c.PW_LOG_TOKENIZED_LINE_BITS for
+ // more info.
+ const uint32_t payload = pw::log_tokenized::Metadata(
+ PW_LOG_LEVEL_FATAL, 0, PW_LOG_FLAGS, line_number)
+ .value();
+ std::array<std::byte, sizeof(tokenized_message)> token_buffer =
+ pw::bytes::CopyInOrder(pw::endian::little, tokenized_message);
- pw_tokenizer_HandleEncodedMessageWithPayload(
- payload, token_buffer, sizeof(token_buffer));
+ pw_log_tokenized_HandleLog(
+ payload,
+ reinterpret_cast<const uint8_t*>(token_buffer.data()),
+ token_buffer.size());
PW_UNREACHABLE;
}
diff --git a/pw_assert_tokenized/public/pw_assert_tokenized/check_tokenized.h b/pw_assert_tokenized/public/pw_assert_tokenized/check_tokenized.h
index bda3a1abf..779c227f9 100644
--- a/pw_assert_tokenized/public/pw_assert_tokenized/check_tokenized.h
+++ b/pw_assert_tokenized/public/pw_assert_tokenized/check_tokenized.h
@@ -14,13 +14,14 @@
#pragma once
#include "pw_assert_tokenized/handler.h"
+#include "pw_log_tokenized/config.h"
#include "pw_tokenizer/tokenize.h"
-#define _PW_ASSERT_TOKENIZED_TO_HANDLER(str) \
- do { \
- const uint32_t token = \
- PW_TOKENIZE_STRING("Check failure in " __FILE__ ": " str); \
- pw_assert_tokenized_HandleCheckFailure(token, __LINE__); \
+#define _PW_ASSERT_TOKENIZED_TO_HANDLER(str) \
+ do { \
+ const uint32_t token = PW_TOKENIZE_STRING( \
+ PW_LOG_TOKENIZED_FORMAT_STRING("Check failure: " str)); \
+ pw_assert_tokenized_HandleCheckFailure(token, __LINE__); \
} while (0)
#define PW_HANDLE_CRASH(...) _PW_ASSERT_TOKENIZED_TO_HANDLER(#__VA_ARGS__)
diff --git a/pw_assert_zephyr/BUILD.gn b/pw_assert_zephyr/BUILD.gn
index 9a6699a8e..1b11f77f5 100644
--- a/pw_assert_zephyr/BUILD.gn
+++ b/pw_assert_zephyr/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_assert_zephyr/CMakeLists.txt b/pw_assert_zephyr/CMakeLists.txt
index 1d55f658d..3f06d6099 100644
--- a/pw_assert_zephyr/CMakeLists.txt
+++ b/pw_assert_zephyr/CMakeLists.txt
@@ -18,10 +18,27 @@ if(NOT CONFIG_PIGWEED_ASSERT)
return()
endif()
-pw_auto_add_simple_module(pw_assert_zephyr
- IMPLEMENTS_FACADE
- pw_assert
+pw_add_library(pw_assert_zephyr.check INTERFACE
+ HEADERS
+ public/pw_assert_zephyr/check_zephyr.h
+ public_overrides/pw_assert_backend/check_backend.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_preprocessor
+ zephyr_interface
)
-pw_set_backend(pw_assert pw_assert_zephyr)
-target_link_libraries(pw_assert_zephyr PUBLIC zephyr_interface)
-zephyr_link_libraries(pw_assert_zephyr)
+zephyr_link_libraries(pw_assert_zephyr.check)
+
+pw_add_library(pw_assert_zephyr.assert INTERFACE
+ HEADERS
+ public/pw_assert_zephyr/assert_zephyr.h
+ public_overrides/pw_assert_backend/assert_backend.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ zephyr_interface
+)
+zephyr_link_libraries(pw_assert_zephyr.assert)
diff --git a/pw_assert_zephyr/docs.rst b/pw_assert_zephyr/docs.rst
index a9313fd2b..b70a15ea2 100644
--- a/pw_assert_zephyr/docs.rst
+++ b/pw_assert_zephyr/docs.rst
@@ -8,10 +8,14 @@ pw_assert_zephyr
Overview
--------
This assert backend implements the ``pw_assert`` facade, by routing the assert
-message to the Zephyr assert subsystem. Failed asserts will call ``k_panic()``.
+message to the Zephyr assert subsystem. Failed asserts will call:
+1) ``__ASSERT_LOC(condition)``
+2) If and only if there's a message ``__ASSERT_MSG_INFO(message, ...)``
+3) ``__ASSERT_POST_ACTION()``
+
To enable the assert module, set ``CONFIG_PIGWEED_ASSERT=y``. After that,
Zephyr's assert configs can be used to control the behavior via CONFIG_ASSERT_
and CONFIG_ASSERT_LEVEL_.
.. _CONFIG_ASSERT: https://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_ASSERT.html#std-kconfig-CONFIG_ASSERT
-.. _CONFIG_ASSERT_LEVEL: https://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_ASSERT_LEVEL.html#std-kconfig-CONFIG_ASSERT_LEVEL \ No newline at end of file
+.. _CONFIG_ASSERT_LEVEL: https://docs.zephyrproject.org/latest/reference/kconfig/CONFIG_ASSERT_LEVEL.html#std-kconfig-CONFIG_ASSERT_LEVEL
diff --git a/pw_assert_zephyr/public/pw_assert_zephyr/assert_zephyr.h b/pw_assert_zephyr/public/pw_assert_zephyr/assert_zephyr.h
index 40c4e16ff..fb25a62e6 100644
--- a/pw_assert_zephyr/public/pw_assert_zephyr/assert_zephyr.h
+++ b/pw_assert_zephyr/public/pw_assert_zephyr/assert_zephyr.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -11,38 +11,8 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-
-// This override header merely points to the true backend, in this case the
-// basic one. The reason to redirect is to permit the use of multiple backends
-// (though only pw_assert/check.h can only point to 1 backend).
#pragma once
-#include <sys/__assert.h>
-
-#include "pw_assert/assert.h"
-
-#define PW_HANDLE_CRASH(...) \
- { \
- __ASSERT_MSG_INFO(__VA_ARGS__); \
- pw_assert_HandleFailure(); \
- }
-
-#define PW_HANDLE_ASSERT_FAILURE(condition_string, ...) \
- { \
- __ASSERT_MSG_INFO("Check failed: " condition_string ". " __VA_ARGS__); \
- __ASSERT_POST_ACTION(); \
- }
+#include <zephyr/sys/__assert.h>
-#define PW_HANDLE_ASSERT_BINARY_COMPARE_FAILURE(arg_a_str, \
- arg_a_val, \
- comparison_op_str, \
- arg_b_str, \
- arg_b_val, \
- type_fmt, \
- message, \
- ...) \
- PW_HANDLE_ASSERT_FAILURE(arg_a_str " (=" type_fmt ") " comparison_op_str \
- " " arg_b_str " (=" type_fmt ")", \
- message, \
- arg_a_val, \
- arg_b_val PW_COMMA_ARGS(__VA_ARGS__))
+#define PW_ASSERT_HANDLE_FAILURE(condition) __ASSERT(false, condition);
diff --git a/pw_assert_zephyr/public/pw_assert_zephyr/check_zephyr.h b/pw_assert_zephyr/public/pw_assert_zephyr/check_zephyr.h
new file mode 100644
index 000000000..380d9b0fa
--- /dev/null
+++ b/pw_assert_zephyr/public/pw_assert_zephyr/check_zephyr.h
@@ -0,0 +1,48 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <zephyr/sys/__assert.h>
+
+#include "pw_preprocessor/compiler.h"
+
+#define PW_HANDLE_CRASH(...) \
+ { \
+ __ASSERT_LOC("PW_CRASH()"); \
+ __ASSERT_MSG_INFO(__VA_ARGS__); \
+ __ASSERT_POST_ACTION(); \
+ PW_UNREACHABLE; \
+ }
+
+#define PW_HANDLE_ASSERT_FAILURE(condition_string, ...) \
+ { \
+ __ASSERT_LOC("PW_CHECK() or PW_DCHECK() failure"); \
+ __ASSERT_MSG_INFO("Check failed: " condition_string ". " __VA_ARGS__); \
+ __ASSERT_POST_ACTION(); \
+ PW_UNREACHABLE; \
+ }
+
+#define PW_HANDLE_ASSERT_BINARY_COMPARE_FAILURE(arg_a_str, \
+ arg_a_val, \
+ comparison_op_str, \
+ arg_b_str, \
+ arg_b_val, \
+ type_fmt, \
+ message, \
+ ...) \
+ PW_HANDLE_ASSERT_FAILURE(arg_a_str " (=" type_fmt ") " comparison_op_str \
+ " " arg_b_str " (=" type_fmt ")", \
+ message, \
+ arg_a_val, \
+ arg_b_val PW_COMMA_ARGS(__VA_ARGS__))
diff --git a/pw_assert_zephyr/public_overrides/pw_assert_backend/assert_backend.h b/pw_assert_zephyr/public_overrides/pw_assert_backend/assert_backend.h
index 63e5ab07b..ebff75429 100644
--- a/pw_assert_zephyr/public_overrides/pw_assert_backend/assert_backend.h
+++ b/pw_assert_zephyr/public_overrides/pw_assert_backend/assert_backend.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -11,10 +11,6 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-
-// This override header merely points to the true backend, in this case the
-// basic one. The reason to redirect is to permit the use of multiple backends
-// (though only pw_assert/check.h can only point to 1 backend).
#pragma once
#include "pw_assert_zephyr/assert_zephyr.h"
diff --git a/pw_assert_zephyr/public_overrides/pw_assert_backend/check_backend.h b/pw_assert_zephyr/public_overrides/pw_assert_backend/check_backend.h
new file mode 100644
index 000000000..bcb63d749
--- /dev/null
+++ b/pw_assert_zephyr/public_overrides/pw_assert_backend/check_backend.h
@@ -0,0 +1,16 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert_zephyr/check_zephyr.h"
diff --git a/pw_async/BUILD.bazel b/pw_async/BUILD.bazel
new file mode 100644
index 000000000..2ae446d6b
--- /dev/null
+++ b/pw_async/BUILD.bazel
@@ -0,0 +1,25 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+filegroup(
+ name = "pw_async_files",
+ srcs = [
+ "fake_dispatcher_test.cc",
+ "public/pw_async/dispatcher.h",
+ "public/pw_async/fake_dispatcher.h",
+ "public/pw_async/fake_dispatcher_fixture.h",
+ "public/pw_async/internal/types.h",
+ "public/pw_async/task.h",
+ ],
+)
diff --git a/pw_async/BUILD.gn b/pw_async/BUILD.gn
new file mode 100644
index 000000000..483b9b5e6
--- /dev/null
+++ b/pw_async/BUILD.gn
@@ -0,0 +1,94 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_async/async.gni")
+import("$dir_pw_async/backend.gni")
+import("$dir_pw_async/fake_dispatcher_fixture.gni")
+import("$dir_pw_async/fake_dispatcher_test.gni")
+import("$dir_pw_build/facade.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+}
+
+pw_source_set("dispatcher") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ "$dir_pw_chrono:system_clock",
+ dir_pw_function,
+ ]
+ public = [ "public/pw_async/dispatcher.h" ]
+ visibility = [
+ ":*",
+ "$dir_pw_async_basic:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+pw_facade("task") {
+ backend = pw_async_TASK_BACKEND
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ "$dir_pw_chrono:system_clock",
+ dir_pw_function,
+ dir_pw_status,
+ ]
+ public = [
+ "public/pw_async/internal/types.h",
+ "public/pw_async/task.h",
+ ]
+ visibility = [
+ ":*",
+ "$dir_pw_async_basic:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+pw_facade("fake_dispatcher") {
+ backend = pw_async_FAKE_DISPATCHER_BACKEND
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_async/fake_dispatcher.h" ]
+ public_deps = [ ":dispatcher" ]
+ visibility = [
+ ":*",
+ "$dir_pw_async_basic:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+fake_dispatcher_fixture("fake_dispatcher_fixture") {
+ backend = ":fake_dispatcher"
+ visibility = [
+ ":*",
+ "$dir_pw_async_basic:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+# Satisfy source_is_in_build_files presubmit step
+pw_source_set("fake_dispatcher_test") {
+ sources = [
+ "fake_dispatcher_test.cc",
+ "public/pw_async/fake_dispatcher_fixture.h",
+ ]
+ visibility = []
+}
diff --git a/pw_web_ui/OWNERS b/pw_async/README.md
index e69de29bb..e69de29bb 100644
--- a/pw_web_ui/OWNERS
+++ b/pw_async/README.md
diff --git a/pw_async/async.gni b/pw_async/async.gni
new file mode 100644
index 000000000..80f44834e
--- /dev/null
+++ b/pw_async/async.gni
@@ -0,0 +1,22 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+declare_args() {
+ # To depend on pw_async, add targets to this list.
+ #
+ # WARNING: This is experimental and *not* guaranteed to work.
+ pw_async_EXPERIMENTAL_MODULE_VISIBILITY = []
+}
diff --git a/pw_async/backend.gni b/pw_async/backend.gni
new file mode 100644
index 000000000..c05f33f58
--- /dev/null
+++ b/pw_async/backend.gni
@@ -0,0 +1,23 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+declare_args() {
+ # Configures the backend to use for the //pw_async:task facade.
+ pw_async_TASK_BACKEND = ""
+
+ # Configures the backend to use for the //pw_async:fake_dispatcher facade.
+ pw_async_FAKE_DISPATCHER_BACKEND = ""
+}
diff --git a/pw_async/docs.rst b/pw_async/docs.rst
new file mode 100644
index 000000000..e1a34e009
--- /dev/null
+++ b/pw_async/docs.rst
@@ -0,0 +1,174 @@
+.. _module-pw_async:
+
+================
+pw_async
+================
+
+--------
+Overview
+--------
+Pigweed's async module provides portable APIs and utilities for writing
+asynchronous code. Currently, it provides:
+
+- Message loop APIs
+
+.. attention::
+ This module is still under construction. The API is not yet stable.
+
+----------
+Dispatcher
+----------
+Dispatcher is an API for a message loop that schedules and executes Tasks. See
+:bdg-ref-primary-line:`module-pw_async_basic` for an example implementation.
+
+Dispatcher is a pure virtual interface that is implemented by backends and
+FakeDispatcher. A virtual interface is used instead of a facade to allow
+substituting a FakeDispatcher for a Dispatcher backend in tests.
+
+Dispatcher API
+==============
+.. doxygenclass:: pw::async::Dispatcher
+ :members:
+
+
+Task API
+==============
+.. doxygenclass:: pw::async::Task
+ :members:
+
+Facade API
+==========
+
+Task
+----
+The Task type represents a work item that is submitted to a Dispatcher. The Task
+facade enables Dispatcher backends to specify custom state and methods.
+
+The active Task backend is configured with the GN variable
+``pw_async_TASK_BACKEND``. The specified target must define a class
+``pw::async::backend::NativeTask`` in the header ``pw_async_backend/task.h``
+that meets the interface requirements in ``public/pw_async/task.h``. Task will
+then trivially wrap ``NativeTask``.
+
+FakeDispatcher
+--------------
+The FakeDispatcher facade is a utility for simulating a real Dispatcher
+in tests. FakeDispatcher simulates time to allow for reliable, fast testing of
+code that uses Dispatcher. FakeDispatcher is a facade instead of a concrete
+implementation because it depends on Task state for processing tasks, which
+varies across Task backends.
+
+The active Task backend is configured with the GN variable
+``pw_async_FAKE_DISPATCHER_BACKEND``. The specified target must define a class
+``pw::async::test::backend::NativeFakeDispatcher`` in the header
+``pw_async_backend/fake_dispatcher.h`` that meets the interface requirements in
+``public/pw_async/task.h``. FakeDispatcher will then trivially wrap
+``NativeFakeDispatcher``.
+
+Testing FakeDispatcher
+^^^^^^^^^^^^^^^^^^^^^^
+The GN template ``fake_dispatcher_tests`` in ``fake_dispatcher_tests.gni``
+creates a test target that tests a FakeDispatcher backend. This enables
+one test suite to be shared across FakeDispatcher backends and ensures
+conformance.
+
+Design
+======
+
+Task Ownership
+--------------
+Tasks are owned by clients rather than the Dispatcher. This avoids either
+memory allocation or queue size limits in Dispatcher implementations. However,
+care must be taken that clients do not destroy Tasks before they have been
+executed or canceled.
+
+Getting Started
+===============
+First, configure the Task backend for the Dispatcher backend you will be using:
+
+.. code-block::
+
+ pw_async_TASK_BACKEND = "$dir_pw_async_basic:task"
+
+
+Next, create an executable target that depends on the Dispatcher backend you
+want to use:
+
+.. code-block::
+
+ pw_executable("hello_world") {
+ sources = [ "main.cc" ]
+ deps = [ "$dir_pw_async_basic:dispatcher" ]
+ }
+
+Next, instantiate the Dispatcher and post a task:
+
+.. code-block:: cpp
+
+ #include "pw_async_basic/dispatcher.h"
+
+ int main() {
+ BasicDispatcher dispatcher;
+
+ // Spawn a thread for the dispatcher to run on.
+ thread::Thread work_thread(thread::stl::Options(), dispatcher);
+
+ Task task([](pw::async::Context& ctx){
+ printf("hello world\n");
+ ctx.dispatcher->RequestStop();
+ });
+
+ // Execute `task` in 5 seconds.
+ dispatcher.PostAfter(task, 5s);
+
+ // Blocks until `task` runs.
+ work_thread.join();
+ return 0;
+ }
+
+The above example runs the dispatcher on a new thread, but it can also run on
+the current/main thread:
+
+.. code-block:: cpp
+
+ #include "pw_async_basic/dispatcher.h"
+
+ int main() {
+ BasicDispatcher dispatcher;
+
+ Task task([](pw::async::Context& ctx){
+ printf("hello world\n");
+ });
+
+ // Execute `task` in 5 seconds.
+ dispatcher.PostAfter(task, 5s);
+
+ dispatcher.Run();
+ return 0;
+ }
+
+Fake Dispatcher
+===============
+To test async code, FakeDispatcher should be dependency injected in place of
+Dispatcher. Then, time should be driven in unit tests using the ``Run*()``
+methods. For convenience, you can use the test fixture
+FakeDispatcherFixture.
+
+.. doxygenclass:: pw::async::test::FakeDispatcherFixture
+ :members:
+
+.. attention::
+
+ ``FakeDispatcher::now()`` will return the simulated time.
+ ``Dispatcher::now()`` should therefore be used to get the current time in
+ async code instead of other sources of time to ensure consistent time values
+ and reliable tests.
+
+-------
+Roadmap
+-------
+- Stabilize Task cancellation API
+- Utility for dynamically allocated Tasks
+- Bazel support
+- CMake support
+- Support for C++20 coroutines
diff --git a/pw_async/fake_dispatcher_fixture.gni b/pw_async/fake_dispatcher_fixture.gni
new file mode 100644
index 000000000..a58be16ac
--- /dev/null
+++ b/pw_async/fake_dispatcher_fixture.gni
@@ -0,0 +1,38 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+# Creates a pw_source_set that provides a concrete FakeDispatcherFixture.
+#
+# Parameters
+#
+# backend (required)
+# [target] The FakeDispatcher backend to use.
+template("fake_dispatcher_fixture") {
+ assert(defined(invoker.backend))
+
+ pw_source_set(target_name) {
+ public = [ "$dir_pw_async/public/pw_async/fake_dispatcher_fixture.h" ]
+ public_deps = [
+ "$dir_pw_unit_test",
+ invoker.backend,
+ ]
+ forward_variables_from(invoker, [ "visibility" ])
+ }
+}
diff --git a/pw_async/fake_dispatcher_test.cc b/pw_async/fake_dispatcher_test.cc
new file mode 100644
index 000000000..3674f4cb0
--- /dev/null
+++ b/pw_async/fake_dispatcher_test.cc
@@ -0,0 +1,216 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_async/fake_dispatcher.h"
+
+#include "gtest/gtest.h"
+#include "pw_thread/thread.h"
+#include "pw_thread_stl/options.h"
+
+#define ASSERT_OK(status) ASSERT_EQ(OkStatus(), status)
+#define ASSERT_CANCELLED(status) ASSERT_EQ(Status::Cancelled(), status)
+
+using namespace std::chrono_literals;
+
+namespace pw::async::test {
+
+TEST(FakeDispatcher, PostTasks) {
+ FakeDispatcher dispatcher;
+
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++count;
+ };
+
+ Task task(inc_count);
+ dispatcher.Post(task);
+
+ Task task2(inc_count);
+ dispatcher.Post(task2);
+
+ Task task3(inc_count);
+ dispatcher.Post(task3);
+
+ // Should not run; RunUntilIdle() does not advance time.
+ Task task4([&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ });
+ dispatcher.PostAfter(task4, 1ms);
+
+ dispatcher.RunUntilIdle();
+ dispatcher.RequestStop();
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(count, 4);
+}
+
+// Lambdas can only capture one ptr worth of memory without allocating, so we
+// group the data we want to share between tasks and their containing tests
+// inside one struct.
+struct TaskPair {
+ Task task_a;
+ Task task_b;
+ int count = 0;
+};
+
+TEST(FakeDispatcher, DelayedTasks) {
+ FakeDispatcher dispatcher;
+ TaskPair tp;
+
+ Task task0([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ tp.count = tp.count * 10 + 4;
+ });
+ dispatcher.PostAfter(task0, 200ms);
+
+ Task task1([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ tp.count = tp.count * 10 + 1;
+ c.dispatcher->PostAfter(tp.task_a, 50ms);
+ c.dispatcher->PostAfter(tp.task_b, 25ms);
+ });
+ dispatcher.PostAfter(task1, 100ms);
+
+ tp.task_a.set_function([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ tp.count = tp.count * 10 + 3;
+ });
+ tp.task_b.set_function([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ tp.count = tp.count * 10 + 2;
+ });
+
+ dispatcher.RunFor(200ms);
+ dispatcher.RequestStop();
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(tp.count, 1234);
+}
+
+TEST(FakeDispatcher, CancelTasks) {
+ FakeDispatcher dispatcher;
+
+ auto shouldnt_run = []([[maybe_unused]] Context& c,
+ [[maybe_unused]] Status status) { FAIL(); };
+
+ TaskPair tp;
+ // This task gets canceled in cancel_task.
+ tp.task_a.set_function(shouldnt_run);
+ dispatcher.PostAfter(tp.task_a, 40ms);
+
+ // This task gets canceled immediately.
+ Task task1(shouldnt_run);
+ dispatcher.PostAfter(task1, 10ms);
+ ASSERT_TRUE(dispatcher.Cancel(task1));
+
+ // This task cancels the first task.
+ Task cancel_task([&tp](Context& c, Status status) {
+ ASSERT_OK(status);
+ ASSERT_TRUE(c.dispatcher->Cancel(tp.task_a));
+ ++tp.count;
+ });
+ dispatcher.PostAfter(cancel_task, 20ms);
+
+ dispatcher.RunFor(50ms);
+ dispatcher.RequestStop();
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(tp.count, 1);
+}
+
+// Test RequestStop() from inside task.
+TEST(FakeDispatcher, RequestStopInsideTask) {
+ FakeDispatcher dispatcher;
+
+ int count = 0;
+ auto cancelled_cb = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+
+ // These tasks are never executed and cleaned up in RequestStop().
+ Task task0(cancelled_cb), task1(cancelled_cb);
+ dispatcher.PostAfter(task0, 20ms);
+ dispatcher.PostAfter(task1, 21ms);
+
+ Task stop_task([&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++count;
+ static_cast<FakeDispatcher*>(c.dispatcher)->RequestStop();
+ static_cast<FakeDispatcher*>(c.dispatcher)->RunUntilIdle();
+ });
+ dispatcher.Post(stop_task);
+
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(count, 3);
+}
+
+TEST(FakeDispatcher, PeriodicTasks) {
+ FakeDispatcher dispatcher;
+
+ int count = 0;
+ Task periodic_task([&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++count;
+ });
+ dispatcher.PostPeriodicAt(periodic_task, 20ms, dispatcher.now() + 50ms);
+
+ // Cancel periodic task after it has run thrice, at +50ms, +70ms, and +90ms.
+ Task cancel_task([&periodic_task](Context& c, Status status) {
+ ASSERT_OK(status);
+ c.dispatcher->Cancel(periodic_task);
+ });
+ dispatcher.PostAfter(cancel_task, 100ms);
+
+ dispatcher.RunFor(300ms);
+ dispatcher.RequestStop();
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(count, 3);
+}
+
+TEST(FakeDispatcher, TasksCancelledByDispatcherDestructor) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ {
+ FakeDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+ }
+
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByRunFor) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ FakeDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+
+ dispatcher.RequestStop();
+ dispatcher.RunFor(5s);
+ ASSERT_EQ(count, 3);
+}
+
+} // namespace pw::async::test
diff --git a/pw_async/fake_dispatcher_test.gni b/pw_async/fake_dispatcher_test.gni
new file mode 100644
index 000000000..33a32ee33
--- /dev/null
+++ b/pw_async/fake_dispatcher_test.gni
@@ -0,0 +1,43 @@
+# Copyright 2019 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+# Creates a test target that tests a FakeDispatcher backend using a common
+# test suite.
+#
+# Parameters
+#
+# backend (required)
+# [target] The FakeDispatcher backend to test.
+template("fake_dispatcher_test") {
+ assert(defined(invoker.backend))
+
+ pw_test(target_name) {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
+ pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != "" &&
+ pw_thread_THREAD_BACKEND != ""
+ deps = [
+ "$dir_pw_sync:timed_thread_notification",
+ "$dir_pw_thread:thread",
+ dir_pw_log,
+ invoker.backend,
+ ]
+ sources = [ "$dir_pw_async/fake_dispatcher_test.cc" ]
+ }
+}
diff --git a/pw_async/public/pw_async/dispatcher.h b/pw_async/public/pw_async/dispatcher.h
new file mode 100644
index 000000000..8faaccd67
--- /dev/null
+++ b/pw_async/public/pw_async/dispatcher.h
@@ -0,0 +1,58 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_chrono/system_clock.h"
+
+namespace pw::async {
+
+class Task;
+
+/// Asynchronous Dispatcher abstract class. A default implementation is provided
+/// in pw_async_basic.
+///
+/// Dispatcher implements VirtualSystemClock so the Dispatcher's time can be
+/// injected into other modules under test. This is useful for consistently
+/// simulating time when using FakeDispatcher (rather than using
+/// chrono::SimulatedSystemClock separately).
+class Dispatcher : public chrono::VirtualSystemClock {
+ public:
+ ~Dispatcher() override = default;
+
+ /// Post caller owned |task|.
+ virtual void Post(Task& task) = 0;
+
+ /// Post caller owned |task| to be run after |delay|.
+ virtual void PostAfter(Task& task, chrono::SystemClock::duration delay) = 0;
+
+ /// Post caller owned |task| to be run at |time|.
+ virtual void PostAt(Task& task, chrono::SystemClock::time_point time) = 0;
+
+ /// Post caller owned |task| to be run immediately then rerun at a regular
+ /// |interval|.
+ virtual void PostPeriodic(Task& task,
+ chrono::SystemClock::duration interval) = 0;
+ /// Post caller owned |task| to be run at |time| then rerun at a regular
+ /// |interval|. |interval| must not be zero.
+ virtual void PostPeriodicAt(Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point time) = 0;
+
+ /// Returns true if |task| is succesfully canceled.
+ /// If cancelation fails, the task may be running or completed.
+ /// Periodic tasks may be posted once more after they are canceled.
+ virtual bool Cancel(Task& task) = 0;
+};
+
+} // namespace pw::async
diff --git a/pw_async/public/pw_async/fake_dispatcher.h b/pw_async/public/pw_async/fake_dispatcher.h
new file mode 100644
index 000000000..43931d032
--- /dev/null
+++ b/pw_async/public/pw_async/fake_dispatcher.h
@@ -0,0 +1,87 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_async/dispatcher.h"
+#include "pw_async_backend/fake_dispatcher.h" // nogncheck
+
+namespace pw::async::test {
+
+/// FakeDispatcher is a facade for an implementation of Dispatcher that is used
+/// in unit tests. FakeDispatcher uses simulated time. RunUntil() and RunFor()
+/// advance time immediately, and now() returns the current simulated time.
+///
+/// To support various Task backends, FakeDispatcher wraps a
+/// backend::NativeFakeDispatcher that implements standard FakeDispatcher
+/// behavior using backend::NativeTask objects.
+class FakeDispatcher final : public Dispatcher {
+ public:
+ FakeDispatcher() : native_dispatcher_(*this) {}
+
+ /// Execute all runnable tasks and return without advancing simulated time.
+ void RunUntilIdle() { native_dispatcher_.RunUntilIdle(); }
+
+ /// Run the dispatcher until Now() has reached `end_time`, executing all tasks
+ /// that come due before then.
+ void RunUntil(chrono::SystemClock::time_point end_time) {
+ native_dispatcher_.RunUntil(end_time);
+ }
+
+ /// Run the Dispatcher until `duration` has elapsed, executing all tasks that
+ /// come due in that period.
+ void RunFor(chrono::SystemClock::duration duration) {
+ native_dispatcher_.RunFor(duration);
+ }
+
+ /// Stop processing tasks. After calling RequestStop(), the next time the
+ /// Dispatcher is run, all waiting Tasks will be dequeued and their
+ /// TaskFunctions called with a PW_STATUS_CANCELLED status.
+ void RequestStop() { native_dispatcher_.RequestStop(); }
+
+ // Dispatcher overrides:
+ void Post(Task& task) override { native_dispatcher_.Post(task); }
+ void PostAfter(Task& task, chrono::SystemClock::duration delay) override {
+ native_dispatcher_.PostAfter(task, delay);
+ }
+ void PostAt(Task& task, chrono::SystemClock::time_point time) override {
+ native_dispatcher_.PostAt(task, time);
+ }
+ void PostPeriodic(Task& task,
+ chrono::SystemClock::duration interval) override {
+ native_dispatcher_.PostPeriodic(task, interval);
+ }
+ void PostPeriodicAt(Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point start_time) override {
+ native_dispatcher_.PostPeriodicAt(task, interval, start_time);
+ }
+ bool Cancel(Task& task) override { return native_dispatcher_.Cancel(task); }
+
+ // VirtualSystemClock overrides:
+ chrono::SystemClock::time_point now() override {
+ return native_dispatcher_.now();
+ }
+
+ // Returns the inner NativeFakeDispatcher containing backend-specific
+ // state/logic. Only non-portable code should call these methods!
+ backend::NativeFakeDispatcher& native_type() { return native_dispatcher_; }
+ const backend::NativeFakeDispatcher& native_type() const {
+ return native_dispatcher_;
+ }
+
+ private:
+ backend::NativeFakeDispatcher native_dispatcher_;
+};
+
+} // namespace pw::async::test
diff --git a/pw_async/public/pw_async/fake_dispatcher_fixture.h b/pw_async/public/pw_async/fake_dispatcher_fixture.h
new file mode 100644
index 000000000..b32efeb92
--- /dev/null
+++ b/pw_async/public/pw_async/fake_dispatcher_fixture.h
@@ -0,0 +1,66 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "gtest/gtest.h"
+#include "pw_async/fake_dispatcher.h"
+
+namespace pw::async::test {
+
+/// Test fixture that is a simple wrapper around a FakeDispatcher.
+///
+/// Example:
+/// @code{.cpp}
+/// using ExampleTest = pw::async::test::FakeDispatcherFixture;
+///
+/// TEST_F(ExampleTest, Example) {
+/// MyClass obj(dispatcher());
+///
+/// obj.ScheduleSomeTasks();
+/// RunUntilIdle();
+/// EXPECT_TRUE(some condition);
+///
+/// obj.ScheduleTaskToRunIn30Seconds();
+/// RunFor(30s);
+/// EXPECT_TRUE(task ran);
+/// }
+/// @endcode
+class FakeDispatcherFixture : public ::testing::Test {
+ public:
+ /// Returns the FakeDispatcher that should be used for dependency injection.
+ FakeDispatcher& dispatcher() { return dispatcher_; }
+
+ /// Returns the current fake time.
+ chrono::SystemClock::time_point now() { return dispatcher_.now(); }
+
+ /// Dispatches all tasks with due times up until `now()`.
+ void RunUntilIdle() { dispatcher_.RunUntilIdle(); }
+
+ /// Dispatches all tasks with due times up to `end_time`, progressively
+ /// advancing the fake clock.
+ void RunUntil(chrono::SystemClock::time_point end_time) {
+ dispatcher_.RunUntil(end_time);
+ }
+
+ /// Dispatches all tasks with due times up to `now() + duration`,
+ /// progressively advancing the fake clock.
+ void RunFor(chrono::SystemClock::duration duration) {
+ dispatcher_.RunFor(duration);
+ }
+
+ private:
+ FakeDispatcher dispatcher_;
+};
+
+} // namespace pw::async::test
diff --git a/pw_async/public/pw_async/internal/types.h b/pw_async/public/pw_async/internal/types.h
new file mode 100644
index 000000000..c8b7ea287
--- /dev/null
+++ b/pw_async/public/pw_async/internal/types.h
@@ -0,0 +1,45 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::async {
+
+class Dispatcher;
+class Task;
+
+struct Context {
+ Dispatcher* dispatcher;
+ Task* task;
+};
+
+// A TaskFunction is a unit of work that is wrapped by a Task and executed on a
+// Dispatcher.
+//
+// TaskFunctions take a `Context` as their first argument. Before executing a
+// Task, the Dispatcher sets the pointer to itself and to the Task in `Context`.
+//
+// TaskFunctions take a `Status` as their second argument. When a Task is
+// running as normal, |status| == PW_STATUS_OK. If a Task will not be able to
+// run as scheduled, the Dispatcher will still invoke the TaskFunction with
+// |status| == PW_STATUS_CANCELLED. This provides an opportunity to reclaim
+// resources held by the Task.
+//
+// A Task will not run as scheduled if, for example, it is still waiting when
+// the Dispatcher shuts down.
+using TaskFunction = Function<void(Context&, Status)>;
+
+} // namespace pw::async
diff --git a/pw_async/public/pw_async/task.h b/pw_async/public/pw_async/task.h
new file mode 100644
index 000000000..c1fa8e665
--- /dev/null
+++ b/pw_async/public/pw_async/task.h
@@ -0,0 +1,62 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <optional>
+
+#include "pw_async/internal/types.h"
+#include "pw_async_backend/task.h"
+
+namespace pw::async {
+
+namespace test {
+class FakeDispatcher;
+}
+
+/// A Task represents a unit of work (TaskFunction) that can be executed on a
+/// Dispatcher. To support various Dispatcher backends, it wraps a
+/// `backend::NativeTask`, which contains backend-specific state and methods.
+class Task final {
+ public:
+ /// The default constructor creates a Task without a function.
+ /// `set_function()` must be called before posting the Task.
+ Task() : native_type_(*this) {}
+
+ /// Constructs a Task that calls `f` when executed on a Dispatcher.
+ explicit Task(TaskFunction&& f) : native_type_(*this, std::move(f)) {}
+
+ Task(const Task&) = delete;
+ Task& operator=(const Task&) = delete;
+ Task(Task&&) = delete;
+ Task& operator=(Task&&) = delete;
+
+ /// Configure the TaskFunction after construction. This MUST NOT be called
+ /// while this Task is pending in a Dispatcher.
+ void set_function(TaskFunction&& f) {
+ native_type_.set_function(std::move(f));
+ }
+
+ /// Executes this task.
+ void operator()(Context& ctx, Status status) { native_type_(ctx, status); }
+
+ /// Returns the inner NativeTask containing backend-specific state. Only
+ /// Dispatcher backends or non-portable code should call these methods!
+ backend::NativeTask& native_type() { return native_type_; }
+ const backend::NativeTask& native_type() const { return native_type_; }
+
+ private:
+ backend::NativeTask native_type_;
+};
+
+} // namespace pw::async
diff --git a/pw_async_basic/BUILD.bazel b/pw_async_basic/BUILD.bazel
new file mode 100644
index 000000000..8a1fb3aca
--- /dev/null
+++ b/pw_async_basic/BUILD.bazel
@@ -0,0 +1,30 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+filegroup(
+ name = "pw_async_files",
+ srcs = [
+ "dispatcher.cc",
+ "dispatcher_test.cc",
+ "fake_dispatcher.cc",
+ "fake_dispatcher_fixture_test.cc",
+ "public/pw_async_basic/dispatcher.h",
+ "public/pw_async_basic/fake_dispatcher.h",
+ "public/pw_async_basic/task.h",
+ "public_overrides/pw_async_backend/fake_dispatcher.h",
+ "public_overrides/pw_async_backend/task.h",
+ "size_report/post_1_task.cc",
+ "size_report/task.cc",
+ ],
+)
diff --git a/pw_async_basic/BUILD.gn b/pw_async_basic/BUILD.gn
new file mode 100644
index 000000000..9ccce5e85
--- /dev/null
+++ b/pw_async_basic/BUILD.gn
@@ -0,0 +1,182 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_async/async.gni")
+import("$dir_pw_async/fake_dispatcher_fixture.gni")
+import("$dir_pw_async/fake_dispatcher_test.gni")
+import("$dir_pw_bloat/bloat.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+}
+
+config("backend_config") {
+ include_dirs = [ "public_overrides" ]
+ visibility = [ ":*" ]
+}
+
+# Backend for //pw_async:task
+pw_source_set("task") {
+ public_configs = [
+ ":public_include_path",
+ ":backend_config",
+ ]
+ public = [
+ "public/pw_async_basic/task.h",
+ "public_overrides/pw_async_backend/task.h",
+ ]
+
+ public_deps = [
+ "$dir_pw_async:task.facade",
+ "$dir_pw_containers:intrusive_list",
+ ]
+ visibility = [
+ ":*",
+ "$dir_pw_async:*",
+ "size_report:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+# Backend for //pw_async:fake_dispatcher
+pw_source_set("fake_dispatcher") {
+ public_configs = [
+ ":public_include_path",
+ ":backend_config",
+ ]
+ sources = [ "fake_dispatcher.cc" ]
+ public = [
+ "public/pw_async_basic/fake_dispatcher.h",
+ "public_overrides/pw_async_backend/fake_dispatcher.h",
+ ]
+ public_deps = [
+ ":task",
+ "$dir_pw_async:fake_dispatcher.facade",
+ ]
+ deps = [ dir_pw_log ]
+ visibility = [
+ ":*",
+ "$dir_pw_async:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+fake_dispatcher_test("fake_dispatcher_test") {
+ backend = ":fake_dispatcher"
+}
+
+fake_dispatcher_fixture("fake_dispatcher_fixture") {
+ backend = ":fake_dispatcher"
+}
+
+pw_test("fake_dispatcher_fixture_test") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ sources = [ "fake_dispatcher_fixture_test.cc" ]
+ deps = [ ":fake_dispatcher_fixture" ]
+}
+
+pw_source_set("dispatcher") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_async_basic/dispatcher.h" ]
+ sources = [ "dispatcher.cc" ]
+ public_deps = [
+ ":task",
+ "$dir_pw_async:dispatcher",
+ "$dir_pw_containers:intrusive_list",
+ "$dir_pw_sync:interrupt_spin_lock",
+ "$dir_pw_sync:timed_thread_notification",
+ "$dir_pw_thread:thread_core",
+ dir_pw_log,
+ ]
+ visibility = [
+ ":*",
+ "size_report:*",
+ ] + pw_async_EXPERIMENTAL_MODULE_VISIBILITY
+}
+
+pw_test("dispatcher_test") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
+ pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ public_deps = [
+ ":dispatcher",
+ "$dir_pw_thread:thread",
+ dir_pw_log,
+ ]
+ sources = [ "dispatcher_test.cc" ]
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":dispatcher_test",
+ ":fake_dispatcher_test",
+ ":fake_dispatcher_fixture_test",
+ ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+ report_deps = [ ":docs_size_report" ]
+}
+
+pw_size_diff("docs_size_report") {
+ title = "pw_async_basic Docs Size Report"
+ base = "$dir_pw_bloat:bloat_base"
+
+ binaries = []
+
+ if (pw_chrono_SYSTEM_CLOCK_BACKEND != "") {
+ binaries += [
+ {
+ target = "size_report:task"
+ label = "Construct a Task"
+ },
+ ]
+ }
+
+ if (binaries == []) {
+ binaries += [
+ {
+ target = "$dir_pw_bloat:bloat_base"
+ label = "No backend is selected."
+ },
+ ]
+ }
+}
+
+# This size report can't be included in docs because the docs build target uses
+# the target-stm32f429i-disc1 toolchain, which does not support timed thread
+# notifications (mutex & condition_variable headers are not available).
+pw_size_diff("size_report") {
+ title = "pw_async_basic Size Report"
+ base = "$dir_pw_bloat:bloat_base"
+
+ binaries = []
+
+ if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
+ pw_sync_THREAD_NOTIFICATION_BACKEND != "" &&
+ pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != "") {
+ binaries += [
+ {
+ target = "size_report:post_1_task"
+ label = "Post 1 Task to BasicDispatcher"
+ },
+ ]
+ }
+}
diff --git a/pw_async_basic/README.md b/pw_async_basic/README.md
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_async_basic/README.md
diff --git a/pw_async_basic/dispatcher.cc b/pw_async_basic/dispatcher.cc
new file mode 100644
index 000000000..682ff4f6d
--- /dev/null
+++ b/pw_async_basic/dispatcher.cc
@@ -0,0 +1,169 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_async_basic/dispatcher.h"
+
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_log/log.h"
+
+using namespace std::chrono_literals;
+
+namespace pw::async {
+
+const chrono::SystemClock::duration SLEEP_DURATION = 5s;
+
+BasicDispatcher::~BasicDispatcher() {
+ RequestStop();
+ lock_.lock();
+ DrainTaskQueue();
+ lock_.unlock();
+}
+
+void BasicDispatcher::Run() {
+ lock_.lock();
+ while (!stop_requested_) {
+ MaybeSleep();
+ ExecuteDueTasks();
+ }
+ DrainTaskQueue();
+ lock_.unlock();
+}
+
+void BasicDispatcher::RunUntilIdle() {
+ lock_.lock();
+ ExecuteDueTasks();
+ if (stop_requested_) {
+ DrainTaskQueue();
+ }
+ lock_.unlock();
+}
+
+void BasicDispatcher::RunUntil(chrono::SystemClock::time_point end_time) {
+ lock_.lock();
+ while (end_time < now() && !stop_requested_) {
+ MaybeSleep();
+ ExecuteDueTasks();
+ }
+ if (stop_requested_) {
+ DrainTaskQueue();
+ }
+ lock_.unlock();
+}
+
+void BasicDispatcher::RunFor(chrono::SystemClock::duration duration) {
+ RunUntil(now() + duration);
+}
+
+void BasicDispatcher::MaybeSleep() {
+ if (task_queue_.empty() || task_queue_.front().due_time_ > now()) {
+ // Sleep until a notification is received or until the due time of the
+ // next task. Notifications are sent when tasks are posted or 'stop' is
+ // requested.
+ chrono::SystemClock::time_point wake_time =
+ task_queue_.empty() ? now() + SLEEP_DURATION
+ : task_queue_.front().due_time_;
+
+ lock_.unlock();
+ PW_LOG_DEBUG("no task due; waiting for signal");
+ timed_notification_.try_acquire_until(wake_time);
+ lock_.lock();
+ }
+}
+
+void BasicDispatcher::ExecuteDueTasks() {
+ while (!task_queue_.empty() && task_queue_.front().due_time_ <= now() &&
+ !stop_requested_) {
+ backend::NativeTask& task = task_queue_.front();
+ task_queue_.pop_front();
+
+ if (task.interval().has_value()) {
+ PostTaskInternal(task, task.due_time_ + task.interval().value());
+ }
+
+ lock_.unlock();
+ PW_LOG_DEBUG("running task");
+ Context ctx{this, &task.task_};
+ task(ctx, OkStatus());
+ lock_.lock();
+ }
+}
+
+void BasicDispatcher::RequestStop() {
+ std::lock_guard lock(lock_);
+ PW_LOG_DEBUG("stop requested");
+ stop_requested_ = true;
+ timed_notification_.release();
+}
+
+void BasicDispatcher::DrainTaskQueue() {
+ PW_LOG_DEBUG("draining task queue");
+ while (!task_queue_.empty()) {
+ backend::NativeTask& task = task_queue_.front();
+ task_queue_.pop_front();
+
+ lock_.unlock();
+ PW_LOG_DEBUG("running cancelled task");
+ Context ctx{this, &task.task_};
+ task(ctx, Status::Cancelled());
+ lock_.lock();
+ }
+}
+
+void BasicDispatcher::Post(Task& task) { PostAt(task, now()); }
+
+void BasicDispatcher::PostAfter(Task& task,
+ chrono::SystemClock::duration delay) {
+ PostAt(task, now() + delay);
+}
+
+void BasicDispatcher::PostAt(Task& task, chrono::SystemClock::time_point time) {
+ lock_.lock();
+ PW_LOG_DEBUG("posting task");
+ PostTaskInternal(task.native_type(), time);
+ lock_.unlock();
+}
+
+void BasicDispatcher::PostPeriodic(Task& task,
+ chrono::SystemClock::duration interval) {
+ PostPeriodicAt(task, interval, now());
+}
+
+void BasicDispatcher::PostPeriodicAt(
+ Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point start_time) {
+ PW_DCHECK(interval != chrono::SystemClock::duration::zero());
+ task.native_type().set_interval(interval);
+ PostAt(task, start_time);
+}
+
+bool BasicDispatcher::Cancel(Task& task) {
+ std::lock_guard lock(lock_);
+ return task_queue_.remove(task.native_type());
+}
+
+void BasicDispatcher::PostTaskInternal(
+ backend::NativeTask& task, chrono::SystemClock::time_point time_due) {
+ task.due_time_ = time_due;
+ auto it_front = task_queue_.begin();
+ auto it_behind = task_queue_.before_begin();
+ while (it_front != task_queue_.end() && time_due > it_front->due_time_) {
+ ++it_front;
+ ++it_behind;
+ }
+ task_queue_.insert_after(it_behind, task);
+ timed_notification_.release();
+}
+
+} // namespace pw::async
diff --git a/pw_async_basic/dispatcher_test.cc b/pw_async_basic/dispatcher_test.cc
new file mode 100644
index 000000000..3f72bbb4f
--- /dev/null
+++ b/pw_async_basic/dispatcher_test.cc
@@ -0,0 +1,200 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_async_basic/dispatcher.h"
+
+#include "gtest/gtest.h"
+#include "pw_log/log.h"
+#include "pw_sync/thread_notification.h"
+#include "pw_thread/thread.h"
+#include "pw_thread_stl/options.h"
+
+#define ASSERT_OK(status) ASSERT_EQ(OkStatus(), status)
+#define ASSERT_CANCELLED(status) ASSERT_EQ(Status::Cancelled(), status)
+
+using namespace std::chrono_literals;
+
+namespace pw::async {
+
+// Lambdas can only capture one ptr worth of memory without allocating, so we
+// group the data we want to share between tasks and their containing tests
+// inside one struct.
+struct TestPrimitives {
+ int count = 0;
+ sync::ThreadNotification notification;
+};
+
+TEST(DispatcherBasic, PostTasks) {
+ BasicDispatcher dispatcher;
+ thread::Thread work_thread(thread::stl::Options(), dispatcher);
+
+ TestPrimitives tp;
+ auto inc_count = [&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++tp.count;
+ };
+
+ Task task(inc_count);
+ dispatcher.Post(task);
+
+ Task task2(inc_count);
+ dispatcher.Post(task2);
+
+ Task task3([&tp]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++tp.count;
+ tp.notification.release();
+ });
+ dispatcher.Post(task3);
+
+ tp.notification.acquire();
+ dispatcher.RequestStop();
+ work_thread.join();
+ ASSERT_EQ(tp.count, 3);
+}
+
+struct TaskPair {
+ Task task_a;
+ Task task_b;
+ int count = 0;
+ sync::ThreadNotification notification;
+};
+
+TEST(DispatcherBasic, ChainedTasks) {
+ BasicDispatcher dispatcher;
+ thread::Thread work_thread(thread::stl::Options(), dispatcher);
+
+ sync::ThreadNotification notification;
+ Task task1([&notification]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ notification.release();
+ });
+
+ Task task2([&task1](Context& c, Status status) {
+ ASSERT_OK(status);
+ c.dispatcher->Post(task1);
+ });
+
+ Task task3([&task2](Context& c, Status status) {
+ ASSERT_OK(status);
+ c.dispatcher->Post(task2);
+ });
+ dispatcher.Post(task3);
+
+ notification.acquire();
+ dispatcher.RequestStop();
+ work_thread.join();
+}
+
+// Test RequestStop() from inside task.
+TEST(DispatcherBasic, RequestStopInsideTask) {
+ BasicDispatcher dispatcher;
+ thread::Thread work_thread(thread::stl::Options(), dispatcher);
+
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+
+ // These tasks are never executed and cleaned up in RequestStop().
+ Task task0(inc_count), task1(inc_count);
+ dispatcher.PostAfter(task0, 20ms);
+ dispatcher.PostAfter(task1, 21ms);
+
+ Task stop_task([&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_OK(status);
+ ++count;
+ static_cast<BasicDispatcher*>(c.dispatcher)->RequestStop();
+ });
+ dispatcher.Post(stop_task);
+
+ work_thread.join();
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByRequestStopInDifferentThread) {
+ BasicDispatcher dispatcher;
+ thread::Thread work_thread(thread::stl::Options(), dispatcher);
+
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+
+ dispatcher.RequestStop();
+ work_thread.join();
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByDispatcherDestructor) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ {
+ BasicDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+ }
+
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByRunUntilIdle) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ BasicDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+
+ dispatcher.RequestStop();
+ dispatcher.RunUntilIdle();
+ ASSERT_EQ(count, 3);
+}
+
+TEST(DispatcherBasic, TasksCancelledByRunFor) {
+ int count = 0;
+ auto inc_count = [&count]([[maybe_unused]] Context& c, Status status) {
+ ASSERT_CANCELLED(status);
+ ++count;
+ };
+ Task task0(inc_count), task1(inc_count), task2(inc_count);
+
+ BasicDispatcher dispatcher;
+ dispatcher.PostAfter(task0, 10s);
+ dispatcher.PostAfter(task1, 10s);
+ dispatcher.PostAfter(task2, 10s);
+
+ dispatcher.RequestStop();
+ dispatcher.RunFor(5s);
+ ASSERT_EQ(count, 3);
+}
+
+} // namespace pw::async
diff --git a/pw_async_basic/docs.rst b/pw_async_basic/docs.rst
new file mode 100644
index 000000000..0363a2683
--- /dev/null
+++ b/pw_async_basic/docs.rst
@@ -0,0 +1,60 @@
+.. _module-pw_async_basic:
+
+================
+pw_async_basic
+================
+
+This module includes basic implementations of pw_async's Dispatcher and
+FakeDispatcher.
+
+---
+API
+---
+.. doxygenclass:: pw::async::BasicDispatcher
+ :members:
+
+-----
+Usage
+-----
+First, set the following GN variables:
+
+.. code-block::
+
+ pw_async_TASK_BACKEND="$dir_pw_async_basic:task"
+ pw_async_FAKE_DISPATCHER_BACKEND="$dir_pw_async_basic:fake_dispatcher"
+
+
+Next, create a target that depends on ``//pw_async_basic:dispatcher``:
+
+.. code-block::
+
+ pw_executable("hello_world") {
+ sources = [ "hello_world.cc" ]
+ deps = [
+ "//pw_async_basic:dispatcher",
+ ]
+ }
+
+Next, construct and use a ``BasicDispatcher``.
+
+.. code-block:: cpp
+
+ #include "pw_async_basic/dispatcher.h"
+
+ void DelayedPrint(pw::async::Dispatcher& dispatcher) {
+ dispatcher.PostAfter([](auto&){
+ printf("hello world\n");
+ }, 5s);
+ }
+
+ int main() {
+ pw::async::BasicDispatcher dispatcher;
+ DelayedPrint(dispatcher);
+ dispatcher.RunFor(10s);
+ return 0;
+ }
+
+-----------
+Size Report
+-----------
+.. include:: docs_size_report
diff --git a/pw_async_basic/fake_dispatcher.cc b/pw_async_basic/fake_dispatcher.cc
new file mode 100644
index 000000000..cbba89e98
--- /dev/null
+++ b/pw_async_basic/fake_dispatcher.cc
@@ -0,0 +1,134 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_async/fake_dispatcher.h"
+
+#include "pw_async/task.h"
+#include "pw_log/log.h"
+
+using namespace std::chrono_literals;
+
+namespace pw::async::test::backend {
+
+NativeFakeDispatcher::NativeFakeDispatcher(Dispatcher& dispatcher)
+ : dispatcher_(dispatcher) {}
+
+NativeFakeDispatcher::~NativeFakeDispatcher() {
+ RequestStop();
+ DrainTaskQueue();
+}
+
+void NativeFakeDispatcher::RunUntilIdle() {
+ ExecuteDueTasks();
+ if (stop_requested_) {
+ DrainTaskQueue();
+ }
+}
+
+void NativeFakeDispatcher::RunUntil(chrono::SystemClock::time_point end_time) {
+ while (!task_queue_.empty() && task_queue_.front().due_time() <= end_time &&
+ !stop_requested_) {
+ now_ = task_queue_.front().due_time();
+ ExecuteDueTasks();
+ }
+
+ if (stop_requested_) {
+ DrainTaskQueue();
+ return;
+ }
+
+ if (now_ < end_time) {
+ now_ = end_time;
+ }
+}
+
+void NativeFakeDispatcher::RunFor(chrono::SystemClock::duration duration) {
+ RunUntil(now() + duration);
+}
+
+void NativeFakeDispatcher::ExecuteDueTasks() {
+ while (!task_queue_.empty() && task_queue_.front().due_time() <= now() &&
+ !stop_requested_) {
+ ::pw::async::backend::NativeTask& task = task_queue_.front();
+ task_queue_.pop_front();
+
+ if (task.interval().has_value()) {
+ PostTaskInternal(task, task.due_time() + task.interval().value());
+ }
+
+ Context ctx{&dispatcher_, &task.task_};
+ task(ctx, OkStatus());
+ }
+}
+
+void NativeFakeDispatcher::RequestStop() {
+ PW_LOG_DEBUG("stop requested");
+ stop_requested_ = true;
+}
+
+void NativeFakeDispatcher::DrainTaskQueue() {
+ while (!task_queue_.empty()) {
+ ::pw::async::backend::NativeTask& task = task_queue_.front();
+ task_queue_.pop_front();
+
+ PW_LOG_DEBUG("running cancelled task");
+ Context ctx{&dispatcher_, &task.task_};
+ task(ctx, Status::Cancelled());
+ }
+}
+
+void NativeFakeDispatcher::Post(Task& task) { PostAt(task, now()); }
+
+void NativeFakeDispatcher::PostAfter(Task& task,
+ chrono::SystemClock::duration delay) {
+ PostAt(task, now() + delay);
+}
+
+void NativeFakeDispatcher::PostAt(Task& task,
+ chrono::SystemClock::time_point time) {
+ PW_LOG_DEBUG("posting task");
+ PostTaskInternal(task.native_type(), time);
+}
+
+void NativeFakeDispatcher::PostPeriodic(
+ Task& task, chrono::SystemClock::duration interval) {
+ PostPeriodicAt(task, interval, now());
+}
+
+void NativeFakeDispatcher::PostPeriodicAt(
+ Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point start_time) {
+ task.native_type().set_interval(interval);
+ PostAt(task, start_time);
+}
+
+bool NativeFakeDispatcher::Cancel(Task& task) {
+ return task_queue_.remove(task.native_type());
+}
+
+void NativeFakeDispatcher::PostTaskInternal(
+ ::pw::async::backend::NativeTask& task,
+ chrono::SystemClock::time_point time_due) {
+ task.set_due_time(time_due);
+ auto it_front = task_queue_.begin();
+ auto it_behind = task_queue_.before_begin();
+ while (it_front != task_queue_.end() && time_due > it_front->due_time()) {
+ ++it_front;
+ ++it_behind;
+ }
+ task_queue_.insert_after(it_behind, task);
+}
+
+} // namespace pw::async::test::backend
diff --git a/pw_async_basic/fake_dispatcher_fixture_test.cc b/pw_async_basic/fake_dispatcher_fixture_test.cc
new file mode 100644
index 000000000..5262c3438
--- /dev/null
+++ b/pw_async_basic/fake_dispatcher_fixture_test.cc
@@ -0,0 +1,36 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_async/fake_dispatcher_fixture.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::async {
+namespace {
+
+using FakeDispatcherFixture = test::FakeDispatcherFixture;
+
+TEST_F(FakeDispatcherFixture, PostTasks) {
+ int count = 0;
+ auto inc_count = [&count](Context& /*c*/, Status /*status*/) { ++count; };
+
+ Task task(inc_count);
+ dispatcher().Post(task);
+
+ ASSERT_EQ(count, 0);
+ RunUntilIdle();
+ ASSERT_EQ(count, 1);
+}
+
+} // namespace
+} // namespace pw::async
diff --git a/pw_async_basic/public/pw_async_basic/dispatcher.h b/pw_async_basic/public/pw_async_basic/dispatcher.h
new file mode 100644
index 000000000..7050d6622
--- /dev/null
+++ b/pw_async_basic/public/pw_async_basic/dispatcher.h
@@ -0,0 +1,96 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_async/dispatcher.h"
+#include "pw_async/task.h"
+#include "pw_sync/interrupt_spin_lock.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/timed_thread_notification.h"
+#include "pw_thread/thread_core.h"
+
+namespace pw::async {
+
+/// BasicDispatcher is a generic implementation of Dispatcher.
+class BasicDispatcher final : public Dispatcher, public thread::ThreadCore {
+ public:
+ explicit BasicDispatcher() = default;
+ ~BasicDispatcher() override;
+
+ /// Execute all runnable tasks and return without waiting.
+ void RunUntilIdle();
+
+ /// Run the dispatcher until Now() has reached `end_time`, executing all tasks
+ /// that come due before then.
+ void RunUntil(chrono::SystemClock::time_point end_time);
+
+ /// Run the dispatcher until `duration` has elapsed, executing all tasks that
+ /// come due in that period.
+ void RunFor(chrono::SystemClock::duration duration);
+
+ /// Stop processing tasks. If the dispatcher is serving a task loop, break out
+ /// of the loop, dequeue all waiting tasks, and call their TaskFunctions with
+ /// a PW_STATUS_CANCELLED status. If no task loop is being served, execute the
+ /// dequeueing procedure the next time the Dispatcher is run.
+ void RequestStop() PW_LOCKS_EXCLUDED(lock_);
+
+ // ThreadCore overrides:
+
+ /// Run the dispatcher until RequestStop() is called. Overrides
+ /// ThreadCore::Run() so that BasicDispatcher is compatible with
+ /// pw::thread::Thread.
+ void Run() override PW_LOCKS_EXCLUDED(lock_);
+
+ // Dispatcher overrides:
+ void Post(Task& task) override;
+ void PostAfter(Task& task, chrono::SystemClock::duration delay) override;
+ void PostAt(Task& task, chrono::SystemClock::time_point time) override;
+ void PostPeriodic(Task& task,
+ chrono::SystemClock::duration interval) override;
+ void PostPeriodicAt(Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point start_time) override;
+ bool Cancel(Task& task) override PW_LOCKS_EXCLUDED(lock_);
+
+ // VirtualSystemClock overrides:
+ chrono::SystemClock::time_point now() override {
+ return chrono::SystemClock::now();
+ }
+
+ private:
+ // Insert |task| into task_queue_ maintaining its min-heap property, keyed by
+ // |time_due|.
+ void PostTaskInternal(backend::NativeTask& task,
+ chrono::SystemClock::time_point time_due)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+
+ // If no tasks are due, sleep until a notification is received, the next task
+ // comes due, or a timeout elapses; whichever occurs first.
+ void MaybeSleep() PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+
+ // Dequeue and run each task that is due.
+ void ExecuteDueTasks() PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+
+ // Dequeue each task and call each TaskFunction with a PW_STATUS_CANCELLED
+ // status.
+ void DrainTaskQueue() PW_EXCLUSIVE_LOCKS_REQUIRED(lock_);
+
+ sync::InterruptSpinLock lock_;
+ sync::TimedThreadNotification timed_notification_;
+ bool stop_requested_ PW_GUARDED_BY(lock_) = false;
+ // A priority queue of scheduled Tasks sorted by earliest due times first.
+ IntrusiveList<backend::NativeTask> task_queue_ PW_GUARDED_BY(lock_);
+};
+
+} // namespace pw::async
diff --git a/pw_async_basic/public/pw_async_basic/fake_dispatcher.h b/pw_async_basic/public/pw_async_basic/fake_dispatcher.h
new file mode 100644
index 000000000..96fd7aae8
--- /dev/null
+++ b/pw_async_basic/public/pw_async_basic/fake_dispatcher.h
@@ -0,0 +1,73 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_async/dispatcher.h"
+#include "pw_async/task.h"
+#include "pw_containers/intrusive_list.h"
+
+namespace pw::async::test::backend {
+
+class NativeFakeDispatcher final {
+ public:
+ explicit NativeFakeDispatcher(Dispatcher& test_dispatcher);
+ ~NativeFakeDispatcher();
+
+ void RequestStop();
+
+ void Post(Task& task);
+
+ void PostAfter(Task& task, chrono::SystemClock::duration delay);
+
+ void PostAt(Task& task, chrono::SystemClock::time_point time);
+
+ void PostPeriodic(Task& task, chrono::SystemClock::duration interval);
+ void PostPeriodicAt(Task& task,
+ chrono::SystemClock::duration interval,
+ chrono::SystemClock::time_point start_time);
+
+ bool Cancel(Task& task);
+
+ void RunUntilIdle();
+
+ void RunUntil(chrono::SystemClock::time_point end_time);
+
+ void RunFor(chrono::SystemClock::duration duration);
+
+ chrono::SystemClock::time_point now() { return now_; }
+
+ private:
+ // Insert |task| into task_queue_ maintaining its min-heap property, keyed by
+ // |time_due|.
+ void PostTaskInternal(::pw::async::backend::NativeTask& task,
+ chrono::SystemClock::time_point time_due);
+
+ // Dequeue and run each task that is due.
+ void ExecuteDueTasks();
+
+ // Dequeue each task and run each TaskFunction with a PW_STATUS_CANCELLED
+ // status.
+ void DrainTaskQueue();
+
+ Dispatcher& dispatcher_;
+ bool stop_requested_ = false;
+
+ // A priority queue of scheduled tasks sorted by earliest due times first.
+ IntrusiveList<::pw::async::backend::NativeTask> task_queue_;
+
+ // Tracks the current time as viewed by the test dispatcher.
+ chrono::SystemClock::time_point now_;
+};
+
+} // namespace pw::async::test::backend
diff --git a/pw_async_basic/public/pw_async_basic/task.h b/pw_async_basic/public/pw_async_basic/task.h
new file mode 100644
index 000000000..d83ea34ab
--- /dev/null
+++ b/pw_async_basic/public/pw_async_basic/task.h
@@ -0,0 +1,74 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_async/internal/types.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_containers/intrusive_list.h"
+
+namespace pw::async {
+class BasicDispatcher;
+namespace test::backend {
+class NativeFakeDispatcher;
+}
+} // namespace pw::async
+
+namespace pw::async::backend {
+
+// Task backend for BasicDispatcher.
+class NativeTask final : public IntrusiveList<NativeTask>::Item {
+ private:
+ friend class ::pw::async::Task;
+ friend class ::pw::async::BasicDispatcher;
+ friend class ::pw::async::test::backend::NativeFakeDispatcher;
+
+ NativeTask(::pw::async::Task& task) : task_(task) {}
+ explicit NativeTask(::pw::async::Task& task, TaskFunction&& f)
+ : func_(std::move(f)), task_(task) {}
+ void operator()(Context& ctx, Status status) { func_(ctx, status); }
+ void set_function(TaskFunction&& f) { func_ = std::move(f); }
+
+ pw::chrono::SystemClock::time_point due_time() const { return due_time_; }
+ void set_due_time(chrono::SystemClock::time_point due_time) {
+ due_time_ = due_time;
+ }
+ std::optional<chrono::SystemClock::duration> interval() const {
+ if (interval_ == chrono::SystemClock::duration::zero()) {
+ return std::nullopt;
+ }
+ return interval_;
+ }
+ void set_interval(std::optional<chrono::SystemClock::duration> interval) {
+ if (!interval.has_value()) {
+ interval_ = chrono::SystemClock::duration::zero();
+ return;
+ }
+ interval_ = *interval;
+ }
+
+ TaskFunction func_ = nullptr;
+ // task_ is placed after func_ to take advantage of the padding that would
+ // otherwise be added here. On 32-bit systems, func_ and due_time_ have an
+ // alignment of 8, but func_ has a size of 12 by default. Thus, 4 bytes of
+ // padding would be added here, which is just enough for a pointer.
+ Task& task_;
+ pw::chrono::SystemClock::time_point due_time_;
+ // A duration of 0 indicates that the task is not periodic.
+ chrono::SystemClock::duration interval_ =
+ chrono::SystemClock::duration::zero();
+};
+
+using NativeTaskHandle = NativeTask&;
+
+} // namespace pw::async::backend
diff --git a/pw_async_basic/public_overrides/pw_async_backend/fake_dispatcher.h b/pw_async_basic/public_overrides/pw_async_backend/fake_dispatcher.h
new file mode 100644
index 000000000..b042422b1
--- /dev/null
+++ b/pw_async_basic/public_overrides/pw_async_backend/fake_dispatcher.h
@@ -0,0 +1,15 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+#include "pw_async_basic/fake_dispatcher.h"
diff --git a/pw_async_basic/public_overrides/pw_async_backend/task.h b/pw_async_basic/public_overrides/pw_async_backend/task.h
new file mode 100644
index 000000000..727a39f37
--- /dev/null
+++ b/pw_async_basic/public_overrides/pw_async_backend/task.h
@@ -0,0 +1,15 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+#include "pw_async_basic/task.h"
diff --git a/pw_async_basic/size_report/BUILD.gn b/pw_async_basic/size_report/BUILD.gn
new file mode 100644
index 000000000..a40fc6f4d
--- /dev/null
+++ b/pw_async_basic/size_report/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_executable("post_1_task") {
+ sources = [ "post_1_task.cc" ]
+ deps = [
+ "$dir_pw_async_basic:dispatcher",
+ "$dir_pw_bloat:bloat_this_binary",
+ ]
+}
+
+pw_executable("task") {
+ sources = [ "task.cc" ]
+ deps = [
+ "$dir_pw_async_basic:task",
+ "$dir_pw_bloat:bloat_this_binary",
+ ]
+}
diff --git a/pw_async_basic/size_report/post_1_task.cc b/pw_async_basic/size_report/post_1_task.cc
new file mode 100644
index 000000000..ef6065b1c
--- /dev/null
+++ b/pw_async_basic/size_report/post_1_task.cc
@@ -0,0 +1,27 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_async_basic/dispatcher.h"
+#include "pw_bloat/bloat_this_binary.h"
+
+int main() {
+ pw::bloat::BloatThisBinary();
+
+ pw::async::BasicDispatcher dispatcher;
+ pw::async::Task task(
+ [](pw::async::Context& /*ctx*/) { printf("hello world\n"); });
+ dispatcher.Post(task);
+ dispatcher.Run();
+ return 0;
+}
diff --git a/pw_async_basic/size_report/task.cc b/pw_async_basic/size_report/task.cc
new file mode 100644
index 000000000..d149346fe
--- /dev/null
+++ b/pw_async_basic/size_report/task.cc
@@ -0,0 +1,23 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_async/task.h"
+
+#include "pw_bloat/bloat_this_binary.h"
+
+int main() {
+ pw::bloat::BloatThisBinary();
+ pw::async::Task task([](pw::async::Context& /*ctx*/) {});
+ return 0;
+}
diff --git a/pw_base64/BUILD.bazel b/pw_base64/BUILD.bazel
index 20c7b0d28..f9b0a75a7 100644
--- a/pw_base64/BUILD.bazel
+++ b/pw_base64/BUILD.bazel
@@ -32,7 +32,9 @@ pw_cc_library(
],
includes = ["public"],
deps = [
+ "//pw_assert",
"//pw_span",
+ "//pw_string:string",
],
)
diff --git a/pw_base64/BUILD.gn b/pw_base64/BUILD.gn
index 07d9674fc..6fa100756 100644
--- a/pw_base64/BUILD.gn
+++ b/pw_base64/BUILD.gn
@@ -25,6 +25,10 @@ config("default_config") {
pw_source_set("pw_base64") {
public_configs = [ ":default_config" ]
public = [ "public/pw_base64/base64.h" ]
+ public_deps = [
+ "$dir_pw_string:string",
+ dir_pw_span,
+ ]
sources = [ "base64.cc" ]
}
diff --git a/pw_base64/CMakeLists.txt b/pw_base64/CMakeLists.txt
index 69f99f800..cf87f56b3 100644
--- a/pw_base64/CMakeLists.txt
+++ b/pw_base64/CMakeLists.txt
@@ -14,7 +14,25 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_auto_add_simple_module(pw_base64
+pw_add_library(pw_base64 STATIC
+ HEADERS
+ public/pw_base64/base64.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_span
+ pw_string.string
+ SOURCES
+ base64.cc
+)
+
+pw_add_test(pw_base64.base64_test
+ SOURCES
+ base64_test.cc
+ base64_test_c.c
+ PRIVATE_DEPS
+ pw_base64
+ GROUPS
+ modules
+ pw_base64
)
diff --git a/pw_base64/base64.cc b/pw_base64/base64.cc
index 77baf371c..97f9d1ec4 100644
--- a/pw_base64/base64.cc
+++ b/pw_base64/base64.cc
@@ -16,6 +16,8 @@
#include <cstdint>
+#include "pw_assert/check.h"
+
namespace pw::base64 {
namespace {
@@ -156,8 +158,7 @@ extern "C" bool pw_Base64IsValid(const char* base64_data, size_t base64_size) {
return true;
}
-size_t Encode(std::span<const std::byte> binary,
- std::span<char> output_buffer) {
+size_t Encode(span<const std::byte> binary, span<char> output_buffer) {
const size_t required_size = EncodedSize(binary.size_bytes());
if (output_buffer.size_bytes() < required_size) {
return 0;
@@ -166,7 +167,7 @@ size_t Encode(std::span<const std::byte> binary,
return required_size;
}
-size_t Decode(std::string_view base64, std::span<std::byte> output_buffer) {
+size_t Decode(std::string_view base64, span<std::byte> output_buffer) {
if (output_buffer.size_bytes() < MaxDecodedSize(base64.size()) ||
!IsValid(base64)) {
return 0;
@@ -174,4 +175,16 @@ size_t Decode(std::string_view base64, std::span<std::byte> output_buffer) {
return Decode(base64, output_buffer.data());
}
+void Encode(span<const std::byte> binary, InlineString<>& output) {
+ const size_t initial_size = output.size();
+ const size_t final_size = initial_size + EncodedSize(binary.size());
+
+ PW_CHECK(final_size <= output.capacity());
+
+ output.resize_and_overwrite([&](char* data, size_t) {
+ Encode(binary, data + initial_size);
+ return final_size;
+ });
+}
+
} // namespace pw::base64
diff --git a/pw_base64/base64_test.cc b/pw_base64/base64_test.cc
index 681a09250..a24006624 100644
--- a/pw_base64/base64_test.cc
+++ b/pw_base64/base64_test.cc
@@ -205,8 +205,7 @@ TEST(Base64, Encode_SingleChar) {
for (const EncodedData& data : kSingleCharTestData) {
const size_t size = EncodedSize(data.binary_size);
ASSERT_EQ(std::strlen(data.encoded_data), size);
- Encode(std::as_bytes(std::span(data.binary_data, data.binary_size)),
- output);
+ Encode(as_bytes(span(data.binary_data, data.binary_size)), output);
output[size] = '\0';
EXPECT_STREQ(data.encoded_data, output);
}
@@ -217,8 +216,7 @@ TEST(Base64, Encode_RandomData) {
for (const EncodedData& data : kRandomTestData) {
const size_t size = EncodedSize(data.binary_size);
ASSERT_EQ(std::strlen(data.encoded_data), size);
- Encode(std::as_bytes(std::span(data.binary_data, data.binary_size)),
- output);
+ Encode(as_bytes(span(data.binary_data, data.binary_size)), output);
output[size] = '\0';
EXPECT_STREQ(data.encoded_data, output);
}
@@ -228,12 +226,31 @@ TEST(Base64, Encode_BoundaryCheck) {
constexpr std::byte data[] = {std::byte{'h'}, std::byte{'i'}};
char output[5] = {};
- EXPECT_EQ(0u, Encode(data, std::span(output, 3)));
+ EXPECT_EQ(0u, Encode(data, span(output, 3)));
EXPECT_STREQ("", output);
- EXPECT_EQ(4u, Encode(data, std::span(output, 4)));
+ EXPECT_EQ(4u, Encode(data, span(output, 4)));
EXPECT_STREQ("aGk=", output);
}
+TEST(Base64, Encode_RandomData_ToInlineString) {
+ for (const EncodedData& data : kRandomTestData) {
+ auto b64 = Encode<11>(as_bytes(span(data.binary_data, data.binary_size)));
+ EXPECT_EQ(data.encoded_data, b64);
+ }
+}
+
+TEST(Base64, Encode_RandomData_ToInlineStringAppend) {
+ for (const EncodedData& data : kRandomTestData) {
+ InlineString<32> output("Wow:");
+
+ InlineString<32> expected(output);
+ expected.append(data.encoded_data);
+
+ Encode(as_bytes(span(data.binary_data, data.binary_size)), output);
+ EXPECT_EQ(expected, output);
+ }
+}
+
TEST(Base64, Decode_SingleChar) {
char output[32];
for (const EncodedData& data : kSingleCharTestData) {
@@ -256,9 +273,9 @@ TEST(Base64, Decode_BoundaryCheck) {
constexpr const char encoded_data[] = "aGk=";
std::byte output[4] = {};
- EXPECT_EQ(0u, Decode(encoded_data, std::span(output, 2)));
+ EXPECT_EQ(0u, Decode(encoded_data, span(output, 2)));
EXPECT_STREQ("", reinterpret_cast<const char*>(output));
- EXPECT_EQ(2u, Decode(encoded_data, std::span(output, 3)));
+ EXPECT_EQ(2u, Decode(encoded_data, span(output, 3)));
EXPECT_STREQ("hi", reinterpret_cast<const char*>(output));
}
@@ -269,6 +286,18 @@ TEST(Base64, Decode_InPlace) {
EXPECT_EQ(0, std::memcmp(expected, buf, sizeof(expected) - 1));
}
+TEST(Base64, DecodeString_InPlace) {
+ constexpr const char expected[] = "This is a secret message";
+ InlineBasicString buf = "VGhpcyBpcyBhIHNlY3JldCBtZXNzYWdl";
+ DecodeInPlace(buf);
+ EXPECT_EQ(sizeof(expected) - 1, buf.size());
+ EXPECT_EQ(expected, buf);
+
+ buf.clear();
+ DecodeInPlace(buf);
+ EXPECT_TRUE(buf.empty());
+}
+
TEST(Base64, Decode_UrlSafeDecode) {
char output[9] = {};
@@ -282,7 +311,7 @@ TEST(Base64, Decode_UrlSafeDecode) {
TEST(Base64, Empty) {
char buffer[] = "DO NOT TOUCH";
EXPECT_EQ(0u, EncodedSize(0));
- Encode(std::as_bytes(std::span("Something cool!!!", 0)), buffer);
+ Encode(as_bytes(span("Something cool!!!", 0)), buffer);
EXPECT_STREQ("DO NOT TOUCH", buffer);
EXPECT_EQ(0u, MaxDecodedSize(0));
@@ -295,11 +324,11 @@ TEST(Base64, ExampleFromRfc3548Section7) {
constexpr uint8_t input[] = {0x14, 0xfb, 0x9c, 0x03, 0xd9, 0x7e};
char output[EncodedSize(sizeof(input)) + 1] = {};
- Encode(std::as_bytes(std::span(input)), output);
+ Encode(as_bytes(span(input)), output);
EXPECT_STREQ("FPucA9l+", output);
- Encode(std::as_bytes(std::span(input, 5)), output);
+ Encode(as_bytes(span(input, 5)), output);
EXPECT_STREQ("FPucA9k=", output);
- Encode(std::as_bytes(std::span(input, 4)), output);
+ Encode(as_bytes(span(input, 4)), output);
EXPECT_STREQ("FPucAw==", output);
EXPECT_EQ(6u, Decode("FPucA9l+", output));
@@ -314,19 +343,19 @@ TEST(Base64, ExampleFromRfc4648Section9) {
char output[EncodedSize(sizeof("foobar")) + 1] = {};
const std::byte* foobar = reinterpret_cast<const std::byte*>("foobar");
- Encode(std::span(foobar, 0), output);
+ Encode(span(foobar, 0), output);
EXPECT_STREQ("", output);
- Encode(std::span(foobar, 1), output);
+ Encode(span(foobar, 1), output);
EXPECT_STREQ("Zg==", output);
- Encode(std::span(foobar, 2), output);
+ Encode(span(foobar, 2), output);
EXPECT_STREQ("Zm8=", output);
- Encode(std::span(foobar, 3), output);
+ Encode(span(foobar, 3), output);
EXPECT_STREQ("Zm9v", output);
- Encode(std::span(foobar, 4), output);
+ Encode(span(foobar, 4), output);
EXPECT_STREQ("Zm9vYg==", output);
- Encode(std::span(foobar, 5), output);
+ Encode(span(foobar, 5), output);
EXPECT_STREQ("Zm9vYmE=", output);
- Encode(std::span(foobar, 6), output);
+ Encode(span(foobar, 6), output);
EXPECT_STREQ("Zm9vYmFy", output);
std::memset(output, '\0', sizeof(output));
diff --git a/pw_base64/base64_test_c.c b/pw_base64/base64_test_c.c
index b2b566f1e..b9bcd53cc 100644
--- a/pw_base64/base64_test_c.c
+++ b/pw_base64/base64_test_c.c
@@ -24,7 +24,7 @@
void pw_Base64CallEncode(const void* binary_data,
const size_t binary_size_bytes,
char* output) {
- return pw_Base64Encode(binary_data, binary_size_bytes, output);
+ pw_Base64Encode(binary_data, binary_size_bytes, output);
}
size_t pw_Base64CallDecode(const char* base64,
diff --git a/pw_base64/public/pw_base64/base64.h b/pw_base64/public/pw_base64/base64.h
index e90510ce7..a0dcc82cb 100644
--- a/pw_base64/public/pw_base64/base64.h
+++ b/pw_base64/public/pw_base64/base64.h
@@ -63,10 +63,12 @@ bool pw_Base64IsValid(const char* base64_data, size_t base64_size);
#ifdef __cplusplus
} // extern "C"
-#include <span>
#include <string_view>
#include <type_traits>
+#include "pw_span/span.h"
+#include "pw_string/string.h"
+
namespace pw::base64 {
// Returns the size of the given number of bytes when encoded as Base64. Base64
@@ -83,14 +85,29 @@ constexpr size_t EncodedSize(size_t binary_size_bytes) {
// output buffers MUST NOT be the same; encoding cannot occur in place.
//
// The resulting string in the output is NOT null-terminated!
-inline void Encode(std::span<const std::byte> binary, char* output) {
+inline void Encode(span<const std::byte> binary, char* output) {
pw_Base64Encode(binary.data(), binary.size_bytes(), output);
}
// Encodes the provided data in Base64 if the result fits in the provided
// buffer. Returns the number of bytes written, which will be 0 if the output
// buffer is too small.
-size_t Encode(std::span<const std::byte> binary, std::span<char> output_buffer);
+size_t Encode(span<const std::byte> binary, span<char> output_buffer);
+
+// Appends Base64 encoded binary data to the provided pw::InlineString. If the
+// data does not fit in the string, an assertion fails.
+void Encode(span<const std::byte> binary, InlineString<>& output);
+
+// Creates a pw::InlineString<> large enough to hold kMaxBinaryDataSizeBytes of
+// binary data when encoded as Base64 and encodes the provided span into it.
+// If the data is larger than kMaxBinaryDataSizeBytes, an assertion fails.
+template <size_t kMaxBinaryDataSizeBytes>
+inline InlineString<EncodedSize(kMaxBinaryDataSizeBytes)> Encode(
+ span<const std::byte> binary) {
+ InlineString<EncodedSize(kMaxBinaryDataSizeBytes)> output;
+ Encode(binary, output);
+ return output;
+}
// Returns the maximum size of decoded Base64 data in bytes. base64_size_bytes
// must be a multiple of 4, since Base64 encodes 3-byte groups into 4-character
@@ -116,7 +133,13 @@ inline size_t Decode(std::string_view base64, void* output) {
// Decodes the provided Base64 data, if the data is valid and fits in the output
// buffer. Returns the number of bytes written, which will be 0 if the data is
// invalid or doesn't fit.
-size_t Decode(std::string_view base64, std::span<std::byte> output_buffer);
+size_t Decode(std::string_view base64, span<std::byte> output_buffer);
+
+template <typename T>
+inline void DecodeInPlace(InlineBasicString<T>& buffer) {
+ static_assert(sizeof(T) == sizeof(char));
+ buffer.resize(Decode(buffer, buffer.data()));
+}
// Returns true if the provided string is valid Base64 encoded data. Accepts
// either the standard (+/) or URL-safe (-_) alphabets.
diff --git a/pw_bloat/BUILD.gn b/pw_bloat/BUILD.gn
index 17d076751..929fd1d4a 100644
--- a/pw_bloat/BUILD.gn
+++ b/pw_bloat/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
import("bloat.gni")
config("default_config") {
@@ -51,3 +52,6 @@ pw_doc_group("docs") {
"examples:simple_bloat_loop",
]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_bloat/bloat.cmake b/pw_bloat/bloat.cmake
index 6a4e9c703..47d231ae5 100644
--- a/pw_bloat/bloat.cmake
+++ b/pw_bloat/bloat.cmake
@@ -27,25 +27,16 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
# ELF_FILE - The input ELF file to process using pw_bloat.bloaty_config
# OUTPUT - The output Bloaty McBloatface configuration file
function(pw_bloaty_config NAME)
- set(option_args)
- set(one_value_args ELF_FILE OUTPUT)
- set(multi_value_args)
- _pw_parse_argv_strict(pw_bloaty_config
- 1 "${option_args}" "${one_value_args}" "${multi_value_args}"
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ ELF_FILE
+ OUTPUT
+ REQUIRED_ARGS
+ ELF_FILE
+ OUTPUT
)
-
- if("${arg_ELF_FILE}" STREQUAL "")
- message(FATAL_ERROR
- "pw_bloaty_config requires an input ELF file in ELF_FILE. "
- "No ELF_FILE was listed for ${NAME}")
- endif()
-
- if("${arg_OUTPUT}" STREQUAL "")
- message(FATAL_ERROR
- "pw_bloaty_config requires an output config file in OUTPUT. "
- "No OUTPUT was listed for ${NAME}")
- endif()
-
add_library(${NAME} INTERFACE)
add_dependencies(${NAME} INTERFACE ${NAME}_generated_config)
diff --git a/pw_bloat/bloat.gni b/pw_bloat/bloat.gni
index 661df3202..52a3a3f80 100644
--- a/pw_bloat/bloat.gni
+++ b/pw_bloat/bloat.gni
@@ -1,4 +1,4 @@
-# Copyright 2019 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# 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
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/evaluate_path_expressions.gni")
import("$dir_pw_build/python_action.gni")
declare_args() {
@@ -21,7 +22,7 @@ declare_args() {
# capacities for the target binaries.
pw_bloat_BLOATY_CONFIG = ""
- # List of toolchains to use in pw_toolchain_size_report templates.
+ # List of toolchains to use in pw_toolchain_size_diff templates.
#
# Each entry is a scope containing the following variables:
#
@@ -32,31 +33,214 @@ declare_args() {
# bloaty_config: Optional Bloaty confirugation file defining the memory
# layout of the binaries as specified in the linker script.
#
- # If this list is empty, pw_toolchain_size_report targets become no-ops.
+ # If this list is empty, pw_toolchain_size_diff targets become no-ops.
pw_bloat_TOOLCHAINS = []
# Controls whether to display size reports in the build output.
pw_bloat_SHOW_SIZE_REPORTS = false
}
+# Creates a size report for a single binary.
+#
+# Args:
+# target: Build target for executable. Required.
+# data_sources: List of datasources from bloaty config file
+# or built-in datasources. Order of sources determines hierarchical
+# output. Optional.
+# github.com/google/bloaty/blob/a1bbc93f5f6f969242046dffd9deb379f6735020/doc/using.md
+# source_filter: Regex to filter data source names in Bloaty. Optional.
+#
+# Example:
+# pw_size_report("foo_bloat") {
+# target = ":foo_static"
+# datasources = "symbols,segment_names"
+# source_filter = "foo"
+# }
+#
+template("pw_size_report") {
+ if (pw_bloat_BLOATY_CONFIG != "") {
+ assert(defined(invoker.target),
+ "Size report must defined a 'target' variable")
+ _all_target_dependencies = [ invoker.target ]
+ _binary_args = []
+
+ if (defined(invoker.source_filter)) {
+ curr_source_filter = invoker.source_filter
+ } else {
+ curr_source_filter = ""
+ }
+
+ if (defined(invoker.data_sources)) {
+ curr_data_sources = string_split(invoker.data_sources, ",")
+ } else {
+ curr_data_sources = ""
+ }
+ _binary_args = [
+ {
+ bloaty_config = rebase_path(pw_bloat_BLOATY_CONFIG, root_build_dir)
+ out_dir = rebase_path(target_gen_dir, root_build_dir)
+ target = "<TARGET_FILE(${invoker.target})>"
+ source_filter = curr_source_filter
+ data_sources = curr_data_sources
+ },
+ ]
+
+ _file_name = "${target_name}_single_binary.json"
+
+ _args_src = "$target_gen_dir/${_file_name}.in"
+ _args_path = "$target_gen_dir/${_file_name}"
+
+ write_file(_args_src,
+ {
+ binaries = _binary_args
+ target_name = target_name
+ out_dir = rebase_path(target_gen_dir, root_build_dir)
+ root = rebase_path("//", root_build_dir)
+ toolchain = current_toolchain
+ default_toolchain = default_toolchain
+ cwd = rebase_path(".", root_build_dir)
+ },
+ "json")
+
+ pw_evaluate_path_expressions("${target_name}.evaluate") {
+ files = [
+ {
+ source = _args_src
+ dest = _args_path
+ },
+ ]
+ }
+
+ _bloat_script_args = [
+ "--gn-arg-path",
+ rebase_path(_args_path, root_build_dir),
+ "--single-report",
+ ]
+
+ _doc_rst_output = "$target_gen_dir/${target_name}"
+ _binary_sizes_output = "$target_gen_dir/${target_name}.binary_sizes.json"
+
+ if (host_os == "win") {
+ # Bloaty is not yet packaged for Windows systems; display a message
+ # indicating this.
+ not_needed("*")
+ not_needed(invoker, "*")
+
+ pw_python_action(target_name) {
+ metadata = {
+ pw_doc_sources = rebase_path([ _doc_rst_output ], root_build_dir)
+ }
+ script = "$dir_pw_bloat/py/pw_bloat/no_bloaty.py"
+ python_deps = [ "$dir_pw_bloat/py" ]
+ args = [ rebase_path(_doc_rst_output, root_build_dir) ]
+ outputs = [ _doc_rst_output ]
+ }
+
+ group(target_name + "_UNUSED_DEPS") {
+ deps = _all_target_dependencies
+ }
+ } else {
+ # Create an action which runs the size report script on the provided
+ # targets.
+ pw_python_action(target_name) {
+ metadata = {
+ pw_doc_sources = rebase_path([ _doc_rst_output ], root_build_dir)
+ }
+ script = "$dir_pw_bloat/py/pw_bloat/bloat.py"
+ python_deps = [ "$dir_pw_bloat/py" ]
+ inputs = [
+ pw_bloat_BLOATY_CONFIG,
+ _args_path,
+ ]
+ outputs = [
+ "${_doc_rst_output}.txt",
+ _binary_sizes_output,
+ _doc_rst_output,
+ ]
+ deps = _all_target_dependencies + [ ":${target_name}.evaluate" ]
+ args = _bloat_script_args
+
+ # Print size reports to stdout when they are generated, if requested.
+ capture_output = !pw_bloat_SHOW_SIZE_REPORTS
+ }
+ }
+ } else {
+ not_needed(invoker, "*")
+ group(target_name) {
+ }
+ }
+}
+
+# Aggregates JSON size report data from several pw_size_report targets into a
+# single output file.
+#
+# Args:
+# deps: List of pw_size_report targets whose data to collect.
+# output: Path to the output JSON file.
+#
+# Example:
+# pw_size_report_aggregation("image_sizes") {
+# deps = [
+# ":app_image_size_report",
+# ":bootloader_image_size_report",
+# ]
+# output = "$root_gen_dir/artifacts/image_sizes.json"
+# }
+#
+template("pw_size_report_aggregation") {
+ assert(defined(invoker.deps) && invoker.deps != [],
+ "pw_size_report_aggregation requires size report dependencies")
+ assert(defined(invoker.output),
+ "pw_size_report_aggregation requires an output file path")
+
+ _input_json_files = []
+
+ foreach(_dep, invoker.deps) {
+ _gen_dir = get_label_info(_dep, "target_gen_dir")
+ _dep_name = get_label_info(_dep, "name")
+ _input_json_files +=
+ [ rebase_path("$_gen_dir/${_dep_name}.binary_sizes.json",
+ root_build_dir) ]
+ }
+
+ pw_python_action(target_name) {
+ script = "$dir_pw_bloat/py/pw_bloat/binary_size_aggregator.py"
+ python_deps = [ "$dir_pw_bloat/py" ]
+ args = [
+ "--output",
+ rebase_path(invoker.output, root_build_dir),
+ ] + _input_json_files
+ outputs = [ invoker.output ]
+ deps = invoker.deps
+ forward_variables_from(invoker, [ "visibility" ])
+ }
+}
+
# Creates a target which runs a size report diff on a set of executables.
#
# Args:
# base: The default base executable target to run the diff against. May be
# omitted if all binaries provide their own base.
+# source_filter: Optional global regex to filter data source names in Bloaty.
+# data_sources: List of datasources from bloaty config file
+# or built-in datasources. Order of sources determines hierarchical
+# output. Optional.
+# github.com/google/bloaty/blob/a1bbc93f5f6f969242046dffd9deb379f6735020/doc/using.md
# binaries: List of executables to compare in the diff.
# Each binary in the list is a scope containing up to three variables:
# label: Descriptive name for the executable. Required.
# target: Build target for the executable. Required.
# base: Optional base diff target. Overrides global base argument.
-# source_filter: Optional regex to filter data source names in Bloaty.
-# title: Optional title string to display with the size report.
-# full_report: Optional boolean flag indicating whether to produce a full
-# symbol size breakdown or a summary.
+# source_filter: Optional regex to filter data source names.
+# Overrides global source_filter argument.
+# data_sources: Optional List of datasources from bloaty config file
+# Overrides global data_sources argument.
+#
#
# Example:
-# pw_size_report("foo_bloat") {
+# pw_size_diff("foo_bloat") {
# base = ":foo_base"
+# data_sources = "segment,symbols"
# binaries = [
# {
# target = ":foo_static"
@@ -65,12 +249,12 @@ declare_args() {
# {
# target = ":foo_dynamic"
# label = "Dynamic"
+# data_sources = "segment_names"
# },
# ]
-# title = "static vs. dynamic foo"
# }
#
-template("pw_size_report") {
+template("pw_size_diff") {
if (pw_bloat_BLOATY_CONFIG != "") {
if (defined(invoker.base)) {
_global_base = invoker.base
@@ -79,10 +263,17 @@ template("pw_size_report") {
_all_target_dependencies = []
}
+ if (defined(invoker.source_filter)) {
+ _global_source_filter = invoker.source_filter
+ }
+
+ if (defined(invoker.data_sources)) {
+ _global_data_sources = string_split(invoker.data_sources, ",")
+ }
+
+ # TODO(brandonvu): Remove once all downstream projects are updated
if (defined(invoker.title)) {
- _title = invoker.title
- } else {
- _title = target_name
+ not_needed(invoker, [ "title" ])
}
# This template creates an action which invokes a Python script to run a
@@ -91,19 +282,16 @@ template("pw_size_report") {
# anything is changed. Most of the code below builds the command-line
# arguments to pass each of the targets into the script.
- _binary_paths = []
- _binary_labels = []
+ # Process each of the binaries, creating an object and storing all the
+ # needed variables into a json. Json is parsed in bloat.py
+ _binaries_args = []
_bloaty_configs = []
- # Process each of the binaries, resolving their full output paths and
- # building them into a list of command-line arguments to the bloat script.
foreach(binary, invoker.binaries) {
assert(defined(binary.label) && defined(binary.target),
"Size report binaries must define 'label' and 'target' variables")
_all_target_dependencies += [ binary.target ]
- _binary_path = "<TARGET_FILE(${binary.target})>"
-
# If the binary defines its own base, use that instead of the global base.
if (defined(binary.base)) {
_binary_base = binary.base
@@ -111,44 +299,79 @@ template("pw_size_report") {
} else if (defined(_global_base)) {
_binary_base = _global_base
} else {
- assert(false, "pw_size_report requires a 'base' file")
+ assert(false, "pw_size_diff requires a 'base' file")
+ }
+
+ if (defined(binary.source_filter)) {
+ _binary_source_filter = binary.source_filter
+ } else if (defined(_global_source_filter)) {
+ _binary_source_filter = _global_source_filter
+ } else {
+ _binary_source_filter = ""
+ }
+
+ _binary_data_sources = []
+ if (defined(binary.data_sources)) {
+ _binary_data_sources = string_split(binary.data_sources, ",")
+ } else if (defined(_global_data_sources)) {
+ _binary_data_sources = _global_data_sources
+ } else {
+ _binary_data_sources = ""
}
# Allow each binary to override the global bloaty config.
if (defined(binary.bloaty_config)) {
+ _binary_bloaty_config = binary.bloaty_config
_bloaty_configs += [ binary.bloaty_config ]
} else {
+ _binary_bloaty_config = pw_bloat_BLOATY_CONFIG
_bloaty_configs += [ pw_bloat_BLOATY_CONFIG ]
}
- _binary_path += ";" + "<TARGET_FILE($_binary_base)>"
+ _binaries_args += [
+ {
+ bloaty_config = rebase_path(_binary_bloaty_config, root_build_dir)
+ target = "<TARGET_FILE(${binary.target})>"
+ base = "<TARGET_FILE($_binary_base)>"
+ source_filter = _binary_source_filter
+ label = binary.label
+ data_sources = _binary_data_sources
+ },
+ ]
+ }
- _binary_paths += [ _binary_path ]
- _binary_labels += [ binary.label ]
+ _file_name = "${target_name}_binaries.json"
+ _diff_source = "$target_gen_dir/${_file_name}.in"
+ _diff_path = "$target_gen_dir/${_file_name}"
+ write_file(_diff_source,
+ {
+ binaries = _binaries_args
+ target_name = target_name
+ out_dir = rebase_path(target_gen_dir, root_build_dir)
+ root = rebase_path("//", root_build_dir)
+ toolchain = current_toolchain
+ default_toolchain = default_toolchain
+ cwd = rebase_path(".", root_build_dir)
+ },
+ "json")
+
+ pw_evaluate_path_expressions("${target_name}.evaluate") {
+ files = [
+ {
+ source = _diff_source
+ dest = _diff_path
+ },
+ ]
}
_bloat_script_args = [
- "--bloaty-config",
- string_join(";", rebase_path(_bloaty_configs, root_build_dir)),
- "--out-dir",
- rebase_path(target_gen_dir, root_build_dir),
- "--target",
- target_name,
- "--title",
- _title,
- "--labels",
- string_join(";", _binary_labels),
+ "--gn-arg-path",
+ rebase_path(_diff_path, root_build_dir),
]
- if (defined(invoker.full_report) && invoker.full_report) {
- _bloat_script_args += [ "--full" ]
- }
-
- if (defined(invoker.source_filter)) {
- _bloat_script_args += [
- "--source-filter",
- invoker.source_filter,
- ]
+ # TODO(brandonvu): Remove once all downstream projects are updated
+ if (defined(invoker.full_report)) {
+ not_needed(invoker, [ "full_report" ])
}
_doc_rst_output = "$target_gen_dir/${target_name}"
@@ -181,13 +404,13 @@ template("pw_size_report") {
}
script = "$dir_pw_bloat/py/pw_bloat/bloat.py"
python_deps = [ "$dir_pw_bloat/py" ]
- inputs = _bloaty_configs
+ inputs = _bloaty_configs + [ _diff_path ]
outputs = [
"${_doc_rst_output}.txt",
_doc_rst_output,
]
- deps = _all_target_dependencies
- args = _bloat_script_args + _binary_paths
+ deps = _all_target_dependencies + [ ":${target_name}.evaluate" ]
+ args = _bloat_script_args
# Print size reports to stdout when they are generated, if requested.
capture_output = !pw_bloat_SHOW_SIZE_REPORTS
@@ -216,7 +439,7 @@ template("pw_size_report") {
#
# Example:
#
-# pw_toolchain_size_report("my_size_report") {
+# pw_toolchain_size_diff("my_size_report") {
# base_executable = {
# sources = [ "base.cc" ]
# }
@@ -227,11 +450,11 @@ template("pw_size_report") {
# }
# }
#
-template("pw_toolchain_size_report") {
+template("pw_toolchain_size_diff") {
assert(defined(invoker.base_executable),
- "pw_toolchain_size_report requires a base_executable")
+ "pw_toolchain_size_diff requires a base_executable")
assert(defined(invoker.diff_executable),
- "pw_toolchain_size_report requires a diff_executable")
+ "pw_toolchain_size_diff requires a diff_executable")
_size_report_binaries = []
@@ -241,7 +464,7 @@ template("pw_toolchain_size_report") {
# Create a base and diff executable for each toolchain, adding the toolchain's
# linker script to the link flags for the executable, and add them all to a
- # list of binaries for the pw_size_report template.
+ # list of binaries for the pw_size_diff template.
foreach(_toolchain, pw_bloat_TOOLCHAINS) {
_prefix = "_${target_name}_${i}_pw_size"
@@ -290,7 +513,7 @@ template("pw_toolchain_size_report") {
_diff_label = get_label_info(":$_diff_target_name", "label_no_toolchain")
_diff_with_toolchain = "$_diff_label(${_toolchain.target})"
- # Append a pw_size_report binary scope to the list comparing the toolchain's
+ # Append a pw_size_diff binary scope to the list comparing the toolchain's
# diff and base executables.
_size_report_binaries += [
{
@@ -310,7 +533,7 @@ template("pw_toolchain_size_report") {
# TODO(frolv): Have a way of indicating that a toolchain should build docs.
if (current_toolchain == default_toolchain && _size_report_binaries != []) {
# Create the size report which runs on the binaries.
- pw_size_report(target_name) {
+ pw_size_diff(target_name) {
forward_variables_from(invoker, [ "title" ])
binaries = _size_report_binaries
}
@@ -334,7 +557,7 @@ template("pw_toolchain_size_report") {
}
}
-# A base_executable for the pw_toolchain_size_report template which contains a
+# A base_executable for the pw_toolchain_size_diff template which contains a
# main() function that loads the bloat_this_binary library and does nothing
# else.
pw_bloat_empty_base = {
diff --git a/pw_bloat/bloat_macros.ld b/pw_bloat/bloat_macros.ld
new file mode 100644
index 000000000..c2f754b57
--- /dev/null
+++ b/pw_bloat/bloat_macros.ld
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2022 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/* Helper macros to define the symbols requires by pw_bloat to detect the
+ * utilization and memory regions of your program.
+ *
+ * Include this file into your pw_linker_script() as follows:
+ *
+ * pw_linker_script("my_linker_script") {
+ * includes = [ "$dir_pw_bloat/bloat_macros.ld" ]
+ * linker_script = "my_project_linker_script.ld"
+ * }
+ */
+
+/* Default alignment used when declaring new sections. In most cases the free
+ * space should be measured down to a multiple of 4 bytes, but this can be
+ * changed in needed. */
+#ifndef PW_BLOAT_SECTION_ALIGN
+#define PW_BLOAT_SECTION_ALIGN 4
+#endif
+
+/* Declares an unused_space section. Instantiate this macro from within the
+ * SECTIONS of your linker script, after every other section, for example:
+ * PW_BLOAT_UNUSED_SPACE_SECTION(FLASH)
+ * PW_BLOAT_UNUSED_SPACE_SECTION(RAM)
+ */
+#define PW_BLOAT_UNUSED_SPACE_SECTION(memory_region) \
+ .memory_region.unused_space(NOLOAD) : ALIGN(PW_BLOAT_SECTION_ALIGN) \
+ { \
+ . = ABSOLUTE(ORIGIN(memory_region) + LENGTH(memory_region)); \
+ } > memory_region
+
+/* Declares a memory region in pw_bloat mapped to the same name. Example:
+ * PW_BLOAT_MEMORY_REGION(FLASH)
+ */
+#define PW_BLOAT_MEMORY_REGION(memory_region) \
+ PW_BLOAT_MEMORY_REGION_MAP(memory_region, memory_region)
+
+/* Declares a memory region in pw_bloat mapped to a different name. Can be used
+ * multiple times to map multiple aliased memory regions to the same name.
+ * PW_BLOAT_MEMORY_REGION_MAP(RAM, ITCM)
+ * PW_BLOAT_MEMORY_REGION_MAP(RAM, DTCM)
+ */
+#define PW_BLOAT_MEMORY_REGION_MAP(name, memory_region) \
+ PW_BLOAT_MEMORY_REGION_MAP_N(name, memory_region, __COUNTER__)
+
+/* Alternative version of PW_BLOAT_MEMORY_REGION_MAP to also specify the index
+ * value. This index value is irrelevant for pw_bloat tools as long as it
+ * doesn't repeat for the same name. Use this macro if you need to specify the
+ * index value for other tools.
+ * Note: this uses two macros to expand __COUNTER__ in
+ * PW_BLOAT_MEMORY_REGION_MAP. */
+#define PW_BLOAT_MEMORY_REGION_MAP_N(name, memory_region, index) \
+ _PW_BLOAT_MEMORY_REGION_MAP_N(name, memory_region, index)
+
+#define _PW_BLOAT_MEMORY_REGION_MAP_N(name, memory_region, index) \
+ pw_bloat_config_memory_region_##name##_start_##index = \
+ ORIGIN(memory_region); \
+ pw_bloat_config_memory_region_##name##_end_##index = \
+ ORIGIN(memory_region) + LENGTH(memory_region);
diff --git a/pw_bloat/bloat_this_binary.cc b/pw_bloat/bloat_this_binary.cc
index 2dbf47df1..655b53ac9 100644
--- a/pw_bloat/bloat_this_binary.cc
+++ b/pw_bloat/bloat_this_binary.cc
@@ -31,7 +31,7 @@ void BloatThisBinary() {
// executable on their device, loop forever instead of running this code.
volatile bool clearly_false_condition = true;
while (clearly_false_condition) {
- counter += 1;
+ counter = counter + 1;
}
// This code uses standard C/C++ functions such as memcpy to prevent them from
diff --git a/pw_bloat/docs.rst b/pw_bloat/docs.rst
index a5f9c3dbe..85441e8f6 100644
--- a/pw_bloat/docs.rst
+++ b/pw_bloat/docs.rst
@@ -5,33 +5,121 @@ pw_bloat
--------
The bloat module provides tools and helpers around using
`Bloaty McBloatface <https://github.com/google/bloaty>`_ including generating
-generate size report cards for output binaries through Pigweed's GN build
+size report cards for output binaries through Pigweed's GN build
system.
Bloat report cards allow tracking the memory usage of a system over time as code
changes are made and provide a breakdown of which parts of the code have the
largest size impact.
+``pw bloat`` CLI command
+========================
+``pw_bloat`` includes a plugin for the Pigweed command line capable of running
+size reports on ELF binaries.
+
+.. note::
+
+ The bloat CLI plugin is still experimental and only supports a small subset
+ of ``pw_bloat``'s capabilities. Notably, it currently only runs on binaries
+ which define memory region symbols; refer to the
+ :ref:`memoryregions documentation <module-pw_bloat-memoryregions>`
+ for details.
+
+Basic usage
+^^^^^^^^^^^
+
+**Running a size report on a single executable**
+
+.. code-block:: sh
+
+ $ pw bloat out/docs/obj/pw_result/size_report/bin/ladder_and_then.elf
+
+ ▒█████▄ █▓ ▄███▒ ▒█ ▒█ ░▓████▒ ░▓████▒ ▒▓████▄
+ ▒█░ █░ ░█▒ ██▒ ▀█▒ ▒█░ █ ▒█ ▒█ ▀ ▒█ ▀ ▒█ ▀█▌
+ ▒█▄▄▄█░ ░█▒ █▓░ ▄▄░ ▒█░ █ ▒█ ▒███ ▒███ ░█ █▌
+ ▒█▀ ░█░ ▓█ █▓ ░█░ █ ▒█ ▒█ ▄ ▒█ ▄ ░█ ▄█▌
+ ▒█ ░█░ ░▓███▀ ▒█▓▀▓█░ ░▓████▒ ░▓████▒ ▒▓████▀
+
+ +----------------------+---------+
+ | memoryregions | sizes |
+ +======================+=========+
+ |FLASH |1,048,064|
+ |RAM | 196,608|
+ |VECTOR_TABLE | 512|
+ +======================+=========+
+ |Total |1,245,184|
+ +----------------------+---------+
+
+**Running a size report diff**
+
+.. code-block:: sh
+
+
+ $ pw bloat out/docs/obj/pw_metric/size_report/bin/one_metric.elf \
+ --diff out/docs/obj/pw_metric/size_report/bin/base.elf \
+ -d symbols
+
+ ▒█████▄ █▓ ▄███▒ ▒█ ▒█ ░▓████▒ ░▓████▒ ▒▓████▄
+ ▒█░ █░ ░█▒ ██▒ ▀█▒ ▒█░ █ ▒█ ▒█ ▀ ▒█ ▀ ▒█ ▀█▌
+ ▒█▄▄▄█░ ░█▒ █▓░ ▄▄░ ▒█░ █ ▒█ ▒███ ▒███ ░█ █▌
+ ▒█▀ ░█░ ▓█ █▓ ░█░ █ ▒█ ▒█ ▄ ▒█ ▄ ░█ ▄█▌
+ ▒█ ░█░ ░▓███▀ ▒█▓▀▓█░ ░▓████▒ ░▓████▒ ▒▓████▀
+
+ +-----------------------------------------------------------------------------------+
+ | |
+ +-----------------------------------------------------------------------------------+
+ | diff| memoryregions | symbols | sizes|
+ +=====+======================+===============================================+======+
+ | |FLASH | | -4|
+ | | |[section .FLASH.unused_space] | -408|
+ | | |main | +60|
+ | | |__sf_fake_stdout | +4|
+ | | |pw_boot_PreStaticMemoryInit | -2|
+ | | |_isatty | -2|
+ | NEW| |_GLOBAL__sub_I_group_foo | +84|
+ | NEW| |pw::metric::Group::~Group() | +34|
+ | NEW| |pw::intrusive_list_impl::List::insert_after() | +32|
+ | NEW| |pw::metric::Metric::Increment() | +32|
+ | NEW| |__cxa_atexit | +28|
+ | NEW| |pw::metric::Metric::Metric() | +28|
+ | NEW| |pw::metric::Metric::as_int() | +28|
+ | NEW| |pw::intrusive_list_impl::List::Item::unlist() | +20|
+ | NEW| |pw::metric::Group::Group() | +18|
+ | NEW| |pw::intrusive_list_impl::List::Item::previous()| +14|
+ | NEW| |pw::metric::TypedMetric<>::~TypedMetric() | +14|
+ | NEW| |__aeabi_atexit | +12|
+ +-----+----------------------+-----------------------------------------------+------+
+ | |RAM | | 0|
+ | | |[section .stack] | -32|
+ | NEW| |group_foo | +16|
+ | NEW| |metric_x | +12|
+ | NEW| |[section .static_init_ram] | +4|
+ +=====+======================+===============================================+======+
+ |Total| | | -4|
+ +-----+----------------------+-----------------------------------------------+------+
+
+
.. _bloat-howto:
-Defining size reports
-=====================
-Size reports are defined using the GN template ``pw_size_report``. The template
+Defining size reports in GN
+===========================
+
+Diff Size Reports
+^^^^^^^^^^^^^^^^^
+Size reports can be defined using the GN template ``pw_size_diff``. The template
requires at least two executable targets on which to perform a size diff. The
base for the size diff can be specified either globally through the top-level
``base`` argument, or individually per-binary within the ``binaries`` list.
**Arguments**
-* ``title``: Title for the report card.
* ``base``: Optional default base target for all listed binaries.
+* ``source_filter``: Optional global regex to filter labels in the diff output.
+* ``data_sources``: Optional global list of datasources from bloaty config file
* ``binaries``: List of binaries to size diff. Each binary specifies a target,
- a label for the diff, and optionally a base target that overrides the default
- base.
-* ``source_filter``: Optional regex to filter labels in the diff output.
-* ``full_report``: Boolean flag indicating whether to output a full report of
- all symbols in the binary, or a summary of the segment size changes. Default
- false.
+ a label for the diff, and optionally a base target, source filter, and data
+ sources that override the global ones (if specified).
+
.. code::
@@ -49,9 +137,9 @@ base for the size diff can be specified either globally through the top-level
sources = [ "hello_iostream.cc" ]
}
- pw_size_report("my_size_report") {
- title = "Hello world program using printf vs. iostream"
+ pw_size_diff("my_size_report") {
base = ":empty_base"
+ data_sources = "symbols,segments"
binaries = [
{
target = ":hello_world_printf"
@@ -60,21 +148,124 @@ base for the size diff can be specified either globally through the top-level
{
target = ":hello_world_iostream"
label = "Hello world using iostream"
+ data_sources = "symbols"
},
]
}
+A sample ``pw_size_diff`` ReST size report table can be found within module
+docs. For example, see the :ref:`pw_checksum-size-report` section of the
+``pw_checksum`` module for more detail.
+
+
+Single Binary Size Reports
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Size reports can also be defined using ``pw_size_report``, which provides
+a size report for a single binary. The template requires a target binary.
+
+**Arguments**
+
+* ``target``: Binary target to run size report on.
+* ``data_sources``: Optional list of data sources to organize outputs.
+* ``source_filter``: Optional regex to filter labels in the output.
+
+.. code::
+
+ import("$dir_pw_bloat/bloat.gni")
+
+ executable("hello_world_iostream") {
+ sources = [ "hello_iostream.cc" ]
+ }
+
+ pw_size_report("hello_world_iostream_size_report") {
+ target = ":hello_iostream"
+ data_sources = "segments,symbols"
+ source_filter = "pw::hello"
+ }
+
+Sample Single Binary ASCII Table Generated
+
+.. code-block::
+
+ ┌─────────────┬──────────────────────────────────────────────────┬──────┐
+ │segment_names│ symbols │ sizes│
+ ├═════════════┼══════════════════════════════════════════════════┼══════┤
+ │FLASH │ │12,072│
+ │ │pw::kvs::KeyValueStore::InitializeMetadata() │ 684│
+ │ │pw::kvs::KeyValueStore::Init() │ 456│
+ │ │pw::kvs::internal::EntryCache::Find() │ 444│
+ │ │pw::kvs::FakeFlashMemory::Write() │ 240│
+ │ │pw::kvs::internal::Entry::VerifyChecksumInFlash() │ 228│
+ │ │pw::kvs::KeyValueStore::GarbageCollectSector() │ 220│
+ │ │pw::kvs::KeyValueStore::RemoveDeletedKeyEntries() │ 220│
+ │ │pw::kvs::KeyValueStore::AppendEntry() │ 204│
+ │ │pw::kvs::KeyValueStore::Get() │ 194│
+ │ │pw::kvs::internal::Entry::Read() │ 188│
+ │ │pw::kvs::ChecksumAlgorithm::Finish() │ 26│
+ │ │pw::kvs::internal::Entry::ReadKey() │ 26│
+ │ │pw::kvs::internal::Sectors::BaseAddress() │ 24│
+ │ │pw::kvs::ChecksumAlgorithm::Update() │ 20│
+ │ │pw::kvs::FlashTestPartition() │ 8│
+ │ │pw::kvs::FakeFlashMemory::Disable() │ 6│
+ │ │pw::kvs::FakeFlashMemory::Enable() │ 6│
+ │ │pw::kvs::FlashMemory::SelfTest() │ 6│
+ │ │pw::kvs::FlashPartition::Init() │ 6│
+ │ │pw::kvs::FlashPartition::sector_size_bytes() │ 6│
+ │ │pw::kvs::FakeFlashMemory::IsEnabled() │ 4│
+ ├─────────────┼──────────────────────────────────────────────────┼──────┤
+ │RAM │ │ 1,424│
+ │ │test_kvs │ 992│
+ │ │pw::kvs::(anonymous namespace)::test_flash │ 384│
+ │ │pw::kvs::(anonymous namespace)::test_partition │ 24│
+ │ │pw::kvs::FakeFlashMemory::no_errors_ │ 12│
+ │ │borrowable_kvs │ 8│
+ │ │kvs_entry_count │ 4│
+ ├═════════════┼══════════════════════════════════════════════════┼══════┤
+ │Total │ │13,496│
+ └─────────────┴──────────────────────────────────────────────────┴──────┘
+
+
Size reports are typically included in ReST documentation, as described in
`Documentation integration`_. Size reports may also be printed in the build
-output if desired. To enable this in the GN build, set the
-``pw_bloat_SHOW_SIZE_REPORTS`` build arg to ``true``.
+output if desired. To enable this in the GN build
+(``pigweed/pw_bloat/bloat.gni``), set the ``pw_bloat_SHOW_SIZE_REPORTS``
+build arg to ``true``.
+
+Collecting size report data
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Each ``pw_size_report`` target outputs a JSON file containing the sizes of all
+top-level labels in the binary. (By default, this represents "segments", i.e.
+ELF program headers.) If a build produces multiple images, it may be useful to
+collect all of their sizes into a single file to provide a snapshot of sizes at
+some point in time --- for example, to display per-commit size deltas through
+CI.
+
+The ``pw_size_report_aggregation`` template is provided to collect multiple size
+reports' data into a single JSON file.
+
+**Arguments**
+
+* ``deps``: List of ``pw_size_report`` targets whose data to collect.
+* ``output``: Path to the output JSON file.
+
+.. code::
+
+ import("$dir_pw_bloat/bloat.gni")
+
+ pw_size_report_aggregation("image_sizes") {
+ deps = [
+ ":app_image_size_report",
+ ":bootloader_image_size_report",
+ ]
+ output = "$root_gen_dir/artifacts/image_sizes.json"
+ }
Documentation integration
=========================
-Bloat reports are easy to add to documentation files. All ``pw_size_report``
-targets output a file containing a tabular report card. This file can be
-imported directly into a ReST documentation file using the ``include``
-directive.
+Bloat reports are easy to add to documentation files. All ``pw_size_diff``
+and ``pw_size_report`` targets output a file containing a tabular report card.
+This file can be imported directly into a ReST documentation file using the
+``include`` directive.
For example, the ``simple_bloat_loop`` and ``simple_bloat_function`` size
reports under ``//pw_bloat/examples`` are imported into this file as follows:
@@ -167,7 +358,7 @@ For example imagine this partial example GNU LD linker script:
SECTIONS
{
/* Main executable code. */
- .code : ALIGN(8)
+ .code : ALIGN(4)
{
/* Application code. */
*(.text)
@@ -175,27 +366,27 @@ For example imagine this partial example GNU LD linker script:
KEEP(*(.init))
KEEP(*(.fini))
- . = ALIGN(8);
+ . = ALIGN(4);
/* Constants.*/
*(.rodata)
*(.rodata*)
} >FLASH
/* Explicitly initialized global and static data. (.data)*/
- .static_init_ram : ALIGN(8)
+ .static_init_ram : ALIGN(4)
{
*(.data)
*(.data*)
- . = ALIGN(8);
+ . = ALIGN(4);
} >RAM AT> FLASH
/* Zero initialized global/static data. (.bss) */
- .zero_init_ram : ALIGN(8)
+ .zero_init_ram (NOLOAD) : ALIGN(4)
{
*(.bss)
*(.bss*)
*(COMMON)
- . = ALIGN(8);
+ . = ALIGN(4);
} >RAM
}
@@ -207,12 +398,32 @@ Could be modified as follows enable ``Free Space`` reporting:
{
FLASH(rx) : ORIGIN = PW_BOOT_FLASH_BEGIN, LENGTH = PW_BOOT_FLASH_SIZE
RAM(rwx) : ORIGIN = PW_BOOT_RAM_BEGIN, LENGTH = PW_BOOT_RAM_SIZE
+
+ /* Each memory region above has an associated .*.unused_space section that
+ * overlays the unused space at the end of the memory segment. These
+ * segments are used by pw_bloat.bloaty_config to create the utilization
+ * data source for bloaty size reports.
+ *
+ * These sections MUST be located immediately after the last section that is
+ * placed in the respective memory region or lld will issue a warning like:
+ *
+ * warning: ignoring memory region assignment for non-allocatable section
+ * '.VECTOR_TABLE.unused_space'
+ *
+ * If this warning occurs, it's also likely that LLD will have created quite
+ * large padded regions in the ELF file due to bad cursor operations. This
+ * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
+ * megabytes.
+ *
+ * Attempting to add sections to the memory region AFTER the unused_space
+ * section will cause the region to overflow.
+ */
}
SECTIONS
{
/* Main executable code. */
- .code : ALIGN(8)
+ .code : ALIGN(4)
{
/* Application code. */
*(.text)
@@ -220,46 +431,57 @@ Could be modified as follows enable ``Free Space`` reporting:
KEEP(*(.init))
KEEP(*(.fini))
- . = ALIGN(8);
+ . = ALIGN(4);
/* Constants.*/
*(.rodata)
*(.rodata*)
} >FLASH
/* Explicitly initialized global and static data. (.data)*/
- .static_init_ram : ALIGN(8)
+ .static_init_ram : ALIGN(4)
{
*(.data)
*(.data*)
- . = ALIGN(8);
+ . = ALIGN(4);
} >RAM AT> FLASH
+ /* Defines a section representing the unused space in the FLASH segment.
+ * This MUST be the last section assigned to the FLASH region.
+ */
+ PW_BLOAT_UNUSED_SPACE(FLASH)
+
/* Zero initialized global/static data. (.bss). */
- .zero_init_ram : ALIGN(8)
+ .zero_init_ram (NOLOAD) : ALIGN(4)
{
*(.bss)
*(.bss*)
*(COMMON)
- . = ALIGN(8);
+ . = ALIGN(4);
} >RAM
- /*
- * Do not declare any output sections after this comment. This area is
- * reserved only for declaring unused sections of memory. These sections are
- * used by pw_bloat.bloaty_config to create the utilization data source for
- * bloaty.
+ /* Defines a section representing the unused space in the RAM segment. This
+ * MUST be the last section assigned to the RAM region.
*/
- .FLASH.unused_space (NOLOAD) : ALIGN(8)
- {
- . = ABSOLUTE(ORIGIN(FLASH) + LENGTH(FLASH));
- } >FLASH
-
- .RAM.unused_space (NOLOAD) : ALIGN(8)
- {
- . = ABSOLUTE(ORIGIN(RAM) + LENGTH(RAM));
- } >RAM
+ PW_BLOAT_UNUSED_SPACE(RAM)
}
+The preprocessor macro ``PW_BLOAT_UNUSED_SPACE`` is defined in
+``pw_bloat/bloat_macros.ld``. To use these macros include this file in your
+``pw_linker_script`` as follows:
+
+.. code-block::
+
+ pw_linker_script("my_linker_script") {
+ includes = [ "$dir_pw_bloat/bloat_macros.ld" ]
+ linker_script = "my_project_linker_script.ld"
+ }
+
+Note that linker scripts are not natively supported by GN and can't be provided
+through ``deps``, the ``bloat_macros.ld`` must be passed in the ``includes``
+list.
+
+.. _module-pw_bloat-memoryregions:
+
``memoryregions`` data source
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Understanding how symbols, sections, and other data sources can be attributed
@@ -274,18 +496,29 @@ files, ``pw_bloat.bloaty_config`` consumes symbols which are defined in the
linker script with a special format to extract this information from the ELF
file: ``pw_bloat_config_memory_region_NAME_{start,end}{_N,}``.
+These symbols are defined by the preprocessor macros ``PW_BLOAT_MEMORY_REGION``
+and ``PW_BLOAT_MEMORY_REGION_MAP`` with the right address and size for the
+regions. To use these macros include the ``pw_bloat/bloat_macros.ld`` in your
+``pw_linker_script`` as follows:
+
+.. code-block::
+
+ pw_linker_script("my_linker_script") {
+ includes = [ "$dir_pw_bloat/bloat_macros.ld" ]
+ linker_script = "my_project_linker_script.ld"
+ }
+
These symbols are then used to determine how to map segments to these memory
regions. Note that segments must be used in order to account for inter-section
padding which are not attributed against any sections.
As an example, if you have a single view in the single memory region named
-``FLASH``, then you should produce the following two symbols in your linker
-script:
+``FLASH``, then you should include the following macro in your linker script to
+generate the symbols needed for the that region:
.. code-block::
- pw_bloat_config_memory_region_FLASH_start = ORIGIN(FLASH);
- pw_bloat_config_memory_region_FLASH_end = ORIGIN(FLASH) + LENGTH(FLASH);
+ PW_BLOAT_MEMORY_REGION(FLASH)
As another example, if you have two aliased memory regions (``DCTM`` and
``ITCM``) into the same effective memory named you'd like to call ``RAM``, then
@@ -293,7 +526,5 @@ you should produce the following four symbols in your linker script:
.. code-block::
- pw_bloat_config_memory_region_RAM_start_0 = ORIGIN(ITCM);
- pw_bloat_config_memory_region_RAM_end_0 = ORIGIN(ITCM) + LENGTH(ITCM);
- pw_bloat_config_memory_region_RAM_start_1 = ORIGIN(DTCM);
- pw_bloat_config_memory_region_RAM_end_1 = ORIGIN(DTCM) + LENGTH(DTCM);
+ PW_BLOAT_MEMORY_REGION_MAP(RAM, ITCM)
+ PW_BLOAT_MEMORY_REGION_MAP(RAM, DTCM)
diff --git a/pw_bloat/examples/BUILD.gn b/pw_bloat/examples/BUILD.gn
index fc4fcc2d4..cca16985f 100644
--- a/pw_bloat/examples/BUILD.gn
+++ b/pw_bloat/examples/BUILD.gn
@@ -16,20 +16,39 @@ import("//build_overrides/pigweed.gni")
import("../bloat.gni")
-pw_toolchain_size_report("simple_bloat_loop") {
- base_executable = {
- sources = [ "simple_base.cc" ]
- }
- diff_executable = {
- sources = [ "simple_loop.cc" ]
- }
+import("$dir_pw_build/target_types.gni")
+
+pw_executable("simple_base") {
+ sources = [ "simple_base.cc" ]
+ deps = [ "$dir_pw_bloat:bloat_this_binary" ]
+}
+
+pw_executable("simple_loop") {
+ sources = [ "simple_loop.cc" ]
+ deps = [ "$dir_pw_bloat:bloat_this_binary" ]
+}
+
+pw_executable("simple_function") {
+ sources = [ "simple_function.cc" ]
+ deps = [ "$dir_pw_bloat:bloat_this_binary" ]
+}
+
+pw_size_diff("simple_bloat_loop") {
+ binaries = [
+ {
+ target = ":simple_loop"
+ base = ":simple_base"
+ label = "Simple bloat loop"
+ },
+ ]
}
-pw_toolchain_size_report("simple_bloat_function") {
- base_executable = {
- sources = [ "simple_base.cc" ]
- }
- diff_executable = {
- sources = [ "simple_function.cc" ]
- }
+pw_size_diff("simple_bloat_function") {
+ binaries = [
+ {
+ target = ":simple_function"
+ base = ":simple_base"
+ label = "Simple bloat function"
+ },
+ ]
}
diff --git a/pw_bloat/py/BUILD.gn b/pw_bloat/py/BUILD.gn
index 028e4e140..2f75579bf 100644
--- a/pw_bloat/py/BUILD.gn
+++ b/pw_bloat/py/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -24,14 +24,23 @@ pw_python_package("py") {
]
sources = [
"pw_bloat/__init__.py",
- "pw_bloat/binary_diff.py",
+ "pw_bloat/__main__.py",
+ "pw_bloat/binary_size_aggregator.py",
"pw_bloat/bloat.py",
- "pw_bloat/bloat_output.py",
"pw_bloat/bloaty_config.py",
+ "pw_bloat/label.py",
+ "pw_bloat/label_output.py",
"pw_bloat/no_bloaty.py",
"pw_bloat/no_toolchains.py",
]
- tests = [ "bloaty_config_test.py" ]
+ tests = [
+ "bloaty_config_test.py",
+ "label_test.py",
+ ]
pylintrc = "$dir_pigweed/.pylintrc"
- python_deps = [ "$dir_pw_cli/py" ]
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ python_deps = [
+ "$dir_pw_build/py",
+ "$dir_pw_cli/py",
+ ]
}
diff --git a/pw_bloat/py/bloaty_config_test.py b/pw_bloat/py/bloaty_config_test.py
index 340ffe2f6..07c3fe15c 100644
--- a/pw_bloat/py/bloaty_config_test.py
+++ b/pw_bloat/py/bloaty_config_test.py
@@ -21,27 +21,22 @@ from pw_bloat import bloaty_config
class BloatyConfigTest(unittest.TestCase):
"""Tests that the bloaty config tool produces the expected config."""
+
def test_map_segments_to_memory_regions(self) -> None:
"""Ensures the mapping works correctly based on a real example."""
segments = {
- 3: (int(0x800f268), int(0x8100200)),
+ 3: (int(0x800F268), int(0x8100200)),
5: (int(0x20004650), int(0x20020650)),
6: (int(0x20020650), int(0x20030000)),
- 1: (int(0x8000200), int(0x800f060)),
+ 1: (int(0x8000200), int(0x800F060)),
4: (int(0x20000208), int(0x20004650)),
2: (int(0x20000000), int(0x20000208)),
0: (int(0x8000000), int(0x8000200)),
}
memory_regions = {
- 'FLASH': {
- 0: (int(0x8000200), int(0x8100200))
- },
- 'RAM': {
- 0: (int(0x20000000), int(0x20030000))
- },
- 'VECTOR_TABLE': {
- 0: (int(0x8000000), int(0x8000200))
- },
+ 'FLASH': {0: (int(0x8000200), int(0x8100200))},
+ 'RAM': {0: (int(0x20000000), int(0x20030000))},
+ 'VECTOR_TABLE': {0: (int(0x8000000), int(0x8000200))},
}
expected = {
3: 'FLASH',
@@ -53,7 +48,8 @@ class BloatyConfigTest(unittest.TestCase):
0: 'VECTOR_TABLE',
}
actual = bloaty_config.map_segments_to_memory_regions(
- segments=segments, memory_regions=memory_regions)
+ segments=segments, memory_regions=memory_regions
+ )
self.assertEqual(expected, actual)
def test_generate_memoryregions_data_source(self) -> None:
@@ -64,49 +60,54 @@ class BloatyConfigTest(unittest.TestCase):
13: 'FLASH',
}
config = bloaty_config.generate_memoryregions_data_source(
- segments_to_memory_regions)
- expected = '\n'.join((
- r'custom_data_source: {',
- r' name: "memoryregions"',
- r' base_data_source: "segments"',
- r' rewrite: {',
- r' pattern:"^LOAD #0 \\[.*\\]$"',
- r' replacement:"RAM"',
- r' }',
- r' rewrite: {',
- r' pattern:"^LOAD #1 \\[.*\\]$"',
- r' replacement:"RAM"',
- r' }',
- r' rewrite: {',
- r' pattern:"^LOAD #13 \\[.*\\]$"',
- r' replacement:"FLASH"',
- r' }',
- r' rewrite: {',
- r' pattern:".*"',
- r' replacement:"Not resident in memory"',
- r' }',
- r'}',
- r'',
- ))
+ segments_to_memory_regions
+ )
+ expected = '\n'.join(
+ (
+ r'custom_data_source: {',
+ r' name: "memoryregions"',
+ r' base_data_source: "segments"',
+ r' rewrite: {',
+ r' pattern:"^LOAD #0 \\[.*\\]$"',
+ r' replacement:"RAM"',
+ r' }',
+ r' rewrite: {',
+ r' pattern:"^LOAD #1 \\[.*\\]$"',
+ r' replacement:"RAM"',
+ r' }',
+ r' rewrite: {',
+ r' pattern:"^LOAD #13 \\[.*\\]$"',
+ r' replacement:"FLASH"',
+ r' }',
+ r' rewrite: {',
+ r' pattern:".*"',
+ r' replacement:"Not resident in memory"',
+ r' }',
+ r'}',
+ r'',
+ )
+ )
self.assertEqual(expected, config)
def test_generate_utilization_data_source(self) -> None:
config = bloaty_config.generate_utilization_data_source()
- expected = '\n'.join((
- 'custom_data_source: {',
- ' name:"utilization"',
- ' base_data_source:"sections"',
- ' rewrite: {',
- ' pattern:"unused_space"',
- ' replacement:"Free space"',
- ' }',
- ' rewrite: {',
- ' pattern:".*"',
- ' replacement:"Used space"',
- ' }',
- '}',
- '',
- ))
+ expected = '\n'.join(
+ (
+ 'custom_data_source: {',
+ ' name:"utilization"',
+ ' base_data_source:"sections"',
+ ' rewrite: {',
+ ' pattern:"unused_space"',
+ ' replacement:"Free space"',
+ ' }',
+ ' rewrite: {',
+ ' pattern:".*"',
+ ' replacement:"Used space"',
+ ' }',
+ '}',
+ '',
+ )
+ )
self.assertEqual(expected, config)
diff --git a/pw_bloat/py/label_test.py b/pw_bloat/py/label_test.py
new file mode 100644
index 000000000..2e2236839
--- /dev/null
+++ b/pw_bloat/py/label_test.py
@@ -0,0 +1,242 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for bloaty configuration tooling."""
+
+import unittest
+import os
+import logging
+import sys
+from pw_bloat.label import DataSourceMap, Label
+
+LIST_LABELS = [
+ Label(name='main()', size=30, parents=tuple(['FLASH', '.code'])),
+ Label(name='foo()', size=100, parents=tuple(['RAM', '.heap'])),
+ Label(name='bar()', size=220, parents=tuple(['RAM', '.heap'])),
+]
+
+logger = logging.getLogger()
+logger.level = logging.DEBUG
+logger.addHandler(logging.StreamHandler(sys.stdout))
+
+
+def get_test_map():
+ pw_root = os.environ.get("PW ROOT")
+ filename = f"{pw_root}/pigweed/pw_bloat/test_label.csv"
+ with open(filename, 'r') as csvfile:
+ ds_map = DataSourceMap.from_bloaty_tsv(csvfile)
+ capacity_patterns = [("^__TEXT$", 459), ("^_", 920834)]
+ for cap_pattern, cap_size in capacity_patterns:
+ ds_map.add_capacity(cap_pattern, cap_size)
+ return ds_map
+
+
+class LabelStructTest(unittest.TestCase):
+ """Testing class for the label structs."""
+
+ def test_data_source_total_size(self):
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ self.assertEqual(ds_map.get_total_size(), 0)
+
+ def test_data_source_single_insert_total_size(self):
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ ds_map.insert_label_hierachy(['FLASH', '.code', 'main()'], 30)
+ self.assertEqual(ds_map.get_total_size(), 30)
+
+ def test_data_source_multiple_insert_total_size(self):
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ ds_map.insert_label_hierachy(['FLASH', '.code', 'main()'], 30)
+ ds_map.insert_label_hierachy(['RAM', '.code', 'foo()'], 100)
+ self.assertEqual(ds_map.get_total_size(), 130)
+
+ def test_parsing_generator_three_datasource_names(self):
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS:
+ ds_map.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name], label.size
+ )
+ list_labels_three = [*LIST_LABELS, Label(name='total', size=350)]
+ for label_hiearchy in ds_map.labels():
+ self.assertIn(label_hiearchy, list_labels_three)
+ self.assertEqual(ds_map.get_total_size(), 350)
+
+ def test_parsing_generator_two_datasource_names(self):
+ ds_map = DataSourceMap(['a', 'b'])
+ ds_label_list = [
+ Label(name='main()', size=30, parents=tuple(['FLASH'])),
+ Label(name='foo()', size=100, parents=tuple(['RAM'])),
+ Label(name='bar()', size=220, parents=tuple(['RAM'])),
+ ]
+ for label in ds_label_list:
+ ds_map.insert_label_hierachy(
+ [label.parents[0], label.name], label.size
+ )
+ list_labels_two = [*ds_label_list, Label(name='total', size=350)]
+ for label_hiearchy in ds_map.labels():
+ self.assertIn(label_hiearchy, list_labels_two)
+ self.assertEqual(ds_map.get_total_size(), 350)
+
+ def test_parsing_generator_specified_datasource_1(self):
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS:
+ ds_map.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name], label.size
+ )
+ list_labels_ds_b = [
+ Label(name='.code', size=30, parents=tuple(['FLASH'])),
+ Label(name='.heap', size=320, parents=tuple(['RAM'])),
+ ]
+ list_labels_ds_b += [Label(name='total', size=350)]
+ for label_hiearchy in ds_map.labels(1):
+ self.assertIn(label_hiearchy, list_labels_ds_b)
+ self.assertEqual(ds_map.get_total_size(), 350)
+
+ def test_parsing_generator_specified_datasource_str_2(self):
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS:
+ ds_map.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name], label.size
+ )
+ list_labels_ds_a = [
+ Label(name='FLASH', size=30, parents=tuple([])),
+ Label(name='RAM', size=320, parents=tuple([])),
+ ]
+ list_labels_ds_a += [Label(name='total', size=350)]
+ for label_hiearchy in ds_map.labels(0):
+ self.assertIn(label_hiearchy, list_labels_ds_a)
+ self.assertEqual(ds_map.get_total_size(), 350)
+
+ def test_parsing_generator_specified_datasource_int(self):
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS:
+ ds_map.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name], label.size
+ )
+ list_labels_ds_a = [
+ Label(name='FLASH', size=30, parents=tuple([])),
+ Label(name='RAM', size=320, parents=tuple([])),
+ ]
+ list_labels_ds_a += [Label(name='total', size=350)]
+ for label_hiearchy in ds_map.labels(0):
+ self.assertIn(label_hiearchy, list_labels_ds_a)
+ self.assertEqual(ds_map.get_total_size(), 350)
+
+ def test_parsing_generator_specified_datasource_int_2(self):
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS:
+ ds_map.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name], label.size
+ )
+ list_labels_ds_b = [
+ Label(name='.code', size=30, parents=tuple(['FLASH'])),
+ Label(name='.heap', size=320, parents=tuple(['RAM'])),
+ ]
+ list_labels_ds_b += [Label(name='total', size=350)]
+ for label_hiearchy in ds_map.labels(1):
+ self.assertIn(label_hiearchy, list_labels_ds_b)
+ self.assertEqual(ds_map.get_total_size(), 350)
+
+ def test_diff_same_ds_labels_diff_sizes(self):
+ """Same map with different sizes."""
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS:
+ ds_map.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name], label.size
+ )
+
+ ds_map2 = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS:
+ ds_map2.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name],
+ label.size + 10,
+ )
+
+ list_labels_ds_b = [
+ Label(
+ name='main()',
+ size=-10,
+ exists_both=True,
+ parents=tuple(['FLASH', '.code']),
+ ),
+ Label(
+ name='foo()',
+ size=-10,
+ exists_both=True,
+ parents=tuple(['RAM', '.heap']),
+ ),
+ Label(
+ name='bar()',
+ size=-10,
+ exists_both=True,
+ parents=tuple(['RAM', '.heap']),
+ ),
+ ]
+
+ ds_map_diff = ds_map.diff(ds_map2)
+
+ for label_hiearchy in ds_map_diff.labels():
+ self.assertIn(label_hiearchy, list_labels_ds_b)
+
+ def test_diff_missing_ds_labels_diff_sizes(self):
+ """Different map with different sizes."""
+ ds_map = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS:
+ ds_map.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name], label.size
+ )
+
+ ds_map2 = DataSourceMap(['a', 'b', 'c'])
+ for label in LIST_LABELS[:-1]:
+ ds_map2.insert_label_hierachy(
+ [label.parents[0], label.parents[1], label.name],
+ label.size + 20,
+ )
+ ds_map2.insert_label_hierachy(
+ [label.parents[0], label.parents[1], 'foobar()'], label.size + 20
+ )
+
+ ds_map2.insert_label_hierachy(["LOAD #5", 'random_load', 'func()'], 250)
+
+ list_labels_ds_b = [
+ Label(
+ name='FLASH',
+ size=20,
+ capacity=None,
+ exists_both=True,
+ parents=(),
+ ),
+ Label(
+ name='RAM',
+ size=-80,
+ capacity=None,
+ exists_both=True,
+ parents=(),
+ ),
+ Label(
+ name='LOAD #5',
+ size=250,
+ capacity=None,
+ exists_both=False,
+ parents=(),
+ ),
+ ]
+
+ ds_map_diff = ds_map2.diff(ds_map)
+
+ for label_hiearchy in ds_map_diff.labels(0):
+ self.assertIn(label_hiearchy, list_labels_ds_b)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_bloat/py/pw_bloat/__main__.py b/pw_bloat/py/pw_bloat/__main__.py
new file mode 100644
index 000000000..30d3bb291
--- /dev/null
+++ b/pw_bloat/py/pw_bloat/__main__.py
@@ -0,0 +1,133 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Size reporting utilities."""
+
+import argparse
+import logging
+from pathlib import Path
+import sys
+from typing import Iterable
+
+from pw_bloat import bloat
+from pw_bloat.label import DataSourceMap
+from pw_bloat.label_output import BloatTableOutput
+import pw_cli.log
+
+_LOG = logging.getLogger(__name__)
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+
+ parser.add_argument(
+ 'binary',
+ help='Path to the ELF file to analyze',
+ metavar='BINARY',
+ type=Path,
+ )
+ parser.add_argument(
+ '-d',
+ '--data-sources',
+ help='Comma-separated list of Bloaty data sources to report',
+ type=lambda s: s.split(','),
+ default=('memoryregions,sections'),
+ )
+ parser.add_argument(
+ '--diff',
+ metavar='BINARY',
+ help='Run a size diff against a base binary file',
+ type=Path,
+ )
+ parser.add_argument(
+ '-v',
+ '--verbose',
+ help=('Print all log messages ' '(only errors are printed by default)'),
+ action='store_true',
+ )
+
+ return parser.parse_args()
+
+
+def _run_size_report(
+ elf: Path, data_sources: Iterable[str] = ()
+) -> DataSourceMap:
+ """Runs a size analysis on an ELF file, returning a pw_bloat size map.
+
+ Returns:
+ A size map of the labels in the binary under the provided data sources.
+
+ Raises:
+ NoMemoryRegions: The binary does not define bloat memory region symbols.
+ """
+
+ bloaty_tsv = bloat.memory_regions_size_report(
+ elf, data_sources=data_sources, extra_args=('--tsv',)
+ )
+
+ return DataSourceMap.from_bloaty_tsv(bloaty_tsv)
+
+
+def _no_memory_regions_error(elf: Path) -> None:
+ _LOG.error('Executable %s does not define any bloat memory regions', elf)
+ _LOG.error(
+ 'Refer to https://pigweed.dev/pw_bloat/#memoryregions-data-source'
+ )
+ _LOG.error('for information on how to configure them.')
+
+
+def _single_binary_report(elf: Path, data_sources: Iterable[str] = ()) -> int:
+ try:
+ data_source_map = _run_size_report(elf, data_sources)
+ except bloat.NoMemoryRegions:
+ _no_memory_regions_error(elf)
+ return 1
+
+ print(BloatTableOutput(data_source_map).create_table())
+ return 0
+
+
+def _diff_report(
+ target: Path, base: Path, data_sources: Iterable[str] = ()
+) -> int:
+ try:
+ base_map = _run_size_report(base, data_sources)
+ target_map = _run_size_report(target, data_sources)
+ except bloat.NoMemoryRegions as err:
+ _no_memory_regions_error(err.elf)
+ return 1
+
+ diff = target_map.diff(base_map)
+
+ print(BloatTableOutput(diff).create_table())
+ return 0
+
+
+def main() -> int:
+ """Run binary size reports."""
+
+ args = _parse_args()
+
+ if not args.verbose:
+ pw_cli.log.set_all_loggers_minimum_level(logging.ERROR)
+
+ if args.diff is not None:
+ return _diff_report(
+ args.binary, args.diff, data_sources=args.data_sources
+ )
+
+ return _single_binary_report(args.binary, data_sources=args.data_sources)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_bloat/py/pw_bloat/binary_diff.py b/pw_bloat/py/pw_bloat/binary_diff.py
deleted file mode 100644
index 75120517d..000000000
--- a/pw_bloat/py/pw_bloat/binary_diff.py
+++ /dev/null
@@ -1,85 +0,0 @@
-# Copyright 2019 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-"""The binary_diff module defines a class which stores size diff information."""
-
-import collections
-import csv
-
-from typing import List, Generator, Type
-
-DiffSegment = collections.namedtuple(
- 'DiffSegment', ['name', 'before', 'after', 'delta', 'capacity'])
-FormattedDiff = collections.namedtuple('FormattedDiff',
- ['segment', 'before', 'delta', 'after'])
-
-
-def format_integer(num: int, force_sign: bool = False) -> str:
- """Formats a integer with commas."""
- prefix = '+' if force_sign and num > 0 else ''
- return '{}{:,}'.format(prefix, num)
-
-
-def format_percent(num: float, force_sign: bool = False) -> str:
- """Formats a decimal ratio as a percentage."""
- prefix = '+' if force_sign and num > 0 else ''
- return '{}{:,.1f}%'.format(prefix, num * 100)
-
-
-class BinaryDiff:
- """A size diff between two binary files."""
- def __init__(self, label: str):
- self.label = label
- self._segments: collections.OrderedDict = collections.OrderedDict()
-
- def add_segment(self, segment: DiffSegment):
- """Adds a segment to the diff."""
- self._segments[segment.name] = segment
-
- def formatted_segments(self) -> Generator[FormattedDiff, None, None]:
- """Yields each of the segments in this diff with formatted data."""
-
- if not self._segments:
- yield FormattedDiff('(all)', '(same)', '0', '(same)')
- return
-
- has_diff_segment = False
-
- for segment in self._segments.values():
- if segment.delta == 0:
- continue
-
- has_diff_segment = True
- yield FormattedDiff(
- segment.name,
- format_integer(segment.before),
- format_integer(segment.delta, force_sign=True),
- format_integer(segment.after),
- )
-
- if not has_diff_segment:
- yield FormattedDiff('(all)', '(same)', '0', '(same)')
-
- @classmethod
- def from_csv(cls: Type['BinaryDiff'], label: str,
- raw_csv: List[str]) -> 'BinaryDiff':
- """Parses a BinaryDiff from bloaty's CSV output."""
-
- diff = cls(label)
- reader = csv.reader(raw_csv)
- for row in reader:
- diff.add_segment(
- DiffSegment(row[0], int(row[5]), int(row[7]), int(row[1]),
- int(row[3])))
-
- return diff
diff --git a/pw_bloat/py/pw_bloat/binary_size_aggregator.py b/pw_bloat/py/pw_bloat/binary_size_aggregator.py
new file mode 100644
index 000000000..d590c3c23
--- /dev/null
+++ b/pw_bloat/py/pw_bloat/binary_size_aggregator.py
@@ -0,0 +1,75 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""
+Collects binary size JSON outputs from bloat targets into a single file.
+"""
+
+import argparse
+import json
+import logging
+from pathlib import Path
+import sys
+
+from typing import Dict, List
+
+import pw_cli.log
+
+_LOG = logging.getLogger(__package__)
+
+
+def _parse_args() -> argparse.Namespace:
+ """Parses the script's arguments."""
+
+ parser = argparse.ArgumentParser(__doc__)
+ parser.add_argument(
+ '--output',
+ type=Path,
+ required=True,
+ help='Output JSON file',
+ )
+ parser.add_argument(
+ 'inputs',
+ type=Path,
+ nargs='+',
+ help='Input JSON files',
+ )
+
+ return parser.parse_args()
+
+
+def main(inputs: List[Path], output: Path) -> int:
+ all_data: Dict[str, int] = {}
+
+ for file in inputs:
+ try:
+ all_data |= json.loads(file.read_text())
+ except FileNotFoundError:
+ target_name = file.name.split('.')[0]
+ _LOG.error('')
+ _LOG.error('JSON input file %s does not exist', file)
+ _LOG.error('')
+ _LOG.error(
+ 'Check that the build target "%s" is a pw_size_report template',
+ target_name,
+ )
+ _LOG.error('')
+ return 1
+
+ output.write_text(json.dumps(all_data, sort_keys=True, indent=2))
+ return 0
+
+
+if __name__ == '__main__':
+ pw_cli.log.install()
+ sys.exit(main(**vars(_parse_args())))
diff --git a/pw_bloat/py/pw_bloat/bloat.py b/pw_bloat/py/pw_bloat/bloat.py
index d62f233db..d14c1c70b 100755
--- a/pw_bloat/py/pw_bloat/bloat.py
+++ b/pw_bloat/py/pw_bloat/bloat.py
@@ -1,4 +1,4 @@
-# Copyright 2019 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -16,68 +16,47 @@ bloat is a script which generates a size report card for binary files.
"""
import argparse
+import json
import logging
import os
+from pathlib import Path
import subprocess
import sys
-from typing import List, Iterable, Optional
+import tempfile
+from typing import Iterable, Optional
import pw_cli.log
-from pw_bloat.binary_diff import BinaryDiff
-from pw_bloat import bloat_output
+from pw_bloat.bloaty_config import generate_bloaty_config
+from pw_bloat.label import DataSourceMap, Label
+from pw_bloat.label_output import (
+ BloatTableOutput,
+ LineCharset,
+ RstOutput,
+ AsciiCharset,
+)
_LOG = logging.getLogger(__name__)
+MAX_COL_WIDTH = 50
+BINARY_SIZES_EXTENSION = '.binary_sizes.json'
+
def parse_args() -> argparse.Namespace:
"""Parses the script's arguments."""
- def delimited_list(delimiter: str, items: Optional[int] = None):
- def _parser(arg: str):
- args = arg.split(delimiter)
-
- if items and len(args) != items:
- raise argparse.ArgumentTypeError(
- 'Argument must be a '
- f'{delimiter}-delimited list with {items} items: "{arg}"')
-
- return args
-
- return _parser
-
- parser = argparse.ArgumentParser(
- 'Generate a size report card for binaries')
- parser.add_argument('--bloaty-config',
- type=delimited_list(';'),
- required=True,
- help='Data source configuration for Bloaty')
- parser.add_argument('--full',
- action='store_true',
- help='Display full bloat breakdown by symbol')
- parser.add_argument('--labels',
- type=delimited_list(';'),
- default='',
- help='Labels for output binaries')
- parser.add_argument('--out-dir',
- type=str,
- required=True,
- help='Directory in which to write output files')
- parser.add_argument('--target',
- type=str,
- required=True,
- help='Build target name')
- parser.add_argument('--title',
- type=str,
- default='pw_bloat',
- help='Report title')
- parser.add_argument('--source-filter',
- type=str,
- help='Bloaty data source filter')
- parser.add_argument('diff_targets',
- type=delimited_list(';', 2),
- nargs='+',
- metavar='DIFF_TARGET',
- help='Binary;base pairs to process')
+
+ parser = argparse.ArgumentParser('Generate a size report card for binaries')
+ parser.add_argument(
+ '--gn-arg-path',
+ type=str,
+ required=True,
+ help='File path to json of binaries',
+ )
+ parser.add_argument(
+ '--single-report',
+ action="store_true",
+ help='Determine if calling single size report',
+ )
return parser.parse_args()
@@ -87,7 +66,7 @@ def run_bloaty(
config: str,
base_file: Optional[str] = None,
data_sources: Iterable[str] = (),
- extra_args: Iterable[str] = ()
+ extra_args: Iterable[str] = (),
) -> bytes:
"""Executes a Bloaty size report on some binary file(s).
@@ -105,20 +84,22 @@ def run_bloaty(
subprocess.CalledProcessError: The Bloaty invocation failed.
"""
- # TODO(frolv): Point the default bloaty path to a prebuilt in Pigweed.
default_bloaty = 'bloaty'
bloaty_path = os.getenv('BLOATY_PATH', default_bloaty)
- # yapf: disable
cmd = [
bloaty_path,
- '-c', config,
- '-d', ','.join(data_sources),
- '--domain', 'vm',
+ '-c',
+ config,
+ '-d',
+ ','.join(data_sources),
+ '--domain',
+ 'vm',
+ '-n',
+ '0',
filename,
- *extra_args
+ *extra_args,
]
- # yapf: enable
if base_file is not None:
cmd.extend(['--', base_file])
@@ -126,82 +107,242 @@ def run_bloaty(
return subprocess.check_output(cmd)
-def main() -> int:
- """Program entry point."""
+class NoMemoryRegions(Exception):
+ """Exception raised if an ELF does not define any memory region symbols."""
- args = parse_args()
+ def __init__(self, elf: Path):
+ super().__init__(f'ELF {elf} does not define memory region symbols')
+ self.elf = elf
+
+
+def memory_regions_size_report(
+ elf: Path,
+ data_sources: Iterable[str] = (),
+ extra_args: Iterable[str] = (),
+) -> Iterable[str]:
+ """Runs a size report on an ELF file using pw_bloat memory region symbols.
+
+ Arguments:
+ elf: The ELF binary on which to run.
+ data_sources: Hierarchical data sources to display.
+ extra_args: Additional command line arguments forwarded to bloaty.
+
+ Returns:
+ The bloaty TSV output detailing the size report.
+
+ Raises:
+ NoMemoryRegions: The ELF does not define memory region symbols.
+ """
+ with tempfile.NamedTemporaryFile() as bloaty_config:
+ with open(elf.resolve(), "rb") as infile, open(
+ bloaty_config.name, "w"
+ ) as outfile:
+ result = generate_bloaty_config(
+ infile,
+ enable_memoryregions=True,
+ enable_utilization=False,
+ out_file=outfile,
+ )
+
+ if not result.has_memoryregions:
+ raise NoMemoryRegions(elf)
+
+ return (
+ run_bloaty(
+ str(elf.resolve()),
+ bloaty_config.name,
+ data_sources=data_sources,
+ extra_args=extra_args,
+ )
+ .decode('utf-8')
+ .splitlines()
+ )
+
+
+def write_file(filename: str, contents: str, out_dir_file: str) -> None:
+ path = os.path.join(out_dir_file, filename)
+ with open(path, 'w') as output_file:
+ output_file.write(contents)
+ _LOG.debug('Output written to %s', path)
+
+
+def create_binary_sizes_json(binary_name: str, labels: Iterable[Label]) -> str:
+ """Creates a binary_sizes.json file content from a list of labels.
+
+ Args:
+ binary_name: the single binary name to attribute segment sizes to.
+ labels: the label.Label content to include
- base_binaries: List[str] = []
- diff_binaries: List[str] = []
+ Returns:
+ a string of content to write to binary_sizes.json file.
+ """
+ json_content = {
+ f'{binary_name} {label.name}': label.size for label in labels
+ }
+ return json.dumps(json_content, sort_keys=True, indent=2)
+
+
+def single_target_output(
+ target: str,
+ bloaty_config: str,
+ target_out_file: str,
+ out_dir: str,
+ data_sources: Iterable[str],
+ extra_args: Iterable[str],
+) -> int:
+ """TODO(frolv) Add docstring."""
try:
- for binary, base in args.diff_targets:
- diff_binaries.append(binary)
- base_binaries.append(base)
- except RuntimeError as err:
- _LOG.error('%s: %s', sys.argv[0], err)
+ single_output = run_bloaty(
+ target,
+ bloaty_config,
+ data_sources=data_sources,
+ extra_args=extra_args,
+ )
+
+ except subprocess.CalledProcessError:
+ _LOG.error('%s: failed to run size report on %s', sys.argv[0], target)
return 1
- data_sources = ['segment_names']
- if args.full:
- data_sources.append('fullsymbols')
+ single_tsv = single_output.decode().splitlines()
+ single_report = BloatTableOutput(
+ DataSourceMap.from_bloaty_tsv(single_tsv), MAX_COL_WIDTH, LineCharset
+ )
+
+ data_source_map = DataSourceMap.from_bloaty_tsv(single_tsv)
+ rst_single_report = BloatTableOutput(
+ data_source_map,
+ MAX_COL_WIDTH,
+ AsciiCharset,
+ True,
+ )
+
+ single_report_table = single_report.create_table()
+
+ # Generates contents for top level summary for binary_sizes.json
+ binary_json_content = create_binary_sizes_json(
+ target, data_source_map.labels(ds_index=0)
+ )
+
+ print(single_report_table)
+ write_file(target_out_file, rst_single_report.create_table(), out_dir)
+ write_file(f'{target_out_file}.txt', single_report_table, out_dir)
+ write_file(
+ f'{target_out_file}{BINARY_SIZES_EXTENSION}',
+ binary_json_content,
+ out_dir,
+ )
+
+ return 0
- # TODO(frolv): CSV output is disabled for full reports as the default Bloaty
- # breakdown is printed. This script should be modified to print a custom
- # symbol breakdown in full reports.
- extra_args = [] if args.full else ['--csv']
- if args.source_filter:
- extra_args.extend(['--source-filter', args.source_filter])
- diffs: List[BinaryDiff] = []
- report = []
+def main() -> int:
+ """Program entry point."""
+
+ args = parse_args()
+ extra_args = ['--tsv']
+ data_sources = ['segment_names', 'symbols']
+ gn_arg_dict = {}
+ json_file = open(args.gn_arg_path)
+ gn_arg_dict = json.load(json_file)
+
+ if args.single_report:
+ single_binary_args = gn_arg_dict['binaries'][0]
+ if single_binary_args['source_filter']:
+ extra_args.extend(
+ ['--source-filter', single_binary_args['source_filter']]
+ )
+ if single_binary_args['data_sources']:
+ data_sources = single_binary_args['data_sources']
+
+ return single_target_output(
+ single_binary_args['target'],
+ single_binary_args['bloaty_config'],
+ gn_arg_dict['target_name'],
+ gn_arg_dict['out_dir'],
+ data_sources,
+ extra_args,
+ )
+
+ default_data_sources = ['segment_names', 'symbols']
+
+ diff_report = ''
+ rst_diff_report = ''
+ for curr_diff_binary in gn_arg_dict['binaries']:
+ curr_extra_args = extra_args.copy()
+ data_sources = default_data_sources
+
+ if curr_diff_binary['source_filter']:
+ curr_extra_args.extend(
+ ['--source-filter', curr_diff_binary['source_filter']]
+ )
+
+ if curr_diff_binary['data_sources']:
+ data_sources = curr_diff_binary['data_sources']
- for i, binary in enumerate(diff_binaries):
- binary_name = (args.labels[i]
- if i < len(args.labels) else os.path.basename(binary))
try:
- output = run_bloaty(binary, args.bloaty_config[i],
- base_binaries[i], data_sources, extra_args)
- if not output:
- continue
-
- # TODO(frolv): Remove when custom output for full mode is added.
- if args.full:
- report.append(binary_name)
- report.append('-' * len(binary_name))
- report.append(output.decode())
- continue
-
- # Ignore the first row as it displays column names.
- bloaty_csv = output.decode().splitlines()[1:]
- diffs.append(BinaryDiff.from_csv(binary_name, bloaty_csv))
+ single_output_base = run_bloaty(
+ curr_diff_binary['base'],
+ curr_diff_binary['bloaty_config'],
+ data_sources=data_sources,
+ extra_args=curr_extra_args,
+ )
+
+ except subprocess.CalledProcessError:
+ _LOG.error(
+ '%s: failed to run base size report on %s',
+ sys.argv[0],
+ curr_diff_binary["base"],
+ )
+ return 1
+
+ try:
+ single_output_target = run_bloaty(
+ curr_diff_binary['target'],
+ curr_diff_binary['bloaty_config'],
+ data_sources=data_sources,
+ extra_args=curr_extra_args,
+ )
+
except subprocess.CalledProcessError:
- _LOG.error('%s: failed to run diff on %s', sys.argv[0], binary)
+ _LOG.error(
+ '%s: failed to run target size report on %s',
+ sys.argv[0],
+ curr_diff_binary['target'],
+ )
return 1
- def write_file(filename: str, contents: str) -> None:
- path = os.path.join(args.out_dir, filename)
- with open(path, 'w') as output_file:
- output_file.write(contents)
- _LOG.debug('Output written to %s', path)
-
- # TODO(frolv): Remove when custom output for full mode is added.
- if not args.full:
- out = bloat_output.TableOutput(args.title,
- diffs,
- charset=bloat_output.LineCharset)
- report.append(out.diff())
-
- rst = bloat_output.RstOutput(diffs)
- write_file(f'{args.target}', rst.diff())
-
- complete_output = '\n'.join(report) + '\n'
- write_file(f'{args.target}.txt', complete_output)
- print(complete_output)
-
- # TODO(frolv): Remove when custom output for full mode is added.
- if args.full:
- write_file(f'{args.target}', complete_output)
+ if not single_output_target or not single_output_base:
+ continue
+
+ base_dsm = DataSourceMap.from_bloaty_tsv(
+ single_output_base.decode().splitlines()
+ )
+ target_dsm = DataSourceMap.from_bloaty_tsv(
+ single_output_target.decode().splitlines()
+ )
+ diff_dsm = target_dsm.diff(base_dsm)
+
+ diff_report += BloatTableOutput(
+ diff_dsm,
+ MAX_COL_WIDTH,
+ LineCharset,
+ diff_label=curr_diff_binary['label'],
+ ).create_table()
+
+ curr_rst_report = RstOutput(diff_dsm, curr_diff_binary['label'])
+ if rst_diff_report == '':
+ rst_diff_report = curr_rst_report.create_table()
+ else:
+ rst_diff_report += f"{curr_rst_report.add_report_row()}\n"
+
+ print(diff_report)
+ write_file(
+ gn_arg_dict['target_name'], rst_diff_report, gn_arg_dict['out_dir']
+ )
+ write_file(
+ f"{gn_arg_dict['target_name']}.txt", diff_report, gn_arg_dict['out_dir']
+ )
return 0
diff --git a/pw_bloat/py/pw_bloat/bloat_output.py b/pw_bloat/py/pw_bloat/bloat_output.py
deleted file mode 100644
index d203717aa..000000000
--- a/pw_bloat/py/pw_bloat/bloat_output.py
+++ /dev/null
@@ -1,212 +0,0 @@
-# Copyright 2019 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-"""Module containing different output formatters for the bloat script."""
-
-import abc
-import enum
-from typing import (Callable, Collection, Dict, List, Optional, Tuple, Type,
- Union)
-
-from pw_bloat.binary_diff import BinaryDiff, FormattedDiff
-
-
-class Output(abc.ABC):
- """An Output produces a size report card in a specific format."""
- def __init__(self,
- title: Optional[str],
- diffs: Collection[BinaryDiff] = ()):
- self._title = title
- self._diffs = diffs
-
- @abc.abstractmethod
- def diff(self) -> str:
- """Creates a report card for a size diff between binaries and a base."""
-
- @abc.abstractmethod
- def absolute(self) -> str:
- """Creates a report card for the absolute size breakdown of binaries."""
-
-
-class AsciiCharset(enum.Enum):
- """Set of ASCII characters for drawing tables."""
- TL = '+'
- TM = '+'
- TR = '+'
- ML = '+'
- MM = '+'
- MR = '+'
- BL = '+'
- BM = '+'
- BR = '+'
- V = '|'
- H = '-'
- HH = '='
-
-
-class LineCharset(enum.Enum):
- """Set of line-drawing characters for tables."""
- TL = '┌'
- TM = '┬'
- TR = '┐'
- ML = '├'
- MM = '┼'
- MR = '┤'
- BL = '└'
- BM = '┴'
- BR = '┘'
- V = '│'
- H = '─'
- HH = '═'
-
-
-def identity(val: str) -> str:
- """Returns a string unmodified."""
- return val
-
-
-class TableOutput(Output):
- """Tabular output."""
-
- LABEL_COLUMN = 'Label'
-
- def __init__(
- self,
- title: Optional[str],
- diffs: Collection[BinaryDiff] = (),
- charset: Union[Type[AsciiCharset],
- Type[LineCharset]] = AsciiCharset,
- preprocess: Callable[[str], str] = identity,
- # TODO(frolv): Make this a Literal type.
- justify: str = 'rjust'):
- self._cs = charset
- self._preprocess = preprocess
- self._justify = justify
-
- super().__init__(title, diffs)
-
- def diff(self) -> str:
- """Build a tabular diff output showing binary size deltas."""
-
- # Calculate the width of each column in the table.
- max_label = len(self.LABEL_COLUMN)
- column_widths = [len(field) for field in FormattedDiff._fields]
-
- for diff in self._diffs:
- max_label = max(max_label, len(diff.label))
- for segment in diff.formatted_segments():
- for i, val in enumerate(segment):
- val = self._preprocess(val)
- column_widths[i] = max(column_widths[i], len(val))
-
- separators = self._row_separators([max_label] + column_widths)
-
- def title_pad(string: str) -> str:
- padding = (len(separators['top']) - len(string)) // 2
- return ' ' * padding + string
-
- titles = [
- self._center_align(val.capitalize(), column_widths[i])
- for i, val in enumerate(FormattedDiff._fields)
- ]
- column_names = [self._center_align(self.LABEL_COLUMN, max_label)
- ] + titles
-
- rows: List[str] = []
-
- if self._title is not None:
- rows.extend([
- title_pad(self._title),
- title_pad(self._cs.H.value * len(self._title)),
- ])
-
- rows.extend([
- separators['top'],
- self._table_row(column_names),
- separators['hdg'],
- ])
-
- for row, diff in enumerate(self._diffs):
- subrows: List[str] = []
-
- for segment in diff.formatted_segments():
- subrow: List[str] = []
- label = diff.label if not subrows else ''
- subrow.append(getattr(label, self._justify)(max_label, ' '))
- subrow.extend([
- getattr(self._preprocess(val),
- self._justify)(column_widths[i], ' ')
- for i, val in enumerate(segment)
- ])
- subrows.append(self._table_row(subrow))
-
- rows.append('\n'.join(subrows))
- rows.append(separators['bot' if row == len(self._diffs) -
- 1 else 'mid'])
-
- return '\n'.join(rows)
-
- def absolute(self) -> str:
- return ''
-
- def _row_separators(self, column_widths: List[int]) -> Dict[str, str]:
- """Returns row separators for a table based on the character set."""
-
- # Left, middle, and right characters for each of the separator rows.
- top = (self._cs.TL.value, self._cs.TM.value, self._cs.TR.value)
- mid = (self._cs.ML.value, self._cs.MM.value, self._cs.MR.value)
- bot = (self._cs.BL.value, self._cs.BM.value, self._cs.BR.value)
-
- def sep(chars: Tuple[str, str, str], heading: bool = False) -> str:
- line = self._cs.HH.value if heading else self._cs.H.value
- lines = [line * width for width in column_widths]
- left = f'{chars[0]}{line}'
- mid = f'{line}{chars[1]}{line}'.join(lines)
- right = f'{line}{chars[2]}'
- return f'{left}{mid}{right}'
-
- return {
- 'top': sep(top),
- 'hdg': sep(mid, True),
- 'mid': sep(mid),
- 'bot': sep(bot),
- }
-
- def _table_row(self, vals: Collection[str]) -> str:
- """Formats a row of the table with the selected character set."""
- vert = self._cs.V.value
- main = f' {vert} '.join(vals)
- return f'{vert} {main} {vert}'
-
- @staticmethod
- def _center_align(val: str, width: int) -> str:
- """Left and right pads a value with spaces to center within a width."""
- space = width - len(val)
- padding = ' ' * (space // 2)
- extra = ' ' if space % 2 == 1 else ''
- return f'{extra}{padding}{val}{padding}'
-
-
-class RstOutput(TableOutput):
- """Tabular output in ASCII format, which is also valid RST."""
- def __init__(self, diffs: Collection[BinaryDiff] = ()):
- # Use RST line blocks within table cells to force each value to appear
- # on a new line in the HTML output.
- def add_rst_block(val: str) -> str:
- return f'| {val}'
-
- super().__init__(None,
- diffs,
- AsciiCharset,
- preprocess=add_rst_block,
- justify='ljust')
diff --git a/pw_bloat/py/pw_bloat/bloaty_config.py b/pw_bloat/py/pw_bloat/bloaty_config.py
index d5a38dc2a..39f7b9193 100644
--- a/pw_bloat/py/pw_bloat/bloaty_config.py
+++ b/pw_bloat/py/pw_bloat/bloaty_config.py
@@ -17,7 +17,7 @@ import argparse
import logging
import re
import sys
-from typing import BinaryIO, Dict, List, Optional, TextIO
+from typing import BinaryIO, Dict, List, NamedTuple, Optional, TextIO
import pw_cli.argument_types
from elftools.elf import elffile # type: ignore
@@ -26,8 +26,9 @@ _LOG = logging.getLogger('bloaty_config')
# 'pw_bloat_config_memory_region_NAME_{start,end}{_N,}' where _N defaults to 0.
_MEMORY_REGION_SYMBOL_RE = re.compile(
- r'pw_bloat_config_memory_region_' +
- r'(?P<name>\w+)_(?P<limit>(start|end))(_(?P<index>\d+))?')
+ r'pw_bloat_config_memory_region_'
+ + r'(?P<name>\w+)_(?P<limit>(start|end))(_(?P<index>\d+))?'
+)
def _parse_args() -> argparse.Namespace:
@@ -35,34 +36,56 @@ def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description='Generates useful bloaty configurations entries',
epilog='Hint: try this:\n'
- ' python -m pw_bloat.bloaty_config my_app.elf -o my_app.bloat')
+ ' python -m pw_bloat.bloaty_config my_app.elf -o my_app.bloat',
+ )
parser.add_argument('elf_file', type=argparse.FileType('rb'))
- parser.add_argument('--output',
- '-o',
- type=argparse.FileType('w'),
- help='The generated bloaty configuration',
- default=sys.stdout)
+ parser.add_argument(
+ '--output',
+ '-o',
+ type=argparse.FileType('w'),
+ help='The generated bloaty configuration',
+ default=sys.stdout,
+ )
parser.add_argument(
'--utilization',
- action=argparse.BooleanOptionalAction,
+ action='store_true',
+ dest='utilization',
default=True,
help=(
- 'Generate the utilization custom_data_source based on sections ' +
- 'with "unused_space" in anywhere in their name'))
+ 'Generate the utilization custom_data_source based on sections '
+ 'with "unused_space" in anywhere in their name'
+ ),
+ )
+ parser.add_argument(
+ '--no-utilization',
+ action='store_false',
+ dest='utilization',
+ )
+
parser.add_argument(
'--memoryregions',
- action=argparse.BooleanOptionalAction,
+ action='store_true',
default=True,
- help=('Generate the memoryregions custom_data_source based on ' +
- 'symbols defined in the linker script matching the following ' +
- 'pattern: ' +
- '"pw::bloat::config::memory_region::NAME[0].{start,end}"'))
- parser.add_argument('-l',
- '--loglevel',
- type=pw_cli.argument_types.log_level,
- default=logging.INFO,
- help='Set the log level'
- '(debug, info, warning, error, critical)')
+ help=(
+ 'Generate the memoryregions custom_data_source based on '
+ 'symbols defined in the linker script matching the following '
+ 'pattern: '
+ '"pw::bloat::config::memory_region::NAME[0].{start,end}"'
+ ),
+ )
+ parser.add_argument(
+ '--no-memoryregions',
+ action='store_false',
+ dest='memoryregions',
+ )
+
+ parser.add_argument(
+ '-l',
+ '--loglevel',
+ type=pw_cli.argument_types.log_level,
+ default=logging.INFO,
+ help='Set the log level' '(debug, info, warning, error, critical)',
+ )
return parser.parse_args()
@@ -134,12 +157,14 @@ def _parse_memory_regions(parsed_elf_file: elffile.ELFFile) -> Optional[Dict]:
for index, limits in ranges.items():
if 'start' not in limits:
missing_range_limits = True
- _LOG.error('%s[%d] is missing the start address', region_name,
- index)
+ _LOG.error(
+ '%s[%d] is missing the start address', region_name, index
+ )
if 'end' not in limits:
missing_range_limits = True
- _LOG.error('%s[%d] is missing the end address', region_name,
- index)
+ _LOG.error(
+ '%s[%d] is missing the end address', region_name, index
+ )
if missing_range_limits:
_LOG.error('Invalid memory regions detected: missing ranges')
return None
@@ -151,8 +176,10 @@ def _parse_memory_regions(parsed_elf_file: elffile.ELFFile) -> Optional[Dict]:
if region_name not in tupled_memory_regions:
tupled_memory_regions[region_name] = {}
for index, limits in ranges.items():
- tupled_memory_regions[region_name][index] = (limits['start'],
- limits['end'])
+ tupled_memory_regions[region_name][index] = (
+ limits['start'],
+ limits['end'],
+ )
# Ensure the memory regions do not overlap.
if _memory_regions_overlap(tupled_memory_regions):
@@ -191,27 +218,37 @@ def _memory_regions_overlap(memory_regions: Dict) -> bool:
"""Returns where any memory regions overlap each other."""
overlaps_detected = False
for current_name, current_ranges in memory_regions.items():
- for current_index, (current_start,
- current_end) in current_ranges.items():
+ for current_index, (
+ current_start,
+ current_end,
+ ) in current_ranges.items():
for other_name, other_ranges in memory_regions.items():
- for other_index, (other_start,
- other_end) in other_ranges.items():
- if (current_name == other_name
- and current_index == other_index):
+ for other_index, (
+ other_start,
+ other_end,
+ ) in other_ranges.items():
+ if (
+ current_name == other_name
+ and current_index == other_index
+ ):
continue # Skip yourself.
# Check if the other region end is within this region.
- other_end_overlaps = (current_start < other_end <=
- current_end)
- other_start_overlaps = (current_start <= other_start <
- current_end)
+ other_end_overlaps = (
+ current_start < other_end <= current_end
+ )
+ other_start_overlaps = (
+ current_start <= other_start < current_end
+ )
if other_end_overlaps or other_start_overlaps:
overlaps_detected = True
- _LOG.error(f'error: {current_name}[{current_index}] ' +
- f'[{hex(current_start)},' +
- f'{hex(current_end)}] overlaps with ' +
- f'{other_name}[{other_index}] '
- f'[{hex(other_start)},' +
- f'{hex(other_end)}] overlaps with ')
+ _LOG.error(
+ f'error: {current_name}[{current_index}] '
+ + f'[{hex(current_start)},'
+ + f'{hex(current_end)}] overlaps with '
+ + f'{other_name}[{other_index}] '
+ f'[{hex(other_start)},'
+ + f'{hex(other_end)}] overlaps with '
+ )
return overlaps_detected
@@ -230,12 +267,14 @@ def _get_segments_to_memory_region_map(elf_file: BinaryIO) -> Optional[Dict]:
segments = _parse_segments(parsed_elf_file)
- return map_segments_to_memory_regions(segments=segments,
- memory_regions=memory_regions)
+ return map_segments_to_memory_regions(
+ segments=segments, memory_regions=memory_regions
+ )
-def map_segments_to_memory_regions(segments: Dict,
- memory_regions: Dict) -> Dict:
+def map_segments_to_memory_regions(
+ segments: Dict, memory_regions: Dict
+) -> Dict:
"""
Maps segments to the virtual memory regions they reside in.
@@ -255,16 +294,21 @@ def map_segments_to_memory_regions(segments: Dict,
for segment, (segment_start, segment_end) in segments.items():
# Note this is the final filter bloaty rewrite pattern format.
for memory_region_name, memory_region_info in memory_regions.items():
- for _, (subregion_start,
- subregion_end) in memory_region_info.items():
- if (segment_start >= subregion_start
- and segment_end <= subregion_end):
+ for _, (
+ subregion_start,
+ subregion_end,
+ ) in memory_region_info.items():
+ if (
+ segment_start >= subregion_start
+ and segment_end <= subregion_end
+ ):
# We found the subregion the segment resides in.
segment_to_memory_region[segment] = memory_region_name
if segment not in segment_to_memory_region:
_LOG.error(
- f'Error: Failed to find memory region for LOAD #{segment} ' +
- f'[{hex(segment_start)},{hex(segment_end)}]')
+ f'Error: Failed to find memory region for LOAD #{segment} '
+ + f'[{hex(segment_start)},{hex(segment_end)}]'
+ )
return segment_to_memory_region
@@ -304,8 +348,26 @@ def generate_utilization_data_source() -> str:
return '\n'.join(output) + '\n'
-def generate_bloaty_config(elf_file: BinaryIO, enable_memoryregions: bool,
- enable_utilization: bool, out_file: TextIO) -> None:
+class BloatyConfigResult(NamedTuple):
+ has_memoryregions: bool
+ has_utilization: bool
+
+
+def generate_bloaty_config(
+ elf_file: BinaryIO,
+ enable_memoryregions: bool,
+ enable_utilization: bool,
+ out_file: TextIO,
+) -> BloatyConfigResult:
+ """Generates a Bloaty config file from symbols within an ELF.
+
+ Returns:
+ Tuple indicating whether a memoryregions data source, a utilization data
+ source, or both were written.
+ """
+
+ result = [False, False]
+
if enable_memoryregions:
# Enable the "memoryregions" data_source if the user provided the
# required pw_bloat specific symbols in their linker script.
@@ -315,11 +377,16 @@ def generate_bloaty_config(elf_file: BinaryIO, enable_memoryregions: bool,
else:
_LOG.info('memoryregions data_source is provided')
out_file.write(
- generate_memoryregions_data_source(segment_to_memory_region))
+ generate_memoryregions_data_source(segment_to_memory_region)
+ )
+ result[0] = True
if enable_utilization:
_LOG.info('utilization data_source is provided')
out_file.write(generate_utilization_data_source())
+ result[1] = True
+
+ return BloatyConfigResult(*result)
def main() -> int:
@@ -328,10 +395,12 @@ def main() -> int:
logging.basicConfig(format='%(message)s', level=args.loglevel)
- generate_bloaty_config(elf_file=args.elf_file,
- enable_memoryregions=args.memoryregions,
- enable_utilization=args.utilization,
- out_file=args.output)
+ generate_bloaty_config(
+ elf_file=args.elf_file,
+ enable_memoryregions=args.memoryregions,
+ enable_utilization=args.utilization,
+ out_file=args.output,
+ )
return 0
diff --git a/pw_bloat/py/pw_bloat/label.py b/pw_bloat/py/pw_bloat/label.py
new file mode 100644
index 000000000..9a12610e7
--- /dev/null
+++ b/pw_bloat/py/pw_bloat/label.py
@@ -0,0 +1,290 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""
+The label module defines a class to store and manipulate size reports.
+"""
+
+from collections import defaultdict
+from dataclasses import dataclass
+from typing import Iterable, Dict, Sequence, Tuple, List, Optional
+import csv
+
+
+@dataclass
+class Label:
+ """Return type of DataSourceMap generator."""
+
+ name: str
+ size: int
+ capacity: Optional[int] = None
+ exists_both: Optional[bool] = None
+ parents: Tuple[str, ...] = ()
+
+ def is_new(self) -> bool:
+ return (not self.exists_both) and self.size > 0
+
+ def is_del(self) -> bool:
+ return (not self.exists_both) and self.size < 0
+
+
+@dataclass
+class LabelInfo:
+ size: int = 0
+ capacity: Optional[int] = None
+ exists_both: Optional[bool] = None
+
+
+class _LabelMap:
+ """Private module to hold parent and child labels with their size."""
+
+ _label_map: Dict[str, Dict[str, LabelInfo]]
+
+ def __init__(self):
+ self._label_map = defaultdict(lambda: defaultdict(LabelInfo))
+
+ def remove(
+ self, parent_label: str, child_label: Optional[str] = None
+ ) -> None:
+ """Delete entire parent label or the child label."""
+ if child_label:
+ del self._label_map[parent_label][child_label]
+ else:
+ del self._label_map[parent_label]
+
+ def __getitem__(self, parent_label: str) -> Dict[str, LabelInfo]:
+ """Indexing LabelMap using '[]' operators by specifying a label."""
+ return self._label_map[parent_label]
+
+ def __contains__(self, parent_label: str) -> bool:
+ return parent_label in self._label_map
+
+ def map_generator(self) -> Iterable[Tuple[str, Dict[str, LabelInfo]]]:
+ for parent_label, label_dict in self._label_map.items():
+ yield parent_label, label_dict
+
+
+class _DataSource:
+ """Private module to store a data source name with a _LabelMap."""
+
+ def __init__(self, name: str):
+ self._name = name
+ self._ds_label_map = _LabelMap()
+
+ def get_name(self) -> str:
+ return self._name
+
+ def add_label(
+ self,
+ parent_label: str,
+ child_label: str,
+ size: int,
+ diff_exist: Optional[bool] = None,
+ ) -> None:
+ curr_label_info = self._ds_label_map[parent_label][child_label]
+ curr_label_info.size += size
+ if curr_label_info.exists_both is None:
+ curr_label_info.exists_both = diff_exist
+
+ def __getitem__(self, parent_label: str) -> Dict[str, LabelInfo]:
+ return self._ds_label_map[parent_label]
+
+ def __contains__(self, parent_label: str) -> bool:
+ return parent_label in self._ds_label_map
+
+ def label_map_generator(self) -> Iterable[Tuple[str, Dict[str, LabelInfo]]]:
+ for parent_label, label_dict in self._ds_label_map.map_generator():
+ yield parent_label, label_dict
+
+
+class DataSourceMap:
+ """Module to store an array of DataSources and capacities.
+
+ An organize way to store a hierachy of labels and their sizes.
+ Includes a capacity array to hold regex patterns for applying
+ capacities to matching label names.
+
+ """
+
+ _BASE_TOTAL_LABEL = 'total'
+
+ @classmethod
+ def from_bloaty_tsv(cls, raw_tsv: Iterable[str]) -> 'DataSourceMap':
+ """Read in Bloaty TSV output and store in DataSourceMap."""
+ reader = csv.reader(raw_tsv, delimiter='\t')
+ top_row = next(reader)
+ vmsize_index = top_row.index('vmsize')
+ ds_map_tsv = cls(top_row[:vmsize_index])
+ for row in reader:
+ ds_map_tsv.insert_label_hierachy(
+ row[:vmsize_index], int(row[vmsize_index])
+ )
+ return ds_map_tsv
+
+ def __init__(self, data_sources_names: Iterable[str]):
+ self._data_sources = list(
+ _DataSource(name) for name in ['base', *data_sources_names]
+ )
+ self._capacity_array: List[Tuple[str, int]] = []
+
+ def label_exists(
+ self, ds_index: int, parent_label: str, child_label: str
+ ) -> bool:
+ return (parent_label in self._data_sources[ds_index]) and (
+ child_label in self._data_sources[ds_index][parent_label]
+ )
+
+ def insert_label_hierachy(
+ self,
+ label_hierarchy: Iterable[str],
+ size: int,
+ diff_exist: Optional[bool] = None,
+ ) -> None:
+ """Insert a hierachy of labels with its size."""
+
+ # Insert initial '__base__' data source that holds the
+ # running total size.
+ self._data_sources[0].add_label(
+ '__base__', self._BASE_TOTAL_LABEL, size
+ )
+ complete_label_hierachy = [self._BASE_TOTAL_LABEL, *label_hierarchy]
+ for index in range(len(complete_label_hierachy) - 1):
+ if complete_label_hierachy[index]:
+ self._data_sources[index + 1].add_label(
+ complete_label_hierachy[index],
+ complete_label_hierachy[index + 1],
+ size,
+ diff_exist,
+ )
+
+ def add_capacity(self, regex_pattern: str, capacity: int) -> None:
+ """Insert regex pattern and capacity into dictionary."""
+ self._capacity_array.append((regex_pattern, capacity))
+
+ def diff(self, base: 'DataSourceMap') -> 'DiffDataSourceMap':
+ """Calculate the difference between 2 DataSourceMaps."""
+ diff_dsm = DiffDataSourceMap(self.get_ds_names())
+ curr_parent = self._BASE_TOTAL_LABEL
+
+ # Iterate through base labels at each datasource index.
+ last_data_source = len(base.get_ds_names()) - 1
+ parent_data_source_index = last_data_source + 1
+ for b_label in base.labels(last_data_source):
+ if last_data_source > 0:
+ curr_parent = b_label.parents[-1]
+ lb_hierachy_names = [*b_label.parents, b_label.name]
+
+ # Check if label exists in target binary DataSourceMap.
+ # Subtract base from target size and insert diff size
+ # into DiffDataSourceMap.
+ if self.label_exists(
+ parent_data_source_index, curr_parent, b_label.name
+ ):
+ diff_size = (
+ self._data_sources[parent_data_source_index][curr_parent][
+ b_label.name
+ ].size
+ ) - b_label.size
+
+ if diff_size:
+ diff_dsm.insert_label_hierachy(
+ lb_hierachy_names, diff_size, True
+ )
+ else:
+ diff_dsm.insert_label_hierachy(lb_hierachy_names, 0, True)
+
+ # label is not present in target - insert with negative size
+ else:
+ diff_dsm.insert_label_hierachy(
+ lb_hierachy_names, -1 * b_label.size, False
+ )
+
+ # Iterate through all of target labels
+ # to find labels new to target from base.
+ for t_label in self.labels(last_data_source):
+ if last_data_source > 0:
+ curr_parent = t_label.parents[-1]
+
+ # New addition to target
+ if not base.label_exists(
+ parent_data_source_index, curr_parent, t_label.name
+ ):
+ diff_dsm.insert_label_hierachy(
+ [*t_label.parents, f"{t_label.name}"], t_label.size, False
+ )
+
+ return diff_dsm
+
+ def get_total_size(self) -> int:
+ return self._data_sources[0]['__base__'][self._BASE_TOTAL_LABEL].size
+
+ def get_ds_names(self) -> Tuple[str, ...]:
+ """List of DataSource names for easy indexing and reference."""
+ return tuple(
+ data_source.get_name() for data_source in self._data_sources[1:]
+ )
+
+ def labels(self, ds_index: Optional[int] = None) -> Iterable[Label]:
+ """Generator that yields a Label depending on specified data source.
+
+ Args:
+ ds_index: Integer index of target data source.
+
+ Returns:
+ Iterable Label objects.
+ """
+ ds_index = len(self._data_sources) if ds_index is None else ds_index + 2
+ yield from self._per_data_source_generator(
+ tuple(), self._data_sources[1:ds_index]
+ )
+
+ def _per_data_source_generator(
+ self,
+ parent_labels: Tuple[str, ...],
+ data_sources: Sequence[_DataSource],
+ ) -> Iterable[Label]:
+ """Recursive generator to return Label based off parent labels."""
+ for ds_index, curr_ds in enumerate(data_sources):
+ for parent_label, label_map in curr_ds.label_map_generator():
+ if not parent_labels:
+ curr_parent = self._BASE_TOTAL_LABEL
+ else:
+ curr_parent = parent_labels[-1]
+ if parent_label == curr_parent:
+ for child_label, label_info in label_map.items():
+ if len(data_sources) == 1:
+ yield Label(
+ child_label,
+ label_info.size,
+ parents=parent_labels,
+ exists_both=label_info.exists_both,
+ )
+ else:
+ yield from self._per_data_source_generator(
+ (*parent_labels, child_label),
+ data_sources[ds_index + 1 :],
+ )
+
+
+class DiffDataSourceMap(DataSourceMap):
+ """DataSourceMap that holds diff information."""
+
+ def has_diff_sublabels(self, top_ds_label: str) -> bool:
+ """Checks if first datasource is identical."""
+ for label in self.labels():
+ if label.size != 0:
+ if (label.parents and (label.parents[0] == top_ds_label)) or (
+ label.name == top_ds_label
+ ):
+ return True
+ return False
diff --git a/pw_bloat/py/pw_bloat/label_output.py b/pw_bloat/py/pw_bloat/label_output.py
new file mode 100644
index 000000000..938cc90e2
--- /dev/null
+++ b/pw_bloat/py/pw_bloat/label_output.py
@@ -0,0 +1,625 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Module for size report ASCII tables from DataSourceMaps."""
+
+import enum
+from typing import (
+ Iterable,
+ Tuple,
+ Union,
+ Type,
+ List,
+ Optional,
+ NamedTuple,
+ cast,
+)
+
+from pw_bloat.label import DataSourceMap, DiffDataSourceMap, Label
+
+
+class AsciiCharset(enum.Enum):
+ """Set of ASCII characters for drawing tables."""
+
+ TL = '+'
+ TM = '+'
+ TR = '+'
+ ML = '+'
+ MM = '+'
+ MR = '+'
+ BL = '+'
+ BM = '+'
+ BR = '+'
+ V = '|'
+ H = '-'
+ HH = '='
+
+
+class LineCharset(enum.Enum):
+ """Set of line-drawing characters for tables."""
+
+ TL = '┌'
+ TM = '┬'
+ TR = '┐'
+ ML = '├'
+ MM = '┼'
+ MR = '┤'
+ BL = '└'
+ BM = '┴'
+ BR = '┘'
+ V = '│'
+ H = '─'
+ HH = '═'
+
+
+class _Align(enum.Enum):
+ CENTER = 0
+ LEFT = 1
+ RIGHT = 2
+
+
+def get_label_status(curr_label: Label) -> str:
+ if curr_label.is_new():
+ return 'NEW'
+ if curr_label.is_del():
+ return 'DEL'
+ return ''
+
+
+def diff_sign_sizes(size: int, diff_mode: bool) -> str:
+ if diff_mode:
+ size_sign = '+' if size > 0 else ''
+ return f"{size_sign}{size:,}"
+ return f"{size:,}"
+
+
+class BloatTableOutput:
+ """ASCII Table generator from DataSourceMap."""
+
+ _RST_PADDING_WIDTH = 6
+ _DEFAULT_MAX_WIDTH = 80
+
+ class _LabelContent(NamedTuple):
+ name: str
+ size: int
+ label_status: str
+
+ def __init__(
+ self,
+ ds_map: Union[DiffDataSourceMap, DataSourceMap],
+ col_max_width: int = _DEFAULT_MAX_WIDTH,
+ charset: Union[Type[AsciiCharset], Type[LineCharset]] = AsciiCharset,
+ rst_output: bool = False,
+ diff_label: Optional[str] = None,
+ ):
+ self._data_source_map = ds_map
+ self._cs = charset
+ self._total_size = 0
+ col_names = [*self._data_source_map.get_ds_names(), 'sizes']
+ self._diff_mode = False
+ self._diff_label = diff_label
+ if isinstance(self._data_source_map, DiffDataSourceMap):
+ col_names = ['diff', *col_names]
+ self._diff_mode = True
+ self._col_names = col_names
+ self._additional_padding = 0
+ self._ascii_table_rows: List[str] = []
+ self._rst_output = rst_output
+ self._total_divider = self._cs.HH.value
+ if self._rst_output:
+ self._total_divider = self._cs.H.value
+ self._additional_padding = self._RST_PADDING_WIDTH
+
+ self._col_widths = self._generate_col_width(col_max_width)
+
+ def _generate_col_width(self, col_max_width: int) -> List[int]:
+ """Find column width for all data sources and sizes."""
+ max_len_size = 0
+ diff_len_col_width = 0
+
+ col_list = [
+ len(ds_name) for ds_name in self._data_source_map.get_ds_names()
+ ]
+ for curr_label in self._data_source_map.labels():
+ self._total_size += curr_label.size
+ max_len_size = max(
+ len(diff_sign_sizes(self._total_size, self._diff_mode)),
+ len(diff_sign_sizes(curr_label.size, self._diff_mode)),
+ max_len_size,
+ )
+ for index, parent_label in enumerate(
+ [*curr_label.parents, curr_label.name]
+ ):
+ if len(parent_label) > col_max_width:
+ col_list[index] = col_max_width
+ elif len(parent_label) > col_list[index]:
+ col_list[index] = len(parent_label)
+
+ diff_same = 0
+ if self._diff_mode:
+ col_list = [len('Total'), *col_list]
+ diff_same = len('(SAME)')
+ col_list.append(max(max_len_size, len('sizes'), diff_same))
+
+ if self._diff_label is not None:
+ sum_all_col_names = sum(col_list)
+ if sum_all_col_names < len(self._diff_label):
+ diff_len_col_width = (
+ len(self._diff_label) - sum_all_col_names
+ ) // len(self._col_names)
+
+ return [
+ (x + self._additional_padding + diff_len_col_width)
+ for x in col_list
+ ]
+
+ def _diff_label_names(
+ self,
+ old_labels: Optional[Tuple[_LabelContent, ...]],
+ new_labels: Tuple[_LabelContent, ...],
+ ) -> Tuple[_LabelContent, ...]:
+ """Return difference between arrays of labels."""
+
+ if old_labels is None:
+ return new_labels
+ diff_list = []
+ for new_lb, old_lb in zip(new_labels, old_labels):
+ if (new_lb.name == old_lb.name) and (new_lb.size == old_lb.size):
+ diff_list.append(self._LabelContent('', 0, ''))
+ else:
+ diff_list.append(new_lb)
+
+ return tuple(diff_list)
+
+ def _label_title_row(self) -> List[str]:
+ label_rows = []
+ label_cells = ''
+ divider_cells = ''
+ for width in self._col_widths:
+ label_cells += ' ' * width + ' '
+ divider_cells += (self._cs.H.value * width) + self._cs.H.value
+ if self._diff_label is not None:
+ label_cells = self._diff_label.center(len(label_cells[:-1]), ' ')
+ label_rows.extend(
+ [
+ f"{self._cs.TL.value}{divider_cells[:-1]}{self._cs.TR.value}",
+ f"{self._cs.V.value}{label_cells}{self._cs.V.value}",
+ f"{self._cs.ML.value}{divider_cells[:-1]}{self._cs.MR.value}",
+ ]
+ )
+ return label_rows
+
+ def create_table(self) -> str:
+ """Parse DataSourceMap to create ASCII table."""
+ curr_lb_hierachy = None
+ last_diff_name = ''
+ if self._diff_mode:
+ self._ascii_table_rows.extend([*self._label_title_row()])
+ else:
+ self._ascii_table_rows.extend(
+ [self._create_border(True, self._cs.H.value)]
+ )
+ self._ascii_table_rows.extend([*self._create_title_row()])
+
+ has_entries = False
+
+ for curr_label in self._data_source_map.labels():
+ if curr_label.size == 0:
+ continue
+
+ has_entries = True
+
+ new_lb_hierachy = tuple(
+ [
+ *self._get_ds_label_size(curr_label.parents),
+ self._LabelContent(
+ curr_label.name,
+ curr_label.size,
+ get_label_status(curr_label),
+ ),
+ ]
+ )
+ diff_list = self._diff_label_names(
+ curr_lb_hierachy, new_lb_hierachy
+ )
+ curr_lb_hierachy = new_lb_hierachy
+
+ if curr_label.parents and curr_label.parents[0] == last_diff_name:
+ continue
+ if (
+ self._diff_mode
+ and diff_list[0].name
+ and (
+ not cast(
+ DiffDataSourceMap, self._data_source_map
+ ).has_diff_sublabels(diff_list[0].name)
+ )
+ ):
+ if (len(self._ascii_table_rows) > 5) and (
+ self._ascii_table_rows[-1][0] != '+'
+ ):
+ self._ascii_table_rows.append(
+ self._row_divider(
+ len(self._col_names), self._cs.H.value
+ )
+ )
+ self._ascii_table_rows.append(
+ self._create_same_label_row(1, diff_list[0].name)
+ )
+
+ last_diff_name = curr_label.parents[0]
+ else:
+ self._ascii_table_rows += self._create_diff_rows(diff_list)
+
+ if self._rst_output and self._ascii_table_rows[-1][0] == '+':
+ self._ascii_table_rows.pop()
+
+ self._ascii_table_rows.extend(
+ [*self._create_total_row(is_empty=not has_entries)]
+ )
+
+ return '\n'.join(self._ascii_table_rows) + '\n'
+
+ def _create_same_label_row(self, col_index: int, label: str) -> str:
+ label_row = ''
+ for col in range(len(self._col_names) - 1):
+ if col == col_index:
+ curr_cell = self._create_cell(label, False, col, _Align.LEFT)
+ else:
+ curr_cell = self._create_cell('', False, col)
+ label_row += curr_cell
+ label_row += self._create_cell(
+ "(SAME)", True, len(self._col_widths) - 1, _Align.RIGHT
+ )
+ return label_row
+
+ def _get_ds_label_size(
+ self, parent_labels: Tuple[str, ...]
+ ) -> Iterable[_LabelContent]:
+ """Produce label, size pairs from parent label names."""
+ parent_label_sizes = []
+ for index, target_label in enumerate(parent_labels):
+ for curr_label in self._data_source_map.labels(index):
+ if curr_label.name == target_label:
+ diff_label = get_label_status(curr_label)
+ parent_label_sizes.append(
+ self._LabelContent(
+ curr_label.name, curr_label.size, diff_label
+ )
+ )
+ break
+ return parent_label_sizes
+
+ def _create_total_row(self, is_empty: bool = False) -> Iterable[str]:
+ complete_total_rows = []
+
+ if self._diff_mode and is_empty:
+ # When diffing two identical binaries, output a row indicating that
+ # the two are the same.
+ no_diff_row = ''
+ for i in range(len(self._col_names)):
+ if i == 0:
+ no_diff_row += self._create_cell(
+ 'N/A', False, i, _Align.CENTER
+ )
+ elif i == len(self._col_names) - 1:
+ no_diff_row += self._create_cell('0', True, i)
+ else:
+ no_diff_row += self._create_cell(
+ '(same)', False, i, _Align.CENTER
+ )
+ complete_total_rows.append(no_diff_row)
+
+ complete_total_rows.append(
+ self._row_divider(len(self._col_names), self._total_divider)
+ )
+ total_row = ''
+
+ for i in range(len(self._col_names)):
+ if i == 0:
+ total_row += self._create_cell('Total', False, i, _Align.LEFT)
+ elif i == len(self._col_names) - 1:
+ total_size_str = diff_sign_sizes(
+ self._total_size, self._diff_mode
+ )
+ total_row += self._create_cell(total_size_str, True, i)
+ else:
+ total_row += self._create_cell('', False, i, _Align.CENTER)
+
+ complete_total_rows.extend(
+ [total_row, self._create_border(False, self._cs.H.value)]
+ )
+ return complete_total_rows
+
+ def _create_diff_rows(
+ self, diff_list: Tuple[_LabelContent, ...]
+ ) -> Iterable[str]:
+ """Create rows for each label according to its index in diff_list."""
+ curr_row = ''
+ diff_index = 0
+ diff_rows = []
+ for index, label_content in enumerate(diff_list):
+ if label_content.name:
+ if self._diff_mode:
+ curr_row += self._create_cell(
+ label_content.label_status, False, 0
+ )
+ diff_index = 1
+ for cell_index in range(
+ diff_index, len(diff_list) + diff_index
+ ):
+ if cell_index == index + diff_index:
+ if (
+ cell_index == diff_index
+ and len(self._ascii_table_rows) > 5
+ and not self._rst_output
+ ):
+ diff_rows.append(
+ self._row_divider(
+ len(self._col_names), self._cs.H.value
+ )
+ )
+ if (
+ len(label_content.name) + self._additional_padding
+ ) > self._col_widths[cell_index]:
+ curr_row = self._multi_row_label(
+ label_content.name, cell_index
+ )
+ break
+ curr_row += self._create_cell(
+ label_content.name, False, cell_index, _Align.LEFT
+ )
+ else:
+ curr_row += self._create_cell('', False, cell_index)
+
+ # Add size end of current row.
+ curr_size = diff_sign_sizes(label_content.size, self._diff_mode)
+ curr_row += self._create_cell(
+ curr_size, True, len(self._col_widths) - 1, _Align.RIGHT
+ )
+ diff_rows.append(curr_row)
+ if self._rst_output:
+ diff_rows.append(
+ self._row_divider(
+ len(self._col_names), self._cs.H.value
+ )
+ )
+ curr_row = ''
+
+ return diff_rows
+
+ def _create_cell(
+ self,
+ content: str,
+ last_cell: bool,
+ col_index: int,
+ align: Optional[_Align] = _Align.RIGHT,
+ ) -> str:
+ v_border = self._cs.V.value
+ if self._rst_output and content:
+ content = content.replace('_', '\\_')
+ pad_diff = self._col_widths[col_index] - len(content)
+ padding = (pad_diff // 2) * ' '
+ odd_pad = ' ' if pad_diff % 2 == 1 else ''
+ string_cell = ''
+
+ if align == _Align.CENTER:
+ string_cell = f'{v_border}{odd_pad}{padding}{content}{padding}'
+ elif align == _Align.LEFT:
+ string_cell = f'{v_border}{content}{padding*2}{odd_pad}'
+ elif align == _Align.RIGHT:
+ string_cell = f'{v_border}{padding*2}{odd_pad}{content}'
+
+ if last_cell:
+ string_cell += self._cs.V.value
+ return string_cell
+
+ def _multi_row_label(self, content: str, target_col_index: int) -> str:
+ """Split content name into multiple rows within correct column."""
+ max_len = self._col_widths[target_col_index] - self._additional_padding
+ split_content = '...'.join(
+ content[max_len:][i : i + max_len - 3]
+ for i in range(0, len(content[max_len:]), max_len - 3)
+ )
+ split_content = f"{content[:max_len]}...{split_content}"
+ split_tab_content = [
+ split_content[i : i + max_len]
+ for i in range(0, len(split_content), max_len)
+ ]
+ multi_label = []
+ curr_row = ''
+ for index, cut_content in enumerate(split_tab_content):
+ last_cell = False
+ for blank_cell_index in range(len(self._col_names)):
+ if blank_cell_index == target_col_index:
+ curr_row += self._create_cell(
+ cut_content, False, target_col_index, _Align.LEFT
+ )
+ else:
+ if blank_cell_index == len(self._col_names) - 1:
+ if index == len(split_tab_content) - 1:
+ break
+ last_cell = True
+ curr_row += self._create_cell(
+ '', last_cell, blank_cell_index
+ )
+ multi_label.append(curr_row)
+ curr_row = ''
+
+ return '\n'.join(multi_label)
+
+ def _row_divider(self, col_num: int, h_div: str) -> str:
+ l_border = ''
+ r_border = ''
+ row_div = ''
+ for col in range(col_num):
+ if col == 0:
+ l_border = self._cs.ML.value
+ r_border = ''
+ elif col == (col_num - 1):
+ l_border = self._cs.MM.value
+ r_border = self._cs.MR.value
+ else:
+ l_border = self._cs.MM.value
+ r_border = ''
+
+ row_div += f"{l_border}{self._col_widths[col] * h_div}{r_border}"
+ return row_div
+
+ def _create_title_row(self) -> Iterable[str]:
+ title_rows = []
+ title_cells = ''
+ last_cell = False
+ for index, curr_name in enumerate(self._col_names):
+ if index == len(self._col_names) - 1:
+ last_cell = True
+ title_cells += self._create_cell(
+ curr_name, last_cell, index, _Align.CENTER
+ )
+ title_rows.extend(
+ [
+ title_cells,
+ self._row_divider(len(self._col_names), self._cs.HH.value),
+ ]
+ )
+ return title_rows
+
+ def _create_border(self, top: bool, h_div: str):
+ """Top or bottom borders of ASCII table."""
+ row_div = ''
+ for col in range(len(self._col_names)):
+ if top:
+ if col == 0:
+ l_div = self._cs.TL.value
+ r_div = ''
+ elif col == (len(self._col_names) - 1):
+ l_div = self._cs.TM.value
+ r_div = self._cs.TR.value
+ else:
+ l_div = self._cs.TM.value
+ r_div = ''
+ else:
+ if col == 0:
+ l_div = self._cs.BL.value
+ r_div = ''
+ elif col == (len(self._col_names) - 1):
+ l_div = self._cs.BM.value
+ r_div = self._cs.BR.value
+ else:
+ l_div = self._cs.BM.value
+ r_div = ''
+
+ row_div += f"{l_div}{self._col_widths[col] * h_div}{r_div}"
+ return row_div
+
+
+class RstOutput:
+ """Tabular output in ASCII format, which is also valid RST."""
+
+ def __init__(
+ self, ds_map: DataSourceMap, table_label: Optional[str] = None
+ ):
+ self._data_source_map = ds_map
+ self._table_label = table_label
+ self._diff_mode = False
+ if isinstance(self._data_source_map, DiffDataSourceMap):
+ self._diff_mode = True
+
+ def create_table(self) -> str:
+ """Initializes RST table and builds first row."""
+ table_builder = [
+ '\n.. list-table::',
+ ' :widths: auto',
+ ' :header-rows: 1\n',
+ ]
+ header_cols = ['Label', 'Segment', 'Delta']
+ for i, col_name in enumerate(header_cols):
+ list_space = '*' if i == 0 else ' '
+ table_builder.append(f" {list_space} - {col_name}")
+
+ return '\n'.join(table_builder) + f'\n{self.add_report_row()}\n'
+
+ def _label_status_unchanged(self, parent_lb_name: str) -> bool:
+ """Determines if parent label has no status change in diff mode."""
+ for curr_lb in self._data_source_map.labels():
+ if curr_lb.size != 0:
+ if (
+ curr_lb.parents and (parent_lb_name == curr_lb.parents[0])
+ ) or (curr_lb.name == parent_lb_name):
+ if get_label_status(curr_lb) != '':
+ return False
+ return True
+
+ def add_report_row(self) -> str:
+ """Add in new size report row with Label, Segment, and Delta.
+
+ Returns:
+ RST string that is the current row with a full symbols
+ table breakdown of the corresponding segment.
+ """
+ table_rows = []
+ curr_row = []
+ curr_label_name = ''
+ for parent_lb in self._data_source_map.labels(0):
+ if parent_lb.size != 0:
+ if (self._table_label is not None) and (
+ curr_label_name != self._table_label
+ ):
+ curr_row.append(f' * - {self._table_label} ')
+ curr_label_name = self._table_label
+ else:
+ curr_row.append(' * -')
+ curr_row.extend(
+ [
+ f' - .. dropdown:: {parent_lb.name}',
+ ' :animate: fade-in\n',
+ ' .. list-table::',
+ ' :widths: auto\n',
+ ]
+ )
+ if self._label_status_unchanged(parent_lb.name):
+ skip_status = 1, '*'
+ else:
+ skip_status = 0, ' '
+ for curr_lb in self._data_source_map.labels():
+ if (curr_lb.size != 0) and (
+ (
+ curr_lb.parents
+ and (parent_lb.name == curr_lb.parents[0])
+ )
+ or (curr_lb.name == parent_lb.name)
+ ):
+ sign_size = diff_sign_sizes(
+ curr_lb.size, self._diff_mode
+ )
+ curr_status = get_label_status(curr_lb)
+ curr_name = curr_lb.name.replace('_', '\\_')
+ to_extend = [
+ f' * - {curr_status}',
+ f' {skip_status[1]} - {sign_size}',
+ f' - {curr_name}\n',
+ ][skip_status[0] :]
+ curr_row.extend(to_extend)
+ curr_row.append(
+ f' - {diff_sign_sizes(parent_lb.size, self._diff_mode)}'
+ )
+ table_rows.extend(curr_row)
+ curr_row = []
+
+ # No size difference.
+ if len(table_rows) == 0:
+ table_rows.extend(
+ [f'\n * - {self._table_label}', ' - (ALL)', ' - 0']
+ )
+
+ return '\n'.join(table_rows)
diff --git a/pw_bloat/py/setup.cfg b/pw_bloat/py/setup.cfg
index b1f87faa9..65c184cb7 100644
--- a/pw_bloat/py/setup.cfg
+++ b/pw_bloat/py/setup.cfg
@@ -22,7 +22,6 @@ description = Tools for generating binary size report cards
packages = find:
zip_safe = False
install_requires =
- pw_cli
pyelftools
[options.package_data]
diff --git a/pw_bloat/test_label.csv b/pw_bloat/test_label.csv
new file mode 100644
index 000000000..50dcc0e82
--- /dev/null
+++ b/pw_bloat/test_label.csv
@@ -0,0 +1,205 @@
+
+sections,segment_names,fullsymbols,vmsize,filesize
+.FLASH.unused_space,LOAD #3 [RW],[section .FLASH.unused_space],1027616,0
+.heap,LOAD #5 [RW],[section .heap],114688,0
+.stack,LOAD #6 [RW],[section .stack],81248,0
+.code,FLASH,_dtoa_r,3036,3036
+.code,FLASH,[section .code],2967,2967
+.code,FLASH,_printf_float,1132,1132
+.code,FLASH,__adddf3,632,632
+.code,FLASH,_vfiprintf_r,608,608
+.code,FLASH,__aeabi_dmul,596,596
+.code,FLASH,_printf_i,588,588
+.code,FLASH,_svfprintf_r,512,512
+.code,FLASH,__aeabi_ddiv,464,464
+.code,FLASH,pw_assert_basic_HandleFailure,388,388
+.code,FLASH,__multiply,340,340
+.code,FLASH,quorem,284,284
+.code,FLASH,__mdiff,276,276
+.code,FLASH,_ctype_,271,271
+.code,FLASH,__sflush_r,268,268
+.code,FLASH,__lshift,224,224
+.code,FLASH,__swsetup_r,220,220
+.code,FLASH,_printf_common,220,220
+.code,FLASH,pw::allocator::FreeListHeap::Free(void*),220,220
+.code,FLASH,__mprec_tens,200,200
+.code,FLASH,__cvt,196,196
+.code,FLASH,__d2b,184,184
+.code,FLASH,__ssputs_r,184,184
+.code,FLASH,__pow5mult,180,180
+.code,FLASH,__swbuf_r,164,164
+.code,FLASH,pw_MallocInit,156,156
+.code,FLASH,__multadd,140,140
+.code,FLASH,__sfp,140,140
+.code,FLASH,_Balloc,128,128
+.code,FLASH,__smakebuf_r,128,128
+.code,FLASH,pw::allocator::FreeListHeap::Allocate(unsigned int),128,128
+.code,FLASH,"pw::allocator::FreeListHeap::FreeListHeap(pw::span<std::byte, 4294967295u>, pw::allocator::FreeList&)",128,128
+.code,FLASH,__exponent,126,126
+.code,FLASH,__cmpdf2,124,124
+.code,FLASH,_fflush_r,120,120
+.code,FLASH,pw::allocator::Block::CrashIfInvalid(),120,120
+.code,FLASH,__sinit,112,112
+.code,FLASH,pw_sys_io_stm32f429_Init,112,112
+.code,FLASH,"pw::allocator::FreeListHeap::Realloc(void*, unsigned int)",108,108
+.code,FLASH,pw_Log,108,108
+.code,FLASH,"pw::allocator::FreeList::RemoveChunk(pw::span<std::byte, 4294967295u>)",102,102
+.code,FLASH,main,100,100
+.code,FLASH,__lo0bits,96,96
+.code,FLASH,__floatdidf,92,92
+.code,FLASH,"pw::allocator::Block::Split(unsigned int, pw::allocator::Block**)",92,92
+.code,FLASH,pw::allocator::FreeList::FindChunk(unsigned int) const,88,88
+.code,FLASH,_vsnprintf_r,86,86
+.code,FLASH,__fixdfsi,80,80
+.code,FLASH,_raise_r,80,80
+.code,FLASH,__swhatbuf_r,76,76
+.code,FLASH,__libc_init_array,72,72
+.code,FLASH,std,72,72
+.code,FLASH,CSWTCH.1,68,68
+.code,FLASH,_Bfree,68,68
+.code,FLASH,__extendsfdf2,68,68
+.code,FLASH,__hi0bits,64,64
+.code,FLASH,_fwalk_reent,64,64
+.code,FLASH,pw::allocator::Block::CheckStatus() const,64,64
+.code,FLASH,"pw::allocator::FreeList::AddChunk(pw::span<std::byte, 4294967295u>)",64,64
+.code,FLASH,"pw::sys_io::WriteLine(std::basic_string_view<char, std::char_traits<char> > const&)",64,64
+.code,FLASH,__assert_func,60,60
+.code,FLASH,"pw::allocator::FreeList::FindChunkPtrForSize(unsigned int, bool) const",60,60
+.code,FLASH,"pw::string::FormatVaList(pw::span<char, 4294967295u>, char const*, std::__va_list)",60,60
+.code,FLASH,pw::StringBuilder::ResizeAndTerminate(unsigned int),58,58
+.code,FLASH,__mcmp,56,56
+.code,FLASH,__swrite,56,56
+.code,FLASH,StaticMemoryInit,52,52
+.code,FLASH,memmove,52,52
+.code,FLASH,pw::allocator::Block::MergeNext(),50,50
+.code,FLASH,__sfputc_r,46,46
+.code,FLASH,__aeabi_dcmpun,44,44
+.code,FLASH,__i2b,44,44
+.code,FLASH,__sfmoreglue,44,44
+.code,FLASH,"pw::StringBuilder::FormatVaList(char const*, std::__va_list)",44,44
+.code,FLASH,"pw::allocator::Block::Init(pw::span<std::byte, 4294967295u>, pw::allocator::Block**)",44,44
+.code,FLASH,"pw::sys_io::WriteBytes(pw::span<std::byte const, 4294967295u>)",44,44
+.code,FLASH,__mprec_bigtens,40,40
+.code,FLASH,pw::StringBuilder::append(char const*),40,40
+.code,FLASH,__sfputs_r,38,38
+.code,FLASH,__ascii_mbtowc,36,36
+.code,FLASH,__floatsidf,36,36
+.code,FLASH,__sseek,36,36
+.code,FLASH,_fstat_r,36,36
+.code,FLASH,_kill_r,36,36
+.code,FLASH,_lseek_r,36,36
+.code,FLASH,_read_r,36,36
+.code,FLASH,_write_r,36,36
+.code,FLASH,fflush,36,36
+.code,FLASH,fprintf,36,36
+.code,FLASH,__sread,34,34
+.code,FLASH,pw_boot_Entry,34,34
+.code,FLASH,__aeabi_ui2d,32,32
+.code,FLASH,__sf_fake_stderr,32,32
+.code,FLASH,__sf_fake_stdin,32,32
+.code,FLASH,__sf_fake_stdout,32,32
+.code,FLASH,_close_r,32,32
+.code,FLASH,_isatty_r,32,32
+.code,FLASH,"pw::StringBuilder::append(char const*, unsigned int)",32,32
+.code,FLASH,"pw::allocator::FreeListHeap::Calloc(unsigned int, unsigned int)",32,32
+.code,FLASH,_ZN2pw9log_basic12_GLOBAL__N_19write_logMUlSt17basic_string_viewIcSt11char_traitsIcEEE_4_FUNES5_,30,30
+.code,FLASH,"pw::StringBuilder::Format(char const*, ...)",30,30
+.code,FLASH,__ascii_wctomb,28,28
+.code,FLASH,memcpy,28,28
+.code,FLASH,pw::allocator::FreeListHeap::InvalidFreeCrash(),28,28
+.code,FLASH,pw_boot_PreStaticMemoryInit,28,28
+.code,FLASH,vsnprintf,28,28
+.code,FLASH,"Divide(float, float, float*)",24,24
+.code,FLASH,pw::allocator::FreeListHeapBuffer<6u>::defaultBuckets,24,24
+.code,FLASH,pw_StatusString,24,24
+.code,FLASH,pw::StringBuilder::HandleStatusWithSize(pw::StatusWithSize),22,22
+.code,FLASH,CSWTCH.3,20,20
+.code,FLASH,__aeabi_dcmpeq,20,20
+.code,FLASH,__aeabi_dcmpge,20,20
+.code,FLASH,__aeabi_dcmpgt,20,20
+.code,FLASH,__aeabi_dcmple,20,20
+.code,FLASH,__aeabi_dcmplt,20,20
+.code,FLASH,pw::StringBuffer<150u>::StringBuffer(),20,20
+.code,FLASH,"pw::Vector<pw::allocator::FreeList::FreeListNode*, 4294967295u>::operator[](unsigned short)",20,20
+.code,FLASH,pw::sys_io::WriteByte(std::byte),20,20
+.code,FLASH,pw_assert_HandleFailure,20,20
+.code,FLASH,"std::basic_string_view<char, std::char_traits<char> >::basic_string_view(char const*)",20,20
+.code,FLASH,__aeabi_cdcmple,16,16
+.code,FLASH,__aeabi_cdrcmple,16,16
+.code,FLASH,__aeabi_ul2d,16,16
+.code,FLASH,__gtdf2,16,16
+.code,FLASH,__wrap__calloc_r,16,16
+.code,FLASH,__wrap__free_r,16,16
+.code,FLASH,__wrap__malloc_r,16,16
+.code,FLASH,__wrap__realloc_r,16,16
+.code,FLASH,__wrap_malloc,16,16
+.code,FLASH,_close,16,16
+.code,FLASH,_fstat,16,16
+.code,FLASH,_getpid,16,16
+.code,FLASH,_isatty,16,16
+.code,FLASH,_kill,16,16
+.code,FLASH,_lseek,16,16
+.code,FLASH,_read,16,16
+.code,FLASH,_write,16,16
+.code,FLASH,memset,16,16
+.code,FLASH,pw_boot_PreStaticConstructorInit,16,16
+.code,FLASH,raise,16,16
+.code,FLASH,strlen,16,16
+.code,FLASH,abort,14,14
+.code,FLASH,pw::StringBuilder::NullTerminate(),14,14
+.code,FLASH,pw::allocator::Block::MergePrev(),14,14
+.code,FLASH,__sfp_lock_acquire,12,12
+.code,FLASH,__sfp_lock_release,12,12
+.code,FLASH,__sinit_lock_acquire,12,12
+.code,FLASH,__sinit_lock_release,12,12
+.code,FLASH,_cleanup_r,12,12
+.code,FLASH,p05.0,12,12
+.code,FLASH,pw::allocator::Block::InnerSize() const,12,12
+.code,FLASH,__sclose,8,8
+.code,FLASH,_localeconv_r,8,8
+.code,FLASH,"pw::StringBuilder::append(std::basic_string_view<char, std::char_traits<char> > const&)",8,8
+.code,FLASH,"WriteLine(std::basic_string_view<char, std::char_traits<char> > const&)",4,4
+.code,FLASH,__aeabi_dsub,4,4
+.code,FLASH,_getpid_r,4,4
+.code,FLASH,_global_impure_ptr,4,4
+.code,FLASH,pw_boot_PreMainInit,4,4
+.code,FLASH,DefaultFaultHandler,2,2
+.code,FLASH,__retarget_lock_acquire_recursive,2,2
+.code,FLASH,__retarget_lock_init_recursive,2,2
+.code,FLASH,__retarget_lock_release_recursive,2,2
+.code,FLASH,_exit,2,2
+.code,FLASH,pw_boot_PostMain,2,2
+.static_init_ram,RAM,__global_locale,368,368
+.static_init_ram,RAM,impure_data,96,96
+.static_init_ram,RAM,kCrashBanner,48,48
+.static_init_ram,RAM,_impure_ptr,4,4
+.static_init_ram,RAM,pw::log_basic::(anonymous namespace)::write_log,4,4
+.VECTOR_TABLE.unused_space,LOAD #0 [RW],[section .VECTOR_TABLE.unused_space],496,0
+.zero_init_ram,RAM,(anonymous namespace)::buf,104,0
+.zero_init_ram,RAM,object.0,24,0
+.zero_init_ram,RAM,errno,8,0
+.zero_init_ram,RAM,completed.1,4,0
+.zero_init_ram,RAM,pw_freelist_heap,4,0
+.zero_init_ram,RAM,unoptimizable,4,0
+.zero_init_ram,RAM,__lock___sinit_recursive_mutex,3,0
+.zero_init_ram,RAM,__lock___sfp_recursive_mutex,1,0
+.vector_table,LOAD #0 [RW],vector_table,16,16
+.ARM.attributes,,,0,48
+.comment,,,0,77
+.debug_abbrev,,,0,25188
+.debug_aranges,,,0,960
+.debug_frame,,,0,6548
+.debug_info,,,0,220270
+.debug_line,,,0,25550
+.debug_loc,,,0,18323
+.debug_ranges,,,0,4288
+.debug_str,,,0,47996
+.shstrtab,,,0,310
+.stab,,,0,204
+.stabstr,,,0,444
+.strtab,,,0,5790
+.symtab,,,0,9696
+[ELF Header],,,0,52
+[ELF Program Headers],,,0,224
+[ELF Section Headers],,,0,1040
+[Unmapped],,,0,110340
diff --git a/pw_blob_store/BUILD.bazel b/pw_blob_store/BUILD.bazel
index 9facc2c3b..c752ae15a 100644
--- a/pw_blob_store/BUILD.bazel
+++ b/pw_blob_store/BUILD.bazel
@@ -69,6 +69,7 @@ pw_cc_test(
"//pw_kvs:fake_flash",
"//pw_kvs:fake_flash_test_key_value_store",
"//pw_log",
+ "//pw_random",
"//pw_sync:borrow",
"//pw_unit_test",
],
diff --git a/pw_blob_store/BUILD.gn b/pw_blob_store/BUILD.gn
index 1280b58e0..92d99d865 100644
--- a/pw_blob_store/BUILD.gn
+++ b/pw_blob_store/BUILD.gn
@@ -37,6 +37,7 @@ pw_source_set("pw_blob_store") {
dir_pw_bytes,
dir_pw_kvs,
dir_pw_preprocessor,
+ dir_pw_span,
dir_pw_status,
dir_pw_stream,
]
@@ -141,13 +142,9 @@ pw_doc_group("docs") {
report_deps = [ ":blob_size" ]
}
-pw_size_report("blob_size") {
+pw_size_diff("blob_size") {
title = "Pigweed BlobStore size report"
- # To see all the symbols, uncomment the following:
- # Note: The size report RST table won't be generated when full_report = true.
- # full_report = true
-
binaries = [
{
target = "size_report:basic_blob"
diff --git a/pw_blob_store/CMakeLists.txt b/pw_blob_store/CMakeLists.txt
index b561bafb9..1577864fd 100644
--- a/pw_blob_store/CMakeLists.txt
+++ b/pw_blob_store/CMakeLists.txt
@@ -14,7 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_blob_store
+pw_add_library(pw_blob_store INTERFACE
PUBLIC_DEPS
pw_bytes
pw_containers
@@ -30,7 +30,7 @@ pw_add_module_library(pw_blob_store
pw_string
)
-pw_add_module_library(pw_blob_store.flat_file_system_entry
+pw_add_library(pw_blob_store.flat_file_system_entry INTERFACE
PUBLIC_DEPS
pw_blob_store
pw_bytes
@@ -52,7 +52,7 @@ pw_add_module_library(pw_blob_store.flat_file_system_entry
pw_add_test(pw_blob_store.blob_store_chunk_write_test
SOURCES
blob_store_chunk_write_test.cc
- DEPS
+ PRIVATE_DEPS
pw_blob_store
GROUPS
pw_blob_store
@@ -61,7 +61,7 @@ pw_add_test(pw_blob_store.blob_store_chunk_write_test
pw_add_test(pw_blob_store.blob_store_deferred_write_test
SOURCES
blob_store_deferred_write_test.cc
- DEPS
+ PRIVATE_DEPS
pw_blob_store
GROUPS
pw_blob_store
@@ -70,7 +70,7 @@ pw_add_test(pw_blob_store.blob_store_deferred_write_test
pw_add_test(pw_blob_store.flat_file_system_entry_test
SOURCES
flat_file_system_entry_test.cc
- DEPS
+ PRIVATE_DEPS
pw_blob_store
pw_blob_store.flat_file_system_entry
GROUPS
diff --git a/pw_blob_store/blob_store.cc b/pw_blob_store/blob_store.cc
index d58455326..03336db94 100644
--- a/pw_blob_store/blob_store.cc
+++ b/pw_blob_store/blob_store.cc
@@ -81,7 +81,7 @@ Status BlobStore::LoadMetadata() {
// kvs_.Get() will return RESOURCE_EXHAUSTED as the file name won't fit in the
// BlobMetadtataHeader object, which is intended behavior.
if (StatusWithSize sws = kvs_.acquire()->Get(
- MetadataKey(), std::as_writable_bytes(std::span(&metadata, 1)));
+ MetadataKey(), as_writable_bytes(span(&metadata, 1)));
!sws.ok() && !sws.IsResourceExhausted()) {
return Status::NotFound();
}
@@ -90,7 +90,7 @@ Status BlobStore::LoadMetadata() {
metadata.v1_metadata.checksum)
.ok()) {
PW_LOG_ERROR("BlobStore init - Invalidating blob with invalid checksum");
- Invalidate().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ Invalidate().IgnoreError(); // TODO(b/242598609): Handle Status properly
return Status::DataLoss();
}
@@ -120,12 +120,12 @@ Status BlobStore::OpenWrite() {
writer_open_ = true;
// Clear any existing contents.
- Invalidate().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ Invalidate().IgnoreError(); // TODO(b/242598609): Handle Status properly
return OkStatus();
}
-StatusWithSize BlobStore::GetFileName(std::span<char> dest) const {
+StatusWithSize BlobStore::GetFileName(span<char> dest) const {
if (!initialized_) {
return StatusWithSize(Status::FailedPrecondition(), 0);
}
@@ -145,7 +145,7 @@ StatusWithSize BlobStore::GetFileName(std::span<char> dest) const {
constexpr size_t kFileNameOffset = sizeof(BlobMetadataHeader);
const StatusWithSize kvs_read_sws =
kvs_.acquire()->Get(MetadataKey(),
- std::as_writable_bytes(dest.first(bytes_to_read)),
+ as_writable_bytes(dest.first(bytes_to_read)),
kFileNameOffset);
status.Update(kvs_read_sws.status());
return StatusWithSize(status, kvs_read_sws.size());
@@ -316,7 +316,7 @@ Status BlobStore::Flush() {
return Status::DataLoss();
}
- ByteSpan data = std::span(write_buffer_.data(), WriteBufferBytesUsed());
+ ByteSpan data = span(write_buffer_.data(), WriteBufferBytesUsed());
size_t write_size_bytes =
(data.size_bytes() / flash_write_size_bytes_) * flash_write_size_bytes_;
if (!CommitToFlash(data.first(write_size_bytes)).ok()) {
@@ -433,7 +433,7 @@ Result<ConstByteSpan> BlobStore::GetMemoryMappedBlob() const {
}
size_t BlobStore::ReadableDataBytes() const {
- // TODO: clean up state related to readable bytes.
+ // TODO(davidrogers): clean up state related to readable bytes.
return flash_address_;
}
@@ -453,7 +453,7 @@ Status BlobStore::Erase() {
// If any writes have been performed, reset the state.
if (flash_address_ != 0) {
- Invalidate().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ Invalidate().IgnoreError(); // TODO(b/242598609): Handle Status properly
}
PW_TRY(partition_.Erase());
@@ -502,7 +502,7 @@ Status BlobStore::ValidateChecksum(size_t blob_size_bytes,
static_cast<unsigned>(blob_size_bytes));
PW_TRY(CalculateChecksumFromFlash(blob_size_bytes));
- Status status = checksum_algo_->Verify(as_bytes(std::span(&expected, 1)));
+ Status status = checksum_algo_->Verify(as_bytes(span(&expected, 1)));
PW_LOG_DEBUG(" checksum verify of %s", status.str());
return status;
@@ -522,7 +522,7 @@ Status BlobStore::CalculateChecksumFromFlash(size_t bytes_to_check) {
std::array<std::byte, kReadBufferSizeBytes> buffer;
while (address < end) {
const size_t read_size = std::min(size_t(end - address), buffer.size());
- PW_TRY(partition_.Read(address, std::span(buffer).first(read_size)));
+ PW_TRY(partition_.Read(address, span(buffer).first(read_size)));
checksum_algo_->Update(buffer.data(), read_size);
address += read_size;
@@ -689,6 +689,7 @@ size_t BlobStore::BlobReader::ConservativeLimit(LimitType limit) const {
Status BlobStore::BlobReader::Open(size_t offset) {
PW_DCHECK(!open_);
+ PW_TRY(store_.Init());
if (!store_.HasData()) {
return Status::FailedPrecondition();
}
@@ -704,7 +705,7 @@ Status BlobStore::BlobReader::Open(size_t offset) {
return status;
}
-size_t BlobStore::BlobReader::DoTell() const {
+size_t BlobStore::BlobReader::DoTell() {
return open_ ? offset_ : kUnknownPosition;
}
diff --git a/pw_blob_store/blob_store_chunk_write_test.cc b/pw_blob_store/blob_store_chunk_write_test.cc
index b789c8d5b..7e533ebe2 100644
--- a/pw_blob_store/blob_store_chunk_write_test.cc
+++ b/pw_blob_store/blob_store_chunk_write_test.cc
@@ -15,7 +15,6 @@
#include <array>
#include <cstddef>
#include <cstring>
-#include <span>
#include "gtest/gtest.h"
#include "pw_blob_store/blob_store.h"
@@ -25,6 +24,7 @@
#include "pw_kvs/test_key_value_store.h"
#include "pw_log/log.h"
#include "pw_random/xor_shift.h"
+#include "pw_span/span.h"
namespace pw::blob_store {
namespace {
@@ -33,23 +33,19 @@ class BlobStoreChunkTest : public ::testing::Test {
protected:
BlobStoreChunkTest() : flash_(kFlashAlignment), partition_(&flash_) {}
- void InitFlashTo(std::span<const std::byte> contents) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ void InitFlashTo(span<const std::byte> contents) {
+ ASSERT_EQ(OkStatus(), partition_.Erase());
std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
}
void InitSourceBufferToRandom(uint64_t seed) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), partition_.Erase());
random::XorShiftStarRng64 rng(seed);
- rng.Get(source_buffer_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ rng.Get(source_buffer_);
}
void InitSourceBufferToFill(char fill) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), partition_.Erase());
std::memset(source_buffer_.data(), fill, source_buffer_.size());
}
diff --git a/pw_blob_store/blob_store_deferred_write_test.cc b/pw_blob_store/blob_store_deferred_write_test.cc
index 5189be2a9..2aafbf380 100644
--- a/pw_blob_store/blob_store_deferred_write_test.cc
+++ b/pw_blob_store/blob_store_deferred_write_test.cc
@@ -15,7 +15,6 @@
#include <array>
#include <cstddef>
#include <cstring>
-#include <span>
#include "gtest/gtest.h"
#include "pw_blob_store/blob_store.h"
@@ -25,6 +24,7 @@
#include "pw_kvs/test_key_value_store.h"
#include "pw_log/log.h"
#include "pw_random/xor_shift.h"
+#include "pw_span/span.h"
namespace pw::blob_store {
namespace {
@@ -37,16 +37,12 @@ class DeferredWriteTest : public ::testing::Test {
void InitFlashToRandom(uint64_t seed) {
random::XorShiftStarRng64 rng(seed);
- StatusWithSize sws = rng.Get(flash_.buffer());
- ASSERT_EQ(OkStatus(), sws.status());
- ASSERT_EQ(sws.size(), flash_.buffer().size());
+ rng.Get(flash_.buffer());
}
void InitBufferToRandom(uint64_t seed) {
random::XorShiftStarRng64 rng(seed);
- StatusWithSize sws = rng.Get(buffer_);
- ASSERT_EQ(OkStatus(), sws.status());
- ASSERT_EQ(sws.size(), buffer_.size());
+ rng.Get(buffer_);
}
void InitBufferToFill(char fill) {
@@ -92,7 +88,7 @@ class DeferredWriteTest : public ::testing::Test {
static_cast<unsigned>(source.size_bytes()));
ASSERT_EQ(OkStatus(), writer.Write(source.first(write_size)));
- // TODO: Add check that the write did not go to flash yet.
+ // TODO(davidrogers): Add check that the write did not go to flash yet.
source = source.subspan(write_size);
bytes_since_flush += write_size;
diff --git a/pw_blob_store/blob_store_test.cc b/pw_blob_store/blob_store_test.cc
index 5b35df22f..52befa000 100644
--- a/pw_blob_store/blob_store_test.cc
+++ b/pw_blob_store/blob_store_test.cc
@@ -17,7 +17,6 @@
#include <array>
#include <cstddef>
#include <cstring>
-#include <span>
#include "gtest/gtest.h"
#include "pw_kvs/crc16_checksum.h"
@@ -26,6 +25,7 @@
#include "pw_kvs/test_key_value_store.h"
#include "pw_log/log.h"
#include "pw_random/xor_shift.h"
+#include "pw_span/span.h"
#ifndef PW_FLASH_TEST_ALIGNMENT
#define PW_FLASH_TEST_ALIGNMENT 1
@@ -40,9 +40,8 @@ class BlobStoreTest : public ::testing::Test {
BlobStoreTest() : flash_(kFlashAlignment), partition_(&flash_) {}
- void InitFlashTo(std::span<const std::byte> contents) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ void InitFlashTo(span<const std::byte> contents) {
+ ASSERT_EQ(OkStatus(), partition_.Erase());
std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
}
@@ -54,8 +53,7 @@ class BlobStoreTest : public ::testing::Test {
std::memset(source_buffer_.data(),
static_cast<int>(flash_.erased_memory_content()),
source_buffer_.size());
- rng.Get(std::span(source_buffer_).first(init_size_bytes))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ rng.Get(span(source_buffer_).first(init_size_bytes));
}
void InitSourceBufferToFill(char fill,
@@ -74,8 +72,7 @@ class BlobStoreTest : public ::testing::Test {
constexpr size_t kBufferSize = 256;
kvs::ChecksumCrc16 checksum;
- ConstByteSpan write_data =
- std::span(source_buffer_).first(write_size_bytes);
+ ConstByteSpan write_data = span(source_buffer_).first(write_size_bytes);
BlobStoreBuffer<kBufferSize> blob(
kBlobTitle, partition_, &checksum, kvs::TestKvs(), kBufferSize);
@@ -162,8 +159,8 @@ class BlobStoreTest : public ::testing::Test {
};
TEST_F(BlobStoreTest, Init_Ok) {
- // TODO: Do init test with flash/kvs explicitly in the different possible
- // entry states.
+ // TODO(davidrogers): Do init test with flash/kvs explicitly in the different
+ // possible entry states.
constexpr size_t kBufferSize = 256;
BlobStoreBuffer<kBufferSize> blob(
"Blob_OK", partition_, nullptr, kvs::TestKvs(), kBufferSize);
@@ -199,8 +196,8 @@ TEST_F(BlobStoreTest, OversizedWriteBuffer) {
InitSourceBufferToRandom(0x123d123);
- ConstByteSpan write_data = std::span(source_buffer_);
- ConstByteSpan original_source = std::span(source_buffer_);
+ ConstByteSpan write_data = span(source_buffer_);
+ ConstByteSpan original_source = span(source_buffer_);
EXPECT_EQ(OkStatus(), partition_.Erase());
@@ -292,8 +289,8 @@ TEST_F(BlobStoreTest, NoWriteBuffer_1Alignment) {
InitSourceBufferToRandom(0xaabd123);
- ConstByteSpan write_data = std::span(source_buffer_);
- ConstByteSpan original_source = std::span(source_buffer_);
+ ConstByteSpan write_data = span(source_buffer_);
+ ConstByteSpan original_source = span(source_buffer_);
EXPECT_EQ(OkStatus(), partition_.Erase());
@@ -301,7 +298,7 @@ TEST_F(BlobStoreTest, NoWriteBuffer_1Alignment) {
partition_,
&checksum,
kvs::TestKvs(),
- std::span<std::byte>(),
+ span<std::byte>(),
kWriteSizeBytes);
EXPECT_EQ(OkStatus(), blob.Init());
@@ -347,8 +344,8 @@ TEST_F(BlobStoreTest, NoWriteBuffer_16Alignment) {
InitSourceBufferToRandom(0x6745d123);
- ConstByteSpan write_data = std::span(source_buffer_);
- ConstByteSpan original_source = std::span(source_buffer_);
+ ConstByteSpan write_data = span(source_buffer_);
+ ConstByteSpan original_source = span(source_buffer_);
EXPECT_EQ(OkStatus(), partition_.Erase());
@@ -356,7 +353,7 @@ TEST_F(BlobStoreTest, NoWriteBuffer_16Alignment) {
partition_,
&checksum,
kvs::TestKvs(),
- std::span<std::byte>(),
+ span<std::byte>(),
kWriteSizeBytes);
EXPECT_EQ(OkStatus(), blob.Init());
@@ -449,8 +446,7 @@ TEST_F(BlobStoreTest, FileNameUndersizedRead) {
EXPECT_EQ(OkStatus(), writer.Open());
EXPECT_EQ(OkStatus(), writer.SetFileName(kFileName));
- EXPECT_EQ(OkStatus(),
- writer.Write(std::as_bytes(std::span("some interesting data"))));
+ EXPECT_EQ(OkStatus(), writer.Write(as_bytes(span("some interesting data"))));
EXPECT_EQ(OkStatus(), writer.Close());
// Ensure the file name can be read from a reader.
@@ -572,7 +568,7 @@ TEST_F(BlobStoreTest, Discard) {
kvs::ChecksumCrc16 checksum;
- // TODO: Do this test with flash/kvs in the different entry state
+ // TODO(davidrogers): Do this test with flash/kvs in the different entry state
// combinations.
constexpr size_t kBufferSize = 256;
@@ -748,7 +744,7 @@ TEST_F(BlobStoreTest, WriteBufferWithRemainderInBuffer) {
EXPECT_EQ(OkStatus(), blob.Init());
const size_t write_size_bytes = kBlobDataSize - 10;
- ConstByteSpan write_data = std::span(source_buffer_).first(write_size_bytes);
+ ConstByteSpan write_data = span(source_buffer_).first(write_size_bytes);
BlobStore::BlobWriterWithBuffer writer(blob);
EXPECT_EQ(OkStatus(), writer.Open());
diff --git a/pw_blob_store/flat_file_system_entry.cc b/pw_blob_store/flat_file_system_entry.cc
index 042141acd..c1bc594db 100644
--- a/pw_blob_store/flat_file_system_entry.cc
+++ b/pw_blob_store/flat_file_system_entry.cc
@@ -16,11 +16,11 @@
#include <cstddef>
#include <mutex>
-#include <span>
#include "pw_assert/check.h"
#include "pw_blob_store/blob_store.h"
#include "pw_file/flat_file_system.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_sync/virtual_basic_lockable.h"
@@ -51,7 +51,7 @@ void FlatFileSystemBlobStoreEntry::EnsureInitialized() {
PW_DCHECK_OK(status);
}
-StatusWithSize FlatFileSystemBlobStoreEntry::Name(std::span<char> dest) {
+StatusWithSize FlatFileSystemBlobStoreEntry::Name(span<char> dest) {
EnsureInitialized();
std::lock_guard lock(blob_store_lock_);
BlobStore::BlobReader reader(blob_store_);
@@ -59,13 +59,12 @@ StatusWithSize FlatFileSystemBlobStoreEntry::Name(std::span<char> dest) {
// When a BlobStore is empty, Open() reports FAILED_PRECONDITION. The
// FlatFileSystemService expects NOT_FOUND when a file is not present at the
// entry.
- switch (status.code()) {
- case Status::FailedPrecondition().code():
- return StatusWithSize(Status::NotFound(), 0);
- case Status::Unavailable().code():
- return StatusWithSize(Status::Unavailable(), 0);
- default:
- return StatusWithSize(Status::Internal(), 0);
+ if (status.IsFailedPrecondition()) {
+ return StatusWithSize(Status::NotFound(), 0);
+ } else if (status.IsUnavailable()) {
+ return StatusWithSize(Status::Unavailable(), 0);
+ } else {
+ return StatusWithSize(Status::Internal(), 0);
}
}
return reader.GetFileName(dest);
@@ -81,7 +80,7 @@ size_t FlatFileSystemBlobStoreEntry::SizeBytes() {
return reader.ConservativeReadLimit();
}
-// TODO(pwbug/488): This file can be deleted even though it is read-only.
+// TODO(b/234888404): This file can be deleted even though it is read-only.
// This type of behavior should be possible to express via the FileSystem RPC
// service.
Status FlatFileSystemBlobStoreEntry::Delete() {
diff --git a/pw_blob_store/flat_file_system_entry_test.cc b/pw_blob_store/flat_file_system_entry_test.cc
index 8410deab9..f0d040102 100644
--- a/pw_blob_store/flat_file_system_entry_test.cc
+++ b/pw_blob_store/flat_file_system_entry_test.cc
@@ -17,7 +17,6 @@
#include <array>
#include <cstddef>
#include <cstring>
-#include <span>
#include "gtest/gtest.h"
#include "pw_blob_store/blob_store.h"
@@ -26,6 +25,7 @@
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/test_key_value_store.h"
#include "pw_random/xor_shift.h"
+#include "pw_span/span.h"
#include "pw_sync/mutex.h"
namespace pw::blob_store {
@@ -55,8 +55,7 @@ class FlatFileSystemBlobStoreEntryTest : public ::testing::Test {
std::memset(source_buffer_.data(),
static_cast<int>(flash_.erased_memory_content()),
source_buffer_.size());
- rng.Get(std::span(source_buffer_).first(init_size_bytes))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ rng.Get(span(source_buffer_).first(init_size_bytes));
}
// Fill the source buffer with random pattern based on given seed, written to
@@ -64,8 +63,7 @@ class FlatFileSystemBlobStoreEntryTest : public ::testing::Test {
void WriteTestBlock(std::string_view file_name, size_t write_size_bytes) {
ASSERT_LE(write_size_bytes, source_buffer_.size());
- ConstByteSpan write_data =
- std::span(source_buffer_).first(write_size_bytes);
+ ConstByteSpan write_data = span(source_buffer_).first(write_size_bytes);
BlobStore::BlobWriter writer(blob_, metadata_buffer_);
EXPECT_EQ(OkStatus(), writer.Open());
diff --git a/pw_blob_store/public/pw_blob_store/blob_store.h b/pw_blob_store/public/pw_blob_store/blob_store.h
index 899ae8368..17a0335a0 100644
--- a/pw_blob_store/public/pw_blob_store/blob_store.h
+++ b/pw_blob_store/public/pw_blob_store/blob_store.h
@@ -14,7 +14,6 @@
#pragma once
#include <cstddef>
-#include <span>
#include "pw_assert/assert.h"
#include "pw_blob_store/internal/metadata_format.h"
@@ -22,6 +21,7 @@
#include "pw_kvs/checksum.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/key_value_store.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_status/try.h"
@@ -66,9 +66,9 @@ class BlobStore {
: store_(store), metadata_buffer_(metadata_buffer), open_(false) {}
BlobWriter(const BlobWriter&) = delete;
BlobWriter& operator=(const BlobWriter&) = delete;
- virtual ~BlobWriter() {
+ ~BlobWriter() override {
if (open_) {
- Close().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ Close().IgnoreError(); // TODO(b/242598609): Handle Status properly
}
}
@@ -200,7 +200,7 @@ class BlobStore {
: BlobWriter(store, metadata_buffer) {}
DeferredWriter(const DeferredWriter&) = delete;
DeferredWriter& operator=(const DeferredWriter&) = delete;
- virtual ~DeferredWriter() {}
+ ~DeferredWriter() override {}
// Flush data in the write buffer. Only a multiple of flash_write_size_bytes
// are written in the flush. Any remainder is held until later for either
@@ -255,7 +255,7 @@ class BlobStore {
BlobReader(const BlobReader&) = delete;
BlobReader& operator=(const BlobReader&) = delete;
- ~BlobReader() {
+ ~BlobReader() override {
if (open_) {
Close().IgnoreError();
}
@@ -297,7 +297,7 @@ class BlobStore {
// NOT_FOUND - No file name set for this blob.
// FAILED_PRECONDITION - not open
//
- StatusWithSize GetFileName(std::span<char> dest) {
+ StatusWithSize GetFileName(span<char> dest) {
return open_ ? store_.GetFileName(dest)
: StatusWithSize::FailedPrecondition();
}
@@ -322,7 +322,7 @@ class BlobStore {
// other than OK. See stream.h for additional details.
size_t ConservativeLimit(LimitType limit) const override;
- size_t DoTell() const override;
+ size_t DoTell() override;
Status DoSeek(ptrdiff_t offset, Whence origin) override;
@@ -542,7 +542,7 @@ class BlobStore {
// first N bytes of the file name.
// NOT_FOUND - No file name set for this blob.
// FAILED_PRECONDITION - BlobStore has not been initialized.
- StatusWithSize GetFileName(std::span<char> dest) const;
+ StatusWithSize GetFileName(span<char> dest) const;
std::string_view name_;
kvs::FlashPartition& partition_;
@@ -559,7 +559,7 @@ class BlobStore {
//
// Internal state for Blob store
//
- // TODO: Consolidate blob state to a single struct
+ // TODO(davidrogers): Consolidate blob state to a single struct
// Initialization has been done.
bool initialized_;
diff --git a/pw_blob_store/public/pw_blob_store/flat_file_system_entry.h b/pw_blob_store/public/pw_blob_store/flat_file_system_entry.h
index 6cefcae6a..870f93cec 100644
--- a/pw_blob_store/public/pw_blob_store/flat_file_system_entry.h
+++ b/pw_blob_store/public/pw_blob_store/flat_file_system_entry.h
@@ -15,10 +15,10 @@
#pragma once
#include <cstddef>
-#include <span>
#include "pw_blob_store/blob_store.h"
#include "pw_file/flat_file_system.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_sync/lock_annotations.h"
@@ -51,7 +51,7 @@ class FlatFileSystemBlobStoreEntry final
// as this class will also lazy-init
Status Init();
- StatusWithSize Name(std::span<char> dest) final;
+ StatusWithSize Name(span<char> dest) final;
size_t SizeBytes() final;
diff --git a/pw_blob_store/public/pw_blob_store/internal/metadata_format.h b/pw_blob_store/public/pw_blob_store/internal/metadata_format.h
index 2d7a2934f..f63bbb16b 100644
--- a/pw_blob_store/public/pw_blob_store/internal/metadata_format.h
+++ b/pw_blob_store/public/pw_blob_store/internal/metadata_format.h
@@ -14,6 +14,7 @@
#pragma once
#include <cstddef>
+#include <cstdint>
#include "pw_preprocessor/compiler.h"
diff --git a/pw_blob_store/size_report/base.cc b/pw_blob_store/size_report/base.cc
index 85ffa1a41..f1c5aa14b 100644
--- a/pw_blob_store/size_report/base.cc
+++ b/pw_blob_store/size_report/base.cc
@@ -51,7 +51,7 @@ int main() {
PW_LOG_INFO("We care about optimizing: %d", *unoptimizable);
void* result =
- std::memset((void*)working_buffer, sizeof(working_buffer), 0x55);
+ std::memset((void*)working_buffer, 0x55, sizeof(working_buffer));
is_set = (result != nullptr);
{
@@ -74,7 +74,7 @@ int main() {
PW_LOG_INFO("Use the variable. %u", unsigned(*val));
std::array<std::byte, 32> blob_source_buffer;
- pw::ConstByteSpan write_data = std::span(blob_source_buffer);
+ pw::ConstByteSpan write_data = pw::span(blob_source_buffer);
char name[16] = "BLOB";
std::array<std::byte, 32> read_buffer;
pw::ByteSpan read_span = read_buffer;
diff --git a/pw_blob_store/size_report/basic_blob.cc b/pw_blob_store/size_report/basic_blob.cc
index a2ea6106c..0bb848705 100644
--- a/pw_blob_store/size_report/basic_blob.cc
+++ b/pw_blob_store/size_report/basic_blob.cc
@@ -55,7 +55,7 @@ int main() {
PW_LOG_INFO("We care about optimizing: %d", *unoptimizable);
void* result =
- std::memset((void*)working_buffer, sizeof(working_buffer), 0x55);
+ std::memset((void*)working_buffer, 0x55, sizeof(working_buffer));
is_set = (result != nullptr);
{
@@ -78,7 +78,7 @@ int main() {
PW_LOG_INFO("Use the variable. %u", unsigned(*val));
std::array<std::byte, 32> blob_source_buffer;
- pw::ConstByteSpan write_data = std::span(blob_source_buffer);
+ pw::ConstByteSpan write_data = pw::span(blob_source_buffer);
char name[16] = "BLOB";
std::array<std::byte, 32> read_buffer;
pw::ByteSpan read_span = read_buffer;
diff --git a/pw_blob_store/size_report/deferred_write_blob.cc b/pw_blob_store/size_report/deferred_write_blob.cc
index 948c5ae01..0af0bcabe 100644
--- a/pw_blob_store/size_report/deferred_write_blob.cc
+++ b/pw_blob_store/size_report/deferred_write_blob.cc
@@ -55,7 +55,7 @@ int main() {
PW_LOG_INFO("We care about optimizing: %d", *unoptimizable);
void* result =
- std::memset((void*)working_buffer, sizeof(working_buffer), 0x55);
+ std::memset((void*)working_buffer, 0x55, sizeof(working_buffer));
is_set = (result != nullptr);
{
@@ -78,7 +78,7 @@ int main() {
PW_LOG_INFO("Use the variable. %u", unsigned(*val));
std::array<std::byte, 32> blob_source_buffer;
- pw::ConstByteSpan write_data = std::span(blob_source_buffer);
+ pw::ConstByteSpan write_data = pw::span(blob_source_buffer);
char name[16] = "BLOB";
std::array<std::byte, 32> read_buffer;
pw::ByteSpan read_span = read_buffer;
diff --git a/pw_bluetooth/BUILD.bazel b/pw_bluetooth/BUILD.bazel
new file mode 100644
index 000000000..df2c6bc3d
--- /dev/null
+++ b/pw_bluetooth/BUILD.bazel
@@ -0,0 +1,112 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_bluetooth",
+ hdrs = [
+ "public/pw_bluetooth/address.h",
+ "public/pw_bluetooth/assigned_uuids.h",
+ "public/pw_bluetooth/constants.h",
+ "public/pw_bluetooth/controller.h",
+ "public/pw_bluetooth/gatt/client.h",
+ "public/pw_bluetooth/gatt/constants.h",
+ "public/pw_bluetooth/gatt/error.h",
+ "public/pw_bluetooth/gatt/server.h",
+ "public/pw_bluetooth/gatt/types.h",
+ "public/pw_bluetooth/host.h",
+ "public/pw_bluetooth/internal/hex.h",
+ "public/pw_bluetooth/internal/raii_ptr.h",
+ "public/pw_bluetooth/low_energy/advertising_data.h",
+ "public/pw_bluetooth/low_energy/bond_data.h",
+ "public/pw_bluetooth/low_energy/central.h",
+ "public/pw_bluetooth/low_energy/connection.h",
+ "public/pw_bluetooth/low_energy/peripheral.h",
+ "public/pw_bluetooth/low_energy/security_mode.h",
+ "public/pw_bluetooth/pairing_delegate.h",
+ "public/pw_bluetooth/peer.h",
+ "public/pw_bluetooth/result.h",
+ "public/pw_bluetooth/types.h",
+ "public/pw_bluetooth/uuid.h",
+ "public/pw_bluetooth/vendor.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_chrono:system_clock",
+ "//pw_containers",
+ "//pw_function",
+ "//pw_status",
+ "//pw_string:string",
+ ],
+)
+
+pw_cc_test(
+ name = "address_test",
+ srcs = [
+ "address_test.cc",
+ ],
+ deps = [
+ ":pw_bluetooth",
+ ],
+)
+
+pw_cc_test(
+ name = "api_test",
+ srcs = [
+ "api_test.cc",
+ ],
+ deps = [
+ ":pw_bluetooth",
+ ],
+)
+
+pw_cc_test(
+ name = "result_test",
+ srcs = [
+ "result_test.cc",
+ ],
+ deps = [
+ ":pw_bluetooth",
+ ],
+)
+
+pw_cc_test(
+ name = "uuid_test",
+ srcs = [
+ "uuid_test.cc",
+ ],
+ deps = [
+ ":pw_bluetooth",
+ ],
+)
+
+# Bazel support for Emboss has not been configured yet, but we need to satisfy
+# presubmit.
+filegroup(
+ name = "emboss_files",
+ srcs = [
+ "emboss_test.cc",
+ "size_report/make_2_views_and_write.cc",
+ "size_report/make_view_and_write.cc",
+ ],
+)
diff --git a/pw_bluetooth/BUILD.gn b/pw_bluetooth/BUILD.gn
new file mode 100644
index 000000000..7f7ea2c0d
--- /dev/null
+++ b/pw_bluetooth/BUILD.gn
@@ -0,0 +1,186 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pigweed/third_party/emboss/build_defs.gni")
+import("$dir_pw_bloat/bloat.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+
+ report_deps = [
+ ":emboss_size_report",
+ ":emboss_size_report_diff",
+ ]
+}
+
+pw_source_set("pw_bluetooth") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "public/pw_bluetooth/address.h",
+ "public/pw_bluetooth/assigned_uuids.h",
+ "public/pw_bluetooth/constants.h",
+ "public/pw_bluetooth/controller.h",
+ "public/pw_bluetooth/gatt/client.h",
+ "public/pw_bluetooth/gatt/constants.h",
+ "public/pw_bluetooth/gatt/error.h",
+ "public/pw_bluetooth/gatt/server.h",
+ "public/pw_bluetooth/gatt/types.h",
+ "public/pw_bluetooth/host.h",
+ "public/pw_bluetooth/internal/hex.h",
+ "public/pw_bluetooth/internal/raii_ptr.h",
+ "public/pw_bluetooth/low_energy/advertising_data.h",
+ "public/pw_bluetooth/low_energy/bond_data.h",
+ "public/pw_bluetooth/low_energy/central.h",
+ "public/pw_bluetooth/low_energy/connection.h",
+ "public/pw_bluetooth/low_energy/peripheral.h",
+ "public/pw_bluetooth/low_energy/security_mode.h",
+ "public/pw_bluetooth/pairing_delegate.h",
+ "public/pw_bluetooth/peer.h",
+ "public/pw_bluetooth/result.h",
+ "public/pw_bluetooth/types.h",
+ "public/pw_bluetooth/uuid.h",
+ "public/pw_bluetooth/vendor.h",
+ ]
+ public_deps = [
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_string:string",
+ dir_pw_containers,
+ dir_pw_function,
+ dir_pw_result,
+ dir_pw_span,
+ dir_pw_status,
+ ]
+}
+
+if (dir_pw_third_party_emboss != "") {
+ config("emboss_include_path") {
+ include_dirs = [ "${target_gen_dir}/public" ]
+ visibility = [ ":*" ]
+ }
+
+ emboss_cc_library("emboss_hci") {
+ public_configs = [ ":emboss_include_path" ]
+ source = "public/pw_bluetooth/hci.emb"
+ }
+
+ emboss_cc_library("emboss_vendor") {
+ public_configs = [ ":emboss_include_path" ]
+ source = "public/pw_bluetooth/vendor.emb"
+ imports = [ "public/pw_bluetooth/hci.emb" ]
+ deps = [ ":emboss_hci" ]
+ }
+} else {
+ group("emboss_hci") {
+ }
+ group("emboss_vendor") {
+ }
+}
+
+pw_test_group("tests") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ tests = [
+ ":address_test",
+ ":api_test",
+ ":result_test",
+ ":uuid_test",
+ ":emboss_test",
+ ]
+}
+
+pw_test("address_test") {
+ sources = [ "address_test.cc" ]
+ deps = [ ":pw_bluetooth" ]
+}
+
+pw_test("api_test") {
+ sources = [ "api_test.cc" ]
+ deps = [ ":pw_bluetooth" ]
+}
+
+pw_test("result_test") {
+ sources = [ "result_test.cc" ]
+ deps = [ ":pw_bluetooth" ]
+}
+
+pw_test("uuid_test") {
+ sources = [ "uuid_test.cc" ]
+ deps = [ ":pw_bluetooth" ]
+}
+
+pw_test("emboss_test") {
+ enable_if = dir_pw_third_party_emboss != ""
+ configs = [ "$dir_pigweed/third_party/emboss:flags" ]
+ sources = [ "emboss_test.cc" ]
+ deps = [
+ ":emboss_hci",
+ ":emboss_vendor",
+ ]
+}
+
+if (dir_pw_third_party_emboss != "") {
+ pw_size_diff("emboss_size_report") {
+ title = "pw_bluetooth Emboss Size Report"
+ base = "$dir_pw_bloat:bloat_base"
+ binaries = [
+ {
+ target = "size_report:make_view_and_write"
+ label = "Make view and write field"
+ },
+ ]
+ }
+
+ pw_size_diff("emboss_size_report_diff") {
+ title = "pw_bluetooth Emboss Size Report diff"
+ base = "size_report:make_view_and_write"
+ binaries = [
+ {
+ target = "size_report:make_2_views_and_write"
+ label = "Size difference when adding a second view"
+ },
+ ]
+ }
+} else {
+ pw_size_diff("emboss_size_report") {
+ title = "pw_bluetooth Emboss Size Report"
+ base = "$dir_pw_bloat:bloat_base"
+ binaries = [
+ {
+ target = "$dir_pw_bloat:bloat_base"
+ label = "Emboss not configured."
+ },
+ ]
+ }
+
+ pw_size_diff("emboss_size_report_diff") {
+ title = "pw_bluetooth Emboss Size Report diff"
+ base = "$dir_pw_bloat:bloat_base"
+ binaries = [
+ {
+ target = "$dir_pw_bloat:bloat_base"
+ label = "Emboss not configured."
+ },
+ ]
+ }
+}
diff --git a/pw_bluetooth/CMakeLists.txt b/pw_bluetooth/CMakeLists.txt
new file mode 100644
index 000000000..4fc7222a7
--- /dev/null
+++ b/pw_bluetooth/CMakeLists.txt
@@ -0,0 +1,87 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_bluetooth INTERFACE
+ HEADERS
+ public/pw_bluetooth/address.h
+ public/pw_bluetooth/assigned_uuids.h
+ public/pw_bluetooth/gatt/client.h
+ public/pw_bluetooth/gatt/constants.h
+ public/pw_bluetooth/gatt/error.h
+ public/pw_bluetooth/gatt/server.h
+ public/pw_bluetooth/gatt/types.h
+ public/pw_bluetooth/internal/hex.h
+ public/pw_bluetooth/internal/raii_ptr.h
+ public/pw_bluetooth/low_energy/advertising_data.h
+ public/pw_bluetooth/low_energy/bond_data.h
+ public/pw_bluetooth/low_energy/central.h
+ public/pw_bluetooth/low_energy/connection.h
+ public/pw_bluetooth/low_energy/peripheral.h
+ public/pw_bluetooth/low_energy/security_mode.h
+ public/pw_bluetooth/constants.h
+ public/pw_bluetooth/controller.h
+ public/pw_bluetooth/host.h
+ public/pw_bluetooth/pairing_delegate.h
+ public/pw_bluetooth/peer.h
+ public/pw_bluetooth/result.h
+ public/pw_bluetooth/types.h
+ public/pw_bluetooth/uuid.h
+ public/pw_bluetooth/vendor.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_containers
+ pw_function
+ pw_status
+ pw_string.string
+ pw_chrono.system_clock
+)
+
+pw_add_test(pw_bluetooth.address_test
+ SOURCES
+ address_test.cc
+ PRIVATE_DEPS
+ pw_bluetooth
+ GROUPS
+ pw_bluetooth
+)
+
+pw_add_test(pw_bluetooth.api_test
+ SOURCES
+ api_test.cc
+ PRIVATE_DEPS
+ pw_bluetooth
+ GROUPS
+ pw_bluetooth
+)
+
+pw_add_test(pw_bluetooth.result_test
+ SOURCES
+ result_test.cc
+ PRIVATE_DEPS
+ pw_bluetooth
+ GROUPS
+ pw_bluetooth
+)
+
+pw_add_test(pw_bluetooth.uuid_test
+ SOURCES
+ uuid_test.cc
+ PRIVATE_DEPS
+ pw_bluetooth
+ GROUPS
+ pw_bluetooth
+)
diff --git a/pw_bluetooth/README.md b/pw_bluetooth/README.md
new file mode 100644
index 000000000..f1526b2df
--- /dev/null
+++ b/pw_bluetooth/README.md
@@ -0,0 +1 @@
+This directory contains Bluetooth Low Energy APIs.
diff --git a/pw_bluetooth/address_test.cc b/pw_bluetooth/address_test.cc
new file mode 100644
index 000000000..c5291ab56
--- /dev/null
+++ b/pw_bluetooth/address_test.cc
@@ -0,0 +1,55 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bluetooth/address.h"
+
+#include <array>
+#include <cstdint>
+
+#include "gtest/gtest.h"
+#include "pw_span/span.h"
+
+namespace pw::bluetooth {
+namespace {
+
+constexpr Address kAddressString{"12:34:56:78:90:ab"};
+constexpr Address kAddressStringFromArray{pw::span<const uint8_t, 6>{
+ std::array<const uint8_t, 6>{0xab, 0x90, 0x78, 0x56, 0x34, 0x12}}};
+
+constexpr Address kAddressArray{pw::span<const uint8_t, 6>{
+ std::array<const uint8_t, 6>{0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f}}};
+
+// Make sure these are actually constexpr.
+static_assert(kAddressString != kAddressArray, "constexpr check");
+static_assert(kAddressString == kAddressStringFromArray, "constexpr check");
+static_assert(kAddressArray.AsSpan().size() == 6, "constexpr check");
+
+TEST(AddressTest, ConstructorTest) {
+ static_assert(kAddressString.ToString() == "12:34:56:78:90:ab",
+ "constexpr check");
+ static_assert(kAddressArray.ToString() == "6f:5e:4d:3c:2b:1a",
+ "constexpr check");
+ static_assert(kAddressArray == Address("6f:5e:4d:3c:2b:1a"),
+ "constexpr check");
+
+ static_assert(kAddressString == kAddressStringFromArray, "constexpr check");
+ static_assert(kAddressString != kAddressArray, "constexpr check");
+
+ auto addr_span = kAddressString.AsSpan();
+ EXPECT_EQ(addr_span.size(), 6u);
+ EXPECT_EQ(addr_span.data()[0], 0xabu);
+ EXPECT_EQ(addr_span.data()[1], 0x90u);
+}
+
+} // namespace
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/api_test.cc b/pw_bluetooth/api_test.cc
new file mode 100644
index 000000000..4fdd04b94
--- /dev/null
+++ b/pw_bluetooth/api_test.cc
@@ -0,0 +1,25 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_bluetooth/host.h"
+
+namespace pw::bluetooth {
+namespace {
+
+// This empty test ensures that the included API compiles.
+TEST(ApiTest, ApiCompiles) {}
+
+} // namespace
+} // namespace pw::bluetooth \ No newline at end of file
diff --git a/pw_bluetooth/docs.rst b/pw_bluetooth/docs.rst
new file mode 100644
index 000000000..428f62513
--- /dev/null
+++ b/pw_bluetooth/docs.rst
@@ -0,0 +1,152 @@
+.. _module-pw_bluetooth:
+
+================
+pw_bluetooth
+================
+The ``pw_bluetooth`` module contains APIs and utilities for the host layer of
+Bluetooth Low Energy.
+
+--------
+Host API
+--------
+.. attention::
+
+ This module is still under construction, the API is not yet stable.
+
+The headers in `public/pw_bluetooth` constitute a Bluetooth Host API. `host.h`
+is the entry point from which all other APIs are exposed. Currently, only Low
+Energy APIs exist.
+
+Host
+====
+.. doxygenclass:: pw::bluetooth::Host
+ :members:
+
+low_energy::Central
+===================
+.. doxygenclass:: pw::bluetooth::low_energy::Central
+ :members:
+
+low_energy::Peripheral
+======================
+.. doxygenclass:: pw::bluetooth::low_energy::Peripheral
+ :members:
+
+low_energy::AdvertisedPeripheral
+================================
+.. doxygenclass:: pw::bluetooth::low_energy::AdvertisedPeripheral
+ :members:
+
+low_energy::Connection
+======================
+.. doxygenclass:: pw::bluetooth::low_energy::Connection
+ :members:
+
+low_energy::ConnectionOptions
+=============================
+.. doxygenstruct:: pw::bluetooth::low_energy::ConnectionOptions
+ :members:
+
+low_energy::RequestedConnectionParameters
+=========================================
+.. doxygenstruct:: pw::bluetooth::low_energy::RequestedConnectionParameters
+ :members:
+
+low_energy::ConnectionParameters
+================================
+.. doxygenstruct:: pw::bluetooth::low_energy::ConnectionParameters
+ :members:
+
+gatt::Server
+============
+.. doxygenclass:: pw::bluetooth::gatt::Server
+ :members:
+
+gatt::LocalServiceInfo
+======================
+.. doxygenstruct:: pw::bluetooth::gatt::LocalServiceInfo
+ :members:
+
+gatt::LocalService
+==================
+.. doxygenclass:: pw::bluetooth::gatt::LocalService
+ :members:
+
+gatt::LocalServiceDelegate
+==========================
+.. doxygenclass:: pw::bluetooth::gatt::LocalServiceDelegate
+ :members:
+
+gatt::Client
+============
+.. doxygenclass:: pw::bluetooth::gatt::Client
+ :members:
+
+gatt::RemoteService
+===================
+.. doxygenclass:: pw::bluetooth::gatt::RemoteService
+ :members:
+
+Callbacks
+=========
+This module contains callback-heavy APIs. Callbacks must not call back into the
+``pw_bluetooth`` APIs unless otherwise noted. This includes calls made by
+destroying objects returned by the API. Additionally, callbacks must not block.
+
+-------------------------
+Emboss Packet Definitions
+-------------------------
+``pw_bluetooth`` contains `Emboss <https://github.com/google/emboss>`_ packet
+definitions. So far, packets from the following protocols are defined:
+
+- HCI
+
+Usage
+=====
+1. Set the `dir_pw_third_party_emboss` GN variable to the path of your Emboss
+checkout.
+
+2. Add `$dir_pw_bluetooth/emboss_hci` (for HCI packets) or
+`$dir_pw_bluetooth/emboss_vendor` (for vendor packets) to your dependency list.
+
+3. Include the generated header files.
+
+.. code-block:: cpp
+
+ #include "pw_bluetooth/hci.emb.h"
+ #include "pw_bluetooth/vendor.emb.h"
+
+4. Construct an Emboss view over a buffer.
+
+.. code-block:: cpp
+
+ std::array<uint8_t, 4> buffer = {0x00, 0x01, 0x02, 0x03};
+ auto view = pw::bluetooth::emboss::MakeTestCommandPacketView(&buffer);
+ EXPECT_EQ(view.payload().Read(), 0x03);
+
+.. note::
+
+ clangd will complain that the generated header file does not exist.
+ You need to build your project to resolve this. Similarly, you need to build
+ in order for .emb file updates to be reflected in the generated headers.
+
+Size Report
+===========
+Delta of +972 when constructing the first packet view and reading/writing a
+field. This includes the runtime library and the 4-byte buffer.
+
+.. include:: emboss_size_report
+
+Delta of +96 when adding a second packet view and reading/writing a field.
+
+.. include:: emboss_size_report_diff
+
+-------
+Roadmap
+-------
+- Create a backend for the Bluetooth API using Fuchsia's Bluetooth stack
+ (sapphire).
+- Stabilize the Bluetooth API.
+- Add BR/EDR APIs.
+- Bazel support
+- CMake support
diff --git a/pw_bluetooth/emboss_test.cc b/pw_bluetooth/emboss_test.cc
new file mode 100644
index 000000000..9db81fb45
--- /dev/null
+++ b/pw_bluetooth/emboss_test.cc
@@ -0,0 +1,28 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_bluetooth/hci.emb.h"
+#include "pw_bluetooth/vendor.emb.h"
+
+namespace pw::bluetooth {
+namespace {
+
+TEST(EmbossTest, MakeView) {
+ std::array<uint8_t, 4> buffer = {0x00, 0x01, 0x02, 0x03};
+ auto view = emboss::MakeTestCommandPacketView(&buffer);
+ EXPECT_EQ(view.payload().Read(), 0x03);
+}
+
+} // namespace
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/address.h b/pw_bluetooth/public/pw_bluetooth/address.h
new file mode 100644
index 000000000..2f56d6606
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/address.h
@@ -0,0 +1,100 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <array>
+#include <cstdint>
+
+#include "pw_assert/assert.h"
+#include "pw_bluetooth/internal/hex.h"
+#include "pw_span/span.h"
+#include "pw_string/string.h"
+
+namespace pw::bluetooth {
+
+// A 48-bit bluetooth device address (BD_ADDR) in little endian format.
+// See Core Spec v5.3 Volume 2, Part B, Section 1.2.
+class Address {
+ public:
+ // String size of a hexadecimal representation of an Address, not including
+ // the null terminator.
+ static constexpr size_t kHexStringSize = 17;
+
+ // Create an Address from its binary representation.
+ // The first byte in the span is the last one in the hex representation, thus
+ // the BD_ADDR 00:11:22:33:44:55 should be created from the span with bytes:
+ // {0x55, 0x44, 0x33, 0x22, 0x11, 0x00}.
+ constexpr Address(const span<const uint8_t, 6> addr_span) : addr_() {
+ static_assert(addr_span.size() == sizeof(addr_));
+ for (size_t i = 0; i < sizeof(addr_); i++) {
+ addr_[i] = addr_span[i];
+ }
+ }
+
+ // Create an address from the hex format "00:11:22:33:44:55". The passed
+ // string must have a length of 17 and a ":" character on the 3rd, 6th, 9th,
+ // 12th and 15th positions. The hexadecimal representation is such that the
+ // first byte in the string is the last byte in the binary representation.
+ constexpr Address(const char (&str_addr)[kHexStringSize + 1]) : addr_() {
+ PW_ASSERT((str_addr[2] == ':') && (str_addr[5] == ':') &&
+ (str_addr[8] == ':') && (str_addr[11] == ':') &&
+ (str_addr[14] == ':'));
+ for (size_t i = 0; i < sizeof(addr_); i++) {
+ uint16_t value = (internal::HexToNibble(str_addr[3 * i]) << 4u) |
+ internal::HexToNibble(str_addr[3 * i + 1]);
+ addr_[sizeof(addr_) - 1 - i] = value;
+ PW_ASSERT(value <= 0xff);
+ }
+ }
+
+ // Return the bluetooth address a the 6-byte binary representation.
+ constexpr span<const uint8_t, 6> AsSpan() const {
+ return span<const uint8_t, 6>{addr_.data(), addr_.size()};
+ }
+
+ // Return an inline pw_string representation of the Address in hexadecimal
+ // using ":" characters as byte separator.
+ constexpr InlineString<kHexStringSize> ToString() const {
+ InlineString<kHexStringSize> ret;
+ for (size_t i = addr_.size(); i-- != 0;) {
+ ret += internal::NibbleToHex(addr_[i] >> 4);
+ ret += internal::NibbleToHex(addr_[i] & 0xf);
+ if (i) {
+ ret += ':';
+ }
+ }
+ return ret;
+ }
+
+ private:
+ std::array<uint8_t, 6> addr_;
+};
+
+// Address comparators:
+constexpr bool operator==(const Address& a, const Address& b) {
+ const auto a_span = a.AsSpan();
+ const auto b_span = b.AsSpan();
+ for (size_t i = 0; i < a_span.size(); i++) {
+ if (a_span[i] != b_span[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+constexpr bool operator!=(const Address& a, const Address& b) {
+ return !(a == b);
+}
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/assigned_uuids.h b/pw_bluetooth/public/pw_bluetooth/assigned_uuids.h
new file mode 100644
index 000000000..c4e44f4f7
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/assigned_uuids.h
@@ -0,0 +1,692 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// 16-bit UUID Assigned Numbers.
+// Source: Bluetooth "16-bit UUID Numbers Document" revision 2022-10-07
+//
+// The UUIDs are grouped according to the type in different classes so users in
+// other namespaces can access them with `using
+// pw::bluetooth::GattDescriptorUuid`.
+
+#include "pw_bluetooth/uuid.h"
+
+namespace pw::bluetooth {
+
+// GATT Descriptor
+class GattDescriptorUuid {
+ public:
+ static constexpr Uuid kCharacteristicExtendedProperties{0x2900};
+ static constexpr Uuid kCharacteristicUserDescription{0x2901};
+ static constexpr Uuid kClientCharacteristicConfiguration{0x2902};
+ static constexpr Uuid kServerCharacteristicConfiguration{0x2903};
+ static constexpr Uuid kCharacteristicPresentationFormat{0x2904};
+ static constexpr Uuid kCharacteristicAggregateFormat{0x2905};
+ static constexpr Uuid kValidRange{0x2906};
+ static constexpr Uuid kExternalReportReference{0x2907};
+ static constexpr Uuid kReportReference{0x2908};
+ static constexpr Uuid kNumberOfDigitals{0x2909};
+ static constexpr Uuid kValueTriggerSetting{0x290a};
+ static constexpr Uuid kEnvironmentalSensingConfiguration{0x290b};
+ static constexpr Uuid kEnvironmentalSensingMeasurement{0x290c};
+ static constexpr Uuid kEnvironmentalSensingTriggerSetting{0x290d};
+ static constexpr Uuid kTimeTriggerSetting{0x290e};
+ static constexpr Uuid kCompleteBrEdrTransportBlockData{0x290f};
+};
+
+// GATT Declarations
+class GattDeclarationsUuid {
+ public:
+ static constexpr Uuid kPrimaryService{0x2800};
+ static constexpr Uuid kSecondaryService{0x2801};
+ static constexpr Uuid kInclude{0x2802};
+ static constexpr Uuid kCharacteristic{0x2803};
+};
+
+// GATT Service
+class GattServiceUuid {
+ public:
+ static constexpr Uuid kGenericAccess{0x1800};
+ static constexpr Uuid kGenericAttribute{0x1801};
+ static constexpr Uuid kImmediateAlert{0x1802};
+ static constexpr Uuid kLinkLoss{0x1803};
+ static constexpr Uuid kTxPower{0x1804};
+ static constexpr Uuid kCurrentTime{0x1805};
+ static constexpr Uuid kReferenceTimeUpdate{0x1806};
+ static constexpr Uuid kNextDstChange{0x1807};
+ static constexpr Uuid kGlucose{0x1808};
+ static constexpr Uuid kHealthThermometer{0x1809};
+ static constexpr Uuid kDeviceInformation{0x180a};
+ static constexpr Uuid kHeartRate{0x180d};
+ static constexpr Uuid kPhoneAlertStatus{0x180e};
+ static constexpr Uuid kBattery{0x180f};
+ static constexpr Uuid kBloodPressure{0x1810};
+ static constexpr Uuid kAlertNotification{0x1811};
+ static constexpr Uuid kHumanInterfaceDevice{0x1812};
+ static constexpr Uuid kScanParameters{0x1813};
+ static constexpr Uuid kRunningSpeedAndCadence{0x1814};
+ static constexpr Uuid kAutomationIo{0x1815};
+ static constexpr Uuid kCyclingSpeedAndCadence{0x1816};
+ static constexpr Uuid kCyclingPower{0x1818};
+ static constexpr Uuid kLocationAndNavigation{0x1819};
+ static constexpr Uuid kEnvironmentalSensing{0x181a};
+ static constexpr Uuid kBodyComposition{0x181b};
+ static constexpr Uuid kUserData{0x181c};
+ static constexpr Uuid kWeightScale{0x181d};
+ static constexpr Uuid kBondManagement{0x181e};
+ static constexpr Uuid kContinuousGlucoseMonitoring{0x181f};
+ static constexpr Uuid kInternetProtocolSupport{0x1820};
+ static constexpr Uuid kIndoorPositioning{0x1821};
+ static constexpr Uuid kPulseOximeter{0x1822};
+ static constexpr Uuid kHttpProxy{0x1823};
+ static constexpr Uuid kTransportDiscovery{0x1824};
+ static constexpr Uuid kObjectTransfer{0x1825};
+ static constexpr Uuid kFitnessMachine{0x1826};
+ static constexpr Uuid kMeshProvisioning{0x1827};
+ static constexpr Uuid kMeshProxy{0x1828};
+ static constexpr Uuid kReconnectionConfiguration{0x1829};
+ static constexpr Uuid kInsulinDelivery{0x183a};
+ static constexpr Uuid kBinarySensor{0x183b};
+ static constexpr Uuid kEmergencyConfiguration{0x183c};
+ static constexpr Uuid kAuthorizationControl{0x183d};
+ static constexpr Uuid kPhysicalActivityMonitor{0x183e};
+ static constexpr Uuid kAudioInputControl{0x1843};
+ static constexpr Uuid kVolumeControl{0x1844};
+ static constexpr Uuid kVolumeOffsetControl{0x1845};
+ static constexpr Uuid kCoordinatedSetIdentification{0x1846};
+ static constexpr Uuid kDeviceTime{0x1847};
+ static constexpr Uuid kMediaControl{0x1848};
+ static constexpr Uuid kGenericMediaControl{0x1849};
+ static constexpr Uuid kConstantToneExtension{0x184a};
+ static constexpr Uuid kTelephoneBearer{0x184b};
+ static constexpr Uuid kGenericTelephoneBearer{0x184c};
+ static constexpr Uuid kMicrophoneControl{0x184d};
+ static constexpr Uuid kAudioStreamControl{0x184e};
+ static constexpr Uuid kBroadcastAudioScan{0x184f};
+ static constexpr Uuid kPublishedAudioCapabilities{0x1850};
+ static constexpr Uuid kBasicAudioAnnouncement{0x1851};
+ static constexpr Uuid kBroadcastAudioAnnouncement{0x1852};
+ static constexpr Uuid kCommonAudio{0x1853};
+ static constexpr Uuid kHearingAccess{0x1854};
+ static constexpr Uuid kTmas{0x1855};
+ static constexpr Uuid kPublicBroadcastAnnouncement{0x1856};
+};
+
+// GATT Unit
+class GattUnitUuid {
+ public:
+ static constexpr Uuid kUnitless{0x2700};
+ static constexpr Uuid kLengthMetre{0x2701};
+ static constexpr Uuid kMassKilogram{0x2702};
+ static constexpr Uuid kTimeSecond{0x2703};
+ static constexpr Uuid kElectricCurrentAmpere{0x2704};
+ static constexpr Uuid kThermodynamicTemperatureKelvin{0x2705};
+ static constexpr Uuid kAmountOfSubstanceMole{0x2706};
+ static constexpr Uuid kLuminousIntensityCandela{0x2707};
+ static constexpr Uuid kAreaSquareMetres{0x2710};
+ static constexpr Uuid kVolumeCubicMetres{0x2711};
+ static constexpr Uuid kVelocityMetresPerSecond{0x2712};
+ static constexpr Uuid kAccelerationMetresPerSecondSquared{0x2713};
+ static constexpr Uuid kWavenumberReciprocalMetre{0x2714};
+ static constexpr Uuid kDensityKilogramPerCubicMetre{0x2715};
+ static constexpr Uuid kSurfaceDensityKilogramPerSquareMetre{0x2716};
+ static constexpr Uuid kSpecificVolumeCubicMetrePerKilogram{0x2717};
+ static constexpr Uuid kCurrentDensityAmperePerSquareMetre{0x2718};
+ static constexpr Uuid kMagneticFieldStrengthAmperePerMetre{0x2719};
+ static constexpr Uuid kAmountConcentrationMolePerCubicMetre{0x271a};
+ static constexpr Uuid kMassConcentrationKilogramPerCubicMetre{0x271b};
+ static constexpr Uuid kLuminanceCandelaPerSquareMetre{0x271c};
+ static constexpr Uuid kRefractiveIndex{0x271d};
+ static constexpr Uuid kRelativePermeability{0x271e};
+ static constexpr Uuid kPlaneAngleRadian{0x2720};
+ static constexpr Uuid kSolidAngleSteradian{0x2721};
+ static constexpr Uuid kFrequencyHertz{0x2722};
+ static constexpr Uuid kForceNewton{0x2723};
+ static constexpr Uuid kPressurePascal{0x2724};
+ static constexpr Uuid kEnergyJoule{0x2725};
+ static constexpr Uuid kPowerWatt{0x2726};
+ static constexpr Uuid kElectricChargeCoulomb{0x2727};
+ static constexpr Uuid kElectricPotentialDifferenceVolt{0x2728};
+ static constexpr Uuid kCapacitanceFarad{0x2729};
+ static constexpr Uuid kElectricResistanceOhm{0x272a};
+ static constexpr Uuid kElectricConductanceSiemens{0x272b};
+ static constexpr Uuid kMagneticFluxWeber{0x272c};
+ static constexpr Uuid kMagneticFluxDensityTesla{0x272d};
+ static constexpr Uuid kInductanceHenry{0x272e};
+ static constexpr Uuid kCelsiusTemperatureDegreeCelsius{0x272f};
+ static constexpr Uuid kLuminousFluxLumen{0x2730};
+ static constexpr Uuid kIlluminanceLux{0x2731};
+ static constexpr Uuid kActivityReferredToARadionuclideBecquerel{0x2732};
+ static constexpr Uuid kAbsorbedDoseGray{0x2733};
+ static constexpr Uuid kDoseEquivalentSievert{0x2734};
+ static constexpr Uuid kCatalyticActivityKatal{0x2735};
+ static constexpr Uuid kDynamicViscosityPascalSecond{0x2740};
+ static constexpr Uuid kMomentOfForceNewtonMetre{0x2741};
+ static constexpr Uuid kSurfaceTensionNewtonPerMetre{0x2742};
+ static constexpr Uuid kAngularVelocityRadianPerSecond{0x2743};
+ static constexpr Uuid kAngularAccelerationRadianPerSecondSquared{0x2744};
+ static constexpr Uuid kHeatFluxDensityWattPerSquareMetre{0x2745};
+ static constexpr Uuid kHeatCapacityJoulePerKelvin{0x2746};
+ static constexpr Uuid kSpecificHeatCapacityJoulePerKilogramKelvin{0x2747};
+ static constexpr Uuid kSpecificEnergyJoulePerKilogram{0x2748};
+ static constexpr Uuid kThermalConductivityWattPerMetreKelvin{0x2749};
+ static constexpr Uuid kEnergyDensityJoulePerCubicMetre{0x274a};
+ static constexpr Uuid kElectricFieldStrengthVoltPerMetre{0x274b};
+ static constexpr Uuid kElectricChargeDensityCoulombPerCubicMetre{0x274c};
+ static constexpr Uuid kSurfaceChargeDensityCoulombPerSquareMetre{0x274d};
+ static constexpr Uuid kElectricFluxDensityCoulombPerSquareMetre{0x274e};
+ static constexpr Uuid kPermittivityFaradPerMetre{0x274f};
+ static constexpr Uuid kPermeabilityHenryPerMetre{0x2750};
+ static constexpr Uuid kMolarEnergyJoulePerMole{0x2751};
+ static constexpr Uuid kMolarEntropyJoulePerMoleKelvin{0x2752};
+ static constexpr Uuid kExposureCoulombPerKilogram{0x2753};
+ static constexpr Uuid kAbsorbedDoseRateGrayPerSecond{0x2754};
+ static constexpr Uuid kRadiantIntensityWattPerSteradian{0x2755};
+ static constexpr Uuid kRadianceWattPerSquareMetreSteradian{0x2756};
+ static constexpr Uuid kCatalyticActivityConcentrationKatalPerCubicMetre{
+ 0x2757};
+ static constexpr Uuid kTimeMinute{0x2760};
+ static constexpr Uuid kTimeHour{0x2761};
+ static constexpr Uuid kTimeDay{0x2762};
+ static constexpr Uuid kPlaneAngleDegree{0x2763};
+ static constexpr Uuid kPlaneAngleMinute{0x2764};
+ static constexpr Uuid kPlaneAngleSecond{0x2765};
+ static constexpr Uuid kAreaHectare{0x2766};
+ static constexpr Uuid kVolumeLitre{0x2767};
+ static constexpr Uuid kMassTonne{0x2768};
+ static constexpr Uuid kPressureBar{0x2780};
+ static constexpr Uuid kPressureMillimetreOfMercury{0x2781};
+ static constexpr Uuid kLengthÅngström{0x2782};
+ static constexpr Uuid kLengthNauticalMile{0x2783};
+ static constexpr Uuid kAreaBarn{0x2784};
+ static constexpr Uuid kVelocityKnot{0x2785};
+ static constexpr Uuid kLogarithmicRadioQuantityNeper{0x2786};
+ static constexpr Uuid kLogarithmicRadioQuantityBel{0x2787};
+ static constexpr Uuid kLengthYard{0x27a0};
+ static constexpr Uuid kLengthParsec{0x27a1};
+ static constexpr Uuid kLengthInch{0x27a2};
+ static constexpr Uuid kLengthFoot{0x27a3};
+ static constexpr Uuid kLengthMile{0x27a4};
+ static constexpr Uuid kPressurePoundForcePerSquareInch{0x27a5};
+ static constexpr Uuid kVelocityKilometrePerHour{0x27a6};
+ static constexpr Uuid kVelocityMilePerHour{0x27a7};
+ static constexpr Uuid kAngularVelocityRevolutionPerMinute{0x27a8};
+ static constexpr Uuid kEnergyGramCalorie{0x27a9};
+ static constexpr Uuid kEnergyKilogramCalorie{0x27aa};
+ static constexpr Uuid kEnergyKilowattHour{0x27ab};
+ static constexpr Uuid kThermodynamicTemperatureDegreeFahrenheit{0x27ac};
+ static constexpr Uuid kPercentage{0x27ad};
+ static constexpr Uuid kPerMille{0x27ae};
+ static constexpr Uuid kPeriodBeatsPerMinute{0x27af};
+ static constexpr Uuid kElectricChargeAmpereHours{0x27b0};
+ static constexpr Uuid kMassDensityMilligramPerDecilitre{0x27b1};
+ static constexpr Uuid kMassDensityMillimolePerLitre{0x27b2};
+ static constexpr Uuid kTimeYear{0x27b3};
+ static constexpr Uuid kTimeMonth{0x27b4};
+ static constexpr Uuid kConcentrationCountPerCubicMetre{0x27b5};
+ static constexpr Uuid kIrradianceWattPerSquareMetre{0x27b6};
+ static constexpr Uuid kMilliliterPerKilogramPerMinute{0x27b7};
+ static constexpr Uuid kMassPound{0x27b8};
+ static constexpr Uuid kMetabolicEquivalent{0x27b9};
+ static constexpr Uuid kStepPerMinute{0x27ba};
+ static constexpr Uuid kStrokePerMinute{0x27bc};
+ static constexpr Uuid kPaceKilometrePerMinute{0x27bd};
+ static constexpr Uuid kLuminousEfficacyLumenPerWatt{0x27be};
+ static constexpr Uuid kLuminousEnergyLumenHour{0x27bf};
+ static constexpr Uuid kLuminousExposureLuxHour{0x27c0};
+ static constexpr Uuid kMassFlowGramPerSecond{0x27c1};
+ static constexpr Uuid kVolumeFlowLitrePerSecond{0x27c2};
+ static constexpr Uuid kSoundPressureDecibel{0x27c3};
+ static constexpr Uuid kPartsPerMillion{0x27c4};
+ static constexpr Uuid kPartsPerBillion{0x27c5};
+ static constexpr Uuid kMassDensityRateMilligramPerDecilitrePerMinute{0x27c6};
+ static constexpr Uuid kElectricalApparentEnergyKilovoltAmpereHour{0x27c7};
+ static constexpr Uuid kElectricalApparentPowerVoltAmpere{0x27c8};
+};
+
+// GATT Characteristic and Object Type
+class GattCharacteristicUuid {
+ public:
+ static constexpr Uuid kDeviceName{0x2a00};
+ static constexpr Uuid kAppearance{0x2a01};
+ static constexpr Uuid kPeripheralPrivacyFlag{0x2a02};
+ static constexpr Uuid kReconnectionAddress{0x2a03};
+ static constexpr Uuid kPeripheralPreferredConnectionParameters{0x2a04};
+ static constexpr Uuid kServiceChanged{0x2a05};
+ static constexpr Uuid kAlertLevel{0x2a06};
+ static constexpr Uuid kTxPowerLevel{0x2a07};
+ static constexpr Uuid kDateTime{0x2a08};
+ static constexpr Uuid kDayOfWeek{0x2a09};
+ static constexpr Uuid kDayDateTime{0x2a0a};
+ static constexpr Uuid kExactTime256{0x2a0c};
+ static constexpr Uuid kDstOffset{0x2a0d};
+ static constexpr Uuid kTimeZone{0x2a0e};
+ static constexpr Uuid kLocalTimeInformation{0x2a0f};
+ static constexpr Uuid kTimeWithDst{0x2a11};
+ static constexpr Uuid kTimeAccuracy{0x2a12};
+ static constexpr Uuid kTimeSource{0x2a13};
+ static constexpr Uuid kReferenceTimeInformation{0x2a14};
+ static constexpr Uuid kTimeUpdateControlPoint{0x2a16};
+ static constexpr Uuid kTimeUpdateState{0x2a17};
+ static constexpr Uuid kGlucoseMeasurement{0x2a18};
+ static constexpr Uuid kBatteryLevel{0x2a19};
+ static constexpr Uuid kTemperatureMeasurement{0x2a1c};
+ static constexpr Uuid kTemperatureType{0x2a1d};
+ static constexpr Uuid kIntermediateTemperature{0x2a1e};
+ static constexpr Uuid kMeasurementInterval{0x2a21};
+ static constexpr Uuid kBootKeyboardInputReport{0x2a22};
+ static constexpr Uuid kSystemId{0x2a23};
+ static constexpr Uuid kModelNumberString{0x2a24};
+ static constexpr Uuid kSerialNumberString{0x2a25};
+ static constexpr Uuid kFirmwareRevisionString{0x2a26};
+ static constexpr Uuid kHardwareRevisionString{0x2a27};
+ static constexpr Uuid kSoftwareRevisionString{0x2a28};
+ static constexpr Uuid kManufacturerNameString{0x2a29};
+ static constexpr Uuid kIeee1107320601RegulatoryCertificationDataList{0x2a2a};
+ static constexpr Uuid kCurrentTime{0x2a2b};
+ static constexpr Uuid kMagneticDeclination{0x2a2c};
+ static constexpr Uuid kScanRefresh{0x2a31};
+ static constexpr Uuid kBootKeyboardOutputReport{0x2a32};
+ static constexpr Uuid kBootMouseInputReport{0x2a33};
+ static constexpr Uuid kGlucoseMeasurementContext{0x2a34};
+ static constexpr Uuid kBloodPressureMeasurement{0x2a35};
+ static constexpr Uuid kIntermediateCuffPressure{0x2a36};
+ static constexpr Uuid kHeartRateMeasurement{0x2a37};
+ static constexpr Uuid kBodySensorLocation{0x2a38};
+ static constexpr Uuid kHeartRateControlPoint{0x2a39};
+ static constexpr Uuid kAlertStatus{0x2a3f};
+ static constexpr Uuid kRingerControlPoint{0x2a40};
+ static constexpr Uuid kRingerSetting{0x2a41};
+ static constexpr Uuid kAlertCategoryIdBitMask{0x2a42};
+ static constexpr Uuid kAlertCategoryId{0x2a43};
+ static constexpr Uuid kAlertNotificationControlPoint{0x2a44};
+ static constexpr Uuid kUnreadAlertStatus{0x2a45};
+ static constexpr Uuid kNewAlert{0x2a46};
+ static constexpr Uuid kSupportedNewAlertCategory{0x2a47};
+ static constexpr Uuid kSupportedUnreadAlertCategory{0x2a48};
+ static constexpr Uuid kBloodPressureFeature{0x2a49};
+ static constexpr Uuid kHidInformation{0x2a4a};
+ static constexpr Uuid kReportMap{0x2a4b};
+ static constexpr Uuid kHidControlPoint{0x2a4c};
+ static constexpr Uuid kReport{0x2a4d};
+ static constexpr Uuid kProtocolMode{0x2a4e};
+ static constexpr Uuid kScanIntervalWindow{0x2a4f};
+ static constexpr Uuid kPnpId{0x2a50};
+ static constexpr Uuid kGlucoseFeature{0x2a51};
+ static constexpr Uuid kRecordAccessControlPoint{0x2a52};
+ static constexpr Uuid kRscMeasurement{0x2a53};
+ static constexpr Uuid kRscFeature{0x2a54};
+ static constexpr Uuid kScControlPoint{0x2a55};
+ static constexpr Uuid kAggregate{0x2a5a};
+ static constexpr Uuid kCscMeasurement{0x2a5b};
+ static constexpr Uuid kCscFeature{0x2a5c};
+ static constexpr Uuid kSensorLocation{0x2a5d};
+ static constexpr Uuid kPlxSpotCheckMeasurement{0x2a5e};
+ static constexpr Uuid kPlxContinuousMeasurement{0x2a5f};
+ static constexpr Uuid kPlxFeatures{0x2a60};
+ static constexpr Uuid kCyclingPowerMeasurement{0x2a63};
+ static constexpr Uuid kCyclingPowerVector{0x2a64};
+ static constexpr Uuid kCyclingPowerFeature{0x2a65};
+ static constexpr Uuid kCyclingPowerControlPoint{0x2a66};
+ static constexpr Uuid kLocationAndSpeed{0x2a67};
+ static constexpr Uuid kNavigation{0x2a68};
+ static constexpr Uuid kPositionQuality{0x2a69};
+ static constexpr Uuid kLnFeature{0x2a6a};
+ static constexpr Uuid kLnControlPoint{0x2a6b};
+ static constexpr Uuid kElevation{0x2a6c};
+ static constexpr Uuid kPressure{0x2a6d};
+ static constexpr Uuid kTemperature{0x2a6e};
+ static constexpr Uuid kHumidity{0x2a6f};
+ static constexpr Uuid kTrueWindSpeed{0x2a70};
+ static constexpr Uuid kTrueWindDirection{0x2a71};
+ static constexpr Uuid kApparentWindSpeed{0x2a72};
+ static constexpr Uuid kApparentWindDirection{0x2a73};
+ static constexpr Uuid kGustFactor{0x2a74};
+ static constexpr Uuid kPollenConcentration{0x2a75};
+ static constexpr Uuid kUvIndex{0x2a76};
+ static constexpr Uuid kIrradiance{0x2a77};
+ static constexpr Uuid kRainfall{0x2a78};
+ static constexpr Uuid kWindChill{0x2a79};
+ static constexpr Uuid kHeatIndex{0x2a7a};
+ static constexpr Uuid kDewPoint{0x2a7b};
+ static constexpr Uuid kDescriptorValueChanged{0x2a7d};
+ static constexpr Uuid kAerobicHeartRateLowerLimit{0x2a7e};
+ static constexpr Uuid kAerobicThreshold{0x2a7f};
+ static constexpr Uuid kAge{0x2a80};
+ static constexpr Uuid kAnaerobicHeartRateLowerLimit{0x2a81};
+ static constexpr Uuid kAnaerobicHeartRateUpperLimit{0x2a82};
+ static constexpr Uuid kAnaerobicThreshold{0x2a83};
+ static constexpr Uuid kAerobicHeartRateUpperLimit{0x2a84};
+ static constexpr Uuid kDateOfBirth{0x2a85};
+ static constexpr Uuid kDateOfThresholdAssessment{0x2a86};
+ static constexpr Uuid kEmailAddress{0x2a87};
+ static constexpr Uuid kFatBurnHeartRateLowerLimit{0x2a88};
+ static constexpr Uuid kFatBurnHeartRateUpperLimit{0x2a89};
+ static constexpr Uuid kFirstName{0x2a8a};
+ static constexpr Uuid kFiveZoneHeartRateLimits{0x2a8b};
+ static constexpr Uuid kGender{0x2a8c};
+ static constexpr Uuid kHeartRateMax{0x2a8d};
+ static constexpr Uuid kHeight{0x2a8e};
+ static constexpr Uuid kHipCircumference{0x2a8f};
+ static constexpr Uuid kLastName{0x2a90};
+ static constexpr Uuid kMaximumRecommendedHeartRate{0x2a91};
+ static constexpr Uuid kRestingHeartRate{0x2a92};
+ static constexpr Uuid kSportTypeForAerobicAndAnaerobicThresholds{0x2a93};
+ static constexpr Uuid kThreeZoneHeartRateLimits{0x2a94};
+ static constexpr Uuid kTwoZoneHeartRateLimits{0x2a95};
+ static constexpr Uuid kVo2Max{0x2a96};
+ static constexpr Uuid kWaistCircumference{0x2a97};
+ static constexpr Uuid kWeight{0x2a98};
+ static constexpr Uuid kDatabaseChangeIncrement{0x2a99};
+ static constexpr Uuid kUserIndex{0x2a9a};
+ static constexpr Uuid kBodyCompositionFeature{0x2a9b};
+ static constexpr Uuid kBodyCompositionMeasurement{0x2a9c};
+ static constexpr Uuid kWeightMeasurement{0x2a9d};
+ static constexpr Uuid kWeightScaleFeature{0x2a9e};
+ static constexpr Uuid kUserControlPoint{0x2a9f};
+ static constexpr Uuid kMagneticFluxDensity2d{0x2aa0};
+ static constexpr Uuid kMagneticFluxDensity3d{0x2aa1};
+ static constexpr Uuid kLanguage{0x2aa2};
+ static constexpr Uuid kBarometricPressureTrend{0x2aa3};
+ static constexpr Uuid kBondManagementControlPoint{0x2aa4};
+ static constexpr Uuid kBondManagementFeature{0x2aa5};
+ static constexpr Uuid kCentralAddressResolution{0x2aa6};
+ static constexpr Uuid kCgmMeasurement{0x2aa7};
+ static constexpr Uuid kCgmFeature{0x2aa8};
+ static constexpr Uuid kCgmStatus{0x2aa9};
+ static constexpr Uuid kCgmSessionStartTime{0x2aaa};
+ static constexpr Uuid kCgmSessionRunTime{0x2aab};
+ static constexpr Uuid kCgmSpecificOpsControlPoint{0x2aac};
+ static constexpr Uuid kIndoorPositioningConfiguration{0x2aad};
+ static constexpr Uuid kLatitude{0x2aae};
+ static constexpr Uuid kLongitude{0x2aaf};
+ static constexpr Uuid kLocalNorthCoordinate{0x2ab0};
+ static constexpr Uuid kLocalEastCoordinate{0x2ab1};
+ static constexpr Uuid kFloorNumber{0x2ab2};
+ static constexpr Uuid kAltitude{0x2ab3};
+ static constexpr Uuid kUncertainty{0x2ab4};
+ static constexpr Uuid kLocationName{0x2ab5};
+ static constexpr Uuid kUri{0x2ab6};
+ static constexpr Uuid kHttpHeaders{0x2ab7};
+ static constexpr Uuid kHttpStatusCode{0x2ab8};
+ static constexpr Uuid kHttpEntityBody{0x2ab9};
+ static constexpr Uuid kHttpControlPoint{0x2aba};
+ static constexpr Uuid kHttpsSecurity{0x2abb};
+ static constexpr Uuid kTdsControlPoint{0x2abc};
+ static constexpr Uuid kOtsFeature{0x2abd};
+ static constexpr Uuid kObjectName{0x2abe};
+ static constexpr Uuid kObjectType{0x2abf};
+ static constexpr Uuid kObjectSize{0x2ac0};
+ static constexpr Uuid kObjectFirstCreated{0x2ac1};
+ static constexpr Uuid kObjectLastModified{0x2ac2};
+ static constexpr Uuid kObjectId{0x2ac3};
+ static constexpr Uuid kObjectProperties{0x2ac4};
+ static constexpr Uuid kObjectActionControlPoint{0x2ac5};
+ static constexpr Uuid kObjectListControlPoint{0x2ac6};
+ static constexpr Uuid kObjectListFilter{0x2ac7};
+ static constexpr Uuid kObjectChanged{0x2ac8};
+ static constexpr Uuid kResolvablePrivateAddressOnly{0x2ac9};
+ static constexpr Uuid kUnspecified{0x2aca};
+ static constexpr Uuid kDirectoryListing{0x2acb};
+ static constexpr Uuid kFitnessMachineFeature{0x2acc};
+ static constexpr Uuid kTreadmillData{0x2acd};
+ static constexpr Uuid kCrossTrainerData{0x2ace};
+ static constexpr Uuid kStepClimberData{0x2acf};
+ static constexpr Uuid kStairClimberData{0x2ad0};
+ static constexpr Uuid kRowerData{0x2ad1};
+ static constexpr Uuid kIndoorBikeData{0x2ad2};
+ static constexpr Uuid kTrainingStatus{0x2ad3};
+ static constexpr Uuid kSupportedSpeedRange{0x2ad4};
+ static constexpr Uuid kSupportedInclinationRange{0x2ad5};
+ static constexpr Uuid kSupportedResistanceLevelRange{0x2ad6};
+ static constexpr Uuid kSupportedHeartRateRange{0x2ad7};
+ static constexpr Uuid kSupportedPowerRange{0x2ad8};
+ static constexpr Uuid kFitnessMachineControlPoint{0x2ad9};
+ static constexpr Uuid kFitnessMachineStatus{0x2ada};
+ static constexpr Uuid kMeshProvisioningDataIn{0x2adb};
+ static constexpr Uuid kMeshProvisioningDataOut{0x2adc};
+ static constexpr Uuid kMeshProxyDataIn{0x2add};
+ static constexpr Uuid kMeshProxyDataOut{0x2ade};
+ static constexpr Uuid kAverageCurrent{0x2ae0};
+ static constexpr Uuid kAverageVoltage{0x2ae1};
+ static constexpr Uuid kBoolean{0x2ae2};
+ static constexpr Uuid kChromaticDistanceFromPlanckian{0x2ae3};
+ static constexpr Uuid kChromaticityCoordinates{0x2ae4};
+ static constexpr Uuid kChromaticityInCctAndDuvValues{0x2ae5};
+ static constexpr Uuid kChromaticityTolerance{0x2ae6};
+ static constexpr Uuid kCie1331995ColorRenderingIndex{0x2ae7};
+ static constexpr Uuid kCoefficient{0x2ae8};
+ static constexpr Uuid kCorrelatedColorTemperature{0x2ae9};
+ static constexpr Uuid kCount16{0x2aea};
+ static constexpr Uuid kCount24{0x2aeb};
+ static constexpr Uuid kCountryCode{0x2aec};
+ static constexpr Uuid kDateUtc{0x2aed};
+ static constexpr Uuid kElectricCurrent{0x2aee};
+ static constexpr Uuid kElectricCurrentRange{0x2aef};
+ static constexpr Uuid kElectricCurrentSpecification{0x2af0};
+ static constexpr Uuid kElectricCurrentStatistics{0x2af1};
+ static constexpr Uuid kEnergy{0x2af2};
+ static constexpr Uuid kEnergyInAPeriodOfDay{0x2af3};
+ static constexpr Uuid kEventStatistics{0x2af4};
+ static constexpr Uuid kFixedString16{0x2af5};
+ static constexpr Uuid kFixedString24{0x2af6};
+ static constexpr Uuid kFixedString36{0x2af7};
+ static constexpr Uuid kFixedString8{0x2af8};
+ static constexpr Uuid kGenericLevel{0x2af9};
+ static constexpr Uuid kGlobalTradeItemNumber{0x2afa};
+ static constexpr Uuid kIlluminance{0x2afb};
+ static constexpr Uuid kLuminousEfficacy{0x2afc};
+ static constexpr Uuid kLuminousEnergy{0x2afd};
+ static constexpr Uuid kLuminousExposure{0x2afe};
+ static constexpr Uuid kLuminousFlux{0x2aff};
+ static constexpr Uuid kLuminousFluxRange{0x2b00};
+ static constexpr Uuid kLuminousIntensity{0x2b01};
+ static constexpr Uuid kMassFlow{0x2b02};
+ static constexpr Uuid kPerceivedLightness{0x2b03};
+ static constexpr Uuid kPercentage8{0x2b04};
+ static constexpr Uuid kPower{0x2b05};
+ static constexpr Uuid kPowerSpecification{0x2b06};
+ static constexpr Uuid kRelativeRuntimeInACurrentRange{0x2b07};
+ static constexpr Uuid kRelativeRuntimeInAGenericLevelRange{0x2b08};
+ static constexpr Uuid kRelativeValueInAVoltageRange{0x2b09};
+ static constexpr Uuid kRelativeValueInAnIlluminanceRange{0x2b0a};
+ static constexpr Uuid kRelativeValueInAPeriodOfDay{0x2b0b};
+ static constexpr Uuid kRelativeValueInATemperatureRange{0x2b0c};
+ static constexpr Uuid kTemperature8{0x2b0d};
+ static constexpr Uuid kTemperature8InAPeriodOfDay{0x2b0e};
+ static constexpr Uuid kTemperature8Statistics{0x2b0f};
+ static constexpr Uuid kTemperatureRange{0x2b10};
+ static constexpr Uuid kTemperatureStatistics{0x2b11};
+ static constexpr Uuid kTimeDecihour8{0x2b12};
+ static constexpr Uuid kTimeExponential8{0x2b13};
+ static constexpr Uuid kTimeHour24{0x2b14};
+ static constexpr Uuid kTimeMillisecond24{0x2b15};
+ static constexpr Uuid kTimeSecond16{0x2b16};
+ static constexpr Uuid kTimeSecond8{0x2b17};
+ static constexpr Uuid kVoltage{0x2b18};
+ static constexpr Uuid kVoltageSpecification{0x2b19};
+ static constexpr Uuid kVoltageStatistics{0x2b1a};
+ static constexpr Uuid kVolumeFlow{0x2b1b};
+ static constexpr Uuid kChromaticityCoordinate{0x2b1c};
+ static constexpr Uuid kRcFeature{0x2b1d};
+ static constexpr Uuid kRcSettings{0x2b1e};
+ static constexpr Uuid kReconnectionConfigurationControlPoint{0x2b1f};
+ static constexpr Uuid kIddStatusChanged{0x2b20};
+ static constexpr Uuid kIddStatus{0x2b21};
+ static constexpr Uuid kIddAnnunciationStatus{0x2b22};
+ static constexpr Uuid kIddFeatures{0x2b23};
+ static constexpr Uuid kIddStatusReaderControlPoint{0x2b24};
+ static constexpr Uuid kIddCommandControlPoint{0x2b25};
+ static constexpr Uuid kIddCommandData{0x2b26};
+ static constexpr Uuid kIddRecordAccessControlPoint{0x2b27};
+ static constexpr Uuid kIddHistoryData{0x2b28};
+ static constexpr Uuid kClientSupportedFeatures{0x2b29};
+ static constexpr Uuid kDatabaseHash{0x2b2a};
+ static constexpr Uuid kBssControlPoint{0x2b2b};
+ static constexpr Uuid kBssResponse{0x2b2c};
+ static constexpr Uuid kEmergencyId{0x2b2d};
+ static constexpr Uuid kEmergencyText{0x2b2e};
+ static constexpr Uuid kAcsStatus{0x2b2f};
+ static constexpr Uuid kAcsDataIn{0x2b30};
+ static constexpr Uuid kAcsDataOutNotify{0x2b31};
+ static constexpr Uuid kAcsDataOutIndicate{0x2b32};
+ static constexpr Uuid kAcsControlPoint{0x2b33};
+ static constexpr Uuid kEnhancedBloodPressureMeasurement{0x2b34};
+ static constexpr Uuid kEnhancedIntermediateCuffPressure{0x2b35};
+ static constexpr Uuid kBloodPressureRecord{0x2b36};
+ static constexpr Uuid kRegisteredUser{0x2b37};
+ static constexpr Uuid kBrEdrHandoverData{0x2b38};
+ static constexpr Uuid kBluetoothSigData{0x2b39};
+ static constexpr Uuid kServerSupportedFeatures{0x2b3a};
+ static constexpr Uuid kPhysicalActivityMonitorFeatures{0x2b3b};
+ static constexpr Uuid kGeneralActivityInstantaneousData{0x2b3c};
+ static constexpr Uuid kGeneralActivitySummaryData{0x2b3d};
+ static constexpr Uuid kCardiorespiratoryActivityInstantaneousData{0x2b3e};
+ static constexpr Uuid kCardiorespiratoryActivitySummaryData{0x2b3f};
+ static constexpr Uuid kStepCounterActivitySummaryData{0x2b40};
+ static constexpr Uuid kSleepActivityInstantaneousData{0x2b41};
+ static constexpr Uuid kSleepActivitySummaryData{0x2b42};
+ static constexpr Uuid kPhysicalActivityMonitorControlPoint{0x2b43};
+ static constexpr Uuid kActivityCurrentSession{0x2b44};
+ static constexpr Uuid kPhysicalActivitySessionDescriptor{0x2b45};
+ static constexpr Uuid kPreferredUnits{0x2b46};
+ static constexpr Uuid kHighResolutionHeight{0x2b47};
+ static constexpr Uuid kMiddleName{0x2b48};
+ static constexpr Uuid kStrideLength{0x2b49};
+ static constexpr Uuid kHandedness{0x2b4a};
+ static constexpr Uuid kDeviceWearingPosition{0x2b4b};
+ static constexpr Uuid kFourZoneHeartRateLimits{0x2b4c};
+ static constexpr Uuid kHighIntensityExerciseThreshold{0x2b4d};
+ static constexpr Uuid kActivityGoal{0x2b4e};
+ static constexpr Uuid kSedentaryIntervalNotification{0x2b4f};
+ static constexpr Uuid kCaloricIntake{0x2b50};
+ static constexpr Uuid kTmapRole{0x2b51};
+ static constexpr Uuid kAudioInputState{0x2b77};
+ static constexpr Uuid kGainSettingsAttribute{0x2b78};
+ static constexpr Uuid kAudioInputType{0x2b79};
+ static constexpr Uuid kAudioInputStatus{0x2b7a};
+ static constexpr Uuid kAudioInputControlPoint{0x2b7b};
+ static constexpr Uuid kAudioInputDescription{0x2b7c};
+ static constexpr Uuid kVolumeState{0x2b7d};
+ static constexpr Uuid kVolumeControlPoint{0x2b7e};
+ static constexpr Uuid kVolumeFlags{0x2b7f};
+ static constexpr Uuid kVolumeOffsetState{0x2b80};
+ static constexpr Uuid kAudioLocation{0x2b81};
+ static constexpr Uuid kVolumeOffsetControlPoint{0x2b82};
+ static constexpr Uuid kAudioOutputDescription{0x2b83};
+ static constexpr Uuid kSetIdentityResolvingKey{0x2b84};
+ static constexpr Uuid kCoordinatedSetSize{0x2b85};
+ static constexpr Uuid kSetMemberLock{0x2b86};
+ static constexpr Uuid kSetMemberRank{0x2b87};
+ static constexpr Uuid kApparentEnergy32{0x2b89};
+ static constexpr Uuid kApparentPower{0x2b8a};
+ static constexpr Uuid kCo2Concentration{0x2b8c};
+ static constexpr Uuid kCosineOfTheAngle{0x2b8d};
+ static constexpr Uuid kDeviceTimeFeature{0x2b8e};
+ static constexpr Uuid kDeviceTimeParameters{0x2b8f};
+ static constexpr Uuid kDeviceTime{0x2b90};
+ static constexpr Uuid kDeviceTimeControlPoint{0x2b91};
+ static constexpr Uuid kTimeChangeLogData{0x2b92};
+ static constexpr Uuid kMediaPlayerName{0x2b93};
+ static constexpr Uuid kMediaPlayerIconObjectId{0x2b94};
+ static constexpr Uuid kMediaPlayerIconUrl{0x2b95};
+ static constexpr Uuid kTrackChanged{0x2b96};
+ static constexpr Uuid kTrackTitle{0x2b97};
+ static constexpr Uuid kTrackDuration{0x2b98};
+ static constexpr Uuid kTrackPosition{0x2b99};
+ static constexpr Uuid kPlaybackSpeed{0x2b9a};
+ static constexpr Uuid kSeekingSpeed{0x2b9b};
+ static constexpr Uuid kCurrentTrackSegmentsObjectId{0x2b9c};
+ static constexpr Uuid kCurrentTrackObjectId{0x2b9d};
+ static constexpr Uuid kNextTrackObjectId{0x2b9e};
+ static constexpr Uuid kParentGroupObjectId{0x2b9f};
+ static constexpr Uuid kCurrentGroupObjectId{0x2ba0};
+ static constexpr Uuid kPlayingOrder{0x2ba1};
+ static constexpr Uuid kPlayingOrdersSupported{0x2ba2};
+ static constexpr Uuid kMediaState{0x2ba3};
+ static constexpr Uuid kMediaControlPoint{0x2ba4};
+ static constexpr Uuid kMediaControlPointOpcodesSupported{0x2ba5};
+ static constexpr Uuid kSearchResultsObjectId{0x2ba6};
+ static constexpr Uuid kSearchControlPoint{0x2ba7};
+ static constexpr Uuid kEnergy32{0x2ba8};
+ static constexpr Uuid kMediaPlayerIconObjectType{0x2ba9};
+ static constexpr Uuid kTrackSegmentsObjectType{0x2baa};
+ static constexpr Uuid kTrackObjectType{0x2bab};
+ static constexpr Uuid kGroupObjectType{0x2bac};
+ static constexpr Uuid kConstantToneExtensionEnable{0x2bad};
+ static constexpr Uuid kAdvertisingConstantToneExtensionMinimumLength{0x2bae};
+ static constexpr Uuid kAdvertisingConstantToneExtensionMinimumTransmitCount{
+ 0x2baf};
+ static constexpr Uuid kAdvertisingConstantToneExtensionTransmitDuration{
+ 0x2bb0};
+ static constexpr Uuid kAdvertisingConstantToneExtensionInterval{0x2bb1};
+ static constexpr Uuid kAdvertisingConstantToneExtensionPhy{0x2bb2};
+ static constexpr Uuid kBearerProviderName{0x2bb3};
+ static constexpr Uuid kBearerUci{0x2bb4};
+ static constexpr Uuid kBearerTechnology{0x2bb5};
+ static constexpr Uuid kBearerUriSchemesSupportedList{0x2bb6};
+ static constexpr Uuid kBearerSignalStrength{0x2bb7};
+ static constexpr Uuid kBearerSignalStrengthReportingInterval{0x2bb8};
+ static constexpr Uuid kBearerListCurrentCalls{0x2bb9};
+ static constexpr Uuid kContentControlId{0x2bba};
+ static constexpr Uuid kStatusFlags{0x2bbb};
+ static constexpr Uuid kIncomingCallTargetBearerUri{0x2bbc};
+ static constexpr Uuid kCallState{0x2bbd};
+ static constexpr Uuid kCallControlPoint{0x2bbe};
+ static constexpr Uuid kCallControlPointOptionalOpcodes{0x2bbf};
+ static constexpr Uuid kTerminationReason{0x2bc0};
+ static constexpr Uuid kIncomingCall{0x2bc1};
+ static constexpr Uuid kCallFriendlyName{0x2bc2};
+ static constexpr Uuid kMute{0x2bc3};
+ static constexpr Uuid kSinkAse{0x2bc4};
+ static constexpr Uuid kSourceAse{0x2bc5};
+ static constexpr Uuid kAseControlPoint{0x2bc6};
+ static constexpr Uuid kBroadcastAudioScanControlPoint{0x2bc7};
+ static constexpr Uuid kBroadcastReceiveState{0x2bc8};
+ static constexpr Uuid kSinkPac{0x2bc9};
+ static constexpr Uuid kSinkAudioLocations{0x2bca};
+ static constexpr Uuid kSourcePac{0x2bcb};
+ static constexpr Uuid kSourceAudioLocations{0x2bcc};
+ static constexpr Uuid kAvailableAudioContexts{0x2bcd};
+ static constexpr Uuid kSupportedAudioContexts{0x2bce};
+ static constexpr Uuid kAmmoniaConcentration{0x2bcf};
+ static constexpr Uuid kCarbonMonoxideConcentration{0x2bd0};
+ static constexpr Uuid kMethaneConcentration{0x2bd1};
+ static constexpr Uuid kNitrogenDioxideConcentration{0x2bd2};
+ static constexpr Uuid kNonMethaneVolatileOrganicCompoundsConcentration{
+ 0x2bd3};
+ static constexpr Uuid kOzoneConcentration{0x2bd4};
+ static constexpr Uuid kParticulateMatterPm1Concentration{0x2bd5};
+ static constexpr Uuid kParticulateMatterPm25Concentration{0x2bd6};
+ static constexpr Uuid kParticulateMatterPm10Concentration{0x2bd7};
+ static constexpr Uuid kSulfurDioxideConcentration{0x2bd8};
+ static constexpr Uuid kSulfurHexafluorideConcentration{0x2bd9};
+ static constexpr Uuid kHearingAidFeatures{0x2bda};
+ static constexpr Uuid kHearingAidPresetControlPoint{0x2bdb};
+ static constexpr Uuid kActivePresetIndex{0x2bdc};
+ static constexpr Uuid kFixedString64{0x2bde};
+ static constexpr Uuid kHighTemperature{0x2bdf};
+ static constexpr Uuid kHighVoltage{0x2be0};
+ static constexpr Uuid kLightDistribution{0x2be1};
+ static constexpr Uuid kLightOutput{0x2be2};
+ static constexpr Uuid kLightSourceType{0x2be3};
+ static constexpr Uuid kNoise{0x2be4};
+ static constexpr Uuid kRelativeRuntimeInACorrelatedColorTemperatureRange{
+ 0x2be5};
+ static constexpr Uuid kTimeSecond32{0x2be6};
+ static constexpr Uuid kVocConcentration{0x2be7};
+ static constexpr Uuid kVoltageFrequency{0x2be8};
+};
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/constants.h b/pw_bluetooth/public/pw_bluetooth/constants.h
new file mode 100644
index 000000000..549dff465
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/constants.h
@@ -0,0 +1,22 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+namespace pw::bluetooth {
+
+// The maximum length of a device name. This value was selected based on the HCI
+// and GAP specifications (v5.2, Vol 4, Part E, 7.3.11 and Vol 3, Part C, 12.1).
+constexpr uint8_t kMaxDeviceNameLength = 248;
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/controller.h b/pw_bluetooth/public/pw_bluetooth/controller.h
new file mode 100644
index 000000000..2461dfe4b
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/controller.h
@@ -0,0 +1,164 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include "pw_bluetooth/vendor.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+#include "pw_result/result.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace pw::bluetooth {
+
+// The Controller class is a shim for communication between the Host and the
+// Controller.
+class Controller {
+ public:
+ // The lifetime of the span is only guaranteed for the lifetime of the
+ // function call.
+ using DataFunction = Function<void(span<const std::byte>)>;
+
+ // Bitmask of features the controller supports.
+ enum class FeaturesBits : uint32_t {
+ // Indicates support for transferring Synchronous Connection-Oriented link
+ // data over the HCI. Offloaded SCO links may still be supported even if HCI
+ // SCO isn't.
+ kHciSco = (1 << 0),
+ // Indicates support for the Set Acl Priority command.
+ kSetAclPriorityCommand = (1 << 1),
+ // Indicates support for the `LE_Get_Vendor_Capabilities` command documented
+ // at
+ // https://source.android.com/docs/core/connect/bluetooth/hci_requirements
+ kAndroidVendorExtensions = (1 << 2),
+ // Bits 3-31 reserved.
+ };
+
+ enum class ScoCodingFormat : uint8_t {
+ kCvsd,
+ kMsbc,
+ };
+
+ enum class ScoEncoding : uint8_t {
+ k8Bits,
+ k16Bits,
+ };
+
+ enum class ScoSampleRate : uint8_t {
+ k8Khz,
+ k16Khz,
+ };
+
+ // Closes the controller. `Close` should be called first to safely clean up
+ // state (which may be an asynchronous process).
+ virtual ~Controller() = default;
+
+ // Sets a function that will be called with HCI event packets received from
+ // the controller. This should be called before `Initialize` or else incoming
+ // packets will be dropped. The lifetime of data passed to `func` is only
+ // guaranteed for the lifetime of the function call.
+ virtual void SetEventFunction(DataFunction func) = 0;
+
+ // Sets a function that will be called with ACL data packets received from the
+ // controller. This should be called before `Initialize` or else incoming
+ // packets will be dropped. The lifetime of data passed to `func` is only
+ // guaranteed for the lifetime of the function call.
+ virtual void SetReceiveAclFunction(DataFunction func) = 0;
+
+ // Sets a function that will be called with SCO packets received from the
+ // controller. On Classic and Dual Mode stacks, this should be called before
+ // `Initialize` or else incoming packets will be dropped. The lifetime of data
+ // passed to `func` is only guaranteed for the lifetime of the function call.
+ virtual void SetReceiveScoFunction(DataFunction func) = 0;
+
+ // Initializes the controller interface and starts processing packets.
+ // `complete_callback` will be called with the result of initialization.
+ // `error_callback` will be called for fatal errors that occur after
+ // initialization. After a fatal error, this object is invalid. `Close` should
+ // be called to ensure a safe clean up.
+ virtual void Initialize(Callback<void(Status)> complete_callback,
+ Callback<void(Status)> error_callback) = 0;
+
+ // Closes the controller interface, resetting all state. `callback` will be
+ // called when closure is complete. After this method is called, this object
+ // should be considered invalid and no other methods should be called
+ // (including `Initialize`).
+ // `callback` will be called with:
+ // OK - the controller interface was successfully closed, or is already closed
+ // INTERNAL - the controller interface could not be closed
+ virtual void Close(Callback<void(Status)> callback) = 0;
+
+ // Sends an HCI command packet to the controller.
+ virtual void SendCommand(span<const std::byte> command) = 0;
+
+ // Sends an ACL data packet to the controller.
+ virtual void SendAclData(span<const std::byte> data) = 0;
+
+ // Sends a SCO data packet to the controller.
+ virtual void SendScoData(span<const std::byte> data) = 0;
+
+ // Configure the HCI for a SCO connection with the indicated parameters.
+ // `SetReceiveScoFunction` must be called before calling this method.
+ // `callback will be called with:
+ // OK - success, packets can be sent/received.
+ // UNIMPLEMENTED - the implementation/controller does not support SCO over HCI
+ // ALREADY_EXISTS - a SCO connection is already configured
+ // INTERNAL - an internal error occurred
+ virtual void ConfigureSco(ScoCodingFormat coding_format,
+ ScoEncoding encoding,
+ ScoSampleRate sample_rate,
+ Callback<void(Status)> callback) = 0;
+
+ // Releases the resources held by an active SCO connection. This should be
+ // called when a SCO connection is closed. `ConfigureSco` must be called
+ // before calling this method.
+ // `callback will be called with:
+ // OK - success, the SCO configuration was reset.
+ // UNIMPLEMENTED - the implementation/controller does not support SCO over HCI
+ // INTERNAL - an internal error occurred
+ virtual void ResetSco(Callback<void(Status)> callback) = 0;
+
+ // Calls `callback` with a bitmask of features supported by the controller.
+ virtual void GetFeatures(Callback<void(FeaturesBits)> callback) = 0;
+
+ // Encodes the vendor command indicated by `parameters`.
+ // `callback` will be called with the result of the encoding request.
+ // The lifetime of data passed to `callback` is only guaranteed for the
+ // lifetime of the function call.
+ virtual void EncodeVendorCommand(
+ VendorCommandParameters parameters,
+ Callback<void(Result<span<const std::byte>>)> callback) = 0;
+};
+
+inline constexpr bool operator&(Controller::FeaturesBits left,
+ Controller::FeaturesBits right) {
+ return static_cast<bool>(
+ static_cast<std::underlying_type_t<Controller::FeaturesBits>>(left) &
+ static_cast<std::underlying_type_t<Controller::FeaturesBits>>(right));
+}
+
+inline constexpr Controller::FeaturesBits operator|(
+ Controller::FeaturesBits left, Controller::FeaturesBits right) {
+ return static_cast<Controller::FeaturesBits>(
+ static_cast<std::underlying_type_t<Controller::FeaturesBits>>(left) |
+ static_cast<std::underlying_type_t<Controller::FeaturesBits>>(right));
+}
+
+inline constexpr Controller::FeaturesBits& operator|=(
+ Controller::FeaturesBits& left, Controller::FeaturesBits right) {
+ return left = left | right;
+}
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/client.h b/pw_bluetooth/public/pw_bluetooth/gatt/client.h
new file mode 100644
index 000000000..e12de22be
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/client.h
@@ -0,0 +1,307 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <memory>
+
+#include "pw_bluetooth/gatt/constants.h"
+#include "pw_bluetooth/gatt/error.h"
+#include "pw_bluetooth/gatt/types.h"
+#include "pw_bluetooth/internal/raii_ptr.h"
+#include "pw_bluetooth/result.h"
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+#include "pw_span/span.h"
+
+namespace pw::bluetooth::gatt {
+
+/// Represents a GATT service on a remote GATT server.
+/// Clients should call `SetErrorCallback` before using in order to handle fatal
+/// errors.
+class RemoteService {
+ public:
+ enum class RemoteServiceError {
+ /// The service has been modified or removed.
+ kServiceRemoved = 0,
+
+ /// The peer serving this service has disconnected.
+ kPeerDisconnected = 1,
+ };
+
+ /// Wrapper around a possible truncated value received from the server.
+ struct ReadValue {
+ /// Characteristic or descriptor handle.
+ Handle handle;
+
+ /// The value of the characteristic or descriptor.
+ Vector<std::byte> value;
+
+ /// True if `value` might be truncated (the buffer was completely filled by
+ /// the server and the read was a short read). `ReadCharacteristic` or
+ /// `ReadDescriptor` should be used to read the complete value.
+ bool maybe_truncated;
+ };
+
+ /// A result returned by `ReadByType`.
+ struct ReadByTypeResult {
+ /// Characteristic or descriptor handle.
+ Handle handle;
+
+ /// The value of the characteristic or descriptor, if it was read
+ /// successfully, or an error explaining why the value could not be read.
+ Result<Error, ReadValue> result;
+ };
+
+ /// Represents the supported options to read a long characteristic or
+ /// descriptor value from a server. Long values are those that may not fit in
+ /// a single message (longer than 22 bytes).
+ struct LongReadOptions {
+ /// The byte to start the read at. Must be less than the length of the
+ /// value.
+ uint16_t offset = 0;
+
+ /// The maximum number of bytes to read.
+ uint16_t max_bytes = kMaxValueLength;
+ };
+
+ /// Represents the supported write modes for writing characteristics &
+ /// descriptors to the server.
+ enum class WriteMode : uint8_t {
+ /// Wait for a response from the server before returning but do not verify
+ /// the echo response. Supported for both characteristics and descriptors.
+ kDefault = 0,
+
+ /// Every value blob is verified against an echo response from the server.
+ /// The procedure is aborted if a value blob has not been reliably delivered
+ /// to the peer. Only supported for characteristics.
+ kReliable = 1,
+
+ /// Delivery will not be confirmed before returning. Writing without a
+ /// response is only supported for short characteristics with the
+ /// `WRITE_WITHOUT_RESPONSE` property. The value must fit into a single
+ /// message. It is guaranteed that at least 20 bytes will fit into a single
+ /// message. If the value does not fit, a `kFailure` error will be produced.
+ /// The value will be written at offset 0. Only supported for
+ /// characteristics.
+ kWithoutResponse = 2,
+ };
+
+ /// Represents the supported options to write a characteristic/descriptor
+ /// value to a server.
+ struct WriteOptions {
+ /// The mode of the write operation. For descriptors, only
+ /// `WriteMode::kDefault` is supported
+ WriteMode mode = WriteMode::kDefault;
+
+ /// Request a write starting at the byte indicated.
+ /// Must be 0 if `mode` is `WriteMode.kWithoutResponse`.
+ uint16_t offset = 0;
+ };
+
+ using ReadByTypeCallback = Function<void(Result<Vector<ReadByTypeResult>>)>;
+ using ReadCallback = Function<void(Result<ReadValue>)>;
+ using NotificationCallback = Function<void(ReadValue)>;
+
+ /// Set a callback that will be called when there is an error with this
+ /// RemoteService, after which this RemoteService will be invalid.
+ void SetErrorCallback(Function<void(RemoteServiceError)>&& error_callback);
+
+ /// Calls `characteristic_callback` with the characteristics and descriptors
+ /// in this service.
+ void DiscoverCharacteristics(
+ Function<void(Characteristic)>&& characteristic_callback);
+
+ /// Reads characteristics and descriptors with the specified type. This method
+ /// is useful for reading values before discovery has completed, thereby
+ /// reducing latency.
+ /// @param uuid The UUID of the characteristics/descriptors to read.
+ /// @param result_callback Results are returned via this callback. Results may
+ /// be empty if no matching values are read. If reading a value results in a
+ /// permission error, the handle and error will be included.
+ ///
+ /// This may fail with the following errors:
+ /// - kInvalidParameters: if `uuid` refers to an internally reserved
+ /// descriptor type (e.g. the Client Characteristic Configuration descriptor).
+ /// - kTooManyResults: More results were read than can fit in a Vector.
+ /// Consider reading characteristics/descriptors individually after performing
+ /// discovery.
+ /// - kFailure: The server returned an error not specific to a single result.
+ void ReadByType(Uuid uuid, ReadByTypeCallback&& result_callback);
+
+ /// Reads the value of a characteristic.
+ /// @param handle The handle of the characteristic to be read.
+ /// @param options If null, a short read will be performed, which may be
+ /// truncated to what fits in a single message (at least 22 bytes). If long
+ /// read options are present, performs a long read with the indicated options.
+ /// @param result_callback called with the result of the read and the value of
+ /// the characteristic if successful.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kInvalidParameters `options` is invalid.
+ /// @retval kReadNotPermitted The server rejected the request.
+ /// @retval kInsufficient* The server rejected the request.
+ /// @retval kFailure The server returned an error not covered by the above.
+ void ReadCharacteristic(Handle handle,
+ std::optional<LongReadOptions> options,
+ ReadCallback&& result_callback);
+
+ /// Writes `value` to the characteristic with `handle` using the provided
+ /// `options`. It is not recommended to send additional writes while a write
+ /// is already in progress (the server may receive simultaneous writes in any
+ /// order).
+ ///
+ /// @param handle Handle of the characteristic to be written to
+ /// @param value The value to be written.
+ /// @param options Options that apply to the write.
+ /// @param result_callback Returns a result upon completion of the write.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kInvalidParameters`options is invalid.
+ /// @retval kWriteNotPermitted The server rejected the request.
+ /// @retval kInsufficient* The server rejected the request.
+ /// @retval kFailure The server returned an error not covered by the above
+ /// errors.
+ void WriteCharacteristic(Handle handle,
+ span<const std::byte> value,
+ WriteOptions options,
+ Function<void(Result<Error>)>&& result_callback);
+
+ /// Reads the value of the characteristic descriptor with `handle` and
+ /// returns it in the reply.
+ /// @param handle The descriptor handle to read.
+ /// @param options Options that apply to the read.
+ /// @param result_callback Returns a result containing the value of the
+ /// descriptor on success.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kInvalidParameters`options` is invalid.
+ /// @retval kReadNotPermitted
+ /// @retval kInsufficient* The server rejected the request.
+ /// @retval kFailure The server returned an error not covered by the above
+ /// errors.
+ void ReadDescriptor(Handle handle,
+ std::optional<LongReadOptions> options,
+ ReadCallback&& result_callback);
+
+ /// Writes `value` to the descriptor with `handle` using the provided
+ /// `options`. It is not recommended to send additional writes while a write
+ /// is already in progress (the server may receive simultaneous writes in any
+ /// order).
+ ///
+ /// @param handle Handle of the descriptor to be written to
+ /// @param value The value to be written.
+ /// @param options Options that apply to the write.
+ /// @param result_callback Returns a result upon completion of the write.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kInvalidParameters `options is invalid
+ /// @retval kWriteNotPermitted The server rejected the request.
+ /// @retval kInsufficient* The server rejected the request.
+ /// @retval kFailure The server returned an error not covered by the above
+ /// errors.
+ void WriteDescriptor(Handle handle,
+ span<const std::byte> value,
+ WriteOptions options,
+ Function<void(Result<Error>)>&& result_callback);
+
+ /// Subscribe to notifications & indications from the characteristic with
+ /// the given `handle`.
+ ///
+ /// Either notifications or indications will be enabled depending on
+ /// characteristic properties. Indications will be preferred if they are
+ /// supported. This operation fails if the characteristic does not have the
+ /// "notify" or "indicate" property.
+ ///
+ /// A write request will be issued to configure the characteristic for
+ /// notifications/indications if it contains a Client Characteristic
+ /// Configuration descriptor. This method fails if an error occurs while
+ /// writing to the descriptor.
+ ///
+ /// On success, `notification_callback` will be called when
+ /// the peer sends a notification or indication. Indications are
+ /// automatically confirmed.
+ ///
+ /// Subscriptions can be canceled with `StopNotifications`.
+ ///
+ /// @param handle the handle of the characteristic to subscribe to.
+ /// @param notification_callback will be called with the values of
+ /// notifications/indications when received.
+ /// @param result_callback called with the result of enabling
+ /// notifications/indications.
+ /// @retval kFailure The characteristic does not support notifications or
+ /// indications.
+ /// @retval kInvalidHandle `handle` is invalid.
+ /// @retval kWriteNotPermitted CCC descriptor write error.
+ /// @retval Insufficient* CCC descriptor write error.
+ void RegisterNotificationCallback(
+ Handle handle,
+ NotificationCallback&& notification_callback,
+ Function<void(Result<Error>)>&& result_callback);
+
+ /// Stops notifications for the characteristic with the given `handle`.
+ void StopNotifications(Handle handle);
+
+ private:
+ /// Disconnect from the remote service. This method is called by the
+ /// ~RemoteService::Ptr() when it goes out of scope, the API client should
+ /// never call this method.
+ void Disconnect();
+
+ public:
+ /// Movable RemoteService smart pointer. The remote server will remain
+ /// connected until the returned RemoteService::Ptr is destroyed.
+ using Ptr = internal::RaiiPtr<RemoteService, &RemoteService::Disconnect>;
+};
+
+/// Represents a GATT client that interacts with services on a GATT server.
+class Client {
+ public:
+ /// Represents a remote GATT service.
+ struct RemoteServiceInfo {
+ /// Uniquely identifies this GATT service.
+ Handle handle;
+
+ /// Indicates whether this is a primary or secondary service.
+ bool primary;
+
+ /// The UUID that identifies the type of this service.
+ /// There may be multiple services with the same UUID.
+ Uuid type;
+ };
+
+ virtual ~Client() = default;
+
+ /// Enumerates existing services found on the peer that this Client
+ /// represents, and provides a stream of updates thereafter. Results can be
+ /// filtered by specifying a list of UUIDs in `uuids`. To further interact
+ /// with services, clients must obtain a RemoteService protocol by calling
+ /// ConnectToService(). `uuid_allowlist` - The allowlist of UUIDs to filter
+ /// services with. `updated_callback` - Will be called with services that are
+ /// updated/modified.
+ /// `removed_callback` - Called with the handles of services
+ /// that have been removed. Note that handles may be reused.
+ virtual void WatchServices(
+ Vector<Uuid> uuid_allowlist,
+ Function<void(RemoteServiceInfo)>&& updated_callback,
+ Function<void(Handle)>&& removed_callback) = 0;
+
+ /// Stops service watching if started by `WatchServices`.
+ virtual void StopWatchingServices();
+
+ /// Connects to a RemoteService. Only 1 connection per service is allowed.
+ /// `handle` - the handle of the service to connect to.
+ ///
+ /// This may fail with the following errors:
+ /// kInvalidParameters - `handle` does not correspond to a known service.
+ virtual Result<Error, RemoteService::Ptr> ConnectToService(Handle handle) = 0;
+};
+
+} // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/constants.h b/pw_bluetooth/public/pw_bluetooth/gatt/constants.h
new file mode 100644
index 000000000..7e25501ad
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/constants.h
@@ -0,0 +1,23 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+namespace pw::bluetooth::gatt {
+
+// The maximum length of an attribute value.
+constexpr uint16_t kMaxValueLength = 512;
+
+} // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/error.h b/pw_bluetooth/public/pw_bluetooth/gatt/error.h
new file mode 100644
index 000000000..d5e38655f
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/error.h
@@ -0,0 +1,136 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+namespace pw::bluetooth::gatt {
+
+/// The values correspond with those in Bluetooth 5.2 Vol. 3 Part G Table 3.4,
+/// and Supplement to the Bluetooth Core Specification v9 Part B Table 1.1,
+/// but this is for ease of reference only. Clients should *not* rely on these
+/// values remaining constant. Omitted values from the spec are handled
+/// internally and will not be returned to clients.
+enum class Error : uint16_t {
+ // ATT Errors
+
+ /// The attribute indicated by the handle is invalid. It may have been
+ /// removed.
+ ///
+ /// This may be returned by a LocalService method.
+ kInvalidHandle = 0x1,
+
+ /// This attribute is not readable.
+ kReadNotPermitted = 0x2,
+
+ /// This attribute is not writable.
+ kWriteNotPermitted = 0x3,
+
+ /// Indicates that the response received from the server was invalid.
+ kInvalidPdu = 0x4,
+
+ /// This attribute requires authentication, but the client is not
+ /// authenticated.
+ kInsufficientAuthentication = 0x5,
+
+ /// Indicates that the offset used in a read or write request exceeds the
+ /// bounds of the value.
+ kInvalidOffset = 0x7,
+
+ /// This attribute requires authorization, but the client is not authorized.
+ kInsufficientAuthorization = 0x8,
+
+ /// This attribute requires a connection encrypted by a larger encryption key.
+ kInsufficientEncryptionKeySize = 0xC,
+
+ /// Indicates that the value given in a write request would exceed the maximum
+ /// length allowed for the destination characteristic or descriptor.
+ kInvalidAttributeValueLength = 0xD,
+
+ /// A general error occurred that can not be classified as one of the more
+ /// specific errors.
+ kUnlikelyError = 0xE,
+
+ /// This attribute requires encryption, but the connection is not encrypted.
+ kInsufficientEncryption = 0xF,
+
+ /// The server had insufficient resources to complete the task.
+ kInsufficientResources = 0x11,
+
+ /// The value was not allowed.
+ kValueNotAllowed = 0x13,
+
+ // Application Errors
+
+ /// Application Errors. The uses of these are specified at the application
+ /// level.
+ kApplicationError80 = 0x80,
+ kApplicationError81 = 0x81,
+ kApplicationError82 = 0x82,
+ kApplicationError83 = 0x83,
+ kApplicationError84 = 0x84,
+ kApplicationError85 = 0x85,
+ kApplicationError86 = 0x86,
+ kApplicationError87 = 0x87,
+ kApplicationError88 = 0x88,
+ kApplicationError89 = 0x89,
+ kApplicationError8A = 0x8A,
+ kApplicationError8B = 0x8B,
+ kApplicationError8C = 0x8C,
+ kApplicationError8D = 0x8D,
+ kApplicationError8E = 0x8E,
+ kApplicationError8F = 0x8F,
+ kApplicationError90 = 0x90,
+ kApplicationError91 = 0x91,
+ kApplicationError92 = 0x92,
+ kApplicationError93 = 0x93,
+ kApplicationError94 = 0x94,
+ kApplicationError95 = 0x95,
+ kApplicationError96 = 0x96,
+ kApplicationError97 = 0x97,
+ kApplicationError98 = 0x98,
+ kApplicationError99 = 0x99,
+ kApplicationError9A = 0x9A,
+ kApplicationError9B = 0x9B,
+ kApplicationError9C = 0x9C,
+ kApplicationError9D = 0x9D,
+ kApplicationError9E = 0x9E,
+ kApplicationError9F = 0x9F,
+
+ // Common Profile and Service Error Codes
+
+ /// Write request was rejected at the profile or service level.
+ kWriteRequestRejected = 0xFC,
+
+ /// The Client Characteristic Configuration Descriptor was improperly
+ /// configured.
+ kCccDescriptorImproperlyConfigured = 0xFD,
+
+ /// Profile or service procedure already in progress.
+ kProcedureAlreadyInProgress = 0xFE,
+
+ /// A value was out of range at the profile or service level.
+ kOutOfRange = 0xFF,
+
+ // Errors not specified by Bluetooth.
+
+ // One or more of the call parameters are invalid. See the parameter
+ // documentation.
+ kInvalidParameters = 0x101,
+
+ // Generic failure.
+ kFailure = 0x102,
+};
+
+} // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/server.h b/pw_bluetooth/public/pw_bluetooth/gatt/server.h
new file mode 100644
index 000000000..34e729131
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/server.h
@@ -0,0 +1,267 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+#include <memory>
+
+#include "pw_bluetooth/gatt/error.h"
+#include "pw_bluetooth/gatt/types.h"
+#include "pw_bluetooth/internal/raii_ptr.h"
+#include "pw_bluetooth/result.h"
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace pw::bluetooth::gatt {
+
+/// Parameters for registering a local GATT service.
+struct LocalServiceInfo {
+ /// A unique (within a Server) handle identifying this service.
+ Handle handle;
+
+ /// Indicates whether this is a primary or secondary service.
+ bool primary;
+
+ /// The UUID that identifies the type of this service.
+ /// There may be multiple services with the same UUID.
+ Uuid type;
+
+ /// The characteristics of this service.
+ span<const Characteristic> characteristics;
+
+ /// Handles of other services that are included by this service.
+ span<const Handle> includes;
+};
+
+/// Interface for serving a local GATT service. This is implemented by the API
+/// client.
+class LocalServiceDelegate {
+ public:
+ virtual ~LocalServiceDelegate() = default;
+
+ /// Called when there is a fatal error related to this service that forces the
+ /// service to close. LocalServiceDelegate methods will no longer be called.
+ /// This invalidates the associated LocalService. It is OK to destroy both
+ /// `LocalServiceDelegate` and the associated `LocalService::Ptr` from within
+ /// this method.
+ virtual void OnError(Error error) = 0;
+
+ /// This notifies the current configuration of a particular
+ /// characteristic/descriptor for a particular peer. It will be called when
+ /// the peer GATT client changes the configuration.
+ ///
+ /// The Bluetooth stack maintains the state of each peer's configuration
+ /// across reconnections. As such, this method will be called with both
+ /// `notify` and `indicate` set to false for each characteristic when a peer
+ /// disconnects. Also, when a peer reconnects this method will be called again
+ /// with the initial, persisted state of the newly-connected peer's
+ /// configuration. However, clients should not rely on this state being
+ /// persisted indefinitely by the Bluetooth stack.
+ ///
+ /// @param peer_id The PeerId of the GATT client associated with this
+ /// particular CCC.
+ /// @param handle The handle of the characteristic associated with the
+ /// `notify` and `indicate` parameters.
+ /// @param notify True if the client has enabled notifications, false
+ /// otherwise.
+ /// @param indicate True if the client has enabled indications, false
+ /// otherwise.
+ virtual void CharacteristicConfiguration(PeerId peer_id,
+ Handle handle,
+ bool notify,
+ bool indicate) = 0;
+
+ /// Called when a peer requests to read the value of a characteristic or
+ /// descriptor. It is guaranteed that the peer satisfies the permissions
+ /// associated with this attribute.
+ ///
+ /// @param peer_id The PeerId of the GATT client making the read request.
+ /// @param handle The handle of the requested descriptor/characteristic.
+ /// @param offset The offset at which to start reading the requested value.
+ /// @param result_callback Called with the value of the characteristic on
+ /// success, or an Error on failure. The value will be truncated to fit in the
+ /// MTU if necessary. It is OK to call `result_callback` in `ReadValue`.
+ virtual void ReadValue(PeerId peer_id,
+ Handle handle,
+ uint32_t offset,
+ Function<void(Result<Error, span<const std::byte>>)>&&
+ result_callback) = 0;
+
+ /// Called when a peer issues a request to write the value of a characteristic
+ /// or descriptor. It is guaranteed that the peer satisfies the permissions
+ /// associated with this attribute.
+ ///
+ /// @param peer_id The PeerId of the GATT client making the write request.
+ /// @param handle The handle of the requested descriptor/characteristic.
+ /// @param offset The offset at which to start writing the requested value. If
+ /// the offset is 0, any existing value should be overwritten by the new
+ /// value. Otherwise, the existing value between `offset:(offset +
+ /// len(value))` should be changed to `value`.
+ /// @param value The new value for the descriptor/characteristic.
+ /// @param status_callback Called with the result of the write.
+ virtual void WriteValue(PeerId peer_id,
+ Handle handle,
+ uint32_t offset,
+ span<const std::byte> value,
+ Function<void(Result<Error>)>&& status_callback) = 0;
+
+ /// Called when the MTU of a peer is updated. Also called for peers that are
+ /// already connected when the server is published.
+ ///
+ /// Notifications and indications must fit in a single packet including both
+ /// the 3-byte notification/indication header and the user-provided payload.
+ /// If these are not used, the MTU can be safely ignored as it is intended for
+ /// use cases where the throughput needs to be optimized.
+ virtual void MtuUpdate(PeerId peer_id, uint16_t mtu) = 0;
+};
+
+/// Interface provided by the backend to interact with a published service.
+/// LocalService is valid for the lifetime of a published GATT service. It is
+/// used to control the service and send notifications/indications.
+class LocalService {
+ public:
+ /// The parameters used to signal a characteristic value change from a
+ /// LocalService to a peer.
+ struct ValueChangedParameters {
+ /// The PeerIds of the peers to signal. The LocalService should respect the
+ /// Characteristic Configuration associated with a peer+handle when deciding
+ /// whether to signal it. If empty, all peers are signalled.
+ span<const PeerId> peer_ids;
+ /// The handle of the characteristic value being signaled.
+ Handle handle;
+ /// The new value for the descriptor/characteristic.
+ span<const std::byte> value;
+ };
+
+ /// The Result type for a ValueChanged indication or notification message. The
+ /// error can be locally generated for notifications and either locally or
+ /// remotely generated for indications.
+ using ValueChangedResult = Result<Error>;
+
+ /// The callback type for a ValueChanged indication or notification
+ /// completion.
+ using ValueChangedCallback = Function<void(ValueChangedResult)>;
+
+ virtual ~LocalService() = default;
+
+ /// Sends a notification to peers. Notifications should be used instead of
+ /// indications when the service does *not* require peer confirmation of the
+ /// update.
+ ///
+ /// Notifications should not be sent to peers which have not enabled
+ /// notifications on a particular characteristic or that have disconnected
+ /// since - if they are sent, they will not be propagated and the
+ /// `completion_callback` will be called with an error condition. The
+ /// Bluetooth stack will track this configuration for the lifetime of the
+ /// service.
+ ///
+ /// The maximum size of the `parameters.value` field depends on the MTU
+ /// negotiated with the peer. A 3-byte header plus the value contents must fit
+ /// in a packet of MTU bytes.
+ ///
+ /// @param parameters The parameters associated with the changed
+ /// characteristic.
+ /// @param completion_callback Called when the notification has been sent to
+ /// all peers or an error is produced when trying to send the notification to
+ /// any of the peers. This function is called only once when all associated
+ /// work is done, if the implementation wishes to receive a call on a
+ /// per-peer basis, they should send this event with a single PeerId in
+ /// `parameters.peer_ids`. Additional values should not be notified until
+ /// this callback is called.
+ virtual void NotifyValue(const ValueChangedParameters& parameters,
+ ValueChangedCallback&& completion_callback) = 0;
+
+ /// Sends an indication to peers. Indications should be used instead of
+ /// notifications when the service *does* require peer confirmation of the
+ /// update.
+ ///
+ /// Indications should not be sent to peers which have not enabled indications
+ /// on a particular characteristic - if they are sent, they will not be
+ /// propagated. The Bluetooth stack will track this configuration for the
+ /// lifetime of the service.
+ ///
+ /// If any of the peers in `parameters.peer_ids` fails to confirm the
+ /// indication within the ATT transaction timeout (30 seconds per
+ /// Bluetooth 5.2 Vol. 4 Part G 3.3.3), the link between the peer and the
+ /// local adapter will be closed.
+ ///
+ /// The maximum size of the `parameters.value` field depends on the MTU
+ /// negotiated with the peer. A 3-byte header plus the value contents must fit
+ /// in a packet of MTU bytes.
+ ///
+ /// @param parameters The parameters associated with the changed
+ /// characteristic.
+ /// @param confirmation When all the peers listed in `parameters.peer_ids`
+ /// have confirmed the indication, `confirmation` is called. If the
+ /// implementation wishes to receive indication confirmations on a per-peer
+ /// basis, they should send this event with a single PeerId in
+ /// `parameters.peer_ids`. Additional values should not be indicated until
+ /// this callback is called.
+ virtual void IndicateValue(const ValueChangedParameters& parameters,
+ ValueChangedCallback&& confirmation) = 0;
+
+ private:
+ /// Unpublish the local service. This method is called by the
+ /// ~LocalService::Ptr() when it goes out of scope, the API client should
+ /// never call this method.
+ virtual void UnpublishService() = 0;
+
+ public:
+ /// Movable LocalService smart pointer. When the LocalService::Ptr object is
+ /// destroyed the service will be unpublished.
+ using Ptr = internal::RaiiPtr<LocalService, &LocalService::UnpublishService>;
+};
+
+/// Interface for a GATT server that serves many GATT services.
+class Server {
+ public:
+ enum class PublishServiceError {
+ kInternalError = 0,
+
+ /// The service handle provided was not unique.
+ kInvalidHandle = 1,
+
+ /// Invalid service UUID provided.
+ kInvalidUuid = 2,
+
+ /// Invalid service characteristics provided.
+ kInvalidCharacteristics = 3,
+
+ /// Invalid service includes provided.
+ kInvalidIncludes = 4,
+ };
+
+ /// The Result passed by PublishService.
+ using PublishServiceResult = Result<PublishServiceError, LocalService::Ptr>;
+
+ virtual ~Server() = default;
+
+ /// Publishes the service defined by `info` and implemented by `delegate` so
+ /// that it is available to all remote peers.
+ ///
+ /// The caller must assign distinct handles to the characteristics and
+ /// descriptors listed in `info`. These identifiers will be used in requests
+ /// sent to `delegate`. On success, a `LocalService::Ptr` is returned. When
+ /// the `LocalService::Ptr` is destroyed or an error occurs
+ /// (LocalServiceDelegate.OnError), the service will be unpublished.
+ virtual void PublishService(
+ const LocalServiceInfo& info,
+ LocalServiceDelegate* delegate,
+ Function<void(PublishServiceResult)>&& result_callback) = 0;
+};
+
+} // namespace pw::bluetooth::gatt
diff --git a/pw_bluetooth/public/pw_bluetooth/gatt/types.h b/pw_bluetooth/public/pw_bluetooth/gatt/types.h
new file mode 100644
index 000000000..1bbe3c6d9
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/gatt/types.h
@@ -0,0 +1,140 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+#include <optional>
+
+#include "pw_bluetooth/types.h"
+#include "pw_span/span.h"
+
+namespace pw::bluetooth::gatt {
+
+// A Handle uniquely identifies a service, characteristic, or descriptor.
+enum class Handle : uint64_t {};
+
+// Possible values for the characteristic properties bitfield. These specify
+// the GATT procedures that are allowed for a particular characteristic.
+enum class CharacteristicPropertyBits : uint16_t {
+ kBroadcast = 0x1,
+ kRead = 0x2,
+ kWriteWithoutResponse = 0x4,
+ kWrite = 0x8,
+ kNotify = 0x10,
+ kIndicate = 0x20,
+ kAuthenticatedSignedWrites = 0x40,
+ kReliableWrite = 0x100,
+ kWritableAuxiliaries = 0x200
+};
+
+// Helper operators to allow combining and comparing CharacteristicPropertyBits
+// values.
+inline constexpr bool operator&(CharacteristicPropertyBits left,
+ CharacteristicPropertyBits right) {
+ return static_cast<bool>(
+ static_cast<std::underlying_type_t<CharacteristicPropertyBits>>(left) &
+ static_cast<std::underlying_type_t<CharacteristicPropertyBits>>(right));
+}
+
+inline constexpr CharacteristicPropertyBits operator|(
+ CharacteristicPropertyBits left, CharacteristicPropertyBits right) {
+ return static_cast<CharacteristicPropertyBits>(
+ static_cast<std::underlying_type_t<CharacteristicPropertyBits>>(left) |
+ static_cast<std::underlying_type_t<CharacteristicPropertyBits>>(right));
+}
+
+inline constexpr CharacteristicPropertyBits& operator|=(
+ CharacteristicPropertyBits& left, CharacteristicPropertyBits right) {
+ return left = left | right;
+}
+
+// Represents encryption, authentication, and authorization permissions that can
+// be assigned to a specific access permission.
+struct SecurityRequirements {
+ // If true, the physical link must be encrypted to access this attribute.
+ bool encryption_required;
+
+ // If true, the physical link must be authenticated to access this
+ // attribute.
+ bool authentication_required;
+
+ // If true, the client needs to be authorized before accessing this
+ // attribute.
+ bool authorization_required;
+};
+
+/// Specifies the access permissions for a specific attribute value.
+struct AttributePermissions {
+ // Specifies whether or not an attribute has the read permission. If null,
+ // then the attribute value cannot be read. Otherwise, it can be read only if
+ // the permissions specified in the SecurityRequirements table are satisfied.
+ std::optional<SecurityRequirements> read;
+
+ // Specifies whether or not an attribute has the write permission. If null,
+ // then the attribute value cannot be written. Otherwise, it can be written
+ // only if the permissions specified in the SecurityRequirements table are
+ // satisfied.
+ std::optional<SecurityRequirements> write;
+
+ // Specifies the security requirements for a client to subscribe to
+ // notifications or indications on a characteristic. A characteristic's
+ // support for notifications or indications is specified using the NOTIFY and
+ // INDICATE characteristic properties. If a local characteristic has one of
+ // these properties then this field can not be null. Otherwise, this field
+ // must be left as null.
+ //
+ // This field is ignored for Descriptors.
+ std::optional<SecurityRequirements> update;
+};
+
+// Represents a local or remote GATT characteristic descriptor.
+struct Descriptor {
+ // Uniquely identifies this descriptor within a service.
+ // For local descriptors, the specified handle must be unique
+ // across all characteristic and descriptor handles in this service.
+ Handle handle;
+
+ // The UUID that identifies the type of this descriptor.
+ Uuid type;
+
+ // The attribute permissions of this descriptor. For remote descriptors, this
+ // value will be null until the permissions are discovered via read and write
+ // requests.
+ std::optional<AttributePermissions> permissions;
+};
+
+// Represents a local or remote GATT characteristic.
+struct Characteristic {
+ // Uniquely identifies this characteristic within a service.
+ // For local characteristics, the specified handle must be unique across
+ // all characteristic and descriptor handles in this service.
+ Handle handle;
+
+ // The UUID that identifies the type of this characteristic.
+ Uuid type;
+
+ // The characteristic properties bitfield. This is a logic or of any number of
+ // values from `CharacteristicPropertyBits` above.
+ CharacteristicPropertyBits properties;
+
+ // The attribute permissions of this characteristic. For remote
+ // characteristics, this value will be null until the permissions are
+ // discovered via read and write requests.
+ std::optional<AttributePermissions> permissions;
+
+ // The descriptors of this characteristic.
+ span<const Descriptor> descriptors;
+};
+
+} // namespace pw::bluetooth::gatt \ No newline at end of file
diff --git a/pw_bluetooth/public/pw_bluetooth/hci.emb b/pw_bluetooth/public/pw_bluetooth/hci.emb
new file mode 100644
index 000000000..d781b3f18
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/hci.emb
@@ -0,0 +1,2570 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# This file contains Emboss definitions for Host Controller Interface packets
+# and types found in the Bluetooth Core Specification. The Emboss compiler is
+# used to generate a C++ header from this file.
+
+[$default byte_order: "LittleEndian"]
+[(cpp) namespace: "pw::bluetooth::emboss"]
+# =========================== Constants =================================
+
+
+enum CodingFormat:
+ -- Coding formats from assigned numbers.
+ -- (https://www.bluetooth.com/specifications/assigned-numbers/host-controller-interface)
+ [maximum_bits: 8]
+ U_LAW = 0x00
+ A_LAW = 0x01
+ CVSD = 0x02
+ TRANSPARENT = 0x03
+ LINEAR_PCM = 0x04
+ MSBC = 0x05
+ LC3 = 0x06
+ G729A = 0x07
+ VENDOR_SPECIFIC = 0xFF
+
+
+enum GenericEnableParam:
+ -- Binary values that can be generically passed to HCI commands that expect a
+ -- 1-octet boolean "enable"/"disable" parameter.
+ [maximum_bits: 8]
+ DISABLE = 0x00
+ ENABLE = 0x01
+
+
+enum InquiryAccessCode:
+ -- General- and Device-specific Inquiry Access Codes (DIACs) for use in Inquiry
+ -- command LAP fields.
+ -- (https://www.bluetooth.com/specifications/assigned-numbers/baseband)
+ [maximum_bits: 24]
+ GIAC = 0x9E8B33
+ -- General Inquiry Access Code
+
+ LIAC = 0x9E8B00
+ -- Limited Dedicated Inquiry Access Code
+
+
+enum PcmDataFormat:
+ -- PCM data formats from assigned numbers.
+ -- (https://www.bluetooth.com/specifications/assigned-numbers/host-controller-interface)
+ [maximum_bits: 8]
+ NOT_APPLICABLE = 0x00
+ ONES_COMPLEMENT = 0x01
+ TWOS_COMPLEMENT = 0x02
+ SIGN_MAGNITUDE = 0x03
+ UNSIGNED = 0x04
+
+
+enum ScoDataPath:
+ [maximum_bits: 8]
+ HCI = 0x00
+ AUDIO_TEST_MODE = 0xFF
+ -- 0x01 - 0xFE specify the logical channel number (vendor specific)
+
+
+enum ConnectionRole:
+ [maximum_bits: 8]
+ CENTRAL = 0x00
+ PERIPHERAL = 0x01
+
+
+enum PageTimeout:
+ [maximum_bits: 16]
+ MIN = 0x0001
+ MAX = 0xFFFF
+ DEFAULT = 0x2000
+
+
+enum ScanInterval:
+ -- The minimum and maximum range values for Page and Inquiry Scan Interval (in time slices)
+ -- Page Scan Interval: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.19)
+ -- Inquiry Scan Interval: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.21)
+ [maximum_bits: 16]
+ MIN = 0x0012
+ MAX = 0x1000
+
+
+enum ScanWindow:
+ -- The minimum and maximum range valeus for Page and Inquiry Scan Window (in time slices)
+ -- Page Scan Window: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.19)
+ -- Inquiry Scan Window: (see Core Spec v5.0, Vol 2, Part E, Section 7.3.21)
+ [maximum_bits: 16]
+ MIN = 0x0011
+ MAX = 0x1000
+
+
+enum StatusCode:
+ -- HCI Error Codes. Refer to Core Spec v5.0, Vol 2, Part D for definitions and
+ -- descriptions. All enum values are in increasing numerical order, however the
+ -- values are listed below for clarity.
+ [maximum_bits: 8]
+ SUCCESS = 0x00
+ UNKNOWN_COMMAND = 0x01
+ UNKNOWN_CONNECTION_ID = 0x02
+ HARDWARE_FAILURE = 0x03
+ PAGE_TIMEOUT = 0x04
+ AUTHENTICATION_FAILURE = 0x05
+ PIN_OR_KEY_MISSING = 0x06
+ MEMORY_CAPACITY_EXCEEDED = 0x07
+ CONNECTION_TIMEOUT = 0x08
+ CONNECTION_LIMIT_EXCEEDED = 0x09
+ SYNCHRONOUS_CONNECTION_LIMIT_EXCEEDED = 0x0A
+ CONNECTION_ALREADY_EXISTS = 0x0B
+ COMMAND_DISALLOWED = 0x0C
+ CONNECTION_REJECTED_LIMITED_RESOURCES = 0x0D
+ CONNECTION_REJECTED_SECURITY = 0x0E
+ CONNECTION_REJECTED_BAD_BD_ADDR = 0x0F
+ CONNECTION_ACCEPT_TIMEOUT_EXCEEDED = 0x10
+ UNSUPPORTED_FEATURE_OR_PARAMETER = 0x11
+ INVALID_HCI_COMMAND_PARAMETERS = 0x12
+ REMOTE_USER_TERMINATED_CONNECTION = 0x13
+ REMOTE_DEVICE_TERMINATED_CONNECTION_LOW_RESOURCES = 0x14
+ REMOTE_DEVICE_TERMINATED_CONNECTION_POWER_OFF = 0x15
+ CONNECTION_TERMINATED_BY_LOCAL_HOST = 0x16
+ REPEATED_ATTEMPTS = 0x17
+ PAIRING_NOT_ALLOWED = 0x18
+ UNKNOWN_LMP_PDU = 0x19
+ UNSUPPORTED_REMOTE_FEATURE = 0x1A
+ SCO_OFFSET_REJECTED = 0x1B
+ SCO_INTERVAL_REJECTED = 0x1C
+ SCO_AIRMODE_REJECTED = 0x1D
+ INVALID_LMP_OR_LL_PARAMETERS = 0x1E
+ UNSPECIFIED_ERROR = 0x1F
+ UNSUPPORTED_LMP_OR_LL_PARAMETER_VALUE = 0x20
+ ROLE_CHANGE_NOT_ALLOWED = 0x21
+ LMP_OR_LL_RESPONSE_TIMEOUT = 0x22
+ LMP_ERROR_TRANSACTION_COLLISION = 0x23
+ LMP_PDU_NOT_ALLOWED = 0x24
+ ENCRYPTION_MODE_NOT_ACCEPTABLE = 0x25
+ LINK_KEY_CANNOT_BE_CHANGED = 0x26
+ REQUESTED_QOS_NOT_SUPPORTED = 0x27
+ INSTANT_PASSED = 0x28
+ PAIRING_WITH_UNIT_KEY_NOT_SUPPORTED = 0x29
+ DIFFERENT_TRANSACTION_COLLISION = 0x2A
+ RESERVED_0 = 0x2B
+ QOS_UNACCEPTABLE_PARAMETER = 0x2C
+ QOS_REJECTED = 0x2D
+ CHANNEL_CLASSIFICATION_NOT_SUPPORTED = 0x2E
+ INSUFFICIENT_SECURITY = 0x2F
+ PARAMETER_OUT_OF_MANDATORY_RANGE = 0x30
+ RESERVED_1 = 0x31
+ ROLE_SWITCH_PENDING = 0x32
+ RESERVED_2 = 0x33
+ RESERVED_SLOT_VIOLATION = 0x34
+ ROLE_SWITCH_FAILED = 0x35
+ EXTENDED_INQUIRY_RESPONSE_TOO_LARGE = 0x36
+ SECURE_SIMPLE_PAIRING_NOT_SUPPORTED_BY_HOST = 0x37
+ HOST_BUSY_PAIRING = 0x38
+ CONNECTION_REJECTED_NO_SUITABLE_CHANNEL_FOUND = 0x39
+ CONTROLLER_BUSY = 0x3A
+ UNACCEPTABLE_CONNECTION_PARAMETERS = 0x3B
+ DIRECTED_ADVERTISING_TIMEOUT = 0x3C
+ CONNECTION_TERMINATED_MIC_FAILURE = 0x3D
+ CONNECTION_FAILED_TO_BE_ESTABLISHED = 0x3E
+ MAC_CONNECTION_FAILED = 0x3F
+ COARSE_CLOCK_ADJUSTMENT_REJECTED = 0x40
+ # 5.0
+ TYPE_0_SUBMAP_NOT_DEFINED = 0x41
+ UNKNOWN_ADVERTISING_IDENTIFIER = 0x42
+ LIMIT_REACHED = 0x43
+ OPERATION_CANCELLED_BY_HOST = 0x44
+
+
+bits ScoPacketType:
+ -- Bitmask of SCO packet types.
+ # SCO packet types
+ 0 [+1] Flag hv1
+ $next [+1] Flag hv2
+ $next [+1] Flag hv3
+ # eSCO packet types
+ $next [+1] Flag ev3
+ $next [+1] Flag ev4
+ $next [+1] Flag ev5
+ $next [+1] Flag not_2_ev3
+ $next [+1] Flag not_3_ev3
+ $next [+1] Flag not_2_ev5
+ $next [+1] Flag not_3_ev5
+ $next [+6] UInt padding
+
+
+bits PacketType:
+ -- Bitmask values for supported Packet Types
+ -- Used for HCI_Create_Connection and HCI_Change_Connection_Packet_Type
+ -- All other bits reserved for future use.
+ 1 [+1] Flag disable_2_dh1
+ 2 [+1] Flag disable_3_dh1
+ 3 [+1] Flag enable_dm1 # Note: always on in >= v1.2
+ 4 [+1] Flag enable_dh1
+ 8 [+1] Flag disable_2_dh3
+ 9 [+1] Flag disable_3_dh3
+ 10 [+1] Flag enable_dm3
+ 11 [+1] Flag enable_dh3
+ 12 [+1] Flag disable_2_dh5
+ 13 [+1] Flag disable_3_dh5
+ 14 [+1] Flag enable_dm5
+ 15 [+1] Flag enable_dh5
+
+
+enum PageScanRepetitionMode:
+ -- The page scan repetition mode, representing a maximum time between Page Scans.
+ -- (See Core Spec v5.0, Volume 2, Part B, Section 8.3.1)
+ [maximum_bits: 8]
+ R0_ = 0x00 # Continuous Scan
+ R1_ = 0x01 # <= 1.28s
+ R2_ = 0x02 # <= 2.56s
+
+
+bits ClockOffset:
+ -- Clock Offset. The lower 15 bits are set to the clock offset as retrieved
+ -- by an Inquiry. The highest bit is set to 1 if the rest of this parameter
+ -- is valid.
+ 15 [+1] Flag valid
+ if valid:
+ 0 [+15] UInt clock_offset
+
+
+struct BdAddr:
+ -- Bluetooth Device Address
+ 0 [+6] UInt bd_addr
+
+
+enum IoCapability:
+ -- All other values reserved for future use.
+ [maximum_bits: 8]
+ DISPLAY_ONLY = 0x00
+ DISPLAY_YES_NO = 0x01
+ KEYBOARD_ONLY = 0x02
+ NO_INPUT_NO_OUTPUT = 0x03
+
+
+enum OobDataPresent:
+ -- Whether there is out-of-band data present, and what type.
+ -- All other values reserved for future use.
+ [maximum_bits: 8]
+ NOT_PRESENT = 0x00
+ P192_ = 0x01
+ P256_ = 0x02
+ P192_AND_P256 = 0x03
+
+# inclusive-language: disable
+
+
+enum AuthenticationRequirements:
+ -- All options without MITM do not require MITM protection, and a numeric
+ -- comparison with automatic accept is allowed.
+ -- All options with MITM do require MITM protection, and IO capabilities should
+ -- be used to determine the authentication procedure.
+ [maximum_bits: 8]
+ NO_BONDING = 0x00
+ MITM_NO_BONDING = 0x01
+ DEDICATED_BONDING = 0x02
+ MITM_DEDICATED_BONDING = 0x03
+ GENERAL_BONDING = 0x04
+ MITM_GENERAL_BONDING = 0x05
+
+# inclusive-language: enable
+
+
+bits ScanEnableBits:
+ -- Bitmask Values for the Scan_Enable parameter in a
+ -- HCI_(Read,Write)_Scan_Enable command.
+ 0 [+1] Flag inquiry
+ -- Inquiry scan enabled
+
+ $next [+1] Flag page
+ -- Page scan enabled
+
+ $next [+6] UInt padding
+
+
+enum InquiryScanType:
+ [maximum_bits: 8]
+ STANDARD = 0x00
+ -- Standard scan (Default) (Mandatory)
+
+ INTERLACED = 0x01
+
+
+struct LocalName:
+ 0 [+248] UInt:8[248] local_name
+
+
+struct ExtendedInquiryResponse:
+ 0 [+240] UInt:8[240] extended_inquiry_response
+
+
+enum LEExtendedDuplicateFilteringOption:
+ -- Possible values that can be used for the |filter_duplicates| parameter in a
+ -- HCI_LE_Set_Extended_Scan_Enable command.
+ [maximum_bits: 8]
+ DISABLED = 0x00
+ ENABLED = 0x01
+ ENABLED_RESET_FOR_EACH_SCAN_PERIOD = 0x02
+ -- Duplicate advertisements in a single scan period should not be sent to the
+ -- Host in advertising report events; this setting shall only be used if the
+ -- Period parameter is non-zero.
+
+
+enum MajorDeviceClass:
+ [maximum_bits: 5]
+ MISCELLANEOUS = 0x00
+ COMPUTER = 0x01
+ PHONE = 0x02
+ LAN = 0x03
+ AUDIO_VIDEO = 0x04
+ PERIPHERAL = 0x05
+ IMAGING = 0x06
+ WEARABLE = 0x07
+ TOY = 0x08
+ HEALTH = 0x09
+ UNCATEGORIZED = 0x1F
+
+
+bits MajorServiceClasses:
+ 0 [+1] Flag limited_discoverable_mode
+ $next [+1] Flag le_audio
+ $next [+1] Flag reserved
+ $next [+1] Flag positioning
+ $next [+1] Flag networking
+ $next [+1] Flag rendering
+ $next [+1] Flag capturing
+ $next [+1] Flag object_transfer
+ $next [+1] Flag audio
+ $next [+1] Flag telephony
+ $next [+1] Flag information
+
+
+enum ComputerMinorDeviceClass:
+ [maximum_bits: 6]
+ UNCATEGORIZED = 0x00
+ DESKTOP_WORKSTATION = 0x01
+ SERVER_CLASS = 0x02
+ LAPTOP = 0x03
+ HANDHELD_PC = 0x04
+ PALM_SIZE_PC = 0x05
+ WEARABLE = 0x06
+ TABLET = 0x07
+
+
+enum PhoneMinorDeviceClass:
+ [maximum_bits: 6]
+ UNCATEGORIZED = 0x00
+ CELLULAR = 0x01
+ CORDLESS = 0x02
+ SMARTPHONE = 0x03
+ WIRED_MODEM_OR_VOID_GATEWAY = 0x04
+ COMMON_ISDN_ACCESS = 0x05
+
+
+enum LANMinorDeviceClass:
+ [maximum_bits: 6]
+ FULLY_AVAILABLE = 0x00
+ UTILIZED_1_TO_17 = 0x08
+ UTILIZED_17_TO_33 = 0x10
+ UTILIZED_33_TO_50 = 0x18
+ UTILIZED_50_TO_67 = 0x20
+ UTILIZED_67_TO_83 = 0x28
+ UTILIZED_83_TO_99 = 0x30
+ NO_SERVICE_AVAILABLE = 0x38
+
+
+enum AudioVideoMinorDeviceClass:
+ [maximum_bits: 6]
+ UNCATEGORIZED = 0x00
+ WEARABLE_HEADSET_DEVICE = 0x01
+ HANDS_FREE_DEVICE = 0x02
+ RESERVED_0 = 0x03
+ MICROPHONE = 0x04
+ LOUDSPEAKER = 0x05
+ HEADPHONES = 0x06
+ PORTABLE_AUDIO = 0x07
+ CAR_AUDIO = 0x08
+ SET_TOP_BOX = 0x09
+ HIFI_AUDIO_DEVICE = 0x0A
+ VCR = 0x0B
+ VIDEO_CAMERA = 0x0C
+ CAMCORDER = 0x0D
+ VIDEO_MONITOR = 0x0E
+ VIDEO_DISPLAY_AND_LOUDSPEAKER = 0x0F
+ VIDEO_CONFERENCING = 0x10
+ RESERVED_1 = 0x11
+ GAMING_TOY = 0x12
+
+
+enum PeripheralMinorDeviceClass0:
+ [maximum_bits: 4]
+ UNCATEGORIZED = 0x00
+ JOYSTICK = 0x01
+ GAMEPAD = 0x02
+ REMOTE_CONTROL = 0x03
+ SENSING_DEVICE = 0x04
+ DIGITIZER_TABLET = 0x05
+ CARD_READER = 0x06
+ DIGITAL_PEN = 0x07
+ HANDHELD_SCANNER = 0x08
+ HANDHELD_GESTURAL_INPUT_DEVICE = 0x09
+
+
+enum PeripheralMinorDeviceClass1:
+ [maximum_bits: 2]
+ UNCATEGORIZED = 0x00
+ KEYBOARD = 0x01
+ POINTING_DEVICE = 0x02
+ COMBO_KEYBOARD_POINTING_DEVICE = 0x03
+
+
+bits PeripheralMinorDeviceClass:
+ 0 [+4] PeripheralMinorDeviceClass0 device_class_0
+ $next [+2] PeripheralMinorDeviceClass1 device_class_1
+
+
+enum ImagingMinorDeviceClass:
+ [maximum_bits: 2]
+ UNCATEGORIZED = 0x00
+
+
+bits ImagingMinorDeviceClassBits:
+ 0 [+2] ImagingMinorDeviceClass device_class
+ $next [+1] Flag display
+ $next [+1] Flag camera
+ $next [+1] Flag scanner
+ $next [+1] Flag printer
+
+
+enum WearableMinorDeviceClass:
+ [maximum_bits: 6]
+ WRISTWATCH = 0x01
+ PAGER = 0x02
+ JACKET = 0x03
+ HELMET = 0x04
+ GLASSES = 0x05
+
+
+enum ToyMinorDeviceClass:
+ [maximum_bits: 6]
+ ROBOT = 0x01
+ VEHICLE = 0x02
+ DOLL = 0x03
+ CONTROLLER = 0x04
+ GAME = 0x05
+
+
+enum HealthMinorDeviceClass:
+ [maximum_bits: 6]
+ UNDEFINED = 0x00
+ BLOOD_PRESSURE_MONITOR = 0x01
+ THERMOMETER = 0x02
+ WEIGHING_SCALE = 0x03
+ GLUCOSE_METER = 0x04
+ PULSE_OXIMETER = 0x05
+ HEART_PULSE_RATE_MONITOR = 0x06
+ HEALTH_DATA_DISPLAY = 0x07
+ STEP_COUNTER = 0x08
+ BODY_COMPOSITION_ANALYZER = 0x09
+ PEAK_FLOW_MONITOR = 0x0A
+ MEDICATION_MONITOR = 0x0B
+ KNEE_PROSTHESIS = 0x0C
+ ANKLE_PROSTHESIS = 0x0D
+ GENERIC_HEALTH_MANAGER = 0x0E
+ PERSONAL_MOBILITY_DEVICE = 0x0F
+
+
+bits ClassOfDevice:
+ -- Defined in Assigned Numbers for the Baseband
+ -- https://www.bluetooth.com/specifications/assigned-numbers/baseband
+ 0 [+2] UInt zero
+ [requires: this == 0]
+
+ if major_device_class == MajorDeviceClass.COMPUTER:
+ 2 [+6] ComputerMinorDeviceClass computer_minor_device_class
+
+ if major_device_class == MajorDeviceClass.PHONE:
+ 2 [+6] PhoneMinorDeviceClass phone_minor_device_class
+
+ if major_device_class == MajorDeviceClass.LAN:
+ 2 [+6] LANMinorDeviceClass lan_minor_device_class
+
+ if major_device_class == MajorDeviceClass.AUDIO_VIDEO:
+ 2 [+6] AudioVideoMinorDeviceClass audio_video_minor_device_class
+
+ if major_device_class == MajorDeviceClass.PERIPHERAL:
+ 2 [+6] PeripheralMinorDeviceClass peripheral_minor_device_class
+
+ if major_device_class == MajorDeviceClass.IMAGING:
+ 2 [+6] ImagingMinorDeviceClassBits imaging_minor_device_class
+
+ if major_device_class == MajorDeviceClass.WEARABLE:
+ 2 [+6] WearableMinorDeviceClass wearable_minor_device_class
+
+ if major_device_class == MajorDeviceClass.TOY:
+ 2 [+6] ToyMinorDeviceClass toy_minor_device_class
+
+ if major_device_class == MajorDeviceClass.HEALTH:
+ 2 [+6] HealthMinorDeviceClass health_minor_device_class
+
+ 8 [+5] MajorDeviceClass major_device_class
+ $next [+11] MajorServiceClasses major_service_classes
+
+
+enum LEPeriodicAdvertisingCreateSyncUseParams:
+ [maximum_bits: 1]
+
+ USE_PARAMS = 0x00
+ -- Use the Advertising_SID, Advertiser_Address_Type, and Adertiser_Address parameters to
+ -- determine which advertiser to listen to.
+
+ USE_PERIODIC_ADVERTISER_LIST = 0x01
+ -- Use the Periodic Advertiser List to determine which advertiser to listen to.
+
+
+bits LEPeriodicAdvertisingCreateSyncOptions:
+ -- First parameter to the LE Periodic Advertising Create Sync command
+
+ 0 [+1] LEPeriodicAdvertisingCreateSyncUseParams advertiser_source
+
+ $next [+1] Flag enable_reporting
+ -- 0: Reporting initially enabled
+ -- 1: Reporting initially disabled
+
+ $next [+1] Flag enable_duplicate_filtering
+ -- 0: Duplicate filtering initially disabled
+ -- 1: Duplicate filtering initially enabled
+
+ $next [+5] UInt padding
+ -- Reserved for future use
+
+
+enum LEPeriodicAdvertisingAddressType:
+ -- Possible values that can be specified for the |advertiser_address_type| in an LE Periodic
+ -- Advertising Create Sync command.
+ [maximum_bits: 8]
+ PUBLIC = 0x00
+ -- Public Device Address or Public Identity Address
+
+ RANDOM = 0x01
+ -- Random Device Address or Random (static) Identity Address
+
+
+bits LEPeriodicAdvertisingSyncCTEType:
+ -- Bit definitions for a |sync_cte_type| field in an LE Periodic Advertising Create Sync command
+
+ 0 [+1] Flag dont_sync_aoa
+ -- Do not sync to packets with an AoA Constant Tone Extension
+
+ $next [+1] Flag dont_sync_aod_1us
+ -- Do not sync to packets with an AoD Constant Tone Extension with 1 microsecond slots
+
+ $next [+1] Flag dont_sync_aod_2us
+ -- Do not sync to packets with an AoD Constant Tone Extension with 2 microsecond slots
+
+ $next [+1] Flag dont_sync_type_3
+ -- Do not sync to packets with a typoe 3 Constant Tone Extension (currently reserved for future
+ -- use)
+
+ $next [+1] Flag dont_sync_without_cte
+ -- Do not sync to packets without a Constant Tone Extension
+
+ $next [+3] UInt padding
+ -- Reserved for future use
+
+
+enum LEAddressType:
+ -- Possible values that can be reported for the |address_type| parameter in a LE
+ -- Advertising Report event.
+
+ [maximum_bits: 8]
+
+ PUBLIC = 0x00
+ -- Public device address (default)
+
+ RANDOM = 0x01
+ -- Random device address
+
+ PUBLIC_IDENTITY = 0x02
+ -- Public Identity Address (Corresponds to Resolved Private Address)
+
+ RANDOM_IDENTITY = 0x03
+ -- Random (static) Identity Address (Corresponds to Resolved Private Address)
+
+ RANDOM_UNRESOLVED = 0xFE
+ -- This is a special value used in LE Extended Advertising Report events to
+ -- indicate a random address that the controller was unable to resolve.
+
+ ANONYMOUS = 0xFF
+ -- This is a special value that is only used in LE Directed Advertising Report
+ -- events.
+ -- Meaning: No address provided (anonymous advertisement)
+
+
+enum LEOwnAddressType:
+ -- Possible values that can be used for the |own_address_type| parameter in various HCI commands
+
+ [maximum_bits: 8]
+
+ PUBLIC = 0x00
+ -- Public Device Address
+
+ RANDOM = 0x01
+ -- Random Device Address
+
+ PRIVATE_DEFAULT_TO_PUBLIC = 0x02
+ -- Controller generates the Resolvable Private Address based on the local IRK from the resolving
+ -- list. If the resolving list contains no matching entry, then use the public address.
+
+ PRIVATE_DEFAULT_TO_RANDOM = 0x03
+ -- Controller generates the Resolvable Private Address based on the local IRK from the resolving
+ -- list. If the resolving list contains no matching entry, then use the random address from
+ -- LE_Set_Random_Address.
+
+
+enum LEPeerAddressType:
+ -- Possible values that can be used for the address_type parameters in various
+ -- HCI commands
+ [maximum_bits: 8]
+ PUBLIC = 0x00
+ RANDOM = 0x01
+ ANONYMOUS = 0xFF
+
+
+enum LEScanType:
+ -- Possible values that can be used for the |scan_type| parameter in various LE HCI commands.
+ [maximum_bits: 8]
+ PASSIVE = 0x00
+ -- Passive Scanning. No scanning PDUs shall be sent (default)
+
+ ACTIVE = 0x01
+ -- Active scanning. Scanning PDUs may be sent.
+
+
+enum LEScanFilterPolicy:
+ -- Possible values that can be used for the |filter_policy| parameter in various LE HCI commands
+ [maximum_bits: 8]
+ BASIC_UNFILTERED = 0x00
+ BASIC_FILTERED = 0x01
+ EXTENDED_UNFILTERED = 0x02
+ EXTENDED_FILTERED = 0x03
+
+
+bits LEPHYBits:
+ 0 [+1] Flag le_1m
+ -- Scan advertisements on the LE 1M PHY
+
+ $next [+1] Flag padding1
+ -- Reserved for future use
+
+ $next [+1] Flag le_coded
+ -- Scan advertisements on the LE Coded PHY
+
+ $next [+5] UInt padding2
+ -- Reserved for future use
+
+
+enum LEPrivacyMode:
+ -- Possible values for the |privacy_mode| parameter in an LE Set Privacy Mode
+ -- command
+ [maximum_bits: 8]
+ NETWORK = 0x00
+ -- Use Network Privacy Mode for this peer device (default).
+
+ DEVICE = 0x01
+ -- Use Device Privacy Mode for this peer device.
+
+
+enum InquiryMode:
+ [maximum_bits: 8]
+ STANDARD = 0x00
+ -- Standard Inquiry Result format (default)
+
+ RSSI = 0x01
+ -- Inquiry Result format with RSSI
+
+ EXTENDED = 0x02
+ -- Inquiry Result format with RSSI or EIR format
+
+
+enum PageScanType:
+ [maximum_bits: 8]
+ STANDARD_SCAN = 0x00
+ -- Standard scan (default) (mandatory)
+
+ INTERLACED_SCAN = 0x01
+ -- Interlaced scan (optional)
+
+
+bits LEEventMask:
+ 0 [+1] Flag le_connection_complete
+ $next [+1] Flag le_advertising_report
+ $next [+1] Flag le_connection_update_complete
+ $next [+1] Flag le_read_remote_features_complete
+ $next [+1] Flag le_long_term_key_request
+ $next [+1] Flag le_remote_connection_parameter_request
+ $next [+1] Flag le_data_length_change
+ $next [+1] Flag le_read_local_p256_public_key_complete
+ $next [+1] Flag le_generate_dhkey_complete
+ $next [+1] Flag le_enhanced_connection_complete
+ $next [+1] Flag le_directed_advertising_report
+ $next [+1] Flag le_phy_update_complete
+ $next [+1] Flag le_extended_advertising_report
+ $next [+1] Flag le_periodic_advertising_sync_established
+ $next [+1] Flag le_periodic_advertising_report
+ $next [+1] Flag le_periodic_advertising_sync_lost
+ $next [+1] Flag le_extended_scan_timeout
+ $next [+1] Flag le_extended_advertising_set_terminated
+ $next [+1] Flag le_scan_request_received
+ $next [+1] Flag le_channel_selection_algorithm
+ $next [+1] Flag le_connectionless_iq_report
+ $next [+1] Flag le_connection_iq_report
+ $next [+1] Flag le_cte_request_failed
+ $next [+1] Flag le_periodic_advertising_sync_transfer_received_event
+ $next [+1] Flag le_cis_established_event
+ $next [+1] Flag le_cis_request_event
+ $next [+1] Flag le_create_big_complete_event
+ $next [+1] Flag le_terminate_big_complete_event
+ $next [+1] Flag le_big_sync_established_event
+ $next [+1] Flag le_big_sync_lost_event
+ $next [+1] Flag le_request_peer_sca_complete_event
+ $next [+1] Flag le_path_loss_threshold_event
+ $next [+1] Flag le_transmit_power_reporting_event
+ $next [+1] Flag le_biginfo_advertising_report_event
+ $next [+1] Flag le_subrate_change_event
+
+
+enum LEAdvertisingType:
+ [maximum_bits: 8]
+ CONNECTABLE_AND_SCANNABLE_UNDIRECTED = 0x00
+ -- ADV_IND
+
+ CONNECTABLE_HIGH_DUTY_CYCLE_DIRECTED = 0x01
+ -- ADV_DIRECT_IND
+
+ SCANNABLE_UNDIRECTED = 0x02
+ -- ADV_SCAN_IND
+
+ NOT_CONNECTABLE_UNDIRECTED = 0x03
+ -- ADV_NONCONN_IND
+
+ CONNECTABLE_LOW_DUTY_CYCLE_DIRECTED = 0x04
+ -- ADV_DIRECT_IND
+
+
+bits LEAdvertisingChannels:
+ 0 [+1] Flag channel_37
+ $next [+1] Flag channel_38
+ $next [+1] Flag channel_39
+
+
+enum LEAdvertisingFilterPolicy:
+ [maximum_bits: 8]
+
+ ALLOW_ALL = 0x00
+ -- Process scan and connection requests from all devices (i.e., the Filter
+ -- Accept List is not in use) (default).
+
+ ALLOW_ALL_CONNECTIONS_AND_USE_FILTER_ACCEPT_LIST_FOR_SCANS = 0x01
+ -- Process connection requests from all devices and scan requests only from
+ -- devices that are in the Filter Accept List.
+
+ ALLOW_ALL_SCANS_AND_USE_FILTER_ACCEPT_LIST_FOR_CONNECTIONS = 0x02
+ -- Process scan requests from all devices and connection requests only from
+ -- devices that are in the Filter Accept List.
+
+ ALLOW_FILTER_ACCEPT_LIST_ONLY = 0x03
+ -- Process scan and connection requests only from devices in the Filter
+ -- Accept List.
+
+
+enum LESetExtendedAdvDataOp:
+ -- Potential values for the Operation parameter in a HCI_LE_Set_Extended_Advertising_Data command.
+ [maximum_bits: 8]
+ INTERMEDIATE_FRAGMENT = 0x00
+ -- Intermediate fragment of fragmented extended advertising data.
+
+ FIRST_FRAGMENT = 0x01
+ -- First fragment of fragmented extended advertising data.
+
+ LAST_FRAGMENT = 0x02
+ -- Last fragment of fragmented extended advertising data.
+
+ COMPLETE = 0x03
+ -- Complete extended advertising data.
+
+ UNCHANGED_DATA = 0x04
+ -- Unchanged data (just update the Advertising DID)
+
+
+enum LEExtendedAdvFragmentPreference:
+ -- Potential values for the Fragment_Preference parameter in a
+ -- HCI_LE_Set_Extended_Advertising_Data command.
+ [maximum_bits: 8]
+ MAY_FRAGMENT = 0x00
+ -- The Controller may fragment all Host advertising data
+
+ SHOULD_NOT_FRAGMENT = 0x01
+ -- The Controller should not fragment or should minimize fragmentation of Host advertising data
+
+
+enum FlowControlMode:
+ [maximum_bits: 8]
+ PACKET_BASED = 0x00
+ DATA_BLOCK_BASED = 0x01
+
+
+bits EventMaskPage2:
+ 8 [+1] Flag number_of_completed_data_blocks_event
+ 14 [+1] Flag triggered_clock_capture_event
+ 15 [+1] Flag synchronization_train_complete_event
+ 16 [+1] Flag synchronization_train_received_event
+ 17 [+1] Flag connectionless_peripheral_broadcast_receive_event
+ 18 [+1] Flag connectionless_peripheral_broadcast_timeout_event
+ 19 [+1] Flag truncated_page_complete_event
+ 20 [+1] Flag peripheral_page_response_timeout_event
+ 21 [+1] Flag connectionless_peripheral_broadcast_channel_map_event
+ 22 [+1] Flag inquiry_response_notification_event
+ 23 [+1] Flag authenticated_payload_timeout_expired_event
+ 24 [+1] Flag sam_status_change_event
+ 25 [+1] Flag encryption_change_event_v2
+
+
+enum LinkType:
+ [maximum_bits: 8]
+ SCO = 0x00
+ ACL = 0x01
+ ESCO = 0x02
+
+
+enum EncryptionStatus:
+ OFF = 0x00
+ ON_WITH_E0_FOR_BREDR_OR_AES_FOR_LE = 0x01
+ ON_WITH_AES_FOR_BREDR = 0x03
+
+
+bits LmpFeatures(page: UInt:8):
+ -- Bit mask of Link Manager Protocol features.
+ if page == 0:
+ 0 [+1] Flag three_slot_packets
+ 1 [+1] Flag five_slot_packets
+ 2 [+1] Flag encryption
+ 3 [+1] Flag slot_offset
+ 4 [+1] Flag timing_accuracy
+ 5 [+1] Flag role_switch
+ 6 [+1] Flag hold_mode
+ 7 [+1] Flag sniff_mode
+ # 8: previously used
+ 9 [+1] Flag power_control_requests
+ 10 [+1] Flag channel_quality_driven_data_rate
+ 11 [+1] Flag sco_link
+ 12 [+1] Flag hv2_packets
+ 13 [+1] Flag hv3_packets
+ 14 [+1] Flag mu_law_log_synchronous_data
+ 15 [+1] Flag a_law_log_synchronous_data
+ 16 [+1] Flag cvsd_synchronous_data
+ 17 [+1] Flag paging_parameter_negotiation
+ 18 [+1] Flag power_control
+ 19 [+1] Flag transparent_synchronous_data
+ 20 [+3] UInt flow_control_lag
+ 23 [+1] Flag broadcast_encryption
+ # 24: reserved for future use
+ 25 [+1] Flag enhanced_data_rate_acl_2_mbs_mode
+ 26 [+1] Flag enhanced_data_rate_acl_3_mbs_mode
+ 27 [+1] Flag enhanced_inquiry_scan
+ 28 [+1] Flag interlaced_inquiry_scan
+ 29 [+1] Flag interlaced_page_scan
+ 30 [+1] Flag rssi_with_inquiry_results
+ 31 [+1] Flag extended_sco_link_ev3_packets
+ 32 [+1] Flag ev4_packets
+ 33 [+1] Flag ev5_packets
+ # 34: reserved for future use
+ 35 [+1] Flag afh_capable_peripheral
+ 36 [+1] Flag afh_classification_peripheral
+ 37 [+1] Flag bredr_not_supported
+ 38 [+1] Flag le_supported_controller
+ 39 [+1] Flag three_slot_enhanced_data_rate_acl_packets
+ 40 [+1] Flag five_slot_enhanced_data_rate_acl_packets
+ 41 [+1] Flag sniff_subrating
+ 42 [+1] Flag pause_encryption
+ 43 [+1] Flag afh_capable_central
+ 44 [+1] Flag afh_classification_central
+ 45 [+1] Flag enhanced_data_rate_esco_2_mbs_mode
+ 46 [+1] Flag enhanced_data_rate_esco_3_mbs_mode
+ 47 [+1] Flag three_slot_enhanced_data_rate_esco_packets
+ 48 [+1] Flag extended_inquiry_response
+ 49 [+1] Flag simultaneous_le_and_bredr_to_same_device_capable_controller
+ # 50: reserved for future use
+ 51 [+1] Flag secure_simple_pairing_controller_support
+ 52 [+1] Flag encapsulated_pdu
+ 53 [+1] Flag erroneous_data_reporting
+ 54 [+1] Flag non_flushable_packet_boundary_flag
+ # 55: reserved for future use
+ 56 [+1] Flag hci_link_supervision_timeout_changed_event
+ 57 [+1] Flag variable_inquiry_tx_power_level
+ 58 [+1] Flag enhanced_power_control
+ # 59-62: reserved for future use
+ 63 [+1] Flag extended_features
+
+ if page == 1:
+ 0 [+1] Flag secure_simple_pairing_host_support
+ 1 [+1] Flag le_supported_host
+ # 2: previously used
+ 3 [+1] Flag secure_connection_host_support
+
+ if page == 2:
+ 0 [+1] Flag connectionless_peripheral_broadcast_transmitter_operation
+ 1 [+1] Flag connectionless_peripheral_broadcast_receiver_operation
+ 2 [+1] Flag synchronization_train
+ 3 [+1] Flag synchronization_scan
+ 4 [+1] Flag hci_inquiry_response_notification_event
+ 5 [+1] Flag generalized_interlaced_scan
+ 6 [+1] Flag coarse_clock_adjustment
+ # 7: reserved for future use
+ 8 [+1] Flag secure_connections_controller_support
+ 9 [+1] Flag ping
+ 10 [+1] Flag slot_availability_mask
+ 11 [+1] Flag train_nudging
+
+
+enum LEClockAccuracy:
+ -- Possible values that can be reported for the |central_clock_accuracy| and
+ -- |advertiser_clock_accuracy| parameters.
+ [maximum_bits: 8]
+ PPM_500 = 0x00
+ PPM_250 = 0x01
+ PPM_150 = 0x02
+ PPM_100 = 0x03
+ PPM_75 = 0x04
+ PPM_50 = 0x05
+ PPM_30 = 0x06
+ PPM_20 = 0x07
+
+# ========================= HCI packet headers ==========================
+
+
+bits OpCodeBits:
+ # Emboss currently lacks support for default field values and cross-type integral equality.
+ # (https://github.com/google/emboss/issues/21)
+ # (https://github.com/google/emboss/issues/23)
+ # Upon the addition of these features, we will transition OpCodeBits to be a parameterized
+ # field which defaults for each HCI packet type to its corresponding OpCode.
+ 0 [+10] UInt ocf
+ $next [+6] UInt ogf
+
+
+struct CommandHeader:
+ -- HCI Command packet header.
+ 0 [+2] OpCodeBits opcode
+ $next [+1] UInt parameter_total_size
+
+
+struct EventHeader:
+ -- HCI Event packet header.
+ 0 [+1] UInt event_code
+ $next [+1] UInt parameter_total_size
+
+# ========================= HCI Command packets =========================
+# Core Spec v5.3 Vol 4, Part E, Section 7
+
+
+struct InquiryCommand:
+ -- Inquiry Command (v1.1) (BR/EDR)
+ --
+ -- Note: NO Command Complete; Sends Inquiry Complete at the end of the
+ -- inquiry to indicate it's completion. No Inquiry Complete event is sent if
+ -- Inquiry is cancelled.
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+3] InquiryAccessCode lap
+ -- LAP (Lower Address Part)
+ -- In the range 0x9E8B00 - 0x9E8B3F, defined by the Bluetooth SIG in
+ -- Baseband Assigned Numbers.
+
+ $next [+1] UInt inquiry_length
+ -- Time before the inquiry is halted. Defined in 1.28s units.
+ -- Range: 0x01 to kInquiryLengthMax in hci_constants.h
+
+ $next [+1] UInt num_responses
+ -- Maximum number of responses before inquiry is halted.
+ -- Set to 0x00 for unlimited.
+
+
+struct InquiryCancelCommand:
+ -- Inquiry Cancel Command (v1.1) (BR/EDR)
+ -- No command parameters
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct CreateConnectionCommand:
+ -- Create Connection (v1.1) (BR/EDR)
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Connection Complete event will indicate that this command has been
+ -- completed.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- BD_ADDR of the device to be connected
+
+ $next [+2] PacketType packet_type
+ -- Mask of allowable packet types.
+
+ $next [+1] PageScanRepetitionMode page_scan_repetition_mode
+ -- The Page Scan Repetition Mode of the remote device as retrieved by Inquiry.
+
+ $next [+1] UInt reserved
+ [requires: this == 0]
+
+ $next [+2] ClockOffset clock_offset
+ -- Clock Offset. The lower 15 bits are set to the clock offset as retrieved
+ -- by an Inquiry. The highest bit is set to 1 if the rest of this parameter
+ -- is valid.
+
+ $next [+1] GenericEnableParam allow_role_switch
+ -- Allow Role Switch.
+ -- Allowed values:
+ -- 0x00 - No role switch allowed, this device will be the central
+ -- 0x01 - Role switch allowed, this device may become peripheral during
+ -- connection setup
+
+
+struct DisconnectCommand:
+ -- Disconnect Command (v1.1) (BR/EDR & LE)
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Disconnection Complete event will indicate that this command has been
+ -- completed.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+
+ $next [+1] StatusCode reason
+ -- Reason for the disconnect.
+
+
+struct CreateConnectionCancelCommand:
+ -- Create Connection Cancel (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- BD_ADDR of the Create Connection Command request
+
+
+struct AcceptConnectionRequestCommand:
+ -- Accept Connection Request (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The 48-bit BD_ADDR of the remote device requesting the connection.
+
+ $next [+1] ConnectionRole role
+
+
+struct RejectConnectionRequestCommand:
+ -- Reject Connection Request (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The 48-bit BD_ADDR of the remote device requesting the connection.
+
+ $next [+1] StatusCode reason
+ -- Must be one of CONNECTION_REJECTED* from StatusCode in this file
+
+
+struct LinkKey:
+ 0 [+16] UInt:8[16] value
+
+
+struct LinkKeyRequestReplyCommand:
+ -- Link Key Request Reply Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The 48-bit BD_ADDR of the remote device requesting the connection.
+
+ let bredr_link_key_size = LinkKey.$size_in_bytes
+ $next [+bredr_link_key_size] LinkKey link_key
+ -- Link key to use for the connection with the peer device.
+
+
+struct LinkKeyRequestNegativeReplyCommand:
+ -- Link Key Request Negative Reply Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- BD_ADDR of the peer device that the host does not have a link key for.
+
+
+struct AuthenticationRequestedCommand:
+ -- Authentication Requested Command (v1.1) (BR/EDR)
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Authentication Complete event will indicate that this command has been
+ -- completed.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ -- Must be the handle of a connected ACL-U logical link.
+
+
+struct SetConnectionEncryptionCommand:
+ -- Set Connection Encryption Command (v1.1) (BR/EDR)
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Encryption Change event will indicate that this command has been completed.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ -- Must be the handle of a connected ACL-U logical link.
+
+ $next [+1] GenericEnableParam encryption_enable
+ -- Whether link level encryption should be turned on or off.
+
+
+struct RemoteNameRequestCommand:
+ -- Remote Name Request Command (v1.1) (BR/EDR)
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Remote Name Request Complete event will indicate that this command has been
+ -- completed.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- Address of the device whose name is to be requested.
+
+ $next [+1] PageScanRepetitionMode page_scan_repetition_mode
+ -- Page Scan Repetition Mode of the device, obtained by Inquiry.
+
+ $next [+1] UInt reserved
+ [requires: this == 0]
+
+ $next [+2] ClockOffset clock_offset
+ -- Clock offset. The lower 15 bits of this represent bits 14-2
+ -- of CLKNPeripheral-CLK, and the highest bit is set when the other
+ -- bits are valid.
+
+
+struct ReadRemoteSupportedFeaturesCommand:
+ -- Read Remote Supported Features Command (v1.1) (BR/EDR)
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Read Remote Supported Features Complete event will indicate that this
+ -- command has been completed.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ -- Must be the handle of a connected ACL-U logical link.
+
+
+struct ReadRemoteExtendedFeaturesCommand:
+ -- Read Remote Extended Features Command (v1.2) (BR/EDR)
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Read Remote Extended Features Complete event will indicate that this
+ -- command has been completed.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ -- Must be the handle of a connected ACL-U logical link.
+
+ $next [+1] UInt page_number
+ -- Page of features to read.
+ -- Values:
+ -- - 0x00 standard features as if requested by Read Remote Supported Features
+ -- - 0x01-0xFF the corresponding features page (see Vol 2, Part C, Sec 3.3).
+
+
+struct ReadRemoteVersionInfoCommand:
+ -- Read Remote Version Information Command (v1.1) (BR/EDR & LE)
+ --
+ -- NOTE on ReturnParams: No Command Complete event will be sent by the
+ -- Controller to indicate that this command has been completed. Instead, the
+ -- Read Remote Version Information Complete event will indicate that this
+ -- command has been completed.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+
+
+struct RejectSynchronousConnectionRequestCommand:
+ -- Reject Synchronous Connection Command (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- Address of the remote device that sent the request.
+
+ $next [+1] StatusCode reason
+ -- Reason the connection request was rejected.
+
+
+struct IoCapabilityRequestReplyCommand:
+ -- IO Capability Request Reply Command (v2.1 + EDR) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in simple pairing process
+
+ $next [+1] IoCapability io_capability
+ -- The IO capabilities of this device.
+
+ $next [+1] OobDataPresent oob_data_present
+ -- Whether there is out-of-band data present, and what type.
+
+ $next [+1] AuthenticationRequirements authentication_requirements
+ -- Authentication requirements of the host.
+
+
+struct UserConfirmationRequestReplyCommand:
+ -- User Confirmation Request Reply Command (v2.1 + EDR) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in simple pairing process
+
+
+struct UserConfirmationRequestNegativeReplyCommand:
+ -- User Confirmation Request Negative Reply Command (v2.1 + EDR) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in simple pairing process
+
+
+struct UserPasskeyRequestReplyCommand:
+ -- User Passkey Request Reply Command (v2.1 + EDR) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in simple pairing process
+
+ $next [+4] UInt numeric_value
+ -- Numeric value (passkey) entered by user.
+ [requires: 0 <= this <= 999999]
+
+
+struct SynchronousConnectionParameters:
+ -- Enhanced Setup Synchronous Connection Command (CSA2) (BR/EDR)
+
+ struct VendorCodingFormat:
+ 0 [+1] CodingFormat coding_format
+ $next [+2] UInt company_id
+ -- See assigned numbers.
+
+ $next [+2] UInt vendor_codec_id
+ -- Shall be ignored if |coding_format| is not VENDOR_SPECIFIC.
+
+ enum ScoRetransmissionEffort:
+ [maximum_bits: 8]
+ NONE = 0x00
+ -- SCO or eSCO
+
+ POWER_OPTIMIZED = 0x01
+ -- eSCO only
+
+ QUALITY_OPTIMIZED = 0x02
+ -- eSCO only
+
+ DONT_CARE = 0xFF
+ -- SCO or eSCO
+
+ 0 [+4] UInt transmit_bandwidth
+ -- Transmit bandwidth in octets per second.
+
+ $next [+4] UInt receive_bandwidth
+ -- Receive bandwidth in octets per second.
+
+ let vcf_size = VendorCodingFormat.$size_in_bytes
+
+ $next [+vcf_size] VendorCodingFormat transmit_coding_format
+ -- Local Controller -> Remote Controller coding format.
+
+ $next [+vcf_size] VendorCodingFormat receive_coding_format
+ -- Remote Controller -> Local Controller coding format.
+
+ $next [+2] UInt transmit_codec_frame_size_bytes
+
+ $next [+2] UInt receive_codec_frame_size_bytes
+
+ $next [+4] UInt input_bandwidth
+ -- Host->Controller data rate in octets per second.
+
+ $next [+4] UInt output_bandwidth
+ -- Controller->Host data rate in octets per second.
+
+ $next [+vcf_size] VendorCodingFormat input_coding_format
+ -- Host->Controller coding format.
+
+ $next [+vcf_size] VendorCodingFormat output_coding_format
+ -- Controller->Host coding format.
+
+ $next [+2] UInt input_coded_data_size_bits
+ -- Size, in bits, of the sample or framed data.
+
+ $next [+2] UInt output_coded_data_size_bits
+ -- Size, in bits, of the sample or framed data.
+
+ $next [+1] PcmDataFormat input_pcm_data_format
+
+ $next [+1] PcmDataFormat output_pcm_data_format
+
+ $next [+1] UInt input_pcm_sample_payload_msb_position
+ -- The number of bit positions within an audio sample that the MSB of
+ -- the sample is away from starting at the MSB of the data.
+
+ $next [+1] UInt output_pcm_sample_payload_msb_position
+ -- The number of bit positions within an audio sample that the MSB of
+ -- the sample is away from starting at the MSB of the data.
+
+ $next [+1] ScoDataPath input_data_path
+
+ $next [+1] ScoDataPath output_data_path
+
+ $next [+1] UInt input_transport_unit_size_bits
+ -- The number of bits in each unit of data received from the Host over the audio data transport.
+ -- 0 indicates "not applicable" (implied by the choice of audio data transport).
+
+ $next [+1] UInt output_transport_unit_size_bits
+ -- The number of bits in each unit of data sent to the Host over the audio data transport.
+ -- 0 indicates "not applicable" (implied by the choice of audio data transport).
+
+ $next [+2] UInt max_latency_ms
+ -- The value in milliseconds representing the upper limit of the sum of
+ -- the synchronous interval, and the size of the eSCO window, where the
+ -- eSCO window is the reserved slots plus the retransmission window.
+ -- Minimum: 0x0004
+ -- Don't care: 0xFFFF
+
+ $next [+2] ScoPacketType packet_types
+ -- Bitmask of allowed packet types.
+
+ $next [+1] ScoRetransmissionEffort retransmission_effort
+
+
+struct EnhancedSetupSynchronousConnectionCommand:
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- The connection handle of the associated ACL link if creating a new (e)SCO connection, or the
+ -- handle of an existing eSCO link if updating connection parameters.
+
+ let scp_size = SynchronousConnectionParameters.$size_in_bytes
+ $next [+scp_size] SynchronousConnectionParameters connection_parameters
+
+
+struct EnhancedAcceptSynchronousConnectionRequestCommand:
+ -- Enhanced Accept Synchronous Connection Request Command (CSA2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The 48-bit BD_ADDR of the remote device requesting the connection.
+
+ let scp_size = SynchronousConnectionParameters.$size_in_bytes
+ $next [+scp_size] SynchronousConnectionParameters connection_parameters
+
+
+struct SetEventMaskCommand:
+ -- Set Event Mask Command (v1.1)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+8] UInt event_mask
+ -- 64-bit Bit mask used to control which HCI events are generated by the HCI for the
+ -- Host. See enum class EventMask in hci_constants.h
+
+
+struct WriteLocalNameCommand:
+ -- Write Local Name Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ let local_name_size = LocalName.$size_in_bytes
+ $next [+local_name_size] LocalName local_name
+ -- A UTF-8 encoded User Friendly Descriptive Name for the device.
+ -- If the name contained in the parameter is shorter than 248 octets, the end
+ -- of the name is indicated by a NULL octet (0x00), and the following octets
+ -- (to fill up 248 octets, which is the length of the parameter) do not have
+ -- valid values.
+
+
+struct WritePageTimeoutCommand:
+ -- Write Page Timeout Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt page_timeout
+ -- Page_Timeout, in time slices (0.625 ms)
+ -- Range: From MIN to MAX in PageTimeout in this file
+ [requires: 0x0001 <= this <= 0xFFFF]
+
+
+struct WriteScanEnableCommand:
+ -- Write Scan Enable Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] ScanEnableBits scan_enable
+ -- Bit Mask of enabled scans. See enum class ScanEnableBits in this file
+ -- for how to construct this bitfield.
+
+
+struct WritePageScanActivityCommand:
+ -- Write Page Scan Activity Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt page_scan_interval
+ -- Page_Scan_Interval, in time slices (0.625ms)
+ -- Valid Range: MIN - MAX in ScanInterval in this file
+ [requires: 0x0012 <= this <= 0x1000]
+
+ $next [+2] UInt page_scan_window
+ -- Page_Scan_Window, in time slices
+ -- Valid Range: MIN - MAX in ScanWindow in this file
+ [requires: 0x0011 <= this <= 0x1000]
+
+
+struct WriteInquiryScanActivityCommand:
+ -- Write Inquiry Scan Activity Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt inquiry_scan_interval
+ -- Inquiry_Scan_Interval, in time slices (0.625ms)
+ -- Valid Range: MIN - MAX in ScanInterval in this file
+ [requires: 0x0012 <= this <= 0x1000]
+
+ $next [+2] UInt inquiry_scan_window
+ -- Inquiry_Scan_Window, in time slices
+ -- Valid Range: MIN - MAX in ScanWindow in this file
+ [requires: 0x0011 <= this <= 0x1000]
+
+
+struct WriteAutomaticFlushTimeoutCommand:
+ -- Write Automatic Flush Timeout Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Connection_Handle (only the lower 12-bits are meaningful).
+ -- Range: 0x0000 to 0x0EFF
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt flush_timeout
+ -- The value for the Flush_Timeout configuration parameter (Core Spec v5.2, Vol 4, Part E, Sec 6.19).
+ -- Range: 0x0000 to 0x07FF. 0x0000 indicates infinite flush timeout (no automatic flush).
+ -- Time = flush_timeout * 0.625ms.
+ -- Time Range: 0.625ms to 1279.375ms.
+ [requires: 0x0000 <= this <= 0x07FF]
+
+
+struct WriteSynchronousFlowControlEnableCommand:
+ -- Write Synchonous Flow Control Enable Command (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam synchronous_flow_control_enable
+ -- If enabled, HCI_Number_Of_Completed_Packets events shall be sent from the controller
+ -- for synchronous connection handles.
+
+
+struct WriteInquiryScanTypeCommand:
+ -- Write Inquiry Scan Type (v1.2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] InquiryScanType inquiry_scan_type
+ -- See enum class InquiryScanType in this file for possible values
+
+
+struct WriteExtendedInquiryResponseCommand:
+ -- Write Extended Inquiry Response (v1.2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt fec_required
+ -- If FEC Encoding is required. (v1.2) (7.3.56)
+
+ let eir_size = ExtendedInquiryResponse.$size_in_bytes
+ $next [+eir_size] ExtendedInquiryResponse extended_inquiry_response
+ -- Extended inquiry response data as defined in Vol 3, Part C, Sec 8
+
+
+struct WriteSimplePairingModeCommand:
+ -- Write Simple Pairing Mode (v2.1 + EDR) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam simple_pairing_mode
+
+
+struct LESetAdvertisingEnableCommand:
+ -- LE Set Advertising Enable command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam advertising_enable
+
+
+struct LESetExtendedAdvertisingEnableData:
+ -- Data fields for variable-length portion of an LE Set Extended Advertising Enable command
+ 0 [+1] UInt advertising_handle
+ $next [+2] UInt duration
+ $next [+1] UInt max_extended_advertising_events
+
+
+struct LESetExtendedAdvertisingDataCommand:
+ -- LE Set Extended Advertising Data Command (v5.0) (LE)
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+1] LESetExtendedAdvDataOp operation
+
+ $next [+1] LEExtendedAdvFragmentPreference fragment_preference
+ -- Provides a hint to the Controller as to whether advertising data should be fragmented.
+
+ $next [+1] UInt advertising_data_length (sz)
+ -- Length of the advertising data included in this command packet, up to
+ -- kMaxLEExtendedAdvertisingDataLength bytes. If the advertising set uses legacy advertising
+ -- PDUs that support advertising data then this shall not exceed kMaxLEAdvertisingDataLength
+ -- bytes.
+ [requires: 0 <= this <= 251]
+
+ $next [+sz] UInt:8[sz] advertising_data
+ -- Variable length advertising data.
+
+
+struct LESetExtendedScanResponseDataCommand:
+ -- LE Set Extended Scan Response Data Command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt advertising_handle
+ -- Used to identify an advertising set
+ [requires: 0x00 <= this <= 0xEF]
+
+ $next [+1] LESetExtendedAdvDataOp operation
+ $next [+1] LEExtendedAdvFragmentPreference fragment_preference
+ -- Provides a hint to the controller as to whether advertising data should be fragmented
+
+ $next [+1] UInt scan_response_data_length (sz)
+ -- The number of octets in the scan_response_data parameter
+ [requires: 0 <= this <= 251]
+
+ $next [+sz] UInt:8[sz] scan_response_data
+ -- Scan response data formatted as defined in Core Spec v5.4, Vol 3, Part C, Section 11
+
+
+struct LESetExtendedAdvertisingEnableCommand:
+ -- LE Set Extended Advertising Enable command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam enable
+ $next [+1] UInt num_sets
+ let single_data_size = LESetExtendedAdvertisingEnableData.$size_in_bytes
+ $next [+single_data_size*num_sets] LESetExtendedAdvertisingEnableData[] data
+
+
+struct LEReadMaxAdvertisingDataLengthCommand:
+ -- LE Read Maximum Advertising Data Length Command (v5.0) (LE)
+ -- This command has no parameters
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadNumSupportedAdvertisingSetsCommand:
+ -- LE Read Number of Supported Advertising Sets Command (v5.0) (LE)
+ -- This command has no parameters
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LERemoveAdvertisingSetCommand:
+ -- LE Remove Advertising Set command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt advertising_handle
+
+
+struct LEClearAdvertisingSetsCommand:
+ -- LE Clear Advertising Sets Command (v5.0) (LE)
+ -- This command has no parameters
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LESetExtendedScanParametersData:
+ -- Data fields for variable-length portion of an LE Set Extneded Scan Parameters command
+
+ 0 [+1] LEScanType scan_type
+
+ $next [+2] UInt scan_interval
+ -- Time interval from when the Controller started its last scan until it begins the subsequent
+ -- scan on the primary advertising physical channel.
+ -- Time = N × 0.625 ms
+ -- Time Range: 2.5 ms to 40.959375 s
+ [requires: 0x0004 <= this]
+
+ $next [+2] UInt scan_window
+ -- Duration of the scan on the primary advertising physical channel.
+ -- Time = N × 0.625 ms
+ -- Time Range: 2.5 ms to 40.959375 s
+ [requires: 0x0004 <= this]
+
+
+struct LESetExtendedScanParametersCommand(num_entries: UInt:8):
+ -- LE Set Extended Scan Parameters Command (v5.0) (LE)
+ -- num_entries corresponds to the number of bits set in the |scanning_phys| field
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] LEOwnAddressType own_address_type
+ $next [+1] LEScanFilterPolicy scanning_filter_policy
+ $next [+1] LEPHYBits scanning_phys
+ let single_entry_size = LESetExtendedScanParametersData.$size_in_bytes
+ let total_entries_size = num_entries*single_entry_size
+ $next [+total_entries_size] LESetExtendedScanParametersData[num_entries] data
+ -- Indicates the type of address being used in the scan request packets (for active scanning).
+
+
+struct LESetExtendedScanEnableCommand:
+ -- LE Set Extended Scan Enable Command (v5.0) (LE)
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+1] GenericEnableParam scanning_enabled
+
+ $next [+1] LEExtendedDuplicateFilteringOption filter_duplicates
+ -- See enum class LEExtendedDuplicateFilteringOption in this file for possible values
+
+ $next [+2] UInt duration
+ -- Possible values:
+ -- 0x0000: Scan continuously until explicitly disabled
+ -- 0x0001-0xFFFF: Scan duration, where:
+ -- Time = N * 10 ms
+ -- Time Range: 10 ms to 655.35 s
+
+ $next [+2] UInt period
+ -- Possible values:
+ -- 0x0000: Periodic scanning disabled (scan continuously)
+ -- 0x0001-0xFFFF: Time interval from when the Controller started its last
+ -- Scan_Duration until it begins the subsequent Scan_Duration, where:
+ -- Time = N * 1.28 sec
+ -- Time Range: 1.28 s to 83,884.8 s
+
+
+struct UserPasskeyRequestNegativeReplyCommand:
+ -- User Passkey Request Negative Reply Command (v2.1 + EDR) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in the simple pairing process.
+
+
+struct IoCapabilityRequestNegativeReplyCommand:
+ -- IO Capability Request Negative Reply Command (v2.1 + EDR) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The BD_ADDR of the remote device involved in the simple pairing process.
+
+ $next [+1] StatusCode reason
+ -- Reason that Simple Pairing was rejected. See 7.1.36 for valid error codes.
+
+
+struct ResetCommand:
+ -- Reset Command (v1.1)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadLocalNameCommand:
+ -- Read Local Name Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadScanEnableCommand:
+ -- Read Scan Enable Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadPageScanActivityCommand:
+ -- Read Page Scan Activity Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadInquiryScanActivityCommand:
+ -- Read Inquiry Scan Activity Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadClassOfDeviceCommand:
+ -- Read Class of Device Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct WriteClassOfDeviceCommand:
+ -- Write Class Of Device Command (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+3] ClassOfDevice class_of_device
+
+
+struct LEPeriodicAdvertisingCreateSyncCommand:
+ -- LE Periodic Advertising Create Sync Command (v5.0) (LE)
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+1] LEPeriodicAdvertisingCreateSyncOptions options
+
+ $next [+1] UInt advertising_sid
+ -- Advertising SID subfield in the ADI field used to identify the Periodic Advertising
+ [requires: 0x00 <= this <= 0x0F]
+
+ $next [+1] LEPeriodicAdvertisingAddressType advertiser_address_type
+
+ $next [+BdAddr.$size_in_bytes] BdAddr advertiser_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or Random (static)
+ -- Identity Address of the advertiser
+
+ $next [+2] UInt skip
+ -- The maximum number of periodic advertising events that can be skipped after a successful
+ -- receive
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt sync_timeout
+ -- Synchronization timeout for the periodic advertising.
+ -- Time = N * 10 ms
+ -- Time Range: 100 ms to 163.84 s
+ [requires: 0x000A <= this <= 0x4000]
+
+ $next [+1] LEPeriodicAdvertisingSyncCTEType sync_cte_type
+ -- Constant Tone Extension sync options
+
+
+struct LEPeriodicAdvertisingCreateSyncCancel:
+ -- LE Periodic Advertising Create Sync Cancel Command (v5.0) (LE)
+ -- Note that this command has no arguments
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEPeriodicAdvertisingTerminateSyncCommand:
+ -- LE Periodic Advertising Terminate Sync Command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt sync_handle
+ -- Identifies the periodic advertising train
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LEAddDeviceToPeriodicAdvertiserListCommand:
+ -- LE Add Device To Periodic Advertiser List command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] LEAddressType advertiser_address_type
+ -- Address type of the advertiser. The LEAddressType::kPublicIdentity and
+ -- LEAddressType::kRandomIdentity values are excluded for this command.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr advertiser_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or
+ -- Random (static) Identity Address of the advertiser.
+
+ $next [+1] UInt advertising_sid
+ -- Advertising SID subfield in the ADI field used to identify the Periodic
+ -- Advertising.
+
+
+struct LERemoveDeviceFromPeriodicAdvertiserListCommand:
+ -- LE Remove Device From Periodic Advertiser List command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt advertiser_address_type
+ -- Address type of the advertiser. The LEAddressType::kPublicIdentity and
+ -- LEAddressType::kRandomIdentity values are excluded for this command.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr advertiser_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or
+ -- Random (static) Identity Address of the advertiser.
+
+ $next [+1] UInt advertising_sid
+ -- Advertising SID subfield in the ADI field used to identify the Periodic
+ -- Advertising.
+
+
+struct LEClearPeriodicAdvertiserListCommand:
+ -- LE Clear Periodic Advertiser List command (v5.0) (LE)
+ -- Note that this command has no arguments
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadPeriodicAdvertiserListSizeCommand:
+ -- LE Read Periodic Advertiser List Size command (v5.0) (LE)
+ -- Note that this command has no arguments
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadTransmitPowerCommand:
+ -- LE Read Transmit Power command (v5.0) (LE)
+ -- Note that this command has no arguments
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadRFPathCompensationCommand:
+ -- LE Read RF Path Compensation command (v5.0) (LE)
+ -- Note that this command has no arguments
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEWriteRFPathCompensationCommand:
+ -- LE Write RF Path Compensation command (v5.0) (LE)
+ -- Values provided are used in the Tx Power Level and RSSI calculation.
+ -- Range: -128.0 dB (0xFB00) ≤ N ≤ 128.0 dB (0x0500)
+ -- Units: 0.1 dB
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] Int rf_tx_path_compensation_value
+ [requires: -1280 <= this <= 1280]
+
+ $next [+2] Int rf_rx_path_compensation_value
+ [requires: -1280 <= this <= 1280]
+
+
+struct LESetPrivacyModeCommand:
+ -- LE Set Privacy Mode command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] LEPeerAddressType peer_identity_address_type
+ -- The peer identity address type (either Public Identity or Private
+ -- Identity).
+
+ $next [+BdAddr.$size_in_bytes] BdAddr peer_identity_address
+ -- Public Identity Address or Random (static) Identity Address of the
+ -- advertiser.
+
+ $next [+1] LEPrivacyMode privacy_mode
+ -- The privacy mode to be used for the given entry on the resolving list.
+
+
+struct ReadInquiryModeCommand:
+ -- Read Inquiry Mode (v1.2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct WriteInquiryModeCommand:
+ -- Write Inquiry Mode (v1.2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] InquiryMode inquiry_mode
+
+
+struct ReadPageScanTypeCommand:
+ -- Read Page Scan Type (v1.2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct WritePageScanTypeCommand:
+ -- Write Page Scan Type (v1.2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] PageScanType page_scan_type
+
+
+struct ReadSimplePairingModeCommand:
+ -- Read Simple Pairing Mode (v2.1 + EDR) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct WriteLEHostSupportCommand:
+ -- Write LE Host Support Command (v4.0) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam le_supported_host
+ -- Sets the LE Supported (Host) Link Manager Protocol feature bit.
+
+ $next [+1] UInt unused
+ -- Core Spec v5.0, Vol 2, Part E, Section 6.35: This parameter was named
+ -- "Simultaneous_LE_Host" and the value is set to "disabled(0x00)" and
+ -- "shall be ignored".
+ -- Core Spec v5.3, Vol 4, Part E, Section 7.3.79: This parameter was renamed
+ -- to "Unused" and "shall be ignored by the controller".
+
+
+struct ReadLocalVersionInformationCommand:
+ -- Read Local Version Information Command (v1.1)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadLocalSupportedCommandsCommand:
+ -- Read Local Supported Commands Command (v1.2)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadBufferSizeCommand:
+ -- Read Buffer Size Command (v1.1)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadBdAddrCommand:
+ -- Read BD_ADDR Command (v1.1) (BR/EDR, LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadLocalSupportedFeaturesCommand:
+ -- Read Local Supported Features Command (v1.1)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct ReadLocalExtendedFeaturesCommand:
+ -- Read Local Extended Features Command (v1.2) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt page_number
+ -- 0x00: Requests the normal LMP features as returned by
+ -- Read_Local_Supported_Features.
+ -- 0x01-0xFF: Return the corresponding page of features.
+
+
+struct ReadEncryptionKeySizeCommand:
+ -- Read Encryption Key Size (v1.1) (BR/EDR)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ -- Identifies an active ACL link (only the lower 12 bits are meaningful).
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LESetEventMaskCommand:
+ -- LE Set Event Mask Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+8] bits:
+ 0 [+35] LEEventMask le_event_mask
+ -- Bitmask that indicates which LE events are generated by the HCI for the Host.
+
+
+struct LEReadBufferSizeCommandV1:
+ -- LE Read Buffer Size Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadBufferSizeCommandV2:
+ -- LE Read Buffer Size Command (v5.2) (LE)
+ -- Version 2 of this command changed the opcode and added ISO return
+ -- parameters.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEReadLocalSupportedFeaturesCommand:
+ -- LE Read Local Supported Features Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LESetRandomAddressCommand:
+ -- LE Set Random Address Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr random_address
+
+
+struct LESetAdvertisingParametersCommand:
+ -- LE Set Advertising Parameters Command (v4.0) (LE)
+
+ [requires: advertising_interval_min <= advertising_interval_max]
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+2] UInt advertising_interval_min
+ -- Default: 0x0800 (1.28 s)
+ -- Time: N * 0.625 ms
+ -- Time Range: 20 ms to 10.24 s
+ [requires: 0x0020 <= this <= 0x4000]
+
+ $next [+2] UInt advertising_interval_max
+ -- Default: 0x0800 (1.28 s)
+ -- Time: N * 0.625 ms
+ -- Time Range: 20 ms to 10.24 s
+ [requires: 0x0020 <= this <= 0x4000]
+
+ $next [+1] LEAdvertisingType adv_type
+ -- Used to determine the packet type that is used for advertising when
+ -- advertising is enabled.
+
+ $next [+1] LEOwnAddressType own_address_type
+
+ $next [+1] LEPeerAddressType peer_address_type
+ -- ANONYMOUS address type not allowed.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr peer_address
+ -- Public Device Address, Random Device Address, Public Identity Address, or
+ -- Random (static) Identity Address of the device to be connected.
+
+ $next [+1] bits:
+
+ 0 [+3] LEAdvertisingChannels advertising_channel_map
+ -- Indicates the advertising channels that shall be used when transmitting
+ -- advertising packets. At least 1 channel must be enabled.
+ -- Default: all channels enabled
+
+ $next [+1] LEAdvertisingFilterPolicy advertising_filter_policy
+ -- This parameter shall be ignored when directed advertising is enabled.
+
+
+struct LEReadAdvertisingChannelTxPowerCommand:
+ -- LE Read Advertising Channel Tx Power Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LESetAdvertisingDataCommand:
+ -- LE Set Advertising Data Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt advertising_data_length
+ -- The number of significant octets in `advertising_data`.
+ [requires: 0x00 <= this <= 0x1F]
+
+ $next [+31] UInt:8[31] advertising_data
+ -- 31 octets of advertising data formatted as defined in Core Spec
+ -- v5.3, Vol 3, Part C, Section 11.
+ -- Default: All octets zero
+
+
+struct LESetScanResponseDataCommand:
+ -- LE Set Scan Response Data Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt scan_response_data_length
+ -- The number of significant octets in `scan_response_data`.
+ [requires: 0x00 <= this <= 0x1F]
+
+ $next [+31] UInt:8[31] scan_response_data
+ -- 31 octets of scan response data formatted as defined in Core Spec
+ -- v5.3, Vol 3, Part C, Section 11.
+ -- Default: All octets zero
+
+
+struct LESetScanParametersCommand:
+ -- LE Set Scan Parameters Command (v4.0) (LE)
+
+ [requires: le_scan_window <= le_scan_interval]
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+1] LEScanType le_scan_type
+ -- Controls the type of scan to perform.
+
+ $next [+2] UInt le_scan_interval
+ -- Default: 0x0010 (10ms)
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+2] UInt le_scan_window
+ -- Default: 0x0010 (10ms)
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+1] LEOwnAddressType own_address_type
+ -- The type of address being used in the scan request packets.
+
+ $next [+1] LEScanFilterPolicy scanning_filter_policy
+
+
+struct LESetScanEnableCommand:
+ -- LE Set Scan Enable Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam le_scan_enable
+ $next [+1] GenericEnableParam filter_duplicates
+ -- Controls whether the Link Layer should filter out duplicate advertising
+ -- reports to the Host, or if the Link Layer should generate advertising
+ -- reports for each packet received. Ignored if le_scan_enable is set to
+ -- disabled.
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.4.3.5
+
+
+struct LECreateConnectionCommand:
+ -- LE Create Connection Command (v4.0) (LE)
+
+ [requires: le_scan_window <= le_scan_interval && connection_interval_min <= connection_interval_max]
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+2] UInt le_scan_interval
+ -- The time interval from when the Controller started the last LE scan until
+ -- it begins the subsequent LE scan.
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+2] UInt le_scan_window
+ -- Amount of time for the duration of the LE scan.
+ -- Time: N * 0.625 ms
+ -- Time Range: 2.5 ms to 10.24 s
+ [requires: 0x0004 <= this <= 0x4000]
+
+ $next [+1] GenericEnableParam initiator_filter_policy
+
+ $next [+1] LEAddressType peer_address_type
+
+ $next [+BdAddr.$size_in_bytes] BdAddr peer_address
+
+ $next [+1] LEOwnAddressType own_address_type
+
+ $next [+2] UInt connection_interval_min
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt connection_interval_max
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt max_latency
+ -- Maximum Peripheral latency for the connection in number of connection
+ -- events.
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ -- Time: N * 10 ms
+ -- Time Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+2] UInt min_connection_event_length
+ -- Time: N * 0.625 ms
+
+ $next [+2] UInt max_connection_event_length
+ -- Time: N * 0.625 ms
+
+
+struct LECreateConnectionCancelCommand:
+ -- LE Create Connection Cancel Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEClearFilterAcceptListCommand:
+ -- LE Clear Filter Accept List Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEAddDeviceToFilterAcceptListCommand:
+ -- LE Add Device To Filter Accept List Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] LEPeerAddressType address_type
+ -- The address type of the peer.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr address
+ -- Public Device Address or Random Device Address of the device to be added
+ -- to the Filter Accept List. Ignored if `address_type` is ANONYMOUS.
+
+
+struct LERemoveDeviceFromFilterAcceptListCommand:
+ -- LE Remove Device From Filter Accept List Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] LEPeerAddressType address_type
+ -- The address type of the peer.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr address
+ -- Public Device Address or Random Device Address of the device to be added
+ -- to the Filter Accept List. Ignored if `address_type` is ANONYMOUS.
+
+
+struct LEConnectionUpdateCommand:
+ -- LE Connection Update Command (v4.0) (LE)
+
+ [requires: connection_interval_min <= connection_interval_max && min_connection_event_length <= max_connection_event_length]
+
+ let hdr_size = CommandHeader.$size_in_bytes
+
+ 0 [+hdr_size] CommandHeader header
+
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt connection_interval_min
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt connection_interval_max
+ -- Time: N * 1.25 ms
+ -- Time Range: 7.5 ms to 4 s.
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt max_latency
+ -- Maximum Peripheral latency for the connection in number of subrated
+ -- connection events.
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ -- Time: N * 10 ms
+ -- Time Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+2] UInt min_connection_event_length
+ -- Time: N * 0.625 ms
+
+ $next [+2] UInt max_connection_event_length
+ -- Time: N * 0.625 ms
+
+
+struct LEReadRemoteFeaturesCommand:
+ -- LE Read Remote Features Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LEEnableEncryptionCommand:
+ -- LE Enable Encryption Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+8] UInt random_number
+ $next [+2] UInt encrypted_diversifier
+ $next [+LinkKey.$size_in_bytes] LinkKey long_term_key
+
+
+struct LELongTermKeyRequestNegativeReplyCommand:
+ -- LE Long Term Key Request Negative Reply Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct LEReadSupportedStatesCommand:
+ -- LE Read Supported States Command (v4.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LEClearResolvingListCommand:
+ -- LE Clear Resolving List Command (v4.2) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+
+
+struct LESetAddressResolutionEnableCommand:
+ -- LE Set Address Resolution Enable Command (v4.2) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] GenericEnableParam address_resolution_enable
+
+
+struct LESetAdvertisingSetRandomAddressCommand:
+ -- LE Set Advertising Set Random Address Command (v5.0) (LE)
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+BdAddr.$size_in_bytes] BdAddr random_address
+ -- The random address to use in the advertising PDUs.
+
+
+struct WriteAuthenticatedPayloadTimeoutCommand:
+ -- Write Authenticated Payload Timeout Command (v4.1) (BR/EDR & LE)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt authenticated_payload_timeout
+ -- Default = 0x0BB8 (30 s)
+ -- Time = N * 10 ms
+ -- Time Range: 10 ms to 655,350 ms
+ [requires: 0x0001 <= this <= 0xFFFF]
+
+
+struct ReadAuthenticatedPayloadTimeoutCommand:
+ -- Read Authenticated Payload Timeout Command (v4.1) (BR/EDR & LE)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct ReadLEHostSupportCommand:
+ -- Read LE Host Support Command (v4.0) (BR/EDR)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+
+
+struct ReadFlowControlModeCommand:
+ -- Read Flow Control Mode Command (v3.0 + HS) (BR/EDR)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+
+
+struct WriteFlowControlModeCommand:
+ -- Write Flow Control Mode Command (v3.0 + HS) (BR/EDR)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+ $next [+1] FlowControlMode flow_control_mode
+
+
+struct SetEventMaskPage2Command:
+ -- Set Event Mask Page 2 Command (v3.0 + HS)
+ 0 [+CommandHeader.$size_in_bytes] CommandHeader header
+ $next [+8] bits:
+ 0 [+26] EventMaskPage2 event_mask_page_2
+ -- Bit mask used to control which HCI events are generated by the HCI for the Host.
+
+# ========================= HCI Event packets ===========================
+# Core Spec v5.3 Vol 4, Part E, Section 7.7
+
+
+struct VendorDebugEvent:
+ -- This opcode is reserved for vendor-specific debugging events.
+ -- See Core Spec v5.3 Vol 4, Part E, Section 5.4.4.
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] UInt subevent_code
+ -- The event code for the vendor subevent.
+
+
+struct InquiryCompleteEvent:
+ -- Inquiry Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+
+
+struct CommandCompleteEvent:
+ -- Core Spec v5.3 Vol 4, Part E, Section 7.7.14
+ -- EventHeader.opcode == 0xe
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] UInt num_hci_command_packets
+ $next [+2] OpCodeBits command_opcode
+ let event_fixed_size = $size_in_bytes-hdr_size
+ let return_parameters_size = header.parameter_total_size-event_fixed_size
+
+
+struct ConnectionCompleteEvent:
+ -- Connection Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The address of the connected device
+
+ $next [+1] LinkType link_type
+ $next [+1] GenericEnableParam encryption_enabled
+
+
+struct ConnectionRequestEvent:
+ -- Connection Request Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ -- The address of the device that's requesting the connection.
+
+ $next [+3] ClassOfDevice class_of_device
+ -- The Class of Device of the device which requests the connection.
+
+ $next [+1] LinkType link_type
+
+
+struct DisconnectionCompleteEvent:
+ -- Disconnection Complete Event (v1.1) (BR/EDR & LE)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] StatusCode reason
+
+
+struct AuthenticationCompleteEvent:
+ -- Authentication Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct RemoteNameRequestCompleteEvent:
+ -- Remote Name Request Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+BdAddr.$size_in_bytes] BdAddr bd_addr
+ $next [+248] UInt:8[248] remote_name
+ -- UTF-8 encoded friendly name. If the name is less than 248 characters, it
+ -- is null terminated and the remaining bytes are not valid.
+
+
+struct EncryptionChangeEventV1:
+ -- Encryption Change Event (v1.1) (BR/EDR & LE)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] EncryptionStatus encryption_enabled
+
+
+struct ChangeConnectionLinkKeyCompleteEvent:
+ -- Change Connection Link Key Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+
+struct ReadRemoteSupportedFeaturesCompleteEvent:
+ -- Read Remote Supported Features Complete Event (v1.1) (BR/EDR)
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] StatusCode status
+ $next [+2] UInt connection_handle
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+8] LmpFeatures(0) lmp_features
+ -- Page 0 of the LMP features.
+
+
+struct LEMetaEvent:
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] UInt subevent_code
+ -- The event code for the LE subevent.
+
+
+struct LEConnectionCompleteSubevent:
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+
+ $next [+1] StatusCode status
+
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+1] ConnectionRole role
+
+ $next [+1] LEPeerAddressType peer_address_type
+
+ $next [+BdAddr.$size_in_bytes] BdAddr peer_address
+ -- Public Device Address or Random Device Address of the peer device.
+
+ $next [+2] UInt connection_interval
+ -- Time: N * 1.25 ms
+ -- Range: 7.5 ms to 4 s
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt peripheral_latency
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- Time: N * 10 ms
+ -- Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+ $next [+1] LEClockAccuracy central_clock_accuracy
+ -- Only valid for a peripheral. On a central, this parameter shall be set to 0x00.
+
+
+struct LEConnectionUpdateCompleteSubevent:
+ 0 [+LEMetaEvent.$size_in_bytes] LEMetaEvent le_meta_event
+
+ $next [+1] StatusCode status
+
+ $next [+2] UInt connection_handle
+ -- Only the lower 12-bits are meaningful.
+ [requires: 0x0000 <= this <= 0x0EFF]
+
+ $next [+2] UInt connection_interval
+ -- Time: N * 1.25 ms
+ -- Range: 7.5 ms to 4 s
+ [requires: 0x0006 <= this <= 0x0C80]
+
+ $next [+2] UInt peripheral_latency
+ [requires: 0x0000 <= this <= 0x01F3]
+
+ $next [+2] UInt supervision_timeout
+ -- Time: N * 10 ms
+ -- Range: 100 ms to 32 s
+ [requires: 0x000A <= this <= 0x0C80]
+
+# ============================ Test packets =============================
+
+
+struct TestCommandPacket:
+ -- Test HCI Command packet with single byte payload.
+ let hdr_size = CommandHeader.$size_in_bytes
+ 0 [+hdr_size] CommandHeader header
+ $next [+1] UInt payload
+
+
+struct TestEventPacket:
+ -- Test HCI Event packet with single byte payload.
+ let hdr_size = EventHeader.$size_in_bytes
+ 0 [+hdr_size] EventHeader header
+ $next [+1] UInt payload
diff --git a/pw_bluetooth/public/pw_bluetooth/host.h b/pw_bluetooth/public/pw_bluetooth/host.h
new file mode 100644
index 000000000..d5f3f3011
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/host.h
@@ -0,0 +1,194 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <optional>
+#include <string_view>
+
+#include "pw_bluetooth/controller.h"
+#include "pw_bluetooth/gatt/client.h"
+#include "pw_bluetooth/gatt/server.h"
+#include "pw_bluetooth/low_energy/bond_data.h"
+#include "pw_bluetooth/low_energy/central.h"
+#include "pw_bluetooth/low_energy/peripheral.h"
+#include "pw_bluetooth/low_energy/security_mode.h"
+#include "pw_bluetooth/pairing_delegate.h"
+#include "pw_bluetooth/peer.h"
+#include "pw_bluetooth/types.h"
+#include "pw_function/function.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace pw::bluetooth {
+
+/// Host is the entrypoint API for interacting with a Bluetooth host stack. Host
+/// is an abstract class that is implemented by a host stack implementation.
+class Host {
+ public:
+ /// Represents the persistent configuration of a single Host instance. This is
+ /// used for identity representation in advertisements & bonding secrets
+ /// recall.
+ struct PersistentData {
+ /// The local Identity Resolving Key used by a Host to generate Resolvable
+ /// Private Addresses when privacy is enabled. May be absent for hosts that
+ /// do not use LE privacy, or that only use Non-Resolvable Private
+ /// Addresses.
+ ///
+ /// NOTE: This key is distributed to LE peers during pairing procedures. The
+ /// client must take care to assign an IRK that consistent with the local
+ /// Host identity.
+ std::optional<Key> identity_resolving_key;
+
+ /// All bonds that use a public identity address must contain the same local
+ /// address.
+ span<const low_energy::BondData> bonds;
+ };
+
+ /// The security level required for this pairing. This corresponds to the
+ /// security levels defined in the Security Manager Protocol in Core spec
+ /// v5.3, Vol 3, Part H, Section 2.3.1
+ enum class PairingSecurityLevel : uint8_t {
+ /// Encrypted without person-in-the-middle protection (unauthenticated)
+ kEncrypted,
+ /// Encrypted with person-in-the-middle protection (authenticated), although
+ /// this level of security does not fully protect against passive
+ /// eavesdroppers
+ kAuthenticated,
+ /// Encrypted with person-in-the-middle protection (authenticated).
+ /// This level of security fully protects against eavesdroppers.
+ kLeSecureConnections,
+ };
+
+ /// Whether or not the device should form a bluetooth bond during the pairing
+ /// prodecure. As described in Core Spec v5.2, Vol 3, Part C, Sec 4.3
+ enum class BondableMode : uint8_t {
+ /// The device will form a bond during pairing with peers
+ kBondable,
+ /// The device will not form a bond during pairing with peers
+ kNonBondable,
+ };
+
+ /// Parameters that give a caller more fine-grained control over the pairing
+ /// process.
+ struct PairingOptions {
+ /// Determines the Security Manager security level to pair with.
+ PairingSecurityLevel security_level = PairingSecurityLevel::kAuthenticated;
+
+ /// Indicated whether the device should form a bond or not during pairing.
+ /// If not present, interpreted as bondable mode.
+ BondableMode bondable_mode = BondableMode::kBondable;
+ };
+
+ /// `Close()` should complete before `Host` is destroyed.
+ virtual ~Host() = default;
+
+ /// Initializes the host stack. Vendor specific controller initialization
+ /// (e.g. loading firmware) must be done before initializing `Host`.
+ ///
+ /// @param controller Pointer to a concrete `Controller` that the host stack
+ /// should use to communicate with the controller.
+ /// @param data Data to persist from a previous instance of `Host`.
+ /// @param on_initialization_complete Called when initialization is complete.
+ /// Other methods should not be called until initialization completes.
+ virtual void Initialize(
+ Controller* controller,
+ PersistentData data,
+ Function<void(Status)>&& on_initialization_complete) = 0;
+
+ /// Safely shuts down the host, ending all active Bluetooth procedures:
+ /// - All objects/pointers associated with this host are destroyed/invalidated
+ /// and all connections disconnected.
+ /// - All scanning and advertising procedures are stopped.
+ ///
+ /// The Host may send events or call callbacks as procedures get terminated.
+ /// @param callback Will be called once all procedures have terminated.
+ virtual void Close(Closure callback) = 0;
+
+ /// Returns a pointer to the Central API, which is used to scan and connect to
+ /// peers.
+ virtual low_energy::Central* Central() = 0;
+
+ /// Returns a pointer to the Peripheral API, which is used to advertise and
+ /// accept connections from peers.
+ virtual low_energy::Peripheral* Peripheral() = 0;
+
+ /// Returns a pointer to the GATT Server API, which is used to publish GATT
+ /// services.
+ virtual gatt::Server* GattServer() = 0;
+
+ /// Deletes a peer from the Bluetooth host. If the peer is connected, it will
+ /// be disconnected. `peer_id` will no longer refer to any peer.
+ ///
+ /// Returns `OK` after no peer exists that's identified by `peer_id` (even
+ /// if it didn't exist), `ABORTED` if the peer could not be disconnected or
+ /// deleted and still exists.
+ virtual Status ForgetPeer(PeerId peer_id) = 0;
+
+ /// Enable or disable the LE privacy feature. When enabled, the host will use
+ /// a private device address in all LE procedures. When disabled, the public
+ /// identity address will be used instead (which is the default).
+ virtual void EnablePrivacy(bool enabled) = 0;
+
+ /// Set the GAP LE Security Mode of the host. Only encrypted,
+ /// connection-based security modes are supported, i.e. Mode 1 and Secure
+ /// Connections Only mode. If the security mode is set to Secure Connections
+ /// Only, any existing encrypted connections which do not meet the security
+ /// requirements of Secure Connections Only mode will be disconnected.
+ virtual void SetSecurityMode(low_energy::SecurityMode security_mode) = 0;
+
+ /// Assigns the pairing delegate that will respond to authentication
+ /// challenges using the given I/O capabilities. Calling this method cancels
+ /// any on-going pairing procedure started using a previous delegate. Pairing
+ /// requests will be rejected if no PairingDelegate has been assigned.
+ virtual void SetPairingDelegate(InputCapability input,
+ OutputCapability output,
+ PairingDelegate* pairing_delegate) = 0;
+
+ /// NOTE: This is intended to satisfy test scenarios that require pairing
+ /// procedures to be initiated without relying on service access. In normal
+ /// operation, Bluetooth security is enforced during service access.
+ ///
+ /// Initiates pairing to the peer with the supplied `peer_id` and `options`.
+ /// Returns an error if no connected peer with `peer_id` is found or the
+ /// pairing procedure fails.
+ ///
+ /// If `options` specifies a higher security level than the current pairing,
+ /// this method attempts to raise the security level. Otherwise this method
+ /// has no effect and returns success.
+ ///
+ /// Returns the following errors via `callback`:
+ /// `NOT_FOUND` - The peer `peer_id` was not found.
+ /// `ABORTED` - The pairing procedure failed.
+ virtual void Pair(PeerId peer_id,
+ PairingOptions options,
+ Function<void(Status)>&& callback) = 0;
+
+ /// Configures a callback to be called when new bond data for a peer has been
+ /// created. This data should be persisted and used to initialize Host in the
+ /// future. New bond data may be received for an already bonded peer, in which
+ /// case the new data should overwrite the old data.
+ virtual void SetBondDataCallback(
+ Function<void(low_energy::BondData)>&& callback) = 0;
+
+ /// Looks up the `PeerId` corresponding to `address`. If `address` does not
+ /// correspond to a known peer, a new `PeerId` will be generated for the
+ /// address. If a `PeerId` cannot be generated, std::nullopt will be returned.
+ virtual std::optional<PeerId> PeerIdFromAddress(Address address) = 0;
+
+ /// Looks up the Address corresponding to `peer_id`. Returns null if `peer_id`
+ /// does not correspond to a known peer.
+ virtual std::optional<Address> DeviceAddressFromPeerId(PeerId peer_id) = 0;
+};
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/internal/hex.h b/pw_bluetooth/public/pw_bluetooth/internal/hex.h
new file mode 100644
index 000000000..3140898fe
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/internal/hex.h
@@ -0,0 +1,46 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+namespace pw::bluetooth::internal {
+
+// Parse a hexadecimal character to a 0-15 number. If the hex char is invalid
+// returns 0x100.
+constexpr uint16_t HexToNibble(char hex) {
+ if (hex >= '0' && hex <= '9') {
+ return hex - '0';
+ }
+ if (hex >= 'A' && hex <= 'F') {
+ return hex - 'A' + 10;
+ }
+ if (hex >= 'a' && hex <= 'f') {
+ return hex - 'a' + 10;
+ }
+ return 0x100;
+}
+
+// Convert a nibble (0 to 15 value) to its lowercase hexadecimal representation.
+constexpr char NibbleToHex(uint8_t value) {
+ if (value < 10) {
+ return '0' + value;
+ }
+ if (value < 16) {
+ return 'a' + value - 10;
+ }
+ return '?';
+}
+
+} // namespace pw::bluetooth::internal
diff --git a/pw_bluetooth/public/pw_bluetooth/internal/raii_ptr.h b/pw_bluetooth/public/pw_bluetooth/internal/raii_ptr.h
new file mode 100644
index 000000000..4d61f62d4
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/internal/raii_ptr.h
@@ -0,0 +1,67 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <memory>
+#include <type_traits>
+
+namespace pw::bluetooth::internal {
+
+// Helper deleter struct to call the OnDestroy template parameter method when
+// RaiiPtr<Api, OnDestroy> is destroyed.
+template <class Api, void (Api::*OnDestroy)()>
+struct RaiiPtrDeleter {
+ void operator()(Api* api) { (api->*OnDestroy)(); }
+};
+
+// Smart pointer class to expose an API with RAII semantics that calls a
+// specific method of the API implementation when it is destroyed, instead of
+// calling delete on the API object like the default std::unique_ptr deleter
+// behavior. This allows to be used like if it was a std::unique_ptr<API> but
+// leaving the memory management implementation details up to the backend
+// implementing the API.
+//
+// Example usage:
+// class SomeApi {
+// public:
+// virtual ~SomeApi() = default;
+//
+// virtual void MethodOne() = 0;
+// virtual void MethodTwo(int) = 0;
+//
+// private:
+// // Method used to release the resource in the backend.
+// virtual void DeleteResource() = 0;
+//
+// public:
+// using Ptr = RaiiPtr<SomeApi, &SomeApi::DeleteResource>;
+// };
+//
+// // The concrete backend implementation.
+// class BackendImplementation final : public SomeApi {
+// public:
+// void MethodOne() override { ... }
+// void MethodTwo(int) override { ... }
+// void DeleteResource() override { ... }
+// };
+//
+// // Example using a static global resource object. GetResource should check
+// // whether the global resource is in use.
+// BackendImplementation backend_impl;
+// SomeApi::Ptr GetResource() { ...; return &backend_impl; }
+//
+template <class Api, void (Api::*OnDestroy)()>
+using RaiiPtr = std::unique_ptr<Api, RaiiPtrDeleter<Api, OnDestroy>>;
+
+} // namespace pw::bluetooth::internal
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/advertising_data.h b/pw_bluetooth/public/pw_bluetooth/low_energy/advertising_data.h
new file mode 100644
index 000000000..8724832cd
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/advertising_data.h
@@ -0,0 +1,65 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+#include "pw_bluetooth/types.h"
+#include "pw_span/span.h"
+
+namespace pw::bluetooth::low_energy {
+
+// A service data field in an advertising data payload.
+struct ServiceData {
+ Uuid uuid;
+ span<const std::byte> data;
+};
+
+// A manufacturer data field in an advertising data payload.
+struct ManufacturerData {
+ uint16_t company_id = 0;
+ span<const std::byte> data;
+};
+
+// Represents advertising and scan response data that are transmitted by a LE
+// peripheral or broadcaster.
+struct AdvertisingData {
+ // Long or short name of the device.
+ std::string_view name;
+
+ // The appearance of the local device.
+ Appearance appearance = Appearance::kUnknown;
+
+ span<const Uuid> service_uuids;
+
+ span<const ServiceData> service_data;
+
+ span<const ManufacturerData> manufacturer_data;
+
+ // String representing a URI to be advertised, as defined in IETF STD 66:
+ // https://tools.ietf.org/html/std66. Each entry should be a UTF-8 string
+ // including the scheme. For more information, see:
+ // https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for allowed
+ // schemes;
+ // https://www.bluetooth.com/specifications/assigned-numbers/uri-scheme-name-string-mapping
+ // for code-points used by the system to compress the scheme to save space in
+ // the payload.
+ span<const std::string_view> uris;
+
+ // Indicates whether the current TX power level should be included in the
+ // advertising data.
+ bool include_tx_power_level = false;
+};
+
+} // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/bond_data.h b/pw_bluetooth/public/pw_bluetooth/low_energy/bond_data.h
new file mode 100644
index 000000000..5029aa9c0
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/bond_data.h
@@ -0,0 +1,78 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <array>
+#include <cstdint>
+#include <optional>
+
+#include "pw_bluetooth/low_energy/connection.h"
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+
+namespace pw::bluetooth::low_energy {
+
+// A 128-bit secret key.
+using Key = std::array<uint8_t, 16>;
+
+/// Represents a LE Long-Term peer key used for link encyrption. The `ediv` and
+/// `rand` fields are zero if distributed using LE Secure Connections pairing.
+struct LongTermKey {
+ Key key;
+ uint16_t ediv;
+ uint16_t rand;
+};
+
+struct BondData {
+ // The identifier that uniquely identifies this peer.
+ PeerId peer_id;
+
+ // The local Bluetooth identity address that this bond is associated with.
+ Address local_address;
+
+ std::optional<DeviceName> name;
+
+ // The identity address of the peer.
+ Address peer_address;
+
+ // The peer's preferred connection parameters, if known.
+ std::optional<RequestedConnectionParameters> connection_parameters;
+
+ // Identity Resolving RemoteKey used to generate and resolve random addresses.
+ Key identity_resolving_remote_key;
+
+ // Connection Signature Resolving RemoteKey used for data signing without
+ // encryption.
+ Key connection_signature_resolving_remote_key;
+
+ // LE long-term key used to encrypt a connection when the peer is in the LE
+ // Peripheral role.
+ //
+ // In legacy pairing (`peer_long_term_key.security.secure_connections` is
+ // false), this key corresponds to the key distributed by the peer. In Secure
+ // Connections pairing there is only one LTK and `peer_long_term_key` is the
+ // same as `local_long_term_key`.
+ LongTermKey peer_long_term_key;
+
+ // LE long-term key used to encrypt a connection when the peer is in the LE
+ // Central role.
+ //
+ // In legacy pairing (`local_long_term_key.security.secure_connections` is
+ // false), this key corresponds to the key distributed by the local device.
+ // In Secure Connections pairing there is only one LTK and
+ // `local_long_term_key` is the same as `peer_long_term_key`.
+ LongTermKey local_long_term_key;
+};
+
+} // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/central.h b/pw_bluetooth/public/pw_bluetooth/low_energy/central.h
new file mode 100644
index 000000000..391e691c8
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/central.h
@@ -0,0 +1,255 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <memory>
+#include <optional>
+
+#include "pw_bluetooth/internal/raii_ptr.h"
+#include "pw_bluetooth/low_energy/advertising_data.h"
+#include "pw_bluetooth/low_energy/connection.h"
+#include "pw_bluetooth/result.h"
+#include "pw_bluetooth/types.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+
+namespace pw::bluetooth::low_energy {
+
+/// Represents the LE central role. Used to scan and connect to peripherals.
+class Central {
+ public:
+ /// Represents an ongoing LE scan.
+ class ScanHandle {
+ public:
+ /// Possible errors that can cause a scan to stop prematurely.
+ enum class ScanError : uint8_t { kCanceled = 0 };
+
+ virtual ~ScanHandle() = 0;
+
+ /// Set a callback that will be called if the scan is stopped due to an
+ /// error in the BLE stack.
+ virtual void SetErrorCallback(Function<void(ScanError)>&& callback) = 0;
+
+ private:
+ /// Stop the current scan. This method is called by the ~ScanHandle::Ptr()
+ /// when it goes out of scope, the API client should never call this method.
+ virtual void StopScan() = 0;
+
+ public:
+ /// Movable ScanHandle smart pointer. The controller will continue scanning
+ /// until the ScanHandle::Ptr is destroyed.
+ using Ptr = internal::RaiiPtr<ScanHandle, &ScanHandle::StopScan>;
+ };
+
+ /// Filter parameters for use during a scan. A discovered peer only matches
+ /// the filter if it satisfies all of the present filter parameters.
+ struct ScanFilter {
+ /// Filter based on advertised service UUID.
+ std::optional<Uuid> service_uuid;
+
+ /// Filter based on service data containing the given UUID.
+ std::optional<Uuid> service_data_uuid;
+
+ /// Filter based on a manufacturer identifier present in the manufacturer
+ /// data. If this filter parameter is set, then the advertising payload must
+ /// contain manufacturer specific data with the provided company identifier
+ /// to satisfy this filter. Manufacturer identifiers can be found at
+ /// https://www.bluetooth.com/specifications/assigned-numbers/company-identifiers/
+ std::optional<uint16_t> manufacturer_id;
+
+ /// Filter based on whether or not a device is connectable. For example, a
+ /// client that is only interested in peripherals that it can connect to can
+ /// set this to true. Similarly a client can scan only for broadcasters by
+ /// setting this to false.
+ std::optional<bool> connectable;
+
+ /// Filter results based on a portion of the advertised device name.
+ /// Substring matches are allowed.
+ /// The name length must be at most pw::bluetooth::kMaxDeviceNameLength.
+ std::optional<std::string_view> name;
+
+ /// Filter results based on the path loss of the radio wave. A device that
+ /// matches this filter must satisfy the following:
+ /// 1. Radio transmission power level and received signal strength must be
+ /// available for the path loss calculation.
+ /// 2. The calculated path loss value must be less than, or equal to,
+ /// `max_path_loss`.
+ ///
+ /// @note This field is calculated using the RSSI and TX Power information
+ /// obtained from advertising and scan response data during a scan
+ /// procedure. It should NOT be confused with information for an active
+ /// connection obtained using the "Path Loss Reporting" feature.
+ std::optional<uint8_t> max_path_loss;
+ };
+
+ /// Parameters used during a scan.
+ struct ScanOptions {
+ /// List of filters for use during a scan. A peripheral that satisfies any
+ /// of these filters will be reported. At least 1 filter must be specified.
+ /// While not recommended, clients that require that all peripherals be
+ /// reported can specify an empty filter.
+ Vector<ScanFilter> filters;
+
+ /// The time interval between scans.
+ /// - Time = N * 0.625ms
+ /// - Range: 0x0004 (2.5ms) - 10.24ms (0x4000)
+ /// - Default: 10ms
+ uint16_t interval = 0x0010;
+
+ /// The duration of the scan. The window must be less than or equal to the
+ /// interval.
+ /// - Time = N * 0.625ms
+ /// - Range: 0x0004 (2.5ms) - 10.24ms (0x4000)
+ /// - Default: 10ms
+ uint16_t window = 0x0010;
+ };
+
+ /// Information obtained from advertising and scan response data broadcast by
+ /// a peer.
+ struct ScanData {
+ /// The radio transmit power level.
+ /// @note This field should NOT be confused with the "connection TX Power
+ /// Level" of a peer that is currently connected to the system obtained via
+ /// the "Transmit Power reporting" feature.
+ std::optional<uint8_t> tx_power;
+
+ /// The appearance of the device.
+ std::optional<Appearance> appearance;
+
+ Vector<Uuid> service_uuids;
+
+ Vector<ServiceData> service_data;
+
+ Vector<ManufacturerData> manufacturer_data;
+
+ /// String representing a URI to be advertised, as defined in IETF STD 66:
+ /// https://tools.ietf.org/html/std66. Each entry should be a UTF-8 string
+ /// including the scheme. For more information, see
+ /// https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml for
+ /// allowed schemes;
+ /// @note Bluetooth advertising compresses schemas over the air to save
+ /// space. See
+ /// https://www.bluetooth.com/specifications/assigned-numbers/uri-scheme-name-string-mapping.
+ Vector<std::string_view> uris;
+
+ /// The time when this scan data was received.
+ chrono::SystemClock::time_point timestamp;
+ };
+
+ struct ScanResult {
+ /// ScanResult is non-copyable because strings are only valid in the result
+ /// callback.
+ ScanResult(const ScanResult&) = delete;
+ ScanResult& operator=(const ScanResult&) = delete;
+
+ /// Uniquely identifies this peer on the current system.
+ PeerId peer_id;
+
+ /// Whether or not this peer is connectable. Non-connectable peers are
+ /// typically in the LE broadcaster role.
+ bool connectable;
+
+ /// The last observed signal strength of this peer. This field is only
+ /// present for a peer that is broadcasting. The RSSI can be stale if the
+ /// peer has not been advertising.
+ ///
+ /// @note This field should NOT be confused with the "connection RSSI" of a
+ /// peer that is currently connected to the system.
+ std::optional<uint8_t> rssi;
+
+ /// Information from advertising and scan response data broadcast by this
+ /// peer. This contains the advertising data last received from the peer.
+ ScanData scan_data;
+
+ /// The name of this peer. The name is often obtained during a scan
+ /// procedure and can get updated during the name discovery procedure
+ /// following a connection.
+ ///
+ /// This field is present if the name is known.
+ std::optional<std::string_view> name;
+
+ /// Timestamp of when the information in this `ScanResult` was last updated.
+ chrono::SystemClock::time_point last_updated;
+ };
+
+ /// Possible errors returned by `Connect`.
+ enum class ConnectError : uint8_t {
+ /// The peer ID is unknown.
+ kUnknownPeer,
+
+ /// The `ConnectionOptions` were invalid.
+ kInvalidOptions,
+
+ /// A connection to the peer already exists.
+ kAlreadyExists,
+
+ /// A connection could not be established.
+ kCouldNotBeEstablished,
+ };
+
+ enum class StartScanError : uint8_t {
+ /// A scan is already in progress. Only 1 scan may be active at a time.
+ kScanInProgress,
+ /// Some of the scan options are invalid.
+ kInvalidParameters,
+ /// An internal error occurred and a scan could not be started.
+ kInternal,
+ };
+
+ /// The Result type returned by Connect() via the passed callback.
+ using ConnectResult = Result<ConnectError, Connection::Ptr>;
+
+ virtual ~Central() = default;
+
+ /// Connect to the peer with the given identifier.
+ ///
+ /// The requested `Connection` represents the client's interest in the LE
+ /// connection to the peer. Destroying the `Connection` will disconnect from
+ /// the peer. Only 1 connection per peer may exist at a time.
+ ///
+ /// The `Connection` will be closed by the system if the connection to the
+ /// peer is lost or an error occurs, as indicated by `Connection.OnError`.
+ ///
+ /// @param peer_id Identifier of the peer to initiate a connection to.
+ /// @param options Options used to configure the connection.
+ /// @param callback Called when a connection is successfully established, or
+ /// an error occurs.
+ ///
+ /// Possible errors are documented in `ConnectError`.
+ virtual void Connect(PeerId peer_id,
+ ConnectionOptions options,
+ Function<void(ConnectResult)>&& callback) = 0;
+
+ /// Scans for nearby LE peripherals and broadcasters. The lifetime of the scan
+ /// session is tied to the returned `ScanHandle` object. Once a scan is
+ /// started, `scan_result_callback` will be called with scan results. Only 1
+ /// scan may be active at a time. It is OK to destroy the `ScanHandle::Ptr`
+ /// object in `scan_result_callback` to stop scanning (no more results will be
+ /// returned).
+ ///
+ /// @param options Options used to configure the scan session.
+ /// @param scan_result_callback If scanning starts successfully,called for LE
+ /// peers that satisfy the filters indicated in `options`. The initial calls
+ /// may report recently discovered peers. Subsequent calls will be made only
+ /// when peers have been scanned or updated since the last call.
+ /// @param scan_started_callback Called with a `ScanHandle` object if the scan
+ /// successfully starts, or a `ScanError` otherwise.
+ virtual void Scan(ScanOptions options,
+ Function<void(ScanResult)>&& scan_result_callback,
+ Function<void(Result<StartScanError, ScanHandle::Ptr>)>&&
+ scan_started_callback) = 0;
+};
+
+} // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h b/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h
new file mode 100644
index 000000000..d3109535d
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/connection.h
@@ -0,0 +1,173 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_bluetooth/gatt/client.h"
+#include "pw_bluetooth/internal/raii_ptr.h"
+#include "pw_bluetooth/types.h"
+
+namespace pw::bluetooth::low_energy {
+
+/// Actual connection parameters returned by the controller.
+struct ConnectionParameters {
+ /// The connection interval indicates the frequency of link layer connection
+ /// events over which data channel PDUs can be transmitted. See Core Spec
+ /// v5.3, Vol 6, Part B, Section 4.5.1 for more information on the link layer
+ /// connection events.
+ /// - Range: 0x0006 to 0x0C80
+ /// - Time: N * 1.25 ms
+ /// - Time Range: 7.5 ms to 4 s.
+ uint16_t interval;
+
+ /// The maximum allowed peripheral connection latency in number of connection
+ /// events. See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ /// - Range: 0x0000 to 0x01F3
+ uint16_t latency;
+
+ /// This defines the maximum time between two received data packet PDUs
+ /// before the connection is considered lost. See Core Spec v5.3, Vol 6, Part
+ /// B, Section 4.5.2.
+ /// - Range: 0x000A to 0x0C80
+ /// - Time: N * 10 ms
+ /// - Time Range: 100 ms to 32 s
+ uint16_t supervision_timeout;
+};
+
+/// Connection parameters that either the local device or a peer device are
+/// requesting.
+struct RequestedConnectionParameters {
+ /// Minimum value for the connection interval. This shall be less than or
+ /// equal to `max_interval`. The connection interval indicates the frequency
+ /// of link layer connection events over which data channel PDUs can be
+ /// transmitted. See Core Spec v5.3, Vol 6, Part B, Section 4.5.1 for more
+ /// information on the link layer connection events.
+ /// - Range: 0x0006 to 0x0C80
+ /// - Time: N * 1.25 ms
+ /// - Time Range: 7.5 ms to 4 s.
+ uint16_t min_interval;
+
+ /// Maximum value for the connection interval. This shall be greater than or
+ /// equal to `min_interval`. The connection interval indicates the frequency
+ /// of link layer connection events over which data channel PDUs can be
+ /// transmitted. See Core Spec v5.3, Vol 6, Part B, Section 4.5.1 for more
+ /// information on the link layer connection events.
+ /// - Range: 0x0006 to 0x0C80
+ /// - Time: N * 1.25 ms
+ /// - Time Range: 7.5 ms to 4 s.
+ uint16_t max_interval;
+
+ /// Maximum peripheral latency for the connection in number of connection
+ /// events. See Core Spec v5.3, Vol 6, Part B, Section 4.5.2.
+ /// - Range: 0x0000 to 0x01F3
+ uint16_t max_latency;
+
+ /// This defines the maximum time between two received data packet PDUs
+ /// before the connection is considered lost. See Core Spec v5.3, Vol 6, Part
+ /// B, Section 4.5.2.
+ /// - Range: 0x000A to 0x0C80
+ /// - Time: N * 10 ms
+ /// - Time Range: 100 ms to 32 s
+ uint16_t supervision_timeout;
+};
+
+/// Represents parameters that are set on a per-connection basis.
+struct ConnectionOptions {
+ /// When true, the connection operates in bondable mode. This means pairing
+ /// will form a bond, or persist across disconnections, if the peer is also
+ /// in bondable mode. When false, the connection operates in non-bondable
+ /// mode, which means the local device only allows pairing that does not form
+ /// a bond.
+ bool bondable_mode = true;
+
+ /// When present, service discovery performed following the connection is
+ /// restricted to primary services that match this field. Otherwise, by
+ /// default all available services are discovered.
+ std::optional<Uuid> service_filter;
+
+ /// When present, specifies the initial connection parameters. Otherwise, the
+ /// connection parameters will be selected by the implementation.
+ std::optional<RequestedConnectionParameters> parameters;
+};
+
+/// Class that represents a connection to a peer. This can be used to interact
+/// with GATT services and establish LE L2CAP channels.
+///
+/// This lifetime of this object is tied to that of the LE connection it
+/// represents. Destroying the object results in a disconnection.
+class Connection {
+ public:
+ /// Possible errors when updating the connection parameters.
+ enum class ConnectionParameterUpdateError : uint8_t {
+ kFailure,
+ kInvalidParameters,
+ kRejected,
+ };
+
+ /// Possible reasons a connection was disconnected.
+ enum class DisconnectReason : uint8_t {
+ kFailure,
+ kRemoteUserTerminatedConnection,
+ /// This usually indicates that the link supervision timeout expired.
+ kConnectionTimeout,
+ };
+
+ /// If a disconnection has not occurred, destroying this object will result in
+ /// disconnection.
+ virtual ~Connection() = default;
+
+ /// Sets a callback that will be called when the peer disconnects or there is
+ /// a connection error that causes a disconnection. This should be configured
+ /// by the client immediately after establishing the connection. `callback`
+ /// will not be called for disconnections initiated by the client (e.g. by
+ /// destroying `Connection`). It is OK to destroy this object from within
+ /// `callback`.
+ virtual void SetDisconnectCallback(
+ Function<void(DisconnectReason)>&& callback) = 0;
+
+ /// Returns a GATT client to the connected peer that is valid for the lifetime
+ /// of this connection. The client is valid for the lifetime of this
+ /// connection.
+ virtual gatt::Client* GattClient() = 0;
+
+ /// Returns the current ATT Maximum Transmission Unit. By subtracting ATT
+ /// headers from the MTU, the maximum payload size of messages can be
+ /// calculated.
+ virtual uint16_t AttMtu() = 0;
+
+ /// Sets a callback that will be called with the new ATT MTU whenever it is
+ /// updated.
+ virtual void SetAttMtuChangeCallback(Function<void(uint16_t)> callback) = 0;
+
+ /// Returns the current connection parameters.
+ virtual ConnectionParameters Parameters() = 0;
+
+ /// Requests an update to the connection parameters. `callback` will be called
+ /// with the result of the request.
+ virtual void RequestConnectionParameterUpdate(
+ RequestedConnectionParameters parameters,
+ Function<void(Result<ConnectionParameterUpdateError>)>&& callback) = 0;
+
+ private:
+ /// Request to disconnect this connection. This method is called by the
+ /// ~Connection::Ptr() when it goes out of scope, the API client should never
+ /// call this method.
+ virtual void Disconnect() = 0;
+
+ public:
+ /// Movable Connection smart pointer. When Connection::Ptr is destroyed the
+ /// Connection will disconnect automatically.
+ using Ptr = internal::RaiiPtr<Connection, &Connection::Disconnect>;
+};
+
+} // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h b/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h
new file mode 100644
index 000000000..34c88fc1e
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/peripheral.h
@@ -0,0 +1,160 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+#include <memory>
+
+#include "pw_bluetooth/internal/raii_ptr.h"
+#include "pw_bluetooth/low_energy/advertising_data.h"
+#include "pw_bluetooth/low_energy/connection.h"
+#include "pw_bluetooth/result.h"
+#include "pw_bluetooth/types.h"
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+
+namespace pw::bluetooth::low_energy {
+
+/// `AdvertisedPeripheral` instances are valid for the duration of advertising.
+class AdvertisedPeripheral {
+ public:
+ virtual ~AdvertisedPeripheral() = default;
+
+ /// Set a callback that will be called when an error occurs and advertising
+ /// has been stopped (invalidating this object). It is OK to destroy the
+ /// `AdvertisedPeripheral::Ptr` object from within `callback`.
+ virtual void SetErrorCallback(Closure callback) = 0;
+
+ /// For connectable advertisements, this callback will be called when an LE
+ /// central connects to the advertisement. It is recommended to set this
+ /// callback immediately after advertising starts to avoid dropping
+ /// connections.
+ ///
+ /// The returned Connection can be used to interact with the peer. It also
+ /// represents a peripheral's ownership over the connection: the client can
+ /// drop the object to request a disconnection. Similarly, the Connection
+ /// error handler is called by the system to indicate that the connection to
+ /// the peer has been lost. While connections are exclusive among peripherals,
+ /// they may be shared with centrals.
+ ///
+ /// If advertising is not stopped, this callback may be called with multiple
+ /// connections over the lifetime of an advertisement. It is OK to destroy
+ /// the `AdvertisedPeripheral::Ptr` object from within `callback` in order to
+ /// stop advertising.
+ virtual void SetConnectionCallback(
+ Function<void(Connection::Ptr)>&& callback) = 0;
+
+ private:
+ /// Stop advertising. This method is called by the
+ /// ~AdvertisedPeripheral::Ptr() when it goes out of scope, the API client
+ /// should never call this method.
+ virtual void StopAdvertising() = 0;
+
+ public:
+ /// Movable AdvertisedPeripheral smart pointer. The peripheral will continue
+ /// advertising until the returned AdvertisedPeripheral::Ptr is destroyed.
+ using Ptr = internal::RaiiPtr<AdvertisedPeripheral,
+ &AdvertisedPeripheral::StopAdvertising>;
+};
+
+/// Represents the LE Peripheral role, which advertises and is connected to.
+class Peripheral {
+ public:
+ /// The range of the time interval between advertisements. Shorter intervals
+ /// result in faster discovery at the cost of higher power consumption. The
+ /// exact interval used is determined by the Bluetooth controller.
+ /// - Time = N * 0.625ms.
+ /// - Time range: 0x0020 (20ms) - 0x4000 (10.24s)
+ struct AdvertisingIntervalRange {
+ /// Default: 1.28s
+ uint16_t min = 0x0800;
+ /// Default: 1.28s
+ uint16_t max = 0x0800;
+ };
+
+ /// Represents the parameters for configuring advertisements.
+ struct AdvertisingParameters {
+ /// The fields that will be encoded in the data section of advertising
+ /// packets.
+ AdvertisingData data;
+
+ /// The fields that are to be sent in a scan response packet. Clients may
+ /// use this to send additional data that does not fit inside an advertising
+ /// packet on platforms that do not support the advertising data length
+ /// extensions.
+ ///
+ /// If present advertisements will be configured to be scannable.
+ std::optional<AdvertisingData> scan_response;
+
+ /// See `AdvertisingIntervalRange` documentation.
+ AdvertisingIntervalRange interval_range;
+
+ /// If present, the controller will broadcast connectable advertisements
+ /// which allow peers to initiate connections to the Peripheral. The fields
+ /// of `ConnectionOptions` will configure any connections set up from
+ /// advertising.
+ std::optional<ConnectionOptions> connection_options;
+ };
+
+ /// Errors returned by `Advertise`.
+ enum class AdvertiseError {
+ /// The operation or parameters requested are not supported on the current
+ /// hardware.
+ kNotSupported = 1,
+
+ /// The provided advertising data exceeds the maximum allowed length when
+ /// encoded.
+ kAdvertisingDataTooLong = 2,
+
+ /// The provided scan response data exceeds the maximum allowed length when
+ /// encoded.
+ kScanResponseDataTooLong = 3,
+
+ /// The requested parameters are invalid.
+ kInvalidParameters = 4,
+
+ /// This may be called if the maximum number of simultaneous advertisements
+ /// has already been reached.
+ kNotEnoughAdvertisingSlots = 5,
+
+ /// Advertising could not be initiated due to a hardware or system error.
+ kFailed = 6,
+ };
+
+ /// Callback for `Advertise()` method.
+ using AdvertiseCallback =
+ Function<void(Result<AdvertiseError, AdvertisedPeripheral::Ptr>)>;
+
+ virtual ~Peripheral() = default;
+
+ /// Start advertising continuously as a LE peripheral. If advertising cannot
+ /// be initiated then `result_callback` will be called with an error. Once
+ /// started, advertising can be stopped by destroying the returned
+ /// `AdvertisedPeripheral::Ptr`.
+ ///
+ /// If the system supports multiple advertising, this may be called as many
+ /// times as there are advertising slots. To reconfigure an advertisement,
+ /// first close the original advertisement and then initiate a new
+ /// advertisement.
+ ///
+ /// @param parameters Parameters used while configuring the advertising
+ /// instance.
+ /// @param result_callback Called once advertising has started or failed. On
+ /// success, called with an `AdvertisedPeripheral` that models the lifetime of
+ /// the advertisement. Destroying it will stop advertising.
+ virtual void Advertise(const AdvertisingParameters& parameters,
+ AdvertiseCallback&& result_callback) = 0;
+};
+
+} // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/low_energy/security_mode.h b/pw_bluetooth/public/pw_bluetooth/low_energy/security_mode.h
new file mode 100644
index 000000000..9fb2250df
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/low_energy/security_mode.h
@@ -0,0 +1,41 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+namespace pw::bluetooth::low_energy {
+
+// The LE Security Mode of a BLE device determines the possible security
+// properties of the device. The security mode does not make specific guarantees
+// about the current security properties of a device's connections; it sets
+// restrictions on the allowable security properties. See Core Spec v5.2 Vol. 3,
+// Part C 10.2 for more details.
+enum class SecurityMode : uint8_t {
+ // In LE Security Mode 1, communication is secured by encryption, and
+ // BLE-based services may specify varying requirements for authentication, key
+ // size, or Secure Connections protection on the encryption keys.
+ kMode1 = 0,
+
+ // In Secure Connections Only mode, all secure communication must use 128 bit,
+ // authenticated, and LE Secure Connections-generated encryption keys. If
+ // these encryption key properties cannot be satisfied by a device due to
+ // system constraints, any connection involving such a device will not be able
+ // to secure the link at all. This mode does not prevent unencrypted
+ // communication; it merely enforces stricter policies on all encrypted
+ // communication.
+ kSecureConnectionsOnly = 1
+};
+
+} // namespace pw::bluetooth::low_energy
diff --git a/pw_bluetooth/public/pw_bluetooth/pairing_delegate.h b/pw_bluetooth/public/pw_bluetooth/pairing_delegate.h
new file mode 100644
index 000000000..c4c5c537f
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/pairing_delegate.h
@@ -0,0 +1,119 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_bluetooth/peer.h"
+#include "pw_function/function.h"
+
+namespace pw::bluetooth {
+
+// Input Capabilities for pairing exchanges.
+// See Core Spec v5.3 Volume 3, Part C, Section 5.2.2.4, Table 5.3.
+enum class InputCapability : uint8_t { kNone, kConfirmation, kKeyboard };
+
+// Output Capabilities for pairing exchanges.
+// See Core Spec v5.3 Volume 3, Part C, Section 5.2.2.4, Table 5.4.
+enum class OutputCapability : uint8_t { kNone, kDisplay };
+
+// Pairing event handler implemented by the API client.
+class PairingDelegate {
+ public:
+ // Different types required by the Security Manager for pairing methods.
+ // Bluetooth SIG has different requirements for different device capabilities.
+ enum class PairingMethod : uint8_t {
+ // The user is asked to accept or reject pairing.
+ kConsent,
+
+ // The user is shown a 6-digit numerical passkey which they must enter on
+ // the
+ // peer device.
+ kPasskeyDisplay,
+
+ // The user is shown a 6-digit numerical passkey which will also shown on
+ // the
+ // peer device. The user must compare the passkeys and accept the pairing if
+ // the passkeys match.
+ kPasskeyComparison,
+
+ // The user is asked to enter a 6-digit passkey.
+ kPasskeyEntry
+ };
+
+ enum class PairingKeypress : uint8_t {
+ // The user has entered a single digit.
+ kDigitEntered,
+
+ // The user has erased a single digit.
+ kDigitErased,
+
+ // The user has cleared the entire passkey.
+ kPasskeyCleared,
+
+ // The user has finished entering the passkey.
+ kPasskeyEntered
+ };
+
+ // Callback for responding to pairing requests.
+ using ResponseCallback =
+ pw::Function<void(bool accept, uint32_t entered_passkey)>;
+
+ // Callback for signaling local keypresses to a peer.
+ using KeypressCallback =
+ pw::Function<void(PeerId peer_id, PairingKeypress keypress)>;
+
+ virtual ~PairingDelegate() = default;
+
+ // Called to initiate a pairing request. The delegate must respond with "true"
+ // or "false" in the callback to either accept or reject the pairing request.
+ // If the pairing method requires a passkey this is returned as well. It is OK
+ // to call `callback` synchronously in this method.
+ //
+ // Any response from this method will be ignored if the `OnPairingComplete`
+ // event has already been sent for `peer`.
+ //
+ // The `displayed_passkey` parameter should be displayed to the user if
+ // `method` equals `PairingMethod::kPasskeyDisplay` or
+ // `PairingMethod.kPasskeyComparison`. Otherwise, this parameter has no
+ // meaning and should be ignored.
+ //
+ // The `entered_passkey` parameter of `callback` only has meaning if `method`
+ // equals `PairingMethod.kPasskeyEntry`. It will be ignored otherwise.
+ virtual void OnPairingRequest(Peer peer,
+ PairingMethod method,
+ uint32_t displayed_passkey,
+ ResponseCallback&& callback) = 0;
+
+ // Called if the pairing procedure for the device with the given ID is
+ // completed. This can be due to successful completion or an error (e.g. due
+ // to cancellation by the peer, a timeout, or disconnection) which is
+ // indicated by `success`.
+ virtual void OnPairingComplete(PeerId peer_id, bool success) = 0;
+
+ // Called to notify keypresses from the peer device during pairing using
+ // `PairingMethod::kPasskeyDisplay`.
+ //
+ // This event is used to provide key press events to the delegate for a
+ // responsive user experience as the user types the passkey on the peer
+ // device. This event will be called once for each keypress.
+ virtual void OnRemoteKeypress(PeerId peer_id, PairingKeypress keypress) = 0;
+
+ // Sets a callback that the client may call on local passkey keypresses during
+ // a `PairingMethod::kPasskeyEntry` pairing request. Signaled keypresses may
+ // be used in the UI of the peer. This should be set immediately by the
+ // Bluetooth stack when the delegate is configured by the client unless
+ // sending local keypresses is not supported.
+ virtual void SetLocalKeypressCallback(KeypressCallback&& callback) = 0;
+};
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/peer.h b/pw_bluetooth/public/pw_bluetooth/peer.h
new file mode 100644
index 000000000..39990dc8c
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/peer.h
@@ -0,0 +1,41 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <optional>
+
+#include "pw_bluetooth/types.h"
+#include "pw_containers/vector.h"
+
+namespace pw::bluetooth {
+
+// Information about a remote Bluetooth device.
+struct Peer {
+ // Uniquely identifies this peer on the current system.
+ PeerId peer_id;
+
+ // Bluetooth device address that identifies this peer.
+ // NOTE: Clients should use the `peer_id` field to keep track of peers
+ // instead of their address.
+ Address address;
+
+ // The name of the peer, if known.
+ std::optional<DeviceName> name;
+
+ // The LE appearance property. Present if the appearance information was
+ // obtained over advertising and/or GATT.
+ std::optional<Appearance> appearance;
+};
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/result.h b/pw_bluetooth/public/pw_bluetooth/result.h
new file mode 100644
index 000000000..ac1349b5c
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/result.h
@@ -0,0 +1,98 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <optional>
+#include <utility>
+
+#include "pw_assert/assert.h"
+
+namespace pw::bluetooth {
+
+// A Result represents the result of an operation which can fail. If it
+// represents an error, it contains an error value. If it represents success, it
+// contains zero or one success value.
+template <typename E, typename... Ts>
+class Result;
+
+// Result specialization for returning OK or an error (E).
+template <typename E>
+class [[nodiscard]] Result<E> {
+ public:
+ constexpr Result() = default;
+ constexpr Result(E error) : error_(error) {}
+
+ constexpr Result(const Result&) = default;
+ constexpr Result& operator=(const Result&) = default;
+
+ constexpr Result(Result&&) = default;
+ constexpr Result& operator=(Result&&) = default;
+
+ [[nodiscard]] constexpr E error() const {
+ PW_ASSERT(error_.has_value());
+ return error_.value();
+ }
+ [[nodiscard]] constexpr bool ok() const { return !error_.has_value(); }
+
+ private:
+ std::optional<E> error_;
+};
+
+// Result specialization for returning some data (T) or an error (E).
+template <typename E, typename T>
+class [[nodiscard]] Result<E, T> {
+ public:
+ constexpr Result(T&& value) : value_(std::move(value)) {}
+ constexpr Result(const T& value) : value_(value) {}
+
+ template <typename... Args>
+ constexpr Result(std::in_place_t, Args&&... args)
+ : value_(std::forward<Args>(args)...) {}
+
+ constexpr Result(E error) : error_(error) {}
+
+ constexpr Result(const Result&) = default;
+ constexpr Result& operator=(const Result&) = default;
+
+ constexpr Result(Result&&) = default;
+ constexpr Result& operator=(Result&&) = default;
+
+ [[nodiscard]] constexpr E error() const {
+ PW_ASSERT(!value_.has_value());
+ return error_;
+ }
+ [[nodiscard]] constexpr bool ok() const { return value_.has_value(); }
+
+ constexpr T& value() & {
+ PW_ASSERT(value_.has_value());
+ return value_.value();
+ }
+
+ constexpr const T& value() const& {
+ PW_ASSERT(value_.has_value());
+ return value_.value();
+ }
+
+ constexpr T&& value() && {
+ PW_ASSERT(value_.has_value());
+ return std::move(value_.value());
+ }
+
+ private:
+ std::optional<T> value_;
+ // error_ is only initialized if value_ is empty.
+ E error_ = {};
+};
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/types.h b/pw_bluetooth/public/pw_bluetooth/types.h
new file mode 100644
index 000000000..d969b6280
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/types.h
@@ -0,0 +1,101 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <array>
+#include <cstdint>
+#include <string_view>
+
+#include "pw_bluetooth/address.h"
+#include "pw_bluetooth/uuid.h"
+
+namespace pw::bluetooth {
+
+// 64-bit unique value used by the system to identify peer devices.
+using PeerId = uint64_t;
+
+using DeviceName = std::string_view;
+
+// A 128-bit secret key.
+using Key = std::array<uint8_t, 16>;
+
+/// Refers to the role of a Bluetooth device in a physical channel piconet.
+enum class ConnectionRole : uint8_t {
+ // The connection initiating device.
+ kCentral,
+ // The advertising device.
+ kPeripheral
+};
+
+/// Possible values for the LE Appearance property which describes the external
+/// appearance of a peer at a high level.
+/// (See the Bluetooth assigned-numbers document:
+/// https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.characteristic.gap.appearance.xml)
+enum class Appearance : uint16_t {
+ kUnknown = 0,
+ kPhone = 64,
+ kComputer = 128,
+ kWatch = 192,
+ kWatchSports = 193,
+ kClock = 256,
+ kDisplay = 320,
+ kRemoteControl = 384,
+ kEyeGlasses = 448,
+ kTag = 512,
+ kKeyring = 576,
+ kMediaPlayer = 640,
+ kBarcodeScanner = 704,
+ kThermometer = 768,
+ kThermometerEar = 769,
+ kHeartRateSensor = 832,
+ kHeartRateSensorBelt = 833,
+ kBloodPressure = 896,
+ kBloodPressureArm = 897,
+ kBloodPressureWrist = 898,
+ kHid = 960,
+ kHidKeyboard = 961,
+ kHidMouse = 962,
+ kHidJoystick = 963,
+ kHidGamepad = 964,
+ kHidDigitizerTablet = 965,
+ kHidCardReader = 966,
+ kHidDigitalPen = 967,
+ kHidBarcodeScanner = 968,
+ kGlucoseMeter = 1024,
+ kRunningWalkingSensor = 1088,
+ kRunningWalkingSensorInShoe = 1089,
+ kRunningWalkingSensorOnShoe = 1090,
+ kRunningWalkingSensorOnHip = 1091,
+ kCycling = 1152,
+ kCyclingComputer = 1153,
+ kCyclingSpeedSensor = 1154,
+ kCyclingCadenceSensor = 1155,
+ kCyclingPowerSensor = 1156,
+ kCyclingSpeedAndCadenceSensor = 1157,
+ kPulseOximeter = 3136,
+ kPulseOximeterFingertip = 3137,
+ kPulseOximeterWrist = 3138,
+ kWeightScale = 3200,
+ kPersonalMobility = 3264,
+ kPersonalMobilityWheelchair = 3265,
+ kPersonalMobilityScooter = 3266,
+ kGlucoseMonitor = 3328,
+ kSportsActivity = 5184,
+ kSportsActivityLocationDisplay = 5185,
+ kSportsActivityLocationAndNavDisplay = 5186,
+ kSportsActivityLocationPod = 5187,
+ kSportsActivityLocationAndNavPod = 5188,
+};
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/uuid.h b/pw_bluetooth/public/pw_bluetooth/uuid.h
new file mode 100644
index 000000000..b102dbd43
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/uuid.h
@@ -0,0 +1,200 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <array>
+#include <climits>
+#include <cstdint>
+
+#include "pw_assert/assert.h"
+#include "pw_bluetooth/internal/hex.h"
+#include "pw_span/span.h"
+#include "pw_string/string.h"
+
+namespace pw::bluetooth {
+
+// A 128-bit Universally Unique Identifier (UUID).
+// See Core Spec v5.3 Volume 3, Part B, Section 2.5.1.
+//
+// Bluetooth defines 16-bit, 32-bit and 128-bit UUID representations for a
+// 128-bit UUID, all of which are used in the protocol. 16-bit UUIDs values
+// define only the "YYYY" portion in the following UUID pattern (with XXXX set
+// as 0), while 32-bit UUID value define the "XXXXYYYY" portion. When using
+// these short UUIDs, the remaining bits are set by the Bluetooth_Base_UUID as
+// follows:
+// XXXXYYYY-0000-1000-8000-00805f9b34fb
+//
+// This class always stores UUID in their 128-bit representation in little
+// endian format.
+class Uuid {
+ public:
+ // String size of a hexadecimal representation of a UUID, not including the
+ // null terminator.
+ static constexpr size_t kHexStringSize = 36;
+
+ // Create a UUID from a span of 128-bit data. UUIDs are represented as
+ // little endian bytes.
+ explicit constexpr Uuid(const span<const uint8_t, 16> uuid_span) : uuid_() {
+ for (size_t i = 0; i < sizeof(uuid_); i++) {
+ uuid_[i] = uuid_span[i];
+ }
+ }
+
+ // Create a UUID from its string representation. This is parsed manually here
+ // so it can be parsed at compile time with constexpr. A valid UUID is a hex
+ // string with hyphen separators at the 9th, 14th, 19th, and 24th
+ // positions, for example:
+ // "0000180a-0000-1000-8000-00805f9b34fb"
+ // The `str` parameter is byte longer than kHexStringSize so it can be
+ // initialized with literal strings including the null terminator, as in
+ // BluetoothBase().
+ constexpr Uuid(const char (&str)[kHexStringSize + 1]) : uuid_() {
+ size_t out_hex_index = 2 * sizeof(uuid_); // UUID is stored little-endian.
+ for (size_t i = 0; i < kHexStringSize; i++) {
+ // Indices at which we expect to find a hyphen ('-') in a UUID string.
+ if (i == 8 || i == 13 || i == 18 || i == 23) {
+ PW_ASSERT(str[i] == '-');
+ continue;
+ }
+ PW_ASSERT(str[i] != 0);
+ out_hex_index--;
+ uint16_t value = internal::HexToNibble(str[i]);
+ PW_ASSERT(value <= 0xf);
+ if (out_hex_index % 2 == 0) {
+ uuid_[out_hex_index / 2] |= value;
+ } else {
+ uuid_[out_hex_index / 2] = value << 4;
+ }
+ }
+ }
+
+ // The Bluetooth_Base_UUID defined by the specification. This is the base for
+ // all 16-bit and 32-bit short UUIDs.
+ static constexpr const Uuid& BluetoothBase();
+
+ constexpr Uuid() : uuid_() {}
+
+ // Create a UUID combining 96-bits from a base UUID with a 16-bit or 32-bit
+ // value. 16-bit values will be extended to 32-bit ones, meaning the that the
+ // 16 most significant bits will be set to 0 regardless of the value on the
+ // base UUID.
+ constexpr Uuid(uint32_t short_uuid, const Uuid& base_uuid)
+ : uuid_(base_uuid.uuid_) {
+ uuid_[kBaseOffset] = short_uuid & 0xff;
+ uuid_[kBaseOffset + 1] = (short_uuid >> CHAR_BIT) & 0xff;
+ uuid_[kBaseOffset + 2] = (short_uuid >> CHAR_BIT * 2) & 0xff;
+ uuid_[kBaseOffset + 3] = (short_uuid >> CHAR_BIT * 3) & 0xff;
+ }
+
+ // Create a short UUID (32-bit or 16-bit) using the standard Bluetooth base
+ // UUID.
+ explicit constexpr Uuid(uint32_t short_uuid)
+ : Uuid(short_uuid, BluetoothBase()) {}
+
+ constexpr Uuid(const Uuid&) = default;
+ constexpr Uuid& operator=(const Uuid&) = default;
+
+ // Return a 2-byte span containing the 16-bit little endian representation of
+ // the UUID. This is useful when Same112BitBase(BluetoothBase()) is true.
+ constexpr span<const uint8_t, 2> As16BitSpan() const {
+ return span<const uint8_t, 2>{uuid_.data() + kBaseOffset, 2u};
+ }
+
+ // Return a 4-byte span containing the 32-bit little endian representation of
+ // the UUID. This is useful when Same96BitBase(BluetoothBase()) is true.
+ constexpr span<const uint8_t, 4> As32BitSpan() const {
+ return span<const uint8_t, 4>{uuid_.data() + kBaseOffset, 4u};
+ }
+
+ // Return the 128-bit (16-byte) little endian representation of the UUID.
+ constexpr span<const uint8_t, 16> As128BitSpan() const {
+ return span<const uint8_t, 16>{uuid_.data(), 16u};
+ }
+
+ // Return whether the UUID shares the same 112-bit base with another UUID.
+ // Sharing the same 112-bit base with BluetoothBase() means that this UUID
+ // can be resented as a 16-bit UUID.
+ constexpr bool Same112BitBase(const Uuid& other) const {
+ return Same96BitBase(other) && uuid_[14] == other.uuid_[14] &&
+ uuid_[15] == other.uuid_[15];
+ }
+
+ // Return whether the UUID shares the same 96-bit base with another UUID.
+ // Sharing the same 96-bit base with BluetoothBase() means that this UUID
+ // can be resented as a 32-bit UUID.
+ constexpr bool Same96BitBase(const Uuid& other) const {
+ for (size_t i = 0; i < 12; i++) {
+ if (uuid_[i] != other.uuid_[i])
+ return false;
+ }
+ return true;
+ }
+
+ // Return whether the UUID is a 16-bit UUID represented as 128-bit using the
+ // BluetoothBase() as the base.
+ constexpr bool Is16BitUuid() const { return Same112BitBase(BluetoothBase()); }
+
+ // Return whether the UUID is a 32-bit UUID represented as 128-bit using the
+ // BluetoothBase() as the base.
+ constexpr bool Is32BitUuid() const { return Same96BitBase(BluetoothBase()); }
+
+ // Return an inline pw_string representation of the UUID in hexadecimal.
+ constexpr InlineString<kHexStringSize> ToString() const {
+ InlineString<kHexStringSize> ret;
+ for (size_t i = uuid_.size(); i-- != 0;) {
+ ret += internal::NibbleToHex(uuid_[i] >> 4);
+ ret += internal::NibbleToHex(uuid_[i] & 0xf);
+ if ((i == 12) || (i == 10) || (i == 8) || (i == 6)) {
+ ret += '-';
+ }
+ }
+ return ret;
+ }
+
+ private:
+ // Offset at which the short 16-bit and 32-bit UUID little-endian data starts
+ // in the uuid_ array.
+ static constexpr size_t kBaseOffset = 12;
+
+ std::array<uint8_t, 16> uuid_;
+};
+
+namespace internal {
+// When BluetoothBase() is used in constexpr expressions it would normally be
+// evaluated to a final different Uuid, such as when used in Uuid(uint32_t),
+// however if a reference to the return value of BluetoothBase() is needed this
+// variable would be the only global symbol that provides it even if it is used
+// from multiple translation units.
+constexpr Uuid kBluetoothBaseUuid{"00000000-0000-1000-8000-00805F9B34FB"};
+} // namespace internal
+
+inline constexpr const Uuid& Uuid::BluetoothBase() {
+ return internal::kBluetoothBaseUuid;
+}
+
+// Uuid comparators:
+constexpr bool operator==(const Uuid& a, const Uuid& b) {
+ const auto a_span = a.As128BitSpan();
+ const auto b_span = b.As128BitSpan();
+ for (size_t i = 0; i < a_span.size(); i++) {
+ if (a_span[i] != b_span[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+constexpr bool operator!=(const Uuid& a, const Uuid& b) { return !(a == b); }
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/public/pw_bluetooth/vendor.emb b/pw_bluetooth/public/pw_bluetooth/vendor.emb
new file mode 100644
index 000000000..9ee77343a
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/vendor.emb
@@ -0,0 +1,112 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# This file contains Emboss packet definitions for extensions to the Bluetooth
+# Host-Controller interface. These extensions are not standardized through the
+# Bluetooth SIG.
+#
+# NOTE: The definitions below are incomplete. They get added as needed.
+# This list will grow as we support more vendor features.
+
+import "hci.emb" as hci
+
+[$default byte_order: "LittleEndian"]
+[(cpp) namespace: "pw::bluetooth::emboss"]
+
+# ======================= Android HCI extensions ========================
+# Documentation: https://source.android.com/devices/bluetooth/hci_requirements
+
+enum Capability:
+ [maximum_bits: 8]
+ NOT_CAPABLE = 0x00
+ CAPABLE = 0x01
+
+bits AudioCodecSupportMask:
+ 0 [+1] Flag sbc
+ 1 [+1] Flag aac
+ 2 [+1] Flag aptx
+ 3 [+1] Flag aptx_hd
+ 4 [+1] Flag ldac
+
+# ============ Commands ============
+
+
+struct LEMultiAdvtEnableCommand:
+ -- LE multi-advertising enable command.
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+ $next [+1] UInt opcode
+ $next [+1] hci.GenericEnableParam enable
+ $next [+1] UInt advertising_handle
+
+struct LEGetVendorCapabilitiesCommand:
+ let hdr_size = hci.CommandHeader.$size_in_bytes
+ 0 [+hdr_size] hci.CommandHeader header
+
+
+# ============ Events ============
+
+
+struct LEMultiAdvtStateChangeSubevent:
+ -- LE multi-advertising state change subevent.
+ 0 [+hci.VendorDebugEvent.$size_in_bytes] hci.VendorDebugEvent vendor_event
+ $next [+1] UInt advertising_handle
+ -- Handle used to identify an advertising set.
+
+ $next [+1] hci.StatusCode status
+ -- Reason for state change. Currently will always be 0x00.
+ -- 0x00: Connection received.
+
+ $next [+2] UInt connection_handle
+ -- Handle used to identify the connection that caused the state change (i.e.
+ -- advertising instance to be disabled). Value will be 0xFFFF if invalid.
+
+struct LEGetVendorCapabilitiesCommandCompleteEvent:
+ let hdr_size = hci.CommandCompleteEvent.$size_in_bytes
+ 0 [+hdr_size] hci.CommandCompleteEvent command_complete
+ $next [+1] hci.StatusCode status
+ $next [+1] UInt max_advt_instances
+ -- Number of advertisement instances supported
+ -- Deprecated in Google feature spec v0.98 and higher
+ $next [+1] Capability offloaded_resolution_of_private_address
+ -- BT chip capability of RPA
+ -- Deprecated in Google feature spec v0.98 and higher
+ $next [+2] UInt total_scan_results_storage
+ -- Storage for scan results in bytes
+ $next [+1] UInt max_irk_list_sz
+ -- Number of IRK entries supported in the firmware
+ $next [+1] Capability filtering_support
+ -- Support for filtering in the controller
+ $next [+1] UInt max_filter
+ -- Number of filters supported
+ $next [+1] Capability activity_energy_info_support
+ -- Supports reporting of activity and energy information
+ $next [+2] bits version_supported:
+ -- Specifies the version of the Google feature spec supported
+ 0 [+8] UInt major_number
+ $next [+8] UInt minor_number
+ $next [+2] UInt total_num_of_advt_tracked
+ -- Total number of advertisers tracked for OnLost/OnFound purposes
+ $next [+1] Capability extended_scan_support
+ -- Supports extended scan window and interval
+ $next [+1] Capability debug_logging_supported
+ -- Supports logging of binary debug information from controller
+ $next [+1] Capability le_address_generation_offloading_support
+ -- Deprecated in Google feature spec v0.98 and higher
+ $next [+4] bits:
+ 0 [+5] AudioCodecSupportMask a2dp_source_offload_capability_mask
+ $next [+1] Capability bluetooth_quality_report_support
+ -- Supports reporting of Bluetooth Quality events
+ $next [+4] bits:
+ 0 [+5] AudioCodecSupportMask dynamic_audio_buffer_support
diff --git a/pw_bluetooth/public/pw_bluetooth/vendor.h b/pw_bluetooth/public/pw_bluetooth/vendor.h
new file mode 100644
index 000000000..9c8636645
--- /dev/null
+++ b/pw_bluetooth/public/pw_bluetooth/vendor.h
@@ -0,0 +1,40 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include <cstdint>
+#include <type_traits>
+#include <variant>
+
+namespace pw::bluetooth {
+
+// The maximum buffer length an encoded command might require.
+// Update when adding new commands that might require a larger buffer.
+constexpr uint16_t kMaxVendorCommandBufferSize = 16;
+
+enum class AclPriority : uint8_t {
+ kNormal = 0,
+ kSource = 1,
+ kSink = 2,
+};
+
+struct SetAclPriorityCommandParameters {
+ uint16_t connection_handle;
+ AclPriority priority;
+};
+
+using VendorCommandParameters = std::variant<SetAclPriorityCommandParameters>;
+
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/result_test.cc b/pw_bluetooth/result_test.cc
new file mode 100644
index 000000000..27dcea009
--- /dev/null
+++ b/pw_bluetooth/result_test.cc
@@ -0,0 +1,70 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bluetooth/result.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::bluetooth {
+namespace {
+
+enum class TestError { kFailure0, kFailure1 };
+
+TEST(ResultTest, NoValue) {
+ EXPECT_TRUE(Result<TestError>().ok());
+
+ Result<TestError> error_result(TestError::kFailure0);
+ ASSERT_FALSE(error_result.ok());
+ EXPECT_EQ(error_result.error(), TestError::kFailure0);
+}
+
+TEST(ResultTest, Value) {
+ Result<TestError, int> ok_result(42);
+ ASSERT_TRUE(ok_result.ok());
+ EXPECT_EQ(ok_result.value(), 42);
+
+ Result<TestError, int> error_result(TestError::kFailure1);
+ ASSERT_FALSE(error_result.ok());
+ EXPECT_EQ(error_result.error(), TestError::kFailure1);
+}
+
+struct NonTrivialDestructor {
+ explicit NonTrivialDestructor(int* destructor_counter)
+ : destructor_counter_(destructor_counter) {}
+ ~NonTrivialDestructor() {
+ EXPECT_NE(nullptr, destructor_counter_);
+ (*destructor_counter_)++;
+ }
+ NonTrivialDestructor(const NonTrivialDestructor&) = delete;
+ NonTrivialDestructor(NonTrivialDestructor&&) = delete;
+
+ int* destructor_counter_;
+};
+
+TEST(ResultTest, NonTrivialDestructorTest) {
+ int counter = 0;
+ {
+ Result<TestError, NonTrivialDestructor> ret(std::in_place, &counter);
+ ASSERT_TRUE(ret.ok());
+ }
+ EXPECT_EQ(counter, 1);
+ {
+ Result<TestError, NonTrivialDestructor> ret(TestError::kFailure0);
+ ASSERT_FALSE(ret.ok());
+ }
+ EXPECT_EQ(counter, 1);
+}
+
+} // namespace
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth/size_report/BUILD.gn b/pw_bluetooth/size_report/BUILD.gn
new file mode 100644
index 000000000..37ad18bd3
--- /dev/null
+++ b/pw_bluetooth/size_report/BUILD.gn
@@ -0,0 +1,36 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pigweed/third_party/emboss/build_defs.gni")
+import("$dir_pw_build/target_types.gni")
+
+if (dir_pw_third_party_emboss != "") {
+ pw_executable("make_view_and_write") {
+ sources = [ "make_view_and_write.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_bluetooth:emboss_hci",
+ ]
+ }
+
+ pw_executable("make_2_views_and_write") {
+ sources = [ "make_2_views_and_write.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_bluetooth:emboss_hci",
+ ]
+ }
+}
diff --git a/pw_bluetooth/size_report/make_2_views_and_write.cc b/pw_bluetooth/size_report/make_2_views_and_write.cc
new file mode 100644
index 000000000..f14752259
--- /dev/null
+++ b/pw_bluetooth/size_report/make_2_views_and_write.cc
@@ -0,0 +1,33 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bloat/bloat_this_binary.h"
+#include "pw_bluetooth/hci.emb.h"
+
+int main() {
+ pw::bloat::BloatThisBinary();
+
+ char buffer[] = {0x00, 0x01, 0x02, 0x03};
+ auto view =
+ pw::bluetooth::emboss::MakeTestCommandPacketView(buffer, sizeof(buffer));
+ view.payload().Write(0x04);
+ view.payload().Read();
+
+ auto view2 =
+ pw::bluetooth::emboss::MakeTestEventPacketView(buffer, sizeof(buffer));
+ view2.payload().Write(0x01);
+ view2.payload().Read();
+
+ return 0;
+}
diff --git a/pw_bluetooth/size_report/make_view_and_write.cc b/pw_bluetooth/size_report/make_view_and_write.cc
new file mode 100644
index 000000000..7275d76ba
--- /dev/null
+++ b/pw_bluetooth/size_report/make_view_and_write.cc
@@ -0,0 +1,28 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bloat/bloat_this_binary.h"
+#include "pw_bluetooth/hci.emb.h"
+
+int main() {
+ pw::bloat::BloatThisBinary();
+
+ char buffer[] = {0x00, 0x01, 0x02, 0x03};
+ auto view =
+ pw::bluetooth::emboss::MakeTestCommandPacketView(buffer, sizeof(buffer));
+ view.payload().Write(0x04);
+ view.payload().Read();
+
+ return 0;
+}
diff --git a/pw_bluetooth/uuid_test.cc b/pw_bluetooth/uuid_test.cc
new file mode 100644
index 000000000..1be688dee
--- /dev/null
+++ b/pw_bluetooth/uuid_test.cc
@@ -0,0 +1,110 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bluetooth/uuid.h"
+
+#include <array>
+#include <cstdint>
+
+#include "gtest/gtest.h"
+#include "pw_span/span.h"
+
+namespace pw::bluetooth {
+namespace {
+
+// 16-bit and 32-bit short form UUID.
+constexpr Uuid kShortUuid16{0x1234};
+constexpr Uuid kShortUuid32{0xabcd1234};
+
+// UUID initialized from a span.
+constexpr Uuid kLongUuidArray{
+ pw::span<const uint8_t, 16>(std::array<uint8_t, 16>{
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})};
+
+// UUID initialized from a string. This is the same as kLongUuidArray but in
+// string form.
+constexpr Uuid kLongUuidString{"100f0e0d-0c0b-0a09-0807-060504030201"};
+
+// UUID initialized combining another UUID with a 32-bit value.
+constexpr Uuid kLongUuidComposed1 = Uuid(0xabcd, kLongUuidString);
+constexpr Uuid kLongUuidComposed2 = Uuid(0x1234abcd, kLongUuidString);
+
+// Make sure that all these values are actually constexpr.
+static_assert(kShortUuid16 != kShortUuid32, "constexpr check");
+static_assert(kLongUuidArray == kLongUuidString, "constexpr check");
+static_assert(kLongUuidComposed2.ToString() ==
+ "1234abcd-0c0b-0a09-0807-060504030201",
+ "constexpr check");
+
+TEST(UuidTest, ConstructorTest) {
+ // Compare 16-bit with 128-bit.
+ EXPECT_EQ(kShortUuid16, Uuid("00001234-0000-1000-8000-00805f9b34fb"));
+ EXPECT_EQ(kShortUuid32, Uuid("abcd1234-0000-1000-8000-00805f9b34fb"));
+
+ EXPECT_EQ(kShortUuid16.ToString(), "00001234-0000-1000-8000-00805f9b34fb");
+ auto short_span = kShortUuid16.As16BitSpan();
+ EXPECT_EQ(2u, short_span.size());
+ EXPECT_EQ(short_span[0], 0x34); // UUIDs are encoded little endian.
+ EXPECT_EQ(short_span[1], 0x12);
+
+ EXPECT_EQ(kLongUuidArray.ToString(), "100f0e0d-0c0b-0a09-0807-060504030201");
+ auto long_span = kLongUuidArray.As128BitSpan();
+ EXPECT_EQ(16u, long_span.size());
+ EXPECT_EQ(long_span[12], 0x0d);
+ EXPECT_EQ(long_span[13], 0x0e);
+
+ // These two are the same UUID initialized in different ways.
+ EXPECT_EQ(kLongUuidArray, kLongUuidString);
+
+ // Composed UUID always set the 32-bits in the first groups, regardless of
+ // whether we pass a 16-bit or 32-bit value, thus the first 0 chars in this
+ // string are 0.
+ static_assert(
+ kLongUuidComposed1.ToString() == "0000abcd-0c0b-0a09-0807-060504030201",
+ "constexpr check");
+ static_assert(
+ kLongUuidComposed2.ToString() == "1234abcd-0c0b-0a09-0807-060504030201",
+ "constexpr check");
+
+ // Check that the standard Bluetooth Base is correct.
+ static_assert(Uuid::BluetoothBase().ToString() ==
+ "00000000-0000-1000-8000-00805f9b34fb",
+ "constexpr check");
+}
+
+TEST(UuidTest, CombineShortUuidTest) {
+ // The short 16-bit value can be represented as 16 or 32 bit, but the 32-bit
+ // one can only be represented as 32-bits.
+ EXPECT_TRUE(kShortUuid16.Is16BitUuid());
+ EXPECT_TRUE(kShortUuid16.Is32BitUuid());
+ EXPECT_FALSE(kShortUuid32.Is16BitUuid());
+ EXPECT_TRUE(kShortUuid32.Is32BitUuid());
+
+ // The composed UUID is not a standard one, but matches the base it was used
+ // to construct it.
+ EXPECT_FALSE(kLongUuidComposed1.Is16BitUuid());
+ EXPECT_FALSE(kLongUuidComposed1.Is32BitUuid());
+
+ EXPECT_TRUE(kLongUuidComposed1.Same96BitBase(kLongUuidString));
+ EXPECT_TRUE(kLongUuidComposed2.Same96BitBase(kLongUuidString));
+
+ // Composing always uses 32-bit values (setting the high bits as 0s) so they
+ // don't match the base, unless it is zero extended.
+ EXPECT_FALSE(kLongUuidComposed1.Same112BitBase(kLongUuidString));
+ EXPECT_FALSE(kLongUuidComposed2.Same112BitBase(kLongUuidString));
+
+ EXPECT_TRUE(kLongUuidComposed1.Same112BitBase(Uuid(0, kLongUuidString)));
+}
+
+} // namespace
+} // namespace pw::bluetooth
diff --git a/pw_bluetooth_hci/BUILD.bazel b/pw_bluetooth_hci/BUILD.bazel
index 053bcaba6..c5de7f694 100644
--- a/pw_bluetooth_hci/BUILD.bazel
+++ b/pw_bluetooth_hci/BUILD.bazel
@@ -43,6 +43,7 @@ pw_cc_library(
deps = [
"//pw_assert",
"//pw_bytes",
+ "//pw_bytes:bit",
"//pw_result",
"//pw_status",
],
@@ -60,6 +61,7 @@ pw_cc_library(
deps = [
":packet",
"//pw_bytes",
+ "//pw_bytes:bit",
"//pw_function",
"//pw_status",
],
@@ -71,6 +73,7 @@ pw_cc_test(
deps = [
":packet",
"//pw_bytes",
+ "//pw_containers",
"//pw_status",
"//pw_unit_test",
],
diff --git a/pw_bluetooth_hci/BUILD.gn b/pw_bluetooth_hci/BUILD.gn
index f701613c5..0f5107163 100644
--- a/pw_bluetooth_hci/BUILD.gn
+++ b/pw_bluetooth_hci/BUILD.gn
@@ -35,9 +35,11 @@ pw_source_set("packet") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_bluetooth_hci/packet.h" ]
public_deps = [
+ "$dir_pw_bytes:bit",
dir_pw_assert,
dir_pw_bytes,
dir_pw_result,
+ dir_pw_span,
]
sources = [ "packet.cc" ]
deps = [ dir_pw_status ]
@@ -49,8 +51,10 @@ pw_source_set("uart_transport") {
sources = [ "uart_transport.cc" ]
public_deps = [
":packet",
+ "$dir_pw_bytes:bit",
dir_pw_bytes,
dir_pw_function,
+ dir_pw_span,
dir_pw_status,
]
}
@@ -59,14 +63,19 @@ pw_test_group("tests") {
tests = [
":packet_test",
":uart_transport_test",
- ":uart_transport_fuzzer",
+ ":uart_transport_fuzzer_test",
]
}
+group("fuzzers") {
+ deps = [ ":uart_transport_fuzzer" ]
+}
+
pw_test("packet_test") {
sources = [ "packet_test.cc" ]
deps = [
":packet",
+ "$dir_pw_containers",
dir_pw_bytes,
dir_pw_status,
]
diff --git a/pw_bluetooth_hci/packet.cc b/pw_bluetooth_hci/packet.cc
index 7eb7236fa..4769fc392 100644
--- a/pw_bluetooth_hci/packet.cc
+++ b/pw_bluetooth_hci/packet.cc
@@ -25,17 +25,17 @@ using pw::bytes::ReadInOrder;
} // namespace
Result<ConstByteSpan> CommandPacket::Encode(ByteSpan buffer,
- std::endian order) const {
+ endian order) const {
ByteBuilder builder(buffer);
builder.PutUint16(opcode_, order);
builder.PutUint8(parameters_.size_bytes());
builder.append(parameters_);
PW_TRY(builder.status());
return ConstByteSpan(builder.data(), builder.size());
-};
+}
std::optional<CommandPacket> CommandPacket::Decode(ConstByteSpan data,
- std::endian order) {
+ endian order) {
if (data.size_bytes() < kHeaderSizeBytes) {
return std::nullopt; // Not enough data to parse the packet header.
}
@@ -48,22 +48,22 @@ std::optional<CommandPacket> CommandPacket::Decode(ConstByteSpan data,
const uint16_t opcode =
ReadInOrder<uint16_t>(order, &data[kOpcodeByteOffset]);
- return CommandPacket(
- opcode, ConstByteSpan(&data[kHeaderSizeBytes], parameter_total_length));
+ return CommandPacket(opcode,
+ data.subspan(kHeaderSizeBytes, parameter_total_length));
}
Result<ConstByteSpan> AsyncDataPacket::Encode(ByteSpan buffer,
- std::endian order) const {
+ endian order) const {
ByteBuilder builder(buffer);
builder.PutUint16(handle_and_fragmentation_bits_, order);
builder.PutUint16(data_.size_bytes(), order);
builder.append(data_);
PW_TRY(builder.status());
return ConstByteSpan(builder.data(), builder.size());
-};
+}
std::optional<AsyncDataPacket> AsyncDataPacket::Decode(ConstByteSpan data,
- std::endian order) {
+ endian order) {
if (data.size_bytes() < kHeaderSizeBytes) {
return std::nullopt; // Not enough data to parse the packet header.
}
@@ -76,23 +76,22 @@ std::optional<AsyncDataPacket> AsyncDataPacket::Decode(ConstByteSpan data,
const uint16_t handle_and_flag_bits = ReadInOrder<uint16_t>(
order, &data[kHandleAndFragmentationBitsByteOffset]);
- return AsyncDataPacket(
- handle_and_flag_bits,
- ConstByteSpan(&data[kHeaderSizeBytes], data_total_length));
+ return AsyncDataPacket(handle_and_flag_bits,
+ data.subspan(kHeaderSizeBytes, data_total_length));
}
Result<ConstByteSpan> SyncDataPacket::Encode(ByteSpan buffer,
- std::endian order) const {
+ endian order) const {
ByteBuilder builder(buffer);
builder.PutUint16(handle_and_status_bits_, order);
builder.PutUint8(data_.size_bytes());
builder.append(data_);
PW_TRY(builder.status());
return ConstByteSpan(builder.data(), builder.size());
-};
+}
std::optional<SyncDataPacket> SyncDataPacket::Decode(ConstByteSpan data,
- std::endian order) {
+ endian order) {
if (data.size_bytes() < kHeaderSizeBytes) {
return std::nullopt; // Not enough data to parse the packet header.
}
@@ -105,9 +104,8 @@ std::optional<SyncDataPacket> SyncDataPacket::Decode(ConstByteSpan data,
const uint16_t handle_and_status_bits =
ReadInOrder<uint16_t>(order, &data[kHandleAndStatusBitsByteOffset]);
- return SyncDataPacket(
- handle_and_status_bits,
- ConstByteSpan(&data[kHeaderSizeBytes], data_total_length));
+ return SyncDataPacket(handle_and_status_bits,
+ data.subspan(kHeaderSizeBytes, data_total_length));
}
Result<ConstByteSpan> EventPacket::Encode(ByteSpan buffer) const {
@@ -117,7 +115,7 @@ Result<ConstByteSpan> EventPacket::Encode(ByteSpan buffer) const {
builder.append(parameters_);
PW_TRY(builder.status());
return ConstByteSpan(builder.data(), builder.size());
-};
+}
std::optional<EventPacket> EventPacket::Decode(ConstByteSpan data) {
if (data.size_bytes() < kHeaderSizeBytes) {
@@ -131,9 +129,8 @@ std::optional<EventPacket> EventPacket::Decode(ConstByteSpan data) {
}
const uint8_t event_code = static_cast<uint8_t>(data[kEventCodeByteOffset]);
- return EventPacket(
- event_code,
- ConstByteSpan(&data[kHeaderSizeBytes], parameter_total_length));
+ return EventPacket(event_code,
+ data.subspan(kHeaderSizeBytes, parameter_total_length));
}
} // namespace pw::bluetooth_hci
diff --git a/pw_bluetooth_hci/packet_test.cc b/pw_bluetooth_hci/packet_test.cc
index f5301d5c9..526397199 100644
--- a/pw_bluetooth_hci/packet_test.cc
+++ b/pw_bluetooth_hci/packet_test.cc
@@ -17,6 +17,7 @@
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
#include "pw_bytes/byte_builder.h"
+#include "pw_containers/algorithm.h"
#include "pw_status/status.h"
namespace pw::bluetooth_hci {
@@ -72,10 +73,7 @@ TEST_F(PacketTest, CommandPacketHeaderOnlyEncodeAndDecode) {
kExpectedEncodedPacket = bytes::MakeArray<const std::byte>(
0b0000'0000, 0b1111'1100, 0b0000'0000);
const ConstByteSpan& encoded_packet = encode_result.value();
- EXPECT_TRUE(std::equal(kExpectedEncodedPacket.begin(),
- kExpectedEncodedPacket.end(),
- encoded_packet.begin(),
- encoded_packet.end()));
+ EXPECT_TRUE(pw::containers::Equal(kExpectedEncodedPacket, encoded_packet));
// First, decode it from a perfectly sized span which we just encoded.
std::optional<CommandPacket> possible_packet =
@@ -127,10 +125,7 @@ TEST_F(PacketTest, CommandPacketWithParametersEncodeAndDecode) {
kExpectedEncodedPacket = bytes::MakeArray<const std::byte>(
0b1010'1010, 0b1010'1010, 0b0000'0010, 1, 2);
const ConstByteSpan& encoded_packet = encode_result.value();
- EXPECT_TRUE(std::equal(kExpectedEncodedPacket.begin(),
- kExpectedEncodedPacket.end(),
- encoded_packet.begin(),
- encoded_packet.end()));
+ EXPECT_TRUE(pw::containers::Equal(kExpectedEncodedPacket, encoded_packet));
// First, decode it from a perfectly sized span which we just encoded.
std::optional<CommandPacket> possible_packet =
@@ -199,10 +194,7 @@ TEST_F(PacketTest, AsyncDataPacketHeaderOnlyEncodeAndDecode) {
kExpectedEncodedPacket = bytes::MakeArray<const std::byte>(
0b0000'0000, 0b1001'0000, 0b0000'0000, 0b0000'0000);
const ConstByteSpan& encoded_packet = encode_result.value();
- EXPECT_TRUE(std::equal(kExpectedEncodedPacket.begin(),
- kExpectedEncodedPacket.end(),
- encoded_packet.begin(),
- encoded_packet.end()));
+ EXPECT_TRUE(pw::containers::Equal(kExpectedEncodedPacket, encoded_packet));
// First, decode it from a perfectly sized span which we just encoded.
std::optional<AsyncDataPacket> possible_packet =
@@ -262,10 +254,7 @@ TEST_F(PacketTest, AsyncDataPacketWithDataEncodeAndDecode) {
kExpectedEncodedPacket = bytes::MakeArray<const std::byte>(
0b0000'0000, 0b1001'0000, 0b0000'0010, 0b0000'0000, 1, 2);
const ConstByteSpan& encoded_packet = encode_result.value();
- EXPECT_TRUE(std::equal(kExpectedEncodedPacket.begin(),
- kExpectedEncodedPacket.end(),
- encoded_packet.begin(),
- encoded_packet.end()));
+ EXPECT_TRUE(pw::containers::Equal(kExpectedEncodedPacket, encoded_packet));
// First, decode it from a perfectly sized span which we just encoded.
std::optional<AsyncDataPacket> possible_packet =
@@ -334,10 +323,7 @@ TEST_F(PacketTest, SyncDataPacketHeaderOnlyEncodeAndDecode) {
kExpectedEncodedPacket = bytes::MakeArray<const std::byte>(
0b0000'0000, 0b0011'0000, 0b0000'0000);
const ConstByteSpan& encoded_packet = encode_result.value();
- EXPECT_TRUE(std::equal(kExpectedEncodedPacket.begin(),
- kExpectedEncodedPacket.end(),
- encoded_packet.begin(),
- encoded_packet.end()));
+ EXPECT_TRUE(pw::containers::Equal(kExpectedEncodedPacket, encoded_packet));
// First, decode it from a perfectly sized span which we just encoded.
std::optional<SyncDataPacket> possible_packet =
@@ -391,10 +377,7 @@ TEST_F(PacketTest, SyncDataPacketWithDataEncodeAndDecode) {
kExpectedEncodedPacket = bytes::MakeArray<const std::byte>(
0b0000'0000, 0b0011'0000, 0b0000'0010, 1, 2);
const ConstByteSpan& encoded_packet = encode_result.value();
- EXPECT_TRUE(std::equal(kExpectedEncodedPacket.begin(),
- kExpectedEncodedPacket.end(),
- encoded_packet.begin(),
- encoded_packet.end()));
+ EXPECT_TRUE(pw::containers::Equal(kExpectedEncodedPacket, encoded_packet));
// First, decode it from a perfectly sized span which we just encoded.
std::optional<SyncDataPacket> possible_packet =
@@ -452,10 +435,7 @@ TEST_F(PacketTest, EventPacketHeaderOnlyEncodeAndDecode) {
kExpectedEncodedPacket =
bytes::MakeArray<const std::byte>(0b1111'11111, 0b0000'0000);
const ConstByteSpan& encoded_packet = encode_result.value();
- EXPECT_TRUE(std::equal(kExpectedEncodedPacket.begin(),
- kExpectedEncodedPacket.end(),
- encoded_packet.begin(),
- encoded_packet.end()));
+ EXPECT_TRUE(pw::containers::Equal(kExpectedEncodedPacket, encoded_packet));
// First, decode it from a perfectly sized span which we just encoded.
std::optional<EventPacket> possible_packet =
@@ -498,10 +478,7 @@ TEST_F(PacketTest, EventPacketWithParametersEncodeAndDecode) {
kExpectedEncodedPacket =
bytes::MakeArray<const std::byte>(0b1111'0000, 0b0000'0010, 1, 2);
const ConstByteSpan& encoded_packet = encode_result.value();
- EXPECT_TRUE(std::equal(kExpectedEncodedPacket.begin(),
- kExpectedEncodedPacket.end(),
- encoded_packet.begin(),
- encoded_packet.end()));
+ EXPECT_TRUE(pw::containers::Equal(kExpectedEncodedPacket, encoded_packet));
// First, decode it from a perfectly sized span which we just encoded.
std::optional<EventPacket> possible_packet =
diff --git a/pw_bluetooth_hci/public/pw_bluetooth_hci/packet.h b/pw_bluetooth_hci/public/pw_bluetooth_hci/packet.h
index 7ccec8934..c576d8ac4 100644
--- a/pw_bluetooth_hci/public/pw_bluetooth_hci/packet.h
+++ b/pw_bluetooth_hci/public/pw_bluetooth_hci/packet.h
@@ -13,11 +13,11 @@
// the License.
#pragma once
-#include <bit>
#include <cstdint>
#include <optional>
#include "pw_assert/assert.h"
+#include "pw_bytes/bit.h"
#include "pw_bytes/span.h"
#include "pw_result/result.h"
@@ -131,8 +131,8 @@ class CommandPacket : public Packet {
}
// Decodes the packet based on the specified endianness.
- static std::optional<CommandPacket> Decode(
- ConstByteSpan data, std::endian order = std::endian::little);
+ static std::optional<CommandPacket> Decode(ConstByteSpan data,
+ endian order = endian::little);
// Encodes the packet based on the specified endianness.
//
@@ -140,7 +140,7 @@ class CommandPacket : public Packet {
// OK - returns the encoded packet.
// RESOURCE_EXHAUSTED - The input buffer is too small for this packet.
Result<ConstByteSpan> Encode(ByteSpan buffer,
- std::endian order = std::endian::little) const;
+ endian order = endian::little) const;
constexpr uint16_t opcode() const { return opcode_; }
@@ -196,8 +196,8 @@ class AsyncDataPacket : public Packet {
}
// Decodes the packet based on the specified endianness.
- static std::optional<AsyncDataPacket> Decode(
- ConstByteSpan data, std::endian order = std::endian::little);
+ static std::optional<AsyncDataPacket> Decode(ConstByteSpan data,
+ endian order = endian::little);
// Encodes the packet based on the specified endianness.
//
@@ -205,7 +205,7 @@ class AsyncDataPacket : public Packet {
// OK - returns the encoded packet.
// RESOURCE_EXHAUSTED - The input buffer is too small for this packet.
Result<ConstByteSpan> Encode(ByteSpan buffer,
- std::endian order = std::endian::little) const;
+ endian order = endian::little) const;
constexpr uint16_t handle_and_fragmentation_bits() const {
return handle_and_fragmentation_bits_;
@@ -266,8 +266,8 @@ class SyncDataPacket : public Packet {
}
// Decodes the packet based on the specified endianness.
- static std::optional<SyncDataPacket> Decode(
- ConstByteSpan data, std::endian order = std::endian::little);
+ static std::optional<SyncDataPacket> Decode(ConstByteSpan data,
+ endian order = endian::little);
// Encodes the packet based on the specified endianness.
//
@@ -275,7 +275,7 @@ class SyncDataPacket : public Packet {
// OK - returns the encoded packet.
// RESOURCE_EXHAUSTED - The input buffer is too small for this packet.
Result<ConstByteSpan> Encode(ByteSpan buffer,
- std::endian order = std::endian::little) const;
+ endian order = endian::little) const;
constexpr uint16_t handle_and_status_bits() const {
return handle_and_status_bits_;
diff --git a/pw_bluetooth_hci/public/pw_bluetooth_hci/uart_transport.h b/pw_bluetooth_hci/public/pw_bluetooth_hci/uart_transport.h
index 6c6c78e1e..c36235eb0 100644
--- a/pw_bluetooth_hci/public/pw_bluetooth_hci/uart_transport.h
+++ b/pw_bluetooth_hci/public/pw_bluetooth_hci/uart_transport.h
@@ -13,9 +13,8 @@
// the License.
#pragma once
-#include <bit>
-
#include "pw_bluetooth_hci/packet.h"
+#include "pw_bytes/bit.h"
#include "pw_bytes/span.h"
#include "pw_function/function.h"
#include "pw_status/status_with_size.h"
diff --git a/pw_bluetooth_hci/uart_transport.cc b/pw_bluetooth_hci/uart_transport.cc
index d62e3005e..0b495668d 100644
--- a/pw_bluetooth_hci/uart_transport.cc
+++ b/pw_bluetooth_hci/uart_transport.cc
@@ -28,7 +28,7 @@ StatusWithSize DecodeHciUartData(ConstByteSpan data,
switch (packet_indicator) {
case kUartCommandPacketIndicator: {
const std::optional<CommandPacket> maybe_packet =
- CommandPacket::Decode(data, std::endian::little);
+ CommandPacket::Decode(data, endian::little);
if (!maybe_packet.has_value()) {
return StatusWithSize(
bytes_consumed); // Not enough data to complete this packet.
@@ -42,7 +42,7 @@ StatusWithSize DecodeHciUartData(ConstByteSpan data,
case kUartAsyncDataPacketIndicator: {
const std::optional<AsyncDataPacket> maybe_packet =
- AsyncDataPacket::Decode(data, std::endian::little);
+ AsyncDataPacket::Decode(data, endian::little);
if (!maybe_packet.has_value()) {
return StatusWithSize(
bytes_consumed); // Not enough data to complete this packet.
@@ -56,7 +56,7 @@ StatusWithSize DecodeHciUartData(ConstByteSpan data,
case kUartSyncDataPacketIndicator: {
const std::optional<SyncDataPacket> maybe_packet =
- SyncDataPacket::Decode(data, std::endian::little);
+ SyncDataPacket::Decode(data, endian::little);
if (!maybe_packet.has_value()) {
return StatusWithSize(
bytes_consumed); // Not enough data to complete this packet.
diff --git a/pw_bluetooth_hci/uart_transport_fuzzer.cc b/pw_bluetooth_hci/uart_transport_fuzzer.cc
index 3097374c2..4324c4ff9 100644
--- a/pw_bluetooth_hci/uart_transport_fuzzer.cc
+++ b/pw_bluetooth_hci/uart_transport_fuzzer.cc
@@ -13,11 +13,11 @@
// the License.
#include <cstddef>
-#include <span>
#include "pw_bluetooth_hci/packet.h"
#include "pw_bluetooth_hci/uart_transport.h"
#include "pw_bytes/span.h"
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
#include "pw_stream/null_stream.h"
@@ -36,18 +36,18 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
const CommandPacket& command_packet = packet.command_packet();
const uint16_t opcode = command_packet.opcode();
- stream.Write(std::as_bytes(std::span<const uint16_t>(&opcode, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&opcode, 1))).IgnoreError();
const uint16_t opcode_command_field =
command_packet.opcode_command_field();
- stream.Write(
- std::as_bytes(std::span<const uint16_t>(&opcode_command_field, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&opcode_command_field, 1)))
+ .IgnoreError();
const uint8_t opcode_group_field = command_packet.opcode_group_field();
- stream.Write(
- std::as_bytes(std::span<const uint8_t>(&opcode_group_field, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&opcode_group_field, 1)))
+ .IgnoreError();
- stream.Write(command_packet.parameters());
+ stream.Write(command_packet.parameters()).IgnoreError();
return;
}
@@ -56,19 +56,21 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
const uint16_t handle_and_fragmentation_bits =
async_data_packet.handle_and_fragmentation_bits();
- stream.Write(std::as_bytes(
- std::span<const uint16_t>(&handle_and_fragmentation_bits, 1)));
+ stream
+ .Write(as_bytes(
+ span<const uint16_t>(&handle_and_fragmentation_bits, 1)))
+ .IgnoreError();
const uint16_t handle = async_data_packet.handle();
- stream.Write(std::as_bytes(std::span<const uint16_t>(&handle, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&handle, 1))).IgnoreError();
const uint8_t pb_flag = async_data_packet.pb_flag();
- stream.Write(std::as_bytes(std::span<const uint8_t>(&pb_flag, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&pb_flag, 1))).IgnoreError();
const uint8_t bc_flag = async_data_packet.bc_flag();
- stream.Write(std::as_bytes(std::span<const uint8_t>(&bc_flag, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&bc_flag, 1))).IgnoreError();
- stream.Write(async_data_packet.data());
+ stream.Write(async_data_packet.data()).IgnoreError();
return;
}
@@ -77,18 +79,18 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
const uint16_t handle_and_status_bits =
sync_data_packet.handle_and_status_bits();
- stream.Write(std::as_bytes(
- std::span<const uint16_t>(&handle_and_status_bits, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&handle_and_status_bits, 1)))
+ .IgnoreError();
const uint16_t handle = sync_data_packet.handle();
- stream.Write(std::as_bytes(std::span<const uint16_t>(&handle, 1)));
+ stream.Write(as_bytes(span<const uint16_t>(&handle, 1))).IgnoreError();
const uint8_t packet_status_flag =
sync_data_packet.packet_status_flag();
- stream.Write(
- std::as_bytes(std::span<const uint8_t>(&packet_status_flag, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&packet_status_flag, 1)))
+ .IgnoreError();
- stream.Write(sync_data_packet.data());
+ stream.Write(sync_data_packet.data()).IgnoreError();
return;
}
@@ -96,9 +98,10 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
const EventPacket& event_packet = packet.event_packet();
const uint8_t event_code = event_packet.event_code();
- stream.Write(std::as_bytes(std::span<const uint8_t>(&event_code, 1)));
+ stream.Write(as_bytes(span<const uint8_t>(&event_code, 1)))
+ .IgnoreError();
- stream.Write(event_packet.parameters());
+ stream.Write(event_packet.parameters()).IgnoreError();
return;
}
@@ -108,7 +111,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
};
const StatusWithSize result =
- DecodeHciUartData(std::as_bytes(std::span(data, size)), packet_callback);
+ DecodeHciUartData(as_bytes(span(data, size)), packet_callback);
result.status().IgnoreError();
return 0;
}
diff --git a/pw_bluetooth_profiles/BUILD.bazel b/pw_bluetooth_profiles/BUILD.bazel
new file mode 100644
index 000000000..680f9f0f8
--- /dev/null
+++ b/pw_bluetooth_profiles/BUILD.bazel
@@ -0,0 +1,51 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# Bazel does not yet support building docs.
+filegroup(
+ name = "docs",
+ srcs = ["docs.rst"],
+)
+
+# Device Information Service 1.1
+pw_cc_library(
+ name = "device_info_service",
+ srcs = [
+ "device_info_service.cc",
+ ],
+ hdrs = [
+ "public/pw_bluetooth_profiles/device_info_service.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ "//pw_bluetooth",
+ ],
+)
+
+pw_cc_test(
+ name = "device_info_service_test",
+ srcs = ["device_info_service_test.cc"],
+ deps = [":device_info_service"],
+)
diff --git a/pw_bluetooth_profiles/BUILD.gn b/pw_bluetooth_profiles/BUILD.gn
new file mode 100644
index 000000000..d09ff93f2
--- /dev/null
+++ b/pw_bluetooth_profiles/BUILD.gn
@@ -0,0 +1,50 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+# Device Information Service 1.1
+pw_source_set("device_info_service") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_bluetooth_profiles/device_info_service.h" ]
+ public_deps = [
+ dir_pw_bluetooth,
+ dir_pw_span,
+ ]
+ deps = [ dir_pw_assert ]
+ sources = [ "device_info_service.cc" ]
+}
+
+pw_test_group("tests") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ tests = [ ":device_info_service_test" ]
+}
+
+pw_test("device_info_service_test") {
+ sources = [ "device_info_service_test.cc" ]
+ deps = [ ":device_info_service" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_bluetooth_profiles/CMakeLists.txt b/pw_bluetooth_profiles/CMakeLists.txt
new file mode 100644
index 000000000..d6cfa7589
--- /dev/null
+++ b/pw_bluetooth_profiles/CMakeLists.txt
@@ -0,0 +1,40 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# CMake does not yet support building docs.
+
+# Device Information Service 1.1
+pw_add_module_library(pw_bluetooth_profiles.device_info_service
+ HEADERS
+ public/pw_bluetooth_profiles/device_info_service.h
+ SOURCES
+ device_info_service.cc
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bluetooth
+ PRIVATE_DEPS
+ pw_assert
+)
+
+pw_add_test(pw_bluetooth_profiles.device_info_service_test
+ SOURCES
+ device_info_service_test.cc
+ PRIVATE_DEPS
+ pw_bluetooth_profiles.device_info_service
+ GROUPS
+ pw_bluetooth_profiles
+)
diff --git a/pw_bluetooth_profiles/device_info_service.cc b/pw_bluetooth_profiles/device_info_service.cc
new file mode 100644
index 000000000..d20e3663d
--- /dev/null
+++ b/pw_bluetooth_profiles/device_info_service.cc
@@ -0,0 +1,67 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bluetooth_profiles/device_info_service.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::bluetooth_profiles {
+
+void DeviceInfoServiceImpl::PublishService(
+ bluetooth::gatt::Server* gatt_server,
+ Callback<
+ void(bluetooth::Result<bluetooth::gatt::Server::PublishServiceError>)>
+ result_callback) {
+ PW_CHECK(publish_service_callback_ == nullptr);
+ publish_service_callback_ = std::move(result_callback);
+ this->delegate_.SetServicePtr(nullptr);
+ return gatt_server->PublishService(
+ service_info_,
+ &delegate_,
+ [this](bluetooth::gatt::Server::PublishServiceResult result) {
+ if (result.ok()) {
+ this->publish_service_callback_({});
+ this->delegate_.SetServicePtr(std::move(result.value()));
+ } else {
+ this->publish_service_callback_(result.error());
+ }
+ });
+}
+
+void DeviceInfoServiceImpl::Delegate::OnError(
+ bluetooth::gatt::Error /* error */) {
+ local_service_.reset();
+}
+
+void DeviceInfoServiceImpl::Delegate::ReadValue(
+ bluetooth::PeerId /* peer_id */,
+ bluetooth::gatt::Handle handle,
+ uint32_t offset,
+ Function<void(
+ bluetooth::Result<bluetooth::gatt::Error, span<const std::byte>>)>&&
+ result_callback) {
+ const uint32_t value_index = static_cast<uint32_t>(handle);
+ if (value_index >= values_.size()) {
+ result_callback(bluetooth::gatt::Error::kInvalidHandle);
+ return;
+ }
+ span<const std::byte> value = values_[value_index];
+ if (offset > value.size()) {
+ result_callback(bluetooth::gatt::Error::kInvalidOffset);
+ return;
+ }
+ result_callback({std::in_place, value.subspan(offset)});
+}
+
+} // namespace pw::bluetooth_profiles
diff --git a/pw_bluetooth_profiles/device_info_service_test.cc b/pw_bluetooth_profiles/device_info_service_test.cc
new file mode 100644
index 000000000..5782ef4a7
--- /dev/null
+++ b/pw_bluetooth_profiles/device_info_service_test.cc
@@ -0,0 +1,171 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bluetooth_profiles/device_info_service.h"
+
+#include <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_bluetooth/gatt/server.h"
+
+using namespace std::string_view_literals;
+
+namespace pw::bluetooth_profiles {
+namespace {
+
+class FakeGattServer final : public bluetooth::gatt::Server {
+ public:
+ // Server overrides:
+ void PublishService(
+ const bluetooth::gatt::LocalServiceInfo& info,
+ bluetooth::gatt::LocalServiceDelegate* delegate,
+ Function<void(PublishServiceResult)>&& result_callback) override {
+ ASSERT_EQ(delegate_, nullptr);
+ delegate_ = delegate;
+ ASSERT_EQ(published_info_, nullptr);
+ published_info_ = &info;
+ local_service_.emplace(this);
+ result_callback(
+ PublishServiceResult(std::in_place, &local_service_.value()));
+ }
+
+ // PublishService call argument getters:
+ const bluetooth::gatt::LocalServiceInfo* published_info() const {
+ return published_info_;
+ }
+ bluetooth::gatt::LocalServiceDelegate* delegate() const { return delegate_; }
+
+ private:
+ class FakeLocalService final : public bluetooth::gatt::LocalService {
+ public:
+ explicit FakeLocalService(FakeGattServer* fake_server)
+ : fake_server_(fake_server) {}
+
+ // LocalService overrides:
+ void NotifyValue(
+ const ValueChangedParameters& /* parameters */,
+ ValueChangedCallback&& /* completion_callback */) override {
+ FAIL(); // Unimplemented
+ }
+ void IndicateValue(
+ const ValueChangedParameters& /* parameters */,
+ Function<void(
+ bluetooth::Result<bluetooth::gatt::Error>)>&& /* confirmation */)
+ override {
+ FAIL(); // Unimplemented
+ }
+
+ private:
+ void UnpublishService() override { fake_server_->local_service_.reset(); }
+
+ FakeGattServer* fake_server_;
+ };
+
+ // The LocalServiceInfo passed when PublishService was called.
+ const bluetooth::gatt::LocalServiceInfo* published_info_ = nullptr;
+
+ bluetooth::gatt::LocalServiceDelegate* delegate_ = nullptr;
+
+ std::optional<FakeLocalService> local_service_;
+};
+
+TEST(DeviceInfoServiceTest, PublishAndReadTest) {
+ FakeGattServer fake_server;
+
+ constexpr auto kUsedFields = DeviceInfo::Field::kModelNumber |
+ DeviceInfo::Field::kSerialNumber |
+ DeviceInfo::Field::kSoftwareRevision;
+ DeviceInfo device_info = {};
+ const auto kModelNumber = "model"sv;
+ device_info.model_number = as_bytes(span{kModelNumber});
+ device_info.serial_number = as_bytes(span{"parallel_number"sv});
+ device_info.software_revision = as_bytes(span{"rev123"sv});
+
+ DeviceInfoService<kUsedFields, bluetooth::gatt::Handle{123}>
+ device_info_service(device_info);
+
+ bool called = false;
+ device_info_service.PublishService(
+ &fake_server,
+ [&called](
+ bluetooth::Result<bluetooth::gatt::Server::PublishServiceError> res) {
+ EXPECT_TRUE(res.ok());
+ called = true;
+ });
+ // The FakeGattServer calls the PublishService callback right away so our
+ // callback should have been called already.
+ EXPECT_TRUE(called);
+
+ ASSERT_NE(fake_server.delegate(), nullptr);
+ ASSERT_NE(fake_server.published_info(), nullptr);
+
+ // Test that the published info looks correct.
+ EXPECT_EQ(3u, fake_server.published_info()->characteristics.size());
+
+ // Test that we can read the characteristics.
+ for (auto& characteristic : fake_server.published_info()->characteristics) {
+ bool read_callback_called = false;
+ fake_server.delegate()->ReadValue(
+ bluetooth::PeerId{1234},
+ characteristic.handle,
+ /*offset=*/0,
+ [&read_callback_called](bluetooth::Result<bluetooth::gatt::Error,
+ span<const std::byte>> res) {
+ EXPECT_TRUE(res.ok());
+ EXPECT_NE(0u, res.value().size());
+ read_callback_called = true;
+ });
+ // The DeviceInfoService always calls the callback from within ReadValue().
+ EXPECT_TRUE(read_callback_called);
+ }
+
+ // Check the actual values.
+ // The order of the characteristics in the LocalServiceInfo must be the order
+ // in which the fields are listed in kCharacteristicFields, so the first
+ // characteristic is the Model Number.
+ span<const std::byte> read_value;
+ fake_server.delegate()->ReadValue(
+ bluetooth::PeerId{1234},
+ fake_server.published_info()->characteristics[0].handle,
+ /*offset=*/0,
+ [&read_value](bluetooth::Result<bluetooth::gatt::Error,
+ span<const std::byte>> res) {
+ EXPECT_TRUE(res.ok());
+ read_value = res.value();
+ });
+ EXPECT_EQ(read_value.size(), kModelNumber.size()); // "model" string.
+ // DeviceInfoService keeps references to the values provides in the
+ // DeviceInfo struct, not copies.
+ EXPECT_EQ(read_value.data(),
+ reinterpret_cast<const std::byte*>(kModelNumber.data()));
+
+ // Read with an offset.
+ const size_t kReadOffset = 3;
+ fake_server.delegate()->ReadValue(
+ bluetooth::PeerId{1234},
+ fake_server.published_info()->characteristics[0].handle,
+ kReadOffset,
+ [&read_value](bluetooth::Result<bluetooth::gatt::Error,
+ span<const std::byte>> res) {
+ EXPECT_TRUE(res.ok());
+ read_value = res.value();
+ });
+ EXPECT_EQ(read_value.size(), kModelNumber.size() - kReadOffset);
+ EXPECT_EQ(
+ read_value.data(),
+ reinterpret_cast<const std::byte*>(kModelNumber.data()) + kReadOffset);
+}
+
+} // namespace
+} // namespace pw::bluetooth_profiles
diff --git a/pw_bluetooth_profiles/docs.rst b/pw_bluetooth_profiles/docs.rst
new file mode 100644
index 000000000..0d79eaec6
--- /dev/null
+++ b/pw_bluetooth_profiles/docs.rst
@@ -0,0 +1,77 @@
+.. _module-pw_bluetooth_profiles:
+
+=====================
+pw_bluetooth_profiles
+=====================
+
+.. attention::
+
+ :bdg-ref-primary-line:`module-pw_bluetooth_profiles` is under construction,
+ depends on the experimental :bdg-ref-primary-line:`module-pw_bluetooth`
+ module and may see significant breaking API changes.
+
+The ``pw_bluetooth_profiles`` module provides a collection of implementations
+for basic Bluetooth profiles built on top of the ``pw_bluetooth`` module API.
+These profiles are independent from each other
+
+--------------------------
+Device Information Service
+--------------------------
+The ``device_info_service`` target implements the Device Information Service
+(DIS) as defined in the specification version 1.1. It exposes up to nine
+different basic properties of the device such as the model, manufacturer or
+serial number, all of which are optional. This module implements the GATT
+server-side service (``bluetooth::gatt::LocalServiceDelegate``) with the
+following limitations:
+
+- The subset of properties exposed and their values are constant throughout the
+ life of the service.
+- The subset of properties is defined at compile time, but the values may be
+ defined at runtime before service initialization. For example, the serial
+ number property might be different for different devices running the same
+ code.
+- All property values must be available in memory while the service is
+ published. Rather than using a callback mechanism to let the user produce the
+ property value at run-time, this module expects those values to be readily
+ available when initialized, but they can be stored in read-only memory.
+
+Usage
+-----
+The main intended usage of the service consists on creating and publishing the
+service, leaving it published forever referencing the values passed on
+initialization.
+
+The subset of properties exposed is a template parameter bit field
+(``DeviceInfo::Field``) and can't be changed at run-time. The ``pw::span``
+values referenced in the ``DeviceInfo`` struct must remain available after
+initialization to avoid copying them to RAM in the service, but the
+``DeviceInfo`` struct itself can be destroyed after initialization.
+
+Example code:
+
+.. code-block:: cpp
+
+ using pw::bluetooth_profiles::DeviceInfo;
+ using pw::bluetooth_profiles::DeviceInfoService;
+
+ // Global serial number for the device, initialized elsewhere.
+ pw::InlineString serial_number(...);
+
+ // Select which fields to expose at compile-time with a constexpr template
+ // parameter.
+ constexpr auto kUsedFields = DeviceInfo::Field::kModelNumber |
+ DeviceInfo::Field::kSerialNumber |
+ DeviceInfo::Field::kSoftwareRevision;
+
+ // Create a DeviceInfo with the values. Values are referenced from the
+ // service, not copied, so they must remain available while the service is
+ // published.
+ DeviceInfo device_info = {};
+ device_info.model_number = pw::as_bytes(pw::span{"My Model"sv});
+ device_info.software_revision = pw::as_bytes(pw::span{REVISION_MACRO});
+ device_info.serial_number = pw::as_bytes(
+ pw::span(serial_number.data(), serial_number.size()));
+
+ DeviceInfoService<kUsedFields, pw::bluetooth::gatt::Handle{123}>
+ device_info_service{device_info};
+ device_info_service.PublishService(...);
diff --git a/pw_bluetooth_profiles/public/pw_bluetooth_profiles/device_info_service.h b/pw_bluetooth_profiles/public/pw_bluetooth_profiles/device_info_service.h
new file mode 100644
index 000000000..ca4636bcc
--- /dev/null
+++ b/pw_bluetooth_profiles/public/pw_bluetooth_profiles/device_info_service.h
@@ -0,0 +1,291 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+#include "pw_bluetooth/assigned_uuids.h"
+#include "pw_bluetooth/gatt/error.h"
+#include "pw_bluetooth/gatt/server.h"
+#include "pw_bluetooth/gatt/types.h"
+#include "pw_span/span.h"
+
+namespace pw::bluetooth_profiles {
+
+// Device information to be exposed by the Device Information Service, according
+// to the DIS spec 1.1. All fields are optional.
+struct DeviceInfo {
+ // Bitmask of the fields present in the DeviceInfoService, each one
+ // corresponding to one of the possible characteristics in the Device
+ // Information Service.
+ enum class Field : uint16_t {
+ kManufacturerName = 1u << 0,
+ kModelNumber = 1u << 1,
+ kSerialNumber = 1u << 2,
+ kHardwareRevision = 1u << 3,
+ kFirmwareRevision = 1u << 4,
+ kSoftwareRevision = 1u << 5,
+ kSystemId = 1u << 6,
+ kRegulatoryInformation = 1u << 7,
+ kPnpId = 1u << 8,
+ };
+
+ // Manufacturer Name String
+ span<const std::byte> manufacturer_name;
+
+ // Model Number String
+ span<const std::byte> model_number;
+
+ // Serial Number String
+ span<const std::byte> serial_number;
+
+ // Hardware Revision String
+ span<const std::byte> hardware_revision;
+
+ // Firmware Revision String
+ span<const std::byte> firmware_revision;
+
+ // Software Revision String
+ span<const std::byte> software_revision;
+
+ // System ID
+ span<const std::byte> system_id;
+
+ // IEEE 11073-20601 Regulatory Certification Data List
+ span<const std::byte> regulatory_information;
+
+ // PnP ID
+ span<const std::byte> pnp_id;
+};
+
+// Helper operator| to allow combining multiple DeviceInfo::Field values.
+static inline constexpr DeviceInfo::Field operator|(DeviceInfo::Field left,
+ DeviceInfo::Field right) {
+ return static_cast<DeviceInfo::Field>(static_cast<uint16_t>(left) |
+ static_cast<uint16_t>(right));
+}
+
+static inline constexpr bool operator&(DeviceInfo::Field left,
+ DeviceInfo::Field right) {
+ return (static_cast<uint16_t>(left) & static_cast<uint16_t>(right)) != 0;
+}
+
+// Shared implementation of the DeviceInfoService<> template class of elements
+// that don't depend on the template parameters.
+class DeviceInfoServiceImpl {
+ public:
+ DeviceInfoServiceImpl(const bluetooth::gatt::LocalServiceInfo& service_info,
+ span<const span<const std::byte>> values)
+ : service_info_(service_info), delegate_(values) {}
+
+ // Publish this service on the passed gatt::Server. The service may be
+ // published only on one Server at a time.
+ void PublishService(
+ bluetooth::gatt::Server* gatt_server,
+ Callback<
+ void(bluetooth::Result<bluetooth::gatt::Server::PublishServiceError>)>
+ result_callback);
+
+ protected:
+ using GattCharacteristicUuid = bluetooth::GattCharacteristicUuid;
+
+ // A struct for describing each one of the optional characteristics available.
+ struct FieldDescriptor {
+ DeviceInfo::Field field_value;
+ span<const std::byte> DeviceInfo::*field_pointer;
+ bluetooth::Uuid characteristic_type;
+ };
+
+ // List of all the fields / characteristics available in the DIS, mapping the
+ // characteristic UUID type to the corresponding field in the DeviceInfo
+ // struct.
+ static constexpr size_t kNumFields = 9;
+ static constexpr std::array<FieldDescriptor, kNumFields>
+ kCharacteristicFields = {{
+ {DeviceInfo::Field::kManufacturerName,
+ &DeviceInfo::manufacturer_name,
+ GattCharacteristicUuid::kManufacturerNameString},
+ {DeviceInfo::Field::kModelNumber,
+ &DeviceInfo::model_number,
+ GattCharacteristicUuid::kModelNumberString},
+ {DeviceInfo::Field::kSerialNumber,
+ &DeviceInfo::serial_number,
+ GattCharacteristicUuid::kSerialNumberString},
+ {DeviceInfo::Field::kHardwareRevision,
+ &DeviceInfo::hardware_revision,
+ GattCharacteristicUuid::kHardwareRevisionString},
+ {DeviceInfo::Field::kFirmwareRevision,
+ &DeviceInfo::firmware_revision,
+ GattCharacteristicUuid::kFirmwareRevisionString},
+ {DeviceInfo::Field::kSoftwareRevision,
+ &DeviceInfo::software_revision,
+ GattCharacteristicUuid::kSoftwareRevisionString},
+ {DeviceInfo::Field::kSystemId,
+ &DeviceInfo::system_id,
+ GattCharacteristicUuid::kSystemId},
+ {DeviceInfo::Field::kRegulatoryInformation,
+ &DeviceInfo::regulatory_information,
+ GattCharacteristicUuid::
+ kIeee1107320601RegulatoryCertificationDataList},
+ {DeviceInfo::Field::kPnpId,
+ &DeviceInfo::pnp_id,
+ GattCharacteristicUuid::kPnpId},
+ }};
+
+ private:
+ class Delegate : public bluetooth::gatt::LocalServiceDelegate {
+ public:
+ explicit Delegate(span<const span<const std::byte>> values)
+ : values_(values) {}
+ ~Delegate() override = default;
+
+ void SetServicePtr(bluetooth::gatt::LocalService::Ptr service) {
+ local_service_ = std::move(service);
+ }
+
+ // LocalServiceDelegate overrides
+ void OnError(bluetooth::gatt::Error error) override;
+
+ void ReadValue(bluetooth::PeerId peer_id,
+ bluetooth::gatt::Handle handle,
+ uint32_t offset,
+ Function<void(bluetooth::Result<bluetooth::gatt::Error,
+ span<const std::byte>>)>&&
+ result_callback) override;
+
+ void WriteValue(bluetooth::PeerId /* peer_id */,
+ bluetooth::gatt::Handle /* handle */,
+ uint32_t /* offset */,
+ span<const std::byte> /* value */,
+ Function<void(bluetooth::Result<bluetooth::gatt::Error>)>&&
+ status_callback) override {
+ status_callback(bluetooth::gatt::Error::kUnlikelyError);
+ }
+
+ void CharacteristicConfiguration(bluetooth::PeerId /* peer_id */,
+ bluetooth::gatt::Handle /* handle */,
+ bool /* notify */,
+ bool /* indicate */) override {
+ // No indications or notifications are supported by this service.
+ }
+
+ void MtuUpdate(bluetooth::PeerId /* peer_id*/,
+ uint16_t /* mtu */) override {
+ // MTU is ignored.
+ }
+
+ private:
+ // LocalService smart pointer returned by the pw_bluetooth API once the
+ // service is published. This field is unused since we don't generate any
+ // Notification or Indication, but deleting this object unpublishes the
+ // service.
+ bluetooth::gatt::LocalService::Ptr local_service_;
+
+ // Device information values for the service_info_ characteristics. The
+ // characteristic Handle is the index into the values_ span.
+ span<const span<const std::byte>> values_;
+ };
+
+ // GATT service info.
+ const bluetooth::gatt::LocalServiceInfo& service_info_;
+
+ // Callback to be called after the service is published.
+ Callback<void(
+ bluetooth::Result<bluetooth::gatt::Server::PublishServiceError>)>
+ publish_service_callback_;
+
+ // The LocalServiceDelegate implementation.
+ Delegate delegate_;
+};
+
+// Device Information Service exposing only the subset of characteristics
+// specified by the bitmask kPresentFields.
+template <DeviceInfo::Field kPresentFields,
+ bluetooth::gatt::Handle kServiceHandle>
+class DeviceInfoService : public DeviceInfoServiceImpl {
+ public:
+ // Handle used to reference this service from other services.
+ static constexpr bluetooth::gatt::Handle kHandle = kServiceHandle;
+
+ // Construct a DeviceInfoService exposing the values provided in the
+ // `device_info` for the subset of characteristics selected by kPresentFields.
+ // DeviceInfo fields for characteristics not selected by kPresentFields are
+ // ignored. The `device_info` reference doesn't need to be kept alive after
+ // the constructor returns, however the data pointed to by the various fields
+ // in `device_info` must be kept available while the service is published.
+ explicit constexpr DeviceInfoService(const DeviceInfo& device_info)
+ : DeviceInfoServiceImpl(kServiceInfo, span{values_}) {
+ size_t count = 0;
+ // Get the device information only for the fields we care about.
+ for (const auto& field : kCharacteristicFields) {
+ if (field.field_value & kPresentFields) {
+ values_[count] = device_info.*(field.field_pointer);
+ count++;
+ }
+ }
+ }
+
+ private:
+ // Return the total number of selected characteristics on this service.
+ static constexpr size_t NumCharacteristics() {
+ size_t ret = 0;
+ for (const auto& field : kCharacteristicFields) {
+ if (field.field_value & kPresentFields) {
+ ret++;
+ }
+ }
+ return ret;
+ }
+ static constexpr size_t kNumCharacteristics = NumCharacteristics();
+
+ // Factory method to build the list of characteristics needed for a given
+ // subset of fields.
+ static constexpr std::array<bluetooth::gatt::Characteristic,
+ kNumCharacteristics>
+ BuildServiceInfoCharacteristics() {
+ std::array<bluetooth::gatt::Characteristic, kNumCharacteristics> ret = {};
+ size_t count = 0;
+ for (const auto& field : kCharacteristicFields) {
+ if (field.field_value & kPresentFields) {
+ ret[count] = bluetooth::gatt::Characteristic{
+ .handle = bluetooth::gatt::Handle(count),
+ .type = field.characteristic_type,
+ .properties = bluetooth::gatt::CharacteristicPropertyBits::kRead,
+ .permissions = bluetooth::gatt::AttributePermissions{},
+ .descriptors = {},
+ };
+ count++;
+ }
+ }
+ return ret;
+ }
+ // Static constexpr array of characteristics for the current subset of fields
+ // kPresentFields.
+ static constexpr auto kServiceInfoCharacteristics =
+ BuildServiceInfoCharacteristics();
+
+ // GATT Service information.
+ static constexpr auto kServiceInfo = bluetooth::gatt::LocalServiceInfo{
+ .handle = kServiceHandle,
+ .primary = true,
+ .type = bluetooth::GattServiceUuid::kDeviceInformation,
+ .characteristics = kServiceInfoCharacteristics,
+ .includes = {},
+ };
+
+ // Storage std::array for the span<const std::byte> of values.
+ std::array<span<const std::byte>, kNumCharacteristics> values_;
+};
+
+} // namespace pw::bluetooth_profiles
diff --git a/pw_boot/BUILD.bazel b/pw_boot/BUILD.bazel
index 887191972..ec4bac8e9 100644
--- a/pw_boot/BUILD.bazel
+++ b/pw_boot/BUILD.bazel
@@ -40,3 +40,14 @@ pw_cc_library(
"@pigweed_config//:pw_boot_backend",
],
)
+
+pw_cc_library(
+ name = "backend_multiplexer",
+ deps = select({
+ "@platforms//cpu:armv7-m": ["//pw_boot_cortex_m:pw_boot_cortex_m"],
+ "@platforms//cpu:armv7e-m": ["//pw_boot_cortex_m:pw_boot_cortex_m"],
+ "@platforms//cpu:armv8-m": ["//pw_boot_cortex_m:pw_boot_cortex_m"],
+ # When building for the host, we don't need a pw_boot implementation.
+ "//conditions:default": [],
+ }),
+)
diff --git a/pw_boot/BUILD.gn b/pw_boot/BUILD.gn
index 0082ad77d..71ad75b29 100644
--- a/pw_boot/BUILD.gn
+++ b/pw_boot/BUILD.gn
@@ -32,3 +32,6 @@ pw_facade("pw_boot") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_boot/docs.rst b/pw_boot/docs.rst
index 23ea54a83..8be00c777 100644
--- a/pw_boot/docs.rst
+++ b/pw_boot/docs.rst
@@ -13,8 +13,9 @@ This module is split into two components:
2. The backend, provided elsewhere, that provides the implementation
.. warning::
- This module is currently NOT stable! Depending on this module may cause
- breakages as this module is updated.
+
+ This module is currently NOT stable! Depending on this module may cause
+ breakages as this module is updated.
Sequence
========
@@ -23,16 +24,16 @@ invocation of the user-implemented functions:
.. code:: cpp
- void pw_boot_Entry() { // Boot entry point provided by backend.
- pw_boot_PreStaticMemoryInit(); // User-implemented function.
- // Static memory initialization.
- pw_boot_PreStaticConstructorInit(); // User-implemented function.
- // C++ static constructors are invoked.
- pw_boot_PreMainInit(); // User-implemented function.
- main(); // User-implemented function.
- pw_boot_PostMain(); // User-implemented function.
- PW_UNREACHABLE;
- }
+ void pw_boot_Entry() { // Boot entry point provided by backend.
+ pw_boot_PreStaticMemoryInit(); // User-implemented function.
+ // Static memory initialization.
+ pw_boot_PreStaticConstructorInit(); // User-implemented function.
+ // C++ static constructors are invoked.
+ pw_boot_PreMainInit(); // User-implemented function.
+ main(); // User-implemented function.
+ pw_boot_PostMain(); // User-implemented function.
+ PW_UNREACHABLE;
+ }
Setup
=====
@@ -42,50 +43,51 @@ User-Implemented Functions
This module expects all of these extern "C" functions to be defined outside this
module:
- - ``int main()``: This is where applications reside.
- - ``void pw_boot_PreStaticMemoryInit()``: This function executes just before
- static memory has been zeroed and static data is intialized. This function
- should set up any early initialization that should be done before static
- memory is initialized, such as:
-
- - Enabling the FPU or other coprocessors.
- - Opting into extra restrictions such as disabling unaligned access to ensure
- the restrictions are active during static RAM initialization.
- - Initial CPU clock, flash, and memory configurations including potentially
- enabling extra memory regions with .bss and .data sections, such as SDRAM
- or backup powered SRAM.
- - Fault handler initialization if required before static memory
- initialization.
-
- .. warning::
+- ``int main()``: This is where applications reside.
+- ``void pw_boot_PreStaticMemoryInit()``: This function executes just before
+ static memory has been zeroed and static data is intialized. This function
+ should set up any early initialization that should be done before static
+ memory is initialized, such as:
+
+ - Enabling the FPU or other coprocessors.
+ - Opting into extra restrictions such as disabling unaligned access to ensure
+ the restrictions are active during static RAM initialization.
+ - Initial CPU clock, flash, and memory configurations including potentially
+ enabling extra memory regions with .bss and .data sections, such as SDRAM
+ or backup powered SRAM.
+ - Fault handler initialization if required before static memory
+ initialization.
+
+ .. warning::
+
Code running in this hook is violating the C spec as static values are not
yet initialized, meaning they have not been loaded (.data) nor
zero-initialized (.bss).
- - ``void pw_boot_PreStaticConstructorInit()``: This function executes just
- before C++ static constructors are called. At this point, other static memory
- has been zero or data initialized. This function should set up any early
- initialization that should be done before C++ static constructors are run,
- such as:
+- ``void pw_boot_PreStaticConstructorInit()``: This function executes just
+ before C++ static constructors are called. At this point, other static memory
+ has been zero or data initialized. This function should set up any early
+ initialization that should be done before C++ static constructors are run,
+ such as:
- - Run time dependencies such as Malloc, and ergo sometimes the RTOS.
- - Persistent memory that survives warm reboots.
- - Enabling the MPU to catch nullptr dereferences during construction.
- - Main stack watermarking.
- - Further fault handling configuration necessary for your platform which
- were not safe before pw_boot_PreStaticRamInit().
- - Boot count and/or boot session UUID management.
+ - Run time dependencies such as Malloc, and ergo sometimes the RTOS.
+ - Persistent memory that survives warm reboots.
+ - Enabling the MPU to catch nullptr dereferences during construction.
+ - Main stack watermarking.
+ - Further fault handling configuration necessary for your platform which
+ were not safe before pw_boot_PreStaticRamInit().
+ - Boot count and/or boot session UUID management.
- - ``void pw_boot_PreMainInit()``: This function executes just before main, and
- can be used for any device initialization that isn't application specific.
- Depending on your platform, this might be turning on a UART, setting up
- default clocks, etc.
+- ``void pw_boot_PreMainInit()``: This function executes just before main, and
+ can be used for any device initialization that isn't application specific.
+ Depending on your platform, this might be turning on a UART, setting up
+ default clocks, etc.
- - ``PW_NO_RETURN void pw_boot_PostMain()``: This function executes after main
- has returned. This could be used for device specific teardown such as an
- infinite loop, soft reset, or QEMU shutdown. In addition, if relevant for
- your application, this would be the place to invoke the global static
- destructors. This function must not return!
+- ``PW_NO_RETURN void pw_boot_PostMain()``: This function executes after main
+ has returned. This could be used for device specific teardown such as an
+ infinite loop, soft reset, or QEMU shutdown. In addition, if relevant for your
+ application, this would be the place to invoke the global static
+ destructors. This function must not return!
If any of these functions are unimplemented, executables will encounter a link
@@ -95,10 +97,10 @@ Backend-Implemented Functions
-----------------------------
Backends for this module must implement the following extern "C" function:
- - ``void pw_boot_Entry()``: This function executes as the entry point for
- the application, and must perform call the user defined methods in the
- appropriate sequence for the target (see Sequence above).
+- ``void pw_boot_Entry()``: This function executes as the entry point for the
+ application, and must perform call the user defined methods in the appropriate
+ sequence for the target (see Sequence above).
Dependencies
============
- * ``pw_preprocessor`` module
+- :bdg-ref-primary-line:`module-pw_preprocessor`
diff --git a/pw_boot_cortex_m/BUILD.bazel b/pw_boot_cortex_m/BUILD.bazel
index 362b0f074..9e35ea419 100644
--- a/pw_boot_cortex_m/BUILD.bazel
+++ b/pw_boot_cortex_m/BUILD.bazel
@@ -20,6 +20,10 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+# Exported because this file may be preprocessed with different defines for
+# different targets (platforms).
+exports_files(["basic_cortex_m.ld"])
+
pw_cc_library(
name = "pw_boot_cortex_m",
srcs = [
diff --git a/pw_boot_cortex_m/BUILD.gn b/pw_boot_cortex_m/BUILD.gn
index d28da4fee..864776671 100644
--- a/pw_boot_cortex_m/BUILD.gn
+++ b/pw_boot_cortex_m/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_boot/backend.gni")
import("$dir_pw_build/linker_script.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# This list should contain the necessary defines for setting pw_boot linker
@@ -73,3 +74,6 @@ if (pw_boot_BACKEND != "$dir_pw_boot_cortex_m" &&
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_boot_cortex_m/basic_cortex_m.ld b/pw_boot_cortex_m/basic_cortex_m.ld
index a14a20423..ae84e3e6f 100644
--- a/pw_boot_cortex_m/basic_cortex_m.ld
+++ b/pw_boot_cortex_m/basic_cortex_m.ld
@@ -65,7 +65,7 @@ ENTRY(pw_boot_Entry)
MEMORY
{
- /* TODO(pwbug/57): Make it possible for projects to freely customize
+ /* TODO(b/234892223): Make it possible for projects to freely customize
* memory regions.
*/
@@ -81,6 +81,26 @@ MEMORY
RAM(rwx) : \
ORIGIN = PW_BOOT_RAM_BEGIN, \
LENGTH = PW_BOOT_RAM_SIZE
+
+ /* Each memory region above has an associated .*.unused_space section that
+ * overlays the unused space at the end of the memory segment. These segments
+ * are used by pw_bloat.bloaty_config to create the utilization data source
+ * for bloaty size reports.
+ *
+ * These sections MUST be located immediately after the last section that is
+ * placed in the respective memory region or lld will issue a warning like:
+ *
+ * warning: ignoring memory region assignment for non-allocatable section
+ * '.VECTOR_TABLE.unused_space'
+ *
+ * If this warning occurs, it's also likely that LLD will have created quite
+ * large padded regions in the ELF file due to bad cursor operations. This
+ * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
+ * megabytes.
+ *
+ * Attempting to add sections to the memory region AFTER the unused_space
+ * section will cause the region to overflow.
+ */
}
SECTIONS
@@ -89,7 +109,13 @@ SECTIONS
* Register) MUST point to this memory location in order to be used. This can
* be done by ensuring this section exists at the default location of the VTOR
* so it's used on reset, or by explicitly setting the VTOR in a bootloader
- * manually to point to &pw_boot_vector_table_addr before interrupts are enabled.
+ * manually to point to &pw_boot_vector_table_addr before interrupts are
+ * enabled.
+ *
+ * The ARMv7-M architecture requires this is at least aligned to 128 bytes,
+ * and aligned to a power of two that is greater than 4 times the number of
+ * supported exceptions. 512 has been selected as it accommodates most
+ * devices' vector tables.
*/
.vector_table : ALIGN(512)
{
@@ -97,17 +123,25 @@ SECTIONS
KEEP(*(.vector_table))
} >VECTOR_TABLE
+ /* Represents unused space in the VECTOR_TABLE segment. This MUST be the last
+ * section assigned to the VECTOR_TABLE region.
+ */
+ .VECTOR_TABLE.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE));
+ } >VECTOR_TABLE
+
/* Main executable code. */
- .code : ALIGN(8)
+ .code : ALIGN(4)
{
- . = ALIGN(8);
+ . = ALIGN(4);
/* Application code. */
*(.text)
*(.text*)
KEEP(*(.init))
KEEP(*(.fini))
- . = ALIGN(8);
+ . = ALIGN(4);
/* Constants.*/
*(.rodata)
*(.rodata*)
@@ -118,7 +152,7 @@ SECTIONS
* Since the region isn't explicitly referenced, specify KEEP to prevent
* link-time garbage collection. SORT is used for sections that have strict
* init/de-init ordering requirements. */
- . = ALIGN(8);
+ . = ALIGN(4);
PROVIDE_HIDDEN(__preinit_array_start = .);
KEEP(*(.preinit_array*))
PROVIDE_HIDDEN(__preinit_array_end = .);
@@ -135,39 +169,63 @@ SECTIONS
} >FLASH
/* Used by unwind-arm/ */
- .ARM : ALIGN(8) {
+ .ARM : ALIGN(4) {
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} >FLASH
/* Explicitly initialized global and static data. (.data)*/
- .static_init_ram : ALIGN(8)
+ .static_init_ram : ALIGN(4)
{
*(.data)
*(.data*)
- . = ALIGN(8);
+ . = ALIGN(4);
} >RAM AT> FLASH
- /* Zero initialized global/static data. (.bss)
- * This section is zero initialized in pw_boot_Entry(). */
- .zero_init_ram : ALIGN(8)
+ /* Represents unused space in the FLASH segment. This MUST be the last section
+ * assigned to the FLASH region.
+ */
+ .FLASH.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(FLASH) + LENGTH(FLASH));
+ } >FLASH
+
+ /* The .zero_init_ram, .heap, and .stack sections below require (NOLOAD)
+ * annotations for LLVM lld, but not GNU ld, because LLVM's lld intentionally
+ * interprets the linker file differently from ld:
+ *
+ * https://discourse.llvm.org/t/lld-vs-ld-section-type-progbits-vs-nobits/5999/3
+ *
+ * Zero initialized global/static data (.bss) is initialized in
+ * pw_boot_Entry() via memset(), so the section doesn't need to be loaded from
+ * flash. The .heap and .stack sections don't require any initialization,
+ * as they only represent allocated memory regions, so they also do not need
+ * to be loaded.
+ */
+ .zero_init_ram (NOLOAD) : ALIGN(4)
{
*(.bss)
*(.bss*)
*(COMMON)
- . = ALIGN(8);
+ . = ALIGN(4);
} >RAM
- .heap : ALIGN(8)
+ .heap (NOLOAD) : ALIGN(4)
{
pw_boot_heap_low_addr = .;
. = . + PW_BOOT_HEAP_SIZE;
- . = ALIGN(8);
+ . = ALIGN(4);
pw_boot_heap_high_addr = .;
} >RAM
- /* Link-time check for stack overlaps. */
+ /* Link-time check for stack overlaps.
+ *
+ * The ARMv7-M architecture may require 8-byte alignment of the stack pointer
+ * rather than 4 in some contexts and implementations, so this region is
+ * 8-byte aligned (see ARMv7-M Architecture Reference Manual DDI0403E
+ * section B1.5.7).
+ */
.stack (NOLOAD) : ALIGN(8)
{
/* Set the address that the main stack pointer should be initialized to. */
@@ -181,32 +239,19 @@ SECTIONS
pw_boot_stack_high_addr = .;
} >RAM
+ /* Represents unused space in the RAM segment. This MUST be the last section
+ * assigned to the RAM region.
+ */
+ .RAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(RAM) + LENGTH(RAM));
+ } >RAM
+
/* Discard unwind info. */
.ARM.extab 0x0 (INFO) :
{
KEEP(*(.ARM.extab*))
}
-
- /*
- * Do not declare any output sections after this comment. This area is
- * reserved only for declaring unused sections of memory. These sections are
- * used by pw_bloat.bloaty_config to create the utilization data source for
- * bloaty.
- */
- .VECTOR_TABLE.unused_space (NOLOAD) : ALIGN(8)
- {
- . = ABSOLUTE(ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE));
- } >VECTOR_TABLE
-
- .FLASH.unused_space (NOLOAD) : ALIGN(8)
- {
- . = ABSOLUTE(ORIGIN(FLASH) + LENGTH(FLASH));
- } >FLASH
-
- .RAM.unused_space (NOLOAD) : ALIGN(8)
- {
- . = ABSOLUTE(ORIGIN(RAM) + LENGTH(RAM));
- } >RAM
}
/* Symbols used by core_init.c: */
diff --git a/pw_boot_cortex_m/bloaty_config.bloaty b/pw_boot_cortex_m/bloaty_config.bloaty
index 07401a00d..ff3cef78c 100644
--- a/pw_boot_cortex_m/bloaty_config.bloaty
+++ b/pw_boot_cortex_m/bloaty_config.bloaty
@@ -18,12 +18,19 @@ custom_data_source: {
name: "segment_names"
base_data_source: "segments"
+ # Segment containing .code section.
rewrite: {
pattern: "LOAD #1"
replacement: "FLASH"
}
+ # Segment containing .static_init_ram section.
rewrite: {
pattern: "LOAD #2"
replacement: "RAM"
}
+ # Segment containing .zero_init_ram section.
+ rewrite: {
+ pattern: "LOAD #4"
+ replacement: "RAM"
+ }
}
diff --git a/pw_boot_cortex_m/core_init.c b/pw_boot_cortex_m/core_init.c
index cbb8af3af..d2d6db3e4 100644
--- a/pw_boot_cortex_m/core_init.c
+++ b/pw_boot_cortex_m/core_init.c
@@ -107,7 +107,7 @@ void StaticMemoryInit(void) {
//
// This function runs immediately at boot because it is at index 1 of the
// interrupt vector table.
-void pw_boot_Entry() {
+void pw_boot_Entry(void) {
// Disable interrupts.
//
// Until pw_boot_PreStaticMemoryInit() has completed, depending on the
diff --git a/pw_boot_cortex_m/docs.rst b/pw_boot_cortex_m/docs.rst
index 73ba80dd5..bff056416 100644
--- a/pw_boot_cortex_m/docs.rst
+++ b/pw_boot_cortex_m/docs.rst
@@ -11,16 +11,17 @@ get many ARMv7-M and ARMv8-M cores booted and ready to run C++ code.
This module is currently designed to support a very minimal device memory layout
configuration:
- - One contiguous region for RAM.
- - One contiguous region for flash.
- - Static, in-flash vector table at the default location expected by the SoC.
+- One contiguous region for RAM.
+- One contiguous region for flash.
+- Static, in-flash vector table at the default location expected by the SoC.
Note that this module is not yet particularly suited for projects that utilize
a bootloader, as it's relatively opinionated regarding where code is stored.
.. warning::
- This module is currently NOT stable! Depending on this module may cause
- breakages as this module is updated.
+
+ This module is currently NOT stable! Depending on this module may cause
+ breakages as this module is updated.
Sequence
========
@@ -30,18 +31,18 @@ pseudo-code invocation of the user-implemented functions:
.. code:: cpp
- void pw_boot_Entry() { // Boot entry point.
- // Interrupts disabled.
- pw_boot_PreStaticMemoryInit(); // User-implemented function.
- // Static memory initialization.
- // Interrupts enabled.
- pw_boot_PreStaticConstructorInit(); // User-implemented function.
- // C++ static constructors are invoked.
- pw_boot_PreMainInit(); // User-implemented function.
- main(); // User-implemented function.
- pw_boot_PostMain(); // User-implemented function.
- PW_UNREACHABLE;
- }
+ void pw_boot_Entry() { // Boot entry point.
+ // Interrupts disabled.
+ pw_boot_PreStaticMemoryInit(); // User-implemented function.
+ // Static memory initialization.
+ // Interrupts enabled.
+ pw_boot_PreStaticConstructorInit(); // User-implemented function.
+ // C++ static constructors are invoked.
+ pw_boot_PreMainInit(); // User-implemented function.
+ main(); // User-implemented function.
+ pw_boot_PostMain(); // User-implemented function.
+ PW_UNREACHABLE;
+ }
Setup
=====
@@ -51,65 +52,66 @@ Processor Selection
Set the ``pw_boot_BACKEND`` variable to the appropriate target for the processor
in use.
- - ``pw_boot_cortex_m:armv7m`` for ARMv7-M cores.
+- ``pw_boot_cortex_m:armv7m`` for ARMv7-M cores.
- - ``pw_boot_cortex_m:armv8m`` for ARMv8-M cores. This sets the MSPLIM register
- so that the main stack pointer (MSP) cannot descend outside the bounds of the
- main stack defined in the linker script. The MSP of the entry point is also
- adjusted to be within the bounds.
+- ``pw_boot_cortex_m:armv8m`` for ARMv8-M cores. This sets the MSPLIM register
+ so that the main stack pointer (MSP) cannot descend outside the bounds of the
+ main stack defined in the linker script. The MSP of the entry point is also
+ adjusted to be within the bounds.
User-Implemented Functions
--------------------------
This module expects all of these extern "C" functions to be defined outside this
module:
- - ``int main()``: This is where applications reside.
- - ``void pw_boot_PreStaticMemoryInit()``: This function executes just before
- static memory has been zeroed and static data is intialized. This function
- should set up any early initialization that should be done before static
- memory is initialized, such as:
-
- - Setup the interrupt vector table and VTOR if required.
- - Enabling the FPU or other coprocessors.
- - Opting into extra restrictions such as disabling unaligned access to ensure
- the restrictions are active during static RAM initialization.
- - Initial CPU clock, flash, and memory configurations including potentially
- enabling extra memory regions with .bss and .data sections, such as SDRAM
- or backup powered SRAM.
- - Fault handler initialization if required before static memory
- initialization.
-
- .. warning::
+- ``int main()``: This is where applications reside.
+- ``void pw_boot_PreStaticMemoryInit()``: This function executes just before
+ static memory has been zeroed and static data is intialized. This function
+ should set up any early initialization that should be done before static
+ memory is initialized, such as:
+
+ - Setup the interrupt vector table and VTOR if required.
+ - Enabling the FPU or other coprocessors.
+ - Opting into extra restrictions such as disabling unaligned access to ensure
+ the restrictions are active during static RAM initialization.
+ - Initial CPU clock, flash, and memory configurations including potentially
+ enabling extra memory regions with .bss and .data sections, such as SDRAM
+ or backup powered SRAM.
+ - Fault handler initialization if required before static memory
+ initialization.
+
+ .. warning::
+
Code running in this hook is violating the C spec as static values are not
yet initialized, meaning they have not been loaded (.data) nor
zero-initialized (.bss).
Interrupts are disabled until after this function returns.
- - ``void pw_boot_PreStaticConstructorInit()``: This function executes just
- before C++ static constructors are called. At this point, other static memory
- has been zero or data initialized. This function should set up any early
- initialization that should be done before C++ static constructors are run,
- such as:
+- ``void pw_boot_PreStaticConstructorInit()``: This function executes just
+ before C++ static constructors are called. At this point, other static memory
+ has been zero or data initialized. This function should set up any early
+ initialization that should be done before C++ static constructors are run,
+ such as:
- - Run time dependencies such as Malloc, and ergo sometimes the RTOS.
- - Persistent memory that survives warm reboots.
- - Enabling the MPU to catch nullptr dereferences during construction.
- - Main stack watermarking.
- - Further fault handling configuration necessary for your platform which
- were not safe before pw_boot_PreStaticRamInit().
- - Boot count and/or boot session UUID management.
+ - Run time dependencies such as Malloc, and ergo sometimes the RTOS.
+ - Persistent memory that survives warm reboots.
+ - Enabling the MPU to catch nullptr dereferences during construction.
+ - Main stack watermarking.
+ - Further fault handling configuration necessary for your platform which were
+ not safe before pw_boot_PreStaticRamInit().
+ - Boot count and/or boot session UUID management.
- - ``void pw_boot_PreMainInit()``: This function executes just before main, and
- can be used for any device initialization that isn't application specific.
- Depending on your platform, this might be turning on a UART, setting up
- default clocks, etc.
+- ``void pw_boot_PreMainInit()``: This function executes just before main, and
+ can be used for any device initialization that isn't application specific.
+ Depending on your platform, this might be turning on a UART, setting up
+ default clocks, etc.
- - ``PW_NO_RETURN void pw_boot_PostMain()``: This function executes after main
- has returned. This could be used for device specific teardown such as an
- infinite loop, soft reset, or QEMU shutdown. In addition, if relevant for
- your application, this would be the place to invoke the global static
- destructors. This function must not return!
+- ``PW_NO_RETURN void pw_boot_PostMain()``: This function executes after main
+ has returned. This could be used for device specific teardown such as an
+ infinite loop, soft reset, or QEMU shutdown. In addition, if relevant for your
+ application, this would be the place to invoke the global static
+ destructors. This function must not return!
If any of these functions are unimplemented, executables will encounter a link
@@ -137,26 +139,26 @@ Example vector table:
.. code-block:: cpp
- typedef void (*InterruptHandler)();
+ typedef void (*InterruptHandler)();
- PW_KEEP_IN_SECTION(".vector_table")
- const InterruptHandler vector_table[] = {
- // The starting location of the stack pointer.
- // This address is NOT an interrupt handler/function pointer, it is simply
- // the address that the main stack pointer should be initialized to. The
- // value is reinterpret casted because it needs to be in the vector table.
- [0] = reinterpret_cast<InterruptHandler>(&pw_boot_stack_high_addr),
+ PW_KEEP_IN_SECTION(".vector_table")
+ const InterruptHandler vector_table[] = {
+ // The starting location of the stack pointer.
+ // This address is NOT an interrupt handler/function pointer, it is simply
+ // the address that the main stack pointer should be initialized to. The
+ // value is reinterpret casted because it needs to be in the vector table.
+ [0] = reinterpret_cast<InterruptHandler>(&pw_boot_stack_high_addr),
- // Reset handler, dictates how to handle reset interrupt. This is the
- // address that the Program Counter (PC) is initialized to at boot.
- [1] = pw_boot_Entry,
+ // Reset handler, dictates how to handle reset interrupt. This is the
+ // address that the Program Counter (PC) is initialized to at boot.
+ [1] = pw_boot_Entry,
- // NMI handler.
- [2] = DefaultFaultHandler,
- // HardFault handler.
- [3] = DefaultFaultHandler,
- ...
- };
+ // NMI handler.
+ [2] = DefaultFaultHandler,
+ // HardFault handler.
+ [3] = DefaultFaultHandler,
+ ...
+ };
Usage
=====
@@ -220,4 +222,4 @@ as part of a Pigweed target configuration.
Dependencies
============
- * ``pw_preprocessor`` module
+- :bdg-ref-primary-line:`module-pw_preprocessor`
diff --git a/pw_build/BUILD.bazel b/pw_build/BUILD.bazel
index 276122752..49e59cdbb 100644
--- a/pw_build/BUILD.bazel
+++ b/pw_build/BUILD.bazel
@@ -22,3 +22,10 @@ config_setting(
"define": "kythe_corpus=pigweed.googlesource.com/pigweed/pigweed",
},
)
+
+# TODO(b/238339027): Run cc_blob_library_test when pw_cc_blob_library is
+# supported in Bazel.
+filegroup(
+ name = "cc_blob_library_test",
+ srcs = ["cc_blob_library_test.cc"],
+)
diff --git a/pw_build/BUILD.gn b/pw_build/BUILD.gn
index 1a74c2bad..25f53433a 100644
--- a/pw_build/BUILD.gn
+++ b/pw_build/BUILD.gn
@@ -14,9 +14,12 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/cc_blob_library.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/relative_source_file_names.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_toolchain/traits.gni")
+import("$dir_pw_unit_test/test.gni")
import("target_types.gni")
# IMPORTANT: The compilation flags in this file must be kept in sync with
@@ -61,6 +64,11 @@ config("optimize_size") {
ldflags = cflags
}
+config("optimize_size_clang") {
+ cflags = [ "-Oz" ]
+ ldflags = cflags
+}
+
# Standard compiler flags to reduce output binary size.
config("reduced_size") {
cflags = [
@@ -80,6 +88,18 @@ config("reduced_size") {
}
}
+config("rust_edition_2015") {
+ rustflags = [ "--edition=2015" ]
+}
+
+config("rust_edition_2018") {
+ rustflags = [ "--edition=2018" ]
+}
+
+config("rust_edition_2021") {
+ rustflags = [ "--edition=2021" ]
+}
+
config("strict_warnings") {
cflags = [
"-Wall",
@@ -114,6 +134,52 @@ config("extra_strict_warnings") {
cflags_c = [ "-Wstrict-prototypes" ]
}
+# This config contains warnings that are enabled for upstream Pigweed.
+# This config MUST NOT be used downstream to allow for warnings to be
+# added in the future without breaking downstream.
+config("internal_strict_warnings") {
+ cflags = [ "-Wswitch-enum" ]
+ cflags_cc = [ "-Wextra-semi" ]
+
+ # TODO(b/243069432): Enable pedantic warnings on Windows when they pass.
+ if (host_os != "win") {
+ configs = [ ":pedantic_warnings" ]
+ }
+}
+
+config("pedantic_warnings") {
+ cflags = [
+ # Enable -Wpedantic, but disable a few warnings.
+ "-Wpedantic",
+
+ # Allow designated initializers, which were added in C++20 but widely
+ # supported prior and permitted by the Google style guide.
+ "-Wno-c++20-designator",
+
+ # Allow empty ... arguments in macros, which are permitted in C++20 but
+ # widely supported prior.
+ "-Wno-gnu-zero-variadic-macro-arguments",
+ ]
+
+ if (pw_toolchain_CXX_STANDARD < pw_toolchain_STANDARD.CXX17) {
+ # Allow C++17 attributes in C++14, since Pigweed targets C++17 or newer.
+ cflags += [ "-Wno-c++17-attribute-extensions" ]
+ }
+}
+
+# Numeric conversion warnings.
+#
+# Originally Pigweed didn't enable this, but we ultimately decided to turn it
+# on since it caused issues with downstream project that enable this warning.
+#
+# b/259746255 tracks converting everything to build with this warning.
+config("conversion_warnings") {
+ # TODO(b/260629756): Remove Windows restriction once fixed for Windows + GCC.
+ if (host_os != "win") {
+ cflags = [ "-Wconversion" ]
+ }
+}
+
config("cpp14") {
cflags_cc = [ "-std=c++14" ]
}
@@ -127,6 +193,29 @@ config("cpp17") {
]
}
+config("cpp20") {
+ cflags_cc = [
+ "-std=c++20",
+ "-Wno-register",
+ ]
+}
+
+# Selects the C++ standard to used based on the pw_toolchain_CXX_STANDARD
+# toolchain trait.
+config("toolchain_cpp_standard") {
+ if (pw_toolchain_CXX_STANDARD == pw_toolchain_STANDARD.CXX14) {
+ configs = [ ":cpp14" ]
+ } else if (pw_toolchain_CXX_STANDARD == pw_toolchain_STANDARD.CXX17) {
+ configs = [ ":cpp17" ]
+ } else if (pw_toolchain_CXX_STANDARD == pw_toolchain_STANDARD.CXX20) {
+ configs = [ ":cpp20" ]
+ } else {
+ assert(false,
+ "Unexpected pw_toolchain_CXX_STANDARD value: " +
+ pw_toolchain_CXX_STANDARD)
+ }
+}
+
# Removes system-dependent prefixes from macros like __FILE__ and debug symbols.
config("relative_paths") {
_transformations = [
@@ -178,24 +267,41 @@ group("link_deps") {
group("empty") {
}
-# Requirements for the pw_python_package lint targets.
-pw_python_requirements("python_lint") {
- requirements = [
- "build",
-
- # NOTE: mypy needs to stay in sync with mypy-protobuf
- # Currently using mypy 0.910 and mypy-protobuf 2.9
- "mypy==0.910",
-
- # typeshed packages (required by mypy > 0.9)
- "types-setuptools",
- "pylint==2.9.3",
- ]
-}
-
pw_doc_group("docs") {
sources = [
"docs.rst",
"python.rst",
]
}
+
+# Pigweed upstream does not use the default toolchain, but other projects may
+# use it. To support using pw_build from the default toolchain without fully
+# configuring the Pigweed build, only instantiate the pw_build tests for a
+# non-default toolchain.
+if (current_toolchain != default_toolchain) {
+ pw_test("cc_blob_library_test") {
+ sources = [ "cc_blob_library_test.cc" ]
+ deps = [ ":test_blob" ]
+ }
+
+ pw_cc_blob_library("test_blob") {
+ out_header = "pw_build/test_blob.h"
+ namespace = "test::ns"
+ blobs = [
+ {
+ file_path = "test_blob_0123.bin"
+ symbol_name = "kFirstBlob0123"
+ alignas = 512
+ },
+ {
+ file_path = "test_blob_0123.bin"
+ symbol_name = "kSecondBlob0123"
+ },
+ ]
+ visibility = [ ":*" ]
+ }
+
+ pw_test_group("tests") {
+ tests = [ ":cc_blob_library_test" ]
+ }
+}
diff --git a/pw_build/CMakeLists.txt b/pw_build/CMakeLists.txt
index 07b44a217..62a310549 100644
--- a/pw_build/CMakeLists.txt
+++ b/pw_build/CMakeLists.txt
@@ -15,19 +15,19 @@
# IMPORTANT: The compilation flags in this file must be kept in sync with
# the GN flags //pw_build/BUILD.gn.
+include("$ENV{PW_ROOT}/pw_build/cc_blob_library.cmake")
+include("$ENV{PW_ROOT}/pw_build/pigweed.cmake")
+
# Target that specifies the standard Pigweed build options.
-add_library(pw_build INTERFACE)
-target_compile_options(pw_build INTERFACE "-g")
-target_link_libraries(pw_build
- INTERFACE
- pw_build.reduced_size
- pw_build.cpp17
-)
-target_compile_options(pw_build
- INTERFACE
+pw_add_library_generic(pw_build INTERFACE
+ PUBLIC_COMPILE_OPTIONS
+ -g
# Force the compiler use colorized output. This is required for Ninja.
$<$<CXX_COMPILER_ID:Clang>:-fcolor-diagnostics>
$<$<CXX_COMPILER_ID:GNU>:-fdiagnostics-color=always>
+ PUBLIC_DEPS
+ pw_build.reduced_size
+ pw_build.cpp17
)
if(ZEPHYR_PIGWEED_MODULE_DIR)
target_link_libraries(pw_build INTERFACE zephyr_interface)
@@ -41,9 +41,8 @@ add_custom_target(pw_tests DEPENDS pw_tests.default)
add_custom_target(pw_run_tests DEPENDS pw_run_tests.default)
# Define the standard Pigweed compile options.
-add_library(pw_build.reduced_size INTERFACE)
-target_compile_options(pw_build.reduced_size
- INTERFACE
+pw_add_library_generic(pw_build.reduced_size INTERFACE
+ PUBLIC_COMPILE_OPTIONS
"-fno-common"
"-fno-exceptions"
"-ffunction-sections"
@@ -63,18 +62,24 @@ target_compile_options(pw_build.reduced_size
set(pw_build_WARNINGS pw_build.strict_warnings
CACHE STRING "Warnings libraries to use for Pigweed upstream code")
-add_library(pw_build.warnings INTERFACE)
-target_link_libraries(pw_build.warnings INTERFACE ${pw_build_WARNINGS})
+pw_add_library_generic(pw_build.warnings INTERFACE
+ PUBLIC_DEPS
+ ${pw_build_WARNINGS}
+)
# TODO(hepler): These Zephyr exceptions should be made by overriding
# pw_build_WARNINGS.
-add_library(pw_build.strict_warnings INTERFACE)
-if(NOT ZEPHYR_PIGWEED_MODULE_DIR)
+if(ZEPHYR_PIGWEED_MODULE_DIR)
+ # -Wtype-limits is incompatible with Kconfig at times, disable it for Zephyr
+ # builds.
+ set(strict_warnings_cond "-Wno-type-limits")
+else()
# Only include these flags if we're not building with Zephyr.
- set(strict_warnings_cond "-Wcast-qual" "-Wundef")
+ set(strict_warnings_cond "-Wundef")
endif()
-target_compile_options(pw_build.strict_warnings
- INTERFACE
+
+pw_add_library_generic(pw_build.strict_warnings INTERFACE
+ PUBLIC_COMPILE_OPTIONS
"-Wall"
"-Wextra"
"-Wimplicit-fallthrough"
@@ -89,26 +94,63 @@ target_compile_options(pw_build.strict_warnings
$<$<COMPILE_LANGUAGE:CXX>:-Wnon-virtual-dtor>
)
-add_library(pw_build.extra_strict_warnings INTERFACE)
if(NOT ZEPHYR_PIGWEED_MODULE_DIR)
# Only include these flags if we're not building with Zephyr.
set(extra_strict_warnings_cond "-Wredundant-decls")
endif()
-target_compile_options(pw_build.extra_strict_warnings
- INTERFACE
+
+pw_add_library_generic(pw_build.extra_strict_warnings INTERFACE
+ PUBLIC_COMPILE_OPTIONS
"-Wshadow"
${extra_strict_warnings_cond}
$<$<COMPILE_LANGUAGE:C>:-Wstrict-prototypes>
)
-add_library(pw_build.cpp17 INTERFACE)
-target_compile_options(pw_build.cpp17
- INTERFACE
+pw_add_library_generic(pw_build.pedantic_warnings INTERFACE
+ PUBLIC_COMPILE_OPTIONS
+ # Enable -Wpedantic, but disable a few warnings.
+ "-Wpedantic"
+
+ # Allow designated initializers, which were added in C++20 but widely
+ # supported prior and permitted by the Google style guide.
+ "-Wno-c++20-designator"
+
+ # Allow empty ... arguments in macros, which are permitted in C++20 but
+ # widely supported prior.
+ "-Wno-gnu-zero-variadic-macro-arguments"
+)
+
+pw_add_library_generic(pw_build.cpp17 INTERFACE
+ PUBLIC_COMPILE_OPTIONS
$<$<COMPILE_LANGUAGE:CXX>:-std=c++17>
# Allow uses of the register keyword, which may appear in C headers.
$<$<COMPILE_LANGUAGE:CXX>:-Wno-register>
)
-# Create an empty source file and library for general use.
-file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/empty_file.c" "")
-add_library(pw_build.empty OBJECT "${CMAKE_CURRENT_BINARY_DIR}/empty_file.c" "")
+# Create an empty C++ source file and library for general use.
+file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/empty_file.cc" "")
+add_library(pw_build.empty OBJECT "${CMAKE_CURRENT_BINARY_DIR}/empty_file.cc" "")
+
+pw_add_test(pw_build.cc_blob_library_test
+ SOURCES
+ cc_blob_library_test.cc
+ PRIVATE_DEPS
+ pw_build.test_blob
+ GROUPS
+ modules
+ pw_build
+)
+
+pw_cc_blob_library(pw_build.test_blob
+ HEADER
+ pw_build/test_blob.h
+ NAMESPACE
+ test::ns
+ BLOB
+ SYMBOL_NAME kFirstBlob0123
+ PATH test_blob_0123.bin
+ ALIGNAS 512
+ BLOB
+ SYMBOL_NAME kSecondBlob0123
+ PATH test_blob_0123.bin
+)
diff --git a/pw_build/bazel_internal/linker_script.ld b/pw_build/bazel_internal/linker_script.ld
index 7c3169e7b..bd6609931 100644
--- a/pw_build/bazel_internal/linker_script.ld
+++ b/pw_build/bazel_internal/linker_script.ld
@@ -31,7 +31,7 @@ ENTRY(pw_boot_Entry)
MEMORY
{
- /* TODO(pwbug/57): Make it possible for projects to freely customize
+ /* TODO(b/234892223): Make it possible for projects to freely customize
* memory regions.
*/
diff --git a/pw_build/bazel_internal/pigweed_internal.bzl b/pw_build/bazel_internal/pigweed_internal.bzl
index 5a2bad12c..ed452ffe8 100644
--- a/pw_build/bazel_internal/pigweed_internal.bzl
+++ b/pw_build/bazel_internal/pigweed_internal.bzl
@@ -15,8 +15,8 @@
# the License.
""" An internal set of tools for creating embedded CC targets. """
+load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain", "use_cpp_toolchain")
load("@rules_cc//cc:action_names.bzl", "C_COMPILE_ACTION_NAME")
-load("@rules_cc//cc:toolchain_utils.bzl", "find_cpp_toolchain")
DEBUGGING = [
"-g",
@@ -39,14 +39,6 @@ STRICT_WARNINGS_COPTS = [
"-Wno-error=deprecated-declarations", # [[deprecated]] attribute
]
-CPP17_COPTS = [
- "-std=c++17",
- "-fno-rtti",
- "-Wnon-virtual-dtor",
- # Allow uses of the register keyword, which may appear in C headers.
- "-Wno-register",
-]
-
DISABLE_PENDING_WORKAROUND_COPTS = [
"-Wno-private-header",
]
@@ -62,8 +54,6 @@ KYTHE_COPTS = [
"-Wno-unknown-warning-option",
]
-PW_DEFAULT_LINKOPTS = []
-
def add_defaults(kwargs):
"""Adds default arguments suitable for both C and C++ code to kwargs.
@@ -73,10 +63,9 @@ def add_defaults(kwargs):
copts = PW_DEFAULT_COPTS + kwargs.get("copts", [])
kwargs["copts"] = select({
- "//pw_build:kythe": copts + KYTHE_COPTS,
+ "@pigweed//pw_build:kythe": copts + KYTHE_COPTS,
"//conditions:default": copts,
})
- kwargs["linkopts"] = kwargs.get("linkopts", []) + PW_DEFAULT_LINKOPTS
# Set linkstatic to avoid building .so files.
kwargs["linkstatic"] = True
@@ -88,64 +77,6 @@ def add_defaults(kwargs):
# it's "minus use_header_modules".
kwargs["features"].append("-use_header_modules")
-def default_cc_and_c_kwargs(kwargs):
- """Splits kwargs into C and C++ arguments adding defaults.
-
- Args:
- kwargs: cc_* arguments to be modified.
-
- Returns:
- A tuple of (cc_cxx_kwargs cc_c_kwargs)
- """
- add_defaults(kwargs)
- kwargs.setdefault("srcs", [])
-
- cc = dict(kwargs.items())
- cc["srcs"] = [src for src in kwargs["srcs"] if not src.endswith(".c")]
- cc["copts"] = cc["copts"] + CPP17_COPTS
-
- c_srcs = [src for src in kwargs["srcs"] if src.endswith(".c")]
-
- if c_srcs:
- c = dict(kwargs.items())
- c["name"] += "_c"
- c["srcs"] = c_srcs + [src for src in kwargs["srcs"] if src.endswith(".h")]
-
- cc["deps"] = cc.get("deps", []) + [":" + c["name"]]
- return cc, c
-
- return cc, None
-
-def add_cc_and_c_targets(target, kwargs): # buildifier: disable=unnamed-macro
- """Splits target into C and C++ targets adding defaults.
-
- Args:
- target: cc_* target to be split.
- kwargs: cc_* arguments to be modified.
- """
- cc_kwargs, c_kwargs = default_cc_and_c_kwargs(kwargs)
-
- if c_kwargs:
- native.cc_library(**c_kwargs)
-
- target(**cc_kwargs)
-
-def has_pw_assert_dep(deps):
- """Checks if the given deps contain a pw_assert dependency
-
- Args:
- deps: List of dependencies
-
- Returns:
- True if the list contains a pw_assert dependency.
- """
- pw_assert_targets = ["//pw_assert", "//pw_assert:pw_assert"]
- pw_assert_targets.append(["@pigweed" + t for t in pw_assert_targets])
- for dep in deps:
- if dep in pw_assert_targets:
- return True
- return False
-
def _preprocess_linker_script_impl(ctx):
cc_toolchain = find_cpp_toolchain(ctx)
output_script = ctx.actions.declare_file(ctx.label.name + ".ld")
@@ -164,6 +95,11 @@ def _preprocess_linker_script_impl(ctx):
cc_toolchain = cc_toolchain,
user_compile_flags = ctx.fragments.cpp.copts + ctx.fragments.cpp.conlyopts,
)
+ action_flags = cc_common.get_memory_inefficient_command_line(
+ feature_configuration = feature_configuration,
+ action_name = C_COMPILE_ACTION_NAME,
+ variables = c_compile_variables,
+ )
env = cc_common.get_environment_variables(
feature_configuration = feature_configuration,
action_name = C_COMPILE_ACTION_NAME,
@@ -186,7 +122,7 @@ def _preprocess_linker_script_impl(ctx):
] + [
"-D" + d
for d in ctx.attr.defines
- ] + ctx.attr.copts,
+ ] + action_flags + ctx.attr.copts,
env = env,
)
return [DefaultInfo(files = depset([output_script]))]
@@ -203,6 +139,6 @@ pw_linker_script = rule(
),
"_cc_toolchain": attr.label(default = Label("@bazel_tools//tools/cpp:current_cc_toolchain")),
},
- toolchains = ["@bazel_tools//tools/cpp:toolchain_type"],
+ toolchains = use_cpp_toolchain(),
fragments = ["cpp"],
)
diff --git a/pw_build/bazel_internal/py_proto_library.bzl b/pw_build/bazel_internal/py_proto_library.bzl
new file mode 100644
index 000000000..4ea006b41
--- /dev/null
+++ b/pw_build/bazel_internal/py_proto_library.bzl
@@ -0,0 +1,30 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""A custom wrapper for py_proto_library."""
+
+load("@com_google_protobuf//:protobuf.bzl", real_py_proto_library = "py_proto_library")
+
+def py_proto_library(**kwargs):
+ """A py_proto_library that respects the "manual" tag.
+
+ Actually it's a little _too_ respectful: it simply removes those targets
+ from the BUILD graph. This is good enough for our use case. Please do not
+ use this outside upstream Pigweed. (For downstream projects, this is
+ equivalent to just commenting out the BUILD target.)
+
+ See https://issues.pigweed.dev/issues/244743459 and
+ https://stackoverflow.com/q/74323506/1224002 for more context.
+ """
+ if "tags" not in kwargs or "manual" not in kwargs["tags"]:
+ real_py_proto_library(**kwargs)
diff --git a/pw_build/cc_blob_library.cmake b/pw_build/cc_blob_library.cmake
new file mode 100644
index 000000000..14bb6ca16
--- /dev/null
+++ b/pw_build/cc_blob_library.cmake
@@ -0,0 +1,171 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+cmake_minimum_required(VERSION 3.20) # string(JSON)
+
+include("$ENV{PW_ROOT}/pw_build/pigweed.cmake")
+
+# Turns binary blobs into a C++ library of hard-coded byte arrays. The byte
+# arrays are constant initialized and are safe to access at any time, including
+# before main().
+#
+# Args:
+#
+# HEADER
+#
+# The header file to generate. Users will include this file exactly as it is
+# written here to reference the byte arrays.
+#
+# NAMESPACE
+#
+# The C++ namespace in which to place the generated blobs.
+#
+# BLOB
+#
+# A blob to be transformed from file to byte array. Multiple blobs may be
+# specified.
+#
+# Blob args:
+#
+# SYMBOL_NAME
+#
+# The C++ symbol name for the byte array.
+#
+# PATH
+#
+# The file path for the binary blob.
+#
+# LINKER_SECTION [optional]
+#
+# If present, places the byte array in the specified linker section.
+#
+# ALIGNAS [optional]
+#
+# If present, the byte array is aligned as specified. The value of this
+# argument is used verbatim in an alignas() specifier for the blob
+# byte array.
+#
+function(pw_cc_blob_library NAME)
+ cmake_parse_arguments(PARSE_ARGV 1 arg "" "HEADER;NAMESPACE" "")
+
+ set(blobs ${arg_UNPARSED_ARGUMENTS})
+ set(blob_files "")
+ set(blob_index 0)
+ set(json_blobs "[]") # Create a JSON list of blobs
+
+ pw_require_args("" arg_ HEADER NAMESPACE)
+
+ while(NOT "${blobs}" STREQUAL "")
+ list(POP_FRONT blobs first_arg)
+
+ if(NOT "${first_arg}" STREQUAL BLOB)
+ message(FATAL_ERROR "Invalid syntax in pw_cc_blob_library: "
+ "Expected 'BLOB', found '${first_arg}'.")
+ endif()
+
+ list(FIND blobs BLOB blob_end)
+ list(SUBLIST blobs 0 "${blob_end}" current_blob)
+
+ cmake_parse_arguments(
+ blob_arg "" "SYMBOL_NAME;PATH;LINKER_SECTION;ALIGNAS" "" "${current_blob}"
+ )
+
+ if(NOT "${blob_arg_UNPARSED_ARGUMENTS}" STREQUAL "")
+ message(FATAL_ERROR "Unexpected BLOB arguments in pw_cc_blob_library: "
+ "${blob_arg_UNPARSED_ARGUMENTS}")
+ endif()
+
+ pw_require_args("BLOB args for ${CMAKE_CURRENT_FUNCTION}"
+ blob_arg_ PATH SYMBOL_NAME)
+
+ cmake_path(ABSOLUTE_PATH blob_arg_PATH)
+ list(APPEND blob_files "${blob_arg_PATH}")
+
+ set(json_blob "{}")
+ _pw_json_set_string_key(json_blob file_path "${blob_arg_PATH}")
+ _pw_json_set_string_key(json_blob symbol_name "${blob_arg_SYMBOL_NAME}")
+
+ if(NOT "${blob_arg_ALIGNAS}" STREQUAL "")
+ _pw_json_set_string_key(json_blob alignas "${blob_arg_ALIGNAS}")
+ endif()
+
+ if(NOT "${blob_arg_LINKER_SECTION}" STREQUAL "")
+ _pw_json_set_string_key(
+ json_blob linker_section "${blob_arg_LINKER_SECTION}")
+ endif()
+
+ string(JSON json_blobs SET "${json_blobs}" "${blob_index}" "${json_blob}")
+
+ if("${blob_end}" EQUAL -1)
+ break()
+ endif()
+
+ list(SUBLIST blobs "${blob_end}" -1 blobs)
+ math(EXPR blob_index "${blob_index}+1")
+ endwhile()
+
+ set(out_dir "${CMAKE_CURRENT_BINARY_DIR}/${NAME}")
+ set(blob_json_file "${out_dir}/blobs.json")
+
+ file(WRITE "${blob_json_file}" "${json_blobs}")
+ set_property( # Ensure the file is regenerated by CMake if it is deleted.
+ DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${blob_json_file}")
+
+ set(generated_header "${out_dir}/public/${arg_HEADER}")
+
+ cmake_path(GET arg_HEADER STEM filename)
+ set(generated_source "${out_dir}/${filename}.cc")
+
+ add_custom_command(
+ COMMAND
+ python3
+ "$ENV{PW_ROOT}/pw_build/py/pw_build/generate_cc_blob_library.py"
+ --blob-file "${blob_json_file}"
+ --header-include "${arg_HEADER}"
+ --out-header "${generated_header}"
+ --out-source "${generated_source}"
+ --namespace "${arg_NAMESPACE}"
+ DEPENDS
+ "$ENV{PW_ROOT}/pw_build/py/pw_build/generate_cc_blob_library.py"
+ "${blob_json_file}"
+ ${blob_files}
+ OUTPUT
+ "${generated_header}"
+ "${generated_source}"
+ )
+
+ add_custom_target("${NAME}._gen"
+ DEPENDS
+ "${generated_header}"
+ "${generated_source}"
+ )
+
+ pw_add_library_generic("${NAME}" OBJECT
+ SOURCES
+ "${generated_source}"
+ PUBLIC_INCLUDES
+ "${out_dir}/public"
+ PUBLIC_DEPS
+ pw_polyfill
+ pw_preprocessor
+ )
+ add_dependencies("${NAME}" "${NAME}._gen")
+endfunction(pw_cc_blob_library)
+
+# Sets a key with a string value in a JSON object.
+macro(_pw_json_set_string_key json_var key value)
+ string(JSON "${json_var}" SET "${${json_var}}" "${key}" "\"${value}\"")
+endmacro()
diff --git a/pw_build/cc_blob_library.gni b/pw_build/cc_blob_library.gni
index 0a8a9d0f5..39c44a36f 100644
--- a/pw_build/cc_blob_library.gni
+++ b/pw_build/cc_blob_library.gni
@@ -18,6 +18,8 @@ import("$dir_pw_build/python_action.gni")
import("$dir_pw_build/target_types.gni")
# Turns binary blobs into a C++ source_set library of hard-coded byte arrays.
+# The byte arrays are constant initialized and are safe to access at any time,
+# including before main().
#
# blobs A list of scopes, where each scope corresponds to a binary
# blob to be transformed from file to byte array. This is a
@@ -30,29 +32,39 @@ import("$dir_pw_build/target_types.gni")
# linker_section [optional]: If present, places the byte array
# in the specified linker section.
#
+# alignas [optional]: If present, the byte array is aligned as
+# specified. The value of this argument is used verbatim
+# in an alignas() specifier for the blob byte array.
+#
# out_header The header file to generate. Users will include this file
# exactly as it is written here to reference the byte arrays.
#
-# namespace An optional (but highly recommended!) C++ namespace to place
-# the generated blobs within.
+# namespace The C++ namespace in which to place the generated blobs.
+#
template("pw_cc_blob_library") {
assert(defined(invoker.blobs), "pw_cc_blob_library requires 'blobs'")
assert(defined(invoker.out_header),
"pw_cc_blob_library requires an 'out_header'")
+ assert(defined(invoker.namespace),
+ "pw_cc_blob_library requires a 'namespace'")
_blobs = []
_blob_files = []
foreach(blob, invoker.blobs) {
assert(defined(blob.symbol_name), "Each 'blob' requires a 'symbol_name'")
assert(defined(blob.file_path), "Each 'blob' requires a 'file_path'")
- blob.file_path = rebase_path(blob.file_path)
- _blobs += [ blob ]
_blob_files += [ blob.file_path ]
+ blob.file_path = rebase_path(blob.file_path, root_build_dir)
+ _blobs += [ blob ]
}
- _blob_json_file = "$target_gen_dir/$target_name.json"
+ _out_dir = "$target_gen_dir/$target_name"
+ _blob_json_file = "$_out_dir/blobs.json"
write_file(_blob_json_file, _blobs, "json")
+ _header = "$_out_dir/public/${invoker.out_header}"
+ _source = "$_out_dir/" + get_path_info(invoker.out_header, "name") + ".cc"
+
pw_python_action("$target_name._gen") {
forward_variables_from(invoker,
[
@@ -60,26 +72,19 @@ template("pw_cc_blob_library") {
"public_deps",
])
module = "pw_build.generate_cc_blob_library"
+ python_deps = [ "$dir_pw_build/py" ]
- _header = "${target_gen_dir}/public/" + invoker.out_header
- _source =
- "${target_gen_dir}/" + get_path_info(invoker.out_header, "name") + ".cc"
args = [
"--blob-file",
rebase_path(_blob_json_file, root_build_dir),
+ "--namespace=${invoker.namespace}",
+ "--header-include=${invoker.out_header}",
"--out-header",
- rebase_path(_header),
+ rebase_path(_header, root_build_dir),
"--out-source",
- rebase_path(_source),
+ rebase_path(_source, root_build_dir),
]
- if (defined(invoker.namespace)) {
- args += [
- "--namespace",
- invoker.namespace,
- ]
- }
-
inputs = [ _blob_json_file ] + _blob_files
outputs = [
_header,
@@ -88,14 +93,18 @@ template("pw_cc_blob_library") {
}
config("$target_name._include_path") {
- include_dirs = [ "${target_gen_dir}/public" ]
+ include_dirs = [ "$_out_dir/public" ]
visibility = [ ":*" ]
}
pw_source_set(target_name) {
- sources = get_target_outputs(":$target_name._gen")
+ public = [ _header ]
+ sources = [ _source ]
public_configs = [ ":$target_name._include_path" ]
- deps = [ ":$target_name._gen" ]
- public_deps = [ "$dir_pw_preprocessor" ]
+ deps = [
+ ":$target_name._gen",
+ dir_pw_preprocessor,
+ ]
+ forward_variables_from(invoker, [ "visibility" ])
}
}
diff --git a/pw_build/cc_blob_library_test.cc b/pw_build/cc_blob_library_test.cc
new file mode 100644
index 000000000..faa4f6a47
--- /dev/null
+++ b/pw_build/cc_blob_library_test.cc
@@ -0,0 +1,50 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <cstddef>
+
+#include "gtest/gtest.h"
+#include "pw_build/test_blob.h"
+
+namespace pw::build {
+namespace {
+
+static_assert(test::ns::kFirstBlob0123.size() == 4);
+static_assert(test::ns::kSecondBlob0123.size() == 4);
+
+TEST(CcBlobLibraryTest, FirstBlobContentsMatch) {
+ EXPECT_EQ(test::ns::kFirstBlob0123[0], std::byte{0});
+ EXPECT_EQ(test::ns::kFirstBlob0123[1], std::byte{1});
+ EXPECT_EQ(test::ns::kFirstBlob0123[2], std::byte{2});
+ EXPECT_EQ(test::ns::kFirstBlob0123[3], std::byte{3});
+}
+
+TEST(CcBlobLibraryTest, SecondBlobContentsMatch) {
+ EXPECT_EQ(test::ns::kSecondBlob0123[0], std::byte{0});
+ EXPECT_EQ(test::ns::kSecondBlob0123[1], std::byte{1});
+ EXPECT_EQ(test::ns::kSecondBlob0123[2], std::byte{2});
+ EXPECT_EQ(test::ns::kSecondBlob0123[3], std::byte{3});
+}
+
+TEST(CcBlobLibraryTest, FirstBlobAlignedTo512) {
+ // This checks that the variable is aligned to 512, but cannot guarantee that
+ // alignas was specified correctly, since it could be aligned to 512 by
+ // coincidence.
+ const uintptr_t addr = reinterpret_cast<uintptr_t>(&test::ns::kFirstBlob0123);
+ constexpr uintptr_t kAlignmentMask = static_cast<uintptr_t>(512 - 1);
+ EXPECT_EQ(addr & kAlignmentMask, 0u);
+}
+
+} // namespace
+} // namespace pw::build
diff --git a/pw_build/cc_executable.gni b/pw_build/cc_executable.gni
index 5165324dc..fe10da498 100644
--- a/pw_build/cc_executable.gni
+++ b/pw_build/cc_executable.gni
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/cc_library.gni")
+import("$dir_pw_build/gn_internal/build_target.gni")
# Note: In general, prefer to import target_types.gni rather than this file.
# cc_executable.gni and cc_library.gni are both provided by target_types.gni.
@@ -23,102 +24,36 @@ import("$dir_pw_build/cc_library.gni")
# templates may need to create pw_source_set targets internally, and can't
# import target_types.gni because it creates a circular import path.
-declare_args() {
- # The name of the GN target type used to build Pigweed executables.
- #
- # If this is a custom template, the .gni file containing the template must
- # be imported at the top of the target configuration file to make it globally
- # available.
- pw_build_EXECUTABLE_TARGET_TYPE = "executable"
-
- # The path to the .gni file that defines pw_build_EXECUTABLE_TARGET_TYPE.
- #
- # If pw_build_EXECUTABLE_TARGET_TYPE is not the default of `executable`, this
- # .gni file is imported to provide the template definition.
- pw_build_EXECUTABLE_TARGET_TYPE_FILE = ""
-}
-
-if (pw_build_EXECUTABLE_TARGET_TYPE != "executable" &&
- pw_build_EXECUTABLE_TARGET_TYPE_FILE != "") {
- import(pw_build_EXECUTABLE_TARGET_TYPE_FILE)
-}
-
-_supported_toolchain_defaults = [
- "configs",
- "public_deps",
-]
+# This template wraps a configurable target type specified by the current
+# toolchain to be used for all pw_executable targets. This allows projects to
+# stamp out unique build logic for each pw_executable target, such as generating
+# .bin files from .elf files, creating token databases, injecting custom data
+# as late-bound build steps, and more.
+#
+# Like pw_source_set, pw_static_library, and pw_shared_library, default configs
+# default visibility, and link deps are applied to the target before forwarding
+# to the underlying type as specified by pw_build_EXECUTABLE_TARGET_TYPE.
+#
+# For more information on the features provided by this template, see the full
+# docs at https://pigweed.dev/pw_build/?highlight=pw_executable#target-types.
+#
+# In addition to the arguments supported by a native executable target, this
+# template introduces the following arguments:
+#
+# remove_configs: (optional) A list of configs / config patterns to remove from
+# the set of default configs specified by the current toolchain
+# configuration.
+# remove_public_deps: (optional) A list of targets to remove from the set of
+# default public_deps specified by the current toolchain configuration.
-# Wrapper for Pigweed executable build targets which uses a globally-defined,
-# configurable target type.
template("pw_executable") {
- _pw_source_files = []
-
- # Boilerplate for tracking target sources. For more information see
- # https://pigweed.dev/pw_build/#target-types
- if (defined(invoker.sources)) {
- foreach(path, invoker.sources) {
- _pw_source_files += [ path ]
- }
- }
- if (defined(invoker.public)) {
- foreach(path, invoker.public) {
- _pw_source_files += [ path ]
- }
- }
-
- _executable_output_dir = "${target_out_dir}/bin"
- if (defined(invoker.output_dir)) {
- _executable_output_dir = invoker.output_dir
- }
-
- target(pw_build_EXECUTABLE_TARGET_TYPE, target_name) {
- import("$dir_pw_build/defaults.gni")
-
- forward_variables_from(invoker, "*", _supported_toolchain_defaults)
-
- # Ensure that we don't overwrite metadata forwared from the invoker above.
- if (defined(metadata)) {
- metadata.pw_source_files = _pw_source_files
- } else {
- metadata = {
- pw_source_files = _pw_source_files
- }
- }
-
- if (!defined(configs)) {
- configs = []
- }
- if (defined(pw_build_defaults.configs)) {
- configs += pw_build_defaults.configs
- }
- if (defined(remove_configs)) {
- if (remove_configs != [] && remove_configs[0] == "*") {
- configs = []
- } else {
- configs += remove_configs # Add configs in case they aren't already
- configs -= remove_configs # present, then remove them.
- }
- }
- if (defined(invoker.configs)) {
- configs += invoker.configs
- }
-
- public_deps = [ "$dir_pw_build:link_deps" ]
- if (defined(pw_build_defaults.public_deps)) {
- public_deps += pw_build_defaults.public_deps
- }
- if (defined(remove_public_deps)) {
- if (remove_public_deps != [] && remove_public_deps[0] == "*") {
- public_deps = []
- } else {
- public_deps += remove_public_deps
- public_deps -= remove_public_deps
- }
- }
- if (defined(invoker.public_deps)) {
- public_deps += invoker.public_deps
- }
-
- output_dir = _executable_output_dir
+ pw_internal_build_target(target_name) {
+ forward_variables_from(invoker, "*")
+ if (!defined(output_dir)) {
+ output_dir = "${target_out_dir}/bin"
+ }
+ add_global_link_deps = true
+ underlying_target_type = pw_build_EXECUTABLE_TARGET_TYPE
+ target_type_file = pw_build_EXECUTABLE_TARGET_TYPE_FILE
}
}
diff --git a/pw_build/cc_library.gni b/pw_build/cc_library.gni
index 5a25f9ff3..02fe625a9 100644
--- a/pw_build/cc_library.gni
+++ b/pw_build/cc_library.gni
@@ -14,6 +14,8 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/gn_internal/build_target.gni")
+
# Note: In general, prefer to import target_types.gni rather than this file.
# cc_executable.gni and cc_library.gni are both provided by target_types.gni.
#
@@ -21,230 +23,40 @@ import("//build_overrides/pigweed.gni")
# templates may need to create pw_source_set targets internally, and can't
# import target_types.gni because it creates a circular import path.
-declare_args() {
- # Additional build targets to add as dependencies for pw_executable,
- # pw_static_library, and pw_shared_library targets. The
- # $dir_pw_build:link_deps target pulls in these libraries.
- #
- # pw_build_LINK_DEPS can be used to break circular dependencies for low-level
- # libraries such as pw_assert.
- pw_build_LINK_DEPS = []
-}
-
-# TODO(frolv): The code in all of the templates below is duplicated, with the
-# exception of the target type. This file could be auto-generated with Python.
-
-_supported_toolchain_defaults = [
- "configs",
- "public_deps",
-]
+# These templates are wrappers for GN's built-in source_set, static_library,
+# and shared_library targets.
+#
+# For more information on the features provided by these templates, see the full
+# docs at https://pigweed.dev/pw_build/?highlight=pw_executable#target-types.
+#
+# In addition to the arguments supported by the underlying native target types,
+# these templates introduce the following arguments:
+#
+# remove_configs: (optional) A list of configs to remove from the set of
+# default configs specified by the current toolchain configuration.
+# remove_public_deps: (optional) A list of targets to remove from the set of
+# default public_deps specified by the current toolchain configuration.
template("pw_source_set") {
- _pw_source_files = []
-
- # Boilerplate for tracking target sources. For more information see
- # https://pigweed.dev/pw_build/#target-types
- if (defined(invoker.sources)) {
- foreach(path, invoker.sources) {
- _pw_source_files += [ path ]
- }
- }
- if (defined(invoker.public)) {
- foreach(path, invoker.public) {
- _pw_source_files += [ path ]
- }
- }
-
- source_set(target_name) {
- import("$dir_pw_build/defaults.gni")
- forward_variables_from(invoker, "*", _supported_toolchain_defaults)
-
- # Ensure that we don't overwrite metadata forwared from the invoker above.
- if (defined(metadata)) {
- metadata.pw_source_files = _pw_source_files
- } else {
- metadata = {
- pw_source_files = _pw_source_files
- }
- }
-
- if (!defined(configs)) {
- configs = []
- }
- if (defined(pw_build_defaults.configs)) {
- configs += pw_build_defaults.configs
- }
- if (defined(remove_configs)) {
- if (remove_configs != [] && remove_configs[0] == "*") {
- configs = []
- } else {
- configs += remove_configs # Add configs in case they aren't already
- configs -= remove_configs # present, then remove them.
- }
- }
- if (defined(invoker.configs)) {
- configs += invoker.configs
- }
-
- if (defined(pw_build_defaults.public_deps)) {
- public_deps = pw_build_defaults.public_deps
- } else {
- public_deps = []
- }
- if (defined(remove_public_deps)) {
- if (remove_public_deps != [] && remove_public_deps[0] == "*") {
- public_deps = []
- } else {
- public_deps += remove_public_deps
- public_deps -= remove_public_deps
- }
- }
- if (defined(invoker.public_deps)) {
- public_deps += invoker.public_deps
- }
+ pw_internal_build_target(target_name) {
+ forward_variables_from(invoker, "*")
+ add_global_link_deps = false
+ underlying_target_type = "source_set"
}
}
template("pw_static_library") {
- _pw_source_files = []
-
- # Boilerplate for tracking target sources. For more information see
- # https://pigweed.dev/pw_build/#target-types
- if (defined(invoker.sources)) {
- foreach(path, invoker.sources) {
- _pw_source_files += [ path ]
- }
- }
- if (defined(invoker.public)) {
- foreach(path, invoker.public) {
- _pw_source_files += [ path ]
- }
- }
-
- _library_output_dir = "${target_out_dir}/lib"
- if (defined(invoker.output_dir)) {
- _library_output_dir = invoker.output_dir
- }
-
- static_library(target_name) {
- import("$dir_pw_build/defaults.gni")
- forward_variables_from(invoker, "*", _supported_toolchain_defaults)
-
- # Ensure that we don't overwrite metadata forwared from the invoker above.
- if (defined(metadata)) {
- metadata.pw_source_files = _pw_source_files
- } else {
- metadata = {
- pw_source_files = _pw_source_files
- }
- }
-
- if (!defined(configs)) {
- configs = []
- }
- if (defined(pw_build_defaults.configs)) {
- configs += pw_build_defaults.configs
- }
- if (defined(remove_configs)) {
- if (remove_configs != [] && remove_configs[0] == "*") {
- configs = []
- } else {
- configs += remove_configs # Add configs in case they aren't already
- configs -= remove_configs # present, then remove them.
- }
- }
- if (defined(invoker.configs)) {
- configs += invoker.configs
- }
-
- public_deps = [ "$dir_pw_build:link_deps" ]
- if (defined(pw_build_defaults.public_deps)) {
- public_deps += pw_build_defaults.public_deps
- }
- if (defined(remove_public_deps)) {
- if (remove_public_deps != [] && remove_public_deps[0] == "*") {
- public_deps = []
- } else {
- public_deps += remove_public_deps
- public_deps -= remove_public_deps
- }
- }
- if (defined(invoker.public_deps)) {
- public_deps += invoker.public_deps
- }
-
- output_dir = _library_output_dir
+ pw_internal_build_target(target_name) {
+ forward_variables_from(invoker, "*")
+ add_global_link_deps = true
+ underlying_target_type = "static_library"
}
}
template("pw_shared_library") {
- _pw_source_files = []
-
- # Boilerplate for tracking target sources. For more information see
- # https://pigweed.dev/pw_build/#target-types
- if (defined(invoker.sources)) {
- foreach(path, invoker.sources) {
- _pw_source_files += [ path ]
- }
- }
- if (defined(invoker.public)) {
- foreach(path, invoker.public) {
- _pw_source_files += [ path ]
- }
- }
-
- _library_output_dir = "${target_out_dir}/lib"
- if (defined(invoker.output_dir)) {
- _library_output_dir = invoker.output_dir
- }
-
- shared_library(target_name) {
- import("$dir_pw_build/defaults.gni")
- forward_variables_from(invoker, "*", _supported_toolchain_defaults)
-
- # Ensure that we don't overwrite metadata forwared from the invoker above.
- if (defined(metadata)) {
- metadata.pw_source_files = _pw_source_files
- } else {
- metadata = {
- pw_source_files = _pw_source_files
- }
- }
-
- if (!defined(configs)) {
- configs = []
- }
- if (defined(pw_build_defaults.configs)) {
- configs += pw_build_defaults.configs
- }
- if (defined(remove_configs)) {
- if (remove_configs != [] && remove_configs[0] == "*") {
- configs = []
- } else {
- configs += remove_configs # Add configs in case they aren't already
- configs -= remove_configs # present, then remove them.
- }
- }
- if (defined(invoker.configs)) {
- configs += invoker.configs
- }
-
- public_deps = [ "$dir_pw_build:link_deps" ]
- if (defined(pw_build_defaults.public_deps)) {
- public_deps += pw_build_defaults.public_deps
- }
- if (defined(remove_public_deps)) {
- if (remove_public_deps != [] && remove_public_deps[0] == "*") {
- public_deps = []
- } else {
- public_deps += remove_public_deps
- public_deps -= remove_public_deps
- }
- }
- if (defined(invoker.public_deps)) {
- public_deps += invoker.public_deps
- }
-
- output_dir = _library_output_dir
+ pw_internal_build_target(target_name) {
+ forward_variables_from(invoker, "*")
+ add_global_link_deps = true
+ underlying_target_type = "shared_library"
}
}
diff --git a/pw_build/constraints/board/BUILD.bazel b/pw_build/constraints/board/BUILD.bazel
index 4696f87bb..58f637f0a 100644
--- a/pw_build/constraints/board/BUILD.bazel
+++ b/pw_build/constraints/board/BUILD.bazel
@@ -28,3 +28,8 @@ constraint_value(
name = "mimxrt595_evk",
constraint_setting = ":board",
)
+
+constraint_value(
+ name = "microbit",
+ constraint_setting = ":board",
+)
diff --git a/pw_build/constraints/chipset/BUILD.bazel b/pw_build/constraints/chipset/BUILD.bazel
index 3a2a2eab1..e292906e3 100644
--- a/pw_build/constraints/chipset/BUILD.bazel
+++ b/pw_build/constraints/chipset/BUILD.bazel
@@ -28,3 +28,8 @@ constraint_value(
name = "lm3s6965evb",
constraint_setting = ":chipset",
)
+
+constraint_value(
+ name = "nrf52833",
+ constraint_setting = ":chipset",
+)
diff --git a/pw_build/constraints/rtos/BUILD.bazel b/pw_build/constraints/rtos/BUILD.bazel
index 10eb73b49..3a4c30e5d 100644
--- a/pw_build/constraints/rtos/BUILD.bazel
+++ b/pw_build/constraints/rtos/BUILD.bazel
@@ -29,7 +29,7 @@ constraint_value(
config_setting(
name = "none_setting",
flag_values = {
- "@pigweed_config//:target_os": ":none",
+ "@pigweed_config//:target_rtos": ":none",
},
)
@@ -41,7 +41,7 @@ constraint_value(
config_setting(
name = "embos_setting",
flag_values = {
- "@pigweed_config//:target_os": ":embos",
+ "@pigweed_config//:target_rtos": ":embos",
},
)
@@ -53,7 +53,7 @@ constraint_value(
config_setting(
name = "freertos_setting",
flag_values = {
- "@pigweed_config//:target_os": ":freertos",
+ "@pigweed_config//:target_rtos": ":freertos",
},
)
@@ -65,6 +65,6 @@ constraint_value(
config_setting(
name = "threadx_setting",
flag_values = {
- "@pigweed_config//:target_os": ":threadx",
+ "@pigweed_config//:target_rtos": ":threadx",
},
)
diff --git a/pw_build/copy_from_cipd.gni b/pw_build/copy_from_cipd.gni
index aff8afac4..a961da10d 100644
--- a/pw_build/copy_from_cipd.gni
+++ b/pw_build/copy_from_cipd.gni
@@ -48,8 +48,8 @@ import("target_types.gni")
# library_path: (required) The path of the static library of interest,
# relative to the root where the manifest's CIPD packages were installed to.
# e.g. if the package is installed by pigweed.json, and the .a file is at
-# //.environment/cipd/pigweed/libs/libsomething.a, this argument should be
-# "libs/libsomething.a".
+# //environment/cipd/packages/pigweed/libs/libsomething.a, this argument
+# should be "libs/libsomething.a".
# cipd_package: (required) The name of the CIPD package. This is the "path" of
# the package in the manifest file.
#
@@ -71,7 +71,7 @@ template("pw_cipd_static_library") {
# might work but is redundant, so serialize calls.
pool = "$dir_pw_build/pool:copy_from_cipd($default_toolchain)"
- # TODO(pwbug/335): This should somehow track the actual .a for changes as
+ # TODO(b/234884827): This should somehow track the actual .a for changes as
# well.
inputs = [ invoker.manifest ]
outputs = [ _out_file ]
diff --git a/pw_build/defaults.gni b/pw_build/defaults.gni
index dc258faaf..50f52db51 100644
--- a/pw_build/defaults.gni
+++ b/pw_build/defaults.gni
@@ -25,6 +25,17 @@ declare_args() {
default_configs = []
default_public_deps = []
remove_default_configs = []
+
+ # Controls the default visibility of C/C++ libraries and executables
+ # (pw_source_set, pw_static_library, pw_shared_library pw_executable). This
+ # can be "*" or a list of paths.
+ #
+ # This is useful for limiting usage of Pigweed modules via an explicit
+ # allowlist. For the GN build to work, pw_build_DEFAULT_VISIBILITY must always
+ # at least include the Pigweed repository ("$dir_pigweed/*").
+ #
+ # Explicitly setting a target's visibility overrides this default.
+ pw_build_DEFAULT_VISIBILITY = "*"
}
# Combine target-specifc and target-agnostic default variables.
@@ -38,12 +49,13 @@ _pw_build_defaults = {
"$dir_pw_build:debugging",
"$dir_pw_build:reduced_size",
"$dir_pw_build:strict_warnings",
- "$dir_pw_build:cpp17",
+ "$dir_pw_build:toolchain_cpp_standard",
"$dir_pw_build:relative_paths",
]
- # TODO(pwbug/602): Remove this once all uses explicitly depend on polyfills.
- public_deps += [ "$dir_pw_polyfill:overrides" ]
+ if (pw_build_DEFAULT_VISIBILITY != "*") {
+ visibility = pw_build_DEFAULT_VISIBILITY
+ }
}
# One more pass, to remove configs
diff --git a/pw_build/docs.rst b/pw_build/docs.rst
index 90e499c9e..a1df9d929 100644
--- a/pw_build/docs.rst
+++ b/pw_build/docs.rst
@@ -37,6 +37,29 @@ compiler defaults. (See Pigweed's ``//BUILDCONFIG.gn``)
``pw_build`` also provides several useful GN templates that are used throughout
Pigweed.
+Build system philosophies
+-------------------------
+While Pigweed's GN build is not hermetic, it strives to adhere to principles of
+`hermeticity <https://bazel.build/concepts/hermeticity>`_. Some guidelines to
+move towards the ideal of hermeticity include:
+
+* Only rely on pre-compiled tools provided by CIPD (or some other versioned,
+ pre-compiled binary distribution mechanism). This eliminates build artifact
+ differences caused by different tool versions or variations (e.g. same tool
+ version built with slightly different compilation flags).
+* Do not use absolute paths in Ninja commands. Typically, these appear when
+ using ``rebase_path("//path/to/my_script.py")``. Most of the time, Ninja
+ steps should be passed paths rebased relative to the build directory (i.e.
+ ``rebase_path("//path/to/my_script.py", root_build_dir)``). This ensures build
+ commands are the same across different machines.
+* Prevent produced artifacts from relying on or referencing system state. This
+ includes time stamps, writing absolute paths to generated artifacts, or
+ producing artifacts that reference system state in a way that prevents them
+ from working the same way on a different machine.
+* Isolate build actions to the build directory. In general, the build system
+ should not add or modify files outside of the build directory. This can cause
+ confusion to users, and makes the concept of a clean build more ambiguous.
+
Target types
------------
.. code-block::
@@ -48,12 +71,66 @@ Target types
}
Pigweed defines wrappers around the four basic GN binary types ``source_set``,
-``executable``, ``static_library``, and ``shared_library``. These wrappers apply
-default arguments to each target, as defined in ``pw_build/default.gni``.
-Arguments may be added or removed globally using the ``default_configs``,
-``default_public_deps``, and ``remove_default_configs`` build args.
-Additionally, arguments may be removed on a per-target basis with the
-``remove_configs`` and ``remove_public_deps`` variables.
+``executable``, ``static_library``, and ``shared_library``. These templates
+do several things:
+
+#. **Add default configs/deps**
+
+ Rather than binding the majority of compiler flags related to C++ standard,
+ cross-compilation, warning/error policy, etc. directly to toolchain
+ invocations, these flags are applied as configs to all ``pw_*`` C/C++ target
+ types. The primary motivations for this are to allow some targets to modify
+ the default set of flags when needed by specifying ``remove_configs``, and to
+ reduce the complexity of building novel toolchains.
+
+ Pigweed's global default configs are set in ``pw_build/default.gni``, and
+ individual platform-specific toolchains extend the list by appending to the
+ ``default_configs`` build argument.
+
+ Default deps were added to support polyfill, which has since been
+ deprecated. Default dependency functionality continues to exist for
+ backwards compatibility.
+
+#. **Optionally add link-time binding**
+
+ Some libraries like pw_assert and pw_log are borderline impossible to
+ implement well without introducing circular dependencies. One solution for
+ addressing this is to break apart the libraries into an interface with
+ minimal dependencies, and an implementation with the bulk of the
+ dependencies that would typically create dependency cycles. In order for the
+ implementation to be linked in, it must be added to the dependency tree of
+ linked artifacts (e.g. ``pw_executable``, ``pw_static_library``). Since
+ there's no way for the libraries themselves to just happily pull in the
+ implementation if someone depends on the interface, the implementation is
+ instead late-bound by adding it as a direct dependency of the final linked
+ artifact. This is all managed through ``pw_build_LINK_DEPS``, which is global
+ for each toolchain and applied to every ``pw_executable``,
+ ``pw_static_library``, and ``pw_shared_library``.
+
+#. **Apply a default visibility policy**
+
+ Projects can globally control the default visibility of pw_* target types by
+ specifying ``pw_build_DEFAULT_VISIBILITY``. This template applies that as the
+ default visibility for any pw_* targets that do not explicitly specify
+ a visibility.
+
+#. **Add source file names as metadata**
+
+ All source file names are collected as
+ `GN metadata <https://gn.googlesource.com/gn/+/main/docs/reference.md#metadata_collection>`_.
+ This list can be writen to a file at build time using ``generated_file``. The
+ primary use case for this is to generate a token database containing all the
+ source files. This allows PW_ASSERT to emit filename tokens even though it
+ can't add them to the elf file because of the reasons described at
+ :ref:`module-pw_assert-assert-api`.
+
+ .. note::
+ ``pw_source_files``, if not rebased will default to outputing module
+ relative paths from a ``generated_file`` target. This is likely not
+ useful. Adding a ``rebase`` argument to ``generated_file`` such as
+ ``rebase = root_build_dir`` will result in usable paths. For an example,
+ see ``//pw_tokenizer/database.gni``'s ``pw_tokenizer_filename_database``
+ template.
The ``pw_executable`` template provides additional functionality around building
complete binaries. As Pigweed is a collection of libraries, it does not know how
@@ -64,27 +141,10 @@ Pigweed build against it. This is controlled by the build variable
template for a project.
In some uncommon cases, a project's ``pw_executable`` template definition may
-need to stamp out some ``pw_source_set``s. Since a pw_executable template can't
+need to stamp out some ``pw_source_set``\s. Since a pw_executable template can't
import ``$dir_pw_build/target_types.gni`` due to circular imports, it should
import ``$dir_pw_build/cc_library.gni`` instead.
-Additionally ``pw_executable``, ``pw_source_set``, ``pw_static_library``, and
-``pw_shared_library`` track source files via the ``pw_source_files`` field the
-target's
-`GN metadata <https://gn.googlesource.com/gn/+/main/docs/reference.md#metadata_collection>`_.
-This list can be writen to a file at build time using ``generated_file``. The
-primary use case for this is to generate a token database containing all the
-source files. This allows PW_ASSERT to emit filename tokens even though it
-can't add them to the elf file because of the resons described at
-:ref:`module-pw_assert-assert-api`.
-
-.. note::
- ``pw_source_files``, if not rebased will default to outputing module relative
- paths from a ``generated_file`` target. This is likely not useful. Adding
- a ``rebase`` argument to ``generated_file`` such as
- ``rebase = root_build_dir`` will result in usable paths. For an example,
- see `//pw_tokenizer/database.gni`'s `pw_tokenizer_filename_database` template.
-
.. tip::
Prefer to use ``pw_executable`` over plain ``executable`` targets to allow
@@ -92,8 +152,17 @@ can't add them to the elf file because of the resons described at
**Arguments**
-All of the ``pw_*`` target type overrides accept any arguments, as they simply
-forward them through to the underlying target.
+All of the ``pw_*`` target type overrides accept any arguments supported by
+the underlying native types, as they simply forward them through to the
+underlying target.
+
+Additionally, the following arguments are also supported:
+
+* **remove_configs**: (optional) A list of configs / config patterns to remove
+ from the set of default configs specified by the current toolchain
+ configuration.
+* **remove_public_deps**: (optional) A list of targets to remove from the set of
+ default public_deps specified by the current toolchain configuration.
.. _module-pw_build-link-deps:
@@ -128,7 +197,13 @@ pw_cc_blob_library
The ``pw_cc_blob_library`` template is useful for embedding binary data into a
program. The template takes in a mapping of symbol names to file paths, and
generates a set of C++ source and header files that embed the contents of the
-passed-in files as arrays.
+passed-in files as arrays of ``std::byte``.
+
+The blob byte arrays are constant initialized and are safe to access at any
+time, including before ``main()``.
+
+``pw_cc_blob_library`` is also available in the CMake build. It is provided by
+``pw_build/cc_blob_library.cmake``.
**Arguments**
@@ -140,6 +215,8 @@ passed-in files as arrays.
* ``file_path``: The file path for the binary blob.
* ``linker_section``: If present, places the byte array in the specified
linker section.
+ * ``alignas``: If present, uses the specified string or integer verbatim in
+ the ``alignas()`` specifier for the byte array.
* ``out_header``: The header file to generate. Users will include this file
exactly as it is written here to reference the byte arrays.
@@ -250,9 +327,10 @@ is set.
pw_python_action
----------------
-The ``pw_python_action`` template is a convenience wrapper around ``action`` for
-running Python scripts. The main benefit it provides is resolution of GN target
-labels to compiled binary files. This allows Python scripts to be written
+The ``pw_python_action`` template is a convenience wrapper around GN's `action
+function <https://gn.googlesource.com/gn/+/main/docs/reference.md#func_action>`_
+for running Python scripts. The main benefit it provides is resolution of GN
+target labels to compiled binary files. This allows Python scripts to be written
independently of GN, taking only filesystem paths as arguments.
Another convenience provided by the template is to allow running scripts without
@@ -280,8 +358,16 @@ target. Additionally, it has some of its own arguments:
* ``working_directory``: Optional file path. When provided the current working
directory will be set to this location before the Python module or script is
run.
+* ``command_launcher``: Optional string. Arguments to prepend to the Python
+ command, e.g. ``'/usr/bin/fakeroot --'`` will run the Python script within a
+ fakeroot environment.
+* ``venv``: Optional gn target of the pw_python_venv that should be used to run
+ this action.
+
+.. _module-pw_build-python-action-expressions:
-**Expressions**
+Expressions
+^^^^^^^^^^^
``pw_python_action`` evaluates expressions in ``args``, the arguments passed to
the script. These expressions function similarly to generator expressions in
@@ -295,7 +381,7 @@ about converting them to files.
.. note::
We intend to replace these expressions with native GN features when possible.
- See `pwbug/347 <http://bugs.pigweed.dev/347>`_.
+ See `b/234886742 <http://issuetracker.google.com/234886742>`_.
The following expressions are supported:
@@ -386,6 +472,76 @@ The following expressions are supported:
stamp = true
}
+.. _module-pw_build-evaluate-path-expressions:
+
+pw_evaluate_path_expressions
+----------------------------
+It is not always feasible to pass information to a script through command line
+arguments. If a script requires a large amount of input data, writing to a file
+is often more convenient. However, doing so bypasses ``pw_python_action``'s GN
+target label resolution, preventing such scripts from working with build
+artifacts in a build system-agnostic manner.
+
+``pw_evaluate_path_expressions`` is designed to address this use case. It takes
+a list of input files and resolves target expressions within them, modifying the
+files in-place.
+
+Refer to ``pw_python_action``'s :ref:`module-pw_build-python-action-expressions`
+section for the list of supported expressions.
+
+.. note::
+
+ ``pw_evaluate_path_expressions`` is typically used as an intermediate
+ sub-target of a larger template, rather than a standalone build target.
+
+**Arguments**
+
+* ``files``: A list of scopes, each containing a ``source`` file to process and
+ a ``dest`` file to which to write the result.
+
+**Example**
+
+The following template defines an executable target which additionally outputs
+the list of object files from which it was compiled, making use of
+``pw_evaluate_path_expressions`` to resolve their paths.
+
+.. code-block::
+
+ import("$dir_pw_build/evaluate_path_expressions.gni")
+
+ template("executable_with_artifacts") {
+ executable("${target_name}.exe") {
+ sources = invoker.sources
+ if defined(invoker.deps) {
+ deps = invoker.deps
+ }
+ }
+
+ _artifacts_input = "$target_gen_dir/${target_name}_artifacts.json.in"
+ _artifacts_output = "$target_gen_dir/${target_name}_artifacts.json"
+ _artifacts = {
+ binary = "<TARGET_FILE(:${target_name}.exe)>"
+ objects = "<TARGET_OBJECTS(:${target_name}.exe)>"
+ }
+ write_file(_artifacts_input, _artifacts, "json")
+
+ pw_evaluate_path_expressions("${target_name}.evaluate") {
+ files = [
+ {
+ source = _artifacts_input
+ dest = _artifacts_output
+ },
+ ]
+ }
+
+ group(target_name) {
+ deps = [
+ ":${target_name}.exe",
+ ":${target_name}.evaluate",
+ ]
+ }
+ }
+
.. _module-pw_build-pw_exec:
pw_exec
@@ -424,6 +580,7 @@ pw_exec
* ``working_directory``: The working directory to execute the subprocess with.
If not specified it will not be set and the subprocess will have whatever
the parent current working directory is.
+* ``visibility``: GN visibility to apply to the underlying target.
**Example**
@@ -682,6 +839,47 @@ on without an error.
The templates for build time errors are defined in ``pw_build/error.gni``.
+Improved Ninja interface
+------------------------
+Ninja includes a basic progress display, showing in a single line the number of
+targets finished, the total number of targets, and the name of the most recent
+target it has either started or finished.
+
+For additional insight into the status of the build, Pigweed includes a Ninja
+wrapper, ``pw-wrap-ninja``, that displays additional real-time information about
+the progress of the build. The wrapper is invoked the same way you'd normally
+invoke Ninja:
+
+.. code-block:: sh
+
+ pw-wrap-ninja -C out
+
+The script lists the progress of the build, as well as the list of targets that
+Ninja is currently building, along with a timer that measures how long each
+target has been building for:
+
+.. code-block::
+
+ [51.3s] Building [8924/10690] ...
+ [10.4s] c++ pw_strict_host_clang_debug/obj/pw_string/string_test.lib.string_test.cc.o
+ [ 9.5s] ACTION //pw_console/py:py.lint.mypy(//pw_build/python_toolchain:python)
+ [ 9.4s] ACTION //pw_console/py:py.lint.pylint(//pw_build/python_toolchain:python)
+ [ 6.1s] clang-tidy ../pw_log_rpc/log_service.cc
+ [ 6.1s] clang-tidy ../pw_log_rpc/log_service_test.cc
+ [ 6.1s] clang-tidy ../pw_log_rpc/rpc_log_drain.cc
+ [ 6.1s] clang-tidy ../pw_log_rpc/rpc_log_drain_test.cc
+ [ 5.4s] c++ pw_strict_host_clang_debug/obj/BUILD_DIR/pw_strict_host_clang_debug/gen/pw...
+ ... and 109 more
+
+This allows you to, at a glance, know what Ninja's currently building, which
+targets are bottlenecking the rest of the build, and which targets are taking
+an unusually long time to complete.
+
+``pw-wrap-ninja`` includes other useful functionality as well. The
+``--write-trace`` option writes a build trace to the specified path, which can
+be viewed in the `Perfetto UI <https://ui.perfetto.dev/>`_, or via Chrome's
+built-in ``chrome://tracing`` tool.
+
CMake
=====
Pigweed's `CMake`_ support is provided primarily for projects that have an
@@ -715,13 +913,32 @@ CMake functions
---------------
CMake convenience functions are defined in ``pw_build/pigweed.cmake``.
-* ``pw_auto_add_simple_module`` -- For modules with only one library,
- automatically declare the library and its tests.
-* ``pw_auto_add_module_tests`` -- Create test targets for all tests in a module.
-* ``pw_add_facade`` -- Declare a module facade.
+* ``pw_add_library_generic`` -- The base helper used to instantiate CMake
+ libraries. This is meant for use in downstream projects as upstream Pigweed
+ modules are expected to use ``pw_add_library``.
+* ``pw_add_library`` -- Add an upstream Pigweed library.
+* ``pw_add_facade_generic`` -- The base helper used to instantiate facade
+ libraries. This is meant for use in downstream projects as upstream Pigweed
+ modules are expected to use ``pw_add_facade``.
+* ``pw_add_facade`` -- Declare an upstream Pigweed facade.
* ``pw_set_backend`` -- Set the backend library to use for a facade.
-* ``pw_add_module_library`` -- Add a library that is part of a module.
-* ``pw_add_test`` -- Declare a test target.
+* ``pw_add_test_generic`` -- The base helper used to instantiate test targets.
+ This is meant for use in downstrema projects as upstream Pigweed modules are
+ expected to use ``pw_add_test``.
+* ``pw_add_test`` -- Declare an upstream Pigweed test target.
+* ``pw_add_test_group`` -- Declare a target to group and bundle test targets.
+* ``pw_target_link_targets`` -- Helper wrapper around ``target_link_libraries``
+ which only supports CMake targets and detects when the target does not exist.
+ Note that generator expressions are not supported.
+* ``pw_add_global_compile_options`` -- Applies compilation options to all
+ targets in the build. This should only be used to add essential compilation
+ options, such as those that affect the ABI. Use ``pw_add_library`` or
+ ``target_compile_options`` to apply other compile options.
+* ``pw_add_error_target`` -- Declares target which reports a message and causes
+ a build failure only when compiled. This is useful when ``FATAL_ERROR``
+ messages cannot be used to catch problems during the CMake configuration
+ phase.
+* ``pw_parse_arguments`` -- Helper to parse CMake function arguments.
See ``pw_build/pigweed.cmake`` for the complete documentation of these
functions.
@@ -737,12 +954,20 @@ similar to GN's build args set with ``gn args``. Unlike GN, CMake does not
support multi-toolchain builds, so these variables have a single global value
per build directory.
-The ``pw_add_facade`` function declares a cache variable named
+The ``pw_add_module_facade`` function declares a cache variable named
``<module_name>_BACKEND`` for each facade. Cache variables can be awkward to
work with, since their values only change when they're assigned, but then
persist accross CMake invocations. These variables should be set in one of the
following ways:
+* Prior to setting a backend, your application should include
+ ``$ENV{PW_ROOT}/backends.cmake``. This file will setup all the backend targets
+ such that any misspelling of a facade or backend will yield a warning.
+
+ .. note::
+ Zephyr developers do not need to do this, backends can be set automatically
+ by enabling the appropriate Kconfig options.
+
* Call ``pw_set_backend`` to set backends appropriate for the target in the
target's toolchain file. The toolchain file is provided to ``cmake`` with
``-DCMAKE_TOOLCHAIN_FILE=<toolchain file>``.
@@ -764,12 +989,13 @@ error message like the following:
.. code-block::
- CMake Error at pw_build/pigweed.cmake:244 (add_custom_target):
- Error evaluating generator expression:
-
- $<TARGET_PROPERTY:my_backend_that_does_not_exist,TYPE>
+ CMake Error at pw_build/pigweed.cmake:257 (message):
+ my_module.my_facade's INTERFACE dep "my_nonexistent_backend" is not
+ a target.
+ Call Stack (most recent call first):
+ pw_build/pigweed.cmake:238:EVAL:1 (_pw_target_link_targets_deferred_check)
+ CMakeLists.txt:DEFERRED
- Target "my_backend_that_does_not_exist" not found.
Toolchain setup
---------------
diff --git a/pw_build/evaluate_path_expressions.gni b/pw_build/evaluate_path_expressions.gni
new file mode 100644
index 000000000..31d1cd7f3
--- /dev/null
+++ b/pw_build/evaluate_path_expressions.gni
@@ -0,0 +1,102 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("python_action.gni")
+
+# Scans files for special GN target expressions and evaluates them, modifying
+# the files in-place.
+#
+# The supported expressions are the same as within the arguments list of a
+# pw_python_action target, namely:
+#
+# <TARGET_FILE(//some/label:here)> - expands to the
+# output file (such as a .a or .elf) from a GN target
+# <TARGET_FILE_IF_EXISTS(//some/label:here)> - expands to
+# the output file if the target exists, or nothing
+# <TARGET_OBJECTS(//some/label:here)> - expands to the
+# object files produced by the provided GN target
+#
+# This template may be useful, for example, to generate an artifact file
+# containing a list of binary objects produced by targets within a build.
+# Typically, this is used as a subtarget of a larger template, rather than a
+# standalone target.
+#
+# Args:
+# files: List of scopes containing `source` and `dest` files. `source` is
+# scanned for expressions, and the result is written to `dest`.
+#
+# Example:
+#
+# template("executable_with_artifacts") {
+# executable("${target_name}.exe") {
+# sources = invoker.sources
+# }
+#
+# _artifacts_input = "$target_gen_dir/${target_name}_artifacts.json.in"
+# _artifacts_output = "$target_gen_dir/${target_name}_artifacts.json"
+# _artifacts = {
+# binary = "<TARGET_FILE(:${target_name}.exe)>"
+# objects = "<TARGET_OBJECTS(:${target_name}.exe)>"
+# }
+# write_file(_artifacts_input, _artifacts, "json")
+#
+# pw_evaluate_path_expressions("${target_name}.evaluate") {
+# files = [
+# {
+# source = _artifacts_input
+# dest = _artifacts_output
+# },
+# ]
+# }
+#
+# group(target_name) {
+# deps = [
+# ":${target_name}.exe",
+# ":${target_name}.evaluate",
+# ]
+# }
+# }
+#
+template("pw_evaluate_path_expressions") {
+ assert(defined(invoker.files),
+ "pw_evaluate_path_expressions requires input files to scan")
+
+ _script_args = [
+ "--gn-root",
+ rebase_path("//", root_build_dir),
+ "--current-path",
+ rebase_path(".", root_build_dir),
+ "--default-toolchain",
+ default_toolchain,
+ "--current-toolchain",
+ current_toolchain,
+ ]
+
+ _inputs = []
+ _outputs = []
+
+ foreach(_file, invoker.files) {
+ _inputs += [ _file.source ]
+ _outputs += [ _file.dest ]
+ _arg = rebase_path(_file.source) + ":" + rebase_path(_file.dest)
+ _script_args += [ _arg ]
+ }
+
+ pw_python_action(target_name) {
+ script = "$dir_pw_build/py/pw_build/gn_resolver.py"
+ inputs = _inputs
+ outputs = _outputs
+ args = _script_args
+ }
+}
diff --git a/pw_build/exec.gni b/pw_build/exec.gni
index 241c87b55..eea4c01c1 100644
--- a/pw_build/exec.gni
+++ b/pw_build/exec.gni
@@ -57,6 +57,8 @@ import("python_action.gni")
# not specified it will not be set and the subprocess will have whatever the
# parent current working directory is.
#
+# visibility: GN visibility to apply to the underlying target.
+#
# Example:
#
# pw_exec("hello_world") {
@@ -138,6 +140,7 @@ template("pw_exec") {
"inputs",
"pool",
"public_deps",
+ "visibility",
])
if (!defined(inputs)) {
diff --git a/pw_build/generated_pigweed_modules_lists.gni b/pw_build/generated_pigweed_modules_lists.gni
index f3d3627ef..77cec9182 100644
--- a/pw_build/generated_pigweed_modules_lists.gni
+++ b/pw_build/generated_pigweed_modules_lists.gni
@@ -14,7 +14,7 @@
# Build args and lists for all modules in Pigweed.
#
-# DO NOT EDIT! Generated by pw_build/py/pw_build/generate_modules_lists.py.
+# DO NOT EDIT! Generated by pw_build/generate_modules_lists.py.
#
# To add modules here, list them in PIGWEED_MODULES and build the
# update_modules target and commit the updated version of this file:
@@ -28,6 +28,7 @@
# Declare a build arg for each module.
declare_args() {
dir_docker = get_path_info("../docker", "abspath")
+ dir_pw_alignment = get_path_info("../pw_alignment", "abspath")
dir_pw_allocator = get_path_info("../pw_allocator", "abspath")
dir_pw_analog = get_path_info("../pw_analog", "abspath")
dir_pw_android_toolchain = get_path_info("../pw_android_toolchain", "abspath")
@@ -37,10 +38,15 @@ declare_args() {
dir_pw_assert_log = get_path_info("../pw_assert_log", "abspath")
dir_pw_assert_tokenized = get_path_info("../pw_assert_tokenized", "abspath")
dir_pw_assert_zephyr = get_path_info("../pw_assert_zephyr", "abspath")
+ dir_pw_async = get_path_info("../pw_async", "abspath")
+ dir_pw_async_basic = get_path_info("../pw_async_basic", "abspath")
dir_pw_base64 = get_path_info("../pw_base64", "abspath")
dir_pw_bloat = get_path_info("../pw_bloat", "abspath")
dir_pw_blob_store = get_path_info("../pw_blob_store", "abspath")
+ dir_pw_bluetooth = get_path_info("../pw_bluetooth", "abspath")
dir_pw_bluetooth_hci = get_path_info("../pw_bluetooth_hci", "abspath")
+ dir_pw_bluetooth_profiles =
+ get_path_info("../pw_bluetooth_profiles", "abspath")
dir_pw_boot = get_path_info("../pw_boot", "abspath")
dir_pw_boot_cortex_m = get_path_info("../pw_boot_cortex_m", "abspath")
dir_pw_build = get_path_info("../pw_build", "abspath")
@@ -55,12 +61,15 @@ declare_args() {
dir_pw_chrono_threadx = get_path_info("../pw_chrono_threadx", "abspath")
dir_pw_chrono_zephyr = get_path_info("../pw_chrono_zephyr", "abspath")
dir_pw_cli = get_path_info("../pw_cli", "abspath")
+ dir_pw_compilation_testing =
+ get_path_info("../pw_compilation_testing", "abspath")
dir_pw_console = get_path_info("../pw_console", "abspath")
dir_pw_containers = get_path_info("../pw_containers", "abspath")
dir_pw_cpu_exception = get_path_info("../pw_cpu_exception", "abspath")
dir_pw_cpu_exception_cortex_m =
get_path_info("../pw_cpu_exception_cortex_m", "abspath")
dir_pw_crypto = get_path_info("../pw_crypto", "abspath")
+ dir_pw_digital_io = get_path_info("../pw_digital_io", "abspath")
dir_pw_docgen = get_path_info("../pw_docgen", "abspath")
dir_pw_doctor = get_path_info("../pw_doctor", "abspath")
dir_pw_env_setup = get_path_info("../pw_env_setup", "abspath")
@@ -71,10 +80,12 @@ declare_args() {
dir_pw_hex_dump = get_path_info("../pw_hex_dump", "abspath")
dir_pw_i2c = get_path_info("../pw_i2c", "abspath")
dir_pw_i2c_mcuxpresso = get_path_info("../pw_i2c_mcuxpresso", "abspath")
+ dir_pw_ide = get_path_info("../pw_ide", "abspath")
dir_pw_interrupt = get_path_info("../pw_interrupt", "abspath")
dir_pw_interrupt_cortex_m =
get_path_info("../pw_interrupt_cortex_m", "abspath")
dir_pw_interrupt_zephyr = get_path_info("../pw_interrupt_zephyr", "abspath")
+ dir_pw_intrusive_ptr = get_path_info("../pw_intrusive_ptr", "abspath")
dir_pw_kvs = get_path_info("../pw_kvs", "abspath")
dir_pw_libc = get_path_info("../pw_libc", "abspath")
dir_pw_log = get_path_info("../pw_log", "abspath")
@@ -93,6 +104,7 @@ declare_args() {
dir_pw_module = get_path_info("../pw_module", "abspath")
dir_pw_multisink = get_path_info("../pw_multisink", "abspath")
dir_pw_package = get_path_info("../pw_package", "abspath")
+ dir_pw_perf_test = get_path_info("../pw_perf_test", "abspath")
dir_pw_persistent_ram = get_path_info("../pw_persistent_ram", "abspath")
dir_pw_polyfill = get_path_info("../pw_polyfill", "abspath")
dir_pw_preprocessor = get_path_info("../pw_preprocessor", "abspath")
@@ -104,6 +116,7 @@ declare_args() {
dir_pw_ring_buffer = get_path_info("../pw_ring_buffer", "abspath")
dir_pw_router = get_path_info("../pw_router", "abspath")
dir_pw_rpc = get_path_info("../pw_rpc", "abspath")
+ dir_pw_rust = get_path_info("../pw_rust", "abspath")
dir_pw_snapshot = get_path_info("../pw_snapshot", "abspath")
dir_pw_software_update = get_path_info("../pw_software_update", "abspath")
dir_pw_span = get_path_info("../pw_span", "abspath")
@@ -129,6 +142,7 @@ declare_args() {
dir_pw_sys_io_emcraft_sf2 =
get_path_info("../pw_sys_io_emcraft_sf2", "abspath")
dir_pw_sys_io_mcuxpresso = get_path_info("../pw_sys_io_mcuxpresso", "abspath")
+ dir_pw_sys_io_pico = get_path_info("../pw_sys_io_pico", "abspath")
dir_pw_sys_io_stdio = get_path_info("../pw_sys_io_stdio", "abspath")
dir_pw_sys_io_stm32cube = get_path_info("../pw_sys_io_stm32cube", "abspath")
dir_pw_sys_io_zephyr = get_path_info("../pw_sys_io_zephyr", "abspath")
@@ -139,6 +153,7 @@ declare_args() {
dir_pw_thread_freertos = get_path_info("../pw_thread_freertos", "abspath")
dir_pw_thread_stl = get_path_info("../pw_thread_stl", "abspath")
dir_pw_thread_threadx = get_path_info("../pw_thread_threadx", "abspath")
+ dir_pw_thread_zephyr = get_path_info("../pw_thread_zephyr", "abspath")
dir_pw_tls_client = get_path_info("../pw_tls_client", "abspath")
dir_pw_tls_client_boringssl =
get_path_info("../pw_tls_client_boringssl", "abspath")
@@ -153,7 +168,7 @@ declare_args() {
dir_pw_unit_test = get_path_info("../pw_unit_test", "abspath")
dir_pw_varint = get_path_info("../pw_varint", "abspath")
dir_pw_watch = get_path_info("../pw_watch", "abspath")
- dir_pw_web_ui = get_path_info("../pw_web_ui", "abspath")
+ dir_pw_web = get_path_info("../pw_web", "abspath")
dir_pw_work_queue = get_path_info("../pw_work_queue", "abspath")
}
@@ -163,6 +178,7 @@ declare_args() {
# A list with paths to all Pigweed module. DO NOT SET THIS BUILD ARGUMENT!
pw_modules = [
dir_docker,
+ dir_pw_alignment,
dir_pw_allocator,
dir_pw_analog,
dir_pw_android_toolchain,
@@ -172,10 +188,14 @@ declare_args() {
dir_pw_assert_log,
dir_pw_assert_tokenized,
dir_pw_assert_zephyr,
+ dir_pw_async,
+ dir_pw_async_basic,
dir_pw_base64,
dir_pw_bloat,
dir_pw_blob_store,
+ dir_pw_bluetooth,
dir_pw_bluetooth_hci,
+ dir_pw_bluetooth_profiles,
dir_pw_boot,
dir_pw_boot_cortex_m,
dir_pw_build,
@@ -190,11 +210,13 @@ declare_args() {
dir_pw_chrono_threadx,
dir_pw_chrono_zephyr,
dir_pw_cli,
+ dir_pw_compilation_testing,
dir_pw_console,
dir_pw_containers,
dir_pw_cpu_exception,
dir_pw_cpu_exception_cortex_m,
dir_pw_crypto,
+ dir_pw_digital_io,
dir_pw_docgen,
dir_pw_doctor,
dir_pw_env_setup,
@@ -205,9 +227,11 @@ declare_args() {
dir_pw_hex_dump,
dir_pw_i2c,
dir_pw_i2c_mcuxpresso,
+ dir_pw_ide,
dir_pw_interrupt,
dir_pw_interrupt_cortex_m,
dir_pw_interrupt_zephyr,
+ dir_pw_intrusive_ptr,
dir_pw_kvs,
dir_pw_libc,
dir_pw_log,
@@ -225,6 +249,7 @@ declare_args() {
dir_pw_module,
dir_pw_multisink,
dir_pw_package,
+ dir_pw_perf_test,
dir_pw_persistent_ram,
dir_pw_polyfill,
dir_pw_preprocessor,
@@ -236,6 +261,7 @@ declare_args() {
dir_pw_ring_buffer,
dir_pw_router,
dir_pw_rpc,
+ dir_pw_rust,
dir_pw_snapshot,
dir_pw_software_update,
dir_pw_span,
@@ -258,6 +284,7 @@ declare_args() {
dir_pw_sys_io_baremetal_stm32f429,
dir_pw_sys_io_emcraft_sf2,
dir_pw_sys_io_mcuxpresso,
+ dir_pw_sys_io_pico,
dir_pw_sys_io_stdio,
dir_pw_sys_io_stm32cube,
dir_pw_sys_io_zephyr,
@@ -268,6 +295,7 @@ declare_args() {
dir_pw_thread_freertos,
dir_pw_thread_stl,
dir_pw_thread_threadx,
+ dir_pw_thread_zephyr,
dir_pw_tls_client,
dir_pw_tls_client_boringssl,
dir_pw_tls_client_mbedtls,
@@ -280,43 +308,89 @@ declare_args() {
dir_pw_unit_test,
dir_pw_varint,
dir_pw_watch,
- dir_pw_web_ui,
+ dir_pw_web,
dir_pw_work_queue,
]
# A list with all Pigweed module test groups. DO NOT SET THIS BUILD ARGUMENT!
pw_module_tests = [
+ "$dir_docker:tests",
+ "$dir_pw_alignment:tests",
"$dir_pw_allocator:tests",
"$dir_pw_analog:tests",
+ "$dir_pw_android_toolchain:tests",
+ "$dir_pw_arduino_build:tests",
"$dir_pw_assert:tests",
+ "$dir_pw_assert_basic:tests",
+ "$dir_pw_assert_log:tests",
+ "$dir_pw_assert_tokenized:tests",
+ "$dir_pw_assert_zephyr:tests",
+ "$dir_pw_async:tests",
+ "$dir_pw_async_basic:tests",
"$dir_pw_base64:tests",
+ "$dir_pw_bloat:tests",
"$dir_pw_blob_store:tests",
+ "$dir_pw_bluetooth:tests",
"$dir_pw_bluetooth_hci:tests",
+ "$dir_pw_bluetooth_profiles:tests",
+ "$dir_pw_boot:tests",
+ "$dir_pw_boot_cortex_m:tests",
+ "$dir_pw_build:tests",
+ "$dir_pw_build_info:tests",
+ "$dir_pw_build_mcuxpresso:tests",
"$dir_pw_bytes:tests",
"$dir_pw_checksum:tests",
"$dir_pw_chrono:tests",
+ "$dir_pw_chrono_embos:tests",
+ "$dir_pw_chrono_freertos:tests",
+ "$dir_pw_chrono_stl:tests",
+ "$dir_pw_chrono_threadx:tests",
+ "$dir_pw_chrono_zephyr:tests",
+ "$dir_pw_cli:tests",
+ "$dir_pw_compilation_testing:tests",
+ "$dir_pw_console:tests",
"$dir_pw_containers:tests",
+ "$dir_pw_cpu_exception:tests",
"$dir_pw_cpu_exception_cortex_m:tests",
"$dir_pw_crypto:tests",
+ "$dir_pw_digital_io:tests",
+ "$dir_pw_docgen:tests",
+ "$dir_pw_doctor:tests",
+ "$dir_pw_env_setup:tests",
"$dir_pw_file:tests",
"$dir_pw_function:tests",
"$dir_pw_fuzzer:tests",
"$dir_pw_hdlc:tests",
"$dir_pw_hex_dump:tests",
"$dir_pw_i2c:tests",
+ "$dir_pw_i2c_mcuxpresso:tests",
+ "$dir_pw_ide:tests",
+ "$dir_pw_interrupt:tests",
+ "$dir_pw_interrupt_cortex_m:tests",
+ "$dir_pw_interrupt_zephyr:tests",
+ "$dir_pw_intrusive_ptr:tests",
"$dir_pw_kvs:tests",
"$dir_pw_libc:tests",
"$dir_pw_log:tests",
+ "$dir_pw_log_android:tests",
+ "$dir_pw_log_basic:tests",
"$dir_pw_log_null:tests",
"$dir_pw_log_rpc:tests",
+ "$dir_pw_log_string:tests",
"$dir_pw_log_tokenized:tests",
+ "$dir_pw_log_zephyr:tests",
+ "$dir_pw_malloc:tests",
"$dir_pw_malloc_freelist:tests",
"$dir_pw_metric:tests",
"$dir_pw_minimal_cpp_stdlib:tests",
+ "$dir_pw_module:tests",
"$dir_pw_multisink:tests",
+ "$dir_pw_package:tests",
+ "$dir_pw_perf_test:tests",
"$dir_pw_persistent_ram:tests",
"$dir_pw_polyfill:tests",
"$dir_pw_preprocessor:tests",
+ "$dir_pw_presubmit:tests",
"$dir_pw_protobuf:tests",
"$dir_pw_protobuf_compiler:tests",
"$dir_pw_random:tests",
@@ -324,34 +398,61 @@ declare_args() {
"$dir_pw_ring_buffer:tests",
"$dir_pw_router:tests",
"$dir_pw_rpc:tests",
+ "$dir_pw_rust:tests",
"$dir_pw_snapshot:tests",
"$dir_pw_software_update:tests",
"$dir_pw_span:tests",
"$dir_pw_spi:tests",
"$dir_pw_status:tests",
+ "$dir_pw_stm32cube_build:tests",
"$dir_pw_stream:tests",
"$dir_pw_string:tests",
+ "$dir_pw_symbolizer:tests",
"$dir_pw_sync:tests",
+ "$dir_pw_sync_baremetal:tests",
+ "$dir_pw_sync_embos:tests",
+ "$dir_pw_sync_freertos:tests",
+ "$dir_pw_sync_stl:tests",
+ "$dir_pw_sync_threadx:tests",
+ "$dir_pw_sync_zephyr:tests",
+ "$dir_pw_sys_io:tests",
+ "$dir_pw_sys_io_arduino:tests",
+ "$dir_pw_sys_io_baremetal_lm3s6965evb:tests",
+ "$dir_pw_sys_io_baremetal_stm32f429:tests",
+ "$dir_pw_sys_io_emcraft_sf2:tests",
+ "$dir_pw_sys_io_mcuxpresso:tests",
+ "$dir_pw_sys_io_pico:tests",
+ "$dir_pw_sys_io_stdio:tests",
+ "$dir_pw_sys_io_stm32cube:tests",
+ "$dir_pw_sys_io_zephyr:tests",
+ "$dir_pw_system:tests",
+ "$dir_pw_target_runner:tests",
"$dir_pw_thread:tests",
"$dir_pw_thread_embos:tests",
"$dir_pw_thread_freertos:tests",
"$dir_pw_thread_stl:tests",
"$dir_pw_thread_threadx:tests",
+ "$dir_pw_thread_zephyr:tests",
"$dir_pw_tls_client:tests",
"$dir_pw_tls_client_boringssl:tests",
"$dir_pw_tls_client_mbedtls:tests",
"$dir_pw_tokenizer:tests",
+ "$dir_pw_tool:tests",
+ "$dir_pw_toolchain:tests",
"$dir_pw_trace:tests",
"$dir_pw_trace_tokenized:tests",
"$dir_pw_transfer:tests",
"$dir_pw_unit_test:tests",
"$dir_pw_varint:tests",
+ "$dir_pw_watch:tests",
+ "$dir_pw_web:tests",
"$dir_pw_work_queue:tests",
]
# A list with all Pigweed modules docs groups. DO NOT SET THIS BUILD ARGUMENT!
pw_module_docs = [
"$dir_docker:docs",
+ "$dir_pw_alignment:docs",
"$dir_pw_allocator:docs",
"$dir_pw_analog:docs",
"$dir_pw_android_toolchain:docs",
@@ -361,10 +462,14 @@ declare_args() {
"$dir_pw_assert_log:docs",
"$dir_pw_assert_tokenized:docs",
"$dir_pw_assert_zephyr:docs",
+ "$dir_pw_async:docs",
+ "$dir_pw_async_basic:docs",
"$dir_pw_base64:docs",
"$dir_pw_bloat:docs",
"$dir_pw_blob_store:docs",
+ "$dir_pw_bluetooth:docs",
"$dir_pw_bluetooth_hci:docs",
+ "$dir_pw_bluetooth_profiles:docs",
"$dir_pw_boot:docs",
"$dir_pw_boot_cortex_m:docs",
"$dir_pw_build:docs",
@@ -379,11 +484,13 @@ declare_args() {
"$dir_pw_chrono_threadx:docs",
"$dir_pw_chrono_zephyr:docs",
"$dir_pw_cli:docs",
+ "$dir_pw_compilation_testing:docs",
"$dir_pw_console:docs",
"$dir_pw_containers:docs",
"$dir_pw_cpu_exception:docs",
"$dir_pw_cpu_exception_cortex_m:docs",
"$dir_pw_crypto:docs",
+ "$dir_pw_digital_io:docs",
"$dir_pw_docgen:docs",
"$dir_pw_doctor:docs",
"$dir_pw_env_setup:docs",
@@ -394,17 +501,21 @@ declare_args() {
"$dir_pw_hex_dump:docs",
"$dir_pw_i2c:docs",
"$dir_pw_i2c_mcuxpresso:docs",
+ "$dir_pw_ide:docs",
"$dir_pw_interrupt:docs",
"$dir_pw_interrupt_cortex_m:docs",
"$dir_pw_interrupt_zephyr:docs",
+ "$dir_pw_intrusive_ptr:docs",
"$dir_pw_kvs:docs",
"$dir_pw_libc:docs",
"$dir_pw_log:docs",
+ "$dir_pw_log_android:docs",
"$dir_pw_log_basic:docs",
"$dir_pw_log_null:docs",
"$dir_pw_log_rpc:docs",
"$dir_pw_log_string:docs",
"$dir_pw_log_tokenized:docs",
+ "$dir_pw_log_zephyr:docs",
"$dir_pw_malloc:docs",
"$dir_pw_malloc_freelist:docs",
"$dir_pw_metric:docs",
@@ -412,6 +523,7 @@ declare_args() {
"$dir_pw_module:docs",
"$dir_pw_multisink:docs",
"$dir_pw_package:docs",
+ "$dir_pw_perf_test:docs",
"$dir_pw_persistent_ram:docs",
"$dir_pw_polyfill:docs",
"$dir_pw_preprocessor:docs",
@@ -423,6 +535,7 @@ declare_args() {
"$dir_pw_ring_buffer:docs",
"$dir_pw_router:docs",
"$dir_pw_rpc:docs",
+ "$dir_pw_rust:docs",
"$dir_pw_snapshot:docs",
"$dir_pw_software_update:docs",
"$dir_pw_span:docs",
@@ -441,9 +554,11 @@ declare_args() {
"$dir_pw_sync_zephyr:docs",
"$dir_pw_sys_io:docs",
"$dir_pw_sys_io_arduino:docs",
+ "$dir_pw_sys_io_baremetal_lm3s6965evb:docs",
"$dir_pw_sys_io_baremetal_stm32f429:docs",
"$dir_pw_sys_io_emcraft_sf2:docs",
"$dir_pw_sys_io_mcuxpresso:docs",
+ "$dir_pw_sys_io_pico:docs",
"$dir_pw_sys_io_stdio:docs",
"$dir_pw_sys_io_stm32cube:docs",
"$dir_pw_sys_io_zephyr:docs",
@@ -454,10 +569,12 @@ declare_args() {
"$dir_pw_thread_freertos:docs",
"$dir_pw_thread_stl:docs",
"$dir_pw_thread_threadx:docs",
+ "$dir_pw_thread_zephyr:docs",
"$dir_pw_tls_client:docs",
"$dir_pw_tls_client_boringssl:docs",
"$dir_pw_tls_client_mbedtls:docs",
"$dir_pw_tokenizer:docs",
+ "$dir_pw_tool:docs",
"$dir_pw_toolchain:docs",
"$dir_pw_trace:docs",
"$dir_pw_trace_tokenized:docs",
@@ -465,7 +582,7 @@ declare_args() {
"$dir_pw_unit_test:docs",
"$dir_pw_varint:docs",
"$dir_pw_watch:docs",
- "$dir_pw_web_ui:docs",
+ "$dir_pw_web:docs",
"$dir_pw_work_queue:docs",
]
}
diff --git a/pw_build/gn_internal/build_target.gni b/pw_build/gn_internal/build_target.gni
new file mode 100644
index 000000000..149c87eac
--- /dev/null
+++ b/pw_build/gn_internal/build_target.gni
@@ -0,0 +1,170 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+declare_args() {
+ # Additional build targets to add as dependencies for pw_executable,
+ # pw_static_library, and pw_shared_library targets. The
+ # $dir_pw_build:link_deps target pulls in these libraries.
+ #
+ # pw_build_LINK_DEPS can be used to break circular dependencies for low-level
+ # libraries such as pw_assert.
+ pw_build_LINK_DEPS = []
+
+ # The name of the GN target type used to build Pigweed executables.
+ #
+ # If this is a custom template, the .gni file containing the template must
+ # be imported at the top of the target configuration file to make it globally
+ # available.
+ pw_build_EXECUTABLE_TARGET_TYPE = "executable"
+
+ # The path to the .gni file that defines pw_build_EXECUTABLE_TARGET_TYPE.
+ #
+ # If pw_build_EXECUTABLE_TARGET_TYPE is not the default of `executable`, this
+ # .gni file is imported to provide the template definition.
+ pw_build_EXECUTABLE_TARGET_TYPE_FILE = ""
+}
+
+# This template is the underlying implementation that defines what makes
+# pw_source_set, pw_executable, pw_shared_library, and pw_static_library unique.
+# For more information, see the documentation at
+# https://pigweed.dev/pw_build/?highlight=pw_executable#target-types
+#
+# In addition to the arguments supported by the underlying native target types,
+# this template introduces the following arguments:
+#
+# add_global_link_deps: (required) If true, adds global link dependencies as
+# specified by the current toolchain via pw_build_LINK_DEPS as dependencies
+# to all instantiations of the current target type.
+# underlying_target_type: (required) The underlying target type to use for this
+# template. This is done so different C/C++ build target types can share the
+# same underlying wrapper implementation.
+# target_type_file: (optional) If the underlying target type is not one of GN's
+# builtin types, the path to the .gni file that defines the template
+# referenced by underlying_target_type. This is exclusively to support
+# pw_exeuctable's behavior that allows a pw_executable to be essentially
+# aliased to a custom target type.
+# remove_configs: (optional) A list of configs to remove from the set of
+# default configs specified by the current toolchain configuration.
+# remove_public_deps: (optional) A list of targets to remove from the set of
+# default public_deps specified by the current toolchain configuration.
+template("pw_internal_build_target") {
+ assert(defined(invoker.underlying_target_type),
+ "Build targets using this template must specify a target type")
+ _pw_source_files = []
+ _supported_toolchain_defaults = [
+ "configs",
+ "public_deps",
+ ]
+
+ # Boilerplate for tracking target sources. For more information see
+ # https://pigweed.dev/pw_build/#target-types
+ if (defined(invoker.sources)) {
+ foreach(path, invoker.sources) {
+ _pw_source_files += [ path ]
+ }
+ }
+ if (defined(invoker.public)) {
+ foreach(path, invoker.public) {
+ _pw_source_files += [ path ]
+ }
+ }
+
+ _builtin_target_types = [
+ "executable",
+ "rust_library",
+ "shared_library",
+ "source_set",
+ "static_library",
+ ]
+ if (filter_include(_builtin_target_types,
+ [ invoker.underlying_target_type ]) == []) {
+ assert(
+ defined(invoker.target_type_file) && invoker.target_type_file != "",
+ string_join(
+ ", ",
+ [
+ "Unknown target type ${invoker.underlying_target_type}",
+ "set target_type_file to the .gni file that defines this type.",
+ ]))
+ import(invoker.target_type_file)
+ }
+
+ target(invoker.underlying_target_type, target_name) {
+ # TODO(b/260111641): This import is terrible, and breaks typical Pigweed GN
+ # build arg naming patterns.
+ import("$dir_pw_build/defaults.gni")
+ forward_variables_from(
+ invoker,
+ "*",
+ _supported_toolchain_defaults + [ "target_type_file" ])
+
+ # Ensure that we don't overwrite metadata forwarded from the invoker above.
+ if (defined(metadata)) {
+ metadata.pw_source_files = _pw_source_files
+ } else {
+ metadata = {
+ pw_source_files = _pw_source_files
+ }
+ }
+
+ if (!defined(configs)) {
+ configs = []
+ }
+ if (defined(pw_build_defaults.configs)) {
+ configs += pw_build_defaults.configs
+ }
+ if (defined(remove_configs)) {
+ if (remove_configs != [] && remove_configs[0] == "*") {
+ configs = []
+ } else {
+ configs -= filter_include(configs, remove_configs)
+ }
+ }
+ if (defined(invoker.configs)) {
+ configs += invoker.configs
+ }
+
+ if (defined(pw_build_defaults.public_deps)) {
+ public_deps = pw_build_defaults.public_deps
+ } else {
+ public_deps = []
+ }
+ if (defined(remove_public_deps)) {
+ if (remove_public_deps != [] && remove_public_deps[0] == "*") {
+ public_deps = []
+ } else {
+ public_deps += remove_public_deps
+ public_deps -= remove_public_deps
+ }
+ }
+ if (defined(invoker.public_deps)) {
+ public_deps += invoker.public_deps
+ }
+
+ assert(defined(add_global_link_deps),
+ "All targets MUST specify whether or not to include link deps")
+ if (!defined(deps)) {
+ deps = []
+ }
+ if (add_global_link_deps) {
+ deps += [ "$dir_pw_build:link_deps" ]
+ }
+
+ if (defined(pw_build_defaults.visibility) && !defined(visibility)) {
+ visibility = pw_build_defaults.visibility
+ }
+ }
+}
diff --git a/pw_build/hil.gni b/pw_build/hil.gni
index bb3b4eef2..1d3b27074 100644
--- a/pw_build/hil.gni
+++ b/pw_build/hil.gni
@@ -18,27 +18,59 @@ import("$dir_pw_build/python.gni")
# A hardware-in-the-loop (HIL) test.
#
-# The HIL tests are assumed to require exclusive access to a device-under-test,
-# and are always executed serially.
+# Use this template to declare an executable that implements a test requiring
+# exclusive access to a hardware device. The template ensures that all such
+# tests are executed serially.
+
+# The arguments to pass to this template largely depend on the specified
+# target_type . The only currently supported target_type is "python", which
+# causes this template to behave simlarly to a pw_python_script . Aside from
+# the arguments explicitly listed below, refer to the pw_python_script
+# template for information on what arguments are required or available.
#
# Args:
+# action_args: Forwarded to the action.
+# action_deps: Forwarded to the action.
+# action_outputs: Forwarded to the action.
# target_type: The type of underlying target that implements the HIL
# test. Currently "python" is the only supported value, producing a
# "py_python_script". Support for other languages will be added in the
# future.
template("pw_hil_test") {
+ assert(!defined(invoker.action), "Can't specify an action for a pw_hil_test")
assert(defined(invoker.target_type),
"pw_hil_test must specify the 'target_type'")
+ _args = []
+ if (defined(invoker.action_args)) {
+ _args += invoker.action_args
+ }
+ _outputs = []
+ if (defined(invoker.action_outputs)) {
+ _outputs += invoker.action_outputs
+ }
+ _deps = []
+ if (defined(invoker.action_deps)) {
+ _deps += invoker.action_deps
+ }
+
if (invoker.target_type == "python") {
pw_python_script(target_name) {
action = {
+ args = _args
pool = "$dir_pw_build/pool:pw_hil_test($default_toolchain)"
stamp = true
+ outputs = _outputs
+ deps = _deps
# We want the test stdout to be saved.
capture_output = false
}
- forward_variables_from(invoker, "*", [ "target_type" ])
+ forward_variables_from(invoker,
+ "*",
+ [
+ "target_type",
+ "action_args",
+ ])
}
}
}
diff --git a/pw_build/host_tool.gni b/pw_build/host_tool.gni
index 53344f3bb..ac7d7c8a7 100644
--- a/pw_build/host_tool.gni
+++ b/pw_build/host_tool.gni
@@ -44,6 +44,7 @@ template("pw_host_tool") {
script = "$dir_pw_build/py/pw_build/host_tool.py"
args = _script_args
deps = [ invoker.tool ]
+ python_deps = [ "$dir_pw_cli/py" ]
stamp = true
}
}
diff --git a/pw_build/linker_script.gni b/pw_build/linker_script.gni
index 0cf681b06..8418f34d4 100644
--- a/pw_build/linker_script.gni
+++ b/pw_build/linker_script.gni
@@ -34,8 +34,11 @@ import("$dir_pw_toolchain/generate_toolchain.gni")
#
# cflags: Flags to pass to the C compiler.
#
+# includes: Include these files when running the C preprocessor.
+#
# inputs: Files that, when changed, should trigger a re-build of the linker
-# script. linker_script is implicitly added to this by the template.
+# script. linker_script and includes are implicitly added to this by the
+# template.
#
# Example:
#
@@ -76,9 +79,22 @@ template("pw_linker_script") {
rebase_path(invoker.linker_script, root_build_dir),
]
- # Include any explicitly listed c flags.
+ # Trigger a re-generation of the linker script when these inputs change.
+ if (defined(invoker.inputs)) {
+ inputs += invoker.inputs
+ }
+
+ # Include any explicitly listed C flags.
if (defined(invoker.cflags)) {
- args += cflags
+ args += invoker.cflags
+ }
+
+ # Include files from the command line.
+ if (defined(invoker.includes)) {
+ inputs += invoker.includes
+ foreach(include_file, invoker.includes) {
+ args += [ "-include" + rebase_path(include_file, root_build_dir) ]
+ }
}
# Add defines.
@@ -110,9 +126,6 @@ template("pw_linker_script") {
# that depends on it.
pw_source_set(target_name) {
inputs = [ _final_linker_script ]
- if (defined(invoker.inputs)) {
- inputs += invoker.inputs
- }
all_dependent_configs = [ ":${target_name}_config" ]
deps = [ ":${target_name}_preprocess" ]
}
diff --git a/pw_build/pigweed.bzl b/pw_build/pigweed.bzl
index a1e24bc10..57274d488 100644
--- a/pw_build/pigweed.bzl
+++ b/pw_build/pigweed.bzl
@@ -15,33 +15,89 @@
load(
"//pw_build/bazel_internal:pigweed_internal.bzl",
- _add_cc_and_c_targets = "add_cc_and_c_targets",
- _has_pw_assert_dep = "has_pw_assert_dep",
+ _add_defaults = "add_defaults",
)
def pw_cc_binary(**kwargs):
+ """Wrapper for cc_binary providing some defaults.
+
+ Specifically, this wrapper,
+
+ * Adds default copts.
+ * Adds a dep on the pw_assert backend.
+ * Sets "linkstatic" to True.
+ * Disables header modules (via the feature -use_header_modules).
+
+ Args:
+ **kwargs: Passed to cc_binary.
+ """
kwargs["deps"] = kwargs.get("deps", [])
- # TODO(pwbug/440): Remove this implicit dependency once we have a better
+ # TODO(b/234877642): Remove this implicit dependency once we have a better
# way to handle the facades without introducing a circular dependency into
# the build.
- if not _has_pw_assert_dep(kwargs["deps"]):
- kwargs["deps"].append("@pigweed//pw_assert")
- _add_cc_and_c_targets(native.cc_binary, kwargs)
+ kwargs["deps"] = kwargs["deps"] + ["@pigweed_config//:pw_assert_backend"]
+ _add_defaults(kwargs)
+ native.cc_binary(**kwargs)
def pw_cc_library(**kwargs):
- _add_cc_and_c_targets(native.cc_library, kwargs)
+ """Wrapper for cc_library providing some defaults.
+
+ Specifically, this wrapper,
+
+ * Adds default copts.
+ * Sets "linkstatic" to True.
+ * Disables header modules (via the feature -use_header_modules).
+
+ Args:
+ **kwargs: Passed to cc_library.
+ """
+ _add_defaults(kwargs)
+ native.cc_library(**kwargs)
def pw_cc_test(**kwargs):
+ """Wrapper for cc_test providing some defaults.
+
+ Specifically, this wrapper,
+
+ * Adds default copts.
+ * Adds a dep on the pw_assert backend.
+ * Adds a dep on //pw_unit_test:simple_printing_main
+ * Sets "linkstatic" to True.
+ * Disables header modules (via the feature -use_header_modules).
+
+ Args:
+ **kwargs: Passed to cc_test.
+ """
kwargs["deps"] = kwargs.get("deps", []) + \
- ["//pw_unit_test:main"]
+ ["@pigweed//pw_unit_test:simple_printing_main"]
- # TODO(pwbug/440): Remove this implicit dependency once we have a better
+ # TODO(b/234877642): Remove this implicit dependency once we have a better
# way to handle the facades without introducing a circular dependency into
# the build.
- if not _has_pw_assert_dep(kwargs["deps"]):
- kwargs["deps"].append("@pigweed//pw_assert")
- _add_cc_and_c_targets(native.cc_test, kwargs)
+ kwargs["deps"] = kwargs["deps"] + ["@pigweed_config//:pw_assert_backend"]
+ _add_defaults(kwargs)
+ native.cc_test(**kwargs)
+
+def pw_cc_perf_test(**kwargs):
+ """A Pigweed performance test.
+
+ This macro produces a cc_binary and,
+
+ * Adds default copts.
+ * Adds a dep on the pw_assert backend.
+ * Adds a dep on //pw_perf_test:logging_main
+ * Sets "linkstatic" to True.
+ * Disables header modules (via the feature -use_header_modules).
+
+ Args:
+ **kwargs: Passed to cc_binary.
+ """
+ kwargs["deps"] = kwargs.get("deps", []) + \
+ ["@pigweed//pw_perf_test:logging_main"]
+ kwargs["deps"] = kwargs["deps"] + ["@pigweed_config//:pw_assert_backend"]
+ _add_defaults(kwargs)
+ native.cc_binary(**kwargs)
def pw_cc_facade(**kwargs):
# Bazel facades should be source only cc_library's this is to simplify
@@ -53,4 +109,5 @@ def pw_cc_facade(**kwargs):
if "srcs" in kwargs.keys():
fail("'srcs' attribute does not exist in pw_cc_facade, please use \
main implementing target.")
- _add_cc_and_c_targets(native.cc_library, kwargs)
+ _add_defaults(kwargs)
+ native.cc_library(**kwargs)
diff --git a/pw_build/pigweed.cmake b/pw_build/pigweed.cmake
index ceed0b609..a1b2e871f 100644
--- a/pw_build/pigweed.cmake
+++ b/pw_build/pigweed.cmake
@@ -13,6 +13,8 @@
# the License.
include_guard(GLOBAL)
+cmake_minimum_required(VERSION 3.19)
+
# The PW_ROOT environment variable should be set in bootstrap. If it is not set,
# set it to the root of the Pigweed repository.
if("$ENV{PW_ROOT}" STREQUAL "")
@@ -22,9 +24,60 @@ if("$ENV{PW_ROOT}" STREQUAL "")
set(ENV{PW_ROOT} "${pw_root}")
endif()
+# TOOD(ewout, hepler): Remove this legacy include once all users pull in
+# pw_unit_test/test.cmake for test functions and variables instead of relying
+# on them to be provided by pw_build/pigweed.cmake.
+include("$ENV{PW_ROOT}/pw_unit_test/test.cmake")
+
+# Wrapper around cmake_parse_arguments that fails with an error if any arguments
+# remained unparsed or a required argument was not provided.
+#
+# All parsed arguments are prefixed with "arg_". This helper can only be used
+# by functions, not macros.
+#
+# Required Arguments:
+#
+# NUM_POSITIONAL_ARGS - PARSE_ARGV <N> arguments for
+# cmake_parse_arguments
+#
+# Optional Args:
+#
+# OPTION_ARGS - <option> arguments for cmake_parse_arguments
+# ONE_VALUE_ARGS - <one_value_keywords> arguments for cmake_parse_arguments
+# MULTI_VALUE_ARGS - <multi_value_keywords> arguments for
+# cmake_parse_arguments
+# REQUIRED_ARGS - required arguments which must be set, these may any
+# argument type (<option>, <one_value_keywords>, and/or
+# <multi_value_keywords>)
+#
+macro(pw_parse_arguments)
+ # First parse the arguments to this macro.
+ cmake_parse_arguments(
+ pw_parse_arg "" "NUM_POSITIONAL_ARGS"
+ "OPTION_ARGS;ONE_VALUE_ARGS;MULTI_VALUE_ARGS;REQUIRED_ARGS"
+ ${ARGN}
+ )
+ pw_require_args("pw_parse_arguments" "pw_parse_arg_" NUM_POSITIONAL_ARGS)
+ if(NOT "${pw_parse_arg_UNPARSED_ARGUMENTS}" STREQUAL "")
+ message(FATAL_ERROR "Unexpected arguments to pw_parse_arguments: "
+ "${pw_parse_arg_UNPARSED_ARGUMENTS}")
+ endif()
+
+ # Now that we have the macro's arguments, process the caller's arguments.
+ pw_parse_arguments_strict("${CMAKE_CURRENT_FUNCTION}"
+ "${pw_parse_arg_NUM_POSITIONAL_ARGS}"
+ "${pw_parse_arg_OPTION_ARGS}"
+ "${pw_parse_arg_ONE_VALUE_ARGS}"
+ "${pw_parse_arg_MULTI_VALUE_ARGS}"
+ )
+ pw_require_args("${CMAKE_CURRENT_FUNCTION}" "arg_"
+ ${pw_parse_arg_REQUIRED_ARGS})
+endmacro()
+
+# TODO(ewout, hepler): Deprecate this function in favor of pw_parse_arguments.
# Wrapper around cmake_parse_arguments that fails with an error if any arguments
# remained unparsed.
-macro(_pw_parse_argv_strict function start_arg options one multi)
+macro(pw_parse_arguments_strict function start_arg options one multi)
cmake_parse_arguments(PARSE_ARGV
"${start_arg}" arg "${options}" "${one}" "${multi}"
)
@@ -37,176 +90,158 @@ macro(_pw_parse_argv_strict function start_arg options one multi)
endif()
endmacro()
-# Automatically creates a library and test targets for the files in a module.
-# This function is only suitable for simple modules that meet the following
-# requirements:
-#
-# - The module exposes exactly one library.
-# - All source files in the module directory are included in the library.
-# - Each test in the module has
-# - exactly one source .cc file,
-# - optionally, one .c source with the same base name as the .cc file,
-# - only a dependency on the main module library.
-# - The module is not a facade.
+# Checks that one or more variables are set. This is used to check that
+# arguments were provided to a function. Fails with FATAL_ERROR if
+# ${ARG_PREFIX}${name} is empty. The FUNCTION_NAME is used in the error message.
+# If FUNCTION_NAME is "", it is set to CMAKE_CURRENT_FUNCTION.
#
-# Modules that do not meet these requirements may not use
-# pw_auto_add_simple_module. Instead, define the module's libraries and tests
-# with pw_add_module_library, pw_add_facade, pw_add_test, and the standard CMake
-# functions, such as add_library, target_link_libraries, etc.
+# Usage:
#
-# This function does the following:
+# pw_require_args(FUNCTION_NAME ARG_PREFIX ARG_NAME [ARG_NAME ...])
#
-# 1. Find all .c and .cc files in the module's root directory.
-# 2. Create a library with the module name using pw_add_module_library with
-# all source files that do not end with _test.cc.
-# 3. Declare a test for each source file that ends with _test.cc.
+# Examples:
#
-# Args:
+# # Checks that arg_FOO is non-empty, using the current function name.
+# pw_require_args("" arg_ FOO)
#
-# IMPLEMENTS_FACADE - this module implements the specified facade
-# PUBLIC_DEPS - public target_link_libraries arguments
-# PRIVATE_DEPS - private target_link_libraries arguments
+# # Checks that FOO and BAR are non-empty, using function name "do_the_thing".
+# pw_require_args(do_the_thing "" FOO BAR)
#
-function(pw_auto_add_simple_module MODULE)
- _pw_parse_argv_strict(pw_auto_add_simple_module 1
- ""
- "IMPLEMENTS_FACADE"
- "PUBLIC_DEPS;PRIVATE_DEPS;TEST_DEPS"
- )
-
- file(GLOB all_sources *.cc *.c)
-
- # Create a library with all source files not ending in _test.
- set(sources "${all_sources}")
- list(FILTER sources EXCLUDE REGEX "_test(\\.cc|(_c)?\\.c)$") # *_test.cc
- list(FILTER sources EXCLUDE REGEX "^test(\\.cc|(_c)?\\.c)$") # test.cc
- list(FILTER sources EXCLUDE REGEX "_fuzzer\\.cc$")
-
- file(GLOB_RECURSE headers *.h)
-
- if(arg_IMPLEMENTS_FACADE)
- set(groups backends)
+macro(pw_require_args FUNCTION_NAME ARG_PREFIX)
+ if("${FUNCTION_NAME}" STREQUAL "")
+ set(_pw_require_args_FUNCTION_NAME "${CMAKE_CURRENT_FUNCTION}")
else()
- set(groups modules "${MODULE}")
+ set(_pw_require_args_FUNCTION_NAME "${FUNCTION_NAME}")
endif()
- pw_add_module_library("${MODULE}"
- IMPLEMENTS_FACADES
- ${arg_IMPLEMENTS_FACADE}
- PUBLIC_DEPS
- ${arg_PUBLIC_DEPS}
- PRIVATE_DEPS
- ${arg_PRIVATE_DEPS}
- SOURCES
- ${sources}
- HEADERS
- ${headers}
- )
-
- pw_auto_add_module_tests("${MODULE}"
- PRIVATE_DEPS
- ${arg_PUBLIC_DEPS}
- ${arg_PRIVATE_DEPS}
- ${arg_TEST_DEPS}
- GROUPS
- ${groups}
- )
-endfunction(pw_auto_add_simple_module)
+ foreach(name IN ITEMS ${ARGN})
+ if("${${ARG_PREFIX}${name}}" STREQUAL "")
+ message(FATAL_ERROR "A value must be provided for ${name} in "
+ "${_pw_require_args_FUNCTION_NAME}.")
+ endif()
+ endforeach()
+endmacro()
-# Creates a test for each source file ending in _test. Tests with mutliple .cc
-# files or different dependencies than the module will not work correctly.
+# pw_target_link_targets: CMake target only form of target_link_libraries.
#
-# Args:
+# Helper wrapper around target_link_libraries which only supports CMake targets
+# and detects when the target does not exist.
+#
+# NOTE: Generator expressions are not supported.
+#
+# Due to the processing order of list files, the list of targets has to be
+# checked at the end of the root CMake list file. Instead of requiring all
+# list files to be modified, a DEFER CALL is used.
+#
+# Required Args:
+#
+# <name> - The library target to add the TARGET link dependencies to.
#
-# PRIVATE_DEPS - dependencies to apply to all tests
-# GROUPS - groups in addition to MODULE to which to add these tests
+# Optional Args:
#
-function(pw_auto_add_module_tests MODULE)
- _pw_parse_argv_strict(pw_auto_add_module_tests 1
- ""
- ""
- "PRIVATE_DEPS;GROUPS"
+# INTERFACE - interface target_link_libraries arguments which are all TARGETs.
+# PUBLIC - public target_link_libraries arguments which are all TARGETs.
+# PRIVATE - private target_link_libraries arguments which are all TARGETs.
+function(pw_target_link_targets NAME)
+ set(types INTERFACE PUBLIC PRIVATE )
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ MULTI_VALUE_ARGS
+ ${types}
)
- file(GLOB cc_tests *_test.cc)
-
- foreach(test IN LISTS cc_tests)
- get_filename_component(test_name "${test}" NAME_WE)
-
- # Find a .c test corresponding with the test .cc file, if any.
- file(GLOB c_test "${test_name}.c" "${test_name}_c.c")
+ if(NOT TARGET "${NAME}")
+ message(FATAL_ERROR "\"${NAME}\" must be a TARGET library")
+ endif()
- pw_add_test("${MODULE}.${test_name}"
- SOURCES
- "${test}"
- ${c_test}
- DEPS
- "$<TARGET_NAME_IF_EXISTS:${MODULE}>"
- ${arg_PRIVATE_DEPS}
- GROUPS
- "${MODULE}"
- ${arg_GROUPS}
- )
+ foreach(type IN LISTS types)
+ foreach(library IN LISTS arg_${type})
+ target_link_libraries(${NAME} ${type} ${library})
+ if(NOT TARGET ${library})
+ # It's possible the target has not yet been defined due to the ordering
+ # of add_subdirectory. Ergo defer the call until the end of the
+ # configuration phase.
+
+ # cmake_language(DEFER ...) evaluates arguments at the time the deferred
+ # call is executed, ergo wrap it in a cmake_language(EVAL CODE ...) to
+ # evaluate the arguments now. The arguments are wrapped in brackets to
+ # avoid re-evaluation at the deferred call.
+ cmake_language(EVAL CODE
+ "cmake_language(DEFER DIRECTORY ${CMAKE_SOURCE_DIR} CALL
+ _pw_target_link_targets_deferred_check
+ [[${NAME}]] [[${type}]] ${library})"
+ )
+ endif()
+ endforeach()
endforeach()
-endfunction(pw_auto_add_module_tests)
+endfunction()
-# Sets the provided variable to the common library arguments.
-macro(_pw_library_args variable)
- set("${variable}" SOURCES HEADERS PUBLIC_DEPS PRIVATE_DEPS ${ARGN})
+# Runs any deferred library checks for pw_target_link_targets.
+#
+# Required Args:
+#
+# <name> - The name of the library target to add the link dependencies to.
+# <type> - The type of the library (INTERFACE, PUBLIC, PRIVATE).
+# <library> - The library to check to assert it's a TARGET.
+function(_pw_target_link_targets_deferred_check NAME TYPE LIBRARY)
+ if(NOT TARGET ${LIBRARY})
+ message(FATAL_ERROR
+ "${NAME}'s ${TYPE} dep \"${LIBRARY}\" is not a target.")
+ endif()
+endfunction()
+
+# Sets the provided variable to the multi_value_keywords from pw_add_library.
+macro(_pw_add_library_multi_value_args variable)
+ set("${variable}" SOURCES HEADERS
+ PUBLIC_DEPS PRIVATE_DEPS
+ PUBLIC_INCLUDES PRIVATE_INCLUDES
+ PUBLIC_DEFINES PRIVATE_DEFINES
+ PUBLIC_COMPILE_OPTIONS PRIVATE_COMPILE_OPTIONS
+ PUBLIC_LINK_OPTIONS PRIVATE_LINK_OPTIONS "${ARGN}")
endmacro()
-# Creates a library in a module. The library has access to the public/ include
-# directory.
+# pw_add_library_generic: Creates a CMake library target.
#
-# Args:
+# Required Args:
+#
+# <name> - The name of the library target to be created.
+# <type> - The library type which must be INTERFACE, OBJECT, STATIC, or
+# SHARED.
+#
+# Optional Args:
#
# SOURCES - source files for this library
# HEADERS - header files for this library
-# PUBLIC_DEPS - public target_link_libraries arguments
-# PRIVATE_DEPS - private target_link_libraries arguments
+# PUBLIC_DEPS - public pw_target_link_targets arguments
+# PRIVATE_DEPS - private pw_target_link_targets arguments
# PUBLIC_INCLUDES - public target_include_directories argument
# PRIVATE_INCLUDES - public target_include_directories argument
-# IMPLEMENTS_FACADES - which facades this library implements
# PUBLIC_DEFINES - public target_compile_definitions arguments
# PRIVATE_DEFINES - private target_compile_definitions arguments
# PUBLIC_COMPILE_OPTIONS - public target_compile_options arguments
# PRIVATE_COMPILE_OPTIONS - private target_compile_options arguments
+# PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE - private target_compile_options BEFORE
+# arguments from the specified deps's INTERFACE_COMPILE_OPTIONS. Note that
+# these deps are not pulled in as target_link_libraries. This should not be
+# exposed by the non-generic API.
# PUBLIC_LINK_OPTIONS - public target_link_options arguments
# PRIVATE_LINK_OPTIONS - private target_link_options arguments
-#
-function(pw_add_module_library NAME)
- _pw_library_args(
- list_args
- PUBLIC_INCLUDES PRIVATE_INCLUDES
- IMPLEMENTS_FACADES
- PUBLIC_DEFINES PRIVATE_DEFINES
- PUBLIC_COMPILE_OPTIONS PRIVATE_COMPILE_OPTIONS
- PUBLIC_LINK_OPTIONS PRIVATE_LINK_OPTIONS
- )
- _pw_parse_argv_strict(pw_add_module_library 1 "" "" "${list_args}")
-
- # Check that the library's name is prefixed by the module name.
- get_filename_component(module "${CMAKE_CURRENT_SOURCE_DIR}" NAME)
-
- if(NOT "${NAME}" MATCHES "${module}(\\.[^\\.]+)?(\\.facade)?$")
- message(FATAL_ERROR
- "Module libraries must match the module name or be in the form "
- "'MODULE_NAME.LIBRARY_NAME'. The library '${NAME}' does not match."
- )
+function(pw_add_library_generic NAME TYPE)
+ set(supported_library_types INTERFACE OBJECT STATIC SHARED)
+ if(NOT "${TYPE}" IN_LIST supported_library_types)
+ message(FATAL_ERROR "\"${TYPE}\" is not a valid library type for ${NAME}. "
+ "Must be INTERFACE, OBJECT, STATIC, or SHARED.")
endif()
- # Instead of forking all of the code below or injecting an empty source file,
- # conditionally select PUBLIC vs INTERFACE depending on whether there are
- # sources to compile.
- if(NOT "${arg_SOURCES}" STREQUAL "")
- add_library("${NAME}" EXCLUDE_FROM_ALL)
- set(public_or_interface PUBLIC)
- else("${arg_SOURCES}" STREQUAL "")
- add_library("${NAME}" EXCLUDE_FROM_ALL INTERFACE)
- set(public_or_interface INTERFACE)
- endif(NOT "${arg_SOURCES}" STREQUAL "")
-
- target_sources("${NAME}" PRIVATE ${arg_SOURCES} ${arg_HEADERS})
+ _pw_add_library_multi_value_args(multi_value_args)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 2
+ MULTI_VALUE_ARGS
+ ${multi_value_args}
+ PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE
+ )
# CMake 3.22 does not have a notion of target_headers yet, so in the mean
# time we ask for headers to be specified for consistency with GN & Bazel and
@@ -222,84 +257,246 @@ function(pw_add_module_library NAME)
endif()
endforeach()
- if(NOT "${arg_PUBLIC_INCLUDES}" STREQUAL "")
- target_include_directories("${NAME}"
- ${public_or_interface}
- ${arg_PUBLIC_INCLUDES}
- )
- else("${arg_PUBLIC_INCLUDES}" STREQUAL "")
- # TODO(pwbug/601): Deprecate this legacy implicit PUBLIC_INCLUDES.
- target_include_directories("${NAME}" ${public_or_interface} public)
- endif(NOT "${arg_PUBLIC_INCLUDES}" STREQUAL "")
-
- if(NOT "${arg_PRIVATE_INCLUDES}" STREQUAL "")
- target_include_directories("${NAME}" PRIVATE ${arg_PRIVATE_INCLUDES})
- endif(NOT "${arg_PRIVATE_INCLUDES}" STREQUAL "")
+ # In order to more easily create the various types of libraries, two hidden
+ # targets are created: NAME._config and NAME._public_config which loosely
+ # mirror the GN configs although we also carry target link dependencies
+ # through these.
+
+ # Add the NAME._config target_link_libraries dependency with the
+ # PRIVATE_INCLUDES, PRIVATE_DEFINES, PRIVATE_COMPILE_OPTIONS,
+ # PRIVATE_LINK_OPTIONS, and PRIVATE_DEPS.
+ add_library("${NAME}._config" INTERFACE EXCLUDE_FROM_ALL)
+ target_include_directories("${NAME}._config"
+ INTERFACE
+ ${arg_PRIVATE_INCLUDES}
+ )
+ target_compile_definitions("${NAME}._config"
+ INTERFACE
+ ${arg_PRIVATE_DEFINES}
+ )
+ target_compile_options("${NAME}._config"
+ INTERFACE
+ ${arg_PRIVATE_COMPILE_OPTIONS}
+ )
+ target_link_options("${NAME}._config"
+ INTERFACE
+ ${arg_PRIVATE_LINK_OPTIONS}
+ )
+ pw_target_link_targets("${NAME}._config"
+ INTERFACE
+ ${arg_PRIVATE_DEPS}
+ )
- target_link_libraries("${NAME}"
- ${public_or_interface}
- pw_build
+ # Add the NAME._public_config target_link_libraries dependency with the
+ # PUBLIC_INCLUDES, PUBLIC_DEFINES, PUBLIC_COMPILE_OPTIONS,
+ # PUBLIC_LINK_OPTIONS, and PUBLIC_DEPS.
+ add_library("${NAME}._public_config" INTERFACE EXCLUDE_FROM_ALL)
+ target_include_directories("${NAME}._public_config"
+ INTERFACE
+ ${arg_PUBLIC_INCLUDES}
+ )
+ target_compile_definitions("${NAME}._public_config"
+ INTERFACE
+ ${arg_PUBLIC_DEFINES}
+ )
+ target_compile_options("${NAME}._public_config"
+ INTERFACE
+ ${arg_PUBLIC_COMPILE_OPTIONS}
+ )
+ target_link_options("${NAME}._public_config"
+ INTERFACE
+ ${arg_PUBLIC_LINK_OPTIONS}
+ )
+ pw_target_link_targets("${NAME}._public_config"
+ INTERFACE
${arg_PUBLIC_DEPS}
)
- if(NOT "${arg_SOURCES}" STREQUAL "")
- target_link_libraries("${NAME}"
+ # Instantiate the library depending on the type using the NAME._config and
+ # NAME._public_config libraries we just created.
+ if("${TYPE}" STREQUAL "INTERFACE")
+ if(NOT "${arg_SOURCES}" STREQUAL "")
+ message(
+ SEND_ERROR "${NAME} cannot have sources as it's an INTERFACE library")
+ endif(NOT "${arg_SOURCES}" STREQUAL "")
+
+ add_library("${NAME}" INTERFACE EXCLUDE_FROM_ALL)
+ target_sources("${NAME}" PRIVATE ${arg_HEADERS})
+ pw_target_link_targets("${NAME}"
+ INTERFACE
+ "${NAME}._public_config"
+ )
+ elseif(("${TYPE}" STREQUAL "STATIC") OR ("${TYPE}" STREQUAL "SHARED"))
+ if("${arg_SOURCES}" STREQUAL "")
+ message(
+ SEND_ERROR "${NAME} must have SOURCES as it's not an INTERFACE library")
+ endif("${arg_SOURCES}" STREQUAL "")
+
+ add_library("${NAME}" "${TYPE}" EXCLUDE_FROM_ALL)
+ target_sources("${NAME}" PRIVATE ${arg_HEADERS} ${arg_SOURCES})
+ pw_target_link_targets("${NAME}"
+ PUBLIC
+ "${NAME}._public_config"
PRIVATE
- pw_build.warnings
- ${arg_PRIVATE_DEPS}
+ "${NAME}._config"
)
- endif(NOT "${arg_SOURCES}" STREQUAL "")
-
- if(NOT "${arg_IMPLEMENTS_FACADES}" STREQUAL "")
- target_include_directories("${NAME}"
- ${public_or_interface}
- public_overrides
+ foreach(compile_option_dep IN LISTS arg_PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE)
+ # This will fail at build time if the target does not exist.
+ target_compile_options("${NAME}" BEFORE PRIVATE
+ $<TARGET_PROPERTY:${compile_option_dep},INTERFACE_COMPILE_OPTIONS>
+ )
+ endforeach()
+ elseif("${TYPE}" STREQUAL "OBJECT")
+ if("${arg_SOURCES}" STREQUAL "")
+ message(
+ SEND_ERROR "${NAME} must have SOURCES as it's not an INTERFACE library")
+ endif("${arg_SOURCES}" STREQUAL "")
+
+ # In order to support OBJECT libraries while maintaining transitive
+ # linking dependencies, the library has to be split up into two where the
+ # outer interface library forwards not only the internal object library
+ # but also its TARGET_OBJECTS.
+ add_library("${NAME}._object" OBJECT EXCLUDE_FROM_ALL)
+ target_sources("${NAME}._object" PRIVATE ${arg_SOURCES})
+ pw_target_link_targets("${NAME}._object"
+ PRIVATE
+ "${NAME}._public_config"
+ "${NAME}._config"
)
- if("${arg_PUBLIC_INCLUDES}" STREQUAL "")
- # TODO(pwbug/601): Deprecate this legacy implicit PUBLIC_INCLUDES.
- target_include_directories("${NAME}"
- ${public_or_interface}
- public_overrides
+ foreach(compile_option_dep IN LISTS arg_PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE)
+ # This will fail at build time if the target does not exist.
+ target_compile_options("${NAME}._object" BEFORE PRIVATE
+ $<TARGET_PROPERTY:${compile_option_dep},INTERFACE_COMPILE_OPTIONS>
)
- endif("${arg_PUBLIC_INCLUDES}" STREQUAL "")
- set(facades ${arg_IMPLEMENTS_FACADES})
- list(TRANSFORM facades APPEND ".facade")
- target_link_libraries("${NAME}" ${public_or_interface} ${facades})
- endif(NOT "${arg_IMPLEMENTS_FACADES}" STREQUAL "")
-
- if(NOT "${arg_PUBLIC_DEFINES}" STREQUAL "")
- target_compile_definitions("${NAME}"
- ${public_or_interface}
- ${arg_PUBLIC_DEFINES}
+ endforeach()
+
+ add_library("${NAME}" INTERFACE EXCLUDE_FROM_ALL)
+ target_sources("${NAME}" PRIVATE ${arg_HEADERS})
+ pw_target_link_targets("${NAME}"
+ INTERFACE
+ "${NAME}._public_config"
+ "${NAME}._object"
)
- endif(NOT "${arg_PUBLIC_DEFINES}" STREQUAL "")
+ target_link_libraries("${NAME}"
+ INTERFACE
+ $<TARGET_OBJECTS:${NAME}._object>
+ )
+ else()
+ message(FATAL_ERROR "Unsupported libary type: ${TYPE}")
+ endif()
+endfunction(pw_add_library_generic)
- if(NOT "${arg_PRIVATE_DEFINES}" STREQUAL "")
- target_compile_definitions("${NAME}" PRIVATE ${arg_PRIVATE_DEFINES})
- endif(NOT "${arg_PRIVATE_DEFINES}" STREQUAL "")
+# Checks that the library's name is prefixed by the relative path with dot
+# separators instead of forward slashes. Ignores paths not under the root
+# directory.
+#
+# Optional Args:
+#
+# REMAP_PREFIXES - support remapping a prefix for checks
+#
+function(_pw_check_name_is_relative_to_root NAME ROOT)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 2
+ MULTI_VALUE_ARGS
+ REMAP_PREFIXES
+ )
- if(NOT "${arg_PUBLIC_COMPILE_OPTIONS}" STREQUAL "")
- target_compile_options("${NAME}"
- ${public_or_interface}
- ${arg_PUBLIC_COMPILE_OPTIONS}
- )
- endif(NOT "${arg_PUBLIC_COMPILE_OPTIONS}" STREQUAL "")
+ file(RELATIVE_PATH rel_path "${ROOT}" "${CMAKE_CURRENT_SOURCE_DIR}")
+ if("${rel_path}" MATCHES "^\\.\\.")
+ return() # Ignore paths not under ROOT
+ endif()
- if(NOT "${arg_PRIVATE_COMPILE_OPTIONS}" STREQUAL "")
- target_compile_options("${NAME}" PRIVATE ${arg_PRIVATE_COMPILE_OPTIONS})
- endif(NOT "${arg_PRIVATE_COMPILE_OPTIONS}" STREQUAL "")
+ list(LENGTH arg_REMAP_PREFIXES remap_arg_count)
+ if("${remap_arg_count}" EQUAL 2)
+ list(GET arg_REMAP_PREFIXES 0 from_prefix)
+ list(GET arg_REMAP_PREFIXES 1 to_prefix)
+ string(REGEX REPLACE "^${from_prefix}" "${to_prefix}" rel_path "${rel_path}")
+ elseif(NOT "${remap_arg_count}" EQUAL 0)
+ message(FATAL_ERROR
+ "If REMAP_PREFIXES is specified, exactly two arguments must be given.")
+ endif()
- if(NOT "${arg_PUBLIC_LINK_OPTIONS}" STREQUAL "")
- target_link_options("${NAME}"
- ${public_or_interface}
- ${arg_PUBLIC_LINK_OPTIONS}
- )
- endif(NOT "${arg_PUBLIC_LINK_OPTIONS}" STREQUAL "")
+ if(NOT "${rel_path}" MATCHES "^\\.\\..*")
+ string(REPLACE "/" "." dot_rel_path "${rel_path}")
+ if(NOT "${NAME}" MATCHES "^${dot_rel_path}(\\.[^\\.]+)?(\\.facade)?$")
+ message(FATAL_ERROR
+ "Module libraries under ${ROOT} must match the module name or be in "
+ "the form 'PATH_TO.THE_TARGET.NAME'. The library '${NAME}' does not "
+ "match. Expected ${dot_rel_path}.LIBRARY_NAME"
+ )
+ endif()
+ endif()
+endfunction(_pw_check_name_is_relative_to_root)
- if(NOT "${arg_PRIVATE_LINK_OPTIONS}" STREQUAL "")
- target_link_options("${NAME}" PRIVATE ${arg_PRIVATE_LINK_OPTIONS})
- endif(NOT "${arg_PRIVATE_LINK_OPTIONS}" STREQUAL "")
-endfunction(pw_add_module_library)
+# Creates a pw module library.
+#
+# Required Args:
+#
+# <name> - The name of the library target to be created.
+# <type> - The library type which must be INTERFACE, OBJECT, STATIC or SHARED.
+#
+# Optional Args:
+#
+# SOURCES - source files for this library
+# HEADERS - header files for this library
+# PUBLIC_DEPS - public pw_target_link_targets arguments
+# PRIVATE_DEPS - private pw_target_link_targets arguments
+# PUBLIC_INCLUDES - public target_include_directories argument
+# PRIVATE_INCLUDES - public target_include_directories argument
+# PUBLIC_DEFINES - public target_compile_definitions arguments
+# PRIVATE_DEFINES - private target_compile_definitions arguments
+# PUBLIC_COMPILE_OPTIONS - public target_compile_options arguments
+# PRIVATE_COMPILE_OPTIONS - private target_compile_options arguments
+# PUBLIC_LINK_OPTIONS - public target_link_options arguments
+# PRIVATE_LINK_OPTIONS - private target_link_options arguments
+#
+function(pw_add_library NAME TYPE)
+ _pw_add_library_multi_value_args(pw_add_library_generic_multi_value_args)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 2
+ MULTI_VALUE_ARGS
+ ${pw_add_library_generic_multi_value_args}
+ )
+
+ _pw_check_name_is_relative_to_root("${NAME}" "$ENV{PW_ROOT}"
+ REMAP_PREFIXES
+ third_party pw_third_party
+ )
+
+ pw_add_library_generic(${NAME} ${TYPE}
+ SOURCES
+ ${arg_SOURCES}
+ HEADERS
+ ${arg_HEADERS}
+ PUBLIC_DEPS
+ # TODO(b/232141950): Apply compilation options that affect ABI
+ # globally in the CMake build instead of injecting them into libraries.
+ pw_build
+ ${arg_PUBLIC_DEPS}
+ PRIVATE_DEPS
+ ${arg_PRIVATE_DEPS}
+ PUBLIC_INCLUDES
+ ${arg_PUBLIC_INCLUDES}
+ PRIVATE_INCLUDES
+ ${arg_PRIVATE_INCLUDES}
+ PUBLIC_DEFINES
+ ${arg_PUBLIC_DEFINES}
+ PRIVATE_DEFINES
+ ${arg_PRIVATE_DEFINES}
+ PUBLIC_COMPILE_OPTIONS
+ ${arg_PUBLIC_COMPILE_OPTIONS}
+ PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE
+ pw_build.warnings
+ PRIVATE_COMPILE_OPTIONS
+ ${arg_PRIVATE_COMPILE_OPTIONS}
+ PUBLIC_LINK_OPTIONS
+ ${arg_PUBLIC_LINK_OPTIONS}
+ PRIVATE_LINK_OPTIONS
+ ${arg_PRIVATE_LINK_OPTIONS}
+ )
+endfunction(pw_add_library)
# Declares a module as a facade.
#
@@ -308,70 +505,238 @@ endfunction(pw_add_module_library)
# module that implements the facade depends on a library named
# MODULE_NAME.facade.
#
-# pw_add_facade accepts the same arguments as pw_add_module_library, except for
-# IMPLEMENTS_FACADES. It also accepts the following argument:
+# pw_add_facade accepts the same arguments as pw_add_library.
+# It also accepts the following argument:
#
-# DEFAULT_BACKEND - which backend to use by default
+# BACKEND - The name of the facade's backend variable.
+function(pw_add_facade NAME TYPE)
+ _pw_add_library_multi_value_args(multi_value_args)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 2
+ ONE_VALUE_ARGS
+ BACKEND
+ MULTI_VALUE_ARGS
+ ${multi_value_args}
+ )
+
+ _pw_check_name_is_relative_to_root("${NAME}" "$ENV{PW_ROOT}"
+ REMAP_PREFIXES
+ third_party pw_third_party
+ )
+
+ pw_add_facade_generic("${NAME}" "${TYPE}"
+ BACKEND
+ ${arg_BACKEND}
+ SOURCES
+ ${arg_SOURCES}
+ HEADERS
+ ${arg_HEADERS}
+ PUBLIC_DEPS
+ # TODO(b/232141950): Apply compilation options that affect ABI
+ # globally in the CMake build instead of injecting them into libraries.
+ pw_build
+ ${arg_PUBLIC_DEPS}
+ PRIVATE_DEPS
+ ${arg_PRIVATE_DEPS}
+ PUBLIC_INCLUDES
+ ${arg_PUBLIC_INCLUDES}
+ PRIVATE_INCLUDES
+ ${arg_PRIVATE_INCLUDES}
+ PUBLIC_DEFINES
+ ${arg_PUBLIC_DEFINES}
+ PRIVATE_DEFINES
+ ${arg_PRIVATE_DEFINES}
+ PUBLIC_COMPILE_OPTIONS
+ ${arg_PUBLIC_COMPILE_OPTIONS}
+ PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE
+ pw_build.warnings
+ PRIVATE_COMPILE_OPTIONS
+ ${arg_PRIVATE_COMPILE_OPTIONS}
+ PUBLIC_LINK_OPTIONS
+ ${arg_PUBLIC_LINK_OPTIONS}
+ PRIVATE_LINK_OPTIONS
+ ${arg_PRIVATE_LINK_OPTIONS}
+ )
+endfunction(pw_add_facade)
+
+# pw_add_facade_generic: Creates a CMake facade library target.
+#
+# Facades are declared as two libraries to avoid circular dependencies.
+# Libraries that use the facade depend on the <name> of this target. The
+# libraries that implement this facade have to depend on an internal library
+# named <name>.facade.
+#
+# Required Args:
+#
+# <name> - The name for the public facade target (<name>) for all users and
+# the suffixed facade target for backend implementers (<name.facade).
+# <type> - The library type which must be INTERFACE, OBJECT, STATIC, or
+# SHARED.
+# BACKEND - The name of the facade's backend variable.
#
-function(pw_add_facade NAME)
- _pw_library_args(list_args)
- _pw_parse_argv_strict(pw_add_facade 1 "" "DEFAULT_BACKEND" "${list_args}")
+# Optional Args:
+#
+# SOURCES - source files for this library
+# HEADERS - header files for this library
+# PUBLIC_DEPS - public pw_target_link_targets arguments
+# PRIVATE_DEPS - private pw_target_link_targets arguments
+# PUBLIC_INCLUDES - public target_include_directories argument
+# PRIVATE_INCLUDES - public target_include_directories argument
+# PUBLIC_DEFINES - public target_compile_definitions arguments
+# PRIVATE_DEFINES - private target_compile_definitions arguments
+# PUBLIC_COMPILE_OPTIONS - public target_compile_options arguments
+# PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE - private target_compile_options BEFORE
+# arguments from the specified deps's INTERFACE_COMPILE_OPTIONS. Note that
+# these deps are not pulled in as target_link_libraries. This should not be
+# exposed by the non-generic API.
+# PRIVATE_COMPILE_OPTIONS - private target_compile_options arguments
+# PUBLIC_LINK_OPTIONS - public target_link_options arguments
+# PRIVATE_LINK_OPTIONS - private target_link_options arguments
+function(pw_add_facade_generic NAME TYPE)
+ set(supported_library_types INTERFACE OBJECT STATIC SHARED)
+ if(NOT "${TYPE}" IN_LIST supported_library_types)
+ message(FATAL_ERROR "\"${TYPE}\" is not a valid library type for ${NAME}. "
+ "Must be INTERFACE, OBJECT, STATIC, or SHARED.")
+ endif()
- # If no backend is set, a script that displays an error message is used
- # instead. If the facade is used in the build, it fails with this error.
- add_custom_target("${NAME}._no_backend_set_message"
- COMMAND
- "${CMAKE_COMMAND}" -E echo
- "ERROR: Attempted to build the ${NAME} facade with no backend."
- "Configure the ${NAME} backend using pw_set_backend or remove all dependencies on it."
- "See https://pigweed.dev/pw_build."
- COMMAND
- "${CMAKE_COMMAND}" -E false
+ _pw_add_library_multi_value_args(multi_value_args)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 2
+ ONE_VALUE_ARGS
+ BACKEND
+ MULTI_VALUE_ARGS
+ ${multi_value_args}
+ PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE
+ REQUIRED_ARGS
+ BACKEND
)
- add_library("${NAME}.NO_BACKEND_SET" INTERFACE)
- add_dependencies("${NAME}.NO_BACKEND_SET" "${NAME}._no_backend_set_message")
- # Set the default backend to the error message if no default is specified.
- if("${arg_DEFAULT_BACKEND}" STREQUAL "")
- set(arg_DEFAULT_BACKEND "${NAME}.NO_BACKEND_SET")
+ if(NOT DEFINED "${arg_BACKEND}")
+ message(FATAL_ERROR "${NAME}'s backend variable ${arg_BACKEND} has not "
+ "been defined, you may be missing a pw_add_backend_variable or "
+ "the *.cmake import to that file.")
+ endif()
+ string(REGEX MATCH ".+_BACKEND" backend_ends_in_backend "${arg_BACKEND}")
+ if(NOT backend_ends_in_backend)
+ message(FATAL_ERROR "The ${NAME} pw_add_generic_facade's BACKEND argument "
+ "(${arg_BACKEND}) must end in _BACKEND (${name_ends_in_backend})")
endif()
- # Declare the backend variable for this facade.
- set("${NAME}_BACKEND" "${arg_DEFAULT_BACKEND}" CACHE STRING
- "Backend for ${NAME}")
+ set(backend_target "${${arg_BACKEND}}")
+ if ("${backend_target}" STREQUAL "")
+ # If no backend is set, a script that displays an error message is used
+ # instead. If the facade is used in the build, it fails with this error.
+ pw_add_error_target("${NAME}.NO_BACKEND_SET"
+ MESSAGE
+ "Attempted to build the ${NAME} facade with no backend set. "
+ "Configure the ${NAME} backend using pw_set_backend or remove all "
+ "dependencies on it. See https://pigweed.dev/pw_build."
+ )
- # This target is never used; it simply tests that the specified backend
- # actually exists in the build. The generator expression will fail to evaluate
- # if the target is not defined.
- add_custom_target(_pw_check_that_backend_for_${NAME}_is_defined
- COMMAND
- ${CMAKE_COMMAND} -E echo "$<TARGET_PROPERTY:${${NAME}_BACKEND},TYPE>"
- )
+ set(backend_target "${NAME}.NO_BACKEND_SET")
+ endif()
# Define the facade library, which is used by the backend to avoid circular
# dependencies.
- add_library("${NAME}.facade" INTERFACE)
- target_include_directories("${NAME}.facade" INTERFACE public)
- target_link_libraries("${NAME}.facade" INTERFACE ${arg_PUBLIC_DEPS})
-
- # Define the public-facing library for this facade, which depends on the
- # header files in .facade target and exposes the dependency on the backend.
- pw_add_module_library("${NAME}"
- SOURCES
- ${arg_SOURCES}
+ pw_add_library_generic("${NAME}.facade" INTERFACE
HEADERS
${arg_HEADERS}
+ PUBLIC_INCLUDES
+ ${arg_PUBLIC_INCLUDES}
+ PUBLIC_DEPS
+ ${arg_PUBLIC_DEPS}
+ PUBLIC_DEFINES
+ ${arg_PUBLIC_DEFINES}
+ PUBLIC_COMPILE_OPTIONS
+ ${arg_PUBLIC_COMPILE_OPTIONS}
+ PUBLIC_LINK_OPTIONS
+ ${arg_PUBLIC_LINK_OPTIONS}
+ )
+
+ # Define the public-facing library for this facade, which depends on the
+ # header files and public interface aspects from the .facade target and
+ # exposes the dependency on the backend along with the private library
+ # target components.
+ pw_add_library_generic("${NAME}" "${TYPE}"
PUBLIC_DEPS
"${NAME}.facade"
- "${${NAME}_BACKEND}"
+ "${backend_target}"
+ SOURCES
+ ${arg_SOURCES}
+ PRIVATE_INCLUDES
+ ${arg_PRIVATE_INCLUDES}
+ PRIVATE_DEPS
+ ${arg_PRIVATE_DEPS}
+ PRIVATE_DEFINES
+ ${arg_PRIVATE_DEFINES}
+ PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE
+ ${arg_PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE}
+ PRIVATE_COMPILE_OPTIONS
+ ${arg_PRIVATE_COMPILE_OPTIONS}
+ PRIVATE_LINK_OPTIONS
+ ${arg_PRIVATE_LINK_OPTIONS}
)
-endfunction(pw_add_facade)
+endfunction(pw_add_facade_generic)
-# Sets which backend to use for the given facade.
-function(pw_set_backend FACADE BACKEND)
- set("${FACADE}_BACKEND" "${BACKEND}" CACHE STRING "Backend for ${NAME}" FORCE)
+# Declare a facade's backend variables which can be overriden later by using
+# pw_set_backend.
+#
+# Required Arguments:
+# NAME - Name of the facade's backend variable.
+#
+# Optional Arguments:
+# DEFAULT_BACKEND - Optional default backend selection for the facade.
+#
+function(pw_add_backend_variable NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ DEFAULT_BACKEND
+ )
+
+ string(REGEX MATCH ".+_BACKEND" name_ends_in_backend "${NAME}")
+ if(NOT name_ends_in_backend)
+ message(FATAL_ERROR "The ${NAME} pw_add_backend_variable's NAME argument "
+ "must end in _BACKEND")
+ endif()
+
+ set("${NAME}" "${arg_DEFAULT_BACKEND}" CACHE STRING
+ "${NAME} backend variable for a facade")
+endfunction()
+
+# Sets which backend to use for the given facade's backend variable.
+function(pw_set_backend NAME BACKEND)
+ # TODO(ewout, hepler): Deprecate this temporarily support which permits the
+ # direct facade name directly, instead of the facade's backend variable name.
+ # Also update this to later assert the variable is DEFINED to catch typos.
+ string(REGEX MATCH ".+_BACKEND" name_ends_in_backend "${NAME}")
+ if(NOT name_ends_in_backend)
+ set(NAME "${NAME}_BACKEND")
+ endif()
+ if(NOT DEFINED "${NAME}")
+ message(WARNING "${NAME} was not defined when pw_set_backend was invoked, "
+ "you may be missing a pw_add_backend_variable or the *.cmake "
+ "import to that file.")
+ endif()
+
+ set("${NAME}" "${BACKEND}" CACHE STRING "backend variable for a facade" FORCE)
endfunction(pw_set_backend)
+# Zephyr specific wrapper for pw_set_backend.
+function(pw_set_zephyr_backend_ifdef COND FACADE BACKEND BACKEND_DECL)
+ if(${${COND}})
+ if(NOT EXISTS "${CMAKE_CURRENT_LIST_DIR}/${BACKEND_DECL}")
+ message(FATAL_ERROR
+ "Can't find backend declaration file '${CMAKE_CURRENT_LIST_DIR}/${BACKEND_DECL}'")
+ endif()
+ include("${CMAKE_CURRENT_LIST_DIR}/${BACKEND_DECL}")
+ pw_set_backend("${FACADE}" "${BACKEND}")
+ endif()
+endfunction()
+
# Set up the default pw_build_DEFAULT_MODULE_CONFIG.
set("pw_build_DEFAULT_MODULE_CONFIG" pw_build.empty CACHE STRING
"Default implementation for all Pigweed module configurations.")
@@ -388,7 +753,7 @@ set("pw_build_DEFAULT_MODULE_CONFIG" pw_build.empty CACHE STRING
#
# NAME: name to use for the target which can be depended on for the config.
function(pw_add_module_config NAME)
- _pw_parse_argv_strict(pw_add_module_config 1 "" "" "")
+ pw_parse_arguments(NUM_POSITIONAL_ARGS 1)
# Declare the module configuration variable for this module.
set("${NAME}" "${pw_build_DEFAULT_MODULE_CONFIG}"
@@ -403,80 +768,92 @@ endfunction(pw_add_module_config)
# pw_set_module_config(pw_build_DEFAULT_MODULE_CONFIG my_config)
# pw_set_module_config(pw_foo_CONFIG my_foo_config)
function(pw_set_module_config NAME LIBRARY)
- _pw_parse_argv_strict(pw_set_module_config 2 "" "" "")
+ pw_parse_arguments(NUM_POSITIONAL_ARGS 2)
# Update the module configuration variable.
set("${NAME}" "${LIBRARY}" CACHE STRING "Config for ${NAME}" FORCE)
endfunction(pw_set_module_config)
-# Declares a unit test. Creates two targets:
+# Adds compiler options to all targets built by CMake. Flags may be added any
+# time after this function is defined. The effect is global; all targets added
+# before or after a pw_add_global_compile_options call will be built with the
+# flags, regardless of where the files are located.
#
-# * <TEST_NAME> - the test executable
-# * <TEST_NAME>.run - builds and runs the test
+# pw_add_global_compile_options takes one optional named argument:
+#
+# LANGUAGES: Which languages (ASM, C, CXX) to apply the options to. Flags
+# apply to all languages by default.
+#
+# All other arguments are interpreted as compiler options.
+function(pw_add_global_compile_options)
+ cmake_parse_arguments(PARSE_ARGV 0 args "" "" "LANGUAGES")
+
+ set(supported_build_languages ASM C CXX)
+
+ if(NOT args_LANGUAGES)
+ set(args_LANGUAGES ${supported_build_languages})
+ endif()
+
+ # Check the selected language.
+ foreach(lang IN LISTS args_LANGUAGES)
+ if(NOT "${lang}" IN_LIST supported_build_languages)
+ message(FATAL_ERROR "'${lang}' is not a supported language. "
+ "Supported languages: ${supported_build_languages}")
+ endif()
+ endforeach()
+
+ # Enumerate which flags variables to set.
+ foreach(lang IN LISTS args_LANGUAGES)
+ list(APPEND cmake_flags_variables "CMAKE_${lang}_FLAGS")
+ endforeach()
+
+ # Set each flag for each specified flags variable.
+ foreach(variable IN LISTS cmake_flags_variables)
+ foreach(flag IN LISTS args_UNPARSED_ARGUMENTS)
+ set(${variable} "${${variable}} ${flag}" CACHE INTERNAL "" FORCE)
+ endforeach()
+ endforeach()
+endfunction(pw_add_global_compile_options)
+
+# pw_add_error_target: Creates a CMake target which fails to build and prints a
+# message
+#
+# This function prints a message and causes a build failure only if you attempt
+# to build the target. This is useful when FATAL_ERROR messages cannot be used
+# to catch problems during the CMake configuration phase.
#
# Args:
#
# NAME: name to use for the target
-# SOURCES: source files for this test
-# DEPS: libraries on which this test depends
-# GROUPS: groups to which to add this test; if none are specified, the test is
-# added to the 'default' and 'all' groups
-#
-function(pw_add_test NAME)
- _pw_parse_argv_strict(pw_add_test 1 "" "" "SOURCES;DEPS;GROUPS")
-
- add_executable("${NAME}" EXCLUDE_FROM_ALL ${arg_SOURCES})
- target_link_libraries("${NAME}"
- PRIVATE
- pw_unit_test
- pw_unit_test.main
- ${arg_DEPS}
+# MESSAGE: The message to print, prefixed with "ERROR: ". The message may be
+# composed of multiple pieces by passing multiple strings.
+#
+function(pw_add_error_target NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ MULTI_VALUE_ARGS
+ MESSAGE
)
- # Tests require at least one source file.
- if(NOT arg_SOURCES)
- target_sources("${NAME}" PRIVATE $<TARGET_PROPERTY:pw_build.empty,SOURCES>)
- endif()
- # Define a target for running the test. The target creates a stamp file to
- # indicate successful test completion. This allows running tests in parallel
- # with Ninja's full dependency resolution.
- add_custom_command(
+ # In case the message is comprised of multiple strings, stitch them together.
+ set(message "ERROR: ")
+ foreach(line IN LISTS arg_MESSAGE)
+ string(APPEND message "${line}")
+ endforeach()
+
+ add_custom_target("${NAME}._error_message"
COMMAND
- # TODO(hepler): This only runs local test binaries. Execute a test runner
- # instead to support device test runs.
- "$<TARGET_FILE:${NAME}>"
+ "${CMAKE_COMMAND}" -E echo "${message}"
COMMAND
- "${CMAKE_COMMAND}" -E touch "${NAME}.stamp"
- DEPENDS
- "${NAME}"
- OUTPUT
- "${NAME}.stamp"
+ "${CMAKE_COMMAND}" -E false
)
- add_custom_target("${NAME}.run" DEPENDS "${NAME}.stamp")
- # Always add tests to the "all" group. If no groups are provided, add the
- # test to the "default" group.
- if(arg_GROUPS)
- set(groups all ${arg_GROUPS})
- else()
- set(groups all default)
- endif()
-
- list(REMOVE_DUPLICATES groups)
- pw_add_test_to_groups("${NAME}" ${groups})
-endfunction(pw_add_test)
-
-# Adds a test target to the specified test groups. Test groups can be built with
-# the pw_tests_GROUP_NAME target or executed with the pw_run_tests_GROUP_NAME
-# target.
-function(pw_add_test_to_groups TEST_NAME)
- foreach(group IN LISTS ARGN)
- if(NOT TARGET "pw_tests.${group}")
- add_custom_target("pw_tests.${group}")
- add_custom_target("pw_run_tests.${group}")
- endif()
-
- add_dependencies("pw_tests.${group}" "${TEST_NAME}")
- add_dependencies("pw_run_tests.${group}" "${TEST_NAME}.run")
- endforeach()
-endfunction(pw_add_test_to_groups)
+ # A static library is provided, in case this rule nominally provides a
+ # compiled output, e.g. to enable $<TARGET_FILE:"${NAME}">.
+ pw_add_library_generic("${NAME}" STATIC
+ SOURCES
+ $<TARGET_PROPERTY:pw_build.empty,SOURCES>
+ )
+ add_dependencies("${NAME}" "${NAME}._error_message")
+endfunction(pw_add_error_target)
diff --git a/pw_build/pigweed_toolchain_upstream.bzl b/pw_build/pigweed_toolchain_upstream.bzl
deleted file mode 100644
index a77cce6be..000000000
--- a/pw_build/pigweed_toolchain_upstream.bzl
+++ /dev/null
@@ -1,78 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-"""Implements the set of dependencies that bazel-embedded requires."""
-
-def _toolchain_upstream_repository_impl(rctx):
- """Creates a remote repository with a set of toolchain components.
-
- The bazel embedded toolchain expects two targets injected_headers
- and polyfill. This is rule generates these targets so that
- bazel-embedded can depend on them and so that the targets can depend
- on pigweeds implementation of polyfill. This rule is only ever
- intended to be instantiated with the name
- "bazel_embedded_upstream_toolchain", and should only be used from
- the "toolchain_upstream_deps" macro.
-
- The bazel-embedded package expects to be able to access these
- targets as @bazel_embedded_upstream_toolchain//:polyfill and
- @bazel_embedded_upstream_toolchain//:injected_headers.
-
- Args:
- rctx: Repository context.
- """
- rctx.file("BUILD", """
-package(default_visibility = ["//visibility:public"])
-load(
- "@bazel_embedded//toolchains/tools/include_tools:defs.bzl",
- "cc_injected_toolchain_header_library",
- "cc_polyfill_toolchain_library",
-)
-
-cc_polyfill_toolchain_library(
- name = "polyfill",
- deps = ["@pigweed//pw_polyfill:toolchain_polyfill_overrides"],
-)
-
-cc_injected_toolchain_header_library(
- name = "injected_headers",
- deps = ["@pigweed//pw_polyfill:toolchain_injected_headers"],
-)
-""")
-
-_toolchain_upstream_repository = repository_rule(
- _toolchain_upstream_repository_impl,
- doc = """
-toolchain_upstream_repository creates a remote repository that can be
-accessed by a toolchain repository to configure system includes.
-
-It's recommended to use this rule through the 'toolchain_upstream_deps'
-macro rather than using this rule directly.
-""",
-)
-
-def toolchain_upstream_deps():
- """Implements the set of dependencies that bazel-embedded requires.
-
- These targets are used to override the default toolchain
- requirements in the remote bazel-embedded toolchain. The remote
- toolchain expects to find two targets;
- - "@bazel_embedded_upstream_toolchain//:polyfill" -> Additional
- system headers for the toolchain
- - "@bazel_embedded_upstream_toolchain//:injected_headers" ->
- Headers that are injected into the toolchain via the -include
- command line argument
- """
- _toolchain_upstream_repository(
- name = "bazel_embedded_upstream_toolchain",
- )
diff --git a/pw_build/platforms/BUILD.bazel b/pw_build/platforms/BUILD.bazel
index 7bbc6a3cf..554957766 100644
--- a/pw_build/platforms/BUILD.bazel
+++ b/pw_build/platforms/BUILD.bazel
@@ -71,7 +71,7 @@ platform(
platform(
name = "cortex_m4_fpu",
constraint_values = ["@pigweed_config//:target_rtos"],
- parents = ["@bazel_embedded//platforms:cortex_m4"],
+ parents = ["@bazel_embedded//platforms:cortex_m4_fpu"],
)
platform(
@@ -99,9 +99,46 @@ platform(
parents = [":cortex_m3"],
)
+platform(
+ name = "nrf52833",
+ constraint_values = ["//pw_build/constraints/chipset:nrf52833"],
+ parents = [":cortex_m0"],
+)
+
# --- Boards ---
platform(
name = "stm32f429i-disc1",
constraint_values = ["//pw_build/constraints/board:stm32f429i-disc1"],
parents = [":stm32f429"],
)
+
+platform(
+ name = "microbit",
+ constraint_values = ["//pw_build/constraints/board:microbit"],
+ parents = [":nrf52833"],
+)
+
+# --- Test platforms ---
+
+# This is a platform for compilation testing of freertos backends. This is not
+# a complete specification of any real target platform.
+platform(
+ name = "testonly_freertos",
+ constraint_values = [
+ # Use FreeRTOS backends.
+ "//pw_build/constraints/rtos:freertos",
+ # Use the FreeRTOS config file for stm32f429i_disc1_stm32cube.
+ "//targets/stm32f429i_disc1_stm32cube:freertos_config_cv",
+ # Use the ARM_CM4F port of FreeRTOS.
+ "@freertos//:port_ARM_CM4F",
+ # Specify this chipset to use the baremetal pw_sys_io backend (because
+ # the default pw_sys_io_stdio backend is not compatible with FreeRTOS).
+ "//pw_build/constraints/chipset:stm32f429",
+ # os:none means, we're not building for any host platform (Windows,
+ # Linux, or Mac). The pw_sys_io_baremetal_stm32f429 backend is only
+ # compatible with os:none.
+ "@platforms//os:none",
+ ],
+ # Inherit from cortex_m4_fpu to use the appropriate Arm toolchain.
+ parents = [":cortex_m4_fpu"],
+)
diff --git a/pw_build/py/BUILD.gn b/pw_build/py/BUILD.gn
index 5341a92b9..16d6711fa 100644
--- a/pw_build/py/BUILD.gn
+++ b/pw_build/py/BUILD.gn
@@ -24,8 +24,10 @@ pw_python_package("py") {
]
sources = [
"pw_build/__init__.py",
+ "pw_build/build_recipe.py",
"pw_build/collect_wheels.py",
"pw_build/copy_from_cipd.py",
+ "pw_build/create_gn_venv.py",
"pw_build/create_python_tree.py",
"pw_build/error.py",
"pw_build/exec.py",
@@ -34,19 +36,29 @@ pw_python_package("py") {
"pw_build/generate_modules_lists.py",
"pw_build/generate_python_package.py",
"pw_build/generate_python_package_gn.py",
+ "pw_build/generate_python_requirements.py",
"pw_build/generated_tests.py",
+ "pw_build/gn_resolver.py",
"pw_build/host_tool.py",
"pw_build/mirror_tree.py",
"pw_build/nop.py",
+ "pw_build/pip_install_python_deps.py",
+ "pw_build/project_builder.py",
+ "pw_build/project_builder_argparse.py",
+ "pw_build/project_builder_context.py",
+ "pw_build/project_builder_prefs.py",
"pw_build/python_package.py",
"pw_build/python_runner.py",
"pw_build/python_wheels.py",
+ "pw_build/wrap_ninja.py",
"pw_build/zip.py",
]
tests = [
+ "build_recipe_test.py",
"create_python_tree_test.py",
"file_prefix_map_test.py",
"generate_cc_blob_library_test.py",
+ "project_builder_prefs_test.py",
"python_runner_test.py",
"zip_test.py",
]
@@ -56,4 +68,5 @@ pw_python_package("py") {
"$dir_pw_presubmit/py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_build/py/build_recipe_test.py b/pw_build/py/build_recipe_test.py
new file mode 100644
index 000000000..6361da5b8
--- /dev/null
+++ b/pw_build/py/build_recipe_test.py
@@ -0,0 +1,114 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for pw_watch.build_recipe"""
+
+from pathlib import Path
+import shlex
+import unittest
+
+from parameterized import parameterized # type: ignore
+
+from pw_build.build_recipe import BuildCommand
+
+
+# pylint: disable=line-too-long
+class TestBuildRecipe(unittest.TestCase):
+ """Tests for creating BuildRecipes."""
+
+ def setUp(self):
+ self.maxDiff = None # pylint: disable=invalid-name
+
+ @parameterized.expand(
+ [
+ (
+ 'build command using make',
+ BuildCommand(
+ build_dir=Path('outmake'),
+ build_system_command='make',
+ build_system_extra_args=['-k'],
+ targets=['maketarget1', 'maketarget2'],
+ ),
+ # result
+ ['make', '-k', '-C', 'outmake', 'maketarget1', 'maketarget2'],
+ ),
+ (
+ 'build command using bazel',
+ BuildCommand(
+ build_dir=Path('outbazel'),
+ build_system_command='bazel',
+ build_system_extra_args=[],
+ targets=['build', '//pw_analog/...', '//pw_assert/...'],
+ ),
+ # result
+ [
+ 'bazel',
+ '--output_base',
+ 'outbazel',
+ 'build',
+ '//pw_analog/...',
+ '//pw_assert/...',
+ ],
+ ),
+ (
+ 'cmake shell command',
+ BuildCommand(
+ build_dir=Path('outcmake'),
+ command=shlex.split('cmake -G Ninja -S ./ -B outcmake'),
+ ),
+ # result
+ ['cmake', '-G', 'Ninja', '-S', './', '-B', 'outcmake'],
+ ),
+ (
+ 'gn shell command',
+ BuildCommand(
+ build_dir=Path('out'),
+ command=shlex.split('gn gen out --export-compile-commands'),
+ ),
+ # result
+ ['gn', 'gen', 'out', '--export-compile-commands'],
+ ),
+ (
+ 'python shell command',
+ BuildCommand(
+ build_dir=Path('outpytest'),
+ command=shlex.split(
+ 'python pw_build/py/build_recipe_test.py'
+ ),
+ ),
+ # result
+ ['python', 'pw_build/py/build_recipe_test.py'],
+ ),
+ (
+ 'gn shell command with a list',
+ BuildCommand(
+ build_dir=Path('out'),
+ command=['gn', 'gen', 'out', '--export-compile-commands'],
+ ),
+ # result
+ ['gn', 'gen', 'out', '--export-compile-commands'],
+ ),
+ ]
+ )
+ def test_build_command_get_args(
+ self,
+ _test_name,
+ build_command,
+ expected_args,
+ ) -> None:
+ """Test BuildCommand get_args."""
+ self.assertEqual(expected_args, build_command.get_args())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_build/py/create_python_tree_test.py b/pw_build/py/create_python_tree_test.py
index 0f221f47a..d545a0ffe 100644
--- a/pw_build/py/create_python_tree_test.py
+++ b/pw_build/py/create_python_tree_test.py
@@ -13,6 +13,7 @@
# the License.
"""Tests for pw_build.create_python_tree"""
+import io
import os
from pathlib import Path
import tempfile
@@ -22,11 +23,16 @@ import unittest
from parameterized import parameterized # type: ignore
from pw_build.python_package import PythonPackage
-from pw_build.create_python_tree import build_python_tree, copy_extra_files
-from pw_build.generate_python_package import _PYPROJECT_FILE as PYPROJECT_TEXT
+from pw_build.create_python_tree import (
+ build_python_tree,
+ copy_extra_files,
+ load_common_config,
+ update_config_with_packages,
+)
+from pw_build.generate_python_package import PYPROJECT_FILE
-def _setup_cfg(package_name: str) -> str:
+def _setup_cfg(package_name: str, install_requires: str = '') -> str:
return f'''
[metadata]
name = {package_name}
@@ -38,6 +44,7 @@ description = Pigweed swiss-army knife
[options]
packages = find:
zip_safe = False
+{install_requires}
[options.package_data]
{package_name} =
@@ -45,22 +52,27 @@ zip_safe = False
'''
-def _create_fake_python_package(location: Path, files: List[str],
- package_name: str) -> None:
+def _create_fake_python_package(
+ location: Path,
+ package_name: str,
+ files: List[str],
+ install_requires: str = '',
+) -> None:
for file in files:
destination = location / file
destination.parent.mkdir(parents=True, exist_ok=True)
text = f'"""{package_name}"""'
if str(destination).endswith('setup.cfg'):
- text = _setup_cfg(package_name)
+ text = _setup_cfg(package_name, install_requires)
elif str(destination).endswith('pyproject.toml'):
# Make sure pyproject.toml file has valid syntax.
- text = PYPROJECT_TEXT
+ text = PYPROJECT_FILE
destination.write_text(text)
class TestCreatePythonTree(unittest.TestCase):
"""Integration tests for create_python_tree."""
+
def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
# Save the starting working directory for returning to later.
@@ -79,16 +91,43 @@ class TestCreatePythonTree(unittest.TestCase):
expected_paths = set(Path(p).as_posix() for p in expected_results)
actual_paths = set(
p.relative_to(install_dir).as_posix()
- for p in install_dir.glob('**/*') if p.is_file())
+ for p in install_dir.glob('**/*')
+ if p.is_file()
+ )
self.assertEqual(expected_paths, actual_paths)
- @parameterized.expand([
- (
- # Test name
- 'working case',
- # Package name
+ def test_update_config_with_packages(self) -> None:
+ """Test merging package setup.cfg files."""
+ temp_root = Path(self.temp_dir.name)
+ common_config = temp_root / 'common_setup.cfg'
+ common_config.write_text(
+ '''
+[metadata]
+name = megapackage
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Pigweed swiss-army knife
+
+[options]
+zip_safe = False
+
+[options.package_data]
+megapackage =
+ py.typed
+'''
+ )
+ config = load_common_config(
+ common_config=common_config, append_git_sha=False, append_date=False
+ )
+ config_metadata = dict(config['metadata'].items())
+ self.assertIn('name', config_metadata)
+
+ pkg1_root = temp_root / 'pkg1'
+ pkg2_root = temp_root / 'pkg2'
+ _create_fake_python_package(
+ pkg1_root,
'mars',
- # File list
[
'planets/BUILD.mars_rocket',
'planets/mars/__init__.py',
@@ -100,18 +139,23 @@ class TestCreatePythonTree(unittest.TestCase):
'planets/pyproject.toml',
'planets/setup.cfg',
],
- # Extra_files
- [],
- # Package definition
- {
+ install_requires='''
+install_requires =
+ coloredlogs
+ coverage
+ cryptography
+ graphlib-backport;python_version<'3.9'
+ httpwatcher
+''',
+ )
+
+ os.chdir(pkg1_root)
+ pkg1 = PythonPackage.from_dict(
+ **{
'generate_setup': {
- 'metadata': {
- 'name': 'mars',
- 'version': '0.0.1'
- },
+ 'metadata': {'name': 'mars', 'version': '0.0.1'},
},
- 'inputs': [
- ],
+ 'inputs': [],
'setup_sources': [
'planets/pyproject.toml',
'planets/setup.cfg',
@@ -126,24 +170,12 @@ class TestCreatePythonTree(unittest.TestCase):
'tests': [
'planets/hohmann_transfer_test.py',
],
- },
- # Output file list
- [
- 'mars/__init__.py',
- 'mars/__main__.py',
- 'mars/moons/__init__.py',
- 'mars/moons/deimos.py',
- 'mars/moons/phobos.py',
- 'mars/tests/hohmann_transfer_test.py',
- ],
- ),
+ }
+ )
- (
- # Test name
- 'with extra files',
- # Package name
+ _create_fake_python_package(
+ pkg2_root,
'saturn',
- # File list
[
'planets/BUILD.saturn_rocket',
'planets/hohmann_transfer_test.py',
@@ -159,14 +191,16 @@ class TestCreatePythonTree(unittest.TestCase):
'planets/setup.cfg',
'planets/setup.py',
],
- # Extra files
- [
- 'planets/BUILD.saturn_rocket > out/saturn/BUILD.rocket',
- ],
- # Package definition
- {
- 'inputs': [
- ],
+ install_requires='''
+install_requires =
+ graphlib-backport;python_version<'3.9'
+ httpwatcher
+''',
+ )
+ os.chdir(pkg2_root)
+ pkg2 = PythonPackage.from_dict(
+ **{
+ 'inputs': [],
'setup_sources': [
'planets/pyproject.toml',
'planets/setup.cfg',
@@ -184,23 +218,167 @@ class TestCreatePythonTree(unittest.TestCase):
],
'tests': [
'planets/hohmann_transfer_test.py',
- ]
- },
- # Output file list
- [
- 'saturn/BUILD.rocket',
- 'saturn/__init__.py',
- 'saturn/__main__.py',
- 'saturn/misson.py',
- 'saturn/moons/__init__.py',
- 'saturn/moons/enceladus.py',
- 'saturn/moons/iapetus.py',
- 'saturn/moons/rhea.py',
- 'saturn/moons/titan.py',
- 'saturn/tests/hohmann_transfer_test.py',
- ],
- ),
- ]) # yapf: disable
+ ],
+ }
+ )
+
+ update_config_with_packages(config=config, python_packages=[pkg1, pkg2])
+
+ setup_cfg_text = io.StringIO()
+ config.write(setup_cfg_text)
+ expected_cfg = '''
+[metadata]
+name = megapackage
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Pigweed swiss-army knife
+
+[options]
+zip_safe = False
+packages = find:
+install_requires =
+ coloredlogs
+ coverage
+ cryptography
+ graphlib-backport;python_version<'3.9'
+ httpwatcher
+
+[options.package_data]
+megapackage =
+ py.typed
+mars =
+ py.typed
+saturn =
+ py.typed
+
+[options.entry_points]
+'''
+ result_cfg_lines = [
+ line.rstrip().replace('\t', ' ')
+ for line in setup_cfg_text.getvalue().splitlines()
+ if line
+ ]
+ expected_cfg_lines = [
+ line.rstrip() for line in expected_cfg.splitlines() if line
+ ]
+ self.assertEqual(expected_cfg_lines, result_cfg_lines)
+
+ @parameterized.expand(
+ [
+ (
+ # Test name
+ 'working case',
+ # Package name
+ 'mars',
+ # File list
+ [
+ 'planets/BUILD.mars_rocket',
+ 'planets/mars/__init__.py',
+ 'planets/mars/__main__.py',
+ 'planets/mars/moons/__init__.py',
+ 'planets/mars/moons/deimos.py',
+ 'planets/mars/moons/phobos.py',
+ 'planets/hohmann_transfer_test.py',
+ 'planets/pyproject.toml',
+ 'planets/setup.cfg',
+ ],
+ # Extra_files
+ [],
+ # Package definition
+ {
+ 'generate_setup': {
+ 'metadata': {'name': 'mars', 'version': '0.0.1'},
+ },
+ 'inputs': [],
+ 'setup_sources': [
+ 'planets/pyproject.toml',
+ 'planets/setup.cfg',
+ ],
+ 'sources': [
+ 'planets/mars/__init__.py',
+ 'planets/mars/__main__.py',
+ 'planets/mars/moons/__init__.py',
+ 'planets/mars/moons/deimos.py',
+ 'planets/mars/moons/phobos.py',
+ ],
+ 'tests': [
+ 'planets/hohmann_transfer_test.py',
+ ],
+ },
+ # Output file list
+ [
+ 'mars/__init__.py',
+ 'mars/__main__.py',
+ 'mars/moons/__init__.py',
+ 'mars/moons/deimos.py',
+ 'mars/moons/phobos.py',
+ 'mars/tests/hohmann_transfer_test.py',
+ ],
+ ),
+ (
+ # Test name
+ 'with extra files',
+ # Package name
+ 'saturn',
+ # File list
+ [
+ 'planets/BUILD.saturn_rocket',
+ 'planets/hohmann_transfer_test.py',
+ 'planets/pyproject.toml',
+ 'planets/saturn/__init__.py',
+ 'planets/saturn/__main__.py',
+ 'planets/saturn/misson.py',
+ 'planets/saturn/moons/__init__.py',
+ 'planets/saturn/moons/enceladus.py',
+ 'planets/saturn/moons/iapetus.py',
+ 'planets/saturn/moons/rhea.py',
+ 'planets/saturn/moons/titan.py',
+ 'planets/setup.cfg',
+ 'planets/setup.py',
+ ],
+ # Extra files
+ [
+ 'planets/BUILD.saturn_rocket > out/saturn/BUILD.rocket',
+ ],
+ # Package definition
+ {
+ 'inputs': [],
+ 'setup_sources': [
+ 'planets/pyproject.toml',
+ 'planets/setup.cfg',
+ 'planets/setup.py',
+ ],
+ 'sources': [
+ 'planets/saturn/__init__.py',
+ 'planets/saturn/__main__.py',
+ 'planets/saturn/misson.py',
+ 'planets/saturn/moons/__init__.py',
+ 'planets/saturn/moons/enceladus.py',
+ 'planets/saturn/moons/iapetus.py',
+ 'planets/saturn/moons/rhea.py',
+ 'planets/saturn/moons/titan.py',
+ ],
+ 'tests': [
+ 'planets/hohmann_transfer_test.py',
+ ],
+ },
+ # Output file list
+ [
+ 'saturn/BUILD.rocket',
+ 'saturn/__init__.py',
+ 'saturn/__main__.py',
+ 'saturn/misson.py',
+ 'saturn/moons/__init__.py',
+ 'saturn/moons/enceladus.py',
+ 'saturn/moons/iapetus.py',
+ 'saturn/moons/rhea.py',
+ 'saturn/moons/titan.py',
+ 'saturn/tests/hohmann_transfer_test.py',
+ ],
+ ),
+ ]
+ )
def test_build_python_tree(
self,
_test_name,
@@ -212,79 +390,83 @@ class TestCreatePythonTree(unittest.TestCase):
) -> None:
"""Check results of build_python_tree and copy_extra_files."""
temp_root = Path(self.temp_dir.name)
- _create_fake_python_package(temp_root, file_list, package_name)
+ _create_fake_python_package(temp_root, package_name, file_list)
os.chdir(temp_root)
install_dir = temp_root / 'out'
package = PythonPackage.from_dict(**package_definition)
- build_python_tree(python_packages=[package],
- tree_destination_dir=install_dir,
- include_tests=True)
+ build_python_tree(
+ python_packages=[package],
+ tree_destination_dir=install_dir,
+ include_tests=True,
+ )
copy_extra_files(extra_files)
# Check expected files are in place.
self._check_result_paths_equal(install_dir, expected_file_list)
- @parameterized.expand([
- (
- # Test name
- 'everything in correct locations',
- # Package name
- 'planets',
- # File list
- [
- 'BUILD.mars_rocket',
- ],
- # Extra_files
- [
- 'BUILD.mars_rocket > out/mars/BUILD.rocket',
- ],
- # Output file list
- [
- 'mars/BUILD.rocket',
- ],
- # Should raise exception
- None,
- ),
- (
- # Test name
- 'missing source files',
- # Package name
- 'planets',
- # File list
- [
- 'BUILD.mars_rocket',
- ],
- # Extra_files
- [
- 'BUILD.venus_rocket > out/venus/BUILD.rocket',
- ],
- # Output file list
- [],
- # Should raise exception
- FileNotFoundError,
- ),
- (
- # Test name
- 'existing destination files',
- # Package name
- 'planets',
- # File list
- [
- 'BUILD.jupiter_rocket',
- 'out/jupiter/BUILD.rocket',
- ],
- # Extra_files
- [
- 'BUILD.jupiter_rocket > out/jupiter/BUILD.rocket',
- ],
- # Output file list
- [],
- # Should raise exception
- FileExistsError,
- ),
- ]) # yapf: disable
+ @parameterized.expand(
+ [
+ (
+ # Test name
+ 'everything in correct locations',
+ # Package name
+ 'planets',
+ # File list
+ [
+ 'BUILD.mars_rocket',
+ ],
+ # Extra_files
+ [
+ 'BUILD.mars_rocket > out/mars/BUILD.rocket',
+ ],
+ # Output file list
+ [
+ 'mars/BUILD.rocket',
+ ],
+ # Should raise exception
+ None,
+ ),
+ (
+ # Test name
+ 'missing source files',
+ # Package name
+ 'planets',
+ # File list
+ [
+ 'BUILD.mars_rocket',
+ ],
+ # Extra_files
+ [
+ 'BUILD.venus_rocket > out/venus/BUILD.rocket',
+ ],
+ # Output file list
+ [],
+ # Should raise exception
+ FileNotFoundError,
+ ),
+ (
+ # Test name
+ 'existing destination files',
+ # Package name
+ 'planets',
+ # File list
+ [
+ 'BUILD.jupiter_rocket',
+ 'out/jupiter/BUILD.rocket',
+ ],
+ # Extra_files
+ [
+ 'BUILD.jupiter_rocket > out/jupiter/BUILD.rocket',
+ ],
+ # Output file list
+ [],
+ # Should raise exception
+ FileExistsError,
+ ),
+ ]
+ )
def test_copy_extra_files(
self,
_test_name,
@@ -296,7 +478,7 @@ class TestCreatePythonTree(unittest.TestCase):
) -> None:
"""Check results of build_python_tree and copy_extra_files."""
temp_root = Path(self.temp_dir.name)
- _create_fake_python_package(temp_root, file_list, package_name)
+ _create_fake_python_package(temp_root, package_name, file_list)
os.chdir(temp_root)
install_dir = temp_root / 'out'
diff --git a/pw_build/py/file_prefix_map_test.py b/pw_build/py/file_prefix_map_test.py
index 72630b6b7..0cb258059 100644
--- a/pw_build/py/file_prefix_map_test.py
+++ b/pw_build/py/file_prefix_map_test.py
@@ -20,51 +20,60 @@ import unittest
from pw_build import file_prefix_map
# pylint: disable=line-too-long
-JSON_SOURCE_FILES = json.dumps([
- "../pigweed/pw_polyfill/standard_library_public/pw_polyfill/standard_library/assert.h",
- "protocol_buffer/gen/pigweed/pw_protobuf/common_protos.proto_library/nanopb/pw_protobuf_protos/status.pb.h",
- "../pigweed/pw_rpc/client_server.cc",
- "../pigweed/pw_rpc/public/pw_rpc/client_server.h",
- "/home/user/pigweed/out/../gen/generated_build_info.cc",
- "/home/user/pigweed/pw_protobuf/encoder.cc",
-])
+JSON_SOURCE_FILES = json.dumps(
+ [
+ "../pigweed/pw_polyfill/standard_library_public/pw_polyfill/standard_library/assert.h",
+ "protocol_buffer/gen/pigweed/pw_protobuf/common_protos.proto_library/nanopb/pw_protobuf_protos/status.pb.h",
+ "../pigweed/pw_rpc/client_server.cc",
+ "../pigweed/pw_rpc/public/pw_rpc/client_server.h",
+ "/home/user/pigweed/out/../gen/generated_build_info.cc",
+ "/home/user/pigweed/pw_protobuf/encoder.cc",
+ ]
+)
-JSON_PATH_TRANSFORMATIONS = json.dumps([
- "/home/user/pigweed/out=out",
- "/home/user/pigweed/=",
- "../=",
- "/home/user/pigweed/out=out",
-])
+JSON_PATH_TRANSFORMATIONS = json.dumps(
+ [
+ "/home/user/pigweed/out=out",
+ "/home/user/pigweed/=",
+ "../=",
+ "/home/user/pigweed/out=out",
+ ]
+)
-EXPECTED_TRANSFORMED_PATHS = json.dumps([
- "pigweed/pw_polyfill/standard_library_public/pw_polyfill/standard_library/assert.h",
- "protocol_buffer/gen/pigweed/pw_protobuf/common_protos.proto_library/nanopb/pw_protobuf_protos/status.pb.h",
- "pigweed/pw_rpc/client_server.cc",
- "pigweed/pw_rpc/public/pw_rpc/client_server.h",
- "out/../gen/generated_build_info.cc",
- "pw_protobuf/encoder.cc",
-])
+EXPECTED_TRANSFORMED_PATHS = json.dumps(
+ [
+ "pigweed/pw_polyfill/standard_library_public/pw_polyfill/standard_library/assert.h",
+ "protocol_buffer/gen/pigweed/pw_protobuf/common_protos.proto_library/nanopb/pw_protobuf_protos/status.pb.h",
+ "pigweed/pw_rpc/client_server.cc",
+ "pigweed/pw_rpc/public/pw_rpc/client_server.h",
+ "out/../gen/generated_build_info.cc",
+ "pw_protobuf/encoder.cc",
+ ]
+)
class FilePrefixMapTest(unittest.TestCase):
def test_prefix_remap(self):
path_list = [
'/foo_root/root_subdir/source.cc',
- '/foo_root/root_subdir/out/../gen.cc'
+ '/foo_root/root_subdir/out/../gen.cc',
]
prefix_maps = [('/foo_root/root_subdir/', ''), ('out/../', 'out/')]
expected_paths = ['source.cc', 'out/gen.cc']
self.assertEqual(
list(file_prefix_map.remap_paths(path_list, prefix_maps)),
- expected_paths)
+ expected_paths,
+ )
def test_json_prefix_map(self):
in_fd = StringIO(JSON_SOURCE_FILES)
prefix_map_fd = StringIO(JSON_PATH_TRANSFORMATIONS)
out_fd = StringIO()
file_prefix_map.remap_json_paths(in_fd, out_fd, prefix_map_fd)
- self.assertEqual(json.loads(out_fd.getvalue()),
- json.loads(EXPECTED_TRANSFORMED_PATHS))
+ self.assertEqual(
+ json.loads(out_fd.getvalue()),
+ json.loads(EXPECTED_TRANSFORMED_PATHS),
+ )
if __name__ == '__main__':
diff --git a/pw_build/py/generate_cc_blob_library_test.py b/pw_build/py/generate_cc_blob_library_test.py
index 2185f85cb..6bdf311fb 100644
--- a/pw_build/py/generate_cc_blob_library_test.py
+++ b/pw_build/py/generate_cc_blob_library_test.py
@@ -15,31 +15,83 @@
from pathlib import Path
import tempfile
-import textwrap
import unittest
from pw_build import generate_cc_blob_library
+COMMENT = """\
+// This file was generated by generate_cc_blob_library.py.
+//
+// DO NOT EDIT!
+//
+// This file contains declarations for byte arrays created from files during the
+// build. The byte arrays are constant initialized and are safe to access at any
+// time, including before main().
+//
+// See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
+"""
+
+COMMON_HEADER_START = (
+ COMMENT
+ + """\
+#pragma once
+
+#include <array>
+#include <cstddef>
+"""
+)
+
+COMMON_SOURCE_START = (
+ COMMENT
+ + """\
+
+#include "path/to/header.h"
+
+#include <array>
+#include <cstddef>
+
+#include "pw_preprocessor/compiler.h"
+"""
+)
+
+FOO_BLOB = """\
+constexpr std::array<std::byte, 2> fooBlob = {
+ std::byte{0x01}, std::byte{0x02},
+};
+"""
+
+BAR_BLOB = """\
+constexpr std::array<std::byte, 10> barBlob = {
+ std::byte{0x01}, std::byte{0x02}, std::byte{0x03}, std::byte{0x04},
+ std::byte{0x05}, std::byte{0x06}, std::byte{0x07}, std::byte{0x08},
+ std::byte{0x09}, std::byte{0x0A},
+};
+"""
+
class TestSplitIntoChunks(unittest.TestCase):
"""Unit tests for the split_into_chunks() function."""
+
def test_perfect_split(self):
"""Tests basic splitting where the iterable divides perfectly."""
data = (1, 7, 0, 1)
self.assertEqual(
((1, 7), (0, 1)),
- tuple(generate_cc_blob_library.split_into_chunks(data, 2)))
+ tuple(generate_cc_blob_library.split_into_chunks(data, 2)),
+ )
def test_split_with_remainder(self):
"""Tests basic splitting where there is a remainder."""
data = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
self.assertEqual(
- ((1, 2, 3), (4, 5, 6), (7, 8, 9), (10, )),
- tuple(generate_cc_blob_library.split_into_chunks(data, 3)))
+ ((1, 2, 3), (4, 5, 6), (7, 8, 9), (10,)),
+ tuple(generate_cc_blob_library.split_into_chunks(data, 3)),
+ )
class TestHeaderFromBlobs(unittest.TestCase):
"""Unit tests for the header_from_blobs() function."""
+
def test_single_blob_header(self):
"""Tests the generation of a header for a single blob."""
foo_blob = Path(tempfile.NamedTemporaryFile().name)
@@ -47,18 +99,11 @@ class TestHeaderFromBlobs(unittest.TestCase):
blobs = [generate_cc_blob_library.Blob('fooBlob', foo_blob, None)]
header = generate_cc_blob_library.header_from_blobs(blobs)
- expected_header = textwrap.dedent("""\
- // This file is auto-generated; Do not hand-modify!
- // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
-
- #pragma once
-
- #include <array>
- #include <cstddef>
-
- extern const std::array<std::byte, 6> fooBlob;
- """)
-
+ expected_header = (
+ f'{COMMON_HEADER_START}'
+ '\n\n' # No namespace, so two blank lines
+ 'extern const std::array<std::byte, 6> fooBlob;\n'
+ )
self.assertEqual(expected_header, header)
def test_multi_blob_header(self):
@@ -73,19 +118,13 @@ class TestHeaderFromBlobs(unittest.TestCase):
]
header = generate_cc_blob_library.header_from_blobs(blobs)
- expected_header = textwrap.dedent("""\
- // This file is auto-generated; Do not hand-modify!
- // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
-
- #pragma once
-
- #include <array>
- #include <cstddef>
-
- extern const std::array<std::byte, 6> fooBlob;
-
- extern const std::array<std::byte, 5> barBlob;
- """)
+ expected_header = (
+ f'{COMMON_HEADER_START}\n'
+ '\n'
+ 'extern const std::array<std::byte, 6> fooBlob;\n'
+ '\n'
+ 'extern const std::array<std::byte, 5> barBlob;\n'
+ )
self.assertEqual(expected_header, header)
@@ -96,57 +135,44 @@ class TestHeaderFromBlobs(unittest.TestCase):
blobs = [generate_cc_blob_library.Blob('fooBlob', foo_blob, None)]
header = generate_cc_blob_library.header_from_blobs(blobs, 'pw::foo')
- expected_header = textwrap.dedent("""\
- // This file is auto-generated; Do not hand-modify!
- // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
-
- #pragma once
-
- #include <array>
- #include <cstddef>
-
- namespace pw::foo {
-
- extern const std::array<std::byte, 6> fooBlob;
-
- } // namespace pw::foo
- """)
+ expected_header = (
+ f'{COMMON_HEADER_START}'
+ '\n'
+ 'namespace pw::foo {\n'
+ '\n'
+ 'extern const std::array<std::byte, 6> fooBlob;\n'
+ '\n'
+ '} // namespace pw::foo\n'
+ )
self.assertEqual(expected_header, header)
class TestArrayDefFromBlobData(unittest.TestCase):
"""Unit tests for the array_def_from_blob_data() function."""
+
def test_single_line(self):
- """Tests the generation of single-line array definitions."""
+ """Tests the generation of array definitions with one line of data."""
foo_data = bytes((1, 2))
foo_definition = generate_cc_blob_library.array_def_from_blob_data(
- 'fooBlob', foo_data)
- expected_definition = ('const std::array<std::byte, 2> fooBlob'
- ' = { std::byte{0x01}, std::byte{0x02} };')
-
- self.assertEqual(expected_definition, foo_definition)
+ generate_cc_blob_library.Blob('fooBlob', Path(), None), foo_data
+ )
+ self.assertEqual(f'\n{FOO_BLOB}', foo_definition)
def test_multi_line(self):
"""Tests the generation of multi-line array definitions."""
- foo_data = bytes((1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
-
- foo_definition = generate_cc_blob_library.array_def_from_blob_data(
- 'fooBlob', foo_data)
- expected_definition = ('const std::array<std::byte, 10> fooBlob = {\n'
- ' std::byte{0x01}, std::byte{0x02}, '
- 'std::byte{0x03}, std::byte{0x04},\n'
- ' std::byte{0x05}, std::byte{0x06}, '
- 'std::byte{0x07}, std::byte{0x08},\n'
- ' std::byte{0x09}, std::byte{0x0A}\n'
- '};')
+ bar_data = bytes((1, 2, 3, 4, 5, 6, 7, 8, 9, 10))
- self.assertEqual(expected_definition, foo_definition)
+ bar_definition = generate_cc_blob_library.array_def_from_blob_data(
+ generate_cc_blob_library.Blob('barBlob', Path(), None), bar_data
+ )
+ self.assertEqual(f'\n{BAR_BLOB}', bar_definition)
class TestSourceFromBlobs(unittest.TestCase):
"""Unit tests for the source_from_blobs() function."""
+
def test_source_with_mixed_blobs(self):
"""Tests generation of a source file with single- and multi-liners."""
foo_blob = Path(tempfile.NamedTemporaryFile().name)
@@ -159,28 +185,11 @@ class TestSourceFromBlobs(unittest.TestCase):
]
source = generate_cc_blob_library.source_from_blobs(
- blobs, 'path/to/header.h')
- expected_source = textwrap.dedent("""\
- // This file is auto-generated; Do not hand-modify!
- // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
-
- #include "path/to/header.h"
-
- #include <array>
- #include <cstddef>
-
- #include "pw_preprocessor/compiler.h"
-
- """)
- expected_source += ('const std::array<std::byte, 2> fooBlob'
- ' = { std::byte{0x01}, std::byte{0x02} };\n\n')
- expected_source += ('const std::array<std::byte, 10> barBlob = {\n'
- ' std::byte{0x01}, std::byte{0x02}, '
- 'std::byte{0x03}, std::byte{0x04},\n'
- ' std::byte{0x05}, std::byte{0x06}, '
- 'std::byte{0x07}, std::byte{0x08},\n'
- ' std::byte{0x09}, std::byte{0x0A}\n'
- '};\n')
+ blobs, 'path/to/header.h'
+ )
+ expected_source = (
+ f'{COMMON_SOURCE_START}' '\n' '\n' f'{FOO_BLOB}' '\n' f'{BAR_BLOB}'
+ )
self.assertEqual(expected_source, source)
@@ -191,63 +200,70 @@ class TestSourceFromBlobs(unittest.TestCase):
blobs = [generate_cc_blob_library.Blob('fooBlob', foo_blob, None)]
source = generate_cc_blob_library.source_from_blobs(
- blobs, 'path/to/header.h', 'pw::foo')
- expected_source = textwrap.dedent("""\
- // This file is auto-generated; Do not hand-modify!
- // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
-
- #include "path/to/header.h"
-
- #include <array>
- #include <cstddef>
+ blobs, 'path/to/header.h', 'pw::foo'
+ )
+ expected_source = (
+ f'{COMMON_SOURCE_START}'
+ '\n'
+ 'namespace pw::foo {\n'
+ '\n'
+ f'{FOO_BLOB}'
+ '\n'
+ '} // namespace pw::foo\n'
+ )
- #include "pw_preprocessor/compiler.h"
-
- namespace pw::foo {
+ self.assertEqual(expected_source, source)
- const std::array<std::byte, 2> fooBlob = { std::byte{0x01}, std::byte{0x02} };
+ def test_source_with_linker_sections(self):
+ """Tests generation of a source file with defined linker sections."""
+ foo_blob = Path(tempfile.NamedTemporaryFile().name)
+ foo_blob.write_bytes(bytes((1, 2)))
+ bar_blob = Path(tempfile.NamedTemporaryFile().name)
+ bar_blob.write_bytes(bytes((1, 2, 3, 4, 5, 6, 7, 8, 9, 10)))
+ blobs = [
+ generate_cc_blob_library.Blob('fooBlob', foo_blob, '.foo_section'),
+ generate_cc_blob_library.Blob('barBlob', bar_blob, '.bar_section'),
+ ]
- } // namespace pw::foo
- """)
+ source = generate_cc_blob_library.source_from_blobs(
+ blobs, 'path/to/header.h'
+ )
+ expected_source = (
+ f'{COMMON_SOURCE_START}'
+ '\n'
+ '\n'
+ 'PW_PLACE_IN_SECTION(".foo_section")\n'
+ f'{FOO_BLOB}'
+ '\n'
+ 'PW_PLACE_IN_SECTION(".bar_section")\n'
+ f'{BAR_BLOB}'
+ )
self.assertEqual(expected_source, source)
- def test_source_with_linker_sections(self):
- """Tests generation of a source file with defined linker sections"""
+ def test_source_with_alignas(self):
+ """Tests generation of a source file with alignas specified."""
foo_blob = Path(tempfile.NamedTemporaryFile().name)
foo_blob.write_bytes(bytes((1, 2)))
bar_blob = Path(tempfile.NamedTemporaryFile().name)
bar_blob.write_bytes(bytes((1, 2, 3, 4, 5, 6, 7, 8, 9, 10)))
blobs = [
- generate_cc_blob_library.Blob('fooBlob', foo_blob, ".foo_section"),
- generate_cc_blob_library.Blob('barBlob', bar_blob, ".bar_section"),
+ generate_cc_blob_library.Blob('fooBlob', foo_blob, None, '64'),
+ generate_cc_blob_library.Blob('barBlob', bar_blob, '.abc', 'int'),
]
source = generate_cc_blob_library.source_from_blobs(
- blobs, 'path/to/header.h')
- expected_source = textwrap.dedent("""\
- // This file is auto-generated; Do not hand-modify!
- // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
-
- #include "path/to/header.h"
-
- #include <array>
- #include <cstddef>
-
- #include "pw_preprocessor/compiler.h"
-
- """)
- expected_source += ('PW_PLACE_IN_SECTION(".foo_section")\n'
- 'const std::array<std::byte, 2> fooBlob'
- ' = { std::byte{0x01}, std::byte{0x02} };\n\n')
- expected_source += ('PW_PLACE_IN_SECTION(".bar_section")\n'
- 'const std::array<std::byte, 10> barBlob = {\n'
- ' std::byte{0x01}, std::byte{0x02}, '
- 'std::byte{0x03}, std::byte{0x04},\n'
- ' std::byte{0x05}, std::byte{0x06}, '
- 'std::byte{0x07}, std::byte{0x08},\n'
- ' std::byte{0x09}, std::byte{0x0A}\n'
- '};\n')
+ blobs, 'path/to/header.h'
+ )
+ expected_source = (
+ f'{COMMON_SOURCE_START}'
+ '\n'
+ '\n'
+ f'alignas(64) {FOO_BLOB}'
+ '\n'
+ 'PW_PLACE_IN_SECTION(".abc")\n'
+ f'alignas(int) {BAR_BLOB}'
+ )
self.assertEqual(expected_source, source)
diff --git a/pw_build/py/project_builder_prefs_test.py b/pw_build/py/project_builder_prefs_test.py
new file mode 100644
index 000000000..f128bee2b
--- /dev/null
+++ b/pw_build/py/project_builder_prefs_test.py
@@ -0,0 +1,129 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for pw_build.project_builder_prefs"""
+
+import argparse
+import copy
+from pathlib import Path
+import tempfile
+from typing import Any, Dict
+import unittest
+from unittest.mock import MagicMock
+
+from pw_build.project_builder_argparse import add_project_builder_arguments
+from pw_build.project_builder_prefs import (
+ ProjectBuilderPrefs,
+ _DEFAULT_CONFIG,
+ load_defaults_from_argparse,
+)
+
+
+def _create_tempfile(content: str) -> Path:
+ with tempfile.NamedTemporaryFile(
+ prefix=f'{__package__}', delete=False
+ ) as output_file:
+ output_file.write(content.encode('UTF-8'))
+ return Path(output_file.name)
+
+
+class TestProjectBuilderPrefs(unittest.TestCase):
+ """Tests for ProjectBuilderPrefs."""
+
+ def setUp(self):
+ self.maxDiff = None # pylint: disable=invalid-name
+
+ def test_load_no_existing_files(self) -> None:
+ # Create a prefs instance with no loaded config.
+ prefs = ProjectBuilderPrefs(
+ load_argparse_arguments=add_project_builder_arguments,
+ project_file=False,
+ project_user_file=False,
+ user_file=False,
+ )
+ # Construct an expected result config.
+ expected_config: Dict[Any, Any] = {}
+ expected_config.update(_DEFAULT_CONFIG)
+ expected_config.update(
+ load_defaults_from_argparse(add_project_builder_arguments)
+ )
+
+ self.assertEqual(
+ prefs._config, expected_config # pylint: disable=protected-access
+ )
+
+ def test_apply_command_line_args(self) -> None:
+ """Check command line args are applied to watch preferences."""
+ # Load default command line arg values.
+ defaults_from_argparse = load_defaults_from_argparse(
+ add_project_builder_arguments
+ )
+
+ # Create a prefs instance with the test config file.
+ prefs = ProjectBuilderPrefs(
+ load_argparse_arguments=add_project_builder_arguments,
+ project_file=False,
+ project_user_file=False,
+ user_file=False,
+ )
+
+ # Construct an expected result config.
+ expected_config: Dict[Any, Any] = copy.copy(_DEFAULT_CONFIG)
+ expected_config.update(defaults_from_argparse)
+
+ # pylint: disable=protected-access
+ prefs._update_config = MagicMock( # type: ignore
+ wraps=prefs._update_config
+ )
+ # pylint: enable=protected-access
+
+ args_dict = copy.deepcopy(defaults_from_argparse)
+ changed_args = {
+ 'jobs': 8,
+ 'colors': False,
+ 'build_system_commands': [
+ ['out', 'bazel build'],
+ ['out', 'bazel test'],
+ ],
+ }
+ args_dict.update(changed_args)
+
+ prefs.apply_command_line_args(argparse.Namespace(**args_dict))
+
+ # apply_command_line_args modifies build_system_commands to match the
+ # prefs dict format.
+ changed_args['build_system_commands'] = {
+ 'default': {'commands': [{'command': 'ninja', 'extra_args': []}]},
+ 'out': {
+ 'commands': [
+ {'command': 'bazel', 'extra_args': ['build']},
+ {'command': 'bazel', 'extra_args': ['test']},
+ ],
+ },
+ }
+
+ # Check that only args changed from their defaults are applied.
+ # pylint: disable=protected-access
+ prefs._update_config.assert_called_once_with(changed_args)
+ # pylint: enable=protected-access
+
+ # Check the result includes the project_config settings and the
+ # changed_args.
+ expected_config.update(changed_args)
+ # pylint: disable=protected-access
+ self.assertEqual(prefs._config, expected_config)
+ # pylint: enable=protected-access
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_build/py/pw_build/build_recipe.py b/pw_build/py/pw_build/build_recipe.py
new file mode 100644
index 000000000..ebd4c1b55
--- /dev/null
+++ b/pw_build/py/pw_build/build_recipe.py
@@ -0,0 +1,530 @@
+#!/usr/bin/env python
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Watch build config dataclasses."""
+
+from dataclasses import dataclass, field
+import logging
+from pathlib import Path
+import shlex
+from typing import Callable, Dict, List, Optional, TYPE_CHECKING
+
+from prompt_toolkit.formatted_text import ANSI, StyleAndTextTuples
+from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
+
+if TYPE_CHECKING:
+ from pw_build.project_builder import ProjectBuilder
+ from pw_build.project_builder_prefs import ProjectBuilderPrefs
+
+_LOG = logging.getLogger('pw_build.watch')
+
+
+class UnknownBuildSystem(Exception):
+ """Exception for requesting unsupported build systems."""
+
+
+class UnknownBuildDir(Exception):
+ """Exception for an unknown build dir before command running."""
+
+
+@dataclass
+class BuildCommand:
+ """Store details of a single build step.
+
+ Example usage:
+
+ .. code-block:: python
+
+ from pw_build.build_recipe import BuildCommand, BuildRecipe
+
+ def should_gen_gn(out: Path):
+ return not (out / 'build.ninja').is_file()
+
+ cmd1 = BuildCommand(build_dir='out',
+ command=['gn', 'gen', '{build_dir}'],
+ run_if=should_gen_gn)
+
+ cmd2 = BuildCommand(build_dir='out',
+ build_system_command='ninja',
+ build_system_extra_args=['-k', '0'],
+ targets=['default']),
+
+ Args:
+ build_dir: Output directory for this build command. This can be omitted
+ if the BuildCommand is included in the steps of a BuildRecipe.
+ build_system_command: This command should end with ``ninja``, ``make``,
+ or ``bazel``.
+ build_system_extra_args: A list of extra arguments passed to the
+ build_system_command. If running ``bazel test`` include ``test`` as
+ an extra arg here.
+ targets: Optional list of targets to build in the build_dir.
+ command: List of strings to run as a command. These are passed to
+ subprocess.run(). Any instances of the ``'{build_dir}'`` string
+ literal will be replaced at run time with the out directory.
+ run_if: A callable function to run before executing this
+ BuildCommand. The callable takes one Path arg for the build_dir. If
+ the callable returns true this command is executed. All
+ BuildCommands are run by default.
+ """
+
+ build_dir: Optional[Path] = None
+ build_system_command: Optional[str] = None
+ build_system_extra_args: List[str] = field(default_factory=list)
+ targets: List[str] = field(default_factory=list)
+ command: List[str] = field(default_factory=list)
+ run_if: Callable[[Path], bool] = lambda _build_dir: True
+
+ def __post_init__(self) -> None:
+ # Copy self._expanded_args from the command list.
+ self._expanded_args: List[str] = []
+ if self.command:
+ self._expanded_args = self.command
+
+ def should_run(self) -> bool:
+ """Return True if this build command should be run."""
+ if self.build_dir:
+ return self.run_if(self.build_dir)
+ return True
+
+ def _get_starting_build_system_args(self) -> List[str]:
+ """Return flags that appear immediately after the build command."""
+ assert self.build_system_command
+ assert self.build_dir
+ if self.build_system_command.endswith('bazel'):
+ return ['--output_base', str(self.build_dir)]
+ return []
+
+ def _get_build_system_args(self) -> List[str]:
+ assert self.build_system_command
+ assert self.build_dir
+
+ # Both make and ninja use -C for a build directory.
+ if self.build_system_command.endswith(
+ 'make'
+ ) or self.build_system_command.endswith('ninja'):
+ return ['-C', str(self.build_dir), *self.targets]
+
+ # Bazel relies on --output_base which is handled by the
+ # _get_starting_build_system_args() function.
+ if self.build_system_command.endswith('bazel'):
+ return [*self.targets]
+
+ raise UnknownBuildSystem(
+ f'\n\nUnknown build system command "{self.build_system_command}" '
+ f'for build directory "{self.build_dir}".\n'
+ 'Supported commands: ninja, bazel, make'
+ )
+
+ def _resolve_expanded_args(self) -> List[str]:
+ """Replace instances of '{build_dir}' with the self.build_dir."""
+ resolved_args = []
+ for arg in self._expanded_args:
+ if arg == "{build_dir}":
+ if not self.build_dir:
+ raise UnknownBuildDir(
+ '\n\nUnknown "{build_dir}" value for command:\n'
+ f' {self._expanded_args}\n'
+ f'In BuildCommand: {repr(self)}\n\n'
+ 'Check build_dir is set for the above BuildCommand'
+ 'or included as a step to a BuildRecipe.'
+ )
+ resolved_args.append(str(self.build_dir.resolve()))
+ else:
+ resolved_args.append(arg)
+ return resolved_args
+
+ def ninja_command(self) -> bool:
+ if self.build_system_command and self.build_system_command.endswith(
+ 'ninja'
+ ):
+ return True
+ return False
+
+ def bazel_command(self) -> bool:
+ if self.build_system_command and self.build_system_command.endswith(
+ 'bazel'
+ ):
+ return True
+ return False
+
+ def bazel_build_command(self) -> bool:
+ if self.bazel_command() and 'build' in self.build_system_extra_args:
+ return True
+ return False
+
+ def bazel_clean_command(self) -> bool:
+ if self.bazel_command() and 'clean' in self.build_system_extra_args:
+ return True
+ return False
+
+ def get_args(
+ self,
+ additional_ninja_args: Optional[List[str]] = None,
+ additional_bazel_args: Optional[List[str]] = None,
+ additional_bazel_build_args: Optional[List[str]] = None,
+ ) -> List[str]:
+ """Return all args required to launch this BuildCommand."""
+ # If this is a plain command step, return self._expanded_args as-is.
+ if not self.build_system_command:
+ return self._resolve_expanded_args()
+
+ # Assmemble user-defined extra args.
+ extra_args = []
+ extra_args.extend(self.build_system_extra_args)
+ if additional_ninja_args and self.ninja_command():
+ extra_args.extend(additional_ninja_args)
+
+ if additional_bazel_build_args and self.bazel_build_command():
+ extra_args.extend(additional_bazel_build_args)
+
+ if additional_bazel_args and self.bazel_command():
+ extra_args.extend(additional_bazel_args)
+
+ build_system_target_args = []
+ if not self.bazel_clean_command():
+ build_system_target_args = self._get_build_system_args()
+
+ # Construct the build system command args.
+ command = [
+ self.build_system_command,
+ *self._get_starting_build_system_args(),
+ *extra_args,
+ *build_system_target_args,
+ ]
+ return command
+
+ def __str__(self) -> str:
+ return ' '.join(shlex.quote(arg) for arg in self.get_args())
+
+
+@dataclass
+class BuildRecipeStatus:
+ """Stores the status of a build recipe."""
+
+ recipe: 'BuildRecipe'
+ current_step: str = ''
+ percent: float = 0.0
+ error_count: int = 0
+ return_code: Optional[int] = None
+ flag_done: bool = False
+ flag_started: bool = False
+ error_lines: Dict[int, List[str]] = field(default_factory=dict)
+
+ def pending(self) -> bool:
+ return self.return_code is None
+
+ def failed(self) -> bool:
+ if self.return_code is not None:
+ return self.return_code != 0
+ return False
+
+ def append_failure_line(self, line: str) -> None:
+ lines = self.error_lines.get(self.error_count, [])
+ lines.append(line)
+ self.error_lines[self.error_count] = lines
+
+ def increment_error_count(self, count: int = 1) -> None:
+ self.error_count += count
+ if self.error_count not in self.error_lines:
+ self.error_lines[self.error_count] = []
+
+ def should_log_failures(self) -> bool:
+ return (
+ self.recipe.project_builder is not None
+ and self.recipe.project_builder.separate_build_file_logging
+ and (not self.recipe.project_builder.send_recipe_logs_to_root)
+ )
+
+ def log_last_failure(self) -> None:
+ """Log the last ninja error if available."""
+ if not self.should_log_failures():
+ return
+
+ logger = self.recipe.error_logger
+ if not logger:
+ return
+
+ _color = self.recipe.project_builder.color # type: ignore
+
+ lines = self.error_lines.get(self.error_count, [])
+ _LOG.error('')
+ _LOG.error(' ╔════════════════════════════════════')
+ _LOG.error(
+ ' ║ START %s Failure #%d:',
+ _color.cyan(self.recipe.display_name),
+ self.error_count,
+ )
+
+ logger.error('')
+ for line in lines:
+ logger.error(line)
+ logger.error('')
+
+ _LOG.error(
+ ' ║ END %s Failure #%d',
+ _color.cyan(self.recipe.display_name),
+ self.error_count,
+ )
+ _LOG.error(" ╚════════════════════════════════════")
+ _LOG.error('')
+
+ def log_entire_recipe_logfile(self) -> None:
+ """Log the entire build logfile if no ninja errors available."""
+ if not self.should_log_failures():
+ return
+
+ recipe_logfile = self.recipe.logfile
+ if not recipe_logfile:
+ return
+
+ _color = self.recipe.project_builder.color # type: ignore
+
+ logfile_path = str(recipe_logfile.resolve())
+
+ _LOG.error('')
+ _LOG.error(' ╔════════════════════════════════════')
+ _LOG.error(
+ ' ║ %s Failure; Entire log below:',
+ _color.cyan(self.recipe.display_name),
+ )
+ _LOG.error(' ║ %s %s', _color.yellow('START'), logfile_path)
+
+ logger = self.recipe.error_logger
+ if not logger:
+ return
+
+ logger.error('')
+ for line in recipe_logfile.read_text(
+ encoding='utf-8', errors='ignore'
+ ).splitlines():
+ logger.error(line)
+ logger.error('')
+
+ _LOG.error(' ║ %s %s', _color.yellow('END'), logfile_path)
+ _LOG.error(" ╚════════════════════════════════════")
+ _LOG.error('')
+
+ def status_slug(self, restarting: bool = False) -> OneStyleAndTextTuple:
+ status = ('', '')
+ if not self.recipe.enabled:
+ return ('fg:ansidarkgray', 'Disabled')
+
+ waiting = False
+ if self.done:
+ if self.passed():
+ status = ('fg:ansigreen', 'OK ')
+ elif self.failed():
+ status = ('fg:ansired', 'FAIL ')
+ elif self.started:
+ status = ('fg:ansiyellow', 'Building')
+ else:
+ waiting = True
+ status = ('default', 'Waiting ')
+
+ # Only show Aborting if the process is building (or has failures).
+ if restarting and not waiting and not self.passed():
+ status = ('fg:ansiyellow', 'Aborting')
+ return status
+
+ def current_step_formatted(self) -> StyleAndTextTuples:
+ formatted_text: StyleAndTextTuples = []
+ if self.passed():
+ return formatted_text
+
+ if self.current_step:
+ if '\x1b' in self.current_step:
+ formatted_text = ANSI(self.current_step).__pt_formatted_text__()
+ else:
+ formatted_text = [('', self.current_step)]
+
+ return formatted_text
+
+ @property
+ def done(self) -> bool:
+ return self.flag_done
+
+ @property
+ def started(self) -> bool:
+ return self.flag_started
+
+ def mark_done(self) -> None:
+ self.flag_done = True
+
+ def mark_started(self) -> None:
+ self.flag_started = True
+
+ def set_failed(self) -> None:
+ self.flag_done = True
+ self.return_code = -1
+
+ def set_passed(self) -> None:
+ self.flag_done = True
+ self.return_code = 0
+
+ def passed(self) -> bool:
+ if self.done and self.return_code is not None:
+ return self.return_code == 0
+ return False
+
+
+@dataclass
+class BuildRecipe:
+ """Dataclass to store a list of BuildCommands.
+
+ Example usage:
+
+ .. code-block:: python
+
+ from pw_build.build_recipe import BuildCommand, BuildRecipe
+
+ def should_gen_gn(out: Path) -> bool:
+ return not (out / 'build.ninja').is_file()
+
+ recipe = BuildRecipe(
+ build_dir='out',
+ title='Vanilla Ninja Build',
+ steps=[
+ BuildCommand(command=['gn', 'gen', '{build_dir}'],
+ run_if=should_gen_gn),
+ BuildCommand(build_system_command='ninja',
+ build_system_extra_args=['-k', '0'],
+ targets=['default']),
+ ],
+ )
+
+ Args:
+ build_dir: Output directory for this BuildRecipe. On init this out dir
+ is set for all included steps.
+ steps: List of BuildCommands to run.
+ title: Custom title. The build_dir is used if this is ommited.
+ """
+
+ build_dir: Path
+ steps: List[BuildCommand] = field(default_factory=list)
+ title: Optional[str] = None
+ enabled: bool = True
+
+ def __hash__(self):
+ return hash((self.build_dir, self.title, len(self.steps)))
+
+ def __post_init__(self) -> None:
+ # Update all included steps to use this recipe's build_dir.
+ for step in self.steps:
+ if self.build_dir:
+ step.build_dir = self.build_dir
+
+ # Set logging variables
+ self._logger: Optional[logging.Logger] = None
+ self.error_logger: Optional[logging.Logger] = None
+ self._logfile: Optional[Path] = None
+ self._status: BuildRecipeStatus = BuildRecipeStatus(self)
+ self.project_builder: Optional['ProjectBuilder'] = None
+
+ def toggle_enabled(self) -> None:
+ self.enabled = not self.enabled
+
+ def set_project_builder(self, project_builder) -> None:
+ self.project_builder = project_builder
+
+ def set_targets(self, new_targets: List[str]) -> None:
+ """Reset all build step targets."""
+ for step in self.steps:
+ step.targets = new_targets
+
+ def set_logger(self, logger: logging.Logger) -> None:
+ self._logger = logger
+
+ def set_error_logger(self, logger: logging.Logger) -> None:
+ self.error_logger = logger
+
+ def set_logfile(self, log_file: Path) -> None:
+ self._logfile = log_file
+
+ def reset_status(self) -> None:
+ self._status = BuildRecipeStatus(self)
+
+ @property
+ def status(self) -> BuildRecipeStatus:
+ return self._status
+
+ @property
+ def log(self) -> logging.Logger:
+ if self._logger:
+ return self._logger
+ return logging.getLogger()
+
+ @property
+ def logfile(self) -> Optional[Path]:
+ return self._logfile
+
+ @property
+ def display_name(self) -> str:
+ if self.title:
+ return self.title
+ return str(self.build_dir)
+
+ def targets(self) -> List[str]:
+ return list(
+ set(target for step in self.steps for target in step.targets)
+ )
+
+ def __str__(self) -> str:
+ message = self.display_name
+ targets = self.targets()
+ if targets:
+ target_list = ' '.join(self.targets())
+ message = f'{message} -- {target_list}'
+ return message
+
+
+def create_build_recipes(prefs: 'ProjectBuilderPrefs') -> List[BuildRecipe]:
+ """Create a list of BuildRecipes from ProjectBuilderPrefs."""
+ build_recipes: List[BuildRecipe] = []
+
+ if prefs.run_commands:
+ for command_str in prefs.run_commands:
+ build_recipes.append(
+ BuildRecipe(
+ build_dir=Path.cwd(),
+ steps=[BuildCommand(command=shlex.split(command_str))],
+ title=command_str,
+ )
+ )
+
+ for build_dir, targets in prefs.build_directories.items():
+ steps: List[BuildCommand] = []
+ build_path = Path(build_dir)
+ if not targets:
+ targets = []
+
+ for (
+ build_system_command,
+ build_system_extra_args,
+ ) in prefs.build_system_commands(build_dir):
+ steps.append(
+ BuildCommand(
+ build_system_command=build_system_command,
+ build_system_extra_args=build_system_extra_args,
+ targets=targets,
+ )
+ )
+
+ build_recipes.append(
+ BuildRecipe(
+ build_dir=build_path,
+ steps=steps,
+ )
+ )
+
+ return build_recipes
diff --git a/pw_build/py/pw_build/collect_wheels.py b/pw_build/py/pw_build/collect_wheels.py
index ac53c2815..5b4da5b55 100644
--- a/pw_build/py/pw_build/collect_wheels.py
+++ b/pw_build/py/pw_build/collect_wheels.py
@@ -18,6 +18,7 @@ import logging
from pathlib import Path
import shutil
import sys
+from typing import Dict
_LOG = logging.getLogger(__name__)
@@ -27,16 +28,21 @@ def _parse_args():
parser.add_argument(
'--prefix',
type=Path,
- help='Root search path to use in conjunction with --wheels_file')
+ help='Root search path to use in conjunction with --wheels_file',
+ )
parser.add_argument(
'--suffix_file',
type=argparse.FileType('r'),
- help=('File that lists subdirs relative to --prefix, one per line,'
- 'to search for .whl files to copy into --out_dir'))
+ help=(
+ 'File that lists subdirs relative to --prefix, one per line,'
+ 'to search for .whl files to copy into --out_dir'
+ ),
+ )
parser.add_argument(
'--out_dir',
type=Path,
- help='Path where all the built and collected .whl files should be put')
+ help='Path where all the built and collected .whl files should be put',
+ )
return parser.parse_args()
@@ -45,12 +51,24 @@ def copy_wheels(prefix, suffix_file, out_dir):
if not out_dir.exists():
out_dir.mkdir()
+ copied_files: Dict[str, Path] = dict()
for suffix in suffix_file.readlines():
path = prefix / suffix.strip()
_LOG.debug('Searching for wheels in %s', path)
if path == out_dir:
continue
for wheel in path.glob('**/*.whl'):
+ if wheel.name in copied_files:
+ _LOG.error(
+ 'Attempting to override %s with %s',
+ copied_files[wheel.name],
+ wheel,
+ )
+ raise FileExistsError(
+ f'{wheel.name} conflict: '
+ f'{copied_files[wheel.name]} and {wheel}'
+ )
+ copied_files[wheel.name] = wheel
_LOG.debug('Copying %s to %s', wheel, out_dir)
shutil.copy(wheel, out_dir)
diff --git a/pw_build/py/pw_build/copy_from_cipd.py b/pw_build/py/pw_build/copy_from_cipd.py
index 82e393c14..7d868fbb9 100755
--- a/pw_build/py/pw_build/copy_from_cipd.py
+++ b/pw_build/py/pw_build/copy_from_cipd.py
@@ -53,49 +53,55 @@ logger = logging.getLogger(__name__)
def parse_args():
+ """Parse arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--verbose',
- '-v',
- help='Verbose output',
- action='store_true')
- parser.add_argument('--manifest',
- required=True,
- type=Path,
- help='Path to CIPD JSON manifest file')
- parser.add_argument('--out-dir',
- type=Path,
- default='.',
- help='Output folder to copy the specified file to')
- parser.add_argument('--package-name',
- required=True,
- help='The CIPD package name')
+ parser.add_argument(
+ '--verbose', '-v', help='Verbose output', action='store_true'
+ )
+ parser.add_argument(
+ '--manifest',
+ required=True,
+ type=Path,
+ help='Path to CIPD JSON manifest file',
+ )
+ parser.add_argument(
+ '--out-dir',
+ type=Path,
+ default='.',
+ help='Output folder to copy the specified file to',
+ )
+ parser.add_argument(
+ '--package-name', required=True, help='The CIPD package name'
+ )
# TODO(pwbug/334) Support multiple values for --file.
- parser.add_argument('--file',
- required=True,
- type=Path,
- help='Path of the file to copy from the CIPD package. '
- 'This is relative to the CIPD package root of the '
- 'provided manifest.')
- parser.add_argument('--cipd-package-root',
- type=Path,
- help="Path to the root of the package's install "
- 'directory. This is usually at '
- 'PW_{manifest name}_CIPD_INSTALL_DIR')
+ parser.add_argument(
+ '--file',
+ required=True,
+ type=Path,
+ help='Path of the file to copy from the CIPD package. '
+ 'This is relative to the CIPD package root of the '
+ 'provided manifest.',
+ )
+ parser.add_argument(
+ '--cipd-package-root',
+ type=Path,
+ help="Path to the root of the package's install "
+ 'directory. This is usually at '
+ 'PW_{manifest name}_CIPD_INSTALL_DIR',
+ )
return parser.parse_args()
def check_version(manifest, cipd_path, package_name):
base_package_name = os.path.basename(package_name)
- instance_id_path = os.path.join(cipd_path, '.versions',
- f'{base_package_name}.cipd_version')
+ instance_id_path = os.path.join(
+ cipd_path, '.versions', f'{base_package_name}.cipd_version'
+ )
with open(instance_id_path, 'r') as ins:
instance_id = json.load(ins)['instance_id']
with open(manifest, 'r') as ins:
- data = json.load(ins)
- # TODO(pwbug/599) Always assume this is a dict.
- if isinstance(data, dict):
- data = data['packages']
+ data = json.load(ins)['packages']
path = None
expected_version = None
@@ -110,8 +116,11 @@ def check_version(manifest, cipd_path, package_name):
output = subprocess.check_output(cmd).decode()
if expected_version not in output:
pw_env_setup.cipd_setup.update.update(
- 'cipd', (manifest, ), os.environ['PW_CIPD_INSTALL_DIR'],
- os.environ['CIPD_CACHE_DIR'])
+ 'cipd',
+ (manifest,),
+ os.environ['PW_CIPD_INSTALL_DIR'],
+ os.environ['CIPD_CACHE_DIR'],
+ )
def main():
@@ -130,13 +139,18 @@ def main():
logger.error(
"The %s environment variable isn't set. Did you forget to run "
'`. ./bootstrap.sh`? Is the %s manifest installed to a '
- 'different path?', args.cipd_var, file_base_name)
+ 'different path?',
+ args.cipd_var,
+ file_base_name,
+ )
sys.exit(1)
check_version(args.manifest, args.cipd_package_root, args.package_name)
- shutil.copyfile(os.path.join(args.cipd_package_root, args.file),
- os.path.join(args.out_dir, args.file))
+ shutil.copyfile(
+ os.path.join(args.cipd_package_root, args.file),
+ os.path.join(args.out_dir, args.file),
+ )
if __name__ == '__main__':
diff --git a/pw_build/py/pw_build/create_gn_venv.py b/pw_build/py/pw_build/create_gn_venv.py
new file mode 100644
index 000000000..e31e0ec2f
--- /dev/null
+++ b/pw_build/py/pw_build/create_gn_venv.py
@@ -0,0 +1,38 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Crate a venv."""
+
+import argparse
+import venv
+from pathlib import Path
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--destination-dir',
+ type=Path,
+ required=True,
+ help='Path to venv directory.',
+ )
+ return parser.parse_args()
+
+
+def main(destination_dir: Path) -> None:
+ if not destination_dir.is_dir():
+ venv.create(destination_dir, symlinks=True, with_pip=True)
+
+
+if __name__ == '__main__':
+ main(**vars(_parse_args()))
diff --git a/pw_build/py/pw_build/create_python_tree.py b/pw_build/py/pw_build/create_python_tree.py
index 90ddee52a..981f12d5d 100644
--- a/pw_build/py/pw_build/create_python_tree.py
+++ b/pw_build/py/pw_build/create_python_tree.py
@@ -22,113 +22,173 @@ import re
import shutil
import subprocess
import tempfile
-from typing import Iterable
+from typing import Iterable, Optional
-from pw_build.python_package import PythonPackage, load_packages
+import setuptools # type: ignore
+
+try:
+ from pw_build.python_package import (
+ PythonPackage,
+ load_packages,
+ change_working_dir,
+ )
+ from pw_build.generate_python_package import PYPROJECT_FILE
+
+except ImportError:
+ # Load from python_package from this directory if pw_build is not available.
+ from python_package import ( # type: ignore
+ PythonPackage,
+ load_packages,
+ change_working_dir,
+ )
+ from generate_python_package import PYPROJECT_FILE # type: ignore
def _parse_args():
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--tree-destination-dir',
- type=Path,
- help='Path to output directory.')
- parser.add_argument('--include-tests',
- action='store_true',
- help='Include tests in the tests dir.')
-
- parser.add_argument('--setupcfg-common-file',
- type=Path,
- help='A file containing the common set of options for'
- 'incluing in the merged setup.cfg provided version.')
- parser.add_argument('--setupcfg-version-append-git-sha',
- action='store_true',
- help='Append the current git SHA to the setup.cfg '
- 'version.')
- parser.add_argument('--setupcfg-version-append-date',
- action='store_true',
- help='Append the current date to the setup.cfg '
- 'version.')
+ parser.add_argument(
+ '--repo-root',
+ type=Path,
+ help='Path to the root git repo.',
+ )
+ parser.add_argument(
+ '--tree-destination-dir', type=Path, help='Path to output directory.'
+ )
+ parser.add_argument(
+ '--include-tests',
+ action='store_true',
+ help='Include tests in the tests dir.',
+ )
+
+ parser.add_argument(
+ '--setupcfg-common-file',
+ type=Path,
+ help='A file containing the common set of options for'
+ 'incluing in the merged setup.cfg provided version.',
+ )
+ parser.add_argument(
+ '--setupcfg-version-append-git-sha',
+ action='store_true',
+ help='Append the current git SHA to the setup.cfg ' 'version.',
+ )
+ parser.add_argument(
+ '--setupcfg-version-append-date',
+ action='store_true',
+ help='Append the current date to the setup.cfg ' 'version.',
+ )
+ parser.add_argument(
+ '--setupcfg-override-name', help='Override metadata.name in setup.cfg'
+ )
+ parser.add_argument(
+ '--setupcfg-override-version',
+ help='Override metadata.version in setup.cfg',
+ )
+ parser.add_argument(
+ '--create-default-pyproject-toml',
+ action='store_true',
+ help='Generate a default pyproject.toml file',
+ )
parser.add_argument(
'--extra-files',
nargs='+',
- help='Paths to extra files that should be included in the output dir.')
+ help='Paths to extra files that should be included in the output dir.',
+ )
parser.add_argument(
'--input-list-files',
nargs='+',
type=Path,
help='Paths to text files containing lists of Python package metadata '
- 'json files.')
+ 'json files.',
+ )
return parser.parse_args()
class UnknownGitSha(Exception):
- "Exception thrown when the current git SHA cannot be found."
-
-
-def get_current_git_sha() -> str:
- git_command = 'git log -1 --pretty=format:%h'
- process = subprocess.run(git_command.split(),
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ """Exception thrown when the current git SHA cannot be found."""
+
+
+def get_current_git_sha(repo_root: Optional[Path] = None) -> str:
+ if not repo_root:
+ repo_root = Path.cwd()
+ git_command = [
+ 'git',
+ '-C',
+ str(repo_root),
+ 'log',
+ '-1',
+ '--pretty=format:%h',
+ ]
+
+ process = subprocess.run(
+ git_command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ )
gitsha = process.stdout.decode()
if process.returncode != 0 or not gitsha:
error_output = f'\n"{git_command}" failed with:' f'\n{gitsha}'
if process.stderr:
error_output += f'\n{process.stderr.decode()}'
- raise UnknownGitSha('Could not determine the current git SHA.' +
- error_output)
+ raise UnknownGitSha(
+ 'Could not determine the current git SHA.' + error_output
+ )
return gitsha.strip()
def get_current_date() -> str:
- return datetime.now().strftime('%Y%m%d%H%M')
+ return datetime.now().strftime('%Y%m%d%H%M%S')
class UnexpectedConfigSection(Exception):
- "Exception thrown when the common configparser contains unexpected values."
+ """Exception thrown when the common config contains unexpected values."""
-def load_common_config(common_config: Path,
- append_git_sha: bool = False,
- append_date: bool = False) -> configparser.ConfigParser:
+def load_common_config(
+ common_config: Optional[Path] = None,
+ package_name_override: Optional[str] = None,
+ package_version_override: Optional[str] = None,
+ append_git_sha: bool = False,
+ append_date: bool = False,
+ repo_root: Optional[Path] = None,
+) -> configparser.ConfigParser:
"""Load an existing ConfigParser file and update metadata.version."""
config = configparser.ConfigParser()
- config.read(common_config)
+ if common_config:
+ config.read(common_config)
+
+ # Metadata and option sections need to exist.
+ if not config.has_section('metadata'):
+ config['metadata'] = {}
+ if not config.has_section('options'):
+ config['options'] = {}
+
+ if package_name_override:
+ config['metadata']['name'] = package_name_override
+ if package_version_override:
+ config['metadata']['version'] = package_version_override
# Check for existing values that should not be present
if config.has_option('options', 'packages'):
value = str(config['options']['packages'])
raise UnexpectedConfigSection(
- f'[options] packages already defined as: {value}')
-
- if config.has_section('options.package_data'):
- raise UnexpectedConfigSection(
- '[options.package_data] already defined as:\n' +
- str(dict(config['options.package_data'].items())))
-
- if config.has_section('options.entry_points'):
- raise UnexpectedConfigSection(
- '[options.entry_points] already defined as:\n' +
- str(dict(config['options.entry_points'].items())))
-
- # Metadata and option sections should already be defined.
- assert config.has_section('metadata')
- assert config.has_section('options')
+ f'[options] packages already defined as: {value}'
+ )
# Append build metadata if applicable.
build_metadata = []
if append_date:
build_metadata.append(get_current_date())
if append_git_sha:
- build_metadata.append(get_current_git_sha())
+ build_metadata.append(get_current_git_sha(repo_root))
if build_metadata:
version_prefix = config['metadata']['version']
build_metadata_text = '.'.join(build_metadata)
- config['metadata']['version'] = (
- f'{version_prefix}+{build_metadata_text}')
+ config['metadata'][
+ 'version'
+ ] = f'{version_prefix}+{build_metadata_text}'
return config
@@ -138,26 +198,29 @@ def update_config_with_packages(
) -> None:
"""Merge setup.cfg files from a set of python packages."""
config['options']['packages'] = 'find:'
- config['options.package_data'] = {}
- config['options.entry_points'] = {}
+ if not config.has_section('options.package_data'):
+ config['options.package_data'] = {}
+ if not config.has_section('options.entry_points'):
+ config['options.entry_points'] = {}
# Save a list of packages being bundled.
included_packages = [pkg.package_name for pkg in python_packages]
for pkg in python_packages:
- assert pkg.config
+ # Skip this package if no setup.cfg is defined.
+ if not pkg.config:
+ continue
# Collect install_requires
if pkg.config.has_option('options', 'install_requires'):
existing_requires = config['options'].get('install_requires', '\n')
- # Requires are delimited by newlines or semicolons.
- # Split existing list on either one.
- this_requires = re.split(r' *[\n;] *',
- pkg.config['options']['install_requires'])
- new_requires = existing_requires.splitlines() + this_requires
+
+ new_requires = existing_requires.splitlines()
+ new_requires += pkg.install_requires_entries()
# Remove requires already included in this merged config.
new_requires = [
- line for line in new_requires
+ line
+ for line in new_requires
if line and line not in included_packages
]
# Remove duplictes and sort require list.
@@ -167,13 +230,22 @@ def update_config_with_packages(
# Collect package_data
if pkg.config.has_section('options.package_data'):
for key, value in pkg.config['options.package_data'].items():
- config['options.package_data'][key] = value
+ existing_values = (
+ config['options.package_data'].get(key, '').splitlines()
+ )
+ new_value = '\n'.join(
+ sorted(set(existing_values + value.splitlines()))
+ )
+ # Remove any empty lines
+ new_value = new_value.replace('\n\n', '\n')
+ config['options.package_data'][key] = new_value
# Collect entry_points
if pkg.config.has_section('options.entry_points'):
for key, value in pkg.config['options.entry_points'].items():
existing_entry_points = config['options.entry_points'].get(
- key, '')
+ key, ''
+ )
new_entry_points = '\n'.join([existing_entry_points, value])
# Remove any empty lines
new_entry_points = new_entry_points.replace('\n\n', '\n')
@@ -181,17 +253,19 @@ def update_config_with_packages(
def write_config(
- common_config: Path,
final_config: configparser.ConfigParser,
tree_destination_dir: Path,
+ common_config: Optional[Path] = None,
) -> None:
"""Write a the final setup.cfg file with license comment block."""
- # Get the license comment block from the common_config.
comment_block_text = ''
- comment_block_match = re.search(r'((^#.*?[\r\n])*)([^#])',
- common_config.read_text(), re.MULTILINE)
- if comment_block_match:
- comment_block_text = comment_block_match.group(1)
+ if common_config:
+ # Get the license comment block from the common_config.
+ comment_block_match = re.search(
+ r'((^#.*?[\r\n])*)([^#])', common_config.read_text(), re.MULTILINE
+ )
+ if comment_block_match:
+ comment_block_text = comment_block_match.group(1)
setup_cfg_file = tree_destination_dir.resolve() / 'setup.cfg'
setup_cfg_text = io.StringIO()
@@ -199,9 +273,53 @@ def write_config(
setup_cfg_file.write_text(comment_block_text + setup_cfg_text.getvalue())
-def build_python_tree(python_packages: Iterable[PythonPackage],
- tree_destination_dir: Path,
- include_tests: bool = False) -> None:
+def setuptools_build_with_base(
+ pkg: PythonPackage, build_base: Path, include_tests: bool = False
+) -> Path:
+ """Run setuptools build for this package."""
+
+ # If there is no setup_dir or setup_sources, just copy this packages
+ # source files.
+ if not pkg.setup_dir:
+ pkg.copy_sources_to(build_base)
+ return build_base
+ # Create the lib install dir in case it doesn't exist.
+ lib_dir_path = build_base / 'lib'
+ lib_dir_path.mkdir(parents=True, exist_ok=True)
+
+ starting_directory = Path.cwd()
+ # cd to the location of setup.py
+ with change_working_dir(pkg.setup_dir):
+ # Run build with temp build-base location
+ # Note: New files will be placed inside lib_dir_path
+ setuptools.setup(
+ script_args=[
+ 'build',
+ '--force',
+ '--build-base',
+ str(build_base),
+ ]
+ )
+
+ new_pkg_dir = lib_dir_path / pkg.package_name
+ # If tests should be included, copy them to the tests dir
+ if include_tests and pkg.tests:
+ test_dir_path = new_pkg_dir / 'tests'
+ test_dir_path.mkdir(parents=True, exist_ok=True)
+
+ for test_source_path in pkg.tests:
+ shutil.copy(
+ starting_directory / test_source_path, test_dir_path
+ )
+
+ return lib_dir_path
+
+
+def build_python_tree(
+ python_packages: Iterable[PythonPackage],
+ tree_destination_dir: Path,
+ include_tests: bool = False,
+) -> None:
"""Install PythonPackages to a destination directory."""
# Create the root destination directory.
@@ -210,21 +328,18 @@ def build_python_tree(python_packages: Iterable[PythonPackage],
shutil.rmtree(destination_path, ignore_errors=True)
destination_path.mkdir(exist_ok=True)
- # Define a temporary location to run setup.py build in.
- with tempfile.TemporaryDirectory() as build_base_name:
- build_base = Path(build_base_name)
+ for pkg in python_packages:
+ # Define a temporary location to run setup.py build in.
+ with tempfile.TemporaryDirectory() as build_base_name:
+ build_base = Path(build_base_name)
- for pkg in python_packages:
- lib_dir_path = pkg.setuptools_build_with_base(
- build_base, include_tests=include_tests)
+ lib_dir_path = setuptools_build_with_base(
+ pkg, build_base, include_tests=include_tests
+ )
# Move installed files from the temp build-base into
# destination_path.
- for new_file in lib_dir_path.glob('*'):
- # Use str(Path) since shutil.move only accepts path-like objects
- # in Python 3.9 and up:
- # https://docs.python.org/3/library/shutil.html#shutil.move
- shutil.move(str(new_file), str(destination_path))
+ shutil.copytree(lib_dir_path, destination_path, dirs_exist_ok=True)
# Clean build base lib folder for next install
shutil.rmtree(lib_dir_path, ignore_errors=True)
@@ -242,41 +357,62 @@ def copy_extra_files(extra_file_strings: Iterable[str]) -> None:
dest_file = Path(input_output[1])
if not source_file.exists():
- raise FileNotFoundError(f'extra_file "{source_file}" not found.\n'
- f' Defined by: "{extra_file_string}"')
+ raise FileNotFoundError(
+ f'extra_file "{source_file}" not found.\n'
+ f' Defined by: "{extra_file_string}"'
+ )
# Copy files and make parent directories.
dest_file.parent.mkdir(parents=True, exist_ok=True)
# Raise an error if the destination file already exists.
if dest_file.exists():
raise FileExistsError(
- f'Copying "{source_file}" would overwrite "{dest_file}"')
+ f'Copying "{source_file}" would overwrite "{dest_file}"'
+ )
shutil.copy(source_file, dest_file)
-def main():
+def _main():
args = _parse_args()
+ # Check the common_config file exists if provided.
+ if args.setupcfg_common_file:
+ assert args.setupcfg_common_file.is_file()
+
py_packages = load_packages(args.input_list_files)
- build_python_tree(python_packages=py_packages,
- tree_destination_dir=args.tree_destination_dir,
- include_tests=args.include_tests)
+ build_python_tree(
+ python_packages=py_packages,
+ tree_destination_dir=args.tree_destination_dir,
+ include_tests=args.include_tests,
+ )
copy_extra_files(args.extra_files)
- if args.setupcfg_common_file:
+ if args.create_default_pyproject_toml:
+ pyproject_path = args.tree_destination_dir / 'pyproject.toml'
+ pyproject_path.write_text(PYPROJECT_FILE)
+
+ if args.setupcfg_common_file or (
+ args.setupcfg_override_name and args.setupcfg_override_version
+ ):
config = load_common_config(
common_config=args.setupcfg_common_file,
+ package_name_override=args.setupcfg_override_name,
+ package_version_override=args.setupcfg_override_version,
append_git_sha=args.setupcfg_version_append_git_sha,
- append_date=args.setupcfg_version_append_date)
+ append_date=args.setupcfg_version_append_date,
+ repo_root=args.repo_root,
+ )
update_config_with_packages(config=config, python_packages=py_packages)
- write_config(common_config=args.setupcfg_common_file,
- final_config=config,
- tree_destination_dir=args.tree_destination_dir)
+ write_config(
+ common_config=args.setupcfg_common_file,
+ final_config=config,
+ tree_destination_dir=args.tree_destination_dir,
+ )
if __name__ == '__main__':
- main()
+ _main()
diff --git a/pw_build/py/pw_build/error.py b/pw_build/py/pw_build/error.py
index 9d0f41e12..9299aa273 100644
--- a/pw_build/py/pw_build/error.py
+++ b/pw_build/py/pw_build/error.py
@@ -30,12 +30,12 @@ _LOG = logging.getLogger(__name__)
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--message',
- required=True,
- help='Error message to print')
- parser.add_argument('--target',
- required=True,
- help='GN target in which the error occurred')
+ parser.add_argument(
+ '--message', required=True, help='Error message to print'
+ )
+ parser.add_argument(
+ '--target', required=True, help='GN target in which the error occurred'
+ )
parser.add_argument('--root', required=True, type=Path, help='GN root')
parser.add_argument('--out', required=True, type=Path, help='GN out dir')
return parser.parse_args()
@@ -55,7 +55,8 @@ def main(message: str, target: str, root: Path, out: Path) -> int:
gn_cmd = subprocess.run(
['gn', 'path', f'--root={root}', out, '//:default', target],
- capture_output=True)
+ capture_output=True,
+ )
path_info = gn_cmd.stdout.decode(errors='replace').rstrip()
relative_out = os.path.relpath(out, root)
@@ -63,8 +64,9 @@ def main(message: str, target: str, root: Path, out: Path) -> int:
if gn_cmd.returncode == 0 and 'No non-data paths found' not in path_info:
_LOG.error('Dependency path to this target:')
_LOG.error('')
- _LOG.error(' gn path %s //:default "%s"\n%s', relative_out, target,
- path_info)
+ _LOG.error(
+ ' gn path %s //:default "%s"\n%s', relative_out, target, path_info
+ )
_LOG.error('')
else:
_LOG.error(
diff --git a/pw_build/py/pw_build/exec.py b/pw_build/py/pw_build/exec.py
index aac800baa..6e367d7eb 100644
--- a/pw_build/py/pw_build/exec.py
+++ b/pw_build/py/pw_build/exec.py
@@ -33,7 +33,7 @@ _LOG = logging.getLogger(__name__)
def argument_parser(
- parser: Optional[argparse.ArgumentParser] = None
+ parser: Optional[argparse.ArgumentParser] = None,
) -> argparse.ArgumentParser:
"""Registers the script's arguments on an argument parser."""
@@ -71,9 +71,11 @@ def argument_parser(
'--target',
help='GN build target that runs the program',
)
- parser.add_argument('--working-directory',
- type=pathlib.Path,
- help='Directory to execute program in')
+ parser.add_argument(
+ '--working-directory',
+ type=pathlib.Path,
+ help='Directory to execute program in',
+ )
parser.add_argument(
'command',
nargs=argparse.REMAINDER,
@@ -117,6 +119,18 @@ def main() -> int:
# Command starts after the "--".
command = args.command[1:]
+ # command[0] is the invoker.prog from gn and gn will escape
+ # the various spaces in the command which means when argparse
+ # gets the string argparse believes this as a single argument
+ # and cannot correctly break the string into a list that
+ # subprocess can handle. By splitting the first element
+ # in the command list, if there is a space, all of the
+ # command[0] elements will be made into a list and if not
+ # then split won't do everything and the old behavior
+ # will continue.
+ front_command = command[0].split(' ')
+ del command[0]
+ command = front_command + command
extra_kw_args = {}
if args.args_file is not None:
@@ -147,8 +161,9 @@ def main() -> int:
if process.returncode != 0 and args.capture_output:
_LOG.error('')
- _LOG.error('Command failed with exit code %d in GN build.',
- process.returncode)
+ _LOG.error(
+ 'Command failed with exit code %d in GN build.', process.returncode
+ )
_LOG.error('')
_LOG.error('Build target:')
_LOG.error('')
diff --git a/pw_build/py/pw_build/file_prefix_map.py b/pw_build/py/pw_build/file_prefix_map.py
index 69daff268..5d6cfadb8 100644
--- a/pw_build/py/pw_build/file_prefix_map.py
+++ b/pw_build/py/pw_build/file_prefix_map.py
@@ -26,25 +26,31 @@ def _parse_args() -> argparse.Namespace:
"""Parses and returns the command line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('in_json',
- type=argparse.FileType('r'),
- help='The JSON file containing a list of file names '
- 'that the prefix map operations should be applied to')
+ parser.add_argument(
+ 'in_json',
+ type=argparse.FileType('r'),
+ help='The JSON file containing a list of file names '
+ 'that the prefix map operations should be applied to',
+ )
parser.add_argument(
'--prefix-map-json',
type=argparse.FileType('r'),
required=True,
- help=
- 'JSON file containing an array of prefix map transformations to apply '
- 'to the strings before tokenizing. These string literal '
- 'transformations are of the form "from=to". All strings with the '
- 'prefix `from` will have the prefix replaced with `to`. '
- 'Transformations are applied in the order they are listed in the JSON '
- 'file.')
+ help=(
+ 'JSON file containing an array of prefix map transformations to '
+ 'apply to the strings before tokenizing. These string literal '
+ 'transformations are of the form "from=to". All strings with the '
+ 'prefix `from` will have the prefix replaced with `to`. '
+ 'Transformations are applied in the order they are listed in the '
+ 'JSON file.'
+ ),
+ )
- parser.add_argument('--output',
- type=argparse.FileType('w'),
- help='File path to write transformed paths to.')
+ parser.add_argument(
+ '--output',
+ type=argparse.FileType('w'),
+ help='File path to write transformed paths to.',
+ )
return parser.parse_args()
@@ -56,8 +62,9 @@ def remap_paths(paths: List[str], prefix_maps: PrefixMaps) -> Iterator[str]:
yield path
-def remap_json_paths(in_json: TextIO, output: TextIO,
- prefix_map_json: TextIO) -> None:
+def remap_json_paths(
+ in_json: TextIO, output: TextIO, prefix_map_json: TextIO
+) -> None:
paths = json.load(in_json)
prefix_maps: PrefixMaps = [
tuple(m.split('=', maxsplit=1)) for m in json.load(prefix_map_json)
diff --git a/pw_build/py/pw_build/generate_cc_blob_library.py b/pw_build/py/pw_build/generate_cc_blob_library.py
index 551701eb5..65b799eaa 100644
--- a/pw_build/py/pw_build/generate_cc_blob_library.py
+++ b/pw_build/py/pw_build/generate_cc_blob_library.py
@@ -19,22 +19,42 @@ import json
from pathlib import Path
from string import Template
import textwrap
-from typing import Any, Generator, Iterable, NamedTuple, Optional, Tuple
-
-HEADER_PREFIX = textwrap.dedent("""\
- // This file is auto-generated; Do not hand-modify!
- // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
-
+from typing import (
+ Any,
+ Generator,
+ Iterable,
+ NamedTuple,
+ Optional,
+ Sequence,
+ Tuple,
+)
+
+COMMENT = f"""\
+// This file was generated by {Path(__file__).name}.
+//
+// DO NOT EDIT!
+//
+// This file contains declarations for byte arrays created from files during the
+// build. The byte arrays are constant initialized and are safe to access at any
+// time, including before main().
+//
+// See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
+"""
+
+HEADER_PREFIX = COMMENT + textwrap.dedent(
+ """\
#pragma once
#include <array>
#include <cstddef>
- """)
+
+ """
+)
SOURCE_PREFIX_TEMPLATE = Template(
- textwrap.dedent("""\
- // This file is auto-generated; Do not hand-modify!
- // See https://pigweed.dev/pw_build/#pw-cc-blob-library for details.
+ COMMENT
+ + textwrap.dedent(
+ """\
#include "$header_path"
@@ -42,24 +62,26 @@ SOURCE_PREFIX_TEMPLATE = Template(
#include <cstddef>
#include "pw_preprocessor/compiler.h"
- """))
+
+ """
+ )
+)
NAMESPACE_OPEN_TEMPLATE = Template('namespace ${namespace} {\n')
-NAMESPACE_CLOSE_TEMPLATE = Template('} // namespace ${namespace}\n')
+NAMESPACE_CLOSE_TEMPLATE = Template('\n} // namespace ${namespace}\n')
BLOB_DECLARATION_TEMPLATE = Template(
- 'extern const std::array<std::byte, ${size_bytes}> ${symbol_name};')
-
-LINKER_SECTION_TEMPLATE = Template('PW_PLACE_IN_SECTION("${linker_section}")')
+ '\nextern const std::array<std::byte, ${size_bytes}> ${symbol_name};\n'
+)
-BLOB_DEFINITION_SINGLE_LINE = Template(
- 'const std::array<std::byte, ${size_bytes}> ${symbol_name}'
- ' = { $bytes };')
+LINKER_SECTION_TEMPLATE = Template('PW_PLACE_IN_SECTION("${linker_section}")\n')
BLOB_DEFINITION_MULTI_LINE = Template(
- 'const std::array<std::byte, ${size_bytes}> ${symbol_name}'
- ' = {\n${bytes_lines}\n};')
+ '\n${section_attr}'
+ '${alignas}constexpr std::array<std::byte, ${size_bytes}> ${symbol_name}'
+ ' = {\n${bytes_lines}\n};\n'
+)
BYTES_PER_LINE = 4
@@ -68,35 +90,57 @@ class Blob(NamedTuple):
symbol_name: str
file_path: Path
linker_section: Optional[str]
+ alignas: Optional[str] = None
+
+ @staticmethod
+ def from_dict(blob_dict: dict) -> 'Blob':
+ return Blob(
+ blob_dict['symbol_name'],
+ Path(blob_dict['file_path']),
+ blob_dict.get('linker_section'),
+ blob_dict.get('alignas'),
+ )
-def parse_args():
+def parse_args() -> dict:
+ """Parse arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--blob-file',
- type=Path,
- required=True,
- help=('Path to json file containing the list of blobs '
- 'to generate.'))
- parser.add_argument('--out-source',
- type=Path,
- required=True,
- help='Path at which to write .cpp file')
- parser.add_argument('--out-header',
- type=Path,
- required=True,
- help='Path at which to write .h file')
- parser.add_argument('--namespace',
- type=str,
- required=False,
- help=('Optional namespace that blobs will be scoped'
- 'within.'))
-
- return parser.parse_args()
+ parser.add_argument(
+ '--blob-file',
+ type=Path,
+ required=True,
+ help=('Path to json file containing the list of blobs ' 'to generate.'),
+ )
+ parser.add_argument(
+ '--header-include',
+ required=True,
+ help='Path to use in #includes for the header',
+ )
+ parser.add_argument(
+ '--out-source',
+ type=Path,
+ required=True,
+ help='Path at which to write .cpp file',
+ )
+ parser.add_argument(
+ '--out-header',
+ type=Path,
+ required=True,
+ help='Path at which to write .h file',
+ )
+ parser.add_argument(
+ '--namespace',
+ type=str,
+ required=False,
+ help=('Optional namespace that blobs will be scoped' 'within.'),
+ )
+
+ return vars(parser.parse_args())
def split_into_chunks(
- data: Iterable[Any],
- chunk_size: int) -> Generator[Tuple[Any, ...], None, None]:
+ data: Iterable[Any], chunk_size: int
+) -> Generator[Tuple[Any, ...], None, None]:
"""Splits an iterable into chunks of a given size."""
data_iterator = iter(data)
chunk = tuple(itertools.islice(data_iterator, chunk_size))
@@ -105,8 +149,9 @@ def split_into_chunks(
chunk = tuple(itertools.islice(data_iterator, chunk_size))
-def header_from_blobs(blobs: Iterable[Blob],
- namespace: Optional[str] = None) -> str:
+def header_from_blobs(
+ blobs: Iterable[Blob], namespace: Optional[str] = None
+) -> str:
"""Generate the contents of a C++ header file from blobs."""
lines = [HEADER_PREFIX]
if namespace:
@@ -114,77 +159,77 @@ def header_from_blobs(blobs: Iterable[Blob],
for blob in blobs:
data = blob.file_path.read_bytes()
lines.append(
- BLOB_DECLARATION_TEMPLATE.substitute(symbol_name=blob.symbol_name,
- size_bytes=len(data)))
- lines.append('')
+ BLOB_DECLARATION_TEMPLATE.substitute(
+ symbol_name=blob.symbol_name, size_bytes=len(data)
+ )
+ )
if namespace:
lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace))
- return '\n'.join(lines)
+ return ''.join(lines)
-def array_def_from_blob_data(symbol_name: str, blob_data: bytes) -> str:
+def array_def_from_blob_data(blob: Blob, blob_data: bytes) -> str:
"""Generates an array definition for the given blob data."""
- byte_strs = ['std::byte{{0x{:02X}}}'.format(b) for b in blob_data]
+ if blob.linker_section:
+ section_attr = LINKER_SECTION_TEMPLATE.substitute(
+ linker_section=blob.linker_section
+ )
+ else:
+ section_attr = ''
- # Try to fit the blob definition on a single line first.
- single_line_def = BLOB_DEFINITION_SINGLE_LINE.substitute(
- symbol_name=symbol_name,
- size_bytes=len(blob_data),
- bytes=', '.join(byte_strs))
- if len(single_line_def) <= 80:
- return single_line_def
+ byte_strs = ['std::byte{{0x{:02X}}}'.format(b) for b in blob_data]
- # Blob definition didn't fit on a single line; do multi-line.
lines = []
for byte_strs_for_line in split_into_chunks(byte_strs, BYTES_PER_LINE):
bytes_segment = ', '.join(byte_strs_for_line)
- lines.append(f' {bytes_segment},')
- # Removing the trailing comma from the final line of bytes.
- lines[-1] = lines[-1].rstrip(',')
+ lines.append(f' {bytes_segment},')
- return BLOB_DEFINITION_MULTI_LINE.substitute(symbol_name=symbol_name,
- size_bytes=len(blob_data),
- bytes_lines='\n'.join(lines))
+ return BLOB_DEFINITION_MULTI_LINE.substitute(
+ section_attr=section_attr,
+ alignas=f'alignas({blob.alignas}) ' if blob.alignas else '',
+ symbol_name=blob.symbol_name,
+ size_bytes=len(blob_data),
+ bytes_lines='\n'.join(lines),
+ )
-def source_from_blobs(blobs: Iterable[Blob],
- header_path: Path,
- namespace: Optional[str] = None) -> str:
+def source_from_blobs(
+ blobs: Iterable[Blob], header_path: str, namespace: Optional[str] = None
+) -> str:
"""Generate the contents of a C++ source file from blobs."""
lines = [SOURCE_PREFIX_TEMPLATE.substitute(header_path=header_path)]
if namespace:
lines.append(NAMESPACE_OPEN_TEMPLATE.substitute(namespace=namespace))
for blob in blobs:
- if blob.linker_section:
- lines.append(
- LINKER_SECTION_TEMPLATE.substitute(
- linker_section=blob.linker_section))
data = blob.file_path.read_bytes()
- lines.append(array_def_from_blob_data(blob.symbol_name, data))
- lines.append('')
+ lines.append(array_def_from_blob_data(blob, data))
if namespace:
lines.append(NAMESPACE_CLOSE_TEMPLATE.substitute(namespace=namespace))
- return '\n'.join(lines)
+ return ''.join(lines)
-def load_blobs(blob_file: Path) -> Iterable[Blob]:
+def load_blobs(blob_file: Path) -> Sequence[Blob]:
with blob_file.open() as blob_fp:
- return [
- Blob(b["symbol_name"], Path(b["file_path"]),
- b.get("linker_section")) for b in json.load(blob_fp)
- ]
+ return [Blob.from_dict(blob) for blob in json.load(blob_fp)]
-def main(blob_file: Path,
- out_source: Path,
- out_header: Path,
- namespace: Optional[str] = None) -> None:
+def main(
+ blob_file: Path,
+ header_include: str,
+ out_source: Path,
+ out_header: Path,
+ namespace: Optional[str] = None,
+) -> None:
blobs = load_blobs(blob_file)
+
+ out_header.parent.mkdir(parents=True, exist_ok=True)
out_header.write_text(header_from_blobs(blobs, namespace))
- out_source.write_text(source_from_blobs(blobs, out_header, namespace))
+
+ out_source.parent.mkdir(parents=True, exist_ok=True)
+ out_source.write_text(source_from_blobs(blobs, header_include, namespace))
if __name__ == '__main__':
- main(**vars(parse_args()))
+ main(**parse_args())
diff --git a/pw_build/py/pw_build/generate_modules_lists.py b/pw_build/py/pw_build/generate_modules_lists.py
index 7026b229f..ae9eb9505 100644
--- a/pw_build/py/pw_build/generate_modules_lists.py
+++ b/pw_build/py/pw_build/generate_modules_lists.py
@@ -23,12 +23,13 @@ Used by modules.gni to generate:
import argparse
import difflib
+import enum
import io
import os
from pathlib import Path
import sys
import subprocess
-from typing import Iterator, List, Optional, Sequence, Tuple
+from typing import Iterator, Optional, Sequence
_COPYRIGHT_NOTICE = '''\
# Copyright 2022 The Pigweed Authors
@@ -48,7 +49,9 @@ _COPYRIGHT_NOTICE = '''\
_WARNING = '\033[31m\033[1mWARNING:\033[0m ' # Red WARNING: prefix
_ERROR = '\033[41m\033[37m\033[1mERROR:\033[0m ' # Red background ERROR: prefix
-_MISSING_MODULES_WARNING = _WARNING + '''\
+_MISSING_MODULES_WARNING = (
+ _WARNING
+ + '''\
The PIGWEED_MODULES list is missing the following modules:
{modules}
@@ -57,8 +60,11 @@ If the listed modules are Pigweed modules, add them to PIGWEED_MODULES.
If the listed modules are not actual Pigweed modules, remove any stray pw_*
directories in the Pigweed repository (git clean -fd).
'''
+)
-_OUT_OF_DATE_WARNING = _ERROR + '''\
+_OUT_OF_DATE_WARNING = (
+ _ERROR
+ + '''\
The generated Pigweed modules list .gni file is out of date!
Regenerate the modules lists and commit it to fix this:
@@ -67,12 +73,16 @@ Regenerate the modules lists and commit it to fix this:
git add {file}
'''
+)
-_FORMAT_FAILED_WARNING = _ERROR + '''\
+_FORMAT_FAILED_WARNING = (
+ _ERROR
+ + '''\
Failed to generate a valid .gni from PIGWEED_MODULES!
This may be a Pigweed bug; please report this to the Pigweed team.
'''
+)
_DO_NOT_SET = 'DO NOT SET THIS BUILD ARGUMENT!'
@@ -80,46 +90,33 @@ _DO_NOT_SET = 'DO NOT SET THIS BUILD ARGUMENT!'
def _module_list_warnings(root: Path, modules: Sequence[str]) -> Iterator[str]:
missing = _missing_modules(root, modules)
if missing:
- yield _MISSING_MODULES_WARNING.format(modules=''.join(
- f'\n - {module}' for module in missing))
+ yield _MISSING_MODULES_WARNING.format(
+ modules=''.join(f'\n - {module}' for module in missing)
+ )
if any(modules[i] > modules[i + 1] for i in range(len(modules) - 1)):
yield _WARNING + 'The PIGWEED_MODULES list is not sorted!'
yield ''
yield 'Apply the following diff to fix the order:'
yield ''
- yield from difflib.unified_diff(modules,
- sorted(modules),
- lineterm='',
- n=1,
- fromfile='PIGWEED_MODULES',
- tofile='PIGWEED_MODULES')
+ yield from difflib.unified_diff(
+ modules,
+ sorted(modules),
+ lineterm='',
+ n=1,
+ fromfile='PIGWEED_MODULES',
+ tofile='PIGWEED_MODULES',
+ )
yield ''
-# TODO(hepler): Add tests and docs targets to all modules.
-def _find_tests_and_docs(
- root: Path, modules: Sequence[str]) -> Tuple[List[str], List[str]]:
- """Lists "tests" and "docs" targets for modules that declare them."""
- tests = []
- docs = []
-
- for module in modules:
- build_gn_contents = root.joinpath(module, 'BUILD.gn').read_bytes()
- if b'group("tests")' in build_gn_contents:
- tests.append(f'"$dir_{module}:tests",')
-
- if b'group("docs")' in build_gn_contents:
- docs.append(f'"$dir_{module}:docs",')
-
- return tests, docs
-
-
-def _generate_modules_gni(root: Path, prefix: Path,
- modules: Sequence[str]) -> Iterator[str]:
+def _generate_modules_gni(
+ prefix: Path, modules: Sequence[str]
+) -> Iterator[str]:
"""Generates a .gni file with variables and lists for Pigweed modules."""
- script = Path(__file__).resolve().relative_to(root.resolve()).as_posix()
+ script_path = Path(__file__).resolve()
+ script = script_path.relative_to(script_path.parent.parent).as_posix()
yield _COPYRIGHT_NOTICE
yield ''
@@ -157,16 +154,20 @@ def _generate_modules_gni(root: Path, prefix: Path,
yield ']'
yield ''
- tests, docs = _find_tests_and_docs(root, modules)
-
yield f'# A list with all Pigweed module test groups. {_DO_NOT_SET}'
yield 'pw_module_tests = ['
- yield from tests
+
+ for module in modules:
+ yield f'"$dir_{module}:tests",'
+
yield ']'
yield ''
yield f'# A list with all Pigweed modules docs groups. {_DO_NOT_SET}'
yield 'pw_module_docs = ['
- yield from docs
+
+ for module in modules:
+ yield f'"$dir_{module}:docs",'
+
yield ']'
yield ''
yield '}'
@@ -175,27 +176,45 @@ def _generate_modules_gni(root: Path, prefix: Path,
def _missing_modules(root: Path, modules: Sequence[str]) -> Sequence[str]:
return sorted(
frozenset(
- str(p.relative_to(root))
- for p in root.glob('pw_*') if p.is_dir()) - frozenset(modules))
+ str(p.relative_to(root)) for p in root.glob('pw_*') if p.is_dir()
+ )
+ - frozenset(modules)
+ )
+
+
+class Mode(enum.Enum):
+ WARN = 0 # Warn if anything is out of date
+ CHECK = 1 # Fail if anything is out of date
+ UPDATE = 2 # Update the generated modules lists
def _parse_args() -> dict:
parser = argparse.ArgumentParser(
description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
parser.add_argument('root', type=Path, help='Root build dir')
parser.add_argument('modules_list', type=Path, help='Input modules list')
parser.add_argument('modules_gni_file', type=Path, help='Output .gni file')
parser.add_argument(
- '--warn-only',
+ '--mode', type=Mode.__getitem__, choices=Mode, required=True
+ )
+ parser.add_argument(
+ '--stamp',
type=Path,
- help='Only check PIGWEED_MODULES; takes a path to a stamp file to use')
-
+ help='Stamp file for operations that should only run once (warn)',
+ )
return vars(parser.parse_args())
-def _main(root: Path, modules_list: Path, modules_gni_file: Path,
- warn_only: Optional[Path]) -> int:
+def main(
+ root: Path,
+ modules_list: Path,
+ modules_gni_file: Path,
+ mode: Mode,
+ stamp: Optional[Path] = None,
+) -> int:
+ """Manages the list of Pigweed modules."""
prefix = Path(os.path.relpath(root, modules_gni_file.parent))
modules = modules_list.read_text().splitlines()
@@ -206,28 +225,51 @@ def _main(root: Path, modules_list: Path, modules_gni_file: Path,
modules.sort() # Sort in case the modules list in case it wasn't sorted.
# Check if the contents of the .gni file are out of date.
- if warn_only:
+ if mode in (Mode.WARN, Mode.CHECK):
text = io.StringIO()
- for line in _generate_modules_gni(root, prefix, modules):
+ for line in _generate_modules_gni(prefix, modules):
print(line, file=text)
- process = subprocess.run(['gn', 'format', '--stdin'],
- input=text.getvalue().encode('utf-8'),
- stdout=subprocess.PIPE)
+ process = subprocess.run(
+ ['gn', 'format', '--stdin'],
+ input=text.getvalue().encode('utf-8'),
+ stdout=subprocess.PIPE,
+ )
if process.returncode != 0:
errors.append(_FORMAT_FAILED_WARNING)
- elif modules_gni_file.read_bytes() != process.stdout:
+
+ # Make a diff of required changes
+ modules_gni_relpath = os.path.relpath(modules_gni_file, root)
+ diff = list(
+ difflib.unified_diff(
+ modules_gni_file.read_text().splitlines(),
+ process.stdout.decode('utf-8', errors='replace').splitlines(),
+ fromfile=os.path.join('a', modules_gni_relpath),
+ tofile=os.path.join('b', modules_gni_relpath),
+ lineterm='',
+ n=1,
+ )
+ )
+ # If any differences were found, print the error and the diff.
+ if diff:
errors.append(
_OUT_OF_DATE_WARNING.format(
out_dir=os.path.relpath(os.curdir, root),
- file=os.path.relpath(modules_gni_file, root)))
- elif not warnings: # Update the modules .gni file.
+ file=os.path.relpath(modules_gni_file, root),
+ )
+ )
+ errors.append('Expected Diff:\n')
+ errors.append('\n'.join(diff))
+ errors.append('\n')
+
+ elif mode is Mode.UPDATE: # Update the modules .gni file
with modules_gni_file.open('w', encoding='utf-8') as file:
- for line in _generate_modules_gni(root, prefix, modules):
+ for line in _generate_modules_gni(prefix, modules):
print(line, file=file)
- process = subprocess.run(['gn', 'format', modules_gni_file],
- stdout=subprocess.DEVNULL)
+ process = subprocess.run(
+ ['gn', 'format', modules_gni_file], stdout=subprocess.DEVNULL
+ )
if process.returncode != 0:
errors.append(_FORMAT_FAILED_WARNING)
@@ -238,17 +280,22 @@ def _main(root: Path, modules_list: Path, modules_gni_file: Path,
# Delete the stamp so this always reruns. Deleting is necessary since
# some of the checks do not depend on input files.
- if warn_only and warn_only.exists():
- warn_only.unlink()
+ if stamp and stamp.exists():
+ stamp.unlink()
+
+ if mode is Mode.WARN:
+ return 0
+
+ if mode is Mode.CHECK:
+ return 1
- # Warnings are non-fatal if warn_only is True.
- return 1 if errors or not warn_only else 0
+ return 1 if errors else 0 # Allow warnings but not errors when updating
- if warn_only:
- warn_only.touch()
+ if stamp:
+ stamp.touch()
return 0
if __name__ == '__main__':
- sys.exit(_main(**_parse_args()))
+ sys.exit(main(**_parse_args()))
diff --git a/pw_build/py/pw_build/generate_python_package.py b/pw_build/py/pw_build/generate_python_package.py
index d93cf8cc4..76ac587b1 100644
--- a/pw_build/py/pw_build/generate_python_package.py
+++ b/pw_build/py/pw_build/generate_python_package.py
@@ -17,7 +17,6 @@ import argparse
from collections import defaultdict
import configparser
from dataclasses import dataclass
-from itertools import chain
import json
from pathlib import Path
import sys
@@ -47,27 +46,37 @@ def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--label', help='Label for this Python package')
- parser.add_argument('--proto-library',
- dest='proto_libraries',
- type=argparse.FileType('r'),
- default=[],
- action='append',
- help='Paths')
- parser.add_argument('--generated-root',
- required=True,
- type=Path,
- help='The base directory for the Python package')
- parser.add_argument('--setup-json',
- required=True,
- type=argparse.FileType('r'),
- help='setup.py keywords as JSON')
- parser.add_argument('--module-as-package',
- action='store_true',
- help='Generate an __init__.py that imports everything')
- parser.add_argument('files',
- type=Path,
- nargs='+',
- help='Relative paths to the files in the package')
+ parser.add_argument(
+ '--proto-library',
+ dest='proto_libraries',
+ type=argparse.FileType('r'),
+ default=[],
+ action='append',
+ help='Paths',
+ )
+ parser.add_argument(
+ '--generated-root',
+ required=True,
+ type=Path,
+ help='The base directory for the Python package',
+ )
+ parser.add_argument(
+ '--setup-json',
+ required=True,
+ type=argparse.FileType('r'),
+ help='setup.py keywords as JSON',
+ )
+ parser.add_argument(
+ '--module-as-package',
+ action='store_true',
+ help='Generate an __init__.py that imports everything',
+ )
+ parser.add_argument(
+ 'files',
+ type=Path,
+ nargs='+',
+ help='Relative paths to the files in the package',
+ )
return parser.parse_args()
@@ -79,7 +88,8 @@ def _check_nested_protos(label: str, proto_info: Dict[str, Any]) -> None:
raise ValueError(
f"{label}'s 'proto_library' is set to {proto_info['label']}, but "
f"that target's 'python_package' is {python_package or 'not set'}. "
- f"Set {proto_info['label']}'s 'python_package' to {label}.")
+ f"Set {proto_info['label']}'s 'python_package' to {label}."
+ )
@dataclass(frozen=True)
@@ -90,8 +100,8 @@ class _ProtoInfo:
def _collect_all_files(
- root: Path, files: List[Path],
- paths_to_collect: Iterable[_ProtoInfo]) -> Dict[str, Set[str]]:
+ root: Path, files: List[Path], paths_to_collect: Iterable[_ProtoInfo]
+) -> Dict[str, Set[str]]:
"""Collects files in output dir, adds to files; returns package_data."""
root.mkdir(exist_ok=True)
@@ -107,10 +117,22 @@ def _collect_all_files(
# Make sure there are __init__.py and py.typed files for each subpackage.
for pkg in subpackages:
- for file in (pkg / name for name in ['__init__.py', 'py.typed']):
- if not file.exists():
- file.touch()
- files.append(file)
+ pytyped = pkg / 'py.typed'
+ if not pytyped.exists():
+ pytyped.touch()
+ files.append(pytyped)
+
+ # Create an __init__.py file if it doesn't already exist.
+ initpy = pkg / '__init__.py'
+ if not initpy.exists():
+ # Use pkgutil.extend_path to treat this as a namespaced package.
+ # This allows imports with the same name to live in multiple
+ # separate PYTHONPATH locations.
+ initpy.write_text(
+ 'from pkgutil import extend_path # type: ignore\n'
+ '__path__ = extend_path(__path__, __name__) # type: ignore\n'
+ )
+ files.append(initpy)
pkg_data: Dict[str, Set[str]] = defaultdict(set)
@@ -124,7 +146,7 @@ def _collect_all_files(
return pkg_data
-_PYPROJECT_FILE = '''\
+PYPROJECT_FILE = '''\
# Generated file. Do not modify.
[build-system]
requires = ['setuptools', 'wheel']
@@ -136,17 +158,16 @@ def _get_setup_keywords(pkg_data: dict, keywords: dict) -> Dict:
"""Gather all setuptools.setup() keyword args."""
options_keywords = dict(
packages=list(pkg_data),
- package_data={pkg: list(files)
- for pkg, files in pkg_data.items()},
+ package_data={pkg: list(files) for pkg, files in pkg_data.items()},
)
keywords['options'].update(options_keywords)
return keywords
-def _write_to_config(config: configparser.ConfigParser,
- data: Dict,
- section: Optional[str] = None):
+def _write_to_config(
+ config: configparser.ConfigParser, data: Dict, section: Optional[str] = None
+):
"""Populate a ConfigParser instance with the contents of a dict."""
# Add a specified section if missing.
if section is not None and not config.has_section(section):
@@ -198,16 +219,19 @@ def _import_module_in_package_init(all_files: List[Path]) -> None:
sources = [
f for f in all_files if f.suffix == '.py' and f.name != '__init__.py'
]
- assert len(sources) == 1, (
- 'Module as package expects a single .py source file')
+ assert (
+ len(sources) == 1
+ ), 'Module as package expects a single .py source file'
- source, = sources
+ (source,) = sources
source.parent.joinpath('__init__.py').write_text(
- f'from {source.stem}.{source.stem} import *\n')
+ f'from {source.stem}.{source.stem} import *\n'
+ )
-def _load_metadata(label: str,
- proto_libraries: Iterable[TextIO]) -> Iterator[_ProtoInfo]:
+def _load_metadata(
+ label: str, proto_libraries: Iterable[TextIO]
+) -> Iterator[_ProtoInfo]:
for proto_library_file in proto_libraries:
info = json.load(proto_library_file)
_check_nested_protos(label, info)
@@ -217,13 +241,21 @@ def _load_metadata(label: str,
with open(dep) as file:
deps.append(json.load(file)['package'])
- yield _ProtoInfo(Path(info['root']),
- tuple(Path(p) for p in info['protoc_outputs']), deps)
-
-
-def main(generated_root: Path, files: List[Path], module_as_package: bool,
- setup_json: TextIO, label: str,
- proto_libraries: Iterable[TextIO]) -> int:
+ yield _ProtoInfo(
+ Path(info['root']),
+ tuple(Path(p) for p in info['protoc_outputs']),
+ deps,
+ )
+
+
+def main(
+ generated_root: Path,
+ files: List[Path],
+ module_as_package: bool,
+ setup_json: TextIO,
+ label: str,
+ proto_libraries: Iterable[TextIO],
+) -> int:
"""Generates a setup.py and other files for a Python package."""
proto_infos = list(_load_metadata(label, proto_libraries))
try:
@@ -233,25 +265,26 @@ def main(generated_root: Path, files: List[Path], module_as_package: bool,
print(
f'ERROR: Failed to generate Python package {label}:\n\n'
f'{textwrap.indent(msg, " ")}\n',
- file=sys.stderr)
+ file=sys.stderr,
+ )
return 1
with setup_json:
setup_keywords = json.load(setup_json)
setup_keywords.setdefault('options', {})
- install_requires = setup_keywords['options'].setdefault(
- 'install_requires', [])
- install_requires += chain.from_iterable(i.deps for i in proto_infos)
+ setup_keywords['options'].setdefault('install_requires', [])
if module_as_package:
_import_module_in_package_init(files)
# Create the pyproject.toml and setup.cfg files for this package.
- generated_root.joinpath('pyproject.toml').write_text(_PYPROJECT_FILE)
- _generate_setup_cfg(pkg_data,
- setup_keywords,
- config_file_path=generated_root.joinpath('setup.cfg'))
+ generated_root.joinpath('pyproject.toml').write_text(PYPROJECT_FILE)
+ _generate_setup_cfg(
+ pkg_data,
+ setup_keywords,
+ config_file_path=generated_root.joinpath('setup.cfg'),
+ )
return 0
diff --git a/pw_build/py/pw_build/generate_python_package_gn.py b/pw_build/py/pw_build/generate_python_package_gn.py
index b3681b65a..aae5cc935 100755
--- a/pw_build/py/pw_build/generate_python_package_gn.py
+++ b/pw_build/py/pw_build/generate_python_package_gn.py
@@ -57,8 +57,9 @@ class PackageFiles(NamedTuple):
def _find_package_files(root_dir: Path) -> PackageFiles:
- files = git_repo.list_files(pathspecs=('*.py', '*.toml', '*.cfg'),
- repo_path=root_dir)
+ files = git_repo.list_files(
+ pathspecs=('*.py', '*.toml', '*.cfg'), repo_path=root_dir
+ )
package_files = PackageFiles([], [], [], [])
@@ -104,7 +105,8 @@ def generate_build_gn(root_dir: Path):
def main(paths: Iterable[Path]):
for path in paths:
path.joinpath('BUILD.gn').write_text(
- '\n'.join(generate_build_gn(path)) + '\n')
+ '\n'.join(generate_build_gn(path)) + '\n'
+ )
if __name__ == '__main__':
diff --git a/pw_build/py/pw_build/generate_python_requirements.py b/pw_build/py/pw_build/generate_python_requirements.py
new file mode 100644
index 000000000..3a7bc67fc
--- /dev/null
+++ b/pw_build/py/pw_build/generate_python_requirements.py
@@ -0,0 +1,135 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Check Python package install_requires are covered."""
+
+import argparse
+import configparser
+from pathlib import Path
+import sys
+
+try:
+ from pw_build.python_package import load_packages
+ from pw_build.create_python_tree import update_config_with_packages
+except ImportError:
+ # Load from python_package from this directory if pw_build is not available.
+ from python_package import load_packages # type: ignore
+ from create_python_tree import update_config_with_packages # type: ignore
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--python-dep-list-files',
+ type=Path,
+ required=True,
+ help=(
+ 'Path to a text file containing the list of Python package '
+ 'metadata json files.'
+ ),
+ )
+ parser.add_argument(
+ '--requirement',
+ type=Path,
+ required=True,
+ help='requirement file to generate',
+ )
+ parser.add_argument(
+ '--gn-packages',
+ required=True,
+ help=(
+ 'Comma separated list of GN python package '
+ 'targets to check for requirements.'
+ ),
+ )
+ parser.add_argument(
+ '--exclude-transitive-deps',
+ action='store_true',
+ help='Exclude checking transitive deps of the specified --gn-packages',
+ )
+ return parser.parse_args()
+
+
+class NoMatchingGnPythonDependency(Exception):
+ """An error occurred while processing a Python dependency."""
+
+
+def main(
+ python_dep_list_files: Path,
+ requirement: Path,
+ gn_packages: str,
+ exclude_transitive_deps: bool,
+) -> int:
+ """Check Python package setup.cfg correctness."""
+
+ # Split the comma separated string and remove leading slashes.
+ gn_target_names = [
+ target.lstrip('/')
+ for target in gn_packages.split(',')
+ if target # The last target may be an empty string.
+ ]
+ for i, gn_target in enumerate(gn_target_names):
+ # Remove metadata subtarget if present.
+ python_package_target = gn_target.replace('._package_metadata(', '(', 1)
+ # Split on the first paren to ignore the toolchain.
+ gn_target_names[i] = python_package_target.split('(')[0]
+
+ py_packages = load_packages([python_dep_list_files], ignore_missing=False)
+
+ target_py_packages = py_packages
+ if exclude_transitive_deps:
+ target_py_packages = []
+ for pkg in py_packages:
+ valid_target = [
+ target in pkg.gn_target_name for target in gn_target_names
+ ]
+ if not any(valid_target):
+ continue
+ target_py_packages.append(pkg)
+
+ if not target_py_packages:
+ gn_targets_to_include = '\n'.join(gn_target_names)
+ declared_py_deps = '\n'.join(pkg.gn_target_name for pkg in py_packages)
+ raise NoMatchingGnPythonDependency(
+ 'No matching GN Python dependency found.\n'
+ 'GN Targets to include:\n'
+ f'{gn_targets_to_include}\n\n'
+ 'Declared Python Dependencies:\n'
+ f'{declared_py_deps}\n\n'
+ )
+
+ config = configparser.ConfigParser()
+ config['options'] = {}
+ update_config_with_packages(
+ config=config, python_packages=target_py_packages
+ )
+
+ output = (
+ '# Auto-generated requirements.txt from the following packages:\n#\n'
+ )
+ output += '\n'.join(
+ '# ' + pkg.gn_target_name
+ for pkg in sorted(
+ target_py_packages, key=lambda pkg: pkg.gn_target_name
+ )
+ )
+
+ output += config['options']['install_requires']
+ output += '\n'
+ requirement.write_text(output)
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(**vars(_parse_args())))
diff --git a/pw_build/py/pw_build/generated_tests.py b/pw_build/py/pw_build/generated_tests.py
index af5480691..c7dd548cf 100644
--- a/pw_build/py/pw_build/generated_tests.py
+++ b/pw_build/py/pw_build/generated_tests.py
@@ -19,8 +19,19 @@ from datetime import datetime
from collections import defaultdict
import unittest
-from typing import (Any, Callable, Dict, Generic, Iterable, Iterator, List,
- Sequence, TextIO, TypeVar, Union)
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Generic,
+ Iterable,
+ Iterator,
+ List,
+ Sequence,
+ TextIO,
+ TypeVar,
+ Union,
+)
_COPYRIGHT = f"""\
// Copyright {datetime.now().year} The Pigweed Authors
@@ -42,13 +53,19 @@ _COPYRIGHT = f"""\
// Generated at {datetime.now().isoformat()}
"""
-_HEADER_CPP = _COPYRIGHT + """\
+_HEADER_CPP = (
+ _COPYRIGHT
+ + """\
// clang-format off
"""
+)
-_HEADER_JS = _COPYRIGHT + """\
+_HEADER_JS = (
+ _COPYRIGHT
+ + """\
/* eslint-env browser, jasmine */
"""
+)
class Error(Exception):
@@ -61,20 +78,23 @@ T = TypeVar('T')
@dataclass
class Context(Generic[T]):
"""Info passed into test generator functions for each test case."""
+
group: str
count: int
total: int
test_case: T
def cc_name(self) -> str:
- name = ''.join(w.capitalize()
- for w in self.group.replace('-', ' ').split(' '))
+ name = ''.join(
+ w.capitalize() for w in self.group.replace('-', ' ').split(' ')
+ )
name = ''.join(c if c.isalnum() else '_' for c in name)
return f'{name}_{self.count}' if self.total > 1 else name
def py_name(self) -> str:
- name = 'test_' + ''.join(c if c.isalnum() else '_'
- for c in self.group.lower())
+ name = 'test_' + ''.join(
+ c if c.isalnum() else '_' for c in self.group.lower()
+ )
return f'{name}_{self.count}' if self.total > 1 else name
def ts_name(self) -> str:
@@ -110,6 +130,7 @@ JsTestGenerator = Callable[[Context[T]], Iterable[str]]
class TestGenerator(Generic[T]):
"""Generates tests for multiple languages from a series of test cases."""
+
def __init__(self, test_cases: Sequence[GroupOrTest[T]]):
self._cases: Dict[str, List[T]] = defaultdict(list)
message = ''
@@ -119,7 +140,8 @@ class TestGenerator(Generic[T]):
if not isinstance(test_cases[0], str):
raise Error(
- 'The first item in the test cases must be a group name string')
+ 'The first item in the test cases must be a group name string'
+ )
for case in test_cases:
if isinstance(case, str):
@@ -143,8 +165,7 @@ class TestGenerator(Generic[T]):
test.__name__ = ctx.py_name()
if test.__name__ in tests:
- raise Error(
- f'Multiple Python tests are named {test.__name__}!')
+ raise Error(f'Multiple Python tests are named {test.__name__}!')
tests[test.__name__] = test
@@ -152,11 +173,15 @@ class TestGenerator(Generic[T]):
def python_tests(self, name: str, define_py_test: PyTestGenerator) -> type:
"""Returns a Python unittest.TestCase class with tests for each case."""
- return type(name, (unittest.TestCase, ),
- self._generate_python_tests(define_py_test))
-
- def _generate_cc_tests(self, define_cpp_test: CcTestGenerator, header: str,
- footer: str) -> Iterator[str]:
+ return type(
+ name,
+ (unittest.TestCase,),
+ self._generate_python_tests(define_py_test),
+ )
+
+ def _generate_cc_tests(
+ self, define_cpp_test: CcTestGenerator, header: str, footer: str
+ ) -> Iterator[str]:
yield _HEADER_CPP
yield header
@@ -166,15 +191,21 @@ class TestGenerator(Generic[T]):
yield footer
- def cc_tests(self, output: TextIO, define_cpp_test: CcTestGenerator,
- header: str, footer: str):
+ def cc_tests(
+ self,
+ output: TextIO,
+ define_cpp_test: CcTestGenerator,
+ header: str,
+ footer: str,
+ ):
"""Writes C++ unit tests for each test case to the given file."""
for line in self._generate_cc_tests(define_cpp_test, header, footer):
output.write(line)
output.write('\n')
- def _generate_ts_tests(self, define_ts_test: JsTestGenerator, header: str,
- footer: str) -> Iterator[str]:
+ def _generate_ts_tests(
+ self, define_ts_test: JsTestGenerator, header: str, footer: str
+ ) -> Iterator[str]:
yield _HEADER_JS
yield header
@@ -182,8 +213,13 @@ class TestGenerator(Generic[T]):
yield from define_ts_test(ctx)
yield footer
- def ts_tests(self, output: TextIO, define_js_test: JsTestGenerator,
- header: str, footer: str):
+ def ts_tests(
+ self,
+ output: TextIO,
+ define_js_test: JsTestGenerator,
+ header: str,
+ footer: str,
+ ):
"""Writes JS unit tests for each test case to the given file."""
for line in self._generate_ts_tests(define_js_test, header, footer):
output.write(line)
@@ -193,7 +229,7 @@ class TestGenerator(Generic[T]):
def _to_chars(data: bytes) -> Iterator[str]:
for i, byte in enumerate(data):
try:
- char = data[i:i + 1].decode()
+ char = data[i : i + 1].decode()
yield char if char.isprintable() else fr'\x{byte:02x}'
except UnicodeDecodeError:
yield fr'\x{byte:02x}'
@@ -209,10 +245,14 @@ def cc_string(data: Union[str, bytes]) -> str:
def parse_test_generation_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description='Generate unit test files')
- parser.add_argument('--generate-cc-test',
- type=argparse.FileType('w'),
- help='Generate the C++ test file')
- parser.add_argument('--generate-ts-test',
- type=argparse.FileType('w'),
- help='Generate the JS test file')
+ parser.add_argument(
+ '--generate-cc-test',
+ type=argparse.FileType('w'),
+ help='Generate the C++ test file',
+ )
+ parser.add_argument(
+ '--generate-ts-test',
+ type=argparse.FileType('w'),
+ help='Generate the JS test file',
+ )
return parser.parse_known_args()[0]
diff --git a/pw_build/py/pw_build/gn_resolver.py b/pw_build/py/pw_build/gn_resolver.py
new file mode 100644
index 000000000..afa01cf76
--- /dev/null
+++ b/pw_build/py/pw_build/gn_resolver.py
@@ -0,0 +1,488 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Evaluates target expressions within a GN build context."""
+
+import argparse
+from dataclasses import dataclass
+import enum
+import logging
+import os
+import re
+import sys
+from pathlib import Path
+from typing import (
+ Callable,
+ Dict,
+ List,
+ Iterable,
+ Iterator,
+ NamedTuple,
+ Optional,
+ Tuple,
+)
+
+_LOG = logging.getLogger(__name__)
+
+
+def abspath(path: Path) -> Path:
+ """Turns a path into an absolute path, not resolving symlinks."""
+ return Path(os.path.abspath(path))
+
+
+class GnPaths(NamedTuple):
+ """The set of paths needed to resolve GN paths to filesystem paths."""
+
+ root: Path
+ build: Path
+ cwd: Path
+
+ # Toolchain label or '' if using the default toolchain
+ toolchain: str
+
+ def resolve(self, gn_path: str) -> Path:
+ """Resolves a GN path to a filesystem path."""
+ if gn_path.startswith('//'):
+ return abspath(self.root.joinpath(gn_path.lstrip('/')))
+
+ return abspath(self.cwd.joinpath(gn_path))
+
+ def resolve_paths(self, gn_paths: str, sep: str = ';') -> str:
+ """Resolves GN paths to filesystem paths in a delimited string."""
+ return sep.join(str(self.resolve(path)) for path in gn_paths.split(sep))
+
+
+@dataclass(frozen=True)
+class Label:
+ """Represents a GN label."""
+
+ name: str
+ dir: Path
+ relative_dir: Path
+ toolchain: Optional['Label']
+ out_dir: Path
+ gen_dir: Path
+
+ def __init__(self, paths: GnPaths, label: str):
+ # Use this lambda to set attributes on this frozen dataclass.
+ set_attr = lambda attr, val: object.__setattr__(self, attr, val)
+
+ # Handle explicitly-specified toolchains
+ if label.endswith(')'):
+ label, toolchain = label[:-1].rsplit('(', 1)
+ else:
+ # Prevent infinite recursion for toolchains
+ toolchain = paths.toolchain if paths.toolchain != label else ''
+
+ set_attr('toolchain', Label(paths, toolchain) if toolchain else None)
+
+ # Split off the :target, if provided, or use the last part of the path.
+ try:
+ directory, name = label.rsplit(':', 1)
+ except ValueError:
+ directory, name = label, label.rsplit('/', 1)[-1]
+
+ set_attr('name', name)
+
+ # Resolve the directory to an absolute path
+ set_attr('dir', paths.resolve(directory))
+ set_attr('relative_dir', self.dir.relative_to(abspath(paths.root)))
+
+ set_attr(
+ 'out_dir',
+ paths.build / self.toolchain_name() / 'obj' / self.relative_dir,
+ )
+ set_attr(
+ 'gen_dir',
+ paths.build / self.toolchain_name() / 'gen' / self.relative_dir,
+ )
+
+ def gn_label(self) -> str:
+ label = f'//{self.relative_dir.as_posix()}:{self.name}'
+ return f'{label}({self.toolchain!r})' if self.toolchain else label
+
+ def toolchain_name(self) -> str:
+ return self.toolchain.name if self.toolchain else ''
+
+ def __repr__(self) -> str:
+ return self.gn_label()
+
+
+class _Artifact(NamedTuple):
+ path: Path
+ variables: Dict[str, str]
+
+
+# Matches a non-phony build statement.
+_GN_NINJA_BUILD_STATEMENT = re.compile(r'^build (.+):[ \n](?!phony\b)')
+
+_OBJECTS_EXTENSIONS = ('.o',)
+
+# Extensions used for compilation artifacts.
+_MAIN_ARTIFACTS = '', '.elf', '.a', '.so', '.dylib', '.exe', '.lib', '.dll'
+
+
+def _get_artifact(entries: List[str]) -> _Artifact:
+ """Attempts to resolve which artifact to use if there are multiple.
+
+ Selects artifacts based on extension. This will not work if a toolchain
+ creates multiple compilation artifacts from one command (e.g. .a and .elf).
+ """
+ assert entries, "There should be at least one entry here!"
+
+ if len(entries) == 1:
+ return _Artifact(Path(entries[0]), {})
+
+ filtered = [p for p in entries if Path(p).suffix in _MAIN_ARTIFACTS]
+
+ if len(filtered) == 1:
+ return _Artifact(Path(filtered[0]), {})
+
+ raise ExpressionError(
+ f'Expected 1, but found {len(filtered)} artifacts, after filtering for '
+ f'extensions {", ".join(repr(e) for e in _MAIN_ARTIFACTS)}: {entries}'
+ )
+
+
+def _parse_build_artifacts(fd) -> Iterator[_Artifact]:
+ """Partially parses the build statements in a Ninja file."""
+ lines = iter(fd)
+
+ def next_line():
+ try:
+ return next(lines)
+ except StopIteration:
+ return None
+
+ # Serves as the parse state (only two states)
+ artifact: Optional[_Artifact] = None
+
+ line = next_line()
+
+ while line is not None:
+ if artifact:
+ if line.startswith(' '): # build variable statements are indented
+ key, value = (a.strip() for a in line.split('=', 1))
+ artifact.variables[key] = value
+ line = next_line()
+ else:
+ yield artifact
+ artifact = None
+ else:
+ match = _GN_NINJA_BUILD_STATEMENT.match(line)
+ if match:
+ artifact = _get_artifact(match.group(1).split())
+
+ line = next_line()
+
+ if artifact:
+ yield artifact
+
+
+def _search_target_ninja(
+ ninja_file: Path, target: Label
+) -> Tuple[Optional[Path], List[Path]]:
+ """Parses the main output file and object files from <target>.ninja."""
+
+ artifact: Optional[Path] = None
+ objects: List[Path] = []
+
+ _LOG.debug('Parsing target Ninja file %s for %s', ninja_file, target)
+
+ with ninja_file.open() as fd:
+ for path, _ in _parse_build_artifacts(fd):
+ # Older GN used .stamp files when there is no build artifact.
+ if path.suffix == '.stamp':
+ continue
+
+ if str(path).endswith(_OBJECTS_EXTENSIONS):
+ objects.append(Path(path))
+ else:
+ assert not artifact, f'Multiple artifacts for {target}!'
+ artifact = Path(path)
+
+ return artifact, objects
+
+
+def _search_toolchain_ninja(
+ ninja_file: Path, paths: GnPaths, target: Label
+) -> Optional[Path]:
+ """Searches the toolchain.ninja file for outputs from the provided target.
+
+ Files created by an action appear in toolchain.ninja instead of in their own
+ <target>.ninja. If the specified target has a single output file in
+ toolchain.ninja, this function returns its path.
+ """
+
+ _LOG.debug('Searching toolchain Ninja file %s for %s', ninja_file, target)
+
+ # Older versions of GN used a .stamp file to signal completion of a target.
+ stamp_dir = target.out_dir.relative_to(paths.build).as_posix()
+ stamp_tool = 'stamp'
+ if target.toolchain_name() != '':
+ stamp_tool = f'{target.toolchain_name()}_stamp'
+ stamp_statement = f'build {stamp_dir}/{target.name}.stamp: {stamp_tool} '
+
+ # Newer GN uses a phony Ninja target to signal completion of a target.
+ phony_dir = Path(
+ target.toolchain_name(), 'phony', target.relative_dir
+ ).as_posix()
+ phony_statement = f'build {phony_dir}/{target.name}: phony '
+
+ with ninja_file.open() as fd:
+ for line in fd:
+ for statement in (phony_statement, stamp_statement):
+ if line.startswith(statement):
+ output_files = line[len(statement) :].strip().split()
+ if len(output_files) == 1:
+ return Path(output_files[0])
+
+ break
+
+ return None
+
+
+def _search_ninja_files(
+ paths: GnPaths, target: Label
+) -> Tuple[bool, Optional[Path], List[Path]]:
+ ninja_file = target.out_dir / f'{target.name}.ninja'
+ if ninja_file.exists():
+ return (True, *_search_target_ninja(ninja_file, target))
+
+ ninja_file = paths.build / target.toolchain_name() / 'toolchain.ninja'
+ if ninja_file.exists():
+ return True, _search_toolchain_ninja(ninja_file, paths, target), []
+
+ return False, None, []
+
+
+@dataclass(frozen=True)
+class TargetInfo:
+ """Provides information about a target parsed from a .ninja file."""
+
+ label: Label
+ generated: bool # True if the Ninja files for this target were generated.
+ artifact: Optional[Path]
+ object_files: Tuple[Path]
+
+ def __init__(self, paths: GnPaths, target: str):
+ object.__setattr__(self, 'label', Label(paths, target))
+
+ generated, artifact, objects = _search_ninja_files(paths, self.label)
+
+ object.__setattr__(self, 'generated', generated)
+ object.__setattr__(self, 'artifact', artifact)
+ object.__setattr__(self, 'object_files', tuple(objects))
+
+ def __repr__(self) -> str:
+ return repr(self.label)
+
+
+class ExpressionError(Exception):
+ """An error occurred while parsing an expression."""
+
+
+class _ArgAction(enum.Enum):
+ APPEND = 0
+ OMIT = 1
+ EMIT_NEW = 2
+
+
+class _Expression:
+ def __init__(self, match: re.Match, ending: int):
+ self._match = match
+ self._ending = ending
+
+ @property
+ def string(self):
+ return self._match.string
+
+ @property
+ def end(self) -> int:
+ return self._ending + len(_ENDING)
+
+ def contents(self) -> str:
+ return self.string[self._match.end() : self._ending]
+
+ def expression(self) -> str:
+ return self.string[self._match.start() : self.end]
+
+
+_Actions = Iterator[Tuple[_ArgAction, str]]
+
+
+def _target_file(paths: GnPaths, expr: _Expression) -> _Actions:
+ target = TargetInfo(paths, expr.contents())
+
+ if not target.generated:
+ raise ExpressionError(f'Target {target} has not been generated by GN!')
+
+ if target.artifact is None:
+ raise ExpressionError(f'Target {target} has no output file!')
+
+ yield _ArgAction.APPEND, str(target.artifact)
+
+
+def _target_file_if_exists(paths: GnPaths, expr: _Expression) -> _Actions:
+ target = TargetInfo(paths, expr.contents())
+
+ if target.generated:
+ if target.artifact is None:
+ raise ExpressionError(f'Target {target} has no output file!')
+
+ if paths.build.joinpath(target.artifact).exists():
+ yield _ArgAction.APPEND, str(target.artifact)
+ return
+
+ yield _ArgAction.OMIT, ''
+
+
+def _target_objects(paths: GnPaths, expr: _Expression) -> _Actions:
+ if expr.expression() != expr.string:
+ raise ExpressionError(
+ f'The expression "{expr.expression()}" in "{expr.string}" may '
+ 'expand to multiple arguments, so it cannot be used alongside '
+ 'other text or expressions'
+ )
+
+ target = TargetInfo(paths, expr.contents())
+ if not target.generated:
+ raise ExpressionError(f'Target {target} has not been generated by GN!')
+
+ for obj in target.object_files:
+ yield _ArgAction.EMIT_NEW, str(obj)
+
+
+# TODO(b/234886742): Replace expressions with native GN features when possible.
+_FUNCTIONS: Dict['str', Callable[[GnPaths, _Expression], _Actions]] = {
+ 'TARGET_FILE': _target_file,
+ 'TARGET_FILE_IF_EXISTS': _target_file_if_exists,
+ 'TARGET_OBJECTS': _target_objects,
+}
+
+_START_EXPRESSION = re.compile(fr'<({"|".join(_FUNCTIONS)})\(')
+_ENDING = ')>'
+
+
+def _expand_arguments(paths: GnPaths, string: str) -> _Actions:
+ pos = 0
+
+ for match in _START_EXPRESSION.finditer(string):
+ if pos != match.start():
+ yield _ArgAction.APPEND, string[pos : match.start()]
+
+ ending = string.find(_ENDING, match.end())
+ if ending == -1:
+ raise ExpressionError(
+ f'Parse error: no terminating "{_ENDING}" '
+ f'was found for "{string[match.start():]}"'
+ )
+
+ expression = _Expression(match, ending)
+ yield from _FUNCTIONS[match.group(1)](paths, expression)
+
+ pos = expression.end
+
+ if pos < len(string):
+ yield _ArgAction.APPEND, string[pos:]
+
+
+def expand_expressions(paths: GnPaths, arg: str) -> Iterable[str]:
+ """Expands <FUNCTION(...)> expressions; yields zero or more arguments."""
+ if arg == '':
+ return ['']
+
+ expanded_args: List[List[str]] = [[]]
+
+ for action, piece in _expand_arguments(paths, arg):
+ if action is _ArgAction.OMIT:
+ return []
+
+ expanded_args[-1].append(piece)
+ if action is _ArgAction.EMIT_NEW:
+ expanded_args.append([])
+
+ return (''.join(arg) for arg in expanded_args if arg)
+
+
+def _parse_args() -> argparse.Namespace:
+ file_pair = lambda s: tuple(Path(p) for p in s.split(':'))
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--gn-root',
+ type=Path,
+ required=True,
+ help=(
+ 'Path to the root of the GN tree; '
+ 'value of rebase_path("//", root_build_dir)'
+ ),
+ )
+ parser.add_argument(
+ '--current-path',
+ type=Path,
+ required=True,
+ help='Value of rebase_path(".", root_build_dir)',
+ )
+ parser.add_argument(
+ '--default-toolchain', required=True, help='Value of default_toolchain'
+ )
+ parser.add_argument(
+ '--current-toolchain', required=True, help='Value of current_toolchain'
+ )
+ parser.add_argument(
+ 'files',
+ metavar='FILE',
+ nargs='+',
+ type=file_pair,
+ help='Pairs of src:dest files to scan for expressions to evaluate',
+ )
+ return parser.parse_args()
+
+
+def _resolve_expressions_in_file(src: Path, dst: Path, paths: GnPaths):
+ dst.write_text(''.join(expand_expressions(paths, src.read_text())))
+
+
+def main(
+ gn_root: Path,
+ current_path: Path,
+ default_toolchain: str,
+ current_toolchain: str,
+ files: Iterable[Tuple[Path, Path]],
+) -> int:
+ """Evaluates GN target expressions within a list of files.
+
+ Modifies the files in-place with their resolved contents.
+ """
+ tool = current_toolchain if current_toolchain != default_toolchain else ''
+ paths = GnPaths(
+ root=abspath(gn_root),
+ build=Path.cwd(),
+ cwd=abspath(current_path),
+ toolchain=tool,
+ )
+
+ for src, dst in files:
+ try:
+ _resolve_expressions_in_file(src, dst, paths)
+ except ExpressionError as err:
+ _LOG.error('Error evaluating expressions in %s:', src)
+ _LOG.error(' %s', err)
+ return 1
+
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main(**vars(_parse_args())))
diff --git a/pw_build/py/pw_build/host_tool.py b/pw_build/py/pw_build/host_tool.py
index 00ba3b040..1c9fd64ae 100644
--- a/pw_build/py/pw_build/host_tool.py
+++ b/pw_build/py/pw_build/host_tool.py
@@ -26,26 +26,26 @@ _LOG = logging.getLogger(__name__)
def argument_parser(
- parser: Optional[argparse.ArgumentParser] = None
+ parser: Optional[argparse.ArgumentParser] = None,
) -> argparse.ArgumentParser:
"""Registers the script's arguments on an argument parser."""
if parser is None:
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--dst',
- type=Path,
- required=True,
- help='Path to host tools directory')
+ parser.add_argument(
+ '--dst', type=Path, required=True, help='Path to host tools directory'
+ )
parser.add_argument('--name', help='Name for the installed tool')
- parser.add_argument('--out-root',
- type=Path,
- required=True,
- help='Root of Ninja out directory')
- parser.add_argument('--src',
- type=Path,
- required=True,
- help='Path to host tool executable')
+ parser.add_argument(
+ '--out-root',
+ type=Path,
+ required=True,
+ help='Root of Ninja out directory',
+ )
+ parser.add_argument(
+ '--src', type=Path, required=True, help='Path to host tool executable'
+ )
return parser
@@ -81,12 +81,14 @@ def main() -> int:
_LOG.error(' %s has been rebuilt but cannot be', name)
_LOG.error(' copied into the host tools directory:')
_LOG.error('')
- _LOG.error(' %s',
- args.dst.relative_to(args.out_root).joinpath(name))
+ _LOG.error(
+ ' %s', args.dst.relative_to(args.out_root).joinpath(name)
+ )
_LOG.error('')
_LOG.error(' This can occur if the program is already running.')
_LOG.error(
- ' If it is running, exit it and try re-running the build.')
+ ' If it is running, exit it and try re-running the build.'
+ )
_LOG.error('')
return 1
diff --git a/pw_build/py/pw_build/mirror_tree.py b/pw_build/py/pw_build/mirror_tree.py
index 46d19a058..51e761478 100644
--- a/pw_build/py/pw_build/mirror_tree.py
+++ b/pw_build/py/pw_build/mirror_tree.py
@@ -17,7 +17,7 @@ import argparse
import os
from pathlib import Path
import shutil
-from typing import Iterable, Iterator, List
+from typing import Iterable, Iterator, List, Optional
def _parse_args() -> argparse.Namespace:
@@ -25,27 +25,31 @@ def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--source-root',
- type=Path,
- required=True,
- help='Prefix to strip from the source files')
- parser.add_argument('sources',
- type=Path,
- nargs='*',
- help='Files to mirror to the directory')
- parser.add_argument('--directory',
- type=Path,
- required=True,
- help='Directory to which to mirror the sources')
- parser.add_argument('--path-file',
- type=Path,
- help='File with paths to files to mirror')
+ parser.add_argument(
+ '--source-root',
+ type=Path,
+ required=True,
+ help='Prefix to strip from the source files',
+ )
+ parser.add_argument(
+ 'sources', type=Path, nargs='*', help='Files to mirror to the directory'
+ )
+ parser.add_argument(
+ '--directory',
+ type=Path,
+ required=True,
+ help='Directory to which to mirror the sources',
+ )
+ parser.add_argument(
+ '--path-file', type=Path, help='File with paths to files to mirror'
+ )
return parser.parse_args()
-def _link_files(source_root: Path, sources: Iterable[Path],
- directory: Path) -> Iterator[Path]:
+def _link_files(
+ source_root: Path, sources: Iterable[Path], directory: Path
+) -> Iterator[Path]:
for source in sources:
dest = directory / source.relative_to(source_root)
dest.parent.mkdir(parents=True, exist_ok=True)
@@ -67,8 +71,9 @@ def _link_files(source_root: Path, sources: Iterable[Path],
yield dest
-def _link_files_or_dirs(paths: Iterable[Path],
- directory: Path) -> Iterator[Path]:
+def _link_files_or_dirs(
+ paths: Iterable[Path], directory: Path
+) -> Iterator[Path]:
"""Links files or directories into the output directory.
Files are linked directly; files in directories are linked as relative paths
@@ -85,10 +90,12 @@ def _link_files_or_dirs(paths: Iterable[Path],
raise FileNotFoundError(f'{path} does not exist!')
-def mirror_paths(source_root: Path,
- sources: Iterable[Path],
- directory: Path,
- path_file: Path = None) -> List[Path]:
+def mirror_paths(
+ source_root: Path,
+ sources: Iterable[Path],
+ directory: Path,
+ path_file: Optional[Path] = None,
+) -> List[Path]:
"""Creates hard links in the provided directory for the provided sources.
Args:
diff --git a/pw_build/py/pw_build/pip_install_python_deps.py b/pw_build/py/pw_build/pip_install_python_deps.py
new file mode 100644
index 000000000..4768bed9c
--- /dev/null
+++ b/pw_build/py/pw_build/pip_install_python_deps.py
@@ -0,0 +1,122 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Pip install Pigweed Python packages."""
+
+import argparse
+from pathlib import Path
+import subprocess
+import sys
+from typing import List, Tuple
+
+try:
+ from pw_build.python_package import load_packages
+except ImportError:
+ # Load from python_package from this directory if pw_build is not available.
+ from python_package import load_packages # type: ignore
+
+
+def _parse_args() -> Tuple[argparse.Namespace, List[str]]:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--python-dep-list-files',
+ type=Path,
+ required=True,
+ help=(
+ 'Path to a text file containing the list of Python package '
+ 'metadata json files.'
+ ),
+ )
+ parser.add_argument(
+ '--gn-packages',
+ required=True,
+ help=(
+ 'Comma separated list of GN python package ' 'targets to install.'
+ ),
+ )
+ parser.add_argument(
+ '--editable-pip-install',
+ action='store_true',
+ help=(
+ 'If true run the pip install command with the '
+ '\'--editable\' option.'
+ ),
+ )
+ return parser.parse_known_args()
+
+
+class NoMatchingGnPythonDependency(Exception):
+ """An error occurred while processing a Python dependency."""
+
+
+def main(
+ python_dep_list_files: Path,
+ editable_pip_install: bool,
+ gn_targets: List[str],
+ pip_args: List[str],
+) -> int:
+ """Find matching python packages to pip install."""
+ pip_target_dirs: List[str] = []
+
+ py_packages = load_packages([python_dep_list_files], ignore_missing=True)
+ for pkg in py_packages:
+ valid_target = [target in pkg.gn_target_name for target in gn_targets]
+ if not any(valid_target):
+ continue
+ top_level_source_dir = pkg.package_dir
+ pip_target_dirs.append(str(top_level_source_dir.parent.resolve()))
+
+ if not pip_target_dirs:
+ raise NoMatchingGnPythonDependency(
+ 'No matching GN Python dependency found to install.\n'
+ 'GN Targets to pip install:\n' + '\n'.join(gn_targets) + '\n\n'
+ 'Declared Python Dependencies:\n'
+ + '\n'.join(pkg.gn_target_name for pkg in py_packages)
+ + '\n\n'
+ )
+
+ for target in pip_target_dirs:
+ command_args = [sys.executable, "-m", "pip"]
+ command_args += pip_args
+ if editable_pip_install:
+ command_args.append('--editable')
+ command_args.append(target)
+
+ process = subprocess.run(
+ command_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
+ pip_output = process.stdout.decode()
+ if process.returncode != 0:
+ print(pip_output)
+ return process.returncode
+ return 0
+
+
+if __name__ == '__main__':
+ # Parse this script's args and pass any remaining args to pip.
+ argparse_args, remaining_args_for_pip = _parse_args()
+
+ # Split the comma separated string and remove leading slashes.
+ gn_target_names = [
+ target.lstrip('/')
+ for target in argparse_args.gn_packages.split(',')
+ if target # The last target may be an empty string.
+ ]
+
+ result = main(
+ python_dep_list_files=argparse_args.python_dep_list_files,
+ editable_pip_install=argparse_args.editable_pip_install,
+ gn_targets=gn_target_names,
+ pip_args=remaining_args_for_pip,
+ )
+ sys.exit(result)
diff --git a/pw_build/py/pw_build/project_builder.py b/pw_build/py/pw_build/project_builder.py
new file mode 100644
index 000000000..cb8ab5941
--- /dev/null
+++ b/pw_build/py/pw_build/project_builder.py
@@ -0,0 +1,953 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Build a Pigweed Project.
+
+Run arbitrary commands or invoke build systems (Ninja, Bazel and make) on one or
+more build directories.
+
+Examples:
+
+ # Build the default target in out/ using ninja.
+ python -m pw_build.project_builder -C out
+
+ # Build pw_run_tests.modules in the out/cmake directory
+ python -m pw_build.project_builder -C out/cmake pw_run_tests.modules
+
+ # Build the default target in out/ and pw_apps in out/cmake
+ python -m pw_build.project_builder -C out -C out/cmake pw_apps
+
+ # Build python.tests in out/ and pw_apps in out/cmake/
+ python -m pw_build.project_builder python.tests -C out/cmake pw_apps
+
+ # Run 'bazel build' and 'bazel test' on the target '//...' in outbazel/
+ python -m pw_build.project_builder --run-command 'mkdir -p outbazel'
+ -C outbazel '//...'
+ --build-system-command outbazel 'bazel build'
+ --build-system-command outbazel 'bazel test'
+"""
+
+import argparse
+import concurrent.futures
+import os
+import logging
+from pathlib import Path
+import re
+import shlex
+import sys
+import subprocess
+import time
+from typing import (
+ Callable,
+ Dict,
+ Generator,
+ List,
+ NoReturn,
+ Optional,
+ Sequence,
+ NamedTuple,
+)
+
+from prompt_toolkit.patch_stdout import StdoutProxy
+
+import pw_cli.env
+import pw_cli.log
+
+from pw_build.build_recipe import BuildRecipe, create_build_recipes
+from pw_build.project_builder_argparse import add_project_builder_arguments
+from pw_build.project_builder_context import get_project_builder_context
+from pw_build.project_builder_prefs import ProjectBuilderPrefs
+
+_COLOR = pw_cli.color.colors()
+_LOG = logging.getLogger('pw_build')
+
+BUILDER_CONTEXT = get_project_builder_context()
+
+PASS_MESSAGE = """
+ ██████╗ █████╗ ███████╗███████╗██╗
+ ██╔══██╗██╔══██╗██╔════╝██╔════╝██║
+ ██████╔╝███████║███████╗███████╗██║
+ ██╔═══╝ ██╔══██║╚════██║╚════██║╚═╝
+ ██║ ██║ ██║███████║███████║██╗
+ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝
+"""
+
+# Pick a visually-distinct font from "PASS" to ensure that readers can't
+# possibly mistake the difference between the two states.
+FAIL_MESSAGE = """
+ ▄██████▒░▄▄▄ ██▓ ░██▓
+ ▓█▓ ░▒████▄ ▓██▒ ░▓██▒
+ ▒████▒ ░▒█▀ ▀█▄ ▒██▒ ▒██░
+ ░▓█▒ ░░██▄▄▄▄██ ░██░ ▒██░
+ ░▒█░ ▓█ ▓██▒░██░░ ████████▒
+ ▒█░ ▒▒ ▓▒█░░▓ ░ ▒░▓ ░
+ ░▒ ▒ ▒▒ ░ ▒ ░░ ░ ▒ ░
+ ░ ░ ░ ▒ ▒ ░ ░ ░
+ ░ ░ ░ ░ ░
+"""
+
+
+class ProjectBuilderCharset(NamedTuple):
+ slug_ok: str
+ slug_fail: str
+ slug_building: str
+
+
+ASCII_CHARSET = ProjectBuilderCharset(
+ _COLOR.green('OK '),
+ _COLOR.red('FAIL'),
+ _COLOR.yellow('... '),
+)
+EMOJI_CHARSET = ProjectBuilderCharset('✔️ ', '❌', '⏱️ ')
+
+
+def _exit(*args) -> NoReturn:
+ _LOG.critical(*args)
+ sys.exit(1)
+
+
+def _exit_due_to_interrupt() -> None:
+ """Abort function called when not using progress bars."""
+ # To keep the log lines aligned with each other in the presence of
+ # a '^C' from the keyboard interrupt, add a newline before the log.
+ print()
+ _LOG.info('Got Ctrl-C; exiting...')
+ BUILDER_CONTEXT.ctrl_c_interrupt()
+
+
+_NINJA_BUILD_STEP = re.compile(
+ r'^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$'
+)
+
+_NINJA_FAILURE_TEXT = '\033[31mFAILED: '
+
+
+def execute_command_no_logging(
+ command: List,
+ env: Dict,
+ recipe: BuildRecipe,
+ # pylint: disable=unused-argument
+ logger: logging.Logger = _LOG,
+ line_processed_callback: Optional[Callable[[BuildRecipe], None]] = None,
+ # pylint: enable=unused-argument
+) -> bool:
+ print()
+ proc = subprocess.Popen(command, env=env, errors='replace')
+ BUILDER_CONTEXT.register_process(recipe, proc)
+ returncode = None
+ while returncode is None:
+ if BUILDER_CONTEXT.build_stopping():
+ proc.terminate()
+ returncode = proc.poll()
+ time.sleep(0.05)
+ print()
+ recipe.status.return_code = returncode
+
+ return proc.returncode == 0
+
+
+def execute_command_with_logging(
+ command: List,
+ env: Dict,
+ recipe: BuildRecipe,
+ logger: logging.Logger = _LOG,
+ line_processed_callback: Optional[Callable[[BuildRecipe], None]] = None,
+) -> bool:
+ """Run a command in a subprocess and log all output."""
+ current_stdout = ''
+ returncode = None
+
+ with subprocess.Popen(
+ command,
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ errors='replace',
+ ) as proc:
+ BUILDER_CONTEXT.register_process(recipe, proc)
+ # Empty line at the start.
+ logger.info('')
+
+ failure_line = False
+ while returncode is None:
+ output = ''
+ error_output = ''
+
+ if proc.stdout:
+ output = proc.stdout.readline()
+ current_stdout += output
+ if proc.stderr:
+ error_output += proc.stderr.readline()
+ current_stdout += error_output
+
+ if not output and not error_output:
+ returncode = proc.poll()
+ continue
+
+ line_match_result = _NINJA_BUILD_STEP.match(output)
+ if line_match_result:
+ if failure_line and not BUILDER_CONTEXT.build_stopping():
+ recipe.status.log_last_failure()
+ failure_line = False
+ matches = line_match_result.groupdict()
+ recipe.status.current_step = line_match_result.group(0)
+ step = int(matches.get('step', 0))
+ total_steps = int(matches.get('total_steps', 1))
+ recipe.status.percent = float(step / total_steps)
+
+ logger_method = logger.info
+ if output.startswith(_NINJA_FAILURE_TEXT):
+ logger_method = logger.error
+ if failure_line and not BUILDER_CONTEXT.build_stopping():
+ recipe.status.log_last_failure()
+ recipe.status.increment_error_count()
+ failure_line = True
+
+ # Mypy output mixes character encoding in color coded output
+ # and uses the 'sgr0' (or exit_attribute_mode) capability from the
+ # host machine's terminfo database.
+ #
+ # This can result in this sequence ending up in STDOUT as
+ # b'\x1b(B\x1b[m'. (B tells terminals to interpret text as
+ # USASCII encoding but will appear in prompt_toolkit as a B
+ # character.
+ #
+ # The following replace calls will strip out those
+ # sequences.
+ stripped_output = output.replace('\x1b(B', '').strip()
+
+ if not line_match_result:
+ logger_method(stripped_output)
+ recipe.status.current_step = stripped_output
+
+ if failure_line:
+ recipe.status.append_failure_line(stripped_output)
+
+ BUILDER_CONTEXT.redraw_progress()
+
+ if line_processed_callback:
+ line_processed_callback(recipe)
+
+ if BUILDER_CONTEXT.build_stopping():
+ proc.terminate()
+
+ recipe.status.return_code = returncode
+
+ # Log the last failure if not done already
+ if failure_line and not BUILDER_CONTEXT.build_stopping():
+ recipe.status.log_last_failure()
+
+ # Empty line at the end.
+ logger.info('')
+
+ return returncode == 0
+
+
+def log_build_recipe_start(
+ index_message: str,
+ project_builder: 'ProjectBuilder',
+ cfg: BuildRecipe,
+ logger: logging.Logger = _LOG,
+) -> None:
+ """Log recipe start and truncate the build logfile."""
+ if project_builder.separate_build_file_logging and cfg.logfile:
+ # Truncate the file
+ with open(cfg.logfile, 'w'):
+ pass
+
+ BUILDER_CONTEXT.mark_progress_started(cfg)
+
+ build_start_msg = [
+ index_message,
+ project_builder.color.cyan('Starting ==>'),
+ project_builder.color.blue('Recipe:'),
+ str(cfg.display_name),
+ project_builder.color.blue('Targets:'),
+ str(' '.join(cfg.targets())),
+ ]
+
+ if cfg.logfile:
+ build_start_msg.extend(
+ [
+ project_builder.color.blue('Logfile:'),
+ str(cfg.logfile.resolve()),
+ ]
+ )
+ build_start_str = ' '.join(build_start_msg)
+
+ # Log start to the root log if recipe logs are not sent.
+ if not project_builder.send_recipe_logs_to_root:
+ logger.info(build_start_str)
+ if cfg.logfile:
+ cfg.log.info(build_start_str)
+
+
+def log_build_recipe_finish(
+ index_message: str,
+ project_builder: 'ProjectBuilder',
+ cfg: BuildRecipe,
+ logger: logging.Logger = _LOG,
+) -> None:
+ """Log recipe finish and any build errors."""
+
+ BUILDER_CONTEXT.mark_progress_done(cfg)
+
+ if BUILDER_CONTEXT.interrupted():
+ level = logging.WARNING
+ tag = project_builder.color.yellow('(ABORT)')
+ elif cfg.status.failed():
+ level = logging.ERROR
+ tag = project_builder.color.red('(FAIL)')
+ else:
+ level = logging.INFO
+ tag = project_builder.color.green('(OK)')
+
+ build_finish_msg = [
+ level,
+ '%s %s %s %s %s',
+ index_message,
+ project_builder.color.cyan('Finished ==>'),
+ project_builder.color.blue('Recipe:'),
+ cfg.display_name,
+ tag,
+ ]
+
+ # Log finish to the root log if recipe logs are not sent.
+ if not project_builder.send_recipe_logs_to_root:
+ logger.log(*build_finish_msg)
+ if cfg.logfile:
+ cfg.log.log(*build_finish_msg)
+
+ if (
+ not BUILDER_CONTEXT.build_stopping()
+ and cfg.status.failed()
+ and cfg.status.error_count == 0
+ ):
+ cfg.status.log_entire_recipe_logfile()
+
+
+class MissingGlobalLogfile(Exception):
+ """Exception raised if a global logfile is not specifed."""
+
+
+class DispatchingFormatter(logging.Formatter):
+ """Dispatch log formatting based on the logger name."""
+
+ def __init__(self, formatters, default_formatter):
+ self._formatters = formatters
+ self._default_formatter = default_formatter
+ super().__init__()
+
+ def format(self, record):
+ logger = logging.getLogger(record.name)
+ formatter = self._formatters.get(logger.name, self._default_formatter)
+ return formatter.format(record)
+
+
+class ProjectBuilder: # pylint: disable=too-many-instance-attributes
+ """Pigweed Project Builder
+
+ Controls how build recipes are executed and logged.
+
+ Example usage:
+
+ .. code-block:: python
+
+ import logging
+ from pathlib import Path
+
+ from pw_build.build_recipe import BuildCommand, BuildRecipe
+ from pw_build.project_builder import ProjectBuilder
+
+ def should_gen_gn(out: Path) -> bool:
+ return not (out / 'build.ninja').is_file()
+
+ recipe = BuildRecipe(
+ build_dir='out',
+ title='Vanilla Ninja Build',
+ steps=[
+ BuildCommand(command=['gn', 'gen', '{build_dir}'],
+ run_if=should_gen_gn),
+ BuildCommand(build_system_command='ninja',
+ build_system_extra_args=['-k', '0'],
+ targets=['default']),
+ ],
+ )
+
+ project_builder = ProjectBuilder(
+ build_recipes=[recipe1, ...]
+ banners=True,
+ log_level=logging.INFO
+ separate_build_file_logging=True,
+ root_logger=logging.getLogger(),
+ root_logfile=Path('build_log.txt'),
+ )
+
+ Args:
+ build_recipes: List of build recipes.
+ jobs: The number of jobs bazel, make, and ninja should use by passing
+ ``-j`` to each.
+ keep_going: If True keep going flags are passed to bazel and ninja with
+ the ``-k`` option.
+ banners: Print the project banner at the start of each build.
+ allow_progress_bars: If False progress bar output will be disabled.
+ colors: Print ANSI colors to stdout and logfiles
+ log_level: Optional log_level, defaults to logging.INFO.
+ root_logfile: Optional root logfile.
+ separate_build_file_logging: If True separate logfiles will be created
+ per build recipe. The location of each file depends on if a
+ ``root_logfile`` is provided. If a root logfile is used each build
+ recipe logfile will be created in the same location. If no
+ root_logfile is specified the per build log files are placed in each
+ build dir as ``log.txt``
+ send_recipe_logs_to_root: If True will send all build recipie output to
+ the root logger. This only makes sense to use if the builds are run
+ in serial.
+ use_verbatim_error_log_formatting: Use a blank log format when printing
+ errors from sub builds to the root logger.
+ """
+
+ def __init__(
+ # pylint: disable=too-many-arguments,too-many-locals
+ self,
+ build_recipes: Sequence[BuildRecipe],
+ jobs: Optional[int] = None,
+ banners: bool = True,
+ keep_going: bool = False,
+ abort_callback: Callable = _exit,
+ execute_command: Callable[
+ [List, Dict, BuildRecipe, logging.Logger, Optional[Callable]], bool
+ ] = execute_command_no_logging,
+ charset: ProjectBuilderCharset = ASCII_CHARSET,
+ colors: bool = True,
+ separate_build_file_logging: bool = False,
+ send_recipe_logs_to_root: bool = False,
+ root_logger: logging.Logger = _LOG,
+ root_logfile: Optional[Path] = None,
+ log_level: int = logging.INFO,
+ allow_progress_bars: bool = True,
+ use_verbatim_error_log_formatting: bool = False,
+ ):
+ self.charset: ProjectBuilderCharset = charset
+ self.abort_callback = abort_callback
+ # Function used to run subprocesses
+ self.execute_command = execute_command
+ self.banners = banners
+ self.build_recipes = build_recipes
+ self.max_name_width = max(
+ [len(str(step.display_name)) for step in self.build_recipes]
+ )
+ # Set project_builder reference in each recipe.
+ for recipe in self.build_recipes:
+ recipe.set_project_builder(self)
+
+ # Save build system args
+ self.extra_ninja_args: List[str] = []
+ self.extra_bazel_args: List[str] = []
+ self.extra_bazel_build_args: List[str] = []
+
+ # Handle jobs and keep going flags.
+ if jobs:
+ job_args = ['-j', f'{jobs}']
+ self.extra_ninja_args.extend(job_args)
+ self.extra_bazel_build_args.extend(job_args)
+ if keep_going:
+ self.extra_ninja_args.extend(['-k', '0'])
+ self.extra_bazel_build_args.extend(['-k'])
+
+ self.colors = colors
+ # Reference to pw_cli.color, will return colored text if colors are
+ # enabled.
+ self.color = pw_cli.color.colors(colors)
+
+ # Pass color setting to bazel
+ if colors:
+ self.extra_bazel_args.append('--color=yes')
+ else:
+ self.extra_bazel_args.append('--color=no')
+
+ # Progress bar enable/disable flag
+ self.allow_progress_bars = allow_progress_bars
+ self.stdout_proxy: Optional[StdoutProxy] = None
+
+ # Logger configuration
+ self.root_logger = root_logger
+ self.default_logfile = root_logfile
+ self.default_log_level = log_level
+ # Create separate logs per build
+ self.separate_build_file_logging = separate_build_file_logging
+ # Propagate logs to the root looger
+ self.send_recipe_logs_to_root = send_recipe_logs_to_root
+
+ # Setup the error logger
+ self.use_verbatim_error_log_formatting = (
+ use_verbatim_error_log_formatting
+ )
+ self.error_logger = logging.getLogger(f'{root_logger.name}.errors')
+ self.error_logger.setLevel(log_level)
+ self.error_logger.propagate = True
+ for recipe in self.build_recipes:
+ recipe.set_error_logger(self.error_logger)
+
+ # Copy of the standard Pigweed style log formatter, used by default if
+ # no formatter exists on the root logger.
+ timestamp_fmt = self.color.black_on_white('%(asctime)s') + ' '
+ self.default_log_formatter = logging.Formatter(
+ timestamp_fmt + '%(levelname)s %(message)s', '%Y%m%d %H:%M:%S'
+ )
+
+ # Empty log formatter (optionally used for error reporting)
+ self.blank_log_formatter = logging.Formatter('%(message)s')
+
+ # Setup the default log handler and inherit user defined formatting on
+ # the root_logger.
+ self.apply_root_log_formatting()
+
+ # Create a root logfile to save what is normally logged to stdout.
+ if root_logfile:
+ # Execute subprocesses and capture logs
+ self.execute_command = execute_command_with_logging
+
+ root_logfile.parent.mkdir(parents=True, exist_ok=True)
+
+ build_log_filehandler = logging.FileHandler(
+ root_logfile,
+ encoding='utf-8',
+ # Truncate the file
+ mode='w',
+ )
+ build_log_filehandler.setLevel(log_level)
+ build_log_filehandler.setFormatter(self.dispatching_log_formatter)
+ root_logger.addHandler(build_log_filehandler)
+
+ # Set each recipe to use the root logger by default.
+ for recipe in self.build_recipes:
+ recipe.set_logger(root_logger)
+
+ # Create separate logfiles per build
+ if separate_build_file_logging:
+ self._create_per_build_logfiles()
+
+ def _create_per_build_logfiles(self) -> None:
+ """Create separate log files per build.
+
+ If a root logfile is used, create per build log files in the same
+ location. If no root logfile is specified create the per build log files
+ in the build dir as ``log.txt``
+ """
+ self.execute_command = execute_command_with_logging
+
+ for recipe in self.build_recipes:
+ sub_logger_name = recipe.display_name.replace('.', '_')
+ new_logger = logging.getLogger(
+ f'{self.root_logger.name}.{sub_logger_name}'
+ )
+ new_logger.setLevel(self.default_log_level)
+ new_logger.propagate = self.send_recipe_logs_to_root
+
+ new_logfile_dir = recipe.build_dir
+ new_logfile_name = Path('log.txt')
+ new_logfile_postfix = ''
+ if self.default_logfile:
+ new_logfile_dir = self.default_logfile.parent
+ new_logfile_name = self.default_logfile
+ new_logfile_postfix = '_' + recipe.display_name.replace(
+ ' ', '_'
+ )
+
+ new_logfile = new_logfile_dir / (
+ new_logfile_name.stem
+ + new_logfile_postfix
+ + new_logfile_name.suffix
+ )
+
+ new_logfile_dir.mkdir(parents=True, exist_ok=True)
+ new_log_filehandler = logging.FileHandler(
+ new_logfile,
+ encoding='utf-8',
+ # Truncate the file
+ mode='w',
+ )
+ new_log_filehandler.setLevel(self.default_log_level)
+ new_log_filehandler.setFormatter(self.dispatching_log_formatter)
+ new_logger.addHandler(new_log_filehandler)
+
+ recipe.set_logger(new_logger)
+ recipe.set_logfile(new_logfile)
+
+ def apply_root_log_formatting(self) -> None:
+ """Inherit user defined formatting from the root_logger."""
+ # Use the the existing root logger formatter if one exists.
+ for handler in logging.getLogger().handlers:
+ if handler.formatter:
+ self.default_log_formatter = handler.formatter
+ break
+
+ formatter_mapping = {
+ self.root_logger.name: self.default_log_formatter,
+ }
+ if self.use_verbatim_error_log_formatting:
+ formatter_mapping[self.error_logger.name] = self.blank_log_formatter
+
+ self.dispatching_log_formatter = DispatchingFormatter(
+ formatter_mapping,
+ self.default_log_formatter,
+ )
+
+ def should_use_progress_bars(self) -> bool:
+ if not self.allow_progress_bars:
+ return False
+ if self.separate_build_file_logging or self.default_logfile:
+ return True
+ return False
+
+ def use_stdout_proxy(self) -> None:
+ """Setup StdoutProxy for progress bars."""
+
+ self.stdout_proxy = StdoutProxy(raw=True)
+ root_logger = logging.getLogger()
+ handlers = root_logger.handlers + self.error_logger.handlers
+
+ for handler in handlers:
+ # Must use type() check here since this returns True:
+ # isinstance(logging.FileHandler, logging.StreamHandler)
+ # pylint: disable=unidiomatic-typecheck
+ if type(handler) == logging.StreamHandler:
+ handler.setStream(self.stdout_proxy) # type: ignore
+ handler.setFormatter(self.dispatching_log_formatter)
+ # pylint: enable=unidiomatic-typecheck
+
+ def flush_log_handlers(self) -> None:
+ root_logger = logging.getLogger()
+ handlers = root_logger.handlers + self.error_logger.handlers
+ for cfg in self:
+ handlers.extend(cfg.log.handlers)
+ for handler in handlers:
+ handler.flush()
+ if self.stdout_proxy:
+ self.stdout_proxy.flush()
+ self.stdout_proxy.close()
+
+ def __len__(self) -> int:
+ return len(self.build_recipes)
+
+ def __getitem__(self, index: int) -> BuildRecipe:
+ return self.build_recipes[index]
+
+ def __iter__(self) -> Generator[BuildRecipe, None, None]:
+ return (build_recipe for build_recipe in self.build_recipes)
+
+ def run_build(
+ self,
+ cfg: BuildRecipe,
+ env: Dict,
+ index_message: Optional[str] = '',
+ ) -> bool:
+ """Run a single build config."""
+ if BUILDER_CONTEXT.build_stopping():
+ return False
+
+ if self.colors:
+ # Force colors in Pigweed subcommands run through the watcher.
+ env['PW_USE_COLOR'] = '1'
+ # Force Ninja to output ANSI colors
+ env['CLICOLOR_FORCE'] = '1'
+
+ build_succeded = False
+ cfg.reset_status()
+ cfg.status.mark_started()
+ for command_step in cfg.steps:
+ command_args = command_step.get_args(
+ additional_ninja_args=self.extra_ninja_args,
+ additional_bazel_args=self.extra_bazel_args,
+ additional_bazel_build_args=self.extra_bazel_build_args,
+ )
+
+ # Verify that the build output directories exist.
+ if (
+ command_step.build_system_command is not None
+ and cfg.build_dir
+ and (not cfg.build_dir.is_dir())
+ ):
+ self.abort_callback(
+ 'Build directory does not exist: %s', cfg.build_dir
+ )
+
+ quoted_command_args = ' '.join(
+ shlex.quote(arg) for arg in command_args
+ )
+ build_succeded = True
+ if command_step.should_run():
+ cfg.log.info(
+ '%s %s %s',
+ index_message,
+ self.color.blue('Run ==>'),
+ quoted_command_args,
+ )
+ build_succeded = self.execute_command(
+ command_args, env, cfg, cfg.log, None
+ )
+ else:
+ cfg.log.info(
+ '%s %s %s',
+ index_message,
+ self.color.yellow('Skipped ==>'),
+ quoted_command_args,
+ )
+
+ BUILDER_CONTEXT.mark_progress_step_complete(cfg)
+ # Don't run further steps if a command fails.
+ if not build_succeded:
+ break
+
+ # If all steps were skipped the return code will not be set. Force
+ # status to passed in this case.
+ if build_succeded and not cfg.status.passed():
+ cfg.status.set_passed()
+
+ cfg.status.mark_done()
+
+ return build_succeded
+
+ def print_pass_fail_banner(
+ self,
+ cancelled: bool = False,
+ logger: logging.Logger = _LOG,
+ ) -> None:
+ # Check conditions where banners should not be shown:
+ # Banner flag disabled.
+ if not self.banners:
+ return
+ # If restarting or interrupted.
+ if BUILDER_CONTEXT.interrupted():
+ if BUILDER_CONTEXT.ctrl_c_pressed:
+ _LOG.info(
+ self.color.yellow('Exited due to keyboard interrupt.')
+ )
+ return
+ # If any build is still pending.
+ if any(recipe.status.pending() for recipe in self):
+ return
+
+ # Show a large color banner for the overall result.
+ if all(recipe.status.passed() for recipe in self) and not cancelled:
+ for line in PASS_MESSAGE.splitlines():
+ logger.info(self.color.green(line))
+ else:
+ for line in FAIL_MESSAGE.splitlines():
+ logger.info(self.color.red(line))
+
+ def print_build_summary(
+ self,
+ cancelled: bool = False,
+ logger: logging.Logger = _LOG,
+ ) -> None:
+ """Print build status summary table."""
+
+ build_descriptions = []
+ build_status = []
+
+ for cfg in self:
+ description = [str(cfg.display_name).ljust(self.max_name_width)]
+ description.append(' '.join(cfg.targets()))
+ build_descriptions.append(' '.join(description))
+
+ if cfg.status.passed():
+ build_status.append(self.charset.slug_ok)
+ elif cfg.status.failed():
+ build_status.append(self.charset.slug_fail)
+ else:
+ build_status.append(self.charset.slug_building)
+
+ if not cancelled:
+ logger.info(' ╔════════════════════════════════════')
+ logger.info(' ║')
+
+ for slug, cmd in zip(build_status, build_descriptions):
+ logger.info(' ║ %s %s', slug, cmd)
+
+ logger.info(' ║')
+ logger.info(" ╚════════════════════════════════════")
+ else:
+ # Build was interrupted.
+ logger.info('')
+ logger.info(' ╔════════════════════════════════════')
+ logger.info(' ║')
+ logger.info(' ║ %s- interrupted', self.charset.slug_fail)
+ logger.info(' ║')
+ logger.info(" ╚════════════════════════════════════")
+
+
+def run_recipe(
+ index: int, project_builder: ProjectBuilder, cfg: BuildRecipe, env
+) -> bool:
+ if BUILDER_CONTEXT.interrupted():
+ return False
+ if not cfg.enabled:
+ return False
+
+ num_builds = len(project_builder)
+ index_message = f'[{index}/{num_builds}]'
+
+ result = False
+
+ log_build_recipe_start(index_message, project_builder, cfg)
+
+ result = project_builder.run_build(cfg, env, index_message=index_message)
+
+ log_build_recipe_finish(index_message, project_builder, cfg)
+
+ return result
+
+
+def run_builds(project_builder: ProjectBuilder, workers: int = 1) -> int:
+ """Execute build steps in the ProjectBuilder and print a summary.
+
+ Returns: 1 for a failed build, 0 for success."""
+ num_builds = len(project_builder)
+ _LOG.info('Starting build with %d directories', num_builds)
+ if project_builder.default_logfile:
+ _LOG.info(
+ '%s %s',
+ project_builder.color.blue('Root logfile:'),
+ project_builder.default_logfile.resolve(),
+ )
+
+ env = os.environ.copy()
+
+ # Print status before starting
+ if not project_builder.should_use_progress_bars():
+ project_builder.print_build_summary()
+ project_builder.print_pass_fail_banner()
+
+ if workers > 1 and not project_builder.separate_build_file_logging:
+ _LOG.warning(
+ project_builder.color.yellow(
+ 'Running in parallel without --separate-logfiles; All build '
+ 'output will be interleaved.'
+ )
+ )
+
+ BUILDER_CONTEXT.set_project_builder(project_builder)
+ BUILDER_CONTEXT.set_building()
+
+ def _cleanup() -> None:
+ if not project_builder.should_use_progress_bars():
+ project_builder.print_build_summary()
+ project_builder.print_pass_fail_banner()
+ project_builder.flush_log_handlers()
+ BUILDER_CONTEXT.set_idle()
+ BUILDER_CONTEXT.exit_progress()
+
+ if workers == 1:
+ # TODO(tonymd): Try to remove this special case. Using
+ # ThreadPoolExecutor when running in serial (workers==1) currently
+ # breaks Ctrl-C handling. Build processes keep running.
+ try:
+ if project_builder.should_use_progress_bars():
+ BUILDER_CONTEXT.add_progress_bars()
+ for i, cfg in enumerate(project_builder, start=1):
+ run_recipe(i, project_builder, cfg, env)
+ # Ctrl-C on Unix generates KeyboardInterrupt
+ # Ctrl-Z on Windows generates EOFError
+ except (KeyboardInterrupt, EOFError):
+ _exit_due_to_interrupt()
+ finally:
+ _cleanup()
+
+ else:
+ with concurrent.futures.ThreadPoolExecutor(
+ max_workers=workers
+ ) as executor:
+ futures = []
+ for i, cfg in enumerate(project_builder, start=1):
+ futures.append(
+ executor.submit(run_recipe, i, project_builder, cfg, env)
+ )
+
+ try:
+ if project_builder.should_use_progress_bars():
+ BUILDER_CONTEXT.add_progress_bars()
+ for future in concurrent.futures.as_completed(futures):
+ future.result()
+ # Ctrl-C on Unix generates KeyboardInterrupt
+ # Ctrl-Z on Windows generates EOFError
+ except (KeyboardInterrupt, EOFError):
+ _exit_due_to_interrupt()
+ finally:
+ _cleanup()
+
+ project_builder.flush_log_handlers()
+ return BUILDER_CONTEXT.exit_code()
+
+
+def main() -> int:
+ """Build a Pigweed Project."""
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser = add_project_builder_arguments(parser)
+ args = parser.parse_args()
+
+ pw_env = pw_cli.env.pigweed_environment()
+ if pw_env.PW_EMOJI:
+ charset = EMOJI_CHARSET
+ else:
+ charset = ASCII_CHARSET
+
+ prefs = ProjectBuilderPrefs(
+ load_argparse_arguments=add_project_builder_arguments
+ )
+ prefs.apply_command_line_args(args)
+ build_recipes = create_build_recipes(prefs)
+
+ log_level = logging.DEBUG if args.debug_logging else logging.INFO
+
+ pw_cli.log.install(
+ level=log_level,
+ use_color=args.colors,
+ hide_timestamp=False,
+ )
+
+ project_builder = ProjectBuilder(
+ build_recipes=build_recipes,
+ jobs=args.jobs,
+ banners=args.banners,
+ keep_going=args.keep_going,
+ colors=args.colors,
+ charset=charset,
+ separate_build_file_logging=args.separate_logfiles,
+ root_logfile=args.logfile,
+ root_logger=_LOG,
+ log_level=log_level,
+ )
+
+ if project_builder.should_use_progress_bars():
+ project_builder.use_stdout_proxy()
+
+ workers = 1
+ if args.parallel:
+ # If parallel is requested and parallel_workers is set to 0 run all
+ # recipes in parallel. That is, use the number of recipes as the worker
+ # count.
+ if args.parallel_workers == 0:
+ workers = len(project_builder)
+ else:
+ workers = args.parallel_workers
+
+ return run_builds(project_builder, workers)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_build/py/pw_build/project_builder_argparse.py b/pw_build/py/pw_build/project_builder_argparse.py
new file mode 100644
index 000000000..18581eb66
--- /dev/null
+++ b/pw_build/py/pw_build/project_builder_argparse.py
@@ -0,0 +1,166 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Pigweed Project Builder Common argparse."""
+
+import argparse
+from pathlib import Path
+
+
+def add_project_builder_arguments(
+ parser: argparse.ArgumentParser,
+) -> argparse.ArgumentParser:
+ """Add ProjectBuilder.main specific arguments."""
+ build_dir_group = parser.add_argument_group(
+ title='Build Directory and Command Options'
+ )
+ build_dir_group.add_argument(
+ '-C',
+ '--build-directory',
+ dest='build_directories',
+ nargs='+',
+ action='append',
+ default=[],
+ metavar=('directory', 'target'),
+ help=(
+ "Specify a build directory and optionally targets to "
+ "build. `pw watch -C out target1 target2` is equivalent to 'ninja "
+ "-C out taret1 target2'. The 'out' directory will be used if no "
+ "others are provided."
+ ),
+ )
+
+ build_dir_group.add_argument(
+ 'default_build_targets',
+ nargs='*',
+ metavar='target',
+ default=[],
+ help=(
+ "Default build targets. For example if the build directory is "
+ "'out' then, 'ninja -C out taret1 target2' will be run. To "
+ "specify one or more directories, use the "
+ "``-C / --build-directory`` option."
+ ),
+ )
+
+ build_dir_group.add_argument(
+ '--build-system-command',
+ nargs=2,
+ action='append',
+ default=[],
+ dest='build_system_commands',
+ metavar=('directory', 'command'),
+ help='Build system command for . Default: ninja',
+ )
+
+ build_dir_group.add_argument(
+ '--run-command',
+ action='append',
+ default=[],
+ help=(
+ 'Additional commands to run. These are run before any -C '
+ 'arguments and may be repeated. For example: '
+ "--run-command 'bazel build //pw_cli/...'"
+ "--run-command 'bazel test //pw_cli/...'"
+ "-C out python.lint python.test"
+ ),
+ )
+
+ build_options_group = parser.add_argument_group(
+ title='Build Execution Options'
+ )
+ build_options_group.add_argument(
+ '-j',
+ '--jobs',
+ type=int,
+ help=(
+ 'Specify the number of cores to use for each build system.'
+ 'This is passed to ninja, bazel and make as "-j"'
+ ),
+ )
+ build_options_group.add_argument(
+ '-k',
+ '--keep-going',
+ action='store_true',
+ help=(
+ 'Keep building past the first failure. This is equivalent to '
+ 'running "ninja -k 0" or "bazel build -k".'
+ ),
+ )
+ build_options_group.add_argument(
+ '--parallel',
+ action='store_true',
+ help='Run all builds in parallel.',
+ )
+ build_options_group.add_argument(
+ '--parallel-workers',
+ default=0,
+ type=int,
+ help=(
+ 'How many builds may run at the same time when --parallel is '
+ 'enabled. Default: 0 meaning run all in parallel.'
+ ),
+ )
+
+ logfile_group = parser.add_argument_group(title='Log File Options')
+ logfile_group.add_argument(
+ '--logfile',
+ type=Path,
+ help='Global build output log file.',
+ )
+
+ logfile_group.add_argument(
+ '--separate-logfiles',
+ action='store_true',
+ help='Create separate log files per build directory.',
+ )
+
+ logfile_group.add_argument(
+ '--debug-logging',
+ action='store_true',
+ help='Enable Python build execution tool debug logging.',
+ )
+
+ output_group = parser.add_argument_group(title='Display Output Options')
+
+ # TODO(b/248257406) Use argparse.BooleanOptionalAction when Python 3.8 is
+ # no longer supported.
+ output_group.add_argument(
+ '--banners',
+ action='store_true',
+ default=True,
+ help='Show pass/fail banners.',
+ )
+ output_group.add_argument(
+ '--no-banners',
+ action='store_false',
+ dest='banners',
+ help='Hide pass/fail banners.',
+ )
+
+ # TODO(b/248257406) Use argparse.BooleanOptionalAction when Python 3.8 is
+ # no longer supported.
+ output_group.add_argument(
+ '--colors',
+ action='store_true',
+ default=True,
+ help='Force color output from ninja.',
+ )
+ output_group.add_argument(
+ '--no-colors',
+ action='store_false',
+ dest='colors',
+ help="Don't force ninja to use color output.",
+ )
+
+ return parser
diff --git a/pw_build/py/pw_build/project_builder_context.py b/pw_build/py/pw_build/project_builder_context.py
new file mode 100644
index 000000000..f45942351
--- /dev/null
+++ b/pw_build/py/pw_build/project_builder_context.py
@@ -0,0 +1,466 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Fetch active Project Builder Context."""
+
+import asyncio
+import concurrent.futures
+from contextvars import ContextVar
+from datetime import datetime
+from dataclasses import dataclass, field
+from enum import Enum
+import logging
+import os
+import subprocess
+from typing import Callable, Dict, List, Optional, NoReturn, TYPE_CHECKING
+
+from prompt_toolkit.formatted_text import (
+ AnyFormattedText,
+ StyleAndTextTuples,
+)
+from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.layout.dimension import AnyDimension, D
+from prompt_toolkit.layout import (
+ AnyContainer,
+ DynamicContainer,
+ FormattedTextControl,
+ Window,
+)
+from prompt_toolkit.shortcuts import ProgressBar, ProgressBarCounter
+from prompt_toolkit.shortcuts.progress_bar.formatters import (
+ Formatter,
+ TimeElapsed,
+)
+from prompt_toolkit.shortcuts.progress_bar import formatters
+
+from pw_build.build_recipe import BuildRecipe
+
+if TYPE_CHECKING:
+ from pw_build.project_builder import ProjectBuilder
+
+_LOG = logging.getLogger('pw_build.watch')
+
+
+def _wait_for_terminate_then_kill(
+ proc: subprocess.Popen, timeout: int = 5
+) -> int:
+ """Wait for a process to end, then kill it if the timeout expires."""
+ returncode = 1
+ try:
+ returncode = proc.wait(timeout=timeout)
+ except subprocess.TimeoutExpired:
+ proc_command = proc.args
+ if isinstance(proc.args, list):
+ proc_command = ' '.join(proc.args)
+ _LOG.debug('Killing %s', proc_command)
+ proc.kill()
+ return returncode
+
+
+class ProjectBuilderState(Enum):
+ IDLE = 'IDLE'
+ BUILDING = 'BUILDING'
+ ABORT = 'ABORT'
+
+
+# pylint: disable=unused-argument
+# Prompt Toolkit progress bar formatter classes follow:
+class BuildStatus(Formatter):
+ """Return OK/FAIL status and last output line for each build."""
+
+ def __init__(self, ctx) -> None:
+ self.ctx = ctx
+
+ def format(
+ self,
+ progress_bar: ProgressBar,
+ progress: ProgressBarCounter,
+ width: int,
+ ) -> AnyFormattedText:
+ for cfg in self.ctx.recipes:
+ if cfg.display_name != progress.label:
+ continue
+
+ build_status: StyleAndTextTuples = []
+ build_status.append(
+ cfg.status.status_slug(restarting=self.ctx.restart_flag)
+ )
+ build_status.append(('', ' '))
+ build_status.extend(cfg.status.current_step_formatted())
+
+ return build_status
+
+ return [('', '')]
+
+ def get_width( # pylint: disable=no-self-use
+ self, progress_bar: ProgressBar
+ ) -> AnyDimension:
+ return D()
+
+
+class TimeElapsedIfStarted(TimeElapsed):
+ """Display the elapsed time if the build has started."""
+
+ def __init__(self, ctx) -> None:
+ self.ctx = ctx
+
+ def format(
+ self,
+ progress_bar: ProgressBar,
+ progress: ProgressBarCounter,
+ width: int,
+ ) -> AnyFormattedText:
+ formatted_text: StyleAndTextTuples = [('', '')]
+ for cfg in self.ctx.recipes:
+ if cfg.display_name != progress.label:
+ continue
+ if cfg.status.started:
+ return super().format(progress_bar, progress, width)
+ return formatted_text
+
+
+# pylint: enable=unused-argument
+
+
+@dataclass
+class ProjectBuilderContext: # pylint: disable=too-many-instance-attributes,too-many-public-methods
+ """Maintains the state of running builds and active subproccesses."""
+
+ current_state: ProjectBuilderState = ProjectBuilderState.IDLE
+ desired_state: ProjectBuilderState = ProjectBuilderState.BUILDING
+ procs: Dict[BuildRecipe, subprocess.Popen] = field(default_factory=dict)
+ recipes: List[BuildRecipe] = field(default_factory=list)
+
+ def __post_init__(self) -> None:
+ self.project_builder: Optional['ProjectBuilder'] = None
+
+ self.progress_bar_formatters = [
+ formatters.Text(' '),
+ formatters.Label(),
+ formatters.Text(' '),
+ BuildStatus(self),
+ formatters.Text(' '),
+ TimeElapsedIfStarted(self),
+ formatters.Text(' '),
+ ]
+
+ self._enter_callback: Optional[Callable] = None
+
+ key_bindings = KeyBindings()
+
+ @key_bindings.add('enter')
+ def _enter_pressed(_event):
+ """Run enter press function."""
+ if self._enter_callback:
+ self._enter_callback()
+
+ self.key_bindings = key_bindings
+
+ self.progress_bar: Optional[ProgressBar] = None
+
+ self._progress_bar_started: bool = False
+
+ self.bottom_toolbar: AnyFormattedText = None
+ self.horizontal_separator = '━'
+ self.title_bar_container: AnyContainer = Window(
+ char=self.horizontal_separator, height=1
+ )
+
+ self.using_fullscreen: bool = False
+ self.restart_flag: bool = False
+ self.ctrl_c_pressed: bool = False
+
+ def using_progress_bars(self) -> bool:
+ return bool(self.progress_bar) or self.using_fullscreen
+
+ def interrupted(self) -> bool:
+ return self.ctrl_c_pressed or self.restart_flag
+
+ def set_bottom_toolbar(self, text: AnyFormattedText) -> None:
+ self.bottom_toolbar = text
+
+ def set_enter_callback(self, callback: Callable) -> None:
+ self._enter_callback = callback
+
+ def ctrl_c_interrupt(self) -> None:
+ """Abort function for when using ProgressBars."""
+ self.ctrl_c_pressed = True
+ self.exit(1)
+
+ def startup_progress(self) -> None:
+ self.progress_bar = ProgressBar(
+ formatters=self.progress_bar_formatters,
+ key_bindings=self.key_bindings,
+ title=self.get_title_bar_text,
+ bottom_toolbar=self.bottom_toolbar,
+ cancel_callback=self.ctrl_c_interrupt,
+ )
+ self.progress_bar.__enter__() # pylint: disable=unnecessary-dunder-call
+
+ self.create_title_bar_container()
+ self.progress_bar.app.layout.container.children[ # type: ignore
+ 0
+ ] = DynamicContainer(lambda: self.title_bar_container)
+ self._progress_bar_started = True
+
+ def exit_progress(self) -> None:
+ if not self.progress_bar:
+ return
+ self.progress_bar.__exit__() # pylint: disable=unnecessary-dunder-call
+
+ def clear_progress_scrollback(self) -> None:
+ if not self.progress_bar:
+ return
+ self.progress_bar._app_loop.call_soon_threadsafe( # pylint: disable=protected-access
+ self.progress_bar.app.renderer.clear
+ )
+
+ def redraw_progress(self) -> None:
+ if not self.progress_bar:
+ return
+ if hasattr(self.progress_bar, 'app'):
+ self.progress_bar.invalidate()
+
+ def get_title_style(self) -> str:
+ if self.restart_flag:
+ return 'fg:ansiyellow'
+
+ # Assume passing
+ style = 'fg:ansigreen'
+
+ if self.current_state == ProjectBuilderState.BUILDING:
+ style = 'fg:ansiyellow'
+
+ for cfg in self.recipes:
+ if cfg.status.failed():
+ style = 'fg:ansired'
+
+ return style
+
+ def exit_code(self) -> int:
+ """Returns a 0 for success, 1 for fail."""
+ for cfg in self.recipes:
+ if cfg.status.failed():
+ return 1
+ return 0
+
+ def get_title_bar_text(
+ self, include_separators: bool = True
+ ) -> StyleAndTextTuples:
+ title = ''
+
+ fail_count = 0
+ done_count = 0
+ for cfg in self.recipes:
+ if cfg.status.failed():
+ fail_count += 1
+ if cfg.status.done:
+ done_count += 1
+
+ if self.restart_flag:
+ title = 'INTERRUPT'
+ elif fail_count > 0:
+ title = f'FAILED ({fail_count})'
+ elif self.current_state == ProjectBuilderState.IDLE and done_count > 0:
+ title = 'PASS'
+ else:
+ title = self.current_state.name
+
+ prefix = ''
+ if include_separators:
+ prefix += f'{self.horizontal_separator}{self.horizontal_separator} '
+
+ return [(self.get_title_style(), f'{prefix}{title} ')]
+
+ def create_title_bar_container(self) -> None:
+ title_text = FormattedTextControl(self.get_title_bar_text)
+ self.title_bar_container = Window(
+ title_text,
+ char=self.horizontal_separator,
+ height=1,
+ # Expand width to max available space
+ dont_extend_width=False,
+ style=self.get_title_style,
+ )
+
+ def add_progress_bars(self) -> None:
+ if not self._progress_bar_started:
+ self.startup_progress()
+ assert self.progress_bar
+ self.clear_progress_bars()
+ for cfg in self.recipes:
+ self.progress_bar(label=cfg.display_name, total=len(cfg.steps))
+
+ def clear_progress_bars(self) -> None:
+ if not self.progress_bar:
+ return
+ self.progress_bar.counters = []
+
+ def mark_progress_step_complete(self, recipe: BuildRecipe) -> None:
+ if not self.progress_bar:
+ return
+ for pbc in self.progress_bar.counters:
+ if pbc.label == recipe.display_name:
+ pbc.item_completed()
+ break
+
+ def mark_progress_done(self, recipe: BuildRecipe) -> None:
+ if not self.progress_bar:
+ return
+
+ for pbc in self.progress_bar.counters:
+ if pbc.label == recipe.display_name:
+ pbc.done = True
+ break
+
+ def mark_progress_started(self, recipe: BuildRecipe) -> None:
+ if not self.progress_bar:
+ return
+
+ for pbc in self.progress_bar.counters:
+ if pbc.label == recipe.display_name:
+ pbc.start_time = datetime.now()
+ break
+
+ def register_process(
+ self, recipe: BuildRecipe, proc: subprocess.Popen
+ ) -> None:
+ self.procs[recipe] = proc
+
+ def terminate_and_wait(
+ self,
+ exit_message: Optional[str] = None,
+ ) -> None:
+ """End a subproces either cleanly or with a kill signal."""
+ if self.is_idle() or self.should_abort():
+ return
+
+ self._signal_abort()
+
+ with concurrent.futures.ThreadPoolExecutor(
+ max_workers=len(self.procs)
+ ) as executor:
+ futures = []
+ for _recipe, proc in self.procs.items():
+ if proc is None:
+ continue
+
+ proc_command = proc.args
+ if isinstance(proc.args, list):
+ proc_command = ' '.join(proc.args)
+ _LOG.debug('Stopping: %s', proc_command)
+
+ futures.append(
+ executor.submit(_wait_for_terminate_then_kill, proc)
+ )
+ for future in concurrent.futures.as_completed(futures):
+ future.result()
+
+ if exit_message:
+ _LOG.info(exit_message)
+ self.set_idle()
+
+ def _signal_abort(self) -> None:
+ self.desired_state = ProjectBuilderState.ABORT
+
+ def build_stopping(self) -> bool:
+ """Return True if the build is restarting or quitting."""
+ return self.should_abort() or self.interrupted()
+
+ def should_abort(self) -> bool:
+ """Return True if the build is restarting."""
+ return self.desired_state == ProjectBuilderState.ABORT
+
+ def is_building(self) -> bool:
+ return self.current_state == ProjectBuilderState.BUILDING
+
+ def is_idle(self) -> bool:
+ return self.current_state == ProjectBuilderState.IDLE
+
+ def set_project_builder(self, project_builder) -> None:
+ self.project_builder = project_builder
+ self.recipes = project_builder.build_recipes
+
+ def set_idle(self) -> None:
+ self.current_state = ProjectBuilderState.IDLE
+ self.desired_state = ProjectBuilderState.IDLE
+
+ def set_building(self) -> None:
+ self.restart_flag = False
+ self.current_state = ProjectBuilderState.BUILDING
+ self.desired_state = ProjectBuilderState.BUILDING
+
+ def restore_stdout_logging(self) -> None: # pylint: disable=no-self-use
+ if not self.using_progress_bars():
+ return
+
+ # Restore logging to STDOUT
+ stdout_handler = logging.StreamHandler()
+ if self.project_builder:
+ stdout_handler.setLevel(self.project_builder.default_log_level)
+ else:
+ stdout_handler.setLevel(logging.INFO)
+ root_logger = logging.getLogger()
+ if self.project_builder and self.project_builder.stdout_proxy:
+ self.project_builder.stdout_proxy.flush()
+ self.project_builder.stdout_proxy.close()
+ root_logger.addHandler(stdout_handler)
+
+ def restore_logging_and_shutdown(
+ self,
+ log_after_shutdown: Optional[Callable[[], None]] = None,
+ ) -> None:
+ self.restore_stdout_logging()
+ _LOG.warning('Abort signal recieved, stopping processes...')
+ if log_after_shutdown:
+ log_after_shutdown()
+ self.terminate_and_wait()
+ # Flush all log handlers
+ # logging.shutdown()
+
+ def exit(
+ self,
+ exit_code: int = 1,
+ log_after_shutdown: Optional[Callable[[], None]] = None,
+ ) -> None:
+ """Exit function called when the user presses ctrl-c."""
+
+ # Note: The correct way to exit Python is via sys.exit() however this
+ # takes a number of seconds when running pw_watch with multiple parallel
+ # builds. Instead, this function calls os._exit() to shutdown
+ # immediately. This is similar to `pw_watch.watch._exit`:
+ # https://cs.opensource.google/pigweed/pigweed/+/main:pw_watch/py/pw_watch/watch.py?q=_exit.code
+
+ if not self.progress_bar:
+ self.restore_logging_and_shutdown(log_after_shutdown)
+ logging.shutdown()
+ os._exit(exit_code) # pylint: disable=protected-access
+
+ # Shut everything down after the progress_bar exits.
+ def _really_exit(future: asyncio.Future) -> NoReturn:
+ self.restore_logging_and_shutdown(log_after_shutdown)
+ logging.shutdown()
+ os._exit(future.result()) # pylint: disable=protected-access
+
+ if self.progress_bar.app.future:
+ self.progress_bar.app.future.add_done_callback(_really_exit)
+ self.progress_bar.app.exit(result=exit_code) # type: ignore
+
+
+PROJECT_BUILDER_CONTEXTVAR = ContextVar(
+ 'pw_build_project_builder_state', default=ProjectBuilderContext()
+)
+
+
+def get_project_builder_context():
+ return PROJECT_BUILDER_CONTEXTVAR.get()
diff --git a/pw_build/py/pw_build/project_builder_prefs.py b/pw_build/py/pw_build/project_builder_prefs.py
new file mode 100644
index 000000000..a13534d9c
--- /dev/null
+++ b/pw_build/py/pw_build/project_builder_prefs.py
@@ -0,0 +1,190 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Pigweed Watch config file preferences loader."""
+
+import argparse
+import copy
+import shlex
+from pathlib import Path
+from typing import Any, Callable, Dict, List, Tuple, Union
+
+from pw_cli.toml_config_loader_mixin import YamlConfigLoaderMixin
+
+_DEFAULT_CONFIG: Dict[Any, Any] = {
+ # Config settings not available as a command line options go here.
+ 'build_system_commands': {
+ 'default': {
+ 'commands': [
+ {
+ 'command': 'ninja',
+ 'extra_args': [],
+ },
+ ],
+ },
+ },
+}
+
+_DEFAULT_PROJECT_FILE = Path('$PW_PROJECT_ROOT/.pw_build.yaml')
+_DEFAULT_PROJECT_USER_FILE = Path('$PW_PROJECT_ROOT/.pw_build.user.yaml')
+_DEFAULT_USER_FILE = Path('$HOME/.pw_build.yaml')
+
+
+def load_defaults_from_argparse(
+ add_parser_arguments: Callable[
+ [argparse.ArgumentParser], argparse.ArgumentParser
+ ]
+) -> Dict[Any, Any]:
+ parser = argparse.ArgumentParser(
+ description='', formatter_class=argparse.RawDescriptionHelpFormatter
+ )
+ parser = add_parser_arguments(parser)
+ default_namespace, _unknown_args = parser.parse_known_args(
+ [], # Pass in blank arguments to avoid catching args from sys.argv.
+ )
+ defaults_flags = vars(default_namespace)
+ return defaults_flags
+
+
+class ProjectBuilderPrefs(YamlConfigLoaderMixin):
+ """Pigweed Watch preferences storage class."""
+
+ def __init__(
+ self,
+ load_argparse_arguments: Callable[
+ [argparse.ArgumentParser], argparse.ArgumentParser
+ ],
+ project_file: Union[Path, bool] = _DEFAULT_PROJECT_FILE,
+ project_user_file: Union[Path, bool] = _DEFAULT_PROJECT_USER_FILE,
+ user_file: Union[Path, bool] = _DEFAULT_USER_FILE,
+ ) -> None:
+ self.load_argparse_arguments = load_argparse_arguments
+
+ self.config_init(
+ config_section_title='pw_build',
+ project_file=project_file,
+ project_user_file=project_user_file,
+ user_file=user_file,
+ default_config=_DEFAULT_CONFIG,
+ environment_var='PW_BUILD_CONFIG_FILE',
+ )
+
+ def reset_config(self) -> None:
+ super().reset_config()
+ self._update_config(
+ load_defaults_from_argparse(self.load_argparse_arguments)
+ )
+
+ def _argparse_build_system_commands_to_prefs( # pylint: disable=no-self-use
+ self, argparse_input: List[List[str]]
+ ) -> Dict[str, Any]:
+ result = copy.copy(_DEFAULT_CONFIG['build_system_commands'])
+ for out_dir, command in argparse_input:
+ new_dir_spec = result.get(out_dir, {})
+ # Get existing commands list
+ new_commands = new_dir_spec.get('commands', [])
+
+ # Convert 'ninja -k 1' to 'ninja' and ['-k', '1']
+ extra_args = []
+ command_tokens = shlex.split(command)
+ if len(command_tokens) > 1:
+ extra_args = command_tokens[1:]
+ command = command_tokens[0]
+
+ # Append the command step
+ new_commands.append({'command': command, 'extra_args': extra_args})
+ new_dir_spec['commands'] = new_commands
+ result[out_dir] = new_dir_spec
+ return result
+
+ def apply_command_line_args(self, new_args: argparse.Namespace) -> None:
+ default_args = load_defaults_from_argparse(self.load_argparse_arguments)
+
+ # Only apply settings that differ from the defaults.
+ changed_settings: Dict[Any, Any] = {}
+ for key, value in vars(new_args).items():
+ if key in default_args and value != default_args[key]:
+ if key == 'build_system_commands':
+ value = self._argparse_build_system_commands_to_prefs(value)
+ changed_settings[key] = value
+
+ self._update_config(changed_settings)
+
+ @property
+ def run_commands(self) -> List[str]:
+ return self._config.get('run_command', [])
+
+ @property
+ def build_directories(self) -> Dict[str, List[str]]:
+ """Returns build directories and the targets to build in each."""
+ build_directories: Union[
+ List[str], Dict[str, List[str]]
+ ] = self._config.get('build_directories', {})
+ final_build_dirs: Dict[str, List[str]] = {}
+
+ if isinstance(build_directories, dict):
+ final_build_dirs = build_directories
+ else:
+ # Convert list style command line arg to dict
+ for build_dir in build_directories:
+ # build_dir should be a list of strings from argparse
+ assert isinstance(build_dir, list)
+ assert isinstance(build_dir[0], str)
+ build_dir_name = build_dir[0]
+ new_targets = build_dir[1:]
+ # Append new targets in case out dirs are repeated on the
+ # command line. For example:
+ # -C out python.tests -C out python.lint
+ existing_targets = final_build_dirs.get(build_dir_name, [])
+ existing_targets.extend(new_targets)
+ final_build_dirs[build_dir_name] = existing_targets
+
+ # If no build directory was specified fall back to 'out' with
+ # default_build_targets or empty targets. If run_commands were supplied,
+ # only run those by returning an empty final_build_dirs list.
+ if not final_build_dirs and not self.run_commands:
+ final_build_dirs['out'] = self._config.get(
+ 'default_build_targets', []
+ )
+
+ return final_build_dirs
+
+ def _get_build_system_commands_for(self, build_dir: str) -> Dict[str, Any]:
+ config_dict = self._config.get('build_system_commands', {})
+ if not config_dict:
+ config_dict = _DEFAULT_CONFIG['build_system_commands']
+ default_system_commands: Dict[str, Any] = config_dict.get('default', {})
+ if default_system_commands is None:
+ default_system_commands = {}
+ build_system_commands = config_dict.get(build_dir)
+
+ # In case 'out:' is in the config but has no contents.
+ if not build_system_commands:
+ return default_system_commands
+
+ return build_system_commands
+
+ def build_system_commands(
+ self, build_dir: str
+ ) -> List[Tuple[str, List[str]]]:
+ build_system_commands = self._get_build_system_commands_for(build_dir)
+
+ command_steps: List[Tuple[str, List[str]]] = []
+ commands: List[Dict[str, Any]] = build_system_commands.get(
+ 'commands', []
+ )
+ for command_step in commands:
+ command_steps.append(
+ (command_step['command'], command_step['extra_args'])
+ )
+ return command_steps
diff --git a/pw_build/py/pw_build/python_package.py b/pw_build/py/pw_build/python_package.py
index fa3be5407..d87de1faf 100644
--- a/pw_build/py/pw_build/python_package.py
+++ b/pw_build/py/pw_build/python_package.py
@@ -16,14 +16,34 @@
import configparser
from contextlib import contextmanager
import copy
-from dataclasses import dataclass
+from dataclasses import dataclass, asdict
+import io
import json
import os
from pathlib import Path
+import pprint
+import re
import shutil
-from typing import Dict, List, Optional, Iterable
+from typing import Any, Dict, List, Optional, Iterable
-import setuptools # type: ignore
+_pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
+
+# List of known environment markers supported by pip.
+# https://peps.python.org/pep-0508/#environment-markers
+_PY_REQUIRE_ENVIRONMENT_MARKER_NAMES = [
+ 'os_name',
+ 'sys_platform',
+ 'platform_machine',
+ 'platform_python_implementation',
+ 'platform_release',
+ 'platform_system',
+ 'platform_version',
+ 'python_version',
+ 'python_full_version',
+ 'implementation_name',
+ 'implementation_version',
+ 'extra',
+]
@contextmanager
@@ -36,6 +56,30 @@ def change_working_dir(directory: Path):
os.chdir(original_dir)
+class UnknownPythonPackageName(Exception):
+ """Exception thrown when a Python package_name cannot be determined."""
+
+
+class MissingSetupSources(Exception):
+ """Exception thrown when a Python package is missing setup source files.
+
+ For example: setup.cfg and pyproject.toml.i
+ """
+
+
+def _sanitize_install_requires(metadata_dict: dict) -> dict:
+ """Convert install_requires lists into strings joined with line breaks."""
+ try:
+ install_requires = metadata_dict['options']['install_requires']
+ if isinstance(install_requires, list):
+ metadata_dict['options']['install_requires'] = '\n'.join(
+ install_requires
+ )
+ except KeyError:
+ pass
+ return metadata_dict
+
+
@dataclass
class PythonPackage:
"""Class to hold a single Python package's metadata."""
@@ -44,7 +88,7 @@ class PythonPackage:
setup_sources: List[Path]
tests: List[Path]
inputs: List[Path]
- gn_target_name: Optional[str] = None
+ gn_target_name: str = ''
generate_setup: Optional[Dict] = None
config: Optional[configparser.ConfigParser] = None
@@ -55,9 +99,7 @@ class PythonPackage:
# Transform string filenames to Paths
for attribute in ['sources', 'tests', 'inputs', 'setup_sources']:
- transformed_kwargs[attribute] = [
- Path(s) for s in kwargs[attribute]
- ]
+ transformed_kwargs[attribute] = [Path(s) for s in kwargs[attribute]]
return PythonPackage(**transformed_kwargs)
@@ -67,15 +109,17 @@ class PythonPackage:
self.config = self._load_config()
@property
- def setup_dir(self) -> Path:
- assert len(self.setup_sources) > 0
+ def setup_dir(self) -> Optional[Path]:
+ if not self.setup_sources:
+ return None
# Assuming all setup_source files live in the same parent directory.
return self.setup_sources[0].parent
@property
def setup_py(self) -> Path:
setup_py = [
- setup_file for setup_file in self.setup_sources
+ setup_file
+ for setup_file in self.setup_sources
if str(setup_file).endswith('setup.py')
]
# setup.py will not exist for GN generated Python packages
@@ -83,78 +127,152 @@ class PythonPackage:
return setup_py[0]
@property
- def setup_cfg(self) -> Path:
+ def setup_cfg(self) -> Optional[Path]:
setup_cfg = [
- setup_file for setup_file in self.setup_sources
+ setup_file
+ for setup_file in self.setup_sources
if str(setup_file).endswith('setup.cfg')
]
- assert len(setup_cfg) == 1
+ if len(setup_cfg) < 1:
+ return None
return setup_cfg[0]
+ def as_dict(self) -> Dict[Any, Any]:
+ """Return a dict representation of this class."""
+ self_dict = asdict(self)
+ if self.config:
+ # Expand self.config into text.
+ setup_cfg_text = io.StringIO()
+ self.config.write(setup_cfg_text)
+ self_dict['config'] = setup_cfg_text.getvalue()
+ return self_dict
+
@property
def package_name(self) -> str:
- assert self.config
- return self.config['metadata']['name']
+ unknown_package_message = (
+ 'Cannot determine the package_name for the Python '
+ f'library/package: {self.gn_target_name}\n\n'
+ 'This could be due to a missing python dependency in GN for:\n'
+ f'{self.gn_target_name}\n\n'
+ )
+
+ if self.config:
+ try:
+ name = self.config['metadata']['name']
+ except KeyError:
+ raise UnknownPythonPackageName(
+ unknown_package_message + _pretty_format(self.as_dict())
+ )
+ return name
+ top_level_source_dir = self.top_level_source_dir
+ if top_level_source_dir:
+ return top_level_source_dir.name
+
+ actual_gn_target_name = self.gn_target_name.split(':')
+ if len(actual_gn_target_name) < 2:
+ raise UnknownPythonPackageName(unknown_package_message)
+
+ return actual_gn_target_name[-1]
@property
def package_dir(self) -> Path:
- return self.setup_cfg.parent / self.package_name
+ if self.setup_cfg and self.setup_cfg.is_file():
+ return self.setup_cfg.parent / self.package_name
+ root_source_dir = self.top_level_source_dir
+ if root_source_dir:
+ return root_source_dir
+ return self.sources[0].parent
+
+ @property
+ def top_level_source_dir(self) -> Optional[Path]:
+ source_dir_paths = sorted(
+ set((len(sfile.parts), sfile.parent) for sfile in self.sources),
+ key=lambda s: s[1],
+ )
+ if not source_dir_paths:
+ return None
+
+ top_level_source_dir = source_dir_paths[0][1]
+ if not top_level_source_dir.is_dir():
+ return None
+
+ return top_level_source_dir
def _load_config(self) -> Optional[configparser.ConfigParser]:
config = configparser.ConfigParser()
+
# Check for a setup.cfg and load that config.
if self.setup_cfg:
- with self.setup_cfg.open() as config_file:
- config.read_file(config_file)
+ if self.setup_cfg.is_file():
+ with self.setup_cfg.open() as config_file:
+ config.read_file(config_file)
+ return config
+ if self.setup_cfg.with_suffix('.json').is_file():
+ return self._load_setup_json_config()
+
+ # Fallback on the generate_setup scope from GN
+ if self.generate_setup:
+ config.read_dict(_sanitize_install_requires(self.generate_setup))
return config
return None
- def setuptools_build_with_base(self,
- build_base: Path,
- include_tests: bool = False) -> Path:
- # Create the lib install dir in case it doesn't exist.
- lib_dir_path = build_base / 'lib'
- lib_dir_path.mkdir(parents=True, exist_ok=True)
-
- starting_directory = Path.cwd()
- # cd to the location of setup.py
- with change_working_dir(self.setup_dir):
- # Run build with temp build-base location
- # Note: New files will be placed inside lib_dir_path
- setuptools.setup(script_args=[
- 'build',
- '--force',
- '--build-base',
- str(build_base),
- ])
-
- new_pkg_dir = lib_dir_path / self.package_name
- # If tests should be included, copy them to the tests dir
- if include_tests and self.tests:
- test_dir_path = new_pkg_dir / 'tests'
- test_dir_path.mkdir(parents=True, exist_ok=True)
-
- for test_source_path in self.tests:
- shutil.copy(starting_directory / test_source_path,
- test_dir_path)
-
- return lib_dir_path
-
- def setuptools_develop(self) -> None:
- with change_working_dir(self.setup_dir):
- setuptools.setup(script_args=['develop'])
-
- def setuptools_install(self) -> None:
- with change_working_dir(self.setup_dir):
- setuptools.setup(script_args=['install'])
-
-
-def load_packages(input_list_files: Iterable[Path]) -> List[PythonPackage]:
+ def _load_setup_json_config(self) -> configparser.ConfigParser:
+ assert self.setup_cfg
+ setup_json = self.setup_cfg.with_suffix('.json')
+ config = configparser.ConfigParser()
+ with setup_json.open() as json_fp:
+ json_dict = _sanitize_install_requires(json.load(json_fp))
+
+ config.read_dict(json_dict)
+ return config
+
+ def copy_sources_to(self, destination: Path) -> None:
+ """Copy this PythonPackage source files to another path."""
+ new_destination = destination / self.package_dir.name
+ new_destination.mkdir(parents=True, exist_ok=True)
+ shutil.copytree(self.package_dir, new_destination, dirs_exist_ok=True)
+
+ def install_requires_entries(self) -> List[str]:
+ """Convert the install_requires entry into a list of strings."""
+ this_requires: List[str] = []
+ # If there's no setup.cfg, do nothing.
+ if not self.config:
+ return this_requires
+
+ # Requires are delimited by newlines or semicolons.
+ # Split existing list on either one.
+ for req in re.split(
+ r' *[\n;] *', self.config['options']['install_requires']
+ ):
+ # Skip empty lines.
+ if not req:
+ continue
+ # Get the name part part of the dep, ignoring any spaces or
+ # other characters.
+ req_name_match = re.match(r'^(?P<name_part>[A-Za-z0-9_-]+)', req)
+ if not req_name_match:
+ continue
+ req_name = req_name_match.groupdict().get('name_part', '')
+ # Check if this is an environment marker.
+ if req_name in _PY_REQUIRE_ENVIRONMENT_MARKER_NAMES:
+ # Append this req as an environment marker for the previous
+ # requirement.
+ this_requires[-1] += f';{req}'
+ continue
+ # Normal pip requirement, save to this_requires.
+ this_requires.append(req)
+ return this_requires
+
+
+def load_packages(
+ input_list_files: Iterable[Path], ignore_missing=False
+) -> List[PythonPackage]:
"""Load Python package metadata and configs."""
packages = []
for input_path in input_list_files:
-
+ if ignore_missing and not input_path.is_file():
+ continue
with input_path.open() as input_file:
# Each line contains the path to a json file.
for json_file in input_file.readlines():
diff --git a/pw_build/py/pw_build/python_runner.py b/pw_build/py/pw_build/python_runner.py
index 8591c4ffc..2bd8ac5c6 100755
--- a/pw_build/py/pw_build/python_runner.py
+++ b/pw_build/py/pw_build/python_runner.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -19,50 +19,69 @@ the command.
import argparse
import atexit
-from dataclasses import dataclass
-import enum
+import json
import logging
import os
from pathlib import Path
-import re
+import platform
import shlex
import subprocess
import sys
import time
-from typing import Callable, Dict, Iterable, Iterator, List, NamedTuple
-from typing import Optional, Tuple
+from typing import List, Optional, Tuple
+
+try:
+ from pw_build import gn_resolver
+ from pw_build.python_package import load_packages
+except (ImportError, ModuleNotFoundError):
+ # Load from python_package from this directory if pw_build is not available.
+ from python_package import load_packages # type: ignore
+ import gn_resolver # type: ignore
if sys.platform != 'win32':
import fcntl # pylint: disable=import-error
+
# TODO(b/227670947): Support Windows.
_LOG = logging.getLogger(__name__)
_LOCK_ACQUISITION_TIMEOUT = 30 * 60 # 30 minutes in seconds
+# TODO(frolv): Remove these aliases once downstream projects are migrated.
+GnPaths = gn_resolver.GnPaths
+expand_expressions = gn_resolver.expand_expressions
+
def _parse_args() -> argparse.Namespace:
"""Parses arguments for this script, splitting out the command to run."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--gn-root',
- type=Path,
- required=True,
- help=('Path to the root of the GN tree; '
- 'value of rebase_path("//", root_build_dir)'))
- parser.add_argument('--current-path',
- type=Path,
- required=True,
- help='Value of rebase_path(".", root_build_dir)')
- parser.add_argument('--default-toolchain',
- required=True,
- help='Value of default_toolchain')
- parser.add_argument('--current-toolchain',
- required=True,
- help='Value of current_toolchain')
+ parser.add_argument(
+ '--gn-root',
+ type=Path,
+ required=True,
+ help=(
+ 'Path to the root of the GN tree; '
+ 'value of rebase_path("//", root_build_dir)'
+ ),
+ )
+ parser.add_argument(
+ '--current-path',
+ type=Path,
+ required=True,
+ help='Value of rebase_path(".", root_build_dir)',
+ )
+ parser.add_argument(
+ '--default-toolchain', required=True, help='Value of default_toolchain'
+ )
+ parser.add_argument(
+ '--current-toolchain', required=True, help='Value of current_toolchain'
+ )
parser.add_argument('--module', help='Run this module instead of a script')
- parser.add_argument('--env',
- action='append',
- help='Environment variables to set as NAME=VALUE')
+ parser.add_argument(
+ '--env',
+ action='append',
+ help='Environment variables to set as NAME=VALUE',
+ )
parser.add_argument(
'--touch',
type=Path,
@@ -79,6 +98,21 @@ def _parse_args() -> argparse.Namespace:
help='Change to this working directory before running the subcommand',
)
parser.add_argument(
+ '--python-dep-list-files',
+ nargs='+',
+ type=Path,
+ help='Paths to text files containing lists of Python package metadata '
+ 'json files.',
+ )
+ parser.add_argument(
+ '--python-virtualenv-config',
+ type=Path,
+ help='Path to a virtualenv json config to use for this action.',
+ )
+ parser.add_argument(
+ '--command-launcher', help='Arguments to prepend to Python command'
+ )
+ parser.add_argument(
'original_cmd',
nargs=argparse.REMAINDER,
help='Python script with arguments to run',
@@ -86,375 +120,14 @@ def _parse_args() -> argparse.Namespace:
parser.add_argument(
'--lockfile',
type=Path,
- required=True,
- help=('Path to a pip lockfile. Any pip execution will aquire an '
- 'exclusive lock on it, any other module a shared lock.'))
+ help=(
+ 'Path to a pip lockfile. Any pip execution will acquire an '
+ 'exclusive lock on it, any other module a shared lock.'
+ ),
+ )
return parser.parse_args()
-class GnPaths(NamedTuple):
- """The set of paths needed to resolve GN paths to filesystem paths."""
- root: Path
- build: Path
- cwd: Path
-
- # Toolchain label or '' if using the default toolchain
- toolchain: str
-
- def resolve(self, gn_path: str) -> Path:
- """Resolves a GN path to a filesystem path."""
- if gn_path.startswith('//'):
- return self.root.joinpath(gn_path.lstrip('/')).resolve()
-
- return self.cwd.joinpath(gn_path).resolve()
-
- def resolve_paths(self, gn_paths: str, sep: str = ';') -> str:
- """Resolves GN paths to filesystem paths in a delimited string."""
- return sep.join(
- str(self.resolve(path)) for path in gn_paths.split(sep))
-
-
-@dataclass(frozen=True)
-class Label:
- """Represents a GN label."""
- name: str
- dir: Path
- relative_dir: Path
- toolchain: Optional['Label']
- out_dir: Path
- gen_dir: Path
-
- def __init__(self, paths: GnPaths, label: str):
- # Use this lambda to set attributes on this frozen dataclass.
- set_attr = lambda attr, val: object.__setattr__(self, attr, val)
-
- # Handle explicitly-specified toolchains
- if label.endswith(')'):
- label, toolchain = label[:-1].rsplit('(', 1)
- else:
- # Prevent infinite recursion for toolchains
- toolchain = paths.toolchain if paths.toolchain != label else ''
-
- set_attr('toolchain', Label(paths, toolchain) if toolchain else None)
-
- # Split off the :target, if provided, or use the last part of the path.
- try:
- directory, name = label.rsplit(':', 1)
- except ValueError:
- directory, name = label, label.rsplit('/', 1)[-1]
-
- set_attr('name', name)
-
- # Resolve the directory to an absolute path
- set_attr('dir', paths.resolve(directory))
- set_attr('relative_dir', self.dir.relative_to(paths.root.resolve()))
-
- set_attr(
- 'out_dir',
- paths.build / self.toolchain_name() / 'obj' / self.relative_dir)
- set_attr(
- 'gen_dir',
- paths.build / self.toolchain_name() / 'gen' / self.relative_dir)
-
- def gn_label(self) -> str:
- label = f'//{self.relative_dir.as_posix()}:{self.name}'
- return f'{label}({self.toolchain!r})' if self.toolchain else label
-
- def toolchain_name(self) -> str:
- return self.toolchain.name if self.toolchain else ''
-
- def __repr__(self) -> str:
- return self.gn_label()
-
-
-class _Artifact(NamedTuple):
- path: Path
- variables: Dict[str, str]
-
-
-# Matches a non-phony build statement.
-_GN_NINJA_BUILD_STATEMENT = re.compile(r'^build (.+):[ \n](?!phony\b)')
-
-# Extensions used for compilation artifacts.
-_MAIN_ARTIFACTS = '', '.elf', '.a', '.so', '.dylib', '.exe', '.lib', '.dll'
-
-
-def _get_artifact(entries: List[str]) -> _Artifact:
- """Attempts to resolve which artifact to use if there are multiple.
-
- Selects artifacts based on extension. This will not work if a toolchain
- creates multiple compilation artifacts from one command (e.g. .a and .elf).
- """
- assert entries, "There should be at least one entry here!"
-
- if len(entries) == 1:
- return _Artifact(Path(entries[0]), {})
-
- filtered = [p for p in entries if Path(p).suffix in _MAIN_ARTIFACTS]
-
- if len(filtered) == 1:
- return _Artifact(Path(filtered[0]), {})
-
- raise ExpressionError(
- f'Expected 1, but found {len(filtered)} artifacts, after filtering for '
- f'extensions {", ".join(repr(e) for e in _MAIN_ARTIFACTS)}: {entries}')
-
-
-def _parse_build_artifacts(fd) -> Iterator[_Artifact]:
- """Partially parses the build statements in a Ninja file."""
- lines = iter(fd)
-
- def next_line():
- try:
- return next(lines)
- except StopIteration:
- return None
-
- # Serves as the parse state (only two states)
- artifact: Optional[_Artifact] = None
-
- line = next_line()
-
- while line is not None:
- if artifact:
- if line.startswith(' '): # build variable statements are indented
- key, value = (a.strip() for a in line.split('=', 1))
- artifact.variables[key] = value
- line = next_line()
- else:
- yield artifact
- artifact = None
- else:
- match = _GN_NINJA_BUILD_STATEMENT.match(line)
- if match:
- artifact = _get_artifact(match.group(1).split())
-
- line = next_line()
-
- if artifact:
- yield artifact
-
-
-def _search_target_ninja(ninja_file: Path,
- target: Label) -> Tuple[Optional[Path], List[Path]]:
- """Parses the main output file and object files from <target>.ninja."""
-
- artifact: Optional[Path] = None
- objects: List[Path] = []
-
- _LOG.debug('Parsing target Ninja file %s for %s', ninja_file, target)
-
- with ninja_file.open() as fd:
- for path, variables in _parse_build_artifacts(fd):
- # Older GN used .stamp files when there is no build artifact.
- if path.suffix == '.stamp':
- continue
-
- if variables:
- assert not artifact, f'Multiple artifacts for {target}!'
- artifact = Path(path)
- else:
- objects.append(Path(path))
-
- return artifact, objects
-
-
-def _search_toolchain_ninja(ninja_file: Path, paths: GnPaths,
- target: Label) -> Optional[Path]:
- """Searches the toolchain.ninja file for outputs from the provided target.
-
- Files created by an action appear in toolchain.ninja instead of in their own
- <target>.ninja. If the specified target has a single output file in
- toolchain.ninja, this function returns its path.
- """
-
- _LOG.debug('Searching toolchain Ninja file %s for %s', ninja_file, target)
-
- # Older versions of GN used a .stamp file to signal completion of a target.
- stamp_dir = target.out_dir.relative_to(paths.build).as_posix()
- stamp_tool = 'stamp'
- if target.toolchain_name() != '':
- stamp_tool = f'{target.toolchain_name()}_stamp'
- stamp_statement = f'build {stamp_dir}/{target.name}.stamp: {stamp_tool} '
-
- # Newer GN uses a phony Ninja target to signal completion of a target.
- phony_dir = Path(target.toolchain_name(), 'phony',
- target.relative_dir).as_posix()
- phony_statement = f'build {phony_dir}/{target.name}: phony '
-
- with ninja_file.open() as fd:
- for line in fd:
- for statement in (phony_statement, stamp_statement):
- if line.startswith(statement):
- output_files = line[len(statement):].strip().split()
- if len(output_files) == 1:
- return Path(output_files[0])
-
- break
-
- return None
-
-
-def _search_ninja_files(
- paths: GnPaths,
- target: Label) -> Tuple[bool, Optional[Path], List[Path]]:
- ninja_file = target.out_dir / f'{target.name}.ninja'
- if ninja_file.exists():
- return (True, *_search_target_ninja(ninja_file, target))
-
- ninja_file = paths.build / target.toolchain_name() / 'toolchain.ninja'
- if ninja_file.exists():
- return True, _search_toolchain_ninja(ninja_file, paths, target), []
-
- return False, None, []
-
-
-@dataclass(frozen=True)
-class TargetInfo:
- """Provides information about a target parsed from a .ninja file."""
-
- label: Label
- generated: bool # True if the Ninja files for this target were generated.
- artifact: Optional[Path]
- object_files: Tuple[Path]
-
- def __init__(self, paths: GnPaths, target: str):
- object.__setattr__(self, 'label', Label(paths, target))
-
- generated, artifact, objects = _search_ninja_files(paths, self.label)
-
- object.__setattr__(self, 'generated', generated)
- object.__setattr__(self, 'artifact', artifact)
- object.__setattr__(self, 'object_files', tuple(objects))
-
- def __repr__(self) -> str:
- return repr(self.label)
-
-
-class ExpressionError(Exception):
- """An error occurred while parsing an expression."""
-
-
-class _ArgAction(enum.Enum):
- APPEND = 0
- OMIT = 1
- EMIT_NEW = 2
-
-
-class _Expression:
- def __init__(self, match: re.Match, ending: int):
- self._match = match
- self._ending = ending
-
- @property
- def string(self):
- return self._match.string
-
- @property
- def end(self) -> int:
- return self._ending + len(_ENDING)
-
- def contents(self) -> str:
- return self.string[self._match.end():self._ending]
-
- def expression(self) -> str:
- return self.string[self._match.start():self.end]
-
-
-_Actions = Iterator[Tuple[_ArgAction, str]]
-
-
-def _target_file(paths: GnPaths, expr: _Expression) -> _Actions:
- target = TargetInfo(paths, expr.contents())
-
- if not target.generated:
- raise ExpressionError(f'Target {target} has not been generated by GN!')
-
- if target.artifact is None:
- raise ExpressionError(f'Target {target} has no output file!')
-
- yield _ArgAction.APPEND, str(target.artifact)
-
-
-def _target_file_if_exists(paths: GnPaths, expr: _Expression) -> _Actions:
- target = TargetInfo(paths, expr.contents())
-
- if target.generated:
- if target.artifact is None:
- raise ExpressionError(f'Target {target} has no output file!')
-
- if paths.build.joinpath(target.artifact).exists():
- yield _ArgAction.APPEND, str(target.artifact)
- return
-
- yield _ArgAction.OMIT, ''
-
-
-def _target_objects(paths: GnPaths, expr: _Expression) -> _Actions:
- if expr.expression() != expr.string:
- raise ExpressionError(
- f'The expression "{expr.expression()}" in "{expr.string}" may '
- 'expand to multiple arguments, so it cannot be used alongside '
- 'other text or expressions')
-
- target = TargetInfo(paths, expr.contents())
- if not target.generated:
- raise ExpressionError(f'Target {target} has not been generated by GN!')
-
- for obj in target.object_files:
- yield _ArgAction.EMIT_NEW, str(obj)
-
-
-# TODO(pwbug/347): Replace expressions with native GN features when possible.
-_FUNCTIONS: Dict['str', Callable[[GnPaths, _Expression], _Actions]] = {
- 'TARGET_FILE': _target_file,
- 'TARGET_FILE_IF_EXISTS': _target_file_if_exists,
- 'TARGET_OBJECTS': _target_objects,
-}
-
-_START_EXPRESSION = re.compile(fr'<({"|".join(_FUNCTIONS)})\(')
-_ENDING = ')>'
-
-
-def _expand_arguments(paths: GnPaths, string: str) -> _Actions:
- pos = 0
-
- for match in _START_EXPRESSION.finditer(string):
- if pos != match.start():
- yield _ArgAction.APPEND, string[pos:match.start()]
-
- ending = string.find(_ENDING, match.end())
- if ending == -1:
- raise ExpressionError(f'Parse error: no terminating "{_ENDING}" '
- f'was found for "{string[match.start():]}"')
-
- expression = _Expression(match, ending)
- yield from _FUNCTIONS[match.group(1)](paths, expression)
-
- pos = expression.end
-
- if pos < len(string):
- yield _ArgAction.APPEND, string[pos:]
-
-
-def expand_expressions(paths: GnPaths, arg: str) -> Iterable[str]:
- """Expands <FUNCTION(...)> expressions; yields zero or more arguments."""
- if arg == '':
- return ['']
-
- expanded_args: List[List[str]] = [[]]
-
- for action, piece in _expand_arguments(paths, arg):
- if action is _ArgAction.OMIT:
- return []
-
- expanded_args[-1].append(piece)
- if action is _ArgAction.EMIT_NEW:
- expanded_args.append([])
-
- return (''.join(arg) for arg in expanded_args if arg)
-
-
class LockAcquisitionTimeoutError(Exception):
"""Raised on a timeout."""
@@ -493,7 +166,8 @@ def acquire_lock(lockfile: Path, exclusive: bool):
while time.monotonic() - start_time < _LOCK_ACQUISITION_TIMEOUT:
try:
fcntl.flock( # type: ignore[name-defined]
- fd, lock_type | fcntl.LOCK_NB) # type: ignore[name-defined]
+ fd, lock_type | fcntl.LOCK_NB # type: ignore[name-defined]
+ )
return # Lock acquired!
except BlockingIOError:
pass # Keep waiting.
@@ -502,10 +176,21 @@ def acquire_lock(lockfile: Path, exclusive: bool):
backoff += 1
raise LockAcquisitionTimeoutError(
- f"Failed to acquire lock {lockfile} in {_LOCK_ACQUISITION_TIMEOUT}")
+ f"Failed to acquire lock {lockfile} in {_LOCK_ACQUISITION_TIMEOUT}"
+ )
+
+
+class MissingPythonDependency(Exception):
+ """An error occurred while processing a Python dependency."""
+
+
+def _load_virtualenv_config(json_file_path: Path) -> Tuple[str, str]:
+ with json_file_path.open() as json_fp:
+ json_dict = json.load(json_fp)
+ return json_dict.get('interpreter'), json_dict.get('path')
-def main( # pylint: disable=too-many-arguments
+def main( # pylint: disable=too-many-arguments,too-many-branches,too-many-locals
gn_root: Path,
current_path: Path,
original_cmd: List[str],
@@ -513,67 +198,162 @@ def main( # pylint: disable=too-many-arguments
current_toolchain: str,
module: Optional[str],
env: Optional[List[str]],
+ python_dep_list_files: List[Path],
+ python_virtualenv_config: Optional[Path],
capture_output: bool,
touch: Optional[Path],
working_directory: Optional[Path],
- lockfile: Path,
+ command_launcher: Optional[str],
+ lockfile: Optional[Path],
) -> int:
"""Script entry point."""
+ python_paths_list = []
+ if python_dep_list_files:
+ py_packages = load_packages(
+ python_dep_list_files,
+ # If this python_action has no gn python_deps this file will be
+ # empty.
+ ignore_missing=True,
+ )
+
+ for pkg in py_packages:
+ top_level_source_dir = pkg.package_dir
+ if not top_level_source_dir:
+ raise MissingPythonDependency(
+ 'Unable to find top level source dir for the Python '
+ f'package "{pkg}"'
+ )
+ # Don't add this dir to the PYTHONPATH if no __init__.py exists.
+ init_py_files = top_level_source_dir.parent.glob('*/__init__.py')
+ if not any(init_py_files):
+ continue
+ python_paths_list.append(
+ gn_resolver.abspath(top_level_source_dir.parent)
+ )
+
+ # Sort the PYTHONPATH list, it will be in a different order each build.
+ python_paths_list = sorted(python_paths_list)
+
if not original_cmd or original_cmd[0] != '--':
_LOG.error('%s requires a command to run', sys.argv[0])
return 1
# GN build scripts are executed from the root build directory.
- root_build_dir = Path.cwd().resolve()
+ root_build_dir = gn_resolver.abspath(Path.cwd())
tool = current_toolchain if current_toolchain != default_toolchain else ''
- paths = GnPaths(root=gn_root.resolve(),
- build=root_build_dir,
- cwd=current_path.resolve(),
- toolchain=tool)
+ paths = gn_resolver.GnPaths(
+ root=gn_resolver.abspath(gn_root),
+ build=root_build_dir,
+ cwd=gn_resolver.abspath(current_path),
+ toolchain=tool,
+ )
command = [sys.executable]
+ python_interpreter = None
+ python_virtualenv = None
+ if python_virtualenv_config:
+ python_interpreter, python_virtualenv = _load_virtualenv_config(
+ python_virtualenv_config
+ )
+
+ if python_interpreter is not None:
+ command = [str(root_build_dir / python_interpreter)]
+
+ if command_launcher is not None:
+ command = shlex.split(command_launcher) + command
+
if module is not None:
command += ['-m', module]
run_args: dict = dict()
+ # Always inherit the environtment by default. If PYTHONPATH or VIRTUALENV is
+ # set below then the environment vars must be copied in or subprocess.run
+ # will run with only the new updated variables.
+ run_args['env'] = os.environ.copy()
if env is not None:
environment = os.environ.copy()
environment.update((k, v) for k, v in (a.split('=', 1) for a in env))
run_args['env'] = environment
+ script_command = original_cmd[0]
+ if script_command == '--':
+ script_command = original_cmd[1]
+
+ is_pip_command = (
+ module == 'pip' or 'pip_install_python_deps.py' in script_command
+ )
+
+ existing_env = run_args['env'] if 'env' in run_args else os.environ.copy()
+ new_env = {}
+ if python_virtualenv:
+ new_env['VIRTUAL_ENV'] = str(root_build_dir / python_virtualenv)
+ bin_folder = 'Scripts' if platform.system() == 'Windows' else 'bin'
+ new_env['PATH'] = os.pathsep.join(
+ [
+ str(root_build_dir / python_virtualenv / bin_folder),
+ existing_env.get('PATH', ''),
+ ]
+ )
+
+ if python_virtualenv and python_paths_list and not is_pip_command:
+ python_path_prepend = os.pathsep.join(
+ str(p) for p in set(python_paths_list)
+ )
+
+ # Append the existing PYTHONPATH to the new one.
+ new_python_path = os.pathsep.join(
+ path_str
+ for path_str in [
+ python_path_prepend,
+ existing_env.get('PYTHONPATH', ''),
+ ]
+ if path_str
+ )
+
+ new_env['PYTHONPATH'] = new_python_path
+
+ if 'env' not in run_args:
+ run_args['env'] = {}
+ run_args['env'].update(new_env)
+
if capture_output:
# Combine stdout and stderr so that error messages are correctly
# interleaved with the rest of the output.
run_args['stdout'] = subprocess.PIPE
run_args['stderr'] = subprocess.STDOUT
+ # Build the command to run.
try:
for arg in original_cmd[1:]:
- command += expand_expressions(paths, arg)
- except ExpressionError as err:
+ command += gn_resolver.expand_expressions(paths, arg)
+ except gn_resolver.ExpressionError as err:
_LOG.error('%s: %s', sys.argv[0], err)
return 1
if working_directory:
run_args['cwd'] = working_directory
- try:
- acquire_lock(lockfile, module == 'pip')
- except LockAcquisitionTimeoutError as exception:
- _LOG.error('%s', exception)
- return 1
+ # TODO(b/235239674): Deprecate the --lockfile option as part of the Python
+ # GN template refactor.
+ if lockfile:
+ try:
+ acquire_lock(lockfile, is_pip_command)
+ except LockAcquisitionTimeoutError as exception:
+ _LOG.error('%s', exception)
+ return 1
_LOG.debug('RUN %s', ' '.join(shlex.quote(arg) for arg in command))
completed_process = subprocess.run(command, **run_args)
if completed_process.returncode != 0:
- _LOG.debug('Command failed; exit code: %d',
- completed_process.returncode)
+ _LOG.debug(
+ 'Command failed; exit code: %d', completed_process.returncode
+ )
if capture_output:
sys.stdout.buffer.write(completed_process.stdout)
elif touch:
diff --git a/pw_build/py/pw_build/python_wheels.py b/pw_build/py/pw_build/python_wheels.py
index 22ad3d531..7f55b3eb3 100644
--- a/pw_build/py/pw_build/python_wheels.py
+++ b/pw_build/py/pw_build/python_wheels.py
@@ -27,9 +27,11 @@ def _parse_args():
parser.add_argument(
'setup_files',
nargs='+',
- help='Path to a setup.py file to invoke to build wheels.')
- parser.add_argument('--out_dir',
- help='Path where the build artifacts should be put.')
+ help='Path to a setup.py file to invoke to build wheels.',
+ )
+ parser.add_argument(
+ '--out_dir', help='Path where the build artifacts should be put.'
+ )
return parser.parse_args()
diff --git a/pw_build/py/pw_build/wrap_ninja.py b/pw_build/py/pw_build/wrap_ninja.py
new file mode 100644
index 000000000..e7a0f4911
--- /dev/null
+++ b/pw_build/py/pw_build/wrap_ninja.py
@@ -0,0 +1,600 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Wrapper for Ninja that adds a real-time status interface."""
+
+import argparse
+import dataclasses
+import enum
+import json
+import os
+import re
+import signal
+import subprocess
+import sys
+import threading
+import time
+from typing import Any, Dict, IO, List, Tuple, Optional
+
+if sys.platform != 'win32':
+ import pty
+
+# The status formatting string for Ninja to use. Includes a sentinel prefix.
+_NINJA_STATUS = '@@!!@@%s,%f,%t>'
+
+# "ANSI" terminal codes for controlling terminal output. This are more-or-less
+# standardized. See https://en.wikipedia.org/wiki/ANSI_escape_code for a more
+# comprehensive list.
+# Terminal code for clearing the entire current line.
+_TERM_CLEAR_LINE_FULL: str = '\x1B[2K'
+# Terminal code for clearing from the cursor to the end of the current line.
+_TERM_CLEAR_LINE_TO_END: str = '\x1B[0K'
+# Terminal code for moving the cursor to the previous line.
+_TERM_MOVE_PREVIOUS_LINE: str = '\x1B[1A'
+# Terminal code for stopping line wrapping.
+_TERM_DISABLE_WRAP: str = '\x1B[?7l'
+# Terminal code for enabling line wrapping.
+_TERM_ENABLE_WRAP: str = '\x1B[?7h'
+
+
+class SignalFlag:
+ """Helper class for intercepting signals and setting a flag to allow
+ gracefully handling them.
+
+ Attributes:
+ signaled: Initially False, becomes True when the process receives a
+ SIGTERM.
+ """
+
+ def __init__(self) -> None:
+ """Initializes the signal handler."""
+ self.signaled = False
+ signal.signal(signal.SIGTERM, self._signal_handler)
+
+ # pylint: disable=unused-argument
+ def _signal_handler(self, signal_number: int, stack_frame: Any) -> None:
+ """Sets the signaled flag, called when signaled."""
+ self.signaled = True
+
+
+class ConsoleRenderer:
+ """Helper class for rendering the TUI using terminal control codes.
+
+ This class has two types of output. First, permanently printed lines
+ (with `print_line` method) behave like normal terminal output and stay in
+ the terminal once printed. Second, "temporary" printed lines follow the
+ most recently printed permanent line, and get rewritten on each call
+ to `render`.
+
+ Attributes:
+ smart_terminal: If true, will print a rich TUI using terminal control
+ codes. Otherwise, this class won't print temporary lines, and
+ permanent lines will be printed without any special control codes.
+ Defaults to true if stdout is connected to a TTY.
+ """
+
+ def __init__(self) -> None:
+ """Initialize the console renderer."""
+ self._queued_lines: List[str] = []
+ self._temporary_lines: List[str] = []
+ self._previous_line_count = 0
+
+ term = os.environ.get('TERM')
+ self.smart_terminal = term and (term != 'dumb') and sys.stdout.isatty()
+
+ def print_line(self, line: str) -> None:
+ """Queue a permanent line for printing during the next render."""
+ self._queued_lines.append(line)
+
+ def set_temporary_lines(self, lines: List[str]) -> None:
+ """Set the temporary lines to be displayed during the next render."""
+ self._temporary_lines = lines[:]
+
+ def render(self) -> None:
+ """Render the current state of the renderer."""
+
+ # If we can't use terminal codes, print out permanent lines and exit.
+ if not self.smart_terminal:
+ for line in self._queued_lines:
+ print(line)
+ self._queued_lines.clear()
+ sys.stdout.flush()
+ return
+
+ # Go back to the end of the last permanent lines.
+ for _ in range(self._previous_line_count):
+ print(_TERM_MOVE_PREVIOUS_LINE, end='')
+
+ # Print any new permanent lines.
+ for line in self._queued_lines:
+ print(line, end='')
+ print(_TERM_CLEAR_LINE_TO_END)
+
+ # Print the new temporary lines.
+ print(_TERM_DISABLE_WRAP, end='')
+ for line in self._temporary_lines:
+ print(_TERM_CLEAR_LINE_FULL, end='')
+ print(line, end='')
+ print(_TERM_CLEAR_LINE_TO_END)
+ print(_TERM_ENABLE_WRAP, end='')
+
+ # Clear any leftover temporary lines from the previous render.
+ lines_to_clear = self._previous_line_count - len(self._temporary_lines)
+ for _ in range(lines_to_clear):
+ print(_TERM_CLEAR_LINE_FULL)
+ for _ in range(lines_to_clear):
+ print(_TERM_MOVE_PREVIOUS_LINE, end='')
+
+ # Flush and update state.
+ sys.stdout.flush()
+ self._previous_line_count = len(self._temporary_lines)
+ self._queued_lines.clear()
+
+
+def _format_duration(duration: float) -> str:
+ """Format a duration (in seconds) as a string."""
+ if duration >= 60:
+ seconds = int(duration % 60)
+ minutes = int(duration / 60)
+ return f'{minutes}m{seconds:02}s'
+ return f'{duration:.1f}s'
+
+
+@dataclasses.dataclass
+class NinjaAction:
+ """A Ninja action: a task that needs to run and complete.
+
+ In Ninja, this is referred to as an "edge".
+
+ Attributes:
+ name: The name of the Ninja action.
+ jobs: The number of running jobs for the action. Some Ninja actions
+ have multiple sub-actions that have the same name.
+ start_time: The time that the Ninja action started. This is based on
+ time.time, not Ninja's own stopwatch.
+ end_time: The time that the Ninja action finished, or None if it is
+ still running.
+ """
+
+ name: str
+ jobs: int = 0
+ start_time: float = dataclasses.field(default_factory=time.time)
+ end_time: Optional[float] = None
+
+
+class NinjaEventKind(enum.Enum):
+ """The kind of Ninja event."""
+
+ ACTION_STARTED = 1
+ ACTION_FINISHED = 2
+ ACTION_LOG = 3
+
+
+@dataclasses.dataclass
+class NinjaEvent:
+ """An event from the Ninja build.
+
+ Attributes:
+ kind: The kind of event.
+ action: The action this event relates to, if any.
+ message: The log message associated with this event, if it an
+ 'ACTION_LOG' event.
+ num_started: The number of started actions when this event occurred.
+ num_finished: The number of finished actions when this event occurred.
+ num_total: The total number of actions when this event occurred.
+ """
+
+ kind: NinjaEventKind
+ action: Optional[NinjaAction] = None
+ log_message: Optional[str] = None
+ num_started: int = 0
+ num_finished: int = 0
+ num_total: int = 0
+
+
+class Ninja:
+ """Wrapper around a Ninja subprocess.
+
+ Parses the Ninja output to maintain a representation of the build graph.
+
+ Attributes:
+ num_started: The number of started actions.
+ num_finished: The number of finished actions.
+ num_total: The (estimated) number of total actions in the build.
+ running_actions: The currently-running actions, keyed by name.
+ actions: A list of all actions, running and completed.
+ last_action_completed: The last action that Ninja finished building.
+ events: The events that have occured in the Ninja build.
+ exited: Whether the Ninja subprocess has exited.
+ lock: The lock guarding the rest of the attributes.
+ process: The Python subprocess running Ninja.
+ start_time: The time that the Ninja build started.
+ """
+
+ num_started: int
+ num_finished: int
+ num_total: int
+ actions: List[NinjaAction]
+ running_actions: Dict[str, NinjaAction]
+ last_action_completed: Optional[NinjaAction]
+ events: List[NinjaEvent]
+ exited: bool
+ lock: threading.Lock
+ process: subprocess.Popen
+ start_time: float
+
+ def __init__(self, args: List[str]) -> None:
+ if sys.platform == 'win32':
+ raise RuntimeError('Ninja wrapper does not support Windows.')
+
+ self.num_started = 0
+ self.num_finished = 0
+ self.num_total = 0
+ self.actions = []
+ self.running_actions = {}
+ self.last_action_completed = None
+ self.events = []
+ self.exited = False
+ self.lock = threading.Lock()
+
+ # Launch ninja and configure pseudo-tty.
+ # pylint: disable-next=no-member,undefined-variable
+ ptty_parent, ptty_child = pty.openpty() # type: ignore
+ ptty_file = os.fdopen(ptty_parent, 'r')
+ env = dict(os.environ)
+ env['NINJA_STATUS'] = _NINJA_STATUS
+ if 'TERM' not in env:
+ env['TERM'] = 'vt100'
+ command = ['ninja'] + args
+ self.process = subprocess.Popen(
+ command,
+ env=env,
+ stdin=subprocess.DEVNULL,
+ stdout=ptty_child,
+ stderr=ptty_child,
+ )
+ os.close(ptty_child)
+ self.start_time = time.time()
+
+ # Start the output monitor thread.
+ thread = threading.Thread(
+ target=self._monitor_thread, args=(ptty_file,)
+ )
+ thread.daemon = True # Don't keep the process alive.
+ thread.start()
+
+ def _monitor_thread(self, file: IO[str]) -> None:
+ """Monitor the Ninja output. Run in a separate thread."""
+ # A Ninja status line starts with "\r" and ends with "\x1B[K". This
+ # tracks whether we're currently in a status line.
+ ninja_status = False
+ buffer: List[str] = []
+ try:
+ while True:
+ char = file.read(1)
+ if char == '\r':
+ ninja_status = True
+ continue
+ if char == '\n':
+ self._process_output(''.join(buffer))
+ buffer = []
+ continue
+ if char == '':
+ # End of file.
+ break
+
+ # Look for the end of a status line, ignoring partial matches.
+ if char == '\x1B' and ninja_status:
+ char = file.read(1)
+ if char == '[':
+ char = file.read(1)
+ if char == 'K':
+ self._process_output(''.join(buffer))
+ buffer = []
+ ninja_status = False
+ continue
+ buffer.append('\x1B[')
+ else:
+ buffer.append('\x1B')
+
+ buffer.append(char)
+ except OSError:
+ pass
+
+ self._process_output(''.join(buffer))
+ self.exited = True
+
+ def _process_output(self, line: str) -> None:
+ """Process a line of output from Ninja, updating the internal state."""
+ with self.lock:
+ if match := re.match(r'^@@!!@@(\d+),(\d+),(\d+)>(.*)', line):
+ actions_started = int(match.group(1))
+ actions_finished = int(match.group(2))
+ actions_total = int(match.group(3))
+ name = match.group(4)
+
+ # Sometimes Ninja delimits lines without \r, which prevents
+ # _monitor_thread from stripping out the final control code,
+ # so just remove it here if it's present.
+ if name.endswith('\x1B[K'):
+ name = name[:-3]
+
+ did_start = actions_started > self.num_started
+ did_finish = actions_finished > self.num_finished
+ self.num_started = actions_started
+ self.num_finished = actions_finished
+ self.num_total = actions_total
+
+ # Special case: first action in a new graph.
+ # This is important for GN's "Regenerating ninja files" action.
+ if actions_started == 1 and actions_finished == 0:
+ for action in self.running_actions.values():
+ action.end_time = time.time()
+ self._add_event(
+ NinjaEvent(NinjaEventKind.ACTION_FINISHED, action)
+ )
+ self.running_actions = {}
+ self.last_action_completed = None
+
+ if did_start:
+ action = self.running_actions.setdefault(
+ name, NinjaAction(name)
+ )
+ if action.jobs == 0:
+ self.actions.append(action)
+ self._add_event(
+ NinjaEvent(NinjaEventKind.ACTION_STARTED, action)
+ )
+ action.jobs += 1
+
+ if did_finish and name in self.running_actions:
+ action = self.running_actions[name]
+ action.jobs -= 1
+ if action.jobs <= 0:
+ self.running_actions.pop(name)
+ self.last_action_completed = action
+ action.end_time = time.time()
+ self._add_event(
+ NinjaEvent(NinjaEventKind.ACTION_FINISHED, action)
+ )
+ else:
+ context_action = None
+ if not line.startswith('ninja: '):
+ context_action = self.last_action_completed
+ self._add_event(
+ NinjaEvent(
+ NinjaEventKind.ACTION_LOG,
+ action=context_action,
+ log_message=line,
+ )
+ )
+
+ def _add_event(self, event: NinjaEvent) -> None:
+ """Add a new event to the event queue."""
+ event.num_started = self.num_started
+ event.num_finished = self.num_finished
+ event.num_total = self.num_total
+ self.events.append(event)
+
+ def write_trace(self, file: IO[str]) -> None:
+ """Write a Chromium trace_event-formatted trace to a file."""
+ now = time.time()
+ threads: List[float] = []
+ events = []
+ actions = sorted(
+ self.actions, key=lambda x: x.end_time or now, reverse=True
+ )
+ for action in actions:
+ # If this action hasn't completed, fake the end time.
+ end_time = action.end_time or now
+
+ # Allocate a "thread id" for the action. We look at threads in
+ # reverse end time and fill in threads from the end to get a
+ # better-looking trace.
+ for tid, busy_time in enumerate(threads):
+ if busy_time >= end_time:
+ threads[tid] = action.start_time
+ break
+ else:
+ tid = len(threads)
+ threads.append(action.start_time)
+
+ events.append(
+ {
+ 'name': action.name,
+ 'cat': 'actions',
+ 'ph': 'X',
+ 'ts': action.start_time * 1000000,
+ 'dur': (end_time - action.start_time) * 1000000,
+ 'pid': 0,
+ 'tid': tid,
+ 'args': {},
+ }
+ )
+ json.dump(events, file)
+
+
+class UI:
+ """Class to handle UI state and rendering."""
+
+ def __init__(self, ninja: Ninja, args: argparse.Namespace) -> None:
+ self._ninja = ninja
+ self._args = args
+ self._renderer = ConsoleRenderer()
+ self._last_log_action: Optional[NinjaAction] = None
+
+ def _get_status_display(self) -> List[str]:
+ """Generates the status display. Must be called under the Ninja lock."""
+ actions = sorted(
+ self._ninja.running_actions.values(), key=lambda x: x.start_time
+ )
+ now = time.time()
+ total_elapsed = _format_duration(now - self._ninja.start_time)
+ lines = [
+ f'[{total_elapsed: >5}] '
+ f'Building [{self._ninja.num_finished}/{self._ninja.num_total}] ...'
+ ]
+ for action in actions[: self._args.ui_max_actions]:
+ elapsed = _format_duration(now - action.start_time)
+ lines.append(f' [{elapsed: >5}] {action.name}')
+ if len(actions) > self._args.ui_max_actions:
+ remaining = len(actions) - self._args.ui_max_actions
+ lines.append(f' ... and {remaining} more')
+ return lines
+
+ def _process_event(self, event: NinjaEvent) -> None:
+ """Processes a Ninja Event. Must be called under the Ninja lock."""
+ show_started = self._args.log_actions
+ show_ended = self._args.log_actions or not self._renderer.smart_terminal
+
+ if event.kind == NinjaEventKind.ACTION_LOG:
+ if event.action and (event.action != self._last_log_action):
+ self._renderer.print_line(f'[{event.action.name}]')
+ self._last_log_action = event.action
+ assert event.log_message is not None
+ self._renderer.print_line(event.log_message)
+
+ if event.kind == NinjaEventKind.ACTION_STARTED and show_started:
+ assert event.action
+ self._renderer.print_line(
+ f'[{event.num_finished}/{event.num_total}] '
+ f'Started [{event.action.name}]'
+ )
+
+ if event.kind == NinjaEventKind.ACTION_FINISHED and show_ended:
+ assert event.action and event.action.end_time is not None
+ duration = _format_duration(
+ event.action.end_time - event.action.start_time
+ )
+ self._renderer.print_line(
+ f'[{event.num_finished}/{event.num_total}] '
+ f'Finished [{event.action.name}] ({duration})'
+ )
+
+ def update(self) -> None:
+ """Updates and re-renders the UI."""
+ with self._ninja.lock:
+ for event in self._ninja.events:
+ self._process_event(event)
+ self._ninja.events = []
+
+ self._renderer.set_temporary_lines(self._get_status_display())
+
+ self._renderer.render()
+
+ def print_summary(self) -> None:
+ """Prints the summary line at the end of the build."""
+ total_time = _format_duration(time.time() - self._ninja.start_time)
+ num_finished = self._ninja.num_finished
+ num_total = self._ninja.num_total
+ self._renderer.print_line(
+ f'Built {num_finished}/{num_total} targets in {total_time}.'
+ )
+ self._renderer.set_temporary_lines([])
+ self._renderer.render()
+
+ def dump_running_actions(self) -> None:
+ """Prints a list of actions that are currently running."""
+ actions = sorted(
+ self._ninja.running_actions.values(), key=lambda x: x.start_time
+ )
+ now = time.time()
+ self._renderer.print_line(f'{len(actions)} running actions:')
+ for action in actions:
+ elapsed = _format_duration(now - action.start_time)
+ self._renderer.print_line(f' [{elapsed: >5}] {action.name}')
+ self._renderer.set_temporary_lines([])
+ self._renderer.render()
+
+ def print_line(self, line: str) -> None:
+ """Print a line to the interface output."""
+ self._renderer.print_line(line)
+
+
+def _parse_args() -> Tuple[argparse.Namespace, List[str]]:
+ """Parses CLI arguments.
+
+ Returns:
+ The tuple containing the parsed arguments and the remaining unparsed
+ arguments to be passed to Ninja.
+ """
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--ui-update-rate',
+ help='update rate of the UI (in seconds)',
+ type=float,
+ default=0.1,
+ )
+ parser.add_argument(
+ '--ui-max-actions',
+ help='maximum build actions to display at once',
+ type=int,
+ default=8,
+ )
+ parser.add_argument(
+ '--log-actions',
+ help='whether to log when actions start and finish',
+ action='store_true',
+ )
+ parser.add_argument(
+ '--write-trace',
+ help='write a Chromium trace_event-style trace to the given file',
+ type=argparse.FileType('w'),
+ )
+ return parser.parse_known_args()
+
+
+def main() -> int:
+ """Main entrypoint for script."""
+ args, ninja_args = _parse_args()
+
+ if sys.platform == 'win32':
+ print(
+ 'WARNING: pw-wrap-ninja does not support Windows. '
+ 'Running ninja directly.'
+ )
+ process = subprocess.run(['ninja'] + ninja_args)
+ return process.returncode
+
+ signal_flag = SignalFlag()
+ ninja = Ninja(ninja_args)
+ interface = UI(ninja, args)
+
+ while True:
+ interface.update()
+ if signal_flag.signaled:
+ interface.print_line('Got SIGTERM, exiting...')
+ ninja.process.terminate()
+ break
+ if ninja.exited:
+ break
+ time.sleep(args.ui_update_rate)
+
+ # Finish up the build output.
+ ninja.process.wait()
+ interface.update()
+ interface.print_summary()
+
+ if args.write_trace is not None:
+ ninja.write_trace(args.write_trace)
+ print(f'Wrote trace to {args.write_trace.name}')
+
+ if signal_flag.signaled:
+ interface.dump_running_actions()
+
+ return ninja.process.returncode
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_build/py/pw_build/zip.py b/pw_build/py/pw_build/zip.py
index 4942d67c8..c66433a53 100644
--- a/pw_build/py/pw_build/zip.py
+++ b/pw_build/py/pw_build/zip.py
@@ -33,19 +33,21 @@ def _parse_args():
'--delimiter',
nargs='?',
default=DEFAULT_DELIMITER,
- help='Symbol that separates the path and the zip path destination.')
+ help='Symbol that separates the path and the zip path destination.',
+ )
parser.add_argument(
'--input_list',
nargs='+',
- help='Paths to files and dirs to zip and their desired zip location.')
+ help='Paths to files and dirs to zip and their desired zip location.',
+ )
parser.add_argument('--out_filename', help='Zip file destination.')
return parser.parse_args()
-def zip_up(input_list: Iterable,
- out_filename: str,
- delimiter=DEFAULT_DELIMITER):
+def zip_up(
+ input_list: Iterable, out_filename: str, delimiter=DEFAULT_DELIMITER
+):
"""Zips up all input files/dirs.
Args:
@@ -68,18 +70,21 @@ def zip_up(input_list: Iterable,
except ValueError as value_error:
msg = (
f'Input in the form of "[filename or dir] {delimiter} '
- f'/zip_destination/" expected. Instead got:\n {_input}')
+ f'/zip_destination/" expected. Instead got:\n {_input}'
+ )
raise ZipError(msg) from value_error
if not source:
raise ZipError(
f'Bad input:\n {_input}\nInput source '
f'cannot be empty. Please specify the input in the form '
- f'of "[filename or dir] {delimiter} /zip_destination/".')
+ f'of "[filename or dir] {delimiter} /zip_destination/".'
+ )
if not destination.startswith('/'):
raise ZipError(
f'Bad input:\n {_input}\nZip desination '
f'"{destination}" must start with "/" to indicate the '
- f'zip file\'s root directory.')
+ f'zip file\'s root directory.'
+ )
source_path = pathlib.Path(source)
destination_path = pathlib.PurePath(destination)
@@ -88,8 +93,9 @@ def zip_up(input_list: Iterable,
# Case: "foo.txt > /mydir/"; destination is dir. Put foo.txt
# into mydir as /mydir/foo.txt
if destination.endswith('/'):
- zip_file.write(source_path,
- destination_path / source_path.name)
+ zip_file.write(
+ source_path, destination_path / source_path.name
+ )
# Case: "foo.txt > /bar.txt"; destination is a file--rename the
# source file: put foo.txt into the zip as /bar.txt
else:
@@ -97,24 +103,32 @@ def zip_up(input_list: Iterable,
continue
# Case: the input source path points to a directory.
if source_path.is_dir():
- zip_up_dir(source, source_path, destination, destination_path,
- zip_file)
+ zip_up_dir(
+ source, source_path, destination, destination_path, zip_file
+ )
continue
raise ZipError(f'Unknown source path\n {source_path}')
-def zip_up_dir(source: str, source_path: pathlib.Path, destination: str,
- destination_path: pathlib.PurePath, zip_file: zipfile.ZipFile):
+def zip_up_dir(
+ source: str,
+ source_path: pathlib.Path,
+ destination: str,
+ destination_path: pathlib.PurePath,
+ zip_file: zipfile.ZipFile,
+):
if not source.endswith('/'):
raise ZipError(
f'Source path:\n {source}\nis a directory, but is '
f'missing a trailing "/". The / requirement helps prevent bugs. '
- f'To fix, add a trailing /:\n {source}/')
+ f'To fix, add a trailing /:\n {source}/'
+ )
if not destination.endswith('/'):
raise ZipError(
f'Destination path:\n {destination}\nis a directory, '
f'but is missing a trailing "/". The / requirement helps prevent '
- f'bugs. To fix, add a trailing /:\n {destination}/')
+ f'bugs. To fix, add a trailing /:\n {destination}/'
+ )
# Walk the directory and add zip all of the files with the
# same structure as the source.
diff --git a/pw_build/py/python_runner_test.py b/pw_build/py/python_runner_test.py
index f9e188c37..1379adf3b 100755
--- a/pw_build/py/python_runner_test.py
+++ b/pw_build/py/python_runner_test.py
@@ -20,8 +20,8 @@ import platform
import tempfile
import unittest
-from pw_build.python_runner import ExpressionError, GnPaths, Label, TargetInfo
-from pw_build.python_runner import expand_expressions
+from pw_build.gn_resolver import ExpressionError, GnPaths, Label, TargetInfo
+from pw_build.gn_resolver import expand_expressions
ROOT = Path(r'C:\gn_root' if platform.system() == 'Windows' else '/gn_root')
@@ -35,6 +35,7 @@ TEST_PATHS = GnPaths(
class LabelTest(unittest.TestCase):
"""Tests GN label parsing."""
+
def setUp(self):
self._paths_and_toolchain_name = [
(TEST_PATHS, 'ToolChain'),
@@ -46,40 +47,48 @@ class LabelTest(unittest.TestCase):
label = Label(paths, '//')
self.assertEqual(label.name, '')
self.assertEqual(label.dir, ROOT)
- self.assertEqual(label.out_dir,
- ROOT.joinpath('out', toolchain, 'obj'))
- self.assertEqual(label.gen_dir,
- ROOT.joinpath('out', toolchain, 'gen'))
+ self.assertEqual(
+ label.out_dir, ROOT.joinpath('out', toolchain, 'obj')
+ )
+ self.assertEqual(
+ label.gen_dir, ROOT.joinpath('out', toolchain, 'gen')
+ )
def test_absolute(self):
for paths, toolchain in self._paths_and_toolchain_name:
label = Label(paths, '//foo/bar:baz')
self.assertEqual(label.name, 'baz')
self.assertEqual(label.dir, ROOT.joinpath('foo/bar'))
- self.assertEqual(label.out_dir,
- ROOT.joinpath('out', toolchain, 'obj/foo/bar'))
- self.assertEqual(label.gen_dir,
- ROOT.joinpath('out', toolchain, 'gen/foo/bar'))
+ self.assertEqual(
+ label.out_dir, ROOT.joinpath('out', toolchain, 'obj/foo/bar')
+ )
+ self.assertEqual(
+ label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/foo/bar')
+ )
def test_absolute_implicit_target(self):
for paths, toolchain in self._paths_and_toolchain_name:
label = Label(paths, '//foo/bar')
self.assertEqual(label.name, 'bar')
self.assertEqual(label.dir, ROOT.joinpath('foo/bar'))
- self.assertEqual(label.out_dir,
- ROOT.joinpath('out', toolchain, 'obj/foo/bar'))
- self.assertEqual(label.gen_dir,
- ROOT.joinpath('out', toolchain, 'gen/foo/bar'))
+ self.assertEqual(
+ label.out_dir, ROOT.joinpath('out', toolchain, 'obj/foo/bar')
+ )
+ self.assertEqual(
+ label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/foo/bar')
+ )
def test_relative(self):
for paths, toolchain in self._paths_and_toolchain_name:
label = Label(paths, ':tgt')
self.assertEqual(label.name, 'tgt')
self.assertEqual(label.dir, ROOT.joinpath('some/cwd'))
- self.assertEqual(label.out_dir,
- ROOT.joinpath('out', toolchain, 'obj/some/cwd'))
- self.assertEqual(label.gen_dir,
- ROOT.joinpath('out', toolchain, 'gen/some/cwd'))
+ self.assertEqual(
+ label.out_dir, ROOT.joinpath('out', toolchain, 'obj/some/cwd')
+ )
+ self.assertEqual(
+ label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/some/cwd')
+ )
def test_relative_subdir(self):
for paths, toolchain in self._paths_and_toolchain_name:
@@ -88,30 +97,37 @@ class LabelTest(unittest.TestCase):
self.assertEqual(label.dir, ROOT.joinpath('some/cwd/tgt'))
self.assertEqual(
label.out_dir,
- ROOT.joinpath('out', toolchain, 'obj/some/cwd/tgt'))
+ ROOT.joinpath('out', toolchain, 'obj/some/cwd/tgt'),
+ )
self.assertEqual(
label.gen_dir,
- ROOT.joinpath('out', toolchain, 'gen/some/cwd/tgt'))
+ ROOT.joinpath('out', toolchain, 'gen/some/cwd/tgt'),
+ )
def test_relative_parent_dir(self):
for paths, toolchain in self._paths_and_toolchain_name:
label = Label(paths, '..:tgt')
self.assertEqual(label.name, 'tgt')
self.assertEqual(label.dir, ROOT.joinpath('some'))
- self.assertEqual(label.out_dir,
- ROOT.joinpath('out', toolchain, 'obj/some'))
- self.assertEqual(label.gen_dir,
- ROOT.joinpath('out', toolchain, 'gen/some'))
+ self.assertEqual(
+ label.out_dir, ROOT.joinpath('out', toolchain, 'obj/some')
+ )
+ self.assertEqual(
+ label.gen_dir, ROOT.joinpath('out', toolchain, 'gen/some')
+ )
class ResolvePathTest(unittest.TestCase):
"""Tests GN path resolution."""
+
def test_resolve_absolute(self):
self.assertEqual(TEST_PATHS.resolve('//'), TEST_PATHS.root)
- self.assertEqual(TEST_PATHS.resolve('//foo/bar'),
- TEST_PATHS.root / 'foo' / 'bar')
- self.assertEqual(TEST_PATHS.resolve('//foo/../baz'),
- TEST_PATHS.root / 'baz')
+ self.assertEqual(
+ TEST_PATHS.resolve('//foo/bar'), TEST_PATHS.root / 'foo' / 'bar'
+ )
+ self.assertEqual(
+ TEST_PATHS.resolve('//foo/../baz'), TEST_PATHS.root / 'baz'
+ )
def test_resolve_relative(self):
self.assertEqual(TEST_PATHS.resolve(''), TEST_PATHS.cwd)
@@ -129,6 +145,9 @@ cflags_cc = -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register
target_output_name = this_is_a_test
build fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o: fake_toolchain_cxx ../fake_module/fake_test.cc
+ source_file_dir = ../fake_module
+ source_file_name = fake_test.cc
+
build fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o: fake_toolchain_cc ../fake_module/fake_test_c.c
build fake_toolchain/obj/fake_module/test/fake_test.elf fake_toolchain/obj/fake_module/test/fake_test.map: fake_toolchain_link fake_toolchain/obj/fake_module/fake_test.fake_test.cc.o fake_toolchain/obj/fake_module/fake_test.fake_test_c.c.o
@@ -149,6 +168,9 @@ cflags_cc = -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register
target_output_name = this_is_a_test
build fake_toolchain/obj/fake_module/fake_source_set.file_a.cc.o: fake_toolchain_cxx ../fake_module/file_a.cc
+ source_file_dir = ../fake_module
+ source_file_name = file_a.cc
+
build fake_toolchain/obj/fake_module/fake_source_set.file_b.c.o: fake_toolchain_cc ../fake_module/file_b.c
build {path} fake_toolchain/obj/fake_module/fake_source_set.file_a.cc.o fake_toolchain/obj/fake_module/fake_source_set.file_b.c.o
@@ -180,19 +202,23 @@ def _create_ninja_files(source_set: str) -> tuple:
outdir = Path(tempdir.name, 'out', 'fake_toolchain', 'obj', 'fake_module')
- paths = GnPaths(root=Path(tempdir.name),
- build=Path(tempdir.name, 'out'),
- cwd=Path(tempdir.name, 'some', 'module'),
- toolchain='//tools:fake_toolchain')
+ paths = GnPaths(
+ root=Path(tempdir.name),
+ build=Path(tempdir.name, 'out'),
+ cwd=Path(tempdir.name, 'some', 'module'),
+ toolchain='//tools:fake_toolchain',
+ )
return tempdir, outdir, paths
class TargetTest(unittest.TestCase):
"""Tests querying GN target information."""
+
def setUp(self):
self._tempdir, self._outdir, self._paths = _create_ninja_files(
- NINJA_SOURCE_SET)
+ NINJA_SOURCE_SET
+ )
self._rel_outdir = self._outdir.relative_to(self._paths.build)
@@ -208,51 +234,62 @@ class TargetTest(unittest.TestCase):
target = TargetInfo(self._paths, '//fake_module:fake_source_set')
self.assertTrue(target.generated)
self.assertEqual(
- set(target.object_files), {
+ set(target.object_files),
+ {
self._rel_outdir / 'fake_source_set.file_a.cc.o',
self._rel_outdir / 'fake_source_set.file_b.c.o',
- })
+ },
+ )
def test_executable_object_files(self):
target = TargetInfo(self._paths, '//fake_module:fake_test')
self.assertEqual(
- set(target.object_files), {
+ set(target.object_files),
+ {
self._rel_outdir / 'fake_test.fake_test.cc.o',
self._rel_outdir / 'fake_test.fake_test_c.c.o',
- })
+ },
+ )
def test_executable_artifact(self):
target = TargetInfo(self._paths, '//fake_module:fake_test')
- self.assertEqual(target.artifact,
- self._rel_outdir / 'test' / 'fake_test.elf')
+ self.assertEqual(
+ target.artifact, self._rel_outdir / 'test' / 'fake_test.elf'
+ )
def test_non_existent_target(self):
- target = TargetInfo(self._paths,
- '//fake_module:definitely_not_a_real_target')
+ target = TargetInfo(
+ self._paths, '//fake_module:definitely_not_a_real_target'
+ )
self.assertFalse(target.generated)
self.assertIsNone(target.artifact)
def test_non_existent_toolchain(self):
target = TargetInfo(
- self._paths, '//fake_module:fake_source_set(//not_a:toolchain)')
+ self._paths, '//fake_module:fake_source_set(//not_a:toolchain)'
+ )
self.assertFalse(target.generated)
self.assertIsNone(target.artifact)
class StampTargetTest(TargetTest):
"""Test with old-style .stamp files instead of phony Ninja targets."""
+
def setUp(self):
self._tempdir, self._outdir, self._paths = _create_ninja_files(
- NINJA_SOURCE_SET_STAMP)
+ NINJA_SOURCE_SET_STAMP
+ )
self._rel_outdir = self._outdir.relative_to(self._paths.build)
class ExpandExpressionsTest(unittest.TestCase):
"""Tests expansion of expressions like <TARGET_FILE(//foo)>."""
+
def setUp(self):
self._tempdir, self._outdir, self._paths = _create_ninja_files(
- NINJA_SOURCE_SET)
+ NINJA_SOURCE_SET
+ )
def tearDown(self):
self._tempdir.cleanup()
@@ -270,19 +307,21 @@ class ExpandExpressionsTest(unittest.TestCase):
self.assertEqual(list(expand_expressions(self._paths, '')), [''])
def test_no_expressions(self):
- self.assertEqual(list(expand_expressions(self._paths, 'foobar')),
- ['foobar'])
+ self.assertEqual(
+ list(expand_expressions(self._paths, 'foobar')), ['foobar']
+ )
self.assertEqual(
list(expand_expressions(self._paths, '<NOT_AN_EXPRESSION()>')),
- ['<NOT_AN_EXPRESSION()>'])
+ ['<NOT_AN_EXPRESSION()>'],
+ )
def test_incomplete_expression(self):
for incomplete_expression in [
- '<TARGET_FILE(',
- '<TARGET_FILE(//foo)',
- '<TARGET_FILE(//foo>',
- '<TARGET_FILE(//foo) >',
- '--arg=<TARGET_FILE_IF_EXISTS(//foo) Hello>',
+ '<TARGET_FILE(',
+ '<TARGET_FILE(//foo)',
+ '<TARGET_FILE(//foo>',
+ '<TARGET_FILE(//foo) >',
+ '--arg=<TARGET_FILE_IF_EXISTS(//foo) Hello>',
]:
with self.assertRaises(ExpressionError):
expand_expressions(self._paths, incomplete_expression)
@@ -293,17 +332,21 @@ class ExpandExpressionsTest(unittest.TestCase):
for expr, expected in [
('<TARGET_FILE(//fake_module:fake_test)>', path),
('--arg=<TARGET_FILE(//fake_module:fake_test)>', f'--arg={path}'),
- ('--argument=<TARGET_FILE(//fake_module:fake_test)>;'
- '<TARGET_FILE(//fake_module:fake_test)>',
- f'--argument={path};{path}'),
+ (
+ '--argument=<TARGET_FILE(//fake_module:fake_test)>;'
+ '<TARGET_FILE(//fake_module:fake_test)>',
+ f'--argument={path};{path}',
+ ),
]:
- self.assertEqual(list(expand_expressions(self._paths, expr)),
- [expected])
+ self.assertEqual(
+ list(expand_expressions(self._paths, expr)), [expected]
+ )
def test_target_objects_no_target_file(self):
with self.assertRaisesRegex(ExpressionError, 'no output file'):
- expand_expressions(self._paths,
- '<TARGET_FILE(//fake_module:fake_source_set)>')
+ expand_expressions(
+ self._paths, '<TARGET_FILE(//fake_module:fake_source_set)>'
+ )
def test_target_file_non_existent_target(self):
with self.assertRaisesRegex(ExpressionError, 'generated'):
@@ -314,32 +357,37 @@ class ExpandExpressionsTest(unittest.TestCase):
for expr, expected in [
('<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>', path),
- ('--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
- f'--arg={path}'),
- ('--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;'
- '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
- f'--argument={path};{path}'),
+ (
+ '--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
+ f'--arg={path}',
+ ),
+ (
+ '--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;'
+ '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
+ f'--argument={path};{path}',
+ ),
]:
- self.assertEqual(list(expand_expressions(self._paths, expr)),
- [expected])
+ self.assertEqual(
+ list(expand_expressions(self._paths, expr)), [expected]
+ )
def test_target_file_if_exists_arg_omitted(self):
for expr in [
- '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
- '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test(fake)>',
- '<TARGET_FILE_IF_EXISTS(//not_a_module:nothing)>',
- '--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
- '--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;'
- '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
+ '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
+ '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test(fake)>',
+ '<TARGET_FILE_IF_EXISTS(//not_a_module:nothing)>',
+ '--arg=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
+ '--argument=<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>;'
+ '<TARGET_FILE_IF_EXISTS(//fake_module:fake_test)>',
]:
self.assertEqual(list(expand_expressions(self._paths, expr)), [])
def test_target_file_if_exists_error_if_never_has_artifact(self):
for expr in [
- '<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>'
- 'bar=<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>'
- '<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>',
- '--foo=<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>',
+ '<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>'
+ 'bar=<TARGET_FILE_IF_EXISTS(//fake_module:fake_source_set)>'
+ '<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>',
+ '--foo=<TARGET_FILE_IF_EXISTS(//fake_module:fake_no_objects)>',
]:
with self.assertRaises(ExpressionError):
expand_expressions(self._paths, expr)
@@ -349,35 +397,46 @@ class ExpandExpressionsTest(unittest.TestCase):
set(
expand_expressions(
self._paths,
- '<TARGET_OBJECTS(//fake_module:fake_source_set)>')), {
- self._path('fake_source_set.file_a.cc.o'),
- self._path('fake_source_set.file_b.c.o')
- })
+ '<TARGET_OBJECTS(//fake_module:fake_source_set)>',
+ )
+ ),
+ {
+ self._path('fake_source_set.file_a.cc.o'),
+ self._path('fake_source_set.file_b.c.o'),
+ },
+ )
self.assertEqual(
set(
expand_expressions(
- self._paths, '<TARGET_OBJECTS(//fake_module:fake_test)>')),
+ self._paths, '<TARGET_OBJECTS(//fake_module:fake_test)>'
+ )
+ ),
{
self._path('fake_test.fake_test.cc.o'),
- self._path('fake_test.fake_test_c.c.o')
- })
+ self._path('fake_test.fake_test_c.c.o'),
+ },
+ )
def test_target_objects_no_objects(self):
self.assertEqual(
list(
expand_expressions(
self._paths,
- '<TARGET_OBJECTS(//fake_module:fake_no_objects)>')), [])
+ '<TARGET_OBJECTS(//fake_module:fake_no_objects)>',
+ )
+ ),
+ [],
+ )
def test_target_objects_other_content_in_arg(self):
for arg in [
- '--foo=<TARGET_OBJECTS(//fake_module:fake_no_objects)>',
- '<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar',
- '--foo<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar',
- '<TARGET_OBJECTS(//fake_module:fake_no_objects)>'
- '<TARGET_OBJECTS(//fake_module:fake_no_objects)>',
- '<TARGET_OBJECTS(//fake_module:fake_source_set)>'
- '<TARGET_OBJECTS(//fake_module:fake_source_set)>',
+ '--foo=<TARGET_OBJECTS(//fake_module:fake_no_objects)>',
+ '<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar',
+ '--foo<TARGET_OBJECTS(//fake_module:fake_no_objects)>bar',
+ '<TARGET_OBJECTS(//fake_module:fake_no_objects)>'
+ '<TARGET_OBJECTS(//fake_module:fake_no_objects)>',
+ '<TARGET_OBJECTS(//fake_module:fake_source_set)>'
+ '<TARGET_OBJECTS(//fake_module:fake_source_set)>',
]:
with self.assertRaises(ExpressionError):
expand_expressions(self._paths, arg)
@@ -389,9 +448,11 @@ class ExpandExpressionsTest(unittest.TestCase):
class StampExpandExpressionsTest(TargetTest):
"""Test with old-style .stamp files instead of phony Ninja targets."""
+
def setUp(self):
self._tempdir, self._outdir, self._paths = _create_ninja_files(
- NINJA_SOURCE_SET_STAMP)
+ NINJA_SOURCE_SET_STAMP
+ )
self._rel_outdir = self._outdir.relative_to(self._paths.build)
diff --git a/pw_build/py/setup.cfg b/pw_build/py/setup.cfg
index 72f76984a..37b8b733b 100644
--- a/pw_build/py/setup.cfg
+++ b/pw_build/py/setup.cfg
@@ -22,15 +22,20 @@ description = Python scripts that support the GN build
packages = find:
zip_safe = False
install_requires =
+ # NOTE: These requirements should stay as >= the lowest version we support.
+ build>=0.8.0
wheel
coverage
- pw_cli
- pw_env_setup
- pw_presubmit
+ setuptools
+ types-setuptools
+ mypy>=0.971
+ pylint>=2.9.3
[options.entry_points]
console_scripts =
copy_from_cipd = pw_build.copy_from_cipd:main
+ pw-build = pw_build.project_builder:main
+ pw-wrap-ninja = pw_build.wrap_ninja:main
[options.package_data]
pw_build = py.typed
diff --git a/pw_build/py/zip_test.py b/pw_build/py/zip_test.py
index 57a3e5d21..6af97894a 100644
--- a/pw_build/py/zip_test.py
+++ b/pw_build/py/zip_test.py
@@ -35,17 +35,17 @@ IN_FILENAMES = [
def make_directory(parent_path: pathlib.Path, dir_name: str, filenames: list):
"""Creates a directory and returns a pathlib.Path() of it's root dir.
- Args:
- parent_path: Path to directory where the new directory will be made.
- dir_name: Name of the new directory.
- filenames: list of file contents of the new directory. Also allows
- the creation of subdirectories. Example:
- [
- 'file1.txt',
- 'subdir/file2.txt'
- ]
+ Args:
+ parent_path: Path to directory where the new directory will be made.
+ dir_name: Name of the new directory.
+ filenames: list of file contents of the new directory. Also allows
+ the creation of subdirectories. Example:
+ [
+ 'file1.txt',
+ 'subdir/file2.txt'
+ ]
- Returns: pathlib.Path() to the newly created directory.
+ Returns: pathlib.Path() to the newly created directory.
"""
root_path = pathlib.Path(parent_path / dir_name)
os.mkdir(root_path)
@@ -53,7 +53,7 @@ def make_directory(parent_path: pathlib.Path, dir_name: str, filenames: list):
# Make the sub directories if they don't already exist.
directories = filename.split('/')[:-1]
for i in range(len(directories)):
- directory = pathlib.PurePath('/'.join(directories[:i + 1]))
+ directory = pathlib.PurePath('/'.join(directories[: i + 1]))
if not (root_path / directory).is_dir():
os.mkdir(root_path / directory)
@@ -79,6 +79,7 @@ def get_directory_contents(path: pathlib.Path):
class TestZipping(unittest.TestCase):
"""Tests for the pw_build.zip module."""
+
def test_zip_up_file(self):
with tempfile.TemporaryDirectory() as tmp_dir:
# Arrange.
@@ -95,8 +96,10 @@ class TestZipping(unittest.TestCase):
expected_path = make_directory(tmp_path, 'expected', ['file1.txt'])
# Assert.
- self.assertSetEqual(get_directory_contents(out_path),
- get_directory_contents(expected_path))
+ self.assertSetEqual(
+ get_directory_contents(out_path),
+ get_directory_contents(expected_path),
+ )
def test_zip_up_dir(self):
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -111,16 +114,22 @@ class TestZipping(unittest.TestCase):
out_path = pathlib.Path(f'{tmp_path}/out/')
with zipfile.ZipFile(out_filename, 'r') as zip_file:
zip_file.extractall(out_path)
- expected_path = make_directory(tmp_path, 'expected', [
- 'file3.txt',
- 'file4.txt',
- 'dir2/file5.txt',
- 'dir2/file6.txt',
- ])
+ expected_path = make_directory(
+ tmp_path,
+ 'expected',
+ [
+ 'file3.txt',
+ 'file4.txt',
+ 'dir2/file5.txt',
+ 'dir2/file6.txt',
+ ],
+ )
# Assert.
- self.assertSetEqual(get_directory_contents(out_path),
- get_directory_contents(expected_path))
+ self.assertSetEqual(
+ get_directory_contents(out_path),
+ get_directory_contents(expected_path),
+ )
def test_file_rename(self):
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -135,12 +144,15 @@ class TestZipping(unittest.TestCase):
out_path = pathlib.Path(f'{tmp_path}/out/')
with zipfile.ZipFile(out_filename, 'r') as zip_file:
zip_file.extractall(out_path)
- expected_path = make_directory(tmp_path, 'expected',
- ['renamed.txt'])
+ expected_path = make_directory(
+ tmp_path, 'expected', ['renamed.txt']
+ )
# Assert.
- self.assertSetEqual(get_directory_contents(out_path),
- get_directory_contents(expected_path))
+ self.assertSetEqual(
+ get_directory_contents(out_path),
+ get_directory_contents(expected_path),
+ )
def test_file_move(self):
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -155,12 +167,15 @@ class TestZipping(unittest.TestCase):
out_path = pathlib.Path(f'{tmp_path}/out/')
with zipfile.ZipFile(out_filename, 'r') as zip_file:
zip_file.extractall(out_path)
- expected_path = make_directory(tmp_path, 'expected',
- ['foo/file1.txt'])
+ expected_path = make_directory(
+ tmp_path, 'expected', ['foo/file1.txt']
+ )
# Assert.
- self.assertSetEqual(get_directory_contents(out_path),
- get_directory_contents(expected_path))
+ self.assertSetEqual(
+ get_directory_contents(out_path),
+ get_directory_contents(expected_path),
+ )
def test_dir_move(self):
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -175,16 +190,22 @@ class TestZipping(unittest.TestCase):
out_path = pathlib.Path(f'{tmp_path}/out/')
with zipfile.ZipFile(out_filename, 'r') as zip_file:
zip_file.extractall(out_path)
- expected_path = make_directory(tmp_path, 'expected', [
- 'foo/file3.txt',
- 'foo/file4.txt',
- 'foo/dir2/file5.txt',
- 'foo/dir2/file6.txt',
- ])
+ expected_path = make_directory(
+ tmp_path,
+ 'expected',
+ [
+ 'foo/file3.txt',
+ 'foo/file4.txt',
+ 'foo/dir2/file5.txt',
+ 'foo/dir2/file6.txt',
+ ],
+ )
# Assert.
- self.assertSetEqual(get_directory_contents(out_path),
- get_directory_contents(expected_path))
+ self.assertSetEqual(
+ get_directory_contents(out_path),
+ get_directory_contents(expected_path),
+ )
def test_change_delimiter(self):
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -203,8 +224,10 @@ class TestZipping(unittest.TestCase):
expected_path = make_directory(tmp_path, 'expected', ['file1.txt'])
# Assert.
- self.assertSetEqual(get_directory_contents(out_path),
- get_directory_contents(expected_path))
+ self.assertSetEqual(
+ get_directory_contents(out_path),
+ get_directory_contents(expected_path),
+ )
def test_wrong_input_syntax_raises_error(self):
with tempfile.TemporaryDirectory() as tmp_dir:
diff --git a/pw_build/python.gni b/pw_build/python.gni
index 0f8de8fce..cf7448799 100644
--- a/pw_build/python.gni
+++ b/pw_build/python.gni
@@ -17,18 +17,18 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/input_group.gni")
import("$dir_pw_build/mirror_tree.gni")
import("$dir_pw_build/python_action.gni")
+import("$dir_pw_build/python_gn_args.gni")
import("$dir_pw_protobuf_compiler/toolchain.gni")
declare_args() {
- # Python tasks, such as running tests and Pylint, are done in a single GN
- # toolchain to avoid unnecessary duplication in the build.
- pw_build_PYTHON_TOOLCHAIN = "$dir_pw_build/python_toolchain:python"
-
# Constraints file selection (arguments to pip install --constraint).
# See pip help install.
pw_build_PIP_CONSTRAINTS =
[ "$dir_pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list" ]
+ # Default pip requirements file for all Pigweed based projects.
+ pw_build_PIP_REQUIREMENTS = []
+
# If true, GN will run each Python test using the coverage command. A separate
# coverage data file for each test will be saved. To generate reports from
# this information run: pw presubmit --step gn_python_test_coverage
@@ -68,11 +68,29 @@ template("_pw_create_aliases_if_name_matches_directory") {
template("_pw_python_static_analysis_mypy") {
pw_python_action(target_name) {
module = "mypy"
+
+ # DOCSTAG: [default-mypy-args]
args = [
"--pretty",
"--show-error-codes",
+
+ # TODO(b/265836842): Namespace packages are enabled by default starting in
+ # mypy 0.991. This caused problems in some configurations, so return
+ # to the prior behavior for now.
+ "--no-namespace-packages",
+
+ # Use a mypy cache dir for this target only to avoid cache conflicts in
+ # parallel mypy invocations.
+ "--cache-dir",
+ rebase_path(target_out_dir, root_build_dir) + "/.mypy_cache",
]
+ # Use this environment variable to force mypy to colorize output.
+ # See https://github.com/python/mypy/issues/7771
+ environment = [ "MYPY_FORCE_COLOR=1" ]
+
+ # DOCSTAG: [default-mypy-args]
+
if (defined(invoker.mypy_ini)) {
args +=
[ "--config-file=" + rebase_path(invoker.mypy_ini, root_build_dir) ]
@@ -81,16 +99,19 @@ template("_pw_python_static_analysis_mypy") {
args += rebase_path(invoker.sources, root_build_dir)
- # Use this environment variable to force mypy to colorize output.
- # See https://github.com/python/mypy/issues/7771
- environment = [ "MYPY_FORCE_COLOR=1" ]
-
stamp = true
deps = invoker.deps
- foreach(dep, invoker.python_deps) {
- deps += [ string_replace(dep, "(", ".lint.mypy(") ]
+ if (defined(invoker.python_deps)) {
+ python_deps = []
+ foreach(dep, invoker.python_deps) {
+ deps += [ string_replace(dep, "(", ".lint.mypy(") ]
+ python_deps += [ dep ]
+ }
+ }
+ if (defined(invoker.python_metadata_deps)) {
+ python_metadata_deps = invoker.python_metadata_deps
}
}
}
@@ -123,8 +144,15 @@ template("_pw_python_static_analysis_pylint") {
public_deps = invoker.deps
- foreach(dep, invoker.python_deps) {
- public_deps += [ string_replace(dep, "(", ".lint.pylint(") ]
+ if (defined(invoker.python_deps)) {
+ python_deps = []
+ foreach(dep, invoker.python_deps) {
+ public_deps += [ string_replace(dep, "(", ".lint.pylint(") ]
+ python_deps += [ dep ]
+ }
+ }
+ if (defined(invoker.python_metadata_deps)) {
+ python_metadata_deps = invoker.python_metadata_deps
}
}
}
@@ -192,6 +220,12 @@ template("pw_python_package") {
_generate_package = false
+ _pydeplabel = get_label_info(":$target_name", "label_with_toolchain")
+
+ # If a package does not run static analysis or if it does but doesn't have
+ # any tests then this variable is not used.
+ not_needed([ "_pydeplabel" ])
+
# Check the generate_setup and import_protos args to determine if this package
# is generated.
if (_is_package) {
@@ -322,13 +356,14 @@ template("pw_python_package") {
# targets in this toolchain.
if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
# Create the package_metadata.json file. This is used by the
- # pw_create_python_source_tree template.
+ # pw_python_distribution template.
_package_metadata_json_file =
"$target_gen_dir/$target_name/package_metadata.json"
# Get Python package metadata and write to disk as JSON.
_package_metadata = {
- gn_target_name = get_label_info(invoker.target_name, "label_no_toolchain")
+ gn_target_name =
+ get_label_info(":${invoker.target_name}", "label_no_toolchain")
# Get package source files
sources = rebase_path(_sources, root_build_dir)
@@ -354,6 +389,32 @@ template("pw_python_package") {
# Finally, write out the json
write_file(_package_metadata_json_file, _package_metadata, "json")
+ # Create a target group for the Python package metadata only. This is a
+ # python_action so the setup sources can be included as inputs.
+ pw_python_action("$target_name._package_metadata") {
+ metadata = {
+ pw_python_package_metadata_json = [ _package_metadata_json_file ]
+ }
+
+ script = "$dir_pw_build/py/pw_build/nop.py"
+
+ if (_generate_package) {
+ inputs = [ "$_setup_dir/setup.json" ]
+ } else {
+ inputs = _setup_sources
+ }
+
+ _pw_internal_run_in_venv = false
+ stamp = true
+
+ # Forward the package_metadata subtarget for all python_deps.
+ public_deps = []
+ foreach(dep, _python_deps) {
+ public_deps += [ get_label_info(dep, "label_no_toolchain") +
+ "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+ }
+
# Declare the main Python package group. This represents the Python files,
# but does not take any actions. GN targets can depend on the package name
# to run when any files in the package change.
@@ -456,63 +517,13 @@ template("pw_python_package") {
inputs += invoker.inputs
}
- deps = _python_deps + _other_deps
+ public_deps = _python_deps + _other_deps
}
}
if (_is_package) {
- # Install this Python package and its dependencies in the current Python
- # environment using pip.
- pw_python_action("$target_name._run_pip_install") {
- module = "pip"
- public_deps = []
- if (defined(invoker.public_deps)) {
- public_deps += invoker.public_deps
- }
-
- args = [
- "install",
-
- # This speeds up pip installs. At this point in the gn build the
- # virtualenv is already activated so build isolation isn't required.
- # This requires that pip, setuptools, and wheel packages are
- # installed.
- "--no-build-isolation",
- ]
-
- inputs = pw_build_PIP_CONSTRAINTS
- foreach(_constraints_file, pw_build_PIP_CONSTRAINTS) {
- args += [
- "--constraint",
- rebase_path(_constraints_file, root_build_dir),
- ]
- }
-
- # For generated packages, reinstall when any files change. For regular
- # packages, only reinstall when setup.py changes.
- if (_generate_package) {
- public_deps += [ ":${invoker.target_name}" ]
- } else {
- inputs += invoker.setup
-
- # Install with --editable since the complete package is in source.
- args += [ "--editable" ]
- }
-
- args += [ rebase_path(_setup_dir, root_build_dir) ]
-
- stamp = true
-
- # Parallel pip installations don't work, so serialize pip invocations.
- pool = "$dir_pw_build/pool:pip($default_toolchain)"
-
- foreach(dep, _python_deps) {
- # We need to add a suffix to the target name, but the label is
- # formatted as "//path/to:target(toolchain)", so we can't just append
- # ".subtarget". Instead, we replace the opening parenthesis of the
- # toolchain with ".suffix(".
- public_deps += [ string_replace(dep, "(", "._run_pip_install(") ]
- }
+ # Stubs for non-package targets.
+ group("$target_name._run_pip_install") {
}
# Builds a Python wheel for this package. Records the output directory
@@ -588,8 +599,10 @@ template("pw_python_package") {
if (_static_analysis != [] || _test_sources != []) {
# All packages to install for either general use or test running.
_test_install_deps = [ ":$target_name.install" ]
- foreach(dep, _python_test_deps + [ "$dir_pw_build:python_lint" ]) {
+
+ foreach(dep, _python_test_deps) {
_test_install_deps += [ string_replace(dep, "(", ".install(") ]
+ _test_install_deps += [ dep ]
}
}
@@ -601,7 +614,11 @@ template("pw_python_package") {
target("_pw_python_static_analysis_$_tool", "$target_name.lint.$_tool") {
sources = _all_py_files
deps = _test_install_deps
- python_deps = _python_deps
+ python_deps = _python_deps + _python_test_deps
+
+ if (_is_package) {
+ python_metadata_deps = [ _pydeplabel ]
+ }
_optional_variables = [
"mypy_ini",
@@ -693,6 +710,16 @@ template("pw_python_package") {
stamp = true
+ # Make sure the python test deps are added to the PYTHONPATH.
+ python_metadata_deps = _python_test_deps
+
+ # If this is a test for a package, add it to PYTHONPATH as well. This is
+ # required if the test source file isn't in the same directory as the
+ # folder containing the package sources to allow local Python imports.
+ if (_is_package) {
+ python_metadata_deps += [ _pydeplabel ]
+ }
+
deps = _test_install_deps
foreach(dep, _python_test_deps) {
@@ -738,6 +765,16 @@ template("pw_python_group") {
}
}
+ # Create a target group for the Python package metadata only.
+ group("$target_name._package_metadata") {
+ # Forward the package_metadata subtarget for all python_deps.
+ public_deps = []
+ foreach(dep, _python_deps) {
+ public_deps += [ get_label_info(dep, "label_no_toolchain") +
+ "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+ }
+
foreach(subtarget, pw_python_package_subtargets) {
group("$target_name.$subtarget") {
public_deps = []
@@ -774,6 +811,8 @@ template("pw_python_script") {
"sources",
"tests",
"python_deps",
+ "python_test_deps",
+ "python_metadata_deps",
"other_deps",
"inputs",
"pylintrc",
@@ -834,29 +873,8 @@ template("pw_python_requirements") {
# Use the same subtargets as pw_python_package so these targets can be listed
# as python_deps of pw_python_packages.
- pw_python_action("$target_name.install") {
- inputs = _requirements_files
-
- module = "pip"
- args = [ "install" ]
-
- foreach(_requirements_file, inputs) {
- args += [
- "--requirement",
- rebase_path(_requirements_file, root_build_dir),
- ]
- }
-
- inputs += pw_build_PIP_CONSTRAINTS
- foreach(_constraints_file, pw_build_PIP_CONSTRAINTS) {
- args += [
- "--constraint",
- rebase_path(_constraints_file, root_build_dir),
- ]
- }
-
- pool = "$dir_pw_build/pool:pip($default_toolchain)"
- stamp = true
+ group("$target_name.install") {
+ # TODO(b/232800695): Remove reliance on this subtarget existing.
}
# Create stubs for the unused subtargets so that pw_python_requirements can be
@@ -866,6 +884,18 @@ template("pw_python_requirements") {
}
}
+ # Create a target group for the Python package metadata only.
+ group("$target_name._package_metadata") {
+ # Forward the package_metadata subtarget for all python_deps.
+ public_deps = []
+ if (defined(invoker.python_deps)) {
+ foreach(dep, invoker.python_deps) {
+ public_deps += [ get_label_info(dep, "label_no_toolchain") +
+ "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+ }
+ }
+
_pw_create_aliases_if_name_matches_directory(target_name) {
}
}
diff --git a/pw_build/python.rst b/pw_build/python.rst
index 3eb9b5fb1..3a50a75b1 100644
--- a/pw_build/python.rst
+++ b/pw_build/python.rst
@@ -1,12 +1,30 @@
.. _module-pw_build-python:
--------------------
-Python GN templates
--------------------
+===================
+Python GN Templates
+===================
The Python build is implemented with GN templates defined in
``pw_build/python.gni``. See the .gni file for complete usage documentation.
-.. seealso:: :ref:`docs-python-build`
+.. seealso::
+
+ - :bdg-ref-primary-line:`docs-python-build` for an overview on how Python in
+ GN is built.
+ - The :bdg-ref-primary-line:`module-pw_build` docs for other GN templates
+ available within Pigweed.
+
+.. _module-pw_build-python-base-templates:
+
+---------------------
+Python Base Templates
+---------------------
+The core subset of templates where you can create Python packages, actions,
+scripts and group them together are listed below.
+
+- :ref:`module-pw_build-pw_python_package`
+- :ref:`module-pw_build-pw_python_action`
+- :ref:`module-pw_build-pw_python_script`
+- :ref:`module-pw_build-pw_python_group`
.. _module-pw_build-pw_python_package:
@@ -30,8 +48,8 @@ same as the directory. For example, these two labels are equivalent:
.. code-block::
- //path/to/my_python_package:my_python_package.tests
- //path/to/my_python_package:tests
+ //path/to/my_python_package:my_python_package.tests
+ //path/to/my_python_package:tests
The actions in a ``pw_python_package`` (e.g. installing packages and running
Pylint) are done within a single GN toolchain to avoid duplication in
@@ -51,15 +69,15 @@ Arguments
.. code-block::
- generate_setup = {
- metadata = {
- name = "a_nifty_package"
- version = "1.2a"
- }
- options = {
- install_requires = [ "a_pip_package" ]
- }
- }
+ generate_setup = {
+ metadata = {
+ name = "a_nifty_package"
+ version = "1.2a"
+ }
+ options = {
+ install_requires = [ "a_pip_package" ]
+ }
+ }
- ``sources`` - Python sources files in the package.
- ``tests`` - Test files for this Python package.
@@ -87,33 +105,45 @@ This is an example Python package declaration for a ``pw_my_module`` module.
.. code-block::
- import("//build_overrides/pigweed.gni")
-
- import("$dir_pw_build/python.gni")
-
- pw_python_package("py") {
- setup = [
- "pyproject.toml",
- "setup.cfg",
- "setup.py",
- ]
- sources = [
- "pw_my_module/__init__.py",
- "pw_my_module/alfa.py",
- "pw_my_module/bravo.py",
- "pw_my_module/charlie.py",
- ]
- tests = [
- "alfa_test.py",
- "charlie_test.py",
- ]
- python_deps = [
- "$dir_pw_status/py",
- ":some_protos.python",
- ]
- python_test_deps = [ "$dir_pw_build/py" ]
- pylintrc = "$dir_pigweed/.pylintrc"
- }
+ import("//build_overrides/pigweed.gni")
+
+ import("$dir_pw_build/python.gni")
+
+ pw_python_package("py") {
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ "setup.py",
+ ]
+ sources = [
+ "pw_my_module/__init__.py",
+ "pw_my_module/alfa.py",
+ "pw_my_module/bravo.py",
+ "pw_my_module/charlie.py",
+ ]
+ tests = [
+ "alfa_test.py",
+ "charlie_test.py",
+ ]
+ python_deps = [
+ "$dir_pw_status/py",
+ ":some_protos.python",
+ ]
+ python_test_deps = [ "$dir_pw_build/py" ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ }
+
+.. _module-pw_build-pw_python_action:
+
+pw_python_action
+================
+The ``pw_python_action`` template is a convenience wrapper around GN's `action
+function <https://gn.googlesource.com/gn/+/main/docs/reference.md#func_action>`_
+for running Python scripts. See
+:bdg-ref-primary-line:`module-pw_build-python-action` in the ``pw_build``
+documentation for usage.
+
+.. _module-pw_build-pw_python_script:
pw_python_script
================
@@ -141,36 +171,156 @@ An action in ``pw_python_script`` can always be replaced with a standalone
- Using a ``pw_python_script`` with an embedded action is a simple way to check
an existing action's script with Pylint or Mypy or to add tests.
+.. _module-pw_build-pw_python_group:
+
pw_python_group
===============
Represents a group of ``pw_python_package`` and ``pw_python_script`` targets.
These targets do not add any files. Their subtargets simply forward to those of
their dependencies.
-pw_python_requirements
-======================
-Represents a set of local and PyPI requirements, with no associated source
-files. These targets serve the role of a ``requirements.txt`` file.
+.. code-block::
+
+ pw_python_group("solar_system_python_packages") {
+ python_deps = [
+ "//planets/mercury/py",
+ "//planets/venus/py",
+ "//planets/earth/py",
+ "//planets/mars/py",
+ "//planets/jupiter/py",
+ "//planets/saturn/py",
+ "//planets/uranus/py",
+ "//planets/neptune/py",
+ "//planetoids/ceres/py",
+ "//planetoids/pluto/py",
+ ]
+ }
+
+----------------------------
+Python Environment Templates
+----------------------------
+Templates that manage the Python build and bootstrap environment are listed
+here.
+
+- :ref:`module-pw_build-pw_python_venv`
+- :ref:`module-pw_build-pw_python_pip_install`
+
+.. _module-pw_build-pw_python_venv:
+
+pw_python_venv
+==============
+Defines and creates a Python virtualenv. This template is used by Pigweed in
+https://cs.pigweed.dev/pigweed/+/main:pw_env_setup/BUILD.gn to create a
+virtualenv for use within the GN build that all Python actions will run in.
+
+Example
+-------
+.. code-block::
+ :caption: Example of a typical Python venv definition in a top level
+ :octicon:`file;1em` ``BUILD.gn``
+
+ declare_args() {
+ pw_build_PYTHON_BUILD_VENV = "//:my_build_venv"
+ }
+
+ pw_python_group("my_product_packages") {
+ python_deps = [
+ "//product_dev_tools/py",
+ "//product_release_tools/py",
+ ]
+ }
+
+ pw_python_venv("my_build_venv") {
+ path = "$root_build_dir/python-build-venv"
+ constraints = [ "//tools/constraints.list" ]
+ requirements = [ "//tools/requirements.txt" ]
+ source_packages = [
+ "$dir_pw_env_setup:core_pigweed_python_packages",
+ "//tools:another_pw_python_package",
+ "//:my_product_packages",
+ ]
+ }
+
+Arguments
+---------
+- ``path``: The directory where the virtualenv will be created. This is relative
+ to the GN root and must begin with "$root_build_dir/" if it lives in the
+ output directory or "//" if it lives in elsewhere.
-When packages are installed by Pigweed, additional version constraints can be
-provided using the ``pw_build_PIP_CONSTRAINTS`` GN arg. This option should
-contain a list of paths to pass to the ``--constraint`` option of ``pip
-install``. This can be used to synchronize dependency upgrades across a project
-which facilitates reproducibility of builds.
+- ``constraints``: A list of constraint files used when performing pip install
+ into this virtualenv. By default this is set to the
+ ``pw_build_PIP_CONSTRAINTS`` GN arg.
-Note using multiple ``pw_python_requirements`` that install different versions
-of the same package will currently cause unpredictable results, while using
-constraints should have correct results (which may be an error indicating a
-conflict).
+- ``requirements``: A list of requirements files to install into this virtualenv
+ on creation. By default this is set to the ``pw_build_PIP_REQUIREMENTS`` GN
+ arg.
+
+ .. seealso::
+
+ For more info on the ``pw_build_PIP_CONSTRAINTS`` and
+ ``pw_build_PIP_REQUIREMENTS`` GN args see:
+ :ref:`docs-python-build-python-gn-requirements-files`
+
+- ``source_packages``: A list of in-tree
+ :ref:`module-pw_build-pw_python_package` or targets that will be checked for
+ external third_party pip dependencies to install into this
+ virtualenv. Note this list of targets isn't actually installed into the
+ virtualenv. Only packages defined inside the [options] install_requires
+ section of each pw_python_package's setup.cfg will be pip installed.
+
+ .. seealso::
+
+ For an example ``setup.cfg`` file see: `Configuring setuptools using
+ setup.cfg files
+ <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`_
+
+
+.. _module-pw_build-pw_python_pip_install:
+
+pw_python_pip_install
+=====================
+This will pip install ``pw_python_package`` targets into the bootstrapped
+developer environment.
+
+Example
+-------
+.. code-block::
+ :caption: Example of a typical Python venv definition in a top level
+ :octicon:`file;1em` ``BUILD.gn``
+
+ pw_python_pip_install("pip_install_my_product_packages") {
+ packages = [
+ "//product_dev_tools/py",
+ "//product_release_tools/py",
+ ]
+ }
+
+Arguments
+---------
+
+- ``packages``: A list of :ref:`module-pw_build-pw_python_package` targets to be
+ pip installed. All packages specified will be installed using a single ``pip
+ install`` command with a ``--constraint`` argument for each constraint file in
+ the ``pw_build_PIP_CONSTRAINTS`` GN arg.
+
+- ``editable``: If true, --editable is passed to the pip install command.
+
+- ``force_reinstall``: If true, ``--force-reinstall`` is passed to the pip
+ install command.
.. _module-pw_build-python-dist:
----------------------
-Python Distributables
----------------------
+------------------------------
+Python Distributable Templates
+------------------------------
Pigweed also provides some templates to make it easier to bundle Python packages
-for deployment. These templates are found in ``pw_build/python_dist.gni``. See
-the .gni file for complete documentation on building distributables.
+for deployment. These templates are found in ``pw_build/python_dist.gni``.
+
+- :ref:`module-pw_build-pw_python_wheels`
+- :ref:`module-pw_build-pw_python_zip_with_setup`
+- :ref:`module-pw_build-pw_python_distribution`
+
+.. _module-pw_build-pw_python_wheels:
pw_python_wheels
================
@@ -196,6 +346,8 @@ out which wheels to collect by traversing the ``pw_python_package_wheels``
<https://gn.googlesource.com/gn/+/HEAD/docs/reference.md#var_metadata>`_ key,
which lists the output directory for each wheel.
+.. _module-pw_build-pw_python_zip_with_setup:
+
pw_python_zip_with_setup
========================
Generates a ``.zip`` archive suitable for deployment outside of the project's
@@ -231,18 +383,19 @@ Example
.. code-block::
- import("//build_overrides/pigweed.gni")
+ import("//build_overrides/pigweed.gni")
- import("$dir_pw_build/python_dist.gni")
+ import("$dir_pw_build/python_dist.gni")
- pw_python_zip_with_setup("my_tools") {
- packages = [ ":some_python_package" ]
- inputs = [ "$dir_pw_build/python_dist/README.md > /${target_name}/" ]
- }
+ pw_python_zip_with_setup("my_tools") {
+ packages = [ ":some_python_package" ]
+ inputs = [ "$dir_pw_build/python_dist/README.md > /${target_name}/" ]
+ }
-pw_create_python_source_tree
-============================
+.. _module-pw_build-pw_python_distribution:
+pw_python_distribution
+======================
Generates a directory of Python packages from source files suitable for
deployment outside of the project developer environment. The resulting directory
contains only files mentioned in each package's ``setup.cfg`` file. This is
@@ -273,34 +426,58 @@ Arguments
file would be overwritten an error is raised.
- ``generate_setup_cfg`` - If included, create a merged ``setup.cfg`` for all
- python Packages using a ``common_config_file`` as a base. That file should
- contain the required fields in the ``metadata`` and ``options`` sections as
- shown in
+ python Packages using either a ``common_config_file`` as a base or ``name``
+ and ``version`` strings. The ``common_config_file`` should contain the
+ required fields in the ``metadata`` and ``options`` sections as shown in
`Configuring setup() using setup.cfg files <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`_.
- ``append_git_sha_to_version`` and ``append_date_to_version`` will optionally
- append the current git SHA or date to the package version string after a ``+``
- sign.
+ ``append_git_sha_to_version = true`` and ``append_date_to_version = true``
+ will optionally append the current git SHA or date to the package version
+ string after a ``+`` sign. You can also opt to include a generated
+ ``pyproject.toml`` file by setting ``include_default_pyproject_file = true``.
.. code-block::
+ :caption: :octicon:`file;1em` Example using a common setup.cfg and
+ pyproject.toml files.
generate_setup_cfg = {
common_config_file = "pypi_common_setup.cfg"
- append_git_sha_to_version = true
append_date_to_version = true
}
+ extra_files = [
+ "//source/pyproject.toml > pyproject.toml"
+ ]
+
+ .. code-block::
+ :caption: :octicon:`file;1em` Example using name and version strings and a
+ default pyproject.toml file.
+
+ generate_setup_cfg = {
+ name = "awesome"
+ version = "1.0.0"
+ include_default_pyproject_file = true
+ append_date_to_version = true
+ }
+
+Using this template will create an additional target for and building a Python
+wheel. For example if you define ``pw_python_distribution("awesome")`` the
+resulting targets that get created will be:
+
+- ``awesome`` - This will create the merged package with all source files in
+ place in the out directory under ``out/obj/awesome/``.
+- ``awesome.wheel`` - This builds a Python wheel from the above source files
+ under ``out/obj/awesome._build_wheel/awesome*.whl``.
Example
-------
-:octicon:`file;1em` ./pw_env_setup/BUILD.gn
-
.. code-block::
+ :caption: :octicon:`file;1em` ./pw_env_setup/BUILD.gn
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python_dist.gni")
- pw_create_python_source_tree("build_python_source_tree") {
+ pw_python_distribution("build_python_source_tree") {
packages = [
":some_python_package",
":another_python_package",
@@ -318,9 +495,10 @@ Example
}
}
-:octicon:`file-directory;1em` ./out/obj/pw_env_setup/build_python_source_tree/
.. code-block:: text
+ :caption: :octicon:`file-directory;1em`
+ ./out/obj/pw_env_setup/build_python_source_tree/
$ tree ./out/obj/pw_env_setup/build_python_source_tree/
├── README.md
diff --git a/pw_build/python_action.gni b/pw_build/python_action.gni
index 1110535cf..0c7662774 100644
--- a/pw_build/python_action.gni
+++ b/pw_build/python_action.gni
@@ -14,6 +14,8 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/python_gn_args.gni")
+
# Defines an action that runs a Python script.
#
# This wraps a regular Python script GN action with an invocation of a script-
@@ -51,6 +53,13 @@ import("//build_overrides/pigweed.gni")
# working_directory Switch to the provided working directory before running
# the Python script or action.
#
+# command_launcher Arguments to prepend to the Python command, e.g.
+# '/usr/bin/fakeroot --' to run the Python script within a
+# fakeroot environment.
+#
+# venv Optional gn target of the pw_python_venv that should be used
+# to run this action.
+#
template("pw_python_action") {
assert(defined(invoker.script) != defined(invoker.module),
"pw_python_action requires either 'script' or 'module'")
@@ -65,15 +74,12 @@ template("pw_python_action") {
"--current-path",
rebase_path(".", root_build_dir),
- # pip lockfile, prevents pip from running in parallel with other Python
- # actions.
- "--lockfile",
- rebase_path("$root_out_dir/pip.lock", root_build_dir),
-
"--default-toolchain=$default_toolchain",
"--current-toolchain=$current_toolchain",
]
+ _use_build_dir_virtualenv = true
+
if (defined(invoker.environment)) {
foreach(variable, invoker.environment) {
_script_args += [ "--env=$variable" ]
@@ -123,6 +129,17 @@ template("pw_python_action") {
"--module",
invoker.module,
]
+
+ # Pip installs should only ever need to occur in the Pigweed
+ # environment. For these actions do not use the build_dir virtualenv.
+ if (invoker.module == "pip") {
+ _use_build_dir_virtualenv = false
+ }
+ }
+
+ # Override to force using or not using the venv.
+ if (defined(invoker._pw_internal_run_in_venv)) {
+ _use_build_dir_virtualenv = invoker._pw_internal_run_in_venv
}
if (defined(invoker.working_directory)) {
@@ -132,17 +149,11 @@ template("pw_python_action") {
]
}
- # "--" indicates the end of arguments to the runner script.
- # Everything beyond this point is interpreted as the command and arguments
- # of the Python script to run.
- _script_args += [ "--" ]
-
- if (defined(invoker.script)) {
- _script_args += [ rebase_path(invoker.script, root_build_dir) ]
- }
-
- if (defined(invoker.args)) {
- _script_args += invoker.args
+ if (defined(invoker.command_launcher)) {
+ _script_args += [
+ "--command-launcher",
+ invoker.command_launcher,
+ ]
}
if (defined(invoker._pw_action_type)) {
@@ -157,15 +168,111 @@ template("pw_python_action") {
_deps = []
}
+ _py_metadata_deps = []
+
if (defined(invoker.python_deps)) {
foreach(dep, invoker.python_deps) {
_deps += [ get_label_info(dep, "label_no_toolchain") + ".install(" +
get_label_info(dep, "toolchain") + ")" ]
+
+ # Ensure each python_dep is added to the PYTHONPATH by depinding on the
+ # ._package_metadata subtarget.
+ _py_metadata_deps += [ get_label_info(dep, "label_no_toolchain") +
+ "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
}
# Add the base target as a dep so the action reruns when any source files
# change, even if the package does not have to be reinstalled.
_deps += invoker.python_deps
+ _deps += _py_metadata_deps
+ }
+
+ # Check for additional PYTHONPATH dependencies.
+ _extra_python_metadata_deps = []
+ if (defined(invoker.python_metadata_deps)) {
+ foreach(dep, invoker.python_metadata_deps) {
+ _extra_python_metadata_deps +=
+ [ get_label_info(dep, "label_no_toolchain") +
+ "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+ }
+
+ _metadata_path_list_file =
+ "${target_gen_dir}/${target_name}_metadata_path_list.txt"
+
+ # GN metadata only dependencies used for setting PYTHONPATH.
+ _metadata_deps = _py_metadata_deps + _extra_python_metadata_deps
+
+ # Build a list of relative paths containing all the python
+ # package_metadata.json files we depend on.
+ _metadata_path_list_target = "${target_name}._metadata_path_list.txt"
+ generated_file(_metadata_path_list_target) {
+ data_keys = [ "pw_python_package_metadata_json" ]
+ rebase = root_build_dir
+ deps = _metadata_deps
+ outputs = [ _metadata_path_list_file ]
+ }
+ _deps += [ ":${_metadata_path_list_target}" ]
+
+ # Set venv options if needed.
+ if (_use_build_dir_virtualenv) {
+ _venv_target_label = pw_build_PYTHON_BUILD_VENV
+ if (defined(invoker.venv)) {
+ _venv_target_label = invoker.venv
+ }
+ _venv_target_label =
+ get_label_info(_venv_target_label, "label_no_toolchain") +
+ "($pw_build_PYTHON_TOOLCHAIN)"
+
+ _venv_json =
+ get_label_info(_venv_target_label, "target_gen_dir") + "/" +
+ get_label_info(_venv_target_label, "name") + "/venv_metadata.json"
+ _script_args += [
+ "--python-virtualenv-config",
+ rebase_path(_venv_json, root_build_dir),
+ ]
+ }
+ _script_args += [
+ "--python-dep-list-files",
+ rebase_path(_metadata_path_list_file, root_build_dir),
+ ]
+
+ # "--" indicates the end of arguments to the runner script.
+ # Everything beyond this point is interpreted as the command and arguments
+ # of the Python script to run.
+ _script_args += [ "--" ]
+
+ if (defined(invoker.script)) {
+ _script_args += [ rebase_path(invoker.script, root_build_dir) ]
+ }
+
+ _forward_python_metadata_deps = false
+ if (defined(invoker._forward_python_metadata_deps)) {
+ _forward_python_metadata_deps = true
+ }
+ if (_forward_python_metadata_deps) {
+ _script_args += [
+ "--python-dep-list-files",
+ rebase_path(_metadata_path_list_file, root_build_dir),
+ ]
+ }
+
+ if (defined(invoker.args)) {
+ _script_args += invoker.args
+ }
+
+ # Assume third party PyPI deps should be available in the build_dir virtualenv.
+ _install_venv_3p_deps = true
+ if (!_use_build_dir_virtualenv ||
+ (defined(invoker._skip_installing_external_python_deps) &&
+ invoker._skip_installing_external_python_deps)) {
+ _install_venv_3p_deps = false
+ }
+
+ # Check that script or module is a present and not a no-op.
+ _run_script_or_module = false
+ if (defined(invoker.script) || defined(invoker.module)) {
+ _run_script_or_module = true
}
target(_action_type, target_name) {
@@ -183,6 +290,11 @@ template("pw_python_action") {
inputs = _inputs
outputs = _outputs
deps = _deps
+
+ if (_install_venv_3p_deps && _run_script_or_module) {
+ deps += [ get_label_info(_venv_target_label, "label_no_toolchain") +
+ "._install_3p_deps($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
}
}
diff --git a/pw_build/python_dist.gni b/pw_build/python_dist.gni
index 36f797ab0..12f0afdbc 100644
--- a/pw_build/python_dist.gni
+++ b/pw_build/python_dist.gni
@@ -14,8 +14,10 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/error.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/python_action.gni")
+import("$dir_pw_build/python_gn_args.gni")
import("$dir_pw_build/zip.gni")
# Builds a directory containing a collection of Python wheels.
@@ -65,6 +67,7 @@ template("pw_python_wheels") {
forward_variables_from(invoker, [ "public_deps" ])
deps = _deps + [ ":$target_name._wheel_paths" ]
module = "pw_build.collect_wheels"
+ python_deps = [ "$dir_pw_build/py" ]
args = [
"--prefix",
@@ -131,7 +134,7 @@ template("pw_python_zip_with_setup") {
output = _zip_path
- # TODO(pwbug/634): Remove the plumbing-through of invoker's public_deps.
+ # TODO(b/235245034): Remove the plumbing-through of invoker's public_deps.
public_deps = _public_deps + [ ":${_outer_name}.wheels" ]
}
}
@@ -153,16 +156,28 @@ template("pw_python_zip_with_setup") {
# extra_files: A list of extra files that should be included in the output. The
# format of each item in this list follows this convention:
# //some/nested/source_file > nested/destination_file
-template("pw_create_python_source_tree") {
+#
+# generate_setup_cfg: A scope containing either common_config_file or 'name'
+# and 'version' If included this creates a merged setup.cfg for all python
+# Packages using either a common_config_file as a base or name and version
+# strings. This scope can optionally include: 'append_git_sha_to_version' or
+# 'append_date_to_version' whic append the current git SHA or date to the
+# package version string after a + sign.
+#
+template("pw_python_distribution") {
+ _metadata_path_list_suffix = "_pw_python_distribution_metadata_path_list.txt"
_output_dir = "${target_out_dir}/${target_name}/"
_metadata_json_file_list =
- "${target_gen_dir}/${target_name}_metadata_path_list.txt"
+ "${target_gen_dir}/${target_name}${_metadata_path_list_suffix}"
# If generating a setup.cfg file a common base file must be provided.
if (defined(invoker.generate_setup_cfg)) {
generate_setup_cfg = invoker.generate_setup_cfg
- assert(defined(generate_setup_cfg.common_config_file),
- "'common_config_file' is required in generate_setup_cfg")
+ assert(
+ defined(generate_setup_cfg.common_config_file) ||
+ (defined(generate_setup_cfg.name) &&
+ defined(generate_setup_cfg.version)),
+ "Either 'common_config_file' or ('name' + 'version') are required in generate_setup_cfg")
}
_extra_file_inputs = []
@@ -200,23 +215,90 @@ template("pw_create_python_source_tree") {
_include_tests = defined(invoker.include_tests) && invoker.include_tests
+ _public_deps = []
+ if (defined(invoker.public_deps)) {
+ _public_deps += invoker.public_deps
+ }
+
+ # Set source files for the Python package metadata json file.
+ _sources = []
+ _setup_sources = [
+ "$_output_dir/pyproject.toml",
+ "$_output_dir/setup.cfg",
+ ]
+ _test_sources = []
+
+ # Create the Python package_metadata.json file so this can be used as a
+ # Python dependency.
+ _package_metadata_json_file =
+ "$target_gen_dir/$target_name/package_metadata.json"
+
+ # Get Python package metadata and write to disk as JSON.
+ _package_metadata = {
+ gn_target_name =
+ get_label_info(":${invoker.target_name}", "label_no_toolchain")
+
+ # Get package source files
+ sources = rebase_path(_sources, root_build_dir)
+
+ # Get setup.cfg, pyproject.toml, or setup.py file
+ setup_sources = rebase_path(_setup_sources, root_build_dir)
+
+ # Get test source files
+ tests = rebase_path(_test_sources, root_build_dir)
+
+ # Get package input files (package data)
+ inputs = []
+ if (defined(invoker.inputs)) {
+ inputs = rebase_path(invoker.inputs, root_build_dir)
+ }
+ inputs += rebase_path(_extra_file_inputs, root_build_dir)
+ }
+
+ # Finally, write out the json
+ write_file(_package_metadata_json_file, _package_metadata, "json")
+
+ group("$target_name._package_metadata") {
+ metadata = {
+ pw_python_package_metadata_json = [ _package_metadata_json_file ]
+ }
+ }
+
+ _package_metadata_targets = []
+ foreach(pkg, invoker.packages) {
+ _package_metadata_targets +=
+ [ get_label_info(pkg, "label_no_toolchain") +
+ "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+
# Build a list of relative paths containing all the python
# package_metadata.json files we depend on.
- generated_file("${target_name}._metadata_path_list.txt") {
+ generated_file("${target_name}.${_metadata_path_list_suffix}") {
data_keys = [ "pw_python_package_metadata_json" ]
rebase = root_build_dir
- deps = invoker.packages
+ deps = _package_metadata_targets
outputs = [ _metadata_json_file_list ]
}
# Run the python action on the metadata_path_list.txt file
pw_python_action(target_name) {
- deps =
- invoker.packages + [ ":${invoker.target_name}._metadata_path_list.txt" ]
+ # Save the Python package metadata so this can be installed using
+ # pw_internal_pip_install.
+ metadata = {
+ pw_python_package_metadata_json = [ _package_metadata_json_file ]
+ }
+
+ deps = invoker.packages +
+ [ ":${invoker.target_name}.${_metadata_path_list_suffix}" ]
+
script = "$dir_pw_build/py/pw_build/create_python_tree.py"
inputs = _extra_file_inputs
+ public_deps = _public_deps
+ _pw_internal_run_in_venv = false
args = [
+ "--repo-root",
+ rebase_path("//", root_build_dir),
"--tree-destination-dir",
rebase_path(_output_dir, root_build_dir),
"--input-list-files",
@@ -237,6 +319,22 @@ template("pw_create_python_source_tree") {
if (defined(generate_setup_cfg.append_date_to_version)) {
args += [ "--setupcfg-version-append-date" ]
}
+ if (defined(generate_setup_cfg.name)) {
+ args += [
+ "--setupcfg-override-name",
+ generate_setup_cfg.name,
+ ]
+ }
+ if (defined(generate_setup_cfg.version)) {
+ args += [
+ "--setupcfg-override-version",
+ generate_setup_cfg.version,
+ ]
+ }
+ if (defined(generate_setup_cfg.include_default_pyproject_file) &&
+ generate_setup_cfg.include_default_pyproject_file == true) {
+ args += [ "--create-default-pyproject-toml" ]
+ }
}
if (_extra_file_args == []) {
@@ -254,4 +352,193 @@ template("pw_create_python_source_tree") {
args += [ "--include-tests" ]
}
}
+
+ # Template to build a bundled Python package wheel.
+ pw_python_action("$target_name._build_wheel") {
+ metadata = {
+ pw_python_package_wheels = [ "$target_out_dir/$target_name" ]
+ }
+ module = "build"
+ args = [
+ rebase_path(_output_dir, root_build_dir),
+ "--wheel",
+ "--no-isolation",
+ "--outdir",
+ ] + rebase_path(metadata.pw_python_package_wheels, root_build_dir)
+
+ public_deps = []
+ if (defined(invoker.public_deps)) {
+ public_deps += invoker.public_deps
+ }
+ public_deps += [ ":${invoker.target_name}" ]
+
+ stamp = true
+ }
+ group("$target_name.wheel") {
+ public_deps = [ ":${invoker.target_name}._build_wheel" ]
+ }
+
+ # Allow using pw_python_distribution targets as a python_dep in
+ # pw_python_group. To do this, create a pw_python_group with the relevant
+ # packages and create wrappers for each subtarget, except those that are
+ # actually implemented by this template.
+ #
+ # This is an ugly workaround that will be removed when the Python build is
+ # refactored (b/235278298).
+ pw_python_group("$target_name._pw_python_group") {
+ python_deps = invoker.packages
+ }
+
+ wrapped_subtargets = pw_python_package_subtargets - [
+ "wheel",
+ "_build_wheel",
+ ]
+
+ foreach(subtarget, wrapped_subtargets) {
+ group("$target_name.$subtarget") {
+ public_deps = [ ":${invoker.target_name}._pw_python_group.$subtarget" ]
+ }
+ }
+}
+
+# TODO(b/232800695): Remove this template when all projects no longer use it.
+template("pw_create_python_source_tree") {
+ pw_python_distribution("$target_name") {
+ forward_variables_from(invoker, "*")
+ }
+}
+
+# Runs pip install on a set of pw_python_packages. This will install
+# pw_python_packages into the user's developer environment.
+#
+# Args:
+# packages: A list of pw_python_package targets to be pip installed.
+# These will be installed one at a time.
+#
+# editable: If true, --editable is passed to the pip install command.
+#
+# force_reinstall: If true, --force-reinstall is passed to the pip install
+# command.
+template("pw_python_pip_install") {
+ if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
+ # Create a target group for the Python package metadata only.
+ group("$target_name._package_metadata") {
+ # Forward the package_metadata subtarget for all python_deps.
+ public_deps = []
+ if (defined(invoker.packages)) {
+ foreach(dep, invoker.packages) {
+ public_deps += [ get_label_info(dep, "label_no_toolchain") +
+ "._package_metadata($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+ }
+ }
+
+ pw_python_action("$target_name") {
+ script = "$dir_pw_build/py/pw_build/pip_install_python_deps.py"
+
+ assert(
+ defined(invoker.packages),
+ "packages = [ 'python_package' ] is required by pw_internal_pip_install")
+
+ public_deps = []
+ if (defined(invoker.public_deps)) {
+ public_deps += invoker.public_deps
+ }
+
+ python_deps = []
+ python_metadata_deps = []
+ if (defined(invoker.packages)) {
+ public_deps += invoker.packages
+ python_deps += invoker.packages
+ python_metadata_deps += invoker.packages
+ }
+
+ python_deps = []
+ if (defined(invoker.python_deps)) {
+ python_deps += invoker.python_deps
+ }
+
+ _pw_internal_run_in_venv = false
+ _forward_python_metadata_deps = true
+
+ _editable_install = false
+ if (defined(invoker.editable)) {
+ _editable_install = invoker.editable
+ }
+
+ _pkg_gn_labels = []
+ foreach(pkg, invoker.packages) {
+ _pkg_gn_labels += [ get_label_info(pkg, "label_no_toolchain") ]
+ }
+
+ args = [
+ "--gn-packages",
+ string_join(",", _pkg_gn_labels),
+ ]
+
+ if (_editable_install) {
+ args += [ "--editable-pip-install" ]
+ }
+
+ args += [
+ "install",
+ "--no-build-isolation",
+ ]
+
+ _force_reinstall = false
+ if (defined(invoker.force_reinstall)) {
+ _force_reinstall = true
+ }
+ if (_force_reinstall) {
+ args += [ "--force-reinstall" ]
+ }
+
+ inputs = pw_build_PIP_CONSTRAINTS
+ foreach(_constraints_file, pw_build_PIP_CONSTRAINTS) {
+ args += [
+ "--constraint",
+ rebase_path(_constraints_file, root_build_dir),
+ ]
+ }
+
+ stamp = true
+
+ # Parallel pip installations don't work, so serialize pip invocations.
+ pool = "$dir_pw_build/pool:pip($default_toolchain)"
+ }
+ } else {
+ group("$target_name") {
+ deps = [ ":$target_name($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+ not_needed("*")
+ not_needed(invoker, "*")
+ }
+
+ group("$target_name.install") {
+ public_deps = [ ":${invoker.target_name}" ]
+ }
+
+ # Allow using pw_internal_pip_install targets as a python_dep in
+ # pw_python_group. To do this, create a pw_python_group with the relevant
+ # packages and create wrappers for each subtarget, except those that are
+ # actually implemented by this template.
+ #
+ # This is an ugly workaround that will be removed when the Python build is
+ # refactored (b/235278298).
+ pw_python_group("$target_name._pw_python_group") {
+ python_deps = invoker.packages
+ }
+
+ foreach(subtarget, pw_python_package_subtargets - [ "install" ]) {
+ group("$target_name.$subtarget") {
+ public_deps = [ ":${invoker.target_name}._pw_python_group.$subtarget" ]
+ }
+ }
+}
+
+# TODO(b/232800695): Remove this template when all projects no longer use it.
+template("pw_internal_pip_install") {
+ pw_python_pip_install("$target_name") {
+ forward_variables_from(invoker, "*")
+ }
}
diff --git a/pw_build/python_dist/setup.sh b/pw_build/python_dist/setup.sh
index 7974caf42..81ddb5d1a 100755
--- a/pw_build/python_dist/setup.sh
+++ b/pw_build/python_dist/setup.sh
@@ -28,8 +28,8 @@ if [ ! -z "${1-}" ]; then
fi
CONSTRAINTS_ARG=""
-if [ -f ${CONSTRAINTS_PATH} ]; then
- CONSTRAINTS_ARG="-c ${CONSTRAINTS_PATH}"
+if [ -f "${CONSTRAINTS_PATH}" ]; then
+ CONSTRAINTS_ARG="-c""${CONSTRAINTS_PATH}"
fi
PY_MAJOR_VERSION=$(${PY_TO_TEST} -c "import sys; print(sys.version_info[0])")
@@ -43,23 +43,26 @@ fi
if [ ! -d "${VENV}" ]
then
- ${PY_TO_TEST} -m venv ${VENV}
+ ${PY_TO_TEST} -m venv "${VENV}"
fi
-${VENV}/bin/python -m pip install --upgrade pip
+"${VENV}/bin/python" -m pip install --upgrade pip
# Uninstall wheels first, in case installing over an existing venv. This is a
# faster and less destructive approach than --force-reinstall to ensure wheels
# whose version numbers haven't incremented still get reinstalled.
-for wheel in $(ls ${DIR}/python_wheels/*.whl)
+IFS_BACKUP="$IFS"
+IFS=$'\n'
+for wheel in $(ls "${DIR}/python_wheels/"*.whl)
do
- ${VENV}/bin/python -m pip uninstall --yes $wheel
+ "${VENV}/bin/python" -m pip uninstall --yes "$wheel"
done
-for wheel in $(ls ${DIR}/python_wheels/*.whl)
+for wheel in $(ls "${DIR}/python_wheels/"*.whl)
do
- ${VENV}/bin/python -m pip install \
- --upgrade --find-links=${DIR}/python_wheels ${CONSTRAINTS_ARG} $wheel
+ "${VENV}/bin/python" -m pip install \
+ --upgrade --find-links="${DIR}/python_wheels" ${CONSTRAINTS_ARG} "$wheel"
done
+IFS="$IFS_BACKUP"
exit 0
diff --git a/pw_build/python_gn_args.gni b/pw_build/python_gn_args.gni
new file mode 100644
index 000000000..11f3aadda
--- /dev/null
+++ b/pw_build/python_gn_args.gni
@@ -0,0 +1,24 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+declare_args() {
+ # Python tasks, such as running tests and Pylint, are done in a single GN
+ # toolchain to avoid unnecessary duplication in the build.
+ pw_build_PYTHON_TOOLCHAIN = "$dir_pw_build/python_toolchain:python"
+
+ # Default gn build virtualenv target.
+ pw_build_PYTHON_BUILD_VENV = "$dir_pw_env_setup:pigweed_build_venv"
+}
diff --git a/pw_build/python_venv.gni b/pw_build/python_venv.gni
new file mode 100644
index 000000000..c36b7f90f
--- /dev/null
+++ b/pw_build/python_venv.gni
@@ -0,0 +1,251 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+import("$dir_pw_build/python_action.gni")
+
+# Defines and creates a Python virtualenv. This template is used by Pigweed in
+# https://cs.pigweed.dev/pigweed/+/main:pw_env_setup/BUILD.gn to
+# create a virtualenv for use within the GN build that all Python actions will
+# run in.
+#
+# Example:
+#
+# pw_python_venv("test_venv") {
+# path = "test-venv"
+# constraints = [ "//tools/constraints.list" ]
+# requirements = [ "//tools/requirements.txt" ]
+# source_packages = [
+# "$dir_pw_cli/py",
+# "$dir_pw_console/py",
+# "//tools:another_pw_python_package",
+# ]
+# }
+#
+# Args:
+# path: The directory where the virtualenv will be created. This is relative
+# to the GN root and must begin with "$root_build_dir/" if it lives in the
+# output directory or "//" if it lives in elsewhere.
+#
+# constraints: A list of constraint files used when performing pip install
+# into this virtualenv. By default this is set to pw_build_PIP_CONSTRAINTS
+#
+# requirements: A list of requirements files to install into this virtualenv
+# on creation. By default this is set to pw_build_PIP_REQUIREMENTS
+#
+# source_packages: A list of in-tree pw_python_package targets that will be
+# checked for external third_party pip dependencies to install into this
+# virtualenv. Note this list of targets isn't actually installed into the
+# virtualenv. Only packages defined inside the [options] install_requires
+# section of each pw_python_package's setup.cfg will be pip installed. See
+# this page for a setup.cfg example:
+# https://setuptools.pypa.io/en/latest/userguide/declarative_config.html
+#
+template("pw_python_venv") {
+ assert(defined(invoker.path), "pw_python_venv requires a 'path'")
+
+ _path = invoker.path
+
+ _generated_requirements_file =
+ "$target_gen_dir/$target_name/generated_requirements.txt"
+
+ _source_packages = []
+ if (defined(invoker.source_packages)) {
+ _source_packages += invoker.source_packages
+ } else {
+ not_needed([
+ "_source_packages",
+ "_generated_requirements_file",
+ ])
+ }
+
+ _source_package_labels = []
+ foreach(pkg, _source_packages) {
+ _source_package_labels += [ get_label_info(pkg, "label_no_toolchain") ]
+ }
+
+ if (defined(invoker.requirements)) {
+ _requirements = invoker.requirements
+ } else {
+ _requirements = pw_build_PIP_REQUIREMENTS
+ }
+
+ if (defined(invoker.constraints)) {
+ _constraints = invoker.constraints
+ } else {
+ _constraints = pw_build_PIP_CONSTRAINTS
+ }
+
+ _python_interpreter = _path + "/bin/python"
+ if (host_os == "win") {
+ _python_interpreter = _path + "/Scripts/python.exe"
+ }
+
+ _venv_metadata_json_file = "$target_gen_dir/$target_name/venv_metadata.json"
+ _venv_metadata = {
+ gn_target_name =
+ get_label_info(":${invoker.target_name}", "label_no_toolchain")
+ path = rebase_path(_path, root_build_dir)
+ generated_requirements =
+ rebase_path(_generated_requirements_file, root_build_dir)
+ requirements = rebase_path(_requirements, root_build_dir)
+ constraints = rebase_path(_constraints, root_build_dir)
+ interpreter = rebase_path(_python_interpreter, root_build_dir)
+ source_packages = _source_package_labels
+ }
+ write_file(_venv_metadata_json_file, _venv_metadata, "json")
+
+ pw_python_action("${target_name}._create_virtualenv") {
+ _pw_internal_run_in_venv = false
+
+ # Note: The if the venv isn't in the out dir then we can't declare
+ # outputs and must stamp instead.
+ stamp = true
+
+ script = "$dir_pw_build/py/pw_build/create_gn_venv.py"
+ args = [
+ "--destination-dir",
+ rebase_path(_path, root_build_dir),
+ ]
+ }
+
+ if (defined(invoker.source_packages) &&
+ current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
+ pw_python_action("${target_name}._generate_3p_requirements") {
+ inputs = _requirements + _constraints
+
+ _pw_internal_run_in_venv = false
+ _forward_python_metadata_deps = true
+
+ script = "$dir_pw_build/py/pw_build/generate_python_requirements.py"
+
+ _pkg_gn_labels = []
+ foreach(pkg, _source_packages) {
+ _pkg_gn_labels += [ get_label_info(pkg, "label_no_toolchain") +
+ "($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+
+ # pw_build/py is always needed for venv creation and Python lint checks.
+ python_metadata_deps =
+ [ get_label_info("$dir_pw_build/py", "label_no_toolchain") +
+ "($pw_build_PYTHON_TOOLCHAIN)" ]
+ python_metadata_deps += _pkg_gn_labels
+
+ args = [
+ "--requirement",
+ rebase_path(_generated_requirements_file, root_build_dir),
+ ]
+ args += [
+ "--gn-packages",
+ string_join(",", _pkg_gn_labels),
+ ]
+
+ outputs = [ _generated_requirements_file ]
+ deps = [ ":${invoker.target_name}._create_virtualenv($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+ } else {
+ group("${target_name}._generate_3p_requirements") {
+ }
+ }
+
+ if (defined(invoker.source_packages) || defined(invoker.requirements)) {
+ if (current_toolchain == pw_build_PYTHON_TOOLCHAIN) {
+ # This target will run 'pip install wheel' in the venv. This is purposely
+ # run before further pip installs so packages that run bdist_wheel as part
+ # of their install process will succeed. Packages that run native compiles
+ # typically do this.
+ pw_python_action("${target_name}._install_base_3p_deps") {
+ module = "pip"
+ _pw_internal_run_in_venv = true
+ _skip_installing_external_python_deps = true
+ args = [
+ "install",
+ "wheel",
+ ]
+ inputs = _constraints
+
+ foreach(_constraints_file, _constraints) {
+ args += [
+ "--constraint",
+ rebase_path(_constraints_file, root_build_dir),
+ ]
+ }
+
+ deps = [ ":${invoker.target_name}._create_virtualenv($pw_build_PYTHON_TOOLCHAIN)" ]
+ stamp = true
+ pool = "$dir_pw_build/pool:pip($default_toolchain)"
+ }
+
+ # Install all 3rd party Python dependencies.
+ pw_python_action("${target_name}._install_3p_deps") {
+ module = "pip"
+ _pw_internal_run_in_venv = true
+ _skip_installing_external_python_deps = true
+ args = [ "install" ]
+
+ # Note: --no-build-isolation should be avoided for installing 3rd party
+ # Python packages that use C/C++ extension modules.
+ # https://setuptools.pypa.io/en/latest/userguide/ext_modules.html
+ inputs = _constraints + _requirements
+
+ # Constraints
+ foreach(_constraints_file, _constraints) {
+ args += [
+ "--constraint",
+ rebase_path(_constraints_file, root_build_dir),
+ ]
+ }
+
+ # Extra requirements files
+ foreach(_requirements_file, _requirements) {
+ args += [
+ "--requirement",
+ rebase_path(_requirements_file, root_build_dir),
+ ]
+ }
+
+ # Generated Python requirements file.
+ if (defined(invoker.source_packages)) {
+ inputs += [ _generated_requirements_file ]
+ args += [
+ "--requirement",
+ rebase_path(_generated_requirements_file, root_build_dir),
+ ]
+ }
+
+ deps = [
+ ":${invoker.target_name}._generate_3p_requirements($pw_build_PYTHON_TOOLCHAIN)",
+ ":${invoker.target_name}._install_base_3p_deps($pw_build_PYTHON_TOOLCHAIN)",
+ ]
+ stamp = true
+ pool = "$dir_pw_build/pool:pip($default_toolchain)"
+ }
+ } else {
+ group("${target_name}._install_3p_deps") {
+ public_deps = [ ":${target_name}($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+ }
+ } else {
+ group("${target_name}._install_3p_deps") {
+ }
+ }
+
+ # Have this target directly depend on _install_3p_deps
+ group("$target_name") {
+ public_deps =
+ [ ":${target_name}._install_3p_deps($pw_build_PYTHON_TOOLCHAIN)" ]
+ }
+}
diff --git a/pw_build/rust_executable.gni b/pw_build/rust_executable.gni
new file mode 100644
index 000000000..cc564b65d
--- /dev/null
+++ b/pw_build/rust_executable.gni
@@ -0,0 +1,56 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/gn_internal/build_target.gni")
+
+# Note: In general, prefer to import target_types.gni rather than this file.
+#
+# This template wraps a configurable target type specified by the current
+# toolchain to be used for all pw_rust_executable targets. This allows projects
+# to stamp out unique build logic for each pw_rust_executable target.
+# This wrapper is analogous to pw_executable with additions to support rust
+# specific parameters such as rust edition and cargo config features.
+#
+# Default configs, default visibility, and link deps are applied to the target
+# before forwarding to the underlying type as specified by
+# pw_build_EXECUTABLE_TARGET_TYPE.
+#
+# For more information on the features provided by this template, see the full
+# docs at https://pigweed.dev/pw_build/?highlight=pw_rust_executable.
+template("pw_rust_executable") {
+ pw_internal_build_target(target_name) {
+ forward_variables_from(invoker, "*")
+
+ _edition = "2021"
+ if (defined(invoker.edition)) {
+ _edition = invoker.edition
+ }
+ assert(_edition == "2015" || _edition == "2018" || _edition == "2021",
+ "edition ${_edition} is not supported")
+
+ if (defined(invoker.configs)) {
+ configs = invoker.configs
+ } else {
+ configs = []
+ }
+ configs += [ "$dir_pw_build:rust_edition_${_edition}" ]
+
+ underlying_target_type = pw_build_EXECUTABLE_TARGET_TYPE
+ target_type_file = pw_build_EXECUTABLE_TARGET_TYPE_FILE
+ output_dir = "${target_out_dir}/bin"
+ add_global_link_deps = true
+ }
+}
diff --git a/pw_build/rust_library.gni b/pw_build/rust_library.gni
new file mode 100644
index 000000000..370f47d53
--- /dev/null
+++ b/pw_build/rust_library.gni
@@ -0,0 +1,65 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/gn_internal/build_target.gni")
+
+# Note: In general, prefer to import target_types.gni rather than this file.
+#
+# This template wraps a configurable target type specified by the current
+# toolchain to be used for all pw_rust_library targets. A wrapper for GN's
+# built-in rust_library target, it is analogous to pw_static_library with
+# additions to support rust specific parameters such as rust edition and cargo
+# config features.
+#
+# For more information on the features provided by this template, see the full
+# docs at https://pigweed.dev/pw_build/?highlight=pw_rust_library
+template("pw_rust_library") {
+ pw_internal_build_target(target_name) {
+ forward_variables_from(invoker, "*")
+
+ crate_name = target_name
+ if (defined(name)) {
+ crate_name = name
+ }
+
+ _edition = "2021"
+ if (defined(edition)) {
+ _edition = edition
+ }
+ assert(_edition == "2015" || _edition == "2018" || _edition == "2021",
+ "edition ${_edition} is not supported")
+
+ if (!defined(configs)) {
+ configs = []
+ }
+ configs += [ "$dir_pw_build:rust_edition_${_edition}" ]
+
+ if (!defined(rustflags)) {
+ rustflags = []
+ }
+ if (defined(features)) {
+ foreach(i, features) {
+ rustflags += [ "--cfg=feature=\"${i}\"" ]
+ }
+ }
+
+ underlying_target_type = "rust_library"
+ crate_name = string_replace(crate_name, "-", "_")
+ output_name = crate_name
+ output_dir = "${target_out_dir}/lib"
+ add_global_link_deps = true
+ }
+}
diff --git a/pw_build/selects.bzl b/pw_build/selects.bzl
index 236b24a11..59a9d0a75 100644
--- a/pw_build/selects.bzl
+++ b/pw_build/selects.bzl
@@ -16,8 +16,8 @@
_RTOS_NONE = "//pw_build/constraints/rtos:none"
# Common select for tagging a target as only compatible with host OS's. This
-# select implements the logic '(Windows or Macos or Linux) and not RTOS'.
-# Example usage:
+# select implements the logic '(Windows, macOS, iOS, Linux, Android, or
+# Chromium OS) and not RTOS'. Example usage:
# load("//pw_build:selects.bzl","TARGET_COMPATIBLE_WITH_HOST_SELECT")
# pw_cc_library(
# name = "some_host_only_lib",
@@ -27,6 +27,9 @@ _RTOS_NONE = "//pw_build/constraints/rtos:none"
TARGET_COMPATIBLE_WITH_HOST_SELECT = {
"@platforms//os:windows": [_RTOS_NONE],
"@platforms//os:macos": [_RTOS_NONE],
+ "@platforms//os:ios": [_RTOS_NONE],
"@platforms//os:linux": [_RTOS_NONE],
+ "@platforms//os:chromiumos": [_RTOS_NONE],
+ "@platforms//os:android": [_RTOS_NONE],
"//conditions:default": ["@platforms//:incompatible"],
}
diff --git a/pw_build/target_types.gni b/pw_build/target_types.gni
index afb03809d..3803c7194 100644
--- a/pw_build/target_types.gni
+++ b/pw_build/target_types.gni
@@ -16,3 +16,5 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/cc_executable.gni")
import("$dir_pw_build/cc_library.gni")
+import("$dir_pw_build/rust_executable.gni")
+import("$dir_pw_build/rust_library.gni")
diff --git a/pw_build/test_blob_0123.bin b/pw_build/test_blob_0123.bin
new file mode 100644
index 000000000..eaf36c1da
--- /dev/null
+++ b/pw_build/test_blob_0123.bin
Binary files differ
diff --git a/pw_build/update_bundle.gni b/pw_build/update_bundle.gni
index 1b0ff7ba0..8e4b766de 100644
--- a/pw_build/update_bundle.gni
+++ b/pw_build/update_bundle.gni
@@ -56,6 +56,7 @@ template("pw_update_bundle") {
_persist_path = "${target_out_dir}/${target_name}/tuf_repo"
}
module = "pw_software_update.update_bundle"
+ python_deps = [ "$dir_pw_software_update/py" ]
args = [
"--out",
rebase_path(_out_path),
diff --git a/pw_build_info/BUILD.bazel b/pw_build_info/BUILD.bazel
index 0f242db4c..d7db7fe0b 100644
--- a/pw_build_info/BUILD.bazel
+++ b/pw_build_info/BUILD.bazel
@@ -15,6 +15,7 @@
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
+ "pw_cc_test",
)
package(default_visibility = ["//visibility:public"])
@@ -30,15 +31,25 @@ pw_cc_library(
"public/pw_build_info/build_id.h",
],
includes = ["public"],
+ linkopts = [
+ "-Lpw_build_info",
+ "-T$(location add_build_id_to_default_linker_script.ld)",
+ "-Wl,--build-id=sha1",
+ ],
deps = [
+ ":add_build_id_to_default_linker_script.ld",
+ ":build_id_linker_snippet.ld",
"//pw_preprocessor",
+ "//pw_span",
],
)
-# This is only used for the python tests.
-filegroup(
- name = "build_id_print_test",
- srcs = [
- "py/print_build_id.cc",
+pw_cc_test(
+ name = "build_id_test",
+ srcs = ["build_id_test.cc"],
+ deps = [
+ ":build_id",
+ "//pw_span",
+ "//pw_unit_test",
],
)
diff --git a/pw_build_info/BUILD.gn b/pw_build_info/BUILD.gn
index 659a01ceb..10560d84d 100644
--- a/pw_build_info/BUILD.gn
+++ b/pw_build_info/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("linker_script") {
inputs = [ "build_id_linker_snippet.ld" ]
@@ -30,11 +31,11 @@ config("linker_script") {
# default linker script instead of overriding it.
ldflags = [
"-T",
- rebase_path("add_build_id_to_default_script.ld", root_build_dir),
+ rebase_path("add_build_id_to_default_linker_script.ld", root_build_dir),
]
lib_dirs = [ "." ]
- inputs += [ "add_build_id_to_default_script.ld" ]
+ inputs += [ "add_build_id_to_default_linker_script.ld" ]
}
visibility = [ ":*" ]
}
@@ -56,17 +57,32 @@ if (current_os != "mac" && current_os != "win") {
":linker_script",
]
public_configs = [ ":public_include_path" ]
- cflags = [
- "-Wno-array-bounds",
- "-Wno-stringop-overflow",
- ]
public = [ "public/pw_build_info/build_id.h" ]
sources = [ "build_id.cc" ]
- deps = [ dir_pw_preprocessor ]
+ deps = [
+ dir_pw_preprocessor,
+ dir_pw_span,
+ ]
}
}
pw_doc_group("docs") {
sources = [ "docs.rst" ]
- inputs = [ "build_id_linker_snippet.ld" ]
+ inputs = [
+ "add_build_id_to_default_linker_script.ld",
+ "build_id_linker_snippet.ld",
+ ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":build_id_test" ]
+}
+
+pw_test("build_id_test") {
+ enable_if = current_os == "linux"
+ deps = [
+ ":build_id",
+ "$dir_pw_span",
+ ]
+ sources = [ "build_id_test.cc" ]
}
diff --git a/pw_build_info/CMakeLists.txt b/pw_build_info/CMakeLists.txt
index 1fb66991d..8ce126ad3 100644
--- a/pw_build_info/CMakeLists.txt
+++ b/pw_build_info/CMakeLists.txt
@@ -15,25 +15,27 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
# GNU build IDs aren't supported by Windows and macOS.
-if((NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "Windows") AND
- (NOT "${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin"))
- pw_add_module_library(pw_build_info.build_id
+if(("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows") OR
+ ("${CMAKE_SYSTEM_NAME}" STREQUAL "Darwin"))
+ pw_add_error_target(pw_build_info.build_id
+ MESSAGE
+ "Attempted to build the pw_build_info.build_id for an unsupported system "
+ "(${CMAKE_SYSTEM_NAME})."
+ )
+else()
+ pw_add_library(pw_build_info.build_id STATIC
HEADERS
public/pw_build_info/build_id.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
- pw_polyfill.cstddef
- pw_polyfill.span
+ pw_span
PUBLIC_LINK_OPTIONS
-Wl,--build-id=sha1
SOURCES
build_id.cc
PRIVATE_DEPS
pw_preprocessor
- PRIVATE_COMPILE_OPTIONS
- -Wno-array-bounds
- -Wno-stringop-overflow
)
endif()
@@ -43,7 +45,7 @@ endif()
if("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
target_link_options(pw_build_info.build_id
PUBLIC
- "-T${CMAKE_CURRENT_SOURCE_DIR}/add_build_id_to_default_script.ld"
+ "-T${CMAKE_CURRENT_SOURCE_DIR}/add_build_id_to_default_linker_script.ld"
"-L${CMAKE_CURRENT_SOURCE_DIR}"
)
endif()
diff --git a/pw_build_info/build_id.cc b/pw_build_info/build_id.cc
index d1e727fbf..88e1426ef 100644
--- a/pw_build_info/build_id.cc
+++ b/pw_build_info/build_id.cc
@@ -16,9 +16,9 @@
#include <cstdint>
#include <cstring>
-#include <span>
#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
extern "C" const uint8_t gnu_build_id_begin;
@@ -38,18 +38,20 @@ PW_PACKED(struct) ElfNoteInfo {
PW_MODIFY_DIAGNOSTICS_PUSH();
PW_MODIFY_DIAGNOSTIC(ignored, "-Warray-bounds");
PW_MODIFY_DIAGNOSTIC_GCC(ignored, "-Wstringop-overflow");
+#if __GNUC__ >= 11
+PW_MODIFY_DIAGNOSTIC_GCC(ignored, "-Wstringop-overread");
+#endif
-std::span<const std::byte> BuildId() {
+span<const std::byte> BuildId() {
// Read the sizes at the beginning of the note section.
ElfNoteInfo build_id_note_sizes;
memcpy(
&build_id_note_sizes, &gnu_build_id_begin, sizeof(build_id_note_sizes));
// Skip the "name" entry of the note section, and return a span to the
// descriptor.
- return std::as_bytes(std::span(&gnu_build_id_begin +
- sizeof(build_id_note_sizes) +
- build_id_note_sizes.name_size,
- build_id_note_sizes.descriptor_size));
+ return as_bytes(span(&gnu_build_id_begin + sizeof(build_id_note_sizes) +
+ build_id_note_sizes.name_size,
+ build_id_note_sizes.descriptor_size));
}
PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_build_info/build_id_test.cc b/pw_build_info/build_id_test.cc
new file mode 100644
index 000000000..a3c1c0640
--- /dev/null
+++ b/pw_build_info/build_id_test.cc
@@ -0,0 +1,30 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_build_info/build_id.h"
+
+#include "gtest/gtest.h"
+#include "pw_span/span.h"
+
+namespace pw::build_info {
+namespace {
+
+TEST(BuildId, BuildIdSize) {
+ span build_id = BuildId();
+ EXPECT_GT(build_id.size(), 0u);
+ EXPECT_LE(build_id.size(), kMaxBuildIdSizeBytes);
+}
+
+} // namespace
+} // namespace pw::build_info
diff --git a/pw_build_info/docs.rst b/pw_build_info/docs.rst
index 18ac9d7bc..376bd4c38 100644
--- a/pw_build_info/docs.rst
+++ b/pw_build_info/docs.rst
@@ -5,7 +5,7 @@ pw_build_info
=============
.. warning::
- This module is under construction and may not be ready for use.
+ This module is incomplete, but the build ID integration is ready for use.
pw_build_info provides tooling, build integration, and libraries for generating,
embedding, and parsing build-related information that is embedded into
@@ -16,7 +16,7 @@ simplifies the process of integrating rich version metadata to answer more
complex questions about compiled binaries.
-------------
-GNU Build IDs
+GNU build IDs
-------------
This module provides C++ and python libraries for reading GNU build IDs
generated by the link step of a C++ executable. These build IDs are essentially
@@ -28,20 +28,91 @@ Linux executables that depend on the ``build_id`` GN target will automatically
generate GNU build IDs. Windows and macOS binaries cannot use this target as
the implementation of GNU build IDs depends on the ELF file format.
-Embedded targets must first explicitly place the GNU build ID section into a
-non-info section of their linker script that is readable by the firmware. The
-following linker snippet may be copied into a read-only section (just like the
-.rodata or .text sections):
+Getting started
+===============
+To generate GNU build IDs as part of your firmware image, you'll need to update
+your embedded target's linker script.
+
+Updating your linker script
+---------------------------
+If your project has a custom linker scipt, you'll need to update it to include
+a section to contain the generated build ID. This section should be placed
+alongside the ``.text`` and ``.rodata`` sections, and named
+``.note.gnu.build-id``.
+
+.. code-block:: none
+
+ /* Main executable code. */
+ .code : ALIGN(4)
+ {
+ . = ALIGN(4);
+ /* Application code. */
+ *(.text)
+ *(.text*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+ ...
+ } >FLASH
+
+ /* GNU build ID section. */
+ .note.gnu.build-id :
+ {
+ . = ALIGN(4);
+ gnu_build_id_begin = .;
+ *(.note.gnu.build-id);
+ } >FLASH
+
+ /* Explicitly initialized global and static data. (.data) */
+ .static_init_ram : ALIGN(4)
+ {
+ *(.data)
+ *(.data*)
+ ...
+ } >RAM AT> FLASH
+
+
+Alternatively, you can copy the following linker snippet into a pre-existing
+section. This makes reading the build ID slower, so whenever possibe prefer
+creating a dedicated section for the build ID.
.. literalinclude:: build_id_linker_snippet.ld
-This snippet may be placed directly into an existing section, as it is not
-required to live in its own dedicated section. When opting to create a
-dedicated section for the build ID to reside in, Pigweed recommends naming the
-section ``.note.gnu.build-id`` as it makes it slightly easier for tools to
-parse the build ID out of a binary. After the linker script has been properly
-set up, the ``build_id`` GN target may be used to read the build ID at
-runtime.
+An example of directly inserting a build ID into an existing section is
+provided below:
+
+.. code-block:: none
+
+ /* Main executable code. */
+ .code : ALIGN(4)
+ {
+ . = ALIGN(4);
+ /* Application code. */
+ *(.text)
+ *(.text*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+
+ . = ALIGN(4);
+ gnu_build_id_begin = .;
+ *(.note.gnu.build-id);
+
+ ...
+ } >FLASH
+
+If your linker script is auto-generated, you may be able to use the
+``INSERT AFTER`` linker script directive to append the build ID as seen in the
+Linux host support for pw_build_info's build ID integration:
+
+.. literalinclude:: add_build_id_to_default_linker_script.ld
+
+Generating the build ID
+-----------------------
+When you depend on ``"$dir_pw_build_info:build_id``, a GNU build ID will be
+generated at the final link step of any binaries that depend on that library
+(whether directly or transitively). Those binaries will be able to read the
+build ID by calling ``pw::build_info::BuildId()``. Note that the build ID
+is not a string, but raw binary data, so to print it you'll need to convert
+it to hex or base64.
Python API reference
====================
diff --git a/pw_build_info/public/pw_build_info/build_id.h b/pw_build_info/public/pw_build_info/build_id.h
index b5535ff3d..55c2de39b 100644
--- a/pw_build_info/public/pw_build_info/build_id.h
+++ b/pw_build_info/public/pw_build_info/build_id.h
@@ -14,7 +14,8 @@
#pragma once
#include <cstddef>
-#include <span>
+
+#include "pw_span/span.h"
namespace pw::build_info {
@@ -25,6 +26,6 @@ inline constexpr size_t kMaxBuildIdSizeBytes = 20;
// Reads a GNU build ID from the address starting at the address of the
// `gnu_build_id_begin` symbol. This must be manually explicitly provided as
// part of a linker script. See build_id_linker_snippet.ld for an example.
-std::span<const std::byte> BuildId();
+span<const std::byte> BuildId();
} // namespace pw::build_info
diff --git a/pw_build_info/py/BUILD.bazel b/pw_build_info/py/BUILD.bazel
new file mode 100644
index 000000000..ddebfb238
--- /dev/null
+++ b/pw_build_info/py/BUILD.bazel
@@ -0,0 +1,41 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "pw_build_info",
+ srcs = [
+ "pw_build_info/__init__.py",
+ "pw_build_info/build_id.py",
+ ],
+)
+
+# This test attempts to run subprocesses directly in the source tree, which is
+# incompatible with sandboxing.
+# TODO(b/241307309): Update this test to work with bazel.
+filegroup(
+ name = "build_id_test",
+ # size = "small",
+ srcs = ["build_id_test.py"],
+ # deps = [":pw_build_info"],
+)
+
+# This is only used for the tests.
+filegroup(
+ name = "print_build_id_cc",
+ srcs = [
+ "print_build_id.cc",
+ ],
+)
diff --git a/pw_build_info/py/BUILD.gn b/pw_build_info/py/BUILD.gn
index 95240b9f7..c0e054f5e 100644
--- a/pw_build_info/py/BUILD.gn
+++ b/pw_build_info/py/BUILD.gn
@@ -17,16 +17,12 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
pw_python_package("py") {
- generate_setup = {
- metadata = {
- name = "pw_build_info"
- version = "0.0.1"
- }
- options = {
- install_requires = [ "pyelftools" ]
- }
- }
inputs = [ "print_build_id.cc" ]
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ "setup.py",
+ ]
sources = [
"pw_build_info/__init__.py",
"pw_build_info/build_id.py",
@@ -36,8 +32,8 @@ pw_python_package("py") {
# AND run an ELF file.
if (host_os == "linux") {
tests = [ "build_id_test.py" ]
- python_test_deps = [ "$dir_pw_cli/py" ]
}
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_build_info/py/build_id_test.py b/pw_build_info/py/build_id_test.py
index 6d420a370..d6eb8e06c 100644
--- a/pw_build_info/py/build_id_test.py
+++ b/pw_build_info/py/build_id_test.py
@@ -17,11 +17,11 @@ import subprocess
import tempfile
import unittest
from pathlib import Path
-from pw_cli import env
+
from pw_build_info import build_id
# Since build_id.cc depends on pw_preprocessor, we have to use the in-tree path.
-_MODULE_DIR = Path(env.pigweed_environment().PW_ROOT) / 'pw_build_info'
+_MODULE_DIR = Path(__file__).parent.parent.resolve()
_MODULE_PY_DIR = Path(__file__).parent.resolve()
_SHA1_BUILD_ID_LENGTH = 20
@@ -29,6 +29,7 @@ _SHA1_BUILD_ID_LENGTH = 20
class TestGnuBuildId(unittest.TestCase):
"""Unit tests for GNU build ID parsing."""
+
def test_build_id_correctness(self):
"""Tests to ensure GNU build IDs are read/written correctly."""
with tempfile.TemporaryDirectory() as exe_dir:
@@ -40,8 +41,10 @@ class TestGnuBuildId(unittest.TestCase):
'build_id.cc',
_MODULE_PY_DIR / 'print_build_id.cc',
'-Ipublic',
+ '-I../pw_polyfill/public',
'-I../pw_preprocessor/public',
- '-std=c++20',
+ '-I../pw_span/public',
+ '-std=c++17',
'-fuse-ld=lld',
'-Wl,-Tadd_build_id_to_default_linker_script.ld',
'-Wl,--build-id=sha1',
@@ -49,36 +52,45 @@ class TestGnuBuildId(unittest.TestCase):
exe_file,
]
- process = subprocess.run(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- cwd=_MODULE_DIR)
- self.assertEqual(process.returncode, 0)
+ process = subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ cwd=_MODULE_DIR,
+ )
+ self.assertEqual(
+ process.returncode, 0, process.stdout.decode(errors='replace')
+ )
# Run the compiled binary so the printed build ID can be read.
- process = subprocess.run([exe_file],
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- cwd=_MODULE_DIR)
+ process = subprocess.run(
+ [exe_file],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ cwd=_MODULE_DIR,
+ )
self.assertEqual(process.returncode, 0)
with open(exe_file, 'rb') as elf:
expected = build_id.read_build_id_from_section(elf)
self.assertEqual(len(expected), _SHA1_BUILD_ID_LENGTH)
- self.assertEqual(process.stdout.decode().rstrip(),
- expected.hex())
+ self.assertEqual(
+ process.stdout.decode().rstrip(), expected.hex()
+ )
# Test method that parses using symbol information.
expected = build_id.read_build_id_from_symbol(elf)
self.assertEqual(len(expected), _SHA1_BUILD_ID_LENGTH)
- self.assertEqual(process.stdout.decode().rstrip(),
- expected.hex())
+ self.assertEqual(
+ process.stdout.decode().rstrip(), expected.hex()
+ )
# Test the user-facing method.
expected = build_id.read_build_id(elf)
self.assertEqual(len(expected), _SHA1_BUILD_ID_LENGTH)
- self.assertEqual(process.stdout.decode().rstrip(),
- expected.hex())
+ self.assertEqual(
+ process.stdout.decode().rstrip(), expected.hex()
+ )
if __name__ == '__main__':
diff --git a/pw_build_info/py/pw_build_info/build_id.py b/pw_build_info/py/pw_build_info/build_id.py
index 17d877d21..0ea003fe8 100644
--- a/pw_build_info/py/pw_build_info/build_id.py
+++ b/pw_build_info/py/pw_build_info/build_id.py
@@ -32,15 +32,19 @@ class GnuBuildIdError(Exception):
def read_build_id_from_section(elf_file: BinaryIO) -> Optional[bytes]:
"""Reads a build ID from a .note.gnu.build-id section."""
parsed_elf_file = elffile.ELFFile(elf_file)
- build_id_section = parsed_elf_file.get_section_by_name(
- '.note.gnu.build-id')
+ build_id_section = parsed_elf_file.get_section_by_name('.note.gnu.build-id')
if build_id_section is None:
return None
- section_notes = list(n for n in notes.iter_notes(
- parsed_elf_file, build_id_section['sh_offset'],
- build_id_section['sh_size']))
+ section_notes = list(
+ n
+ for n in notes.iter_notes(
+ parsed_elf_file,
+ build_id_section['sh_offset'],
+ build_id_section['sh_size'],
+ )
+ )
if len(section_notes) != 1:
raise GnuBuildIdError('GNU build ID section contains multiple notes')
@@ -66,9 +70,9 @@ def _addr_is_in_segment(addr: int, segment) -> bool:
def _read_build_id_from_offset(elf, offset: int) -> bytes:
"""Attempts to read a GNU build ID from an offset in an elf file."""
- note = elftools.common.utils.struct_parse(elf.structs.Elf_Nhdr,
- elf.stream,
- stream_pos=offset)
+ note = elftools.common.utils.struct_parse(
+ elf.structs.Elf_Nhdr, elf.stream, stream_pos=offset
+ )
elf.stream.seek(offset + elf.structs.Elf_Nhdr.sizeof())
name = elf.stream.read(note['n_namesz'])
@@ -106,8 +110,9 @@ def read_build_id_from_symbol(elf_file: BinaryIO) -> Optional[bytes]:
build_id_start_addr = gnu_build_id_sym['st_value']
for segment in parsed_elf_file.iter_segments():
if segment.section_in_segment(matching_section):
- offset = build_id_start_addr - segment['p_vaddr'] + segment[
- 'p_offset']
+ offset = (
+ build_id_start_addr - segment['p_vaddr'] + segment['p_offset']
+ )
return _read_build_id_from_offset(parsed_elf_file, offset)
return None
@@ -156,9 +161,11 @@ def _parse_args():
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('elf_file',
- type=argparse.FileType('rb'),
- help='The .elf to parse build info from')
+ parser.add_argument(
+ 'elf_file',
+ type=argparse.FileType('rb'),
+ help='The .elf to parse build info from',
+ )
return parser.parse_args()
diff --git a/pw_build_info/py/pyproject.toml b/pw_build_info/py/pyproject.toml
new file mode 100644
index 000000000..798b747ec
--- /dev/null
+++ b/pw_build_info/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_build_info/py/setup.cfg b/pw_build_info/py/setup.cfg
new file mode 100644
index 000000000..50f16cbf7
--- /dev/null
+++ b/pw_build_info/py/setup.cfg
@@ -0,0 +1,30 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+[metadata]
+name = pw_build_info
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Python scripts that support the GN build
+
+[options]
+packages = find:
+zip_safe = False
+install_requires =
+ pyelftools
+
+[options.package_data]
+pw_build_info =
+ py.typed
+ print_build_id.cc
diff --git a/pw_build_info/py/setup.py b/pw_build_info/py/setup.py
new file mode 100644
index 000000000..c1606beaa
--- /dev/null
+++ b/pw_build_info/py/setup.py
@@ -0,0 +1,18 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_build_info"""
+
+import setuptools # type: ignore
+
+setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_build_mcuxpresso/BUILD.gn b/pw_build_mcuxpresso/BUILD.gn
index 9a6699a8e..1b11f77f5 100644
--- a/pw_build_mcuxpresso/BUILD.gn
+++ b/pw_build_mcuxpresso/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_build_mcuxpresso/py/BUILD.gn b/pw_build_mcuxpresso/py/BUILD.gn
index 8d7eb656d..e640bd372 100644
--- a/pw_build_mcuxpresso/py/BUILD.gn
+++ b/pw_build_mcuxpresso/py/BUILD.gn
@@ -29,4 +29,5 @@ pw_python_package("py") {
]
tests = [ "tests/components_test.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py
index 1a39e5025..c537cf7e4 100644
--- a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/__main__.py
@@ -18,19 +18,24 @@ import argparse
import pathlib
import sys
-from pw_build_mcuxpresso import components
+try:
+ from pw_build_mcuxpresso import components
+except ImportError:
+ # Load from this directory if pw_build_mcuxpresso is not available.
+ import components # type: ignore
def _parse_args() -> argparse.Namespace:
"""Setup argparse and parse command line args."""
parser = argparse.ArgumentParser()
- subparsers = parser.add_subparsers(dest='command',
- metavar='<command>',
- required=True)
+ subparsers = parser.add_subparsers(
+ dest='command', metavar='<command>', required=True
+ )
project_parser = subparsers.add_parser(
- 'project', help='output components of an MCUXpresso project')
+ 'project', help='output components of an MCUXpresso project'
+ )
project_parser.add_argument('manifest_filename', type=pathlib.Path)
project_parser.add_argument('--include', type=str, action='append')
project_parser.add_argument('--exclude', type=str, action='append')
@@ -44,10 +49,12 @@ def main():
args = _parse_args()
if args.command == 'project':
- components.project(args.manifest_filename,
- include=args.include,
- exclude=args.exclude,
- path_prefix=args.path_prefix)
+ components.project(
+ args.manifest_filename,
+ include=args.include,
+ exclude=args.exclude,
+ path_prefix=args.path_prefix,
+ )
sys.exit(0)
diff --git a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py
index 061f5a0ab..a7223a434 100644
--- a/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py
+++ b/pw_build_mcuxpresso/py/pw_build_mcuxpresso/components.py
@@ -27,14 +27,15 @@ def _gn_str_out(name: str, val: Any):
def _gn_list_str_out(name: str, val: List[Any]):
"""Outputs list of strings in GN format with correct escaping."""
- list_str = ','.join('"' + str(x).replace('"', r'\"').replace('$', r'\$') +
- '"' for x in val)
+ list_str = ','.join(
+ '"' + str(x).replace('"', r'\"').replace('$', r'\$') + '"' for x in val
+ )
print(f'{name} = [{list_str}]')
-def _gn_list_path_out(name: str,
- val: List[pathlib.Path],
- path_prefix: Optional[str] = None):
+def _gn_list_path_out(
+ name: str, val: List[pathlib.Path], path_prefix: Optional[str] = None
+):
"""Outputs list of paths in GN format with common prefix."""
if path_prefix is not None:
str_val = list(f'{path_prefix}/{str(d)}' for d in val)
@@ -71,8 +72,9 @@ def get_component(
return (component, None)
-def parse_defines(root: xml.etree.ElementTree.Element,
- component_id: str) -> List[str]:
+def parse_defines(
+ root: xml.etree.ElementTree.Element, component_id: str
+) -> List[str]:
"""Parse pre-processor definitions for a component.
Schema:
@@ -113,8 +115,9 @@ def _parse_define(define: xml.etree.ElementTree.Element) -> str:
return f'{name}={value}'
-def parse_include_paths(root: xml.etree.ElementTree.Element,
- component_id: str) -> List[pathlib.Path]:
+def parse_include_paths(
+ root: xml.etree.ElementTree.Element, component_id: str
+) -> List[pathlib.Path]:
"""Parse include directories for a component.
Schema:
@@ -141,12 +144,15 @@ def parse_include_paths(root: xml.etree.ElementTree.Element,
include_paths.extend(
_parse_include_path(include_path, base_path)
- for include_path in component.findall(include_xpath))
+ for include_path in component.findall(include_xpath)
+ )
return include_paths
-def _parse_include_path(include_path: xml.etree.ElementTree.Element,
- base_path: Optional[pathlib.Path]) -> pathlib.Path:
+def _parse_include_path(
+ include_path: xml.etree.ElementTree.Element,
+ base_path: Optional[pathlib.Path],
+) -> pathlib.Path:
"""Parse <include_path> manifest stanza.
Schema:
@@ -165,8 +171,9 @@ def _parse_include_path(include_path: xml.etree.ElementTree.Element,
return base_path / path
-def parse_headers(root: xml.etree.ElementTree.Element,
- component_id: str) -> List[pathlib.Path]:
+def parse_headers(
+ root: xml.etree.ElementTree.Element, component_id: str
+) -> List[pathlib.Path]:
"""Parse header files for a component.
Schema:
@@ -186,8 +193,9 @@ def parse_headers(root: xml.etree.ElementTree.Element,
return _parse_sources(root, component_id, 'c_include')
-def parse_sources(root: xml.etree.ElementTree.Element,
- component_id: str) -> List[pathlib.Path]:
+def parse_sources(
+ root: xml.etree.ElementTree.Element, component_id: str
+) -> List[pathlib.Path]:
"""Parse source files for a component.
Schema:
@@ -210,8 +218,9 @@ def parse_sources(root: xml.etree.ElementTree.Element,
return source_files
-def parse_libs(root: xml.etree.ElementTree.Element,
- component_id: str) -> List[pathlib.Path]:
+def parse_libs(
+ root: xml.etree.ElementTree.Element, component_id: str
+) -> List[pathlib.Path]:
"""Parse pre-compiled libraries for a component.
Schema:
@@ -231,8 +240,9 @@ def parse_libs(root: xml.etree.ElementTree.Element,
return _parse_sources(root, component_id, 'lib')
-def _parse_sources(root: xml.etree.ElementTree.Element, component_id: str,
- source_type: str) -> List[pathlib.Path]:
+def _parse_sources(
+ root: xml.etree.ElementTree.Element, component_id: str, source_type: str
+) -> List[pathlib.Path]:
"""Parse <source> manifest stanza.
Schema:
@@ -261,13 +271,16 @@ def _parse_sources(root: xml.etree.ElementTree.Element, component_id: str,
if base_path is not None:
relative_path = base_path / relative_path
- sources.extend(relative_path / files.attrib['mask']
- for files in source.findall('./files'))
+ sources.extend(
+ relative_path / files.attrib['mask']
+ for files in source.findall('./files')
+ )
return sources
-def parse_dependencies(root: xml.etree.ElementTree.Element,
- component_id: str) -> List[str]:
+def parse_dependencies(
+ root: xml.etree.ElementTree.Element, component_id: str
+) -> List[str]:
"""Parse the list of dependencies for a component.
Optional dependencies are ignored for parsing since they have to be
@@ -333,10 +346,12 @@ def _parse_dependency(dependency: xml.etree.ElementTree.Element) -> List[str]:
return []
-def check_dependencies(root: xml.etree.ElementTree.Element,
- component_id: str,
- include: List[str],
- exclude: Optional[List[str]] = None) -> bool:
+def check_dependencies(
+ root: xml.etree.ElementTree.Element,
+ component_id: str,
+ include: List[str],
+ exclude: Optional[List[str]] = None,
+) -> bool:
"""Check the list of optional dependencies for a component.
Verifies that the optional dependencies for a component are satisfied by
@@ -358,9 +373,11 @@ def check_dependencies(root: xml.etree.ElementTree.Element,
return True
-def _check_dependency(dependency: xml.etree.ElementTree.Element,
- include: List[str],
- exclude: Optional[List[str]] = None) -> bool:
+def _check_dependency(
+ dependency: xml.etree.ElementTree.Element,
+ include: List[str],
+ exclude: Optional[List[str]] = None,
+) -> bool:
"""Check a dependency for a component.
Verifies that the given {dependency} is satisfied by components listed in
@@ -376,8 +393,9 @@ def _check_dependency(dependency: xml.etree.ElementTree.Element,
"""
if dependency.tag == 'component_dependency':
component_id = dependency.attrib['value']
- return component_id in include or (exclude is not None
- and component_id in exclude)
+ return component_id in include or (
+ exclude is not None and component_id in exclude
+ )
if dependency.tag == 'all':
for subdependency in dependency:
if not _check_dependency(subdependency, include, exclude=exclude):
@@ -399,9 +417,15 @@ def _check_dependency(dependency: xml.etree.ElementTree.Element,
def create_project(
root: xml.etree.ElementTree.Element,
include: List[str],
- exclude: Optional[List[str]] = None
-) -> Tuple[List[str], List[str], List[pathlib.Path], List[pathlib.Path],
- List[pathlib.Path], List[pathlib.Path]]:
+ exclude: Optional[List[str]] = None,
+) -> Tuple[
+ List[str],
+ List[str],
+ List[pathlib.Path],
+ List[pathlib.Path],
+ List[pathlib.Path],
+ List[pathlib.Path],
+]:
"""Create a project from a list of specified components.
Args:
@@ -429,23 +453,47 @@ def create_project(
return (
project_list,
- sum((parse_defines(root, component_id)
- for component_id in project_list), []),
- sum((parse_include_paths(root, component_id)
- for component_id in project_list), []),
- sum((parse_headers(root, component_id)
- for component_id in project_list), []),
- sum((parse_sources(root, component_id)
- for component_id in project_list), []),
- sum((parse_libs(root, component_id) for component_id in project_list),
- []),
+ sum(
+ (
+ parse_defines(root, component_id)
+ for component_id in project_list
+ ),
+ [],
+ ),
+ sum(
+ (
+ parse_include_paths(root, component_id)
+ for component_id in project_list
+ ),
+ [],
+ ),
+ sum(
+ (
+ parse_headers(root, component_id)
+ for component_id in project_list
+ ),
+ [],
+ ),
+ sum(
+ (
+ parse_sources(root, component_id)
+ for component_id in project_list
+ ),
+ [],
+ ),
+ sum(
+ (parse_libs(root, component_id) for component_id in project_list),
+ [],
+ ),
)
-def project(manifest_path: pathlib.Path,
- include: Optional[List[str]] = None,
- exclude: Optional[List[str]] = None,
- path_prefix: Optional[str] = None):
+def project(
+ manifest_path: pathlib.Path,
+ include: Optional[List[str]] = None,
+ exclude: Optional[List[str]] = None,
+ path_prefix: Optional[str] = None,
+):
"""Output GN scope for a project with the specified components.
Args:
@@ -459,12 +507,19 @@ def project(manifest_path: pathlib.Path,
tree = xml.etree.ElementTree.parse(manifest_path)
root = tree.getroot()
- (component_ids, defines, include_dirs, headers, sources, libs) = \
- create_project(root, include, exclude=exclude)
+ (
+ component_ids,
+ defines,
+ include_dirs,
+ headers,
+ sources,
+ libs,
+ ) = create_project(root, include, exclude=exclude)
for component_id in component_ids:
if not check_dependencies(
- root, component_id, component_ids, exclude=exclude):
+ root, component_id, component_ids, exclude=exclude
+ ):
return
_gn_list_str_out('defines', defines)
diff --git a/pw_build_mcuxpresso/py/tests/components_test.py b/pw_build_mcuxpresso/py/tests/components_test.py
index e3559a81e..94ca6828b 100644
--- a/pw_build_mcuxpresso/py/tests/components_test.py
+++ b/pw_build_mcuxpresso/py/tests/components_test.py
@@ -22,6 +22,7 @@ from pw_build_mcuxpresso import components
class GetComponentTest(unittest.TestCase):
"""get_component tests."""
+
def test_without_basepath(self):
test_manifest_xml = '''
<manifest>
@@ -75,6 +76,7 @@ class GetComponentTest(unittest.TestCase):
class ParseDefinesTest(unittest.TestCase):
"""parse_defines tests."""
+
def test_parse_defines(self):
test_manifest_xml = '''
<manifest>
@@ -110,6 +112,7 @@ class ParseDefinesTest(unittest.TestCase):
class ParseIncludePathsTest(unittest.TestCase):
"""parse_include_paths tests."""
+
def test_parse_include_paths(self):
test_manifest_xml = '''
<manifest>
@@ -126,9 +129,9 @@ class ParseIncludePathsTest(unittest.TestCase):
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
include_paths = components.parse_include_paths(root, 'test')
- self.assertEqual(include_paths,
- [pathlib.Path('example'),
- pathlib.Path('asm')])
+ self.assertEqual(
+ include_paths, [pathlib.Path('example'), pathlib.Path('asm')]
+ )
def test_with_base_path(self):
test_manifest_xml = '''
@@ -148,8 +151,8 @@ class ParseIncludePathsTest(unittest.TestCase):
self.assertEqual(
include_paths,
- [pathlib.Path('src/example'),
- pathlib.Path('src/asm')])
+ [pathlib.Path('src/example'), pathlib.Path('src/asm')],
+ )
def test_unknown_type(self):
test_manifest_xml = '''
@@ -185,6 +188,7 @@ class ParseIncludePathsTest(unittest.TestCase):
class ParseHeadersTest(unittest.TestCase):
"""parse_headers tests."""
+
def test_parse_headers(self):
test_manifest_xml = '''
<manifest>
@@ -201,10 +205,13 @@ class ParseHeadersTest(unittest.TestCase):
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
headers = components.parse_headers(root, 'test')
- self.assertEqual(headers, [
- pathlib.Path('include/test.h'),
- pathlib.Path('include/test_types.h')
- ])
+ self.assertEqual(
+ headers,
+ [
+ pathlib.Path('include/test.h'),
+ pathlib.Path('include/test_types.h'),
+ ],
+ )
def test_with_base_path(self):
test_manifest_xml = '''
@@ -222,10 +229,13 @@ class ParseHeadersTest(unittest.TestCase):
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
headers = components.parse_headers(root, 'test')
- self.assertEqual(headers, [
- pathlib.Path('src/include/test.h'),
- pathlib.Path('src/include/test_types.h')
- ])
+ self.assertEqual(
+ headers,
+ [
+ pathlib.Path('src/include/test.h'),
+ pathlib.Path('src/include/test_types.h'),
+ ],
+ )
def test_multiple_sets(self):
test_manifest_xml = '''
@@ -245,10 +255,13 @@ class ParseHeadersTest(unittest.TestCase):
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
headers = components.parse_headers(root, 'test')
- self.assertEqual(headers, [
- pathlib.Path('include/test.h'),
- pathlib.Path('internal/test_types.h')
- ])
+ self.assertEqual(
+ headers,
+ [
+ pathlib.Path('include/test.h'),
+ pathlib.Path('internal/test_types.h'),
+ ],
+ )
def test_no_headers(self):
test_manifest_xml = '''
@@ -267,6 +280,7 @@ class ParseHeadersTest(unittest.TestCase):
class ParseSourcesTest(unittest.TestCase):
"""parse_sources tests."""
+
def test_parse_sources(self):
test_manifest_xml = '''
<manifest>
@@ -284,9 +298,8 @@ class ParseSourcesTest(unittest.TestCase):
sources = components.parse_sources(root, 'test')
self.assertEqual(
- sources,
- [pathlib.Path('src/main.cc'),
- pathlib.Path('src/test.cc')])
+ sources, [pathlib.Path('src/main.cc'), pathlib.Path('src/test.cc')]
+ )
def test_with_base_path(self):
test_manifest_xml = '''
@@ -306,8 +319,8 @@ class ParseSourcesTest(unittest.TestCase):
self.assertEqual(
sources,
- [pathlib.Path('src/app/main.cc'),
- pathlib.Path('src/app/test.cc')])
+ [pathlib.Path('src/app/main.cc'), pathlib.Path('src/app/test.cc')],
+ )
def test_multiple_sets(self):
test_manifest_xml = '''
@@ -333,12 +346,15 @@ class ParseSourcesTest(unittest.TestCase):
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
sources = components.parse_sources(root, 'test')
- self.assertEqual(sources, [
- pathlib.Path('shared/test.cc'),
- pathlib.Path('lib/test.c'),
- pathlib.Path('app/main.cc'),
- pathlib.Path('startup/boot.s')
- ])
+ self.assertEqual(
+ sources,
+ [
+ pathlib.Path('shared/test.cc'),
+ pathlib.Path('lib/test.c'),
+ pathlib.Path('app/main.cc'),
+ pathlib.Path('startup/boot.s'),
+ ],
+ )
def test_unknown_type(self):
test_manifest_xml = '''
@@ -374,6 +390,7 @@ class ParseSourcesTest(unittest.TestCase):
class ParseLibsTest(unittest.TestCase):
"""parse_libs tests."""
+
def test_parse_libs(self):
test_manifest_xml = '''
<manifest>
@@ -392,8 +409,8 @@ class ParseLibsTest(unittest.TestCase):
self.assertEqual(
libs,
- [pathlib.Path('gcc/libtest.a'),
- pathlib.Path('gcc/libtest_arm.a')])
+ [pathlib.Path('gcc/libtest.a'), pathlib.Path('gcc/libtest_arm.a')],
+ )
def test_with_base_path(self):
test_manifest_xml = '''
@@ -411,10 +428,13 @@ class ParseLibsTest(unittest.TestCase):
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
libs = components.parse_libs(root, 'test')
- self.assertEqual(libs, [
- pathlib.Path('src/gcc/libtest.a'),
- pathlib.Path('src/gcc/libtest_arm.a')
- ])
+ self.assertEqual(
+ libs,
+ [
+ pathlib.Path('src/gcc/libtest.a'),
+ pathlib.Path('src/gcc/libtest_arm.a'),
+ ],
+ )
def test_multiple_sets(self):
test_manifest_xml = '''
@@ -436,8 +456,8 @@ class ParseLibsTest(unittest.TestCase):
self.assertEqual(
libs,
- [pathlib.Path('gcc/libtest.a'),
- pathlib.Path('arm/libtest_arm.a')])
+ [pathlib.Path('gcc/libtest.a'), pathlib.Path('arm/libtest_arm.a')],
+ )
def test_no_libs(self):
test_manifest_xml = '''
@@ -456,6 +476,7 @@ class ParseLibsTest(unittest.TestCase):
class ParseDependenciesTest(unittest.TestCase):
"""parse_dependencies tests."""
+
def test_component_dependency(self):
test_manifest_xml = '''
<manifest>
@@ -560,6 +581,7 @@ class ParseDependenciesTest(unittest.TestCase):
class CheckDependenciesTest(unittest.TestCase):
"""check_dependencies tests."""
+
def test_any_of_satisfied(self):
test_manifest_xml = '''
<manifest>
@@ -577,9 +599,9 @@ class CheckDependenciesTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- satisfied = components.check_dependencies(root,
- 'test', ['test', 'foo'],
- exclude=None)
+ satisfied = components.check_dependencies(
+ root, 'test', ['test', 'foo'], exclude=None
+ )
self.assertEqual(satisfied, True)
@@ -600,9 +622,9 @@ class CheckDependenciesTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- satisfied = components.check_dependencies(root,
- 'test', ['test'],
- exclude=None)
+ satisfied = components.check_dependencies(
+ root, 'test', ['test'], exclude=None
+ )
self.assertEqual(satisfied, False)
@@ -623,9 +645,9 @@ class CheckDependenciesTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- satisfied = components.check_dependencies(root,
- 'test', ['test'],
- exclude=['foo'])
+ satisfied = components.check_dependencies(
+ root, 'test', ['test'], exclude=['foo']
+ )
self.assertEqual(satisfied, True)
@@ -649,7 +671,8 @@ class CheckDependenciesTest(unittest.TestCase):
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
satisfied = components.check_dependencies(
- root, 'test', ['test', 'foo', 'bar', 'baz'], exclude=None)
+ root, 'test', ['test', 'foo', 'bar', 'baz'], exclude=None
+ )
self.assertEqual(satisfied, True)
@@ -672,10 +695,9 @@ class CheckDependenciesTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- satisfied = components.check_dependencies(root,
- 'test',
- ['test', 'foo', 'bar'],
- exclude=None)
+ satisfied = components.check_dependencies(
+ root, 'test', ['test', 'foo', 'bar'], exclude=None
+ )
self.assertEqual(satisfied, False)
@@ -698,10 +720,9 @@ class CheckDependenciesTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- satisfied = components.check_dependencies(root,
- 'test',
- ['test', 'foo', 'bar'],
- exclude=['baz'])
+ satisfied = components.check_dependencies(
+ root, 'test', ['test', 'foo', 'bar'], exclude=['baz']
+ )
self.assertEqual(satisfied, True)
@@ -725,9 +746,9 @@ class CheckDependenciesTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- satisfied = components.check_dependencies(root,
- 'test', ['test', 'frodo'],
- exclude=None)
+ satisfied = components.check_dependencies(
+ root, 'test', ['test', 'frodo'], exclude=None
+ )
self.assertEqual(satisfied, True)
@@ -751,9 +772,9 @@ class CheckDependenciesTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- satisfied = components.check_dependencies(root,
- 'test', ['test'],
- exclude=None)
+ satisfied = components.check_dependencies(
+ root, 'test', ['test'], exclude=None
+ )
self.assertEqual(satisfied, False)
@@ -777,15 +798,16 @@ class CheckDependenciesTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- satisfied = components.check_dependencies(root,
- 'test', ['test'],
- exclude=['frodo'])
+ satisfied = components.check_dependencies(
+ root, 'test', ['test'], exclude=['frodo']
+ )
self.assertEqual(satisfied, True)
class CreateProjectTest(unittest.TestCase):
"""create_project tests."""
+
def test_create_project(self):
"""test creating a project."""
test_manifest_xml = '''
@@ -882,27 +904,43 @@ class CreateProjectTest(unittest.TestCase):
</manifest>
'''
root = xml.etree.ElementTree.fromstring(test_manifest_xml)
- (component_ids, defines, include_dirs, headers, sources, libs) = \
- components.create_project(root, ['test', 'frodo'],
- exclude=['bilbo'])
+ (
+ component_ids,
+ defines,
+ include_dirs,
+ headers,
+ sources,
+ libs,
+ ) = components.create_project(
+ root, ['test', 'frodo'], exclude=['bilbo']
+ )
self.assertEqual(component_ids, ['test', 'frodo', 'foo', 'bar'])
self.assertEqual(defines, ['FRODO', 'FOO', 'BAR'])
- self.assertEqual(include_dirs, [
- pathlib.Path('frodo/include'),
- pathlib.Path('foo/include'),
- pathlib.Path('bar/include')
- ])
- self.assertEqual(headers, [
- pathlib.Path('frodo/include/frodo.h'),
- pathlib.Path('foo/include/foo.h'),
- pathlib.Path('bar/include/bar.h')
- ])
- self.assertEqual(sources, [
- pathlib.Path('frodo/src/frodo.cc'),
- pathlib.Path('foo/src/foo.cc'),
- pathlib.Path('bar/src/bar.cc')
- ])
+ self.assertEqual(
+ include_dirs,
+ [
+ pathlib.Path('frodo/include'),
+ pathlib.Path('foo/include'),
+ pathlib.Path('bar/include'),
+ ],
+ )
+ self.assertEqual(
+ headers,
+ [
+ pathlib.Path('frodo/include/frodo.h'),
+ pathlib.Path('foo/include/foo.h'),
+ pathlib.Path('bar/include/bar.h'),
+ ],
+ )
+ self.assertEqual(
+ sources,
+ [
+ pathlib.Path('frodo/src/frodo.cc'),
+ pathlib.Path('foo/src/foo.cc'),
+ pathlib.Path('bar/src/bar.cc'),
+ ],
+ )
self.assertEqual(libs, [pathlib.Path('frodo/libonering.a')])
diff --git a/pw_bytes/Android.bp b/pw_bytes/Android.bp
new file mode 100644
index 000000000..6864c8654
--- /dev/null
+++ b/pw_bytes/Android.bp
@@ -0,0 +1,36 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_bytes",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_span_headers",
+ ],
+ host_supported: true,
+ srcs: [
+ "byte_builder.cc"
+ ],
+ static_libs: [
+ "pw_containers",
+ "pw_status",
+ ],
+} \ No newline at end of file
diff --git a/pw_bytes/BUILD.bazel b/pw_bytes/BUILD.bazel
index 960959335..7a6cbf6b4 100644
--- a/pw_bytes/BUILD.bazel
+++ b/pw_bytes/BUILD.bazel
@@ -36,17 +36,21 @@ pw_cc_library(
],
includes = ["public"],
deps = [
+ ":bit",
+ "//pw_containers:iterator",
"//pw_polyfill",
- "//pw_polyfill:bit",
- "//pw_polyfill:cstddef",
- "//pw_polyfill:iterator",
- "//pw_polyfill:type_traits",
"//pw_preprocessor",
"//pw_span",
"//pw_status",
],
)
+pw_cc_library(
+ name = "bit",
+ hdrs = ["public/pw_bytes/bit.h"],
+ includes = ["public"],
+)
+
pw_cc_test(
name = "array_test",
srcs = ["array_test.cc"],
@@ -57,6 +61,15 @@ pw_cc_test(
)
pw_cc_test(
+ name = "bit_test",
+ srcs = ["bit_test.cc"],
+ deps = [
+ ":bit",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "byte_builder_test",
srcs = ["byte_builder_test.cc"],
deps = [
diff --git a/pw_bytes/BUILD.gn b/pw_bytes/BUILD.gn
index 554fda1fa..a486880ae 100644
--- a/pw_bytes/BUILD.gn
+++ b/pw_bytes/BUILD.gn
@@ -19,13 +19,13 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
-config("default_config") {
+config("public_include_path") {
include_dirs = [ "public" ]
visibility = [ ":*" ]
}
pw_source_set("pw_bytes") {
- public_configs = [ ":default_config" ]
+ public_configs = [ ":public_include_path" ]
public = [
"public/pw_bytes/array.h",
"public/pw_bytes/byte_builder.h",
@@ -35,14 +35,24 @@ pw_source_set("pw_bytes") {
]
sources = [ "byte_builder.cc" ]
public_deps = [
+ "$dir_pw_bytes:bit",
+ "$dir_pw_containers:iterator",
dir_pw_preprocessor,
+ dir_pw_span,
dir_pw_status,
]
}
+pw_source_set("bit") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_bytes/bit.h" ]
+ remove_public_deps = [ "*" ]
+}
+
pw_test_group("tests") {
tests = [
":array_test",
+ ":bit_test",
":byte_builder_test",
":endian_test",
":units_test",
@@ -58,6 +68,11 @@ pw_test("array_test") {
sources = [ "array_test.cc" ]
}
+pw_test("bit_test") {
+ deps = [ ":bit" ]
+ sources = [ "bit_test.cc" ]
+}
+
pw_test("byte_builder_test") {
deps = [ ":pw_bytes" ]
sources = [ "byte_builder_test.cc" ]
@@ -78,7 +93,7 @@ pw_doc_group("docs") {
report_deps = [ ":byte_builder_size_report" ]
}
-pw_size_report("byte_builder_size_report") {
+pw_size_diff("byte_builder_size_report") {
title = "Using pw::ByteBuilder"
binaries = [
diff --git a/pw_bytes/CMakeLists.txt b/pw_bytes/CMakeLists.txt
index 17033868a..c6f2e677e 100644
--- a/pw_bytes/CMakeLists.txt
+++ b/pw_bytes/CMakeLists.txt
@@ -14,7 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_bytes
+pw_add_library(pw_bytes STATIC
HEADERS
public/pw_bytes/array.h
public/pw_bytes/byte_builder.h
@@ -24,10 +24,11 @@ pw_add_module_library(pw_bytes
PUBLIC_INCLUDES
public
PUBLIC_DEPS
+ pw_bytes.bit
+ pw_containers.iterator
pw_polyfill
- pw_polyfill.cstddef
- pw_polyfill.span
pw_preprocessor
+ pw_span
pw_status
SOURCES
byte_builder.cc
@@ -36,20 +37,37 @@ if(Zephyr_FOUND AND CONFIG_PIGWEED_BYTES)
zephyr_link_libraries(pw_bytes)
endif()
+pw_add_library(pw_bytes.bit INTERFACE
+ HEADERS
+ public/pw_bytes/bit.h
+ PUBLIC_INCLUDES
+ public
+)
+
pw_add_test(pw_bytes.array_test
SOURCES
array_test.cc
- DEPS
+ PRIVATE_DEPS
pw_bytes
GROUPS
modules
pw_bytes
)
+pw_add_test(pw_bytes.bit_test
+ SOURCES
+ bit_test.cc
+ PRIVATE_DEPS
+ pw_bytes.bit
+ GROUPS
+ modules
+ pw_bytes
+)
+
pw_add_test(pw_bytes.byte_builder_test
SOURCES
byte_builder_test.cc
- DEPS
+ PRIVATE_DEPS
pw_bytes
GROUPS
modules
@@ -59,7 +77,7 @@ pw_add_test(pw_bytes.byte_builder_test
pw_add_test(pw_bytes.endian_test
SOURCES
endian_test.cc
- DEPS
+ PRIVATE_DEPS
pw_bytes
GROUPS
modules
@@ -69,7 +87,7 @@ pw_add_test(pw_bytes.endian_test
pw_add_test(pw_bytes.units_test
SOURCES
units_test.cc
- DEPS
+ PRIVATE_DEPS
pw_bytes
GROUPS
modules
diff --git a/pw_bytes/bit_test.cc b/pw_bytes/bit_test.cc
new file mode 100644
index 000000000..372341c7d
--- /dev/null
+++ b/pw_bytes/bit_test.cc
@@ -0,0 +1,50 @@
+
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bytes/bit.h"
+
+#include <cstdint>
+#include <cstring>
+
+#include "gtest/gtest.h"
+
+namespace pw::bytes {
+namespace {
+
+TEST(Endian, NativeIsBigOrLittle) {
+ EXPECT_TRUE(endian::native == endian::little ||
+ endian::native == endian::big);
+}
+
+TEST(Endian, NativeIsCorrect) {
+ constexpr uint32_t kInteger = 0x11223344u;
+ int8_t bytes[sizeof(kInteger)] = {};
+ std::memcpy(bytes, &kInteger, sizeof(kInteger));
+
+ if (endian::native == endian::little) {
+ EXPECT_EQ(bytes[0], 0x44);
+ EXPECT_EQ(bytes[1], 0x33);
+ EXPECT_EQ(bytes[2], 0x22);
+ EXPECT_EQ(bytes[3], 0x11);
+ } else {
+ EXPECT_EQ(bytes[0], 0x11);
+ EXPECT_EQ(bytes[1], 0x22);
+ EXPECT_EQ(bytes[2], 0x33);
+ EXPECT_EQ(bytes[3], 0x44);
+ }
+}
+
+} // namespace
+} // namespace pw::bytes
diff --git a/pw_bytes/byte_builder.cc b/pw_bytes/byte_builder.cc
index 96552a692..9c082fd29 100644
--- a/pw_bytes/byte_builder.cc
+++ b/pw_bytes/byte_builder.cc
@@ -17,13 +17,13 @@
namespace pw {
ByteBuilder& ByteBuilder::append(size_t count, std::byte b) {
- std::byte* const append_destination = buffer_.begin() + size_;
+ std::byte* const append_destination = buffer_.data() + size_;
std::fill_n(append_destination, ResizeForAppend(count), b);
return *this;
}
ByteBuilder& ByteBuilder::append(const void* bytes, size_t count) {
- std::byte* const append_destination = buffer_.begin() + size_;
+ std::byte* const append_destination = buffer_.data() + size_;
std::copy_n(static_cast<const std::byte*>(bytes),
ResizeForAppend(count),
append_destination);
diff --git a/pw_bytes/byte_builder_test.cc b/pw_bytes/byte_builder_test.cc
index 97e2bb691..8c33044ee 100644
--- a/pw_bytes/byte_builder_test.cc
+++ b/pw_bytes/byte_builder_test.cc
@@ -401,15 +401,15 @@ TEST(ByteBuffer, Putting16ByteInts_Full_kLittleEndian) {
TEST(ByteBuffer, Putting16ByteInts_Exhausted_kBigEndian) {
ByteBuffer<5> bb;
- bb.PutInt16(0xFFF7, std::endian::big);
- bb.PutUint16(0x0008, std::endian::big);
+ bb.PutInt16(0xFFF7, endian::big);
+ bb.PutUint16(0x0008, endian::big);
EXPECT_EQ(byte{0xFF}, bb.data()[0]);
EXPECT_EQ(byte{0xF7}, bb.data()[1]);
EXPECT_EQ(byte{0x00}, bb.data()[2]);
EXPECT_EQ(byte{0x08}, bb.data()[3]);
- bb.PutInt16(0xFAFA, std::endian::big);
+ bb.PutInt16(0xFAFA, endian::big);
EXPECT_EQ(4u, bb.size());
EXPECT_EQ(Status::ResourceExhausted(), bb.status());
}
@@ -433,8 +433,8 @@ TEST(ByteBuffer, Putting32ByteInts_Full_kLittleEndian) {
TEST(ByteBuffer, Putting32ByteInts_Exhausted_kBigEndian) {
ByteBuffer<10> bb;
- bb.PutInt32(0xF92927B2, std::endian::big);
- bb.PutUint32(0x0C90739E, std::endian::big);
+ bb.PutInt32(0xF92927B2, endian::big);
+ bb.PutUint32(0x0C90739E, endian::big);
EXPECT_EQ(byte{0xF9}, bb.data()[0]);
EXPECT_EQ(byte{0x29}, bb.data()[1]);
@@ -445,7 +445,7 @@ TEST(ByteBuffer, Putting32ByteInts_Exhausted_kBigEndian) {
EXPECT_EQ(byte{0x73}, bb.data()[6]);
EXPECT_EQ(byte{0x9E}, bb.data()[7]);
- bb.PutInt32(-114743374, std::endian::big);
+ bb.PutInt32(-114743374, endian::big);
EXPECT_EQ(8u, bb.size());
EXPECT_EQ(Status::ResourceExhausted(), bb.status());
}
@@ -477,8 +477,8 @@ TEST(ByteBuffer, Putting64ByteInts_Full_kLittleEndian) {
TEST(ByteBuffer, Putting64ByteInts_Exhausted_kBigEndian) {
ByteBuffer<20> bb;
- bb.PutUint64(0x000001E8A7A0D569, std::endian::big);
- bb.PutInt64(0xFFFFFE17585F2A97, std::endian::big);
+ bb.PutUint64(0x000001E8A7A0D569, endian::big);
+ bb.PutInt64(0xFFFFFE17585F2A97, endian::big);
EXPECT_EQ(byte{0x00}, bb.data()[0]);
EXPECT_EQ(byte{0x00}, bb.data()[1]);
@@ -497,7 +497,7 @@ TEST(ByteBuffer, Putting64ByteInts_Exhausted_kBigEndian) {
EXPECT_EQ(byte{0x2A}, bb.data()[14]);
EXPECT_EQ(byte{0x97}, bb.data()[15]);
- bb.PutInt64(-6099875637501324530, std::endian::big);
+ bb.PutInt64(-6099875637501324530, endian::big);
EXPECT_EQ(16u, bb.size());
EXPECT_EQ(Status::ResourceExhausted(), bb.status());
}
@@ -505,9 +505,9 @@ TEST(ByteBuffer, Putting64ByteInts_Exhausted_kBigEndian) {
TEST(ByteBuffer, PuttingInts_MixedTypes_MixedEndian) {
ByteBuffer<16> bb;
bb.PutUint8(0x03);
- bb.PutInt16(0xFD6D, std::endian::big);
+ bb.PutInt16(0xFD6D, endian::big);
bb.PutUint32(0x482B3D9E);
- bb.PutInt64(0x9A1C3641843DF317, std::endian::big);
+ bb.PutInt64(0x9A1C3641843DF317, endian::big);
bb.PutInt8(0xFB);
EXPECT_EQ(byte{0x03}, bb.data()[0]);
@@ -791,34 +791,34 @@ TEST(ByteBuffer, Iterator_PeekValues_1Byte) {
TEST(ByteBuffer, Iterator_PeekValues_2Bytes) {
ByteBuffer<4> bb;
bb.PutInt16(0xA7F1);
- bb.PutUint16(0xF929, std::endian::big);
+ bb.PutUint16(0xF929, endian::big);
auto it = bb.begin();
EXPECT_EQ(it.PeekInt16(), int16_t(0xA7F1));
it = it + 2;
- EXPECT_EQ(it.PeekUint16(std::endian::big), uint16_t(0xF929));
+ EXPECT_EQ(it.PeekUint16(endian::big), uint16_t(0xF929));
}
TEST(ByteBuffer, Iterator_PeekValues_4Bytes) {
ByteBuffer<8> bb;
bb.PutInt32(0xFFFFFFF1);
- bb.PutUint32(0xF92927B2, std::endian::big);
+ bb.PutUint32(0xF92927B2, endian::big);
auto it = bb.begin();
EXPECT_EQ(it.PeekInt32(), int32_t(0xFFFFFFF1));
it = it + 4;
- EXPECT_EQ(it.PeekUint32(std::endian::big), uint32_t(0xF92927B2));
+ EXPECT_EQ(it.PeekUint32(endian::big), uint32_t(0xF92927B2));
}
TEST(ByteBuffer, Iterator_PeekValues_8Bytes) {
ByteBuffer<16> bb;
bb.PutUint64(0x000001E8A7A0D569);
- bb.PutInt64(0xFFFFFE17585F2A97, std::endian::big);
+ bb.PutInt64(0xFFFFFE17585F2A97, endian::big);
auto it = bb.begin();
EXPECT_EQ(it.PeekUint64(), uint64_t(0x000001E8A7A0D569));
it = it + 8;
- EXPECT_EQ(it.PeekInt64(std::endian::big), int64_t(0xFFFFFE17585F2A97));
+ EXPECT_EQ(it.PeekInt64(endian::big), int64_t(0xFFFFFE17585F2A97));
}
TEST(ByteBuffer, Iterator_ReadValues_1Byte) {
@@ -836,31 +836,42 @@ TEST(ByteBuffer, Iterator_ReadValues_1Byte) {
TEST(ByteBuffer, Iterator_ReadValues_2Bytes) {
ByteBuffer<4> bb;
bb.PutInt16(0xA7F1);
- bb.PutUint16(0xF929, std::endian::big);
+ bb.PutUint16(0xF929, endian::big);
auto it = bb.begin();
EXPECT_EQ(it.ReadInt16(), int16_t(0xA7F1));
- EXPECT_EQ(it.ReadUint16(std::endian::big), uint16_t(0xF929));
+ EXPECT_EQ(it.ReadUint16(endian::big), uint16_t(0xF929));
}
TEST(ByteBuffer, Iterator_ReadValues_4Bytes) {
ByteBuffer<8> bb;
bb.PutInt32(0xFFFFFFF1);
- bb.PutUint32(0xF92927B2, std::endian::big);
+ bb.PutUint32(0xF92927B2, endian::big);
auto it = bb.begin();
EXPECT_EQ(it.ReadInt32(), int32_t(0xFFFFFFF1));
- EXPECT_EQ(it.ReadUint32(std::endian::big), uint32_t(0xF92927B2));
+ EXPECT_EQ(it.ReadUint32(endian::big), uint32_t(0xF92927B2));
}
TEST(ByteBuffer, Iterator_ReadValues_8Bytes) {
ByteBuffer<16> bb;
bb.PutUint64(0x000001E8A7A0D569);
- bb.PutInt64(0xFFFFFE17585F2A97, std::endian::big);
+ bb.PutInt64(0xFFFFFE17585F2A97, endian::big);
auto it = bb.begin();
EXPECT_EQ(it.ReadUint64(), uint64_t(0x000001E8A7A0D569));
- EXPECT_EQ(it.ReadInt64(std::endian::big), int64_t(0xFFFFFE17585F2A97));
+ EXPECT_EQ(it.ReadInt64(endian::big), int64_t(0xFFFFFE17585F2A97));
}
+
+TEST(ByteBuffer, ConvertsToSpan) {
+ ByteBuffer<16> bb;
+ bb.push_back(std::byte{210});
+
+ span<const std::byte> byte_span(bb);
+ EXPECT_EQ(byte_span.data(), bb.data());
+ EXPECT_EQ(byte_span.size(), bb.size());
+ EXPECT_EQ(byte_span[0], std::byte{210});
+}
+
} // namespace
} // namespace pw
diff --git a/pw_bytes/docs.rst b/pw_bytes/docs.rst
index 6313abb89..a625fe04c 100644
--- a/pw_bytes/docs.rst
+++ b/pw_bytes/docs.rst
@@ -1,30 +1,33 @@
.. _module-pw_bytes:
----------
+=========
pw_bytes
----------
+=========
pw_bytes is a collection of utilities for manipulating binary data.
+-------------
Compatibility
-=============
+-------------
C++17
+------------
Dependencies
-============
+------------
* ``pw_preprocessor``
* ``pw_status``
* ``pw_span``
+--------
Features
-========
+--------
pw_bytes/array.h
-----------------
+================
Functions for working with byte arrays, primarily for building fixed-size byte
arrays at compile time.
pw_bytes/byte_builder.h
------------------------
+=======================
.. cpp:class:: ByteBuilder
``ByteBuilder`` is a class that facilitates building or reading arrays of
@@ -36,26 +39,35 @@ pw_bytes/byte_builder.h
``ByteBuilder`` with an internally allocated buffer.
Size report: using ByteBuffer
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+-----------------------------
.. include:: byte_builder_size_report
+pw_bytes/bit.h
+================
+Implementation of features provided by C++20's ``<bit>`` header. Supported
+features:
+
+* ``pw::endian`` -- Implementation of the ``std::endian`` enum. If
+ ``std::endian`` is available, ``pw::endian`` is an alias of it.
+
pw_bytes/endian.h
------------------
+=================
Functions for converting the endianness of integral values.
pw_bytes/units.h
-----------------
+================
Constants, functions and user-defined literals for specifying a number of bytes
in powers of two, as defined by IEC 60027-2 A.2 and ISO/IEC 80000:13-2008.
The supported suffixes include:
- * ``_B`` for bytes (1024^0)
- * ``_KiB`` for kibibytes (1024^1)
- * ``_MiB`` for mibibytes (1024^2)
- * ``_GiB`` for gibibytes (1024^3)
- * ``_TiB`` for tebibytes (1024^4)
- * ``_PiB`` for pebibytes (1024^5)
- * ``_EiB`` for exbibytes (1024^6)
+
+* ``_B`` for bytes (1024\ :sup:`0`)
+* ``_KiB`` for kibibytes (1024\ :sup:`1`)
+* ``_MiB`` for mebibytes (1024\ :sup:`2`)
+* ``_GiB`` for gibibytes (1024\ :sup:`3`)
+* ``_TiB`` for tebibytes (1024\ :sup:`4`)
+* ``_PiB`` for pebibytes (1024\ :sup:`5`)
+* ``_EiB`` for exbibytes (1024\ :sup:`6`)
In order to use these you must use a using namespace directive, for example:
@@ -78,7 +90,8 @@ also similar functions:
constexpr size_t kBufferSizeBytes = pw::bytes::MiB(1) + pw::bytes::KiB(42);
+------
Zephyr
-======
+------
To enable ``pw_bytes`` for Zephyr add ``CONFIG_PIGWEED_BYTES=y`` to the
project's configuration.
diff --git a/pw_bytes/endian_test.cc b/pw_bytes/endian_test.cc
index 7b49ee7cb..f6874b256 100644
--- a/pw_bytes/endian_test.cc
+++ b/pw_bytes/endian_test.cc
@@ -22,9 +22,8 @@
namespace pw::bytes {
namespace {
-constexpr std::endian kNonNative = (std::endian::native == std::endian::little)
- ? std::endian::big
- : std::endian::little;
+constexpr endian kNonNative =
+ (endian::native == endian::little) ? endian::big : endian::little;
// ConvertOrderTo/From
//
@@ -36,28 +35,28 @@ constexpr std::endian kNonNative = (std::endian::native == std::endian::little)
// Native endianess conversions (should do nothing)
// Convert unsigned to native endianness
-static_assert(ConvertOrderTo(std::endian::native, uint8_t{0x12}) == uint8_t{0x12});
-static_assert(ConvertOrderTo(std::endian::native, uint16_t{0x0011}) == uint16_t{0x0011});
-static_assert(ConvertOrderTo(std::endian::native, uint32_t{0x33221100}) == uint32_t{0x33221100});
-static_assert(ConvertOrderTo(std::endian::native, uint64_t{0x0011223344556677}) == uint64_t{0x0011223344556677});
+static_assert(ConvertOrderTo(endian::native, uint8_t{0x12}) == uint8_t{0x12});
+static_assert(ConvertOrderTo(endian::native, uint16_t{0x0011}) == uint16_t{0x0011});
+static_assert(ConvertOrderTo(endian::native, uint32_t{0x33221100}) == uint32_t{0x33221100});
+static_assert(ConvertOrderTo(endian::native, uint64_t{0x0011223344556677}) == uint64_t{0x0011223344556677});
// Convert signed to native endianness
-static_assert(ConvertOrderTo(std::endian::native, int8_t{0x12}) == int8_t{0x12});
-static_assert(ConvertOrderTo(std::endian::native, int16_t{0x0011}) == int16_t{0x0011});
-static_assert(ConvertOrderTo(std::endian::native, int32_t{0x33221100}) == int32_t{0x33221100});
-static_assert(ConvertOrderTo(std::endian::native, int64_t{0x0011223344556677}) == int64_t{0x0011223344556677});
+static_assert(ConvertOrderTo(endian::native, int8_t{0x12}) == int8_t{0x12});
+static_assert(ConvertOrderTo(endian::native, int16_t{0x0011}) == int16_t{0x0011});
+static_assert(ConvertOrderTo(endian::native, int32_t{0x33221100}) == int32_t{0x33221100});
+static_assert(ConvertOrderTo(endian::native, int64_t{0x0011223344556677}) == int64_t{0x0011223344556677});
// Convert unsigned from native endianness
-static_assert(ConvertOrderFrom(std::endian::native, uint8_t{0x12}) == uint8_t{0x12});
-static_assert(ConvertOrderFrom(std::endian::native, uint16_t{0x0011}) == uint16_t{0x0011});
-static_assert(ConvertOrderFrom(std::endian::native, uint32_t{0x33221100}) == uint32_t{0x33221100});
-static_assert(ConvertOrderFrom(std::endian::native, uint64_t{0x0011223344556677}) == uint64_t{0x0011223344556677});
+static_assert(ConvertOrderFrom(endian::native, uint8_t{0x12}) == uint8_t{0x12});
+static_assert(ConvertOrderFrom(endian::native, uint16_t{0x0011}) == uint16_t{0x0011});
+static_assert(ConvertOrderFrom(endian::native, uint32_t{0x33221100}) == uint32_t{0x33221100});
+static_assert(ConvertOrderFrom(endian::native, uint64_t{0x0011223344556677}) == uint64_t{0x0011223344556677});
// Convert signed from native endianness
-static_assert(ConvertOrderFrom(std::endian::native, int8_t{0x12}) == int8_t{0x12});
-static_assert(ConvertOrderFrom(std::endian::native, int16_t{0x0011}) == int16_t{0x0011});
-static_assert(ConvertOrderFrom(std::endian::native, int32_t{0x33221100}) == int32_t{0x33221100});
-static_assert(ConvertOrderFrom(std::endian::native, int64_t{0x0011223344556677}) == int64_t{0x0011223344556677});
+static_assert(ConvertOrderFrom(endian::native, int8_t{0x12}) == int8_t{0x12});
+static_assert(ConvertOrderFrom(endian::native, int16_t{0x0011}) == int16_t{0x0011});
+static_assert(ConvertOrderFrom(endian::native, int32_t{0x33221100}) == int32_t{0x33221100});
+static_assert(ConvertOrderFrom(endian::native, int64_t{0x0011223344556677}) == int64_t{0x0011223344556677});
// Non-native endianess conversions (should reverse byte order)
@@ -107,55 +106,55 @@ constexpr bool Equal(const T& lhs, const U& rhs) {
// clang-format off
// 8-bit little
-static_assert(Equal(CopyInOrder(std::endian::little, '?'),
+static_assert(Equal(CopyInOrder(endian::little, '?'),
Array<'?'>()));
-static_assert(Equal(CopyInOrder(std::endian::little, uint8_t{0x10}),
+static_assert(Equal(CopyInOrder(endian::little, uint8_t{0x10}),
Array<0x10>()));
-static_assert(Equal(CopyInOrder(std::endian::little, static_cast<int8_t>(0x10)),
+static_assert(Equal(CopyInOrder(endian::little, static_cast<int8_t>(0x10)),
Array<0x10>()));
// 8-bit big
-static_assert(Equal(CopyInOrder(std::endian::big, '?'),
+static_assert(Equal(CopyInOrder(endian::big, '?'),
Array<'?'>()));
-static_assert(Equal(CopyInOrder(std::endian::big, static_cast<uint8_t>(0x10)),
+static_assert(Equal(CopyInOrder(endian::big, static_cast<uint8_t>(0x10)),
Array<0x10>()));
-static_assert(Equal(CopyInOrder(std::endian::big, static_cast<int8_t>(0x10)),
+static_assert(Equal(CopyInOrder(endian::big, static_cast<int8_t>(0x10)),
Array<0x10>()));
// 16-bit little
-static_assert(Equal(CopyInOrder(std::endian::little, uint16_t{0xAB12}),
+static_assert(Equal(CopyInOrder(endian::little, uint16_t{0xAB12}),
Array<0x12, 0xAB>()));
-static_assert(Equal(CopyInOrder(std::endian::little, static_cast<int16_t>(0xAB12)),
+static_assert(Equal(CopyInOrder(endian::little, static_cast<int16_t>(0xAB12)),
Array<0x12, 0xAB>()));
// 16-bit big
-static_assert(Equal(CopyInOrder(std::endian::big, uint16_t{0xAB12}),
+static_assert(Equal(CopyInOrder(endian::big, uint16_t{0xAB12}),
Array<0xAB, 0x12>()));
-static_assert(Equal(CopyInOrder(std::endian::big, static_cast<int16_t>(0xAB12)),
+static_assert(Equal(CopyInOrder(endian::big, static_cast<int16_t>(0xAB12)),
Array<0xAB, 0x12>()));
// 32-bit little
-static_assert(Equal(CopyInOrder(std::endian::little, uint32_t{0xAABBCCDD}),
+static_assert(Equal(CopyInOrder(endian::little, uint32_t{0xAABBCCDD}),
Array<0xDD, 0xCC, 0xBB, 0xAA>()));
-static_assert(Equal(CopyInOrder(std::endian::little, static_cast<int32_t>(0xAABBCCDD)),
+static_assert(Equal(CopyInOrder(endian::little, static_cast<int32_t>(0xAABBCCDD)),
Array<0xDD, 0xCC, 0xBB, 0xAA>()));
// 32-bit big
-static_assert(Equal(CopyInOrder(std::endian::big, uint32_t{0xAABBCCDD}),
+static_assert(Equal(CopyInOrder(endian::big, uint32_t{0xAABBCCDD}),
Array<0xAA, 0xBB, 0xCC, 0xDD>()));
-static_assert(Equal(CopyInOrder(std::endian::big, static_cast<int32_t>(0xAABBCCDD)),
+static_assert(Equal(CopyInOrder(endian::big, static_cast<int32_t>(0xAABBCCDD)),
Array<0xAA, 0xBB, 0xCC, 0xDD>()));
// 64-bit little
-static_assert(Equal(CopyInOrder(std::endian::little, uint64_t{0xAABBCCDD11223344}),
+static_assert(Equal(CopyInOrder(endian::little, uint64_t{0xAABBCCDD11223344}),
Array<0x44, 0x33, 0x22, 0x11, 0xDD, 0xCC, 0xBB, 0xAA>()));
-static_assert(Equal(CopyInOrder(std::endian::little, static_cast<int64_t>(0xAABBCCDD11223344ull)),
+static_assert(Equal(CopyInOrder(endian::little, static_cast<int64_t>(0xAABBCCDD11223344ull)),
Array<0x44, 0x33, 0x22, 0x11, 0xDD, 0xCC, 0xBB, 0xAA>()));
// 64-bit big
-static_assert(Equal(CopyInOrder(std::endian::big, uint64_t{0xAABBCCDD11223344}),
+static_assert(Equal(CopyInOrder(endian::big, uint64_t{0xAABBCCDD11223344}),
Array<0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44>()));
-static_assert(Equal(CopyInOrder(std::endian::big, static_cast<int64_t>(0xAABBCCDD11223344ull)),
+static_assert(Equal(CopyInOrder(endian::big, static_cast<int64_t>(0xAABBCCDD11223344ull)),
Array<0xAA, 0xBB, 0xCC, 0xDD, 0x11, 0x22, 0x33, 0x44>()));
// clang-format on
@@ -163,118 +162,139 @@ static_assert(Equal(CopyInOrder(std::endian::big, static_cast<int64_t>(0xAABBCCD
constexpr const char* kNumber = "\x11\x22\x33\x44\xaa\xbb\xcc\xdd";
TEST(ReadInOrder, 8Bit_Big) {
- EXPECT_EQ(ReadInOrder<uint8_t>(std::endian::big, "\0"), 0u);
- EXPECT_EQ(ReadInOrder<uint8_t>(std::endian::big, "\x80"), 0x80u);
- EXPECT_EQ(ReadInOrder<uint8_t>(std::endian::big, kNumber), 0x11u);
+ EXPECT_EQ(ReadInOrder<uint8_t>(endian::big, "\0"), 0u);
+ EXPECT_EQ(ReadInOrder<uint8_t>(endian::big, "\x80"), 0x80u);
+ EXPECT_EQ(ReadInOrder<uint8_t>(endian::big, kNumber), 0x11u);
- EXPECT_EQ(ReadInOrder<int8_t>(std::endian::big, "\0"), 0);
- EXPECT_EQ(ReadInOrder<int8_t>(std::endian::big, "\x80"), -128);
- EXPECT_EQ(ReadInOrder<int8_t>(std::endian::big, kNumber), 0x11);
+ EXPECT_EQ(ReadInOrder<int8_t>(endian::big, "\0"), 0);
+ EXPECT_EQ(ReadInOrder<int8_t>(endian::big, "\x80"), -128);
+ EXPECT_EQ(ReadInOrder<int8_t>(endian::big, kNumber), 0x11);
}
TEST(ReadInOrder, 8Bit_Little) {
- EXPECT_EQ(ReadInOrder<uint8_t>(std::endian::little, "\0"), 0u);
- EXPECT_EQ(ReadInOrder<uint8_t>(std::endian::little, "\x80"), 0x80u);
- EXPECT_EQ(ReadInOrder<uint8_t>(std::endian::little, kNumber), 0x11u);
+ EXPECT_EQ(ReadInOrder<uint8_t>(endian::little, "\0"), 0u);
+ EXPECT_EQ(ReadInOrder<uint8_t>(endian::little, "\x80"), 0x80u);
+ EXPECT_EQ(ReadInOrder<uint8_t>(endian::little, kNumber), 0x11u);
- EXPECT_EQ(ReadInOrder<int8_t>(std::endian::little, "\0"), 0);
- EXPECT_EQ(ReadInOrder<int8_t>(std::endian::little, "\x80"), -128);
- EXPECT_EQ(ReadInOrder<int8_t>(std::endian::little, kNumber), 0x11);
+ EXPECT_EQ(ReadInOrder<int8_t>(endian::little, "\0"), 0);
+ EXPECT_EQ(ReadInOrder<int8_t>(endian::little, "\x80"), -128);
+ EXPECT_EQ(ReadInOrder<int8_t>(endian::little, kNumber), 0x11);
}
TEST(ReadInOrder, 16Bit_Big) {
- EXPECT_EQ(ReadInOrder<uint16_t>(std::endian::big, "\0\0"), 0u);
- EXPECT_EQ(ReadInOrder<uint16_t>(std::endian::big, "\x80\0"), 0x8000u);
- EXPECT_EQ(ReadInOrder<uint16_t>(std::endian::big, kNumber), 0x1122u);
+ EXPECT_EQ(ReadInOrder<uint16_t>(endian::big, "\0\0"), 0u);
+ EXPECT_EQ(ReadInOrder<uint16_t>(endian::big, "\x80\0"), 0x8000u);
+ EXPECT_EQ(ReadInOrder<uint16_t>(endian::big, kNumber), 0x1122u);
- EXPECT_EQ(ReadInOrder<int16_t>(std::endian::big, "\0\0"), 0);
- EXPECT_EQ(ReadInOrder<int16_t>(std::endian::big, "\x80\0"), -32768);
- EXPECT_EQ(ReadInOrder<int16_t>(std::endian::big, kNumber), 0x1122);
+ EXPECT_EQ(ReadInOrder<int16_t>(endian::big, "\0\0"), 0);
+ EXPECT_EQ(ReadInOrder<int16_t>(endian::big, "\x80\0"), -32768);
+ EXPECT_EQ(ReadInOrder<int16_t>(endian::big, kNumber), 0x1122);
}
TEST(ReadInOrder, 16Bit_Little) {
- EXPECT_EQ(ReadInOrder<uint16_t>(std::endian::little, "\0\0"), 0u);
- EXPECT_EQ(ReadInOrder<uint16_t>(std::endian::little, "\x80\0"), 0x80u);
- EXPECT_EQ(ReadInOrder<uint16_t>(std::endian::little, kNumber), 0x2211u);
+ EXPECT_EQ(ReadInOrder<uint16_t>(endian::little, "\0\0"), 0u);
+ EXPECT_EQ(ReadInOrder<uint16_t>(endian::little, "\x80\0"), 0x80u);
+ EXPECT_EQ(ReadInOrder<uint16_t>(endian::little, kNumber), 0x2211u);
- EXPECT_EQ(ReadInOrder<int16_t>(std::endian::little, "\0\0"), 0);
- EXPECT_EQ(ReadInOrder<int16_t>(std::endian::little, "\x80\0"), 0x80);
- EXPECT_EQ(ReadInOrder<int16_t>(std::endian::little, kNumber), 0x2211);
+ EXPECT_EQ(ReadInOrder<int16_t>(endian::little, "\0\0"), 0);
+ EXPECT_EQ(ReadInOrder<int16_t>(endian::little, "\x80\0"), 0x80);
+ EXPECT_EQ(ReadInOrder<int16_t>(endian::little, kNumber), 0x2211);
}
TEST(ReadInOrder, 32Bit_Big) {
- EXPECT_EQ(ReadInOrder<uint32_t>(std::endian::big, "\0\0\0\0"), 0u);
- EXPECT_EQ(ReadInOrder<uint32_t>(std::endian::big, "\x80\0\0\0"), 0x80000000u);
- EXPECT_EQ(ReadInOrder<uint32_t>(std::endian::big, kNumber), 0x11223344u);
+ EXPECT_EQ(ReadInOrder<uint32_t>(endian::big, "\0\0\0\0"), 0u);
+ EXPECT_EQ(ReadInOrder<uint32_t>(endian::big, "\x80\0\0\0"), 0x80000000u);
+ EXPECT_EQ(ReadInOrder<uint32_t>(endian::big, kNumber), 0x11223344u);
- EXPECT_EQ(ReadInOrder<int32_t>(std::endian::big, "\0\0\0\0"), 0);
- EXPECT_EQ(ReadInOrder<int32_t>(std::endian::big, "\x80\0\0\0"), -2147483648);
- EXPECT_EQ(ReadInOrder<int32_t>(std::endian::big, kNumber), 0x11223344);
+ EXPECT_EQ(ReadInOrder<int32_t>(endian::big, "\0\0\0\0"), 0);
+ EXPECT_EQ(ReadInOrder<int32_t>(endian::big, "\x80\0\0\0"), -2147483648);
+ EXPECT_EQ(ReadInOrder<int32_t>(endian::big, kNumber), 0x11223344);
}
TEST(ReadInOrder, 32Bit_Little) {
- EXPECT_EQ(ReadInOrder<uint32_t>(std::endian::little, "\0\0\0\0"), 0u);
- EXPECT_EQ(ReadInOrder<uint32_t>(std::endian::little, "\x80\0\0\0"), 0x80u);
- EXPECT_EQ(ReadInOrder<uint32_t>(std::endian::little, kNumber), 0x44332211u);
+ EXPECT_EQ(ReadInOrder<uint32_t>(endian::little, "\0\0\0\0"), 0u);
+ EXPECT_EQ(ReadInOrder<uint32_t>(endian::little, "\x80\0\0\0"), 0x80u);
+ EXPECT_EQ(ReadInOrder<uint32_t>(endian::little, kNumber), 0x44332211u);
- EXPECT_EQ(ReadInOrder<int32_t>(std::endian::little, "\0\0\0\0"), 0);
- EXPECT_EQ(ReadInOrder<int32_t>(std::endian::little, "\x80\0\0\0"), 0x80);
- EXPECT_EQ(ReadInOrder<int32_t>(std::endian::little, kNumber), 0x44332211);
+ EXPECT_EQ(ReadInOrder<int32_t>(endian::little, "\0\0\0\0"), 0);
+ EXPECT_EQ(ReadInOrder<int32_t>(endian::little, "\x80\0\0\0"), 0x80);
+ EXPECT_EQ(ReadInOrder<int32_t>(endian::little, kNumber), 0x44332211);
}
TEST(ReadInOrder, 64Bit_Big) {
- EXPECT_EQ(ReadInOrder<uint64_t>(std::endian::big, "\0\0\0\0\0\0\0\0"), 0u);
- EXPECT_EQ(ReadInOrder<uint64_t>(std::endian::big, "\x80\0\0\0\0\0\0\0"),
+ EXPECT_EQ(ReadInOrder<uint64_t>(endian::big, "\0\0\0\0\0\0\0\0"), 0u);
+ EXPECT_EQ(ReadInOrder<uint64_t>(endian::big, "\x80\0\0\0\0\0\0\0"),
0x80000000'00000000llu);
- EXPECT_EQ(ReadInOrder<uint64_t>(std::endian::big, kNumber),
- 0x11223344AABBCCDDu);
+ EXPECT_EQ(ReadInOrder<uint64_t>(endian::big, kNumber), 0x11223344AABBCCDDu);
- EXPECT_EQ(ReadInOrder<int64_t>(std::endian::big, "\0\0\0\0\0\0\0\0"), 0);
- EXPECT_EQ(ReadInOrder<int64_t>(std::endian::big, "\x80\0\0\0\0\0\0\0"),
+ EXPECT_EQ(ReadInOrder<int64_t>(endian::big, "\0\0\0\0\0\0\0\0"), 0);
+ EXPECT_EQ(ReadInOrder<int64_t>(endian::big, "\x80\0\0\0\0\0\0\0"),
static_cast<int64_t>(1llu << 63));
- EXPECT_EQ(ReadInOrder<int64_t>(std::endian::big, kNumber),
- 0x11223344AABBCCDD);
+ EXPECT_EQ(ReadInOrder<int64_t>(endian::big, kNumber), 0x11223344AABBCCDD);
}
TEST(ReadInOrder, 64Bit_Little) {
- EXPECT_EQ(ReadInOrder<uint64_t>(std::endian::little, "\0\0\0\0\0\0\0\0"), 0u);
- EXPECT_EQ(ReadInOrder<uint64_t>(std::endian::little, "\x80\0\0\0\0\0\0\0"),
- 0x80u);
- EXPECT_EQ(ReadInOrder<uint64_t>(std::endian::little, kNumber),
+ EXPECT_EQ(ReadInOrder<uint64_t>(endian::little, "\0\0\0\0\0\0\0\0"), 0u);
+ EXPECT_EQ(ReadInOrder<uint64_t>(endian::little, "\x80\0\0\0\0\0\0\0"), 0x80u);
+ EXPECT_EQ(ReadInOrder<uint64_t>(endian::little, kNumber),
0xDDCCBBAA44332211u);
- EXPECT_EQ(ReadInOrder<int64_t>(std::endian::little, "\0\0\0\0\0\0\0\0"), 0);
- EXPECT_EQ(ReadInOrder<int64_t>(std::endian::little, "\x80\0\0\0\0\0\0\0"),
- 0x80);
- EXPECT_EQ(ReadInOrder<int64_t>(std::endian::little, kNumber),
+ EXPECT_EQ(ReadInOrder<int64_t>(endian::little, "\0\0\0\0\0\0\0\0"), 0);
+ EXPECT_EQ(ReadInOrder<int64_t>(endian::little, "\x80\0\0\0\0\0\0\0"), 0x80);
+ EXPECT_EQ(ReadInOrder<int64_t>(endian::little, kNumber),
static_cast<int64_t>(0xDDCCBBAA44332211));
}
TEST(ReadInOrder, StdArray) {
std::array<std::byte, 4> buffer = Array<1, 2, 3, 4>();
- EXPECT_EQ(0x04030201, ReadInOrder<int32_t>(std::endian::little, buffer));
- EXPECT_EQ(0x01020304, ReadInOrder<int32_t>(std::endian::big, buffer));
+ EXPECT_EQ(0x04030201, ReadInOrder<int32_t>(endian::little, buffer));
+ EXPECT_EQ(0x01020304, ReadInOrder<int32_t>(endian::big, buffer));
}
TEST(ReadInOrder, CArray) {
char buffer[5] = {1, 2, 3, 4, 99};
- EXPECT_EQ(0x04030201, ReadInOrder<int32_t>(std::endian::little, buffer));
- EXPECT_EQ(0x01020304, ReadInOrder<int32_t>(std::endian::big, buffer));
+ EXPECT_EQ(0x04030201, ReadInOrder<int32_t>(endian::little, buffer));
+ EXPECT_EQ(0x01020304, ReadInOrder<int32_t>(endian::big, buffer));
}
TEST(ReadInOrder, BoundsChecking_Ok) {
constexpr auto buffer = Array<1, 2, 3, 4>();
uint16_t value = 0;
- EXPECT_TRUE(ReadInOrder(std::endian::little, buffer, value));
+ EXPECT_TRUE(ReadInOrder(endian::little, buffer, value));
EXPECT_EQ(0x0201, value);
}
TEST(ReadInOrder, BoundsChecking_TooSmall) {
constexpr auto buffer = Array<1, 2, 3>();
int32_t value = 0;
- EXPECT_FALSE(ReadInOrder(std::endian::little, buffer, value));
+ EXPECT_FALSE(ReadInOrder(endian::little, buffer, value));
EXPECT_EQ(0, value);
}
+TEST(ReadInOrder, PartialLittleEndian) {
+ constexpr auto buffer = Array<1, 2, 3, 4>();
+
+ EXPECT_EQ(0x00000000, ReadInOrder<int32_t>(endian::little, buffer.data(), 0));
+ EXPECT_EQ(0x00000001, ReadInOrder<int32_t>(endian::little, buffer.data(), 1));
+ EXPECT_EQ(0x00000201, ReadInOrder<int32_t>(endian::little, buffer.data(), 2));
+ EXPECT_EQ(0x00030201, ReadInOrder<int32_t>(endian::little, buffer.data(), 3));
+ EXPECT_EQ(0x04030201, ReadInOrder<int32_t>(endian::little, buffer.data(), 4));
+ EXPECT_EQ(0x04030201, ReadInOrder<int32_t>(endian::little, buffer.data(), 5));
+ EXPECT_EQ(0x04030201,
+ ReadInOrder<int32_t>(endian::little, buffer.data(), 100));
+}
+
+TEST(ReadInOrder, PartialBigEndian) {
+ constexpr auto buffer = Array<1, 2, 3, 4>();
+
+ EXPECT_EQ(0x00000000, ReadInOrder<int32_t>(endian::big, buffer.data(), 0));
+ EXPECT_EQ(0x01000000, ReadInOrder<int32_t>(endian::big, buffer.data(), 1));
+ EXPECT_EQ(0x01020000, ReadInOrder<int32_t>(endian::big, buffer.data(), 2));
+ EXPECT_EQ(0x01020300, ReadInOrder<int32_t>(endian::big, buffer.data(), 3));
+ EXPECT_EQ(0x01020304, ReadInOrder<int32_t>(endian::big, buffer.data(), 4));
+ EXPECT_EQ(0x01020304, ReadInOrder<int32_t>(endian::big, buffer.data(), 5));
+ EXPECT_EQ(0x01020304, ReadInOrder<int32_t>(endian::big, buffer.data(), 100));
+}
+
} // namespace
} // namespace pw::bytes
diff --git a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/bit.h b/pw_bytes/public/pw_bytes/bit.h
index 72cea9e49..c712719e6 100644
--- a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/bit.h
+++ b/pw_bytes/public/pw_bytes/bit.h
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -11,14 +11,21 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+
+// Features from the <bit> header introduced in C++20.
#pragma once
-#include "pw_polyfill/standard_library/namespace.h"
+#if __has_include(<bit>)
+#include <bit>
+#endif // __has_include(<bit>)
+
+namespace pw {
+
+#ifdef __cpp_lib_endian
-_PW_POLYFILL_BEGIN_NAMESPACE_STD
+using std::endian;
-#ifndef __cpp_lib_endian
-#define __cpp_lib_endian 201907L
+#elif defined(__GNUC__)
enum class endian {
little = __ORDER_LITTLE_ENDIAN__,
@@ -26,6 +33,12 @@ enum class endian {
native = __BYTE_ORDER__,
};
+#else
+
+static_assert(false,
+ "The pw::endian enum is not defined for this compiler. Add a "
+ "definition to pw_bytes/bit.h.");
+
#endif // __cpp_lib_endian
-_PW_POLYFILL_END_NAMESPACE_STD
+} // namespace pw
diff --git a/pw_bytes/public/pw_bytes/byte_builder.h b/pw_bytes/public/pw_bytes/byte_builder.h
index 978de5be3..1f8a387a4 100644
--- a/pw_bytes/public/pw_bytes/byte_builder.h
+++ b/pw_bytes/public/pw_bytes/byte_builder.h
@@ -15,12 +15,13 @@
#include <algorithm>
#include <array>
-#include <bit>
#include <cstddef>
#include <cstring>
+#include "pw_bytes/bit.h"
#include "pw_bytes/endian.h"
#include "pw_bytes/span.h"
+#include "pw_containers/iterator.h"
#include "pw_preprocessor/compiler.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -43,11 +44,13 @@ class ByteBuilder {
public:
using difference_type = ptrdiff_t;
using value_type = std::byte;
- using pointer = std::byte*;
- using reference = std::byte&;
- using iterator_category = std::random_access_iterator_tag;
+ using element_type = const std::byte;
+ using pointer = const std::byte*;
+ using reference = const std::byte&;
+ using iterator_category = containers::contiguous_iterator_tag;
- explicit constexpr iterator(const std::byte* byte_ptr) : byte_(byte_ptr) {}
+ explicit constexpr iterator(const std::byte* byte_ptr = nullptr)
+ : byte_(byte_ptr) {}
constexpr iterator& operator++() {
byte_ += 1;
@@ -71,21 +74,27 @@ class ByteBuilder {
return previous;
}
- constexpr iterator operator+=(int n) {
+ constexpr iterator& operator+=(int n) {
byte_ += n;
return *this;
}
constexpr iterator operator+(int n) const { return iterator(byte_ + n); }
- constexpr iterator operator-=(int n) { return operator+=(-n); }
+ constexpr iterator& operator-=(int n) { return operator+=(-n); }
constexpr iterator operator-(int n) const { return iterator(byte_ - n); }
- constexpr ptrdiff_t operator-(const iterator& rhs) const {
+ constexpr difference_type operator-(const iterator& rhs) const {
return byte_ - rhs.byte_;
}
+ constexpr reference operator*() const { return *byte_; }
+
+ constexpr pointer operator->() const { return byte_; }
+
+ constexpr reference operator[](int index) const { return byte_[index]; }
+
constexpr bool operator==(const iterator& rhs) const {
return byte_ == rhs.byte_;
}
@@ -110,41 +119,35 @@ class ByteBuilder {
return !operator<(rhs);
}
- constexpr const std::byte& operator*() const { return *byte_; }
-
- constexpr const std::byte& operator[](int index) const {
- return byte_[index];
- }
-
// The Peek methods will retreive ordered (Little/Big Endian) values
// located at the iterator position without moving the iterator forward.
int8_t PeekInt8() const { return static_cast<int8_t>(PeekUint8()); }
uint8_t PeekUint8() const {
- return bytes::ReadInOrder<uint8_t>(std::endian::little, byte_);
+ return bytes::ReadInOrder<uint8_t>(endian::little, byte_);
}
- int16_t PeekInt16(std::endian order = std::endian::little) const {
+ int16_t PeekInt16(endian order = endian::little) const {
return static_cast<int16_t>(PeekUint16(order));
}
- uint16_t PeekUint16(std::endian order = std::endian::little) const {
+ uint16_t PeekUint16(endian order = endian::little) const {
return bytes::ReadInOrder<uint16_t>(order, byte_);
}
- int32_t PeekInt32(std::endian order = std::endian::little) const {
+ int32_t PeekInt32(endian order = endian::little) const {
return static_cast<int32_t>(PeekUint32(order));
}
- uint32_t PeekUint32(std::endian order = std::endian::little) const {
+ uint32_t PeekUint32(endian order = endian::little) const {
return bytes::ReadInOrder<uint32_t>(order, byte_);
}
- int64_t PeekInt64(std::endian order = std::endian::little) const {
+ int64_t PeekInt64(endian order = endian::little) const {
return static_cast<int64_t>(PeekUint64(order));
}
- uint64_t PeekUint64(std::endian order = std::endian::little) const {
+ uint64_t PeekUint64(endian order = endian::little) const {
return bytes::ReadInOrder<uint64_t>(order, byte_);
}
@@ -154,36 +157,36 @@ class ByteBuilder {
int8_t ReadInt8() { return static_cast<int8_t>(ReadUint8()); }
uint8_t ReadUint8() {
- uint8_t value = bytes::ReadInOrder<uint8_t>(std::endian::little, byte_);
+ uint8_t value = bytes::ReadInOrder<uint8_t>(endian::little, byte_);
byte_ += 1;
return value;
}
- int16_t ReadInt16(std::endian order = std::endian::little) {
+ int16_t ReadInt16(endian order = endian::little) {
return static_cast<int16_t>(ReadUint16(order));
}
- uint16_t ReadUint16(std::endian order = std::endian::little) {
+ uint16_t ReadUint16(endian order = endian::little) {
uint16_t value = bytes::ReadInOrder<uint16_t>(order, byte_);
byte_ += 2;
return value;
}
- int32_t ReadInt32(std::endian order = std::endian::little) {
+ int32_t ReadInt32(endian order = endian::little) {
return static_cast<int32_t>(ReadUint32(order));
}
- uint32_t ReadUint32(std::endian order = std::endian::little) {
+ uint32_t ReadUint32(endian order = endian::little) {
uint32_t value = bytes::ReadInOrder<uint32_t>(order, byte_);
byte_ += 4;
return value;
}
- int64_t ReadInt64(std::endian order = std::endian::little) {
+ int64_t ReadInt64(endian order = endian::little) {
return static_cast<int64_t>(ReadUint64(order));
}
- uint64_t ReadUint64(std::endian order = std::endian::little) {
+ uint64_t ReadUint64(endian order = endian::little) {
int64_t value = bytes::ReadInOrder<int64_t>(order, byte_);
byte_ += 8;
return value;
@@ -244,7 +247,7 @@ class ByteBuilder {
void clear() {
size_ = 0;
status_ = OkStatus();
- };
+ }
// Sets the statuses to OkStatus();
void clear_status() { status_ = OkStatus(); }
@@ -294,35 +297,29 @@ class ByteBuilder {
ByteBuilder& PutInt8(int8_t val) { return WriteInOrder(val); }
// Put methods for inserting different 16-bit ints
- ByteBuilder& PutUint16(uint16_t value,
- std::endian order = std::endian::little) {
+ ByteBuilder& PutUint16(uint16_t value, endian order = endian::little) {
return WriteInOrder(bytes::ConvertOrderTo(order, value));
}
- ByteBuilder& PutInt16(int16_t value,
- std::endian order = std::endian::little) {
+ ByteBuilder& PutInt16(int16_t value, endian order = endian::little) {
return PutUint16(static_cast<uint16_t>(value), order);
}
// Put methods for inserting different 32-bit ints
- ByteBuilder& PutUint32(uint32_t value,
- std::endian order = std::endian::little) {
+ ByteBuilder& PutUint32(uint32_t value, endian order = endian::little) {
return WriteInOrder(bytes::ConvertOrderTo(order, value));
}
- ByteBuilder& PutInt32(int32_t value,
- std::endian order = std::endian::little) {
+ ByteBuilder& PutInt32(int32_t value, endian order = endian::little) {
return PutUint32(static_cast<uint32_t>(value), order);
}
// Put methods for inserting different 64-bit ints
- ByteBuilder& PutUint64(uint64_t value,
- std::endian order = std::endian::little) {
+ ByteBuilder& PutUint64(uint64_t value, endian order = endian::little) {
return WriteInOrder(bytes::ConvertOrderTo(order, value));
}
- ByteBuilder& PutInt64(int64_t value,
- std::endian order = std::endian::little) {
+ ByteBuilder& PutInt64(int64_t value, endian order = endian::little) {
return PutUint64(static_cast<uint64_t>(value), order);
}
@@ -334,7 +331,7 @@ class ByteBuilder {
void CopySizeAndStatus(const ByteBuilder& other) {
size_ = other.size_;
status_ = other.status_;
- };
+ }
private:
template <typename T>
@@ -410,4 +407,8 @@ class ByteBuffer : public ByteBuilder {
std::array<std::byte, kSizeBytes> buffer_;
};
+constexpr ByteBuilder::iterator operator+(int n, ByteBuilder::iterator it) {
+ return it + n;
+}
+
} // namespace pw
diff --git a/pw_bytes/public/pw_bytes/endian.h b/pw_bytes/public/pw_bytes/endian.h
index 08b650393..215af47c6 100644
--- a/pw_bytes/public/pw_bytes/endian.h
+++ b/pw_bytes/public/pw_bytes/endian.h
@@ -13,14 +13,15 @@
// the License.
#pragma once
+#include <algorithm>
#include <array>
-#include <bit>
#include <cstring>
-#include <span>
#include <type_traits>
#include "pw_bytes/array.h"
+#include "pw_bytes/bit.h"
#include "pw_bytes/span.h"
+#include "pw_span/span.h"
namespace pw::bytes {
namespace internal {
@@ -114,7 +115,7 @@ constexpr T ReverseBytes(T value) {
// directly, since the value will be meaningless. Such values are only suitable
// to memcpy'd or sent to a different device.
template <typename T>
-constexpr T ConvertOrder(std::endian from, std::endian to, T value) {
+constexpr T ConvertOrder(endian from, endian to, T value) {
return from == to ? value : internal::ReverseBytes(value);
}
@@ -122,19 +123,19 @@ constexpr T ConvertOrder(std::endian from, std::endian to, T value) {
// this function changes the value's endianness, the result should only be used
// to memcpy the bytes to a buffer or send to a different device.
template <typename T>
-constexpr T ConvertOrderTo(std::endian to_endianness, T value) {
- return ConvertOrder(std::endian::native, to_endianness, value);
+constexpr T ConvertOrderTo(endian to_endianness, T value) {
+ return ConvertOrder(endian::native, to_endianness, value);
}
// Converts a value from the specified byte order to the native byte order.
template <typename T>
-constexpr T ConvertOrderFrom(std::endian from_endianness, T value) {
- return ConvertOrder(from_endianness, std::endian::native, value);
+constexpr T ConvertOrderFrom(endian from_endianness, T value) {
+ return ConvertOrder(from_endianness, endian::native, value);
}
// Copies the value to a std::array with the specified endianness.
template <typename T>
-constexpr auto CopyInOrder(std::endian order, T value) {
+constexpr auto CopyInOrder(endian order, T value) {
return internal::CopyLittleEndian(ConvertOrderTo(order, value));
}
@@ -144,41 +145,54 @@ constexpr auto CopyInOrder(std::endian order, T value) {
// absolutely certain the input buffer is large enough, use the ReadInOrder
// overload that returns bool, which checks the buffer size at runtime.
template <typename T>
-T ReadInOrder(std::endian order, const void* buffer) {
+T ReadInOrder(endian order, const void* buffer) {
T value;
std::memcpy(&value, buffer, sizeof(value));
return ConvertOrderFrom(order, value);
}
+// Reads up to the smaller of max_bytes_to_read and sizeof(T) bytes from a
+// buffer with the specified endianness.
+//
+// The value is zero-initialized. If max_bytes_to_read is smaller than
+// sizeof(T), the upper bytes of the value are 0.
+//
+// The buffer **MUST** be at least as large as the smaller of max_bytes_to_read
+// and sizeof(T)!
+template <typename T>
+T ReadInOrder(endian order, const void* buffer, size_t max_bytes_to_read) {
+ T value = {};
+ std::memcpy(&value, buffer, std::min(sizeof(value), max_bytes_to_read));
+ return ConvertOrderFrom(order, value);
+}
+
// ReadInOrder from a static-extent span, with compile-time bounds checking.
template <typename T,
typename B,
size_t kBufferSize,
- typename = std::enable_if_t<kBufferSize != std::dynamic_extent &&
+ typename = std::enable_if_t<kBufferSize != dynamic_extent &&
sizeof(B) == sizeof(std::byte)>>
-T ReadInOrder(std::endian order, std::span<B, kBufferSize> buffer) {
+T ReadInOrder(endian order, span<B, kBufferSize> buffer) {
static_assert(kBufferSize >= sizeof(T));
return ReadInOrder<T>(order, buffer.data());
}
// ReadInOrder from a std::array, with compile-time bounds checking.
template <typename T, typename B, size_t kBufferSize>
-T ReadInOrder(std::endian order, const std::array<B, kBufferSize>& buffer) {
- return ReadInOrder<T>(order, std::span(buffer));
+T ReadInOrder(endian order, const std::array<B, kBufferSize>& buffer) {
+ return ReadInOrder<T>(order, span(buffer));
}
// ReadInOrder from a C array, with compile-time bounds checking.
template <typename T, typename B, size_t kBufferSize>
-T ReadInOrder(std::endian order, const B (&buffer)[kBufferSize]) {
- return ReadInOrder<T>(order, std::span(buffer));
+T ReadInOrder(endian order, const B (&buffer)[kBufferSize]) {
+ return ReadInOrder<T>(order, span(buffer));
}
// Reads a value with the specified endianness from the buffer, with bounds
// checking. Returns true if successful, false if buffer is too small for a T.
template <typename T>
-[[nodiscard]] bool ReadInOrder(std::endian order,
- ConstByteSpan buffer,
- T& value) {
+[[nodiscard]] bool ReadInOrder(endian order, ConstByteSpan buffer, T& value) {
if (buffer.size() < sizeof(T)) {
return false;
}
diff --git a/pw_bytes/public/pw_bytes/span.h b/pw_bytes/public/pw_bytes/span.h
index b3ebf5602..87d7a5e9a 100644
--- a/pw_bytes/public/pw_bytes/span.h
+++ b/pw_bytes/public/pw_bytes/span.h
@@ -14,13 +14,14 @@
#pragma once
#include <cstddef>
-#include <span>
+
+#include "pw_span/span.h"
namespace pw {
// Aliases for spans of bytes.
-using ByteSpan = std::span<std::byte>;
+using ByteSpan = span<std::byte>;
-using ConstByteSpan = std::span<const std::byte>;
+using ConstByteSpan = span<const std::byte>;
} // namespace pw
diff --git a/pw_bytes/public/pw_bytes/units.h b/pw_bytes/public/pw_bytes/units.h
index 97bb8d635..43dd8a59a 100644
--- a/pw_bytes/public/pw_bytes/units.h
+++ b/pw_bytes/public/pw_bytes/units.h
@@ -22,7 +22,7 @@ namespace pw::bytes {
inline constexpr unsigned long long int kBytesInKibibyte = 1ull << 10;
// Mebibytes (MiB): 1024^2 or 2^20
-inline constexpr unsigned long long int kBytesInMibibyte = 1ull << 20;
+inline constexpr unsigned long long int kBytesInMebibyte = 1ull << 20;
// Gibibytes (GiB): 1024^3 or 2^30
inline constexpr unsigned long long int kBytesInGibibyte = 1ull << 30;
@@ -44,31 +44,31 @@ inline constexpr unsigned long long int kBytesInExbibyte = 1ull << 60;
// #include "pw_bytes/units.h"
//
// constexpr size_t kBufferSizeBytes = pw::bytes::MiB(1) + pw::bytes::KiB(42);
-inline constexpr unsigned long long int B(unsigned long long int bytes) {
+constexpr unsigned long long int B(unsigned long long int bytes) {
return bytes;
}
-inline constexpr unsigned long long int KiB(unsigned long long int kibibytes) {
+constexpr unsigned long long int KiB(unsigned long long int kibibytes) {
return kibibytes * kBytesInKibibyte;
}
-inline constexpr unsigned long long int MiB(unsigned long long int mibibytes) {
- return mibibytes * kBytesInMibibyte;
+constexpr unsigned long long int MiB(unsigned long long int mebibytes) {
+ return mebibytes * kBytesInMebibyte;
}
-inline constexpr unsigned long long int GiB(unsigned long long int gibibytes) {
+constexpr unsigned long long int GiB(unsigned long long int gibibytes) {
return gibibytes * kBytesInGibibyte;
}
-inline constexpr unsigned long long int TiB(unsigned long long int tebibytes) {
+constexpr unsigned long long int TiB(unsigned long long int tebibytes) {
return tebibytes * kBytesInTebibyte;
}
-inline constexpr unsigned long long int PiB(unsigned long long int pebibytes) {
+constexpr unsigned long long int PiB(unsigned long long int pebibytes) {
return pebibytes * kBytesInPebibyte;
}
-inline constexpr unsigned long long int EiB(unsigned long long int exbibytes) {
+constexpr unsigned long long int EiB(unsigned long long int exbibytes) {
return exbibytes * kBytesInExbibyte;
}
@@ -80,7 +80,7 @@ namespace unit_literals {
// The supported prefixes include:
// _B for bytes (1024^0)
// _KiB for kibibytes (1024^1)
-// _MiB for mibibytes (1024^2)
+// _MiB for mebibytes (1024^2)
// _GiB for gibibytes (1024^3)
// _TiB for tebibytes (1024^4)
// _PiB for pebibytes (1024^5)
@@ -103,8 +103,8 @@ constexpr unsigned long long int operator""_KiB(
}
constexpr unsigned long long int operator""_MiB(
- unsigned long long int mibibytes) {
- return mibibytes * kBytesInMibibyte;
+ unsigned long long int mebibytes) {
+ return mebibytes * kBytesInMebibyte;
}
constexpr unsigned long long int operator""_GiB(
diff --git a/pw_bytes/size_report/BUILD.bazel b/pw_bytes/size_report/BUILD.bazel
index 201b0a6b0..e628d95ca 100644
--- a/pw_bytes/size_report/BUILD.bazel
+++ b/pw_bytes/size_report/BUILD.bazel
@@ -22,8 +22,19 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_binary(
- name = "build_byte_buffer",
+ name = "with_byte_builder",
srcs = ["byte_builder_size_report.cc"],
+ local_defines = ["USE_BYTE_BUILDER=1"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_bytes",
+ ],
+)
+
+pw_cc_binary(
+ name = "without_byte_builder",
+ srcs = ["byte_builder_size_report.cc"],
+ local_defines = ["USE_BYTE_BUILDER=0"],
deps = [
"//pw_bloat:bloat_this_binary",
"//pw_bytes",
diff --git a/pw_bytes/size_report/byte_builder_size_report.cc b/pw_bytes/size_report/byte_builder_size_report.cc
index 4cde2e6f1..050f6579b 100644
--- a/pw_bytes/size_report/byte_builder_size_report.cc
+++ b/pw_bytes/size_report/byte_builder_size_report.cc
@@ -17,10 +17,10 @@
// Building the file: ninja -C out build_me
#include <array>
-#include <bit>
#include <cstdint>
#include <cstdio>
+#include "pw_bytes/bit.h"
#include "pw_bytes/byte_builder.h"
#if !defined(USE_BYTE_BUILDER)
@@ -35,14 +35,14 @@ ByteBuffer<8> bb;
void PutBytes() {
bb.PutUint32(0x482B3D9E);
- bb.PutInt32(0x482B3D9E, std::endian::big);
+ bb.PutInt32(0x482B3D9E, endian::big);
}
void ReadBytes() {
auto it = bb.begin();
std::printf("%u\n", static_cast<unsigned>(it.ReadUint32()));
- std::printf("%d\n", static_cast<int>(it.ReadInt32(std::endian::big)));
+ std::printf("%d\n", static_cast<int>(it.ReadInt32(endian::big)));
}
#else // !USE_BYTE_BUILDER
@@ -53,7 +53,7 @@ void PutBytes() {
uint32_t kVal1 = 0x482B3D9E;
int32_t kVal2 = 0x482B3D9E;
- if (std::endian::native == std::endian::little) {
+ if (endian::native == endian::little) {
std::memcpy(b_array, &kVal1, sizeof(kVal1));
kVal2 = int32_t(((kVal2 & 0x000000FF) << 3 * 8) | //
@@ -76,7 +76,7 @@ void ReadBytes() {
uint32_t kVal1;
int32_t kVal2;
- if (std::endian::native == std::endian::little) {
+ if (endian::native == endian::little) {
std::memcpy(&kVal1, b_array, sizeof(kVal1));
std::memcpy(&kVal2, b_array + 4, sizeof(kVal2));
kVal2 = int32_t(((kVal2 & 0x000000FF) << 3 * 8) | //
diff --git a/pw_checksum/BUILD.bazel b/pw_checksum/BUILD.bazel
index 6cb4fe546..adf33dd53 100644
--- a/pw_checksum/BUILD.bazel
+++ b/pw_checksum/BUILD.bazel
@@ -15,6 +15,7 @@
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
+ "pw_cc_perf_test",
"pw_cc_test",
)
@@ -31,6 +32,7 @@ pw_cc_library(
hdrs = [
"public/pw_checksum/crc16_ccitt.h",
"public/pw_checksum/crc32.h",
+ "public/pw_checksum/internal/config.h",
],
includes = ["public"],
deps = [
@@ -64,3 +66,21 @@ pw_cc_test(
"//pw_unit_test",
],
)
+
+pw_cc_perf_test(
+ name = "crc32_perf_test",
+ srcs = ["crc32_perf_test.cc"],
+ deps = [
+ ":pw_checksum",
+ "//pw_bytes",
+ ],
+)
+
+pw_cc_perf_test(
+ name = "crc16_perf_test",
+ srcs = ["crc16_ccitt_perf_test.cc"],
+ deps = [
+ ":pw_checksum",
+ "//pw_bytes",
+ ],
+)
diff --git a/pw_checksum/BUILD.gn b/pw_checksum/BUILD.gn
index d1209afef..450d70154 100644
--- a/pw_checksum/BUILD.gn
+++ b/pw_checksum/BUILD.gn
@@ -14,16 +14,34 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_bloat/bloat.gni")
+import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_perf_test/perf_test.gni")
import("$dir_pw_unit_test/test.gni")
-config("default_config") {
+declare_args() {
+ # The build target that overrides the default configuration options for this
+ # module. This should point to a source set that provides defines through a
+ # public config (which may -include a file or add defines directly).
+ pw_checksum_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
+}
+
+config("public_include_path") {
include_dirs = [ "public" ]
}
+pw_source_set("config") {
+ sources = [ "public/pw_checksum/internal/config.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ pw_checksum_CONFIG ]
+ visibility = [ ":*" ] # Only allow this module to depend on ":config"
+ friend = [ ":*" ] # Allow this module to access the config.h header.
+}
+
pw_source_set("pw_checksum") {
- public_configs = [ ":default_config" ]
+ public_configs = [ ":public_include_path" ]
public = [
"public/pw_checksum/crc16_ccitt.h",
"public/pw_checksum/crc32.h",
@@ -32,7 +50,14 @@ pw_source_set("pw_checksum") {
"crc16_ccitt.cc",
"crc32.cc",
]
- public_deps = [ dir_pw_bytes ]
+ public_deps = [
+ ":config",
+ dir_pw_bytes,
+ dir_pw_span,
+ ]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test_group("tests") {
@@ -51,6 +76,9 @@ pw_test("crc16_ccitt_test") {
"crc16_ccitt_test.cc",
"crc16_ccitt_test_c.c",
]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
}
pw_test("crc32_test") {
@@ -62,8 +90,69 @@ pw_test("crc32_test") {
"crc32_test.cc",
"crc32_test_c.c",
]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_perf_test("crc32_perf_tests") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ deps = [
+ ":pw_checksum",
+ dir_pw_bytes,
+ ]
+ sources = [ "crc32_perf_test.cc" ]
+}
+
+pw_perf_test("crc16_perf_tests") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ deps = [
+ ":pw_checksum",
+ dir_pw_bytes,
+ ]
+ sources = [ "crc16_ccitt_perf_test.cc" ]
+}
+
+group("perf_tests") {
+ deps = [
+ ":crc16_perf_tests",
+ ":crc32_perf_tests",
+ ]
+}
+
+pw_size_diff("size_report") {
+ title = "Checksum sizes"
+
+ binaries = [
+ {
+ target = "size_report:crc16_checksum"
+ base = "size_report:noop_checksum"
+ label = "CRC16 with 256-entry table"
+ },
+ {
+ target = "size_report:crc32_8bit_checksum"
+ base = "size_report:noop_checksum"
+ label = "CRC32: 8 bits per iteration, 256-entry table"
+ },
+ {
+ target = "size_report:crc32_4bit_checksum"
+ base = "size_report:noop_checksum"
+ label = "CRC32: 4 bits per iteration, 16-entry table"
+ },
+ {
+ target = "size_report:crc32_1bit_checksum"
+ base = "size_report:noop_checksum"
+ label = "CRC32: 1 bit per iteration, no table"
+ },
+ {
+ target = "size_report:fletcher16_checksum"
+ base = "size_report:noop_checksum"
+ label = "Fletcher16 (illustrative only)"
+ },
+ ]
}
pw_doc_group("docs") {
sources = [ "docs.rst" ]
+ report_deps = [ ":size_report" ]
}
diff --git a/pw_checksum/CMakeLists.txt b/pw_checksum/CMakeLists.txt
index 6a79e47ab..c3fcb5582 100644
--- a/pw_checksum/CMakeLists.txt
+++ b/pw_checksum/CMakeLists.txt
@@ -14,16 +14,15 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_checksum
+pw_add_library(pw_checksum STATIC
HEADERS
public/pw_checksum/crc16_ccitt.h
public/pw_checksum/crc32.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
- pw_polyfill.cstddef
- pw_polyfill.span
pw_bytes
+ pw_span
SOURCES
crc16_ccitt.cc
crc32.cc
@@ -36,7 +35,7 @@ pw_add_test(pw_checksum.crc16_ccitt_test
SOURCES
crc16_ccitt_test.cc
crc16_ccitt_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_checksum
pw_random
GROUPS
@@ -48,7 +47,7 @@ pw_add_test(pw_checksum.crc32_test
SOURCES
crc32_test.cc
crc32_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_checksum
pw_random
GROUPS
diff --git a/pw_checksum/config.gni b/pw_checksum/config.gni
new file mode 100644
index 000000000..3d41a4d19
--- /dev/null
+++ b/pw_checksum/config.gni
@@ -0,0 +1,17 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/module_config.gni")
diff --git a/pw_checksum/crc16_ccitt.cc b/pw_checksum/crc16_ccitt.cc
index 05ce8e9cf..413c85e62 100644
--- a/pw_checksum/crc16_ccitt.cc
+++ b/pw_checksum/crc16_ccitt.cc
@@ -60,7 +60,8 @@ extern "C" uint16_t pw_checksum_Crc16Ccitt(const void* data,
const uint8_t* const array = static_cast<const uint8_t*>(data);
for (size_t i = 0; i < size_bytes; ++i) {
- value = kCrc16CcittTable[((value >> 8) ^ array[i]) & 0xffu] ^ (value << 8);
+ value = kCrc16CcittTable[((value >> 8u) ^ array[i]) & 0xffu] ^
+ static_cast<uint16_t>(value << 8u);
}
return value;
diff --git a/pw_checksum/crc16_ccitt_perf_test.cc b/pw_checksum/crc16_ccitt_perf_test.cc
new file mode 100644
index 000000000..0c6a01cc8
--- /dev/null
+++ b/pw_checksum/crc16_ccitt_perf_test.cc
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <string_view>
+
+#include "pw_bytes/array.h"
+#include "pw_checksum/crc16_ccitt.h"
+#include "pw_perf_test/perf_test.h"
+#include "pw_span/span.h"
+
+namespace pw::checksum {
+namespace {
+
+constexpr std::string_view kString =
+ "In the beginning the Universe was created. This has made a lot of "
+ "people very angry and been widely regarded as a bad move.";
+constexpr auto kBytes = bytes::Initialized<1000>([](size_t i) { return i; });
+
+PW_PERF_TEST_SIMPLE(CcittCalculationBytes, Crc16Ccitt::Calculate, kBytes);
+
+PW_PERF_TEST_SIMPLE(CcittCalculationString,
+ Crc16Ccitt::Calculate,
+ as_bytes(span(kString)));
+
+} // namespace
+} // namespace pw::checksum
diff --git a/pw_checksum/crc16_ccitt_test.cc b/pw_checksum/crc16_ccitt_test.cc
index 3ac9c78f6..9fefd8f55 100644
--- a/pw_checksum/crc16_ccitt_test.cc
+++ b/pw_checksum/crc16_ccitt_test.cc
@@ -35,7 +35,7 @@ constexpr std::string_view kString =
constexpr uint16_t kStringCrc = 0xC184;
TEST(Crc16, Empty) {
- EXPECT_EQ(Crc16Ccitt::Calculate(std::span<std::byte>()),
+ EXPECT_EQ(Crc16Ccitt::Calculate(span<std::byte>()),
Crc16Ccitt::kInitialValue);
}
@@ -48,24 +48,22 @@ TEST(Crc16, ByteByByte) {
}
TEST(Crc16, Buffer) {
- EXPECT_EQ(Crc16Ccitt::Calculate(std::as_bytes(std::span(kBytes))),
- kBufferCrc);
+ EXPECT_EQ(Crc16Ccitt::Calculate(as_bytes(span(kBytes))), kBufferCrc);
}
TEST(Crc16, String) {
- EXPECT_EQ(Crc16Ccitt::Calculate(std::as_bytes(std::span(kString))),
- kStringCrc);
+ EXPECT_EQ(Crc16Ccitt::Calculate(as_bytes(span(kString))), kStringCrc);
}
TEST(Crc16Class, Buffer) {
Crc16Ccitt crc16;
- crc16.Update(std::as_bytes(std::span(kBytes)));
+ crc16.Update(as_bytes(span(kBytes)));
EXPECT_EQ(crc16.value(), kBufferCrc);
}
TEST(Crc16Class, String) {
Crc16Ccitt crc16;
- crc16.Update(std::as_bytes(std::span(kString)));
+ crc16.Update(as_bytes(span(kString)));
EXPECT_EQ(crc16.value(), kStringCrc);
}
diff --git a/pw_checksum/crc32.cc b/pw_checksum/crc32.cc
index 46a809167..68e3996a8 100644
--- a/pw_checksum/crc32.cc
+++ b/pw_checksum/crc32.cc
@@ -17,60 +17,79 @@
namespace pw::checksum {
namespace {
-constexpr uint32_t kCrc32Table[] = {
- 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
- 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
- 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
- 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
- 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
- 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
- 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
- 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
- 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
- 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
- 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
- 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
- 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
- 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
- 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
- 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
- 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
- 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
- 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
- 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
- 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
- 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
- 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
- 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
- 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
- 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
- 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
- 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
- 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
- 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
- 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
- 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
- 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
- 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
- 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
- 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
- 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
- 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
- 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
- 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
- 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
- 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
- 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d};
+// Calculates the partial CRC32 of the low order kBits of value using
+// the reversed polynomial kPolynomial. This is a building block for
+// both implementing a tableless CRC32 implementation as well as generating
+// look up tables for tables based implementations.
+//
+// Information on CRC32 can be found at:
+// https://en.wikipedia.org/wiki/Cyclic_redundancy_check
+template <std::size_t kBits, uint32_t kPolynomial>
+constexpr uint32_t Crc32ProcessDataChunk(uint32_t value) {
+ for (uint32_t j = 0; j < kBits; j++) {
+ value = (value >> 1u) ^
+ (static_cast<uint32_t>(-static_cast<int32_t>(value & 1u)) &
+ kPolynomial);
+ }
+ return value;
+}
+
+// Generates a lookup table for a table based CRC32 implementation.
+// The table pre-computes the CRC for every value representable by
+// kBits of data. kPolynomial is used as the reversed polynomial
+// for the computation. The returned table will have 2^kBits entries.
+template <std::size_t kBits, uint32_t kPolynomial>
+constexpr std::array<uint32_t, (1 << kBits)> GenerateCrc32Table() {
+ std::array<uint32_t, (1 << kBits)> table{};
+ for (uint32_t i = 0; i < (1 << kBits); i++) {
+ table[i] = Crc32ProcessDataChunk<kBits, kPolynomial>(i);
+ }
+ return table;
+}
+
+// Reversed polynomial for the commonly used CRC32 variant. See:
+// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Polynomial_representations_of_cyclic_redundancy_checks
+constexpr uint32_t kCrc32Polynomial = 0xEDB88320;
} // namespace
-extern "C" uint32_t _pw_checksum_InternalCrc32(const void* data,
- size_t size_bytes,
- uint32_t state) {
- const uint8_t* array = static_cast<const uint8_t*>(data);
+extern "C" uint32_t _pw_checksum_InternalCrc32EightBit(const void* data,
+ size_t size_bytes,
+ uint32_t state) {
+ static constexpr std::array<uint32_t, 256> kCrc32Table =
+ GenerateCrc32Table<8, kCrc32Polynomial>();
+ const uint8_t* data_bytes = static_cast<const uint8_t*>(data);
+
+ for (size_t i = 0; i < size_bytes; ++i) {
+ state = kCrc32Table[(state ^ data_bytes[i]) & 0xFFu] ^ (state >> 8);
+ }
+
+ return state;
+}
+
+extern "C" uint32_t _pw_checksum_InternalCrc32FourBit(const void* data,
+ size_t size_bytes,
+ uint32_t state) {
+ static constexpr std::array<uint32_t, 16> kCrc32Table =
+ GenerateCrc32Table<4, kCrc32Polynomial>();
+ const uint8_t* data_bytes = static_cast<const uint8_t*>(data);
+
+ for (size_t i = 0; i < size_bytes; ++i) {
+ state ^= data_bytes[i];
+ state = kCrc32Table[state & 0x0f] ^ (state >> 4);
+ state = kCrc32Table[state & 0x0f] ^ (state >> 4);
+ }
+
+ return state;
+}
+
+extern "C" uint32_t _pw_checksum_InternalCrc32OneBit(const void* data,
+ size_t size_bytes,
+ uint32_t state) {
+ const uint8_t* data_bytes = static_cast<const uint8_t*>(data);
for (size_t i = 0; i < size_bytes; ++i) {
- state = kCrc32Table[(state ^ array[i]) & 0xFFu] ^ (state >> 8);
+ state = Crc32ProcessDataChunk<8, kCrc32Polynomial>(state ^ data_bytes[i]);
}
return state;
diff --git a/pw_checksum/crc32_perf_test.cc b/pw_checksum/crc32_perf_test.cc
new file mode 100644
index 000000000..2d96e20a7
--- /dev/null
+++ b/pw_checksum/crc32_perf_test.cc
@@ -0,0 +1,58 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <cstdint>
+#include <string_view>
+
+#include "pw_bytes/array.h"
+#include "pw_checksum/crc32.h"
+#include "pw_perf_test/perf_test.h"
+#include "pw_span/span.h"
+
+namespace pw::checksum {
+namespace {
+
+constexpr std::string_view kString =
+ "In the beginning the Universe was created. This has made a lot of "
+ "people very angry and been widely regarded as a bad move.";
+constexpr auto kBytes = bytes::Array<1, 2, 3, 4, 5, 6, 7, 8, 9>();
+
+void Crc32OneBitTest(perf_test::State& state, span<const std::byte> data) {
+ while (state.KeepRunning()) {
+ Crc32OneBit::Calculate(data);
+ }
+}
+
+void Crc32FourBitTest(perf_test::State& state, span<const std::byte> data) {
+ while (state.KeepRunning()) {
+ Crc32FourBit::Calculate(data);
+ }
+}
+
+void Crc32EightBitTest(perf_test::State& state, span<const std::byte> data) {
+ while (state.KeepRunning()) {
+ Crc32EightBit::Calculate(data);
+ }
+}
+
+PW_PERF_TEST(CrcOneBitStringTest, Crc32OneBitTest, as_bytes(span(kString)));
+PW_PERF_TEST(CrcFourBitStringTest, Crc32FourBitTest, as_bytes(span(kString)));
+PW_PERF_TEST(CrcEightBitStringTest, Crc32EightBitTest, as_bytes(span(kString)));
+
+PW_PERF_TEST(CrcOneBitBytesTest, Crc32OneBitTest, kBytes);
+PW_PERF_TEST(CrcFourBitBytesTest, Crc32FourBitTest, kBytes);
+PW_PERF_TEST(CrcEightBitBytesTest, Crc32EightBitTest, kBytes);
+
+} // namespace
+} // namespace pw::checksum
diff --git a/pw_checksum/crc32_test.cc b/pw_checksum/crc32_test.cc
index 59d649ab5..24e59e832 100644
--- a/pw_checksum/crc32_test.cc
+++ b/pw_checksum/crc32_test.cc
@@ -13,11 +13,12 @@
// the License.
#include "pw_checksum/crc32.h"
-#include <span>
#include <string_view>
#include "gtest/gtest.h"
+#include "public/pw_checksum/crc32.h"
#include "pw_bytes/array.h"
+#include "pw_span/span.h"
namespace pw::checksum {
namespace {
@@ -39,44 +40,87 @@ constexpr std::string_view kString =
constexpr uint32_t kStringCrc = 0x9EC87F88;
TEST(Crc32, Empty) {
- EXPECT_EQ(Crc32::Calculate(std::span<std::byte>()), PW_CHECKSUM_EMPTY_CRC32);
+ EXPECT_EQ(Crc32::Calculate(span<std::byte>()), PW_CHECKSUM_EMPTY_CRC32);
+ EXPECT_EQ(Crc32EightBit::Calculate(span<std::byte>()),
+ PW_CHECKSUM_EMPTY_CRC32);
+ EXPECT_EQ(Crc32FourBit::Calculate(span<std::byte>()),
+ PW_CHECKSUM_EMPTY_CRC32);
+ EXPECT_EQ(Crc32OneBit::Calculate(span<std::byte>()), PW_CHECKSUM_EMPTY_CRC32);
}
TEST(Crc32, Buffer) {
- EXPECT_EQ(Crc32::Calculate(std::as_bytes(std::span(kBytes))), kBufferCrc);
+ EXPECT_EQ(Crc32::Calculate(as_bytes(span(kBytes))), kBufferCrc);
+ EXPECT_EQ(Crc32EightBit::Calculate(as_bytes(span(kBytes))), kBufferCrc);
+ EXPECT_EQ(Crc32FourBit::Calculate(as_bytes(span(kBytes))), kBufferCrc);
+ EXPECT_EQ(Crc32OneBit::Calculate(as_bytes(span(kBytes))), kBufferCrc);
}
TEST(Crc32, String) {
- EXPECT_EQ(Crc32::Calculate(std::as_bytes(std::span(kString))), kStringCrc);
+ EXPECT_EQ(Crc32::Calculate(as_bytes(span(kString))), kStringCrc);
+ EXPECT_EQ(Crc32EightBit::Calculate(as_bytes(span(kString))), kStringCrc);
+ EXPECT_EQ(Crc32FourBit::Calculate(as_bytes(span(kString))), kStringCrc);
+ EXPECT_EQ(Crc32OneBit::Calculate(as_bytes(span(kString))), kStringCrc);
}
-TEST(Crc32Class, ByteByByte) {
- Crc32 crc;
+template <typename CrcVariant>
+void TestByByte() {
+ CrcVariant crc;
for (std::byte b : kBytes) {
crc.Update(b);
}
EXPECT_EQ(crc.value(), kBufferCrc);
}
-TEST(Crc32Class, Buffer) {
- Crc32 crc32;
- crc32.Update(std::as_bytes(std::span(kBytes)));
+TEST(Crc32Class, ByteByByte) {
+ TestByByte<Crc32>();
+ TestByByte<Crc32EightBit>();
+ TestByByte<Crc32FourBit>();
+ TestByByte<Crc32OneBit>();
+}
+
+template <typename CrcVariant>
+void TestBuffer() {
+ CrcVariant crc32;
+ crc32.Update(as_bytes(span(kBytes)));
EXPECT_EQ(crc32.value(), kBufferCrc);
}
-TEST(Crc32Class, BufferAppend) {
- Crc32 crc32;
+TEST(Crc32Class, Buffer) {
+ TestBuffer<Crc32>();
+ TestBuffer<Crc32EightBit>();
+ TestBuffer<Crc32FourBit>();
+ TestBuffer<Crc32OneBit>();
+}
+
+template <typename CrcVariant>
+void TestBufferAppend() {
+ CrcVariant crc32;
crc32.Update(kBytesPart0);
crc32.Update(kBytesPart1);
EXPECT_EQ(crc32.value(), kBufferCrc);
}
-TEST(Crc32Class, String) {
- Crc32 crc32;
- crc32.Update(std::as_bytes(std::span(kString)));
+TEST(Crc32Class, BufferAppend) {
+ TestBufferAppend<Crc32>();
+ TestBufferAppend<Crc32EightBit>();
+ TestBufferAppend<Crc32FourBit>();
+ TestBufferAppend<Crc32OneBit>();
+}
+
+template <typename CrcVariant>
+void TestString() {
+ CrcVariant crc32;
+ crc32.Update(as_bytes(span(kString)));
EXPECT_EQ(crc32.value(), kStringCrc);
}
+TEST(Crc32Class, String) {
+ TestString<Crc32>();
+ TestString<Crc32EightBit>();
+ TestString<Crc32FourBit>();
+ TestString<Crc32OneBit>();
+}
+
extern "C" uint32_t CallChecksumCrc32(const void* data, size_t size_bytes);
extern "C" uint32_t CallChecksumCrc32Append(const void* data,
size_t size_bytes,
diff --git a/pw_checksum/docs.rst b/pw_checksum/docs.rst
index 456e08038..aa9a0a831 100644
--- a/pw_checksum/docs.rst
+++ b/pw_checksum/docs.rst
@@ -54,6 +54,69 @@ pw_checksum/crc32.h
uint32_t crc = Crc32(my_data);
crc = Crc32(more_data, crc);
+.. _CRC32 Implementations:
+
+Implementations
+---------------
+Pigweed provides 3 different CRC32 implementations with different size and
+runtime tradeoffs. The below table summarizes the variants. For more detailed
+size information see the :ref:`pw_checksum-size-report` below. Instructions
+counts were calculated by hand by analyzing the
+`assembly <https://godbolt.org/z/nY1bbb5Pb>`_. Clock Cycle counts were measured
+using :ref:`module-pw_perf_test` on a STM32F429I-DISC1 development board.
+
+
+.. list-table::
+ :header-rows: 1
+
+ * - Variant
+ - Relative size (see Size Report below)
+ - Speed
+ - Lookup table size (entries)
+ - Instructions/byte (M33/-Os)
+ - Clock Cycles (123 char string)
+ - Clock Cycles (9 bytes)
+ * - 8 bits per iteration (default)
+ - large
+ - fastest
+ - 256
+ - 8
+ - 1538
+ - 170
+ * - 4 bits per iteration
+ - small
+ - fast
+ - 16
+ - 13
+ - 2153
+ - 215
+ * - 1 bit per iteration
+ - smallest
+ - slow
+ - 0
+ - 43
+ - 7690
+ - 622
+
+The default implementation provided by the APIs above can be selected through
+:ref:`Module Configuration Options`. Additionally ``pw_checksum`` provides
+variants of the C++ API to explicitly use each of the implementations. These
+classes provide the same API as ``Crc32``:
+
+* ``Crc32EightBit``
+* ``Crc32FourBit``
+* ``Crc32OneBit``
+
+.. _pw_checksum-size-report:
+
+Size report
+===========
+The CRC module currently optimizes for speed instead of binary size, by using
+pre-computed 256-entry tables to reduce the CPU cycles per byte CRC
+calculation.
+
+.. include:: size_report
+
Compatibility
=============
* C
@@ -63,6 +126,24 @@ Dependencies
============
* ``pw_span``
+.. _Module Configuration Options:
+
+Module Configuration Options
+============================
+The following configurations can be adjusted via compile-time configuration of
+this module, see the
+:ref:`module documentation <module-structure-compile-time-configuration>` for
+more details.
+
+.. c:macro:: PW_CHECKSUM_CRC32_DEFAULT_IMPL
+
+ Selects which of the :ref:`CRC32 Implementations` the default CRC32 APIs
+ use. Set to one of the following values:
+
+ * ``PW_CHECKSUM_CRC32_8BITS``
+ * ``PW_CHECKSUM_CRC32_4BITS``
+ * ``PW_CHECKSUM_CRC32_1BITS``
+
Zephyr
======
To enable ``pw_checksum`` for Zephyr add ``CONFIG_PIGWEED_CHECKSUM=y`` to the
diff --git a/pw_checksum/public/pw_checksum/crc16_ccitt.h b/pw_checksum/public/pw_checksum/crc16_ccitt.h
index 0a9daff9b..663d7653a 100644
--- a/pw_checksum/public/pw_checksum/crc16_ccitt.h
+++ b/pw_checksum/public/pw_checksum/crc16_ccitt.h
@@ -35,9 +35,8 @@ uint16_t pw_checksum_Crc16Ccitt(const void* data,
#ifdef __cplusplus
} // extern "C"
-#include <span>
-
#include "pw_bytes/span.h"
+#include "pw_span/span.h"
namespace pw::checksum {
@@ -49,7 +48,7 @@ class Crc16Ccitt {
// Calculates the CRC-16-CCITT for the provided data and returns it as a
// uint16_t. To update a CRC in multiple calls, use an instance of the
// Crc16Ccitt class or pass the previous value as the initial_value argument.
- static uint16_t Calculate(std::span<const std::byte> data,
+ static uint16_t Calculate(span<const std::byte> data,
uint16_t initial_value = kInitialValue) {
return pw_checksum_Crc16Ccitt(
data.data(), data.size_bytes(), initial_value);
@@ -62,9 +61,7 @@ class Crc16Ccitt {
constexpr Crc16Ccitt() : value_(kInitialValue) {}
- void Update(std::span<const std::byte> data) {
- value_ = Calculate(data, value_);
- }
+ void Update(span<const std::byte> data) { value_ = Calculate(data, value_); }
void Update(std::byte data) { Update(ByteSpan(&data, 1)); }
diff --git a/pw_checksum/public/pw_checksum/crc32.h b/pw_checksum/public/pw_checksum/crc32.h
index a86636576..f97d48115 100644
--- a/pw_checksum/public/pw_checksum/crc32.h
+++ b/pw_checksum/public/pw_checksum/crc32.h
@@ -20,6 +20,8 @@
#include <stddef.h>
#include <stdint.h>
+#include "pw_checksum/internal/config.h"
+
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
@@ -33,9 +35,24 @@ extern "C" {
#define _PW_CHECKSUM_CRC32_INITIAL_STATE 0xFFFFFFFFu
// Internal implementation function for CRC32. Do not call it directly.
-uint32_t _pw_checksum_InternalCrc32(const void* data,
- size_t size_bytes,
- uint32_t state);
+uint32_t _pw_checksum_InternalCrc32EightBit(const void* data,
+ size_t size_bytes,
+ uint32_t state);
+
+uint32_t _pw_checksum_InternalCrc32FourBit(const void* data,
+ size_t size_bytes,
+ uint32_t state);
+uint32_t _pw_checksum_InternalCrc32OneBit(const void* data,
+ size_t size_bytes,
+ uint32_t state);
+
+#if PW_CHECKSUM_CRC32_DEFAULT_IMPL == PW_CHECKSUM_CRC32_8BITS
+#define _pw_checksum_InternalCrc32 _pw_checksum_InternalCrc32EightBit
+#elif PW_CHECKSUM_CRC32_DEFAULT_IMPL == PW_CHECKSUM_CRC32_4BITS
+#define _pw_checksum_InternalCrc32 _pw_checksum_InternalCrc32FourBit
+#elif PW_CHECKSUM_CRC32_DEFAULT_IMPL == PW_CHECKSUM_CRC32_1BITS
+#define _pw_checksum_InternalCrc32 _pw_checksum_InternalCrc32OneBit
+#endif
// Calculates the CRC32 for the provided data.
static inline uint32_t pw_checksum_Crc32(const void* data, size_t size_bytes) {
@@ -57,7 +74,7 @@ static inline uint32_t pw_checksum_Crc32Append(const void* data,
#ifdef __cplusplus
} // extern "C"
-#include <span>
+#include "pw_span/span.h"
namespace pw::checksum {
@@ -65,21 +82,23 @@ namespace pw::checksum {
//
// This class is more efficient than the CRC32 C functions since it doesn't
// finalize the value each time it is appended to.
-class Crc32 {
+template <uint32_t (*kChecksumFunction)(const void*, size_t, uint32_t)>
+class Crc32Impl {
public:
// Calculates the CRC32 for the provided data and returns it as a uint32_t.
// To update a CRC in multiple pieces, use an instance of the Crc32 class.
- static uint32_t Calculate(std::span<const std::byte> data) {
- return pw_checksum_Crc32(data.data(), data.size_bytes());
+ static uint32_t Calculate(span<const std::byte> data) {
+ return ~kChecksumFunction(
+ data.data(), data.size_bytes(), _PW_CHECKSUM_CRC32_INITIAL_STATE);
}
- constexpr Crc32() : state_(kInitialValue) {}
+ constexpr Crc32Impl() : state_(kInitialValue) {}
- void Update(std::span<const std::byte> data) {
- state_ = _pw_checksum_InternalCrc32(data.data(), data.size(), state_);
+ void Update(span<const std::byte> data) {
+ state_ = kChecksumFunction(data.data(), data.size(), state_);
}
- void Update(std::byte data) { Update(std::span(&data, 1)); }
+ void Update(std::byte data) { Update(span(&data, 1)); }
// Returns the value of the CRC32 for all data passed to Update.
uint32_t value() const { return ~state_; }
@@ -93,6 +112,11 @@ class Crc32 {
uint32_t state_;
};
+using Crc32 = Crc32Impl<_pw_checksum_InternalCrc32>;
+using Crc32EightBit = Crc32Impl<_pw_checksum_InternalCrc32EightBit>;
+using Crc32FourBit = Crc32Impl<_pw_checksum_InternalCrc32FourBit>;
+using Crc32OneBit = Crc32Impl<_pw_checksum_InternalCrc32OneBit>;
+
} // namespace pw::checksum
#endif // __cplusplus
diff --git a/pw_checksum/public/pw_checksum/internal/config.h b/pw_checksum/public/pw_checksum/internal/config.h
new file mode 100644
index 000000000..f35d28a43
--- /dev/null
+++ b/pw_checksum/public/pw_checksum/internal/config.h
@@ -0,0 +1,29 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#define PW_CHECKSUM_CRC32_8BITS 8
+#define PW_CHECKSUM_CRC32_4BITS 4
+#define PW_CHECKSUM_CRC32_1BITS 1
+
+#ifndef PW_CHECKSUM_CRC32_DEFAULT_IMPL
+#define PW_CHECKSUM_CRC32_DEFAULT_IMPL PW_CHECKSUM_CRC32_8BITS
+#endif // PW_CHECKSUM_CRC32_DEFAULT_IMPL
+
+#ifdef __cplusplus
+static_assert(PW_CHECKSUM_CRC32_DEFAULT_IMPL == PW_CHECKSUM_CRC32_8BITS ||
+ PW_CHECKSUM_CRC32_DEFAULT_IMPL == PW_CHECKSUM_CRC32_4BITS ||
+ PW_CHECKSUM_CRC32_DEFAULT_IMPL == PW_CHECKSUM_CRC32_1BITS);
+#endif // __cplusplus
diff --git a/pw_checksum/size_report/BUILD.bazel b/pw_checksum/size_report/BUILD.bazel
new file mode 100644
index 000000000..2ae0b2e83
--- /dev/null
+++ b/pw_checksum/size_report/BUILD.bazel
@@ -0,0 +1,109 @@
+# Copyright 2019 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_binary",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# TODO(keir): Figure out why sharing deps doesn't work. Switching the hardcoded
+# deps to this shared variable breaks with a confusing error message;
+#
+# ERROR: /Users/keir/wrk/pigweed/pw_checksum/size_report/BUILD.bazel:46:13:
+# Label '//pw_assert:pw_assert' is duplicated in the 'deps'
+# attribute of rule 'crc16_checksum'
+# ERROR: package contains errors: pw_checksum/size_report
+#
+
+pw_cc_binary(
+ name = "noop_checksum",
+ srcs = ["run_checksum.cc"],
+ copts = ["-DUSE_NOOP_CHECKSUM=1"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_checksum",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_span",
+ ],
+)
+
+pw_cc_binary(
+ name = "crc16_checksum",
+ srcs = ["run_checksum.cc"],
+ copts = ["-DUSE_CRC16_CHECKSUM=1"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_checksum",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_span",
+ ],
+)
+
+pw_cc_binary(
+ name = "crc32_8bit_checksum",
+ srcs = ["run_checksum.cc"],
+ copts = ["-DUSE_CRC32_8BIT_CHECKSUM=1"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_checksum",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_span",
+ ],
+)
+
+pw_cc_binary(
+ name = "crc32_4bit_checksum",
+ srcs = ["run_checksum.cc"],
+ copts = ["-DUSE_CRC32_4BIT_CHECKSUM=1"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_checksum",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_span",
+ ],
+)
+
+pw_cc_binary(
+ name = "crc32_1bit_checksum",
+ srcs = ["run_checksum.cc"],
+ copts = ["-DUSE_CRC32_1BIT_CHECKSUM=1"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_checksum",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_span",
+ ],
+)
+
+pw_cc_binary(
+ name = "fletcher16_checksum",
+ srcs = ["run_checksum.cc"],
+ copts = ["-DUSE_FLETCHER16_CHECKSUM=1"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_checksum",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_span",
+ ],
+)
diff --git a/pw_checksum/size_report/BUILD.gn b/pw_checksum/size_report/BUILD.gn
new file mode 100644
index 000000000..634cad4ac
--- /dev/null
+++ b/pw_checksum/size_report/BUILD.gn
@@ -0,0 +1,107 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_executable("noop_checksum") {
+ sources = [ "run_checksum.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_log",
+ "$dir_pw_preprocessor",
+ "$dir_pw_span",
+ "..",
+ ]
+ defines = [ "USE_NOOP_CHECKSUM=1" ]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_executable("crc32_8bit_checksum") {
+ sources = [ "run_checksum.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_log",
+ "$dir_pw_preprocessor",
+ "$dir_pw_span",
+ "..",
+ ]
+ defines = [ "USE_CRC32_8BIT_CHECKSUM=1" ]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_executable("crc32_4bit_checksum") {
+ sources = [ "run_checksum.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_log",
+ "$dir_pw_preprocessor",
+ "$dir_pw_span",
+ "..",
+ ]
+ defines = [ "USE_CRC32_4BIT_CHECKSUM=1" ]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_executable("crc32_1bit_checksum") {
+ sources = [ "run_checksum.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_log",
+ "$dir_pw_preprocessor",
+ "$dir_pw_span",
+ "..",
+ ]
+ defines = [ "USE_CRC32_1BIT_CHECKSUM=1" ]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_executable("crc16_checksum") {
+ sources = [ "run_checksum.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_log",
+ "$dir_pw_preprocessor",
+ "$dir_pw_span",
+ "..",
+ ]
+ defines = [ "USE_CRC16_CHECKSUM=1" ]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
+
+pw_executable("fletcher16_checksum") {
+ sources = [ "run_checksum.cc" ]
+ deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_log",
+ "$dir_pw_preprocessor",
+ "$dir_pw_span",
+ "..",
+ ]
+ defines = [ "USE_FLETCHER16_CHECKSUM=1" ]
+
+ # TODO(b/259746255): Remove this when everything compiles with -Wconversion.
+ configs = [ "$dir_pw_build:conversion_warnings" ]
+}
diff --git a/pw_checksum/size_report/run_checksum.cc b/pw_checksum/size_report/run_checksum.cc
new file mode 100644
index 000000000..d37219081
--- /dev/null
+++ b/pw_checksum/size_report/run_checksum.cc
@@ -0,0 +1,119 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <cstring>
+
+#include "pw_bloat/bloat_this_binary.h"
+#include "pw_log/log.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
+
+#ifdef USE_CRC16_CHECKSUM
+#include "pw_checksum/crc16_ccitt.h"
+using TheChecksum = pw::checksum::Crc16Ccitt;
+#endif
+
+#ifdef USE_CRC32_8BIT_CHECKSUM
+#include "pw_checksum/crc32.h"
+using TheChecksum = pw::checksum::Crc32EightBit;
+#endif
+
+#ifdef USE_CRC32_4BIT_CHECKSUM
+#include "pw_checksum/crc32.h"
+using TheChecksum = pw::checksum::Crc32FourBit;
+#endif
+
+#ifdef USE_CRC32_1BIT_CHECKSUM
+#include "pw_checksum/crc32.h"
+using TheChecksum = pw::checksum::Crc32OneBit;
+#endif
+
+namespace pw::checksum {
+
+#ifdef USE_NOOP_CHECKSUM
+class NoOpChecksum {
+ public:
+ static uint32_t Calculate(span<const std::byte>) { return arbitrary_value; }
+
+ // Don't inline to prevent the compiler from optimizing out the checksum.
+ PW_NO_INLINE void Update(span<const std::byte>) {}
+
+ PW_NO_INLINE void Update(std::byte) {}
+
+ PW_NO_INLINE uint32_t value() const { return arbitrary_value; }
+ void clear() {}
+
+ private:
+ // static volatile uint32_t arbitrary_value;
+ const static uint32_t arbitrary_value = 10;
+};
+using TheChecksum = NoOpChecksum;
+#endif
+
+// Fletcher16 is a simple checksum that shouldn't be used in production, but is
+// interesting from a size comparison perspective.
+#ifdef USE_FLETCHER16_CHECKSUM
+class Fletcher16 {
+ public:
+ Fletcher16() : sum1_(0), sum2_(0) {}
+
+ // Don't inline to prevent the compiler from optimizing out the checksum.
+ PW_NO_INLINE static uint32_t Calculate(span<const std::byte> data) {
+ Fletcher16 checksum;
+ checksum.Update(data);
+ return checksum.value();
+ }
+
+ PW_NO_INLINE void Update(span<const std::byte> data) {
+ for (std::byte b : data) {
+ sum1_ = static_cast<uint16_t>((sum1_ + static_cast<uint16_t>(b)) % 255u);
+ sum2_ = static_cast<uint16_t>((sum2_ + sum1_) % 255u);
+ }
+ }
+ PW_NO_INLINE void Update(std::byte) {}
+ PW_NO_INLINE uint32_t value() const { return (sum2_ << 8) | sum1_; };
+ void clear() {}
+
+ private:
+ uint16_t sum1_ = 0;
+ uint16_t sum2_ = 0;
+};
+using TheChecksum = Fletcher16;
+#endif
+
+char buffer[128];
+char* volatile get_buffer = buffer;
+volatile unsigned get_size;
+
+unsigned RunChecksum() {
+ // Trick the optimizer and also satisfy the type checker.
+ get_size = sizeof(buffer);
+ char* local_buffer = get_buffer;
+ unsigned local_size = get_size;
+
+ // Calculate the checksum and stash it in a volatile variable so the compiler
+ // can't optimize it out.
+ TheChecksum checksum;
+ checksum.Update(pw::as_bytes(span(local_buffer, local_size)));
+ uint32_t value = static_cast<uint32_t>(checksum.value());
+ *get_buffer = static_cast<char>(value);
+ return 0;
+}
+
+} // namespace pw::checksum
+
+int main() {
+ pw::bloat::BloatThisBinary();
+ return pw::checksum::RunChecksum();
+}
diff --git a/pw_chrono/BUILD.bazel b/pw_chrono/BUILD.bazel
index 0704ccafa..172abf850 100644
--- a/pw_chrono/BUILD.bazel
+++ b/pw_chrono/BUILD.bazel
@@ -18,6 +18,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -96,6 +98,25 @@ pw_cc_library(
}),
)
+proto_library(
+ name = "chrono_proto",
+ srcs = [
+ "chrono.proto",
+ ],
+ import_prefix = "pw_chrono_protos",
+ strip_import_prefix = "/pw_chrono",
+)
+
+py_proto_library(
+ name = "chrono_proto_pb2",
+ srcs = ["chrono.proto"],
+)
+
+pw_proto_library(
+ name = "chrono_proto_cc",
+ deps = [":chrono_proto"],
+)
+
pw_cc_library(
name = "simulated_system_clock",
hdrs = [
diff --git a/pw_chrono/BUILD.gn b/pw_chrono/BUILD.gn
index 433ab9065..8c8004f0d 100644
--- a/pw_chrono/BUILD.gn
+++ b/pw_chrono/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
@@ -100,6 +101,11 @@ pw_test("system_timer_facade_test") {
]
}
+pw_proto_library("protos") {
+ sources = [ "chrono.proto" ]
+ prefix = "pw_chrono_protos"
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_chrono/CMakeLists.txt b/pw_chrono/CMakeLists.txt
index 8e316725c..5d865e496 100644
--- a/pw_chrono/CMakeLists.txt
+++ b/pw_chrono/CMakeLists.txt
@@ -13,15 +13,20 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_chrono/backend.cmake)
+include($ENV{PW_ROOT}/pw_sync/backend.cmake)
+include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
-pw_add_module_library(pw_chrono.epoch
+pw_add_library(pw_chrono.epoch INTERFACE
HEADERS
public/pw_chrono/epoch.h
PUBLIC_INCLUDES
public
)
-pw_add_facade(pw_chrono.system_clock
+pw_add_facade(pw_chrono.system_clock STATIC
+ BACKEND
+ pw_chrono.system_clock_BACKEND
HEADERS
public/pw_chrono/internal/system_clock_macros.h
public/pw_chrono/system_clock.h
@@ -34,7 +39,9 @@ pw_add_facade(pw_chrono.system_clock
system_clock.cc
)
-pw_add_facade(pw_chrono.system_timer
+pw_add_facade(pw_chrono.system_timer INTERFACE
+ BACKEND
+ pw_chrono.system_timer_BACKEND
HEADERS
public/pw_chrono/system_timer.h
PUBLIC_INCLUDES
@@ -45,7 +52,7 @@ pw_add_facade(pw_chrono.system_timer
)
# Dependency injectable implementation of pw::chrono::SystemClock::Interface.
-pw_add_module_library(pw_chrono.simulated_system_clock
+pw_add_library(pw_chrono.simulated_system_clock INTERFACE
HEADERS
public/pw_chrono/simulated_system_clock.h
PUBLIC_INCLUDES
@@ -55,31 +62,32 @@ pw_add_module_library(pw_chrono.simulated_system_clock
pw_sync.interrupt_spin_lock
)
-# TODO(ewout): Renable this once we've resolved the backend variable definition
-# ordering issue, likely by mirroring GN's definition of variables in external
-# files which can be imported where needed.
-# if((NOT "${pw_chrono.system_clock_BACKEND}"
-# STREQUAL "pw_chrono.system_clock.NO_BACKEND_SET") AND
-# (NOT "${pw_sync.interrupt_spin_lock_BACKEND}"
-# STREQUAL "pw_sync.interrupt_spin_lock.NO_BACKEND_SET"))
-# pw_add_test(pw_chrono.simulated_system_clock_test
-# SOURCES
-# simulated_system_clock_test.cc
-# DEPS
-# pw_chrono.simulated_system_clock
-# GROUPS
-# modules
-# pw_chrono
-# )
-# endif()
+pw_proto_library(pw_chrono.protos
+ SOURCES
+ chrono.proto
+ PREFIX
+ pw_chrono_protos
+)
+
+if((NOT "${pw_chrono.system_clock_BACKEND}" STREQUAL "") AND
+ (NOT "${pw_sync.interrupt_spin_lock_BACKEND}" STREQUAL ""))
+ pw_add_test(pw_chrono.simulated_system_clock_test
+ SOURCES
+ simulated_system_clock_test.cc
+ PRIVATE_DEPS
+ pw_chrono.simulated_system_clock
+ GROUPS
+ modules
+ pw_chrono
+ )
+endif()
-if(NOT "${pw_chrono.system_clock_BACKEND}"
- STREQUAL "pw_chrono.system_clock.NO_BACKEND_SET")
+if(NOT "${pw_chrono.system_clock_BACKEND}" STREQUAL "")
pw_add_test(pw_chrono.system_clock_facade_test
SOURCES
system_clock_facade_test.cc
system_clock_facade_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_chrono.system_clock
pw_preprocessor
GROUPS
@@ -88,12 +96,11 @@ if(NOT "${pw_chrono.system_clock_BACKEND}"
)
endif()
-if(NOT "${pw_chrono.system_timer_BACKEND}"
- STREQUAL "pw_chrono.system_timer.NO_BACKEND_SET")
+if(NOT "${pw_chrono.system_timer_BACKEND}" STREQUAL "")
pw_add_test(pw_chrono.system_timer_facade_test
SOURCES
system_timer_facade_test.cc
- DEPS
+ PRIVATE_DEPS
pw_chrono.system_timer
pw_sync.thread_notification
GROUPS
diff --git a/pw_chrono/backend.cmake b/pw_chrono/backend.cmake
new file mode 100644
index 000000000..5a4572e8a
--- /dev/null
+++ b/pw_chrono/backend.cmake
@@ -0,0 +1,22 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_chrono module's system_clock.
+pw_add_backend_variable(pw_chrono.system_clock_BACKEND)
+
+# Backend for the pw_chrono module's system_timer.
+pw_add_backend_variable(pw_chrono.system_timer_BACKEND)
diff --git a/pw_chrono/chrono.proto b/pw_chrono/chrono.proto
new file mode 100644
index 000000000..17bbda147
--- /dev/null
+++ b/pw_chrono/chrono.proto
@@ -0,0 +1,101 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+syntax = "proto3";
+
+package pw.chrono;
+
+option java_outer_classname = "Chrono";
+
+message EpochType {
+ enum Enum {
+ UNKNOWN = 0;
+
+ TIME_SINCE_BOOT = 1;
+
+ // Time since 00:00:00 UTC, Thursday, 1 January 1970, including leap
+ // seconds.
+ UTC_WALL_CLOCK = 2;
+
+ // Time since 00:00:00, 6 January 1980 UTC. Leap seconds are not inserted
+ // into GPS. Thus, every time a leap second is inserted into UTC, UTC
+ // falls another second behind GPS.
+ GPS_WALL_CLOCK = 3;
+
+ // Time since 00:00:00, 1 January 1958, and is offset 10 seconds ahead of
+ // UTC at that date (i.e., its epoch, 1958-01-01 00:00:00 TAI, is
+ // 1957-12-31 23:59:50 UTC). Leap seconds are not inserted into TAI. Thus,
+ // every time a leap second is inserted into UTC, UTC falls another second
+ // behind TAI.
+ TAI_WALL_CLOCK = 4;
+ };
+}
+
+// A representation of a clock's parameters.
+//
+// There are two major components to representing a steady, monotonic clock:
+//
+// 1. A representation of the clock's period.
+// 2. A representation of the clock's epoch.
+//
+// To support a wide range of clock configurations, ClockParameters represents
+// a clock's period as fractions of a second. Concretely:
+//
+// Clock period (seconds) =
+// tick_period_seconds_numerator / tick_period_seconds_denominator
+//
+// So a simple 1KHz clock can be represented as:
+//
+// tick_period_seconds_numerator = 1
+// tick_period_seconds_denominator = 1000
+// Clock period = 1 / 1000 = 0.001 seconds
+// Clock frequency = 1 / 0.001 = 1,000 Hz
+//
+// Failing to specify one or both of the period members of a ClockParameters
+// message leaves the configuration specification incomplete and invalid.
+//
+// While clock period alone is enough to represent a duration if given a number
+// of ticks, an epoch is required to make a clock represent a time point.
+// EpochType optionally provides this information. Specifying an EpochType
+// defines what a tick count of `0` represents. Some epoch types (e.g. UTC, GPS,
+// TAI) allow the clock to resolve to real-world time points. If the EpochType
+// is relative to boot or unknown, however, the clock is only sufficiently
+// specified for relative time measurement without additional external
+// information.
+message ClockParameters {
+ int32 tick_period_seconds_numerator = 1; // Required
+ int32 tick_period_seconds_denominator = 2; // Required
+ optional EpochType.Enum epoch_type = 3;
+}
+
+// A point in time relative to a clock's epoch.
+message TimePoint {
+ // The duration that has elapsed (number of clock ticks) since the epoch,
+ // where the tick period and epoch are specified by the clock parameters.
+ //
+ // The meaning of `timestamp` is unspecified without an associated
+ // ClockParameters.
+ int64 timestamp = 1; // Required
+ ClockParameters clock_parameters = 2; // Required
+}
+
+// The time of a snapshot capture. Supports multiple timestamps to
+// cover multiple time bases or clocks (e.g. time since boot, time
+// from epoch, etc).
+//
+// This is an overlay proto for Snapshot, see more details here:
+// https://pigweed.dev/pw_snapshot/proto_format.html#module-specific-data
+message SnapshotTimestamps {
+ repeated TimePoint timestamps = 22;
+}
diff --git a/pw_chrono/docs.rst b/pw_chrono/docs.rst
index 4495b8713..776b5cd12 100644
--- a/pw_chrono/docs.rst
+++ b/pw_chrono/docs.rst
@@ -8,7 +8,7 @@ leveraging many pieces of STL's the ``std::chrono`` library but with a focus
on portability for constrained embedded devices and maintaining correctness.
.. note::
- This module is still under construction, the API is not yet stable.
+ This module is still under construction, the API is not yet stable.
-------------------------------
``duration`` and ``time_point``
@@ -47,41 +47,41 @@ following simplified model, ignoring most of their member functions:
.. code-block:: cpp
- namespace std::chrono {
+ namespace std::chrono {
- template<class Rep, class Period = std::ratio<1, 1>>
- class duration {
- public:
- using rep = Rep;
- using period = Period;
+ template<class Rep, class Period = std::ratio<1, 1>>
+ class duration {
+ public:
+ using rep = Rep;
+ using period = Period;
- constexpr rep count() const { return tick_count_; }
+ constexpr rep count() const { return tick_count_; }
- static constexpr duration zero() noexcept {
- return duration(0);
- }
+ static constexpr duration zero() noexcept {
+ return duration(0);
+ }
- // Other member functions...
+ // Other member functions...
- private:
- rep tick_count_;
- };
+ private:
+ rep tick_count_;
+ };
- template<class Clock, class Duration = typename Clock::duration>
- class time_point {
- public:
- using duration = Duration;
- using rep = Duration::rep;
- using period = Duration::period;
- using clock = Clock;
+ template<class Clock, class Duration = typename Clock::duration>
+ class time_point {
+ public:
+ using duration = Duration;
+ using rep = Duration::rep;
+ using period = Duration::period;
+ using clock = Clock;
- constexpr duration time_since_epoch() const { return time_since_epoch_; }
+ constexpr duration time_since_epoch() const { return time_since_epoch_; }
- // Other member functions...
+ // Other member functions...
- private:
- duration time_since_epoch_;
- };
+ private:
+ duration time_since_epoch_;
+ };
} // namespace std::chrono
@@ -107,11 +107,11 @@ With this guidance one can avoid common pitfalls like ``uint32_t`` millisecond
tick rollover bugs when using RTOSes every 49.7 days.
.. warning::
- We recommend avoiding the ``duration<>::min()`` and ``duration<>::max()``
- helper member functions where possible as they exceed the ±292 years duration
- limit assumption. There's an immediate risk of integer underflow or overflow
- for any arithmetic operations. Consider using ``std::optional`` instead of
- priming a variable with a value at the limit.
+ We recommend avoiding the ``duration<>::min()`` and ``duration<>::max()``
+ helper member functions where possible as they exceed the ±292 years duration
+ limit assumption. There's an immediate risk of integer underflow or overflow
+ for any arithmetic operations. Consider using ``std::optional`` instead of
+ priming a variable with a value at the limit.
Helper duration types and literals
==================================
@@ -129,11 +129,11 @@ As an example you can use these as follows:
.. code-block:: cpp
- #include <chrono>
+ #include <chrono>
- void Foo() {
- Bar(std::chrono::milliseconds(42));
- }
+ void Foo() {
+ Bar(std::chrono::milliseconds(42));
+ }
In addition, the inline namespace ``std::literals::chrono_literals`` includes:
@@ -148,12 +148,12 @@ As an example you can use these as follows:
.. code-block:: cpp
- using std::literals::chrono_literals::ms;
- // Or if you want them all: using namespace std::chrono_literals;
+ using std::literals::chrono_literals::ms;
+ // Or if you want them all: using namespace std::chrono_literals;
- void Foo() {
- Bar(42ms);
- }
+ void Foo() {
+ Bar(42ms);
+ }
For these helper duration types to be compatible with API's that take a
`SystemClock::duration` either an :ref:`implicit<Implicit lossless conversions>`
@@ -167,29 +167,29 @@ a 1kHz RTOS tick period and you would like to express a timeout duration:
.. code-block:: cpp
- // Instead of using ticks which are not portable between RTOS configurations,
- // as the tick period may be different:
- constexpr uint32_t kFooNotificationTimeoutTicks = 42;
- bool TryGetNotificationFor(uint32_t ticks);
-
- // And instead of using a time unit which is prone to accidental conversion
- // errors as all variables must maintain the time units:
- constexpr uint32_t kFooNotificationTimeoutMs = 42;
- bool TryGetNotificationFor(uint32_t milliseconds);
-
- // We can instead use a defined clock and its duration for the kernel and rely
- // on implicit lossless conversions:
- #include <chrono>
- #include "pw_chrono/system_clock.h"
- constexpr SystemClock::duration kFooNotificationTimeout =
- std::chrono::milliseconds(42);
- bool TryGetNotificationFor(SystemClock::duration timeout);
-
- void MaybeProcessNotification() {
- if (TryGetNotificationFor(kFooNotificationTimeout)) {
- ProcessNotification();
- }
- }
+ // Instead of using ticks which are not portable between RTOS configurations,
+ // as the tick period may be different:
+ constexpr uint32_t kFooNotificationTimeoutTicks = 42;
+ bool TryGetNotificationFor(uint32_t ticks);
+
+ // And instead of using a time unit which is prone to accidental conversion
+ // errors as all variables must maintain the time units:
+ constexpr uint32_t kFooNotificationTimeoutMs = 42;
+ bool TryGetNotificationFor(uint32_t milliseconds);
+
+ // We can instead use a defined clock and its duration for the kernel and rely
+ // on implicit lossless conversions:
+ #include <chrono>
+ #include "pw_chrono/system_clock.h"
+ constexpr SystemClock::duration kFooNotificationTimeout =
+ std::chrono::milliseconds(42);
+ bool TryGetNotificationFor(SystemClock::duration timeout);
+
+ void MaybeProcessNotification() {
+ if (TryGetNotificationFor(kFooNotificationTimeout)) {
+ ProcessNotification();
+ }
+ }
.. _Implicit lossless conversions:
@@ -213,15 +213,15 @@ some values like ``0``, ``1000``, etc.
.. code-block:: cpp
- #include <chrono>
+ #include <chrono>
- constexpr std::chrono::milliseconds this_compiles =
- std::chrono::seconds(42);
+ constexpr std::chrono::milliseconds this_compiles =
+ std::chrono::seconds(42);
- // This cannot compile, because for some duration values it is lossy even
- // though this particular value can be in theory converted to whole seconds.
- // constexpr std::chrono::seconds this_does_not_compile =
- // std::chrono::milliseconds(1000);
+ // This cannot compile, because for some duration values it is lossy even
+ // though this particular value can be in theory converted to whole seconds.
+ // constexpr std::chrono::seconds this_does_not_compile =
+ // std::chrono::milliseconds(1000);
.. _Explicit lossy conversions:
@@ -246,41 +246,41 @@ recommends explicitly using:
as a more explicit form of std::chrono::ceil.
.. Note::
- Pigweed does not recommend using ``std::chrono::duration_cast<>`` which
- truncates dowards zero like ``static_cast``. This is typically not the desired
- rounding behavior when dealing with time units. Instead, where possible we
- recommend the more explicit, self-documenting ``std::chrono::floor``,
- ``std::chrono::round``, and ``std::chrono::ceil``.
+ Pigweed does not recommend using ``std::chrono::duration_cast<>`` which
+ truncates dowards zero like ``static_cast``. This is typically not the desired
+ rounding behavior when dealing with time units. Instead, where possible we
+ recommend the more explicit, self-documenting ``std::chrono::floor``,
+ ``std::chrono::round``, and ``std::chrono::ceil``.
Now knowing this, the previous example could be portably and correctly handled
as follows:
.. code-block:: cpp
- #include <chrono>
+ #include <chrono>
- #include "pw_chrono/system_clock.h"
+ #include "pw_chrono/system_clock.h"
- // We want to round up to ensure we block for at least the specified duration,
- // instead of rounding down. Imagine for example the extreme case where you
- // may round down to zero or one, you would definitely want to at least block.
- constexpr SystemClock::duration kFooNotificationTimeout =
- std::chrono::ceil(std::chrono::milliseconds(42));
- bool TryGetNotificationFor(SystemClock::duration timeout);
+ // We want to round up to ensure we block for at least the specified duration,
+ // instead of rounding down. Imagine for example the extreme case where you
+ // may round down to zero or one, you would definitely want to at least block.
+ constexpr SystemClock::duration kFooNotificationTimeout =
+ std::chrono::ceil(std::chrono::milliseconds(42));
+ bool TryGetNotificationFor(SystemClock::duration timeout);
- void MaybeProcessNotification() {
- if (TryGetNotificationFor(kFooNotificationTimeout)) {
- ProcessNotification();
- }
- }
+ void MaybeProcessNotification() {
+ if (TryGetNotificationFor(kFooNotificationTimeout)) {
+ ProcessNotification();
+ }
+ }
This code is lossless if the clock period is 1kHz and it's correct using a
division which rounds up when the clock period is 128Hz.
.. Note::
- When using ``pw::chrono::SystemClock::duration`` for timeouts we recommend
- using its ``SystemClock::for_at_least()`` to round up timeouts in a more
- explicit, self documenting manner which uses ``std::chrono::ceil`` internally.
+ When using ``pw::chrono::SystemClock::duration`` for timeouts we recommend
+ using its ``SystemClock::for_at_least()`` to round up timeouts in a more
+ explicit, self documenting manner which uses ``std::chrono::ceil`` internally.
Use of ``count()`` and ``time_since_epoch()``
=============================================
@@ -317,10 +317,10 @@ This same risk exists if a continuously running hardware timer is used for a
software timer service.
.. Note::
- When calculating deadlines based on a timeout when using
- ``pw::chrono::SystemClock::timeout``, we recommend using its
- ``SystemClock::TimePointAfterAtLeast()`` which adds an extra tick for you
- internally.
+ When calculating deadlines based on a timeout when using
+ ``pw::chrono::SystemClock::timeout``, we recommend using its
+ ``SystemClock::TimePointAfterAtLeast()`` which adds an extra tick for you
+ internally.
------
Clocks
@@ -425,91 +425,44 @@ scheduler in Pigweed including pw_sync, pw_thread, etc.
C++
---
-
-.. cpp:class:: pw::chrono::SystemClock
-
- .. cpp:type:: rep = int64_t;
-
- .. cpp:type:: period = std::ratio<PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_NUMBERATOR, PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_DENOMINATOR>;
-
- The period is specified by the backend.
-
- .. cpp:type:: duration = std::chrono::duration<rep, period>;
-
- .. cpp:type:: time_point = std::chrono::time_point<SystemClock>;
-
- .. cpp:member:: static constexpr Epoch epoch = backend::kSystemClockEpoch;
-
- The epoch must be provided by the backend.
-
- .. cpp:member:: static constexpr bool is_monotonic = true;
-
- The time points of this clock cannot decrease.
-
- .. cpp:member:: static constexpr bool is_steady = false;
-
- However, the time between ticks of this clock may slightly vary due to sleep
- modes. The duration during sleep may be ignored or backfilled with another
- clock.
-
- .. cpp:member:: static constexpr bool is_free_running = backend::kSystemClockFreeRunning;
-
- The now() function may not move forward while in a critical section or
- interrupt. This must be provided by the backend.
-
- .. cpp:member:: static constexpr bool is_stopped_in_halting_debug_mode = true;
-
- The clock must stop while in halting debug mode.
-
- .. cpp:member:: static constexpr bool is_always_enabled = true;
-
- The now() function can be invoked at any time.
-
- .. cpp:member:: static constexpr bool is_nmi_safe = backend::kSystemClockNmiSafe;
-
- The now() function may work in non-masking interrupts, depending on the
- backend. This must be provided by the backend.
-
- .. cpp:function:: static time_point now() noexcept;
-
- This is thread and IRQ safe.
-
- .. cpp:function:: template <class Rep, class Period> static constexpr duration for_at_least(std::chrono::duration<Rep, Period> d);
-
- This is purely a helper, identical to directly using std::chrono::ceil, to
- convert a duration type which cannot be implicitly converted where the
- result is rounded up.
-
- .. cpp:function:: static time_point TimePointAfterAtLeast(duration after_at_least);
-
- Computes the nearest time_point after the specified duration has elapsed.
-
- This is useful for translating delay or timeout durations into deadlines.
-
- The time_point is computed based on now() plus the specified duration
- where a singular clock tick is added to handle partial ticks. This ensures
- that a duration of at least 1 tick does not result in [0,1] ticks and
- instead in [1,2] ticks.
-
+.. doxygenstruct:: pw::chrono::SystemClock
+ :members:
Example in C++
--------------
-
.. code-block:: cpp
- #include <chrono>
-
- #include "pw_chrono/system_clock.h"
-
- void Foo() {
- const SystemClock::time_point before = SystemClock::now();
- TakesALongTime();
- const SystemClock::duration time_taken = SystemClock::now() - before;
- bool took_way_too_long = false;
- if (time_taken > std::chrono::seconds(42)) {
- took_way_too_long = true;
- }
- }
+ #include <chrono>
+
+ #include "pw_chrono/system_clock.h"
+
+ void Foo() {
+ const SystemClock::time_point before = SystemClock::now();
+ TakesALongTime();
+ const SystemClock::duration time_taken = SystemClock::now() - before;
+ bool took_way_too_long = false;
+ if (time_taken > std::chrono::seconds(42)) {
+ took_way_too_long = true;
+ }
+ }
+
+Protobuf
+========
+Sometimes it's desirable to communicate high resolution time points and
+durations from one device to another. For this, ``pw_chrono`` provides protobuf
+representations of clock parameters (``pw.chrono.ClockParameters``) and time
+points (``pw.chrono.TimePoint``). These types are less succinct than simple
+single-purpose fields like ``ms_since_boot`` or ``unix_timestamp``, but allow
+timestamps to be communicated in terms of the tick rate of a device, potentially
+providing significantly higher resolution. Logging, tracing, and system state
+snapshots are use cases that benefit from this additional resolution.
+
+This module provides an overlay proto (``pw.chrono.SnapshotTimestamps``) for
+usage with ``pw_snapshot`` to encourage capture of high resolution timestamps
+in device snapshots. Simplified capture utilies and host-side tooling to
+interpret this data are not yet provided by ``pw_chrono``.
+
+There is tooling that take these proto and make them more human readable.
---------------
Software Timers
@@ -535,116 +488,66 @@ The ExpiryCallback is either invoked from a high priority thread or an
interrupt. Ergo ExpiryCallbacks should be treated as if they are executed by an
interrupt, meaning:
- * Processing inside of the callback should be kept to a minimum.
+* Processing inside of the callback should be kept to a minimum.
- * Callbacks should never attempt to block.
+* Callbacks should never attempt to block.
- * APIs which are not interrupt safe such as pw::sync::Mutex should not be used!
+* APIs which are not interrupt safe such as pw::sync::Mutex should not be used!
C++
---
-.. cpp:class:: pw::chrono::SystemTimer
-
- .. cpp:function:: SystemTimer(ExpiryCallback callback)
-
- Constructs the SystemTimer based on the user provided
- ``pw::Function<void(SystemClock::time_point expired_deadline)>``. Note that
- The ExpiryCallback is either invoked from a high priority thread or an
- interrupt.
-
- .. note::
- For a given timer instance, its ExpiryCallback will not preempt itself.
- This makes it appear like there is a single executor of a timer instance's
- ExpiryCallback.
-
- .. cpp:function:: ~SystemTimer()
-
- Cancels the timer and blocks if necssary if the callback is already being
- processed.
-
- **Postcondition:** The expiry callback is not in progress and will not be
- called in the future.
-
- .. cpp:function:: void InvokeAfter(chrono::SystemClock::duration delay)
-
- Invokes the expiry callback as soon as possible after at least the
- specified duration.
-
- Scheduling a callback cancels the existing callback (if pending).
- If the callback is already being executed while you reschedule it, it will
- finish callback execution to completion. You are responsible for any
- critical section locks which may be needed for timer coordination.
-
- This is thread safe, it may not be IRQ safe.
-
- .. cpp:function:: void InvokeAt(chrono::SystemClock::time_point timestamp)
-
- Invokes the expiry callback as soon as possible starting at the specified
- time_point.
-
- Scheduling a callback cancels the existing callback (if pending).
- If the callback is already being executed while you reschedule it, it will
- finish callback execution to completion. You are responsible for any
- critical section locks which may be needed for timer coordination.
-
- This is thread safe, it may not be IRQ safe.
-
- .. cpp:function:: void Cancel()
-
- Cancels the software timer expiry callback if pending.
-
- Canceling a timer which isn't scheduled does nothing.
-
- If the callback is already being executed while you cancel it, it will
- finish callback execution to completion. You are responsible for any
- synchronization which is needed for thread safety.
-
- This is thread safe, it may not be IRQ safe.
-
- .. list-table::
-
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``SystemTimer::SystemTimer``
- - ✔
- -
- -
- * - ``SystemTimer::~SystemTimer``
- - ✔
- -
- -
- * - ``void SystemTimer::InvokeAfter``
- - ✔
- -
- -
- * - ``void SystemTimer::InvokeAt``
- - ✔
- -
- -
- * - ``void SystemTimer::Cancel``
- - ✔
- -
- -
+.. doxygenclass:: pw::chrono::SystemTimer
+ :members:
+
+.. cpp:namespace-push:: pw::chrono::SystemTimer
+
+.. list-table::
+ :widths: 70 10 10 10
+ :header-rows: 1
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:func:`pw::chrono::SystemTimer::SystemTimer`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::chrono::SystemTimer::~SystemTimer`
+ - ✔
+ -
+ -
+ * - :cpp:func:`InvokeAfter`
+ - ✔
+ -
+ -
+ * - :cpp:func:`InvokeAt`
+ - ✔
+ -
+ -
+ * - :cpp:func:`Cancel`
+ - ✔
+ -
+ -
+
+.. cpp:namespace-pop::
Example in C++
--------------
-
.. code-block:: cpp
- #include "pw_chrono/system_clock.h"
- #include "pw_chrono/system_timer.h"
- #include "pw_log/log.h"
+ #include "pw_chrono/system_clock.h"
+ #include "pw_chrono/system_timer.h"
+ #include "pw_log/log.h"
- using namespace std::chrono_literals;
+ using namespace std::chrono_literals;
- void DoFoo(pw::chrono::SystemClock::time_point expired_deadline) {
- PW_LOG_INFO("Timer callback invoked!");
- }
+ void DoFoo(pw::chrono::SystemClock::time_point expired_deadline) {
+ PW_LOG_INFO("Timer callback invoked!");
+ }
- pw::chrono::SystemTimer foo_timer(DoFoo);
+ pw::chrono::SystemTimer foo_timer(DoFoo);
- void DoFooLater() {
- foo_timer.InvokeAfter(42ms); // DoFoo will be invoked after 42ms.
- }
+ void DoFooLater() {
+ foo_timer.InvokeAfter(42ms); // DoFoo will be invoked after 42ms.
+ }
diff --git a/pw_chrono/public/pw_chrono/internal/system_clock_macros.h b/pw_chrono/public/pw_chrono/internal/system_clock_macros.h
index 623ec9b9e..c905dd7b3 100644
--- a/pw_chrono/public/pw_chrono/internal/system_clock_macros.h
+++ b/pw_chrono/public/pw_chrono/internal/system_clock_macros.h
@@ -13,8 +13,15 @@
// the License.
#pragma once
+// The C implementation of this macro requires a C99 compound literal. In C++,
+// avoid the compound literal in case -Wc99-extensions is enabled.
+#ifdef __cplusplus
+#define _PW_SYSTEM_CLOCK_DURATION(num_ticks) \
+ (pw_chrono_SystemClock_Duration{.ticks = (num_ticks)})
+#else
#define _PW_SYSTEM_CLOCK_DURATION(num_ticks) \
((pw_chrono_SystemClock_Duration){.ticks = (num_ticks)})
+#endif // __cplusplus
// clang-format off
diff --git a/pw_chrono/public/pw_chrono/simulated_system_clock.h b/pw_chrono/public/pw_chrono/simulated_system_clock.h
index 8fefe4737..cd436717e 100644
--- a/pw_chrono/public/pw_chrono/simulated_system_clock.h
+++ b/pw_chrono/public/pw_chrono/simulated_system_clock.h
@@ -58,7 +58,7 @@ class SimulatedSystemClock : public VirtualSystemClock {
SystemClock::time_point now() override {
std::lock_guard lock(interrupt_spin_lock_);
return current_timestamp_;
- };
+ }
private:
// In theory atomics could be used if 64bit atomics are supported, however
diff --git a/pw_chrono/public/pw_chrono/system_clock.h b/pw_chrono/public/pw_chrono/system_clock.h
index ad6e90eb7..72035fb56 100644
--- a/pw_chrono/public/pw_chrono/system_clock.h
+++ b/pw_chrono/public/pw_chrono/system_clock.h
@@ -36,130 +36,139 @@
namespace pw::chrono {
namespace backend {
-// The ARM AEBI does not permit the opaque 'time_point' to be passed via
-// registers, ergo the underlying fundamental type is forward declared.
-// A SystemCLock tick has the units of one SystemClock::period duration.
-// This must be thread and IRQ safe and provided by the backend.
+/// The ARM AEBI does not permit the opaque 'time_point' to be passed via
+/// registers, ergo the underlying fundamental type is forward declared.
+/// A SystemCLock tick has the units of one SystemClock::period duration.
+/// This must be thread and IRQ safe and provided by the backend.
+///
int64_t GetSystemClockTickCount();
} // namespace backend
-// The SystemClock represents an unsteady, monotonic clock.
-//
-// The epoch of this clock is unspecified and may not be related to wall time
-// (for example, it can be time since boot). The time between ticks of this
-// clock may vary due to sleep modes and potential interrupt handling.
-// SystemClock meets the requirements of C++'s TrivialClock and Pigweed's
-// PigweedClock.
-//
-// SystemClock is compatible with C++'s Clock & TrivialClock including:
-// SystemClock::rep
-// SystemClock::period
-// SystemClock::duration
-// SystemClock::time_point
-// SystemClock::is_steady
-// SystemClock::now()
-//
-// Example:
-//
-// SystemClock::time_point before = SystemClock::now();
-// TakesALongTime();
-// SystemClock::duration time_taken = SystemClock::now() - before;
-// bool took_way_too_long = false;
-// if (time_taken > std::chrono::seconds(42)) {
-// took_way_too_long = true;
-// }
-//
-// This code is thread & IRQ safe, it may be NMI safe depending on is_nmi_safe.
+/// The `SystemClock` represents an unsteady, monotonic clock.
+///
+/// The epoch of this clock is unspecified and may not be related to wall time
+/// (for example, it can be time since boot). The time between ticks of this
+/// clock may vary due to sleep modes and potential interrupt handling.
+/// `SystemClock` meets the requirements of C++'s `TrivialClock` and Pigweed's
+/// `PigweedClock.`
+///
+/// `SystemClock` is compatible with C++'s `Clock` & `TrivialClock` including:
+/// - `SystemClock::rep`
+/// - `SystemClock::period`
+/// - `SystemClock::duration`
+/// - `SystemClock::time_point`
+/// - `SystemClock::is_steady`
+/// - `SystemClock::now()`
+///
+/// Example:
+///
+/// @code
+/// SystemClock::time_point before = SystemClock::now();
+/// TakesALongTime();
+/// SystemClock::duration time_taken = SystemClock::now() - before;
+/// bool took_way_too_long = false;
+/// if (time_taken > std::chrono::seconds(42)) {
+/// took_way_too_long = true;
+/// }
+/// @endcode
+///
+/// This code is thread & IRQ safe, it may be NMI safe depending on is_nmi_safe.
+///
struct SystemClock {
using rep = int64_t;
- // The period must be provided by the backend.
+ /// The period must be provided by the backend.
using period = std::ratio<PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_NUMERATOR,
PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_DENOMINATOR>;
+ /// Alias for durations representable with this clock.
using duration = std::chrono::duration<rep, period>;
using time_point = std::chrono::time_point<SystemClock>;
- // The epoch must be provided by the backend.
+ /// The epoch must be provided by the backend.
static constexpr Epoch epoch = backend::kSystemClockEpoch;
- // The time points of this clock cannot decrease, however the time between
- // ticks of this clock may slightly vary due to sleep modes. The duration
- // during sleep may be ignored or backfilled with another clock.
+ /// The time points of this clock cannot decrease, however the time between
+ /// ticks of this clock may slightly vary due to sleep modes. The duration
+ /// during sleep may be ignored or backfilled with another clock.
static constexpr bool is_monotonic = true;
static constexpr bool is_steady = false;
- // The now() function may not move forward while in a critical section or
- // interrupt. This must be provided by the backend.
+ /// The now() function may not move forward while in a critical section or
+ /// interrupt. This must be provided by the backend.
static constexpr bool is_free_running = backend::kSystemClockFreeRunning;
- // The clock must stop while in halting debug mode.
+ /// The clock must stop while in halting debug mode.
static constexpr bool is_stopped_in_halting_debug_mode = true;
- // The now() function can be invoked at any time.
+ /// The now() function can be invoked at any time.
static constexpr bool is_always_enabled = true;
- // The now() function may work in non-masking interrupts, depending on the
- // backend. This must be provided by the backend.
+ /// The now() function may work in non-masking interrupts, depending on the
+ /// backend. This must be provided by the backend.
static constexpr bool is_nmi_safe = backend::kSystemClockNmiSafe;
- // This is thread and IRQ safe. This must be provided by the backend.
+ /// This is thread and IRQ safe. This must be provided by the backend.
static time_point now() noexcept {
return time_point(duration(backend::GetSystemClockTickCount()));
}
- // This is purely a helper, identical to directly using std::chrono::ceil, to
- // convert a duration type which cannot be implicitly converted where the
- // result is rounded up.
+ /// This is purely a helper, identical to directly using std::chrono::ceil, to
+ /// convert a duration type which cannot be implicitly converted where the
+ /// result is rounded up.
template <class Rep, class Period>
static constexpr duration for_at_least(std::chrono::duration<Rep, Period> d) {
return std::chrono::ceil<duration>(d);
- };
-
- // Computes the nearest time_point after the specified duration has elapsed.
- //
- // This is useful for translating delay or timeout durations into deadlines.
- //
- // The time_point is computed based on now() plus the specified duration
- // where a singular clock tick is added to handle partial ticks. This ensures
- // that a duration of at least 1 tick does not result in [0,1] ticks and
- // instead in [1,2] ticks.
+ }
+
+ /// Computes the nearest time_point after the specified duration has elapsed.
+ ///
+ /// This is useful for translating delay or timeout durations into deadlines.
+ ///
+ /// The time_point is computed based on now() plus the specified duration
+ /// where a singular clock tick is added to handle partial ticks. This ensures
+ /// that a duration of at least 1 tick does not result in [0,1] ticks and
+ /// instead in [1,2] ticks.
static time_point TimePointAfterAtLeast(duration after_at_least) {
return now() + after_at_least + duration(1);
}
};
-// An abstract interface representing a SystemClock.
-//
-// This interface allows decoupling code that uses time from the code that
-// creates a point in time. You can use this to your advantage by injecting
-// Clocks into interfaces rather than having implementations call
-// SystemClock::now() directly. However, this comes at a cost of a vtable per
-// implementation and more importantly passing and maintaining references to the
-// VirtualSystemCLock for all of the users.
-//
-// The VirtualSystemClock::RealClock() function returns a reference to the
-// real global SystemClock.
-//
-// Example:
-//
-// void DoFoo(VirtualSystemClock& system_clock) {
-// SystemClock::time_point now = clock.now();
-// // ... Code which consumes now.
-// }
-//
-// // Production code:
-// DoFoo(VirtualSystemCLock::RealClock);
-//
-// // Test code:
-// MockClock test_clock();
-// DoFoo(test_clock);
-//
-// This interface is thread and IRQ safe.
+/// An abstract interface representing a SystemClock.
+///
+/// This interface allows decoupling code that uses time from the code that
+/// creates a point in time. You can use this to your advantage by injecting
+/// Clocks into interfaces rather than having implementations call
+/// `SystemClock::now()` directly. However, this comes at a cost of a vtable per
+/// implementation and more importantly passing and maintaining references to
+/// the VirtualSystemCLock for all of the users.
+///
+/// The `VirtualSystemClock::RealClock()` function returns a reference to the
+/// real global SystemClock.
+///
+/// Example:
+///
+/// @code
+/// void DoFoo(VirtualSystemClock& system_clock) {
+/// SystemClock::time_point now = clock.now();
+/// // ... Code which consumes now.
+/// }
+///
+/// // Production code:
+/// DoFoo(VirtualSystemCLock::RealClock);
+///
+/// // Test code:
+/// MockClock test_clock();
+/// DoFoo(test_clock);
+/// @endcode
+///
+/// This interface is thread and IRQ safe.
class VirtualSystemClock {
public:
- // Returns a reference to the real system clock to aid instantiation.
+ /// Returns a reference to the real system clock to aid instantiation.
static VirtualSystemClock& RealClock();
virtual ~VirtualSystemClock() = default;
+
+ /// Returns the current time.
virtual SystemClock::time_point now() = 0;
};
diff --git a/pw_chrono/public/pw_chrono/system_timer.h b/pw_chrono/public/pw_chrono/system_timer.h
index f356edecc..0ec9d5e67 100644
--- a/pw_chrono/public/pw_chrono/system_timer.h
+++ b/pw_chrono/public/pw_chrono/system_timer.h
@@ -19,46 +19,50 @@
namespace pw::chrono {
-// The SystemTimer allows an ExpiryCallback be executed at a set time in the
-// future.
-//
-// The base SystemTimer only supports a one-shot style timer with a callback.
-// A periodic timer can be implemented by rescheduling the timer in the callback
-// through InvokeAt(kDesiredPeriod + expired_deadline).
-//
-// When implementing a periodic layer on top, the user should be mindful of
-// handling missed periodic callbacks. They could opt to invoke the callback
-// multiple times with the expected expired_deadline values or instead
-// saturate and invoke the callback only once with the latest expired_deadline.
-//
-// The entire API is thread safe, however it is NOT always IRQ safe.
+/// The `SystemTimer` allows an `ExpiryCallback` be executed at a set time in
+/// the future.
+///
+/// The base `SystemTimer` only supports a one-shot style timer with a callback.
+/// A periodic timer can be implemented by rescheduling the timer in the
+/// callback through `InvokeAt(kDesiredPeriod + expired_deadline)`.
+///
+/// When implementing a periodic layer on top, the user should be mindful of
+/// handling missed periodic callbacks. They could opt to invoke the callback
+/// multiple times with the expected expired_deadline values or instead saturate
+/// and invoke the callback only once with the latest expired_deadline.
+///
+/// The entire API is thread safe, however it is NOT always IRQ safe.
class SystemTimer {
public:
using native_handle_type = backend::NativeSystemTimerHandle;
- // The ExpiryCallback is either invoked from a high priority thread or an
- // interrupt.
- //
- // For a given timer instance, its ExpiryCallback will not preempt itself.
- // This makes it appear like there is a single executor of a timer instance's
- // ExpiryCallback.
- //
- // Ergo ExpiryCallbacks should be treated as if they are executed by an
- // interrupt, meaning:
- // - Processing inside of the callback should be kept to a minimum.
- // - Callbacks should never attempt to block.
- // - APIs which are not interrupt safe such as pw::sync::Mutex should not be
- // used!
+ /// The `ExpiryCallback` is either invoked from a high priority thread or an
+ /// interrupt.
+ ///
+ /// For a given timer instance, its `ExpiryCallback` will not preempt itself.
+ /// This makes it appear like there is a single executor of a timer instance's
+ /// `ExpiryCallback`.
+ ///
+ /// Ergo ExpiryCallbacks should be treated as if they are executed by an
+ /// interrupt, meaning:
+ /// - Processing inside of the callback should be kept to a minimum.
+ /// - Callbacks should never attempt to block.
+ /// - APIs which are not interrupt safe such as pw::sync::Mutex should not be
+ /// used!
using ExpiryCallback =
Function<void(SystemClock::time_point expired_deadline)>;
+ /// Constructs the SystemTimer based on the user provided
+ /// `pw::Function<void(SystemClock::time_point expired_deadline)>`. Note that
+ /// The `ExpiryCallback` is either invoked from a high priority thread or an
+ /// interrupt.
SystemTimer(ExpiryCallback&& callback);
- // Cancels the timer and blocks if necssary if the callback is already being
- // processed.
- //
- // Postcondition: The expiry callback is not in progress and will not be
- // called in the future.
+ /// Cancels the timer and blocks if necssary if the callback is already being
+ /// processed.
+ ///
+ /// @b Postcondition: The expiry callback is not in progress and will not be
+ /// called in the future.
~SystemTimer();
SystemTimer(const SystemTimer&) = delete;
@@ -66,43 +70,43 @@ class SystemTimer {
SystemTimer& operator=(const SystemTimer&) = delete;
SystemTimer& operator=(SystemTimer&&) = delete;
- // Invokes the expiry callback as soon as possible after at least the
- // specified duration.
- //
- // Scheduling a callback cancels the existing callback (if pending).
- // If the callback is already being executed while you reschedule it, it will
- // finish callback execution to completion. You are responsible for any
- // critical section locks which may be needed for timer coordination.
- //
- // This is thread safe, it may not be IRQ safe.
+ /// Invokes the expiry callback as soon as possible after at least the
+ /// specified duration.
+ ///
+ /// Scheduling a callback cancels the existing callback (if pending).
+ /// If the callback is already being executed while you reschedule it, it will
+ /// finish callback execution to completion. You are responsible for any
+ /// critical section locks which may be needed for timer coordination.
+ ///
+ /// This is thread safe, it may not be IRQ safe.
void InvokeAfter(SystemClock::duration delay);
- // Invokes the expiry callback as soon as possible starting at the specified
- // time_point.
- //
- // Scheduling a callback cancels the existing callback (if pending).
- // If the callback is already being executed while you reschedule it, it will
- // finish callback execution to completion. You are responsible for any
- // critical section locks which may be needed for timer coordination.
- //
- // This is thread safe, it may not be IRQ safe.
+ /// Invokes the expiry callback as soon as possible starting at the specified
+ /// time_point.
+ ///
+ /// Scheduling a callback cancels the existing callback (if pending).
+ /// If the callback is already being executed while you reschedule it, it will
+ /// finish callback execution to completion. You are responsible for any
+ /// critical section locks which may be needed for timer coordination.
+ ///
+ /// This is thread safe, it may not be IRQ safe.
void InvokeAt(SystemClock::time_point timestamp);
- // Cancels the software timer expiry callback if pending.
- //
- // Canceling a timer which isn't scheduled does nothing.
- //
- // If the callback is already being executed while you cancel it, it will
- // finish callback execution to completion. You are responsible for any
- // synchronization which is needed for thread safety.
- //
- // This is thread safe, it may not be IRQ safe.
+ /// Cancels the software timer expiry callback if pending.
+ ///
+ /// Canceling a timer which isn't scheduled does nothing.
+ ///
+ /// If the callback is already being executed while you cancel it, it will
+ /// finish callback execution to completion. You are responsible for any
+ /// synchronization which is needed for thread safety.
+ ///
+ /// This is thread safe, it may not be IRQ safe.
void Cancel();
native_handle_type native_handle();
private:
- // This may be a wrapper around a native type with additional members.
+ /// This may be a wrapper around a native type with additional members.
backend::NativeSystemTimer native_type_;
};
diff --git a/pw_chrono/py/BUILD.bazel b/pw_chrono/py/BUILD.bazel
new file mode 100644
index 000000000..424250125
--- /dev/null
+++ b/pw_chrono/py/BUILD.bazel
@@ -0,0 +1,37 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "pw_chrono",
+ srcs = [
+ "pw_chrono/__init__.py",
+ "pw_chrono/timestamp_analyzer.py",
+ ],
+ deps = [
+ "//pw_chrono:chrono_proto_pb2",
+ ],
+)
+
+py_test(
+ name = "timestamp_analyzer_test",
+ srcs = [
+ "timestamp_analyzer_test.py",
+ ],
+ deps = [
+ ":pw_chrono",
+ "//pw_chrono:chrono_proto_pb2",
+ ],
+)
diff --git a/pw_chrono/py/BUILD.gn b/pw_chrono/py/BUILD.gn
new file mode 100644
index 000000000..8f6b3d130
--- /dev/null
+++ b/pw_chrono/py/BUILD.gn
@@ -0,0 +1,36 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+import("$dir_pw_docgen/docs.gni")
+
+pw_python_package("py") {
+ generate_setup = {
+ metadata = {
+ name = "pw_chrono"
+ version = "0.0.1"
+ }
+ }
+
+ sources = [
+ "pw_chrono/__init__.py",
+ "pw_chrono/timestamp_analyzer.py",
+ ]
+ tests = [ "timestamp_analyzer_test.py" ]
+ python_deps = [ "..:protos.python" ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+}
diff --git a/pw_chrono/py/pw_chrono/__init__.py b/pw_chrono/py/pw_chrono/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_chrono/py/pw_chrono/__init__.py
diff --git a/pw_chrono/py/pw_chrono/py.typed b/pw_chrono/py/pw_chrono/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_chrono/py/pw_chrono/py.typed
diff --git a/pw_chrono/py/pw_chrono/timestamp_analyzer.py b/pw_chrono/py/pw_chrono/timestamp_analyzer.py
new file mode 100644
index 000000000..6869d826c
--- /dev/null
+++ b/pw_chrono/py/pw_chrono/timestamp_analyzer.py
@@ -0,0 +1,66 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Library to analyze timestamp."""
+
+from typing import List
+import datetime
+from pw_chrono_protos import chrono_pb2
+
+_UTC_EPOCH = datetime.datetime(1970, 1, 1, 00, 00, 00)
+
+_UNKNOWN = chrono_pb2.EpochType.Enum.UNKNOWN
+_TIME_SINCE_BOOT = chrono_pb2.EpochType.Enum.TIME_SINCE_BOOT
+_UTC_WALL_CLOCK = chrono_pb2.EpochType.Enum.UTC_WALL_CLOCK
+
+
+def process_snapshot(serialized_snapshot: bytes):
+ captured_timestamps = chrono_pb2.SnapshotTimestamps()
+ captured_timestamps.ParseFromString(serialized_snapshot)
+ return timestamp_output(captured_timestamps)
+
+
+def timestamp_output(timestamps: chrono_pb2.SnapshotTimestamps):
+ output: List[str] = []
+ if not timestamps.timestamps:
+ return ''
+
+ plural = '' if len(timestamps.timestamps) == 1 else 's'
+ output.append(f'Snapshot capture timestamp{plural}')
+ for timepoint in timestamps.timestamps:
+ time = timestamp_snapshot_analyzer(timepoint)
+ clock_epoch_type = timepoint.clock_parameters.epoch_type
+ if clock_epoch_type == _TIME_SINCE_BOOT:
+ output.append(f' Time since boot: {time}')
+ elif clock_epoch_type == _UTC_WALL_CLOCK:
+ utc_time = time + _UTC_EPOCH
+ output.append(f' UTC time: {utc_time}')
+ else:
+ output.append(f' Time since unknown epoch {_UNKNOWN}: unknown')
+
+ return '\n'.join(output)
+
+
+def timestamp_snapshot_analyzer(
+ captured_timepoint: chrono_pb2.TimePoint,
+) -> datetime.timedelta:
+ ticks = captured_timepoint.timestamp
+ clock_period = (
+ captured_timepoint.clock_parameters.tick_period_seconds_numerator
+ / captured_timepoint.clock_parameters.tick_period_seconds_denominator
+ )
+ elapsed_seconds = ticks * clock_period
+
+ time_delta = datetime.timedelta(seconds=elapsed_seconds)
+
+ return time_delta
diff --git a/pw_chrono/py/timestamp_analyzer_test.py b/pw_chrono/py/timestamp_analyzer_test.py
new file mode 100644
index 000000000..90eda246b
--- /dev/null
+++ b/pw_chrono/py/timestamp_analyzer_test.py
@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for the timestamp analyzer."""
+
+import unittest
+from pw_chrono.timestamp_analyzer import process_snapshot
+from pw_chrono_protos import chrono_pb2
+
+
+class TimestampTest(unittest.TestCase):
+ """Test for the timestamp analyzer."""
+
+ def test_no_timepoint(self):
+ time_stamps = chrono_pb2.SnapshotTimestamps()
+ self.assertEqual('', str(process_snapshot(time_stamps)))
+
+ def test_timestamp_unknown_epoch_type(self):
+ time_stamps = chrono_pb2.SnapshotTimestamps()
+
+ time_point = chrono_pb2.TimePoint()
+ unkown = chrono_pb2.EpochType.Enum.UNKNOWN
+ time_point.clock_parameters.epoch_type = unkown
+
+ time_stamps.timestamps.append(time_point)
+
+ expected = '\n'.join(
+ (
+ 'Snapshot capture timestamp',
+ ' Time since unknown epoch 0: unknown',
+ )
+ )
+
+ self.assertEqual(expected, str(process_snapshot(time_stamps)))
+
+ def test_timestamp_with_time_since_boot(self):
+ time_stamps = chrono_pb2.SnapshotTimestamps()
+
+ time_point = chrono_pb2.TimePoint()
+ time_since_boot = chrono_pb2.EpochType.Enum.TIME_SINCE_BOOT
+ time_point.clock_parameters.epoch_type = time_since_boot
+ time_point.timestamp = 100
+ time_point.clock_parameters.tick_period_seconds_numerator = 1
+ time_point.clock_parameters.tick_period_seconds_denominator = 1000
+
+ time_stamps.timestamps.append(time_point)
+
+ expected = '\n'.join(
+ ('Snapshot capture timestamp', ' Time since boot: 2:24:00')
+ )
+
+ self.assertEqual(expected, str(process_snapshot(time_stamps)))
+
+ def test_timestamp_with_utc_wall_clock(self):
+ time_stamps = chrono_pb2.SnapshotTimestamps()
+
+ time_point = chrono_pb2.TimePoint()
+ utc_wall_clock = chrono_pb2.EpochType.Enum.UTC_WALL_CLOCK
+ time_point.clock_parameters.epoch_type = utc_wall_clock
+ time_point.timestamp = 100
+ time_point.clock_parameters.tick_period_seconds_numerator = 1
+ time_point.clock_parameters.tick_period_seconds_denominator = 1000
+
+ time_stamps.timestamps.append(time_point)
+
+ expected = '\n'.join(
+ ('Snapshot capture timestamp', ' UTC time: 1970-01-01 02:24:00')
+ )
+
+ self.assertEqual(expected, str(process_snapshot(time_stamps)))
+
+ def test_timestamp_with_time_since_boot_and_utc_wall_clock(self):
+ time_stamps = chrono_pb2.SnapshotTimestamps()
+
+ time_point = chrono_pb2.TimePoint()
+ time_since_boot = chrono_pb2.EpochType.Enum.TIME_SINCE_BOOT
+ time_point.clock_parameters.epoch_type = time_since_boot
+ time_point.timestamp = 100
+ time_point.clock_parameters.tick_period_seconds_numerator = 1
+ time_point.clock_parameters.tick_period_seconds_denominator = 1000
+ time_stamps.timestamps.append(time_point)
+
+ time_point = chrono_pb2.TimePoint()
+ utc_wall_clock = chrono_pb2.EpochType.Enum.UTC_WALL_CLOCK
+ time_point.clock_parameters.epoch_type = utc_wall_clock
+ time_point.timestamp = 100
+ time_point.clock_parameters.tick_period_seconds_numerator = 1
+ time_point.clock_parameters.tick_period_seconds_denominator = 1000
+ time_stamps.timestamps.append(time_point)
+
+ expected = '\n'.join(
+ (
+ 'Snapshot capture timestamps',
+ ' Time since boot: 2:24:00',
+ ' UTC time: 1970-01-01 02:24:00',
+ )
+ )
+
+ self.assertEqual(expected, str(process_snapshot(time_stamps)))
diff --git a/pw_chrono/system_clock_facade_test.cc b/pw_chrono/system_clock_facade_test.cc
index af17a6549..803976042 100644
--- a/pw_chrono/system_clock_facade_test.cc
+++ b/pw_chrono/system_clock_facade_test.cc
@@ -34,6 +34,19 @@ pw_chrono_SystemClock_Duration pw_chrono_SystemClock_CallTimeElapsed(
pw_chrono_SystemClock_Nanoseconds pw_chrono_SystemClock_CallDurationToNsFloor(
pw_chrono_SystemClock_Duration ticks);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_100ms(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_10s(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_1min(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_2h(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_100msCeil(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_10sCeil(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_1minCeil(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_2hCeil(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_100msFloor(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_10sFloor(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_1minFloor(void);
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_2hFloor(void);
+
} // extern "C"
// While testing that the clock ticks (i.e. moves forward) we want to ensure a
@@ -110,5 +123,33 @@ TEST(SystemClock, DurationCastInC) {
kRoundedArbitraryDurationInC));
}
+// Though the macros are intended for C use, test them in this file in C++.
+TEST(SystemClock, DurationMacros) {
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_100ms().ticks,
+ PW_SYSTEM_CLOCK_MS(100).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_10s().ticks,
+ PW_SYSTEM_CLOCK_S(10).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_1min().ticks,
+ PW_SYSTEM_CLOCK_MIN(1).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_2h().ticks,
+ PW_SYSTEM_CLOCK_H(2).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_100msCeil().ticks,
+ PW_SYSTEM_CLOCK_MS_CEIL(100).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_10sCeil().ticks,
+ PW_SYSTEM_CLOCK_S_CEIL(10).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_1minCeil().ticks,
+ PW_SYSTEM_CLOCK_MIN_CEIL(1).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_2hCeil().ticks,
+ PW_SYSTEM_CLOCK_H_CEIL(2).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_100msFloor().ticks,
+ PW_SYSTEM_CLOCK_MS_FLOOR(100).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_10sFloor().ticks,
+ PW_SYSTEM_CLOCK_S_FLOOR(10).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_1minFloor().ticks,
+ PW_SYSTEM_CLOCK_MIN_FLOOR(1).ticks);
+ EXPECT_EQ(pw_chrono_SystemClock_Macros_2hFloor().ticks,
+ PW_SYSTEM_CLOCK_H_FLOOR(2).ticks);
+}
+
} // namespace
} // namespace pw::chrono
diff --git a/pw_chrono/system_clock_facade_test_c.c b/pw_chrono/system_clock_facade_test_c.c
index d8b21b699..6b5e0c403 100644
--- a/pw_chrono/system_clock_facade_test_c.c
+++ b/pw_chrono/system_clock_facade_test_c.c
@@ -31,3 +31,51 @@ pw_chrono_SystemClock_Nanoseconds pw_chrono_SystemClock_CallDurationToNsFloor(
pw_chrono_SystemClock_Duration ticks) {
return pw_chrono_SystemClock_DurationToNsFloor(ticks);
}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_100ms(void) {
+ return PW_SYSTEM_CLOCK_MS(100);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_10s(void) {
+ return PW_SYSTEM_CLOCK_S(10);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_1min(void) {
+ return PW_SYSTEM_CLOCK_MIN(1);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_2h(void) {
+ return PW_SYSTEM_CLOCK_H(2);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_100msCeil(void) {
+ return PW_SYSTEM_CLOCK_MS_CEIL(100);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_10sCeil(void) {
+ return PW_SYSTEM_CLOCK_S_CEIL(10);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_1minCeil(void) {
+ return PW_SYSTEM_CLOCK_MIN_CEIL(1);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_2hCeil(void) {
+ return PW_SYSTEM_CLOCK_H_CEIL(2);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_100msFloor(void) {
+ return PW_SYSTEM_CLOCK_MS_FLOOR(100);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_10sFloor(void) {
+ return PW_SYSTEM_CLOCK_S_FLOOR(10);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_1minFloor(void) {
+ return PW_SYSTEM_CLOCK_MIN_FLOOR(1);
+}
+
+pw_chrono_SystemClock_Duration pw_chrono_SystemClock_Macros_2hFloor(void) {
+ return PW_SYSTEM_CLOCK_H_FLOOR(2);
+}
diff --git a/pw_chrono/system_timer_facade_test.cc b/pw_chrono/system_timer_facade_test.cc
index d819e7089..5a9b50de5 100644
--- a/pw_chrono/system_timer_facade_test.cc
+++ b/pw_chrono/system_timer_facade_test.cc
@@ -84,7 +84,7 @@ TEST(SystemTimer, StaticInvokeAt) {
EXPECT_GE(SystemClock::now(), expired_deadline);
EXPECT_EQ(expired_deadline, expected_deadline);
callback_ran_notification.release();
- };
+ }
SystemClock::time_point expected_deadline;
sync::ThreadNotification callback_ran_notification;
@@ -108,7 +108,7 @@ TEST(SystemTimer, InvokeAt) {
EXPECT_GE(SystemClock::now(), expired_deadline);
EXPECT_EQ(expired_deadline, expected_deadline);
callback_ran_notification.release();
- };
+ }
SystemClock::time_point expected_deadline;
sync::ThreadNotification callback_ran_notification;
@@ -137,7 +137,7 @@ TEST(SystemTimer, InvokeAfter) {
EXPECT_GE(SystemClock::now(), expired_deadline);
EXPECT_GE(expired_deadline, expected_min_deadline);
callback_ran_notification.release();
- };
+ }
SystemClock::time_point expected_min_deadline;
sync::ThreadNotification callback_ran_notification;
@@ -167,7 +167,7 @@ TEST(SystemTimer, CancelFromCallback) {
void OnExpiryCallback(SystemClock::time_point) override {
timer().Cancel();
callback_ran_notification.release();
- };
+ }
sync::ThreadNotification callback_ran_notification;
};
@@ -184,7 +184,7 @@ TEST(SystemTimer, RescheduleAndCancelFromCallback) {
timer().InvokeAfter(kRoundedArbitraryShortDuration);
timer().Cancel();
callback_ran_notification.release();
- };
+ }
sync::ThreadNotification callback_ran_notification;
};
@@ -209,7 +209,7 @@ TEST(SystemTimer, RescheduleFromCallback) {
} else {
callbacks_done_notification.release();
}
- };
+ }
const uint8_t kRequiredInvocations = 5;
const SystemClock::duration kPeriod = kRoundedArbitraryShortDuration;
@@ -240,7 +240,7 @@ TEST(SystemTimer, DoubleRescheduleFromCallback) {
} else {
callbacks_done_notification.release();
}
- };
+ }
const uint8_t kExpectedInvocations = 2;
const SystemClock::duration kPeriod = kRoundedArbitraryShortDuration;
diff --git a/pw_chrono_embos/BUILD.bazel b/pw_chrono_embos/BUILD.bazel
index b86d63d76..7d176b21b 100644
--- a/pw_chrono_embos/BUILD.bazel
+++ b/pw_chrono_embos/BUILD.bazel
@@ -71,7 +71,7 @@ pw_cc_library(
"//pw_chrono:system_clock",
"//pw_function",
"//pw_chrono:system_timer_facade",
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
],
)
@@ -87,7 +87,7 @@ pw_cc_library(
":system_clock",
"//pw_assert",
"//pw_interrupt:context",
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
],
)
diff --git a/pw_chrono_embos/BUILD.gn b/pw_chrono_embos/BUILD.gn
index b4ad97c2b..5b7f36634 100644
--- a/pw_chrono_embos/BUILD.gn
+++ b/pw_chrono_embos/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# The build target that overrides the default configuration options for this
@@ -98,3 +99,6 @@ pw_source_set("system_timer") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_chrono_freertos/BUILD.bazel b/pw_chrono_freertos/BUILD.bazel
index 1fafeda1e..60c9eee0c 100644
--- a/pw_chrono_freertos/BUILD.bazel
+++ b/pw_chrono_freertos/BUILD.bazel
@@ -38,6 +38,7 @@ pw_cc_library(
],
deps = [
"//pw_chrono:epoch",
+ "@freertos",
],
)
@@ -52,8 +53,9 @@ pw_cc_library(
deps = [
":system_clock_headers",
"//pw_chrono:system_clock_facade",
- # TODO: This should depend on FreeRTOS but our third parties currently
- # do not have Bazel support.
+ "//pw_interrupt:context",
+ "//pw_sync:interrupt_spin_lock",
+ "@freertos",
],
)
@@ -73,7 +75,7 @@ pw_cc_library(
"//pw_chrono:system_clock",
"//pw_function",
"//pw_chrono:system_timer_facade",
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
+ # TODO(b/234876414): This should depend on FreeRTOS but our third parties
# currently do not have Bazel support.
],
)
@@ -88,7 +90,7 @@ pw_cc_library(
"//pw_chrono:system_timer_facade",
":system_clock",
"//pw_assert",
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
+ # TODO(b/234876414): This should depend on FreeRTOS but our third parties
# currently do not have Bazel support.
],
)
diff --git a/pw_chrono_freertos/BUILD.gn b/pw_chrono_freertos/BUILD.gn
index afe8afe9c..4174d4797 100644
--- a/pw_chrono_freertos/BUILD.gn
+++ b/pw_chrono_freertos/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# The build target that overrides the default configuration options for this
@@ -97,3 +98,6 @@ pw_source_set("system_timer") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_chrono_freertos/CMakeLists.txt b/pw_chrono_freertos/CMakeLists.txt
index 391f2e3e2..d243e5ce3 100644
--- a/pw_chrono_freertos/CMakeLists.txt
+++ b/pw_chrono_freertos/CMakeLists.txt
@@ -16,7 +16,7 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_module_config(pw_chrono_freertos_CONFIG)
-pw_add_module_library(pw_chrono_freertos.config
+pw_add_library(pw_chrono_freertos.config INTERFACE
HEADERS
public/pw_chrono_freertos/config.h
PUBLIC_INCLUDES
@@ -27,9 +27,7 @@ pw_add_module_library(pw_chrono_freertos.config
)
# This target provides the backend for pw::chrono::SystemClock.
-pw_add_module_library(pw_chrono_freertos.system_clock
- IMPLEMENTS_FACADES
- pw_chrono.system_clock
+pw_add_library(pw_chrono_freertos.system_clock STATIC
HEADERS
public/pw_chrono_freertos/system_clock_config.h
public/pw_chrono_freertos/system_clock_constants.h
@@ -38,8 +36,9 @@ pw_add_module_library(pw_chrono_freertos.system_clock
public
public_overrides
PUBLIC_DEPS
- pw_chrono_freertos.config
pw_chrono.epoch
+ pw_chrono.system_clock.facade
+ pw_chrono_freertos.config
pw_third_party.freertos
SOURCES
system_clock.cc
@@ -49,9 +48,7 @@ pw_add_module_library(pw_chrono_freertos.system_clock
)
# This target provides the backend for pw::chrono::SystemTimer.
-pw_add_module_library(pw_chrono_freertos.system_timer
- IMPLEMENTS_FACADES
- pw_chrono.system_timer
+pw_add_library(pw_chrono_freertos.system_timer STATIC
HEADERS
public/pw_chrono_freertos/system_timer_inline.h
public/pw_chrono_freertos/system_timer_native.h
@@ -62,6 +59,7 @@ pw_add_module_library(pw_chrono_freertos.system_timer
public_overrides
PUBLIC_DEPS
pw_chrono.system_clock
+ pw_chrono.system_timer.facade
pw_third_party.freertos
SOURCES
system_timer.cc
diff --git a/pw_chrono_stl/BUILD.gn b/pw_chrono_stl/BUILD.gn
index 9ecbd85c1..5acf4467e 100644
--- a/pw_chrono_stl/BUILD.gn
+++ b/pw_chrono_stl/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
include_dirs = [ "public" ]
@@ -69,3 +70,6 @@ pw_source_set("system_timer") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_chrono_stl/CMakeLists.txt b/pw_chrono_stl/CMakeLists.txt
index 1e839336f..0f1ed5626 100644
--- a/pw_chrono_stl/CMakeLists.txt
+++ b/pw_chrono_stl/CMakeLists.txt
@@ -15,9 +15,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
# This target provides the backend for pw::chrono::SystemClock.
-pw_add_module_library(pw_chrono_stl.system_clock
- IMPLEMENTS_FACADES
- pw_chrono.system_clock
+pw_add_library(pw_chrono_stl.system_clock INTERFACE
HEADERS
public/pw_chrono_stl/system_clock_config.h
public/pw_chrono_stl/system_clock_inline.h
@@ -28,12 +26,11 @@ pw_add_module_library(pw_chrono_stl.system_clock
public_overrides
PUBLIC_DEPS
pw_chrono.epoch
+ pw_chrono.system_clock.facade
)
# This target provides the backend for pw::chrono::SystemTimer.
-pw_add_module_library(pw_chrono_stl.system_timer
- IMPLEMENTS_FACADES
- pw_chrono.system_timer
+pw_add_library(pw_chrono_stl.system_timer STATIC
HEADERS
public/pw_chrono_stl/system_timer_inline.h
public/pw_chrono_stl/system_timer_native.h
@@ -44,6 +41,7 @@ pw_add_module_library(pw_chrono_stl.system_timer
public_overrides
PUBLIC_DEPS
pw_chrono.system_clock
+ pw_chrono.system_timer.facade
pw_function
SOURCES
system_timer.cc
diff --git a/pw_chrono_stl/public/pw_chrono_stl/system_clock_config.h b/pw_chrono_stl/public/pw_chrono_stl/system_clock_config.h
index 9766fa244..88ff7dbf6 100644
--- a/pw_chrono_stl/public/pw_chrono_stl/system_clock_config.h
+++ b/pw_chrono_stl/public/pw_chrono_stl/system_clock_config.h
@@ -18,7 +18,7 @@
// compatibility and we rely on implicit conversion to tell us at compile time
// whether this is incompatible.
#define PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_NUMERATOR 1
-#define PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_DENOMINATOR 100'000'0000
+#define PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_DENOMINATOR 1000000000
#ifdef __cplusplus
diff --git a/pw_chrono_threadx/BUILD.bazel b/pw_chrono_threadx/BUILD.bazel
index 8a5dafe90..94b0e6ac5 100644
--- a/pw_chrono_threadx/BUILD.bazel
+++ b/pw_chrono_threadx/BUILD.bazel
@@ -52,7 +52,7 @@ pw_cc_library(
deps = [
":system_clock_headers",
"//pw_chrono:system_clock_facade",
- # TODO: This should depend on ThreadX but our third parties currently
- # do not have Bazel support.
+ # TODO(ewout): This should depend on ThreadX but our third parties
+ # currently do not have Bazel support.
],
)
diff --git a/pw_chrono_threadx/BUILD.gn b/pw_chrono_threadx/BUILD.gn
index 3cd04348c..4bc4fc48c 100644
--- a/pw_chrono_threadx/BUILD.gn
+++ b/pw_chrono_threadx/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# The build target that overrides the default configuration options for this
@@ -71,3 +72,6 @@ pw_source_set("system_clock") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_chrono_zephyr/BUILD.gn b/pw_chrono_zephyr/BUILD.gn
index 9a6699a8e..1b11f77f5 100644
--- a/pw_chrono_zephyr/BUILD.gn
+++ b/pw_chrono_zephyr/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_chrono_zephyr/CMakeLists.txt b/pw_chrono_zephyr/CMakeLists.txt
index ad979c9eb..bea4f6de2 100644
--- a/pw_chrono_zephyr/CMakeLists.txt
+++ b/pw_chrono_zephyr/CMakeLists.txt
@@ -14,14 +14,23 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK)
- pw_add_module_library(pw_chrono_zephyr.system_clock
- IMPLEMENTS_FACADES
- pw_chrono.system_clock
- PUBLIC_DEPS
- pw_function
- )
- pw_set_backend(pw_chrono.system_clock pw_chrono_zephyr.system_clock)
+pw_add_library(pw_chrono_zephyr.system_clock INTERFACE
+ HEADERS
+ public/pw_chrono_zephyr/system_clock_constants.h
+ public/pw_chrono_zephyr/system_clock_config.h
+ public/pw_chrono_zephyr/system_clock_inline.h
+ public_overrides/pw_chrono_backend/system_clock_config.h
+ public_overrides/pw_chrono_backend/system_clock_inline.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_function
+ pw_chrono.epoch
+ pw_chrono.system_clock.facade
+)
+
+if(CONFIG_PIGWEED_CHRONO_SYSTEM_CLOCK)
zephyr_link_interface(pw_chrono_zephyr.system_clock)
zephyr_link_libraries(pw_chrono_zephyr.system_clock)
-endif() \ No newline at end of file
+endif()
diff --git a/pw_chrono_zephyr/Kconfig b/pw_chrono_zephyr/Kconfig
index c6eacaca5..4996cb02b 100644
--- a/pw_chrono_zephyr/Kconfig
+++ b/pw_chrono_zephyr/Kconfig
@@ -12,14 +12,7 @@
# License for the specific language governing permissions and limitations under
# the License.
-menuconfig PIGWEED_CHRONO
- bool "Enable the Pigweed chrono facade (pw_chrono)"
- select PIGWEED_PREPROCESSOR
-
-if PIGWEED_CHRONO
-
config PIGWEED_CHRONO_SYSTEM_CLOCK
bool "Enabled the Pigweed chrono system clock library (pw_chrono.system_clock)"
+ select PIGWEED_PREPROCESSOR
select PIGWEED_FUNCTION
-
-endif # PIGWEED_CHRONO
diff --git a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_config.h b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_config.h
index 55bfda5e6..75def0147 100644
--- a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_config.h
+++ b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_config.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
// Use the Zephyr config's tick rate.
#define PW_CHRONO_SYSTEM_CLOCK_PERIOD_SECONDS_NUMERATOR 1
diff --git a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h
index 4013b5c38..33641d849 100644
--- a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h
+++ b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_constants.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
#include "pw_chrono/system_clock.h"
diff --git a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_inline.h b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_inline.h
index 9ea227811..1af2a2678 100644
--- a/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_inline.h
+++ b/pw_chrono_zephyr/public/pw_chrono_zephyr/system_clock_inline.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
#include "pw_chrono/system_clock.h"
diff --git a/pw_cli/BUILD.gn b/pw_cli/BUILD.gn
index 79f575b8f..17500d594 100644
--- a/pw_cli/BUILD.gn
+++ b/pw_cli/BUILD.gn
@@ -15,8 +15,12 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
other_deps = [ "py" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_cli/docs.rst b/pw_cli/docs.rst
index 90197643f..b6dc32e3e 100644
--- a/pw_cli/docs.rst
+++ b/pw_cli/docs.rst
@@ -13,13 +13,14 @@ the Pigweed environment, these commands will be available for use.
.. code-block:: text
- doctor Check that the environment is set up correctly for Pigweed.
- format Check and fix formatting for source files.
- help Display detailed information about pw commands.
- logdemo Show how logs look at various levels.
- module-check Check that a module matches Pigweed's module guidelines.
- test Run Pigweed unit tests built using GN.
- watch Watch files for changes and rebuild.
+ doctor Check that the environment is set up correctly for Pigweed.
+ format Check and fix formatting for source files.
+ help Display detailed information about pw commands.
+ ide Configure editors and IDEs to work best with Pigweed.
+ logdemo Show how logs look at various levels.
+ module Utilities for managing modules.
+ test Run Pigweed unit tests built using GN.
+ watch Watch files for changes and rebuild.
To see an up-to-date list of ``pw`` subcommands, run ``pw --help``.
diff --git a/pw_cli/py/BUILD.bazel b/pw_cli/py/BUILD.bazel
index eac6f4c3e..c77ce2ca2 100644
--- a/pw_cli/py/BUILD.bazel
+++ b/pw_cli/py/BUILD.bazel
@@ -34,6 +34,8 @@ py_library(
"pw_cli/process.py",
"pw_cli/pw_command_plugins.py",
"pw_cli/requires.py",
+ "pw_cli/toml_config_loader_mixin.py",
+ "pw_cli/yaml_config_loader_mixin.py",
],
imports = ["."],
)
@@ -49,10 +51,10 @@ py_binary(
)
py_test(
- name = "plugins_test",
+ name = "envparse_test",
size = "small",
srcs = [
- "plugins_test.py",
+ "envparse_test.py",
],
deps = [
":pw_cli",
@@ -60,10 +62,10 @@ py_test(
)
py_test(
- name = "envparse_test",
+ name = "plugins_test",
size = "small",
srcs = [
- "envparse_test.py",
+ "plugins_test.py",
],
deps = [
":pw_cli",
diff --git a/pw_cli/py/BUILD.gn b/pw_cli/py/BUILD.gn
index 0057e8978..4bb4cd2e3 100644
--- a/pw_cli/py/BUILD.gn
+++ b/pw_cli/py/BUILD.gn
@@ -36,10 +36,25 @@ pw_python_package("py") {
"pw_cli/process.py",
"pw_cli/pw_command_plugins.py",
"pw_cli/requires.py",
+ "pw_cli/toml_config_loader_mixin.py",
+ "pw_cli/yaml_config_loader_mixin.py",
]
tests = [
"envparse_test.py",
"plugins_test.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+}
+
+pw_python_script("process_integration_test") {
+ sources = [ "process_integration_test.py" ]
+ python_deps = [ ":py" ]
+
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+
+ action = {
+ stamp = true
+ }
}
diff --git a/pw_cli/py/envparse_test.py b/pw_cli/py/envparse_test.py
index cbeb332b6..e1e9b5497 100644
--- a/pw_cli/py/envparse_test.py
+++ b/pw_cli/py/envparse_test.py
@@ -31,6 +31,7 @@ def error(value: str):
class TestEnvironmentParser(unittest.TestCase):
"""Tests for envparse.EnvironmentParser."""
+
def setUp(self):
self.raw_env = {
'PATH': '/bin:/usr/bin:/usr/local/bin',
@@ -91,6 +92,7 @@ class TestEnvironmentParser(unittest.TestCase):
class TestEnvironmentParserWithPrefix(unittest.TestCase):
"""Tests for envparse.EnvironmentParser using a prefix."""
+
def setUp(self):
self.raw_env = {
'PW_FOO': '001',
@@ -100,8 +102,9 @@ class TestEnvironmentParserWithPrefix(unittest.TestCase):
}
def test_parse_unrecognized_variable(self):
- parser = envparse.EnvironmentParser(prefix='PW_',
- error_on_unrecognized=True)
+ parser = envparse.EnvironmentParser(
+ prefix='PW_', error_on_unrecognized=True
+ )
parser.add_var('PW_FOO')
parser.add_var('PW_BAR')
@@ -109,24 +112,27 @@ class TestEnvironmentParserWithPrefix(unittest.TestCase):
parser.parse_env(env=self.raw_env)
def test_parse_unrecognized_but_allowed_suffix(self):
- parser = envparse.EnvironmentParser(prefix='PW_',
- error_on_unrecognized=True)
+ parser = envparse.EnvironmentParser(
+ prefix='PW_', error_on_unrecognized=True
+ )
parser.add_allowed_suffix('_ALLOWED_SUFFIX')
env = parser.parse_env(env={'PW_FOO_ALLOWED_SUFFIX': '001'})
self.assertEqual(env.PW_FOO_ALLOWED_SUFFIX, '001')
def test_parse_allowed_suffix_but_not_suffix(self):
- parser = envparse.EnvironmentParser(prefix='PW_',
- error_on_unrecognized=True)
+ parser = envparse.EnvironmentParser(
+ prefix='PW_', error_on_unrecognized=True
+ )
parser.add_allowed_suffix('_ALLOWED_SUFFIX')
with self.assertRaises(ValueError):
parser.parse_env(env={'PW_FOO_ALLOWED_SUFFIX_FOO': '001'})
def test_parse_ignore_unrecognized(self):
- parser = envparse.EnvironmentParser(prefix='PW_',
- error_on_unrecognized=False)
+ parser = envparse.EnvironmentParser(
+ prefix='PW_', error_on_unrecognized=False
+ )
parser.add_var('PW_FOO')
parser.add_var('PW_BAR')
@@ -135,27 +141,39 @@ class TestEnvironmentParserWithPrefix(unittest.TestCase):
self.assertEqual(env.PW_BAR, self.raw_env['PW_BAR'])
def test_add_var_without_prefix(self):
- parser = envparse.EnvironmentParser(prefix='PW_',
- error_on_unrecognized=True)
+ parser = envparse.EnvironmentParser(
+ prefix='PW_', error_on_unrecognized=True
+ )
with self.assertRaises(ValueError):
parser.add_var('FOO')
class TestStrictBool(unittest.TestCase):
"""Tests for envparse.strict_bool."""
+
def setUp(self):
self.good_bools = ['true', '1', 'TRUE', 'tRuE']
self.bad_bools = [
- '', 'false', '0', 'foo', '2', '999', 'ok', 'yes', 'no'
+ '',
+ 'false',
+ '0',
+ 'foo',
+ '2',
+ '999',
+ 'ok',
+ 'yes',
+ 'no',
]
def test_good_bools(self):
self.assertTrue(
- all(envparse.strict_bool(val) for val in self.good_bools))
+ all(envparse.strict_bool(val) for val in self.good_bools)
+ )
def test_bad_bools(self):
self.assertFalse(
- any(envparse.strict_bool(val) for val in self.bad_bools))
+ any(envparse.strict_bool(val) for val in self.bad_bools)
+ )
if __name__ == '__main__':
diff --git a/pw_cli/py/plugins_test.py b/pw_cli/py/plugins_test.py
index c2063996d..a3f35132d 100644
--- a/pw_cli/py/plugins_test.py
+++ b/pw_cli/py/plugins_test.py
@@ -42,10 +42,12 @@ def _create_files(directory: str, files: Dict[str, str]) -> Iterator[Path]:
class TestPlugin(unittest.TestCase):
"""Tests for plugins.Plugins."""
+
def test_target_name_attribute(self) -> None:
self.assertEqual(
plugins.Plugin('abc', _no_docstring).target_name,
- f'{__name__}._no_docstring')
+ f'{__name__}._no_docstring',
+ )
def test_target_name_no_name_attribute(self) -> None:
has_no_name = 'no __name__'
@@ -53,42 +55,49 @@ class TestPlugin(unittest.TestCase):
self.assertEqual(
plugins.Plugin('abc', has_no_name).target_name,
- '<unknown>.no __name__')
+ '<unknown>.no __name__',
+ )
_TEST_PLUGINS = {
- 'TEST_PLUGINS': (f'test_plugin {__name__} _with_docstring\n'
- f'other_plugin {__name__} _no_docstring\n'),
- 'nested/in/dirs/TEST_PLUGINS':
- f'test_plugin {__name__} _no_docstring\n',
+ 'TEST_PLUGINS': (
+ f'test_plugin {__name__} _with_docstring\n'
+ f'other_plugin {__name__} _no_docstring\n'
+ ),
+ 'nested/in/dirs/TEST_PLUGINS': f'test_plugin {__name__} _no_docstring\n',
}
class TestPluginRegistry(unittest.TestCase):
"""Tests for plugins.Registry."""
+
def setUp(self) -> None:
self._registry = plugins.Registry(
- validator=plugins.callable_with_no_args)
+ validator=plugins.callable_with_no_args
+ )
def test_register(self) -> None:
- self.assertIsNotNone(self._registry.register('a_plugin',
- _no_docstring))
+ self.assertIsNotNone(self._registry.register('a_plugin', _no_docstring))
self.assertIs(self._registry['a_plugin'].target, _no_docstring)
def test_register_by_name(self) -> None:
self.assertIsNotNone(
- self._registry.register_by_name('plugin_one', __name__,
- '_no_docstring'))
+ self._registry.register_by_name(
+ 'plugin_one', __name__, '_no_docstring'
+ )
+ )
self.assertIsNotNone(
- self._registry.register('plugin_two', _no_docstring))
+ self._registry.register('plugin_two', _no_docstring)
+ )
self.assertIs(self._registry['plugin_one'].target, _no_docstring)
self.assertIs(self._registry['plugin_two'].target, _no_docstring)
def test_register_by_name_undefined_module(self) -> None:
with self.assertRaisesRegex(plugins.Error, 'No module named'):
- self._registry.register_by_name('plugin_two', 'not a module',
- 'something')
+ self._registry.register_by_name(
+ 'plugin_two', 'not a module', 'something'
+ )
def test_register_by_name_undefined_function(self) -> None:
with self.assertRaisesRegex(plugins.Error, 'does not exist'):
@@ -116,7 +125,8 @@ class TestPluginRegistry(unittest.TestCase):
def test_register_cannot_overwrite(self) -> None:
self.assertIsNotNone(self._registry.register('foo', lambda: None))
self.assertIsNotNone(
- self._registry.register_by_name('bar', __name__, '_no_docstring'))
+ self._registry.register_by_name('bar', __name__, '_no_docstring')
+ )
with self.assertRaises(plugins.Error):
self._registry.register('foo', lambda: None)
@@ -141,8 +151,9 @@ class TestPluginRegistry(unittest.TestCase):
def test_register_directory_with_restriction(self) -> None:
with tempfile.TemporaryDirectory() as tempdir:
paths = list(_create_files(tempdir, _TEST_PLUGINS))
- self._registry.register_directory(paths[0].parent, 'TEST_PLUGINS',
- Path(tempdir, 'nested', 'in'))
+ self._registry.register_directory(
+ paths[0].parent, 'TEST_PLUGINS', Path(tempdir, 'nested', 'in')
+ )
self.assertNotIn('other_plugin', self._registry)
@@ -161,10 +172,13 @@ class TestPluginRegistry(unittest.TestCase):
self.assertIn(__doc__, '\n'.join(self._registry.detailed_help(['a'])))
- self.assertNotIn(__doc__,
- '\n'.join(self._registry.detailed_help(['b'])))
- self.assertIn(_with_docstring.__doc__,
- '\n'.join(self._registry.detailed_help(['b'])))
+ self.assertNotIn(
+ __doc__, '\n'.join(self._registry.detailed_help(['b']))
+ )
+ self.assertIn(
+ _with_docstring.__doc__,
+ '\n'.join(self._registry.detailed_help(['b'])),
+ )
def test_empty_string_if_no_help(self) -> None:
fake_module_name = f'{__name__}.fake_module_for_test{id(self)}'
@@ -174,7 +188,6 @@ class TestPluginRegistry(unittest.TestCase):
sys.modules[fake_module_name] = fake_module
try:
-
function = lambda: None
function.__module__ = fake_module_name
self.assertIsNotNone(self._registry.register('a', function))
diff --git a/pw_cli/py/process_integration_test.py b/pw_cli/py/process_integration_test.py
new file mode 100644
index 000000000..3d05b1a1b
--- /dev/null
+++ b/pw_cli/py/process_integration_test.py
@@ -0,0 +1,73 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for pw_cli.process."""
+
+import unittest
+import sys
+import textwrap
+
+from pw_cli import process
+
+import psutil # type: ignore
+
+
+FAST_TIMEOUT_SECONDS = 0.1
+KILL_SIGNALS = set({-9, 137})
+PYTHON = sys.executable
+
+
+class RunTest(unittest.TestCase):
+ """Tests for process.run."""
+
+ def test_returns_output(self) -> None:
+ echo_str = 'foobar'
+ print_str = f'print("{echo_str}")'
+ result = process.run(PYTHON, '-c', print_str)
+ self.assertEqual(result.output, b'foobar\n')
+
+ def test_timeout_kills_process(self) -> None:
+ sleep_100_seconds = 'import time; time.sleep(100)'
+ result = process.run(
+ PYTHON, '-c', sleep_100_seconds, timeout=FAST_TIMEOUT_SECONDS
+ )
+ self.assertIn(result.returncode, KILL_SIGNALS)
+
+ def test_timeout_kills_subprocess(self) -> None:
+ # Spawn a subprocess which waits for 100 seconds, print its pid,
+ # then wait for 100 seconds.
+ sleep_in_subprocess = textwrap.dedent(
+ f"""
+ import subprocess
+ import time
+
+ child = subprocess.Popen(
+ ['{PYTHON}', '-c', 'import time; print("booh"); time.sleep(100)']
+ )
+ print(child.pid, flush=True)
+ time.sleep(100)
+ """
+ )
+ result = process.run(
+ PYTHON, '-c', sleep_in_subprocess, timeout=FAST_TIMEOUT_SECONDS
+ )
+ self.assertIn(result.returncode, KILL_SIGNALS)
+ # THe first line of the output is the PID of the child sleep process.
+ child_pid_str, sep, remainder = result.output.partition(b'\n')
+ del sep, remainder
+ child_pid = int(child_pid_str)
+ self.assertFalse(psutil.pid_exists(child_pid))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_cli/py/pw_cli/__main__.py b/pw_cli/py/pw_cli/__main__.py
index ad6ceeb58..3320a4556 100644
--- a/pw_cli/py/pw_cli/__main__.py
+++ b/pw_cli/py/pw_cli/__main__.py
@@ -29,7 +29,7 @@ def main() -> NoReturn:
args = arguments.parse_args()
- pw_cli.log.install(level=args.loglevel)
+ pw_cli.log.install(level=args.loglevel, debug_log=args.debug_log)
# Start with the most critical part of the Pigweed command line tool.
if not args.no_banner:
diff --git a/pw_cli/py/pw_cli/argument_types.py b/pw_cli/py/pw_cli/argument_types.py
index a719e1693..0db022a19 100644
--- a/pw_cli/py/pw_cli/argument_types.py
+++ b/pw_cli/py/pw_cli/argument_types.py
@@ -31,4 +31,5 @@ def log_level(arg: str) -> int:
return getattr(logging, arg.upper())
except AttributeError:
raise argparse.ArgumentTypeError(
- f'"{arg.upper()}" is not a valid log level')
+ f'"{arg.upper()}" is not a valid log level'
+ )
diff --git a/pw_cli/py/pw_cli/arguments.py b/pw_cli/py/pw_cli/arguments.py
index 86ff6e266..f7fe37b4e 100644
--- a/pw_cli/py/pw_cli/arguments.py
+++ b/pw_cli/py/pw_cli/arguments.py
@@ -26,7 +26,7 @@ _HELP_HEADER = '''The Pigweed command line interface (CLI).
Example uses:
pw logdemo
- pw --loglevel debug watch out/clang
+ pw --loglevel debug watch -C out
'''
@@ -46,6 +46,7 @@ def format_help(registry: plugins.Registry) -> str:
class _ArgumentParserWithBanner(argparse.ArgumentParser):
"""Parser that the Pigweed banner when there are parsing errors."""
+
def error(self, message: str) -> NoReturn:
print_banner()
self.print_usage(sys.stderr)
@@ -58,37 +59,53 @@ def _parser() -> argparse.ArgumentParser:
prog='pw',
add_help=False,
description=_HELP_HEADER,
- formatter_class=argparse.RawDescriptionHelpFormatter)
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
# Do not use the built-in help argument so that displaying the help info can
# be deferred until the pw plugins have been registered.
- argparser.add_argument('-h',
- '--help',
- action='store_true',
- help='Display this help message and exit')
+ argparser.add_argument(
+ '-h',
+ '--help',
+ action='store_true',
+ help='Display this help message and exit',
+ )
argparser.add_argument(
'-C',
'--directory',
type=argument_types.directory,
default=Path.cwd(),
- help='Change to this directory before doing anything')
+ help='Change to this directory before doing anything',
+ )
argparser.add_argument(
'-l',
'--loglevel',
type=argument_types.log_level,
default=logging.INFO,
- help='Set the log level (debug, info, warning, error, critical)')
- argparser.add_argument('--no-banner',
- action='store_true',
- help='Do not print the Pigweed banner')
+ help='Set the log level (debug, info, warning, error, critical)',
+ )
+ argparser.add_argument(
+ '--debug-log',
+ help=(
+ 'Additionally log to this file at debug level; does not affect '
+ 'terminal output'
+ ),
+ )
+ argparser.add_argument(
+ '--no-banner',
+ action='store_true',
+ help='Do not print the Pigweed banner',
+ )
argparser.add_argument(
'command',
nargs='?',
- help='Which command to run; see supported commands below')
+ help='Which command to run; see supported commands below',
+ )
argparser.add_argument(
'plugin_args',
metavar='...',
nargs=argparse.REMAINDER,
- help='Remaining arguments are forwarded to the command')
+ help='Remaining arguments are forwarded to the command',
+ )
return argparser
diff --git a/pw_cli/py/pw_cli/branding.py b/pw_cli/py/pw_cli/branding.py
index 81274eced..358e4019b 100644
--- a/pw_cli/py/pw_cli/branding.py
+++ b/pw_cli/py/pw_cli/branding.py
@@ -41,14 +41,18 @@ def banner() -> str:
# Take the banner from the file PW_BRANDING_BANNER; or use the default.
banner_filename = parsed_env.PW_BRANDING_BANNER
- _memoized_banner = (Path(banner_filename).read_text()
- if banner_filename else _PIGWEED_BANNER)
+ _memoized_banner = (
+ Path(banner_filename).read_text()
+ if banner_filename
+ else _PIGWEED_BANNER
+ )
# Color the banner if requested.
banner_color = parsed_env.PW_BRANDING_BANNER_COLOR
if banner_color != '':
set_color = operator.attrgetter(banner_color)(pw_cli.color.colors())
_memoized_banner = '\n'.join(
- set_color(line) for line in _memoized_banner.splitlines())
+ set_color(line) for line in _memoized_banner.splitlines()
+ )
return _memoized_banner
diff --git a/pw_cli/py/pw_cli/color.py b/pw_cli/py/pw_cli/color.py
index ef2563aad..855a8d11c 100644
--- a/pw_cli/py/pw_cli/color.py
+++ b/pw_cli/py/pw_cli/color.py
@@ -35,6 +35,7 @@ class _Color:
# pylint: disable=too-few-public-methods
# pylint: disable=too-many-instance-attributes
"""Helpers to surround text with ASCII color escapes"""
+
def __init__(self):
self.none = str
self.red = _make_color(31, 1)
@@ -49,10 +50,13 @@ class _Color:
self.bold_magenta = _make_color(30, 45)
self.bold_white = _make_color(37, 1)
self.black_on_white = _make_color(30, 47) # black fg white bg
+ self.black_on_green = _make_color(30, 42) # black fg green bg
+ self.black_on_red = _make_color(30, 41) # black fg red bg
class _NoColor:
"""Fake version of the _Color class that doesn't colorize."""
+
def __getattr__(self, _):
return str
@@ -64,8 +68,9 @@ def colors(enabled: Optional[bool] = None) -> Union[_Color, _NoColor]:
"""
if enabled is None:
env = pw_cli.env.pigweed_environment()
- enabled = env.PW_USE_COLOR or (sys.stdout.isatty()
- and sys.stderr.isatty())
+ enabled = env.PW_USE_COLOR or (
+ sys.stdout.isatty() and sys.stderr.isatty()
+ )
if enabled and os.name == 'nt':
# Enable ANSI color codes in Windows cmd.exe.
diff --git a/pw_cli/py/pw_cli/env.py b/pw_cli/py/pw_cli/env.py
index c1e47751c..74deff8ea 100644
--- a/pw_cli/py/pw_cli/env.py
+++ b/pw_cli/py/pw_cli/env.py
@@ -13,6 +13,7 @@
# the License.
"""The env module defines the environment variables used by Pigweed."""
+from pathlib import Path
from typing import Optional
from pw_cli import envparse
@@ -27,15 +28,21 @@ def pigweed_environment_parser() -> envparse.EnvironmentParser:
parser.add_var('PW_EMOJI', type=envparse.strict_bool, default=False)
parser.add_var('PW_ENVSETUP')
parser.add_var('PW_ENVSETUP_FULL')
- parser.add_var('PW_ENVSETUP_NO_BANNER',
- type=envparse.strict_bool,
- default=False)
- parser.add_var('PW_ENVSETUP_QUIET',
- type=envparse.strict_bool,
- default=False)
- parser.add_var('PW_ENVIRONMENT_ROOT')
- parser.add_var('PW_PROJECT_ROOT')
- parser.add_var('PW_ROOT')
+ parser.add_var(
+ 'PW_ENVSETUP_NO_BANNER', type=envparse.strict_bool, default=False
+ )
+ parser.add_var(
+ 'PW_ENVSETUP_QUIET', type=envparse.strict_bool, default=False
+ )
+ parser.add_var('PW_ENVIRONMENT_ROOT', type=Path)
+ parser.add_var('PW_PACKAGE_ROOT', type=Path)
+ parser.add_var('PW_PROJECT_ROOT', type=Path)
+ parser.add_var('PW_ROOT', type=Path)
+ parser.add_var(
+ 'PW_DISABLE_ROOT_GIT_REPO_CHECK',
+ type=envparse.strict_bool,
+ default=False,
+ )
parser.add_var('PW_SKIP_BOOTSTRAP')
parser.add_var('PW_SUBPROCESS', type=envparse.strict_bool, default=False)
parser.add_var('PW_USE_COLOR', type=envparse.strict_bool, default=False)
@@ -43,24 +50,33 @@ def pigweed_environment_parser() -> envparse.EnvironmentParser:
parser.add_allowed_suffix('_CIPD_INSTALL_DIR')
- parser.add_var('PW_ENVSETUP_DISABLE_SPINNER',
- type=envparse.strict_bool,
- default=False)
+ parser.add_var(
+ 'PW_ENVSETUP_DISABLE_SPINNER', type=envparse.strict_bool, default=False
+ )
parser.add_var('PW_DOCTOR_SKIP_CIPD_CHECKS')
- parser.add_var('PW_ACTIVATE_SKIP_CHECKS',
- type=envparse.strict_bool,
- default=False)
+ parser.add_var(
+ 'PW_ACTIVATE_SKIP_CHECKS', type=envparse.strict_bool, default=False
+ )
parser.add_var('PW_BANNER_FUNC')
parser.add_var('PW_BRANDING_BANNER')
parser.add_var('PW_BRANDING_BANNER_COLOR', default='magenta')
- parser.add_var('PW_PRESUBMIT_DISABLE_SUBPROCESS_CAPTURE',
- type=envparse.strict_bool)
+ parser.add_var(
+ 'PW_PRESUBMIT_DISABLE_SUBPROCESS_CAPTURE', type=envparse.strict_bool
+ )
parser.add_var('PW_CONSOLE_CONFIG_FILE')
parser.add_var('PW_ENVIRONMENT_NO_ERROR_ON_UNRECOGNIZED')
+ parser.add_var('PW_CIPD_SERVICE_ACCOUNT_JSON')
+
+ # RBE environment variables
+ parser.add_var('PW_USE_RBE', default=False)
+ parser.add_var('PW_RBE_DEBUG', default=False)
+ parser.add_var('PW_RBE_CLANG_CONFIG', default='')
+ parser.add_var('PW_RBE_ARM_GCC_CONFIG', default='')
+
return parser
diff --git a/pw_cli/py/pw_cli/envparse.py b/pw_cli/py/pw_cli/envparse.py
index e27eb5b8c..d9ed9de47 100644
--- a/pw_cli/py/pw_cli/envparse.py
+++ b/pw_cli/py/pw_cli/envparse.py
@@ -16,11 +16,22 @@
import argparse
from dataclasses import dataclass
import os
-from typing import Callable, Dict, Generic, IO, List, Mapping, Optional, TypeVar
-from typing import Union
-
-
-class EnvNamespace(argparse.Namespace): # pylint: disable=too-few-public-methods
+from typing import (
+ Callable,
+ Dict,
+ Generic,
+ IO,
+ List,
+ Mapping,
+ Optional,
+ TypeVar,
+ Union,
+)
+
+
+class EnvNamespace(
+ argparse.Namespace
+): # pylint: disable=too-few-public-methods
"""Base class for parsed environment variable namespaces."""
@@ -42,11 +53,13 @@ class EnvironmentValueError(Exception):
function through the __cause__ attribute for more detailed information on
the error.
"""
+
def __init__(self, variable: str, value: str):
self.variable: str = variable
self.value: str = value
super().__init__(
- f'Bad value for environment variable {variable}: {value}')
+ f'Bad value for environment variable {variable}: {value}'
+ )
class EnvironmentParser:
@@ -71,9 +84,12 @@ class EnvironmentParser:
configure_logging(env.PW_LOG_LEVEL, env.PW_LOG_FILE)
"""
- def __init__(self,
- prefix: Optional[str] = None,
- error_on_unrecognized: Union[bool, None] = None) -> None:
+
+ def __init__(
+ self,
+ prefix: Optional[str] = None,
+ error_on_unrecognized: Union[bool, None] = None,
+ ) -> None:
self._prefix: Optional[str] = prefix
if error_on_unrecognized is None:
varname = 'PW_ENVIRONMENT_NO_ERROR_ON_UNRECOGNIZED'
@@ -104,7 +120,8 @@ class EnvironmentParser:
"""
if self._prefix is not None and not name.startswith(self._prefix):
raise ValueError(
- f'Variable {name} does not have prefix {self._prefix}')
+ f'Variable {name} does not have prefix {self._prefix}'
+ )
self._variables[name] = VariableDescriptor(name, type, default)
@@ -113,8 +130,9 @@ class EnvironmentParser:
self._allowed_suffixes.append(suffix)
- def parse_env(self,
- env: Optional[Mapping[str, str]] = None) -> EnvNamespace:
+ def parse_env(
+ self, env: Optional[Mapping[str, str]] = None
+ ) -> EnvNamespace:
"""Parses known environment variables into a namespace.
Args:
@@ -141,17 +159,21 @@ class EnvironmentParser:
allowed_suffixes = tuple(self._allowed_suffixes)
for var in env:
- if (not hasattr(namespace, var)
- and (self._prefix is None or var.startswith(self._prefix))
- and var.endswith(allowed_suffixes)):
+ if (
+ not hasattr(namespace, var)
+ and (self._prefix is None or var.startswith(self._prefix))
+ and var.endswith(allowed_suffixes)
+ ):
setattr(namespace, var, env[var])
if self._prefix is not None and self._error_on_unrecognized:
for var in env:
- if (var.startswith(self._prefix) and var not in self._variables
- and not var.endswith(allowed_suffixes)):
- raise ValueError(
- f'Unrecognized environment variable {var}')
+ if (
+ var.startswith(self._prefix)
+ and var not in self._variables
+ and not var.endswith(allowed_suffixes)
+ ):
+ raise ValueError(f'Unrecognized environment variable {var}')
return namespace
@@ -160,21 +182,24 @@ class EnvironmentParser:
# List of emoji which are considered to represent "True".
-_BOOLEAN_TRUE_EMOJI = set([
- '✔️',
- '👍',
- '👍🏻',
- '👍🏼',
- '👍🏽',
- '👍🏾',
- '👍🏿',
- '💯',
-])
+_BOOLEAN_TRUE_EMOJI = set(
+ [
+ '✔️',
+ '👍',
+ '👍🏻',
+ '👍🏼',
+ '👍🏽',
+ '👍🏾',
+ '👍🏿',
+ '💯',
+ ]
+)
def strict_bool(value: str) -> bool:
- return (value == '1' or value.lower() == 'true'
- or value in _BOOLEAN_TRUE_EMOJI)
+ return (
+ value == '1' or value.lower() == 'true' or value in _BOOLEAN_TRUE_EMOJI
+ )
# TODO(mohrr) Switch to Literal when no longer supporting Python 3.7.
diff --git a/pw_cli/py/pw_cli/log.py b/pw_cli/py/pw_cli/log.py
index 3ef755470..c8414d1f5 100644
--- a/pw_cli/py/pw_cli/log.py
+++ b/pw_cli/py/pw_cli/log.py
@@ -15,11 +15,11 @@
import logging
from pathlib import Path
+import sys
from typing import NamedTuple, Optional, Union, Iterator
-import pw_cli.color
-import pw_cli.env
-import pw_cli.plugins
+from pw_cli.color import colors as pw_cli_colors
+from pw_cli.env import pigweed_environment
# Log level used for captured output of a subprocess run through pw.
LOGLEVEL_STDOUT = 21
@@ -37,6 +37,7 @@ class _LogLevel(NamedTuple):
# Shorten all the log levels to 3 characters for column-aligned logs.
# Color the logs using ANSI codes.
+# fmt: off
_LOG_LEVELS = (
_LogLevel(LOGLEVEL_FATAL, 'bold_red', 'FTL', '☠️ '),
_LogLevel(logging.CRITICAL, 'bold_magenta', 'CRT', '‼️ '),
@@ -45,7 +46,8 @@ _LOG_LEVELS = (
_LogLevel(logging.INFO, 'magenta', 'INF', 'ℹ️ '),
_LogLevel(LOGLEVEL_STDOUT, 'cyan', 'OUT', '💬'),
_LogLevel(logging.DEBUG, 'blue', 'DBG', '👾'),
-) # yapf: disable
+)
+# fmt: on
_LOG = logging.getLogger(__name__)
_STDERR_HANDLER = logging.StreamHandler()
@@ -72,18 +74,29 @@ def main() -> None:
_LOG.debug('Adding 1 to i')
-def _setup_handler(handler: logging.Handler, formatter: logging.Formatter,
- level: Union[str, int], logger: logging.Logger) -> None:
+def _setup_handler(
+ handler: logging.Handler,
+ formatter: logging.Formatter,
+ level: Union[str, int],
+ logger: logging.Logger,
+) -> None:
handler.setLevel(level)
handler.setFormatter(formatter)
logger.addHandler(handler)
-def install(level: Union[str, int] = logging.INFO,
- use_color: bool = None,
- hide_timestamp: bool = False,
- log_file: Union[str, Path] = None,
- logger: Optional[logging.Logger] = None) -> None:
+def install(
+ level: Union[str, int] = logging.INFO,
+ use_color: Optional[bool] = None,
+ hide_timestamp: bool = False,
+ log_file: Optional[Union[str, Path]] = None,
+ logger: Optional[logging.Logger] = None,
+ debug_log: Optional[Union[str, Path]] = None,
+ time_format: str = '%Y%m%d %H:%M:%S',
+ msec_format: str = '%s,%03d',
+ include_msec: bool = False,
+ message_format: str = '%(levelname)s %(message)s',
+) -> None:
"""Configures the system logger for the default pw command log format.
If you have Python loggers separate from the root logger you can use
@@ -107,17 +120,27 @@ def install(level: Union[str, int] = logging.INFO,
use_color: When `True` include ANSI escape sequences to colorize log
messages.
hide_timestamp: When `True` omit timestamps from the log formatting.
- log_file: File to save logs into.
+ log_file: File to send logs into instead of the terminal.
logger: Python Logger instance to install Pigweed formatting into.
Defaults to the Python root logger: `logging.getLogger()`.
-
+ debug_log: File to log to from all levels, regardless of chosen log level.
+ Logs will go here in addition to the terminal.
+ time_format: Default time format string.
+ msec_format: Default millisecond format string. This should be a format
+ string that accepts a both a string ``%s`` and an integer ``%d``. The
+ default Python format for this string is ``%s,%03d``.
+ include_msec: Whether or not to include the millisecond part of log
+ timestamps.
+ message_format: The message format string. By default this includes
+ levelname and message. The asctime field is prepended to this unless
+ hide_timestamp=True.
"""
if not logger:
logger = logging.getLogger()
- colors = pw_cli.color.colors(use_color)
+ colors = pw_cli_colors(use_color)
- env = pw_cli.env.pigweed_environment()
+ env = pigweed_environment()
if env.PW_SUBPROCESS or hide_timestamp:
# If the logger is being run in the context of a pw subprocess, the
# time and date are omitted (since pw_cli.process will provide them).
@@ -128,8 +151,20 @@ def install(level: Union[str, int] = logging.INFO,
# colored text.
timestamp_fmt = colors.black_on_white('%(asctime)s') + ' '
- formatter = logging.Formatter(timestamp_fmt + '%(levelname)s %(message)s',
- '%Y%m%d %H:%M:%S')
+ formatter = logging.Formatter(fmt=timestamp_fmt + message_format)
+
+ formatter.default_time_format = time_format
+ if include_msec:
+ formatter.default_msec_format = msec_format
+ else:
+ # Python 3.8 and lower does not check if default_msec_format is set.
+ # https://github.com/python/cpython/blob/3.8/Lib/logging/__init__.py#L611
+ # https://github.com/python/cpython/blob/3.9/Lib/logging/__init__.py#L605
+ if sys.version_info >= (
+ 3,
+ 9,
+ ):
+ formatter.default_msec_format = ''
# Set the log level on the root logger to NOTSET, so that all logs
# propagated from child loggers are handled.
@@ -141,11 +176,25 @@ def install(level: Union[str, int] = logging.INFO,
if log_file:
# Set utf-8 encoding for the log file. Encoding errors may come up on
# Windows if the default system encoding is set to cp1250.
- _setup_handler(logging.FileHandler(log_file, encoding='utf-8'),
- formatter, level, logger)
+ _setup_handler(
+ logging.FileHandler(log_file, encoding='utf-8'),
+ formatter,
+ level,
+ logger,
+ )
# Since we're using a file, filter logs out of the stderr handler.
_STDERR_HANDLER.setLevel(logging.CRITICAL + 1)
+ if debug_log:
+ # Set utf-8 encoding for the log file. Encoding errors may come up on
+ # Windows if the default system encoding is set to cp1250.
+ _setup_handler(
+ logging.FileHandler(debug_log, encoding='utf-8'),
+ formatter,
+ logging.DEBUG,
+ logger,
+ )
+
if env.PW_EMOJI:
name_attr = 'emoji'
colorize = lambda ll: str
diff --git a/pw_cli/py/pw_cli/plugins.py b/pw_cli/py/pw_cli/plugins.py
index ebaf3fb52..b82b7f675 100644
--- a/pw_cli/py/pw_cli/plugins.py
+++ b/pw_cli/py/pw_cli/plugins.py
@@ -46,6 +46,7 @@ _BUILT_IN = '<built-in>'
class Error(Exception):
"""Indicates that a plugin is invalid or cannot be registered."""
+
def __str__(self):
"""Displays the error as a string, including the __cause__ if present.
@@ -54,8 +55,10 @@ class Error(Exception):
if self.__cause__ is None:
return super().__str__()
- return (f'{super().__str__()} '
- f'({type(self.__cause__).__name__}: {self.__cause__})')
+ return (
+ f'{super().__str__()} '
+ f'({type(self.__cause__).__name__}: {self.__cause__})'
+ )
def _get_module(member: object) -> types.ModuleType:
@@ -69,9 +72,15 @@ class Plugin:
Each plugin resolves to a Python object, typically a function.
"""
+
@classmethod
- def from_name(cls, name: str, module_name: str, member_name: str,
- source: Optional[Path]) -> 'Plugin':
+ def from_name(
+ cls,
+ name: str,
+ module_name: str,
+ member_name: str,
+ source: Optional[Path],
+ ) -> 'Plugin':
"""Creates a plugin by module and attribute name.
Args:
@@ -86,17 +95,26 @@ class Plugin:
try:
module = importlib.import_module(module_name)
except Exception as err:
+ _LOG.debug(
+ 'Failed to import module "%s" for "%s" plugin',
+ module_name,
+ name,
+ exc_info=True,
+ )
raise Error(f'Failed to import module "{module_name}"') from err
try:
member = getattr(module, member_name)
except AttributeError as err:
raise Error(
- f'"{module_name}.{member_name}" does not exist') from err
+ f'"{module_name}.{member_name}" does not exist'
+ ) from err
return cls(name, member, source)
- def __init__(self, name: str, target: Any, source: Path = None) -> None:
+ def __init__(
+ self, name: str, target: Any, source: Optional[Path] = None
+ ) -> None:
"""Creates a plugin for the provided target."""
self.name = name
self._module = _get_module(target)
@@ -105,8 +123,10 @@ class Plugin:
@property
def target_name(self) -> str:
- return (f'{self._module.__name__}.'
- f'{getattr(self.target, "__name__", self.target)}')
+ return (
+ f'{self._module.__name__}.'
+ f'{getattr(self.target, "__name__", self.target)}'
+ )
@property
def source_name(self) -> str:
@@ -137,9 +157,11 @@ class Plugin:
yield f'source {self.source_name}'
def __repr__(self) -> str:
- return (f'{self.__class__.__name__}(name={self.name!r}, '
- f'target={self.target_name}'
- f'{f", source={self.source_name!r}" if self.source else ""})')
+ return (
+ f'{self.__class__.__name__}(name={self.name!r}, '
+ f'target={self.target_name}'
+ f'{f", source={self.source_name!r}" if self.source else ""})'
+ )
def callable_with_no_args(plugin: Plugin) -> None:
@@ -150,20 +172,26 @@ def callable_with_no_args(plugin: Plugin) -> None:
try:
params = inspect.signature(plugin.target).parameters
except TypeError:
- raise Error('Plugin functions must be callable, but '
- f'{plugin.target_name} is a '
- f'{type(plugin.target).__name__}')
+ raise Error(
+ 'Plugin functions must be callable, but '
+ f'{plugin.target_name} is a '
+ f'{type(plugin.target).__name__}'
+ )
positional = sum(p.default == p.empty for p in params.values())
if positional:
- raise Error(f'Plugin functions cannot have any required positional '
- f'arguments, but {plugin.target_name} has {positional}')
+ raise Error(
+ f'Plugin functions cannot have any required positional '
+ f'arguments, but {plugin.target_name} has {positional}'
+ )
class Registry(collections.abc.Mapping):
"""Manages a set of plugins from Python modules or plugins files."""
- def __init__(self,
- validator: Callable[[Plugin], Any] = lambda _: None) -> None:
+
+ def __init__(
+ self, validator: Callable[[Plugin], Any] = lambda _: None
+ ) -> None:
"""Creates a new, empty plugins registry.
Args:
@@ -173,8 +201,7 @@ class Registry(collections.abc.Mapping):
self._registry: Dict[str, Plugin] = {}
self._sources: Set[Path] = set() # Paths to plugins files
- self._errors: Dict[str,
- List[Exception]] = collections.defaultdict(list)
+ self._errors: Dict[str, List[Exception]] = collections.defaultdict(list)
self._validate_plugin = validator
def __getitem__(self, name: str) -> Plugin:
@@ -183,8 +210,10 @@ class Registry(collections.abc.Mapping):
return self._registry[name]
if name in self._errors:
- raise KeyError(f'Registration for "{name}" failed: ' +
- ', '.join(str(e) for e in self._errors[name]))
+ raise KeyError(
+ f'Registration for "{name}" failed: '
+ + ', '.join(str(e) for e in self._errors[name])
+ )
raise KeyError(f'The plugin "{name}" has not been registered')
@@ -218,7 +247,8 @@ class Registry(collections.abc.Mapping):
raise Error(
f'Attempted to register built-in plugin "{plugin.name}", but '
'a plugin with that name was previously registered '
- f'({self[plugin.name]})!')
+ f'({self[plugin.name]})!'
+ )
# Run the user-provided validation function, which raises exceptions
# if there are errors.
@@ -230,20 +260,30 @@ class Registry(collections.abc.Mapping):
return True
if existing.source is None:
- _LOG.debug('%s: Overriding built-in plugin "%s" with %s',
- plugin.source_name, plugin.name, plugin.target_name)
+ _LOG.debug(
+ '%s: Overriding built-in plugin "%s" with %s',
+ plugin.source_name,
+ plugin.name,
+ plugin.target_name,
+ )
return True
if plugin.source != existing.source:
_LOG.debug(
'%s: The plugin "%s" was previously registered in %s; '
- 'ignoring registration as %s', plugin.source_name, plugin.name,
- self._registry[plugin.name].source, plugin.target_name)
+ 'ignoring registration as %s',
+ plugin.source_name,
+ plugin.name,
+ self._registry[plugin.name].source,
+ plugin.target_name,
+ )
elif plugin.source not in self._sources:
_LOG.warning(
'%s: "%s" is registered file multiple times in this file! '
- 'Only the first registration takes effect', plugin.source_name,
- plugin.name)
+ 'Only the first registration takes effect',
+ plugin.source_name,
+ plugin.name,
+ )
return False
@@ -251,14 +291,17 @@ class Registry(collections.abc.Mapping):
"""Registers an object as a plugin."""
return self._register(Plugin(name, target, None))
- def register_by_name(self,
- name: str,
- module_name: str,
- member_name: str,
- source: Path = None) -> Optional[Plugin]:
+ def register_by_name(
+ self,
+ name: str,
+ module_name: str,
+ member_name: str,
+ source: Optional[Path] = None,
+ ) -> Optional[Plugin]:
"""Registers an object from its module and name as a plugin."""
return self._register(
- Plugin.from_name(name, module_name, member_name, source))
+ Plugin.from_name(name, module_name, member_name, source)
+ )
def _register(self, plugin: Plugin) -> Optional[Plugin]:
# Prohibit functions not from a plugins file from overriding others.
@@ -266,8 +309,12 @@ class Registry(collections.abc.Mapping):
return None
self._registry[plugin.name] = plugin
- _LOG.debug('%s: Registered plugin "%s" for %s', plugin.source_name,
- plugin.name, plugin.target_name)
+ _LOG.debug(
+ '%s: Registered plugin "%s" for %s',
+ plugin.source_name,
+ plugin.name,
+ plugin.target_name,
+ )
return plugin
@@ -289,22 +336,33 @@ class Registry(collections.abc.Mapping):
_LOG.error(
'%s:%d: Failed to parse plugin entry "%s": '
'Expected 3 items (name, module, function), '
- 'got %d', path, lineno, line, len(line.split()))
+ 'got %d',
+ path,
+ lineno,
+ line,
+ len(line.split()),
+ )
continue
try:
self.register_by_name(name, module, function, path)
except Error as err:
self._errors[name].append(err)
- _LOG.error('%s: Failed to register plugin "%s": %s', path,
- name, err)
+ _LOG.error(
+ '%s: Failed to register plugin "%s": %s',
+ path,
+ name,
+ err,
+ )
self._sources.add(path)
- def register_directory(self,
- directory: Path,
- file_name: str,
- restrict_to: Path = None) -> None:
+ def register_directory(
+ self,
+ directory: Path,
+ file_name: str,
+ restrict_to: Optional[Path] = None,
+ ) -> None:
"""Finds and registers plugins from plugins files in a directory.
Args:
@@ -319,7 +377,9 @@ class Registry(collections.abc.Mapping):
if restrict_to is not None and restrict_to not in path.parents:
_LOG.debug(
"Skipping plugins file %s because it's outside of %s",
- path, restrict_to)
+ path,
+ restrict_to,
+ )
continue
_LOG.debug('Found plugins file %s', path)
@@ -327,11 +387,15 @@ class Registry(collections.abc.Mapping):
def short_help(self) -> str:
"""Returns a help string for the registered plugins."""
- width = max(len(name)
- for name in self._registry) + 1 if self._registry else 1
+ width = (
+ max(len(name) for name in self._registry) + 1
+ if self._registry
+ else 1
+ )
help_items = '\n'.join(
f' {name:{width}} {plugin.help()}'
- for name, plugin in sorted(self._registry.items()))
+ for name, plugin in sorted(self._registry.items())
+ )
return f'supported plugins:\n{help_items}'
def detailed_help(self, plugins: Iterable[str] = ()) -> Iterator[str]:
@@ -341,9 +405,9 @@ class Registry(collections.abc.Mapping):
yield '\ndetailed plugin information:'
- wrapper = TextWrapper(width=80,
- initial_indent=' ',
- subsequent_indent=' ' * 11)
+ wrapper = TextWrapper(
+ width=80, initial_indent=' ', subsequent_indent=' ' * 11
+ )
plugins = sorted(plugins)
for plugin in plugins:
@@ -360,19 +424,19 @@ class Registry(collections.abc.Mapping):
yield 'Plugins files:'
if self._sources:
- yield from (f' [{i}] {file}'
- for i, file in enumerate(self._sources, 1))
+ yield from (
+ f' [{i}] {file}' for i, file in enumerate(self._sources, 1)
+ )
else:
yield ' (none found)'
- def plugin(self,
- function: Callable = None,
- *,
- name: str = None) -> Callable[[Callable], Callable]:
+ def plugin(
+ self, function: Optional[Callable] = None, *, name: Optional[str] = None
+ ) -> Callable[[Callable], Callable]:
"""Decorator that registers a function with this plugin registry."""
+
def decorator(function: Callable) -> Callable:
- self.register(function.__name__ if name is None else name,
- function)
+ self.register(function.__name__ if name is None else name, function)
return function
if function is None:
@@ -407,8 +471,9 @@ def find_all_in_parents(name: str, path: Path) -> Iterator[Path]:
path = result.parent.parent
-def import_submodules(module: types.ModuleType,
- recursive: bool = False) -> None:
+def import_submodules(
+ module: types.ModuleType, recursive: bool = False
+) -> None:
"""Imports the submodules of a package.
This can be used to collect plugins registered with a decorator from a
diff --git a/pw_cli/py/pw_cli/process.py b/pw_cli/py/pw_cli/process.py
index 2366fc066..98852e35f 100644
--- a/pw_cli/py/pw_cli/process.py
+++ b/pw_cli/py/pw_cli/process.py
@@ -18,11 +18,14 @@ import logging
import os
import shlex
import tempfile
-from typing import IO, Tuple, Union, Optional
+from typing import Dict, IO, Tuple, Union, Optional
import pw_cli.color
import pw_cli.log
+import psutil # type: ignore
+
+
_COLOR = pw_cli.color.colors()
_LOG = logging.getLogger(__name__)
@@ -32,9 +35,18 @@ PW_SUBPROCESS_ENV = 'PW_SUBPROCESS'
class CompletedProcess:
- """Information about a process executed in run_async."""
- def __init__(self, process: 'asyncio.subprocess.Process',
- output: Union[bytes, IO[bytes]]):
+ """Information about a process executed in run_async.
+
+ Attributes:
+ pid: The process identifier.
+ returncode: The return code of the process.
+ """
+
+ def __init__(
+ self,
+ process: 'asyncio.subprocess.Process',
+ output: Union[bytes, IO[bytes]],
+ ):
assert process.returncode is not None
self.returncode: int = process.returncode
self.pid = process.pid
@@ -58,7 +70,8 @@ async def _run_and_log(program: str, args: Tuple[str, ...], env: dict):
*args,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.STDOUT,
- env=env)
+ env=env,
+ )
output = bytearray()
@@ -69,31 +82,105 @@ async def _run_and_log(program: str, args: Tuple[str, ...], env: dict):
break
output += line
- _LOG.log(pw_cli.log.LOGLEVEL_STDOUT, '[%s] %s',
- _COLOR.bold_white(process.pid),
- line.decode(errors='replace').rstrip())
+ _LOG.log(
+ pw_cli.log.LOGLEVEL_STDOUT,
+ '[%s] %s',
+ _COLOR.bold_white(process.pid),
+ line.decode(errors='replace').rstrip(),
+ )
return process, bytes(output)
-async def run_async(program: str,
- *args: str,
- log_output: bool = False,
- timeout: Optional[float] = None) -> CompletedProcess:
+async def _kill_process_and_children(
+ process: 'asyncio.subprocess.Process',
+) -> None:
+ """Kills child processes of a process with PID `pid`."""
+ # Look up child processes before sending the kill signal to the parent,
+ # as otherwise the child lookup would fail.
+ pid = process.pid
+ try:
+ process_util = psutil.Process(pid=pid)
+ kill_list = list(process_util.children(recursive=True))
+ except psutil.NoSuchProcess:
+ # Creating the kill list raced with parent process completion.
+ #
+ # Don't bother cleaning up child processes of parent processes that
+ # exited on their own.
+ kill_list = []
+
+ for proc in kill_list:
+ try:
+ proc.kill()
+ except psutil.NoSuchProcess:
+ pass
+
+ def wait_for_all() -> None:
+ for proc in kill_list:
+ try:
+ proc.wait()
+ except psutil.NoSuchProcess:
+ pass
+
+ # Wait for process completion on the executor to avoid blocking the
+ # event loop.
+ loop = asyncio.get_event_loop()
+ wait_for_children = loop.run_in_executor(None, wait_for_all)
+
+ # Send a kill signal to the main process before waiting for the children
+ # to complete.
+ try:
+ process.kill()
+ await process.wait()
+ except ProcessLookupError:
+ _LOG.debug(
+ 'Process completed before it could be killed. '
+ 'This may have been caused by the killing one of its '
+ 'child subprocesses.',
+ )
+
+ await wait_for_children
+
+
+async def run_async(
+ program: str,
+ *args: str,
+ env: Optional[Dict[str, str]] = None,
+ log_output: bool = False,
+ timeout: Optional[float] = None,
+) -> CompletedProcess:
"""Runs a command, capturing and optionally logging its output.
+ Args:
+ program: The program to run in a new process.
+ args: The arguments to pass to the program.
+ env: An optional mapping of environment variables within which to run
+ the process.
+ log_output: Whether to log stdout and stderr of the process to this
+ process's stdout (prefixed with the PID of the subprocess from which
+ the output originated). If unspecified, the child process's stdout
+ and stderr will be captured, and both will be stored in the returned
+ `CompletedProcess`'s output`.
+ timeout: An optional floating point number of seconds to allow the
+ subprocess to run before killing it and its children. If unspecified,
+ the subprocess will be allowed to continue exiting until it completes.
+
Returns a CompletedProcess with details from the process.
"""
- _LOG.debug('Running `%s`',
- ' '.join(shlex.quote(arg) for arg in [program, *args]))
+ _LOG.debug(
+ 'Running `%s`', ' '.join(shlex.quote(arg) for arg in [program, *args])
+ )
- env = os.environ.copy()
- env[PW_SUBPROCESS_ENV] = '1'
+ hydrated_env = os.environ.copy()
+ if env is not None:
+ for key, value in env.items():
+ hydrated_env[key] = value
+ hydrated_env[PW_SUBPROCESS_ENV] = '1'
output: Union[bytes, IO[bytes]]
if log_output:
- process, output = await _run_and_log(program, args, env)
+ process, output = await _run_and_log(program, args, hydrated_env)
else:
output = tempfile.TemporaryFile()
process = await asyncio.create_subprocess_exec(
@@ -101,19 +188,19 @@ async def run_async(program: str,
*args,
stdout=output,
stderr=asyncio.subprocess.STDOUT,
- env=env)
+ env=hydrated_env,
+ )
try:
await asyncio.wait_for(process.wait(), timeout)
except asyncio.TimeoutError:
_LOG.error('%s timed out after %d seconds', program, timeout)
- process.kill()
- await process.wait()
+ await _kill_process_and_children(process)
if process.returncode:
_LOG.error('%s exited with status %d', program, process.returncode)
else:
- _LOG.info('%s exited successfully', program)
+ _LOG.error('%s exited successfully', program)
return CompletedProcess(process, output)
diff --git a/pw_cli/py/pw_cli/pw_command_plugins.py b/pw_cli/py/pw_cli/pw_command_plugins.py
index 845e0078f..99b86fdd8 100644
--- a/pw_cli/py/pw_cli/pw_command_plugins.py
+++ b/pw_cli/py/pw_cli/pw_command_plugins.py
@@ -29,12 +29,15 @@ def _register_builtin_plugins(registry: plugins.Registry) -> None:
"""Registers the commands that are included with pw by default."""
# Register these by name to avoid circular dependencies.
+ registry.register_by_name('bloat', 'pw_bloat.__main__', 'main')
registry.register_by_name('doctor', 'pw_doctor.doctor', 'main')
- registry.register_by_name('python-packages',
- 'pw_env_setup.python_packages', 'main')
registry.register_by_name('format', 'pw_presubmit.format_code', 'main')
+ registry.register_by_name('keep-sorted', 'pw_presubmit.keep_sorted', 'main')
registry.register_by_name('logdemo', 'pw_cli.log', 'main')
- registry.register_by_name('module-check', 'pw_module.check', 'main')
+ registry.register_by_name('module', 'pw_module.__main__', 'main')
+ registry.register_by_name(
+ 'python-packages', 'pw_env_setup.python_packages', 'main'
+ )
registry.register_by_name('test', 'pw_unit_test.test_runner', 'main')
registry.register_by_name('watch', 'pw_watch.watch', 'main')
@@ -44,10 +47,12 @@ def _register_builtin_plugins(registry: plugins.Registry) -> None:
def _help_command():
"""Display detailed information about pw commands."""
parser = argparse.ArgumentParser(description=_help_command.__doc__)
- parser.add_argument('plugins',
- metavar='plugin',
- nargs='*',
- help='command for which to display detailed info')
+ parser.add_argument(
+ 'plugins',
+ metavar='plugin',
+ nargs='*',
+ help='command for which to display detailed info',
+ )
print(arguments.format_help(_plugin_registry), file=sys.stderr)
@@ -58,7 +63,8 @@ def _help_command():
def register(directory: Path) -> None:
_register_builtin_plugins(_plugin_registry)
_plugin_registry.register_directory(
- directory, REGISTRY_FILE, Path(os.environ.get('PW_PROJECT_ROOT', '')))
+ directory, REGISTRY_FILE, Path(os.environ.get('PW_PROJECT_ROOT', ''))
+ )
def errors() -> dict:
diff --git a/pw_cli/py/pw_cli/requires.py b/pw_cli/py/pw_cli/requires.py
index bf67dd345..79a2b61de 100755
--- a/pw_cli/py/pw_cli/requires.py
+++ b/pw_cli/py/pw_cli/requires.py
@@ -27,6 +27,7 @@ For more see http://go/pigweed-ci-cq-intro.
"""
import argparse
+import json
import logging
from pathlib import Path
import re
@@ -40,11 +41,13 @@ HELPER_PROJECT = 'requires-helper'
HELPER_REPO = 'sso://{}/{}'.format(HELPER_GERRIT, HELPER_PROJECT)
# Pass checks that look for "DO NOT ..." and block submission.
-_DNS = ' '.join((
- 'DO',
- 'NOT',
- 'SUBMIT',
-))
+_DNS = ' '.join(
+ (
+ 'DO',
+ 'NOT',
+ 'SUBMIT',
+ )
+)
# Subset of the output from pushing to Gerrit.
DEFAULT_OUTPUT = f'''
@@ -100,17 +103,24 @@ def clone(requires_dir: Path) -> None:
def create_commit(requires_dir: Path, requirements) -> None:
+ """Create a commit in the local tree with the given requirements."""
change_id = str(uuid.uuid4()).replace('-', '00')
_LOG.debug('change_id %s', change_id)
- path = requires_dir / change_id
+
+ reqs = []
+ for req in requirements:
+ gerrit_name, number = req.split(':', 1)
+ reqs.append({'gerrit_name': gerrit_name, 'number': number})
+
+ path = requires_dir / 'patches.json'
_LOG.debug('path %s', path)
- with open(path, 'w'):
- pass
+ with open(path, 'w') as outs:
+ json.dump(reqs, outs)
_run_command(['git', 'add', path], cwd=requires_dir)
commit_message = [
- f'{_DNS} {change_id[0:10]}',
+ f'{_DNS} {change_id[0:10]}\n\n',
'',
f'Change-Id: I{change_id}',
]
diff --git a/pw_cli/py/pw_cli/toml_config_loader_mixin.py b/pw_cli/py/pw_cli/toml_config_loader_mixin.py
new file mode 100644
index 000000000..bac57ae24
--- /dev/null
+++ b/pw_cli/py/pw_cli/toml_config_loader_mixin.py
@@ -0,0 +1,50 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Toml config file loader mixin."""
+
+from typing import Any, Dict, List
+
+import toml # type: ignore
+
+from pw_cli.yaml_config_loader_mixin import YamlConfigLoaderMixin
+
+
+class TomlConfigLoaderMixin(YamlConfigLoaderMixin):
+ """TOML Config file loader mixin.
+
+ Use this mixin to load toml file settings and save them into
+ ``self._config``. For example:
+
+ ::
+
+ from pw_cli.toml_config_loader_mixin import TomlConfigLoaderMixin
+
+ class PwBloatPrefs(TomlConfigLoaderMixin):
+ def __init__(self) -> None:
+ self.config_init(
+ config_section_title='pw_bloat',
+ project_file=Path('$PW_PROJECT_ROOT/.pw_bloat.toml'),
+ project_user_file=Path(
+ '$PW_PROJECT_ROOT/.pw_bloat.user.toml'),
+ user_file=Path('~/.pw_bloat.toml'),
+ default_config={},
+ environment_var='PW_BLOAT_CONFIG_FILE',
+ )
+
+ """
+
+ def _load_config_from_string( # pylint: disable=no-self-use
+ self, file_contents: str
+ ) -> List[Dict[Any, Any]]:
+ return [toml.loads(file_contents)]
diff --git a/pw_console/py/pw_console/yaml_config_loader_mixin.py b/pw_cli/py/pw_cli/yaml_config_loader_mixin.py
index ff009a0f9..4bbf47bef 100644
--- a/pw_console/py/pw_console/yaml_config_loader_mixin.py
+++ b/pw_cli/py/pw_cli/yaml_config_loader_mixin.py
@@ -16,7 +16,7 @@
import os
import logging
from pathlib import Path
-from typing import Any, Dict, Optional, Union
+from typing import Any, Dict, List, Optional, Union
import yaml
@@ -36,7 +36,7 @@ class YamlConfigLoaderMixin:
::
class ConsolePrefs(YamlConfigLoaderMixin):
- def __init__(
+ def __init__(self) -> None:
self.config_init(
config_section_title='pw_console',
project_file=Path('project_file.yaml'),
@@ -47,12 +47,13 @@ class YamlConfigLoaderMixin:
)
"""
+
def config_init(
self,
config_section_title: str,
- project_file: Union[Path, bool] = None,
- project_user_file: Union[Path, bool] = None,
- user_file: Union[Path, bool] = None,
+ project_file: Optional[Union[Path, bool]] = None,
+ project_user_file: Optional[Union[Path, bool]] = None,
+ user_file: Optional[Union[Path, bool]] = None,
default_config: Optional[Dict[Any, Any]] = None,
environment_var: Optional[str] = None,
) -> None:
@@ -101,17 +102,20 @@ class YamlConfigLoaderMixin:
if project_file and isinstance(project_file, Path):
self.project_file = Path(
- os.path.expandvars(str(project_file.expanduser())))
+ os.path.expandvars(str(project_file.expanduser()))
+ )
self.load_config_file(self.project_file)
if project_user_file and isinstance(project_user_file, Path):
self.project_user_file = Path(
- os.path.expandvars(str(project_user_file.expanduser())))
+ os.path.expandvars(str(project_user_file.expanduser()))
+ )
self.load_config_file(self.project_user_file)
if user_file and isinstance(user_file, Path):
self.user_file = Path(
- os.path.expandvars(str(user_file.expanduser())))
+ os.path.expandvars(str(user_file.expanduser()))
+ )
self.load_config_file(self.user_file)
# Check for a config file specified by an environment variable.
@@ -122,7 +126,8 @@ class YamlConfigLoaderMixin:
env_file_path = Path(environment_config)
if not env_file_path.is_file():
raise FileNotFoundError(
- f'Cannot load config file: {env_file_path}')
+ f'Cannot load config file: {env_file_path}'
+ )
self.reset_config()
self.load_config_file(env_file_path)
@@ -135,11 +140,16 @@ class YamlConfigLoaderMixin:
self._config: Dict[Any, Any] = {}
self._update_config(self.default_config)
+ def _load_config_from_string( # pylint: disable=no-self-use
+ self, file_contents: str
+ ) -> List[Dict[Any, Any]]:
+ return list(yaml.safe_load_all(file_contents))
+
def load_config_file(self, file_path: Path) -> None:
if not file_path.is_file():
return
- cfgs = yaml.safe_load_all(file_path.read_text())
+ cfgs = self._load_config_from_string(file_path.read_text())
for cfg in cfgs:
if self._config_section_title in cfg:
@@ -149,6 +159,8 @@ class YamlConfigLoaderMixin:
self._update_config(cfg)
else:
raise MissingConfigTitle(
- '\n\nThe YAML config file "{}" is missing the expected '
+ '\n\nThe config file "{}" is missing the expected '
'"config_title: {}" setting.'.format(
- str(file_path), self._config_section_title))
+ str(file_path), self._config_section_title
+ )
+ )
diff --git a/pw_cli/py/setup.cfg b/pw_cli/py/setup.cfg
index be8343850..20af8de6f 100644
--- a/pw_cli/py/setup.cfg
+++ b/pw_cli/py/setup.cfg
@@ -21,6 +21,10 @@ description = Pigweed swiss-army knife
[options]
packages = find:
zip_safe = False
+install_requires =
+ psutil
+ pyyaml
+ toml
[options.entry_points]
console_scripts = pw = pw_cli.__main__:main
diff --git a/pw_compilation_testing/BUILD.bazel b/pw_compilation_testing/BUILD.bazel
new file mode 100644
index 000000000..5a6fb9f09
--- /dev/null
+++ b/pw_compilation_testing/BUILD.bazel
@@ -0,0 +1,28 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+licenses(["notice"])
+
+# Negative compilation testing is not yet supported in Bazel.
+pw_cc_library(
+ name = "negative_compilation_testing",
+ hdrs = ["public/pw_compilation_testing/negative_compilation.h"],
+ includes = ["public"],
+ visibility = ["//:__subpackages__"], # Restrict to Pigweed
+)
diff --git a/pw_compilation_testing/BUILD.gn b/pw_compilation_testing/BUILD.gn
new file mode 100644
index 000000000..8040359df
--- /dev/null
+++ b/pw_compilation_testing/BUILD.gn
@@ -0,0 +1,45 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("negative_compilation_testing") {
+ public = [ "public/pw_compilation_testing/negative_compilation.h" ]
+ public_configs = [ ":public_include_path" ]
+ visibility = [ ":internal_pigweed_use_only" ]
+}
+
+# The pw_compilation_testing header cannot be used outside of a negative
+# compilation test. To create a negative compilation test, use the
+# pw_cc_negative_compilation_test() GN template or specify
+# negative_compilation_test = true in a pw_test() template.
+group("internal_pigweed_use_only") {
+ public_deps = [ ":negative_compilation_testing" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_compilation_testing/CMakeLists.txt b/pw_compilation_testing/CMakeLists.txt
new file mode 100644
index 000000000..ccf5ee831
--- /dev/null
+++ b/pw_compilation_testing/CMakeLists.txt
@@ -0,0 +1,23 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Negative compilation testing is not yet supported in CMake.
+pw_add_library(pw_compilation_testing._pigweed_only_negative_compilation INTERFACE
+ HEADERS
+ public/pw_compilation_testing/negative_compilation.h
+ PUBLIC_INCLUDES
+ public
+)
diff --git a/pw_compilation_testing/OWNERS b/pw_compilation_testing/OWNERS
new file mode 100644
index 000000000..d96cbc68d
--- /dev/null
+++ b/pw_compilation_testing/OWNERS
@@ -0,0 +1 @@
+hepler@google.com
diff --git a/pw_compilation_testing/docs.rst b/pw_compilation_testing/docs.rst
new file mode 100644
index 000000000..c08b985b9
--- /dev/null
+++ b/pw_compilation_testing/docs.rst
@@ -0,0 +1,195 @@
+.. _module-pw_compilation_testing:
+
+======================
+pw_compilation_testing
+======================
+The pw_compilation_testing module provides for negative compilation (NC)
+testing. Negative compilation tests ensure that code that should not compile
+does not compile. Negative compilation testing is helpful in a variety of
+scenarios, for example:
+
+- Testing for compiler errors, such as ``[[nodiscard]]`` checks.
+- Testing that a template cannot be instantiated with certain types.
+- Testing that a ``static_assert`` statement is triggered as expected.
+- For a ``constexpr`` function, testing that a ``PW_ASSERT`` is triggered as
+ expected.
+
+Negative compilation tests are only supported in GN currently. Negative
+compilation tests are not currently supported in GN on Windows due to
+`b/241565082 <https://issues.pigweed.dev/241565082>`_.
+
+.. warning::
+
+ This module is in an early, experimental state. Do not use it unless you have
+ consulted with the Pigweed team.
+
+---------------------------------
+Negative compilation test example
+---------------------------------
+.. code-block:: cpp
+
+ #include "gtest/gtest.h"
+ #include "pw_compilation_testing/negative_compilation.h"
+
+ template <int kValue>
+ struct MyStruct {
+ static_assert(kValue % 2 == 0, "wrong number!");
+
+ constexpr int MultiplyOdd(int runtime_value) const {
+ PW_ASSERT(runtime_value % 2 == 0);
+ return kValue * runtime_value;
+ }
+ };
+
+ [[maybe_unused]] MyStruct<16> this_one_works;
+
+ // NC tests cannot be compiled, so they are created in preprocessor #if or
+ // #elif blocks. These NC tests check that a static_assert statement fails if
+ // the code is compiled.
+ #if PW_NC_TEST(NegativeOddNumber)
+ PW_NC_EXPECT("wrong number!");
+ [[maybe_unused]] MyStruct<-1> illegal;
+ #elif PW_NC_TEST(PositiveOddNumber)
+ PW_NC_EXPECT("wrong number!");
+ [[maybe_unused]] MyStruct<5> this_is_illegal;
+ #endif // PW_NC_TEST
+
+ struct Foo {
+ // Negative compilation tests can go anywhere in a source file.
+ #if PW_NC_TEST(IllegalValueAsClassMember)
+ PW_NC_EXPECT("wrong number!");
+ MyStruct<12> also_illegal;
+ #endif // PW_NC_TEST
+ };
+
+ TEST(MyStruct, MultiplyOdd) {
+ MyStruct<5> five;
+ EXPECT_EQ(five.MultiplyOdd(3), 15);
+
+ // This NC test checks that a specific PW_ASSERT() fails when expected.
+ // This only works in an NC test if the PW_ASSERT() fails while the compiler
+ // is executing constexpr code. The test code is used in a constexpr
+ // statement to force compile-time evaluation.
+ #if PW_NC_TEST(MyStruct_MultiplyOdd_AssertsOnOddNumber)
+ [[maybe_unused]] constexpr auto fail = [] {
+ PW_NC_EXPECT("PW_ASSERT\(runtime_value % 2 == 0\);");
+ MyStruct<3> my_struct;
+ return my_struct.MultiplyOdd(4); // Even number, PW_ASSERT should fail.
+ }();
+ #endif // PW_NC_TEST
+ }
+
+------------------------------------
+Creating a negative compilation test
+------------------------------------
+- Declare a ``pw_cc_negative_compilation_test()`` GN target or set
+ ``negative_compilation_test = true`` in a ``pw_test()`` target.
+- Add the test to the build in a toolchain with negative compilation testing
+ enabled (``pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = true``).
+- In the test source files, add
+ ``#include "pw_compilation_testing/negative_compilation.h"``.
+- Use the ``PW_NC_TEST(TestName)`` macro in a ``#if`` statement.
+- Immediately after the ``PW_NC_TEST(TestName)``, provide one or more
+ Python-style regular expressions with the ``PW_NC_EXPECT()`` macro, one per
+ line.
+- Execute the tests by running the build.
+
+To simplify parsing, all ``PW_NC_TEST()`` statements must fit on a single line.
+``PW_NC_EXPECT()`` statements may span multiple lines, but must contain a single
+regular expression as a string literal. The string may be comprised of multiple
+implicitly concatenated string literals. The ``PW_NC_EXPECT()`` statement cannot
+contain anything else except for ``//``-style comments.
+
+Test assertions
+===============
+Negative compilation tests must have at least one assertion about the
+compilation output. The assertion macros must be placed immediately after the
+line with the ``PW_NC_TEST()`` or the test will fail.
+
+.. c:macro:: PW_NC_EXPECT(regex_string_literal)
+
+ When negative compilation tests are run, checks the compilation output for the
+ provided regular expression. The argument to the ``PW_NC_EXPECT()`` statement
+ must be a string literal. The literal is interpreted character-for-character
+ as a Python raw string literal and compiled as a Python `re
+ <https://docs.python.org/3/library/re.html>`_ regular expression.
+
+ For example, ``PW_NC_EXPECT("something (went|has gone) wrong!")`` searches the
+ failed compilation output with the Python regular expression
+ ``re.compile("something (went|has gone) wrong!")``.
+
+.. c:macro:: PW_NC_EXPECT_GCC(regex_string_literal)
+
+ Same as :c:macro:`PW_NC_EXPECT`, but only applies when compiling with GCC.
+
+.. c:macro:: PW_NC_EXPECT_CLANG(regex_string_literal)
+
+ Same as :c:macro:`PW_NC_EXPECT`, but only applies when compiling with Clang.
+
+.. admonition:: Test expectation tips
+ :class: tip
+
+ Be as specific as possible, but avoid compiler-specific error text. Try
+ matching against the following:
+
+ - ``static_assert`` messages.
+ - Contents of specific failing lines of source code:
+ ``PW_NC_EXPECT("PW_ASSERT\(!empty\(\));")``.
+ - Comments on affected lines: ``PW_NC_EXPECT("// Cannot construct from
+ nullptr")``.
+ - Function names: ``PW_NC_EXPECT("SomeFunction\(\).*private")``.
+
+ Do not match against the following:
+
+ - Source file paths.
+ - Source line numbers.
+ - Compiler-specific wording of error messages, except when necessary.
+
+------
+Design
+------
+The basic flow for negative compilation testing is as follows.
+
+- The user defines negative compilation tests in preprocessor ``#if`` blocks
+ using the ``PW_NC_TEST()`` and :c:macro:`PW_NC_EXPECT` macros.
+- The build invokes the ``pw_compilation_testing.generator`` script. The
+ generator script:
+
+ - finds ``PW_NC_TEST()`` statements and extracts a list of test cases,
+ - finds all associated :c:macro:`PW_NC_EXPECT` statements, and
+ - generates build targets for each negative compilation tests,
+ passing the test information and expectations to the targets.
+
+- The build compiles the test source file with all tests disabled.
+- The build invokes the negative compilation test targets, which run the
+ ``pw_compilation_testing.runner`` script. The test runner script:
+
+ - invokes the compiler, setting a preprocessor macro that enables the ``#if``
+ block for the test.
+ - captures the compilation output, and
+ - checks the compilation output for the :c:macro:`PW_NC_EXPECT` expressions.
+
+- If compilation failed, and the output matches the test case's
+ :c:macro:`PW_NC_EXPECT` expressions, the test passes.
+- If compilation succeeded or the :c:macro:`PW_NC_EXPECT` expressions did not
+ match the output, the test fails.
+
+Existing frameworks
+===================
+Pigweed's negative compilation tests were inspired by Chromium's `no-compile
+tests <https://www.chromium.org/developers/testing/no-compile-tests/>`_
+tests and a similar framework used internally at Google. Pigweed's negative
+compilation testing framework improves on these systems in a few respects:
+
+- Trivial integration with unit tests. Negative compilation tests can easily be
+ placed alongside other unit tests instead of in separate files.
+- Safer, more natural macro-based API for test declarations. Other systems use
+ ``#ifdef`` macro checks to define test cases, which fail silently when there
+ are typos. Pigweed's framework uses function-like macros, which provide a
+ clean and natural API, catch typos, and ensure the test is integrated with the
+ NC test framework.
+- More readable, flexible test assertions. Other frameworks place assertions in
+ comments after test names, while Pigweed's framework uses function-like
+ macros. Pigweed also supports compiler-specific assertions.
+- Assertions are required. This helps ensure that compilation fails for the
+ expected reason and not for an accidental typo or unrelated issue.
diff --git a/pw_compilation_testing/negative_compilation_test.gni b/pw_compilation_testing/negative_compilation_test.gni
new file mode 100644
index 000000000..5569cef4c
--- /dev/null
+++ b/pw_compilation_testing/negative_compilation_test.gni
@@ -0,0 +1,90 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python_action.gni")
+import("$dir_pw_build/target_types.gni")
+
+declare_args() {
+ # Enables or disables negative compilation tests for the current toolchain.
+ # Disabled by default since negative compilation tests increase gn gen time
+ # significantly.
+ pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = false
+}
+
+# Declares a compilation failure test. If
+# pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED is true, negative
+# complilation tests will be executed in the build. These tests pass if
+# compilation fails when #if block test cases are enabled in the code.
+#
+# Compilation failure tests may also be declared as part of a unit test in
+# pw_test by settin negative_compilation_tests = true.
+template("pw_cc_negative_compilation_test") {
+ assert(defined(invoker.sources) && invoker.sources != [],
+ "pw_cc_negative_compilation_test requires 'sources' to be provided")
+
+ if (pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED) {
+ _out_dir = "$target_gen_dir/$target_name"
+
+ _args = [
+ "--output",
+ rebase_path(_out_dir, root_build_dir),
+ "--base",
+ get_label_info(":$target_name._base", "label_no_toolchain"),
+ "--name",
+ target_name,
+ ]
+
+ # List the source files as both a GN path and a file system path.
+ foreach(file, invoker.sources) {
+ _args += [ get_path_info(file, "abspath") + ";" +
+ rebase_path(file, root_build_dir) ]
+ }
+
+ # Invoke the generator script, which generates a BUILD.gn file with a GN
+ # action for each NC test. The test names are returned as a list.
+ _tests = exec_script(
+ "$dir_pw_compilation_testing/py/pw_compilation_testing/generator.py",
+ _args,
+ "list lines",
+ invoker.sources)
+
+ # Create a group of the generated NC test targets.
+ group(target_name) {
+ deps = []
+ foreach(test, _tests) {
+ deps += [ "$_out_dir:$target_name.$test.negative_compilation_test" ]
+ }
+ }
+ } else {
+ # If compilation testing is disabled, only compile the base file, which the
+ # negative compilation test targets depend on. Use an action for this target
+ # so that depending on it will not link in any source files.
+ pw_python_action(target_name) {
+ script = "$dir_pw_build/py/pw_build/nop.py"
+ stamp = true
+ deps = [ ":$target_name._base" ]
+ }
+ }
+
+ # The base target is the sources with no tests enabled.
+ pw_source_set(target_name + "._base") {
+ forward_variables_from(invoker, "*")
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ "$dir_pw_compilation_testing:internal_pigweed_use_only" ]
+ }
+}
diff --git a/pw_compilation_testing/public/pw_compilation_testing/negative_compilation.h b/pw_compilation_testing/public/pw_compilation_testing/negative_compilation.h
new file mode 100644
index 000000000..c9c2f458b
--- /dev/null
+++ b/pw_compilation_testing/public/pw_compilation_testing/negative_compilation.h
@@ -0,0 +1,49 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#if defined(PW_NEGATIVE_COMPILATION_TESTS_ENABLED) && \
+ PW_NEGATIVE_COMPILATION_TESTS_ENABLED == 1
+
+// Declares a compilation failure test. Must be used in a #if or #elif
+// statement. The code in the section must not compile.
+//
+// Internally, this expands to a macro that enables or disables the code section
+// for the compilation failure test. It would be possible to use a plain macro
+// and defined(...) for this, but the function-like macro gives a cleaner
+// interface and catches typos that would otherwise cause tests to silently be
+// skipped.
+#define PW_NC_TEST(test_case) PW_NC_TEST_EXECUTE_CASE_##test_case
+
+#else
+
+// If testing is disbaled, always evaluate false to disable the test case.
+#define PW_NC_TEST(test_case) 0 && PW_NC_TEST_EXECUTE_CASE_##test_case
+
+#endif // PW_NEGATIVE_COMPILATION_TESTS_ENABLED
+
+// Checks that the compilation output matches the provided regex in a negative
+// compilation test. The regex must be a simple string literal. In Python, the
+// string is taken directly from the C++ source, interpreted as a regular string
+// literal, and compiled as a regular expression.
+#define PW_NC_EXPECT(regex) \
+ static_assert(PW_NEGATIVE_COMPILATION_TESTS_ENABLED == 1, "")
+
+// Checks that the compilation output matches the regex in Clang compilers only.
+#define PW_NC_EXPECT_CLANG(regex) \
+ static_assert(PW_NEGATIVE_COMPILATION_TESTS_ENABLED == 1, "")
+
+// Checks that the compilation output matches the regex in GCC compilers only.
+#define PW_NC_EXPECT_GCC(regex) \
+ static_assert(PW_NEGATIVE_COMPILATION_TESTS_ENABLED == 1, "")
diff --git a/pw_compilation_testing/py/BUILD.gn b/pw_compilation_testing/py/BUILD.gn
new file mode 100644
index 000000000..7a8bbed73
--- /dev/null
+++ b/pw_compilation_testing/py/BUILD.gn
@@ -0,0 +1,34 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ "setup.py",
+ ]
+ sources = [
+ "pw_compilation_testing/__init__.py",
+ "pw_compilation_testing/generator.py",
+ "pw_compilation_testing/runner.py",
+ ]
+ tests = [ "generator_test.py" ]
+ python_deps = [ "$dir_pw_cli/py" ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+}
diff --git a/pw_compilation_testing/py/generator_test.py b/pw_compilation_testing/py/generator_test.py
new file mode 100644
index 000000000..99fd8211f
--- /dev/null
+++ b/pw_compilation_testing/py/generator_test.py
@@ -0,0 +1,122 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for the negative compilation test generator."""
+
+from pathlib import Path
+import re
+import tempfile
+import unittest
+
+from pw_compilation_testing.generator import (
+ Compiler,
+ Expectation,
+ ParseError,
+ TestCase,
+ enumerate_tests,
+)
+
+SOURCE = r'''
+#if PW_NC_TEST(FirstTest)
+PW_NC_EXPECT("abcdef");
+
+SomeSourceCode();
+
+#endif // PW_NC_TEST
+
+#if PW_NC_TEST(SecondTest)
+PW_NC_EXPECT("\"\"abc123" // Include " and other escapes in the string
+ "def'456\""
+ // Goodbye
+ "ghi\n\t789" // ???
+); // abc
+
+#endif // PW_NC_TEST
+'''
+
+ILLEGAL_COMMENT = '''
+#if PW_NC_TEST(FirstTest)
+PW_NC_EXPECT("abcdef" /* illegal comment */);
+
+#endif // PW_NC_TEST
+'''
+
+UNTERMINATED_EXPECTATION = '#if PW_NC_TEST(FirstTest)\nPW_NC_EXPECT("abcdef"\n'
+
+
+def _write_to_temp_file(contents: str) -> Path:
+ file = tempfile.NamedTemporaryFile('w', delete=False)
+ file.write(contents)
+ file.close()
+ return Path(file.name)
+
+
+# pylint: disable=missing-function-docstring
+
+
+class ParserTest(unittest.TestCase):
+ """Tests parsing negative compilation tests from a file."""
+
+ def test_successful(self) -> None:
+ try:
+ path = _write_to_temp_file(SOURCE)
+
+ self.assertEqual(
+ [
+ TestCase(
+ 'TestSuite',
+ 'FirstTest',
+ (Expectation(Compiler.ANY, re.compile('abcdef'), 3),),
+ path,
+ 2,
+ ),
+ TestCase(
+ 'TestSuite',
+ 'SecondTest',
+ (
+ Expectation(
+ Compiler.ANY,
+ re.compile('""abc123def\'456"ghi\\n\\t789'),
+ 10,
+ ),
+ ),
+ path,
+ 9,
+ ),
+ ],
+ list(enumerate_tests('TestSuite', [path])),
+ )
+ finally:
+ path.unlink()
+
+ def test_illegal_comment(self) -> None:
+ try:
+ path = _write_to_temp_file(ILLEGAL_COMMENT)
+ with self.assertRaises(ParseError):
+ list(enumerate_tests('TestSuite', [path]))
+ finally:
+ path.unlink()
+
+ def test_unterminated_expectation(self) -> None:
+ try:
+ path = _write_to_temp_file(UNTERMINATED_EXPECTATION)
+ with self.assertRaises(ParseError) as err:
+ list(enumerate_tests('TestSuite', [path]))
+ finally:
+ path.unlink()
+
+ self.assertIn('Unterminated', str(err.exception))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_compilation_testing/py/pw_compilation_testing/__init__.py b/pw_compilation_testing/py/pw_compilation_testing/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_compilation_testing/py/pw_compilation_testing/__init__.py
diff --git a/pw_compilation_testing/py/pw_compilation_testing/generator.py b/pw_compilation_testing/py/pw_compilation_testing/generator.py
new file mode 100644
index 000000000..fbf66f90e
--- /dev/null
+++ b/pw_compilation_testing/py/pw_compilation_testing/generator.py
@@ -0,0 +1,435 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Generates compile fail test GN targets.
+
+Scans source files for PW_NC_TEST(...) statements and generates a
+BUILD.gn file with a target for each test. This allows the compilation failure
+tests to run in parallel in Ninja.
+
+This file is executed during gn gen, so it cannot rely on any setup that occurs
+during the build.
+"""
+
+import argparse
+import base64
+from collections import defaultdict
+from dataclasses import dataclass
+from enum import Enum
+from pathlib import Path
+import pickle
+import re
+import sys
+from typing import (
+ Iterable,
+ Iterator,
+ List,
+ NamedTuple,
+ NoReturn,
+ Optional,
+ Pattern,
+ Sequence,
+ Set,
+ Tuple,
+)
+
+# Matches the #if or #elif statement that starts a compile fail test.
+_TEST_START = re.compile(r'^[ \t]*#[ \t]*(?:el)?if[ \t]+PW_NC_TEST\([ \t]*')
+
+# Matches the name of a test case.
+_TEST_NAME = re.compile(
+ r'(?P<name>[a-zA-Z0-9_]+)[ \t]*\)[ \t]*(?://.*|/\*.*)?$'
+)
+
+# Negative compilation test commands take the form PW_NC_EXPECT("regex"),
+# PW_NC_EXPECT_GCC("regex"), or PW_NC_EXPECT_CLANG("regex"). PW_NC_EXPECT() is
+# an error.
+_EXPECT_START = re.compile(r'^[ \t]*PW_NC_EXPECT(?P<compiler>_GCC|_CLANG)?\(')
+
+# EXPECT statements are regular expressions that must match the compiler output.
+# They must fit on a single line.
+_EXPECT_REGEX = re.compile(r'(?P<regex>"[^\n]+")\);[ \t]*(?://.*|/\*.*)?$')
+
+
+class Compiler(Enum):
+ ANY = 0
+ GCC = 1
+ CLANG = 2
+
+ @staticmethod
+ def from_command(command: str) -> 'Compiler':
+ if command.endswith(('clang', 'clang++')):
+ return Compiler.CLANG
+
+ if command.endswith(('gcc', 'g++')):
+ return Compiler.GCC
+
+ raise ValueError(
+ f"Unrecognized compiler '{command}'; update the Compiler enum "
+ f'in {Path(__file__).name} to account for this'
+ )
+
+ def matches(self, other: 'Compiler') -> bool:
+ return self is other or self is Compiler.ANY or other is Compiler.ANY
+
+
+@dataclass(frozen=True)
+class Expectation:
+ compiler: Compiler
+ pattern: Pattern[str]
+ line: int
+
+
+@dataclass(frozen=True)
+class TestCase:
+ suite: str
+ case: str
+ expectations: Tuple[Expectation, ...]
+ source: Path
+ line: int
+
+ def name(self) -> str:
+ return f'{self.suite}.{self.case}'
+
+ def serialize(self) -> str:
+ return base64.b64encode(pickle.dumps(self)).decode()
+
+ @classmethod
+ def deserialize(cls, serialized: str) -> 'Expectation':
+ return pickle.loads(base64.b64decode(serialized))
+
+
+class ParseError(Exception):
+ """Failed to parse a PW_NC_TEST."""
+
+ def __init__(
+ self,
+ message: str,
+ file: Path,
+ lines: Sequence[str],
+ error_lines: Sequence[int],
+ ) -> None:
+ for i in error_lines:
+ message += f'\n{file.name}:{i + 1}: {lines[i]}'
+ super().__init__(message)
+
+
+class _ExpectationParser:
+ """Parses expecatations from 'PW_NC_EXPECT(' to the final ');'."""
+
+ class _State:
+ SPACE = 0 # Space characters, which are ignored
+ COMMENT_START = 1 # First / in a //-style comment
+ COMMENT = 2 # Everything after // on a line
+ OPEN_QUOTE = 3 # Starting quote for a string literal
+ CHARACTERS = 4 # Characters within a string literal
+ ESCAPE = 5 # \ within a string literal, which may escape a "
+ CLOSE_PAREN = 6 # Closing parenthesis to the PW_NC_EXPECT statement.
+
+ def __init__(self, index: int, compiler: Compiler) -> None:
+ self.index = index
+ self._compiler = compiler
+ self._state = self._State.SPACE
+ self._contents: List[str] = []
+
+ def parse(self, chars: str) -> Optional[Expectation]:
+ """State machine that parses characters in PW_NC_EXPECT()."""
+ for char in chars:
+ if self._state is self._State.SPACE:
+ if char == '"':
+ self._state = self._State.CHARACTERS
+ elif char == ')':
+ self._state = self._State.CLOSE_PAREN
+ elif char == '/':
+ self._state = self._State.COMMENT_START
+ elif not char.isspace():
+ raise ValueError(f'Unexpected character "{char}"')
+ elif self._state is self._State.COMMENT_START:
+ if char == '*':
+ raise ValueError(
+ '"/* */" comments are not supported; use // instead'
+ )
+ if char != '/':
+ raise ValueError(f'Unexpected character "{char}"')
+ self._state = self._State.COMMENT
+ elif self._state is self._State.COMMENT:
+ if char == '\n':
+ self._state = self._State.SPACE
+ elif self._state is self._State.CHARACTERS:
+ if char == '"':
+ self._state = self._State.SPACE
+ elif char == '\\':
+ self._state = self._State.ESCAPE
+ else:
+ self._contents.append(char)
+ elif self._state is self._State.ESCAPE:
+ # Include escaped " directly. Restore the \ for other chars.
+ if char != '"':
+ self._contents.append('\\')
+ self._contents.append(char)
+ self._state = self._State.CHARACTERS
+ elif self._state is self._State.CLOSE_PAREN:
+ if char != ';':
+ raise ValueError(f'Expected ";", found "{char}"')
+
+ return self._expectation(''.join(self._contents))
+
+ return None
+
+ def _expectation(self, regex: str) -> Expectation:
+ if '"""' in regex:
+ raise ValueError('The regular expression cannot contain """')
+
+ # Evaluate the string from the C++ source as a raw literal.
+ re_string = eval(f'r"""{regex}"""') # pylint: disable=eval-used
+ if not isinstance(re_string, str):
+ raise ValueError('The regular expression must be a string!')
+
+ try:
+ return Expectation(
+ self._compiler, re.compile(re_string), self.index + 1
+ )
+ except re.error as error:
+ raise ValueError('Invalid regular expression: ' + error.msg)
+
+
+class _NegativeCompilationTestSource:
+ def __init__(self, file: Path) -> None:
+ self._file = file
+ self._lines = self._file.read_text().splitlines(keepends=True)
+
+ self._parsed_expectations: Set[int] = set()
+
+ def _error(self, message: str, *error_lines: int) -> NoReturn:
+ raise ParseError(message, self._file, self._lines, error_lines)
+
+ def _parse_expectations(self, start: int) -> Iterator[Expectation]:
+ expectation: Optional[_ExpectationParser] = None
+
+ for index in range(start, len(self._lines)):
+ line = self._lines[index]
+
+ # Skip empty or comment lines
+ if not line or line.isspace() or line.lstrip().startswith('//'):
+ continue
+
+ # Look for a 'PW_NC_EXPECT(' in the code.
+ if not expectation:
+ expect_match = _EXPECT_START.match(line)
+ if not expect_match:
+ break # No expectation found, stop processing.
+
+ compiler = expect_match['compiler'] or 'ANY'
+ expectation = _ExpectationParser(
+ index, Compiler[compiler.lstrip('_')]
+ )
+
+ self._parsed_expectations.add(index)
+
+ # Remove the 'PW_NC_EXPECT(' so the line starts with the regex.
+ line = line[expect_match.end() :]
+
+ # Find the regex after previously finding 'PW_NC_EXPECT('.
+ try:
+ if parsed_expectation := expectation.parse(line.lstrip()):
+ yield parsed_expectation
+
+ expectation = None
+ except ValueError as err:
+ self._error(
+ f'Failed to parse PW_NC_EXPECT() statement:\n\n {err}.\n\n'
+ 'PW_NC_EXPECT() statements must contain only a string '
+ 'literal with a valid Python regular expression and '
+ 'optional //-style comments.',
+ index,
+ )
+
+ if expectation:
+ self._error(
+ 'Unterminated PW_NC_EXPECT() statement!', expectation.index
+ )
+
+ def _check_for_stray_expectations(self) -> None:
+ all_expectations = frozenset(
+ i
+ for i in range(len(self._lines))
+ if _EXPECT_START.match(self._lines[i])
+ )
+ stray = all_expectations - self._parsed_expectations
+ if stray:
+ self._error(
+ f'Found {len(stray)} stray PW_NC_EXPECT() commands!',
+ *sorted(stray),
+ )
+
+ def parse(self, suite: str) -> Iterator[TestCase]:
+ """Finds all negative compilation tests in this source file."""
+ for index, line in enumerate(self._lines):
+ case_match = _TEST_START.match(line)
+ if not case_match:
+ continue
+
+ name_match = _TEST_NAME.match(line, case_match.end())
+ if not name_match:
+ self._error(
+ 'Negative compilation test syntax error. '
+ f"Expected test name, found '{line[case_match.end():]}'",
+ index,
+ )
+
+ expectations = tuple(self._parse_expectations(index + 1))
+ yield TestCase(
+ suite, name_match['name'], expectations, self._file, index + 1
+ )
+
+ self._check_for_stray_expectations()
+
+
+def enumerate_tests(suite: str, paths: Iterable[Path]) -> Iterator[TestCase]:
+ """Parses PW_NC_TEST statements from a file."""
+ for path in paths:
+ yield from _NegativeCompilationTestSource(path).parse(suite)
+
+
+class SourceFile(NamedTuple):
+ gn_path: str
+ file_path: Path
+
+
+def generate_gn_target(
+ base: str, source_list: str, test: TestCase, all_tests: str
+) -> Iterator[str]:
+ yield f'''\
+pw_python_action("{test.name()}.negative_compilation_test") {{
+ script = "$dir_pw_compilation_testing/py/pw_compilation_testing/runner.py"
+ inputs = [{source_list}]
+ args = [
+ "--toolchain-ninja=$_toolchain_ninja",
+ "--target-ninja=$_target_ninja",
+ "--test-data={test.serialize()}",
+ "--all-tests={all_tests}",
+ ]
+ deps = ["{base}"]
+ python_deps = [
+ "$dir_pw_cli/py",
+ "$dir_pw_compilation_testing/py",
+ ]
+ stamp = true
+}}
+'''
+
+
+def generate_gn_build(
+ base: str,
+ sources: Iterable[SourceFile],
+ tests: List[TestCase],
+ all_tests: str,
+) -> Iterator[str]:
+ """Generates the BUILD.gn file with compilation failure test targets."""
+ _, base_name = base.rsplit(':', 1)
+
+ yield 'import("//build_overrides/pigweed.gni")'
+ yield ''
+ yield 'import("$dir_pw_build/python_action.gni")'
+ yield ''
+ yield (
+ '_toolchain_ninja = '
+ 'rebase_path("$root_out_dir/toolchain.ninja", root_build_dir)'
+ )
+ yield (
+ '_target_ninja = '
+ f'rebase_path(get_label_info("{base}", "target_out_dir") +'
+ f'"/{base_name}.ninja", root_build_dir)'
+ )
+ yield ''
+
+ gn_source_list = ', '.join(f'"{gn_path}"' for gn_path, _ in sources)
+ for test in tests:
+ yield from generate_gn_target(base, gn_source_list, test, all_tests)
+
+
+def _main(
+ name: str, base: str, sources: Iterable[SourceFile], output: Path
+) -> int:
+ print_stderr = lambda s: print(s, file=sys.stderr)
+
+ try:
+ tests = list(enumerate_tests(name, (s.file_path for s in sources)))
+ except ParseError as error:
+ print_stderr(f'ERROR: {error}')
+ return 1
+
+ if not tests:
+ print_stderr(f'The test "{name}" has no negative compilation tests!')
+ print_stderr(
+ 'Add PW_NC_TEST() cases or remove this negative ' 'compilation test'
+ )
+ return 1
+
+ tests_by_case = defaultdict(list)
+ for test in tests:
+ tests_by_case[test.case].append(test)
+
+ duplicates = [tests for tests in tests_by_case.values() if len(tests) > 1]
+ if duplicates:
+ print_stderr('There are duplicate negative compilation test cases!')
+ print_stderr('The following test cases appear more than once:')
+ for tests in duplicates:
+ print_stderr(f'\n {tests[0].case} ({len(tests)} occurrences):')
+ for test in tests:
+ print_stderr(f' {test.source.name}:{test.line}')
+ return 1
+
+ output.mkdir(parents=True, exist_ok=True)
+ build_gn = output.joinpath('BUILD.gn')
+ with build_gn.open('w') as fd:
+ for line in generate_gn_build(
+ base, sources, tests, output.joinpath('tests.txt').as_posix()
+ ):
+ print(line, file=fd)
+
+ with output.joinpath('tests.txt').open('w') as fd:
+ for test in tests:
+ print(test.case, file=fd)
+
+ # Print the test case names to stdout for consumption by GN.
+ for test in tests:
+ print(test.case)
+
+ return 0
+
+
+def _parse_args() -> dict:
+ """Parses command-line arguments."""
+
+ def source_file(arg: str) -> SourceFile:
+ gn_path, file_path = arg.split(';', 1)
+ return SourceFile(gn_path, Path(file_path))
+
+ parser = argparse.ArgumentParser(
+ description='Emits an error when a facade has a null backend'
+ )
+ parser.add_argument('--output', type=Path, help='Output directory')
+ parser.add_argument('--name', help='Name of the NC test')
+ parser.add_argument('--base', help='GN label for the base target to build')
+ parser.add_argument(
+ 'sources',
+ nargs='+',
+ type=source_file,
+ help='Source file with the no-compile tests',
+ )
+ return vars(parser.parse_args())
+
+
+if __name__ == '__main__':
+ sys.exit(_main(**_parse_args()))
diff --git a/pw_compilation_testing/py/pw_compilation_testing/py.typed b/pw_compilation_testing/py/pw_compilation_testing/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_compilation_testing/py/pw_compilation_testing/py.typed
diff --git a/pw_compilation_testing/py/pw_compilation_testing/runner.py b/pw_compilation_testing/py/pw_compilation_testing/runner.py
new file mode 100644
index 000000000..f2349c4fb
--- /dev/null
+++ b/pw_compilation_testing/py/pw_compilation_testing/runner.py
@@ -0,0 +1,288 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Executes a compilation failure test."""
+
+import argparse
+import logging
+from pathlib import Path
+import re
+import string
+import sys
+import subprocess
+from typing import Dict, List, Optional
+
+import pw_cli.log
+
+from pw_compilation_testing.generator import Compiler, Expectation, TestCase
+
+_LOG = logging.getLogger(__package__)
+
+_RULE_REGEX = re.compile('^rule (?:cxx|.*_cxx)$')
+_NINJA_VARIABLE = re.compile('^([a-zA-Z0-9_]+) = ?')
+
+
+# TODO(hepler): Could do this step just once and output the results.
+def find_cc_rule(toolchain_ninja_file: Path) -> Optional[str]:
+ """Searches the toolchain.ninja file for the cc rule."""
+ cmd_prefix = ' command = '
+
+ found_rule = False
+
+ with toolchain_ninja_file.open() as fd:
+ for line in fd:
+ if found_rule:
+ if line.startswith(cmd_prefix):
+ cmd = line[len(cmd_prefix) :].strip()
+ if cmd.startswith('ccache '):
+ cmd = cmd[len('ccache ') :]
+ return cmd
+
+ if not line.startswith(' '):
+ break
+ elif _RULE_REGEX.match(line):
+ found_rule = True
+
+ return None
+
+
+def _parse_ninja_variables(target_ninja_file: Path) -> Dict[str, str]:
+ variables: Dict[str, str] = {}
+
+ with target_ninja_file.open() as fd:
+ for line in fd:
+ match = _NINJA_VARIABLE.match(line)
+ if match:
+ variables[match.group(1)] = line[match.end() :].strip()
+
+ return variables
+
+
+_EXPECTED_GN_VARS = (
+ 'asmflags',
+ 'cflags',
+ 'cflags_c',
+ 'cflags_cc',
+ 'cflags_objc',
+ 'cflags_objcc',
+ 'defines',
+ 'include_dirs',
+)
+
+_ENABLE_TEST_MACRO = '-DPW_NC_TEST_EXECUTE_CASE_'
+# Regular expression to find and remove ANSI escape sequences, based on
+# https://stackoverflow.com/a/14693789.
+_ANSI_ESCAPE_SEQUENCES = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
+
+
+class _TestFailure(Exception):
+ pass
+
+
+def _red(message: str) -> str:
+ return f'\033[31m\033[1m{message}\033[0m'
+
+
+_TITLE_1 = ' NEGATIVE '
+_TITLE_2 = ' COMPILATION TEST '
+
+_BOX_TOP = f'┏{"━" * len(_TITLE_1)}┓'
+_BOX_MID_1 = f'┃{_red(_TITLE_1)}┃ \033[1m{{test_name}}\033[0m'
+_BOX_MID_2 = f'┃{_red(_TITLE_2)}┃ \033[0m{{source}}:{{line}}\033[0m'
+_BOX_BOT = f'┻{"━" * (len(_TITLE_1))}┻{"━" * (77 - len(_TITLE_1))}┓'
+_FOOTER = '\n' + '━' * 79 + '┛'
+
+
+def _start_failure(test: TestCase, command: str) -> None:
+ print(_BOX_TOP, file=sys.stderr)
+ print(_BOX_MID_1.format(test_name=test.name()), file=sys.stderr)
+ print(
+ _BOX_MID_2.format(source=test.source, line=test.line), file=sys.stderr
+ )
+ print(_BOX_BOT, file=sys.stderr)
+ print(file=sys.stderr)
+
+ _LOG.debug('Compilation command:\n%s', command)
+
+
+def _check_results(
+ test: TestCase, command: str, process: subprocess.CompletedProcess
+) -> None:
+ stderr = process.stderr.decode(errors='replace')
+
+ if process.returncode == 0:
+ _start_failure(test, command)
+ _LOG.error('Compilation succeeded, but it should have failed!')
+ _LOG.error('Update the test code so that is fails to compile.')
+ raise _TestFailure
+
+ compiler_str = command.split(' ', 1)[0]
+ compiler = Compiler.from_command(compiler_str)
+
+ _LOG.debug('%s is %s', compiler_str, compiler)
+ expectations: List[Expectation] = [
+ e for e in test.expectations if compiler.matches(e.compiler)
+ ]
+
+ _LOG.debug(
+ '%s: Checking compilation from %s (%s) for %d of %d patterns:',
+ test.name(),
+ compiler_str,
+ compiler,
+ len(expectations),
+ len(test.expectations),
+ )
+ for expectation in expectations:
+ _LOG.debug(' %s', expectation.pattern.pattern)
+
+ if not expectations:
+ _start_failure(test, command)
+ _LOG.error(
+ 'Compilation with %s failed, but no PW_NC_EXPECT() patterns '
+ 'that apply to %s were provided',
+ compiler_str,
+ compiler_str,
+ )
+
+ _LOG.error('Compilation output:\n%s', stderr)
+ _LOG.error('')
+ _LOG.error(
+ 'Add at least one PW_NC_EXPECT("<regex>") or '
+ 'PW_NC_EXPECT_%s("<regex>") expectation to %s',
+ compiler.name,
+ test.case,
+ )
+ raise _TestFailure
+
+ no_color = _ANSI_ESCAPE_SEQUENCES.sub('', stderr)
+
+ failed = [e for e in expectations if not e.pattern.search(no_color)]
+ if failed:
+ _start_failure(test, command)
+ _LOG.error(
+ 'Compilation with %s failed, but the output did not '
+ 'match the expected patterns.',
+ compiler_str,
+ )
+ _LOG.error(
+ '%d of %d expected patterns did not match:',
+ len(failed),
+ len(expectations),
+ )
+ _LOG.error('')
+ for expectation in expectations:
+ _LOG.error(
+ ' %s %s:%d: %s',
+ '❌' if expectation in failed else '✅',
+ test.source.name,
+ expectation.line,
+ expectation.pattern.pattern,
+ )
+ _LOG.error('')
+
+ _LOG.error('Compilation output:\n%s', stderr)
+ _LOG.error('')
+ _LOG.error(
+ 'Update the test so that compilation fails with the '
+ 'expected output'
+ )
+ raise _TestFailure
+
+
+def _execute_test(
+ test: TestCase,
+ command: str,
+ variables: Dict[str, str],
+ all_tests: List[str],
+) -> None:
+ variables['in'] = str(test.source)
+
+ command = string.Template(command).substitute(variables)
+ command = ' '.join(
+ [
+ command,
+ '-DPW_NEGATIVE_COMPILATION_TESTS_ENABLED',
+ # Define macros to disable all tests except this one.
+ *(
+ f'{_ENABLE_TEST_MACRO}{t}={1 if test.case == t else 0}'
+ for t in all_tests
+ ),
+ ]
+ )
+ process = subprocess.run(command, shell=True, capture_output=True)
+
+ _check_results(test, command, process)
+
+
+def _main(
+ test: TestCase, toolchain_ninja: Path, target_ninja: Path, all_tests: Path
+) -> int:
+ """Compiles a compile fail test and returns 1 if compilation succeeds."""
+ command = find_cc_rule(toolchain_ninja)
+
+ if command is None:
+ _LOG.critical(
+ 'Failed to find C++ compilation command in %s', toolchain_ninja
+ )
+ return 2
+
+ variables = {key: '' for key in _EXPECTED_GN_VARS}
+ variables.update(_parse_ninja_variables(target_ninja))
+
+ variables['out'] = str(
+ target_ninja.parent / f'{target_ninja.stem}.compile_fail_test.out'
+ )
+
+ try:
+ _execute_test(
+ test, command, variables, all_tests.read_text().splitlines()
+ )
+ except _TestFailure:
+ print(_FOOTER, file=sys.stderr)
+ return 1
+
+ return 0
+
+
+def _parse_args() -> dict:
+ """Parses command-line arguments."""
+
+ parser = argparse.ArgumentParser(
+ description='Emits an error when a facade has a null backend'
+ )
+ parser.add_argument(
+ '--toolchain-ninja',
+ type=Path,
+ required=True,
+ help='Ninja file with the compilation command for the toolchain',
+ )
+ parser.add_argument(
+ '--target-ninja',
+ type=Path,
+ required=True,
+ help='Ninja file with the compilation commands to the test target',
+ )
+ parser.add_argument(
+ '--test-data',
+ dest='test',
+ required=True,
+ type=TestCase.deserialize,
+ help='Serialized TestCase object',
+ )
+ parser.add_argument('--all-tests', type=Path, help='List of all tests')
+ return vars(parser.parse_args())
+
+
+if __name__ == '__main__':
+ pw_cli.log.install(level=logging.INFO)
+ sys.exit(_main(**_parse_args()))
diff --git a/pw_compilation_testing/py/pyproject.toml b/pw_compilation_testing/py/pyproject.toml
new file mode 100644
index 000000000..798b747ec
--- /dev/null
+++ b/pw_compilation_testing/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_compilation_testing/py/setup.cfg b/pw_compilation_testing/py/setup.cfg
new file mode 100644
index 000000000..a730a249e
--- /dev/null
+++ b/pw_compilation_testing/py/setup.cfg
@@ -0,0 +1,26 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+[metadata]
+name = pw_compilation_testing
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Tools for testing that a C/C++ source file does not compile
+
+[options]
+packages = pw_compilation_testing
+zip_safe = False
+
+[options.package_data]
+pw_compilation_testing = py.typed
diff --git a/pw_compilation_testing/py/setup.py b/pw_compilation_testing/py/setup.py
new file mode 100644
index 000000000..299dea40a
--- /dev/null
+++ b/pw_compilation_testing/py/setup.py
@@ -0,0 +1,18 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""The pw_compilation_testing package supports negative compilation testing."""
+
+import setuptools # type: ignore
+
+setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_console/BUILD.gn b/pw_console/BUILD.gn
index 712e74a6b..ec3b19895 100644
--- a/pw_console/BUILD.gn
+++ b/pw_console/BUILD.gn
@@ -15,18 +15,21 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
inputs = [
- "images/calculator_plugin.png",
- "images/clock_plugin1.png",
- "images/clock_plugin2.png",
+ "images/calculator_plugin.svg",
+ "images/clock_plugin1.svg",
+ "images/clock_plugin2.svg",
+ "images/2048_plugin1.svg",
"images/command_runner_main_menu.svg",
"images/pw_system_boot.png",
"images/python_completion.png",
"images/serial_debug.svg",
"py/pw_console/plugins/calc_pane.py",
"py/pw_console/plugins/clock_pane.py",
+ "py/pw_console/plugins/twenty48_pane.py",
]
sources = [
"docs.rst",
@@ -37,3 +40,6 @@ pw_doc_group("docs") {
"testing.rst",
]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_console/embedding.rst b/pw_console/embedding.rst
index aad4d106e..07a944324 100644
--- a/pw_console/embedding.rst
+++ b/pw_console/embedding.rst
@@ -9,7 +9,7 @@ Using embed()
-------------
``pw console`` is invoked by calling ``PwConsoleEmbed().embed()`` in your
own Python script. For a complete example of an embedded device console script see
-:bdg-link-primary-line:`pw_system/py/pw_system/console.py <https://cs.opensource.google/pigweed/pigweed/+/main:pw_system/py/pw_system/console.py>`.
+:bdg-link-primary-line:`pw_system/py/pw_system/console.py <https://cs.pigweed.dev/pigweed/+/main:pw_system/py/pw_system/console.py>`.
.. automodule:: pw_console.embed
:members: PwConsoleEmbed
@@ -31,8 +31,8 @@ User plugin instances are created before starting-up and passed to the Pigweed
Console embed instance. Typically, a console is started by creating a
``PwConsoleEmbed()`` instance, calling customization functions, then calling
``.embed()`` as shown in `Using embed()`_. Adding plugins functions similarly by
-calling ``add_top_toolbar``, ``add_bottom_toolbar`` or
-``add_window_plugin``. For example:
+calling ``add_top_toolbar``, ``add_bottom_toolbar``,
+``add_floating_window_plugin`` or ``add_window_plugin``. For example:
.. code-block:: python
diff --git a/pw_console/images/2048_plugin1.svg b/pw_console/images/2048_plugin1.svg
new file mode 100644
index 000000000..75567d754
--- /dev/null
+++ b/pw_console/images/2048_plugin1.svg
@@ -0,0 +1,1213 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1151pt" height="744pt" viewBox="0 0 1151 744" version="1.1">
+<defs>
+<g>
+<symbol overflow="visible" id="glyph0-0">
+<path style="stroke:none;" d="M 3.203125 -1.046875 L 10.796875 -1.046875 L 10.796875 -19.8125 L 3.203125 -19.8125 Z M 1.265625 0.875 L 1.265625 -21.734375 L 12.734375 -21.734375 L 12.734375 0.875 Z M 7.59375 -9.09375 L 7.59375 -8.375 L 5.671875 -8.375 L 5.671875 -9.109375 C 5.671875 -9.796875 5.878906 -10.441406 6.296875 -11.046875 L 7.828125 -13.296875 C 8.097656 -13.679688 8.234375 -14.035156 8.234375 -14.359375 C 8.234375 -14.816406 8.097656 -15.207031 7.828125 -15.53125 C 7.566406 -15.851562 7.210938 -16.015625 6.765625 -16.015625 C 5.960938 -16.015625 5.34375 -15.484375 4.90625 -14.421875 L 3.34375 -15.34375 C 4.1875 -16.957031 5.332031 -17.765625 6.78125 -17.765625 C 7.75 -17.765625 8.550781 -17.4375 9.1875 -16.78125 C 9.820312 -16.132812 10.140625 -15.320312 10.140625 -14.34375 C 10.140625 -13.644531 9.898438 -12.941406 9.421875 -12.234375 L 7.8125 -9.890625 C 7.664062 -9.679688 7.59375 -9.414062 7.59375 -9.09375 Z M 6.78125 -6.75 C 7.144531 -6.75 7.457031 -6.617188 7.71875 -6.359375 C 7.988281 -6.109375 8.125 -5.796875 8.125 -5.421875 C 8.125 -5.046875 7.988281 -4.726562 7.71875 -4.46875 C 7.457031 -4.207031 7.144531 -4.078125 6.78125 -4.078125 C 6.394531 -4.078125 6.070312 -4.207031 5.8125 -4.46875 C 5.5625 -4.726562 5.4375 -5.046875 5.4375 -5.421875 C 5.4375 -5.796875 5.5625 -6.109375 5.8125 -6.359375 C 6.070312 -6.617188 6.394531 -6.75 6.78125 -6.75 Z M 6.78125 -6.75 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-1">
+<path style="stroke:none;" d=""/>
+</symbol>
+<symbol overflow="visible" id="glyph0-2">
+<path style="stroke:none;" d="M 11.546875 1.8125 L 4.296875 1.8125 L 4.296875 -22.671875 L 11.546875 -22.671875 L 11.546875 -20.609375 L 6.625 -20.609375 L 6.625 -0.265625 L 11.546875 -0.265625 Z M 11.546875 1.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-3">
+<path style="stroke:none;" d="M 12.8125 -18.875 L 5.25 -18.875 L 5.25 -12.125 L 11.328125 -12.125 L 11.328125 -10.140625 L 5.25 -10.140625 L 5.25 0 L 2.921875 0 L 2.921875 -20.859375 L 12.8125 -20.859375 Z M 12.8125 -18.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-4">
+<path style="stroke:none;" d="M 8.484375 -20.734375 C 8.484375 -20.234375 8.3125 -19.804688 7.96875 -19.453125 C 7.632812 -19.109375 7.210938 -18.9375 6.703125 -18.9375 C 6.191406 -18.9375 5.765625 -19.109375 5.421875 -19.453125 C 5.078125 -19.804688 4.90625 -20.234375 4.90625 -20.734375 C 4.90625 -21.222656 5.078125 -21.640625 5.421875 -21.984375 C 5.765625 -22.335938 6.191406 -22.515625 6.703125 -22.515625 C 7.210938 -22.515625 7.632812 -22.335938 7.96875 -21.984375 C 8.3125 -21.640625 8.484375 -21.222656 8.484375 -20.734375 Z M 12 0 L 2.25 0 L 2.25 -1.984375 L 5.96875 -1.984375 L 5.96875 -13.5 L 3.296875 -13.5 L 3.296875 -15.484375 L 8.296875 -15.484375 L 8.296875 -1.984375 L 12 -1.984375 Z M 12 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-5">
+<path style="stroke:none;" d="M 11.875 0 L 2.125 0 L 2.125 -1.984375 L 5.84375 -1.984375 L 5.84375 -18.875 L 3.1875 -18.875 L 3.1875 -20.859375 L 8.171875 -20.859375 L 8.171875 -1.984375 L 11.875 -1.984375 Z M 11.875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-6">
+<path style="stroke:none;" d="M 12.203125 -7.28125 L 4.109375 -7.28125 L 4.109375 -6.8125 C 4.109375 -4.8125 4.398438 -3.484375 4.984375 -2.828125 C 5.566406 -2.171875 6.359375 -1.84375 7.359375 -1.84375 C 8.628906 -1.84375 9.644531 -2.488281 10.40625 -3.78125 L 12.296875 -2.578125 C 11.109375 -0.679688 9.410156 0.265625 7.203125 0.265625 C 5.628906 0.265625 4.332031 -0.296875 3.3125 -1.421875 C 2.289062 -2.554688 1.78125 -4.351562 1.78125 -6.8125 L 1.78125 -8.6875 C 1.78125 -11.132812 2.289062 -12.925781 3.3125 -14.0625 C 4.332031 -15.195312 5.5625 -15.765625 7 -15.765625 C 8.53125 -15.765625 9.78125 -15.238281 10.75 -14.1875 C 11.71875 -13.144531 12.203125 -11.441406 12.203125 -9.078125 Z M 9.890625 -9.328125 C 9.890625 -10.898438 9.617188 -12.015625 9.078125 -12.671875 C 8.535156 -13.328125 7.84375 -13.65625 7 -13.65625 C 6.195312 -13.65625 5.515625 -13.328125 4.953125 -12.671875 C 4.390625 -12.015625 4.109375 -10.898438 4.109375 -9.328125 Z M 9.890625 -9.328125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-7">
+<path style="stroke:none;" d="M 9.703125 1.8125 L 2.46875 1.8125 L 2.46875 -0.265625 L 7.375 -0.265625 L 7.375 -20.609375 L 2.46875 -20.609375 L 2.46875 -22.671875 L 9.703125 -22.671875 Z M 9.703125 1.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-8">
+<path style="stroke:none;" d="M 12.203125 0 L 2.578125 0 L 2.578125 -20.859375 L 12.203125 -20.859375 L 12.203125 -18.875 L 4.90625 -18.875 L 4.90625 -12.125 L 10.296875 -12.125 L 10.296875 -10.140625 L 4.90625 -10.140625 L 4.90625 -1.984375 L 12.203125 -1.984375 Z M 12.203125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-9">
+<path style="stroke:none;" d="M 1.59375 -8.8125 C 1.59375 -11.28125 2.03125 -13.054688 2.90625 -14.140625 C 3.789062 -15.222656 4.847656 -15.765625 6.078125 -15.765625 C 7.671875 -15.765625 8.875 -15.046875 9.6875 -13.609375 L 9.6875 -20.859375 L 12 -20.859375 L 12 0 L 9.6875 0 L 9.6875 -1.859375 C 8.875 -0.441406 7.671875 0.265625 6.078125 0.265625 C 4.847656 0.265625 3.789062 -0.269531 2.90625 -1.34375 C 2.03125 -2.425781 1.59375 -4.203125 1.59375 -6.671875 Z M 3.984375 -6.671875 C 3.984375 -4.878906 4.207031 -3.625 4.65625 -2.90625 C 5.113281 -2.195312 5.78125 -1.84375 6.65625 -1.84375 C 8.1875 -1.84375 9.195312 -2.867188 9.6875 -4.921875 L 9.6875 -10.4375 C 9.207031 -12.582031 8.195312 -13.65625 6.65625 -13.65625 C 5.78125 -13.65625 5.113281 -13.296875 4.65625 -12.578125 C 4.207031 -11.867188 3.984375 -10.613281 3.984375 -8.8125 Z M 3.984375 -6.671875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-10">
+<path style="stroke:none;" d="M 12.953125 -2.15625 C 11.867188 -0.539062 10.414062 0.265625 8.59375 0.265625 C 7.394531 0.265625 6.382812 -0.140625 5.5625 -0.953125 C 4.75 -1.773438 4.34375 -2.984375 4.34375 -4.578125 L 4.34375 -13.5 L 1.75 -13.5 L 1.75 -15.484375 L 4.34375 -15.484375 L 4.34375 -20.859375 L 6.671875 -20.859375 L 6.671875 -15.484375 L 11.890625 -15.484375 L 11.890625 -13.5 L 6.671875 -13.5 L 6.671875 -4.4375 C 6.671875 -3.582031 6.851562 -2.9375 7.21875 -2.5 C 7.582031 -2.0625 8.085938 -1.84375 8.734375 -1.84375 C 9.804688 -1.84375 10.625 -2.378906 11.1875 -3.453125 Z M 12.953125 -2.15625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-11">
+<path style="stroke:none;" d="M 5.6875 0 L 3.109375 -9.609375 C 1.941406 -13.910156 1.359375 -17.273438 1.359375 -19.703125 L 1.359375 -20.859375 L 3.6875 -20.859375 L 3.6875 -19.703125 C 3.6875 -17.691406 4.125 -14.804688 5 -11.046875 L 7 -2.609375 L 9 -11.046875 C 9.875 -14.804688 10.3125 -17.691406 10.3125 -19.703125 L 10.3125 -20.859375 L 12.640625 -20.859375 L 12.640625 -19.703125 C 12.640625 -17.273438 12.054688 -13.910156 10.890625 -9.609375 L 8.3125 0 Z M 5.6875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-12">
+<path style="stroke:none;" d="M 9.578125 -1.28125 L 10.125 -6.296875 C 10.351562 -8.390625 10.46875 -10.53125 10.46875 -12.71875 L 10.46875 -15.484375 L 12.75 -15.484375 L 12.75 -13.421875 C 12.75 -11.554688 12.484375 -9.179688 11.953125 -6.296875 L 10.796875 0 L 8.609375 0 L 7 -6.96875 L 5.390625 0 L 3.203125 0 L 2.046875 -6.296875 C 1.523438 -9.179688 1.265625 -11.554688 1.265625 -13.421875 L 1.265625 -15.484375 L 3.53125 -15.484375 L 3.53125 -12.71875 C 3.53125 -10.53125 3.644531 -8.390625 3.875 -6.296875 L 4.421875 -1.28125 L 6.265625 -9.25 L 7.734375 -9.25 Z M 9.578125 -1.28125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-13">
+<path style="stroke:none;" d="M 9.734375 -1.265625 L 10.09375 -6.1875 C 10.226562 -8.007812 10.296875 -10.070312 10.296875 -12.375 L 10.296875 -20.859375 L 12.625 -20.859375 L 12.625 -15.828125 C 12.625 -12.398438 12.375 -9.1875 11.875 -6.1875 L 10.90625 0 L 8.71875 0 L 7 -8.078125 L 5.28125 0 L 3.09375 0 L 2.125 -6.1875 C 1.625 -9.1875 1.375 -12.398438 1.375 -15.828125 L 1.375 -20.859375 L 3.703125 -20.859375 L 3.703125 -12.375 C 3.703125 -10.070312 3.769531 -8.007812 3.90625 -6.1875 L 4.265625 -1.265625 L 6.265625 -10.671875 L 7.734375 -10.671875 Z M 9.734375 -1.265625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-14">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.03125 C 9.6875 -11.375 9.476562 -12.304688 9.0625 -12.828125 C 8.644531 -13.359375 8.070312 -13.625 7.34375 -13.625 C 5.8125 -13.625 4.800781 -12.570312 4.3125 -10.46875 L 4.3125 0 L 1.984375 0 L 1.984375 -15.484375 L 4.3125 -15.484375 L 4.3125 -13.5 C 5.300781 -15.007812 6.503906 -15.765625 7.921875 -15.765625 C 9.066406 -15.765625 10.035156 -15.335938 10.828125 -14.484375 C 11.617188 -13.628906 12.015625 -12.191406 12.015625 -10.171875 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-15">
+<path style="stroke:none;" d="M 12.21875 -6.703125 C 12.21875 -4.328125 11.726562 -2.570312 10.75 -1.4375 C 9.78125 -0.300781 8.53125 0.265625 7 0.265625 C 5.46875 0.265625 4.21875 -0.300781 3.25 -1.4375 C 2.28125 -2.570312 1.796875 -4.328125 1.796875 -6.703125 L 1.796875 -8.8125 C 1.796875 -11.175781 2.28125 -12.925781 3.25 -14.0625 C 4.21875 -15.195312 5.46875 -15.765625 7 -15.765625 C 8.53125 -15.765625 9.78125 -15.195312 10.75 -14.0625 C 11.726562 -12.925781 12.21875 -11.175781 12.21875 -8.8125 Z M 9.90625 -6.703125 L 9.90625 -8.8125 C 9.90625 -10.632812 9.628906 -11.894531 9.078125 -12.59375 C 8.535156 -13.300781 7.84375 -13.65625 7 -13.65625 C 6.15625 -13.65625 5.460938 -13.300781 4.921875 -12.59375 C 4.378906 -11.894531 4.109375 -10.632812 4.109375 -8.8125 L 4.109375 -6.703125 C 4.109375 -4.867188 4.378906 -3.597656 4.921875 -2.890625 C 5.460938 -2.191406 6.15625 -1.84375 7 -1.84375 C 7.84375 -1.84375 8.535156 -2.191406 9.078125 -2.890625 C 9.628906 -3.597656 9.90625 -4.867188 9.90625 -6.703125 Z M 9.90625 -6.703125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-16">
+<path style="stroke:none;" d="M 7.0625 -1.796875 C 8.019531 -1.796875 8.75 -2.023438 9.25 -2.484375 C 9.75 -2.941406 10 -3.5 10 -4.15625 C 10 -5.0625 9.445312 -5.765625 8.34375 -6.265625 L 5.359375 -7.625 C 3.503906 -8.5 2.578125 -9.75 2.578125 -11.375 C 2.578125 -12.644531 3.03125 -13.691406 3.9375 -14.515625 C 4.851562 -15.347656 6.035156 -15.765625 7.484375 -15.765625 C 9.546875 -15.765625 11.128906 -14.8125 12.234375 -12.90625 L 10.34375 -11.765625 C 9.757812 -13.054688 8.804688 -13.703125 7.484375 -13.703125 C 6.691406 -13.703125 6.0625 -13.5 5.59375 -13.09375 C 5.132812 -12.695312 4.90625 -12.203125 4.90625 -11.609375 C 4.90625 -10.816406 5.421875 -10.175781 6.453125 -9.6875 L 9.265625 -8.375 C 11.296875 -7.425781 12.3125 -5.984375 12.3125 -4.046875 C 12.3125 -2.898438 11.835938 -1.894531 10.890625 -1.03125 C 9.941406 -0.164062 8.65625 0.265625 7.03125 0.265625 C 4.75 0.265625 2.96875 -0.789062 1.6875 -2.90625 L 3.59375 -4.0625 C 4.351562 -2.550781 5.507812 -1.796875 7.0625 -1.796875 Z M 7.0625 -1.796875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-17">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.4375 L 4.3125 -10.4375 L 4.3125 0 L 1.984375 0 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -12.421875 L 9.6875 -12.421875 L 9.6875 -20.859375 L 12.015625 -20.859375 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-18">
+<path style="stroke:none;" d="M 12.421875 -6.65625 C 12.421875 -4.21875 11.953125 -2.453125 11.015625 -1.359375 C 10.085938 -0.273438 9.054688 0.265625 7.921875 0.265625 C 6.328125 0.265625 5.128906 -0.457031 4.328125 -1.90625 L 4.328125 4.828125 L 2 4.828125 L 2 -15.484375 L 4.328125 -15.484375 L 4.328125 -13.640625 C 5.128906 -15.054688 6.328125 -15.765625 7.921875 -15.765625 C 9.054688 -15.765625 10.085938 -15.222656 11.015625 -14.140625 C 11.953125 -13.066406 12.421875 -11.289062 12.421875 -8.8125 Z M 10.015625 -8.8125 C 10.015625 -10.625 9.765625 -11.882812 9.265625 -12.59375 C 8.765625 -13.300781 8.125 -13.65625 7.34375 -13.65625 C 5.8125 -13.65625 4.804688 -12.59375 4.328125 -10.46875 L 4.328125 -5.046875 C 4.796875 -2.910156 5.800781 -1.84375 7.34375 -1.84375 C 8.125 -1.84375 8.765625 -2.203125 9.265625 -2.921875 C 9.765625 -3.640625 10.015625 -4.882812 10.015625 -6.65625 Z M 10.015625 -8.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-19">
+<path style="stroke:none;" d="M 12.953125 -3.09375 C 11.648438 -0.851562 9.878906 0.265625 7.640625 0.265625 C 6.128906 0.265625 4.851562 -0.238281 3.8125 -1.25 C 2.769531 -2.269531 2.25 -4.132812 2.25 -6.84375 L 2.25 -14.046875 C 2.25 -16.679688 2.769531 -18.523438 3.8125 -19.578125 C 4.851562 -20.640625 6.117188 -21.171875 7.609375 -21.171875 C 9.953125 -21.171875 11.6875 -20.039062 12.8125 -17.78125 L 10.90625 -16.625 C 10.21875 -18.238281 9.125 -19.046875 7.625 -19.046875 C 6.757812 -19.046875 6.035156 -18.726562 5.453125 -18.09375 C 4.878906 -17.457031 4.59375 -16.109375 4.59375 -14.046875 L 4.59375 -6.84375 C 4.59375 -4.78125 4.878906 -3.429688 5.453125 -2.796875 C 6.035156 -2.160156 6.757812 -1.84375 7.625 -1.84375 C 9.125 -1.84375 10.265625 -2.6875 11.046875 -4.375 Z M 12.953125 -3.09375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-20">
+<path style="stroke:none;" d="M 12.453125 -18.875 L 8.15625 -18.875 L 8.15625 0 L 5.84375 0 L 5.84375 -18.875 L 1.546875 -18.875 L 1.546875 -20.859375 L 12.453125 -20.859375 Z M 12.453125 -18.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-21">
+<path style="stroke:none;" d="M 12.328125 0 L 10.015625 0 L 10.015625 -5.515625 C 10.015625 -9.023438 10.148438 -13.148438 10.421875 -17.890625 L 7.71875 -9.25 L 6.28125 -9.25 L 3.578125 -17.890625 C 3.859375 -13.148438 4 -9.023438 4 -5.515625 L 4 0 L 1.671875 0 L 1.671875 -20.859375 L 4 -20.859375 L 7 -12.203125 L 10.015625 -20.859375 L 12.328125 -20.859375 Z M 12.328125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-22">
+<path style="stroke:none;" d="M 12.53125 -4.875 C 12.53125 -3.457031 12.054688 -2.242188 11.109375 -1.234375 C 10.171875 -0.234375 8.867188 0.265625 7.203125 0.265625 C 4.578125 0.265625 2.660156 -0.878906 1.453125 -3.171875 L 3.171875 -4.46875 C 4.078125 -2.71875 5.414062 -1.84375 7.1875 -1.84375 C 8.007812 -1.84375 8.71875 -2.09375 9.3125 -2.59375 C 9.90625 -3.101562 10.203125 -3.859375 10.203125 -4.859375 C 10.203125 -5.671875 9.691406 -6.554688 8.671875 -7.515625 L 4.1875 -11.703125 C 2.9375 -12.878906 2.3125 -14.359375 2.3125 -16.140625 C 2.3125 -17.554688 2.769531 -18.75 3.6875 -19.71875 C 4.613281 -20.6875 5.878906 -21.171875 7.484375 -21.171875 C 9.566406 -21.171875 11.253906 -20.21875 12.546875 -18.3125 L 10.765625 -16.90625 C 9.992188 -18.332031 8.90625 -19.046875 7.5 -19.046875 C 6.6875 -19.046875 6.007812 -18.8125 5.46875 -18.34375 C 4.925781 -17.882812 4.65625 -17.148438 4.65625 -16.140625 C 4.65625 -15.109375 5.0625 -14.210938 5.875 -13.453125 L 10.34375 -9.296875 C 11.800781 -7.910156 12.53125 -6.4375 12.53125 -4.875 Z M 12.53125 -4.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-23">
+<path style="stroke:none;" d="M 11.671875 0 L 9.359375 0 L 9.359375 -1.75 C 8.367188 -0.40625 7.125 0.265625 5.625 0.265625 C 4.351562 0.265625 3.328125 -0.164062 2.546875 -1.03125 C 1.765625 -1.90625 1.375 -3 1.375 -4.3125 C 1.375 -5.65625 1.863281 -6.78125 2.84375 -7.6875 C 3.820312 -8.601562 5.253906 -9.0625 7.140625 -9.0625 L 9.359375 -9.0625 L 9.359375 -10.734375 C 9.359375 -12.679688 8.429688 -13.65625 6.578125 -13.65625 C 5.210938 -13.65625 4.191406 -13.054688 3.515625 -11.859375 L 1.75 -13.171875 C 2.96875 -14.898438 4.582031 -15.765625 6.59375 -15.765625 C 8.070312 -15.765625 9.285156 -15.335938 10.234375 -14.484375 C 11.191406 -13.640625 11.671875 -12.390625 11.671875 -10.734375 Z M 9.359375 -3.640625 L 9.359375 -7.1875 L 7.140625 -7.1875 C 6.023438 -7.1875 5.1875 -6.925781 4.625 -6.40625 C 4.0625 -5.894531 3.78125 -5.195312 3.78125 -4.3125 C 3.78125 -2.664062 4.582031 -1.84375 6.1875 -1.84375 C 7.457031 -1.84375 8.515625 -2.441406 9.359375 -3.640625 Z M 9.359375 -3.640625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-24">
+<path style="stroke:none;" d="M 13 -12.859375 L 10.984375 -11.96875 C 10.523438 -13.09375 9.753906 -13.65625 8.671875 -13.65625 C 6.390625 -13.65625 5.25 -11.535156 5.25 -7.296875 L 5.25 0 L 2.921875 0 L 2.921875 -15.484375 L 5.25 -15.484375 L 5.25 -13 C 6.164062 -14.84375 7.425781 -15.765625 9.03125 -15.765625 C 10.894531 -15.765625 12.21875 -14.796875 13 -12.859375 Z M 13 -12.859375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-25">
+<path style="stroke:none;" d="M 12.84375 -2.578125 C 11.65625 -0.679688 9.957031 0.265625 7.75 0.265625 C 6.164062 0.265625 4.863281 -0.296875 3.84375 -1.421875 C 2.820312 -2.554688 2.3125 -4.347656 2.3125 -6.796875 L 2.3125 -8.703125 C 2.3125 -11.140625 2.820312 -12.925781 3.84375 -14.0625 C 4.863281 -15.195312 6.164062 -15.765625 7.75 -15.765625 C 9.957031 -15.765625 11.65625 -14.8125 12.84375 -12.90625 L 10.953125 -11.71875 C 10.179688 -13.007812 9.160156 -13.65625 7.890625 -13.65625 C 6.898438 -13.65625 6.113281 -13.328125 5.53125 -12.671875 C 4.945312 -12.015625 4.65625 -10.691406 4.65625 -8.703125 L 4.65625 -6.796875 C 4.65625 -4.804688 4.945312 -3.484375 5.53125 -2.828125 C 6.113281 -2.171875 6.898438 -1.84375 7.890625 -1.84375 C 9.160156 -1.84375 10.179688 -2.488281 10.953125 -3.78125 Z M 12.84375 -2.578125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-26">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.015625 C 9.6875 -11.398438 9.476562 -12.351562 9.0625 -12.875 C 8.644531 -13.394531 8.070312 -13.65625 7.34375 -13.65625 C 5.8125 -13.65625 4.800781 -12.59375 4.3125 -10.46875 L 4.3125 0 L 1.984375 0 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -13.625 C 5.300781 -15.050781 6.503906 -15.765625 7.921875 -15.765625 C 9.066406 -15.765625 10.035156 -15.335938 10.828125 -14.484375 C 11.617188 -13.628906 12.015625 -12.179688 12.015625 -10.140625 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-27">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -2.015625 C 8.695312 -0.492188 7.492188 0.265625 6.078125 0.265625 C 4.929688 0.265625 3.960938 -0.164062 3.171875 -1.03125 C 2.378906 -1.90625 1.984375 -3.460938 1.984375 -5.703125 L 1.984375 -15.484375 L 4.3125 -15.484375 L 4.3125 -5.703125 C 4.3125 -4.203125 4.519531 -3.179688 4.9375 -2.640625 C 5.351562 -2.109375 5.925781 -1.84375 6.65625 -1.84375 C 8.1875 -1.84375 9.195312 -2.851562 9.6875 -4.875 L 9.6875 -15.484375 L 12.015625 -15.484375 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-28">
+<path style="stroke:none;" d="M 12.28125 -7.296875 L 1.71875 -7.296875 L 1.71875 -9.28125 L 12.28125 -9.28125 Z M 12.28125 -7.296875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-29">
+<path style="stroke:none;" d="M 13.046875 -13.625 L 12.234375 -13.625 L 10.984375 -13.75 C 11.671875 -12.90625 12.015625 -12.046875 12.015625 -11.171875 C 12.015625 -9.972656 11.582031 -8.976562 10.71875 -8.1875 C 9.851562 -7.394531 8.671875 -7 7.171875 -7 C 6.609375 -7 6.097656 -7 5.640625 -7 C 4.898438 -6.519531 4.53125 -6.054688 4.53125 -5.609375 C 4.53125 -5.140625 4.675781 -4.789062 4.96875 -4.5625 C 5.257812 -4.34375 5.738281 -4.234375 6.40625 -4.234375 L 7.703125 -4.234375 C 9.628906 -4.234375 11.023438 -3.820312 11.890625 -3 C 12.753906 -2.1875 13.1875 -1.15625 13.1875 0.09375 C 13.1875 1.5 12.632812 2.660156 11.53125 3.578125 C 10.4375 4.492188 8.988281 4.953125 7.1875 4.953125 C 5.3125 4.953125 3.882812 4.523438 2.90625 3.671875 C 1.9375 2.828125 1.453125 1.800781 1.453125 0.59375 C 1.453125 -0.988281 2.25 -2.1875 3.84375 -3 C 2.664062 -3.488281 2.078125 -4.238281 2.078125 -5.25 C 2.078125 -6.269531 2.703125 -7.132812 3.953125 -7.84375 C 2.671875 -8.707031 2.03125 -9.898438 2.03125 -11.421875 C 2.03125 -12.679688 2.484375 -13.707031 3.390625 -14.5 C 4.296875 -15.300781 5.554688 -15.703125 7.171875 -15.703125 C 7.648438 -15.703125 8.804688 -15.628906 10.640625 -15.484375 L 13.046875 -15.484375 Z M 9.78125 -11.4375 C 9.78125 -12.050781 9.550781 -12.59375 9.09375 -13.0625 C 8.632812 -13.53125 7.953125 -13.765625 7.046875 -13.765625 C 6.109375 -13.765625 5.414062 -13.539062 4.96875 -13.09375 C 4.519531 -12.65625 4.296875 -12.085938 4.296875 -11.390625 C 4.296875 -10.585938 4.53125 -9.972656 5 -9.546875 C 5.476562 -9.128906 6.144531 -8.921875 7 -8.921875 C 7.925781 -8.921875 8.617188 -9.140625 9.078125 -9.578125 C 9.546875 -10.015625 9.78125 -10.632812 9.78125 -11.4375 Z M 10.78125 0.09375 C 10.78125 -0.59375 10.554688 -1.132812 10.109375 -1.53125 C 9.660156 -1.9375 8.859375 -2.140625 7.703125 -2.140625 L 6.765625 -2.140625 C 5.710938 -2.140625 4.960938 -1.898438 4.515625 -1.421875 C 4.066406 -0.941406 3.84375 -0.328125 3.84375 0.421875 C 3.84375 1.203125 4.132812 1.800781 4.71875 2.21875 C 5.300781 2.644531 6.070312 2.859375 7.03125 2.859375 C 9.53125 2.859375 10.78125 1.9375 10.78125 0.09375 Z M 10.78125 0.09375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-30">
+<path style="stroke:none;" d="M 12.421875 -6.671875 C 12.421875 -4.203125 11.976562 -2.425781 11.09375 -1.34375 C 10.207031 -0.269531 9.148438 0.265625 7.921875 0.265625 C 6.328125 0.265625 5.128906 -0.441406 4.328125 -1.859375 L 4.328125 0 L 2 0 L 2 -20.859375 L 4.328125 -20.859375 L 4.328125 -13.609375 C 5.128906 -15.046875 6.328125 -15.765625 7.921875 -15.765625 C 9.148438 -15.765625 10.207031 -15.222656 11.09375 -14.140625 C 11.976562 -13.054688 12.421875 -11.28125 12.421875 -8.8125 Z M 10.015625 -6.671875 L 10.015625 -8.8125 C 10.015625 -10.613281 9.785156 -11.867188 9.328125 -12.578125 C 8.878906 -13.296875 8.21875 -13.65625 7.34375 -13.65625 C 5.800781 -13.65625 4.796875 -12.625 4.328125 -10.5625 L 4.328125 -4.921875 C 4.804688 -2.867188 5.8125 -1.84375 7.34375 -1.84375 C 8.21875 -1.84375 8.878906 -2.195312 9.328125 -2.90625 C 9.785156 -3.625 10.015625 -4.878906 10.015625 -6.671875 Z M 10.015625 -6.671875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-31">
+<path style="stroke:none;" d="M 12.8125 0 L 10.484375 0 C 10.410156 -1.132812 9.851562 -2.460938 8.8125 -3.984375 L 6.5 -7.28125 L 4.765625 -5.1875 L 4.765625 0 L 2.453125 0 L 2.453125 -20.859375 L 4.765625 -20.859375 L 4.765625 -8.046875 L 8.1875 -12.25 C 9.332031 -13.65625 9.914062 -14.734375 9.9375 -15.484375 L 12.265625 -15.484375 L 12.234375 -14.96875 C 12.191406 -14.15625 11.421875 -12.835938 9.921875 -11.015625 L 8.0625 -8.734375 L 10.46875 -5.25 C 11.9375 -3.125 12.703125 -1.554688 12.765625 -0.546875 Z M 12.8125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-32">
+<path style="stroke:none;" d="M 4.140625 2.5 L 5.921875 -2.125 L 2.703125 -10.015625 C 1.941406 -11.910156 1.53125 -13.332031 1.46875 -14.28125 L 1.390625 -15.484375 L 3.71875 -15.484375 L 3.78125 -14.375 C 3.820312 -13.738281 4.179688 -12.503906 4.859375 -10.671875 L 7 -4.828125 L 9.140625 -10.59375 C 9.804688 -12.457031 10.164062 -13.71875 10.21875 -14.375 L 10.28125 -15.484375 L 12.609375 -15.484375 L 12.53125 -14.28125 C 12.476562 -13.4375 12.066406 -12.015625 11.296875 -10.015625 L 6.359375 2.796875 C 6.179688 3.234375 6.078125 3.742188 6.046875 4.328125 L 6.015625 4.828125 L 3.6875 4.828125 L 3.71875 4.328125 C 3.757812 3.742188 3.898438 3.132812 4.140625 2.5 Z M 4.140625 2.5 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-33">
+<path style="stroke:none;" d="M 12.65625 0 L 10.765625 0 L 10.765625 -11.6875 C 10.765625 -13.007812 10.375 -13.671875 9.59375 -13.671875 C 8.914062 -13.671875 8.363281 -13.160156 7.9375 -12.140625 L 7.9375 0 L 6.0625 0 L 6.0625 -11.6875 C 6.039062 -13.007812 5.726562 -13.671875 5.125 -13.671875 C 4.351562 -13.671875 3.726562 -13.160156 3.25 -12.140625 L 3.25 0 L 1.34375 0 L 1.34375 -15.484375 L 3.25 -15.484375 L 3.25 -13.953125 C 3.945312 -15.160156 4.804688 -15.765625 5.828125 -15.765625 C 6.742188 -15.765625 7.375 -15.160156 7.71875 -13.953125 C 8.375 -15.160156 9.195312 -15.765625 10.1875 -15.765625 C 11.832031 -15.765625 12.65625 -14.441406 12.65625 -11.796875 Z M 12.65625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-34">
+<path style="stroke:none;" d="M 8.34375 0 L 5.65625 0 L 2.984375 -8.09375 C 2.054688 -10.863281 1.550781 -12.957031 1.46875 -14.375 L 1.390625 -15.484375 L 3.71875 -15.484375 L 3.78125 -14.375 C 3.863281 -13.03125 4.304688 -11.046875 5.109375 -8.421875 L 7 -2.1875 L 8.890625 -8.421875 C 9.691406 -11.046875 10.132812 -13.03125 10.21875 -14.375 L 10.28125 -15.484375 L 12.609375 -15.484375 L 12.53125 -14.375 C 12.445312 -12.957031 11.941406 -10.863281 11.015625 -8.09375 Z M 8.34375 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-35">
+<path style="stroke:none;" d="M 9.6875 -2.421875 C 9.6875 -1.710938 9.429688 -1.085938 8.921875 -0.546875 C 8.410156 -0.00390625 7.765625 0.265625 6.984375 0.265625 C 6.203125 0.265625 5.5625 -0.00390625 5.0625 -0.546875 C 4.570312 -1.085938 4.328125 -1.710938 4.328125 -2.421875 C 4.328125 -3.117188 4.570312 -3.738281 5.0625 -4.28125 C 5.5625 -4.832031 6.203125 -5.109375 6.984375 -5.109375 C 7.765625 -5.109375 8.410156 -4.832031 8.921875 -4.28125 C 9.429688 -3.726562 9.6875 -3.109375 9.6875 -2.421875 Z M 9.6875 -13.078125 C 9.6875 -12.359375 9.429688 -11.726562 8.921875 -11.1875 C 8.410156 -10.65625 7.765625 -10.390625 6.984375 -10.390625 C 6.203125 -10.390625 5.5625 -10.65625 5.0625 -11.1875 C 4.570312 -11.726562 4.328125 -12.359375 4.328125 -13.078125 C 4.328125 -13.785156 4.570312 -14.410156 5.0625 -14.953125 C 5.5625 -15.492188 6.203125 -15.765625 6.984375 -15.765625 C 7.765625 -15.765625 8.410156 -15.488281 8.921875 -14.9375 C 9.429688 -14.394531 9.6875 -13.773438 9.6875 -13.078125 Z M 9.6875 -13.078125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-36">
+<path style="stroke:none;" d="M 7.421875 0 L 7.421875 -18.0625 L 3.5 -14.984375 L 2.453125 -16.765625 L 7.6875 -20.859375 L 9.765625 -20.859375 L 9.765625 0 Z M 7.421875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-37">
+<path style="stroke:none;" d="M 12.1875 -5.65625 L 10.828125 -5.65625 L 10.828125 0 L 8.5 0 L 8.5 -5.65625 L 1.15625 -5.65625 L 1.15625 -7.640625 L 7.96875 -20.859375 L 10.828125 -20.859375 L 10.828125 -7.640625 L 12.1875 -7.640625 Z M 8.5 -7.640625 L 8.5 -17.46875 L 3.4375 -7.640625 Z M 8.5 -7.640625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-38">
+<path style="stroke:none;" d="M 1.328125 -3.171875 L 3.234375 -4.4375 C 3.878906 -2.707031 5.078125 -1.84375 6.828125 -1.84375 C 7.734375 -1.84375 8.488281 -2.160156 9.09375 -2.796875 C 9.707031 -3.429688 10.015625 -4.550781 10.015625 -6.15625 C 10.015625 -7.664062 9.65625 -8.691406 8.9375 -9.234375 C 8.21875 -9.785156 7.378906 -10.0625 6.421875 -10.0625 L 5.421875 -10.0625 L 5.421875 -12.046875 L 6.421875 -12.046875 C 7.234375 -12.046875 7.960938 -12.332031 8.609375 -12.90625 C 9.253906 -13.488281 9.578125 -14.390625 9.578125 -15.609375 C 9.578125 -16.804688 9.316406 -17.675781 8.796875 -18.21875 C 8.285156 -18.769531 7.628906 -19.046875 6.828125 -19.046875 C 5.347656 -19.046875 4.285156 -18.179688 3.640625 -16.453125 L 1.734375 -17.703125 C 3.023438 -20.015625 4.726562 -21.171875 6.84375 -21.171875 C 8.34375 -21.171875 9.5625 -20.660156 10.5 -19.640625 C 11.445312 -18.628906 11.921875 -17.285156 11.921875 -15.609375 C 11.921875 -13.367188 10.992188 -11.851562 9.140625 -11.0625 C 11.273438 -10.457031 12.34375 -8.820312 12.34375 -6.15625 C 12.34375 -4.0625 11.820312 -2.46875 10.78125 -1.375 C 9.75 -0.28125 8.4375 0.265625 6.84375 0.265625 C 4.445312 0.265625 2.609375 -0.878906 1.328125 -3.171875 Z M 1.328125 -3.171875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-39">
+<path style="stroke:none;" d="M 12.09375 0 L 2.4375 0 L 2.4375 -1.984375 C 2.4375 -4.128906 3.460938 -6.515625 5.515625 -9.140625 L 8.609375 -13.109375 C 9.378906 -14.085938 9.765625 -15.0625 9.765625 -16.03125 C 9.765625 -17.050781 9.515625 -17.804688 9.015625 -18.296875 C 8.523438 -18.796875 7.851562 -19.046875 7 -19.046875 C 5.519531 -19.046875 4.457031 -18.15625 3.8125 -16.375 L 1.90625 -17.703125 C 3.09375 -20.015625 4.789062 -21.171875 7 -21.171875 C 8.414062 -21.171875 9.617188 -20.679688 10.609375 -19.703125 C 11.597656 -18.734375 12.09375 -17.503906 12.09375 -16.015625 C 12.09375 -14.546875 11.5 -13.054688 10.3125 -11.546875 L 7.265625 -7.625 C 5.597656 -5.488281 4.765625 -3.609375 4.765625 -1.984375 L 12.09375 -1.984375 Z M 12.09375 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-40">
+<path style="stroke:none;" d="M 12.546875 -6.28125 C 12.546875 -4.195312 12.054688 -2.582031 11.078125 -1.4375 C 10.097656 -0.300781 8.847656 0.265625 7.328125 0.265625 C 5.910156 0.265625 4.6875 -0.300781 3.65625 -1.4375 C 2.625 -2.582031 2.109375 -4.515625 2.109375 -7.234375 L 2.109375 -13.40625 C 2.109375 -16.03125 2.601562 -17.976562 3.59375 -19.25 C 4.59375 -20.53125 5.921875 -21.171875 7.578125 -21.171875 C 9.441406 -21.171875 10.976562 -20.21875 12.1875 -18.3125 L 10.28125 -16.984375 C 9.6875 -18.359375 8.796875 -19.046875 7.609375 -19.046875 C 6.617188 -19.046875 5.84375 -18.640625 5.28125 -17.828125 C 4.71875 -17.023438 4.4375 -15.550781 4.4375 -13.40625 L 4.4375 -10.484375 C 5.269531 -12.015625 6.46875 -12.78125 8.03125 -12.78125 C 9.382812 -12.78125 10.472656 -12.226562 11.296875 -11.125 C 12.128906 -10.019531 12.546875 -8.40625 12.546875 -6.28125 Z M 10.234375 -6.28125 C 10.234375 -7.96875 9.988281 -9.117188 9.5 -9.734375 C 9.007812 -10.359375 8.378906 -10.671875 7.609375 -10.671875 C 5.941406 -10.671875 4.882812 -9.675781 4.4375 -7.6875 C 4.4375 -5.15625 4.707031 -3.546875 5.25 -2.859375 C 5.800781 -2.179688 6.5 -1.84375 7.34375 -1.84375 C 8.144531 -1.84375 8.828125 -2.179688 9.390625 -2.859375 C 9.953125 -3.546875 10.234375 -4.6875 10.234375 -6.28125 Z M 10.234375 -6.28125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-41">
+<path style="stroke:none;" d="M 6.984375 0.265625 C 5.398438 0.265625 4.085938 -0.234375 3.046875 -1.234375 C 2.003906 -2.242188 1.484375 -3.601562 1.484375 -5.3125 C 1.484375 -7.863281 2.675781 -9.820312 5.0625 -11.1875 C 3.050781 -12.59375 2.046875 -14.242188 2.046875 -16.140625 C 2.046875 -17.648438 2.515625 -18.863281 3.453125 -19.78125 C 4.398438 -20.707031 5.578125 -21.171875 6.984375 -21.171875 C 8.410156 -21.171875 9.59375 -20.707031 10.53125 -19.78125 C 11.476562 -18.863281 11.953125 -17.648438 11.953125 -16.140625 C 11.953125 -14.242188 10.945312 -12.59375 8.9375 -11.1875 C 11.320312 -9.820312 12.515625 -7.863281 12.515625 -5.3125 C 12.515625 -3.601562 11.992188 -2.242188 10.953125 -1.234375 C 9.910156 -0.234375 8.585938 0.265625 6.984375 0.265625 Z M 6.984375 -19.046875 C 6.222656 -19.046875 5.597656 -18.796875 5.109375 -18.296875 C 4.617188 -17.804688 4.375 -17.085938 4.375 -16.140625 C 4.375 -14.609375 5.242188 -13.304688 6.984375 -12.234375 C 8.742188 -13.304688 9.625 -14.609375 9.625 -16.140625 C 9.625 -17.085938 9.378906 -17.804688 8.890625 -18.296875 C 8.410156 -18.796875 7.773438 -19.046875 6.984375 -19.046875 Z M 6.984375 -9.875 C 4.867188 -8.832031 3.8125 -7.3125 3.8125 -5.3125 C 3.8125 -4.050781 4.101562 -3.15625 4.6875 -2.625 C 5.28125 -2.101562 6.046875 -1.84375 6.984375 -1.84375 C 7.941406 -1.84375 8.710938 -2.101562 9.296875 -2.625 C 9.890625 -3.15625 10.1875 -4.050781 10.1875 -5.3125 C 10.1875 -7.3125 9.117188 -8.832031 6.984375 -9.875 Z M 6.984375 -9.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-42">
+<path style="stroke:none;" d="M 1.421875 -10.453125 C 1.421875 -11.898438 1.882812 -14.296875 2.8125 -17.640625 C 3.4375 -19.992188 4.8125 -21.171875 6.9375 -21.171875 C 9.144531 -21.171875 10.5625 -19.992188 11.1875 -17.640625 C 12.113281 -14.296875 12.578125 -11.898438 12.578125 -10.453125 C 12.578125 -9.003906 12.113281 -6.609375 11.1875 -3.265625 C 10.5625 -0.910156 9.144531 0.265625 6.9375 0.265625 C 4.8125 0.265625 3.4375 -0.910156 2.8125 -3.265625 C 1.882812 -6.609375 1.421875 -9.003906 1.421875 -10.453125 Z M 10.25 -10.453125 C 10.25 -11.910156 9.84375 -14.109375 9.03125 -17.046875 C 8.695312 -18.359375 8 -19.019531 6.9375 -19.03125 C 5.957031 -19.019531 5.304688 -18.359375 4.984375 -17.046875 C 4.160156 -14.109375 3.75 -11.910156 3.75 -10.453125 C 3.75 -8.992188 4.160156 -6.785156 4.984375 -3.828125 C 5.304688 -2.515625 5.957031 -1.851562 6.9375 -1.84375 C 8 -1.851562 8.695312 -2.515625 9.03125 -3.828125 C 9.84375 -6.785156 10.25 -8.992188 10.25 -10.453125 Z M 8.890625 -8.265625 L 7.453125 -8.265625 L 5.109375 -12.953125 L 6.546875 -12.953125 Z M 8.890625 -8.265625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-43">
+<path style="stroke:none;" d="M 12.34375 -6.359375 C 12.34375 -4.210938 11.78125 -2.613281 10.65625 -1.5625 C 9.53125 -0.519531 8.039062 0 6.1875 0 L 2.171875 0 L 2.171875 -20.859375 L 6.40625 -20.859375 C 8.195312 -20.859375 9.539062 -20.398438 10.4375 -19.484375 C 11.332031 -18.566406 11.78125 -17.347656 11.78125 -15.828125 C 11.78125 -13.628906 10.835938 -12.175781 8.953125 -11.46875 C 11.210938 -10.78125 12.34375 -9.078125 12.34375 -6.359375 Z M 9.375 -15.828125 C 9.375 -16.953125 9.128906 -17.738281 8.640625 -18.1875 C 8.148438 -18.644531 7.40625 -18.875 6.40625 -18.875 L 4.5 -18.875 L 4.5 -12.203125 L 6.140625 -12.203125 C 7.046875 -12.203125 7.8125 -12.492188 8.4375 -13.078125 C 9.0625 -13.660156 9.375 -14.578125 9.375 -15.828125 Z M 9.9375 -6.359375 C 9.9375 -7.816406 9.671875 -8.828125 9.140625 -9.390625 C 8.609375 -9.953125 7.867188 -10.234375 6.921875 -10.234375 L 4.5 -10.234375 L 4.5 -1.984375 L 6.1875 -1.984375 C 7.519531 -1.984375 8.476562 -2.296875 9.0625 -2.921875 C 9.644531 -3.554688 9.9375 -4.703125 9.9375 -6.359375 Z M 9.9375 -6.359375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-44">
+<path style="stroke:none;" d="M 4.90625 -3.234375 C 4.175781 -2.210938 3.773438 -1.132812 3.703125 0 L 1.375 0 L 1.40625 -0.53125 C 1.46875 -1.550781 2.050781 -2.835938 3.15625 -4.390625 L 5.703125 -7.984375 L 3.515625 -11.09375 C 2.410156 -12.632812 1.828125 -13.921875 1.765625 -14.953125 L 1.75 -15.484375 L 4.078125 -15.484375 C 4.140625 -14.359375 4.535156 -13.28125 5.265625 -12.25 L 7 -9.8125 L 8.734375 -12.25 C 9.460938 -13.28125 9.859375 -14.359375 9.921875 -15.484375 L 12.25 -15.484375 L 12.234375 -14.953125 C 12.171875 -13.921875 11.585938 -12.632812 10.484375 -11.09375 L 8.28125 -7.984375 L 10.84375 -4.390625 C 11.945312 -2.835938 12.53125 -1.550781 12.59375 -0.53125 L 12.625 0 L 10.296875 0 C 10.222656 -1.132812 9.820312 -2.210938 9.09375 -3.234375 L 7 -6.171875 Z M 4.90625 -3.234375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-45">
+<path style="stroke:none;" d="M 4.96875 0 L 2.640625 0 L 2.640625 -20.859375 L 7.1875 -20.859375 C 9.039062 -20.859375 10.441406 -20.351562 11.390625 -19.34375 C 12.335938 -18.34375 12.8125 -17.003906 12.8125 -15.328125 C 12.8125 -13.441406 12.289062 -11.992188 11.25 -10.984375 C 10.207031 -9.972656 8.851562 -9.46875 7.1875 -9.46875 L 4.96875 -9.46875 Z M 4.96875 -11.453125 L 7.171875 -11.453125 C 8.242188 -11.453125 9.050781 -11.722656 9.59375 -12.265625 C 10.132812 -12.804688 10.40625 -13.828125 10.40625 -15.328125 C 10.40625 -16.609375 10.15625 -17.519531 9.65625 -18.0625 C 9.164062 -18.601562 8.335938 -18.875 7.171875 -18.875 L 4.96875 -18.875 Z M 4.96875 -11.453125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-46">
+<path style="stroke:none;" d="M 7.265625 -9.515625 L 4.84375 -9.515625 L 4.84375 0 L 2.515625 0 L 2.515625 -20.859375 L 7.0625 -20.859375 C 8.914062 -20.859375 10.289062 -20.359375 11.1875 -19.359375 C 12.09375 -18.359375 12.546875 -17.015625 12.546875 -15.328125 C 12.546875 -13.898438 12.253906 -12.722656 11.671875 -11.796875 C 11.097656 -10.878906 10.382812 -10.257812 9.53125 -9.9375 L 9.78125 -9.5625 C 11.507812 -6.863281 12.460938 -3.988281 12.640625 -0.9375 L 12.703125 0 L 10.375 0 L 10.34375 -0.71875 C 10.164062 -3.632812 9.253906 -6.378906 7.609375 -8.953125 Z M 4.84375 -11.484375 L 7.0625 -11.484375 C 8.132812 -11.484375 8.914062 -11.800781 9.40625 -12.4375 C 9.894531 -13.082031 10.140625 -14.050781 10.140625 -15.34375 C 10.140625 -16.613281 9.894531 -17.519531 9.40625 -18.0625 C 8.914062 -18.601562 8.132812 -18.875 7.0625 -18.875 L 4.84375 -18.875 Z M 4.84375 -11.484375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-47">
+<path style="stroke:none;" d="M 3.8125 -1.15625 L 3.8125 0 L 1.484375 0 L 1.484375 -1.15625 C 1.484375 -3.15625 2.082031 -6.519531 3.28125 -11.25 L 5.6875 -20.859375 L 8.3125 -20.859375 L 10.71875 -11.25 C 11.914062 -6.519531 12.515625 -3.15625 12.515625 -1.15625 L 12.515625 0 L 10.1875 0 L 10.1875 -1.15625 C 10.1875 -2.101562 10.054688 -3.34375 9.796875 -4.875 L 4.203125 -4.875 C 3.941406 -3.34375 3.8125 -2.101562 3.8125 -1.15625 Z M 7 -17.171875 L 5.265625 -9.953125 C 4.992188 -8.835938 4.757812 -7.804688 4.5625 -6.859375 L 9.421875 -6.859375 C 9.234375 -7.804688 9.003906 -8.835938 8.734375 -9.953125 Z M 7 -17.171875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-48">
+<path style="stroke:none;" d="M 12.125 -6.328125 C 12.125 -3.179688 11.171875 -1.148438 9.265625 -0.234375 L 10.78125 3.5625 L 8.3125 3.5625 L 7 0.265625 C 5.488281 0.253906 4.253906 -0.273438 3.296875 -1.328125 C 2.347656 -2.378906 1.875 -4.046875 1.875 -6.328125 L 1.875 -14.5625 C 1.875 -16.851562 2.347656 -18.523438 3.296875 -19.578125 C 4.253906 -20.640625 5.488281 -21.171875 7 -21.171875 C 8.507812 -21.171875 9.738281 -20.640625 10.6875 -19.578125 C 11.644531 -18.523438 12.125 -16.851562 12.125 -14.5625 Z M 9.796875 -6.4375 L 9.796875 -14.5625 C 9.796875 -16.269531 9.546875 -17.441406 9.046875 -18.078125 C 8.546875 -18.722656 7.859375 -19.046875 6.984375 -19.046875 C 6.109375 -19.046875 5.425781 -18.722656 4.9375 -18.078125 C 4.457031 -17.441406 4.21875 -16.269531 4.21875 -14.5625 L 4.21875 -6.4375 C 4.21875 -4.644531 4.457031 -3.429688 4.9375 -2.796875 C 5.425781 -2.160156 6.113281 -1.84375 7 -1.84375 C 7.863281 -1.84375 8.546875 -2.160156 9.046875 -2.796875 C 9.546875 -3.441406 9.796875 -4.65625 9.796875 -6.4375 Z M 9.796875 -6.4375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-49">
+<path style="stroke:none;" d="M 1.59375 -8.8125 C 1.59375 -11.289062 2.03125 -13.066406 2.90625 -14.140625 C 3.789062 -15.222656 4.847656 -15.765625 6.078125 -15.765625 C 7.671875 -15.765625 8.875 -15.050781 9.6875 -13.625 L 9.6875 -15.484375 L 12 -15.484375 L 12 4.828125 L 9.6875 4.828125 L 9.6875 -1.90625 C 8.875 -0.457031 7.671875 0.265625 6.078125 0.265625 C 4.847656 0.265625 3.789062 -0.273438 2.90625 -1.359375 C 2.03125 -2.453125 1.59375 -4.21875 1.59375 -6.65625 Z M 3.984375 -6.65625 C 3.984375 -4.882812 4.207031 -3.640625 4.65625 -2.921875 C 5.113281 -2.203125 5.78125 -1.84375 6.65625 -1.84375 C 8.195312 -1.84375 9.207031 -2.910156 9.6875 -5.046875 L 9.6875 -10.46875 C 9.195312 -12.59375 8.1875 -13.65625 6.65625 -13.65625 C 5.78125 -13.65625 5.113281 -13.300781 4.65625 -12.59375 C 4.207031 -11.882812 3.984375 -10.625 3.984375 -8.8125 Z M 3.984375 -6.65625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-50">
+<path style="stroke:none;" d="M 11.0625 -17.859375 C 10.695312 -18.640625 10.019531 -19.03125 9.03125 -19.03125 C 8.382812 -19.03125 7.878906 -18.820312 7.515625 -18.40625 C 7.160156 -17.988281 6.984375 -17.242188 6.984375 -16.171875 L 6.984375 -14.609375 L 12.203125 -14.609375 L 12.203125 -12.640625 L 6.984375 -12.640625 L 6.984375 0 L 4.65625 0 L 4.65625 -12.640625 L 2.046875 -12.640625 L 2.046875 -14.609375 L 4.65625 -14.609375 L 4.65625 -16.03125 C 4.65625 -17.789062 5.0625 -19.082031 5.875 -19.90625 C 6.695312 -20.738281 7.703125 -21.15625 8.890625 -21.15625 C 10.710938 -21.15625 12.019531 -20.523438 12.8125 -19.265625 Z M 11.0625 -17.859375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-51">
+<path style="stroke:none;" d="M 3.5625 -7.390625 C 3.90625 -7.390625 4.191406 -7.1875 4.421875 -6.78125 C 4.984375 -5.882812 5.367188 -5.4375 5.578125 -5.4375 C 5.679688 -5.4375 5.75 -5.503906 5.78125 -5.640625 C 6.664062 -8.816406 7.515625 -11.3125 8.328125 -13.125 C 9.148438 -14.945312 9.847656 -16.269531 10.421875 -17.09375 C 11.078125 -18 11.707031 -18.453125 12.3125 -18.453125 C 12.46875 -18.453125 12.597656 -18.410156 12.703125 -18.328125 C 12.816406 -18.253906 12.875 -18.140625 12.875 -17.984375 C 12.875 -17.796875 12.753906 -17.507812 12.515625 -17.125 C 10.628906 -13.84375 8.894531 -9.082031 7.3125 -2.84375 C 7.1875 -2.425781 6.875 -2.117188 6.375 -1.921875 C 5.875 -1.722656 5.484375 -1.625 5.203125 -1.625 C 4.640625 -1.625 3.957031 -2.203125 3.15625 -3.359375 C 2.351562 -4.515625 1.953125 -5.25 1.953125 -5.5625 C 1.953125 -5.957031 2.148438 -6.359375 2.546875 -6.765625 C 2.953125 -7.179688 3.289062 -7.390625 3.5625 -7.390625 Z M 3.5625 -7.390625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-52">
+<path style="stroke:none;" d="M -0.265625 -9.421875 L -0.265625 -11.390625 L 14.265625 -11.390625 L 14.265625 -9.421875 Z M -0.265625 -9.421875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-53">
+<path style="stroke:none;" d="M 0 -7.125 L 0 -9.109375 L 14 -9.109375 L 14 -7.125 Z M 0 -11.71875 L 0 -13.703125 L 14 -13.703125 L 14 -11.71875 Z M 0 -11.71875 "/>
+</symbol>
+</g>
+</defs>
+<g id="surface11275">
+<rect x="0" y="0" width="1151" height="744" style="fill:rgb(15.686275%,17.254902%,20.392157%);fill-opacity:1;stroke:none;"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 17 0 L 577 0 L 577 31 L 17 31 Z M 17 0 "/>
+<g style="fill:rgb(84.705882%,87.058824%,91.372549%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="17" y="26"/>
+ <use xlink:href="#glyph0-2" x="31" y="26"/>
+ <use xlink:href="#glyph0-3" x="45" y="26"/>
+ <use xlink:href="#glyph0-4" x="59" y="26"/>
+ <use xlink:href="#glyph0-5" x="73" y="26"/>
+ <use xlink:href="#glyph0-6" x="87" y="26"/>
+ <use xlink:href="#glyph0-7" x="101" y="26"/>
+ <use xlink:href="#glyph0-1" x="115" y="26"/>
+ <use xlink:href="#glyph0-2" x="129" y="26"/>
+ <use xlink:href="#glyph0-8" x="143" y="26"/>
+ <use xlink:href="#glyph0-9" x="157" y="26"/>
+ <use xlink:href="#glyph0-4" x="171" y="26"/>
+ <use xlink:href="#glyph0-10" x="185" y="26"/>
+ <use xlink:href="#glyph0-7" x="199" y="26"/>
+ <use xlink:href="#glyph0-1" x="213" y="26"/>
+ <use xlink:href="#glyph0-2" x="227" y="26"/>
+ <use xlink:href="#glyph0-11" x="241" y="26"/>
+ <use xlink:href="#glyph0-4" x="255" y="26"/>
+ <use xlink:href="#glyph0-6" x="269" y="26"/>
+ <use xlink:href="#glyph0-12" x="283" y="26"/>
+ <use xlink:href="#glyph0-7" x="297" y="26"/>
+ <use xlink:href="#glyph0-1" x="311" y="26"/>
+ <use xlink:href="#glyph0-2" x="325" y="26"/>
+ <use xlink:href="#glyph0-13" x="339" y="26"/>
+ <use xlink:href="#glyph0-4" x="353" y="26"/>
+ <use xlink:href="#glyph0-14" x="367" y="26"/>
+ <use xlink:href="#glyph0-9" x="381" y="26"/>
+ <use xlink:href="#glyph0-15" x="395" y="26"/>
+ <use xlink:href="#glyph0-12" x="409" y="26"/>
+ <use xlink:href="#glyph0-16" x="423" y="26"/>
+ <use xlink:href="#glyph0-7" x="437" y="26"/>
+ <use xlink:href="#glyph0-1" x="451" y="26"/>
+ <use xlink:href="#glyph0-2" x="465" y="26"/>
+ <use xlink:href="#glyph0-17" x="479" y="26"/>
+ <use xlink:href="#glyph0-6" x="493" y="26"/>
+ <use xlink:href="#glyph0-5" x="507" y="26"/>
+ <use xlink:href="#glyph0-18" x="521" y="26"/>
+ <use xlink:href="#glyph0-7" x="535" y="26"/>
+ <use xlink:href="#glyph0-1" x="549" y="26"/>
+ <use xlink:href="#glyph0-1" x="563" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 577 0 L 815 0 L 815 31 L 577 31 Z M 577 0 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="577" y="26"/>
+ <use xlink:href="#glyph0-15" x="591" y="26"/>
+ <use xlink:href="#glyph0-14" x="605" y="26"/>
+ <use xlink:href="#glyph0-16" x="619" y="26"/>
+ <use xlink:href="#glyph0-15" x="633" y="26"/>
+ <use xlink:href="#glyph0-5" x="647" y="26"/>
+ <use xlink:href="#glyph0-6" x="661" y="26"/>
+ <use xlink:href="#glyph0-1" x="675" y="26"/>
+ <use xlink:href="#glyph0-20" x="689" y="26"/>
+ <use xlink:href="#glyph0-6" x="703" y="26"/>
+ <use xlink:href="#glyph0-16" x="717" y="26"/>
+ <use xlink:href="#glyph0-10" x="731" y="26"/>
+ <use xlink:href="#glyph0-1" x="745" y="26"/>
+ <use xlink:href="#glyph0-21" x="759" y="26"/>
+ <use xlink:href="#glyph0-15" x="773" y="26"/>
+ <use xlink:href="#glyph0-9" x="787" y="26"/>
+ <use xlink:href="#glyph0-6" x="801" y="26"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="578" y="26"/>
+ <use xlink:href="#glyph0-15" x="592" y="26"/>
+ <use xlink:href="#glyph0-14" x="606" y="26"/>
+ <use xlink:href="#glyph0-16" x="620" y="26"/>
+ <use xlink:href="#glyph0-15" x="634" y="26"/>
+ <use xlink:href="#glyph0-5" x="648" y="26"/>
+ <use xlink:href="#glyph0-6" x="662" y="26"/>
+ <use xlink:href="#glyph0-1" x="676" y="26"/>
+ <use xlink:href="#glyph0-20" x="690" y="26"/>
+ <use xlink:href="#glyph0-6" x="704" y="26"/>
+ <use xlink:href="#glyph0-16" x="718" y="26"/>
+ <use xlink:href="#glyph0-10" x="732" y="26"/>
+ <use xlink:href="#glyph0-1" x="746" y="26"/>
+ <use xlink:href="#glyph0-21" x="760" y="26"/>
+ <use xlink:href="#glyph0-15" x="774" y="26"/>
+ <use xlink:href="#glyph0-9" x="788" y="26"/>
+ <use xlink:href="#glyph0-6" x="802" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 815 0 L 843 0 L 843 31 L 815 31 Z M 815 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 843 0 L 857 0 L 857 31 L 843 31 Z M 843 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 857 0 L 1025 0 L 1025 31 L 857 31 Z M 857 0 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-22" x="857" y="26"/>
+ <use xlink:href="#glyph0-6" x="871" y="26"/>
+ <use xlink:href="#glyph0-23" x="885" y="26"/>
+ <use xlink:href="#glyph0-24" x="899" y="26"/>
+ <use xlink:href="#glyph0-25" x="913" y="26"/>
+ <use xlink:href="#glyph0-26" x="927" y="26"/>
+ <use xlink:href="#glyph0-1" x="941" y="26"/>
+ <use xlink:href="#glyph0-21" x="955" y="26"/>
+ <use xlink:href="#glyph0-6" x="969" y="26"/>
+ <use xlink:href="#glyph0-14" x="983" y="26"/>
+ <use xlink:href="#glyph0-27" x="997" y="26"/>
+ <use xlink:href="#glyph0-1" x="1011" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1025 0 L 1109 0 L 1109 31 L 1025 31 Z M 1025 0 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="1025" y="26"/>
+ <use xlink:href="#glyph0-10" x="1039" y="26"/>
+ <use xlink:href="#glyph0-24" x="1053" y="26"/>
+ <use xlink:href="#glyph0-5" x="1067" y="26"/>
+ <use xlink:href="#glyph0-28" x="1081" y="26"/>
+ <use xlink:href="#glyph0-18" x="1095" y="26"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="1026" y="26"/>
+ <use xlink:href="#glyph0-10" x="1040" y="26"/>
+ <use xlink:href="#glyph0-24" x="1054" y="26"/>
+ <use xlink:href="#glyph0-5" x="1068" y="26"/>
+ <use xlink:href="#glyph0-28" x="1082" y="26"/>
+ <use xlink:href="#glyph0-18" x="1096" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1109 0 L 1123 0 L 1123 31 L 1109 31 Z M 1109 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 1123 0 L 1137 0 L 1137 31 L 1123 31 Z M 1123 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 0 L 1151 0 L 1151 31 L 1137 31 Z M 1137 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 0 L 17 0 L 17 31 L 0 31 Z M 0 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 31 L 255 31 L 255 62 L 17 62 Z M 17 31 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-3" x="17" y="57"/>
+ <use xlink:href="#glyph0-15" x="31" y="57"/>
+ <use xlink:href="#glyph0-24" x="45" y="57"/>
+ <use xlink:href="#glyph0-6" x="59" y="57"/>
+ <use xlink:href="#glyph0-29" x="73" y="57"/>
+ <use xlink:href="#glyph0-24" x="87" y="57"/>
+ <use xlink:href="#glyph0-15" x="101" y="57"/>
+ <use xlink:href="#glyph0-27" x="115" y="57"/>
+ <use xlink:href="#glyph0-14" x="129" y="57"/>
+ <use xlink:href="#glyph0-9" x="143" y="57"/>
+ <use xlink:href="#glyph0-1" x="157" y="57"/>
+ <use xlink:href="#glyph0-19" x="171" y="57"/>
+ <use xlink:href="#glyph0-15" x="185" y="57"/>
+ <use xlink:href="#glyph0-5" x="199" y="57"/>
+ <use xlink:href="#glyph0-15" x="213" y="57"/>
+ <use xlink:href="#glyph0-24" x="227" y="57"/>
+ <use xlink:href="#glyph0-16" x="241" y="57"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;" d="M 17 61 L 255 61 L 255 62 L 17 62 Z M 17 61 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 255 31 L 1137 31 L 1137 62 L 255 62 Z M 255 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 31 L 1151 31 L 1151 62 L 1137 62 Z M 1137 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 31 L 17 31 L 17 62 L 0 62 Z M 0 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 62 L 143 62 L 143 93 L 17 93 Z M 17 62 "/>
+<g style="fill:rgb(10.588235%,13.333333%,16.078431%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="17" y="88"/>
+ <use xlink:href="#glyph0-14" x="31" y="88"/>
+ <use xlink:href="#glyph0-16" x="45" y="88"/>
+ <use xlink:href="#glyph0-4" x="59" y="88"/>
+ <use xlink:href="#glyph0-30" x="73" y="88"/>
+ <use xlink:href="#glyph0-5" x="87" y="88"/>
+ <use xlink:href="#glyph0-23" x="101" y="88"/>
+ <use xlink:href="#glyph0-25" x="115" y="88"/>
+ <use xlink:href="#glyph0-31" x="129" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 143 62 L 241 62 L 241 93 L 143 93 Z M 143 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 241 62 L 339 62 L 339 93 L 241 93 Z M 241 62 "/>
+<g style="fill:rgb(100%,42.352941%,41.960784%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="88"/>
+ <use xlink:href="#glyph0-14" x="255" y="88"/>
+ <use xlink:href="#glyph0-16" x="269" y="88"/>
+ <use xlink:href="#glyph0-4" x="283" y="88"/>
+ <use xlink:href="#glyph0-24" x="297" y="88"/>
+ <use xlink:href="#glyph0-6" x="311" y="88"/>
+ <use xlink:href="#glyph0-9" x="325" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 339 62 L 437 62 L 437 93 L 339 93 Z M 339 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 437 62 L 563 62 L 563 93 L 437 93 Z M 437 62 "/>
+<g style="fill:rgb(59.607843%,74.509804%,39.607843%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="437" y="88"/>
+ <use xlink:href="#glyph0-14" x="451" y="88"/>
+ <use xlink:href="#glyph0-16" x="465" y="88"/>
+ <use xlink:href="#glyph0-4" x="479" y="88"/>
+ <use xlink:href="#glyph0-29" x="493" y="88"/>
+ <use xlink:href="#glyph0-24" x="507" y="88"/>
+ <use xlink:href="#glyph0-6" x="521" y="88"/>
+ <use xlink:href="#glyph0-6" x="535" y="88"/>
+ <use xlink:href="#glyph0-14" x="549" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 563 62 L 661 62 L 661 93 L 563 93 Z M 563 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 661 62 L 801 62 L 801 93 L 661 93 Z M 661 62 "/>
+<g style="fill:rgb(92.54902%,74.509804%,48.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="661" y="88"/>
+ <use xlink:href="#glyph0-14" x="675" y="88"/>
+ <use xlink:href="#glyph0-16" x="689" y="88"/>
+ <use xlink:href="#glyph0-4" x="703" y="88"/>
+ <use xlink:href="#glyph0-32" x="717" y="88"/>
+ <use xlink:href="#glyph0-6" x="731" y="88"/>
+ <use xlink:href="#glyph0-5" x="745" y="88"/>
+ <use xlink:href="#glyph0-5" x="759" y="88"/>
+ <use xlink:href="#glyph0-15" x="773" y="88"/>
+ <use xlink:href="#glyph0-12" x="787" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 801 62 L 899 62 L 899 93 L 801 93 Z M 801 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 899 62 L 1011 62 L 1011 93 L 899 93 Z M 899 62 "/>
+<g style="fill:rgb(31.764706%,68.627451%,93.72549%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="899" y="88"/>
+ <use xlink:href="#glyph0-14" x="913" y="88"/>
+ <use xlink:href="#glyph0-16" x="927" y="88"/>
+ <use xlink:href="#glyph0-4" x="941" y="88"/>
+ <use xlink:href="#glyph0-30" x="955" y="88"/>
+ <use xlink:href="#glyph0-5" x="969" y="88"/>
+ <use xlink:href="#glyph0-27" x="983" y="88"/>
+ <use xlink:href="#glyph0-6" x="997" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 1011 62 L 1109 62 L 1109 93 L 1011 93 Z M 1011 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 1109 62 L 1137 62 L 1137 93 L 1109 93 Z M 1109 62 "/>
+<g style="fill:rgb(77.647059%,47.058824%,86.666667%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="1109" y="88"/>
+ <use xlink:href="#glyph0-14" x="1123" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 62 L 1151 62 L 1151 93 L 1137 93 Z M 1137 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 62 L 17 62 L 17 93 L 0 93 Z M 0 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 93 L 73 93 L 73 124 L 17 124 Z M 17 93 "/>
+<g style="fill:rgb(77.647059%,47.058824%,86.666667%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="17" y="119"/>
+ <use xlink:href="#glyph0-4" x="31" y="119"/>
+ <use xlink:href="#glyph0-33" x="45" y="119"/>
+ <use xlink:href="#glyph0-23" x="59" y="119"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 93 L 745 93 L 745 124 L 73 124 Z M 73 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 93 L 1137 93 L 1137 124 L 745 124 Z M 745 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 93 L 1151 93 L 1151 124 L 1137 124 Z M 1137 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 93 L 17 93 L 17 124 L 0 124 Z M 0 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 124 L 73 124 L 73 155 L 17 155 Z M 17 124 "/>
+<g style="fill:rgb(32.941176%,34.901961%,36.862745%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="17" y="150"/>
+ <use xlink:href="#glyph0-14" x="31" y="150"/>
+ <use xlink:href="#glyph0-16" x="45" y="150"/>
+ <use xlink:href="#glyph0-4" x="59" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 124 L 101 124 L 101 155 L 73 155 Z M 73 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,66.666667%);fill-opacity:1;" d="M 101 124 L 185 124 L 185 155 L 101 155 Z M 101 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(66.666667%,40%,86.666667%);fill-opacity:1;" d="M 185 124 L 269 124 L 269 155 L 185 155 Z M 185 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,66.666667%);fill-opacity:1;" d="M 269 124 L 353 124 L 353 155 L 269 155 Z M 269 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,53.333333%,40%);fill-opacity:1;" d="M 353 124 L 437 124 L 437 155 L 353 155 Z M 353 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 437 124 L 745 124 L 745 155 L 437 155 Z M 437 124 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="437" y="150"/>
+ <use xlink:href="#glyph0-1" x="451" y="150"/>
+ <use xlink:href="#glyph0-21" x="465" y="150"/>
+ <use xlink:href="#glyph0-15" x="479" y="150"/>
+ <use xlink:href="#glyph0-34" x="493" y="150"/>
+ <use xlink:href="#glyph0-6" x="507" y="150"/>
+ <use xlink:href="#glyph0-16" x="521" y="150"/>
+ <use xlink:href="#glyph0-35" x="535" y="150"/>
+ <use xlink:href="#glyph0-1" x="549" y="150"/>
+ <use xlink:href="#glyph0-36" x="563" y="150"/>
+ <use xlink:href="#glyph0-37" x="577" y="150"/>
+ <use xlink:href="#glyph0-37" x="591" y="150"/>
+ <use xlink:href="#glyph0-1" x="605" y="150"/>
+ <use xlink:href="#glyph0-1" x="619" y="150"/>
+ <use xlink:href="#glyph0-1" x="633" y="150"/>
+ <use xlink:href="#glyph0-1" x="647" y="150"/>
+ <use xlink:href="#glyph0-1" x="661" y="150"/>
+ <use xlink:href="#glyph0-1" x="675" y="150"/>
+ <use xlink:href="#glyph0-1" x="689" y="150"/>
+ <use xlink:href="#glyph0-1" x="703" y="150"/>
+ <use xlink:href="#glyph0-1" x="717" y="150"/>
+ <use xlink:href="#glyph0-1" x="731" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 124 L 885 124 L 885 155 L 745 155 Z M 745 124 "/>
+<g style="fill:rgb(94.117647%,80.784314%,61.176471%);fill-opacity:1;">
+ <use xlink:href="#glyph0-4" x="745" y="150"/>
+ <use xlink:href="#glyph0-29" x="759" y="150"/>
+ <use xlink:href="#glyph0-26" x="773" y="150"/>
+ <use xlink:href="#glyph0-10" x="787" y="150"/>
+ <use xlink:href="#glyph0-32" x="801" y="150"/>
+ <use xlink:href="#glyph0-6" x="815" y="150"/>
+ <use xlink:href="#glyph0-5" x="829" y="150"/>
+ <use xlink:href="#glyph0-5" x="843" y="150"/>
+ <use xlink:href="#glyph0-15" x="857" y="150"/>
+ <use xlink:href="#glyph0-12" x="871" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 885 124 L 899 124 L 899 155 L 885 155 Z M 885 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 899 124 L 1095 124 L 1095 155 L 899 155 Z M 899 124 "/>
+<g style="fill:rgb(48.627451%,76.470588%,95.294118%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="899" y="150"/>
+ <use xlink:href="#glyph0-14" x="913" y="150"/>
+ <use xlink:href="#glyph0-16" x="927" y="150"/>
+ <use xlink:href="#glyph0-4" x="941" y="150"/>
+ <use xlink:href="#glyph0-30" x="955" y="150"/>
+ <use xlink:href="#glyph0-24" x="969" y="150"/>
+ <use xlink:href="#glyph0-4" x="983" y="150"/>
+ <use xlink:href="#glyph0-29" x="997" y="150"/>
+ <use xlink:href="#glyph0-26" x="1011" y="150"/>
+ <use xlink:href="#glyph0-10" x="1025" y="150"/>
+ <use xlink:href="#glyph0-30" x="1039" y="150"/>
+ <use xlink:href="#glyph0-5" x="1053" y="150"/>
+ <use xlink:href="#glyph0-27" x="1067" y="150"/>
+ <use xlink:href="#glyph0-6" x="1081" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 1095 124 L 1109 124 L 1109 155 L 1095 155 Z M 1095 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 1109 124 L 1137 124 L 1137 155 L 1109 155 Z M 1109 124 "/>
+<g style="fill:rgb(83.137255%,60%,89.803922%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="1109" y="150"/>
+ <use xlink:href="#glyph0-14" x="1123" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 124 L 1151 124 L 1151 155 L 1137 155 Z M 1137 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 124 L 17 124 L 17 155 L 0 155 Z M 0 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 155 L 73 155 L 73 186 L 17 186 Z M 17 155 "/>
+<g style="fill:rgb(83.137255%,60%,89.803922%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="17" y="181"/>
+ <use xlink:href="#glyph0-4" x="31" y="181"/>
+ <use xlink:href="#glyph0-30" x="45" y="181"/>
+ <use xlink:href="#glyph0-24" x="59" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 155 L 101 155 L 101 186 L 73 186 Z M 73 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,66.666667%);fill-opacity:1;" d="M 101 155 L 185 155 L 185 186 L 101 186 Z M 101 155 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="101" y="181"/>
+ <use xlink:href="#glyph0-1" x="115" y="181"/>
+ <use xlink:href="#glyph0-38" x="129" y="181"/>
+ <use xlink:href="#glyph0-39" x="143" y="181"/>
+ <use xlink:href="#glyph0-1" x="157" y="181"/>
+ <use xlink:href="#glyph0-1" x="171" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(66.666667%,40%,86.666667%);fill-opacity:1;" d="M 185 155 L 269 155 L 269 186 L 185 186 Z M 185 155 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="185" y="181"/>
+ <use xlink:href="#glyph0-1" x="199" y="181"/>
+ <use xlink:href="#glyph0-40" x="213" y="181"/>
+ <use xlink:href="#glyph0-37" x="227" y="181"/>
+ <use xlink:href="#glyph0-1" x="241" y="181"/>
+ <use xlink:href="#glyph0-1" x="255" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,66.666667%);fill-opacity:1;" d="M 269 155 L 353 155 L 353 186 L 269 186 Z M 269 155 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="269" y="181"/>
+ <use xlink:href="#glyph0-1" x="283" y="181"/>
+ <use xlink:href="#glyph0-38" x="297" y="181"/>
+ <use xlink:href="#glyph0-39" x="311" y="181"/>
+ <use xlink:href="#glyph0-1" x="325" y="181"/>
+ <use xlink:href="#glyph0-1" x="339" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,53.333333%,40%);fill-opacity:1;" d="M 353 155 L 437 155 L 437 186 L 353 186 Z M 353 155 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="353" y="181"/>
+ <use xlink:href="#glyph0-1" x="367" y="181"/>
+ <use xlink:href="#glyph0-41" x="381" y="181"/>
+ <use xlink:href="#glyph0-1" x="395" y="181"/>
+ <use xlink:href="#glyph0-1" x="409" y="181"/>
+ <use xlink:href="#glyph0-1" x="423" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 437 155 L 745 155 L 745 186 L 437 186 Z M 437 155 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="437" y="181"/>
+ <use xlink:href="#glyph0-1" x="451" y="181"/>
+ <use xlink:href="#glyph0-20" x="465" y="181"/>
+ <use xlink:href="#glyph0-4" x="479" y="181"/>
+ <use xlink:href="#glyph0-33" x="493" y="181"/>
+ <use xlink:href="#glyph0-6" x="507" y="181"/>
+ <use xlink:href="#glyph0-35" x="521" y="181"/>
+ <use xlink:href="#glyph0-1" x="535" y="181"/>
+ <use xlink:href="#glyph0-1" x="549" y="181"/>
+ <use xlink:href="#glyph0-38" x="563" y="181"/>
+ <use xlink:href="#glyph0-37" x="577" y="181"/>
+ <use xlink:href="#glyph0-35" x="591" y="181"/>
+ <use xlink:href="#glyph0-42" x="605" y="181"/>
+ <use xlink:href="#glyph0-39" x="619" y="181"/>
+ <use xlink:href="#glyph0-1" x="633" y="181"/>
+ <use xlink:href="#glyph0-1" x="647" y="181"/>
+ <use xlink:href="#glyph0-1" x="661" y="181"/>
+ <use xlink:href="#glyph0-1" x="675" y="181"/>
+ <use xlink:href="#glyph0-1" x="689" y="181"/>
+ <use xlink:href="#glyph0-1" x="703" y="181"/>
+ <use xlink:href="#glyph0-1" x="717" y="181"/>
+ <use xlink:href="#glyph0-1" x="731" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 155 L 1137 155 L 1137 186 L 745 186 Z M 745 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 155 L 1151 155 L 1151 186 L 1137 186 Z M 1137 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 155 L 17 155 L 17 186 L 0 186 Z M 0 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 186 L 73 186 L 73 217 L 17 217 Z M 17 186 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-43" x="17" y="212"/>
+ <use xlink:href="#glyph0-23" x="31" y="212"/>
+ <use xlink:href="#glyph0-25" x="45" y="212"/>
+ <use xlink:href="#glyph0-31" x="59" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;" d="M 17 216 L 73 216 L 73 217 L 17 217 Z M 17 216 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 186 L 101 186 L 101 217 L 73 217 Z M 73 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,66.666667%);fill-opacity:1;" d="M 101 186 L 185 186 L 185 217 L 101 217 Z M 101 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(66.666667%,40%,86.666667%);fill-opacity:1;" d="M 185 186 L 269 186 L 269 217 L 185 217 Z M 185 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,66.666667%);fill-opacity:1;" d="M 269 186 L 353 186 L 353 217 L 269 217 Z M 269 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,53.333333%,40%);fill-opacity:1;" d="M 353 186 L 437 186 L 437 217 L 353 217 Z M 353 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 437 186 L 745 186 L 745 217 L 437 217 Z M 437 186 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="437" y="212"/>
+ <use xlink:href="#glyph0-1" x="451" y="212"/>
+ <use xlink:href="#glyph0-21" x="465" y="212"/>
+ <use xlink:href="#glyph0-23" x="479" y="212"/>
+ <use xlink:href="#glyph0-44" x="493" y="212"/>
+ <use xlink:href="#glyph0-35" x="507" y="212"/>
+ <use xlink:href="#glyph0-1" x="521" y="212"/>
+ <use xlink:href="#glyph0-36" x="535" y="212"/>
+ <use xlink:href="#glyph0-39" x="549" y="212"/>
+ <use xlink:href="#glyph0-41" x="563" y="212"/>
+ <use xlink:href="#glyph0-1" x="577" y="212"/>
+ <use xlink:href="#glyph0-1" x="591" y="212"/>
+ <use xlink:href="#glyph0-1" x="605" y="212"/>
+ <use xlink:href="#glyph0-1" x="619" y="212"/>
+ <use xlink:href="#glyph0-1" x="633" y="212"/>
+ <use xlink:href="#glyph0-1" x="647" y="212"/>
+ <use xlink:href="#glyph0-1" x="661" y="212"/>
+ <use xlink:href="#glyph0-1" x="675" y="212"/>
+ <use xlink:href="#glyph0-1" x="689" y="212"/>
+ <use xlink:href="#glyph0-1" x="703" y="212"/>
+ <use xlink:href="#glyph0-1" x="717" y="212"/>
+ <use xlink:href="#glyph0-1" x="731" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 186 L 1137 186 L 1137 217 L 745 217 Z M 745 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 186 L 1151 186 L 1151 217 L 1137 217 Z M 1137 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 186 L 17 186 L 17 217 L 0 217 Z M 0 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(10.588235%,13.333333%,16.078431%);fill-opacity:1;" d="M 17 217 L 73 217 L 73 248 L 17 248 Z M 17 217 "/>
+<g style="fill:rgb(90.588235%,90.588235%,90.588235%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="17" y="243"/>
+ <use xlink:href="#glyph0-14" x="31" y="243"/>
+ <use xlink:href="#glyph0-16" x="45" y="243"/>
+ <use xlink:href="#glyph0-4" x="59" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 217 L 101 217 L 101 248 L 73 248 Z M 73 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(40%,40%,86.666667%);fill-opacity:1;" d="M 101 217 L 185 217 L 185 248 L 101 248 Z M 101 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,40%);fill-opacity:1;" d="M 185 217 L 269 217 L 269 248 L 185 248 Z M 185 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,66.666667%,40%);fill-opacity:1;" d="M 269 217 L 353 217 L 353 248 L 269 248 Z M 269 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 353 217 L 745 217 L 745 248 L 353 248 Z M 353 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.54902%,74.509804%,48.235294%);fill-opacity:1;" d="M 745 217 L 801 217 L 801 248 L 745 248 Z M 745 217 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-5" x="745" y="243"/>
+ <use xlink:href="#glyph0-5" x="759" y="243"/>
+ <use xlink:href="#glyph0-15" x="773" y="243"/>
+ <use xlink:href="#glyph0-12" x="787" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 801 217 L 899 217 L 899 248 L 801 248 Z M 801 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(31.764706%,68.627451%,93.72549%);fill-opacity:1;" d="M 899 217 L 1011 217 L 1011 248 L 899 248 Z M 899 217 "/>
+<g style="fill:rgb(90.588235%,90.588235%,90.588235%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="899" y="243"/>
+ <use xlink:href="#glyph0-14" x="913" y="243"/>
+ <use xlink:href="#glyph0-16" x="927" y="243"/>
+ <use xlink:href="#glyph0-4" x="941" y="243"/>
+ <use xlink:href="#glyph0-30" x="955" y="243"/>
+ <use xlink:href="#glyph0-5" x="969" y="243"/>
+ <use xlink:href="#glyph0-27" x="983" y="243"/>
+ <use xlink:href="#glyph0-6" x="997" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 1011 217 L 1109 217 L 1109 248 L 1011 248 Z M 1011 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(77.647059%,47.058824%,86.666667%);fill-opacity:1;" d="M 1109 217 L 1137 217 L 1137 248 L 1109 248 Z M 1109 217 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="1109" y="243"/>
+ <use xlink:href="#glyph0-14" x="1123" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 217 L 1151 217 L 1151 248 L 1137 248 Z M 1137 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 217 L 17 217 L 17 248 L 0 248 Z M 0 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(77.647059%,47.058824%,86.666667%);fill-opacity:1;" d="M 17 248 L 73 248 L 73 279 L 17 279 Z M 17 248 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="17" y="274"/>
+ <use xlink:href="#glyph0-4" x="31" y="274"/>
+ <use xlink:href="#glyph0-33" x="45" y="274"/>
+ <use xlink:href="#glyph0-23" x="59" y="274"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 248 L 101 248 L 101 279 L 73 279 Z M 73 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(40%,40%,86.666667%);fill-opacity:1;" d="M 101 248 L 185 248 L 185 279 L 101 279 Z M 101 248 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="101" y="274"/>
+ <use xlink:href="#glyph0-36" x="115" y="274"/>
+ <use xlink:href="#glyph0-39" x="129" y="274"/>
+ <use xlink:href="#glyph0-41" x="143" y="274"/>
+ <use xlink:href="#glyph0-1" x="157" y="274"/>
+ <use xlink:href="#glyph0-1" x="171" y="274"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,40%);fill-opacity:1;" d="M 185 248 L 269 248 L 269 279 L 185 279 Z M 185 248 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="185" y="274"/>
+ <use xlink:href="#glyph0-1" x="199" y="274"/>
+ <use xlink:href="#glyph0-36" x="213" y="274"/>
+ <use xlink:href="#glyph0-40" x="227" y="274"/>
+ <use xlink:href="#glyph0-1" x="241" y="274"/>
+ <use xlink:href="#glyph0-1" x="255" y="274"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,66.666667%,40%);fill-opacity:1;" d="M 269 248 L 353 248 L 353 279 L 269 279 Z M 269 248 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="269" y="274"/>
+ <use xlink:href="#glyph0-1" x="283" y="274"/>
+ <use xlink:href="#glyph0-37" x="297" y="274"/>
+ <use xlink:href="#glyph0-1" x="311" y="274"/>
+ <use xlink:href="#glyph0-1" x="325" y="274"/>
+ <use xlink:href="#glyph0-1" x="339" y="274"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 353 248 L 745 248 L 745 279 L 353 279 Z M 353 248 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="353" y="274"/>
+ <use xlink:href="#glyph0-1" x="367" y="274"/>
+ <use xlink:href="#glyph0-42" x="381" y="274"/>
+ <use xlink:href="#glyph0-1" x="395" y="274"/>
+ <use xlink:href="#glyph0-1" x="409" y="274"/>
+ <use xlink:href="#glyph0-1" x="423" y="274"/>
+ <use xlink:href="#glyph0-1" x="437" y="274"/>
+ <use xlink:href="#glyph0-1" x="451" y="274"/>
+ <use xlink:href="#glyph0-45" x="465" y="274"/>
+ <use xlink:href="#glyph0-24" x="479" y="274"/>
+ <use xlink:href="#glyph0-6" x="493" y="274"/>
+ <use xlink:href="#glyph0-16" x="507" y="274"/>
+ <use xlink:href="#glyph0-16" x="521" y="274"/>
+ <use xlink:href="#glyph0-1" x="535" y="274"/>
+ <use xlink:href="#glyph0-46" x="549" y="274"/>
+ <use xlink:href="#glyph0-1" x="563" y="274"/>
+ <use xlink:href="#glyph0-10" x="577" y="274"/>
+ <use xlink:href="#glyph0-15" x="591" y="274"/>
+ <use xlink:href="#glyph0-1" x="605" y="274"/>
+ <use xlink:href="#glyph0-24" x="619" y="274"/>
+ <use xlink:href="#glyph0-6" x="633" y="274"/>
+ <use xlink:href="#glyph0-16" x="647" y="274"/>
+ <use xlink:href="#glyph0-10" x="661" y="274"/>
+ <use xlink:href="#glyph0-23" x="675" y="274"/>
+ <use xlink:href="#glyph0-24" x="689" y="274"/>
+ <use xlink:href="#glyph0-10" x="703" y="274"/>
+ <use xlink:href="#glyph0-1" x="717" y="274"/>
+ <use xlink:href="#glyph0-1" x="731" y="274"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 248 L 1137 248 L 1137 279 L 745 279 Z M 745 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 248 L 1151 248 L 1151 279 L 1137 279 Z M 1137 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 248 L 17 248 L 17 279 L 0 279 Z M 0 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(32.941176%,34.901961%,36.862745%);fill-opacity:1;" d="M 17 279 L 73 279 L 73 310 L 17 310 Z M 17 279 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="17" y="305"/>
+ <use xlink:href="#glyph0-14" x="31" y="305"/>
+ <use xlink:href="#glyph0-16" x="45" y="305"/>
+ <use xlink:href="#glyph0-4" x="59" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 279 L 101 279 L 101 310 L 73 310 Z M 73 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(40%,40%,86.666667%);fill-opacity:1;" d="M 101 279 L 185 279 L 185 310 L 101 310 Z M 101 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,40%,40%);fill-opacity:1;" d="M 185 279 L 269 279 L 269 310 L 185 310 Z M 185 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,66.666667%,40%);fill-opacity:1;" d="M 269 279 L 353 279 L 353 310 L 269 310 Z M 269 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 353 279 L 745 279 L 745 310 L 353 310 Z M 353 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(94.117647%,80.784314%,61.176471%);fill-opacity:1;" d="M 745 279 L 885 279 L 885 310 L 745 310 Z M 745 279 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-4" x="745" y="305"/>
+ <use xlink:href="#glyph0-29" x="759" y="305"/>
+ <use xlink:href="#glyph0-26" x="773" y="305"/>
+ <use xlink:href="#glyph0-10" x="787" y="305"/>
+ <use xlink:href="#glyph0-32" x="801" y="305"/>
+ <use xlink:href="#glyph0-6" x="815" y="305"/>
+ <use xlink:href="#glyph0-5" x="829" y="305"/>
+ <use xlink:href="#glyph0-5" x="843" y="305"/>
+ <use xlink:href="#glyph0-15" x="857" y="305"/>
+ <use xlink:href="#glyph0-12" x="871" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 885 279 L 899 279 L 899 310 L 885 310 Z M 885 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(48.627451%,76.470588%,95.294118%);fill-opacity:1;" d="M 899 279 L 1095 279 L 1095 310 L 899 310 Z M 899 279 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="899" y="305"/>
+ <use xlink:href="#glyph0-14" x="913" y="305"/>
+ <use xlink:href="#glyph0-16" x="927" y="305"/>
+ <use xlink:href="#glyph0-4" x="941" y="305"/>
+ <use xlink:href="#glyph0-30" x="955" y="305"/>
+ <use xlink:href="#glyph0-24" x="969" y="305"/>
+ <use xlink:href="#glyph0-4" x="983" y="305"/>
+ <use xlink:href="#glyph0-29" x="997" y="305"/>
+ <use xlink:href="#glyph0-26" x="1011" y="305"/>
+ <use xlink:href="#glyph0-10" x="1025" y="305"/>
+ <use xlink:href="#glyph0-30" x="1039" y="305"/>
+ <use xlink:href="#glyph0-5" x="1053" y="305"/>
+ <use xlink:href="#glyph0-27" x="1067" y="305"/>
+ <use xlink:href="#glyph0-6" x="1081" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 1095 279 L 1109 279 L 1109 310 L 1095 310 Z M 1095 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(83.137255%,60%,89.803922%);fill-opacity:1;" d="M 1109 279 L 1137 279 L 1137 310 L 1109 310 Z M 1109 279 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="1109" y="305"/>
+ <use xlink:href="#glyph0-14" x="1123" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 279 L 1151 279 L 1151 310 L 1137 310 Z M 1137 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 279 L 17 279 L 17 310 L 0 310 Z M 0 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(83.137255%,60%,89.803922%);fill-opacity:1;" d="M 17 310 L 73 310 L 73 341 L 17 341 Z M 17 310 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="17" y="336"/>
+ <use xlink:href="#glyph0-4" x="31" y="336"/>
+ <use xlink:href="#glyph0-30" x="45" y="336"/>
+ <use xlink:href="#glyph0-24" x="59" y="336"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 310 L 101 310 L 101 341 L 73 341 Z M 73 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,66.666667%,40%);fill-opacity:1;" d="M 101 310 L 185 310 L 185 341 L 101 341 Z M 101 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,86.666667%,40%);fill-opacity:1;" d="M 185 310 L 269 310 L 269 341 L 185 341 Z M 185 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 269 310 L 745 310 L 745 341 L 269 341 Z M 269 310 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="269" y="336"/>
+ <use xlink:href="#glyph0-1" x="283" y="336"/>
+ <use xlink:href="#glyph0-1" x="297" y="336"/>
+ <use xlink:href="#glyph0-1" x="311" y="336"/>
+ <use xlink:href="#glyph0-1" x="325" y="336"/>
+ <use xlink:href="#glyph0-1" x="339" y="336"/>
+ <use xlink:href="#glyph0-1" x="353" y="336"/>
+ <use xlink:href="#glyph0-1" x="367" y="336"/>
+ <use xlink:href="#glyph0-1" x="381" y="336"/>
+ <use xlink:href="#glyph0-1" x="395" y="336"/>
+ <use xlink:href="#glyph0-1" x="409" y="336"/>
+ <use xlink:href="#glyph0-1" x="423" y="336"/>
+ <use xlink:href="#glyph0-1" x="437" y="336"/>
+ <use xlink:href="#glyph0-1" x="451" y="336"/>
+ <use xlink:href="#glyph0-47" x="465" y="336"/>
+ <use xlink:href="#glyph0-24" x="479" y="336"/>
+ <use xlink:href="#glyph0-24" x="493" y="336"/>
+ <use xlink:href="#glyph0-15" x="507" y="336"/>
+ <use xlink:href="#glyph0-12" x="521" y="336"/>
+ <use xlink:href="#glyph0-1" x="535" y="336"/>
+ <use xlink:href="#glyph0-31" x="549" y="336"/>
+ <use xlink:href="#glyph0-6" x="563" y="336"/>
+ <use xlink:href="#glyph0-32" x="577" y="336"/>
+ <use xlink:href="#glyph0-16" x="591" y="336"/>
+ <use xlink:href="#glyph0-1" x="605" y="336"/>
+ <use xlink:href="#glyph0-10" x="619" y="336"/>
+ <use xlink:href="#glyph0-15" x="633" y="336"/>
+ <use xlink:href="#glyph0-1" x="647" y="336"/>
+ <use xlink:href="#glyph0-33" x="661" y="336"/>
+ <use xlink:href="#glyph0-15" x="675" y="336"/>
+ <use xlink:href="#glyph0-34" x="689" y="336"/>
+ <use xlink:href="#glyph0-6" x="703" y="336"/>
+ <use xlink:href="#glyph0-1" x="717" y="336"/>
+ <use xlink:href="#glyph0-1" x="731" y="336"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 310 L 1137 310 L 1137 341 L 745 341 Z M 745 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 310 L 1151 310 L 1151 341 L 1137 341 Z M 1137 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 310 L 17 310 L 17 341 L 0 341 Z M 0 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 341 L 73 341 L 73 372 L 17 372 Z M 17 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 341 L 101 341 L 101 372 L 73 372 Z M 73 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,66.666667%,40%);fill-opacity:1;" d="M 101 341 L 185 341 L 185 372 L 101 372 Z M 101 341 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="101" y="367"/>
+ <use xlink:href="#glyph0-1" x="115" y="367"/>
+ <use xlink:href="#glyph0-37" x="129" y="367"/>
+ <use xlink:href="#glyph0-1" x="143" y="367"/>
+ <use xlink:href="#glyph0-1" x="157" y="367"/>
+ <use xlink:href="#glyph0-1" x="171" y="367"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,86.666667%,40%);fill-opacity:1;" d="M 185 341 L 269 341 L 269 372 L 185 372 Z M 185 341 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="185" y="367"/>
+ <use xlink:href="#glyph0-1" x="199" y="367"/>
+ <use xlink:href="#glyph0-39" x="213" y="367"/>
+ <use xlink:href="#glyph0-1" x="227" y="367"/>
+ <use xlink:href="#glyph0-1" x="241" y="367"/>
+ <use xlink:href="#glyph0-1" x="255" y="367"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 269 341 L 745 341 L 745 372 L 269 372 Z M 269 341 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="269" y="367"/>
+ <use xlink:href="#glyph0-1" x="283" y="367"/>
+ <use xlink:href="#glyph0-42" x="297" y="367"/>
+ <use xlink:href="#glyph0-1" x="311" y="367"/>
+ <use xlink:href="#glyph0-1" x="325" y="367"/>
+ <use xlink:href="#glyph0-1" x="339" y="367"/>
+ <use xlink:href="#glyph0-1" x="353" y="367"/>
+ <use xlink:href="#glyph0-1" x="367" y="367"/>
+ <use xlink:href="#glyph0-42" x="381" y="367"/>
+ <use xlink:href="#glyph0-1" x="395" y="367"/>
+ <use xlink:href="#glyph0-1" x="409" y="367"/>
+ <use xlink:href="#glyph0-1" x="423" y="367"/>
+ <use xlink:href="#glyph0-1" x="437" y="367"/>
+ <use xlink:href="#glyph0-1" x="451" y="367"/>
+ <use xlink:href="#glyph0-1" x="465" y="367"/>
+ <use xlink:href="#glyph0-1" x="479" y="367"/>
+ <use xlink:href="#glyph0-1" x="493" y="367"/>
+ <use xlink:href="#glyph0-1" x="507" y="367"/>
+ <use xlink:href="#glyph0-1" x="521" y="367"/>
+ <use xlink:href="#glyph0-1" x="535" y="367"/>
+ <use xlink:href="#glyph0-1" x="549" y="367"/>
+ <use xlink:href="#glyph0-1" x="563" y="367"/>
+ <use xlink:href="#glyph0-1" x="577" y="367"/>
+ <use xlink:href="#glyph0-1" x="591" y="367"/>
+ <use xlink:href="#glyph0-1" x="605" y="367"/>
+ <use xlink:href="#glyph0-1" x="619" y="367"/>
+ <use xlink:href="#glyph0-1" x="633" y="367"/>
+ <use xlink:href="#glyph0-1" x="647" y="367"/>
+ <use xlink:href="#glyph0-1" x="661" y="367"/>
+ <use xlink:href="#glyph0-1" x="675" y="367"/>
+ <use xlink:href="#glyph0-1" x="689" y="367"/>
+ <use xlink:href="#glyph0-1" x="703" y="367"/>
+ <use xlink:href="#glyph0-1" x="717" y="367"/>
+ <use xlink:href="#glyph0-1" x="731" y="367"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 341 L 1137 341 L 1137 372 L 745 372 Z M 745 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 341 L 1151 341 L 1151 372 L 1137 372 Z M 1137 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 341 L 17 341 L 17 372 L 0 372 Z M 0 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 372 L 73 372 L 73 403 L 17 403 Z M 17 372 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="17" y="398"/>
+ <use xlink:href="#glyph0-27" x="31" y="398"/>
+ <use xlink:href="#glyph0-24" x="45" y="398"/>
+ <use xlink:href="#glyph0-24" x="59" y="398"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;" d="M 17 402 L 73 402 L 73 403 L 17 403 Z M 17 402 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 372 L 101 372 L 101 403 L 73 403 Z M 73 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,66.666667%,40%);fill-opacity:1;" d="M 101 372 L 185 372 L 185 403 L 101 403 Z M 101 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,86.666667%,40%);fill-opacity:1;" d="M 185 372 L 269 372 L 269 403 L 185 403 Z M 185 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 269 372 L 745 372 L 745 403 L 269 403 Z M 269 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 372 L 1137 372 L 1137 403 L 745 403 Z M 745 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 372 L 1151 372 L 1151 403 L 1137 403 Z M 1137 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 372 L 17 372 L 17 403 L 0 403 Z M 0 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 403 L 73 403 L 73 434 L 17 434 Z M 17 403 "/>
+<g style="fill:rgb(74.901961%,38.039216%,41.568627%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="429"/>
+ <use xlink:href="#glyph0-5" x="31" y="429"/>
+ <use xlink:href="#glyph0-23" x="45" y="429"/>
+ <use xlink:href="#glyph0-16" x="59" y="429"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 403 L 101 403 L 101 434 L 73 434 Z M 73 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,86.666667%,40%);fill-opacity:1;" d="M 101 403 L 185 403 L 185 434 L 101 434 Z M 101 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 185 403 L 745 403 L 745 434 L 185 434 Z M 185 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 403 L 1137 403 L 1137 434 L 745 434 Z M 745 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 403 L 1151 403 L 1151 434 L 1137 434 Z M 1137 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 403 L 17 403 L 17 434 L 0 434 Z M 0 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 434 L 73 434 L 73 465 L 17 465 Z M 17 434 "/>
+<g style="fill:rgb(81.568627%,52.941176%,43.921569%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="460"/>
+ <use xlink:href="#glyph0-5" x="31" y="460"/>
+ <use xlink:href="#glyph0-23" x="45" y="460"/>
+ <use xlink:href="#glyph0-16" x="59" y="460"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 434 L 101 434 L 101 465 L 73 465 Z M 73 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,86.666667%,40%);fill-opacity:1;" d="M 101 434 L 185 434 L 185 465 L 101 465 Z M 101 434 "/>
+<g style="fill:rgb(0%,0%,0%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="101" y="460"/>
+ <use xlink:href="#glyph0-1" x="115" y="460"/>
+ <use xlink:href="#glyph0-39" x="129" y="460"/>
+ <use xlink:href="#glyph0-1" x="143" y="460"/>
+ <use xlink:href="#glyph0-1" x="157" y="460"/>
+ <use xlink:href="#glyph0-1" x="171" y="460"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 185 434 L 745 434 L 745 465 L 185 465 Z M 185 434 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="185" y="460"/>
+ <use xlink:href="#glyph0-1" x="199" y="460"/>
+ <use xlink:href="#glyph0-42" x="213" y="460"/>
+ <use xlink:href="#glyph0-1" x="227" y="460"/>
+ <use xlink:href="#glyph0-1" x="241" y="460"/>
+ <use xlink:href="#glyph0-1" x="255" y="460"/>
+ <use xlink:href="#glyph0-1" x="269" y="460"/>
+ <use xlink:href="#glyph0-1" x="283" y="460"/>
+ <use xlink:href="#glyph0-42" x="297" y="460"/>
+ <use xlink:href="#glyph0-1" x="311" y="460"/>
+ <use xlink:href="#glyph0-1" x="325" y="460"/>
+ <use xlink:href="#glyph0-1" x="339" y="460"/>
+ <use xlink:href="#glyph0-1" x="353" y="460"/>
+ <use xlink:href="#glyph0-1" x="367" y="460"/>
+ <use xlink:href="#glyph0-42" x="381" y="460"/>
+ <use xlink:href="#glyph0-1" x="395" y="460"/>
+ <use xlink:href="#glyph0-1" x="409" y="460"/>
+ <use xlink:href="#glyph0-1" x="423" y="460"/>
+ <use xlink:href="#glyph0-1" x="437" y="460"/>
+ <use xlink:href="#glyph0-1" x="451" y="460"/>
+ <use xlink:href="#glyph0-1" x="465" y="460"/>
+ <use xlink:href="#glyph0-1" x="479" y="460"/>
+ <use xlink:href="#glyph0-1" x="493" y="460"/>
+ <use xlink:href="#glyph0-1" x="507" y="460"/>
+ <use xlink:href="#glyph0-1" x="521" y="460"/>
+ <use xlink:href="#glyph0-1" x="535" y="460"/>
+ <use xlink:href="#glyph0-1" x="549" y="460"/>
+ <use xlink:href="#glyph0-1" x="563" y="460"/>
+ <use xlink:href="#glyph0-1" x="577" y="460"/>
+ <use xlink:href="#glyph0-1" x="591" y="460"/>
+ <use xlink:href="#glyph0-1" x="605" y="460"/>
+ <use xlink:href="#glyph0-1" x="619" y="460"/>
+ <use xlink:href="#glyph0-1" x="633" y="460"/>
+ <use xlink:href="#glyph0-1" x="647" y="460"/>
+ <use xlink:href="#glyph0-1" x="661" y="460"/>
+ <use xlink:href="#glyph0-1" x="675" y="460"/>
+ <use xlink:href="#glyph0-1" x="689" y="460"/>
+ <use xlink:href="#glyph0-1" x="703" y="460"/>
+ <use xlink:href="#glyph0-1" x="717" y="460"/>
+ <use xlink:href="#glyph0-1" x="731" y="460"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 434 L 1137 434 L 1137 465 L 745 465 Z M 745 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 434 L 1151 434 L 1151 465 L 1137 465 Z M 1137 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 434 L 17 434 L 17 465 L 0 465 Z M 0 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 465 L 73 465 L 73 496 L 17 496 Z M 17 465 "/>
+<g style="fill:rgb(92.156863%,79.607843%,54.509804%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="491"/>
+ <use xlink:href="#glyph0-5" x="31" y="491"/>
+ <use xlink:href="#glyph0-23" x="45" y="491"/>
+ <use xlink:href="#glyph0-16" x="59" y="491"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 465 L 101 465 L 101 496 L 73 496 Z M 73 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(86.666667%,86.666667%,40%);fill-opacity:1;" d="M 101 465 L 185 465 L 185 496 L 101 496 Z M 101 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 185 465 L 745 465 L 745 496 L 185 496 Z M 185 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 465 L 1137 465 L 1137 496 L 745 496 Z M 745 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 465 L 1151 465 L 1151 496 L 1137 496 Z M 1137 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 465 L 17 465 L 17 496 L 0 496 Z M 0 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 496 L 73 496 L 73 527 L 17 527 Z M 17 496 "/>
+<g style="fill:rgb(63.921569%,74.509804%,54.901961%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="522"/>
+ <use xlink:href="#glyph0-5" x="31" y="522"/>
+ <use xlink:href="#glyph0-23" x="45" y="522"/>
+ <use xlink:href="#glyph0-16" x="59" y="522"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 496 L 745 496 L 745 527 L 73 527 Z M 73 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 496 L 1137 496 L 1137 527 L 745 527 Z M 745 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 496 L 1151 496 L 1151 527 L 1137 527 Z M 1137 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 496 L 17 496 L 17 527 L 0 527 Z M 0 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 527 L 73 527 L 73 558 L 17 558 Z M 17 527 "/>
+<g style="fill:rgb(53.333333%,75.294118%,81.568627%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="553"/>
+ <use xlink:href="#glyph0-5" x="31" y="553"/>
+ <use xlink:href="#glyph0-23" x="45" y="553"/>
+ <use xlink:href="#glyph0-16" x="59" y="553"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;" d="M 73 527 L 87 527 L 87 558 L 73 558 Z M 73 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 87 527 L 171 527 L 171 558 L 87 558 Z M 87 527 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="87" y="553"/>
+ <use xlink:href="#glyph0-39" x="101" y="553"/>
+ <use xlink:href="#glyph0-42" x="115" y="553"/>
+ <use xlink:href="#glyph0-37" x="129" y="553"/>
+ <use xlink:href="#glyph0-41" x="143" y="553"/>
+ <use xlink:href="#glyph0-1" x="157" y="553"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="88" y="553"/>
+ <use xlink:href="#glyph0-39" x="102" y="553"/>
+ <use xlink:href="#glyph0-42" x="116" y="553"/>
+ <use xlink:href="#glyph0-37" x="130" y="553"/>
+ <use xlink:href="#glyph0-41" x="144" y="553"/>
+ <use xlink:href="#glyph0-1" x="158" y="553"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 171 527 L 199 527 L 199 558 L 171 558 Z M 171 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 199 527 L 213 527 L 213 558 L 199 558 Z M 199 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 213 527 L 325 527 L 325 558 L 213 558 Z M 213 527 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-46" x="213" y="553"/>
+ <use xlink:href="#glyph0-6" x="227" y="553"/>
+ <use xlink:href="#glyph0-16" x="241" y="553"/>
+ <use xlink:href="#glyph0-10" x="255" y="553"/>
+ <use xlink:href="#glyph0-23" x="269" y="553"/>
+ <use xlink:href="#glyph0-24" x="283" y="553"/>
+ <use xlink:href="#glyph0-10" x="297" y="553"/>
+ <use xlink:href="#glyph0-1" x="311" y="553"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 325 527 L 339 527 L 339 558 L 325 558 Z M 325 527 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-46" x="325" y="553"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-46" x="326" y="553"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 339 527 L 353 527 L 353 558 L 339 558 Z M 339 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 353 527 L 381 527 L 381 558 L 353 558 Z M 353 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 381 527 L 395 527 L 395 558 L 381 558 Z M 381 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 395 527 L 465 527 L 465 558 L 395 558 Z M 395 527 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-48" x="395" y="553"/>
+ <use xlink:href="#glyph0-27" x="409" y="553"/>
+ <use xlink:href="#glyph0-4" x="423" y="553"/>
+ <use xlink:href="#glyph0-10" x="437" y="553"/>
+ <use xlink:href="#glyph0-1" x="451" y="553"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 465 527 L 479 527 L 479 558 L 465 558 Z M 465 527 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-49" x="465" y="553"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-49" x="466" y="553"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 479 527 L 493 527 L 493 558 L 479 558 Z M 479 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 493 527 L 745 527 L 745 558 L 493 558 Z M 493 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 745 527 L 1137 527 L 1137 558 L 745 558 Z M 745 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 527 L 1151 527 L 1151 558 L 1137 558 Z M 1137 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 527 L 17 527 L 17 558 L 0 558 Z M 0 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 558 L 283 558 L 283 589 L 17 589 Z M 17 558 "/>
+<g style="fill:rgb(50.588235%,63.137255%,75.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="584"/>
+ <use xlink:href="#glyph0-5" x="31" y="584"/>
+ <use xlink:href="#glyph0-23" x="45" y="584"/>
+ <use xlink:href="#glyph0-16" x="59" y="584"/>
+ <use xlink:href="#glyph0-16" x="73" y="584"/>
+ <use xlink:href="#glyph0-35" x="87" y="584"/>
+ <use xlink:href="#glyph0-10" x="101" y="584"/>
+ <use xlink:href="#glyph0-26" x="115" y="584"/>
+ <use xlink:href="#glyph0-6" x="129" y="584"/>
+ <use xlink:href="#glyph0-33" x="143" y="584"/>
+ <use xlink:href="#glyph0-6" x="157" y="584"/>
+ <use xlink:href="#glyph0-28" x="171" y="584"/>
+ <use xlink:href="#glyph0-50" x="185" y="584"/>
+ <use xlink:href="#glyph0-29" x="199" y="584"/>
+ <use xlink:href="#glyph0-28" x="213" y="584"/>
+ <use xlink:href="#glyph0-30" x="227" y="584"/>
+ <use xlink:href="#glyph0-5" x="241" y="584"/>
+ <use xlink:href="#glyph0-27" x="255" y="584"/>
+ <use xlink:href="#glyph0-6" x="269" y="584"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 283 558 L 1137 558 L 1137 589 L 283 589 Z M 283 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 558 L 1151 558 L 1151 589 L 1137 589 Z M 1137 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 558 L 17 558 L 17 589 L 0 589 Z M 0 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 589 L 311 589 L 311 620 L 17 620 Z M 17 589 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="615"/>
+ <use xlink:href="#glyph0-5" x="31" y="615"/>
+ <use xlink:href="#glyph0-23" x="45" y="615"/>
+ <use xlink:href="#glyph0-16" x="59" y="615"/>
+ <use xlink:href="#glyph0-16" x="73" y="615"/>
+ <use xlink:href="#glyph0-35" x="87" y="615"/>
+ <use xlink:href="#glyph0-10" x="101" y="615"/>
+ <use xlink:href="#glyph0-26" x="115" y="615"/>
+ <use xlink:href="#glyph0-6" x="129" y="615"/>
+ <use xlink:href="#glyph0-33" x="143" y="615"/>
+ <use xlink:href="#glyph0-6" x="157" y="615"/>
+ <use xlink:href="#glyph0-28" x="171" y="615"/>
+ <use xlink:href="#glyph0-50" x="185" y="615"/>
+ <use xlink:href="#glyph0-29" x="199" y="615"/>
+ <use xlink:href="#glyph0-28" x="213" y="615"/>
+ <use xlink:href="#glyph0-18" x="227" y="615"/>
+ <use xlink:href="#glyph0-27" x="241" y="615"/>
+ <use xlink:href="#glyph0-24" x="255" y="615"/>
+ <use xlink:href="#glyph0-18" x="269" y="615"/>
+ <use xlink:href="#glyph0-5" x="283" y="615"/>
+ <use xlink:href="#glyph0-6" x="297" y="615"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 311 589 L 1137 589 L 1137 620 L 311 620 Z M 311 589 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 589 L 1151 589 L 1151 620 L 1137 620 Z M 1137 589 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 589 L 17 589 L 17 620 L 0 620 Z M 0 589 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 620 L 325 620 L 325 651 L 17 651 Z M 17 620 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="646"/>
+ <use xlink:href="#glyph0-5" x="31" y="646"/>
+ <use xlink:href="#glyph0-23" x="45" y="646"/>
+ <use xlink:href="#glyph0-16" x="59" y="646"/>
+ <use xlink:href="#glyph0-16" x="73" y="646"/>
+ <use xlink:href="#glyph0-35" x="87" y="646"/>
+ <use xlink:href="#glyph0-10" x="101" y="646"/>
+ <use xlink:href="#glyph0-26" x="115" y="646"/>
+ <use xlink:href="#glyph0-6" x="129" y="646"/>
+ <use xlink:href="#glyph0-33" x="143" y="646"/>
+ <use xlink:href="#glyph0-6" x="157" y="646"/>
+ <use xlink:href="#glyph0-28" x="171" y="646"/>
+ <use xlink:href="#glyph0-50" x="185" y="646"/>
+ <use xlink:href="#glyph0-29" x="199" y="646"/>
+ <use xlink:href="#glyph0-28" x="213" y="646"/>
+ <use xlink:href="#glyph0-33" x="227" y="646"/>
+ <use xlink:href="#glyph0-23" x="241" y="646"/>
+ <use xlink:href="#glyph0-29" x="255" y="646"/>
+ <use xlink:href="#glyph0-6" x="269" y="646"/>
+ <use xlink:href="#glyph0-14" x="283" y="646"/>
+ <use xlink:href="#glyph0-10" x="297" y="646"/>
+ <use xlink:href="#glyph0-23" x="311" y="646"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 325 620 L 1137 620 L 1137 651 L 325 651 Z M 325 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 620 L 1151 620 L 1151 651 L 1137 651 Z M 1137 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 620 L 17 620 L 17 651 L 0 651 Z M 0 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(15.294118%,17.254902%,21.176471%);fill-opacity:1;" d="M 17 651 L 1137 651 L 1137 682 L 17 682 Z M 17 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 651 L 1151 651 L 1151 682 L 1137 682 Z M 1137 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 651 L 17 651 L 17 682 L 0 682 Z M 0 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 17 682 L 31 682 L 31 713 L 17 713 Z M 17 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 31 682 L 129 682 L 129 713 L 31 713 Z M 31 682 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="31" y="708"/>
+ <use xlink:href="#glyph0-19" x="45" y="708"/>
+ <use xlink:href="#glyph0-5" x="59" y="708"/>
+ <use xlink:href="#glyph0-15" x="73" y="708"/>
+ <use xlink:href="#glyph0-25" x="87" y="708"/>
+ <use xlink:href="#glyph0-31" x="101" y="708"/>
+ <use xlink:href="#glyph0-1" x="115" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 129 682 L 157 682 L 157 713 L 129 713 Z M 129 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 157 682 L 171 682 L 171 713 L 157 713 Z M 157 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 171 682 L 311 682 L 311 713 L 171 713 Z M 171 682 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-11" x="171" y="708"/>
+ <use xlink:href="#glyph0-4" x="185" y="708"/>
+ <use xlink:href="#glyph0-6" x="199" y="708"/>
+ <use xlink:href="#glyph0-12" x="213" y="708"/>
+ <use xlink:href="#glyph0-1" x="227" y="708"/>
+ <use xlink:href="#glyph0-21" x="241" y="708"/>
+ <use xlink:href="#glyph0-15" x="255" y="708"/>
+ <use xlink:href="#glyph0-9" x="269" y="708"/>
+ <use xlink:href="#glyph0-6" x="283" y="708"/>
+ <use xlink:href="#glyph0-1" x="297" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 311 682 L 325 682 L 325 713 L 311 713 Z M 311 682 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-34" x="311" y="708"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-34" x="312" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 325 682 L 339 682 L 339 713 L 325 713 Z M 325 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 339 682 L 367 682 L 367 713 L 339 713 Z M 339 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 367 682 L 381 682 L 381 713 L 367 713 Z M 367 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 381 682 L 437 682 L 437 713 L 381 713 Z M 381 682 "/>
+<g style="fill:rgb(84.705882%,87.058824%,91.372549%);fill-opacity:1;">
+ <use xlink:href="#glyph0-2" x="381" y="708"/>
+ <use xlink:href="#glyph0-51" x="395" y="708"/>
+ <use xlink:href="#glyph0-7" x="409" y="708"/>
+ <use xlink:href="#glyph0-1" x="423" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 437 682 L 507 682 L 507 713 L 437 713 Z M 437 682 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-13" x="437" y="708"/>
+ <use xlink:href="#glyph0-24" x="451" y="708"/>
+ <use xlink:href="#glyph0-23" x="465" y="708"/>
+ <use xlink:href="#glyph0-18" x="479" y="708"/>
+ <use xlink:href="#glyph0-1" x="493" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 507 682 L 521 682 L 521 713 L 507 713 Z M 507 682 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-12" x="507" y="708"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-12" x="508" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 521 682 L 535 682 L 535 713 L 521 713 Z M 521 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 535 682 L 815 682 L 815 713 L 535 713 Z M 535 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 815 682 L 829 682 L 829 713 L 815 713 Z M 815 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 829 682 L 1025 682 L 1025 713 L 829 713 Z M 829 682 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="829" y="708"/>
+ <use xlink:href="#glyph0-5" x="843" y="708"/>
+ <use xlink:href="#glyph0-4" x="857" y="708"/>
+ <use xlink:href="#glyph0-25" x="871" y="708"/>
+ <use xlink:href="#glyph0-31" x="885" y="708"/>
+ <use xlink:href="#glyph0-1" x="899" y="708"/>
+ <use xlink:href="#glyph0-10" x="913" y="708"/>
+ <use xlink:href="#glyph0-15" x="927" y="708"/>
+ <use xlink:href="#glyph0-1" x="941" y="708"/>
+ <use xlink:href="#glyph0-50" x="955" y="708"/>
+ <use xlink:href="#glyph0-15" x="969" y="708"/>
+ <use xlink:href="#glyph0-25" x="983" y="708"/>
+ <use xlink:href="#glyph0-27" x="997" y="708"/>
+ <use xlink:href="#glyph0-16" x="1011" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1025 682 L 1039 682 L 1039 713 L 1025 713 Z M 1025 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 1039 682 L 1081 682 L 1081 713 L 1039 713 Z M 1039 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 682 L 1151 682 L 1151 713 L 1137 713 Z M 1137 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 1081 682 L 1137 682 L 1137 713 L 1081 713 Z M 1081 682 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-52" x="1081" y="708"/>
+ <use xlink:href="#glyph0-53" x="1095" y="708"/>
+ <use xlink:href="#glyph0-53" x="1109" y="708"/>
+ <use xlink:href="#glyph0-52" x="1123" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 682 L 17 682 L 17 713 L 0 713 Z M 0 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 17 713 L 45 713 L 45 744 L 17 744 Z M 17 713 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 45 713 L 1152 713 L 1152 744 L 45 744 Z M 45 713 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 713 L 17 713 L 17 744 L 0 744 Z M 0 713 "/>
+</g>
+</svg>
diff --git a/pw_console/images/calculator_plugin.png b/pw_console/images/calculator_plugin.png
deleted file mode 100644
index 84f641493..000000000
--- a/pw_console/images/calculator_plugin.png
+++ /dev/null
Binary files differ
diff --git a/pw_console/images/calculator_plugin.svg b/pw_console/images/calculator_plugin.svg
new file mode 100644
index 000000000..56d29176b
--- /dev/null
+++ b/pw_console/images/calculator_plugin.svg
@@ -0,0 +1,1385 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1151pt" height="713pt" viewBox="0 0 1151 713" version="1.1">
+<defs>
+<g>
+<symbol overflow="visible" id="glyph0-0">
+<path style="stroke:none;" d="M 3.203125 -1.046875 L 10.796875 -1.046875 L 10.796875 -19.8125 L 3.203125 -19.8125 Z M 1.265625 0.875 L 1.265625 -21.734375 L 12.734375 -21.734375 L 12.734375 0.875 Z M 7.59375 -9.09375 L 7.59375 -8.375 L 5.671875 -8.375 L 5.671875 -9.109375 C 5.671875 -9.796875 5.878906 -10.441406 6.296875 -11.046875 L 7.828125 -13.296875 C 8.097656 -13.679688 8.234375 -14.035156 8.234375 -14.359375 C 8.234375 -14.816406 8.097656 -15.207031 7.828125 -15.53125 C 7.566406 -15.851562 7.210938 -16.015625 6.765625 -16.015625 C 5.960938 -16.015625 5.34375 -15.484375 4.90625 -14.421875 L 3.34375 -15.34375 C 4.1875 -16.957031 5.332031 -17.765625 6.78125 -17.765625 C 7.75 -17.765625 8.550781 -17.4375 9.1875 -16.78125 C 9.820312 -16.132812 10.140625 -15.320312 10.140625 -14.34375 C 10.140625 -13.644531 9.898438 -12.941406 9.421875 -12.234375 L 7.8125 -9.890625 C 7.664062 -9.679688 7.59375 -9.414062 7.59375 -9.09375 Z M 6.78125 -6.75 C 7.144531 -6.75 7.457031 -6.617188 7.71875 -6.359375 C 7.988281 -6.109375 8.125 -5.796875 8.125 -5.421875 C 8.125 -5.046875 7.988281 -4.726562 7.71875 -4.46875 C 7.457031 -4.207031 7.144531 -4.078125 6.78125 -4.078125 C 6.394531 -4.078125 6.070312 -4.207031 5.8125 -4.46875 C 5.5625 -4.726562 5.4375 -5.046875 5.4375 -5.421875 C 5.4375 -5.796875 5.5625 -6.109375 5.8125 -6.359375 C 6.070312 -6.617188 6.394531 -6.75 6.78125 -6.75 Z M 6.78125 -6.75 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-1">
+<path style="stroke:none;" d=""/>
+</symbol>
+<symbol overflow="visible" id="glyph0-2">
+<path style="stroke:none;" d="M 11.546875 1.8125 L 4.296875 1.8125 L 4.296875 -22.671875 L 11.546875 -22.671875 L 11.546875 -20.609375 L 6.625 -20.609375 L 6.625 -0.265625 L 11.546875 -0.265625 Z M 11.546875 1.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-3">
+<path style="stroke:none;" d="M 12.8125 -18.875 L 5.25 -18.875 L 5.25 -12.125 L 11.328125 -12.125 L 11.328125 -10.140625 L 5.25 -10.140625 L 5.25 0 L 2.921875 0 L 2.921875 -20.859375 L 12.8125 -20.859375 Z M 12.8125 -18.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-4">
+<path style="stroke:none;" d="M 8.484375 -20.734375 C 8.484375 -20.234375 8.3125 -19.804688 7.96875 -19.453125 C 7.632812 -19.109375 7.210938 -18.9375 6.703125 -18.9375 C 6.191406 -18.9375 5.765625 -19.109375 5.421875 -19.453125 C 5.078125 -19.804688 4.90625 -20.234375 4.90625 -20.734375 C 4.90625 -21.222656 5.078125 -21.640625 5.421875 -21.984375 C 5.765625 -22.335938 6.191406 -22.515625 6.703125 -22.515625 C 7.210938 -22.515625 7.632812 -22.335938 7.96875 -21.984375 C 8.3125 -21.640625 8.484375 -21.222656 8.484375 -20.734375 Z M 12 0 L 2.25 0 L 2.25 -1.984375 L 5.96875 -1.984375 L 5.96875 -13.5 L 3.296875 -13.5 L 3.296875 -15.484375 L 8.296875 -15.484375 L 8.296875 -1.984375 L 12 -1.984375 Z M 12 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-5">
+<path style="stroke:none;" d="M 11.875 0 L 2.125 0 L 2.125 -1.984375 L 5.84375 -1.984375 L 5.84375 -18.875 L 3.1875 -18.875 L 3.1875 -20.859375 L 8.171875 -20.859375 L 8.171875 -1.984375 L 11.875 -1.984375 Z M 11.875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-6">
+<path style="stroke:none;" d="M 12.203125 -7.28125 L 4.109375 -7.28125 L 4.109375 -6.8125 C 4.109375 -4.8125 4.398438 -3.484375 4.984375 -2.828125 C 5.566406 -2.171875 6.359375 -1.84375 7.359375 -1.84375 C 8.628906 -1.84375 9.644531 -2.488281 10.40625 -3.78125 L 12.296875 -2.578125 C 11.109375 -0.679688 9.410156 0.265625 7.203125 0.265625 C 5.628906 0.265625 4.332031 -0.296875 3.3125 -1.421875 C 2.289062 -2.554688 1.78125 -4.351562 1.78125 -6.8125 L 1.78125 -8.6875 C 1.78125 -11.132812 2.289062 -12.925781 3.3125 -14.0625 C 4.332031 -15.195312 5.5625 -15.765625 7 -15.765625 C 8.53125 -15.765625 9.78125 -15.238281 10.75 -14.1875 C 11.71875 -13.144531 12.203125 -11.441406 12.203125 -9.078125 Z M 9.890625 -9.328125 C 9.890625 -10.898438 9.617188 -12.015625 9.078125 -12.671875 C 8.535156 -13.328125 7.84375 -13.65625 7 -13.65625 C 6.195312 -13.65625 5.515625 -13.328125 4.953125 -12.671875 C 4.390625 -12.015625 4.109375 -10.898438 4.109375 -9.328125 Z M 9.890625 -9.328125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-7">
+<path style="stroke:none;" d="M 9.703125 1.8125 L 2.46875 1.8125 L 2.46875 -0.265625 L 7.375 -0.265625 L 7.375 -20.609375 L 2.46875 -20.609375 L 2.46875 -22.671875 L 9.703125 -22.671875 Z M 9.703125 1.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-8">
+<path style="stroke:none;" d="M 12.203125 0 L 2.578125 0 L 2.578125 -20.859375 L 12.203125 -20.859375 L 12.203125 -18.875 L 4.90625 -18.875 L 4.90625 -12.125 L 10.296875 -12.125 L 10.296875 -10.140625 L 4.90625 -10.140625 L 4.90625 -1.984375 L 12.203125 -1.984375 Z M 12.203125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-9">
+<path style="stroke:none;" d="M 1.59375 -8.8125 C 1.59375 -11.28125 2.03125 -13.054688 2.90625 -14.140625 C 3.789062 -15.222656 4.847656 -15.765625 6.078125 -15.765625 C 7.671875 -15.765625 8.875 -15.046875 9.6875 -13.609375 L 9.6875 -20.859375 L 12 -20.859375 L 12 0 L 9.6875 0 L 9.6875 -1.859375 C 8.875 -0.441406 7.671875 0.265625 6.078125 0.265625 C 4.847656 0.265625 3.789062 -0.269531 2.90625 -1.34375 C 2.03125 -2.425781 1.59375 -4.203125 1.59375 -6.671875 Z M 3.984375 -6.671875 C 3.984375 -4.878906 4.207031 -3.625 4.65625 -2.90625 C 5.113281 -2.195312 5.78125 -1.84375 6.65625 -1.84375 C 8.1875 -1.84375 9.195312 -2.867188 9.6875 -4.921875 L 9.6875 -10.4375 C 9.207031 -12.582031 8.195312 -13.65625 6.65625 -13.65625 C 5.78125 -13.65625 5.113281 -13.296875 4.65625 -12.578125 C 4.207031 -11.867188 3.984375 -10.613281 3.984375 -8.8125 Z M 3.984375 -6.671875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-10">
+<path style="stroke:none;" d="M 12.953125 -2.15625 C 11.867188 -0.539062 10.414062 0.265625 8.59375 0.265625 C 7.394531 0.265625 6.382812 -0.140625 5.5625 -0.953125 C 4.75 -1.773438 4.34375 -2.984375 4.34375 -4.578125 L 4.34375 -13.5 L 1.75 -13.5 L 1.75 -15.484375 L 4.34375 -15.484375 L 4.34375 -20.859375 L 6.671875 -20.859375 L 6.671875 -15.484375 L 11.890625 -15.484375 L 11.890625 -13.5 L 6.671875 -13.5 L 6.671875 -4.4375 C 6.671875 -3.582031 6.851562 -2.9375 7.21875 -2.5 C 7.582031 -2.0625 8.085938 -1.84375 8.734375 -1.84375 C 9.804688 -1.84375 10.625 -2.378906 11.1875 -3.453125 Z M 12.953125 -2.15625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-11">
+<path style="stroke:none;" d="M 5.6875 0 L 3.109375 -9.609375 C 1.941406 -13.910156 1.359375 -17.273438 1.359375 -19.703125 L 1.359375 -20.859375 L 3.6875 -20.859375 L 3.6875 -19.703125 C 3.6875 -17.691406 4.125 -14.804688 5 -11.046875 L 7 -2.609375 L 9 -11.046875 C 9.875 -14.804688 10.3125 -17.691406 10.3125 -19.703125 L 10.3125 -20.859375 L 12.640625 -20.859375 L 12.640625 -19.703125 C 12.640625 -17.273438 12.054688 -13.910156 10.890625 -9.609375 L 8.3125 0 Z M 5.6875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-12">
+<path style="stroke:none;" d="M 9.578125 -1.28125 L 10.125 -6.296875 C 10.351562 -8.390625 10.46875 -10.53125 10.46875 -12.71875 L 10.46875 -15.484375 L 12.75 -15.484375 L 12.75 -13.421875 C 12.75 -11.554688 12.484375 -9.179688 11.953125 -6.296875 L 10.796875 0 L 8.609375 0 L 7 -6.96875 L 5.390625 0 L 3.203125 0 L 2.046875 -6.296875 C 1.523438 -9.179688 1.265625 -11.554688 1.265625 -13.421875 L 1.265625 -15.484375 L 3.53125 -15.484375 L 3.53125 -12.71875 C 3.53125 -10.53125 3.644531 -8.390625 3.875 -6.296875 L 4.421875 -1.28125 L 6.265625 -9.25 L 7.734375 -9.25 Z M 9.578125 -1.28125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-13">
+<path style="stroke:none;" d="M 9.734375 -1.265625 L 10.09375 -6.1875 C 10.226562 -8.007812 10.296875 -10.070312 10.296875 -12.375 L 10.296875 -20.859375 L 12.625 -20.859375 L 12.625 -15.828125 C 12.625 -12.398438 12.375 -9.1875 11.875 -6.1875 L 10.90625 0 L 8.71875 0 L 7 -8.078125 L 5.28125 0 L 3.09375 0 L 2.125 -6.1875 C 1.625 -9.1875 1.375 -12.398438 1.375 -15.828125 L 1.375 -20.859375 L 3.703125 -20.859375 L 3.703125 -12.375 C 3.703125 -10.070312 3.769531 -8.007812 3.90625 -6.1875 L 4.265625 -1.265625 L 6.265625 -10.671875 L 7.734375 -10.671875 Z M 9.734375 -1.265625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-14">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.03125 C 9.6875 -11.375 9.476562 -12.304688 9.0625 -12.828125 C 8.644531 -13.359375 8.070312 -13.625 7.34375 -13.625 C 5.8125 -13.625 4.800781 -12.570312 4.3125 -10.46875 L 4.3125 0 L 1.984375 0 L 1.984375 -15.484375 L 4.3125 -15.484375 L 4.3125 -13.5 C 5.300781 -15.007812 6.503906 -15.765625 7.921875 -15.765625 C 9.066406 -15.765625 10.035156 -15.335938 10.828125 -14.484375 C 11.617188 -13.628906 12.015625 -12.191406 12.015625 -10.171875 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-15">
+<path style="stroke:none;" d="M 12.21875 -6.703125 C 12.21875 -4.328125 11.726562 -2.570312 10.75 -1.4375 C 9.78125 -0.300781 8.53125 0.265625 7 0.265625 C 5.46875 0.265625 4.21875 -0.300781 3.25 -1.4375 C 2.28125 -2.570312 1.796875 -4.328125 1.796875 -6.703125 L 1.796875 -8.8125 C 1.796875 -11.175781 2.28125 -12.925781 3.25 -14.0625 C 4.21875 -15.195312 5.46875 -15.765625 7 -15.765625 C 8.53125 -15.765625 9.78125 -15.195312 10.75 -14.0625 C 11.726562 -12.925781 12.21875 -11.175781 12.21875 -8.8125 Z M 9.90625 -6.703125 L 9.90625 -8.8125 C 9.90625 -10.632812 9.628906 -11.894531 9.078125 -12.59375 C 8.535156 -13.300781 7.84375 -13.65625 7 -13.65625 C 6.15625 -13.65625 5.460938 -13.300781 4.921875 -12.59375 C 4.378906 -11.894531 4.109375 -10.632812 4.109375 -8.8125 L 4.109375 -6.703125 C 4.109375 -4.867188 4.378906 -3.597656 4.921875 -2.890625 C 5.460938 -2.191406 6.15625 -1.84375 7 -1.84375 C 7.84375 -1.84375 8.535156 -2.191406 9.078125 -2.890625 C 9.628906 -3.597656 9.90625 -4.867188 9.90625 -6.703125 Z M 9.90625 -6.703125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-16">
+<path style="stroke:none;" d="M 7.0625 -1.796875 C 8.019531 -1.796875 8.75 -2.023438 9.25 -2.484375 C 9.75 -2.941406 10 -3.5 10 -4.15625 C 10 -5.0625 9.445312 -5.765625 8.34375 -6.265625 L 5.359375 -7.625 C 3.503906 -8.5 2.578125 -9.75 2.578125 -11.375 C 2.578125 -12.644531 3.03125 -13.691406 3.9375 -14.515625 C 4.851562 -15.347656 6.035156 -15.765625 7.484375 -15.765625 C 9.546875 -15.765625 11.128906 -14.8125 12.234375 -12.90625 L 10.34375 -11.765625 C 9.757812 -13.054688 8.804688 -13.703125 7.484375 -13.703125 C 6.691406 -13.703125 6.0625 -13.5 5.59375 -13.09375 C 5.132812 -12.695312 4.90625 -12.203125 4.90625 -11.609375 C 4.90625 -10.816406 5.421875 -10.175781 6.453125 -9.6875 L 9.265625 -8.375 C 11.296875 -7.425781 12.3125 -5.984375 12.3125 -4.046875 C 12.3125 -2.898438 11.835938 -1.894531 10.890625 -1.03125 C 9.941406 -0.164062 8.65625 0.265625 7.03125 0.265625 C 4.75 0.265625 2.96875 -0.789062 1.6875 -2.90625 L 3.59375 -4.0625 C 4.351562 -2.550781 5.507812 -1.796875 7.0625 -1.796875 Z M 7.0625 -1.796875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-17">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.4375 L 4.3125 -10.4375 L 4.3125 0 L 1.984375 0 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -12.421875 L 9.6875 -12.421875 L 9.6875 -20.859375 L 12.015625 -20.859375 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-18">
+<path style="stroke:none;" d="M 12.421875 -6.65625 C 12.421875 -4.21875 11.953125 -2.453125 11.015625 -1.359375 C 10.085938 -0.273438 9.054688 0.265625 7.921875 0.265625 C 6.328125 0.265625 5.128906 -0.457031 4.328125 -1.90625 L 4.328125 4.828125 L 2 4.828125 L 2 -15.484375 L 4.328125 -15.484375 L 4.328125 -13.640625 C 5.128906 -15.054688 6.328125 -15.765625 7.921875 -15.765625 C 9.054688 -15.765625 10.085938 -15.222656 11.015625 -14.140625 C 11.953125 -13.066406 12.421875 -11.289062 12.421875 -8.8125 Z M 10.015625 -8.8125 C 10.015625 -10.625 9.765625 -11.882812 9.265625 -12.59375 C 8.765625 -13.300781 8.125 -13.65625 7.34375 -13.65625 C 5.8125 -13.65625 4.804688 -12.59375 4.328125 -10.46875 L 4.328125 -5.046875 C 4.796875 -2.910156 5.800781 -1.84375 7.34375 -1.84375 C 8.125 -1.84375 8.765625 -2.203125 9.265625 -2.921875 C 9.765625 -3.640625 10.015625 -4.882812 10.015625 -6.65625 Z M 10.015625 -8.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-19">
+<path style="stroke:none;" d="M 12.953125 -3.09375 C 11.648438 -0.851562 9.878906 0.265625 7.640625 0.265625 C 6.128906 0.265625 4.851562 -0.238281 3.8125 -1.25 C 2.769531 -2.269531 2.25 -4.132812 2.25 -6.84375 L 2.25 -14.046875 C 2.25 -16.679688 2.769531 -18.523438 3.8125 -19.578125 C 4.851562 -20.640625 6.117188 -21.171875 7.609375 -21.171875 C 9.953125 -21.171875 11.6875 -20.039062 12.8125 -17.78125 L 10.90625 -16.625 C 10.21875 -18.238281 9.125 -19.046875 7.625 -19.046875 C 6.757812 -19.046875 6.035156 -18.726562 5.453125 -18.09375 C 4.878906 -17.457031 4.59375 -16.109375 4.59375 -14.046875 L 4.59375 -6.84375 C 4.59375 -4.78125 4.878906 -3.429688 5.453125 -2.796875 C 6.035156 -2.160156 6.757812 -1.84375 7.625 -1.84375 C 9.125 -1.84375 10.265625 -2.6875 11.046875 -4.375 Z M 12.953125 -3.09375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-20">
+<path style="stroke:none;" d="M 12.453125 -18.875 L 8.15625 -18.875 L 8.15625 0 L 5.84375 0 L 5.84375 -18.875 L 1.546875 -18.875 L 1.546875 -20.859375 L 12.453125 -20.859375 Z M 12.453125 -18.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-21">
+<path style="stroke:none;" d="M 12.328125 0 L 10.015625 0 L 10.015625 -5.515625 C 10.015625 -9.023438 10.148438 -13.148438 10.421875 -17.890625 L 7.71875 -9.25 L 6.28125 -9.25 L 3.578125 -17.890625 C 3.859375 -13.148438 4 -9.023438 4 -5.515625 L 4 0 L 1.671875 0 L 1.671875 -20.859375 L 4 -20.859375 L 7 -12.203125 L 10.015625 -20.859375 L 12.328125 -20.859375 Z M 12.328125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-22">
+<path style="stroke:none;" d="M 12.53125 -4.875 C 12.53125 -3.457031 12.054688 -2.242188 11.109375 -1.234375 C 10.171875 -0.234375 8.867188 0.265625 7.203125 0.265625 C 4.578125 0.265625 2.660156 -0.878906 1.453125 -3.171875 L 3.171875 -4.46875 C 4.078125 -2.71875 5.414062 -1.84375 7.1875 -1.84375 C 8.007812 -1.84375 8.71875 -2.09375 9.3125 -2.59375 C 9.90625 -3.101562 10.203125 -3.859375 10.203125 -4.859375 C 10.203125 -5.671875 9.691406 -6.554688 8.671875 -7.515625 L 4.1875 -11.703125 C 2.9375 -12.878906 2.3125 -14.359375 2.3125 -16.140625 C 2.3125 -17.554688 2.769531 -18.75 3.6875 -19.71875 C 4.613281 -20.6875 5.878906 -21.171875 7.484375 -21.171875 C 9.566406 -21.171875 11.253906 -20.21875 12.546875 -18.3125 L 10.765625 -16.90625 C 9.992188 -18.332031 8.90625 -19.046875 7.5 -19.046875 C 6.6875 -19.046875 6.007812 -18.8125 5.46875 -18.34375 C 4.925781 -17.882812 4.65625 -17.148438 4.65625 -16.140625 C 4.65625 -15.109375 5.0625 -14.210938 5.875 -13.453125 L 10.34375 -9.296875 C 11.800781 -7.910156 12.53125 -6.4375 12.53125 -4.875 Z M 12.53125 -4.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-23">
+<path style="stroke:none;" d="M 11.671875 0 L 9.359375 0 L 9.359375 -1.75 C 8.367188 -0.40625 7.125 0.265625 5.625 0.265625 C 4.351562 0.265625 3.328125 -0.164062 2.546875 -1.03125 C 1.765625 -1.90625 1.375 -3 1.375 -4.3125 C 1.375 -5.65625 1.863281 -6.78125 2.84375 -7.6875 C 3.820312 -8.601562 5.253906 -9.0625 7.140625 -9.0625 L 9.359375 -9.0625 L 9.359375 -10.734375 C 9.359375 -12.679688 8.429688 -13.65625 6.578125 -13.65625 C 5.210938 -13.65625 4.191406 -13.054688 3.515625 -11.859375 L 1.75 -13.171875 C 2.96875 -14.898438 4.582031 -15.765625 6.59375 -15.765625 C 8.070312 -15.765625 9.285156 -15.335938 10.234375 -14.484375 C 11.191406 -13.640625 11.671875 -12.390625 11.671875 -10.734375 Z M 9.359375 -3.640625 L 9.359375 -7.1875 L 7.140625 -7.1875 C 6.023438 -7.1875 5.1875 -6.925781 4.625 -6.40625 C 4.0625 -5.894531 3.78125 -5.195312 3.78125 -4.3125 C 3.78125 -2.664062 4.582031 -1.84375 6.1875 -1.84375 C 7.457031 -1.84375 8.515625 -2.441406 9.359375 -3.640625 Z M 9.359375 -3.640625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-24">
+<path style="stroke:none;" d="M 13 -12.859375 L 10.984375 -11.96875 C 10.523438 -13.09375 9.753906 -13.65625 8.671875 -13.65625 C 6.390625 -13.65625 5.25 -11.535156 5.25 -7.296875 L 5.25 0 L 2.921875 0 L 2.921875 -15.484375 L 5.25 -15.484375 L 5.25 -13 C 6.164062 -14.84375 7.425781 -15.765625 9.03125 -15.765625 C 10.894531 -15.765625 12.21875 -14.796875 13 -12.859375 Z M 13 -12.859375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-25">
+<path style="stroke:none;" d="M 12.84375 -2.578125 C 11.65625 -0.679688 9.957031 0.265625 7.75 0.265625 C 6.164062 0.265625 4.863281 -0.296875 3.84375 -1.421875 C 2.820312 -2.554688 2.3125 -4.347656 2.3125 -6.796875 L 2.3125 -8.703125 C 2.3125 -11.140625 2.820312 -12.925781 3.84375 -14.0625 C 4.863281 -15.195312 6.164062 -15.765625 7.75 -15.765625 C 9.957031 -15.765625 11.65625 -14.8125 12.84375 -12.90625 L 10.953125 -11.71875 C 10.179688 -13.007812 9.160156 -13.65625 7.890625 -13.65625 C 6.898438 -13.65625 6.113281 -13.328125 5.53125 -12.671875 C 4.945312 -12.015625 4.65625 -10.691406 4.65625 -8.703125 L 4.65625 -6.796875 C 4.65625 -4.804688 4.945312 -3.484375 5.53125 -2.828125 C 6.113281 -2.171875 6.898438 -1.84375 7.890625 -1.84375 C 9.160156 -1.84375 10.179688 -2.488281 10.953125 -3.78125 Z M 12.84375 -2.578125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-26">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.015625 C 9.6875 -11.398438 9.476562 -12.351562 9.0625 -12.875 C 8.644531 -13.394531 8.070312 -13.65625 7.34375 -13.65625 C 5.8125 -13.65625 4.800781 -12.59375 4.3125 -10.46875 L 4.3125 0 L 1.984375 0 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -13.625 C 5.300781 -15.050781 6.503906 -15.765625 7.921875 -15.765625 C 9.066406 -15.765625 10.035156 -15.335938 10.828125 -14.484375 C 11.617188 -13.628906 12.015625 -12.179688 12.015625 -10.140625 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-27">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -2.015625 C 8.695312 -0.492188 7.492188 0.265625 6.078125 0.265625 C 4.929688 0.265625 3.960938 -0.164062 3.171875 -1.03125 C 2.378906 -1.90625 1.984375 -3.460938 1.984375 -5.703125 L 1.984375 -15.484375 L 4.3125 -15.484375 L 4.3125 -5.703125 C 4.3125 -4.203125 4.519531 -3.179688 4.9375 -2.640625 C 5.351562 -2.109375 5.925781 -1.84375 6.65625 -1.84375 C 8.1875 -1.84375 9.195312 -2.851562 9.6875 -4.875 L 9.6875 -15.484375 L 12.015625 -15.484375 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-28">
+<path style="stroke:none;" d="M 12.28125 -7.296875 L 1.71875 -7.296875 L 1.71875 -9.28125 L 12.28125 -9.28125 Z M 12.28125 -7.296875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-29">
+<path style="stroke:none;" d="M 1.875 -6.328125 L 1.875 -14.5625 C 1.875 -16.851562 2.347656 -18.523438 3.296875 -19.578125 C 4.253906 -20.640625 5.488281 -21.171875 7 -21.171875 C 8.507812 -21.171875 9.738281 -20.640625 10.6875 -19.578125 C 11.644531 -18.523438 12.125 -16.851562 12.125 -14.5625 L 12.125 -6.328125 C 12.125 -4.046875 11.644531 -2.375 10.6875 -1.3125 C 9.738281 -0.257812 8.507812 0.265625 7 0.265625 C 5.488281 0.253906 4.253906 -0.273438 3.296875 -1.328125 C 2.347656 -2.378906 1.875 -4.046875 1.875 -6.328125 Z M 4.21875 -6.328125 C 4.21875 -4.609375 4.457031 -3.429688 4.9375 -2.796875 C 5.425781 -2.160156 6.109375 -1.84375 6.984375 -1.84375 C 7.859375 -1.84375 8.546875 -2.160156 9.046875 -2.796875 C 9.546875 -3.441406 9.796875 -4.617188 9.796875 -6.328125 L 9.796875 -14.5625 C 9.796875 -16.269531 9.546875 -17.441406 9.046875 -18.078125 C 8.546875 -18.722656 7.859375 -19.046875 6.984375 -19.046875 C 6.109375 -19.046875 5.425781 -18.726562 4.9375 -18.09375 C 4.457031 -17.457031 4.21875 -16.28125 4.21875 -14.5625 Z M 4.21875 -6.328125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-30">
+<path style="stroke:none;" d="M 11.453125 0 L 2.53125 0 L 2.53125 -1.984375 L 5.828125 -1.984375 L 5.828125 -18.875 L 3.234375 -18.875 L 3.234375 -20.859375 L 10.75 -20.859375 L 10.75 -18.875 L 8.15625 -18.875 L 8.15625 -1.984375 L 11.453125 -1.984375 Z M 11.453125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-31">
+<path style="stroke:none;" d="M 9.6875 -2.421875 C 9.6875 -1.710938 9.429688 -1.085938 8.921875 -0.546875 C 8.410156 -0.00390625 7.765625 0.265625 6.984375 0.265625 C 6.203125 0.265625 5.5625 -0.00390625 5.0625 -0.546875 C 4.570312 -1.085938 4.328125 -1.710938 4.328125 -2.421875 C 4.328125 -3.117188 4.570312 -3.738281 5.0625 -4.28125 C 5.5625 -4.832031 6.203125 -5.109375 6.984375 -5.109375 C 7.765625 -5.109375 8.410156 -4.832031 8.921875 -4.28125 C 9.429688 -3.726562 9.6875 -3.109375 9.6875 -2.421875 Z M 9.6875 -13.078125 C 9.6875 -12.359375 9.429688 -11.726562 8.921875 -11.1875 C 8.410156 -10.65625 7.765625 -10.390625 6.984375 -10.390625 C 6.203125 -10.390625 5.5625 -10.65625 5.0625 -11.1875 C 4.570312 -11.726562 4.328125 -12.359375 4.328125 -13.078125 C 4.328125 -13.785156 4.570312 -14.410156 5.0625 -14.953125 C 5.5625 -15.492188 6.203125 -15.765625 6.984375 -15.765625 C 7.765625 -15.765625 8.410156 -15.488281 8.921875 -14.9375 C 9.429688 -14.394531 9.6875 -13.773438 9.6875 -13.078125 Z M 9.6875 -13.078125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-32">
+<path style="stroke:none;" d="M 7.421875 0 L 7.421875 -18.0625 L 3.5 -14.984375 L 2.453125 -16.765625 L 7.6875 -20.859375 L 9.765625 -20.859375 L 9.765625 0 Z M 7.421875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-33">
+<path style="stroke:none;" d="M 12.828125 -7.296875 L 8.03125 -7.296875 L 8.03125 -2.46875 L 5.96875 -2.46875 L 5.96875 -7.296875 L 1.171875 -7.296875 L 1.171875 -9.28125 L 5.96875 -9.28125 L 5.96875 -14.125 L 8.03125 -14.125 L 8.03125 -9.28125 L 12.828125 -9.28125 Z M 12.828125 -7.296875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-34">
+<path style="stroke:none;" d="M 12.09375 0 L 2.4375 0 L 2.4375 -1.984375 C 2.4375 -4.128906 3.460938 -6.515625 5.515625 -9.140625 L 8.609375 -13.109375 C 9.378906 -14.085938 9.765625 -15.0625 9.765625 -16.03125 C 9.765625 -17.050781 9.515625 -17.804688 9.015625 -18.296875 C 8.523438 -18.796875 7.851562 -19.046875 7 -19.046875 C 5.519531 -19.046875 4.457031 -18.15625 3.8125 -16.375 L 1.90625 -17.703125 C 3.09375 -20.015625 4.789062 -21.171875 7 -21.171875 C 8.414062 -21.171875 9.617188 -20.679688 10.609375 -19.703125 C 11.597656 -18.734375 12.09375 -17.503906 12.09375 -16.015625 C 12.09375 -14.546875 11.5 -13.054688 10.3125 -11.546875 L 7.265625 -7.625 C 5.597656 -5.488281 4.765625 -3.609375 4.765625 -1.984375 L 12.09375 -1.984375 Z M 12.09375 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-35">
+<path style="stroke:none;" d="M 1.328125 -3.171875 L 3.234375 -4.4375 C 3.878906 -2.707031 5.078125 -1.84375 6.828125 -1.84375 C 7.734375 -1.84375 8.488281 -2.160156 9.09375 -2.796875 C 9.707031 -3.429688 10.015625 -4.550781 10.015625 -6.15625 C 10.015625 -7.664062 9.65625 -8.691406 8.9375 -9.234375 C 8.21875 -9.785156 7.378906 -10.0625 6.421875 -10.0625 L 5.421875 -10.0625 L 5.421875 -12.046875 L 6.421875 -12.046875 C 7.234375 -12.046875 7.960938 -12.332031 8.609375 -12.90625 C 9.253906 -13.488281 9.578125 -14.390625 9.578125 -15.609375 C 9.578125 -16.804688 9.316406 -17.675781 8.796875 -18.21875 C 8.285156 -18.769531 7.628906 -19.046875 6.828125 -19.046875 C 5.347656 -19.046875 4.285156 -18.179688 3.640625 -16.453125 L 1.734375 -17.703125 C 3.023438 -20.015625 4.726562 -21.171875 6.84375 -21.171875 C 8.34375 -21.171875 9.5625 -20.660156 10.5 -19.640625 C 11.445312 -18.628906 11.921875 -17.285156 11.921875 -15.609375 C 11.921875 -13.367188 10.992188 -11.851562 9.140625 -11.0625 C 11.273438 -10.457031 12.34375 -8.820312 12.34375 -6.15625 C 12.34375 -4.0625 11.820312 -2.46875 10.78125 -1.375 C 9.75 -0.28125 8.4375 0.265625 6.84375 0.265625 C 4.445312 0.265625 2.609375 -0.878906 1.328125 -3.171875 Z M 1.328125 -3.171875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-36">
+<path style="stroke:none;" d="M 3.5625 -10.4375 C 3.5625 -15.988281 5.960938 -20.425781 10.765625 -23.75 L 11.9375 -22.15625 C 7.84375 -19.238281 5.796875 -15.332031 5.796875 -10.4375 C 5.796875 -5.53125 7.84375 -1.617188 11.9375 1.296875 L 10.765625 2.890625 C 5.960938 -0.441406 3.5625 -4.882812 3.5625 -10.4375 Z M 3.5625 -10.4375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-37">
+<path style="stroke:none;" d="M 12.1875 -5.65625 L 10.828125 -5.65625 L 10.828125 0 L 8.5 0 L 8.5 -5.65625 L 1.15625 -5.65625 L 1.15625 -7.640625 L 7.96875 -20.859375 L 10.828125 -20.859375 L 10.828125 -7.640625 L 12.1875 -7.640625 Z M 8.5 -7.640625 L 8.5 -17.46875 L 3.4375 -7.640625 Z M 8.5 -7.640625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-38">
+<path style="stroke:none;" d="M 10.4375 -10.4375 C 10.4375 -4.882812 8.035156 -0.441406 3.234375 2.890625 L 2.0625 1.296875 C 6.15625 -1.617188 8.203125 -5.53125 8.203125 -10.4375 C 8.203125 -15.332031 6.15625 -19.238281 2.0625 -22.15625 L 3.234375 -23.75 C 8.035156 -20.425781 10.4375 -15.988281 10.4375 -10.4375 Z M 10.4375 -10.4375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-39">
+<path style="stroke:none;" d="M 7 -7.75 L 4.3125 -3.6875 L 2.53125 -4.953125 L 5.53125 -8.8125 L 0.828125 -10.140625 L 1.5 -12.21875 L 6.0625 -10.546875 L 5.859375 -15.484375 L 8.125 -15.484375 L 7.9375 -10.546875 L 12.5 -12.21875 L 13.171875 -10.140625 L 8.46875 -8.8125 L 11.453125 -4.953125 L 9.6875 -3.671875 Z M 7 -7.75 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-40">
+<path style="stroke:none;" d="M 1.421875 -10.453125 C 1.421875 -11.898438 1.882812 -14.296875 2.8125 -17.640625 C 3.4375 -19.992188 4.8125 -21.171875 6.9375 -21.171875 C 9.144531 -21.171875 10.5625 -19.992188 11.1875 -17.640625 C 12.113281 -14.296875 12.578125 -11.898438 12.578125 -10.453125 C 12.578125 -9.003906 12.113281 -6.609375 11.1875 -3.265625 C 10.5625 -0.910156 9.144531 0.265625 6.9375 0.265625 C 4.8125 0.265625 3.4375 -0.910156 2.8125 -3.265625 C 1.882812 -6.609375 1.421875 -9.003906 1.421875 -10.453125 Z M 10.25 -10.453125 C 10.25 -11.910156 9.84375 -14.109375 9.03125 -17.046875 C 8.695312 -18.359375 8 -19.019531 6.9375 -19.03125 C 5.957031 -19.019531 5.304688 -18.359375 4.984375 -17.046875 C 4.160156 -14.109375 3.75 -11.910156 3.75 -10.453125 C 3.75 -8.992188 4.160156 -6.785156 4.984375 -3.828125 C 5.304688 -2.515625 5.957031 -1.851562 6.9375 -1.84375 C 8 -1.851562 8.695312 -2.515625 9.03125 -3.828125 C 9.84375 -6.785156 10.25 -8.992188 10.25 -10.453125 Z M 8.890625 -8.265625 L 7.453125 -8.265625 L 5.109375 -12.953125 L 6.546875 -12.953125 Z M 8.890625 -8.265625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-41">
+<path style="stroke:none;" d="M 9.6875 -2.421875 C 9.6875 -1.710938 9.429688 -1.085938 8.921875 -0.546875 C 8.410156 -0.00390625 7.765625 0.265625 6.984375 0.265625 C 6.203125 0.265625 5.5625 -0.00390625 5.0625 -0.546875 C 4.570312 -1.085938 4.328125 -1.710938 4.328125 -2.421875 C 4.328125 -3.117188 4.570312 -3.738281 5.0625 -4.28125 C 5.5625 -4.832031 6.203125 -5.109375 6.984375 -5.109375 C 7.765625 -5.109375 8.410156 -4.832031 8.921875 -4.28125 C 9.429688 -3.726562 9.6875 -3.109375 9.6875 -2.421875 Z M 9.6875 -2.421875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-42">
+<path style="stroke:none;" d="M 12.171875 -6.515625 C 12.171875 -4.359375 11.648438 -2.6875 10.609375 -1.5 C 9.566406 -0.320312 8.300781 0.265625 6.8125 0.265625 C 4.550781 0.265625 2.847656 -0.804688 1.703125 -2.953125 L 3.609375 -4.21875 C 4.253906 -2.632812 5.316406 -1.84375 6.796875 -1.84375 C 7.597656 -1.84375 8.300781 -2.203125 8.90625 -2.921875 C 9.519531 -3.640625 9.828125 -4.835938 9.828125 -6.515625 C 9.828125 -8.203125 9.523438 -9.382812 8.921875 -10.0625 C 8.328125 -10.738281 7.351562 -11.078125 6 -11.078125 L 2.90625 -11.078125 L 2.90625 -20.859375 L 11.734375 -20.859375 L 11.734375 -18.875 L 5.234375 -18.875 L 5.234375 -13.1875 L 6.40625 -13.1875 C 8.132812 -13.1875 9.523438 -12.664062 10.578125 -11.625 C 11.640625 -10.59375 12.171875 -8.890625 12.171875 -6.515625 Z M 12.171875 -6.515625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-43">
+<path style="stroke:none;" d="M 1.453125 -14.609375 C 1.453125 -16.691406 1.941406 -18.304688 2.921875 -19.453125 C 3.898438 -20.597656 5.148438 -21.171875 6.671875 -21.171875 C 8.085938 -21.171875 9.3125 -20.597656 10.34375 -19.453125 C 11.375 -18.304688 11.890625 -16.375 11.890625 -13.65625 L 11.890625 -7.5 C 11.890625 -4.875 11.394531 -2.921875 10.40625 -1.640625 C 9.414062 -0.367188 8.082031 0.265625 6.40625 0.265625 C 4.550781 0.265625 3.019531 -0.679688 1.8125 -2.578125 L 3.71875 -3.828125 C 4.3125 -2.503906 5.207031 -1.84375 6.40625 -1.84375 C 7.382812 -1.84375 8.15625 -2.242188 8.71875 -3.046875 C 9.289062 -3.859375 9.578125 -5.34375 9.578125 -7.5 L 9.578125 -10.40625 C 8.785156 -8.875 7.582031 -8.109375 5.96875 -8.109375 C 4.613281 -8.109375 3.519531 -8.660156 2.6875 -9.765625 C 1.863281 -10.878906 1.453125 -12.492188 1.453125 -14.609375 Z M 3.78125 -14.609375 C 3.78125 -12.898438 4.019531 -11.738281 4.5 -11.125 C 4.988281 -10.507812 5.625 -10.203125 6.40625 -10.203125 C 8.070312 -10.203125 9.128906 -11.269531 9.578125 -13.40625 C 9.578125 -15.8125 9.300781 -17.351562 8.75 -18.03125 C 8.195312 -18.707031 7.5 -19.046875 6.65625 -19.046875 C 5.851562 -19.046875 5.171875 -18.707031 4.609375 -18.03125 C 4.054688 -17.351562 3.78125 -16.210938 3.78125 -14.609375 Z M 3.78125 -14.609375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-44">
+<path style="stroke:none;" d="M 12.546875 -6.28125 C 12.546875 -4.195312 12.054688 -2.582031 11.078125 -1.4375 C 10.097656 -0.300781 8.847656 0.265625 7.328125 0.265625 C 5.910156 0.265625 4.6875 -0.300781 3.65625 -1.4375 C 2.625 -2.582031 2.109375 -4.515625 2.109375 -7.234375 L 2.109375 -13.40625 C 2.109375 -16.03125 2.601562 -17.976562 3.59375 -19.25 C 4.59375 -20.53125 5.921875 -21.171875 7.578125 -21.171875 C 9.441406 -21.171875 10.976562 -20.21875 12.1875 -18.3125 L 10.28125 -16.984375 C 9.6875 -18.359375 8.796875 -19.046875 7.609375 -19.046875 C 6.617188 -19.046875 5.84375 -18.640625 5.28125 -17.828125 C 4.71875 -17.023438 4.4375 -15.550781 4.4375 -13.40625 L 4.4375 -10.484375 C 5.269531 -12.015625 6.46875 -12.78125 8.03125 -12.78125 C 9.382812 -12.78125 10.472656 -12.226562 11.296875 -11.125 C 12.128906 -10.019531 12.546875 -8.40625 12.546875 -6.28125 Z M 10.234375 -6.28125 C 10.234375 -7.96875 9.988281 -9.117188 9.5 -9.734375 C 9.007812 -10.359375 8.378906 -10.671875 7.609375 -10.671875 C 5.941406 -10.671875 4.882812 -9.675781 4.4375 -7.6875 C 4.4375 -5.15625 4.707031 -3.546875 5.25 -2.859375 C 5.800781 -2.179688 6.5 -1.84375 7.34375 -1.84375 C 8.144531 -1.84375 8.828125 -2.179688 9.390625 -2.859375 C 9.953125 -3.546875 10.234375 -4.6875 10.234375 -6.28125 Z M 10.234375 -6.28125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-45">
+<path style="stroke:none;" d="M 6.984375 0.265625 C 5.398438 0.265625 4.085938 -0.234375 3.046875 -1.234375 C 2.003906 -2.242188 1.484375 -3.601562 1.484375 -5.3125 C 1.484375 -7.863281 2.675781 -9.820312 5.0625 -11.1875 C 3.050781 -12.59375 2.046875 -14.242188 2.046875 -16.140625 C 2.046875 -17.648438 2.515625 -18.863281 3.453125 -19.78125 C 4.398438 -20.707031 5.578125 -21.171875 6.984375 -21.171875 C 8.410156 -21.171875 9.59375 -20.707031 10.53125 -19.78125 C 11.476562 -18.863281 11.953125 -17.648438 11.953125 -16.140625 C 11.953125 -14.242188 10.945312 -12.59375 8.9375 -11.1875 C 11.320312 -9.820312 12.515625 -7.863281 12.515625 -5.3125 C 12.515625 -3.601562 11.992188 -2.242188 10.953125 -1.234375 C 9.910156 -0.234375 8.585938 0.265625 6.984375 0.265625 Z M 6.984375 -19.046875 C 6.222656 -19.046875 5.597656 -18.796875 5.109375 -18.296875 C 4.617188 -17.804688 4.375 -17.085938 4.375 -16.140625 C 4.375 -14.609375 5.242188 -13.304688 6.984375 -12.234375 C 8.742188 -13.304688 9.625 -14.609375 9.625 -16.140625 C 9.625 -17.085938 9.378906 -17.804688 8.890625 -18.296875 C 8.410156 -18.796875 7.773438 -19.046875 6.984375 -19.046875 Z M 6.984375 -9.875 C 4.867188 -8.832031 3.8125 -7.3125 3.8125 -5.3125 C 3.8125 -4.050781 4.101562 -3.15625 4.6875 -2.625 C 5.28125 -2.101562 6.046875 -1.84375 6.984375 -1.84375 C 7.941406 -1.84375 8.710938 -2.101562 9.296875 -2.625 C 9.890625 -3.15625 10.1875 -4.050781 10.1875 -5.3125 C 10.1875 -7.3125 9.117188 -8.832031 6.984375 -9.875 Z M 6.984375 -9.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-46">
+<path style="stroke:none;" d="M 8.8125 -10.5625 C 7.5 -7.9375 6.804688 -5.113281 6.734375 -2.09375 L 6.65625 0 L 4.328125 0 L 4.40625 -2.25 C 4.488281 -5.195312 5.367188 -8.347656 7.046875 -11.703125 L 8.921875 -15.484375 C 9.578125 -16.765625 9.90625 -17.894531 9.90625 -18.875 L 1.859375 -18.875 L 1.859375 -20.859375 L 12.21875 -20.859375 L 12.21875 -19.90625 C 12.21875 -18.195312 11.707031 -16.34375 10.6875 -14.34375 Z M 8.8125 -10.5625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-47">
+<path style="stroke:none;" d="M 4.90625 -3.234375 C 4.175781 -2.210938 3.773438 -1.132812 3.703125 0 L 1.375 0 L 1.40625 -0.53125 C 1.46875 -1.550781 2.050781 -2.835938 3.15625 -4.390625 L 5.703125 -7.984375 L 3.515625 -11.09375 C 2.410156 -12.632812 1.828125 -13.921875 1.765625 -14.953125 L 1.75 -15.484375 L 4.078125 -15.484375 C 4.140625 -14.359375 4.535156 -13.28125 5.265625 -12.25 L 7 -9.8125 L 8.734375 -12.25 C 9.460938 -13.28125 9.859375 -14.359375 9.921875 -15.484375 L 12.25 -15.484375 L 12.234375 -14.953125 C 12.171875 -13.921875 11.585938 -12.632812 10.484375 -11.09375 L 8.28125 -7.984375 L 10.84375 -4.390625 C 11.945312 -2.835938 12.53125 -1.550781 12.59375 -0.53125 L 12.625 0 L 10.296875 0 C 10.222656 -1.132812 9.820312 -2.210938 9.09375 -3.234375 L 7 -6.171875 Z M 4.90625 -3.234375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-48">
+<path style="stroke:none;" d="M 12.015625 -6.265625 C 12.015625 -4.015625 11.53125 -2.363281 10.5625 -1.3125 C 9.59375 -0.257812 8.40625 0.265625 7 0.265625 C 5.59375 0.265625 4.40625 -0.257812 3.4375 -1.3125 C 2.46875 -2.363281 1.984375 -4.015625 1.984375 -6.265625 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -6.265625 C 4.3125 -4.535156 4.566406 -3.363281 5.078125 -2.75 C 5.597656 -2.144531 6.234375 -1.84375 6.984375 -1.84375 C 7.742188 -1.84375 8.382812 -2.144531 8.90625 -2.75 C 9.425781 -3.363281 9.6875 -4.535156 9.6875 -6.265625 L 9.6875 -20.859375 L 12.015625 -20.859375 Z M 12.015625 -6.265625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-49">
+<path style="stroke:none;" d="M 12.671875 -8.5625 C 12.671875 -5.832031 12.046875 -3.722656 10.796875 -2.234375 C 9.554688 -0.742188 7.875 0 5.75 0 L 2.296875 0 L 2.296875 -20.859375 L 5.75 -20.859375 C 7.875 -20.859375 9.554688 -20.15625 10.796875 -18.75 C 12.046875 -17.351562 12.671875 -15.300781 12.671875 -12.59375 Z M 10.28125 -8.5625 L 10.28125 -12.59375 C 10.28125 -14.71875 9.875 -16.296875 9.0625 -17.328125 C 8.25 -18.359375 7.144531 -18.875 5.75 -18.875 L 4.609375 -18.875 L 4.609375 -1.984375 L 5.75 -1.984375 C 7.144531 -1.984375 8.25 -2.535156 9.0625 -3.640625 C 9.875 -4.742188 10.28125 -6.382812 10.28125 -8.5625 Z M 10.28125 -8.5625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-50">
+<path style="stroke:none;" d="M -0.265625 -9.421875 L -0.265625 -11.390625 L 14.265625 -11.390625 L 14.265625 -9.421875 Z M -0.265625 -9.421875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-51">
+<path style="stroke:none;" d="M 1.59375 -13.4375 L 1.59375 -15.9375 L 12.9375 -9.765625 L 12.9375 -6.828125 L 1.59375 -0.640625 L 1.59375 -3.140625 L 11.234375 -8.296875 Z M 1.59375 -13.4375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-52">
+<path style="stroke:none;" d="M 7.265625 -9.515625 L 4.84375 -9.515625 L 4.84375 0 L 2.515625 0 L 2.515625 -20.859375 L 7.0625 -20.859375 C 8.914062 -20.859375 10.289062 -20.359375 11.1875 -19.359375 C 12.09375 -18.359375 12.546875 -17.015625 12.546875 -15.328125 C 12.546875 -13.898438 12.253906 -12.722656 11.671875 -11.796875 C 11.097656 -10.878906 10.382812 -10.257812 9.53125 -9.9375 L 9.78125 -9.5625 C 11.507812 -6.863281 12.460938 -3.988281 12.640625 -0.9375 L 12.703125 0 L 10.375 0 L 10.34375 -0.71875 C 10.164062 -3.632812 9.253906 -6.378906 7.609375 -8.953125 Z M 4.84375 -11.484375 L 7.0625 -11.484375 C 8.132812 -11.484375 8.914062 -11.800781 9.40625 -12.4375 C 9.894531 -13.082031 10.140625 -14.050781 10.140625 -15.34375 C 10.140625 -16.613281 9.894531 -17.519531 9.40625 -18.0625 C 8.914062 -18.601562 8.132812 -18.875 7.0625 -18.875 L 4.84375 -18.875 Z M 4.84375 -11.484375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-53">
+<path style="stroke:none;" d="M 4.140625 2.5 L 5.921875 -2.125 L 2.703125 -10.015625 C 1.941406 -11.910156 1.53125 -13.332031 1.46875 -14.28125 L 1.390625 -15.484375 L 3.71875 -15.484375 L 3.78125 -14.375 C 3.820312 -13.738281 4.179688 -12.503906 4.859375 -10.671875 L 7 -4.828125 L 9.140625 -10.59375 C 9.804688 -12.457031 10.164062 -13.71875 10.21875 -14.375 L 10.28125 -15.484375 L 12.609375 -15.484375 L 12.53125 -14.28125 C 12.476562 -13.4375 12.066406 -12.015625 11.296875 -10.015625 L 6.359375 2.796875 C 6.179688 3.234375 6.078125 3.742188 6.046875 4.328125 L 6.015625 4.828125 L 3.6875 4.828125 L 3.71875 4.328125 C 3.757812 3.742188 3.898438 3.132812 4.140625 2.5 Z M 4.140625 2.5 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-54">
+<path style="stroke:none;" d="M 0 -7.125 L 0 -9.109375 L 14 -9.109375 L 14 -7.125 Z M 0 -11.71875 L 0 -13.703125 L 14 -13.703125 L 14 -11.71875 Z M 0 -11.71875 "/>
+</symbol>
+</g>
+<clipPath id="clip1">
+ <path d="M 17 603 L 1138 603 L 1138 606 L 17 606 Z M 17 603 "/>
+</clipPath>
+</defs>
+<g id="surface9134">
+<rect x="0" y="0" width="1151" height="713" style="fill:rgb(15.686275%,17.254902%,20.392157%);fill-opacity:1;stroke:none;"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 73 620 L 87 620 L 87 651 L 73 651 Z M 73 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 17 0 L 577 0 L 577 31 L 17 31 Z M 17 0 "/>
+<g style="fill:rgb(84.705882%,87.058824%,91.372549%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="17" y="26"/>
+ <use xlink:href="#glyph0-2" x="31" y="26"/>
+ <use xlink:href="#glyph0-3" x="45" y="26"/>
+ <use xlink:href="#glyph0-4" x="59" y="26"/>
+ <use xlink:href="#glyph0-5" x="73" y="26"/>
+ <use xlink:href="#glyph0-6" x="87" y="26"/>
+ <use xlink:href="#glyph0-7" x="101" y="26"/>
+ <use xlink:href="#glyph0-1" x="115" y="26"/>
+ <use xlink:href="#glyph0-2" x="129" y="26"/>
+ <use xlink:href="#glyph0-8" x="143" y="26"/>
+ <use xlink:href="#glyph0-9" x="157" y="26"/>
+ <use xlink:href="#glyph0-4" x="171" y="26"/>
+ <use xlink:href="#glyph0-10" x="185" y="26"/>
+ <use xlink:href="#glyph0-7" x="199" y="26"/>
+ <use xlink:href="#glyph0-1" x="213" y="26"/>
+ <use xlink:href="#glyph0-2" x="227" y="26"/>
+ <use xlink:href="#glyph0-11" x="241" y="26"/>
+ <use xlink:href="#glyph0-4" x="255" y="26"/>
+ <use xlink:href="#glyph0-6" x="269" y="26"/>
+ <use xlink:href="#glyph0-12" x="283" y="26"/>
+ <use xlink:href="#glyph0-7" x="297" y="26"/>
+ <use xlink:href="#glyph0-1" x="311" y="26"/>
+ <use xlink:href="#glyph0-2" x="325" y="26"/>
+ <use xlink:href="#glyph0-13" x="339" y="26"/>
+ <use xlink:href="#glyph0-4" x="353" y="26"/>
+ <use xlink:href="#glyph0-14" x="367" y="26"/>
+ <use xlink:href="#glyph0-9" x="381" y="26"/>
+ <use xlink:href="#glyph0-15" x="395" y="26"/>
+ <use xlink:href="#glyph0-12" x="409" y="26"/>
+ <use xlink:href="#glyph0-16" x="423" y="26"/>
+ <use xlink:href="#glyph0-7" x="437" y="26"/>
+ <use xlink:href="#glyph0-1" x="451" y="26"/>
+ <use xlink:href="#glyph0-2" x="465" y="26"/>
+ <use xlink:href="#glyph0-17" x="479" y="26"/>
+ <use xlink:href="#glyph0-6" x="493" y="26"/>
+ <use xlink:href="#glyph0-5" x="507" y="26"/>
+ <use xlink:href="#glyph0-18" x="521" y="26"/>
+ <use xlink:href="#glyph0-7" x="535" y="26"/>
+ <use xlink:href="#glyph0-1" x="549" y="26"/>
+ <use xlink:href="#glyph0-1" x="563" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 577 0 L 815 0 L 815 31 L 577 31 Z M 577 0 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="577" y="26"/>
+ <use xlink:href="#glyph0-15" x="591" y="26"/>
+ <use xlink:href="#glyph0-14" x="605" y="26"/>
+ <use xlink:href="#glyph0-16" x="619" y="26"/>
+ <use xlink:href="#glyph0-15" x="633" y="26"/>
+ <use xlink:href="#glyph0-5" x="647" y="26"/>
+ <use xlink:href="#glyph0-6" x="661" y="26"/>
+ <use xlink:href="#glyph0-1" x="675" y="26"/>
+ <use xlink:href="#glyph0-20" x="689" y="26"/>
+ <use xlink:href="#glyph0-6" x="703" y="26"/>
+ <use xlink:href="#glyph0-16" x="717" y="26"/>
+ <use xlink:href="#glyph0-10" x="731" y="26"/>
+ <use xlink:href="#glyph0-1" x="745" y="26"/>
+ <use xlink:href="#glyph0-21" x="759" y="26"/>
+ <use xlink:href="#glyph0-15" x="773" y="26"/>
+ <use xlink:href="#glyph0-9" x="787" y="26"/>
+ <use xlink:href="#glyph0-6" x="801" y="26"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="578" y="26"/>
+ <use xlink:href="#glyph0-15" x="592" y="26"/>
+ <use xlink:href="#glyph0-14" x="606" y="26"/>
+ <use xlink:href="#glyph0-16" x="620" y="26"/>
+ <use xlink:href="#glyph0-15" x="634" y="26"/>
+ <use xlink:href="#glyph0-5" x="648" y="26"/>
+ <use xlink:href="#glyph0-6" x="662" y="26"/>
+ <use xlink:href="#glyph0-1" x="676" y="26"/>
+ <use xlink:href="#glyph0-20" x="690" y="26"/>
+ <use xlink:href="#glyph0-6" x="704" y="26"/>
+ <use xlink:href="#glyph0-16" x="718" y="26"/>
+ <use xlink:href="#glyph0-10" x="732" y="26"/>
+ <use xlink:href="#glyph0-1" x="746" y="26"/>
+ <use xlink:href="#glyph0-21" x="760" y="26"/>
+ <use xlink:href="#glyph0-15" x="774" y="26"/>
+ <use xlink:href="#glyph0-9" x="788" y="26"/>
+ <use xlink:href="#glyph0-6" x="802" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 815 0 L 843 0 L 843 31 L 815 31 Z M 815 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 843 0 L 857 0 L 857 31 L 843 31 Z M 843 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 857 0 L 1025 0 L 1025 31 L 857 31 Z M 857 0 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-22" x="857" y="26"/>
+ <use xlink:href="#glyph0-6" x="871" y="26"/>
+ <use xlink:href="#glyph0-23" x="885" y="26"/>
+ <use xlink:href="#glyph0-24" x="899" y="26"/>
+ <use xlink:href="#glyph0-25" x="913" y="26"/>
+ <use xlink:href="#glyph0-26" x="927" y="26"/>
+ <use xlink:href="#glyph0-1" x="941" y="26"/>
+ <use xlink:href="#glyph0-21" x="955" y="26"/>
+ <use xlink:href="#glyph0-6" x="969" y="26"/>
+ <use xlink:href="#glyph0-14" x="983" y="26"/>
+ <use xlink:href="#glyph0-27" x="997" y="26"/>
+ <use xlink:href="#glyph0-1" x="1011" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1025 0 L 1109 0 L 1109 31 L 1025 31 Z M 1025 0 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="1025" y="26"/>
+ <use xlink:href="#glyph0-10" x="1039" y="26"/>
+ <use xlink:href="#glyph0-24" x="1053" y="26"/>
+ <use xlink:href="#glyph0-5" x="1067" y="26"/>
+ <use xlink:href="#glyph0-28" x="1081" y="26"/>
+ <use xlink:href="#glyph0-18" x="1095" y="26"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="1026" y="26"/>
+ <use xlink:href="#glyph0-10" x="1040" y="26"/>
+ <use xlink:href="#glyph0-24" x="1054" y="26"/>
+ <use xlink:href="#glyph0-5" x="1068" y="26"/>
+ <use xlink:href="#glyph0-28" x="1082" y="26"/>
+ <use xlink:href="#glyph0-18" x="1096" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1109 0 L 1123 0 L 1123 31 L 1109 31 Z M 1109 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 1123 0 L 1137 0 L 1137 31 L 1123 31 Z M 1123 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 0 L 1151 0 L 1151 31 L 1137 31 Z M 1137 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 0 L 17 0 L 17 31 L 0 31 Z M 0 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 31 L 1137 31 L 1137 62 L 17 62 Z M 17 31 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="17" y="57"/>
+ <use xlink:href="#glyph0-23" x="31" y="57"/>
+ <use xlink:href="#glyph0-5" x="45" y="57"/>
+ <use xlink:href="#glyph0-25" x="59" y="57"/>
+ <use xlink:href="#glyph0-27" x="73" y="57"/>
+ <use xlink:href="#glyph0-5" x="87" y="57"/>
+ <use xlink:href="#glyph0-23" x="101" y="57"/>
+ <use xlink:href="#glyph0-10" x="115" y="57"/>
+ <use xlink:href="#glyph0-15" x="129" y="57"/>
+ <use xlink:href="#glyph0-24" x="143" y="57"/>
+ <use xlink:href="#glyph0-1" x="157" y="57"/>
+ <use xlink:href="#glyph0-29" x="171" y="57"/>
+ <use xlink:href="#glyph0-27" x="185" y="57"/>
+ <use xlink:href="#glyph0-10" x="199" y="57"/>
+ <use xlink:href="#glyph0-18" x="213" y="57"/>
+ <use xlink:href="#glyph0-27" x="227" y="57"/>
+ <use xlink:href="#glyph0-10" x="241" y="57"/>
+ <use xlink:href="#glyph0-1" x="255" y="57"/>
+ <use xlink:href="#glyph0-1" x="269" y="57"/>
+ <use xlink:href="#glyph0-1" x="283" y="57"/>
+ <use xlink:href="#glyph0-1" x="297" y="57"/>
+ <use xlink:href="#glyph0-1" x="311" y="57"/>
+ <use xlink:href="#glyph0-1" x="325" y="57"/>
+ <use xlink:href="#glyph0-1" x="339" y="57"/>
+ <use xlink:href="#glyph0-1" x="353" y="57"/>
+ <use xlink:href="#glyph0-1" x="367" y="57"/>
+ <use xlink:href="#glyph0-1" x="381" y="57"/>
+ <use xlink:href="#glyph0-1" x="395" y="57"/>
+ <use xlink:href="#glyph0-1" x="409" y="57"/>
+ <use xlink:href="#glyph0-1" x="423" y="57"/>
+ <use xlink:href="#glyph0-1" x="437" y="57"/>
+ <use xlink:href="#glyph0-1" x="451" y="57"/>
+ <use xlink:href="#glyph0-1" x="465" y="57"/>
+ <use xlink:href="#glyph0-1" x="479" y="57"/>
+ <use xlink:href="#glyph0-1" x="493" y="57"/>
+ <use xlink:href="#glyph0-1" x="507" y="57"/>
+ <use xlink:href="#glyph0-1" x="521" y="57"/>
+ <use xlink:href="#glyph0-1" x="535" y="57"/>
+ <use xlink:href="#glyph0-1" x="549" y="57"/>
+ <use xlink:href="#glyph0-1" x="563" y="57"/>
+ <use xlink:href="#glyph0-1" x="577" y="57"/>
+ <use xlink:href="#glyph0-1" x="591" y="57"/>
+ <use xlink:href="#glyph0-1" x="605" y="57"/>
+ <use xlink:href="#glyph0-1" x="619" y="57"/>
+ <use xlink:href="#glyph0-1" x="633" y="57"/>
+ <use xlink:href="#glyph0-1" x="647" y="57"/>
+ <use xlink:href="#glyph0-1" x="661" y="57"/>
+ <use xlink:href="#glyph0-1" x="675" y="57"/>
+ <use xlink:href="#glyph0-1" x="689" y="57"/>
+ <use xlink:href="#glyph0-1" x="703" y="57"/>
+ <use xlink:href="#glyph0-1" x="717" y="57"/>
+ <use xlink:href="#glyph0-1" x="731" y="57"/>
+ <use xlink:href="#glyph0-1" x="745" y="57"/>
+ <use xlink:href="#glyph0-1" x="759" y="57"/>
+ <use xlink:href="#glyph0-1" x="773" y="57"/>
+ <use xlink:href="#glyph0-1" x="787" y="57"/>
+ <use xlink:href="#glyph0-1" x="801" y="57"/>
+ <use xlink:href="#glyph0-1" x="815" y="57"/>
+ <use xlink:href="#glyph0-1" x="829" y="57"/>
+ <use xlink:href="#glyph0-1" x="843" y="57"/>
+ <use xlink:href="#glyph0-1" x="857" y="57"/>
+ <use xlink:href="#glyph0-1" x="871" y="57"/>
+ <use xlink:href="#glyph0-1" x="885" y="57"/>
+ <use xlink:href="#glyph0-1" x="899" y="57"/>
+ <use xlink:href="#glyph0-1" x="913" y="57"/>
+ <use xlink:href="#glyph0-1" x="927" y="57"/>
+ <use xlink:href="#glyph0-1" x="941" y="57"/>
+ <use xlink:href="#glyph0-1" x="955" y="57"/>
+ <use xlink:href="#glyph0-1" x="969" y="57"/>
+ <use xlink:href="#glyph0-1" x="983" y="57"/>
+ <use xlink:href="#glyph0-1" x="997" y="57"/>
+ <use xlink:href="#glyph0-1" x="1011" y="57"/>
+ <use xlink:href="#glyph0-1" x="1025" y="57"/>
+ <use xlink:href="#glyph0-1" x="1039" y="57"/>
+ <use xlink:href="#glyph0-1" x="1053" y="57"/>
+ <use xlink:href="#glyph0-1" x="1067" y="57"/>
+ <use xlink:href="#glyph0-1" x="1081" y="57"/>
+ <use xlink:href="#glyph0-1" x="1095" y="57"/>
+ <use xlink:href="#glyph0-1" x="1109" y="57"/>
+ <use xlink:href="#glyph0-1" x="1123" y="57"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 31 L 1151 31 L 1151 62 L 1137 62 Z M 1137 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 31 L 17 31 L 17 62 L 0 62 Z M 0 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 62 L 1137 62 L 1137 93 L 17 93 Z M 17 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 62 L 1151 62 L 1151 93 L 1137 93 Z M 1137 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 62 L 17 62 L 17 93 L 0 93 Z M 0 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 93 L 1137 93 L 1137 124 L 17 124 Z M 17 93 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-30" x="17" y="119"/>
+ <use xlink:href="#glyph0-14" x="31" y="119"/>
+ <use xlink:href="#glyph0-31" x="45" y="119"/>
+ <use xlink:href="#glyph0-1" x="59" y="119"/>
+ <use xlink:href="#glyph0-1" x="73" y="119"/>
+ <use xlink:href="#glyph0-32" x="87" y="119"/>
+ <use xlink:href="#glyph0-33" x="101" y="119"/>
+ <use xlink:href="#glyph0-34" x="115" y="119"/>
+ <use xlink:href="#glyph0-1" x="129" y="119"/>
+ <use xlink:href="#glyph0-1" x="143" y="119"/>
+ <use xlink:href="#glyph0-1" x="157" y="119"/>
+ <use xlink:href="#glyph0-1" x="171" y="119"/>
+ <use xlink:href="#glyph0-1" x="185" y="119"/>
+ <use xlink:href="#glyph0-1" x="199" y="119"/>
+ <use xlink:href="#glyph0-1" x="213" y="119"/>
+ <use xlink:href="#glyph0-1" x="227" y="119"/>
+ <use xlink:href="#glyph0-1" x="241" y="119"/>
+ <use xlink:href="#glyph0-1" x="255" y="119"/>
+ <use xlink:href="#glyph0-1" x="269" y="119"/>
+ <use xlink:href="#glyph0-1" x="283" y="119"/>
+ <use xlink:href="#glyph0-1" x="297" y="119"/>
+ <use xlink:href="#glyph0-1" x="311" y="119"/>
+ <use xlink:href="#glyph0-1" x="325" y="119"/>
+ <use xlink:href="#glyph0-1" x="339" y="119"/>
+ <use xlink:href="#glyph0-1" x="353" y="119"/>
+ <use xlink:href="#glyph0-1" x="367" y="119"/>
+ <use xlink:href="#glyph0-1" x="381" y="119"/>
+ <use xlink:href="#glyph0-1" x="395" y="119"/>
+ <use xlink:href="#glyph0-1" x="409" y="119"/>
+ <use xlink:href="#glyph0-1" x="423" y="119"/>
+ <use xlink:href="#glyph0-1" x="437" y="119"/>
+ <use xlink:href="#glyph0-1" x="451" y="119"/>
+ <use xlink:href="#glyph0-1" x="465" y="119"/>
+ <use xlink:href="#glyph0-1" x="479" y="119"/>
+ <use xlink:href="#glyph0-1" x="493" y="119"/>
+ <use xlink:href="#glyph0-1" x="507" y="119"/>
+ <use xlink:href="#glyph0-1" x="521" y="119"/>
+ <use xlink:href="#glyph0-1" x="535" y="119"/>
+ <use xlink:href="#glyph0-1" x="549" y="119"/>
+ <use xlink:href="#glyph0-1" x="563" y="119"/>
+ <use xlink:href="#glyph0-1" x="577" y="119"/>
+ <use xlink:href="#glyph0-1" x="591" y="119"/>
+ <use xlink:href="#glyph0-1" x="605" y="119"/>
+ <use xlink:href="#glyph0-1" x="619" y="119"/>
+ <use xlink:href="#glyph0-1" x="633" y="119"/>
+ <use xlink:href="#glyph0-1" x="647" y="119"/>
+ <use xlink:href="#glyph0-1" x="661" y="119"/>
+ <use xlink:href="#glyph0-1" x="675" y="119"/>
+ <use xlink:href="#glyph0-1" x="689" y="119"/>
+ <use xlink:href="#glyph0-1" x="703" y="119"/>
+ <use xlink:href="#glyph0-1" x="717" y="119"/>
+ <use xlink:href="#glyph0-1" x="731" y="119"/>
+ <use xlink:href="#glyph0-1" x="745" y="119"/>
+ <use xlink:href="#glyph0-1" x="759" y="119"/>
+ <use xlink:href="#glyph0-1" x="773" y="119"/>
+ <use xlink:href="#glyph0-1" x="787" y="119"/>
+ <use xlink:href="#glyph0-1" x="801" y="119"/>
+ <use xlink:href="#glyph0-1" x="815" y="119"/>
+ <use xlink:href="#glyph0-1" x="829" y="119"/>
+ <use xlink:href="#glyph0-1" x="843" y="119"/>
+ <use xlink:href="#glyph0-1" x="857" y="119"/>
+ <use xlink:href="#glyph0-1" x="871" y="119"/>
+ <use xlink:href="#glyph0-1" x="885" y="119"/>
+ <use xlink:href="#glyph0-1" x="899" y="119"/>
+ <use xlink:href="#glyph0-1" x="913" y="119"/>
+ <use xlink:href="#glyph0-1" x="927" y="119"/>
+ <use xlink:href="#glyph0-1" x="941" y="119"/>
+ <use xlink:href="#glyph0-1" x="955" y="119"/>
+ <use xlink:href="#glyph0-1" x="969" y="119"/>
+ <use xlink:href="#glyph0-1" x="983" y="119"/>
+ <use xlink:href="#glyph0-1" x="997" y="119"/>
+ <use xlink:href="#glyph0-1" x="1011" y="119"/>
+ <use xlink:href="#glyph0-1" x="1025" y="119"/>
+ <use xlink:href="#glyph0-1" x="1039" y="119"/>
+ <use xlink:href="#glyph0-1" x="1053" y="119"/>
+ <use xlink:href="#glyph0-1" x="1067" y="119"/>
+ <use xlink:href="#glyph0-1" x="1081" y="119"/>
+ <use xlink:href="#glyph0-1" x="1095" y="119"/>
+ <use xlink:href="#glyph0-1" x="1109" y="119"/>
+ <use xlink:href="#glyph0-1" x="1123" y="119"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 93 L 1151 93 L 1151 124 L 1137 124 Z M 1137 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 93 L 17 93 L 17 124 L 0 124 Z M 0 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 124 L 1137 124 L 1137 155 L 17 155 Z M 17 124 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-29" x="17" y="150"/>
+ <use xlink:href="#glyph0-27" x="31" y="150"/>
+ <use xlink:href="#glyph0-10" x="45" y="150"/>
+ <use xlink:href="#glyph0-31" x="59" y="150"/>
+ <use xlink:href="#glyph0-1" x="73" y="150"/>
+ <use xlink:href="#glyph0-35" x="87" y="150"/>
+ <use xlink:href="#glyph0-1" x="101" y="150"/>
+ <use xlink:href="#glyph0-1" x="115" y="150"/>
+ <use xlink:href="#glyph0-1" x="129" y="150"/>
+ <use xlink:href="#glyph0-1" x="143" y="150"/>
+ <use xlink:href="#glyph0-1" x="157" y="150"/>
+ <use xlink:href="#glyph0-1" x="171" y="150"/>
+ <use xlink:href="#glyph0-1" x="185" y="150"/>
+ <use xlink:href="#glyph0-1" x="199" y="150"/>
+ <use xlink:href="#glyph0-1" x="213" y="150"/>
+ <use xlink:href="#glyph0-1" x="227" y="150"/>
+ <use xlink:href="#glyph0-1" x="241" y="150"/>
+ <use xlink:href="#glyph0-1" x="255" y="150"/>
+ <use xlink:href="#glyph0-1" x="269" y="150"/>
+ <use xlink:href="#glyph0-1" x="283" y="150"/>
+ <use xlink:href="#glyph0-1" x="297" y="150"/>
+ <use xlink:href="#glyph0-1" x="311" y="150"/>
+ <use xlink:href="#glyph0-1" x="325" y="150"/>
+ <use xlink:href="#glyph0-1" x="339" y="150"/>
+ <use xlink:href="#glyph0-1" x="353" y="150"/>
+ <use xlink:href="#glyph0-1" x="367" y="150"/>
+ <use xlink:href="#glyph0-1" x="381" y="150"/>
+ <use xlink:href="#glyph0-1" x="395" y="150"/>
+ <use xlink:href="#glyph0-1" x="409" y="150"/>
+ <use xlink:href="#glyph0-1" x="423" y="150"/>
+ <use xlink:href="#glyph0-1" x="437" y="150"/>
+ <use xlink:href="#glyph0-1" x="451" y="150"/>
+ <use xlink:href="#glyph0-1" x="465" y="150"/>
+ <use xlink:href="#glyph0-1" x="479" y="150"/>
+ <use xlink:href="#glyph0-1" x="493" y="150"/>
+ <use xlink:href="#glyph0-1" x="507" y="150"/>
+ <use xlink:href="#glyph0-1" x="521" y="150"/>
+ <use xlink:href="#glyph0-1" x="535" y="150"/>
+ <use xlink:href="#glyph0-1" x="549" y="150"/>
+ <use xlink:href="#glyph0-1" x="563" y="150"/>
+ <use xlink:href="#glyph0-1" x="577" y="150"/>
+ <use xlink:href="#glyph0-1" x="591" y="150"/>
+ <use xlink:href="#glyph0-1" x="605" y="150"/>
+ <use xlink:href="#glyph0-1" x="619" y="150"/>
+ <use xlink:href="#glyph0-1" x="633" y="150"/>
+ <use xlink:href="#glyph0-1" x="647" y="150"/>
+ <use xlink:href="#glyph0-1" x="661" y="150"/>
+ <use xlink:href="#glyph0-1" x="675" y="150"/>
+ <use xlink:href="#glyph0-1" x="689" y="150"/>
+ <use xlink:href="#glyph0-1" x="703" y="150"/>
+ <use xlink:href="#glyph0-1" x="717" y="150"/>
+ <use xlink:href="#glyph0-1" x="731" y="150"/>
+ <use xlink:href="#glyph0-1" x="745" y="150"/>
+ <use xlink:href="#glyph0-1" x="759" y="150"/>
+ <use xlink:href="#glyph0-1" x="773" y="150"/>
+ <use xlink:href="#glyph0-1" x="787" y="150"/>
+ <use xlink:href="#glyph0-1" x="801" y="150"/>
+ <use xlink:href="#glyph0-1" x="815" y="150"/>
+ <use xlink:href="#glyph0-1" x="829" y="150"/>
+ <use xlink:href="#glyph0-1" x="843" y="150"/>
+ <use xlink:href="#glyph0-1" x="857" y="150"/>
+ <use xlink:href="#glyph0-1" x="871" y="150"/>
+ <use xlink:href="#glyph0-1" x="885" y="150"/>
+ <use xlink:href="#glyph0-1" x="899" y="150"/>
+ <use xlink:href="#glyph0-1" x="913" y="150"/>
+ <use xlink:href="#glyph0-1" x="927" y="150"/>
+ <use xlink:href="#glyph0-1" x="941" y="150"/>
+ <use xlink:href="#glyph0-1" x="955" y="150"/>
+ <use xlink:href="#glyph0-1" x="969" y="150"/>
+ <use xlink:href="#glyph0-1" x="983" y="150"/>
+ <use xlink:href="#glyph0-1" x="997" y="150"/>
+ <use xlink:href="#glyph0-1" x="1011" y="150"/>
+ <use xlink:href="#glyph0-1" x="1025" y="150"/>
+ <use xlink:href="#glyph0-1" x="1039" y="150"/>
+ <use xlink:href="#glyph0-1" x="1053" y="150"/>
+ <use xlink:href="#glyph0-1" x="1067" y="150"/>
+ <use xlink:href="#glyph0-1" x="1081" y="150"/>
+ <use xlink:href="#glyph0-1" x="1095" y="150"/>
+ <use xlink:href="#glyph0-1" x="1109" y="150"/>
+ <use xlink:href="#glyph0-1" x="1123" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 124 L 1151 124 L 1151 155 L 1137 155 Z M 1137 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 124 L 17 124 L 17 155 L 0 155 Z M 0 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 155 L 1137 155 L 1137 186 L 17 186 Z M 17 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 155 L 1151 155 L 1151 186 L 1137 186 Z M 1137 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 155 L 17 155 L 17 186 L 0 186 Z M 0 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 186 L 1137 186 L 1137 217 L 17 217 Z M 17 186 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-30" x="17" y="212"/>
+ <use xlink:href="#glyph0-14" x="31" y="212"/>
+ <use xlink:href="#glyph0-31" x="45" y="212"/>
+ <use xlink:href="#glyph0-1" x="59" y="212"/>
+ <use xlink:href="#glyph0-1" x="73" y="212"/>
+ <use xlink:href="#glyph0-36" x="87" y="212"/>
+ <use xlink:href="#glyph0-32" x="101" y="212"/>
+ <use xlink:href="#glyph0-33" x="115" y="212"/>
+ <use xlink:href="#glyph0-34" x="129" y="212"/>
+ <use xlink:href="#glyph0-33" x="143" y="212"/>
+ <use xlink:href="#glyph0-35" x="157" y="212"/>
+ <use xlink:href="#glyph0-33" x="171" y="212"/>
+ <use xlink:href="#glyph0-37" x="185" y="212"/>
+ <use xlink:href="#glyph0-38" x="199" y="212"/>
+ <use xlink:href="#glyph0-39" x="213" y="212"/>
+ <use xlink:href="#glyph0-39" x="227" y="212"/>
+ <use xlink:href="#glyph0-34" x="241" y="212"/>
+ <use xlink:href="#glyph0-1" x="255" y="212"/>
+ <use xlink:href="#glyph0-1" x="269" y="212"/>
+ <use xlink:href="#glyph0-1" x="283" y="212"/>
+ <use xlink:href="#glyph0-1" x="297" y="212"/>
+ <use xlink:href="#glyph0-1" x="311" y="212"/>
+ <use xlink:href="#glyph0-1" x="325" y="212"/>
+ <use xlink:href="#glyph0-1" x="339" y="212"/>
+ <use xlink:href="#glyph0-1" x="353" y="212"/>
+ <use xlink:href="#glyph0-1" x="367" y="212"/>
+ <use xlink:href="#glyph0-1" x="381" y="212"/>
+ <use xlink:href="#glyph0-1" x="395" y="212"/>
+ <use xlink:href="#glyph0-1" x="409" y="212"/>
+ <use xlink:href="#glyph0-1" x="423" y="212"/>
+ <use xlink:href="#glyph0-1" x="437" y="212"/>
+ <use xlink:href="#glyph0-1" x="451" y="212"/>
+ <use xlink:href="#glyph0-1" x="465" y="212"/>
+ <use xlink:href="#glyph0-1" x="479" y="212"/>
+ <use xlink:href="#glyph0-1" x="493" y="212"/>
+ <use xlink:href="#glyph0-1" x="507" y="212"/>
+ <use xlink:href="#glyph0-1" x="521" y="212"/>
+ <use xlink:href="#glyph0-1" x="535" y="212"/>
+ <use xlink:href="#glyph0-1" x="549" y="212"/>
+ <use xlink:href="#glyph0-1" x="563" y="212"/>
+ <use xlink:href="#glyph0-1" x="577" y="212"/>
+ <use xlink:href="#glyph0-1" x="591" y="212"/>
+ <use xlink:href="#glyph0-1" x="605" y="212"/>
+ <use xlink:href="#glyph0-1" x="619" y="212"/>
+ <use xlink:href="#glyph0-1" x="633" y="212"/>
+ <use xlink:href="#glyph0-1" x="647" y="212"/>
+ <use xlink:href="#glyph0-1" x="661" y="212"/>
+ <use xlink:href="#glyph0-1" x="675" y="212"/>
+ <use xlink:href="#glyph0-1" x="689" y="212"/>
+ <use xlink:href="#glyph0-1" x="703" y="212"/>
+ <use xlink:href="#glyph0-1" x="717" y="212"/>
+ <use xlink:href="#glyph0-1" x="731" y="212"/>
+ <use xlink:href="#glyph0-1" x="745" y="212"/>
+ <use xlink:href="#glyph0-1" x="759" y="212"/>
+ <use xlink:href="#glyph0-1" x="773" y="212"/>
+ <use xlink:href="#glyph0-1" x="787" y="212"/>
+ <use xlink:href="#glyph0-1" x="801" y="212"/>
+ <use xlink:href="#glyph0-1" x="815" y="212"/>
+ <use xlink:href="#glyph0-1" x="829" y="212"/>
+ <use xlink:href="#glyph0-1" x="843" y="212"/>
+ <use xlink:href="#glyph0-1" x="857" y="212"/>
+ <use xlink:href="#glyph0-1" x="871" y="212"/>
+ <use xlink:href="#glyph0-1" x="885" y="212"/>
+ <use xlink:href="#glyph0-1" x="899" y="212"/>
+ <use xlink:href="#glyph0-1" x="913" y="212"/>
+ <use xlink:href="#glyph0-1" x="927" y="212"/>
+ <use xlink:href="#glyph0-1" x="941" y="212"/>
+ <use xlink:href="#glyph0-1" x="955" y="212"/>
+ <use xlink:href="#glyph0-1" x="969" y="212"/>
+ <use xlink:href="#glyph0-1" x="983" y="212"/>
+ <use xlink:href="#glyph0-1" x="997" y="212"/>
+ <use xlink:href="#glyph0-1" x="1011" y="212"/>
+ <use xlink:href="#glyph0-1" x="1025" y="212"/>
+ <use xlink:href="#glyph0-1" x="1039" y="212"/>
+ <use xlink:href="#glyph0-1" x="1053" y="212"/>
+ <use xlink:href="#glyph0-1" x="1067" y="212"/>
+ <use xlink:href="#glyph0-1" x="1081" y="212"/>
+ <use xlink:href="#glyph0-1" x="1095" y="212"/>
+ <use xlink:href="#glyph0-1" x="1109" y="212"/>
+ <use xlink:href="#glyph0-1" x="1123" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 186 L 1151 186 L 1151 217 L 1137 217 Z M 1137 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 186 L 17 186 L 17 217 L 0 217 Z M 0 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 217 L 1137 217 L 1137 248 L 17 248 Z M 17 217 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-29" x="17" y="243"/>
+ <use xlink:href="#glyph0-27" x="31" y="243"/>
+ <use xlink:href="#glyph0-10" x="45" y="243"/>
+ <use xlink:href="#glyph0-31" x="59" y="243"/>
+ <use xlink:href="#glyph0-1" x="73" y="243"/>
+ <use xlink:href="#glyph0-32" x="87" y="243"/>
+ <use xlink:href="#glyph0-40" x="101" y="243"/>
+ <use xlink:href="#glyph0-40" x="115" y="243"/>
+ <use xlink:href="#glyph0-1" x="129" y="243"/>
+ <use xlink:href="#glyph0-1" x="143" y="243"/>
+ <use xlink:href="#glyph0-1" x="157" y="243"/>
+ <use xlink:href="#glyph0-1" x="171" y="243"/>
+ <use xlink:href="#glyph0-1" x="185" y="243"/>
+ <use xlink:href="#glyph0-1" x="199" y="243"/>
+ <use xlink:href="#glyph0-1" x="213" y="243"/>
+ <use xlink:href="#glyph0-1" x="227" y="243"/>
+ <use xlink:href="#glyph0-1" x="241" y="243"/>
+ <use xlink:href="#glyph0-1" x="255" y="243"/>
+ <use xlink:href="#glyph0-1" x="269" y="243"/>
+ <use xlink:href="#glyph0-1" x="283" y="243"/>
+ <use xlink:href="#glyph0-1" x="297" y="243"/>
+ <use xlink:href="#glyph0-1" x="311" y="243"/>
+ <use xlink:href="#glyph0-1" x="325" y="243"/>
+ <use xlink:href="#glyph0-1" x="339" y="243"/>
+ <use xlink:href="#glyph0-1" x="353" y="243"/>
+ <use xlink:href="#glyph0-1" x="367" y="243"/>
+ <use xlink:href="#glyph0-1" x="381" y="243"/>
+ <use xlink:href="#glyph0-1" x="395" y="243"/>
+ <use xlink:href="#glyph0-1" x="409" y="243"/>
+ <use xlink:href="#glyph0-1" x="423" y="243"/>
+ <use xlink:href="#glyph0-1" x="437" y="243"/>
+ <use xlink:href="#glyph0-1" x="451" y="243"/>
+ <use xlink:href="#glyph0-1" x="465" y="243"/>
+ <use xlink:href="#glyph0-1" x="479" y="243"/>
+ <use xlink:href="#glyph0-1" x="493" y="243"/>
+ <use xlink:href="#glyph0-1" x="507" y="243"/>
+ <use xlink:href="#glyph0-1" x="521" y="243"/>
+ <use xlink:href="#glyph0-1" x="535" y="243"/>
+ <use xlink:href="#glyph0-1" x="549" y="243"/>
+ <use xlink:href="#glyph0-1" x="563" y="243"/>
+ <use xlink:href="#glyph0-1" x="577" y="243"/>
+ <use xlink:href="#glyph0-1" x="591" y="243"/>
+ <use xlink:href="#glyph0-1" x="605" y="243"/>
+ <use xlink:href="#glyph0-1" x="619" y="243"/>
+ <use xlink:href="#glyph0-1" x="633" y="243"/>
+ <use xlink:href="#glyph0-1" x="647" y="243"/>
+ <use xlink:href="#glyph0-1" x="661" y="243"/>
+ <use xlink:href="#glyph0-1" x="675" y="243"/>
+ <use xlink:href="#glyph0-1" x="689" y="243"/>
+ <use xlink:href="#glyph0-1" x="703" y="243"/>
+ <use xlink:href="#glyph0-1" x="717" y="243"/>
+ <use xlink:href="#glyph0-1" x="731" y="243"/>
+ <use xlink:href="#glyph0-1" x="745" y="243"/>
+ <use xlink:href="#glyph0-1" x="759" y="243"/>
+ <use xlink:href="#glyph0-1" x="773" y="243"/>
+ <use xlink:href="#glyph0-1" x="787" y="243"/>
+ <use xlink:href="#glyph0-1" x="801" y="243"/>
+ <use xlink:href="#glyph0-1" x="815" y="243"/>
+ <use xlink:href="#glyph0-1" x="829" y="243"/>
+ <use xlink:href="#glyph0-1" x="843" y="243"/>
+ <use xlink:href="#glyph0-1" x="857" y="243"/>
+ <use xlink:href="#glyph0-1" x="871" y="243"/>
+ <use xlink:href="#glyph0-1" x="885" y="243"/>
+ <use xlink:href="#glyph0-1" x="899" y="243"/>
+ <use xlink:href="#glyph0-1" x="913" y="243"/>
+ <use xlink:href="#glyph0-1" x="927" y="243"/>
+ <use xlink:href="#glyph0-1" x="941" y="243"/>
+ <use xlink:href="#glyph0-1" x="955" y="243"/>
+ <use xlink:href="#glyph0-1" x="969" y="243"/>
+ <use xlink:href="#glyph0-1" x="983" y="243"/>
+ <use xlink:href="#glyph0-1" x="997" y="243"/>
+ <use xlink:href="#glyph0-1" x="1011" y="243"/>
+ <use xlink:href="#glyph0-1" x="1025" y="243"/>
+ <use xlink:href="#glyph0-1" x="1039" y="243"/>
+ <use xlink:href="#glyph0-1" x="1053" y="243"/>
+ <use xlink:href="#glyph0-1" x="1067" y="243"/>
+ <use xlink:href="#glyph0-1" x="1081" y="243"/>
+ <use xlink:href="#glyph0-1" x="1095" y="243"/>
+ <use xlink:href="#glyph0-1" x="1109" y="243"/>
+ <use xlink:href="#glyph0-1" x="1123" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 217 L 1151 217 L 1151 248 L 1137 248 Z M 1137 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 217 L 17 217 L 17 248 L 0 248 Z M 0 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 248 L 1137 248 L 1137 279 L 17 279 Z M 17 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 248 L 1151 248 L 1151 279 L 1137 279 Z M 1137 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 248 L 17 248 L 17 279 L 0 279 Z M 0 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 279 L 1137 279 L 1137 310 L 17 310 Z M 17 279 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-30" x="17" y="305"/>
+ <use xlink:href="#glyph0-14" x="31" y="305"/>
+ <use xlink:href="#glyph0-31" x="45" y="305"/>
+ <use xlink:href="#glyph0-1" x="59" y="305"/>
+ <use xlink:href="#glyph0-1" x="73" y="305"/>
+ <use xlink:href="#glyph0-35" x="87" y="305"/>
+ <use xlink:href="#glyph0-41" x="101" y="305"/>
+ <use xlink:href="#glyph0-32" x="115" y="305"/>
+ <use xlink:href="#glyph0-37" x="129" y="305"/>
+ <use xlink:href="#glyph0-32" x="143" y="305"/>
+ <use xlink:href="#glyph0-42" x="157" y="305"/>
+ <use xlink:href="#glyph0-43" x="171" y="305"/>
+ <use xlink:href="#glyph0-34" x="185" y="305"/>
+ <use xlink:href="#glyph0-44" x="199" y="305"/>
+ <use xlink:href="#glyph0-42" x="213" y="305"/>
+ <use xlink:href="#glyph0-35" x="227" y="305"/>
+ <use xlink:href="#glyph0-42" x="241" y="305"/>
+ <use xlink:href="#glyph0-43" x="255" y="305"/>
+ <use xlink:href="#glyph0-1" x="269" y="305"/>
+ <use xlink:href="#glyph0-39" x="283" y="305"/>
+ <use xlink:href="#glyph0-1" x="297" y="305"/>
+ <use xlink:href="#glyph0-34" x="311" y="305"/>
+ <use xlink:href="#glyph0-41" x="325" y="305"/>
+ <use xlink:href="#glyph0-40" x="339" y="305"/>
+ <use xlink:href="#glyph0-1" x="353" y="305"/>
+ <use xlink:href="#glyph0-1" x="367" y="305"/>
+ <use xlink:href="#glyph0-1" x="381" y="305"/>
+ <use xlink:href="#glyph0-1" x="395" y="305"/>
+ <use xlink:href="#glyph0-1" x="409" y="305"/>
+ <use xlink:href="#glyph0-1" x="423" y="305"/>
+ <use xlink:href="#glyph0-1" x="437" y="305"/>
+ <use xlink:href="#glyph0-1" x="451" y="305"/>
+ <use xlink:href="#glyph0-1" x="465" y="305"/>
+ <use xlink:href="#glyph0-1" x="479" y="305"/>
+ <use xlink:href="#glyph0-1" x="493" y="305"/>
+ <use xlink:href="#glyph0-1" x="507" y="305"/>
+ <use xlink:href="#glyph0-1" x="521" y="305"/>
+ <use xlink:href="#glyph0-1" x="535" y="305"/>
+ <use xlink:href="#glyph0-1" x="549" y="305"/>
+ <use xlink:href="#glyph0-1" x="563" y="305"/>
+ <use xlink:href="#glyph0-1" x="577" y="305"/>
+ <use xlink:href="#glyph0-1" x="591" y="305"/>
+ <use xlink:href="#glyph0-1" x="605" y="305"/>
+ <use xlink:href="#glyph0-1" x="619" y="305"/>
+ <use xlink:href="#glyph0-1" x="633" y="305"/>
+ <use xlink:href="#glyph0-1" x="647" y="305"/>
+ <use xlink:href="#glyph0-1" x="661" y="305"/>
+ <use xlink:href="#glyph0-1" x="675" y="305"/>
+ <use xlink:href="#glyph0-1" x="689" y="305"/>
+ <use xlink:href="#glyph0-1" x="703" y="305"/>
+ <use xlink:href="#glyph0-1" x="717" y="305"/>
+ <use xlink:href="#glyph0-1" x="731" y="305"/>
+ <use xlink:href="#glyph0-1" x="745" y="305"/>
+ <use xlink:href="#glyph0-1" x="759" y="305"/>
+ <use xlink:href="#glyph0-1" x="773" y="305"/>
+ <use xlink:href="#glyph0-1" x="787" y="305"/>
+ <use xlink:href="#glyph0-1" x="801" y="305"/>
+ <use xlink:href="#glyph0-1" x="815" y="305"/>
+ <use xlink:href="#glyph0-1" x="829" y="305"/>
+ <use xlink:href="#glyph0-1" x="843" y="305"/>
+ <use xlink:href="#glyph0-1" x="857" y="305"/>
+ <use xlink:href="#glyph0-1" x="871" y="305"/>
+ <use xlink:href="#glyph0-1" x="885" y="305"/>
+ <use xlink:href="#glyph0-1" x="899" y="305"/>
+ <use xlink:href="#glyph0-1" x="913" y="305"/>
+ <use xlink:href="#glyph0-1" x="927" y="305"/>
+ <use xlink:href="#glyph0-1" x="941" y="305"/>
+ <use xlink:href="#glyph0-1" x="955" y="305"/>
+ <use xlink:href="#glyph0-1" x="969" y="305"/>
+ <use xlink:href="#glyph0-1" x="983" y="305"/>
+ <use xlink:href="#glyph0-1" x="997" y="305"/>
+ <use xlink:href="#glyph0-1" x="1011" y="305"/>
+ <use xlink:href="#glyph0-1" x="1025" y="305"/>
+ <use xlink:href="#glyph0-1" x="1039" y="305"/>
+ <use xlink:href="#glyph0-1" x="1053" y="305"/>
+ <use xlink:href="#glyph0-1" x="1067" y="305"/>
+ <use xlink:href="#glyph0-1" x="1081" y="305"/>
+ <use xlink:href="#glyph0-1" x="1095" y="305"/>
+ <use xlink:href="#glyph0-1" x="1109" y="305"/>
+ <use xlink:href="#glyph0-1" x="1123" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 279 L 1151 279 L 1151 310 L 1137 310 Z M 1137 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 279 L 17 279 L 17 310 L 0 310 Z M 0 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 310 L 1137 310 L 1137 341 L 17 341 Z M 17 310 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-29" x="17" y="336"/>
+ <use xlink:href="#glyph0-27" x="31" y="336"/>
+ <use xlink:href="#glyph0-10" x="45" y="336"/>
+ <use xlink:href="#glyph0-31" x="59" y="336"/>
+ <use xlink:href="#glyph0-1" x="73" y="336"/>
+ <use xlink:href="#glyph0-44" x="87" y="336"/>
+ <use xlink:href="#glyph0-41" x="101" y="336"/>
+ <use xlink:href="#glyph0-34" x="115" y="336"/>
+ <use xlink:href="#glyph0-45" x="129" y="336"/>
+ <use xlink:href="#glyph0-35" x="143" y="336"/>
+ <use xlink:href="#glyph0-32" x="157" y="336"/>
+ <use xlink:href="#glyph0-45" x="171" y="336"/>
+ <use xlink:href="#glyph0-42" x="185" y="336"/>
+ <use xlink:href="#glyph0-35" x="199" y="336"/>
+ <use xlink:href="#glyph0-40" x="213" y="336"/>
+ <use xlink:href="#glyph0-46" x="227" y="336"/>
+ <use xlink:href="#glyph0-32" x="241" y="336"/>
+ <use xlink:href="#glyph0-45" x="255" y="336"/>
+ <use xlink:href="#glyph0-1" x="269" y="336"/>
+ <use xlink:href="#glyph0-1" x="283" y="336"/>
+ <use xlink:href="#glyph0-1" x="297" y="336"/>
+ <use xlink:href="#glyph0-1" x="311" y="336"/>
+ <use xlink:href="#glyph0-1" x="325" y="336"/>
+ <use xlink:href="#glyph0-1" x="339" y="336"/>
+ <use xlink:href="#glyph0-1" x="353" y="336"/>
+ <use xlink:href="#glyph0-1" x="367" y="336"/>
+ <use xlink:href="#glyph0-1" x="381" y="336"/>
+ <use xlink:href="#glyph0-1" x="395" y="336"/>
+ <use xlink:href="#glyph0-1" x="409" y="336"/>
+ <use xlink:href="#glyph0-1" x="423" y="336"/>
+ <use xlink:href="#glyph0-1" x="437" y="336"/>
+ <use xlink:href="#glyph0-1" x="451" y="336"/>
+ <use xlink:href="#glyph0-1" x="465" y="336"/>
+ <use xlink:href="#glyph0-1" x="479" y="336"/>
+ <use xlink:href="#glyph0-1" x="493" y="336"/>
+ <use xlink:href="#glyph0-1" x="507" y="336"/>
+ <use xlink:href="#glyph0-1" x="521" y="336"/>
+ <use xlink:href="#glyph0-1" x="535" y="336"/>
+ <use xlink:href="#glyph0-1" x="549" y="336"/>
+ <use xlink:href="#glyph0-1" x="563" y="336"/>
+ <use xlink:href="#glyph0-1" x="577" y="336"/>
+ <use xlink:href="#glyph0-1" x="591" y="336"/>
+ <use xlink:href="#glyph0-1" x="605" y="336"/>
+ <use xlink:href="#glyph0-1" x="619" y="336"/>
+ <use xlink:href="#glyph0-1" x="633" y="336"/>
+ <use xlink:href="#glyph0-1" x="647" y="336"/>
+ <use xlink:href="#glyph0-1" x="661" y="336"/>
+ <use xlink:href="#glyph0-1" x="675" y="336"/>
+ <use xlink:href="#glyph0-1" x="689" y="336"/>
+ <use xlink:href="#glyph0-1" x="703" y="336"/>
+ <use xlink:href="#glyph0-1" x="717" y="336"/>
+ <use xlink:href="#glyph0-1" x="731" y="336"/>
+ <use xlink:href="#glyph0-1" x="745" y="336"/>
+ <use xlink:href="#glyph0-1" x="759" y="336"/>
+ <use xlink:href="#glyph0-1" x="773" y="336"/>
+ <use xlink:href="#glyph0-1" x="787" y="336"/>
+ <use xlink:href="#glyph0-1" x="801" y="336"/>
+ <use xlink:href="#glyph0-1" x="815" y="336"/>
+ <use xlink:href="#glyph0-1" x="829" y="336"/>
+ <use xlink:href="#glyph0-1" x="843" y="336"/>
+ <use xlink:href="#glyph0-1" x="857" y="336"/>
+ <use xlink:href="#glyph0-1" x="871" y="336"/>
+ <use xlink:href="#glyph0-1" x="885" y="336"/>
+ <use xlink:href="#glyph0-1" x="899" y="336"/>
+ <use xlink:href="#glyph0-1" x="913" y="336"/>
+ <use xlink:href="#glyph0-1" x="927" y="336"/>
+ <use xlink:href="#glyph0-1" x="941" y="336"/>
+ <use xlink:href="#glyph0-1" x="955" y="336"/>
+ <use xlink:href="#glyph0-1" x="969" y="336"/>
+ <use xlink:href="#glyph0-1" x="983" y="336"/>
+ <use xlink:href="#glyph0-1" x="997" y="336"/>
+ <use xlink:href="#glyph0-1" x="1011" y="336"/>
+ <use xlink:href="#glyph0-1" x="1025" y="336"/>
+ <use xlink:href="#glyph0-1" x="1039" y="336"/>
+ <use xlink:href="#glyph0-1" x="1053" y="336"/>
+ <use xlink:href="#glyph0-1" x="1067" y="336"/>
+ <use xlink:href="#glyph0-1" x="1081" y="336"/>
+ <use xlink:href="#glyph0-1" x="1095" y="336"/>
+ <use xlink:href="#glyph0-1" x="1109" y="336"/>
+ <use xlink:href="#glyph0-1" x="1123" y="336"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 310 L 1151 310 L 1151 341 L 1137 341 Z M 1137 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 310 L 17 310 L 17 341 L 0 341 Z M 0 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 341 L 1137 341 L 1137 372 L 17 372 Z M 17 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 341 L 1151 341 L 1151 372 L 1137 372 Z M 1137 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 341 L 17 341 L 17 372 L 0 372 Z M 0 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 372 L 1137 372 L 1137 403 L 17 403 Z M 17 372 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-30" x="17" y="398"/>
+ <use xlink:href="#glyph0-14" x="31" y="398"/>
+ <use xlink:href="#glyph0-31" x="45" y="398"/>
+ <use xlink:href="#glyph0-1" x="59" y="398"/>
+ <use xlink:href="#glyph0-1" x="73" y="398"/>
+ <use xlink:href="#glyph0-6" x="87" y="398"/>
+ <use xlink:href="#glyph0-47" x="101" y="398"/>
+ <use xlink:href="#glyph0-4" x="115" y="398"/>
+ <use xlink:href="#glyph0-10" x="129" y="398"/>
+ <use xlink:href="#glyph0-1" x="143" y="398"/>
+ <use xlink:href="#glyph0-1" x="157" y="398"/>
+ <use xlink:href="#glyph0-1" x="171" y="398"/>
+ <use xlink:href="#glyph0-1" x="185" y="398"/>
+ <use xlink:href="#glyph0-1" x="199" y="398"/>
+ <use xlink:href="#glyph0-1" x="213" y="398"/>
+ <use xlink:href="#glyph0-1" x="227" y="398"/>
+ <use xlink:href="#glyph0-1" x="241" y="398"/>
+ <use xlink:href="#glyph0-1" x="255" y="398"/>
+ <use xlink:href="#glyph0-1" x="269" y="398"/>
+ <use xlink:href="#glyph0-1" x="283" y="398"/>
+ <use xlink:href="#glyph0-1" x="297" y="398"/>
+ <use xlink:href="#glyph0-1" x="311" y="398"/>
+ <use xlink:href="#glyph0-1" x="325" y="398"/>
+ <use xlink:href="#glyph0-1" x="339" y="398"/>
+ <use xlink:href="#glyph0-1" x="353" y="398"/>
+ <use xlink:href="#glyph0-1" x="367" y="398"/>
+ <use xlink:href="#glyph0-1" x="381" y="398"/>
+ <use xlink:href="#glyph0-1" x="395" y="398"/>
+ <use xlink:href="#glyph0-1" x="409" y="398"/>
+ <use xlink:href="#glyph0-1" x="423" y="398"/>
+ <use xlink:href="#glyph0-1" x="437" y="398"/>
+ <use xlink:href="#glyph0-1" x="451" y="398"/>
+ <use xlink:href="#glyph0-1" x="465" y="398"/>
+ <use xlink:href="#glyph0-1" x="479" y="398"/>
+ <use xlink:href="#glyph0-1" x="493" y="398"/>
+ <use xlink:href="#glyph0-1" x="507" y="398"/>
+ <use xlink:href="#glyph0-1" x="521" y="398"/>
+ <use xlink:href="#glyph0-1" x="535" y="398"/>
+ <use xlink:href="#glyph0-1" x="549" y="398"/>
+ <use xlink:href="#glyph0-1" x="563" y="398"/>
+ <use xlink:href="#glyph0-1" x="577" y="398"/>
+ <use xlink:href="#glyph0-1" x="591" y="398"/>
+ <use xlink:href="#glyph0-1" x="605" y="398"/>
+ <use xlink:href="#glyph0-1" x="619" y="398"/>
+ <use xlink:href="#glyph0-1" x="633" y="398"/>
+ <use xlink:href="#glyph0-1" x="647" y="398"/>
+ <use xlink:href="#glyph0-1" x="661" y="398"/>
+ <use xlink:href="#glyph0-1" x="675" y="398"/>
+ <use xlink:href="#glyph0-1" x="689" y="398"/>
+ <use xlink:href="#glyph0-1" x="703" y="398"/>
+ <use xlink:href="#glyph0-1" x="717" y="398"/>
+ <use xlink:href="#glyph0-1" x="731" y="398"/>
+ <use xlink:href="#glyph0-1" x="745" y="398"/>
+ <use xlink:href="#glyph0-1" x="759" y="398"/>
+ <use xlink:href="#glyph0-1" x="773" y="398"/>
+ <use xlink:href="#glyph0-1" x="787" y="398"/>
+ <use xlink:href="#glyph0-1" x="801" y="398"/>
+ <use xlink:href="#glyph0-1" x="815" y="398"/>
+ <use xlink:href="#glyph0-1" x="829" y="398"/>
+ <use xlink:href="#glyph0-1" x="843" y="398"/>
+ <use xlink:href="#glyph0-1" x="857" y="398"/>
+ <use xlink:href="#glyph0-1" x="871" y="398"/>
+ <use xlink:href="#glyph0-1" x="885" y="398"/>
+ <use xlink:href="#glyph0-1" x="899" y="398"/>
+ <use xlink:href="#glyph0-1" x="913" y="398"/>
+ <use xlink:href="#glyph0-1" x="927" y="398"/>
+ <use xlink:href="#glyph0-1" x="941" y="398"/>
+ <use xlink:href="#glyph0-1" x="955" y="398"/>
+ <use xlink:href="#glyph0-1" x="969" y="398"/>
+ <use xlink:href="#glyph0-1" x="983" y="398"/>
+ <use xlink:href="#glyph0-1" x="997" y="398"/>
+ <use xlink:href="#glyph0-1" x="1011" y="398"/>
+ <use xlink:href="#glyph0-1" x="1025" y="398"/>
+ <use xlink:href="#glyph0-1" x="1039" y="398"/>
+ <use xlink:href="#glyph0-1" x="1053" y="398"/>
+ <use xlink:href="#glyph0-1" x="1067" y="398"/>
+ <use xlink:href="#glyph0-1" x="1081" y="398"/>
+ <use xlink:href="#glyph0-1" x="1095" y="398"/>
+ <use xlink:href="#glyph0-1" x="1109" y="398"/>
+ <use xlink:href="#glyph0-1" x="1123" y="398"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 372 L 1151 372 L 1151 403 L 1137 403 Z M 1137 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 372 L 17 372 L 17 403 L 0 403 Z M 0 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 403 L 1137 403 L 1137 434 L 17 434 Z M 17 403 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-29" x="17" y="429"/>
+ <use xlink:href="#glyph0-27" x="31" y="429"/>
+ <use xlink:href="#glyph0-10" x="45" y="429"/>
+ <use xlink:href="#glyph0-31" x="59" y="429"/>
+ <use xlink:href="#glyph0-1" x="73" y="429"/>
+ <use xlink:href="#glyph0-48" x="87" y="429"/>
+ <use xlink:href="#glyph0-16" x="101" y="429"/>
+ <use xlink:href="#glyph0-6" x="115" y="429"/>
+ <use xlink:href="#glyph0-1" x="129" y="429"/>
+ <use xlink:href="#glyph0-6" x="143" y="429"/>
+ <use xlink:href="#glyph0-47" x="157" y="429"/>
+ <use xlink:href="#glyph0-4" x="171" y="429"/>
+ <use xlink:href="#glyph0-10" x="185" y="429"/>
+ <use xlink:href="#glyph0-36" x="199" y="429"/>
+ <use xlink:href="#glyph0-38" x="213" y="429"/>
+ <use xlink:href="#glyph0-1" x="227" y="429"/>
+ <use xlink:href="#glyph0-15" x="241" y="429"/>
+ <use xlink:href="#glyph0-24" x="255" y="429"/>
+ <use xlink:href="#glyph0-1" x="269" y="429"/>
+ <use xlink:href="#glyph0-19" x="283" y="429"/>
+ <use xlink:href="#glyph0-10" x="297" y="429"/>
+ <use xlink:href="#glyph0-24" x="311" y="429"/>
+ <use xlink:href="#glyph0-5" x="325" y="429"/>
+ <use xlink:href="#glyph0-28" x="339" y="429"/>
+ <use xlink:href="#glyph0-49" x="353" y="429"/>
+ <use xlink:href="#glyph0-1" x="367" y="429"/>
+ <use xlink:href="#glyph0-36" x="381" y="429"/>
+ <use xlink:href="#glyph0-4" x="395" y="429"/>
+ <use xlink:href="#glyph0-41" x="409" y="429"/>
+ <use xlink:href="#glyph0-6" x="423" y="429"/>
+ <use xlink:href="#glyph0-41" x="437" y="429"/>
+ <use xlink:href="#glyph0-1" x="451" y="429"/>
+ <use xlink:href="#glyph0-8" x="465" y="429"/>
+ <use xlink:href="#glyph0-29" x="479" y="429"/>
+ <use xlink:href="#glyph0-3" x="493" y="429"/>
+ <use xlink:href="#glyph0-38" x="507" y="429"/>
+ <use xlink:href="#glyph0-1" x="521" y="429"/>
+ <use xlink:href="#glyph0-10" x="535" y="429"/>
+ <use xlink:href="#glyph0-15" x="549" y="429"/>
+ <use xlink:href="#glyph0-1" x="563" y="429"/>
+ <use xlink:href="#glyph0-6" x="577" y="429"/>
+ <use xlink:href="#glyph0-47" x="591" y="429"/>
+ <use xlink:href="#glyph0-4" x="605" y="429"/>
+ <use xlink:href="#glyph0-10" x="619" y="429"/>
+ <use xlink:href="#glyph0-1" x="633" y="429"/>
+ <use xlink:href="#glyph0-1" x="647" y="429"/>
+ <use xlink:href="#glyph0-1" x="661" y="429"/>
+ <use xlink:href="#glyph0-1" x="675" y="429"/>
+ <use xlink:href="#glyph0-1" x="689" y="429"/>
+ <use xlink:href="#glyph0-1" x="703" y="429"/>
+ <use xlink:href="#glyph0-1" x="717" y="429"/>
+ <use xlink:href="#glyph0-1" x="731" y="429"/>
+ <use xlink:href="#glyph0-1" x="745" y="429"/>
+ <use xlink:href="#glyph0-1" x="759" y="429"/>
+ <use xlink:href="#glyph0-1" x="773" y="429"/>
+ <use xlink:href="#glyph0-1" x="787" y="429"/>
+ <use xlink:href="#glyph0-1" x="801" y="429"/>
+ <use xlink:href="#glyph0-1" x="815" y="429"/>
+ <use xlink:href="#glyph0-1" x="829" y="429"/>
+ <use xlink:href="#glyph0-1" x="843" y="429"/>
+ <use xlink:href="#glyph0-1" x="857" y="429"/>
+ <use xlink:href="#glyph0-1" x="871" y="429"/>
+ <use xlink:href="#glyph0-1" x="885" y="429"/>
+ <use xlink:href="#glyph0-1" x="899" y="429"/>
+ <use xlink:href="#glyph0-1" x="913" y="429"/>
+ <use xlink:href="#glyph0-1" x="927" y="429"/>
+ <use xlink:href="#glyph0-1" x="941" y="429"/>
+ <use xlink:href="#glyph0-1" x="955" y="429"/>
+ <use xlink:href="#glyph0-1" x="969" y="429"/>
+ <use xlink:href="#glyph0-1" x="983" y="429"/>
+ <use xlink:href="#glyph0-1" x="997" y="429"/>
+ <use xlink:href="#glyph0-1" x="1011" y="429"/>
+ <use xlink:href="#glyph0-1" x="1025" y="429"/>
+ <use xlink:href="#glyph0-1" x="1039" y="429"/>
+ <use xlink:href="#glyph0-1" x="1053" y="429"/>
+ <use xlink:href="#glyph0-1" x="1067" y="429"/>
+ <use xlink:href="#glyph0-1" x="1081" y="429"/>
+ <use xlink:href="#glyph0-1" x="1095" y="429"/>
+ <use xlink:href="#glyph0-1" x="1109" y="429"/>
+ <use xlink:href="#glyph0-1" x="1123" y="429"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 403 L 1151 403 L 1151 434 L 1137 434 Z M 1137 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 403 L 17 403 L 17 434 L 0 434 Z M 0 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 434 L 1137 434 L 1137 465 L 17 465 Z M 17 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 434 L 1151 434 L 1151 465 L 1137 465 Z M 1137 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 434 L 17 434 L 17 465 L 0 465 Z M 0 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 465 L 1137 465 L 1137 496 L 17 496 Z M 17 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 465 L 1151 465 L 1151 496 L 1137 496 Z M 1137 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 465 L 17 465 L 17 496 L 0 496 Z M 0 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 496 L 1137 496 L 1137 527 L 17 527 Z M 17 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 496 L 1151 496 L 1151 527 L 1137 527 Z M 1137 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 496 L 17 496 L 17 527 L 0 527 Z M 0 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 527 L 1137 527 L 1137 558 L 17 558 Z M 17 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 527 L 1151 527 L 1151 558 L 1137 558 Z M 1137 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 527 L 17 527 L 17 558 L 0 558 Z M 0 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 558 L 1137 558 L 1137 589 L 17 589 Z M 17 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 558 L 1151 558 L 1151 589 L 1137 589 Z M 1137 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 558 L 17 558 L 17 589 L 0 589 Z M 0 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 589 L 1151 589 L 1151 620 L 1137 620 Z M 1137 589 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 589 L 1137 589 L 1137 620 L 17 620 Z M 17 589 "/>
+<g clip-path="url(#clip1)" clip-rule="nonzero">
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-50" x="17" y="615"/>
+ <use xlink:href="#glyph0-50" x="31" y="615"/>
+ <use xlink:href="#glyph0-50" x="45" y="615"/>
+ <use xlink:href="#glyph0-50" x="59" y="615"/>
+ <use xlink:href="#glyph0-50" x="73" y="615"/>
+ <use xlink:href="#glyph0-50" x="87" y="615"/>
+ <use xlink:href="#glyph0-50" x="101" y="615"/>
+ <use xlink:href="#glyph0-50" x="115" y="615"/>
+ <use xlink:href="#glyph0-50" x="129" y="615"/>
+ <use xlink:href="#glyph0-50" x="143" y="615"/>
+ <use xlink:href="#glyph0-50" x="157" y="615"/>
+ <use xlink:href="#glyph0-50" x="171" y="615"/>
+ <use xlink:href="#glyph0-50" x="185" y="615"/>
+ <use xlink:href="#glyph0-50" x="199" y="615"/>
+ <use xlink:href="#glyph0-50" x="213" y="615"/>
+ <use xlink:href="#glyph0-50" x="227" y="615"/>
+ <use xlink:href="#glyph0-50" x="241" y="615"/>
+ <use xlink:href="#glyph0-50" x="255" y="615"/>
+ <use xlink:href="#glyph0-50" x="269" y="615"/>
+ <use xlink:href="#glyph0-50" x="283" y="615"/>
+ <use xlink:href="#glyph0-50" x="297" y="615"/>
+ <use xlink:href="#glyph0-50" x="311" y="615"/>
+ <use xlink:href="#glyph0-50" x="325" y="615"/>
+ <use xlink:href="#glyph0-50" x="339" y="615"/>
+ <use xlink:href="#glyph0-50" x="353" y="615"/>
+ <use xlink:href="#glyph0-50" x="367" y="615"/>
+ <use xlink:href="#glyph0-50" x="381" y="615"/>
+ <use xlink:href="#glyph0-50" x="395" y="615"/>
+ <use xlink:href="#glyph0-50" x="409" y="615"/>
+ <use xlink:href="#glyph0-50" x="423" y="615"/>
+ <use xlink:href="#glyph0-50" x="437" y="615"/>
+ <use xlink:href="#glyph0-50" x="451" y="615"/>
+ <use xlink:href="#glyph0-50" x="465" y="615"/>
+ <use xlink:href="#glyph0-50" x="479" y="615"/>
+ <use xlink:href="#glyph0-50" x="493" y="615"/>
+ <use xlink:href="#glyph0-50" x="507" y="615"/>
+ <use xlink:href="#glyph0-50" x="521" y="615"/>
+ <use xlink:href="#glyph0-50" x="535" y="615"/>
+ <use xlink:href="#glyph0-50" x="549" y="615"/>
+ <use xlink:href="#glyph0-50" x="563" y="615"/>
+ <use xlink:href="#glyph0-50" x="577" y="615"/>
+ <use xlink:href="#glyph0-50" x="591" y="615"/>
+ <use xlink:href="#glyph0-50" x="605" y="615"/>
+ <use xlink:href="#glyph0-50" x="619" y="615"/>
+ <use xlink:href="#glyph0-50" x="633" y="615"/>
+ <use xlink:href="#glyph0-50" x="647" y="615"/>
+ <use xlink:href="#glyph0-50" x="661" y="615"/>
+ <use xlink:href="#glyph0-50" x="675" y="615"/>
+ <use xlink:href="#glyph0-50" x="689" y="615"/>
+ <use xlink:href="#glyph0-50" x="703" y="615"/>
+ <use xlink:href="#glyph0-50" x="717" y="615"/>
+ <use xlink:href="#glyph0-50" x="731" y="615"/>
+ <use xlink:href="#glyph0-50" x="745" y="615"/>
+ <use xlink:href="#glyph0-50" x="759" y="615"/>
+ <use xlink:href="#glyph0-50" x="773" y="615"/>
+ <use xlink:href="#glyph0-50" x="787" y="615"/>
+ <use xlink:href="#glyph0-50" x="801" y="615"/>
+ <use xlink:href="#glyph0-50" x="815" y="615"/>
+ <use xlink:href="#glyph0-50" x="829" y="615"/>
+ <use xlink:href="#glyph0-50" x="843" y="615"/>
+ <use xlink:href="#glyph0-50" x="857" y="615"/>
+ <use xlink:href="#glyph0-50" x="871" y="615"/>
+ <use xlink:href="#glyph0-50" x="885" y="615"/>
+ <use xlink:href="#glyph0-50" x="899" y="615"/>
+ <use xlink:href="#glyph0-50" x="913" y="615"/>
+ <use xlink:href="#glyph0-50" x="927" y="615"/>
+ <use xlink:href="#glyph0-50" x="941" y="615"/>
+ <use xlink:href="#glyph0-50" x="955" y="615"/>
+ <use xlink:href="#glyph0-50" x="969" y="615"/>
+ <use xlink:href="#glyph0-50" x="983" y="615"/>
+ <use xlink:href="#glyph0-50" x="997" y="615"/>
+ <use xlink:href="#glyph0-50" x="1011" y="615"/>
+ <use xlink:href="#glyph0-50" x="1025" y="615"/>
+ <use xlink:href="#glyph0-50" x="1039" y="615"/>
+ <use xlink:href="#glyph0-50" x="1053" y="615"/>
+ <use xlink:href="#glyph0-50" x="1067" y="615"/>
+ <use xlink:href="#glyph0-50" x="1081" y="615"/>
+ <use xlink:href="#glyph0-50" x="1095" y="615"/>
+ <use xlink:href="#glyph0-50" x="1109" y="615"/>
+ <use xlink:href="#glyph0-50" x="1123" y="615"/>
+</g>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 589 L 17 589 L 17 620 L 0 620 Z M 0 589 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 620 L 1137 620 L 1137 651 L 17 651 Z M 17 620 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-51" x="17" y="646"/>
+ <use xlink:href="#glyph0-51" x="31" y="646"/>
+ <use xlink:href="#glyph0-51" x="45" y="646"/>
+ <use xlink:href="#glyph0-1" x="59" y="646"/>
+ <use xlink:href="#glyph0-1" x="73" y="646"/>
+ <use xlink:href="#glyph0-1" x="87" y="646"/>
+ <use xlink:href="#glyph0-1" x="101" y="646"/>
+ <use xlink:href="#glyph0-1" x="115" y="646"/>
+ <use xlink:href="#glyph0-1" x="129" y="646"/>
+ <use xlink:href="#glyph0-1" x="143" y="646"/>
+ <use xlink:href="#glyph0-1" x="157" y="646"/>
+ <use xlink:href="#glyph0-1" x="171" y="646"/>
+ <use xlink:href="#glyph0-1" x="185" y="646"/>
+ <use xlink:href="#glyph0-1" x="199" y="646"/>
+ <use xlink:href="#glyph0-1" x="213" y="646"/>
+ <use xlink:href="#glyph0-1" x="227" y="646"/>
+ <use xlink:href="#glyph0-1" x="241" y="646"/>
+ <use xlink:href="#glyph0-1" x="255" y="646"/>
+ <use xlink:href="#glyph0-1" x="269" y="646"/>
+ <use xlink:href="#glyph0-1" x="283" y="646"/>
+ <use xlink:href="#glyph0-1" x="297" y="646"/>
+ <use xlink:href="#glyph0-1" x="311" y="646"/>
+ <use xlink:href="#glyph0-1" x="325" y="646"/>
+ <use xlink:href="#glyph0-1" x="339" y="646"/>
+ <use xlink:href="#glyph0-1" x="353" y="646"/>
+ <use xlink:href="#glyph0-1" x="367" y="646"/>
+ <use xlink:href="#glyph0-1" x="381" y="646"/>
+ <use xlink:href="#glyph0-1" x="395" y="646"/>
+ <use xlink:href="#glyph0-1" x="409" y="646"/>
+ <use xlink:href="#glyph0-1" x="423" y="646"/>
+ <use xlink:href="#glyph0-1" x="437" y="646"/>
+ <use xlink:href="#glyph0-1" x="451" y="646"/>
+ <use xlink:href="#glyph0-1" x="465" y="646"/>
+ <use xlink:href="#glyph0-1" x="479" y="646"/>
+ <use xlink:href="#glyph0-1" x="493" y="646"/>
+ <use xlink:href="#glyph0-1" x="507" y="646"/>
+ <use xlink:href="#glyph0-1" x="521" y="646"/>
+ <use xlink:href="#glyph0-1" x="535" y="646"/>
+ <use xlink:href="#glyph0-1" x="549" y="646"/>
+ <use xlink:href="#glyph0-1" x="563" y="646"/>
+ <use xlink:href="#glyph0-1" x="577" y="646"/>
+ <use xlink:href="#glyph0-1" x="591" y="646"/>
+ <use xlink:href="#glyph0-1" x="605" y="646"/>
+ <use xlink:href="#glyph0-1" x="619" y="646"/>
+ <use xlink:href="#glyph0-1" x="633" y="646"/>
+ <use xlink:href="#glyph0-1" x="647" y="646"/>
+ <use xlink:href="#glyph0-1" x="661" y="646"/>
+ <use xlink:href="#glyph0-1" x="675" y="646"/>
+ <use xlink:href="#glyph0-1" x="689" y="646"/>
+ <use xlink:href="#glyph0-1" x="703" y="646"/>
+ <use xlink:href="#glyph0-1" x="717" y="646"/>
+ <use xlink:href="#glyph0-1" x="731" y="646"/>
+ <use xlink:href="#glyph0-1" x="745" y="646"/>
+ <use xlink:href="#glyph0-1" x="759" y="646"/>
+ <use xlink:href="#glyph0-1" x="773" y="646"/>
+ <use xlink:href="#glyph0-1" x="787" y="646"/>
+ <use xlink:href="#glyph0-1" x="801" y="646"/>
+ <use xlink:href="#glyph0-1" x="815" y="646"/>
+ <use xlink:href="#glyph0-1" x="829" y="646"/>
+ <use xlink:href="#glyph0-1" x="843" y="646"/>
+ <use xlink:href="#glyph0-1" x="857" y="646"/>
+ <use xlink:href="#glyph0-1" x="871" y="646"/>
+ <use xlink:href="#glyph0-1" x="885" y="646"/>
+ <use xlink:href="#glyph0-1" x="899" y="646"/>
+ <use xlink:href="#glyph0-1" x="913" y="646"/>
+ <use xlink:href="#glyph0-1" x="927" y="646"/>
+ <use xlink:href="#glyph0-1" x="941" y="646"/>
+ <use xlink:href="#glyph0-1" x="955" y="646"/>
+ <use xlink:href="#glyph0-1" x="969" y="646"/>
+ <use xlink:href="#glyph0-1" x="983" y="646"/>
+ <use xlink:href="#glyph0-1" x="997" y="646"/>
+ <use xlink:href="#glyph0-1" x="1011" y="646"/>
+ <use xlink:href="#glyph0-1" x="1025" y="646"/>
+ <use xlink:href="#glyph0-1" x="1039" y="646"/>
+ <use xlink:href="#glyph0-1" x="1053" y="646"/>
+ <use xlink:href="#glyph0-1" x="1067" y="646"/>
+ <use xlink:href="#glyph0-1" x="1081" y="646"/>
+ <use xlink:href="#glyph0-1" x="1095" y="646"/>
+ <use xlink:href="#glyph0-1" x="1109" y="646"/>
+ <use xlink:href="#glyph0-1" x="1123" y="646"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 620 L 1151 620 L 1151 651 L 1137 651 Z M 1137 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 620 L 17 620 L 17 651 L 0 651 Z M 0 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;" d="M 17 651 L 31 651 L 31 682 L 17 682 Z M 17 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 31 651 L 199 651 L 199 682 L 31 682 Z M 31 651 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="31" y="677"/>
+ <use xlink:href="#glyph0-19" x="45" y="677"/>
+ <use xlink:href="#glyph0-23" x="59" y="677"/>
+ <use xlink:href="#glyph0-5" x="73" y="677"/>
+ <use xlink:href="#glyph0-25" x="87" y="677"/>
+ <use xlink:href="#glyph0-27" x="101" y="677"/>
+ <use xlink:href="#glyph0-5" x="115" y="677"/>
+ <use xlink:href="#glyph0-23" x="129" y="677"/>
+ <use xlink:href="#glyph0-10" x="143" y="677"/>
+ <use xlink:href="#glyph0-15" x="157" y="677"/>
+ <use xlink:href="#glyph0-24" x="171" y="677"/>
+ <use xlink:href="#glyph0-1" x="185" y="677"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="32" y="677"/>
+ <use xlink:href="#glyph0-19" x="46" y="677"/>
+ <use xlink:href="#glyph0-23" x="60" y="677"/>
+ <use xlink:href="#glyph0-5" x="74" y="677"/>
+ <use xlink:href="#glyph0-25" x="88" y="677"/>
+ <use xlink:href="#glyph0-27" x="102" y="677"/>
+ <use xlink:href="#glyph0-5" x="116" y="677"/>
+ <use xlink:href="#glyph0-23" x="130" y="677"/>
+ <use xlink:href="#glyph0-10" x="144" y="677"/>
+ <use xlink:href="#glyph0-15" x="158" y="677"/>
+ <use xlink:href="#glyph0-24" x="172" y="677"/>
+ <use xlink:href="#glyph0-1" x="186" y="677"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 199 651 L 227 651 L 227 682 L 199 682 Z M 199 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 227 651 L 241 651 L 241 682 L 227 682 Z M 227 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 241 651 L 465 651 L 465 682 L 241 682 Z M 241 651 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-52" x="241" y="677"/>
+ <use xlink:href="#glyph0-27" x="255" y="677"/>
+ <use xlink:href="#glyph0-14" x="269" y="677"/>
+ <use xlink:href="#glyph0-1" x="283" y="677"/>
+ <use xlink:href="#glyph0-19" x="297" y="677"/>
+ <use xlink:href="#glyph0-23" x="311" y="677"/>
+ <use xlink:href="#glyph0-5" x="325" y="677"/>
+ <use xlink:href="#glyph0-25" x="339" y="677"/>
+ <use xlink:href="#glyph0-27" x="353" y="677"/>
+ <use xlink:href="#glyph0-5" x="367" y="677"/>
+ <use xlink:href="#glyph0-23" x="381" y="677"/>
+ <use xlink:href="#glyph0-10" x="395" y="677"/>
+ <use xlink:href="#glyph0-4" x="409" y="677"/>
+ <use xlink:href="#glyph0-15" x="423" y="677"/>
+ <use xlink:href="#glyph0-14" x="437" y="677"/>
+ <use xlink:href="#glyph0-1" x="451" y="677"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 465 651 L 535 651 L 535 682 L 465 682 Z M 465 651 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-8" x="465" y="677"/>
+ <use xlink:href="#glyph0-14" x="479" y="677"/>
+ <use xlink:href="#glyph0-10" x="493" y="677"/>
+ <use xlink:href="#glyph0-6" x="507" y="677"/>
+ <use xlink:href="#glyph0-24" x="521" y="677"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-8" x="466" y="677"/>
+ <use xlink:href="#glyph0-14" x="480" y="677"/>
+ <use xlink:href="#glyph0-10" x="494" y="677"/>
+ <use xlink:href="#glyph0-6" x="508" y="677"/>
+ <use xlink:href="#glyph0-24" x="522" y="677"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 535 651 L 549 651 L 549 682 L 535 682 Z M 535 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 549 651 L 577 651 L 577 682 L 549 682 Z M 549 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 577 651 L 591 651 L 591 682 L 577 682 Z M 577 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 591 651 L 759 651 L 759 682 L 591 682 Z M 591 651 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="591" y="677"/>
+ <use xlink:href="#glyph0-15" x="605" y="677"/>
+ <use xlink:href="#glyph0-18" x="619" y="677"/>
+ <use xlink:href="#glyph0-53" x="633" y="677"/>
+ <use xlink:href="#glyph0-1" x="647" y="677"/>
+ <use xlink:href="#glyph0-29" x="661" y="677"/>
+ <use xlink:href="#glyph0-27" x="675" y="677"/>
+ <use xlink:href="#glyph0-10" x="689" y="677"/>
+ <use xlink:href="#glyph0-18" x="703" y="677"/>
+ <use xlink:href="#glyph0-27" x="717" y="677"/>
+ <use xlink:href="#glyph0-10" x="731" y="677"/>
+ <use xlink:href="#glyph0-1" x="745" y="677"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 759 651 L 843 651 L 843 682 L 759 682 Z M 759 651 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="759" y="677"/>
+ <use xlink:href="#glyph0-10" x="773" y="677"/>
+ <use xlink:href="#glyph0-24" x="787" y="677"/>
+ <use xlink:href="#glyph0-5" x="801" y="677"/>
+ <use xlink:href="#glyph0-28" x="815" y="677"/>
+ <use xlink:href="#glyph0-25" x="829" y="677"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="760" y="677"/>
+ <use xlink:href="#glyph0-10" x="774" y="677"/>
+ <use xlink:href="#glyph0-24" x="788" y="677"/>
+ <use xlink:href="#glyph0-5" x="802" y="677"/>
+ <use xlink:href="#glyph0-28" x="816" y="677"/>
+ <use xlink:href="#glyph0-25" x="830" y="677"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 843 651 L 857 651 L 857 682 L 843 682 Z M 843 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 857 651 L 1081 651 L 1081 682 L 857 682 Z M 857 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 651 L 1151 651 L 1151 682 L 1137 682 Z M 1137 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1081 651 L 1137 651 L 1137 682 L 1081 682 Z M 1081 651 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-50" x="1081" y="677"/>
+ <use xlink:href="#glyph0-54" x="1095" y="677"/>
+ <use xlink:href="#glyph0-54" x="1109" y="677"/>
+ <use xlink:href="#glyph0-50" x="1123" y="677"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-50" x="1082" y="677"/>
+ <use xlink:href="#glyph0-54" x="1096" y="677"/>
+ <use xlink:href="#glyph0-54" x="1110" y="677"/>
+ <use xlink:href="#glyph0-50" x="1124" y="677"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 651 L 17 651 L 17 682 L 0 682 Z M 0 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;" d="M 73 620 L 87 620 L 87 651 L 73 651 Z M 73 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 17 682 L 45 682 L 45 713 L 17 713 Z M 17 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 45 682 L 1152 682 L 1152 713 L 45 713 Z M 45 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 682 L 17 682 L 17 713 L 0 713 Z M 0 682 "/>
+</g>
+</svg>
diff --git a/pw_console/images/clock_plugin1.png b/pw_console/images/clock_plugin1.png
deleted file mode 100644
index 1924024cb..000000000
--- a/pw_console/images/clock_plugin1.png
+++ /dev/null
Binary files differ
diff --git a/pw_console/images/clock_plugin1.svg b/pw_console/images/clock_plugin1.svg
new file mode 100644
index 000000000..a0045a169
--- /dev/null
+++ b/pw_console/images/clock_plugin1.svg
@@ -0,0 +1,378 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1151pt" height="248pt" viewBox="0 0 1151 248" version="1.1">
+<defs>
+<g>
+<symbol overflow="visible" id="glyph0-0">
+<path style="stroke:none;" d="M 3.203125 -1.046875 L 10.796875 -1.046875 L 10.796875 -19.8125 L 3.203125 -19.8125 Z M 1.265625 0.875 L 1.265625 -21.734375 L 12.734375 -21.734375 L 12.734375 0.875 Z M 7.59375 -9.09375 L 7.59375 -8.375 L 5.671875 -8.375 L 5.671875 -9.109375 C 5.671875 -9.796875 5.878906 -10.441406 6.296875 -11.046875 L 7.828125 -13.296875 C 8.097656 -13.679688 8.234375 -14.035156 8.234375 -14.359375 C 8.234375 -14.816406 8.097656 -15.207031 7.828125 -15.53125 C 7.566406 -15.851562 7.210938 -16.015625 6.765625 -16.015625 C 5.960938 -16.015625 5.34375 -15.484375 4.90625 -14.421875 L 3.34375 -15.34375 C 4.1875 -16.957031 5.332031 -17.765625 6.78125 -17.765625 C 7.75 -17.765625 8.550781 -17.4375 9.1875 -16.78125 C 9.820312 -16.132812 10.140625 -15.320312 10.140625 -14.34375 C 10.140625 -13.644531 9.898438 -12.941406 9.421875 -12.234375 L 7.8125 -9.890625 C 7.664062 -9.679688 7.59375 -9.414062 7.59375 -9.09375 Z M 6.78125 -6.75 C 7.144531 -6.75 7.457031 -6.617188 7.71875 -6.359375 C 7.988281 -6.109375 8.125 -5.796875 8.125 -5.421875 C 8.125 -5.046875 7.988281 -4.726562 7.71875 -4.46875 C 7.457031 -4.207031 7.144531 -4.078125 6.78125 -4.078125 C 6.394531 -4.078125 6.070312 -4.207031 5.8125 -4.46875 C 5.5625 -4.726562 5.4375 -5.046875 5.4375 -5.421875 C 5.4375 -5.796875 5.5625 -6.109375 5.8125 -6.359375 C 6.070312 -6.617188 6.394531 -6.75 6.78125 -6.75 Z M 6.78125 -6.75 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-1">
+<path style="stroke:none;" d=""/>
+</symbol>
+<symbol overflow="visible" id="glyph0-2">
+<path style="stroke:none;" d="M 11.546875 1.8125 L 4.296875 1.8125 L 4.296875 -22.671875 L 11.546875 -22.671875 L 11.546875 -20.609375 L 6.625 -20.609375 L 6.625 -0.265625 L 11.546875 -0.265625 Z M 11.546875 1.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-3">
+<path style="stroke:none;" d="M 12.8125 -18.875 L 5.25 -18.875 L 5.25 -12.125 L 11.328125 -12.125 L 11.328125 -10.140625 L 5.25 -10.140625 L 5.25 0 L 2.921875 0 L 2.921875 -20.859375 L 12.8125 -20.859375 Z M 12.8125 -18.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-4">
+<path style="stroke:none;" d="M 8.484375 -20.734375 C 8.484375 -20.234375 8.3125 -19.804688 7.96875 -19.453125 C 7.632812 -19.109375 7.210938 -18.9375 6.703125 -18.9375 C 6.191406 -18.9375 5.765625 -19.109375 5.421875 -19.453125 C 5.078125 -19.804688 4.90625 -20.234375 4.90625 -20.734375 C 4.90625 -21.222656 5.078125 -21.640625 5.421875 -21.984375 C 5.765625 -22.335938 6.191406 -22.515625 6.703125 -22.515625 C 7.210938 -22.515625 7.632812 -22.335938 7.96875 -21.984375 C 8.3125 -21.640625 8.484375 -21.222656 8.484375 -20.734375 Z M 12 0 L 2.25 0 L 2.25 -1.984375 L 5.96875 -1.984375 L 5.96875 -13.5 L 3.296875 -13.5 L 3.296875 -15.484375 L 8.296875 -15.484375 L 8.296875 -1.984375 L 12 -1.984375 Z M 12 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-5">
+<path style="stroke:none;" d="M 11.875 0 L 2.125 0 L 2.125 -1.984375 L 5.84375 -1.984375 L 5.84375 -18.875 L 3.1875 -18.875 L 3.1875 -20.859375 L 8.171875 -20.859375 L 8.171875 -1.984375 L 11.875 -1.984375 Z M 11.875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-6">
+<path style="stroke:none;" d="M 12.203125 -7.28125 L 4.109375 -7.28125 L 4.109375 -6.8125 C 4.109375 -4.8125 4.398438 -3.484375 4.984375 -2.828125 C 5.566406 -2.171875 6.359375 -1.84375 7.359375 -1.84375 C 8.628906 -1.84375 9.644531 -2.488281 10.40625 -3.78125 L 12.296875 -2.578125 C 11.109375 -0.679688 9.410156 0.265625 7.203125 0.265625 C 5.628906 0.265625 4.332031 -0.296875 3.3125 -1.421875 C 2.289062 -2.554688 1.78125 -4.351562 1.78125 -6.8125 L 1.78125 -8.6875 C 1.78125 -11.132812 2.289062 -12.925781 3.3125 -14.0625 C 4.332031 -15.195312 5.5625 -15.765625 7 -15.765625 C 8.53125 -15.765625 9.78125 -15.238281 10.75 -14.1875 C 11.71875 -13.144531 12.203125 -11.441406 12.203125 -9.078125 Z M 9.890625 -9.328125 C 9.890625 -10.898438 9.617188 -12.015625 9.078125 -12.671875 C 8.535156 -13.328125 7.84375 -13.65625 7 -13.65625 C 6.195312 -13.65625 5.515625 -13.328125 4.953125 -12.671875 C 4.390625 -12.015625 4.109375 -10.898438 4.109375 -9.328125 Z M 9.890625 -9.328125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-7">
+<path style="stroke:none;" d="M 9.703125 1.8125 L 2.46875 1.8125 L 2.46875 -0.265625 L 7.375 -0.265625 L 7.375 -20.609375 L 2.46875 -20.609375 L 2.46875 -22.671875 L 9.703125 -22.671875 Z M 9.703125 1.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-8">
+<path style="stroke:none;" d="M 12.203125 0 L 2.578125 0 L 2.578125 -20.859375 L 12.203125 -20.859375 L 12.203125 -18.875 L 4.90625 -18.875 L 4.90625 -12.125 L 10.296875 -12.125 L 10.296875 -10.140625 L 4.90625 -10.140625 L 4.90625 -1.984375 L 12.203125 -1.984375 Z M 12.203125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-9">
+<path style="stroke:none;" d="M 1.59375 -8.8125 C 1.59375 -11.28125 2.03125 -13.054688 2.90625 -14.140625 C 3.789062 -15.222656 4.847656 -15.765625 6.078125 -15.765625 C 7.671875 -15.765625 8.875 -15.046875 9.6875 -13.609375 L 9.6875 -20.859375 L 12 -20.859375 L 12 0 L 9.6875 0 L 9.6875 -1.859375 C 8.875 -0.441406 7.671875 0.265625 6.078125 0.265625 C 4.847656 0.265625 3.789062 -0.269531 2.90625 -1.34375 C 2.03125 -2.425781 1.59375 -4.203125 1.59375 -6.671875 Z M 3.984375 -6.671875 C 3.984375 -4.878906 4.207031 -3.625 4.65625 -2.90625 C 5.113281 -2.195312 5.78125 -1.84375 6.65625 -1.84375 C 8.1875 -1.84375 9.195312 -2.867188 9.6875 -4.921875 L 9.6875 -10.4375 C 9.207031 -12.582031 8.195312 -13.65625 6.65625 -13.65625 C 5.78125 -13.65625 5.113281 -13.296875 4.65625 -12.578125 C 4.207031 -11.867188 3.984375 -10.613281 3.984375 -8.8125 Z M 3.984375 -6.671875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-10">
+<path style="stroke:none;" d="M 12.953125 -2.15625 C 11.867188 -0.539062 10.414062 0.265625 8.59375 0.265625 C 7.394531 0.265625 6.382812 -0.140625 5.5625 -0.953125 C 4.75 -1.773438 4.34375 -2.984375 4.34375 -4.578125 L 4.34375 -13.5 L 1.75 -13.5 L 1.75 -15.484375 L 4.34375 -15.484375 L 4.34375 -20.859375 L 6.671875 -20.859375 L 6.671875 -15.484375 L 11.890625 -15.484375 L 11.890625 -13.5 L 6.671875 -13.5 L 6.671875 -4.4375 C 6.671875 -3.582031 6.851562 -2.9375 7.21875 -2.5 C 7.582031 -2.0625 8.085938 -1.84375 8.734375 -1.84375 C 9.804688 -1.84375 10.625 -2.378906 11.1875 -3.453125 Z M 12.953125 -2.15625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-11">
+<path style="stroke:none;" d="M 5.6875 0 L 3.109375 -9.609375 C 1.941406 -13.910156 1.359375 -17.273438 1.359375 -19.703125 L 1.359375 -20.859375 L 3.6875 -20.859375 L 3.6875 -19.703125 C 3.6875 -17.691406 4.125 -14.804688 5 -11.046875 L 7 -2.609375 L 9 -11.046875 C 9.875 -14.804688 10.3125 -17.691406 10.3125 -19.703125 L 10.3125 -20.859375 L 12.640625 -20.859375 L 12.640625 -19.703125 C 12.640625 -17.273438 12.054688 -13.910156 10.890625 -9.609375 L 8.3125 0 Z M 5.6875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-12">
+<path style="stroke:none;" d="M 9.578125 -1.28125 L 10.125 -6.296875 C 10.351562 -8.390625 10.46875 -10.53125 10.46875 -12.71875 L 10.46875 -15.484375 L 12.75 -15.484375 L 12.75 -13.421875 C 12.75 -11.554688 12.484375 -9.179688 11.953125 -6.296875 L 10.796875 0 L 8.609375 0 L 7 -6.96875 L 5.390625 0 L 3.203125 0 L 2.046875 -6.296875 C 1.523438 -9.179688 1.265625 -11.554688 1.265625 -13.421875 L 1.265625 -15.484375 L 3.53125 -15.484375 L 3.53125 -12.71875 C 3.53125 -10.53125 3.644531 -8.390625 3.875 -6.296875 L 4.421875 -1.28125 L 6.265625 -9.25 L 7.734375 -9.25 Z M 9.578125 -1.28125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-13">
+<path style="stroke:none;" d="M 9.734375 -1.265625 L 10.09375 -6.1875 C 10.226562 -8.007812 10.296875 -10.070312 10.296875 -12.375 L 10.296875 -20.859375 L 12.625 -20.859375 L 12.625 -15.828125 C 12.625 -12.398438 12.375 -9.1875 11.875 -6.1875 L 10.90625 0 L 8.71875 0 L 7 -8.078125 L 5.28125 0 L 3.09375 0 L 2.125 -6.1875 C 1.625 -9.1875 1.375 -12.398438 1.375 -15.828125 L 1.375 -20.859375 L 3.703125 -20.859375 L 3.703125 -12.375 C 3.703125 -10.070312 3.769531 -8.007812 3.90625 -6.1875 L 4.265625 -1.265625 L 6.265625 -10.671875 L 7.734375 -10.671875 Z M 9.734375 -1.265625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-14">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.03125 C 9.6875 -11.375 9.476562 -12.304688 9.0625 -12.828125 C 8.644531 -13.359375 8.070312 -13.625 7.34375 -13.625 C 5.8125 -13.625 4.800781 -12.570312 4.3125 -10.46875 L 4.3125 0 L 1.984375 0 L 1.984375 -15.484375 L 4.3125 -15.484375 L 4.3125 -13.5 C 5.300781 -15.007812 6.503906 -15.765625 7.921875 -15.765625 C 9.066406 -15.765625 10.035156 -15.335938 10.828125 -14.484375 C 11.617188 -13.628906 12.015625 -12.191406 12.015625 -10.171875 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-15">
+<path style="stroke:none;" d="M 12.21875 -6.703125 C 12.21875 -4.328125 11.726562 -2.570312 10.75 -1.4375 C 9.78125 -0.300781 8.53125 0.265625 7 0.265625 C 5.46875 0.265625 4.21875 -0.300781 3.25 -1.4375 C 2.28125 -2.570312 1.796875 -4.328125 1.796875 -6.703125 L 1.796875 -8.8125 C 1.796875 -11.175781 2.28125 -12.925781 3.25 -14.0625 C 4.21875 -15.195312 5.46875 -15.765625 7 -15.765625 C 8.53125 -15.765625 9.78125 -15.195312 10.75 -14.0625 C 11.726562 -12.925781 12.21875 -11.175781 12.21875 -8.8125 Z M 9.90625 -6.703125 L 9.90625 -8.8125 C 9.90625 -10.632812 9.628906 -11.894531 9.078125 -12.59375 C 8.535156 -13.300781 7.84375 -13.65625 7 -13.65625 C 6.15625 -13.65625 5.460938 -13.300781 4.921875 -12.59375 C 4.378906 -11.894531 4.109375 -10.632812 4.109375 -8.8125 L 4.109375 -6.703125 C 4.109375 -4.867188 4.378906 -3.597656 4.921875 -2.890625 C 5.460938 -2.191406 6.15625 -1.84375 7 -1.84375 C 7.84375 -1.84375 8.535156 -2.191406 9.078125 -2.890625 C 9.628906 -3.597656 9.90625 -4.867188 9.90625 -6.703125 Z M 9.90625 -6.703125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-16">
+<path style="stroke:none;" d="M 7.0625 -1.796875 C 8.019531 -1.796875 8.75 -2.023438 9.25 -2.484375 C 9.75 -2.941406 10 -3.5 10 -4.15625 C 10 -5.0625 9.445312 -5.765625 8.34375 -6.265625 L 5.359375 -7.625 C 3.503906 -8.5 2.578125 -9.75 2.578125 -11.375 C 2.578125 -12.644531 3.03125 -13.691406 3.9375 -14.515625 C 4.851562 -15.347656 6.035156 -15.765625 7.484375 -15.765625 C 9.546875 -15.765625 11.128906 -14.8125 12.234375 -12.90625 L 10.34375 -11.765625 C 9.757812 -13.054688 8.804688 -13.703125 7.484375 -13.703125 C 6.691406 -13.703125 6.0625 -13.5 5.59375 -13.09375 C 5.132812 -12.695312 4.90625 -12.203125 4.90625 -11.609375 C 4.90625 -10.816406 5.421875 -10.175781 6.453125 -9.6875 L 9.265625 -8.375 C 11.296875 -7.425781 12.3125 -5.984375 12.3125 -4.046875 C 12.3125 -2.898438 11.835938 -1.894531 10.890625 -1.03125 C 9.941406 -0.164062 8.65625 0.265625 7.03125 0.265625 C 4.75 0.265625 2.96875 -0.789062 1.6875 -2.90625 L 3.59375 -4.0625 C 4.351562 -2.550781 5.507812 -1.796875 7.0625 -1.796875 Z M 7.0625 -1.796875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-17">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.4375 L 4.3125 -10.4375 L 4.3125 0 L 1.984375 0 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -12.421875 L 9.6875 -12.421875 L 9.6875 -20.859375 L 12.015625 -20.859375 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-18">
+<path style="stroke:none;" d="M 12.421875 -6.65625 C 12.421875 -4.21875 11.953125 -2.453125 11.015625 -1.359375 C 10.085938 -0.273438 9.054688 0.265625 7.921875 0.265625 C 6.328125 0.265625 5.128906 -0.457031 4.328125 -1.90625 L 4.328125 4.828125 L 2 4.828125 L 2 -15.484375 L 4.328125 -15.484375 L 4.328125 -13.640625 C 5.128906 -15.054688 6.328125 -15.765625 7.921875 -15.765625 C 9.054688 -15.765625 10.085938 -15.222656 11.015625 -14.140625 C 11.953125 -13.066406 12.421875 -11.289062 12.421875 -8.8125 Z M 10.015625 -8.8125 C 10.015625 -10.625 9.765625 -11.882812 9.265625 -12.59375 C 8.765625 -13.300781 8.125 -13.65625 7.34375 -13.65625 C 5.8125 -13.65625 4.804688 -12.59375 4.328125 -10.46875 L 4.328125 -5.046875 C 4.796875 -2.910156 5.800781 -1.84375 7.34375 -1.84375 C 8.125 -1.84375 8.765625 -2.203125 9.265625 -2.921875 C 9.765625 -3.640625 10.015625 -4.882812 10.015625 -6.65625 Z M 10.015625 -8.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-19">
+<path style="stroke:none;" d="M 12.953125 -3.09375 C 11.648438 -0.851562 9.878906 0.265625 7.640625 0.265625 C 6.128906 0.265625 4.851562 -0.238281 3.8125 -1.25 C 2.769531 -2.269531 2.25 -4.132812 2.25 -6.84375 L 2.25 -14.046875 C 2.25 -16.679688 2.769531 -18.523438 3.8125 -19.578125 C 4.851562 -20.640625 6.117188 -21.171875 7.609375 -21.171875 C 9.953125 -21.171875 11.6875 -20.039062 12.8125 -17.78125 L 10.90625 -16.625 C 10.21875 -18.238281 9.125 -19.046875 7.625 -19.046875 C 6.757812 -19.046875 6.035156 -18.726562 5.453125 -18.09375 C 4.878906 -17.457031 4.59375 -16.109375 4.59375 -14.046875 L 4.59375 -6.84375 C 4.59375 -4.78125 4.878906 -3.429688 5.453125 -2.796875 C 6.035156 -2.160156 6.757812 -1.84375 7.625 -1.84375 C 9.125 -1.84375 10.265625 -2.6875 11.046875 -4.375 Z M 12.953125 -3.09375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-20">
+<path style="stroke:none;" d="M 12.453125 -18.875 L 8.15625 -18.875 L 8.15625 0 L 5.84375 0 L 5.84375 -18.875 L 1.546875 -18.875 L 1.546875 -20.859375 L 12.453125 -20.859375 Z M 12.453125 -18.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-21">
+<path style="stroke:none;" d="M 12.328125 0 L 10.015625 0 L 10.015625 -5.515625 C 10.015625 -9.023438 10.148438 -13.148438 10.421875 -17.890625 L 7.71875 -9.25 L 6.28125 -9.25 L 3.578125 -17.890625 C 3.859375 -13.148438 4 -9.023438 4 -5.515625 L 4 0 L 1.671875 0 L 1.671875 -20.859375 L 4 -20.859375 L 7 -12.203125 L 10.015625 -20.859375 L 12.328125 -20.859375 Z M 12.328125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-22">
+<path style="stroke:none;" d="M 12.53125 -4.875 C 12.53125 -3.457031 12.054688 -2.242188 11.109375 -1.234375 C 10.171875 -0.234375 8.867188 0.265625 7.203125 0.265625 C 4.578125 0.265625 2.660156 -0.878906 1.453125 -3.171875 L 3.171875 -4.46875 C 4.078125 -2.71875 5.414062 -1.84375 7.1875 -1.84375 C 8.007812 -1.84375 8.71875 -2.09375 9.3125 -2.59375 C 9.90625 -3.101562 10.203125 -3.859375 10.203125 -4.859375 C 10.203125 -5.671875 9.691406 -6.554688 8.671875 -7.515625 L 4.1875 -11.703125 C 2.9375 -12.878906 2.3125 -14.359375 2.3125 -16.140625 C 2.3125 -17.554688 2.769531 -18.75 3.6875 -19.71875 C 4.613281 -20.6875 5.878906 -21.171875 7.484375 -21.171875 C 9.566406 -21.171875 11.253906 -20.21875 12.546875 -18.3125 L 10.765625 -16.90625 C 9.992188 -18.332031 8.90625 -19.046875 7.5 -19.046875 C 6.6875 -19.046875 6.007812 -18.8125 5.46875 -18.34375 C 4.925781 -17.882812 4.65625 -17.148438 4.65625 -16.140625 C 4.65625 -15.109375 5.0625 -14.210938 5.875 -13.453125 L 10.34375 -9.296875 C 11.800781 -7.910156 12.53125 -6.4375 12.53125 -4.875 Z M 12.53125 -4.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-23">
+<path style="stroke:none;" d="M 11.671875 0 L 9.359375 0 L 9.359375 -1.75 C 8.367188 -0.40625 7.125 0.265625 5.625 0.265625 C 4.351562 0.265625 3.328125 -0.164062 2.546875 -1.03125 C 1.765625 -1.90625 1.375 -3 1.375 -4.3125 C 1.375 -5.65625 1.863281 -6.78125 2.84375 -7.6875 C 3.820312 -8.601562 5.253906 -9.0625 7.140625 -9.0625 L 9.359375 -9.0625 L 9.359375 -10.734375 C 9.359375 -12.679688 8.429688 -13.65625 6.578125 -13.65625 C 5.210938 -13.65625 4.191406 -13.054688 3.515625 -11.859375 L 1.75 -13.171875 C 2.96875 -14.898438 4.582031 -15.765625 6.59375 -15.765625 C 8.070312 -15.765625 9.285156 -15.335938 10.234375 -14.484375 C 11.191406 -13.640625 11.671875 -12.390625 11.671875 -10.734375 Z M 9.359375 -3.640625 L 9.359375 -7.1875 L 7.140625 -7.1875 C 6.023438 -7.1875 5.1875 -6.925781 4.625 -6.40625 C 4.0625 -5.894531 3.78125 -5.195312 3.78125 -4.3125 C 3.78125 -2.664062 4.582031 -1.84375 6.1875 -1.84375 C 7.457031 -1.84375 8.515625 -2.441406 9.359375 -3.640625 Z M 9.359375 -3.640625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-24">
+<path style="stroke:none;" d="M 13 -12.859375 L 10.984375 -11.96875 C 10.523438 -13.09375 9.753906 -13.65625 8.671875 -13.65625 C 6.390625 -13.65625 5.25 -11.535156 5.25 -7.296875 L 5.25 0 L 2.921875 0 L 2.921875 -15.484375 L 5.25 -15.484375 L 5.25 -13 C 6.164062 -14.84375 7.425781 -15.765625 9.03125 -15.765625 C 10.894531 -15.765625 12.21875 -14.796875 13 -12.859375 Z M 13 -12.859375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-25">
+<path style="stroke:none;" d="M 12.84375 -2.578125 C 11.65625 -0.679688 9.957031 0.265625 7.75 0.265625 C 6.164062 0.265625 4.863281 -0.296875 3.84375 -1.421875 C 2.820312 -2.554688 2.3125 -4.347656 2.3125 -6.796875 L 2.3125 -8.703125 C 2.3125 -11.140625 2.820312 -12.925781 3.84375 -14.0625 C 4.863281 -15.195312 6.164062 -15.765625 7.75 -15.765625 C 9.957031 -15.765625 11.65625 -14.8125 12.84375 -12.90625 L 10.953125 -11.71875 C 10.179688 -13.007812 9.160156 -13.65625 7.890625 -13.65625 C 6.898438 -13.65625 6.113281 -13.328125 5.53125 -12.671875 C 4.945312 -12.015625 4.65625 -10.691406 4.65625 -8.703125 L 4.65625 -6.796875 C 4.65625 -4.804688 4.945312 -3.484375 5.53125 -2.828125 C 6.113281 -2.171875 6.898438 -1.84375 7.890625 -1.84375 C 9.160156 -1.84375 10.179688 -2.488281 10.953125 -3.78125 Z M 12.84375 -2.578125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-26">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.015625 C 9.6875 -11.398438 9.476562 -12.351562 9.0625 -12.875 C 8.644531 -13.394531 8.070312 -13.65625 7.34375 -13.65625 C 5.8125 -13.65625 4.800781 -12.59375 4.3125 -10.46875 L 4.3125 0 L 1.984375 0 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -13.625 C 5.300781 -15.050781 6.503906 -15.765625 7.921875 -15.765625 C 9.066406 -15.765625 10.035156 -15.335938 10.828125 -14.484375 C 11.617188 -13.628906 12.015625 -12.179688 12.015625 -10.140625 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-27">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -2.015625 C 8.695312 -0.492188 7.492188 0.265625 6.078125 0.265625 C 4.929688 0.265625 3.960938 -0.164062 3.171875 -1.03125 C 2.378906 -1.90625 1.984375 -3.460938 1.984375 -5.703125 L 1.984375 -15.484375 L 4.3125 -15.484375 L 4.3125 -5.703125 C 4.3125 -4.203125 4.519531 -3.179688 4.9375 -2.640625 C 5.351562 -2.109375 5.925781 -1.84375 6.65625 -1.84375 C 8.1875 -1.84375 9.195312 -2.851562 9.6875 -4.875 L 9.6875 -15.484375 L 12.015625 -15.484375 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-28">
+<path style="stroke:none;" d="M 12.28125 -7.296875 L 1.71875 -7.296875 L 1.71875 -9.28125 L 12.28125 -9.28125 Z M 12.28125 -7.296875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-29">
+<path style="stroke:none;" d="M 12.09375 0 L 2.4375 0 L 2.4375 -1.984375 C 2.4375 -4.128906 3.460938 -6.515625 5.515625 -9.140625 L 8.609375 -13.109375 C 9.378906 -14.085938 9.765625 -15.0625 9.765625 -16.03125 C 9.765625 -17.050781 9.515625 -17.804688 9.015625 -18.296875 C 8.523438 -18.796875 7.851562 -19.046875 7 -19.046875 C 5.519531 -19.046875 4.457031 -18.15625 3.8125 -16.375 L 1.90625 -17.703125 C 3.09375 -20.015625 4.789062 -21.171875 7 -21.171875 C 8.414062 -21.171875 9.617188 -20.679688 10.609375 -19.703125 C 11.597656 -18.734375 12.09375 -17.503906 12.09375 -16.015625 C 12.09375 -14.546875 11.5 -13.054688 10.3125 -11.546875 L 7.265625 -7.625 C 5.597656 -5.488281 4.765625 -3.609375 4.765625 -1.984375 L 12.09375 -1.984375 Z M 12.09375 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-30">
+<path style="stroke:none;" d="M 1.421875 -10.453125 C 1.421875 -11.898438 1.882812 -14.296875 2.8125 -17.640625 C 3.4375 -19.992188 4.8125 -21.171875 6.9375 -21.171875 C 9.144531 -21.171875 10.5625 -19.992188 11.1875 -17.640625 C 12.113281 -14.296875 12.578125 -11.898438 12.578125 -10.453125 C 12.578125 -9.003906 12.113281 -6.609375 11.1875 -3.265625 C 10.5625 -0.910156 9.144531 0.265625 6.9375 0.265625 C 4.8125 0.265625 3.4375 -0.910156 2.8125 -3.265625 C 1.882812 -6.609375 1.421875 -9.003906 1.421875 -10.453125 Z M 10.25 -10.453125 C 10.25 -11.910156 9.84375 -14.109375 9.03125 -17.046875 C 8.695312 -18.359375 8 -19.019531 6.9375 -19.03125 C 5.957031 -19.019531 5.304688 -18.359375 4.984375 -17.046875 C 4.160156 -14.109375 3.75 -11.910156 3.75 -10.453125 C 3.75 -8.992188 4.160156 -6.785156 4.984375 -3.828125 C 5.304688 -2.515625 5.957031 -1.851562 6.9375 -1.84375 C 8 -1.851562 8.695312 -2.515625 9.03125 -3.828125 C 9.84375 -6.785156 10.25 -8.992188 10.25 -10.453125 Z M 8.890625 -8.265625 L 7.453125 -8.265625 L 5.109375 -12.953125 L 6.546875 -12.953125 Z M 8.890625 -8.265625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-31">
+<path style="stroke:none;" d="M 12.1875 -5.65625 L 10.828125 -5.65625 L 10.828125 0 L 8.5 0 L 8.5 -5.65625 L 1.15625 -5.65625 L 1.15625 -7.640625 L 7.96875 -20.859375 L 10.828125 -20.859375 L 10.828125 -7.640625 L 12.1875 -7.640625 Z M 8.5 -7.640625 L 8.5 -17.46875 L 3.4375 -7.640625 Z M 8.5 -7.640625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-32">
+<path style="stroke:none;" d="M 7.421875 0 L 7.421875 -18.0625 L 3.5 -14.984375 L 2.453125 -16.765625 L 7.6875 -20.859375 L 9.765625 -20.859375 L 9.765625 0 Z M 7.421875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-33">
+<path style="stroke:none;" d="M 9.6875 -2.421875 C 9.6875 -1.710938 9.429688 -1.085938 8.921875 -0.546875 C 8.410156 -0.00390625 7.765625 0.265625 6.984375 0.265625 C 6.203125 0.265625 5.5625 -0.00390625 5.0625 -0.546875 C 4.570312 -1.085938 4.328125 -1.710938 4.328125 -2.421875 C 4.328125 -3.117188 4.570312 -3.738281 5.0625 -4.28125 C 5.5625 -4.832031 6.203125 -5.109375 6.984375 -5.109375 C 7.765625 -5.109375 8.410156 -4.832031 8.921875 -4.28125 C 9.429688 -3.726562 9.6875 -3.109375 9.6875 -2.421875 Z M 9.6875 -13.078125 C 9.6875 -12.359375 9.429688 -11.726562 8.921875 -11.1875 C 8.410156 -10.65625 7.765625 -10.390625 6.984375 -10.390625 C 6.203125 -10.390625 5.5625 -10.65625 5.0625 -11.1875 C 4.570312 -11.726562 4.328125 -12.359375 4.328125 -13.078125 C 4.328125 -13.785156 4.570312 -14.410156 5.0625 -14.953125 C 5.5625 -15.492188 6.203125 -15.765625 6.984375 -15.765625 C 7.765625 -15.765625 8.410156 -15.488281 8.921875 -14.9375 C 9.429688 -14.394531 9.6875 -13.773438 9.6875 -13.078125 Z M 9.6875 -13.078125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-34">
+<path style="stroke:none;" d="M 12.171875 -6.515625 C 12.171875 -4.359375 11.648438 -2.6875 10.609375 -1.5 C 9.566406 -0.320312 8.300781 0.265625 6.8125 0.265625 C 4.550781 0.265625 2.847656 -0.804688 1.703125 -2.953125 L 3.609375 -4.21875 C 4.253906 -2.632812 5.316406 -1.84375 6.796875 -1.84375 C 7.597656 -1.84375 8.300781 -2.203125 8.90625 -2.921875 C 9.519531 -3.640625 9.828125 -4.835938 9.828125 -6.515625 C 9.828125 -8.203125 9.523438 -9.382812 8.921875 -10.0625 C 8.328125 -10.738281 7.351562 -11.078125 6 -11.078125 L 2.90625 -11.078125 L 2.90625 -20.859375 L 11.734375 -20.859375 L 11.734375 -18.875 L 5.234375 -18.875 L 5.234375 -13.1875 L 6.40625 -13.1875 C 8.132812 -13.1875 9.523438 -12.664062 10.578125 -11.625 C 11.640625 -10.59375 12.171875 -8.890625 12.171875 -6.515625 Z M 12.171875 -6.515625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-35">
+<path style="stroke:none;" d="M 6.984375 0.265625 C 5.398438 0.265625 4.085938 -0.234375 3.046875 -1.234375 C 2.003906 -2.242188 1.484375 -3.601562 1.484375 -5.3125 C 1.484375 -7.863281 2.675781 -9.820312 5.0625 -11.1875 C 3.050781 -12.59375 2.046875 -14.242188 2.046875 -16.140625 C 2.046875 -17.648438 2.515625 -18.863281 3.453125 -19.78125 C 4.398438 -20.707031 5.578125 -21.171875 6.984375 -21.171875 C 8.410156 -21.171875 9.59375 -20.707031 10.53125 -19.78125 C 11.476562 -18.863281 11.953125 -17.648438 11.953125 -16.140625 C 11.953125 -14.242188 10.945312 -12.59375 8.9375 -11.1875 C 11.320312 -9.820312 12.515625 -7.863281 12.515625 -5.3125 C 12.515625 -3.601562 11.992188 -2.242188 10.953125 -1.234375 C 9.910156 -0.234375 8.585938 0.265625 6.984375 0.265625 Z M 6.984375 -19.046875 C 6.222656 -19.046875 5.597656 -18.796875 5.109375 -18.296875 C 4.617188 -17.804688 4.375 -17.085938 4.375 -16.140625 C 4.375 -14.609375 5.242188 -13.304688 6.984375 -12.234375 C 8.742188 -13.304688 9.625 -14.609375 9.625 -16.140625 C 9.625 -17.085938 9.378906 -17.804688 8.890625 -18.296875 C 8.410156 -18.796875 7.773438 -19.046875 6.984375 -19.046875 Z M 6.984375 -9.875 C 4.867188 -8.832031 3.8125 -7.3125 3.8125 -5.3125 C 3.8125 -4.050781 4.101562 -3.15625 4.6875 -2.625 C 5.28125 -2.101562 6.046875 -1.84375 6.984375 -1.84375 C 7.941406 -1.84375 8.710938 -2.101562 9.296875 -2.625 C 9.890625 -3.15625 10.1875 -4.050781 10.1875 -5.3125 C 10.1875 -7.3125 9.117188 -8.832031 6.984375 -9.875 Z M 6.984375 -9.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-36">
+<path style="stroke:none;" d="M 12.8125 0 L 10.484375 0 C 10.410156 -1.132812 9.851562 -2.460938 8.8125 -3.984375 L 6.5 -7.28125 L 4.765625 -5.1875 L 4.765625 0 L 2.453125 0 L 2.453125 -20.859375 L 4.765625 -20.859375 L 4.765625 -8.046875 L 8.1875 -12.25 C 9.332031 -13.65625 9.914062 -14.734375 9.9375 -15.484375 L 12.265625 -15.484375 L 12.234375 -14.96875 C 12.191406 -14.15625 11.421875 -12.835938 9.921875 -11.015625 L 8.0625 -8.734375 L 10.46875 -5.25 C 11.9375 -3.125 12.703125 -1.554688 12.765625 -0.546875 Z M 12.8125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-37">
+<path style="stroke:none;" d="M 8.34375 0 L 5.65625 0 L 2.984375 -8.09375 C 2.054688 -10.863281 1.550781 -12.957031 1.46875 -14.375 L 1.390625 -15.484375 L 3.71875 -15.484375 L 3.78125 -14.375 C 3.863281 -13.03125 4.304688 -11.046875 5.109375 -8.421875 L 7 -2.1875 L 8.890625 -8.421875 C 9.691406 -11.046875 10.132812 -13.03125 10.21875 -14.375 L 10.28125 -15.484375 L 12.609375 -15.484375 L 12.53125 -14.375 C 12.445312 -12.957031 11.941406 -10.863281 11.015625 -8.09375 Z M 8.34375 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-38">
+<path style="stroke:none;" d="M -0.265625 -9.421875 L -0.265625 -11.390625 L 14.265625 -11.390625 L 14.265625 -9.421875 Z M -0.265625 -9.421875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-39">
+<path style="stroke:none;" d="M 0 -7.125 L 0 -9.109375 L 14 -9.109375 L 14 -7.125 Z M 0 -11.71875 L 0 -13.703125 L 14 -13.703125 L 14 -11.71875 Z M 0 -11.71875 "/>
+</symbol>
+</g>
+</defs>
+<g id="surface9805">
+<rect x="0" y="0" width="1151" height="248" style="fill:rgb(15.686275%,17.254902%,20.392157%);fill-opacity:1;stroke:none;"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 17 0 L 577 0 L 577 31 L 17 31 Z M 17 0 "/>
+<g style="fill:rgb(84.705882%,87.058824%,91.372549%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="17" y="26"/>
+ <use xlink:href="#glyph0-2" x="31" y="26"/>
+ <use xlink:href="#glyph0-3" x="45" y="26"/>
+ <use xlink:href="#glyph0-4" x="59" y="26"/>
+ <use xlink:href="#glyph0-5" x="73" y="26"/>
+ <use xlink:href="#glyph0-6" x="87" y="26"/>
+ <use xlink:href="#glyph0-7" x="101" y="26"/>
+ <use xlink:href="#glyph0-1" x="115" y="26"/>
+ <use xlink:href="#glyph0-2" x="129" y="26"/>
+ <use xlink:href="#glyph0-8" x="143" y="26"/>
+ <use xlink:href="#glyph0-9" x="157" y="26"/>
+ <use xlink:href="#glyph0-4" x="171" y="26"/>
+ <use xlink:href="#glyph0-10" x="185" y="26"/>
+ <use xlink:href="#glyph0-7" x="199" y="26"/>
+ <use xlink:href="#glyph0-1" x="213" y="26"/>
+ <use xlink:href="#glyph0-2" x="227" y="26"/>
+ <use xlink:href="#glyph0-11" x="241" y="26"/>
+ <use xlink:href="#glyph0-4" x="255" y="26"/>
+ <use xlink:href="#glyph0-6" x="269" y="26"/>
+ <use xlink:href="#glyph0-12" x="283" y="26"/>
+ <use xlink:href="#glyph0-7" x="297" y="26"/>
+ <use xlink:href="#glyph0-1" x="311" y="26"/>
+ <use xlink:href="#glyph0-2" x="325" y="26"/>
+ <use xlink:href="#glyph0-13" x="339" y="26"/>
+ <use xlink:href="#glyph0-4" x="353" y="26"/>
+ <use xlink:href="#glyph0-14" x="367" y="26"/>
+ <use xlink:href="#glyph0-9" x="381" y="26"/>
+ <use xlink:href="#glyph0-15" x="395" y="26"/>
+ <use xlink:href="#glyph0-12" x="409" y="26"/>
+ <use xlink:href="#glyph0-16" x="423" y="26"/>
+ <use xlink:href="#glyph0-7" x="437" y="26"/>
+ <use xlink:href="#glyph0-1" x="451" y="26"/>
+ <use xlink:href="#glyph0-2" x="465" y="26"/>
+ <use xlink:href="#glyph0-17" x="479" y="26"/>
+ <use xlink:href="#glyph0-6" x="493" y="26"/>
+ <use xlink:href="#glyph0-5" x="507" y="26"/>
+ <use xlink:href="#glyph0-18" x="521" y="26"/>
+ <use xlink:href="#glyph0-7" x="535" y="26"/>
+ <use xlink:href="#glyph0-1" x="549" y="26"/>
+ <use xlink:href="#glyph0-1" x="563" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 577 0 L 815 0 L 815 31 L 577 31 Z M 577 0 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="577" y="26"/>
+ <use xlink:href="#glyph0-15" x="591" y="26"/>
+ <use xlink:href="#glyph0-14" x="605" y="26"/>
+ <use xlink:href="#glyph0-16" x="619" y="26"/>
+ <use xlink:href="#glyph0-15" x="633" y="26"/>
+ <use xlink:href="#glyph0-5" x="647" y="26"/>
+ <use xlink:href="#glyph0-6" x="661" y="26"/>
+ <use xlink:href="#glyph0-1" x="675" y="26"/>
+ <use xlink:href="#glyph0-20" x="689" y="26"/>
+ <use xlink:href="#glyph0-6" x="703" y="26"/>
+ <use xlink:href="#glyph0-16" x="717" y="26"/>
+ <use xlink:href="#glyph0-10" x="731" y="26"/>
+ <use xlink:href="#glyph0-1" x="745" y="26"/>
+ <use xlink:href="#glyph0-21" x="759" y="26"/>
+ <use xlink:href="#glyph0-15" x="773" y="26"/>
+ <use xlink:href="#glyph0-9" x="787" y="26"/>
+ <use xlink:href="#glyph0-6" x="801" y="26"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="578" y="26"/>
+ <use xlink:href="#glyph0-15" x="592" y="26"/>
+ <use xlink:href="#glyph0-14" x="606" y="26"/>
+ <use xlink:href="#glyph0-16" x="620" y="26"/>
+ <use xlink:href="#glyph0-15" x="634" y="26"/>
+ <use xlink:href="#glyph0-5" x="648" y="26"/>
+ <use xlink:href="#glyph0-6" x="662" y="26"/>
+ <use xlink:href="#glyph0-1" x="676" y="26"/>
+ <use xlink:href="#glyph0-20" x="690" y="26"/>
+ <use xlink:href="#glyph0-6" x="704" y="26"/>
+ <use xlink:href="#glyph0-16" x="718" y="26"/>
+ <use xlink:href="#glyph0-10" x="732" y="26"/>
+ <use xlink:href="#glyph0-1" x="746" y="26"/>
+ <use xlink:href="#glyph0-21" x="760" y="26"/>
+ <use xlink:href="#glyph0-15" x="774" y="26"/>
+ <use xlink:href="#glyph0-9" x="788" y="26"/>
+ <use xlink:href="#glyph0-6" x="802" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 815 0 L 843 0 L 843 31 L 815 31 Z M 815 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 843 0 L 857 0 L 857 31 L 843 31 Z M 843 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 857 0 L 1025 0 L 1025 31 L 857 31 Z M 857 0 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-22" x="857" y="26"/>
+ <use xlink:href="#glyph0-6" x="871" y="26"/>
+ <use xlink:href="#glyph0-23" x="885" y="26"/>
+ <use xlink:href="#glyph0-24" x="899" y="26"/>
+ <use xlink:href="#glyph0-25" x="913" y="26"/>
+ <use xlink:href="#glyph0-26" x="927" y="26"/>
+ <use xlink:href="#glyph0-1" x="941" y="26"/>
+ <use xlink:href="#glyph0-21" x="955" y="26"/>
+ <use xlink:href="#glyph0-6" x="969" y="26"/>
+ <use xlink:href="#glyph0-14" x="983" y="26"/>
+ <use xlink:href="#glyph0-27" x="997" y="26"/>
+ <use xlink:href="#glyph0-1" x="1011" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1025 0 L 1109 0 L 1109 31 L 1025 31 Z M 1025 0 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="1025" y="26"/>
+ <use xlink:href="#glyph0-10" x="1039" y="26"/>
+ <use xlink:href="#glyph0-24" x="1053" y="26"/>
+ <use xlink:href="#glyph0-5" x="1067" y="26"/>
+ <use xlink:href="#glyph0-28" x="1081" y="26"/>
+ <use xlink:href="#glyph0-18" x="1095" y="26"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="1026" y="26"/>
+ <use xlink:href="#glyph0-10" x="1040" y="26"/>
+ <use xlink:href="#glyph0-24" x="1054" y="26"/>
+ <use xlink:href="#glyph0-5" x="1068" y="26"/>
+ <use xlink:href="#glyph0-28" x="1082" y="26"/>
+ <use xlink:href="#glyph0-18" x="1096" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1109 0 L 1123 0 L 1123 31 L 1109 31 Z M 1109 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 1123 0 L 1137 0 L 1137 31 L 1123 31 Z M 1123 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 0 L 1151 0 L 1151 31 L 1137 31 Z M 1137 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 0 L 17 0 L 17 31 L 0 31 Z M 0 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 31 L 1137 31 L 1137 62 L 17 62 Z M 17 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 31 L 1151 31 L 1151 62 L 1137 62 Z M 1137 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 31 L 17 31 L 17 62 L 0 62 Z M 0 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 62 L 45 62 L 45 93 L 17 93 Z M 17 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 45 62 L 185 62 L 185 93 L 45 93 Z M 45 62 "/>
+<g style="fill:rgb(53.333333%,75.294118%,81.568627%);fill-opacity:1;">
+ <use xlink:href="#glyph0-29" x="45" y="88"/>
+ <use xlink:href="#glyph0-30" x="59" y="88"/>
+ <use xlink:href="#glyph0-29" x="73" y="88"/>
+ <use xlink:href="#glyph0-29" x="87" y="88"/>
+ <use xlink:href="#glyph0-28" x="101" y="88"/>
+ <use xlink:href="#glyph0-30" x="115" y="88"/>
+ <use xlink:href="#glyph0-31" x="129" y="88"/>
+ <use xlink:href="#glyph0-28" x="143" y="88"/>
+ <use xlink:href="#glyph0-32" x="157" y="88"/>
+ <use xlink:href="#glyph0-32" x="171" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 185 62 L 199 62 L 199 93 L 185 93 Z M 185 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 199 62 L 311 62 L 311 93 L 199 93 Z M 199 62 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-32" x="199" y="88"/>
+ <use xlink:href="#glyph0-32" x="213" y="88"/>
+ <use xlink:href="#glyph0-33" x="227" y="88"/>
+ <use xlink:href="#glyph0-31" x="241" y="88"/>
+ <use xlink:href="#glyph0-31" x="255" y="88"/>
+ <use xlink:href="#glyph0-33" x="269" y="88"/>
+ <use xlink:href="#glyph0-34" x="283" y="88"/>
+ <use xlink:href="#glyph0-35" x="297" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 311 62 L 1137 62 L 1137 93 L 311 93 Z M 311 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 62 L 1151 62 L 1151 93 L 1137 93 Z M 1137 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 62 L 17 62 L 17 93 L 0 93 Z M 0 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 93 L 1137 93 L 1137 124 L 17 124 Z M 17 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 93 L 1151 93 L 1151 124 L 1137 124 Z M 1137 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 93 L 17 93 L 17 124 L 0 124 Z M 0 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 124 L 1137 124 L 1137 155 L 17 155 Z M 17 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 124 L 1151 124 L 1151 155 L 1137 155 Z M 1137 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 124 L 17 124 L 17 155 L 0 155 Z M 0 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 155 L 1137 155 L 1137 186 L 17 186 Z M 17 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 155 L 1151 155 L 1151 186 L 1137 186 Z M 1137 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 155 L 17 155 L 17 186 L 0 186 Z M 0 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;" d="M 17 186 L 31 186 L 31 217 L 17 217 Z M 17 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 31 186 L 129 186 L 129 217 L 31 217 Z M 31 186 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="31" y="212"/>
+ <use xlink:href="#glyph0-19" x="45" y="212"/>
+ <use xlink:href="#glyph0-5" x="59" y="212"/>
+ <use xlink:href="#glyph0-15" x="73" y="212"/>
+ <use xlink:href="#glyph0-25" x="87" y="212"/>
+ <use xlink:href="#glyph0-36" x="101" y="212"/>
+ <use xlink:href="#glyph0-1" x="115" y="212"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="32" y="212"/>
+ <use xlink:href="#glyph0-19" x="46" y="212"/>
+ <use xlink:href="#glyph0-5" x="60" y="212"/>
+ <use xlink:href="#glyph0-15" x="74" y="212"/>
+ <use xlink:href="#glyph0-25" x="88" y="212"/>
+ <use xlink:href="#glyph0-36" x="102" y="212"/>
+ <use xlink:href="#glyph0-1" x="116" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 129 186 L 157 186 L 157 217 L 129 217 Z M 129 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 157 186 L 171 186 L 171 217 L 157 217 Z M 157 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 171 186 L 311 186 L 311 217 L 171 217 Z M 171 186 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-11" x="171" y="212"/>
+ <use xlink:href="#glyph0-4" x="185" y="212"/>
+ <use xlink:href="#glyph0-6" x="199" y="212"/>
+ <use xlink:href="#glyph0-12" x="213" y="212"/>
+ <use xlink:href="#glyph0-1" x="227" y="212"/>
+ <use xlink:href="#glyph0-21" x="241" y="212"/>
+ <use xlink:href="#glyph0-15" x="255" y="212"/>
+ <use xlink:href="#glyph0-9" x="269" y="212"/>
+ <use xlink:href="#glyph0-6" x="283" y="212"/>
+ <use xlink:href="#glyph0-1" x="297" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 311 186 L 325 186 L 325 217 L 311 217 Z M 311 186 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-37" x="311" y="212"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-37" x="312" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 325 186 L 339 186 L 339 217 L 325 217 Z M 325 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 339 186 L 367 186 L 367 217 L 339 217 Z M 339 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 367 186 L 381 186 L 381 217 L 367 217 Z M 367 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 381 186 L 437 186 L 437 217 L 381 217 Z M 381 186 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-2" x="381" y="212"/>
+ <use xlink:href="#glyph0-1" x="395" y="212"/>
+ <use xlink:href="#glyph0-7" x="409" y="212"/>
+ <use xlink:href="#glyph0-1" x="423" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 437 186 L 507 186 L 507 217 L 437 217 Z M 437 186 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-13" x="437" y="212"/>
+ <use xlink:href="#glyph0-24" x="451" y="212"/>
+ <use xlink:href="#glyph0-23" x="465" y="212"/>
+ <use xlink:href="#glyph0-18" x="479" y="212"/>
+ <use xlink:href="#glyph0-1" x="493" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 507 186 L 521 186 L 521 217 L 507 217 Z M 507 186 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-12" x="507" y="212"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-12" x="508" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 521 186 L 535 186 L 535 217 L 521 217 Z M 521 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 535 186 L 1081 186 L 1081 217 L 535 217 Z M 535 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 186 L 1151 186 L 1151 217 L 1137 217 Z M 1137 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1081 186 L 1137 186 L 1137 217 L 1081 217 Z M 1081 186 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-38" x="1081" y="212"/>
+ <use xlink:href="#glyph0-39" x="1095" y="212"/>
+ <use xlink:href="#glyph0-39" x="1109" y="212"/>
+ <use xlink:href="#glyph0-38" x="1123" y="212"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-38" x="1082" y="212"/>
+ <use xlink:href="#glyph0-39" x="1096" y="212"/>
+ <use xlink:href="#glyph0-39" x="1110" y="212"/>
+ <use xlink:href="#glyph0-38" x="1124" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 186 L 17 186 L 17 217 L 0 217 Z M 0 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 17 217 L 45 217 L 45 248 L 17 248 Z M 17 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 45 217 L 1152 217 L 1152 248 L 45 248 Z M 45 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 217 L 17 217 L 17 248 L 0 248 Z M 0 217 "/>
+</g>
+</svg>
diff --git a/pw_console/images/clock_plugin2.png b/pw_console/images/clock_plugin2.png
deleted file mode 100644
index 5d982f6b1..000000000
--- a/pw_console/images/clock_plugin2.png
+++ /dev/null
Binary files differ
diff --git a/pw_console/images/clock_plugin2.svg b/pw_console/images/clock_plugin2.svg
new file mode 100644
index 000000000..614c115fe
--- /dev/null
+++ b/pw_console/images/clock_plugin2.svg
@@ -0,0 +1,1413 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1151pt" height="1054pt" viewBox="0 0 1151 1054" version="1.1">
+<defs>
+<g>
+<symbol overflow="visible" id="glyph0-0">
+<path style="stroke:none;" d="M 3.203125 -1.046875 L 10.796875 -1.046875 L 10.796875 -19.8125 L 3.203125 -19.8125 Z M 1.265625 0.875 L 1.265625 -21.734375 L 12.734375 -21.734375 L 12.734375 0.875 Z M 7.59375 -9.09375 L 7.59375 -8.375 L 5.671875 -8.375 L 5.671875 -9.109375 C 5.671875 -9.796875 5.878906 -10.441406 6.296875 -11.046875 L 7.828125 -13.296875 C 8.097656 -13.679688 8.234375 -14.035156 8.234375 -14.359375 C 8.234375 -14.816406 8.097656 -15.207031 7.828125 -15.53125 C 7.566406 -15.851562 7.210938 -16.015625 6.765625 -16.015625 C 5.960938 -16.015625 5.34375 -15.484375 4.90625 -14.421875 L 3.34375 -15.34375 C 4.1875 -16.957031 5.332031 -17.765625 6.78125 -17.765625 C 7.75 -17.765625 8.550781 -17.4375 9.1875 -16.78125 C 9.820312 -16.132812 10.140625 -15.320312 10.140625 -14.34375 C 10.140625 -13.644531 9.898438 -12.941406 9.421875 -12.234375 L 7.8125 -9.890625 C 7.664062 -9.679688 7.59375 -9.414062 7.59375 -9.09375 Z M 6.78125 -6.75 C 7.144531 -6.75 7.457031 -6.617188 7.71875 -6.359375 C 7.988281 -6.109375 8.125 -5.796875 8.125 -5.421875 C 8.125 -5.046875 7.988281 -4.726562 7.71875 -4.46875 C 7.457031 -4.207031 7.144531 -4.078125 6.78125 -4.078125 C 6.394531 -4.078125 6.070312 -4.207031 5.8125 -4.46875 C 5.5625 -4.726562 5.4375 -5.046875 5.4375 -5.421875 C 5.4375 -5.796875 5.5625 -6.109375 5.8125 -6.359375 C 6.070312 -6.617188 6.394531 -6.75 6.78125 -6.75 Z M 6.78125 -6.75 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-1">
+<path style="stroke:none;" d=""/>
+</symbol>
+<symbol overflow="visible" id="glyph0-2">
+<path style="stroke:none;" d="M 11.546875 1.8125 L 4.296875 1.8125 L 4.296875 -22.671875 L 11.546875 -22.671875 L 11.546875 -20.609375 L 6.625 -20.609375 L 6.625 -0.265625 L 11.546875 -0.265625 Z M 11.546875 1.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-3">
+<path style="stroke:none;" d="M 12.8125 -18.875 L 5.25 -18.875 L 5.25 -12.125 L 11.328125 -12.125 L 11.328125 -10.140625 L 5.25 -10.140625 L 5.25 0 L 2.921875 0 L 2.921875 -20.859375 L 12.8125 -20.859375 Z M 12.8125 -18.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-4">
+<path style="stroke:none;" d="M 8.484375 -20.734375 C 8.484375 -20.234375 8.3125 -19.804688 7.96875 -19.453125 C 7.632812 -19.109375 7.210938 -18.9375 6.703125 -18.9375 C 6.191406 -18.9375 5.765625 -19.109375 5.421875 -19.453125 C 5.078125 -19.804688 4.90625 -20.234375 4.90625 -20.734375 C 4.90625 -21.222656 5.078125 -21.640625 5.421875 -21.984375 C 5.765625 -22.335938 6.191406 -22.515625 6.703125 -22.515625 C 7.210938 -22.515625 7.632812 -22.335938 7.96875 -21.984375 C 8.3125 -21.640625 8.484375 -21.222656 8.484375 -20.734375 Z M 12 0 L 2.25 0 L 2.25 -1.984375 L 5.96875 -1.984375 L 5.96875 -13.5 L 3.296875 -13.5 L 3.296875 -15.484375 L 8.296875 -15.484375 L 8.296875 -1.984375 L 12 -1.984375 Z M 12 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-5">
+<path style="stroke:none;" d="M 11.875 0 L 2.125 0 L 2.125 -1.984375 L 5.84375 -1.984375 L 5.84375 -18.875 L 3.1875 -18.875 L 3.1875 -20.859375 L 8.171875 -20.859375 L 8.171875 -1.984375 L 11.875 -1.984375 Z M 11.875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-6">
+<path style="stroke:none;" d="M 12.203125 -7.28125 L 4.109375 -7.28125 L 4.109375 -6.8125 C 4.109375 -4.8125 4.398438 -3.484375 4.984375 -2.828125 C 5.566406 -2.171875 6.359375 -1.84375 7.359375 -1.84375 C 8.628906 -1.84375 9.644531 -2.488281 10.40625 -3.78125 L 12.296875 -2.578125 C 11.109375 -0.679688 9.410156 0.265625 7.203125 0.265625 C 5.628906 0.265625 4.332031 -0.296875 3.3125 -1.421875 C 2.289062 -2.554688 1.78125 -4.351562 1.78125 -6.8125 L 1.78125 -8.6875 C 1.78125 -11.132812 2.289062 -12.925781 3.3125 -14.0625 C 4.332031 -15.195312 5.5625 -15.765625 7 -15.765625 C 8.53125 -15.765625 9.78125 -15.238281 10.75 -14.1875 C 11.71875 -13.144531 12.203125 -11.441406 12.203125 -9.078125 Z M 9.890625 -9.328125 C 9.890625 -10.898438 9.617188 -12.015625 9.078125 -12.671875 C 8.535156 -13.328125 7.84375 -13.65625 7 -13.65625 C 6.195312 -13.65625 5.515625 -13.328125 4.953125 -12.671875 C 4.390625 -12.015625 4.109375 -10.898438 4.109375 -9.328125 Z M 9.890625 -9.328125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-7">
+<path style="stroke:none;" d="M 9.703125 1.8125 L 2.46875 1.8125 L 2.46875 -0.265625 L 7.375 -0.265625 L 7.375 -20.609375 L 2.46875 -20.609375 L 2.46875 -22.671875 L 9.703125 -22.671875 Z M 9.703125 1.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-8">
+<path style="stroke:none;" d="M 12.203125 0 L 2.578125 0 L 2.578125 -20.859375 L 12.203125 -20.859375 L 12.203125 -18.875 L 4.90625 -18.875 L 4.90625 -12.125 L 10.296875 -12.125 L 10.296875 -10.140625 L 4.90625 -10.140625 L 4.90625 -1.984375 L 12.203125 -1.984375 Z M 12.203125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-9">
+<path style="stroke:none;" d="M 1.59375 -8.8125 C 1.59375 -11.28125 2.03125 -13.054688 2.90625 -14.140625 C 3.789062 -15.222656 4.847656 -15.765625 6.078125 -15.765625 C 7.671875 -15.765625 8.875 -15.046875 9.6875 -13.609375 L 9.6875 -20.859375 L 12 -20.859375 L 12 0 L 9.6875 0 L 9.6875 -1.859375 C 8.875 -0.441406 7.671875 0.265625 6.078125 0.265625 C 4.847656 0.265625 3.789062 -0.269531 2.90625 -1.34375 C 2.03125 -2.425781 1.59375 -4.203125 1.59375 -6.671875 Z M 3.984375 -6.671875 C 3.984375 -4.878906 4.207031 -3.625 4.65625 -2.90625 C 5.113281 -2.195312 5.78125 -1.84375 6.65625 -1.84375 C 8.1875 -1.84375 9.195312 -2.867188 9.6875 -4.921875 L 9.6875 -10.4375 C 9.207031 -12.582031 8.195312 -13.65625 6.65625 -13.65625 C 5.78125 -13.65625 5.113281 -13.296875 4.65625 -12.578125 C 4.207031 -11.867188 3.984375 -10.613281 3.984375 -8.8125 Z M 3.984375 -6.671875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-10">
+<path style="stroke:none;" d="M 12.953125 -2.15625 C 11.867188 -0.539062 10.414062 0.265625 8.59375 0.265625 C 7.394531 0.265625 6.382812 -0.140625 5.5625 -0.953125 C 4.75 -1.773438 4.34375 -2.984375 4.34375 -4.578125 L 4.34375 -13.5 L 1.75 -13.5 L 1.75 -15.484375 L 4.34375 -15.484375 L 4.34375 -20.859375 L 6.671875 -20.859375 L 6.671875 -15.484375 L 11.890625 -15.484375 L 11.890625 -13.5 L 6.671875 -13.5 L 6.671875 -4.4375 C 6.671875 -3.582031 6.851562 -2.9375 7.21875 -2.5 C 7.582031 -2.0625 8.085938 -1.84375 8.734375 -1.84375 C 9.804688 -1.84375 10.625 -2.378906 11.1875 -3.453125 Z M 12.953125 -2.15625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-11">
+<path style="stroke:none;" d="M 5.6875 0 L 3.109375 -9.609375 C 1.941406 -13.910156 1.359375 -17.273438 1.359375 -19.703125 L 1.359375 -20.859375 L 3.6875 -20.859375 L 3.6875 -19.703125 C 3.6875 -17.691406 4.125 -14.804688 5 -11.046875 L 7 -2.609375 L 9 -11.046875 C 9.875 -14.804688 10.3125 -17.691406 10.3125 -19.703125 L 10.3125 -20.859375 L 12.640625 -20.859375 L 12.640625 -19.703125 C 12.640625 -17.273438 12.054688 -13.910156 10.890625 -9.609375 L 8.3125 0 Z M 5.6875 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-12">
+<path style="stroke:none;" d="M 9.578125 -1.28125 L 10.125 -6.296875 C 10.351562 -8.390625 10.46875 -10.53125 10.46875 -12.71875 L 10.46875 -15.484375 L 12.75 -15.484375 L 12.75 -13.421875 C 12.75 -11.554688 12.484375 -9.179688 11.953125 -6.296875 L 10.796875 0 L 8.609375 0 L 7 -6.96875 L 5.390625 0 L 3.203125 0 L 2.046875 -6.296875 C 1.523438 -9.179688 1.265625 -11.554688 1.265625 -13.421875 L 1.265625 -15.484375 L 3.53125 -15.484375 L 3.53125 -12.71875 C 3.53125 -10.53125 3.644531 -8.390625 3.875 -6.296875 L 4.421875 -1.28125 L 6.265625 -9.25 L 7.734375 -9.25 Z M 9.578125 -1.28125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-13">
+<path style="stroke:none;" d="M 9.734375 -1.265625 L 10.09375 -6.1875 C 10.226562 -8.007812 10.296875 -10.070312 10.296875 -12.375 L 10.296875 -20.859375 L 12.625 -20.859375 L 12.625 -15.828125 C 12.625 -12.398438 12.375 -9.1875 11.875 -6.1875 L 10.90625 0 L 8.71875 0 L 7 -8.078125 L 5.28125 0 L 3.09375 0 L 2.125 -6.1875 C 1.625 -9.1875 1.375 -12.398438 1.375 -15.828125 L 1.375 -20.859375 L 3.703125 -20.859375 L 3.703125 -12.375 C 3.703125 -10.070312 3.769531 -8.007812 3.90625 -6.1875 L 4.265625 -1.265625 L 6.265625 -10.671875 L 7.734375 -10.671875 Z M 9.734375 -1.265625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-14">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.03125 C 9.6875 -11.375 9.476562 -12.304688 9.0625 -12.828125 C 8.644531 -13.359375 8.070312 -13.625 7.34375 -13.625 C 5.8125 -13.625 4.800781 -12.570312 4.3125 -10.46875 L 4.3125 0 L 1.984375 0 L 1.984375 -15.484375 L 4.3125 -15.484375 L 4.3125 -13.5 C 5.300781 -15.007812 6.503906 -15.765625 7.921875 -15.765625 C 9.066406 -15.765625 10.035156 -15.335938 10.828125 -14.484375 C 11.617188 -13.628906 12.015625 -12.191406 12.015625 -10.171875 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-15">
+<path style="stroke:none;" d="M 12.21875 -6.703125 C 12.21875 -4.328125 11.726562 -2.570312 10.75 -1.4375 C 9.78125 -0.300781 8.53125 0.265625 7 0.265625 C 5.46875 0.265625 4.21875 -0.300781 3.25 -1.4375 C 2.28125 -2.570312 1.796875 -4.328125 1.796875 -6.703125 L 1.796875 -8.8125 C 1.796875 -11.175781 2.28125 -12.925781 3.25 -14.0625 C 4.21875 -15.195312 5.46875 -15.765625 7 -15.765625 C 8.53125 -15.765625 9.78125 -15.195312 10.75 -14.0625 C 11.726562 -12.925781 12.21875 -11.175781 12.21875 -8.8125 Z M 9.90625 -6.703125 L 9.90625 -8.8125 C 9.90625 -10.632812 9.628906 -11.894531 9.078125 -12.59375 C 8.535156 -13.300781 7.84375 -13.65625 7 -13.65625 C 6.15625 -13.65625 5.460938 -13.300781 4.921875 -12.59375 C 4.378906 -11.894531 4.109375 -10.632812 4.109375 -8.8125 L 4.109375 -6.703125 C 4.109375 -4.867188 4.378906 -3.597656 4.921875 -2.890625 C 5.460938 -2.191406 6.15625 -1.84375 7 -1.84375 C 7.84375 -1.84375 8.535156 -2.191406 9.078125 -2.890625 C 9.628906 -3.597656 9.90625 -4.867188 9.90625 -6.703125 Z M 9.90625 -6.703125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-16">
+<path style="stroke:none;" d="M 7.0625 -1.796875 C 8.019531 -1.796875 8.75 -2.023438 9.25 -2.484375 C 9.75 -2.941406 10 -3.5 10 -4.15625 C 10 -5.0625 9.445312 -5.765625 8.34375 -6.265625 L 5.359375 -7.625 C 3.503906 -8.5 2.578125 -9.75 2.578125 -11.375 C 2.578125 -12.644531 3.03125 -13.691406 3.9375 -14.515625 C 4.851562 -15.347656 6.035156 -15.765625 7.484375 -15.765625 C 9.546875 -15.765625 11.128906 -14.8125 12.234375 -12.90625 L 10.34375 -11.765625 C 9.757812 -13.054688 8.804688 -13.703125 7.484375 -13.703125 C 6.691406 -13.703125 6.0625 -13.5 5.59375 -13.09375 C 5.132812 -12.695312 4.90625 -12.203125 4.90625 -11.609375 C 4.90625 -10.816406 5.421875 -10.175781 6.453125 -9.6875 L 9.265625 -8.375 C 11.296875 -7.425781 12.3125 -5.984375 12.3125 -4.046875 C 12.3125 -2.898438 11.835938 -1.894531 10.890625 -1.03125 C 9.941406 -0.164062 8.65625 0.265625 7.03125 0.265625 C 4.75 0.265625 2.96875 -0.789062 1.6875 -2.90625 L 3.59375 -4.0625 C 4.351562 -2.550781 5.507812 -1.796875 7.0625 -1.796875 Z M 7.0625 -1.796875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-17">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.4375 L 4.3125 -10.4375 L 4.3125 0 L 1.984375 0 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -12.421875 L 9.6875 -12.421875 L 9.6875 -20.859375 L 12.015625 -20.859375 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-18">
+<path style="stroke:none;" d="M 12.421875 -6.65625 C 12.421875 -4.21875 11.953125 -2.453125 11.015625 -1.359375 C 10.085938 -0.273438 9.054688 0.265625 7.921875 0.265625 C 6.328125 0.265625 5.128906 -0.457031 4.328125 -1.90625 L 4.328125 4.828125 L 2 4.828125 L 2 -15.484375 L 4.328125 -15.484375 L 4.328125 -13.640625 C 5.128906 -15.054688 6.328125 -15.765625 7.921875 -15.765625 C 9.054688 -15.765625 10.085938 -15.222656 11.015625 -14.140625 C 11.953125 -13.066406 12.421875 -11.289062 12.421875 -8.8125 Z M 10.015625 -8.8125 C 10.015625 -10.625 9.765625 -11.882812 9.265625 -12.59375 C 8.765625 -13.300781 8.125 -13.65625 7.34375 -13.65625 C 5.8125 -13.65625 4.804688 -12.59375 4.328125 -10.46875 L 4.328125 -5.046875 C 4.796875 -2.910156 5.800781 -1.84375 7.34375 -1.84375 C 8.125 -1.84375 8.765625 -2.203125 9.265625 -2.921875 C 9.765625 -3.640625 10.015625 -4.882812 10.015625 -6.65625 Z M 10.015625 -8.8125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-19">
+<path style="stroke:none;" d="M 12.953125 -3.09375 C 11.648438 -0.851562 9.878906 0.265625 7.640625 0.265625 C 6.128906 0.265625 4.851562 -0.238281 3.8125 -1.25 C 2.769531 -2.269531 2.25 -4.132812 2.25 -6.84375 L 2.25 -14.046875 C 2.25 -16.679688 2.769531 -18.523438 3.8125 -19.578125 C 4.851562 -20.640625 6.117188 -21.171875 7.609375 -21.171875 C 9.953125 -21.171875 11.6875 -20.039062 12.8125 -17.78125 L 10.90625 -16.625 C 10.21875 -18.238281 9.125 -19.046875 7.625 -19.046875 C 6.757812 -19.046875 6.035156 -18.726562 5.453125 -18.09375 C 4.878906 -17.457031 4.59375 -16.109375 4.59375 -14.046875 L 4.59375 -6.84375 C 4.59375 -4.78125 4.878906 -3.429688 5.453125 -2.796875 C 6.035156 -2.160156 6.757812 -1.84375 7.625 -1.84375 C 9.125 -1.84375 10.265625 -2.6875 11.046875 -4.375 Z M 12.953125 -3.09375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-20">
+<path style="stroke:none;" d="M 12.453125 -18.875 L 8.15625 -18.875 L 8.15625 0 L 5.84375 0 L 5.84375 -18.875 L 1.546875 -18.875 L 1.546875 -20.859375 L 12.453125 -20.859375 Z M 12.453125 -18.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-21">
+<path style="stroke:none;" d="M 12.328125 0 L 10.015625 0 L 10.015625 -5.515625 C 10.015625 -9.023438 10.148438 -13.148438 10.421875 -17.890625 L 7.71875 -9.25 L 6.28125 -9.25 L 3.578125 -17.890625 C 3.859375 -13.148438 4 -9.023438 4 -5.515625 L 4 0 L 1.671875 0 L 1.671875 -20.859375 L 4 -20.859375 L 7 -12.203125 L 10.015625 -20.859375 L 12.328125 -20.859375 Z M 12.328125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-22">
+<path style="stroke:none;" d="M 12.53125 -4.875 C 12.53125 -3.457031 12.054688 -2.242188 11.109375 -1.234375 C 10.171875 -0.234375 8.867188 0.265625 7.203125 0.265625 C 4.578125 0.265625 2.660156 -0.878906 1.453125 -3.171875 L 3.171875 -4.46875 C 4.078125 -2.71875 5.414062 -1.84375 7.1875 -1.84375 C 8.007812 -1.84375 8.71875 -2.09375 9.3125 -2.59375 C 9.90625 -3.101562 10.203125 -3.859375 10.203125 -4.859375 C 10.203125 -5.671875 9.691406 -6.554688 8.671875 -7.515625 L 4.1875 -11.703125 C 2.9375 -12.878906 2.3125 -14.359375 2.3125 -16.140625 C 2.3125 -17.554688 2.769531 -18.75 3.6875 -19.71875 C 4.613281 -20.6875 5.878906 -21.171875 7.484375 -21.171875 C 9.566406 -21.171875 11.253906 -20.21875 12.546875 -18.3125 L 10.765625 -16.90625 C 9.992188 -18.332031 8.90625 -19.046875 7.5 -19.046875 C 6.6875 -19.046875 6.007812 -18.8125 5.46875 -18.34375 C 4.925781 -17.882812 4.65625 -17.148438 4.65625 -16.140625 C 4.65625 -15.109375 5.0625 -14.210938 5.875 -13.453125 L 10.34375 -9.296875 C 11.800781 -7.910156 12.53125 -6.4375 12.53125 -4.875 Z M 12.53125 -4.875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-23">
+<path style="stroke:none;" d="M 11.671875 0 L 9.359375 0 L 9.359375 -1.75 C 8.367188 -0.40625 7.125 0.265625 5.625 0.265625 C 4.351562 0.265625 3.328125 -0.164062 2.546875 -1.03125 C 1.765625 -1.90625 1.375 -3 1.375 -4.3125 C 1.375 -5.65625 1.863281 -6.78125 2.84375 -7.6875 C 3.820312 -8.601562 5.253906 -9.0625 7.140625 -9.0625 L 9.359375 -9.0625 L 9.359375 -10.734375 C 9.359375 -12.679688 8.429688 -13.65625 6.578125 -13.65625 C 5.210938 -13.65625 4.191406 -13.054688 3.515625 -11.859375 L 1.75 -13.171875 C 2.96875 -14.898438 4.582031 -15.765625 6.59375 -15.765625 C 8.070312 -15.765625 9.285156 -15.335938 10.234375 -14.484375 C 11.191406 -13.640625 11.671875 -12.390625 11.671875 -10.734375 Z M 9.359375 -3.640625 L 9.359375 -7.1875 L 7.140625 -7.1875 C 6.023438 -7.1875 5.1875 -6.925781 4.625 -6.40625 C 4.0625 -5.894531 3.78125 -5.195312 3.78125 -4.3125 C 3.78125 -2.664062 4.582031 -1.84375 6.1875 -1.84375 C 7.457031 -1.84375 8.515625 -2.441406 9.359375 -3.640625 Z M 9.359375 -3.640625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-24">
+<path style="stroke:none;" d="M 13 -12.859375 L 10.984375 -11.96875 C 10.523438 -13.09375 9.753906 -13.65625 8.671875 -13.65625 C 6.390625 -13.65625 5.25 -11.535156 5.25 -7.296875 L 5.25 0 L 2.921875 0 L 2.921875 -15.484375 L 5.25 -15.484375 L 5.25 -13 C 6.164062 -14.84375 7.425781 -15.765625 9.03125 -15.765625 C 10.894531 -15.765625 12.21875 -14.796875 13 -12.859375 Z M 13 -12.859375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-25">
+<path style="stroke:none;" d="M 12.84375 -2.578125 C 11.65625 -0.679688 9.957031 0.265625 7.75 0.265625 C 6.164062 0.265625 4.863281 -0.296875 3.84375 -1.421875 C 2.820312 -2.554688 2.3125 -4.347656 2.3125 -6.796875 L 2.3125 -8.703125 C 2.3125 -11.140625 2.820312 -12.925781 3.84375 -14.0625 C 4.863281 -15.195312 6.164062 -15.765625 7.75 -15.765625 C 9.957031 -15.765625 11.65625 -14.8125 12.84375 -12.90625 L 10.953125 -11.71875 C 10.179688 -13.007812 9.160156 -13.65625 7.890625 -13.65625 C 6.898438 -13.65625 6.113281 -13.328125 5.53125 -12.671875 C 4.945312 -12.015625 4.65625 -10.691406 4.65625 -8.703125 L 4.65625 -6.796875 C 4.65625 -4.804688 4.945312 -3.484375 5.53125 -2.828125 C 6.113281 -2.171875 6.898438 -1.84375 7.890625 -1.84375 C 9.160156 -1.84375 10.179688 -2.488281 10.953125 -3.78125 Z M 12.84375 -2.578125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-26">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -10.015625 C 9.6875 -11.398438 9.476562 -12.351562 9.0625 -12.875 C 8.644531 -13.394531 8.070312 -13.65625 7.34375 -13.65625 C 5.8125 -13.65625 4.800781 -12.59375 4.3125 -10.46875 L 4.3125 0 L 1.984375 0 L 1.984375 -20.859375 L 4.3125 -20.859375 L 4.3125 -13.625 C 5.300781 -15.050781 6.503906 -15.765625 7.921875 -15.765625 C 9.066406 -15.765625 10.035156 -15.335938 10.828125 -14.484375 C 11.617188 -13.628906 12.015625 -12.179688 12.015625 -10.140625 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-27">
+<path style="stroke:none;" d="M 12.015625 0 L 9.6875 0 L 9.6875 -2.015625 C 8.695312 -0.492188 7.492188 0.265625 6.078125 0.265625 C 4.929688 0.265625 3.960938 -0.164062 3.171875 -1.03125 C 2.378906 -1.90625 1.984375 -3.460938 1.984375 -5.703125 L 1.984375 -15.484375 L 4.3125 -15.484375 L 4.3125 -5.703125 C 4.3125 -4.203125 4.519531 -3.179688 4.9375 -2.640625 C 5.351562 -2.109375 5.925781 -1.84375 6.65625 -1.84375 C 8.1875 -1.84375 9.195312 -2.851562 9.6875 -4.875 L 9.6875 -15.484375 L 12.015625 -15.484375 Z M 12.015625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-28">
+<path style="stroke:none;" d="M 12.28125 -7.296875 L 1.71875 -7.296875 L 1.71875 -9.28125 L 12.28125 -9.28125 Z M 12.28125 -7.296875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-29">
+<path style="stroke:none;" d="M 13.046875 -13.625 L 12.234375 -13.625 L 10.984375 -13.75 C 11.671875 -12.90625 12.015625 -12.046875 12.015625 -11.171875 C 12.015625 -9.972656 11.582031 -8.976562 10.71875 -8.1875 C 9.851562 -7.394531 8.671875 -7 7.171875 -7 C 6.609375 -7 6.097656 -7 5.640625 -7 C 4.898438 -6.519531 4.53125 -6.054688 4.53125 -5.609375 C 4.53125 -5.140625 4.675781 -4.789062 4.96875 -4.5625 C 5.257812 -4.34375 5.738281 -4.234375 6.40625 -4.234375 L 7.703125 -4.234375 C 9.628906 -4.234375 11.023438 -3.820312 11.890625 -3 C 12.753906 -2.1875 13.1875 -1.15625 13.1875 0.09375 C 13.1875 1.5 12.632812 2.660156 11.53125 3.578125 C 10.4375 4.492188 8.988281 4.953125 7.1875 4.953125 C 5.3125 4.953125 3.882812 4.523438 2.90625 3.671875 C 1.9375 2.828125 1.453125 1.800781 1.453125 0.59375 C 1.453125 -0.988281 2.25 -2.1875 3.84375 -3 C 2.664062 -3.488281 2.078125 -4.238281 2.078125 -5.25 C 2.078125 -6.269531 2.703125 -7.132812 3.953125 -7.84375 C 2.671875 -8.707031 2.03125 -9.898438 2.03125 -11.421875 C 2.03125 -12.679688 2.484375 -13.707031 3.390625 -14.5 C 4.296875 -15.300781 5.554688 -15.703125 7.171875 -15.703125 C 7.648438 -15.703125 8.804688 -15.628906 10.640625 -15.484375 L 13.046875 -15.484375 Z M 9.78125 -11.4375 C 9.78125 -12.050781 9.550781 -12.59375 9.09375 -13.0625 C 8.632812 -13.53125 7.953125 -13.765625 7.046875 -13.765625 C 6.109375 -13.765625 5.414062 -13.539062 4.96875 -13.09375 C 4.519531 -12.65625 4.296875 -12.085938 4.296875 -11.390625 C 4.296875 -10.585938 4.53125 -9.972656 5 -9.546875 C 5.476562 -9.128906 6.144531 -8.921875 7 -8.921875 C 7.925781 -8.921875 8.617188 -9.140625 9.078125 -9.578125 C 9.546875 -10.015625 9.78125 -10.632812 9.78125 -11.4375 Z M 10.78125 0.09375 C 10.78125 -0.59375 10.554688 -1.132812 10.109375 -1.53125 C 9.660156 -1.9375 8.859375 -2.140625 7.703125 -2.140625 L 6.765625 -2.140625 C 5.710938 -2.140625 4.960938 -1.898438 4.515625 -1.421875 C 4.066406 -0.941406 3.84375 -0.328125 3.84375 0.421875 C 3.84375 1.203125 4.132812 1.800781 4.71875 2.21875 C 5.300781 2.644531 6.070312 2.859375 7.03125 2.859375 C 9.53125 2.859375 10.78125 1.9375 10.78125 0.09375 Z M 10.78125 0.09375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-30">
+<path style="stroke:none;" d="M 12.421875 -6.671875 C 12.421875 -4.203125 11.976562 -2.425781 11.09375 -1.34375 C 10.207031 -0.269531 9.148438 0.265625 7.921875 0.265625 C 6.328125 0.265625 5.128906 -0.441406 4.328125 -1.859375 L 4.328125 0 L 2 0 L 2 -20.859375 L 4.328125 -20.859375 L 4.328125 -13.609375 C 5.128906 -15.046875 6.328125 -15.765625 7.921875 -15.765625 C 9.148438 -15.765625 10.207031 -15.222656 11.09375 -14.140625 C 11.976562 -13.054688 12.421875 -11.28125 12.421875 -8.8125 Z M 10.015625 -6.671875 L 10.015625 -8.8125 C 10.015625 -10.613281 9.785156 -11.867188 9.328125 -12.578125 C 8.878906 -13.296875 8.21875 -13.65625 7.34375 -13.65625 C 5.800781 -13.65625 4.796875 -12.625 4.328125 -10.5625 L 4.328125 -4.921875 C 4.804688 -2.867188 5.8125 -1.84375 7.34375 -1.84375 C 8.21875 -1.84375 8.878906 -2.195312 9.328125 -2.90625 C 9.785156 -3.625 10.015625 -4.878906 10.015625 -6.671875 Z M 10.015625 -6.671875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-31">
+<path style="stroke:none;" d="M 12.8125 0 L 10.484375 0 C 10.410156 -1.132812 9.851562 -2.460938 8.8125 -3.984375 L 6.5 -7.28125 L 4.765625 -5.1875 L 4.765625 0 L 2.453125 0 L 2.453125 -20.859375 L 4.765625 -20.859375 L 4.765625 -8.046875 L 8.1875 -12.25 C 9.332031 -13.65625 9.914062 -14.734375 9.9375 -15.484375 L 12.265625 -15.484375 L 12.234375 -14.96875 C 12.191406 -14.15625 11.421875 -12.835938 9.921875 -11.015625 L 8.0625 -8.734375 L 10.46875 -5.25 C 11.9375 -3.125 12.703125 -1.554688 12.765625 -0.546875 Z M 12.8125 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-32">
+<path style="stroke:none;" d="M 4.140625 2.5 L 5.921875 -2.125 L 2.703125 -10.015625 C 1.941406 -11.910156 1.53125 -13.332031 1.46875 -14.28125 L 1.390625 -15.484375 L 3.71875 -15.484375 L 3.78125 -14.375 C 3.820312 -13.738281 4.179688 -12.503906 4.859375 -10.671875 L 7 -4.828125 L 9.140625 -10.59375 C 9.804688 -12.457031 10.164062 -13.71875 10.21875 -14.375 L 10.28125 -15.484375 L 12.609375 -15.484375 L 12.53125 -14.28125 C 12.476562 -13.4375 12.066406 -12.015625 11.296875 -10.015625 L 6.359375 2.796875 C 6.179688 3.234375 6.078125 3.742188 6.046875 4.328125 L 6.015625 4.828125 L 3.6875 4.828125 L 3.71875 4.328125 C 3.757812 3.742188 3.898438 3.132812 4.140625 2.5 Z M 4.140625 2.5 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-33">
+<path style="stroke:none;" d="M 12.65625 0 L 10.765625 0 L 10.765625 -11.6875 C 10.765625 -13.007812 10.375 -13.671875 9.59375 -13.671875 C 8.914062 -13.671875 8.363281 -13.160156 7.9375 -12.140625 L 7.9375 0 L 6.0625 0 L 6.0625 -11.6875 C 6.039062 -13.007812 5.726562 -13.671875 5.125 -13.671875 C 4.351562 -13.671875 3.726562 -13.160156 3.25 -12.140625 L 3.25 0 L 1.34375 0 L 1.34375 -15.484375 L 3.25 -15.484375 L 3.25 -13.953125 C 3.945312 -15.160156 4.804688 -15.765625 5.828125 -15.765625 C 6.742188 -15.765625 7.375 -15.160156 7.71875 -13.953125 C 8.375 -15.160156 9.195312 -15.765625 10.1875 -15.765625 C 11.832031 -15.765625 12.65625 -14.441406 12.65625 -11.796875 Z M 12.65625 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-34">
+<path style="stroke:none;" d="M 12.34375 -6.359375 C 12.34375 -4.210938 11.78125 -2.613281 10.65625 -1.5625 C 9.53125 -0.519531 8.039062 0 6.1875 0 L 2.171875 0 L 2.171875 -20.859375 L 6.40625 -20.859375 C 8.195312 -20.859375 9.539062 -20.398438 10.4375 -19.484375 C 11.332031 -18.566406 11.78125 -17.347656 11.78125 -15.828125 C 11.78125 -13.628906 10.835938 -12.175781 8.953125 -11.46875 C 11.210938 -10.78125 12.34375 -9.078125 12.34375 -6.359375 Z M 9.375 -15.828125 C 9.375 -16.953125 9.128906 -17.738281 8.640625 -18.1875 C 8.148438 -18.644531 7.40625 -18.875 6.40625 -18.875 L 4.5 -18.875 L 4.5 -12.203125 L 6.140625 -12.203125 C 7.046875 -12.203125 7.8125 -12.492188 8.4375 -13.078125 C 9.0625 -13.660156 9.375 -14.578125 9.375 -15.828125 Z M 9.9375 -6.359375 C 9.9375 -7.816406 9.671875 -8.828125 9.140625 -9.390625 C 8.609375 -9.953125 7.867188 -10.234375 6.921875 -10.234375 L 4.5 -10.234375 L 4.5 -1.984375 L 6.1875 -1.984375 C 7.519531 -1.984375 8.476562 -2.296875 9.0625 -2.921875 C 9.644531 -3.554688 9.9375 -4.703125 9.9375 -6.359375 Z M 9.9375 -6.359375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-35">
+<path style="stroke:none;" d="M 9.6875 -2.421875 C 9.6875 -1.710938 9.429688 -1.085938 8.921875 -0.546875 C 8.410156 -0.00390625 7.765625 0.265625 6.984375 0.265625 C 6.203125 0.265625 5.5625 -0.00390625 5.0625 -0.546875 C 4.570312 -1.085938 4.328125 -1.710938 4.328125 -2.421875 C 4.328125 -3.117188 4.570312 -3.738281 5.0625 -4.28125 C 5.5625 -4.832031 6.203125 -5.109375 6.984375 -5.109375 C 7.765625 -5.109375 8.410156 -4.832031 8.921875 -4.28125 C 9.429688 -3.726562 9.6875 -3.109375 9.6875 -2.421875 Z M 9.6875 -13.078125 C 9.6875 -12.359375 9.429688 -11.726562 8.921875 -11.1875 C 8.410156 -10.65625 7.765625 -10.390625 6.984375 -10.390625 C 6.203125 -10.390625 5.5625 -10.65625 5.0625 -11.1875 C 4.570312 -11.726562 4.328125 -12.359375 4.328125 -13.078125 C 4.328125 -13.785156 4.570312 -14.410156 5.0625 -14.953125 C 5.5625 -15.492188 6.203125 -15.765625 6.984375 -15.765625 C 7.765625 -15.765625 8.410156 -15.488281 8.921875 -14.9375 C 9.429688 -14.394531 9.6875 -13.773438 9.6875 -13.078125 Z M 9.6875 -13.078125 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-36">
+<path style="stroke:none;" d="M 11.0625 -17.859375 C 10.695312 -18.640625 10.019531 -19.03125 9.03125 -19.03125 C 8.382812 -19.03125 7.878906 -18.820312 7.515625 -18.40625 C 7.160156 -17.988281 6.984375 -17.242188 6.984375 -16.171875 L 6.984375 -14.609375 L 12.203125 -14.609375 L 12.203125 -12.640625 L 6.984375 -12.640625 L 6.984375 0 L 4.65625 0 L 4.65625 -12.640625 L 2.046875 -12.640625 L 2.046875 -14.609375 L 4.65625 -14.609375 L 4.65625 -16.03125 C 4.65625 -17.789062 5.0625 -19.082031 5.875 -19.90625 C 6.695312 -20.738281 7.703125 -21.15625 8.890625 -21.15625 C 10.710938 -21.15625 12.019531 -20.523438 12.8125 -19.265625 Z M 11.0625 -17.859375 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-37">
+<path style="stroke:none;" d="M 8.34375 0 L 5.65625 0 L 2.984375 -8.09375 C 2.054688 -10.863281 1.550781 -12.957031 1.46875 -14.375 L 1.390625 -15.484375 L 3.71875 -15.484375 L 3.78125 -14.375 C 3.863281 -13.03125 4.304688 -11.046875 5.109375 -8.421875 L 7 -2.1875 L 8.890625 -8.421875 C 9.691406 -11.046875 10.132812 -13.03125 10.21875 -14.375 L 10.28125 -15.484375 L 12.609375 -15.484375 L 12.53125 -14.375 C 12.445312 -12.957031 11.941406 -10.863281 11.015625 -8.09375 Z M 8.34375 0 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-38">
+<path style="stroke:none;" d="M 3.5625 -7.390625 C 3.90625 -7.390625 4.191406 -7.1875 4.421875 -6.78125 C 4.984375 -5.882812 5.367188 -5.4375 5.578125 -5.4375 C 5.679688 -5.4375 5.75 -5.503906 5.78125 -5.640625 C 6.664062 -8.816406 7.515625 -11.3125 8.328125 -13.125 C 9.148438 -14.945312 9.847656 -16.269531 10.421875 -17.09375 C 11.078125 -18 11.707031 -18.453125 12.3125 -18.453125 C 12.46875 -18.453125 12.597656 -18.410156 12.703125 -18.328125 C 12.816406 -18.253906 12.875 -18.140625 12.875 -17.984375 C 12.875 -17.796875 12.753906 -17.507812 12.515625 -17.125 C 10.628906 -13.84375 8.894531 -9.082031 7.3125 -2.84375 C 7.1875 -2.425781 6.875 -2.117188 6.375 -1.921875 C 5.875 -1.722656 5.484375 -1.625 5.203125 -1.625 C 4.640625 -1.625 3.957031 -2.203125 3.15625 -3.359375 C 2.351562 -4.515625 1.953125 -5.25 1.953125 -5.5625 C 1.953125 -5.957031 2.148438 -6.359375 2.546875 -6.765625 C 2.953125 -7.179688 3.289062 -7.390625 3.5625 -7.390625 Z M 3.5625 -7.390625 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-39">
+<path style="stroke:none;" d="M -0.265625 -9.421875 L -0.265625 -11.390625 L 14.265625 -11.390625 L 14.265625 -9.421875 Z M -0.265625 -9.421875 "/>
+</symbol>
+<symbol overflow="visible" id="glyph0-40">
+<path style="stroke:none;" d="M 0 -7.125 L 0 -9.109375 L 14 -9.109375 L 14 -7.125 Z M 0 -11.71875 L 0 -13.703125 L 14 -13.703125 L 14 -11.71875 Z M 0 -11.71875 "/>
+</symbol>
+</g>
+</defs>
+<g id="surface10366">
+<rect x="0" y="0" width="1151" height="1054" style="fill:rgb(15.686275%,17.254902%,20.392157%);fill-opacity:1;stroke:none;"/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 17 0 L 577 0 L 577 31 L 17 31 Z M 17 0 "/>
+<g style="fill:rgb(84.705882%,87.058824%,91.372549%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="17" y="26"/>
+ <use xlink:href="#glyph0-2" x="31" y="26"/>
+ <use xlink:href="#glyph0-3" x="45" y="26"/>
+ <use xlink:href="#glyph0-4" x="59" y="26"/>
+ <use xlink:href="#glyph0-5" x="73" y="26"/>
+ <use xlink:href="#glyph0-6" x="87" y="26"/>
+ <use xlink:href="#glyph0-7" x="101" y="26"/>
+ <use xlink:href="#glyph0-1" x="115" y="26"/>
+ <use xlink:href="#glyph0-2" x="129" y="26"/>
+ <use xlink:href="#glyph0-8" x="143" y="26"/>
+ <use xlink:href="#glyph0-9" x="157" y="26"/>
+ <use xlink:href="#glyph0-4" x="171" y="26"/>
+ <use xlink:href="#glyph0-10" x="185" y="26"/>
+ <use xlink:href="#glyph0-7" x="199" y="26"/>
+ <use xlink:href="#glyph0-1" x="213" y="26"/>
+ <use xlink:href="#glyph0-2" x="227" y="26"/>
+ <use xlink:href="#glyph0-11" x="241" y="26"/>
+ <use xlink:href="#glyph0-4" x="255" y="26"/>
+ <use xlink:href="#glyph0-6" x="269" y="26"/>
+ <use xlink:href="#glyph0-12" x="283" y="26"/>
+ <use xlink:href="#glyph0-7" x="297" y="26"/>
+ <use xlink:href="#glyph0-1" x="311" y="26"/>
+ <use xlink:href="#glyph0-2" x="325" y="26"/>
+ <use xlink:href="#glyph0-13" x="339" y="26"/>
+ <use xlink:href="#glyph0-4" x="353" y="26"/>
+ <use xlink:href="#glyph0-14" x="367" y="26"/>
+ <use xlink:href="#glyph0-9" x="381" y="26"/>
+ <use xlink:href="#glyph0-15" x="395" y="26"/>
+ <use xlink:href="#glyph0-12" x="409" y="26"/>
+ <use xlink:href="#glyph0-16" x="423" y="26"/>
+ <use xlink:href="#glyph0-7" x="437" y="26"/>
+ <use xlink:href="#glyph0-1" x="451" y="26"/>
+ <use xlink:href="#glyph0-2" x="465" y="26"/>
+ <use xlink:href="#glyph0-17" x="479" y="26"/>
+ <use xlink:href="#glyph0-6" x="493" y="26"/>
+ <use xlink:href="#glyph0-5" x="507" y="26"/>
+ <use xlink:href="#glyph0-18" x="521" y="26"/>
+ <use xlink:href="#glyph0-7" x="535" y="26"/>
+ <use xlink:href="#glyph0-1" x="549" y="26"/>
+ <use xlink:href="#glyph0-1" x="563" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 577 0 L 815 0 L 815 31 L 577 31 Z M 577 0 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="577" y="26"/>
+ <use xlink:href="#glyph0-15" x="591" y="26"/>
+ <use xlink:href="#glyph0-14" x="605" y="26"/>
+ <use xlink:href="#glyph0-16" x="619" y="26"/>
+ <use xlink:href="#glyph0-15" x="633" y="26"/>
+ <use xlink:href="#glyph0-5" x="647" y="26"/>
+ <use xlink:href="#glyph0-6" x="661" y="26"/>
+ <use xlink:href="#glyph0-1" x="675" y="26"/>
+ <use xlink:href="#glyph0-20" x="689" y="26"/>
+ <use xlink:href="#glyph0-6" x="703" y="26"/>
+ <use xlink:href="#glyph0-16" x="717" y="26"/>
+ <use xlink:href="#glyph0-10" x="731" y="26"/>
+ <use xlink:href="#glyph0-1" x="745" y="26"/>
+ <use xlink:href="#glyph0-21" x="759" y="26"/>
+ <use xlink:href="#glyph0-15" x="773" y="26"/>
+ <use xlink:href="#glyph0-9" x="787" y="26"/>
+ <use xlink:href="#glyph0-6" x="801" y="26"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="578" y="26"/>
+ <use xlink:href="#glyph0-15" x="592" y="26"/>
+ <use xlink:href="#glyph0-14" x="606" y="26"/>
+ <use xlink:href="#glyph0-16" x="620" y="26"/>
+ <use xlink:href="#glyph0-15" x="634" y="26"/>
+ <use xlink:href="#glyph0-5" x="648" y="26"/>
+ <use xlink:href="#glyph0-6" x="662" y="26"/>
+ <use xlink:href="#glyph0-1" x="676" y="26"/>
+ <use xlink:href="#glyph0-20" x="690" y="26"/>
+ <use xlink:href="#glyph0-6" x="704" y="26"/>
+ <use xlink:href="#glyph0-16" x="718" y="26"/>
+ <use xlink:href="#glyph0-10" x="732" y="26"/>
+ <use xlink:href="#glyph0-1" x="746" y="26"/>
+ <use xlink:href="#glyph0-21" x="760" y="26"/>
+ <use xlink:href="#glyph0-15" x="774" y="26"/>
+ <use xlink:href="#glyph0-9" x="788" y="26"/>
+ <use xlink:href="#glyph0-6" x="802" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 815 0 L 843 0 L 843 31 L 815 31 Z M 815 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 843 0 L 857 0 L 857 31 L 843 31 Z M 843 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 857 0 L 1025 0 L 1025 31 L 857 31 Z M 857 0 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-22" x="857" y="26"/>
+ <use xlink:href="#glyph0-6" x="871" y="26"/>
+ <use xlink:href="#glyph0-23" x="885" y="26"/>
+ <use xlink:href="#glyph0-24" x="899" y="26"/>
+ <use xlink:href="#glyph0-25" x="913" y="26"/>
+ <use xlink:href="#glyph0-26" x="927" y="26"/>
+ <use xlink:href="#glyph0-1" x="941" y="26"/>
+ <use xlink:href="#glyph0-21" x="955" y="26"/>
+ <use xlink:href="#glyph0-6" x="969" y="26"/>
+ <use xlink:href="#glyph0-14" x="983" y="26"/>
+ <use xlink:href="#glyph0-27" x="997" y="26"/>
+ <use xlink:href="#glyph0-1" x="1011" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1025 0 L 1109 0 L 1109 31 L 1025 31 Z M 1025 0 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="1025" y="26"/>
+ <use xlink:href="#glyph0-10" x="1039" y="26"/>
+ <use xlink:href="#glyph0-24" x="1053" y="26"/>
+ <use xlink:href="#glyph0-5" x="1067" y="26"/>
+ <use xlink:href="#glyph0-28" x="1081" y="26"/>
+ <use xlink:href="#glyph0-18" x="1095" y="26"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="1026" y="26"/>
+ <use xlink:href="#glyph0-10" x="1040" y="26"/>
+ <use xlink:href="#glyph0-24" x="1054" y="26"/>
+ <use xlink:href="#glyph0-5" x="1068" y="26"/>
+ <use xlink:href="#glyph0-28" x="1082" y="26"/>
+ <use xlink:href="#glyph0-18" x="1096" y="26"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1109 0 L 1123 0 L 1123 31 L 1109 31 Z M 1109 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(21.568627%,24.313725%,29.803922%);fill-opacity:1;" d="M 1123 0 L 1137 0 L 1137 31 L 1123 31 Z M 1123 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 0 L 1151 0 L 1151 31 L 1137 31 Z M 1137 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 0 L 17 0 L 17 31 L 0 31 Z M 0 0 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 31 L 255 31 L 255 62 L 17 62 Z M 17 31 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-3" x="17" y="57"/>
+ <use xlink:href="#glyph0-15" x="31" y="57"/>
+ <use xlink:href="#glyph0-24" x="45" y="57"/>
+ <use xlink:href="#glyph0-6" x="59" y="57"/>
+ <use xlink:href="#glyph0-29" x="73" y="57"/>
+ <use xlink:href="#glyph0-24" x="87" y="57"/>
+ <use xlink:href="#glyph0-15" x="101" y="57"/>
+ <use xlink:href="#glyph0-27" x="115" y="57"/>
+ <use xlink:href="#glyph0-14" x="129" y="57"/>
+ <use xlink:href="#glyph0-9" x="143" y="57"/>
+ <use xlink:href="#glyph0-1" x="157" y="57"/>
+ <use xlink:href="#glyph0-19" x="171" y="57"/>
+ <use xlink:href="#glyph0-15" x="185" y="57"/>
+ <use xlink:href="#glyph0-5" x="199" y="57"/>
+ <use xlink:href="#glyph0-15" x="213" y="57"/>
+ <use xlink:href="#glyph0-24" x="227" y="57"/>
+ <use xlink:href="#glyph0-16" x="241" y="57"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;" d="M 17 61 L 255 61 L 255 62 L 17 62 Z M 17 61 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 255 31 L 1137 31 L 1137 62 L 255 62 Z M 255 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 31 L 1151 31 L 1151 62 L 1137 62 Z M 1137 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 31 L 17 31 L 17 62 L 0 62 Z M 0 31 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 62 L 143 62 L 143 93 L 17 93 Z M 17 62 "/>
+<g style="fill:rgb(10.588235%,13.333333%,16.078431%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="17" y="88"/>
+ <use xlink:href="#glyph0-14" x="31" y="88"/>
+ <use xlink:href="#glyph0-16" x="45" y="88"/>
+ <use xlink:href="#glyph0-4" x="59" y="88"/>
+ <use xlink:href="#glyph0-30" x="73" y="88"/>
+ <use xlink:href="#glyph0-5" x="87" y="88"/>
+ <use xlink:href="#glyph0-23" x="101" y="88"/>
+ <use xlink:href="#glyph0-25" x="115" y="88"/>
+ <use xlink:href="#glyph0-31" x="129" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 143 62 L 241 62 L 241 93 L 143 93 Z M 143 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 241 62 L 339 62 L 339 93 L 241 93 Z M 241 62 "/>
+<g style="fill:rgb(100%,42.352941%,41.960784%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="88"/>
+ <use xlink:href="#glyph0-14" x="255" y="88"/>
+ <use xlink:href="#glyph0-16" x="269" y="88"/>
+ <use xlink:href="#glyph0-4" x="283" y="88"/>
+ <use xlink:href="#glyph0-24" x="297" y="88"/>
+ <use xlink:href="#glyph0-6" x="311" y="88"/>
+ <use xlink:href="#glyph0-9" x="325" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 339 62 L 437 62 L 437 93 L 339 93 Z M 339 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 437 62 L 563 62 L 563 93 L 437 93 Z M 437 62 "/>
+<g style="fill:rgb(59.607843%,74.509804%,39.607843%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="437" y="88"/>
+ <use xlink:href="#glyph0-14" x="451" y="88"/>
+ <use xlink:href="#glyph0-16" x="465" y="88"/>
+ <use xlink:href="#glyph0-4" x="479" y="88"/>
+ <use xlink:href="#glyph0-29" x="493" y="88"/>
+ <use xlink:href="#glyph0-24" x="507" y="88"/>
+ <use xlink:href="#glyph0-6" x="521" y="88"/>
+ <use xlink:href="#glyph0-6" x="535" y="88"/>
+ <use xlink:href="#glyph0-14" x="549" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 563 62 L 661 62 L 661 93 L 563 93 Z M 563 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 661 62 L 801 62 L 801 93 L 661 93 Z M 661 62 "/>
+<g style="fill:rgb(92.54902%,74.509804%,48.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="661" y="88"/>
+ <use xlink:href="#glyph0-14" x="675" y="88"/>
+ <use xlink:href="#glyph0-16" x="689" y="88"/>
+ <use xlink:href="#glyph0-4" x="703" y="88"/>
+ <use xlink:href="#glyph0-32" x="717" y="88"/>
+ <use xlink:href="#glyph0-6" x="731" y="88"/>
+ <use xlink:href="#glyph0-5" x="745" y="88"/>
+ <use xlink:href="#glyph0-5" x="759" y="88"/>
+ <use xlink:href="#glyph0-15" x="773" y="88"/>
+ <use xlink:href="#glyph0-12" x="787" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 801 62 L 899 62 L 899 93 L 801 93 Z M 801 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 899 62 L 1011 62 L 1011 93 L 899 93 Z M 899 62 "/>
+<g style="fill:rgb(31.764706%,68.627451%,93.72549%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="899" y="88"/>
+ <use xlink:href="#glyph0-14" x="913" y="88"/>
+ <use xlink:href="#glyph0-16" x="927" y="88"/>
+ <use xlink:href="#glyph0-4" x="941" y="88"/>
+ <use xlink:href="#glyph0-30" x="955" y="88"/>
+ <use xlink:href="#glyph0-5" x="969" y="88"/>
+ <use xlink:href="#glyph0-27" x="983" y="88"/>
+ <use xlink:href="#glyph0-6" x="997" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 1011 62 L 1109 62 L 1109 93 L 1011 93 Z M 1011 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 1109 62 L 1137 62 L 1137 93 L 1109 93 Z M 1109 62 "/>
+<g style="fill:rgb(77.647059%,47.058824%,86.666667%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="1109" y="88"/>
+ <use xlink:href="#glyph0-14" x="1123" y="88"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 62 L 1151 62 L 1151 93 L 1137 93 Z M 1137 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 62 L 17 62 L 17 93 L 0 93 Z M 0 62 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 93 L 143 93 L 143 124 L 17 124 Z M 17 93 "/>
+<g style="fill:rgb(77.647059%,47.058824%,86.666667%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="17" y="119"/>
+ <use xlink:href="#glyph0-4" x="31" y="119"/>
+ <use xlink:href="#glyph0-33" x="45" y="119"/>
+ <use xlink:href="#glyph0-23" x="59" y="119"/>
+ <use xlink:href="#glyph0-29" x="73" y="119"/>
+ <use xlink:href="#glyph0-6" x="87" y="119"/>
+ <use xlink:href="#glyph0-14" x="101" y="119"/>
+ <use xlink:href="#glyph0-10" x="115" y="119"/>
+ <use xlink:href="#glyph0-23" x="129" y="119"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 143 93 L 241 93 L 241 124 L 143 124 Z M 143 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 241 93 L 353 93 L 353 124 L 241 124 Z M 241 93 "/>
+<g style="fill:rgb(27.45098%,85.098039%,100%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="119"/>
+ <use xlink:href="#glyph0-14" x="255" y="119"/>
+ <use xlink:href="#glyph0-16" x="269" y="119"/>
+ <use xlink:href="#glyph0-4" x="283" y="119"/>
+ <use xlink:href="#glyph0-25" x="297" y="119"/>
+ <use xlink:href="#glyph0-32" x="311" y="119"/>
+ <use xlink:href="#glyph0-23" x="325" y="119"/>
+ <use xlink:href="#glyph0-14" x="339" y="119"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 353 93 L 451 93 L 451 124 L 353 124 Z M 353 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 451 93 L 563 93 L 563 124 L 451 124 Z M 451 93 "/>
+<g style="fill:rgb(87.45098%,87.45098%,87.45098%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="451" y="119"/>
+ <use xlink:href="#glyph0-14" x="465" y="119"/>
+ <use xlink:href="#glyph0-16" x="479" y="119"/>
+ <use xlink:href="#glyph0-4" x="493" y="119"/>
+ <use xlink:href="#glyph0-29" x="507" y="119"/>
+ <use xlink:href="#glyph0-24" x="521" y="119"/>
+ <use xlink:href="#glyph0-23" x="535" y="119"/>
+ <use xlink:href="#glyph0-32" x="549" y="119"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 563 93 L 1137 93 L 1137 124 L 563 124 Z M 563 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 93 L 1151 93 L 1151 124 L 1137 124 Z M 1137 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 93 L 17 93 L 17 124 L 0 124 Z M 0 93 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 124 L 227 124 L 227 155 L 17 155 Z M 17 124 "/>
+<g style="fill:rgb(32.941176%,34.901961%,36.862745%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="17" y="150"/>
+ <use xlink:href="#glyph0-14" x="31" y="150"/>
+ <use xlink:href="#glyph0-16" x="45" y="150"/>
+ <use xlink:href="#glyph0-4" x="59" y="150"/>
+ <use xlink:href="#glyph0-30" x="73" y="150"/>
+ <use xlink:href="#glyph0-24" x="87" y="150"/>
+ <use xlink:href="#glyph0-4" x="101" y="150"/>
+ <use xlink:href="#glyph0-29" x="115" y="150"/>
+ <use xlink:href="#glyph0-26" x="129" y="150"/>
+ <use xlink:href="#glyph0-10" x="143" y="150"/>
+ <use xlink:href="#glyph0-30" x="157" y="150"/>
+ <use xlink:href="#glyph0-5" x="171" y="150"/>
+ <use xlink:href="#glyph0-23" x="185" y="150"/>
+ <use xlink:href="#glyph0-25" x="199" y="150"/>
+ <use xlink:href="#glyph0-31" x="213" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 227 124 L 241 124 L 241 155 L 227 155 Z M 227 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 241 124 L 423 124 L 423 155 L 241 155 Z M 241 124 "/>
+<g style="fill:rgb(100%,56.470588%,56.470588%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="150"/>
+ <use xlink:href="#glyph0-14" x="255" y="150"/>
+ <use xlink:href="#glyph0-16" x="269" y="150"/>
+ <use xlink:href="#glyph0-4" x="283" y="150"/>
+ <use xlink:href="#glyph0-30" x="297" y="150"/>
+ <use xlink:href="#glyph0-24" x="311" y="150"/>
+ <use xlink:href="#glyph0-4" x="325" y="150"/>
+ <use xlink:href="#glyph0-29" x="339" y="150"/>
+ <use xlink:href="#glyph0-26" x="353" y="150"/>
+ <use xlink:href="#glyph0-10" x="367" y="150"/>
+ <use xlink:href="#glyph0-24" x="381" y="150"/>
+ <use xlink:href="#glyph0-6" x="395" y="150"/>
+ <use xlink:href="#glyph0-9" x="409" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 423 124 L 437 124 L 437 155 L 423 155 Z M 423 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 437 124 L 647 124 L 647 155 L 437 155 Z M 437 124 "/>
+<g style="fill:rgb(69.411765%,80.784314%,54.509804%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="437" y="150"/>
+ <use xlink:href="#glyph0-14" x="451" y="150"/>
+ <use xlink:href="#glyph0-16" x="465" y="150"/>
+ <use xlink:href="#glyph0-4" x="479" y="150"/>
+ <use xlink:href="#glyph0-30" x="493" y="150"/>
+ <use xlink:href="#glyph0-24" x="507" y="150"/>
+ <use xlink:href="#glyph0-4" x="521" y="150"/>
+ <use xlink:href="#glyph0-29" x="535" y="150"/>
+ <use xlink:href="#glyph0-26" x="549" y="150"/>
+ <use xlink:href="#glyph0-10" x="563" y="150"/>
+ <use xlink:href="#glyph0-29" x="577" y="150"/>
+ <use xlink:href="#glyph0-24" x="591" y="150"/>
+ <use xlink:href="#glyph0-6" x="605" y="150"/>
+ <use xlink:href="#glyph0-6" x="619" y="150"/>
+ <use xlink:href="#glyph0-14" x="633" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 647 124 L 661 124 L 661 155 L 647 155 Z M 647 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 661 124 L 885 124 L 885 155 L 661 155 Z M 661 124 "/>
+<g style="fill:rgb(94.117647%,80.784314%,61.176471%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="661" y="150"/>
+ <use xlink:href="#glyph0-14" x="675" y="150"/>
+ <use xlink:href="#glyph0-16" x="689" y="150"/>
+ <use xlink:href="#glyph0-4" x="703" y="150"/>
+ <use xlink:href="#glyph0-30" x="717" y="150"/>
+ <use xlink:href="#glyph0-24" x="731" y="150"/>
+ <use xlink:href="#glyph0-4" x="745" y="150"/>
+ <use xlink:href="#glyph0-29" x="759" y="150"/>
+ <use xlink:href="#glyph0-26" x="773" y="150"/>
+ <use xlink:href="#glyph0-10" x="787" y="150"/>
+ <use xlink:href="#glyph0-32" x="801" y="150"/>
+ <use xlink:href="#glyph0-6" x="815" y="150"/>
+ <use xlink:href="#glyph0-5" x="829" y="150"/>
+ <use xlink:href="#glyph0-5" x="843" y="150"/>
+ <use xlink:href="#glyph0-15" x="857" y="150"/>
+ <use xlink:href="#glyph0-12" x="871" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 885 124 L 899 124 L 899 155 L 885 155 Z M 885 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 899 124 L 1095 124 L 1095 155 L 899 155 Z M 899 124 "/>
+<g style="fill:rgb(48.627451%,76.470588%,95.294118%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="899" y="150"/>
+ <use xlink:href="#glyph0-14" x="913" y="150"/>
+ <use xlink:href="#glyph0-16" x="927" y="150"/>
+ <use xlink:href="#glyph0-4" x="941" y="150"/>
+ <use xlink:href="#glyph0-30" x="955" y="150"/>
+ <use xlink:href="#glyph0-24" x="969" y="150"/>
+ <use xlink:href="#glyph0-4" x="983" y="150"/>
+ <use xlink:href="#glyph0-29" x="997" y="150"/>
+ <use xlink:href="#glyph0-26" x="1011" y="150"/>
+ <use xlink:href="#glyph0-10" x="1025" y="150"/>
+ <use xlink:href="#glyph0-30" x="1039" y="150"/>
+ <use xlink:href="#glyph0-5" x="1053" y="150"/>
+ <use xlink:href="#glyph0-27" x="1067" y="150"/>
+ <use xlink:href="#glyph0-6" x="1081" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 1095 124 L 1109 124 L 1109 155 L 1095 155 Z M 1095 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 1109 124 L 1137 124 L 1137 155 L 1109 155 Z M 1109 124 "/>
+<g style="fill:rgb(83.137255%,60%,89.803922%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="1109" y="150"/>
+ <use xlink:href="#glyph0-14" x="1123" y="150"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 124 L 1151 124 L 1151 155 L 1137 155 Z M 1137 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 124 L 17 124 L 17 155 L 0 155 Z M 0 124 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 155 L 227 155 L 227 186 L 17 186 Z M 17 155 "/>
+<g style="fill:rgb(83.137255%,60%,89.803922%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="17" y="181"/>
+ <use xlink:href="#glyph0-4" x="31" y="181"/>
+ <use xlink:href="#glyph0-30" x="45" y="181"/>
+ <use xlink:href="#glyph0-24" x="59" y="181"/>
+ <use xlink:href="#glyph0-4" x="73" y="181"/>
+ <use xlink:href="#glyph0-29" x="87" y="181"/>
+ <use xlink:href="#glyph0-26" x="101" y="181"/>
+ <use xlink:href="#glyph0-10" x="115" y="181"/>
+ <use xlink:href="#glyph0-33" x="129" y="181"/>
+ <use xlink:href="#glyph0-23" x="143" y="181"/>
+ <use xlink:href="#glyph0-29" x="157" y="181"/>
+ <use xlink:href="#glyph0-6" x="171" y="181"/>
+ <use xlink:href="#glyph0-14" x="185" y="181"/>
+ <use xlink:href="#glyph0-10" x="199" y="181"/>
+ <use xlink:href="#glyph0-23" x="213" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 227 155 L 241 155 L 241 186 L 227 186 Z M 227 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 241 155 L 437 155 L 437 186 L 241 186 Z M 241 155 "/>
+<g style="fill:rgb(45.490196%,88.627451%,100%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="181"/>
+ <use xlink:href="#glyph0-14" x="255" y="181"/>
+ <use xlink:href="#glyph0-16" x="269" y="181"/>
+ <use xlink:href="#glyph0-4" x="283" y="181"/>
+ <use xlink:href="#glyph0-30" x="297" y="181"/>
+ <use xlink:href="#glyph0-24" x="311" y="181"/>
+ <use xlink:href="#glyph0-4" x="325" y="181"/>
+ <use xlink:href="#glyph0-29" x="339" y="181"/>
+ <use xlink:href="#glyph0-26" x="353" y="181"/>
+ <use xlink:href="#glyph0-10" x="367" y="181"/>
+ <use xlink:href="#glyph0-25" x="381" y="181"/>
+ <use xlink:href="#glyph0-32" x="395" y="181"/>
+ <use xlink:href="#glyph0-23" x="409" y="181"/>
+ <use xlink:href="#glyph0-14" x="423" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 437 155 L 451 155 L 451 186 L 437 186 Z M 437 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 451 155 L 577 155 L 577 186 L 451 186 Z M 451 155 "/>
+<g style="fill:rgb(90.588235%,90.588235%,90.588235%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="451" y="181"/>
+ <use xlink:href="#glyph0-14" x="465" y="181"/>
+ <use xlink:href="#glyph0-16" x="479" y="181"/>
+ <use xlink:href="#glyph0-4" x="493" y="181"/>
+ <use xlink:href="#glyph0-12" x="507" y="181"/>
+ <use xlink:href="#glyph0-26" x="521" y="181"/>
+ <use xlink:href="#glyph0-4" x="535" y="181"/>
+ <use xlink:href="#glyph0-10" x="549" y="181"/>
+ <use xlink:href="#glyph0-6" x="563" y="181"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 577 155 L 1137 155 L 1137 186 L 577 186 Z M 577 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 155 L 1151 155 L 1151 186 L 1137 186 Z M 1137 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 155 L 17 155 L 17 186 L 0 186 Z M 0 155 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 186 L 255 186 L 255 217 L 17 217 Z M 17 186 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-34" x="17" y="212"/>
+ <use xlink:href="#glyph0-23" x="31" y="212"/>
+ <use xlink:href="#glyph0-25" x="45" y="212"/>
+ <use xlink:href="#glyph0-31" x="59" y="212"/>
+ <use xlink:href="#glyph0-29" x="73" y="212"/>
+ <use xlink:href="#glyph0-24" x="87" y="212"/>
+ <use xlink:href="#glyph0-15" x="101" y="212"/>
+ <use xlink:href="#glyph0-27" x="115" y="212"/>
+ <use xlink:href="#glyph0-14" x="129" y="212"/>
+ <use xlink:href="#glyph0-9" x="143" y="212"/>
+ <use xlink:href="#glyph0-1" x="157" y="212"/>
+ <use xlink:href="#glyph0-19" x="171" y="212"/>
+ <use xlink:href="#glyph0-15" x="185" y="212"/>
+ <use xlink:href="#glyph0-5" x="199" y="212"/>
+ <use xlink:href="#glyph0-15" x="213" y="212"/>
+ <use xlink:href="#glyph0-24" x="227" y="212"/>
+ <use xlink:href="#glyph0-16" x="241" y="212"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;" d="M 17 216 L 255 216 L 255 217 L 17 217 Z M 17 216 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 255 186 L 1137 186 L 1137 217 L 255 217 Z M 255 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 186 L 1151 186 L 1151 217 L 1137 217 Z M 1137 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 186 L 17 186 L 17 217 L 0 217 Z M 0 186 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(10.588235%,13.333333%,16.078431%);fill-opacity:1;" d="M 17 217 L 143 217 L 143 248 L 17 248 Z M 17 217 "/>
+<g style="fill:rgb(90.588235%,90.588235%,90.588235%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="17" y="243"/>
+ <use xlink:href="#glyph0-14" x="31" y="243"/>
+ <use xlink:href="#glyph0-16" x="45" y="243"/>
+ <use xlink:href="#glyph0-4" x="59" y="243"/>
+ <use xlink:href="#glyph0-30" x="73" y="243"/>
+ <use xlink:href="#glyph0-5" x="87" y="243"/>
+ <use xlink:href="#glyph0-23" x="101" y="243"/>
+ <use xlink:href="#glyph0-25" x="115" y="243"/>
+ <use xlink:href="#glyph0-31" x="129" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 143 217 L 241 217 L 241 248 L 143 248 Z M 143 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,42.352941%,41.960784%);fill-opacity:1;" d="M 241 217 L 339 217 L 339 248 L 241 248 Z M 241 217 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="243"/>
+ <use xlink:href="#glyph0-14" x="255" y="243"/>
+ <use xlink:href="#glyph0-16" x="269" y="243"/>
+ <use xlink:href="#glyph0-4" x="283" y="243"/>
+ <use xlink:href="#glyph0-24" x="297" y="243"/>
+ <use xlink:href="#glyph0-6" x="311" y="243"/>
+ <use xlink:href="#glyph0-9" x="325" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 339 217 L 437 217 L 437 248 L 339 248 Z M 339 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(59.607843%,74.509804%,39.607843%);fill-opacity:1;" d="M 437 217 L 563 217 L 563 248 L 437 248 Z M 437 217 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="437" y="243"/>
+ <use xlink:href="#glyph0-14" x="451" y="243"/>
+ <use xlink:href="#glyph0-16" x="465" y="243"/>
+ <use xlink:href="#glyph0-4" x="479" y="243"/>
+ <use xlink:href="#glyph0-29" x="493" y="243"/>
+ <use xlink:href="#glyph0-24" x="507" y="243"/>
+ <use xlink:href="#glyph0-6" x="521" y="243"/>
+ <use xlink:href="#glyph0-6" x="535" y="243"/>
+ <use xlink:href="#glyph0-14" x="549" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 563 217 L 661 217 L 661 248 L 563 248 Z M 563 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.54902%,74.509804%,48.235294%);fill-opacity:1;" d="M 661 217 L 801 217 L 801 248 L 661 248 Z M 661 217 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="661" y="243"/>
+ <use xlink:href="#glyph0-14" x="675" y="243"/>
+ <use xlink:href="#glyph0-16" x="689" y="243"/>
+ <use xlink:href="#glyph0-4" x="703" y="243"/>
+ <use xlink:href="#glyph0-32" x="717" y="243"/>
+ <use xlink:href="#glyph0-6" x="731" y="243"/>
+ <use xlink:href="#glyph0-5" x="745" y="243"/>
+ <use xlink:href="#glyph0-5" x="759" y="243"/>
+ <use xlink:href="#glyph0-15" x="773" y="243"/>
+ <use xlink:href="#glyph0-12" x="787" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 801 217 L 899 217 L 899 248 L 801 248 Z M 801 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(31.764706%,68.627451%,93.72549%);fill-opacity:1;" d="M 899 217 L 1011 217 L 1011 248 L 899 248 Z M 899 217 "/>
+<g style="fill:rgb(90.588235%,90.588235%,90.588235%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="899" y="243"/>
+ <use xlink:href="#glyph0-14" x="913" y="243"/>
+ <use xlink:href="#glyph0-16" x="927" y="243"/>
+ <use xlink:href="#glyph0-4" x="941" y="243"/>
+ <use xlink:href="#glyph0-30" x="955" y="243"/>
+ <use xlink:href="#glyph0-5" x="969" y="243"/>
+ <use xlink:href="#glyph0-27" x="983" y="243"/>
+ <use xlink:href="#glyph0-6" x="997" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 1011 217 L 1109 217 L 1109 248 L 1011 248 Z M 1011 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(77.647059%,47.058824%,86.666667%);fill-opacity:1;" d="M 1109 217 L 1137 217 L 1137 248 L 1109 248 Z M 1109 217 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="1109" y="243"/>
+ <use xlink:href="#glyph0-14" x="1123" y="243"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 217 L 1151 217 L 1151 248 L 1137 248 Z M 1137 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 217 L 17 217 L 17 248 L 0 248 Z M 0 217 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(77.647059%,47.058824%,86.666667%);fill-opacity:1;" d="M 17 248 L 143 248 L 143 279 L 17 279 Z M 17 248 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="17" y="274"/>
+ <use xlink:href="#glyph0-4" x="31" y="274"/>
+ <use xlink:href="#glyph0-33" x="45" y="274"/>
+ <use xlink:href="#glyph0-23" x="59" y="274"/>
+ <use xlink:href="#glyph0-29" x="73" y="274"/>
+ <use xlink:href="#glyph0-6" x="87" y="274"/>
+ <use xlink:href="#glyph0-14" x="101" y="274"/>
+ <use xlink:href="#glyph0-10" x="115" y="274"/>
+ <use xlink:href="#glyph0-23" x="129" y="274"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 143 248 L 241 248 L 241 279 L 143 279 Z M 143 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(27.45098%,85.098039%,100%);fill-opacity:1;" d="M 241 248 L 353 248 L 353 279 L 241 279 Z M 241 248 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="274"/>
+ <use xlink:href="#glyph0-14" x="255" y="274"/>
+ <use xlink:href="#glyph0-16" x="269" y="274"/>
+ <use xlink:href="#glyph0-4" x="283" y="274"/>
+ <use xlink:href="#glyph0-25" x="297" y="274"/>
+ <use xlink:href="#glyph0-32" x="311" y="274"/>
+ <use xlink:href="#glyph0-23" x="325" y="274"/>
+ <use xlink:href="#glyph0-14" x="339" y="274"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 353 248 L 451 248 L 451 279 L 353 279 Z M 353 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(87.45098%,87.45098%,87.45098%);fill-opacity:1;" d="M 451 248 L 563 248 L 563 279 L 451 279 Z M 451 248 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="451" y="274"/>
+ <use xlink:href="#glyph0-14" x="465" y="274"/>
+ <use xlink:href="#glyph0-16" x="479" y="274"/>
+ <use xlink:href="#glyph0-4" x="493" y="274"/>
+ <use xlink:href="#glyph0-29" x="507" y="274"/>
+ <use xlink:href="#glyph0-24" x="521" y="274"/>
+ <use xlink:href="#glyph0-23" x="535" y="274"/>
+ <use xlink:href="#glyph0-32" x="549" y="274"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 563 248 L 1137 248 L 1137 279 L 563 279 Z M 563 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 248 L 1151 248 L 1151 279 L 1137 279 Z M 1137 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 248 L 17 248 L 17 279 L 0 279 Z M 0 248 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(32.941176%,34.901961%,36.862745%);fill-opacity:1;" d="M 17 279 L 227 279 L 227 310 L 17 310 Z M 17 279 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="17" y="305"/>
+ <use xlink:href="#glyph0-14" x="31" y="305"/>
+ <use xlink:href="#glyph0-16" x="45" y="305"/>
+ <use xlink:href="#glyph0-4" x="59" y="305"/>
+ <use xlink:href="#glyph0-30" x="73" y="305"/>
+ <use xlink:href="#glyph0-24" x="87" y="305"/>
+ <use xlink:href="#glyph0-4" x="101" y="305"/>
+ <use xlink:href="#glyph0-29" x="115" y="305"/>
+ <use xlink:href="#glyph0-26" x="129" y="305"/>
+ <use xlink:href="#glyph0-10" x="143" y="305"/>
+ <use xlink:href="#glyph0-30" x="157" y="305"/>
+ <use xlink:href="#glyph0-5" x="171" y="305"/>
+ <use xlink:href="#glyph0-23" x="185" y="305"/>
+ <use xlink:href="#glyph0-25" x="199" y="305"/>
+ <use xlink:href="#glyph0-31" x="213" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 227 279 L 241 279 L 241 310 L 227 310 Z M 227 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,56.470588%,56.470588%);fill-opacity:1;" d="M 241 279 L 423 279 L 423 310 L 241 310 Z M 241 279 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="305"/>
+ <use xlink:href="#glyph0-14" x="255" y="305"/>
+ <use xlink:href="#glyph0-16" x="269" y="305"/>
+ <use xlink:href="#glyph0-4" x="283" y="305"/>
+ <use xlink:href="#glyph0-30" x="297" y="305"/>
+ <use xlink:href="#glyph0-24" x="311" y="305"/>
+ <use xlink:href="#glyph0-4" x="325" y="305"/>
+ <use xlink:href="#glyph0-29" x="339" y="305"/>
+ <use xlink:href="#glyph0-26" x="353" y="305"/>
+ <use xlink:href="#glyph0-10" x="367" y="305"/>
+ <use xlink:href="#glyph0-24" x="381" y="305"/>
+ <use xlink:href="#glyph0-6" x="395" y="305"/>
+ <use xlink:href="#glyph0-9" x="409" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 423 279 L 437 279 L 437 310 L 423 310 Z M 423 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(69.411765%,80.784314%,54.509804%);fill-opacity:1;" d="M 437 279 L 647 279 L 647 310 L 437 310 Z M 437 279 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="437" y="305"/>
+ <use xlink:href="#glyph0-14" x="451" y="305"/>
+ <use xlink:href="#glyph0-16" x="465" y="305"/>
+ <use xlink:href="#glyph0-4" x="479" y="305"/>
+ <use xlink:href="#glyph0-30" x="493" y="305"/>
+ <use xlink:href="#glyph0-24" x="507" y="305"/>
+ <use xlink:href="#glyph0-4" x="521" y="305"/>
+ <use xlink:href="#glyph0-29" x="535" y="305"/>
+ <use xlink:href="#glyph0-26" x="549" y="305"/>
+ <use xlink:href="#glyph0-10" x="563" y="305"/>
+ <use xlink:href="#glyph0-29" x="577" y="305"/>
+ <use xlink:href="#glyph0-24" x="591" y="305"/>
+ <use xlink:href="#glyph0-6" x="605" y="305"/>
+ <use xlink:href="#glyph0-6" x="619" y="305"/>
+ <use xlink:href="#glyph0-14" x="633" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 647 279 L 661 279 L 661 310 L 647 310 Z M 647 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(94.117647%,80.784314%,61.176471%);fill-opacity:1;" d="M 661 279 L 885 279 L 885 310 L 661 310 Z M 661 279 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="661" y="305"/>
+ <use xlink:href="#glyph0-14" x="675" y="305"/>
+ <use xlink:href="#glyph0-16" x="689" y="305"/>
+ <use xlink:href="#glyph0-4" x="703" y="305"/>
+ <use xlink:href="#glyph0-30" x="717" y="305"/>
+ <use xlink:href="#glyph0-24" x="731" y="305"/>
+ <use xlink:href="#glyph0-4" x="745" y="305"/>
+ <use xlink:href="#glyph0-29" x="759" y="305"/>
+ <use xlink:href="#glyph0-26" x="773" y="305"/>
+ <use xlink:href="#glyph0-10" x="787" y="305"/>
+ <use xlink:href="#glyph0-32" x="801" y="305"/>
+ <use xlink:href="#glyph0-6" x="815" y="305"/>
+ <use xlink:href="#glyph0-5" x="829" y="305"/>
+ <use xlink:href="#glyph0-5" x="843" y="305"/>
+ <use xlink:href="#glyph0-15" x="857" y="305"/>
+ <use xlink:href="#glyph0-12" x="871" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 885 279 L 899 279 L 899 310 L 885 310 Z M 885 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(48.627451%,76.470588%,95.294118%);fill-opacity:1;" d="M 899 279 L 1095 279 L 1095 310 L 899 310 Z M 899 279 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="899" y="305"/>
+ <use xlink:href="#glyph0-14" x="913" y="305"/>
+ <use xlink:href="#glyph0-16" x="927" y="305"/>
+ <use xlink:href="#glyph0-4" x="941" y="305"/>
+ <use xlink:href="#glyph0-30" x="955" y="305"/>
+ <use xlink:href="#glyph0-24" x="969" y="305"/>
+ <use xlink:href="#glyph0-4" x="983" y="305"/>
+ <use xlink:href="#glyph0-29" x="997" y="305"/>
+ <use xlink:href="#glyph0-26" x="1011" y="305"/>
+ <use xlink:href="#glyph0-10" x="1025" y="305"/>
+ <use xlink:href="#glyph0-30" x="1039" y="305"/>
+ <use xlink:href="#glyph0-5" x="1053" y="305"/>
+ <use xlink:href="#glyph0-27" x="1067" y="305"/>
+ <use xlink:href="#glyph0-6" x="1081" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 1095 279 L 1109 279 L 1109 310 L 1095 310 Z M 1095 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(83.137255%,60%,89.803922%);fill-opacity:1;" d="M 1109 279 L 1137 279 L 1137 310 L 1109 310 Z M 1109 279 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="1109" y="305"/>
+ <use xlink:href="#glyph0-14" x="1123" y="305"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 279 L 1151 279 L 1151 310 L 1137 310 Z M 1137 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 279 L 17 279 L 17 310 L 0 310 Z M 0 279 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(83.137255%,60%,89.803922%);fill-opacity:1;" d="M 17 310 L 227 310 L 227 341 L 17 341 Z M 17 310 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-16" x="17" y="336"/>
+ <use xlink:href="#glyph0-4" x="31" y="336"/>
+ <use xlink:href="#glyph0-30" x="45" y="336"/>
+ <use xlink:href="#glyph0-24" x="59" y="336"/>
+ <use xlink:href="#glyph0-4" x="73" y="336"/>
+ <use xlink:href="#glyph0-29" x="87" y="336"/>
+ <use xlink:href="#glyph0-26" x="101" y="336"/>
+ <use xlink:href="#glyph0-10" x="115" y="336"/>
+ <use xlink:href="#glyph0-33" x="129" y="336"/>
+ <use xlink:href="#glyph0-23" x="143" y="336"/>
+ <use xlink:href="#glyph0-29" x="157" y="336"/>
+ <use xlink:href="#glyph0-6" x="171" y="336"/>
+ <use xlink:href="#glyph0-14" x="185" y="336"/>
+ <use xlink:href="#glyph0-10" x="199" y="336"/>
+ <use xlink:href="#glyph0-23" x="213" y="336"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 227 310 L 241 310 L 241 341 L 227 341 Z M 227 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(45.490196%,88.627451%,100%);fill-opacity:1;" d="M 241 310 L 437 310 L 437 341 L 241 341 Z M 241 310 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="241" y="336"/>
+ <use xlink:href="#glyph0-14" x="255" y="336"/>
+ <use xlink:href="#glyph0-16" x="269" y="336"/>
+ <use xlink:href="#glyph0-4" x="283" y="336"/>
+ <use xlink:href="#glyph0-30" x="297" y="336"/>
+ <use xlink:href="#glyph0-24" x="311" y="336"/>
+ <use xlink:href="#glyph0-4" x="325" y="336"/>
+ <use xlink:href="#glyph0-29" x="339" y="336"/>
+ <use xlink:href="#glyph0-26" x="353" y="336"/>
+ <use xlink:href="#glyph0-10" x="367" y="336"/>
+ <use xlink:href="#glyph0-25" x="381" y="336"/>
+ <use xlink:href="#glyph0-32" x="395" y="336"/>
+ <use xlink:href="#glyph0-23" x="409" y="336"/>
+ <use xlink:href="#glyph0-14" x="423" y="336"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 437 310 L 451 310 L 451 341 L 437 341 Z M 437 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(90.588235%,90.588235%,90.588235%);fill-opacity:1;" d="M 451 310 L 577 310 L 577 341 L 451 341 Z M 451 310 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-23" x="451" y="336"/>
+ <use xlink:href="#glyph0-14" x="465" y="336"/>
+ <use xlink:href="#glyph0-16" x="479" y="336"/>
+ <use xlink:href="#glyph0-4" x="493" y="336"/>
+ <use xlink:href="#glyph0-12" x="507" y="336"/>
+ <use xlink:href="#glyph0-26" x="521" y="336"/>
+ <use xlink:href="#glyph0-4" x="535" y="336"/>
+ <use xlink:href="#glyph0-10" x="549" y="336"/>
+ <use xlink:href="#glyph0-6" x="563" y="336"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 577 310 L 1137 310 L 1137 341 L 577 341 Z M 577 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 310 L 1151 310 L 1151 341 L 1137 341 Z M 1137 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 310 L 17 310 L 17 341 L 0 341 Z M 0 310 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 341 L 1137 341 L 1137 372 L 17 372 Z M 17 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 341 L 1151 341 L 1151 372 L 1137 372 Z M 1137 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 341 L 17 341 L 17 372 L 0 372 Z M 0 341 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 372 L 451 372 L 451 403 L 17 403 Z M 17 372 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="17" y="398"/>
+ <use xlink:href="#glyph0-27" x="31" y="398"/>
+ <use xlink:href="#glyph0-24" x="45" y="398"/>
+ <use xlink:href="#glyph0-24" x="59" y="398"/>
+ <use xlink:href="#glyph0-6" x="73" y="398"/>
+ <use xlink:href="#glyph0-14" x="87" y="398"/>
+ <use xlink:href="#glyph0-10" x="101" y="398"/>
+ <use xlink:href="#glyph0-1" x="115" y="398"/>
+ <use xlink:href="#glyph0-20" x="129" y="398"/>
+ <use xlink:href="#glyph0-26" x="143" y="398"/>
+ <use xlink:href="#glyph0-6" x="157" y="398"/>
+ <use xlink:href="#glyph0-33" x="171" y="398"/>
+ <use xlink:href="#glyph0-6" x="185" y="398"/>
+ <use xlink:href="#glyph0-1" x="199" y="398"/>
+ <use xlink:href="#glyph0-3" x="213" y="398"/>
+ <use xlink:href="#glyph0-15" x="227" y="398"/>
+ <use xlink:href="#glyph0-24" x="241" y="398"/>
+ <use xlink:href="#glyph0-6" x="255" y="398"/>
+ <use xlink:href="#glyph0-29" x="269" y="398"/>
+ <use xlink:href="#glyph0-24" x="283" y="398"/>
+ <use xlink:href="#glyph0-15" x="297" y="398"/>
+ <use xlink:href="#glyph0-27" x="311" y="398"/>
+ <use xlink:href="#glyph0-14" x="325" y="398"/>
+ <use xlink:href="#glyph0-9" x="339" y="398"/>
+ <use xlink:href="#glyph0-1" x="353" y="398"/>
+ <use xlink:href="#glyph0-19" x="367" y="398"/>
+ <use xlink:href="#glyph0-15" x="381" y="398"/>
+ <use xlink:href="#glyph0-5" x="395" y="398"/>
+ <use xlink:href="#glyph0-15" x="409" y="398"/>
+ <use xlink:href="#glyph0-24" x="423" y="398"/>
+ <use xlink:href="#glyph0-16" x="437" y="398"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;" d="M 17 402 L 451 402 L 451 403 L 17 403 Z M 17 402 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 451 372 L 1137 372 L 1137 403 L 451 403 Z M 451 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 372 L 1151 372 L 1151 403 L 1137 403 Z M 1137 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 372 L 17 372 L 17 403 L 0 403 Z M 0 372 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 403 L 269 403 L 269 434 L 17 434 Z M 17 403 "/>
+<g style="fill:rgb(74.901961%,38.039216%,41.568627%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="429"/>
+ <use xlink:href="#glyph0-5" x="31" y="429"/>
+ <use xlink:href="#glyph0-23" x="45" y="429"/>
+ <use xlink:href="#glyph0-16" x="59" y="429"/>
+ <use xlink:href="#glyph0-16" x="73" y="429"/>
+ <use xlink:href="#glyph0-35" x="87" y="429"/>
+ <use xlink:href="#glyph0-10" x="101" y="429"/>
+ <use xlink:href="#glyph0-26" x="115" y="429"/>
+ <use xlink:href="#glyph0-6" x="129" y="429"/>
+ <use xlink:href="#glyph0-33" x="143" y="429"/>
+ <use xlink:href="#glyph0-6" x="157" y="429"/>
+ <use xlink:href="#glyph0-28" x="171" y="429"/>
+ <use xlink:href="#glyph0-36" x="185" y="429"/>
+ <use xlink:href="#glyph0-29" x="199" y="429"/>
+ <use xlink:href="#glyph0-28" x="213" y="429"/>
+ <use xlink:href="#glyph0-24" x="227" y="429"/>
+ <use xlink:href="#glyph0-6" x="241" y="429"/>
+ <use xlink:href="#glyph0-9" x="255" y="429"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 269 403 L 1137 403 L 1137 434 L 269 434 Z M 269 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 403 L 1151 403 L 1151 434 L 1137 434 Z M 1137 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 403 L 17 403 L 17 434 L 0 434 Z M 0 403 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 434 L 311 434 L 311 465 L 17 465 Z M 17 434 "/>
+<g style="fill:rgb(81.568627%,52.941176%,43.921569%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="460"/>
+ <use xlink:href="#glyph0-5" x="31" y="460"/>
+ <use xlink:href="#glyph0-23" x="45" y="460"/>
+ <use xlink:href="#glyph0-16" x="59" y="460"/>
+ <use xlink:href="#glyph0-16" x="73" y="460"/>
+ <use xlink:href="#glyph0-35" x="87" y="460"/>
+ <use xlink:href="#glyph0-10" x="101" y="460"/>
+ <use xlink:href="#glyph0-26" x="115" y="460"/>
+ <use xlink:href="#glyph0-6" x="129" y="460"/>
+ <use xlink:href="#glyph0-33" x="143" y="460"/>
+ <use xlink:href="#glyph0-6" x="157" y="460"/>
+ <use xlink:href="#glyph0-28" x="171" y="460"/>
+ <use xlink:href="#glyph0-36" x="185" y="460"/>
+ <use xlink:href="#glyph0-29" x="199" y="460"/>
+ <use xlink:href="#glyph0-28" x="213" y="460"/>
+ <use xlink:href="#glyph0-15" x="227" y="460"/>
+ <use xlink:href="#glyph0-24" x="241" y="460"/>
+ <use xlink:href="#glyph0-23" x="255" y="460"/>
+ <use xlink:href="#glyph0-14" x="269" y="460"/>
+ <use xlink:href="#glyph0-29" x="283" y="460"/>
+ <use xlink:href="#glyph0-6" x="297" y="460"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 311 434 L 1137 434 L 1137 465 L 311 465 Z M 311 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 434 L 1151 434 L 1151 465 L 1137 465 Z M 1137 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 434 L 17 434 L 17 465 L 0 465 Z M 0 434 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 465 L 311 465 L 311 496 L 17 496 Z M 17 465 "/>
+<g style="fill:rgb(92.156863%,79.607843%,54.509804%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="491"/>
+ <use xlink:href="#glyph0-5" x="31" y="491"/>
+ <use xlink:href="#glyph0-23" x="45" y="491"/>
+ <use xlink:href="#glyph0-16" x="59" y="491"/>
+ <use xlink:href="#glyph0-16" x="73" y="491"/>
+ <use xlink:href="#glyph0-35" x="87" y="491"/>
+ <use xlink:href="#glyph0-10" x="101" y="491"/>
+ <use xlink:href="#glyph0-26" x="115" y="491"/>
+ <use xlink:href="#glyph0-6" x="129" y="491"/>
+ <use xlink:href="#glyph0-33" x="143" y="491"/>
+ <use xlink:href="#glyph0-6" x="157" y="491"/>
+ <use xlink:href="#glyph0-28" x="171" y="491"/>
+ <use xlink:href="#glyph0-36" x="185" y="491"/>
+ <use xlink:href="#glyph0-29" x="199" y="491"/>
+ <use xlink:href="#glyph0-28" x="213" y="491"/>
+ <use xlink:href="#glyph0-32" x="227" y="491"/>
+ <use xlink:href="#glyph0-6" x="241" y="491"/>
+ <use xlink:href="#glyph0-5" x="255" y="491"/>
+ <use xlink:href="#glyph0-5" x="269" y="491"/>
+ <use xlink:href="#glyph0-15" x="283" y="491"/>
+ <use xlink:href="#glyph0-12" x="297" y="491"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 311 465 L 1137 465 L 1137 496 L 311 496 Z M 311 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 465 L 1151 465 L 1151 496 L 1137 496 Z M 1137 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 465 L 17 465 L 17 496 L 0 496 Z M 0 465 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 496 L 297 496 L 297 527 L 17 527 Z M 17 496 "/>
+<g style="fill:rgb(63.921569%,74.509804%,54.901961%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="522"/>
+ <use xlink:href="#glyph0-5" x="31" y="522"/>
+ <use xlink:href="#glyph0-23" x="45" y="522"/>
+ <use xlink:href="#glyph0-16" x="59" y="522"/>
+ <use xlink:href="#glyph0-16" x="73" y="522"/>
+ <use xlink:href="#glyph0-35" x="87" y="522"/>
+ <use xlink:href="#glyph0-10" x="101" y="522"/>
+ <use xlink:href="#glyph0-26" x="115" y="522"/>
+ <use xlink:href="#glyph0-6" x="129" y="522"/>
+ <use xlink:href="#glyph0-33" x="143" y="522"/>
+ <use xlink:href="#glyph0-6" x="157" y="522"/>
+ <use xlink:href="#glyph0-28" x="171" y="522"/>
+ <use xlink:href="#glyph0-36" x="185" y="522"/>
+ <use xlink:href="#glyph0-29" x="199" y="522"/>
+ <use xlink:href="#glyph0-28" x="213" y="522"/>
+ <use xlink:href="#glyph0-29" x="227" y="522"/>
+ <use xlink:href="#glyph0-24" x="241" y="522"/>
+ <use xlink:href="#glyph0-6" x="255" y="522"/>
+ <use xlink:href="#glyph0-6" x="269" y="522"/>
+ <use xlink:href="#glyph0-14" x="283" y="522"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 297 496 L 1137 496 L 1137 527 L 297 527 Z M 297 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 496 L 1151 496 L 1151 527 L 1137 527 Z M 1137 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 496 L 17 496 L 17 527 L 0 527 Z M 0 496 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 527 L 283 527 L 283 558 L 17 558 Z M 17 527 "/>
+<g style="fill:rgb(53.333333%,75.294118%,81.568627%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="553"/>
+ <use xlink:href="#glyph0-5" x="31" y="553"/>
+ <use xlink:href="#glyph0-23" x="45" y="553"/>
+ <use xlink:href="#glyph0-16" x="59" y="553"/>
+ <use xlink:href="#glyph0-16" x="73" y="553"/>
+ <use xlink:href="#glyph0-35" x="87" y="553"/>
+ <use xlink:href="#glyph0-10" x="101" y="553"/>
+ <use xlink:href="#glyph0-26" x="115" y="553"/>
+ <use xlink:href="#glyph0-6" x="129" y="553"/>
+ <use xlink:href="#glyph0-33" x="143" y="553"/>
+ <use xlink:href="#glyph0-6" x="157" y="553"/>
+ <use xlink:href="#glyph0-28" x="171" y="553"/>
+ <use xlink:href="#glyph0-36" x="185" y="553"/>
+ <use xlink:href="#glyph0-29" x="199" y="553"/>
+ <use xlink:href="#glyph0-28" x="213" y="553"/>
+ <use xlink:href="#glyph0-25" x="227" y="553"/>
+ <use xlink:href="#glyph0-32" x="241" y="553"/>
+ <use xlink:href="#glyph0-23" x="255" y="553"/>
+ <use xlink:href="#glyph0-14" x="269" y="553"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 283 527 L 1137 527 L 1137 558 L 283 558 Z M 283 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 527 L 1151 527 L 1151 558 L 1137 558 Z M 1137 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 527 L 17 527 L 17 558 L 0 558 Z M 0 527 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 558 L 283 558 L 283 589 L 17 589 Z M 17 558 "/>
+<g style="fill:rgb(50.588235%,63.137255%,75.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="584"/>
+ <use xlink:href="#glyph0-5" x="31" y="584"/>
+ <use xlink:href="#glyph0-23" x="45" y="584"/>
+ <use xlink:href="#glyph0-16" x="59" y="584"/>
+ <use xlink:href="#glyph0-16" x="73" y="584"/>
+ <use xlink:href="#glyph0-35" x="87" y="584"/>
+ <use xlink:href="#glyph0-10" x="101" y="584"/>
+ <use xlink:href="#glyph0-26" x="115" y="584"/>
+ <use xlink:href="#glyph0-6" x="129" y="584"/>
+ <use xlink:href="#glyph0-33" x="143" y="584"/>
+ <use xlink:href="#glyph0-6" x="157" y="584"/>
+ <use xlink:href="#glyph0-28" x="171" y="584"/>
+ <use xlink:href="#glyph0-36" x="185" y="584"/>
+ <use xlink:href="#glyph0-29" x="199" y="584"/>
+ <use xlink:href="#glyph0-28" x="213" y="584"/>
+ <use xlink:href="#glyph0-30" x="227" y="584"/>
+ <use xlink:href="#glyph0-5" x="241" y="584"/>
+ <use xlink:href="#glyph0-27" x="255" y="584"/>
+ <use xlink:href="#glyph0-6" x="269" y="584"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 283 558 L 1137 558 L 1137 589 L 283 589 Z M 283 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 558 L 1151 558 L 1151 589 L 1137 589 Z M 1137 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 558 L 17 558 L 17 589 L 0 589 Z M 0 558 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 589 L 311 589 L 311 620 L 17 620 Z M 17 589 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="615"/>
+ <use xlink:href="#glyph0-5" x="31" y="615"/>
+ <use xlink:href="#glyph0-23" x="45" y="615"/>
+ <use xlink:href="#glyph0-16" x="59" y="615"/>
+ <use xlink:href="#glyph0-16" x="73" y="615"/>
+ <use xlink:href="#glyph0-35" x="87" y="615"/>
+ <use xlink:href="#glyph0-10" x="101" y="615"/>
+ <use xlink:href="#glyph0-26" x="115" y="615"/>
+ <use xlink:href="#glyph0-6" x="129" y="615"/>
+ <use xlink:href="#glyph0-33" x="143" y="615"/>
+ <use xlink:href="#glyph0-6" x="157" y="615"/>
+ <use xlink:href="#glyph0-28" x="171" y="615"/>
+ <use xlink:href="#glyph0-36" x="185" y="615"/>
+ <use xlink:href="#glyph0-29" x="199" y="615"/>
+ <use xlink:href="#glyph0-28" x="213" y="615"/>
+ <use xlink:href="#glyph0-18" x="227" y="615"/>
+ <use xlink:href="#glyph0-27" x="241" y="615"/>
+ <use xlink:href="#glyph0-24" x="255" y="615"/>
+ <use xlink:href="#glyph0-18" x="269" y="615"/>
+ <use xlink:href="#glyph0-5" x="283" y="615"/>
+ <use xlink:href="#glyph0-6" x="297" y="615"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 311 589 L 1137 589 L 1137 620 L 311 620 Z M 311 589 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 589 L 1151 589 L 1151 620 L 1137 620 Z M 1137 589 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 589 L 17 589 L 17 620 L 0 620 Z M 0 589 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 620 L 325 620 L 325 651 L 17 651 Z M 17 620 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="646"/>
+ <use xlink:href="#glyph0-5" x="31" y="646"/>
+ <use xlink:href="#glyph0-23" x="45" y="646"/>
+ <use xlink:href="#glyph0-16" x="59" y="646"/>
+ <use xlink:href="#glyph0-16" x="73" y="646"/>
+ <use xlink:href="#glyph0-35" x="87" y="646"/>
+ <use xlink:href="#glyph0-10" x="101" y="646"/>
+ <use xlink:href="#glyph0-26" x="115" y="646"/>
+ <use xlink:href="#glyph0-6" x="129" y="646"/>
+ <use xlink:href="#glyph0-33" x="143" y="646"/>
+ <use xlink:href="#glyph0-6" x="157" y="646"/>
+ <use xlink:href="#glyph0-28" x="171" y="646"/>
+ <use xlink:href="#glyph0-36" x="185" y="646"/>
+ <use xlink:href="#glyph0-29" x="199" y="646"/>
+ <use xlink:href="#glyph0-28" x="213" y="646"/>
+ <use xlink:href="#glyph0-33" x="227" y="646"/>
+ <use xlink:href="#glyph0-23" x="241" y="646"/>
+ <use xlink:href="#glyph0-29" x="255" y="646"/>
+ <use xlink:href="#glyph0-6" x="269" y="646"/>
+ <use xlink:href="#glyph0-14" x="283" y="646"/>
+ <use xlink:href="#glyph0-10" x="297" y="646"/>
+ <use xlink:href="#glyph0-23" x="311" y="646"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 325 620 L 1137 620 L 1137 651 L 325 651 Z M 325 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 620 L 1151 620 L 1151 651 L 1137 651 Z M 1137 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 620 L 17 620 L 17 651 L 0 651 Z M 0 620 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 651 L 1137 651 L 1137 682 L 17 682 Z M 17 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 651 L 1151 651 L 1151 682 L 1137 682 Z M 1137 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 651 L 17 651 L 17 682 L 0 682 Z M 0 651 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 682 L 451 682 L 451 713 L 17 713 Z M 17 682 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-19" x="17" y="708"/>
+ <use xlink:href="#glyph0-27" x="31" y="708"/>
+ <use xlink:href="#glyph0-24" x="45" y="708"/>
+ <use xlink:href="#glyph0-24" x="59" y="708"/>
+ <use xlink:href="#glyph0-6" x="73" y="708"/>
+ <use xlink:href="#glyph0-14" x="87" y="708"/>
+ <use xlink:href="#glyph0-10" x="101" y="708"/>
+ <use xlink:href="#glyph0-1" x="115" y="708"/>
+ <use xlink:href="#glyph0-20" x="129" y="708"/>
+ <use xlink:href="#glyph0-26" x="143" y="708"/>
+ <use xlink:href="#glyph0-6" x="157" y="708"/>
+ <use xlink:href="#glyph0-33" x="171" y="708"/>
+ <use xlink:href="#glyph0-6" x="185" y="708"/>
+ <use xlink:href="#glyph0-1" x="199" y="708"/>
+ <use xlink:href="#glyph0-34" x="213" y="708"/>
+ <use xlink:href="#glyph0-23" x="227" y="708"/>
+ <use xlink:href="#glyph0-25" x="241" y="708"/>
+ <use xlink:href="#glyph0-31" x="255" y="708"/>
+ <use xlink:href="#glyph0-29" x="269" y="708"/>
+ <use xlink:href="#glyph0-24" x="283" y="708"/>
+ <use xlink:href="#glyph0-15" x="297" y="708"/>
+ <use xlink:href="#glyph0-27" x="311" y="708"/>
+ <use xlink:href="#glyph0-14" x="325" y="708"/>
+ <use xlink:href="#glyph0-9" x="339" y="708"/>
+ <use xlink:href="#glyph0-1" x="353" y="708"/>
+ <use xlink:href="#glyph0-19" x="367" y="708"/>
+ <use xlink:href="#glyph0-15" x="381" y="708"/>
+ <use xlink:href="#glyph0-5" x="395" y="708"/>
+ <use xlink:href="#glyph0-15" x="409" y="708"/>
+ <use xlink:href="#glyph0-24" x="423" y="708"/>
+ <use xlink:href="#glyph0-16" x="437" y="708"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;" d="M 17 712 L 451 712 L 451 713 L 17 713 Z M 17 712 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 451 682 L 1137 682 L 1137 713 L 451 713 Z M 451 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 682 L 1151 682 L 1151 713 L 1137 713 Z M 1137 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 682 L 17 682 L 17 713 L 0 713 Z M 0 682 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(74.901961%,38.039216%,41.568627%);fill-opacity:1;" d="M 17 713 L 269 713 L 269 744 L 17 744 Z M 17 713 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="739"/>
+ <use xlink:href="#glyph0-5" x="31" y="739"/>
+ <use xlink:href="#glyph0-23" x="45" y="739"/>
+ <use xlink:href="#glyph0-16" x="59" y="739"/>
+ <use xlink:href="#glyph0-16" x="73" y="739"/>
+ <use xlink:href="#glyph0-35" x="87" y="739"/>
+ <use xlink:href="#glyph0-10" x="101" y="739"/>
+ <use xlink:href="#glyph0-26" x="115" y="739"/>
+ <use xlink:href="#glyph0-6" x="129" y="739"/>
+ <use xlink:href="#glyph0-33" x="143" y="739"/>
+ <use xlink:href="#glyph0-6" x="157" y="739"/>
+ <use xlink:href="#glyph0-28" x="171" y="739"/>
+ <use xlink:href="#glyph0-30" x="185" y="739"/>
+ <use xlink:href="#glyph0-29" x="199" y="739"/>
+ <use xlink:href="#glyph0-28" x="213" y="739"/>
+ <use xlink:href="#glyph0-24" x="227" y="739"/>
+ <use xlink:href="#glyph0-6" x="241" y="739"/>
+ <use xlink:href="#glyph0-9" x="255" y="739"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 269 713 L 1137 713 L 1137 744 L 269 744 Z M 269 713 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 713 L 1151 713 L 1151 744 L 1137 744 Z M 1137 713 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 713 L 17 713 L 17 744 L 0 744 Z M 0 713 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(81.568627%,52.941176%,43.921569%);fill-opacity:1;" d="M 17 744 L 311 744 L 311 775 L 17 775 Z M 17 744 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="770"/>
+ <use xlink:href="#glyph0-5" x="31" y="770"/>
+ <use xlink:href="#glyph0-23" x="45" y="770"/>
+ <use xlink:href="#glyph0-16" x="59" y="770"/>
+ <use xlink:href="#glyph0-16" x="73" y="770"/>
+ <use xlink:href="#glyph0-35" x="87" y="770"/>
+ <use xlink:href="#glyph0-10" x="101" y="770"/>
+ <use xlink:href="#glyph0-26" x="115" y="770"/>
+ <use xlink:href="#glyph0-6" x="129" y="770"/>
+ <use xlink:href="#glyph0-33" x="143" y="770"/>
+ <use xlink:href="#glyph0-6" x="157" y="770"/>
+ <use xlink:href="#glyph0-28" x="171" y="770"/>
+ <use xlink:href="#glyph0-30" x="185" y="770"/>
+ <use xlink:href="#glyph0-29" x="199" y="770"/>
+ <use xlink:href="#glyph0-28" x="213" y="770"/>
+ <use xlink:href="#glyph0-15" x="227" y="770"/>
+ <use xlink:href="#glyph0-24" x="241" y="770"/>
+ <use xlink:href="#glyph0-23" x="255" y="770"/>
+ <use xlink:href="#glyph0-14" x="269" y="770"/>
+ <use xlink:href="#glyph0-29" x="283" y="770"/>
+ <use xlink:href="#glyph0-6" x="297" y="770"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 311 744 L 1137 744 L 1137 775 L 311 775 Z M 311 744 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 744 L 1151 744 L 1151 775 L 1137 775 Z M 1137 744 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 744 L 17 744 L 17 775 L 0 775 Z M 0 744 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(92.156863%,79.607843%,54.509804%);fill-opacity:1;" d="M 17 775 L 311 775 L 311 806 L 17 806 Z M 17 775 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="801"/>
+ <use xlink:href="#glyph0-5" x="31" y="801"/>
+ <use xlink:href="#glyph0-23" x="45" y="801"/>
+ <use xlink:href="#glyph0-16" x="59" y="801"/>
+ <use xlink:href="#glyph0-16" x="73" y="801"/>
+ <use xlink:href="#glyph0-35" x="87" y="801"/>
+ <use xlink:href="#glyph0-10" x="101" y="801"/>
+ <use xlink:href="#glyph0-26" x="115" y="801"/>
+ <use xlink:href="#glyph0-6" x="129" y="801"/>
+ <use xlink:href="#glyph0-33" x="143" y="801"/>
+ <use xlink:href="#glyph0-6" x="157" y="801"/>
+ <use xlink:href="#glyph0-28" x="171" y="801"/>
+ <use xlink:href="#glyph0-30" x="185" y="801"/>
+ <use xlink:href="#glyph0-29" x="199" y="801"/>
+ <use xlink:href="#glyph0-28" x="213" y="801"/>
+ <use xlink:href="#glyph0-32" x="227" y="801"/>
+ <use xlink:href="#glyph0-6" x="241" y="801"/>
+ <use xlink:href="#glyph0-5" x="255" y="801"/>
+ <use xlink:href="#glyph0-5" x="269" y="801"/>
+ <use xlink:href="#glyph0-15" x="283" y="801"/>
+ <use xlink:href="#glyph0-12" x="297" y="801"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 311 775 L 1137 775 L 1137 806 L 311 806 Z M 311 775 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 775 L 1151 775 L 1151 806 L 1137 806 Z M 1137 775 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 775 L 17 775 L 17 806 L 0 806 Z M 0 775 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(63.921569%,74.509804%,54.901961%);fill-opacity:1;" d="M 17 806 L 297 806 L 297 837 L 17 837 Z M 17 806 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="832"/>
+ <use xlink:href="#glyph0-5" x="31" y="832"/>
+ <use xlink:href="#glyph0-23" x="45" y="832"/>
+ <use xlink:href="#glyph0-16" x="59" y="832"/>
+ <use xlink:href="#glyph0-16" x="73" y="832"/>
+ <use xlink:href="#glyph0-35" x="87" y="832"/>
+ <use xlink:href="#glyph0-10" x="101" y="832"/>
+ <use xlink:href="#glyph0-26" x="115" y="832"/>
+ <use xlink:href="#glyph0-6" x="129" y="832"/>
+ <use xlink:href="#glyph0-33" x="143" y="832"/>
+ <use xlink:href="#glyph0-6" x="157" y="832"/>
+ <use xlink:href="#glyph0-28" x="171" y="832"/>
+ <use xlink:href="#glyph0-30" x="185" y="832"/>
+ <use xlink:href="#glyph0-29" x="199" y="832"/>
+ <use xlink:href="#glyph0-28" x="213" y="832"/>
+ <use xlink:href="#glyph0-29" x="227" y="832"/>
+ <use xlink:href="#glyph0-24" x="241" y="832"/>
+ <use xlink:href="#glyph0-6" x="255" y="832"/>
+ <use xlink:href="#glyph0-6" x="269" y="832"/>
+ <use xlink:href="#glyph0-14" x="283" y="832"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 297 806 L 1137 806 L 1137 837 L 297 837 Z M 297 806 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 806 L 1151 806 L 1151 837 L 1137 837 Z M 1137 806 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 806 L 17 806 L 17 837 L 0 837 Z M 0 806 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(53.333333%,75.294118%,81.568627%);fill-opacity:1;" d="M 17 837 L 283 837 L 283 868 L 17 868 Z M 17 837 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="863"/>
+ <use xlink:href="#glyph0-5" x="31" y="863"/>
+ <use xlink:href="#glyph0-23" x="45" y="863"/>
+ <use xlink:href="#glyph0-16" x="59" y="863"/>
+ <use xlink:href="#glyph0-16" x="73" y="863"/>
+ <use xlink:href="#glyph0-35" x="87" y="863"/>
+ <use xlink:href="#glyph0-10" x="101" y="863"/>
+ <use xlink:href="#glyph0-26" x="115" y="863"/>
+ <use xlink:href="#glyph0-6" x="129" y="863"/>
+ <use xlink:href="#glyph0-33" x="143" y="863"/>
+ <use xlink:href="#glyph0-6" x="157" y="863"/>
+ <use xlink:href="#glyph0-28" x="171" y="863"/>
+ <use xlink:href="#glyph0-30" x="185" y="863"/>
+ <use xlink:href="#glyph0-29" x="199" y="863"/>
+ <use xlink:href="#glyph0-28" x="213" y="863"/>
+ <use xlink:href="#glyph0-25" x="227" y="863"/>
+ <use xlink:href="#glyph0-32" x="241" y="863"/>
+ <use xlink:href="#glyph0-23" x="255" y="863"/>
+ <use xlink:href="#glyph0-14" x="269" y="863"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 283 837 L 1137 837 L 1137 868 L 283 868 Z M 283 837 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 837 L 1151 837 L 1151 868 L 1137 868 Z M 1137 837 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 837 L 17 837 L 17 868 L 0 868 Z M 0 837 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(50.588235%,63.137255%,75.686275%);fill-opacity:1;" d="M 17 868 L 283 868 L 283 899 L 17 899 Z M 17 868 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="894"/>
+ <use xlink:href="#glyph0-5" x="31" y="894"/>
+ <use xlink:href="#glyph0-23" x="45" y="894"/>
+ <use xlink:href="#glyph0-16" x="59" y="894"/>
+ <use xlink:href="#glyph0-16" x="73" y="894"/>
+ <use xlink:href="#glyph0-35" x="87" y="894"/>
+ <use xlink:href="#glyph0-10" x="101" y="894"/>
+ <use xlink:href="#glyph0-26" x="115" y="894"/>
+ <use xlink:href="#glyph0-6" x="129" y="894"/>
+ <use xlink:href="#glyph0-33" x="143" y="894"/>
+ <use xlink:href="#glyph0-6" x="157" y="894"/>
+ <use xlink:href="#glyph0-28" x="171" y="894"/>
+ <use xlink:href="#glyph0-30" x="185" y="894"/>
+ <use xlink:href="#glyph0-29" x="199" y="894"/>
+ <use xlink:href="#glyph0-28" x="213" y="894"/>
+ <use xlink:href="#glyph0-30" x="227" y="894"/>
+ <use xlink:href="#glyph0-5" x="241" y="894"/>
+ <use xlink:href="#glyph0-27" x="255" y="894"/>
+ <use xlink:href="#glyph0-6" x="269" y="894"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 283 868 L 1137 868 L 1137 899 L 283 899 Z M 283 868 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 868 L 1151 868 L 1151 899 L 1137 899 Z M 1137 868 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 868 L 17 868 L 17 899 L 0 899 Z M 0 868 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;" d="M 17 899 L 311 899 L 311 930 L 17 930 Z M 17 899 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="925"/>
+ <use xlink:href="#glyph0-5" x="31" y="925"/>
+ <use xlink:href="#glyph0-23" x="45" y="925"/>
+ <use xlink:href="#glyph0-16" x="59" y="925"/>
+ <use xlink:href="#glyph0-16" x="73" y="925"/>
+ <use xlink:href="#glyph0-35" x="87" y="925"/>
+ <use xlink:href="#glyph0-10" x="101" y="925"/>
+ <use xlink:href="#glyph0-26" x="115" y="925"/>
+ <use xlink:href="#glyph0-6" x="129" y="925"/>
+ <use xlink:href="#glyph0-33" x="143" y="925"/>
+ <use xlink:href="#glyph0-6" x="157" y="925"/>
+ <use xlink:href="#glyph0-28" x="171" y="925"/>
+ <use xlink:href="#glyph0-30" x="185" y="925"/>
+ <use xlink:href="#glyph0-29" x="199" y="925"/>
+ <use xlink:href="#glyph0-28" x="213" y="925"/>
+ <use xlink:href="#glyph0-18" x="227" y="925"/>
+ <use xlink:href="#glyph0-27" x="241" y="925"/>
+ <use xlink:href="#glyph0-24" x="255" y="925"/>
+ <use xlink:href="#glyph0-18" x="269" y="925"/>
+ <use xlink:href="#glyph0-5" x="283" y="925"/>
+ <use xlink:href="#glyph0-6" x="297" y="925"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 311 899 L 1137 899 L 1137 930 L 311 930 Z M 311 899 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 899 L 1151 899 L 1151 930 L 1137 930 Z M 1137 899 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 899 L 17 899 L 17 930 L 0 930 Z M 0 899 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;" d="M 17 930 L 325 930 L 325 961 L 17 961 Z M 17 930 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-25" x="17" y="956"/>
+ <use xlink:href="#glyph0-5" x="31" y="956"/>
+ <use xlink:href="#glyph0-23" x="45" y="956"/>
+ <use xlink:href="#glyph0-16" x="59" y="956"/>
+ <use xlink:href="#glyph0-16" x="73" y="956"/>
+ <use xlink:href="#glyph0-35" x="87" y="956"/>
+ <use xlink:href="#glyph0-10" x="101" y="956"/>
+ <use xlink:href="#glyph0-26" x="115" y="956"/>
+ <use xlink:href="#glyph0-6" x="129" y="956"/>
+ <use xlink:href="#glyph0-33" x="143" y="956"/>
+ <use xlink:href="#glyph0-6" x="157" y="956"/>
+ <use xlink:href="#glyph0-28" x="171" y="956"/>
+ <use xlink:href="#glyph0-30" x="185" y="956"/>
+ <use xlink:href="#glyph0-29" x="199" y="956"/>
+ <use xlink:href="#glyph0-28" x="213" y="956"/>
+ <use xlink:href="#glyph0-33" x="227" y="956"/>
+ <use xlink:href="#glyph0-23" x="241" y="956"/>
+ <use xlink:href="#glyph0-29" x="255" y="956"/>
+ <use xlink:href="#glyph0-6" x="269" y="956"/>
+ <use xlink:href="#glyph0-14" x="283" y="956"/>
+ <use xlink:href="#glyph0-10" x="297" y="956"/>
+ <use xlink:href="#glyph0-23" x="311" y="956"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 325 930 L 1137 930 L 1137 961 L 325 961 Z M 325 930 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 930 L 1151 930 L 1151 961 L 1137 961 Z M 1137 930 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 930 L 17 930 L 17 961 L 0 961 Z M 0 930 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(18.039216%,20.392157%,25.098039%);fill-opacity:1;" d="M 17 961 L 1137 961 L 1137 992 L 17 992 Z M 17 961 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 961 L 1151 961 L 1151 992 L 1137 992 Z M 1137 961 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 961 L 17 961 L 17 992 L 0 992 Z M 0 961 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;" d="M 17 992 L 31 992 L 31 1023 L 17 1023 Z M 17 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 31 992 L 129 992 L 129 1023 L 31 1023 Z M 31 992 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="31" y="1018"/>
+ <use xlink:href="#glyph0-19" x="45" y="1018"/>
+ <use xlink:href="#glyph0-5" x="59" y="1018"/>
+ <use xlink:href="#glyph0-15" x="73" y="1018"/>
+ <use xlink:href="#glyph0-25" x="87" y="1018"/>
+ <use xlink:href="#glyph0-31" x="101" y="1018"/>
+ <use xlink:href="#glyph0-1" x="115" y="1018"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-1" x="32" y="1018"/>
+ <use xlink:href="#glyph0-19" x="46" y="1018"/>
+ <use xlink:href="#glyph0-5" x="60" y="1018"/>
+ <use xlink:href="#glyph0-15" x="74" y="1018"/>
+ <use xlink:href="#glyph0-25" x="88" y="1018"/>
+ <use xlink:href="#glyph0-31" x="102" y="1018"/>
+ <use xlink:href="#glyph0-1" x="116" y="1018"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 129 992 L 157 992 L 157 1023 L 129 1023 Z M 129 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 157 992 L 171 992 L 171 1023 L 157 1023 Z M 157 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 171 992 L 311 992 L 311 1023 L 171 1023 Z M 171 992 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-11" x="171" y="1018"/>
+ <use xlink:href="#glyph0-4" x="185" y="1018"/>
+ <use xlink:href="#glyph0-6" x="199" y="1018"/>
+ <use xlink:href="#glyph0-12" x="213" y="1018"/>
+ <use xlink:href="#glyph0-1" x="227" y="1018"/>
+ <use xlink:href="#glyph0-21" x="241" y="1018"/>
+ <use xlink:href="#glyph0-15" x="255" y="1018"/>
+ <use xlink:href="#glyph0-9" x="269" y="1018"/>
+ <use xlink:href="#glyph0-6" x="283" y="1018"/>
+ <use xlink:href="#glyph0-1" x="297" y="1018"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 311 992 L 325 992 L 325 1023 L 311 1023 Z M 311 992 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-37" x="311" y="1018"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-37" x="312" y="1018"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 325 992 L 339 992 L 339 1023 L 325 1023 Z M 325 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 339 992 L 367 992 L 367 1023 L 339 1023 Z M 339 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 367 992 L 381 992 L 381 1023 L 367 1023 Z M 367 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 381 992 L 437 992 L 437 1023 L 381 1023 Z M 381 992 "/>
+<g style="fill:rgb(92.54902%,93.72549%,95.686275%);fill-opacity:1;">
+ <use xlink:href="#glyph0-2" x="381" y="1018"/>
+ <use xlink:href="#glyph0-38" x="395" y="1018"/>
+ <use xlink:href="#glyph0-7" x="409" y="1018"/>
+ <use xlink:href="#glyph0-1" x="423" y="1018"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 437 992 L 507 992 L 507 1023 L 437 1023 Z M 437 992 "/>
+<g style="fill:rgb(89.803922%,91.372549%,94.117647%);fill-opacity:1;">
+ <use xlink:href="#glyph0-13" x="437" y="1018"/>
+ <use xlink:href="#glyph0-24" x="451" y="1018"/>
+ <use xlink:href="#glyph0-23" x="465" y="1018"/>
+ <use xlink:href="#glyph0-18" x="479" y="1018"/>
+ <use xlink:href="#glyph0-1" x="493" y="1018"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 507 992 L 521 992 L 521 1023 L 507 1023 Z M 507 992 "/>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-12" x="507" y="1018"/>
+</g>
+<g style="fill:rgb(66.27451%,63.137255%,88.235294%);fill-opacity:1;">
+ <use xlink:href="#glyph0-12" x="508" y="1018"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(29.803922%,33.72549%,41.568627%);fill-opacity:1;" d="M 521 992 L 535 992 L 535 1023 L 521 1023 Z M 521 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 535 992 L 1081 992 L 1081 1023 L 535 1023 Z M 535 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 1137 992 L 1151 992 L 1151 1023 L 1137 1023 Z M 1137 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(26.27451%,29.803922%,36.862745%);fill-opacity:1;" d="M 1081 992 L 1137 992 L 1137 1023 L 1081 1023 Z M 1081 992 "/>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-39" x="1081" y="1018"/>
+ <use xlink:href="#glyph0-40" x="1095" y="1018"/>
+ <use xlink:href="#glyph0-40" x="1109" y="1018"/>
+ <use xlink:href="#glyph0-39" x="1123" y="1018"/>
+</g>
+<g style="fill:rgb(70.588235%,55.686275%,67.843137%);fill-opacity:1;">
+ <use xlink:href="#glyph0-39" x="1082" y="1018"/>
+ <use xlink:href="#glyph0-40" x="1096" y="1018"/>
+ <use xlink:href="#glyph0-40" x="1110" y="1018"/>
+ <use xlink:href="#glyph0-39" x="1124" y="1018"/>
+</g>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 992 L 17 992 L 17 1023 L 0 1023 Z M 0 992 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 17 1023 L 45 1023 L 45 1054 L 17 1054 Z M 17 1023 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 45 1023 L 1152 1023 L 1152 1054 L 45 1054 Z M 45 1023 "/>
+<path style=" stroke:none;fill-rule:nonzero;fill:rgb(12.941176%,14.117647%,16.862745%);fill-opacity:1;" d="M 0 1023 L 17 1023 L 17 1054 L 0 1054 Z M 0 1023 "/>
+</g>
+</svg>
diff --git a/pw_console/plugins.rst b/pw_console/plugins.rst
index b1ad999c3..e4e5cbaae 100644
--- a/pw_console/plugins.rst
+++ b/pw_console/plugins.rst
@@ -19,7 +19,8 @@ Creating new plugins has a few high level steps:
background tasks.
2. Enable the plugin before pw_console startup by calling ``add_window_plugin``,
- ``add_top_toolbar`` or ``add_bottom_toolbar``. See the
+ ``add_floating_window_plugin``, ``add_top_toolbar`` or
+ ``add_bottom_toolbar``. See the
:ref:`module-pw_console-embedding-plugins` section of the
:ref:`module-pw_console-embedding` for an example.
@@ -89,7 +90,7 @@ of the window.
Both input and output fields are prompt_toolkit `TextArea`_ objects which can
have their own options like syntax highlighting.
-.. figure:: images/calculator_plugin.png
+.. figure:: images/calculator_plugin.svg
:alt: Screenshot of the CalcPane plugin showing some math calculations.
Screenshot of the ``CalcPane`` plugin showing some math calculations.
@@ -102,7 +103,7 @@ Clock
The ClockPane is another WindowPane based plugin that displays a clock and some
formatted text examples. It inherits from both WindowPane and PluginMixin.
-.. figure:: images/clock_plugin1.png
+.. figure:: images/clock_plugin1.svg
:alt: ClockPane plugin screenshot showing the clock text.
``ClockPane`` plugin screenshot showing the clock text.
@@ -113,7 +114,7 @@ triggers UI re-draws. There are also two toolbar buttons to toggle view mode
:kbd:`v` key or mouse clicking on the :guilabel:`View Mode` button will toggle
the view to show some formatted text samples:
-.. figure:: images/clock_plugin2.png
+.. figure:: images/clock_plugin2.svg
:alt: ClockPane plugin screenshot showing formatted text examples.
``ClockPane`` plugin screenshot showing formatted text examples.
@@ -121,6 +122,32 @@ the view to show some formatted text samples:
Like the CalcPane example the code is heavily commented to guide plugin authors
through developmenp. See the :ref:`clock_pane_code` below for the full source.
+2048 Game
+=========
+This is a plugin that demonstrates more complex user interaction by playing a
+game of 2048.
+
+Similar to the ``ClockPane`` the ``Twenty48Pane`` class inherits from
+``PluginMixin`` to manage background tasks. With a few differences:
+
+- Uses ``FloatingWindowPane`` to create a floating window instead of a
+ standard tiled window.
+- Implements the ``get_top_level_menus`` function to create a new ``[2048]``
+ menu in Pigweed Console's own main menu bar.
+- Adds custom game keybindings which are set within the ``Twenty48Control``
+ class. That is the prompt_toolkit ``FormattedTextControl`` widget which
+ receives keyboard input when the game is in focus.
+
+The ``Twenty48Game`` class is separate from the user interface and handles
+managing the game state as well as printing the game board. The
+``Twenty48Game.__pt_formatted_text__()`` function is responsible for drawing the
+game board using prompt_toolkit style and text tuples.
+
+.. figure:: images/2048_plugin1.svg
+ :alt: Twenty48Pane plugin screenshot showing the game board.
+
+ ``Twenty48Pane`` plugin screenshot showing the game board.
+
--------
Appendix
--------
@@ -140,7 +167,16 @@ Code Listing: ``clock_pane.py``
:language: python
:linenos:
-.. _WindowPane: https://cs.opensource.google/pigweed/pigweed/+/main:pw_console/py/pw_console/widgets/window_pane.py
-.. _WindowPaneToolbar: https://cs.opensource.google/pigweed/pigweed/+/main:pw_console/py/pw_console/widgets/window_pane_toolbar.py
+.. _twenty48_pane_code:
+
+Code Listing: ``twenty48_pane.py``
+==================================
+.. literalinclude:: ./py/pw_console/plugins/twenty48_pane.py
+ :language: python
+ :linenos:
+
+
+.. _WindowPane: https://cs.pigweed.dev/pigweed/+/main:pw_console/py/pw_console/widgets/window_pane.py
+.. _WindowPaneToolbar: https://cs.pigweed.dev/pigweed/+/main:pw_console/py/pw_console/widgets/window_pane_toolbar.py
.. _calculator.py example: https://github.com/prompt-toolkit/python-prompt-toolkit/blob/3.0.23/examples/full-screen/calculator.py
.. _TextArea: https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.widgets.TextArea
diff --git a/pw_console/py/BUILD.bazel b/pw_console/py/BUILD.bazel
new file mode 100644
index 000000000..a10a1c437
--- /dev/null
+++ b/pw_console/py/BUILD.bazel
@@ -0,0 +1,236 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test")
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "event_count_history",
+ srcs = ["pw_console/widgets/event_count_history.py"],
+)
+
+py_library(
+ name = "pyserial_wrapper",
+ srcs = ["pw_console/pyserial_wrapper.py"],
+ deps = [
+ ":event_count_history",
+ ],
+)
+
+py_library(
+ name = "pw_console",
+ srcs = [
+ "pw_console/__init__.py",
+ "pw_console/__main__.py",
+ "pw_console/command_runner.py",
+ "pw_console/console_app.py",
+ "pw_console/console_log_server.py",
+ "pw_console/console_prefs.py",
+ "pw_console/docs/__init__.py",
+ "pw_console/embed.py",
+ "pw_console/filter_toolbar.py",
+ "pw_console/get_pw_console_app.py",
+ "pw_console/help_window.py",
+ "pw_console/html/__init__.py",
+ "pw_console/key_bindings.py",
+ "pw_console/log_filter.py",
+ "pw_console/log_line.py",
+ "pw_console/log_pane.py",
+ "pw_console/log_pane_saveas_dialog.py",
+ "pw_console/log_pane_selection_dialog.py",
+ "pw_console/log_pane_toolbars.py",
+ "pw_console/log_screen.py",
+ "pw_console/log_store.py",
+ "pw_console/log_view.py",
+ "pw_console/mouse.py",
+ "pw_console/pigweed_code_style.py",
+ "pw_console/plugin_mixin.py",
+ "pw_console/plugins/__init__.py",
+ "pw_console/plugins/bandwidth_toolbar.py",
+ "pw_console/plugins/calc_pane.py",
+ "pw_console/plugins/clock_pane.py",
+ "pw_console/plugins/twenty48_pane.py",
+ "pw_console/progress_bar/__init__.py",
+ "pw_console/progress_bar/progress_bar_impl.py",
+ "pw_console/progress_bar/progress_bar_state.py",
+ "pw_console/progress_bar/progress_bar_task_counter.py",
+ "pw_console/pw_ptpython_repl.py",
+ "pw_console/python_logging.py",
+ "pw_console/quit_dialog.py",
+ "pw_console/repl_pane.py",
+ "pw_console/search_toolbar.py",
+ "pw_console/style.py",
+ "pw_console/templates/__init__.py",
+ "pw_console/test_mode.py",
+ "pw_console/text_formatting.py",
+ "pw_console/widgets/__init__.py",
+ "pw_console/widgets/border.py",
+ "pw_console/widgets/checkbox.py",
+ "pw_console/widgets/mouse_handlers.py",
+ "pw_console/widgets/table.py",
+ "pw_console/widgets/window_pane.py",
+ "pw_console/widgets/window_pane_toolbar.py",
+ "pw_console/window_list.py",
+ "pw_console/window_manager.py",
+ ],
+ data = [
+ "pw_console/docs/user_guide.rst",
+ "pw_console/html/index.html",
+ "pw_console/html/main.js",
+ "pw_console/html/style.css",
+ "pw_console/py.typed",
+ "pw_console/templates/keybind_list.jinja",
+ "pw_console/templates/repl_output.jinja",
+ ],
+ imports = ["."],
+ deps = [
+ ":event_count_history",
+ ":pyserial_wrapper",
+ "//pw_cli/py:pw_cli",
+ "//pw_log_tokenized/py:pw_log_tokenized",
+ ],
+)
+
+py_binary(
+ name = "pw_console_test_mode",
+ srcs = [
+ "pw_console/__main__.py",
+ ],
+ main = "pw_console/__main__.py",
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "command_runner_test",
+ size = "small",
+ srcs = [
+ "command_runner_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "console_app_test",
+ size = "small",
+ srcs = [
+ "console_app_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "console_prefs_test",
+ size = "small",
+ srcs = [
+ "console_prefs_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "help_window_test",
+ size = "small",
+ srcs = [
+ "help_window_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "log_filter_test",
+ size = "small",
+ srcs = [
+ "log_filter_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "log_store_test",
+ size = "small",
+ srcs = [
+ "log_store_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "log_view_test",
+ size = "small",
+ srcs = [
+ "log_view_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "repl_pane_test",
+ size = "small",
+ srcs = [
+ "repl_pane_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "table_test",
+ size = "small",
+ srcs = [
+ "table_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "text_formatting_test",
+ size = "small",
+ srcs = [
+ "text_formatting_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
+
+py_test(
+ name = "window_manager_test",
+ size = "small",
+ srcs = [
+ "window_manager_test.py",
+ ],
+ deps = [
+ ":pw_console",
+ ],
+)
diff --git a/pw_console/py/BUILD.gn b/pw_console/py/BUILD.gn
index b7164c95b..9e053e2e7 100644
--- a/pw_console/py/BUILD.gn
+++ b/pw_console/py/BUILD.gn
@@ -27,11 +27,14 @@ pw_python_package("py") {
"pw_console/__main__.py",
"pw_console/command_runner.py",
"pw_console/console_app.py",
+ "pw_console/console_log_server.py",
"pw_console/console_prefs.py",
+ "pw_console/docs/__init__.py",
"pw_console/embed.py",
"pw_console/filter_toolbar.py",
"pw_console/get_pw_console_app.py",
"pw_console/help_window.py",
+ "pw_console/html/__init__.py",
"pw_console/key_bindings.py",
"pw_console/log_filter.py",
"pw_console/log_line.py",
@@ -49,6 +52,7 @@ pw_python_package("py") {
"pw_console/plugins/bandwidth_toolbar.py",
"pw_console/plugins/calc_pane.py",
"pw_console/plugins/clock_pane.py",
+ "pw_console/plugins/twenty48_pane.py",
"pw_console/progress_bar/__init__.py",
"pw_console/progress_bar/progress_bar_impl.py",
"pw_console/progress_bar/progress_bar_state.py",
@@ -60,6 +64,8 @@ pw_python_package("py") {
"pw_console/repl_pane.py",
"pw_console/search_toolbar.py",
"pw_console/style.py",
+ "pw_console/templates/__init__.py",
+ "pw_console/test_mode.py",
"pw_console/text_formatting.py",
"pw_console/widgets/__init__.py",
"pw_console/widgets/border.py",
@@ -71,7 +77,6 @@ pw_python_package("py") {
"pw_console/widgets/window_pane_toolbar.py",
"pw_console/window_list.py",
"pw_console/window_manager.py",
- "pw_console/yaml_config_loader_mixin.py",
]
tests = [
"command_runner_test.py",
@@ -89,12 +94,16 @@ pw_python_package("py") {
python_deps = [
"$dir_pw_cli/py",
"$dir_pw_log_tokenized/py",
- "$dir_pw_tokenizer/py",
]
inputs = [
+ "pw_console/docs/user_guide.rst",
"pw_console/templates/keybind_list.jinja",
"pw_console/templates/repl_output.jinja",
+ "pw_console/html/index.html",
+ "pw_console/html/main.js",
+ "pw_console/html/style.css",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_console/py/command_runner_test.py b/pw_console/py/command_runner_test.py
index 45362fe90..822b46488 100644
--- a/pw_console/py/command_runner_test.py
+++ b/pw_console/py/command_runner_test.py
@@ -22,6 +22,7 @@ from unittest.mock import MagicMock
from prompt_toolkit.application import create_app_session
from prompt_toolkit.output import ColorDepth
+
# inclusive-language: ignore
from prompt_toolkit.output import DummyOutput as FakeOutput
@@ -31,14 +32,15 @@ from pw_console.text_formatting import (
flatten_formatted_text_tuples,
join_adjacent_style_tuples,
)
-from window_manager_test import target_list_and_pane, window_pane_titles
def _create_console_app(log_pane_count=2):
- console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT,
- prefs=ConsolePrefs(project_file=False,
- project_user_file=False,
- user_file=False))
+ prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
+ prefs.set_code_theme('default')
+ console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT, prefs=prefs)
+
console_app.prefs.reset_config()
# Setup log panes
@@ -53,8 +55,33 @@ def _create_console_app(log_pane_count=2):
return console_app
+def window_pane_titles(window_manager):
+ return [
+ [
+ pane.pane_title() + ' - ' + pane.pane_subtitle()
+ for pane in window_list.active_panes
+ ]
+ for window_list in window_manager.window_lists
+ ]
+
+
+def target_list_and_pane(window_manager, list_index, pane_index):
+ # pylint: disable=protected-access
+ # Bypass prompt_toolkit has_focus()
+ pane = window_manager.window_lists[list_index].active_panes[pane_index]
+ # If the pane is in focus it will be visible.
+ pane.show_pane = True
+ window_manager._get_active_window_list_and_pane = MagicMock( # type: ignore
+ return_value=(
+ window_manager.window_lists[list_index],
+ window_manager.window_lists[list_index].active_panes[pane_index],
+ )
+ )
+
+
class TestCommandRunner(unittest.TestCase):
"""Tests for CommandRunner."""
+
def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
@@ -62,21 +89,26 @@ class TestCommandRunner(unittest.TestCase):
with create_app_session(output=FakeOutput()):
console_app = _create_console_app(log_pane_count=2)
flattened_menu_items = [
- text for text, handler in
- console_app.command_runner.load_menu_items()
+ text
+ # pylint: disable=line-too-long
+ for text, handler in console_app.command_runner.load_menu_items()
+ # pylint: enable=line-too-long
]
# Check some common menu items exist.
self.assertIn('[File] > Open Logger', flattened_menu_items)
- self.assertIn('[File] > Themes > UI Themes > High Contrast',
- flattened_menu_items)
+ self.assertIn(
+ '[File] > Themes > UI Themes > High Contrast',
+ flattened_menu_items,
+ )
self.assertIn('[Help] > User Guide', flattened_menu_items)
self.assertIn('[Help] > Keyboard Shortcuts', flattened_menu_items)
# Check for log windows
self.assertRegex(
'\n'.join(flattened_menu_items),
- re.compile(r'^\[Windows\] > .* LogPane-[0-9]+ > .*$',
- re.MULTILINE),
+ re.compile(
+ r'^\[Windows\] > .* LogPane-[0-9]+ > .*$', re.MULTILINE
+ ),
)
def test_filter_and_highlight_matches(self) -> None:
@@ -86,7 +118,8 @@ class TestCommandRunner(unittest.TestCase):
command_runner = console_app.command_runner
command_runner.filter_completions = MagicMock(
- wraps=command_runner.filter_completions)
+ wraps=command_runner.filter_completions
+ )
command_runner.width = 20
# Define custom completion items
@@ -102,8 +135,10 @@ class TestCommandRunner(unittest.TestCase):
]
command_runner.filter_completions.assert_not_called()
- command_runner.set_completions(window_title='Test Completions',
- load_completions=get_completions)
+ command_runner.set_completions(
+ window_title='Test Completions',
+ load_completions=get_completions,
+ )
command_runner.filter_completions.assert_called_once()
command_runner.filter_completions.reset_mock()
@@ -112,7 +147,9 @@ class TestCommandRunner(unittest.TestCase):
# Flatten resulting formatted text
result_items = join_adjacent_style_tuples(
flatten_formatted_text_tuples(
- command_runner.completion_fragments))
+ command_runner.completion_fragments
+ )
+ )
# index 0: the selected line
# index 1: the rest of the completions with line breaks
@@ -124,30 +161,40 @@ class TestCommandRunner(unittest.TestCase):
self.assertEqual(len(first_item_text.splitlines()), 1)
self.assertEqual(len(second_item_text.splitlines()), 3)
# First line is highlighted as a selected item
- self.assertEqual(first_item_style,
- 'class:command-runner-selected-item')
+ self.assertEqual(
+ first_item_style, 'class:command-runner-selected-item'
+ )
self.assertIn('[File] > Open Logger', first_item_text)
# Type: file open
command_runner.input_field.buffer.text = 'file open'
- self.assertEqual(command_runner.input_field.buffer.text,
- 'file open')
+ self.assertEqual(
+ command_runner.input_field.buffer.text, 'file open'
+ )
# Run the filter
command_runner.filter_completions()
# Flatten resulting formatted text
result_items = join_adjacent_style_tuples(
flatten_formatted_text_tuples(
- command_runner.completion_fragments))
+ command_runner.completion_fragments
+ )
+ )
# Check file and open are highlighted
self.assertEqual(
result_items[:4],
[
('class:command-runner-selected-item', '['),
- ('class:command-runner-selected-item '
- 'class:command-runner-fuzzy-highlight-0 ', 'File'),
+ (
+ 'class:command-runner-selected-item '
+ 'class:command-runner-fuzzy-highlight-0 ',
+ 'File',
+ ),
('class:command-runner-selected-item', '] > '),
- ('class:command-runner-selected-item '
- 'class:command-runner-fuzzy-highlight-1 ', 'Open'),
+ (
+ 'class:command-runner-selected-item '
+ 'class:command-runner-fuzzy-highlight-1 ',
+ 'Open',
+ ),
],
)
@@ -157,18 +204,26 @@ class TestCommandRunner(unittest.TestCase):
command_runner.filter_completions()
result_items = join_adjacent_style_tuples(
flatten_formatted_text_tuples(
- command_runner.completion_fragments))
+ command_runner.completion_fragments
+ )
+ )
# Check file and open are highlighted, the fuzzy-highlight class
# should be swapped.
self.assertEqual(
result_items[:4],
[
('class:command-runner-selected-item', '['),
- ('class:command-runner-selected-item '
- 'class:command-runner-fuzzy-highlight-1 ', 'File'),
+ (
+ 'class:command-runner-selected-item '
+ 'class:command-runner-fuzzy-highlight-1 ',
+ 'File',
+ ),
('class:command-runner-selected-item', '] > '),
- ('class:command-runner-selected-item '
- 'class:command-runner-fuzzy-highlight-0 ', 'Open'),
+ (
+ 'class:command-runner-selected-item '
+ 'class:command-runner-fuzzy-highlight-0 ',
+ 'Open',
+ ),
],
)
@@ -177,7 +232,9 @@ class TestCommandRunner(unittest.TestCase):
command_runner.filter_completions()
result_items = join_adjacent_style_tuples(
flatten_formatted_text_tuples(
- command_runner.completion_fragments))
+ command_runner.completion_fragments
+ )
+ )
self.assertEqual(len(first_item_text.splitlines()), 1)
self.assertEqual(len(second_item_text.splitlines()), 3)
@@ -187,18 +244,29 @@ class TestCommandRunner(unittest.TestCase):
command_runner.filter_completions()
result_items = join_adjacent_style_tuples(
flatten_formatted_text_tuples(
- command_runner.completion_fragments))
+ command_runner.completion_fragments
+ )
+ )
self.assertEqual(len(result_items), 3)
# First line - not selected
self.assertEqual(result_items[0], ('', '[File] > Open Logger\n'))
# Second line - is selected
- self.assertEqual(result_items[1],
- ('class:command-runner-selected-item',
- '[Windows] > 1: Host Logs > Show/Hide\n'))
+ self.assertEqual(
+ result_items[1],
+ (
+ 'class:command-runner-selected-item',
+ '[Windows] > 1: Host Logs > Show/Hide\n',
+ ),
+ )
# Third and fourth lines separated by \n - not selected
- self.assertEqual(result_items[2],
- ('', '[Windows] > 2: Device Logs > Show/Hide\n'
- '[Help] > User Guide'))
+ self.assertEqual(
+ result_items[2],
+ (
+ '',
+ '[Windows] > 2: Device Logs > Show/Hide\n'
+ '[Help] > User Guide',
+ ),
+ )
def test_run_action(self) -> None:
"""Check running an action works correctly."""
@@ -224,15 +292,17 @@ class TestCommandRunner(unittest.TestCase):
# pylint: disable=protected-access
command_runner._make_regexes = MagicMock(
- wraps=command_runner._make_regexes)
+ wraps=command_runner._make_regexes
+ )
# pylint: enable=protected-access
command_runner.filter_completions()
# Filter should only be re-run if input text changed
command_runner.filter_completions()
command_runner._make_regexes.assert_called_once() # pylint: disable=protected-access
- self.assertIn('[View] > Move Window Right',
- command_runner.selected_item_text)
+ self.assertIn(
+ '[View] > Move Window Right', command_runner.selected_item_text
+ )
# Run the Move Window Right action
command_runner._run_selected_item() # pylint: disable=protected-access
# Dialog should be closed
diff --git a/pw_console/py/console_app_test.py b/pw_console/py/console_app_test.py
index 4dec65f79..8d0512264 100644
--- a/pw_console/py/console_app_test.py
+++ b/pw_console/py/console_app_test.py
@@ -18,6 +18,7 @@ import unittest
from prompt_toolkit.application import create_app_session
from prompt_toolkit.output import ColorDepth
+
# inclusive-language: ignore
from prompt_toolkit.output import DummyOutput as FakeOutput
@@ -27,25 +28,31 @@ from pw_console.console_prefs import ConsolePrefs
class TestConsoleApp(unittest.TestCase):
"""Tests for ConsoleApp."""
+
def test_instantiate(self) -> None:
"""Test init."""
with create_app_session(output=FakeOutput()):
- console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT,
- prefs=ConsolePrefs(
- project_file=False,
- project_user_file=False,
- user_file=False))
+ prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
+ prefs.set_code_theme('default')
+ console_app = ConsoleApp(
+ color_depth=ColorDepth.DEPTH_8_BIT, prefs=prefs
+ )
+
self.assertIsNotNone(console_app)
def test_multiple_loggers_in_one_pane(self) -> None:
"""Test window resizing."""
# pylint: disable=protected-access
with create_app_session(output=FakeOutput()):
- console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT,
- prefs=ConsolePrefs(
- project_file=False,
- project_user_file=False,
- user_file=False))
+ prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
+ prefs.set_code_theme('default')
+ console_app = ConsoleApp(
+ color_depth=ColorDepth.DEPTH_8_BIT, prefs=prefs
+ )
loggers = {
'Logs': [
@@ -62,10 +69,14 @@ class TestConsoleApp(unittest.TestCase):
self.assertEqual(len(window_list.active_panes), 2)
self.assertEqual(window_list.active_panes[0].pane_title(), 'Logs')
- self.assertEqual(window_list.active_panes[0]._pane_subtitle,
- 'test_log1, test_log2, test_log3')
- self.assertEqual(window_list.active_panes[0].pane_subtitle(),
- 'test_log1 + 3 more')
+ self.assertEqual(
+ window_list.active_panes[0]._pane_subtitle,
+ 'test_log1, test_log2, test_log3',
+ )
+ self.assertEqual(
+ window_list.active_panes[0].pane_subtitle(),
+ 'test_log1 + 3 more',
+ )
if __name__ == '__main__':
diff --git a/pw_console/py/console_prefs_test.py b/pw_console/py/console_prefs_test.py
index 958c00f2a..ece3da266 100644
--- a/pw_console/py/console_prefs_test.py
+++ b/pw_console/py/console_prefs_test.py
@@ -13,7 +13,6 @@
# the License.
"""Tests for pw_console.console_app"""
-from datetime import datetime
from pathlib import Path
import tempfile
import unittest
@@ -28,38 +27,36 @@ from pw_console.console_prefs import (
def _create_tempfile(content: str) -> Path:
- # Grab the current system timestamp as a string.
- isotime = datetime.now().isoformat(sep='_', timespec='seconds')
- isotime = isotime.replace(':', '')
-
- with tempfile.NamedTemporaryFile(prefix=f'{__package__}_{isotime}_',
- delete=False) as output_file:
- file_path = Path(output_file.name)
+ with tempfile.NamedTemporaryFile(
+ prefix=f'{__package__}', delete=False
+ ) as output_file:
output_file.write(content.encode('UTF-8'))
- return file_path
+ return Path(output_file.name)
class TestConsolePrefs(unittest.TestCase):
"""Tests for ConsolePrefs."""
+
def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
def test_load_no_existing_files(self) -> None:
- prefs = ConsolePrefs(project_file=False,
- project_user_file=False,
- user_file=False)
+ prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
self.assertEqual(_DEFAULT_CONFIG, prefs._config)
self.assertTrue(str(prefs.repl_history).endswith('pw_console_history'))
- self.assertTrue(
- str(prefs.search_history).endswith('pw_console_search'))
+ self.assertTrue(str(prefs.search_history).endswith('pw_console_search'))
def test_load_empty_file(self) -> None:
# Create an empty file
project_config_file = _create_tempfile('')
try:
- prefs = ConsolePrefs(project_file=project_config_file,
- project_user_file=False,
- user_file=False)
+ prefs = ConsolePrefs(
+ project_file=project_config_file,
+ project_user_file=False,
+ user_file=False,
+ )
result_settings = {
k: v
for k, v in prefs._config.items()
@@ -86,9 +83,11 @@ class TestConsolePrefs(unittest.TestCase):
}
project_config_file = _create_tempfile(yaml.dump(project_config))
try:
- prefs = ConsolePrefs(project_file=project_config_file,
- project_user_file=False,
- user_file=False)
+ prefs = ConsolePrefs(
+ project_file=project_config_file,
+ project_user_file=False,
+ user_file=False,
+ )
result_settings = {
k: v
for k, v in prefs._config.items()
@@ -125,7 +124,8 @@ class TestConsolePrefs(unittest.TestCase):
},
}
project_user_config_file = _create_tempfile(
- yaml.dump(project_user_config))
+ yaml.dump(project_user_config)
+ )
user_config = {
'pw_console': {
@@ -135,32 +135,39 @@ class TestConsolePrefs(unittest.TestCase):
}
user_config_file = _create_tempfile(yaml.dump(user_config))
try:
- prefs = ConsolePrefs(project_file=project_config_file,
- project_user_file=project_user_config_file,
- user_file=user_config_file)
+ prefs = ConsolePrefs(
+ project_file=project_config_file,
+ project_user_file=project_user_config_file,
+ user_file=user_config_file,
+ )
# Set by the project
- self.assertEqual(project_config['pw_console']['code_theme'],
- prefs.code_theme)
+ self.assertEqual(
+ project_config['pw_console']['code_theme'], prefs.code_theme
+ )
self.assertEqual(
project_config['pw_console']['swap_light_and_dark'],
- prefs.swap_light_and_dark)
+ prefs.swap_light_and_dark,
+ )
# Project user setting, result should not be project only setting.
project_history = project_config['pw_console']['repl_history']
assert isinstance(project_history, str)
self.assertNotEqual(
- Path(project_history).expanduser(), prefs.repl_history)
+ Path(project_history).expanduser(), prefs.repl_history
+ )
history = project_user_config['pw_console']['repl_history']
assert isinstance(history, str)
self.assertEqual(Path(history).expanduser(), prefs.repl_history)
# User config overrides project and project_user
- self.assertEqual(user_config['pw_console']['ui_theme'],
- prefs.ui_theme)
+ self.assertEqual(
+ user_config['pw_console']['ui_theme'], prefs.ui_theme
+ )
self.assertEqual(
Path(user_config['pw_console']['search_history']).expanduser(),
- prefs.search_history)
+ prefs.search_history,
+ )
# ui_theme should not be the project_user file setting
project_user_theme = project_user_config['pw_console']['ui_theme']
self.assertNotEqual(project_user_theme, prefs.ui_theme)
diff --git a/pw_console/py/help_window_test.py b/pw_console/py/help_window_test.py
index 2f25cac31..1a1f11072 100644
--- a/pw_console/py/help_window_test.py
+++ b/pw_console/py/help_window_test.py
@@ -23,8 +23,11 @@ from prompt_toolkit.key_binding import KeyBindings
from pw_console.help_window import HelpWindow
+_PW_CONSOLE_MODULE = 'pw_console'
+
+
_jinja_env = Environment(
- loader=PackageLoader('pw_console'),
+ loader=PackageLoader(_PW_CONSOLE_MODULE),
undefined=make_logging_undefined(logger=logging.getLogger('pw_console')),
trim_blocks=True,
lstrip_blocks=True,
@@ -40,6 +43,7 @@ def _create_app_mock():
class TestHelpWindow(unittest.TestCase):
"""Tests for HelpWindow text and keybind lists."""
+
def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
@@ -76,7 +80,7 @@ class TestHelpWindow(unittest.TestCase):
},
)
- def test_generate_help_text(self) -> None:
+ def test_generate_keybind_help_text(self) -> None:
"""Test keybind list template generation."""
global_bindings = KeyBindings()
@@ -107,50 +111,62 @@ class TestHelpWindow(unittest.TestCase):
help_window = HelpWindow(
app,
preamble='Pigweed CLI v0.1',
- additional_help_text=inspect.cleandoc("""
+ additional_help_text=inspect.cleandoc(
+ """
Welcome to the Pigweed Console!
Please enjoy this extra help text.
- """),
+ """
+ ),
)
help_window.add_keybind_help_text('Global', global_bindings)
help_window.add_keybind_help_text('Focus', focus_bindings)
- help_window.generate_help_text()
+ help_window.generate_keybind_help_text()
self.assertIn(
- inspect.cleandoc("""
+ inspect.cleandoc(
+ """
Welcome to the Pigweed Console!
Please enjoy this extra help text.
- """),
+ """
+ ),
help_window.help_text,
)
self.assertIn(
- inspect.cleandoc("""
+ inspect.cleandoc(
+ """
==== Global Keys ====
- """),
+ """
+ ),
help_window.help_text,
)
self.assertIn(
- inspect.cleandoc("""
+ inspect.cleandoc(
+ """
Toggle help window. ----------------- F1
Quit the application. --------------- Ctrl-Q
Ctrl-W
- """),
+ """
+ ),
help_window.help_text,
)
self.assertIn(
- inspect.cleandoc("""
+ inspect.cleandoc(
+ """
==== Focus Keys ====
- """),
+ """
+ ),
help_window.help_text,
)
self.assertIn(
- inspect.cleandoc("""
+ inspect.cleandoc(
+ """
Move focus to the next widget. ------ Ctrl-Down
Ctrl-Right
Shift-Tab
Move focus to the previous widget. -- Ctrl-Left
Ctrl-Up
- """),
+ """
+ ),
help_window.help_text,
)
diff --git a/pw_console/py/log_filter_test.py b/pw_console/py/log_filter_test.py
index d885560c0..7309c7abd 100644
--- a/pw_console/py/log_filter_test.py
+++ b/pw_console/py/log_filter_test.py
@@ -32,55 +32,58 @@ from pw_console.log_filter import (
class TestLogFilter(unittest.TestCase):
"""Tests for LogFilter."""
+
def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
# pylint: disable=anomalous-backslash-in-string
- @parameterized.expand([
- (
- 'raw string',
- SearchMatcher.STRING,
- 'f(x)',
- 'f\(x\)',
- re.IGNORECASE,
- ),
- (
- 'simple regex',
- SearchMatcher.REGEX,
- 'f(x)',
- 'f(x)',
- re.IGNORECASE,
- ),
- (
- 'regex with case sensitivity',
- SearchMatcher.REGEX,
- 'f(X)',
- 'f(X)',
- re.RegexFlag(0),
- ),
- (
- 'regex with error',
- SearchMatcher.REGEX,
- 'f of (x', # Un-terminated open paren
- 'f of (x',
- re.IGNORECASE,
- True, # fails_validation
- ),
- (
- 'simple fuzzy',
- SearchMatcher.FUZZY,
- 'f x y',
- '(f)(.*?)(x)(.*?)(y)',
- re.IGNORECASE,
- ),
- (
- 'fuzzy with case sensitivity',
- SearchMatcher.FUZZY,
- 'f X y',
- '(f)(.*?)(X)(.*?)(y)',
- re.RegexFlag(0),
- ),
- ]) # yapf: disable
+ @parameterized.expand(
+ [
+ (
+ 'raw string',
+ SearchMatcher.STRING,
+ 'f(x)',
+ 'f\(x\)',
+ re.IGNORECASE,
+ ),
+ (
+ 'simple regex',
+ SearchMatcher.REGEX,
+ 'f(x)',
+ 'f(x)',
+ re.IGNORECASE,
+ ),
+ (
+ 'regex with case sensitivity',
+ SearchMatcher.REGEX,
+ 'f(X)',
+ 'f(X)',
+ re.RegexFlag(0),
+ ),
+ (
+ 'regex with error',
+ SearchMatcher.REGEX,
+ 'f of (x', # Un-terminated open paren
+ 'f of (x',
+ re.IGNORECASE,
+ True, # fails_validation
+ ),
+ (
+ 'simple fuzzy',
+ SearchMatcher.FUZZY,
+ 'f x y',
+ '(f)(.*?)(x)(.*?)(y)',
+ re.IGNORECASE,
+ ),
+ (
+ 'fuzzy with case sensitivity',
+ SearchMatcher.FUZZY,
+ 'f X y',
+ '(f)(.*?)(X)(.*?)(y)',
+ re.RegexFlag(0),
+ ),
+ ]
+ )
def test_preprocess_search_regex(
self,
_name,
@@ -91,15 +94,17 @@ class TestLogFilter(unittest.TestCase):
should_fail_validation=False,
) -> None:
"""Test preprocess_search_regex returns the expected regex settings."""
- result_text, re_flag = preprocess_search_regex(input_text,
- input_matcher)
+ result_text, re_flag = preprocess_search_regex(
+ input_text, input_matcher
+ )
self.assertEqual(expected_regex, result_text)
self.assertEqual(expected_re_flag, re_flag)
if should_fail_validation:
document = Document(text=input_text)
- with self.assertRaisesRegex(ValidationError,
- r'Regex Error.*at position [0-9]+'):
+ with self.assertRaisesRegex(
+ ValidationError, r'Regex Error.*at position [0-9]+'
+ ):
RegexValidator().validate(document)
def _create_logs(self, log_messages):
@@ -110,76 +115,90 @@ class TestLogFilter(unittest.TestCase):
return log_context
- @parameterized.expand([
- (
- 'simple fuzzy',
- SearchMatcher.FUZZY,
- 'log item',
- [
- ('Log some item', {'planet': 'Jupiter'}),
- ('Log another item', {'planet': 'Earth'}),
- ('Some exception', {'planet': 'Earth'}),
- ],
- [
- 'Log some item',
- 'Log another item',
- ],
- None, # field
- False, # invert
- ),
- (
- 'simple fuzzy inverted',
- SearchMatcher.FUZZY,
- 'log item',
- [
- ('Log some item', dict()),
- ('Log another item', dict()),
- ('Some exception', dict()),
- ],
- [
- 'Some exception',
- ],
- None, # field
- True, # invert
- ),
- (
- 'regex with field',
- SearchMatcher.REGEX,
- 'earth',
- [
- ('Log some item',
- dict(extra_metadata_fields={'planet': 'Jupiter'})),
- ('Log another item',
- dict(extra_metadata_fields={'planet': 'Earth'})),
- ('Some exception',
- dict(extra_metadata_fields={'planet': 'Earth'})),
- ],
- [
- 'Log another item',
- 'Some exception',
- ],
- 'planet', # field
- False, # invert
- ),
- (
- 'regex with field inverted',
- SearchMatcher.REGEX,
- 'earth',
- [
- ('Log some item',
- dict(extra_metadata_fields={'planet': 'Jupiter'})),
- ('Log another item',
- dict(extra_metadata_fields={'planet': 'Earth'})),
- ('Some exception',
- dict(extra_metadata_fields={'planet': 'Earth'})),
- ],
- [
- 'Log some item',
- ],
- 'planet', # field
- True, # invert
- ),
- ]) # yapf: disable
+ @parameterized.expand(
+ [
+ (
+ 'simple fuzzy',
+ SearchMatcher.FUZZY,
+ 'log item',
+ [
+ ('Log some item', {'planet': 'Jupiter'}),
+ ('Log another item', {'planet': 'Earth'}),
+ ('Some exception', {'planet': 'Earth'}),
+ ],
+ [
+ 'Log some item',
+ 'Log another item',
+ ],
+ None, # field
+ False, # invert
+ ),
+ (
+ 'simple fuzzy inverted',
+ SearchMatcher.FUZZY,
+ 'log item',
+ [
+ ('Log some item', dict()),
+ ('Log another item', dict()),
+ ('Some exception', dict()),
+ ],
+ [
+ 'Some exception',
+ ],
+ None, # field
+ True, # invert
+ ),
+ (
+ 'regex with field',
+ SearchMatcher.REGEX,
+ 'earth',
+ [
+ (
+ 'Log some item',
+ dict(extra_metadata_fields={'planet': 'Jupiter'}),
+ ),
+ (
+ 'Log another item',
+ dict(extra_metadata_fields={'planet': 'Earth'}),
+ ),
+ (
+ 'Some exception',
+ dict(extra_metadata_fields={'planet': 'Earth'}),
+ ),
+ ],
+ [
+ 'Log another item',
+ 'Some exception',
+ ],
+ 'planet', # field
+ False, # invert
+ ),
+ (
+ 'regex with field inverted',
+ SearchMatcher.REGEX,
+ 'earth',
+ [
+ (
+ 'Log some item',
+ dict(extra_metadata_fields={'planet': 'Jupiter'}),
+ ),
+ (
+ 'Log another item',
+ dict(extra_metadata_fields={'planet': 'Earth'}),
+ ),
+ (
+ 'Some exception',
+ dict(extra_metadata_fields={'planet': 'Earth'}),
+ ),
+ ],
+ [
+ 'Log some item',
+ ],
+ 'planet', # field
+ True, # invert
+ ),
+ ]
+ )
def test_log_filter_matches(
self,
_name,
@@ -191,8 +210,9 @@ class TestLogFilter(unittest.TestCase):
invert=False,
) -> None:
"""Test log filter matches expected lines."""
- result_text, re_flag = preprocess_search_regex(input_text,
- input_matcher)
+ result_text, re_flag = preprocess_search_regex(
+ input_text, input_matcher
+ )
log_filter = LogFilter(
regex=re.compile(result_text, re_flag),
input_text=input_text,
@@ -205,7 +225,8 @@ class TestLogFilter(unittest.TestCase):
for record in logs.records:
if log_filter.matches(
- LogLine(record, record.message, record.message)):
+ LogLine(record, record.message, record.message)
+ ):
matched_lines.append(record.message)
self.assertEqual(expected_matched_lines, matched_lines)
diff --git a/pw_console/py/log_store_test.py b/pw_console/py/log_store_test.py
index 37b628733..8359cc5bf 100644
--- a/pw_console/py/log_store_test.py
+++ b/pw_console/py/log_store_test.py
@@ -22,8 +22,11 @@ from pw_console.console_prefs import ConsolePrefs
def _create_log_store():
- log_store = LogStore(prefs=ConsolePrefs(
- project_file=False, project_user_file=False, user_file=False))
+ log_store = LogStore(
+ prefs=ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
+ )
assert not log_store.table.prefs.show_python_file
viewer = MagicMock()
@@ -34,6 +37,7 @@ def _create_log_store():
class TestLogStore(unittest.TestCase):
"""Tests for LogStore."""
+
def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
@@ -64,11 +68,13 @@ class TestLogStore(unittest.TestCase):
log_store, _viewer = _create_log_store()
# Log some messagse on 3 separate logger instances
- for i, logger_name in enumerate([
+ for i, logger_name in enumerate(
+ [
'log_store.test',
'log_store.dev',
'log_store.production',
- ]):
+ ]
+ ):
test_log = logging.getLogger(logger_name)
with self.assertLogs(test_log, level='DEBUG') as _log_context:
test_log.addHandler(log_store)
@@ -109,11 +115,15 @@ class TestLogStore(unittest.TestCase):
# Log table with extra columns
with self.assertLogs(test_log, level='DEBUG') as _log_context:
test_log.addHandler(log_store)
- test_log.debug('Test log %s',
- extra=dict(extra_metadata_fields={
- 'planet': 'Jupiter',
- 'galaxy': 'Milky Way'
- }))
+ test_log.debug(
+ 'Test log %s',
+ extra=dict(
+ extra_metadata_fields={
+ 'planet': 'Jupiter',
+ 'galaxy': 'Milky Way',
+ }
+ ),
+ )
self.assertEqual(
[
diff --git a/pw_console/py/log_view_test.py b/pw_console/py/log_view_test.py
index a21a5647d..a46409e19 100644
--- a/pw_console/py/log_view_test.py
+++ b/pw_console/py/log_view_test.py
@@ -43,9 +43,9 @@ def _create_log_view():
log_pane.current_log_pane_height = 10
application = MagicMock()
- application.prefs = ConsolePrefs(project_file=False,
- project_user_file=False,
- user_file=False)
+ application.prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
application.prefs.reset_config()
log_view = LogView(log_pane, application)
return log_view, log_pane
@@ -198,8 +198,8 @@ class TestLogView(unittest.TestCase):
# Mock time to always return the same value.
mock_time = MagicMock( # type: ignore
- return_value=time.mktime(
- datetime(2021, 7, 13, 0, 0, 0).timetuple()))
+ return_value=time.mktime(datetime(2021, 7, 13, 0, 0, 0).timetuple())
+ )
with patch('time.time', new=mock_time):
log_view, log_pane = self._create_log_view_with_logs(log_count=4)
@@ -224,26 +224,28 @@ class TestLogView(unittest.TestCase):
('', ' '),
('class:log-level-10', 'DEBUG'),
('', ' Test log 0'),
-
('class:log-time', '20210713 00:00:00'),
('', ' '),
('class:log-level-10', 'DEBUG'),
('', ' Test log 1'),
-
('class:log-time', '20210713 00:00:00'),
('', ' '),
('class:log-level-10', 'DEBUG'),
('', ' Test log 2'),
-
('class:selected-log-line class:log-time', '20210713 00:00:00'),
('class:selected-log-line ', ' '),
('class:selected-log-line class:log-level-10', 'DEBUG'),
- ('class:selected-log-line ',
- ' Test log 3 ')
- ] # yapf: disable
+ (
+ 'class:selected-log-line ',
+ ' Test log 3 ',
+ ),
+ ]
+ # pylint: disable=protected-access
result_text = join_adjacent_style_tuples(
- flatten_formatted_text_tuples(log_view._line_fragment_cache)) # pylint: disable=protected-access
+ flatten_formatted_text_tuples(log_view._line_fragment_cache)
+ )
+ # pylint: enable=protected-access
self.assertEqual(result_text, expected_formatted_text)
@@ -253,7 +255,8 @@ class TestLogView(unittest.TestCase):
# Create log_view with 4 logs
starting_log_count = 4
log_view, _pane = self._create_log_view_with_logs(
- log_count=starting_log_count)
+ log_count=starting_log_count
+ )
log_view.render_content()
# Check setup is correct
@@ -261,9 +264,11 @@ class TestLogView(unittest.TestCase):
self.assertEqual(log_view.get_current_line(), 3)
self.assertEqual(log_view.get_total_count(), 4)
self.assertEqual(
- list(log.record.message
- for log in log_view._get_visible_log_lines()),
- ['Test log 0', 'Test log 1', 'Test log 2', 'Test log 3'])
+ list(
+ log.record.message for log in log_view._get_visible_log_lines()
+ ),
+ ['Test log 0', 'Test log 1', 'Test log 2', 'Test log 3'],
+ )
# Clear scrollback
log_view.clear_scrollback()
@@ -277,8 +282,11 @@ class TestLogView(unittest.TestCase):
self.assertEqual(log_view.get_total_count(), 4)
# No lines returned
self.assertEqual(
- list(log.record.message
- for log in log_view._get_visible_log_lines()), [])
+ list(
+ log.record.message for log in log_view._get_visible_log_lines()
+ ),
+ [],
+ )
# Add Log 4 more lines
test_log = logging.getLogger('log_view.test')
@@ -295,9 +303,11 @@ class TestLogView(unittest.TestCase):
self.assertEqual(log_view.get_total_count(), 8)
# Only the last 4 logs should appear
self.assertEqual(
- list(log.record.message
- for log in log_view._get_visible_log_lines()),
- ['Test log 4', 'Test log 5', 'Test log 6', 'Test log 7'])
+ list(
+ log.record.message for log in log_view._get_visible_log_lines()
+ ),
+ ['Test log 4', 'Test log 5', 'Test log 6', 'Test log 7'],
+ )
log_view.scroll_to_bottom()
log_view.render_content()
@@ -311,11 +321,20 @@ class TestLogView(unittest.TestCase):
self.assertEqual(log_view.get_total_count(), 8)
# All logs should appear
self.assertEqual(
- list(log.record.message
- for log in log_view._get_visible_log_lines()), [
- 'Test log 0', 'Test log 1', 'Test log 2', 'Test log 3',
- 'Test log 4', 'Test log 5', 'Test log 6', 'Test log 7'
- ])
+ list(
+ log.record.message for log in log_view._get_visible_log_lines()
+ ),
+ [
+ 'Test log 0',
+ 'Test log 1',
+ 'Test log 2',
+ 'Test log 3',
+ 'Test log 4',
+ 'Test log 5',
+ 'Test log 6',
+ 'Test log 7',
+ ],
+ )
log_view.scroll_to_bottom()
log_view.render_content()
@@ -334,7 +353,8 @@ class TestLogView(unittest.TestCase):
# Create log_view with 4 logs
starting_log_count = 4
log_view, _pane = self._create_log_view_with_logs(
- log_count=starting_log_count)
+ log_count=starting_log_count
+ )
log_view.render_content()
# Check setup is correct
@@ -342,9 +362,11 @@ class TestLogView(unittest.TestCase):
self.assertEqual(log_view.get_current_line(), 3)
self.assertEqual(log_view.get_total_count(), 4)
self.assertEqual(
- list(log.record.message
- for log in log_view._get_visible_log_lines()),
- ['Test log 0', 'Test log 1', 'Test log 2', 'Test log 3'])
+ list(
+ log.record.message for log in log_view._get_visible_log_lines()
+ ),
+ ['Test log 0', 'Test log 1', 'Test log 2', 'Test log 3'],
+ )
self.assertEqual(log_view.log_screen.cursor_position, 9)
# Force the cursor_position to be larger than the log_screen
@@ -376,14 +398,17 @@ class TestLogView(unittest.TestCase):
log_pane.current_log_pane_height = 10
log_view.log_screen.reset_logs = MagicMock(
- wraps=log_view.log_screen.reset_logs)
+ wraps=log_view.log_screen.reset_logs
+ )
log_view.log_screen.get_lines = MagicMock(
- wraps=log_view.log_screen.get_lines)
+ wraps=log_view.log_screen.get_lines
+ )
log_view.render_content()
log_view.log_screen.reset_logs.assert_called_once()
log_view.log_screen.get_lines.assert_called_once_with(
- marked_logs_start=None, marked_logs_end=None)
+ marked_logs_start=None, marked_logs_end=None
+ )
log_view.log_screen.get_lines.reset_mock()
log_view.log_screen.reset_logs.reset_mock()
@@ -391,12 +416,14 @@ class TestLogView(unittest.TestCase):
self.assertIsNone(log_view.marked_logs_end)
log_view.visual_select_line(Point(0, 9))
self.assertEqual(
- (99, 99), (log_view.marked_logs_start, log_view.marked_logs_end))
+ (99, 99), (log_view.marked_logs_start, log_view.marked_logs_end)
+ )
log_view.visual_select_line(Point(0, 8))
log_view.visual_select_line(Point(0, 7))
self.assertEqual(
- (97, 99), (log_view.marked_logs_start, log_view.marked_logs_end))
+ (97, 99), (log_view.marked_logs_start, log_view.marked_logs_end)
+ )
log_view.clear_visual_selection()
self.assertIsNone(log_view.marked_logs_start)
@@ -407,7 +434,8 @@ class TestLogView(unittest.TestCase):
log_view.visual_select_line(Point(0, 3))
log_view.visual_select_line(Point(0, 4))
self.assertEqual(
- (91, 94), (log_view.marked_logs_start, log_view.marked_logs_end))
+ (91, 94), (log_view.marked_logs_start, log_view.marked_logs_end)
+ )
# Make sure the log screen was not re-generated.
log_view.log_screen.reset_logs.assert_not_called()
@@ -418,7 +446,8 @@ class TestLogView(unittest.TestCase):
log_view.log_screen.reset_logs.assert_called_once()
# Check the visual selection was specified
log_view.log_screen.get_lines.assert_called_once_with(
- marked_logs_start=91, marked_logs_end=94)
+ marked_logs_start=91, marked_logs_end=94
+ )
log_view.log_screen.get_lines.reset_mock()
log_view.log_screen.reset_logs.reset_mock()
@@ -426,7 +455,9 @@ class TestLogView(unittest.TestCase):
if _PYTHON_3_8:
from unittest import IsolatedAsyncioTestCase # type: ignore # pylint: disable=no-name-in-module
- class TestLogViewFiltering(IsolatedAsyncioTestCase): # pylint: disable=undefined-variable
+ class TestLogViewFiltering(
+ IsolatedAsyncioTestCase
+ ): # pylint: disable=undefined-variable
"""Test LogView log filtering capabilities."""
# pylint: disable=invalid-name
@@ -446,90 +477,99 @@ if _PYTHON_3_8:
return log_view, log_pane
- @parameterized.expand([
- (
- # Test name
- 'regex filter',
- # Search input_text
- 'log.*item',
- # input_logs
- [
- ('Log some item', dict()),
- ('Log another item', dict()),
- ('Some exception', dict()),
- ],
- # expected_matched_lines
- [
- 'Log some item',
- 'Log another item',
- ],
- # expected_match_line_numbers
- {0: 0, 1: 1},
- # expected_export_text
+ @parameterized.expand(
+ [
(
- ' DEBUG Log some item\n'
- ' DEBUG Log another item\n'
+ # Test name
+ 'regex filter',
+ # Search input_text
+ 'log.*item',
+ # input_logs
+ [
+ ('Log some item', dict()),
+ ('Log another item', dict()),
+ ('Some exception', dict()),
+ ],
+ # expected_matched_lines
+ [
+ 'Log some item',
+ 'Log another item',
+ ],
+ # expected_match_line_numbers
+ {0: 0, 1: 1},
+ # expected_export_text
+ (' DEBUG Log some item\n DEBUG Log another item\n'),
+ None, # field
+ False, # invert
),
- None, # field
- False, # invert
- ),
- (
- # Test name
- 'regex filter with field',
- # Search input_text
- 'earth',
- # input_logs
- [
- ('Log some item',
- dict(extra_metadata_fields={'planet': 'Jupiter'})),
- ('Log another item',
- dict(extra_metadata_fields={'planet': 'Earth'})),
- ('Some exception',
- dict(extra_metadata_fields={'planet': 'Earth'})),
- ],
- # expected_matched_lines
- [
- 'Log another item',
- 'Some exception',
- ],
- # expected_match_line_numbers
- {1: 0, 2: 1},
- # expected_export_text
(
- ' DEBUG Earth Log another item\n'
- ' DEBUG Earth Some exception\n'
+ # Test name
+ 'regex filter with field',
+ # Search input_text
+ 'earth',
+ # input_logs
+ [
+ (
+ 'Log some item',
+ dict(extra_metadata_fields={'planet': 'Jupiter'}),
+ ),
+ (
+ 'Log another item',
+ dict(extra_metadata_fields={'planet': 'Earth'}),
+ ),
+ (
+ 'Some exception',
+ dict(extra_metadata_fields={'planet': 'Earth'}),
+ ),
+ ],
+ # expected_matched_lines
+ [
+ 'Log another item',
+ 'Some exception',
+ ],
+ # expected_match_line_numbers
+ {1: 0, 2: 1},
+ # expected_export_text
+ (
+ ' DEBUG Earth Log another item\n'
+ ' DEBUG Earth Some exception\n'
+ ),
+ 'planet', # field
+ False, # invert
),
- 'planet', # field
- False, # invert
- ),
- (
- # Test name
- 'regex filter with field inverted',
- # Search input_text
- 'earth',
- # input_logs
- [
- ('Log some item',
- dict(extra_metadata_fields={'planet': 'Jupiter'})),
- ('Log another item',
- dict(extra_metadata_fields={'planet': 'Earth'})),
- ('Some exception',
- dict(extra_metadata_fields={'planet': 'Earth'})),
- ],
- # expected_matched_lines
- [
- 'Log some item',
- ],
- # expected_match_line_numbers
- {0: 0},
- # expected_export_text
(
- ' DEBUG Jupiter Log some item\n'
+ # Test name
+ 'regex filter with field inverted',
+ # Search input_text
+ 'earth',
+ # input_logs
+ [
+ (
+ 'Log some item',
+ dict(extra_metadata_fields={'planet': 'Jupiter'}),
+ ),
+ (
+ 'Log another item',
+ dict(extra_metadata_fields={'planet': 'Earth'}),
+ ),
+ (
+ 'Some exception',
+ dict(extra_metadata_fields={'planet': 'Earth'}),
+ ),
+ ],
+ # expected_matched_lines
+ [
+ 'Log some item',
+ ],
+ # expected_match_line_numbers
+ {0: 0},
+ # expected_export_text
+ (' DEBUG Jupiter Log some item\n'),
+ 'planet', # field
+ True, # invert
),
- 'planet', # field
- True, # invert
- ),
- ]) # yapf: disable
+ ]
+ )
async def test_log_filtering(
self,
_test_name,
@@ -549,40 +589,49 @@ if _PYTHON_3_8:
# Apply the search and wait for the match count background task
log_view.new_search(input_text, invert=invert, field=field)
await log_view.search_match_count_task
- self.assertEqual(log_view.search_matched_lines,
- expected_match_line_numbers)
+ self.assertEqual(
+ log_view.search_matched_lines, expected_match_line_numbers
+ )
# Apply the filter and wait for the filter background task
log_view.apply_filter()
await log_view.filter_existing_logs_task
# Do the number of logs match the expected count?
- self.assertEqual(log_view.get_total_count(),
- len(expected_matched_lines))
+ self.assertEqual(
+ log_view.get_total_count(), len(expected_matched_lines)
+ )
self.assertEqual(
[log.record.message for log in log_view.filtered_logs],
- expected_matched_lines)
+ expected_matched_lines,
+ )
# Check exported text respects filtering
- log_text = log_view._logs_to_text( # pylint: disable=protected-access
- use_table_formatting=True)
+ log_text = (
+ log_view._logs_to_text( # pylint: disable=protected-access
+ use_table_formatting=True
+ )
+ )
# Remove leading time from resulting logs
log_text_no_datetime = ''
for line in log_text.splitlines():
- log_text_no_datetime += (line[17:] + '\n')
+ log_text_no_datetime += line[17:] + '\n'
self.assertEqual(log_text_no_datetime, expected_export_text)
# Select the bottom log line
log_view.render_content()
log_view.visual_select_line(Point(0, 9)) # Window height is 10
# Export to text
- log_text = log_view._logs_to_text( # pylint: disable=protected-access
- selected_lines_only=True,
- use_table_formatting=False)
+ log_text = (
+ log_view._logs_to_text( # pylint: disable=protected-access
+ selected_lines_only=True, use_table_formatting=False
+ )
+ )
self.assertEqual(
# Remove date, time, and level
log_text[24:].strip(),
- expected_matched_lines[0].strip())
+ expected_matched_lines[0].strip(),
+ )
# Clear filters and check the numbe of lines is back to normal.
log_view.clear_filters()
diff --git a/pw_console/py/pw_console/__main__.py b/pw_console/py/pw_console/__main__.py
index 936bd32b5..2881b0a5c 100644
--- a/pw_console/py/pw_console/__main__.py
+++ b/pw_console/py/pw_console/__main__.py
@@ -18,46 +18,53 @@ import inspect
import logging
from pathlib import Path
import sys
+from typing import Optional, Dict
-import pw_cli.log
-import pw_cli.argument_types
+from pw_cli import log as pw_cli_log
+from pw_cli import argument_types
-import pw_console
-import pw_console.python_logging
+from pw_console import PwConsoleEmbed
+from pw_console.python_logging import create_temp_log_file
from pw_console.log_store import LogStore
from pw_console.plugins.calc_pane import CalcPane
from pw_console.plugins.clock_pane import ClockPane
+from pw_console.plugins.twenty48_pane import Twenty48Pane
+from pw_console.test_mode import FAKE_DEVICE_LOGGER_NAME
_LOG = logging.getLogger(__package__)
_ROOT_LOG = logging.getLogger('')
-# TODO(tonymd): Remove this when no downstream projects are using it.
-def create_temp_log_file():
- return pw_console.python_logging.create_temp_log_file()
-
-
def _build_argument_parser() -> argparse.ArgumentParser:
"""Setup argparse."""
- parser = argparse.ArgumentParser(description=__doc__)
+ parser = argparse.ArgumentParser(
+ prog="python -m pw_console", description=__doc__
+ )
- parser.add_argument('-l',
- '--loglevel',
- type=pw_cli.argument_types.log_level,
- default=logging.DEBUG,
- help='Set the log level'
- '(debug, info, warning, error, critical)')
+ parser.add_argument(
+ '-l',
+ '--loglevel',
+ type=argument_types.log_level,
+ default=logging.DEBUG,
+ help='Set the log level' '(debug, info, warning, error, critical)',
+ )
parser.add_argument('--logfile', help='Pigweed Console log file.')
- parser.add_argument('--test-mode',
- action='store_true',
- help='Enable fake log messages for testing purposes.')
- parser.add_argument('--config-file',
- type=Path,
- help='Path to a pw_console yaml config file.')
- parser.add_argument('--console-debug-log-file',
- help='Log file to send console debug messages to.')
+ parser.add_argument(
+ '--test-mode',
+ action='store_true',
+ help='Enable fake log messages for testing purposes.',
+ )
+ parser.add_argument(
+ '--config-file',
+ type=Path,
+ help='Path to a pw_console yaml config file.',
+ )
+ parser.add_argument(
+ '--console-debug-log-file',
+ help='Log file to send console debug messages to.',
+ )
return parser
@@ -71,19 +78,23 @@ def main() -> int:
if not args.logfile:
# Create a temp logfile to prevent logs from appearing over stdout. This
# would corrupt the prompt toolkit UI.
- args.logfile = pw_console.python_logging.create_temp_log_file()
+ args.logfile = create_temp_log_file()
- pw_cli.log.install(level=args.loglevel,
- use_color=True,
- hide_timestamp=False,
- log_file=args.logfile)
+ pw_cli_log.install(
+ level=args.loglevel,
+ use_color=True,
+ hide_timestamp=False,
+ log_file=args.logfile,
+ )
if args.console_debug_log_file:
- pw_cli.log.install(level=logging.DEBUG,
- use_color=True,
- hide_timestamp=False,
- log_file=args.console_debug_log_file,
- logger=logging.getLogger('pw_console'))
+ pw_cli_log.install(
+ level=logging.DEBUG,
+ use_color=True,
+ hide_timestamp=False,
+ log_file=args.console_debug_log_file,
+ logger=logging.getLogger('pw_console'),
+ )
global_vars = None
default_loggers = {}
@@ -92,12 +103,11 @@ def main() -> int:
_ROOT_LOG.addHandler(root_log_store)
_ROOT_LOG.debug('pw_console test-mode starting...')
- fake_logger = logging.getLogger(
- pw_console.console_app.FAKE_DEVICE_LOGGER_NAME)
+ fake_logger = logging.getLogger(FAKE_DEVICE_LOGGER_NAME)
default_loggers = {
# Don't include pw_console package logs (_LOG) in the log pane UI.
# Add the fake logger for test_mode.
- 'Fake Device Logs': [fake_logger],
+ 'Fake Device': [fake_logger],
'PwConsole Debug': [logging.getLogger('pw_console')],
'All Logs': root_log_store,
}
@@ -108,7 +118,8 @@ def main() -> int:
app_title = None
if args.test_mode:
app_title = 'Console Test Mode'
- help_text = inspect.cleandoc("""
+ help_text = inspect.cleandoc(
+ """
Welcome to the Pigweed Console Test Mode!
Example commands:
@@ -116,9 +127,10 @@ def main() -> int:
rpcs.pw.rpc.EchoService.Echo(msg='hello!')
LOG.warning('Message appears console log window.')
- """)
+ """
+ )
- console = pw_console.PwConsoleEmbed(
+ console = PwConsoleEmbed(
global_vars=global_vars,
loggers=default_loggers,
test_mode=args.test_mode,
@@ -127,16 +139,48 @@ def main() -> int:
config_file_path=args.config_file,
)
- # Add example plugins used to validate behavior in the Pigweed Console
- # manual test procedure: https://pigweed.dev/pw_console/testing.html
+ overriden_window_config: Optional[Dict] = None
+ # Add example plugins and log panes used to validate behavior in the Pigweed
+ # Console manual test procedure: https://pigweed.dev/pw_console/testing.html
if args.test_mode:
+ fake_logger.propagate = False
+ console.setup_python_logging(loggers_with_no_propagation=[fake_logger])
+
_ROOT_LOG.debug('pw_console.PwConsoleEmbed init complete')
_ROOT_LOG.debug('Adding plugins...')
console.add_window_plugin(ClockPane())
console.add_window_plugin(CalcPane())
+ console.add_floating_window_plugin(
+ Twenty48Pane(include_resize_handle=False), left=4
+ )
_ROOT_LOG.debug('Starting prompt_toolkit full-screen application...')
- console.embed()
+ overriden_window_config = {
+ 'Split 1 stacked': {
+ 'Fake Device': None,
+ 'Fake Keys': {
+ 'duplicate_of': 'Fake Device',
+ 'filters': {
+ 'keys': {'regex': '[^ ]+'},
+ },
+ },
+ 'Fake USB': {
+ 'duplicate_of': 'Fake Device',
+ 'filters': {
+ 'module': {'regex': 'USB'},
+ },
+ },
+ },
+ 'Split 2 tabbed': {
+ 'Python Repl': None,
+ 'All Logs': None,
+ 'PwConsole Debug': None,
+ 'Calculator': None,
+ 'Clock': None,
+ },
+ }
+
+ console.embed(override_window_config=overriden_window_config)
if args.logfile:
print(f'Logs saved to: {args.logfile}')
diff --git a/pw_console/py/pw_console/command_runner.py b/pw_console/py/pw_console/command_runner.py
index 88ff984b9..e2caf1212 100644
--- a/pw_console/py/pw_console/command_runner.py
+++ b/pw_console/py/pw_console/command_runner.py
@@ -51,9 +51,11 @@ from prompt_toolkit.layout import (
from prompt_toolkit.widgets import MenuItem
from prompt_toolkit.widgets import TextArea
-import pw_console.widgets.border
-import pw_console.widgets.checkbox
-import pw_console.widgets.mouse_handlers
+from pw_console.widgets import (
+ create_border,
+ mouse_handlers,
+ to_keybind_indicator,
+)
if TYPE_CHECKING:
from pw_console.console_app import ConsoleApp
@@ -61,8 +63,9 @@ if TYPE_CHECKING:
_LOG = logging.getLogger(__package__)
-def flatten_menu_items(items: List[MenuItem],
- prefix: str = '') -> Iterator[Tuple[str, Callable]]:
+def flatten_menu_items(
+ items: List[MenuItem], prefix: str = ''
+) -> Iterator[Tuple[str, Callable]]:
"""Flatten nested prompt_toolkit MenuItems into text and callable tuples."""
for item in items:
new_text = []
@@ -81,23 +84,22 @@ def flatten_menu_items(items: List[MenuItem],
def highlight_matches(
- regexes: Iterable[re.Pattern],
- line_fragments: StyleAndTextTuples) -> StyleAndTextTuples:
+ regexes: Iterable[re.Pattern], line_fragments: StyleAndTextTuples
+) -> StyleAndTextTuples:
"""Highlight regex matches in prompt_toolkit FormattedTextTuples."""
line_text = fragment_list_to_text(line_fragments)
exploded_fragments = explode_text_fragments(line_fragments)
- def apply_highlighting(fragments: StyleAndTextTuples,
- index: int,
- matching_regex_index: int = 0) -> None:
+ def apply_highlighting(
+ fragments: StyleAndTextTuples, index: int, matching_regex_index: int = 0
+ ) -> None:
# Expand all fragments and apply the highlighting style.
old_style, _text, *_ = fragments[index]
# There are 6 fuzzy-highlight styles defined in style.py. Get an index
# from 0-5 to use one style after the other in turn.
style_index = matching_regex_index % 6
fragments[index] = (
- old_style +
- f' class:command-runner-fuzzy-highlight-{style_index} ',
+ old_style + f' class:command-runner-fuzzy-highlight-{style_index} ',
fragments[index][1],
)
@@ -116,14 +118,15 @@ class CommandRunner:
# pylint: disable=too-many-instance-attributes
def __init__(
- self,
- application: ConsoleApp,
- window_title: str = None,
- load_completions: Optional[Callable[[],
- List[Tuple[str,
- Callable]]]] = None,
- width: int = 80,
- height: int = 10):
+ self,
+ application: ConsoleApp,
+ window_title: Optional[str] = None,
+ load_completions: Optional[
+ Callable[[], List[Tuple[str, Callable]]]
+ ] = None,
+ width: int = 80,
+ height: int = 10,
+ ):
# Parent pw_console application
self.application = application
# Visibility toggle
@@ -157,9 +160,14 @@ class CommandRunner:
# Command runner text input field
self.input_field = TextArea(
prompt=[
- ('class:command-runner-setting', '> ',
- functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self))
+ (
+ 'class:command-runner-setting',
+ '> ',
+ functools.partial(
+ mouse_handlers.on_click,
+ self.focus_self,
+ ),
+ )
],
focusable=True,
focus_on_click=True,
@@ -205,10 +213,12 @@ class CommandRunner:
self.command_runner_content = HSplit(
[
# Input field and buttons on the same line
- VSplit([
- self.input_field,
- input_field_buttons_container,
- ]),
+ VSplit(
+ [
+ self.input_field,
+ input_field_buttons_container,
+ ]
+ ),
# Completion items below
command_items_window,
],
@@ -229,7 +239,7 @@ class CommandRunner:
def _create_bordered_content(self) -> None:
"""Wrap self.command_runner_content in a border."""
# This should be called whenever the window_title changes.
- self.bordered_content = pw_console.widgets.border.create_border(
+ self.bordered_content = create_border(
self.command_runner_content,
title=self.window_title,
border_style='class:command-runner-border',
@@ -270,7 +280,8 @@ class CommandRunner:
def content_width(self) -> int:
"""Return the smaller value of self.width and the available width."""
window_manager_width = (
- self.application.window_manager.current_window_manager_width)
+ self.application.window_manager.current_window_manager_width
+ )
if not window_manager_width:
window_manager_width = self.width
return min(self.width, window_manager_width)
@@ -298,17 +309,19 @@ class CommandRunner:
def set_completions(
self,
- window_title: str = None,
- load_completions: Optional[Callable[[], List[Tuple[str,
- Callable]]]] = None,
+ window_title: Optional[str] = None,
+ load_completions: Optional[
+ Callable[[], List[Tuple[str, Callable]]]
+ ] = None,
) -> None:
"""Set window title and callable to fetch possible completions.
Call this function whenever new completion items need to be loaded.
"""
self.window_title = window_title if window_title else 'Menu Items'
- self.load_completions = (load_completions
- if load_completions else self.load_menu_items)
+ self.load_completions = (
+ load_completions if load_completions else self.load_menu_items
+ )
self._reset_selected_item()
self.completions = []
@@ -374,7 +387,8 @@ class CommandRunner:
self.selected_item_handler = handler
text = text.ljust(self.content_width())
fragments: StyleAndTextTuples = highlight_matches(
- regexes, [(style, text + '\n')])
+ regexes, [(style, text + '\n')]
+ )
self.completion_fragments.append(fragments)
i += 1
@@ -397,7 +411,8 @@ class CommandRunner:
# Don't move past the height of the window or the length of possible
# items.
min(self.height, len(self.completion_fragments)) - 1,
- self.selected_item + 1)
+ self.selected_item + 1,
+ )
self.application.redraw_ui()
def _previous_item(self) -> None:
@@ -407,13 +422,11 @@ class CommandRunner:
def _get_input_field_button_fragments(self) -> StyleAndTextTuples:
# Mouse handlers
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self)
- cancel = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.close_dialog)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
+ cancel = functools.partial(mouse_handlers.on_click, self.close_dialog)
select_item = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self._run_selected_item)
+ mouse_handlers.on_click, self._run_selected_item
+ )
separator_text = ('', ' ', focus)
@@ -424,18 +437,21 @@ class CommandRunner:
# Cancel button
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='Ctrl-c',
description='Cancel',
mouse_handler=cancel,
base_style=button_style,
- ))
+ )
+ )
fragments.append(separator_text)
# Run button
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
- 'Enter', 'Run', select_item, base_style=button_style))
+ to_keybind_indicator(
+ 'Enter', 'Run', select_item, base_style=button_style
+ )
+ )
return fragments
def render_completion_items(self) -> StyleAndTextTuples:
@@ -473,7 +489,9 @@ class CommandRunner:
# Actions that launch new command runners, close_dialog should not run.
for command_text in [
- '[File] > Open Logger',
+ '[File] > Insert Repl Snippet',
+ '[File] > Insert Repl History',
+ '[File] > Open Logger',
]:
if command_text in self.selected_item_text:
close_dialog = False
@@ -482,12 +500,14 @@ class CommandRunner:
# Actions that change what is in focus should be run after closing the
# command runner dialog.
for command_text in [
- '[View] > Focus Next Window/Tab',
- '[View] > Focus Prev Window/Tab',
- # All help menu entries open popup windows.
- '[Help] > ',
- # This focuses on a save dialog bor.
- 'Save/Export a copy',
+ '[File] > Games > ',
+ '[View] > Focus Next Window/Tab',
+ '[View] > Focus Prev Window/Tab',
+ # All help menu entries open popup windows.
+ '[Help] > ',
+ # This focuses on a save dialog bor.
+ 'Save/Export a copy',
+ '[Windows] > Floating ',
]:
if command_text in self.selected_item_text:
close_dialog_first = True
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index 560d94672..38f0bfa38 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -16,14 +16,17 @@
import asyncio
import builtins
import functools
+import socketserver
+import importlib.resources
import logging
import os
from pathlib import Path
import sys
+import time
from threading import Thread
-from typing import Any, Callable, Iterable, List, Optional, Tuple, Union
+from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union
-from jinja2 import Environment, FileSystemLoader, make_logging_undefined
+from jinja2 import Environment, DictLoader, make_logging_undefined
from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.output import ColorDepth
@@ -51,38 +54,48 @@ from prompt_toolkit.history import (
)
from ptpython.layout import CompletionVisualisation # type: ignore
from ptpython.key_bindings import ( # type: ignore
- load_python_bindings, load_sidebar_bindings,
+ load_python_bindings,
+ load_sidebar_bindings,
)
+from pw_console.command_runner import CommandRunner
+from pw_console.console_log_server import (
+ ConsoleLogHTTPRequestHandler,
+ pw_console_http_server,
+)
from pw_console.console_prefs import ConsolePrefs
from pw_console.help_window import HelpWindow
-from pw_console.command_runner import CommandRunner
-import pw_console.key_bindings
+from pw_console.key_bindings import create_key_bindings
from pw_console.log_pane import LogPane
from pw_console.log_store import LogStore
from pw_console.pw_ptpython_repl import PwPtPythonRepl
from pw_console.python_logging import all_loggers
from pw_console.quit_dialog import QuitDialog
from pw_console.repl_pane import ReplPane
-import pw_console.style
-import pw_console.widgets.checkbox
-import pw_console.widgets.mouse_handlers
+from pw_console.style import generate_styles
+from pw_console.test_mode import start_fake_logger
+from pw_console.widgets import (
+ FloatingWindowPane,
+ mouse_handlers,
+ to_checkbox_text,
+ to_keybind_indicator,
+)
from pw_console.window_manager import WindowManager
_LOG = logging.getLogger(__package__)
+_ROOT_LOG = logging.getLogger('')
+
+_SYSTEM_COMMAND_LOG = logging.getLogger('pw_console_system_command')
-# Fake logger for --test-mode
-FAKE_DEVICE_LOGGER_NAME = 'pw_console_fake_device'
-_FAKE_DEVICE_LOG = logging.getLogger(FAKE_DEVICE_LOGGER_NAME)
-# Don't send fake_device logs to the root Python logger.
-_FAKE_DEVICE_LOG.propagate = False
+_PW_CONSOLE_MODULE = 'pw_console'
-MAX_FPS = 15
+MAX_FPS = 30
MIN_REDRAW_INTERVAL = (60.0 / MAX_FPS) / 60.0
class FloatingMessageBar(ConditionalContainer):
"""Floating message bar for showing status messages."""
+
def __init__(self, application):
super().__init__(
FormattedTextToolbar(
@@ -90,12 +103,16 @@ class FloatingMessageBar(ConditionalContainer):
style='class:toolbar_inactive',
),
filter=Condition(
- lambda: application.message and application.message != ''))
+ lambda: application.message and application.message != ''
+ ),
+ )
-def _add_log_handler_to_pane(logger: Union[str, logging.Logger],
- pane: 'LogPane',
- level_name: Optional[str] = None) -> None:
+def _add_log_handler_to_pane(
+ logger: Union[str, logging.Logger],
+ pane: 'LogPane',
+ level_name: Optional[str] = None,
+) -> None:
"""A log pane handler for a given logger instance."""
if not pane:
return
@@ -103,7 +120,8 @@ def _add_log_handler_to_pane(logger: Union[str, logging.Logger],
def get_default_colordepth(
- color_depth: Optional[ColorDepth] = None) -> ColorDepth:
+ color_depth: Optional[ColorDepth] = None,
+) -> ColorDepth:
# Set prompt_toolkit color_depth to the highest possible.
if color_depth is None:
# Default to 24bit color
@@ -135,10 +153,21 @@ class ConsoleApp:
color_depth=None,
extra_completers=None,
prefs=None,
+ floating_window_plugins: Optional[
+ List[Tuple[FloatingWindowPane, Dict]]
+ ] = None,
):
self.prefs = prefs if prefs else ConsolePrefs()
self.color_depth = get_default_colordepth(color_depth)
+ # Max frequency in seconds of prompt_toolkit UI redraws triggered by new
+ # log lines.
+ self.log_ui_update_frequency = 0.1 # 10 FPS
+ self._last_ui_update_time = time.time()
+
+ self.http_server: Optional[socketserver.TCPServer] = None
+ self.html_files: Dict[str, str] = {}
+
# Create a default global and local symbol table. Values are the same
# structure as what is returned by globals():
# https://docs.python.org/3/library/functions.html#globals
@@ -152,13 +181,24 @@ class ConsoleApp:
local_vars = local_vars or global_vars
+ jinja_templates = {
+ t: importlib.resources.read_text(
+ f'{_PW_CONSOLE_MODULE}.templates', t
+ )
+ for t in importlib.resources.contents(
+ f'{_PW_CONSOLE_MODULE}.templates'
+ )
+ if t.endswith('.jinja')
+ }
+
# Setup the Jinja environment
self.jinja_env = Environment(
# Load templates automatically from pw_console/templates
- loader=FileSystemLoader(Path(__file__).parent / 'templates'),
+ loader=DictLoader(jinja_templates),
# Raise errors if variables are undefined in templates
undefined=make_logging_undefined(
- logger=logging.getLogger(__package__), ),
+ logger=logging.getLogger(__package__),
+ ),
# Trim whitespace in templates
trim_blocks=True,
lstrip_blocks=True,
@@ -169,10 +209,12 @@ class ConsoleApp:
# History instance for search toolbars.
self.search_history: History = ThreadedHistory(
- FileHistory(self.search_history_filename))
+ FileHistory(self.search_history_filename)
+ )
# Event loop for executing user repl code.
self.user_code_loop = asyncio.new_event_loop()
+ self.test_mode_log_loop = asyncio.new_event_loop()
self.app_title = app_title if app_title else 'Pigweed Console'
@@ -187,13 +229,16 @@ class ConsoleApp:
self.message = [('class:logo', self.app_title), ('', ' ')]
self.message.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
'Ctrl-p',
'Search Menu',
- functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.open_command_runner_main_menu),
+ functools.partial(
+ mouse_handlers.on_click,
+ self.open_command_runner_main_menu,
+ ),
base_style='class:toolbar-button-inactive',
- ))
+ )
+ )
# One space separator
self.message.append(('', ' '))
@@ -202,14 +247,23 @@ class ConsoleApp:
# Downstream project specific help text
self.app_help_text = help_text if help_text else None
- self.app_help_window = HelpWindow(self,
- additional_help_text=help_text,
- title=(self.app_title + ' Help'))
- self.app_help_window.generate_help_text()
+ self.app_help_window = HelpWindow(
+ self,
+ additional_help_text=help_text,
+ title=(self.app_title + ' Help'),
+ )
+ self.app_help_window.generate_keybind_help_text()
self.prefs_file_window = HelpWindow(self, title='.pw_console.yaml')
self.prefs_file_window.load_yaml_text(
- self.prefs.current_config_as_yaml())
+ self.prefs.current_config_as_yaml()
+ )
+
+ self.floating_window_plugins: List[FloatingWindowPane] = []
+ if floating_window_plugins:
+ self.floating_window_plugins = [
+ plugin for plugin, _ in floating_window_plugins
+ ]
# Used for tracking which pane was in focus before showing help window.
self.last_focused_pane = None
@@ -231,6 +285,8 @@ class ConsoleApp:
)
self.pw_ptpython_repl.use_code_colorscheme(self.prefs.code_theme)
+ self.system_command_output_pane: Optional[LogPane] = None
+
if self.prefs.swap_light_and_dark:
self.toggle_light_theme()
@@ -244,7 +300,7 @@ class ConsoleApp:
self.quit_dialog = QuitDialog(self)
# Key bindings registry.
- self.key_bindings = pw_console.key_bindings.create_key_bindings(self)
+ self.key_bindings = create_key_bindings(self)
# Create help window text based global key_bindings and active panes.
self._update_help_window()
@@ -292,35 +348,52 @@ class ConsoleApp:
# Callable to get width
width=self.keybind_help_window.content_width,
),
- # Completion menu that can overlap other panes since it lives in
- # the top level Float container.
- Float(
- xcursor=True,
- ycursor=True,
- content=ConditionalContainer(
- content=CompletionsMenu(
- scroll_offset=(lambda: self.pw_ptpython_repl.
- completion_menu_scroll_offset),
- max_height=16,
+ ]
+
+ if floating_window_plugins:
+ self.floats.extend(
+ [
+ Float(content=plugin_container, **float_args)
+ for plugin_container, float_args in floating_window_plugins
+ ]
+ )
+
+ self.floats.extend(
+ [
+ # Completion menu that can overlap other panes since it lives in
+ # the top level Float container.
+ # pylint: disable=line-too-long
+ Float(
+ xcursor=True,
+ ycursor=True,
+ content=ConditionalContainer(
+ content=CompletionsMenu(
+ scroll_offset=(
+ lambda: self.pw_ptpython_repl.completion_menu_scroll_offset
+ ),
+ max_height=16,
+ ),
+ # Only show our completion if ptpython's is disabled.
+ filter=Condition(
+ lambda: self.pw_ptpython_repl.completion_visualisation
+ == CompletionVisualisation.NONE
+ ),
),
- # Only show our completion if ptpython's is disabled.
- filter=Condition(
- lambda: self.pw_ptpython_repl.completion_visualisation
- == CompletionVisualisation.NONE),
),
- ),
- Float(
- content=self.command_runner,
- # Callable to get width
- width=self.command_runner.content_width,
- **self.prefs.command_runner_position,
- ),
- Float(
- content=self.quit_dialog,
- top=2,
- left=2,
- ),
- ]
+ # pylint: enable=line-too-long
+ Float(
+ content=self.command_runner,
+ # Callable to get width
+ width=self.command_runner.content_width,
+ **self.prefs.command_runner_position,
+ ),
+ Float(
+ content=self.quit_dialog,
+ top=2,
+ left=2,
+ ),
+ ]
+ )
# prompt_toolkit root container.
self.root_container = MenuContainer(
@@ -353,18 +426,24 @@ class ConsoleApp:
# Create the prompt_toolkit Application instance.
self.application: Application = Application(
layout=self.layout,
- key_bindings=merge_key_bindings([
- # Pull key bindings from ptpython
- load_python_bindings(self.pw_ptpython_repl),
- load_sidebar_bindings(self.pw_ptpython_repl),
- self.window_manager.key_bindings,
- self.key_bindings,
- ]),
- style=DynamicStyle(lambda: merge_styles([
- self._current_theme,
- # Include ptpython styles
- self.pw_ptpython_repl._current_style, # pylint: disable=protected-access
- ])),
+ key_bindings=merge_key_bindings(
+ [
+ # Pull key bindings from ptpython
+ load_python_bindings(self.pw_ptpython_repl),
+ load_sidebar_bindings(self.pw_ptpython_repl),
+ self.window_manager.key_bindings,
+ self.key_bindings,
+ ]
+ ),
+ style=DynamicStyle(
+ lambda: merge_styles(
+ [
+ self._current_theme,
+ # Include ptpython styles
+ self.pw_ptpython_repl._current_style, # pylint: disable=protected-access
+ ]
+ )
+ ),
style_transformation=self.pw_ptpython_repl.style_transformation,
enable_page_navigation_bindings=True,
full_screen=True,
@@ -381,9 +460,9 @@ class ConsoleApp:
# Run the function for a particular menu item.
return_value = function_to_run()
# It's return value dictates if the main menu should close or not.
- # - True: The main menu stays open. This is the default prompt_toolkit
+ # - False: The main menu stays open. This is the default prompt_toolkit
# menu behavior.
- # - False: The main menu closes.
+ # - True: The main menu closes.
# Update menu content. This will refresh checkboxes and add/remove
# items.
@@ -394,27 +473,35 @@ class ConsoleApp:
self.focus_main_menu()
def open_new_log_pane_for_logger(
- self,
- logger_name: str,
- level_name='NOTSET',
- window_title: Optional[str] = None) -> None:
+ self,
+ logger_name: str,
+ level_name='NOTSET',
+ window_title: Optional[str] = None,
+ ) -> None:
pane_title = window_title if window_title else logger_name
self.run_pane_menu_option(
- functools.partial(self.add_log_handler,
- pane_title, [logger_name],
- log_level_name=level_name))
+ functools.partial(
+ self.add_log_handler,
+ pane_title,
+ [logger_name],
+ log_level_name=level_name,
+ )
+ )
def set_ui_theme(self, theme_name: str) -> Callable:
call_function = functools.partial(
self.run_pane_menu_option,
- functools.partial(self.load_theme, theme_name))
+ functools.partial(self.load_theme, theme_name),
+ )
return call_function
def set_code_theme(self, theme_name: str) -> Callable:
call_function = functools.partial(
self.run_pane_menu_option,
- functools.partial(self.pw_ptpython_repl.use_code_colorscheme,
- theme_name))
+ functools.partial(
+ self.pw_ptpython_repl.use_code_colorscheme, theme_name
+ ),
+ )
return call_function
def update_menu_items(self):
@@ -429,7 +516,8 @@ class ConsoleApp:
def open_command_runner_loggers(self) -> None:
self.command_runner.set_completions(
window_title='Open Logger',
- load_completions=self._create_logger_completions)
+ load_completions=self._create_logger_completions,
+ )
if not self.command_runner_is_open():
self.command_runner.open_dialog()
@@ -437,20 +525,85 @@ class ConsoleApp:
completions: List[Tuple[str, Callable]] = [
(
'root',
- functools.partial(self.open_new_log_pane_for_logger,
- '',
- window_title='root'),
+ functools.partial(
+ self.open_new_log_pane_for_logger, '', window_title='root'
+ ),
),
]
all_logger_names = sorted([logger.name for logger in all_loggers()])
for logger_name in all_logger_names:
- completions.append((
- logger_name,
- functools.partial(self.open_new_log_pane_for_logger,
- logger_name),
- ))
+ completions.append(
+ (
+ logger_name,
+ functools.partial(
+ self.open_new_log_pane_for_logger, logger_name
+ ),
+ )
+ )
+ return completions
+
+ def open_command_runner_history(self) -> None:
+ self.command_runner.set_completions(
+ window_title='History',
+ load_completions=self._create_history_completions,
+ )
+ if not self.command_runner_is_open():
+ self.command_runner.open_dialog()
+
+ def _create_history_completions(self) -> List[Tuple[str, Callable]]:
+ return [
+ (
+ description,
+ functools.partial(
+ self.repl_pane.insert_text_into_input_buffer, text
+ ),
+ )
+ for description, text in self.repl_pane.history_completions()
+ ]
+
+ def open_command_runner_snippets(self) -> None:
+ self.command_runner.set_completions(
+ window_title='Snippets',
+ load_completions=self._create_snippet_completions,
+ )
+ if not self.command_runner_is_open():
+ self.command_runner.open_dialog()
+
+ def _http_server_entry(self) -> None:
+ handler = functools.partial(
+ ConsoleLogHTTPRequestHandler, self.html_files
+ )
+ pw_console_http_server(3000, handler)
+
+ def start_http_server(self):
+ if self.http_server is not None:
+ return
+
+ html_package_path = f'{_PW_CONSOLE_MODULE}.html'
+ self.html_files = {
+ '/{}'.format(t): importlib.resources.read_text(html_package_path, t)
+ for t in importlib.resources.contents(html_package_path)
+ if Path(t).suffix in ['.css', '.html', '.js']
+ }
+
+ server_thread = Thread(
+ target=self._http_server_entry, args=(), daemon=True
+ )
+ server_thread.start()
+
+ def _create_snippet_completions(self) -> List[Tuple[str, Callable]]:
+ completions: List[Tuple[str, Callable]] = [
+ (
+ description,
+ functools.partial(
+ self.repl_pane.insert_text_into_input_buffer, text
+ ),
+ )
+ for description, text in self.prefs.snippet_completions()
+ ]
+
return completions
def _create_menu_items(self):
@@ -461,8 +614,9 @@ class ConsoleApp:
'UI Themes',
children=[
MenuItem('Default: Dark', self.set_ui_theme('dark')),
- MenuItem('High Contrast',
- self.set_ui_theme('high-contrast-dark')),
+ MenuItem(
+ 'High Contrast', self.set_ui_theme('high-contrast-dark')
+ ),
MenuItem('Nord', self.set_ui_theme('nord')),
MenuItem('Nord Light', self.set_ui_theme('nord-light')),
MenuItem('Moonlight', self.set_ui_theme('moonlight')),
@@ -471,25 +625,23 @@ class ConsoleApp:
MenuItem(
'Code Themes',
children=[
- MenuItem('Code: pigweed-code',
- self.set_code_theme('pigweed-code')),
- MenuItem('Code: pigweed-code-light',
- self.set_code_theme('pigweed-code-light')),
- MenuItem('Code: material',
- self.set_code_theme('material')),
- MenuItem('Code: gruvbox-light',
- self.set_code_theme('gruvbox-light')),
- MenuItem('Code: gruvbox-dark',
- self.set_code_theme('gruvbox-dark')),
- MenuItem('Code: tomorrow-night',
- self.set_code_theme('tomorrow-night')),
- MenuItem('Code: tomorrow-night-bright',
- self.set_code_theme('tomorrow-night-bright')),
- MenuItem('Code: tomorrow-night-blue',
- self.set_code_theme('tomorrow-night-blue')),
- MenuItem('Code: tomorrow-night-eighties',
- self.set_code_theme('tomorrow-night-eighties')),
- MenuItem('Code: dracula', self.set_code_theme('dracula')),
+ MenuItem(
+ 'Code: pigweed-code',
+ self.set_code_theme('pigweed-code'),
+ ),
+ MenuItem(
+ 'Code: pigweed-code-light',
+ self.set_code_theme('pigweed-code-light'),
+ ),
+ MenuItem('Code: material', self.set_code_theme('material')),
+ MenuItem(
+ 'Code: gruvbox-light',
+ self.set_code_theme('gruvbox-light'),
+ ),
+ MenuItem(
+ 'Code: gruvbox-dark',
+ self.set_code_theme('gruvbox-dark'),
+ ),
MenuItem('Code: zenburn', self.set_code_theme('zenburn')),
],
),
@@ -500,55 +652,81 @@ class ConsoleApp:
MenuItem(
'[File]',
children=[
- MenuItem('Open Logger',
- handler=self.open_command_runner_loggers),
+ MenuItem(
+ 'Insert Repl Snippet',
+ handler=self.open_command_runner_snippets,
+ ),
+ MenuItem(
+ 'Insert Repl History',
+ handler=self.open_command_runner_history,
+ ),
+ MenuItem(
+ 'Open Logger', handler=self.open_command_runner_loggers
+ ),
MenuItem(
'Log Table View',
children=[
+ # pylint: disable=line-too-long
MenuItem(
'{check} Hide Date'.format(
- check=pw_console.widgets.checkbox.
- to_checkbox_text(
+ check=to_checkbox_text(
self.prefs.hide_date_from_log_time,
- end='')),
+ end='',
+ )
+ ),
handler=functools.partial(
self.run_pane_menu_option,
functools.partial(
self.toggle_pref_option,
- 'hide_date_from_log_time')),
+ 'hide_date_from_log_time',
+ ),
+ ),
),
MenuItem(
'{check} Show Source File'.format(
- check=pw_console.widgets.checkbox.
- to_checkbox_text(
- self.prefs.show_source_file, end='')),
+ check=to_checkbox_text(
+ self.prefs.show_source_file, end=''
+ )
+ ),
handler=functools.partial(
self.run_pane_menu_option,
- functools.partial(self.toggle_pref_option,
- 'show_source_file')),
+ functools.partial(
+ self.toggle_pref_option,
+ 'show_source_file',
+ ),
+ ),
),
MenuItem(
'{check} Show Python File'.format(
- check=pw_console.widgets.checkbox.
- to_checkbox_text(
- self.prefs.show_python_file, end='')),
+ check=to_checkbox_text(
+ self.prefs.show_python_file, end=''
+ )
+ ),
handler=functools.partial(
self.run_pane_menu_option,
- functools.partial(self.toggle_pref_option,
- 'show_python_file')),
+ functools.partial(
+ self.toggle_pref_option,
+ 'show_python_file',
+ ),
+ ),
),
MenuItem(
'{check} Show Python Logger'.format(
- check=pw_console.widgets.checkbox.
- to_checkbox_text(
- self.prefs.show_python_logger,
- end='')),
+ check=to_checkbox_text(
+ self.prefs.show_python_logger, end=''
+ )
+ ),
handler=functools.partial(
self.run_pane_menu_option,
- functools.partial(self.toggle_pref_option,
- 'show_python_logger')),
+ functools.partial(
+ self.toggle_pref_option,
+ 'show_python_logger',
+ ),
+ ),
),
- ]),
+ # pylint: enable=line-too-long
+ ],
+ ),
MenuItem('-'),
MenuItem(
'Themes',
@@ -564,14 +742,29 @@ class ConsoleApp:
MenuItem(
'[Edit]',
children=[
- MenuItem('Paste to Python Input',
- handler=self.repl_pane.
- paste_system_clipboard_to_input_buffer),
+ # pylint: disable=line-too-long
+ MenuItem(
+ 'Paste to Python Input',
+ handler=self.repl_pane.paste_system_clipboard_to_input_buffer,
+ ),
+ # pylint: enable=line-too-long
MenuItem('-'),
- MenuItem('Copy all Python Output',
- handler=self.repl_pane.copy_all_output_text),
- MenuItem('Copy all Python Input',
- handler=self.repl_pane.copy_all_input_text),
+ MenuItem(
+ 'Copy all Python Output',
+ handler=self.repl_pane.copy_all_output_text,
+ ),
+ MenuItem(
+ 'Copy all Python Input',
+ handler=self.repl_pane.copy_all_input_text,
+ ),
+ MenuItem('-'),
+ MenuItem(
+ 'Clear Python Input', self.repl_pane.clear_input_buffer
+ ),
+ MenuItem(
+ 'Clear Python Output',
+ self.repl_pane.clear_output_buffer,
+ ),
],
),
]
@@ -581,86 +774,167 @@ class ConsoleApp:
'[View]',
children=[
# [Menu Item ][Keybind ]
- MenuItem('Focus Next Window/Tab Ctrl-Alt-n',
- handler=self.window_manager.focus_next_pane),
+ MenuItem(
+ 'Focus Next Window/Tab Ctrl-Alt-n',
+ handler=self.window_manager.focus_next_pane,
+ ),
# [Menu Item ][Keybind ]
- MenuItem('Focus Prev Window/Tab Ctrl-Alt-p',
- handler=self.window_manager.focus_previous_pane),
+ MenuItem(
+ 'Focus Prev Window/Tab Ctrl-Alt-p',
+ handler=self.window_manager.focus_previous_pane,
+ ),
MenuItem('-'),
-
# [Menu Item ][Keybind ]
- MenuItem('Move Window Up Ctrl-Alt-Up',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.move_pane_up)),
+ MenuItem(
+ 'Move Window Up Ctrl-Alt-Up',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.move_pane_up,
+ ),
+ ),
# [Menu Item ][Keybind ]
- MenuItem('Move Window Down Ctrl-Alt-Down',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.move_pane_down)),
+ MenuItem(
+ 'Move Window Down Ctrl-Alt-Down',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.move_pane_down,
+ ),
+ ),
# [Menu Item ][Keybind ]
- MenuItem('Move Window Left Ctrl-Alt-Left',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.move_pane_left)),
+ MenuItem(
+ 'Move Window Left Ctrl-Alt-Left',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.move_pane_left,
+ ),
+ ),
# [Menu Item ][Keybind ]
- MenuItem('Move Window Right Ctrl-Alt-Right',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.move_pane_right)),
+ MenuItem(
+ 'Move Window Right Ctrl-Alt-Right',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.move_pane_right,
+ ),
+ ),
MenuItem('-'),
-
# [Menu Item ][Keybind ]
- MenuItem('Shrink Height Alt-Minus',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.shrink_pane)),
+ MenuItem(
+ 'Shrink Height Alt-Minus',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.shrink_pane,
+ ),
+ ),
# [Menu Item ][Keybind ]
- MenuItem('Enlarge Height Alt-=',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.enlarge_pane)),
+ MenuItem(
+ 'Enlarge Height Alt-=',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.enlarge_pane,
+ ),
+ ),
MenuItem('-'),
-
# [Menu Item ][Keybind ]
- MenuItem('Shrink Column Alt-,',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.shrink_split)),
+ MenuItem(
+ 'Shrink Column Alt-,',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.shrink_split,
+ ),
+ ),
# [Menu Item ][Keybind ]
- MenuItem('Enlarge Column Alt-.',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.enlarge_split)),
+ MenuItem(
+ 'Enlarge Column Alt-.',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.enlarge_split,
+ ),
+ ),
MenuItem('-'),
-
# [Menu Item ][Keybind ]
- MenuItem('Balance Window Sizes Ctrl-u',
- handler=functools.partial(
- self.run_pane_menu_option,
- self.window_manager.balance_window_sizes)),
+ MenuItem(
+ 'Balance Window Sizes Ctrl-u',
+ handler=functools.partial(
+ self.run_pane_menu_option,
+ self.window_manager.balance_window_sizes,
+ ),
+ ),
],
),
]
- window_menu = self.window_manager.create_window_menu()
+ window_menu_items = self.window_manager.create_window_menu_items()
+
+ floating_window_items = []
+ if self.floating_window_plugins:
+ floating_window_items.append(MenuItem('-', None))
+ floating_window_items.extend(
+ MenuItem(
+ 'Floating Window {index}: {title}'.format(
+ index=pane_index + 1,
+ title=pane.menu_title(),
+ ),
+ children=[
+ MenuItem(
+ # pylint: disable=line-too-long
+ '{check} Show/Hide Window'.format(
+ check=to_checkbox_text(pane.show_pane, end='')
+ ),
+ # pylint: enable=line-too-long
+ handler=functools.partial(
+ self.run_pane_menu_option, pane.toggle_dialog
+ ),
+ ),
+ ]
+ + [
+ MenuItem(
+ text,
+ handler=functools.partial(
+ self.run_pane_menu_option, handler
+ ),
+ )
+ for text, handler in pane.get_window_menu_options()
+ ],
+ )
+ for pane_index, pane in enumerate(self.floating_window_plugins)
+ )
+ window_menu_items.extend(floating_window_items)
+
+ window_menu = [MenuItem('[Windows]', children=window_menu_items)]
+
+ top_level_plugin_menus = []
+ for pane in self.window_manager.active_panes():
+ top_level_plugin_menus.extend(pane.get_top_level_menus())
+ if self.floating_window_plugins:
+ for pane in self.floating_window_plugins:
+ top_level_plugin_menus.extend(pane.get_top_level_menus())
help_menu_items = [
- MenuItem(self.user_guide_window.menu_title(),
- handler=self.user_guide_window.toggle_display),
- MenuItem(self.keybind_help_window.menu_title(),
- handler=self.keybind_help_window.toggle_display),
+ MenuItem(
+ self.user_guide_window.menu_title(),
+ handler=self.user_guide_window.toggle_display,
+ ),
+ MenuItem(
+ self.keybind_help_window.menu_title(),
+ handler=self.keybind_help_window.toggle_display,
+ ),
MenuItem('-'),
- MenuItem('View Key Binding Config',
- handler=self.prefs_file_window.toggle_display),
+ MenuItem(
+ 'View Key Binding Config',
+ handler=self.prefs_file_window.toggle_display,
+ ),
]
if self.app_help_text:
- help_menu_items.extend([
- MenuItem('-'),
- MenuItem(self.app_help_window.menu_title(),
- handler=self.app_help_window.toggle_display)
- ])
+ help_menu_items.extend(
+ [
+ MenuItem('-'),
+ MenuItem(
+ self.app_help_window.menu_title(),
+ handler=self.app_help_window.toggle_display,
+ ),
+ ]
+ )
help_menu = [
# Info / Help
@@ -670,7 +944,14 @@ class ConsoleApp:
),
]
- return file_menu + edit_menu + view_menu + window_menu + help_menu
+ return (
+ file_menu
+ + edit_menu
+ + view_menu
+ + top_level_plugin_menus
+ + window_menu
+ + help_menu
+ )
def focus_main_menu(self):
"""Set application focus to the main menu."""
@@ -690,7 +971,8 @@ class ConsoleApp:
"""Toggle light and dark theme colors."""
# Use ptpython's style_transformation to swap dark and light colors.
self.pw_ptpython_repl.swap_light_and_dark = (
- not self.pw_ptpython_repl.swap_light_and_dark)
+ not self.pw_ptpython_repl.swap_light_and_dark
+ )
if self.application:
self.focus_main_menu()
@@ -699,17 +981,17 @@ class ConsoleApp:
def load_theme(self, theme_name=None):
"""Regenerate styles for the current theme_name."""
- self._current_theme = pw_console.style.generate_styles(theme_name)
+ self._current_theme = generate_styles(theme_name)
if theme_name:
self.prefs.set_ui_theme(theme_name)
- def _create_log_pane(self,
- title: str = '',
- log_store: Optional[LogStore] = None) -> 'LogPane':
+ def _create_log_pane(
+ self, title: str = '', log_store: Optional[LogStore] = None
+ ) -> 'LogPane':
# Create one log pane.
- log_pane = LogPane(application=self,
- pane_title=title,
- log_store=log_store)
+ log_pane = LogPane(
+ application=self, pane_title=title, log_store=log_store
+ )
self.window_manager.add_pane(log_pane)
return log_pane
@@ -726,11 +1008,12 @@ class ConsoleApp:
self._update_help_window()
def add_log_handler(
- self,
- window_title: str,
- logger_instances: Union[Iterable[logging.Logger], LogStore],
- separate_log_panes: bool = False,
- log_level_name: Optional[str] = None) -> Optional[LogPane]:
+ self,
+ window_title: str,
+ logger_instances: Union[Iterable[logging.Logger], LogStore],
+ separate_log_panes: bool = False,
+ log_level_name: Optional[str] = None,
+ ) -> Optional[LogPane]:
"""Add the Log pane as a handler for this logger instance."""
existing_log_pane = None
@@ -745,13 +1028,15 @@ class ConsoleApp:
log_store = logger_instances
if not existing_log_pane or separate_log_panes:
- existing_log_pane = self._create_log_pane(title=window_title,
- log_store=log_store)
+ existing_log_pane = self._create_log_pane(
+ title=window_title, log_store=log_store
+ )
if isinstance(logger_instances, list):
for logger in logger_instances:
- _add_log_handler_to_pane(logger, existing_log_pane,
- log_level_name)
+ _add_log_handler_to_pane(
+ logger, existing_log_pane, log_level_name
+ )
self.refresh_layout()
return existing_log_pane
@@ -763,11 +1048,16 @@ class ConsoleApp:
def start_user_code_thread(self):
"""Create a thread for running user code so the UI isn't blocked."""
- thread = Thread(target=self._user_code_thread_entry,
- args=(),
- daemon=True)
+ thread = Thread(
+ target=self._user_code_thread_entry, args=(), daemon=True
+ )
thread.start()
+ def _test_mode_log_thread_entry(self):
+ """Entry point for the user code thread."""
+ asyncio.set_event_loop(self.test_mode_log_loop)
+ self.test_mode_log_loop.run_forever()
+
def _update_help_window(self):
"""Generate the help window text based on active pane keybindings."""
# Add global mouse bindings to the help text.
@@ -777,14 +1067,17 @@ class ConsoleApp:
}
self.keybind_help_window.add_custom_keybinds_help_text(
- 'Global Mouse', mouse_functions)
+ 'Global Mouse', mouse_functions
+ )
# Add global key bindings to the help text.
- self.keybind_help_window.add_keybind_help_text('Global',
- self.key_bindings)
+ self.keybind_help_window.add_keybind_help_text(
+ 'Global', self.key_bindings
+ )
self.keybind_help_window.add_keybind_help_text(
- 'Window Management', self.window_manager.key_bindings)
+ 'Window Management', self.window_manager.key_bindings
+ )
# Add activated plugin key bindings to the help text.
for pane in self.window_manager.active_panes():
@@ -792,12 +1085,14 @@ class ConsoleApp:
help_section_title = pane.__class__.__name__
if isinstance(key_bindings, KeyBindings):
self.keybind_help_window.add_keybind_help_text(
- help_section_title, key_bindings)
+ help_section_title, key_bindings
+ )
elif isinstance(key_bindings, dict):
self.keybind_help_window.add_custom_keybinds_help_text(
- help_section_title, key_bindings)
+ help_section_title, key_bindings
+ )
- self.keybind_help_window.generate_help_text()
+ self.keybind_help_window.generate_keybind_help_text()
def toggle_log_line_wrapping(self):
"""Menu item handler to toggle line wrapping of all log panes."""
@@ -817,23 +1112,39 @@ class ConsoleApp:
def modal_window_is_open(self):
"""Return true if any modal window or dialog is open."""
+ floating_window_is_open = (
+ self.keybind_help_window.show_window
+ or self.prefs_file_window.show_window
+ or self.user_guide_window.show_window
+ or self.quit_dialog.show_dialog
+ or self.command_runner.show_dialog
+ )
+
if self.app_help_text:
- return (self.app_help_window.show_window
- or self.keybind_help_window.show_window
- or self.prefs_file_window.show_window
- or self.user_guide_window.show_window
- or self.quit_dialog.show_dialog
- or self.command_runner.show_dialog)
- return (self.keybind_help_window.show_window
- or self.prefs_file_window.show_window
- or self.user_guide_window.show_window
- or self.quit_dialog.show_dialog
- or self.command_runner.show_dialog)
+ floating_window_is_open = (
+ self.app_help_window.show_window or floating_window_is_open
+ )
+
+ floating_plugin_is_open = any(
+ plugin.show_pane for plugin in self.floating_window_plugins
+ )
+
+ return floating_window_is_open or floating_plugin_is_open
def exit_console(self):
"""Quit the console prompt_toolkit application UI."""
self.application.exit()
+ def logs_redraw(self):
+ emit_time = time.time()
+ # Has enough time passed since last UI redraw due to new logs?
+ if emit_time > self._last_ui_update_time + self.log_ui_update_frequency:
+ # Update last log time
+ self._last_ui_update_time = emit_time
+
+ # Trigger Prompt Toolkit UI redraw.
+ self.redraw_ui()
+
def redraw_ui(self):
"""Redraw the prompt_toolkit UI."""
if hasattr(self, 'application'):
@@ -841,10 +1152,37 @@ class ConsoleApp:
# loop.
self.application.invalidate()
+ def setup_command_runner_log_pane(self) -> None:
+ if not self.system_command_output_pane is None:
+ return
+
+ self.system_command_output_pane = LogPane(
+ application=self, pane_title='Shell Output'
+ )
+ self.system_command_output_pane.add_log_handler(
+ _SYSTEM_COMMAND_LOG, level_name='INFO'
+ )
+ self.system_command_output_pane.log_view.log_store.formatter = (
+ logging.Formatter('%(message)s')
+ )
+ self.system_command_output_pane.table_view = False
+ self.system_command_output_pane.show_pane = True
+ # Enable line wrapping
+ self.system_command_output_pane.toggle_wrap_lines()
+ # Blank right side toolbar text
+ # pylint: disable=protected-access
+ self.system_command_output_pane._pane_subtitle = ' '
+ # pylint: enable=protected-access
+ self.window_manager.add_pane(self.system_command_output_pane)
+
async def run(self, test_mode=False):
"""Start the prompt_toolkit UI."""
if test_mode:
- background_log_task = asyncio.create_task(self.log_forever())
+ background_log_task = start_fake_logger(
+ lines=self.user_guide_window.help_text_area.document.lines,
+ log_thread_entry=self._test_mode_log_thread_entry,
+ log_thread_loop=self.test_mode_log_loop,
+ )
# Repl pane has focus by default, if it's hidden switch focus to another
# visible pane.
@@ -853,49 +1191,12 @@ class ConsoleApp:
try:
unused_result = await self.application.run_async(
- set_exception_handler=True)
+ set_exception_handler=True
+ )
finally:
if test_mode:
background_log_task.cancel()
- async def log_forever(self):
- """Test mode async log generator coroutine that runs forever."""
- message_count = 0
- # Sample log line format:
- # Log message [= ] # 100
-
- # Fake module column names.
- module_names = ['APP', 'RADIO', 'BAT', 'USB', 'CPU']
- while True:
- if message_count > 32 or message_count < 2:
- await asyncio.sleep(1)
- bar_size = 10
- position = message_count % bar_size
- bar_content = " " * (bar_size - position - 1) + "="
- if position > 0:
- bar_content = "=".rjust(position) + " " * (bar_size - position)
- new_log_line = 'Log message [{}] # {}'.format(
- bar_content, message_count)
- if message_count % 10 == 0:
- new_log_line += (
- ' Lorem ipsum \033[34m\033[1mdolor sit amet\033[0m'
- ', consectetur '
- 'adipiscing elit.') * 8
- if message_count % 11 == 0:
- new_log_line += ' '
- new_log_line += (
- '[PYTHON] START\n'
- 'In []: import time;\n'
- ' def t(s):\n'
- ' time.sleep(s)\n'
- ' return "t({}) seconds done".format(s)\n\n')
-
- module_name = module_names[message_count % len(module_names)]
- _FAKE_DEVICE_LOG.info(new_log_line,
- extra=dict(extra_metadata_fields=dict(
- module=module_name, file='fake_app.cc')))
- message_count += 1
-
# TODO(tonymd): Remove this alias when not used by downstream projects.
def embed(
@@ -904,6 +1205,10 @@ def embed(
) -> None:
"""PwConsoleEmbed().embed() alias."""
# Import here to avoid circular dependency
- from pw_console.embed import PwConsoleEmbed # pylint: disable=import-outside-toplevel
+ # pylint: disable=import-outside-toplevel
+ from pw_console.embed import PwConsoleEmbed
+
+ # pylint: enable=import-outside-toplevel
+
console = PwConsoleEmbed(*args, **kwargs)
console.embed()
diff --git a/pw_console/py/pw_console/console_log_server.py b/pw_console/py/pw_console/console_log_server.py
new file mode 100644
index 000000000..c1d130fb0
--- /dev/null
+++ b/pw_console/py/pw_console/console_log_server.py
@@ -0,0 +1,78 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Console HTTP Log Server functions."""
+
+import logging
+from pathlib import Path
+import mimetypes
+import http.server
+from typing import Dict, Callable
+
+_LOG = logging.getLogger(__package__)
+
+
+def _start_serving(port: int, handler: Callable) -> bool:
+ try:
+ with http.server.HTTPServer(('', port), handler) as httpd:
+ _LOG.debug('Serving on port %i', port)
+ httpd.serve_forever()
+ return True
+ except OSError:
+ _LOG.debug('Port %i failed.', port)
+ return False
+
+
+def pw_console_http_server(starting_port: int, handler: Callable) -> None:
+ for i in range(100):
+ if _start_serving(starting_port + i, handler):
+ break
+
+
+class ConsoleLogHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
+ """Request handler that serves files from pw_console.html package data."""
+
+ def __init__(self, html_files: Dict[str, str], *args, **kwargs):
+ self.html_files = html_files
+ super().__init__(*args, **kwargs)
+
+ def do_GET(self): # pylint: disable=invalid-name
+ _LOG.debug(
+ '%s: %s',
+ self.client_address[0],
+ self.raw_requestline.decode('utf-8').strip(),
+ )
+
+ path = self.path
+ if path == '/':
+ path = '/index.html'
+
+ if path not in self.html_files:
+ self.send_error(http.server.HTTPStatus.NOT_FOUND, 'File not found')
+ return
+
+ content: str = self.html_files[path].encode('utf-8')
+ content_type = 'application/octet-stream'
+ mime_guess, _ = mimetypes.guess_type(Path(path).name)
+ if mime_guess:
+ content_type = mime_guess
+
+ self.send_response(http.server.HTTPStatus.OK)
+ self.send_header('Content-type', content_type)
+ self.send_header('Content-Length', str(len(content)))
+ self.send_header('Last-Modified', self.date_time_string())
+ self.end_headers()
+ self.wfile.write(content)
+
+ def log_message(self, *args, **kwargs):
+ pass
diff --git a/pw_console/py/pw_console/console_prefs.py b/pw_console/py/pw_console/console_prefs.py
index 1fda908f5..94219cf7b 100644
--- a/pw_console/py/pw_console/console_prefs.py
+++ b/pw_console/py/pw_console/console_prefs.py
@@ -15,14 +15,15 @@
import os
from pathlib import Path
-from typing import Dict, Callable, List, Union
+from typing import Dict, Callable, List, Tuple, Union
from prompt_toolkit.key_binding import KeyBindings
import yaml
-from pw_console.style import get_theme_colors
+from pw_cli.yaml_config_loader_mixin import YamlConfigLoaderMixin
+
+from pw_console.style import get_theme_colors, generate_styles
from pw_console.key_bindings import DEFAULT_KEY_BINDINGS
-from pw_console.yaml_config_loader_mixin import YamlConfigLoaderMixin
_DEFAULT_REPL_HISTORY: Path = Path.home() / '.pw_console_history'
_DEFAULT_SEARCH_HISTORY: Path = Path.home() / '.pw_console_search'
@@ -49,11 +50,11 @@ _DEFAULT_CONFIG = {
'command_runner': {
'width': 80,
'height': 10,
- 'position': {
- 'top': 3
- },
+ 'position': {'top': 3},
},
'key_bindings': DEFAULT_KEY_BINDINGS,
+ 'snippets': {},
+ 'user_snippets': {},
}
_DEFAULT_PROJECT_FILE = Path('$PW_PROJECT_ROOT/.pw_console.yaml')
@@ -69,8 +70,9 @@ class EmptyWindowList(Exception):
"""Exception for window lists with no content."""
-def error_unknown_window(window_title: str,
- existing_pane_titles: List[str]) -> None:
+def error_unknown_window(
+ window_title: str, existing_pane_titles: List[str]
+) -> None:
"""Raise an error when the window config has an unknown title.
If a window title does not already exist on startup it must have a loggers:
@@ -88,17 +90,21 @@ def error_unknown_window(window_title: str,
f'add "duplicate_of: {existing_pane_title_example}" to your config.\n'
'If this is a brand new window, include a "loggers:" section.\n'
'See also: '
- 'https://pigweed.dev/pw_console/docs/user_guide.html#example-config')
+ 'https://pigweed.dev/pw_console/docs/user_guide.html#example-config'
+ )
-def error_empty_window_list(window_list_title: str, ) -> None:
+def error_empty_window_list(
+ window_list_title: str,
+) -> None:
"""Raise an error if a window list is empty."""
raise EmptyWindowList(
f'\n\nError: The window layout heading "{window_list_title}" contains '
'no windows.\n'
'See also: '
- 'https://pigweed.dev/pw_console/docs/user_guide.html#example-config')
+ 'https://pigweed.dev/pw_console/docs/user_guide.html#example-config'
+ )
class ConsolePrefs(YamlConfigLoaderMixin):
@@ -121,6 +127,7 @@ class ConsolePrefs(YamlConfigLoaderMixin):
environment_var='PW_CONSOLE_CONFIG_FILE',
)
+ self._snippet_completions: List[Tuple[str, str]] = []
self.registered_commands = DEFAULT_KEY_BINDINGS
self.registered_commands.update(self.user_key_bindings)
@@ -139,6 +146,9 @@ class ConsolePrefs(YamlConfigLoaderMixin):
def code_theme(self) -> str:
return self._config.get('code_theme', '')
+ def set_code_theme(self, theme_name: str):
+ self._config['code_theme'] = theme_name
+
@property
def swap_light_and_dark(self) -> bool:
return self._config.get('swap_light_and_dark', False)
@@ -187,13 +197,12 @@ class ConsolePrefs(YamlConfigLoaderMixin):
self._config[name] = not existing_setting
@property
- def column_order(self) -> list:
+ def column_order(self) -> List:
return self._config.get('column_order', [])
- def column_style(self,
- column_name: str,
- column_value: str,
- default='') -> str:
+ def column_style(
+ self, column_name: str, column_value: str, default=''
+ ) -> str:
column_colors = self._config.get('column_colors', {})
column_style = default
@@ -205,25 +214,40 @@ class ConsolePrefs(YamlConfigLoaderMixin):
column_style = column_colors[column_name].get('default', default)
# Check for value specific color, otherwise use the default.
column_style = column_colors[column_name].get(
- column_value, column_style)
+ column_value, column_style
+ )
return column_style
+ def pw_console_color_config(self) -> Dict[str, Dict]:
+ column_colors = self._config.get('column_colors', {})
+ theme_styles = generate_styles(self.ui_theme)
+ style_classes = dict(theme_styles.style_rules)
+
+ color_config = {}
+ color_config['classes'] = style_classes
+ color_config['column_values'] = column_colors
+ return {'__pw_console_colors': color_config}
+
@property
def window_column_split_method(self) -> str:
return self._config.get('window_column_split_method', 'vertical')
@property
- def windows(self) -> dict:
+ def windows(self) -> Dict:
return self._config.get('windows', {})
+ def set_windows(self, new_config: Dict) -> None:
+ self._config['windows'] = new_config
+
@property
- def window_column_modes(self) -> list:
+ def window_column_modes(self) -> List:
return list(column_type for column_type in self.windows.keys())
@property
def command_runner_position(self) -> Dict[str, int]:
- position = self._config.get('command_runner',
- {}).get('position', {'top': 3})
+ position = self._config.get('command_runner', {}).get(
+ 'position', {'top': 3}
+ )
return {
key: value
for key, value in position.items()
@@ -243,9 +267,9 @@ class ConsolePrefs(YamlConfigLoaderMixin):
return self._config.get('key_bindings', {})
def current_config_as_yaml(self) -> str:
- yaml_options = dict(sort_keys=True,
- default_style='',
- default_flow_style=False)
+ yaml_options = dict(
+ sort_keys=True, default_style='', default_flow_style=False
+ )
title = {'config_title': 'pw_console'}
text = '\n'
@@ -268,7 +292,8 @@ class ConsolePrefs(YamlConfigLoaderMixin):
window_options = window_dict if window_dict else {}
# Use 'duplicate_of: Title' if it exists, otherwise use the key.
titles.append(
- window_options.get('duplicate_of', window_key_title))
+ window_options.get('duplicate_of', window_key_title)
+ )
return set(titles)
def get_function_keys(self, name: str) -> List:
@@ -278,13 +303,16 @@ class ConsolePrefs(YamlConfigLoaderMixin):
except KeyError as error:
raise KeyError('Unbound key function: {}'.format(name)) from error
- def register_named_key_function(self, name: str,
- default_bindings: List[str]) -> None:
+ def register_named_key_function(
+ self, name: str, default_bindings: List[str]
+ ) -> None:
self.registered_commands[name] = default_bindings
- def register_keybinding(self, name: str, key_bindings: KeyBindings,
- **kwargs) -> Callable:
+ def register_keybinding(
+ self, name: str, key_bindings: KeyBindings, **kwargs
+ ) -> Callable:
"""Apply registered keys for the given named function."""
+
def decorator(handler: Callable) -> Callable:
"`handler` is a callable or Binding."
for keys in self.get_function_keys(name):
@@ -292,3 +320,41 @@ class ConsolePrefs(YamlConfigLoaderMixin):
return handler
return decorator
+
+ @property
+ def snippets(self) -> Dict:
+ return self._config.get('snippets', {})
+
+ @property
+ def user_snippets(self) -> Dict:
+ return self._config.get('user_snippets', {})
+
+ def snippet_completions(self) -> List[Tuple[str, str]]:
+ if self._snippet_completions:
+ return self._snippet_completions
+
+ all_descriptions: List[str] = []
+ all_descriptions.extend(self.user_snippets.keys())
+ all_descriptions.extend(self.snippets.keys())
+ if not all_descriptions:
+ return []
+ max_description_width = max(
+ len(description) for description in all_descriptions
+ )
+
+ all_snippets: List[Tuple[str, str]] = []
+ all_snippets.extend(self.user_snippets.items())
+ all_snippets.extend(self.snippets.items())
+
+ self._snippet_completions = [
+ (
+ description.ljust(max_description_width) + ' : ' +
+ # Flatten linebreaks in the text.
+ ' '.join([line.lstrip() for line in text.splitlines()]),
+ # Pass original text as the completion result.
+ text,
+ )
+ for description, text in all_snippets
+ ]
+
+ return self._snippet_completions
diff --git a/pw_console/py/pw_console/docs/__init__.py b/pw_console/py/pw_console/docs/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_console/py/pw_console/docs/__init__.py
diff --git a/pw_console/py/pw_console/docs/user_guide.rst b/pw_console/py/pw_console/docs/user_guide.rst
index 3f1edcb15..43da6d0d4 100644
--- a/pw_console/py/pw_console/docs/user_guide.rst
+++ b/pw_console/py/pw_console/docs/user_guide.rst
@@ -3,9 +3,10 @@
User Guide
==========
-.. seealso::
+.. tip::
- This guide can be viewed online at:
+ This guide can be viewed while running pw_console under the ``[Help]`` menu
+ or online at:
https://pigweed.dev/pw_console/py/pw_console/docs/user_guide.html
@@ -16,10 +17,14 @@ in a single-window terminal based interface.
Starting the Console
--------------------
-::
+Launching the console may be different if you implement your own custom console
+startup script. To launch pw_console in upstream Pigweed you can run in test
+mode with ``pw-console --test-mode``.
- pw rpc -s localhost:33000 --proto-globs pw_rpc/echo.proto
+.. seealso::
+ Running pw_console for the :ref:`target-stm32f429i-disc1-stm32cube` and
+ :ref:`target-host-device-simulator` targets.
Exiting
~~~~~~~
@@ -578,6 +583,8 @@ loaded later in the startup sequence.
.. code-block:: yaml
+ ---
+ config_title: pw_console
ui_theme: nord
code_theme: pigweed-code
swap_light_and_dark: False
@@ -778,6 +785,23 @@ Example Config
log-pane.shift-line-to-center:
- z z
+ # Python Repl Snippets (Project owned)
+ snippets:
+ Count Ten Times: |
+ for i in range(10):
+ print(i)
+ Local Variables: |
+ locals()
+
+ # Python Repl Snippets (User owned)
+ user_snippets:
+ Pretty print format function: |
+ import pprint
+ _pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
+ Global variables: |
+ globals()
+
+
Changing Keyboard Shortcuts
---------------------------
diff --git a/pw_console/py/pw_console/embed.py b/pw_console/py/pw_console/embed.py
index 5ef83f065..47cd86d60 100644
--- a/pw_console/py/pw_console/embed.py
+++ b/pw_console/py/pw_console/embed.py
@@ -16,15 +16,21 @@
import asyncio
import logging
from pathlib import Path
-from typing import Any, Dict, List, Iterable, Optional, Union
+from typing import Any, Dict, List, Iterable, Optional, Tuple, Union
from prompt_toolkit.completion import WordCompleter
from pw_console.console_app import ConsoleApp
from pw_console.get_pw_console_app import PW_CONSOLE_APP_CONTEXTVAR
from pw_console.plugin_mixin import PluginMixin
-import pw_console.python_logging
-from pw_console.widgets import WindowPane, WindowPaneToolbar
+from pw_console.python_logging import (
+ setup_python_logging as pw_console_setup_python_logging,
+)
+from pw_console.widgets import (
+ FloatingWindowPane,
+ WindowPane,
+ WindowPaneToolbar,
+)
def _set_console_app_instance(plugin: Any, console_app: ConsoleApp) -> None:
@@ -38,16 +44,19 @@ class PwConsoleEmbed:
"""Embed class for customizing the console before startup."""
# pylint: disable=too-many-instance-attributes
- def __init__(self,
- global_vars=None,
- local_vars=None,
- loggers: Optional[Union[Dict[str, Iterable[logging.Logger]],
- Iterable]] = None,
- test_mode=False,
- repl_startup_message: Optional[str] = None,
- help_text: Optional[str] = None,
- app_title: Optional[str] = None,
- config_file_path: Optional[Union[str, Path]] = None) -> None:
+ def __init__(
+ self,
+ global_vars=None,
+ local_vars=None,
+ loggers: Optional[
+ Union[Dict[str, Iterable[logging.Logger]], Iterable]
+ ] = None,
+ test_mode=False,
+ repl_startup_message: Optional[str] = None,
+ help_text: Optional[str] = None,
+ app_title: Optional[str] = None,
+ config_file_path: Optional[Union[str, Path]] = None,
+ ) -> None:
"""Call this to embed pw console at the call point within your program.
Example usage:
@@ -65,7 +74,7 @@ class PwConsoleEmbed:
loggers={
'Host Logs': [
logging.getLogger(__package__),
- logging.getLogger(__file__),
+ logging.getLogger(__name__),
],
'Device Logs': [
logging.getLogger('usb_gadget'),
@@ -115,8 +124,9 @@ class PwConsoleEmbed:
self.repl_startup_message = repl_startup_message
self.help_text = help_text
self.app_title = app_title
- self.config_file_path = Path(
- config_file_path) if config_file_path else None
+ self.config_file_path = (
+ Path(config_file_path) if config_file_path else None
+ )
self.console_app: Optional[ConsoleApp] = None
self.extra_completers: List = []
@@ -124,6 +134,7 @@ class PwConsoleEmbed:
self.setup_python_logging_called = False
self.hidden_by_default_windows: List[str] = []
self.window_plugins: List[WindowPane] = []
+ self.floating_window_plugins: List[Tuple[FloatingWindowPane, Dict]] = []
self.top_toolbar_plugins: List[WindowPaneToolbar] = []
self.bottom_toolbar_plugins: List[WindowPaneToolbar] = []
@@ -135,6 +146,42 @@ class PwConsoleEmbed:
"""
self.window_plugins.append(window_pane)
+ def add_floating_window_plugin(
+ self, window_pane: FloatingWindowPane, **float_args
+ ) -> None:
+ """Include a custom floating window pane plugin.
+
+ This adds a FloatingWindowPane class to the pw_console UI. The first
+ argument should be the window to add and the remaining keyword arguments
+ are passed to the prompt_toolkit Float() class. This allows positioning
+ of the floating window. By default the floating window will be
+ centered. To anchor the window to a side or corner of the screen set the
+ ``left``, ``right``, ``top``, or ``bottom`` keyword args.
+
+ For example:
+
+ .. code-block:: python
+
+ from pw_console import PwConsoleEmbed
+
+ console = PwConsoleEmbed(...)
+ my_plugin = MyPlugin()
+ # Anchor this floating window 2 rows away from the top and 4 columns
+ # away from the left edge of the screen.
+ console.add_floating_window_plugin(my_plugin, top=2, left=4)
+
+ See all possible keyword args in the prompt_toolkit documentation:
+ https://python-prompt-toolkit.readthedocs.io/en/stable/pages/reference.html#prompt_toolkit.layout.Float
+
+ Args:
+ window_pane: Any instance of the FloatingWindowPane class.
+ left: Distance to the left edge of the screen
+ right: Distance to the right edge of the screen
+ top: Distance to the top edge of the screen
+ bottom: Distance to the bottom edge of the screen
+ """
+ self.floating_window_plugins.append((window_pane, float_args))
+
def add_top_toolbar(self, toolbar: WindowPaneToolbar) -> None:
"""Include a toolbar plugin to display on the top of the screen.
@@ -157,9 +204,9 @@ class PwConsoleEmbed:
"""
self.bottom_toolbar_plugins.append(toolbar)
- def add_sentence_completer(self,
- word_meta_dict: Dict[str, str],
- ignore_case=True) -> None:
+ def add_sentence_completer(
+ self, word_meta_dict: Dict[str, str], ignore_case=True
+ ) -> None:
"""Include a custom completer that matches on the entire repl input.
Args:
@@ -196,16 +243,20 @@ class PwConsoleEmbed:
elif isinstance(self.loggers, dict):
for window_title, logger_instances in self.loggers.items():
window_pane = self.console_app.add_log_handler(
- window_title, logger_instances)
-
- if (window_pane and window_pane.pane_title()
- in self.hidden_by_default_windows):
+ window_title, logger_instances
+ )
+
+ if (
+ window_pane
+ and window_pane.pane_title()
+ in self.hidden_by_default_windows
+ ):
window_pane.show_pane = False
def setup_python_logging(
self,
last_resort_filename: Optional[str] = None,
- loggers_with_no_propagation: Optional[Iterable[logging.Logger]] = None
+ loggers_with_no_propagation: Optional[Iterable[logging.Logger]] = None,
) -> None:
"""Setup friendly logging for full-screen prompt_toolkit applications.
@@ -232,15 +283,16 @@ class PwConsoleEmbed:
logger.
"""
self.setup_python_logging_called = True
- pw_console.python_logging.setup_python_logging(
- last_resort_filename, loggers_with_no_propagation)
+ pw_console_setup_python_logging(
+ last_resort_filename, loggers_with_no_propagation
+ )
def hide_windows(self, *window_titles) -> None:
"""Hide window panes specified by title on console startup."""
for window_title in window_titles:
self.hidden_by_default_windows.append(window_title)
- def embed(self) -> None:
+ def embed(self, override_window_config: Optional[Dict] = None) -> None:
"""Start the console."""
# Create the ConsoleApp instance.
@@ -251,6 +303,7 @@ class PwConsoleEmbed:
help_text=self.help_text,
app_title=self.app_title,
extra_completers=self.extra_completers,
+ floating_window_plugins=self.floating_window_plugins,
)
PW_CONSOLE_APP_CONTEXTVAR.set(self.console_app) # type: ignore
# Setup Python logging and log panes.
@@ -274,6 +327,10 @@ class PwConsoleEmbed:
_set_console_app_instance(toolbar, self.console_app)
self.console_app.window_manager.add_bottom_toolbar(toolbar)
+ # Init floating window plugins.
+ for floating_window, _ in self.floating_window_plugins:
+ _set_console_app_instance(floating_window, self.console_app)
+
# Rebuild prompt_toolkit containers, menu items, and help content with
# any new plugins added above.
self.console_app.refresh_layout()
@@ -282,6 +339,8 @@ class PwConsoleEmbed:
if self.config_file_path:
self.console_app.load_clean_config(self.config_file_path)
+ if override_window_config:
+ self.console_app.prefs.set_windows(override_window_config)
self.console_app.apply_window_config()
# Hide the repl pane if it's in the hidden windows list.
@@ -303,5 +362,6 @@ class PwConsoleEmbed:
toolbar.plugin_start()
# Start the prompt_toolkit UI app.
- asyncio.run(self.console_app.run(test_mode=self.test_mode),
- debug=self.test_mode)
+ asyncio.run(
+ self.console_app.run(test_mode=self.test_mode), debug=self.test_mode
+ )
diff --git a/pw_console/py/pw_console/filter_toolbar.py b/pw_console/py/pw_console/filter_toolbar.py
index df37669ec..9018180b0 100644
--- a/pw_console/py/pw_console/filter_toolbar.py
+++ b/pw_console/py/pw_console/filter_toolbar.py
@@ -28,9 +28,14 @@ from prompt_toolkit.layout import (
)
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
-import pw_console.widgets.checkbox
-import pw_console.widgets.mouse_handlers
-import pw_console.style
+from pw_console.style import (
+ get_button_style,
+ get_toolbar_style,
+)
+from pw_console.widgets import (
+ mouse_handlers,
+ to_keybind_indicator,
+)
if TYPE_CHECKING:
from pw_console.log_pane import LogPane
@@ -41,8 +46,7 @@ class FilterToolbar(ConditionalContainer):
TOOLBAR_HEIGHT = 1
- def mouse_handler_delete_filter(self, filter_text,
- mouse_event: MouseEvent):
+ def mouse_handler_delete_filter(self, filter_text, mouse_event: MouseEvent):
"""Delete the given log filter."""
if mouse_event.event_type == MouseEventType.MOUSE_UP:
self.log_pane.log_view.delete_filter(filter_text)
@@ -55,7 +59,7 @@ class FilterToolbar(ConditionalContainer):
space = ('', ' ')
fragments = [('class:filter-bar-title', ' Filters '), separator]
- button_style = pw_console.style.get_button_style(self.log_pane)
+ button_style = get_button_style(self.log_pane)
for filter_text, log_filter in self.log_pane.log_view.filters.items():
fragments.append(('class:filter-bar-delimiter', '<'))
@@ -64,17 +68,21 @@ class FilterToolbar(ConditionalContainer):
fragments.append(('class:filter-bar-setting', 'NOT '))
if log_filter.field:
- fragments.append(
- ('class:filter-bar-setting', log_filter.field))
+ fragments.append(('class:filter-bar-setting', log_filter.field))
fragments.append(space)
fragments.append(('', filter_text))
fragments.append(space)
fragments.append(
- (button_style + ' class:filter-bar-delete', ' (X) ',
- functools.partial(self.mouse_handler_delete_filter,
- filter_text))) # type: ignore
+ (
+ button_style + ' class:filter-bar-delete',
+ ' (X) ',
+ functools.partial(
+ self.mouse_handler_delete_filter, filter_text
+ ),
+ )
+ ) # type: ignore
fragments.append(('class:filter-bar-delimiter', '>'))
fragments.append(separator)
@@ -83,36 +91,42 @@ class FilterToolbar(ConditionalContainer):
def get_center_fragments(self):
"""Return formatted text tokens for display."""
clear_filters = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self.log_pane.log_view.clear_filters)
+ mouse_handlers.on_click,
+ self.log_pane.log_view.clear_filters,
+ )
- button_style = pw_console.style.get_button_style(self.log_pane)
+ button_style = get_button_style(self.log_pane)
- return pw_console.widgets.checkbox.to_keybind_indicator(
+ return to_keybind_indicator(
'Ctrl-Alt-r',
'Clear Filters',
clear_filters,
- base_style=button_style)
+ base_style=button_style,
+ )
def __init__(self, log_pane: 'LogPane'):
self.log_pane = log_pane
left_bar_control = FormattedTextControl(self.get_left_fragments)
- left_bar_window = Window(content=left_bar_control,
- align=WindowAlign.LEFT,
- dont_extend_width=True)
+ left_bar_window = Window(
+ content=left_bar_control,
+ align=WindowAlign.LEFT,
+ dont_extend_width=True,
+ )
center_bar_control = FormattedTextControl(self.get_center_fragments)
- center_bar_window = Window(content=center_bar_control,
- align=WindowAlign.LEFT,
- dont_extend_width=False)
+ center_bar_window = Window(
+ content=center_bar_control,
+ align=WindowAlign.LEFT,
+ dont_extend_width=False,
+ )
super().__init__(
VSplit(
[
left_bar_window,
center_bar_window,
],
- style=functools.partial(pw_console.style.get_toolbar_style,
- self.log_pane,
- dim=True),
+ style=functools.partial(
+ get_toolbar_style, self.log_pane, dim=True
+ ),
height=1,
align=HorizontalAlign.LEFT,
),
diff --git a/pw_console/py/pw_console/help_window.py b/pw_console/py/pw_console/help_window.py
index d8ca7ad55..d90a234db 100644
--- a/pw_console/py/pw_console/help_window.py
+++ b/pw_console/py/pw_console/help_window.py
@@ -14,10 +14,10 @@
"""Help window container class."""
import functools
+import importlib.resources
import inspect
import logging
-from pathlib import Path
-from typing import Dict, TYPE_CHECKING
+from typing import Dict, Optional, TYPE_CHECKING
from prompt_toolkit.document import Document
from prompt_toolkit.filters import Condition
@@ -37,13 +37,22 @@ from prompt_toolkit.widgets import Box, TextArea
from pygments.lexers.markup import RstLexer # type: ignore
from pygments.lexers.data import YamlLexer # type: ignore
-import pw_console.widgets.mouse_handlers
+
+from pw_console.style import (
+ get_pane_indicator,
+)
+from pw_console.widgets import (
+ mouse_handlers,
+ to_keybind_indicator,
+)
if TYPE_CHECKING:
from pw_console.console_app import ConsoleApp
_LOG = logging.getLogger(__package__)
+_PW_CONSOLE_MODULE = 'pw_console'
+
def _longest_line_length(text):
"""Return the longest line in the given text."""
@@ -78,24 +87,30 @@ class HelpWindow(ConditionalContainer):
"""Close the current dialog window."""
self.toggle_display()
- @register('help-window.copy-all', key_bindings)
- def _copy_all(_event: KeyPressEvent) -> None:
- """Close the current dialog window."""
- self.copy_all_text()
+ if not self.disable_ctrl_c:
+
+ @register('help-window.copy-all', key_bindings)
+ def _copy_all(_event: KeyPressEvent) -> None:
+ """Close the current dialog window."""
+ self.copy_all_text()
help_text_area.control.key_bindings = key_bindings
return help_text_area
- def __init__(self,
- application: 'ConsoleApp',
- preamble: str = '',
- additional_help_text: str = '',
- title: str = '') -> None:
+ def __init__(
+ self,
+ application: 'ConsoleApp',
+ preamble: str = '',
+ additional_help_text: str = '',
+ title: str = '',
+ disable_ctrl_c: bool = False,
+ ) -> None:
# Dict containing key = section title and value = list of key bindings.
self.application: 'ConsoleApp' = application
self.show_window: bool = False
self.help_text_sections: Dict[str, Dict] = {}
self._pane_title: str = title
+ self.disable_ctrl_c = disable_ctrl_c
# Tracks the last focused container, to enable restoring focus after
# closing the dialog.
@@ -106,8 +121,11 @@ class HelpWindow(ConditionalContainer):
self.additional_help_text: str = additional_help_text
self.help_text: str = ''
- self.max_additional_help_text_width: int = (_longest_line_length(
- self.additional_help_text) if additional_help_text else 0)
+ self.max_additional_help_text_width: int = (
+ _longest_line_length(self.additional_help_text)
+ if additional_help_text
+ else 0
+ )
self.max_description_width: int = 0
self.max_key_list_width: int = 0
self.max_line_length: int = 0
@@ -115,35 +133,47 @@ class HelpWindow(ConditionalContainer):
self.help_text_area: TextArea = self._create_help_text_area()
close_mouse_handler = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self.toggle_display)
+ mouse_handlers.on_click, self.toggle_display
+ )
copy_mouse_handler = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self.copy_all_text)
+ mouse_handlers.on_click, self.copy_all_text
+ )
toolbar_padding = 1
toolbar_title = ' ' * toolbar_padding
toolbar_title += self.pane_title()
buttons = []
+ if not self.disable_ctrl_c:
+ buttons.extend(
+ to_keybind_indicator(
+ 'Ctrl-c',
+ 'Copy All',
+ copy_mouse_handler,
+ base_style='class:toolbar-button-active',
+ )
+ )
+ buttons.append(('', ' '))
+
buttons.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
- 'Ctrl-c',
- 'Copy All',
- copy_mouse_handler,
- base_style='class:toolbar-button-active'))
- buttons.append(('', ' '))
- buttons.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
'q',
'Close',
close_mouse_handler,
- base_style='class:toolbar-button-active'))
+ base_style='class:toolbar-button-active',
+ )
+ )
top_toolbar = VSplit(
[
Window(
content=FormattedTextControl(
# [('', toolbar_title)]
- functools.partial(pw_console.style.get_pane_indicator,
- self, toolbar_title)),
+ functools.partial(
+ get_pane_indicator,
+ self,
+ toolbar_title,
+ )
+ ),
align=WindowAlign.LEFT,
dont_extend_width=True,
),
@@ -162,17 +192,19 @@ class HelpWindow(ConditionalContainer):
style='class:toolbar_active',
)
- self.container = HSplit([
- top_toolbar,
- Box(
- body=DynamicContainer(lambda: self.help_text_area),
- padding=Dimension(preferred=1, max=1),
- padding_bottom=0,
- padding_top=0,
- char=' ',
- style='class:frame.border', # Same style used for Frame.
- ),
- ])
+ self.container = HSplit(
+ [
+ top_toolbar,
+ Box(
+ body=DynamicContainer(lambda: self.help_text_area),
+ padding=Dimension(preferred=1, max=1),
+ padding_bottom=0,
+ padding_top=0,
+ char=' ',
+ style='class:frame.border', # Same style used for Frame.
+ ),
+ ]
+ )
super().__init__(
self.container,
@@ -196,7 +228,8 @@ class HelpWindow(ConditionalContainer):
def copy_all_text(self):
"""Copy all text in the Python input to the system clipboard."""
self.application.application.clipboard.set_text(
- self.help_text_area.buffer.text)
+ self.help_text_area.buffer.text
+ )
def toggle_display(self):
"""Toggle visibility of this help window."""
@@ -227,26 +260,30 @@ class HelpWindow(ConditionalContainer):
scrollbar_width = 1
desired_width = self.max_line_length + (
- left_side_frame_and_padding_width +
- right_side_frame_and_padding_width + scrollbar_padding +
- scrollbar_width)
+ left_side_frame_and_padding_width
+ + right_side_frame_and_padding_width
+ + scrollbar_padding
+ + scrollbar_width
+ )
desired_width = max(60, desired_width)
window_manager_width = (
- self.application.window_manager.current_window_manager_width)
+ self.application.window_manager.current_window_manager_width
+ )
if not window_manager_width:
window_manager_width = 80
return min(desired_width, window_manager_width)
def load_user_guide(self):
- rstdoc = Path(__file__).parent / 'docs/user_guide.rst'
+ rstdoc_text = importlib.resources.read_text(
+ f'{_PW_CONSOLE_MODULE}.docs', 'user_guide.rst'
+ )
max_line_length = 0
rst_text = ''
- with rstdoc.open() as rstfile:
- for line in rstfile.readlines():
- if 'https://' not in line and len(line) > max_line_length:
- max_line_length = len(line)
- rst_text += line
+ for line in rstdoc_text.splitlines():
+ if 'https://' not in line and len(line) > max_line_length:
+ max_line_length = len(line)
+ rst_text += line + '\n'
self.max_line_length = max_line_length
self.help_text_area = self._create_help_text_area(
@@ -266,12 +303,21 @@ class HelpWindow(ConditionalContainer):
text=content,
)
- def generate_help_text(self):
+ def set_help_text(
+ self, text: str, lexer: Optional[PygmentsLexer] = None
+ ) -> None:
+ self.help_text_area = self._create_help_text_area(
+ lexer=lexer,
+ text=text,
+ )
+ self._update_help_text_area(text)
+
+ def generate_keybind_help_text(self) -> str:
"""Generate help text based on added key bindings."""
template = self.application.get_template('keybind_list.jinja')
- self.help_text = template.render(
+ text = template.render(
sections=self.help_text_sections,
max_additional_help_text_width=self.max_additional_help_text_width,
max_description_width=self.max_description_width,
@@ -280,14 +326,19 @@ class HelpWindow(ConditionalContainer):
additional_help_text=self.additional_help_text,
)
+ self._update_help_text_area(text)
+ return text
+
+ def _update_help_text_area(self, text: str) -> None:
+ self.help_text = text
+
# Find the longest line in the rendered template.
self.max_line_length = _longest_line_length(self.help_text)
# Replace the TextArea content.
- self.help_text_area.buffer.document = Document(text=self.help_text,
- cursor_position=0)
-
- return self.help_text
+ self.help_text_area.buffer.document = Document(
+ text=self.help_text, cursor_position=0
+ )
def add_custom_keybinds_help_text(self, section_name, key_bindings: Dict):
"""Add hand written key_bindings."""
@@ -319,11 +370,13 @@ class HelpWindow(ConditionalContainer):
# Get the existing list of keys for this function or make a new one.
key_list = self.help_text_sections[section_name].get(
- description, list())
+ description, list()
+ )
# Save the name of the key e.g. F1, q, ControlQ, ControlUp
key_name = ' '.join(
- [getattr(key, 'name', str(key)) for key in binding.keys])
+ [getattr(key, 'name', str(key)) for key in binding.keys]
+ )
key_name = key_name.replace('Control', 'Ctrl-')
key_name = key_name.replace('Shift', 'Shift-')
key_name = key_name.replace('Escape ', 'Alt-')
diff --git a/pw_console/py/pw_console/html/__init__.py b/pw_console/py/pw_console/html/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_console/py/pw_console/html/__init__.py
diff --git a/pw_console/py/pw_console/html/index.html b/pw_console/py/pw_console/html/index.html
new file mode 100644
index 000000000..5d7a21b09
--- /dev/null
+++ b/pw_console/py/pw_console/html/index.html
@@ -0,0 +1,37 @@
+<!--
+Copyright 2022 The Pigweed Authors
+
+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
+
+ https://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.
+-->
+<head>
+ <link href="./style.css" rel="stylesheet" />
+</head>
+
+<body>
+ <div class="table-container">
+ <div class="log-header">
+ <div class="log-entry">
+ <span class="timestamp">Time</span>
+ <span class="level">Level</span>
+ <span class="module">Module</span>
+ <span class="time">Timestamp</span>
+ <span class="keys">Keys</span>
+ <span class="msg">Message</span>
+ </div>
+ </div>
+ <div class="log-container"></div>
+ </div>
+
+ <script src="https://unpkg.com/virtualized-list@2.2.0/umd/virtualized-list.min.js"></script>
+ <script src="./main.js"></script>
+</body>
diff --git a/pw_console/py/pw_console/html/main.js b/pw_console/py/pw_console/html/main.js
new file mode 100644
index 000000000..d08d019ed
--- /dev/null
+++ b/pw_console/py/pw_console/html/main.js
@@ -0,0 +1,261 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+var VirtualizedList = window.VirtualizedList.default;
+const rowHeight = 30;
+
+function formatDate(dt) {
+ function pad2(n) {
+ return (n < 10 ? '0' : '') + n;
+ }
+
+ return dt.getFullYear() + pad2(dt.getMonth() + 1) + pad2(dt.getDate()) + ' ' +
+ pad2(dt.getHours()) + ':' + pad2(dt.getMinutes()) + ':' +
+ pad2(dt.getSeconds());
+}
+
+let data = [];
+function clearLogs() {
+ data = [{
+ 'message': 'Logs started',
+ 'levelno': 20,
+ time: formatDate(new Date()),
+ 'levelname': '\u001b[35m\u001b[1mINF\u001b[0m',
+ 'args': [],
+ 'fields': {'module': '', 'file': '', 'timestamp': '', 'keys': ''}
+ }];
+}
+clearLogs();
+
+let nonAdditionalDataFields =
+ ['_hosttime', 'levelname', 'levelno', 'args', 'fields', 'message', 'time'];
+let additionalHeaders = [];
+function updateHeadersFromData(data) {
+ let dirty = false;
+ Object.keys(data).forEach((columnName) => {
+ if (nonAdditionalDataFields.indexOf(columnName) === -1 &&
+ additionalHeaders.indexOf(columnName) === -1) {
+ additionalHeaders.push(columnName);
+ dirty = true;
+ }
+ });
+ Object.keys(data.fields || {}).forEach((columnName) => {
+ if (nonAdditionalDataFields.indexOf(columnName) === -1 &&
+ additionalHeaders.indexOf(columnName) === -1) {
+ additionalHeaders.push(columnName);
+ dirty = true;
+ }
+ });
+
+ const headerDOM = document.querySelector('.log-header');
+ if (dirty) {
+ headerDOM.innerHTML = `
+ <span class="_hosttime">Time</span>
+ <span class="level">Level</span>
+ ${
+ additionalHeaders
+ .map((key) => `
+ <span class="${key}">${key}</span>
+ `).join('\n')}
+ <span class="msg">Message</span>`
+ }
+
+ // Also update column widths to match actual row.
+ const headerChildren = Array.from(headerDOM.children);
+
+ const firstRow = document.querySelector('.log-container .log-entry');
+ const firstRowChildren = Array.from(firstRow.children);
+ headerChildren.forEach((col, index) => {
+ if (firstRowChildren[index]) {
+ col.setAttribute(
+ 'style',
+ `width:${firstRowChildren[index].getBoundingClientRect().width}`);
+ col.setAttribute('title', col.innerText);
+ }
+ })
+}
+
+function getUrlHashParameter(param) {
+ var params = getUrlHashParameters();
+ return params[param];
+}
+
+function getUrlHashParameters() {
+ var sPageURL = window.location.hash;
+ if (sPageURL)
+ sPageURL = sPageURL.split('#')[1];
+ var pairs = sPageURL.split('&');
+ var object = {};
+ pairs.forEach(function(pair, i) {
+ pair = pair.split('=');
+ if (pair[0] !== '')
+ object[pair[0]] = pair[1];
+ });
+ return object;
+}
+let currentTheme = {};
+let defaultLogStyleRule = 'color: #ffffff;';
+let columnStyleRules = {};
+let defaultColumnStyles = [];
+let logLevelStyles = {};
+const logLevelToString = {
+ 10: 'DBG',
+ 20: 'INF',
+ 21: 'OUT',
+ 30: 'WRN',
+ 40: 'ERR',
+ 50: 'CRT',
+ 70: 'FTL'
+};
+
+function setCurrentTheme(newTheme) {
+ currentTheme = newTheme;
+ defaultLogStyleRule = parseStyle(newTheme.default);
+ document.querySelector('body').setAttribute('style', defaultLogStyleRule);
+ // Apply default font styles to columns
+ let styles = [];
+ Object.keys(newTheme).forEach(key => {
+ if (key.startsWith('log-table-column-')) {
+ styles.push(newTheme[key]);
+ }
+ if (key.startsWith('log-level-')) {
+ logLevelStyles[parseInt(key.replace('log-level-', ''))] =
+ parseStyle(newTheme[key]);
+ }
+ });
+ defaultColumnStyles = styles;
+}
+
+function parseStyle(rule) {
+ const ruleList = rule.split(' ');
+ let outputStyle = ruleList.map(fragment => {
+ if (fragment.startsWith('bg:')) {
+ return `background-color: ${fragment.replace('bg:', '')}`
+ } else if (fragment === 'bold') {
+ return `font-weight: bold`;
+ } else if (fragment === 'underline') {
+ return `text-decoration: underline`;
+ } else if (fragment.startsWith('#')) {
+ return `color: ${fragment}`;
+ }
+ });
+ return outputStyle.join(';')
+}
+
+function applyStyling(data, applyColors = false) {
+ let colIndex = 0;
+ Object.keys(data).forEach(key => {
+ if (columnStyleRules[key] && typeof data[key] === 'string') {
+ Object.keys(columnStyleRules[key]).forEach(token => {
+ data[key] = data[key].replaceAll(
+ token,
+ `<span
+ style="${defaultLogStyleRule};${
+ applyColors ? (defaultColumnStyles
+ [colIndex % defaultColumnStyles.length]) :
+ ''};${parseStyle(columnStyleRules[key][token])};">
+ ${token}
+ </span>`);
+ });
+ } else if (key === 'fields') {
+ data[key] = applyStyling(data.fields, true);
+ }
+ if (applyColors) {
+ data[key] = `<span
+ style="${
+ parseStyle(
+ defaultColumnStyles[colIndex % defaultColumnStyles.length])}">
+ ${data[key]}
+ </span>`;
+ }
+ colIndex++;
+ });
+ return data;
+}
+
+(function() {
+const container = document.querySelector('.log-container');
+const height = window.innerHeight - 50
+let follow = true;
+// Initialize our VirtualizedList
+var virtualizedList = new VirtualizedList(container, {
+ height,
+ rowCount: data.length,
+ rowHeight: rowHeight,
+ estimatedRowHeight: rowHeight,
+ renderRow: (index) => {
+ const element = document.createElement('div');
+ element.classList.add('log-entry');
+ element.setAttribute('style', `height: ${rowHeight}px;`);
+ const logData = data[index];
+ element.innerHTML = `
+ <span class="time">${logData.time}</span>
+ <span class="level" style="${logLevelStyles[logData.levelno] || ''}">${
+ logLevelToString[logData.levelno]}</span>
+ ${
+ additionalHeaders
+ .map(
+ (key) => `
+ <span class="${key}">${
+ logData[key] || logData.fields[key] || ''}</span>
+ `).join('\n')}
+ <span class="msg">${logData.message}</span>
+ `;
+ return element;
+ },
+ initialIndex: 0,
+ onScroll: (scrollTop, event) => {
+ const offset =
+ virtualizedList._sizeAndPositionManager.getUpdatedOffsetForIndex({
+ containerSize: height,
+ targetIndex: data.length - 1,
+ });
+
+ if (scrollTop < offset) {
+ follow = false;
+ } else {
+ follow = true;
+ }
+ }
+});
+
+const port = getUrlHashParameter('ws')
+const hostname = location.hostname || '127.0.0.1';
+var ws = new WebSocket(`ws://${hostname}:${port}/`);
+ws.onmessage = function(event) {
+ let dataObj;
+ try {
+ dataObj = JSON.parse(event.data);
+ } catch (e) {
+ }
+ if (!dataObj)
+ return;
+
+ if (dataObj.__pw_console_colors) {
+ const colors = dataObj.__pw_console_colors;
+ setCurrentTheme(colors.classes);
+ if (colors.column_values) {
+ columnStyleRules = {...colors.column_values};
+ }
+ } else {
+ const currentData = {...dataObj, time: formatDate(new Date())};
+ updateHeadersFromData(currentData);
+ data.push(applyStyling(currentData));
+ virtualizedList.setRowCount(data.length);
+ if (follow) {
+ virtualizedList.scrollToIndex(data.length - 1);
+ }
+ }
+};
+})();
diff --git a/pw_console/py/pw_console/html/style.css b/pw_console/py/pw_console/html/style.css
new file mode 100644
index 000000000..2c92a48d0
--- /dev/null
+++ b/pw_console/py/pw_console/html/style.css
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2022 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+body {
+ background-color: rgb(46, 46, 46);
+ color: #ffffff;
+ overflow: hidden;
+ margin: 0;
+}
+
+.table-container {
+ display: table;
+ width: 100%;
+ border-spacing: 30px 0px;
+}
+
+.log-header {
+ font-size: 18px;
+ font-family: monospace;
+}
+
+.log-container {
+ width: 100%;
+ height: calc(100vh - 50px);
+ overflow-y: auto;
+ border-top: 1px solid #DDD;
+ font-size: 18px;
+ font-family: monospace;
+}
+
+.log-header {
+ width: 100%;
+ font-weight: bold;
+ display: table-row;
+}
+
+.log-container .row>span {
+ display: table-cell;
+ padding: 20px 18px;
+
+}
+
+.log-header>span {
+ text-transform: capitalize;
+ overflow: hidden;
+ display: inline-block;
+ margin-left: 30px;
+}
+
+.log-entry {
+ display: table-row;
+}
+
+.log-entry>span {
+ display: table-cell;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.log-entry .msg {
+ flex: 1;
+}
diff --git a/pw_console/py/pw_console/key_bindings.py b/pw_console/py/pw_console/key_bindings.py
index c44d8663c..3db259217 100644
--- a/pw_console/py/pw_console/key_bindings.py
+++ b/pw_console/py/pw_console/key_bindings.py
@@ -24,9 +24,8 @@ from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous
from prompt_toolkit.key_binding.key_bindings import Binding
-import pw_console.pw_ptpython_repl
-__all__ = ('create_key_bindings', )
+__all__ = ('create_key_bindings',)
_LOG = logging.getLogger(__package__)
@@ -46,6 +45,7 @@ DEFAULT_KEY_BINDINGS: Dict[str, List[str]] = {
'log-pane.remove-duplicated-log-pane': ['delete'],
'log-pane.clear-history': ['C'],
'log-pane.toggle-follow': ['f'],
+ 'log-pane.toggle-web-browser': ['O'],
'log-pane.move-cursor-up': ['up', 'k'],
'log-pane.move-cursor-down': ['down', 'j'],
'log-pane.visual-select-up': ['s-up'],
@@ -73,8 +73,9 @@ DEFAULT_KEY_BINDINGS: Dict[str, List[str]] = {
'window-manager.move-pane-down': ['escape c-up'], # Alt-Ctrl-
'window-manager.move-pane-up': ['escape c-down'], # Alt-Ctrl-
'window-manager.enlarge-pane': ['escape ='], # Alt-= (mnemonic: Alt Plus)
- 'window-manager.shrink-pane':
- ['escape -'], # Alt-minus (mnemonic: Alt Minus)
+ 'window-manager.shrink-pane': [
+ 'escape -'
+ ], # Alt-minus (mnemonic: Alt Minus)
'window-manager.shrink-split': ['escape ,'], # Alt-, (mnemonic: Alt <)
'window-manager.enlarge-split': ['escape .'], # Alt-. (mnemonic: Alt >)
'window-manager.focus-prev-pane': ['escape c-p'], # Ctrl-Alt-p
@@ -84,6 +85,8 @@ DEFAULT_KEY_BINDINGS: Dict[str, List[str]] = {
'python-repl.copy-all-output': ['escape c-c'],
'python-repl.copy-clear-or-cancel': ['c-c'],
'python-repl.paste-to-input': ['c-v'],
+ 'python-repl.history-search': ['c-r'],
+ 'python-repl.snippet-search': ['c-t'],
'save-as-dialog.cancel': ['escape', 'c-c', 'c-d'],
'quit-dialog.no': ['escape', 'n', 'c-c'],
'quit-dialog.yes': ['y', 'c-d'],
@@ -106,9 +109,11 @@ def create_key_bindings(console_app) -> KeyBindings:
key_bindings = KeyBindings()
register = console_app.prefs.register_keybinding
- @register('global.open-user-guide',
- key_bindings,
- filter=Condition(lambda: not console_app.modal_window_is_open()))
+ @register(
+ 'global.open-user-guide',
+ key_bindings,
+ filter=Condition(lambda: not console_app.modal_window_is_open()),
+ )
def show_help(event):
"""Toggle user guide window."""
console_app.user_guide_window.toggle_display()
@@ -116,9 +121,11 @@ def create_key_bindings(console_app) -> KeyBindings:
# F2 is ptpython settings
# F3 is ptpython history
- @register('global.open-menu-search',
- key_bindings,
- filter=Condition(lambda: not console_app.modal_window_is_open()))
+ @register(
+ 'global.open-menu-search',
+ key_bindings,
+ filter=Condition(lambda: not console_app.modal_window_is_open()),
+ )
def show_command_runner(event):
"""Open command runner window."""
console_app.open_command_runner_main_menu()
@@ -136,9 +143,11 @@ def create_key_bindings(console_app) -> KeyBindings:
# Bindings for when the ReplPane input field is in focus.
# These are hidden from help window global keyboard shortcuts since the
# method names end with `_hidden`.
- @register('python-repl.copy-clear-or-cancel',
- key_bindings,
- filter=has_focus(console_app.pw_ptpython_repl))
+ @register(
+ 'python-repl.copy-clear-or-cancel',
+ key_bindings,
+ filter=has_focus(console_app.pw_ptpython_repl),
+ )
def handle_ctrl_c_hidden(event):
"""Reset the python repl on Ctrl-c"""
console_app.repl_pane.ctrl_c()
@@ -151,24 +160,47 @@ def create_key_bindings(console_app) -> KeyBindings:
@register(
'global.exit-with-confirmation',
key_bindings,
- filter=console_app.pw_ptpython_repl.input_empty_if_in_focus_condition(
- ) | has_focus(console_app.quit_dialog))
+ filter=console_app.pw_ptpython_repl.input_empty_if_in_focus_condition()
+ | has_focus(console_app.quit_dialog),
+ )
def quit(event):
"""Quit with confirmation dialog."""
# If the python repl is in focus and has text input then Ctrl-d will
# delete forward characters instead.
console_app.quit_dialog.open_dialog()
- @register('python-repl.paste-to-input',
- key_bindings,
- filter=has_focus(console_app.pw_ptpython_repl))
+ @register(
+ 'python-repl.paste-to-input',
+ key_bindings,
+ filter=has_focus(console_app.pw_ptpython_repl),
+ )
def paste_into_repl(event):
"""Reset the python repl on Ctrl-c"""
console_app.repl_pane.paste_system_clipboard_to_input_buffer()
- @register('python-repl.copy-all-output',
- key_bindings,
- filter=console_app.repl_pane.input_or_output_has_focus())
+ @register(
+ 'python-repl.history-search',
+ key_bindings,
+ filter=has_focus(console_app.pw_ptpython_repl),
+ )
+ def history_search(event):
+ """Open the repl history search dialog."""
+ console_app.open_command_runner_history()
+
+ @register(
+ 'python-repl.snippet-search',
+ key_bindings,
+ filter=has_focus(console_app.pw_ptpython_repl),
+ )
+ def insert_snippet(event):
+ """Open the repl snippet search dialog."""
+ console_app.open_command_runner_snippets()
+
+ @register(
+ 'python-repl.copy-all-output',
+ key_bindings,
+ filter=console_app.repl_pane.input_or_output_has_focus(),
+ )
def copy_repl_output_text(event):
"""Copy all Python output to the system clipboard."""
console_app.repl_pane.copy_all_output_text()
diff --git a/pw_console/py/pw_console/log_filter.py b/pw_console/py/pw_console/log_filter.py
index 9873a46de..60411efc1 100644
--- a/pw_console/py/pw_console/log_filter.py
+++ b/pw_console/py/pw_console/log_filter.py
@@ -34,6 +34,7 @@ _UPPERCASE_REGEX = re.compile(r'[A-Z]')
class SearchMatcher(Enum):
"""Possible search match methods."""
+
FUZZY = 'FUZZY'
REGEX = 'REGEX'
STRING = 'STRING'
@@ -42,8 +43,9 @@ class SearchMatcher(Enum):
DEFAULT_SEARCH_MATCHER = SearchMatcher.REGEX
-def preprocess_search_regex(text,
- matcher: SearchMatcher = DEFAULT_SEARCH_MATCHER):
+def preprocess_search_regex(
+ text, matcher: SearchMatcher = DEFAULT_SEARCH_MATCHER
+):
# Ignorecase unless the text has capital letters in it.
regex_flags = re.IGNORECASE
if _UPPERCASE_REGEX.search(text):
@@ -54,7 +56,8 @@ def preprocess_search_regex(text,
text_tokens = text.split(' ')
if len(text_tokens) > 1:
text = '(.*?)'.join(
- ['({})'.format(re.escape(text)) for text in text_tokens])
+ ['({})'.format(re.escape(text)) for text in text_tokens]
+ )
elif matcher == SearchMatcher.STRING:
# Escape any regex specific characters to match the string literal.
text = re.escape(text)
@@ -67,50 +70,55 @@ def preprocess_search_regex(text,
class RegexValidator(Validator):
"""Validation of regex input."""
+
def validate(self, document):
"""Check search input for regex syntax errors."""
regex_text, regex_flags = preprocess_search_regex(document.text)
try:
re.compile(regex_text, regex_flags)
except re.error as error:
- raise ValidationError(error.pos,
- "Regex Error: %s" % error) from error
+ raise ValidationError(
+ error.pos, "Regex Error: %s" % error
+ ) from error
@dataclass
class LogFilter:
"""Log Filter Dataclass."""
+
regex: re.Pattern
input_text: Optional[str] = None
invert: bool = False
field: Optional[str] = None
def pattern(self):
- return self.regex.pattern
+ return self.regex.pattern # pylint: disable=no-member
def matches(self, log: LogLine):
field = log.ansi_stripped_log
if self.field:
if hasattr(log, 'metadata') and hasattr(log.metadata, 'fields'):
- field = log.metadata.fields.get(self.field,
- log.ansi_stripped_log)
+ field = log.metadata.fields.get(
+ self.field, log.ansi_stripped_log
+ )
if hasattr(log.record, 'extra_metadata_fields'): # type: ignore
field = log.record.extra_metadata_fields.get( # type: ignore
- self.field, log.ansi_stripped_log)
+ self.field, log.ansi_stripped_log
+ )
if self.field == 'lvl':
field = log.record.levelname
elif self.field == 'time':
field = log.record.asctime
- match = self.regex.search(field)
+ match = self.regex.search(field) # pylint: disable=no-member
if self.invert:
return not match
return match
- def highlight_search_matches(self,
- line_fragments,
- selected=False) -> StyleAndTextTuples:
+ def highlight_search_matches(
+ self, line_fragments, selected=False
+ ) -> StyleAndTextTuples:
"""Highlight search matches in the current line_fragment."""
line_text = fragment_list_to_text(line_fragments)
exploded_fragments = explode_text_fragments(line_fragments)
@@ -135,7 +143,9 @@ class LogFilter:
apply_highlighting(exploded_fragments, i)
else:
# Highlight each non-overlapping search match.
- for match in self.regex.finditer(line_text):
+ for match in self.regex.finditer( # pylint: disable=no-member
+ line_text
+ ): # pylint: disable=no-member
for fragment_i in range(match.start(), match.end()):
apply_highlighting(exploded_fragments, fragment_i)
diff --git a/pw_console/py/pw_console/log_line.py b/pw_console/py/pw_console/log_line.py
index 97d75f49d..0277166af 100644
--- a/pw_console/py/pw_console/log_line.py
+++ b/pw_console/py/pw_console/log_line.py
@@ -26,6 +26,7 @@ from pw_log_tokenized import FormatStringWithMetadata
@dataclass
class LogLine:
"""Class to hold a single log event."""
+
record: logging.LogRecord
formatted_log: str
ansi_stripped_log: str
@@ -42,9 +43,12 @@ class LogLine:
"""Parse log metadata fields from various sources."""
# 1. Parse any metadata from the message itself.
- self.metadata = FormatStringWithMetadata(str(self.record.message))
+ self.metadata = FormatStringWithMetadata(
+ str(self.record.message) # pylint: disable=no-member
+ ) # pylint: disable=no-member
self.formatted_log = self.formatted_log.replace(
- self.metadata.raw_string, self.metadata.message)
+ self.metadata.raw_string, self.metadata.message
+ )
# Remove any trailing line breaks.
self.formatted_log = self.formatted_log.rstrip()
@@ -63,8 +67,9 @@ class LogLine:
# See:
# https://docs.python.org/3/library/logging.html#logging.debug
if hasattr(self.record, 'extra_metadata_fields') and (
- self.record.extra_metadata_fields): # type: ignore
- fields = self.record.extra_metadata_fields # type: ignore
+ self.record.extra_metadata_fields # type: ignore # pylint: disable=no-member
+ ):
+ fields = self.record.extra_metadata_fields # type: ignore # pylint: disable=no-member
for key, value in fields.items():
self.metadata.fields[key] = value
@@ -89,8 +94,8 @@ class LogLine:
# Create prompt_toolkit FormattedText tuples based on the log ANSI
# escape sequences.
if self.fragment_cache is None:
- self.fragment_cache = ANSI(self.formatted_log +
- '\n' # Add a trailing linebreak
- ).__pt_formatted_text__()
+ self.fragment_cache = ANSI(
+ self.formatted_log + '\n' # Add a trailing linebreak
+ ).__pt_formatted_text__()
return self.fragment_cache
diff --git a/pw_console/py/pw_console/log_pane.py b/pw_console/py/pw_console/log_pane.py
index eb4fbfda4..c9bfa8f84 100644
--- a/pw_console/py/pw_console/log_pane.py
+++ b/pw_console/py/pw_console/log_pane.py
@@ -16,7 +16,16 @@
import functools
import logging
import re
-from typing import Any, List, Optional, Union, TYPE_CHECKING
+import time
+from typing import (
+ Any,
+ Callable,
+ List,
+ Optional,
+ TYPE_CHECKING,
+ Tuple,
+ Union,
+)
from prompt_toolkit.application.current import get_app
from prompt_toolkit.filters import (
@@ -33,15 +42,17 @@ from prompt_toolkit.layout import (
ConditionalContainer,
Float,
FloatContainer,
+ FormattedTextControl,
+ HSplit,
UIContent,
UIControl,
VerticalAlign,
+ VSplit,
Window,
+ WindowAlign,
)
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseButton
-import pw_console.widgets.checkbox
-import pw_console.style
from pw_console.log_view import LogView
from pw_console.log_pane_toolbars import (
LineInfoBar,
@@ -52,13 +63,22 @@ from pw_console.log_pane_selection_dialog import LogPaneSelectionDialog
from pw_console.log_store import LogStore
from pw_console.search_toolbar import SearchToolbar
from pw_console.filter_toolbar import FilterToolbar
+
+from pw_console.style import (
+ get_pane_style,
+)
from pw_console.widgets import (
ToolbarButton,
WindowPane,
WindowPaneHSplit,
WindowPaneToolbar,
+ create_border,
+ mouse_handlers,
+ to_checkbox_text,
+ to_keybind_indicator,
)
+
if TYPE_CHECKING:
from pw_console.console_app import ConsoleApp
@@ -68,6 +88,7 @@ _LOG = logging.getLogger(__package__)
class LogContentControl(UIControl):
"""LogPane prompt_toolkit UIControl for displaying LogContainer lines."""
+
def __init__(self, log_pane: 'LogPane') -> None:
# pylint: disable=too-many-locals
self.log_pane = log_pane
@@ -114,7 +135,8 @@ class LogContentControl(UIControl):
"""Remove log pane."""
if self.log_pane.is_a_duplicate:
self.log_pane.application.window_manager.remove_pane(
- self.log_pane)
+ self.log_pane
+ )
@register('log-pane.clear-history', key_bindings)
def _clear_history(_event: KeyPressEvent) -> None:
@@ -136,6 +158,11 @@ class LogContentControl(UIControl):
"""Toggle log line following."""
self.log_pane.toggle_follow()
+ @register('log-pane.toggle-web-browser', key_bindings)
+ def _toggle_browser(_event: KeyPressEvent) -> None:
+ """View logs in browser."""
+ self.log_pane.toggle_websocket_server()
+
@register('log-pane.move-cursor-up', key_bindings)
def _up(_event: KeyPressEvent) -> None:
"""Move cursor up."""
@@ -240,9 +267,11 @@ class LogContentControl(UIControl):
# Create a UIContent instance if none exists
if self.uicontent is None:
- self.uicontent = UIContent(get_line=lambda i: self.lines[i],
- line_count=len(self.lines),
- show_cursor=False)
+ self.uicontent = UIContent(
+ get_line=lambda i: self.lines[i],
+ line_count=len(self.lines),
+ show_cursor=False,
+ )
# Update line_count
self.uicontent.line_count = len(self.lines)
@@ -257,13 +286,16 @@ class LogContentControl(UIControl):
# 1. check if a mouse drag just completed.
# 2. If not in focus, switch focus to this log pane
# If in focus, move the cursor to that position.
- if (mouse_event.event_type == MouseEventType.MOUSE_UP
- and mouse_event.button == MouseButton.LEFT):
-
+ if (
+ mouse_event.event_type == MouseEventType.MOUSE_UP
+ and mouse_event.button == MouseButton.LEFT
+ ):
# If a drag was in progress and this is the first mouse release
# press, set the stop flag.
- if (self.visual_select_mode_drag_start
- and not self.visual_select_mode_drag_stop):
+ if (
+ self.visual_select_mode_drag_start
+ and not self.visual_select_mode_drag_stop
+ ):
self.visual_select_mode_drag_stop = True
if not has_focus(self)():
@@ -287,11 +319,15 @@ class LogContentControl(UIControl):
# Mouse drag with left button should start selecting lines.
# The log pane does not need to be in focus to start this.
- if (mouse_event.event_type == MouseEventType.MOUSE_MOVE
- and mouse_event.button == MouseButton.LEFT):
+ if (
+ mouse_event.event_type == MouseEventType.MOUSE_MOVE
+ and mouse_event.button == MouseButton.LEFT
+ ):
# If a previous mouse drag was completed, clear the selection.
- if (self.visual_select_mode_drag_start
- and self.visual_select_mode_drag_stop):
+ if (
+ self.visual_select_mode_drag_start
+ and self.visual_select_mode_drag_stop
+ ):
self.log_pane.log_view.clear_visual_selection()
# Drag select in progress, set flags accordingly.
self.visual_select_mode_drag_start = True
@@ -317,6 +353,161 @@ class LogContentControl(UIControl):
return NotImplemented
+class LogPaneWebsocketDialog(ConditionalContainer):
+ """Dialog box for showing the websocket URL."""
+
+ # Height of the dialog box contens in lines of text.
+ DIALOG_HEIGHT = 2
+
+ def __init__(self, log_pane: 'LogPane'):
+ self.log_pane = log_pane
+
+ self._last_action_message: str = ''
+ self._last_action_time: float = 0
+
+ info_bar_control = FormattedTextControl(self.get_info_fragments)
+ info_bar_window = Window(
+ content=info_bar_control,
+ height=1,
+ align=WindowAlign.LEFT,
+ dont_extend_width=False,
+ )
+
+ message_bar_control = FormattedTextControl(self.get_message_fragments)
+ message_bar_window = Window(
+ content=message_bar_control,
+ height=1,
+ align=WindowAlign.RIGHT,
+ dont_extend_width=False,
+ )
+
+ action_bar_control = FormattedTextControl(self.get_action_fragments)
+ action_bar_window = Window(
+ content=action_bar_control,
+ height=1,
+ align=WindowAlign.RIGHT,
+ dont_extend_width=True,
+ )
+
+ super().__init__(
+ create_border(
+ HSplit(
+ [
+ info_bar_window,
+ VSplit([message_bar_window, action_bar_window]),
+ ],
+ height=LogPaneWebsocketDialog.DIALOG_HEIGHT,
+ style='class:saveas-dialog',
+ ),
+ content_height=LogPaneWebsocketDialog.DIALOG_HEIGHT,
+ title='Websocket Log Server',
+ border_style='class:saveas-dialog-border',
+ left_margin_columns=1,
+ ),
+ filter=Condition(lambda: self.log_pane.websocket_dialog_active),
+ )
+
+ def focus_self(self) -> None:
+ # Nothing in this dialog can be focused, focus on the parent log_pane
+ # instead.
+ self.log_pane.application.focus_on_container(self.log_pane)
+
+ def close_dialog(self) -> None:
+ """Close this dialog."""
+ self.log_pane.toggle_websocket_server()
+ self.log_pane.websocket_dialog_active = False
+ self.log_pane.application.focus_on_container(self.log_pane)
+ self.log_pane.redraw_ui()
+
+ def _set_action_message(self, text: str) -> None:
+ self._last_action_time = time.time()
+ self._last_action_message = text
+
+ def copy_url_to_clipboard(self) -> None:
+ self.log_pane.application.application.clipboard.set_text(
+ self.log_pane.log_view.get_web_socket_url()
+ )
+ self._set_action_message('Copied!')
+
+ def get_message_fragments(self):
+ """Return FormattedText with the last action message."""
+ # Mouse handlers
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
+ # Separator should have the focus mouse handler so clicking on any
+ # whitespace focuses the input field.
+ separator_text = ('', ' ', focus)
+
+ if self._last_action_time + 10 > time.time():
+ return [
+ ('class:theme-fg-yellow', self._last_action_message, focus),
+ separator_text,
+ ]
+ return [separator_text]
+
+ def get_info_fragments(self):
+ """Return FormattedText with current URL info."""
+ # Mouse handlers
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
+ # Separator should have the focus mouse handler so clicking on any
+ # whitespace focuses the input field.
+ separator_text = ('', ' ', focus)
+
+ fragments = [
+ ('class:saveas-dialog-setting', 'URL: ', focus),
+ (
+ 'class:saveas-dialog-title',
+ self.log_pane.log_view.get_web_socket_url(),
+ focus,
+ ),
+ separator_text,
+ ]
+ return fragments
+
+ def get_action_fragments(self):
+ """Return FormattedText with the action buttons."""
+ # Mouse handlers
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
+ cancel = functools.partial(mouse_handlers.on_click, self.close_dialog)
+ copy = functools.partial(
+ mouse_handlers.on_click,
+ self.copy_url_to_clipboard,
+ )
+
+ # Separator should have the focus mouse handler so clicking on any
+ # whitespace focuses the input field.
+ separator_text = ('', ' ', focus)
+
+ # Default button style
+ button_style = 'class:toolbar-button-inactive'
+
+ fragments = []
+
+ # Action buttons
+ fragments.extend(
+ to_keybind_indicator(
+ key=None,
+ description='Stop',
+ mouse_handler=cancel,
+ base_style=button_style,
+ )
+ )
+
+ fragments.append(separator_text)
+ fragments.extend(
+ to_keybind_indicator(
+ key=None,
+ description='Copy to Clipboard',
+ mouse_handler=copy,
+ base_style=button_style,
+ )
+ )
+
+ # One space separator
+ fragments.append(('', ' ', focus))
+
+ return fragments
+
+
class LogPane(WindowPane):
"""LogPane class."""
@@ -336,9 +527,9 @@ class LogPane(WindowPane):
self.is_a_duplicate = False
# Create the log container which stores and handles incoming logs.
- self.log_view: LogView = LogView(self,
- self.application,
- log_store=log_store)
+ self.log_view: LogView = LogView(
+ self, self.application, log_store=log_store
+ )
# Log pane size variables. These are updated just befor rendering the
# pane by the LogLineHSplit class.
@@ -356,35 +547,60 @@ class LogPane(WindowPane):
self.saveas_dialog_active = False
self.visual_selection_dialog = LogPaneSelectionDialog(self)
+ self.websocket_dialog = LogPaneWebsocketDialog(self)
+ self.websocket_dialog_active = False
+
# Table header bar, only shown if table view is active.
self.table_header_toolbar = TableToolbar(self)
# Create the bottom toolbar for the whole log pane.
self.bottom_toolbar = WindowPaneToolbar(self)
self.bottom_toolbar.add_button(
- ToolbarButton('/', 'Search', self.start_search))
+ ToolbarButton('/', 'Search', self.start_search)
+ )
self.bottom_toolbar.add_button(
- ToolbarButton('Ctrl-o', 'Save', self.start_saveas))
+ ToolbarButton('Ctrl-o', 'Save', self.start_saveas)
+ )
self.bottom_toolbar.add_button(
- ToolbarButton('f',
- 'Follow',
- self.toggle_follow,
- is_checkbox=True,
- checked=lambda: self.log_view.follow))
+ ToolbarButton(
+ 'f',
+ 'Follow',
+ self.toggle_follow,
+ is_checkbox=True,
+ checked=lambda: self.log_view.follow,
+ )
+ )
self.bottom_toolbar.add_button(
- ToolbarButton('t',
- 'Table',
- self.toggle_table_view,
- is_checkbox=True,
- checked=lambda: self.table_view))
+ ToolbarButton(
+ 't',
+ 'Table',
+ self.toggle_table_view,
+ is_checkbox=True,
+ checked=lambda: self.table_view,
+ )
+ )
self.bottom_toolbar.add_button(
- ToolbarButton('w',
- 'Wrap',
- self.toggle_wrap_lines,
- is_checkbox=True,
- checked=lambda: self.wrap_lines))
+ ToolbarButton(
+ 'w',
+ 'Wrap',
+ self.toggle_wrap_lines,
+ is_checkbox=True,
+ checked=lambda: self.wrap_lines,
+ )
+ )
self.bottom_toolbar.add_button(
- ToolbarButton('C', 'Clear', self.clear_history))
+ ToolbarButton('C', 'Clear', self.clear_history)
+ )
+
+ self.bottom_toolbar.add_button(
+ ToolbarButton(
+ 'Shift-o',
+ 'Open in browser',
+ self.toggle_websocket_server,
+ is_checkbox=True,
+ checked=lambda: self.log_view.websocket_running,
+ )
+ )
self.log_content_control = LogContentControl(self)
@@ -406,7 +622,7 @@ class LogPane(WindowPane):
dont_extend_width=False,
# Needed for log lines ANSI sequences that don't specify foreground
# or background colors.
- style=functools.partial(pw_console.style.get_pane_style, self),
+ style=functools.partial(get_pane_style, self),
)
# Root level container
@@ -426,25 +642,39 @@ class LogPane(WindowPane):
align=VerticalAlign.BOTTOM,
height=lambda: self.height,
width=lambda: self.width,
- style=functools.partial(pw_console.style.get_pane_style,
- self),
+ style=functools.partial(get_pane_style, self),
),
floats=[
Float(top=0, right=0, height=1, content=LineInfoBar(self)),
- Float(top=0,
- right=0,
- height=LogPaneSelectionDialog.DIALOG_HEIGHT,
- content=self.visual_selection_dialog),
- Float(top=3,
- left=2,
- right=2,
- height=LogPaneSaveAsDialog.DIALOG_HEIGHT + 2,
- content=self.saveas_dialog),
- ]),
- filter=Condition(lambda: self.show_pane))
+ Float(
+ top=0,
+ right=0,
+ height=LogPaneSelectionDialog.DIALOG_HEIGHT,
+ content=self.visual_selection_dialog,
+ ),
+ Float(
+ top=3,
+ left=2,
+ right=2,
+ height=LogPaneSaveAsDialog.DIALOG_HEIGHT + 2,
+ content=self.saveas_dialog,
+ ),
+ Float(
+ top=1,
+ left=2,
+ right=2,
+ height=LogPaneWebsocketDialog.DIALOG_HEIGHT + 2,
+ content=self.websocket_dialog,
+ ),
+ ],
+ ),
+ filter=Condition(lambda: self.show_pane),
+ )
@property
def table_view(self):
+ if self.log_view.websocket_running:
+ return False
return self._table_view
@table_view.setter
@@ -458,10 +688,12 @@ class LogPane(WindowPane):
# List active filters
if self.log_view.filtering_on:
title += ' (FILTERS: '
- title += ' '.join([
- log_filter.pattern()
- for log_filter in self.log_view.filters.values()
- ])
+ title += ' '.join(
+ [
+ log_filter.pattern()
+ for log_filter in self.log_view.filters.values()
+ ]
+ )
title += ')'
return title
@@ -483,6 +715,8 @@ class LogPane(WindowPane):
def start_search(self):
"""Show the search bar to begin a search."""
+ if self.log_view.websocket_running:
+ return
# Show the search bar
self.search_bar_active = True
# Focus on the search bar
@@ -500,8 +734,10 @@ class LogPane(WindowPane):
def pane_resized(self) -> bool:
"""Return True if the current window size has changed."""
- return (self.last_log_pane_width != self.current_log_pane_width
- or self.last_log_pane_height != self.current_log_pane_height)
+ return (
+ self.last_log_pane_width != self.current_log_pane_width
+ or self.last_log_pane_height != self.current_log_pane_height
+ )
def update_pane_size(self, width, height):
"""Save width and height of the log pane for the current UI render
@@ -543,12 +779,26 @@ class LogPane(WindowPane):
self.log_view.clear_scrollback()
self.redraw_ui()
+ def toggle_websocket_server(self):
+ """Start or stop websocket server to send logs."""
+ if self.log_view.websocket_running:
+ self.log_view.stop_websocket_thread()
+ self.websocket_dialog_active = False
+ else:
+ self.search_toolbar.close_search_bar()
+ self.log_view.start_websocket_thread()
+ self.application.start_http_server()
+ self.saveas_dialog_active = False
+ self.websocket_dialog_active = True
+
def get_all_key_bindings(self) -> List:
"""Return all keybinds for this pane."""
# Return log content control keybindings
return [self.log_content_control.get_key_bindings()]
- def get_all_menu_options(self) -> List:
+ def get_window_menu_options(
+ self,
+ ) -> List[Tuple[str, Union[Callable, None]]]:
"""Return all menu options for the log pane."""
options = [
@@ -561,22 +811,30 @@ class LogPane(WindowPane):
('-', None),
(
'{check} Line wrapping'.format(
- check=pw_console.widgets.checkbox.to_checkbox_text(
- self.wrap_lines, end='')),
+ check=to_checkbox_text(self.wrap_lines, end='')
+ ),
self.toggle_wrap_lines,
),
(
'{check} Table view'.format(
- check=pw_console.widgets.checkbox.to_checkbox_text(
- self._table_view, end='')),
+ check=to_checkbox_text(self._table_view, end='')
+ ),
self.toggle_table_view,
),
(
'{check} Follow'.format(
- check=pw_console.widgets.checkbox.to_checkbox_text(
- self.log_view.follow, end='')),
+ check=to_checkbox_text(self.log_view.follow, end='')
+ ),
self.toggle_follow,
),
+ (
+ '{check} Open in web browser'.format(
+ check=to_checkbox_text(
+ self.log_view.websocket_running, end=''
+ )
+ ),
+ self.toggle_websocket_server,
+ ),
# Menu separator
('-', None),
(
@@ -589,11 +847,14 @@ class LogPane(WindowPane):
),
]
if self.is_a_duplicate:
- options += [(
- 'Remove/Delete pane',
- functools.partial(self.application.window_manager.remove_pane,
- self),
- )]
+ options += [
+ (
+ 'Remove/Delete pane',
+ functools.partial(
+ self.application.window_manager.remove_pane, self
+ ),
+ )
+ ]
# Search / Filter section
options += [
@@ -626,14 +887,17 @@ class LogPane(WindowPane):
if field == 'all':
field = None
if self.log_view.new_search(
- search_string,
- invert=inverted,
- field=field,
- search_matcher=matcher_name,
- interactive=False,
+ search_string,
+ invert=inverted,
+ field=field,
+ search_matcher=matcher_name,
+ interactive=False,
):
self.log_view.install_new_filter()
+ # Trigger any existing log messages to be added to the view.
+ self.log_view.new_logs_arrived()
+
def create_duplicate(self) -> 'LogPane':
"""Create a duplicate of this LogView."""
new_pane = LogPane(self.application, pane_title=self.pane_title())
@@ -658,9 +922,11 @@ class LogPane(WindowPane):
# Add the new pane.
self.application.window_manager.add_pane(new_pane)
- def add_log_handler(self,
- logger: Union[str, logging.Logger],
- level_name: Optional[str] = None) -> None:
+ def add_log_handler(
+ self,
+ logger: Union[str, logging.Logger],
+ level_name: Optional[str] = None,
+ ) -> None:
"""Add a log handlers to this LogPane."""
if isinstance(logger, logging.Logger):
@@ -672,7 +938,5 @@ class LogPane(WindowPane):
if not hasattr(logging, level_name):
raise Exception(f'Unknown log level: {level_name}')
logger_instance.level = getattr(logging, level_name, logging.INFO)
- logger_instance.addHandler(self.log_view.log_store # type: ignore
- )
- self.append_pane_subtitle( # type: ignore
- logger_instance.name)
+ logger_instance.addHandler(self.log_view.log_store) # type: ignore
+ self.append_pane_subtitle(logger_instance.name) # type: ignore
diff --git a/pw_console/py/pw_console/log_pane_saveas_dialog.py b/pw_console/py/pw_console/log_pane_saveas_dialog.py
index f142a8af8..b3adb090f 100644
--- a/pw_console/py/pw_console/log_pane_saveas_dialog.py
+++ b/pw_console/py/pw_console/log_pane_saveas_dialog.py
@@ -36,10 +36,12 @@ from prompt_toolkit.validation import (
Validator,
)
-import pw_console.widgets.checkbox
-import pw_console.widgets.border
-import pw_console.widgets.mouse_handlers
-import pw_console.style
+from pw_console.widgets import (
+ create_border,
+ mouse_handlers,
+ to_checkbox_with_keybind_indicator,
+ to_keybind_indicator,
+)
if TYPE_CHECKING:
from pw_console.log_pane import LogPane
@@ -47,6 +49,7 @@ if TYPE_CHECKING:
class PathValidator(Validator):
"""Validation of file path input."""
+
def validate(self, document):
"""Check input path leads to a valid parent directory."""
target_path = Path(document.text).expanduser()
@@ -55,17 +58,20 @@ class PathValidator(Validator):
raise ValidationError(
# Set cursor position to the end
len(document.text),
- "Directory doesn't exist: %s" % document.text)
+ "Directory doesn't exist: %s" % document.text,
+ )
if target_path.is_dir():
raise ValidationError(
# Set cursor position to the end
len(document.text),
- "File input is an existing directory: %s" % document.text)
+ "File input is an existing directory: %s" % document.text,
+ )
class LogPaneSaveAsDialog(ConditionalContainer):
"""Dialog box for saving logs to a file."""
+
# Height of the dialog box contens in lines of text.
DIALOG_HEIGHT = 3
@@ -81,9 +87,14 @@ class LogPaneSaveAsDialog(ConditionalContainer):
self.input_field = TextArea(
prompt=[
- ('class:saveas-dialog-setting', 'File: ',
- functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self))
+ (
+ 'class:saveas-dialog-setting',
+ 'File: ',
+ functools.partial(
+ mouse_handlers.on_click,
+ self.focus_self,
+ ),
+ )
],
# Pre-fill the current working directory.
text=self.starting_file_path,
@@ -102,18 +113,21 @@ class LogPaneSaveAsDialog(ConditionalContainer):
self.input_field.buffer.cursor_position = len(self.starting_file_path)
- settings_bar_control = FormattedTextControl(
- self.get_settings_fragments)
- settings_bar_window = Window(content=settings_bar_control,
- height=1,
- align=WindowAlign.LEFT,
- dont_extend_width=False)
+ settings_bar_control = FormattedTextControl(self.get_settings_fragments)
+ settings_bar_window = Window(
+ content=settings_bar_control,
+ height=1,
+ align=WindowAlign.LEFT,
+ dont_extend_width=False,
+ )
action_bar_control = FormattedTextControl(self.get_action_fragments)
- action_bar_window = Window(content=action_bar_control,
- height=1,
- align=WindowAlign.RIGHT,
- dont_extend_width=False)
+ action_bar_window = Window(
+ content=action_bar_control,
+ height=1,
+ align=WindowAlign.RIGHT,
+ dont_extend_width=False,
+ )
# Add additional keybindings for the input_field text area.
key_bindings = KeyBindings()
@@ -127,7 +141,7 @@ class LogPaneSaveAsDialog(ConditionalContainer):
self.input_field.control.key_bindings = key_bindings
super().__init__(
- pw_console.widgets.border.create_border(
+ create_border(
HSplit(
[
settings_bar_window,
@@ -155,15 +169,19 @@ class LogPaneSaveAsDialog(ConditionalContainer):
def _toggle_table_formatting(self):
self._export_with_table_formatting = (
- not self._export_with_table_formatting)
+ not self._export_with_table_formatting
+ )
def _toggle_selected_lines(self):
self._export_with_selected_lines_only = (
- not self._export_with_selected_lines_only)
+ not self._export_with_selected_lines_only
+ )
- def set_export_options(self,
- table_format: Optional[bool] = None,
- selected_lines_only: Optional[bool] = None) -> None:
+ def set_export_options(
+ self,
+ table_format: Optional[bool] = None,
+ selected_lines_only: Optional[bool] = None,
+ ) -> None:
# Allows external callers such as the line selection dialog to set
# export format options.
if table_format is not None:
@@ -187,9 +205,10 @@ class LogPaneSaveAsDialog(ConditionalContainer):
return False
if self.log_pane.log_view.export_logs(
- file_name=input_text,
- use_table_formatting=self._export_with_table_formatting,
- selected_lines_only=self._export_with_selected_lines_only):
+ file_name=input_text,
+ use_table_formatting=self._export_with_table_formatting,
+ selected_lines_only=self._export_with_selected_lines_only,
+ ):
self.close_dialog()
# Reset selected_lines_only
self.set_export_options(selected_lines_only=False)
@@ -202,14 +221,15 @@ class LogPaneSaveAsDialog(ConditionalContainer):
def get_settings_fragments(self):
"""Return FormattedText with current save settings."""
# Mouse handlers
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
toggle_table_formatting = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self._toggle_table_formatting)
+ mouse_handlers.on_click,
+ self._toggle_table_formatting,
+ )
toggle_selected_lines = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self._toggle_selected_lines)
+ mouse_handlers.on_click,
+ self._toggle_selected_lines,
+ )
# Separator should have the focus mouse handler so clicking on any
# whitespace focuses the input field.
@@ -223,24 +243,28 @@ class LogPaneSaveAsDialog(ConditionalContainer):
# Table checkbox
fragments.extend(
- pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
+ to_checkbox_with_keybind_indicator(
checked=self._export_with_table_formatting,
key='', # No key shortcut help text
description='Table Formatting',
mouse_handler=toggle_table_formatting,
- base_style=button_style))
+ base_style=button_style,
+ )
+ )
# Two space separator
fragments.append(separator_text)
# Selected lines checkbox
fragments.extend(
- pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
+ to_checkbox_with_keybind_indicator(
checked=self._export_with_selected_lines_only,
key='', # No key shortcut help text
description='Selected Lines Only',
mouse_handler=toggle_selected_lines,
- base_style=button_style))
+ base_style=button_style,
+ )
+ )
# Two space separator
fragments.append(separator_text)
@@ -250,12 +274,9 @@ class LogPaneSaveAsDialog(ConditionalContainer):
def get_action_fragments(self):
"""Return FormattedText with the save action buttons."""
# Mouse handlers
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self)
- cancel = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.close_dialog)
- save = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.save_action)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
+ cancel = functools.partial(mouse_handlers.on_click, self.close_dialog)
+ save = functools.partial(mouse_handlers.on_click, self.save_action)
# Separator should have the focus mouse handler so clicking on any
# whitespace focuses the input field.
@@ -267,24 +288,26 @@ class LogPaneSaveAsDialog(ConditionalContainer):
fragments = [separator_text]
# Cancel button
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='Ctrl-c',
description='Cancel',
mouse_handler=cancel,
base_style=button_style,
- ))
+ )
+ )
# Two space separator
fragments.append(separator_text)
# Save button
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='Enter',
description='Save',
mouse_handler=save,
base_style=button_style,
- ))
+ )
+ )
# One space separator
fragments.append(('', ' ', focus))
diff --git a/pw_console/py/pw_console/log_pane_selection_dialog.py b/pw_console/py/pw_console/log_pane_selection_dialog.py
index 9c76f5131..ec0b1c9b7 100644
--- a/pw_console/py/pw_console/log_pane_selection_dialog.py
+++ b/pw_console/py/pw_console/log_pane_selection_dialog.py
@@ -25,10 +25,12 @@ from prompt_toolkit.layout import (
WindowAlign,
)
-import pw_console.style
-import pw_console.widgets.checkbox
-import pw_console.widgets.border
-import pw_console.widgets.mouse_handlers
+from pw_console.widgets import (
+ create_border,
+ mouse_handlers,
+ to_checkbox_with_keybind_indicator,
+ to_keybind_indicator,
+)
if TYPE_CHECKING:
from pw_console.log_pane import LogPane
@@ -40,6 +42,7 @@ class LogPaneSelectionDialog(ConditionalContainer):
Displays number of lines selected, buttons for copying to the clipboar or
saving to a file, and buttons to select all or cancel (clear) the
selection."""
+
# Height of the dialog box contens in lines of text.
DIALOG_HEIGHT = 3
@@ -51,14 +54,16 @@ class LogPaneSelectionDialog(ConditionalContainer):
self._table_flag: bool = True
selection_bar_control = FormattedTextControl(self.get_fragments)
- selection_bar_window = Window(content=selection_bar_control,
- height=1,
- align=WindowAlign.LEFT,
- dont_extend_width=False,
- style='class:selection-dialog')
+ selection_bar_window = Window(
+ content=selection_bar_control,
+ height=1,
+ align=WindowAlign.LEFT,
+ dont_extend_width=False,
+ style='class:selection-dialog',
+ )
super().__init__(
- pw_console.widgets.border.create_border(
+ create_border(
selection_bar_window,
(LogPaneSelectionDialog.DIALOG_HEIGHT - 1),
border_style='class:selection-dialog-border',
@@ -66,7 +71,8 @@ class LogPaneSelectionDialog(ConditionalContainer):
top=False,
right=False,
),
- filter=Condition(lambda: self.log_view.visual_select_mode))
+ filter=Condition(lambda: self.log_view.visual_select_mode),
+ )
def focus_log_pane(self):
self.log_pane.application.focus_on_container(self.log_pane)
@@ -85,65 +91,82 @@ class LogPaneSelectionDialog(ConditionalContainer):
def _copy_selection(self) -> None:
if self.log_view.export_logs(
- to_clipboard=True,
- use_table_formatting=self._table_flag,
- selected_lines_only=True,
- add_markdown_fence=self._markdown_flag,
+ to_clipboard=True,
+ use_table_formatting=self._table_flag,
+ selected_lines_only=True,
+ add_markdown_fence=self._markdown_flag,
):
self._select_none()
def _saveas_file(self) -> None:
- self.log_pane.start_saveas(table_format=self._table_flag,
- selected_lines_only=True)
+ self.log_pane.start_saveas(
+ table_format=self._table_flag, selected_lines_only=True
+ )
def get_fragments(self):
"""Return formatted text tuples for both rows of the selection
dialog."""
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_log_pane)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_log_pane)
one_space = ('', ' ', focus)
two_spaces = ('', ' ', focus)
select_all = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._select_all)
+ mouse_handlers.on_click, self._select_all
+ )
select_none = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._select_none)
+ mouse_handlers.on_click, self._select_none
+ )
copy_selection = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._copy_selection)
+ mouse_handlers.on_click, self._copy_selection
+ )
saveas_file = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._saveas_file)
+ mouse_handlers.on_click, self._saveas_file
+ )
toggle_markdown = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self._toggle_markdown_flag)
+ mouse_handlers.on_click,
+ self._toggle_markdown_flag,
+ )
toggle_table = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self._toggle_table_flag)
+ mouse_handlers.on_click, self._toggle_table_flag
+ )
button_style = 'class:toolbar-button-inactive'
# First row of text
- fragments = [('class:selection-dialog-title', ' {} Selected '.format(
- self.log_view.visual_selected_log_count()), focus), one_space,
- ('class:selection-dialog-default-fg', 'Format: ', focus)]
+ fragments = [
+ (
+ 'class:selection-dialog-title',
+ ' {} Selected '.format(
+ self.log_view.visual_selected_log_count()
+ ),
+ focus,
+ ),
+ one_space,
+ ('class:selection-dialog-default-fg', 'Format: ', focus),
+ ]
# Table and Markdown options
fragments.extend(
- pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
+ to_checkbox_with_keybind_indicator(
self._table_flag,
key='',
description='Table',
mouse_handler=toggle_table,
- base_style='class:selection-dialog-default-bg'))
+ base_style='class:selection-dialog-default-bg',
+ )
+ )
fragments.extend(
- pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
+ to_checkbox_with_keybind_indicator(
self._markdown_flag,
key='',
description='Markdown',
mouse_handler=toggle_markdown,
- base_style='class:selection-dialog-default-bg'))
+ base_style='class:selection-dialog-default-bg',
+ )
+ )
# Line break
fragments.append(('', '\n'))
@@ -152,40 +175,44 @@ class LogPaneSelectionDialog(ConditionalContainer):
fragments.append(one_space)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='Ctrl-c',
description='Cancel',
mouse_handler=select_none,
base_style=button_style,
- ))
+ )
+ )
fragments.append(two_spaces)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='Ctrl-a',
description='Select All',
mouse_handler=select_all,
base_style=button_style,
- ))
+ )
+ )
fragments.append(two_spaces)
fragments.append(one_space)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='',
description='Save as File',
mouse_handler=saveas_file,
base_style=button_style,
- ))
+ )
+ )
fragments.append(two_spaces)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='',
description='Copy',
mouse_handler=copy_selection,
base_style=button_style,
- ))
+ )
+ )
fragments.append(one_space)
return fragments
diff --git a/pw_console/py/pw_console/log_pane_toolbars.py b/pw_console/py/pw_console/log_pane_toolbars.py
index 038b9530f..35778f4a1 100644
--- a/pw_console/py/pw_console/log_pane_toolbars.py
+++ b/pw_console/py/pw_console/log_pane_toolbars.py
@@ -27,9 +27,7 @@ from prompt_toolkit.layout import (
HorizontalAlign,
)
-import pw_console.widgets.checkbox
-import pw_console.widgets.mouse_handlers
-import pw_console.style
+from pw_console.style import get_toolbar_style
if TYPE_CHECKING:
from pw_console.log_pane import LogPane
@@ -37,6 +35,7 @@ if TYPE_CHECKING:
class LineInfoBar(ConditionalContainer):
"""One line bar for showing current and total log lines."""
+
def get_tokens(self):
"""Return formatted text tokens for display."""
tokens = ' {} / {} '.format(
@@ -48,30 +47,37 @@ class LineInfoBar(ConditionalContainer):
def __init__(self, log_pane: 'LogPane'):
self.log_pane = log_pane
info_bar_control = FormattedTextControl(self.get_tokens)
- info_bar_window = Window(content=info_bar_control,
- align=WindowAlign.RIGHT,
- dont_extend_width=True)
+ info_bar_window = Window(
+ content=info_bar_control,
+ align=WindowAlign.RIGHT,
+ dont_extend_width=True,
+ )
super().__init__(
- VSplit([info_bar_window],
- height=1,
- style=functools.partial(pw_console.style.get_toolbar_style,
- self.log_pane,
- dim=True),
- align=HorizontalAlign.RIGHT),
+ VSplit(
+ [info_bar_window],
+ height=1,
+ style=functools.partial(
+ get_toolbar_style, self.log_pane, dim=True
+ ),
+ align=HorizontalAlign.RIGHT,
+ ),
# Only show current/total line info if not auto-following
# logs. Similar to tmux behavior.
- filter=Condition(lambda: not self.log_pane.log_view.follow))
+ filter=Condition(lambda: not self.log_pane.log_view.follow),
+ )
class TableToolbar(ConditionalContainer):
"""One line toolbar for showing table headers."""
+
TOOLBAR_HEIGHT = 1
def __init__(self, log_pane: 'LogPane'):
# FormattedText of the table column headers.
table_header_bar_control = FormattedTextControl(
- log_pane.log_view.render_table_header)
+ log_pane.log_view.render_table_header
+ )
# Left justify the header content.
table_header_bar_window = Window(
content=table_header_bar_control,
@@ -79,11 +85,14 @@ class TableToolbar(ConditionalContainer):
dont_extend_width=False,
)
super().__init__(
- VSplit([table_header_bar_window],
- height=1,
- style=functools.partial(pw_console.style.get_toolbar_style,
- log_pane,
- dim=True),
- align=HorizontalAlign.LEFT),
- filter=Condition(lambda: log_pane.table_view and log_pane.log_view.
- get_total_count() > 0))
+ VSplit(
+ [table_header_bar_window],
+ height=1,
+ style=functools.partial(get_toolbar_style, log_pane, dim=True),
+ align=HorizontalAlign.LEFT,
+ ),
+ filter=Condition(
+ lambda: log_pane.table_view
+ and log_pane.log_view.get_total_count() > 0
+ ),
+ )
diff --git a/pw_console/py/pw_console/log_screen.py b/pw_console/py/pw_console/log_screen.py
index e3a0f0583..2e0d5f26d 100644
--- a/pw_console/py/pw_console/log_screen.py
+++ b/pw_console/py/pw_console/log_screen.py
@@ -81,6 +81,7 @@ class ScreenLine:
logs. The subline is 0 since each line is the first one for this log. Both
have a height of 1 since no line wrapping was performed.
"""
+
# The StyleAndTextTuples for this line ending with a '\n'. These are the raw
# prompt_toolkit formatted text tuples to display on screen. The colors and
# spacing can change depending on the formatters used in the
@@ -125,11 +126,13 @@ class LogScreen:
It is responsible for moving the cursor_position, prepending and appending
log lines as the user moves the cursor."""
+
# Callable functions to retrieve logs and display formatting.
get_log_source: Callable[[], Tuple[int, collections.deque[LogLine]]]
get_line_wrapping: Callable[[], bool]
- get_log_formatter: Callable[[], Optional[Callable[[LogLine],
- StyleAndTextTuples]]]
+ get_log_formatter: Callable[
+ [], Optional[Callable[[LogLine], StyleAndTextTuples]]
+ ]
get_search_filter: Callable[[], Optional[LogFilter]]
get_search_highlight: Callable[[], bool]
@@ -144,7 +147,8 @@ class LogScreen:
# wrapping to be displayed it will be represented by multiple ScreenLine
# instances in this deque.
line_buffer: collections.deque[ScreenLine] = dataclasses.field(
- default_factory=collections.deque)
+ default_factory=collections.deque
+ )
def __post_init__(self) -> None:
# Empty screen flag. Will be true if the screen contains only newlines.
@@ -188,8 +192,9 @@ class LogScreen:
# 6 the range below will be:
# >>> list(i for i in range((10 - 6) + 1, 10 + 1))
# [5, 6, 7, 8, 9, 10]
- for i in range((log_index - max_log_messages_to_fetch) + 1,
- log_index + 1):
+ for i in range(
+ (log_index - max_log_messages_to_fetch) + 1, log_index + 1
+ ):
# If i is < 0 it's an invalid log, skip to the next line. The next
# index could be 0 or higher since we are traversing in increasing
# order.
@@ -223,11 +228,12 @@ class LogScreen:
# Loop through a copy of the line_buffer in case it is mutated before
# this function is complete.
for i, line in enumerate(list(self.line_buffer)):
-
# Is this line the cursor_position? Apply line highlighting
- if (i == self.cursor_position
- and (self.cursor_position < len(self.line_buffer))
- and not self.line_buffer[self.cursor_position].empty()):
+ if (
+ i == self.cursor_position
+ and (self.cursor_position < len(self.line_buffer))
+ and not self.line_buffer[self.cursor_position].empty()
+ ):
# Fill in empty charaters to the width of the screen. This
# ensures the backgound is highlighted to the edge of the
# screen.
@@ -239,10 +245,13 @@ class LogScreen:
# Apply a style to highlight this line.
all_lines.append(
- to_formatted_text(new_fragments,
- style='class:selected-log-line'))
+ to_formatted_text(
+ new_fragments, style='class:selected-log-line'
+ )
+ )
elif line.log_index is not None and (
- marked_logs_start <= line.log_index <= marked_logs_end):
+ marked_logs_start <= line.log_index <= marked_logs_end
+ ):
new_fragments = fill_character_width(
line.fragments,
len(line.fragments) - 1, # -1 for the ending line break
@@ -251,8 +260,10 @@ class LogScreen:
# Apply a style to highlight this line.
all_lines.append(
- to_formatted_text(new_fragments,
- style='class:marked-log-line'))
+ to_formatted_text(
+ new_fragments, style='class:marked-log-line'
+ )
+ )
else:
all_lines.append(line.fragments)
@@ -301,8 +312,10 @@ class LogScreen:
new_index = self.cursor_position - 1
if new_index < 0:
break
- if (new_index < len(self.line_buffer)
- and self.line_buffer[new_index].empty()):
+ if (
+ new_index < len(self.line_buffer)
+ and self.line_buffer[new_index].empty()
+ ):
# The next line is empty and has no content.
break
self.cursor_position -= 1
@@ -324,8 +337,10 @@ class LogScreen:
new_index = self.cursor_position + 1
if new_index >= self.height:
break
- if (new_index < len(self.line_buffer)
- and self.line_buffer[new_index].empty()):
+ if (
+ new_index < len(self.line_buffer)
+ and self.line_buffer[new_index].empty()
+ ):
# The next line is empty and has no content.
break
self.cursor_position += 1
@@ -374,8 +389,9 @@ class LogScreen:
remaining_lines = self.scroll_subline(amount)
if remaining_lines != 0 and current_line.log_index is not None:
# Restore original selected line.
- self._move_selection_to_log(current_line.log_index,
- current_line.subline)
+ self._move_selection_to_log(
+ current_line.log_index, current_line.subline
+ )
return
# Lines scrolled as expected, set cursor_position to top.
self.cursor_position = 0
@@ -398,13 +414,14 @@ class LogScreen:
remaining_lines = self.scroll_subline(amount)
if remaining_lines != 0 and current_line.log_index is not None:
# Restore original selected line.
- self._move_selection_to_log(current_line.log_index,
- current_line.subline)
+ self._move_selection_to_log(
+ current_line.log_index, current_line.subline
+ )
return
# Lines scrolled as expected, set cursor_position to center.
self.cursor_position -= amount
- self.cursor_position -= (current_line.height - 1)
+ self.cursor_position -= current_line.height - 1
def scroll_subline(self, line_count: int = 1) -> int:
"""Move the cursor down or up by positive or negative lines.
@@ -475,8 +492,10 @@ class LogScreen:
def get_line_at_cursor_position(self) -> ScreenLine:
"""Returns the ScreenLine under the cursor."""
- if (self.cursor_position >= len(self.line_buffer)
- or self.cursor_position < 0):
+ if (
+ self.cursor_position >= len(self.line_buffer)
+ or self.cursor_position < 0
+ ):
return ScreenLine([('', '')])
return self.line_buffer[self.cursor_position]
@@ -545,8 +564,9 @@ class LogScreen:
log_index = self.line_buffer[-1].log_index
return log_index
- def _get_fragments_per_line(self,
- log_index: int) -> List[StyleAndTextTuples]:
+ def _get_fragments_per_line(
+ self, log_index: int
+ ) -> List[StyleAndTextTuples]:
"""Return a list of lines wrapped to the screen width for a log.
Before fetching the log message this function updates the log_source and
@@ -575,7 +595,8 @@ class LogScreen:
line_fragments, _log_line_height = insert_linebreaks(
fragments,
max_line_width=self.width,
- truncate_long_lines=truncate_lines)
+ truncate_long_lines=truncate_lines,
+ )
# Convert the existing flattened fragments to a list of lines.
fragments_per_line = split_lines(line_fragments)
@@ -599,15 +620,16 @@ class LogScreen:
fragments_per_line = self._get_fragments_per_line(log_index)
# Target the last subline if the subline arg is set to -1.
- fetch_last_subline = (subline == -1)
+ fetch_last_subline = subline == -1
for line_index, line in enumerate(fragments_per_line):
# If we are looking for a specific subline and this isn't it, skip.
if subline is not None:
# If subline is set to -1 we need to append the last subline of
# this log message. Skip this line if it isn't the last one.
- if fetch_last_subline and (line_index !=
- len(fragments_per_line) - 1):
+ if fetch_last_subline and (
+ line_index != len(fragments_per_line) - 1
+ ):
continue
# If subline is not -1 (0 or higher) and this isn't the desired
# line, skip to the next one.
@@ -620,7 +642,8 @@ class LogScreen:
log_index=log_index,
subline=line_index,
height=len(fragments_per_line),
- ))
+ )
+ )
# Remove lines from the bottom if over the screen height.
if len(self.line_buffer) > self.height:
@@ -647,7 +670,8 @@ class LogScreen:
log_index=log_index,
subline=line_index,
height=len(fragments_per_line),
- ))
+ )
+ )
# Remove lines from the top if over the screen height.
if len(self.line_buffer) > self.height:
diff --git a/pw_console/py/pw_console/log_store.py b/pw_console/py/pw_console/log_store.py
index ed6ebb447..8a8c4e83c 100644
--- a/pw_console/py/pw_console/log_store.py
+++ b/pw_console/py/pw_console/log_store.py
@@ -16,15 +16,14 @@
from __future__ import annotations
import collections
import logging
-import sys
from datetime import datetime
from typing import Dict, List, Optional, TYPE_CHECKING
-import pw_cli.color
+from pw_cli.color import colors as pw_cli_colors
from pw_console.console_prefs import ConsolePrefs
from pw_console.log_line import LogLine
-import pw_console.text_formatting
+from pw_console.text_formatting import strip_ansi
from pw_console.widgets.table import TableView
if TYPE_CHECKING:
@@ -66,7 +65,7 @@ class LogStore(logging.Handler):
loggers={
'Host Logs': [
logging.getLogger(__package__),
- logging.getLogger(__file__),
+ logging.getLogger(__name__),
],
# Set the LogStore as the value of this logger window.
'Device Logs': device_log_store,
@@ -77,23 +76,21 @@ class LogStore(logging.Handler):
console.setup_python_logging()
console.embed()
"""
+
def __init__(self, prefs: Optional[ConsolePrefs] = None):
"""Initializes the LogStore instance."""
# ConsolePrefs may not be passed on init. For example, if the user is
# creating a LogStore to capture log messages before console startup.
if not prefs:
- prefs = ConsolePrefs(project_file=False,
- project_user_file=False,
- user_file=False)
+ prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
self.prefs = prefs
# Log storage deque for fast addition and deletion from the beginning
# and end of the iterable.
self.logs: collections.deque = collections.deque()
- # Estimate of the logs in memory.
- self.byte_size: int = 0
-
# Only allow this many log lines in memory.
self.max_history_size: int = 1000000
@@ -130,7 +127,7 @@ class LogStore(logging.Handler):
def set_formatting(self) -> None:
"""Setup log formatting."""
# Copy of pw_cli log formatter
- colors = pw_cli.color.colors(True)
+ colors = pw_cli_colors(True)
timestamp_prefix = colors.black_on_white('%(asctime)s') + ' '
timestamp_format = '%Y%m%d %H:%M:%S'
format_string = timestamp_prefix + '%(levelname)s %(message)s'
@@ -146,16 +143,15 @@ class LogStore(logging.Handler):
def clear_logs(self):
"""Erase all stored pane lines."""
self.logs = collections.deque()
- self.byte_size = 0
self.channel_counts = {}
self.channel_formatted_prefix_widths = {}
self.line_index = 0
def get_channel_counts(self):
"""Return the seen channel log counts for this conatiner."""
- return ', '.join([
- f'{name}: {count}' for name, count in self.channel_counts.items()
- ])
+ return ', '.join(
+ [f'{name}: {count}' for name, count in self.channel_counts.items()]
+ )
def get_total_count(self):
"""Total size of the logs store."""
@@ -171,10 +167,12 @@ class LogStore(logging.Handler):
"""Save the formatted prefix width if this is a new logger channel
name."""
if self.formatter and (
- record.name
- not in self.channel_formatted_prefix_widths.keys()):
+ record.name not in self.channel_formatted_prefix_widths.keys()
+ ):
# Find the width of the formatted timestamp and level
- format_string = self.formatter._fmt # pylint: disable=protected-access
+ format_string = (
+ self.formatter._fmt # pylint: disable=protected-access
+ )
# There may not be a _fmt defined.
if not format_string:
@@ -182,42 +180,51 @@ class LogStore(logging.Handler):
format_without_message = format_string.replace('%(message)s', '')
# If any other style parameters are left, get the width of them.
- if (format_without_message and 'asctime' in format_without_message
- and 'levelname' in format_without_message):
+ if (
+ format_without_message
+ and 'asctime' in format_without_message
+ and 'levelname' in format_without_message
+ ):
formatted_time_and_level = format_without_message % dict(
- asctime=record.asctime, levelname=record.levelname)
+ asctime=record.asctime, levelname=record.levelname
+ )
# Delete ANSI escape sequences.
- ansi_stripped_time_and_level = (
- pw_console.text_formatting.strip_ansi(
- formatted_time_and_level))
+ ansi_stripped_time_and_level = strip_ansi(
+ formatted_time_and_level
+ )
self.channel_formatted_prefix_widths[record.name] = len(
- ansi_stripped_time_and_level)
+ ansi_stripped_time_and_level
+ )
else:
self.channel_formatted_prefix_widths[record.name] = 0
# Set the max width of all known formats so far.
self.longest_channel_prefix_width = max(
- self.channel_formatted_prefix_widths.values())
+ self.channel_formatted_prefix_widths.values()
+ )
def _append_log(self, record: logging.LogRecord):
"""Add a new log event."""
# Format incoming log line.
formatted_log = self.format(record)
- ansi_stripped_log = pw_console.text_formatting.strip_ansi(
- formatted_log)
+ ansi_stripped_log = strip_ansi(formatted_log)
# Save this log.
self.logs.append(
- LogLine(record=record,
- formatted_log=formatted_log,
- ansi_stripped_log=ansi_stripped_log))
+ LogLine(
+ record=record,
+ formatted_log=formatted_log,
+ ansi_stripped_log=ansi_stripped_log,
+ )
+ )
# Increment this logger count
- self.channel_counts[record.name] = self.channel_counts.get(
- record.name, 0) + 1
+ self.channel_counts[record.name] = (
+ self.channel_counts.get(record.name, 0) + 1
+ )
- # TODO(pwbug/614): Revisit calculating prefix widths automatically when
- # line wrapping indentation is supported.
+ # TODO(b/235271486): Revisit calculating prefix widths automatically
+ # when line wrapping indentation is supported.
# Set the prefix width to 0
self.channel_formatted_prefix_widths[record.name] = 0
@@ -227,12 +234,6 @@ class LogStore(logging.Handler):
# Check for bigger column widths.
self.table.update_metadata_column_widths(self.logs[-1])
- # Update estimated byte_size.
- self.byte_size += sys.getsizeof(self.logs[-1])
- # If the total log lines is > max_history_size, delete the oldest line.
- if self.get_total_count() > self.max_history_size:
- self.byte_size -= sys.getsizeof(self.logs.popleft())
-
def emit(self, record) -> None:
"""Process a new log record.
diff --git a/pw_console/py/pw_console/log_view.py b/pw_console/py/pw_console/log_view.py
index 2b54f9eb2..74ebd3d30 100644
--- a/pw_console/py/pw_console/log_view.py
+++ b/pw_console/py/pw_console/log_view.py
@@ -19,15 +19,17 @@ import collections
import copy
from enum import Enum
import itertools
+import json
import logging
import operator
from pathlib import Path
import re
-import time
+from threading import Thread
from typing import Callable, Dict, List, Optional, Tuple, TYPE_CHECKING
from prompt_toolkit.data_structures import Point
from prompt_toolkit.formatted_text import StyleAndTextTuples
+import websockets
from pw_console.log_filter import (
DEFAULT_SEARCH_MATCHER,
@@ -38,6 +40,7 @@ from pw_console.log_filter import (
)
from pw_console.log_screen import ScreenLine, LogScreen
from pw_console.log_store import LogStore
+from pw_console.python_logging import log_record_to_json
from pw_console.text_formatting import remove_formatting
if TYPE_CHECKING:
@@ -50,6 +53,7 @@ _LOG = logging.getLogger(__package__)
class FollowEvent(Enum):
"""Follow mode scroll event types."""
+
SEARCH_MATCH = 'scroll_to_bottom'
STICKY_FOLLOW = 'scroll_to_bottom_with_sticky_follow'
@@ -67,8 +71,9 @@ class LogView:
):
# Parent LogPane reference. Updated by calling `set_log_pane()`.
self.log_pane = log_pane
- self.log_store = log_store if log_store else LogStore(
- prefs=application.prefs)
+ self.log_store = (
+ log_store if log_store else LogStore(prefs=application.prefs)
+ )
self.log_store.set_prefs(application.prefs)
self.log_store.register_viewer(self)
@@ -108,7 +113,8 @@ class LogView:
# Filter
self.filtering_on: bool = False
self.filters: 'collections.OrderedDict[str, LogFilter]' = (
- collections.OrderedDict())
+ collections.OrderedDict()
+ )
self.filtered_logs: collections.deque = collections.deque()
self.filter_existing_logs_task: Optional[asyncio.Task] = None
@@ -128,12 +134,10 @@ class LogView:
self._reset_log_screen_on_next_render: bool = True
self._user_scroll_event: bool = False
- # Max frequency in seconds of prompt_toolkit UI redraws triggered by new
- # log lines.
- self._ui_update_frequency = 0.05
- self._last_ui_update_time = time.time()
self._last_log_store_index = 0
self._new_logs_since_last_render = True
+ self._new_logs_since_last_websocket_serve = True
+ self._last_served_websocket_index = -1
# Should new log lines be tailed?
self.follow: bool = True
@@ -143,6 +147,78 @@ class LogView:
# Cache of formatted text tuples used in the last UI render.
self._line_fragment_cache: List[StyleAndTextTuples] = []
+ # websocket server variables
+ self.websocket_running: bool = False
+ self.websocket_server = None
+ self.websocket_port = None
+ self.websocket_loop = asyncio.new_event_loop()
+
+ # Check if any logs are already in the log_store and update the view.
+ self.new_logs_arrived()
+
+ def _websocket_thread_entry(self):
+ """Entry point for the user code thread."""
+ asyncio.set_event_loop(self.websocket_loop)
+ self.websocket_server = websockets.serve( # type: ignore # pylint: disable=no-member
+ self._send_logs_over_websockets, '127.0.0.1'
+ )
+ self.websocket_loop.run_until_complete(self.websocket_server)
+ self.websocket_port = self.websocket_server.ws_server.sockets[
+ 0
+ ].getsockname()[1]
+ self.log_pane.application.application.clipboard.set_text(
+ self.get_web_socket_url()
+ )
+ self.websocket_running = True
+ self.websocket_loop.run_forever()
+
+ def start_websocket_thread(self):
+ """Create a thread for running user code so the UI isn't blocked."""
+ thread = Thread(
+ target=self._websocket_thread_entry, args=(), daemon=True
+ )
+ thread.start()
+
+ def stop_websocket_thread(self):
+ """Stop websocket server."""
+ if self.websocket_running:
+ self.websocket_loop.call_soon_threadsafe(self.websocket_loop.stop)
+ self.websocket_server = None
+ self.websocket_port = None
+ self.websocket_running = False
+ if self.filtering_on:
+ self._restart_filtering()
+
+ async def _send_logs_over_websockets(self, websocket, _path) -> None:
+ formatter: Callable[[LogLine], str] = operator.attrgetter(
+ 'ansi_stripped_log'
+ )
+ formatter = lambda log: log_record_to_json(log.record)
+
+ theme_colors = json.dumps(
+ self.log_pane.application.prefs.pw_console_color_config()
+ )
+ # Send colors
+ await websocket.send(theme_colors)
+
+ while True:
+ # Wait for new logs
+ if not self._new_logs_since_last_websocket_serve:
+ await asyncio.sleep(0.5)
+
+ _start_log_index, log_source = self._get_log_lines()
+ log_index_range = range(
+ self._last_served_websocket_index + 1, self.get_total_count()
+ )
+
+ for i in log_index_range:
+ log_text = formatter(log_source[i])
+ await websocket.send(log_text)
+ self._last_served_websocket_index = i
+
+ # Flag that all logs have been served.
+ self._new_logs_since_last_websocket_serve = False
+
def view_mode_changed(self) -> None:
self._reset_log_screen_on_next_render = True
@@ -232,16 +308,15 @@ class LogView:
self._set_match_position(i)
return
- def set_search_regex(self,
- text,
- invert,
- field,
- matcher: Optional[SearchMatcher] = None) -> bool:
+ def set_search_regex(
+ self, text, invert, field, matcher: Optional[SearchMatcher] = None
+ ) -> bool:
search_matcher = matcher if matcher else self.search_matcher
_LOG.debug(search_matcher)
regex_text, regex_flags = preprocess_search_regex(
- text, matcher=search_matcher)
+ text, matcher=search_matcher
+ )
try:
compiled_regex = re.compile(regex_text, regex_flags)
@@ -271,8 +346,10 @@ class LogView:
"""Start a new search for the given text."""
valid_matchers = list(s.name for s in SearchMatcher)
selected_matcher: Optional[SearchMatcher] = None
- if (search_matcher is not None
- and search_matcher.upper() in valid_matchers):
+ if (
+ search_matcher is not None
+ and search_matcher.upper() in valid_matchers
+ ):
selected_matcher = SearchMatcher(search_matcher.upper())
if not self.set_search_regex(text, invert, field, selected_matcher):
@@ -284,7 +361,8 @@ class LogView:
if interactive:
# Start count historical search matches task.
self.search_match_count_task = asyncio.create_task(
- self.count_search_matches())
+ self.count_search_matches()
+ )
# Default search direction when hitting enter in the search bar.
if interactive:
@@ -299,7 +377,8 @@ class LogView:
# Save this log_index and its match number.
log_index: match_number
for match_number, log_index in enumerate(
- sorted(self.search_matched_lines.keys()))
+ sorted(self.search_matched_lines.keys())
+ )
}
def disable_search_highlighting(self):
@@ -317,7 +396,8 @@ class LogView:
# Start filtering existing log lines.
self.filter_existing_logs_task = asyncio.create_task(
- self.filter_past_logs())
+ self.filter_past_logs()
+ )
# Reset existing search
self.clear_search()
@@ -339,6 +419,8 @@ class LogView:
def apply_filter(self):
"""Set new filter and schedule historical log filter asyncio task."""
+ if self.websocket_running:
+ return
self.install_new_filter()
self._restart_filtering()
@@ -363,7 +445,8 @@ class LogView:
_, logs = self._get_log_lines()
if self._scrollback_start_index > 0:
return collections.deque(
- itertools.islice(logs, self.hidden_line_count(), len(logs)))
+ itertools.islice(logs, self.hidden_line_count(), len(logs))
+ )
return logs
def _get_table_formatter(self) -> Optional[Callable]:
@@ -392,7 +475,8 @@ class LogView:
self.clear_search()
self.filtering_on = False
self.filters: 'collections.OrderedDict[str, re.Pattern]' = (
- collections.OrderedDict())
+ collections.OrderedDict()
+ )
self.filtered_logs.clear()
# Reset scrollback start
self._scrollback_start_index = 0
@@ -415,7 +499,7 @@ class LogView:
self.save_search_matched_line(i)
# Pause every 100 lines or so
if i % 100 == 0:
- await asyncio.sleep(.1)
+ await asyncio.sleep(0.1)
async def filter_past_logs(self):
"""Filter past log lines."""
@@ -431,7 +515,7 @@ class LogView:
# TODO(tonymd): Tune these values.
# Pause every 100 lines or so
if i % 100 == 0:
- await asyncio.sleep(.1)
+ await asyncio.sleep(0.1)
def set_log_pane(self, log_pane: 'LogPane'):
"""Set the parent LogPane instance."""
@@ -449,8 +533,11 @@ class LogView:
def get_total_count(self):
"""Total size of the logs store."""
- return (len(self.filtered_logs)
- if self.filtering_on else self.log_store.get_total_count())
+ return (
+ len(self.filtered_logs)
+ if self.filtering_on
+ else self.log_store.get_total_count()
+ )
def get_last_log_index(self):
total = self.get_total_count()
@@ -531,24 +618,19 @@ class LogView:
self._last_log_store_index = latest_total
self._new_logs_since_last_render = True
+ self._new_logs_since_last_websocket_serve = True
if self.follow:
# Set the follow event flag for the next render_content call.
self.follow_event = FollowEvent.STICKY_FOLLOW
- # Trigger a UI update
- self._update_prompt_toolkit_ui()
-
- def _update_prompt_toolkit_ui(self):
- """Update Prompt Toolkit UI if a certain amount of time has passed."""
- emit_time = time.time()
- # Has enough time passed since last UI redraw?
- if emit_time > self._last_ui_update_time + self._ui_update_frequency:
- # Update last log time
- self._last_ui_update_time = emit_time
+ if self.websocket_running:
+ # No terminal screen redraws are required.
+ return
- # Trigger Prompt Toolkit UI redraw.
- self.log_pane.application.redraw_ui()
+ # Trigger a UI update if the log window is visible.
+ if self.log_pane.show_pane:
+ self.log_pane.application.logs_redraw()
def get_cursor_position(self) -> Point:
"""Return the position of the cursor."""
@@ -656,9 +738,9 @@ class LogView:
# Select the new line
self.visual_select_line(self.get_cursor_position(), autoscroll=False)
- def visual_select_line(self,
- mouse_position: Point,
- autoscroll: bool = True) -> None:
+ def visual_select_line(
+ self, mouse_position: Point, autoscroll: bool = True
+ ) -> None:
"""Mark the log under mouse_position as visually selected."""
# Check mouse_position is valid
if not 0 <= mouse_position.y < len(self.log_screen.line_buffer):
@@ -728,14 +810,19 @@ class LogView:
self.scroll(-1 * lines)
def log_start_end_indexes_changed(self) -> bool:
- return (self._last_start_index != self._current_start_index
- or self._last_end_index != self._current_end_index)
+ return (
+ self._last_start_index != self._current_start_index
+ or self._last_end_index != self._current_end_index
+ )
def render_table_header(self):
"""Get pre-formatted table header."""
return self.log_store.render_table_header()
- def render_content(self) -> list:
+ def get_web_socket_url(self):
+ return f'http://127.0.0.1:3000/#ws={self.websocket_port}'
+
+ def render_content(self) -> List:
"""Return logs to display on screen as a list of FormattedText tuples.
This function determines when the log screen requires re-rendeing based
@@ -745,6 +832,10 @@ class LogView:
"""
screen_update_needed = False
+ # Disable rendering if user is viewing logs on web
+ if self.websocket_running:
+ return []
+
# Check window size
if self.log_pane.pane_resized():
self._window_width = self.log_pane.current_log_pane_width
@@ -753,8 +844,10 @@ class LogView:
self._reset_log_screen_on_next_render = True
if self.follow_event is not None:
- if (self.follow_event == FollowEvent.SEARCH_MATCH
- and self.last_search_matched_log):
+ if (
+ self.follow_event == FollowEvent.SEARCH_MATCH
+ and self.last_search_matched_log
+ ):
self.log_index = self.last_search_matched_log
self.last_search_matched_log = None
self._reset_log_screen_on_next_render = True
@@ -780,14 +873,16 @@ class LogView:
last_rendered_log_index = self.log_screen.last_appended_log_index
# If so many logs have arrived than can fit on the screen, redraw
# the whole screen from the new position.
- if (current_log_index -
- last_rendered_log_index) > self.log_screen.height:
+ if (
+ current_log_index - last_rendered_log_index
+ ) > self.log_screen.height:
self.log_screen.reset_logs(log_index=self.log_index)
# A small amount of logs have arrived, append them one at a time
# without redrawing the whole screen.
else:
- for i in range(last_rendered_log_index + 1,
- current_log_index + 1):
+ for i in range(
+ last_rendered_log_index + 1, current_log_index + 1
+ ):
self.log_screen.append_log(i)
screen_update_needed = True
@@ -814,22 +909,29 @@ class LogView:
selected_lines_only: bool = False,
) -> str:
"""Convert all or selected log messages to plaintext."""
+
def get_table_string(log: LogLine) -> str:
return remove_formatting(self.log_store.table.formatted_row(log))
- formatter: Callable[[LogLine],
- str] = operator.attrgetter('ansi_stripped_log')
+ formatter: Callable[[LogLine], str] = operator.attrgetter(
+ 'ansi_stripped_log'
+ )
if use_table_formatting:
formatter = get_table_string
_start_log_index, log_source = self._get_log_lines()
- log_index_range = range(self._scrollback_start_index,
- self.get_total_count())
- if (selected_lines_only and self.marked_logs_start is not None
- and self.marked_logs_end is not None):
- log_index_range = range(self.marked_logs_start,
- self.marked_logs_end + 1)
+ log_index_range = range(
+ self._scrollback_start_index, self.get_total_count()
+ )
+ if (
+ selected_lines_only
+ and self.marked_logs_start is not None
+ and self.marked_logs_end is not None
+ ):
+ log_index_range = range(
+ self.marked_logs_start, self.marked_logs_end + 1
+ )
text_output = ''
for i in log_index_range:
@@ -849,8 +951,9 @@ class LogView:
add_markdown_fence: bool = False,
) -> bool:
"""Export log lines to file or clipboard."""
- text_output = self._logs_to_text(use_table_formatting,
- selected_lines_only)
+ text_output = self._logs_to_text(
+ use_table_formatting, selected_lines_only
+ )
if file_name:
target_path = Path(file_name).expanduser()
@@ -862,7 +965,8 @@ class LogView:
if add_markdown_fence:
text_output = '```\n' + text_output + '```\n'
self.log_pane.application.application.clipboard.set_text(
- text_output)
+ text_output
+ )
_LOG.debug('Copied logs to clipboard.')
return True
diff --git a/pw_console/py/pw_console/pigweed_code_style.py b/pw_console/py/pw_console/pigweed_code_style.py
index 325cac47a..a21cd6829 100644
--- a/pw_console/py/pw_console/pigweed_code_style.py
+++ b/pw_console/py/pw_console/pigweed_code_style.py
@@ -13,37 +13,93 @@
# the License.
"""Brighter PigweedCode pygments style."""
-import copy
import re
from prompt_toolkit.styles.style_transformation import get_opposite_color
from pygments.style import Style # type: ignore
-from pygments.token import Comment, Keyword, Name, Generic # type: ignore
-from pygments_style_dracula.dracula import DraculaStyle # type: ignore
-
-_style_list = copy.copy(DraculaStyle.styles)
-
-# Darker Prompt
-_style_list[Generic.Prompt] = '#bfbfbf'
-# Lighter comments
-_style_list[Comment] = '#778899'
-_style_list[Comment.Hashbang] = '#778899'
-_style_list[Comment.Multiline] = '#778899'
-_style_list[Comment.Preproc] = '#ff79c6'
-_style_list[Comment.Single] = '#778899'
-_style_list[Comment.Special] = '#778899'
-# Lighter output
-_style_list[Generic.Output] = '#f8f8f2'
-_style_list[Generic.Emph] = '#f8f8f2'
-# Remove 'italic' from these
-_style_list[Keyword.Declaration] = '#8be9fd'
-_style_list[Name.Builtin] = '#8be9fd'
-_style_list[Name.Label] = '#8be9fd'
-_style_list[Name.Variable] = '#8be9fd'
-_style_list[Name.Variable.Class] = '#8be9fd'
-_style_list[Name.Variable.Global] = '#8be9fd'
-_style_list[Name.Variable.Instance] = '#8be9fd'
+from pygments.token import Token # type: ignore
+
+_style_list = {
+ Token.Comment: '#778899', # Lighter comments
+ Token.Comment.Hashbang: '#778899',
+ Token.Comment.Multiline: '#778899',
+ Token.Comment.Preproc: '#ff79c6',
+ Token.Comment.PreprocFile: '',
+ Token.Comment.Single: '#778899',
+ Token.Comment.Special: '#778899',
+ Token.Error: '#f8f8f2',
+ Token.Escape: '',
+ Token.Generic.Deleted: '#8b080b',
+ Token.Generic.Emph: '#f8f8f2',
+ Token.Generic.Error: '#f8f8f2',
+ Token.Generic.Heading: '#f8f8f2 bold',
+ Token.Generic.Inserted: '#f8f8f2 bold',
+ Token.Generic.Output: '#f8f8f2',
+ Token.Generic.Prompt: '#bfbfbf', # Darker Prompt
+ Token.Generic.Strong: '#f8f8f2',
+ Token.Generic.Subheading: '#f8f8f2 bold',
+ Token.Generic.Traceback: '#f8f8f2',
+ Token.Generic: '#f8f8f2',
+ Token.Keyword.Constant: '#ff79c6',
+ Token.Keyword.Declaration: '#8be9fd',
+ Token.Keyword.Namespace: '#ff79c6',
+ Token.Keyword.Pseudo: '#ff79c6',
+ Token.Keyword.Reserved: '#ff79c6',
+ Token.Keyword.Type: '#8be9fd',
+ Token.Keyword: '#ff79c6',
+ Token.Literal.Date: '#f8f8f2',
+ Token.Literal.Number.Bin: '#bd93f9',
+ Token.Literal.Number.Float: '#bd93f9',
+ Token.Literal.Number.Hex: '#bd93f9',
+ Token.Literal.Number.Integer.Long: '#bd93f9',
+ Token.Literal.Number.Integer: '#bd93f9',
+ Token.Literal.Number.Oct: '#bd93f9',
+ Token.Literal.Number: '#bd93f9',
+ Token.Literal.String.Affix: '',
+ Token.Literal.String.Backtick: '#f1fa8c',
+ Token.Literal.String.Char: '#f1fa8c',
+ Token.Literal.String.Delimiter: '',
+ Token.Literal.String.Doc: '#f1fa8c',
+ Token.Literal.String.Double: '#f1fa8c',
+ Token.Literal.String.Escape: '#f1fa8c',
+ Token.Literal.String.Heredoc: '#f1fa8c',
+ Token.Literal.String.Interpol: '#f1fa8c',
+ Token.Literal.String.Other: '#f1fa8c',
+ Token.Literal.String.Regex: '#f1fa8c',
+ Token.Literal.String.Single: '#f1fa8c',
+ Token.Literal.String.Symbol: '#f1fa8c',
+ Token.Literal.String: '#f1fa8c',
+ Token.Literal: '#f8f8f2',
+ Token.Name.Attribute: '#50fa7b',
+ Token.Name.Builtin: '#8be9fd',
+ Token.Name.Builtin.Pseudo: '#f8f8f2',
+ Token.Name.Class: '#50fa7b',
+ Token.Name.Constant: '#f8f8f2',
+ Token.Name.Decorator: '#f8f8f2',
+ Token.Name.Entity: '#f8f8f2',
+ Token.Name.Exception: '#f8f8f2',
+ Token.Name.Function.Magic: '',
+ Token.Name.Function: '#50fa7b',
+ Token.Name.Label: '#8be9fd',
+ Token.Name.Namespace: '#f8f8f2',
+ Token.Name.Other: '#f8f8f2',
+ Token.Name.Property: '',
+ Token.Name.Tag: '#ff79c6',
+ Token.Name.Variable: '#8be9fd',
+ Token.Name.Variable.Class: '#8be9fd',
+ Token.Name.Variable.Global: '#8be9fd',
+ Token.Name.Variable.Instance: '#8be9fd',
+ Token.Name.Variable.Magic: '',
+ Token.Name: '#f8f8f2',
+ Token.Operator.Word: '#ff79c6',
+ Token.Operator: '#ff79c6',
+ Token.Other: '#f8f8f2',
+ Token.Punctuation: '#f8f8f2',
+ Token.Text.Whitespace: '#f8f8f2',
+ Token.Text: '#f8f8f2',
+ Token: '',
+}
_COLOR_REGEX = re.compile(r'#(?P<hex>[0-9a-fA-F]{6}) *(?P<rest>.*?)$')
@@ -63,7 +119,6 @@ def swap_light_dark(color: str) -> str:
class PigweedCodeStyle(Style):
-
background_color = '#2e2e2e'
default_style = ''
@@ -71,11 +126,7 @@ class PigweedCodeStyle(Style):
class PigweedCodeLightStyle(Style):
-
background_color = '#f8f8f8'
default_style = ''
- styles = {
- key: swap_light_dark(value)
- for key, value in _style_list.items()
- }
+ styles = {key: swap_light_dark(value) for key, value in _style_list.items()}
diff --git a/pw_console/py/pw_console/plugin_mixin.py b/pw_console/py/pw_console/plugin_mixin.py
index 146108086..5413934c5 100644
--- a/pw_console/py/pw_console/plugin_mixin.py
+++ b/pw_console/py/pw_console/plugin_mixin.py
@@ -80,6 +80,7 @@ class PluginMixin:
.. _Future: https://docs.python.org/3/library/asyncio-future.html
"""
+
def plugin_init(
self,
plugin_callback: Optional[Callable[..., bool]] = None,
@@ -130,7 +131,8 @@ class PluginMixin:
# This function will be executed in a separate thread.
self._plugin_periodically_run_callback(),
# Using this asyncio event loop.
- self.plugin_event_loop) # type: ignore
+ self.plugin_event_loop,
+ ) # type: ignore
def plugin_stop(self):
self.plugin_enable_background_task = False
diff --git a/pw_console/py/pw_console/plugins/bandwidth_toolbar.py b/pw_console/py/pw_console/plugins/bandwidth_toolbar.py
index e661a417f..e0135a6c5 100644
--- a/pw_console/py/pw_console/plugins/bandwidth_toolbar.py
+++ b/pw_console/py/pw_console/plugins/bandwidth_toolbar.py
@@ -22,6 +22,7 @@ from pw_console.pyserial_wrapper import BANDWIDTH_HISTORY_CONTEXTVAR
class BandwidthToolbar(WindowPaneToolbar, PluginMixin):
"""Toolbar for displaying bandwidth history."""
+
TOOLBAR_HEIGHT = 1
def _update_toolbar_text(self):
@@ -33,18 +34,27 @@ class BandwidthToolbar(WindowPaneToolbar, PluginMixin):
self.plugin_logger.debug('BandwidthToolbar _update_toolbar_text')
for count_name, events in self.history.items():
- tokens.extend([
- ('', ' '),
- ('class:theme-bg-active class:theme-fg-active',
- ' {}: '.format(count_name.title())),
- ('class:theme-bg-active class:theme-fg-cyan',
- '{:.3f} '.format(events.last_count())),
- ('class:theme-bg-active class:theme-fg-orange',
- '{} '.format(events.display_unit_title)),
- ])
+ tokens.extend(
+ [
+ ('', ' '),
+ (
+ 'class:theme-bg-active class:theme-fg-active',
+ ' {}: '.format(count_name.title()),
+ ),
+ (
+ 'class:theme-bg-active class:theme-fg-cyan',
+ '{:.3f} '.format(events.last_count()),
+ ),
+ (
+ 'class:theme-bg-active class:theme-fg-orange',
+ '{} '.format(events.display_unit_title),
+ ),
+ ]
+ )
if count_name == 'total':
tokens.append(
- ('class:theme-fg-cyan', '{}'.format(events.sparkline())))
+ ('class:theme-fg-cyan', '{}'.format(events.sparkline()))
+ )
self.formatted_text = tokens
@@ -57,9 +67,9 @@ class BandwidthToolbar(WindowPaneToolbar, PluginMixin):
return [('class:theme-fg-blue', 'Serial Bandwidth Usage ')]
def __init__(self, *args, **kwargs):
- super().__init__(*args,
- center_section_align=WindowAlign.RIGHT,
- **kwargs)
+ super().__init__(
+ *args, center_section_align=WindowAlign.RIGHT, **kwargs
+ )
self.history = BANDWIDTH_HISTORY_CONTEXTVAR.get()
self.show_toolbar = True
@@ -67,8 +77,10 @@ class BandwidthToolbar(WindowPaneToolbar, PluginMixin):
# Buttons for display in the center
self.add_button(
- ToolbarButton(description='Refresh',
- mouse_handler=self._update_toolbar_text))
+ ToolbarButton(
+ description='Refresh', mouse_handler=self._update_toolbar_text
+ )
+ )
# Set plugin options
self.background_task_update_count: int = 0
@@ -81,6 +93,8 @@ class BandwidthToolbar(WindowPaneToolbar, PluginMixin):
def _background_task(self) -> bool:
self.background_task_update_count += 1
self._update_toolbar_text()
- self.plugin_logger.debug('BandwidthToolbar Scheduled Update: #%s',
- self.background_task_update_count)
+ self.plugin_logger.debug(
+ 'BandwidthToolbar Scheduled Update: #%s',
+ self.background_task_update_count,
+ )
return True
diff --git a/pw_console/py/pw_console/plugins/calc_pane.py b/pw_console/py/pw_console/plugins/calc_pane.py
index f85a60072..b316e0cbe 100644
--- a/pw_console/py/pw_console/plugins/calc_pane.py
+++ b/pw_console/py/pw_console/plugins/calc_pane.py
@@ -42,6 +42,7 @@ class CalcPane(WindowPane):
Both input and output fields are prompt_toolkit TextArea objects which can
have their own options like syntax highlighting.
"""
+
def __init__(self):
# Call WindowPane.__init__ and set the title to 'Calculator'
super().__init__(pane_title='Calculator')
@@ -101,14 +102,16 @@ class CalcPane(WindowPane):
description='Run Calculation', # Button name
# Function to run when clicked.
mouse_handler=self.run_calculation,
- ))
+ )
+ )
self.bottom_toolbar.add_button(
ToolbarButton(
key='Ctrl-c', # Key binding for this function
description='Copy Output', # Button name
# Function to run when clicked.
mouse_handler=self.copy_all_output,
- ))
+ )
+ )
# self.container is the root container that contains objects to be
# rendered in the UI, one on top of the other.
@@ -142,15 +145,15 @@ class CalcPane(WindowPane):
prefs.register_named_key_function(
'calc-pane.copy-selected-text',
# default bindings
- ['c-c'])
+ ['c-c'],
+ )
# For setting additional keybindings to the output_field.
key_bindings = KeyBindings()
# Map the 'calc-pane.copy-selected-text' function keybind to the
# _copy_all_output function below. This will set
- @prefs.register_keybinding('calc-pane.copy-selected-text',
- key_bindings)
+ @prefs.register_keybinding('calc-pane.copy-selected-text', key_bindings)
def _copy_all_output(_event: KeyPressEvent) -> None:
"""Copy selected text from the output buffer."""
self.copy_selected_output()
@@ -176,7 +179,8 @@ class CalcPane(WindowPane):
output = "\n\nIn: {}\nOut: {}".format(
self.input_field.text,
# NOTE: Don't use 'eval' in real code (this is just an example)
- eval(self.input_field.text)) # pylint: disable=eval-used
+ eval(self.input_field.text), # pylint: disable=eval-used
+ )
except BaseException as exception: # pylint: disable=broad-except
output = "\n\n{}".format(exception)
@@ -186,7 +190,8 @@ class CalcPane(WindowPane):
# Update the output_field with the new contents and move the
# cursor_position to the end.
self.output_field.buffer.document = Document(
- text=new_text, cursor_position=len(new_text))
+ text=new_text, cursor_position=len(new_text)
+ )
def copy_selected_output(self):
"""Copy highlighted text in the output_field to the system clipboard."""
@@ -196,4 +201,5 @@ class CalcPane(WindowPane):
def copy_all_output(self):
"""Copy all text in the output_field to the system clipboard."""
self.application.application.clipboard.set_text(
- self.output_field.buffer.text)
+ self.output_field.buffer.text
+ )
diff --git a/pw_console/py/pw_console/plugins/clock_pane.py b/pw_console/py/pw_console/plugins/clock_pane.py
index 090ac8191..397c7c314 100644
--- a/pw_console/py/pw_console/plugins/clock_pane.py
+++ b/pw_console/py/pw_console/plugins/clock_pane.py
@@ -41,6 +41,7 @@ class ClockControl(FormattedTextControl):
This is the prompt_toolkit class that is responsible for drawing the clock,
handling keybindings if in focus, and mouse input.
"""
+
def __init__(self, clock_pane: 'ClockPane', *args, **kwargs) -> None:
self.clock_pane = clock_pane
@@ -110,6 +111,7 @@ class ClockPane(WindowPane, PluginMixin):
For an example see:
https://pigweed.dev/pw_console/embedding.html#adding-plugins
"""
+
def __init__(self, *args, **kwargs):
super().__init__(*args, pane_title='Clock', **kwargs)
# Some toggle settings to change view and wrap lines.
@@ -161,7 +163,8 @@ class ClockPane(WindowPane, PluginMixin):
description='View Mode', # Button name
# Function to run when clicked.
mouse_handler=self.toggle_view_mode,
- ))
+ )
+ )
# Add a checkbox button to display if wrap_lines is enabled.
self.bottom_toolbar.add_button(
@@ -174,7 +177,8 @@ class ClockPane(WindowPane, PluginMixin):
is_checkbox=True,
# lambda that returns the state of the checkbox
checked=lambda: self.wrap_lines,
- ))
+ )
+ )
# self.container is the root container that contains objects to be
# rendered in the UI, one on top of the other.
@@ -201,8 +205,10 @@ class ClockPane(WindowPane, PluginMixin):
self.background_task_update_count += 1
# Make a log message for debugging purposes. For more info see:
# https://pigweed.dev/pw_console/plugins.html#debugging-plugin-behavior
- self.plugin_logger.debug('background_task_update_count: %s',
- self.background_task_update_count)
+ self.plugin_logger.debug(
+ 'background_task_update_count: %s',
+ self.background_task_update_count,
+ )
# Returning True in the background task will force the user interface to
# re-draw.
@@ -231,8 +237,9 @@ class ClockPane(WindowPane, PluginMixin):
# pylint: disable=no-self-use
# Get the date and time
- date, time = datetime.now().isoformat(sep='_',
- timespec='seconds').split('_')
+ date, time = (
+ datetime.now().isoformat(sep='_', timespec='seconds').split('_')
+ )
# Formatted text is represented as (style, text) tuples.
# For more examples see:
@@ -240,7 +247,7 @@ class ClockPane(WindowPane, PluginMixin):
# These styles are selected using class names and start with the
# 'class:' prefix. For all classes defined by Pigweed Console see:
- # https://cs.opensource.google/pigweed/pigweed/+/main:pw_console/py/pw_console/style.py;l=189
+ # https://cs.pigweed.dev/pigweed/+/main:pw_console/py/pw_console/style.py;l=189
# Date in cyan matching the current Pigweed Console theme.
date_with_color = ('class:theme-fg-cyan', date)
@@ -252,14 +259,16 @@ class ClockPane(WindowPane, PluginMixin):
space = ('', ' ')
# Concatenate the (style, text) tuples.
- return FormattedText([
- line_break,
- space,
- space,
- date_with_color,
- space,
- time_with_color,
- ])
+ return FormattedText(
+ [
+ line_break,
+ space,
+ space,
+ date_with_color,
+ space,
+ time_with_color,
+ ]
+ )
def _get_example_text(self):
"""Examples of how to create formatted text."""
@@ -279,155 +288,178 @@ class ClockPane(WindowPane, PluginMixin):
# Standard ANSI colors examples
fragments.append(
- FormattedText([
- # These tuples follow this format:
- # (style_string, text_to_display)
- ('ansiblack', 'ansiblack'),
- wide_space,
- ('ansired', 'ansired'),
- wide_space,
- ('ansigreen', 'ansigreen'),
- wide_space,
- ('ansiyellow', 'ansiyellow'),
- wide_space,
- ('ansiblue', 'ansiblue'),
- wide_space,
- ('ansimagenta', 'ansimagenta'),
- wide_space,
- ('ansicyan', 'ansicyan'),
- wide_space,
- ('ansigray', 'ansigray'),
- wide_space,
+ FormattedText(
+ [
+ # These tuples follow this format:
+ # (style_string, text_to_display)
+ ('ansiblack', 'ansiblack'),
+ wide_space,
+ ('ansired', 'ansired'),
+ wide_space,
+ ('ansigreen', 'ansigreen'),
+ wide_space,
+ ('ansiyellow', 'ansiyellow'),
+ wide_space,
+ ('ansiblue', 'ansiblue'),
+ wide_space,
+ ('ansimagenta', 'ansimagenta'),
+ wide_space,
+ ('ansicyan', 'ansicyan'),
+ wide_space,
+ ('ansigray', 'ansigray'),
+ wide_space,
+ newline,
+ ('ansibrightblack', 'ansibrightblack'),
+ space,
+ ('ansibrightred', 'ansibrightred'),
+ space,
+ ('ansibrightgreen', 'ansibrightgreen'),
+ space,
+ ('ansibrightyellow', 'ansibrightyellow'),
+ space,
+ ('ansibrightblue', 'ansibrightblue'),
+ space,
+ ('ansibrightmagenta', 'ansibrightmagenta'),
+ space,
+ ('ansibrightcyan', 'ansibrightcyan'),
+ space,
+ ('ansiwhite', 'ansiwhite'),
+ space,
+ ]
+ )
+ )
+
+ fragments.append(HTML('\n<u>Background Colors</u>\n'))
+ fragments.append(
+ FormattedText(
+ [
+ # Here's an example of a style that specifies both
+ # background and foreground colors. The background color is
+ # prefixed with 'bg:'. The foreground color follows that
+ # with no prefix.
+ ('bg:ansiblack ansiwhite', 'ansiblack'),
+ wide_space,
+ ('bg:ansired', 'ansired'),
+ wide_space,
+ ('bg:ansigreen', 'ansigreen'),
+ wide_space,
+ ('bg:ansiyellow', 'ansiyellow'),
+ wide_space,
+ ('bg:ansiblue ansiwhite', 'ansiblue'),
+ wide_space,
+ ('bg:ansimagenta', 'ansimagenta'),
+ wide_space,
+ ('bg:ansicyan', 'ansicyan'),
+ wide_space,
+ ('bg:ansigray', 'ansigray'),
+ wide_space,
+ ('', '\n'),
+ ('bg:ansibrightblack', 'ansibrightblack'),
+ space,
+ ('bg:ansibrightred', 'ansibrightred'),
+ space,
+ ('bg:ansibrightgreen', 'ansibrightgreen'),
+ space,
+ ('bg:ansibrightyellow', 'ansibrightyellow'),
+ space,
+ ('bg:ansibrightblue', 'ansibrightblue'),
+ space,
+ ('bg:ansibrightmagenta', 'ansibrightmagenta'),
+ space,
+ ('bg:ansibrightcyan', 'ansibrightcyan'),
+ space,
+ ('bg:ansiwhite', 'ansiwhite'),
+ space,
+ ]
+ )
+ )
+
+ # pylint: disable=line-too-long
+ # These themes use Pigweed Console style classes. See full list in:
+ # https://cs.pigweed.dev/pigweed/+/main:pw_console/py/pw_console/style.py;l=189
+ # pylint: enable=line-too-long
+ fragments.append(HTML('\n\n<u>Current Theme Foreground Colors</u>\n'))
+ fragments.append(
+ [
+ ('class:theme-fg-red', 'class:theme-fg-red'),
newline,
- ('ansibrightblack', 'ansibrightblack'),
- space,
- ('ansibrightred', 'ansibrightred'),
- space,
- ('ansibrightgreen', 'ansibrightgreen'),
- space,
- ('ansibrightyellow', 'ansibrightyellow'),
- space,
- ('ansibrightblue', 'ansibrightblue'),
- space,
- ('ansibrightmagenta', 'ansibrightmagenta'),
+ ('class:theme-fg-orange', 'class:theme-fg-orange'),
+ newline,
+ ('class:theme-fg-yellow', 'class:theme-fg-yellow'),
+ newline,
+ ('class:theme-fg-green', 'class:theme-fg-green'),
+ newline,
+ ('class:theme-fg-cyan', 'class:theme-fg-cyan'),
+ newline,
+ ('class:theme-fg-blue', 'class:theme-fg-blue'),
+ newline,
+ ('class:theme-fg-purple', 'class:theme-fg-purple'),
+ newline,
+ ('class:theme-fg-magenta', 'class:theme-fg-magenta'),
+ newline,
+ ]
+ )
+
+ fragments.append(HTML('\n<u>Current Theme Background Colors</u>\n'))
+ fragments.append(
+ [
+ ('class:theme-bg-red', 'class:theme-bg-red'),
+ newline,
+ ('class:theme-bg-orange', 'class:theme-bg-orange'),
+ newline,
+ ('class:theme-bg-yellow', 'class:theme-bg-yellow'),
+ newline,
+ ('class:theme-bg-green', 'class:theme-bg-green'),
+ newline,
+ ('class:theme-bg-cyan', 'class:theme-bg-cyan'),
+ newline,
+ ('class:theme-bg-blue', 'class:theme-bg-blue'),
+ newline,
+ ('class:theme-bg-purple', 'class:theme-bg-purple'),
+ newline,
+ ('class:theme-bg-magenta', 'class:theme-bg-magenta'),
+ newline,
+ ]
+ )
+
+ fragments.append(HTML('\n<u>Theme UI Colors</u>\n'))
+ fragments.append(
+ [
+ ('class:theme-fg-default', 'class:theme-fg-default'),
space,
- ('ansibrightcyan', 'ansibrightcyan'),
+ ('class:theme-bg-default', 'class:theme-bg-default'),
space,
- ('ansiwhite', 'ansiwhite'),
+ ('class:theme-bg-active', 'class:theme-bg-active'),
space,
- ]))
-
- fragments.append(HTML('\n<u>Background Colors</u>\n'))
- fragments.append(
- FormattedText([
- # Here's an example of a style that specifies both background
- # and foreground colors. The background color is prefixed with
- # 'bg:'. The foreground color follows that with no prefix.
- ('bg:ansiblack ansiwhite', 'ansiblack'),
- wide_space,
- ('bg:ansired', 'ansired'),
- wide_space,
- ('bg:ansigreen', 'ansigreen'),
- wide_space,
- ('bg:ansiyellow', 'ansiyellow'),
- wide_space,
- ('bg:ansiblue ansiwhite', 'ansiblue'),
- wide_space,
- ('bg:ansimagenta', 'ansimagenta'),
- wide_space,
- ('bg:ansicyan', 'ansicyan'),
- wide_space,
- ('bg:ansigray', 'ansigray'),
- wide_space,
- ('', '\n'),
- ('bg:ansibrightblack', 'ansibrightblack'),
+ ('class:theme-fg-active', 'class:theme-fg-active'),
space,
- ('bg:ansibrightred', 'ansibrightred'),
+ ('class:theme-bg-inactive', 'class:theme-bg-inactive'),
space,
- ('bg:ansibrightgreen', 'ansibrightgreen'),
+ ('class:theme-fg-inactive', 'class:theme-fg-inactive'),
+ newline,
+ ('class:theme-fg-dim', 'class:theme-fg-dim'),
space,
- ('bg:ansibrightyellow', 'ansibrightyellow'),
+ ('class:theme-bg-dim', 'class:theme-bg-dim'),
space,
- ('bg:ansibrightblue', 'ansibrightblue'),
+ ('class:theme-bg-dialog', 'class:theme-bg-dialog'),
space,
- ('bg:ansibrightmagenta', 'ansibrightmagenta'),
+ (
+ 'class:theme-bg-line-highlight',
+ 'class:theme-bg-line-highlight',
+ ),
space,
- ('bg:ansibrightcyan', 'ansibrightcyan'),
+ (
+ 'class:theme-bg-button-active',
+ 'class:theme-bg-button-active',
+ ),
space,
- ('bg:ansiwhite', 'ansiwhite'),
+ (
+ 'class:theme-bg-button-inactive',
+ 'class:theme-bg-button-inactive',
+ ),
space,
- ]))
-
- # These themes use Pigweed Console style classes. See full list in:
- # https://cs.opensource.google/pigweed/pigweed/+/main:pw_console/py/pw_console/style.py;l=189
- fragments.append(HTML('\n\n<u>Current Theme Foreground Colors</u>\n'))
- fragments.append([
- ('class:theme-fg-red', 'class:theme-fg-red'),
- newline,
- ('class:theme-fg-orange', 'class:theme-fg-orange'),
- newline,
- ('class:theme-fg-yellow', 'class:theme-fg-yellow'),
- newline,
- ('class:theme-fg-green', 'class:theme-fg-green'),
- newline,
- ('class:theme-fg-cyan', 'class:theme-fg-cyan'),
- newline,
- ('class:theme-fg-blue', 'class:theme-fg-blue'),
- newline,
- ('class:theme-fg-purple', 'class:theme-fg-purple'),
- newline,
- ('class:theme-fg-magenta', 'class:theme-fg-magenta'),
- newline,
- ])
-
- fragments.append(HTML('\n<u>Current Theme Background Colors</u>\n'))
- fragments.append([
- ('class:theme-bg-red', 'class:theme-bg-red'),
- newline,
- ('class:theme-bg-orange', 'class:theme-bg-orange'),
- newline,
- ('class:theme-bg-yellow', 'class:theme-bg-yellow'),
- newline,
- ('class:theme-bg-green', 'class:theme-bg-green'),
- newline,
- ('class:theme-bg-cyan', 'class:theme-bg-cyan'),
- newline,
- ('class:theme-bg-blue', 'class:theme-bg-blue'),
- newline,
- ('class:theme-bg-purple', 'class:theme-bg-purple'),
- newline,
- ('class:theme-bg-magenta', 'class:theme-bg-magenta'),
- newline,
- ])
-
- fragments.append(HTML('\n<u>Theme UI Colors</u>\n'))
- fragments.append([
- ('class:theme-fg-default', 'class:theme-fg-default'),
- space,
- ('class:theme-bg-default', 'class:theme-bg-default'),
- space,
- ('class:theme-bg-active', 'class:theme-bg-active'),
- space,
- ('class:theme-fg-active', 'class:theme-fg-active'),
- space,
- ('class:theme-bg-inactive', 'class:theme-bg-inactive'),
- space,
- ('class:theme-fg-inactive', 'class:theme-fg-inactive'),
- newline,
- ('class:theme-fg-dim', 'class:theme-fg-dim'),
- space,
- ('class:theme-bg-dim', 'class:theme-bg-dim'),
- space,
- ('class:theme-bg-dialog', 'class:theme-bg-dialog'),
- space,
- ('class:theme-bg-line-highlight', 'class:theme-bg-line-highlight'),
- space,
- ('class:theme-bg-button-active', 'class:theme-bg-button-active'),
- space,
- ('class:theme-bg-button-inactive',
- 'class:theme-bg-button-inactive'),
- space,
- ])
+ ]
+ )
# Return all formatted text lists merged together.
return merge_formatted_text(fragments)
diff --git a/pw_console/py/pw_console/plugins/twenty48_pane.py b/pw_console/py/pw_console/plugins/twenty48_pane.py
new file mode 100644
index 000000000..891b2481d
--- /dev/null
+++ b/pw_console/py/pw_console/plugins/twenty48_pane.py
@@ -0,0 +1,561 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Example Plugin that displays some dynamic content: a game of 2048."""
+
+from random import choice
+from typing import Iterable, List, Tuple, TYPE_CHECKING
+import time
+
+from prompt_toolkit.filters import has_focus
+from prompt_toolkit.formatted_text import StyleAndTextTuples
+from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
+from prompt_toolkit.layout import (
+ AnyContainer,
+ Dimension,
+ FormattedTextControl,
+ HSplit,
+ Window,
+ WindowAlign,
+ VSplit,
+)
+from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
+from prompt_toolkit.widgets import MenuItem
+
+from pw_console.widgets import (
+ create_border,
+ FloatingWindowPane,
+ ToolbarButton,
+ WindowPaneToolbar,
+)
+from pw_console.plugin_mixin import PluginMixin
+from pw_console.get_pw_console_app import get_pw_console_app
+
+if TYPE_CHECKING:
+ from pw_console.console_app import ConsoleApp
+
+Twenty48Cell = Tuple[int, int, int]
+
+
+class Twenty48Game:
+ """2048 Game."""
+
+ def __init__(self) -> None:
+ self.colors = {
+ 2: 'bg:#dd6',
+ 4: 'bg:#da6',
+ 8: 'bg:#d86',
+ 16: 'bg:#d66',
+ 32: 'bg:#d6a',
+ 64: 'bg:#a6d',
+ 128: 'bg:#66d',
+ 256: 'bg:#68a',
+ 512: 'bg:#6a8',
+ 1024: 'bg:#6d6',
+ 2048: 'bg:#0f8',
+ 4096: 'bg:#0ff',
+ }
+ self.board: List[List[int]]
+ self.last_board: List[Twenty48Cell]
+ self.move_count: int
+ self.width: int = 4
+ self.height: int = 4
+ self.max_value: int = 0
+ self.start_time: float
+ self.reset_game()
+
+ def reset_game(self) -> None:
+ self.start_time = time.time()
+ self.max_value = 2
+ self.move_count = 0
+ self.board = []
+ for _i in range(self.height):
+ self.board.append([0] * self.width)
+ self.last_board = list(self.all_cells())
+ self.add_random_tiles(2)
+
+ def stats(self) -> StyleAndTextTuples:
+ """Returns stats on the game in progress."""
+ elapsed_time = int(time.time() - self.start_time)
+ minutes = int(elapsed_time / 60.0)
+ seconds = elapsed_time % 60
+ fragments: StyleAndTextTuples = []
+ fragments.append(('', '\n'))
+ fragments.append(('', f'Moves: {self.move_count}'))
+ fragments.append(('', '\n'))
+ fragments.append(('', 'Time: {:0>2}:{:0>2}'.format(minutes, seconds)))
+ fragments.append(('', '\n'))
+ fragments.append(('', f'Max: {self.max_value}'))
+ fragments.append(('', '\n\n'))
+ fragments.append(('', 'Press R to restart\n'))
+ fragments.append(('', '\n'))
+ fragments.append(('', 'Arrow keys to move'))
+ return fragments
+
+ def __pt_formatted_text__(self) -> StyleAndTextTuples:
+ """Returns the game board formatted in a grid with colors."""
+ fragments: StyleAndTextTuples = []
+
+ def print_row(row: List[int], include_number: bool = False) -> None:
+ fragments.append(('', ' '))
+ for col in row:
+ style = 'class:theme-fg-default '
+ if col > 0:
+ style = '#000 '
+ style += self.colors.get(col, '')
+ text = ' ' * 6
+ if include_number:
+ text = '{:^6}'.format(col)
+ fragments.append((style, text))
+ fragments.append(('', '\n'))
+
+ fragments.append(('', '\n'))
+ for row in self.board:
+ print_row(row)
+ print_row(row, include_number=True)
+ print_row(row)
+
+ return fragments
+
+ def __repr__(self) -> str:
+ board = ''
+ for row_cells in self.board:
+ for column in row_cells:
+ board += '{:^6}'.format(column)
+ board += '\n'
+ return board
+
+ def all_cells(self) -> Iterable[Twenty48Cell]:
+ for row, row_cells in enumerate(self.board):
+ for col, cell_value in enumerate(row_cells):
+ yield (row, col, cell_value)
+
+ def update_max_value(self) -> None:
+ for _row, _col, value in self.all_cells():
+ if value > self.max_value:
+ self.max_value = value
+
+ def empty_cells(self) -> Iterable[Twenty48Cell]:
+ for row, row_cells in enumerate(self.board):
+ for col, cell_value in enumerate(row_cells):
+ if cell_value != 0:
+ continue
+ yield (row, col, cell_value)
+
+ def _board_changed(self) -> bool:
+ return self.last_board != list(self.all_cells())
+
+ def complete_move(self) -> None:
+ if not self._board_changed():
+ # Move did nothing, ignore.
+ return
+
+ self.update_max_value()
+ self.move_count += 1
+ self.add_random_tiles()
+ self.last_board = list(self.all_cells())
+
+ def add_random_tiles(self, count: int = 1) -> None:
+ for _i in range(count):
+ empty_cells = list(self.empty_cells())
+ if not empty_cells:
+ return
+ row, col, _value = choice(empty_cells)
+ self.board[row][col] = 2
+
+ def row(self, row_index: int) -> Iterable[Twenty48Cell]:
+ for col, cell_value in enumerate(self.board[row_index]):
+ yield (row_index, col, cell_value)
+
+ def col(self, col_index: int) -> Iterable[Twenty48Cell]:
+ for row, row_cells in enumerate(self.board):
+ for col, cell_value in enumerate(row_cells):
+ if col == col_index:
+ yield (row, col, cell_value)
+
+ def non_zero_row_values(self, index: int) -> Tuple[List, List]:
+ non_zero_values = [
+ value for row, col, value in self.row(index) if value != 0
+ ]
+ padding = [0] * (self.width - len(non_zero_values))
+ return (non_zero_values, padding)
+
+ def move_right(self) -> None:
+ for i in range(self.height):
+ non_zero_values, padding = self.non_zero_row_values(i)
+ self.board[i] = padding + non_zero_values
+
+ def move_left(self) -> None:
+ for i in range(self.height):
+ non_zero_values, padding = self.non_zero_row_values(i)
+ self.board[i] = non_zero_values + padding
+
+ def add_horizontal(self, reverse=False) -> None:
+ for i in range(self.width):
+ this_row = list(self.row(i))
+ if reverse:
+ this_row = list(reversed(this_row))
+ for row, col, this_cell in this_row:
+ if this_cell == 0 or col >= self.width - 1:
+ continue
+ next_cell = self.board[row][col + 1]
+ if this_cell == next_cell:
+ self.board[row][col] = 0
+ self.board[row][col + 1] = this_cell * 2
+ break
+
+ def non_zero_col_values(self, index: int) -> Tuple[List, List]:
+ non_zero_values = [
+ value for row, col, value in self.col(index) if value != 0
+ ]
+ padding = [0] * (self.height - len(non_zero_values))
+ return (non_zero_values, padding)
+
+ def _set_column(self, col_index: int, values: List[int]) -> None:
+ for row, value in enumerate(values):
+ self.board[row][col_index] = value
+
+ def add_vertical(self, reverse=False) -> None:
+ for i in range(self.height):
+ this_column = list(self.col(i))
+ if reverse:
+ this_column = list(reversed(this_column))
+ for row, col, this_cell in this_column:
+ if this_cell == 0 or row >= self.height - 1:
+ continue
+ next_cell = self.board[row + 1][col]
+ if this_cell == next_cell:
+ self.board[row][col] = 0
+ self.board[row + 1][col] = this_cell * 2
+ break
+
+ def move_down(self) -> None:
+ for col_index in range(self.width):
+ non_zero_values, padding = self.non_zero_col_values(col_index)
+ self._set_column(col_index, padding + non_zero_values)
+
+ def move_up(self) -> None:
+ for col_index in range(self.width):
+ non_zero_values, padding = self.non_zero_col_values(col_index)
+ self._set_column(col_index, non_zero_values + padding)
+
+ def press_down(self) -> None:
+ self.move_down()
+ self.add_vertical(reverse=True)
+ self.move_down()
+ self.complete_move()
+
+ def press_up(self) -> None:
+ self.move_up()
+ self.add_vertical()
+ self.move_up()
+ self.complete_move()
+
+ def press_right(self) -> None:
+ self.move_right()
+ self.add_horizontal(reverse=True)
+ self.move_right()
+ self.complete_move()
+
+ def press_left(self) -> None:
+ self.move_left()
+ self.add_horizontal()
+ self.move_left()
+ self.complete_move()
+
+
+class Twenty48Control(FormattedTextControl):
+ """Example prompt_toolkit UIControl for displaying formatted text.
+
+ This is the prompt_toolkit class that is responsible for drawing the 2048,
+ handling keybindings if in focus, and mouse input.
+ """
+
+ def __init__(self, twenty48_pane: 'Twenty48Pane', *args, **kwargs) -> None:
+ self.twenty48_pane = twenty48_pane
+ self.game = self.twenty48_pane.game
+
+ # Set some custom key bindings to toggle the view mode and wrap lines.
+ key_bindings = KeyBindings()
+
+ @key_bindings.add('R')
+ def _restart(_event: KeyPressEvent) -> None:
+ """Restart the game."""
+ self.game.reset_game()
+
+ @key_bindings.add('q')
+ def _quit(_event: KeyPressEvent) -> None:
+ """Quit the game."""
+ self.twenty48_pane.close_dialog()
+
+ @key_bindings.add('j')
+ @key_bindings.add('down')
+ def _move_down(_event: KeyPressEvent) -> None:
+ """Move down"""
+ self.game.press_down()
+
+ @key_bindings.add('k')
+ @key_bindings.add('up')
+ def _move_up(_event: KeyPressEvent) -> None:
+ """Move up."""
+ self.game.press_up()
+
+ @key_bindings.add('h')
+ @key_bindings.add('left')
+ def _move_left(_event: KeyPressEvent) -> None:
+ """Move left."""
+ self.game.press_left()
+
+ @key_bindings.add('l')
+ @key_bindings.add('right')
+ def _move_right(_event: KeyPressEvent) -> None:
+ """Move right."""
+ self.game.press_right()
+
+ # Include the key_bindings keyword arg when passing to the parent class
+ # __init__ function.
+ kwargs['key_bindings'] = key_bindings
+ # Call the parent FormattedTextControl.__init__
+ super().__init__(*args, **kwargs)
+
+ def mouse_handler(self, mouse_event: MouseEvent):
+ """Mouse handler for this control."""
+ # If the user clicks anywhere this function is run.
+
+ # Mouse positions relative to this control. x is the column starting
+ # from the left size as zero. y is the row starting with the top as
+ # zero.
+ _click_x = mouse_event.position.x
+ _click_y = mouse_event.position.y
+
+ # Mouse click behavior usually depends on if this window pane is in
+ # focus. If not in focus, then focus on it when left clicking. If
+ # already in focus then perform the action specific to this window.
+
+ # If not in focus, change focus to this 2048 pane and do nothing else.
+ if not has_focus(self.twenty48_pane)():
+ if mouse_event.event_type == MouseEventType.MOUSE_UP:
+ get_pw_console_app().focus_on_container(self.twenty48_pane)
+ # Mouse event handled, return None.
+ return None
+
+ # If code reaches this point, this window is already in focus.
+ # if mouse_event.event_type == MouseEventType.MOUSE_UP:
+ # # Toggle the view mode.
+ # self.twenty48_pane.toggle_view_mode()
+ # # Mouse event handled, return None.
+ # return None
+
+ # Mouse event not handled, return NotImplemented.
+ return NotImplemented
+
+
+class Twenty48Pane(FloatingWindowPane, PluginMixin):
+ """Example Pigweed Console plugin to play 2048.
+
+ The Twenty48Pane is a WindowPane based plugin that displays an interactive
+ game of 2048. It inherits from both WindowPane and PluginMixin. It can be
+ added on console startup by calling: ::
+
+ my_console.add_window_plugin(Twenty48Pane())
+
+ For an example see:
+ https://pigweed.dev/pw_console/embedding.html#adding-plugins
+ """
+
+ def __init__(self, include_resize_handle: bool = True, **kwargs):
+ super().__init__(
+ pane_title='2048',
+ height=Dimension(preferred=17),
+ width=Dimension(preferred=50),
+ **kwargs,
+ )
+ self.game = Twenty48Game()
+
+ # Hide by default.
+ self.show_pane = False
+
+ # Create a toolbar for display at the bottom of the 2048 window. It
+ # will show the window title and buttons.
+ self.bottom_toolbar = WindowPaneToolbar(
+ self, include_resize_handle=include_resize_handle
+ )
+
+ # Add a button to restart the game.
+ self.bottom_toolbar.add_button(
+ ToolbarButton(
+ key='R', # Key binding help text for this function
+ description='Restart', # Button name
+ # Function to run when clicked.
+ mouse_handler=self.game.reset_game,
+ )
+ )
+ # Add a button to restart the game.
+ self.bottom_toolbar.add_button(
+ ToolbarButton(
+ key='q', # Key binding help text for this function
+ description='Quit', # Button name
+ # Function to run when clicked.
+ mouse_handler=self.close_dialog,
+ )
+ )
+
+ # Every FormattedTextControl object (Twenty48Control) needs to live
+ # inside a prompt_toolkit Window() instance. Here is where you specify
+ # alignment, style, and dimensions. See the prompt_toolkit docs for all
+ # opitons:
+ # https://python-prompt-toolkit.readthedocs.io/en/latest/pages/reference.html#prompt_toolkit.layout.Window
+ self.twenty48_game_window = Window(
+ # Set the content to a Twenty48Control instance.
+ content=Twenty48Control(
+ self, # This Twenty48Pane class
+ self.game, # Content from Twenty48Game.__pt_formatted_text__()
+ show_cursor=False,
+ focusable=True,
+ ),
+ # Make content left aligned
+ align=WindowAlign.LEFT,
+ # These two set to false make this window fill all available space.
+ dont_extend_width=True,
+ dont_extend_height=False,
+ wrap_lines=False,
+ width=Dimension(preferred=28),
+ height=Dimension(preferred=15),
+ )
+
+ self.twenty48_stats_window = Window(
+ content=Twenty48Control(
+ self, # This Twenty48Pane class
+ self.game.stats, # Content from Twenty48Game.stats()
+ show_cursor=False,
+ focusable=True,
+ ),
+ # Make content left aligned
+ align=WindowAlign.LEFT,
+ # These two set to false make this window fill all available space.
+ width=Dimension(preferred=20),
+ dont_extend_width=False,
+ dont_extend_height=False,
+ wrap_lines=False,
+ )
+
+ # self.container is the root container that contains objects to be
+ # rendered in the UI, one on top of the other.
+ self.container = self._create_pane_container(
+ create_border(
+ HSplit(
+ [
+ # Vertical split content
+ VSplit(
+ [
+ # Left side will show the game board.
+ self.twenty48_game_window,
+ # Stats will be shown on the right.
+ self.twenty48_stats_window,
+ ]
+ ),
+ # The bottom_toolbar is shown below the VSplit.
+ self.bottom_toolbar,
+ ]
+ ),
+ title='2048',
+ border_style='class:command-runner-border',
+ # left_margin_columns=1,
+ # right_margin_columns=1,
+ )
+ )
+
+ self.dialog_content: List[AnyContainer] = [
+ # Vertical split content
+ VSplit(
+ [
+ # Left side will show the game board.
+ self.twenty48_game_window,
+ # Stats will be shown on the right.
+ self.twenty48_stats_window,
+ ]
+ ),
+ # The bottom_toolbar is shown below the VSplit.
+ self.bottom_toolbar,
+ ]
+ # Wrap the dialog content in a border
+ self.bordered_dialog_content = create_border(
+ HSplit(self.dialog_content),
+ title='2048',
+ border_style='class:command-runner-border',
+ )
+ # self.container is the root container that contains objects to be
+ # rendered in the UI, one on top of the other.
+ if include_resize_handle:
+ self.container = self._create_pane_container(*self.dialog_content)
+ else:
+ self.container = self._create_pane_container(
+ self.bordered_dialog_content
+ )
+
+ # This plugin needs to run a task in the background periodically and
+ # uses self.plugin_init() to set which function to run, and how often.
+ # This is provided by PluginMixin. See the docs for more info:
+ # https://pigweed.dev/pw_console/plugins.html#background-tasks
+ self.plugin_init(
+ plugin_callback=self._background_task,
+ # Run self._background_task once per second.
+ plugin_callback_frequency=1.0,
+ plugin_logger_name='pw_console_example_2048_plugin',
+ )
+
+ def get_top_level_menus(self) -> List[MenuItem]:
+ def _toggle_dialog() -> None:
+ self.toggle_dialog()
+
+ return [
+ MenuItem(
+ '[2048]',
+ children=[
+ MenuItem(
+ 'Example Top Level Menu', handler=None, disabled=True
+ ),
+ # Menu separator
+ MenuItem('-', None),
+ MenuItem('Show/Hide 2048 Game', handler=_toggle_dialog),
+ MenuItem('Restart', handler=self.game.reset_game),
+ ],
+ ),
+ ]
+
+ def pw_console_init(self, app: 'ConsoleApp') -> None:
+ """Set the Pigweed Console application instance.
+
+ This function is called after the Pigweed Console starts up and allows
+ access to the user preferences. Prefs is required for creating new
+ user-remappable keybinds."""
+ self.application = app
+
+ def _background_task(self) -> bool:
+ """Function run in the background for the ClockPane plugin."""
+ # Optional: make a log message for debugging purposes. For more info
+ # see:
+ # https://pigweed.dev/pw_console/plugins.html#debugging-plugin-behavior
+ # self.plugin_logger.debug('background_task_update_count: %s',
+ # self.background_task_update_count)
+
+ # Returning True in the background task will force the user interface to
+ # re-draw.
+ # Returning False means no updates required.
+
+ if self.show_pane:
+ # Return true so the game clock is updated.
+ return True
+
+ # Game window is hidden, don't redraw.
+ return False
diff --git a/pw_console/py/pw_console/progress_bar/__init__.py b/pw_console/py/pw_console/progress_bar/__init__.py
index 5e0fa188b..4a9a0337f 100644
--- a/pw_console/py/pw_console/progress_bar/__init__.py
+++ b/pw_console/py/pw_console/progress_bar/__init__.py
@@ -14,7 +14,8 @@
"""Pigweed Console progress bar functions."""
from pw_console.progress_bar.progress_bar_state import TASKS_CONTEXTVAR
from pw_console.progress_bar.progress_bar_task_counter import (
- ProgressBarTaskCounter)
+ ProgressBarTaskCounter,
+)
__all__ = [
'start_progress',
@@ -32,17 +33,17 @@ def start_progress(task_name: str, total: int, hide_eta=False):
progress_state.tasks[task_name] = ProgressBarTaskCounter(
name=task_name,
total=total,
- prompt_toolkit_counter=progress_state.instance(range(total),
- label=task_name))
+ prompt_toolkit_counter=progress_state.instance(
+ range(total), label=task_name
+ ),
+ )
ptc = progress_state.tasks[task_name].prompt_toolkit_counter
ptc.hide_eta = hide_eta # type: ignore
-def update_progress(task_name: str,
- count=1,
- completed=False,
- canceled=False,
- new_total=None):
+def update_progress(
+ task_name: str, count=1, completed=False, canceled=False, new_total=None
+):
progress_state = TASKS_CONTEXTVAR.get()
# The caller may not actually get canceled and will continue trying to
# update after an interrupt.
@@ -60,7 +61,9 @@ def update_progress(task_name: str,
progress_state.tasks[task_name].update(count)
# Check if all tasks are complete
- if (progress_state.instance is not None
- and progress_state.all_tasks_complete):
+ if (
+ progress_state.instance is not None
+ and progress_state.all_tasks_complete
+ ):
if hasattr(progress_state.instance, '__exit__'):
progress_state.instance.__exit__()
diff --git a/pw_console/py/pw_console/progress_bar/progress_bar_impl.py b/pw_console/py/pw_console/progress_bar/progress_bar_impl.py
index fc818ede3..08dd62d55 100644
--- a/pw_console/py/pw_console/progress_bar/progress_bar_impl.py
+++ b/pw_console/py/pw_console/progress_bar/progress_bar_impl.py
@@ -59,7 +59,6 @@ class TextIfNotHidden(Text):
) -> AnyFormattedText:
formatted_text = super().format(progress_bar, progress, width)
if hasattr(progress, 'hide_eta') and progress.hide_eta: # type: ignore
-
formatted_text = [('', ' ' * width)]
return formatted_text
@@ -92,13 +91,13 @@ class TimeLeftIfNotHidden(TimeLeft):
class ProgressBarImpl:
"""ProgressBar for rendering in an existing prompt_toolkit application."""
+
def __init__(
self,
title: AnyFormattedText = None,
formatters: Optional[Sequence[Formatter]] = None,
style: Optional[BaseStyle] = None,
) -> None:
-
self.title = title
self.formatters = formatters or create_default_formatters()
self.counters: List[ProgressBarCounter[object]] = []
@@ -121,19 +120,23 @@ class ProgressBarImpl:
progress_controls = [
Window(
- content=_ProgressControl(self, f), # type: ignore
+ content=_ProgressControl(self, f, None), # type: ignore
width=functools.partial(width_for_formatter, f),
- ) for f in self.formatters
+ )
+ for f in self.formatters
]
- self.container = HSplit([
- title_toolbar,
- VSplit(
- progress_controls,
- height=lambda: D(min=len(self.counters),
- max=len(self.counters)),
- ),
- ])
+ self.container = HSplit(
+ [
+ title_toolbar,
+ VSplit(
+ progress_controls,
+ height=lambda: D(
+ min=len(self.counters), max=len(self.counters)
+ ),
+ ),
+ ]
+ )
def __pt_container__(self):
return self.container
@@ -162,6 +165,7 @@ class ProgressBarImpl:
data,
label=label,
remove_when_done=remove_when_done,
- total=total)
+ total=total,
+ )
self.counters.append(counter)
return counter
diff --git a/pw_console/py/pw_console/progress_bar/progress_bar_state.py b/pw_console/py/pw_console/progress_bar/progress_bar_state.py
index de8486a59..159cc3165 100644
--- a/pw_console/py/pw_console/progress_bar/progress_bar_state.py
+++ b/pw_console/py/pw_console/progress_bar/progress_bar_state.py
@@ -30,17 +30,15 @@ from pw_console.progress_bar.progress_bar_impl import (
TimeLeftIfNotHidden,
)
from pw_console.progress_bar.progress_bar_task_counter import (
- ProgressBarTaskCounter)
+ ProgressBarTaskCounter,
+)
from pw_console.style import generate_styles
CUSTOM_FORMATTERS = [
formatters.Label(suffix=': '),
formatters.Rainbow(
- formatters.Bar(start='|Pigw',
- end='|',
- sym_a='e',
- sym_b='d!',
- sym_c=' ')),
+ formatters.Bar(start='|Pigw', end='|', sym_a='e', sym_b='d!', sym_c=' ')
+ ),
formatters.Text(' '),
formatters.Progress(),
formatters.Text(' ['),
@@ -67,16 +65,18 @@ class ProgressBarState:
"""Pigweed Console wide state for all repl progress bars.
An instance of this class is intended to be a global variable."""
+
tasks: Dict[str, ProgressBarTaskCounter] = field(default_factory=dict)
instance: Optional[Union[ProgressBar, ProgressBarImpl]] = None
def _install_sigint_handler(self) -> None:
"""Add ctrl-c handling if not running inside pw_console"""
+
def handle_sigint(_signum, _frame):
# Shut down the ProgressBar prompt_toolkit application
prog_bar = self.instance
if prog_bar is not None and hasattr(prog_bar, '__exit__'):
- prog_bar.__exit__()
+ prog_bar.__exit__() # pylint: disable=unnecessary-dunder-call
raise KeyboardInterrupt
signal.signal(signal.SIGINT, handle_sigint)
@@ -85,15 +85,17 @@ class ProgressBarState:
prog_bar = self.instance
if not prog_bar:
if prompt_toolkit_app_running():
- prog_bar = ProgressBarImpl(style=get_app_or_none().style,
- formatters=CUSTOM_FORMATTERS)
+ prog_bar = ProgressBarImpl(
+ style=get_app_or_none().style, formatters=CUSTOM_FORMATTERS
+ )
else:
self._install_sigint_handler()
- prog_bar = ProgressBar(style=generate_styles(),
- formatters=CUSTOM_FORMATTERS)
+ prog_bar = ProgressBar(
+ style=generate_styles(), formatters=CUSTOM_FORMATTERS
+ )
# Start the ProgressBar prompt_toolkit application in a separate
# thread.
- prog_bar.__enter__()
+ prog_bar.__enter__() # pylint: disable=unnecessary-dunder-call
self.instance = prog_bar
return self.instance
@@ -103,8 +105,11 @@ class ProgressBarState:
if task.completed or task.canceled:
ptc = task.prompt_toolkit_counter
self.tasks.pop(task_name, None)
- if (self.instance and self.instance.counters
- and ptc in self.instance.counters):
+ if (
+ self.instance
+ and self.instance.counters
+ and ptc in self.instance.counters
+ ):
self.instance.counters.remove(ptc)
@property
@@ -128,5 +133,6 @@ class ProgressBarState:
return None
-TASKS_CONTEXTVAR = (ContextVar('pw_console_progress_bar_tasks',
- default=ProgressBarState()))
+TASKS_CONTEXTVAR = ContextVar(
+ 'pw_console_progress_bar_tasks', default=ProgressBarState()
+)
diff --git a/pw_console/py/pw_console/progress_bar/progress_bar_task_counter.py b/pw_console/py/pw_console/progress_bar/progress_bar_task_counter.py
index 43eee1920..1da0cfb31 100644
--- a/pw_console/py/pw_console/progress_bar/progress_bar_task_counter.py
+++ b/pw_console/py/pw_console/progress_bar/progress_bar_task_counter.py
@@ -30,6 +30,7 @@ def _redraw_ui() -> None:
@dataclass
class ProgressBarTaskCounter:
"""Class to hold a single progress bar state."""
+
name: str
total: int
count: int = 0
diff --git a/pw_console/py/pw_console/pw_ptpython_repl.py b/pw_console/py/pw_console/pw_ptpython_repl.py
index 5d68578f8..03f405424 100644
--- a/pw_console/py/pw_console/pw_ptpython_repl.py
+++ b/pw_console/py/pw_console/pw_ptpython_repl.py
@@ -17,8 +17,12 @@ import asyncio
import functools
import io
import logging
+import os
import sys
+import shlex
+import subprocess
from typing import Iterable, Optional, TYPE_CHECKING
+from unittest.mock import patch
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.controls import BufferControl
@@ -29,28 +33,61 @@ from prompt_toolkit.filters import (
to_filter,
)
from ptpython.completer import ( # type: ignore
- CompletePrivateAttributes, PythonCompleter,
+ CompletePrivateAttributes,
+ PythonCompleter,
)
import ptpython.repl # type: ignore
from ptpython.layout import ( # type: ignore
- CompletionVisualisation, Dimension,
+ CompletionVisualisation,
+ Dimension,
)
+import pygments.plugin
-import pw_console.text_formatting
+from pw_console.pigweed_code_style import (
+ PigweedCodeStyle,
+ PigweedCodeLightStyle,
+)
+from pw_console.text_formatting import remove_formatting
if TYPE_CHECKING:
from pw_console.repl_pane import ReplPane
_LOG = logging.getLogger(__package__)
+_SYSTEM_COMMAND_LOG = logging.getLogger('pw_console_system_command')
+
+_original_find_plugin_styles = pygments.plugin.find_plugin_styles
+
+
+def _wrapped_find_plugin_styles():
+ """Patch pygment find_plugin_styles to also include Pigweed codes styles
+
+ This allows using these themes without requiring Python entrypoints.
+ """
+ for style in [
+ ('pigweed-code', PigweedCodeStyle),
+ ('pigweed-code-light', PigweedCodeLightStyle),
+ ]:
+ yield style
+ yield from _original_find_plugin_styles()
class MissingPtpythonBufferControl(Exception):
"""Exception for a missing ptpython BufferControl object."""
-class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-instance-attributes
+def _user_input_is_a_shell_command(text: str) -> bool:
+ return text.startswith('!')
+
+
+class PwPtPythonRepl(
+ ptpython.repl.PythonRepl
+): # pylint: disable=too-many-instance-attributes
"""A ptpython repl class with changes to code execution and output related
methods."""
+
+ @patch(
+ 'pygments.styles.find_plugin_styles', new=_wrapped_find_plugin_styles
+ )
def __init__(
self,
*args,
@@ -58,7 +95,6 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
extra_completers: Optional[Iterable] = None,
**ptpython_kwargs,
):
-
completer = None
if extra_completers:
# Create the default python completer used by
@@ -94,7 +130,8 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
self.show_status_bar = False
self.show_exit_confirmation = False
self.complete_private_attributes = (
- CompletePrivateAttributes.IF_NO_PUBLIC)
+ CompletePrivateAttributes.IF_NO_PUBLIC
+ )
# Function signature that shows args, kwargs, and types under the cursor
# of the input window.
@@ -106,7 +143,8 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
# Turn off the completion menu in ptpython. The CompletionsMenu in
# ConsoleApp.root_container will handle this.
self.completion_visualisation: CompletionVisualisation = (
- CompletionVisualisation.NONE)
+ CompletionVisualisation.NONE
+ )
# Additional state variables.
self.repl_pane: 'Optional[ReplPane]' = None
@@ -123,7 +161,8 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
'ptpython/layout.py#L598\n'
'\n'
'The installed version of ptpython may not be compatible with'
- ' pw console; please try re-running environment setup.')
+ ' pw console; please try re-running environment setup.'
+ )
try:
# Fetch the Window's BufferControl object.
@@ -136,8 +175,12 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
# [create_python_input_window()] + extra_body
# ), ...
ptpython_buffer_control = (
- self.ptpython_layout.root_container.children[0].children[0].
- children[0].content.children[0].content)
+ self.ptpython_layout.root_container.children[0] # type: ignore
+ .children[0]
+ .children[0]
+ .content.children[0]
+ .content
+ )
# This should be a BufferControl instance
if not isinstance(ptpython_buffer_control, BufferControl):
raise MissingPtpythonBufferControl(error_message)
@@ -160,14 +203,12 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
def _save_result(self, formatted_text):
"""Save the last repl execution result."""
- unformatted_result = pw_console.text_formatting.remove_formatting(
- formatted_text)
+ unformatted_result = remove_formatting(formatted_text)
self._last_result = unformatted_result
def _save_exception(self, formatted_text):
"""Save the last repl exception."""
- unformatted_result = pw_console.text_formatting.remove_formatting(
- formatted_text)
+ unformatted_result = remove_formatting(formatted_text)
self._last_exception = unformatted_result
def clear_last_result(self):
@@ -216,8 +257,7 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
if result_object is not None:
# Use ptpython formatted results:
formatted_result = self._format_result_output(result_object)
- result_text = pw_console.text_formatting.remove_formatting(
- formatted_result)
+ result_text = remove_formatting(formatted_result)
# Job is finished, append the last result.
self.repl_pane.append_result_to_executed_code(
@@ -232,11 +272,59 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
# Rebuild output buffer.
self.repl_pane.update_output_buffer(
- 'pw_ptpython_repl.user_code_complete_callback')
+ 'pw_ptpython_repl.user_code_complete_callback'
+ )
# Trigger a prompt_toolkit application redraw.
self.repl_pane.application.application.invalidate()
+ async def _run_system_command( # pylint: disable=no-self-use
+ self, text, stdout_proxy, _stdin_proxy
+ ) -> int:
+ """Run a shell command and print results to the repl."""
+ command = shlex.split(text)
+ returncode = None
+ env = os.environ.copy()
+ # Force colors in Pigweed subcommands and some terminal apps.
+ env['PW_USE_COLOR'] = '1'
+ env['CLICOLOR_FORCE'] = '1'
+
+ def _handle_output(output):
+ # Force tab characters to 8 spaces to prevent \t from showing in
+ # prompt_toolkit.
+ output = output.replace('\t', ' ')
+ # Strip some ANSI sequences that don't render.
+ output = output.replace('\x1b(B\x1b[m', '')
+ output = output.replace('\x1b[1m', '')
+ stdout_proxy.write(output)
+ _SYSTEM_COMMAND_LOG.info(output.rstrip())
+
+ with subprocess.Popen(
+ command,
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ errors='replace',
+ ) as proc:
+ # Print the command
+ _SYSTEM_COMMAND_LOG.info('')
+ _SYSTEM_COMMAND_LOG.info('$ %s', text)
+ while returncode is None:
+ if not proc.stdout:
+ continue
+
+ # Check for one line and update.
+ output = proc.stdout.readline()
+ _handle_output(output)
+
+ returncode = proc.poll()
+
+ # Print any remaining lines.
+ for output in proc.stdout.readlines():
+ _handle_output(output)
+
+ return returncode
+
async def _run_user_code(self, text, stdout_proxy, stdin_proxy):
"""Run user code and capture stdout+err.
@@ -256,7 +344,12 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
# Run user repl code
try:
- result = await self.run_and_show_expression_async(text)
+ if _user_input_is_a_shell_command(text):
+ result = await self._run_system_command(
+ text[1:], stdout_proxy, stdin_proxy
+ )
+ else:
+ result = await self.run_and_show_expression_async(text)
finally:
# Always restore original stdout and stderr
sys.stdout = original_stdout
@@ -269,7 +362,7 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
return {
'stdout': stdout_contents,
'stderr': stderr_contents,
- 'result': result
+ 'result': result,
}
def _accept_handler(self, buff: Buffer) -> bool:
@@ -297,23 +390,30 @@ class PwPtPythonRepl(ptpython.repl.PythonRepl): # pylint: disable=too-many-inst
repl_input_text = ''
# Override stdout
temp_stdout.write(
- 'Error: Interactive help() is not compatible with this repl.')
+ 'Error: Interactive help() is not compatible with this repl.'
+ )
+
+ # Pop open the system command log pane for shell commands.
+ if _user_input_is_a_shell_command(repl_input_text):
+ self.repl_pane.application.setup_command_runner_log_pane()
# Execute the repl code in the the separate user_code thread loop.
future = asyncio.run_coroutine_threadsafe(
# This function will be executed in a separate thread.
self._run_user_code(repl_input_text, temp_stdout, temp_stderr),
# Using this asyncio event loop.
- self.repl_pane.application.user_code_loop) # type: ignore
+ self.repl_pane.application.user_code_loop,
+ ) # type: ignore
# Save the input text and future object.
- self.repl_pane.append_executed_code(repl_input_text, future,
- temp_stdout,
- temp_stderr) # type: ignore
+ self.repl_pane.append_executed_code(
+ repl_input_text, future, temp_stdout, temp_stderr
+ ) # type: ignore
# Run user_code_complete_callback() when done.
- done_callback = functools.partial(self.user_code_complete_callback,
- repl_input_text)
+ done_callback = functools.partial(
+ self.user_code_complete_callback, repl_input_text
+ )
future.add_done_callback(done_callback)
# Rebuild the parent ReplPane output buffer.
diff --git a/pw_console/py/pw_console/pyserial_wrapper.py b/pw_console/py/pw_console/pyserial_wrapper.py
index ac00e0520..57c297a52 100644
--- a/pw_console/py/pw_console/pyserial_wrapper.py
+++ b/pw_console/py/pw_console/pyserial_wrapper.py
@@ -12,19 +12,24 @@
# License for the specific language governing permissions and limitations under
# the License.
"""Wrapers for pyserial classes to log read and write data."""
+from __future__ import annotations
from contextvars import ContextVar
import logging
import textwrap
+from typing import TYPE_CHECKING
-import serial # type: ignore
+import serial
from pw_console.widgets.event_count_history import EventCountHistory
+if TYPE_CHECKING:
+ from _typeshed import ReadableBuffer
+
_LOG = logging.getLogger('pw_console.serial_debug_logger')
-def _log_hex_strings(data, prefix=''):
+def _log_hex_strings(data: bytes, prefix=''):
"""Create alinged hex number and character view log messages."""
# Make a list of 2 character hex number strings.
hex_numbers = textwrap.wrap(data.hex(), 2)
@@ -36,7 +41,7 @@ def _log_hex_strings(data, prefix=''):
.replace("'>", '', 1) # Remove ' from the end
.rjust(2)
for b in data
- ] # yapf: disable
+ ]
# Replace non-printable bytes with dots.
for i, num in enumerate(hex_numbers):
@@ -46,70 +51,120 @@ def _log_hex_strings(data, prefix=''):
hex_numbers_msg = ' '.join(hex_numbers)
hex_chars_msg = ' '.join(hex_chars)
- _LOG.debug('%s%s',
- prefix,
- hex_numbers_msg,
- extra=dict(extra_metadata_fields={
- 'msg': hex_numbers_msg,
- }))
- _LOG.debug('%s%s',
- prefix,
- hex_chars_msg,
- extra=dict(extra_metadata_fields={
- 'msg': hex_chars_msg,
- }))
-
-
-BANDWIDTH_HISTORY_CONTEXTVAR = (ContextVar('pw_console_bandwidth_history',
- default={
- 'total':
- EventCountHistory(interval=3),
- 'read':
- EventCountHistory(interval=3),
- 'write':
- EventCountHistory(interval=3),
- }))
+ _LOG.debug(
+ '%s%s',
+ prefix,
+ hex_numbers_msg,
+ extra=dict(
+ extra_metadata_fields={
+ 'msg': hex_numbers_msg,
+ 'view': 'hex',
+ }
+ ),
+ )
+ _LOG.debug(
+ '%s%s',
+ prefix,
+ hex_chars_msg,
+ extra=dict(
+ extra_metadata_fields={
+ 'msg': hex_chars_msg,
+ 'view': 'chars',
+ }
+ ),
+ )
+
+
+BANDWIDTH_HISTORY_CONTEXTVAR = ContextVar(
+ 'pw_console_bandwidth_history',
+ default={
+ 'total': EventCountHistory(interval=3),
+ 'read': EventCountHistory(interval=3),
+ 'write': EventCountHistory(interval=3),
+ },
+)
class SerialWithLogging(serial.Serial): # pylint: disable=too-many-ancestors
"""pyserial with read and write wrappers for logging."""
+
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pw_bps_history = BANDWIDTH_HISTORY_CONTEXTVAR.get()
- def read(self, *args, **kwargs):
- data = super().read(*args, **kwargs)
+ def read(self, size: int = 1) -> bytes:
+ data = super().read(size)
self.pw_bps_history['read'].log(len(data))
self.pw_bps_history['total'].log(len(data))
if len(data) > 0:
prefix = 'Read %2d B: ' % len(data)
- _LOG.debug('%s%s',
- prefix,
- data,
- extra=dict(extra_metadata_fields={
- 'mode': 'Read',
- 'bytes': len(data),
- 'msg': str(data),
- }))
+ _LOG.debug(
+ '%s%s',
+ prefix,
+ data,
+ extra=dict(
+ extra_metadata_fields={
+ 'mode': 'Read',
+ 'bytes': len(data),
+ 'view': 'bytes',
+ 'msg': str(data),
+ }
+ ),
+ )
_log_hex_strings(data, prefix=prefix)
+ # Print individual lines
+ for line in data.decode(
+ encoding='utf-8', errors='ignore'
+ ).splitlines():
+ _LOG.debug(
+ '%s',
+ line,
+ extra=dict(
+ extra_metadata_fields={
+ 'msg': line,
+ 'view': 'lines',
+ }
+ ),
+ )
+
return data
- def write(self, data, *args, **kwargs):
- self.pw_bps_history['write'].log(len(data))
- self.pw_bps_history['total'].log(len(data))
+ def write(self, data: ReadableBuffer) -> None:
+ if isinstance(data, bytes) and len(data) > 0:
+ self.pw_bps_history['write'].log(len(data))
+ self.pw_bps_history['total'].log(len(data))
- if len(data) > 0:
prefix = 'Write %2d B: ' % len(data)
- _LOG.debug('%s%s',
- prefix,
- data,
- extra=dict(extra_metadata_fields={
- 'mode': 'Write',
- 'bytes': len(data),
- 'msg': str(data)
- }))
+ _LOG.debug(
+ '%s%s',
+ prefix,
+ data,
+ extra=dict(
+ extra_metadata_fields={
+ 'mode': 'Write',
+ 'bytes': len(data),
+ 'view': 'bytes',
+ 'msg': str(data),
+ }
+ ),
+ )
_log_hex_strings(data, prefix=prefix)
- super().write(data, *args, **kwargs)
+ # Print individual lines
+ for line in data.decode(
+ encoding='utf-8', errors='ignore'
+ ).splitlines():
+ _LOG.debug(
+ '%s',
+ line,
+ extra=dict(
+ extra_metadata_fields={
+ 'msg': line,
+ 'view': 'lines',
+ }
+ ),
+ )
+
+ super().write(data)
diff --git a/pw_console/py/pw_console/python_logging.py b/pw_console/py/pw_console/python_logging.py
index 7f5d9beaa..362158909 100644
--- a/pw_console/py/pw_console/python_logging.py
+++ b/pw_console/py/pw_console/python_logging.py
@@ -14,10 +14,11 @@
"""Python logging helper fuctions."""
import copy
+from datetime import datetime
+import json
import logging
import tempfile
-from datetime import datetime
-from typing import Iterable, Iterator, Optional
+from typing import Any, Dict, Iterable, Iterator, Optional
def all_loggers() -> Iterator[logging.Logger]:
@@ -28,8 +29,9 @@ def all_loggers() -> Iterator[logging.Logger]:
yield logging.getLogger(logger_name)
-def create_temp_log_file(prefix: Optional[str] = None,
- add_time: bool = True) -> str:
+def create_temp_log_file(
+ prefix: Optional[str] = None, add_time: bool = True
+) -> str:
"""Create a unique tempfile for saving logs.
Example format: /tmp/pw_console_2021-05-04_151807_8hem6iyq
@@ -38,23 +40,25 @@ def create_temp_log_file(prefix: Optional[str] = None,
prefix = str(__package__)
# Grab the current system timestamp as a string.
- isotime = datetime.now().isoformat(sep='_', timespec='seconds')
+ isotime = datetime.now().isoformat(sep="_", timespec="seconds")
# Timestamp string should not have colons in it.
- isotime = isotime.replace(':', '')
+ isotime = isotime.replace(":", "")
if add_time:
- prefix += f'_{isotime}'
+ prefix += f"_{isotime}"
log_file_name = None
- with tempfile.NamedTemporaryFile(prefix=f'{prefix}_',
- delete=False) as log_file:
+ with tempfile.NamedTemporaryFile(
+ prefix=f"{prefix}_", delete=False
+ ) as log_file:
log_file_name = log_file.name
return log_file_name
def set_logging_last_resort_file_handler(
- file_name: Optional[str] = None) -> None:
+ file_name: Optional[str] = None,
+) -> None:
log_file = file_name if file_name else create_temp_log_file()
logging.lastResort = logging.FileHandler(log_file)
@@ -64,13 +68,15 @@ def disable_stdout_handlers(logger: logging.Logger) -> None:
for handler in copy.copy(logger.handlers):
# Must use type() check here since this returns True:
# isinstance(logging.FileHandler, logging.StreamHandler)
- if type(handler) == logging.StreamHandler: # pylint: disable=unidiomatic-typecheck
+ # pylint: disable=unidiomatic-typecheck
+ if type(handler) == logging.StreamHandler:
logger.removeHandler(handler)
+ # pylint: enable=unidiomatic-typecheck
def setup_python_logging(
last_resort_filename: Optional[str] = None,
- loggers_with_no_propagation: Optional[Iterable[logging.Logger]] = None
+ loggers_with_no_propagation: Optional[Iterable[logging.Logger]] = None,
) -> None:
"""Disable log handlers for full screen prompt_toolkit applications."""
if not loggers_with_no_propagation:
@@ -90,21 +96,93 @@ def setup_python_logging(
# Prevent these loggers from propagating to the root logger.
hidden_host_loggers = [
- 'pw_console',
- 'pw_console.plugins',
-
+ "pw_console",
+ "pw_console.plugins",
# prompt_toolkit triggered debug log messages
- 'prompt_toolkit',
- 'prompt_toolkit.buffer',
- 'parso.python.diff',
- 'parso.cache',
- 'pw_console.serial_debug_logger',
+ "prompt_toolkit",
+ "prompt_toolkit.buffer",
+ "parso.python.diff",
+ "parso.cache",
+ "pw_console.serial_debug_logger",
]
for logger_name in hidden_host_loggers:
logging.getLogger(logger_name).propagate = False
# Set asyncio log level to WARNING
- logging.getLogger('asyncio').setLevel(logging.WARNING)
+ logging.getLogger("asyncio").setLevel(logging.WARNING)
# Always set DEBUG level for serial debug.
- logging.getLogger('pw_console.serial_debug_logger').setLevel(logging.DEBUG)
+ logging.getLogger("pw_console.serial_debug_logger").setLevel(logging.DEBUG)
+
+
+def log_record_to_json(record: logging.LogRecord) -> str:
+ log_dict: Dict[str, Any] = {}
+ log_dict["message"] = record.getMessage()
+ log_dict["levelno"] = record.levelno
+ log_dict["levelname"] = record.levelname
+ log_dict["args"] = record.args
+
+ if hasattr(record, "extra_metadata_fields") and (
+ record.extra_metadata_fields # type: ignore
+ ):
+ fields = record.extra_metadata_fields # type: ignore
+ log_dict["fields"] = {}
+ for key, value in fields.items():
+ if key == "msg":
+ log_dict["message"] = value
+ continue
+
+ log_dict["fields"][key] = str(value)
+
+ return json.dumps(log_dict)
+
+
+class JsonLogFormatter(logging.Formatter):
+ """Json Python logging Formatter
+
+ Use this formatter to log pw_console messages to a file in json
+ format. Column values normally shown in table view will be populated in the
+ 'fields' key.
+
+ Example log entry:
+
+ .. code-block:: json
+
+ {
+ "message": "System init",
+ "levelno": 20,
+ "levelname": "INF",
+ "args": [
+ "0:00",
+ "pw_system ",
+ "System init"
+ ],
+ "fields": {
+ "module": "pw_system",
+ "file": "pw_system/init.cc",
+ "timestamp": "0:00"
+ }
+ }
+
+ Example usage:
+
+ .. code-block:: python
+
+ import logging
+ import pw_console.python_logging
+
+ _DEVICE_LOG = logging.getLogger('rpc_device')
+
+ json_filehandler = logging.FileHandler('logs.json', encoding='utf-8')
+ json_filehandler.setLevel(logging.DEBUG)
+ json_filehandler.setFormatter(
+ pw_console.python_logging.JsonLogFormatter())
+ _DEVICE_LOG.addHandler(json_filehandler)
+
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def format(self, record: logging.LogRecord) -> str:
+ return log_record_to_json(record)
diff --git a/pw_console/py/pw_console/quit_dialog.py b/pw_console/py/pw_console/quit_dialog.py
index b466580eb..6195dd2fb 100644
--- a/pw_console/py/pw_console/quit_dialog.py
+++ b/pw_console/py/pw_console/quit_dialog.py
@@ -30,9 +30,11 @@ from prompt_toolkit.layout import (
WindowAlign,
)
-import pw_console.widgets.border
-import pw_console.widgets.checkbox
-import pw_console.widgets.mouse_handlers
+from pw_console.widgets import (
+ create_border,
+ mouse_handlers,
+ to_keybind_indicator,
+)
if TYPE_CHECKING:
from pw_console.console_app import ConsoleApp
@@ -45,17 +47,18 @@ class QuitDialog(ConditionalContainer):
DIALOG_HEIGHT = 2
- def __init__(self,
- application: ConsoleApp,
- on_quit: Optional[Callable] = None):
+ def __init__(
+ self, application: ConsoleApp, on_quit: Optional[Callable] = None
+ ):
self.application = application
self.show_dialog = False
# Tracks the last focused container, to enable restoring focus after
# closing the dialog.
self.last_focused_pane = None
- self.on_quit_function = (on_quit if on_quit else
- self._default_on_quit_function)
+ self.on_quit_function = (
+ on_quit if on_quit else self._default_on_quit_function
+ )
# Quit keybindings are active when this dialog is in focus
key_bindings = KeyBindings()
@@ -82,13 +85,15 @@ class QuitDialog(ConditionalContainer):
get_cursor_position=lambda: Point(len(self.exit_message), 0),
)
- action_bar_window = Window(content=action_bar_control,
- height=QuitDialog.DIALOG_HEIGHT,
- align=WindowAlign.LEFT,
- dont_extend_width=False)
+ action_bar_window = Window(
+ content=action_bar_control,
+ height=QuitDialog.DIALOG_HEIGHT,
+ align=WindowAlign.LEFT,
+ dont_extend_width=False,
+ )
super().__init__(
- pw_console.widgets.border.create_border(
+ create_border(
HSplit(
[action_bar_window],
height=QuitDialog.DIALOG_HEIGHT,
@@ -133,12 +138,11 @@ class QuitDialog(ConditionalContainer):
"""Return FormattedText with action buttons."""
# Mouse handlers
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self)
- cancel = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.close_dialog)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
+ cancel = functools.partial(mouse_handlers.on_click, self.close_dialog)
quit_action = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self.quit_action)
+ mouse_handlers.on_click, self.quit_action
+ )
# Separator should have the focus mouse handler so clicking on any
# whitespace focuses the input field.
@@ -152,24 +156,26 @@ class QuitDialog(ConditionalContainer):
# Cancel button
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='n / Ctrl-c',
description='Cancel',
mouse_handler=cancel,
base_style=button_style,
- ))
+ )
+ )
# Two space separator
fragments.append(separator_text)
# Save button
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='y / Ctrl-d',
description='Quit',
mouse_handler=quit_action,
base_style=button_style,
- ))
+ )
+ )
# One space separator
fragments.append(('', ' ', focus))
diff --git a/pw_console/py/pw_console/repl_pane.py b/pw_console/py/pw_console/repl_pane.py
index 10d52a5e5..20a3b9504 100644
--- a/pw_console/py/pw_console/repl_pane.py
+++ b/pw_console/py/pw_console/repl_pane.py
@@ -21,11 +21,14 @@ import pprint
from dataclasses import dataclass
from typing import (
Any,
+ Awaitable,
Callable,
Dict,
List,
Optional,
+ Tuple,
TYPE_CHECKING,
+ Union,
)
from prompt_toolkit.filters import (
@@ -46,19 +49,19 @@ from prompt_toolkit.layout import (
)
from prompt_toolkit.lexers import PygmentsLexer # type: ignore
from pygments.lexers.python import PythonConsoleLexer # type: ignore
+
# Alternative Formatting
# from IPython.lib.lexers import IPythonConsoleLexer # type: ignore
from pw_console.progress_bar.progress_bar_state import TASKS_CONTEXTVAR
from pw_console.pw_ptpython_repl import PwPtPythonRepl
+from pw_console.style import get_pane_style
from pw_console.widgets import (
ToolbarButton,
WindowPane,
WindowPaneHSplit,
WindowPaneToolbar,
)
-import pw_console.mouse
-import pw_console.style
if TYPE_CHECKING:
from pw_console.console_app import ConsoleApp
@@ -74,13 +77,15 @@ _REPL_OUTPUT_SCROLL_AMOUNT = 5
@dataclass
class UserCodeExecution:
"""Class to hold a single user repl execution event."""
+
input: str
future: concurrent.futures.Future
output: str
stdout: str
stderr: str
- stdout_check_task: Optional[concurrent.futures.Future] = None
+ stdout_check_task: Optional[Awaitable] = None
result_object: Optional[Any] = None
+ result_str: Optional[str] = None
exception_text: Optional[str] = None
@property
@@ -109,7 +114,7 @@ class ReplPane(WindowPane):
) -> None:
super().__init__(application, pane_title)
- self.executed_code: List = []
+ self.executed_code: List[UserCodeExecution] = []
self.application = application
self.pw_ptpython_repl = python_repl
@@ -141,9 +146,11 @@ class ReplPane(WindowPane):
# Override output buffer mouse wheel scroll
self.output_field.window._scroll_up = ( # type: ignore
- self.scroll_output_up)
+ self.scroll_output_up
+ )
self.output_field.window._scroll_down = ( # type: ignore
- self.scroll_output_down)
+ self.scroll_output_down
+ )
self.bottom_toolbar = self._create_input_toolbar()
self.results_toolbar = self._create_output_toolbar()
@@ -164,10 +171,14 @@ class ReplPane(WindowPane):
# 2. Progress bars if any
ConditionalContainer(
DynamicContainer(
- self.get_progress_bar_task_container),
+ self.get_progress_bar_task_container
+ ),
filter=Condition(
- lambda: not self.progress_state.
- all_tasks_complete)),
+ # pylint: disable=line-too-long
+ lambda: not self.progress_state.all_tasks_complete
+ # pylint: enable=line-too-long
+ ),
+ ),
# 3. Static separator toolbar.
self.results_toolbar,
],
@@ -188,11 +199,12 @@ class ReplPane(WindowPane):
# Repl pane dimensions
height=lambda: self.height,
width=lambda: self.width,
- style=functools.partial(pw_console.style.get_pane_style,
- self),
+ style=functools.partial(get_pane_style, self),
),
- floats=[]),
- filter=Condition(lambda: self.show_pane))
+ floats=[],
+ ),
+ filter=Condition(lambda: self.show_pane),
+ )
def toggle_wrap_output_lines(self):
"""Enable or disable output line wraping/truncation."""
@@ -254,11 +266,15 @@ class ReplPane(WindowPane):
focus_check_container=self.pw_ptpython_repl,
)
bottom_toolbar.add_button(
- ToolbarButton('Ctrl-v', 'Paste',
- self.paste_system_clipboard_to_input_buffer))
+ ToolbarButton(
+ 'Ctrl-v', 'Paste', self.paste_system_clipboard_to_input_buffer
+ )
+ )
bottom_toolbar.add_button(
- ToolbarButton('Ctrl-c', 'Copy / Clear',
- self.copy_or_clear_input_buffer))
+ ToolbarButton(
+ 'Ctrl-c', 'Copy / Clear', self.copy_or_clear_input_buffer
+ )
+ )
bottom_toolbar.add_button(ToolbarButton('Enter', 'Run', self.run_code))
bottom_toolbar.add_button(ToolbarButton('F2', 'Settings'))
bottom_toolbar.add_button(ToolbarButton('F3', 'History'))
@@ -273,19 +289,32 @@ class ReplPane(WindowPane):
include_resize_handle=False,
)
results_toolbar.add_button(
- ToolbarButton(description='Wrap lines',
- mouse_handler=self.toggle_wrap_output_lines,
- is_checkbox=True,
- checked=lambda: self.wrap_output_lines))
+ ToolbarButton(
+ description='Wrap lines',
+ mouse_handler=self.toggle_wrap_output_lines,
+ is_checkbox=True,
+ checked=lambda: self.wrap_output_lines,
+ )
+ )
results_toolbar.add_button(
- ToolbarButton('Ctrl-Alt-c', 'Copy All Output',
- self.copy_all_output_text))
+ ToolbarButton(
+ 'Ctrl-Alt-c', 'Copy All Output', self.copy_all_output_text
+ )
+ )
results_toolbar.add_button(
- ToolbarButton('Ctrl-c', 'Copy Selected Text',
- self.copy_output_selection))
+ ToolbarButton(
+ 'Ctrl-c', 'Copy Selected Text', self.copy_output_selection
+ )
+ )
results_toolbar.add_button(
- ToolbarButton('Shift+Arrows / Mouse Drag', 'Select Text'))
+ ToolbarButton(
+ description='Clear', mouse_handler=self.clear_output_buffer
+ )
+ )
+ results_toolbar.add_button(
+ ToolbarButton('Shift+Arrows / Mouse Drag', 'Select Text')
+ )
return results_toolbar
@@ -302,12 +331,14 @@ class ReplPane(WindowPane):
def copy_all_output_text(self):
"""Copy all text in the Python output to the system clipboard."""
self.application.application.clipboard.set_text(
- self.output_field.buffer.text)
+ self.output_field.buffer.text
+ )
def copy_all_input_text(self):
"""Copy all text in the Python input to the system clipboard."""
self.application.application.clipboard.set_text(
- self.pw_ptpython_repl.default_buffer.text)
+ self.pw_ptpython_repl.default_buffer.text
+ )
# pylint: disable=no-self-use
def get_all_key_bindings(self) -> List:
@@ -316,16 +347,36 @@ class ReplPane(WindowPane):
# return [load_python_bindings(self.pw_ptpython_repl)]
# Hand-crafted bindings for display in the HelpWindow:
- return [{
- 'Execute code': ['Enter', 'Option-Enter', 'Alt-Enter'],
- 'Reverse search history': ['Ctrl-r'],
- 'Erase input buffer.': ['Ctrl-c'],
- 'Show settings.': ['F2'],
- 'Show history.': ['F3'],
- }]
-
- def get_all_menu_options(self):
- return []
+ return [
+ {
+ 'Execute code': ['Enter', 'Option-Enter', 'Alt-Enter'],
+ 'Reverse search history': ['Ctrl-r'],
+ 'Erase input buffer.': ['Ctrl-c'],
+ 'Show settings.': ['F2'],
+ 'Show history.': ['F3'],
+ }
+ ]
+
+ def get_window_menu_options(
+ self,
+ ) -> List[Tuple[str, Union[Callable, None]]]:
+ return [
+ (
+ 'Python Input > Paste',
+ self.paste_system_clipboard_to_input_buffer,
+ ),
+ ('Python Input > Copy or Clear', self.copy_or_clear_input_buffer),
+ ('Python Input > Run', self.run_code),
+ # Menu separator
+ ('-', None),
+ (
+ 'Python Output > Toggle Wrap lines',
+ self.toggle_wrap_output_lines,
+ ),
+ ('Python Output > Copy All', self.copy_all_output_text),
+ ('Python Output > Copy Selection', self.copy_output_selection),
+ ('Python Output > Clear', self.clear_output_buffer),
+ ]
def run_code(self):
"""Trigger a repl code execution on mouse click."""
@@ -339,6 +390,9 @@ class ReplPane(WindowPane):
else:
self.interrupt_last_code_execution()
+ def insert_text_into_input_buffer(self, text: str) -> None:
+ self.pw_ptpython_repl.default_buffer.insert_text(text)
+
def paste_system_clipboard_to_input_buffer(self, erase_buffer=False):
if erase_buffer:
self.clear_input_buffer()
@@ -352,6 +406,10 @@ class ReplPane(WindowPane):
# Clear any displayed function signatures.
self.pw_ptpython_repl.on_reset()
+ def clear_output_buffer(self):
+ self.executed_code.clear()
+ self.update_output_buffer()
+
def copy_or_clear_input_buffer(self):
# Copy selected text if a selection is active.
if self.pw_ptpython_repl.default_buffer.selection_state:
@@ -388,8 +446,9 @@ class ReplPane(WindowPane):
for line in text.splitlines():
_LOG.debug('[PYTHON %s] %s', prefix, line.strip())
- async def periodically_check_stdout(self, user_code: UserCodeExecution,
- stdout_proxy, stderr_proxy):
+ async def periodically_check_stdout(
+ self, user_code: UserCodeExecution, stdout_proxy, stderr_proxy
+ ):
while not user_code.future.done():
await asyncio.sleep(0.3)
stdout_text_so_far = stdout_proxy.getvalue()
@@ -403,15 +462,13 @@ class ReplPane(WindowPane):
self.update_output_buffer('repl_pane.periodic_check')
def append_executed_code(self, text, future, temp_stdout, temp_stderr):
- user_code = UserCodeExecution(input=text,
- future=future,
- output=None,
- stdout=None,
- stderr=None)
+ user_code = UserCodeExecution(
+ input=text, future=future, output=None, stdout=None, stderr=None
+ )
background_stdout_check = asyncio.create_task(
- self.periodically_check_stdout(user_code, temp_stdout,
- temp_stderr))
+ self.periodically_check_stdout(user_code, temp_stdout, temp_stderr)
+ )
user_code.stdout_check_task = background_stdout_check
self.executed_code.append(user_code)
self._log_executed_code(user_code, prefix='START')
@@ -426,7 +483,6 @@ class ReplPane(WindowPane):
exception_text='',
result_object=None,
):
-
code = self._get_executed_code(future)
if code:
code.output = result_text
@@ -434,21 +490,36 @@ class ReplPane(WindowPane):
code.stderr = stderr_text
code.exception_text = exception_text
code.result_object = result_object
+ if result_object is not None:
+ code.result_str = self._format_result_object(result_object)
+
self._log_executed_code(code, prefix='FINISH')
self.update_output_buffer('repl_pane.append_result_to_executed_code')
- def get_output_buffer_text(self, code_items=None, show_index=True):
- content_width = (self.current_pane_width
- if self.current_pane_width else 80)
+ def _format_result_object(self, result_object: Any) -> str:
+ """Pretty print format a Python object respecting the window width."""
+ content_width = (
+ self.current_pane_width if self.current_pane_width else 80
+ )
pprint_respecting_width = pprint.PrettyPrinter(
- indent=2, width=content_width).pformat
+ indent=2, width=content_width
+ ).pformat
+
+ return pprint_respecting_width(result_object)
+ def get_output_buffer_text(
+ self,
+ code_items: Optional[List[UserCodeExecution]] = None,
+ show_index: bool = True,
+ ):
executed_code = code_items or self.executed_code
template = self.application.get_template('repl_output.jinja')
- return template.render(code_items=executed_code,
- result_format=pprint_respecting_width,
- show_index=show_index)
+
+ return template.render(
+ code_items=executed_code,
+ show_index=show_index,
+ )
def update_output_buffer(self, *unused_args):
text = self.get_output_buffer_text()
@@ -456,16 +527,31 @@ class ReplPane(WindowPane):
# instead of the end of the last line.
text += '\n'
self.output_field.buffer.set_document(
- Document(text=text, cursor_position=len(text)))
+ Document(text=text, cursor_position=len(text))
+ )
self.application.redraw_ui()
def input_or_output_has_focus(self) -> Condition:
@Condition
def test() -> bool:
- if has_focus(self.output_field)() or has_focus(
- self.pw_ptpython_repl)():
+ if (
+ has_focus(self.output_field)()
+ or has_focus(self.pw_ptpython_repl)()
+ ):
return True
return False
return test
+
+ def history_completions(self) -> List[Tuple[str, str]]:
+ return [
+ (
+ ' '.join([line.lstrip() for line in text.splitlines()]),
+ # Pass original text as the completion result.
+ text,
+ )
+ for text in list(
+ self.pw_ptpython_repl.history.load_history_strings()
+ )
+ ]
diff --git a/pw_console/py/pw_console/search_toolbar.py b/pw_console/py/pw_console/search_toolbar.py
index 72ad91794..c140a074e 100644
--- a/pw_console/py/pw_console/search_toolbar.py
+++ b/pw_console/py/pw_console/search_toolbar.py
@@ -37,8 +37,11 @@ from prompt_toolkit.widgets import TextArea
from prompt_toolkit.validation import DynamicValidator
from pw_console.log_view import RegexValidator, SearchMatcher
-# import pw_console.widgets.checkbox
-import pw_console.widgets.mouse_handlers
+from pw_console.widgets import (
+ mouse_handlers,
+ to_checkbox_with_keybind_indicator,
+ to_keybind_indicator,
+)
if TYPE_CHECKING:
from pw_console.log_pane import LogPane
@@ -59,9 +62,14 @@ class SearchToolbar(ConditionalContainer):
self.input_field = TextArea(
prompt=[
- ('class:search-bar-setting', '/',
- functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self))
+ (
+ 'class:search-bar-setting',
+ '/',
+ functools.partial(
+ mouse_handlers.on_click,
+ self.focus_self,
+ ),
+ )
],
focusable=True,
focus_on_click=True,
@@ -111,28 +119,38 @@ class SearchToolbar(ConditionalContainer):
HSplit(
[
# Top row
- VSplit([
- # Search Settings toggles, only show if the search input
- # field is in focus.
- ConditionalContainer(settings_bar_window,
- filter=has_focus(
- self.input_field)),
-
- # Match count numbers and buttons, only show if the
- # search input is NOT in focus.
- ConditionalContainer(
- match_count_window,
- filter=~has_focus(self.input_field)), # pylint: disable=invalid-unary-operand-type
- ConditionalContainer(
- match_buttons_window,
- filter=~has_focus(self.input_field)), # pylint: disable=invalid-unary-operand-type
- ]),
+ VSplit(
+ [
+ # Search Settings toggles, only show if the search
+ # input field is in focus.
+ ConditionalContainer(
+ settings_bar_window,
+ filter=has_focus(self.input_field),
+ ),
+ # Match count numbers and buttons, only show if the
+ # search input is NOT in focus.
+ # pylint: disable=invalid-unary-operand-type
+ ConditionalContainer(
+ match_count_window,
+ filter=~has_focus(self.input_field),
+ ),
+ ConditionalContainer(
+ match_buttons_window,
+ filter=~has_focus(self.input_field),
+ ),
+ # pylint: enable=invalid-unary-operand-type
+ ]
+ ),
# Bottom row
- VSplit([
- self.input_field,
- ConditionalContainer(input_field_buttons_window,
- filter=has_focus(self))
- ])
+ VSplit(
+ [
+ self.input_field,
+ ConditionalContainer(
+ input_field_buttons_window,
+ filter=has_focus(self),
+ ),
+ ]
+ ),
],
height=SearchToolbar.TOOLBAR_HEIGHT,
style='class:search-bar',
@@ -215,7 +233,8 @@ class SearchToolbar(ConditionalContainer):
def _toggle_search_follow(self) -> None:
self.log_view.follow_search_match = (
- not self.log_view.follow_search_match)
+ not self.log_view.follow_search_match
+ )
# If automatically jumping to the next search match, disable normal
# follow mode.
if self.log_view.follow_search_match:
@@ -241,14 +260,15 @@ class SearchToolbar(ConditionalContainer):
# Don't apply an empty search.
return False
- if self.log_pane.log_view.new_search(buff.text,
- invert=self._search_invert,
- field=self._search_field):
+ if self.log_pane.log_view.new_search(
+ buff.text, invert=self._search_invert, field=self._search_field
+ ):
self._search_successful = True
# Don't close the search bar, instead focus on the log content.
self.log_pane.application.focus_on_container(
- self.log_pane.log_display_window)
+ self.log_pane.log_display_window
+ )
# Keep existing search text.
return True
@@ -257,12 +277,13 @@ class SearchToolbar(ConditionalContainer):
def get_search_help_fragments(self):
"""Return FormattedText with search general help keybinds."""
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
start_search = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._start_search)
+ mouse_handlers.on_click, self._start_search
+ )
close_search = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self.cancel_search)
+ mouse_handlers.on_click, self.cancel_search
+ )
# Search toolbar is darker than pane toolbars, use the darker button
# style here.
@@ -277,27 +298,33 @@ class SearchToolbar(ConditionalContainer):
fragments.extend(separator_text)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
- 'Enter', 'Search', start_search, base_style=button_style))
+ to_keybind_indicator(
+ 'Enter', 'Search', start_search, base_style=button_style
+ )
+ )
fragments.extend(separator_text)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
- 'Ctrl-c', 'Cancel', close_search, base_style=button_style))
+ to_keybind_indicator(
+ 'Ctrl-c', 'Cancel', close_search, base_style=button_style
+ )
+ )
return fragments
def get_search_settings_fragments(self):
"""Return FormattedText with current search settings and keybinds."""
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_self)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_self)
next_field = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._next_field)
+ mouse_handlers.on_click, self._next_field
+ )
toggle_invert = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._invert_search)
+ mouse_handlers.on_click, self._invert_search
+ )
next_matcher = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self.log_pane.log_view.select_next_search_matcher)
+ mouse_handlers.on_click,
+ self.log_pane.log_view.select_next_search_matcher,
+ )
separator_text = [('', ' ', focus)]
@@ -312,42 +339,51 @@ class SearchToolbar(ConditionalContainer):
fragments.extend(separator_text)
selected_column_text = [
- (button_style + ' class:search-bar-setting',
- (self._search_field.title() if self._search_field else 'All'),
- next_field),
+ (
+ button_style + ' class:search-bar-setting',
+ (self._search_field.title() if self._search_field else 'All'),
+ next_field,
+ ),
]
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
'Ctrl-t',
'Column:',
next_field,
middle_fragments=selected_column_text,
base_style=button_style,
- ))
+ )
+ )
fragments.extend(separator_text)
fragments.extend(
- pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
+ to_checkbox_with_keybind_indicator(
self._search_invert,
'Ctrl-v',
'Invert',
toggle_invert,
- base_style=button_style))
+ base_style=button_style,
+ )
+ )
fragments.extend(separator_text)
# Matching Method
current_matcher_text = [
- (button_style + ' class:search-bar-setting',
- str(self.log_pane.log_view.search_matcher.name), next_matcher)
+ (
+ button_style + ' class:search-bar-setting',
+ str(self.log_pane.log_view.search_matcher.name),
+ next_matcher,
+ )
]
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
'Ctrl-n',
'Matcher:',
next_matcher,
middle_fragments=current_matcher_text,
base_style=button_style,
- ))
+ )
+ )
fragments.extend(separator_text)
return fragments
@@ -359,13 +395,13 @@ class SearchToolbar(ConditionalContainer):
def get_match_count_fragments(self):
"""Return formatted text for the match count indicator."""
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_log_pane)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_log_pane)
two_spaces = ('', ' ', focus)
# Check if this line is a search match
match_number = self.log_view.search_matched_lines.get(
- self.log_view.log_index, -1)
+ self.log_view.log_index, -1
+ )
# If valid, increment the zero indexed value by one for better human
# readability.
@@ -377,77 +413,89 @@ class SearchToolbar(ConditionalContainer):
return [
('class:search-match-count-dialog-title', ' Match ', focus),
- ('', '{} / {}'.format(match_number,
- len(self.log_view.search_matched_lines)),
- focus),
+ (
+ '',
+ '{} / {}'.format(
+ match_number, len(self.log_view.search_matched_lines)
+ ),
+ focus,
+ ),
two_spaces,
]
def get_button_fragments(self) -> StyleAndTextTuples:
"""Return formatted text for the action buttons."""
- focus = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.focus_log_pane)
+ focus = functools.partial(mouse_handlers.on_click, self.focus_log_pane)
one_space = ('', ' ', focus)
two_spaces = ('', ' ', focus)
- cancel = functools.partial(pw_console.widgets.mouse_handlers.on_click,
- self.cancel_search)
+ cancel = functools.partial(mouse_handlers.on_click, self.cancel_search)
create_filter = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._create_filter)
+ mouse_handlers.on_click, self._create_filter
+ )
next_match = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._next_match)
+ mouse_handlers.on_click, self._next_match
+ )
previous_match = functools.partial(
- pw_console.widgets.mouse_handlers.on_click, self._previous_match)
+ mouse_handlers.on_click, self._previous_match
+ )
toggle_search_follow = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self._toggle_search_follow)
+ mouse_handlers.on_click,
+ self._toggle_search_follow,
+ )
button_style = 'class:toolbar-button-inactive'
fragments = []
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='n',
description='Next',
mouse_handler=next_match,
base_style=button_style,
- ))
+ )
+ )
fragments.append(two_spaces)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='N',
description='Previous',
mouse_handler=previous_match,
base_style=button_style,
- ))
+ )
+ )
fragments.append(two_spaces)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='Ctrl-c',
description='Cancel',
mouse_handler=cancel,
base_style=button_style,
- ))
+ )
+ )
fragments.append(two_spaces)
fragments.extend(
- pw_console.widgets.checkbox.to_keybind_indicator(
+ to_keybind_indicator(
key='Ctrl-Alt-f',
description='Add Filter',
mouse_handler=create_filter,
base_style=button_style,
- ))
+ )
+ )
fragments.append(two_spaces)
fragments.extend(
- pw_console.widgets.checkbox.to_checkbox_with_keybind_indicator(
+ to_checkbox_with_keybind_indicator(
checked=self.log_view.follow_search_match,
key='',
description='Jump to new matches',
mouse_handler=toggle_search_follow,
- base_style=button_style))
+ base_style=button_style,
+ )
+ )
fragments.append(one_space)
return fragments
diff --git a/pw_console/py/pw_console/style.py b/pw_console/py/pw_console/style.py
index da252b478..6f90465e8 100644
--- a/pw_console/py/pw_console/style.py
+++ b/pw_console/py/pw_console/style.py
@@ -213,7 +213,7 @@ _THEME_NAME_MAPPING = {
'dark': DarkColors(),
'high-contrast-dark': HighContrastDarkColors(),
'ansi': AnsiTerm(),
-} # yapf: disable
+}
def get_theme_colors(theme_name=''):
@@ -233,20 +233,20 @@ def generate_styles(theme_name='dark'):
'pane_inactive': 'bg:{} {}'.format(theme.dim_bg, theme.dim_fg),
# Use default for active panes.
'pane_active': 'bg:{} {}'.format(theme.default_bg, theme.default_fg),
-
# Brighten active pane toolbars.
'toolbar_active': 'bg:{} {}'.format(theme.active_bg, theme.active_fg),
- 'toolbar_inactive': 'bg:{} {}'.format(theme.inactive_bg,
- theme.inactive_fg),
-
+ 'toolbar_inactive': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.inactive_fg
+ ),
# Dimmer toolbar.
- 'toolbar_dim_active': 'bg:{} {}'.format(theme.active_bg,
- theme.active_fg),
- 'toolbar_dim_inactive': 'bg:{} {}'.format(theme.default_bg,
- theme.inactive_fg),
+ 'toolbar_dim_active': 'bg:{} {}'.format(
+ theme.active_bg, theme.active_fg
+ ),
+ 'toolbar_dim_inactive': 'bg:{} {}'.format(
+ theme.default_bg, theme.inactive_fg
+ ),
# Used for pane titles
'toolbar_accent': theme.cyan_accent,
-
'toolbar-button-decoration': '{}'.format(theme.cyan_accent),
'toolbar-setting-active': 'bg:{} {}'.format(
theme.green_accent,
@@ -254,79 +254,75 @@ def generate_styles(theme_name='dark'):
),
'toolbar-button-active': 'bg:{}'.format(theme.button_active_bg),
'toolbar-button-inactive': 'bg:{}'.format(theme.button_inactive_bg),
-
# prompt_toolkit scrollbar styles:
- 'scrollbar.background': 'bg:{} {}'.format(theme.default_bg,
- theme.default_fg),
+ 'scrollbar.background': 'bg:{} {}'.format(
+ theme.default_bg, theme.default_fg
+ ),
# Scrollbar handle, bg is the bar color.
- 'scrollbar.button': 'bg:{} {}'.format(theme.purple_accent,
- theme.default_bg),
- 'scrollbar.arrow': 'bg:{} {}'.format(theme.default_bg,
- theme.blue_accent),
+ 'scrollbar.button': 'bg:{} {}'.format(
+ theme.purple_accent, theme.default_bg
+ ),
+ 'scrollbar.arrow': 'bg:{} {}'.format(
+ theme.default_bg, theme.blue_accent
+ ),
# Unstyled scrollbar classes:
# 'scrollbar.start'
# 'scrollbar.end'
-
# Top menu bar styles
'menu-bar': 'bg:{} {}'.format(theme.inactive_bg, theme.inactive_fg),
- 'menu-bar.selected-item': 'bg:{} {}'.format(theme.blue_accent,
- theme.inactive_bg),
+ 'menu-bar.selected-item': 'bg:{} {}'.format(
+ theme.blue_accent, theme.inactive_bg
+ ),
# Menu background
'menu': 'bg:{} {}'.format(theme.dialog_bg, theme.dim_fg),
# Menu item separator
'menu-border': theme.magenta_accent,
-
# Top bar logo + keyboard shortcuts
- 'logo': '{} bold'.format(theme.magenta_accent),
+ 'logo': '{} bold'.format(theme.magenta_accent),
'keybind': '{} bold'.format(theme.purple_accent),
'keyhelp': theme.dim_fg,
-
# Help window styles
'help_window_content': 'bg:{} {}'.format(theme.dialog_bg, theme.dim_fg),
'frame.border': 'bg:{} {}'.format(theme.dialog_bg, theme.purple_accent),
-
'pane_indicator_active': 'bg:{}'.format(theme.magenta_accent),
'pane_indicator_inactive': 'bg:{}'.format(theme.inactive_bg),
-
'pane_title_active': '{} bold'.format(theme.magenta_accent),
'pane_title_inactive': '{}'.format(theme.purple_accent),
-
- 'window-tab-active': 'bg:{} {}'.format(theme.active_bg,
- theme.cyan_accent),
- 'window-tab-inactive': 'bg:{} {}'.format(theme.inactive_bg,
- theme.inactive_fg),
-
- 'pane_separator': 'bg:{} {}'.format(theme.default_bg,
- theme.purple_accent),
-
+ 'window-tab-active': 'bg:{} {}'.format(
+ theme.active_bg, theme.cyan_accent
+ ),
+ 'window-tab-inactive': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.inactive_fg
+ ),
+ 'pane_separator': 'bg:{} {}'.format(
+ theme.default_bg, theme.purple_accent
+ ),
# Search matches
'search': 'bg:{} {}'.format(theme.cyan_accent, theme.default_bg),
- 'search.current': 'bg:{} {}'.format(theme.cyan_accent,
- theme.default_bg),
-
+ 'search.current': 'bg:{} {}'.format(
+ theme.cyan_accent, theme.default_bg
+ ),
# Highlighted line styles
'selected-log-line': 'bg:{}'.format(theme.line_highlight_bg),
'marked-log-line': 'bg:{}'.format(theme.selected_line_bg),
'cursor-line': 'bg:{} nounderline'.format(theme.line_highlight_bg),
-
# Messages like 'Window too small'
- 'warning-text': 'bg:{} {}'.format(theme.default_bg,
- theme.yellow_accent),
-
- 'log-time': 'bg:{} {}'.format(theme.default_fg,
- theme.default_bg),
-
+ 'warning-text': 'bg:{} {}'.format(
+ theme.default_bg, theme.yellow_accent
+ ),
+ 'log-time': 'bg:{} {}'.format(theme.default_fg, theme.default_bg),
# Apply foreground only for level and column values. This way the text
# can inherit the background color of the parent window pane or line
# selection.
'log-level-{}'.format(logging.CRITICAL): '{} bold'.format(
- theme.red_accent),
+ theme.red_accent
+ ),
'log-level-{}'.format(logging.ERROR): '{}'.format(theme.red_accent),
'log-level-{}'.format(logging.WARNING): '{}'.format(
- theme.yellow_accent),
+ theme.yellow_accent
+ ),
'log-level-{}'.format(logging.INFO): '{}'.format(theme.purple_accent),
'log-level-{}'.format(logging.DEBUG): '{}'.format(theme.blue_accent),
-
'log-table-column-0': '{}'.format(theme.cyan_accent),
'log-table-column-1': '{}'.format(theme.green_accent),
'log-table-column-2': '{}'.format(theme.yellow_accent),
@@ -335,52 +331,55 @@ def generate_styles(theme_name='dark'):
'log-table-column-5': '{}'.format(theme.blue_accent),
'log-table-column-6': '{}'.format(theme.orange_accent),
'log-table-column-7': '{}'.format(theme.red_accent),
-
'search-bar': 'bg:{}'.format(theme.inactive_bg),
- 'search-bar-title': 'bg:{} {}'.format(theme.cyan_accent,
- theme.default_bg),
+ 'search-bar-title': 'bg:{} {}'.format(
+ theme.cyan_accent, theme.default_bg
+ ),
'search-bar-setting': '{}'.format(theme.cyan_accent),
- 'search-bar-border': 'bg:{} {}'.format(theme.inactive_bg,
- theme.cyan_accent),
+ 'search-bar-border': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.cyan_accent
+ ),
'search-match-count-dialog': 'bg:{}'.format(theme.inactive_bg),
'search-match-count-dialog-title': '{}'.format(theme.cyan_accent),
'search-match-count-dialog-default-fg': '{}'.format(theme.default_fg),
'search-match-count-dialog-border': 'bg:{} {}'.format(
- theme.inactive_bg,
- theme.cyan_accent),
-
+ theme.inactive_bg, theme.cyan_accent
+ ),
'filter-bar': 'bg:{}'.format(theme.inactive_bg),
- 'filter-bar-title': 'bg:{} {}'.format(theme.red_accent,
- theme.default_bg),
+ 'filter-bar-title': 'bg:{} {}'.format(
+ theme.red_accent, theme.default_bg
+ ),
'filter-bar-setting': '{}'.format(theme.cyan_accent),
'filter-bar-delete': '{}'.format(theme.red_accent),
'filter-bar-delimiter': '{}'.format(theme.purple_accent),
-
'saveas-dialog': 'bg:{}'.format(theme.inactive_bg),
- 'saveas-dialog-title': 'bg:{} {}'.format(theme.inactive_bg,
- theme.default_fg),
+ 'saveas-dialog-title': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.default_fg
+ ),
'saveas-dialog-setting': '{}'.format(theme.cyan_accent),
- 'saveas-dialog-border': 'bg:{} {}'.format(theme.inactive_bg,
- theme.cyan_accent),
-
+ 'saveas-dialog-border': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.cyan_accent
+ ),
'selection-dialog': 'bg:{}'.format(theme.inactive_bg),
'selection-dialog-title': '{}'.format(theme.yellow_accent),
'selection-dialog-default-fg': '{}'.format(theme.default_fg),
'selection-dialog-action-bg': 'bg:{}'.format(theme.yellow_accent),
'selection-dialog-action-fg': '{}'.format(theme.button_inactive_bg),
- 'selection-dialog-border': 'bg:{} {}'.format(theme.inactive_bg,
- theme.yellow_accent),
-
+ 'selection-dialog-border': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.yellow_accent
+ ),
'quit-dialog': 'bg:{}'.format(theme.inactive_bg),
- 'quit-dialog-border': 'bg:{} {}'.format(theme.inactive_bg,
- theme.red_accent),
-
+ 'quit-dialog-border': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.red_accent
+ ),
'command-runner': 'bg:{}'.format(theme.inactive_bg),
- 'command-runner-title': 'bg:{} {}'.format(theme.inactive_bg,
- theme.default_fg),
+ 'command-runner-title': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.default_fg
+ ),
'command-runner-setting': '{}'.format(theme.purple_accent),
- 'command-runner-border': 'bg:{} {}'.format(theme.inactive_bg,
- theme.purple_accent),
+ 'command-runner-border': 'bg:{} {}'.format(
+ theme.inactive_bg, theme.purple_accent
+ ),
'command-runner-selected-item': 'bg:{}'.format(theme.selected_line_bg),
'command-runner-fuzzy-highlight-0': '{}'.format(theme.blue_accent),
'command-runner-fuzzy-highlight-1': '{}'.format(theme.cyan_accent),
@@ -388,7 +387,6 @@ def generate_styles(theme_name='dark'):
'command-runner-fuzzy-highlight-3': '{}'.format(theme.yellow_accent),
'command-runner-fuzzy-highlight-4': '{}'.format(theme.orange_accent),
'command-runner-fuzzy-highlight-5': '{}'.format(theme.red_accent),
-
# Progress Bar Styles
# Entire set of ProgressBars - no title is used in pw_console
'title': '',
@@ -407,7 +405,6 @@ def generate_styles(theme_name='dark'):
'total': '{}'.format(theme.cyan_accent),
'time-elapsed': '{}'.format(theme.purple_accent),
'time-left': '{}'.format(theme.magenta_accent),
-
# Named theme color classes for use in user plugins.
'theme-fg-red': '{}'.format(theme.red_accent),
'theme-fg-orange': '{}'.format(theme.orange_accent),
@@ -425,25 +422,19 @@ def generate_styles(theme_name='dark'):
'theme-bg-blue': 'bg:{}'.format(theme.blue_accent),
'theme-bg-purple': 'bg:{}'.format(theme.purple_accent),
'theme-bg-magenta': 'bg:{}'.format(theme.magenta_accent),
-
'theme-bg-active': 'bg:{}'.format(theme.active_bg),
'theme-fg-active': '{}'.format(theme.active_fg),
-
'theme-bg-inactive': 'bg:{}'.format(theme.inactive_bg),
'theme-fg-inactive': '{}'.format(theme.inactive_fg),
-
'theme-fg-default': '{}'.format(theme.default_fg),
'theme-bg-default': 'bg:{}'.format(theme.default_bg),
-
'theme-fg-dim': '{}'.format(theme.dim_fg),
'theme-bg-dim': 'bg:{}'.format(theme.dim_bg),
-
'theme-bg-dialog': 'bg:{}'.format(theme.dialog_bg),
'theme-bg-line-highlight': 'bg:{}'.format(theme.line_highlight_bg),
-
'theme-bg-button-active': 'bg:{}'.format(theme.button_active_bg),
'theme-bg-button-inactive': 'bg:{}'.format(theme.button_inactive_bg),
- } # yapf: disable
+ }
return Style.from_dict(pw_console_styles)
@@ -469,10 +460,9 @@ def get_pane_style(pt_container) -> str:
return 'class:pane_inactive'
-def get_pane_indicator(pt_container,
- title,
- mouse_handler=None,
- hide_indicator=False) -> StyleAndTextTuples:
+def get_pane_indicator(
+ pt_container, title, mouse_handler=None, hide_indicator=False
+) -> StyleAndTextTuples:
"""Return formatted text for a pane indicator and title."""
inactive_indicator: OneStyleAndTextTuple
@@ -481,8 +471,11 @@ def get_pane_indicator(pt_container,
active_title: OneStyleAndTextTuple
if mouse_handler:
- inactive_indicator = ('class:pane_indicator_inactive', ' ',
- mouse_handler)
+ inactive_indicator = (
+ 'class:pane_indicator_inactive',
+ ' ',
+ mouse_handler,
+ )
active_indicator = ('class:pane_indicator_active', ' ', mouse_handler)
inactive_title = ('class:pane_title_inactive', title, mouse_handler)
active_title = ('class:pane_title_active', title, mouse_handler)
diff --git a/pw_console/py/pw_console/templates/__init__.py b/pw_console/py/pw_console/templates/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_console/py/pw_console/templates/__init__.py
diff --git a/pw_console/py/pw_console/templates/repl_output.jinja b/pw_console/py/pw_console/templates/repl_output.jinja
index 4c482c109..976cb8b1d 100644
--- a/pw_console/py/pw_console/templates/repl_output.jinja
+++ b/pw_console/py/pw_console/templates/repl_output.jinja
@@ -30,8 +30,8 @@ Running...
{% endif %}
{% if code.exception_text %}
{{ code.exception_text }}
-{% elif code.result_object %}
-{{ result_format(code.result_object) }}
+{% elif code.result_str %}
+{{ code.result_str }}
{% elif code.output %}
{{ code.output }}
{% endif %}
diff --git a/pw_console/py/pw_console/test_mode.py b/pw_console/py/pw_console/test_mode.py
new file mode 100644
index 000000000..815792c1d
--- /dev/null
+++ b/pw_console/py/pw_console/test_mode.py
@@ -0,0 +1,86 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_console test mode functions."""
+
+import asyncio
+import time
+import re
+import random
+import logging
+from threading import Thread
+from typing import Dict, List, Tuple
+
+FAKE_DEVICE_LOGGER_NAME = 'pw_console_fake_device'
+
+_ROOT_LOG = logging.getLogger('')
+_FAKE_DEVICE_LOG = logging.getLogger(FAKE_DEVICE_LOGGER_NAME)
+
+
+def start_fake_logger(lines, log_thread_entry, log_thread_loop):
+ fake_log_messages = prepare_fake_logs(lines)
+
+ test_log_thread = Thread(target=log_thread_entry, args=(), daemon=True)
+ test_log_thread.start()
+
+ background_log_task = asyncio.run_coroutine_threadsafe(
+ # This function will be executed in a separate thread.
+ log_forever(fake_log_messages),
+ # Using this asyncio event loop.
+ log_thread_loop,
+ ) # type: ignore
+ return background_log_task
+
+
+def prepare_fake_logs(lines) -> List[Tuple[str, Dict]]:
+ fake_logs: List[Tuple[str, Dict]] = []
+ key_regex = re.compile(r':kbd:`(?P<key>[^`]+)`')
+ for line in lines:
+ if not line:
+ continue
+
+ keyboard_key = ''
+ search = key_regex.search(line)
+ if search:
+ keyboard_key = search.group(1)
+
+ fake_logs.append((line, {'keys': keyboard_key}))
+ return fake_logs
+
+
+async def log_forever(fake_log_messages: List[Tuple[str, Dict]]):
+ """Test mode async log generator coroutine that runs forever."""
+ _ROOT_LOG.info('Fake log device connected.')
+ start_time = time.time()
+ message_count = 0
+
+ # Fake module column names.
+ module_names = ['APP', 'RADIO', 'BAT', 'USB', 'CPU']
+ while True:
+ if message_count > 32 or message_count < 2:
+ await asyncio.sleep(0.1)
+ fake_log = random.choice(fake_log_messages)
+
+ module_name = module_names[message_count % len(module_names)]
+ _FAKE_DEVICE_LOG.info(
+ fake_log[0],
+ extra=dict(
+ extra_metadata_fields=dict(
+ module=module_name,
+ file='fake_app.cc',
+ timestamp=time.time() - start_time,
+ **fake_log[1],
+ )
+ ),
+ )
+ message_count += 1
diff --git a/pw_console/py/pw_console/text_formatting.py b/pw_console/py/pw_console/text_formatting.py
index 6fb53f98b..485f13fd4 100644
--- a/pw_console/py/pw_console/text_formatting.py
+++ b/pw_console/py/pw_console/text_formatting.py
@@ -30,7 +30,8 @@ def strip_ansi(text: str):
def split_lines(
- input_fragments: StyleAndTextTuples) -> List[StyleAndTextTuples]:
+ input_fragments: StyleAndTextTuples,
+) -> List[StyleAndTextTuples]:
"""Break a flattened list of StyleAndTextTuples into a list of lines.
Ending line breaks are not preserved."""
@@ -50,9 +51,10 @@ def split_lines(
def insert_linebreaks(
- input_fragments: StyleAndTextTuples,
- max_line_width: int,
- truncate_long_lines: bool = True) -> Tuple[StyleAndTextTuples, int]:
+ input_fragments: StyleAndTextTuples,
+ max_line_width: int,
+ truncate_long_lines: bool = True,
+) -> Tuple[StyleAndTextTuples, int]:
"""Add line breaks at max_line_width if truncate_long_lines is True.
Returns input_fragments with each character as it's own formatted text
@@ -118,7 +120,8 @@ def insert_linebreaks(
def join_adjacent_style_tuples(
- fragments: StyleAndTextTuples) -> StyleAndTextTuples:
+ fragments: StyleAndTextTuples,
+) -> StyleAndTextTuples:
"""Join adjacent FormattedTextTuples if they have the same style."""
new_fragments: StyleAndTextTuples = []
@@ -145,13 +148,15 @@ def join_adjacent_style_tuples(
return new_fragments
-def fill_character_width(input_fragments: StyleAndTextTuples,
- fragment_width: int,
- window_width: int,
- line_wrapping: bool = False,
- remaining_width: int = 0,
- horizontal_scroll_amount: int = 0,
- add_cursor: bool = False) -> StyleAndTextTuples:
+def fill_character_width(
+ input_fragments: StyleAndTextTuples,
+ fragment_width: int,
+ window_width: int,
+ line_wrapping: bool = False,
+ remaining_width: int = 0,
+ horizontal_scroll_amount: int = 0,
+ add_cursor: bool = False,
+) -> StyleAndTextTuples:
"""Fill line to the width of the window using spaces."""
# Calculate the number of spaces to add at the end.
empty_characters = window_width - fragment_width
@@ -195,7 +200,8 @@ def fill_character_width(input_fragments: StyleAndTextTuples,
def flatten_formatted_text_tuples(
- lines: Iterable[StyleAndTextTuples]) -> StyleAndTextTuples:
+ lines: Iterable[StyleAndTextTuples],
+) -> StyleAndTextTuples:
"""Flatten a list of lines of FormattedTextTuples
This function will also remove trailing newlines to avoid displaying extra
diff --git a/pw_console/py/pw_console/widgets/__init__.py b/pw_console/py/pw_console/widgets/__init__.py
index 946e3af5a..133df8201 100644
--- a/pw_console/py/pw_console/widgets/__init__.py
+++ b/pw_console/py/pw_console/widgets/__init__.py
@@ -14,6 +14,7 @@
"""Pigweed Console Reusable UI widgets."""
# pylint: disable=unused-import
+from pw_console.widgets.border import create_border
from pw_console.widgets.checkbox import (
ToolbarButton,
to_checkbox,
@@ -23,5 +24,9 @@ from pw_console.widgets.checkbox import (
to_checkbox_text,
)
from pw_console.widgets.mouse_handlers import on_click
-from pw_console.widgets.window_pane import WindowPane, WindowPaneHSplit
+from pw_console.widgets.window_pane import (
+ FloatingWindowPane,
+ WindowPane,
+ WindowPaneHSplit,
+)
from pw_console.widgets.window_pane_toolbar import WindowPaneToolbar
diff --git a/pw_console/py/pw_console/widgets/border.py b/pw_console/py/pw_console/widgets/border.py
index 0cf1170ef..33096560d 100644
--- a/pw_console/py/pw_console/widgets/border.py
+++ b/pw_console/py/pw_console/widgets/border.py
@@ -28,7 +28,7 @@ def create_border(
# pylint: disable=too-many-arguments
content: AnyContainer,
content_height: Optional[int] = None,
- title: str = '',
+ title: Union[Callable[[], str], str] = '',
border_style: Union[Callable[[], str], str] = '',
base_style: Union[Callable[[], str], str] = '',
top: bool = True,
@@ -49,13 +49,17 @@ def create_border(
top_border_items: List[AnyContainer] = []
if left:
top_border_items.append(
- Window(width=1, height=1, char=top_left_char, style=border_style))
+ Window(width=1, height=1, char=top_left_char, style=border_style)
+ )
title_text = None
if title:
- title_text = FormattedTextControl([
- ('', f'{horizontal_char}{horizontal_char} {title} ')
- ])
+ if isinstance(title, str):
+ title_text = FormattedTextControl(
+ [('', f'{horizontal_char}{horizontal_char} {title} ')]
+ )
+ else:
+ title_text = FormattedTextControl(title)
top_border_items.append(
Window(
@@ -63,49 +67,66 @@ def create_border(
char=horizontal_char,
# Expand width to max available space
dont_extend_width=False,
- style=border_style))
+ style=border_style,
+ )
+ )
if right:
top_border_items.append(
- Window(width=1, height=1, char=top_right_char, style=border_style))
+ Window(width=1, height=1, char=top_right_char, style=border_style)
+ )
content_items: List[AnyContainer] = []
if left:
content_items.append(
- Window(width=1,
- height=content_height,
- char=vertical_char,
- style=border_style))
+ Window(
+ width=1,
+ height=content_height,
+ char=vertical_char,
+ style=border_style,
+ )
+ )
if left_margin_columns > 0:
content_items.append(
- Window(width=left_margin_columns,
- height=content_height,
- char=' ',
- style=border_style))
+ Window(
+ width=left_margin_columns,
+ height=content_height,
+ char=' ',
+ style=border_style,
+ )
+ )
content_items.append(content)
if right_margin_columns > 0:
content_items.append(
- Window(width=right_margin_columns,
- height=content_height,
- char=' ',
- style=border_style))
+ Window(
+ width=right_margin_columns,
+ height=content_height,
+ char=' ',
+ style=border_style,
+ )
+ )
if right:
content_items.append(
- Window(width=1, height=2, char=vertical_char, style=border_style))
+ Window(width=1, height=2, char=vertical_char, style=border_style)
+ )
bottom_border_items: List[AnyContainer] = []
if left:
bottom_border_items.append(
- Window(width=1, height=1, char=bottom_left_char))
+ Window(width=1, height=1, char=bottom_left_char)
+ )
bottom_border_items.append(
Window(
char=horizontal_char,
# Expand width to max available space
- dont_extend_width=False))
+ dont_extend_width=False,
+ )
+ )
if right:
bottom_border_items.append(
- Window(width=1, height=1, char=bottom_right_char))
+ Window(width=1, height=1, char=bottom_right_char)
+ )
rows: List[AnyContainer] = []
if top:
@@ -113,9 +134,7 @@ def create_border(
rows.append(VSplit(content_items, height=content_height))
if bottom:
rows.append(
- VSplit(bottom_border_items,
- height=1,
- padding=0,
- style=border_style))
+ VSplit(bottom_border_items, height=1, padding=0, style=border_style)
+ )
return HSplit(rows, style=base_style)
diff --git a/pw_console/py/pw_console/widgets/checkbox.py b/pw_console/py/pw_console/widgets/checkbox.py
index 91805e543..874ce990f 100644
--- a/pw_console/py/pw_console/widgets/checkbox.py
+++ b/pw_console/py/pw_console/widgets/checkbox.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
# Copyright 2021 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
@@ -79,19 +77,21 @@ def to_checkbox_with_keybind_indicator(
):
"""Create a clickable keybind indicator with checkbox for toolbars."""
if mouse_handler:
- return to_keybind_indicator(key,
- description,
- mouse_handler,
- leading_fragments=[
- to_checkbox(checked, mouse_handler,
- **checkbox_kwargs)
- ],
- base_style=base_style)
+ return to_keybind_indicator(
+ key,
+ description,
+ mouse_handler,
+ leading_fragments=[
+ to_checkbox(checked, mouse_handler, **checkbox_kwargs)
+ ],
+ base_style=base_style,
+ )
return to_keybind_indicator(
key,
description,
leading_fragments=[to_checkbox(checked, **checkbox_kwargs)],
- base_style=base_style)
+ base_style=base_style,
+ )
def to_keybind_indicator(
@@ -114,7 +114,8 @@ def to_keybind_indicator(
def append_fragment_with_base_style(frag_list, fragment) -> None:
if mouse_handler:
frag_list.append(
- (base_style + fragment[0], fragment[1], mouse_handler))
+ (base_style + fragment[0], fragment[1], mouse_handler)
+ )
else:
frag_list.append((base_style + fragment[0], fragment[1]))
@@ -126,7 +127,8 @@ def to_keybind_indicator(
# Function name
if mouse_handler:
fragments.append(
- (base_style + description_style, description, mouse_handler))
+ (base_style + description_style, description, mouse_handler)
+ )
else:
fragments.append((base_style + description_style, description))
@@ -137,8 +139,9 @@ def to_keybind_indicator(
# Separator and keybind
if key:
if mouse_handler:
- fragments.append((base_style + description_style, _KEY_SEPARATOR,
- mouse_handler))
+ fragments.append(
+ (base_style + description_style, _KEY_SEPARATOR, mouse_handler)
+ )
fragments.append((base_style + key_style, key, mouse_handler))
else:
fragments.append((base_style + description_style, _KEY_SEPARATOR))
diff --git a/pw_console/py/pw_console/widgets/event_count_history.py b/pw_console/py/pw_console/widgets/event_count_history.py
index 1779ad0dc..242f615e4 100644
--- a/pw_console/py/pw_console/widgets/event_count_history.py
+++ b/pw_console/py/pw_console/widgets/event_count_history.py
@@ -84,8 +84,8 @@ class EventCountHistory:
def last_count_with_units(self) -> str:
return '{:.3f} [{}]'.format(
- self._last_count * self.display_unit_factor,
- self.display_unit_title)
+ self._last_count * self.display_unit_factor, self.display_unit_title
+ )
def __repr__(self) -> str:
sparkline = ''
@@ -96,9 +96,9 @@ class EventCountHistory:
def __pt_formatted_text__(self):
return [('', self.__repr__())]
- def sparkline(self,
- min_value: int = 0,
- max_value: Optional[int] = None) -> str:
+ def sparkline(
+ self, min_value: int = 0, max_value: Optional[int] = None
+ ) -> str:
msg = ''.rjust(self.history_limit)
if len(self.history) == 0:
return msg
@@ -112,8 +112,10 @@ class EventCountHistory:
msg = ''
for i in self.history:
# (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
- index = int((((1.0 * i) - minimum) / max_minus_min) *
- len(self.scale_characters))
+ index = int(
+ (((1.0 * i) - minimum) / max_minus_min)
+ * len(self.scale_characters)
+ )
if index >= len(self.scale_characters):
index = len(self.scale_characters) - 1
msg += self.scale_characters[index]
diff --git a/pw_console/py/pw_console/widgets/mouse_handlers.py b/pw_console/py/pw_console/widgets/mouse_handlers.py
index af3c58644..d9d04e980 100644
--- a/pw_console/py/pw_console/widgets/mouse_handlers.py
+++ b/pw_console/py/pw_console/widgets/mouse_handlers.py
@@ -53,6 +53,7 @@ def on_click(on_click_function: Callable, mouse_event: MouseEvent):
class EmptyMouseHandler(MouseHandlers):
"""MouseHandler that does not propagate events."""
+
def set_mouse_handler_for_range(
self,
x_min: int,
diff --git a/pw_console/py/pw_console/widgets/table.py b/pw_console/py/pw_console/widgets/table.py
index ea3154392..e486fd2b9 100644
--- a/pw_console/py/pw_console/widgets/table.py
+++ b/pw_console/py/pw_console/widgets/table.py
@@ -20,7 +20,7 @@ from prompt_toolkit.formatted_text import StyleAndTextTuples
from pw_console.console_prefs import ConsolePrefs
from pw_console.log_line import LogLine
-import pw_console.text_formatting
+from pw_console.text_formatting import strip_ansi
class TableView:
@@ -52,18 +52,19 @@ class TableView:
self.column_padding = ' ' * self.prefs.spaces_between_columns
def all_column_names(self):
- columns_names = [
- name for name, _width in self._ordered_column_widths()
- ]
+ columns_names = [name for name, _width in self._ordered_column_widths()]
return columns_names + ['message']
def _width_of_justified_fields(self):
"""Calculate the width of all columns except LAST_TABLE_COLUMN_NAMES."""
padding_width = len(self.column_padding)
- used_width = sum([
- width + padding_width for key, width in self.column_widths.items()
- if key not in TableView.LAST_TABLE_COLUMN_NAMES
- ])
+ used_width = sum(
+ [
+ width + padding_width
+ for key, width in self.column_widths.items()
+ if key not in TableView.LAST_TABLE_COLUMN_NAMES
+ ]
+ )
return used_width
def _ordered_column_widths(self):
@@ -94,8 +95,14 @@ class TableView:
return ordered_columns.items()
- def update_metadata_column_widths(self, log: LogLine):
+ def update_metadata_column_widths(self, log: LogLine) -> None:
"""Calculate the max widths for each metadata field."""
+ if log.metadata is None:
+ log.update_metadata()
+ # If extra fields still don't exist, no need to update column widths.
+ if log.metadata is None:
+ return
+
for field_name, value in log.metadata.fields.items():
value_string = str(value)
@@ -110,8 +117,7 @@ class TableView:
self.column_widths[field_name] = len(value_string)
# Update log level character width.
- ansi_stripped_level = pw_console.text_formatting.strip_ansi(
- log.record.levelname)
+ ansi_stripped_level = strip_ansi(log.record.levelname)
if len(ansi_stripped_level) > self.column_widths['level']:
self.column_widths['level'] = len(ansi_stripped_level)
@@ -125,15 +131,15 @@ class TableView:
# Update time column width to current prefs setting
self.column_widths['time'] = self._default_time_width
if self.prefs.hide_date_from_log_time:
- self.column_widths['time'] = (self._default_time_width -
- self._year_month_day_width)
+ self.column_widths['time'] = (
+ self._default_time_width - self._year_month_day_width
+ )
for name, width in self._ordered_column_widths():
# These fields will be shown at the end
- if name in ['msg', 'message']:
+ if name in TableView.LAST_TABLE_COLUMN_NAMES:
continue
- fragments.append(
- (default_style, name.title()[:width].ljust(width)))
+ fragments.append((default_style, name.title()[:width].ljust(width)))
fragments.append(('', self.column_padding))
fragments.append((default_style, 'Message'))
@@ -163,7 +169,7 @@ class TableView:
columns = {}
for name, width in self._ordered_column_widths():
# Skip these modifying these fields
- if name in ['msg', 'message']:
+ if name in TableView.LAST_TABLE_COLUMN_NAMES:
continue
# hasattr checks are performed here since a log record may not have
@@ -172,25 +178,28 @@ class TableView:
if name == 'time' and hasattr(log.record, 'asctime'):
time_text = log.record.asctime
if self.prefs.hide_date_from_log_time:
- time_text = time_text[self._year_month_day_width:]
- time_style = self.prefs.column_style('time',
- time_text,
- default='class:log-time')
- columns['time'] = (time_style,
- time_text.ljust(self.column_widths['time']))
+ time_text = time_text[self._year_month_day_width :]
+ time_style = self.prefs.column_style(
+ 'time', time_text, default='class:log-time'
+ )
+ columns['time'] = (
+ time_style,
+ time_text.ljust(self.column_widths['time']),
+ )
continue
if name == 'level' and hasattr(log.record, 'levelname'):
# Remove any existing ANSI formatting and apply our colors.
- level_text = pw_console.text_formatting.strip_ansi(
- log.record.levelname)
+ level_text = strip_ansi(log.record.levelname)
level_style = self.prefs.column_style(
'level',
level_text,
- default='class:log-level-{}'.format(log.record.levelno))
- columns['level'] = (level_style,
- level_text.ljust(
- self.column_widths['level']))
+ default='class:log-level-{}'.format(log.record.levelno),
+ )
+ columns['level'] = (
+ level_style,
+ level_text.ljust(self.column_widths['level']),
+ )
continue
value = log.metadata.fields.get(name, ' ')
@@ -211,8 +220,7 @@ class TableView:
# Grab the message to appear after the justified columns with ANSI
# escape sequences removed.
- message_text = pw_console.text_formatting.strip_ansi(
- log.record.message)
+ message_text = strip_ansi(log.record.message)
message = log.metadata.fields.get(
'msg',
message_text.rstrip(), # Remove any trailing line breaks
@@ -239,12 +247,15 @@ class TableView:
# For raw strings that don't have their own ANSI colors, apply the
# theme color style for this column.
if isinstance(column_value, str):
- fallback_style = 'class:log-table-column-{}'.format(
- i + index_modifier) if 0 <= i <= 7 else default_style
-
- style = self.prefs.column_style(column_name,
- column_value.rstrip(),
- default=fallback_style)
+ fallback_style = (
+ 'class:log-table-column-{}'.format(i + index_modifier)
+ if 0 <= i <= 7
+ else default_style
+ )
+
+ style = self.prefs.column_style(
+ column_name, column_value.rstrip(), default=fallback_style
+ )
table_fragments.append((style, column_value))
table_fragments.append(padding_formatted_text)
diff --git a/pw_console/py/pw_console/widgets/window_pane.py b/pw_console/py/pw_console/widgets/window_pane.py
index ab2484a74..1c9bd7284 100644
--- a/pw_console/py/pw_console/widgets/window_pane.py
+++ b/pw_console/py/pw_console/widgets/window_pane.py
@@ -14,7 +14,7 @@
"""Window pane base class."""
from abc import ABC
-from typing import Any, Optional, TYPE_CHECKING, Union
+from typing import Any, Callable, List, Optional, Tuple, TYPE_CHECKING, Union
import functools
from prompt_toolkit.layout.dimension import AnyDimension
@@ -27,12 +27,10 @@ from prompt_toolkit.layout import (
HSplit,
walk,
)
+from prompt_toolkit.widgets import MenuItem
from pw_console.get_pw_console_app import get_pw_console_app
-
-import pw_console.widgets.checkbox
-import pw_console.widgets.mouse_handlers
-import pw_console.style
+from pw_console.style import get_pane_style
if TYPE_CHECKING:
from pw_console.console_app import ConsoleApp
@@ -44,6 +42,7 @@ class WindowPaneHSplit(HSplit):
This overrides the write_to_screen function to save the width and height of
the container to be rendered.
"""
+
def __init__(self, parent_window_pane, *args, **kwargs):
# Save a reference to the parent window pane.
self.parent_window_pane = parent_window_pane
@@ -60,11 +59,18 @@ class WindowPaneHSplit(HSplit):
) -> None:
# Save the width and height for the current render pass. This will be
# used by the log pane to render the correct amount of log lines.
- self.parent_window_pane.update_pane_size(write_position.width,
- write_position.height)
+ self.parent_window_pane.update_pane_size(
+ write_position.width, write_position.height
+ )
# Continue writing content to the screen.
- super().write_to_screen(screen, mouse_handlers, write_position,
- parent_style, erase_bg, z_index)
+ super().write_to_screen(
+ screen,
+ mouse_handlers,
+ write_position,
+ parent_style,
+ erase_bg,
+ z_index,
+ )
class WindowPane(ABC):
@@ -86,6 +92,8 @@ class WindowPane(ABC):
self._pane_title = pane_title
self._pane_subtitle: str = ''
+ self.extra_tab_style: Optional[str] = None
+
# Default width and height to 10 lines each. They will be resized by the
# WindowManager later.
self.height = height if height else Dimension(preferred=10)
@@ -145,7 +153,7 @@ class WindowPane(ABC):
object."""
return self.container # pylint: disable=no-member
- def get_all_key_bindings(self) -> list:
+ def get_all_key_bindings(self) -> List:
"""Return keybinds for display in the help window.
For example:
@@ -167,7 +175,9 @@ class WindowPane(ABC):
# pylint: disable=no-self-use
return []
- def get_all_menu_options(self) -> list:
+ def get_window_menu_options(
+ self,
+ ) -> List[Tuple[str, Union[Callable, None]]]:
"""Return menu options for the window pane.
Should return a list of tuples containing with the display text and
@@ -176,10 +186,17 @@ class WindowPane(ABC):
# pylint: disable=no-self-use
return []
+ def get_top_level_menus(self) -> List[MenuItem]:
+ """Return MenuItems to be displayed on the main pw_console menu bar."""
+ # pylint: disable=no-self-use
+ return []
+
def pane_resized(self) -> bool:
"""Return True if the current window size has changed."""
- return (self.last_pane_width != self.current_pane_width
- or self.last_pane_height != self.current_pane_height)
+ return (
+ self.last_pane_width != self.current_pane_width
+ or self.last_pane_height != self.current_pane_height
+ )
def update_pane_size(self, width, height) -> None:
"""Save pane width and height for the current UI render pass."""
@@ -198,9 +215,10 @@ class WindowPane(ABC):
# Window pane dimensions
height=lambda: self.height,
width=lambda: self.width,
- style=functools.partial(pw_console.style.get_pane_style, self),
+ style=functools.partial(get_pane_style, self),
),
- filter=Condition(lambda: self.show_pane))
+ filter=Condition(lambda: self.show_pane),
+ )
def has_child_container(self, child_container: AnyContainer) -> bool:
if not child_container:
@@ -209,3 +227,45 @@ class WindowPane(ABC):
if container == child_container:
return True
return False
+
+
+class FloatingWindowPane(WindowPane):
+ """The Pigweed Console FloatingWindowPane class."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ # Tracks the last focused container, to enable restoring focus after
+ # closing the dialog.
+ self.last_focused_pane = None
+
+ def close_dialog(self) -> None:
+ """Close runner dialog box."""
+ self.show_pane = False
+
+ # Restore original focus if possible.
+ if self.last_focused_pane:
+ self.application.focus_on_container(self.last_focused_pane)
+ else:
+ # Fallback to focusing on the main menu.
+ self.application.focus_main_menu()
+
+ self.application.update_menu_items()
+
+ def open_dialog(self) -> None:
+ self.show_pane = True
+ self.last_focused_pane = self.application.focused_window()
+ self.focus_self()
+ self.application.redraw_ui()
+
+ self.application.update_menu_items()
+
+ def toggle_dialog(self) -> bool:
+ if self.show_pane:
+ self.close_dialog()
+ else:
+ self.open_dialog()
+ # The focused window has changed. Return true so
+ # ConsoleApp.run_pane_menu_option does not set the focus to the main
+ # menu.
+ return True
diff --git a/pw_console/py/pw_console/widgets/window_pane_toolbar.py b/pw_console/py/pw_console/widgets/window_pane_toolbar.py
index 3c16beb62..18ec4cd9a 100644
--- a/pw_console/py/pw_console/widgets/window_pane_toolbar.py
+++ b/pw_console/py/pw_console/widgets/window_pane_toolbar.py
@@ -28,19 +28,24 @@ from prompt_toolkit.layout import (
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
from pw_console.get_pw_console_app import get_pw_console_app
-import pw_console.style
+from pw_console.style import (
+ get_pane_indicator,
+ get_button_style,
+ get_toolbar_style,
+)
from pw_console.widgets import (
ToolbarButton,
+ mouse_handlers,
to_checkbox_with_keybind_indicator,
to_keybind_indicator,
)
-import pw_console.widgets.mouse_handlers
_LOG = logging.getLogger(__package__)
class WindowPaneResizeHandle(FormattedTextControl):
"""Button to initiate window pane resize drag events."""
+
def __init__(self, parent_window_pane: Any, *args, **kwargs) -> None:
self.parent_window_pane = parent_window_pane
super().__init__(*args, **kwargs)
@@ -50,7 +55,8 @@ class WindowPaneResizeHandle(FormattedTextControl):
# Start resize mouse drag event
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
get_pw_console_app().window_manager.start_resize_pane(
- self.parent_window_pane)
+ self.parent_window_pane
+ )
# Mouse event handled, return None.
return None
@@ -60,6 +66,7 @@ class WindowPaneResizeHandle(FormattedTextControl):
class WindowPaneToolbar:
"""One line toolbar for display at the bottom of of a window pane."""
+
# pylint: disable=too-many-instance-attributes
TOOLBAR_HEIGHT = 1
@@ -71,20 +78,18 @@ class WindowPaneToolbar:
# No title was set, fetch the parent window pane title if available.
parent_pane_title = self.parent_window_pane.pane_title()
title = parent_pane_title if parent_pane_title else title
- return pw_console.style.get_pane_indicator(self.focus_check_container,
- f' {title} ',
- self.focus_mouse_handler)
+ return get_pane_indicator(
+ self.focus_check_container, f' {title} ', self.focus_mouse_handler
+ )
def get_center_text_tokens(self):
"""Return formatted text tokens for display in the center part of the
toolbar."""
- button_style = pw_console.style.get_button_style(
- self.focus_check_container)
+ button_style = get_button_style(self.focus_check_container)
# FormattedTextTuple contents: (Style, Text, Mouse handler)
- separator_text = [('', ' ')
- ] # 2 spaces of separaton between keybinds.
+ separator_text = [('', ' ')] # 2 spaces of separaton between keybinds.
if self.focus_mouse_handler:
separator_text = [('', ' ', self.focus_mouse_handler)]
@@ -95,8 +100,9 @@ class WindowPaneToolbar:
on_click_handler = None
if button.mouse_handler:
on_click_handler = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- button.mouse_handler)
+ mouse_handlers.on_click,
+ button.mouse_handler,
+ )
if button.is_checkbox:
fragments.extend(
@@ -105,13 +111,18 @@ class WindowPaneToolbar:
button.key,
button.description,
on_click_handler,
- base_style=button_style))
+ base_style=button_style,
+ )
+ )
else:
fragments.extend(
- to_keybind_indicator(button.key,
- button.description,
- on_click_handler,
- base_style=button_style))
+ to_keybind_indicator(
+ button.key,
+ button.description,
+ on_click_handler,
+ base_style=button_style,
+ )
+ )
fragments.extend(separator_text)
@@ -124,22 +135,40 @@ class WindowPaneToolbar:
"""Return formatted text tokens for display."""
fragments = []
if not has_focus(self.focus_check_container.__pt_container__())():
- fragments.append((
- 'class:toolbar-button-inactive class:toolbar-button-decoration',
- ' ', self.focus_mouse_handler))
- fragments.append(('class:toolbar-button-inactive class:keyhelp',
- 'click to focus', self.focus_mouse_handler))
- fragments.append((
- 'class:toolbar-button-inactive class:toolbar-button-decoration',
- ' ', self.focus_mouse_handler))
- fragments.append(
- ('', ' {} '.format(self.subtitle()), self.focus_mouse_handler))
+ if self.click_to_focus_text:
+ fragments.append(
+ (
+ 'class:toolbar-button-inactive '
+ 'class:toolbar-button-decoration',
+ ' ',
+ self.focus_mouse_handler,
+ )
+ )
+ fragments.append(
+ (
+ 'class:toolbar-button-inactive class:keyhelp',
+ self.click_to_focus_text,
+ self.focus_mouse_handler,
+ )
+ )
+ fragments.append(
+ (
+ 'class:toolbar-button-inactive '
+ 'class:toolbar-button-decoration',
+ ' ',
+ self.focus_mouse_handler,
+ )
+ )
+ if self.subtitle:
+ fragments.append(
+ ('', ' {} '.format(self.subtitle()), self.focus_mouse_handler)
+ )
return fragments
def get_resize_handle(self):
- return pw_console.style.get_pane_indicator(self.focus_check_container,
- '─══─',
- hide_indicator=True)
+ return get_pane_indicator(
+ self.focus_check_container, '─══─', hide_indicator=True
+ )
def add_button(self, button: ToolbarButton):
self.buttons.append(button)
@@ -153,11 +182,12 @@ class WindowPaneToolbar:
focus_action_callable: Optional[Callable] = None,
center_section_align: WindowAlign = WindowAlign.LEFT,
include_resize_handle: bool = True,
+ click_to_focus_text: str = 'click to focus',
):
-
self.parent_window_pane = parent_window_pane
self.title = title
self.subtitle = subtitle
+ self.click_to_focus_text = click_to_focus_text
# Assume check this container for focus
self.focus_check_container = self
@@ -186,8 +216,9 @@ class WindowPaneToolbar:
self.focus_mouse_handler = None
if self.focus_action_callable:
self.focus_mouse_handler = functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- self.focus_action_callable)
+ mouse_handlers.on_click,
+ self.focus_action_callable,
+ )
self.buttons: List[ToolbarButton] = []
self.show_toolbar = True
@@ -211,8 +242,9 @@ class WindowPaneToolbar:
dont_extend_width=True,
)
- get_toolbar_style = functools.partial(
- pw_console.style.get_toolbar_style, self.focus_check_container)
+ wrapped_get_toolbar_style = functools.partial(
+ get_toolbar_style, self.focus_check_container
+ )
sections = [
self.left_section_window,
@@ -234,14 +266,15 @@ class WindowPaneToolbar:
self.toolbar_vsplit = VSplit(
sections,
height=WindowPaneToolbar.TOOLBAR_HEIGHT,
- style=get_toolbar_style,
+ style=wrapped_get_toolbar_style,
)
self.container = self._create_toolbar_container(self.toolbar_vsplit)
def _create_toolbar_container(self, content):
return ConditionalContainer(
- content, filter=Condition(lambda: self.show_toolbar))
+ content, filter=Condition(lambda: self.show_toolbar)
+ )
def __pt_container__(self):
"""Return the prompt_toolkit root container for this log pane.
diff --git a/pw_console/py/pw_console/window_list.py b/pw_console/py/pw_console/window_list.py
index ec21b5810..2f543eb50 100644
--- a/pw_console/py/pw_console/window_list.py
+++ b/pw_console/py/pw_console/window_list.py
@@ -31,8 +31,7 @@ from prompt_toolkit.layout import (
)
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType, MouseButton
-import pw_console.style
-import pw_console.widgets.mouse_handlers
+from pw_console.widgets import mouse_handlers as pw_console_mouse_handlers
if TYPE_CHECKING:
# pylint: disable=ungrouped-imports
@@ -43,6 +42,7 @@ _LOG = logging.getLogger(__package__)
class DisplayMode(Enum):
"""WindowList display modes."""
+
STACK = 'Stacked'
TABBED = 'Tabbed'
@@ -60,6 +60,7 @@ class WindowListHSplit(HSplit):
of the container for the current render pass. It also handles overriding
mouse handlers for triggering window resize adjustments.
"""
+
def __init__(self, parent_window_list, *args, **kwargs):
# Save a reference to the parent window pane.
self.parent_window_list = parent_window_list
@@ -78,8 +79,7 @@ class WindowListHSplit(HSplit):
# Is resize mode active?
if self.parent_window_list.resize_mode:
# Ignore future mouse_handler updates.
- new_mouse_handlers = (
- pw_console.widgets.mouse_handlers.EmptyMouseHandler())
+ new_mouse_handlers = pw_console_mouse_handlers.EmptyMouseHandler()
# Set existing mouse_handlers to the parent_window_list's
# mouse_handler. This will handle triggering resize events.
mouse_handlers.set_mouse_handler_for_range(
@@ -87,15 +87,25 @@ class WindowListHSplit(HSplit):
write_position.xpos + write_position.width,
write_position.ypos,
write_position.ypos + write_position.height,
- self.parent_window_list.mouse_handler)
+ self.parent_window_list.mouse_handler,
+ )
# Save the width, height, and draw position for the current render pass.
self.parent_window_list.update_window_list_size(
- write_position.width, write_position.height, write_position.xpos,
- write_position.ypos)
+ write_position.width,
+ write_position.height,
+ write_position.xpos,
+ write_position.ypos,
+ )
# Continue writing content to the screen.
- super().write_to_screen(screen, new_mouse_handlers, write_position,
- parent_style, erase_bg, z_index)
+ super().write_to_screen(
+ screen,
+ new_mouse_handlers,
+ write_position,
+ parent_style,
+ erase_bg,
+ z_index,
+ )
class WindowList:
@@ -207,14 +217,16 @@ class WindowList:
command_runner_focused_pane = None
if self.application.command_runner_is_open():
command_runner_focused_pane = (
- self.application.command_runner_last_focused_pane())
+ self.application.command_runner_last_focused_pane()
+ )
for index, pane in enumerate(self.active_panes):
in_focus = False
if has_focus(pane)():
in_focus = True
elif command_runner_focused_pane and pane.has_child_container(
- command_runner_focused_pane):
+ command_runner_focused_pane
+ ):
in_focus = True
if in_focus:
@@ -224,6 +236,7 @@ class WindowList:
return focused_pane
def get_pane_titles(self, omit_subtitles=False, use_menu_title=True):
+ """Return formatted text for the window pane tab bar."""
fragments = []
separator = ('', ' ')
fragments.append(separator)
@@ -234,24 +247,39 @@ class WindowList:
if omit_subtitles:
text = f' {title} '
- fragments.append((
- # Style
- ('class:window-tab-active' if pane_index
- == self.focused_pane_index else 'class:window-tab-inactive'),
- # Text
- text,
- # Mouse handler
- functools.partial(
- pw_console.widgets.mouse_handlers.on_click,
- functools.partial(self.switch_to_tab, pane_index),
- ),
- ))
+ tab_style = (
+ 'class:window-tab-active'
+ if pane_index == self.focused_pane_index
+ else 'class:window-tab-inactive'
+ )
+ if pane.extra_tab_style:
+ tab_style += ' ' + pane.extra_tab_style
+
+ fragments.append(
+ (
+ # Style
+ tab_style,
+ # Text
+ text,
+ # Mouse handler
+ functools.partial(
+ pw_console_mouse_handlers.on_click,
+ functools.partial(self.switch_to_tab, pane_index),
+ ),
+ )
+ )
fragments.append(separator)
return fragments
def switch_to_tab(self, index: int):
self.focused_pane_index = index
+ # Make the selected tab visible and hide the rest.
+ for i, pane in enumerate(self.active_panes):
+ pane.show_pane = False
+ if i == index:
+ pane.show_pane = True
+
# refresh_ui() will focus on the new tab container.
self.refresh_ui()
@@ -259,8 +287,15 @@ class WindowList:
self.display_mode = mode
if self.display_mode == DisplayMode.TABBED:
+ # Default to focusing on the first window / tab.
self.focused_pane_index = 0
- # Un-hide all panes, they must be visible to switch between tabs.
+ # Hide all other panes so log redraw events are not triggered.
+ for pane in self.active_panes:
+ pane.show_pane = False
+ # Keep the selected tab visible
+ self.active_panes[self.focused_pane_index].show_pane = True
+ else:
+ # Un-hide all panes if switching from tabbed back to stacked.
for pane in self.active_panes:
pane.show_pane = True
@@ -274,7 +309,8 @@ class WindowList:
if self.display_mode == DisplayMode.TABBED:
self.application.focus_on_container(
- self.active_panes[self.focused_pane_index])
+ self.active_panes[self.focused_pane_index]
+ )
self.application.redraw_ui()
@@ -300,8 +336,9 @@ class WindowList:
self._set_window_heights(new_heights)
- def update_window_list_size(self, width, height, xposition,
- yposition) -> None:
+ def update_window_list_size(
+ self, width, height, xposition, yposition
+ ) -> None:
"""Save width and height of the repl pane for the current UI render
pass."""
if width:
@@ -311,24 +348,25 @@ class WindowList:
self.last_window_list_height = self.current_window_list_height
self.current_window_list_height = height
if xposition:
- self.last_window_list_xposition = (
- self.current_window_list_xposition)
+ self.last_window_list_xposition = self.current_window_list_xposition
self.current_window_list_xposition = xposition
if yposition:
- self.last_window_list_yposition = (
- self.current_window_list_yposition)
+ self.last_window_list_yposition = self.current_window_list_yposition
self.current_window_list_yposition = yposition
- if (self.current_window_list_width != self.last_window_list_width
- or self.current_window_list_height !=
- self.last_window_list_height):
+ if (
+ self.current_window_list_width != self.last_window_list_width
+ or self.current_window_list_height != self.last_window_list_height
+ ):
self.rebalance_window_heights()
def mouse_handler(self, mouse_event: MouseEvent):
mouse_position = mouse_event.position
- if (mouse_event.event_type == MouseEventType.MOUSE_MOVE
- and mouse_event.button == MouseButton.LEFT):
+ if (
+ mouse_event.event_type == MouseEventType.MOUSE_MOVE
+ and mouse_event.button == MouseButton.LEFT
+ ):
self.mouse_resize(mouse_position.x, mouse_position.y)
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
self.stop_resize()
@@ -366,16 +404,21 @@ class WindowList:
def _create_window_tab_toolbar(self):
tab_bar_control = FormattedTextControl(
- functools.partial(self.get_pane_titles,
- omit_subtitles=True,
- use_menu_title=False))
- tab_bar_window = Window(content=tab_bar_control,
- align=WindowAlign.LEFT,
- dont_extend_width=True)
+ functools.partial(
+ self.get_pane_titles, omit_subtitles=True, use_menu_title=False
+ )
+ )
+ tab_bar_window = Window(
+ content=tab_bar_control,
+ align=WindowAlign.LEFT,
+ dont_extend_width=True,
+ )
- spacer = Window(content=FormattedTextControl([('', '')]),
- align=WindowAlign.LEFT,
- dont_extend_width=False)
+ spacer = Window(
+ content=FormattedTextControl([('', '')]),
+ align=WindowAlign.LEFT,
+ dont_extend_width=False,
+ )
tab_toolbar = VSplit(
[
@@ -440,7 +483,8 @@ class WindowList:
existing_pane_index -= 1
try:
self.application.focus_on_container(
- self.active_panes[existing_pane_index])
+ self.active_panes[existing_pane_index]
+ )
except ValueError:
# ValueError will be raised if the the pane at
# existing_pane_index can't be accessed.
@@ -481,15 +525,13 @@ class WindowList:
self._update_resize_current_row()
self.application.redraw_ui()
- def adjust_pane_size(self,
- pane,
- diff: int = _WINDOW_HEIGHT_ADJUST) -> None:
+ def adjust_pane_size(self, pane, diff: int = _WINDOW_HEIGHT_ADJUST) -> None:
"""Increase or decrease a given pane's height."""
# Placeholder next_pane value to allow setting width and height without
# any consequences if there is no next visible pane.
- next_pane = HSplit([],
- height=Dimension(preferred=10),
- width=Dimension(preferred=10)) # type: ignore
+ next_pane = HSplit(
+ [], height=Dimension(preferred=10), width=Dimension(preferred=10)
+ ) # type: ignore
# Try to get the next visible pane to subtract a weight value from.
next_visible_pane = self._get_next_visible_pane_after(pane)
if next_visible_pane:
@@ -498,8 +540,9 @@ class WindowList:
# If the last pane is selected, and there are at least 2 panes, make
# next_pane the previous pane.
try:
- if len(self.active_panes) >= 2 and (self.active_panes.index(pane)
- == len(self.active_panes) - 1):
+ if len(self.active_panes) >= 2 and (
+ self.active_panes.index(pane) == len(self.active_panes) - 1
+ ):
next_pane = self.active_panes[-2]
except ValueError:
# Ignore ValueError raised if self.active_panes[-2] doesn't exist.
@@ -535,8 +578,9 @@ class WindowList:
old_values = [
p.height.preferred for p in self.active_panes if p.show_pane
]
- new_heights = [int(available_height / len(old_values))
- ] * len(old_values)
+ new_heights = [int(available_height / len(old_values))] * len(
+ old_values
+ )
self._set_window_heights(new_heights)
@@ -584,11 +628,3 @@ class WindowList:
if next_pane.show_pane:
return next_pane
return None
-
- def focus_next_visible_pane(self, pane):
- """Focus on the next visible window pane if possible."""
- next_visible_pane = self._get_next_visible_pane_after(pane)
- if next_visible_pane:
- self.application.layout.focus(next_visible_pane)
- return
- self.application.focus_main_menu()
diff --git a/pw_console/py/pw_console/window_manager.py b/pw_console/py/pw_console/window_manager.py
index 044751bf8..817e1d7ff 100644
--- a/pw_console/py/pw_console/window_manager.py
+++ b/pw_console/py/pw_console/window_manager.py
@@ -35,9 +35,11 @@ from prompt_toolkit.widgets import MenuItem
from pw_console.console_prefs import ConsolePrefs, error_unknown_window
from pw_console.log_pane import LogPane
-import pw_console.widgets.checkbox
-from pw_console.widgets import WindowPaneToolbar
-import pw_console.widgets.mouse_handlers
+from pw_console.widgets import (
+ WindowPaneToolbar,
+ to_checkbox_text,
+)
+from pw_console.widgets import mouse_handlers as pw_console_mouse_handlers
from pw_console.window_list import WindowList, DisplayMode
_LOG = logging.getLogger(__package__)
@@ -48,8 +50,10 @@ _WINDOW_SPLIT_ADJUST = 1
class WindowListResizeHandle(FormattedTextControl):
"""Button to initiate window list resize drag events."""
- def __init__(self, window_manager, window_list: Any, *args,
- **kwargs) -> None:
+
+ def __init__(
+ self, window_manager, window_list: Any, *args, **kwargs
+ ) -> None:
self.window_manager = window_manager
self.window_list = window_list
super().__init__(*args, **kwargs)
@@ -73,6 +77,7 @@ class WindowManagerVSplit(VSplit):
of the container for the current render pass. It also handles overriding
mouse handlers for triggering window resize adjustments.
"""
+
def __init__(self, parent_window_manager, *args, **kwargs):
# Save a reference to the parent window pane.
self.parent_window_manager = parent_window_manager
@@ -91,8 +96,7 @@ class WindowManagerVSplit(VSplit):
# Is resize mode active?
if self.parent_window_manager.resize_mode:
# Ignore future mouse_handler updates.
- new_mouse_handlers = (
- pw_console.widgets.mouse_handlers.EmptyMouseHandler())
+ new_mouse_handlers = pw_console_mouse_handlers.EmptyMouseHandler()
# Set existing mouse_handlers to the parent_window_managers's
# mouse_handler. This will handle triggering resize events.
mouse_handlers.set_mouse_handler_for_range(
@@ -100,14 +104,22 @@ class WindowManagerVSplit(VSplit):
write_position.xpos + write_position.width,
write_position.ypos,
write_position.ypos + write_position.height,
- self.parent_window_manager.mouse_handler)
+ self.parent_window_manager.mouse_handler,
+ )
# Save the width and height for the current render pass.
self.parent_window_manager.update_window_manager_size(
- write_position.width, write_position.height)
+ write_position.width, write_position.height
+ )
# Continue writing content to the screen.
- super().write_to_screen(screen, new_mouse_handlers, write_position,
- parent_style, erase_bg, z_index)
+ super().write_to_screen(
+ screen,
+ new_mouse_handlers,
+ write_position,
+ parent_style,
+ erase_bg,
+ z_index,
+ )
class WindowManagerHSplit(HSplit):
@@ -117,6 +129,7 @@ class WindowManagerHSplit(HSplit):
of the container for the current render pass. It also handles overriding
mouse handlers for triggering window resize adjustments.
"""
+
def __init__(self, parent_window_manager, *args, **kwargs):
# Save a reference to the parent window pane.
self.parent_window_manager = parent_window_manager
@@ -135,8 +148,7 @@ class WindowManagerHSplit(HSplit):
# Is resize mode active?
if self.parent_window_manager.resize_mode:
# Ignore future mouse_handler updates.
- new_mouse_handlers = (
- pw_console.widgets.mouse_handlers.EmptyMouseHandler())
+ new_mouse_handlers = pw_console_mouse_handlers.EmptyMouseHandler()
# Set existing mouse_handlers to the parent_window_managers's
# mouse_handler. This will handle triggering resize events.
mouse_handlers.set_mouse_handler_for_range(
@@ -144,14 +156,22 @@ class WindowManagerHSplit(HSplit):
write_position.xpos + write_position.width,
write_position.ypos,
write_position.ypos + write_position.height,
- self.parent_window_manager.mouse_handler)
+ self.parent_window_manager.mouse_handler,
+ )
# Save the width and height for the current render pass.
self.parent_window_manager.update_window_manager_size(
- write_position.width, write_position.height)
+ write_position.width, write_position.height
+ )
# Continue writing content to the screen.
- super().write_to_screen(screen, new_mouse_handlers, write_position,
- parent_style, erase_bg, z_index)
+ super().write_to_screen(
+ screen,
+ new_mouse_handlers,
+ write_position,
+ parent_style,
+ erase_bg,
+ z_index,
+ )
class WindowManager:
@@ -193,13 +213,16 @@ class WindowManager:
self.last_window_manager_height = self.current_window_manager_height
self.current_window_manager_height = height
- if (self.current_window_manager_width != self.last_window_manager_width
- or self.current_window_manager_height !=
- self.last_window_manager_height):
+ if (
+ self.current_window_manager_width != self.last_window_manager_width
+ or self.current_window_manager_height
+ != self.last_window_manager_height
+ ):
self.rebalance_window_list_sizes()
- def _set_window_list_sizes(self, new_heights: List[int],
- new_widths: List[int]) -> None:
+ def _set_window_list_sizes(
+ self, new_heights: List[int], new_widths: List[int]
+ ) -> None:
for window_list in self.window_lists:
window_list.height = Dimension(preferred=new_heights[0])
new_heights = new_heights[1:]
@@ -221,9 +244,7 @@ class WindowManager:
old_height_total = max(sum(old_heights), 1)
old_width_total = max(sum(old_widths), 1)
- height_percentages = [
- value / old_height_total for value in old_heights
- ]
+ height_percentages = [value / old_height_total for value in old_heights]
width_percentages = [value / old_width_total for value in old_widths]
new_heights = [
@@ -240,9 +261,7 @@ class WindowManager:
self.current_window_manager_height for h in new_heights
]
else:
- new_widths = [
- self.current_window_manager_width for h in new_widths
- ]
+ new_widths = [self.current_window_manager_width for h in new_widths]
self._set_window_list_sizes(new_heights, new_widths)
@@ -309,7 +328,8 @@ class WindowManager:
def delete_empty_window_lists(self):
empty_lists = [
- window_list for window_list in self.window_lists
+ window_list
+ for window_list in self.window_lists
if window_list.empty()
]
for empty_list in empty_lists:
@@ -348,7 +368,8 @@ class WindowManager:
separator_padding,
Window(
content=WindowListResizeHandle(
- self, window_list, "║\n║\n║"),
+ self, window_list, "║\n║\n║"
+ ),
char='│',
width=1,
dont_extend_height=True,
@@ -382,7 +403,8 @@ class WindowManager:
def update_root_container_body(self):
# Replace the root MenuContainer body with the new split.
self.application.root_container.container.content.children[
- 1] = self.create_root_container()
+ 1
+ ] = self.create_root_container()
def _get_active_window_list_and_pane(self):
active_pane = None
@@ -405,8 +427,10 @@ class WindowManager:
return index
def run_action_on_active_pane(self, function_name):
- _active_window_list, active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ _active_window_list,
+ active_pane,
+ ) = self._get_active_window_list_and_pane()
if not hasattr(active_pane, function_name):
return
method_to_call = getattr(active_pane, function_name)
@@ -419,8 +443,10 @@ class WindowManager:
def focus_next_pane(self, reverse_order=False) -> None:
"""Focus on the next visible window pane or tab."""
- active_window_list, active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ active_pane,
+ ) = self._get_active_window_list_and_pane()
if active_window_list is None:
return
@@ -442,15 +468,17 @@ class WindowManager:
# Action: Switch to the first pane of the next window list.
if next_pane_index >= pane_count or next_pane_index < 0:
# Get the next window_list
- next_window_list_index = ((active_window_list_index + increment) %
- window_list_count)
+ next_window_list_index = (
+ active_window_list_index + increment
+ ) % window_list_count
next_window_list = self.window_lists[next_window_list_index]
# If tabbed window mode is enabled, switch to the first tab.
if next_window_list.display_mode == DisplayMode.TABBED:
if reverse_order:
next_window_list.switch_to_tab(
- len(next_window_list.active_panes) - 1)
+ len(next_window_list.active_panes) - 1
+ )
else:
next_window_list.switch_to_tab(0)
return
@@ -484,8 +512,10 @@ class WindowManager:
return
def move_pane_left(self):
- active_window_list, active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ active_pane,
+ ) = self._get_active_window_list_and_pane()
if not active_window_list:
return
@@ -512,8 +542,10 @@ class WindowManager:
self.delete_empty_window_lists()
def move_pane_right(self):
- active_window_list, active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ active_pane,
+ ) = self._get_active_window_list_and_pane()
if not active_window_list:
return
@@ -538,32 +570,40 @@ class WindowManager:
self.delete_empty_window_lists()
def move_pane_up(self):
- active_window_list, _active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ _active_pane,
+ ) = self._get_active_window_list_and_pane()
if not active_window_list:
return
active_window_list.move_pane_up()
def move_pane_down(self):
- active_window_list, _active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ _active_pane,
+ ) = self._get_active_window_list_and_pane()
if not active_window_list:
return
active_window_list.move_pane_down()
def shrink_pane(self):
- active_window_list, _active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ _active_pane,
+ ) = self._get_active_window_list_and_pane()
if not active_window_list:
return
active_window_list.shrink_pane()
def enlarge_pane(self):
- active_window_list, _active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ _active_pane,
+ ) = self._get_active_window_list_and_pane()
if not active_window_list:
return
@@ -573,16 +613,20 @@ class WindowManager:
if len(self.window_lists) < 2:
return
- active_window_list, _active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ _active_pane,
+ ) = self._get_active_window_list_and_pane()
if not active_window_list:
return
self.adjust_split_size(active_window_list, -_WINDOW_SPLIT_ADJUST)
def enlarge_split(self):
- active_window_list, _active_pane = (
- self._get_active_window_list_and_pane())
+ (
+ active_window_list,
+ _active_pane,
+ ) = self._get_active_window_list_and_pane()
if not active_window_list:
return
@@ -599,20 +643,23 @@ class WindowManager:
available_width = self.current_window_manager_width
old_heights = [w.height.preferred for w in self.window_lists]
old_widths = [w.width.preferred for w in self.window_lists]
- new_heights = [int(available_height / len(old_heights))
- ] * len(old_heights)
+ new_heights = [int(available_height / len(old_heights))] * len(
+ old_heights
+ )
new_widths = [int(available_width / len(old_widths))] * len(old_widths)
self._set_window_list_sizes(new_heights, new_widths)
def _get_next_window_list_for_resizing(
- self, window_list: WindowList) -> Optional[WindowList]:
+ self, window_list: WindowList
+ ) -> Optional[WindowList]:
window_list_index = self.window_list_index(window_list)
if window_list_index is None:
return None
- next_window_list_index = ((window_list_index + 1) %
- len(self.window_lists))
+ next_window_list_index = (window_list_index + 1) % len(
+ self.window_lists
+ )
# Use the previous window if we are on the last split
if window_list_index == len(self.window_lists) - 1:
@@ -621,9 +668,9 @@ class WindowManager:
next_window_list = self.window_lists[next_window_list_index]
return next_window_list
- def adjust_split_size(self,
- window_list: WindowList,
- diff: int = _WINDOW_SPLIT_ADJUST) -> None:
+ def adjust_split_size(
+ self, window_list: WindowList, diff: int = _WINDOW_SPLIT_ADJUST
+ ) -> None:
"""Increase or decrease a given window_list's vertical split width."""
# No need to resize if only one split.
if len(self.window_lists) < 2:
@@ -670,8 +717,7 @@ class WindowManager:
def toggle_pane(self, pane):
"""Toggle a pane on or off."""
- window_list, _pane_index = (
- self._find_window_list_and_pane_index(pane))
+ window_list, _pane_index = self.find_window_list_and_pane_index(pane)
# Don't hide the window if tabbed mode is enabled. Switching to a
# separate tab is preffered.
@@ -695,8 +741,9 @@ class WindowManager:
def check_for_all_hidden_panes_and_unhide(self) -> None:
"""Scan for window_lists containing only hidden panes."""
for window_list in self.window_lists:
- all_hidden = all(not pane.show_pane
- for pane in window_list.active_panes)
+ all_hidden = all(
+ not pane.show_pane for pane in window_list.active_panes
+ )
if all_hidden:
# Unhide the first pane
self.toggle_pane(window_list.active_panes[0])
@@ -713,17 +760,19 @@ class WindowManager:
def active_panes(self):
"""Return all active panes from all window lists."""
return chain.from_iterable(
- map(operator.attrgetter('active_panes'), self.window_lists))
+ map(operator.attrgetter('active_panes'), self.window_lists)
+ )
def start_resize_pane(self, pane):
- window_list, pane_index = self._find_window_list_and_pane_index(pane)
+ window_list, pane_index = self.find_window_list_and_pane_index(pane)
window_list.start_resize(pane, pane_index)
def mouse_resize(self, xpos, ypos):
if self.resize_target_window_list_index is None:
return
target_window_list = self.window_lists[
- self.resize_target_window_list_index]
+ self.resize_target_window_list_index
+ ]
diff = ypos - self.resize_current_row
if self.vertical_window_list_spliting():
@@ -739,8 +788,10 @@ class WindowManager:
"""MouseHandler used when resize_mode == True."""
mouse_position = mouse_event.position
- if (mouse_event.event_type == MouseEventType.MOUSE_MOVE
- and mouse_event.button == MouseButton.LEFT):
+ if (
+ mouse_event.event_type == MouseEventType.MOUSE_MOVE
+ and mouse_event.button == MouseButton.LEFT
+ ):
self.mouse_resize(mouse_position.x, mouse_position.y)
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
self.stop_resize()
@@ -829,7 +880,7 @@ class WindowManager:
self.resize_current_row = 0
self.resize_current_column = 0
- def _find_window_list_and_pane_index(self, pane: Any):
+ def find_window_list_and_pane_index(self, pane: Any):
pane_index = None
parent_window_list = None
for window_list in self.window_lists:
@@ -840,8 +891,9 @@ class WindowManager:
return parent_window_list, pane_index
def remove_pane(self, existing_pane: Any):
- window_list, _pane_index = (
- self._find_window_list_and_pane_index(existing_pane))
+ window_list, _pane_index = self.find_window_list_and_pane_index(
+ existing_pane
+ )
if window_list:
window_list.remove_pane(existing_pane)
# Reset focus if this list is empty
@@ -853,7 +905,8 @@ class WindowManager:
window_list.reset_pane_sizes()
def _remove_panes_from_layout(
- self, pane_titles: Iterable[str]) -> Dict[str, Any]:
+ self, pane_titles: Iterable[str]
+ ) -> Dict[str, Any]:
# Gather pane objects and remove them from the window layout.
collected_panes = {}
@@ -862,11 +915,14 @@ class WindowManager:
# iterating.
for pane in copy.copy(window_list.active_panes):
if pane.pane_title() in pane_titles:
- collected_panes[pane.pane_title()] = (
- window_list.remove_pane_no_checks(pane))
+ collected_panes[
+ pane.pane_title()
+ ] = window_list.remove_pane_no_checks(pane)
return collected_panes
- def _set_pane_options(self, pane, options: dict) -> None: # pylint: disable=no-self-use
+ def _set_pane_options( # pylint: disable=no-self-use
+ self, pane, options: dict
+ ) -> None:
if options.get('hidden', False):
# Hide this pane
pane.show_pane = False
@@ -884,17 +940,19 @@ class WindowManager:
mode = DisplayMode.TABBED
self.window_lists[column_index].set_display_mode(mode)
- def _create_new_log_pane_with_loggers(self, window_title, window_options,
- existing_pane_titles) -> LogPane:
+ def _create_new_log_pane_with_loggers(
+ self, window_title, window_options, existing_pane_titles
+ ) -> LogPane:
if 'loggers' not in window_options:
error_unknown_window(window_title, existing_pane_titles)
- new_pane = LogPane(application=self.application,
- pane_title=window_title)
+ new_pane = LogPane(
+ application=self.application, pane_title=window_title
+ )
# Add logger handlers
- for logger_name, logger_options in window_options.get('loggers',
- {}).items():
-
+ for logger_name, logger_options in window_options.get(
+ 'loggers', {}
+ ).items():
log_level_name = logger_options.get('level', None)
new_pane.add_log_handler(logger_name, level_name=log_level_name)
return new_pane
@@ -908,14 +966,17 @@ class WindowManager:
unique_titles = prefs.unique_window_titles
collected_panes = self._remove_panes_from_layout(unique_titles)
existing_pane_titles = [
- p.pane_title() for p in collected_panes.values()
+ p.pane_title()
+ for p in collected_panes.values()
if isinstance(p, LogPane)
]
# Keep track of original non-duplicated pane titles
already_added_panes = []
- for column_index, column in enumerate(prefs.windows.items()): # pylint: disable=too-many-nested-blocks
+ for column_index, column in enumerate(
+ prefs.windows.items()
+ ): # pylint: disable=too-many-nested-blocks
_column_type, windows = column
# Add a new window_list if needed
if column_index >= len(self.window_lists):
@@ -934,11 +995,14 @@ class WindowManager:
# Check if this pane is brand new, ready to be added, or should
# be duplicated.
- if (window_title not in already_added_panes
- and window_title not in collected_panes):
+ if (
+ window_title not in already_added_panes
+ and window_title not in collected_panes
+ ):
# New pane entirely
new_pane = self._create_new_log_pane_with_loggers(
- window_title, window_options, existing_pane_titles)
+ window_title, window_options, existing_pane_titles
+ )
elif window_title not in already_added_panes:
# First time adding this pane
@@ -956,11 +1020,13 @@ class WindowManager:
# Set window size and visibility
self._set_pane_options(new_pane, window_options)
# Add the new pane
- self.window_lists[column_index].add_pane_no_checks(
- new_pane)
- # Apply log filters
+ self.window_lists[column_index].add_pane_no_checks(new_pane)
+ # Apply log pane options
if isinstance(new_pane, LogPane):
new_pane.apply_filters_from_config(window_options)
+ # Auto-start the websocket log server if requested.
+ if window_options.get('view_in_web', False):
+ new_pane.toggle_websocket_server()
# Update column display modes.
self._set_window_list_display_modes(prefs)
@@ -974,7 +1040,7 @@ class WindowManager:
# Focus on the first visible pane.
self.focus_first_visible_pane()
- def create_window_menu(self):
+ def create_window_menu_items(self) -> List[MenuItem]:
"""Build the [Window] menu for the current set of window lists."""
root_menu_items = []
for window_list_index, window_list in enumerate(self.window_lists):
@@ -982,21 +1048,25 @@ class WindowManager:
menu_items.append(
MenuItem(
'Column {index} View Modes'.format(
- index=window_list_index + 1),
+ index=window_list_index + 1
+ ),
children=[
MenuItem(
'{check} {display_mode} Windows'.format(
display_mode=display_mode.value,
- check=pw_console.widgets.checkbox.
- to_checkbox_text(
+ check=to_checkbox_text(
window_list.display_mode == display_mode,
end='',
- )),
+ ),
+ ),
handler=functools.partial(
- window_list.set_display_mode, display_mode),
- ) for display_mode in DisplayMode
+ window_list.set_display_mode, display_mode
+ ),
+ )
+ for display_mode in DisplayMode
],
- ))
+ )
+ )
menu_items.extend(
MenuItem(
'{index}: {title}'.format(
@@ -1006,25 +1076,25 @@ class WindowManager:
children=[
MenuItem(
'{check} Show/Hide Window'.format(
- check=pw_console.widgets.checkbox.
- to_checkbox_text(pane.show_pane, end='')),
+ check=to_checkbox_text(pane.show_pane, end='')
+ ),
handler=functools.partial(self.toggle_pane, pane),
),
- ] + [
- MenuItem(text,
- handler=functools.partial(
- self.application.run_pane_menu_option,
- handler))
- for text, handler in pane.get_all_menu_options()
+ ]
+ + [
+ MenuItem(
+ text,
+ handler=functools.partial(
+ self.application.run_pane_menu_option, handler
+ ),
+ )
+ for text, handler in pane.get_window_menu_options()
],
- ) for pane_index, pane in enumerate(window_list.active_panes))
+ )
+ for pane_index, pane in enumerate(window_list.active_panes)
+ )
if window_list_index + 1 < len(self.window_lists):
menu_items.append(MenuItem('-'))
root_menu_items.extend(menu_items)
- menu = MenuItem(
- '[Windows]',
- children=root_menu_items,
- )
-
- return [menu]
+ return root_menu_items
diff --git a/pw_console/py/repl_pane_test.py b/pw_console/py/repl_pane_test.py
index 86f41cea7..db325b6ce 100644
--- a/pw_console/py/repl_pane_test.py
+++ b/pw_console/py/repl_pane_test.py
@@ -44,6 +44,7 @@ if _PYTHON_3_8:
class TestReplPane(IsolatedAsyncioTestCase):
"""Tests for ReplPane."""
+
def setUp(self): # pylint: disable=invalid-name
self.maxDiff = None # pylint: disable=invalid-name
@@ -62,7 +63,8 @@ if _PYTHON_3_8:
pw_ptpython_repl = PwPtPythonRepl(
get_globals=lambda: global_vars,
get_locals=lambda: global_vars,
- color_depth=ColorDepth.DEPTH_8_BIT)
+ color_depth=ColorDepth.DEPTH_8_BIT,
+ )
repl_pane = ReplPane(
application=app,
python_repl=pw_ptpython_repl,
@@ -71,53 +73,60 @@ if _PYTHON_3_8:
self.assertEqual(repl_pane, pw_ptpython_repl.repl_pane)
# Define a function, should return nothing.
- code = inspect.cleandoc("""
+ code = inspect.cleandoc(
+ """
def run():
print('The answer is ', end='')
return 1+1+4+16+20
- """)
+ """
+ )
temp_stdout = io.StringIO()
temp_stderr = io.StringIO()
# pylint: disable=protected-access
result = asyncio.run(
- pw_ptpython_repl._run_user_code(code, temp_stdout,
- temp_stderr))
- self.assertEqual(result, {
- 'stdout': '',
- 'stderr': '',
- 'result': None
- })
+ pw_ptpython_repl._run_user_code(code, temp_stdout, temp_stderr)
+ )
+ self.assertEqual(
+ result, {'stdout': '', 'stderr': '', 'result': None}
+ )
temp_stdout = io.StringIO()
temp_stderr = io.StringIO()
# Check stdout and return value
result = asyncio.run(
- pw_ptpython_repl._run_user_code('run()', temp_stdout,
- temp_stderr))
- self.assertEqual(result, {
- 'stdout': 'The answer is ',
- 'stderr': '',
- 'result': 42
- })
+ pw_ptpython_repl._run_user_code(
+ 'run()', temp_stdout, temp_stderr
+ )
+ )
+ self.assertEqual(
+ result, {'stdout': 'The answer is ', 'stderr': '', 'result': 42}
+ )
temp_stdout = io.StringIO()
temp_stderr = io.StringIO()
# Check for repl exception
result = asyncio.run(
- pw_ptpython_repl._run_user_code('return "blah"', temp_stdout,
- temp_stderr))
- self.assertIn("SyntaxError: 'return' outside function",
- pw_ptpython_repl._last_exception) # type: ignore
+ pw_ptpython_repl._run_user_code(
+ 'return "blah"', temp_stdout, temp_stderr
+ )
+ )
+ self.assertIn(
+ "SyntaxError: 'return' outside function",
+ pw_ptpython_repl._last_exception, # type: ignore
+ )
async def test_user_thread(self) -> None:
"""Test user code thread."""
with create_app_session(output=FakeOutput()):
# Setup Mocks
- app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT,
- prefs=ConsolePrefs(project_file=False,
- project_user_file=False,
- user_file=False))
+ prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
+ prefs.set_code_theme('default')
+ app = ConsoleApp(
+ color_depth=ColorDepth.DEPTH_8_BIT, prefs=prefs
+ )
app.start_user_code_thread()
@@ -125,18 +134,23 @@ if _PYTHON_3_8:
repl_pane = app.repl_pane
# Mock update_output_buffer to track number of update calls
- repl_pane.update_output_buffer = MagicMock(
- wraps=repl_pane.update_output_buffer)
+ repl_pane.update_output_buffer = MagicMock( # type: ignore
+ wraps=repl_pane.update_output_buffer
+ )
# Mock complete callback
- pw_ptpython_repl.user_code_complete_callback = MagicMock(
- wraps=pw_ptpython_repl.user_code_complete_callback)
+ pw_ptpython_repl.user_code_complete_callback = ( # type: ignore
+ MagicMock(
+ wraps=pw_ptpython_repl.user_code_complete_callback
+ )
+ )
# Repl done flag for tests
user_code_done = threading.Event()
# Run some code
- code = inspect.cleandoc("""
+ code = inspect.cleandoc(
+ """
import time
def run():
for i in range(2):
@@ -144,20 +158,24 @@ if _PYTHON_3_8:
print(i)
print('The answer is ', end='')
return 1+1+4+16+20
- """)
+ """
+ )
input_buffer = MagicMock(text=code)
- pw_ptpython_repl._accept_handler(input_buffer) # pylint: disable=protected-access
+ # pylint: disable=protected-access
+ pw_ptpython_repl._accept_handler(input_buffer)
+ # pylint: enable=protected-access
# Get last executed code object.
user_code1 = repl_pane.executed_code[-1]
# Wait for repl code to finish.
user_code1.future.add_done_callback(
- lambda future: user_code_done.set())
+ lambda future: user_code_done.set()
+ )
# Wait for stdout monitoring to complete.
if user_code1.stdout_check_task:
await user_code1.stdout_check_task
# Wait for test done callback.
- user_code_done.wait(timeout=3)
+ user_code_done.wait()
# Check user_code1 results
# NOTE: Avoid using assert_has_calls. Thread timing can make the
@@ -171,11 +189,14 @@ if _PYTHON_3_8:
call('pw_ptpython_repl.user_code_complete_callback'),
]
for expected_call in expected_calls:
- self.assertIn(expected_call,
- repl_pane.update_output_buffer.mock_calls)
+ self.assertIn(
+ expected_call, repl_pane.update_output_buffer.mock_calls
+ )
- pw_ptpython_repl.user_code_complete_callback.assert_called_once(
+ user_code_complete_callback = (
+ pw_ptpython_repl.user_code_complete_callback
)
+ user_code_complete_callback.assert_called_once()
self.assertIsNotNone(user_code1)
self.assertTrue(user_code1.future.done())
@@ -192,18 +213,21 @@ if _PYTHON_3_8:
# Run some code
input_buffer = MagicMock(text='run()')
- pw_ptpython_repl._accept_handler(input_buffer) # pylint: disable=protected-access
+ # pylint: disable=protected-access
+ pw_ptpython_repl._accept_handler(input_buffer)
+ # pylint: enable=protected-access
# Get last executed code object.
user_code2 = repl_pane.executed_code[-1]
# Wait for repl code to finish.
user_code2.future.add_done_callback(
- lambda future: user_code_done.set())
+ lambda future: user_code_done.set()
+ )
# Wait for stdout monitoring to complete.
if user_code2.stdout_check_task:
await user_code2.stdout_check_task
# Wait for test done callback.
- user_code_done.wait(timeout=3)
+ user_code_done.wait()
# Check user_code2 results
# NOTE: Avoid using assert_has_calls. Thread timing can make the
@@ -226,11 +250,13 @@ if _PYTHON_3_8:
call('repl_pane.periodic_check'),
]
for expected_call in expected_calls:
- self.assertIn(expected_call,
- repl_pane.update_output_buffer.mock_calls)
+ self.assertIn(
+ expected_call, repl_pane.update_output_buffer.mock_calls
+ )
- pw_ptpython_repl.user_code_complete_callback.assert_called_once(
- )
+ # pylint: disable=line-too-long
+ pw_ptpython_repl.user_code_complete_callback.assert_called_once()
+ # pylint: enable=line-too-long
self.assertIsNotNone(user_code2)
self.assertTrue(user_code2.future.done())
self.assertEqual(user_code2.input, 'run()')
diff --git a/pw_console/py/setup.cfg b/pw_console/py/setup.cfg
index 8d3ce30ab..60021a431 100644
--- a/pw_console/py/setup.cfg
+++ b/pw_console/py/setup.cfg
@@ -24,27 +24,26 @@ zip_safe = False
install_requires =
ipython
jinja2
- prompt_toolkit>=3.0.26
+ prompt-toolkit>=3.0.26
ptpython>=3.0.20
- pw_cli
- pw_tokenizer
pygments
- pygments-style-dracula
- pygments-style-tomorrow
pyperclip
+ pyserial>=3.5,<4.0
pyyaml
types-pygments
- types-PyYAML
+ types-pyserial>=3.5,<4.0
+ types-pyyaml
+ websockets
[options.entry_points]
console_scripts = pw-console = pw_console.__main__:main
-pygments.styles =
- pigweed-code = pw_console.pigweed_code_style:PigweedCodeStyle
- pigweed-code-light = pw_console.pigweed_code_style:PigweedCodeLightStyle
[options.package_data]
pw_console =
docs/user_guide.rst
+ html/index.html
+ html/main.js
+ html/style.css
py.typed
templates/keybind_list.jinja
templates/repl_output.jinja
diff --git a/pw_console/py/table_test.py b/pw_console/py/table_test.py
index d8896c926..6714c230f 100644
--- a/pw_console/py/table_test.py
+++ b/pw_console/py/table_test.py
@@ -39,72 +39,83 @@ formatter = logging.Formatter(
'%(levelname)s'
'\x1b[0m'
' '
- '%(message)s', _TIMESTAMP_FORMAT)
+ '%(message)s',
+ _TIMESTAMP_FORMAT,
+)
def make_log(**kwargs):
"""Create a LogLine instance."""
# Construct a LogRecord
- attributes = dict(name='testlogger',
- levelno=logging.INFO,
- levelname='INF',
- msg='[%s] %.3f %s',
- args=('MOD1', 3.14159, 'Real message here'),
- created=_TIMESTAMP_SAMPLE.timestamp(),
- filename='test.py',
- lineno=42,
- pathname='/home/user/test.py')
+ attributes = dict(
+ name='testlogger',
+ levelno=logging.INFO,
+ levelname='INF',
+ msg='[%s] %.3f %s',
+ args=('MOD1', 3.14159, 'Real message here'),
+ created=_TIMESTAMP_SAMPLE.timestamp(),
+ filename='test.py',
+ lineno=42,
+ pathname='/home/user/test.py',
+ )
# Override any above attrs that are passed in.
attributes.update(kwargs)
# Create the record
record = logging.makeLogRecord(dict(attributes))
# Format
formatted_message = formatter.format(record)
- log_line = LogLine(record=record,
- formatted_log=formatted_message,
- ansi_stripped_log='')
+ log_line = LogLine(
+ record=record, formatted_log=formatted_message, ansi_stripped_log=''
+ )
log_line.update_metadata()
return log_line
class TestTableView(unittest.TestCase):
"""Tests for rendering log lines into tables."""
+
def setUp(self):
# Show large diffs
self.maxDiff = None # pylint: disable=invalid-name
- self.prefs = ConsolePrefs(project_file=False,
- project_user_file=False,
- user_file=False)
+ self.prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
self.prefs.reset_config()
- @parameterized.expand([
- (
- 'Correct column widths with all fields set',
- [
- make_log(
- args=('M1', 1.2345, 'Something happened'),
- extra_metadata_fields=dict(module='M1', anumber=12)),
-
- make_log(
- args=('MD2', 567.5, 'Another cool event'),
- extra_metadata_fields=dict(module='MD2', anumber=123)),
- ],
- dict(module=len('MD2'), anumber=len('123')),
- ),
- (
- 'Missing metadata fields on some rows',
- [
- make_log(
- args=('M1', 54321.2, 'Something happened'),
- extra_metadata_fields=dict(module='M1', anumber=54321.2)),
-
- make_log(
- args=('MOD2', 567.5, 'Another cool event'),
- extra_metadata_fields=dict(module='MOD2')),
- ],
- dict(module=len('MOD2'), anumber=len('54321.200')),
- ),
- ]) # yapf: disable
+ @parameterized.expand(
+ [
+ (
+ 'Correct column widths with all fields set',
+ [
+ make_log(
+ args=('M1', 1.2345, 'Something happened'),
+ extra_metadata_fields=dict(module='M1', anumber=12),
+ ),
+ make_log(
+ args=('MD2', 567.5, 'Another cool event'),
+ extra_metadata_fields=dict(module='MD2', anumber=123),
+ ),
+ ],
+ dict(module=len('MD2'), anumber=len('123')),
+ ),
+ (
+ 'Missing metadata fields on some rows',
+ [
+ make_log(
+ args=('M1', 54321.2, 'Something happened'),
+ extra_metadata_fields=dict(
+ module='M1', anumber=54321.2
+ ),
+ ),
+ make_log(
+ args=('MOD2', 567.5, 'Another cool event'),
+ extra_metadata_fields=dict(module='MOD2'),
+ ),
+ ],
+ dict(module=len('MOD2'), anumber=len('54321.200')),
+ ),
+ ]
+ )
def test_column_widths(self, _name, logs, expected_widths) -> None:
"""Test colum widths calculation."""
table = TableView(self.prefs)
@@ -125,44 +136,53 @@ class TestTableView(unittest.TestCase):
}
self.assertCountEqual(expected_widths, results)
- @parameterized.expand([
- (
- 'Build header adding fields incrementally',
- [
- make_log(
- args=('MODULE2', 567.5, 'Another cool event'),
- extra_metadata_fields=dict(
- # timestamp missing
- module='MODULE2')),
-
- make_log(
- args=('MODULE1', 54321.2, 'Something happened'),
- extra_metadata_fields=dict(
+ @parameterized.expand(
+ [
+ (
+ 'Build header adding fields incrementally',
+ [
+ make_log(
+ args=('MODULE2', 567.5, 'Another cool event'),
+ extra_metadata_fields=dict(
+ # timestamp missing
+ module='MODULE2'
+ ),
+ ),
+ make_log(
+ args=('MODULE1', 54321.2, 'Something happened'),
+ extra_metadata_fields=dict(
+ # timestamp added in
+ module='MODULE1',
+ timestamp=54321.2,
+ ),
+ ),
+ ],
+ [
+ [
+ ('bold', 'Time '),
+ _TABLE_PADDING_FRAGMENT,
+ ('bold', 'Lev'),
+ _TABLE_PADDING_FRAGMENT,
+ ('bold', 'Module '),
+ _TABLE_PADDING_FRAGMENT,
+ ('bold', 'Message'),
+ ],
+ [
+ ('bold', 'Time '),
+ _TABLE_PADDING_FRAGMENT,
+ ('bold', 'Lev'),
+ _TABLE_PADDING_FRAGMENT,
+ ('bold', 'Module '),
+ _TABLE_PADDING_FRAGMENT,
# timestamp added in
- module='MODULE1', timestamp=54321.2)),
- ],
- [
- [('bold', 'Time '),
- _TABLE_PADDING_FRAGMENT,
- ('bold', 'Lev'),
- _TABLE_PADDING_FRAGMENT,
- ('bold', 'Module '),
- _TABLE_PADDING_FRAGMENT,
- ('bold', 'Message')],
-
- [('bold', 'Time '),
- _TABLE_PADDING_FRAGMENT,
- ('bold', 'Lev'),
- _TABLE_PADDING_FRAGMENT,
- ('bold', 'Module '),
- _TABLE_PADDING_FRAGMENT,
- # timestamp added in
- ('bold', 'Timestamp'),
- _TABLE_PADDING_FRAGMENT,
- ('bold', 'Message')],
- ],
- ),
- ]) # yapf: disable
+ ('bold', 'Timestamp'),
+ _TABLE_PADDING_FRAGMENT,
+ ('bold', 'Message'),
+ ],
+ ],
+ ),
+ ]
+ )
def test_formatted_header(self, _name, logs, expected_headers) -> None:
"""Test colum widths calculation."""
table = TableView(self.prefs)
@@ -171,68 +191,73 @@ class TestTableView(unittest.TestCase):
table.update_metadata_column_widths(log)
self.assertEqual(table.formatted_header(), header)
- @parameterized.expand([
- (
- 'Build rows adding fields incrementally',
- [
- make_log(
- args=('MODULE2', 567.5, 'Another cool event'),
- extra_metadata_fields=dict(
- # timestamp missing
- module='MODULE2',
- msg='Another cool event')),
-
- make_log(
- args=('MODULE2', 567.5, 'Another cool event'),
- extra_metadata_fields=dict(
- # timestamp and msg missing
- module='MODULE2')),
-
- make_log(
- args=('MODULE1', 54321.2, 'Something happened'),
- extra_metadata_fields=dict(
- # timestamp added in
- module='MODULE1', timestamp=54321.2,
- msg='Something happened')),
- ],
- [
+ @parameterized.expand(
+ [
+ (
+ 'Build rows adding fields incrementally',
[
- ('class:log-time', _TIMESTAMP_SAMPLE_STRING),
- _TABLE_PADDING_FRAGMENT,
- ('class:log-level-20', 'INF'),
- _TABLE_PADDING_FRAGMENT,
- ('class:log-table-column-0', 'MODULE2'),
- _TABLE_PADDING_FRAGMENT,
- ('', 'Another cool event'),
- ('', '\n')
+ make_log(
+ args=('MODULE2', 567.5, 'Another cool event'),
+ extra_metadata_fields=dict(
+ # timestamp missing
+ module='MODULE2',
+ msg='Another cool event',
+ ),
+ ),
+ make_log(
+ args=('MODULE2', 567.5, 'Another cool event'),
+ extra_metadata_fields=dict(
+ # timestamp and msg missing
+ module='MODULE2'
+ ),
+ ),
+ make_log(
+ args=('MODULE1', 54321.2, 'Something happened'),
+ extra_metadata_fields=dict(
+ # timestamp added in
+ module='MODULE1',
+ timestamp=54321.2,
+ msg='Something happened',
+ ),
+ ),
],
-
- [
- ('class:log-time', _TIMESTAMP_SAMPLE_STRING),
- _TABLE_PADDING_FRAGMENT,
- ('class:log-level-20', 'INF'),
- _TABLE_PADDING_FRAGMENT,
- ('class:log-table-column-0', 'MODULE2'),
- _TABLE_PADDING_FRAGMENT,
- ('', '[MODULE2] 567.500 Another cool event'),
- ('', '\n')
- ],
-
[
- ('class:log-time', _TIMESTAMP_SAMPLE_STRING),
- _TABLE_PADDING_FRAGMENT,
- ('class:log-level-20', 'INF'),
- _TABLE_PADDING_FRAGMENT,
- ('class:log-table-column-0', 'MODULE1'),
- _TABLE_PADDING_FRAGMENT,
- ('class:log-table-column-1', '54321.200'),
- _TABLE_PADDING_FRAGMENT,
- ('', 'Something happened'),
- ('', '\n')
+ [
+ ('class:log-time', _TIMESTAMP_SAMPLE_STRING),
+ _TABLE_PADDING_FRAGMENT,
+ ('class:log-level-20', 'INF'),
+ _TABLE_PADDING_FRAGMENT,
+ ('class:log-table-column-0', 'MODULE2'),
+ _TABLE_PADDING_FRAGMENT,
+ ('', 'Another cool event'),
+ ('', '\n'),
+ ],
+ [
+ ('class:log-time', _TIMESTAMP_SAMPLE_STRING),
+ _TABLE_PADDING_FRAGMENT,
+ ('class:log-level-20', 'INF'),
+ _TABLE_PADDING_FRAGMENT,
+ ('class:log-table-column-0', 'MODULE2'),
+ _TABLE_PADDING_FRAGMENT,
+ ('', '[MODULE2] 567.500 Another cool event'),
+ ('', '\n'),
+ ],
+ [
+ ('class:log-time', _TIMESTAMP_SAMPLE_STRING),
+ _TABLE_PADDING_FRAGMENT,
+ ('class:log-level-20', 'INF'),
+ _TABLE_PADDING_FRAGMENT,
+ ('class:log-table-column-0', 'MODULE1'),
+ _TABLE_PADDING_FRAGMENT,
+ ('class:log-table-column-1', '54321.200'),
+ _TABLE_PADDING_FRAGMENT,
+ ('', 'Something happened'),
+ ('', '\n'),
+ ],
],
- ],
- ),
- ]) # yapf: disable
+ ),
+ ]
+ )
def test_formatted_rows(self, _name, logs, expected_log_format) -> None:
"""Test colum widths calculation."""
table = TableView(self.prefs)
diff --git a/pw_console/py/text_formatting_test.py b/pw_console/py/text_formatting_test.py
index 340cc5816..c1af2ccc5 100644
--- a/pw_console/py/text_formatting_test.py
+++ b/pw_console/py/text_formatting_test.py
@@ -27,160 +27,176 @@ from pw_console.text_formatting import (
class TestTextFormatting(unittest.TestCase):
"""Tests for manipulating prompt_toolkit formatted text tuples."""
+
def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
- @parameterized.expand([
- (
- 'with short prefix height 2',
- len('LINE that should be wrapped'), # text_width
- len('| |'), # screen_width
- len('--->'), # prefix_width
- ( 'LINE that should b\n'
- '--->e wrapped \n').count('\n'), # expected_height
- len( '_____'), # expected_trailing_characters
- ),
- (
- 'with short prefix height 3',
- len('LINE that should be wrapped three times.'), # text_width
- len('| |'), # screen_width
- len('--->'), # prefix_width
- ( 'LINE that should b\n'
- '--->e wrapped thre\n'
- '--->e times. \n').count('\n'), # expected_height
- len( '______'), # expected_trailing_characters
- ),
- (
- 'with short prefix height 4',
- len('LINE that should be wrapped even more times, say four.'),
- len('| |'), # screen_width
- len('--->'), # prefix_width
- ( 'LINE that should b\n'
- '--->e wrapped even\n'
- '---> more times, s\n'
- '--->ay four. \n').count('\n'), # expected_height
- len( '______'), # expected_trailing_characters
- ),
- (
- 'no wrapping needed',
- len('LINE wrapped'), # text_width
- len('| |'), # screen_width
- len('--->'), # prefix_width
- ( 'LINE wrapped \n').count('\n'), # expected_height
- len( '______'), # expected_trailing_characters
- ),
- (
- 'prefix is > screen width',
- len('LINE that should be wrapped'), # text_width
- len('| |'), # screen_width
- len('------------------>'), # prefix_width
- ( 'LINE that should b\n'
- 'e wrapped \n').count('\n'), # expected_height
- len( '_________'), # expected_trailing_characters
- ),
- (
- 'prefix is == screen width',
- len('LINE that should be wrapped'), # text_width
- len('| |'), # screen_width
- len('----------------->'), # prefix_width
- ( 'LINE that should b\n'
- 'e wrapped \n').count('\n'), # expected_height
- len( '_________'), # expected_trailing_characters
- ),
- ]) # yapf: disable
-
- def test_get_line_height(self, _name, text_width, screen_width,
- prefix_width, expected_height,
- expected_trailing_characters) -> None:
+ @parameterized.expand(
+ [
+ (
+ 'with short prefix height 2',
+ len('LINE that should be wrapped'), # text_width
+ len('| |'), # screen_width
+ len('--->'), # prefix_width
+ ('LINE that should b\n' '--->e wrapped \n').count(
+ '\n'
+ ), # expected_height
+ len('_____'), # expected_trailing_characters
+ ),
+ (
+ 'with short prefix height 3',
+ len('LINE that should be wrapped three times.'), # text_width
+ len('| |'), # screen_width
+ len('--->'), # prefix_width
+ (
+ 'LINE that should b\n'
+ '--->e wrapped thre\n'
+ '--->e times. \n'
+ ).count(
+ '\n'
+ ), # expected_height
+ len('______'), # expected_trailing_characters
+ ),
+ (
+ 'with short prefix height 4',
+ len('LINE that should be wrapped even more times, say four.'),
+ len('| |'), # screen_width
+ len('--->'), # prefix_width
+ (
+ 'LINE that should b\n'
+ '--->e wrapped even\n'
+ '---> more times, s\n'
+ '--->ay four. \n'
+ ).count(
+ '\n'
+ ), # expected_height
+ len('______'), # expected_trailing_characters
+ ),
+ (
+ 'no wrapping needed',
+ len('LINE wrapped'), # text_width
+ len('| |'), # screen_width
+ len('--->'), # prefix_width
+ ('LINE wrapped \n').count('\n'), # expected_height
+ len('______'), # expected_trailing_characters
+ ),
+ (
+ 'prefix is > screen width',
+ len('LINE that should be wrapped'), # text_width
+ len('| |'), # screen_width
+ len('------------------>'), # prefix_width
+ ('LINE that should b\n' 'e wrapped \n').count(
+ '\n'
+ ), # expected_height
+ len('_________'), # expected_trailing_characters
+ ),
+ (
+ 'prefix is == screen width',
+ len('LINE that should be wrapped'), # text_width
+ len('| |'), # screen_width
+ len('----------------->'), # prefix_width
+ ('LINE that should b\n' 'e wrapped \n').count(
+ '\n'
+ ), # expected_height
+ len('_________'), # expected_trailing_characters
+ ),
+ ]
+ )
+ def test_get_line_height(
+ self,
+ _name,
+ text_width,
+ screen_width,
+ prefix_width,
+ expected_height,
+ expected_trailing_characters,
+ ) -> None:
"""Test line height calculations."""
- height, remaining_width = get_line_height(text_width, screen_width,
- prefix_width)
+ height, remaining_width = get_line_height(
+ text_width, screen_width, prefix_width
+ )
self.assertEqual(height, expected_height)
self.assertEqual(remaining_width, expected_trailing_characters)
# pylint: disable=line-too-long
- @parameterized.expand([
- (
- 'One line with ANSI escapes and no included breaks',
- 12, # screen_width
- False, # truncate_long_lines
- 'Lorem ipsum \x1b[34m\x1b[1mdolor sit amet\x1b[0m, consectetur adipiscing elit.', # message
- ANSI(
- # Line 1
- 'Lorem ipsum \n'
- # Line 2
- '\x1b[34m\x1b[1m' # zero width
- 'dolor sit am\n'
- # Line 3
- 'et'
- '\x1b[0m' # zero width
- ', consecte\n'
- # Line 4
- 'tur adipisci\n'
- # Line 5
- 'ng elit.\n').__pt_formatted_text__(),
- 5, # expected_height
- ),
- (
- 'One line with ANSI escapes and included breaks',
- 12, # screen_width
- False, # truncate_long_lines
- 'Lorem\n ipsum \x1b[34m\x1b[1mdolor sit amet\x1b[0m, consectetur adipiscing elit.', # message
- ANSI(
- # Line 1
- 'Lorem\n'
- # Line 2
- ' ipsum \x1b[34m\x1b[1mdolor\n'
- # Line 3
- ' sit amet\x1b[0m, c\n'
- # Line 4
- 'onsectetur a\n'
- # Line 5
- 'dipiscing el\n'
- # Line 6
- 'it.\n'
- ).__pt_formatted_text__(),
- 6, # expected_height
- ),
- (
- 'One line with ANSI escapes and included breaks; truncate lines enabled',
- 12, # screen_width
- True, # truncate_long_lines
- 'Lorem\n ipsum dolor sit amet, consectetur adipiscing \nelit.\n', # message
- ANSI(
- # Line 1
- 'Lorem\n'
- # Line 2
- ' ipsum dolor\n'
- # Line 3
- 'elit.\n'
+ @parameterized.expand(
+ [
+ (
+ 'One line with ANSI escapes and no included breaks',
+ 12, # screen_width
+ False, # truncate_long_lines
+ 'Lorem ipsum \x1b[34m\x1b[1mdolor sit amet\x1b[0m, consectetur adipiscing elit.', # message
+ ANSI(
+ # Line 1
+ 'Lorem ipsum \n'
+ # Line 2
+ '\x1b[34m\x1b[1m' # zero width
+ 'dolor sit am\n'
+ # Line 3
+ 'et'
+ '\x1b[0m' # zero width
+ ', consecte\n'
+ # Line 4
+ 'tur adipisci\n'
+ # Line 5
+ 'ng elit.\n'
).__pt_formatted_text__(),
- 3, # expected_height
- ),
- (
- 'wrapping enabled with a line break just after screen_width',
- 10, # screen_width
- False, # truncate_long_lines
- '01234567890\nTest Log\n', # message
- ANSI(
- '0123456789\n'
- '0\n'
- 'Test Log\n'
+ 5, # expected_height
+ ),
+ (
+ 'One line with ANSI escapes and included breaks',
+ 12, # screen_width
+ False, # truncate_long_lines
+ 'Lorem\n ipsum \x1b[34m\x1b[1mdolor sit amet\x1b[0m, consectetur adipiscing elit.', # message
+ ANSI(
+ # Line 1
+ 'Lorem\n'
+ # Line 2
+ ' ipsum \x1b[34m\x1b[1mdolor\n'
+ # Line 3
+ ' sit amet\x1b[0m, c\n'
+ # Line 4
+ 'onsectetur a\n'
+ # Line 5
+ 'dipiscing el\n'
+ # Line 6
+ 'it.\n'
).__pt_formatted_text__(),
- 3, # expected_height
- ),
- (
- 'log message with a line break at screen_width',
- 10, # screen_width
- True, # truncate_long_lines
- '0123456789\nTest Log\n', # message
- ANSI(
- '0123456789\n'
- 'Test Log\n'
+ 6, # expected_height
+ ),
+ (
+ 'One line with ANSI escapes and included breaks; truncate lines enabled',
+ 12, # screen_width
+ True, # truncate_long_lines
+ 'Lorem\n ipsum dolor sit amet, consectetur adipiscing \nelit.\n', # message
+ ANSI(
+ # Line 1
+ 'Lorem\n'
+ # Line 2
+ ' ipsum dolor\n'
+ # Line 3
+ 'elit.\n'
).__pt_formatted_text__(),
- 2, # expected_height
- ),
- ]) # yapf: disable
+ 3, # expected_height
+ ),
+ (
+ 'wrapping enabled with a line break just after screen_width',
+ 10, # screen_width
+ False, # truncate_long_lines
+ '01234567890\nTest Log\n', # message
+ ANSI('0123456789\n' '0\n' 'Test Log\n').__pt_formatted_text__(),
+ 3, # expected_height
+ ),
+ (
+ 'log message with a line break at screen_width',
+ 10, # screen_width
+ True, # truncate_long_lines
+ '0123456789\nTest Log\n', # message
+ ANSI('0123456789\n' 'Test Log\n').__pt_formatted_text__(),
+ 2, # expected_height
+ ),
+ ]
+ )
# pylint: enable=line-too-long
def test_insert_linebreaks(
self,
@@ -198,56 +214,58 @@ class TestTextFormatting(unittest.TestCase):
fragments, line_height = insert_linebreaks(
formatted_text,
max_line_width=screen_width,
- truncate_long_lines=truncate_long_lines)
+ truncate_long_lines=truncate_long_lines,
+ )
self.assertEqual(fragments, expected_fragments)
self.assertEqual(line_height, expected_height)
- @parameterized.expand([
- (
- 'flattened split',
- ANSI(
- 'Lorem\n'
- ' ipsum dolor\n'
- 'elit.\n'
- ).__pt_formatted_text__(),
- [
- ANSI('Lorem').__pt_formatted_text__(),
- ANSI(' ipsum dolor').__pt_formatted_text__(),
- ANSI('elit.').__pt_formatted_text__(),
- ], # expected_lines
- ),
- (
- 'split fragments from insert_linebreaks',
- insert_linebreaks(
+ @parameterized.expand(
+ [
+ (
+ 'flattened split',
ANSI(
- 'Lorem\n ipsum dolor sit amet, consectetur adipiscing elit.'
+ 'Lorem\n' ' ipsum dolor\n' 'elit.\n'
).__pt_formatted_text__(),
- max_line_width=15,
- # [0] for the fragments, [1] is line_height
- truncate_long_lines=False)[0],
- [
- ANSI('Lorem').__pt_formatted_text__(),
- ANSI(' ipsum dolor si').__pt_formatted_text__(),
- ANSI('t amet, consect').__pt_formatted_text__(),
- ANSI('etur adipiscing').__pt_formatted_text__(),
- ANSI(' elit.').__pt_formatted_text__(),
- ],
- ),
- (
- 'empty lines',
- # Each line should have at least one StyleAndTextTuple but without
- # an ending line break.
- [
- ('', '\n'),
- ('', '\n'),
- ],
- [
- [('', '')],
- [('', '')],
- ],
- )
- ]) # yapf: disable
+ [
+ ANSI('Lorem').__pt_formatted_text__(),
+ ANSI(' ipsum dolor').__pt_formatted_text__(),
+ ANSI('elit.').__pt_formatted_text__(),
+ ], # expected_lines
+ ),
+ (
+ 'split fragments from insert_linebreaks',
+ insert_linebreaks(
+ ANSI(
+ 'Lorem\n ipsum dolor sit amet, consectetur adipiscing elit.'
+ ).__pt_formatted_text__(),
+ max_line_width=15,
+ # [0] for the fragments, [1] is line_height
+ truncate_long_lines=False,
+ )[0],
+ [
+ ANSI('Lorem').__pt_formatted_text__(),
+ ANSI(' ipsum dolor si').__pt_formatted_text__(),
+ ANSI('t amet, consect').__pt_formatted_text__(),
+ ANSI('etur adipiscing').__pt_formatted_text__(),
+ ANSI(' elit.').__pt_formatted_text__(),
+ ],
+ ),
+ (
+ 'empty lines',
+ # Each line should have at least one StyleAndTextTuple but without
+ # an ending line break.
+ [
+ ('', '\n'),
+ ('', '\n'),
+ ],
+ [
+ [('', '')],
+ [('', '')],
+ ],
+ ),
+ ]
+ )
def test_split_lines(
self,
_name,
diff --git a/pw_console/py/window_manager_test.py b/pw_console/py/window_manager_test.py
index 676d07cb4..f1b778e88 100644
--- a/pw_console/py/window_manager_test.py
+++ b/pw_console/py/window_manager_test.py
@@ -19,6 +19,7 @@ from unittest.mock import MagicMock
from prompt_toolkit.application import create_app_session
from prompt_toolkit.output import ColorDepth
+
# inclusive-language: ignore
from prompt_toolkit.output import DummyOutput as FakeOutput
@@ -29,17 +30,16 @@ from pw_console.window_list import _WINDOW_HEIGHT_ADJUST, DisplayMode
def _create_console_app(logger_count=2):
- console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT,
- prefs=ConsolePrefs(project_file=False,
- project_user_file=False,
- user_file=False))
+ prefs = ConsolePrefs(
+ project_file=False, project_user_file=False, user_file=False
+ )
+ prefs.set_code_theme('default')
+ console_app = ConsoleApp(color_depth=ColorDepth.DEPTH_8_BIT, prefs=prefs)
console_app.focus_on_container = MagicMock()
loggers = {}
for i in range(logger_count):
- loggers['Log{}'.format(i)] = [
- logging.getLogger('test_log{}'.format(i))
- ]
+ loggers['Log{}'.format(i)] = [logging.getLogger('test_log{}'.format(i))]
for window_title, logger_instances in loggers.items():
console_app.add_log_handler(window_title, logger_instances)
return console_app
@@ -52,8 +52,9 @@ _DEFAULT_WINDOW_HEIGHT = 10
def _window_list_widths(window_manager):
- window_manager.update_window_manager_size(_WINDOW_MANAGER_WIDTH,
- _WINDOW_MANAGER_HEIGHT)
+ window_manager.update_window_manager_size(
+ _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT
+ )
return [
window_list.width.preferred
@@ -62,8 +63,9 @@ def _window_list_widths(window_manager):
def _window_list_heights(window_manager):
- window_manager.update_window_manager_size(_WINDOW_MANAGER_WIDTH,
- _WINDOW_MANAGER_HEIGHT)
+ window_manager.update_window_manager_size(
+ _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT
+ )
return [
window_list.height.preferred
@@ -72,8 +74,9 @@ def _window_list_heights(window_manager):
def _window_pane_widths(window_manager, window_list_index=0):
- window_manager.update_window_manager_size(_WINDOW_MANAGER_WIDTH,
- _WINDOW_MANAGER_HEIGHT)
+ window_manager.update_window_manager_size(
+ _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT
+ )
return [
pane.width.preferred
@@ -82,8 +85,9 @@ def _window_pane_widths(window_manager, window_list_index=0):
def _window_pane_heights(window_manager, window_list_index=0):
- window_manager.update_window_manager_size(_WINDOW_MANAGER_WIDTH,
- _WINDOW_MANAGER_HEIGHT)
+ window_manager.update_window_manager_size(
+ _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT
+ )
return [
pane.height.preferred
@@ -99,27 +103,33 @@ def _window_pane_counts(window_manager):
def window_pane_titles(window_manager):
- return [[
- pane.pane_title() + ' - ' + pane.pane_subtitle()
- for pane in window_list.active_panes
- ] for window_list in window_manager.window_lists]
+ return [
+ [
+ pane.pane_title() + ' - ' + pane.pane_subtitle()
+ for pane in window_list.active_panes
+ ]
+ for window_list in window_manager.window_lists
+ ]
def target_list_and_pane(window_manager, list_index, pane_index):
# pylint: disable=protected-access
# Bypass prompt_toolkit has_focus()
- window_manager._get_active_window_list_and_pane = (
- MagicMock( # type: ignore
- return_value=(
- window_manager.window_lists[list_index],
- window_manager.window_lists[list_index].
- active_panes[pane_index],
- )))
+ pane = window_manager.window_lists[list_index].active_panes[pane_index]
+ # If the pane is in focus it will be visible.
+ pane.show_pane = True
+ window_manager._get_active_window_list_and_pane = MagicMock( # type: ignore
+ return_value=(
+ window_manager.window_lists[list_index],
+ window_manager.window_lists[list_index].active_panes[pane_index],
+ )
+ )
class TestWindowManager(unittest.TestCase):
# pylint: disable=protected-access
"""Tests for window management functions."""
+
def setUp(self):
self.maxDiff = None # pylint: disable=invalid-name
@@ -155,8 +165,10 @@ class TestWindowManager(unittest.TestCase):
target_pane = window_manager.window_lists[2].active_panes[0]
- result_window_list, result_pane_index = (
- window_manager._find_window_list_and_pane_index(target_pane))
+ (
+ result_window_list,
+ result_pane_index,
+ ) = window_manager.find_window_list_and_pane_index(target_pane)
self.assertEqual(
(result_window_list, result_pane_index),
(window_manager.window_lists[2], 0),
@@ -181,10 +193,13 @@ class TestWindowManager(unittest.TestCase):
# Move one pane to the right, creating a new window_list split.
window_manager.move_pane_right()
- self.assertEqual(_window_list_widths(window_manager), [
- int(_WINDOW_MANAGER_WIDTH / 2),
- int(_WINDOW_MANAGER_WIDTH / 2),
- ])
+ self.assertEqual(
+ _window_list_widths(window_manager),
+ [
+ int(_WINDOW_MANAGER_WIDTH / 2),
+ int(_WINDOW_MANAGER_WIDTH / 2),
+ ],
+ )
# Move another pane to the right twice, creating a third
# window_list split.
@@ -217,10 +232,8 @@ class TestWindowManager(unittest.TestCase):
_window_list_widths(window_manager),
[
int(_WINDOW_MANAGER_WIDTH / 3),
- int(_WINDOW_MANAGER_WIDTH / 3) -
- (2 * _WINDOW_SPLIT_ADJUST),
- int(_WINDOW_MANAGER_WIDTH / 3) +
- (2 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 3) - (2 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 3) + (2 * _WINDOW_SPLIT_ADJUST),
],
)
@@ -232,10 +245,8 @@ class TestWindowManager(unittest.TestCase):
self.assertEqual(
_window_list_widths(window_manager),
[
- int(_WINDOW_MANAGER_WIDTH / 3) -
- (1 * _WINDOW_SPLIT_ADJUST),
- int(_WINDOW_MANAGER_WIDTH / 3) +
- (1 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 3) - (1 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 3) + (1 * _WINDOW_SPLIT_ADJUST),
int(_WINDOW_MANAGER_WIDTH / 3),
],
)
@@ -249,10 +260,8 @@ class TestWindowManager(unittest.TestCase):
_window_list_widths(window_manager),
[
int(_WINDOW_MANAGER_WIDTH / 3),
- int(_WINDOW_MANAGER_WIDTH / 3) +
- (1 * _WINDOW_SPLIT_ADJUST),
- int(_WINDOW_MANAGER_WIDTH / 3) -
- (1 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 3) + (1 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 3) - (1 * _WINDOW_SPLIT_ADJUST),
],
)
@@ -265,10 +274,8 @@ class TestWindowManager(unittest.TestCase):
_window_list_widths(window_manager),
[
int(_WINDOW_MANAGER_WIDTH / 3),
- int(_WINDOW_MANAGER_WIDTH / 3) -
- (3 * _WINDOW_SPLIT_ADJUST),
- int(_WINDOW_MANAGER_WIDTH / 3) +
- (3 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 3) - (3 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 3) + (3 * _WINDOW_SPLIT_ADJUST),
],
)
@@ -282,11 +289,9 @@ class TestWindowManager(unittest.TestCase):
self.assertEqual(
_window_list_widths(window_manager),
[
- int(_WINDOW_MANAGER_WIDTH / 2) -
- (3 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 2) - (3 * _WINDOW_SPLIT_ADJUST),
# This split is removed
- int(_WINDOW_MANAGER_WIDTH / 2) +
- (2 * _WINDOW_SPLIT_ADJUST),
+ int(_WINDOW_MANAGER_WIDTH / 2) + (2 * _WINDOW_SPLIT_ADJUST),
],
)
@@ -313,11 +318,17 @@ class TestWindowManager(unittest.TestCase):
]
self.assertEqual(
list_pane_titles[0],
- [('', ' '), ('class:window-tab-inactive', ' Log2 test_log2 '),
- ('', ' '), ('class:window-tab-inactive', ' Log1 test_log1 '),
- ('', ' '), ('class:window-tab-inactive', ' Log0 test_log0 '),
- ('', ' '), ('class:window-tab-inactive', ' Python Repl '),
- ('', ' ')],
+ [
+ ('', ' '),
+ ('class:window-tab-inactive', ' Log2 test_log2 '),
+ ('', ' '),
+ ('class:window-tab-inactive', ' Log1 test_log1 '),
+ ('', ' '),
+ ('class:window-tab-inactive', ' Log0 test_log0 '),
+ ('', ' '),
+ ('class:window-tab-inactive', ' Python Repl '),
+ ('', ' '),
+ ],
)
def test_window_pane_movement_resizing(self) -> None:
@@ -329,7 +340,8 @@ class TestWindowManager(unittest.TestCase):
# 4 panes, 3 for the loggers and 1 for the repl.
self.assertEqual(
- len(window_manager.first_window_list().active_panes), 4)
+ len(window_manager.first_window_list().active_panes), 4
+ )
def target_window_pane(index: int):
# Bypass prompt_toolkit has_focus()
@@ -338,11 +350,13 @@ class TestWindowManager(unittest.TestCase):
return_value=(
window_manager.window_lists[0],
window_manager.window_lists[0].active_panes[index],
- )))
+ )
+ )
+ )
window_list = console_app.window_manager.first_window_list()
- window_list.get_current_active_pane = (
- MagicMock( # type: ignore
- return_value=window_list.active_panes[index]))
+ window_list.get_current_active_pane = MagicMock( # type: ignore
+ return_value=window_list.active_panes[index]
+ )
# Target the first window pane
target_window_pane(0)
@@ -361,7 +375,8 @@ class TestWindowManager(unittest.TestCase):
# Reset pane sizes
window_manager.window_lists[0].current_window_list_height = (
- 4 * _DEFAULT_WINDOW_HEIGHT)
+ 4 * _DEFAULT_WINDOW_HEIGHT
+ )
window_manager.reset_pane_sizes()
self.assertEqual(
_window_pane_heights(window_manager),
@@ -464,6 +479,7 @@ class TestWindowManager(unittest.TestCase):
console_app = _create_console_app(logger_count=4)
window_manager = console_app.window_manager
+ window_manager.window_lists[0].set_display_mode(DisplayMode.STACK)
self.assertEqual(
window_pane_titles(window_manager),
[
@@ -485,7 +501,8 @@ class TestWindowManager(unittest.TestCase):
window_manager.focus_next_pane()
# Pane index 1 should now be focused.
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[1])
+ window_manager.window_lists[0].active_panes[1]
+ )
console_app.focus_on_container.reset_mock()
# Set the first pane in focus.
@@ -495,7 +512,8 @@ class TestWindowManager(unittest.TestCase):
# Previous pane should wrap around to the last pane in the first
# window_list.
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[-1])
+ window_manager.window_lists[0].active_panes[-1]
+ )
console_app.focus_on_container.reset_mock()
# Set the last pane in focus.
@@ -505,7 +523,8 @@ class TestWindowManager(unittest.TestCase):
# Next pane should wrap around to the first pane in the first
# window_list.
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[0])
+ window_manager.window_lists[0].active_panes[0]
+ )
console_app.focus_on_container.reset_mock()
# Scenario: Move between panes with a single tabbed window list.
@@ -517,7 +536,8 @@ class TestWindowManager(unittest.TestCase):
# Setup the switch_to_tab mock
window_manager.window_lists[0].switch_to_tab = MagicMock(
- wraps=window_manager.window_lists[0].switch_to_tab)
+ wraps=window_manager.window_lists[0].switch_to_tab
+ )
# Set the first pane/tab in focus.
target_list_and_pane(window_manager, 0, 0)
@@ -525,10 +545,12 @@ class TestWindowManager(unittest.TestCase):
window_manager.focus_next_pane()
# Check switch_to_tab is called
window_manager.window_lists[
- 0].switch_to_tab.assert_called_once_with(1)
+ 0
+ ].switch_to_tab.assert_called_once_with(1)
# And that focus_on_container is called only once
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[1])
+ window_manager.window_lists[0].active_panes[1]
+ )
console_app.focus_on_container.reset_mock()
window_manager.window_lists[0].switch_to_tab.reset_mock()
@@ -538,10 +560,12 @@ class TestWindowManager(unittest.TestCase):
window_manager.focus_next_pane()
# Check switch_to_tab is called
window_manager.window_lists[
- 0].switch_to_tab.assert_called_once_with(0)
+ 0
+ ].switch_to_tab.assert_called_once_with(0)
# And that focus_on_container is called only once
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[0])
+ window_manager.window_lists[0].active_panes[0]
+ )
console_app.focus_on_container.reset_mock()
window_manager.window_lists[0].switch_to_tab.reset_mock()
@@ -551,10 +575,12 @@ class TestWindowManager(unittest.TestCase):
window_manager.focus_previous_pane()
# Check switch_to_tab is called
window_manager.window_lists[
- 0].switch_to_tab.assert_called_once_with(4)
+ 0
+ ].switch_to_tab.assert_called_once_with(4)
# And that focus_on_container is called only once
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[4])
+ window_manager.window_lists[0].active_panes[4]
+ )
console_app.focus_on_container.reset_mock()
window_manager.window_lists[0].switch_to_tab.reset_mock()
@@ -584,14 +610,16 @@ class TestWindowManager(unittest.TestCase):
# Setup the switch_to_tab mock on the second window_list
window_manager.window_lists[1].switch_to_tab = MagicMock(
- wraps=window_manager.window_lists[1].switch_to_tab)
+ wraps=window_manager.window_lists[1].switch_to_tab
+ )
# Set Log1 in focus
target_list_and_pane(window_manager, 0, 2)
window_manager.focus_next_pane()
# Log0 should now have focus
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[1].active_panes[0])
+ window_manager.window_lists[1].active_panes[0]
+ )
console_app.focus_on_container.reset_mock()
# Set Log0 in focus
@@ -599,11 +627,13 @@ class TestWindowManager(unittest.TestCase):
window_manager.focus_previous_pane()
# Log1 should now have focus
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[2])
+ window_manager.window_lists[0].active_panes[2]
+ )
# The first window list is in tabbed mode so switch_to_tab should be
# called once.
window_manager.window_lists[
- 0].switch_to_tab.assert_called_once_with(2)
+ 0
+ ].switch_to_tab.assert_called_once_with(2)
# Reset
window_manager.window_lists[0].switch_to_tab.reset_mock()
console_app.focus_on_container.reset_mock()
@@ -613,9 +643,11 @@ class TestWindowManager(unittest.TestCase):
window_manager.focus_next_pane()
# Log3 should now have focus
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[0])
+ window_manager.window_lists[0].active_panes[0]
+ )
window_manager.window_lists[
- 0].switch_to_tab.assert_called_once_with(0)
+ 0
+ ].switch_to_tab.assert_called_once_with(0)
# Reset
window_manager.window_lists[0].switch_to_tab.reset_mock()
console_app.focus_on_container.reset_mock()
@@ -625,9 +657,11 @@ class TestWindowManager(unittest.TestCase):
window_manager.focus_next_pane()
# Log2 should now have focus
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[0].active_panes[1])
+ window_manager.window_lists[0].active_panes[1]
+ )
window_manager.window_lists[
- 0].switch_to_tab.assert_called_once_with(1)
+ 0
+ ].switch_to_tab.assert_called_once_with(1)
# Reset
window_manager.window_lists[0].switch_to_tab.reset_mock()
console_app.focus_on_container.reset_mock()
@@ -637,7 +671,8 @@ class TestWindowManager(unittest.TestCase):
window_manager.focus_previous_pane()
# Log0 should now have focus
console_app.focus_on_container.assert_called_once_with(
- window_manager.window_lists[1].active_panes[0])
+ window_manager.window_lists[1].active_panes[0]
+ )
# The second window list is in stacked mode so switch_to_tab should
# not be called.
window_manager.window_lists[1].switch_to_tab.assert_not_called()
@@ -652,8 +687,9 @@ class TestWindowManager(unittest.TestCase):
window_manager = console_app.window_manager
# Required before moving windows
- window_manager.update_window_manager_size(_WINDOW_MANAGER_WIDTH,
- _WINDOW_MANAGER_HEIGHT)
+ window_manager.update_window_manager_size(
+ _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT
+ )
window_manager.create_root_container()
# Vertical split by default
@@ -694,8 +730,7 @@ class TestWindowManager(unittest.TestCase):
self.assertEqual(_window_list_widths(window_manager), widths)
# Decrease size of first split
- window_manager.adjust_split_size(window_manager.window_lists[0],
- -4)
+ window_manager.adjust_split_size(window_manager.window_lists[0], -4)
widths = [
widths[0] - (4 * _WINDOW_SPLIT_ADJUST),
widths[1] + (4 * _WINDOW_SPLIT_ADJUST),
@@ -728,13 +763,15 @@ class TestWindowManager(unittest.TestCase):
window_manager = console_app.window_manager
# We want horizontal window splits
- window_manager.vertical_window_list_spliting = (MagicMock(
- return_value=False))
+ window_manager.vertical_window_list_spliting = MagicMock(
+ return_value=False
+ )
self.assertFalse(window_manager.vertical_window_list_spliting())
# Required before moving windows
- window_manager.update_window_manager_size(_WINDOW_MANAGER_WIDTH,
- _WINDOW_MANAGER_HEIGHT)
+ window_manager.update_window_manager_size(
+ _WINDOW_MANAGER_WIDTH, _WINDOW_MANAGER_HEIGHT
+ )
window_manager.create_root_container()
# Move windows to create 3 splits
@@ -772,8 +809,7 @@ class TestWindowManager(unittest.TestCase):
self.assertEqual(_window_list_heights(window_manager), heights)
# Decrease size of first split
- window_manager.adjust_split_size(window_manager.window_lists[0],
- -4)
+ window_manager.adjust_split_size(window_manager.window_lists[0], -4)
heights = [
heights[0] - (4 * _WINDOW_SPLIT_ADJUST),
heights[1] + (4 * _WINDOW_SPLIT_ADJUST),
diff --git a/pw_console/testing.rst b/pw_console/testing.rst
index 0f9b719f2..af91318fb 100644
--- a/pw_console/testing.rst
+++ b/pw_console/testing.rst
@@ -35,7 +35,7 @@ Log Pane: Basic Actions
- ✅
* - 1
- - Click the :guilabel:`Fake Device Logs` window title
+ - Click the :guilabel:`Fake Device` window title
- Log pane is focused
- |checkbox|
@@ -140,7 +140,7 @@ Log Pane: Search and Filtering
- ✅
* - 1
- - Click the :guilabel:`Fake Device Logs` window title
+ - Click the :guilabel:`Fake Device` window title
- Log pane is focused
- |checkbox|
@@ -356,6 +356,39 @@ Help Windows
- Window is hidden
- |checkbox|
+Floating Windows
+^^^^^^^^^^^^^^^^
+
+.. list-table::
+ :widths: 5 45 45 5
+ :header-rows: 1
+
+ * - #
+ - Test Action
+ - Expected Result
+ - ✅
+
+ * - 1
+ - Start ``pw-console --test-mode`` press ``Ctrl-p``
+ - The :guilabel:`Menu Items` command runner dialog appears.
+ - |checkbox|
+
+ * - 2
+ - Type :kbd:`exit` and press :kbd:`enter`.
+ - The console exits.
+ - |checkbox|
+
+ * - 3
+ - Restart ``pw-console`` but without the ``--test-mode`` option.
+ - Console starts up with ONLY the Python Results and Repl windows.
+ - |checkbox|
+
+ * - 4
+ - Press ``Ctrl-p``
+ - The :guilabel:`Menu Items` command runner dialog appears.
+ - |checkbox|
+
+
Window Management
^^^^^^^^^^^^^^^^^
@@ -369,12 +402,12 @@ Window Management
- ✅
* - 1
- - | Click the :guilabel:`Fake Device Logs` window title
+ - | Click the :guilabel:`Fake Device` window title
- Log pane is focused
- |checkbox|
* - 2
- - | Click the menu :guilabel:`Windows > #: Fake Device Logs...`
+ - | Click the menu :guilabel:`Windows > #: Fake Device...`
| Click :guilabel:`Duplicate pane`
- | 3 panes are visible:
| Log pane on top
@@ -383,8 +416,8 @@ Window Management
- |checkbox|
* - 3
- - | Click the :guilabel:`Python Input` window title
- - Python Input pane is focused
+ - | Click the :guilabel:`Python Repl` window title
+ - Python Repl pane is focused
- |checkbox|
* - 4
@@ -410,7 +443,7 @@ Window Management
- |checkbox|
* - 7
- - | Click the menu :guilabel:`Windows > #: Fake Device Logs...`
+ - | Click the menu :guilabel:`Windows > #: Fake Device...`
| Click :guilabel:`Remove pane`
- | 2 panes are visible:
| Repl pane on the top
@@ -418,7 +451,7 @@ Window Management
- |checkbox|
* - 8
- - | Click the :guilabel:`Python Input`
+ - | Click the :guilabel:`Python Repl`
| window title
- Repl pane is focused
- |checkbox|
@@ -487,7 +520,7 @@ Mouse Window Resizing
- ✅
* - 1
- - | Click the :guilabel:`Fake Device Logs` window
+ - | Click the :guilabel:`Fake Device` window
- Log pane is focused
- |checkbox|
@@ -507,7 +540,7 @@ Mouse Window Resizing
* - 4
- Click the :guilabel:`View > Move Window Right`
- - :guilabel:`Fake Device Logs` should appear in a right side split
+ - :guilabel:`Fake Device` should appear in a right side split
- |checkbox|
* - 5
@@ -549,7 +582,7 @@ Copy Paste
- ✅
* - 1
- - | Click the :guilabel:`Fake Device Logs` window title
+ - | Click the :guilabel:`Fake Device` window title
- Log pane is focused
- |checkbox|
@@ -573,7 +606,7 @@ Copy Paste
- | Copy this text in your browser or
| text editor to the system clipboard:
| ``print('copy paste test!')``
- - | Click the :guilabel:`Python Input` window title
+ - | Click the :guilabel:`Python Repl` window title
| Press :kbd:`Ctrl-v`
| ``print('copy paste test!')`` appears
| after the prompt.
@@ -615,8 +648,8 @@ Copy Paste
- |checkbox|
* - 10
- - Click the :guilabel:`Python Input` window title
- - Python Input is focused
+ - Click the :guilabel:`Python Repl` window title
+ - Python Repl is focused
- |checkbox|
* - 11
@@ -641,8 +674,8 @@ Incremental Stdout
- ✅
* - 1
- - | Click the :guilabel:`Python Input` window title
- - Python Input pane is focused
+ - | Click the :guilabel:`Python Repl` window title
+ - Python Repl pane is focused
- |checkbox|
* - 2
@@ -655,7 +688,7 @@ Incremental Stdout
| (not all at once after a delay).
- |checkbox|
-Python Input & Output
+Python Repl & Output
^^^^^^^^^^^^^^^^^^^^^
.. list-table::
@@ -678,8 +711,8 @@ Python Input & Output
- |checkbox|
* - 3
- - Click empty whitespace in the ``Python Input`` window
- - Python Input pane is focused
+ - Click empty whitespace in the ``Python Repl`` window
+ - Python Repl pane is focused
- |checkbox|
* - 4
@@ -698,17 +731,85 @@ Python Input & Output
* - 5
- | Enter the following text and press :kbd:`Enter` to run
- | ``globals()``
+ | ``locals()``
- | The results should appear pretty printed
- |checkbox|
* - 6
- - | With the cursor over the Python Output,
+ - | Enter the following text and press :kbd:`Enter` to run
+ | ``zzzz = 'test'``
+ - | No new results are shown
+ | The previous ``locals()`` output does not show ``'zzzz': 'test'``
+ - |checkbox|
+
+ * - 7
+ - | Enter the following text and press :kbd:`Enter` to run
+ | ``locals()``
+ - | The output ends with ``'zzzz': 'test'}``
+ - |checkbox|
+
+ * - 8
+ - | With the cursor over the Python Results,
| use the mouse wheel to scroll up and down.
- | The output window should be able to scroll all
| the way to the beginning and end of the buffer.
- |checkbox|
+ * - 9
+ - Click empty whitespace in the ``Python Repl`` window
+ - Python Repl pane is focused
+ - |checkbox|
+
+ * - 10
+ - | Enter the following text and press :kbd:`Enter` to run
+ | ``!ls``
+ - | 1. Shell output of running the ``ls`` command should appear in the
+ | results window.
+ | 2. A new log window pane should appear titled ``Shell Output``.
+ | 3. The Shell Output window should show the command that was run and the
+ | output:
+ | ``$ ls``
+ | ``activate.bat``
+ | ``activate.sh``
+ - |checkbox|
+
+Web Log Viewer
+^^^^^^^^^^^^^^
+
+.. list-table::
+ :widths: 5 45 45 5
+ :header-rows: 1
+
+ * - #
+ - Test Action
+ - Expected Result
+ - ✅
+
+ * - 1
+ - | Start the pw console test mode by
+ | running ``pw console --test-mode``
+ - | Console starts up showing an ``Fake Device`` window.
+ - |checkbox|
+
+ * - 2
+ - | Focus on ``Fake Device`` panel and press :kbd:`Shift-o` to enable web log viewer
+ - | This should hide log stream in the console and automatically copy the
+ | URL to log viewer to the clipboard
+ - |checkbox|
+ * - 3
+ - | Focus on the ``Fake Keys`` panel with a filter applied. Then press
+ | :kbd:`Shift-o` to enable another web log viewer for that new pane. Open the
+ | new URL in Chrome
+ - | This log viewer should have filters pre-applied
+ - |checkbox|
+ * - 4
+ - | Press :kbd:`Shift-o` again on both log panes to disable web log view
+ - | This should re-enable log stream in console and stop streaming logs to
+ | web view
+ - |checkbox|
+
+
+
Early Startup
^^^^^^^^^^^^^
@@ -793,11 +894,11 @@ Quit Confirmation Dialog
* - 9
- | Press :kbd:`q`
- - | The help window disappears and the Python Input is in focus.
+ - | The help window disappears and the Python Repl is in focus.
- |checkbox|
* - 10
- - | Type some text into the Python Input.
+ - | Type some text into the Python Repl.
| Press :kbd:`Home` or move the cursor to the
| beginning of the text you just entered.
| Press :kbd:`Ctrl-d`
@@ -805,7 +906,7 @@ Quit Confirmation Dialog
- |checkbox|
* - 11
- - | Press :kbd:`Ctrl-c` to clear the Python Input text
+ - | Press :kbd:`Ctrl-c` to clear the Python Repl text
| Press :kbd:`Ctrl-d`
- | The quit dialog appears.
- |checkbox|
diff --git a/pw_containers/Android.bp b/pw_containers/Android.bp
new file mode 100644
index 000000000..45581b02e
--- /dev/null
+++ b/pw_containers/Android.bp
@@ -0,0 +1,35 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_containers",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_preprocessor_headers",
+ ],
+ host_supported: true,
+ srcs: [
+ "intrusive_list.cc",
+ ],
+}
diff --git a/pw_containers/BUILD.bazel b/pw_containers/BUILD.bazel
index 6928f097c..91f996226 100644
--- a/pw_containers/BUILD.bazel
+++ b/pw_containers/BUILD.bazel
@@ -25,6 +25,7 @@ licenses(["notice"])
pw_cc_library(
name = "pw_containers",
deps = [
+ ":algorithm",
":flat_map",
":intrusive_list",
":vector",
@@ -32,6 +33,15 @@ pw_cc_library(
)
pw_cc_library(
+ name = "algorithm",
+ hdrs = [
+ "public/pw_containers/algorithm.h",
+ "public/pw_containers/internal/algorithm_internal.h",
+ ],
+ includes = ["public"],
+)
+
+pw_cc_library(
name = "intrusive_list",
srcs = [
"intrusive_list.cc",
@@ -41,7 +51,16 @@ pw_cc_library(
"public/pw_containers/intrusive_list.h",
],
includes = ["public"],
- deps = ["//pw_assert"],
+ deps = [
+ "//pw_assert",
+ "//pw_compilation_testing:negative_compilation_testing",
+ ],
+)
+
+pw_cc_library(
+ name = "iterator",
+ hdrs = ["public/pw_containers/iterator.h"],
+ includes = ["public"],
)
pw_cc_library(
@@ -51,7 +70,7 @@ pw_cc_library(
],
includes = ["public"],
deps = [
- "//pw_assert",
+ "//pw_assert:facade",
"//pw_polyfill",
],
)
@@ -60,7 +79,10 @@ pw_cc_library(
name = "filtered_view",
hdrs = ["public/pw_containers/filtered_view.h"],
includes = ["public"],
- deps = ["//pw_assert"],
+ deps = [
+ "//pw_assert",
+ "//pw_containers",
+ ],
)
pw_cc_library(
@@ -82,6 +104,17 @@ pw_cc_library(
)
pw_cc_test(
+ name = "algorithm_test",
+ srcs = ["algorithm_test.cc"],
+ deps = [
+ ":algorithm",
+ ":intrusive_list",
+ ":vector",
+ "//pw_span",
+ ],
+)
+
+pw_cc_test(
name = "filtered_view_test",
srcs = ["filtered_view_test.cc"],
deps = [
diff --git a/pw_containers/BUILD.gn b/pw_containers/BUILD.gn
index 42835a66e..e1632ada8 100644
--- a/pw_containers/BUILD.gn
+++ b/pw_containers/BUILD.gn
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
@@ -24,15 +25,28 @@ config("public_include_path") {
group("pw_containers") {
public_deps = [
+ ":algorithm",
":flat_map",
":intrusive_list",
":vector",
]
}
+pw_source_set("algorithm") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "public/pw_containers/algorithm.h",
+ "public/pw_containers/internal/algorithm_internal.h",
+ ]
+}
+
pw_source_set("filtered_view") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_containers/filtered_view.h" ]
+ public_deps = [
+ dir_pw_assert,
+ dir_pw_preprocessor,
+ ]
}
pw_source_set("flat_map") {
@@ -40,6 +54,12 @@ pw_source_set("flat_map") {
public = [ "public/pw_containers/flat_map.h" ]
}
+pw_source_set("iterator") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ dir_pw_polyfill ]
+ public = [ "public/pw_containers/iterator.h" ]
+}
+
pw_source_set("to_array") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_containers/to_array.h" ]
@@ -47,7 +67,10 @@ pw_source_set("to_array") {
pw_source_set("vector") {
public_configs = [ ":public_include_path" ]
- public_deps = [ dir_pw_assert ]
+ public_deps = [
+ dir_pw_assert,
+ dir_pw_polyfill,
+ ]
public = [ "public/pw_containers/vector.h" ]
}
@@ -68,6 +91,7 @@ pw_source_set("intrusive_list") {
pw_test_group("tests") {
tests = [
+ ":algorithm_test",
":filtered_view_test",
":flat_map_test",
":intrusive_list_test",
@@ -77,11 +101,22 @@ pw_test_group("tests") {
]
}
+pw_test("algorithm_test") {
+ sources = [ "algorithm_test.cc" ]
+ deps = [
+ ":algorithm",
+ ":intrusive_list",
+ ":vector",
+ dir_pw_span,
+ ]
+}
+
pw_test("filtered_view_test") {
sources = [ "filtered_view_test.cc" ]
deps = [
":filtered_view",
":intrusive_list",
+ dir_pw_span,
]
}
@@ -111,8 +146,46 @@ pw_test("intrusive_list_test") {
":intrusive_list",
"$dir_pw_preprocessor",
]
+ negative_compilation_tests = true
}
pw_doc_group("docs") {
sources = [ "docs.rst" ]
+ report_deps = [ ":containers_size_report" ]
+}
+
+pw_size_diff("containers_size_report") {
+ title = "Pigweed containers size report"
+ binaries = [
+ {
+ target = "size_report:linked_list_one_item"
+ base = "size_report:base"
+ label = "linked list one item"
+ },
+ {
+ target = "size_report:linked_list_two_item"
+ base = "size_report:base"
+ label = "linked list two item"
+ },
+ {
+ target = "size_report:linked_list_four_item"
+ base = "size_report:base"
+ label = "linked list four item"
+ },
+ {
+ target = "size_report:intrusive_list_one_item"
+ base = "size_report:base"
+ label = "intrusive list one item"
+ },
+ {
+ target = "size_report:intrusive_list_two_item"
+ base = "size_report:base"
+ label = "intrusive list two item"
+ },
+ {
+ target = "size_report:intrusive_list_four_item"
+ base = "size_report:base"
+ label = "intrusive list four item"
+ },
+ ]
}
diff --git a/pw_containers/CMakeLists.txt b/pw_containers/CMakeLists.txt
index e960e3d5e..4b12aa2ce 100644
--- a/pw_containers/CMakeLists.txt
+++ b/pw_containers/CMakeLists.txt
@@ -14,8 +14,9 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_containers
+pw_add_library(pw_containers INTERFACE
PUBLIC_DEPS
+ pw_containers.algorithm
pw_containers.flat_map
pw_containers.intrusive_list
pw_containers.vector
@@ -24,44 +25,63 @@ if(Zephyr_FOUND AND CONFIG_PIGWEED_CONTAINERS)
zephyr_link_libraries(pw_containers)
endif()
-pw_add_module_library(pw_containers.filtered_view
+pw_add_library(pw_containers.algorithm INTERFACE
+ HEADERS
+ public/pw_containers/algorithm.h
+ public/pw_containers/internal/algorithm_internal.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_library(pw_containers.filtered_view INTERFACE
HEADERS
public/pw_containers/filtered_view.h
PUBLIC_INCLUDES
public
+ PUBLIC_DEPS
+ pw_assert
+ pw_preprocessor
)
-pw_add_module_library(pw_containers.flat_map
+pw_add_library(pw_containers.flat_map INTERFACE
HEADERS
public/pw_containers/flat_map.h
PUBLIC_INCLUDES
public
)
-pw_add_module_library(pw_containers.to_array
+pw_add_library(pw_containers.iterator INTERFACE
+ HEADERS
+ public/pw_containers/iterator.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_library(pw_containers.to_array INTERFACE
HEADERS
public/pw_containers/to_array.h
PUBLIC_INCLUDES
public
)
-pw_add_module_library(pw_containers.vector
+pw_add_library(pw_containers.vector INTERFACE
HEADERS
public/pw_containers/vector.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
pw_assert
+ pw_polyfill
)
-pw_add_module_library(pw_containers.wrapped_iterator
+pw_add_library(pw_containers.wrapped_iterator INTERFACE
HEADERS
public/pw_containers/wrapped_iterator.h
PUBLIC_INCLUDES
public
)
-pw_add_module_library(pw_containers.intrusive_list
+pw_add_library(pw_containers.intrusive_list STATIC
HEADERS
public/pw_containers/internal/intrusive_list_impl.h
public/pw_containers/intrusive_list.h
@@ -73,13 +93,26 @@ pw_add_module_library(pw_containers.intrusive_list
pw_assert
)
+pw_add_test(pw_containers.algorithm_test
+ SOURCES
+ algorithm_test.cc
+ PRIVATE_DEPS
+ pw_containers.algorithm
+ pw_containers.intrusive_list
+ pw_containers.vector
+ pw_span
+ GROUPS
+ modules
+ pw_containers
+)
+
pw_add_test(pw_containers.filtered_view_test
SOURCES
filtered_view_test.cc
- DEPS
+ PRIVATE_DEPS
pw_containers.filtered_view
pw_containers.intrusive_list
- pw_polyfill.span
+ pw_span
GROUPS
modules
pw_containers
@@ -88,7 +121,7 @@ pw_add_test(pw_containers.filtered_view_test
pw_add_test(pw_containers.flat_map_test
SOURCES
flat_map_test.cc
- DEPS
+ PRIVATE_DEPS
pw_containers.flat_map
GROUPS
modules
@@ -98,7 +131,7 @@ pw_add_test(pw_containers.flat_map_test
pw_add_test(pw_containers.to_array_test
SOURCES
to_array_test.cc
- DEPS
+ PRIVATE_DEPS
pw_containers.to_array
GROUPS
modules
@@ -108,7 +141,7 @@ pw_add_test(pw_containers.to_array_test
pw_add_test(pw_containers.vector_test
SOURCES
vector_test.cc
- DEPS
+ PRIVATE_DEPS
pw_containers.vector
GROUPS
modules
@@ -118,7 +151,7 @@ pw_add_test(pw_containers.vector_test
pw_add_test(pw_containers.wrapped_iterator_test
SOURCES
wrapped_iterator_test.cc
- DEPS
+ PRIVATE_DEPS
pw_containers.wrapped_iterator
GROUPS
modules
@@ -128,7 +161,8 @@ pw_add_test(pw_containers.wrapped_iterator_test
pw_add_test(pw_containers.intrusive_list_test
SOURCES
intrusive_list_test.cc
- DEPS
+ PRIVATE_DEPS
+ pw_compilation_testing._pigweed_only_negative_compilation
pw_containers.intrusive_list
pw_preprocessor
GROUPS
diff --git a/pw_containers/algorithm_test.cc b/pw_containers/algorithm_test.cc
new file mode 100644
index 000000000..b4d1c0c5d
--- /dev/null
+++ b/pw_containers/algorithm_test.cc
@@ -0,0 +1,294 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+//
+// These tests were forked from
+// https://cs.opensource.google/abseil/abseil-cpp/+/main:absl/algorithm/algorithm_test.cc;drc=38b704384cd2f17590b3922b97744be0b43622c9
+// However they were modified to work with containers available in Pigweed
+// without dynamic allocations, i.e. no std::vector, std::map, std::list, etc.
+#include "pw_containers/algorithm.h"
+
+#include <algorithm>
+#include <iterator>
+
+#include "gtest/gtest.h"
+#include "pw_containers/intrusive_list.h"
+#include "pw_containers/vector.h"
+#include "pw_span/span.h"
+
+namespace {
+
+class TestItem : public pw::IntrusiveList<TestItem>::Item {
+ public:
+ TestItem() : number_(0) {}
+ TestItem(int number) : number_(number) {}
+
+ // Add equality comparison to ensure comparisons are done by identity rather
+ // than equality for the remove function.
+ bool operator==(const TestItem& other) const {
+ return number_ == other.number_;
+ }
+
+ private:
+ int number_;
+};
+
+// Most of these tests just check that the code compiles, not that it
+// does the right thing. That's fine since the functions just forward
+// to the STL implementation.
+class NonMutatingTest : public testing::Test {
+ protected:
+ std::array<int, 3> span_array_ = {1, 2, 3};
+ pw::span<int> span_{NonMutatingTest::span_array_};
+ pw::Vector<int, 3> vector_ = {1, 2, 3};
+ int array_[3] = {1, 2, 3};
+};
+
+struct AccumulateCalls {
+ void operator()(int value) { calls.push_back(value); }
+ pw::Vector<int, 10> calls;
+};
+
+bool Predicate(int value) { return value < 3; }
+bool BinPredicate(int v1, int v2) { return v1 < v2; }
+bool Equals(int v1, int v2) { return v1 == v2; }
+
+} // namespace
+
+TEST_F(NonMutatingTest, AllOf) {
+ const pw::Vector<int>& v = vector_;
+ EXPECT_FALSE(pw::containers::AllOf(v, [](int x) { return x > 1; }));
+ EXPECT_TRUE(pw::containers::AllOf(v, [](int x) { return x > 0; }));
+}
+
+TEST_F(NonMutatingTest, AnyOf) {
+ const pw::Vector<int>& v = vector_;
+ EXPECT_TRUE(pw::containers::AnyOf(v, [](int x) { return x > 2; }));
+ EXPECT_FALSE(pw::containers::AnyOf(v, [](int x) { return x > 5; }));
+}
+
+TEST_F(NonMutatingTest, NoneOf) {
+ const pw::Vector<int>& v = vector_;
+ EXPECT_FALSE(pw::containers::NoneOf(v, [](int x) { return x > 2; }));
+ EXPECT_TRUE(pw::containers::NoneOf(v, [](int x) { return x > 5; }));
+}
+
+TEST_F(NonMutatingTest, ForEach) {
+ AccumulateCalls c = pw::containers::ForEach(span_, AccumulateCalls());
+ std::sort(c.calls.begin(), c.calls.end());
+ EXPECT_EQ(vector_, c.calls);
+}
+
+TEST_F(NonMutatingTest, FindReturnsCorrectType) {
+ auto it = pw::containers::Find(span_, 3);
+ EXPECT_EQ(3, *it);
+ pw::containers::Find(vector_, 3);
+}
+
+TEST_F(NonMutatingTest, FindIf) { pw::containers::FindIf(span_, Predicate); }
+
+TEST_F(NonMutatingTest, FindIfNot) {
+ pw::containers::FindIfNot(span_, Predicate);
+}
+
+TEST_F(NonMutatingTest, FindEnd) {
+ pw::containers::FindEnd(array_, vector_);
+ pw::containers::FindEnd(vector_, array_);
+}
+
+TEST_F(NonMutatingTest, FindEndWithPredicate) {
+ pw::containers::FindEnd(array_, vector_, BinPredicate);
+ pw::containers::FindEnd(vector_, array_, BinPredicate);
+}
+
+TEST_F(NonMutatingTest, FindFirstOf) {
+ pw::containers::FindFirstOf(span_, array_);
+ pw::containers::FindFirstOf(array_, span_);
+}
+
+TEST_F(NonMutatingTest, FindFirstOfWithPredicate) {
+ pw::containers::FindFirstOf(span_, array_, BinPredicate);
+ pw::containers::FindFirstOf(array_, span_, BinPredicate);
+}
+
+TEST_F(NonMutatingTest, AdjacentFind) { pw::containers::AdjacentFind(array_); }
+
+TEST_F(NonMutatingTest, AdjacentFindWithPredicate) {
+ pw::containers::AdjacentFind(array_, BinPredicate);
+}
+
+TEST_F(NonMutatingTest, Count) {
+ EXPECT_EQ(1, pw::containers::Count(span_, 3));
+}
+
+TEST_F(NonMutatingTest, CountIf) {
+ EXPECT_EQ(2, pw::containers::CountIf(span_, Predicate));
+}
+
+TEST_F(NonMutatingTest, Mismatch) {
+ // Testing necessary as pw::containers::Mismatch executes logic.
+ {
+ auto result = pw::containers::Mismatch(span_, vector_);
+ EXPECT_EQ(result.first, span_.end());
+ EXPECT_EQ(result.second, vector_.end());
+ }
+ {
+ auto result = pw::containers::Mismatch(vector_, span_);
+ EXPECT_EQ(result.first, vector_.end());
+ EXPECT_EQ(result.second, span_.end());
+ }
+
+ vector_.back() = 5;
+ {
+ auto result = pw::containers::Mismatch(span_, vector_);
+ EXPECT_EQ(result.first, std::prev(span_.end()));
+ EXPECT_EQ(result.second, std::prev(vector_.end()));
+ }
+ {
+ auto result = pw::containers::Mismatch(vector_, span_);
+ EXPECT_EQ(result.first, std::prev(vector_.end()));
+ EXPECT_EQ(result.second, std::prev(span_.end()));
+ }
+
+ vector_.pop_back();
+ {
+ auto result = pw::containers::Mismatch(span_, vector_);
+ EXPECT_EQ(result.first, std::prev(span_.end()));
+ EXPECT_EQ(result.second, vector_.end());
+ }
+ {
+ auto result = pw::containers::Mismatch(vector_, span_);
+ EXPECT_EQ(result.first, vector_.end());
+ EXPECT_EQ(result.second, std::prev(span_.end()));
+ }
+ {
+ struct NoNotEquals {
+ constexpr bool operator==(NoNotEquals) const { return true; }
+ constexpr bool operator!=(NoNotEquals) const = delete;
+ };
+ pw::Vector<NoNotEquals, 1> first;
+ std::array<NoNotEquals, 1> second;
+
+ // Check this still compiles.
+ pw::containers::Mismatch(first, second);
+ }
+}
+
+TEST_F(NonMutatingTest, MismatchWithPredicate) {
+ // Testing necessary as pw::containers::Mismatch executes logic.
+ {
+ auto result = pw::containers::Mismatch(span_, vector_, BinPredicate);
+ EXPECT_EQ(result.first, span_.begin());
+ EXPECT_EQ(result.second, vector_.begin());
+ }
+ {
+ auto result = pw::containers::Mismatch(vector_, span_, BinPredicate);
+ EXPECT_EQ(result.first, vector_.begin());
+ EXPECT_EQ(result.second, span_.begin());
+ }
+
+ vector_.front() = 0;
+ {
+ auto result = pw::containers::Mismatch(span_, vector_, BinPredicate);
+ EXPECT_EQ(result.first, span_.begin());
+ EXPECT_EQ(result.second, vector_.begin());
+ }
+ {
+ auto result = pw::containers::Mismatch(vector_, span_, BinPredicate);
+ EXPECT_EQ(result.first, std::next(vector_.begin()));
+ EXPECT_EQ(result.second, std::next(span_.begin()));
+ }
+
+ vector_.clear();
+ {
+ auto result = pw::containers::Mismatch(span_, vector_, BinPredicate);
+ EXPECT_EQ(result.first, span_.begin());
+ EXPECT_EQ(result.second, vector_.end());
+ }
+ {
+ auto result = pw::containers::Mismatch(vector_, span_, BinPredicate);
+ EXPECT_EQ(result.first, vector_.end());
+ EXPECT_EQ(result.second, span_.begin());
+ }
+}
+
+TEST_F(NonMutatingTest, Equal) {
+ EXPECT_TRUE(pw::containers::Equal(vector_, span_));
+ EXPECT_TRUE(pw::containers::Equal(span_, vector_));
+ EXPECT_TRUE(pw::containers::Equal(span_, array_));
+ EXPECT_TRUE(pw::containers::Equal(array_, vector_));
+
+ // Test that behavior appropriately differs from that of equal().
+ pw::Vector<int, 4> vector_plus = {1, 2, 3};
+ vector_plus.push_back(4);
+ EXPECT_FALSE(pw::containers::Equal(vector_plus, span_));
+ EXPECT_FALSE(pw::containers::Equal(span_, vector_plus));
+ EXPECT_FALSE(pw::containers::Equal(array_, vector_plus));
+}
+
+TEST_F(NonMutatingTest, EqualWithPredicate) {
+ EXPECT_TRUE(pw::containers::Equal(vector_, span_, Equals));
+ EXPECT_TRUE(pw::containers::Equal(span_, vector_, Equals));
+ EXPECT_TRUE(pw::containers::Equal(array_, span_, Equals));
+ EXPECT_TRUE(pw::containers::Equal(vector_, array_, Equals));
+
+ // Test that behavior appropriately differs from that of equal().
+ pw::Vector<int, 4> vector_plus = {1, 2, 3};
+ vector_plus.push_back(4);
+ EXPECT_FALSE(pw::containers::Equal(vector_plus, span_, Equals));
+ EXPECT_FALSE(pw::containers::Equal(span_, vector_plus, Equals));
+ EXPECT_FALSE(pw::containers::Equal(vector_plus, array_, Equals));
+}
+
+TEST_F(NonMutatingTest, IsPermutation) {
+ auto vector_permut_ = vector_;
+ std::next_permutation(vector_permut_.begin(), vector_permut_.end());
+ EXPECT_TRUE(pw::containers::IsPermutation(vector_permut_, span_));
+ EXPECT_TRUE(pw::containers::IsPermutation(span_, vector_permut_));
+
+ // Test that behavior appropriately differs from that of is_permutation().
+ pw::Vector<int, 4> vector_plus = {1, 2, 3};
+ vector_plus.push_back(4);
+ EXPECT_FALSE(pw::containers::IsPermutation(vector_plus, span_));
+ EXPECT_FALSE(pw::containers::IsPermutation(span_, vector_plus));
+}
+
+TEST_F(NonMutatingTest, IsPermutationWithPredicate) {
+ auto vector_permut_ = vector_;
+ std::next_permutation(vector_permut_.begin(), vector_permut_.end());
+ EXPECT_TRUE(pw::containers::IsPermutation(vector_permut_, span_, Equals));
+ EXPECT_TRUE(pw::containers::IsPermutation(span_, vector_permut_, Equals));
+
+ // Test that behavior appropriately differs from that of is_permutation().
+ pw::Vector<int, 4> vector_plus = {1, 2, 3};
+ vector_plus.push_back(4);
+ EXPECT_FALSE(pw::containers::IsPermutation(vector_plus, span_, Equals));
+ EXPECT_FALSE(pw::containers::IsPermutation(span_, vector_plus, Equals));
+}
+
+TEST_F(NonMutatingTest, Search) {
+ pw::containers::Search(span_, vector_);
+ pw::containers::Search(vector_, span_);
+ pw::containers::Search(array_, span_);
+}
+
+TEST_F(NonMutatingTest, SearchWithPredicate) {
+ pw::containers::Search(span_, vector_, BinPredicate);
+ pw::containers::Search(vector_, span_, BinPredicate);
+}
+
+TEST_F(NonMutatingTest, SearchN) { pw::containers::SearchN(span_, 3, 1); }
+
+TEST_F(NonMutatingTest, SearchNWithPredicate) {
+ pw::containers::SearchN(span_, 3, 1, BinPredicate);
+}
diff --git a/pw_containers/docs.rst b/pw_containers/docs.rst
index 07093db78..0af2eda2c 100644
--- a/pw_containers/docs.rst
+++ b/pw_containers/docs.rst
@@ -44,65 +44,65 @@ whichever object is the current tail.
That means two key things:
- - An instantiated ``IntrusiveList<T>::Item`` must remain in scope for the
- lifetime of the ``IntrusiveList`` it has been added to.
- - A linked list item CANNOT be included in two lists. Attempting to do so
- results in an assert failure.
+- An instantiated ``IntrusiveList<T>::Item`` will be removed from its
+ corresponding ``IntrusiveList`` when it goes out of scope.
+- A linked list item CANNOT be included in two lists. Attempting to do so
+ results in an assert failure.
.. code-block:: cpp
- class Square
- : public pw::IntrusiveList<Square>::Item {
- public:
- Square(unsigned int side_length) : side_length(side_length) {}
- unsigned long Area() { return side_length * side_length; }
-
- private:
- unsigned int side_length;
- };
-
- pw::IntrusiveList<Square> squares;
-
- Square small(1);
- Square large(4000);
- // These elements are not copied into the linked list, the original objects
- // are just chained together and can be accessed via
- // `IntrusiveList<Square> squares`.
- squares.push_back(small);
- squares.push_back(large);
-
- {
- // When different_scope goes out of scope, it removes itself from the list.
- Square different_scope = Square(5);
- squares.push_back(&different_scope);
- }
-
- for (const auto& square : squares) {
- PW_LOG_INFO("Found a square with an area of %lu", square.Area());
- }
-
- // Like std::forward_list, an iterator is invalidated when the item it refers
- // to is removed. It is *NOT* safe to remove items from a list while iterating
- // over it in a range-based for loop.
- for (const auto& square_bad_example : squares) {
- if (square_bad_example.verticies() != 4) {
- // BAD EXAMPLE of how to remove matching items from a singly linked list.
- squares.remove(square_bad_example); // NEVER DO THIS! THIS IS A BUG!
- }
- }
-
- // To remove items while iterating, use an iterator to the previous item.
- auto previous = squares.before_begin();
- auto current = squares.begin();
-
- while (current != squares.end()) {
- if (current->verticies() != 4) {
- current = squares.erase_after(previous);
- } else {
- previous = current;
- ++current;
- }
- }
+ class Square
+ : public pw::IntrusiveList<Square>::Item {
+ public:
+ Square(unsigned int side_length) : side_length(side_length) {}
+ unsigned long Area() { return side_length * side_length; }
+
+ private:
+ unsigned int side_length;
+ };
+
+ pw::IntrusiveList<Square> squares;
+
+ Square small(1);
+ Square large(4000);
+ // These elements are not copied into the linked list, the original objects
+ // are just chained together and can be accessed via
+ // `IntrusiveList<Square> squares`.
+ squares.push_back(small);
+ squares.push_back(large);
+
+ {
+ // When different_scope goes out of scope, it removes itself from the list.
+ Square different_scope = Square(5);
+ squares.push_back(&different_scope);
+ }
+
+ for (const auto& square : squares) {
+ PW_LOG_INFO("Found a square with an area of %lu", square.Area());
+ }
+
+ // Like std::forward_list, an iterator is invalidated when the item it refers
+ // to is removed. It is *NOT* safe to remove items from a list while iterating
+ // over it in a range-based for loop.
+ for (const auto& square_bad_example : squares) {
+ if (square_bad_example.verticies() != 4) {
+ // BAD EXAMPLE of how to remove matching items from a singly linked list.
+ squares.remove(square_bad_example); // NEVER DO THIS! THIS IS A BUG!
+ }
+ }
+
+ // To remove items while iterating, use an iterator to the previous item.
+ auto previous = squares.before_begin();
+ auto current = squares.begin();
+
+ while (current != squares.end()) {
+ if (current->verticies() != 4) {
+ current = squares.erase_after(previous);
+ } else {
+ previous = current;
+ ++current;
+ }
+ }
pw::containers::FlatMap
=======================
@@ -125,11 +125,11 @@ a lambda or class that implements ``operator()`` for the container's value type.
.. code-block:: cpp
- std::array<int, 99> kNumbers = {3, 1, 4, 1, ...};
+ std::array<int, 99> kNumbers = {3, 1, 4, 1, ...};
- for (int even : FilteredView(kNumbers, [](int n) { return n % 2 == 0; })) {
- PW_LOG_INFO("This number is even: %d", even);
- }
+ for (int even : FilteredView(kNumbers, [](int n) { return n % 2 == 0; })) {
+ PW_LOG_INFO("This number is even: %d", even);
+ }
pw::containers::WrappedIterator
===============================
@@ -147,24 +147,24 @@ values in an array by 2.
.. code-block:: cpp
- // Divides values in a std::array by two.
- class DoubleIterator
- : public pw::containers::WrappedIterator<DoubleIterator, const int*, int> {
- public:
- constexpr DoubleIterator(const int* it) : WrappedIterator(it) {}
+ // Divides values in a std::array by two.
+ class DoubleIterator
+ : public pw::containers::WrappedIterator<DoubleIterator, const int*, int> {
+ public:
+ constexpr DoubleIterator(const int* it) : WrappedIterator(it) {}
- int operator*() const { return value() * 2; }
+ int operator*() const { return value() * 2; }
- // Don't define operator-> since this iterator returns by value.
- };
+ // Don't define operator-> since this iterator returns by value.
+ };
- constexpr std::array<int, 6> kArray{0, 1, 2, 3, 4, 5};
+ constexpr std::array<int, 6> kArray{0, 1, 2, 3, 4, 5};
- void SomeFunction {
- for (DoubleIterator it(kArray.begin()); it != DoubleIterator(kArray.end()); ++it) {
- // The iterator yields 0, 2, 4, 6, 8, 10 instead of the original values.
- }
- };
+ void SomeFunction {
+ for (DoubleIterator it(kArray.begin()); it != DoubleIterator(kArray.end()); ++it) {
+ // The iterator yields 0, 2, 4, 6, 8, 10 instead of the original values.
+ }
+ };
``WrappedIterator`` may be used in concert with ``FilteredView`` to create a
view that iterates over a matching values in a container and applies a
@@ -185,15 +185,135 @@ pw::containers::to_array
========================
``pw::containers::to_array`` is a C++14-compatible implementation of C++20's
`std::to_array <https://en.cppreference.com/w/cpp/container/array/to_array>`_.
-It converts a C array to a ``std::array``.
+In C++20, it is an alias for ``std::to_array``. It converts a C array to a
+``std::array``.
+
+pw_containers/algorithm.h
+=========================
+Pigweed provides a set of Container-based versions of algorithmic functions
+within the C++ standard library, based on a subset of
+``absl/algorithm/container.h``.
+
+.. cpp:function:: bool pw::containers::AllOf()
+
+ Container-based version of the <algorithm> ``std::all_of()`` function to
+ test if all elements within a container satisfy a condition.
+
+
+.. cpp:function:: bool pw::containers::AnyOf()
+
+ Container-based version of the <algorithm> ``std::any_of()`` function to
+ test if any element in a container fulfills a condition.
+
+
+.. cpp:function:: bool pw::containers::NoneOf()
+
+ Container-based version of the <algorithm> ``std::none_of()`` function to
+ test if no elements in a container fulfill a condition.
+
+
+.. cpp:function:: pw::containers::ForEach()
+
+ Container-based version of the <algorithm> ``std::for_each()`` function to
+ apply a function to a container's elements.
+
+
+.. cpp:function:: pw::containers::Find()
+
+ Container-based version of the <algorithm> ``std::find()`` function to find
+ the first element containing the passed value within a container value.
+
+
+.. cpp:function:: pw::containers::FindIf()
+
+ Container-based version of the <algorithm> ``std::find_if()`` function to find
+ the first element in a container matching the given condition.
+
+
+.. cpp:function:: pw::containers::FindIfNot()
+
+ Container-based version of the <algorithm> ``std::find_if_not()`` function to
+ find the first element in a container not matching the given condition.
+
+
+.. cpp:function:: pw::containers::FindEnd()
+
+ Container-based version of the <algorithm> ``std::find_end()`` function to
+ find the last subsequence within a container.
+
+
+.. cpp:function:: pw::containers::FindFirstOf()
+
+ Container-based version of the <algorithm> ``std::find_first_of()`` function
+ to find the first element within the container that is also within the options
+ container.
+
+
+.. cpp:function:: pw::containers::AdjacentFind()
+
+ Container-based version of the <algorithm> ``std::adjacent_find()`` function
+ to find equal adjacent elements within a container.
+
+
+.. cpp:function:: pw::containers::Count()
+
+ Container-based version of the <algorithm> ``std::count()`` function to count
+ values that match within a container.
+
+
+.. cpp:function:: pw::containers::CountIf()
+
+ Container-based version of the <algorithm> ``std::count_if()`` function to
+ count values matching a condition within a container.
+
+
+.. cpp:function:: pw::containers::Mismatch()
+
+ Container-based version of the <algorithm> ``std::mismatch()`` function to
+ return the first element where two ordered containers differ. Applies ``==``
+ to the first ``N`` elements of ``c1`` and ``c2``, where
+ ``N = min(size(c1), size(c2)).`` the function's test condition. Applies
+ ``pred`` to the first N elements of ``c1`` and ``c2``, where
+ ``N = min(size(c1), size(c2))``.
+
+
+.. cpp:function:: bool pw::containers::Equal()
+
+ Container-based version of the <algorithm> ``std::equal()`` function to
+ test whether two containers are equal.
+
+ .. note::
+
+ The semantics of ``Equal()`` are slightly different than those of
+ ``std::equal()``: while the latter iterates over the second container only
+ up to the size of the first container, ``Equal()`` also checks whether the
+ container sizes are equal. This better matches expectations about
+ ``Equal()`` based on its signature.
+
+.. cpp:function:: bool pw::containers::IsPermutation()
+
+ Container-based version of the <algorithm> ``std::is_permutation()`` function
+ to test whether a container is a permutation of another.
+
+
+.. cpp:function:: pw::containers::Search()
+
+ Container-based version of the <algorithm> ``std::search()`` function to
+ search a container for a subsequence.
+
+
+.. cpp:function:: pw::containers::SearchN()
+
+ Container-based version of the <algorithm> ``std::search_n()`` function to
+ search a container for the first sequence of N elements.
Compatibility
=============
-* C++17
+- C++17
Dependencies
============
-* ``pw_span``
+- :bdg-ref-primary-line:`module-pw_span`
Zephyr
======
diff --git a/pw_containers/filtered_view_test.cc b/pw_containers/filtered_view_test.cc
index 158398197..4d32a58ce 100644
--- a/pw_containers/filtered_view_test.cc
+++ b/pw_containers/filtered_view_test.cc
@@ -15,10 +15,10 @@
#include "pw_containers/filtered_view.h"
#include <array>
-#include <span>
#include "gtest/gtest.h"
#include "pw_containers/intrusive_list.h"
+#include "pw_span/span.h"
namespace pw::containers {
namespace {
@@ -69,7 +69,7 @@ TEST(FilteredView, EmptyContainer) {
IntrusiveList<Item> intrusive_list;
for (const Item& unused :
- FilteredView(nothing, [](const Item&) { return true; })) {
+ FilteredView(intrusive_list, [](const Item&) { return true; })) {
static_cast<void>(unused);
FAIL();
}
@@ -115,7 +115,7 @@ TEST(FilteredView, IntrusiveList_MatchNone) {
IntrusiveList<Item> intrusive_list({&item_1, &item_2, &item_3});
for (const Item& unused :
- FilteredView(kArray, [](const Item&) { return false; })) {
+ FilteredView(intrusive_list, [](const Item&) { return false; })) {
static_cast<void>(unused);
FAIL();
}
diff --git a/pw_containers/flat_map_test.cc b/pw_containers/flat_map_test.cc
index 51cf005d2..bd236bd4c 100644
--- a/pw_containers/flat_map_test.cc
+++ b/pw_containers/flat_map_test.cc
@@ -178,4 +178,13 @@ TEST(FlatMap, MapsWithUnsortedKeys) {
EXPECT_EQ(too_short.begin()->first, 0);
}
+TEST(FlatMap, DontDereferenceEnd) {
+ constexpr FlatMap<int, const char*, 2> unsorted_array({{
+ {2, "hello"},
+ {1, "goodbye"},
+ }});
+
+ EXPECT_EQ(unsorted_array.contains(3), false);
+}
+
} // namespace pw::containers
diff --git a/pw_containers/intrusive_list_test.cc b/pw_containers/intrusive_list_test.cc
index 2681cc0fb..060a56c91 100644
--- a/pw_containers/intrusive_list_test.cc
+++ b/pw_containers/intrusive_list_test.cc
@@ -19,6 +19,7 @@
#include <cstdint>
#include "gtest/gtest.h"
+#include "pw_compilation_testing/negative_compilation.h"
#include "pw_preprocessor/util.h"
namespace pw {
@@ -414,7 +415,8 @@ TEST(IntrusiveList, CompareConstAndNonConstIterator) {
EXPECT_EQ(list.end(), list.cend());
}
-#if defined(PW_COMPILE_FAIL_TEST_incompatible_iterator_types)
+#if PW_NC_TEST(IncompatibleIteratorTypes)
+PW_NC_EXPECT("comparison (of|between) distinct pointer types");
struct OtherItem : public IntrusiveList<OtherItem>::Item {};
@@ -424,11 +426,11 @@ TEST(IntrusiveList, CompareConstAndNonConstIterator_CompilationFails) {
static_cast<void>(list.end() == list2.end());
}
-#endif
+#endif // PW_NC_TEST
+
+#if PW_NC_TEST(CannotModifyThroughConstIterator)
+PW_NC_EXPECT("function is not marked const|discards qualifiers");
-// TODO(pwbug/47): These tests should fail to compile, enable when no-compile
-// tests are set up in Pigweed.
-#if defined(PW_COMPILE_FAIL_TEST_cannot_modify_through_const_iterator)
TEST(IntrusiveList, ConstIteratorModify) {
TestItem item1(1);
TestItem item2(99);
@@ -445,9 +447,9 @@ TEST(IntrusiveList, ConstIteratorModify) {
it++;
}
}
-#endif // Compile failure test
+#endif // PW_NC_TEST
-// TODO(pwbug/88): These tests should trigger a CHECK failure. This requires
+// TODO(b/235289499): These tests should trigger a CHECK failure. This requires
// using a testing version of pw_assert.
#define TESTING_CHECK_FAILURES_IS_SUPPORTED 0
#if TESTING_CHECK_FAILURES_IS_SUPPORTED
@@ -655,7 +657,7 @@ TEST(IntrusiveList, SizeScoped) {
// Test that a list of items derived from a different Item class can be created.
class DerivedTestItem : public TestItem {};
-TEST(InstrusiveList, AddItemsOfDerivedClassToList) {
+TEST(IntrusiveList, AddItemsOfDerivedClassToList) {
IntrusiveList<TestItem> list;
DerivedTestItem item1;
@@ -667,7 +669,7 @@ TEST(InstrusiveList, AddItemsOfDerivedClassToList) {
EXPECT_EQ(2u, list.size());
}
-TEST(InstrusiveList, ListOfDerivedClassItems) {
+TEST(IntrusiveList, ListOfDerivedClassItems) {
IntrusiveList<DerivedTestItem> derived_from_compatible_item_type;
DerivedTestItem item1;
@@ -675,14 +677,17 @@ TEST(InstrusiveList, ListOfDerivedClassItems) {
EXPECT_EQ(1u, derived_from_compatible_item_type.size());
-// TODO(pwbug/47): Make these proper automated compilation failure tests.
-#if defined(PW_COMPILE_FAIL_TEST_cannot_add_base_class_to_derived_class_list)
+#if PW_NC_TEST(CannotAddBaseClassToDerivedClassList)
+ PW_NC_EXPECT_CLANG("cannot bind to a value of unrelated type");
+ PW_NC_EXPECT_GCC("cannot convert");
+
TestItem item2;
derived_from_compatible_item_type.push_front(item2);
#endif
}
-#if defined(PW_COMPILE_FAIL_TEST_incompatibile_item_type)
+#if PW_NC_TEST(IncompatibileItemType)
+PW_NC_EXPECT("IntrusiveList items must be derived from IntrusiveList<T>::Item");
struct Foo {};
@@ -690,13 +695,14 @@ class BadItem : public IntrusiveList<Foo>::Item {};
[[maybe_unused]] IntrusiveList<BadItem> derived_from_incompatible_item_type;
-#elif defined(PW_COMPILE_FAIL_TEST_does_not_inherit_from_item)
+#elif PW_NC_TEST(DoesNotInheritFromItem)
+PW_NC_EXPECT("IntrusiveList items must be derived from IntrusiveList<T>::Item");
struct NotAnItem {};
[[maybe_unused]] IntrusiveList<NotAnItem> list;
-#endif
+#endif // PW_NC_TEST
} // namespace
} // namespace pw
diff --git a/pw_containers/public/pw_containers/algorithm.h b/pw_containers/public/pw_containers/algorithm.h
new file mode 100644
index 000000000..e8e81efcf
--- /dev/null
+++ b/pw_containers/public/pw_containers/algorithm.h
@@ -0,0 +1,366 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+//
+// -----------------------------------------------------------------------------
+// File: algorithm.h
+// -----------------------------------------------------------------------------
+//
+// This header file provides Container-based versions of algorithmic functions
+// within the C++ standard library, based on a subset of
+// absl/algorithm/container.h. The following standard library sets of functions
+// are covered within this file:
+//
+// * <algorithm> functions
+//
+// The standard library functions operate on iterator ranges; the functions
+// within this API operate on containers, though many return iterator ranges.
+//
+// All functions within this API are CamelCase instead of their std::
+// snake_case counterparts. Calls such as `pw::containers::Foo(container, ...)
+// are equivalent to std:: functions such as
+// `std::foo(std::begin(cont), std::end(cont), ...)`. Functions that act on
+// iterators but not conceptually on iterator ranges (e.g. `std::iter_swap`)
+// have no equivalent here.
+//
+// For template parameter and variable naming, `C` indicates the container type
+// to which the function is applied, `Pred` indicates the predicate object type
+// to be used by the function and `T` indicates the applicable element type.
+//
+// This was forked from
+// https://cs.opensource.google/abseil/abseil-cpp/+/main:absl/algorithm/algorithm.h;drc=12bc53e0318d80569270a5b26ccbc62b52022b89
+#pragma once
+
+#include <algorithm>
+#include <utility>
+
+#include "pw_containers/internal/algorithm_internal.h"
+
+namespace pw::containers {
+
+//------------------------------------------------------------------------------
+// <algorithm> Non-modifying sequence operations
+//------------------------------------------------------------------------------
+
+// AllOf()
+//
+// Container-based version of the <algorithm> `std::all_of()` function to
+// test if all elements within a container satisfy a condition.
+template <typename C, typename Pred>
+bool AllOf(const C& c, Pred&& pred) {
+ return std::all_of(std::begin(c), std::end(c), std::forward<Pred>(pred));
+}
+
+// AnyOf()
+//
+// Container-based version of the <algorithm> `std::any_of()` function to
+// test if any element in a container fulfills a condition.
+template <typename C, typename Pred>
+bool AnyOf(const C& c, Pred&& pred) {
+ return std::any_of(std::begin(c), std::end(c), std::forward<Pred>(pred));
+}
+
+// NoneOf()
+//
+// Container-based version of the <algorithm> `std::none_of()` function to
+// test if no elements in a container fulfill a condition.
+template <typename C, typename Pred>
+bool NoneOf(const C& c, Pred&& pred) {
+ return std::none_of(std::begin(c), std::end(c), std::forward<Pred>(pred));
+}
+
+// ForEach()
+//
+// Container-based version of the <algorithm> `std::for_each()` function to
+// apply a function to a container's elements.
+template <typename C, typename Function>
+std::decay_t<Function> ForEach(C&& c, Function&& f) {
+ return std::for_each(std::begin(c), std::end(c), std::forward<Function>(f));
+}
+
+// Find()
+//
+// Container-based version of the <algorithm> `std::find()` function to find
+// the first element containing the passed value within a container value.
+template <typename C, typename T>
+internal_algorithm::ContainerIter<C> Find(C& c, T&& value) {
+ return std::find(std::begin(c), std::end(c), std::forward<T>(value));
+}
+
+// FindIf()
+//
+// Container-based version of the <algorithm> `std::find_if()` function to find
+// the first element in a container matching the given condition.
+template <typename C, typename Pred>
+internal_algorithm::ContainerIter<C> FindIf(C& c, Pred&& pred) {
+ return std::find_if(std::begin(c), std::end(c), std::forward<Pred>(pred));
+}
+
+// FindIfNot()
+//
+// Container-based version of the <algorithm> `std::find_if_not()` function to
+// find the first element in a container not matching the given condition.
+template <typename C, typename Pred>
+internal_algorithm::ContainerIter<C> FindIfNot(C& c, Pred&& pred) {
+ return std::find_if_not(std::begin(c), std::end(c), std::forward<Pred>(pred));
+}
+
+// FindEnd()
+//
+// Container-based version of the <algorithm> `std::find_end()` function to
+// find the last subsequence within a container.
+template <typename Sequence1, typename Sequence2>
+internal_algorithm::ContainerIter<Sequence1> FindEnd(Sequence1& sequence,
+ Sequence2& subsequence) {
+ return std::find_end(std::begin(sequence),
+ std::end(sequence),
+ std::begin(subsequence),
+ std::end(subsequence));
+}
+
+// Overload of FindEnd() for using a predicate evaluation other than `==` as
+// the function's test condition.
+template <typename Sequence1, typename Sequence2, typename BinaryPredicate>
+internal_algorithm::ContainerIter<Sequence1> FindEnd(Sequence1& sequence,
+ Sequence2& subsequence,
+ BinaryPredicate&& pred) {
+ return std::find_end(std::begin(sequence),
+ std::end(sequence),
+ std::begin(subsequence),
+ std::end(subsequence),
+ std::forward<BinaryPredicate>(pred));
+}
+
+// FindFirstOf()
+//
+// Container-based version of the <algorithm> `std::find_first_of()` function to
+// find the first element within the container that is also within the options
+// container.
+template <typename C1, typename C2>
+internal_algorithm::ContainerIter<C1> FindFirstOf(C1& container, C2& options) {
+ return std::find_first_of(std::begin(container),
+ std::end(container),
+ std::begin(options),
+ std::end(options));
+}
+
+// Overload of FindFirstOf() for using a predicate evaluation other than
+// `==` as the function's test condition.
+template <typename C1, typename C2, typename BinaryPredicate>
+internal_algorithm::ContainerIter<C1> FindFirstOf(C1& container,
+ C2& options,
+ BinaryPredicate&& pred) {
+ return std::find_first_of(std::begin(container),
+ std::end(container),
+ std::begin(options),
+ std::end(options),
+ std::forward<BinaryPredicate>(pred));
+}
+
+// AdjacentFind()
+//
+// Container-based version of the <algorithm> `std::adjacent_find()` function to
+// find equal adjacent elements within a container.
+template <typename Sequence>
+internal_algorithm::ContainerIter<Sequence> AdjacentFind(Sequence& sequence) {
+ return std::adjacent_find(std::begin(sequence), std::end(sequence));
+}
+
+// Overload of AdjacentFind() for using a predicate evaluation other than
+// `==` as the function's test condition.
+template <typename Sequence, typename BinaryPredicate>
+internal_algorithm::ContainerIter<Sequence> AdjacentFind(
+ Sequence& sequence, BinaryPredicate&& pred) {
+ return std::adjacent_find(std::begin(sequence),
+ std::end(sequence),
+ std::forward<BinaryPredicate>(pred));
+}
+
+// Count()
+//
+// Container-based version of the <algorithm> `std::count()` function to count
+// values that match within a container.
+template <typename C, typename T>
+internal_algorithm::ContainerDifferenceType<const C> Count(const C& c,
+ T&& value) {
+ return std::count(std::begin(c), std::end(c), std::forward<T>(value));
+}
+
+// CountIOf()
+//
+// Container-based version of the <algorithm> `std::count_if()` function to
+// count values matching a condition within a container.
+template <typename C, typename Pred>
+internal_algorithm::ContainerDifferenceType<const C> CountIf(const C& c,
+ Pred&& pred) {
+ return std::count_if(std::begin(c), std::end(c), std::forward<Pred>(pred));
+}
+
+// Mismatch()
+//
+// Container-based version of the <algorithm> `std::mismatch()` function to
+// return the first element where two ordered containers differ. Applies `==` to
+// the first N elements of `c1` and `c2`, where N = min(size(c1), size(c2)).
+template <typename C1, typename C2>
+internal_algorithm::ContainerIterPairType<C1, C2> Mismatch(C1& c1, C2& c2) {
+ auto first1 = std::begin(c1);
+ auto last1 = std::end(c1);
+ auto first2 = std::begin(c2);
+ auto last2 = std::end(c2);
+
+ for (; first1 != last1 && first2 != last2; ++first1, (void)++first2) {
+ // Negates equality because Cpp17EqualityComparable doesn't require clients
+ // to overload both `operator==` and `operator!=`.
+ if (!(*first1 == *first2)) {
+ break;
+ }
+ }
+
+ return std::make_pair(first1, first2);
+}
+
+// Overload of Mismatch() for using a predicate evaluation other than `==` as
+// the function's test condition. Applies `pred`to the first N elements of `c1`
+// and `c2`, where N = min(size(c1), size(c2)).
+template <typename C1, typename C2, typename BinaryPredicate>
+internal_algorithm::ContainerIterPairType<C1, C2> Mismatch(
+ C1& c1, C2& c2, BinaryPredicate pred) {
+ auto first1 = std::begin(c1);
+ auto last1 = std::end(c1);
+ auto first2 = std::begin(c2);
+ auto last2 = std::end(c2);
+
+ for (; first1 != last1 && first2 != last2; ++first1, (void)++first2) {
+ if (!pred(*first1, *first2)) {
+ break;
+ }
+ }
+
+ return std::make_pair(first1, first2);
+}
+
+// Equal()
+//
+// Container-based version of the <algorithm> `std::equal()` function to
+// test whether two containers are equal.
+//
+// NOTE: the semantics of Equal() are slightly different than those of
+// equal(): while the latter iterates over the second container only up to the
+// size of the first container, Equal() also checks whether the container
+// sizes are equal. This better matches expectations about Equal() based on
+// its signature.
+//
+// Example:
+// vector v1 = <1, 2, 3>;
+// vector v2 = <1, 2, 3, 4>;
+// equal(std::begin(v1), std::end(v1), std::begin(v2)) returns true
+// Equal(v1, v2) returns false
+
+template <typename C1, typename C2>
+bool Equal(const C1& c1, const C2& c2) {
+ return ((std::size(c1) == std::size(c2)) &&
+ std::equal(std::begin(c1), std::end(c1), std::begin(c2)));
+}
+
+// Overload of Equal() for using a predicate evaluation other than `==` as
+// the function's test condition.
+template <typename C1, typename C2, typename BinaryPredicate>
+bool Equal(const C1& c1, const C2& c2, BinaryPredicate&& pred) {
+ return ((std::size(c1) == std::size(c2)) &&
+ std::equal(std::begin(c1),
+ std::end(c1),
+ std::begin(c2),
+ std::forward<BinaryPredicate>(pred)));
+}
+
+// IsPermutation()
+//
+// Container-based version of the <algorithm> `std::is_permutation()` function
+// to test whether a container is a permutation of another.
+template <typename C1, typename C2>
+bool IsPermutation(const C1& c1, const C2& c2) {
+ using std::begin;
+ using std::end;
+ return c1.size() == c2.size() &&
+ std::is_permutation(begin(c1), end(c1), begin(c2));
+}
+
+// Overload of IsPermutation() for using a predicate evaluation other than
+// `==` as the function's test condition.
+template <typename C1, typename C2, typename BinaryPredicate>
+bool IsPermutation(const C1& c1, const C2& c2, BinaryPredicate&& pred) {
+ using std::begin;
+ using std::end;
+ return c1.size() == c2.size() &&
+ std::is_permutation(begin(c1),
+ end(c1),
+ begin(c2),
+ std::forward<BinaryPredicate>(pred));
+}
+
+// Search()
+//
+// Container-based version of the <algorithm> `std::search()` function to search
+// a container for a subsequence.
+template <typename Sequence1, typename Sequence2>
+internal_algorithm::ContainerIter<Sequence1> Search(Sequence1& sequence,
+ Sequence2& subsequence) {
+ return std::search(std::begin(sequence),
+ std::end(sequence),
+ std::begin(subsequence),
+ std::end(subsequence));
+}
+
+// Overload of Search() for using a predicate evaluation other than
+// `==` as the function's test condition.
+template <typename Sequence1, typename Sequence2, typename BinaryPredicate>
+internal_algorithm::ContainerIter<Sequence1> Search(Sequence1& sequence,
+ Sequence2& subsequence,
+ BinaryPredicate&& pred) {
+ return std::search(std::begin(sequence),
+ std::end(sequence),
+ std::begin(subsequence),
+ std::end(subsequence),
+ std::forward<BinaryPredicate>(pred));
+}
+
+// SearchN()
+//
+// Container-based version of the <algorithm> `std::search_n()` function to
+// search a container for the first sequence of N elements.
+template <typename Sequence, typename Size, typename T>
+internal_algorithm::ContainerIter<Sequence> SearchN(Sequence& sequence,
+ Size count,
+ T&& value) {
+ return std::search_n(
+ std::begin(sequence), std::end(sequence), count, std::forward<T>(value));
+}
+
+// Overload of SearchN() for using a predicate evaluation other than
+// `==` as the function's test condition.
+template <typename Sequence,
+ typename Size,
+ typename T,
+ typename BinaryPredicate>
+internal_algorithm::ContainerIter<Sequence> SearchN(Sequence& sequence,
+ Size count,
+ T&& value,
+ BinaryPredicate&& pred) {
+ return std::search_n(std::begin(sequence),
+ std::end(sequence),
+ count,
+ std::forward<T>(value),
+ std::forward<BinaryPredicate>(pred));
+}
+
+} // namespace pw::containers
diff --git a/pw_containers/public/pw_containers/filtered_view.h b/pw_containers/public/pw_containers/filtered_view.h
index 7daf821f2..b1b499176 100644
--- a/pw_containers/public/pw_containers/filtered_view.h
+++ b/pw_containers/public/pw_containers/filtered_view.h
@@ -17,6 +17,7 @@
#include <iterator>
#include "pw_assert/assert.h"
+#include "pw_preprocessor/compiler.h"
namespace pw::containers {
@@ -160,6 +161,7 @@ FilteredView<Container, Filter>::iterator::operator--() {
}
PW_ASSERT(false);
+ PW_UNREACHABLE;
}
} // namespace pw::containers
diff --git a/pw_containers/public/pw_containers/flat_map.h b/pw_containers/public/pw_containers/flat_map.h
index b4c2ab537..7ce97cab0 100644
--- a/pw_containers/public/pw_containers/flat_map.h
+++ b/pw_containers/public/pw_containers/flat_map.h
@@ -21,9 +21,21 @@
namespace pw::containers {
+// Define and use a custom Pair object. This is because std::pair does not
+// support constexpr assignment until C++20. The assignment is needed since
+// the array of pairs will be sorted in the constructor (if not already).
+template <typename First, typename Second>
+struct Pair {
+ First first;
+ Second second;
+};
+
+template <typename T1, typename T2>
+Pair(T1, T2) -> Pair<T1, T2>;
+
// A simple, fixed-size associative array with lookup by key or value.
//
-// FlatMaps are initialized with a std::array of FlatMap::Pair objects:
+// FlatMaps are initialized with a std::array of Pair<K, V> objects:
// FlatMap<int, int> map({{{1, 2}, {3, 4}}});
//
// The keys do not need to be sorted as the constructor will sort the items
@@ -31,15 +43,6 @@ namespace pw::containers {
template <typename Key, typename Value, size_t kArraySize>
class FlatMap {
public:
- // Define and use a custom Pair object. This is because std::pair does not
- // support constexpr assignment until C++20. The assignment is needed since
- // the array of pairs will be sorted in the constructor (if not already).
- template <typename First, typename Second>
- struct Pair {
- First first;
- Second second;
- };
-
using key_type = Key;
using mapped_type = Value;
using value_type = Pair<key_type, mapped_type>;
@@ -73,7 +76,10 @@ class FlatMap {
}
const_iterator it = lower_bound(key);
- return key == it->first ? it : end();
+ if (it == end() || it->first != key) {
+ return end();
+ }
+ return it;
}
constexpr const_iterator lower_bound(const key_type& key) const {
diff --git a/pw_containers/public/pw_containers/internal/algorithm_internal.h b/pw_containers/public/pw_containers/internal/algorithm_internal.h
new file mode 100644
index 000000000..9d376e09a
--- /dev/null
+++ b/pw_containers/public/pw_containers/internal/algorithm_internal.h
@@ -0,0 +1,38 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstddef>
+#include <iterator>
+#include <utility>
+
+namespace pw::containers::internal_algorithm {
+
+// The type of the iterator given by begin(c) (possibly std::begin(c)).
+// ContainerIter<const vector<T>> gives vector<T>::const_iterator,
+// while ContainerIter<vector<T>> gives vector<T>::iterator.
+template <typename C>
+using ContainerIter = decltype(std::begin(std::declval<C&>()));
+
+// An MSVC bug involving template parameter substitution requires us to use
+// decltype() here instead of just std::pair.
+template <typename C1, typename C2>
+using ContainerIterPairType =
+ decltype(std::make_pair(ContainerIter<C1>(), ContainerIter<C2>()));
+
+template <typename C>
+using ContainerDifferenceType = decltype(std::distance(
+ std::declval<ContainerIter<C>>(), std::declval<ContainerIter<C>>()));
+
+} // namespace pw::containers::internal_algorithm
diff --git a/pw_containers/public/pw_containers/iterator.h b/pw_containers/public/pw_containers/iterator.h
new file mode 100644
index 000000000..d9a8f7296
--- /dev/null
+++ b/pw_containers/public/pw_containers/iterator.h
@@ -0,0 +1,34 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <iterator>
+
+#include "pw_polyfill/standard.h"
+
+namespace pw::containers {
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(20)
+
+// If std::contiguous_iterator_tag exists, use it directly.
+using std::contiguous_iterator_tag;
+
+#else
+
+// If std::contiguous_iterator_tag does not exist, define a stand-in type.
+struct contiguous_iterator_tag : public std::random_access_iterator_tag {};
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(20)
+
+} // namespace pw::containers
diff --git a/pw_containers/public/pw_containers/to_array.h b/pw_containers/public/pw_containers/to_array.h
index 63a107fd6..fa02c274b 100644
--- a/pw_containers/public/pw_containers/to_array.h
+++ b/pw_containers/public/pw_containers/to_array.h
@@ -20,6 +20,14 @@
namespace pw {
namespace containers {
+
+// If std::to_array is available, use it as pw::containers::to_array.
+#ifdef __cpp_lib_to_array
+
+using std::to_array;
+
+#else
+
namespace impl {
template <typename T, size_t kSize, size_t... kIndices>
@@ -36,8 +44,8 @@ constexpr std::array<std::remove_cv_t<T>, kSize> MoveArray(
} // namespace impl
-// pw::containers::to_array is C++14-compatible implementation of C++20's
-// std::to_array.
+// If C++20's std::to_array is not available, implement pw::containers::to_array
+// in a C++14-compatible way.
template <typename T, size_t kSize>
constexpr std::array<std::remove_cv_t<T>, kSize> to_array(T (&values)[kSize]) {
return impl::CopyArray(values, std::make_index_sequence<kSize>{});
@@ -48,5 +56,7 @@ constexpr std::array<std::remove_cv_t<T>, kSize> to_array(T (&&values)[kSize]) {
return impl::MoveArray(std::move(values), std::make_index_sequence<kSize>{});
}
+#endif // __cpp_lib_to_array
+
} // namespace containers
} // namespace pw
diff --git a/pw_containers/public/pw_containers/vector.h b/pw_containers/public/pw_containers/vector.h
index 34380749e..62ebc8aa1 100644
--- a/pw_containers/public/pw_containers/vector.h
+++ b/pw_containers/public/pw_containers/vector.h
@@ -20,6 +20,7 @@
#include <iterator>
#include <limits>
#include <new>
+#include <string_view>
#include <type_traits>
#include <utility>
@@ -113,6 +114,18 @@ class Vector : public Vector<T, vector_impl::kGeneric> {
Vector(std::initializer_list<T> list)
: Vector<T, vector_impl::kGeneric>(kMaxSize, list) {}
+ static constexpr size_t max_size() { return kMaxSize; }
+
+ // Construct from std::string_view when T is char.
+ template <typename U = T,
+ typename = std::enable_if_t<std::is_same_v<U, char>>>
+ Vector(std::string_view source) : Vector(source.begin(), source.end()) {}
+
+ // Construct from const char* when T is char.
+ template <typename U = T,
+ typename = std::enable_if_t<std::is_same_v<U, char>>>
+ Vector(const char* source) : Vector(std::string_view(source)) {}
+
Vector& operator=(const Vector& other) {
Vector<T>::assign(other.begin(), other.end());
return *this;
@@ -314,26 +327,28 @@ class Vector<T, vector_impl::kGeneric>
void clear() noexcept;
- // TODO(hepler): insert, emplace, and erase are not yet implemented.
- // Currently, items can only be added to or removed from the end.
- iterator insert(const_iterator index, const T& value);
+ iterator insert(const_iterator index, size_type count, const T& value);
- iterator insert(const_iterator index, T&& value);
+ iterator insert(const_iterator index, const T& value) {
+ return insert(index, 1, value);
+ }
- iterator insert(const_iterator index, size_type count, const T& value);
+ iterator insert(const_iterator index, T&& value);
template <typename Iterator>
iterator insert(const_iterator index, Iterator first, Iterator last);
- iterator insert(const_iterator index, std::initializer_list<T> list);
+ iterator insert(const_iterator index, std::initializer_list<T> list) {
+ return insert(index, list.begin(), list.end());
+ }
template <typename... Args>
iterator emplace(const_iterator index, Args&&... args);
- iterator erase(const_iterator index);
-
iterator erase(const_iterator first, const_iterator last);
+ iterator erase(const_iterator index) { return erase(index, index + 1); }
+
void push_back(const T& value) { emplace_back(value); }
void push_back(T&& value) { emplace_back(std::move(value)); }
@@ -433,8 +448,10 @@ bool operator>=(const Vector<T, kLhsSize>& lhs,
template <typename T>
void Vector<T, vector_impl::kGeneric>::clear() noexcept {
- for (auto& item : *this) {
- item.~T();
+ if constexpr (!std::is_trivially_destructible_v<value_type>) {
+ for (auto& item : *this) {
+ item.~T();
+ }
}
size_ = 0;
}
@@ -451,7 +468,9 @@ void Vector<T, vector_impl::kGeneric>::emplace_back(Args&&... args) {
template <typename T>
void Vector<T, vector_impl::kGeneric>::pop_back() {
if (!empty()) {
- back().~T();
+ if constexpr (!std::is_trivially_destructible_v<value_type>) {
+ back().~T();
+ }
size_ -= 1;
}
}
@@ -469,6 +488,110 @@ void Vector<T, vector_impl::kGeneric>::resize(size_type new_size,
}
template <typename T>
+typename Vector<T>::iterator Vector<T>::insert(Vector<T>::const_iterator index,
+ T&& value) {
+ PW_DASSERT(index >= cbegin());
+ PW_DASSERT(index <= cend());
+ PW_DASSERT(!full());
+
+ iterator insertion_point = begin() + std::distance(cbegin(), index);
+ if (insertion_point == end()) {
+ emplace_back(std::move(value));
+ return insertion_point;
+ }
+
+ std::move_backward(insertion_point, end(), end() + 1);
+ *insertion_point = std::move(value);
+ ++size_;
+
+ // Return an iterator pointing to the inserted value.
+ return insertion_point;
+}
+
+template <typename T>
+template <typename Iterator>
+typename Vector<T>::iterator Vector<T>::insert(Vector<T>::const_iterator index,
+ Iterator first,
+ Iterator last) {
+ PW_DASSERT(index >= cbegin());
+ PW_DASSERT(index <= cend());
+ PW_DASSERT(!full());
+
+ iterator insertion_point = begin() + std::distance(cbegin(), index);
+
+ const size_t insertion_count = std::distance(first, last);
+ if (insertion_count == 0) {
+ return insertion_point;
+ }
+ PW_DASSERT(size() + insertion_count <= max_size());
+
+ iterator return_value = insertion_point;
+
+ if (insertion_point != end()) {
+ std::move_backward(insertion_point, end(), end() + insertion_count);
+ }
+
+ while (first != last) {
+ *insertion_point = *first;
+ ++first;
+ ++insertion_point;
+ }
+ size_ += insertion_count;
+
+ // Return an iterator pointing to the first element inserted.
+ return return_value;
+}
+
+template <typename T>
+typename Vector<T>::iterator Vector<T>::insert(Vector<T>::const_iterator index,
+ size_type count,
+ const T& value) {
+ PW_DASSERT(index >= cbegin());
+ PW_DASSERT(index <= cend());
+ PW_DASSERT(size() + count <= max_size());
+
+ iterator insertion_point = begin() + std::distance(cbegin(), index);
+ if (count == size_type{}) {
+ return insertion_point;
+ }
+
+ if (insertion_point != end()) {
+ std::move_backward(insertion_point, end(), end() + count);
+ }
+
+ iterator return_value = insertion_point;
+
+ for (size_type final_count = size_ + count; size_ != final_count; ++size_) {
+ *insertion_point = value;
+ ++insertion_point;
+ }
+
+ // Return an iterator pointing to the first element inserted.
+ return return_value;
+}
+
+template <typename T>
+typename Vector<T>::iterator Vector<T>::erase(Vector<T>::const_iterator first,
+ Vector<T>::const_iterator last) {
+ iterator source = begin() + std::distance(cbegin(), last);
+ if (first == last) {
+ return source;
+ }
+
+ if constexpr (!std::is_trivially_destructible_v<T>) {
+ std::destroy(first, last);
+ }
+
+ iterator destination = begin() + std::distance(cbegin(), first);
+ iterator new_end = std::move(source, end(), destination);
+
+ size_ = std::distance(begin(), new_end);
+
+ // Return an iterator following the last removed element.
+ return new_end;
+}
+
+template <typename T>
template <typename Iterator>
void Vector<T, vector_impl::kGeneric>::CopyFrom(Iterator first, Iterator last) {
while (first != last) {
diff --git a/pw_containers/size_report/BUILD.bazel b/pw_containers/size_report/BUILD.bazel
new file mode 100644
index 000000000..6c64f609b
--- /dev/null
+++ b/pw_containers/size_report/BUILD.bazel
@@ -0,0 +1,93 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_binary",
+ "pw_cc_library",
+)
+
+pw_cc_library(
+ name = "base_lib",
+ hdrs = ["base.h"],
+)
+
+pw_cc_binary(
+ name = "base",
+ srcs = ["base.cc"],
+ deps = [
+ ":base_lib",
+ "//pw_containers",
+ ],
+)
+
+pw_cc_binary(
+ name = "linked_list_one_item",
+ srcs = ["linked_list.cc"],
+ defines = ["ITEM_COUNT=1"],
+ deps = [
+ ":base_lib",
+ "//pw_containers",
+ ],
+)
+
+pw_cc_binary(
+ name = "linked_list_two_item",
+ srcs = ["linked_list.cc"],
+ defines = ["ITEM_COUNT=2"],
+ deps = [
+ ":base_lib",
+ "//pw_containers",
+ ],
+)
+
+pw_cc_binary(
+ name = "linked_list_four_item",
+ srcs = ["linked_list.cc"],
+ defines = ["ITEM_COUNT=4"],
+ deps = [
+ ":base_lib",
+ "//pw_containers",
+ ],
+)
+
+pw_cc_binary(
+ name = "intrusive_list_one_item",
+ srcs = ["intrusive_list.cc"],
+ defines = ["ITEM_COUNT=1"],
+ deps = [
+ ":base_lib",
+ "//pw_containers",
+ ],
+)
+
+pw_cc_binary(
+ name = "intrusive_list_two_item",
+ srcs = ["intrusive_list.cc"],
+ defines = ["ITEM_COUNT=2"],
+ deps = [
+ ":base_lib",
+ "//pw_containers",
+ ],
+)
+
+pw_cc_binary(
+ name = "intrusive_list_four_item",
+ srcs = ["intrusive_list.cc"],
+ defines = ["ITEM_COUNT=4"],
+ deps = [
+ ":base_lib",
+ "//pw_containers",
+ ],
+)
diff --git a/pw_containers/size_report/BUILD.gn b/pw_containers/size_report/BUILD.gn
new file mode 100644
index 000000000..e0c451eb6
--- /dev/null
+++ b/pw_containers/size_report/BUILD.gn
@@ -0,0 +1,80 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/target_types.gni")
+
+pw_source_set("base_lib") {
+ public = [ "base.h" ]
+ public_deps = [ dir_pw_containers ]
+}
+
+pw_executable("base") {
+ sources = [ "base.cc" ]
+ deps = [ ":base_lib" ]
+}
+
+pw_executable("linked_list_one_item") {
+ sources = [ "linked_list.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+ defines = [ "ITEM_COUNT=1" ]
+}
+
+pw_executable("linked_list_two_item") {
+ sources = [ "linked_list.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+ defines = [ "ITEM_COUNT=2" ]
+}
+
+pw_executable("linked_list_four_item") {
+ sources = [ "linked_list.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+ defines = [ "ITEM_COUNT=4" ]
+}
+
+pw_executable("intrusive_list_one_item") {
+ sources = [ "intrusive_list.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+ defines = [ "ITEM_COUNT=1" ]
+}
+
+pw_executable("intrusive_list_two_item") {
+ sources = [ "intrusive_list.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+ defines = [ "ITEM_COUNT=2" ]
+}
+
+pw_executable("intrusive_list_four_item") {
+ sources = [ "intrusive_list.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+ defines = [ "ITEM_COUNT=4" ]
+}
diff --git a/pw_containers/size_report/base.cc b/pw_containers/size_report/base.cc
new file mode 100644
index 000000000..5082e9df1
--- /dev/null
+++ b/pw_containers/size_report/base.cc
@@ -0,0 +1,19 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "base.h"
+
+static BaseContainer size_report_data;
+
+int main() { return size_report_data.LoadData(); }
diff --git a/pw_containers/size_report/base.h b/pw_containers/size_report/base.h
new file mode 100644
index 000000000..67b117bae
--- /dev/null
+++ b/pw_containers/size_report/base.h
@@ -0,0 +1,62 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_containers/intrusive_list.h"
+
+//
+// Baseline data container structure.
+//
+// This structure is a container for data used to establish a baseline for size
+// report diffs.
+//
+// The baseline contains one global instance of this structure. Other binaries
+// should contain one global instance of a subclass of this structure, adding
+// any data that should be a part of the size report diff.
+//
+// This structure may be used to load sections of code and data in order to
+// establish a basline for size report diffs. This structure causes sections to
+// be loaded by referencing symbols within those sections.
+//
+// Simple references to symbols can be optimized out if they don't have any
+// external effects. The LoadData method uses functions and other symbols to
+// generate and return a run-time value. In this way, the symbol references
+// cannot be optimized out.
+//
+struct BaseContainer {
+ class BaseItem : public pw::IntrusiveList<BaseItem>::Item {};
+
+ BaseContainer() {
+ static BaseItem item;
+ GetList().push_front(item);
+ }
+
+ //
+ // Causes code and data sections to be loaded. Returns a generated value
+ // based on symbols in those sections to force them to be loaded. The caller
+ // should ensure that the return value has some external effect (e.g.,
+ // returning the value from the "main" function).
+ //
+ long LoadData() { return reinterpret_cast<long>(this) ^ GetList().size(); }
+
+ static pw::IntrusiveList<BaseItem>& GetList() {
+ static pw::IntrusiveList<BaseItem> list;
+ return list;
+ }
+
+ // Explicit padding. If this structure is empty, the baseline will include
+ // padding that won't appear in subclasses, so the padding is explicitly
+ // added here so it appears in both the baseline and all subclasses.
+ char padding[8];
+};
diff --git a/pw_containers/size_report/intrusive_list.cc b/pw_containers/size_report/intrusive_list.cc
new file mode 100644
index 000000000..897f267a6
--- /dev/null
+++ b/pw_containers/size_report/intrusive_list.cc
@@ -0,0 +1,38 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "base.h"
+
+#ifndef ITEM_COUNT
+#define ITEM_COUNT 1
+#endif
+
+struct ExampleItem : public pw::IntrusiveList<ExampleItem>::Item {};
+
+struct ExampleContainer {
+ pw::IntrusiveList<ExampleItem> item_list;
+};
+
+static struct IntrusiveListContainer : BaseContainer {
+ ExampleContainer example_container;
+ ExampleItem example_item[ITEM_COUNT];
+} size_report_data;
+
+int main() {
+ for (auto& example_item : size_report_data.example_item) {
+ size_report_data.example_container.item_list.push_front(example_item);
+ }
+
+ return size_report_data.LoadData();
+}
diff --git a/pw_containers/size_report/linked_list.cc b/pw_containers/size_report/linked_list.cc
new file mode 100644
index 000000000..7dcf4b6be
--- /dev/null
+++ b/pw_containers/size_report/linked_list.cc
@@ -0,0 +1,45 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "base.h"
+
+#ifndef ITEM_COUNT
+#define ITEM_COUNT 1
+#endif
+
+struct ExampleItem {
+ ExampleItem* next;
+};
+
+struct ExampleContainer {
+ void AddItem(ExampleItem* example_item) {
+ example_item->next = item_list;
+ item_list = example_item;
+ }
+
+ ExampleItem* item_list = nullptr;
+};
+
+static struct LinkedListContainer : BaseContainer {
+ ExampleContainer example_container;
+ ExampleItem example_item[ITEM_COUNT];
+} size_report_data;
+
+int main() {
+ for (auto& example_item : size_report_data.example_item) {
+ size_report_data.example_container.AddItem(&example_item);
+ }
+
+ return size_report_data.LoadData();
+}
diff --git a/pw_containers/to_array_test.cc b/pw_containers/to_array_test.cc
index 783b0787e..3010c07f2 100644
--- a/pw_containers/to_array_test.cc
+++ b/pw_containers/to_array_test.cc
@@ -14,9 +14,12 @@
#include "pw_containers/to_array.h"
+#include <cstring>
+
#include "gtest/gtest.h"
-namespace pw::containers {
+namespace pw {
+namespace containers {
namespace {
TEST(Array, ToArray_StringLiteral) {
@@ -26,7 +29,7 @@ TEST(Array, ToArray_StringLiteral) {
TEST(Array, ToArray_Inline) {
constexpr std::array<int, 3> kArray = to_array({1, 2, 3});
- static_assert(kArray.size() == 3);
+ static_assert(kArray.size() == 3, "Size should be 3 as initialized");
EXPECT_EQ(kArray[0], 1);
}
@@ -56,4 +59,5 @@ TEST(Array, ToArray_MoveOnly) {
}
} // namespace
-} // namespace pw::containers
+} // namespace containers
+} // namespace pw
diff --git a/pw_containers/vector_test.cc b/pw_containers/vector_test.cc
index 30d5e173b..4bde24758 100644
--- a/pw_containers/vector_test.cc
+++ b/pw_containers/vector_test.cc
@@ -21,6 +21,8 @@
namespace pw {
namespace {
+using namespace std::literals::string_view_literals;
+
// Since pw::Vector<T, N> downcasts to a pw::Vector<T, 0>, ensure that the
// alignment doesn't change.
static_assert(alignof(Vector<std::max_align_t, 0>) ==
@@ -74,6 +76,19 @@ struct Counter {
moved += 1;
}
+ Counter& operator=(const Counter& other) {
+ value = other.value;
+ created += 1;
+ return *this;
+ }
+
+ Counter& operator=(Counter&& other) {
+ value = other.value;
+ other.value = 0;
+ moved += 1;
+ return *this;
+ }
+
~Counter() { destroyed += 1; }
int value;
@@ -158,6 +173,61 @@ TEST(Vector, Construct_InitializerList) {
EXPECT_EQ(vector[1], 200);
}
+struct Aggregate {
+ int integer;
+ Vector<char, 8> vector;
+};
+
+TEST(Vector, Construct_String) {
+ const auto vector = Vector<char, 8>{"Hello"};
+ EXPECT_EQ(5u, vector.size());
+ EXPECT_EQ("Hello"sv, std::string_view(vector.data(), vector.size()));
+}
+
+TEST(Vector, Construct_StringTruncates) {
+ const auto vector = Vector<char, 8>{"Hello from a long string"};
+ EXPECT_EQ(8u, vector.size());
+ EXPECT_EQ("Hello fr"sv, std::string_view(vector.data(), vector.size()));
+}
+
+TEST(Vector, Construct_AssignFromString) {
+ Vector<char, 8> vector = "Hello";
+ EXPECT_EQ(5u, vector.size());
+ EXPECT_EQ("Hello"sv, std::string_view(vector.data(), vector.size()));
+}
+
+TEST(Vector, Construct_AggregateString) {
+ Aggregate aggregate = {.integer = 42, .vector = "Hello"};
+ EXPECT_EQ(5u, aggregate.vector.size());
+ EXPECT_EQ("Hello"sv,
+ std::string_view(aggregate.vector.data(), aggregate.vector.size()));
+}
+
+TEST(Vector, Construct_StringView) {
+ const auto vector = Vector<char, 8>{"Hello"sv};
+ EXPECT_EQ(5u, vector.size());
+ EXPECT_EQ("Hello"sv, std::string_view(vector.data(), vector.size()));
+}
+
+TEST(Vector, Construct_StringViewTruncates) {
+ const auto vector = Vector<char, 8>{"Hello from a long string"sv};
+ EXPECT_EQ(8u, vector.size());
+ EXPECT_EQ("Hello fr"sv, std::string_view(vector.data(), vector.size()));
+}
+
+TEST(Vector, Construct_AssignFromStringView) {
+ Vector<char, 8> vector = "Hello"sv;
+ EXPECT_EQ(5u, vector.size());
+ EXPECT_EQ("Hello"sv, std::string_view(vector.data(), vector.size()));
+}
+
+TEST(Vector, Construct_AggregateStringView) {
+ Aggregate aggregate = {.integer = 42, .vector = "Hello"sv};
+ EXPECT_EQ(5u, aggregate.vector.size());
+ EXPECT_EQ("Hello"sv,
+ std::string_view(aggregate.vector.data(), aggregate.vector.size()));
+}
+
TEST(Vector, Destruct_ZeroLength) {
Counter::Reset();
@@ -410,6 +480,212 @@ TEST(Vector, Modify_Resize_Zero) {
EXPECT_EQ(vector.size(), 0u);
}
+TEST(Vector, Modify_Erase_TrivialRangeBegin) {
+ Vector<size_t, 10> vector;
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ vector.emplace_back(i);
+ }
+
+ vector.erase(vector.begin() + 2, vector.end());
+ EXPECT_EQ(vector.size(), 2u);
+
+ for (size_t i = 0; i < vector.size(); ++i) {
+ EXPECT_EQ(vector[i], i);
+ }
+}
+
+TEST(Vector, Modify_Erase_TrivialRangeEnd) {
+ Vector<size_t, 10> vector;
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ vector.emplace_back(i);
+ }
+
+ vector.erase(vector.begin(), vector.end() - 2);
+ EXPECT_EQ(vector.size(), 2u);
+
+ for (size_t i = 0; i < vector.size(); ++i) {
+ EXPECT_EQ(vector[i], 8u + i);
+ }
+}
+
+TEST(Vector, Modify_Erase_TrivialSingleItem) {
+ Vector<size_t, 10> vector;
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ vector.emplace_back(i);
+ }
+
+ vector.erase(vector.begin() + 9);
+
+ EXPECT_EQ(vector.size(), 9u);
+ EXPECT_EQ(vector.at(8), 8u);
+ EXPECT_EQ(vector.at(0), 0u);
+
+ vector.erase(vector.begin());
+ EXPECT_EQ(vector.size(), 8u);
+ EXPECT_EQ(vector.at(0), 1u);
+}
+
+TEST(Vector, Modify_Erase_NonTrivialRangeBegin) {
+ Counter::Reset();
+ Vector<Counter, 10> vector;
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ vector.emplace_back(static_cast<int>(i));
+ }
+
+ for (size_t i = 0; i < vector.size(); ++i) {
+ EXPECT_EQ(vector[i].value, static_cast<int>(i));
+ }
+
+ vector.erase(vector.begin() + 2, vector.end());
+ EXPECT_EQ(vector.size(), 2u);
+
+ for (size_t i = 0; i < vector.size(); ++i) {
+ EXPECT_EQ(vector[i].value, static_cast<int>(i));
+ }
+
+ EXPECT_EQ(Counter::destroyed, 8);
+ EXPECT_EQ(Counter::moved, 0);
+}
+
+TEST(Vector, Modify_Erase_NonTrivialRangeEnd) {
+ Counter::Reset();
+ Vector<Counter, 10> vector;
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ vector.emplace_back(static_cast<int>(i));
+ }
+
+ vector.erase(vector.begin(), vector.end() - 2);
+ EXPECT_EQ(vector.size(), 2u);
+
+ for (size_t i = 0; i < vector.size(); ++i) {
+ EXPECT_EQ(vector[i].value, static_cast<int>(8u + i));
+ }
+
+ EXPECT_EQ(Counter::destroyed, 8);
+ EXPECT_EQ(Counter::moved, 2);
+}
+
+TEST(Vector, Modify_Erase_None) {
+ Counter::Reset();
+ Vector<Counter, 10> vector;
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ vector.emplace_back(static_cast<int>(i));
+ }
+
+ vector.erase(vector.begin() + 2, vector.begin() + 2);
+ EXPECT_EQ(vector.size(), 10u);
+
+ EXPECT_EQ(Counter::destroyed, 0);
+ EXPECT_EQ(Counter::moved, 0);
+ EXPECT_EQ(Counter::created, 10);
+}
+
+TEST(Vector, Modify_Insert_End) {
+ Counter::Reset();
+ Vector<Counter, 10> vector;
+
+ for (size_t i = 0; i < 8u; ++i) {
+ vector.emplace_back(static_cast<int>(i));
+ }
+
+ EXPECT_EQ(vector.size(), 8u);
+ EXPECT_EQ(Counter::created, 8);
+
+ decltype(vector)::iterator it = vector.insert(vector.cend(), 8);
+ EXPECT_EQ(it->value, 8);
+ EXPECT_EQ(vector.at(8).value, 8);
+
+ EXPECT_EQ(Counter::destroyed, 1);
+ EXPECT_EQ(Counter::moved, 1);
+ EXPECT_EQ(Counter::created, 9);
+}
+
+TEST(Vector, Modify_Insert_Begin) {
+ Counter::Reset();
+ Vector<Counter, 10> vector;
+
+ for (size_t i = 0; i < 8u; ++i) {
+ vector.emplace_back(static_cast<int>(i));
+ }
+
+ EXPECT_EQ(vector.size(), 8u);
+
+ decltype(vector)::iterator it = vector.insert(vector.cbegin(), 123);
+ EXPECT_EQ(it->value, 123);
+ EXPECT_EQ(vector.at(0).value, 123);
+ EXPECT_EQ(vector.at(8).value, 7);
+
+ EXPECT_EQ(Counter::moved, 9);
+}
+
+TEST(Vector, Modify_Insert_CountCopies) {
+ Counter::Reset();
+ Vector<Counter, 10> vector;
+
+ vector.emplace_back(123);
+ vector.insert(vector.cbegin() + 1, 8, Counter{421});
+ EXPECT_EQ(vector.at(0).value, 123);
+ EXPECT_EQ(vector.size(), 9u);
+
+ for (size_t i = 1; i < vector.size(); ++i) {
+ EXPECT_EQ(vector[i].value, 421);
+ }
+
+ EXPECT_EQ(Counter::moved, 0);
+ EXPECT_EQ(Counter::created, 10);
+}
+
+TEST(Vector, Modify_Insert_IteratorRange) {
+ std::array array_to_insert_first{0, 1, 2, 8, 9};
+ std::array array_to_insert_middle{3, 4, 5, 6, 7};
+
+ Vector<int, 10> vector(array_to_insert_first.begin(),
+ array_to_insert_first.end());
+
+ decltype(vector)::iterator it = vector.insert(vector.cbegin() + 3,
+ array_to_insert_middle.begin(),
+ array_to_insert_middle.end());
+ EXPECT_EQ(*it, array_to_insert_middle.front());
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ EXPECT_EQ(vector[i], static_cast<int>(i));
+ }
+}
+
+TEST(Vector, Modify_Insert_InitializerListRange) {
+ std::array array_to_insert_first{0, 1, 2, 8, 9};
+ Vector<int, 10> vector(array_to_insert_first.begin(),
+ array_to_insert_first.end());
+
+ decltype(vector)::iterator it =
+ vector.insert(vector.cbegin() + 3, {3, 4, 5, 6, 7});
+ EXPECT_EQ(*it, 3);
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ EXPECT_EQ(vector[i], static_cast<int>(i));
+ }
+}
+
+TEST(Vector, Modify_Insert_NonTrivial_InitializerListRange) {
+ std::array<Counter, 5> array_to_insert_first{0, 1, 2, 8, 9};
+ Vector<Counter, 10> vector(array_to_insert_first.begin(),
+ array_to_insert_first.end());
+
+ decltype(vector)::iterator it = vector.insert(
+ vector.cbegin() + 3, std::initializer_list<Counter>{3, 4, 5, 6, 7});
+ EXPECT_EQ(it->value, 3);
+
+ for (size_t i = 0; i < vector.max_size(); ++i) {
+ EXPECT_EQ(vector[i].value, static_cast<int>(i));
+ }
+}
+
TEST(Vector, Generic) {
Vector<int, 10> vector{1, 2, 3, 4, 5};
@@ -431,6 +707,19 @@ TEST(Vector, Generic) {
}
}
+TEST(Vector, ConstexprMaxSize) {
+ Vector<int, 10> vector;
+ Vector<int, vector.max_size()> vector2;
+
+ EXPECT_EQ(vector.max_size(), vector2.max_size());
+
+ // The following code would fail with the following compiler error:
+ // "non-type template argument is not a constant expression"
+ // Reason: the generic_vector doesn't return a constexpr max_size value.
+ // Vector<int>& generic_vector(vector);
+ // Vector<int, generic_vector.max_size()> vector3;
+}
+
// Test that Vector<T> is trivially destructible when its type is.
static_assert(std::is_trivially_destructible_v<Vector<int>>);
static_assert(std::is_trivially_destructible_v<Vector<int, 4>>);
diff --git a/pw_cpu_exception/BUILD.gn b/pw_cpu_exception/BUILD.gn
index f81194d37..4183dd6ee 100644
--- a/pw_cpu_exception/BUILD.gn
+++ b/pw_cpu_exception/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
import("backend.gni")
config("public_include_path") {
@@ -100,7 +101,10 @@ pw_facade("handler") {
pw_facade("support") {
backend = pw_cpu_exception_SUPPORT_BACKEND
public_configs = [ ":public_include_path" ]
- public_deps = [ ":entry" ]
+ public_deps = [
+ ":entry",
+ dir_pw_span,
+ ]
public = [ "public/pw_cpu_exception/support.h" ]
}
@@ -115,3 +119,6 @@ pw_source_set("basic_handler") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_cpu_exception/CMakeLists.txt b/pw_cpu_exception/CMakeLists.txt
index 6cc9cedb6..faa5fd937 100644
--- a/pw_cpu_exception/CMakeLists.txt
+++ b/pw_cpu_exception/CMakeLists.txt
@@ -13,8 +13,11 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_cpu_exception/backend.cmake)
-pw_add_facade(pw_cpu_exception.entry
+pw_add_facade(pw_cpu_exception.entry INTERFACE
+ BACKEND
+ pw_cpu_exception.entry_BACKEND
HEADERS
public/pw_cpu_exception/entry.h
public/pw_cpu_exception/state.h
@@ -24,7 +27,9 @@ pw_add_facade(pw_cpu_exception.entry
pw_preprocessor
)
-pw_add_facade(pw_cpu_exception.handler
+pw_add_facade(pw_cpu_exception.handler STATIC
+ BACKEND
+ pw_cpu_exception.handler_BACKEND
HEADERS
public/pw_cpu_exception/handler.h
PUBLIC_INCLUDES
@@ -36,7 +41,9 @@ pw_add_facade(pw_cpu_exception.handler
start_exception_handler.cc
)
-pw_add_facade(pw_cpu_exception.support
+pw_add_facade(pw_cpu_exception.support INTERFACE
+ BACKEND
+ pw_cpu_exception.support_BACKEND
HEADERS
public/pw_cpu_exception/support.h
PUBLIC_INCLUDES
@@ -45,7 +52,7 @@ pw_add_facade(pw_cpu_exception.support
pw_cpu_exception.entry
)
-pw_add_module_library(pw_cpu_exception.basic_handler
+pw_add_library(pw_cpu_exception.basic_handler STATIC
SOURCES
basic_handler.cc
PRIVATE_DEPS
diff --git a/pw_cpu_exception/backend.cmake b/pw_cpu_exception/backend.cmake
new file mode 100644
index 000000000..1f56d2034
--- /dev/null
+++ b/pw_cpu_exception/backend.cmake
@@ -0,0 +1,21 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backends for the pw_cpu_exception module.
+pw_add_backend_variable(pw_cpu_exception.entry_BACKEND)
+pw_add_backend_variable(pw_cpu_exception.handler_BACKEND)
+pw_add_backend_variable(pw_cpu_exception.support_BACKEND)
diff --git a/pw_cpu_exception/backend.gni b/pw_cpu_exception/backend.gni
index 07214d2e3..de37b8391 100644
--- a/pw_cpu_exception/backend.gni
+++ b/pw_cpu_exception/backend.gni
@@ -13,7 +13,7 @@
# the License.
declare_args() {
- # Backend for the pw_cpu_exception module.
+ # Backends for the pw_cpu_exception module.
pw_cpu_exception_ENTRY_BACKEND = ""
pw_cpu_exception_HANDLER_BACKEND = ""
pw_cpu_exception_SUPPORT_BACKEND = ""
diff --git a/pw_cpu_exception/public/pw_cpu_exception/support.h b/pw_cpu_exception/public/pw_cpu_exception/support.h
index c8df96458..f2cc19706 100644
--- a/pw_cpu_exception/public/pw_cpu_exception/support.h
+++ b/pw_cpu_exception/public/pw_cpu_exception/support.h
@@ -20,22 +20,21 @@
#pragma once
#include <cstdint>
-#include <span>
#include "pw_cpu_exception/state.h"
+#include "pw_span/span.h"
namespace pw::cpu_exception {
// Gets raw CPU state as a single contiguous block of data. The particular
// contents will depend on the specific backend and platform.
-std::span<const uint8_t> RawFaultingCpuState(
+span<const uint8_t> RawFaultingCpuState(
const pw_cpu_exception_State& cpu_state);
// Writes CPU state as a formatted string to a string builder.
// NEVER depend on the format of this output. This is exclusively FYI human
// readable output.
-void ToString(const pw_cpu_exception_State& cpu_state,
- const std::span<char>& dest);
+void ToString(const pw_cpu_exception_State& cpu_state, const span<char>& dest);
// Logs captured CPU state using pw_log at PW_LOG_LEVEL_INFO.
void LogCpuState(const pw_cpu_exception_State& cpu_state);
diff --git a/pw_cpu_exception_cortex_m/BUILD.bazel b/pw_cpu_exception_cortex_m/BUILD.bazel
index 012331082..102d5ebf8 100644
--- a/pw_cpu_exception_cortex_m/BUILD.bazel
+++ b/pw_cpu_exception_cortex_m/BUILD.bazel
@@ -17,6 +17,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -33,7 +35,7 @@ pw_cc_library(
includes = ["public"],
deps = [
"//pw_preprocessor",
- "//pw_preprocessor:arch",
+ "//pw_preprocessor:cortex_m",
],
)
@@ -42,12 +44,19 @@ pw_cc_library(
srcs = ["util.cc"],
hdrs = ["public/pw_cpu_exception_cortex_m/util.h"],
includes = ["public"],
+ target_compatible_with = select({
+ "@platforms//cpu:armv7-m": [],
+ "@platforms//cpu:armv7e-m": [],
+ "@platforms//cpu:armv7e-mf": [],
+ "@platforms//cpu:armv8-m": [],
+ "//conditions:default": ["@platforms//:incompatible"],
+ }),
deps = [
":config",
":cortex_m_constants",
":cpu_state",
"//pw_log",
- "//pw_preprocessor:arch",
+ "//pw_preprocessor:cortex_m",
],
)
@@ -61,7 +70,7 @@ pw_cc_library(
":util",
"//pw_log",
"//pw_preprocessor",
- "//pw_preprocessor:arch",
+ "//pw_preprocessor:cortex_m",
"//pw_string",
],
)
@@ -74,7 +83,7 @@ pw_cc_library(
deps = [
":config",
":cpu_state",
- ":cpu_state_protos",
+ ":cpu_state_protos_cc.pwpb",
":support",
"//pw_protobuf",
"//pw_status",
@@ -85,6 +94,18 @@ pw_cc_library(
proto_library(
name = "cpu_state_protos",
srcs = ["pw_cpu_exception_cortex_m_protos/cpu_state.proto"],
+ import_prefix = "pw_cpu_exception_cortex_m_protos",
+ strip_import_prefix = "/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_protos",
+)
+
+py_proto_library(
+ name = "cpu_state_protos_pb2",
+ srcs = ["pw_cpu_exception_cortex_m_protos/cpu_state.proto"],
+)
+
+pw_proto_library(
+ name = "cpu_state_protos_cc",
+ deps = [":cpu_state_protos"],
)
pw_cc_library(
@@ -102,10 +123,10 @@ pw_cc_library(
":proto_dump",
":support",
":util",
- # TODO(pwbug/101): Need to add support for facades/backends to Bazel.
- "//pw_cpu_exception",
+ # TODO(b/242183021): Need to add support for facades/backends to Bazel.
+ # "//pw_cpu_exception",
"//pw_preprocessor",
- "//pw_preprocessor:arch",
+ "//pw_preprocessor:cortex_m",
],
)
@@ -117,14 +138,14 @@ pw_cc_library(
":config",
":cortex_m_constants",
":cpu_state",
- ":cpu_state_protos",
+ ":cpu_state_protos_cc.pwpb",
":proto_dump",
":util",
"//pw_log",
"//pw_protobuf",
"//pw_status",
- "//pw_thread:protos",
"//pw_thread:snapshot",
+ "//pw_thread:thread_cc.pwpb",
],
)
@@ -132,7 +153,7 @@ pw_cc_library(
name = "cortex_m_constants",
hdrs = ["pw_cpu_exception_cortex_m_private/cortex_m_constants.h"],
visibility = ["//visibility:private"],
- deps = ["//pw_preprocessor:arch"],
+ deps = ["//pw_preprocessor:cortex_m"],
)
pw_cc_test(
@@ -141,6 +162,7 @@ pw_cc_test(
"exception_entry_test.cc",
],
deps = [
+ ":config",
":cpu_exception",
":cpu_state",
],
diff --git a/pw_cpu_exception_cortex_m/BUILD.gn b/pw_cpu_exception_cortex_m/BUILD.gn
index dd91dea27..7f029d5ab 100644
--- a/pw_cpu_exception_cortex_m/BUILD.gn
+++ b/pw_cpu_exception_cortex_m/BUILD.gn
@@ -55,6 +55,7 @@ pw_source_set("support") {
"$dir_pw_cpu_exception:support.facade",
"$dir_pw_preprocessor:arch",
dir_pw_log,
+ dir_pw_span,
dir_pw_string,
]
sources = [ "support.cc" ]
@@ -209,17 +210,19 @@ pw_test_group("tests") {
tests = [ ":cpu_exception_entry_test" ]
}
-# TODO(pwbug/583): Add ARMv8-M mainline coverage.
+# TODO(b/234888156): Add ARMv8-M mainline coverage.
pw_test("cpu_exception_entry_test") {
enable_if = pw_cpu_exception_ENTRY_BACKEND ==
"$dir_pw_cpu_exception_cortex_m:cpu_exception"
deps = [
+ ":config",
":cortex_m_constants",
":cpu_exception",
":cpu_state",
"$dir_pw_cpu_exception:entry",
"$dir_pw_cpu_exception:handler",
"$dir_pw_cpu_exception:support",
+ dir_pw_span,
]
sources = [ "exception_entry_test.cc" ]
}
diff --git a/pw_cpu_exception_cortex_m/CMakeLists.txt b/pw_cpu_exception_cortex_m/CMakeLists.txt
index 82d12b45d..fe0eaa108 100644
--- a/pw_cpu_exception_cortex_m/CMakeLists.txt
+++ b/pw_cpu_exception_cortex_m/CMakeLists.txt
@@ -17,14 +17,14 @@ include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
pw_add_module_config(pw_cpu_exception_cortex_m_CONFIG)
-pw_add_module_library(pw_cpu_exception_cortex_m.config
+pw_add_library(pw_cpu_exception_cortex_m.config INTERFACE
HEADERS
pw_cpu_exception_cortex_m_private/config.h
PUBLIC_DEPS
${pw_cpu_exception_cortex_m_CONFIG}
)
-pw_add_module_library(pw_cpu_exception_cortex_m.cpu_state
+pw_add_library(pw_cpu_exception_cortex_m.cpu_state INTERFACE
HEADERS
public/pw_cpu_exception_cortex_m/cpu_state.h
PUBLIC_INCLUDES
@@ -34,9 +34,7 @@ pw_add_module_library(pw_cpu_exception_cortex_m.cpu_state
pw_preprocessor.arch
)
-pw_add_module_library(pw_cpu_exception_cortex_m.cpu_exception
- IMPLEMENTS_FACADES
- pw_cpu_exception.entry
+pw_add_library(pw_cpu_exception_cortex_m.cpu_exception STATIC
HEADERS
public_overrides/pw_cpu_exception_backend/state.h
PUBLIC_INCLUDES
@@ -54,7 +52,7 @@ pw_add_module_library(pw_cpu_exception_cortex_m.cpu_exception
entry.cc
)
-pw_add_module_library(pw_cpu_exception_cortex_m.util
+pw_add_library(pw_cpu_exception_cortex_m.util STATIC
HEADERS
public/pw_cpu_exception_cortex_m/util.h
PUBLIC_INCLUDES
@@ -70,16 +68,14 @@ pw_add_module_library(pw_cpu_exception_cortex_m.util
util.cc
)
-pw_add_module_library(pw_cpu_exception_cortex_m.support
- IMPLEMENTS_FACADES
- pw_cpu_exception.support
+pw_add_library(pw_cpu_exception_cortex_m.support STATIC
PRIVATE_DEPS
pw_cpu_exception_cortex_m.config
pw_cpu_exception_cortex_m.constants
pw_cpu_exception_cortex_m.util
pw_log
- pw_polyfill.span
pw_preprocessor.arch
+ pw_span
pw_string
SOURCES
support.cc
@@ -90,7 +86,7 @@ pw_proto_library(pw_cpu_exception_cortex_m.cpu_state_protos
pw_cpu_exception_cortex_m_protos/cpu_state.proto
)
-pw_add_module_library(pw_cpu_exception_cortex_m.proto_dump
+pw_add_library(pw_cpu_exception_cortex_m.proto_dump STATIC
HEADERS
public/pw_cpu_exception_cortex_m/proto_dump.h
PUBLIC_INCLUDES
@@ -107,7 +103,11 @@ pw_add_module_library(pw_cpu_exception_cortex_m.proto_dump
proto_dump.cc
)
-pw_add_module_library(pw_cpu_exception_cortex_m.snapshot
+pw_add_library(pw_cpu_exception_cortex_m.snapshot STATIC
+ HEADERS
+ public/pw_cpu_exception_cortex_m/snapshot.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_cpu_exception_cortex_m.cpu_state
pw_cpu_exception_cortex_m.cpu_state_protos.pwpb
@@ -121,34 +121,32 @@ pw_add_module_library(pw_cpu_exception_cortex_m.snapshot
pw_cpu_exception_cortex_m.proto_dump
pw_cpu_exception_cortex_m.util
pw_log
- pw_polyfill.span
+ pw_span
SOURCES
snapshot.cc
- HEADERS
- public/pw_cpu_exception_cortex_m/snapshot.h
)
-pw_add_module_library(pw_cpu_exception_cortex_m.constants
- PUBLIC_DEPS
- pw_preprocessor.arch
+pw_add_library(pw_cpu_exception_cortex_m.constants INTERFACE
HEADERS
pw_cpu_exception_cortex_m_private/cortex_m_constants.h
+ PUBLIC_DEPS
+ pw_preprocessor.arch
)
-# TODO(pwbug/583): Add ARMv8-M mainline coverage.
+# TODO(b/234888156): Add ARMv8-M mainline coverage.
if("${pw_cpu_exception.entry_BACKEND}" STREQUAL
"pw_cpu_exception_cortex_m.cpu_exception")
pw_add_test(pw_cpu_exception_cortex_m.cpu_exception_entry_test
SOURCES
exception_entry_test.cc
- DEPS
+ PRIVATE_DEPS
pw_cpu_exception.entry
pw_cpu_exception.handler
pw_cpu_exception.support
- pw_cpu_exception_cortex_m.cortex_m_constants
+ pw_cpu_exception_cortex_m.constants
pw_cpu_exception_cortex_m.cpu_exception
pw_cpu_exception_cortex_m.cpu_state
- pw_polyfill.span
+ pw_span
GROUPS
modules
pw_cpu_exception_cortex_m
@@ -157,7 +155,7 @@ if("${pw_cpu_exception.entry_BACKEND}" STREQUAL
pw_add_test(pw_cpu_exception_cortex_m.util_test
SOURCES
util_test.cc
- DEPS
+ PRIVATE_DEPS
pw_cpu_exception_cortex_m.cpu_state
pw_cpu_exception_cortex_m.util
GROUPS
diff --git a/pw_cpu_exception_cortex_m/exception_entry_test.cc b/pw_cpu_exception_cortex_m/exception_entry_test.cc
index 8b7c2f6c0..4978efb08 100644
--- a/pw_cpu_exception_cortex_m/exception_entry_test.cc
+++ b/pw_cpu_exception_cortex_m/exception_entry_test.cc
@@ -13,7 +13,6 @@
// the License.
#include <cstdint>
-#include <span>
#include <type_traits>
#include "gtest/gtest.h"
@@ -21,7 +20,9 @@
#include "pw_cpu_exception/handler.h"
#include "pw_cpu_exception/support.h"
#include "pw_cpu_exception_cortex_m/cpu_state.h"
+#include "pw_cpu_exception_cortex_m_private/config.h"
#include "pw_cpu_exception_cortex_m_private/cortex_m_constants.h"
+#include "pw_span/span.h"
namespace pw::cpu_exception::cortex_m {
namespace {
@@ -87,17 +88,15 @@ inline void EndCriticalSection(uint32_t previous_state) {
}
void EnableFpu() {
-#if defined(PW_ARMV7M_ENABLE_FPU) && PW_ARMV7M_ENABLE_FPU == 1
- // TODO(pwbug/17): Replace when Pigweed config system is added.
- cortex_m_cpacr |= kFpuEnableMask;
-#endif // defined(PW_ARMV7M_ENABLE_FPU) && PW_ARMV7M_ENABLE_FPU == 1
+ if (PW_ARMV7M_ENABLE_FPU == 1) {
+ cortex_m_cpacr |= kFpuEnableMask;
+ }
}
void DisableFpu() {
-#if defined(PW_ARMV7M_ENABLE_FPU) && PW_ARMV7M_ENABLE_FPU == 1
- // TODO(pwbug/17): Replace when Pigweed config system is added.
- cortex_m_cpacr &= ~kFpuEnableMask;
-#endif // defined(PW_ARMV7M_ENABLE_FPU) && PW_ARMV7M_ENABLE_FPU == 1
+ if (PW_ARMV7M_ENABLE_FPU == 1) {
+ cortex_m_cpacr &= ~kFpuEnableMask;
+ }
}
// Counter that is incremented if the test's exception handler correctly handles
@@ -119,7 +118,7 @@ size_t current_fault_depth = 0;
pw_cpu_exception_State captured_states[kMaxFaultDepth] = {};
pw_cpu_exception_State& captured_state = captured_states[0];
-// Flag used to check if the contents of std::span matches the captured state.
+// Flag used to check if the contents of span matches the captured state.
bool span_matches = false;
// Variable to be manipulated by function that uses floating
@@ -458,10 +457,9 @@ TEST(FaultEntry, NestedFault) {
static_cast<uint32_t>(captured_states[0].base.lr));
}
-// TODO(pwbug/17): Replace when Pigweed config system is added.
// Disable tests that rely on hardware FPU if this module wasn't built with
// hardware FPU support.
-#if defined(PW_ARMV7M_ENABLE_FPU) && PW_ARMV7M_ENABLE_FPU == 1
+#if PW_ARMV7M_ENABLE_FPU == 1
// Populate some of the extended set of captured registers, then trigger
// exception. This function uses floating point to validate float context
@@ -527,7 +525,7 @@ TEST(FaultEntry, FloatUnalignedStackFault) {
EXPECT_EQ(float_test_value, kFloatTestPattern);
}
-#endif // defined(PW_ARMV7M_ENABLE_FPU) && PW_ARMV7M_ENABLE_FPU == 1
+#endif // PW_ARMV7M_ENABLE_FPU == 1
void TestingExceptionHandler(pw_cpu_exception_State* state) {
if (++current_fault_depth > kMaxFaultDepth) {
@@ -575,8 +573,8 @@ void TestingExceptionHandler(pw_cpu_exception_State* state) {
state,
sizeof(pw_cpu_exception_State));
- // Ensure std::span compares to be the same.
- std::span<const uint8_t> state_span = RawFaultingCpuState(*state);
+ // Ensure span compares to be the same.
+ span<const uint8_t> state_span = RawFaultingCpuState(*state);
EXPECT_EQ(state_span.size(), sizeof(pw_cpu_exception_State));
if (std::memcmp(state, state_span.data(), state_span.size()) == 0) {
span_matches = true;
diff --git a/pw_cpu_exception_cortex_m/public/pw_cpu_exception_cortex_m/snapshot.h b/pw_cpu_exception_cortex_m/public/pw_cpu_exception_cortex_m/snapshot.h
index 50ba3a303..2ba59df87 100644
--- a/pw_cpu_exception_cortex_m/public/pw_cpu_exception_cortex_m/snapshot.h
+++ b/pw_cpu_exception_cortex_m/public/pw_cpu_exception_cortex_m/snapshot.h
@@ -38,7 +38,7 @@ Status SnapshotMainStackThread(
const pw_cpu_exception_State& cpu_state,
uintptr_t stack_low_addr,
uintptr_t stack_high_addr,
- thread::SnapshotThreadInfo::StreamEncoder& encoder,
+ thread::proto::SnapshotThreadInfo::StreamEncoder& encoder,
thread::ProcessThreadStackCallback& thread_stack_callback);
// Captures the main stack thread if active as part of a snapshot based on the
@@ -49,7 +49,7 @@ Status SnapshotMainStackThread(
Status SnapshotMainStackThread(
uintptr_t stack_low_addr,
uintptr_t stack_high_addr,
- thread::SnapshotThreadInfo::StreamEncoder& encoder,
+ thread::proto::SnapshotThreadInfo::StreamEncoder& encoder,
thread::ProcessThreadStackCallback& thread_stack_callback);
// Captures the main stack thread if active as part of the cpu register state if
@@ -61,7 +61,7 @@ inline Status SnapshotMainStackThread(
const pw_cpu_exception_State* optional_cpu_state,
uintptr_t stack_low_addr,
uintptr_t stack_high_addr,
- thread::SnapshotThreadInfo::StreamEncoder& encoder,
+ thread::proto::SnapshotThreadInfo::StreamEncoder& encoder,
thread::ProcessThreadStackCallback& thread_stack_callback) {
if (optional_cpu_state != nullptr) {
return SnapshotMainStackThread(*optional_cpu_state,
diff --git a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
index eb0b7dd85..67f4fae11 100644
--- a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
+++ b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/config.h
@@ -40,3 +40,9 @@
#ifndef PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
#define PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP 0
#endif // PW_CPU_EXCEPTION_CORTEX_M_EXTENDED_CFSR_DUMP
+
+// Whether the floating-point unit is enabled.
+// TODO(b/264897542): This should be an Arm target trait.
+#ifndef PW_ARMV7M_ENABLE_FPU
+#define PW_ARMV7M_ENABLE_FPU 1
+#endif // PW_ARMV7M_ENABLE_FPU
diff --git a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h
index a47d67251..255057669 100644
--- a/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h
+++ b/pw_cpu_exception_cortex_m/pw_cpu_exception_cortex_m_private/cortex_m_constants.h
@@ -85,7 +85,7 @@ constexpr uint32_t kXpsrIpsrMask = 0b1'1111'1111;
constexpr uint32_t kControlThreadModeStackMask = 0x1u << 1; // 0=MSP, 1=PSP
// Memory mapped registers. (ARMv7-M Section B3.2.2, Table B3-4)
-// TODO(pwbug/316): Only some of these are supported on ARMv6-M.
+// TODO(b/234891073): Only some of these are supported on ARMv6-M.
inline volatile uint32_t& cortex_m_cfsr =
*reinterpret_cast<volatile uint32_t*>(0xE000ED28u);
inline volatile uint32_t& cortex_m_mmfar =
diff --git a/pw_cpu_exception_cortex_m/py/BUILD.bazel b/pw_cpu_exception_cortex_m/py/BUILD.bazel
new file mode 100644
index 000000000..93f5f82a4
--- /dev/null
+++ b/pw_cpu_exception_cortex_m/py/BUILD.bazel
@@ -0,0 +1,51 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_python//python:defs.bzl", "py_binary")
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "exception_analyzer",
+ srcs = [
+ "pw_cpu_exception_cortex_m/__init__.py",
+ "pw_cpu_exception_cortex_m/cortex_m_constants.py",
+ "pw_cpu_exception_cortex_m/exception_analyzer.py",
+ ],
+ imports = ["."],
+ deps = [
+ "//pw_cpu_exception_cortex_m:cpu_state_protos_pb2",
+ "//pw_symbolizer/py:pw_symbolizer",
+ ],
+)
+
+py_binary(
+ name = "cfsr_decoder",
+ srcs = ["pw_cpu_exception_cortex_m/cfsr_decoder.py"],
+ deps = [
+ ":exception_analyzer",
+ "//pw_cli/py:pw_cli",
+ "//pw_cpu_exception_cortex_m:cpu_state_protos_pb2",
+ ],
+)
+
+py_test(
+ name = "exception_analyzer_test",
+ size = "small",
+ srcs = ["exception_analyzer_test.py"],
+ deps = [
+ ":exception_analyzer",
+ "//pw_symbolizer/py:pw_symbolizer",
+ ],
+)
diff --git a/pw_cpu_exception_cortex_m/py/BUILD.gn b/pw_cpu_exception_cortex_m/py/BUILD.gn
index 9de835eb8..c6425ee7d 100644
--- a/pw_cpu_exception_cortex_m/py/BUILD.gn
+++ b/pw_cpu_exception_cortex_m/py/BUILD.gn
@@ -36,4 +36,5 @@ pw_python_package("py") {
"..:cpu_state_protos.python",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_cpu_exception_cortex_m/py/exception_analyzer_test.py b/pw_cpu_exception_cortex_m/py/exception_analyzer_test.py
index e12ee6bd6..056ecde29 100644
--- a/pw_cpu_exception_cortex_m/py/exception_analyzer_test.py
+++ b/pw_cpu_exception_cortex_m/py/exception_analyzer_test.py
@@ -14,7 +14,9 @@
# the License.
"""Tests dumped Cortex-M CPU state."""
+import textwrap
import unittest
+
from pw_cpu_exception_cortex_m import exception_analyzer, cortex_m_constants
from pw_cpu_exception_cortex_m_protos import cpu_state_pb2
import pw_symbolizer
@@ -24,11 +26,13 @@ import pw_symbolizer
class BasicFaultTest(unittest.TestCase):
"""Test basic fault analysis functions."""
+
def test_empty_state(self):
"""Ensure an empty CPU state proto doesn't indicate an active fault."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
+ cpu_state_proto
+ )
self.assertFalse(cpu_state_info.is_fault_active())
def test_cfsr_fault(self):
@@ -36,18 +40,20 @@ class BasicFaultTest(unittest.TestCase):
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = (
cortex_m_constants.PW_CORTEX_M_CFSR_STKOF_MASK
- | cortex_m_constants.PW_CORTEX_M_CFSR_MUNSTKERR_MASK)
+ | cortex_m_constants.PW_CORTEX_M_CFSR_MUNSTKERR_MASK
+ )
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
+ cpu_state_proto
+ )
self.assertTrue(cpu_state_info.is_fault_active())
def test_icsr_fault(self):
"""Ensure a fault is active if ICSR says the handler is active."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
- cpu_state_proto.icsr = (
- cortex_m_constants.PW_CORTEX_M_HARD_FAULT_ISR_NUM)
+ cpu_state_proto.icsr = cortex_m_constants.PW_CORTEX_M_HARD_FAULT_ISR_NUM
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
+ cpu_state_proto
+ )
self.assertTrue(cpu_state_info.is_fault_active())
def test_cfsr_fields(self):
@@ -55,9 +61,11 @@ class BasicFaultTest(unittest.TestCase):
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = (
cortex_m_constants.PW_CORTEX_M_CFSR_STKOF_MASK
- | cortex_m_constants.PW_CORTEX_M_CFSR_MUNSTKERR_MASK)
+ | cortex_m_constants.PW_CORTEX_M_CFSR_MUNSTKERR_MASK
+ )
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
+ cpu_state_proto
+ )
active_fields = [
field.name for field in cpu_state_info.active_cfsr_fields()
]
@@ -68,11 +76,13 @@ class BasicFaultTest(unittest.TestCase):
class ExceptionCauseTest(unittest.TestCase):
"""Test exception cause analysis."""
+
def test_empty_cpu_state(self):
"""Ensure empty CPU state has no known cause."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
+ cpu_state_proto
+ )
self.assertEqual(cpu_state_info.exception_cause(), 'unknown exception')
def test_unknown_exception(self):
@@ -81,7 +91,8 @@ class ExceptionCauseTest(unittest.TestCase):
# Set CFSR to a valid value.
cpu_state_proto.cfsr = 0
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
+ cpu_state_proto
+ )
self.assertEqual(cpu_state_info.exception_cause(), 'unknown exception')
def test_single_usage_fault(self):
@@ -89,16 +100,19 @@ class ExceptionCauseTest(unittest.TestCase):
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = cortex_m_constants.PW_CORTEX_M_CFSR_STKOF_MASK
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
- self.assertEqual(cpu_state_info.exception_cause(),
- 'usage fault [STKOF]')
+ cpu_state_proto
+ )
+ self.assertEqual(
+ cpu_state_info.exception_cause(), 'usage fault [STKOF]'
+ )
def test_single_usage_fault_without_fields(self):
"""Ensure disabling show_active_cfsr_fields hides field names."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = cortex_m_constants.PW_CORTEX_M_CFSR_STKOF_MASK
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
+ cpu_state_proto
+ )
self.assertEqual(cpu_state_info.exception_cause(False), 'usage fault')
def test_multiple_faults(self):
@@ -106,62 +120,81 @@ class ExceptionCauseTest(unittest.TestCase):
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = (
cortex_m_constants.PW_CORTEX_M_CFSR_STKOF_MASK
- | cortex_m_constants.PW_CORTEX_M_CFSR_UNSTKERR_MASK)
+ | cortex_m_constants.PW_CORTEX_M_CFSR_UNSTKERR_MASK
+ )
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
- self.assertEqual(cpu_state_info.exception_cause(),
- 'usage fault, bus fault [UNSTKERR] [STKOF]')
+ cpu_state_proto
+ )
+ self.assertEqual(
+ cpu_state_info.exception_cause(),
+ 'usage fault, bus fault [UNSTKERR] [STKOF]',
+ )
def test_mmfar_missing(self):
"""Ensure if mmfar is valid but missing it is handled safely."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = (
cortex_m_constants.PW_CORTEX_M_CFSR_MUNSTKERR_MASK
- | cortex_m_constants.PW_CORTEX_M_CFSR_MMARVALID_MASK)
+ | cortex_m_constants.PW_CORTEX_M_CFSR_MMARVALID_MASK
+ )
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
- self.assertEqual(cpu_state_info.exception_cause(False),
- 'memory management fault at ???')
+ cpu_state_proto
+ )
+ self.assertEqual(
+ cpu_state_info.exception_cause(False),
+ 'memory management fault at ???',
+ )
def test_mmfar_valid(self):
"""Validate output format of valid MMFAR."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = (
cortex_m_constants.PW_CORTEX_M_CFSR_MUNSTKERR_MASK
- | cortex_m_constants.PW_CORTEX_M_CFSR_MMARVALID_MASK)
- cpu_state_proto.mmfar = 0x722470e4
+ | cortex_m_constants.PW_CORTEX_M_CFSR_MMARVALID_MASK
+ )
+ cpu_state_proto.mmfar = 0x722470E4
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
- self.assertEqual(cpu_state_info.exception_cause(False),
- 'memory management fault at 0x722470e4')
+ cpu_state_proto
+ )
+ self.assertEqual(
+ cpu_state_info.exception_cause(False),
+ 'memory management fault at 0x722470e4',
+ )
def test_imprecise_bus_fault(self):
"""Check that imprecise bus faults are identified correctly."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = (
cortex_m_constants.PW_CORTEX_M_CFSR_IMPRECISERR_MASK
- | cortex_m_constants.PW_CORTEX_M_CFSR_IBUSERR_MASK)
+ | cortex_m_constants.PW_CORTEX_M_CFSR_IBUSERR_MASK
+ )
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
- self.assertEqual(cpu_state_info.exception_cause(False),
- 'imprecise bus fault')
+ cpu_state_proto
+ )
+ self.assertEqual(
+ cpu_state_info.exception_cause(False), 'imprecise bus fault'
+ )
class TextDumpTest(unittest.TestCase):
"""Test larger state dumps."""
+
def test_registers(self):
"""Validate output of general register dumps."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
- cpu_state_proto.pc = 0xdfadd966
- cpu_state_proto.mmfar = 0xaf2ea98a
- cpu_state_proto.r0 = 0xf3b235b1
+ cpu_state_proto.pc = 0xDFADD966
+ cpu_state_proto.mmfar = 0xAF2EA98A
+ cpu_state_proto.r0 = 0xF3B235B1
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
- expected_dump = '\n'.join((
- 'pc 0xdfadd966',
- 'mmfar 0xaf2ea98a',
- 'r0 0xf3b235b1',
- ))
+ cpu_state_proto
+ )
+ expected_dump = '\n'.join(
+ (
+ 'pc 0xdfadd966',
+ 'mmfar 0xaf2ea98a',
+ 'r0 0xf3b235b1',
+ )
+ )
self.assertEqual(cpu_state_info.dump_registers(), expected_dump)
def test_symbolization(self):
@@ -169,38 +202,45 @@ class TextDumpTest(unittest.TestCase):
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
known_symbols = (
pw_symbolizer.Symbol(0x0800A200, 'foo()', 'src/foo.c', 41),
- pw_symbolizer.Symbol(0x08000004, 'boot_entry()',
- 'src/vector_table.c', 5),
+ pw_symbolizer.Symbol(
+ 0x08000004, 'boot_entry()', 'src/vector_table.c', 5
+ ),
)
symbolizer = pw_symbolizer.FakeSymbolizer(known_symbols)
cpu_state_proto.pc = 0x0800A200
cpu_state_proto.lr = 0x08000004
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto, symbolizer)
- expected_dump = '\n'.join((
- 'pc 0x0800a200 foo() (src/foo.c:41)',
- 'lr 0x08000004 boot_entry() (src/vector_table.c:5)',
- ))
+ cpu_state_proto, symbolizer
+ )
+ expected_dump = '\n'.join(
+ (
+ 'pc 0x0800a200 foo() (src/foo.c:41)',
+ 'lr 0x08000004 boot_entry() (src/vector_table.c:5)',
+ )
+ )
self.assertEqual(cpu_state_info.dump_registers(), expected_dump)
def test_dump_no_cfsr(self):
"""Validate basic CPU state dump."""
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
- cpu_state_proto.pc = 0xd2603058
- cpu_state_proto.mmfar = 0x8e4eb9a2
- cpu_state_proto.r0 = 0xdb5e7168
- cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
- expected_dump = '\n'.join((
- 'Exception caused by a unknown exception.',
- '',
- 'No active Crash Fault Status Register (CFSR) fields.',
- '',
- 'All registers:',
- 'pc 0xd2603058',
- 'mmfar 0x8e4eb9a2',
- 'r0 0xdb5e7168',
- ))
+ cpu_state_proto.pc = 0xD2603058
+ cpu_state_proto.mmfar = 0x8E4EB9A2
+ cpu_state_proto.r0 = 0xDB5E7168
+ cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
+ cpu_state_proto
+ )
+ expected_dump = '\n'.join(
+ (
+ 'Exception caused by a unknown exception.',
+ '',
+ 'No active Crash Fault Status Register (CFSR) fields.',
+ '',
+ 'All registers:',
+ 'pc 0xd2603058',
+ 'mmfar 0x8e4eb9a2',
+ 'r0 0xdb5e7168',
+ )
+ )
self.assertEqual(str(cpu_state_info), expected_dump)
def test_dump_with_cfsr(self):
@@ -208,31 +248,37 @@ class TextDumpTest(unittest.TestCase):
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = (
cortex_m_constants.PW_CORTEX_M_CFSR_PRECISERR_MASK
- | cortex_m_constants.PW_CORTEX_M_CFSR_BFARVALID_MASK)
- cpu_state_proto.pc = 0xd2603058
- cpu_state_proto.bfar = 0xdeadbeef
- cpu_state_proto.mmfar = 0x8e4eb9a2
- cpu_state_proto.r0 = 0xdb5e7168
- cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
- expected_dump = '\n'.join((
- 'Exception caused by a bus fault at 0xdeadbeef.',
- '',
- 'Active Crash Fault Status Register (CFSR) fields:',
- 'PRECISERR Precise data bus error.',
- ' A data bus error has occurred, and the PC value stacked for',
- ' the exception return points to the instruction that caused',
- ' the fault. When the processor sets this bit to 1, it writes',
- ' the faulting address to the BFAR',
- 'BFARVALID BFAR is valid.',
- '',
- 'All registers:',
- 'pc 0xd2603058',
- 'cfsr 0x00008200',
- 'mmfar 0x8e4eb9a2',
- 'bfar 0xdeadbeef',
- 'r0 0xdb5e7168',
- ))
+ | cortex_m_constants.PW_CORTEX_M_CFSR_BFARVALID_MASK
+ )
+ cpu_state_proto.pc = 0xD2603058
+ cpu_state_proto.bfar = 0xDEADBEEF
+ cpu_state_proto.mmfar = 0x8E4EB9A2
+ cpu_state_proto.r0 = 0xDB5E7168
+ cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
+ cpu_state_proto
+ )
+ expected_dump = textwrap.dedent(
+ """
+ Exception caused by a bus fault at 0xdeadbeef.
+
+ Active Crash Fault Status Register (CFSR) fields:
+ PRECISERR Precise data bus error.
+ A data bus error has occurred, and the PC value stacked for
+ the exception return points to the instruction that caused
+ the fault. When the processor sets this bit to 1, it writes
+ the faulting address to the BFAR
+ BFARVALID BFAR is valid.
+
+ All registers:
+ pc 0xd2603058
+ cfsr 0x00008200
+ mmfar 0x8e4eb9a2
+ bfar 0xdeadbeef
+ r0 0xdb5e7168
+ """.strip(
+ '\n'
+ )
+ ).strip()
self.assertEqual(str(cpu_state_info), expected_dump)
diff --git a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/__init__.py b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/__init__.py
index 99a4f92b5..db41e573b 100644
--- a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/__init__.py
+++ b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/__init__.py
@@ -13,4 +13,6 @@
# the License.
"""Python tooling for Cortex-M CPU state analysis."""
from pw_cpu_exception_cortex_m.exception_analyzer import (
- CortexMExceptionAnalyzer, process_snapshot)
+ CortexMExceptionAnalyzer,
+ process_snapshot,
+)
diff --git a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cfsr_decoder.py b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cfsr_decoder.py
index 380fb1f79..0db960ebd 100644
--- a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cfsr_decoder.py
+++ b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cfsr_decoder.py
@@ -41,9 +41,9 @@ _LOG = logging.getLogger('decode_cfsr')
def _parse_args() -> argparse.Namespace:
"""Parses arguments for this script, splitting out the command to run."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('cfsr',
- type=lambda val: int(val, 0),
- help='The Cortex-M CFSR to decode')
+ parser.add_argument(
+ 'cfsr', type=lambda val: int(val, 0), help='The Cortex-M CFSR to decode'
+ )
return parser.parse_args()
@@ -51,7 +51,8 @@ def dump_cfsr(cfsr: int) -> int:
cpu_state_proto = cpu_state_pb2.ArmV7mCpuState()
cpu_state_proto.cfsr = cfsr
cpu_state_info = exception_analyzer.CortexMExceptionAnalyzer(
- cpu_state_proto)
+ cpu_state_proto
+ )
_LOG.info(cpu_state_info)
return 0
diff --git a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cortex_m_constants.py b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cortex_m_constants.py
index b44aa501a..af85d67e8 100644
--- a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cortex_m_constants.py
+++ b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/cortex_m_constants.py
@@ -38,154 +38,243 @@ PW_CORTEX_M_ICSR_VECTACTIVE_MASK = (1 << 9) - 1
PW_CORTEX_M_HFSR_FORCED_MASK = 0x1 << 30
# Masks for different sections of CFSR. (ARMv7-M Section B3.2.15)
-PW_CORTEX_M_CFSR_MEM_FAULT_MASK = 0x000000ff
-PW_CORTEX_M_CFSR_BUS_FAULT_MASK = 0x0000ff00
-PW_CORTEX_M_CFSR_USAGE_FAULT_MASK = 0xffff0000
+PW_CORTEX_M_CFSR_MEM_FAULT_MASK = 0x000000FF
+PW_CORTEX_M_CFSR_BUS_FAULT_MASK = 0x0000FF00
+PW_CORTEX_M_CFSR_USAGE_FAULT_MASK = 0xFFFF0000
# Masks for individual bits of CFSR. (ARMv7-M Section B3.2.15)
# Memory faults (MemManage Status Register) =
-PW_CORTEX_M_CFSR_MEM_FAULT_START = (0x1)
-PW_CORTEX_M_CFSR_IACCVIOL_MASK = (PW_CORTEX_M_CFSR_MEM_FAULT_START << 0)
-PW_CORTEX_M_CFSR_DACCVIOL_MASK = (PW_CORTEX_M_CFSR_MEM_FAULT_START << 1)
-PW_CORTEX_M_CFSR_MUNSTKERR_MASK = (PW_CORTEX_M_CFSR_MEM_FAULT_START << 3)
-PW_CORTEX_M_CFSR_MSTKERR_MASK = (PW_CORTEX_M_CFSR_MEM_FAULT_START << 4)
-PW_CORTEX_M_CFSR_MLSPERR_MASK = (PW_CORTEX_M_CFSR_MEM_FAULT_START << 5)
-PW_CORTEX_M_CFSR_MMARVALID_MASK = (PW_CORTEX_M_CFSR_MEM_FAULT_START << 7)
+PW_CORTEX_M_CFSR_MEM_FAULT_START = 0x1
+PW_CORTEX_M_CFSR_IACCVIOL_MASK = PW_CORTEX_M_CFSR_MEM_FAULT_START << 0
+PW_CORTEX_M_CFSR_DACCVIOL_MASK = PW_CORTEX_M_CFSR_MEM_FAULT_START << 1
+PW_CORTEX_M_CFSR_MUNSTKERR_MASK = PW_CORTEX_M_CFSR_MEM_FAULT_START << 3
+PW_CORTEX_M_CFSR_MSTKERR_MASK = PW_CORTEX_M_CFSR_MEM_FAULT_START << 4
+PW_CORTEX_M_CFSR_MLSPERR_MASK = PW_CORTEX_M_CFSR_MEM_FAULT_START << 5
+PW_CORTEX_M_CFSR_MMARVALID_MASK = PW_CORTEX_M_CFSR_MEM_FAULT_START << 7
# Bus faults (BusFault Status Register) =
-PW_CORTEX_M_CFSR_BUS_FAULT_START = (0x1 << 8)
-PW_CORTEX_M_CFSR_IBUSERR_MASK = (PW_CORTEX_M_CFSR_BUS_FAULT_START << 0)
-PW_CORTEX_M_CFSR_PRECISERR_MASK = (PW_CORTEX_M_CFSR_BUS_FAULT_START << 1)
-PW_CORTEX_M_CFSR_IMPRECISERR_MASK = (PW_CORTEX_M_CFSR_BUS_FAULT_START << 2)
-PW_CORTEX_M_CFSR_UNSTKERR_MASK = (PW_CORTEX_M_CFSR_BUS_FAULT_START << 3)
-PW_CORTEX_M_CFSR_STKERR_MASK = (PW_CORTEX_M_CFSR_BUS_FAULT_START << 4)
-PW_CORTEX_M_CFSR_LSPERR_MASK = (PW_CORTEX_M_CFSR_BUS_FAULT_START << 5)
-PW_CORTEX_M_CFSR_BFARVALID_MASK = (PW_CORTEX_M_CFSR_BUS_FAULT_START << 7)
+PW_CORTEX_M_CFSR_BUS_FAULT_START = 0x1 << 8
+PW_CORTEX_M_CFSR_IBUSERR_MASK = PW_CORTEX_M_CFSR_BUS_FAULT_START << 0
+PW_CORTEX_M_CFSR_PRECISERR_MASK = PW_CORTEX_M_CFSR_BUS_FAULT_START << 1
+PW_CORTEX_M_CFSR_IMPRECISERR_MASK = PW_CORTEX_M_CFSR_BUS_FAULT_START << 2
+PW_CORTEX_M_CFSR_UNSTKERR_MASK = PW_CORTEX_M_CFSR_BUS_FAULT_START << 3
+PW_CORTEX_M_CFSR_STKERR_MASK = PW_CORTEX_M_CFSR_BUS_FAULT_START << 4
+PW_CORTEX_M_CFSR_LSPERR_MASK = PW_CORTEX_M_CFSR_BUS_FAULT_START << 5
+PW_CORTEX_M_CFSR_BFARVALID_MASK = PW_CORTEX_M_CFSR_BUS_FAULT_START << 7
# Usage faults (UsageFault Status Register) =
-PW_CORTEX_M_CFSR_USAGE_FAULT_START = (0x1 << 16)
-PW_CORTEX_M_CFSR_UNDEFINSTR_MASK = (PW_CORTEX_M_CFSR_USAGE_FAULT_START << 0)
-PW_CORTEX_M_CFSR_INVSTATE_MASK = (PW_CORTEX_M_CFSR_USAGE_FAULT_START << 1)
-PW_CORTEX_M_CFSR_INVPC_MASK = (PW_CORTEX_M_CFSR_USAGE_FAULT_START << 2)
-PW_CORTEX_M_CFSR_NOCP_MASK = (PW_CORTEX_M_CFSR_USAGE_FAULT_START << 3)
-PW_CORTEX_M_CFSR_STKOF_MASK = (PW_CORTEX_M_CFSR_USAGE_FAULT_START << 4)
-PW_CORTEX_M_CFSR_UNALIGNED_MASK = (PW_CORTEX_M_CFSR_USAGE_FAULT_START << 8)
-PW_CORTEX_M_CFSR_DIVBYZERO_MASK = (PW_CORTEX_M_CFSR_USAGE_FAULT_START << 9)
+PW_CORTEX_M_CFSR_USAGE_FAULT_START = 0x1 << 16
+PW_CORTEX_M_CFSR_UNDEFINSTR_MASK = PW_CORTEX_M_CFSR_USAGE_FAULT_START << 0
+PW_CORTEX_M_CFSR_INVSTATE_MASK = PW_CORTEX_M_CFSR_USAGE_FAULT_START << 1
+PW_CORTEX_M_CFSR_INVPC_MASK = PW_CORTEX_M_CFSR_USAGE_FAULT_START << 2
+PW_CORTEX_M_CFSR_NOCP_MASK = PW_CORTEX_M_CFSR_USAGE_FAULT_START << 3
+PW_CORTEX_M_CFSR_STKOF_MASK = PW_CORTEX_M_CFSR_USAGE_FAULT_START << 4
+PW_CORTEX_M_CFSR_UNALIGNED_MASK = PW_CORTEX_M_CFSR_USAGE_FAULT_START << 8
+PW_CORTEX_M_CFSR_DIVBYZERO_MASK = PW_CORTEX_M_CFSR_USAGE_FAULT_START << 9
# TODO(amontanez): We could probably make a whole module on bit field handling
# in python.
BitField = collections.namedtuple(
- 'BitField', ['name', 'bit_mask', 'description', 'long_description'])
+ 'BitField', ['name', 'bit_mask', 'description', 'long_description']
+)
# Information about faults from:
# * ARM Cortex-M4 Devices Generic User Guide 4.3.10
# * ARM Cortex-M33 Devices Generic User Guide 4.2.11
PW_CORTEX_M_CFSR_BIT_FIELDS = [
- BitField('IACCVIOL', PW_CORTEX_M_CFSR_IACCVIOL_MASK,
- 'Instruction access violation fault.',
- ('The processor attempted an instruction fetch from a location',
- 'that does not permit execution. The PC value stacked for the',
- 'exception return points to the faulting instruction.')),
- BitField('DACCVIOL', PW_CORTEX_M_CFSR_DACCVIOL_MASK,
- 'Data access violation fault.',
- ('The processor attempted a load or store at a location that',
- 'does not permit the operation. The PC value stacked for the',
- 'exception return points to the faulting instruction. The',
- 'processor has loaded the MMFAR with the address of the',
- 'attempted access.')),
- BitField('MUNSTKERR', PW_CORTEX_M_CFSR_MUNSTKERR_MASK,
- 'MemManage fault on unstacking for a return from exception.',
- ('Unstack for an exception return has caused one or more access',
- 'violations. This fault is chained to the handler. This means',
- 'that when this bit is 1, the original return stack is still',
- 'present. The processor has not adjusted the SP from the',
- 'failing return, and has not performed a new save. The',
- 'processor has not written a fault address to the MMAR.')),
- BitField('MSTKERR', PW_CORTEX_M_CFSR_MSTKERR_MASK,
- 'MemManage fault on stacking for exception entry.',
- ('When this bit is 1, the SP is still adjusted but the values',
- 'in the context area on the stack might be incorrect. The',
- 'processor has not written a fault address to the MMAR.')),
- BitField('MLSPERR', PW_CORTEX_M_CFSR_MLSPERR_MASK,
- 'MemManage Fault during FPU lazy state preservation.', ''),
- BitField('MMARVALID', PW_CORTEX_M_CFSR_MMARVALID_MASK,
- 'MMFAR register is valid.', ''),
- BitField('IBUSERR', PW_CORTEX_M_CFSR_IBUSERR_MASK,
- 'Instruction bus error.',
- ('The processor attempted to issue an invalid instruction. It',
- 'detects the instruction bus error on prefetching, but this',
- 'flag is only set to 1 if it attempts to issue the faulting',
- 'instruction. When this bit is set, the processor has not',
- 'written a fault address to the BFAR.')),
- BitField('PRECISERR', PW_CORTEX_M_CFSR_PRECISERR_MASK,
- 'Precise data bus error.',
- ('A data bus error has occurred, and the PC value stacked for',
- 'the exception return points to the instruction that caused',
- 'the fault. When the processor sets this bit to 1, it writes',
- 'the faulting address to the BFAR')),
- BitField('IMPRECISERR', PW_CORTEX_M_CFSR_IMPRECISERR_MASK,
- 'Imprecise data bus error.',
- ('A data bus error has occurred, but the return address in the',
- 'stack frame is not related to the instruction that caused the',
- 'error. This is an asynchronous fault. Therefore, if it is',
- 'detected when the priority of the current processes is higher',
- 'than the BusFault priority, the BusFault becomes pending and',
- 'becomes active only when the processor returns from all higher',
- 'priority processes. If a precise fault occurs before the',
- 'processor enters the handler for the imprecise BusFault, the',
- 'handler detects both IMPRECISERR set to 1 and one of the',
- 'precise fault status bits set to 1')),
- BitField('UNSTKERR', PW_CORTEX_M_CFSR_UNSTKERR_MASK,
- 'BusFault on Unstacking for a return from exception.',
- ('Unstack for an exception return has caused one or more',
- 'BusFaults. This fault is chained to the handler. This means',
- 'when the processor sets this bit to 1, the original return',
- 'stack is still present. The processor does not adjust the SP',
- 'from the failing return, does not perform a new save, and does',
- 'not write a fault address to the BFAR')),
- BitField('STKERR', PW_CORTEX_M_CFSR_STKERR_MASK,
- 'BusFault on Stacking for Exception Entry.',
- ('Stacking for an exception entry has caused one or more',
- 'BusFaults. When the processor sets this bit to 1, the SP is',
- 'still adjusted but the values in the context area on the stack',
- 'might be incorrect. The processor does not write a fault',
- 'address to the BFAR')),
- BitField('LSPERR', PW_CORTEX_M_CFSR_LSPERR_MASK,
- 'BusFault during FPU lazy state preservation.', ''),
- BitField('BFARVALID', PW_CORTEX_M_CFSR_BFARVALID_MASK, 'BFAR is valid.',
- ''),
- BitField('UNDEFINSTR', PW_CORTEX_M_CFSR_UNDEFINSTR_MASK,
- 'Undefined Instruction UsageFault.',
- ('The processor has attempted to execute an undefined',
- 'instruction. When this bit is set to 1, the PC value stacked',
- 'for the exception return points to the undefined instruction.',
- 'An undefined instruction is an instruction that the processor',
- 'cannot decode.')),
- BitField('INVSTATE', PW_CORTEX_M_CFSR_INVSTATE_MASK,
- 'Invalid State UsageFault.',
- ('The processor has attempted to execute an instruction that',
- 'makes illegal use of the EPSR. The PC value stacked for the',
- 'exception return points to the instruction that attempt',
- 'illegal use of the EPSR')),
- BitField('INVPC', PW_CORTEX_M_CFSR_INVPC_MASK,
- 'Invalid PC Load UsageFault.',
- ('The processor has attempted an illegal load of EXC_RETURN',
- 'to the PC, as a result of an invalid context, or an invalid',
- 'EXC_RETURN value. The PC value stacked for the exception',
- 'return points to the instruction that tried to perform the',
- 'illegal load of the PC')),
- BitField('NOCP', PW_CORTEX_M_CFSR_NOCP_MASK,
- 'Coprocessor disabled or not present.', ''),
+ BitField(
+ 'IACCVIOL',
+ PW_CORTEX_M_CFSR_IACCVIOL_MASK,
+ 'Instruction access violation fault.',
+ (
+ 'The processor attempted an instruction fetch from a location',
+ 'that does not permit execution. The PC value stacked for the',
+ 'exception return points to the faulting instruction.',
+ ),
+ ),
+ BitField(
+ 'DACCVIOL',
+ PW_CORTEX_M_CFSR_DACCVIOL_MASK,
+ 'Data access violation fault.',
+ (
+ 'The processor attempted a load or store at a location that',
+ 'does not permit the operation. The PC value stacked for the',
+ 'exception return points to the faulting instruction. The',
+ 'processor has loaded the MMFAR with the address of the',
+ 'attempted access.',
+ ),
+ ),
+ BitField(
+ 'MUNSTKERR',
+ PW_CORTEX_M_CFSR_MUNSTKERR_MASK,
+ 'MemManage fault on unstacking for a return from exception.',
+ (
+ 'Unstack for an exception return has caused one or more access',
+ 'violations. This fault is chained to the handler. This means',
+ 'that when this bit is 1, the original return stack is still',
+ 'present. The processor has not adjusted the SP from the',
+ 'failing return, and has not performed a new save. The',
+ 'processor has not written a fault address to the MMAR.',
+ ),
+ ),
+ BitField(
+ 'MSTKERR',
+ PW_CORTEX_M_CFSR_MSTKERR_MASK,
+ 'MemManage fault on stacking for exception entry.',
+ (
+ 'When this bit is 1, the SP is still adjusted but the values',
+ 'in the context area on the stack might be incorrect. The',
+ 'processor has not written a fault address to the MMAR.',
+ ),
+ ),
+ BitField(
+ 'MLSPERR',
+ PW_CORTEX_M_CFSR_MLSPERR_MASK,
+ 'MemManage Fault during FPU lazy state preservation.',
+ '',
+ ),
+ BitField(
+ 'MMARVALID',
+ PW_CORTEX_M_CFSR_MMARVALID_MASK,
+ 'MMFAR register is valid.',
+ '',
+ ),
+ BitField(
+ 'IBUSERR',
+ PW_CORTEX_M_CFSR_IBUSERR_MASK,
+ 'Instruction bus error.',
+ (
+ 'The processor attempted to issue an invalid instruction. It',
+ 'detects the instruction bus error on prefetching, but this',
+ 'flag is only set to 1 if it attempts to issue the faulting',
+ 'instruction. When this bit is set, the processor has not',
+ 'written a fault address to the BFAR.',
+ ),
+ ),
+ BitField(
+ 'PRECISERR',
+ PW_CORTEX_M_CFSR_PRECISERR_MASK,
+ 'Precise data bus error.',
+ (
+ 'A data bus error has occurred, and the PC value stacked for',
+ 'the exception return points to the instruction that caused',
+ 'the fault. When the processor sets this bit to 1, it writes',
+ 'the faulting address to the BFAR',
+ ),
+ ),
+ BitField(
+ 'IMPRECISERR',
+ PW_CORTEX_M_CFSR_IMPRECISERR_MASK,
+ 'Imprecise data bus error.',
+ (
+ 'A data bus error has occurred, but the return address in the',
+ 'stack frame is not related to the instruction that caused the',
+ 'error. This is an asynchronous fault. Therefore, if it is',
+ 'detected when the priority of the current processes is higher',
+ 'than the BusFault priority, the BusFault becomes pending and',
+ 'becomes active only when the processor returns from all higher',
+ 'priority processes. If a precise fault occurs before the',
+ 'processor enters the handler for the imprecise BusFault, the',
+ 'handler detects both IMPRECISERR set to 1 and one of the',
+ 'precise fault status bits set to 1',
+ ),
+ ),
+ BitField(
+ 'UNSTKERR',
+ PW_CORTEX_M_CFSR_UNSTKERR_MASK,
+ 'BusFault on Unstacking for a return from exception.',
+ (
+ 'Unstack for an exception return has caused one or more',
+ 'BusFaults. This fault is chained to the handler. This means',
+ 'when the processor sets this bit to 1, the original return',
+ 'stack is still present. The processor does not adjust the SP',
+ 'from the failing return, does not perform a new save, and does',
+ 'not write a fault address to the BFAR',
+ ),
+ ),
+ BitField(
+ 'STKERR',
+ PW_CORTEX_M_CFSR_STKERR_MASK,
+ 'BusFault on Stacking for Exception Entry.',
+ (
+ 'Stacking for an exception entry has caused one or more',
+ 'BusFaults. When the processor sets this bit to 1, the SP is',
+ 'still adjusted but the values in the context area on the stack',
+ 'might be incorrect. The processor does not write a fault',
+ 'address to the BFAR',
+ ),
+ ),
+ BitField(
+ 'LSPERR',
+ PW_CORTEX_M_CFSR_LSPERR_MASK,
+ 'BusFault during FPU lazy state preservation.',
+ '',
+ ),
+ BitField(
+ 'BFARVALID', PW_CORTEX_M_CFSR_BFARVALID_MASK, 'BFAR is valid.', ''
+ ),
+ BitField(
+ 'UNDEFINSTR',
+ PW_CORTEX_M_CFSR_UNDEFINSTR_MASK,
+ 'Undefined Instruction UsageFault.',
+ (
+ 'The processor has attempted to execute an undefined',
+ 'instruction. When this bit is set to 1, the PC value stacked',
+ 'for the exception return points to the undefined instruction.',
+ 'An undefined instruction is an instruction that the processor',
+ 'cannot decode.',
+ ),
+ ),
+ BitField(
+ 'INVSTATE',
+ PW_CORTEX_M_CFSR_INVSTATE_MASK,
+ 'Invalid State UsageFault.',
+ (
+ 'The processor has attempted to execute an instruction that',
+ 'makes illegal use of the EPSR. The PC value stacked for the',
+ 'exception return points to the instruction that attempt',
+ 'illegal use of the EPSR',
+ ),
+ ),
+ BitField(
+ 'INVPC',
+ PW_CORTEX_M_CFSR_INVPC_MASK,
+ 'Invalid PC Load UsageFault.',
+ (
+ 'The processor has attempted an illegal load of EXC_RETURN',
+ 'to the PC, as a result of an invalid context, or an invalid',
+ 'EXC_RETURN value. The PC value stacked for the exception',
+ 'return points to the instruction that tried to perform the',
+ 'illegal load of the PC',
+ ),
+ ),
+ BitField(
+ 'NOCP',
+ PW_CORTEX_M_CFSR_NOCP_MASK,
+ 'Coprocessor disabled or not present.',
+ '',
+ ),
BitField('STKOF', PW_CORTEX_M_CFSR_STKOF_MASK, 'Stack overflowed.', ''),
- BitField('UNALIGNED', PW_CORTEX_M_CFSR_UNALIGNED_MASK,
- 'Unaligned access UsageFault.',
- ('The processor has made an unaligned memory access. This fault',
- 'can be enabled or disabled using the UNALIGN_TRP bit in the',
- 'CCR. Unaligned LDM, STM, LDRD, and STRD instructions always',
- 'fault irrespective of the CCR setting.')),
- BitField('DIVBYZERO', PW_CORTEX_M_CFSR_DIVBYZERO_MASK, 'Divide by zero.',
- ('The processor has executed an SDIV or UDIV instruction with',
- 'a divisor of 0. The PC value stacked for the exception',
- 'return points to the instruction that performed the divide',
- 'by zero. This fault can be enabled or disabled using the',
- 'DIV_0_TRP bit in the CCR.')),
+ BitField(
+ 'UNALIGNED',
+ PW_CORTEX_M_CFSR_UNALIGNED_MASK,
+ 'Unaligned access UsageFault.',
+ (
+ 'The processor has made an unaligned memory access. This fault',
+ 'can be enabled or disabled using the UNALIGN_TRP bit in the',
+ 'CCR. Unaligned LDM, STM, LDRD, and STRD instructions always',
+ 'fault irrespective of the CCR setting.',
+ ),
+ ),
+ BitField(
+ 'DIVBYZERO',
+ PW_CORTEX_M_CFSR_DIVBYZERO_MASK,
+ 'Divide by zero.',
+ (
+ 'The processor has executed an SDIV or UDIV instruction with',
+ 'a divisor of 0. The PC value stacked for the exception',
+ 'return points to the instruction that performed the divide',
+ 'by zero. This fault can be enabled or disabled using the',
+ 'DIV_0_TRP bit in the CCR.',
+ ),
+ ),
]
diff --git a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py
index dd96af20a..82500a135 100644
--- a/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py
+++ b/pw_cpu_exception_cortex_m/py/pw_cpu_exception_cortex_m/exception_analyzer.py
@@ -20,20 +20,40 @@ from pw_cpu_exception_cortex_m_protos import cpu_state_pb2
import pw_symbolizer
# These registers are symbolized when dumped.
-_SYMBOLIZED_REGISTERS = ('pc', 'lr', 'bfar', 'mmfar', 'msp', 'psp', 'r0', 'r1',
- 'r2', 'r3', 'r4', 'r5', 'r6', 'r7', 'r8', 'r9', 'r10',
- 'r11', 'r12')
+_SYMBOLIZED_REGISTERS = (
+ 'pc',
+ 'lr',
+ 'bfar',
+ 'mmfar',
+ 'msp',
+ 'psp',
+ 'r0',
+ 'r1',
+ 'r2',
+ 'r3',
+ 'r4',
+ 'r5',
+ 'r6',
+ 'r7',
+ 'r8',
+ 'r9',
+ 'r10',
+ 'r11',
+ 'r12',
+)
class CortexMExceptionAnalyzer:
"""This class provides helper functions to dump a ArmV7mCpuState proto."""
- def __init__(self,
- cpu_state,
- symbolizer: Optional[pw_symbolizer.Symbolizer] = None):
+
+ def __init__(
+ self, cpu_state, symbolizer: Optional[pw_symbolizer.Symbolizer] = None
+ ):
self._cpu_state = cpu_state
self._symbolizer = symbolizer
- self._active_cfsr_fields: Optional[Tuple[cortex_m_constants.BitField,
- ...]] = None
+ self._active_cfsr_fields: Optional[
+ Tuple[cortex_m_constants.BitField, ...]
+ ] = None
def active_cfsr_fields(self) -> Tuple[cortex_m_constants.BitField, ...]:
"""Returns a list of BitFields for each active CFSR flag."""
@@ -56,10 +76,13 @@ class CortexMExceptionAnalyzer:
if self._cpu_state.HasField('icsr'):
exception_number = (
self._cpu_state.icsr
- & cortex_m_constants.PW_CORTEX_M_ICSR_VECTACTIVE_MASK)
- if (cortex_m_constants.PW_CORTEX_M_HARD_FAULT_ISR_NUM <=
- exception_number <=
- cortex_m_constants.PW_CORTEX_M_USAGE_FAULT_ISR_NUM):
+ & cortex_m_constants.PW_CORTEX_M_ICSR_VECTACTIVE_MASK
+ )
+ if (
+ cortex_m_constants.PW_CORTEX_M_HARD_FAULT_ISR_NUM
+ <= exception_number
+ <= cortex_m_constants.PW_CORTEX_M_USAGE_FAULT_ISR_NUM
+ ):
return True
return False
@@ -67,8 +90,11 @@ class CortexMExceptionAnalyzer:
"""Returns true if the current CPU state indicates a nested fault."""
if not self.is_fault_active():
return False
- if (self._cpu_state.HasField('hfsr') and self._cpu_state.hfsr
- & cortex_m_constants.PW_CORTEX_M_HFSR_FORCED_MASK):
+ if (
+ self._cpu_state.HasField('hfsr')
+ and self._cpu_state.hfsr
+ & cortex_m_constants.PW_CORTEX_M_HFSR_FORCED_MASK
+ ):
return True
return False
@@ -90,31 +116,49 @@ class CortexMExceptionAnalyzer:
split_major_cause = lambda cause: cause if not cause else cause + ', '
if self._cpu_state.HasField('cfsr') and self.is_fault_active():
- if (self._cpu_state.cfsr
- & cortex_m_constants.PW_CORTEX_M_CFSR_USAGE_FAULT_MASK):
+ if (
+ self._cpu_state.cfsr
+ & cortex_m_constants.PW_CORTEX_M_CFSR_USAGE_FAULT_MASK
+ ):
cause += 'usage fault'
- if (self._cpu_state.cfsr
- & cortex_m_constants.PW_CORTEX_M_CFSR_MEM_FAULT_MASK):
+ if (
+ self._cpu_state.cfsr
+ & cortex_m_constants.PW_CORTEX_M_CFSR_MEM_FAULT_MASK
+ ):
cause = split_major_cause(cause)
cause += 'memory management fault'
- if (self._cpu_state.cfsr
- & cortex_m_constants.PW_CORTEX_M_CFSR_MMARVALID_MASK):
- addr = '???' if not self._cpu_state.HasField(
- 'mmfar') else f'0x{self._cpu_state.mmfar:08x}'
+ if (
+ self._cpu_state.cfsr
+ & cortex_m_constants.PW_CORTEX_M_CFSR_MMARVALID_MASK
+ ):
+ addr = (
+ '???'
+ if not self._cpu_state.HasField('mmfar')
+ else f'0x{self._cpu_state.mmfar:08x}'
+ )
cause += f' at {addr}'
- if (self._cpu_state.cfsr
- & cortex_m_constants.PW_CORTEX_M_CFSR_BUS_FAULT_MASK):
+ if (
+ self._cpu_state.cfsr
+ & cortex_m_constants.PW_CORTEX_M_CFSR_BUS_FAULT_MASK
+ ):
cause = split_major_cause(cause)
- if (self._cpu_state.cfsr &
- cortex_m_constants.PW_CORTEX_M_CFSR_IMPRECISERR_MASK):
+ if (
+ self._cpu_state.cfsr
+ & cortex_m_constants.PW_CORTEX_M_CFSR_IMPRECISERR_MASK
+ ):
cause += 'imprecise '
cause += 'bus fault'
- if (self._cpu_state.cfsr
- & cortex_m_constants.PW_CORTEX_M_CFSR_BFARVALID_MASK):
- addr = '???' if not self._cpu_state.HasField(
- 'bfar') else f'0x{self._cpu_state.bfar:08x}'
+ if (
+ self._cpu_state.cfsr
+ & cortex_m_constants.PW_CORTEX_M_CFSR_BFARVALID_MASK
+ ):
+ addr = (
+ '???'
+ if not self._cpu_state.HasField('bfar')
+ else f'0x{self._cpu_state.bfar:08x}'
+ )
cause += f' at {addr}'
if show_active_cfsr_fields:
for field in self.active_cfsr_fields():
@@ -129,8 +173,10 @@ class CortexMExceptionAnalyzer:
if self._cpu_state.HasField(field.name):
register_value = getattr(self._cpu_state, field.name)
register_str = f'{field.name:<10} 0x{register_value:08x}'
- if (self._symbolizer is not None
- and field.name in _SYMBOLIZED_REGISTERS):
+ if (
+ self._symbolizer is not None
+ and field.name in _SYMBOLIZED_REGISTERS
+ ):
symbol = self._symbolizer.symbolize(register_value)
if symbol.name:
register_str += f' {symbol}'
@@ -143,34 +189,42 @@ class CortexMExceptionAnalyzer:
for field in self.active_cfsr_fields():
fields.append(f'{field.name:<11} {field.description}')
if isinstance(field.long_description, tuple):
- long_desc = ' {}'.format('\n '.join(
- field.long_description))
+ long_desc = ' {}'.format(
+ '\n '.join(field.long_description)
+ )
fields.append(long_desc)
return '\n'.join(fields)
def __str__(self):
dump = [f'Exception caused by a {self.exception_cause(False)}.', '']
if self.active_cfsr_fields():
- dump.extend((
- 'Active Crash Fault Status Register (CFSR) fields:',
- self.dump_active_active_cfsr_fields(),
- '',
- ))
+ dump.extend(
+ (
+ 'Active Crash Fault Status Register (CFSR) fields:',
+ self.dump_active_active_cfsr_fields(),
+ '',
+ )
+ )
else:
- dump.extend((
- 'No active Crash Fault Status Register (CFSR) fields.',
- '',
- ))
- dump.extend((
- 'All registers:',
- self.dump_registers(),
- ))
+ dump.extend(
+ (
+ 'No active Crash Fault Status Register (CFSR) fields.',
+ '',
+ )
+ )
+ dump.extend(
+ (
+ 'All registers:',
+ self.dump_registers(),
+ )
+ )
return '\n'.join(dump)
def process_snapshot(
- serialized_snapshot: bytes,
- symbolizer: Optional[pw_symbolizer.Symbolizer] = None) -> str:
+ serialized_snapshot: bytes,
+ symbolizer: Optional[pw_symbolizer.Symbolizer] = None,
+) -> str:
"""Returns the stringified result of a SnapshotCpuStateOverlay message run
though a CortexMExceptionAnalyzer.
"""
@@ -178,8 +232,9 @@ def process_snapshot(
snapshot.ParseFromString(serialized_snapshot)
if snapshot.HasField('armv7m_cpu_state'):
- state_analyzer = CortexMExceptionAnalyzer(snapshot.armv7m_cpu_state,
- symbolizer)
+ state_analyzer = CortexMExceptionAnalyzer(
+ snapshot.armv7m_cpu_state, symbolizer
+ )
return f'{state_analyzer}\n'
return ''
diff --git a/pw_cpu_exception_cortex_m/py/setup.cfg b/pw_cpu_exception_cortex_m/py/setup.cfg
index 178a77a64..838040f9b 100644
--- a/pw_cpu_exception_cortex_m/py/setup.cfg
+++ b/pw_cpu_exception_cortex_m/py/setup.cfg
@@ -21,10 +21,6 @@ description = Tools for analyzing dumped ARM Cortex-M CPU exceptions
[options]
packages = find:
zip_safe = False
-install_requires =
- protobuf
- pw_cli
- pw_protobuf_compiler
[options.package_data]
pw_cpu_exception_cortex_m = py.typed
diff --git a/pw_cpu_exception_cortex_m/snapshot.cc b/pw_cpu_exception_cortex_m/snapshot.cc
index f4925ed49..8850f7a8c 100644
--- a/pw_cpu_exception_cortex_m/snapshot.cc
+++ b/pw_cpu_exception_cortex_m/snapshot.cc
@@ -38,25 +38,26 @@ Status CaptureMainStack(
uintptr_t stack_low_addr,
uintptr_t stack_high_addr,
uintptr_t stack_pointer,
- thread::SnapshotThreadInfo::StreamEncoder& snapshot_encoder,
+ thread::proto::SnapshotThreadInfo::StreamEncoder& snapshot_encoder,
thread::ProcessThreadStackCallback& thread_stack_callback) {
- thread::Thread::StreamEncoder encoder = snapshot_encoder.GetThreadsEncoder();
+ thread::proto::Thread::StreamEncoder encoder =
+ snapshot_encoder.GetThreadsEncoder();
const char* thread_name;
- thread::ThreadState::Enum thread_state;
+ thread::proto::ThreadState::Enum thread_state;
if (mode == ProcessorMode::kHandlerMode) {
thread_name = kMainStackHandlerModeName;
PW_LOG_DEBUG("Capturing thread info for Main Stack (Handler Mode)");
- thread_state = thread::ThreadState::Enum::INTERRUPT_HANDLER;
+ thread_state = thread::proto::ThreadState::Enum::INTERRUPT_HANDLER;
PW_LOG_DEBUG("Thread state: INTERRUPT_HANDLER");
} else { // mode == ProcessorMode::kThreadMode
- thread_name = kMainStackHandlerModeName;
+ thread_name = kMainStackThreadModeName;
PW_LOG_DEBUG("Capturing thread info for Main Stack (Thread Mode)");
- thread_state = thread::ThreadState::Enum::RUNNING;
+ thread_state = thread::proto::ThreadState::Enum::RUNNING;
PW_LOG_DEBUG("Thread state: RUNNING");
}
encoder.WriteState(thread_state);
- encoder.WriteName(std::as_bytes(std::span(std::string_view(thread_name))));
+ encoder.WriteName(as_bytes(span(std::string_view(thread_name))));
const thread::StackContext thread_ctx = {
.thread_name = thread_name,
@@ -84,7 +85,7 @@ Status SnapshotCpuState(
Status SnapshotMainStackThread(
uintptr_t stack_low_addr,
uintptr_t stack_high_addr,
- thread::SnapshotThreadInfo::StreamEncoder& encoder,
+ thread::proto::SnapshotThreadInfo::StreamEncoder& encoder,
thread::ProcessThreadStackCallback& thread_stack_callback) {
uintptr_t stack_pointer;
asm volatile("mrs %0, msp\n" : "=r"(stack_pointer));
@@ -130,7 +131,7 @@ Status SnapshotMainStackThread(
const pw_cpu_exception_State& cpu_state,
uintptr_t stack_low_addr,
uintptr_t stack_high_addr,
- thread::SnapshotThreadInfo::StreamEncoder& encoder,
+ thread::proto::SnapshotThreadInfo::StreamEncoder& encoder,
thread::ProcessThreadStackCallback& thread_stack_callback) {
if (!MainStackActive(cpu_state)) {
return OkStatus(); // Main stack wasn't active, nothing to capture.
diff --git a/pw_cpu_exception_cortex_m/support.cc b/pw_cpu_exception_cortex_m/support.cc
index 719ac5b61..8de223308 100644
--- a/pw_cpu_exception_cortex_m/support.cc
+++ b/pw_cpu_exception_cortex_m/support.cc
@@ -16,7 +16,6 @@
#include <cinttypes>
#include <cstdint>
-#include <span>
#include "pw_cpu_exception_cortex_m/cpu_state.h"
#include "pw_cpu_exception_cortex_m/util.h"
@@ -24,19 +23,18 @@
#include "pw_cpu_exception_cortex_m_private/cortex_m_constants.h"
#include "pw_log/log.h"
#include "pw_preprocessor/arch.h"
+#include "pw_span/span.h"
#include "pw_string/string_builder.h"
namespace pw::cpu_exception {
-std::span<const uint8_t> RawFaultingCpuState(
+span<const uint8_t> RawFaultingCpuState(
const pw_cpu_exception_State& cpu_state) {
- return std::span(reinterpret_cast<const uint8_t*>(&cpu_state),
- sizeof(cpu_state));
+ return span(reinterpret_cast<const uint8_t*>(&cpu_state), sizeof(cpu_state));
}
// Using this function adds approximately 100 bytes to binary size.
-void ToString(const pw_cpu_exception_State& cpu_state,
- const std::span<char>& dest) {
+void ToString(const pw_cpu_exception_State& cpu_state, const span<char>& dest) {
StringBuilder builder(dest);
const cortex_m::ExceptionRegisters& base = cpu_state.base;
const cortex_m::ExtraRegisters& extended = cpu_state.extended;
diff --git a/pw_crypto/BUILD.bazel b/pw_crypto/BUILD.bazel
index d8fc6f34c..f0fb37245 100644
--- a/pw_crypto/BUILD.bazel
+++ b/pw_crypto/BUILD.bazel
@@ -32,7 +32,17 @@ pw_cc_facade(
deps = [
"//pw_assert",
"//pw_bytes",
+ "//pw_log",
"//pw_status",
+ "//pw_stream",
+ ],
+)
+
+pw_cc_library(
+ name = "sha256",
+ deps = [
+ ":sha256_facade",
+ "@pigweed_config//:pw_crypto_sha256_backend",
],
)
@@ -43,7 +53,9 @@ pw_cc_library(
"public/pw_crypto/sha256_mbedtls.h",
"public_overrides/mbedtls/pw_crypto/sha256_backend.h",
],
- includes = ["public_overrides"],
+ includes = ["public_overrides/mbedtls"],
+ # TODO(b/236321905): Requires BUILD.bazel files for mbedtls
+ tags = ["manual"],
deps = [":sha256_facade"],
)
@@ -54,15 +66,22 @@ pw_cc_library(
"public/pw_crypto/sha256_boringssl.h",
"public_overrides/boringssl/pw_crypto/sha256_backend.h",
],
- includes = ["public_overrides"],
- deps = [":sha256_facade"],
+ includes = [
+ "public",
+ "public_overrides/boringssl",
+ ],
+ deps = [
+ ":sha256_facade",
+ "//pw_log",
+ "@boringssl//:ssl",
+ ],
)
pw_cc_test(
name = "sha256_test",
srcs = ["sha256_test.cc"],
deps = [
- ":sha256_facade",
+ ":sha256",
"//pw_unit_test",
],
)
@@ -74,15 +93,19 @@ pw_cc_library(
"public/pw_crypto/sha256_mock.h",
"public_overrides/mock/pw_crypto/sha256_backend.h",
],
- includes = ["public_overrides"],
+ includes = [
+ "public",
+ "public_overrides/mock",
+ ],
deps = [":sha256_facade"],
)
pw_cc_test(
name = "sha256_mock_test",
srcs = ["sha256_mock_test.cc"],
+ # TODO(b/236321905): Requires BUILD.bazel files for mbedtls
+ tags = ["manual"],
deps = [
- ":sha256_facade",
":sha256_mock",
"//pw_unit_test",
],
@@ -101,31 +124,48 @@ pw_cc_facade(
)
pw_cc_library(
+ name = "ecdsa",
+ deps = [
+ ":ecdsa_facade",
+ "@pigweed_config//:pw_crypto_ecdsa_backend",
+ ],
+)
+
+pw_cc_library(
name = "ecdsa_mbedtls",
srcs = ["ecdsa_mbedtls.cc"],
+ # TODO(b/236321905): Requires BUILD.bazel files for mbedtls
+ tags = ["manual"],
deps = [":ecdsa_facade"],
)
pw_cc_library(
name = "ecdsa_boringssl",
srcs = ["ecdsa_boringssl.cc"],
- deps = [":ecdsa_facade"],
+ deps = [
+ ":ecdsa_facade",
+ "//pw_log",
+ "@boringssl//:ssl",
+ ],
)
pw_cc_library(
name = "ecdsa_uecc",
srcs = [
"ecdsa_uecc.cc",
- "micro-ecc/uEDD.c",
],
- deps = [":ecdsa_facade"],
+ deps = [
+ ":ecdsa_facade",
+ "//pw_log",
+ "@micro_ecc//:uecc",
+ ],
)
pw_cc_test(
name = "ecdsa_test",
srcs = ["ecdsa_test.cc"],
deps = [
- ":ecdsa_facade",
+ ":ecdsa",
"//pw_unit_test",
],
)
diff --git a/pw_crypto/BUILD.gn b/pw_crypto/BUILD.gn
index 38c10c0f1..919ab7bf0 100644
--- a/pw_crypto/BUILD.gn
+++ b/pw_crypto/BUILD.gn
@@ -19,6 +19,7 @@ import("$dir_pw_build/facade.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_crypto/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/micro_ecc/micro_ecc.gni")
import("$dir_pw_unit_test/test.gni")
config("default_config") {
@@ -44,7 +45,7 @@ pw_doc_group("docs") {
report_deps = [ ":size_report" ]
}
-pw_size_report("size_report") {
+pw_size_diff("size_report") {
title = "pw::crypto Size Report"
base = "$dir_pw_bloat:bloat_base"
@@ -83,6 +84,7 @@ pw_test_group("tests") {
":sha256_test",
":sha256_mock_test",
":ecdsa_test",
+ ":ecdsa_uecc_little_endian_test",
]
}
@@ -193,8 +195,28 @@ pw_source_set("ecdsa_uecc") {
public_deps = [ ":ecdsa.facade" ]
}
+pw_source_set("ecdsa_uecc_little_endian") {
+ sources = [ "ecdsa_uecc.cc" ]
+ deps = [
+ "$dir_pw_log",
+ "$dir_pw_third_party/micro_ecc:micro_ecc_little_endian",
+ ]
+ public_deps = [ ":ecdsa.facade" ]
+}
+
+# This test targets the specific backend pointed to by
+# pw_crypto_ECDSA_BACKEND.
pw_test("ecdsa_test") {
enable_if = pw_crypto_ECDSA_BACKEND != ""
deps = [ ":ecdsa" ]
sources = [ "ecdsa_test.cc" ]
}
+
+# This test targets the micro_ecc little endian backend specifically.
+#
+# TODO(b/273819841) deduplicate all backend tests.
+pw_test("ecdsa_uecc_little_endian_test") {
+ enable_if = dir_pw_third_party_micro_ecc != ""
+ sources = [ "ecdsa_test.cc" ]
+ deps = [ ":ecdsa_uecc_little_endian" ]
+}
diff --git a/pw_crypto/docs.rst b/pw_crypto/docs.rst
index 6ec17ad13..1230cd35d 100644
--- a/pw_crypto/docs.rst
+++ b/pw_crypto/docs.rst
@@ -98,10 +98,13 @@ configured.
# Install and configure MbedTLS
pw package install mbedtls
- gn gen out \
- --args='dir_pw_third_party_mbedtls="//.environment/packages/mbedtls" \
- pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_mbedtls" \
- pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_mbedtls"'
+ gn gen out --args='
+ import("//build_overrides/pigweed_environment.gni")
+
+ dir_pw_third_party_mbedtls=pw_env_setup_PACKAGE_ROOT+"/mbedtls"
+ pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_mbedtls"
+ pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_mbedtls"
+ '
ninja -C out
@@ -143,10 +146,13 @@ configured.
# Install and configure BoringSSL
pw package install boringssl
- gn gen out \
- --args='dir_pw_third_party_boringssl="//.environment/packages/boringssl" \
- pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_boringssl" \
- pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_boringssl"'
+ gn gen out --args='
+ import("//build_overrides/pigweed_environment.gni")
+
+ dir_pw_third_party_boringssl=pw_env_setup_PACKAGE_ROOT+"/boringssl"
+ pw_crypto_SHA256_BACKEND="//pw_crypto:sha256_boringssl"
+ pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_boringssl"
+ '
ninja -C out
@@ -162,7 +168,18 @@ To select Micro ECC, the library needs to be installed and configured.
# Install and configure Micro ECC
pw package install micro-ecc
- gn gen out --args='dir_pw_third_party_micro_ecc="//.environment/packages/micro-ecc" pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_uecc"'
+ gn gen out --args='
+ import("//build_overrides/pigweed_environment.gni")
+
+ dir_pw_third_party_micro_ecc=pw_env_setup_PACKAGE_ROOT+"/micro-ecc"
+ pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_uecc"
+ '
+
+The default micro-ecc backend uses big endian as is standard practice. It also
+has a little-endian configuration which can be used to slightly reduce call
+stack frame use and/or when non pw_crypto clients use the same micro-ecc
+with a little-endian configuration. The little-endian version of micro-ecc
+can be selected with ``pw_crypto_ECDSA_BACKEND="//pw_crypto:ecdsa_uecc_little_endian"``
Note Micro-ECC does not implement any hashing functions, so you will need to use other backends for SHA256 functionality if needed.
diff --git a/pw_crypto/ecdsa_boringssl.cc b/pw_crypto/ecdsa_boringssl.cc
index 4d361e3fa..6f87d9cfa 100644
--- a/pw_crypto/ecdsa_boringssl.cc
+++ b/pw_crypto/ecdsa_boringssl.cc
@@ -14,10 +14,18 @@
#define PW_LOG_MODULE_NAME "ECDSA-BSSL"
#define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
+#include "pw_preprocessor/compiler.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wcast-qual");
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wgnu-anonymous-struct");
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wnested-anon-types");
#include "openssl/bn.h"
#include "openssl/ec.h"
#include "openssl/ecdsa.h"
#include "openssl/nid.h"
+PW_MODIFY_DIAGNOSTICS_POP();
+
#include "pw_crypto/ecdsa.h"
#include "pw_log/log.h"
diff --git a/pw_crypto/ecdsa_test.cc b/pw_crypto/ecdsa_test.cc
index 63d1fd0e3..0d717ad7b 100644
--- a/pw_crypto/ecdsa_test.cc
+++ b/pw_crypto/ecdsa_test.cc
@@ -21,7 +21,7 @@
namespace pw::crypto::ecdsa {
namespace {
-#define AS_BYTES(s) std::as_bytes(std::span(s, sizeof(s) - 1))
+#define AS_BYTES(s) as_bytes(span(s, sizeof(s) - 1))
#define ASSERT_OK(expr) ASSERT_EQ(OkStatus(), expr)
#define ASSERT_FAIL(expr) ASSERT_NE(OkStatus(), expr)
diff --git a/pw_crypto/ecdsa_uecc.cc b/pw_crypto/ecdsa_uecc.cc
index 937d79d56..912a28b44 100644
--- a/pw_crypto/ecdsa_uecc.cc
+++ b/pw_crypto/ecdsa_uecc.cc
@@ -14,6 +14,8 @@
#define PW_LOG_MODULE_NAME "ECDSA-UECC"
#define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
+#include <cstring>
+
#include "pw_crypto/ecdsa.h"
#include "pw_log/log.h"
#include "uECC.h"
@@ -21,33 +23,62 @@
namespace pw::crypto::ecdsa {
constexpr size_t kP256CurveOrderBytes = 32;
+constexpr size_t kP256PublicKeySize = 2 * kP256CurveOrderBytes + 1;
+constexpr size_t kP256SignatureSize = kP256CurveOrderBytes * 2;
Status VerifyP256Signature(ConstByteSpan public_key,
ConstByteSpan digest,
ConstByteSpan signature) {
- const uint8_t* public_key_bytes =
- reinterpret_cast<const uint8_t*>(public_key.data());
- const uint8_t* digest_bytes = reinterpret_cast<const uint8_t*>(digest.data());
- const uint8_t* signature_bytes =
- reinterpret_cast<const uint8_t*>(signature.data());
-
- uECC_Curve curve = uECC_secp256r1();
+ // Signature expected in raw format (r||s)
+ if (signature.size() != kP256SignatureSize) {
+ PW_LOG_DEBUG("Bad signature format");
+ return Status::InvalidArgument();
+ }
// Supports SEC 1 uncompressed form (04||X||Y) only.
- if (public_key.size() != (2 * kP256CurveOrderBytes + 1) ||
- public_key_bytes[0] != 0x04) {
+ if (public_key.size() != kP256PublicKeySize ||
+ std::to_integer<uint8_t>(public_key.data()[0]) != 0x04) {
PW_LOG_DEBUG("Bad public key format");
return Status::InvalidArgument();
}
- // Make sure the public key is on the curve.
- if (!uECC_valid_public_key(public_key_bytes + 1, curve)) {
- return Status::InvalidArgument();
- }
+#if defined(uECC_VLI_NATIVE_LITTLE_ENDIAN) && uECC_VLI_NATIVE_LITTLE_ENDIAN
+ // uECC_VLI_NATIVE_LITTLE_ENDIAN is defined with a non-zero value when
+ // pw_crypto_ECDSA_BACKEND is set to "//pw_crypto:ecdsa_uecc_little_endian".
+ //
+ // Since pw_crypto APIs are big endian only (standard practice), here we
+ // need to convert input parameters to little endian.
+ //
+ // Additionally uECC requires these little endian buffers to be word aligned
+ // in case unaligned accesses are not supported by the hardware. We choose
+ // the maximum 8-byte alignment to avoid referrencing internal uECC headers.
+ alignas(8) uint8_t signature_bytes[kP256SignatureSize];
+ memcpy(signature_bytes, signature.data(), sizeof(signature_bytes));
+ std::reverse(signature_bytes, signature_bytes + kP256CurveOrderBytes); // r
+ std::reverse(signature_bytes + kP256CurveOrderBytes,
+ signature_bytes + sizeof(signature_bytes)); // s
- // Signature expected in raw format (r||s)
- if (signature.size() != kP256CurveOrderBytes * 2) {
- PW_LOG_DEBUG("Bad signature format");
+ alignas(8) uint8_t public_key_bytes[kP256PublicKeySize - 1];
+ memcpy(public_key_bytes, public_key.data() + 1, sizeof(public_key_bytes));
+ std::reverse(public_key_bytes, public_key_bytes + kP256CurveOrderBytes); // X
+ std::reverse(public_key_bytes + kP256CurveOrderBytes,
+ public_key_bytes + sizeof(public_key_bytes)); // Y
+
+ alignas(8) uint8_t digest_bytes[kP256CurveOrderBytes];
+ memcpy(digest_bytes, digest.data(), sizeof(digest_bytes));
+ std::reverse(digest_bytes, digest_bytes + sizeof(digest_bytes));
+#else
+ const uint8_t* public_key_bytes =
+ reinterpret_cast<const uint8_t*>(public_key.data()) + 1;
+ const uint8_t* digest_bytes = reinterpret_cast<const uint8_t*>(digest.data());
+ const uint8_t* signature_bytes =
+ reinterpret_cast<const uint8_t*>(signature.data());
+#endif // uECC_VLI_NATIVE_LITTLE_ENDIAN
+
+ uECC_Curve curve = uECC_secp256r1();
+ // Make sure the public key is on the curve.
+ if (!uECC_valid_public_key(public_key_bytes, curve)) {
+ PW_LOG_DEBUG("Bad public key curve");
return Status::InvalidArgument();
}
@@ -59,7 +90,7 @@ Status VerifyP256Signature(ConstByteSpan public_key,
}
// Verify the signature.
- if (!uECC_verify(public_key_bytes + 1,
+ if (!uECC_verify(public_key_bytes,
digest_bytes,
digest.size(),
signature_bytes,
diff --git a/pw_crypto/public/pw_crypto/sha256_boringssl.h b/pw_crypto/public/pw_crypto/sha256_boringssl.h
index 50b2ffdce..b157c8ba3 100644
--- a/pw_crypto/public/pw_crypto/sha256_boringssl.h
+++ b/pw_crypto/public/pw_crypto/sha256_boringssl.h
@@ -14,7 +14,17 @@
#pragma once
+#include "pw_preprocessor/compiler.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wcast-qual");
+// "-Wpedantic" because some downstream compiler don't recognize the following
+// two commented-out options.
+// PW_MODIFY_DIAGNOSTIC(ignored, "-Wgnu-anonymous-struct");
+// PW_MODIFY_DIAGNOSTIC(ignored, "-Wnested-anon-types");
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wpedantic");
#include "openssl/sha.h"
+PW_MODIFY_DIAGNOSTICS_POP();
namespace pw::crypto::sha256::backend {
diff --git a/pw_crypto/sha256_mock.cc b/pw_crypto/sha256_mock.cc
index ed289eb04..d6e187396 100644
--- a/pw_crypto/sha256_mock.cc
+++ b/pw_crypto/sha256_mock.cc
@@ -12,6 +12,8 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include "pw_crypto/sha256_mock.h"
+
#include "pw_crypto/sha256.h"
#include "pw_status/status.h"
diff --git a/pw_crypto/sha256_mock_test.cc b/pw_crypto/sha256_mock_test.cc
index 77c065b74..66705d373 100644
--- a/pw_crypto/sha256_mock_test.cc
+++ b/pw_crypto/sha256_mock_test.cc
@@ -21,7 +21,7 @@
namespace pw::crypto::sha256 {
namespace {
-#define AS_BYTES(str) std::as_bytes(std::span(str, sizeof(str) - 1))
+#define AS_BYTES(str) as_bytes(span(str, sizeof(str) - 1))
#define ASSERT_OK(expr) ASSERT_EQ(OkStatus(), expr)
#define ASSERT_FAIL(expr) ASSERT_NE(OkStatus(), expr)
diff --git a/pw_crypto/sha256_test.cc b/pw_crypto/sha256_test.cc
index 32e8566e8..e9899f096 100644
--- a/pw_crypto/sha256_test.cc
+++ b/pw_crypto/sha256_test.cc
@@ -25,7 +25,7 @@ namespace {
#define ASSERT_OK(expr) ASSERT_EQ(OkStatus(), expr)
#define ASSERT_FAIL(expr) ASSERT_NE(OkStatus(), expr)
-#define AS_BYTES(s) std::as_bytes(std::span(s, sizeof(s) - 1))
+#define AS_BYTES(s) as_bytes(span(s, sizeof(s) - 1))
// Generated in Python 3 with:
// `hashlib.sha256('Hello, Pigweed!'.encode('ascii')).hexdigest()`.
diff --git a/pw_crypto/size_report/BUILD.bazel b/pw_crypto/size_report/BUILD.bazel
index 1166d1e58..f49a3406b 100644
--- a/pw_crypto/size_report/BUILD.bazel
+++ b/pw_crypto/size_report/BUILD.bazel
@@ -24,9 +24,17 @@ licenses(["notice"])
pw_cc_binary(
name = "sha256_simple",
srcs = ["sha256_simple.cc"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_crypto:sha256",
+ ],
)
pw_cc_binary(
name = "ecdsa_p256_verify",
srcs = ["ecdsa_p256_verify.cc"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_crypto:ecdsa",
+ ],
)
diff --git a/pw_crypto/size_report/ecdsa_p256_verify.cc b/pw_crypto/size_report/ecdsa_p256_verify.cc
index f50e01bba..7818a6eb8 100644
--- a/pw_crypto/size_report/ecdsa_p256_verify.cc
+++ b/pw_crypto/size_report/ecdsa_p256_verify.cc
@@ -18,7 +18,7 @@
#include "pw_crypto/ecdsa.h"
namespace {
-#define STR_TO_BYTES(s) std::as_bytes(std::span(s, sizeof(s) - 1))
+#define STR_TO_BYTES(s) pw::as_bytes(pw::span(s, sizeof(s) - 1))
// The SHA256 digest of "Hello, Pigweed!", 32 bytes.
#define DIGEST \
diff --git a/pw_crypto/size_report/sha256_simple.cc b/pw_crypto/size_report/sha256_simple.cc
index c8d6ceccc..15e67d720 100644
--- a/pw_crypto/size_report/sha256_simple.cc
+++ b/pw_crypto/size_report/sha256_simple.cc
@@ -19,7 +19,7 @@
namespace {
#define MESSAGE "Hello, Pigweed!"
-#define STR_TO_BYTES(s) std::as_bytes(std::span(s, std::strlen(s)))
+#define STR_TO_BYTES(s) pw::as_bytes(pw::span(s, std::strlen(s)))
} // namespace
int main() {
diff --git a/pw_digital_io/BUILD.bazel b/pw_digital_io/BUILD.bazel
new file mode 100644
index 000000000..a4632a5e9
--- /dev/null
+++ b/pw_digital_io/BUILD.bazel
@@ -0,0 +1,48 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_digital_io",
+ srcs = ["digital_io.cc"],
+ hdrs = [
+ "public/pw_digital_io/digital_io.h",
+ "public/pw_digital_io/internal/conversions.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_assert",
+ "//pw_function",
+ "//pw_result",
+ "//pw_status",
+ ],
+)
+
+pw_cc_test(
+ name = "digital_io_test",
+ srcs = ["digital_io_test.cc"],
+ deps = [
+ ":pw_digital_io",
+ "//pw_unit_test",
+ ],
+)
diff --git a/pw_digital_io/BUILD.gn b/pw_digital_io/BUILD.gn
new file mode 100644
index 000000000..2cf894932
--- /dev/null
+++ b/pw_digital_io/BUILD.gn
@@ -0,0 +1,53 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("pw_digital_io") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "public/pw_digital_io/digital_io.h",
+ "public/pw_digital_io/internal/conversions.h",
+ ]
+ sources = [ "digital_io.cc" ]
+ public_deps = [
+ dir_pw_assert,
+ dir_pw_function,
+ dir_pw_result,
+ dir_pw_status,
+ ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":digital_io_test" ]
+}
+
+pw_test("digital_io_test") {
+ sources = [ "digital_io_test.cc" ]
+ deps = [ ":pw_digital_io" ]
+}
diff --git a/pw_web_ui/web_bundle.bzl b/pw_digital_io/CMakeLists.txt
index d785bdbcc..8c1dbe181 100644
--- a/pw_web_ui/web_bundle.bzl
+++ b/pw_digital_io/CMakeLists.txt
@@ -11,22 +11,33 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
-"""Create web bundle."""
-load("@npm//@bazel/rollup:index.bzl", "rollup_bundle")
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-def web_bundle(name, deps, entry_point):
- rollup_bundle(
- name = name,
- deps = deps + [
- "@npm//@rollup/plugin-node-resolve",
- "@npm//@rollup/plugin-commonjs",
- "@npm//rollup-plugin-node-builtins",
- "@npm//rollup-plugin-node-globals",
- "@npm//rollup-plugin-sourcemaps",
- ],
- entry_point = entry_point,
- config_file = "//pw_web_ui:rollup.config.js",
- sourcemap = "inline",
- format = "cjs",
- )
+pw_add_library(pw_digital_io STATIC
+ HEADERS
+ public/pw_digital_io/digital_io.h
+ public/pw_digital_io/internal/conversions.h
+ PUBLIC_INCLUDES
+ public
+ SOURCES
+ digital_io.cc
+ PUBLIC_DEPS
+ pw_assert
+ pw_function
+ pw_result
+ pw_status
+)
+if(Zephyr_FOUND AND CONFIG_PIGWEED_DIGITAL_IO)
+ zephyr_link_libraries(pw_digital_io)
+endif()
+
+pw_add_test(pw_digital_io.stream_test
+ SOURCES
+ digital_io_test.cc
+ PRIVATE_DEPS
+ pw_digital_io
+ GROUPS
+ modules
+ pw_digital_io
+)
diff --git a/pw_digital_io/OWNERS b/pw_digital_io/OWNERS
new file mode 100644
index 000000000..dcdb6bdf8
--- /dev/null
+++ b/pw_digital_io/OWNERS
@@ -0,0 +1 @@
+amarkov@google.com
diff --git a/pw_digital_io/README.md b/pw_digital_io/README.md
new file mode 100644
index 000000000..7aee49542
--- /dev/null
+++ b/pw_digital_io/README.md
@@ -0,0 +1,7 @@
+This directory contains the `pw` Digital IO Hardware Abstraction Layer (HAL).
+This HAL defines interfaces for working with Digital IO lines that provide
+different combinations of capabilities (input, output, and/or interrupts).
+Hardware specific backends provide implementations of these interfaces for
+different hardware platforms.
+
+Warning: This module is under construction and may not be ready for use.
diff --git a/pw_digital_io/digital_io.cc b/pw_digital_io/digital_io.cc
new file mode 100644
index 000000000..b845d3f33
--- /dev/null
+++ b/pw_digital_io/digital_io.cc
@@ -0,0 +1,60 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_digital_io/digital_io.h"
+
+namespace pw::digital_io {
+
+Status DigitalInterrupt::DoSetState(State) {
+ PW_CRASH("DoSetState not implemented");
+}
+Result<State> DigitalInterrupt::DoGetState() {
+ PW_CRASH("DoGetState not implemented");
+}
+
+Status DigitalIn::DoSetState(State) { PW_CRASH("DoSetState not implemented"); }
+Status DigitalIn::DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) {
+ PW_CRASH("DoSetInterruptHandler not implemented");
+}
+Status DigitalIn::DoEnableInterruptHandler(bool) {
+ PW_CRASH("DoEnableInterruptHandler not implemented");
+}
+
+Status DigitalInInterrupt::DoSetState(State) {
+ PW_CRASH("DoSetState not implemented");
+}
+
+Result<State> DigitalOut::DoGetState() {
+ PW_CRASH("DoGetState not implemented");
+}
+Status DigitalOut::DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) {
+ PW_CRASH("DoSetInterruptHandler not implemented");
+}
+Status DigitalOut::DoEnableInterruptHandler(bool) {
+ PW_CRASH("DoEnableInterruptHandler not implemented");
+}
+
+Result<State> DigitalOutInterrupt::DoGetState() {
+ PW_CRASH("DoGetState not implemented");
+}
+
+Status DigitalInOut::DoSetInterruptHandler(InterruptTrigger,
+ InterruptHandler&&) {
+ PW_CRASH("DoSetInterruptHandler not implemented");
+}
+Status DigitalInOut::DoEnableInterruptHandler(bool) {
+ PW_CRASH("DoEnableInterruptHandler not implemented");
+}
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io/digital_io_test.cc b/pw_digital_io/digital_io_test.cc
new file mode 100644
index 000000000..edbf59261
--- /dev/null
+++ b/pw_digital_io/digital_io_test.cc
@@ -0,0 +1,350 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_digital_io/digital_io.h"
+
+#include "gtest/gtest.h"
+#include "pw_status/status.h"
+
+namespace pw::digital_io {
+namespace {
+
+// The base class should be compact.
+static_assert(sizeof(DigitalIoOptional) <= 2 * sizeof(void*),
+ "DigitalIo should be no larger than two pointers (vtable pointer "
+ "& packed members)");
+
+// Skeleton implementations to test DigitalIo methods.
+class TestDigitalInterrupt : public DigitalInterrupt {
+ public:
+ TestDigitalInterrupt() = default;
+
+ private:
+ Status DoEnable(bool) override { return OkStatus(); }
+
+ Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) override {
+ return OkStatus();
+ }
+ Status DoEnableInterruptHandler(bool) override { return OkStatus(); }
+};
+
+class TestDigitalIn : public DigitalIn {
+ public:
+ TestDigitalIn() : state_(State::kInactive) {}
+
+ private:
+ Status DoEnable(bool) override { return OkStatus(); }
+ Result<State> DoGetState() override { return state_; }
+
+ const State state_;
+};
+
+class TestDigitalInInterrupt : public DigitalInInterrupt {
+ public:
+ TestDigitalInInterrupt() : state_(State::kInactive) {}
+
+ private:
+ Status DoEnable(bool) override { return OkStatus(); }
+ Result<State> DoGetState() override { return state_; }
+
+ Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) override {
+ return OkStatus();
+ }
+ Status DoEnableInterruptHandler(bool) override { return OkStatus(); }
+
+ const State state_;
+};
+
+class TestDigitalOut : public DigitalOut {
+ public:
+ TestDigitalOut() {}
+
+ private:
+ Status DoEnable(bool) override { return OkStatus(); }
+ Status DoSetState(State) override { return OkStatus(); }
+};
+
+class TestDigitalOutInterrupt : public DigitalOutInterrupt {
+ public:
+ TestDigitalOutInterrupt() {}
+
+ private:
+ Status DoEnable(bool) override { return OkStatus(); }
+ Status DoSetState(State) override { return OkStatus(); }
+
+ Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) override {
+ return OkStatus();
+ }
+ Status DoEnableInterruptHandler(bool) override { return OkStatus(); }
+};
+
+class TestDigitalInOut : public DigitalInOut {
+ public:
+ TestDigitalInOut() : state_(State::kInactive) {}
+
+ private:
+ Status DoEnable(bool) override { return OkStatus(); }
+ Result<State> DoGetState() override { return state_; }
+ Status DoSetState(State state) override {
+ state_ = state;
+ return OkStatus();
+ }
+
+ State state_;
+};
+
+class TestDigitalInOutInterrupt : public DigitalInOutInterrupt {
+ public:
+ TestDigitalInOutInterrupt() : state_(State::kInactive) {}
+
+ private:
+ Status DoEnable(bool) override { return OkStatus(); }
+ Result<State> DoGetState() override { return state_; }
+ Status DoSetState(State state) override {
+ state_ = state;
+ return OkStatus();
+ }
+
+ Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) override {
+ return OkStatus();
+ }
+ Status DoEnableInterruptHandler(bool) override { return OkStatus(); }
+
+ State state_;
+};
+
+// Test conversions between different interfaces.
+static_assert(!std::is_convertible<TestDigitalInterrupt, DigitalIn&>());
+static_assert(!std::is_convertible<TestDigitalInterrupt, DigitalOut&>());
+static_assert(
+ !std::is_convertible<TestDigitalInterrupt, DigitalInInterrupt&>());
+static_assert(
+ !std::is_convertible<TestDigitalInterrupt, DigitalOutInterrupt&>());
+static_assert(
+ !std::is_convertible<TestDigitalInterrupt, DigitalInOutInterrupt&>());
+
+static_assert(!std::is_convertible<TestDigitalIn, DigitalOut&>());
+static_assert(!std::is_convertible<TestDigitalIn, DigitalInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalIn, DigitalInInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalIn, DigitalOutInterrupt&>());
+
+static_assert(std::is_convertible<TestDigitalInInterrupt, DigitalIn&>());
+static_assert(!std::is_convertible<TestDigitalInInterrupt, DigitalOut&>());
+static_assert(std::is_convertible<TestDigitalInInterrupt, DigitalInterrupt&>());
+static_assert(
+ !std::is_convertible<TestDigitalInInterrupt, DigitalOutInterrupt&>());
+
+static_assert(!std::is_convertible<TestDigitalOut, DigitalIn&>());
+static_assert(!std::is_convertible<TestDigitalOut, DigitalInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalOut, DigitalInInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalOut, DigitalOutInterrupt&>());
+
+static_assert(!std::is_convertible<TestDigitalOutInterrupt, DigitalIn&>());
+static_assert(std::is_convertible<TestDigitalOutInterrupt, DigitalOut&>());
+static_assert(
+ std::is_convertible<TestDigitalOutInterrupt, DigitalInterrupt&>());
+static_assert(
+ !std::is_convertible<TestDigitalOutInterrupt, DigitalInInterrupt&>());
+
+static_assert(std::is_convertible<TestDigitalInOut, DigitalIn&>());
+static_assert(std::is_convertible<TestDigitalInOut, DigitalOut&>());
+static_assert(!std::is_convertible<TestDigitalInOut, DigitalInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalInOut, DigitalInInterrupt&>());
+static_assert(!std::is_convertible<TestDigitalInOut, DigitalOutInterrupt&>());
+
+static_assert(std::is_convertible<TestDigitalInOutInterrupt, DigitalIn&>());
+static_assert(std::is_convertible<TestDigitalInOutInterrupt, DigitalOut&>());
+static_assert(
+ std::is_convertible<TestDigitalInOutInterrupt, DigitalInterrupt&>());
+static_assert(
+ std::is_convertible<TestDigitalInOutInterrupt, DigitalInInterrupt&>());
+static_assert(
+ std::is_convertible<TestDigitalInOutInterrupt, DigitalOutInterrupt&>());
+
+void FakeInterruptHandler(State) {}
+
+template <typename Line>
+void TestInput(Line& line) {
+ ASSERT_EQ(OkStatus(), line.Enable());
+
+ auto state_result = line.GetState();
+ ASSERT_EQ(OkStatus(), state_result.status());
+ ASSERT_EQ(State::kInactive, state_result.value());
+
+ auto active_result = line.IsStateActive();
+ ASSERT_EQ(OkStatus(), active_result.status());
+ ASSERT_EQ(false, active_result.value());
+
+ ASSERT_EQ(OkStatus(), line.Disable());
+}
+
+template <typename Line>
+void TestOutput(Line& line) {
+ ASSERT_EQ(OkStatus(), line.Enable());
+
+ ASSERT_EQ(OkStatus(), line.SetState(State::kActive));
+ ASSERT_EQ(OkStatus(), line.SetState(State::kInactive));
+
+ ASSERT_EQ(OkStatus(), line.SetStateActive());
+ ASSERT_EQ(OkStatus(), line.SetStateInactive());
+
+ ASSERT_EQ(OkStatus(), line.Disable());
+}
+
+template <typename Line>
+void TestOutputReadback(Line& line) {
+ ASSERT_EQ(OkStatus(), line.Enable());
+
+ ASSERT_EQ(OkStatus(), line.SetState(State::kActive));
+ auto state_result = line.GetState();
+ ASSERT_EQ(OkStatus(), state_result.status());
+ ASSERT_EQ(State::kActive, state_result.value());
+
+ ASSERT_EQ(OkStatus(), line.SetState(State::kInactive));
+ state_result = line.GetState();
+ ASSERT_EQ(OkStatus(), state_result.status());
+ ASSERT_EQ(State::kInactive, state_result.value());
+
+ ASSERT_EQ(OkStatus(), line.SetStateActive());
+ auto active_result = line.IsStateActive();
+ ASSERT_EQ(OkStatus(), active_result.status());
+ ASSERT_EQ(true, active_result.value());
+
+ ASSERT_EQ(OkStatus(), line.SetStateInactive());
+ active_result = line.IsStateActive();
+ ASSERT_EQ(OkStatus(), active_result.status());
+ ASSERT_EQ(false, active_result.value());
+
+ ASSERT_EQ(OkStatus(), line.Disable());
+}
+
+template <typename Line>
+void TestInterrupt(Line& line) {
+ ASSERT_EQ(OkStatus(), line.Enable());
+
+ ASSERT_EQ(OkStatus(),
+ line.SetInterruptHandler(InterruptTrigger::kBothEdges,
+ FakeInterruptHandler));
+ ASSERT_EQ(OkStatus(), line.EnableInterruptHandler());
+ ASSERT_EQ(OkStatus(), line.EnableInterruptHandler());
+ ASSERT_EQ(OkStatus(), line.DisableInterruptHandler());
+ ASSERT_EQ(OkStatus(), line.ClearInterruptHandler());
+
+ ASSERT_EQ(OkStatus(), line.Disable());
+}
+
+TEST(Digital, Interrupt) {
+ TestDigitalInterrupt line;
+ DigitalIoOptional& optional_line = line;
+
+ ASSERT_EQ(false, optional_line.provides_input());
+ ASSERT_EQ(false, optional_line.provides_output());
+ ASSERT_EQ(true, optional_line.provides_interrupt());
+
+ TestInterrupt(line);
+ TestInterrupt(optional_line);
+}
+
+TEST(Digital, In) {
+ TestDigitalIn line;
+ DigitalIoOptional& optional_line = line;
+
+ ASSERT_EQ(true, optional_line.provides_input());
+ ASSERT_EQ(false, optional_line.provides_output());
+ ASSERT_EQ(false, optional_line.provides_interrupt());
+
+ TestInput(line);
+ TestInput(optional_line);
+}
+
+TEST(Digital, InInterrupt) {
+ TestDigitalInInterrupt line;
+ DigitalIoOptional& optional_line = line;
+
+ ASSERT_EQ(true, optional_line.provides_input());
+ ASSERT_EQ(false, optional_line.provides_output());
+ ASSERT_EQ(true, optional_line.provides_interrupt());
+
+ TestInput(line);
+ TestInterrupt(line);
+
+ TestInput(optional_line);
+ TestInterrupt(optional_line);
+}
+
+TEST(Digital, Out) {
+ TestDigitalOut line;
+ DigitalIoOptional& optional_line = line;
+
+ ASSERT_EQ(false, optional_line.provides_input());
+ ASSERT_EQ(true, optional_line.provides_output());
+ ASSERT_EQ(false, optional_line.provides_interrupt());
+
+ TestOutput(line);
+ TestOutput(optional_line);
+}
+
+TEST(Digital, OutInterrupt) {
+ TestDigitalOutInterrupt line;
+ DigitalIoOptional& optional_line = line;
+
+ ASSERT_EQ(false, optional_line.provides_input());
+ ASSERT_EQ(true, optional_line.provides_output());
+ ASSERT_EQ(true, optional_line.provides_interrupt());
+
+ TestOutput(line);
+ TestInterrupt(line);
+
+ TestOutput(optional_line);
+ TestInterrupt(optional_line);
+}
+
+TEST(Digital, InOut) {
+ TestDigitalInOut line;
+ DigitalIoOptional& optional_line = line;
+
+ ASSERT_EQ(true, optional_line.provides_input());
+ ASSERT_EQ(true, optional_line.provides_output());
+ ASSERT_EQ(false, optional_line.provides_interrupt());
+
+ TestInput(line);
+ TestOutput(line);
+ TestOutputReadback(line);
+
+ TestInput(optional_line);
+ TestOutput(optional_line);
+ TestOutputReadback(optional_line);
+}
+
+TEST(DigitalIo, InOutInterrupt) {
+ TestDigitalInOutInterrupt line;
+ DigitalIoOptional& optional_line = line;
+
+ ASSERT_EQ(true, optional_line.provides_input());
+ ASSERT_EQ(true, optional_line.provides_output());
+ ASSERT_EQ(true, optional_line.provides_interrupt());
+
+ TestInput(line);
+ TestOutput(line);
+ TestOutputReadback(line);
+ TestInterrupt(line);
+
+ TestInput(optional_line);
+ TestOutput(optional_line);
+ TestOutputReadback(optional_line);
+ TestInterrupt(optional_line);
+}
+
+} // namespace
+} // namespace pw::digital_io
diff --git a/pw_digital_io/docs.rst b/pw_digital_io/docs.rst
new file mode 100644
index 000000000..46f3ba077
--- /dev/null
+++ b/pw_digital_io/docs.rst
@@ -0,0 +1,284 @@
+.. _module-pw_digital_io:
+
+.. cpp:namespace-push:: pw::digital_io
+
+=============
+pw_digital_io
+=============
+.. warning::
+ This module is under construction and may not be ready for use.
+
+``pw_digital_io`` provides a set of interfaces for using General Purpose Input
+and Output (GPIO) lines for simple Digital I/O. This module can either be used
+directly by the application code or wrapped in a device driver for more complex
+peripherals.
+
+--------
+Overview
+--------
+The interfaces provide an abstract concept of a **Digital IO line**. The
+interfaces abstract away details about the hardware and platform-specific
+drivers. A platform-specific backend is responsible for configuring lines and
+providing an implementation of the interface that matches the capabilities and
+intended usage of the line.
+
+Example API usage:
+
+.. code-block:: cpp
+
+ using namespace pw::digital_io;
+
+ Status UpdateLedFromSwitch(const DigitalIn& switch, DigitalOut& led) {
+ PW_TRY_ASSIGN(const DigitalIo::State state, switch.GetState());
+ return led.SetState(state);
+ }
+
+ Status ListenForButtonPress(DigitalInterrupt& button) {
+ PW_TRY(button.SetInterruptHandler(Trigger::kActivatingEdge,
+ [](State sampled_state) {
+ // Handle the button press.
+ // NOTE: this may run in an interrupt context!
+ }));
+ return button.EnableInterruptHandler();
+ }
+
+-------------------------
+pw::digital_io Interfaces
+-------------------------
+There are 3 basic capabilities of a Digital IO line:
+
+* Input - Get the state of the line.
+* Output - Set the state of the line.
+* Interrupt - Register a handler that is called when a trigger happens.
+
+.. note:: **Capabilities** refer to how the line is intended to be used in a
+ particular device given its actual physical wiring, rather than the
+ theoretical capabilities of the hardware.
+
+Additionally, all lines can be *enabled* and *disabled*:
+
+* Enable - tell the hardware to apply power to an output line, connect any
+ pull-up/down resistors, etc.
+* Disable - tell the hardware to stop applying power and return the line to its
+ default state. This may save power or allow some other component to drive a
+ shared line.
+
+.. note:: The initial state of a line is implementation-defined and may not
+ match either the "enabled" or "disabled" state. Users of the API who need
+ to ensure the line is disabled (ex. output is not driving the line) should
+ explicitly call ``Disable()``.
+
+Functionality overview
+======================
+The following table summarizes the interfaces and their required functionality:
+
+.. list-table::
+ :header-rows: 1
+ :stub-columns: 1
+
+ * -
+ - Interrupts Not Required
+ - Interrupts Required
+ * - Input/Output Not Required
+ -
+ - :cpp:class:`DigitalInterrupt`
+ * - Input Required
+ - :cpp:class:`DigitalIn`
+ - :cpp:class:`DigitalInInterrupt`
+ * - Output Required
+ - :cpp:class:`DigitalOut`
+ - :cpp:class:`DigitalOutInterrupt`
+ * - Input/Output Required
+ - :cpp:class:`DigitalInOut`
+ - :cpp:class:`DigitalInOutInterrupt`
+
+Synchronization requirements
+============================
+* An instance of a line has exclusive ownership of that line and may be used
+ independently of other line objects without additional synchronization.
+* Access to a single line instance must be synchronized at the application
+ level. For example, by wrapping the line instance in ``pw::Borrowable``.
+* Unless otherwise stated, the line interface must not be used from within an
+ interrupt context.
+
+------------
+Design Notes
+------------
+The interfaces are intended to support many but not all use cases, and they do
+not cover every possible type of functionality supported by the hardware. There
+will be edge cases that require the backend to expose some additional (custom)
+interfaces, or require the use of a lower-level API.
+
+Examples of intended use cases:
+
+* Do input and output on lines that have two logical states - active and
+ inactive - regardless of the underlying hardware configuration.
+
+ * Example: Read the state of a switch.
+ * Example: Control a simple LED with on/off.
+ * Example: Activate/deactivate power for a peripheral.
+ * Example: Trigger reset of an I2C bus.
+
+* Run code based on an external interrupt.
+
+ * Example: Trigger when a hardware switch is flipped.
+ * Example: Trigger when device is connected to external power.
+ * Example: Handle data ready signals from peripherals connected to
+ I2C/SPI/etc.
+
+* Enable and disable lines as part of a high-level policy:
+
+ * Example: For power management - disable lines to use less power.
+ * Example: To support shared lines used for multiple purposes (ex. GPIO or
+ I2C).
+
+Examples of use cases we want to allow but don't explicitly support in the API:
+
+* Software-controlled pull up/down resistors, high drive, polarity controls,
+ etc.
+
+ * It's up to the backend implementation to expose configuration for these
+ settings.
+ * Enabling a line should set it into the state that is configured in the
+ backend.
+
+* Level-triggered interrupts on RTOS platforms.
+
+ * We explicitly support disabling the interrupt handler while in the context
+ of the handler.
+ * Otherwise, it's up to the backend to provide any additional level-trigger
+ support.
+
+Examples of uses cases we explicitly don't plan to support:
+
+* Using Digital IO to simulate serial interfaces like I2C (bit banging), or any
+ use cases requiring exact timing and access to line voltage, clock controls,
+ etc.
+* Mode selection - controlling hardware multiplexing or logically switching from
+ GPIO to I2C mode.
+
+API decisions that have been deferred:
+
+* Supporting operations on multiple lines in parallel - for example to simulate
+ a memory register or other parallel interface.
+* Helpers to support different patterns for interrupt handlers - running in the
+ interrupt context, dispatching to a dedicated thread, using a pw_sync
+ primitive, etc.
+
+The following sub-sections discuss specific design decisions in detail.
+
+States vs. voltage levels
+=========================
+Digital IO line values are represented as **active** and **inactive** states.
+These states abstract away the actual electrical level and other physical
+properties of the line. This allows applications to interact with Digital IO
+lines across targets that may have different physical configurations. It is up
+to the backend to provide a consistent definition of state.
+
+Interrupt handling
+==================
+Interrupt handling is part of this API. The alternative was to have a separate
+API for interrupts. We wanted to have a single object that refers to each line
+and represents all the functionality that is available on the line.
+
+Interrupt triggers are configured through the ``SetInterruptHandler`` method.
+The type of trigger is tightly coupled to what the handler wants to do with that
+trigger.
+
+The handler is passed the latest known sampled state of the line. Otherwise
+handlers running in an interrupt context cannot query the state of the line.
+
+Class Hierarchy
+===============
+``pw_digital_io`` contains a 2-level hierarchy of classes.
+
+* ``DigitalIoOptional`` acts as the base class and represents a line that does
+ not guarantee any particular functionality is available.
+
+ * This should be rarely used in APIs. Prefer to use one of the derived
+ classes.
+ * This class is never extended outside this module. Extend one of the derived
+ classes.
+
+* Derived classes represent a line with a particular combination of
+ functionality.
+
+ * Use a specific class in APIs to represent the requirements.
+ * Extend the specific class that has the actual capabilities of the line.
+
+In the future, we may add new classes that describe lines with **optional**
+functionality. For example, ``DigitalInOptionalInterrupt`` could describe a line
+that supports input and optionally supports interrupts.
+
+When using any classes with optional functionality, including
+``DigitalIoOptional``, you must check that a functionality is available using
+the ``provides_*`` runtime flags. Calling a method that is not supported will
+trigger ``PW_CRASH``.
+
+We define the public API through non-virtual methods declared in
+``DigitalIoOptional``. These methods delegate to private pure virtual methods.
+
+Type Conversions
+================
+Conversions are provided between classes with compatible requirements. For
+example:
+
+.. code-block:: cpp
+
+ DigitalInInterrupt& in_interrupt_line;
+ DigitalIn& in_line = in_interrupt_line;
+
+ DigitalInInterrupt* in_interrupt_line_ptr;
+ DigitalIn* in_line_ptr = &in_interrupt_line_ptr->as<DigitalIn>();
+
+Asynchronous APIs
+=================
+At present, ``pw_digital_io`` is synchronous. All the API calls are expected to
+block until the operation is complete. This is desirable for simple GPIO chips
+that are controlled through direct register access. However, this may be
+undesirable for GPIO extenders controlled through I2C or another shared bus.
+
+The API may be extended in the future to add asynchronous capabilities, or a
+separate asynchronous API may be created.
+
+Backend Implemention Notes
+==========================
+* Derived classes explicitly list the non-virtual methods as public or private
+ depending on the supported set of functionality. For example, ``DigitalIn``
+ declare ``GetState`` public and ``SetState`` private.
+* Derived classes that exclude a particular functionality provide a private,
+ final implementation of the unsupported virtual method that crashes if it is
+ called. For example, ``DigitalIn`` implements ``DoSetState`` to trigger
+ ``PW_CRASH``.
+* Backend implementations provide real implementation for the remaining pure
+ virtual functions of the class they extend.
+* Classes that support optional functionality make the non-virtual optional
+ methods public, but they do not provide an implementation for the pure virtual
+ functions. These classes are never extended.
+* Backend implementations **must** check preconditions for each operations. For
+ example, check that the line is actually enabled before trying to get/set the
+ state of the line. Otherwise return ``pw::Status::FailedPrecondition()``.
+* Backends *may* leave the line in an uninitialized state after construction,
+ but implementors are strongly encouraged to initialize the line to a known
+ state.
+
+ * If backends initialize the line, it must be initialized to the disabled
+ state. i.e. the same state it would be in after calling ``Enable()``
+ followed by ``Disable()``.
+ * Calling ``Disable()`` on an uninitialized line must put it into the disabled
+ state.
+
+------------
+Dependencies
+------------
+* :ref:`module-pw_assert`
+* :ref:`module-pw_function`
+* :ref:`module-pw_result`
+* :ref:`module-pw_status`
+
+.. cpp:namespace-pop::
+
+Zephyr
+======
+To enable ``pw_digital_io`` for Zephyr add ``CONFIG_PIGWEED_DIGITAL_IO=y`` to
+the project's configuration.
diff --git a/pw_digital_io/public/pw_digital_io/digital_io.h b/pw_digital_io/public/pw_digital_io/digital_io.h
new file mode 100644
index 000000000..ba8516b01
--- /dev/null
+++ b/pw_digital_io/public/pw_digital_io/digital_io.h
@@ -0,0 +1,541 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert/check.h"
+#include "pw_digital_io/internal/conversions.h"
+#include "pw_function/function.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+
+namespace pw::digital_io {
+
+// The logical state of a digital line.
+enum class State : bool {
+ kActive = true,
+ kInactive = false,
+};
+
+// The triggering configuration for an interrupt handler.
+enum class InterruptTrigger : int {
+ // Trigger on transition from kInactive to kActive.
+ kActivatingEdge,
+ // Trigger on transition from kActive to kInactive.
+ kDeactivatingEdge,
+ // Trigger on any state transition between kActive and kInactive.
+ kBothEdges,
+};
+
+// Interrupt handling function. The argument contains the latest known state of
+// the line. It is backend-specific if, when, and how this state is updated.
+using InterruptHandler = ::pw::Function<void(State sampled_state)>;
+
+// A digital I/O line that may support input, output, and interrupts, but makes
+// no guarantees about whether any operations are supported. You must check the
+// various provides_* flags before calling optional methods. Unsupported methods
+// invoke PW_CRASH.
+//
+// All methods are potentially blocking. Unless otherwise specified, access from
+// multiple threads to a single line must be externally synchronized - for
+// example using pw::Borrowable. Unless otherwise specified, none of the methods
+// are safe to call from an interrupt handler. Therefore, this abstraction may
+// not be suitable for bitbanging and other low-level uses of GPIO.
+//
+// Note that the initial state of a line is not guaranteed to be consistent with
+// either the "enabled" or "disabled" state. Users of the API who need to ensure
+// the line is disabled (ex. output not driving the line) should call Disable.
+//
+// This class should almost never be used in APIs directly. Instead, use one of
+// the derived classes that explicitly supports the functionality that your
+// API needs.
+//
+// This class cannot be extended directly. Instead, extend one of the
+// derived classes that explicitly support the functionality that you want to
+// implement.
+//
+class DigitalIoOptional {
+ public:
+ virtual ~DigitalIoOptional() = default;
+
+ // True if input (getting state) is supported.
+ constexpr bool provides_input() const { return config_.input; }
+ // True if output (setting state) is supported.
+ constexpr bool provides_output() const { return config_.output; }
+ // True if interrupt handlers can be registered.
+ constexpr bool provides_interrupt() const { return config_.interrupt; }
+
+ // Get the state of the line.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Returns:
+ //
+ // OK - an active or inactive state.
+ // FAILED_PRECONDITION - The line has not been enabled.
+ // Other status codes as defined by the backend.
+ //
+ Result<State> GetState() { return DoGetState(); }
+
+ // Set the state of the line.
+ //
+ // Callers are responsible to wait for the voltage level to settle after this
+ // call returns.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Returns:
+ //
+ // OK - the state has been set.
+ // FAILED_PRECONDITION - The line has not been enabled.
+ // Other status codes as defined by the backend.
+ //
+ Status SetState(State state) { return DoSetState(state); }
+
+ // Check if the line is in the active state.
+ //
+ // The line is in the active state when GetState() returns State::kActive.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Returns:
+ //
+ // OK - true if the line is in the active state, otherwise false.
+ // FAILED_PRECONDITION - The line has not been enabled.
+ // Other status codes as defined by the backend.
+ //
+ Result<bool> IsStateActive() {
+ PW_TRY_ASSIGN(const State state, GetState());
+ return state == State::kActive;
+ }
+
+ // Sets the line to the active state. Equivalent to SetState(State::kActive).
+ //
+ // Callers are responsible to wait for the voltage level to settle after this
+ // call returns.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Returns:
+ //
+ // OK - the state has been set.
+ // FAILED_PRECONDITION - The line has not been enabled.
+ // Other status codes as defined by the backend.
+ //
+ Status SetStateActive() { return SetState(State::kActive); }
+
+ // Sets the line to the inactive state. Equivalent to
+ // SetState(State::kInactive).
+ //
+ // Callers are responsible to wait for the voltage level to settle after this
+ // call returns.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Returns:
+ //
+ // OK - the state has been set.
+ // FAILED_PRECONDITION - The line has not been enabled.
+ // Other status codes as defined by the backend.
+ //
+ Status SetStateInactive() { return SetState(State::kInactive); }
+
+ // Set an interrupt handler to execute when an interrupt is triggered, and
+ // Configure the condition for triggering the interrupt.
+ //
+ // The handler is executed in a backend-specific context - this may be a
+ // system interrupt handler or a shared notification thread. Do not do any
+ // blocking or expensive work in the handler. The only universally safe
+ // operations are the IRQ-safe functions on pw_sync primitives.
+ //
+ // In particular, it is NOT safe to get the state of a DigitalIo line - either
+ // from this line or any other DigitalIoOptional instance - inside the
+ // handler.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Precondition: no handler is currently set.
+ //
+ // Returns:
+ // OK - the interrupt handler was configured.
+ // INVALID_ARGUMENT - handler is empty.
+ // Other status codes as defined by the backend.
+ //
+ Status SetInterruptHandler(InterruptTrigger trigger,
+ InterruptHandler&& handler) {
+ if (handler == nullptr) {
+ return Status::InvalidArgument();
+ }
+ return DoSetInterruptHandler(trigger, std::move(handler));
+ }
+
+ // Clear the interrupt handler and disable interrupts if enabled.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Returns:
+ // OK - the itnerrupt handler was cleared.
+ // Other status codes as defined by the backend.
+ //
+ Status ClearInterruptHandler() {
+ return DoSetInterruptHandler(InterruptTrigger::kActivatingEdge, nullptr);
+ }
+
+ // Enable interrupts which will trigger the interrupt handler.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Precondition: a handler has been set using SetInterruptHandler.
+ //
+ // Returns:
+ // OK - the interrupt handler was configured.
+ // FAILED_PRECONDITION - The line has not been enabled.
+ // Other status codes as defined by the backend.
+ //
+ Status EnableInterruptHandler() { return DoEnableInterruptHandler(true); }
+
+ // Disable the interrupt handler. This is a no-op if interrupts are disabled.
+ //
+ // This method can be called inside the interrupt handler for this line
+ // without any external synchronization. However, the exact behavior is
+ // backend-specific. There may be queued events that will trigger the handler
+ // again after this call returns.
+ //
+ // Returns:
+ // OK - the interrupt handler was configured.
+ // Other status codes as defined by the backend.
+ //
+ Status DisableInterruptHandler() { return DoEnableInterruptHandler(false); }
+
+ // Enable the line to initialize it into the default state as determined by
+ // the backend. This may enable pull-up/down resistors, drive the line high or
+ // low, etc. The line must be enabled before getting/setting the state
+ // or enabling interrupts.
+ //
+ // Callers are responsible to wait for the voltage level to settle after this
+ // call returns.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Returns:
+ // OK - the line is enabled and ready for use.
+ // Other status codes as defined by the backend.
+ //
+ Status Enable() { return DoEnable(true); }
+
+ // Disable the line to power down any pull-up/down resistors and disconnect
+ // from any voltage sources. This is usually done to save power. Interrupt
+ // handlers are automatically disabled.
+ //
+ // This method is not thread-safe and cannot be used in interrupt handlers.
+ //
+ // Returns:
+ // OK - the line is disabled.
+ // Other status codes as defined by the backend.
+ //
+ Status Disable() { return DoEnable(false); }
+
+ private:
+ friend class DigitalInterrupt;
+ friend class DigitalIn;
+ friend class DigitalInInterrupt;
+ friend class DigitalOut;
+ friend class DigitalOutInterrupt;
+ friend class DigitalInOut;
+ friend class DigitalInOutInterrupt;
+
+ // Private constructor so that only friends can extend us.
+ constexpr DigitalIoOptional(internal::Provides config) : config_(config) {}
+
+ // Implemented by derived classes to provide different functionality.
+ // See the documentation of the public functions for requirements.
+ virtual Status DoEnable(bool enable) = 0;
+ virtual Result<State> DoGetState() = 0;
+ virtual Status DoSetState(State level) = 0;
+ virtual Status DoSetInterruptHandler(InterruptTrigger trigger,
+ InterruptHandler&& handler) = 0;
+ virtual Status DoEnableInterruptHandler(bool enable) = 0;
+
+ // The configuration of this line.
+ const internal::Provides config_;
+};
+
+// A digital I/O line that supports only interrupts.
+//
+// The input and output methods are hidden and must not be called.
+//
+// Use this class in APIs when only interrupt functionality is required.
+// Extend this class to implement a line that only supports interrupts.
+//
+class DigitalInterrupt
+ : public DigitalIoOptional,
+ public internal::Conversions<DigitalInterrupt, DigitalIoOptional> {
+ public:
+ // Available functionality
+ using DigitalIoOptional::ClearInterruptHandler;
+ using DigitalIoOptional::DisableInterruptHandler;
+ using DigitalIoOptional::EnableInterruptHandler;
+ using DigitalIoOptional::SetInterruptHandler;
+
+ protected:
+ constexpr DigitalInterrupt()
+ : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalInterrupt>()) {}
+
+ private:
+ // Unavailable functionality
+ using DigitalIoOptional::provides_input;
+ using DigitalIoOptional::provides_interrupt;
+ using DigitalIoOptional::provides_output;
+
+ using DigitalIoOptional::GetState;
+ using DigitalIoOptional::IsStateActive;
+ using DigitalIoOptional::SetState;
+ using DigitalIoOptional::SetStateActive;
+ using DigitalIoOptional::SetStateInactive;
+
+ // These overrides invoke PW_CRASH.
+ Status DoSetState(State) final;
+ Result<State> DoGetState() final;
+};
+
+// A digital I/O line that supports only input (getting state).
+//
+// The output and interrupt methods are hidden and must not be called.
+//
+// Use this class in APIs when only input functionality is required.
+// Extend this class to implement a line that only supports getting state.
+//
+class DigitalIn : public DigitalIoOptional,
+ public internal::Conversions<DigitalIn, DigitalIoOptional> {
+ public:
+ // Available functionality
+ using DigitalIoOptional::GetState;
+ using DigitalIoOptional::IsStateActive;
+
+ protected:
+ constexpr DigitalIn()
+ : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalIn>()) {}
+
+ private:
+ // Unavailable functionality
+ using DigitalIoOptional::provides_input;
+ using DigitalIoOptional::provides_interrupt;
+ using DigitalIoOptional::provides_output;
+
+ using DigitalIoOptional::ClearInterruptHandler;
+ using DigitalIoOptional::DisableInterruptHandler;
+ using DigitalIoOptional::EnableInterruptHandler;
+ using DigitalIoOptional::SetInterruptHandler;
+ using DigitalIoOptional::SetState;
+ using DigitalIoOptional::SetStateActive;
+ using DigitalIoOptional::SetStateInactive;
+
+ // These overrides invoke PW_CRASH.
+ Status DoSetState(State) final;
+ Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) final;
+ Status DoEnableInterruptHandler(bool) final;
+};
+
+// An input line that supports interrupts.
+//
+// The output methods are hidden and must not be called.
+//
+// Use in APIs when input and interrupt functionality is required.
+//
+// Extend this class to implement a line that supports input (getting state) and
+// listening for interrupts at the same time.
+//
+class DigitalInInterrupt
+ : public DigitalIoOptional,
+ public internal::Conversions<DigitalInInterrupt, DigitalIoOptional> {
+ public:
+ // Available functionality
+ using DigitalIoOptional::ClearInterruptHandler;
+ using DigitalIoOptional::DisableInterruptHandler;
+ using DigitalIoOptional::EnableInterruptHandler;
+ using DigitalIoOptional::GetState;
+ using DigitalIoOptional::IsStateActive;
+ using DigitalIoOptional::SetInterruptHandler;
+
+ protected:
+ constexpr DigitalInInterrupt()
+ : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalInInterrupt>()) {}
+
+ private:
+ // Unavailable functionality
+ using DigitalIoOptional::provides_input;
+ using DigitalIoOptional::provides_interrupt;
+ using DigitalIoOptional::provides_output;
+
+ using DigitalIoOptional::SetState;
+ using DigitalIoOptional::SetStateActive;
+ using DigitalIoOptional::SetStateInactive;
+
+ // These overrides invoke PW_CRASH.
+ Status DoSetState(State) final;
+};
+
+// A digital I/O line that supports only output (setting state).
+//
+// Input and interrupt functions are hidden and must not be called.
+//
+// Use in APIs when only output functionality is required.
+// Extend this class to implement a line that supports output only.
+//
+class DigitalOut : public DigitalIoOptional,
+ public internal::Conversions<DigitalOut, DigitalIoOptional> {
+ public:
+ // Available functionality
+ using DigitalIoOptional::SetState;
+ using DigitalIoOptional::SetStateActive;
+ using DigitalIoOptional::SetStateInactive;
+
+ protected:
+ constexpr DigitalOut()
+ : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalOut>()) {}
+
+ private:
+ // Unavailable functionality
+ using DigitalIoOptional::provides_input;
+ using DigitalIoOptional::provides_interrupt;
+ using DigitalIoOptional::provides_output;
+
+ using DigitalIoOptional::ClearInterruptHandler;
+ using DigitalIoOptional::DisableInterruptHandler;
+ using DigitalIoOptional::EnableInterruptHandler;
+ using DigitalIoOptional::GetState;
+ using DigitalIoOptional::IsStateActive;
+ using DigitalIoOptional::SetInterruptHandler;
+
+ // These overrides invoke PW_CRASH.
+ Result<State> DoGetState() final;
+ Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) final;
+ Status DoEnableInterruptHandler(bool) final;
+};
+
+// A digital I/O line that supports output and interrupts.
+//
+// Input methods are hidden and must not be called.
+//
+// Use in APIs when output and interrupt functionality is required. For
+// example, to represent a two-way signalling line.
+//
+// Extend this class to implement a line that supports both output and
+// listening for interrupts at the same time.
+//
+class DigitalOutInterrupt
+ : public DigitalIoOptional,
+ public internal::Conversions<DigitalOutInterrupt, DigitalIoOptional> {
+ public:
+ // Available functionality
+ using DigitalIoOptional::ClearInterruptHandler;
+ using DigitalIoOptional::DisableInterruptHandler;
+ using DigitalIoOptional::EnableInterruptHandler;
+ using DigitalIoOptional::SetInterruptHandler;
+ using DigitalIoOptional::SetState;
+ using DigitalIoOptional::SetStateActive;
+ using DigitalIoOptional::SetStateInactive;
+
+ protected:
+ constexpr DigitalOutInterrupt()
+ : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalOutInterrupt>()) {}
+
+ private:
+ // Unavailable functionality
+ using DigitalIoOptional::provides_input;
+ using DigitalIoOptional::provides_interrupt;
+ using DigitalIoOptional::provides_output;
+
+ using DigitalIoOptional::GetState;
+ using DigitalIoOptional::IsStateActive;
+
+ // These overrides invoke PW_CRASH.
+ Result<State> DoGetState() final;
+};
+
+// A digital I/O line that supports both input and output.
+//
+// Use in APIs when both input and output functionality is required. For
+// example, to represent a line which is shared by multiple controllers.
+//
+// Extend this class to implement a line that supports both input and output at
+// the same time.
+//
+class DigitalInOut
+ : public DigitalIoOptional,
+ public internal::Conversions<DigitalInOut, DigitalIoOptional> {
+ public:
+ // Available functionality
+ using DigitalIoOptional::GetState;
+ using DigitalIoOptional::IsStateActive;
+ using DigitalIoOptional::SetState;
+ using DigitalIoOptional::SetStateActive;
+ using DigitalIoOptional::SetStateInactive;
+
+ protected:
+ constexpr DigitalInOut()
+ : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalInOut>()) {}
+
+ private:
+ // Unavailable functionality
+ using DigitalIoOptional::provides_input;
+ using DigitalIoOptional::provides_interrupt;
+ using DigitalIoOptional::provides_output;
+
+ using DigitalIoOptional::ClearInterruptHandler;
+ using DigitalIoOptional::DisableInterruptHandler;
+ using DigitalIoOptional::EnableInterruptHandler;
+ using DigitalIoOptional::SetInterruptHandler;
+
+ // These overrides invoke PW_CRASH.
+ Status DoSetInterruptHandler(InterruptTrigger, InterruptHandler&&) final;
+ Status DoEnableInterruptHandler(bool) final;
+};
+
+// A line that supports input, output, and interrupts.
+//
+// Use in APIs when input, output, and interrupts are required. For example to
+// represent a two-way shared line with state transition notifications.
+//
+// Extend this class to implement a line that supports all the functionality at
+// the same time.
+//
+class DigitalInOutInterrupt
+ : public DigitalIoOptional,
+ public internal::Conversions<DigitalInOutInterrupt, DigitalIoOptional> {
+ public:
+ // Available functionality
+ using DigitalIoOptional::ClearInterruptHandler;
+ using DigitalIoOptional::DisableInterruptHandler;
+ using DigitalIoOptional::EnableInterruptHandler;
+ using DigitalIoOptional::GetState;
+ using DigitalIoOptional::IsStateActive;
+ using DigitalIoOptional::SetInterruptHandler;
+ using DigitalIoOptional::SetState;
+ using DigitalIoOptional::SetStateActive;
+ using DigitalIoOptional::SetStateInactive;
+
+ protected:
+ constexpr DigitalInOutInterrupt()
+ : DigitalIoOptional(internal::AlwaysProvidedBy<DigitalInOutInterrupt>()) {
+ }
+
+ private:
+ // Unavailable functionality
+ using DigitalIoOptional::provides_input;
+ using DigitalIoOptional::provides_interrupt;
+ using DigitalIoOptional::provides_output;
+};
+
+} // namespace pw::digital_io
diff --git a/pw_digital_io/public/pw_digital_io/internal/conversions.h b/pw_digital_io/public/pw_digital_io/internal/conversions.h
new file mode 100644
index 000000000..ec2585d07
--- /dev/null
+++ b/pw_digital_io/public/pw_digital_io/internal/conversions.h
@@ -0,0 +1,148 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <type_traits>
+
+namespace pw::digital_io {
+namespace internal {
+
+// A type trait that describes the functionality required by a particular type
+// of line, and also the functionality that we assume is always provided by an
+// instance of that line. Used by Converter to determine if a conversion should
+// be allowed.
+//
+// Specialize this for each type of DigitalIO line by defining a required
+// functionality as `true` and optional functionality as `false`.
+//
+// Specializations must define the following fields:
+// static constexpr bool input;
+// static constexpr bool output;
+// static constexpr bool interrupt;
+//
+template <typename T>
+struct Requires;
+
+// Concrete struct describing the available functionality of a line at runtime.
+struct Provides {
+ bool input;
+ bool output;
+ bool interrupt;
+};
+
+// Returns the functionality always provided by the given type.
+template <typename T>
+constexpr Provides AlwaysProvidedBy() {
+ return {
+ .input = Requires<T>::input,
+ .output = Requires<T>::output,
+ .interrupt = Requires<T>::interrupt,
+ };
+}
+
+// Provides conversion operators between line objects based on the available
+// functionality.
+template <typename Self, typename CommonBase>
+class Conversions {
+ private:
+ // Static check to enable conversions from `Self` to `T`.
+ template <
+ typename T,
+ typename = std::enable_if_t<std::is_base_of_v<CommonBase, T>>,
+ typename = std::enable_if_t<Requires<Self>::input || !Requires<T>::input>,
+ typename =
+ std::enable_if_t<Requires<Self>::output || !Requires<T>::output>,
+ typename = std::enable_if_t<Requires<Self>::interrupt ||
+ !Requires<T>::interrupt>>
+ struct Enabled {};
+
+ public:
+ template <typename T, typename = Enabled<T>>
+ constexpr operator T&() {
+ return as<T>();
+ }
+
+ template <typename T, typename = Enabled<T>>
+ constexpr operator const T&() const {
+ return as<T>();
+ }
+
+ template <typename T, typename = Enabled<T>>
+ constexpr T& as() {
+ return static_cast<T&>(static_cast<CommonBase&>(static_cast<Self&>(*this)));
+ }
+
+ template <typename T, typename = Enabled<T>>
+ constexpr const T& as() const {
+ return static_cast<const T&>(
+ static_cast<const CommonBase&>(static_cast<const Self&>(*this)));
+ }
+};
+
+} // namespace internal
+
+// Specializations of Requires for each of the line types.
+// These live outside the `internal` namespace so that the forward class
+// declarations are in the correct namespace.
+
+template <>
+struct internal::Requires<class DigitalInterrupt> {
+ static constexpr bool input = false;
+ static constexpr bool output = false;
+ static constexpr bool interrupt = true;
+};
+
+template <>
+struct internal::Requires<class DigitalIn> {
+ static constexpr bool input = true;
+ static constexpr bool output = false;
+ static constexpr bool interrupt = false;
+};
+
+template <>
+struct internal::Requires<class DigitalInInterrupt> {
+ static constexpr bool input = true;
+ static constexpr bool output = false;
+ static constexpr bool interrupt = true;
+};
+
+template <>
+struct internal::Requires<class DigitalOut> {
+ static constexpr bool input = false;
+ static constexpr bool output = true;
+ static constexpr bool interrupt = false;
+};
+
+template <>
+struct internal::Requires<class DigitalOutInterrupt> {
+ static constexpr bool input = false;
+ static constexpr bool output = true;
+ static constexpr bool interrupt = true;
+};
+
+template <>
+struct internal::Requires<class DigitalInOut> {
+ static constexpr bool input = true;
+ static constexpr bool output = true;
+ static constexpr bool interrupt = false;
+};
+
+template <>
+struct internal::Requires<class DigitalInOutInterrupt> {
+ static constexpr bool input = true;
+ static constexpr bool output = true;
+ static constexpr bool interrupt = true;
+};
+
+} // namespace pw::digital_io
diff --git a/pw_docgen/BUILD.gn b/pw_docgen/BUILD.gn
index 0aaa8cdf0..e9226cc1f 100644
--- a/pw_docgen/BUILD.gn
+++ b/pw_docgen/BUILD.gn
@@ -14,8 +14,12 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_unit_test/test.gni")
import("docs.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_docgen/docs.gni b/pw_docgen/docs.gni
index b0ae93a16..2d0ae00cf 100644
--- a/pw_docgen/docs.gni
+++ b/pw_docgen/docs.gni
@@ -34,7 +34,11 @@ declare_args() {
# report_deps: Report card targets on which documentation depends.
# other_deps: Dependencies on any other types of targets.
template("pw_doc_group") {
- assert(defined(invoker.sources), "pw_doc_group requires a list of sources")
+ if (defined(invoker.sources)) {
+ _sources = invoker.sources
+ } else {
+ _sources = []
+ }
if (defined(invoker.inputs)) {
_inputs = invoker.inputs
@@ -42,6 +46,9 @@ template("pw_doc_group") {
_inputs = []
}
+ assert(defined(invoker.sources) || defined(invoker.inputs),
+ "pw_doc_group requires at least one of sources or inputs")
+
_all_deps = []
if (defined(invoker.group_deps)) {
_all_deps += invoker.group_deps
@@ -57,11 +64,11 @@ template("pw_doc_group") {
# rebuilt on file modifications.
pw_input_group(target_name) {
metadata = {
- pw_doc_sources = rebase_path(invoker.sources, root_build_dir)
+ pw_doc_sources = rebase_path(_sources, root_build_dir)
pw_doc_inputs = rebase_path(_inputs, root_build_dir)
}
deps = _all_deps
- inputs = invoker.sources + _inputs
+ inputs = _sources + _inputs
}
}
@@ -122,11 +129,19 @@ template("pw_doc_gen") {
_script_args += rebase_path(invoker.sources, root_build_dir)
+ # Required to set the PYTHONPATH for any automodule/class/function RST
+ # directives.
+ _python_metadata_deps = [ "$dir_pw_docgen/py" ]
+ if (defined(invoker.python_metadata_deps)) {
+ _python_metadata_deps += invoker.python_metadata_deps
+ }
+
if (pw_docgen_BUILD_DOCS) {
pw_python_action(target_name) {
script = "$dir_pw_docgen/py/pw_docgen/docgen.py"
args = _script_args
deps = [ ":$_metadata_file_target" ]
+ python_metadata_deps = _python_metadata_deps
inputs = [ invoker.conf ]
inputs += invoker.sources
stamp = true
diff --git a/pw_docgen/docs.rst b/pw_docgen/docs.rst
index c722d0a17..6971384a3 100644
--- a/pw_docgen/docs.rst
+++ b/pw_docgen/docs.rst
@@ -69,7 +69,7 @@ groups, causing them to be built with it.
* ``inputs``: Additional resources required for the docs (images, data files,
etc.)
* ``group_deps``: Other ``pw_doc_group`` targets required by this one.
-* ``report_deps``: Report card generating targets (e.g. ``pw_size_report``) on
+* ``report_deps``: Report card generating targets (e.g. ``pw_size_diff``) on
which the docs depend.
**Example**
@@ -168,6 +168,52 @@ Sphinx Extensions
This module houses Pigweed-specific extensions for the Sphinx documentation
generator. Extensions are included and configured in ``docs/conf.py``.
+module_metadata
+---------------
+Per :ref:`SEED-0102 <seed-0102>`, Pigweed module documentation has a standard
+format. The ``pigweed-module`` Sphinx directive provides that format and
+registers module metadata that can be used elsewhere in the Sphinx build.
+
+We need to add the directive after the document title, and add a class *to*
+the document title to achieve the title & subtitle formatting. Here's an
+example:
+
+.. code-block:: rst
+
+ .. rst-class:: with-subtitle
+
+ =========
+ pw_string
+ =========
+
+ .. pigweed-module::
+ :name: pw_string
+ :tagline: Efficient, easy, and safe string manipulation
+ :status: stable
+ :languages: C++14, C++17
+ :code-size-impact: 500 to 1500 bytes
+ :get-started: module-pw_string-get-started
+ :design: module-pw_string-design
+ :guides: module-pw_string-guide
+ :api: module-pw_string-api
+
+ Module sales pitch goes here!
+
+Directive options
+_________________
+- ``name``: The module name (required)
+- ``tagline``: A very short tagline that summarizes the module (required)
+- ``status``: One of ``experimental``, ``unstable``, and ``stable`` (required)
+- ``is-deprecated``: A flag indicating that the module is deprecated
+- ``languages``: A comma-separated list of languages the module supports
+- ``code-size-impact``: A summarize of the average code size impact
+- ``get-started``: A reference to the getting started section (required)
+- ``tutorials``: A reference to the tutorials section
+- ``guides``: A reference to the guides section
+- ``design``: A reference to the design considerations section (required)
+- ``concepts``: A reference to the concepts documentation
+- ``api``: A reference to the API documentation
+
google_analytics
----------------
When this extension is included and a ``google_analytics_id`` is set in the
diff --git a/pw_docgen/py/BUILD.gn b/pw_docgen/py/BUILD.gn
index dc23ed42f..55c48aaeb 100644
--- a/pw_docgen/py/BUILD.gn
+++ b/pw_docgen/py/BUILD.gn
@@ -27,6 +27,8 @@ pw_python_package("py") {
"pw_docgen/docgen.py",
"pw_docgen/sphinx/__init__.py",
"pw_docgen/sphinx/google_analytics.py",
+ "pw_docgen/sphinx/module_metadata.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_docgen/py/pw_docgen/docgen.py b/pw_docgen/py/pw_docgen/docgen.py
index 7b65e7754..764717d4a 100644
--- a/pw_docgen/py/pw_docgen/docgen.py
+++ b/pw_docgen/py/pw_docgen/docgen.py
@@ -41,37 +41,45 @@ def parse_args() -> argparse.Namespace:
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--sphinx-build-dir',
- required=True,
- help='Directory in which to build docs')
- parser.add_argument('--conf',
- required=True,
- help='Path to conf.py file for Sphinx')
- parser.add_argument('--gn-root',
- required=True,
- help='Root of the GN build tree')
- parser.add_argument('--gn-gen-root',
- required=True,
- help='Root of the GN gen tree')
- parser.add_argument('sources',
- nargs='+',
- help='Paths to the root level rst source files')
- parser.add_argument('--out-dir',
- required=True,
- help='Output directory for rendered HTML docs')
- parser.add_argument('--metadata',
- required=True,
- type=argparse.FileType('r'),
- help='Metadata JSON file')
- parser.add_argument('--google-analytics-id',
- const=None,
- help='Enables Google Analytics with the provided ID')
+ parser.add_argument(
+ '--sphinx-build-dir',
+ required=True,
+ help='Directory in which to build docs',
+ )
+ parser.add_argument(
+ '--conf', required=True, help='Path to conf.py file for Sphinx'
+ )
+ parser.add_argument(
+ '--gn-root', required=True, help='Root of the GN build tree'
+ )
+ parser.add_argument(
+ '--gn-gen-root', required=True, help='Root of the GN gen tree'
+ )
+ parser.add_argument(
+ 'sources', nargs='+', help='Paths to the root level rst source files'
+ )
+ parser.add_argument(
+ '--out-dir',
+ required=True,
+ help='Output directory for rendered HTML docs',
+ )
+ parser.add_argument(
+ '--metadata',
+ required=True,
+ type=argparse.FileType('r'),
+ help='Metadata JSON file',
+ )
+ parser.add_argument(
+ '--google-analytics-id',
+ const=None,
+ help='Enables Google Analytics with the provided ID',
+ )
return parser.parse_args()
-def build_docs(src_dir: str,
- dst_dir: str,
- google_analytics_id: Optional[str] = None) -> int:
+def build_docs(
+ src_dir: str, dst_dir: str, google_analytics_id: Optional[str] = None
+) -> int:
"""Runs Sphinx to render HTML documentation from a doc tree."""
# TODO(frolv): Specify the Sphinx script from a prebuilts path instead of
@@ -88,6 +96,7 @@ def build_docs(src_dir: str,
def copy_doc_tree(args: argparse.Namespace) -> None:
"""Copies doc source and input files into a build tree."""
+
def build_path(path):
"""Converts a source path to a filename in the build directory."""
if path.startswith(args.gn_root):
@@ -102,8 +111,9 @@ def copy_doc_tree(args: argparse.Namespace) -> None:
os.makedirs(args.sphinx_build_dir)
for source_path in args.sources:
- os.link(source_path,
- f'{args.sphinx_build_dir}/{Path(source_path).name}')
+ os.link(
+ source_path, f'{args.sphinx_build_dir}/{Path(source_path).name}'
+ )
os.link(args.conf, f'{args.sphinx_build_dir}/conf.py')
# Map of directory path to list of source and destination file paths.
@@ -128,7 +138,7 @@ def main() -> int:
if os.path.exists(args.sphinx_build_dir):
shutil.rmtree(args.sphinx_build_dir)
- # TODO(pwbug/164): Printing the header causes unicode problems on Windows.
+ # TODO(b/235349854): Printing the header causes unicode problems on Windows.
# Disabled for now; re-enable once the root issue is fixed.
# print(SCRIPT_HEADER)
copy_doc_tree(args)
@@ -136,8 +146,9 @@ def main() -> int:
# Flush all script output before running Sphinx.
print('-' * 80, flush=True)
- return build_docs(args.sphinx_build_dir, args.out_dir,
- args.google_analytics_id)
+ return build_docs(
+ args.sphinx_build_dir, args.out_dir, args.google_analytics_id
+ )
if __name__ == '__main__':
diff --git a/pw_docgen/py/pw_docgen/sphinx/google_analytics.py b/pw_docgen/py/pw_docgen/sphinx/google_analytics.py
index 3816fa25f..4ff34bf14 100644
--- a/pw_docgen/py/pw_docgen/sphinx/google_analytics.py
+++ b/pw_docgen/py/pw_docgen/sphinx/google_analytics.py
@@ -14,7 +14,9 @@
"""A Sphinx extension to add a Google Analytics tag to generated docs"""
-def add_google_analytics_tag(app, pagename, templatename, context, doctree): # pylint: disable=unused-argument
+def add_google_analytics_tag(
+ app, pagename, templatename, context, doctree
+): # pylint: disable=unused-argument
if app.config.google_analytics_id is None:
return
@@ -22,15 +24,16 @@ def add_google_analytics_tag(app, pagename, templatename, context, doctree): #
context['metatags'] = ''
# pylint: disable=line-too-long
- context['metatags'] += (
- f"""<script async src="https://www.googletagmanager.com/gtag/js?id={app.config.google_analytics_id}"></script>
+ context[
+ 'metatags'
+ ] += f"""<script async src="https://www.googletagmanager.com/gtag/js?id={app.config.google_analytics_id}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){{dataLayer.push(arguments);}}
gtag('js', new Date());
gtag('config', '{app.config.google_analytics_id}');
-</script>""")
+</script>"""
def setup(app):
diff --git a/pw_docgen/py/pw_docgen/sphinx/module_metadata.py b/pw_docgen/py/pw_docgen/sphinx/module_metadata.py
new file mode 100644
index 000000000..d6fe05497
--- /dev/null
+++ b/pw_docgen/py/pw_docgen/sphinx/module_metadata.py
@@ -0,0 +1,226 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Sphinx directives for Pigweed module metadata"""
+
+from typing import List
+
+import docutils
+
+# pylint: disable=consider-using-from-import
+import docutils.parsers.rst.directives as directives # type: ignore
+
+# pylint: enable=consider-using-from-import
+from sphinx.application import Sphinx as SphinxApplication
+from sphinx.util.docutils import SphinxDirective
+from sphinx_design.badges_buttons import ButtonRefDirective # type: ignore
+from sphinx_design.cards import CardDirective # type: ignore
+
+
+def status_choice(arg):
+ return directives.choice(arg, ('experimental', 'unstable', 'stable'))
+
+
+def status_badge(module_status: str) -> str:
+ role = ':bdg-primary:'
+ return role + f'`{module_status.title()}`'
+
+
+def cs_url(module_name: str):
+ return f'https://cs.opensource.google/pigweed/pigweed/+/main:{module_name}/'
+
+
+def concat_tags(*tag_lists: List[str]):
+ all_tags = tag_lists[0]
+
+ for tag_list in tag_lists[1:]:
+ if len(tag_list) > 0:
+ all_tags.append(':octicon:`dot-fill`')
+ all_tags.extend(tag_list)
+
+ return all_tags
+
+
+class PigweedModuleDirective(SphinxDirective):
+ """Directive registering module metadata, rendering title & info card."""
+
+ required_arguments = 0
+ final_argument_whitespace = True
+ has_content = True
+ option_spec = {
+ 'name': directives.unchanged_required,
+ 'tagline': directives.unchanged_required,
+ 'status': status_choice,
+ 'is-deprecated': directives.flag,
+ 'languages': directives.unchanged,
+ 'code-size-impact': directives.unchanged,
+ 'facade': directives.unchanged,
+ 'get-started': directives.unchanged_required,
+ 'tutorials': directives.unchanged,
+ 'guides': directives.unchanged,
+ 'concepts': directives.unchanged,
+ 'design': directives.unchanged_required,
+ 'api': directives.unchanged,
+ }
+
+ def try_get_option(self, option: str):
+ try:
+ return self.options[option]
+ except KeyError:
+ raise self.error(f' :{option}: option is required')
+
+ def maybe_get_option(self, option: str):
+ try:
+ return self.options[option]
+ except KeyError:
+ return None
+
+ def create_section_button(self, title: str, ref: str):
+ node = docutils.nodes.list_item(classes=['pw-module-section-button'])
+ node += ButtonRefDirective(
+ name='',
+ arguments=[ref],
+ options={'color': 'primary'},
+ content=[title],
+ lineno=0,
+ content_offset=0,
+ block_text='',
+ state=self.state,
+ state_machine=self.state_machine,
+ ).run()
+
+ return node
+
+ def register_metadata(self):
+ module_name = self.try_get_option('name')
+
+ if 'facade' in self.options:
+ facade = self.options['facade']
+
+ # Initialize the module relationship dict if needed
+ if not hasattr(self.env, 'pw_module_relationships'):
+ self.env.pw_module_relationships = {}
+
+ # Initialize the backend list for this facade if needed
+ if facade not in self.env.pw_module_relationships:
+ self.env.pw_module_relationships[facade] = []
+
+ # Add this module as a backend of the provided facade
+ self.env.pw_module_relationships[facade].append(module_name)
+
+ if 'is-deprecated' in self.options:
+ # Initialize the deprecated modules list if needed
+ if not hasattr(self.env, 'pw_modules_deprecated'):
+ self.env.pw_modules_deprecated = []
+
+ self.env.pw_modules_deprecated.append(module_name)
+
+ def run(self):
+ tagline = docutils.nodes.paragraph(
+ text=self.try_get_option('tagline'),
+ classes=['section-subtitle'],
+ )
+
+ status_tags: List[str] = [
+ status_badge(self.try_get_option('status')),
+ ]
+
+ if 'is-deprecated' in self.options:
+ status_tags.append(':bdg-danger:`Deprecated`')
+
+ language_tags = []
+
+ if 'languages' in self.options:
+ languages = self.options['languages'].split(',')
+
+ if len(languages) > 0:
+ for language in languages:
+ language_tags.append(f':bdg-info:`{language.strip()}`')
+
+ code_size_impact = []
+
+ if code_size_text := self.maybe_get_option('code-size-impact'):
+ code_size_impact.append(f'**Code Size Impact:** {code_size_text}')
+
+ # Move the directive content into a section that we can render wherever
+ # we want.
+ content = docutils.nodes.paragraph()
+ self.state.nested_parse(self.content, 0, content)
+
+ # The card inherits its content from this node's content, which we've
+ # already pulled out. So we can replace this node's content with the
+ # content we need in the card.
+ self.content = docutils.statemachine.StringList(
+ concat_tags(status_tags, language_tags, code_size_impact)
+ )
+
+ card = CardDirective.create_card(
+ inst=self,
+ arguments=[],
+ options={},
+ )
+
+ # Create the top-level section buttons.
+ section_buttons = docutils.nodes.bullet_list(
+ classes=['pw-module-section-buttons']
+ )
+
+ # This is the pattern for required sections.
+ section_buttons += self.create_section_button(
+ 'Get Started', self.try_get_option('get-started')
+ )
+
+ # This is the pattern for optional sections.
+ if (tutorials_ref := self.maybe_get_option('tutorials')) is not None:
+ section_buttons += self.create_section_button(
+ 'Tutorials', tutorials_ref
+ )
+
+ if (guides_ref := self.maybe_get_option('guides')) is not None:
+ section_buttons += self.create_section_button('Guides', guides_ref)
+
+ if (concepts_ref := self.maybe_get_option('concepts')) is not None:
+ section_buttons += self.create_section_button(
+ 'Concepts', concepts_ref
+ )
+
+ section_buttons += self.create_section_button(
+ 'Design', self.try_get_option('design')
+ )
+
+ if (api_ref := self.maybe_get_option('api')) is not None:
+ section_buttons += self.create_section_button(
+ 'API Reference', api_ref
+ )
+
+ return [tagline, section_buttons, content, card]
+
+
+def build_backend_lists(app, _doctree, _fromdocname):
+ env = app.builder.env
+
+ if not hasattr(env, 'pw_module_relationships'):
+ env.pw_module_relationships = {}
+
+
+def setup(app: SphinxApplication):
+ app.add_directive('pigweed-module', PigweedModuleDirective)
+
+ # At this event, the documents and metadata have been generated, and now we
+ # can modify the doctree to reflect the metadata.
+ app.connect('doctree-resolved', build_backend_lists)
+
+ return {
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
diff --git a/pw_docgen/py/setup.cfg b/pw_docgen/py/setup.cfg
index 62d65c378..41f7505ef 100644
--- a/pw_docgen/py/setup.cfg
+++ b/pw_docgen/py/setup.cfg
@@ -22,9 +22,12 @@ description = Generate Sphinx documentation
packages = find:
zip_safe = False
install_requires =
- sphinx >3
+ sphinx>=5.3.0
+ sphinx-argparse
sphinx-rtd-theme
- sphinxcontrib-mermaid >=0.7.1
+ sphinxcontrib-mermaid>=0.7.1
+ sphinx-design>=0.3.0
+
[options.package_data]
pw_docgen = py.typed
diff --git a/pw_doctor/BUILD.gn b/pw_doctor/BUILD.gn
index 09696722b..45e4c14f1 100644
--- a/pw_doctor/BUILD.gn
+++ b/pw_doctor/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_doctor/docs.rst b/pw_doctor/docs.rst
index 6fb524e1c..3ed403dae 100644
--- a/pw_doctor/docs.rst
+++ b/pw_doctor/docs.rst
@@ -8,14 +8,19 @@ it checks that things exactly match what is expected and it checks that things
look compatible without.
Projects that adjust the behavior of pw_env_setup may need to customize
-these checks, but unfortunately this is not supported yet.
+these checks, but unfortunately this is not generally supported yet.
Checks carried out by pw_doctor include:
* The bootstrapped OS matches the current OS.
* ``PW_ROOT`` is defined and points to the root of the Pigweed repo.
+
+ - If your copy of pigweed is intentionally vendored and not a git repo (or
+ submodule), set ``PW_DISABLE_ROOT_GIT_REPO_CHECK=1`` during bootstrap to
+ suppress the anti-vendoring portion of this check.
+
* The presubmit git hook is installed.
-* The current Python version is 3.8 or 3.9.
+* Python is one of the :ref:`supported versions <docs-concepts-python-version>`.
* The Pigweed virtual env is active.
* CIPD is set up correctly and in use.
* The CIPD packages required by Pigweed are up to date.
diff --git a/pw_doctor/py/BUILD.gn b/pw_doctor/py/BUILD.gn
index 1750e5dab..f3b2101d4 100644
--- a/pw_doctor/py/BUILD.gn
+++ b/pw_doctor/py/BUILD.gn
@@ -26,6 +26,10 @@ pw_python_package("py") {
"pw_doctor/__init__.py",
"pw_doctor/doctor.py",
]
- python_deps = [ "$dir_pw_cli/py" ]
+ python_deps = [
+ "$dir_pw_cli/py",
+ "$dir_pw_env_setup/py",
+ ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_doctor/py/pw_doctor/doctor.py b/pw_doctor/py/pw_doctor/doctor.py
index 1d2d37345..cf3f8651f 100755
--- a/pw_doctor/py/pw_doctor/doctor.py
+++ b/pw_doctor/py/pw_doctor/doctor.py
@@ -24,7 +24,7 @@ import shutil
import subprocess
import sys
import tempfile
-from typing import Callable, Iterable, List, Set
+from typing import Callable, Iterable, List, Optional, Set
import pw_cli.pw_command_plugins
import pw_env_setup.cipd_setup.update as cipd_update
@@ -41,16 +41,18 @@ class _Fatal(Exception):
class Doctor:
- def __init__(self, *, log: logging.Logger = None, strict: bool = False):
+ def __init__(
+ self, *, log: Optional[logging.Logger] = None, strict: bool = False
+ ):
self.strict = strict
self.log = log or logging.getLogger(__name__)
self.failures: Set[str] = set()
def run(self, checks: Iterable[Callable]):
with futures.ThreadPoolExecutor() as executor:
- futures.wait([
- executor.submit(self._run_check, c, executor) for c in checks
- ])
+ futures.wait(
+ [executor.submit(self._run_check, c, executor) for c in checks]
+ )
def _run_check(self, check, executor):
ctx = DoctorContext(self, check.__name__, executor)
@@ -62,14 +64,16 @@ class Doctor:
pass
except: # pylint: disable=bare-except
self.failures.add(ctx.check)
- self.log.exception('%s failed with an unexpected exception',
- check.__name__)
+ self.log.exception(
+ '%s failed with an unexpected exception', check.__name__
+ )
self.log.debug('Completed check %s', ctx.check)
class DoctorContext:
"""The context object provided to each context function."""
+
def __init__(self, doctor: Doctor, check: str, executor: futures.Executor):
self._doctor = doctor
self.check = check
@@ -79,7 +83,8 @@ class DoctorContext:
def submit(self, function, *args, **kwargs):
"""Starts running the provided function in parallel."""
self._futures.append(
- self._executor.submit(self._run_job, function, *args, **kwargs))
+ self._executor.submit(self._run_job, function, *args, **kwargs)
+ )
def wait(self):
"""Waits for all parallel tasks started with submit() to complete."""
@@ -94,7 +99,8 @@ class DoctorContext:
except: # pylint: disable=bare-except
self._doctor.failures.add(self.check)
self._doctor.log.exception(
- '%s failed with an unexpected exception', self.check)
+ '%s failed with an unexpected exception', self.check
+ )
def fatal(self, fmt, *args, **kwargs):
"""Same as error() but terminates the check early."""
@@ -135,6 +141,21 @@ def pw_plugins(ctx: DoctorContext):
ctx.error('Not all pw plugins loaded successfully')
+def unames_are_equivalent(
+ uname_actual: str, uname_expected: str, rosetta: bool = False
+) -> bool:
+ """Determine if uname values are equivalent for this tool's purposes."""
+
+ # Support `mac-arm64` through Rosetta until `mac-arm64` binaries are ready
+ # Expected and actual unames will not literally match on M1 Macs because
+ # they pretend to be Intel Macs for the purpose of environment setup. But
+ # that's intentional and doesn't require any user action.
+ if rosetta and "Darwin" in uname_expected and "arm64" in uname_expected:
+ uname_expected = uname_expected.replace("arm64", "x86_64")
+
+ return uname_actual == uname_expected
+
+
@register_into(CHECKS)
def env_os(ctx: DoctorContext):
"""Check that the environment matches this machine."""
@@ -148,19 +169,26 @@ def env_os(ctx: DoctorContext):
with open(config, 'r') as ins:
data = json.load(ins)
if data['os'] != os.name:
- ctx.error('Current OS (%s) does not match bootstrapped OS (%s)',
- os.name, data['os'])
+ ctx.error(
+ 'Current OS (%s) does not match bootstrapped OS (%s)',
+ os.name,
+ data['os'],
+ )
# Skipping sysname and nodename in os.uname(). nodename could change
# based on the current network. sysname won't change, but is
# redundant because it's contained in release or version, and
# skipping it here simplifies logic.
uname = ' '.join(getattr(os, 'uname', lambda: ())()[2:])
- if data['uname'] != uname:
+ rosetta_envvar = os.environ.get('_PW_ROSETTA', '0')
+ rosetta = rosetta_envvar.strip().lower() != '0'
+ if not unames_are_equivalent(uname, data['uname'], rosetta):
ctx.warning(
'Current uname (%s) does not match Bootstrap uname (%s), '
- 'you may need to rerun bootstrap on this system', uname,
- data['uname'])
+ 'you may need to rerun bootstrap on this system',
+ uname,
+ data['uname'],
+ )
@register_into(CHECKS)
@@ -171,12 +199,22 @@ def pw_root(ctx: DoctorContext):
except KeyError:
ctx.fatal('PW_ROOT not set')
+ # If pigweed is intentionally vendored and not in a git repo or submodule,
+ # set PW_DISABLE_ROOT_GIT_REPO_CHECK=1 during bootstrap to suppress the
+ # following check.
+ if os.environ.get('PW_DISABLE_ROOT_GIT_REPO_CHECK', '0') == '1':
+ return
+
git_root = pathlib.Path(
- call_stdout(['git', 'rev-parse', '--show-toplevel'], cwd=root).strip())
+ call_stdout(['git', 'rev-parse', '--show-toplevel'], cwd=root).strip()
+ )
git_root = git_root.resolve()
if root != git_root:
- ctx.error('PW_ROOT (%s) != `git rev-parse --show-toplevel` (%s)', root,
- git_root)
+ ctx.error(
+ 'PW_ROOT (%s) != `git rev-parse --show-toplevel` (%s)',
+ root,
+ git_root,
+ )
@register_into(CHECKS)
@@ -192,8 +230,10 @@ def git_hook(ctx: DoctorContext):
hook = root / '.git' / 'hooks' / 'pre-push'
if not os.path.isfile(hook):
- ctx.info('Presubmit hook not installed, please run '
- "'pw presubmit --install' before pushing changes.")
+ ctx.info(
+ 'Presubmit hook not installed, please run '
+ "'pw presubmit --install' before pushing changes."
+ )
@register_into(CHECKS)
@@ -201,17 +241,21 @@ def python_version(ctx: DoctorContext):
"""Check the Python version is correct."""
actual = sys.version_info
expected = (3, 8)
- latest = (3, 9)
- if (actual[0:2] < expected or actual[0] != expected[0]
- or actual[0:2] > latest):
+ if actual[0:2] < expected or actual[0] != expected[0]:
# If we get the wrong version but it still came from CIPD print a
# warning but give it a pass.
if 'chromium' in sys.version:
- ctx.warning('Python %d.%d.x expected, got Python %d.%d.%d',
- *expected, *actual[0:3])
+ ctx.warning(
+ 'Python %d.%d.x expected, got Python %d.%d.%d',
+ *expected,
+ *actual[0:3],
+ )
else:
- ctx.error('Python %d.%d.x required, got Python %d.%d.%d',
- *expected, *actual[0:3])
+ ctx.error(
+ 'Python %d.%d.x required, got Python %d.%d.%d',
+ *expected,
+ *actual[0:3],
+ )
@register_into(CHECKS)
@@ -251,12 +295,16 @@ def cipd(ctx: DoctorContext):
ctx.fatal('cipd not in PATH (%s)', os.environ['PATH'])
temp = tempfile.NamedTemporaryFile(prefix='cipd', delete=False)
- subprocess.run(['cipd', 'acl-check', '-json-output', temp.name, cipd_path],
- stdout=subprocess.PIPE)
+ subprocess.run(
+ ['cipd', 'acl-check', '-json-output', temp.name, cipd_path],
+ stdout=subprocess.PIPE,
+ )
if not json.load(temp)['result']:
ctx.fatal(
"can't access %s CIPD directory, have you run "
- "'cipd auth-login'?", cipd_path)
+ "'cipd auth-login'?",
+ cipd_path,
+ )
commands_expected_from_cipd = [
'arm-none-eabi-gcc',
@@ -275,11 +323,16 @@ def cipd(ctx: DoctorContext):
for command in commands_expected_from_cipd:
path = shutil.which(command)
if path is None:
- ctx.error('could not find %s in PATH (%s)', command,
- os.environ['PATH'])
+ ctx.error(
+ 'could not find %s in PATH (%s)', command, os.environ['PATH']
+ )
elif 'cipd' not in path:
- ctx.warning('not using %s from cipd, got %s (path is %s)', command,
- path, os.environ['PATH'])
+ ctx.warning(
+ 'not using %s from cipd, got %s (path is %s)',
+ command,
+ path,
+ os.environ['PATH'],
+ )
@register_into(CHECKS)
@@ -300,37 +353,47 @@ def cipd_versions(ctx: DoctorContext):
def check_cipd(package, install_path):
if platform not in package['platforms']:
- ctx.debug("skipping %s because it doesn't apply to %s",
- package['path'], platform)
+ ctx.debug(
+ "skipping %s because it doesn't apply to %s",
+ package['path'],
+ platform,
+ )
return
tags_without_refs = [x for x in package['tags'] if ':' in x]
if not tags_without_refs:
- ctx.debug('skipping %s because it tracks a ref, not a tag (%s)',
- package['path'], ', '.join(package['tags']))
+ ctx.debug(
+ 'skipping %s because it tracks a ref, not a tag (%s)',
+ package['path'],
+ ', '.join(package['tags']),
+ )
return
ctx.debug('checking version of %s', package['path'])
- name = [
- part for part in package['path'].split('/') if '{' not in part
- ][-1]
+ name = [part for part in package['path'].split('/') if '{' not in part][
+ -1
+ ]
# If the exact path is specified in the JSON file use it, and require it
# exist.
if 'version_file' in package:
path = install_path / package['version_file']
- if not path.is_file():
- ctx.error(f'no version file for {name} at {path}')
- return
-
+ notify_method = ctx.error
# Otherwise, follow a heuristic to find the file but don't require the
# file to exist.
else:
path = install_path / '.versions' / f'{name}.cipd_version'
- if not path.is_file():
- ctx.debug(f'no version file for {name} at {path}')
- return
+ notify_method = ctx.debug
+
+ # Check if a .exe cipd_version exists on Windows.
+ path_windows = install_path / '.versions' / f'{name}.exe.cipd_version'
+ if os.name == 'nt' and path_windows.is_file():
+ path = path_windows
+
+ if not path.is_file():
+ notify_method(f'no version file for {name} at {path}')
+ return
with path.open() as ins:
installed = json.load(ins)
@@ -352,17 +415,41 @@ def cipd_versions(ctx: DoctorContext):
if tag not in output:
ctx.error(
'CIPD package %s in %s is out of date, please rerun '
- 'bootstrap', installed['package_name'], install_path)
+ 'bootstrap',
+ installed['package_name'],
+ install_path,
+ )
else:
- ctx.debug('CIPD package %s in %s is current',
- installed['package_name'], install_path)
-
+ ctx.debug(
+ 'CIPD package %s in %s is current',
+ installed['package_name'],
+ install_path,
+ )
+
+ deduped_packages = cipd_update.deduplicate_packages(
+ cipd_update.all_packages(json_paths)
+ )
for json_path in json_paths:
ctx.debug(f'Checking packages in {json_path}')
+ if not json_path.exists():
+ ctx.error(
+ 'CIPD package file %s may have been deleted, please '
+ 'rerun bootstrap',
+ json_path,
+ )
+ continue
+
install_path = pathlib.Path(
- cipd_update.package_installation_path(cipd_dir, json_path))
+ cipd_update.package_installation_path(cipd_dir, json_path)
+ )
for package in json.loads(json_path.read_text()).get('packages', ()):
+ if package not in deduped_packages:
+ ctx.debug(
+ f'Skipping overridden package {package["path"]} '
+ f'with tag(s) {package["tags"]}'
+ )
+ continue
ctx.submit(check_cipd, package, install_path)
@@ -389,7 +476,8 @@ def symlinks(ctx: DoctorContext):
ctx.warning(
'Symlinks are not supported or current user does not have '
'permission to use them. This may cause build issues. If on '
- 'Windows, turn on Development Mode to enable symlink support.')
+ 'Windows, turn on Development Mode to enable symlink support.'
+ )
def run_doctor(strict=False, checks=None):
@@ -409,7 +497,8 @@ def run_doctor(strict=False, checks=None):
"Your environment setup has completed, but something isn't right "
'and some things may not work correctly. You may continue with '
'development, but please seek support at '
- 'https://bugs.pigweed.dev/ or by reaching out to your team.')
+ 'https://issues.pigweed.dev/new or by reaching out to your team.'
+ )
else:
doctor.log.info('Environment passes all checks!')
return len(doctor.failures)
diff --git a/pw_env_setup/BUILD.gn b/pw_env_setup/BUILD.gn
index 6cf0dfc0b..20e4bedd5 100644
--- a/pw_env_setup/BUILD.gn
+++ b/pw_env_setup/BUILD.gn
@@ -16,100 +16,142 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/python_dist.gni")
+import("$dir_pw_build/python_gn_args.gni")
+import("$dir_pw_build/python_venv.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
inputs = [ "doc_resources/pw_env_setup_output.png" ]
sources = [ "docs.rst" ]
}
-_pigweed_python_deps = [
- # Python packages
- "$dir_pw_allocator/py",
- "$dir_pw_arduino_build/py",
- "$dir_pw_bloat/py",
- "$dir_pw_build/py",
- "$dir_pw_build_info/py",
- "$dir_pw_build_mcuxpresso/py",
- "$dir_pw_cli/py",
- "$dir_pw_console/py",
- "$dir_pw_cpu_exception_cortex_m/py",
- "$dir_pw_docgen/py",
- "$dir_pw_doctor/py",
- "$dir_pw_env_setup/py",
- "$dir_pw_hdlc/py",
- "$dir_pw_log:protos.python",
- "$dir_pw_log_tokenized/py",
- "$dir_pw_module/py",
- "$dir_pw_package/py",
- "$dir_pw_presubmit/py",
- "$dir_pw_protobuf/py",
- "$dir_pw_protobuf_compiler/py",
- "$dir_pw_rpc/py",
- "$dir_pw_snapshot/py:pw_snapshot",
- "$dir_pw_snapshot/py:pw_snapshot_metadata",
- "$dir_pw_software_update/py",
- "$dir_pw_status/py",
- "$dir_pw_stm32cube_build/py",
- "$dir_pw_symbolizer/py",
- "$dir_pw_system/py",
- "$dir_pw_thread/py",
- "$dir_pw_tls_client/py",
- "$dir_pw_tokenizer/py",
- "$dir_pw_toolchain/py",
- "$dir_pw_trace/py",
- "$dir_pw_trace_tokenized/py",
- "$dir_pw_transfer/py",
- "$dir_pw_unit_test/py",
- "$dir_pw_watch/py",
-]
-
+pw_python_group("core_pigweed_python_packages") {
+ python_deps = [
+ "$dir_pw_allocator/py",
+ "$dir_pw_arduino_build/py",
+ "$dir_pw_bloat/py",
+ "$dir_pw_build/py",
+ "$dir_pw_build_info/py",
+ "$dir_pw_build_mcuxpresso/py",
+ "$dir_pw_chrono/py",
+ "$dir_pw_cli/py",
+ "$dir_pw_compilation_testing/py",
+ "$dir_pw_console/py",
+ "$dir_pw_cpu_exception_cortex_m/py",
+ "$dir_pw_docgen/py",
+ "$dir_pw_doctor/py",
+ "$dir_pw_env_setup/py",
+ "$dir_pw_hdlc/py",
+ "$dir_pw_ide/py",
+ "$dir_pw_log:protos.python",
+ "$dir_pw_log_tokenized/py",
+ "$dir_pw_metric/py",
+ "$dir_pw_module/py",
+ "$dir_pw_package/py",
+ "$dir_pw_presubmit/py",
+ "$dir_pw_protobuf/py",
+ "$dir_pw_protobuf_compiler/py",
+ "$dir_pw_rpc/py",
+ "$dir_pw_snapshot/py:pw_snapshot",
+ "$dir_pw_snapshot/py:pw_snapshot_metadata",
+ "$dir_pw_software_update/py",
+ "$dir_pw_status/py",
+ "$dir_pw_stm32cube_build/py",
+ "$dir_pw_symbolizer/py",
+ "$dir_pw_system/py",
+ "$dir_pw_thread/py",
+ "$dir_pw_thread_freertos/py",
+ "$dir_pw_tls_client/py",
+ "$dir_pw_tokenizer/py",
+ "$dir_pw_toolchain/py",
+ "$dir_pw_trace/py",
+ "$dir_pw_trace_tokenized/py",
+ "$dir_pw_transfer/py",
+ "$dir_pw_unit_test/py",
+ "$dir_pw_watch/py",
+ ]
+}
pw_python_group("python") {
- python_deps = _pigweed_python_deps
- python_deps += [
+ python_deps = [
+ ":core_pigweed_python_packages",
+
+ # This target creates and installs Python package named 'pigweed' including
+ # every package listed in :core_pigweed_python_packages
+ ":pip_install_pigweed_package",
+
# Standalone scripts
+ # These targets are included here in order to ensure they are linted.
"$dir_pw_hdlc/rpc_example:example_script",
"$dir_pw_rpc/py:python_client_cpp_server_test",
+ "$dir_pw_third_party/fuchsia:generate_fuchsia_patch",
"$dir_pw_third_party/nanopb:generate_nanopb_proto",
- "$dir_pw_transfer/py:python_cpp_transfer_test",
-
- # Python requirements for CIPD packages that don't have dedicated modules.
- ":renode_requirements",
+ "$dir_pw_unit_test:test_group_metadata_test",
]
}
+pw_python_venv("pigweed_build_venv") {
+ path = "$root_build_dir/python-venv"
+ source_packages = [ ":core_pigweed_python_packages" ]
+}
+
# Python packages for supporting specific targets.
-pw_python_group("target_support_packages") {
- python_deps = [
+pw_python_pip_install("target_support_packages") {
+ packages = [
"$dir_pigweed/targets/lm3s6965evb_qemu/py",
"$dir_pigweed/targets/stm32f429i_disc1/py",
]
+ editable = true
}
-pw_python_requirements("renode_requirements") {
- requirements = [
- "psutil",
- "pyyaml",
- "robotframework==3.1",
+# This target is responsible for building the Python source uploaded to PyPI:
+# https://pypi.org/project/pigweed/
+pw_python_distribution("pypi_pigweed_python_source_tree") {
+ packages = [ ":core_pigweed_python_packages" ]
+ generate_setup_cfg = {
+ common_config_file = "pypi_common_setup.cfg"
+ }
+ extra_files = [
+ "$dir_pigweed/LICENSE > LICENSE",
+ "$dir_pigweed/README.md > README.md",
+ "pypi_pyproject.toml > pyproject.toml",
]
}
-pw_create_python_source_tree("build_pigweed_python_source_tree") {
- packages = _pigweed_python_deps
- include_tests = true
+# This target is creates the 'pigweed' Python package installed to the user's
+# dev environment. It's similar to the source tree for PyPI but it appends the
+# current date to the version so pip will consider it more up to date than the
+# one in PyPI.
+pw_python_distribution("generate_pigweed_python_package") {
+ packages = [ ":core_pigweed_python_packages" ]
+
+ generate_setup_cfg = {
+ common_config_file = "pypi_common_setup.cfg"
+ append_date_to_version = true
+ include_default_pyproject_file = true
+ }
extra_files = [
- "$dir_pigweed/pw_build/py/BUILD.bazel > pw_build/BUILD.bazel",
- "$dir_pigweed/pw_cli/py/BUILD.bazel > pw_cli/BUILD.bazel",
- "$dir_pigweed/pw_hdlc/py/BUILD.bazel > pw_hdlc/BUILD.bazel",
- "$dir_pigweed/pw_protobuf/py/BUILD.bazel > pw_protobuf/BUILD.bazel",
- "$dir_pigweed/pw_protobuf_compiler/py/BUILD.bazel > pw_protobuf_compiler/BUILD.bazel",
- "$dir_pigweed/pw_rpc/py/BUILD.bazel > pw_rpc/BUILD.bazel",
- "$dir_pigweed/pw_status/py/BUILD.bazel > pw_status/BUILD.bazel",
+ "$dir_pigweed/LICENSE > LICENSE",
+ "$dir_pigweed/README.md > README.md",
]
}
-pw_create_python_source_tree("pypi_pigweed_python_source_tree") {
- packages = _pigweed_python_deps
+# This pip installs the generate_pigweed_python_package into the user venv.
+pw_python_pip_install("pip_install_pigweed_package") {
+ packages = [ ":generate_pigweed_python_package" ]
+}
+
+# The next three targets are not built by default but demonstrate how to
+# generate Python distributions with only a subset of Python packages
+# included. The result could be bundled into PyOxidizer or distributed as a
+# standalone wheel.
+
+# To create this wheel run:
+# ninja -C out pw_env_setup:generate_pw_env_setup_distribution.wheel
+# The resulting wheel is available in
+# out/obj/pw_env_setup/generate_pw_env_setup_distribution._build_wheel/
+pw_python_distribution("generate_pw_env_setup_distribution") {
+ packages = [ "$dir_pw_env_setup/py" ]
generate_setup_cfg = {
common_config_file = "pypi_common_setup.cfg"
}
@@ -119,3 +161,51 @@ pw_create_python_source_tree("pypi_pigweed_python_source_tree") {
"pypi_pyproject.toml > pyproject.toml",
]
}
+
+# To create this wheel run:
+# ninja -C out pw_env_setup:generate_pw_system_distribution.wheel
+# The resulting wheel is available in
+# out/obj/pw_env_setup/generate_pw_system_distribution._build_wheel/
+pw_python_distribution("generate_pw_system_distribution") {
+ packages = [ "$dir_pw_system/py" ]
+ generate_setup_cfg = {
+ common_config_file = "pypi_common_setup.cfg"
+ }
+ extra_files = [
+ "$dir_pigweed/LICENSE > LICENSE",
+ "$dir_pigweed/README.md > README.md",
+ "pypi_pyproject.toml > pyproject.toml",
+ ]
+}
+
+pw_python_group("hdlc_proto_rpc_python_packages") {
+ python_deps = [
+ "$dir_pw_hdlc/py",
+ "$dir_pw_protobuf_compiler/py",
+ "$dir_pw_rpc/py",
+ "$dir_pw_tokenizer/py",
+ ]
+}
+
+# To create this wheel run:
+# ninja -C out pw_env_setup:generate_hdlc_proto_rpc_tokenizer_distribution.wheel
+# The resulting wheel is available in
+# out/obj/pw_env_setup/generate_hdlc_proto_rpc_tokenizer_distribution._build_wheel/
+pw_python_distribution("generate_hdlc_proto_rpc_tokenizer_distribution") {
+ packages = [ ":hdlc_proto_rpc_python_packages" ]
+
+ generate_setup_cfg = {
+ common_config_file = "pypi_common_setup.cfg"
+ append_date_to_version = true
+ }
+ extra_files = [
+ "$dir_pigweed/LICENSE > LICENSE",
+ "$dir_pigweed/README.md > README.md",
+ "pypi_pyproject.toml > pyproject.toml",
+ ]
+}
+
+if (current_toolchain != default_toolchain) {
+ pw_test_group("tests") {
+ }
+}
diff --git a/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl b/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl
index f7faf8198..c6d462557 100644
--- a/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl
+++ b/pw_env_setup/bazel/cipd_setup/cipd_rules.bzl
@@ -83,6 +83,9 @@ _pigweed_deps = repository_rule(
"_python_packages_json": attr.label(
default = "@pigweed//pw_env_setup:py/pw_env_setup/cipd_setup/python.json",
),
+ "_upstream_testing_packages_json": attr.label(
+ default = "@pigweed//pw_env_setup:py/pw_env_setup/cipd_setup/testing.json",
+ ),
},
)
diff --git a/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl b/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl
index bbd2c95e8..6bbc06585 100644
--- a/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl
+++ b/pw_env_setup/bazel/cipd_setup/internal/cipd_internal.bzl
@@ -43,7 +43,7 @@ def platform_normalized(rctx):
else:
fail("Could not normalize os:", rctx.os.name)
-# TODO(pwbug/388): Enable unused variable check.
+# TODO(b/234879770): Enable unused variable check.
# buildifier: disable=unused-variable
def arch_normalized(rctx):
"""Normalizes the architecture string to match CIPDs naming system.
@@ -55,7 +55,7 @@ def arch_normalized(rctx):
str: Normalized architecture.
"""
- # TODO(pwbug/388): Find a way to get host architecture information from a
+ # TODO(b/234879770): Find a way to get host architecture information from a
# repository context.
return "amd64"
diff --git a/pw_env_setup/bazel_only.json b/pw_env_setup/bazel_only.json
index 3e5fa965c..e3d615f13 100644
--- a/pw_env_setup/bazel_only.json
+++ b/pw_env_setup/bazel_only.json
@@ -2,7 +2,7 @@
"root_variable": "PW_ROOT",
"cipd_package_files": [
"pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json",
- "pw_env_setup/py/pw_env_setup/cipd_setup/python.json",
+ "pw_env_setup/py/pw_env_setup/cipd_setup/python.json"
],
"virtualenv": {
"gn_root": ".",
diff --git a/pw_env_setup/compatibility.json b/pw_env_setup/compatibility.json
index 402ea7e0a..023cf0bea 100644
--- a/pw_env_setup/compatibility.json
+++ b/pw_env_setup/compatibility.json
@@ -1,7 +1,7 @@
{
"root_variable": "PW_ROOT",
"cipd_package_files": [
- "pw_env_setup/py/pw_env_setup/cipd_setup/default.json",
+ "pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json",
"pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json"
],
"virtualenv": {
diff --git a/pw_env_setup/config.json b/pw_env_setup/config.json
index 5d3e5b0a9..327e2b78e 100644
--- a/pw_env_setup/config.json
+++ b/pw_env_setup/config.json
@@ -1,14 +1,21 @@
{
"root_variable": "PW_ROOT",
"cipd_package_files": [
- "pw_env_setup/py/pw_env_setup/cipd_setup/default.json"
+ "pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json"
],
"virtualenv": {
"gn_root": ".",
"gn_targets": [
":python.install"
+ ],
+ "requirements": [
+ "pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt"
+ ],
+ "constraints": [
+ "pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list"
]
},
"pw_packages": [],
+ "rosetta": "allow",
"gni_file": "build_overrides/pigweed_environment.gni"
}
diff --git a/pw_env_setup/docs.rst b/pw_env_setup/docs.rst
index ecaecb303..85cd7cb95 100644
--- a/pw_env_setup/docs.rst
+++ b/pw_env_setup/docs.rst
@@ -7,12 +7,13 @@ A classic problem in the embedded space is reducing the time from git clone
to having a binary executing on a device. The issue is that an entire suite
of tools is needed for non-trivial production embedded projects. For example:
- - A C++ compiler for your target device, and also for your host
- - A build system or three; for example, GN, Ninja, CMake, Bazel
- - A code formatting program like clang-format
- - A debugger like OpenOCD to flash and debug your embedded device
- - A known Python version with known modules installed for scripting
- - A Go compiler for the Go-based command line tools
+- A C++ compiler for your target device, and also for your host
+- A build system or three; for example, GN, Ninja, CMake, Bazel
+- A code formatting program like clang-format
+- A debugger like OpenOCD to flash and debug your embedded device (OpenOCD
+ support removed for Windows)
+- A known Python version with known modules installed for scripting
+- A Go compiler for the Go-based command line tools
...and so on
@@ -37,15 +38,17 @@ command reinitializes a previously configured environment, and if none is found,
runs bootstrap.
.. note::
- On Windows the scripts used to set up the environment are ``bootstrap.bat``
- and ``activate.bat``. For simplicity they will be referred to with the ``.sh``
- endings unless the distinction is relevant.
+
+ On Windows the scripts used to set up the environment are ``bootstrap.bat``
+ and ``activate.bat``. For simplicity they will be referred to with the
+ ``.sh`` endings unless the distinction is relevant.
.. warning::
- At this time ``pw_env_setup`` works for us, but isn’t well tested. We don’t
- suggest relying on it just yet. However, we are interested in experience
- reports; if you give it a try, please `send us a note`_ about your
- experience.
+
+ At this time ``pw_env_setup`` works for us, but isn’t well tested. We don’t
+ suggest relying on it just yet. However, we are interested in experience
+ reports; if you give it a try, please `send us a note`_ about your
+ experience.
.. _send us a note: pigweed@googlegroups.com
@@ -68,30 +71,30 @@ assumes `bootstrap.sh` is at the top level of your repository.
.. code-block:: bash
- # Do not include a "#!" line, this must be sourced and not executed.
+ # Do not include a "#!" line, this must be sourced and not executed.
- # This assumes the user is sourcing this file from it's parent directory. See
- # below for a more flexible way to handle this.
- PROJ_SETUP_SCRIPT_PATH="$(pwd)/bootstrap.sh"
+ # This assumes the user is sourcing this file from it's parent directory. See
+ # below for a more flexible way to handle this.
+ PROJ_SETUP_SCRIPT_PATH="$(pwd)/bootstrap.sh"
- export PW_PROJECT_ROOT="$(_python_abspath "$(dirname "$PROJ_SETUP_SCRIPT_PATH")")"
+ export PW_PROJECT_ROOT="$(_python_abspath "$(dirname "$PROJ_SETUP_SCRIPT_PATH")")"
- # You may wish to check if the user is attempting to execute this script
- # instead of sourcing it. See below for an example of how to handle that
- # situation.
+ # You may wish to check if the user is attempting to execute this script
+ # instead of sourcing it. See below for an example of how to handle that
+ # situation.
- # Source Pigweed's bootstrap utility script.
- # Using '.' instead of 'source' for POSIX compatibility. Since users don't use
- # dash directly, using 'source' in most documentation so users don't get
- # confused and try to `./bootstrap.sh`.
- . "$PW_PROJECT_ROOT/third_party/pigweed/pw_env_setup/util.sh"
+ # Source Pigweed's bootstrap utility script.
+ # Using '.' instead of 'source' for POSIX compatibility. Since users don't use
+ # dash directly, using 'source' in most documentation so users don't get
+ # confused and try to `./bootstrap.sh`.
+ . "$PW_PROJECT_ROOT/third_party/pigweed/pw_env_setup/util.sh"
- pw_check_root "$PW_ROOT"
- _PW_ACTUAL_ENVIRONMENT_ROOT="$(pw_get_env_root)"
- export _PW_ACTUAL_ENVIRONMENT_ROOT
- SETUP_SH="$_PW_ACTUAL_ENVIRONMENT_ROOT/activate.sh"
- pw_bootstrap --args... # See below for details about args.
- pw_finalize bootstrap "$SETUP_SH"
+ pw_check_root "$PW_ROOT"
+ _PW_ACTUAL_ENVIRONMENT_ROOT="$(pw_get_env_root)"
+ export _PW_ACTUAL_ENVIRONMENT_ROOT
+ SETUP_SH="$_PW_ACTUAL_ENVIRONMENT_ROOT/activate.sh"
+ pw_bootstrap --args... # See below for details about args.
+ pw_finalize bootstrap "$SETUP_SH"
Bazel Usage
@@ -101,31 +104,30 @@ rather than using `bootstrap.sh`. e.g.
.. code:: python
- # WORKSPACE
+ # WORKSPACE
- load("//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl", "pigweed_deps")
+ load("//pw_env_setup/bazel/cipd_setup:cipd_rules.bzl", "pigweed_deps")
- # Setup CIPD client and packages.
- # Required by: pigweed.
- # Used by modules: all.
- pigweed_deps()
+ # Setup CIPD client and packages.
+ # Required by: pigweed.
+ # Used by modules: all.
+ pigweed_deps()
- load("@cipd_deps//:cipd_init.bzl", "cipd_init")
+ load("@cipd_deps//:cipd_init.bzl", "cipd_init")
- cipd_init()
+ cipd_init()
-This will make the entire set of Pigweeds remote repositories available
-to your project. Though these repositories will only be donwloaded if
-you use them. To get a full list of the remote repositories that this
-configures, run:
+This will make the entire set of Pigweeds remote repositories available to your
+project. Though these repositories will only be donwloaded if you use them. To
+get a full list of the remote repositories that this configures, run:
.. code:: sh
- bazel query //external:all | grep cipd_
+ bazel query //external:all | grep cipd_
-All files and executables in each CIPD remote repository is exported
-and visible either directely (`@cipd_<dep>//:<file>`) or from 'all' filegroup
+All files and executables in each CIPD remote repository is exported and visible
+either directely (`@cipd_<dep>//:<file>`) or from 'all' filegroup
(`@cipd_<dep>//:all`).
From here it is possible to get access to the Bloaty binaries using the
@@ -133,8 +135,8 @@ following command. For example;
.. code:: sh
- bazel run @cipd_pigweed_third_party_bloaty_embedded_linux_amd64//:bloaty \
- -- --help
+ bazel run @cipd_pigweed_third_party_bloaty_embedded_linux_amd64//:bloaty \
+ -- --help
User-Friendliness
-----------------
@@ -144,31 +146,31 @@ that case you'll need the following at the top of `bootstrap.sh`.
.. code-block:: bash
- _python_abspath () {
- python -c "import os.path; print(os.path.abspath('$@'))"
- }
-
- # Use this code from Pigweed's bootstrap to find the path to this script when
- # sourced. This should work with common shells. PW_CHECKOUT_ROOT is only used in
- # presubmit tests with strange setups, and can be omitted if you're not using
- # Pigweed's automated testing infrastructure.
- if test -n "$PW_CHECKOUT_ROOT"; then
- PROJ_SETUP_SCRIPT_PATH="$(_python_abspath "$PW_CHECKOUT_ROOT/bootstrap.sh")"
- unset PW_CHECKOUT_ROOT
- # Shell: bash.
- elif test -n "$BASH"; then
- PROJ_SETUP_SCRIPT_PATH="$(_python_abspath "$BASH_SOURCE")"
- # Shell: zsh.
- elif test -n "$ZSH_NAME"; then
- PROJ_SETUP_SCRIPT_PATH="$(_python_abspath "${(%):-%N}")"
- # Shell: dash.
- elif test ${0##*/} = dash; then
- PROJ_SETUP_SCRIPT_PATH="$(_python_abspath \
- "$(lsof -p $$ -Fn0 | tail -1 | sed 's#^[^/]*##;')")"
- # If everything else fails, try $0. It could work.
- else
- PROJ_SETUP_SCRIPT_PATH="$(_python_abspath "$0")"
- fi
+ _python_abspath () {
+ python -c "import os.path; print(os.path.abspath('$@'))"
+ }
+
+ # Use this code from Pigweed's bootstrap to find the path to this script when
+ # sourced. This should work with common shells. PW_CHECKOUT_ROOT is only used in
+ # presubmit tests with strange setups, and can be omitted if you're not using
+ # Pigweed's automated testing infrastructure.
+ if test -n "$PW_CHECKOUT_ROOT"; then
+ PROJ_SETUP_SCRIPT_PATH="$(_python_abspath "$PW_CHECKOUT_ROOT/bootstrap.sh")"
+ unset PW_CHECKOUT_ROOT
+ # Shell: bash.
+ elif test -n "$BASH"; then
+ PROJ_SETUP_SCRIPT_PATH="$(_python_abspath "$BASH_SOURCE")"
+ # Shell: zsh.
+ elif test -n "$ZSH_NAME"; then
+ PROJ_SETUP_SCRIPT_PATH="$(_python_abspath "${(%):-%N}")"
+ # Shell: dash.
+ elif test ${0##*/} = dash; then
+ PROJ_SETUP_SCRIPT_PATH="$(_python_abspath \
+ "$(lsof -p $$ -Fn0 | tail -1 | sed 's#^[^/]*##;')")"
+ # If everything else fails, try $0. It could work.
+ else
+ PROJ_SETUP_SCRIPT_PATH="$(_python_abspath "$0")"
+ fi
You may also wish to check if the user is attempting to execute `bootstrap.sh`
instead of sourcing it. Executing `bootstrap.sh` would download everything
@@ -177,31 +179,30 @@ process. To check for this add the following.
.. code-block:: bash
- # Check if this file is being executed or sourced.
- _pw_sourced=0
- # If not running in Pigweed's automated testing infrastructure the
- # SWARMING_BOT_ID check is unnecessary.
- if [ -n "$SWARMING_BOT_ID" ]; then
- # If set we're running on swarming and don't need this check.
- _pw_sourced=1
- elif [ -n "$ZSH_EVAL_CONTEXT" ]; then
- case $ZSH_EVAL_CONTEXT in *:file) _pw_sourced=1;; esac
- elif [ -n "$KSH_VERSION" ]; then
- [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != \
- "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] \
- && _pw_sourced=1
- elif [ -n "$BASH_VERSION" ]; then
- (return 0 2>/dev/null) && _pw_sourced=1
- else # All other shells: examine $0 for known shell binary filenames
- # Detects `sh` and `dash`; add additional shell filenames as needed.
- case ${0##*/} in sh|dash) _pw_sourced=1;; esac
- fi
-
- _pw_eval_sourced "$_pw_sourced"
+ # Check if this file is being executed or sourced.
+ _pw_sourced=0
+ # If not running in Pigweed's automated testing infrastructure the
+ # SWARMING_BOT_ID check is unnecessary.
+ if [ -n "$SWARMING_BOT_ID" ]; then
+ # If set we're running on swarming and don't need this check.
+ _pw_sourced=1
+ elif [ -n "$ZSH_EVAL_CONTEXT" ]; then
+ case $ZSH_EVAL_CONTEXT in *:file) _pw_sourced=1;; esac
+ elif [ -n "$KSH_VERSION" ]; then
+ [ "$(cd $(dirname -- $0) && pwd -P)/$(basename -- $0)" != \
+ "$(cd $(dirname -- ${.sh.file}) && pwd -P)/$(basename -- ${.sh.file})" ] \
+ && _pw_sourced=1
+ elif [ -n "$BASH_VERSION" ]; then
+ (return 0 2>/dev/null) && _pw_sourced=1
+ else # All other shells: examine $0 for known shell binary filenames
+ # Detects `sh` and `dash`; add additional shell filenames as needed.
+ case ${0##*/} in sh|dash) _pw_sourced=1;; esac
+ fi
+
+ _pw_eval_sourced "$_pw_sourced"
Downstream Projects Using Different Packages
********************************************
-
Projects depending on Pigweed but using additional or different packages should
copy the Pigweed `sample project`'s ``bootstrap.sh`` and ``config.json`` and
update the call to ``pw_bootstrap``. Search for "downstream" for other places
@@ -227,27 +228,27 @@ here.
.. code-block:: json
- {
- "included_files": [
- "foo.json"
- ],
- "packages": [
- {
- "path": "infra/3pp/tools/go/${platform}",
- "platforms": [
- "linux-amd64",
- "linux-arm64",
- "mac-amd64",
- "windows-amd64"
- ],
- "subdir": "pa/th",
- "tags": [
- "version:2@1.16.3"
- ],
- "version_file": ".versions/go.cipd_version"
- }
- ]
- }
+ {
+ "included_files": [
+ "foo.json"
+ ],
+ "packages": [
+ {
+ "path": "infra/3pp/tools/go/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "subdir": "pa/th",
+ "tags": [
+ "version:2@1.16.3"
+ ],
+ "version_file": ".versions/go.cipd_version"
+ }
+ ]
+ }
``virtualenv.gn_args``
Any necessary GN args to be used when installing Python packages.
@@ -262,6 +263,19 @@ here.
only installing Pigweed Python packages, use the location of the Pigweed
submodule.
+``virtualenv.requirements``
+ A list of Python Pip requirements files for installing into the Pigweed
+ virtualenv. Each file will be passed as additional ``--requirement`` argument
+ to a single ```pip install`` at the beginning of bootstrap's ``Python
+ environment`` setup stage. See the `Requirements Files documentation`_ for
+ details on what can be specified using requirements files.
+
+``virtualenv.constraints``
+ A list of Python Pip constraints files. These constraints will be passed to
+ every ``pip`` invocation as an additional ``--constraint`` argument during
+ bootstrap. virtualenv. See the `Constraints Files documentation`_ for details
+ on formatting.
+
``virtualenv.system_packages``
A boolean value that can be used the give the Python virtual environment
access to the system site packages. Defaults to ``false``.
@@ -289,43 +303,55 @@ here.
the environment, for reading by tools that don't inherit an environment from
a sourced ``bootstrap.sh``.
+``rosetta``
+ Whether to use Rosetta to use amd64 packages on arm64 Macs. Accepted values
+ are ``never``, ``allow``, and ``force``. For now, ``allow`` means ``force``.
+ At some point in the future ``allow`` will be changed to mean ``never``.
+
An example of a config file is below.
.. code-block:: json
- {
- "root_variable": "EXAMPLE_ROOT",
- "cipd_package_files": [
- "pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json",
- "pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json"
- "tools/myprojectname.json"
- ],
- "virtualenv": {
- "gn_root": ".",
- "gn_targets": [
- ":python.install",
- ],
- "system_packages": false
- },
- "pw_packages": [],
- "optional_submodules": [
- "optional/submodule/one",
- "optional/submodule/two"
- ],
- "gni_file": "tools/environment.gni",
- "json_file": "tools/environment.json"
- }
+ {
+ "root_variable": "EXAMPLE_ROOT",
+ "cipd_package_files": [
+ "pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json",
+ "pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json"
+ "tools/myprojectname.json"
+ ],
+ "virtualenv": {
+ "gn_root": ".",
+ "gn_targets": [
+ ":python.install",
+ ],
+ "system_packages": false
+ },
+ "pw_packages": [],
+ "optional_submodules": [
+ "optional/submodule/one",
+ "optional/submodule/two"
+ ],
+ "gni_file": "tools/environment.gni",
+ "json_file": "tools/environment.json",
+ "rosetta": "allow"
+ }
+
+Only the packages necessary for almost all projects based on Pigweed are
+included in the ``pigweed.json`` file. A number of other files are present in
+that directory for projects that need more than the minimum. Internal-Google
+projects using LUCI should at least include ``luci.json``.
In case the CIPD packages need to be referenced from other scripts, variables
like ``PW_${BASENAME}_CIPD_INSTALL_DIR`` point to the CIPD install directories,
-where ``${BASENAME}`` is "PIGWEED" for
-"pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json" and "LUCI" for
-"pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json". This example would
-set the following environment variables.
+where ``${BASENAME}`` is ``"PIGWEED"`` for
+``"pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json"`` and
+``"LUCI"`` for
+``"pigweed/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json"``. This example
+would set the following environment variables.
- - ``PW_LUCI_CIPD_INSTALL_DIR``
- - ``PW_MYPROJECTNAME_CIPD_INSTALL_DIR``
- - ``PW_PIGWEED_CIPD_INSTALL_DIR``
+- ``PW_LUCI_CIPD_INSTALL_DIR``
+- ``PW_MYPROJECTNAME_CIPD_INSTALL_DIR``
+- ``PW_PIGWEED_CIPD_INSTALL_DIR``
These directories are also referenced in the gni_file specified by the
environment config file as ``dir_cipd_${BASENAME}``. This allows the GN build to
@@ -371,8 +397,8 @@ versions of additional packages your project depends on, run
``.gn`` file (see `Pigweed's .gn file`_ for an example).
.. _pip constraints file: https://pip.pypa.io/en/stable/user_guide/#constraints-files
-.. _default constraints: https://cs.opensource.google/pigweed/pigweed/+/main:pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
-.. _Pigweed's .gn file: https://cs.opensource.google/pigweed/pigweed/+/main:.gn
+.. _default constraints: https://cs.pigweed.dev/pigweed/+/main:pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
+.. _Pigweed's .gn file: https://cs.pigweed.dev/pigweed/+/main:.gn
To update packages, set ``pw_build_PIP_CONSTRAINTS = []``, delete the
environment, and bootstrap again. Then run the ``list`` command from above
@@ -400,9 +426,15 @@ never need to set these.
Python executable to be used, for example "python2" or "python3". Defaults to
"python".
+``PW_CIPD_SERVICE_ACCOUNT_JSON``
+ Value to pass as ``-service-account-json`` to CIPD invocations. This should
+ point either to a service account JSON key file, or be the magical value
+ ``:gce`` to tell the tool to fetch tokens from GCE metadata server.
+
``PW_ENVIRONMENT_ROOT``
- Location to which packages are installed. Defaults to ``.environment`` folder
- within the checkout root.
+ Location to which packages are installed. Defaults to ``environment`` folder
+ within the checkout root. This variable is cleared after environment setup is
+ complete.
``PW_ENVSETUP_DISABLE_SPINNER``
Disable the spinner during env setup. Intended to be used when the output is
@@ -449,6 +481,9 @@ The following environment variables are set by env setup.
all-caps version of the basename of the package file, without the extension.
(E.g., "path/foo.json" becomes ``PW_FOO_CIPD_INSTALL_DIR``.)
+``PW_PACKAGE_ROOT``
+ Location that packages installed by ``pw package`` will be installed to.
+
``VIRTUAL_ENV``
Path to Pigweed's virtualenv.
@@ -464,60 +499,60 @@ and modify. An example ``actions.json`` is shown below. The "append" and
.. code-block:: json
- {
- "modify": {
- "PATH": {
- "append": [],
- "prepend": [
- "<pigweed-root>/.environment/cipd",
- "<pigweed-root>/.environment/cipd/pigweed",
- "<pigweed-root>/.environment/cipd/pigweed/bin",
- "<pigweed-root>/.environment/cipd/luci",
- "<pigweed-root>/.environment/cipd/luci/bin",
- "<pigweed-root>/.environment/pigweed-venv/bin",
- "<pigweed-root>/out/host/host_tools"
- ],
- "remove": []
- }
- },
- "set": {
- "PW_PROJECT_ROOT": "<pigweed-root>",
- "PW_ROOT": "<pigweed-root>",
- "_PW_ACTUAL_ENVIRONMENT_ROOT": "<pigweed-root>/.environment",
- "PW_CIPD_INSTALL_DIR": "<pigweed-root>/.environment/cipd",
- "CIPD_CACHE_DIR": "<home>/.cipd-cache-dir",
- "PW_PIGWEED_CIPD_INSTALL_DIR": "<pigweed-root>/.environment/cipd/pigweed",
- "PW_LUCI_CIPD_INSTALL_DIR": "<pigweed-root>/.environment/cipd/luci",
- "VIRTUAL_ENV": "<pigweed-root>/.environment/pigweed-venv",
- "PYTHONHOME": null,
- "__PYVENV_LAUNCHER__": null
- }
- }
+ {
+ "modify": {
+ "PATH": {
+ "append": [],
+ "prepend": [
+ "<pigweed-root>/environment/cipd",
+ "<pigweed-root>/environment/cipd/pigweed",
+ "<pigweed-root>/environment/cipd/pigweed/bin",
+ "<pigweed-root>/environment/cipd/luci",
+ "<pigweed-root>/environment/cipd/luci/bin",
+ "<pigweed-root>/environment/pigweed-venv/bin",
+ "<pigweed-root>/out/host/host_tools"
+ ],
+ "remove": []
+ }
+ },
+ "set": {
+ "PW_PROJECT_ROOT": "<pigweed-root>",
+ "PW_ROOT": "<pigweed-root>",
+ "_PW_ACTUAL_ENVIRONMENT_ROOT": "<pigweed-root>/environment",
+ "PW_CIPD_INSTALL_DIR": "<pigweed-root>/environment/cipd",
+ "CIPD_CACHE_DIR": "<home>/.cipd-cache-dir",
+ "PW_PIGWEED_CIPD_INSTALL_DIR": "<pigweed-root>/environment/cipd/pigweed",
+ "PW_LUCI_CIPD_INSTALL_DIR": "<pigweed-root>/environment/cipd/luci",
+ "VIRTUAL_ENV": "<pigweed-root>/environment/pigweed-venv",
+ "PYTHONHOME": null,
+ "__PYVENV_LAUNCHER__": null
+ }
+ }
Many of these variables are directly exposed to the GN build as well, through
the GNI file specified in the environment config file.
.. code-block::
- declare_args() {
- dir_cipd_pigweed = "<pigweed-root>/.environment/cipd/packages/pigweed"
- dir_cipd_luci = "<pigweed-root>/.environment/cipd/packages/luci"
- dir_virtual_env = "<pigweed-root>/.environment/pigweed-venv"
- }
+ declare_args() {
+ pw_env_setup_CIPD_PIGWEED = "<environment-root>/cipd/packages/pigweed"
+ pw_env_setup_CIPD_LUCI = "<environment-root>/cipd/packages/luci"
+ pw_env_setup_VIRTUAL_ENV = "<environment-root>/pigweed-venv"
+ pw_env_setup_PACKAGE_ROOT = "<environment-root>/packages"
+ }
It's straightforward to use these variables.
.. code-block:: cpp
- import("//build_overrides/pigweed_environment.gni")
+ import("//build_overrides/pigweed_environment.gni")
- deps = [ "$dir_cipd_pigweed/..." ]
+ deps = [ "$pw_env_setup_CIPD_PIGWEED/..." ]
Implementation
**************
-
The environment is set up by installing CIPD and Python packages in
-``PW_ENVIRONMENT_ROOT`` or ``<checkout>/.environment``, and saving modifications
+``PW_ENVIRONMENT_ROOT`` or ``<checkout>/environment``, and saving modifications
to environment variables in setup scripts in those directories. To support
multiple operating systems this is done in an operating system-agnostic manner
and then written into operating system-specific files to be sourced now and in
@@ -528,3 +563,6 @@ high-level commands to system-specific initialization files is shown below.
.. image:: doc_resources/pw_env_setup_output.png
:alt: Mapping of high-level commands to system-specific commands.
:align: left
+
+.. _Requirements Files documentation: https://pip.pypa.io/en/stable/user_guide/#requirements-files
+.. _Constraints Files documentation: https://pip.pypa.io/en/stable/user_guide/#constraints-files
diff --git a/pw_env_setup/get_pw_env_setup.sh b/pw_env_setup/get_pw_env_setup.sh
index ff74220bd..88f25ba69 100755
--- a/pw_env_setup/get_pw_env_setup.sh
+++ b/pw_env_setup/get_pw_env_setup.sh
@@ -14,7 +14,7 @@
# the License.
if [ -z "$PW_ENVIRONMENT_ROOT" ]; then
- PW_ENVIRONMENT_ROOT="$PW_ROOT/.environment"
+ PW_ENVIRONMENT_ROOT="$PW_ROOT/environment"
fi
PREFIX="$PW_ENVIRONMENT_ROOT/bootstrap"
mkdir -p "$PREFIX"
@@ -47,7 +47,10 @@ if [ "$ARCH" = "x86_64" ]; then
fi
# Support `mac-arm64` through Rosetta until `mac-arm64` binaries are ready
-if [[ "$OS" = "mac" ] && [ "$ARCH" = "arm64" ]]; then
+if [[ "$OS" = "mac" ] && \
+ [ "$ARCH" = "arm64" ] && \
+ [ -n "$PW_BOOTSTRAP_USE_ROSETTA" ]]
+then
ARCH="amd64"
fi
diff --git a/pw_env_setup/post-checkout-hook-helper.sh b/pw_env_setup/post-checkout-hook-helper.sh
index 91d106488..3708798fa 100755
--- a/pw_env_setup/post-checkout-hook-helper.sh
+++ b/pw_env_setup/post-checkout-hook-helper.sh
@@ -33,6 +33,7 @@ echo -n "Updating CIPD packages..."
--install-dir "$_PW_ACTUAL_ENVIRONMENT_ROOT" \
--quiet \
--trust-cipd-hash \
+ --skip-submodule-check \
--cipd-only
echo "done."
diff --git a/pw_env_setup/py/BUILD.gn b/pw_env_setup/py/BUILD.gn
index cce9a21b8..7b68fcbda 100644
--- a/pw_env_setup/py/BUILD.gn
+++ b/pw_env_setup/py/BUILD.gn
@@ -30,6 +30,7 @@ pw_python_package("py") {
"pw_env_setup/cipd_setup/update.py",
"pw_env_setup/cipd_setup/wrapper.py",
"pw_env_setup/colors.py",
+ "pw_env_setup/config_file.py",
"pw_env_setup/env_setup.py",
"pw_env_setup/environment.py",
"pw_env_setup/gni_visitor.py",
@@ -48,4 +49,5 @@ pw_python_package("py") {
"json_visitor_test.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_env_setup/py/environment_test.py b/pw_env_setup/py/environment_test.py
index 6b12b99e4..54627a4cd 100644
--- a/pw_env_setup/py/environment_test.py
+++ b/pw_env_setup/py/environment_test.py
@@ -55,10 +55,11 @@ def _evaluate_env_in_shell(env):
# Write env sourcing script to file.
with tempfile.NamedTemporaryFile(
- prefix='pw-test-written-env-',
- suffix='.bat' if os.name == 'nt' else '.sh',
- delete=False,
- mode='w+') as temp:
+ prefix='pw-test-written-env-',
+ suffix='.bat' if os.name == 'nt' else '.sh',
+ delete=False,
+ mode='w+',
+ ) as temp:
env.write(temp)
temp_name = temp.name
@@ -95,6 +96,7 @@ def _evaluate_env_in_shell(env):
# pylint: disable=too-many-public-methods
class EnvironmentTest(unittest.TestCase):
"""Tests for env_setup.environment."""
+
def setUp(self):
self.env = environment.Environment()
@@ -245,6 +247,7 @@ class EnvironmentTest(unittest.TestCase):
class _PrependAppendEnvironmentTest(unittest.TestCase):
"""Tests for env_setup.environment."""
+
def __init__(self, *args, **kwargs):
windows = kwargs.pop('windows', False)
pathsep = kwargs.pop('pathsep', os.pathsep)
@@ -259,17 +262,18 @@ class _PrependAppendEnvironmentTest(unittest.TestCase):
# Likewise if we're testing POSIX behavior and actually on a POSIX
# system. Tests can check self.run_shell_tests and exit without
# doing anything.
- real_windows = (os.name == 'nt')
- self.run_shell_tests = (self.windows == real_windows)
+ real_windows = os.name == 'nt'
+ self.run_shell_tests = self.windows == real_windows
def setUp(self):
- self.env = environment.Environment(windows=self.windows,
- pathsep=self.pathsep,
- allcaps=self.allcaps)
+ self.env = environment.Environment(
+ windows=self.windows, pathsep=self.pathsep, allcaps=self.allcaps
+ )
self.var_already_set = self.env.normalize_key('VAR_ALREADY_SET')
os.environ[self.var_already_set] = self.pathsep.join(
- 'one two three'.split())
+ 'one two three'.split()
+ )
self.assertIn(self.var_already_set, os.environ)
self.var_not_set = self.env.normalize_key('VAR_NOT_SET')
@@ -293,8 +297,9 @@ class _AppendPrependTestMixin(object):
orig = os.environ[self.var_already_set]
self.env.prepend(self.var_already_set, 'path')
with self.env(export=False) as env:
- self.assertEqual(env[self.var_already_set],
- self.pathsep.join(('path', orig)))
+ self.assertEqual(
+ env[self.var_already_set], self.pathsep.join(('path', orig))
+ )
def test_prepend_present_written(self):
if not self.run_shell_tests:
@@ -303,8 +308,9 @@ class _AppendPrependTestMixin(object):
orig = os.environ[self.var_already_set]
self.env.prepend(self.var_already_set, 'path')
env = _evaluate_env_in_shell(self.env)
- self.assertEqual(env[self.var_already_set],
- self.pathsep.join(('path', orig)))
+ self.assertEqual(
+ env[self.var_already_set], self.pathsep.join(('path', orig))
+ )
def test_prepend_notpresent_ctx(self):
self.env.prepend(self.var_not_set, 'path')
@@ -323,8 +329,9 @@ class _AppendPrependTestMixin(object):
orig = os.environ[self.var_already_set]
self.env.append(self.var_already_set, 'path')
with self.env(export=False) as env:
- self.assertEqual(env[self.var_already_set],
- self.pathsep.join((orig, 'path')))
+ self.assertEqual(
+ env[self.var_already_set], self.pathsep.join((orig, 'path'))
+ )
def test_append_present_written(self):
if not self.run_shell_tests:
@@ -333,8 +340,9 @@ class _AppendPrependTestMixin(object):
orig = os.environ[self.var_already_set]
self.env.append(self.var_already_set, 'path')
env = _evaluate_env_in_shell(self.env)
- self.assertEqual(env[self.var_already_set],
- self.pathsep.join((orig, 'path')))
+ self.assertEqual(
+ env[self.var_already_set], self.pathsep.join((orig, 'path'))
+ )
def test_append_notpresent_ctx(self):
self.env.append(self.var_not_set, 'path')
@@ -350,52 +358,65 @@ class _AppendPrependTestMixin(object):
self.assertEqual(env[self.var_not_set], 'path')
def test_remove_ctx(self):
- self.env.set(self.var_not_set,
- self.pathsep.join(('path', 'one', 'path', 'two', 'path')))
+ self.env.set(
+ self.var_not_set,
+ self.pathsep.join(('path', 'one', 'path', 'two', 'path')),
+ )
self.env.append(self.var_not_set, 'path')
with self.env(export=False) as env:
- self.assertEqual(env[self.var_not_set],
- self.pathsep.join(('one', 'two', 'path')))
+ self.assertEqual(
+ env[self.var_not_set], self.pathsep.join(('one', 'two', 'path'))
+ )
def test_remove_written(self):
if not self.run_shell_tests:
return
- if self.windows: # TODO(pwbug/231) Re-enable for Windows.
+ if self.windows:
return
- self.env.set(self.var_not_set,
- self.pathsep.join(('path', 'one', 'path', 'two', 'path')))
+ self.env.set(
+ self.var_not_set,
+ self.pathsep.join(('path', 'one', 'path', 'two', 'path')),
+ )
self.env.append(self.var_not_set, 'path')
env = _evaluate_env_in_shell(self.env)
- self.assertEqual(env[self.var_not_set],
- self.pathsep.join(('one', 'two', 'path')))
+ self.assertEqual(
+ env[self.var_not_set], self.pathsep.join(('one', 'two', 'path'))
+ )
def test_remove_ctx_space(self):
- self.env.set(self.var_not_set,
- self.pathsep.join(('pa th', 'one', 'pa th', 'two')))
+ self.env.set(
+ self.var_not_set,
+ self.pathsep.join(('pa th', 'one', 'pa th', 'two')),
+ )
self.env.append(self.var_not_set, 'pa th')
with self.env(export=False) as env:
- self.assertEqual(env[self.var_not_set],
- self.pathsep.join(('one', 'two', 'pa th')))
+ self.assertEqual(
+ env[self.var_not_set],
+ self.pathsep.join(('one', 'two', 'pa th')),
+ )
def test_remove_written_space(self):
if not self.run_shell_tests:
return
- if self.windows: # TODO(pwbug/231) Re-enable for Windows.
+ if self.windows:
return
- self.env.set(self.var_not_set,
- self.pathsep.join(('pa th', 'one', 'pa th', 'two')))
+ self.env.set(
+ self.var_not_set,
+ self.pathsep.join(('pa th', 'one', 'pa th', 'two')),
+ )
self.env.append(self.var_not_set, 'pa th')
env = _evaluate_env_in_shell(self.env)
- self.assertEqual(env[self.var_not_set],
- self.pathsep.join(('one', 'two', 'pa th')))
+ self.assertEqual(
+ env[self.var_not_set], self.pathsep.join(('one', 'two', 'pa th'))
+ )
def test_remove_ctx_empty(self):
self.env.remove(self.var_not_set, 'path')
@@ -411,8 +432,9 @@ class _AppendPrependTestMixin(object):
self.assertNotIn(self.var_not_set, env)
-class WindowsEnvironmentTest(_PrependAppendEnvironmentTest,
- _AppendPrependTestMixin):
+class WindowsEnvironmentTest(
+ _PrependAppendEnvironmentTest, _AppendPrependTestMixin
+):
def __init__(self, *args, **kwargs):
kwargs['pathsep'] = ';'
kwargs['windows'] = True
@@ -420,14 +442,15 @@ class WindowsEnvironmentTest(_PrependAppendEnvironmentTest,
super(WindowsEnvironmentTest, self).__init__(*args, **kwargs)
-class PosixEnvironmentTest(_PrependAppendEnvironmentTest,
- _AppendPrependTestMixin):
+class PosixEnvironmentTest(
+ _PrependAppendEnvironmentTest, _AppendPrependTestMixin
+):
def __init__(self, *args, **kwargs):
kwargs['pathsep'] = ':'
kwargs['windows'] = False
kwargs['allcaps'] = False
super(PosixEnvironmentTest, self).__init__(*args, **kwargs)
- self.real_windows = (os.name == 'nt')
+ self.real_windows = os.name == 'nt'
class WindowsCaseInsensitiveTest(unittest.TestCase):
@@ -456,5 +479,6 @@ class WindowsCaseInsensitiveTest(unittest.TestCase):
if __name__ == '__main__':
import sys
+
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
unittest.main()
diff --git a/pw_env_setup/py/json_visitor_test.py b/pw_env_setup/py/json_visitor_test.py
index 73ed3c725..3baddde86 100644
--- a/pw_env_setup/py/json_visitor_test.py
+++ b/pw_env_setup/py/json_visitor_test.py
@@ -34,6 +34,7 @@ from pw_env_setup import environment, json_visitor
# pylint: disable=super-with-arguments
class JSONVisitorTest(unittest.TestCase):
"""Tests for env_setup.json_visitor."""
+
def setUp(self):
self.env = environment.Environment()
@@ -60,22 +61,16 @@ class JSONVisitorTest(unittest.TestCase):
self.env.append('VAR', 'path2')
self.env.append('VAR', 'path3')
self._assert_json(
- {'modify': {
- 'VAR': {
- 'append': 'path1 path2 path3'.split()
- }
- }})
+ {'modify': {'VAR': {'append': 'path1 path2 path3'.split()}}}
+ )
def test_prepend(self):
self.env.prepend('VAR', 'path1')
self.env.prepend('VAR', 'path2')
self.env.prepend('VAR', 'path3')
self._assert_json(
- {'modify': {
- 'VAR': {
- 'prepend': 'path3 path2 path1'.split()
- }
- }})
+ {'modify': {'VAR': {'prepend': 'path3 path2 path1'.split()}}}
+ )
def test_remove(self):
self.env.remove('VAR', 'path1')
diff --git a/pw_env_setup/py/pw_env_setup/apply_visitor.py b/pw_env_setup/py/pw_env_setup/apply_visitor.py
index b26f33a7b..c3449e413 100644
--- a/pw_env_setup/py/pw_env_setup/apply_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/apply_visitor.py
@@ -21,6 +21,7 @@ import os
class ApplyVisitor(object): # pylint: disable=useless-object-inheritance
"""Applies an Environment to the current process."""
+
def __init__(self, *args, **kwargs):
pathsep = kwargs.pop('pathsep', os.pathsep)
super(ApplyVisitor, self).__init__(*args, **kwargs)
@@ -51,11 +52,13 @@ class ApplyVisitor(object): # pylint: disable=useless-object-inheritance
def visit_prepend(self, prepend):
self._environ[prepend.name] = self._pathsep.join(
- (prepend.value, self._environ.get(prepend.name, '')))
+ (prepend.value, self._environ.get(prepend.name, ''))
+ )
def visit_append(self, append):
self._environ[append.name] = self._pathsep.join(
- (self._environ.get(append.name, ''), append.value))
+ (self._environ.get(append.name, ''), append.value)
+ )
def visit_echo(self, echo):
pass # Not relevant for apply.
diff --git a/pw_env_setup/py/pw_env_setup/batch_visitor.py b/pw_env_setup/py/pw_env_setup/batch_visitor.py
index e7c535381..4ca43ba1e 100644
--- a/pw_env_setup/py/pw_env_setup/batch_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/batch_visitor.py
@@ -22,6 +22,7 @@ _SCRIPT_END_LABEL = '_pw_end'
class BatchVisitor(object): # pylint: disable=useless-object-inheritance
"""Serializes an Environment into a batch file."""
+
def __init__(self, *args, **kwargs):
pathsep = kwargs.pop('pathsep', ':')
super(BatchVisitor, self).__init__(*args, **kwargs)
@@ -33,7 +34,8 @@ class BatchVisitor(object): # pylint: disable=useless-object-inheritance
try:
self._replacements = tuple(
(key, env.get(key) if value is None else value)
- for key, value in env.replacements)
+ for key, value in env.replacements
+ )
self._outs = outs
self._outs.write('@echo off\n')
@@ -54,8 +56,9 @@ class BatchVisitor(object): # pylint: disable=useless-object-inheritance
def visit_set(self, set): # pylint: disable=redefined-builtin
value = self._apply_replacements(set)
- self._outs.write('set {name}={value}\n'.format(name=set.name,
- value=value))
+ self._outs.write(
+ 'set {name}={value}\n'.format(name=set.name, value=value)
+ )
def visit_clear(self, clear):
self._outs.write('set {name}=\n'.format(name=clear.name))
@@ -71,14 +74,16 @@ class BatchVisitor(object): # pylint: disable=useless-object-inheritance
def visit_prepend(self, prepend):
value = self._apply_replacements(prepend)
value = self._join(value, '%{}%'.format(prepend.name))
- self._outs.write('set {name}={value}\n'.format(name=prepend.name,
- value=value))
+ self._outs.write(
+ 'set {name}={value}\n'.format(name=prepend.name, value=value)
+ )
def visit_append(self, append):
value = self._apply_replacements(append)
value = self._join('%{}%'.format(append.name), value)
- self._outs.write('set {name}={value}\n'.format(name=append.name,
- value=value))
+ self._outs.write(
+ 'set {name}={value}\n'.format(name=append.name, value=value)
+ )
def visit_echo(self, echo):
if echo.newline:
@@ -101,14 +106,17 @@ class BatchVisitor(object): # pylint: disable=useless-object-inheritance
# Assume failing command produced relevant output.
self._outs.write(
- 'if %ERRORLEVEL% neq 0 goto {}\n'.format(_SCRIPT_END_LABEL))
+ 'if %ERRORLEVEL% neq 0 goto {}\n'.format(_SCRIPT_END_LABEL)
+ )
def visit_doctor(self, doctor):
self._outs.write('if "%PW_ACTIVATE_SKIP_CHECKS%"=="" (\n')
self.visit_command(doctor)
self._outs.write(') else (\n')
- self._outs.write('echo Skipping environment check because '
- 'PW_ACTIVATE_SKIP_CHECKS is set\n')
+ self._outs.write(
+ 'echo Skipping environment check because '
+ 'PW_ACTIVATE_SKIP_CHECKS is set\n'
+ )
self._outs.write(')\n')
def visit_blank_line(self, blank_line):
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version b/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version
index 815525ce3..8c208ba63 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version
@@ -1 +1 @@
-git_revision:1897f73d5e9b48a00ac6935127751b18a8e2a6d7
+git_revision:fc9d7a3dff50d001c7c7dcf9f5f312be5d2d2ab2
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests b/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests
index a1226a1d0..83cff0d34 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/.cipd_version.digests
@@ -1,23 +1,25 @@
# This file was generated by
#
# cipd selfupdate-roll -version-file .cipd_version \
-# -version git_revision:1897f73d5e9b48a00ac6935127751b18a8e2a6d7
+# -version git_revision:fc9d7a3dff50d001c7c7dcf9f5f312be5d2d2ab2
#
# Do not modify manually. All changes will be overwritten.
# Use 'cipd selfupdate-roll ...' to modify.
-aix-ppc64 sha256 f3c819c44da1ebe55cea4e792c3b311218d5beab419a2e2fe1c65520a97bf88f
-linux-386 sha256 c0a16c1521e6691184eebcb971a3a4c0b5144a3922b4aeaaa4cdb2eabf5c5ab3
-linux-amd64 sha256 bc3ae4d3dfff7827930cfb92fa7aa067ae2428acb0d46c5c5269fe8268faa65b
-linux-arm64 sha256 312a9c2f4de0fb74fb510909b182f6dc34b59590022ccd8dbb23f0049029aea4
-linux-armv6l sha256 552221c6f5f2e14522b8200ccb83d94052bb51c1d640c85d11671535fa028b88
-linux-mips64 sha256 2f4c3b33b925ecfabe2eb8569e952971f85dee165ab77cde367ef182a3fa2b9f
-linux-mips64le sha256 b40e5b0d2bcebfbb7dca4e1221b57b39b38cc57ce943300120ffdcf6ea83f2d3
-linux-mipsle sha256 bd0ed1738e4597d8f45b969e80fbd1919792be7bf4cb862f5ece442d4050aea9
-linux-ppc64 sha256 242b99f085473cdbcfa66fc800248a43863b1c1b932436f464de5cd0b79d3a17
-linux-ppc64le sha256 0001aa0ee79450e1efaedf58b032f8a491c0006a1b66b440b91bef0f2afe883b
-linux-s390x sha256 f15344e8b15c3e1448894a9d4511974fa8225e8bc9ebe61e5cb09cc68749c31e
-mac-amd64 sha256 d951395043a43f4f972af9a858550e9ffe82acabd39da301e025070357c8b2a1
-mac-arm64 sha256 9e298f81d18413910daa68392b0e60e9b40b9c0ab1fb817596b21669af36fd65
-windows-386 sha256 2ade2352f199d07f565701a09da972328de46dc66a26c216bb83c501f0d3e947
-windows-amd64 sha256 49cb17221ef4e6b6344c0b521614ec5845da60c3901b39297af6927bca435295
+aix-ppc64 sha256 85a888acea9b4ac164f5cd5372e49817b8d370bd5c8231ac1bb77f2b8ed01e69
+linux-386 sha256 3201e8c5ad57b010a5fbd89783a56fbddd4dc75a96d3117017f3c15c16139736
+linux-amd64 sha256 f907698b1717f54834adc921eb080021efb76b3345daf470d10b53cbe7b37b6e
+linux-arm64 sha256 270881ea4f432d2503f828222f9e2cc63b03f6a7716b2da0922a70ce84c83ff7
+linux-armv6l sha256 ae222dfee5fdd224f1c55cec215a6fde3839adcc342bc82bfcea238e5d399a89
+linux-mips64 sha256 235bf307c9e1b35127249289c82bbc52a713d8cbc931f967963ea4b9d56b3c17
+linux-mips64le sha256 c40ef945f04023fca51cbb9aaf478585bdcc87aad74f2373896070d6b7d0f9e2
+linux-mipsle sha256 647820d9efc212cf640ad1ddd7fb9436ec6cefee10906b36ec08436df9112d0f
+linux-ppc64 sha256 364e433abd4b4e313a0d14e888430613a6fb58903bc5456782ecadb5583d7f8c
+linux-ppc64le sha256 3a26b608c6a93141c9bd2dc10ffc97214ab5b79eb67452f2235c3c903f3f82cb
+linux-riscv64 sha256 35e4fc4c5f1bafdd9028e082c1cc8d565cb0c407d1574b2ff29bd491bd7ba780
+linux-s390x sha256 80918c0fc689ac9efa22cc4929a370114b133cb97ba79e42ea4332418db404da
+mac-amd64 sha256 5ae46728fb897d28e9688272a517dee4e554c999134fdd5d0db66f6661170a1b
+mac-arm64 sha256 723ee4f5fd9d39f3e2f27d9fe1e2354da45c28ad861726937eabff7325659290
+windows-386 sha256 d7553e8a758339343d583929ff9ca6ae6d4b0e81928803abb28193a954243164
+windows-amd64 sha256 3ef438c1a2a5fc86b52043519786331b0a79f9d226f3fa5b2294299857f04766
+windows-arm64 sha256 744e9ddda0f5560a0e7e34108969054c1d3626305a5d51b507e70b36f3c2cbb9
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/arm.json b/pw_env_setup/py/pw_env_setup/cipd_setup/arm.json
index ff12cd23f..31cac6493 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/arm.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/arm.json
@@ -1,16 +1,17 @@
{
"packages": [
{
- "path": "pigweed/third_party/gcc-arm-none-eabi/${platform}",
+ "path": "fuchsia/third_party/armgcc/${platform}",
"platforms": [
"linux-amd64",
+ "linux-arm64",
"mac-amd64",
"windows-amd64"
],
"tags": [
- "version:10-2020-q4-major"
+ "version:2@10.3-2021.10.1"
],
- "version_file": ".versions/gcc-arm-none-eabi.cipd_version"
+ "version_file": ".versions/gcc-arm-none-eabi.version"
}
]
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json b/pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json
index c053f272e..f555cca25 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/bazel.json
@@ -1,14 +1,17 @@
{
+ "included_files": [
+ "buildifier.json"
+ ],
"packages": [
{
- "path": "pigweed/third_party/bazel/${platform}",
+ "path": "fuchsia/third_party/bazel/${platform}",
"platforms": [
"linux-amd64",
"mac-amd64",
"windows-amd64"
],
"tags": [
- "version:5.0.0.1"
+ "version:2@6.0.0.5"
],
"version_file": ".versions/bazel.cipd_version"
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/black.json b/pw_env_setup/py/pw_env_setup/cipd_setup/black.json
new file mode 100644
index 000000000..eaa4fefff
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/black.json
@@ -0,0 +1,16 @@
+{
+ "packages": [
+ {
+ "path": "fuchsia/third_party/black/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@22.10.0.1"
+ ],
+ "version_file": ".versions/black.version"
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json b/pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json
new file mode 100644
index 000000000..d21126d82
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/buildifier.json
@@ -0,0 +1,17 @@
+{
+ "packages": [
+ {
+ "path": "infra/3pp/tools/buildifier/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@6.0.1"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json b/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json
new file mode 100644
index 000000000..e200afe69
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/cmake.json
@@ -0,0 +1,20 @@
+{
+ "packages": [
+ {
+ "_comment": {
+ "version_file": ".versions/cmake.cipd_version"
+ },
+ "path": "infra/3pp/tools/cmake/${platform}",
+ "platforms": [
+ "mac-amd64",
+ "mac-arm64",
+ "linux-amd64",
+ "linux-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@3.26.0.chromium.7"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json b/pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json
index 7afe4d365..ddbcdac15 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/compatibility.json
@@ -1,15 +1,16 @@
{
"packages": [
{
- "path": "infra/3pp/tools/cpython3/${os}-${arch=amd64}",
+ "path": "infra/3pp/tools/cpython3/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
"tags": [
- "version:3.7.7.chromium.10"
- ]
- },
- {
- "path": "pigweed/third_party/gcc-arm-none-eabi/${os}-${arch=amd64}",
- "tags": [
- "version:9-2020-q2-update"
+ "version:2@3.8.10.chromium.24"
]
}
]
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/default.json b/pw_env_setup/py/pw_env_setup/cipd_setup/default.json
index b20f27051..292544092 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/default.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/default.json
@@ -1,9 +1,8 @@
{
"included_files": [
- "pigweed.json",
"arm.json",
- "python.json",
- "bazel.json",
- "luci.json"
+ "buildifier.json",
+ "pigweed.json",
+ "python.json"
]
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/doxygen.json b/pw_env_setup/py/pw_env_setup/cipd_setup/doxygen.json
new file mode 100644
index 000000000..603fa8ecc
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/doxygen.json
@@ -0,0 +1,15 @@
+{
+ "packages": [
+ {
+ "path": "pigweed/third_party/doxygen/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:1.9.6-1"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/go.json b/pw_env_setup/py/pw_env_setup/cipd_setup/go.json
new file mode 100644
index 000000000..0feb0dbaa
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/go.json
@@ -0,0 +1,28 @@
+{
+ "packages": [
+ {
+ "path": "infra/3pp/tools/go/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@1.20.2"
+ ]
+ },
+ {
+ "path": "pigweed/third_party/protoc-gen-go/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:1.3.2"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json b/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json
new file mode 100644
index 000000000..cf0e8f7bf
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/host_tools.json
@@ -0,0 +1,16 @@
+{
+ "packages": [
+ {
+ "path": "pigweed/host_tools/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "git_revision:284a05aeae3cf2e4579e6518ab3a5316058da6d4"
+ ],
+ "version_file": ".versions/host_tools.cipd_version"
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/kythe.json b/pw_env_setup/py/pw_env_setup/cipd_setup/kythe.json
index 05407096c..805191f99 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/kythe.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/kythe.json
@@ -1,12 +1,12 @@
{
- 'packages': [
+ "packages": [
{
"path": "fuchsia/third_party/kythe",
"platforms": [
"linux-amd64"
],
"tags": [
- "version:0.0.46"
+ "version:1.0.3"
]
},
{
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json b/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json
index e7519557a..87c746c53 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/luci.json
@@ -85,6 +85,20 @@
"version_file": ".versions/led.cipd_version"
},
{
+ "path": "infra/tools/luci/swarming/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "latest"
+ ],
+ "version_file": ".versions/swarming.cipd_version"
+ },
+ {
"path": "infra/tools/luci/logdog/logdog/${platform}",
"platforms": [
"linux-amd64",
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json b/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
index 6d893858d..117ad9712 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/pigweed.json
@@ -10,7 +10,7 @@
"windows-amd64"
],
"tags": [
- "git_revision:f27bae882b2178ccc3c24f314c88db9a34118992"
+ "git_revision:41fef642de70ecdcaaa26be96d56a0398f95abd4"
],
"version_file": ".versions/gn.cipd_version"
},
@@ -24,32 +24,19 @@
"windows-amd64"
],
"tags": [
- "version:2@1.10.2"
+ "version:2@1.11.1.chromium.7"
]
},
{
- "path": "fuchsia/third_party/cmake/${platform}",
- "platforms": [
- "mac-amd64",
- "linux-amd64",
- "linux-arm64",
- "windows-amd64"
- ],
- "tags": [
- "version:3.23.20220402-g6733ad4"
- ],
- "version_file": ".versions/cmake.cipd_version"
- },
- {
- "path": "pigweed/third_party/bloaty-embedded/${platform}",
+ "path": "fuchsia/third_party/bloaty/${platform}",
"platforms": [
"linux-amd64",
"mac-amd64"
],
"tags": [
- "git_revision:2d87d204057b419f5290f8d38b61b9c2c5b4fb52-2"
+ "git_revision:c057ba4f43db0506d4ba8c096925b054b02a8bd3"
],
- "version_file": ".versions/bloaty-embedded.cipd_version"
+ "version_file": ".versions/bloaty.cipd_version"
},
{
"path": "infra/3pp/tools/protoc/${platform}",
@@ -64,7 +51,7 @@
]
},
{
- "_comment": "Until there's an arm64 Mac version, get the amd64 Mac version on arm64 Macs.",
+ "_comment": "Always get the amd64 version on Mac until there's an arm64 version",
"path": "infra/3pp/tools/protoc/mac-amd64",
"platforms": [
"mac-arm64"
@@ -82,33 +69,22 @@
"windows-amd64"
],
"tags": [
- "git_revision:1aa59ff2f789776ebfa2d4b315fd3ea589652b4a"
+ "git_revision:3a20597776a5d2920e511d81653b4d2b6ca0c855"
],
"version_file": ".versions/clang.cipd_version"
},
{
- "path": "infra/3pp/tools/go/${platform}",
+ "path": "fuchsia/third_party/rust/${platform}",
"platforms": [
"linux-amd64",
"linux-arm64",
- "mac-amd64",
- "mac-arm64",
- "windows-amd64"
+ "mac-amd64"
],
+ "subdir": "rust",
"tags": [
- "version:2@1.18"
- ]
- },
- {
- "path": "pigweed/third_party/protoc-gen-go/${platform}",
- "platforms": [
- "linux-amd64",
- "mac-amd64",
- "windows-amd64"
+ "git_revision:b7b7f2716ee1655a696d3d64c3e12638d0dd19c0"
],
- "tags": [
- "version:1.3.2"
- ]
+ "version_file": ".versions/rust.cipd_version"
},
{
"path": "infra/3pp/tools/openocd/${platform}",
@@ -116,8 +92,7 @@
"linux-amd64",
"linux-arm64",
"mac-amd64",
- "mac-arm64",
- "windows-amd64"
+ "mac-arm64"
],
"tags": [
"version:2@0.11.0-3"
@@ -133,28 +108,6 @@
]
},
{
- "path": "pigweed/host_tools/${platform}",
- "platforms": [
- "linux-amd64",
- "mac-amd64",
- "windows-amd64"
- ],
- "tags": [
- "git_revision:3f30a2e1ac848bd3dde1322ee2ace4d4f935c29d"
- ],
- "version_file": ".versions/host_tools.cipd_version"
- },
- {
- "path": "infra/rbe/client/${platform}",
- "platforms": [
- "linux-amd64",
- "windows-amd64"
- ],
- "tags": [
- "re_client_version:0.41.4.3f0d8bb"
- ]
- },
- {
"path": "fuchsia/third_party/qemu/${platform}",
"platforms": [
"linux-amd64",
@@ -162,51 +115,19 @@
"mac-amd64"
],
"tags": [
- "git_revision:44f28df24767cf9dca1ddc9b23157737c4cbb645"
+ "git_revision:823a3f11fb8f04c3c3cc0f95f968fef1bfc6534f,1"
],
"version_file": ".versions/qemu.cipd_version"
},
{
- "path": "fuchsia/third_party/kythe",
- "platforms": [
- "linux-amd64"
- ],
- "subdir": "kythe",
- "tags": [
- "version:1.0.3"
- ]
- },
- {
- "path": "fuchsia/third_party/kythe-libs/${platform}",
- "platforms": [
- "linux-amd64"
- ],
- "subdir": "kythe",
- "tags": [
- "version:2020-08-05"
- ]
- },
- {
- "path": "infra/3pp/tools/renode/${platform}",
+ "path": "fuchsia/third_party/sysroot/linux",
"platforms": [
"linux-amd64",
"linux-arm64"
],
+ "subdir": "clang_sysroot",
"tags": [
- "version:2@renode-1.11.0+20210306gite7897c1"
- ]
- },
- {
- "path": "infra/3pp/tools/buildifier/${platform}",
- "platforms": [
- "linux-amd64",
- "linux-arm64",
- "mac-amd64",
- "mac-arm64",
- "windows-amd64"
- ],
- "tags": [
- "version:2@5.0.1"
+ "git_revision:b07475f83bae0e0744ce5ab5c07b602ececc7fd2"
]
}
]
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/python.json b/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
index 210e597f7..02dcdfcdb 100644
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/python.json
@@ -1,27 +1,16 @@
{
"packages": [
{
- "_comment": "When bumping the minor version (e.g., to 3.9.x) also update env_setup/virtualenv/init.py to check for the new version.",
"path": "infra/3pp/tools/cpython3/${platform}",
"platforms": [
"linux-amd64",
"linux-arm64",
"mac-amd64",
+ "mac-arm64",
"windows-amd64"
],
"tags": [
- "version:2@3.9.5.chromium.19"
- ],
- "version_file": ".versions/cpython3.cipd_version"
- },
- {
- "_comment": "TODO(pwbug/455) Use 3.9 for Macs too.",
- "path": "infra/3pp/tools/cpython3/${platform}",
- "platforms": [
- "mac-arm64"
- ],
- "tags": [
- "version:2@3.8.10.chromium.21"
+ "version:2@3.10.8.chromium.23"
],
"version_file": ".versions/cpython3.cipd_version"
}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/rbe.json b/pw_env_setup/py/pw_env_setup/cipd_setup/rbe.json
new file mode 100644
index 000000000..953cc668e
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/rbe.json
@@ -0,0 +1,16 @@
+{
+ "packages": [
+ {
+ "path": "infra/rbe/client/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "re_client_version:0.72.0.b874055-gomaip"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/riscv.json b/pw_env_setup/py/pw_env_setup/cipd_setup/riscv.json
new file mode 100644
index 000000000..38e2fc2dd
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/riscv.json
@@ -0,0 +1,15 @@
+{
+ "packages": [
+ {
+ "path": "pigweed/third_party/riscv64-unknown-elf/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "mac-amd64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:10.2.0-2020.12.8"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/testing.json b/pw_env_setup/py/pw_env_setup/cipd_setup/testing.json
new file mode 100644
index 000000000..e42babbc7
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/testing.json
@@ -0,0 +1,14 @@
+{
+ "packages": [
+ {
+ "_comment": "Binaries used for pw_transfer backwards-compatibility testing",
+ "path": "pigweed/pw_transfer_test_binaries/${platform}",
+ "platforms": [
+ "linux-amd64"
+ ],
+ "tags": [
+ "version:pw_transfer_test_binaries_528098d588f307881af83f769207b8e6e1b57520-linux-amd64-cipd.cipd"
+ ]
+ }
+ ]
+ }
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/update.py b/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
index eb2b835b1..deac4b0df 100755
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/update.py
@@ -21,6 +21,7 @@ The stdout of this script is meant to be executed by the invoking shell.
from __future__ import print_function
+import collections
import hashlib
import json
import os
@@ -30,8 +31,12 @@ import subprocess
import sys
-def check_auth(cipd, package_files, spin):
+def check_auth(cipd, package_files, cipd_service_account, spin):
"""Check have access to CIPD pigweed directory."""
+ cmd = [cipd]
+ extra_args = []
+ if cipd_service_account:
+ extra_args.extend(['-service-account-json', cipd_service_account])
paths = []
for package_file in package_files:
@@ -48,8 +53,9 @@ def check_auth(cipd, package_files, spin):
username = None
try:
- output = subprocess.check_output([cipd, 'auth-info'],
- stderr=subprocess.STDOUT).decode()
+ output = subprocess.check_output(
+ cmd + ['auth-info'] + extra_args, stderr=subprocess.STDOUT
+ ).decode()
logged_in = True
match = re.search(r'Logged in as (\S*)\.', output)
@@ -66,7 +72,8 @@ def check_auth(cipd, package_files, spin):
# Not catching CalledProcessError because 'cipd ls' seems to never
# return an error code unless it can't reach the CIPD server.
output = subprocess.check_output(
- [cipd, 'ls', path], stderr=subprocess.STDOUT).decode()
+ cmd + ['ls', path] + extra_args, stderr=subprocess.STDOUT
+ ).decode()
if 'No matching packages' not in output:
continue
@@ -75,8 +82,10 @@ def check_auth(cipd, package_files, spin):
# 'cipd instances' does use an error code if there's no such package
# or that package is inaccessible.
try:
- subprocess.check_output([cipd, 'instances', path],
- stderr=subprocess.STDOUT)
+ subprocess.check_output(
+ cmd + ['instances', path] + extra_args,
+ stderr=subprocess.STDOUT,
+ )
except subprocess.CalledProcessError:
inaccessible_paths.append(path)
@@ -88,14 +97,17 @@ def check_auth(cipd, package_files, spin):
with spin.pause():
stderr = lambda *args: print(*args, file=sys.stderr)
stderr()
- stderr('Not logged in to CIPD and no anonymous access to the '
- 'following CIPD paths:')
+ stderr(
+ 'Not logged in to CIPD and no anonymous access to the '
+ 'following CIPD paths:'
+ )
for path in inaccessible_paths:
stderr(' {}'.format(path))
stderr()
stderr('Attempting CIPD login')
try:
- subprocess.check_call([cipd, 'auth-login'])
+ # Note that with -service-account-json, auth-login is a no-op.
+ subprocess.check_call(cmd + ['auth-login'] + extra_args)
except subprocess.CalledProcessError:
stderr('CIPD login failed')
return False
@@ -108,8 +120,11 @@ def check_auth(cipd, package_files, spin):
username_part = ''
if username:
username_part = '({}) '.format(username)
- stderr('Your account {}does not have access to the following '
- 'paths'.format(username_part))
+ stderr(
+ 'Your account {}does not have access to the following '
+ 'paths'.format(username_part)
+ )
+ stderr('(or they do not exist)')
for path in inaccessible_paths:
stderr(' {}'.format(path))
stderr('=' * 60)
@@ -118,7 +133,8 @@ def check_auth(cipd, package_files, spin):
return True
-def platform():
+def platform(rosetta=False):
+ """Return the CIPD platform string of the current system."""
osname = {
'darwin': 'mac',
'linux': 'linux',
@@ -137,7 +153,7 @@ def platform():
platform_arch = '{}-{}'.format(osname, arch).lower()
# Support `mac-arm64` through Rosetta until `mac-arm64` binaries are ready
- if platform_arch == 'mac-arm64':
+ if platform_arch == 'mac-arm64' and rosetta:
return 'mac-amd64'
return platform_arch
@@ -178,9 +194,8 @@ def all_package_files(env_vars, package_files):
return result
-def write_ensure_file(package_files, ensure_file):
+def all_packages(package_files):
packages = []
-
for package_file in package_files:
name = package_file_name(package_file)
with open(package_file, 'r') as ins:
@@ -191,16 +206,35 @@ def write_ensure_file(package_files, ensure_file):
else:
package['subdir'] = name
packages.extend(file_packages)
+ return packages
+
+
+def deduplicate_packages(packages):
+ deduped = collections.OrderedDict()
+ for package in reversed(packages):
+ if package['path'] in deduped:
+ del deduped[package['path']]
+ deduped[package['path']] = package
+ return reversed(list(deduped.values()))
+
+
+def write_ensure_file(
+ package_files, ensure_file, platform
+): # pylint: disable=redefined-outer-name
+ packages = all_packages(package_files)
+ deduped_packages = deduplicate_packages(packages)
with open(ensure_file, 'w') as outs:
- outs.write('$VerifiedPlatform linux-amd64\n'
- '$VerifiedPlatform mac-amd64\n'
- '$ParanoidMode CheckPresence\n')
+ outs.write(
+ '$VerifiedPlatform linux-amd64\n'
+ '$VerifiedPlatform mac-amd64\n'
+ '$ParanoidMode CheckPresence\n'
+ )
- for pkg in packages:
+ for pkg in deduped_packages:
# If this is a new-style package manifest platform handling must
# be done here instead of by the cipd executable.
- if 'platforms' in pkg and platform() not in pkg['platforms']:
+ if 'platforms' in pkg and platform not in pkg['platforms']:
continue
outs.write('@Subdir {}\n'.format(pkg.get('subdir', '')))
@@ -218,15 +252,17 @@ def package_installation_path(root_install_dir, package_file):
root_install_dir: The CIPD installation directory.
package_file: The path to the .json package definition file.
"""
- return os.path.join(root_install_dir, 'packages',
- package_file_name(package_file))
+ return os.path.join(
+ root_install_dir, 'packages', package_file_name(package_file)
+ )
-def update(
+def update( # pylint: disable=too-many-locals
cipd,
package_files,
root_install_dir,
cache_dir,
+ rosetta=False,
env_vars=None,
spin=None,
trust_hash=False,
@@ -241,8 +277,9 @@ def update(
# This file is read by 'pw doctor' which needs to know which package files
# were used in the environment.
- package_files_file = os.path.join(root_install_dir,
- '_all_package_files.json')
+ package_files_file = os.path.join(
+ root_install_dir, '_all_package_files.json'
+ )
with open(package_files_file, 'w') as outs:
json.dump(package_files, outs, indent=2)
@@ -258,21 +295,37 @@ def update(
if not pw_root:
pw_root = os.environ['PW_ROOT']
+ plat = platform(rosetta)
+
ensure_file = os.path.join(root_install_dir, 'packages.ensure')
- write_ensure_file(package_files, ensure_file)
+ write_ensure_file(package_files, ensure_file, plat)
install_dir = os.path.join(root_install_dir, 'packages')
cmd = [
cipd,
'ensure',
- '-ensure-file', ensure_file,
- '-root', install_dir,
- '-log-level', 'debug',
- '-json-output', os.path.join(root_install_dir, 'packages.json'),
- '-cache-dir', cache_dir,
- '-max-threads', '0', # 0 means use CPU count.
- ] # yapf: disable
+ '-ensure-file',
+ ensure_file,
+ '-root',
+ install_dir,
+ '-log-level',
+ 'debug',
+ '-json-output',
+ os.path.join(root_install_dir, 'packages.json'),
+ '-cache-dir',
+ cache_dir,
+ '-max-threads',
+ '0', # 0 means use CPU count.
+ ]
+
+ cipd_service_account = None
+ if env_vars:
+ cipd_service_account = env_vars.get('PW_CIPD_SERVICE_ACCOUNT_JSON')
+ if not cipd_service_account:
+ cipd_service_account = os.environ.get('PW_CIPD_SERVICE_ACCOUNT_JSON')
+ if cipd_service_account:
+ cmd.extend(['-service-account-json', cipd_service_account])
hasher = hashlib.sha256()
encoded = '\0'.join(cmd)
@@ -298,10 +351,9 @@ def update(
if digest == digest_file:
return True
- if not check_auth(cipd, package_files, spin):
+ if not check_auth(cipd, package_files, cipd_service_account, spin):
return False
- # TODO(pwbug/135) Use function from common utility module.
log = os.path.join(root_install_dir, 'packages.log')
try:
with open(log, 'w') as outs:
@@ -318,25 +370,27 @@ def update(
# Set environment variables so tools can later find things under, for
# example, 'share'.
if env_vars:
- for package_file in package_files:
+ for package_file in reversed(package_files):
name = package_file_name(package_file)
file_install_dir = os.path.join(install_dir, name)
# Some executables get installed at top-level and some get
# installed under 'bin'. A small number of old packages prefix the
# entire tree with the platform (e.g., chromium/third_party/tcl).
for bin_dir in (
- file_install_dir,
- os.path.join(file_install_dir, 'bin'),
- os.path.join(file_install_dir, platform(), 'bin'),
+ file_install_dir,
+ os.path.join(file_install_dir, 'bin'),
+ os.path.join(file_install_dir, plat, 'bin'),
):
if os.path.isdir(bin_dir):
env_vars.prepend('PATH', bin_dir)
- env_vars.set('PW_{}_CIPD_INSTALL_DIR'.format(name.upper()),
- file_install_dir)
+ env_vars.set(
+ 'PW_{}_CIPD_INSTALL_DIR'.format(name.upper()), file_install_dir
+ )
# Windows has its own special toolchain.
if os.name == 'nt':
env_vars.prepend(
- 'PATH', os.path.join(file_install_dir, 'mingw64', 'bin'))
+ 'PATH', os.path.join(file_install_dir, 'mingw64', 'bin')
+ )
return True
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json b/pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json
new file mode 100644
index 000000000..a91f638cf
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/upstream.json
@@ -0,0 +1,15 @@
+{
+ "included_files": [
+ "bazel.json",
+ "cmake.json",
+ "default.json",
+ "doxygen.json",
+ "go.json",
+ "host_tools.json",
+ "kythe.json",
+ "luci.json",
+ "rbe.json",
+ "testing.json",
+ "web.json"
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/web.json b/pw_env_setup/py/pw_env_setup/cipd_setup/web.json
new file mode 100644
index 000000000..3424c8456
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/web.json
@@ -0,0 +1,17 @@
+{
+ "packages": [
+ {
+ "path": "infra/3pp/tools/nodejs/${platform}",
+ "platforms": [
+ "linux-amd64",
+ "linux-arm64",
+ "mac-amd64",
+ "mac-arm64",
+ "windows-amd64"
+ ],
+ "tags": [
+ "version:2@18.4.0"
+ ]
+ }
+ ]
+}
diff --git a/pw_env_setup/py/pw_env_setup/cipd_setup/wrapper.py b/pw_env_setup/py/pw_env_setup/cipd_setup/wrapper.py
index 6ca7db3c9..31b54c1b1 100755
--- a/pw_env_setup/py/pw_env_setup/cipd_setup/wrapper.py
+++ b/pw_env_setup/py/pw_env_setup/cipd_setup/wrapper.py
@@ -69,8 +69,13 @@ try:
SCRIPT_DIR = os.path.dirname(__file__)
except NameError: # __file__ not defined.
try:
- SCRIPT_DIR = os.path.join(os.environ['PW_ROOT'], 'pw_env_setup', 'py',
- 'pw_env_setup', 'cipd_setup')
+ SCRIPT_DIR = os.path.join(
+ os.environ['PW_ROOT'],
+ 'pw_env_setup',
+ 'py',
+ 'pw_env_setup',
+ 'cipd_setup',
+ )
except KeyError:
raise Exception('Environment variable PW_ROOT not set')
@@ -85,10 +90,14 @@ try:
except KeyError:
try:
with open(os.devnull, 'w') as outs:
- PW_ROOT = subprocess.check_output(
- ['git', 'rev-parse', '--show-toplevel'],
- stderr=outs,
- ).strip().decode('utf-8')
+ PW_ROOT = (
+ subprocess.check_output(
+ ['git', 'rev-parse', '--show-toplevel'],
+ stderr=outs,
+ )
+ .strip()
+ .decode('utf-8')
+ )
except subprocess.CalledProcessError:
PW_ROOT = ''
@@ -117,10 +126,12 @@ def platform_normalized():
raise Exception('unrecognized os: {}'.format(os_name))
-def arch_normalized():
+def arch_normalized(rosetta=False):
"""Normalize arch into format expected in CIPD paths."""
machine = platform.machine()
+ if platform_normalized() == 'mac' and rosetta:
+ return 'amd64'
if machine.startswith(('arm', 'aarch')):
machine = machine.replace('aarch', 'arm')
if machine == 'arm64':
@@ -133,14 +144,8 @@ def arch_normalized():
raise Exception('unrecognized arch: {}'.format(machine))
-def platform_arch_normalized():
- platform_arch = '{}-{}'.format(platform_normalized(), arch_normalized())
-
- # Support `mac-arm64` through Rosetta until `mac-arm64` binaries are ready
- if platform_arch == 'mac-arm64':
- platform_arch = 'mac-amd64'
-
- return platform_arch
+def platform_arch_normalized(rosetta=False):
+ return '{}-{}'.format(platform_normalized(), arch_normalized(rosetta))
def user_agent():
@@ -148,7 +153,8 @@ def user_agent():
try:
rev = subprocess.check_output(
- ['git', '-C', SCRIPT_DIR, 'rev-parse', 'HEAD']).strip()
+ ['git', '-C', SCRIPT_DIR, 'rev-parse', 'HEAD']
+ ).strip()
except subprocess.CalledProcessError:
rev = '???'
@@ -167,10 +173,10 @@ def actual_hash(path):
return hasher.hexdigest()
-def expected_hash():
+def expected_hash(rosetta=False):
"""Pulls expected hash from digests file."""
- expected_plat = platform_arch_normalized()
+ expected_plat = platform_arch_normalized(rosetta)
with open(DIGESTS_FILE, 'r') as ins:
for line in ins:
@@ -178,10 +184,9 @@ def expected_hash():
if line.startswith('#') or not line:
continue
plat, hashtype, hashval = line.split()
- if (hashtype == 'sha256' and plat == expected_plat):
+ if hashtype == 'sha256' and plat == expected_plat:
return hashval
- raise Exception('platform {} not in {}'.format(expected_plat,
- DIGESTS_FILE))
+ raise Exception('platform {} not in {}'.format(expected_plat, DIGESTS_FILE))
def https_connect_with_proxy(target_url):
@@ -200,14 +205,15 @@ def https_connect_with_proxy(target_url):
py_version = sys.version_info.major
if py_version >= 3:
headers['Proxy-Authorization'] = 'Basic ' + str(
- base64.b64encode(auth.encode()).decode())
+ base64.b64encode(auth.encode()).decode()
+ )
else:
headers['Proxy-Authorization'] = 'Basic ' + base64.b64encode(auth)
conn.set_tunnel(target_url, 443, headers)
return conn
-def client_bytes():
+def client_bytes(rosetta=False):
"""Pull down the CIPD client and return it as a bytes object.
Often CIPD_HOST returns a 302 FOUND with a pointer to
@@ -222,18 +228,20 @@ def client_bytes():
conn = https_connect_with_proxy(CIPD_HOST)
except AttributeError:
print('=' * 70)
- print('''
+ print(
+ '''
It looks like this version of Python does not support SSL. This is common
when using Homebrew. If using Homebrew please run the following commands.
If not using Homebrew check how your version of Python was built.
brew install openssl # Probably already installed, but good to confirm.
brew uninstall python && brew install python
-'''.strip())
+'''.strip()
+ )
print('=' * 70)
raise
- full_platform = platform_arch_normalized()
+ full_platform = platform_arch_normalized(rosetta)
if full_platform not in SUPPORTED_PLATFORMS:
raise UnsupportedPlatform(full_platform)
@@ -272,7 +280,8 @@ brew uninstall python && brew install python
"Otherwise, check that your machine's Python can use SSL, "
'testing with the httplib module on Python 2 or http.client on '
'Python 3.',
- file=sys.stderr)
+ file=sys.stderr,
+ )
raise
# Found client bytes.
@@ -291,11 +300,16 @@ brew uninstall python && brew install python
else:
break
- raise Exception('failed to download client from https://{}{}'.format(
- CIPD_HOST, path))
+ raise Exception(
+ 'failed to download client from https://{}{}'.format(CIPD_HOST, path)
+ )
-def bootstrap(client, silent=('PW_ENVSETUP_QUIET' in os.environ)):
+def bootstrap(
+ client,
+ silent=('PW_ENVSETUP_QUIET' in os.environ),
+ rosetta=False,
+):
"""Bootstrap cipd client installation."""
client_dir = os.path.dirname(client)
@@ -303,19 +317,24 @@ def bootstrap(client, silent=('PW_ENVSETUP_QUIET' in os.environ)):
os.makedirs(client_dir)
if not silent:
- print('Bootstrapping cipd client for {}'.format(
- platform_arch_normalized()))
+ print(
+ 'Bootstrapping cipd client for {}'.format(
+ platform_arch_normalized(rosetta)
+ )
+ )
tmp_path = client + '.tmp'
with open(tmp_path, 'wb') as tmp:
- tmp.write(client_bytes())
+ tmp.write(client_bytes(rosetta))
- expected = expected_hash()
+ expected = expected_hash(rosetta=rosetta)
actual = actual_hash(tmp_path)
if expected != actual:
- raise Exception('digest of downloaded CIPD client is incorrect, '
- 'check that digests file is current')
+ raise Exception(
+ 'digest of downloaded CIPD client is incorrect, '
+ 'check that digests file is current'
+ )
os.chmod(tmp_path, 0o755)
os.rename(tmp_path, client)
@@ -327,9 +346,11 @@ def selfupdate(client):
cmd = [
client,
'selfupdate',
- '-version-file', VERSION_FILE,
- '-service-url', 'https://{}'.format(CIPD_HOST),
- ] # yapf: disable
+ '-version-file',
+ VERSION_FILE,
+ '-service-url',
+ 'https://{}'.format(CIPD_HOST),
+ ]
subprocess.check_call(cmd)
@@ -340,7 +361,12 @@ def _default_client(install_dir):
return client
-def init(install_dir=DEFAULT_INSTALL_DIR, silent=False, client=None):
+def init(
+ install_dir=DEFAULT_INSTALL_DIR,
+ silent=False,
+ client=None,
+ rosetta=False,
+):
"""Install/update cipd client."""
if not client:
@@ -349,14 +375,16 @@ def init(install_dir=DEFAULT_INSTALL_DIR, silent=False, client=None):
os.environ['CIPD_HTTP_USER_AGENT_PREFIX'] = user_agent()
if not os.path.isfile(client):
- bootstrap(client, silent)
+ bootstrap(client, silent, rosetta=rosetta)
try:
selfupdate(client)
except subprocess.CalledProcessError:
- print('CIPD selfupdate failed. Bootstrapping then retrying...',
- file=sys.stderr)
- bootstrap(client)
+ print(
+ 'CIPD selfupdate failed. Bootstrapping then retrying...',
+ file=sys.stderr,
+ )
+ bootstrap(client, rosetta=rosetta)
selfupdate(client)
return client
@@ -375,15 +403,17 @@ def main(install_dir=DEFAULT_INSTALL_DIR, silent=False):
raise
except Exception:
- print('Failed to initialize CIPD. Run '
- '`CIPD_HTTP_USER_AGENT_PREFIX={user_agent}/manual {client} '
- "selfupdate -version-file '{version_file}'` "
- 'to diagnose if this is persistent.'.format(
- user_agent=user_agent(),
- client=client,
- version_file=VERSION_FILE,
- ),
- file=sys.stderr)
+ print(
+ 'Failed to initialize CIPD. Run '
+ '`CIPD_HTTP_USER_AGENT_PREFIX={user_agent}/manual {client} '
+ "selfupdate -version-file '{version_file}'` "
+ 'to diagnose if this is persistent.'.format(
+ user_agent=user_agent(),
+ client=client,
+ version_file=VERSION_FILE,
+ ),
+ file=sys.stderr,
+ )
raise
return client
diff --git a/pw_env_setup/py/pw_env_setup/colors.py b/pw_env_setup/py/pw_env_setup/colors.py
index 22b06a9d3..58b3c0e62 100644
--- a/pw_env_setup/py/pw_env_setup/colors.py
+++ b/pw_env_setup/py/pw_env_setup/colors.py
@@ -28,6 +28,7 @@ def _make_color(*codes):
class Color: # pylint: disable=too-few-public-methods
"""Helpers to surround text with ASCII color escapes"""
+
bold = _make_color(1)
red = _make_color(31)
bold_red = _make_color(31, 1)
diff --git a/pw_env_setup/py/pw_env_setup/config_file.py b/pw_env_setup/py/pw_env_setup/config_file.py
new file mode 100644
index 000000000..fc8fa40c5
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/config_file.py
@@ -0,0 +1,43 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Reads and parses the Pigweed config file.
+
+See also https://pigweed.dev/seed/0101-pigweed.json.html.
+"""
+
+import json
+import os
+
+
+def _get_project_root(env=None):
+ if not env:
+ env = os.environ
+ for var in ('PW_PROJECT_ROOT', 'PW_ROOT'):
+ if var in env:
+ return env[var]
+ raise ValueError('environment variable PW_PROJECT_ROOT not set')
+
+
+def path(env=None):
+ """Return the path where pigweed.json should reside."""
+ return os.path.join(_get_project_root(env=env), 'pigweed.json')
+
+
+def load(env=None):
+ """Load pigweed.json if it exists and return the contents."""
+ config_path = path(env=env)
+ if not os.path.isfile(config_path):
+ return {}
+ with open(config_path, 'r') as ins:
+ return json.load(ins)
diff --git a/pw_env_setup/py/pw_env_setup/env_setup.py b/pw_env_setup/py/pw_env_setup/env_setup.py
index d742e4f5b..7b1966847 100755
--- a/pw_env_setup/py/pw_env_setup/env_setup.py
+++ b/pw_env_setup/py/pw_env_setup/env_setup.py
@@ -35,11 +35,6 @@ import subprocess
import sys
import time
-# TODO(pwbug/67): Remove import hacks once the oxidized prebuilt binaries are
-# proven stable for first-time bootstrapping. For now, continue to support
-# running directly from source without assuming a functioning Python
-# environment when running for the first time.
-
# If we're running oxidized, filesystem-centric import hacks won't work. In that
# case, jump straight to the imports and assume oxidation brought in the deps.
if not getattr(sys, 'oxidized', False):
@@ -59,11 +54,13 @@ if not getattr(sys, 'oxidized', False):
'Unable to locate pw_env_setup module; cannot continue.\n'
'\n'
'Try updating to one of the standard Python implemetations:\n'
- ' https://www.python.org/downloads/')
+ ' https://www.python.org/downloads/'
+ )
sys.path = [
os.path.abspath(os.path.join(filename, os.path.pardir, os.path.pardir))
]
import pw_env_setup # pylint: disable=unused-import
+
sys.path = old_sys_path
# pylint: disable=wrong-import-position
@@ -76,15 +73,13 @@ from pw_env_setup import virtualenv_setup
from pw_env_setup import windows_env_start
-# TODO(pwbug/67, pwbug/68) switch to shutil.which().
-def _which(executable,
- pathsep=os.pathsep,
- use_pathext=None,
- case_sensitive=None):
+def _which(
+ executable, pathsep=os.pathsep, use_pathext=None, case_sensitive=None
+):
if use_pathext is None:
- use_pathext = (os.name == 'nt')
+ use_pathext = os.name == 'nt'
if case_sensitive is None:
- case_sensitive = (os.name != 'nt' and sys.platform != 'darwin')
+ case_sensitive = os.name != 'nt' and sys.platform != 'darwin'
if not case_sensitive:
executable = executable.lower()
@@ -171,22 +166,37 @@ class MissingSubmodulesError(Exception):
# pylint: disable=too-many-arguments
class EnvSetup(object):
"""Run environment setup for Pigweed."""
- def __init__(self, pw_root, cipd_cache_dir, shell_file, quiet, install_dir,
- virtualenv_root, strict, virtualenv_gn_out_dir, json_file,
- project_root, config_file, use_existing_cipd,
- use_pinned_pip_packages, cipd_only, trust_cipd_hash):
+
+ def __init__(
+ self,
+ pw_root,
+ cipd_cache_dir,
+ shell_file,
+ quiet,
+ install_dir,
+ strict,
+ virtualenv_gn_out_dir,
+ json_file,
+ project_root,
+ config_file,
+ use_existing_cipd,
+ check_submodules,
+ use_pinned_pip_packages,
+ cipd_only,
+ trust_cipd_hash,
+ ):
self._env = environment.Environment()
self._project_root = project_root
self._pw_root = pw_root
- self._setup_root = os.path.join(pw_root, 'pw_env_setup', 'py',
- 'pw_env_setup')
+ self._setup_root = os.path.join(
+ pw_root, 'pw_env_setup', 'py', 'pw_env_setup'
+ )
self._cipd_cache_dir = cipd_cache_dir
self._shell_file = shell_file
self._is_windows = os.name == 'nt'
self._quiet = quiet
self._install_dir = install_dir
- self._virtualenv_root = (virtualenv_root
- or os.path.join(install_dir, 'pigweed-venv'))
+ self._virtualenv_root = os.path.join(self._install_dir, 'pigweed-venv')
self._strict = strict
self._cipd_only = cipd_only
self._trust_cipd_hash = trust_cipd_hash
@@ -199,6 +209,7 @@ class EnvSetup(object):
self._cipd_package_file = []
self._virtualenv_requirements = []
+ self._virtualenv_constraints = []
self._virtualenv_gn_targets = []
self._virtualenv_gn_args = []
self._use_pinned_pip_packages = use_pinned_pip_packages
@@ -208,6 +219,8 @@ class EnvSetup(object):
self._pw_packages = []
self._root_variable = None
+ self._check_submodules = check_submodules
+
self._json_file = json_file
self._gni_file = None
@@ -216,7 +229,7 @@ class EnvSetup(object):
if config_file:
self._parse_config_file(config_file)
- self._check_submodules()
+ self._check_submodule_presence()
self._use_existing_cipd = use_existing_cipd
self._virtualenv_gn_out_dir = virtualenv_gn_out_dir
@@ -260,6 +273,12 @@ class EnvSetup(object):
self._root_variable = config.pop('root_variable', None)
+ rosetta = config.pop('rosetta', 'allow')
+ if rosetta not in ('never', 'allow', 'force'):
+ raise ValueError(rosetta)
+ self._rosetta = rosetta in ('allow', 'force')
+ self._env.set('_PW_ROSETTA', str(int(self._rosetta)))
+
if 'json_file' in config:
self._json_file = config.pop('json_file')
@@ -272,11 +291,13 @@ class EnvSetup(object):
raise ValueError(
'{} contains both "optional_submodules" and '
'"required_submodules", but these options are mutually '
- 'exclusive'.format(self._config_file_name))
+ 'exclusive'.format(self._config_file_name)
+ )
self._cipd_package_file.extend(
os.path.join(self._project_root, x)
- for x in config.pop('cipd_package_files', ()))
+ for x in config.pop('cipd_package_files', ())
+ )
for pkg in config.pop('pw_packages', ()):
self._pw_packages.append(pkg)
@@ -290,67 +311,93 @@ class EnvSetup(object):
for target in virtualenv.pop('gn_targets', ()):
self._virtualenv_gn_targets.append(
- virtualenv_setup.GnTarget('{}#{}'.format(root, target)))
+ virtualenv_setup.GnTarget('{}#{}'.format(root, target))
+ )
self._virtualenv_gn_args = virtualenv.pop('gn_args', ())
self._virtualenv_system_packages = virtualenv.pop(
- 'system_packages', False)
+ 'system_packages', False
+ )
+
+ for req_txt in virtualenv.pop('requirements', ()):
+ self._virtualenv_requirements.append(
+ os.path.join(self._project_root, req_txt)
+ )
+
+ for constraint_txt in virtualenv.pop('constraints', ()):
+ self._virtualenv_constraints.append(
+ os.path.join(self._project_root, constraint_txt)
+ )
if virtualenv:
raise ConfigFileError(
'unrecognized option in {}: "virtualenv.{}"'.format(
- self._config_file_name, next(iter(virtualenv))))
+ self._config_file_name, next(iter(virtualenv))
+ )
+ )
if config:
- raise ConfigFileError('unrecognized option in {}: "{}"'.format(
- self._config_file_name, next(iter(config))))
+ raise ConfigFileError(
+ 'unrecognized option in {}: "{}"'.format(
+ self._config_file_name, next(iter(config))
+ )
+ )
- def _check_submodules(self):
- unitialized = set()
+ def _check_submodule_presence(self):
+ uninitialized = set()
# Don't check submodule presence if using the Android Repo Tool.
if os.path.isdir(os.path.join(self._project_root, '.repo')):
return
+ if not self._check_submodules:
+ return
+
cmd = ['git', 'submodule', 'status', '--recursive']
for line in subprocess.check_output(
- cmd, cwd=self._project_root).splitlines():
+ cmd, cwd=self._project_root
+ ).splitlines():
if isinstance(line, bytes):
line = line.decode()
# Anything but an initial '-' means the submodule is initialized.
if not line.startswith('-'):
continue
- unitialized.add(line.split()[1])
+ uninitialized.add(line.split()[1])
- missing = unitialized - set(self._optional_submodules)
+ missing = uninitialized - set(self._optional_submodules)
if self._required_submodules:
- missing = set(self._required_submodules) & unitialized
+ missing = set(self._required_submodules) & uninitialized
if missing:
print(
'Not all submodules are initialized. Please run the '
'following commands.',
- file=sys.stderr)
+ file=sys.stderr,
+ )
print('', file=sys.stderr)
- for miss in missing:
- print(' git submodule update --init {}'.format(miss),
- file=sys.stderr)
+ for miss in sorted(missing):
+ print(
+ ' git submodule update --init {}'.format(miss),
+ file=sys.stderr,
+ )
print('', file=sys.stderr)
if self._required_submodules:
print(
'If these submodules are not required, remove them from '
'the "required_submodules"',
- file=sys.stderr)
+ file=sys.stderr,
+ )
else:
print(
'If these submodules are not required, add them to the '
'"optional_submodules"',
- file=sys.stderr)
+ file=sys.stderr,
+ )
print('list in the environment config JSON file:', file=sys.stderr)
print(' {}'.format(self._config_file_name), file=sys.stderr)
@@ -359,8 +406,12 @@ class EnvSetup(object):
raise MissingSubmodulesError(', '.join(sorted(missing)))
def _write_gni_file(self):
- gni_file = os.path.join(self._project_root, 'build_overrides',
- 'pigweed_environment.gni')
+ if self._cipd_only:
+ return
+
+ gni_file = os.path.join(
+ self._project_root, 'build_overrides', 'pigweed_environment.gni'
+ )
if self._gni_file:
gni_file = os.path.join(self._project_root, self._gni_file)
@@ -398,37 +449,49 @@ class EnvSetup(object):
steps = [('CIPD package manager', self.cipd)]
self._log(
- Color.bold('Downloading and installing packages into local '
- 'source directory:\n'))
+ Color.bold(
+ 'Downloading and installing packages into local '
+ 'source directory:\n'
+ )
+ )
max_name_len = max(len(name) for name, _ in steps)
- self._env.comment('''
+ self._env.comment(
+ '''
This file is automatically generated. DO NOT EDIT!
For details, see $PW_ROOT/pw_env_setup/py/pw_env_setup/env_setup.py and
$PW_ROOT/pw_env_setup/py/pw_env_setup/environment.py.
-'''.strip())
+'''.strip()
+ )
if not self._is_windows:
- self._env.comment('''
+ self._env.comment(
+ '''
For help debugging errors in this script, uncomment the next line.
set -x
Then use `set +x` to go back to normal.
-'''.strip())
+'''.strip()
+ )
self._env.echo(
Color.bold(
- 'Activating environment (setting environment variables):'))
+ 'Activating environment (setting environment variables):'
+ )
+ )
self._env.echo('')
for name, step in steps:
- self._log(' Setting up {name:.<{width}}...'.format(
- name=name, width=max_name_len),
- end='',
- flush=True)
+ self._log(
+ ' Setting up {name:.<{width}}...'.format(
+ name=name, width=max_name_len
+ ),
+ end='',
+ flush=True,
+ )
self._env.echo(
- ' Setting environment variables for {name:.<{width}}...'.
- format(name=name, width=max_name_len),
+ ' Setting environment variables for '
+ '{name:.<{width}}...'.format(name=name, width=max_name_len),
newline=False,
)
@@ -453,7 +516,8 @@ Then use `set +x` to go back to normal.
if not os.path.isdir(log_dir):
os.makedirs(log_dir)
actions_json = os.path.join(
- log_dir, 'post-{}.json'.format(name.replace(' ', '_')))
+ log_dir, 'post-{}.json'.format(name.replace(' ', '_'))
+ )
with open(actions_json, 'w') as outs:
self._env.json(outs)
@@ -475,7 +539,8 @@ Then use `set +x` to go back to normal.
self._env.echo()
self._env.echo(
- Color.bold('Environment looks good, you are ready to go!'))
+ Color.bold('Environment looks good, you are ready to go!')
+ )
self._env.echo()
# Don't write new files if all we did was update CIPD packages.
@@ -487,7 +552,8 @@ Then use `set +x` to go back to normal.
deactivate = os.path.join(
self._install_dir,
- 'deactivate{}'.format(os.path.splitext(self._shell_file)[1]))
+ 'deactivate{}'.format(os.path.splitext(self._shell_file)[1]),
+ )
with open(deactivate, 'w') as outs:
self._env.write_deactivate(outs)
@@ -502,10 +568,12 @@ Then use `set +x` to go back to normal.
with open(os.path.join(self._install_dir, 'config.json'), 'w') as outs:
outs.write(
- json.dumps(config, indent=4, separators=(',', ': ')) + '\n')
+ json.dumps(config, indent=4, separators=(',', ': ')) + '\n'
+ )
- json_file = (self._json_file
- or os.path.join(self._install_dir, 'actions.json'))
+ json_file = self._json_file or os.path.join(
+ self._install_dir, 'actions.json'
+ )
with open(json_file, 'w') as outs:
self._env.json(outs)
@@ -524,27 +592,35 @@ Then use `set +x` to go back to normal.
else:
try:
- cipd_client = cipd_wrapper.init(install_dir, silent=True)
+ cipd_client = cipd_wrapper.init(
+ install_dir,
+ silent=True,
+ rosetta=self._rosetta,
+ )
except cipd_wrapper.UnsupportedPlatform as exc:
- return result_func((' {!r}'.format(exc), ))(
+ return result_func((' {!r}'.format(exc),))(
_Result.Status.SKIPPED,
' abandoning CIPD setup',
)
package_files, glob_warnings = self._process_globs(
- self._cipd_package_file)
+ self._cipd_package_file
+ )
result = result_func(glob_warnings)
if not package_files:
return result(_Result.Status.SKIPPED)
- if not cipd_update.update(cipd=cipd_client,
- root_install_dir=install_dir,
- package_files=package_files,
- cache_dir=self._cipd_cache_dir,
- env_vars=self._env,
- spin=spin,
- trust_hash=self._trust_cipd_hash):
+ if not cipd_update.update(
+ cipd=cipd_client,
+ root_install_dir=install_dir,
+ package_files=package_files,
+ cache_dir=self._cipd_cache_dir,
+ env_vars=self._env,
+ rosetta=self._rosetta,
+ spin=spin,
+ trust_hash=self._trust_cipd_hash,
+ ):
return result(_Result.Status.FAILED)
return result(_Result.Status.DONE)
@@ -553,8 +629,14 @@ Then use `set +x` to go back to normal.
"""Setup virtualenv."""
requirements, req_glob_warnings = self._process_globs(
- self._virtualenv_requirements)
- result = result_func(req_glob_warnings)
+ self._virtualenv_requirements
+ )
+
+ constraints, constraint_glob_warnings = self._process_globs(
+ self._virtualenv_constraints
+ )
+
+ result = result_func(req_glob_warnings + constraint_glob_warnings)
orig_python3 = _which('python3')
with self._env():
@@ -566,8 +648,9 @@ Then use `set +x` to go back to normal.
# to address this. Detect if we did so and if so create a copy of
# python3.exe called python.exe so that virtualenv works.
if orig_python3 != new_python3 and self._is_windows:
- python3_copy = os.path.join(os.path.dirname(new_python3),
- 'python.exe')
+ python3_copy = os.path.join(
+ os.path.dirname(new_python3), 'python.exe'
+ )
if not os.path.exists(python3_copy):
shutil.copyfile(new_python3, python3_copy)
new_python3 = python3_copy
@@ -576,16 +659,17 @@ Then use `set +x` to go back to normal.
return result(_Result.Status.SKIPPED)
if not virtualenv_setup.install(
- project_root=self._project_root,
- venv_path=self._virtualenv_root,
- requirements=requirements,
- gn_args=self._virtualenv_gn_args,
- gn_targets=self._virtualenv_gn_targets,
- gn_out_dir=self._virtualenv_gn_out_dir,
- python=new_python3,
- env=self._env,
- system_packages=self._virtualenv_system_packages,
- use_pinned_pip_packages=self._use_pinned_pip_packages,
+ project_root=self._project_root,
+ venv_path=self._virtualenv_root,
+ requirements=requirements,
+ constraints=constraints,
+ gn_args=self._virtualenv_gn_args,
+ gn_targets=self._virtualenv_gn_targets,
+ gn_out_dir=self._virtualenv_gn_out_dir,
+ python=new_python3,
+ env=self._env,
+ system_packages=self._virtualenv_system_packages,
+ use_pinned_pip_packages=self._use_pinned_pip_packages,
):
return result(_Result.Status.FAILED)
@@ -596,25 +680,29 @@ Then use `set +x` to go back to normal.
result = result_func()
+ pkg_dir = os.path.join(self._install_dir, 'packages')
+ self._env.set('PW_PACKAGE_ROOT', pkg_dir)
+
+ if not os.path.isdir(pkg_dir):
+ os.makedirs(pkg_dir)
+
if not self._pw_packages:
return result(_Result.Status.SKIPPED)
- logdir = os.path.join(self._install_dir, 'packages')
- if not os.path.isdir(logdir):
- os.makedirs(logdir)
-
for pkg in self._pw_packages:
print('installing {}'.format(pkg))
- cmd = ['pw', 'package', 'install', pkg]
+ cmd = ['pw', 'package', 'install', '--force', pkg]
- log = os.path.join(logdir, '{}.log'.format(pkg))
+ log = os.path.join(pkg_dir, '{}.log'.format(pkg))
try:
with open(log, 'w') as outs, self._env():
print(*cmd, file=outs)
- subprocess.check_call(cmd,
- cwd=self._project_root,
- stdout=outs,
- stderr=subprocess.STDOUT)
+ subprocess.check_call(
+ cmd,
+ cwd=self._project_root,
+ stdout=outs,
+ stderr=subprocess.STDOUT,
+ )
except subprocess.CalledProcessError:
with open(log, 'r') as ins:
sys.stderr.write(ins.read())
@@ -633,22 +721,23 @@ Then use `set +x` to go back to normal.
def win_scripts(self, unused_spin):
# These scripts act as a compatibility layer for windows.
env_setup_dir = os.path.join(self._pw_root, 'pw_env_setup')
- self._env.prepend('PATH', os.path.join(env_setup_dir,
- 'windows_scripts'))
+ self._env.prepend(
+ 'PATH', os.path.join(env_setup_dir, 'windows_scripts')
+ )
return _Result(_Result.Status.DONE)
def parse(argv=None):
"""Parse command-line arguments."""
- parser = argparse.ArgumentParser()
+ parser = argparse.ArgumentParser(prog="python -m pw_env_setup.env_setup")
pw_root = os.environ.get('PW_ROOT', None)
if not pw_root:
try:
with open(os.devnull, 'w') as outs:
pw_root = subprocess.check_output(
- ['git', 'rev-parse', '--show-toplevel'],
- stderr=outs).strip()
+ ['git', 'rev-parse', '--show-toplevel'], stderr=outs
+ ).strip()
except subprocess.CalledProcessError:
pw_root = None
@@ -668,8 +757,9 @@ def parse(argv=None):
parser.add_argument(
'--cipd-cache-dir',
- default=os.environ.get('CIPD_CACHE_DIR',
- os.path.expanduser('~/.cipd-cache-dir')),
+ default=os.environ.get(
+ 'CIPD_CACHE_DIR', os.path.expanduser('~/.cipd-cache-dir')
+ ),
)
parser.add_argument(
@@ -708,15 +798,11 @@ def parse(argv=None):
parser.add_argument(
'--virtualenv-gn-out-dir',
- help=('Output directory to use when building and installing Python '
- 'packages with GN; defaults to a unique path in the environment '
- 'directory.'))
-
- parser.add_argument(
- '--virtualenv-root',
- help=('Root of virtualenv directory. Default: '
- '<install_dir>/pigweed-venv'),
- default=None,
+ help=(
+ 'Output directory to use when building and installing Python '
+ 'packages with GN; defaults to a unique path in the environment '
+ 'directory.'
+ ),
)
parser.add_argument('--json-file', help=argparse.SUPPRESS, default=None)
@@ -746,6 +832,13 @@ def parse(argv=None):
action='store_true',
)
+ parser.add_argument(
+ '--skip-submodule-check',
+ help='Skip checking for submodule presence.',
+ dest='check_submodules',
+ action='store_false',
+ )
+
args = parser.parse_args(argv)
return args
diff --git a/pw_env_setup/py/pw_env_setup/environment.py b/pw_env_setup/py/pw_env_setup/environment.py
index 39a601ad4..c87de8847 100644
--- a/pw_env_setup/py/pw_env_setup/environment.py
+++ b/pw_env_setup/py/pw_env_setup/environment.py
@@ -70,13 +70,13 @@ class _Action(object): # pylint: disable=useless-object-inheritance
def accept(self, visitor):
del visitor
- raise AcceptNotOverridden('accept() not overridden for {}'.format(
- self.__class__.__name__))
+ raise AcceptNotOverridden(
+ 'accept() not overridden for {}'.format(self.__class__.__name__)
+ )
- def write_deactivate(self,
- outs,
- windows=(os.name == 'nt'),
- replacements=()):
+ def write_deactivate(
+ self, outs, windows=(os.name == 'nt'), replacements=()
+ ):
pass
@@ -95,26 +95,34 @@ class _VariableAction(_Action):
# In python2, unicode is a distinct type.
valid_types = (str, unicode)
except NameError:
- valid_types = (str, )
+ valid_types = (str,)
if not isinstance(self.name, valid_types):
- raise BadNameType('variable name {!r} not of type str'.format(
- self.name))
+ raise BadNameType(
+ 'variable name {!r} not of type str'.format(self.name)
+ )
if not isinstance(self.value, valid_types):
- raise BadValueType('{!r} value {!r} not of type str'.format(
- self.name, self.value))
+ raise BadValueType(
+ '{!r} value {!r} not of type str'.format(self.name, self.value)
+ )
# Empty strings as environment variable values have different behavior
# on different operating systems. Just don't allow them.
if not self.allow_empty_values and self.value == '':
- raise EmptyValue('{!r} value {!r} is the empty string'.format(
- self.name, self.value))
+ raise EmptyValue(
+ '{!r} value {!r} is the empty string'.format(
+ self.name, self.value
+ )
+ )
# Many tools have issues with newlines in environment variable values.
# Just don't allow them.
if '\n' in self.value:
- raise NewlineInValue('{!r} value {!r} contains a newline'.format(
- self.name, self.value))
+ raise NewlineInValue(
+ '{!r} value {!r} contains a newline'.format(
+ self.name, self.value
+ )
+ )
if not re.match(r'^[A-Z_][A-Z0-9_]*$', self.name, re.IGNORECASE):
raise BadVariableName('bad variable name {!r}'.format(self.name))
@@ -126,12 +134,14 @@ class _VariableAction(_Action):
env.pop(self.name, None)
def __repr__(self):
- return '{}({}, {})'.format(self.__class__.__name__, self.name,
- self.value)
+ return '{}({}, {})'.format(
+ self.__class__.__name__, self.name, self.value
+ )
class Set(_VariableAction):
"""Set a variable."""
+
def __init__(self, *args, **kwargs):
deactivate = kwargs.pop('deactivate', True)
super(Set, self).__init__(*args, **kwargs)
@@ -143,6 +153,7 @@ class Set(_VariableAction):
class Clear(_VariableAction):
"""Remove a variable from the environment."""
+
def __init__(self, *args, **kwargs):
kwargs['value'] = ''
kwargs['allow_empty_values'] = True
@@ -154,6 +165,7 @@ class Clear(_VariableAction):
class Remove(_VariableAction):
"""Remove a value from a PATH-like variable."""
+
def accept(self, visitor):
visitor.visit_remove(self)
@@ -169,6 +181,7 @@ def _append_prepend_check(action):
class Prepend(_VariableAction):
"""Prepend a value to a PATH-like variable."""
+
def __init__(self, name, value, join, *args, **kwargs):
super(Prepend, self).__init__(name, value, *args, **kwargs)
self._join = join
@@ -183,6 +196,7 @@ class Prepend(_VariableAction):
class Append(_VariableAction):
"""Append a value to a PATH-like variable. (Uncommon, see Prepend.)"""
+
def __init__(self, name, value, join, *args, **kwargs):
super(Append, self).__init__(name, value, *args, **kwargs)
self._join = join
@@ -201,6 +215,7 @@ class BadEchoValue(ValueError):
class Echo(_Action):
"""Echo a value to the terminal."""
+
def __init__(self, value, newline, *args, **kwargs):
# These values act funny on Windows.
if value.lower() in ('off', 'on'):
@@ -218,6 +233,7 @@ class Echo(_Action):
class Comment(_Action):
"""Add a comment to the init script."""
+
def __init__(self, value, *args, **kwargs):
super(Comment, self).__init__(*args, **kwargs)
self.value = value
@@ -231,6 +247,7 @@ class Comment(_Action):
class Command(_Action):
"""Run a command."""
+
def __init__(self, command, *args, **kwargs):
exit_on_error = kwargs.pop('exit_on_error', True)
super(Command, self).__init__(*args, **kwargs)
@@ -248,10 +265,8 @@ class Command(_Action):
class Doctor(Command):
def __init__(self, *args, **kwargs):
log_level = 'warn' if 'PW_ENVSETUP_QUIET' in os.environ else 'info'
- super(Doctor, self).__init__(
- command=['pw', '--no-banner', '--loglevel', log_level, 'doctor'],
- *args,
- **kwargs)
+ cmd = ['pw', '--no-banner', '--loglevel', log_level, 'doctor']
+ super(Doctor, self).__init__(command=cmd, *args, **kwargs)
def accept(self, visitor):
visitor.visit_doctor(self)
@@ -262,6 +277,7 @@ class Doctor(Command):
class BlankLine(_Action):
"""Write a blank line to the init script."""
+
def accept(self, visitor):
visitor.visit_blank_line(self)
@@ -303,6 +319,7 @@ class Environment(object):
These changes can be accessed by writing them to a file for bash-like
shells to source or by using this as a context manager.
"""
+
def __init__(self, *args, **kwargs):
pathsep = kwargs.pop('pathsep', os.pathsep)
windows = kwargs.pop('windows', os.name == 'nt')
@@ -466,6 +483,7 @@ class Environment(object):
Yields the new environment object.
"""
+ orig_env = {}
try:
if export:
orig_env = os.environ.copy()
diff --git a/pw_env_setup/py/pw_env_setup/gni_visitor.py b/pw_env_setup/py/pw_env_setup/gni_visitor.py
index cd60d80ed..c24d35d76 100644
--- a/pw_env_setup/py/pw_env_setup/gni_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/gni_visitor.py
@@ -15,6 +15,9 @@
from __future__ import print_function
+import ntpath
+import os
+import posixpath
import re
# Disable super() warnings since this file must be Python 2 compatible.
@@ -27,25 +30,28 @@ class GNIVisitor(object): # pylint: disable=useless-object-inheritance
Example gni file:
declare_args() {
- dir_cipd_default = "<ENVIRONMENT_DIR>/cipd/packages/default"
- dir_cipd_pigweed = "<ENVIRONMENT_DIR>/cipd/packages/pigweed"
- dir_cipd_arm = "<ENVIRONMENT_DIR>/cipd/packages/arm"
- dir_cipd_python = "<ENVIRONMENT_DIR>/cipd/packages/python"
- dir_cipd_bazel = "<ENVIRONMENT_DIR>/cipd/packages/bazel"
- dir_cipd_luci = "<ENVIRONMENT_DIR>/cipd/packages/luci"
- dir_virtual_env = "<ENVIRONMENT_DIR>/pigweed-venv"
+ pw_env_setup_CIPD_DEFAULT = "//<ENVIRONMENT_DIR>/cipd/packages/default"
+ pw_env_setup_CIPD_PIGWEED = "//<ENVIRONMENT_DIR>/cipd/packages/pigweed"
+ pw_env_setup_CIPD_ARM = "//<ENVIRONMENT_DIR>/cipd/packages/arm"
+ pw_env_setup_CIPD_PYTHON = "//<ENVIRONMENT_DIR>/cipd/packages/python"
+ pw_env_setup_CIPD_BAZEL = "//<ENVIRONMENT_DIR>/cipd/packages/bazel"
+ pw_env_setup_CIPD_LUCI = "//<ENVIRONMENT_DIR>/cipd/packages/luci"
+ pw_env_setup_VIRTUAL_ENV = "//<ENVIRONMENT_DIR>/pigweed-venv"
}
"""
+
def __init__(self, project_root, *args, **kwargs):
super(GNIVisitor, self).__init__(*args, **kwargs)
self._project_root = project_root
self._lines = []
def serialize(self, env, outs):
- self._lines.append("""
+ self._lines.append(
+ """
# This file is automatically generated by Pigweed's environment setup. Do not
# edit it manually or check it in.
-""".strip())
+""".strip()
+ )
self._lines.append('declare_args() {')
@@ -57,14 +63,36 @@ class GNIVisitor(object): # pylint: disable=useless-object-inheritance
print(line, file=outs)
self._lines = []
+ def _abspath_to_gn_path(self, path):
+ gn_path = os.path.relpath(path, start=self._project_root)
+ if os.name == 'nt':
+ # GN paths are posix-style, so convert to posix. This
+ # find-and-replace is a little crude, but python 2.7 doesn't support
+ # pathlib.
+ gn_path = gn_path.replace(ntpath.sep, posixpath.sep)
+ return '//{}'.format(gn_path)
+
def visit_set(self, set): # pylint: disable=redefined-builtin
match = re.search(r'PW_(.*)_CIPD_INSTALL_DIR', set.name)
if match:
- name = 'dir_cipd_{}'.format(match.group(1).lower())
- self._lines.append(' {} = "{}"'.format(name, set.value))
+ name = 'pw_env_setup_CIPD_{}'.format(match.group(1))
+ self._lines.append(
+ ' {} = "{}"'.format(name, self._abspath_to_gn_path(set.value))
+ )
if set.name == 'VIRTUAL_ENV':
- self._lines.append(' dir_virtual_env = "{}"'.format(set.value))
+ self._lines.append(
+ ' pw_env_setup_VIRTUAL_ENV = "{}"'.format(
+ self._abspath_to_gn_path(set.value)
+ )
+ )
+
+ if set.name == 'PW_PACKAGE_ROOT':
+ self._lines.append(
+ ' pw_env_setup_PACKAGE_ROOT = "{}"'.format(
+ self._abspath_to_gn_path(set.value)
+ )
+ )
def visit_clear(self, clear):
pass
diff --git a/pw_env_setup/py/pw_env_setup/json_visitor.py b/pw_env_setup/py/pw_env_setup/json_visitor.py
index f96acac6b..8c217bf74 100644
--- a/pw_env_setup/py/pw_env_setup/json_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/json_visitor.py
@@ -21,6 +21,7 @@ import json
class JSONVisitor(object): # pylint: disable=useless-object-inheritance
"""Serializes an Environment into a JSON file."""
+
def __init__(self, *args, **kwargs):
super(JSONVisitor, self).__init__(*args, **kwargs)
self._data = {}
diff --git a/pw_env_setup/py/pw_env_setup/python_packages.py b/pw_env_setup/py/pw_env_setup/python_packages.py
index f2fb1383b..34cd04307 100644
--- a/pw_env_setup/py/pw_env_setup/python_packages.py
+++ b/pw_env_setup/py/pw_env_setup/python_packages.py
@@ -16,32 +16,84 @@
"""Save list of installed packages and versions."""
import argparse
-import subprocess
+import itertools
import sys
-from typing import Dict, List, TextIO, Union
+from pathlib import Path
+from typing import Dict, Iterator, List, Optional, Union
+import pkg_resources
-def _installed_packages():
+
+def _installed_packages() -> Iterator[str]:
"""Run pip python_packages and write to out."""
- cmd = [
- 'python',
- '-m',
- 'pip',
- 'freeze',
- '--exclude-editable',
- '--local',
- ]
- proc = subprocess.run(cmd, capture_output=True)
- for line in proc.stdout.decode().splitlines():
- if ' @ ' not in line:
- yield line
+ installed_packages = list(
+ pkg.as_requirement()
+ for pkg in pkg_resources.working_set # pylint: disable=not-an-iterable
+ # Non-editable packages only
+ if isinstance(pkg, pkg_resources.DistInfoDistribution) # type: ignore
+ # This will skip packages with local versions.
+ # For example text after a plus sign: 1.2.3+dev456
+ and not pkg.parsed_version.local
+ # These are always installed by default in:
+ # pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
+ and pkg.key not in ['pip', 'setuptools', 'wheel']
+ )
+ for req in sorted(
+ installed_packages, key=lambda pkg: pkg.name.lower() # type: ignore
+ ):
+ yield str(req)
-def ls(out: TextIO) -> int: # pylint: disable=invalid-name
- """Run pip python_packages and write to out."""
- for package in _installed_packages():
+def ls(output_file: Optional[Path]) -> int: # pylint: disable=invalid-name
+ """Run pip python_packages and write to output_file."""
+ actual_requirements = frozenset(
+ pkg_resources.Requirement.parse(line) for line in _installed_packages()
+ )
+ missing_requirements = set()
+
+ # If updating an existing file, load the existing requirements to find lines
+ # that are missing in the active environment.
+ if output_file:
+ existing_lines = output_file.read_text().splitlines()
+ expected_requirements = set(
+ pkg_resources.Requirement.parse(line) for line in existing_lines
+ )
+ missing_requirements = expected_requirements - actual_requirements
+
+ new_requirements: List[pkg_resources.Requirement] = list(
+ actual_requirements
+ )
+
+ for requirement in missing_requirements:
+ # Preserve this requirement if it has a marker that doesn't apply to
+ # the current environment. For example a line with ;python_version <
+ # "3.9" if running on Python 3.9 or higher will be saved.
+ if requirement.marker and not requirement.marker.evaluate():
+ new_requirements.append(requirement)
+ continue
+
+ # If this package is in the active environment then it has a marker
+ # in the existing file that should be preserved.
+ try:
+ found_package = pkg_resources.working_set.find(requirement)
+ # If the package version doesn't match, save the new version.
+ except pkg_resources.VersionConflict:
+ found_package = None
+ if found_package:
+ # Delete the old line with no marker.
+ new_requirements.remove(found_package.as_requirement())
+ # Add the existing requirement line that includes the marker.
+ new_requirements.append(requirement)
+
+ out = output_file.open('w') if output_file else sys.stdout
+
+ for package in sorted(
+ new_requirements, key=lambda pkg: pkg.name.lower() # type: ignore
+ ):
print(package, file=out)
+ if output_file:
+ out.close()
return 0
@@ -53,45 +105,178 @@ def _stderr(*args, **kwargs):
return print(*args, file=sys.stderr, **kwargs)
-def diff(expected: TextIO) -> int:
+def _load_requirements_lines(*req_files: Path) -> Iterator[str]:
+ for req_file in req_files:
+ for line in req_file.read_text().splitlines():
+ # Ignore comments and blank lines
+ if line.startswith('#') or line == '':
+ continue
+ yield line
+
+
+def diff(
+ expected: Path, ignore_requirements_file: Optional[List[Path]] = None
+) -> int:
"""Report on differences between installed and expected versions."""
actual_lines = set(_installed_packages())
- expected_lines = set(expected.read().splitlines())
+ expected_lines = set(_load_requirements_lines(expected))
+ ignored_lines = set()
+ if ignore_requirements_file:
+ ignored_lines = set(_load_requirements_lines(*ignore_requirements_file))
if actual_lines == expected_lines:
_stderr('package versions are identical')
return 0
- removed_entries: Dict[str, str] = dict(
- x.split('==', 1) # type: ignore[misc]
- for x in expected_lines - actual_lines)
- added_entries: Dict[str, str] = dict(
- x.split('==', 1) # type: ignore[misc]
- for x in actual_lines - expected_lines)
+ actual_requirements = frozenset(
+ pkg_resources.Requirement.parse(line) for line in actual_lines
+ )
+ expected_requirements = frozenset(
+ pkg_resources.Requirement.parse(line) for line in expected_lines
+ )
+ ignored_requirements = frozenset(
+ pkg_resources.Requirement.parse(line) for line in ignored_lines
+ )
+
+ removed_requirements = expected_requirements - actual_requirements
+ added_requirements = actual_requirements - expected_requirements
- new_packages = set(added_entries) - set(removed_entries)
- removed_packages = set(removed_entries) - set(added_entries)
- updated_packages = set(added_entries).intersection(set(removed_entries))
+ removed_packages: Dict[pkg_resources.Requirement, str] = {}
+ updated_packages: Dict[pkg_resources.Requirement, str] = {}
+ new_packages: Dict[pkg_resources.Requirement, str] = {}
+ reformatted_packages: Dict[pkg_resources.Requirement, str] = {}
- if removed_packages:
- _stderr('Removed packages')
- for package in removed_packages:
- _stderr(f' {package}=={removed_entries[package]}')
+ for line in expected_lines:
+ requirement = pkg_resources.Requirement.parse(line)
+
+ # Check for lines that need reformatting
+ # This will catch lines that use underscores instead of dashes in the
+ # name of missing spaces after specifiers.
+
+ # Match this requirement with the original one found in
+ # actual_requirements.
+ #
+ # Note the requirement variable may equal its counterpart in
+ # actual_requirements due to the .hashCpm on the Requirement class
+ # checking their normalized name. Since we are looking for formatting
+ # mismatches here we need to retrieve the requirement instance from the
+ # actual_requirements set.
+ matching_found_requirement = {
+ req: req for req in actual_requirements
+ }.get(requirement)
+
+ # If the actual requirement line doesn't match.
+ if (
+ matching_found_requirement
+ and str(matching_found_requirement) != line
+ ):
+ reformatted_packages[matching_found_requirement] = line
+
+ # If this requirement isn't in the active enviroment and the line
+ # doesn't match the repr: flag for reformatting.
+ if not matching_found_requirement and str(requirement) != line:
+ reformatted_packages[requirement] = line
+
+ # If a requirement specifier is used and it doesn't apply, skip this
+ # line. See details for requirement specifiers at:
+ # https://pip.pypa.io/en/stable/reference/requirement-specifiers/#requirement-specifiers
+ if requirement.marker and not requirement.marker.evaluate():
+ continue
+
+ # Try to find this requirement in the current environment.
+ try:
+ found_package = pkg_resources.working_set.find(requirement)
+ # If the package version doesn't match, save the new version.
+ except pkg_resources.VersionConflict as err:
+ found_package = None
+ if err.dist:
+ found_package = err.dist
+
+ # If this requirement isn't in the environment, it was removed.
+ if not found_package:
+ removed_packages[requirement] = line
+ continue
+
+ # If found_package is set, the version doesn't match so it was updated.
+ if requirement.specs != found_package.as_requirement().specs:
+ updated_packages[found_package.as_requirement()] = line
+
+ ignored_distributions = list(
+ distribution
+ for distribution in pkg_resources.working_set # pylint: disable=not-an-iterable
+ if distribution.as_requirement() in ignored_requirements
+ )
+ expected_distributions = list(
+ distribution
+ for distribution in pkg_resources.working_set # pylint: disable=not-an-iterable
+ if distribution.as_requirement() in expected_requirements
+ )
+
+ def get_requirements(
+ dist_info: pkg_resources.Distribution,
+ ) -> Iterator[pkg_resources.Distribution]:
+ """Return requirement that are not in expected_distributions."""
+ for req in dist_info.requires():
+ req_dist_info = pkg_resources.working_set.find(req)
+ if not req_dist_info:
+ continue
+ if req_dist_info in expected_distributions:
+ continue
+ yield req_dist_info
+
+ def expand_requirements(
+ reqs: List[pkg_resources.Distribution],
+ ) -> Iterator[List[pkg_resources.Distribution]]:
+ """Recursively expand requirements."""
+ for dist_info in reqs:
+ deps = list(get_requirements(dist_info))
+ if deps:
+ yield deps
+ yield from expand_requirements(deps)
+
+ ignored_transitive_deps = set(
+ itertools.chain.from_iterable(
+ expand_requirements(ignored_distributions)
+ )
+ )
+
+ # Check for new packages
+ for requirement in added_requirements - removed_requirements:
+ if requirement in updated_packages:
+ continue
+ if requirement in ignored_requirements or ignored_transitive_deps:
+ continue
+
+ new_packages[requirement] = str(requirement)
+
+ # Print status messages to stderr
+
+ if reformatted_packages:
+ _stderr('Requirements that need reformatting:')
+ for requirement, line in reformatted_packages.items():
+ _stderr(f' {line}')
+ _stderr(' should be:')
+ _stderr(f' {str(requirement)}')
if updated_packages:
_stderr('Updated packages')
- for package in updated_packages:
- _stderr(f' {package}=={added_entries[package]} (from '
- f'{removed_entries[package]})')
+ for requirement, line in updated_packages.items():
+ _stderr(f' {str(requirement)} (from {line})')
+
+ if removed_packages:
+ _stderr('Removed packages')
+ for requirement in removed_packages:
+ _stderr(f' {requirement}')
if new_packages:
_stderr('New packages')
- for package in new_packages:
- _stderr(f' {package}=={added_entries[package]}')
+ for requirement in new_packages:
+ _stderr(f' {requirement}')
if updated_packages or new_packages:
_stderr("Package versions don't match!")
- _stderr(f"""
+ _stderr(
+ f"""
Please do the following:
* purge your environment directory
@@ -102,7 +287,8 @@ Please do the following:
* Windows: 'bootstrap.bat'
* update the constraint file
* 'pw python-packages list {expected.name}'
-""")
+"""
+ )
return -1
return 0
@@ -110,38 +296,36 @@ Please do the following:
def parse(argv: Union[List[str], None] = None) -> argparse.Namespace:
"""Parse command-line arguments."""
- parser = argparse.ArgumentParser()
+ parser = argparse.ArgumentParser(
+ prog="python -m pw_env_setup.python_packages"
+ )
subparsers = parser.add_subparsers(dest='cmd')
list_parser = subparsers.add_parser(
- 'list', aliases=('ls', ), help='List installed package versions.')
- list_parser.add_argument('out',
- type=argparse.FileType('w'),
- default=sys.stdout,
- nargs='?')
+ 'list', aliases=('ls',), help='List installed package versions.'
+ )
+ list_parser.add_argument('output_file', type=Path, nargs='?')
diff_parser = subparsers.add_parser(
'diff',
help='Show differences between expected and actual package versions.',
)
- diff_parser.add_argument('expected', type=argparse.FileType('r'))
+ diff_parser.add_argument('expected', type=Path)
+ diff_parser.add_argument(
+ '--ignore-requirements-file', type=Path, action='append'
+ )
return parser.parse_args(argv)
def main() -> int:
- try:
- args = vars(parse())
- cmd = args.pop('cmd')
- if cmd == 'diff':
- return diff(**args)
- if cmd == 'list':
- return ls(**args)
- return -1
- except subprocess.CalledProcessError as err:
- print(file=sys.stderr)
- print(err.output, file=sys.stderr)
- raise
+ args = vars(parse())
+ cmd = args.pop('cmd')
+ if cmd == 'diff':
+ return diff(**args)
+ if cmd == 'list':
+ return ls(**args)
+ return -1
if __name__ == '__main__':
diff --git a/pw_env_setup/py/pw_env_setup/shell_visitor.py b/pw_env_setup/py/pw_env_setup/shell_visitor.py
index ccbde5104..71849294d 100644
--- a/pw_env_setup/py/pw_env_setup/shell_visitor.py
+++ b/pw_env_setup/py/pw_env_setup/shell_visitor.py
@@ -27,29 +27,35 @@ class _BaseShellVisitor(object): # pylint: disable=useless-object-inheritance
self._outs = None
def _remove_value_from_path(self, variable, value):
- return ('{variable}="$(echo "${variable}"'
- ' | sed "s|{pathsep}{value}{pathsep}|{pathsep}|g;"'
- ' | sed "s|^{value}{pathsep}||g;"'
- ' | sed "s|{pathsep}{value}$||g;"'
- ')"\nexport {variable}\n'.format(variable=variable,
- value=value,
- pathsep=self._pathsep))
+ return (
+ '{variable}="$(echo "${variable}"'
+ ' | sed "s|{pathsep}{value}{pathsep}|{pathsep}|g;"'
+ ' | sed "s|^{value}{pathsep}||g;"'
+ ' | sed "s|{pathsep}{value}$||g;"'
+ ')"\nexport {variable}\n'.format(
+ variable=variable, value=value, pathsep=self._pathsep
+ )
+ )
def visit_hash(self, hash): # pylint: disable=redefined-builtin
del hash
self._outs.write(
- inspect.cleandoc('''
+ inspect.cleandoc(
+ '''
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting past
# commands the $PATH changes we made may not be respected.
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
hash -r\n
fi
- '''))
+ '''
+ )
+ )
class ShellVisitor(_BaseShellVisitor):
"""Serializes an Environment into a shell file."""
+
def __init__(self, *args, **kwargs):
super(ShellVisitor, self).__init__(*args, **kwargs)
self._replacements = ()
@@ -58,7 +64,8 @@ class ShellVisitor(_BaseShellVisitor):
try:
self._replacements = tuple(
(key, env.get(key) if value is None else value)
- for key, value in env.replacements)
+ for key, value in env.replacements
+ )
self._outs = outs
env.accept(self)
@@ -76,16 +83,21 @@ class ShellVisitor(_BaseShellVisitor):
def visit_set(self, set): # pylint: disable=redefined-builtin
value = self._apply_replacements(set)
- self._outs.write('{name}="{value}"\nexport {name}\n'.format(
- name=set.name, value=value))
+ self._outs.write(
+ '{name}="{value}"\nexport {name}\n'.format(
+ name=set.name, value=value
+ )
+ )
def visit_clear(self, clear):
self._outs.write('unset {name}\n'.format(**vars(clear)))
def visit_remove(self, remove):
value = self._apply_replacements(remove)
- self._outs.write('# Remove \n# {value}\n# from\n# {value}\n# '
- 'before adding it back.\n')
+ self._outs.write(
+ '# Remove \n# {value}\n# from {name} before adding it '
+ 'back.\n'.format(value=remove.value, name=remove.name)
+ )
self._outs.write(self._remove_value_from_path(remove.name, value))
def _join(self, *args):
@@ -96,14 +108,20 @@ class ShellVisitor(_BaseShellVisitor):
def visit_prepend(self, prepend):
value = self._apply_replacements(prepend)
value = self._join(value, '${}'.format(prepend.name))
- self._outs.write('{name}="{value}"\nexport {name}\n'.format(
- name=prepend.name, value=value))
+ self._outs.write(
+ '{name}="{value}"\nexport {name}\n'.format(
+ name=prepend.name, value=value
+ )
+ )
def visit_append(self, append):
value = self._apply_replacements(append)
value = self._join('${}'.format(append.name), value)
- self._outs.write('{name}="{value}"\nexport {name}\n'.format(
- name=append.name, value=value))
+ self._outs.write(
+ '{name}="{value}"\nexport {name}\n'.format(
+ name=append.name, value=value
+ )
+ )
def visit_echo(self, echo):
# TODO(mohrr) use shlex.quote().
@@ -131,8 +149,10 @@ class ShellVisitor(_BaseShellVisitor):
self._outs.write('if [ -z "$PW_ACTIVATE_SKIP_CHECKS" ]; then\n')
self.visit_command(doctor)
self._outs.write('else\n')
- self._outs.write('echo Skipping environment check because '
- 'PW_ACTIVATE_SKIP_CHECKS is set\n')
+ self._outs.write(
+ 'echo Skipping environment check because '
+ 'PW_ACTIVATE_SKIP_CHECKS is set\n'
+ )
self._outs.write('fi\n')
def visit_blank_line(self, blank_line):
@@ -140,12 +160,16 @@ class ShellVisitor(_BaseShellVisitor):
self._outs.write('\n')
def visit_function(self, function):
- self._outs.write('{name}() {{\n{body}\n}}\n'.format(
- name=function.name, body=function.body))
+ self._outs.write(
+ '{name}() {{\n{body}\n}}\n'.format(
+ name=function.name, body=function.body
+ )
+ )
class DeactivateShellVisitor(_BaseShellVisitor):
"""Removes values from an Environment."""
+
def __init__(self, *args, **kwargs):
pathsep = kwargs.pop('pathsep', ':')
super(DeactivateShellVisitor, self).__init__(*args, **kwargs)
@@ -172,11 +196,13 @@ class DeactivateShellVisitor(_BaseShellVisitor):
def visit_prepend(self, prepend):
self._outs.write(
- self._remove_value_from_path(prepend.name, prepend.value))
+ self._remove_value_from_path(prepend.name, prepend.value)
+ )
def visit_append(self, append):
self._outs.write(
- self._remove_value_from_path(append.name, append.value))
+ self._remove_value_from_path(append.name, append.value)
+ )
def visit_echo(self, echo):
pass # Not relevant.
diff --git a/pw_env_setup/py/pw_env_setup/spinner.py b/pw_env_setup/py/pw_env_setup/spinner.py
index 0a5c09968..03c07a943 100644
--- a/pw_env_setup/py/pw_env_setup/spinner.py
+++ b/pw_env_setup/py/pw_env_setup/spinner.py
@@ -22,6 +22,7 @@ import time
class Spinner(object): # pylint: disable=useless-object-inheritance
"""Spinner!"""
+
def __init__(self, quiet=False):
self._done = None
self._thread = None
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/__init__.py b/pw_env_setup/py/pw_env_setup/virtualenv_setup/__init__.py
index 9e6cb96d6..f5e0adc55 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/__init__.py
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/__init__.py
@@ -13,5 +13,4 @@
# the License.
"""Sets up a Python 3 virtualenv for Pigweed."""
-# TODO(pwbug/67) move install.py contents to this file.
from .install import *
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/__main__.py b/pw_env_setup/py/pw_env_setup/virtualenv_setup/__main__.py
index 29ff21af3..dfd97ac38 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/__main__.py
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/__main__.py
@@ -17,45 +17,60 @@ import argparse
import os
import sys
-# TODO(pwbug/67) switch back to 'from pw_env_setup import virtualenv_setup'.
-# from pw_env_setup import virtualenv_setup
# pylint: disable=import-error
-import install as virtualenv_setup # type: ignore
+try:
+ from pw_env_setup import virtualenv_setup
+except ImportError:
+ import install as virtualenv_setup # type: ignore
# pylint: enable=import-error
def _main():
- parser = argparse.ArgumentParser(description=__doc__)
+ parser = argparse.ArgumentParser(
+ prog="python -m pw_env_setup.virtualenv_setup", description=__doc__
+ )
project_root = os.environ.get('PW_PROJECT_ROOT', None)
- parser.add_argument('--project-root',
- default=project_root,
- required=not project_root,
- help='Path to overall project root.')
- parser.add_argument('--venv_path',
- required=True,
- help='Path at which to create the venv')
- parser.add_argument('-r',
- '--requirements',
- default=[],
- action='append',
- help='requirements.txt files to install')
- parser.add_argument('--gn-target',
- dest='gn_targets',
- default=[],
- action='append',
- type=virtualenv_setup.GnTarget,
- help='GN targets that install packages')
- parser.add_argument('--quick-setup',
- dest='full_envsetup',
- action='store_false',
- default='PW_ENVSETUP_FULL' in os.environ,
- help=('Do full setup or only minimal checks to see if '
- 'full setup is required.'))
- parser.add_argument('--python',
- default=sys.executable,
- help='Python to use when creating virtualenv.')
+ parser.add_argument(
+ '--project-root',
+ default=project_root,
+ required=not project_root,
+ help='Path to overall project root.',
+ )
+ parser.add_argument(
+ '--venv_path', required=True, help='Path at which to create the venv'
+ )
+ parser.add_argument(
+ '-r',
+ '--requirements',
+ default=[],
+ action='append',
+ help='requirements.txt files to install',
+ )
+ parser.add_argument(
+ '--gn-target',
+ dest='gn_targets',
+ default=[],
+ action='append',
+ type=virtualenv_setup.GnTarget,
+ help='GN targets that install packages',
+ )
+ parser.add_argument(
+ '--quick-setup',
+ dest='full_envsetup',
+ action='store_false',
+ default='PW_ENVSETUP_FULL' in os.environ,
+ help=(
+ 'Do full setup or only minimal checks to see if '
+ 'full setup is required.'
+ ),
+ )
+ parser.add_argument(
+ '--python',
+ default=sys.executable,
+ help='Python to use when creating virtualenv.',
+ )
virtualenv_setup.install(**vars(parser.parse_args()))
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
index ef1d395a4..bee66aaf3 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/constraint.list
@@ -1,10 +1,11 @@
alabaster==0.7.12
appdirs==1.4.4
-astroid==2.6.6
+astroid==2.14.2
Babel==2.9.1
backcall==0.2.0
-beautifulsoup4==4.10.0
-build==0.7.0
+black==23.1.0
+build==0.8.0
+cachetools==5.0.0
certifi==2021.10.8
cffi==1.15.0
charset-normalizer==2.0.10
@@ -12,12 +13,17 @@ coloredlogs==15.0.1
coverage==6.3
cryptography==36.0.1
decorator==5.1.1
+dill==0.3.6
docutils==0.17.1
-furo==2022.1.2
-future==0.18.2
-grpcio==1.43.0
-grpcio-tools==1.43.0
-httpwatcher==0.5.2
+breathe==4.34.0
+google-api-core==2.7.1
+google-auth==2.6.3
+google-cloud-core==2.2.3
+google-cloud-storage==2.2.1
+google-crc32c==1.3.0
+google-resumable-media==2.3.2
+googleapis-common-protos==1.56.2
+graphlib-backport==1.0.3; python_version < "3.9"
humanfriendly==10.0
idna==3.3
imagesize==1.3.0
@@ -29,59 +35,62 @@ lazy-object-proxy==1.7.1
MarkupSafe==2.0.1
matplotlib-inline==0.1.3
mccabe==0.6.1
-mypy==0.910
-mypy-extensions==0.4.3
-mypy-protobuf==2.9
-packaging==21.3
+mypy==1.0.1
+mypy-extensions==1.0.0
+mypy-protobuf==3.3.0
+packaging==23.0
parameterized==0.8.1
parso==0.8.3
pep517==0.12.0
pexpect==4.8.0
+platformdirs==3.0.0
pickleshare==0.7.5
-prompt-toolkit==3.0.26
-protobuf==3.19.1
-psutil==5.9.0
-ptpython==3.0.20
+prompt-toolkit==3.0.36
+protobuf==3.20.2
+psutil==5.9.4
+ptpython==3.0.22
ptyprocess==0.7.0
+pyasn1==0.4.8
+pyasn1-modules==0.2.8
pycparser==2.21
pyelftools==0.27
-Pygments==2.11.2
-pygments-style-dracula==1.2.5.1
-pygments-style-tomorrow==1.0.0.1
-pylint==2.9.3
+Pygments==2.14.0
+pylint==2.16.2
pyparsing==3.0.6
pyperclip==1.8.2
pyserial==3.5
pytz==2021.3
PyYAML==6.0
requests==2.27.1
-robotframework==3.1
+rsa==4.8
scan-build==2.0.19
+setuptools==63.4.3
six==1.16.0
snowballstemmer==2.2.0
-soupsieve==2.3.1
-Sphinx==4.3.2
-sphinx-design==0.0.13
-sphinx-rtd-theme==1.0.0
+Sphinx==5.3.0
+sphinx-rtd-theme==1.2.0
+sphinx-argparse==0.4.0
+sphinx-copybutton==0.5.1
+sphinx-design==0.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
-sphinxcontrib-mermaid==0.7.1
+sphinxcontrib-mermaid==0.8
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
toml==0.10.2
-tomli==2.0.0
-tornado==4.5.3
+tomli==2.0.1
+tomlkit==0.11.6
traitlets==5.1.1
types-docutils==0.17.4
types-futures==3.3.2
-types-protobuf==3.18.4
+types-protobuf==3.20.4.6
types-Pygments==2.9.13
-types-PyYAML==6.0.3
-types-setuptools==57.4.7
+types-PyYAML==6.0.7
+types-setuptools==63.4.1
types-six==1.16.9
-typing_extensions==4.0.1
+typing-extensions==4.4.0
urllib3==1.26.8
watchdog==2.1.6
wcwidth==0.2.5
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py b/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
index 067b244b8..f346d0557 100644
--- a/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/install.py
@@ -24,6 +24,7 @@ import re
import shutil
import subprocess
import sys
+import stat
import tempfile
# Grabbing datetime string once so it will always be the same for all GnTarget
@@ -35,7 +36,8 @@ class GnTarget(object): # pylint: disable=useless-object-inheritance
def __init__(self, val):
self.directory, self.target = val.split('#', 1)
self.name = '-'.join(
- (re.sub(r'\W+', '_', self.target).strip('_'), _DATETIME_STRING))
+ (re.sub(r'\W+', '_', self.target).strip('_'), _DATETIME_STRING)
+ )
def git_stdout(*args, **kwargs):
@@ -75,7 +77,6 @@ def _required_packages(requirements):
return packages
-# TODO(pwbug/135) Move to common utility module.
def _check_call(args, **kwargs):
stdout = kwargs.get('stdout', sys.stdout)
@@ -109,7 +110,6 @@ def _find_files_by_name(roots, name, allow_nesting=False):
def _check_venv(python, version, venv_path, pyvenv_cfg):
- # TODO(pwbug/400) Re-enable this check on Windows.
if platform.system().lower() == 'windows':
return
@@ -130,11 +130,51 @@ def _check_venv(python, version, venv_path, pyvenv_cfg):
shutil.rmtree(venv_path)
-def install( # pylint: disable=too-many-arguments
+def _check_python_install_permissions(python):
+ # These pickle files are not included on windows.
+ # The path on windows is environment/cipd/packages/python/bin/Lib/lib2to3/
+ if platform.system().lower() == 'windows':
+ return
+
+ # Make any existing lib2to3 pickle files read+write. This is needed for
+ # importing yapf.
+ lib2to3_path = os.path.join(
+ os.path.dirname(os.path.dirname(python)), 'lib', 'python3.9', 'lib2to3'
+ )
+ pickle_file_paths = []
+ if os.path.isdir(lib2to3_path):
+ pickle_file_paths.extend(
+ file_path
+ for file_path in os.listdir(lib2to3_path)
+ if '.pickle' in file_path
+ )
+ try:
+ for pickle_file in pickle_file_paths:
+ pickle_full_path = os.path.join(lib2to3_path, pickle_file)
+ os.chmod(
+ pickle_full_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP
+ )
+ except PermissionError:
+ pass
+
+
+def _flatten(*items):
+ """Yields items from a series of items and nested iterables."""
+
+ for item in items:
+ if isinstance(item, (list, tuple)):
+ for i in _flatten(*item):
+ yield i
+ else:
+ yield item
+
+
+def install( # pylint: disable=too-many-arguments,too-many-locals
project_root,
venv_path,
full_envsetup=True,
- requirements=(),
+ requirements=None,
+ constraints=None,
gn_args=(),
gn_targets=(),
gn_out_dir=None,
@@ -145,11 +185,12 @@ def install( # pylint: disable=too-many-arguments
):
"""Creates a venv and installs all packages in this Git repo."""
- version = subprocess.check_output(
- (python, '--version'), stderr=subprocess.STDOUT).strip().decode()
- # We expect Python 3.8, but if it came from CIPD let it pass anyway.
- if ('3.8' not in version and '3.9' not in version
- and 'chromium' not in version):
+ version = (
+ subprocess.check_output((python, '--version'), stderr=subprocess.STDOUT)
+ .strip()
+ .decode()
+ )
+ if ' 3.' not in version:
print('=' * 60, file=sys.stderr)
print('Unexpected Python version:', version, file=sys.stderr)
print('=' * 60, file=sys.stderr)
@@ -175,6 +216,7 @@ def install( # pylint: disable=too-many-arguments
pyvenv_cfg = os.path.join(venv_path, 'pyvenv.cfg')
+ _check_python_install_permissions(python)
_check_venv(python, version, venv_path, pyvenv_cfg)
if full_envsetup or not os.path.exists(pyvenv_cfg):
@@ -209,29 +251,53 @@ def install( # pylint: disable=too-many-arguments
# all come from 'pw'-prefixed packages we installed with --editable.
# Source: https://stackoverflow.com/a/48972085
for egg_link in glob.glob(
- os.path.join(venv_path, 'lib/python*/site-packages/*.egg-link')):
+ os.path.join(venv_path, 'lib/python*/site-packages/*.egg-link')
+ ):
os.unlink(egg_link)
def pip_install(*args):
+ args = list(_flatten(args))
with env():
- cmd = [venv_python, '-m', 'pip', 'install'] + list(args)
+ cmd = [venv_python, '-m', 'pip', 'install'] + args
return _check_call(cmd)
+ constraint_args = []
+ if constraints:
+ constraint_args.extend(
+ '--constraint={}'.format(constraint) for constraint in constraints
+ )
+
pip_install(
'--log',
os.path.join(venv_path, 'pip-upgrade.log'),
'--upgrade',
'pip',
'setuptools',
+ 'toml', # Needed for pyproject.toml package installs.
# Include wheel so pip installs can be done without build
# isolation.
- 'wheel')
+ 'wheel',
+ constraint_args,
+ )
+
+ # TODO(tonymd): Remove this when projects have defined requirements.
+ if (not requirements) and constraints:
+ requirements = constraints
if requirements:
- requirement_args = tuple('--requirement={}'.format(req)
- for req in requirements)
- pip_install('--log', os.path.join(venv_path, 'pip-requirements.log'),
- *requirement_args)
+ requirement_args = []
+ # Note: --no-build-isolation should be avoided for installing 3rd party
+ # Python packages that use C/C++ extension modules.
+ # https://setuptools.pypa.io/en/latest/userguide/ext_modules.html
+ requirement_args.extend(
+ '--requirement={}'.format(req) for req in requirements
+ )
+ combined_requirement_args = requirement_args + constraint_args
+ pip_install(
+ '--log',
+ os.path.join(venv_path, 'pip-requirements.log'),
+ combined_requirement_args,
+ )
def install_packages(gn_target):
if gn_out_dir is None:
@@ -266,15 +332,17 @@ def install( # pylint: disable=too-many-arguments
gn_cmd.append('--args={}'.format(' '.join(args)))
print(gn_cmd, file=outs)
- subprocess.check_call(gn_cmd,
- cwd=os.path.join(project_root,
- gn_target.directory),
- stdout=outs,
- stderr=outs)
+ subprocess.check_call(
+ gn_cmd,
+ cwd=os.path.join(project_root, gn_target.directory),
+ stdout=outs,
+ stderr=outs,
+ )
except subprocess.CalledProcessError as err:
with open(gn_log_path, 'r') as ins:
- raise subprocess.CalledProcessError(err.returncode, err.cmd,
- ins.read())
+ raise subprocess.CalledProcessError(
+ err.returncode, err.cmd, ins.read()
+ )
ninja_log = 'ninja-{}.log'.format(gn_target.name)
ninja_log_path = os.path.join(venv_path, ninja_log)
@@ -286,8 +354,9 @@ def install( # pylint: disable=too-many-arguments
subprocess.check_call(ninja_cmd, stdout=outs, stderr=outs)
except subprocess.CalledProcessError as err:
with open(ninja_log_path, 'r') as ins:
- raise subprocess.CalledProcessError(err.returncode, err.cmd,
- ins.read())
+ raise subprocess.CalledProcessError(
+ err.returncode, err.cmd, ins.read()
+ )
with open(os.path.join(venv_path, 'pip-list.log'), 'w') as outs:
subprocess.check_call(
diff --git a/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt b/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt
new file mode 100644
index 000000000..fcb9baf65
--- /dev/null
+++ b/pw_env_setup/py/pw_env_setup/virtualenv_setup/pigweed_upstream_requirements.txt
@@ -0,0 +1,23 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Pigweed upstream specific depenencies:
+# pigweed.dev Sphinx themes
+furo==2022.12.7
+sphinx-copybutton==0.5.1
+myst-parser==0.18.1
+breathe==4.34.0
+# Renode requirements
+psutil==5.9.4
+robotframework==5.0.1
diff --git a/pw_env_setup/py/pw_env_setup/windows_env_start.py b/pw_env_setup/py/pw_env_setup/windows_env_start.py
index 4d330753f..652496071 100644
--- a/pw_env_setup/py/pw_env_setup/windows_env_start.py
+++ b/pw_env_setup/py/pw_env_setup/windows_env_start.py
@@ -46,22 +46,31 @@ def print_banner(bootstrap, no_shell_file):
if bootstrap:
print(
- Color.green('\n BOOTSTRAP! Bootstrap may take a few minutes; '
- 'please be patient'))
+ Color.green(
+ '\n BOOTSTRAP! Bootstrap may take a few minutes; '
+ 'please be patient'
+ )
+ )
print(
Color.green(
- ' On Windows, this stage is extremely slow (~10 minutes).\n'))
+ ' On Windows, this stage is extremely slow (~10 minutes).\n'
+ )
+ )
else:
print(
Color.green(
'\n ACTIVATOR! This sets your console environment variables.\n'
- ))
+ )
+ )
if no_shell_file:
print(Color.bold_red('Error!\n'))
print(
- Color.red(' Your Pigweed environment does not seem to be'
- ' configured.'))
+ Color.red(
+ ' Your Pigweed environment does not seem to be'
+ ' configured.'
+ )
+ )
print(Color.red(' Run bootstrap.bat to perform initial setup.'))
return 0
@@ -69,7 +78,9 @@ def print_banner(bootstrap, no_shell_file):
def parse():
"""Parse command-line arguments."""
- parser = argparse.ArgumentParser()
+ parser = argparse.ArgumentParser(
+ prog="python -m pw_env_setup.windows_env_start"
+ )
parser.add_argument('--bootstrap', action='store_true')
parser.add_argument('--no-shell-file', action='store_true')
return parser.parse_args()
diff --git a/pw_env_setup/py/python_packages_test.py b/pw_env_setup/py/python_packages_test.py
index 27720c6c9..92c79bff7 100755
--- a/pw_env_setup/py/python_packages_test.py
+++ b/pw_env_setup/py/python_packages_test.py
@@ -14,66 +14,82 @@
# the License.
"""Tests the python_packages module."""
-import collections
-import io
+import importlib.metadata
+from pathlib import Path
+import tempfile
import unittest
from unittest import mock
from pw_env_setup import python_packages
-def _subprocess_run_stdout(stdout=b'foo==1.0\nbar==2.0\npw-foo @ file:...\n'):
- def subprocess_run(*unused_args, **unused_kwargs):
- CompletedProcess = collections.namedtuple('CompletedProcess', 'stdout')
- return CompletedProcess(stdout=stdout)
+class TestPythonPackages(unittest.TestCase):
+ """Tests the python_packages module."""
- return subprocess_run
+ def setUp(self):
+ self.existing_pkgs_minus_toml = '\n'.join(
+ pkg
+ for pkg in python_packages._installed_packages() # pylint: disable=protected-access
+ if not pkg.startswith('toml==')
+ )
+ self.temp_dir = tempfile.TemporaryDirectory()
+ self.temp_path = Path(self.temp_dir.name)
+ def tearDown(self):
+ self.temp_dir.cleanup()
+
+ def test_list(self):
+ # pylint: disable=protected-access
+ pkgs = list(python_packages._installed_packages())
+ # pylint: enable=protected-access
+ toml_version = importlib.metadata.version('toml')
+
+ self.assertIn(f'toml=={toml_version}', pkgs)
+ self.assertNotIn('pw-foo', pkgs)
-class TestPythonPackages(unittest.TestCase):
- """Tests the python_packages module."""
- @mock.patch('pw_env_setup.python_packages.subprocess.run',
- side_effect=_subprocess_run_stdout())
- def test_list(self, unused_mock):
- buf = io.StringIO()
- python_packages.ls(buf)
- self.assertIn('foo==1.0', buf.getvalue())
- self.assertIn('bar==2.0', buf.getvalue())
- self.assertNotIn('pw-foo', buf.getvalue())
-
- @mock.patch('pw_env_setup.python_packages.subprocess.run',
- side_effect=_subprocess_run_stdout())
@mock.patch('pw_env_setup.python_packages._stderr')
- def test_diff_removed(self, stderr_mock, unused_mock):
- expected = io.StringIO('foo==1.0\nbar==2.0\nbaz==3.0\n')
- expected.name = 'test.name'
- self.assertFalse(python_packages.diff(expected))
+ def test_diff_removed(self, stderr_mock):
+ expected = 'foo==1.0\nbar==2.0\nbaz==3.0\n'
+ expected_file = self.temp_path / 'test_diff_removed_expected'
+ expected_file.write_text(expected, encoding='utf-8')
+
+ # Removed packages should trigger a failure.
+ self.assertEqual(-1, python_packages.diff(expected_file))
stderr_mock.assert_any_call('Removed packages')
+ stderr_mock.assert_any_call(' foo==1.0')
+ stderr_mock.assert_any_call(' bar==2.0')
stderr_mock.assert_any_call(' baz==3.0')
- @mock.patch('pw_env_setup.python_packages.subprocess.run',
- side_effect=_subprocess_run_stdout())
@mock.patch('pw_env_setup.python_packages._stderr')
- def test_diff_updated(self, stderr_mock, unused_mock):
- expected = io.StringIO('foo==1.0\nbar==1.9\n')
- expected.name = 'test.name'
- self.assertTrue(python_packages.diff(expected))
+ def test_diff_updated(self, stderr_mock):
+ expected = 'toml>=0.0.1\n' + self.existing_pkgs_minus_toml
+ expected_file = self.temp_path / 'test_diff_updated_expected'
+ expected_file.write_text(expected, encoding='utf-8')
+
+ toml_version = importlib.metadata.version('toml')
+
+ # Updated packages should trigger a failure.
+ self.assertEqual(-1, python_packages.diff(expected_file))
stderr_mock.assert_any_call('Updated packages')
- stderr_mock.assert_any_call(' bar==2.0 (from 1.9)')
- stderr_mock.assert_any_call("Package versions don't match!")
+ stderr_mock.assert_any_call(
+ f' toml=={toml_version} (from toml>=0.0.1)'
+ )
- @mock.patch('pw_env_setup.python_packages.subprocess.run',
- side_effect=_subprocess_run_stdout())
@mock.patch('pw_env_setup.python_packages._stderr')
- def test_diff_new(self, stderr_mock, unused_mock):
- expected = io.StringIO('foo==1.0\n')
- expected.name = 'test.name'
- self.assertTrue(python_packages.diff(expected))
+ def test_diff_new(self, stderr_mock):
+ expected = self.existing_pkgs_minus_toml
+ expected_file = self.temp_path / 'test_diff_new_expected'
+ expected_file.write_text(expected, encoding='utf-8')
+
+ toml_version = importlib.metadata.version('toml')
+
+ # New packages should trigger a failure.
+ self.assertEqual(-1, python_packages.diff(expected_file))
stderr_mock.assert_any_call('New packages')
- stderr_mock.assert_any_call(' bar==2.0')
+ stderr_mock.assert_any_call(f' toml=={toml_version}')
stderr_mock.assert_any_call("Package versions don't match!")
diff --git a/pw_env_setup/pypi_common_setup.cfg b/pw_env_setup/pypi_common_setup.cfg
index f4ad8fbda..e9a410e1f 100644
--- a/pw_env_setup/pypi_common_setup.cfg
+++ b/pw_env_setup/pypi_common_setup.cfg
@@ -13,7 +13,7 @@
# the License.
[metadata]
name = pigweed
-version = 0.0.7
+version = 0.0.13
author = Pigweed Authors
author_email = pigweed-developers@googlegroups.com
description = Pigweed Python modules
@@ -21,9 +21,12 @@ long_description = file: README.md
long_description_content_type = text/markdown
url = https://pigweed.dev
project_urls =
- Bug Tracker = https://bugs.chromium.org/p/pigweed/issues/list
- Code Search = https://cs.opensource.google/pigweed/pigweed
- Gerrit = https://pigweed-review.googlesource.com
+ Documentation = https://pigweed.dev
+ Source Code = https://cs.pigweed.dev/pigweed
+ Code Reviews = https://pigweed-review.googlesource.com
+ Mailing List = https://groups.google.com/g/pigweed
+ Issue Tracker = https://issues.pigweed.dev/issues?q=status:open
+ Chat = https://discord.gg/M9NSeTA
classifiers =
License :: OSI Approved :: Apache Software License
Operating System :: OS Independent
diff --git a/pw_env_setup/util.sh b/pw_env_setup/util.sh
index d0aa2e787..ffcc0c308 100644
--- a/pw_env_setup/util.sh
+++ b/pw_env_setup/util.sh
@@ -67,6 +67,14 @@ pw_bold_white() {
echo -e "\033[1;37m$*\033[0m"
}
+pw_error() {
+ echo -e "\033[1;31m$*\033[0m" >& /dev/stderr
+}
+
+pw_error_info() {
+ echo -e "\033[0;31m$*\033[0m" >& /dev/stderr
+}
+
pw_eval_sourced() {
if [ "$1" -eq 0 ]; then
# TODO(pwbug/354) Remove conditional after all downstream projects have
@@ -76,14 +84,15 @@ pw_eval_sourced() {
else
_PW_NAME=$(basename "$_BOOTSTRAP_PATH" .sh)
fi
- pw_bold_red "Error: Attempting to $_PW_NAME in a subshell"
- pw_red " Since $_PW_NAME.sh modifies your shell's environment variables,"
- pw_red " it must be sourced rather than executed. In particular, "
- pw_red " 'bash $_PW_NAME.sh' will not work since the modified "
- pw_red " environment will get destroyed at the end of the script. "
- pw_red " Instead, source the script's contents in your shell:"
- pw_red ""
- pw_red " \$ source $_PW_NAME.sh"
+ pw_error "Error: Attempting to $_PW_NAME in a subshell"
+ pw_error_info " Since $_PW_NAME.sh modifies your shell's environment"
+ pw_error_info " variables, it must be sourced rather than executed. In"
+ pw_error_info " particular, 'bash $_PW_NAME.sh' will not work since the "
+ pw_error_info " modified environment will get destroyed at the end of the"
+ pw_error_info " script. Instead, source the script's contents in your"
+ pw_error_info " shell:"
+ pw_error_info ""
+ pw_error_info " \$ source $_PW_NAME.sh"
exit 1
fi
}
@@ -91,29 +100,59 @@ pw_eval_sourced() {
pw_check_root() {
_PW_ROOT="$1"
if [[ "$_PW_ROOT" = *" "* ]]; then
- pw_bold_red "Error: The Pigweed path contains spaces\n"
- pw_red " The path '$_PW_ROOT' contains spaces. "
- pw_red " Pigweed's Python environment currently requires Pigweed to be "
- pw_red " at a path without spaces. Please checkout Pigweed in a "
- pw_red " directory without spaces and retry running bootstrap."
- return
+ pw_error "Error: The Pigweed path contains spaces\n"
+ pw_error_info " The path '$_PW_ROOT' contains spaces. "
+ pw_error_info " Pigweed's Python environment currently requires Pigweed to"
+ pw_error_info " be at a path without spaces. Please checkout Pigweed in a"
+ pw_error_info " directory without spaces and retry running bootstrap."
+ return -1
fi
}
pw_get_env_root() {
- # PW_ENVIRONMENT_ROOT allows developers to specify where the environment
- # should be installed. bootstrap.sh scripts should not use that variable to
- # store the result of this function. This separation allows scripts to assume
- # PW_ENVIRONMENT_ROOT came from the developer and not from a previous
- # bootstrap possibly from another workspace.
- if [ -z "$PW_ENVIRONMENT_ROOT" ]; then
- if [ -n "$PW_PROJECT_ROOT" ]; then
- echo "$PW_PROJECT_ROOT/.environment"
- else
- echo "$PW_ROOT/.environment"
+ # PW_ENVIRONMENT_ROOT allows callers to specify where the environment should
+ # be installed. bootstrap.sh scripts should not use that variable to store the
+ # result of this function. This separation allows scripts to assume
+ # PW_ENVIRONMENT_ROOT came from the caller and not from a previous bootstrap
+ # possibly from another workspace. PW_ENVIRONMENT_ROOT will be cleared after
+ # environment setup completes.
+ if [ -n "$PW_ENVIRONMENT_ROOT" ]; then
+ echo "$PW_ENVIRONMENT_ROOT"
+ return
+ fi
+
+ # Determine project-level root directory.
+ if [ -n "$PW_PROJECT_ROOT" ]; then
+ _PW_ENV_PREFIX="$PW_PROJECT_ROOT"
+ else
+ _PW_ENV_PREFIX="$PW_ROOT"
+ fi
+
+ # If <root>/environment exists, use it. Otherwise, if <root>/.environment
+ # exists, use it. Finally, use <root>/environment.
+ _PW_DOTENV="$_PW_ENV_PREFIX/.environment"
+ _PW_ENV="$_PW_ENV_PREFIX/environment"
+
+ if [ -d "$_PW_DOTENV" ]; then
+ if [ -d "$_PW_ENV" ]; then
+ pw_error "Error: both possible environment directories exist."
+ pw_error_info " $_PW_DOTENV"
+ pw_error_info " $_PW_ENV"
+ pw_error_info " If only one of these folders exists it will be used for"
+ pw_error_info " the Pigweed environment. If neither exists"
+ pw_error_info " '<...>/environment' will be used. Since both exist,"
+ pw_error_info " bootstrap doesn't know which to use. Please delete one"
+ pw_error_info " or both and rerun bootstrap."
+ exit 1
fi
+ fi
+
+ if [ -d "$_PW_ENV" ]; then
+ echo "$_PW_ENV"
+ elif [ -d "$_PW_DOTENV" ]; then
+ echo "$_PW_DOTENV"
else
- echo "$PW_ENVIRONMENT_ROOT"
+ echo "$_PW_ENV"
fi
}
@@ -209,14 +248,15 @@ pw_bootstrap() {
local _pw_alias_check=0
alias python > /dev/null 2> /dev/null || _pw_alias_check=$?
if [ "$_pw_alias_check" -eq 0 ]; then
- pw_bold_red "Error: 'python' is an alias"
- pw_red "The shell has a 'python' alias set. This causes many obscure"
- pw_red "Python-related issues both in and out of Pigweed. Please remove"
- pw_red "the Python alias from your shell init file or at least run the"
- pw_red "following command before bootstrapping Pigweed."
- pw_red
- pw_red " unalias python"
- pw_red
+ pw_error "Error: 'python' is an alias"
+ pw_error_info "The shell has a 'python' alias set. This causes many obscure"
+ pw_error_info "Python-related issues both in and out of Pigweed. Please"
+ pw_error_info "remove the Python alias from your shell init file or at"
+ pw_error_info "least run the following command before bootstrapping"
+ pw_error_info "Pigweed."
+ pw_error_info
+ pw_error_info " unalias python"
+ pw_error_info
return
fi
@@ -230,10 +270,10 @@ pw_bootstrap() {
elif command -v python > /dev/null 2> /dev/null; then
_PW_PYTHON=python
else
- pw_bold_red "Error: No system Python present\n"
- pw_red " Pigweed's bootstrap process requires a local system Python."
- pw_red " Please install Python on your system, add it to your PATH"
- pw_red " and re-try running bootstrap."
+ pw_error "Error: No system Python present\n"
+ pw_error_info " Pigweed's bootstrap process requires a local system"
+ pw_error_info " Python. Please install Python on your system, add it to "
+ pw_error_info " your PATH and re-try running bootstrap."
return
fi
@@ -245,6 +285,11 @@ pw_bootstrap() {
_PW_ENV_SETUP_STATUS="$?"
fi
+ # Write the directory path at bootstrap time into the directory. This helps
+ # us double-check things are still in the same space when calling activate.
+ _PW_ENV_ROOT_TXT="$_PW_ACTUAL_ENVIRONMENT_ROOT/env_root.txt"
+ echo "$_PW_ACTUAL_ENVIRONMENT_ROOT" > "$_PW_ENV_ROOT_TXT"
+
# Create the environment README file. Use quotes to prevent alias expansion.
"cp" "$PW_ROOT/pw_env_setup/destination.md" "$_PW_ACTUAL_ENVIRONMENT_ROOT/README.md"
}
@@ -258,6 +303,28 @@ pw_finalize() {
_PW_NAME="$1"
_PW_SETUP_SH="$2"
+ # Check that the environment directory agrees that the path it's at matches
+ # where it thinks it should be. If not, bail.
+ _PW_ENV_ROOT_TXT="$_PW_ACTUAL_ENVIRONMENT_ROOT/env_root.txt"
+ if [ -f "$_PW_ENV_ROOT_TXT" ]; then
+ _PW_PREV_ENV_ROOT="$(cat $_PW_ENV_ROOT_TXT)"
+ if [ "$_PW_ACTUAL_ENVIRONMENT_ROOT" != "$_PW_PREV_ENV_ROOT" ]; then
+ pw_error "Error: Environment directory moved"
+ pw_error_info "This Pigweed environment was created at"
+ pw_error_info
+ pw_error_info " $_PW_PREV_ENV_ROOT"
+ pw_error_info
+ pw_error_info "But it is now being activated from"
+ pw_error_info
+ pw_error_info " $_PW_ACTUAL_ENVIRONMENT_ROOT"
+ pw_error_info
+ pw_error_info "This is likely because the checkout moved. After moving "
+ pw_error_info "the checkout a full '. ./bootstrap.sh' is required."
+ pw_error_info
+ _PW_ENV_SETUP_STATUS=1
+ fi
+ fi
+
if [ "$_PW_ENV_SETUP_STATUS" -ne 0 ]; then
return
fi
@@ -278,24 +345,34 @@ pw_finalize() {
echo
fi
else
- pw_red "Error during $_PW_NAME--see messages above."
+ pw_error "Error during $_PW_NAME--see messages above."
fi
else
- pw_red "Error during $_PW_NAME--see messages above."
+ pw_error "Error during $_PW_NAME--see messages above."
fi
}
+pw_install_post_checkout_hook() {
+ cp "$PW_ROOT/pw_env_setup/post-checkout-hook.sh" "$PW_PROJECT_ROOT/.git/hooks/post-checkout"
+}
+
pw_cleanup() {
unset _PW_BANNER
unset _PW_BANNER_FUNC
unset PW_BANNER_FUNC
unset _PW_ENV_SETUP
unset _PW_NAME
+ unset PW_ENVIRONMENT_ROOT
unset _PW_PYTHON
+ unset _PW_ENV_ROOT_TXT
+ unset _PW_PREV_ENV_ROOT
unset _PW_SETUP_SH
unset _PW_DEACTIVATE_SH
unset _NEW_PW_ROOT
unset _PW_ENV_SETUP_STATUS
+ unset _PW_ENV_PREFIX
+ unset _PW_ENV
+ unset _PW_DOTENV
unset -f pw_none
unset -f pw_red
@@ -315,6 +392,9 @@ pw_cleanup() {
unset -f pw_bootstrap
unset -f pw_activate
unset -f pw_finalize
+ unset -f pw_install_post_checkout_hook
unset -f pw_cleanup
unset -f _pw_hello
+ unset -f pw_error
+ unset -f pw_error_info
}
diff --git a/pw_file/BUILD.bazel b/pw_file/BUILD.bazel
index 7eda65c43..53f83ebc4 100644
--- a/pw_file/BUILD.bazel
+++ b/pw_file/BUILD.bazel
@@ -17,14 +17,34 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
proto_library(
- name = "proto",
+ name = "file_proto",
srcs = ["file.proto"],
+ deps = [
+ "//pw_protobuf:common_proto",
+ ],
+)
+
+pw_proto_library(
+ name = "file_cc_proto",
+ deps = [
+ ":file_proto",
+ "//pw_protobuf:common_proto",
+ ],
+)
+
+py_proto_library(
+ name = "file_proto_py_pb2",
+ srcs = ["file.proto"],
+ # TODO(b/241456982): Get this target to build.
+ tags = ["manual"],
)
pw_cc_library(
@@ -35,11 +55,13 @@ pw_cc_library(
hdrs = [
"public/pw_file/flat_file_system.h",
],
+ includes = ["public"],
deps = [
- ":proto",
+ ":file_cc_proto.pwpb",
+ ":file_cc_proto.raw_rpc",
"//pw_bytes",
"//pw_result",
- "//pw_rpc/raw:method",
+ "//pw_rpc/raw:server_api",
"//pw_status",
],
)
@@ -50,8 +72,8 @@ pw_cc_test(
"flat_file_system_test.cc",
],
deps = [
+ ":file_cc_proto.pwpb",
":flat_file_system",
- ":proto",
"//pw_bytes",
"//pw_protobuf",
"//pw_rpc/raw:test_method_context",
diff --git a/pw_file/BUILD.gn b/pw_file/BUILD.gn
index e36b1829b..f9ee8a64f 100644
--- a/pw_file/BUILD.gn
+++ b/pw_file/BUILD.gn
@@ -36,6 +36,7 @@ pw_source_set("flat_file_system") {
dir_pw_bytes,
dir_pw_log,
dir_pw_result,
+ dir_pw_span,
dir_pw_status,
]
public_configs = [ ":public_includes" ]
diff --git a/pw_file/CMakeLists.txt b/pw_file/CMakeLists.txt
index 646931a84..9bb56d1ea 100644
--- a/pw_file/CMakeLists.txt
+++ b/pw_file/CMakeLists.txt
@@ -15,7 +15,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
-pw_add_module_library(pw_file.flat_file_system
+pw_add_library(pw_file.flat_file_system INTERFACE
PUBLIC_DEPS
pw_file.proto.pwpb
pw_file.proto.raw_rpc
@@ -24,7 +24,7 @@ pw_add_module_library(pw_file.flat_file_system
pw_log
pw_result
pw_status
- TEST_DEPS
+ PRIVATE_DEPS
pw_rpc.test_utils
)
@@ -32,7 +32,7 @@ pw_proto_library(pw_file.proto
SOURCES
file.proto
DEPS
- pw_protobuf.common_protos
+ pw_protobuf.common_proto
PREFIX
pw_file
)
diff --git a/pw_file/flat_file_system.cc b/pw_file/flat_file_system.cc
index 48aaf6952..9960bd348 100644
--- a/pw_file/flat_file_system.cc
+++ b/pw_file/flat_file_system.cc
@@ -18,7 +18,6 @@
#include <cstddef>
#include <cstdint>
-#include <span>
#include <string_view>
#include "pw_assert/check.h"
@@ -29,6 +28,7 @@
#include "pw_protobuf/encoder.h"
#include "pw_protobuf/serialized_size.h"
#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -37,13 +37,13 @@ namespace pw::file {
using Entry = FlatFileSystemService::Entry;
Status FlatFileSystemService::EnumerateFile(
- Entry& entry, pw::file::ListResponse::StreamEncoder& output_encoder) {
+ Entry& entry, pwpb::ListResponse::StreamEncoder& output_encoder) {
StatusWithSize sws = entry.Name(file_name_buffer_);
if (!sws.ok()) {
return sws.status();
}
{
- pw::file::Path::StreamEncoder encoder = output_encoder.GetPathsEncoder();
+ pwpb::Path::StreamEncoder encoder = output_encoder.GetPathsEncoder();
encoder
.WritePath(reinterpret_cast<const char*>(file_name_buffer_.data()),
@@ -60,7 +60,7 @@ void FlatFileSystemService::EnumerateAllFiles(RawServerWriter& writer) {
for (Entry* entry : entries_) {
PW_DCHECK_NOTNULL(entry);
// For now, don't try to pack entries.
- pw::file::ListResponse::MemoryEncoder encoder(encoding_buffer_);
+ pwpb::ListResponse::MemoryEncoder encoder(encoding_buffer_);
if (Status status = EnumerateFile(*entry, encoder); !status.ok()) {
if (status != Status::NotFound()) {
PW_LOG_ERROR("Failed to enumerate file (id: %u) with status %d",
@@ -73,12 +73,12 @@ void FlatFileSystemService::EnumerateAllFiles(RawServerWriter& writer) {
Status write_status = writer.Write(encoder);
if (!write_status.ok()) {
writer.Finish(write_status)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return;
}
}
writer.Finish(OkStatus())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
void FlatFileSystemService::List(ConstByteSpan request,
@@ -87,7 +87,7 @@ void FlatFileSystemService::List(ConstByteSpan request,
// If a file name was provided, try and find and enumerate the file.
while (decoder.Next().ok()) {
if (decoder.FieldNumber() !=
- static_cast<uint32_t>(pw::file::ListRequest::Fields::PATH)) {
+ static_cast<uint32_t>(pwpb::ListRequest::Fields::kPath)) {
continue;
}
@@ -95,7 +95,7 @@ void FlatFileSystemService::List(ConstByteSpan request,
if (!decoder.ReadString(&file_name_view).ok() ||
file_name_view.length() == 0) {
writer.Finish(Status::DataLoss())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return;
}
@@ -103,20 +103,20 @@ void FlatFileSystemService::List(ConstByteSpan request,
Result<Entry*> result = FindFile(file_name_view);
if (!result.ok()) {
writer.Finish(result.status())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return;
}
- pw::file::ListResponse::MemoryEncoder encoder(encoding_buffer_);
+ pwpb::ListResponse::MemoryEncoder encoder(encoding_buffer_);
Status proto_encode_status = EnumerateFile(*result.value(), encoder);
if (!proto_encode_status.ok()) {
writer.Finish(proto_encode_status)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return;
}
writer.Finish(writer.Write(encoder))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return;
}
@@ -129,7 +129,7 @@ void FlatFileSystemService::Delete(ConstByteSpan request,
protobuf::Decoder decoder(request);
while (decoder.Next().ok()) {
if (decoder.FieldNumber() !=
- static_cast<uint32_t>(pw::file::DeleteRequest::Fields::PATH)) {
+ static_cast<uint32_t>(pwpb::DeleteRequest::Fields::kPath)) {
continue;
}
diff --git a/pw_file/flat_file_system_test.cc b/pw_file/flat_file_system_test.cc
index 204391ca4..722861921 100644
--- a/pw_file/flat_file_system_test.cc
+++ b/pw_file/flat_file_system_test.cc
@@ -17,7 +17,6 @@
#include <array>
#include <cstddef>
#include <cstdint>
-#include <span>
#include <string_view>
#include "gtest/gtest.h"
@@ -25,6 +24,7 @@
#include "pw_file/file.pwpb.h"
#include "pw_protobuf/decoder.h"
#include "pw_rpc/raw/test_method_context.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -36,7 +36,7 @@ class FakeFile : public FlatFileSystemService::Entry {
constexpr FakeFile(std::string_view file_name, size_t size, uint32_t file_id)
: name_(file_name), size_(size), file_id_(file_id) {}
- StatusWithSize Name(std::span<char> dest) override {
+ StatusWithSize Name(span<char> dest) override {
if (name_.empty()) {
return StatusWithSize(Status::NotFound(), 0);
}
@@ -84,7 +84,7 @@ void ComparePathToEntry(ConstByteSpan serialized_path,
protobuf::Decoder decoder(serialized_path);
while (decoder.Next().ok()) {
switch (decoder.FieldNumber()) {
- case static_cast<uint32_t>(pw::file::Path::Fields::PATH): {
+ case static_cast<uint32_t>(pw::file::pwpb::Path::Fields::kPath): {
std::string_view serialized_name;
EXPECT_EQ(OkStatus(), decoder.ReadString(&serialized_name));
size_t name_bytes_to_read =
@@ -96,7 +96,7 @@ void ComparePathToEntry(ConstByteSpan serialized_path,
break;
}
- case static_cast<uint32_t>(pw::file::Path::Fields::PERMISSIONS): {
+ case static_cast<uint32_t>(pw::file::pwpb::Path::Fields::kPermissions): {
uint32_t seralized_permissions;
EXPECT_EQ(OkStatus(), decoder.ReadUint32(&seralized_permissions));
EXPECT_EQ(static_cast<uint32_t>(entry->Permissions()),
@@ -104,7 +104,7 @@ void ComparePathToEntry(ConstByteSpan serialized_path,
break;
}
- case static_cast<uint32_t>(pw::file::Path::Fields::SIZE_BYTES): {
+ case static_cast<uint32_t>(pw::file::pwpb::Path::Fields::kSizeBytes): {
uint32_t serialized_file_size;
EXPECT_EQ(OkStatus(), decoder.ReadUint32(&serialized_file_size));
EXPECT_EQ(static_cast<uint32_t>(entry->SizeBytes()),
@@ -112,7 +112,7 @@ void ComparePathToEntry(ConstByteSpan serialized_path,
break;
}
- case static_cast<uint32_t>(pw::file::Path::Fields::FILE_ID): {
+ case static_cast<uint32_t>(pw::file::pwpb::Path::Fields::kFileId): {
uint32_t serialized_file_id;
EXPECT_EQ(OkStatus(), decoder.ReadUint32(&serialized_file_id));
EXPECT_EQ(static_cast<uint32_t>(entry->FileId()), serialized_file_id);
@@ -121,14 +121,14 @@ void ComparePathToEntry(ConstByteSpan serialized_path,
default:
// unexpected result.
- // TODO something here.
+ // TODO(amontanez) something here.
break;
}
}
}
size_t ValidateExpectedPaths(
- std::span<FlatFileSystemService::Entry*> flat_file_system,
+ span<FlatFileSystemService::Entry*> flat_file_system,
const rpc::PayloadsView& results) {
size_t serialized_path_entry_count = 0;
size_t file_system_index = 0;
@@ -136,7 +136,7 @@ size_t ValidateExpectedPaths(
protobuf::Decoder decoder(response);
while (decoder.Next().ok()) {
constexpr uint32_t kListResponsePathsFieldNumber =
- static_cast<uint32_t>(pw::file::ListResponse::Fields::PATHS);
+ static_cast<uint32_t>(pw::file::pwpb::ListResponse::Fields::kPaths);
EXPECT_EQ(decoder.FieldNumber(), kListResponsePathsFieldNumber);
if (decoder.FieldNumber() != kListResponsePathsFieldNumber) {
return 0;
@@ -172,7 +172,7 @@ TEST(FlatFileSystem, EncodingBufferSizeBytes) {
TEST(FlatFileSystem, List_NoFiles) {
PW_RAW_TEST_METHOD_CONTEXT(FlatFileSystemServiceWithBuffer<1>, List)
- ctx{std::span<FlatFileSystemService::Entry*>()};
+ ctx{span<FlatFileSystemService::Entry*>()};
ctx.call(ConstByteSpan());
EXPECT_TRUE(ctx.done());
diff --git a/pw_file/public/pw_file/flat_file_system.h b/pw_file/public/pw_file/flat_file_system.h
index 1035dbdba..d501e448a 100644
--- a/pw_file/public/pw_file/flat_file_system.h
+++ b/pw_file/public/pw_file/flat_file_system.h
@@ -15,7 +15,6 @@
#include <cstddef>
#include <cstdint>
-#include <span>
#include <string_view>
#include "pw_bytes/span.h"
@@ -24,6 +23,7 @@
#include "pw_protobuf/serialized_size.h"
#include "pw_result/result.h"
#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -38,7 +38,7 @@ class FlatFileSystemService
public:
class Entry {
public:
- using FilePermissions = pw::file::Path::Permissions;
+ using FilePermissions = ::pw::file::pwpb::Path::Permissions;
using Id = uint32_t;
Entry() = default;
@@ -57,7 +57,7 @@ class FlatFileSystemService
// OK - Successfully read file name to `dest`.
// NOT_FOUND - No file to enumerate for this entry.
// RESOURCE_EXHAUSTED - `dest` buffer too small to fit the full file name.
- virtual StatusWithSize Name(std::span<char> dest) = 0;
+ virtual StatusWithSize Name(span<char> dest) = 0;
virtual size_t SizeBytes() = 0;
virtual FilePermissions Permissions() const = 0;
@@ -79,7 +79,7 @@ class FlatFileSystemService
size_t minimum_entries = 1) {
return minimum_entries *
protobuf::SizeOfDelimitedField(
- ListResponse::Fields::PATHS,
+ pwpb::ListResponse::Fields::kPaths,
EncodedPathProtoSizeBytes(max_file_name_length));
}
@@ -94,9 +94,9 @@ class FlatFileSystemService
// files. Should be large enough to hold the longest expected file name.
// The span's underlying buffer must outlive this object.
// max_file_name_length - Number of bytes to reserve for the file name.
- constexpr FlatFileSystemService(std::span<Entry*> entry_list,
- std::span<std::byte> encoding_buffer,
- std::span<char> file_name_buffer)
+ constexpr FlatFileSystemService(span<Entry*> entry_list,
+ span<std::byte> encoding_buffer,
+ span<char> file_name_buffer)
: encoding_buffer_(encoding_buffer),
file_name_buffer_(file_name_buffer),
entries_(entry_list) {}
@@ -113,24 +113,24 @@ class FlatFileSystemService
// Returns the maximum size of a single encoded Path proto.
static constexpr size_t EncodedPathProtoSizeBytes(
size_t max_file_name_length) {
- return protobuf::SizeOfFieldString(Path::Fields::PATH,
+ return protobuf::SizeOfFieldString(pwpb::Path::Fields::kPath,
max_file_name_length) +
- protobuf::SizeOfFieldEnum(Path::Fields::PERMISSIONS,
- Path::Permissions::READ_AND_WRITE) +
- protobuf::SizeOfFieldUint32(Path::Fields::SIZE_BYTES) +
- protobuf::SizeOfFieldUint32(Path::Fields::FILE_ID);
+ protobuf::SizeOfFieldEnum(pwpb::Path::Fields::kPermissions,
+ pwpb::Path::Permissions::READ_AND_WRITE) +
+ protobuf::SizeOfFieldUint32(pwpb::Path::Fields::kSizeBytes) +
+ protobuf::SizeOfFieldUint32(pwpb::Path::Fields::kFileId);
}
Result<Entry*> FindFile(std::string_view file_name);
Status FindAndDeleteFile(std::string_view file_name);
Status EnumerateFile(Entry& entry,
- pw::file::ListResponse::StreamEncoder& output_encoder);
+ pwpb::ListResponse::StreamEncoder& output_encoder);
void EnumerateAllFiles(RawServerWriter& writer);
- const std::span<std::byte> encoding_buffer_;
- const std::span<char> file_name_buffer_;
- const std::span<Entry*> entries_;
+ const span<std::byte> encoding_buffer_;
+ const span<char> file_name_buffer_;
+ const span<Entry*> entries_;
};
// Provides the encoding and file name buffers to a FlatFileSystemService.
@@ -138,7 +138,7 @@ template <unsigned kMaxFileNameLength,
unsigned kMinGuaranteedEntriesPerResponse = 1>
class FlatFileSystemServiceWithBuffer : public FlatFileSystemService {
public:
- constexpr FlatFileSystemServiceWithBuffer(std::span<Entry*> entry_list)
+ constexpr FlatFileSystemServiceWithBuffer(span<Entry*> entry_list)
: FlatFileSystemService(entry_list, encoding_buffer_, file_name_buffer_) {
}
diff --git a/pw_function/Android.bp b/pw_function/Android.bp
new file mode 100644
index 000000000..96cef6ae3
--- /dev/null
+++ b/pw_function/Android.bp
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_function_headers",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: [
+ "public",
+ ],
+ host_supported: true,
+}
diff --git a/pw_function/BUILD.bazel b/pw_function/BUILD.bazel
index 273ae58ba..e130d1c87 100644
--- a/pw_function/BUILD.bazel
+++ b/pw_function/BUILD.bazel
@@ -30,18 +30,52 @@ pw_cc_library(
pw_cc_library(
name = "pw_function",
- srcs = ["public/pw_function/internal/function.h"],
hdrs = ["public/pw_function/function.h"],
includes = ["public"],
deps = [
":config",
"//pw_assert",
"//pw_preprocessor",
+ "//third_party/fuchsia:fit",
],
)
pw_cc_test(
name = "function_test",
srcs = ["function_test.cc"],
- deps = [":pw_function"],
+ deps = [
+ ":pw_function",
+ "//pw_compilation_testing:negative_compilation_testing",
+ ],
+)
+
+pw_cc_library(
+ name = "pointer",
+ srcs = ["public/pw_function/internal/static_invoker.h"],
+ hdrs = ["public/pw_function/pointer.h"],
+ includes = ["public"],
+)
+
+pw_cc_test(
+ name = "pointer_test",
+ srcs = ["pointer_test.cc"],
+ deps = [
+ ":pointer",
+ ":pw_function",
+ ],
+)
+
+pw_cc_library(
+ name = "scope_guard",
+ hdrs = ["public/pw_function/scope_guard.h"],
+ includes = ["public"],
+)
+
+pw_cc_test(
+ name = "scope_guard_test",
+ srcs = ["scope_guard_test.cc"],
+ deps = [
+ ":pw_function",
+ ":scope_guard",
+ ],
)
diff --git a/pw_function/BUILD.gn b/pw_function/BUILD.gn
index 17befd136..efea1cc8d 100644
--- a/pw_function/BUILD.gn
+++ b/pw_function/BUILD.gn
@@ -43,11 +43,22 @@ pw_source_set("pw_function") {
public_configs = [ ":public_include_path" ]
public_deps = [
":config",
+ "$dir_pw_third_party/fuchsia:fit",
dir_pw_assert,
dir_pw_preprocessor,
]
public = [ "public/pw_function/function.h" ]
- sources = [ "public/pw_function/internal/function.h" ]
+}
+
+pw_source_set("pointer") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_function/pointer.h" ]
+ sources = [ "public/pw_function/internal/static_invoker.h" ]
+}
+
+pw_source_set("scope_guard") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_function/scope_guard.h" ]
}
pw_doc_group("docs") {
@@ -59,7 +70,12 @@ pw_doc_group("docs") {
}
pw_test_group("tests") {
- tests = [ ":function_test" ]
+ tests = [
+ ":function_test",
+ ":pointer_test",
+ ":scope_guard_test",
+ "$dir_pw_third_party/fuchsia:function_tests",
+ ]
}
pw_test("function_test") {
@@ -68,9 +84,26 @@ pw_test("function_test") {
dir_pw_polyfill,
]
sources = [ "function_test.cc" ]
+ negative_compilation_tests = true
+}
+
+pw_test("pointer_test") {
+ deps = [
+ ":pointer",
+ ":pw_function",
+ ]
+ sources = [ "pointer_test.cc" ]
+}
+
+pw_test("scope_guard_test") {
+ sources = [ "scope_guard_test.cc" ]
+ deps = [
+ ":pw_function",
+ ":scope_guard",
+ ]
}
-pw_size_report("function_size") {
+pw_size_diff("function_size") {
title = "Pigweed function size report"
binaries = [
@@ -82,7 +115,7 @@ pw_size_report("function_size") {
]
}
-pw_size_report("callable_size") {
+pw_size_diff("callable_size") {
title = "Size comparison of callable objects"
binaries = [
diff --git a/pw_function/CMakeLists.txt b/pw_function/CMakeLists.txt
index 0dc579eec..8b32db534 100644
--- a/pw_function/CMakeLists.txt
+++ b/pw_function/CMakeLists.txt
@@ -16,7 +16,7 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_module_config(pw_function_CONFIG)
-pw_add_module_library(pw_function.config
+pw_add_library(pw_function.config INTERFACE
HEADERS
public/pw_function/config.h
PUBLIC_INCLUDES
@@ -25,16 +25,16 @@ pw_add_module_library(pw_function.config
${pw_function_CONFIG}
)
-pw_add_module_library(pw_function
+pw_add_library(pw_function INTERFACE
HEADERS
public/pw_function/function.h
- public/pw_function/internal/function.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
pw_assert
pw_function.config
pw_preprocessor
+ pw_third_party.fuchsia.fit
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_FUNCTION)
zephyr_link_libraries(pw_function)
@@ -43,10 +43,48 @@ endif()
pw_add_test(pw_function.function_test
SOURCES
function_test.cc
- DEPS
+ PRIVATE_DEPS
+ pw_compilation_testing._pigweed_only_negative_compilation
pw_function
pw_polyfill
GROUPS
modules
pw_function
)
+
+pw_add_library(pw_function.pointer INTERFACE
+ HEADERS
+ public/pw_function/pointer.h
+ public/pw_function/internal/static_invoker.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_test(pw_function.pointer_test
+ SOURCES
+ pointer_test.cc
+ PRIVATE_DEPS
+ pw_function
+ pw_function.pointer
+ GROUPS
+ modules
+ pw_function
+)
+
+pw_add_library(pw_function.scope_guard INTERFACE
+ HEADERS
+ public/pw_function/scope_guard.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_test(pw_function.scope_guard_test
+ SOURCES
+ scope_guard_test.cc
+ PRIVATE_DEPS
+ pw_function
+ pw_function.scope_guard
+ GROUPS
+ modules
+ pw_function
+)
diff --git a/pw_function/docs.rst b/pw_function/docs.rst
index 14ce90608..18041ef51 100644
--- a/pw_function/docs.rst
+++ b/pw_function/docs.rst
@@ -1,20 +1,20 @@
.. _module-pw_function:
------------
+===========
pw_function
------------
-The function module provides a standard, general-purpose API for wrapping
-callable objects.
-
-.. note::
- This module is under construction and its API is not complete.
+===========
+The ``pw_function`` module provides a standard, general-purpose API for
+wrapping callable objects. ``pw_function`` is similar in spirit and API to
+``std::function``, but doesn't allocate, and uses several tricks to prevent
+code bloat.
+--------
Overview
-========
+--------
Basic usage
------------
-``pw_function`` defines the ``pw::Function`` class. A ``Function`` is a
+===========
+``pw_function`` defines the :cpp:type:`pw::Function` class. A ``Function`` is a
move-only callable wrapper constructable from any callable object. Functions
are templated on the signature of the callable they store.
@@ -40,7 +40,7 @@ Functions are nullable. Invoking a null function triggers a runtime assert.
.. code-block:: c++
- // A function intialized without a callable is implicitly null.
+ // A function initialized without a callable is implicitly null.
pw::Function<void()> null_function;
// Null functions may also be explicitly created or set.
@@ -54,9 +54,9 @@ Functions are nullable. Invoking a null function triggers a runtime assert.
function();
}
-``pw::Function``'s default constructor is ``constexpr``, so default-constructed
-functions may be used in classes with ``constexpr`` constructors and in
-``constinit`` expressions.
+:cpp:type:`pw::Function`'s default constructor is ``constexpr``, so
+default-constructed functions may be used in classes with ``constexpr``
+constructors and in ``constinit`` expressions.
.. code-block:: c++
@@ -72,14 +72,19 @@ functions may be used in classes with ``constexpr`` constructors and in
constinit MyClass instance;
Storage
--------
+=======
By default, a ``Function`` stores its callable inline within the object. The
inline storage size defaults to the size of two pointers, but is configurable
through the build system. The size of a ``Function`` object is equivalent to its
inline storage size.
+The :cpp:type:`pw::InlineFunction` alias is similar to :cpp:type:`pw::Function`,
+but is always inlined. That is, even if dynamic allocation is enabled for
+:cpp:type:`pw::Function`, :cpp:type:`pw::InlineFunction` will fail to compile if
+the callable is larger than the inline storage size.
+
Attempting to construct a function from a callable larger than its inline size
-is a compile-time error.
+is a compile-time error unless dynamic allocation is enabled.
.. admonition:: Inline storage size
@@ -105,32 +110,31 @@ is a compile-time error.
// Compiler error: sizeof(MyCallable) exceeds function's inline storage size.
pw::Function<int(int)> function((MyCallable()));
-..
- For larger callables, a ``Function`` can be constructed with an external buffer
- in which the callable should be stored. The user must ensure that the lifetime
- of the buffer exceeds that of the function object.
-
- .. code-block:: c++
+.. admonition:: Dynamic allocation
- // Initialize a function with an external 16-byte buffer in which to store its
- // callable. The callable will be stored in the buffer regardless of whether
- // it fits inline.
- pw::FunctionStorage<16> storage;
- pw::Function<int()> get_random_number([]() { return 4; }, storage);
+ When ``PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION`` is enabled, a ``Function``
+ will use dynamic allocation to store callables that exceed the inline size.
+ When it is enabled but a compile-time check for the inlining is still required
+ ``pw::InlineFunction`` can be used.
- .. admonition:: External storage
-
- Functions which use external storage still take up the configured inline
- storage size, which should be accounted for when storing function objects.
-
-In the future, ``pw::Function`` may support dynamic allocation of callable
-storage using the system allocator. This operation will always be explicit.
+.. warning::
+ If ``PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION`` is enabled then attempts to cast
+ from `:cpp:type:`pw::InlineFunction` to a regular :cpp:type:`pw::Function`
+ will **ALWAYS** allocate memory.
+---------
API usage
+---------
+
+Reference
=========
+.. doxygentypedef:: pw::Function
+.. doxygentypedef:: pw::InlineFunction
+.. doxygentypedef:: pw::Callback
+.. doxygentypedef:: pw::InlineCallback
-``pw::Function`` function parameters
-------------------------------------
+``pw::Function`` as a function parameter
+========================================
When implementing an API which takes a callback, a ``Function`` can be used in
place of a function pointer or equivalent callable.
@@ -143,12 +147,13 @@ place of a function pointer or equivalent callable.
// signature template for clarity.
void DoTheThing(int arg, const pw::Function<void(int result)>& callback);
-``pw::Function`` is movable, but not copyable, so APIs must accept
-``pw::Function`` objects either by const reference (``const
+:cpp:type:`pw::Function` is movable, but not copyable, so APIs must accept
+:cpp:type:`pw::Function` objects either by const reference (``const
pw::Function<void()>&``) or rvalue reference (``const pw::Function<void()>&&``).
-If the ``pw::Function`` simply needs to be called, it should be passed by const
-reference. If the ``pw::Function`` needs to be stored, it should be passed as an
-rvalue reference and moved into a ``pw::Function`` variable as appropriate.
+If the :cpp:type:`pw::Function` simply needs to be called, it should be passed
+by const reference. If the :cpp:type:`pw::Function` needs to be stored, it
+should be passed as an rvalue reference and moved into a
+:cpp:type:`pw::Function` variable as appropriate.
.. code-block:: c++
@@ -164,38 +169,40 @@ rvalue reference and moved into a ``pw::Function`` variable as appropriate.
stored_callback_ = std::move(callback);
}
-.. admonition:: Rules of thumb for passing a ``pw::Function`` to a function
+.. admonition:: Rules of thumb for passing a :cpp:type:`pw::Function` to a function
* **Pass by value**: Never.
- This results in unnecessary ``pw::Function`` instances and move operations.
+ This results in unnecessary :cpp:type:`pw::Function` instances and move
+ operations.
* **Pass by const reference** (``const pw::Function&``): When the
- ``pw::Function`` is only invoked.
+ :cpp:type:`pw::Function` is only invoked.
- When a ``pw::Function`` is called or inspected, but not moved, take a const
- reference to avoid copies and support temporaries.
+ When a :cpp:type:`pw::Function` is called or inspected, but not moved, take
+ a const reference to avoid copies and support temporaries.
* **Pass by rvalue reference** (``pw::Function&&``): When the
- ``pw::Function`` is moved.
-
- When the function takes ownership of the ``pw::Function`` object, always
- use an rvalue reference (``pw::Function<void()>&&``) instead of a mutable
- lvalue reference (``pw::Function<void()>&``). An rvalue reference forces
- the caller to ``std::move`` when passing a preexisting ``pw::Function``
- variable, which makes the transfer of ownership explicit. It is possible to
- move-assign from an lvalue reference, but this fails to make it obvious to
- the caller that the object is no longer valid.
+ :cpp:type:`pw::Function` is moved.
+
+ When the function takes ownership of the :cpp:type:`pw::Function` object,
+ always use an rvalue reference (``pw::Function<void()>&&``) instead of a
+ mutable lvalue reference (``pw::Function<void()>&``). An rvalue reference
+ forces the caller to ``std::move`` when passing a preexisting
+ :cpp:type:`pw::Function` variable, which makes the transfer of ownership
+ explicit. It is possible to move-assign from an lvalue reference, but this
+ fails to make it obvious to the caller that the object is no longer valid.
* **Pass by non-const reference** (``pw::Function&``): Rarely, when modifying
a variable.
Non-const references are only necessary when modifying an existing
- ``pw::Function`` variable. Use an rvalue reference instead if the
- ``pw::Function`` is moved into another variable.
+ :cpp:type:`pw::Function` variable. Use an rvalue reference instead if the
+ :cpp:type:`pw::Function` is moved into another variable.
Calling functions that use ``pw::Function``
--------------------------------------------
-A ``pw::Function`` can be implicitly constructed from any callback object. When
-calling an API that takes a ``pw::Function``, simply pass the callable object.
-There is no need to create an intermediate ``pw::Function`` object.
+===========================================
+A :cpp:type:`pw::Function` can be implicitly constructed from any callback
+object. When calling an API that takes a :cpp:type:`pw::Function`, simply pass
+the callable object. There is no need to create an intermediate
+:cpp:type:`pw::Function` object.
.. code-block:: c++
@@ -205,10 +212,10 @@ There is no need to create an intermediate ``pw::Function`` object.
// Implicitly creates a pw::Function from a capturing lambda and stores it.
StoreTheCallback([this](int result) { result_ = result; });
-When working with an existing ``pw::Function`` variable, the variable can be
-passed directly to functions that take a const reference. If the function takes
-ownership of the ``pw::Function``, move the ``pw::Function`` variable at the
-call site.
+When working with an existing :cpp:type:`pw::Function` variable, the variable
+can be passed directly to functions that take a const reference. If the function
+takes ownership of the :cpp:type:`pw::Function`, move the
+:cpp:type:`pw::Function` variable at the call site.
.. code-block:: c++
@@ -218,36 +225,59 @@ call site.
// Takes ownership of the pw::Function.
void StoreTheCallback(std::move(my_function));
+``pw::Callback`` for one-shot functions
+=======================================
+:cpp:type:`pw::Callback` is a specialization of :cpp:type:`pw::Function` that
+can only be called once. After a :cpp:type:`pw::Callback` is called, the target
+function is destroyed. A :cpp:type:`pw::Callback` in the "already called" state
+has the same state as a :cpp:type:`pw::Callback` that has been assigned to
+nullptr.
+
+Invoking ``pw::Function`` from a C-style API
+============================================
+.. doxygenfile:: pw_function/pointer.h
+ :sections: detaileddescription
+
+.. doxygenfunction:: GetFunctionPointer()
+.. doxygenfunction:: GetFunctionPointer(const FunctionType&)
+.. doxygenfunction:: GetFunctionPointerContextFirst()
+.. doxygenfunction:: GetFunctionPointerContextFirst(const FunctionType&)
+
+----------
+ScopeGuard
+----------
+.. doxygenclass:: pw::ScopeGuard
+ :members:
+
+------------
Size reports
-============
+------------
Function class
---------------
-The following size report compares an API using a ``pw::Function`` to a
+==============
+The following size report compares an API using a :cpp:type:`pw::Function` to a
traditional function pointer.
.. include:: function_size
Callable sizes
---------------
+==============
The table below demonstrates typical sizes of various callable types, which can
be used as a reference when sizing external buffers for ``Function`` objects.
.. include:: callable_size
+------
Design
-======
-``pw::Function`` is based largely on
-`fbl::Function <https://cs.opensource.google/fuchsia/fuchsia/+/main:zircon/system/ulib/fbl/include/fbl/function.h>`_
-from Fuchsia with some changes to make it more suitable for embedded
-development.
+------
+:cpp:type:`pw::Function` is an alias of
+`fit::function <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h;drc=f66f54fca0c11a1168d790bcc3d8a5a3d940218d>`_.
-Functions are movable, but not copyable. This allows them to store and manage
-callables without having to perform bookkeeping such as reference counting, and
-avoids any reliance on dynamic memory management. The result is a simpler
-implementation which is easy to conceptualize and use in an embedded context.
+:cpp:type:`pw::Callback` is an alias of
+`fit::callback <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/include/lib/fit/function.h;drc=f66f54fca0c11a1168d790bcc3d8a5a3d940218d>`_.
+------
Zephyr
-======
+------
To enable ``pw_function` for Zephyr add ``CONFIG_PIGWEED_FUNCTION=y`` to the
project's configuration.
diff --git a/pw_function/function_test.cc b/pw_function/function_test.cc
index 8ba01d93e..ee247d879 100644
--- a/pw_function/function_test.cc
+++ b/pw_function/function_test.cc
@@ -15,31 +15,35 @@
#include "pw_function/function.h"
#include "gtest/gtest.h"
+#include "pw_compilation_testing/negative_compilation.h"
#include "pw_polyfill/language_feature_macros.h"
namespace pw {
namespace {
-// TODO(pwbug/47): Convert this to a compilation failure test.
-#if defined(PW_COMPILE_FAIL_TEST_CannotInstantiateWithNonFunction)
+#if PW_NC_TEST(CannotInstantiateWithNonFunction)
+PW_NC_EXPECT("must be instantiated with a function type");
[[maybe_unused]] Function<int> function_pointer;
-#elif defined(PW_COMPILE_FAIL_TEST_CannotInstantiateWithFunctionPointer1)
+#elif PW_NC_TEST(CannotInstantiateWithFunctionPointer1)
+PW_NC_EXPECT("must be instantiated with a function type");
[[maybe_unused]] Function<void (*)()> function_pointer;
-#elif defined(PW_COMPILE_FAIL_TEST_CannotInstantiateWithFunctionPointer2)
+#elif PW_NC_TEST(CannotInstantiateWithFunctionPointer2)
+PW_NC_EXPECT("must be instantiated with a function type");
[[maybe_unused]] void SomeFunction(int);
[[maybe_unused]] Function<decltype(&SomeFunction)> function_pointer;
-#elif defined(PW_COMPILE_FAIL_TEST_CannotInstantiateWithFunctionReference)
+#elif PW_NC_TEST(CannotInstantiateWithFunctionReference)
+PW_NC_EXPECT("must be instantiated with a function type");
[[maybe_unused]] Function<void (&)()> function_pointer;
-#endif // compile fail tests
+#endif // PW_NC_TEST
// Ensure that Function can be constant initialized.
[[maybe_unused]] PW_CONSTINIT Function<void()> can_be_constant_initialized;
@@ -55,6 +59,12 @@ void CallbackAdd(int a, int b, pw::Function<void(int sum)> callback) {
callback(a + b);
}
+void InlineCallbackAdd(int a,
+ int b,
+ pw::InlineFunction<void(int sum)> callback) {
+ callback(a + b);
+}
+
int add_result = -1;
void free_add_callback(int sum) { add_result = sum; }
@@ -77,6 +87,24 @@ TEST(Function, ConstructInPlace_CapturingLambda) {
EXPECT_EQ(result, 44);
}
+TEST(InlineFunction, ConstructInPlace_FreeFunction) {
+ add_result = -1;
+ InlineCallbackAdd(25, 17, free_add_callback);
+ EXPECT_EQ(add_result, 42);
+}
+
+TEST(InlineFunction, ConstructInPlace_NonCapturingLambda) {
+ add_result = -1;
+ InlineCallbackAdd(25, 18, [](int sum) { add_result = sum; });
+ EXPECT_EQ(add_result, 43);
+}
+
+TEST(InlineFunction, ConstructInPlace_CapturingLambda) {
+ int result = -1;
+ InlineCallbackAdd(25, 19, [&](int sum) { result = sum; });
+ EXPECT_EQ(result, 44);
+}
+
class CallableObject {
public:
CallableObject(int* result) : result_(result) {}
@@ -96,6 +124,12 @@ TEST(Function, ConstructInPlace_CallableObject) {
EXPECT_EQ(result, 45);
}
+TEST(InlineFunction, ConstructInPlace_CallableObject) {
+ int result = -1;
+ InlineCallbackAdd(25, 20, CallableObject(&result));
+ EXPECT_EQ(result, 45);
+}
+
class MemberFunctionTest : public ::testing::Test {
protected:
MemberFunctionTest() : result_(-1) {}
@@ -198,6 +232,18 @@ TEST(Function, Move_Inline) {
#endif // __clang_analyzer__
}
+TEST(InlineFunction, Move_InlineFunctionToFunction) {
+ InlineFunction<int(int, int)> moved(Multiply);
+ EXPECT_NE(moved, nullptr);
+ Function<int(int, int)> multiply(std::move(moved));
+ EXPECT_EQ(multiply(3, 3), 9);
+
+// Ignore use-after-move.
+#ifndef __clang_analyzer__
+ EXPECT_EQ(moved, nullptr);
+#endif // __clang_analyzer__
+}
+
TEST(Function, MoveAssign_Inline) {
Function<int(int, int)> moved(Multiply);
EXPECT_NE(moved, nullptr);
@@ -210,6 +256,18 @@ TEST(Function, MoveAssign_Inline) {
#endif // __clang_analyzer__
}
+TEST(InlineFunction, MoveAssign_InlineFunctionToFunction) {
+ InlineFunction<int(int, int)> moved(Multiply);
+ EXPECT_NE(moved, nullptr);
+ Function<int(int, int)> multiply = std::move(moved);
+ EXPECT_EQ(multiply(3, 3), 9);
+
+// Ignore use-after-move.
+#ifndef __clang_analyzer__
+ EXPECT_EQ(moved, nullptr);
+#endif // __clang_analyzer__
+}
+
TEST(Function, MoveAssign_Callable) {
Function<int(int, int)> operation = Multiply;
EXPECT_EQ(operation(3, 3), 9);
@@ -217,6 +275,13 @@ TEST(Function, MoveAssign_Callable) {
EXPECT_EQ(operation(3, 3), 6);
}
+TEST(InlineFunction, MoveAssign_Callable) {
+ InlineFunction<int(int, int)> operation = Multiply;
+ EXPECT_EQ(operation(3, 3), 9);
+ operation = [](int a, int b) -> int { return a + b; };
+ EXPECT_EQ(operation(3, 3), 6);
+}
+
class MoveTracker {
public:
MoveTracker() : move_count_(0) {}
@@ -232,9 +297,9 @@ class MoveTracker {
TEST(Function, Move_CustomObject) {
Function<int()> moved((MoveTracker()));
- EXPECT_EQ(moved(), 2); // internally moves twice on construction
+ EXPECT_EQ(moved(), 1);
Function<int()> tracker(std::move(moved));
- EXPECT_EQ(tracker(), 3);
+ EXPECT_EQ(tracker(), 2);
// Ignore use-after-move.
#ifndef __clang_analyzer__
@@ -244,9 +309,9 @@ TEST(Function, Move_CustomObject) {
TEST(Function, MoveAssign_CustomObject) {
Function<int()> moved((MoveTracker()));
- EXPECT_EQ(moved(), 2); // internally moves twice on construction
+ EXPECT_EQ(moved(), 1);
Function<int()> tracker = std::move(moved);
- EXPECT_EQ(tracker(), 3);
+ EXPECT_EQ(tracker(), 2);
// Ignore use-after-move.
#ifndef __clang_analyzer__
@@ -276,6 +341,52 @@ TEST(Function, MoveOnlyType) {
EXPECT_TRUE(function(std::move(move_only)));
}
+TEST(Function, CallbackCanOnlyBeCalledOnce) {
+ Callback<void()> cb([]() {});
+ cb();
+ EXPECT_FALSE(cb);
+ EXPECT_EQ(cb, nullptr);
+}
+
+TEST(Function, CallbackDestroysTargetAfterBeingCalled) {
+ class MoveOnlyDestructionCounter {
+ public:
+ MoveOnlyDestructionCounter(int* destroyed_count)
+ : destroyed_(destroyed_count) {}
+
+ MoveOnlyDestructionCounter(const MoveOnlyDestructionCounter& other) =
+ delete;
+ MoveOnlyDestructionCounter& operator=(
+ const MoveOnlyDestructionCounter& other) = delete;
+
+ MoveOnlyDestructionCounter(MoveOnlyDestructionCounter&& t) {
+ *this = std::move(t);
+ }
+ MoveOnlyDestructionCounter& operator=(MoveOnlyDestructionCounter&& t) {
+ destroyed_ = t.destroyed_;
+ t.destroyed_ = nullptr;
+ return *this;
+ }
+
+ ~MoveOnlyDestructionCounter() {
+ if (destroyed_) {
+ (*destroyed_)++;
+ }
+ }
+
+ private:
+ int* destroyed_;
+ };
+
+ int destroyed_count = 0;
+ MoveOnlyDestructionCounter destruction_counter(&destroyed_count);
+ Callback<void()> cb = [destruction_counter =
+ std::move(destruction_counter)]() {};
+ EXPECT_EQ(destroyed_count, 0);
+ cb();
+ EXPECT_EQ(destroyed_count, 1);
+}
+
} // namespace
} // namespace pw
diff --git a/pw_function/pointer_test.cc b/pw_function/pointer_test.cc
new file mode 100644
index 000000000..4951db219
--- /dev/null
+++ b/pw_function/pointer_test.cc
@@ -0,0 +1,126 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_function/pointer.h"
+
+#include "gtest/gtest.h"
+#include "pw_function/function.h"
+
+namespace pw::function {
+namespace {
+
+extern "C" void pw_function_test_InvokeFromCApi(void (*function)(void* context),
+ void* context) {
+ function(context);
+}
+
+extern "C" int pw_function_test_Sum(int (*summer)(int a, int b, void* context),
+ void* context) {
+ return summer(60, 40, context);
+}
+
+TEST(StaticInvoker, Function_NoArguments) {
+ int value = 0;
+ Function<void()> set_value_to_42([&value] { value += 42; });
+
+ pw_function_test_InvokeFromCApi(GetFunctionPointer(set_value_to_42),
+ &set_value_to_42);
+
+ EXPECT_EQ(value, 42);
+
+ pw_function_test_InvokeFromCApi(
+ GetFunctionPointer<decltype(set_value_to_42)>(), &set_value_to_42);
+
+ EXPECT_EQ(value, 84);
+}
+
+TEST(StaticInvoker, Function_WithArguments) {
+ int sum = 0;
+ Function<int(int, int)> sum_stuff([&sum](int a, int b) {
+ sum += a + b;
+ return a + b;
+ });
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer(sum_stuff), &sum_stuff));
+
+ EXPECT_EQ(sum, 100);
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer<decltype(sum_stuff)>(),
+ &sum_stuff));
+
+ EXPECT_EQ(sum, 200);
+}
+
+TEST(StaticInvoker, Callback_NoArguments) {
+ int value = 0;
+ Callback<void()> set_value_to_42([&value] { value += 42; });
+
+ pw_function_test_InvokeFromCApi(GetFunctionPointer(set_value_to_42),
+ &set_value_to_42);
+
+ EXPECT_EQ(value, 42);
+}
+
+TEST(StaticInvoker, Callback_WithArguments) {
+ int sum = 0;
+ Callback<int(int, int)> sum_stuff([&sum](int a, int b) {
+ sum += a + b;
+ return a + b;
+ });
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer<decltype(sum_stuff)>(),
+ &sum_stuff));
+
+ EXPECT_EQ(sum, 100);
+}
+
+TEST(StaticInvoker, Lambda_NoArguments) {
+ int value = 0;
+ auto set_value_to_42([&value] { value += 42; });
+
+ pw_function_test_InvokeFromCApi(GetFunctionPointer(set_value_to_42),
+ &set_value_to_42);
+
+ EXPECT_EQ(value, 42);
+
+ pw_function_test_InvokeFromCApi(
+ GetFunctionPointer<decltype(set_value_to_42)>(), &set_value_to_42);
+
+ EXPECT_EQ(value, 84);
+}
+
+TEST(StaticInvoker, Lambda_WithArguments) {
+ int sum = 0;
+ auto sum_stuff = [&sum](int a, int b) {
+ sum += a + b;
+ return a + b;
+ };
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer(sum_stuff), &sum_stuff));
+
+ EXPECT_EQ(sum, 100);
+
+ EXPECT_EQ(100,
+ pw_function_test_Sum(GetFunctionPointer<decltype(sum_stuff)>(),
+ &sum_stuff));
+
+ EXPECT_EQ(sum, 200);
+}
+
+} // namespace
+} // namespace pw::function
diff --git a/pw_function/public/pw_function/config.h b/pw_function/public/pw_function/config.h
index b98db4b87..b83489adc 100644
--- a/pw_function/public/pw_function/config.h
+++ b/pw_function/public/pw_function/config.h
@@ -21,10 +21,10 @@
// also the size of the Function object itself. Callables larger than this are
// stored externally to the function.
//
-// This defaults to 2 pointers, which is capable of storing common callables
-// such as function pointers and simple lambdas.
+// This defaults to 1 pointer, which is capable of storing common callables
+// such as function pointers and lambdas with a single capture.
#ifndef PW_FUNCTION_INLINE_CALLABLE_SIZE
-#define PW_FUNCTION_INLINE_CALLABLE_SIZE (2 * sizeof(void*))
+#define PW_FUNCTION_INLINE_CALLABLE_SIZE (sizeof(void*))
#endif // PW_FUNCTION_INLINE_CALLABLE_SIZE
static_assert(PW_FUNCTION_INLINE_CALLABLE_SIZE > 0 &&
@@ -32,8 +32,6 @@ static_assert(PW_FUNCTION_INLINE_CALLABLE_SIZE > 0 &&
// Whether functions should allocate memory dynamically (using operator new) if
// a callable is larger than the inline size.
-//
-// NOTE: This is not currently used.
#ifndef PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION
#define PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION 0
#endif // PW_FUNCTION_ENABLE_DYNAMIC_ALLOCATION
diff --git a/pw_function/public/pw_function/function.h b/pw_function/public/pw_function/function.h
index 5fd5fc259..99842b348 100644
--- a/pw_function/public/pw_function/function.h
+++ b/pw_function/public/pw_function/function.h
@@ -13,146 +13,84 @@
// the License.
#pragma once
-#include "pw_function/internal/function.h"
+#include "lib/fit/function.h"
+#include "pw_function/config.h"
namespace pw {
-// pw::Function is a wrapper for an aribtrary callable object. It can be used by
-// callback-based APIs to allow callers to provide any type of callable.
-//
-// Example:
-//
-// template <typename T>
-// bool All(const pw::Vector<T>& items,
-// pw::Function<bool(const T& item)> predicate) {
-// for (const T& item : items) {
-// if (!predicate(item)) {
-// return false;
-// }
-// }
-// return true;
-// }
-//
-// bool ElementsArePostive(const pw::Vector<int>& items) {
-// return All(items, [](const int& i) { return i > 0; });
-// }
-//
-// bool IsEven(const int& i) { return i % 2 == 0; }
-//
-// bool ElementsAreEven(const pw::Vector<int>& items) {
-// return All(items, IsEven);
-// }
-//
-template <typename Callable>
-class Function {
- static_assert(std::is_function_v<Callable>,
- "pw::Function may only be instantianted for a function type, "
- "such as pw::Function<void(int)>.");
-};
+/// `pw::Function` is a wrapper for an arbitrary callable object. It can be used
+/// by callback-based APIs to allow callers to provide any type of callable.
+///
+/// Example:
+/// @code{.cpp}
+///
+/// template <typename T>
+/// bool All(const pw::Vector<T>& items,
+/// pw::Function<bool(const T& item)> predicate) {
+/// for (const T& item : items) {
+/// if (!predicate(item)) {
+/// return false;
+/// }
+/// }
+/// return true;
+/// }
+///
+/// bool ElementsArePositive(const pw::Vector<int>& items) {
+/// return All(items, [](const int& i) { return i > 0; });
+/// }
+///
+/// bool IsEven(const int& i) { return i % 2 == 0; }
+///
+/// bool ElementsAreEven(const pw::Vector<int>& items) {
+/// return All(items, IsEven);
+/// }
+///
+/// @endcode
+template <typename Callable,
+ size_t inline_target_size =
+ function_internal::config::kInlineCallableSize>
+using Function = fit::function_impl<
+ inline_target_size,
+ /*require_inline=*/!function_internal::config::kEnableDynamicAllocation,
+ Callable>;
+
+/// Version of `pw::Function` that exclusively uses inline storage.
+///
+/// IMPORTANT: If `pw::Function` is configured to allow dynamic allocations then
+/// any attempt to convert `pw::InlineFunction` to `pw::Function` will ALWAYS
+/// allocate.
+///
+// TODO(b/252852651): Remove warning above when conversion from
+// `fit::inline_function` to `fit::function` doesn't allocate anymore.
+template <typename Callable,
+ size_t inline_target_size =
+ function_internal::config::kInlineCallableSize>
+using InlineFunction = fit::inline_function<Callable, inline_target_size>;
using Closure = Function<void()>;
-template <typename Return, typename... Args>
-class Function<Return(Args...)> {
- public:
- constexpr Function() = default;
- constexpr Function(std::nullptr_t) : Function() {}
-
- template <typename Callable>
- Function(Callable callable) {
- if (function_internal::IsNull(callable)) {
- holder_.InitializeNullTarget();
- } else {
- holder_.InitializeInlineTarget(std::move(callable));
- }
- }
-
- Function(Function&& other) {
- holder_.MoveInitializeTargetFrom(other.holder_);
- other.holder_.InitializeNullTarget();
- }
-
- Function& operator=(Function&& other) {
- holder_.DestructTarget();
- holder_.MoveInitializeTargetFrom(other.holder_);
- other.holder_.InitializeNullTarget();
- return *this;
- }
-
- Function& operator=(std::nullptr_t) {
- holder_.DestructTarget();
- holder_.InitializeNullTarget();
- return *this;
- }
-
- template <typename Callable>
- Function& operator=(Callable callable) {
- holder_.DestructTarget();
- if (function_internal::IsNull(callable)) {
- holder_.InitializeNullTarget();
- } else {
- holder_.InitializeInlineTarget(std::move(callable));
- }
- return *this;
- }
-
- ~Function() { holder_.DestructTarget(); }
-
- template <typename... PassedArgs>
- Return operator()(PassedArgs&&... args) const {
- return holder_.target()(std::forward<PassedArgs>(args)...);
- };
-
- explicit operator bool() const { return !holder_.target().IsNull(); }
-
- private:
- // TODO(frolv): This is temporarily private while the API is worked out.
- template <typename Callable, size_t kSizeBytes>
- Function(Callable&& callable,
- function_internal::FunctionStorage<kSizeBytes>& storage)
- : Function(callable, &storage) {
- static_assert(sizeof(Callable) <= kSizeBytes,
- "pw::Function callable does not fit into provided storage");
- }
-
- // Constructs a function that stores its callable at the provided location.
- // Public constructors wrapping this must ensure that the memory region is
- // capable of storing the callable in terms of both size and alignment.
- template <typename Callable>
- Function(Callable&& callable, void* storage) {
- if (function_internal::IsNull(callable)) {
- holder_.InitializeNullTarget();
- } else {
- holder_.InitializeMemoryTarget(std::forward(callable), storage);
- }
- }
-
- function_internal::FunctionTargetHolder<
- function_internal::config::kInlineCallableSize,
- Return,
- Args...>
- holder_;
-};
-
-// nullptr comparisions for functions.
-template <typename T>
-bool operator==(const Function<T>& f, std::nullptr_t) {
- return !static_cast<bool>(f);
-}
-
-template <typename T>
-bool operator!=(const Function<T>& f, std::nullptr_t) {
- return static_cast<bool>(f);
-}
-
-template <typename T>
-bool operator==(std::nullptr_t, const Function<T>& f) {
- return !static_cast<bool>(f);
-}
-
-template <typename T>
-bool operator!=(std::nullptr_t, const Function<T>& f) {
- return static_cast<bool>(f);
-}
+/// `pw::Callback` is identical to @cpp_type{pw::Function} except:
+///
+/// 1. On the first call to invoke a `pw::Callback`, the target function held
+/// by the `pw::Callback` cannot be called again.
+/// 2. When a `pw::Callback` is invoked for the first time, the target function
+/// is released and destructed, along with any resources owned by that
+/// function (typically the objects captured by a lambda).
+///
+/// A `pw::Callback` in the "already called" state has the same state as a
+/// `pw::Callback` that has been assigned to `nullptr`.
+template <typename Callable,
+ size_t inline_target_size =
+ function_internal::config::kInlineCallableSize>
+using Callback = fit::callback_impl<
+ inline_target_size,
+ /*require_inline=*/!function_internal::config::kEnableDynamicAllocation,
+ Callable>;
+
+/// Version of `pw::Callback` that exclusively uses inline storage.
+template <typename Callable,
+ size_t inline_target_size =
+ function_internal::config::kInlineCallableSize>
+using InlineCallback = fit::inline_callback<Callable, inline_target_size>;
} // namespace pw
diff --git a/pw_function/public/pw_function/internal/function.h b/pw_function/public/pw_function/internal/function.h
deleted file mode 100644
index 8cffd0259..000000000
--- a/pw_function/public/pw_function/internal/function.h
+++ /dev/null
@@ -1,259 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-#pragma once
-
-#include <cstddef>
-#include <new>
-#include <utility>
-
-#include "pw_assert/assert.h"
-#include "pw_function/config.h"
-#include "pw_preprocessor/compiler.h"
-
-namespace pw::function_internal {
-
-template <typename T, typename Comparison = bool>
-struct NullEq {
- static constexpr bool Test(const T&) { return false; }
-};
-
-// Partial specialization for values of T comparable to nullptr.
-template <typename T>
-struct NullEq<T, decltype(std::declval<T>() == nullptr)> {
- // This is intended to be used for comparing function pointers to nullptr, but
- // the specialization also matches Ts that implicitly convert to a function
- // pointer, such as function types. The compiler may then complain that the
- // comparison is false, as the address is known at compile time and cannot be
- // nullptr. Silence this warning. (The compiler will optimize out the
- // comparison.)
- PW_MODIFY_DIAGNOSTICS_PUSH();
- PW_MODIFY_DIAGNOSTIC(ignored, "-Waddress");
- static constexpr bool Test(const T& v) { return v == nullptr; }
- PW_MODIFY_DIAGNOSTICS_POP();
-};
-
-// Tests whether a value is considered to be null.
-template <typename T>
-static constexpr bool IsNull(const T& v) {
- return NullEq<T>::Test(v);
-}
-
-// FunctionTarget is an interface for storing a callable object and providing a
-// way to invoke it. The GenericFunctionTarget expresses the interface common to
-// all pw::Function instantiations. The derived FunctionTarget class adds the
-// call operator, which is templated on the function arguments and return type.
-class GenericFunctionTarget {
- public:
- constexpr GenericFunctionTarget() = default;
-
- GenericFunctionTarget(const GenericFunctionTarget&) = delete;
- GenericFunctionTarget(GenericFunctionTarget&&) = delete;
- GenericFunctionTarget& operator=(const GenericFunctionTarget&) = delete;
- GenericFunctionTarget& operator=(GenericFunctionTarget&&) = delete;
-
- virtual void Destroy() {}
-
- // Only returns true for NullFunctionTarget.
- virtual bool IsNull() const { return false; }
-
- // Move initialize the function target to a provided location.
- virtual void MoveInitializeTo(void* ptr) = 0;
-
- protected:
- ~GenericFunctionTarget() = default; // The destructor is never called.
-};
-
-// FunctionTarget is an interface for storing a callable object and providing a
-// way to invoke it.
-template <typename Return, typename... Args>
-class FunctionTarget : public GenericFunctionTarget {
- public:
- constexpr FunctionTarget() = default;
-
- // Invoke the callable stored by the function target.
- virtual Return operator()(Args... args) const = 0;
-
- protected:
- ~FunctionTarget() = default; // The destructor is never called.
-};
-
-// A function target that does not store any callable. Attempting to invoke it
-// results in a crash.
-class NullFunctionTarget final : public FunctionTarget<void> {
- public:
- constexpr NullFunctionTarget() = default;
-
- NullFunctionTarget(const NullFunctionTarget&) = delete;
- NullFunctionTarget(NullFunctionTarget&&) = delete;
- NullFunctionTarget& operator=(const NullFunctionTarget&) = delete;
- NullFunctionTarget& operator=(NullFunctionTarget&&) = delete;
-
- bool IsNull() const final { return true; }
-
- void operator()() const final { PW_ASSERT(false); }
-
- void MoveInitializeTo(void* ptr) final { new (ptr) NullFunctionTarget(); }
-};
-
-// Function target that stores a callable as a member within the class.
-template <typename Callable, typename Return, typename... Args>
-class InlineFunctionTarget final : public FunctionTarget<Return, Args...> {
- public:
- explicit InlineFunctionTarget(Callable&& callable)
- : callable_(std::move(callable)) {}
-
- void Destroy() final { callable_.~Callable(); }
-
- InlineFunctionTarget(const InlineFunctionTarget&) = delete;
- InlineFunctionTarget& operator=(const InlineFunctionTarget&) = delete;
-
- InlineFunctionTarget(InlineFunctionTarget&& other)
- : callable_(std::move(other.callable_)) {}
- InlineFunctionTarget& operator=(InlineFunctionTarget&&) = default;
-
- Return operator()(Args... args) const final {
- return callable_(std::forward<Args>(args)...);
- }
-
- void MoveInitializeTo(void* ptr) final {
- new (ptr) InlineFunctionTarget(std::move(*this));
- }
-
- private:
- // This must be mutable to support custom objects that implement operator() in
- // a non-const way.
- mutable Callable callable_;
-};
-
-// Function target which stores a callable at a provided location in memory.
-// The creating context must ensure that the region is properly sized and
-// aligned for the callable.
-template <typename Callable, typename Return, typename... Args>
-class MemoryFunctionTarget final : public FunctionTarget<Return, Args...> {
- public:
- MemoryFunctionTarget(void* address, Callable&& callable) : address_(address) {
- new (address_) Callable(std::move(callable));
- }
-
- void Destroy() final {
- // Multiple MemoryFunctionTargets may have referred to the same callable
- // (due to moves), but only one can have a valid pointer to it. The owner is
- // responsible for destructing the callable.
- if (address_ != nullptr) {
- callable().~Callable();
- }
- }
-
- MemoryFunctionTarget(const MemoryFunctionTarget&) = delete;
- MemoryFunctionTarget& operator=(const MemoryFunctionTarget&) = delete;
-
- // Transfer the pointer to the initialized callable to this object without
- // reinitializing the callable, clearing the address from the other.
- MemoryFunctionTarget(MemoryFunctionTarget&& other)
- : address_(other.address_) {
- other.address_ = nullptr;
- }
- MemoryFunctionTarget& operator=(MemoryFunctionTarget&&) = default;
-
- Return operator()(Args... args) const final { return callable()(args...); }
-
- void MoveInitializeTo(void* ptr) final {
- new (ptr) MemoryFunctionTarget(std::move(*this));
- }
-
- private:
- Callable& callable() {
- return *std::launder(reinterpret_cast<Callable*>(address_));
- }
- const Callable& callable() const {
- return *std::launder(reinterpret_cast<const Callable*>(address_));
- }
-
- void* address_;
-};
-
-template <size_t kSizeBytes>
-using FunctionStorage =
- std::aligned_storage_t<kSizeBytes, alignof(std::max_align_t)>;
-
-// A FunctionTargetHolder stores an instance of a FunctionTarget implementation.
-//
-// The concrete implementation is initialized in an internal buffer by calling
-// one of the initialization functions. After initialization, all
-// implementations are accessed through the virtual FunctionTarget base.
-template <size_t kSizeBytes, typename Return, typename... Args>
-class FunctionTargetHolder {
- public:
- constexpr FunctionTargetHolder() : null_function_ {}
- {}
-
- FunctionTargetHolder(const FunctionTargetHolder&) = delete;
- FunctionTargetHolder(FunctionTargetHolder&&) = delete;
- FunctionTargetHolder& operator=(const FunctionTargetHolder&) = delete;
- FunctionTargetHolder& operator=(FunctionTargetHolder&&) = delete;
-
- constexpr void InitializeNullTarget() {
- static_assert(sizeof(NullFunctionTarget) <= kSizeBytes,
- "NullFunctionTarget must fit within FunctionTargetHolder");
- new (&null_function_) NullFunctionTarget;
- }
-
- // Initializes an InlineFunctionTarget with the callable, failing if it is too
- // large.
- template <typename Callable>
- void InitializeInlineTarget(Callable callable) {
- using InlineFunctionTarget =
- InlineFunctionTarget<Callable, Return, Args...>;
- static_assert(sizeof(InlineFunctionTarget) <= kSizeBytes,
- "Inline callable must fit within FunctionTargetHolder");
- new (&bits_) InlineFunctionTarget(std::move(callable));
- }
-
- // Initializes a MemoryTarget that stores the callable at the provided
- // location.
- template <typename Callable>
- void InitializeMemoryTarget(Callable callable, void* storage) {
- using MemoryFunctionTarget =
- MemoryFunctionTarget<Callable, Return, Args...>;
- static_assert(sizeof(MemoryFunctionTarget) <= kSizeBytes,
- "MemoryFunctionTarget must fit within FunctionTargetHolder");
- new (&bits_) MemoryFunctionTarget(storage, std::move(callable));
- }
-
- void DestructTarget() { target().Destroy(); }
-
- // Initializes the function target within this callable from another target
- // holder's function target.
- void MoveInitializeTargetFrom(FunctionTargetHolder& other) {
- other.target().MoveInitializeTo(&bits_);
- }
-
- // The stored implementation is accessed by punning to the virtual base class.
- using Target = FunctionTarget<Return, Args...>;
- Target& target() { return *std::launder(reinterpret_cast<Target*>(&bits_)); }
- const Target& target() const {
- return *std::launder(reinterpret_cast<const Target*>(&bits_));
- }
-
- private:
- // Storage for an implementation of the FunctionTarget interface. Make this a
- // union with NullFunctionTarget so that the constexpr constructor can
- // initialize null_function_ directly.
- union {
- FunctionStorage<kSizeBytes> bits_;
- NullFunctionTarget null_function_;
- };
-};
-
-} // namespace pw::function_internal
diff --git a/pw_function/public/pw_function/internal/static_invoker.h b/pw_function/public/pw_function/internal/static_invoker.h
new file mode 100644
index 000000000..73605af34
--- /dev/null
+++ b/pw_function/public/pw_function/internal/static_invoker.h
@@ -0,0 +1,43 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// This code is in its own header since Doxygen / Breathe can't parse it.
+
+namespace pw::function::internal {
+
+template <typename FunctionType, typename CallOperatorType>
+struct StaticInvoker;
+
+template <typename FunctionType, typename Return, typename... Args>
+struct StaticInvoker<FunctionType, Return (FunctionType::*)(Args...)> {
+ // Invoker function with the context argument last to match libc. Could add a
+ // version with the context first if needed.
+ static Return InvokeWithContextLast(Args... args, void* context) {
+ return static_cast<FunctionType*>(context)->operator()(
+ std::forward<Args>(args)...);
+ }
+
+ static Return InvokeWithContextFirst(void* context, Args... args) {
+ return static_cast<FunctionType*>(context)->operator()(
+ std::forward<Args>(args)...);
+ }
+};
+
+// Make the const version identical to the non-const version.
+template <typename FunctionType, typename Return, typename... Args>
+struct StaticInvoker<FunctionType, Return (FunctionType::*)(Args...) const>
+ : StaticInvoker<FunctionType, Return (FunctionType::*)(Args...)> {};
+
+} // namespace pw::function::internal
diff --git a/pw_function/public/pw_function/pointer.h b/pw_function/public/pw_function/pointer.h
new file mode 100644
index 000000000..8915eb062
--- /dev/null
+++ b/pw_function/public/pw_function/pointer.h
@@ -0,0 +1,97 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+/// @file pw_function/pointer.h
+///
+/// Traditional callback APIs often use a function pointer and `void*` context
+/// argument. The context argument makes it possible to use the callback
+/// function with non-global data. For example, the `qsort_s` and `bsearch_s`
+/// functions take a pointer to a comparison function that has `void*` context
+/// as its last parameter. @cpp_type{pw::Function} does not naturally work with
+/// these kinds of APIs.
+///
+/// The functions below make it simple to adapt a @cpp_type{pw::Function} for
+/// use with APIs that accept a function pointer and `void*` context argument.
+
+#include <utility>
+
+#include "pw_function/internal/static_invoker.h"
+
+namespace pw::function {
+
+/// Returns a function pointer that invokes a `pw::Function`, lambda, or other
+/// callable object from a `void*` context argument. This makes it possible to
+/// use C++ callables with C-style APIs that take a function pointer and `void*`
+/// context.
+///
+/// The returned function pointer has the same return type and arguments as the
+/// `pw::Function` or `pw::Callback`, except that the last parameter is a
+/// `void*`. `GetFunctionPointerContextFirst` places the `void*` context
+/// parameter first.
+///
+/// The following example adapts a C++ lambda function for use with C-style API
+/// that takes an `int (*)(int, void*)` function and a `void*` context.
+///
+/// @code{.cpp}
+///
+/// void TakesAFunctionPointer(int (*function)(int, void*), void* context);
+///
+/// void UseFunctionPointerApiWithPwFunction() {
+/// // Declare a callable object so a void* pointer can be obtained for it.
+/// auto my_function = [captures](int value) {
+/// // ...
+/// return value + captures;
+/// };
+///
+/// // Invoke the API with the function pointer and callable pointer.
+/// TakesAFunctionPointer(pw::function::GetFunctionPointer(my_function),
+/// &my_function);
+/// }
+///
+/// @endcode
+///
+/// The function returned from this must ONLY be used with the exact type for
+/// which it was created! Function pointer / context APIs are not type safe.
+template <typename FunctionType>
+constexpr auto GetFunctionPointer() {
+ return internal::StaticInvoker<
+ FunctionType,
+ decltype(&FunctionType::operator())>::InvokeWithContextLast;
+}
+
+/// `GetFunctionPointer` overload that uses the type of the function passed to
+/// this call.
+template <typename FunctionType>
+constexpr auto GetFunctionPointer(const FunctionType&) {
+ return GetFunctionPointer<FunctionType>();
+}
+
+/// Same as `GetFunctionPointer`, but the context argument is passed first.
+/// Returns a `void(void*, int)` function for a `pw::Function<void(int)>`.
+template <typename FunctionType>
+constexpr auto GetFunctionPointerContextFirst() {
+ return internal::StaticInvoker<
+ FunctionType,
+ decltype(&FunctionType::operator())>::InvokeWithContextFirst;
+}
+
+/// `GetFunctionPointerContextFirst` overload that uses the type of the function
+/// passed to this call.
+template <typename FunctionType>
+constexpr auto GetFunctionPointerContextFirst(const FunctionType&) {
+ return GetFunctionPointerContextFirst<FunctionType>();
+}
+
+} // namespace pw::function
diff --git a/pw_function/public/pw_function/scope_guard.h b/pw_function/public/pw_function/scope_guard.h
new file mode 100644
index 000000000..ea790ad72
--- /dev/null
+++ b/pw_function/public/pw_function/scope_guard.h
@@ -0,0 +1,87 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <utility>
+
+namespace pw {
+
+/// `ScopeGuard` ensures that the specified functor is executed no matter how
+/// the current scope exits, unless it is dismissed.
+///
+/// Example:
+///
+/// @code
+/// pw::Status SomeFunction() {
+/// PW_TRY(OperationOne());
+/// ScopeGuard undo_operation_one(UndoOperationOne);
+/// PW_TRY(OperationTwo());
+/// ScopeGuard undo_operation_two(UndoOperationTwo);
+/// PW_TRY(OperationThree());
+/// undo_operation_one.Dismiss();
+/// undo_operation_two.Dismiss();
+/// return pw::OkStatus();
+/// }
+/// @endcode
+template <typename Functor>
+class ScopeGuard {
+ public:
+ constexpr ScopeGuard(Functor&& functor)
+ : functor_(std::forward<Functor>(functor)), dismissed_(false) {}
+
+ constexpr ScopeGuard(ScopeGuard&& other) noexcept
+ : functor_(std::move(other.functor_)), dismissed_(other.dismissed_) {
+ other.dismissed_ = true;
+ }
+
+ template <typename OtherFunctor>
+ constexpr ScopeGuard(ScopeGuard<OtherFunctor>&& other)
+ : functor_(std::move(other.functor_)), dismissed_(other.dismissed_) {
+ other.dismissed_ = true;
+ }
+
+ ~ScopeGuard() {
+ if (dismissed_) {
+ return;
+ }
+ functor_();
+ }
+
+ ScopeGuard& operator=(ScopeGuard&& other) noexcept {
+ functor_ = std::move(other.functor_);
+ dismissed_ = std::move(other.dismissed_);
+ other.dismissed_ = true;
+ }
+
+ ScopeGuard() = delete;
+ ScopeGuard(const ScopeGuard&) = delete;
+ ScopeGuard& operator=(const ScopeGuard&) = delete;
+
+ /// Dismisses the `ScopeGuard`, meaning it will no longer execute the
+ /// `Functor` when it goes out of scope.
+ void Dismiss() { dismissed_ = true; }
+
+ private:
+ template <typename OtherFunctor>
+ friend class ScopeGuard;
+
+ Functor functor_;
+ bool dismissed_;
+};
+
+// Enable type deduction for a compatible function pointer.
+template <typename Function>
+ScopeGuard(Function()) -> ScopeGuard<Function (*)()>;
+
+} // namespace pw
diff --git a/pw_function/scope_guard_test.cc b/pw_function/scope_guard_test.cc
new file mode 100644
index 000000000..404293a1f
--- /dev/null
+++ b/pw_function/scope_guard_test.cc
@@ -0,0 +1,88 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_function/scope_guard.h"
+
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "pw_function/function.h"
+
+namespace pw {
+namespace {
+
+TEST(ScopeGuard, ExecutesLambda) {
+ bool executed = false;
+ {
+ ScopeGuard guarded_lambda([&] { executed = true; });
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+static bool static_executed = false;
+void set_static_executed() { static_executed = true; }
+
+TEST(ScopeGuard, ExecutesFunction) {
+ {
+ ScopeGuard guarded_function(set_static_executed);
+
+ EXPECT_FALSE(static_executed);
+ }
+ EXPECT_TRUE(static_executed);
+}
+
+TEST(ScopeGuard, ExecutesPwFunction) {
+ bool executed = false;
+ pw::Function<void()> pw_function([&]() { executed = true; });
+ {
+ ScopeGuard guarded_pw_function(std::move(pw_function));
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+TEST(ScopeGuard, Dismiss) {
+ bool executed = false;
+ {
+ ScopeGuard guard([&] { executed = true; });
+ EXPECT_FALSE(executed);
+ guard.Dismiss();
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_FALSE(executed);
+}
+
+TEST(ScopeGuard, MoveConstructor) {
+ bool executed = false;
+ ScopeGuard first_guard([&] { executed = true; });
+ {
+ ScopeGuard second_guard(std::move(first_guard));
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+TEST(ScopeGuard, MoveOperator) {
+ bool executed = false;
+ ScopeGuard first_guard([&] { executed = true; });
+ {
+ ScopeGuard second_guard = std::move(first_guard);
+ EXPECT_FALSE(executed);
+ }
+ EXPECT_TRUE(executed);
+}
+
+} // namespace
+} // namespace pw
diff --git a/pw_function/size_report/BUILD.bazel b/pw_function/size_report/BUILD.bazel
index 7de277c6a..c21b7b22b 100644
--- a/pw_function/size_report/BUILD.bazel
+++ b/pw_function/size_report/BUILD.bazel
@@ -42,6 +42,7 @@ pw_cc_binary(
pw_cc_binary(
name = "callable_size",
srcs = ["callable_size.cc"],
+ copts = ["-Wno-unused-private-field"],
defines = ["_BASE=1"],
deps = [
"//pw_bloat:bloat_this_binary",
diff --git a/pw_fuzzer/BUILD.gn b/pw_fuzzer/BUILD.gn
index 91cbd0884..142b7617d 100644
--- a/pw_fuzzer/BUILD.gn
+++ b/pw_fuzzer/BUILD.gn
@@ -17,32 +17,39 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_fuzzer/fuzzer.gni")
-import("$dir_pw_fuzzer/oss_fuzz.gni")
config("public_include_path") {
include_dirs = [ "public" ]
visibility = [ ":*" ]
}
-# This is added automatically by the `pw_fuzzer` template.
-config("fuzzing") {
- common_flags = [ "-fsanitize=fuzzer" ]
- cflags = common_flags
- ldflags = common_flags
-}
+# Add flags for adding LLVM sanitizer coverage for fuzzing. This is added by
+# the host_clang_fuzz toolchains.
+config("instrumentation") {
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ # OSS-Fuzz manipulates compiler flags directly. See
+ # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
+ cflags_c = string_split(getenv("CFLAGS"))
+ cflags_cc = string_split(getenv("CXXFLAGS"))
-# OSS-Fuzz needs to be able to specify its own compilers and add flags.
-config("oss_fuzz") {
- # OSS-Fuzz doesn't always link with -fsanitize=fuzzer, sometimes it uses
- #-fsanitize=fuzzer-no-link and provides the fuzzing engine explicitly to be
- # passed to the linker.
- ldflags = [ getenv("LIB_FUZZING_ENGINE") ]
+ # OSS-Fuzz sets "-stdlib=libc++", which conflicts with the "-nostdinc++" set
+ # by `pw_minimal_cpp_stdlib`.
+ cflags_cc += [ "-Wno-unused-command-line-argument" ]
+ } else {
+ cflags = [ "-fsanitize=fuzzer-no-link" ]
+ }
}
-config("oss_fuzz_extra") {
- cflags_c = oss_fuzz_extra_cflags_c
- cflags_cc = oss_fuzz_extra_cflags_cc
- ldflags = oss_fuzz_extra_ldflags
+# Add flags for linking against compiler-rt's libFuzzer. This is added
+# automatically by `pw_fuzzer`.
+config("engine") {
+ if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ # OSS-Fuzz manipulates linker flags directly. See
+ # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
+ ldflags = string_split(getenv("LDFLAGS")) + [ getenv("LIB_FUZZING_ENGINE") ]
+ } else {
+ ldflags = [ "-fsanitize=fuzzer" ]
+ }
}
pw_source_set("pw_fuzzer") {
@@ -81,11 +88,15 @@ pw_doc_group("docs") {
pw_fuzzer("toy_fuzzer") {
sources = [ "examples/toy_fuzzer.cc" ]
deps = [
- "$dir_pw_result",
- "$dir_pw_string",
+ ":pw_fuzzer",
+ dir_pw_status,
]
}
pw_test_group("tests") {
- tests = [ ":toy_fuzzer" ]
+ tests = [ ":toy_fuzzer_test" ]
+}
+
+group("fuzzers") {
+ deps = [ ":toy_fuzzer" ]
}
diff --git a/pw_fuzzer/docs.rst b/pw_fuzzer/docs.rst
index e52b9f163..02bf05f45 100644
--- a/pw_fuzzer/docs.rst
+++ b/pw_fuzzer/docs.rst
@@ -68,8 +68,9 @@ Building fuzzers with GN
To build a fuzzer, do the following:
-1. Add the GN target using ``pw_fuzzer`` GN template, and add it to your the
- test group of the module:
+1. Add the GN target to the module using ``pw_fuzzer`` GN template. If you wish
+ to limit when the generated unit test is run, you can set `enable_test_if` in
+ the same manner as `enable_if` for `pw_test`:
.. code::
@@ -79,25 +80,65 @@ To build a fuzzer, do the following:
pw_fuzzer("my_fuzzer") {
sources = [ "my_fuzzer.cc" ]
deps = [ ":my_lib" ]
+ enable_test_if = device_has_1m_flash
}
+2. Add the generated unit test to the module's test group. This test verifies
+ the fuzzer can build and run, even when not being built in a fuzzing
+ toolchain.
+
+.. code::
+
+ # In $dir_my_module/BUILD.gn
pw_test_group("tests") {
tests = [
- ":existing_tests", ...
- ":my_fuzzer", # <- Added!
+ ...
+ ":my_fuzzer_test",
+ ]
+ }
+
+3. If your module does not already have a group of fuzzers, add it and include
+ it in the top level fuzzers target. Depending on your project, the specific
+ toolchain may differ. Fuzzer toolchains are those with
+ ``pw_toolchain_FUZZING_ENABLED`` set to true. Examples include
+ ``host_clang_fuzz`` and any toolchains that extend it.
+
+.. code::
+
+ # In //BUILD.gn
+ group("fuzzers") {
+ deps = [
+ ...
+ "$dir_my_module:fuzzers($dir_pigweed/targets/host:host_clang_fuzz)",
+ ]
+ }
+
+4. Add your fuzzer to the module's group of fuzzers.
+
+.. code::
+
+ group("fuzzers") {
+ deps = [
+ ...
+ ":my_fuzzer",
]
}
-2. Select your choice of sanitizers ("address" is also the current default).
+5. If desired, select a sanitizer runtime. By default,
+ `//targets/host:host_clang_fuzz` uses "address" if no sanitizer is specified.
See LLVM for `valid options`_.
.. code:: sh
$ gn gen out --args='pw_toolchain_SANITIZERS=["address"]'
-3. Build normally, e.g. using ``pw watch``.
+6. Build the fuzzers!
-.. _run:
+.. code:: sh
+
+ $ ninja -C out fuzzers
+
+.. _bazel:
Building and running fuzzers with Bazel
=======================================
@@ -142,6 +183,8 @@ To build a fuzzer, do the following:
bazel test //my_module:my_fuzz_test --config asan-libfuzzer
+.. _run:
+
Running fuzzers locally
=======================
diff --git a/pw_fuzzer/examples/toy_fuzzer.cc b/pw_fuzzer/examples/toy_fuzzer.cc
index 58ae46122..99b04df67 100644
--- a/pw_fuzzer/examples/toy_fuzzer.cc
+++ b/pw_fuzzer/examples/toy_fuzzer.cc
@@ -21,70 +21,32 @@
#include <cstddef>
#include <cstdint>
-#include <cstring>
-#include <span>
+#include <string_view>
-#include "pw_result/result.h"
-#include "pw_string/util.h"
+#include "pw_fuzzer/fuzzed_data_provider.h"
+#include "pw_status/status.h"
+namespace pw::fuzzer::example {
namespace {
// The code to fuzz. This would normally be in separate library.
-void toy_example(const char* word1, const char* word2) {
- bool greeted = false;
- if (word1[0] == 'h') {
- if (word1[1] == 'e') {
- if (word1[2] == 'l') {
- if (word1[3] == 'l') {
- if (word1[4] == 'o') {
- greeted = true;
- }
- }
- }
- }
- }
- if (word2[0] == 'w') {
- if (word2[1] == 'o') {
- if (word2[2] == 'r') {
- if (word2[3] == 'l') {
- if (word2[4] == 'd') {
- if (greeted) {
- // Our "defect", simulating a crash.
- __builtin_trap();
- }
- }
- }
- }
+Status SomeAPI(std::string_view s1, std::string_view s2) {
+ if (s1 == "hello") {
+ if (s2 == "world") {
+ abort();
}
}
+ return OkStatus();
}
} // namespace
+} // namespace pw::fuzzer::example
// The fuzz target function
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
- // We want to split our input into two strings.
- const std::span<const char> input(reinterpret_cast<const char*>(data), size);
-
- // If that's not feasible, toss this input. The fuzzer will quickly learn that
- // inputs without null-terminators are uninteresting.
- const pw::Result<size_t> possible_word1_size =
- pw::string::NullTerminatedLength(input);
- if (!possible_word1_size.ok()) {
- return 0;
- }
- const std::span<const char> word1 =
- input.first(possible_word1_size.value() + 1);
-
- // Actually, inputs without TWO null terminators are uninteresting.
- std::span<const char> remaining_input = input.subspan(word1.size());
- if (!pw::string::NullTerminatedLength(remaining_input).ok()) {
- return 0;
- }
-
- // Call the code we're targeting!
- toy_example(word1.data(), remaining_input.data());
-
- // By convention, the fuzzer always returns zero.
+ FuzzedDataProvider provider(data, size);
+ std::string s1 = provider.ConsumeRandomLengthString();
+ std::string s2 = provider.ConsumeRemainingBytesAsString();
+ pw::fuzzer::example::SomeAPI(s1, s2).IgnoreError();
return 0;
}
diff --git a/pw_fuzzer/fuzzer.bzl b/pw_fuzzer/fuzzer.bzl
index 2c82feea6..3d158f155 100644
--- a/pw_fuzzer/fuzzer.bzl
+++ b/pw_fuzzer/fuzzer.bzl
@@ -16,14 +16,13 @@
load("@rules_fuzzing//fuzzing:cc_defs.bzl", "cc_fuzz_test")
load(
"//pw_build/bazel_internal:pigweed_internal.bzl",
- _add_cc_and_c_targets = "add_cc_and_c_targets",
- _has_pw_assert_dep = "has_pw_assert_dep",
+ _add_defaults = "add_defaults",
)
def pw_cc_fuzz_test(**kwargs):
- # TODO(pwbug/440): Remove this implicit dependency once we have a better
+ # TODO(b/234877642): Remove this implicit dependency once we have a better
# way to handle the facades without introducing a circular dependency into
# the build.
- if not _has_pw_assert_dep(kwargs["deps"]):
- kwargs["deps"].append("@pigweed//pw_assert")
- _add_cc_and_c_targets(cc_fuzz_test, kwargs)
+ kwargs["deps"].append("@pigweed_config//:pw_assert_backend")
+ _add_defaults(kwargs)
+ cc_fuzz_test(**kwargs)
diff --git a/pw_fuzzer/fuzzer.gni b/pw_fuzzer/fuzzer.gni
index 9bcf50e02..a43a2d9cb 100644
--- a/pw_fuzzer/fuzzer.gni
+++ b/pw_fuzzer/fuzzer.gni
@@ -14,10 +14,11 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/error.gni")
import("$dir_pw_toolchain/host_clang/toolchains.gni")
import("$dir_pw_unit_test/test.gni")
-# Creates a libFuzzer-based fuzzer executable target.
+# Creates a libFuzzer-based fuzzer executable target and unit test
#
# This will link `sources` and `deps` with the libFuzzer compiler runtime. The
# `sources` and `deps` should include a definition of the standard LLVM fuzz
@@ -25,82 +26,47 @@ import("$dir_pw_unit_test/test.gni")
# //pw_fuzzer/docs.rst
# https://llvm.org/docs/LibFuzzer.html
#
+# Additionally, this creates a unit test that does not generate fuzzer inputs
+# and simply executes the fuzz target function with fixed inputs. This is useful
+# for verifying the fuzz target function compiles, links, and runs even when not
+# using a fuzzing-capable host or toolchain.
+#
+# Args:
+# - enable_test_if: (optional) Passed as `enable_if` to the unit test.
+# Remaining arguments are the same as `pw_executable`.
+#
template("pw_fuzzer") {
- # This currently is ONLY supported on Linux and Mac using clang (debug).
- # TODO(pwbug/179): Add Windows here after testing.
- fuzzing_platforms = [
- "linux",
- "mac",
- ]
-
- fuzzing_toolchains =
- [ get_path_info("$dir_pigweed/targets/host:host_clang_fuzz", "abspath") ]
-
- # This is how GN says 'elem in list':
- can_fuzz = fuzzing_platforms + [ host_os ] - [ host_os ] != fuzzing_platforms
-
- can_fuzz = fuzzing_toolchains + [ current_toolchain ] -
- [ current_toolchain ] != fuzzing_toolchains && can_fuzz
-
- if (can_fuzz && pw_toolchain_SANITIZERS != []) {
- # Build the actual fuzzer using the fuzzing config.
- pw_executable(target_name) {
- forward_variables_from(invoker, "*", [ "visibility" ])
- forward_variables_from(invoker, [ "visibility" ])
-
- if (!defined(deps)) {
- deps = []
- }
- deps += [ dir_pw_fuzzer ]
-
- if (!defined(configs)) {
- configs = []
- }
- if (pw_toolchain_OSS_FUZZ_ENABLED) {
- configs += [ "$dir_pw_fuzzer:oss_fuzz" ]
- } else {
- configs += [ "$dir_pw_fuzzer:fuzzing" ]
- }
-
- _fuzzer_output_dir = "${target_out_dir}/bin"
- if (defined(invoker.output_dir)) {
- _fuzzer_output_dir = invoker.output_dir
- }
- output_dir = _fuzzer_output_dir
-
- # Metadata for this fuzzer when used as part of a pw_test_group target.
- metadata = {
- tests = [
- {
- type = "fuzzer"
- test_name = target_name
- test_directory = rebase_path(output_dir, root_build_dir)
- },
- ]
- }
- }
-
- # No-op target to satisfy `pw_test_group`. It is empty as we don't want to
- # automatically run fuzzers.
- group(target_name + ".run") {
+ if (!pw_toolchain_FUZZING_ENABLED) {
+ pw_error(target_name) {
+ message_lines = [ "Toolchain does not enable fuzzing." ]
}
-
- # No-op target to satisfy `pw_test`. It is empty as we don't need a separate
- # lib target.
- group(target_name + ".lib") {
+ not_needed(invoker, "*")
+ } else if (pw_toolchain_SANITIZERS == []) {
+ pw_error(target_name) {
+ message_lines = [ "No sanitizer runtime set." ]
}
+ not_needed(invoker, "*")
} else {
- # Build a unit test that exercise the fuzz target function.
- pw_test(target_name) {
- # TODO(pwbug/195): Re-enable when there's better configurability for
- # on-device fuzz testing.
- enable_if = false
- sources = []
+ pw_executable(target_name) {
+ configs = []
deps = []
- forward_variables_from(invoker, "*", [ "visibility" ])
+ forward_variables_from(invoker,
+ "*",
+ [
+ "enable_test_if",
+ "visibility",
+ ])
forward_variables_from(invoker, [ "visibility" ])
- sources += [ "$dir_pw_fuzzer/pw_fuzzer_disabled.cc" ]
- deps += [ "$dir_pw_fuzzer:run_as_unit_test" ]
+ configs += [ "$dir_pw_fuzzer:engine" ]
+ deps += [ dir_pw_fuzzer ]
}
}
+
+ pw_test("${target_name}_test") {
+ deps = []
+ forward_variables_from(invoker, "*", [ "visibility" ])
+ forward_variables_from(invoker, [ "visibility" ])
+ deps += [ "$dir_pw_fuzzer:run_as_unit_test" ]
+ enable_if = !defined(enable_test_if) || enable_test_if
+ }
}
diff --git a/pw_fuzzer/pw_fuzzer_disabled.cc b/pw_fuzzer/pw_fuzzer_disabled.cc
index 7d01cff24..5b47a1073 100644
--- a/pw_fuzzer/pw_fuzzer_disabled.cc
+++ b/pw_fuzzer/pw_fuzzer_disabled.cc
@@ -32,4 +32,4 @@ TEST(Fuzzer, EmptyInput) {
EXPECT_EQ(LLVMFuzzerTestOneInput(nullptr, 0), 0);
}
-// TODO(pwbug/178): Add support for testing a seed corpus.
+// TODO(b/234883542): Add support for testing a seed corpus.
diff --git a/pw_hdlc/BUILD.bazel b/pw_hdlc/BUILD.bazel
index 99d2031f2..e05925e78 100644
--- a/pw_hdlc/BUILD.bazel
+++ b/pw_hdlc/BUILD.bazel
@@ -15,6 +15,7 @@
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
+ "pw_cc_test",
)
package(default_visibility = ["//visibility:public"])
@@ -31,6 +32,7 @@ pw_cc_library(
],
hdrs = [
"public/pw_hdlc/decoder.h",
+ "public/pw_hdlc/encoded_size.h",
"public/pw_hdlc/encoder.h",
],
includes = ["public"],
@@ -53,6 +55,7 @@ pw_cc_library(
deps = [
":pw_hdlc",
"//pw_rpc",
+ "//pw_span",
],
)
@@ -81,7 +84,27 @@ pw_cc_library(
],
)
-cc_test(
+# A backend for pw_rpc's `system_server` that sends and receives HDLC-framed RPC
+# packets over pw_sys_io.
+#
+# Warning: This system server is polling and blocking, so it's not
+# production-ready. This exists for simplifying initial bringup/testing, and
+# should not be used in any performance-sensitive application.
+pw_cc_library(
+ name = "hdlc_sys_io_system_server",
+ srcs = [
+ "hdlc_sys_io_system_server.cc",
+ ],
+ deps = [
+ ":pw_rpc",
+ ":rpc_channel_output",
+ "//pw_log",
+ "//pw_rpc/system_server:facade",
+ "//pw_stream:sys_io_stream",
+ ],
+)
+
+pw_cc_test(
name = "encoder_test",
srcs = ["encoder_test.cc"],
deps = [
@@ -91,7 +114,7 @@ cc_test(
],
)
-cc_test(
+pw_cc_test(
name = "decoder_test",
srcs = ["decoder_test.cc"],
deps = [
@@ -102,7 +125,20 @@ cc_test(
],
)
-cc_test(
+pw_cc_test(
+ name = "encoded_size_test",
+ srcs = ["encoded_size_test.cc"],
+ deps = [
+ ":pw_hdlc",
+ "//pw_bytes",
+ "//pw_result",
+ "//pw_stream",
+ "//pw_unit_test",
+ "//pw_varint",
+ ],
+)
+
+pw_cc_test(
name = "wire_packet_parser_test",
srcs = ["wire_packet_parser_test.cc"],
deps = [
@@ -111,11 +147,12 @@ cc_test(
],
)
-cc_test(
+pw_cc_test(
name = "rpc_channel_test",
srcs = ["rpc_channel_test.cc"],
deps = [
":pw_hdlc",
+ ":rpc_channel_output",
"//pw_stream",
"//pw_unit_test",
],
diff --git a/pw_hdlc/BUILD.gn b/pw_hdlc/BUILD.gn
index 7ee98c189..754a29a98 100644
--- a/pw_hdlc/BUILD.gn
+++ b/pw_hdlc/BUILD.gn
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
@@ -26,6 +27,7 @@ config("default_config") {
group("pw_hdlc") {
public_deps = [
":decoder",
+ ":encoded_size",
":encoder",
]
}
@@ -37,6 +39,17 @@ pw_source_set("common") {
visibility = [ ":*" ]
}
+pw_source_set("encoded_size") {
+ public_configs = [ ":default_config" ]
+ public = [ "public/pw_hdlc/encoded_size.h" ]
+ public_deps = [
+ ":common",
+ "$dir_pw_bytes",
+ "$dir_pw_span",
+ "$dir_pw_varint",
+ ]
+}
+
pw_source_set("decoder") {
public_configs = [ ":default_config" ]
public = [ "public/pw_hdlc/decoder.h" ]
@@ -46,6 +59,7 @@ pw_source_set("decoder") {
dir_pw_bytes,
dir_pw_checksum,
dir_pw_result,
+ dir_pw_span,
dir_pw_status,
]
deps = [ dir_pw_log ]
@@ -63,9 +77,11 @@ pw_source_set("encoder") {
":common",
dir_pw_bytes,
dir_pw_checksum,
+ dir_pw_span,
dir_pw_status,
dir_pw_stream,
]
+ deps = [ ":encoded_size" ]
friend = [ ":*" ]
}
@@ -75,6 +91,7 @@ pw_source_set("rpc_channel_output") {
public_deps = [
":pw_hdlc",
"$dir_pw_rpc:server",
+ dir_pw_span,
]
}
@@ -103,11 +120,29 @@ pw_source_set("packet_parser") {
]
}
+# A backend for pw_rpc's `system_server` that sends and receives HDLC-framed RPC
+# packets over pw_sys_io.
+#
+# Warning: This system server is polling and blocking, so it's not
+# production-ready. This exists for simplifying initial bringup/testing, and
+# should not be used in any performance-sensitive application.
+pw_source_set("hdlc_sys_io_system_server") {
+ deps = [
+ "$dir_pw_hdlc:pw_rpc",
+ "$dir_pw_hdlc:rpc_channel_output",
+ "$dir_pw_rpc/system_server:facade",
+ "$dir_pw_stream:sys_io_stream",
+ dir_pw_log,
+ ]
+ sources = [ "hdlc_sys_io_system_server.cc" ]
+}
+
pw_test_group("tests") {
tests = [
":encoder_test",
":decoder_test",
":rpc_channel_test",
+ ":encoded_size_test",
":wire_packet_parser_test",
]
group_deps = [
@@ -117,6 +152,17 @@ pw_test_group("tests") {
]
}
+pw_test("encoded_size_test") {
+ deps = [
+ ":pw_hdlc",
+ "$dir_pw_bytes",
+ "$dir_pw_result",
+ "$dir_pw_stream",
+ "$dir_pw_varint",
+ ]
+ sources = [ "encoded_size_test.cc" ]
+}
+
pw_test("encoder_test") {
deps = [ ":pw_hdlc" ]
sources = [ "encoder_test.cc" ]
@@ -156,6 +202,23 @@ pw_test("wire_packet_parser_test") {
sources = [ "wire_packet_parser_test.cc" ]
}
+pw_size_diff("size_report") {
+ title = "HDLC sizes"
+
+ binaries = [
+ {
+ target = "size_report:full"
+ base = "size_report:base"
+ label = "HDLC encode and decode"
+ },
+ {
+ target = "size_report:full_crc"
+ base = "size_report:base_crc"
+ label = "HDLC encode and decode, ignoring CRC and varint"
+ },
+ ]
+}
+
pw_doc_group("docs") {
sources = [
"docs.rst",
@@ -165,4 +228,5 @@ pw_doc_group("docs") {
"py/pw_hdlc/decode.py",
"py/pw_hdlc/encode.py",
]
+ report_deps = [ ":size_report" ]
}
diff --git a/pw_hdlc/CMakeLists.txt b/pw_hdlc/CMakeLists.txt
index 97626c2ef..c34a3b97c 100644
--- a/pw_hdlc/CMakeLists.txt
+++ b/pw_hdlc/CMakeLists.txt
@@ -14,23 +14,156 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_auto_add_simple_module(pw_hdlc
+add_subdirectory(rpc_example)
+
+pw_add_library(pw_hdlc INTERFACE
PUBLIC_DEPS
- pw_assert
+ pw_hdlc.decoder
+ pw_hdlc.encoded_size
+ pw_hdlc.encoder
+)
+
+pw_add_library(pw_hdlc.common INTERFACE
+ HEADERS
+ public/pw_hdlc/internal/protocol.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_varint
+)
+
+pw_add_library(pw_hdlc.encoded_size INTERFACE
+ HEADERS
+ public/pw_hdlc/encoded_size.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_hdlc.common
+ pw_bytes
+ pw_span
+ pw_varint
+)
+
+pw_add_library(pw_hdlc.decoder STATIC
+ HEADERS
+ public/pw_hdlc/decoder.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_hdlc.common
pw_bytes
pw_checksum
pw_result
- pw_router.packet_parser
- pw_rpc.common
+ pw_span
+ pw_status
+ PRIVATE_DEPS
+ pw_log
+ SOURCES
+ decoder.cc
+)
+
+pw_add_library(pw_hdlc.encoder STATIC
+ HEADERS
+ public/pw_hdlc/encoder.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_hdlc.common
+ pw_bytes
+ pw_checksum
+ pw_span
pw_status
pw_stream
+ PRIVATE_DEPS
+ pw_hdlc.encoded_size
+ SOURCES
+ encoder.cc
+ public/pw_hdlc/internal/encoder.h
+)
+
+pw_add_library(pw_hdlc.rpc_channel_output INTERFACE
+ HEADERS
+ public/pw_hdlc/rpc_channel.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_hdlc
+ pw_rpc.server
+ pw_span
+)
+
+pw_add_library(pw_hdlc.pw_rpc STATIC
+ HEADERS
+ public/pw_hdlc/rpc_packets.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_hdlc
+ pw_rpc.server
pw_sys_io
+ SOURCES
+ rpc_packets.cc
+)
+
+pw_add_library(pw_hdlc.packet_parser STATIC
+ HEADERS
+ public/pw_hdlc/wire_packet_parser.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_hdlc
+ pw_router.packet_parser
+ PRIVATE_DEPS
+ pw_bytes
+ pw_checksum
+ SOURCES
+ wire_packet_parser.cc
+)
+
+pw_add_library(pw_hdlc.hdlc_sys_io_system_server STATIC
PRIVATE_DEPS
+ pw_hdlc.pw_rpc
+ pw_hdlc.rpc_channel_output
+ pw_rpc.system_server.facade
+ pw_stream.sys_io_stream
pw_log
+ SOURCES
+ hdlc_sys_io_system_server.cc
)
-add_subdirectory(rpc_example)
-
-if(Zephyr_FOUND AND CONFIG_PIGWEED_HDLC)
- zephyr_link_libraries(pw_hdlc)
+if(Zephyr_FOUND AND CONFIG_PIGWEED_HDLC_RPC)
+ zephyr_link_libraries(pw_hdlc.pw_rpc)
endif()
+
+pw_add_test(pw_hdlc.decoder_test
+ SOURCES
+ decoder_test.cc
+ PRIVATE_DEPS
+ pw_bytes
+ pw_hdlc
+ GROUPS
+ modules
+ pw_hdlc
+)
+
+pw_add_test(pw_hdlc.rpc_channel_test
+ SOURCES
+ rpc_channel_test.cc
+ PRIVATE_DEPS
+ pw_hdlc
+ pw_hdlc.rpc_channel_output
+ GROUPS
+ modules
+ pw_hdlc
+)
+
+pw_add_test(pw_hdlc.wire_packet_parser_test
+ SOURCES
+ wire_packet_parser_test.cc
+ PRIVATE_DEPS
+ pw_bytes
+ pw_hdlc.packet_parser
+ GROUPS
+ modules
+ pw_hdlc
+)
diff --git a/pw_hdlc/Kconfig b/pw_hdlc/Kconfig
index de042ed90..3fc4c00cc 100644
--- a/pw_hdlc/Kconfig
+++ b/pw_hdlc/Kconfig
@@ -14,6 +14,11 @@
config PIGWEED_HDLC
bool "Enable Pigweed HDLC library (pw_hdlc)"
+ select DEPRECATED
+ select PIGWEED_HDLC_RPC
+
+config PIGWEED_HDLC_RPC
+ bool "Enable Pigweed HDLC library (pw_hdlc.pw_rpc)"
select PIGWEED_ASSERT
select PIGWEED_BYTES
select PIGWEED_CHECKSUM
@@ -24,3 +29,4 @@ config PIGWEED_HDLC
select PIGWEED_STREAM
select PIGWEED_SYS_IO
select PIGWEED_LOG
+
diff --git a/pw_hdlc/decoder.cc b/pw_hdlc/decoder.cc
index b66cdf9bc..82452b2fb 100644
--- a/pw_hdlc/decoder.cc
+++ b/pw_hdlc/decoder.cc
@@ -123,7 +123,7 @@ Status Decoder::CheckFrame() const {
return Status::Unavailable();
}
- if (current_frame_size_ < Frame::kMinSizeBytes) {
+ if (current_frame_size_ < Frame::kMinContentSizeBytes) {
PW_LOG_ERROR("Received %lu-byte frame; frame must be at least 6 bytes",
static_cast<unsigned long>(current_frame_size_));
return Status::DataLoss();
@@ -155,7 +155,7 @@ bool Decoder::VerifyFrameCheckSequence() const {
}
uint32_t actual_fcs =
- bytes::ReadInOrder<uint32_t>(std::endian::little, fcs_buffer);
+ bytes::ReadInOrder<uint32_t>(endian::little, fcs_buffer);
return actual_fcs == fcs_.value();
}
diff --git a/pw_hdlc/decoder_test.cc b/pw_hdlc/decoder_test.cc
index af842ea01..358bb26dc 100644
--- a/pw_hdlc/decoder_test.cc
+++ b/pw_hdlc/decoder_test.cc
@@ -125,7 +125,7 @@ TEST(Decoder, TooLargeForBuffer_ReportsResourceExhausted) {
TEST(Decoder, TooLargeForBuffer_StaysWithinBufferBoundaries) {
std::array<byte, 16> buffer = bytes::Initialized<16>('?');
- Decoder decoder(std::span(buffer.data(), 8));
+ Decoder decoder(span(buffer.data(), 8));
for (byte b : bytes::String("~12345678901234567890\xf2\x19\x63\x90")) {
EXPECT_EQ(Status::Unavailable(), decoder.Process(b).status());
diff --git a/pw_hdlc/docs.rst b/pw_hdlc/docs.rst
index 3004891a3..523a0c42f 100644
--- a/pw_hdlc/docs.rst
+++ b/pw_hdlc/docs.rst
@@ -1,8 +1,8 @@
.. _module-pw_hdlc:
--------
+=======
pw_hdlc
--------
+=======
`High-Level Data Link Control (HDLC)
<https://en.wikipedia.org/wiki/High-Level_Data_Link_Control>`_ is a data link
layer protocol intended for serial communication between devices. HDLC is
@@ -32,11 +32,12 @@ remote procedure calls (RPCs) on embedded on devices.
rpc_example/docs
+--------------------
Protocol Description
-====================
+--------------------
Frames
-------
+======
The HDLC implementation in ``pw_hdlc`` supports only HDLC unnumbered
information frames. These frames are encoded as follows:
@@ -55,7 +56,7 @@ information frames. These frames are encoded as follows:
Encoding and sending data
--------------------------
+=========================
This module first writes an initial frame delimiter byte (0x7E) to indicate the
beginning of the frame. Before sending any of the payload data through serial,
the special bytes are escaped:
@@ -73,7 +74,7 @@ frame check sequence is calculated, escaped, and written after. After this, a
final frame delimiter byte (0x7E) is written to mark the end of the frame.
Decoding received bytes
------------------------
+=======================
Frames may be received in multiple parts, so we need to store the received data
in a buffer until the ending frame delimiter (0x7E) is read. When the
``pw_hdlc`` decoder receives data, it unescapes it and adds it to a buffer.
@@ -84,8 +85,9 @@ and does the following:
* If the checksum verification fails, the frame is discarded and an error is
reported.
+---------
API Usage
-=========
+---------
There are two primary functions of the ``pw_hdlc`` module:
* **Encoding** data by constructing a frame with the escaped payload bytes and
@@ -94,12 +96,12 @@ There are two primary functions of the ``pw_hdlc`` module:
check sequence, and returning successfully decoded frames.
Encoder
--------
+=======
The Encoder API provides a single function that encodes data as an HDLC
unnumbered information frame.
C++
-^^^
+---
.. cpp:namespace:: pw
.. cpp:function:: Status hdlc::WriteUIFrame(uint64_t address, ConstByteSpan data, stream::Writer& writer)
@@ -124,7 +126,7 @@ C++
}
Python
-^^^^^^
+------
.. automodule:: pw_hdlc.encode
:members:
@@ -138,10 +140,10 @@ Python
ser.write(encode.ui_frame(address, b'your data here!'))
Typescript
-^^^^^^^^^^
+----------
Encoder
--------
+=======
The Encoder class provides a way to build complete, escaped HDLC UI frames.
.. js:method:: Encoder.uiFrame(address, data)
@@ -151,7 +153,7 @@ The Encoder class provides a way to build complete, escaped HDLC UI frames.
:returns: Uint8Array containing a complete HDLC frame.
Decoder
--------
+=======
The decoder class unescapes received bytes and adds them to a buffer. Complete,
valid HDLC frames are yielded as they are received.
@@ -161,7 +163,7 @@ valid HDLC frames are yielded as they are received.
:yields: Frame complete frames.
C++
-^^^
+---
.. cpp:class:: pw::hdlc::Decoder
.. cpp:function:: pw::Result<Frame> Process(std::byte b)
@@ -205,7 +207,7 @@ decoding HDLC frames:
}
Python
-^^^^^^
+------
.. autoclass:: pw_hdlc.decode.FrameDecoder
:members:
@@ -224,8 +226,7 @@ Below is an example using the decoder class to decode data read from serial:
# Handle the decoded frame
Typescript
-^^^^^^^^^^
-
+----------
Decodes one or more HDLC frames from a stream of data.
.. js:method:: process(data)
@@ -239,37 +240,123 @@ Decodes one or more HDLC frames from a stream of data.
:param Uint8Array data: bytes to be decoded.
:yields: Valid HDLC frames, logging any errors.
+Allocating buffers
+------------------
+Since HDLC's encoding overhead changes with payload size and what data is being
+encoded, this module provides helper functions that are useful for determining
+the size of buffers by providing worst-case sizes of frames given a certain
+payload size and vice-versa.
+
+.. code-block:: cpp
+
+ #include "pw_assert/check.h"
+ #include "pw_bytes/span.h"
+ #include "pw_hdlc/encoder"
+ #include "pw_hdlc/encoded_size.h"
+ #include "pw_status/status.h"
+
+ // The max on-the-wire size in bytes of a single HDLC frame after encoding.
+ constexpr size_t kMtu = 512;
+ constexpr size_t kRpcEncodeBufferSize = pw::hdlc::MaxSafePayloadSize(kMtu);
+ std::array<std::byte, kRpcEncodeBufferSize> rpc_encode_buffer;
+
+ // Any data encoded to this buffer is guaranteed to fit in the MTU after
+ // HDLC encoding.
+ pw::ConstByteSpan GetRpcEncodeBuffer() {
+ return rpc_encode_buffer;
+ }
+
+The HDLC ``Decoder`` has its own helper for allocating a buffer since it doesn't
+need the entire escaped frame in-memory to decode, and therefore has slightly
+lower overhead.
+
+.. code-block:: cpp
+
+ #include "pw_hdlc/decoder.h"
+
+ // The max on-the-wire size in bytes of a single HDLC frame after encoding.
+ constexpr size_t kMtu = 512;
+
+ // Create a decoder given the MTU constraint.
+ constexpr size_t kDecoderBufferSize =
+ pw::hdlc::Decoder::RequiredBufferSizeForFrameSize(kMtu);
+ pw::hdlc::DecoderBuffer<kDecoderBufferSize> decoder;
+
+-------------------
Additional features
-===================
+-------------------
+
+Interleaving unstructured data with HDLC
+========================================
+It is possible to decode HDLC frames from a stream using different protocols or
+unstructured data. This is not recommended, but may be necessary when
+introducing HDLC to an existing system.
+
+The ``FrameAndNonFrameDecoder`` Python class supports working with raw data and
+HDLC frames in the same stream.
-pw::stream::SysIoWriter
-------------------------
-The ``SysIoWriter`` C++ class implements the ``Writer`` interface with
-``pw::sys_io``. This Writer may be used by the C++ encoder to send HDLC frames
-over serial.
+.. autoclass:: pw_hdlc.decode.FrameAndNonFrameDecoder
+ :members:
+
+RpcChannelOutput
+================
+The ``RpcChannelOutput`` implements pw_rpc's ``pw::rpc::ChannelOutput``
+interface, simplifying the process of creating an RPC channel over HDLC. A
+``pw::stream::Writer`` must be provided as the underlying transport
+implementation.
+
+If your HDLC routing path has a Maximum Transmission Unit (MTU) limitation,
+using the ``FixedMtuChannelOutput`` is strongly recommended to verify that the
+currently configured max RPC payload size (dictated by pw_rpc's static encode
+buffer) will always fit safely within the limits of the fixed HDLC MTU *after*
+HDLC encoding.
HdlcRpcClient
--------------
+=============
.. autoclass:: pw_hdlc.rpc.HdlcRpcClient
:members:
.. autoclass:: pw_hdlc.rpc.HdlcRpcLocalServerAndClient
:members:
+Example pw::rpc::system_server backend
+======================================
+This module includes an example implementation of ``pw_rpc``'s ``system_server``
+facade. This implementation sends HDLC encoded RPC packets via ``pw_sys_io``,
+and has blocking sends/reads, so it is hardly performance-oriented and
+unsuitable for performance-sensitive applications. This mostly servers as a
+simplistic example for quickly bringing up RPC over HDLC on bare-metal targets.
+
+-----------
+Size report
+-----------
+The HDLC module currently optimizes for robustness and flexibility instead of
+binary size or performance.
+
+There are two size reports: the first shows the cost of everything needed to
+use HDLC, including the dependencies on common modules like CRC32 from
+pw_checksum and variable-length integer handling from pw_varint. The other is
+the cost if your application is already linking those functions. pw_varint is
+commonly used since it's necessary for protocol buffer handling, so is often
+already present.
+
+.. include:: size_report
+
+-------
Roadmap
-=======
+-------
- **Expanded protocol support** - ``pw_hdlc`` currently only supports
unnumbered information frames. Support for different frame types and
extended control fields may be added in the future.
-- **Higher performance** - We plan to improve the overall performance of the
- decoder and encoder implementations by using SIMD/NEON.
-
+-------------
Compatibility
-=============
+-------------
C++17
+------
Zephyr
-======
-To enable ``pw_hdlc`` for Zephyr add ``CONFIG_PIGWEED_HDLC=y`` to the project's
-configuration.
+------
+To enable ``pw_hdlc.pw_rpc`` for Zephyr add ``CONFIG_PIGWEED_HDLC_RPC=y`` to
+the project's configuration.
+
diff --git a/pw_hdlc/encoded_size_test.cc b/pw_hdlc/encoded_size_test.cc
new file mode 100644
index 000000000..216d4822b
--- /dev/null
+++ b/pw_hdlc/encoded_size_test.cc
@@ -0,0 +1,313 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_hdlc/encoded_size.h"
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/array.h"
+#include "pw_hdlc/decoder.h"
+#include "pw_hdlc/encoder.h"
+#include "pw_hdlc/internal/encoder.h"
+#include "pw_result/result.h"
+#include "pw_stream/memory_stream.h"
+#include "pw_varint/varint.h"
+
+namespace pw::hdlc {
+namespace {
+
+// The varint-encoded address that represents the value that will result in the
+// largest on-the-wire address after HDLC escaping.
+constexpr auto kWidestVarintAddress =
+ bytes::String("\x7e\x7e\x7e\x7e\x7e\x7e\x7e\x7e\x7e\x03");
+
+// This is the decoded varint value of kWidestVarintAddress. This is
+// pre-calculated as a constant to simplify tests.
+constexpr uint64_t kWidestAddress = 0xbf7efdfbf7efdfbf;
+
+// UI frames created by WriteUIFrame() will never be have an escaped control
+// field, but it's technically possible for other HDLC frame types to produce
+// control bytes that would need to be escaped.
+constexpr size_t kEscapedControlCost = kControlSize;
+
+// UI frames created by WriteUIFrame() will never have an escaped control
+// field, but it's technically possible for other HDLC frame types to produce
+// control bytes that would need to be escaped.
+constexpr size_t kEscapedFcsCost = kMaxEscapedFcsSize - kFcsSize;
+
+// Due to API limitations, the worst case buffer calculations used by the HDLC
+// encoder/decoder can't be fully saturated. This constexpr value accounts for
+// this by expressing the delta between the constant largest testable HDLC frame
+// and the calculated worst-case-scenario.
+constexpr size_t kTestLimitationsOverhead =
+ kEscapedControlCost + kEscapedFcsCost;
+
+// A payload only containing bytes that need to be escaped.
+constexpr auto kFullyEscapedPayload =
+ bytes::String("\x7e\x7e\x7e\x7e\x7e\x7e\x7e\x7e");
+
+constexpr uint8_t kEscapeAddress = static_cast<uint8_t>(kFlag);
+constexpr uint8_t kNoEscapeAddress = 6;
+
+TEST(EncodedSize, Constants_WidestAddress) {
+ uint64_t address = 0;
+ size_t address_size =
+ varint::Decode(kWidestVarintAddress, &address, kAddressFormat);
+ EXPECT_EQ(address_size, 10u);
+ EXPECT_EQ(address_size, kMaxAddressSize);
+ EXPECT_EQ(kMaxEscapedVarintAddressSize, 19u);
+ EXPECT_EQ(EscapedSize(kWidestVarintAddress), kMaxEscapedVarintAddressSize);
+ EXPECT_EQ(address, kWidestAddress);
+ EXPECT_EQ(varint::EncodedSize(kWidestAddress), 10u);
+}
+
+TEST(EncodedSize, EscapedSize_AllEscapeBytes) {
+ EXPECT_EQ(EscapedSize(kFullyEscapedPayload), kFullyEscapedPayload.size() * 2);
+}
+
+TEST(EncodedSize, EscapedSize_NoEscapeBytes) {
+ constexpr auto kData = bytes::String("\x01\x23\x45\x67\x89\xab\xcd\xef");
+ EXPECT_EQ(EscapedSize(kData), kData.size());
+}
+
+TEST(EncodedSize, EscapedSize_SomeEscapeBytes) {
+ constexpr auto kData = bytes::String("\x7epabu\x7d");
+ EXPECT_EQ(EscapedSize(kData), kData.size() + 2);
+}
+
+TEST(EncodedSize, EscapedSize_Address) {
+ EXPECT_EQ(EscapedSize(kWidestVarintAddress),
+ varint::EncodedSize(kWidestAddress) * 2 - 1);
+}
+
+TEST(EncodedSize, MaxEncodedSize_Overload) {
+ EXPECT_EQ(MaxEncodedFrameSize(kFullyEscapedPayload.size()),
+ MaxEncodedFrameSize(kWidestAddress, kFullyEscapedPayload));
+}
+
+TEST(EncodedSize, MaxEncodedSize_EmptyPayload) {
+ EXPECT_EQ(14u, MaxEncodedFrameSize(kNoEscapeAddress, {}));
+ EXPECT_EQ(14u, MaxEncodedFrameSize(kEscapeAddress, {}));
+}
+
+TEST(EncodedSize, MaxEncodedSize_PayloadWithoutEscapes) {
+ constexpr auto data = bytes::Array<0x00, 0x01, 0x02, 0x03>();
+ EXPECT_EQ(18u, MaxEncodedFrameSize(kNoEscapeAddress, data));
+ EXPECT_EQ(18u, MaxEncodedFrameSize(kEscapeAddress, data));
+}
+
+TEST(EncodedSize, MaxEncodedSize_PayloadWithOneEscape) {
+ constexpr auto data = bytes::Array<0x00, 0x01, 0x7e, 0x03>();
+ EXPECT_EQ(19u, MaxEncodedFrameSize(kNoEscapeAddress, data));
+ EXPECT_EQ(19u, MaxEncodedFrameSize(kEscapeAddress, data));
+}
+
+TEST(EncodedSize, MaxEncodedSize_PayloadWithAllEscapes) {
+ constexpr auto data = bytes::Initialized<8>(0x7e);
+ EXPECT_EQ(30u, MaxEncodedFrameSize(kNoEscapeAddress, data));
+ EXPECT_EQ(30u, MaxEncodedFrameSize(kEscapeAddress, data));
+}
+
+TEST(EncodedSize, MaxPayload_UndersizedFrame) {
+ EXPECT_EQ(MaxSafePayloadSize(4), 0u);
+}
+
+TEST(EncodedSize, MaxPayload_SmallFrame) {
+ EXPECT_EQ(MaxSafePayloadSize(128), 48u);
+}
+
+TEST(EncodedSize, MaxPayload_MediumFrame) {
+ EXPECT_EQ(MaxSafePayloadSize(512), 240u);
+}
+
+TEST(EncodedSize, FrameToPayloadInversion_Odd) {
+ static constexpr size_t kIntendedPayloadSize = 1234567891;
+ EXPECT_EQ(MaxSafePayloadSize(MaxEncodedFrameSize(kIntendedPayloadSize)),
+ kIntendedPayloadSize);
+}
+
+TEST(EncodedSize, PayloadToFrameInversion_Odd) {
+ static constexpr size_t kIntendedFrameSize = 1234567891;
+ EXPECT_EQ(MaxEncodedFrameSize(MaxSafePayloadSize(kIntendedFrameSize)),
+ kIntendedFrameSize);
+}
+
+TEST(EncodedSize, FrameToPayloadInversion_Even) {
+ static constexpr size_t kIntendedPayloadSize = 42;
+ EXPECT_EQ(MaxSafePayloadSize(MaxEncodedFrameSize(kIntendedPayloadSize)),
+ kIntendedPayloadSize);
+}
+
+TEST(EncodedSize, PayloadToFrameInversion_Even) {
+ static constexpr size_t kIntendedFrameSize = 42;
+ // Because of HDLC encoding overhead requirements, the last byte of the
+ // intended frame size is wasted because it doesn't allow sufficient space for
+ // another byte since said additional byte could require escaping, therefore
+ // requiring a second byte to increase the safe payload size by one.
+ const size_t max_frame_usage =
+ MaxEncodedFrameSize(MaxSafePayloadSize(kIntendedFrameSize));
+ EXPECT_EQ(max_frame_usage, kIntendedFrameSize - 1);
+
+ // There's no further change if the inversion is done again since the frame
+ // size is aligned to the reduced bounds.
+ EXPECT_EQ(MaxEncodedFrameSize(MaxSafePayloadSize(max_frame_usage)),
+ kIntendedFrameSize - 1);
+}
+
+TEST(EncodedSize, MostlyEscaped) {
+ constexpr auto kMostlyEscapedPayload =
+ bytes::String(":)\x7e\x7e\x7e\x7e\x7e\x7e\x7e\x7e");
+ constexpr size_t kUnescapedBytes =
+ 2 * kMostlyEscapedPayload.size() - EscapedSize(kMostlyEscapedPayload);
+ // Subtracting 2 should still leave enough space since two bytes won't need
+ // to be escaped.
+ constexpr size_t kExpectedMaxFrameSize =
+ MaxEncodedFrameSize(kMostlyEscapedPayload.size()) - kUnescapedBytes;
+ std::array<std::byte, kExpectedMaxFrameSize> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ EXPECT_EQ(kUnescapedBytes, 2u);
+ EXPECT_EQ(OkStatus(),
+ WriteUIFrame(kWidestAddress, kFullyEscapedPayload, writer));
+ EXPECT_EQ(writer.size(),
+ kExpectedMaxFrameSize - kTestLimitationsOverhead - kUnescapedBytes);
+}
+
+TEST(EncodedSize, BigAddress_SaturatedPayload) {
+ constexpr size_t kExpectedMaxFrameSize =
+ MaxEncodedFrameSize(kFullyEscapedPayload.size());
+ std::array<std::byte, kExpectedMaxFrameSize> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ EXPECT_EQ(OkStatus(),
+ WriteUIFrame(kWidestAddress, kFullyEscapedPayload, writer));
+ EXPECT_EQ(writer.size(), kExpectedMaxFrameSize - kTestLimitationsOverhead);
+}
+
+TEST(EncodedSize, BigAddress_OffByOne) {
+ constexpr size_t kExpectedMaxFrameSize =
+ MaxEncodedFrameSize(kFullyEscapedPayload.size()) - 1;
+ std::array<std::byte, kExpectedMaxFrameSize> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ EXPECT_EQ(Status::ResourceExhausted(),
+ WriteUIFrame(kWidestAddress, kFullyEscapedPayload, writer));
+}
+
+TEST(EncodedSize, SmallAddress_SaturatedPayload) {
+ constexpr auto kSmallerEscapedAddress = bytes::String("\x7e\x7d");
+ // varint::Decode() is not constexpr, so this is a hard-coded and then runtime
+ // validated.
+ constexpr size_t kVarintDecodedAddress = 7999;
+ constexpr size_t kExpectedMaxFrameSize =
+ MaxEncodedFrameSize(kVarintDecodedAddress, kFullyEscapedPayload);
+ std::array<std::byte, kExpectedMaxFrameSize> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+
+ uint64_t address = 0;
+ size_t address_size =
+ varint::Decode(kSmallerEscapedAddress, &address, kAddressFormat);
+ EXPECT_EQ(address, kVarintDecodedAddress);
+ EXPECT_EQ(address_size, 2u);
+
+ EXPECT_EQ(OkStatus(), WriteUIFrame(address, kFullyEscapedPayload, writer));
+ EXPECT_EQ(writer.size(), kExpectedMaxFrameSize - kTestLimitationsOverhead);
+}
+
+TEST(EncodedSize, SmallAddress_OffByOne) {
+ constexpr auto kSmallerEscapedAddress = bytes::String("\x7e\x7d");
+ // varint::Decode() is not constexpr, so this is a hard-coded and then runtime
+ // validated.
+ constexpr size_t kVarintDecodedAddress = 7999;
+ constexpr size_t kExpectedMaxFrameSize =
+ MaxEncodedFrameSize(kVarintDecodedAddress, kFullyEscapedPayload);
+ std::array<std::byte, kExpectedMaxFrameSize - 1> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+
+ uint64_t address = 0;
+ size_t address_size =
+ varint::Decode(kSmallerEscapedAddress, &address, kAddressFormat);
+ EXPECT_EQ(address, kVarintDecodedAddress);
+ EXPECT_EQ(address_size, 2u);
+
+ EXPECT_EQ(Status::ResourceExhausted(),
+ WriteUIFrame(address, kFullyEscapedPayload, writer));
+}
+
+TEST(DecodedSize, BigAddress_SaturatedPayload) {
+ constexpr auto kNoEscapePayload =
+ bytes::String("The decoder needs the most space when there's no escapes");
+ constexpr size_t kExpectedMaxFrameSize =
+ MaxEncodedFrameSize(kNoEscapePayload.size());
+ std::array<std::byte, kExpectedMaxFrameSize> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ EXPECT_EQ(OkStatus(),
+ WriteUIFrame(kNoEscapeAddress, kNoEscapePayload, writer));
+
+ // Allocate at least enough real buffer space.
+ constexpr size_t kDecoderBufferSize =
+ Decoder::RequiredBufferSizeForFrameSize(kExpectedMaxFrameSize);
+ std::array<std::byte, kDecoderBufferSize> buffer;
+
+ // Pretend the supported frame size is whatever the final size of the encoded
+ // frame was.
+ const size_t max_frame_size =
+ Decoder::RequiredBufferSizeForFrameSize(writer.size());
+
+ Decoder decoder(ByteSpan(buffer).first(max_frame_size));
+ for (const std::byte b : writer.WrittenData()) {
+ Result<Frame> frame = decoder.Process(b);
+ if (frame.ok()) {
+ EXPECT_EQ(frame->address(), kNoEscapeAddress);
+ EXPECT_EQ(frame->data().size(), kNoEscapePayload.size());
+ EXPECT_TRUE(std::memcmp(frame->data().data(),
+ kNoEscapePayload.begin(),
+ kNoEscapePayload.size()) == 0);
+ }
+ }
+}
+
+TEST(DecodedSize, BigAddress_OffByOne) {
+ constexpr auto kNoEscapePayload =
+ bytes::String("The decoder needs the most space when there's no escapes");
+ constexpr size_t kExpectedMaxFrameSize =
+ MaxEncodedFrameSize(kNoEscapePayload.size());
+ std::array<std::byte, kExpectedMaxFrameSize> dest_buffer;
+ stream::MemoryWriter writer(dest_buffer);
+ EXPECT_EQ(OkStatus(),
+ WriteUIFrame(kNoEscapeAddress, kNoEscapePayload, writer));
+
+ // Allocate at least enough real buffer space.
+ constexpr size_t kDecoderBufferSize =
+ Decoder::RequiredBufferSizeForFrameSize(kExpectedMaxFrameSize);
+ std::array<std::byte, kDecoderBufferSize> buffer;
+
+ // Pretend the supported frame size is whatever the final size of the encoded
+ // frame was.
+ const size_t max_frame_size =
+ Decoder::RequiredBufferSizeForFrameSize(writer.size());
+
+ Decoder decoder(ByteSpan(buffer).first(max_frame_size - 1));
+ for (size_t i = 0; i < writer.size(); i++) {
+ Result<Frame> frame = decoder.Process(writer[i]);
+ if (i < writer.size() - 1) {
+ EXPECT_EQ(frame.status(), Status::Unavailable());
+ } else {
+ EXPECT_EQ(frame.status(), Status::ResourceExhausted());
+ }
+ }
+}
+
+} // namespace
+} // namespace pw::hdlc
diff --git a/pw_hdlc/encoder.cc b/pw_hdlc/encoder.cc
index 9b1b027f6..436125547 100644
--- a/pw_hdlc/encoder.cc
+++ b/pw_hdlc/encoder.cc
@@ -18,10 +18,11 @@
#include <array>
#include <cstddef>
#include <cstring>
-#include <span>
#include "pw_bytes/endian.h"
+#include "pw_hdlc/encoded_size.h"
#include "pw_hdlc/internal/encoder.h"
+#include "pw_span/span.h"
#include "pw_varint/varint.h"
using std::byte;
@@ -44,7 +45,7 @@ Status Encoder::WriteData(ConstByteSpan data) {
while (true) {
auto end = std::find_if(begin, data.end(), NeedsEscaping);
- if (Status status = writer_.Write(std::span(begin, end)); !status.ok()) {
+ if (Status status = writer_.Write(span(begin, end)); !status.ok()) {
return status;
}
if (end == data.end()) {
@@ -60,24 +61,13 @@ Status Encoder::WriteData(ConstByteSpan data) {
Status Encoder::FinishFrame() {
if (Status status =
- WriteData(bytes::CopyInOrder(std::endian::little, fcs_.value()));
+ WriteData(bytes::CopyInOrder(endian::little, fcs_.value()));
!status.ok()) {
return status;
}
return writer_.Write(kFlag);
}
-size_t Encoder::MaxEncodedSize(uint64_t address, ConstByteSpan payload) {
- constexpr size_t kFcsMaxSize = 8; // Worst case FCS: 0x7e7e7e7e.
- size_t max_encoded_address_size = varint::EncodedSize(address) * 2;
- size_t encoded_payload_size =
- payload.size() +
- std::count_if(payload.begin(), payload.end(), NeedsEscaping);
-
- return max_encoded_address_size + sizeof(kUnusedControl) +
- encoded_payload_size + kFcsMaxSize;
-}
-
Status Encoder::StartFrame(uint64_t address, std::byte control) {
fcs_.clear();
if (Status status = writer_.Write(kFlag); !status.ok()) {
@@ -92,7 +82,7 @@ Status Encoder::StartFrame(uint64_t address, std::byte control) {
}
metadata_buffer[metadata_size++] = control;
- return WriteData(std::span(metadata_buffer).first(metadata_size));
+ return WriteData(span(metadata_buffer).first(metadata_size));
}
} // namespace internal
@@ -100,8 +90,7 @@ Status Encoder::StartFrame(uint64_t address, std::byte control) {
Status WriteUIFrame(uint64_t address,
ConstByteSpan payload,
stream::Writer& writer) {
- if (internal::Encoder::MaxEncodedSize(address, payload) >
- writer.ConservativeWriteLimit()) {
+ if (MaxEncodedFrameSize(address, payload) > writer.ConservativeWriteLimit()) {
return Status::ResourceExhausted();
}
diff --git a/pw_hdlc/encoder_test.cc b/pw_hdlc/encoder_test.cc
index 1904fbcb7..3f23c3db0 100644
--- a/pw_hdlc/encoder_test.cc
+++ b/pw_hdlc/encoder_test.cc
@@ -17,9 +17,11 @@
#include <algorithm>
#include <array>
#include <cstddef>
+#include <cstring>
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
+#include "pw_hdlc/encoded_size.h"
#include "pw_hdlc/internal/encoder.h"
#include "pw_hdlc/internal/protocol.h"
#include "pw_stream/memory_stream.h"
@@ -44,16 +46,17 @@ constexpr uint8_t kEncodedAddress = (kAddress << 1) | 1;
class WriteUnnumberedFrame : public ::testing::Test {
protected:
- WriteUnnumberedFrame() : writer_(buffer_) {}
+ WriteUnnumberedFrame() : buffer_{}, writer_(buffer_) {}
+ // Allocate a buffer that will fit any 7-byte payload.
+ std::array<byte, MaxEncodedFrameSize(7)> buffer_;
stream::MemoryWriter writer_;
- std::array<byte, 32> buffer_;
};
constexpr byte kUnnumberedControl = byte{0x3};
TEST_F(WriteUnnumberedFrame, EmptyPayload) {
- ASSERT_EQ(OkStatus(), WriteUIFrame(kAddress, std::span<byte>(), writer_));
+ ASSERT_EQ(OkStatus(), WriteUIFrame(kAddress, span<byte>(), writer_));
EXPECT_ENCODER_WROTE(bytes::Concat(
kFlag, kEncodedAddress, kUnnumberedControl, uint32_t{0x832d343f}, kFlag));
}
@@ -186,42 +189,11 @@ class ErrorWriter : public stream::NonSeekableWriter {
Status DoWrite(ConstByteSpan) override { return Status::Unimplemented(); }
};
-TEST(WriteUnnumberedFrame, WriterError) {
+TEST(WriteUIFrame, WriterError) {
ErrorWriter writer;
EXPECT_EQ(Status::Unimplemented(),
WriteUIFrame(kAddress, bytes::Array<0x01>(), writer));
}
} // namespace
-
-namespace internal {
-namespace {
-
-constexpr uint8_t kEscapeAddress = 0x7d;
-
-TEST(Encoder, MaxEncodedSize_EmptyPayload) {
- EXPECT_EQ(11u, Encoder::MaxEncodedSize(kAddress, {}));
- EXPECT_EQ(11u, Encoder::MaxEncodedSize(kEscapeAddress, {}));
-}
-
-TEST(Encoder, MaxEncodedSize_PayloadWithoutEscapes) {
- constexpr auto data = bytes::Array<0x00, 0x01, 0x02, 0x03>();
- EXPECT_EQ(15u, Encoder::MaxEncodedSize(kAddress, data));
- EXPECT_EQ(15u, Encoder::MaxEncodedSize(kEscapeAddress, data));
-}
-
-TEST(Encoder, MaxEncodedSize_PayloadWithOneEscape) {
- constexpr auto data = bytes::Array<0x00, 0x01, 0x7e, 0x03>();
- EXPECT_EQ(16u, Encoder::MaxEncodedSize(kAddress, data));
- EXPECT_EQ(16u, Encoder::MaxEncodedSize(kEscapeAddress, data));
-}
-
-TEST(Encoder, MaxEncodedSize_PayloadWithAllEscapes) {
- constexpr auto data = bytes::Initialized<8>(0x7e);
- EXPECT_EQ(27u, Encoder::MaxEncodedSize(kAddress, data));
- EXPECT_EQ(27u, Encoder::MaxEncodedSize(kEscapeAddress, data));
-}
-
-} // namespace
-} // namespace internal
} // namespace pw::hdlc
diff --git a/targets/mimxrt595_evk/system_rpc_server.cc b/pw_hdlc/hdlc_sys_io_system_server.cc
index 52be51435..684f1786e 100644
--- a/targets/mimxrt595_evk/system_rpc_server.cc
+++ b/pw_hdlc/hdlc_sys_io_system_server.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -14,6 +14,7 @@
#include <cstddef>
+#include "pw_hdlc/encoded_size.h"
#include "pw_hdlc/rpc_channel.h"
#include "pw_hdlc/rpc_packets.h"
#include "pw_log/log.h"
@@ -23,16 +24,19 @@
namespace pw::rpc::system_server {
namespace {
-constexpr size_t kMaxTransmissionUnit = 256;
+// Hard-coded to 1055 bytes, which is enough to fit 512-byte payloads when using
+// HDLC framing.
+constexpr size_t kMaxTransmissionUnit = 1055;
+
+static_assert(kMaxTransmissionUnit ==
+ hdlc::MaxEncodedFrameSize(rpc::cfg::kEncodingBufferSizeBytes));
// Used to write HDLC data to pw::sys_io.
stream::SysIoWriter writer;
-stream::SysIoReader reader;
// Set up the output channel for the pw_rpc server to use.
-hdlc::RpcChannelOutput hdlc_channel_output(writer,
- pw::hdlc::kDefaultRpcAddress,
- "HDLC channel");
+hdlc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
+ writer, pw::hdlc::kDefaultRpcAddress, "HDLC channel");
Channel channels[] = {pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
rpc::Server server(channels);
@@ -42,15 +46,17 @@ void Init() {
// Send log messages to HDLC address 1. This prevents logs from interfering
// with pw_rpc communications.
pw::log_basic::SetOutput([](std::string_view log) {
- pw::hdlc::WriteUIFrame(1, std::as_bytes(std::span(log)), writer);
+ pw::hdlc::WriteUIFrame(1, as_bytes(span<const char>(log)), writer);
});
}
rpc::Server& Server() { return server; }
Status Start() {
+ constexpr size_t kDecoderBufferSize =
+ hdlc::Decoder::RequiredBufferSizeForFrameSize(kMaxTransmissionUnit);
// Declare a buffer for decoding incoming HDLC frames.
- std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+ std::array<std::byte, kDecoderBufferSize> input_buffer;
hdlc::Decoder decoder(input_buffer);
while (true) {
@@ -62,7 +68,7 @@ Status Start() {
if (auto result = decoder.Process(byte); result.ok()) {
hdlc::Frame& frame = result.value();
if (frame.address() == hdlc::kDefaultRpcAddress) {
- server.ProcessPacket(frame.data(), hdlc_channel_output);
+ server.ProcessPacket(frame.data());
}
}
}
diff --git a/pw_hdlc/java/main/dev/pigweed/pw_hdlc/BUILD.bazel b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/BUILD.bazel
new file mode 100644
index 000000000..e29791eae
--- /dev/null
+++ b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/BUILD.bazel
@@ -0,0 +1,32 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+java_library(
+ name = "pw_hdlc",
+ srcs = [
+ "CustomVarInt.java",
+ "Decoder.java",
+ "Encoder.java",
+ "Frame.java",
+ "Protocol.java",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pw_log/java/main/dev/pigweed/pw_log",
+ "//third_party/google_auto:value",
+ "@com_google_protobuf//java/lite",
+ "@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/pw_hdlc/java/main/dev/pigweed/pw_hdlc/CustomVarInt.java b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/CustomVarInt.java
new file mode 100644
index 000000000..85a5b6c47
--- /dev/null
+++ b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/CustomVarInt.java
@@ -0,0 +1,73 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_hdlc;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Encodes and decodes Varint in pigweed's custom, one terminated, least significant format.
+ * Regular protobuf format is zero terminated, most significant.
+ */
+public final class CustomVarInt {
+ private static final int MAX_COUNT = 10;
+
+ public static void putVarLong(long v, ByteBuffer sink) {
+ while (true) {
+ int bits = (((int) v) & 0x7f) << 1;
+ v >>>= 7;
+ if (v == 0) {
+ sink.put((byte) (bits | 0x01));
+ return;
+ }
+ sink.put((byte) bits);
+ }
+ }
+
+ public static int varLongSize(long v) {
+ int result = 0;
+ do {
+ result++;
+ v >>>= 7;
+ } while (v != 0);
+ return result;
+ }
+
+ public static long getVarLong(ByteBuffer src) {
+ long decodedValue = 0;
+ int count = 0;
+
+ while (src.hasRemaining()) {
+ if (count >= MAX_COUNT) {
+ return 0;
+ }
+ byte b = src.get();
+ // Add the bottom seven bits of the next byte to the result.
+ decodedValue |= ((long) ((b >> 1) & 0x7F)) << (7 * count);
+
+ // Stop decoding if the end is reached.
+ if (isLastByte(b)) {
+ return decodedValue;
+ }
+ count++;
+ }
+ return 0;
+ }
+
+ private static boolean isLastByte(byte b) {
+ return (b & 1) == 1;
+ }
+
+ private CustomVarInt() {}
+} \ No newline at end of file
diff --git a/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Decoder.java b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Decoder.java
new file mode 100644
index 000000000..d5e0a0be1
--- /dev/null
+++ b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Decoder.java
@@ -0,0 +1,172 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_hdlc;
+
+import com.google.common.io.BaseEncoding;
+import dev.pigweed.pw_hdlc.Protocol;
+import dev.pigweed.pw_log.Logger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.zip.CRC32;
+
+/**
+ * Decodes Pigweed's HDLC frames received over the wire and passes complete frames to the registered
+ * listener.
+ */
+public class Decoder {
+ private static final Logger logger = Logger.forClass(Decoder.class);
+ // Should be more than enough
+ private static final int MAX_FRAME_SIZE_BYTES = 4096;
+
+ private final OnCompleteFrame listener;
+ private State state = State.INTER_FRAME;
+ private int currentFrameSize = 0;
+ private final byte[] buffer = new byte[MAX_FRAME_SIZE_BYTES];
+
+ /** OnCompleteFrame */
+ public interface OnCompleteFrame {
+ void onCompleteFrame(Frame frame);
+ }
+
+ enum State {
+ INTER_FRAME,
+ FRAME,
+ ESCAPE_FRAME,
+ }
+
+ public Decoder(OnCompleteFrame listener) {
+ this.listener = listener;
+ }
+
+ public void process(ByteBuffer buffer) {
+ while (buffer.hasRemaining()) {
+ process(buffer.get());
+ }
+ }
+
+ public void process(byte[] buffer) {
+ for (byte b : buffer) {
+ process(b);
+ }
+ }
+
+ public void process(byte b) {
+ switch (state) {
+ case INTER_FRAME:
+ if (b == Protocol.FLAG) {
+ state = State.FRAME;
+
+ // Report an error if non-flag bytes were read between frames.
+ if (currentFrameSize != 0) {
+ reset();
+ return;
+ }
+ } else {
+ // Count bytes to track how many are discarded.
+ currentFrameSize += 1;
+ }
+ return;
+ case FRAME: {
+ if (b == Protocol.FLAG) {
+ if (checkFrame()) {
+ ByteBuffer message = ByteBuffer.wrap(buffer, 0, currentFrameSize);
+ Frame frame = Frame.parse(message);
+ if (frame != null) {
+ listener.onCompleteFrame(frame);
+ }
+ }
+ reset();
+ } else if (b == Protocol.ESCAPE) {
+ state = State.ESCAPE_FRAME;
+ } else {
+ appendByte(b);
+ }
+ return;
+ }
+ case ESCAPE_FRAME:
+ // The flag character cannot be escaped; return an error.
+ if (b == Protocol.FLAG) {
+ state = State.FRAME;
+ reset();
+ }
+
+ if (b == Protocol.ESCAPE) {
+ // Two escape characters in a row is illegal -- invalidate this frame.
+ // The frame is reported abandoned when the next flag byte appears.
+ state = State.INTER_FRAME;
+
+ // Count the escape byte so that the inter-frame state detects an error.
+ currentFrameSize += 1;
+ } else {
+ state = State.FRAME;
+ appendByte(escape(b));
+ }
+ }
+ }
+
+ private void reset() {
+ currentFrameSize = 0;
+ }
+
+ private void appendByte(byte b) {
+ if (currentFrameSize < MAX_FRAME_SIZE_BYTES) {
+ buffer[currentFrameSize] = b;
+ }
+
+ // Always increase size: if it is larger than the buffer, overflow occurred.
+ currentFrameSize += 1;
+ }
+
+ private static byte escape(byte b) {
+ return (byte) (b ^ Protocol.ESCAPE_CONSTANT);
+ }
+
+ private boolean checkFrame() {
+ // Empty frames are not an error; repeated flag characters are okay.
+ if (currentFrameSize == 0) {
+ return true;
+ }
+
+ if (currentFrameSize < Frame.MIN_FRAME_SIZE_BYTES) {
+ logger.atWarning().log(
+ "Frame length (%d) less than %d", currentFrameSize, Frame.MIN_FRAME_SIZE_BYTES);
+ return false;
+ }
+
+ if (!verifyFrameCheckSequence()) {
+ logger.atWarning().log("Frame CRC verification failed");
+ return false;
+ }
+
+ if (currentFrameSize > MAX_FRAME_SIZE_BYTES) {
+ logger.atWarning().log("Frame too big (%d > %d)", currentFrameSize, MAX_FRAME_SIZE_BYTES);
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean verifyFrameCheckSequence() {
+ int crcOffset = currentFrameSize - Frame.CRC_SIZE;
+ CRC32 crc32 = new CRC32();
+ crc32.update(buffer, 0, crcOffset);
+ int actualCrc = (int) crc32.getValue();
+ return actualCrc
+ == ByteBuffer.wrap(buffer, crcOffset, Frame.CRC_SIZE)
+ .order(ByteOrder.LITTLE_ENDIAN)
+ .asIntBuffer()
+ .get();
+ }
+}
diff --git a/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Encoder.java b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Encoder.java
new file mode 100644
index 000000000..ae0374103
--- /dev/null
+++ b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Encoder.java
@@ -0,0 +1,156 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_hdlc;
+
+import dev.pigweed.pw_hdlc.CustomVarInt;
+import dev.pigweed.pw_hdlc.Protocol;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.zip.CRC32;
+
+/** Encodes a payload with pigweed's HDLC format */
+public class Encoder {
+ private final CRC32 crc32 = new CRC32();
+ private final OutputStream output;
+
+ /** Writes the payload as an HDLC unnumbered information (UI) frame. */
+ public static void writeUiFrame(long address, ByteBuffer payload, OutputStream output)
+ throws IOException {
+ Encoder encoder = new Encoder(output);
+ encoder.startUnnumberedFrame(address);
+ encoder.writeData(payload);
+ encoder.finishFrame();
+ }
+
+ public static void writeFrame(Frame frame, OutputStream output) throws IOException {
+ Encoder encoder = new Encoder(output);
+ encoder.startFrame(frame.getAddress(), frame.getControl());
+ encoder.writeData(frame.getPayload());
+ encoder.finishFrame();
+ }
+
+ /** Returns the encoded size of a UI frame with the provided payload. */
+ public static int getUiFrameEncodedSize(long address, byte[] payload) {
+ return getUiFrameEncodedSize(address, ByteBuffer.wrap(payload));
+ }
+
+ public static int getUiFrameEncodedSize(long address, ByteBuffer payload) {
+ ByteCountingOutputStream output = new ByteCountingOutputStream();
+ try {
+ writeUiFrame(address, payload, output);
+ } catch (IOException e) {
+ throw new AssertionError("ByteCountingOutputStream does not throw exceptions!", e);
+ }
+ return output.writtenBytes;
+ }
+
+ /** Returns the number of payload bytes that can be encoded in a UI frame of the given size. */
+ public static int getMaxPayloadBytesInUiFrameOfSize(
+ long address, byte[] payload, int maxEncodedUiFrameSizeBytes) {
+ return getMaxPayloadBytesInUiFrameOfSize(
+ address, ByteBuffer.wrap(payload), maxEncodedUiFrameSizeBytes);
+ }
+
+ public static int getMaxPayloadBytesInUiFrameOfSize(
+ long address, ByteBuffer payload, int maxEncodedUiFrameSizeBytes) {
+ ByteCountingOutputStream output = new ByteCountingOutputStream();
+ Encoder encoder = new Encoder(output);
+ int payloadBytesProcessed = 0;
+
+ try {
+ encoder.startUnnumberedFrame(address);
+ while (payload.hasRemaining()) {
+ encoder.write(payload.get());
+ if (output.writtenBytes + encoder.getFrameSuffixSize() > maxEncodedUiFrameSizeBytes) {
+ break;
+ }
+ payloadBytesProcessed += 1;
+ }
+ } catch (IOException e) {
+ throw new AssertionError("ByteCountingOutputStream does not throw exceptions!", e);
+ }
+ return payloadBytesProcessed;
+ }
+
+ private Encoder(OutputStream output) {
+ this.output = output;
+ }
+
+ private void startUnnumberedFrame(long address) throws IOException {
+ startFrame(address, Protocol.UNNUMBERED_INFORMATION);
+ }
+
+ private void startFrame(long address, byte control) throws IOException {
+ crc32.reset();
+ output.write(Protocol.FLAG);
+ writeVarLong(address);
+ byte[] wrappedControl = {control};
+ writeData(ByteBuffer.wrap(wrappedControl));
+ }
+
+ private void finishFrame() throws IOException {
+ writeCrc();
+ output.write(Protocol.FLAG);
+ }
+
+ private void writeCrc() throws IOException {
+ long crc = crc32.getValue();
+ byte[] buffer = {(byte) crc, (byte) (crc >> 8), (byte) (crc >> 16), (byte) (crc >> 24)};
+ writeData(ByteBuffer.wrap(buffer));
+ }
+
+ private void writeVarLong(long data) throws IOException {
+ ByteBuffer buffer = ByteBuffer.allocate(CustomVarInt.varLongSize(data));
+ CustomVarInt.putVarLong(data, buffer);
+ buffer.rewind();
+ writeData(buffer);
+ }
+
+ private void writeData(ByteBuffer data) throws IOException {
+ while (data.hasRemaining()) {
+ byte b = data.get();
+ write(b);
+ crc32.update(b);
+ }
+ }
+
+ private void write(byte b) throws IOException {
+ if (b == Protocol.FLAG) {
+ output.write(Protocol.ESCAPED_FLAG);
+ } else if (b == Protocol.ESCAPE) {
+ output.write(Protocol.ESCAPED_ESCAPE);
+ } else {
+ output.write(b);
+ }
+ }
+
+ private int getFrameSuffixSize() {
+ int crc32Escapes = 0;
+ for (int value = (int) crc32.getValue(); value != 0; value >>>= 8) {
+ crc32Escapes += (value & 0xFF) == Protocol.FLAG || (value & 0xFF) == Protocol.ESCAPE ? 1 : 0;
+ }
+ return 4 /* CRC32 */ + crc32Escapes + 1 /* final flag byte */;
+ }
+
+ private static class ByteCountingOutputStream extends OutputStream {
+ private int writtenBytes;
+
+ @Override
+ public void write(int b) {
+ writtenBytes += 1;
+ }
+ }
+} \ No newline at end of file
diff --git a/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Frame.java b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Frame.java
new file mode 100644
index 000000000..946ec74f2
--- /dev/null
+++ b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Frame.java
@@ -0,0 +1,69 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_hdlc;
+
+import java.nio.ByteBuffer;
+import javax.annotation.Nullable;
+
+/** Contents of an HDLC frame (after removing flags and CRC) */
+public class Frame {
+ // address, control + CRC32
+ public static final int MIN_FRAME_SIZE_BYTES = 6;
+ public static final int CRC_SIZE = 4;
+
+ private final long address;
+ private final byte control;
+ private final ByteBuffer payload;
+
+ @Nullable
+ public static Frame parse(ByteBuffer payload) {
+ long address = CustomVarInt.getVarLong(payload);
+ if (address == 0) {
+ return null;
+ }
+ byte control = payload.get();
+ int dataSize = payload.remaining() - CRC_SIZE;
+ if (dataSize < 0) {
+ return null;
+ }
+ ByteBuffer data = ByteBuffer.allocate(dataSize);
+ payload.get(data.array());
+ return new Frame(address, control, data);
+ }
+
+ private Frame(long address, byte control, ByteBuffer payload) {
+ this.address = address;
+ this.control = control;
+ this.payload = payload;
+ }
+
+ public long getAddress() {
+ return address;
+ }
+
+ public byte getControl() {
+ return control;
+ }
+
+ public ByteBuffer getPayload() {
+ return payload;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Frame{address=0x%x, control=0x%x, payload=%s}", address, control, payload);
+ }
+} \ No newline at end of file
diff --git a/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Protocol.java b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Protocol.java
new file mode 100644
index 000000000..3549973f5
--- /dev/null
+++ b/pw_hdlc/java/main/dev/pigweed/pw_hdlc/Protocol.java
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_hdlc;
+
+/** Constants shared between Encoder and Decoder */
+public final class Protocol {
+ static final byte UNNUMBERED_INFORMATION = 0x03;
+ static final byte FLAG = 0x7E;
+ static final byte ESCAPE = 0x7D;
+ static final byte[] ESCAPED_FLAG = {ESCAPE, 0x5E};
+ static final byte[] ESCAPED_ESCAPE = {ESCAPE, 0x5D};
+ static final byte ESCAPE_CONSTANT = 0x20;
+
+ private Protocol() {}
+} \ No newline at end of file
diff --git a/pw_hdlc/java/test/dev/pigweed/pw_hdlc/BUILD.bazel b/pw_hdlc/java/test/dev/pigweed/pw_hdlc/BUILD.bazel
new file mode 100644
index 000000000..07c9373e7
--- /dev/null
+++ b/pw_hdlc/java/test/dev/pigweed/pw_hdlc/BUILD.bazel
@@ -0,0 +1,72 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+licenses(["notice"])
+
+java_test(
+ name = "DecoderTest",
+ size = "small",
+ srcs = ["DecoderTest.java"],
+ test_class = "dev.pigweed.pw_hdlc.DecoderTest",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pw_hdlc/java/main/dev/pigweed/pw_hdlc",
+ "//pw_log/java/main/dev/pigweed/pw_log",
+ "@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_flogger_flogger_system_backend",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_truth_truth",
+ ],
+)
+
+java_test(
+ name = "EncoderTest",
+ size = "small",
+ srcs = ["EncoderTest.java"],
+ test_class = "dev.pigweed.pw_hdlc.EncoderTest",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pw_hdlc/java/main/dev/pigweed/pw_hdlc",
+ "//pw_log/java/main/dev/pigweed/pw_log",
+ "@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_flogger_flogger_system_backend",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_truth_truth",
+ ],
+)
+
+java_test(
+ name = "FrameTest",
+ size = "small",
+ srcs = ["FrameTest.java"],
+ test_class = "dev.pigweed.pw_hdlc.FrameTest",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pw_hdlc/java/main/dev/pigweed/pw_hdlc",
+ "//pw_log/java/main/dev/pigweed/pw_log",
+ "@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_flogger_flogger_system_backend",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_truth_truth",
+ ],
+)
+
+test_suite(
+ name = "pw_hdlc",
+ tests = [
+ ":DecoderTest",
+ ":EncoderTest",
+ ":FrameTest",
+ ],
+)
diff --git a/pw_hdlc/java/test/dev/pigweed/pw_hdlc/DecoderTest.java b/pw_hdlc/java/test/dev/pigweed/pw_hdlc/DecoderTest.java
new file mode 100644
index 000000000..bdd206aa9
--- /dev/null
+++ b/pw_hdlc/java/test/dev/pigweed/pw_hdlc/DecoderTest.java
@@ -0,0 +1,88 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_hdlc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import org.junit.Test;
+
+public class DecoderTest {
+ private final ArrayList<Frame> frames = new ArrayList<>();
+ private final Decoder decoder = new Decoder(frames::add);
+
+ @Test
+ public void processSingleFrame() {
+ byte[] data = {'~', '1', '2', '3', '4', (byte) 0xa3, (byte) 0xe0, (byte) 0xe3, (byte) 0x9b};
+ decoder.process(data);
+ assertThat(frames).isEmpty();
+ decoder.process(Protocol.FLAG);
+
+ assertThat(frames).hasSize(1);
+ Frame frame = frames.get(0);
+ assertThat(frame.getAddress()).isEqualTo(24);
+ assertThat(frame.getControl()).isEqualTo(50);
+ ByteBuffer payload = frame.getPayload();
+ assertThat(payload.remaining()).isEqualTo(2);
+ assertThat(payload.get()).isEqualTo((byte) 51);
+ assertThat(payload.get()).isEqualTo((byte) 52);
+ }
+
+ @Test
+ public void processTwoFrames() {
+ byte[] data = {'~',
+ '1',
+ '2',
+ '3',
+ '4',
+ (byte) 0xa3,
+ (byte) 0xe0,
+ (byte) 0xe3,
+ (byte) 0x9b,
+ '~',
+ '~',
+ '5',
+ '6',
+ '7',
+ '8',
+ 0x07,
+ 0x56,
+ 0x52,
+ 0x7d,
+ 0x7e ^ 0x20,
+ '~'};
+
+ decoder.process(data);
+
+ assertThat(frames).hasSize(2);
+ // First frame
+ Frame frame = frames.get(0);
+ assertThat(frame.getAddress()).isEqualTo(24);
+ assertThat(frame.getControl()).isEqualTo(50);
+ ByteBuffer payload = frame.getPayload();
+ assertThat(payload.remaining()).isEqualTo(2);
+ assertThat(payload.get()).isEqualTo((byte) 51);
+ assertThat(payload.get()).isEqualTo((byte) 52);
+ // Second frame
+ frame = frames.get(1);
+ assertThat(frame.getAddress()).isEqualTo(26);
+ assertThat(frame.getControl()).isEqualTo(54);
+ payload = frame.getPayload();
+ assertThat(payload.remaining()).isEqualTo(2);
+ assertThat(payload.get()).isEqualTo((byte) 55);
+ assertThat(payload.get()).isEqualTo((byte) 56);
+ }
+} \ No newline at end of file
diff --git a/pw_hdlc/java/test/dev/pigweed/pw_hdlc/EncoderTest.java b/pw_hdlc/java/test/dev/pigweed/pw_hdlc/EncoderTest.java
new file mode 100644
index 000000000..4617e6193
--- /dev/null
+++ b/pw_hdlc/java/test/dev/pigweed/pw_hdlc/EncoderTest.java
@@ -0,0 +1,328 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_hdlc;
+
+import static com.google.common.truth.Truth.assertThat;
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import org.junit.Test;
+
+public class EncoderTest {
+ private static final int OUTPUT_SIZE = 1024;
+ private static final byte UNNUMBERED_CONTROL = 0x03;
+ private static final byte ADDRESS = 0x7B;
+ private static final byte ENCODED_ADDRESS = (byte) ((ADDRESS << 1) | 1);
+ private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(OUTPUT_SIZE);
+
+ @Test
+ public void emptyPayload() throws IOException {
+ ByteBuffer emptyPayload = ByteBuffer.allocate(0);
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .writeCrc(0x832d343f)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(ADDRESS, emptyPayload, outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void oneBytePayload() throws IOException {
+ byte[] payload = {'A'};
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .write((byte) 'A')
+ .writeCrc(0x653c9e82)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void oneBytePayload_escape0x7d() throws IOException {
+ byte[] payload = {0x7d};
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .write(Protocol.ESCAPE)
+ .write((byte) (0x7d ^ 0x20))
+ .writeCrc(0x4a53e205)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void oneBytePayload_escape0x7e() throws IOException {
+ byte[] payload = {0x7e};
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .write(Protocol.ESCAPE)
+ .write((byte) (0x7e ^ 0x20))
+ .writeCrc(0xd35ab3bf)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void addressNeedsEscaping() throws IOException {
+ byte address = 0x7d >> 1;
+ byte[] payload = {'A'};
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(Protocol.ESCAPE)
+ .write((byte) 0x5d)
+ .write(UNNUMBERED_CONTROL)
+ .write((byte) 'A')
+ .writeCrc(0x899E00D4)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(address, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void crcNeedsEscaping() throws IOException {
+ byte[] payload = {'a', 'a'};
+ // The CRC-32 of {ENCODED_ADDRESS, UNNUMBERED_CONTROL, "aa"} is 0x7ee04473, so
+ // the 0x7e must be escaped.
+ byte[] expectedCrc = {0x73, 0x44, (byte) 0xe0, 0x7d, 0x5e};
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .write(payload)
+ .write(expectedCrc)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void multiplePayloads() throws IOException {
+ byte[] payload1 = {'A', 'B', 'C'};
+ byte[] payload2 = {'D', 'E', 'F'};
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .write(payload1)
+ .writeCrc(0x72410ee4)
+ .writeFlag()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .write(payload2)
+ .writeCrc(0x4ba1ae47)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload1), outputStream);
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload2), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void payloadWithNoEscapes() throws IOException {
+ byte[] payload = "1995 toyota corolla".getBytes(US_ASCII);
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .write(payload)
+ .writeCrc(0x53ee911c)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void multiByteAddress() throws IOException {
+ long address = 0x3fff;
+ byte[] encodedAddress = {(byte) 0xfe, (byte) 0xff};
+ byte[] payload = "abc".getBytes(US_ASCII);
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(encodedAddress)
+ .write(UNNUMBERED_CONTROL)
+ .write(payload)
+ .writeCrc(0x8cee2978)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(address, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void payloadWithMultipleEscapes() throws IOException {
+ byte[] payload = {0x7E, 0x7B, 0x61, 0x62, 0x63, 0x7D, 0x7E};
+ byte[] escapedPayload = {0x7D, 0x5E, 0x7B, 0x61, 0x62, 0x63, 0x7D, 0x5D, 0x7D, 0x5E};
+ byte[] expectedResult = new ExpectedValueBuilder()
+ .writeFlag()
+ .write(ENCODED_ADDRESS)
+ .write(UNNUMBERED_CONTROL)
+ .write(escapedPayload)
+ .writeCrc(0x1563a4e6)
+ .writeFlag()
+ .build();
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void writeFrame() throws IOException {
+ byte[] payload = {5, (byte) 0xab, 0x42, 0x24, (byte) 0xf9, 0x54, (byte) 0xfb, 0x3d};
+ byte[] expectedResult =
+ new ExpectedValueBuilder().writeFlag().write(payload).writeFlag().build();
+ Frame frame = Frame.parse(ByteBuffer.wrap(payload));
+
+ Encoder.writeFrame(frame, outputStream);
+
+ assertThat(outputStream.toByteArray()).isEqualTo(expectedResult);
+ }
+
+ @Test
+ public void getUiFrameEncodedSize_empty() throws IOException {
+ byte[] payload = {};
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(Encoder.getUiFrameEncodedSize(ADDRESS, payload)).isEqualTo(outputStream.size());
+ }
+
+ @Test
+ public void getUiFrameEncodedSize_oneByte() throws IOException {
+ byte[] payload = {0x61};
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(Encoder.getUiFrameEncodedSize(ADDRESS, payload)).isEqualTo(outputStream.size());
+ }
+
+ @Test
+ public void getUiFrameEncodedSize_multipleEscapes() throws IOException {
+ byte[] payload = {0x7E, 0x7B, 0x61, 0x62, 0x63, 0x7D, 0x7E};
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(Encoder.getUiFrameEncodedSize(ADDRESS, payload)).isEqualTo(outputStream.size());
+ }
+
+ @Test
+ public void getMaxPayloadBytesInUiFrameOfSize_emptyPayload() {
+ byte[] payload = {};
+
+ assertThat(Encoder.getMaxPayloadBytesInUiFrameOfSize(ADDRESS, payload, 1000)).isEqualTo(0);
+ }
+
+ @Test
+ public void getMaxPayloadBytesInUiFrameOfSize_emptyFrame() {
+ byte[] payload = {1, 2, 3, 4};
+
+ assertThat(Encoder.getMaxPayloadBytesInUiFrameOfSize(ADDRESS, payload, 0)).isEqualTo(0);
+ }
+
+ @Test
+ public void getMaxPayloadBytesInUiFrameOfSize_exactFit() throws IOException {
+ byte[] payload = {0x7E, 0x7B, 0x61, 0x62, 0x63, 0x7D, 0x7E};
+ final int requiredReservedSize = Encoder.getUiFrameEncodedSize(ADDRESS, payload);
+
+ Encoder.writeUiFrame(ADDRESS, ByteBuffer.wrap(payload), outputStream);
+
+ assertThat(Encoder.getMaxPayloadBytesInUiFrameOfSize(ADDRESS, payload, requiredReservedSize))
+ .isEqualTo(payload.length);
+ // Not enough room for the last payload byte, which is escaped, so needs two bytes.
+ assertThat(
+ Encoder.getMaxPayloadBytesInUiFrameOfSize(ADDRESS, payload, requiredReservedSize - 1))
+ .isEqualTo(payload.length - 1);
+ assertThat(
+ Encoder.getMaxPayloadBytesInUiFrameOfSize(ADDRESS, payload, requiredReservedSize - 2))
+ .isEqualTo(payload.length - 1);
+ }
+
+ @Test
+ public void maxPayloadBytesInUiFrameOfSize_extraRoom() {
+ byte[] payload = {1, 2, 3, 4};
+
+ assertThat(Encoder.getMaxPayloadBytesInUiFrameOfSize(ADDRESS, payload, 1000))
+ .isEqualTo(payload.length);
+ }
+
+ private static class ExpectedValueBuilder {
+ private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(OUTPUT_SIZE);
+
+ public ExpectedValueBuilder writeFlag() {
+ outputStream.write(Protocol.FLAG);
+ return this;
+ }
+
+ public ExpectedValueBuilder write(byte b) {
+ outputStream.write(b);
+ return this;
+ }
+
+ public ExpectedValueBuilder write(byte[] buffer) throws IOException {
+ outputStream.write(buffer);
+ return this;
+ }
+
+ public ExpectedValueBuilder writeCrc(int crc) {
+ outputStream.write(0xFF & crc);
+ outputStream.write(0xFF & (crc >> 8));
+ outputStream.write(0xFF & (crc >> 16));
+ outputStream.write(0xFF & (crc >> 24));
+ return this;
+ }
+
+ byte[] build() {
+ return outputStream.toByteArray();
+ }
+ }
+} \ No newline at end of file
diff --git a/pw_hdlc/java/test/dev/pigweed/pw_hdlc/FrameTest.java b/pw_hdlc/java/test/dev/pigweed/pw_hdlc/FrameTest.java
new file mode 100644
index 000000000..12084263d
--- /dev/null
+++ b/pw_hdlc/java/test/dev/pigweed/pw_hdlc/FrameTest.java
@@ -0,0 +1,53 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_hdlc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.nio.ByteBuffer;
+import org.junit.Test;
+
+public class FrameTest {
+ @Test
+ public void fields() {
+ byte[] payload = {5, (byte) 0xab, 0x42, 0x24, (byte) 0xf9, 0x54, (byte) 0xfb, 0x3d};
+ byte[] data = {0x42, 0x24};
+
+ Frame frame = Frame.parse(ByteBuffer.wrap(payload));
+
+ assertThat(frame.getAddress()).isEqualTo(2);
+ assertThat(frame.getControl()).isEqualTo((byte) 0xab);
+ assertThat(frame.getPayload()).isEqualTo(ByteBuffer.wrap(data));
+ }
+
+ @Test
+ public void echo() {
+ byte[] payload =
+ hexStringToByteArray("0095032A070A0568656C6C6F080110101D52D0FB1425E90E478B3000DD5F709F");
+
+ Frame frame = Frame.parse(ByteBuffer.wrap(payload));
+
+ assertThat(frame.getAddress()).isEqualTo(9472);
+ assertThat(frame.getControl()).isEqualTo(3);
+ }
+
+ private static byte[] hexStringToByteArray(String s) {
+ byte[] result = new byte[s.length() / 2];
+ for (int i = 0; i < s.length(); i += 2) {
+ result[i / 2] = (byte) Integer.parseInt(s.substring(i, i + 2), 16);
+ }
+ return result;
+ }
+} \ No newline at end of file
diff --git a/pw_hdlc/public/pw_hdlc/decoder.h b/pw_hdlc/public/pw_hdlc/decoder.h
index 94caf17b0..0290a2ad8 100644
--- a/pw_hdlc/public/pw_hdlc/decoder.h
+++ b/pw_hdlc/public/pw_hdlc/decoder.h
@@ -22,6 +22,7 @@
#include "pw_assert/assert.h"
#include "pw_bytes/span.h"
#include "pw_checksum/crc32.h"
+#include "pw_hdlc/internal/protocol.h"
#include "pw_result/result.h"
#include "pw_status/status.h"
@@ -30,19 +31,11 @@ namespace pw::hdlc {
// Represents the contents of an HDLC frame -- the unescaped data between two
// flag bytes. Instances of Frame are only created when a full, valid frame has
// been read.
-//
-// For now, the Frame class assumes a single-byte control field and a 32-bit
-// frame check sequence (FCS).
class Frame {
- private:
- static constexpr size_t kMinimumAddressSize = 1;
- static constexpr size_t kControlSize = 1;
- static constexpr size_t kFcsSize = sizeof(uint32_t);
-
public:
// The minimum size of a frame, excluding control bytes (flag or escape).
- static constexpr size_t kMinSizeBytes =
- kMinimumAddressSize + kControlSize + kFcsSize;
+ static constexpr size_t kMinContentSizeBytes =
+ kMinAddressSize + kControlSize + kFcsSize;
static Result<Frame> Parse(ConstByteSpan frame);
@@ -95,6 +88,17 @@ class Decoder {
//
Result<Frame> Process(std::byte new_byte);
+ // Returns the buffer space required for a `Decoder` to successfully decode a
+ // frame whose on-the-wire HDLC encoded size does not exceed `max_frame_size`.
+ static constexpr size_t RequiredBufferSizeForFrameSize(
+ size_t max_frame_size) {
+ // Flag bytes aren't stored in the internal buffer, so we can save a couple
+ // bytes.
+ return max_frame_size < Frame::kMinContentSizeBytes
+ ? Frame::kMinContentSizeBytes
+ : max_frame_size - 2;
+ }
+
// Processes a span of data and calls the provided callback with each frame or
// error.
template <typename F, typename... Args>
@@ -115,7 +119,7 @@ class Decoder {
void Clear() {
state_ = State::kInterFrame;
Reset();
- };
+ }
private:
// State enum class is used to make the Decoder a finite state machine.
@@ -165,7 +169,7 @@ class DecoderBuffer : public Decoder {
static constexpr size_t max_size() { return kSizeBytes; }
private:
- static_assert(kSizeBytes >= Frame::kMinSizeBytes);
+ static_assert(kSizeBytes >= Frame::kMinContentSizeBytes);
std::array<std::byte, kSizeBytes> frame_buffer_;
};
diff --git a/pw_hdlc/public/pw_hdlc/encoded_size.h b/pw_hdlc/public/pw_hdlc/encoded_size.h
new file mode 100644
index 000000000..ca7d92a8b
--- /dev/null
+++ b/pw_hdlc/public/pw_hdlc/encoded_size.h
@@ -0,0 +1,92 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <type_traits>
+
+#include "pw_bytes/span.h"
+#include "pw_hdlc/internal/protocol.h"
+#include "pw_span/span.h"
+#include "pw_varint/varint.h"
+
+namespace pw::hdlc {
+
+// Calculates the size of a series of bytes after HDLC escaping.
+constexpr size_t EscapedSize(ConstByteSpan data) {
+ size_t count = 0;
+ for (std::byte b : data) {
+ count += NeedsEscaping(b) ? 2 : 1;
+ }
+ return count;
+}
+
+template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
+constexpr size_t EscapedSize(T val) {
+ return EscapedSize(as_bytes(span(&val, 1)));
+}
+
+// Calculate the buffer space required for encoding an HDLC frame with a given
+// payload size. This uses worst-case escaping cost as part of the calculation,
+// which is extremely expensive but guarantees the payload will always fit in
+// the frame AND the value can be evaluated at compile-time.
+//
+// This is NOT a perfect inverse of MaxSafePayloadSize()! This is because
+// increasing the frame size by one doesn't mean another payload byte can safely
+// fit since it might need to be escaped.
+constexpr size_t MaxEncodedFrameSize(size_t max_payload_size) {
+ return 2 * sizeof(kFlag) + kMaxEscapedVarintAddressSize + kMaxEscapedFcsSize +
+ kMaxEscapedControlSize + max_payload_size * 2;
+}
+
+// Calculates a maximum the on-the-wire encoded size for a given payload
+// destined for the provided address. Because payload escaping and some of the
+// address is accounted for, there's significantly less wasted space when
+// compared to MaxEncodedFrameSize(). However, because the checksum, address,
+// and control fields are not precisely calculated, there's up to 17 bytes of
+// potentially unused overhead left over by this estimation. This is done to
+// improve the speed of this calculation, since precisely calculating all of
+// this information isn't nearly as efficient.
+constexpr size_t MaxEncodedFrameSize(uint64_t address, ConstByteSpan payload) {
+ // The largest on-the-wire escaped varint will never exceed
+ // kMaxEscapedVarintAddressSize since the 10th varint byte can never be an
+ // byte that needs escaping.
+ const size_t max_encoded_address_size =
+ std::min(varint::EncodedSize(address) * 2, kMaxEscapedVarintAddressSize);
+ return 2 * sizeof(kFlag) + max_encoded_address_size + kMaxEscapedFcsSize +
+ kMaxEscapedControlSize + EscapedSize(payload);
+}
+
+// Calculates the maximum safe payload size of an HDLC-encoded frame. As long as
+// a payload does not exceed this value, it will always be safe to encode it
+// as an HDLC frame in a buffer of size max_frame_size.
+//
+// When max_frame_size is too small to safely fit any payload data, this
+// function returns zero.
+//
+// This is NOT a perfect inverse of MaxEncodedFrameSize()! This is because
+// increasing the frame size by one doesn't mean another payload byte can safely
+// fit since it might need to be escaped.
+constexpr size_t MaxSafePayloadSize(size_t max_frame_size) {
+ constexpr size_t kMaxConstantOverhead =
+ 2 * sizeof(kFlag) + kMaxEscapedVarintAddressSize + kMaxEscapedFcsSize +
+ kMaxEscapedControlSize;
+ return max_frame_size < kMaxConstantOverhead
+ ? 0
+ : (max_frame_size - kMaxConstantOverhead) / 2;
+}
+
+} // namespace pw::hdlc
diff --git a/pw_hdlc/public/pw_hdlc/encoder.h b/pw_hdlc/public/pw_hdlc/encoder.h
index 8dcf66fb7..8cd58379a 100644
--- a/pw_hdlc/public/pw_hdlc/encoder.h
+++ b/pw_hdlc/public/pw_hdlc/encoder.h
@@ -23,10 +23,10 @@ namespace pw::hdlc {
// writer. The complete frame contains the following:
//
// - HDLC flag byte (0x7e)
-// - Address
+// - Address (variable length, up to 10 bytes)
// - UI-frame control (metadata) byte
// - Payload (0 or more bytes)
-// - Frame check sequence (CRC-32)
+// - Frame check sequence (CRC-32, 4 bytes)
// - HDLC flag byte (0x7e)
//
Status WriteUIFrame(uint64_t address,
diff --git a/pw_hdlc/public/pw_hdlc/internal/encoder.h b/pw_hdlc/public/pw_hdlc/internal/encoder.h
index 2f3b4b0a6..9fce72255 100644
--- a/pw_hdlc/public/pw_hdlc/internal/encoder.h
+++ b/pw_hdlc/public/pw_hdlc/internal/encoder.h
@@ -13,8 +13,13 @@
// the License.
#pragma once
+#include <cstddef>
+#include <cstdint>
+
+#include "pw_bytes/span.h"
#include "pw_checksum/crc32.h"
#include "pw_hdlc/internal/protocol.h"
+#include "pw_status/status.h"
#include "pw_stream/stream.h"
namespace pw::hdlc::internal {
@@ -43,10 +48,6 @@ class Encoder {
// Finishes a frame. Writes the frame check sequence and a terminating flag.
Status FinishFrame();
- // Runs a pass through a payload, returning the worst-case encoded size for a
- // frame containing it. Does not calculate CRC to improve efficiency.
- static size_t MaxEncodedSize(uint64_t address, ConstByteSpan payload);
-
private:
// Indicates this an information packet with sequence numbers set to 0.
static constexpr std::byte kUnusedControl = std::byte{0};
diff --git a/pw_hdlc/public/pw_hdlc/internal/protocol.h b/pw_hdlc/public/pw_hdlc/internal/protocol.h
index f11b34fab..e471c9140 100644
--- a/pw_hdlc/public/pw_hdlc/internal/protocol.h
+++ b/pw_hdlc/public/pw_hdlc/internal/protocol.h
@@ -28,6 +28,37 @@ inline constexpr std::array<std::byte, 2> kEscapedFlag = {kEscape,
inline constexpr std::array<std::byte, 2> kEscapedEscape = {kEscape,
std::byte{0x5D}};
+// This implementation also only supports addresses up to the maximum value of a
+// 64-bit unsigned integer.
+inline constexpr size_t kMinAddressSize = 1;
+inline constexpr size_t kMaxAddressSize =
+ varint::EncodedSize(std::numeric_limits<uint64_t>::max());
+
+// The biggest on-the-wire size of a FCS after HDLC escaping. If, for example,
+// the FCS is 0x7e7e7e7e the encoded value will be:
+// [ 0x7e, 0x5e, 0x7e, 0x5e, 0x7e, 0x5e, 0x7e, 0x5e ]
+inline constexpr size_t kMaxEscapedFcsSize = 8;
+
+// The biggest on-the-wire size of an HDLC address after escaping. This number
+// is not just kMaxAddressSize * 2 because the kFlag and kEscape bytes are not
+// valid terminating bytes of a one-terminated least significant varint. This
+// means that the practical biggest varint BEFORE escaping looks like this:
+// [ 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x7e, 0x03 ]
+// The first nine bytes will be escaped, but the tenth never will be escaped
+// since any values other than 0x03 would cause a uint64_t to overflow.
+inline constexpr size_t kMaxEscapedVarintAddressSize =
+ (kMaxAddressSize * 2) - 1;
+
+// This implementation does not support extended control fields.
+inline constexpr size_t kControlSize = 1;
+
+// The control byte may need to be escaped. When it is, this is its maximum
+// size.
+inline constexpr size_t kMaxEscapedControlSize = 2;
+
+// This implementation only supports a 32-bit CRC-32 Frame Check Sequence (FCS).
+inline constexpr size_t kFcsSize = sizeof(uint32_t);
+
inline constexpr varint::Format kAddressFormat =
varint::Format::kOneTerminatedLeastSignificant;
diff --git a/pw_hdlc/public/pw_hdlc/rpc_channel.h b/pw_hdlc/public/pw_hdlc/rpc_channel.h
index 7f0cbf246..045c3c1c5 100644
--- a/pw_hdlc/public/pw_hdlc/rpc_channel.h
+++ b/pw_hdlc/public/pw_hdlc/rpc_channel.h
@@ -14,11 +14,12 @@
#pragma once
#include <array>
-#include <span>
#include "pw_assert/assert.h"
+#include "pw_hdlc/encoded_size.h"
#include "pw_hdlc/encoder.h"
#include "pw_rpc/channel.h"
+#include "pw_span/span.h"
#include "pw_stream/stream.h"
namespace pw::hdlc {
@@ -38,7 +39,7 @@ class RpcChannelOutput : public rpc::ChannelOutput {
const char* channel_name)
: ChannelOutput(channel_name), writer_(writer), address_(address) {}
- Status Send(std::span<const std::byte> buffer) override {
+ Status Send(span<const std::byte> buffer) override {
return hdlc::WriteUIFrame(address_, buffer, writer_);
}
@@ -47,4 +48,34 @@ class RpcChannelOutput : public rpc::ChannelOutput {
const uint64_t address_;
};
+// A RpcChannelOutput that ensures all packets produced by pw_rpc will safely
+// fit in the specified MTU after HDLC encoding.
+template <size_t kMaxTransmissionUnit>
+class FixedMtuChannelOutput : public RpcChannelOutput {
+ public:
+ constexpr FixedMtuChannelOutput(stream::Writer& writer,
+ uint64_t address,
+ const char* channel_name)
+ : RpcChannelOutput(writer, address, channel_name) {}
+
+ // Provide a constexpr helper for the maximum safe payload size.
+ static constexpr size_t MaxSafePayloadSize() {
+ static_assert(rpc::cfg::kEncodingBufferSizeBytes <=
+ hdlc::MaxSafePayloadSize(kMaxTransmissionUnit),
+ "pw_rpc's encode buffer is large enough to produce HDLC "
+ "frames that will exceed the bounds of this channel's MTU");
+ static_assert(
+ rpc::MaxSafePayloadSize(
+ hdlc::MaxSafePayloadSize(kMaxTransmissionUnit)) > 0,
+ "The combined MTU and RPC encode buffer size do not afford enough "
+ "space for any RPC payload data to safely be encoded into RPC packets");
+ return rpc::MaxSafePayloadSize(
+ hdlc::MaxSafePayloadSize(kMaxTransmissionUnit));
+ }
+
+ // Users of pw_rpc should only care about the maximum payload size, despite
+ // this being labeled as a MTU.
+ size_t MaximumTransmissionUnit() override { return MaxSafePayloadSize(); }
+};
+
} // namespace pw::hdlc
diff --git a/pw_hdlc/public/pw_hdlc/rpc_packets.h b/pw_hdlc/public/pw_hdlc/rpc_packets.h
index d22137c99..d52ef2c5e 100644
--- a/pw_hdlc/public/pw_hdlc/rpc_packets.h
+++ b/pw_hdlc/public/pw_hdlc/rpc_packets.h
@@ -27,8 +27,7 @@ inline constexpr uint8_t kDefaultRpcAddress = 'R';
// Reads HDLC frames with sys_io::ReadByte, using decode_buffer to store frames.
// HDLC frames sent to rpc_address are passed to the RPC server.
Status ReadAndProcessPackets(rpc::Server& server,
- rpc::ChannelOutput& output,
- std::span<std::byte> decode_buffer,
+ span<std::byte> decode_buffer,
unsigned rpc_address = kDefaultRpcAddress);
} // namespace pw::hdlc
diff --git a/pw_hdlc/py/BUILD.gn b/pw_hdlc/py/BUILD.gn
index 8e9c157ff..5e8b4c03c 100644
--- a/pw_hdlc/py/BUILD.gn
+++ b/pw_hdlc/py/BUILD.gn
@@ -35,10 +35,14 @@ pw_python_package("py") {
"encode_test.py",
]
python_deps = [
- "$dir_pw_console/py",
"$dir_pw_protobuf_compiler/py",
"$dir_pw_rpc/py",
]
- python_test_deps = [ "$dir_pw_build/py" ]
+ python_test_deps = [
+ "$dir_pw_build/py",
+ "$dir_pw_log:protos.python",
+ "$dir_pw_tokenizer/py:test_proto.python",
+ ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_hdlc/py/decode_test.py b/pw_hdlc/py/decode_test.py
index f230ad2ef..db0b3fabe 100755
--- a/pw_hdlc/py/decode_test.py
+++ b/pw_hdlc/py/decode_test.py
@@ -14,12 +14,19 @@
# the License.
"""Contains the Python decoder tests and generates C++ decoder tests."""
-from typing import Iterator, List, NamedTuple, Tuple, Union
+import queue
+from typing import Iterator, List, NamedTuple, Optional, Tuple, Union
import unittest
from pw_build.generated_tests import Context, PyTest, TestGenerator, GroupOrTest
from pw_build.generated_tests import parse_test_generation_args
-from pw_hdlc.decode import Frame, FrameDecoder, FrameStatus, NO_ADDRESS
+from pw_hdlc.decode import (
+ Frame,
+ FrameDecoder,
+ FrameAndNonFrameDecoder,
+ FrameStatus,
+ NO_ADDRESS,
+)
from pw_hdlc.protocol import frame_check_sequence as fcs
from pw_hdlc.protocol import encode_address
@@ -27,9 +34,9 @@ from pw_hdlc.protocol import encode_address
def _encode(address: int, control: int, data: bytes) -> bytes:
frame = encode_address(address) + bytes([control]) + data
frame += fcs(frame)
- frame = frame.replace(b'\x7d', b'\x7d\x5d')
- frame = frame.replace(b'\x7e', b'\x7d\x5e')
- return b''.join([b'\x7e', frame, b'\x7e'])
+ frame = frame.replace(b'}', b'}\x5d')
+ frame = frame.replace(b'~', b'}\x5e')
+ return b''.join([b'~', frame, b'~'])
class Expected(NamedTuple):
@@ -45,8 +52,12 @@ class Expected(NamedTuple):
def __eq__(self, other) -> bool:
"""Define == so an Expected and a Frame can be compared."""
- return (self.address == other.address and self.control == other.control
- and self.data == other.data and self.status is other.status)
+ return (
+ self.address == other.address
+ and self.control == other.control
+ and self.data == other.data
+ and self.status is other.status
+ )
class ExpectedRaw(NamedTuple):
@@ -55,137 +66,283 @@ class ExpectedRaw(NamedTuple):
def __eq__(self, other) -> bool:
"""Define == so an ExpectedRaw and a Frame can be compared."""
- return (self.raw_encoded == other.raw_encoded
- and self.status is other.status)
+ return (
+ self.raw_encoded == other.raw_encoded
+ and self.status is other.status
+ )
+
+
+class TestCase(NamedTuple):
+ data: bytes
+ frames: List[Union[Expected, ExpectedRaw]]
+ raw_data: bytes
-Expectation = Union[Expected, ExpectedRaw]
+def case(data: bytes, frames: list, raw: Optional[bytes] = None) -> TestCase:
+ """Creates a TestCase, filling in the default value for the raw bytes."""
+ if raw is not None:
+ return TestCase(data, frames, raw)
+ if not frames or all(f.status is not FrameStatus.OK for f in frames):
+ return TestCase(data, frames, data)
+ if all(f.status is FrameStatus.OK for f in frames):
+ return TestCase(data, frames, b'')
+ raise AssertionError(
+ f'Must specify expected non-frame data for this test case ({data=})!'
+ )
+
_PARTIAL = fcs(b'\x0ACmsg\x5e')
-_ESCAPED_FLAG_TEST_CASE = (
- b'\x7e\x0ACmsg\x7d\x7e' + _PARTIAL + b'\x7e',
+_ESCAPED_FLAG_TEST_CASE = case(
+ b'~\x0ACmsg}~' + _PARTIAL + b'~',
[
Expected.error(FrameStatus.FRAMING_ERROR),
Expected.error(FrameStatus.FRAMING_ERROR),
],
)
-TEST_CASES: Tuple[GroupOrTest[Tuple[bytes, List[Expectation]]], ...] = (
+# Test cases are a tuple with the following elements:
+#
+# - raw data stream
+# - expected valid & invalid frames
+# - [optional] expected raw, non-HDLC data; defaults to the full raw data
+# stream if no valid frames are expected, or b'' if only valid frames are
+# expected
+#
+# These tests are executed twice: once for the standard HDLC decoder, and a
+# second time for the FrameAndNonFrameDecoder. The FrameAndNonFrameDecoder tests
+# flush the non-frame data to simulate a timeout or MTU overflow, so the
+# expected raw data includes all bytes not in an HDLC frame.
+TEST_CASES: Tuple[GroupOrTest[TestCase], ...] = (
'Empty payload',
- (_encode(0, 0, b''), [Expected(0, b'\0', b'')]),
- (_encode(55, 0x99, b''), [Expected(55, b'\x99', b'')]),
- (_encode(55, 0x99, b'') * 3, [Expected(55, b'\x99', b'')] * 3),
+ case(_encode(0, 0, b''), [Expected(0, b'\0', b'')]),
+ case(_encode(55, 0x99, b''), [Expected(55, b'\x99', b'')]),
+ case(_encode(55, 0x99, b'') * 3, [Expected(55, b'\x99', b'')] * 3),
'Simple one-byte payload',
- (_encode(0, 0, b'\0'), [Expected(0, b'\0', b'\0')]),
- (_encode(123, 0, b'A'), [Expected(123, b'\0', b'A')]),
+ case(_encode(0, 0, b'\0'), [Expected(0, b'\0', b'\0')]),
+ case(_encode(123, 0, b'A'), [Expected(123, b'\0', b'A')]),
'Simple multi-byte payload',
- (_encode(0, 0, b'Hello, world!'), [Expected(0, b'\0', b'Hello, world!')]),
- (_encode(123, 0, b'\0\0\1\0\0'), [Expected(123, b'\0', b'\0\0\1\0\0')]),
+ case(
+ _encode(0, 0, b'Hello, world!'), [Expected(0, b'\0', b'Hello, world!')]
+ ),
+ case(_encode(123, 0, b'\0\0\1\0\0'), [Expected(123, b'\0', b'\0\0\1\0\0')]),
'Escaped one-byte payload',
- (_encode(1, 2, b'\x7e'), [Expected(1, b'\2', b'\x7e')]),
- (_encode(1, 2, b'\x7d'), [Expected(1, b'\2', b'\x7d')]),
- (_encode(1, 2, b'\x7e') + _encode(1, 2, b'\x7d'),
- [Expected(1, b'\2', b'\x7e'),
- Expected(1, b'\2', b'\x7d')]),
+ case(_encode(1, 2, b'~'), [Expected(1, b'\2', b'~')]),
+ case(_encode(1, 2, b'}'), [Expected(1, b'\2', b'}')]),
+ case(
+ _encode(1, 2, b'~') + _encode(1, 2, b'}'),
+ [Expected(1, b'\2', b'~'), Expected(1, b'\2', b'}')],
+ ),
'Escaped address',
- (_encode(0x7e, 0, b'A'), [Expected(0x7e, b'\0', b'A')]),
- (_encode(0x7d, 0, b'B'), [Expected(0x7d, b'\0', b'B')]),
+ case(_encode(0x7E, 0, b'A'), [Expected(0x7E, b'\0', b'A')]),
+ case(_encode(0x7D, 0, b'B'), [Expected(0x7D, b'\0', b'B')]),
'Escaped control',
- (_encode(0, 0x7e, b'C'), [Expected(0, b'\x7e', b'C')]),
- (_encode(0, 0x7d, b'D'), [Expected(0, b'\x7d', b'D')]),
+ case(_encode(0, 0x7E, b'C'), [Expected(0, b'~', b'C')]),
+ case(_encode(0, 0x7D, b'D'), [Expected(0, b'}', b'D')]),
'Escaped address and control',
- (_encode(0x7e, 0x7d, b'E'), [Expected(0x7e, b'\x7d', b'E')]),
- (_encode(0x7d, 0x7e, b'F'), [Expected(0x7d, b'\x7e', b'F')]),
- (_encode(0x7e, 0x7e, b'\x7e'), [Expected(0x7e, b'\x7e', b'\x7e')]),
+ case(_encode(0x7E, 0x7D, b'E'), [Expected(0x7E, b'}', b'E')]),
+ case(_encode(0x7D, 0x7E, b'F'), [Expected(0x7D, b'~', b'F')]),
+ case(_encode(0x7E, 0x7E, b'~'), [Expected(0x7E, b'~', b'~')]),
'Multibyte address',
- (_encode(128, 0, b'big address'), [Expected(128, b'\0', b'big address')]),
- (_encode(0xffffffff, 0, b'\0\0\1\0\0'),
- [Expected(0xffffffff, b'\0', b'\0\0\1\0\0')]),
+ case(
+ _encode(128, 0, b'big address'), [Expected(128, b'\0', b'big address')]
+ ),
+ case(
+ _encode(0xFFFFFFFF, 0, b'\0\0\1\0\0'),
+ [Expected(0xFFFFFFFF, b'\0', b'\0\0\1\0\0')],
+ ),
'Multiple frames separated by single flag',
- (_encode(0, 0, b'A')[:-1] + _encode(1, 2, b'123'),
- [Expected(0, b'\0', b'A'),
- Expected(1, b'\2', b'123')]),
- (_encode(0xff, 0, b'Yo')[:-1] * 3 + b'\x7e',
- [Expected(0xff, b'\0', b'Yo')] * 3),
- 'Ignore empty frames',
- (b'\x7e\x7e', []),
- (b'\x7e' * 10, []),
- (b'\x7e\x7e' + _encode(1, 2, b'3') + b'\x7e' * 5,
- [Expected(1, b'\2', b'3')]),
- (b'\x7e' * 10 + _encode(1, 2, b':O') + b'\x7e' * 3 + _encode(3, 4, b':P'),
- [Expected(1, b'\2', b':O'),
- Expected(3, b'\4', b':P')]),
+ case(
+ _encode(0, 0, b'A')[:-1] + _encode(1, 2, b'123'),
+ [Expected(0, b'\0', b'A'), Expected(1, b'\2', b'123')],
+ ),
+ case(
+ _encode(0xFF, 0, b'Yo')[:-1] * 3 + b'~',
+ [Expected(0xFF, b'\0', b'Yo')] * 3,
+ ),
+ 'Empty frames produce framing errors with raw data',
+ case(b'~~', [ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR)], b'~~'),
+ case(
+ b'~' * 10,
+ [
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ],
+ ),
+ case(
+ b'~~' + _encode(1, 2, b'3') + b'~' * 5,
+ [
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ Expected(1, b'\2', b'3'),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ # One flag byte remains in the decoding state machine.
+ ],
+ b'~~~~~~~',
+ ),
+ case(
+ b'~' * 10 + _encode(1, 2, b':O') + b'~' * 3 + _encode(3, 4, b':P'),
+ [
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ Expected(1, b'\2', b':O'),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ Expected(3, b'\4', b':P'),
+ ],
+ b'~' * 13,
+ ),
'Cannot escape flag',
- (b'\x7e\xAA\x7d\x7e\xab\x00Hello' + fcs(b'\xab\0Hello') + b'\x7e', [
- Expected.error(FrameStatus.FRAMING_ERROR),
- Expected(0x55, b'\0', b'Hello'),
- ]),
+ case(
+ b'~\xAA}~\xab\x00Hello' + fcs(b'\xab\0Hello') + b'~',
+ [
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ Expected(0x55, b'\0', b'Hello'),
+ ],
+ b'~\xAA}',
+ ),
_ESCAPED_FLAG_TEST_CASE,
'Frame too short',
- (b'\x7e1\x7e', [Expected.error(FrameStatus.FRAMING_ERROR)]),
- (b'\x7e12\x7e', [Expected.error(FrameStatus.FRAMING_ERROR)]),
- (b'\x7e12345\x7e', [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'~1~', [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'~12~', [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'~12345~', [Expected.error(FrameStatus.FRAMING_ERROR)]),
'Multibyte address too long',
- (_encode(2 ** 100, 0, b'too long'),
- [Expected.error(FrameStatus.BAD_ADDRESS)]),
+ case(
+ _encode(2**100, 0, b'too long'),
+ [Expected.error(FrameStatus.BAD_ADDRESS)],
+ ),
'Incorrect frame check sequence',
- (b'\x7e123456\x7e', [Expected.error(FrameStatus.FCS_MISMATCH)]),
- (b'\x7e\1\2msg\xff\xff\xff\xff\x7e',
- [Expected.error(FrameStatus.FCS_MISMATCH)]),
- (_encode(0xA, 0xB, b'???')[:-2] + _encode(1, 2, b'def'), [
- Expected.error(FrameStatus.FCS_MISMATCH),
- Expected(1, b'\2', b'def'),
- ]),
+ case(b'~123456~', [Expected.error(FrameStatus.FCS_MISMATCH)]),
+ case(
+ b'~\1\2msg\xff\xff\xff\xff~', [Expected.error(FrameStatus.FCS_MISMATCH)]
+ ),
+ case(
+ _encode(0xA, 0xB, b'???')[:-2] + _encode(1, 2, b'def'),
+ [
+ Expected.error(FrameStatus.FCS_MISMATCH),
+ Expected(1, b'\2', b'def'),
+ ],
+ _encode(0xA, 0xB, b'???')[:-2],
+ ),
'Invalid escape in address',
- (b'\x7e\x7d\x7d\0' + fcs(b'\x5d\0') + b'\x7e',
- [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(
+ b'~}}\0' + fcs(b'\x5d\0') + b'~',
+ [Expected.error(FrameStatus.FRAMING_ERROR)],
+ ),
'Invalid escape in control',
- (b'\x7e\0\x7d\x7d' + fcs(b'\0\x5d') + b'\x7e',
- [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(
+ b'~\0}}' + fcs(b'\0\x5d') + b'~',
+ [Expected.error(FrameStatus.FRAMING_ERROR)],
+ ),
'Invalid escape in data',
- (b'\x7e\0\1\x7d\x7d' + fcs(b'\0\1\x5d') + b'\x7e',
- [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(
+ b'~\0\1}}' + fcs(b'\0\1\x5d') + b'~',
+ [Expected.error(FrameStatus.FRAMING_ERROR)],
+ ),
'Frame ends with escape',
- (b'\x7e\x7d\x7e', [Expected.error(FrameStatus.FRAMING_ERROR)]),
- (b'\x7e\1\x7d\x7e', [Expected.error(FrameStatus.FRAMING_ERROR)]),
- (b'\x7e\1\2abc\x7d\x7e', [Expected.error(FrameStatus.FRAMING_ERROR)]),
- (b'\x7e\1\2abcd\x7d\x7e', [Expected.error(FrameStatus.FRAMING_ERROR)]),
- (b'\x7e\1\2abcd1234\x7d\x7e', [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'~}~', [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'~\1}~', [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'~\1\2abc}~', [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'~\1\2abcd}~', [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'~\1\2abcd1234}~', [Expected.error(FrameStatus.FRAMING_ERROR)]),
'Inter-frame data is only escapes',
- (b'\x7e\x7d\x7e\x7d\x7e', [
- Expected.error(FrameStatus.FRAMING_ERROR),
- Expected.error(FrameStatus.FRAMING_ERROR),
- ]),
- (b'\x7e\x7d\x7d\x7e\x7d\x7d\x7e', [
- Expected.error(FrameStatus.FRAMING_ERROR),
- Expected.error(FrameStatus.FRAMING_ERROR),
- ]),
+ case(
+ b'~}~}~',
+ [
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ ],
+ ),
+ case(
+ b'~}}~}}~',
+ [
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ ],
+ ),
'Data before first flag',
- (b'\0\1' + fcs(b'\0\1'), []),
- (b'\0\1' + fcs(b'\0\1') + b'\x7e',
- [Expected.error(FrameStatus.FRAMING_ERROR)]),
+ case(b'\0\1' + fcs(b'\0\1'), []),
+ case(
+ b'\0\1' + fcs(b'\0\1') + b'~',
+ [Expected.error(FrameStatus.FRAMING_ERROR)],
+ ),
'No frames emitted until flag',
- (_encode(1, 2, b'3')[:-1], []),
- (b'\x7e' + _encode(1, 2, b'3')[1:-1] * 2, []),
+ case(_encode(1, 2, b'3')[:-1], []),
+ case(b'~' + _encode(1, 2, b'3')[1:-1] * 2, []),
'Only flag and escape characters can be escaped',
- (b'\x7e\x7d\0' + _encode(1, 2, b'3'),
- [Expected.error(FrameStatus.FRAMING_ERROR),
- Expected(1, b'\2', b'3')]),
- (b'\x7e1234\x7da' + _encode(1, 2, b'3'),
- [Expected.error(FrameStatus.FRAMING_ERROR),
- Expected(1, b'\2', b'3')]),
+ case(
+ b'~}\0' + _encode(1, 2, b'3'),
+ [Expected.error(FrameStatus.FRAMING_ERROR), Expected(1, b'\2', b'3')],
+ b'~}\0',
+ ),
+ case(
+ b'~1234}a' + _encode(1, 2, b'3'),
+ [Expected.error(FrameStatus.FRAMING_ERROR), Expected(1, b'\2', b'3')],
+ b'~1234}a',
+ ),
'Invalid frame records raw data',
- (b'Hello?~', [ExpectedRaw(b'Hello?', FrameStatus.FRAMING_ERROR)]),
- (b'~~Hel\x7d\x7dlo~',
- [ExpectedRaw(b'Hel\x7d\x7dlo', FrameStatus.FRAMING_ERROR)]),
- (b'Hello?~~~~~', [ExpectedRaw(b'Hello?', FrameStatus.FRAMING_ERROR)]),
- (b'~~~~Hello?~~~~~', [ExpectedRaw(b'Hello?', FrameStatus.FCS_MISMATCH)]),
- (b'Hello?~~Goodbye~', [
- ExpectedRaw(b'Hello?', FrameStatus.FRAMING_ERROR),
- ExpectedRaw(b'Goodbye', FrameStatus.FCS_MISMATCH),
- ]),
-) # yapf: disable
-# Formatting for the above tuple is very slow, so disable yapf.
+ case(b'Hello?~', [ExpectedRaw(b'Hello?~', FrameStatus.FRAMING_ERROR)]),
+ case(
+ b'~~Hel}}lo~',
+ [
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'Hel}}lo~', FrameStatus.FRAMING_ERROR),
+ ],
+ ),
+ case(
+ b'Hello?~~~~~',
+ [
+ ExpectedRaw(b'Hello?~', FrameStatus.FRAMING_ERROR),
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ ],
+ ),
+ case(
+ b'~~~~Hello?~~~~~',
+ [
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'Hello?~', FrameStatus.FCS_MISMATCH),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~~', FrameStatus.FRAMING_ERROR),
+ ],
+ ),
+ case(
+ b'Hello?~~Goodbye~',
+ [
+ ExpectedRaw(b'Hello?~', FrameStatus.FRAMING_ERROR),
+ ExpectedRaw(b'~Goodbye~', FrameStatus.FCS_MISMATCH),
+ ],
+ ),
+ 'Valid data followed by frame followed by invalid',
+ case(
+ b'Hi~ this is a log message\r\n'
+ + _encode(0, 0, b'')
+ + b'More log messages!\r\n',
+ [
+ Expected.error(FrameStatus.FRAMING_ERROR),
+ Expected.error(FrameStatus.FCS_MISMATCH),
+ Expected(0, b'\0', b''),
+ ],
+ b'Hi~ this is a log message\r\nMore log messages!\r\n',
+ ),
+ case(
+ b'Hi~ this is a log message\r\n',
+ [Expected.error(FrameStatus.FRAMING_ERROR)],
+ ),
+ case(
+ b'~Hi~' + _encode(1, 2, b'def') + b' How are you?',
+ [Expected.error(FrameStatus.FRAMING_ERROR), Expected(1, b'\2', b'def')],
+ b'~Hi~ How are you?',
+ ),
+)
+# Formatting for the above tuple is very slow, so disable yapf. Manually enable
+# it as needed to format the test cases.
_TESTS = TestGenerator(TEST_CASES)
@@ -270,10 +427,19 @@ _TS_FOOTER = """\
"""
+def _py_only_frame(frame: Frame) -> bool:
+ """Returns true for frames only returned by the Python library"""
+ return (
+ frame.status is FrameStatus.FRAMING_ERROR and frame.raw_encoded == b'~~'
+ )
+
+
def _cpp_test(ctx: Context) -> Iterator[str]:
"""Generates a C++ test for the provided test data."""
- data, _ = ctx.test_case
- frames = list(FrameDecoder().process(data))
+ data, _, _ = ctx.test_case
+ frames = [
+ f for f in list(FrameDecoder().process(data)) if not _py_only_frame(f)
+ ]
data_bytes = ''.join(rf'\x{byte:02x}' for byte in data)
yield f'TEST(Decoder, {ctx.cc_name()}) {{'
@@ -281,10 +447,13 @@ def _cpp_test(ctx: Context) -> Iterator[str]:
for i, frame in enumerate(frames, 1):
if frame.ok() or frame.status is FrameStatus.BAD_ADDRESS:
- frame_bytes = ''.join(rf'\x{byte:02x}'
- for byte in frame.raw_decoded)
- yield (f' static constexpr auto kDecodedFrame{i:02} = '
- f'bytes::String("{frame_bytes}");')
+ frame_bytes = ''.join(
+ rf'\x{byte:02x}' for byte in frame.raw_decoded
+ )
+ yield (
+ f' static constexpr auto kDecodedFrame{i:02} = '
+ f'bytes::String("{frame_bytes}");'
+ )
else:
yield f' // Frame {i}: {frame.status.value}'
@@ -327,23 +496,73 @@ def _cpp_test(ctx: Context) -> Iterator[str]:
}}"""
-def _define_py_test(ctx: Context) -> PyTest:
- data, expected_frames = ctx.test_case
+def _define_py_decoder_test(ctx: Context) -> PyTest:
+ data, expected_frames, _ = ctx.test_case
def test(self) -> None:
+ self.maxDiff = None
# Decode in one call
- self.assertEqual(expected_frames,
- list(FrameDecoder().process(data)),
- msg=f'{ctx.group}: {data!r}')
+ self.assertEqual(
+ expected_frames,
+ list(FrameDecoder().process(data)),
+ msg=f'{ctx.group}: {data!r}',
+ )
# Decode byte-by-byte
decoder = FrameDecoder()
decoded_frames: List[Frame] = []
for i in range(len(data)):
- decoded_frames += decoder.process(data[i:i + 1])
+ decoded_frames += decoder.process(data[i : i + 1])
+
+ self.assertEqual(
+ expected_frames,
+ decoded_frames,
+ msg=f'{ctx.group} (byte-by-byte): {data!r}',
+ )
+
+ return test
+
+
+def _define_raw_decoder_py_test(ctx: Context) -> PyTest:
+ raw_data, expected_frames, expected_non_frame_data = ctx.test_case
+
+ # The non-frame data decoder only yields valid frames.
+ expected_frames = [f for f in expected_frames if f.status is FrameStatus.OK]
+
+ def test(self) -> None:
+ self.maxDiff = None
+
+ non_frame_data = bytearray()
+
+ # Decode in one call
+ decoder = FrameAndNonFrameDecoder(
+ non_frame_data_handler=non_frame_data.extend
+ )
- self.assertEqual(expected_frames,
- decoded_frames,
- msg=f'{ctx.group} (byte-by-byte): {data!r}')
+ self.assertEqual(
+ expected_frames,
+ list(decoder.process(raw_data)),
+ msg=f'{ctx.group}: {raw_data!r}',
+ )
+
+ decoder.flush_non_frame_data()
+ self.assertEqual(expected_non_frame_data, bytes(non_frame_data))
+
+ # Decode byte-by-byte
+ non_frame_data.clear()
+ decoder = FrameAndNonFrameDecoder(
+ non_frame_data_handler=non_frame_data.extend
+ )
+ decoded_frames: List[Frame] = []
+ for i in range(len(raw_data)):
+ decoded_frames += decoder.process(raw_data[i : i + 1])
+
+ self.assertEqual(
+ expected_frames,
+ decoded_frames,
+ msg=f'{ctx.group} (byte-by-byte): {raw_data!r}',
+ )
+ decoder.flush_non_frame_data()
+ self.assertEqual(expected_non_frame_data, bytes(non_frame_data))
return test
@@ -354,8 +573,10 @@ def _ts_byte_array(data: bytes) -> str:
def _ts_test(ctx: Context) -> Iterator[str]:
"""Generates a TS test for the provided test data."""
- data, _ = ctx.test_case
- frames = list(FrameDecoder().process(data))
+ data, _, _ = ctx.test_case
+ frames = [
+ f for f in list(FrameDecoder().process(data)) if not _py_only_frame(f)
+ ]
data_bytes = _ts_byte_array(data)
yield f' it(\'{ctx.ts_name()}\', () => {{'
@@ -367,9 +588,11 @@ def _ts_test(ctx: Context) -> Iterator[str]:
frame_bytes = _ts_byte_array(frame.data)
if frame is Expected:
- yield (f' new Expected({frame.address}, '
- f'new Uint8Array({control_bytes}), '
- f'new Uint8Array({frame_bytes}), {frame.status}),')
+ yield (
+ f' new Expected({frame.address}, '
+ f'new Uint8Array({control_bytes}), '
+ f'new Uint8Array({frame_bytes}), {frame.status}),'
+ )
else:
raw = _ts_byte_array(frame.raw_encoded)
yield (
@@ -408,15 +631,89 @@ def _ts_test(ctx: Context) -> Iterator[str]:
# Class that tests all cases in TEST_CASES.
-DecoderTest = _TESTS.python_tests('DecoderTest', _define_py_test)
+DecoderTest = _TESTS.python_tests('DecoderTest', _define_py_decoder_test)
+NonFrameDecoderTest = _TESTS.python_tests(
+ 'NonFrameDecoderTest', _define_raw_decoder_py_test
+)
+
+
+class AdditionalNonFrameDecoderTests(unittest.TestCase):
+ """Additional tests for the non-frame decoder."""
+
+ def test_shared_flags_waits_for_tilde_to_emit_data(self) -> None:
+ non_frame_data = bytearray()
+ decoder = FrameAndNonFrameDecoder(non_frame_data.extend)
+
+ self.assertEqual(
+ [Expected(0, b'\0', b'')], list(decoder.process(_encode(0, 0, b'')))
+ )
+ self.assertEqual(non_frame_data, b'')
+
+ self.assertEqual([], list(decoder.process(b'uh oh, no tilde!')))
+ self.assertEqual(non_frame_data, b'')
+
+ self.assertEqual([], list(decoder.process(b'~')))
+ self.assertEqual(non_frame_data, b'uh oh, no tilde!')
+
+ def test_no_shared_flags_immediately_emits_data(self) -> None:
+ non_frame_data = bytearray()
+ decoder = FrameAndNonFrameDecoder(
+ non_frame_data.extend, handle_shared_flags=False
+ )
+
+ self.assertEqual(
+ [Expected(0, b'\0', b'')], list(decoder.process(_encode(0, 0, b'')))
+ )
+ self.assertEqual(non_frame_data, b'')
+
+ self.assertEqual([], list(decoder.process(b'uh oh, no tilde!')))
+ self.assertEqual(non_frame_data, b'uh oh, no tilde!')
+
+ def test_emits_data_if_mtu_is_exceeded(self) -> None:
+ frame_start = b'~this looks like a real frame'
+
+ non_frame_data = bytearray()
+ decoder = FrameAndNonFrameDecoder(
+ non_frame_data.extend, mtu=len(frame_start)
+ )
+
+ self.assertEqual([], list(decoder.process(frame_start)))
+ self.assertEqual(non_frame_data, b'')
+
+ self.assertEqual([], list(decoder.process(b'!')))
+ self.assertEqual(non_frame_data, frame_start + b'!')
+
+ def test_emits_data_if_timeout_expires(self) -> None:
+ frame_start = b'~this looks like a real frame'
+
+ non_frame_data: 'queue.Queue[bytes]' = queue.Queue()
+ decoder = FrameAndNonFrameDecoder(non_frame_data.put, timeout_s=0.001)
+
+ self.assertEqual([], list(decoder.process(frame_start)))
+ self.assertEqual(non_frame_data.get(timeout=2), frame_start)
+
+ def test_emits_raw_data_and_valid_frame_if_flushed_partway(self) -> None:
+ payload = b'Do you wanna ride in my blimp?'
+ frame = _encode(1, 2, payload)
+
+ non_frame_data = bytearray()
+ decoder = FrameAndNonFrameDecoder(non_frame_data.extend)
+
+ self.assertEqual([], list(decoder.process(frame[:5])))
+ decoder.flush_non_frame_data()
+
+ self.assertEqual(
+ [Expected(1, b'\2', payload)], list(decoder.process(frame[5:]))
+ )
+
if __name__ == '__main__':
args = parse_test_generation_args()
if args.generate_cc_test:
- _TESTS.cc_tests(args.generate_cc_test, _cpp_test, _CPP_HEADER,
- _CPP_FOOTER)
+ _TESTS.cc_tests(
+ args.generate_cc_test, _cpp_test, _CPP_HEADER, _CPP_FOOTER
+ )
elif args.generate_ts_test:
- _TESTS.ts_tests(args.generate_ts_test, _ts_test, _TS_HEADER,
- _TS_FOOTER)
+ _TESTS.ts_tests(args.generate_ts_test, _ts_test, _TS_HEADER, _TS_FOOTER)
else:
unittest.main()
diff --git a/pw_hdlc/py/encode_test.py b/pw_hdlc/py/encode_test.py
index fa48c58c8..aaa0c72ac 100755
--- a/pw_hdlc/py/encode_test.py
+++ b/pw_hdlc/py/encode_test.py
@@ -29,32 +29,44 @@ def _with_fcs(data: bytes) -> bytes:
class TestEncodeUIFrame(unittest.TestCase):
"""Tests Encoding bytes with different arguments using a custom serial."""
+
def test_empty(self):
- self.assertEqual(encode.ui_frame(0, b''),
- FLAG + _with_fcs(b'\x01\x03') + FLAG)
- self.assertEqual(encode.ui_frame(0x1a, b''),
- FLAG + _with_fcs(b'\x35\x03') + FLAG)
+ self.assertEqual(
+ encode.ui_frame(0, b''), FLAG + _with_fcs(b'\x01\x03') + FLAG
+ )
+ self.assertEqual(
+ encode.ui_frame(0x1A, b''), FLAG + _with_fcs(b'\x35\x03') + FLAG
+ )
def test_1byte(self):
- self.assertEqual(encode.ui_frame(0, b'A'),
- FLAG + _with_fcs(b'\x01\x03A') + FLAG)
+ self.assertEqual(
+ encode.ui_frame(0, b'A'), FLAG + _with_fcs(b'\x01\x03A') + FLAG
+ )
def test_multibyte(self):
- self.assertEqual(encode.ui_frame(0, b'123456789'),
- FLAG + _with_fcs(b'\x01\x03123456789') + FLAG)
+ self.assertEqual(
+ encode.ui_frame(0, b'123456789'),
+ FLAG + _with_fcs(b'\x01\x03123456789') + FLAG,
+ )
def test_multibyte_address(self):
- self.assertEqual(encode.ui_frame(128, b'123456789'),
- FLAG + _with_fcs(b'\x00\x03\x03123456789') + FLAG)
+ self.assertEqual(
+ encode.ui_frame(128, b'123456789'),
+ FLAG + _with_fcs(b'\x00\x03\x03123456789') + FLAG,
+ )
def test_escape(self):
self.assertEqual(
- encode.ui_frame(0x3e, b'\x7d'),
- FLAG + b'\x7d\x5d\x03\x7d\x5d' + _fcs(b'\x7d\x03\x7d') + FLAG)
+ encode.ui_frame(0x3E, b'\x7d'),
+ FLAG + b'\x7d\x5d\x03\x7d\x5d' + _fcs(b'\x7d\x03\x7d') + FLAG,
+ )
self.assertEqual(
- encode.ui_frame(0x3e, b'A\x7e\x7dBC'),
- FLAG + b'\x7d\x5d\x03A\x7d\x5e\x7d\x5dBC' +
- _fcs(b'\x7d\x03A\x7e\x7dBC') + FLAG)
+ encode.ui_frame(0x3E, b'A\x7e\x7dBC'),
+ FLAG
+ + b'\x7d\x5d\x03A\x7d\x5e\x7d\x5dBC'
+ + _fcs(b'\x7d\x03A\x7e\x7dBC')
+ + FLAG,
+ )
if __name__ == '__main__':
diff --git a/pw_hdlc/py/pw_hdlc/decode.py b/pw_hdlc/py/pw_hdlc/decode.py
index 3c5b487a6..43b628650 100644
--- a/pw_hdlc/py/pw_hdlc/decode.py
+++ b/pw_hdlc/py/pw_hdlc/decode.py
@@ -15,7 +15,9 @@
import enum
import logging
-from typing import Iterator, Optional
+import threading
+import time
+from typing import Iterable, Optional, Callable, Any
import zlib
from pw_hdlc import protocol
@@ -24,10 +26,12 @@ _LOG = logging.getLogger('pw_hdlc')
NO_ADDRESS = -1
_MIN_FRAME_SIZE = 6 # 1 B address + 1 B control + 4 B CRC-32
+_FLAG_BYTE = bytes([protocol.FLAG])
class FrameStatus(enum.Enum):
"""Indicates that an error occurred."""
+
OK = 'OK'
FCS_MISMATCH = 'frame check sequence failure'
FRAMING_ERROR = 'invalid flag or escape characters'
@@ -36,15 +40,19 @@ class FrameStatus(enum.Enum):
class Frame:
"""Represents an HDLC frame."""
- def __init__(self,
- raw_encoded: bytes,
- raw_decoded: bytes,
- status: FrameStatus = FrameStatus.OK):
+
+ def __init__(
+ self,
+ raw_encoded: bytes,
+ raw_decoded: bytes,
+ status: FrameStatus = FrameStatus.OK,
+ ):
"""Parses fields from an HDLC frame.
Arguments:
- raw_encoded: The complete HDLC-encoded frame, excluding HDLC flag
- characters.
+ raw_encoded: The complete HDLC-encoded frame, including any HDLC
+ flag bytes. In the case of back to back frames, the
+ beginning flag byte may be omitted.
raw_decoded: The complete decoded frame (address, control,
information, FCS).
status: Whether parsing the frame succeeded.
@@ -64,8 +72,8 @@ class Frame:
return
self.address = address
- self.control = raw_decoded[address_length:address_length + 1]
- self.data = raw_decoded[address_length + 1:-4]
+ self.control = raw_decoded[address_length : address_length + 1]
+ self.data = raw_decoded[address_length + 1 : -4]
def ok(self) -> bool:
"""True if this represents a valid frame.
@@ -78,10 +86,15 @@ class Frame:
def __repr__(self) -> str:
if self.ok():
- body = (f'address={self.address}, control={self.control!r}, '
- f'data={self.data!r}')
+ body = (
+ f'address={self.address}, control={self.control!r}, '
+ f'data={self.data!r}'
+ )
else:
- body = str(self.status)
+ body = (
+ f'raw_encoded={self.raw_encoded!r}, '
+ f'status={str(self.status)}'
+ )
return f'{type(self).__name__}({body})'
@@ -105,12 +118,13 @@ def _check_frame(frame_data: bytes) -> FrameStatus:
class FrameDecoder:
"""Decodes one or more HDLC frames from a stream of data."""
- def __init__(self):
+
+ def __init__(self) -> None:
self._decoded_data = bytearray()
self._raw_data = bytearray()
self._state = _State.INTERFRAME
- def process(self, data: bytes) -> Iterator[Frame]:
+ def process(self, data: bytes) -> Iterable[Frame]:
"""Decodes and yields HDLC frames, including corrupt frames.
The ok() method on Frame indicates whether it is valid or represents a
@@ -120,44 +134,53 @@ class FrameDecoder:
Frames, which may be valid (frame.ok()) or corrupt (!frame.ok())
"""
for byte in data:
- frame = self._process_byte(byte)
+ frame = self.process_byte(byte)
if frame:
yield frame
- def process_valid_frames(self, data: bytes) -> Iterator[Frame]:
+ def process_valid_frames(self, data: bytes) -> Iterable[Frame]:
"""Decodes and yields valid HDLC frames, logging any errors."""
for frame in self.process(data):
if frame.ok():
yield frame
else:
- _LOG.warning('Failed to decode frame: %s; discarded %d bytes',
- frame.status.value, len(frame.raw_encoded))
+ _LOG.warning(
+ 'Failed to decode frame: %s; discarded %d bytes',
+ frame.status.value,
+ len(frame.raw_encoded),
+ )
_LOG.debug('Discarded data: %s', frame.raw_encoded)
def _finish_frame(self, status: FrameStatus) -> Frame:
+ # HDLC frames always start and end with a flag character, though the
+ # character may be shared with other frames. Ensure the raw encoding of
+ # OK frames always includes the start and end flags for consistency.
+ if status is FrameStatus.OK:
+ if not self._raw_data.startswith(_FLAG_BYTE):
+ self._raw_data.insert(0, protocol.FLAG)
+
frame = Frame(bytes(self._raw_data), bytes(self._decoded_data), status)
self._raw_data.clear()
self._decoded_data.clear()
return frame
- def _process_byte(self, byte: int) -> Optional[Frame]:
+ def process_byte(self, byte: int) -> Optional[Frame]:
+ """Processes a single byte and returns a frame if one was completed."""
frame: Optional[Frame] = None
- # Record every byte except the flag character.
- if byte != protocol.FLAG:
- self._raw_data.append(byte)
+ self._raw_data.append(byte)
if self._state is _State.INTERFRAME:
if byte == protocol.FLAG:
- if self._raw_data:
+ if len(self._raw_data) != 1:
frame = self._finish_frame(FrameStatus.FRAMING_ERROR)
self._state = _State.FRAME
elif self._state is _State.FRAME:
if byte == protocol.FLAG:
- if self._raw_data:
- frame = self._finish_frame(_check_frame(
- self._decoded_data))
+ # On back to back frames, we may see a repeated FLAG byte.
+ if len(self._raw_data) > 1:
+ frame = self._finish_frame(_check_frame(self._decoded_data))
self._state = _State.FRAME
elif byte == protocol.ESCAPE:
@@ -177,3 +200,116 @@ class FrameDecoder:
raise AssertionError(f'Invalid decoder state: {self._state}')
return frame
+
+
+class FrameAndNonFrameDecoder:
+ """Processes both HDLC frames and non-frame data in a stream."""
+
+ def __init__(
+ self,
+ non_frame_data_handler: Callable[[bytes], Any],
+ *,
+ mtu: Optional[int] = None,
+ timeout_s: Optional[float] = None,
+ handle_shared_flags: bool = True,
+ ) -> None:
+ """Yields valid HDLC frames and passes non-frame data to callback.
+
+ Args:
+ mtu: Maximum bytes to receive before flushing raw data. If a valid
+ HDLC frame contains more than MTU bytes, the valid frame will be
+ emitted, but part of the frame will be included in the raw data.
+ timeout_s: How long to wait before automatically flushing raw data. If
+ a timeout occurs partway through a valid frame, the frame will be
+ emitted, but part of the frame will be included in the raw data.
+ handle_shared_flags: Whether to permit HDLC frames to share a single
+ flag byte between frames. If False, partial HDLC frames may be
+ emitted as raw data when HDLC frames share a flag byte, but raw
+ data won't have to wait for a timeout or full MTU to be flushed.
+ """
+ self._non_frame_data_handler = non_frame_data_handler
+ self._mtu = mtu
+ self._shared_flags = handle_shared_flags
+ self._timeout_s = timeout_s
+
+ self._raw_data = bytearray()
+ self._hdlc_decoder = FrameDecoder()
+ self._last_data_time = time.time()
+ self._lock = threading.Lock()
+
+ if self._timeout_s is not None:
+ threading.Thread(target=self._timeout_thread, daemon=True).start()
+
+ def flush_non_frame_data(self) -> None:
+ """Flushes any data in the buffer as non-frame data.
+
+ If a valid HDLC frame was flushed partway, the data for the first part
+ of the frame will be included both in the raw data and in the frame.
+ """
+ with self._lock:
+ self._flush_non_frame()
+
+ def _flush_non_frame(self, to_index: Optional[int] = None):
+ if self._raw_data:
+ self._non_frame_data_handler(bytes(self._raw_data[:to_index]))
+ del self._raw_data[:to_index]
+
+ def _timeout_thread(self) -> None:
+ assert self._timeout_s is not None
+
+ while True:
+ time.sleep(self._timeout_s)
+ with self._lock:
+ if time.time() - self._last_data_time > self._timeout_s:
+ self._flush_non_frame()
+
+ def process(self, data: bytes) -> Iterable[Frame]:
+ """Processes a stream of mixed HDLC and unstructured data.
+
+ Yields OK frames and calls non_frame_data_handler with non-HDLC data.
+ """
+ with self._lock:
+ for byte in data:
+ yield from self._process_byte(byte)
+
+ # Flush the data if it is larger than the MTU, or flag bytes are not
+ # being shared and no initial flag was seen.
+ if (self._mtu is not None and len(self._raw_data) > self._mtu) or (
+ not self._shared_flags
+ and not self._raw_data.startswith(_FLAG_BYTE)
+ ):
+ self._flush_non_frame()
+
+ self._last_data_time = time.time()
+
+ def _process_byte(self, byte: int) -> Iterable[Frame]:
+ self._raw_data.append(byte)
+ frame = self._hdlc_decoder.process_byte(byte)
+
+ if frame is None:
+ return
+
+ if frame.ok():
+ # Drop the valid frame from the data. Only drop matching bytes in
+ # case the frame was flushed prematurely.
+ for suffix_byte in reversed(frame.raw_encoded):
+ if not self._raw_data or self._raw_data[-1] != suffix_byte:
+ break
+ self._raw_data.pop()
+
+ self._flush_non_frame() # Flush the raw data before the frame.
+
+ if self._mtu is not None and len(frame.raw_encoded) > self._mtu:
+ _LOG.warning(
+ 'Found a valid %d B HDLC frame, but the MTU is set to %d! '
+ 'The MTU setting may be incorrect.',
+ self._mtu,
+ len(frame.raw_encoded),
+ )
+
+ yield frame
+ else:
+ # Don't flush a final flag byte yet because it might be the start of
+ # an HDLC frame.
+ to_index = -1 if self._raw_data[-1] == protocol.FLAG else None
+ self._flush_non_frame(to_index)
diff --git a/pw_hdlc/py/pw_hdlc/encode.py b/pw_hdlc/py/pw_hdlc/encode.py
index ceede92da..0122058cd 100644
--- a/pw_hdlc/py/pw_hdlc/encode.py
+++ b/pw_hdlc/py/pw_hdlc/encode.py
@@ -21,8 +21,11 @@ _FLAG_BYTE = bytes([protocol.FLAG])
def ui_frame(address: int, data: bytes) -> bytes:
"""Encodes an HDLC UI-frame with a CRC-32 frame check sequence."""
- frame = protocol.encode_address(
- address) + protocol.UFrameControl.unnumbered_information().data + data
+ frame = (
+ protocol.encode_address(address)
+ + protocol.UFrameControl.unnumbered_information().data
+ + data
+ )
frame += protocol.frame_check_sequence(frame)
frame = frame.replace(_ESCAPE_BYTE, b'\x7d\x5d')
frame = frame.replace(_FLAG_BYTE, b'\x7d\x5e')
diff --git a/pw_hdlc/py/pw_hdlc/protocol.py b/pw_hdlc/py/pw_hdlc/protocol.py
index f12b79b30..f5762b87f 100644
--- a/pw_hdlc/py/pw_hdlc/protocol.py
+++ b/pw_hdlc/py/pw_hdlc/protocol.py
@@ -44,7 +44,7 @@ def encode_address(address: int) -> bytes:
result = bytearray()
while True:
- result += bytes([(address & 0x7f) << 1])
+ result += bytes([(address & 0x7F) << 1])
address >>= 7
if address == 0:
diff --git a/pw_hdlc/py/pw_hdlc/rpc.py b/pw_hdlc/py/pw_hdlc/rpc.py
index b41275331..cb0960524 100644
--- a/pw_hdlc/py/pw_hdlc/rpc.py
+++ b/pw_hdlc/py/pw_hdlc/rpc.py
@@ -13,19 +13,27 @@
# the License.
"""Utilities for using HDLC with pw_rpc."""
-import collections
from concurrent.futures import ThreadPoolExecutor
import io
import logging
from queue import SimpleQueue
-import random
import sys
import threading
import time
import socket
import subprocess
-from typing import (Any, BinaryIO, Callable, Deque, Dict, Iterable, List,
- NoReturn, Optional, Sequence, Tuple, Union)
+from typing import (
+ Any,
+ BinaryIO,
+ Callable,
+ Dict,
+ Iterable,
+ List,
+ NoReturn,
+ Optional,
+ Sequence,
+ Union,
+)
from pw_protobuf_compiler import python_protos
import pw_rpc
@@ -41,9 +49,11 @@ DEFAULT_ADDRESS = ord('R')
_VERBOSE = logging.DEBUG - 1
-def channel_output(writer: Callable[[bytes], Any],
- address: int = DEFAULT_ADDRESS,
- delay_s: float = 0) -> Callable[[bytes], None]:
+def channel_output(
+ writer: Callable[[bytes], Any],
+ address: int = DEFAULT_ADDRESS,
+ delay_s: float = 0,
+) -> Callable[[bytes], None]:
"""Returns a function that can be used as a channel output for pw_rpc."""
if delay_s:
@@ -72,17 +82,19 @@ def _handle_error(frame: Frame) -> None:
FrameHandlers = Dict[int, Callable[[Frame], Any]]
-def read_and_process_data(read: Callable[[], bytes],
- on_read_error: Callable[[Exception], Any],
- frame_handlers: FrameHandlers,
- error_handler: Callable[[Frame],
- Any] = _handle_error,
- handler_threads: Optional[int] = 1) -> NoReturn:
+def read_and_process_data(
+ read: Callable[[], bytes],
+ on_read_error: Callable[[Exception], Any],
+ frame_handlers: FrameHandlers,
+ error_handler: Callable[[Frame], Any] = _handle_error,
+ handler_threads: Optional[int] = 1,
+) -> NoReturn:
"""Continuously reads and handles HDLC frames.
Passes frames to an executor that calls frame handler functions in other
threads.
"""
+
def handle_frame(frame: Frame):
try:
if not frame.ok():
@@ -92,8 +104,9 @@ def read_and_process_data(read: Callable[[], bytes],
try:
frame_handlers[frame.address](frame)
except KeyError:
- _LOG.warning('Unhandled frame for address %d: %s',
- frame.address, frame)
+ _LOG.warning(
+ 'Unhandled frame for address %d: %s', frame.address, frame
+ )
except: # pylint: disable=bare-except
_LOG.exception('Exception in HDLC frame handler thread')
@@ -126,21 +139,26 @@ def default_channels(write: Callable[[bytes], Any]) -> List[pw_rpc.Channel]:
return [pw_rpc.Channel(1, channel_output(write))]
-PathsModulesOrProtoLibrary = Union[Iterable[python_protos.PathOrModule],
- python_protos.Library]
+PathsModulesOrProtoLibrary = Union[
+ Iterable[python_protos.PathOrModule], python_protos.Library
+]
class HdlcRpcClient:
"""An RPC client configured to run over HDLC."""
- def __init__(self,
- read: Callable[[], bytes],
- paths_or_modules: PathsModulesOrProtoLibrary,
- channels: Iterable[pw_rpc.Channel],
- output: Callable[[bytes], Any] = write_to_file,
- client_impl: pw_rpc.client.ClientImpl = None,
- *,
- _incoming_packet_filter_for_testing: pw_rpc.
- ChannelManipulator = None):
+
+ def __init__(
+ self,
+ read: Callable[[], bytes],
+ paths_or_modules: PathsModulesOrProtoLibrary,
+ channels: Iterable[pw_rpc.Channel],
+ output: Callable[[bytes], Any] = write_to_file,
+ client_impl: Optional[pw_rpc.client.ClientImpl] = None,
+ *,
+ _incoming_packet_filter_for_testing: Optional[
+ pw_rpc.ChannelManipulator
+ ] = None,
+ ):
"""Creates an RPC client configured to communicate using HDLC.
Args:
@@ -157,8 +175,9 @@ class HdlcRpcClient:
if client_impl is None:
client_impl = callback_client.Impl()
- self.client = pw_rpc.Client.from_modules(client_impl, channels,
- self.protos.modules())
+ self.client = pw_rpc.Client.from_modules(
+ client_impl, channels, self.protos.modules()
+ )
rpc_output: Callable[[bytes], Any] = self._handle_rpc_packet
if _incoming_packet_filter_for_testing is not None:
@@ -171,12 +190,13 @@ class HdlcRpcClient:
}
# Start background thread that reads and processes RPC packets.
- threading.Thread(target=read_and_process_data,
- daemon=True,
- args=(read, lambda exc: None,
- frame_handlers)).start()
+ threading.Thread(
+ target=read_and_process_data,
+ daemon=True,
+ args=(read, lambda exc: None, frame_handlers),
+ ).start()
- def rpcs(self, channel_id: int = None) -> Any:
+ def rpcs(self, channel_id: Optional[int] = None) -> Any:
"""Returns object for accessing services on the specified channel.
This skips some intermediate layers to make it simpler to invoke RPCs
@@ -201,9 +221,9 @@ def _try_connect(port: int, attempts: int = 10) -> socket.socket:
that length of time can vary. This retries with a short delay rather than
having to wait for the worst case delay every time.
"""
+ timeout_s = 0.001
while True:
- attempts -= 1
- time.sleep(0.001)
+ time.sleep(timeout_s)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -211,12 +231,16 @@ def _try_connect(port: int, attempts: int = 10) -> socket.socket:
return sock
except ConnectionRefusedError:
sock.close()
+ attempts -= 1
if attempts <= 0:
raise
+ timeout_s *= 2
+
class SocketSubprocess:
"""Executes a subprocess and connects to it with a socket."""
+
def __init__(self, command: Sequence, port: int) -> None:
self._server_process = subprocess.Popen(command, stdin=subprocess.PIPE)
self.stdin = self._server_process.stdin
@@ -242,65 +266,12 @@ class SocketSubprocess:
self.close()
-class PacketFilter(pw_rpc.ChannelManipulator):
- """Determines if a packet should be kept or dropped for testing purposes."""
- _Action = Callable[[int], Tuple[bool, bool]]
- _KEEP = lambda _: (True, False)
- _DROP = lambda _: (False, False)
-
- def __init__(self, name: str) -> None:
- super().__init__()
- self.name = name
- self.packet_count = 0
- self._actions: Deque[PacketFilter._Action] = collections.deque()
-
- def process_and_send(self, packet: bytes):
- if self.keep_packet(packet):
- self.send_packet(packet)
-
- def reset(self) -> None:
- self.packet_count = 0
- self._actions.clear()
-
- def keep(self, count: int) -> None:
- """Keeps the next count packets."""
- self._actions.extend(PacketFilter._KEEP for _ in range(count))
-
- def drop(self, count: int) -> None:
- """Drops the next count packets."""
- self._actions.extend(PacketFilter._DROP for _ in range(count))
-
- def drop_every(self, every: int) -> None:
- """Drops every Nth packet forever."""
- self._actions.append(lambda count: (count % every != 0, True))
-
- def randomly_drop(self, one_in: int, gen: random.Random) -> None:
- """Drops packets randomly forever."""
- self._actions.append(lambda _: (gen.randrange(one_in) != 0, True))
-
- def keep_packet(self, packet: bytes) -> bool:
- """Returns whether the provided packet should be kept or dropped."""
- self.packet_count += 1
-
- if not self._actions:
- return True
-
- keep, repeat = self._actions[0](self.packet_count)
-
- if not repeat:
- self._actions.popleft()
-
- if not keep:
- _LOG.debug('Dropping %s packet %d for testing: %s', self.name,
- self.packet_count, packet)
- return keep
-
-
class HdlcRpcLocalServerAndClient:
"""Runs an RPC server in a subprocess and connects to it over a socket.
This can be used to run a local RPC server in an integration test.
"""
+
def __init__(
self,
server_command: Sequence,
@@ -308,7 +279,7 @@ class HdlcRpcLocalServerAndClient:
protos: PathsModulesOrProtoLibrary,
*,
incoming_processor: Optional[pw_rpc.ChannelManipulator] = None,
- outgoing_processor: Optional[pw_rpc.ChannelManipulator] = None
+ outgoing_processor: Optional[pw_rpc.ChannelManipulator] = None,
) -> None:
"""Creates a new HdlcRpcLocalServerAndClient."""
@@ -332,7 +303,8 @@ class HdlcRpcLocalServerAndClient:
protos,
default_channels(self.channel_output),
self.output.write,
- _incoming_packet_filter_for_testing=incoming_processor).client
+ _incoming_packet_filter_for_testing=incoming_processor,
+ ).client
def _read_from_socket(self):
while True:
diff --git a/pw_hdlc/py/pw_hdlc/rpc_console.py b/pw_hdlc/py/pw_hdlc/rpc_console.py
index 38bc6e0f5..bbfc4a702 100644
--- a/pw_hdlc/py/pw_hdlc/rpc_console.py
+++ b/pw_hdlc/py/pw_hdlc/rpc_console.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -13,293 +13,20 @@
# the License.
"""Console for interacting with pw_rpc over HDLC.
-To start the console, provide a serial port as the --device argument and paths
-or globs for .proto files that define the RPC services to support:
+This command is no longer supported. Please run pw_system.console instead.
- python -m pw_hdlc.rpc_console --device /dev/ttyUSB0 sample.proto
-
-This starts an IPython console for communicating with the connected device. A
-few variables are predefined in the interactive console. These include:
-
- rpcs - used to invoke RPCs
- device - the serial device used for communication
- client - the pw_rpc.Client
- protos - protocol buffer messages indexed by proto package
-
-An example echo RPC command:
-
- rpcs.pw.rpc.EchoService.Echo(msg="hello!")
+ python -m pw_system.console --device /dev/ttyUSB0 --proto-globs sample.proto
"""
-import argparse
-import glob
-from inspect import cleandoc
-import logging
-from pathlib import Path
import sys
-from types import ModuleType
-from typing import (
- Any,
- BinaryIO,
- Collection,
- Iterable,
- Iterator,
- List,
- Optional,
- Union,
-)
-import socket
-
-import serial # type: ignore
-
-import pw_cli.log
-import pw_console.python_logging
-from pw_console import PwConsoleEmbed
-from pw_console.pyserial_wrapper import SerialWithLogging
-from pw_console.plugins.bandwidth_toolbar import BandwidthToolbar
-
-from pw_log.proto import log_pb2
-from pw_rpc.console_tools.console import ClientInfo, flattened_rpc_completions
-from pw_rpc import callback_client
-from pw_tokenizer.database import LoadTokenDatabases
-from pw_tokenizer.detokenize import Detokenizer, detokenize_base64
-from pw_tokenizer import tokens
-
-from pw_hdlc.rpc import HdlcRpcClient, default_channels
-
-_LOG = logging.getLogger(__name__)
-_DEVICE_LOG = logging.getLogger('rpc_device')
+# TODO(tonymd): Delete this when no longer needed.
PW_RPC_MAX_PACKET_SIZE = 256
-SOCKET_SERVER = 'localhost'
-SOCKET_PORT = 33000
-MKFIFO_MODE = 0o666
-
-
-def _parse_args():
- """Parses and returns the command line arguments."""
- parser = argparse.ArgumentParser(description=__doc__)
- group = parser.add_mutually_exclusive_group(required=True)
- group.add_argument('-d', '--device', help='the serial port to use')
- parser.add_argument('-b',
- '--baudrate',
- type=int,
- default=115200,
- help='the baud rate to use')
- parser.add_argument(
- '--serial-debug',
- action='store_true',
- help=('Enable debug log tracing of all data passed through'
- 'pyserial read and write.'))
- parser.add_argument(
- '-o',
- '--output',
- type=argparse.FileType('wb'),
- default=sys.stdout.buffer,
- help=('The file to which to write device output (HDLC channel 1); '
- 'provide - or omit for stdout.'))
- parser.add_argument('--logfile', help='Console debug log file.')
- group.add_argument('-s',
- '--socket-addr',
- type=str,
- help='use socket to connect to server, type default for\
- localhost:33000, or manually input the server address:port')
- parser.add_argument("--token-databases",
- metavar='elf_or_token_database',
- nargs="+",
- action=LoadTokenDatabases,
- help="Path to tokenizer database csv file(s).")
- parser.add_argument('--config-file',
- type=Path,
- help='Path to a pw_console yaml config file.')
- parser.add_argument('--proto-globs',
- nargs='+',
- help='glob pattern for .proto files')
- return parser.parse_args()
-
-
-def _expand_globs(globs: Iterable[str]) -> Iterator[Path]:
- for pattern in globs:
- for file in glob.glob(pattern, recursive=True):
- yield Path(file)
-
-
-def _start_ipython_terminal(client: HdlcRpcClient,
- serial_debug: bool = False,
- config_file_path: Optional[Path] = None) -> None:
- """Starts an interactive IPython terminal with preset variables."""
- local_variables = dict(
- client=client,
- device=client.client.channel(1),
- rpcs=client.client.channel(1).rpcs,
- protos=client.protos.packages,
- # Include the active pane logger for creating logs in the repl.
- DEVICE_LOG=_DEVICE_LOG,
- LOG=logging.getLogger(),
- )
-
- welcome_message = cleandoc("""
- Welcome to the Pigweed Console!
-
- Help: Press F1 or click the [Help] menu
- To move focus: Press Shift-Tab or click on a window
-
- Example Python commands:
-
- device.rpcs.pw.rpc.EchoService.Echo(msg='hello!')
- LOG.warning('Message appears in Host Logs window.')
- DEVICE_LOG.warning('Message appears in Device Logs window.')
- """)
-
- client_info = ClientInfo('device',
- client.client.channel(1).rpcs, client.client)
- completions = flattened_rpc_completions([client_info])
-
- log_windows = {
- 'Device Logs': [_DEVICE_LOG],
- 'Host Logs': [logging.getLogger()],
- }
- if serial_debug:
- log_windows['Serial Debug'] = [
- logging.getLogger('pw_console.serial_debug_logger')
- ]
-
- interactive_console = PwConsoleEmbed(
- global_vars=local_variables,
- local_vars=None,
- loggers=log_windows,
- repl_startup_message=welcome_message,
- help_text=__doc__,
- config_file_path=config_file_path,
- )
- interactive_console.hide_windows('Host Logs')
- interactive_console.add_sentence_completer(completions)
- if serial_debug:
- interactive_console.add_bottom_toolbar(BandwidthToolbar())
-
- # Setup Python logger propagation
- interactive_console.setup_python_logging()
-
- # Don't send device logs to the root logger.
- _DEVICE_LOG.propagate = False
-
- interactive_console.embed()
-
-
-class SocketClientImpl:
- def __init__(self, config: str):
- self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- socket_server = ''
- socket_port = 0
-
- if config == 'default':
- socket_server = SOCKET_SERVER
- socket_port = SOCKET_PORT
- else:
- socket_server, socket_port_str = config.split(':')
- socket_port = int(socket_port_str)
- self.socket.connect((socket_server, socket_port))
-
- def write(self, data: bytes):
- self.socket.sendall(data)
-
- def read(self, num_bytes: int = PW_RPC_MAX_PACKET_SIZE):
- return self.socket.recv(num_bytes)
-
-
-def console(device: str,
- baudrate: int,
- proto_globs: Collection[str],
- token_databases: Collection[tokens.Database],
- socket_addr: str,
- logfile: str,
- output: Any,
- serial_debug: bool = False,
- config_file: Optional[Path] = None) -> int:
- """Starts an interactive RPC console for HDLC."""
- # argparse.FileType doesn't correctly handle '-' for binary files.
- if output is sys.stdout:
- output = sys.stdout.buffer
-
- if not logfile:
- # Create a temp logfile to prevent logs from appearing over stdout. This
- # would corrupt the prompt toolkit UI.
- logfile = pw_console.python_logging.create_temp_log_file()
- pw_cli.log.install(logging.INFO, True, False, logfile)
-
- detokenizer = None
- if token_databases:
- detokenizer = Detokenizer(tokens.Database.merged(*token_databases),
- show_errors=False)
-
- if not proto_globs:
- proto_globs = ['**/*.proto']
-
- protos: List[Union[ModuleType, Path]] = list(_expand_globs(proto_globs))
-
- # Append compiled log.proto library to avoid include errors when manually
- # provided, and shadowing errors due to ordering when the default global
- # search path is used.
- protos.append(log_pb2)
-
- if not protos:
- _LOG.critical('No .proto files were found with %s',
- ', '.join(proto_globs))
- _LOG.critical('At least one .proto file is required')
- return 1
-
- _LOG.debug('Found %d .proto files found with %s', len(protos),
- ', '.join(proto_globs))
-
- serial_impl = serial.Serial
- if serial_debug:
- serial_impl = SerialWithLogging
-
- if socket_addr is None:
- serial_device = serial_impl(
- device,
- baudrate,
- timeout=0, # Non-blocking mode
- )
- read = lambda: serial_device.read(8192)
- write = serial_device.write
- else:
- try:
- socket_device = SocketClientImpl(socket_addr)
- read = socket_device.read
- write = socket_device.write
- except ValueError:
- _LOG.exception('Failed to initialize socket at %s', socket_addr)
- return 1
-
- callback_client_impl = callback_client.Impl(
- default_unary_timeout_s=5.0,
- default_stream_timeout_s=None,
- )
- _start_ipython_terminal(
- HdlcRpcClient(read,
- protos,
- default_channels(write),
- lambda data: detokenize_and_write_to_output(
- data, output, detokenizer),
- client_impl=callback_client_impl), serial_debug,
- config_file)
- return 0
-
-
-def detokenize_and_write_to_output(data: bytes,
- unused_output: BinaryIO = sys.stdout.buffer,
- detokenizer=None):
- log_line = data
- if detokenizer:
- log_line = detokenize_base64(detokenizer, data)
-
- for line in log_line.decode(errors="surrogateescape").splitlines():
- _DEVICE_LOG.info(line)
def main() -> int:
- return console(**vars(_parse_args()))
+ print(__doc__)
+ return 1
if __name__ == '__main__':
diff --git a/pw_hdlc/py/setup.cfg b/pw_hdlc/py/setup.cfg
index b3b18a3a1..f5d4ea6ac 100644
--- a/pw_hdlc/py/setup.cfg
+++ b/pw_hdlc/py/setup.cfg
@@ -21,12 +21,6 @@ description = Tools for Encoding/Decoding data using the HDLC protocol
[options]
packages = find:
zip_safe = False
-install_requires =
- ipython
- pw_console
- pw_protobuf_compiler
- pw_rpc
-tests_require = pw_build
[options.package_data]
pw_hdlc = py.typed
diff --git a/pw_hdlc/rpc_channel_test.cc b/pw_hdlc/rpc_channel_test.cc
index e0b972f2b..edccbdd1d 100644
--- a/pw_hdlc/rpc_channel_test.cc
+++ b/pw_hdlc/rpc_channel_test.cc
@@ -34,6 +34,7 @@
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
+#include "pw_hdlc/encoded_size.h"
#include "pw_stream/memory_stream.h"
using std::byte;
@@ -46,8 +47,9 @@ constexpr uint8_t kAddress = 0x7b; // 123
constexpr uint8_t kEncodedAddress = (kAddress << 1) | 1;
constexpr byte kControl = byte{0x3}; // UI-frame control sequence.
-// Size of the in-memory buffer to use for this test.
-constexpr size_t kSinkBufferSize = 15;
+// Size of the in-memory buffer to use for this test. All tests send a one-byte
+// payload.
+constexpr size_t kSinkBufferSize = MaxEncodedFrameSize(1);
TEST(RpcChannelOutput, 1BytePayload) {
stream::MemoryWriterBuffer<kSinkBufferSize> memory_writer;
@@ -61,8 +63,7 @@ TEST(RpcChannelOutput, 1BytePayload) {
constexpr auto expected = bytes::Concat(
kFlag, kEncodedAddress, kControl, 'A', uint32_t{0x653c9e82}, kFlag);
- EXPECT_EQ(OkStatus(),
- output.Send(std::span(buffer).first(sizeof(test_data))));
+ EXPECT_EQ(OkStatus(), output.Send(span(buffer).first(sizeof(test_data))));
ASSERT_EQ(memory_writer.bytes_written(), expected.size());
EXPECT_EQ(
@@ -87,7 +88,7 @@ TEST(RpcChannelOutput, EscapingPayloadTest) {
byte{0x7d} ^ byte{0x20},
uint32_t{0x4a53e205},
kFlag);
- EXPECT_EQ(OkStatus(), output.Send(std::span(buffer).first(test_data.size())));
+ EXPECT_EQ(OkStatus(), output.Send(span(buffer).first(test_data.size())));
ASSERT_EQ(memory_writer.bytes_written(), 10u);
EXPECT_EQ(
@@ -96,6 +97,15 @@ TEST(RpcChannelOutput, EscapingPayloadTest) {
0);
}
+TEST(FixedMtuChannelOutput, CompileTest) {
+ constexpr size_t kRequiredMtu =
+ MaxEncodedFrameSize(rpc::cfg::kEncodingBufferSizeBytes);
+ stream::MemoryWriterBuffer<kRequiredMtu> memory_writer;
+ [[maybe_unused]] FixedMtuChannelOutput<kRequiredMtu> channel_output(
+ memory_writer, kAddress, "RpcChannelOutput");
+ EXPECT_EQ(channel_output.MaxSafePayloadSize(), rpc::MaxSafePayloadSize());
+}
+
TEST(RpcChannelOutputBuffer, 1BytePayload) {
stream::MemoryWriterBuffer<kSinkBufferSize> memory_writer;
@@ -108,8 +118,7 @@ TEST(RpcChannelOutputBuffer, 1BytePayload) {
constexpr auto expected = bytes::Concat(
kFlag, kEncodedAddress, kControl, 'A', uint32_t{0x653c9e82}, kFlag);
- EXPECT_EQ(OkStatus(),
- output.Send(std::span(buffer).first(sizeof(test_data))));
+ EXPECT_EQ(OkStatus(), output.Send(span(buffer).first(sizeof(test_data))));
ASSERT_EQ(memory_writer.bytes_written(), expected.size());
EXPECT_EQ(
@@ -134,8 +143,7 @@ TEST(RpcChannelOutputBuffer, MultibyteAddress) {
uint32_t{0xd393a8a0},
kFlag);
- EXPECT_EQ(OkStatus(),
- output.Send(std::span(buffer).first(sizeof(test_data))));
+ EXPECT_EQ(OkStatus(), output.Send(span(buffer).first(sizeof(test_data))));
ASSERT_EQ(memory_writer.bytes_written(), expected.size());
EXPECT_EQ(
diff --git a/pw_hdlc/rpc_example/BUILD.bazel b/pw_hdlc/rpc_example/BUILD.bazel
index fd942e33f..29a86f94e 100644
--- a/pw_hdlc/rpc_example/BUILD.bazel
+++ b/pw_hdlc/rpc_example/BUILD.bazel
@@ -14,24 +14,22 @@
load(
"//pw_build:pigweed.bzl",
- "pw_cc_library",
+ "pw_cc_binary",
)
-pw_cc_library(
+pw_cc_binary(
name = "rpc_example",
srcs = [
"hdlc_rpc_server.cc",
"main.cc",
],
- hdrs = [
- "public/pw_hdlc/decoder.h",
- "public/pw_hdlc/hdlc_channel.h",
- "public/pw_hdlc/rpc_server_packets.h",
- ],
+ tags = ["manual"], # TODO(b/241575924): Fix the Bazel build for rpc_example
deps = [
"//pw_hdlc",
"//pw_hdlc:pw_rpc",
"//pw_log",
"//pw_rpc",
+ "//pw_rpc/nanopb:echo_service",
+ "//pw_rpc/system_server",
],
)
diff --git a/pw_hdlc/rpc_example/BUILD.gn b/pw_hdlc/rpc_example/BUILD.gn
index 06a640776..ecadf4409 100644
--- a/pw_hdlc/rpc_example/BUILD.gn
+++ b/pw_hdlc/rpc_example/BUILD.gn
@@ -40,6 +40,12 @@ if (dir_pw_third_party_nanopb == "") {
pw_python_script("example_script") {
sources = [ "example_script.py" ]
- python_deps = [ "$dir_pw_hdlc/py" ]
+ python_deps = [
+ "$dir_pw_build/py",
+ "$dir_pw_hdlc/py",
+ "$dir_pw_log:protos.python",
+ "$dir_pw_tokenizer/py:test_proto.python",
+ ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_hdlc/rpc_example/CMakeLists.txt b/pw_hdlc/rpc_example/CMakeLists.txt
index 67f4110a9..47ccd3a3e 100644
--- a/pw_hdlc/rpc_example/CMakeLists.txt
+++ b/pw_hdlc/rpc_example/CMakeLists.txt
@@ -21,6 +21,7 @@ add_executable(pw_hdlc.rpc_example EXCLUDE_FROM_ALL
target_link_libraries(pw_hdlc.rpc_example
PRIVATE
pw_hdlc
+ pw_hdlc.pw_rpc
pw_log
pw_rpc.nanopb.echo_service
pw_rpc.server
diff --git a/pw_hdlc/rpc_example/docs.rst b/pw_hdlc/rpc_example/docs.rst
index 0db8bfa5e..b2555594a 100644
--- a/pw_hdlc/rpc_example/docs.rst
+++ b/pw_hdlc/rpc_example/docs.rst
@@ -52,14 +52,14 @@ replacing ``/dev/ttyACM0`` with the correct serial device for your board.
.. code-block:: text
- $ python -m pw_hdlc.rpc_console --device /dev/ttyACM0
+ $ python -m pw_system.console --device /dev/ttyACM0
Console for interacting with pw_rpc over HDLC.
To start the console, provide a serial port as the --device argument and paths
or globs for .proto files that define the RPC services to support:
- python -m pw_hdlc.rpc_console --device /dev/ttyUSB0 sample.proto
+ python -m pw_system.console --device /dev/ttyUSB0 --proto-globs pw_rpc/echo.proto
This starts an IPython console for communicating with the connected device. A
few variables are predefined in the interactive console. These include:
@@ -123,7 +123,7 @@ Run pw_rpc client (i.e. use echo.proto)
.. code-block:: sh
- python -m pw_hdlc.rpc_console path/to/echo.proto -s localhost:33000
+ python -m pw_system.console path/to/echo.proto -s localhost:33000
Run pw_rpc server
diff --git a/pw_hdlc/rpc_example/example_script.py b/pw_hdlc/rpc_example/example_script.py
index c97924027..36a3cf85d 100755
--- a/pw_hdlc/rpc_example/example_script.py
+++ b/pw_hdlc/rpc_example/example_script.py
@@ -18,7 +18,7 @@ import argparse
import os
from pathlib import Path
-import serial # type: ignore
+import serial
from pw_hdlc.rpc import HdlcRpcClient, default_channels
@@ -29,8 +29,9 @@ PROTO = Path(os.environ['PW_ROOT'], 'pw_rpc/echo.proto')
def script(device: str, baud: int) -> None:
# Set up a pw_rpc client that uses HDLC.
ser = serial.Serial(device, baud, timeout=0.01)
- client = HdlcRpcClient(lambda: ser.read(4096), [PROTO],
- default_channels(ser.write))
+ client = HdlcRpcClient(
+ lambda: ser.read(4096), [PROTO], default_channels(ser.write)
+ )
# Make a shortcut to the EchoService.
echo_service = client.rpcs().pw.rpc.EchoService
@@ -52,16 +53,18 @@ def script(device: str, baud: int) -> None:
def main():
parser = argparse.ArgumentParser(
description=__doc__,
- formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- parser.add_argument('--device',
- '-d',
- default='/dev/ttyACM0',
- help='serial device to use')
- parser.add_argument('--baud',
- '-b',
- type=int,
- default=115200,
- help='baud rate for the serial device')
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ parser.add_argument(
+ '--device', '-d', default='/dev/ttyACM0', help='serial device to use'
+ )
+ parser.add_argument(
+ '--baud',
+ '-b',
+ type=int,
+ default=115200,
+ help='baud rate for the serial device',
+ )
script(**vars(parser.parse_args()))
diff --git a/pw_hdlc/rpc_example/hdlc_rpc_server.cc b/pw_hdlc/rpc_example/hdlc_rpc_server.cc
index b878e64b7..11d1218c1 100644
--- a/pw_hdlc/rpc_example/hdlc_rpc_server.cc
+++ b/pw_hdlc/rpc_example/hdlc_rpc_server.cc
@@ -13,7 +13,6 @@
// the License.
#include <array>
-#include <span>
#include <string_view>
#include "pw_assert/check.h"
@@ -23,6 +22,7 @@
#include "pw_rpc/echo_service_nanopb.h"
#include "pw_rpc/server.h"
#include "pw_rpc_system_server/rpc_server.h"
+#include "pw_span/span.h"
namespace hdlc_example {
namespace {
diff --git a/pw_hdlc/rpc_packets.cc b/pw_hdlc/rpc_packets.cc
index e3cf4ac9c..6241dc4f2 100644
--- a/pw_hdlc/rpc_packets.cc
+++ b/pw_hdlc/rpc_packets.cc
@@ -20,8 +20,7 @@
namespace pw::hdlc {
Status ReadAndProcessPackets(rpc::Server& server,
- rpc::ChannelOutput& output,
- std::span<std::byte> decode_buffer,
+ span<std::byte> decode_buffer,
unsigned rpc_address) {
Decoder decoder(decode_buffer);
@@ -32,8 +31,8 @@ Status ReadAndProcessPackets(rpc::Server& server,
if (auto result = decoder.Process(data); result.ok()) {
Frame& frame = result.value();
if (frame.address() == rpc_address) {
- server.ProcessPacket(frame.data(), output)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ server.ProcessPacket(frame.data())
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
}
}
diff --git a/pw_hdlc/size_report/BUILD.bazel b/pw_hdlc/size_report/BUILD.bazel
new file mode 100644
index 000000000..ef1eed932
--- /dev/null
+++ b/pw_hdlc/size_report/BUILD.bazel
@@ -0,0 +1,67 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_binary",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_bytes",
+ "//pw_checksum",
+ "//pw_hdlc",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_span",
+ "//pw_stream",
+]
+
+pw_cc_binary(
+ name = "base",
+ srcs = ["hdlc_size_report.cc"],
+ deps = deps,
+)
+
+pw_cc_binary(
+ name = "base_crc",
+ srcs = ["hdlc_size_report.cc"],
+ copts = ["-DENABLE_CRC=1"],
+ deps = deps,
+)
+
+pw_cc_binary(
+ name = "full",
+ srcs = ["hdlc_size_report.cc"],
+ copts = [
+ "-DENABLE_ENCODE=1",
+ "-DENABLE_DECODE=1",
+ "-DENABLE_CRC",
+ ],
+ deps = deps,
+)
+
+pw_cc_binary(
+ name = "full_crc",
+ srcs = ["hdlc_size_report.cc"],
+ copts = [
+ "-DENABLE_ENCODE=1",
+ "-DENABLE_DECODE=1",
+ ],
+ deps = deps,
+)
diff --git a/pw_hdlc/size_report/BUILD.gn b/pw_hdlc/size_report/BUILD.gn
new file mode 100644
index 000000000..d6bf2d6b1
--- /dev/null
+++ b/pw_hdlc/size_report/BUILD.gn
@@ -0,0 +1,57 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+common_deps = [
+ "$dir_pw_bloat:bloat_this_binary",
+ "$dir_pw_bytes",
+ "$dir_pw_log",
+ "$dir_pw_preprocessor",
+ "$dir_pw_span",
+ "$dir_pw_stream",
+ "..",
+]
+
+pw_executable("base") {
+ sources = [ "hdlc_size_report.cc" ]
+ deps = common_deps
+}
+
+pw_executable("base_crc") {
+ sources = [ "hdlc_size_report.cc" ]
+ deps = common_deps
+ defines = [ "ENABLE_CRC=1" ]
+}
+
+pw_executable("full") {
+ sources = [ "hdlc_size_report.cc" ]
+ defines = [
+ "ENABLE_DECODE=1",
+ "ENABLE_ENCODE=1",
+ ]
+ deps = common_deps
+}
+
+pw_executable("full_crc") {
+ sources = [ "hdlc_size_report.cc" ]
+ defines = [
+ "ENABLE_DECODE=1",
+ "ENABLE_ENCODE=1",
+ "ENABLE_CRC=1",
+ ]
+ deps = common_deps
+}
diff --git a/pw_hdlc/size_report/hdlc_size_report.cc b/pw_hdlc/size_report/hdlc_size_report.cc
new file mode 100644
index 000000000..b8cf35e44
--- /dev/null
+++ b/pw_hdlc/size_report/hdlc_size_report.cc
@@ -0,0 +1,105 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <cstring>
+
+#include "pw_bloat/bloat_this_binary.h"
+#include "pw_bytes/span.h"
+#include "pw_checksum/crc32.h"
+#include "pw_hdlc/decoder.h"
+#include "pw_hdlc/encoder.h"
+#include "pw_log/log.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_result/result.h"
+#include "pw_span/span.h"
+#include "pw_stream/memory_stream.h"
+#include "pw_varint/varint.h"
+
+namespace pw::size_report {
+
+PW_NO_INLINE
+Result<int> ForceResultInBloat() { return 43; }
+
+std::byte buffer[128];
+std::byte* volatile get_buffer = buffer;
+volatile unsigned get_size;
+
+PW_NO_INLINE
+ByteSpan GetBufferSpan() {
+ if (ForceResultInBloat().ok()) {
+ PW_LOG_INFO("Got result!");
+ }
+ // Trick the optimizer and also satisfy the type checker.
+ get_size = sizeof(buffer);
+ std::byte* local_buffer = get_buffer;
+ unsigned local_size = get_size;
+ return span(local_buffer, local_size);
+}
+
+#ifdef ENABLE_CRC
+PW_NO_INLINE
+unsigned RunChecksum() {
+ // Trigger varint
+ varint::Encode(55, GetBufferSpan());
+ uint64_t decoded_number;
+ size_t num_bytes = varint::Decode(GetBufferSpan(), &decoded_number);
+
+ // Calculate the checksum and stash it in a volatile variable so the compiler
+ // can't optimize it out.
+ ::pw::checksum::Crc32 checksum;
+ checksum.Update(GetBufferSpan());
+ uint32_t value = static_cast<uint32_t>(checksum.value());
+ value += num_bytes + static_cast<uint32_t>(decoded_number);
+ *get_buffer = static_cast<std::byte>(value);
+ return 0;
+}
+#endif
+
+PW_NO_INLINE
+void HdlcSizeReport() {
+ // Create a buffer with some data to bloat the binary with stream and related.
+ std::array<std::byte, 100> data;
+ std::fill(data.begin(), data.end(), std::byte(0));
+ stream::MemoryWriterBuffer<250> destination;
+ destination.Write(data);
+ if (destination.Write(GetBufferSpan()).ok()) {
+ PW_LOG_INFO("Wrote successfully");
+ }
+
+#ifdef ENABLE_CRC
+ RunChecksum();
+#endif
+
+#ifdef ENABLE_ENCODE
+ hdlc::WriteUIFrame(123, data, destination);
+#endif
+
+#ifdef ENABLE_DECODE
+ hdlc::Decoder decoder(data);
+ Result<hdlc::Frame> frame = decoder.Process(std::byte('~'));
+ // Force use of nodiscard frame.
+ if (frame.ok()) {
+ get_size = get_size + 1;
+ }
+ decoder.Clear();
+#endif
+}
+
+} // namespace pw::size_report
+
+int main() {
+ pw::bloat::BloatThisBinary();
+ pw::size_report::HdlcSizeReport();
+ return 0;
+}
diff --git a/pw_hdlc/ts/BUILD.bazel b/pw_hdlc/ts/BUILD.bazel
deleted file mode 100644
index f4999ff6e..000000000
--- a/pw_hdlc/ts/BUILD.bazel
+++ /dev/null
@@ -1,68 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@npm//@bazel/typescript:index.bzl", "ts_library", "ts_project")
-load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
-
-package(default_visibility = ["//visibility:public"])
-
-ts_project(
- name = "lib",
- srcs = [
- "decoder.ts",
- "encoder.ts",
- "index.ts",
- "protocol.ts",
- "util.ts",
- ],
- declaration = True,
- source_map = True,
- deps = ["@npm//:node_modules"], # can't use fine-grained deps
-)
-
-js_library(
- name = "pw_hdlc",
- package_name = "@pigweed/pw_hdlc",
- srcs = ["package.json"],
- deps = [":lib"],
-)
-
-ts_library(
- name = "hdlc_test_lib",
- srcs = [
- "decoder_test.ts",
- "encoder_test.ts",
- ],
- deps = [
- ":lib",
- "@npm//@types/jasmine",
- "@npm//@types/node",
- "@npm//buffer",
- ],
-)
-
-jasmine_node_test(
- name = "hdlc_test",
- srcs = [
- ":hdlc_test_lib",
- ],
-)
-
-# needed for embedding into downstream projects
-filegroup(name = "pw_hdlc__contents")
-
-filegroup(name = "pw_hdlc__files")
-
-filegroup(name = "pw_hdlc__nested_node_modules")
diff --git a/pw_hdlc/ts/decoder_test.ts b/pw_hdlc/ts/decoder_test.ts
index af12ca3db..4a1996441 100644
--- a/pw_hdlc/ts/decoder_test.ts
+++ b/pw_hdlc/ts/decoder_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,8 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
-import 'jasmine';
+/* eslint-env browser */
import {Decoder} from './decoder';
import * as protocol from './protocol';
diff --git a/pw_hdlc/ts/encoder_test.ts b/pw_hdlc/ts/encoder_test.ts
index cb8a9b4de..2780fa93e 100644
--- a/pw_hdlc/ts/encoder_test.ts
+++ b/pw_hdlc/ts/encoder_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,10 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
-import 'jasmine';
-
-import {Buffer} from 'buffer';
+/* eslint-env browser */
import {Encoder} from './encoder';
import * as protocol from './protocol';
diff --git a/pw_hdlc/ts/package.json b/pw_hdlc/ts/package.json
deleted file mode 100644
index bd8660dd7..000000000
--- a/pw_hdlc/ts/package.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "name": "@pigweed/pw_hdlc",
- "version": "1.0.0",
- "main": "index.js",
- "license": "Apache-2.0",
- "dependencies": {
- "@bazel/jasmine": "^4.1.0",
- "@types/crc": "^3.4.0",
- "@types/jasmine": "^3.9.0",
- "buffer": "^6.0.3",
- "crc": "^3.8.0",
- "jasmine": "^3.9.0",
- "jasmine-core": "^3.9.0"
- }
-}
diff --git a/pw_hdlc/ts/tsconfig.json b/pw_hdlc/ts/tsconfig.json
deleted file mode 100644
index 4ddd637e9..000000000
--- a/pw_hdlc/ts/tsconfig.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "compilerOptions": {
- "allowUnreachableCode": false,
- "allowUnusedLabels": false,
- "declaration": true,
- "forceConsistentCasingInFileNames": true,
- "lib": [
- "es2018",
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "module": "commonjs",
- "noEmitOnError": true,
- "noFallthroughCasesInSwitch": true,
- "noImplicitReturns": true,
- "pretty": true,
- "sourceMap": true,
- "strict": true,
- "target": "es2018",
- "jsx": "react",
- "plugins": [
- {
- "name": "@bazel/tsetse",
- "disabledRules": [
- "must-use-promises"
- ]
- }
- ]
- },
- "exclude": [
- "node_modules"
- ]
-}
diff --git a/pw_hdlc/ts/yarn.lock b/pw_hdlc/ts/yarn.lock
deleted file mode 100644
index 640750233..000000000
--- a/pw_hdlc/ts/yarn.lock
+++ /dev/null
@@ -1,512 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@bazel/jasmine@^4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-4.1.0.tgz#ec9fc5179af265de47aba8bb40a094e9b062aab2"
- integrity sha512-AUKzBZ12qKkcI5apXzL/2VKfsF4tHkdLPNsF/p6gEnIW4/aYb6M9wZOFsUh1MLYds+kqx1zN90EGfiZKa6wbOw==
- dependencies:
- c8 "~7.5.0"
- jasmine-reporters "~2.4.0"
-
-"@bcoe/v8-coverage@^0.2.3":
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
- integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
-
-"@istanbuljs/schema@^0.1.2":
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
- integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
-
-"@types/crc@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@types/crc/-/crc-3.4.0.tgz#2366beb4399cd734b33e42c7ac809576e617d48a"
- integrity sha1-I2a+tDmc1zSzPkLHrICVduYX1Io=
- dependencies:
- "@types/node" "*"
-
-"@types/is-windows@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-1.0.0.tgz#1011fa129d87091e2f6faf9042d6704cdf2e7be0"
- integrity sha512-tJ1rq04tGKuIJoWIH0Gyuwv4RQ3+tIu7wQrC0MV47raQ44kIzXSSFKfrxFUOWVRvesoF7mrTqigXmqoZJsXwTg==
-
-"@types/istanbul-lib-coverage@^2.0.1":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
- integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
-
-"@types/jasmine@^3.9.0":
- version "3.9.1"
- resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.9.1.tgz#94c65ee8bf9d24d9e1d84abaed57b6e0da8b49de"
- integrity sha512-PVpjh8S8lqKFKurWSKdFATlfBHGPzgy0PoDdzQ+rr78jTQ0aacyh9YndzZcAUPxhk4kRujItFFGQdUJ7flHumw==
-
-"@types/node@*":
- version "16.9.1"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
- integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
-
-ansi-regex@^5.0.0:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
- integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
- dependencies:
- color-convert "^2.0.1"
-
-balanced-match@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
- integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-js@^1.3.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
- integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-buffer@^5.1.0:
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
- integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
- dependencies:
- base64-js "^1.3.1"
- ieee754 "^1.1.13"
-
-buffer@^6.0.3:
- version "6.0.3"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
- integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
- dependencies:
- base64-js "^1.3.1"
- ieee754 "^1.2.1"
-
-c8@~7.5.0:
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/c8/-/c8-7.5.0.tgz#a69439ab82848f344a74bb25dc5dd4e867764481"
- integrity sha512-GSkLsbvDr+FIwjNSJ8OwzWAyuznEYGTAd1pzb/Kr0FMLuV4vqYJTyjboDTwmlUNAG6jAU3PFWzqIdKrOt1D8tw==
- dependencies:
- "@bcoe/v8-coverage" "^0.2.3"
- "@istanbuljs/schema" "^0.1.2"
- find-up "^5.0.0"
- foreground-child "^2.0.0"
- furi "^2.0.0"
- istanbul-lib-coverage "^3.0.0"
- istanbul-lib-report "^3.0.0"
- istanbul-reports "^3.0.2"
- rimraf "^3.0.0"
- test-exclude "^6.0.0"
- v8-to-istanbul "^7.1.0"
- yargs "^16.0.0"
- yargs-parser "^20.0.0"
-
-cliui@^7.0.2:
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
- integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^7.0.0"
-
-color-convert@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
- integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
- dependencies:
- color-name "~1.1.4"
-
-color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-
-convert-source-map@^1.6.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
- integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
- dependencies:
- safe-buffer "~5.1.1"
-
-crc@^3.8.0:
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
- integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
- dependencies:
- buffer "^5.1.0"
-
-cross-spawn@^7.0.0:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
- integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
- dependencies:
- path-key "^3.1.0"
- shebang-command "^2.0.0"
- which "^2.0.1"
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-escalade@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
- integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-find-up@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
- integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
- dependencies:
- locate-path "^6.0.0"
- path-exists "^4.0.0"
-
-foreground-child@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53"
- integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==
- dependencies:
- cross-spawn "^7.0.0"
- signal-exit "^3.0.2"
-
-fs.realpath@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
- integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-
-furi@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/furi/-/furi-2.0.0.tgz#13d85826a1af21acc691da6254b3888fc39f0b4a"
- integrity sha512-uKuNsaU0WVaK/vmvj23wW1bicOFfyqSsAIH71bRZx8kA4Xj+YCHin7CJKJJjkIsmxYaPFLk9ljmjEyB7xF7WvQ==
- dependencies:
- "@types/is-windows" "^1.0.0"
- is-windows "^1.0.2"
-
-get-caller-file@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
- integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
- version "7.1.7"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
- integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-has-flag@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
- integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-html-escaper@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
- integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
-
-ieee754@^1.1.13, ieee754@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
- integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-inflight@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
- integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
- dependencies:
- once "^1.3.0"
- wrappy "1"
-
-inherits@2:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-windows@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
- integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
-
-isexe@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
- integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
-
-istanbul-lib-coverage@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz#f5944a37c70b550b02a78a5c3b2055b280cec8ec"
- integrity sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==
-
-istanbul-lib-report@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
- integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==
- dependencies:
- istanbul-lib-coverage "^3.0.0"
- make-dir "^3.0.0"
- supports-color "^7.1.0"
-
-istanbul-reports@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
- integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
- dependencies:
- html-escaper "^2.0.0"
- istanbul-lib-report "^3.0.0"
-
-jasmine-core@^3.9.0, jasmine-core@~3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.9.0.tgz#09a3c8169fe98ec69440476d04a0e4cb4d59e452"
- integrity sha512-Tv3kVbPCGVrjsnHBZ38NsPU3sDOtNa0XmbG2baiyJqdb5/SPpDO6GVwJYtUryl6KB4q1Ssckwg612ES9Z0dreQ==
-
-jasmine-reporters@~2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.4.0.tgz#708c17ae70ba6671e3a930bb1b202aab80a31409"
- integrity sha512-jxONSrBLN1vz/8zCx5YNWQSS8iyDAlXQ5yk1LuqITe4C6iXCDx5u6Q0jfNtkKhL4qLZPe69fL+AWvXFt9/x38w==
- dependencies:
- mkdirp "^0.5.1"
- xmldom "^0.5.0"
-
-jasmine@^3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.9.0.tgz#286c4f9f88b69defc24acf3989af5533d5c6a0e6"
- integrity sha512-JgtzteG7xnqZZ51fg7N2/wiQmXon09szkALcRMTgCMX4u/m17gVJFjObnvw5FXkZOWuweHPaPRVB6DI2uN0wVA==
- dependencies:
- glob "^7.1.6"
- jasmine-core "~3.9.0"
-
-locate-path@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
- integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
- dependencies:
- p-locate "^5.0.0"
-
-make-dir@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
- integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
- dependencies:
- semver "^6.0.0"
-
-minimatch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimist@^1.2.5:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
- integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
-
-mkdirp@^0.5.1:
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
- integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
- dependencies:
- minimist "^1.2.5"
-
-once@^1.3.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
- integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
- dependencies:
- wrappy "1"
-
-p-limit@^3.0.2:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
- integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
- dependencies:
- yocto-queue "^0.1.0"
-
-p-locate@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
- integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
- dependencies:
- p-limit "^3.0.2"
-
-path-exists@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
- integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-is-absolute@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
- integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-
-path-key@^3.1.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
- integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-
-require-directory@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
- integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
-
-rimraf@^3.0.0:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
-safe-buffer@~5.1.1:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
- integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
-semver@^6.0.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
- integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-shebang-command@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
- integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
- dependencies:
- shebang-regex "^3.0.0"
-
-shebang-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
- integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-
-signal-exit@^3.0.2:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
- integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
-
-source-map@^0.7.3:
- version "0.7.3"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
- integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
-
-string-width@^4.1.0, string-width@^4.2.0:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
- integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.0"
-
-strip-ansi@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
- integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
- dependencies:
- ansi-regex "^5.0.0"
-
-supports-color@^7.1.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
- integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
- dependencies:
- has-flag "^4.0.0"
-
-test-exclude@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
- integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
- dependencies:
- "@istanbuljs/schema" "^0.1.2"
- glob "^7.1.4"
- minimatch "^3.0.4"
-
-v8-to-istanbul@^7.1.0:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1"
- integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==
- dependencies:
- "@types/istanbul-lib-coverage" "^2.0.1"
- convert-source-map "^1.6.0"
- source-map "^0.7.3"
-
-which@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
- integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
- dependencies:
- isexe "^2.0.0"
-
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrappy@1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
- integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-
-xmldom@^0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e"
- integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==
-
-y18n@^5.0.5:
- version "5.0.8"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
- integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yargs-parser@^20.0.0, yargs-parser@^20.2.2:
- version "20.2.9"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
- integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
-yargs@^16.0.0:
- version "16.2.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
- integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
- dependencies:
- cliui "^7.0.2"
- escalade "^3.1.1"
- get-caller-file "^2.0.5"
- require-directory "^2.1.1"
- string-width "^4.2.0"
- y18n "^5.0.5"
- yargs-parser "^20.2.2"
-
-yocto-queue@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
- integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
diff --git a/pw_hdlc/wire_packet_parser.cc b/pw_hdlc/wire_packet_parser.cc
index 992dfdee4..d86718095 100644
--- a/pw_hdlc/wire_packet_parser.cc
+++ b/pw_hdlc/wire_packet_parser.cc
@@ -22,7 +22,7 @@
namespace pw::hdlc {
bool WirePacketParser::Parse(ConstByteSpan packet) {
- if (packet.size_bytes() < Frame::kMinSizeBytes) {
+ if (packet.size_bytes() < Frame::kMinContentSizeBytes) {
return false;
}
diff --git a/pw_hex_dump/BUILD.gn b/pw_hex_dump/BUILD.gn
index 7f0bb441f..2ad280e35 100644
--- a/pw_hex_dump/BUILD.gn
+++ b/pw_hex_dump/BUILD.gn
@@ -26,6 +26,7 @@ pw_source_set("pw_hex_dump") {
public_configs = [ ":default_config" ]
public_deps = [
dir_pw_bytes,
+ dir_pw_span,
dir_pw_status,
]
deps = [ dir_pw_string ]
diff --git a/pw_hex_dump/hex_dump.cc b/pw_hex_dump/hex_dump.cc
index 1758b528f..e13b45b37 100644
--- a/pw_hex_dump/hex_dump.cc
+++ b/pw_hex_dump/hex_dump.cc
@@ -66,7 +66,7 @@ void AddGroupingByte(size_t byte_index,
} // namespace
-Status DumpAddr(std::span<char> dest, uintptr_t addr) {
+Status DumpAddr(span<char> dest, uintptr_t addr) {
if (dest.data() == nullptr) {
return Status::InvalidArgument();
}
@@ -145,15 +145,15 @@ Status FormattedHexDumper::DumpLine() {
// easy way to control zero padding for hex address.
if (flags.prefix_mode != AddressMode::kDisabled) {
uintptr_t val;
+ size_t significant;
if (flags.prefix_mode == AddressMode::kAbsolute) {
val = reinterpret_cast<uintptr_t>(source_data_.data());
builder << "0x";
- uint8_t significant = HexDigitCount(val);
+ significant = HexDigitCount(val);
builder.append(sizeof(uintptr_t) * 2 - significant, '0');
} else {
val = current_offset_;
- size_t significant =
- HexDigitCount(source_data_.size_bytes() + current_offset_);
+ significant = HexDigitCount(val);
if (significant < kMinOffsetChars) {
builder.append(kMinOffsetChars - significant, '0');
}
@@ -161,7 +161,7 @@ Status FormattedHexDumper::DumpLine() {
if (val != 0) {
builder << reinterpret_cast<void*>(val);
} else {
- builder.append(2, '0');
+ builder.append(significant, '0');
}
builder << kAddressSeparator;
}
@@ -198,7 +198,7 @@ Status FormattedHexDumper::DumpLine() {
return builder.status();
}
-Status FormattedHexDumper::SetLineBuffer(std::span<char> dest) {
+Status FormattedHexDumper::SetLineBuffer(span<char> dest) {
if (dest.data() == nullptr || dest.size_bytes() == 0) {
return Status::InvalidArgument();
}
diff --git a/pw_hex_dump/hex_dump_test.cc b/pw_hex_dump/hex_dump_test.cc
index c2b4126bc..a95b292d6 100644
--- a/pw_hex_dump/hex_dump_test.cc
+++ b/pw_hex_dump/hex_dump_test.cc
@@ -18,11 +18,11 @@
#include <cinttypes>
#include <cstdint>
#include <cstring>
-#include <span>
#include <string_view>
#include "gtest/gtest.h"
#include "pw_log/log.h"
+#include "pw_span/span.h"
namespace pw::dump {
namespace {
@@ -308,8 +308,8 @@ TEST_F(HexDump, FormattedHexDump_AsciiHeaderGroupEvery) {
}
TEST_F(HexDump, FormattedHexDump_OffsetPrefix) {
- constexpr const char* expected1 = "0000";
- constexpr const char* expected2 = "0010";
+ constexpr const char* expected1 = "0000:";
+ constexpr const char* expected2 = "0010:";
default_flags_.bytes_per_line = 16;
default_flags_.prefix_mode = FormattedHexDumper::AddressMode::kOffset;
@@ -329,14 +329,52 @@ TEST_F(HexDump, FormattedHexDump_OffsetPrefix) {
EXPECT_STREQ(expected2, dest_.data());
}
+TEST_F(HexDump, FormattedHexDump_OffsetPrefix_ShortLine) {
+ constexpr const char* expected = "0000:";
+
+ default_flags_.bytes_per_line = 16;
+ default_flags_.prefix_mode = FormattedHexDumper::AddressMode::kOffset;
+ dumper_ = FormattedHexDumper(dest_, default_flags_);
+
+ EXPECT_TRUE(dumper_.BeginDump(pw::span(source_data).first(8)).ok());
+ // Dump first and only line.
+ EXPECT_TRUE(dumper_.DumpLine().ok());
+ // Truncate string to only contain the offset.
+ dest_[strlen(expected)] = '\0';
+ EXPECT_STREQ(expected, dest_.data());
+}
+
+TEST_F(HexDump, FormattedHexDump_OffsetPrefix_LongData) {
+ constexpr std::array<std::byte, 300> long_data = {std::byte{0xff}};
+
+ constexpr const char* expected1 = "0000:";
+ constexpr const char* expected2 = "0010:";
+
+ default_flags_.bytes_per_line = 16;
+ default_flags_.prefix_mode = FormattedHexDumper::AddressMode::kOffset;
+ dumper_ = FormattedHexDumper(dest_, default_flags_);
+
+ EXPECT_TRUE(dumper_.BeginDump(long_data).ok());
+ // Dump first line.
+ EXPECT_TRUE(dumper_.DumpLine().ok());
+ // Truncate string to only contain the offset.
+ dest_[strlen(expected1)] = '\0';
+ EXPECT_STREQ(expected1, dest_.data());
+
+ // Dump second line.
+ EXPECT_TRUE(dumper_.DumpLine().ok());
+ // Truncate string to only contain the offset.
+ dest_[strlen(expected2)] = '\0';
+ EXPECT_STREQ(expected2, dest_.data());
+}
+
TEST_F(HexDump, FormattedHexDump_AbsolutePrefix) {
constexpr size_t kTestBytesPerLine = 16;
std::array<char, kHexAddrStringSize + 1> expected1;
std::array<char, kHexAddrStringSize + 1> expected2;
- DumpAddr(expected1, source_data.data())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- DumpAddr(expected2, source_data.data() + kTestBytesPerLine)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), DumpAddr(expected1, source_data.data()));
+ ASSERT_EQ(OkStatus(),
+ DumpAddr(expected2, source_data.data() + kTestBytesPerLine));
default_flags_.bytes_per_line = kTestBytesPerLine;
default_flags_.prefix_mode = FormattedHexDumper::AddressMode::kAbsolute;
@@ -346,12 +384,14 @@ TEST_F(HexDump, FormattedHexDump_AbsolutePrefix) {
// Dump first line.
EXPECT_TRUE(dumper_.DumpLine().ok());
// Truncate string to only contain the offset.
+ EXPECT_EQ(dest_[kHexAddrStringSize], ':');
dest_[kHexAddrStringSize] = '\0';
EXPECT_STREQ(expected1.data(), dest_.data());
// Dump second line.
EXPECT_TRUE(dumper_.DumpLine().ok());
// Truncate string to only contain the offset.
+ EXPECT_EQ(dest_[kHexAddrStringSize], ':');
dest_[kHexAddrStringSize] = '\0';
EXPECT_STREQ(expected2.data(), dest_.data());
}
@@ -405,7 +445,7 @@ TEST_F(SmallBuffer, PrefixIncreasesBufferRequirement) {
TEST(BadBuffer, ZeroSize) {
char buffer[1] = {static_cast<char>(0xaf)};
- FormattedHexDumper dumper(std::span<char>(buffer, 0));
+ FormattedHexDumper dumper(span<char>(buffer, 0));
EXPECT_EQ(dumper.BeginDump(source_data), Status::FailedPrecondition());
EXPECT_EQ(dumper.DumpLine(), Status::FailedPrecondition());
EXPECT_EQ(buffer[0], static_cast<char>(0xaf));
@@ -413,7 +453,7 @@ TEST(BadBuffer, ZeroSize) {
TEST(BadBuffer, NullPtrDest) {
FormattedHexDumper dumper;
- EXPECT_EQ(dumper.SetLineBuffer(std::span<char>()), Status::InvalidArgument());
+ EXPECT_EQ(dumper.SetLineBuffer(span<char>()), Status::InvalidArgument());
EXPECT_EQ(dumper.BeginDump(source_data), Status::FailedPrecondition());
EXPECT_EQ(dumper.DumpLine(), Status::FailedPrecondition());
}
@@ -421,7 +461,8 @@ TEST(BadBuffer, NullPtrDest) {
TEST(BadBuffer, NullPtrSrc) {
char buffer[24] = {static_cast<char>(0)};
FormattedHexDumper dumper(buffer);
- EXPECT_EQ(dumper.BeginDump(ByteSpan(nullptr, 64)), Status::InvalidArgument());
+ EXPECT_EQ(dumper.BeginDump(ByteSpan(static_cast<std::byte*>(nullptr), 64)),
+ Status::InvalidArgument());
// Don't actually dump nullptr in this test as it could cause a crash.
}
diff --git a/pw_hex_dump/public/pw_hex_dump/hex_dump.h b/pw_hex_dump/public/pw_hex_dump/hex_dump.h
index aaf6ef1b2..b08ce0414 100644
--- a/pw_hex_dump/public/pw_hex_dump/hex_dump.h
+++ b/pw_hex_dump/public/pw_hex_dump/hex_dump.h
@@ -15,9 +15,9 @@
#pragma once
#include <cstdint>
-#include <span>
#include "pw_bytes/span.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::dump {
@@ -97,17 +97,17 @@ class FormattedHexDumper {
.prefix_mode = AddressMode::kOffset};
FormattedHexDumper() = default;
- FormattedHexDumper(std::span<char> dest) {
+ FormattedHexDumper(span<char> dest) {
SetLineBuffer(dest)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
- FormattedHexDumper(std::span<char> dest, Flags config_flags)
+ FormattedHexDumper(span<char> dest, Flags config_flags)
: flags(config_flags) {
SetLineBuffer(dest)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
- // TODO(pwbug/218): Add iterator support.
+ // TODO(b/234892215): Add iterator support.
// Set the destination buffer that the hex dumper will write to line-by-line.
//
@@ -116,7 +116,7 @@ class FormattedHexDumper {
// current formatting configuration.
// INVALID_ARGUMENT - The destination buffer is invalid (nullptr or zero-
// length).
- Status SetLineBuffer(std::span<char> dest);
+ Status SetLineBuffer(span<char> dest);
// Begin dumping the provided data. Does NOT populate the line buffer with
// a string, simply resets the statefulness to track this buffer.
@@ -151,7 +151,7 @@ class FormattedHexDumper {
Status PrintFormatHeader();
size_t current_offset_;
- std::span<char> dest_;
+ span<char> dest_;
ConstByteSpan source_data_;
};
@@ -169,8 +169,8 @@ class FormattedHexDumper {
// OK - Address has been written to the buffer.
// INVALID_ARGUMENT - The destination buffer is invalid (nullptr).
// RESOURCE_EXHAUSTED - The destination buffer is too small. No data written.
-Status DumpAddr(std::span<char> dest, uintptr_t addr);
-inline Status DumpAddr(std::span<char> dest, const void* ptr) {
+Status DumpAddr(span<char> dest, uintptr_t addr);
+inline Status DumpAddr(span<char> dest, const void* ptr) {
uintptr_t addr = reinterpret_cast<uintptr_t>(ptr);
return DumpAddr(dest, addr);
}
diff --git a/pw_i2c/BUILD.bazel b/pw_i2c/BUILD.bazel
index a70cf9f63..98afc2230 100644
--- a/pw_i2c/BUILD.bazel
+++ b/pw_i2c/BUILD.bazel
@@ -103,6 +103,7 @@ pw_cc_library(
":address",
":initiator",
"//pw_assert",
+ "//pw_containers",
"//pw_containers:to_array",
"//pw_unit_test",
],
@@ -128,6 +129,7 @@ pw_cc_test(
deps = [
":initiator_mock",
"//pw_bytes",
+ "//pw_containers",
"//pw_unit_test",
],
)
@@ -139,6 +141,8 @@ pw_cc_test(
],
deps = [
":device",
+ ":initiator_mock",
+ "//pw_containers",
"//pw_unit_test",
],
)
diff --git a/pw_i2c/BUILD.gn b/pw_i2c/BUILD.gn
index e049cde89..700f8edad 100644
--- a/pw_i2c/BUILD.gn
+++ b/pw_i2c/BUILD.gn
@@ -50,6 +50,7 @@ pw_source_set("device") {
"$dir_pw_bytes",
"$dir_pw_chrono:system_clock",
"$dir_pw_status",
+ dir_pw_span,
]
}
@@ -76,6 +77,7 @@ pw_source_set("mock") {
public_deps = [
":initiator",
"$dir_pw_bytes",
+ "$dir_pw_containers",
"$dir_pw_containers:to_array",
]
deps = [
@@ -111,7 +113,11 @@ pw_test("address_test") {
pw_test("device_test") {
enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
sources = [ "device_test.cc" ]
- deps = [ ":device" ]
+ deps = [
+ ":device",
+ ":mock",
+ "$dir_pw_containers",
+ ]
}
pw_test("register_device_test") {
@@ -126,7 +132,10 @@ pw_test("register_device_test") {
pw_test("initiator_mock_test") {
enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
sources = [ "initiator_mock_test.cc" ]
- deps = [ ":mock" ]
+ deps = [
+ ":mock",
+ "$dir_pw_containers",
+ ]
}
pw_doc_group("docs") {
diff --git a/pw_i2c/address_test.cc b/pw_i2c/address_test.cc
index 71ac11fe7..0d3a90e83 100644
--- a/pw_i2c/address_test.cc
+++ b/pw_i2c/address_test.cc
@@ -39,10 +39,10 @@ TEST(Address, TenBitRuntimeChecked) {
EXPECT_EQ(ten_bit.GetTenBit(), Address::kMaxTenBitAddress);
}
-// TODO(pwbug/88): Verify assert behaviour when trying to get a 7bit address out
-// of a 10bit address.
+// TODO(b/235289499): Verify assert behaviour when trying to get a 7bit address
+// out of a 10bit address.
-// TODO(pwbug/47): Add tests to ensure the constexpr constructors fail to
+// TODO(b/234882063): Add tests to ensure the constexpr constructors fail to
// compile with invalid addresses once no-copmile tests are set up in Pigweed.
} // namespace pw::i2c
diff --git a/pw_i2c/device_test.cc b/pw_i2c/device_test.cc
index dea5b1ef9..367e6c825 100644
--- a/pw_i2c/device_test.cc
+++ b/pw_i2c/device_test.cc
@@ -11,42 +11,83 @@
// 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 "pw_i2c/device.h"
+#include <chrono>
+
#include "gtest/gtest.h"
#include "pw_bytes/byte_builder.h"
+#include "pw_containers/algorithm.h"
+#include "pw_i2c/initiator_mock.h"
-namespace pw {
-namespace i2c {
+using namespace std::literals::chrono_literals;
+
+namespace pw::i2c {
namespace {
-// Fake test initiator that's used for testing.
-class TestInitiator : public Initiator {
- public:
- explicit TestInitiator() {}
-
- private:
- Status DoWriteReadFor(Address,
- ConstByteSpan,
- ByteSpan,
- chrono::SystemClock::duration) override {
- // Empty implementation.
- return OkStatus();
- }
-
- ByteBuffer<10> write_buffer_;
- ByteBuffer<10> read_buffer_;
-};
-
-// This test just checks to make sure the Device object compiles.
-// TODO(b/185609270): Full test coverage.
-TEST(DeviceCompilationTest, CompileOk) {
+TEST(Device, WriteReadForOk) {
+ constexpr Address kTestDeviceAddress = Address::SevenBit<0x3F>();
+
+ constexpr auto kExpectWrite1 = bytes::Array<1, 2, 3>();
+ constexpr auto kExpectRead1 = bytes::Array<1, 2>();
+
+ auto expected_transactions = MakeExpectedTransactionArray({Transaction(
+ OkStatus(), kTestDeviceAddress, kExpectWrite1, kExpectRead1, 2ms)});
+
+ MockInitiator initiator(expected_transactions);
+
+ Device device = Device(initiator, kTestDeviceAddress);
+
+ std::array<std::byte, kExpectRead1.size()> read1;
+ EXPECT_EQ(device.WriteReadFor(kExpectWrite1, read1, 2ms), OkStatus());
+ EXPECT_TRUE(pw::containers::Equal(read1, kExpectRead1));
+ EXPECT_EQ(initiator.Finalize(), OkStatus());
+}
+
+TEST(Device, WriteForOk) {
+ constexpr Address kTestDeviceAddress = Address::SevenBit<0x3F>();
+
+ constexpr auto kExpectWrite1 = bytes::Array<1, 2, 3>();
+
+ auto expected_transactions = MakeExpectedTransactionArray(
+ {WriteTransaction(OkStatus(), kTestDeviceAddress, kExpectWrite1, 2ms)});
+
+ MockInitiator initiator(expected_transactions);
+ Device device = Device(initiator, kTestDeviceAddress);
+
+ EXPECT_EQ(device.WriteFor(kExpectWrite1, 2ms), OkStatus());
+ EXPECT_EQ(initiator.Finalize(), OkStatus());
+}
+
+TEST(Device, ReadForOk) {
constexpr Address kTestDeviceAddress = Address::SevenBit<0x3F>();
- TestInitiator initiator;
+ constexpr auto kExpectRead1 = bytes::Array<1, 2, 3>();
+
+ auto expected_transactions = MakeExpectedTransactionArray(
+ {ReadTransaction(OkStatus(), kTestDeviceAddress, kExpectRead1, 2ms)});
+
+ MockInitiator initiator(expected_transactions);
Device device = Device(initiator, kTestDeviceAddress);
+
+ std::array<std::byte, kExpectRead1.size()> read1;
+ EXPECT_EQ(device.ReadFor(read1, 2ms), OkStatus());
+ EXPECT_EQ(initiator.Finalize(), OkStatus());
+}
+
+TEST(Device, ProbeDeviceForOk) {
+ constexpr Address kTestDeviceAddress = Address::SevenBit<0x3F>();
+
+ auto expected_transactions = MakeExpectedTransactionArray(
+ {ProbeTransaction(OkStatus(), kTestDeviceAddress, 2ms)});
+
+ MockInitiator initiator(expected_transactions);
+ Device device = Device(initiator, kTestDeviceAddress);
+
+ EXPECT_EQ(initiator.ProbeDeviceFor(kTestDeviceAddress, 2ms), OkStatus());
+ EXPECT_EQ(initiator.Finalize(), OkStatus());
}
} // namespace
-} // namespace i2c
-} // namespace pw
+} // namespace pw::i2c
diff --git a/pw_i2c/docs.rst b/pw_i2c/docs.rst
index 168a78412..24369689f 100644
--- a/pw_i2c/docs.rst
+++ b/pw_i2c/docs.rst
@@ -59,18 +59,21 @@ list. An example of this is shown below:
constexpr auto kExpectWrite1 = pw::bytes::Array<1, 2, 3, 4, 5>();
constexpr auto kExpectWrite2 = pw::bytes::Array<3, 4, 5>();
auto expected_transactions = MakeExpectedTransactionArray(
- {WriteTransaction(pw::OkStatus(), kAddress1, kExpectWrite1, 1ms),
+ {ProbeTransaction(pw::OkStatus, kAddress1, 2ms),
+ WriteTransaction(pw::OkStatus(), kAddress1, kExpectWrite1, 1ms),
WriteTransaction(pw::OkStatus(), kAddress2, kExpectWrite2, 1ms)});
MockInitiator i2c_mock(expected_transactions);
// Begin driver code
+ Status status = i2c_mock.ProbeDeviceFor(kAddress1, 2ms);
+
ConstByteSpan write1 = kExpectWrite1;
// write1 is ok as i2c_mock expects {1, 2, 3, 4, 5} == {1, 2, 3, 4, 5}
Status status = i2c_mock.WriteFor(kAddress1, write1, 2ms);
// Takes the first two bytes from the expected array to build a mismatching
// span to write.
- ConstByteSpan write2 = std::span(kExpectWrite2).first(2);
+ ConstByteSpan write2 = pw::span(kExpectWrite2).first(2);
// write2 fails as i2c_mock expects {3, 4, 5} != {3, 4}
status = i2c_mock.WriteFor(kAddress2, write2, 2ms);
// End driver code
diff --git a/pw_i2c/initiator_mock.cc b/pw_i2c/initiator_mock.cc
index 1056d29fc..7ba6bf84a 100644
--- a/pw_i2c/initiator_mock.cc
+++ b/pw_i2c/initiator_mock.cc
@@ -15,6 +15,7 @@
#include "gtest/gtest.h"
#include "pw_assert/check.h"
+#include "pw_containers/algorithm.h"
namespace pw::i2c {
@@ -36,10 +37,7 @@ Status MockInitiator::DoWriteReadFor(Address device_address,
ConstByteSpan expected_tx_buffer =
expected_transactions_[expected_transaction_index_].write_buffer();
- EXPECT_TRUE(std::equal(expected_tx_buffer.begin(),
- expected_tx_buffer.end(),
- tx_buffer.begin(),
- tx_buffer.end()));
+ EXPECT_TRUE(pw::containers::Equal(expected_tx_buffer, tx_buffer));
ConstByteSpan expected_rx_buffer =
expected_transactions_[expected_transaction_index_].read_buffer();
diff --git a/pw_i2c/initiator_mock_test.cc b/pw_i2c/initiator_mock_test.cc
index beed22f6a..1abbee1ed 100644
--- a/pw_i2c/initiator_mock_test.cc
+++ b/pw_i2c/initiator_mock_test.cc
@@ -16,12 +16,13 @@
#include <array>
#include <chrono>
-#include <span>
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
#include "pw_bytes/span.h"
+#include "pw_containers/algorithm.h"
#include "pw_i2c/address.h"
+#include "pw_span/span.h"
using namespace std::literals::chrono_literals;
@@ -43,13 +44,11 @@ TEST(Transaction, Read) {
std::array<std::byte, kExpectRead1.size()> read1;
EXPECT_EQ(mocked_i2c.ReadFor(kAddress1, read1, 2ms), OkStatus());
- EXPECT_TRUE(std::equal(
- read1.begin(), read1.end(), kExpectRead1.begin(), kExpectRead1.end()));
+ EXPECT_TRUE(pw::containers::Equal(read1, kExpectRead1));
std::array<std::byte, kExpectRead2.size()> read2;
EXPECT_EQ(mocked_i2c.ReadFor(kAddress2, read2, 2ms), OkStatus());
- EXPECT_TRUE(std::equal(
- read2.begin(), read2.end(), kExpectRead2.begin(), kExpectRead2.end()));
+ EXPECT_TRUE(pw::containers::Equal(read2, kExpectRead2));
EXPECT_EQ(mocked_i2c.Finalize(), OkStatus());
}
@@ -93,16 +92,28 @@ TEST(Transaction, WriteRead) {
std::array<std::byte, kExpectRead1.size()> read1;
EXPECT_EQ(mocked_i2c.WriteReadFor(kAddress1, kExpectWrite1, read1, 2ms),
OkStatus());
- EXPECT_TRUE(std::equal(read1.begin(), read1.end(), kExpectRead1.begin()));
+ EXPECT_TRUE(pw::containers::Equal(read1, kExpectRead1));
std::array<std::byte, kExpectRead1.size()> read2;
EXPECT_EQ(mocked_i2c.WriteReadFor(kAddress2, kExpectWrite2, read2, 2ms),
OkStatus());
- EXPECT_TRUE(std::equal(
- read2.begin(), read2.end(), kExpectRead2.begin(), kExpectRead2.end()));
+ EXPECT_TRUE(pw::containers::Equal(read2, kExpectRead2));
EXPECT_EQ(mocked_i2c.Finalize(), OkStatus());
}
+TEST(Transaction, Probe) {
+ static constexpr Address kAddress1 = Address::SevenBit<0x01>();
+
+ auto expected_transactions = MakeExpectedTransactionArray({
+ ProbeTransaction(OkStatus(), kAddress1, 2ms),
+ });
+
+ MockInitiator mock_initiator(expected_transactions);
+
+ EXPECT_EQ(mock_initiator.ProbeDeviceFor(kAddress1, 2ms), OkStatus());
+ EXPECT_EQ(mock_initiator.Finalize(), OkStatus());
+}
+
} // namespace
-} // namespace pw::i2c \ No newline at end of file
+} // namespace pw::i2c
diff --git a/pw_i2c/public/pw_i2c/initiator.h b/pw_i2c/public/pw_i2c/initiator.h
index 15d56fca5..8b09a67e0 100644
--- a/pw_i2c/public/pw_i2c/initiator.h
+++ b/pw_i2c/public/pw_i2c/initiator.h
@@ -83,8 +83,8 @@ class Initiator {
chrono::SystemClock::duration timeout) {
return WriteReadFor(
device_address,
- std::span(static_cast<const std::byte*>(tx_buffer), tx_size_bytes),
- std::span(static_cast<std::byte*>(rx_buffer), rx_size_bytes),
+ span(static_cast<const std::byte*>(tx_buffer), tx_size_bytes),
+ span(static_cast<std::byte*>(rx_buffer), rx_size_bytes),
timeout);
}
@@ -118,7 +118,7 @@ class Initiator {
chrono::SystemClock::duration timeout) {
return WriteFor(
device_address,
- std::span(static_cast<const std::byte*>(tx_buffer), tx_size_bytes),
+ span(static_cast<const std::byte*>(tx_buffer), tx_size_bytes),
timeout);
}
@@ -151,7 +151,7 @@ class Initiator {
size_t rx_size_bytes,
chrono::SystemClock::duration timeout) {
return ReadFor(device_address,
- std::span(static_cast<std::byte*>(rx_buffer), rx_size_bytes),
+ span(static_cast<std::byte*>(rx_buffer), rx_size_bytes),
timeout);
}
diff --git a/pw_i2c/public/pw_i2c/initiator_mock.h b/pw_i2c/public/pw_i2c/initiator_mock.h
index 67fc2fadb..7ef3936af 100644
--- a/pw_i2c/public/pw_i2c/initiator_mock.h
+++ b/pw_i2c/public/pw_i2c/initiator_mock.h
@@ -40,6 +40,17 @@ class Transaction {
address_(device_address),
timeout_(timeout) {}
+ // Alternate Transaction constructor for use in ProbeTransaction.
+ constexpr Transaction(
+ Status expected_return_value,
+ Address device_address,
+ std::optional<chrono::SystemClock::duration> timeout = std::nullopt)
+ : Transaction(expected_return_value,
+ device_address,
+ ConstByteSpan(),
+ ignored_buffer_,
+ timeout) {}
+
// Gets the buffer that is virtually read.
ConstByteSpan read_buffer() const { return read_buffer_; }
@@ -61,6 +72,7 @@ class Transaction {
const Status return_value_;
const ConstByteSpan read_buffer_;
const ConstByteSpan write_buffer_;
+ static constexpr std::array<std::byte, 1> ignored_buffer_ = {};
const Address address_;
const std::optional<chrono::SystemClock::duration> timeout_;
};
@@ -91,6 +103,15 @@ constexpr Transaction WriteTransaction(
timeout);
}
+// ProbeTransaction is a helper that constructs a one-byte read transaction.
+// For use in testing Probe transactions with the Mock Initiator.
+constexpr Transaction ProbeTransaction(
+ Status expected_return_value,
+ Address device_address,
+ std::optional<chrono::SystemClock::duration> timeout = std::nullopt) {
+ return Transaction(expected_return_value, device_address, timeout);
+}
+
// MockInitiator takes a series of read and/or write transactions and
// compares them against user/driver input.
//
@@ -99,7 +120,7 @@ constexpr Transaction WriteTransaction(
// frame.
class MockInitiator : public Initiator {
public:
- explicit constexpr MockInitiator(std::span<Transaction> transaction_list)
+ explicit constexpr MockInitiator(span<Transaction> transaction_list)
: expected_transactions_(transaction_list),
expected_transaction_index_(0) {}
@@ -116,7 +137,7 @@ class MockInitiator : public Initiator {
}
// Runs Finalize() regardless of whether it was already optionally finalized.
- ~MockInitiator();
+ ~MockInitiator() override;
private:
// Implements a mocked backend for the i2c initiator.
@@ -137,7 +158,7 @@ class MockInitiator : public Initiator {
ByteSpan rx_buffer,
chrono::SystemClock::duration timeout) override;
- std::span<Transaction> expected_transactions_;
+ span<Transaction> expected_transactions_;
size_t expected_transaction_index_;
};
diff --git a/pw_i2c/public/pw_i2c/register_device.h b/pw_i2c/public/pw_i2c/register_device.h
index 3d3cfcedb..0f9b4df39 100644
--- a/pw_i2c/public/pw_i2c/register_device.h
+++ b/pw_i2c/public/pw_i2c/register_device.h
@@ -66,8 +66,8 @@ class RegisterDevice : public Device {
// register_address_size: Size of the register address.
constexpr RegisterDevice(Initiator& initiator,
Address address,
- std::endian register_address_order,
- std::endian data_order,
+ endian register_address_order,
+ endian data_order,
RegisterAddressSize register_address_size)
: Device(initiator, address),
register_address_order_(register_address_order),
@@ -81,7 +81,7 @@ class RegisterDevice : public Device {
// register_address_size: Size of the register address.
constexpr RegisterDevice(Initiator& initiator,
Address address,
- std::endian order,
+ endian order,
RegisterAddressSize register_address_size)
: Device(initiator, address),
register_address_order_(order),
@@ -119,17 +119,17 @@ class RegisterDevice : public Device {
chrono::SystemClock::duration timeout);
Status WriteRegisters8(uint32_t register_address,
- std::span<const uint8_t> register_data,
+ span<const uint8_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout);
Status WriteRegisters16(uint32_t register_address,
- std::span<const uint16_t> register_data,
+ span<const uint16_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout);
Status WriteRegisters32(uint32_t register_address,
- std::span<const uint32_t> register_data,
+ span<const uint32_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout);
@@ -159,15 +159,15 @@ class RegisterDevice : public Device {
chrono::SystemClock::duration timeout);
Status ReadRegisters8(uint32_t register_address,
- std::span<uint8_t> return_data,
+ span<uint8_t> return_data,
chrono::SystemClock::duration timeout);
Status ReadRegisters16(uint32_t register_address,
- std::span<uint16_t> return_data,
+ span<uint16_t> return_data,
chrono::SystemClock::duration timeout);
Status ReadRegisters32(uint32_t register_address,
- std::span<uint32_t> return_data,
+ span<uint32_t> return_data,
chrono::SystemClock::duration timeout);
// Writes the register address first before data.
@@ -241,8 +241,8 @@ class RegisterDevice : public Device {
ByteSpan buffer,
chrono::SystemClock::duration timeout);
- const std::endian register_address_order_;
- const std::endian data_order_;
+ const endian register_address_order_;
+ const endian data_order_;
const RegisterAddressSize register_address_size_;
};
@@ -260,11 +260,11 @@ inline Status RegisterDevice::WriteRegisters(
inline Status RegisterDevice::WriteRegisters8(
uint32_t register_address,
- std::span<const uint8_t> register_data,
+ span<const uint8_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout) {
return WriteRegisters(register_address,
- std::as_bytes(register_data),
+ as_bytes(register_data),
sizeof(decltype(register_data)::value_type),
buffer,
timeout);
@@ -272,11 +272,11 @@ inline Status RegisterDevice::WriteRegisters8(
inline Status RegisterDevice::WriteRegisters16(
uint32_t register_address,
- std::span<const uint16_t> register_data,
+ span<const uint16_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout) {
return WriteRegisters(register_address,
- std::as_bytes(register_data),
+ as_bytes(register_data),
sizeof(decltype(register_data)::value_type),
buffer,
timeout);
@@ -284,11 +284,11 @@ inline Status RegisterDevice::WriteRegisters16(
inline Status RegisterDevice::WriteRegisters32(
uint32_t register_address,
- std::span<const uint32_t> register_data,
+ span<const uint32_t> register_data,
ByteSpan buffer,
chrono::SystemClock::duration timeout) {
return WriteRegisters(register_address,
- std::as_bytes(register_data),
+ as_bytes(register_data),
sizeof(decltype(register_data)::value_type),
buffer,
timeout);
@@ -301,7 +301,7 @@ inline Status RegisterDevice::WriteRegister(
std::array<std::byte, sizeof(register_data) + sizeof(register_address)>
byte_buffer;
return WriteRegisters(register_address,
- std::span(&register_data, 1),
+ span(&register_data, 1),
sizeof(register_data),
byte_buffer,
timeout);
@@ -314,7 +314,7 @@ inline Status RegisterDevice::WriteRegister8(
std::array<std::byte, sizeof(register_data) + sizeof(register_address)>
byte_buffer;
return WriteRegisters(register_address,
- std::as_bytes(std::span(&register_data, 1)),
+ as_bytes(span(&register_data, 1)),
sizeof(register_data),
byte_buffer,
timeout);
@@ -327,7 +327,7 @@ inline Status RegisterDevice::WriteRegister16(
std::array<std::byte, sizeof(register_data) + sizeof(register_address)>
byte_buffer;
return WriteRegisters(register_address,
- std::as_bytes(std::span(&register_data, 1)),
+ as_bytes(span(&register_data, 1)),
sizeof(register_data),
byte_buffer,
timeout);
@@ -340,7 +340,7 @@ inline Status RegisterDevice::WriteRegister32(
std::array<std::byte, sizeof(register_data) + sizeof(register_address)>
byte_buffer;
return WriteRegisters(register_address,
- std::as_bytes(std::span(&register_data, 1)),
+ as_bytes(span(&register_data, 1)),
sizeof(register_data),
byte_buffer,
timeout);
@@ -348,20 +348,20 @@ inline Status RegisterDevice::WriteRegister32(
inline Status RegisterDevice::ReadRegisters8(
uint32_t register_address,
- std::span<uint8_t> return_data,
+ span<uint8_t> return_data,
chrono::SystemClock::duration timeout) {
// For a single byte, there's no endian data, and so we can return the
// data as is.
return ReadRegisters(
- register_address, std::as_writable_bytes(return_data), timeout);
+ register_address, as_writable_bytes(return_data), timeout);
}
inline Status RegisterDevice::ReadRegisters16(
uint32_t register_address,
- std::span<uint16_t> return_data,
+ span<uint16_t> return_data,
chrono::SystemClock::duration timeout) {
- PW_TRY(ReadRegisters(
- register_address, std::as_writable_bytes(return_data), timeout));
+ PW_TRY(
+ ReadRegisters(register_address, as_writable_bytes(return_data), timeout));
// Post process endian information.
for (uint16_t& register_value : return_data) {
@@ -373,10 +373,10 @@ inline Status RegisterDevice::ReadRegisters16(
inline Status RegisterDevice::ReadRegisters32(
uint32_t register_address,
- std::span<uint32_t> return_data,
+ span<uint32_t> return_data,
chrono::SystemClock::duration timeout) {
- PW_TRY(ReadRegisters(
- register_address, std::as_writable_bytes(return_data), timeout));
+ PW_TRY(
+ ReadRegisters(register_address, as_writable_bytes(return_data), timeout));
// TODO(b/185952662): Extend endian in pw_byte to support this conversion
// as optimization.
@@ -391,14 +391,14 @@ inline Status RegisterDevice::ReadRegisters32(
inline Result<std::byte> RegisterDevice::ReadRegister(
uint32_t register_address, chrono::SystemClock::duration timeout) {
std::byte data = {};
- PW_TRY(ReadRegisters(register_address, std::span(&data, 1), timeout));
+ PW_TRY(ReadRegisters(register_address, span(&data, 1), timeout));
return data;
}
inline Result<uint8_t> RegisterDevice::ReadRegister8(
uint32_t register_address, chrono::SystemClock::duration timeout) {
uint8_t data = 0;
- PW_TRY(ReadRegisters8(register_address, std::span(&data, 1), timeout));
+ PW_TRY(ReadRegisters8(register_address, span(&data, 1), timeout));
return data;
}
diff --git a/pw_i2c/register_device.cc b/pw_i2c/register_device.cc
index 188782d79..9c3b15e29 100644
--- a/pw_i2c/register_device.cc
+++ b/pw_i2c/register_device.cc
@@ -26,7 +26,7 @@ namespace {
void PutRegisterAddressInByteBuilder(
ByteBuilder& byte_builder,
const uint32_t register_address,
- const std::endian order,
+ const endian order,
RegisterAddressSize register_address_size) {
// TODO(b/185952662): Simplify the call site by extending the byte builder
// and endian API.
@@ -50,7 +50,7 @@ void PutRegisterAddressInByteBuilder(
void PutRegisterData16InByteBuilder(ByteBuilder& byte_builder,
ConstByteSpan register_data,
- const std::endian order) {
+ const endian order) {
uint32_t data_pointer_index = 0;
while (data_pointer_index < register_data.size()) {
@@ -63,7 +63,7 @@ void PutRegisterData16InByteBuilder(ByteBuilder& byte_builder,
Status PutRegisterData32InByteBuilder(ByteBuilder& byte_builder,
ConstByteSpan register_data,
- const std::endian order) {
+ const endian order) {
uint32_t data_pointer_index = 0;
while (data_pointer_index < register_data.size()) {
@@ -112,7 +112,7 @@ Status RegisterDevice::WriteRegisters(const uint32_t register_address,
case 4:
PutRegisterData32InByteBuilder(builder, register_data, data_order_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
break;
default:
diff --git a/pw_i2c/register_device_test.cc b/pw_i2c/register_device_test.cc
index 93f15242b..34d63398c 100644
--- a/pw_i2c/register_device_test.cc
+++ b/pw_i2c/register_device_test.cc
@@ -71,7 +71,7 @@ TEST(RegisterDevice, Construction) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k1Byte);
}
@@ -79,7 +79,7 @@ TEST(RegisterDevice, WriteRegisters8With2RegistersAnd1ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k1Byte);
std::array<std::byte, 2> register_data = {std::byte{0xCD}, std::byte{0xEF}};
@@ -108,7 +108,7 @@ TEST(RegisterDevice, WriteRegisters8With2RegistersAnd2ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k2Bytes);
constexpr uint32_t kRegisterAddress = 0x89AB;
@@ -138,7 +138,7 @@ TEST(RegisterDevice, WriteRegisters16With2RegistersAnd2ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k2Bytes);
constexpr uint32_t kRegisterAddress = 0x89AB;
@@ -171,10 +171,8 @@ TEST(RegisterDevice, WriteRegisters16With2RegistersAnd2ByteAddress) {
TEST(RegisterDevice, WriteRegisters16With2RegistersAnd2ByteAddressBigEndian) {
TestInitiator initiator;
- RegisterDevice device(initiator,
- kTestDeviceAddress,
- std::endian::big,
- RegisterAddressSize::k2Bytes);
+ RegisterDevice device(
+ initiator, kTestDeviceAddress, endian::big, RegisterAddressSize::k2Bytes);
constexpr uint32_t kRegisterAddress = 0x89AB;
std::array<uint16_t, 2> register_data = {0xCDEF, 0x1234};
@@ -189,7 +187,7 @@ TEST(RegisterDevice, WriteRegisters16With2RegistersAnd2ByteAddressBigEndian) {
// Check address.
const uint16_t kActualAddress = *(reinterpret_cast<uint16_t*>(
const_cast<std::byte*>(test_device_builder.data())));
- EXPECT_EQ(bytes::ReadInOrder<uint16_t>(std::endian::big, &kRegisterAddress),
+ EXPECT_EQ(bytes::ReadInOrder<uint16_t>(endian::big, &kRegisterAddress),
kActualAddress);
// Check data.
@@ -201,7 +199,7 @@ TEST(RegisterDevice, WriteRegisters16With2RegistersAnd2ByteAddressBigEndian) {
for (uint32_t i = 0; i < (test_device_builder.size() - kAddressSize) /
sizeof(register_data[0]);
i++) {
- EXPECT_EQ(bytes::ReadInOrder<uint16_t>(std::endian::big, &register_data[i]),
+ EXPECT_EQ(bytes::ReadInOrder<uint16_t>(endian::big, &register_data[i]),
read_pointer[i]);
}
}
@@ -210,7 +208,7 @@ TEST(RegisterDevice, WriteRegisters8BufferTooSmall) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k2Bytes);
constexpr uint32_t kRegisterAddress = 0x89AB;
@@ -225,7 +223,7 @@ TEST(RegisterDevice, WriteRegister16With1ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k1Byte);
constexpr uint32_t kRegisterAddress = 0xAB;
@@ -254,7 +252,7 @@ TEST(RegisterDevice, WriteRegister32With1ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k1Byte);
constexpr uint32_t kRegisterAddress = 0xAB;
@@ -283,7 +281,7 @@ TEST(RegisterDevice, WriteRegister16with2ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k2Bytes);
constexpr uint32_t kRegisterAddress = 0xAB23;
@@ -311,10 +309,8 @@ TEST(RegisterDevice, WriteRegister16with2ByteAddress) {
TEST(RegisterDevice, WriteRegister16With1ByteAddressAndBigEndian) {
TestInitiator initiator;
- RegisterDevice device(initiator,
- kTestDeviceAddress,
- std::endian::big,
- RegisterAddressSize::k1Byte);
+ RegisterDevice device(
+ initiator, kTestDeviceAddress, endian::big, RegisterAddressSize::k1Byte);
constexpr uint32_t kRegisterAddress = 0xAB;
constexpr uint16_t kRegisterData = 0xBCDE;
@@ -341,10 +337,8 @@ TEST(RegisterDevice, WriteRegister16With1ByteAddressAndBigEndian) {
TEST(RegisterDevice, WriteRegister32With1ByteAddressAndBigEndian) {
TestInitiator initiator;
- RegisterDevice device(initiator,
- kTestDeviceAddress,
- std::endian::big,
- RegisterAddressSize::k1Byte);
+ RegisterDevice device(
+ initiator, kTestDeviceAddress, endian::big, RegisterAddressSize::k1Byte);
constexpr uint32_t kRegisterAddress = 0xAB;
constexpr uint32_t kRegisterData = 0xBCCDDEEF;
@@ -371,10 +365,8 @@ TEST(RegisterDevice, WriteRegister32With1ByteAddressAndBigEndian) {
TEST(RegisterDevice, WriteRegister16With2ByteAddressAndBigEndian) {
TestInitiator initiator;
- RegisterDevice device(initiator,
- kTestDeviceAddress,
- std::endian::big,
- RegisterAddressSize::k2Bytes);
+ RegisterDevice device(
+ initiator, kTestDeviceAddress, endian::big, RegisterAddressSize::k2Bytes);
constexpr uint32_t kRegisterAddress = 0xAB11;
constexpr uint16_t kRegisterData = 0xBCDF;
@@ -389,7 +381,7 @@ TEST(RegisterDevice, WriteRegister16With2ByteAddressAndBigEndian) {
// Check address.
const uint16_t kActualAddress = *(reinterpret_cast<uint16_t*>(
const_cast<std::byte*>(test_device_builder.data())));
- EXPECT_EQ(bytes::ReadInOrder<uint16_t>(std::endian::big, &kRegisterAddress),
+ EXPECT_EQ(bytes::ReadInOrder<uint16_t>(endian::big, &kRegisterAddress),
kActualAddress);
// Check data.
@@ -405,7 +397,7 @@ TEST(RegisterDevice, ReadRegisters8ByteWith2RegistersAnd1ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k1Byte);
std::array<std::byte, 2> register_data = {std::byte{0xCD}, std::byte{0xEF}};
@@ -435,12 +427,12 @@ TEST(RegisterDevice, ReadRegisters8IntWith2RegistersAnd1ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k1Byte);
std::array<uint8_t, 2> register_data = {0xCD, 0xEF};
- initiator.SetReadData(std::as_writable_bytes(
- std::span(register_data.data(), register_data.size())));
+ initiator.SetReadData(
+ as_writable_bytes(span(register_data.data(), register_data.size())));
std::array<uint8_t, 2> buffer;
constexpr uint32_t kRegisterAddress = 0xAB;
@@ -466,7 +458,7 @@ TEST(RegisterDevice, ReadRegisters8ByteWith2RegistersAnd2ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k2Bytes);
std::array<std::byte, 2> register_data = {std::byte{0xCD}, std::byte{0xEF}};
@@ -496,12 +488,12 @@ TEST(RegisterDevice, ReadRegisters16With2RegistersAnd2ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k2Bytes);
std::array<uint16_t, 2> register_data = {0xCDEF, 0x1234};
- initiator.SetReadData(std::as_writable_bytes(
- std::span(register_data.data(), register_data.size())));
+ initiator.SetReadData(
+ as_writable_bytes(span(register_data.data(), register_data.size())));
std::array<uint16_t, 2> buffer;
constexpr uint32_t kRegisterAddress = 0xAB;
@@ -525,14 +517,12 @@ TEST(RegisterDevice, ReadRegisters16With2RegistersAnd2ByteAddress) {
TEST(RegisterDevice, ReadRegisters16With2RegistersAnd2ByteAddressBigEndian) {
TestInitiator initiator;
- RegisterDevice device(initiator,
- kTestDeviceAddress,
- std::endian::big,
- RegisterAddressSize::k2Bytes);
+ RegisterDevice device(
+ initiator, kTestDeviceAddress, endian::big, RegisterAddressSize::k2Bytes);
std::array<uint16_t, 2> register_data = {0xCDEF, 0x1234};
- initiator.SetReadData(std::as_writable_bytes(
- std::span(register_data.data(), register_data.size())));
+ initiator.SetReadData(
+ as_writable_bytes(span(register_data.data(), register_data.size())));
std::array<uint16_t, 2> buffer;
constexpr uint32_t kRegisterAddress = 0xAB;
@@ -546,12 +536,12 @@ TEST(RegisterDevice, ReadRegisters16With2RegistersAnd2ByteAddressBigEndian) {
const uint16_t kActualAddress = *(reinterpret_cast<uint16_t*>(
const_cast<std::byte*>(address_buffer.data())));
- EXPECT_EQ(bytes::ReadInOrder<uint16_t>(std::endian::big, &kRegisterAddress),
+ EXPECT_EQ(bytes::ReadInOrder<uint16_t>(endian::big, &kRegisterAddress),
kActualAddress);
// Check data.
for (uint32_t i = 0; i < buffer.size(); i++) {
- EXPECT_EQ(bytes::ReadInOrder<uint16_t>(std::endian::big, &register_data[i]),
+ EXPECT_EQ(bytes::ReadInOrder<uint16_t>(endian::big, &register_data[i]),
buffer[i]);
}
}
@@ -560,7 +550,7 @@ TEST(RegisterDevice, ReadRegister16With1ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k1Byte);
std::array<std::byte, 2> register_data = {std::byte{0xCD}, std::byte{0xEF}};
@@ -591,7 +581,7 @@ TEST(RegisterDevice, ReadRegister32With1ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k1Byte);
std::array<std::byte, 4> register_data = {
@@ -623,7 +613,7 @@ TEST(RegisterDevice, ReadRegister16With2ByteAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::little,
+ endian::little,
RegisterAddressSize::k2Bytes);
std::array<std::byte, 2> register_data = {std::byte{0x98}, std::byte{0x76}};
@@ -652,10 +642,8 @@ TEST(RegisterDevice, ReadRegister16With2ByteAddress) {
TEST(RegisterDevice, ReadRegister16With1ByteAddressAndBigEndian) {
TestInitiator initiator;
- RegisterDevice device(initiator,
- kTestDeviceAddress,
- std::endian::big,
- RegisterAddressSize::k1Byte);
+ RegisterDevice device(
+ initiator, kTestDeviceAddress, endian::big, RegisterAddressSize::k1Byte);
std::array<std::byte, 2> register_data = {std::byte{0x98}, std::byte{0x76}};
initiator.SetReadData(register_data);
@@ -685,10 +673,8 @@ TEST(RegisterDevice, ReadRegister16With1ByteAddressAndBigEndian) {
TEST(RegisterDevice, ReadRegister32With1ByteAddressAndBigEndian) {
TestInitiator initiator;
- RegisterDevice device(initiator,
- kTestDeviceAddress,
- std::endian::big,
- RegisterAddressSize::k1Byte);
+ RegisterDevice device(
+ initiator, kTestDeviceAddress, endian::big, RegisterAddressSize::k1Byte);
std::array<std::byte, 4> register_data = {
std::byte{0x98}, std::byte{0x76}, std::byte{0x54}, std::byte{0x32}};
@@ -719,10 +705,8 @@ TEST(RegisterDevice, ReadRegister32With1ByteAddressAndBigEndian) {
TEST(RegisterDevice, ReadRegister16With2ByteAddressAndBigEndian) {
TestInitiator initiator;
- RegisterDevice device(initiator,
- kTestDeviceAddress,
- std::endian::big,
- RegisterAddressSize::k2Bytes);
+ RegisterDevice device(
+ initiator, kTestDeviceAddress, endian::big, RegisterAddressSize::k2Bytes);
std::array<std::byte, 2> register_data = {std::byte{0x98}, std::byte{0x76}};
initiator.SetReadData(register_data);
@@ -739,7 +723,7 @@ TEST(RegisterDevice, ReadRegister16With2ByteAddressAndBigEndian) {
const uint16_t kActualAddress = *(reinterpret_cast<uint16_t*>(
const_cast<std::byte*>(address_buffer.data())));
- EXPECT_EQ(bytes::ReadInOrder<uint16_t>(std::endian::big, &kRegisterAddress),
+ EXPECT_EQ(bytes::ReadInOrder<uint16_t>(endian::big, &kRegisterAddress),
kActualAddress);
// Check data.
@@ -755,8 +739,8 @@ TEST(RegisterDevice, ReadRegister16With2ByteBigEndianAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::big,
- std::endian::little,
+ endian::big,
+ endian::little,
RegisterAddressSize::k2Bytes);
std::array<std::byte, 2> register_data = {std::byte{0x98}, std::byte{0x76}};
@@ -774,7 +758,7 @@ TEST(RegisterDevice, ReadRegister16With2ByteBigEndianAddress) {
const uint16_t kActualAddress = *(reinterpret_cast<uint16_t*>(
const_cast<std::byte*>(address_buffer.data())));
- EXPECT_EQ(bytes::ReadInOrder<uint16_t>(std::endian::big, &kRegisterAddress),
+ EXPECT_EQ(bytes::ReadInOrder<uint16_t>(endian::big, &kRegisterAddress),
kActualAddress);
// Check data.
@@ -788,8 +772,8 @@ TEST(RegisterDevice, WriteRegister16with2ByteBigEndianAddress) {
TestInitiator initiator;
RegisterDevice device(initiator,
kTestDeviceAddress,
- std::endian::big,
- std::endian::little,
+ endian::big,
+ endian::little,
RegisterAddressSize::k2Bytes);
constexpr uint32_t kRegisterAddress = 0xAB11;
@@ -805,7 +789,7 @@ TEST(RegisterDevice, WriteRegister16with2ByteBigEndianAddress) {
// Check address.
const uint16_t kActualAddress = *(reinterpret_cast<uint16_t*>(
const_cast<std::byte*>(test_device_builder.data())));
- EXPECT_EQ(bytes::ReadInOrder<uint16_t>(std::endian::big, &kRegisterAddress),
+ EXPECT_EQ(bytes::ReadInOrder<uint16_t>(endian::big, &kRegisterAddress),
kActualAddress);
// Check data.
diff --git a/pw_i2c_mcuxpresso/BUILD.bazel b/pw_i2c_mcuxpresso/BUILD.bazel
index baaa7f147..9f82586f4 100644
--- a/pw_i2c_mcuxpresso/BUILD.bazel
+++ b/pw_i2c_mcuxpresso/BUILD.bazel
@@ -24,6 +24,9 @@ pw_cc_library(
name = "pw_i2c_mcuxpresso",
srcs = ["initiator.cc"],
hdrs = ["public/pw_i2c_mcuxpresso/initiator.h"],
+ includes = ["public"],
+ # TODO(b/259153338): Get this to build.
+ tags = ["manual"],
deps = [
"//pw_chrono:system_clock",
"//pw_i2c:initiator",
diff --git a/pw_i2c_mcuxpresso/BUILD.gn b/pw_i2c_mcuxpresso/BUILD.gn
index b3b62b401..3ff44c6bb 100644
--- a/pw_i2c_mcuxpresso/BUILD.gn
+++ b/pw_i2c_mcuxpresso/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+import("$dir_pw_unit_test/test.gni")
config("default_config") {
include_dirs = [ "public" ]
@@ -42,3 +43,6 @@ if (pw_third_party_mcuxpresso_SDK != "") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_i2c_mcuxpresso/OWNERS b/pw_i2c_mcuxpresso/OWNERS
index 504a930e5..a9a1aa4b3 100644
--- a/pw_i2c_mcuxpresso/OWNERS
+++ b/pw_i2c_mcuxpresso/OWNERS
@@ -1,2 +1,2 @@
ewout@google.com
-swatiwagh@google.com \ No newline at end of file
+swatiwagh@google.com
diff --git a/pw_ide/BUILD.gn b/pw_ide/BUILD.gn
new file mode 100644
index 000000000..ab4a0d832
--- /dev/null
+++ b/pw_ide/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_ide/README.md b/pw_ide/README.md
new file mode 100644
index 000000000..42d145050
--- /dev/null
+++ b/pw_ide/README.md
@@ -0,0 +1,2 @@
+Tools for Pigweed editor and IDE support.
+[Learn more here.](https://pigweed.dev/pw_ide)
diff --git a/pw_ide/docs.rst b/pw_ide/docs.rst
new file mode 100644
index 000000000..d28d93e86
--- /dev/null
+++ b/pw_ide/docs.rst
@@ -0,0 +1,195 @@
+.. _module-pw_ide:
+
+------
+pw_ide
+------
+This module provides tools for supporting code editor and IDE features for
+Pigweed projects.
+
+Usage
+=====
+
+Setup
+-----
+Most of the time, ``pw ide setup`` is all you need to get started.
+
+Configuration
+-------------
+``pw_ide`` has a built-in default configuration. You can create a configuration
+file if you need to override those defaults.
+
+A project configuration can be defined in ``.pw_ide.yaml`` in the project root.
+This configuration will be checked into source control and apply to all
+developers of the project. Each user can also create a ``.pw_ide.user.yaml``
+file that overrides both the default and project settings, is not checked into
+source control, and applies only to that checkout of the project. All of these
+files have the same schema, in which these options can be configured:
+
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.working_dir
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.build_dir
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.compdb_paths
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.targets
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.target_inference
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.default_target
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.setup
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.clangd_additional_query_drivers
+.. autoproperty:: pw_ide.settings.PigweedIdeSettings.editors
+
+C++ Code Intelligence
+---------------------
+`clangd <https://clangd.llvm.org/>`_ is a language server that provides C/C++
+code intelligence features to any editor that supports the language server
+protocol (LSP). It uses a
+`compilation database <https://clang.llvm.org/docs/JSONCompilationDatabase.html>`_,
+a JSON file containing the compile commands for the project. Projects that have
+multiple targets and/or use multiple toolchains need separate compilation
+databases for each target/toolchain. ``pw_ide`` provides tools for managing
+those databases.
+
+Assuming you have a compilation database output from a build system, start with:
+
+.. code-block:: bash
+
+ pw ide cpp --process <path or glob to your compile_commands.json file(s)>
+
+The ``pw_ide`` working directory will now contain one or more compilation
+database files, each for a separate target among the targets defined in
+``.pw_ide.yaml``. If you're using GN, you can generate the initial compilation
+database and process it in a single command by adding the ``--gn`` flag.
+
+List the available targets with:
+
+.. code-block:: bash
+
+ pw ide cpp --list
+
+Then set the target that ``clangd`` should use with:
+
+.. code-block:: bash
+
+ pw ide cpp --set <selected target name>
+
+``clangd`` can now be configured to point to the ``compile_commands.json`` file
+in the ``pw_ide`` working directory and provide code intelligence for the
+selected target. If you select a new target, ``clangd`` *does not* need to be
+reconfigured to look at a new file (in other words, ``clangd`` can always be
+pointed at the same, stable ``compile_commands.json`` file). However,
+``clangd`` may need to be restarted when the target changes.
+
+``clangd`` must be run within the activated Pigweed environment in order for
+``clangd.sh`` instead of directly using the ``clangd`` binary.
+
+``clangd`` must be run with arguments that provide the Pigweed environment paths
+to the correct toolchains and sysroots. One way to do this is to launch your
+editor from the terminal in an activated environment (e.g. running ``vim`` from
+the terminal), in which case nothing special needs to be done as long as your
+toolchains are in the Pigweed environment or ``$PATH``. But if you launch your
+editor outside of the activated environment (e.g. launching Visual Studio Code
+from your GUI shell's launcher), you can generate the command that invokes
+``clangd`` with the right arguments with:
+
+.. code-block:: bash
+
+ pw ide cpp --clangd-command
+
+Python Code Intelligence
+------------------------
+Any Python language server should work well with Pigweed projects as long as
+it's configured to use the Pigweed virtual environment. You can output the path
+to the virtual environment on your system with:
+
+.. code-block:: bash
+
+ pw ide python --venv
+
+Docs Code Intelligence
+----------------------
+The `esbonio <https://github.com/swyddfa/esbonio>`_ language server will provide
+code intelligence for RestructuredText and Sphinx. It works well with Pigweed
+projects as long as it is pointed to Pigweed's Python virtual environment. For
+Visual Studio Code, simply install the esbonio extension, which will be
+recommended to you after setting up ``pw_ide``. Once it's installed, a prompt
+will ask if you want to automatically install esbonio in your Pigweed Python
+environment. After that, give esbonio some time to index, then you're done!
+
+Command-Line Interface Reference
+--------------------------------
+.. argparse::
+ :module: pw_ide.cli
+ :func: _build_argument_parser
+ :prog: pw ide
+
+Design
+======
+
+Supporting ``clangd`` for Embedded Projects
+-------------------------------------------
+There are three main challenges that often prevent ``clangd`` from working
+out-of-the-box with embedded projects:
+
+#. Embedded projects cross-compile using alternative toolchains, rather than
+ using the system toolchain. ``clangd`` doesn't know about those toolchains
+ by default.
+
+#. Embedded projects (particularly Pigweed project) often have *multiple*
+ targets that use *multiple* toolchains. Most build systems that generate
+ compilation databases put all compile commands in a single database, meaning
+ a single file can have multiple, conflicting compile commands. ``clangd``
+ will typically use the first one it finds, which may not be the one you want.
+
+#. Pigweed projects have build steps that use languages other than C/C++. These
+ steps are not relevant to ``clangd`` but many build systems will include them
+ in the compilation database anyway.
+
+To deal with these challenges, ``pw_ide`` processes the compilation database you
+provide, yielding one or more compilation databases that are valid, consistent,
+and specific to a particular build target. This enables code intelligence and
+navigation features that reflect that build.
+
+After processing a compilation database, ``pw_ide`` knows what targets are
+available and provides tools for selecting which target is active. These tools
+can be integrated into code editors, but are ultimately CLI-driven and
+editor-agnostic. Enabling code intelligence in your editor may be as simple as
+configuring its language server protocol client to use the ``clangd`` command
+that ``pw_ide`` can generate for you.
+
+When to provide additional configuration to support your use cases
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+The default configuration for ``clangd`` in ``pw_ide`` should work without
+additional configuration as long as you're using only toolchains provided by
+Pigweed and your native host toolchain. If you're using other toolchains, keep
+reading.
+
+``clangd`` needs two pieces of information to use a toolchain:
+
+#. A path to the compiler, which will be taken from the compile command.
+
+#. The same compiler to be reflected in the
+ `query driver <https://releases.llvm.org/10.0.0/tools/clang/tools/extra/docs/clangd/Configuration.html>`_
+ argument provided when running ``clangd``.
+
+When using ``pw_ide`` with external toolchains, you only need to add a path to
+the compiler to ``clangd_additional_query_drivers`` in your project's
+``pw_ide.yaml`` file. When processing a compilation database, ``pw_ide`` will
+use the query driver globs to find your compiler and configure ``clangd`` to
+use it.
+
+Selected API Reference
+^^^^^^^^^^^^^^^^^^^^^^
+.. automodule:: pw_ide.cpp
+ :members: CppCompileCommand, CppCompilationDatabase, CppCompilationDatabasesMap, CppIdeFeaturesState, path_to_executable, target_is_enabled, ClangdSettings
+
+Automated Support for Code Editors & IDEs
+-----------------------------------------
+``pw_ide`` provides a consistent framework for automatically applying settings
+for code editors, where default settings can be defined within ``pw_ide``,
+which can be overridden by project settings, which in turn can be overridden
+by individual user settings.
+
+Selected API Reference
+^^^^^^^^^^^^^^^^^^^^^^
+.. automodule:: pw_ide.editors
+ :members: EditorSettingsDefinition, EditorSettingsFile, EditorSettingsManager
+
+.. automodule:: pw_ide.vscode
+ :members: VscSettingsType, VscSettingsManager
diff --git a/pw_ide/py/BUILD.gn b/pw_ide/py/BUILD.gn
new file mode 100644
index 000000000..7438cc234
--- /dev/null
+++ b/pw_ide/py/BUILD.gn
@@ -0,0 +1,49 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ "setup.py",
+ ]
+ sources = [
+ "pw_ide/__init__.py",
+ "pw_ide/__main__.py",
+ "pw_ide/activate.py",
+ "pw_ide/cli.py",
+ "pw_ide/commands.py",
+ "pw_ide/cpp.py",
+ "pw_ide/editors.py",
+ "pw_ide/exceptions.py",
+ "pw_ide/python.py",
+ "pw_ide/settings.py",
+ "pw_ide/symlinks.py",
+ "pw_ide/vscode.py",
+ ]
+ tests = [
+ "commands_test.py",
+ "cpp_test.py",
+ "editors_test.py",
+ "test_cases.py",
+ "vscode_test.py",
+ ]
+ python_deps = [ "$dir_pw_console/py" ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+}
diff --git a/pw_ide/py/commands_test.py b/pw_ide/py/commands_test.py
new file mode 100644
index 000000000..3bc90ac3e
--- /dev/null
+++ b/pw_ide/py/commands_test.py
@@ -0,0 +1,53 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for pw_ide.commands"""
+
+import logging
+import os
+import unittest
+
+from pw_ide.commands import _make_working_dir, LoggingStatusReporter
+from pw_ide.settings import PW_IDE_DIR_NAME
+
+from test_cases import PwIdeTestCase
+
+_LOG = logging.getLogger(__package__)
+
+
+class TestMakeWorkingDir(PwIdeTestCase):
+ """Tests _make_working_dir"""
+
+ def test_does_not_exist_creates_dir(self):
+ settings = self.make_ide_settings(working_dir=PW_IDE_DIR_NAME)
+ self.assertFalse(settings.working_dir.exists())
+ _make_working_dir(
+ reporter=LoggingStatusReporter(_LOG), settings=settings
+ )
+ self.assertTrue(settings.working_dir.exists())
+
+ def test_does_exist_is_idempotent(self):
+ settings = self.make_ide_settings(working_dir=PW_IDE_DIR_NAME)
+ _make_working_dir(
+ reporter=LoggingStatusReporter(_LOG), settings=settings
+ )
+ modified_when_1 = os.path.getmtime(settings.working_dir)
+ _make_working_dir(
+ reporter=LoggingStatusReporter(_LOG), settings=settings
+ )
+ modified_when_2 = os.path.getmtime(settings.working_dir)
+ self.assertEqual(modified_when_1, modified_when_2)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_ide/py/cpp_test.py b/pw_ide/py/cpp_test.py
new file mode 100644
index 000000000..87d57f3e0
--- /dev/null
+++ b/pw_ide/py/cpp_test.py
@@ -0,0 +1,983 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for pw_ide.cpp"""
+
+import json
+from pathlib import Path
+from typing import cast, Dict, List, Optional, Tuple, TypedDict, Union
+import unittest
+from unittest.mock import ANY, Mock, patch
+
+# pylint: disable=protected-access
+from pw_ide.cpp import (
+ _COMPDB_FILE_PREFIX,
+ _COMPDB_FILE_SEPARATOR,
+ _COMPDB_FILE_EXTENSION,
+ _COMPDB_CACHE_DIR_PREFIX,
+ _COMPDB_CACHE_DIR_SEPARATOR,
+ path_to_executable,
+ aggregate_compilation_database_targets,
+ compdb_generate_cache_path,
+ compdb_generate_file_path,
+ compdb_target_from_path,
+ CppCompilationDatabase,
+ CppCompilationDatabasesMap,
+ CppCompileCommand,
+ CppCompileCommandDict,
+ CppIdeFeaturesState,
+ infer_target,
+ _infer_target_pos,
+ InvalidTargetException,
+)
+
+from pw_ide.exceptions import UnresolvablePathException
+from pw_ide.symlinks import set_symlink
+
+from test_cases import PwIdeTestCase
+
+
+class TestCompDbGenerateFilePath(unittest.TestCase):
+ """Tests compdb_generate_file_path"""
+
+ def test_with_target_includes_target(self) -> None:
+ name = 'foo'
+ actual = str(compdb_generate_file_path('foo'))
+ expected = (
+ f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
+ f'{name}{_COMPDB_FILE_EXTENSION}'
+ )
+ self.assertEqual(actual, expected)
+
+ def test_without_target_omits_target(self) -> None:
+ actual = str(compdb_generate_file_path())
+ expected = f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_EXTENSION}'
+ self.assertEqual(actual, expected)
+
+
+class TestCompDbGenerateCachePath(unittest.TestCase):
+ """Tests compdb_generate_cache_path"""
+
+ def test_with_target_includes_target(self) -> None:
+ name = 'foo'
+ actual = str(compdb_generate_cache_path('foo'))
+ expected = (
+ f'{_COMPDB_CACHE_DIR_PREFIX}' f'{_COMPDB_CACHE_DIR_SEPARATOR}{name}'
+ )
+ self.assertEqual(actual, expected)
+
+ def test_without_target_omits_target(self) -> None:
+ actual = str(compdb_generate_cache_path())
+ expected = f'{_COMPDB_CACHE_DIR_PREFIX}'
+ self.assertEqual(actual, expected)
+
+
+class _CompDbTargetFromPathTestCase(TypedDict):
+ path: str
+ target: Optional[str]
+
+
+class TestCompDbTargetFromPath(unittest.TestCase):
+ """Tests compdb_target_from_path"""
+
+ def run_test(self, path: Path, expected_target: Optional[str]) -> None:
+ target = compdb_target_from_path(path)
+ self.assertEqual(target, expected_target)
+
+ def test_correct_target_from_path(self) -> None:
+ """Test that the expected target is extracted from the file path."""
+ cases: List[_CompDbTargetFromPathTestCase] = [
+ {
+ 'path': (
+ f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
+ f'pw_strict_host_clang_debug{_COMPDB_FILE_EXTENSION}'
+ ),
+ 'target': 'pw_strict_host_clang_debug',
+ },
+ {
+ 'path': (
+ f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
+ f'stm32f429i_disc1_debug{_COMPDB_FILE_EXTENSION}'
+ ),
+ 'target': 'stm32f429i_disc1_debug',
+ },
+ {
+ 'path': (
+ f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
+ f'{_COMPDB_FILE_EXTENSION}'
+ ),
+ 'target': None,
+ },
+ {'path': 'foompile_barmmands.json', 'target': None},
+ {'path': 'foompile_barmmands_target_x.json', 'target': None},
+ {'path': '', 'target': None},
+ ]
+
+ for case in cases:
+ self.run_test(Path(case['path']), case['target'])
+
+
+class TestPathToExecutable(PwIdeTestCase):
+ """Tests path_to_executable"""
+
+ def test_valid_absolute_path_in_env_returns_same_path(self):
+ executable_path = self.temp_dir_path / 'clang'
+ query_drivers = [f'{self.temp_dir_path}/*']
+ result = path_to_executable(
+ str(executable_path.absolute()), path_globs=query_drivers
+ )
+ self.assertIsNotNone(result)
+ self.assertEqual(str(result), str(executable_path.absolute()))
+
+ def test_valid_absolute_path_outside_env_returns_same_path(self):
+ executable_path = Path('/usr/bin/clang')
+ query_drivers = ['/**/*']
+ result = path_to_executable(
+ str(executable_path.absolute()), path_globs=query_drivers
+ )
+ self.assertIsNotNone(result)
+ self.assertEqual(str(result), str(executable_path.absolute()))
+
+ def test_valid_relative_path_returns_same_path(self):
+ executable_path = Path('../clang')
+ query_drivers = [f'{self.temp_dir_path}/*']
+ result = path_to_executable(
+ str(executable_path), path_globs=query_drivers
+ )
+ self.assertIsNotNone(result)
+ self.assertEqual(str(result), str(executable_path))
+
+ def test_valid_no_path_returns_new_path(self):
+ executable_path = Path('clang')
+ query_drivers = [f'{self.temp_dir_path}/*']
+ expected_path = self.temp_dir_path / executable_path
+ self.touch_temp_file(expected_path)
+ result = path_to_executable(
+ str(executable_path), path_globs=query_drivers
+ )
+ self.assertIsNotNone(result)
+ self.assertEqual(str(result), str(expected_path))
+
+ def test_invalid_absolute_path_in_env_returns_same_path(self):
+ executable_path = self.temp_dir_path / 'exe'
+ query_drivers = [f'{self.temp_dir_path}/*']
+ result = path_to_executable(
+ str(executable_path.absolute()), path_globs=query_drivers
+ )
+ self.assertIsNone(result)
+
+ def test_invalid_absolute_path_outside_env_returns_same_path(self):
+ executable_path = Path('/usr/bin/exe')
+ query_drivers = ['/**/*']
+ result = path_to_executable(
+ str(executable_path.absolute()), path_globs=query_drivers
+ )
+ self.assertIsNone(result)
+
+ def test_invalid_relative_path_returns_same_path(self):
+ executable_path = Path('../exe')
+ query_drivers = [f'{self.temp_dir_path}/*']
+ result = path_to_executable(
+ str(executable_path), path_globs=query_drivers
+ )
+ self.assertIsNone(result)
+
+ def test_invalid_no_path_returns_new_path(self):
+ executable_path = Path('exe')
+ query_drivers = [f'{self.temp_dir_path}/*']
+ expected_path = self.temp_dir_path / executable_path
+ self.touch_temp_file(expected_path)
+ result = path_to_executable(
+ str(executable_path), path_globs=query_drivers
+ )
+ self.assertIsNone(result)
+
+
+class TestInferTarget(unittest.TestCase):
+ """Tests infer_target"""
+
+ def test_infer_target_pos(self):
+ test_cases = [
+ ('?', [0]),
+ ('*/?', [1]),
+ ('*/*/?', [2]),
+ ('*/?/?', [1, 2]),
+ ]
+
+ for glob, result in test_cases:
+ self.assertEqual(_infer_target_pos(glob), result)
+
+ def test_infer_target(self):
+ test_cases = [
+ ('?', 'target/thing.o', 'target'),
+ ('*/?', 'variants/target/foo/bar/thing.o', 'target'),
+ ('*/*/*/*/?', 'foo/bar/baz/hi/target/obj/thing.o', 'target'),
+ ('*/?/?', 'variants/target/foo/bar/thing.o', 'target_foo'),
+ ]
+
+ for glob, output_path, result in test_cases:
+ self.assertEqual(
+ infer_target(glob, Path(''), Path(output_path)), result
+ )
+
+
+class TestCppCompileCommand(PwIdeTestCase):
+ """Tests CppCompileCommand"""
+
+ def run_process_test_with_valid_commands(
+ self, command: str, expected_executable: Optional[str]
+ ) -> None:
+ compile_command = CppCompileCommand(
+ command=command, file='', directory=''
+ )
+ processed_compile_command = cast(
+ CppCompileCommand,
+ compile_command.process(default_path=self.temp_dir_path),
+ )
+ self.assertIsNotNone(processed_compile_command)
+ self.assertEqual(
+ processed_compile_command.executable_name, expected_executable
+ )
+
+ def run_process_test_with_invalid_commands(self, command: str) -> None:
+ compile_command = CppCompileCommand(
+ command=command, file='', directory=''
+ )
+
+ with self.assertRaises(UnresolvablePathException):
+ compile_command.process(default_path=self.temp_dir_path)
+
+ def test_process_valid_with_gn_compile_command(self) -> None:
+ """Test output against typical GN-generated compile commands."""
+
+ cases: List[Dict[str, str]] = [
+ {
+ # pylint: disable=line-too-long
+ 'command': 'arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ 'executable': 'arm-none-eabi-g++',
+ },
+ {
+ # pylint: disable=line-too-long
+ 'command': '../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ 'executable': 'clang++',
+ },
+ ]
+
+ for case in cases:
+ self.run_process_test_with_valid_commands(
+ case['command'], case['executable']
+ )
+
+ def test_process_invalid_with_gn_compile_command(self) -> None:
+ """Test output against typical GN-generated compile commands."""
+
+ # pylint: disable=line-too-long
+ command = "python ../pw_toolchain/py/pw_toolchain/clang_tidy.py --source-exclude 'third_party/.*' --source-exclude '.*packages/mbedtls.*' --source-exclude '.*packages/boringssl.*' --skip-include-path 'mbedtls/include' --skip-include-path 'mbedtls' --skip-include-path 'boringssl/src/include' --skip-include-path 'boringssl' --skip-include-path 'pw_tls_client/generate_test_data' --source-file ../pw_allocator/freelist.cc --source-root '../' --export-fixes pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/freelist.freelist.cc.o.yaml -- ../environment/cipd/packages/pigweed/bin/clang++ END_OF_INVOKER -MMD -MF pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/freelist.freelist.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_containers/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/freelist.cc -o pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/freelist.freelist.cc.o && touch pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/freelist.freelist.cc.o"
+ # pylint: enable=line-too-long
+
+ compile_command = CppCompileCommand(
+ command=command, file='', directory=''
+ )
+ self.assertIsNone(
+ compile_command.process(default_path=self.temp_dir_path)
+ )
+
+ def test_process_unresolvable_with_gn_compile_command(self) -> None:
+ """Test output against typical GN-generated compile commands."""
+
+ # pylint: disable=line-too-long
+ command = 'clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o'
+ # pylint: enable=line-too-long
+
+ compile_command = CppCompileCommand(
+ command=command, file='', directory=''
+ )
+
+ processed_compile_command = cast(
+ CppCompileCommand, compile_command.process()
+ )
+ self.assertIsNotNone(processed_compile_command)
+ self.assertIsNotNone(processed_compile_command.command)
+ # .split() to avoid failing on whitespace differences.
+ self.assertCountEqual(
+ cast(str, processed_compile_command.command).split(),
+ command.split(),
+ )
+
+ def test_process_unresolvable_strict_with_gn_compile_command(self) -> None:
+ """Test output against typical GN-generated compile commands."""
+
+ # pylint: disable=line-too-long
+ command = 'clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o'
+ # pylint: enable=line-too-long
+
+ compile_command = CppCompileCommand(
+ command=command, file='', directory=''
+ )
+
+ with self.assertRaises(UnresolvablePathException):
+ compile_command.process(strict=True)
+
+ def test_process_blank_with_gn_compile_command(self) -> None:
+ """Test output against typical GN-generated compile commands."""
+
+ command = ''
+ compile_command = CppCompileCommand(
+ command=command, file='', directory=''
+ )
+
+ result = compile_command.process(default_path=self.temp_dir_path)
+ self.assertIsNone(result)
+
+
+class TestCppCompilationDatabase(PwIdeTestCase):
+ """Tests CppCompilationDatabase"""
+
+ def setUp(self):
+ self.build_dir = Path('/pigweed/pigweed/out')
+
+ self.fixture: List[CppCompileCommandDict] = [
+ {
+ # pylint: disable=line-too-long
+ 'command': 'arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/block.cc',
+ },
+ {
+ # pylint: disable=line-too-long
+ 'command': '../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/block.cc',
+ },
+ ]
+
+ self.expected: List[CppCompileCommandDict] = [
+ {
+ **self.fixture[0],
+ # pylint: disable=line-too-long
+ 'output': 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ },
+ {
+ **self.fixture[1],
+ # pylint: disable=line-too-long
+ 'output': 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ },
+ ]
+
+ self.fixture_merge_1 = [
+ {
+ # pylint: disable=line-too-long
+ 'command': 'g++ -MMD -MF pw_strict_host_gcc_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -Og -Wshadow -Wredundant-decls -Wswitch-enum -Wpedantic -Wno-c++20-designator -Wno-gnu-zero-variadic-macro-arguments -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_SPAN_ENABLE_ASSERTS=true -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_gcc_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/block.cc',
+ 'output': 'pw_strict_host_gcc_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ },
+ {
+ # pylint: disable=line-too-long
+ 'command': 'g++ -MMD -MF pw_strict_host_gcc_debug/obj/pw_allocator/freelist.freelist.cc.o.d -Wno-psabi -Og -Wshadow -Wredundant-decls -Wswitch-enum -Wpedantic -Wno-c++20-designator -Wno-gnu-zero-variadic-macro-arguments -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=//pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_SPAN_ENABLE_ASSERTS=true -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_containers/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_span/public -I../pw_status/public -c ../pw_allocator/freelist.cc -o pw_strict_host_gcc_debug/obj/pw_allocator/freelist.freelist.cc.o',
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/freelist.cc',
+ 'output': 'pw_strict_host_gcc_debug/obj/pw_allocator/freelist.freelist.cc.o',
+ # pylint: enable=line-too-long
+ },
+ ]
+
+ self.fixture_merge_2 = [
+ {
+ # pylint: disable=line-too-long
+ 'command': 'g++ -MMD -MF pw_strict_host_gcc_debug/obj/pw_base64/pw_base64.base64.cc.o.d -Wno-psabi -Og -Wshadow -Wredundant-decls -Wswitch-enum -Wpedantic -Wno-c++20-designator -Wno-gnu-zero-variadic-macro-arguments -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_SPAN_ENABLE_ASSERTS=true -I../pw_base64/public -I../pw_string/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_span/public -c ../pw_base64/base64.cc -o pw_strict_host_gcc_debug/obj/pw_base64/pw_base64.base64.cc.o',
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_base64/base64.cc',
+ 'output': 'pw_strict_host_gcc_debug/obj/pw_base64/pw_base64.base64.cc.o',
+ # pylint: enable=line-too-long
+ },
+ {
+ # pylint: disable=line-too-long
+ 'command': 'g++ -MMD -MF pw_strict_host_gcc_debug/obj/pw_checksum/pw_checksum.crc32.cc.o.d -Wno-psabi -Og -Wshadow -Wredundant-decls -Wswitch-enum -Wpedantic -Wno-c++20-designator -Wno-gnu-zero-variadic-macro-arguments -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_SPAN_ENABLE_ASSERTS=true -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_checksum/public -I../pw_bytes/public -I../pw_containers/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_preprocessor/public -I../pw_span/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_status/public -c ../pw_checksum/crc32.cc -o pw_strict_host_gcc_debug/obj/pw_checksum/pw_checksum.crc32.cc.o',
+ 'directory': 'pigweed/pigweed/out',
+ 'file': '../pw_checksum/crc32.cc',
+ 'output': 'pw_strict_host_gcc_debug/obj/pw_checksum/pw_checksum.crc32.cc.o',
+ # pylint: enable=line-too-long
+ },
+ ]
+
+ return super().setUp()
+
+ def test_merge(self):
+ compdb1 = CppCompilationDatabase.load(
+ self.fixture_merge_1, self.build_dir
+ )
+ compdb2 = CppCompilationDatabase.load(
+ self.fixture_merge_2, self.build_dir
+ )
+ compdb1.merge(compdb2)
+ result = [compile_command.as_dict() for compile_command in compdb1]
+ expected = [*self.fixture_merge_1, *self.fixture_merge_2]
+ self.assertCountEqual(result, expected)
+
+ def test_merge_no_dupes(self):
+ compdb1 = CppCompilationDatabase.load(
+ self.fixture_merge_1, self.build_dir
+ )
+ fixture_combo = [*self.fixture_merge_1, *self.fixture_merge_2]
+ compdb2 = CppCompilationDatabase.load(fixture_combo, self.build_dir)
+ compdb1.merge(compdb2)
+ result = [compile_command.as_dict() for compile_command in compdb1]
+ expected = [*self.fixture_merge_1, *self.fixture_merge_2]
+ self.assertCountEqual(result, expected)
+
+ def test_load_from_dicts(self):
+ compdb = CppCompilationDatabase.load(self.fixture, self.build_dir)
+ self.assertCountEqual(compdb.as_dicts(), self.expected)
+
+ def test_load_from_json(self):
+ compdb = CppCompilationDatabase.load(
+ json.dumps(self.fixture), self.build_dir
+ )
+ self.assertCountEqual(compdb.as_dicts(), self.expected)
+
+ def test_load_from_path(self):
+ with self.make_temp_file(
+ f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_EXTENSION}',
+ json.dumps(self.fixture),
+ ) as (_, file_path):
+ path = file_path
+
+ compdb = CppCompilationDatabase.load(path, self.build_dir)
+ self.assertCountEqual(compdb.as_dicts(), self.expected)
+
+ def test_load_from_file_handle(self):
+ with self.make_temp_file(
+ f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_EXTENSION}',
+ json.dumps(self.fixture),
+ ) as (file, _):
+ compdb = CppCompilationDatabase.load(file, self.build_dir)
+
+ self.assertCountEqual(compdb.as_dicts(), self.expected)
+
+ def test_process(self):
+ """Test processing against a typical sample of raw output from GN."""
+
+ targets = [
+ 'pw_strict_host_clang_debug',
+ 'stm32f429i_disc1_debug',
+ 'isosceles_debug',
+ ]
+
+ settings = self.make_ide_settings(targets=targets)
+
+ # pylint: disable=line-too-long
+ raw_db: List[CppCompileCommandDict] = [
+ {
+ 'command': 'arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': str(self.build_dir),
+ 'file': '../pw_allocator/block.cc',
+ },
+ {
+ 'command': '../environment/cipd/packages/pigweed/bin/isosceles-clang++ -MMD -MF isosceles_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': str(self.build_dir),
+ 'file': '../pw_allocator/block.cc',
+ },
+ {
+ 'command': '../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': str(self.build_dir),
+ 'file': '../pw_allocator/block.cc',
+ },
+ {
+ 'command': "python ../pw_toolchain/py/pw_toolchain/clang_tidy.py --source-exclude 'third_party/.*' --source-exclude '.*packages/mbedtls.*' --source-exclude '.*packages/boringssl.*' --skip-include-path 'mbedtls/include' --skip-include-path 'mbedtls' --skip-include-path 'boringssl/src/include' --skip-include-path 'boringssl' --skip-include-path 'pw_tls_client/generate_test_data' --source-file ../pw_allocator/block.cc --source-root '../' --export-fixes pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.yaml -- ../environment/cipd/packages/pigweed/bin/clang++ END_OF_INVOKER -MMD -MF pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o && touch pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o",
+ 'directory': str(self.build_dir),
+ 'file': '../pw_allocator/block.cc',
+ },
+ ]
+
+ expected_compdbs = {
+ 'isosceles_debug': [
+ {
+ 'command':
+ # Ensures path format matches OS (e.g. Windows)
+ f'{Path("../environment/cipd/packages/pigweed/bin/isosceles-clang++")} -MMD -MF isosceles_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': str(self.build_dir),
+ 'file': '../pw_allocator/block.cc',
+ 'output': 'isosceles_debug/obj/pw_allocator/block.block.cc.o',
+ },
+ ],
+ 'pw_strict_host_clang_debug': [
+ {
+ 'command':
+ # Ensures path format matches OS (e.g. Windows)
+ f'{Path("../environment/cipd/packages/pigweed/bin/clang++")} -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': str(self.build_dir),
+ 'file': '../pw_allocator/block.cc',
+ 'output': 'pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ },
+ ],
+ 'stm32f429i_disc1_debug': [
+ {
+ 'command':
+ # Ensures this test avoids the unpathed compiler search
+ f'{self.temp_dir_path / "arm-none-eabi-g++"} -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': str(self.build_dir),
+ 'file': '../pw_allocator/block.cc',
+ 'output': 'stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ },
+ ],
+ }
+ # pylint: enable=line-too-long
+
+ compdbs = CppCompilationDatabase.load(
+ raw_db, build_dir=self.build_dir
+ ).process(settings, default_path=self.temp_dir_path)
+ compdbs_as_dicts = {
+ target: compdb.as_dicts() for target, compdb in compdbs.items()
+ }
+
+ self.assertCountEqual(
+ list(compdbs_as_dicts.keys()), list(expected_compdbs.keys())
+ )
+ self.assertDictEqual(compdbs_as_dicts, expected_compdbs)
+
+
+class TestCppCompilationDatabasesMap(PwIdeTestCase):
+ """Tests CppCompilationDatabasesMap"""
+
+ def setUp(self):
+ # pylint: disable=line-too-long
+ self.fixture_1 = lambda target: [
+ CppCompileCommand(
+ **{
+ 'command': f'g++ -MMD -MF {target}/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -Og -Wshadow -Wredundant-decls -Wswitch-enum -Wpedantic -Wno-c++20-designator -Wno-gnu-zero-variadic-macro-arguments -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_SPAN_ENABLE_ASSERTS=true -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_gcc_debug/obj/pw_allocator/block.block.cc.o',
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/block.cc',
+ }
+ ),
+ CppCompileCommand(
+ **{
+ 'command': f'g++ -MMD -MF {target}/obj/pw_allocator/freelist.freelist.cc.o.d -Wno-psabi -Og -Wshadow -Wredundant-decls -Wswitch-enum -Wpedantic -Wno-c++20-designator -Wno-gnu-zero-variadic-macro-arguments -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=//pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_SPAN_ENABLE_ASSERTS=true -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_containers/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_span/public -I../pw_status/public -c ../pw_allocator/freelist.cc -o pw_strict_host_gcc_debug/obj/pw_allocator/freelist.freelist.cc.o',
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/freelist.cc',
+ }
+ ),
+ ]
+
+ self.fixture_2 = lambda target: [
+ CppCompileCommand(
+ **{
+ 'command': f'g++ -MMD -MF {target}/obj/pw_base64/pw_base64.base64.cc.o.d -Wno-psabi -Og -Wshadow -Wredundant-decls -Wswitch-enum -Wpedantic -Wno-c++20-designator -Wno-gnu-zero-variadic-macro-arguments -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_SPAN_ENABLE_ASSERTS=true -I../pw_base64/public -I../pw_string/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_span/public -c ../pw_base64/base64.cc -o pw_strict_host_gcc_debug/obj/pw_base64/pw_base64.base64.cc.o',
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_base64/base64.cc',
+ }
+ ),
+ CppCompileCommand(
+ **{
+ 'command': f'g++ -MMD -MF {target}/obj/pw_checksum/pw_checksum.crc32.cc.o.d -Wno-psabi -Og -Wshadow -Wredundant-decls -Wswitch-enum -Wpedantic -Wno-c++20-designator -Wno-gnu-zero-variadic-macro-arguments -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_SPAN_ENABLE_ASSERTS=true -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_checksum/public -I../pw_bytes/public -I../pw_containers/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_preprocessor/public -I../pw_span/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_status/public -c ../pw_checksum/crc32.cc -o pw_strict_host_gcc_debug/obj/pw_checksum/pw_checksum.crc32.cc.o',
+ 'directory': 'pigweed/pigweed/out',
+ 'file': '../pw_checksum/crc32.cc',
+ }
+ ),
+ ]
+ # pylint: enable=line-too-long
+ super().setUp()
+
+ def test_merge_0_db_set(self):
+ with self.assertRaises(ValueError):
+ CppCompilationDatabasesMap.merge()
+
+ def test_merge_1_db_set(self):
+ settings = self.make_ide_settings()
+ target = 'test_target'
+ db_set = CppCompilationDatabasesMap(settings)
+ db_set[target] = self.fixture_1(target)
+ result = CppCompilationDatabasesMap.merge(db_set)
+ self.assertCountEqual(result._dbs, db_set._dbs)
+
+ def test_merge_2_db_sets_different_targets(self):
+ settings = self.make_ide_settings()
+ target1 = 'test_target_1'
+ target2 = 'test_target_2'
+ db_set1 = CppCompilationDatabasesMap(settings)
+ db_set1[target1] = self.fixture_1(target1)
+ db_set2 = CppCompilationDatabasesMap(settings)
+ db_set2[target2] = self.fixture_2(target2)
+ result = CppCompilationDatabasesMap.merge(db_set1, db_set2)
+ self.assertEqual(len(result), 2)
+ self.assertCountEqual(result._dbs, {**db_set1._dbs, **db_set2._dbs})
+
+ def test_merge_2_db_sets_duplicated_targets(self):
+ settings = self.make_ide_settings()
+ target1 = 'test_target_1'
+ target2 = 'test_target_2'
+ db_set1 = CppCompilationDatabasesMap(settings)
+ db_set1[target1] = self.fixture_1(target1)
+ db_set2 = CppCompilationDatabasesMap(settings)
+ db_set2[target2] = self.fixture_2(target2)
+ db_set_combo = CppCompilationDatabasesMap.merge(db_set1, db_set2)
+ result = CppCompilationDatabasesMap.merge(db_set1, db_set_combo)
+ self.assertEqual(len(result), 2)
+ self.assertCountEqual(result._dbs, {**db_set1._dbs, **db_set2._dbs})
+
+
+class TestCppIdeFeaturesState(PwIdeTestCase):
+ """Tests CppIdeFeaturesState"""
+
+ def test_finds_all_compdbs(self) -> None:
+ """Test that it finds all compilation databases on initalization."""
+
+ targets = [
+ 'pw_strict_host_clang_debug',
+ 'stm32f429i_disc1_debug',
+ ]
+
+ # Simulate a dir with n compilation databases and m < n cache dirs.
+ files_data: List[Tuple[Union[Path, str], str]] = [
+ (compdb_generate_file_path(target), '') for target in targets
+ ]
+
+ files_data.append((compdb_generate_cache_path(targets[0]), ''))
+
+ # Symlinks
+ files_data.append((compdb_generate_file_path(), ''))
+ files_data.append((compdb_generate_cache_path(), ''))
+
+ expected = [
+ (
+ f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
+ f'{targets[0]}{_COMPDB_FILE_EXTENSION}',
+ f'{_COMPDB_CACHE_DIR_PREFIX}{_COMPDB_CACHE_DIR_SEPARATOR}'
+ f'{targets[0]}',
+ ),
+ (
+ f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_SEPARATOR}'
+ f'{targets[1]}{_COMPDB_FILE_EXTENSION}',
+ None,
+ ),
+ ]
+
+ settings = self.make_ide_settings(targets=targets)
+
+ with self.make_temp_files(files_data):
+ found_compdbs = CppIdeFeaturesState(settings)
+
+ # Strip out the temp dir path data.
+ get_name = lambda p: p.name if p is not None else None
+ found_compdbs_str = [
+ (
+ get_name(target.compdb_file_path),
+ get_name(target.compdb_cache_path),
+ )
+ for target in found_compdbs
+ ]
+
+ self.assertCountEqual(found_compdbs_str, expected)
+
+ def test_finds_all_targets(self) -> None:
+ """Test that it finds all targets on initalization."""
+ targets = [
+ 'pw_strict_host_clang_debug',
+ 'stm32f429i_disc1_debug',
+ ]
+
+ settings = self.make_ide_settings(targets=targets)
+
+ with self.make_temp_file(
+ compdb_generate_file_path(targets[0])
+ ), self.make_temp_file(compdb_generate_file_path(targets[1])):
+ found_targets = CppIdeFeaturesState(settings).targets
+
+ self.assertCountEqual(found_targets, targets)
+
+ def test_get_target_finds_valid_target(self) -> None:
+ target = 'pw_strict_host_clang_debug'
+ settings = self.make_ide_settings(targets=[target])
+
+ compdb_filename = (
+ f'{_COMPDB_FILE_PREFIX}'
+ f'{_COMPDB_FILE_SEPARATOR}'
+ f'{target}{_COMPDB_FILE_EXTENSION}'
+ )
+
+ symlink = self.temp_dir_path / compdb_generate_file_path()
+
+ with self.make_temp_file(Path(compdb_filename)) as (_, compdb):
+ set_symlink(compdb, symlink)
+
+ found_target = CppIdeFeaturesState(settings).current_target
+ self.assertEqual(found_target, target)
+
+ def test_get_target_returns_none_with_no_symlink(self) -> None:
+ target = 'pw_strict_host_clang_debug'
+ settings = self.make_ide_settings(targets=[target])
+ found_target = CppIdeFeaturesState(settings).current_target
+ self.assertIsNone(found_target)
+
+ @patch('os.readlink')
+ def test_get_target_returns_none_with_bad_symlink(
+ self, mock_readlink: Mock
+ ) -> None:
+ target = 'pw_strict_host_clang_debug'
+ settings = self.make_ide_settings(targets=[target])
+
+ mock_readlink.return_value = (
+ f'{_COMPDB_FILE_PREFIX}'
+ f'{_COMPDB_FILE_SEPARATOR}'
+ f'{target}{_COMPDB_FILE_EXTENSION}'
+ )
+
+ found_target = CppIdeFeaturesState(settings).current_target
+ self.assertIsNone(found_target)
+
+ @patch('os.remove')
+ @patch('os.mkdir')
+ @patch('os.symlink')
+ def test_set_target_sets_valid_target_when_no_target_set(
+ self, mock_symlink: Mock, mock_mkdir: Mock, mock_remove: Mock
+ ) -> None:
+ """Test the case where no symlinks have been set."""
+
+ target = 'pw_strict_host_clang_debug'
+ settings = self.make_ide_settings(targets=[target])
+ compdb_symlink_path = compdb_generate_file_path()
+ cache_symlink_path = compdb_generate_cache_path()
+
+ with self.make_temp_file(compdb_generate_file_path(target)):
+ CppIdeFeaturesState(settings).current_target = target
+
+ mock_mkdir.assert_any_call(
+ self.path_in_temp_dir(compdb_generate_cache_path(target))
+ )
+
+ target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
+ compdb_generate_file_path(target), compdb_symlink_path
+ )
+ mock_symlink.assert_any_call(*target_and_symlink_in_temp_dir, ANY)
+
+ target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
+ compdb_generate_cache_path(target), cache_symlink_path
+ )
+ mock_symlink.assert_any_call(*target_and_symlink_in_temp_dir, ANY)
+
+ mock_remove.assert_not_called()
+
+ @patch('os.remove')
+ @patch('os.mkdir')
+ @patch('os.symlink')
+ def test_set_target_sets_valid_target_when_target_already_set(
+ self, mock_symlink: Mock, mock_mkdir: Mock, mock_remove: Mock
+ ) -> None:
+ """Test the case where symlinks have been set, and now we're setting
+ them to a different target."""
+
+ targets = [
+ 'pw_strict_host_clang_debug',
+ 'stm32f429i_disc1_debug',
+ ]
+
+ settings = self.make_ide_settings(targets=targets)
+ compdb_symlink_path = compdb_generate_file_path()
+ cache_symlink_path = compdb_generate_cache_path()
+
+ # Set the first target, which should initalize the symlinks.
+ with self.make_temp_file(
+ compdb_generate_file_path(targets[0])
+ ), self.make_temp_file(compdb_generate_file_path(targets[1])):
+ CppIdeFeaturesState(settings).current_target = targets[0]
+
+ mock_mkdir.assert_any_call(
+ self.path_in_temp_dir(compdb_generate_cache_path(targets[0]))
+ )
+
+ mock_remove.assert_not_called()
+
+ # Simulate symlink creation
+ with self.make_temp_file(compdb_symlink_path), self.make_temp_file(
+ cache_symlink_path
+ ):
+ # Set the second target, which should replace the symlinks
+ CppIdeFeaturesState(settings).current_target = targets[1]
+
+ mock_mkdir.assert_any_call(
+ self.path_in_temp_dir(
+ compdb_generate_cache_path(targets[1])
+ )
+ )
+
+ mock_remove.assert_any_call(
+ self.path_in_temp_dir(compdb_symlink_path)
+ )
+
+ mock_remove.assert_any_call(
+ self.path_in_temp_dir(cache_symlink_path)
+ )
+
+ target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
+ compdb_generate_file_path(targets[1]), compdb_symlink_path
+ )
+ mock_symlink.assert_any_call(
+ *target_and_symlink_in_temp_dir, ANY
+ )
+
+ target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
+ compdb_generate_cache_path(targets[1]), cache_symlink_path
+ )
+ mock_symlink.assert_any_call(
+ *target_and_symlink_in_temp_dir, ANY
+ )
+
+ @patch('os.remove')
+ @patch('os.mkdir')
+ @patch('os.symlink')
+ def test_set_target_sets_valid_target_back_and_forth(
+ self, mock_symlink: Mock, mock_mkdir: Mock, mock_remove: Mock
+ ) -> None:
+ """Test the case where symlinks have been set, we set them to a second
+ target, and now we're setting them back to the first target."""
+
+ targets = [
+ 'pw_strict_host_clang_debug',
+ 'stm32f429i_disc1_debug',
+ ]
+
+ settings = self.make_ide_settings(targets=targets)
+ compdb_symlink_path = compdb_generate_file_path()
+ cache_symlink_path = compdb_generate_cache_path()
+
+ # Set the first target, which should initalize the symlinks
+ with self.make_temp_file(
+ compdb_generate_file_path(targets[0])
+ ), self.make_temp_file(compdb_generate_file_path(targets[1])):
+ CppIdeFeaturesState(settings).current_target = targets[0]
+
+ # Simulate symlink creation
+ with self.make_temp_file(compdb_symlink_path), self.make_temp_file(
+ cache_symlink_path
+ ):
+ # Set the second target, which should replace the symlinks
+ CppIdeFeaturesState(settings).current_target = targets[1]
+
+ # Reset mocks to clear events prior to those under test
+ mock_symlink.reset_mock()
+ mock_mkdir.reset_mock()
+ mock_remove.reset_mock()
+
+ # Set the first target again, which should also replace the
+ # symlinks and reuse the existing cache folder
+ CppIdeFeaturesState(settings).current_target = targets[0]
+
+ mock_mkdir.assert_any_call(
+ self.path_in_temp_dir(
+ compdb_generate_cache_path(targets[0])
+ )
+ )
+
+ mock_remove.assert_any_call(
+ self.path_in_temp_dir(compdb_symlink_path)
+ )
+
+ mock_remove.assert_any_call(
+ self.path_in_temp_dir(cache_symlink_path)
+ )
+
+ target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
+ compdb_generate_file_path(targets[0]), compdb_symlink_path
+ )
+ mock_symlink.assert_any_call(
+ *target_and_symlink_in_temp_dir, ANY
+ )
+
+ target_and_symlink_in_temp_dir = self.paths_in_temp_dir(
+ compdb_generate_cache_path(targets[0]), cache_symlink_path
+ )
+ mock_symlink.assert_any_call(
+ *target_and_symlink_in_temp_dir, ANY
+ )
+
+ @patch('os.symlink')
+ def test_set_target_invalid_target_not_in_enabled_targets_raises(
+ self, mock_symlink: Mock
+ ):
+ target = 'pw_strict_host_clang_debug'
+ settings = self.make_ide_settings(targets=[target])
+
+ with self.make_temp_file(
+ compdb_generate_file_path(target)
+ ), self.assertRaises(InvalidTargetException):
+ CppIdeFeaturesState(settings).current_target = 'foo'
+ mock_symlink.assert_not_called()
+
+ @patch('os.symlink')
+ def test_set_target_invalid_target_not_in_available_targets_raises(
+ self, mock_symlink: Mock
+ ):
+ target = 'pw_strict_host_clang_debug'
+ settings = self.make_ide_settings(targets=[target])
+
+ with self.assertRaises(InvalidTargetException):
+ CppIdeFeaturesState(settings).current_target = target
+ mock_symlink.assert_not_called()
+
+
+class TestAggregateCompilationDatabaseTargets(PwIdeTestCase):
+ """Tests aggregate_compilation_database_targets"""
+
+ def test_gets_all_legitimate_targets(self):
+ """Test compilation target aggregation against a typical sample of raw
+ output from GN."""
+
+ targets = [
+ 'pw_strict_host_clang_debug',
+ 'stm32f429i_disc1_debug',
+ ]
+
+ settings = self.make_ide_settings(targets=targets)
+
+ raw_db: List[CppCompileCommandDict] = [
+ {
+ # pylint: disable=line-too-long
+ 'command': 'arm-none-eabi-g++ -MMD -MF stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o.d -Wno-psabi -mabi=aapcs -mthumb --sysroot=../environment/cipd/packages/arm -specs=nano.specs -specs=nosys.specs -mcpu=cortex-m4 -mfloat-abi=hard -mfpu=fpv4-sp-d16 -Og -Wshadow -Wredundant-decls -u_printf_float -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -DPW_ARMV7M_ENABLE_FPU=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/assert_compatibility_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o stm32f429i_disc1_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/block.cc',
+ },
+ {
+ # pylint: disable=line-too-long
+ 'command': '../environment/cipd/packages/pigweed/bin/clang++ -MMD -MF pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug/obj/pw_allocator/block.block.cc.o',
+ # pylint: enable=line-too-long
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/block.cc',
+ },
+ {
+ # pylint: disable=line-too-long
+ 'command': "python ../pw_toolchain/py/pw_toolchain/clang_tidy.py --source-exclude 'third_party/.*' --source-exclude '.*packages/mbedtls.*' --source-exclude '.*packages/boringssl.*' --skip-include-path 'mbedtls/include' --skip-include-path 'mbedtls' --skip-include-path 'boringssl/src/include' --skip-include-path 'boringssl' --skip-include-path 'pw_tls_client/generate_test_data' --source-file ../pw_allocator/block.cc --source-root '../' --export-fixes pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.yaml -- ../environment/cipd/packages/pigweed/bin/clang++ END_OF_INVOKER -MMD -MF pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o.d -g3 --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk -Og -Wshadow -Wredundant-decls -Wthread-safety -Wswitch-enum -fdiagnostics-color -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Wimplicit-fallthrough -Wcast-qual -Wundef -Wpointer-arith -Werror -Wno-error=cpp -Wno-error=deprecated-declarations -ffile-prefix-map=/pigweed/pigweed/out=out -ffile-prefix-map=/pigweed/pigweed/= -ffile-prefix-map=../= -ffile-prefix-map=/pigweed/pigweed/out=out -Wextra-semi -fno-rtti -Wnon-virtual-dtor -std=c++17 -Wno-register -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS=1 -DPW_STATUS_CFG_CHECK_IF_USED=1 -I../pw_allocator/public -I../pw_assert/public -I../pw_assert/print_and_abort_assert_public_overrides -I../pw_preprocessor/public -I../pw_assert_basic/public_overrides -I../pw_assert_basic/public -I../pw_span/public -I../pw_polyfill/public -I../pw_polyfill/standard_library_public -I../pw_status/public -c ../pw_allocator/block.cc -o pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o && touch pw_strict_host_clang_debug.static_analysis/obj/pw_allocator/block.block.cc.o",
+ # pylint: enable=line-too-long
+ 'directory': '/pigweed/pigweed/out',
+ 'file': '../pw_allocator/block.cc',
+ },
+ ]
+
+ root = Path('/pigweed/pigweed/out')
+
+ aggregated_targets = aggregate_compilation_database_targets(
+ raw_db, settings, root, default_path=self.temp_dir_path
+ )
+
+ self.assertCountEqual(aggregated_targets, targets)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_ide/py/editors_test.py b/pw_ide/py/editors_test.py
new file mode 100644
index 000000000..cce3722ff
--- /dev/null
+++ b/pw_ide/py/editors_test.py
@@ -0,0 +1,342 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for pw_ide.editors"""
+
+from collections import OrderedDict
+from enum import Enum
+import unittest
+
+from pw_ide.editors import (
+ dict_deep_merge,
+ EditorSettingsFile,
+ EditorSettingsManager,
+ JsonFileFormat,
+)
+
+from test_cases import PwIdeTestCase
+
+
+class TestDictDeepMerge(unittest.TestCase):
+ """Tests dict_deep_merge"""
+
+ def test_invariants_with_dict_success(self):
+ # pylint: disable=unnecessary-lambda
+ dict_a = {'hello': 'world'}
+ dict_b = {'foo': 'bar'}
+
+ expected = {
+ 'hello': 'world',
+ 'foo': 'bar',
+ }
+
+ result = dict_deep_merge(dict_b, dict_a, lambda: dict())
+ self.assertEqual(result, expected)
+
+ def test_invariants_with_dict_implicit_ctor_success(self):
+ # pylint: disable=unnecessary-lambda
+ dict_a = {'hello': 'world'}
+ dict_b = {'foo': 'bar'}
+
+ expected = {
+ 'hello': 'world',
+ 'foo': 'bar',
+ }
+
+ result = dict_deep_merge(dict_b, dict_a)
+ self.assertEqual(result, expected)
+
+ def test_invariants_with_dict_fails_wrong_ctor_type(self):
+ # pylint: disable=unnecessary-lambda
+ dict_a = {'hello': 'world'}
+ dict_b = {'foo': 'bar'}
+
+ with self.assertRaises(TypeError):
+ dict_deep_merge(dict_b, dict_a, lambda: OrderedDict())
+
+ def test_invariants_with_ordered_dict_success(self):
+ # pylint: disable=unnecessary-lambda
+ dict_a = OrderedDict({'hello': 'world'})
+ dict_b = OrderedDict({'foo': 'bar'})
+
+ expected = OrderedDict(
+ {
+ 'hello': 'world',
+ 'foo': 'bar',
+ }
+ )
+
+ result = dict_deep_merge(dict_b, dict_a, lambda: OrderedDict())
+ self.assertEqual(result, expected)
+
+ def test_invariants_with_ordered_dict_implicit_ctor_success(self):
+ # pylint: disable=unnecessary-lambda
+ dict_a = OrderedDict({'hello': 'world'})
+ dict_b = OrderedDict({'foo': 'bar'})
+
+ expected = OrderedDict(
+ {
+ 'hello': 'world',
+ 'foo': 'bar',
+ }
+ )
+
+ result = dict_deep_merge(dict_b, dict_a)
+ self.assertEqual(result, expected)
+
+ def test_invariants_with_ordered_dict_fails_wrong_ctor_type(self):
+ # pylint: disable=unnecessary-lambda
+ dict_a = OrderedDict({'hello': 'world'})
+ dict_b = OrderedDict({'foo': 'bar'})
+
+ with self.assertRaises(TypeError):
+ dict_deep_merge(dict_b, dict_a, lambda: dict())
+
+
+class TestEditorSettingsFile(PwIdeTestCase):
+ """Tests EditorSettingsFile"""
+
+ def test_open_new_file_and_write(self):
+ name = 'settings'
+ json_fmt = JsonFileFormat()
+ settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'world'
+
+ with open(self.temp_dir_path / f'{name}.{json_fmt.ext}') as file:
+ settings_dict = json_fmt.load(file)
+
+ self.assertEqual(settings_dict['hello'], 'world')
+
+ def test_open_new_file_and_get(self):
+ name = 'settings'
+ json_fmt = JsonFileFormat()
+ settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'world'
+
+ settings_dict = settings_file.get()
+ self.assertEqual(settings_dict['hello'], 'world')
+
+ def test_open_new_file_no_backup(self):
+ name = 'settings'
+ json_fmt = JsonFileFormat()
+ settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'world'
+
+ backup_files = [
+ path
+ for path in self.temp_dir_path.iterdir()
+ if path.name != f'{name}.{json_fmt.ext}'
+ ]
+
+ self.assertEqual(len(backup_files), 0)
+
+ def test_open_existing_file_and_backup(self):
+ name = 'settings'
+ json_fmt = JsonFileFormat()
+ settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'world'
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'mundo'
+
+ settings_dict = settings_file.get()
+ self.assertEqual(settings_dict['hello'], 'mundo')
+
+ backup_files = [
+ path
+ for path in self.temp_dir_path.iterdir()
+ if path.name != f'{name}.{json_fmt.ext}'
+ ]
+
+ self.assertEqual(len(backup_files), 1)
+
+ with open(backup_files[0]) as file:
+ settings_dict = json_fmt.load(file)
+
+ self.assertEqual(settings_dict['hello'], 'world')
+
+ def test_open_existing_file_with_reinit_and_backup(self):
+ name = 'settings'
+ json_fmt = JsonFileFormat()
+ settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'world'
+
+ with settings_file.modify(reinit=True) as settings:
+ settings['hello'] = 'mundo'
+
+ settings_dict = settings_file.get()
+ self.assertEqual(settings_dict['hello'], 'mundo')
+
+ backup_files = [
+ path
+ for path in self.temp_dir_path.iterdir()
+ if path.name != f'{name}.{json_fmt.ext}'
+ ]
+
+ self.assertEqual(len(backup_files), 1)
+
+ with open(backup_files[0]) as file:
+ settings_dict = json_fmt.load(file)
+
+ self.assertEqual(settings_dict['hello'], 'world')
+
+ def open_existing_file_no_change_no_backup(self):
+ name = 'settings'
+ json_fmt = JsonFileFormat()
+ settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'world'
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'world'
+
+ settings_dict = settings_file.get()
+ self.assertEqual(settings_dict['hello'], 'world')
+
+ backup_files = [
+ path
+ for path in self.temp_dir_path.iterdir()
+ if path.name != f'{name}.{json_fmt.ext}'
+ ]
+
+ self.assertEqual(len(backup_files), 0)
+
+ with open(backup_files[0]) as file:
+ settings_dict = json_fmt.load(file)
+
+ self.assertEqual(settings_dict['hello'], 'world')
+
+ def test_write_bad_file_restore_backup(self):
+ name = 'settings'
+ json_fmt = JsonFileFormat()
+ settings_file = EditorSettingsFile(self.temp_dir_path, name, json_fmt)
+
+ with settings_file.modify() as settings:
+ settings['hello'] = 'world'
+
+ with self.assertRaises(TypeError):
+ with settings_file.modify() as settings:
+ settings['hello'] = object()
+
+ settings_dict = settings_file.get()
+ self.assertEqual(settings_dict['hello'], 'world')
+
+ backup_files = [
+ path
+ for path in self.temp_dir_path.iterdir()
+ if path.name != f'{name}.{json_fmt.ext}'
+ ]
+
+ self.assertEqual(len(backup_files), 0)
+
+
+class EditorSettingsTestType(Enum):
+ SETTINGS = 'settings'
+
+
+class TestEditorSettingsManager(PwIdeTestCase):
+ """Tests EditorSettingsManager"""
+
+ def test_settings_merge(self):
+ """Test that settings merge as expected in isolation."""
+ default_settings = OrderedDict(
+ {
+ 'foo': 'bar',
+ 'baz': 'qux',
+ 'lorem': OrderedDict(
+ {
+ 'ipsum': 'dolor',
+ }
+ ),
+ }
+ )
+
+ types_with_defaults = {
+ EditorSettingsTestType.SETTINGS: lambda _: default_settings
+ }
+
+ ide_settings = self.make_ide_settings()
+ json_fmt = JsonFileFormat()
+ manager = EditorSettingsManager(
+ ide_settings, self.temp_dir_path, json_fmt, types_with_defaults
+ )
+
+ project_settings = OrderedDict(
+ {
+ 'alpha': 'beta',
+ 'baz': 'xuq',
+ 'foo': 'rab',
+ }
+ )
+
+ with manager.project(
+ EditorSettingsTestType.SETTINGS
+ ).modify() as settings:
+ dict_deep_merge(project_settings, settings)
+
+ user_settings = OrderedDict(
+ {
+ 'baz': 'xqu',
+ 'lorem': OrderedDict(
+ {
+ 'ipsum': 'sit amet',
+ 'consectetur': 'adipiscing',
+ }
+ ),
+ }
+ )
+
+ with manager.user(EditorSettingsTestType.SETTINGS).modify() as settings:
+ dict_deep_merge(user_settings, settings)
+
+ expected = {
+ 'alpha': 'beta',
+ 'foo': 'rab',
+ 'baz': 'xqu',
+ 'lorem': {
+ 'ipsum': 'sit amet',
+ 'consectetur': 'adipiscing',
+ },
+ }
+
+ with manager.active(
+ EditorSettingsTestType.SETTINGS
+ ).modify() as active_settings:
+ manager.default(EditorSettingsTestType.SETTINGS).sync_to(
+ active_settings
+ )
+ manager.project(EditorSettingsTestType.SETTINGS).sync_to(
+ active_settings
+ )
+ manager.user(EditorSettingsTestType.SETTINGS).sync_to(
+ active_settings
+ )
+
+ self.assertCountEqual(
+ manager.active(EditorSettingsTestType.SETTINGS).get(), expected
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_ide/py/pw_ide/__init__.py b/pw_ide/py/pw_ide/__init__.py
new file mode 100644
index 000000000..edab7cf66
--- /dev/null
+++ b/pw_ide/py/pw_ide/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Configure IDE support for Pigweed projects."""
diff --git a/pw_ide/py/pw_ide/__main__.py b/pw_ide/py/pw_ide/__main__.py
new file mode 100644
index 000000000..b83bb6c37
--- /dev/null
+++ b/pw_ide/py/pw_ide/__main__.py
@@ -0,0 +1,27 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Configure IDE support for Pigweed projects."""
+
+import sys
+from typing import NoReturn
+
+from pw_ide.cli import parse_args_and_dispatch_command
+
+
+def main() -> NoReturn:
+ sys.exit(parse_args_and_dispatch_command())
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_ide/py/pw_ide/activate.py b/pw_ide/py/pw_ide/activate.py
new file mode 100644
index 000000000..082543bde
--- /dev/null
+++ b/pw_ide/py/pw_ide/activate.py
@@ -0,0 +1,489 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+# pylint: disable=line-too-long
+"""Pigweed shell activation script.
+
+Aside from importing it, this script can be used in three ways:
+
+1. Activate the Pigweed environment in your current shell (i.e., modify your
+ interactive shell's environment with Pigweed environment variables).
+
+ Using bash (assuming a global Python 3 is in $PATH):
+ source <(python3 ./pw_ide/activate.py -s bash)
+
+ Using bash (using the environment Python):
+ source <({environment}/pigweed-venv/bin/python ./pw_ide/activate.py -s bash)
+
+2. Run a shell command or executable in an activated shell (i.e. apply a
+ modified environment to a subprocess without affecting your current
+ interactive shell).
+
+ Example (assuming a global Python 3 is in $PATH):
+ python3 ./pw_ide/activate.py -x 'pw ide cpp --list'
+
+ Example (using the environment Python):
+ {environment}/pigweed-venv/bin/python ./pw_ide/activate.py -x 'pw ide cpp --list'
+
+ Example (using the environment Python on Windows):
+ {environment}/pigweed-venv/Scripts/pythonw.exe ./pw_ide/activate.py -x 'pw ide cpp --list'
+
+3. Produce a JSON representation of the Pigweed activated environment (-O) or
+ the diff against your current environment that produces an activated
+ environment (-o). See the help for more detailed information on the options
+ available.
+
+ Example (assuming a global Python 3 is in $PATH):
+ python3 ./pw_ide/activate.py -o
+
+ Example (using the environment Python):
+ {environment}/pigweed-venv/bin/python ./pw_ide/activate.py -o
+
+ Example (using the environment Python on Windows):
+ {environment}/pigweed-venv/Scripts/pythonw.exe ./pw_ide/activate.py -o
+"""
+# pylint: enable=line-too-long
+
+from abc import abstractmethod, ABC
+import argparse
+from collections import defaultdict
+from inspect import cleandoc
+import json
+import os
+from pathlib import Path
+import shlex
+import subprocess
+import sys
+from typing import cast, Dict, Optional
+
+# This expects this file to be in the Python module. If it ever moves
+# (e.g. to the root of the repository), this will need to change.
+_PW_PROJECT_PATH = Path(
+ os.environ.get(
+ 'PW_PROJECT_ROOT', os.environ.get('PW_ROOT', Path(__file__).parents[3])
+ )
+)
+
+
+def assumed_environment_root() -> Optional[Path]:
+ """Infer the path to the Pigweed environment directory.
+
+ First we look at the environment variable that should contain the path if
+ we're operating in an activated Pigweed environment. If we don't find the
+ path there, we check a few known locations. If we don't find an environment
+ directory in any of those locations, we return None.
+ """
+ actual_environment_root = os.environ.get('_PW_ACTUAL_ENVIRONMENT_ROOT')
+ if (
+ actual_environment_root is not None
+ and (root_path := Path(actual_environment_root)).exists()
+ ):
+ return root_path.absolute()
+
+ default_environment = _PW_PROJECT_PATH / 'environment'
+ if default_environment.exists():
+ return default_environment.absolute()
+
+ default_dot_environment = _PW_PROJECT_PATH / '.environment'
+ if default_dot_environment.exists():
+ return default_dot_environment.absolute()
+
+ return None
+
+
+# We're looking for the `actions.json` file that allows us to activate the
+# Pigweed environment. That file is located in the Pigweed environment
+# directory, so if we found an environment directory, this variable will
+# have the path to `actions.json`. If it doesn't find an environment directory
+# (e.g., this isn't being executed in the context of a Pigweed project), this
+# will be None. Note that this is the "default" config file path because
+# callers of functions that need this path can provide their own paths to an
+# `actions.json` file.
+_DEFAULT_CONFIG_FILE_PATH = (
+ None
+ if assumed_environment_root() is None
+ else cast(Path, assumed_environment_root()) / 'actions.json'
+)
+
+
+def _sanitize_path(
+ path: str, project_root_prefix: str, user_home_prefix: str
+) -> str:
+ """Given a path, return a sanitized path.
+
+ By default, environment variable paths are usually absolute. If we want
+ those paths to work across multiple systems, we need to sanitize them. This
+ takes a string that may be a path, and if it is indeed a path, it returns
+ the sanitized path, which is relative to either the repository root or the
+ user's home directory. If it's not a path, it just returns the input.
+
+ You can provide the strings that should be substituted for the project root
+ and the user's home directory. This may be useful for applications that have
+ their own way of representing those directories.
+
+ Note that this is intended to work on Pigweed environment variables, which
+ should all be relative to either of those two locations. Paths that aren't
+ (e.g. the path to a system binary) won't really be sanitized.
+ """
+ # Return the argument if it's not actually a path.
+ # This strategy relies on the fact that env_setup outputs absolute paths for
+ # all path env vars. So if we get a variable that's not an absolute path, it
+ # must not be a path at all.
+ if not Path(path).is_absolute():
+ return path
+
+ project_root = _PW_PROJECT_PATH.resolve()
+ user_home = Path.home().resolve()
+ resolved_path = Path(path).resolve()
+
+ # TODO(b/248257406) Remove once we drop support for Python 3.8.
+ def is_relative_to(path: Path, other: Path) -> bool:
+ try:
+ path.relative_to(other)
+ return True
+ except ValueError:
+ return False
+
+ if is_relative_to(resolved_path, project_root):
+ return f'{project_root_prefix}/' + str(
+ resolved_path.relative_to(project_root)
+ )
+
+ if is_relative_to(resolved_path, user_home):
+ return f'{user_home_prefix}/' + str(
+ resolved_path.relative_to(user_home)
+ )
+
+ # Path is not in the project root or user home, so just return it as is.
+ return path
+
+
+class ShellModifier(ABC):
+ """Abstract class for shell modifiers.
+
+ A shell modifier provides an interface for modifying the environment
+ variables in various shells. You can pass in a current environment state
+ as a dictionary during instantiation and modify it and/or modify shell state
+ through other side effects.
+ """
+
+ separator = ':'
+ comment = '# '
+
+ def __init__(
+ self,
+ env: Optional[Dict[str, str]] = None,
+ env_only: bool = False,
+ path_var: str = '$PATH',
+ project_root: str = '.',
+ user_home: str = '~',
+ ):
+ # This will contain only the modifications to the environment, with
+ # no elements of the existing environment aside from variables included
+ # here. In that sense, it's like a diff against the existing
+ # environment, or a structured form of the shell modification side
+ # effects.
+ default_env_mod = {'PATH': path_var}
+ self.env_mod = default_env_mod.copy()
+
+ # This is seeded with the existing environment, and then is modified.
+ # So it contains the complete new environment after modifications.
+ # If no existing environment is provided, this is identical to env_mod.
+ env = env if env is not None else default_env_mod.copy()
+ self.env: Dict[str, str] = defaultdict(str, env)
+
+ # Will contain the side effects, i.e. commands executed in the shell to
+ # modify its environment.
+ self.side_effects = ''
+
+ # Set this to not do any side effects, but just modify the environment
+ # stored in this class.
+ self.env_only = env_only
+
+ self.project_root = project_root
+ self.user_home = user_home
+
+ def do_effect(self, effect: str):
+ """Add to the commands that will affect the shell's environment.
+
+ This is a no-op if the shell modifier is set to only store shell
+ modification data rather than doing the side effects.
+ """
+ if not self.env_only:
+ self.side_effects += f'{effect}\n'
+
+ def modify_env(
+ self,
+ config_file_path: Optional[Path] = _DEFAULT_CONFIG_FILE_PATH,
+ sanitize: bool = False,
+ ) -> 'ShellModifier':
+ """Modify the current shell state per the actions.json file provided."""
+ json_file_options = {}
+
+ if config_file_path is None:
+ raise RuntimeError(
+ 'This must be run from a bootstrapped Pigweed directory!'
+ )
+
+ with config_file_path.open('r') as json_file:
+ json_file_options = json.loads(json_file.read())
+
+ root = self.project_root
+ home = self.user_home
+
+ # Set env vars
+ for var_name, value in json_file_options.get('set', dict()).items():
+ if value is not None:
+ value = _sanitize_path(value, root, home) if sanitize else value
+ self.set_variable(var_name, value)
+
+ # Prepend & append env vars
+ for var_name, mode_changes in json_file_options.get(
+ 'modify', dict()
+ ).items():
+ for mode_name, values in mode_changes.items():
+ if mode_name in ['prepend', 'append']:
+ modify_variable = self.prepend_variable
+
+ if mode_name == 'append':
+ modify_variable = self.append_variable
+
+ for value in values:
+ value = (
+ _sanitize_path(value, root, home)
+ if sanitize
+ else value
+ )
+ modify_variable(var_name, value)
+
+ return self
+
+ @abstractmethod
+ def set_variable(self, var_name: str, value: str) -> None:
+ pass
+
+ @abstractmethod
+ def prepend_variable(self, var_name: str, value: str) -> None:
+ pass
+
+ @abstractmethod
+ def append_variable(self, var_name: str, value: str) -> None:
+ pass
+
+
+class BashShellModifier(ShellModifier):
+ """Shell modifier for bash."""
+
+ def set_variable(self, var_name: str, value: str):
+ self.env[var_name] = value
+ self.env_mod[var_name] = value
+ quoted_value = shlex.quote(value)
+ self.do_effect(f'export {var_name}={quoted_value}')
+
+ def prepend_variable(self, var_name: str, value: str) -> None:
+ self.env[var_name] = f'{value}{self.separator}{self.env[var_name]}'
+ self.env_mod[
+ var_name
+ ] = f'{value}{self.separator}{self.env_mod[var_name]}'
+ quoted_value = shlex.quote(value)
+ self.do_effect(
+ f'export {var_name}={quoted_value}{self.separator}${var_name}'
+ )
+
+ def append_variable(self, var_name: str, value: str) -> None:
+ self.env[var_name] = f'{self.env[var_name]}{self.separator}{value}'
+ self.env_mod[
+ var_name
+ ] = f'{self.env_mod[var_name]}{self.separator}{value}'
+ quoted_value = shlex.quote(value)
+ self.do_effect(
+ f'export {var_name}=${var_name}{self.separator}{quoted_value}'
+ )
+
+
+def _build_argument_parser() -> argparse.ArgumentParser:
+ """Set up `argparse`."""
+ doc = __doc__
+
+ try:
+ env_root = assumed_environment_root()
+ except RuntimeError:
+ env_root = None
+
+ # Substitute in the actual environment path in the help text, if we can
+ # find it. If not, leave the placeholder text.
+ if env_root is not None:
+ doc = doc.replace(
+ '{environment}', str(env_root.relative_to(Path.cwd()))
+ )
+
+ parser = argparse.ArgumentParser(
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ description=doc,
+ )
+
+ default_config_file_path = None
+
+ if _DEFAULT_CONFIG_FILE_PATH is not None:
+ default_config_file_path = _DEFAULT_CONFIG_FILE_PATH.relative_to(
+ Path.cwd()
+ )
+
+ parser.add_argument(
+ '-c',
+ '--config-file',
+ default=_DEFAULT_CONFIG_FILE_PATH,
+ type=Path,
+ help='Path to actions.json config file, which defines '
+ 'the modifications to the shell environment '
+ 'needed to activate Pigweed. '
+ f'Default: {default_config_file_path}',
+ )
+
+ default_shell = Path(os.environ['SHELL']).name
+ parser.add_argument(
+ '-s',
+ '--shell-mode',
+ default=default_shell,
+ help='Which shell is being used. ' f'Default: {default_shell}',
+ )
+
+ parser.add_argument(
+ '-o',
+ '--out',
+ action='store_true',
+ help='Write only the modifications to the environment ' 'out to JSON.',
+ )
+
+ parser.add_argument(
+ '-O',
+ '--out-all',
+ action='store_true',
+ help='Write the complete modified environment to ' 'JSON.',
+ )
+
+ parser.add_argument(
+ '-n',
+ '--sanitize',
+ action='store_true',
+ help='Sanitize paths that are relative to the repo '
+ 'root or user home directory so that they are portable '
+ 'to other workstations.',
+ )
+
+ parser.add_argument(
+ '--path-var',
+ default='$PATH',
+ help='The string to substitute for the existing $PATH. Default: $PATH',
+ )
+
+ parser.add_argument(
+ '--project-root',
+ default='.',
+ help='The string to substitute for the project root when sanitizing '
+ 'paths. Default: .',
+ )
+
+ parser.add_argument(
+ '--user-home',
+ default='~',
+ help='The string to substitute for the user\'s home when sanitizing '
+ 'paths. Default: ~',
+ )
+
+ parser.add_argument(
+ '-x',
+ '--exec',
+ help='A command to execute in the activated shell.',
+ metavar='COMMAND',
+ )
+
+ return parser
+
+
+def main() -> int:
+ """The main CLI script."""
+ args, _unused_extra_args = _build_argument_parser().parse_known_args()
+ env = os.environ.copy()
+ config_file_path = args.config_file
+
+ if not config_file_path.exists():
+ sys.stderr.write(f'File not found! {config_file_path}')
+ sys.stderr.write(
+ 'This must be run from a bootstrapped Pigweed ' 'project directory.'
+ )
+ sys.exit(1)
+
+ # If we're executing a command in a subprocess, don't modify the current
+ # shell's state. Instead, apply the modified state to the subprocess.
+ env_only = args.exec is not None
+
+ # Assume bash by default.
+ shell_modifier = BashShellModifier
+
+ # TODO(chadnorvell): if args.shell_mode == 'zsh', 'ksh', 'fish'...
+ try:
+ modified_env = shell_modifier(
+ env=env,
+ env_only=env_only,
+ path_var=args.path_var,
+ project_root=args.project_root,
+ user_home=args.user_home,
+ ).modify_env(config_file_path, args.sanitize)
+ except (FileNotFoundError, json.JSONDecodeError):
+ sys.stderr.write(
+ 'Unable to read file: {}\n'
+ 'Please run this in bash or zsh:\n'
+ ' . ./bootstrap.sh\n'.format(str(config_file_path))
+ )
+
+ sys.exit(1)
+
+ if args.out_all:
+ print(json.dumps(modified_env.env, sort_keys=True, indent=2))
+ return 0
+
+ if args.out:
+ print(json.dumps(modified_env.env_mod, sort_keys=True, indent=2))
+ return 0
+
+ if args.exec is not None:
+ # We're executing a command in a subprocess with the modified env.
+ return subprocess.run(
+ args.exec, env=modified_env.env, shell=True
+ ).returncode
+
+ # If we got here, we're trying to modify the current shell's env.
+ print(modified_env.side_effects)
+
+ # Let's warn the user if the output is going to stdout instead of being
+ # executed by the shell.
+ python_path = Path(sys.executable).relative_to(os.getcwd())
+ c = shell_modifier.comment # pylint: disable=invalid-name
+ print(
+ cleandoc(
+ f"""
+ {c}
+ {c}Can you see these commands? If so, you probably wanted to
+ {c}source this script instead of running it. Try this instead:
+ {c}
+ {c} . <({str(python_path)} {' '.join(sys.argv)})
+ {c}
+ {c}Run this script with `-h` for more help."""
+ )
+ )
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_ide/py/pw_ide/cli.py b/pw_ide/py/pw_ide/cli.py
new file mode 100644
index 000000000..dd3928b6d
--- /dev/null
+++ b/pw_ide/py/pw_ide/cli.py
@@ -0,0 +1,435 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""CLI tools for pw_ide."""
+
+import argparse
+import enum
+from inspect import cleandoc
+from pathlib import Path
+import re
+from typing import Any, Callable, Dict, List, Optional, Protocol
+
+from pw_ide.commands import (
+ cmd_clear,
+ cmd_cpp,
+ cmd_python,
+ cmd_reset,
+ cmd_setup,
+ cmd_vscode,
+)
+
+from pw_ide.vscode import VscSettingsType
+
+
+def _get_docstring(obj: Any) -> Optional[str]:
+ doc: Optional[str] = getattr(obj, '__doc__', None)
+ return doc
+
+
+class _ParsedDocstring:
+ """Parses help content out of a standard docstring."""
+
+ def __init__(self, obj: Any) -> None:
+ self.description = ''
+ self.epilog = ''
+
+ if obj is not None and (doc := _get_docstring(obj)) is not None:
+ lines = doc.split('\n')
+ self.description = lines.pop(0)
+
+ # Eliminate the blank line between the summary and the main content
+ if len(lines) > 0:
+ lines.pop(0)
+
+ self.epilog = cleandoc('\n'.join(lines))
+
+
+class SphinxStripperState(enum.Enum):
+ SEARCHING = 0
+ COLLECTING = 1
+ HANDLING = 2
+
+
+class SphinxStripper:
+ """Strip Sphinx directives from text.
+
+ The caller can provide an object with methods named _handle_directive_{}
+ to handle specific directives. Otherwise, the default will apply.
+
+ Feed text line by line to .process(line), then get the processed text back
+ with .result().
+ """
+
+ def __init__(self, handler: Any) -> None:
+ self.handler = handler
+ self.directive: str = ''
+ self.tag: str = ''
+ self.lines_to_handle: List[str] = []
+ self.handled_lines: List[str] = []
+ self._prev_state: SphinxStripperState = SphinxStripperState.SEARCHING
+ self._curr_state: SphinxStripperState = SphinxStripperState.SEARCHING
+
+ @property
+ def state(self) -> SphinxStripperState:
+ return self._curr_state
+
+ @state.setter
+ def state(self, value: SphinxStripperState) -> None:
+ self._prev_state = self._curr_state
+ self._curr_state = value
+
+ def search_for_directives(self, line: str) -> None:
+ match = re.search(
+ r'^\.\.\s*(?P<directive>[\-\w]+)::\s*(?P<tag>[\-\w]+)$', line
+ )
+
+ if match is not None:
+ self.directive = match.group('directive')
+ self.tag = match.group('tag')
+ self.state = SphinxStripperState.COLLECTING
+ else:
+ self.handled_lines.append(line)
+
+ def collect_lines(self, line) -> None:
+ # Collect lines associated with a directive, including blank lines in
+ # the middle of the directive text, but not the blank line between the
+ # directive and the start of its text.
+ if not (line.strip() == '' and len(self.lines_to_handle) == 0):
+ self.lines_to_handle.append(line)
+
+ def handle_lines(self, line: str = '') -> None:
+ handler_fn = f'_handle_directive_{self.directive.replace("-", "_")}'
+
+ self.handled_lines.extend(
+ getattr(self.handler, handler_fn, lambda _, s: s)(
+ self.tag, self.lines_to_handle
+ )
+ )
+
+ self.handled_lines.append(line)
+ self.lines_to_handle = []
+ self.state = SphinxStripperState.SEARCHING
+
+ def process_line(self, line: str) -> None:
+ if self.state == SphinxStripperState.SEARCHING:
+ self.search_for_directives(line)
+
+ else:
+ if self.state == SphinxStripperState.COLLECTING:
+ # Assume that indented text below the directive is associated
+ # with the directive.
+ if line.strip() == '' or line[0] in (' ', '\t'):
+ self.collect_lines(line)
+ # When we encounter non-indented text, we're done with this
+ # directive.
+ else:
+ self.state = SphinxStripperState.HANDLING
+
+ if self.state == SphinxStripperState.HANDLING:
+ self.handle_lines(line)
+
+ def result(self) -> str:
+ if self.state == SphinxStripperState.COLLECTING:
+ self.state = SphinxStripperState.HANDLING
+ self.handle_lines()
+
+ return '\n'.join(self.handled_lines)
+
+
+class RawDescriptionSphinxStrippedHelpFormatter(
+ argparse.RawDescriptionHelpFormatter
+):
+ """An argparse formatter that strips Sphinx directives.
+
+ CLI command docstrings can contain Sphinx directives for rendering in docs.
+ But we don't want to include those directives when printing to the terminal.
+ So we strip them and, if appropriate, replace them with something better
+ suited to terminal output.
+ """
+
+ def _reformat(self, text: str) -> str:
+ """Given a block of text, replace Sphinx directives.
+
+ Directive handlers will be provided with the directive name, its tag,
+ and all of the associated lines of text. "Association" is determined by
+ those lines being indented to any degree under the directive.
+
+ Unhandled directives will only have the directive line removed.
+ """
+ sphinx_stripper = SphinxStripper(self)
+
+ for line in text.splitlines():
+ sphinx_stripper.process_line(line)
+
+ # The space at the end prevents the final blank line from being stripped
+ # by argparse, which provides breathing room between the text and the
+ # prompt.
+ return sphinx_stripper.result() + ' '
+
+ def _format_text(self, text: str) -> str:
+ # This overrides an arparse method that is not technically a public API.
+ return super()._format_text(self._reformat(text))
+
+ def _handle_directive_code_block( # pylint: disable=no-self-use
+ self, tag: str, lines: List[str]
+ ) -> List[str]:
+ if tag == 'bash':
+ processed_lines = []
+
+ for line in lines:
+ if line.strip() == '':
+ processed_lines.append(line)
+ else:
+ stripped_line = line.lstrip()
+ indent = len(line) - len(stripped_line)
+ spaces = ' ' * indent
+ processed_line = f'{spaces}$ {stripped_line}'
+ processed_lines.append(processed_line)
+
+ return processed_lines
+
+ return lines
+
+
+class _ParserAdder(Protocol):
+ """Return type for _parser_adder.
+
+ Essentially expresses the type of __call__, which cannot be expressed in
+ type annotations.
+ """
+
+ def __call__(
+ self, subcommand_handler: Callable[..., None], *args: Any, **kwargs: Any
+ ) -> argparse.ArgumentParser:
+ ...
+
+
+def _parser_adder(subcommand_parser) -> _ParserAdder:
+ """Create subcommand parsers with a consistent format.
+
+ When given a subcommand handler, this will produce a parser that pulls the
+ description, help, and epilog values from its docstring, and passes parsed
+ args on to to the function.
+
+ Create a subcommand parser, then feed it to this to get an `add_parser`
+ function:
+
+ .. code-block:: python
+
+ subcommand_parser = parser_root.add_subparsers(help='Subcommands')
+ add_parser = _parser_adder(subcommand_parser)
+
+ Then use `add_parser` instead of `subcommand_parser.add_parser`.
+ """
+
+ def _add_parser(
+ subcommand_handler: Callable[..., None], *args, **kwargs
+ ) -> argparse.ArgumentParser:
+ doc = _ParsedDocstring(subcommand_handler)
+ default_kwargs = dict(
+ # Displayed in list of subcommands
+ description=doc.description,
+ # Displayed as top-line summary for this subcommand's help
+ help=doc.description,
+ # Displayed as detailed help text for this subcommand's help
+ epilog=doc.epilog,
+ # Ensures that formatting is preserved and Sphinx directives are
+ # stripped out when printing to the terminal
+ formatter_class=RawDescriptionSphinxStrippedHelpFormatter,
+ )
+
+ new_kwargs = {**default_kwargs, **kwargs}
+ parser = subcommand_parser.add_parser(*args, **new_kwargs)
+ parser.set_defaults(func=subcommand_handler)
+ return parser
+
+ return _add_parser
+
+
+def _build_argument_parser() -> argparse.ArgumentParser:
+ parser_root = argparse.ArgumentParser(prog='pw ide', description=__doc__)
+
+ parser_root.set_defaults(
+ func=lambda *_args, **_kwargs: parser_root.print_help()
+ )
+
+ subcommand_parser = parser_root.add_subparsers(help='Subcommands')
+ add_parser = _parser_adder(subcommand_parser)
+
+ add_parser(cmd_setup, 'setup')
+
+ parser_reset = add_parser(cmd_reset, 'reset')
+ parser_reset.add_argument(
+ '--hard',
+ action='store_true',
+ help='completely remove the .pw_ide working '
+ 'dir and supported editor files',
+ )
+
+ parser_cpp = add_parser(cmd_cpp, 'cpp')
+ parser_cpp.add_argument(
+ '-l',
+ '--list',
+ dest='should_list_targets',
+ action='store_true',
+ help='list the targets available for C/C++ ' 'language analysis',
+ )
+ parser_cpp.add_argument(
+ '-g',
+ '--get',
+ dest='should_get_target',
+ action='store_true',
+ help='print the current target used for C/C++ ' 'language analysis',
+ )
+ parser_cpp.add_argument(
+ '-s',
+ '--set',
+ dest='target_to_set',
+ metavar='TARGET',
+ help='set the target to use for C/C++ language ' 'server analysis',
+ )
+ parser_cpp.add_argument(
+ '--set-default',
+ dest='use_default_target',
+ action='store_true',
+ help='set the C/C++ analysis target to the default '
+ 'defined in pw_ide settings',
+ )
+ parser_cpp.add_argument(
+ '--no-override',
+ dest='override_current_target',
+ action='store_const',
+ const=False,
+ default=True,
+ help='if called with --set, don\'t override the '
+ 'current target if one is already set',
+ )
+ parser_cpp.add_argument(
+ '--ninja',
+ dest='should_run_ninja',
+ action='store_true',
+ help='use Ninja to generate a compilation database',
+ )
+ parser_cpp.add_argument(
+ '--gn',
+ dest='should_run_gn',
+ action='store_true',
+ help='run gn gen {out} --export-compile-commands, '
+ 'along with any other arguments defined in args.gn',
+ )
+ parser_cpp.add_argument(
+ '-p',
+ '--process',
+ dest='compdb_file_paths',
+ metavar='COMPILATION_DATABASE_FILES',
+ type=Path,
+ nargs='*',
+ help='process a file or several files matching '
+ 'the clang compilation database format',
+ )
+ parser_cpp.add_argument(
+ '--build-dir',
+ type=Path,
+ help='override the build directory defined in ' 'pw_ide settings',
+ )
+ parser_cpp.add_argument(
+ '--clangd-command',
+ action='store_true',
+ help='print the command for your system that runs '
+ 'clangd in the activated Pigweed environment',
+ )
+ parser_cpp.add_argument(
+ '--clangd-command-for',
+ dest='clangd_command_system',
+ metavar='SYSTEM',
+ help='print the command for the specified system '
+ 'that runs clangd in the activated Pigweed '
+ 'environment',
+ )
+
+ parser_python = add_parser(cmd_python, 'python')
+ parser_python.add_argument(
+ '--venv',
+ dest='should_print_venv',
+ action='store_true',
+ help='print the path to the Pigweed Python ' 'virtual environment',
+ )
+
+ parser_vscode = add_parser(cmd_vscode, 'vscode')
+ parser_vscode.add_argument(
+ '--include',
+ nargs='+',
+ type=VscSettingsType,
+ metavar='SETTINGS_TYPE',
+ help='update only these settings types',
+ )
+ parser_vscode.add_argument(
+ '--exclude',
+ nargs='+',
+ type=VscSettingsType,
+ metavar='SETTINGS_TYPE',
+ help='do not update these settings types',
+ )
+ parser_vscode.add_argument(
+ '--no-override',
+ action='store_true',
+ help='don\'t overwrite existing active ' 'settings files',
+ )
+
+ parser_clear = add_parser(cmd_clear, 'clear')
+ parser_clear.add_argument(
+ '--compdb',
+ action='store_true',
+ help='delete all compilation database from ' 'the working directory',
+ )
+ parser_clear.add_argument(
+ '--cache',
+ action='store_true',
+ help='delete all compilation database caches '
+ 'from the working directory',
+ )
+ parser_clear.add_argument(
+ '--editor',
+ metavar='EDITOR',
+ help='delete the active settings file for '
+ 'the provided supported editor',
+ )
+ parser_clear.add_argument(
+ '--editor-backups',
+ metavar='EDITOR',
+ help='delete backup settings files for '
+ 'the provided supported editor',
+ )
+
+ return parser_root
+
+
+def _parse_args() -> argparse.Namespace:
+ args = _build_argument_parser().parse_args()
+ return args
+
+
+def _dispatch_command(func: Callable, **kwargs: Dict[str, Any]) -> int:
+ """Dispatch arguments to a subcommand handler.
+
+ Each CLI subcommand is handled by handler function, which is registered
+ with the subcommand parser with `parser.set_defaults(func=handler)`.
+ By calling this function with the parsed args, the appropriate subcommand
+ handler is called, and the arguments are passed to it as kwargs.
+ """
+ return func(**kwargs)
+
+
+def parse_args_and_dispatch_command() -> int:
+ return _dispatch_command(**vars(_parse_args()))
diff --git a/pw_ide/py/pw_ide/commands.py b/pw_ide/py/pw_ide/commands.py
new file mode 100644
index 000000000..09785f5a5
--- /dev/null
+++ b/pw_ide/py/pw_ide/commands.py
@@ -0,0 +1,845 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_ide CLI command handlers."""
+
+import logging
+from pathlib import Path
+import shlex
+import shutil
+import subprocess
+import sys
+from typing import cast, Callable, List, Optional, Set, Tuple, Union
+
+from pw_cli.color import colors
+
+from pw_ide.cpp import (
+ ClangdSettings,
+ compdb_generate_file_path,
+ CppCompilationDatabase,
+ CppCompilationDatabasesMap,
+ CppIdeFeaturesState,
+ delete_compilation_databases,
+ delete_compilation_database_caches,
+ MAX_COMMANDS_TARGET_FILENAME,
+)
+
+from pw_ide.exceptions import (
+ BadCompDbException,
+ InvalidTargetException,
+ MissingCompDbException,
+)
+
+from pw_ide.python import PythonPaths
+
+from pw_ide.settings import (
+ PigweedIdeSettings,
+ SupportedEditor,
+ SupportedEditorName,
+)
+
+from pw_ide import vscode
+from pw_ide.vscode import VscSettingsManager, VscSettingsType
+
+
+def _no_color(msg: str) -> str:
+ return msg
+
+
+def _split_lines(msg: Union[str, List[str]]) -> Tuple[str, List[str]]:
+ """Turn a list of strings into a tuple of the first and list of rest."""
+ if isinstance(msg, str):
+ return (msg, [])
+
+ return (msg[0], msg[1:])
+
+
+class StatusReporter:
+ """Print user-friendly status reports to the terminal for CLI tools.
+
+ The output of ``demo()`` looks something like this, but more colorful:
+
+ .. code-block:: none
+
+ • FYI, here's some information:
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ Donec condimentum metus molestie metus maximus ultricies ac id dolor.
+ ✓ This is okay, no changes needed.
+ ✓ We changed some things successfully!
+ ⚠ Uh oh, you might want to be aware of this.
+ ❌ This is bad! Things might be broken!
+
+ You can instead redirect these lines to logs without formatting by
+ substituting ``LoggingStatusReporter``. Consumers of this should be
+ designed to take any subclass and not make assumptions about where the
+ output will go. But the reason you would choose this over plain logging is
+ because you want to support pretty-printing to the terminal.
+
+ This is also "themable" in the sense that you can subclass this, override
+ the methods with whatever formatting you want, and supply the subclass to
+ anything that expects an instance of this.
+
+ Key:
+
+ - info: Plain ol' informational status.
+ - ok: Something was checked and it was okay.
+ - new: Something needed to be changed/updated and it was successfully.
+ - wrn: Warning, non-critical.
+ - err: Error, critical.
+
+ This doesn't expose the %-style string formatting that is used in idiomatic
+ Python logging, but this shouldn't be used for performance-critical logging
+ situations anyway.
+ """
+
+ def _report( # pylint: disable=no-self-use
+ self,
+ msg: Union[str, List[str]],
+ color: Callable[[str], str],
+ char: str,
+ func: Callable,
+ silent: bool,
+ ) -> None:
+ """Actually print/log/whatever the status lines."""
+ first_line, rest_lines = _split_lines(msg)
+ first_line = color(f'{char} {first_line}')
+ spaces = ' ' * len(char)
+ rest_lines = [color(f'{spaces} {line}') for line in rest_lines]
+
+ if not silent:
+ for line in [first_line, *rest_lines]:
+ func(line)
+
+ def demo(self):
+ """Run this to see what your status reporter output looks like."""
+ self.info(
+ [
+ 'FYI, here\'s some information:',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ 'Donec condimentum metus molestie metus maximus ultricies '
+ 'ac id dolor.',
+ ]
+ )
+ self.ok('This is okay, no changes needed.')
+ self.new('We changed some things successfully!')
+ self.wrn('Uh oh, you might want to be aware of this.')
+ self.err('This is bad! Things might be broken!')
+
+ def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '\u2022', print, silent)
+
+ def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, colors().blue, '\u2713', print, silent)
+
+ def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, colors().green, '\u2713', print, silent)
+
+ def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, colors().yellow, '\u26A0', print, silent)
+
+ def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, colors().red, '\u274C', print, silent)
+
+
+class LoggingStatusReporter(StatusReporter):
+ """Print status lines to logs instead of to the terminal."""
+
+ def __init__(self, logger: logging.Logger) -> None:
+ self.logger = logger
+ super().__init__()
+
+ def _report(
+ self,
+ msg: Union[str, List[str]],
+ color: Callable[[str], str],
+ char: str,
+ func: Callable,
+ silent: bool,
+ ) -> None:
+ first_line, rest_lines = _split_lines(msg)
+
+ if not silent:
+ for line in [first_line, *rest_lines]:
+ func(line)
+
+ def info(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.info, silent)
+
+ def ok(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.info, silent)
+
+ def new(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.info, silent)
+
+ def wrn(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.warning, silent)
+
+ def err(self, msg: Union[str, List[str]], silent: bool = False) -> None:
+ self._report(msg, _no_color, '', self.logger.error, silent)
+
+
+def _make_working_dir(
+ reporter: StatusReporter, settings: PigweedIdeSettings, quiet: bool = False
+) -> None:
+ if not settings.working_dir.exists():
+ settings.working_dir.mkdir()
+ reporter.new(
+ 'Initialized the Pigweed IDE working directory at '
+ f'{settings.working_dir}'
+ )
+ else:
+ if not quiet:
+ reporter.ok(
+ 'Pigweed IDE working directory already present at '
+ f'{settings.working_dir}'
+ )
+
+
+def _report_unrecognized_editor(reporter: StatusReporter, editor: str) -> None:
+ supported_editors = ', '.join(sorted([ed.value for ed in SupportedEditor]))
+ reporter.wrn(f'Unrecognized editor: {editor}')
+ reporter.wrn('This may not be an automatically-supported editor.')
+ reporter.wrn(f'Automatically-supported editors: {supported_editors}')
+
+
+def cmd_clear(
+ compdb: bool,
+ cache: bool,
+ editor: Optional[SupportedEditorName],
+ editor_backups: Optional[SupportedEditorName],
+ silent: bool = False,
+ reporter: StatusReporter = StatusReporter(),
+ pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
+) -> None:
+ """Clear components of the IDE features.
+
+ In contrast to the ``reset`` subcommand, ``clear`` allows you to specify
+ components to delete. You will not need this command under normal
+ circumstances.
+ """
+ if compdb:
+ delete_compilation_databases(pw_ide_settings)
+ reporter.wrn('Cleared compilation databases', silent)
+
+ if cache:
+ delete_compilation_database_caches(pw_ide_settings)
+ reporter.wrn('Cleared compilation database caches', silent)
+
+ if editor is not None:
+ try:
+ validated_editor = SupportedEditor(editor)
+ except ValueError:
+ _report_unrecognized_editor(reporter, cast(str, editor))
+ sys.exit(1)
+
+ if validated_editor == SupportedEditor.VSCODE:
+ vsc_settings_manager = VscSettingsManager(pw_ide_settings)
+ vsc_settings_manager.delete_all_active_settings()
+
+ reporter.wrn(
+ f'Cleared active settings for {validated_editor.value}', silent
+ )
+
+ if editor_backups is not None:
+ try:
+ validated_editor = SupportedEditor(editor_backups)
+ except ValueError:
+ _report_unrecognized_editor(reporter, cast(str, editor))
+ sys.exit(1)
+
+ if validated_editor == SupportedEditor.VSCODE:
+ vsc_settings_manager = VscSettingsManager(pw_ide_settings)
+ vsc_settings_manager.delete_all_backups()
+
+ reporter.wrn(
+ f'Cleared backup settings for {validated_editor.value}',
+ silent=silent,
+ )
+
+
+def cmd_reset(
+ hard: bool = False,
+ reporter: StatusReporter = StatusReporter(),
+ pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
+) -> None:
+ """Reset IDE settings.
+
+ This will clear your .pw_ide working directory and active settings for
+ supported editors, restoring your repository to a pre-"pw ide setup" state.
+ Any clangd caches in the working directory will not be removed, so that they
+ don't need to be generated again later. All backed up supported editor
+ settings will also be left in place.
+
+ Adding the --hard flag will completely delete the .pw_ide directory and all
+ supported editor backup settings, restoring your repository to a
+ pre-`pw ide setup` state.
+
+ This command does not affect this project's pw_ide and editor settings or
+ your own pw_ide and editor override settings.
+ """
+ delete_compilation_databases(pw_ide_settings)
+ vsc_settings_manager = VscSettingsManager(pw_ide_settings)
+ vsc_settings_manager.delete_all_active_settings()
+
+ if hard:
+ try:
+ shutil.rmtree(pw_ide_settings.working_dir)
+ except FileNotFoundError:
+ pass
+
+ vsc_settings_manager.delete_all_backups()
+
+ reporter.wrn('Pigweed IDE settings were reset!')
+
+
+def cmd_setup(
+ reporter: StatusReporter = StatusReporter(),
+ pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
+) -> None:
+ """Set up or update your Pigweed project IDE features.
+
+ This will automatically set up your development environment with all the
+ features that Pigweed IDE supports, with sensible defaults.
+
+ At minimum, this command will create the .pw_ide working directory and
+ create settings files for all supported editors. Projects can define
+ additional setup steps in .pw_ide.yaml.
+
+ When new IDE features are introduced in the future (either by Pigweed or
+ your downstream project), you can re-run this command to set up the new
+ features. It will not overwrite or break any of your existing configuration.
+ """
+ _make_working_dir(reporter, pw_ide_settings)
+
+ if pw_ide_settings.editor_enabled('vscode'):
+ cmd_vscode()
+
+ for command in pw_ide_settings.setup:
+ subprocess.run(shlex.split(command))
+
+
+def cmd_vscode(
+ include: Optional[List[VscSettingsType]] = None,
+ exclude: Optional[List[VscSettingsType]] = None,
+ no_override: bool = False,
+ reporter: StatusReporter = StatusReporter(),
+ pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
+) -> None:
+ """Configure support for Visual Studio Code.
+
+ This will replace your current Visual Studio Code (VSC) settings for this
+ project (in ``.vscode/settings.json``, etc.) with the following sets of
+ settings, in order:
+
+ - The Pigweed default settings
+ - Your project's settings, if any (in ``.vscode/pw_project_settings.json``)
+ - Your personal settings, if any (in ``.vscode/pw_user_settings.json``)
+
+ In other words, settings files lower on the list can override settings
+ defined by those higher on the list. Settings defined in the sources above
+ are not active in VSC until they are merged and output to the current
+ settings file by running:
+
+ .. code-block:: bash
+
+ pw ide vscode
+
+ Refer to the Visual Studio Code documentation for more information about
+ these settings: https://code.visualstudio.com/docs/getstarted/settings
+
+ This command also manages VSC tasks (``.vscode/tasks.json``) and extensions
+ (``.vscode/extensions.json``). You can explicitly control which of these
+ settings types ("settings", "tasks", and "extensions") is modified by
+ this command by using the ``--include`` or ``--exclude`` options.
+
+ Your current VSC settings will never change unless you run ``pw ide``
+ commands. Since the current VSC settings are an artifact built from the
+ three settings files described above, you should avoid manually editing
+ that file; it will be replaced the next time you run ``pw ide vscode``. A
+ backup of your previous settings file will be made, and you can diff it
+ against the new file to see what changed.
+
+ These commands will never modify your VSC user settings, which are
+ stored outside of the project repository and apply globally to all VSC
+ instances.
+
+ The settings files are system-specific and shouldn't be checked into the
+ repository, except for the project settings (those with ``pw_project_``),
+ which can be used to define consistent settings for everyone working on the
+ project.
+
+ Note that support for VSC can be disabled at the project level or the user
+ level by adding the following to .pw_ide.yaml or .pw_ide.user.yaml
+ respectively:
+
+ .. code-block:: yaml
+
+ editors:
+ vscode: false
+
+ Likewise, it can be enabled by setting that value to true. It is enabled by
+ default.
+ """
+ if not pw_ide_settings.editor_enabled('vscode'):
+ reporter.wrn('Visual Studio Code support is disabled in settings!')
+ sys.exit(1)
+
+ if not vscode.DEFAULT_SETTINGS_PATH.exists():
+ vscode.DEFAULT_SETTINGS_PATH.mkdir()
+
+ vsc_manager = VscSettingsManager(pw_ide_settings)
+
+ if include is None:
+ include_set = set(VscSettingsType.all())
+ else:
+ include_set = set(include)
+
+ if exclude is None:
+ exclude_set: Set[VscSettingsType] = set()
+ else:
+ exclude_set = set(exclude)
+
+ types_to_update = cast(
+ List[VscSettingsType], tuple(include_set - exclude_set)
+ )
+
+ for settings_type in types_to_update:
+ active_settings_existed = vsc_manager.active(settings_type).is_present()
+
+ if no_override and active_settings_existed:
+ reporter.ok(
+ f'Visual Studio Code active {settings_type.value} '
+ 'already present; will not overwrite'
+ )
+
+ else:
+ with vsc_manager.active(settings_type).modify(
+ reinit=True
+ ) as active_settings:
+ vsc_manager.default(settings_type).sync_to(active_settings)
+ vsc_manager.project(settings_type).sync_to(active_settings)
+ vsc_manager.user(settings_type).sync_to(active_settings)
+
+ verb = 'Updated' if active_settings_existed else 'Created'
+ reporter.new(
+ f'{verb} Visual Studio Code active ' f'{settings_type.value}'
+ )
+
+
+# TODO(chadnorvell): Break up this function.
+# The linting errors are a nuisance but they're beginning to have a point.
+def cmd_cpp( # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
+ should_list_targets: bool,
+ should_get_target: bool,
+ target_to_set: Optional[str],
+ compdb_file_paths: Optional[List[Path]],
+ build_dir: Optional[Path],
+ use_default_target: bool = False,
+ should_run_ninja: bool = False,
+ should_run_gn: bool = False,
+ override_current_target: bool = True,
+ clangd_command: bool = False,
+ clangd_command_system: Optional[str] = None,
+ reporter: StatusReporter = StatusReporter(),
+ pw_ide_settings: PigweedIdeSettings = PigweedIdeSettings(),
+) -> None:
+ """Configure C/C++ code intelligence support.
+
+ Code intelligence can be provided by clangd or other language servers that
+ use the clangd compilation database format, defined at:
+ https://clang.llvm.org/docs/JSONCompilationDatabase.html
+
+ This command helps you use clangd with Pigweed projects, which use multiple
+ toolchains within a distinct environment, and often define multiple targets.
+ This means compilation units are likely have multiple compile commands, and
+ clangd is not equipped to deal with this out of the box. We handle this by:
+
+ - Processing the compilation database produced the build system into
+ multiple internally-consistent compilation databases, one for each target
+ (where a "target" is a particular build for a particular system using a
+ particular toolchain).
+
+ - Providing commands to select which target you want to use for code
+ analysis.
+
+ Refer to the Pigweed documentation or your build system's documentation to
+ learn how to produce a clangd compilation database. Once you have one, run
+ this command to process it (or provide a glob to process multiple):
+
+ .. code-block:: bash
+
+ pw ide cpp --process {path to compile_commands.json}
+
+ If you're using GN to generate the compilation database, you can do that and
+ process it in a single command:
+
+ .. code-block:: bash
+
+ pw ide cpp --gn
+
+ You can do the same for a Ninja build (whether it was generated by GN or
+ another way):
+
+ .. code-block:: bash
+
+ pw ide cpp --ninja
+
+ You can now examine the targets that are available to you:
+
+ .. code-block:: bash
+
+ pw ide cpp --list
+
+ ... and select the target you want to use:
+
+ .. code-block:: bash
+
+ pw ide cpp --set host_clang
+
+ As long as your editor or language server plugin is properly configured, you
+ will now get code intelligence features relevant to that particular target.
+
+ You can see what target is selected by running:
+
+ .. code-block:: bash
+
+ pw ide cpp
+
+ Whenever you switch to a target you haven't used before, clangd will need to
+ index the build, which may take several minutes. These indexes are cached,
+ so you can switch between targets without re-indexing each time.
+
+ If your build configuration changes significantly (e.g. you add a new file
+ to the project), you will need to re-process the compilation database for
+ that change to be recognized. Your target selection will not change, and
+ your index will only need to be incrementally updated.
+
+ You can generate the clangd command your editor needs to run with:
+
+ .. code-block:: bash
+
+ pw ide cpp --clangd-command
+
+ If your editor uses JSON for configuration, you can export the same command
+ in that format:
+
+ .. code-block:: bash
+
+ pw ide cpp --clangd-command-for json
+ """
+ _make_working_dir(reporter, pw_ide_settings, quiet=True)
+
+ # If true, no arguments were provided so we do the default behavior.
+ default = True
+
+ build_dir = (
+ build_dir if build_dir is not None else pw_ide_settings.build_dir
+ )
+
+ if compdb_file_paths is not None:
+ should_process = True
+
+ if len(compdb_file_paths) == 0:
+ compdb_file_paths = pw_ide_settings.compdb_paths_expanded
+ else:
+ should_process = False
+ # This simplifies typing in the rest of this method. We rely on
+ # `should_process` instead of the status of this variable.
+ compdb_file_paths = []
+
+ # Order of operations matters from here on. It should be possible to run
+ # a build system command to generate a compilation database, then process
+ # the compilation database, then successfully set the target in a single
+ # command.
+
+ # Use Ninja to generate the initial compile_commands.json
+ if should_run_ninja:
+ default = False
+
+ ninja_commands = ['ninja', '-t', 'compdb']
+ reporter.info(f'Running Ninja: {" ".join(ninja_commands)}')
+
+ output_compdb_file_path = build_dir / compdb_generate_file_path()
+
+ try:
+ # Ninja writes to STDOUT, so we capture to a file.
+ with open(output_compdb_file_path, 'w') as compdb_file:
+ result = subprocess.run(
+ ninja_commands,
+ cwd=build_dir,
+ stdout=compdb_file,
+ stderr=subprocess.PIPE,
+ )
+ except FileNotFoundError:
+ reporter.err(f'Could not open path! {str(output_compdb_file_path)}')
+
+ if result.returncode == 0:
+ reporter.info('Ran Ninja successfully!')
+ should_process = True
+ compdb_file_paths.append(output_compdb_file_path)
+ else:
+ reporter.err('Something went wrong!')
+ # Convert from bytes and remove trailing newline
+ err = result.stderr.decode().split('\n')[:-1]
+
+ for line in err:
+ reporter.err(line)
+
+ sys.exit(1)
+
+ # Use GN to generate the initial compile_commands.json
+ if should_run_gn:
+ default = False
+
+ gn_commands = ['gn', 'gen', str(build_dir), '--export-compile-commands']
+
+ try:
+ with open(build_dir / 'args.gn') as args_file:
+ gn_args = [
+ line
+ for line in args_file.readlines()
+ if not line.startswith('#')
+ ]
+ except FileNotFoundError:
+ gn_args = []
+
+ gn_args_string = 'none' if len(gn_args) == 0 else ', '.join(gn_args)
+
+ reporter.info(
+ [f'Running GN: {" ".join(gn_commands)} (args: {gn_args_string})']
+ )
+
+ result = subprocess.run(gn_commands, capture_output=True)
+ gn_status_lines = ['Ran GN successfully!']
+
+ if result.returncode == 0:
+ # Convert from bytes and remove trailing newline
+ out = result.stdout.decode().split('\n')[:-1]
+
+ for line in out:
+ gn_status_lines.append(line)
+
+ reporter.info(gn_status_lines)
+ should_process = True
+ output_compdb_file_path = build_dir / compdb_generate_file_path()
+ compdb_file_paths.append(output_compdb_file_path)
+ else:
+ reporter.err('Something went wrong!')
+ # Convert from bytes and remove trailing newline
+ err = result.stderr.decode().split('\n')[:-1]
+
+ for line in err:
+ reporter.err(line)
+
+ sys.exit(1)
+
+ if should_process:
+ default = False
+ prev_targets = len(CppIdeFeaturesState(pw_ide_settings))
+ compdb_databases: List[CppCompilationDatabasesMap] = []
+ last_processed_path = Path()
+
+ for compdb_file_path in compdb_file_paths:
+ # If the path is a dir, append the default compile commands
+ # file name.
+ if compdb_file_path.is_dir():
+ compdb_file_path /= compdb_generate_file_path()
+
+ try:
+ compdb_databases.append(
+ CppCompilationDatabase.load(
+ Path(compdb_file_path), build_dir
+ ).process(
+ settings=pw_ide_settings,
+ path_globs=pw_ide_settings.clangd_query_drivers(),
+ )
+ )
+ except MissingCompDbException:
+ reporter.err(f'File not found: {str(compdb_file_path)}')
+
+ if '*' in str(compdb_file_path):
+ reporter.wrn(
+ 'It looks like you provided a glob that '
+ 'did not match any files.'
+ )
+
+ sys.exit(1)
+ # TODO(chadnorvell): Recover more gracefully from errors.
+ except BadCompDbException:
+ reporter.err(
+ 'File does not match compilation database format: '
+ f'{str(compdb_file_path)}'
+ )
+ sys.exit(1)
+
+ last_processed_path = compdb_file_path
+
+ if len(compdb_databases) == 0:
+ reporter.err(
+ 'No compilation databases found in: '
+ f'{str(compdb_file_paths)}'
+ )
+ sys.exit(1)
+
+ try:
+ CppCompilationDatabasesMap.merge(*compdb_databases).write()
+ except TypeError:
+ reporter.err('Could not serialize file to JSON!')
+
+ total_targets = len(CppIdeFeaturesState(pw_ide_settings))
+ new_targets = total_targets - prev_targets
+
+ if len(compdb_file_paths) == 1:
+ processed_text = str(last_processed_path)
+ else:
+ processed_text = f'{len(compdb_file_paths)} compilation databases'
+
+ reporter.new(
+ [
+ f'Processed {processed_text} '
+ f'to {pw_ide_settings.working_dir}',
+ f'{total_targets} targets are now available '
+ f'({new_targets} are new)',
+ ]
+ )
+
+ if use_default_target:
+ defined_default = pw_ide_settings.default_target
+ max_commands_target: Optional[str] = None
+
+ try:
+ with open(
+ pw_ide_settings.working_dir / MAX_COMMANDS_TARGET_FILENAME
+ ) as max_commands_target_file:
+ max_commands_target = max_commands_target_file.readline()
+ except FileNotFoundError:
+ pass
+
+ if defined_default is None and max_commands_target is None:
+ reporter.err('Can\'t use default target because none is defined!')
+ reporter.wrn('Have you processed a compilation database yet?')
+ sys.exit(1)
+
+ target_to_set = (
+ defined_default
+ if defined_default is not None
+ else max_commands_target
+ )
+
+ if target_to_set is not None:
+ default = False
+
+ # Always set the target if it's not already set, but if it is,
+ # respect the --no-override flag.
+ should_set_target = (
+ CppIdeFeaturesState(pw_ide_settings).current_target is None
+ or override_current_target
+ )
+
+ if should_set_target:
+ try:
+ CppIdeFeaturesState(
+ pw_ide_settings
+ ).current_target = target_to_set
+ except InvalidTargetException:
+ reporter.err(
+ [
+ f'Invalid target! {target_to_set} not among the '
+ 'defined targets.',
+ 'Check .pw_ide.yaml or .pw_ide.user.yaml for defined '
+ 'targets.',
+ ]
+ )
+ sys.exit(1)
+ except MissingCompDbException:
+ reporter.err(
+ [
+ f'File not found for target! {target_to_set}',
+ 'Did you run pw ide cpp --process '
+ '{path to compile_commands.json}?',
+ ]
+ )
+ sys.exit(1)
+
+ reporter.new(
+ 'Set C/C++ language server analysis target to: '
+ f'{target_to_set}'
+ )
+ else:
+ reporter.ok(
+ 'Target already is set and will not be overridden: '
+ f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
+ )
+
+ if clangd_command:
+ default = False
+ reporter.info(
+ [
+ 'Command to run clangd with Pigweed paths:',
+ ClangdSettings(pw_ide_settings).command(),
+ ]
+ )
+
+ if clangd_command_system is not None:
+ default = False
+ reporter.info(
+ [
+ 'Command to run clangd with Pigweed paths for '
+ f'{clangd_command_system}:',
+ ClangdSettings(pw_ide_settings).command(clangd_command_system),
+ ]
+ )
+
+ if should_list_targets:
+ default = False
+ targets_list_status = [
+ 'C/C++ targets available for language server analysis:'
+ ]
+
+ for target in sorted(
+ CppIdeFeaturesState(pw_ide_settings).enabled_available_targets
+ ):
+ targets_list_status.append(f'\t{target}')
+
+ reporter.info(targets_list_status)
+
+ if should_get_target or default:
+ reporter.info(
+ 'Current C/C++ language server analysis target: '
+ f'{CppIdeFeaturesState(pw_ide_settings).current_target}'
+ )
+
+
+def cmd_python(
+ should_print_venv: bool, reporter: StatusReporter = StatusReporter()
+) -> None:
+ """Configure Python code intelligence support.
+
+ You can generate the path to the Python virtual environment interpreter that
+ your editor/language server should use with:
+
+ .. code-block:: bash
+
+ pw ide python --venv
+ """
+ # If true, no arguments were provided and we should do the default
+ # behavior.
+ default = True
+
+ if should_print_venv or default:
+ reporter.info(
+ [
+ 'Location of the Pigweed Python virtual environment:',
+ PythonPaths().interpreter,
+ ]
+ )
diff --git a/pw_ide/py/pw_ide/cpp.py b/pw_ide/py/pw_ide/cpp.py
new file mode 100644
index 000000000..e58d66d74
--- /dev/null
+++ b/pw_ide/py/pw_ide/cpp.py
@@ -0,0 +1,1082 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Configure C/C++ IDE support for Pigweed projects.
+
+We support C/C++ code analysis via ``clangd``, or other language servers that
+are compatible with the ``clangd`` compilation database format.
+
+While clangd can work well out of the box for typical C++ codebases, some work
+is required to coax it to work for embedded projects. In particular, Pigweed
+projects use multiple toolchains within a distinct environment, and almost
+always define multiple targets. This means compilation units are likely have
+multiple compile commands and the toolchain executables are unlikely to be in
+your path. ``clangd`` is not equipped to deal with this out of the box. We
+handle this by:
+
+- Processing the compilation database produced by the build system into
+ multiple internally-consistent compilation databases, one for each target
+ (where a "target" is a particular build for a particular system using a
+ particular toolchain).
+
+- Creating unambiguous paths to toolchain drivers to ensure the right toolchain
+ is used and that clangd knows where to find that toolchain's system headers.
+
+- Providing tools for working with several compilation databases that are
+ spiritually similar to tools like ``pyenv``, ``rbenv``, etc.
+
+In short, we take the probably-broken compilation database that the build system
+generates, process it into several not-broken compilation databases in the
+``pw_ide`` working directory, and provide a stable symlink that points to the
+selected active target's compliation database. If ``clangd`` is configured to
+point at the symlink and is set up with the right paths, you'll get code
+intelligence.
+"""
+
+from collections import defaultdict
+from dataclasses import dataclass
+import glob
+from io import TextIOBase
+import json
+import os
+from pathlib import Path
+import platform
+from typing import (
+ Any,
+ cast,
+ Callable,
+ Dict,
+ Generator,
+ List,
+ Optional,
+ Tuple,
+ TypedDict,
+ Union,
+)
+
+from pw_ide.exceptions import (
+ BadCompDbException,
+ InvalidTargetException,
+ MissingCompDbException,
+ UnresolvablePathException,
+)
+
+from pw_ide.settings import PigweedIdeSettings, PW_PIGWEED_CIPD_INSTALL_DIR
+from pw_ide.symlinks import set_symlink
+
+_COMPDB_FILE_PREFIX = 'compile_commands'
+_COMPDB_FILE_SEPARATOR = '_'
+_COMPDB_FILE_EXTENSION = '.json'
+
+_COMPDB_CACHE_DIR_PREFIX = '.cache'
+_COMPDB_CACHE_DIR_SEPARATOR = '_'
+
+COMPDB_FILE_GLOB = f'{_COMPDB_FILE_PREFIX}*{_COMPDB_FILE_EXTENSION}'
+COMPDB_CACHE_DIR_GLOB = f'{_COMPDB_CACHE_DIR_PREFIX}*'
+
+MAX_COMMANDS_TARGET_FILENAME = 'max_commands_target'
+
+_SUPPORTED_TOOLCHAIN_EXECUTABLES = ('clang', 'gcc', 'g++')
+
+
+def compdb_generate_file_path(target: str = '') -> Path:
+ """Generate a compilation database file path."""
+
+ path = Path(f'{_COMPDB_FILE_PREFIX}{_COMPDB_FILE_EXTENSION}')
+
+ if target:
+ path = path.with_name(
+ f'{_COMPDB_FILE_PREFIX}'
+ f'{_COMPDB_FILE_SEPARATOR}{target}'
+ f'{_COMPDB_FILE_EXTENSION}'
+ )
+
+ return path
+
+
+def compdb_generate_cache_path(target: str = '') -> Path:
+ """Generate a compilation database cache directory path."""
+
+ path = Path(f'{_COMPDB_CACHE_DIR_PREFIX}')
+
+ if target:
+ path = path.with_name(
+ f'{_COMPDB_CACHE_DIR_PREFIX}'
+ f'{_COMPDB_CACHE_DIR_SEPARATOR}{target}'
+ )
+
+ return path
+
+
+def compdb_target_from_path(filename: Path) -> Optional[str]:
+ """Get a target name from a compilation database path."""
+
+ # The length of the common compilation database file name prefix
+ prefix_length = len(_COMPDB_FILE_PREFIX) + len(_COMPDB_FILE_SEPARATOR)
+
+ if len(filename.stem) <= prefix_length:
+ # This will return None for the symlink filename, and any filename that
+ # is too short to be a compilation database.
+ return None
+
+ if filename.stem[:prefix_length] != (
+ _COMPDB_FILE_PREFIX + _COMPDB_FILE_SEPARATOR
+ ):
+ # This will return None for any files that don't have the common prefix.
+ return None
+
+ return filename.stem[prefix_length:]
+
+
+def _none_to_empty_str(value: Optional[str]) -> str:
+ return value if value is not None else ''
+
+
+def _none_if_not_exists(path: Path) -> Optional[Path]:
+ return path if path.exists() else None
+
+
+def compdb_cache_path_if_exists(
+ working_dir: Path, target: Optional[str]
+) -> Optional[Path]:
+ return _none_if_not_exists(
+ working_dir / compdb_generate_cache_path(_none_to_empty_str(target))
+ )
+
+
+def target_is_enabled(
+ target: Optional[str], settings: PigweedIdeSettings
+) -> bool:
+ """Determine if a target is enabled.
+
+ By default, all targets are enabled. If specific targets are defined in a
+ settings file, only those targets will be enabled.
+ """
+
+ if target is None:
+ return False
+
+ if len(settings.targets) == 0:
+ return True
+
+ return target in settings.targets
+
+
+def path_to_executable(
+ exe: str,
+ *,
+ default_path: Optional[Path] = None,
+ path_globs: Optional[List[str]] = None,
+ strict: bool = False,
+) -> Optional[Path]:
+ """Return the path to a compiler executable.
+
+ In a ``clang`` compile command, the executable may or may not include a
+ path. For example:
+
+ .. code-block:: none
+
+ /usr/bin/clang <- includes a path
+ ../path/to/my/clang <- includes a path
+ clang <- doesn't include a path
+
+ If it includes a path, then ``clangd`` will have no problem finding the
+ driver, so we can simply return the path. If the executable *doesn't*
+ include a path, then ``clangd`` will search ``$PATH``, and may not find the
+ intended driver unless you actually want the default system toolchain or
+ Pigweed paths have been added to ``$PATH``. So this function provides two
+ options for resolving those ambiguous paths:
+
+ - Provide a default path, and all executables without a path will be
+ re-written with a path within the default path.
+ - Provide the a set of globs that will be used to search for the executable,
+ which will normally be the query driver globs used with clangd.
+
+ By default, if neither of these options is chosen, or if the executable
+ cannot be found within the provided globs, the pathless executable that was
+ provided will be returned, and clangd will resort to searching $PATH. If you
+ instead pass ``strict=True``, this will raise an exception if an unambiguous
+ path cannot be constructed.
+
+ This function only tries to ensure that all executables have a path to
+ eliminate ambiguity. A couple of important things to keep in mind:
+
+ - This doesn't guarantee that the path exists or an executable actually
+ exists at the path. It only ensures that some path is provided to an
+ executable.
+ - An executable being present at the indicated path doesn't guarantee that
+ it will work flawlessly for clangd code analysis. The clangd
+ ``--query-driver`` argument needs to include a path to this executable in
+ order for its bundled headers to be resolved correctly.
+
+ This function also filters out invalid or unsupported drivers. For example,
+ build systems will sometimes naively include build steps for Python or other
+ languages in the compilation database, which are not usable with clangd.
+ As a result, this function has four possible end states:
+
+ - It returns a path with an executable that can be used as a ``clangd``
+ driver.
+ - It returns ``None``, meaning the compile command was invalid.
+ - It returns the same string that was provided (as a ``Path``), if a path
+ couldn't be resolved and ``strict=False``.
+ - It raises an ``UnresolvablePathException`` if the executable cannot be
+ placed in an unambiguous path and ``strict=True``.
+ """
+ maybe_path = Path(exe)
+
+ # We were give an empty string, not a path. Not a valid command.
+ if len(maybe_path.parts) == 0:
+ return None
+
+ # Determine if the executable name matches supported drivers.
+ is_supported_driver = False
+
+ for supported_executable in _SUPPORTED_TOOLCHAIN_EXECUTABLES:
+ if supported_executable in maybe_path.name:
+ is_supported_driver = True
+
+ if not is_supported_driver:
+ return None
+
+ # Now, ensure the executable has a path.
+
+ # This is either a relative or absolute path -- return it.
+ if len(maybe_path.parts) > 1:
+ return maybe_path
+
+ # If we got here, there's only one "part", so we assume it's an executable
+ # without a path. This logic doesn't work with a path like `./exe` since
+ # that also yields only one part. So currently this breaks if you actually
+ # have your compiler executable in your root build directory, which is
+ # (hopefully) very rare.
+
+ # If we got a default path, use it.
+ if default_path is not None:
+ return default_path / maybe_path
+
+ # Otherwise, try to find the executable within the query driver globs.
+ # Note that unlike the previous paths, this path will only succeed if an
+ # executable actually exists somewhere in the query driver globs.
+ if path_globs is not None:
+ for path_glob in path_globs:
+ for path_str in glob.iglob(path_glob):
+ path = Path(path_str)
+ if path.name == maybe_path.name:
+ return path.absolute()
+
+ if strict:
+ raise UnresolvablePathException(
+ f'Cannot place {exe} in an unambiguous path!'
+ )
+
+ return maybe_path
+
+
+def command_parts(command: str) -> Tuple[str, List[str]]:
+ """Return the executable string and the rest of the command tokens."""
+ parts = command.split()
+ head = parts[0] if len(parts) > 0 else ''
+ tail = parts[1:] if len(parts) > 1 else []
+ return head, tail
+
+
+# This is a clumsy way to express optional keys, which is not directly
+# supported in TypedDicts right now.
+class BaseCppCompileCommandDict(TypedDict):
+ file: str
+ directory: str
+ output: Optional[str]
+
+
+class CppCompileCommandDictWithCommand(BaseCppCompileCommandDict):
+ command: str
+
+
+class CppCompileCommandDictWithArguments(BaseCppCompileCommandDict):
+ arguments: List[str]
+
+
+CppCompileCommandDict = Union[
+ CppCompileCommandDictWithCommand, CppCompileCommandDictWithArguments
+]
+
+
+class CppCompileCommand:
+ """A representation of a clang compilation database compile command.
+
+ See: https://clang.llvm.org/docs/JSONCompilationDatabase.html
+ """
+
+ def __init__(
+ self,
+ file: str,
+ directory: str,
+ command: Optional[str] = None,
+ arguments: Optional[List[str]] = None,
+ output: Optional[str] = None,
+ ) -> None:
+ # Per the spec, either one of these two must be present. clangd seems
+ # to prefer "arguments" when both are present.
+ if command is None and arguments is None:
+ raise TypeError(
+ 'A compile command requires either \'command\' '
+ 'or \'arguments\'.'
+ )
+
+ if command is None:
+ raise TypeError(
+ 'Compile commands without \'command\' ' 'are not supported yet.'
+ )
+
+ self._command = command
+ self._arguments = arguments
+ self._file = file
+ self._directory = directory
+
+ executable, tokens = command_parts(command)
+ self._executable_path = Path(executable)
+ self._inferred_output: Optional[str] = None
+
+ try:
+ # Find the output argument and grab its value.
+ output_flag_idx = tokens.index('-o')
+ self._inferred_output = tokens[output_flag_idx + 1]
+ except ValueError:
+ # No -o found, probably not a C/C++ compile command.
+ self._inferred_output = None
+ except IndexError:
+ # It has an -o but no argument after it.
+ raise TypeError(
+ 'Failed to load compile command with no output argument!'
+ )
+
+ self._provided_output = output
+ self.target: Optional[str] = None
+
+ @property
+ def file(self) -> str:
+ return self._file
+
+ @property
+ def directory(self) -> str:
+ return self._directory
+
+ @property
+ def command(self) -> Optional[str]:
+ return self._command
+
+ @property
+ def arguments(self) -> Optional[List[str]]:
+ return self._arguments
+
+ @property
+ def output(self) -> Optional[str]:
+ # We're ignoring provided output values for now.
+ return self._inferred_output
+
+ @property
+ def output_path(self) -> Optional[Path]:
+ if self.output is None:
+ return None
+
+ return Path(self.directory) / Path(self.output)
+
+ @property
+ def executable_path(self) -> Path:
+ return self._executable_path
+
+ @property
+ def executable_name(self) -> str:
+ return self.executable_path.name
+
+ @classmethod
+ def from_dict(
+ cls, compile_command_dict: Dict[str, Any]
+ ) -> 'CppCompileCommand':
+ return cls(
+ # We want to let possible Nones through to raise at runtime.
+ file=cast(str, compile_command_dict.get('file')),
+ directory=cast(str, compile_command_dict.get('directory')),
+ command=compile_command_dict.get('command'),
+ arguments=compile_command_dict.get('arguments'),
+ output=compile_command_dict.get('output'),
+ )
+
+ @classmethod
+ def try_from_dict(
+ cls, compile_command_dict: Dict[str, Any]
+ ) -> Optional['CppCompileCommand']:
+ try:
+ return cls.from_dict(compile_command_dict)
+ except TypeError:
+ return None
+
+ def process(
+ self,
+ *,
+ default_path: Optional[Path] = None,
+ path_globs: Optional[List[str]] = None,
+ strict: bool = False,
+ ) -> Optional['CppCompileCommand']:
+ """Process a compile command.
+
+ At minimum, a compile command from a clang compilation database needs to
+ be correlated with its target, and this method returns the target name
+ with the compile command. But it also cleans up other things we need for
+ reliable code intelligence:
+
+ - Some targets may not be valid C/C++ compile commands. For example,
+ some build systems will naively include build steps for Python or for
+ linting commands. We want to filter those out.
+
+ - Some compile commands don't provide a path to the compiler executable
+ (referred to by clang as the "driver"). In that case, clangd is very
+ unlikely to find the executable unless it happens to be in ``$PATH``.
+ The ``--query-driver`` argument to ``clangd`` allowlists
+ executables/drivers for use its use, but clangd doesn't use it to
+ resolve ambiguous paths. We bridge that gap here. Any executable
+ without a path will be either placed in the provided default path or
+ searched for in the query driver globs and be replaced with a path to
+ the executable.
+ """
+ if self.command is None:
+ raise NotImplementedError(
+ 'Compile commands without \'command\' ' 'are not supported yet.'
+ )
+
+ executable_str, tokens = command_parts(self.command)
+ executable_path = path_to_executable(
+ executable_str,
+ default_path=default_path,
+ path_globs=path_globs,
+ strict=strict,
+ )
+
+ if executable_path is None or self.output is None:
+ return None
+
+ # TODO(chadnorvell): Some commands include the executable multiple
+ # times. It's not clear if that affects clangd.
+ new_command = f'{str(executable_path)} {" ".join(tokens)}'
+
+ return self.__class__(
+ file=self.file,
+ directory=self.directory,
+ command=new_command,
+ arguments=None,
+ output=self.output,
+ )
+
+ def as_dict(self) -> CppCompileCommandDict:
+ base_compile_command_dict: BaseCppCompileCommandDict = {
+ 'file': self.file,
+ 'directory': self.directory,
+ 'output': self.output,
+ }
+
+ # TODO(chadnorvell): Support "arguments". The spec requires that a
+ # We don't support "arguments" at all right now. When we do, we should
+ # preferentially include "arguments" only, and only include "command"
+ # when "arguments" is not present.
+ if self.command is not None:
+ compile_command_dict: CppCompileCommandDictWithCommand = {
+ 'command': self.command,
+ # Unfortunately dict spreading doesn't work with mypy.
+ 'file': base_compile_command_dict['file'],
+ 'directory': base_compile_command_dict['directory'],
+ 'output': base_compile_command_dict['output'],
+ }
+ else:
+ raise NotImplementedError(
+ 'Compile commands without \'command\' ' 'are not supported yet.'
+ )
+
+ return compile_command_dict
+
+
+def _infer_target_pos(target_glob: str) -> List[int]:
+ """Infer the position of the target in a compilation unit artifact path."""
+ tokens = Path(target_glob).parts
+ positions = []
+
+ for pos, token in enumerate(tokens):
+ if token == '?':
+ positions.append(pos)
+ elif token == '*':
+ pass
+ else:
+ raise ValueError(f'Invalid target inference token: {token}')
+
+ return positions
+
+
+def infer_target(
+ target_glob: str, root: Path, output_path: Path
+) -> Optional[str]:
+ """Infer a target from a compilation unit artifact path.
+
+ See the documentation for ``PigweedIdeSettings.target_inference``."""
+ target_pos = _infer_target_pos(target_glob)
+
+ if len(target_pos) == 0:
+ return None
+
+ # Depending on the build system and project configuration, the target name
+ # may be in the "directory" or the "output" of the compile command. So we
+ # need to construct the full path that combines both and use that to search
+ # for the target.
+ subpath = output_path.relative_to(root)
+ return '_'.join([subpath.parts[pos] for pos in target_pos])
+
+
+LoadableToCppCompilationDatabase = Union[
+ List[Dict[str, Any]], str, TextIOBase, Path
+]
+
+
+class CppCompilationDatabase:
+ """A representation of a clang compilation database.
+
+ See: https://clang.llvm.org/docs/JSONCompilationDatabase.html
+ """
+
+ def __init__(self, build_dir: Optional[Path] = None) -> None:
+ self._db: List[CppCompileCommand] = []
+
+ # Only compilation databases that are loaded will have this, and it
+ # contains the root directory of the build that the compilation
+ # database is based on. Processed compilation databases will not have
+ # a value here.
+ self._build_dir = build_dir
+
+ def __len__(self) -> int:
+ return len(self._db)
+
+ def __getitem__(self, index: int) -> CppCompileCommand:
+ return self._db[index]
+
+ def __iter__(self) -> Generator[CppCompileCommand, None, None]:
+ return (compile_command for compile_command in self._db)
+
+ def add(self, *commands: CppCompileCommand):
+ """Add compile commands to the compilation database."""
+ self._db.extend(commands)
+
+ def merge(self, other: 'CppCompilationDatabase') -> None:
+ """Merge values from another database into this one.
+
+ This will not overwrite a compile command that already exists for a
+ particular file.
+ """
+ self_dict = {c.file: c for c in self._db}
+
+ for compile_command in other:
+ if compile_command.file not in self_dict:
+ self_dict[compile_command.file] = compile_command
+
+ self._db = list(self_dict.values())
+
+ def as_dicts(self) -> List[CppCompileCommandDict]:
+ return [compile_command.as_dict() for compile_command in self._db]
+
+ def to_json(self) -> str:
+ """Output the compilation database to a JSON string."""
+
+ return json.dumps(self.as_dicts(), indent=2, sort_keys=True)
+
+ def to_file(self, path: Path):
+ """Write the compilation database to a JSON file."""
+
+ with open(path, 'w') as file:
+ json.dump(self.as_dicts(), file, indent=2, sort_keys=True)
+
+ @classmethod
+ def load(
+ cls, compdb_to_load: LoadableToCppCompilationDatabase, build_dir: Path
+ ) -> 'CppCompilationDatabase':
+ """Load a compilation database.
+
+ You can provide a JSON file handle or path, a JSON string, or a native
+ Python data structure that matches the format (list of dicts).
+ """
+ db_as_dicts: List[Dict[str, Any]]
+
+ if isinstance(compdb_to_load, list):
+ # The provided data is already in the format we want it to be in,
+ # probably, and if it isn't we'll find out when we try to
+ # instantiate the database.
+ db_as_dicts = compdb_to_load
+ else:
+ if isinstance(compdb_to_load, Path):
+ # The provided data is a path to a file, presumably JSON.
+ try:
+ compdb_data = compdb_to_load.read_text()
+ except FileNotFoundError:
+ raise MissingCompDbException()
+ elif isinstance(compdb_to_load, TextIOBase):
+ # The provided data is a file handle, presumably JSON.
+ compdb_data = compdb_to_load.read()
+ elif isinstance(compdb_to_load, str):
+ # The provided data is a a string, presumably JSON.
+ compdb_data = compdb_to_load
+
+ db_as_dicts = json.loads(compdb_data)
+
+ compdb = cls(build_dir=build_dir)
+
+ try:
+ compdb.add(
+ *[
+ compile_command
+ for compile_command_dict in db_as_dicts
+ if (
+ compile_command := CppCompileCommand.try_from_dict(
+ compile_command_dict
+ )
+ )
+ is not None
+ ]
+ )
+ except TypeError:
+ # This will arise if db_as_dicts is not actually a list of dicts
+ raise BadCompDbException()
+
+ return compdb
+
+ def process(
+ self,
+ settings: PigweedIdeSettings,
+ *,
+ default_path: Optional[Path] = None,
+ path_globs: Optional[List[str]] = None,
+ strict: bool = False,
+ ) -> 'CppCompilationDatabasesMap':
+ """Process a ``clangd`` compilation database file.
+
+ Given a clang compilation database that may have commands for multiple
+ valid or invalid targets/toolchains, keep only the valid compile
+ commands and store them in target-specific compilation databases.
+ """
+ if self._build_dir is None:
+ raise ValueError(
+ 'Can only process a compilation database that '
+ 'contains a root build directory, usually '
+ 'specified when loading the file. Are you '
+ 'trying to process an already-processed '
+ 'compilation database?'
+ )
+
+ clean_compdbs = CppCompilationDatabasesMap(settings)
+
+ for compile_command in self:
+ processed_command = compile_command.process(
+ default_path=default_path, path_globs=path_globs, strict=strict
+ )
+
+ if (
+ processed_command is not None
+ and processed_command.output_path is not None
+ ):
+ target = infer_target(
+ settings.target_inference,
+ self._build_dir,
+ processed_command.output_path,
+ )
+
+ if target_is_enabled(target, settings):
+ # This invariant is satisfied by target_is_enabled
+ target = cast(str, target)
+ processed_command.target = target
+ clean_compdbs[target].add(processed_command)
+
+ return clean_compdbs
+
+
+class CppCompilationDatabasesMap:
+ """Container for a map of target name to compilation database."""
+
+ def __init__(self, settings: PigweedIdeSettings):
+ self.settings = settings
+ self._dbs: Dict[str, CppCompilationDatabase] = defaultdict(
+ CppCompilationDatabase
+ )
+
+ def __len__(self) -> int:
+ return len(self._dbs)
+
+ def __getitem__(self, key: str) -> CppCompilationDatabase:
+ return self._dbs[key]
+
+ def __setitem__(self, key: str, item: CppCompilationDatabase) -> None:
+ self._dbs[key] = item
+
+ @property
+ def targets(self) -> List[str]:
+ return list(self._dbs.keys())
+
+ def items(
+ self,
+ ) -> Generator[Tuple[str, CppCompilationDatabase], None, None]:
+ return ((key, value) for (key, value) in self._dbs.items())
+
+ def write(self) -> None:
+ """Write compilation databases to target-specific JSON files."""
+ # This also writes out a file with the name of the target that has the
+ # largest number of commands, i.e., the target with the broadest
+ # compilation unit coverage. We can use this as a default target of
+ # last resort.
+ max_commands = 0
+ max_commands_target = None
+
+ for target, compdb in self.items():
+ if max_commands_target is None or len(compdb) > max_commands:
+ max_commands_target = target
+ max_commands = len(compdb)
+
+ compdb.to_file(
+ self.settings.working_dir / compdb_generate_file_path(target)
+ )
+
+ max_commands_target_path = (
+ self.settings.working_dir / MAX_COMMANDS_TARGET_FILENAME
+ )
+
+ if max_commands_target is not None:
+ if max_commands_target_path.exists():
+ max_commands_target_path.unlink()
+
+ with open(
+ max_commands_target_path, 'x'
+ ) as max_commands_target_file:
+ max_commands_target_file.write(max_commands_target)
+
+ @classmethod
+ def merge(
+ cls, *db_sets: 'CppCompilationDatabasesMap'
+ ) -> 'CppCompilationDatabasesMap':
+ """Merge several sets of processed compilation databases.
+
+ If you process N compilation databases produced by a build system,
+ you'll end up with N sets of processed compilation databases,
+ containing databases for one or more targets each. This method
+ merges them into one set of databases with one database per target.
+
+ The expectation is that the vast majority of the time, each of the
+ raw compilation databases that are processed will contain distinct
+ targets, meaning that the keys of each ``CppCompilationDatabases``
+ object that's merged will be unique to each object, and this operation
+ is nothing more than a shallow merge.
+
+ However, this also supports the case where targets may overlap between
+ ``CppCompilationDatabases`` objects. In that case, we prioritize
+ correctness, ensuring that the resulting compilation databases will
+ work correctly with clangd. This means not including duplicate compile
+ commands for the same file in the same target's database. The choice
+ of which duplicate compile command ends up in the final database is
+ unspecified and subject to change. Note also that this method expects
+ the ``settings`` value to be the same between all of the provided
+ ``CppCompilationDatabases`` objects.
+ """
+ if len(db_sets) == 0:
+ raise ValueError(
+ 'At least one set of compilation databases is ' 'required.'
+ )
+
+ # Shortcut for the most common case.
+ if len(db_sets) == 1:
+ return db_sets[0]
+
+ merged = cls(db_sets[0].settings)
+
+ for dbs in db_sets:
+ for target, db in dbs.items():
+ merged[target].merge(db)
+
+ return merged
+
+
+@dataclass(frozen=True)
+class CppIdeFeaturesTarget:
+ """Data pertaining to a C++ code analysis target."""
+
+ name: str
+ compdb_file_path: Path
+ compdb_cache_path: Optional[Path]
+ is_enabled: bool
+
+
+class CppIdeFeaturesState:
+ """The state of the C++ analysis targets in the working directory.
+
+ Targets can be:
+
+ - **Available**: A compilation database is present for this target.
+ - **Enabled**: Any targets are enabled by default, but a subset can be
+ enabled instead in the pw_ide settings. Enabled targets need
+ not be available if they haven't had a compilation database
+ created through processing yet.
+ - **Valid**: Is both available and enabled.
+ - **Current**: The one currently activated target that is exposed to clangd.
+ """
+
+ def __init__(self, settings: PigweedIdeSettings) -> None:
+ self.settings = settings
+
+ # We filter out Nones below, so we can assume its a str
+ target: Callable[[Path], str] = lambda path: cast(
+ str, compdb_target_from_path(path)
+ )
+
+ # Contains every compilation database that's present in the working dir.
+ # This dict comprehension looks monstrous, but it just finds targets and
+ # associates the target names with their CppIdeFeaturesTarget objects.
+ self.targets: Dict[str, CppIdeFeaturesTarget] = {
+ target(file_path): CppIdeFeaturesTarget(
+ name=target(file_path),
+ compdb_file_path=file_path,
+ compdb_cache_path=compdb_cache_path_if_exists(
+ settings.working_dir, compdb_target_from_path(file_path)
+ ),
+ is_enabled=target_is_enabled(target(file_path), settings),
+ )
+ for file_path in settings.working_dir.iterdir()
+ if file_path.match(
+ f'{_COMPDB_FILE_PREFIX}*{_COMPDB_FILE_EXTENSION}'
+ )
+ # This filters out the symlink
+ and compdb_target_from_path(file_path) is not None
+ }
+
+ # Contains the currently selected target.
+ self._current_target: Optional[CppIdeFeaturesTarget] = None
+
+ # This is diagnostic data; it tells us what the current target should
+ # be, even if the state of the working directory is corrupted and the
+ # compilation database for the target isn't actually present. Anything
+ # that requires a compilation database to be definitely present should
+ # use `current_target` instead of these values.
+ self.current_target_name: Optional[str] = None
+ self.current_target_file_path: Optional[Path] = None
+ self.current_target_exists: Optional[bool] = None
+
+ # Contains the name of the target that has the most compile commands,
+ # i.e., the target with the most file coverage in the project.
+ self._max_commands_target: Optional[str] = None
+
+ try:
+ src_file = Path(
+ os.readlink(
+ (settings.working_dir / compdb_generate_file_path())
+ )
+ )
+
+ self.current_target_file_path = src_file
+ self.current_target_name = compdb_target_from_path(src_file)
+
+ if not self.current_target_file_path.exists():
+ self.current_target_exists = False
+
+ else:
+ self.current_target_exists = True
+ self._current_target = CppIdeFeaturesTarget(
+ name=target(src_file),
+ compdb_file_path=src_file,
+ compdb_cache_path=compdb_cache_path_if_exists(
+ settings.working_dir, target(src_file)
+ ),
+ is_enabled=target_is_enabled(target(src_file), settings),
+ )
+ except (FileNotFoundError, OSError):
+ # If the symlink doesn't exist, there is no current target.
+ pass
+
+ try:
+ with open(
+ settings.working_dir / MAX_COMMANDS_TARGET_FILENAME
+ ) as max_commands_target_file:
+ self._max_commands_target = max_commands_target_file.readline()
+ except FileNotFoundError:
+ # If the file doesn't exist, a compilation database probably
+ # hasn't been processed yet.
+ pass
+
+ def __len__(self) -> int:
+ return len(self.targets)
+
+ def __getitem__(self, index: str) -> CppIdeFeaturesTarget:
+ return self.targets[index]
+
+ def __iter__(self) -> Generator[CppIdeFeaturesTarget, None, None]:
+ return (target for target in self.targets.values())
+
+ @property
+ def current_target(self) -> Optional[str]:
+ """The name of current target used for code analysis.
+
+ The presence of a symlink with the expected filename pointing to a
+ compilation database matching the expected filename format is the source
+ of truth on what the current target is.
+ """
+ return (
+ self._current_target.name
+ if self._current_target is not None
+ else None
+ )
+
+ @current_target.setter
+ def current_target(self, target: Optional[str]) -> None:
+ settings = self.settings
+
+ if not self.is_valid_target(target):
+ raise InvalidTargetException()
+
+ # The check above rules out None.
+ target = cast(str, target)
+
+ compdb_symlink_path = settings.working_dir / compdb_generate_file_path()
+
+ compdb_target_path = settings.working_dir / compdb_generate_file_path(
+ target
+ )
+
+ if not compdb_target_path.exists():
+ raise MissingCompDbException()
+
+ set_symlink(compdb_target_path, compdb_symlink_path)
+
+ cache_symlink_path = settings.working_dir / compdb_generate_cache_path()
+
+ cache_target_path = settings.working_dir / compdb_generate_cache_path(
+ target
+ )
+
+ if not cache_target_path.exists():
+ os.mkdir(cache_target_path)
+
+ set_symlink(cache_target_path, cache_symlink_path)
+
+ @property
+ def max_commands_target(self) -> Optional[str]:
+ """The target with the most compile commands.
+
+ The return value is the name of the target with the largest number of
+ compile commands (i.e., the largest coverage across the files in the
+ project). This can be a useful "default target of last resort".
+ """
+ return self._max_commands_target
+
+ @property
+ def available_targets(self) -> List[str]:
+ return list(self.targets.keys())
+
+ @property
+ def enabled_available_targets(self) -> Generator[str, None, None]:
+ return (
+ name for name, target in self.targets.items() if target.is_enabled
+ )
+
+ def is_valid_target(self, target: Optional[str]) -> bool:
+ if target is None or (data := self.targets.get(target, None)) is None:
+ return False
+
+ return data.is_enabled
+
+
+def aggregate_compilation_database_targets(
+ compdb_file: LoadableToCppCompilationDatabase,
+ settings: PigweedIdeSettings,
+ build_dir: Path,
+ *,
+ default_path: Optional[Path] = None,
+ path_globs: Optional[List[str]] = None,
+) -> List[str]:
+ """Return all valid unique targets from a ``clang`` compilation database."""
+ compdbs_map = CppCompilationDatabase.load(compdb_file, build_dir).process(
+ settings, default_path=default_path, path_globs=path_globs
+ )
+
+ return compdbs_map.targets
+
+
+def delete_compilation_databases(settings: PigweedIdeSettings) -> None:
+ """Delete all compilation databases in the working directory.
+
+ This leaves cache directories in place.
+ """
+ if settings.working_dir.exists():
+ for path in settings.working_dir.iterdir():
+ if path.name.startswith(_COMPDB_FILE_PREFIX):
+ try:
+ path.unlink()
+ except FileNotFoundError:
+ pass
+
+
+def delete_compilation_database_caches(settings: PigweedIdeSettings) -> None:
+ """Delete all compilation database caches in the working directory.
+
+ This leaves all compilation databases in place.
+ """
+ if settings.working_dir.exists():
+ for path in settings.working_dir.iterdir():
+ if path.name.startswith(_COMPDB_CACHE_DIR_PREFIX):
+ try:
+ path.unlink()
+ except FileNotFoundError:
+ pass
+
+
+class ClangdSettings:
+ """Makes system-specific settings for running ``clangd`` with Pigweed."""
+
+ def __init__(self, settings: PigweedIdeSettings):
+ self.compile_commands_dir: Path = PigweedIdeSettings().working_dir
+ self.clangd_path: Path = (
+ Path(PW_PIGWEED_CIPD_INSTALL_DIR) / 'bin' / 'clangd'
+ )
+
+ self.arguments: List[str] = [
+ f'--compile-commands-dir={self.compile_commands_dir}',
+ f'--query-driver={settings.clangd_query_driver_str()}',
+ '--background-index',
+ '--clang-tidy',
+ ]
+
+ def command(self, system: str = platform.system()) -> str:
+ """Return the command that runs clangd with Pigweed paths."""
+
+ def make_command(line_continuation: str):
+ arguments = f' {line_continuation}\n'.join(
+ f' {arg}' for arg in self.arguments
+ )
+ return f'\n{self.clangd_path} {line_continuation}\n{arguments}'
+
+ if system.lower() == 'json':
+ return '\n' + json.dumps(
+ [str(self.clangd_path), *self.arguments], indent=2
+ )
+
+ if system.lower() in ['cmd', 'batch']:
+ return make_command('`')
+
+ if system.lower() in ['powershell', 'pwsh']:
+ return make_command('^')
+
+ if system.lower() == 'windows':
+ return (
+ f'\nIn PowerShell:\n{make_command("`")}'
+ f'\n\nIn Command Prompt:\n{make_command("^")}'
+ )
+
+ # Default case for *sh-like shells.
+ return make_command('\\')
diff --git a/pw_ide/py/pw_ide/editors.py b/pw_ide/py/pw_ide/editors.py
new file mode 100644
index 000000000..f59e35346
--- /dev/null
+++ b/pw_ide/py/pw_ide/editors.py
@@ -0,0 +1,626 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Framework for configuring code editors for Pigweed projects.
+
+Editors and IDEs vary in the way they're configured and the options they
+provide for configuration. As long as an editor uses files we can parse to
+store its settings, this framework can be used to provide a consistent
+interface to managing those settings in the context of a Pigweed project.
+
+Ideally, we want to provide three levels of editor settings for a project:
+
+- User settings (specific to the user's checkout)
+- Project settings (included in source control, consistent for all users)
+- Default settings (defined by Pigweed)
+
+... where the settings on top can override (or cascade over) settings defined
+below.
+
+Some editors already provide mechanisms for achieving this, but in ways that
+are particular to that editor, and many other editors don't provide this
+mechanism at all. So we provide it in a uniform way here by adding a fourth
+settings level, active settings, which are the actual settings files the editor
+uses. Active settings are *built* (rather than edited or cloned) by looking for
+user, project, and default settings (which are defined by Pigweed and ignored
+by the editor) and combining them in the order described above. In this way,
+Pigweed can provide sensible defaults, projects can define additional settings
+to provide a uniform development experience, and users have the freedom to make
+their own changes.
+"""
+
+# TODO(chadnorvell): Import collections.OrderedDict when we don't need to
+# support Python 3.8 anymore.
+from collections import defaultdict
+from contextlib import contextmanager
+from dataclasses import dataclass
+import enum
+import json
+from pathlib import Path
+import time
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Generator,
+ Generic,
+ Literal,
+ Optional,
+ OrderedDict,
+ TypeVar,
+)
+
+import json5 # type: ignore
+
+from pw_ide.settings import PigweedIdeSettings
+
+
+class _StructuredFileFormat:
+ """Base class for structured settings file formats."""
+
+ @property
+ def ext(self) -> str:
+ return 'null'
+
+ def load(self, *args, **kwargs) -> OrderedDict:
+ raise ValueError(
+ f'Cannot load from file with {self.__class__.__name__}!'
+ )
+
+ def dump(self, data: OrderedDict, *args, **kwargs) -> None:
+ raise ValueError(f'Cannot dump to file with {self.__class__.__name__}!')
+
+
+class JsonFileFormat(_StructuredFileFormat):
+ """JSON file format."""
+
+ @property
+ def ext(self) -> str:
+ return 'json'
+
+ def load(self, *args, **kwargs) -> OrderedDict:
+ """Load JSON into an ordered dict."""
+ kwargs['object_pairs_hook'] = OrderedDict
+ return json.load(*args, **kwargs)
+
+ def dump(self, data: OrderedDict, *args, **kwargs) -> None:
+ """Dump JSON in a readable format."""
+ kwargs['indent'] = 2
+ json.dump(data, *args, **kwargs)
+
+
+class Json5FileFormat(_StructuredFileFormat):
+ """JSON5 file format.
+
+ Supports parsing files with comments and trailing commas.
+ """
+
+ @property
+ def ext(self) -> str:
+ return 'json'
+
+ def load(self, *args, **kwargs) -> OrderedDict:
+ """Load JSON into an ordered dict."""
+ kwargs['object_pairs_hook'] = OrderedDict
+ return json5.load(*args, **kwargs)
+
+ def dump(self, data: OrderedDict, *args, **kwargs) -> None:
+ """Dump JSON in a readable format."""
+ kwargs['indent'] = 2
+ kwargs['quote_keys'] = True
+ json5.dump(data, *args, **kwargs)
+
+
+# Allows constraining to dicts and dict subclasses, while also constraining to
+# the *same* dict subclass.
+_DictLike = TypeVar('_DictLike', bound=Dict)
+
+
+def dict_deep_merge(
+ src: _DictLike,
+ dest: _DictLike,
+ ctor: Optional[Callable[[], _DictLike]] = None,
+) -> _DictLike:
+ """Deep merge dict-like `src` into dict-like `dest`.
+
+ `dest` is mutated in place and also returned.
+
+ `src` and `dest` need to be the same subclass of dict. If they're anything
+ other than basic dicts, you need to also provide a constructor that returns
+ an empty dict of the same subclass.
+ """
+ # Ensure that src and dest are the same type of dict.
+ # These kinds of direct class comparisons are un-Pythonic, but the invariant
+ # here really is that they be exactly the same class, rather than "same" in
+ # the polymorphic sense.
+ if dest.__class__ != src.__class__:
+ raise TypeError(
+ 'Cannot merge dicts of different subclasses!\n'
+ f'src={src.__class__.__name__}, '
+ f'dest={dest.__class__.__name__}'
+ )
+
+ # If a constructor for this subclass wasn't provided, try using a
+ # zero-arg constructor for the provided dicts.
+ if ctor is None:
+ ctor = lambda: src.__class__() # pylint: disable=unnecessary-lambda
+
+ # Ensure that we have a way to construct an empty dict of the same type.
+ try:
+ empty_dict = ctor()
+ except TypeError:
+ # The constructor has required arguments.
+ raise TypeError(
+ 'When merging a dict subclass, you must provide a '
+ 'constructor for the subclass that produces an empty '
+ 'dict.\n'
+ f'src/dest={src.__class__.__name__}'
+ )
+
+ if empty_dict.__class__ != src.__class__:
+ # The constructor returns something of the wrong type.
+ raise TypeError(
+ 'When merging a dict subclass, you must provide a '
+ 'constructor for the subclass that produces an empty '
+ 'dict.\n'
+ f'src/dest={src.__class__.__name__}, '
+ f'constructor={ctor().__class__.__name__}'
+ )
+
+ for key, value in src.items():
+ empty_dict = ctor()
+ # The value is a nested dict; recursively merge.
+ if isinstance(value, src.__class__):
+ node = dest.setdefault(key, empty_dict)
+ dict_deep_merge(value, node, ctor)
+ # The value is something else; copy it over.
+ # TODO(chadnorvell): This doesn't deep merge other data structures, e.g.
+ # lists, lists of dicts, dicts of lists, etc.
+ else:
+ dest[key] = value
+
+ return dest
+
+
+# Editor settings are manipulated via this dict-like data structure. We use
+# OrderedDict to avoid non-deterministic changes to settings files and to make
+# diffs more readable. Note that the values here can't really be "Any". They
+# need to be JSON serializable, and any embedded dicts should also be
+# OrderedDicts.
+EditorSettingsDict = OrderedDict[str, Any]
+
+# A callback that provides default settings in dict form when given ``pw_ide``
+# settings (which may be ignored in many cases).
+DefaultSettingsCallback = Callable[[PigweedIdeSettings], EditorSettingsDict]
+
+
+class EditorSettingsDefinition:
+ """Provides access to a particular group of editor settings.
+
+ A particular editor may have one or more settings *types* (e.g., editor
+ settings vs. automated tasks settings, or separate settings files for
+ each supported language). ``pw_ide`` also supports multiple settings
+ *levels*, where the "active" settings are built from default, project,
+ and user settings. Each combination of settings type and level will have
+ one ``EditorSettingsDefinition``, which may be in memory (e.g., for default
+ settings defined in code) or may be backed by a file (see
+ ``EditorSettingsFile``).
+
+ Settings are accessed using the ``modify`` context manager, which provides
+ you a dict-like data structure to manipulate.
+
+ Initial settings can be provided in the constructor via a callback that
+ takes an instance of ``PigweedIdeSettings`` and returns a settings dict.
+ This allows the initial settings to be dependent on overall IDE features
+ settings.
+ """
+
+ def __init__(
+ self,
+ pw_ide_settings: Optional[PigweedIdeSettings] = None,
+ data: Optional[DefaultSettingsCallback] = None,
+ ):
+ self._data: EditorSettingsDict = OrderedDict()
+
+ if data is not None and pw_ide_settings is not None:
+ self._data = data(pw_ide_settings)
+
+ def __repr__(self) -> str:
+ return f'<{self.__class__.__name__}: (in memory)>'
+
+ def get(self) -> EditorSettingsDict:
+ """Return the settings as an ordered dict."""
+ return self._data
+
+ @contextmanager
+ def modify(self, reinit: bool = False):
+ """Modify a settings file via an ordered dict."""
+ if reinit:
+ new_data: OrderedDict[str, Any] = OrderedDict()
+ yield new_data
+ self._data = new_data
+ else:
+ yield self._data
+
+ def sync_to(self, settings: EditorSettingsDict) -> None:
+ """Merge this set of settings on top of the provided settings."""
+ self_settings = self.get()
+ settings = dict_deep_merge(self_settings, settings)
+
+ def is_present(self) -> bool: # pylint: disable=no-self-use
+ return True
+
+ def delete(self) -> None:
+ pass
+
+ def delete_backups(self) -> None:
+ pass
+
+
+class EditorSettingsFile(EditorSettingsDefinition):
+ """Provides access to an editor settings defintion stored in a file.
+
+ It's assumed that the editor's settings are stored in a file format that
+ can be deserialized to Python dicts. The settings are represented by
+ an ordered dict to make the diff that results from modifying the settings
+ as easy to read as possible (assuming it has a plain text representation).
+
+ This represents the concept of a file; the file may not actually be
+ present on disk yet.
+ """
+
+ def __init__(
+ self, settings_dir: Path, name: str, file_format: _StructuredFileFormat
+ ) -> None:
+ self._name = name
+ self._format = file_format
+ self._path = settings_dir / f'{name}.{self._format.ext}'
+ super().__init__()
+
+ def __repr__(self) -> str:
+ return f'<{self.__class__.__name__}: {str(self._path)}>'
+
+ def _backup_filename(self, glob=False):
+ timestamp = time.strftime('%Y%m%d_%H%M%S')
+ timestamp = '*' if glob else timestamp
+ backup_str = f'.{timestamp}.bak'
+ return f'{self._name}{backup_str}.{self._format.ext}'
+
+ def _make_backup(self) -> Path:
+ return self._path.replace(self._path.with_name(self._backup_filename()))
+
+ def _restore_backup(self, backup: Path) -> Path:
+ return backup.replace(self._path)
+
+ def get(self) -> EditorSettingsDict:
+ """Read a settings file into an ordered dict.
+
+ This does not keep the file context open, so while the dict is
+ mutable, any changes will not be written to disk.
+ """
+ try:
+ with self._path.open() as file:
+ settings: OrderedDict = self._format.load(file)
+ except FileNotFoundError:
+ settings = OrderedDict()
+
+ return settings
+
+ @contextmanager
+ def modify(self, reinit: bool = False):
+ """Modify a settings file via an ordered dict.
+
+ Get the dict when entering the context, then modify it like any
+ other dict, with the caveat that whatever goes into it needs to be
+ JSON-serializable. Example:
+
+ .. code-block:: python
+
+ with settings_file.modify() as settings:
+ settings[foo] = bar
+
+ After modifying the settings and leaving this context, the file will
+ be written. If the file already exists, a backup will be made. If a
+ failure occurs while writing the new file, it will be deleted and the
+ backup will be restored.
+
+ If the ``reinit`` argument is set, a new, empty file will be created
+ instead of modifying any existing file. If there is an existing file,
+ it will still be backed up.
+ """
+ if self._path.exists():
+ should_load_existing = True
+ should_backup = True
+ else:
+ should_load_existing = False
+ should_backup = False
+
+ if reinit:
+ should_load_existing = False
+
+ if should_load_existing:
+ with self._path.open() as file:
+ settings: OrderedDict = self._format.load(file)
+ else:
+ settings = OrderedDict()
+
+ prev_settings = settings.copy()
+
+ # TODO(chadnorvell): There's a subtle bug here where you can't assign
+ # to this var and have it take effect. You have to modify it in place.
+ # But you won't notice until things don't get written to disk.
+ yield settings
+
+ # If the settings haven't changed, don't create a backup.
+ if should_load_existing:
+ if settings == prev_settings:
+ should_backup = False
+
+ if should_backup:
+ # Move the current file to a new backup file. This frees the main
+ # file for open('x').
+ backup = self._make_backup()
+ else:
+ backup = None
+ # If the file exists and we didn't move it to a backup file, delete
+ # it so we can open('x') it again.
+ if self._path.exists():
+ self._path.unlink()
+
+ file = self._path.open('x')
+
+ try:
+ self._format.dump(settings, file)
+ except TypeError:
+ # We'll get this error if we try to sneak something in that's
+ # not JSON-serializable. Unless we handle this, we'll end up
+ # with a partially-written file that can't be parsed. So we
+ # delete that and restore the backup.
+ file.close()
+ self._path.unlink()
+
+ if backup is not None:
+ self._restore_backup(backup)
+
+ raise
+ finally:
+ if not file.closed:
+ file.close()
+
+ def is_present(self) -> bool:
+ return self._path.exists()
+
+ def delete(self) -> None:
+ try:
+ self._path.unlink()
+ except FileNotFoundError:
+ pass
+
+ def delete_backups(self) -> None:
+ glob = self._backup_filename(glob=True)
+
+ for path in self._path.glob(glob):
+ path.unlink()
+
+
+_SettingsLevelName = Literal['default', 'active', 'project', 'user']
+
+
+@dataclass(frozen=True)
+class SettingsLevelData:
+ name: _SettingsLevelName
+ is_user_configurable: bool
+ is_file: bool
+
+
+class SettingsLevel(enum.Enum):
+ """Cascading set of settings.
+
+ This provides a unified mechanism for having active settings (those
+ actually used by an editor) be built from default settings in Pigweed,
+ project settings checked into the project's repository, and user settings
+ particular to one checkout of the project, each of which can override
+ settings higher up in the chain.
+ """
+
+ DEFAULT = SettingsLevelData(
+ 'default', is_user_configurable=False, is_file=False
+ )
+ PROJECT = SettingsLevelData(
+ 'project', is_user_configurable=True, is_file=True
+ )
+ USER = SettingsLevelData('user', is_user_configurable=True, is_file=True)
+ ACTIVE = SettingsLevelData(
+ 'active', is_user_configurable=False, is_file=True
+ )
+
+ @property
+ def is_user_configurable(self) -> bool:
+ return self.value.is_user_configurable
+
+ @property
+ def is_file(self) -> bool:
+ return self.value.is_file
+
+ @classmethod
+ def all_levels(cls) -> Generator['SettingsLevel', None, None]:
+ return (level for level in cls)
+
+ @classmethod
+ def all_not_default(cls) -> Generator['SettingsLevel', None, None]:
+ return (level for level in cls if level is not cls.DEFAULT)
+
+ @classmethod
+ def all_user_configurable(cls) -> Generator['SettingsLevel', None, None]:
+ return (level for level in cls if level.is_user_configurable)
+
+ @classmethod
+ def all_files(cls) -> Generator['SettingsLevel', None, None]:
+ return (level for level in cls if level.is_file)
+
+
+# A map of configurable settings levels and the string that will be prepended
+# to their files to indicate their settings level.
+SettingsFilePrefixes = Dict[SettingsLevel, str]
+
+# Each editor will have one or more settings types that typically reflect each
+# of the files used to define their settings. So each editor should have an
+# enum type that defines each of those settings types, and this type var
+# represents that generically. The value of each enum case should be the file
+# name of that settings file, without the extension.
+# TODO(chadnorvell): Would be great to constrain this to enums, but bound=
+# doesn't do what we want with Enum or EnumMeta.
+_SettingsTypeT = TypeVar('_SettingsTypeT')
+
+# Maps each settings type with the callback that generates the default settings
+# for that settings type.
+EditorSettingsTypesWithDefaults = Dict[_SettingsTypeT, DefaultSettingsCallback]
+
+
+class EditorSettingsManager(Generic[_SettingsTypeT]):
+ """Manages all settings for a particular editor.
+
+ This is where you interact with an editor's settings (actually in a
+ subclass of this class, not here). Initializing this class sets up access
+ to one or more settings files for an editor (determined by
+ ``_SettingsTypeT``, fulfilled by an enum that defines each of an editor's
+ settings files), along with the cascading settings levels.
+ """
+
+ # Prefixes should only be defined for settings that will be stored on disk
+ # and are not the active settings file, which will use the name without a
+ # prefix. This may be overridden in child classes, but typically should
+ # not be.
+ prefixes: SettingsFilePrefixes = {
+ SettingsLevel.PROJECT: 'pw_project_',
+ SettingsLevel.USER: 'pw_user_',
+ }
+
+ # These must be overridden in child classes.
+ default_settings_dir: Path = None # type: ignore
+ file_format: _StructuredFileFormat = _StructuredFileFormat()
+ types_with_defaults: EditorSettingsTypesWithDefaults[_SettingsTypeT] = {}
+
+ def __init__(
+ self,
+ pw_ide_settings: PigweedIdeSettings,
+ settings_dir: Optional[Path] = None,
+ file_format: Optional[_StructuredFileFormat] = None,
+ types_with_defaults: Optional[
+ EditorSettingsTypesWithDefaults[_SettingsTypeT]
+ ] = None,
+ ):
+ if SettingsLevel.ACTIVE in self.__class__.prefixes:
+ raise ValueError(
+ 'You cannot assign a file name prefix to '
+ 'an active settings file.'
+ )
+
+ # This lets us use ``self._prefixes`` transparently for any file,
+ # including active settings files, since it will provide an empty
+ # string prefix for those files. In other words, while the class
+ # attribute `prefixes` can only be defined for configurable settings,
+ # `self._prefixes` extends it to work for any settings file.
+ self._prefixes = defaultdict(str, self.__class__.prefixes)
+
+ # The default settings directory is defined by the subclass attribute
+ # `default_settings_dir`, and that value is used the vast majority of
+ # the time. But you can inject an alternative directory in the
+ # constructor if needed (e.g. for tests).
+ self._settings_dir = (
+ settings_dir
+ if settings_dir is not None
+ else self.__class__.default_settings_dir
+ )
+
+ # The backing file format should normally be defined by the class
+ # attribute ``file_format``, but can be overridden in the constructor.
+ self._file_format: _StructuredFileFormat = (
+ file_format
+ if file_format is not None
+ else self.__class__.file_format
+ )
+
+ # The settings types with their defaults should normally be defined by
+ # the class attribute ``types_with_defaults``, but can be overridden
+ # in the constructor.
+ self._types_with_defaults = (
+ types_with_defaults
+ if types_with_defaults is not None
+ else self.__class__.types_with_defaults
+ )
+
+ # For each of the settings levels, there is a settings definition for
+ # each settings type. Those settings definitions may be stored in files
+ # or not.
+ self._settings_definitions: Dict[
+ SettingsLevel, Dict[_SettingsTypeT, EditorSettingsDefinition]
+ ] = {}
+
+ self._settings_types = tuple(self._types_with_defaults.keys())
+
+ # Initialize the default settings level for each settings type, which
+ # defined in code, not files.
+ self._settings_definitions[SettingsLevel.DEFAULT] = {}
+
+ for (
+ settings_type
+ ) in (
+ self._types_with_defaults
+ ): # pylint: disable=consider-using-dict-items
+ self._settings_definitions[SettingsLevel.DEFAULT][
+ settings_type
+ ] = EditorSettingsDefinition(
+ pw_ide_settings, self._types_with_defaults[settings_type]
+ )
+
+ # Initialize the settings definitions for each settings type for each
+ # settings level that's stored on disk.
+ for level in SettingsLevel.all_files():
+ self._settings_definitions[level] = {}
+
+ for settings_type in self._types_with_defaults:
+ name = f'{self._prefixes[level]}{settings_type.value}'
+ self._settings_definitions[level][
+ settings_type
+ ] = EditorSettingsFile(
+ self._settings_dir, name, self._file_format
+ )
+
+ def default(self, settings_type: _SettingsTypeT):
+ """Default settings for the provided settings type."""
+ return self._settings_definitions[SettingsLevel.DEFAULT][settings_type]
+
+ def project(self, settings_type: _SettingsTypeT):
+ """Project settings for the provided settings type."""
+ return self._settings_definitions[SettingsLevel.PROJECT][settings_type]
+
+ def user(self, settings_type: _SettingsTypeT):
+ """User settings for the provided settings type."""
+ return self._settings_definitions[SettingsLevel.USER][settings_type]
+
+ def active(self, settings_type: _SettingsTypeT):
+ """Active settings for the provided settings type."""
+ return self._settings_definitions[SettingsLevel.ACTIVE][settings_type]
+
+ def delete_all_active_settings(self) -> None:
+ """Delete all active settings files."""
+ for settings_type in self._settings_types:
+ self.active(settings_type).delete()
+
+ def delete_all_backups(self) -> None:
+ """Delete all backup files."""
+ for settings_type in self._settings_types:
+ self.project(settings_type).delete_backups()
+ self.user(settings_type).delete_backups()
+ self.active(settings_type).delete_backups()
diff --git a/pw_ide/py/pw_ide/exceptions.py b/pw_ide/py/pw_ide/exceptions.py
new file mode 100644
index 000000000..23e127dc2
--- /dev/null
+++ b/pw_ide/py/pw_ide/exceptions.py
@@ -0,0 +1,34 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_ide exceptions."""
+
+
+class UnsupportedPlatformException(Exception):
+ """Raised when an action is attempted on an unsupported platform."""
+
+
+class InvalidTargetException(Exception):
+ """Exception for invalid compilation targets."""
+
+
+class BadCompDbException(Exception):
+ """Exception for compliation databases that don't conform to the format."""
+
+
+class MissingCompDbException(Exception):
+ """Exception for missing compilation database files."""
+
+
+class UnresolvablePathException(Exception):
+ """Raised when an ambiguous path cannot be resolved."""
diff --git a/pw_ide/py/pw_ide/py.typed b/pw_ide/py/pw_ide/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_ide/py/pw_ide/py.typed
diff --git a/pw_ide/py/pw_ide/python.py b/pw_ide/py/pw_ide/python.py
new file mode 100644
index 000000000..80dfd2b1a
--- /dev/null
+++ b/pw_ide/py/pw_ide/python.py
@@ -0,0 +1,53 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Configure Python IDE support for Pigweed projects."""
+
+from collections import defaultdict
+import os
+from pathlib import Path
+import platform
+from typing import Dict, NamedTuple
+
+_PYTHON_VENV_PATH = (
+ Path(os.path.expandvars('$_PW_ACTUAL_ENVIRONMENT_ROOT')) / 'pigweed-venv'
+)
+
+
+class _PythonPathsForPlatform(NamedTuple):
+ bin_dir_name: str = 'bin'
+ interpreter_name: str = 'python3'
+
+
+# When given a platform (e.g. the output of platform.system()), this dict gives
+# the platform-specific virtualenv path names.
+_PYTHON_PATHS_FOR_PLATFORM: Dict[str, _PythonPathsForPlatform] = defaultdict(
+ _PythonPathsForPlatform
+)
+_PYTHON_PATHS_FOR_PLATFORM['Windows'] = _PythonPathsForPlatform(
+ bin_dir_name='Scripts', interpreter_name='pythonw.exe'
+)
+
+
+class PythonPaths:
+ """Holds the platform-specific Python environment paths.
+
+ The directory layout of Python virtual environments varies among
+ platforms. This class holds the data needed to find the right paths
+ for a specific platform.
+ """
+
+ def __init__(self, system=platform.system()):
+ (bin_dir_name, interpreter_name) = _PYTHON_PATHS_FOR_PLATFORM[system]
+ self.bin_dir = _PYTHON_VENV_PATH / bin_dir_name
+ self.interpreter = self.bin_dir / interpreter_name
diff --git a/pw_ide/py/pw_ide/settings.py b/pw_ide/py/pw_ide/settings.py
new file mode 100644
index 000000000..549bf8c8f
--- /dev/null
+++ b/pw_ide/py/pw_ide/settings.py
@@ -0,0 +1,323 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_ide settings."""
+
+import enum
+from inspect import cleandoc
+import glob
+import os
+from pathlib import Path
+from typing import Any, cast, Dict, List, Literal, Optional, Union
+import yaml
+
+from pw_cli.yaml_config_loader_mixin import YamlConfigLoaderMixin
+
+PW_IDE_DIR_NAME = '.pw_ide'
+PW_IDE_DEFAULT_DIR = (
+ Path(os.path.expandvars('$PW_PROJECT_ROOT')) / PW_IDE_DIR_NAME
+)
+
+PW_PIGWEED_CIPD_INSTALL_DIR = Path(
+ os.path.expandvars('$PW_PIGWEED_CIPD_INSTALL_DIR')
+)
+
+PW_ARM_CIPD_INSTALL_DIR = Path(os.path.expandvars('$PW_ARM_CIPD_INSTALL_DIR'))
+
+_DEFAULT_BUILD_DIR_NAME = 'out'
+_DEFAULT_BUILD_DIR = (
+ Path(os.path.expandvars('$PW_PROJECT_ROOT')) / _DEFAULT_BUILD_DIR_NAME
+)
+
+_DEFAULT_COMPDB_PATHS = [_DEFAULT_BUILD_DIR]
+_DEFAULT_TARGET_INFERENCE = '?'
+
+SupportedEditorName = Literal['vscode']
+
+
+class SupportedEditor(enum.Enum):
+ VSCODE = 'vscode'
+
+
+_DEFAULT_SUPPORTED_EDITORS: Dict[SupportedEditorName, bool] = {
+ 'vscode': True,
+}
+
+_DEFAULT_CONFIG: Dict[str, Any] = {
+ 'clangd_additional_query_drivers': [],
+ 'build_dir': _DEFAULT_BUILD_DIR,
+ 'compdb_paths': _DEFAULT_BUILD_DIR_NAME,
+ 'default_target': None,
+ 'editors': _DEFAULT_SUPPORTED_EDITORS,
+ 'setup': ['pw --no-banner ide cpp --gn --set-default --no-override'],
+ 'targets': [],
+ 'target_inference': _DEFAULT_TARGET_INFERENCE,
+ 'working_dir': PW_IDE_DEFAULT_DIR,
+}
+
+_DEFAULT_PROJECT_FILE = Path('$PW_PROJECT_ROOT/.pw_ide.yaml')
+_DEFAULT_PROJECT_USER_FILE = Path('$PW_PROJECT_ROOT/.pw_ide.user.yaml')
+_DEFAULT_USER_FILE = Path('$HOME/.pw_ide.yaml')
+
+
+class PigweedIdeSettings(YamlConfigLoaderMixin):
+ """Pigweed IDE features settings storage class."""
+
+ def __init__(
+ self,
+ project_file: Union[Path, bool] = _DEFAULT_PROJECT_FILE,
+ project_user_file: Union[Path, bool] = _DEFAULT_PROJECT_USER_FILE,
+ user_file: Union[Path, bool] = _DEFAULT_USER_FILE,
+ default_config: Optional[Dict[str, Any]] = None,
+ ) -> None:
+ self.config_init(
+ config_section_title='pw_ide',
+ project_file=project_file,
+ project_user_file=project_user_file,
+ user_file=user_file,
+ default_config=_DEFAULT_CONFIG
+ if default_config is None
+ else default_config,
+ environment_var='PW_IDE_CONFIG_FILE',
+ )
+
+ @property
+ def working_dir(self) -> Path:
+ """Path to the ``pw_ide`` working directory.
+
+ The working directory holds C++ compilation databases and caches, and
+ other supporting files. This should not be a directory that's regularly
+ deleted or manipulated by other processes (e.g. the GN ``out``
+ directory) nor should it be committed to source control.
+ """
+ return Path(self._config.get('working_dir', PW_IDE_DEFAULT_DIR))
+
+ @property
+ def build_dir(self) -> Path:
+ """The build system's root output directory.
+
+ We will use this as the output directory when automatically running
+ build system commands, and will use it to resolve target names using
+ target name inference when processing compilation databases. This can
+ be the same build directory used for general-purpose builds, but it
+ does not have to be.
+ """
+ return Path(self._config.get('build_dir', _DEFAULT_BUILD_DIR))
+
+ @property
+ def compdb_paths(self) -> str:
+ """A path glob to search for compilation databases.
+
+ These paths can be to files or to directories. Paths that are
+ directories will be appended with the default file name for
+ ``clangd`` compilation databases, ``compile_commands.json``.
+ """
+ return self._config.get('compdb_paths', _DEFAULT_BUILD_DIR_NAME)
+
+ @property
+ def compdb_paths_expanded(self) -> List[Path]:
+ return [Path(node) for node in glob.iglob(self.compdb_paths)]
+
+ @property
+ def targets(self) -> List[str]:
+ """The list of targets that should be enabled for code analysis.
+
+ In this case, "target" is analogous to a GN target, i.e., a particular
+ build configuration. By default, all available targets are enabled. By
+ adding targets to this list, you can constrain the targets that are
+ enabled for code analysis to a subset of those that are available, which
+ may be useful if your project has many similar targets that are
+ redundant from a code analysis perspective.
+
+ Target names need to match the name of the directory that holds the
+ build system artifacts for the target. For example, GN outputs build
+ artifacts for the ``pw_strict_host_clang_debug`` target in a directory
+ with that name in its output directory. So that becomes the canonical
+ name for the target.
+ """
+ return self._config.get('targets', list())
+
+ @property
+ def target_inference(self) -> str:
+ """A glob-like string for extracting a target name from an output path.
+
+ Build systems and projects have varying ways of organizing their build
+ directory structure. For a given compilation unit, we need to know how
+ to extract the build's target name from the build artifact path. A
+ simple example:
+
+ .. code-block:: none
+
+ clang++ hello.cc -o host/obj/hello.cc.o
+
+ The top-level directory ``host`` is the target name we want. The same
+ compilation unit might be used with another build target:
+
+ .. code-block:: none
+
+ gcc-arm-none-eabi hello.cc -o arm_dev_board/obj/hello.cc.o
+
+ In this case, this compile command is associated with the
+ ``arm_dev_board`` target.
+
+ When importing and processing a compilation database, we assume by
+ default that for each compile command, the corresponding target name is
+ the name of the top level directory within the build directory root
+ that contains the build artifact. This is the default behavior for most
+ build systems. However, if your project is structured differently, you
+ can provide a glob-like string that indicates how to extract the target
+ name from build artifact path.
+
+ A ``*`` indicates any directory, and ``?`` indicates the directory that
+ has the name of the target. The path is resolved from the build
+ directory root, and anything deeper than the target directory is
+ ignored. For example, a glob indicating that the directory two levels
+ down from the build directory root has the target name would be
+ expressed with ``*/*/?``.
+ """
+ return self._config.get('target_inference', _DEFAULT_TARGET_INFERENCE)
+
+ @property
+ def default_target(self) -> Optional[str]:
+ """The default target to use when calling ``--set-default``.
+
+ This target will be selected when ``pw ide cpp --set-default`` is
+ called. You can define an explicit default target here. If that command
+ is invoked without a default target definition, ``pw_ide`` will try to
+ infer the best choice of default target. Currently, it selects the
+ target with the broadest compilation unit coverage.
+ """
+ return self._config.get('default_target', None)
+
+ @property
+ def setup(self) -> List[str]:
+ """A sequence of commands to automate IDE features setup.
+
+ ``pw ide setup`` should do everything necessary to get the project from
+ a fresh checkout to a working default IDE experience. This defines the
+ list of commands that makes that happen, which will be executed
+ sequentially in subprocesses. These commands should be idempotent, so
+ that the user can run them at any time to update their IDE features
+ configuration without the risk of putting those features in a bad or
+ unexpected state.
+ """
+ return self._config.get('setup', list())
+
+ @property
+ def clangd_additional_query_drivers(self) -> List[str]:
+ """Additional query driver paths that clangd should use.
+
+ By default, ``pw_ide`` supplies driver paths for the toolchains included
+ in Pigweed. If you are using toolchains that are not supplied by
+ Pigweed, you should include path globs to your toolchains here. These
+ paths will be given higher priority than the Pigweed toolchain paths.
+ """
+ return self._config.get('clangd_additional_query_drivers', list())
+
+ def clangd_query_drivers(self) -> List[str]:
+ return [
+ *[str(Path(p)) for p in self.clangd_additional_query_drivers],
+ str(PW_PIGWEED_CIPD_INSTALL_DIR / 'bin' / '*'),
+ str(PW_ARM_CIPD_INSTALL_DIR / 'bin' / '*'),
+ ]
+
+ def clangd_query_driver_str(self) -> str:
+ return ','.join(self.clangd_query_drivers())
+
+ @property
+ def editors(self) -> Dict[str, bool]:
+ """Enable or disable automated support for editors.
+
+ Automatic support for some editors is provided by ``pw_ide``, which is
+ accomplished through generating configuration files in your project
+ directory. All supported editors are enabled by default, but you can
+ disable editors by adding an ``'<editor>': false`` entry.
+ """
+ return self._config.get('editors', _DEFAULT_SUPPORTED_EDITORS)
+
+ def editor_enabled(self, editor: SupportedEditorName) -> bool:
+ """True if the provided editor is enabled in settings.
+
+ This module will integrate the project with all supported editors by
+ default. If the project or user want to disable particular editors,
+ they can do so in the appropriate settings file.
+ """
+ return self._config.get('editors', {}).get(editor, False)
+
+
+def _docstring_set_default(
+ obj: Any, default: Any, literal: bool = False
+) -> None:
+ """Add a default value annotation to a docstring.
+
+ Formatting isn't allowed in docstrings, so by default we can't inject
+ variables that we would like to appear in the documentation, like the
+ default value of a property. But we can use this function to add it
+ separately.
+ """
+ if obj.__doc__ is not None:
+ default = str(default)
+
+ if literal:
+ lines = default.splitlines()
+
+ if len(lines) == 0:
+ return
+ if len(lines) == 1:
+ default = f'Default: ``{lines[0]}``'
+ else:
+ default = 'Default:\n\n.. code-block::\n\n ' + '\n '.join(
+ lines
+ )
+
+ doc = cast(str, obj.__doc__)
+ obj.__doc__ = f'{cleandoc(doc)}\n\n{default}'
+
+
+_docstring_set_default(
+ PigweedIdeSettings.working_dir, PW_IDE_DIR_NAME, literal=True
+)
+_docstring_set_default(
+ PigweedIdeSettings.build_dir, _DEFAULT_BUILD_DIR_NAME, literal=True
+)
+_docstring_set_default(
+ PigweedIdeSettings.compdb_paths,
+ _DEFAULT_CONFIG['compdb_paths'],
+ literal=True,
+)
+_docstring_set_default(
+ PigweedIdeSettings.targets, _DEFAULT_CONFIG['targets'], literal=True
+)
+_docstring_set_default(
+ PigweedIdeSettings.default_target,
+ _DEFAULT_CONFIG['default_target'],
+ literal=True,
+)
+_docstring_set_default(
+ PigweedIdeSettings.target_inference,
+ _DEFAULT_CONFIG['target_inference'],
+ literal=True,
+)
+_docstring_set_default(
+ PigweedIdeSettings.setup, _DEFAULT_CONFIG['setup'], literal=True
+)
+_docstring_set_default(
+ PigweedIdeSettings.clangd_additional_query_drivers,
+ _DEFAULT_CONFIG['clangd_additional_query_drivers'],
+ literal=True,
+)
+_docstring_set_default(
+ PigweedIdeSettings.editors,
+ yaml.dump(_DEFAULT_SUPPORTED_EDITORS),
+ literal=True,
+)
diff --git a/pw_ide/py/pw_ide/symlinks.py b/pw_ide/py/pw_ide/symlinks.py
new file mode 100644
index 000000000..1799ea9f4
--- /dev/null
+++ b/pw_ide/py/pw_ide/symlinks.py
@@ -0,0 +1,24 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tools for managing symlinks."""
+
+import os
+from pathlib import Path
+
+
+def set_symlink(target_path: Path, symlink_path: Path) -> None:
+ if symlink_path.exists():
+ os.remove(symlink_path)
+
+ os.symlink(target_path, symlink_path, target_path.is_dir())
diff --git a/pw_ide/py/pw_ide/vscode.py b/pw_ide/py/pw_ide/vscode.py
new file mode 100644
index 000000000..c564fe0af
--- /dev/null
+++ b/pw_ide/py/pw_ide/vscode.py
@@ -0,0 +1,337 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Configure Visual Studio Code (VSC) for Pigweed projects.
+
+VSC recognizes three sources of configurable settings:
+
+1. Project settings, stored in {project root}/.vscode/settings.json
+2. Workspace settings, stored in (workspace root)/.vscode/settings.json;
+ a workspace is a collection of projects/repositories that are worked on
+ together in a single VSC instance
+3. The user's personal settings, which are stored somewhere in the user's home
+ directory, and are applied to all instances of VSC
+
+This provides three levels of settings cascading:
+
+ Workspace <- Project <- User
+
+... where the arrow indicates the ability to override.
+
+Out of these three, only project settings are useful to Pigweed projects. User
+settings are essentially global and outside the scope of Pigweed. Workspaces are
+seldom used and don't work well with the Pigweed directory structure.
+
+Nonetheless, we want a three-tiered settings structure for Pigweed projects too:
+
+A. Default settings provided by Pigweed, configuring VSC to use IDE features
+B. Project-level overrides that downstream projects may define
+C. User-level overrides that individual users may define
+
+We accomplish all of that with only the project settings described in #1 above.
+
+Default settings are defined in this module. Project settings can be defined in
+.vscode/pw_project_settings.json and should be checked into the repository. User
+settings can be defined in .vscode/pw_user_settings.json and should not be
+checked into the repository. None of these settings have any effect until they
+are merged into VSC's settings (.vscode/settings.json) via the functions in this
+module. Those resulting settings are system-specific and should also not be
+checked into the repository.
+
+We provide the same structure to both tasks and extensions as well. Defaults
+are provided by Pigweed, can be augmented or overridden at the project level
+with .vscode/pw_project_tasks.json and .vscode/pw_project_extensions.json,
+can be augmented or overridden by an individual developer with
+.vscode/pw_user_tasks.json and .vscode/pw_user.extensions.json, and none of
+this takes effect until they are merged into VSC's active settings files
+(.vscode/tasks.json and .vscode/extensions.json) by running the appropriate
+command.
+"""
+
+# TODO(chadnorvell): Import collections.OrderedDict when we don't need to
+# support Python 3.8 anymore.
+from enum import Enum
+import json
+import os
+from pathlib import Path
+import platform
+from typing import Any, Dict, List, OrderedDict
+
+from pw_ide.activate import BashShellModifier
+from pw_ide.cpp import ClangdSettings
+
+from pw_ide.editors import (
+ EditorSettingsDict,
+ EditorSettingsManager,
+ EditorSettingsTypesWithDefaults,
+ Json5FileFormat,
+)
+
+from pw_ide.python import PythonPaths
+from pw_ide.settings import PigweedIdeSettings
+
+
+def _vsc_os(system: str = platform.system()):
+ """Return the OS tag that VSC expects."""
+ if system == 'Darwin':
+ return 'osx'
+
+ return system.lower()
+
+
+def _activated_env() -> OrderedDict[str, Any]:
+ """Return the environment diff needed to provide Pigweed activation.
+
+ The integrated terminal will already include the user's default environment
+ (e.g. from their shell init scripts). This provides the modifications to
+ the environment needed for Pigweed activation.
+ """
+ # Not all environments have an actions.json, which this ultimately relies
+ # on (e.g. tests in CI). No problem, just return an empty dict instead.
+ try:
+ env = (
+ BashShellModifier(env_only=True, path_var='${env:PATH}')
+ .modify_env()
+ .env_mod
+ )
+ except (FileNotFoundError, json.JSONDecodeError):
+ env = dict()
+
+ return OrderedDict(env)
+
+
+def _local_terminal_integrated_env() -> Dict[str, Any]:
+ """VSC setting to activate the integrated terminal."""
+ return {f'terminal.integrated.env.{_vsc_os()}': _activated_env()}
+
+
+def _local_clangd_settings(ide_settings: PigweedIdeSettings) -> Dict[str, Any]:
+ """VSC settings for running clangd with Pigweed paths."""
+ clangd_settings = ClangdSettings(ide_settings)
+ return {
+ 'clangd.path': str(clangd_settings.clangd_path),
+ 'clangd.arguments': clangd_settings.arguments,
+ }
+
+
+def _local_python_settings() -> Dict[str, Any]:
+ """VSC settings for finding the Python virtualenv."""
+ paths = PythonPaths()
+ return {
+ 'python.defaultInterpreterPath': str(paths.interpreter),
+ 'python.formatting.yapfPath': str(paths.bin_dir / 'yapf'),
+ }
+
+
+# The order is preserved despite starting with a plain dict because in Python
+# 3.6+, plain dicts are actually ordered as an implementation detail. This could
+# break on interpreters other than CPython, or if the implementation changes in
+# the future. However, for now, this is much more readable than the more robust
+# alternative of instantiating with a list of tuples.
+_DEFAULT_SETTINGS: EditorSettingsDict = OrderedDict(
+ {
+ "editor.detectIndentation": False,
+ "editor.rulers": [80],
+ "editor.tabSize": 2,
+ "files.associations": OrderedDict({"*.inc": "cpp"}),
+ "files.exclude": OrderedDict(
+ {
+ "**/*.egg-info": True,
+ "**/.mypy_cache": True,
+ "**/__pycache__": True,
+ ".cache": True,
+ ".cipd": True,
+ ".environment": True,
+ ".presubmit": True,
+ ".pw_ide": True,
+ ".pw_ide.user.yaml": True,
+ "bazel-bin": True,
+ "bazel-out": True,
+ "bazel-pigweed": True,
+ "bazel-testlogs": True,
+ "build": True,
+ "environment": True,
+ "node_modules": True,
+ "out": True,
+ }
+ ),
+ "files.insertFinalNewline": True,
+ "files.trimTrailingWhitespace": True,
+ "search.useGlobalIgnoreFiles": True,
+ "grunt.autoDetect": "off",
+ "gulp.autoDetect": "off",
+ "jake.autoDetect": "off",
+ "npm.autoDetect": "off",
+ "clangd.onConfigChanged": "restart",
+ "C_Cpp.intelliSenseEngine": "Disabled",
+ "[cpp]": OrderedDict(
+ {"editor.defaultFormatter": "llvm-vs-code-extensions.vscode-clangd"}
+ ),
+ "python.formatting.provider": "yapf",
+ "[python]": OrderedDict({"editor.tabSize": 4}),
+ "typescript.tsc.autoDetect": "off",
+ "[gn]": OrderedDict({"editor.defaultFormatter": "msedge-dev.gnls"}),
+ "[proto3]": OrderedDict(
+ {"editor.defaultFormatter": "zxh404.vscode-proto3"}
+ ),
+ }
+)
+
+# pylint: disable=line-too-long
+_DEFAULT_TASKS: EditorSettingsDict = OrderedDict(
+ {
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "shell",
+ "label": "Pigweed IDE: Format",
+ "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw format --fix'",
+ "problemMatcher": [],
+ },
+ {
+ "type": "shell",
+ "label": "Pigweed IDE: Presubmit",
+ "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw presubmit'",
+ "problemMatcher": [],
+ },
+ {
+ "label": "Pigweed IDE: Set Python Virtual Environment",
+ "command": "${command:python.setInterpreter}",
+ "problemMatcher": [],
+ },
+ {
+ "label": "Pigweed IDE: Restart Python Language Server",
+ "command": "${command:python.analysis.restartLanguageServer}",
+ "problemMatcher": [],
+ },
+ {
+ "label": "Pigweed IDE: Restart C++ Language Server",
+ "command": "${command:clangd.restart}",
+ "problemMatcher": [],
+ },
+ {
+ "type": "shell",
+ "label": "Pigweed IDE: Process C++ Compilation Database from GN",
+ "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --gn --process out/compile_commands.json'",
+ "problemMatcher": [],
+ },
+ {
+ "type": "shell",
+ "label": "Pigweed IDE: Setup",
+ "command": "python3 ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide setup'",
+ "problemMatcher": [],
+ },
+ {
+ "type": "shell",
+ "label": "Pigweed IDE: Current C++ Code Analysis Target",
+ "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp'",
+ "problemMatcher": [],
+ },
+ {
+ "type": "shell",
+ "label": "Pigweed IDE: List C++ Code Analysis Targets",
+ "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --list'",
+ "problemMatcher": [],
+ },
+ {
+ "type": "shell",
+ "label": "Pigweed IDE: Set C++ Code Analysis Target",
+ "command": "${workspaceFolder}/.pw_ide/python ${workspaceFolder}/pw_ide/py/pw_ide/activate.py -x 'pw ide cpp --set ${input:target}'",
+ "problemMatcher": [],
+ },
+ ],
+ "inputs": [
+ {
+ "id": "target",
+ "type": "promptString",
+ "description": "C++ code analysis target",
+ }
+ ],
+ }
+)
+# pylint: enable=line-too-long
+
+_DEFAULT_EXTENSIONS: EditorSettingsDict = OrderedDict(
+ {
+ "recommendations": [
+ "llvm-vs-code-extensions.vscode-clangd",
+ "ms-python.python",
+ "npclaudiu.vscode-gn",
+ "msedge-dev.gnls",
+ "zxh404.vscode-proto3",
+ "josetr.cmake-language-support-vscode",
+ "swyddfa.esbonio",
+ ],
+ "unwantedRecommendations": [
+ "ms-vscode.cpptools",
+ "persidskiy.vscode-gnformat",
+ "lextudio.restructuredtext",
+ "trond-snekvik.simple-rst",
+ ],
+ }
+)
+
+
+def _default_settings(
+ pw_ide_settings: PigweedIdeSettings,
+) -> EditorSettingsDict:
+ return OrderedDict(
+ {
+ **_DEFAULT_SETTINGS,
+ **_local_terminal_integrated_env(),
+ **_local_clangd_settings(pw_ide_settings),
+ **_local_python_settings(),
+ }
+ )
+
+
+def _default_tasks(_pw_ide_settings: PigweedIdeSettings) -> EditorSettingsDict:
+ return _DEFAULT_TASKS
+
+
+def _default_extensions(
+ _pw_ide_settings: PigweedIdeSettings,
+) -> EditorSettingsDict:
+ return _DEFAULT_EXTENSIONS
+
+
+DEFAULT_SETTINGS_PATH = Path(os.path.expandvars('$PW_PROJECT_ROOT')) / '.vscode'
+
+
+class VscSettingsType(Enum):
+ """Visual Studio Code settings files.
+
+ VSC supports editor settings (``settings.json``), recommended
+ extensions (``extensions.json``), and tasks (``tasks.json``).
+ """
+
+ SETTINGS = 'settings'
+ TASKS = 'tasks'
+ EXTENSIONS = 'extensions'
+
+ @classmethod
+ def all(cls) -> List['VscSettingsType']:
+ return list(cls)
+
+
+class VscSettingsManager(EditorSettingsManager[VscSettingsType]):
+ """Manages all settings for Visual Studio Code."""
+
+ default_settings_dir = DEFAULT_SETTINGS_PATH
+ file_format = Json5FileFormat()
+
+ types_with_defaults: EditorSettingsTypesWithDefaults = {
+ VscSettingsType.SETTINGS: _default_settings,
+ VscSettingsType.TASKS: _default_tasks,
+ VscSettingsType.EXTENSIONS: _default_extensions,
+ }
diff --git a/pw_ide/py/pyproject.toml b/pw_ide/py/pyproject.toml
new file mode 100644
index 000000000..ac821710a
--- /dev/null
+++ b/pw_ide/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_ide/py/setup.cfg b/pw_ide/py/setup.cfg
new file mode 100644
index 000000000..c7334c3ab
--- /dev/null
+++ b/pw_ide/py/setup.cfg
@@ -0,0 +1,30 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+[metadata]
+name = pw_ide
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Tools for Pigweed editor and IDE support
+
+[options]
+packages = find:
+install_requires =
+ json5>=0.9.10
+
+[options.entry_points]
+console_scripts = pw-ide = pw_ide.__main__:main
+
+[options.package_data]
+pw_ide = py.typed
diff --git a/pw_ide/py/setup.py b/pw_ide/py/setup.py
new file mode 100644
index 000000000..9807aa4e8
--- /dev/null
+++ b/pw_ide/py/setup.py
@@ -0,0 +1,18 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_ide"""
+
+import setuptools # type: ignore
+
+setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_ide/py/test_cases.py b/pw_ide/py/test_cases.py
new file mode 100644
index 000000000..25df41162
--- /dev/null
+++ b/pw_ide/py/test_cases.py
@@ -0,0 +1,175 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_ide test classes."""
+
+from contextlib import contextmanager
+from io import TextIOWrapper
+from pathlib import Path
+import tempfile
+from typing import Generator, List, Optional, Tuple, Union
+import unittest
+
+from pw_ide.settings import PigweedIdeSettings
+
+
+class TempDirTestCase(unittest.TestCase):
+ """Run tests that need access to a temporary directory."""
+
+ def setUp(self) -> None:
+ self.temp_dir = tempfile.TemporaryDirectory()
+ self.temp_dir_path = Path(self.temp_dir.name)
+
+ def tearDown(self) -> None:
+ self.temp_dir.cleanup()
+ return super().tearDown()
+
+ @contextmanager
+ def make_temp_file(
+ self, filename: Union[Path, str], content: str = ''
+ ) -> Generator[Tuple[TextIOWrapper, Path], None, None]:
+ """Create a temp file in the test case's temp dir.
+
+ Returns a tuple containing the file reference and the file's path.
+ The file can be read immediately.
+ """
+ path = self.temp_dir_path / filename
+
+ with open(path, 'a+', encoding='utf-8') as file:
+ file.write(content)
+ file.flush()
+ file.seek(0)
+ yield (file, path)
+
+ def touch_temp_file(
+ self, filename: Union[Path, str], content: str = ''
+ ) -> None:
+ """Create a temp file in the test case's temp dir, without context."""
+ with self.make_temp_file(filename, content):
+ pass
+
+ @contextmanager
+ def open_temp_file(
+ self,
+ filename: Union[Path, str],
+ ) -> Generator[Tuple[TextIOWrapper, Path], None, None]:
+ """Open an existing temp file in the test case's temp dir.
+
+ Returns a tuple containing the file reference and the file's path.
+ """
+ path = self.temp_dir_path / filename
+
+ with open(path, 'r', encoding='utf-8') as file:
+ yield (file, path)
+
+ @contextmanager
+ def make_temp_files(
+ self, files_data: List[Tuple[Union[Path, str], str]]
+ ) -> Generator[List[TextIOWrapper], None, None]:
+ """Create several temp files in the test case's temp dir.
+
+ Provide a list of file name and content tuples. Saves you the trouble
+ of excessive `with self.make_temp_file, self.make_temp_file...`
+ nesting, and allows programmatic definition of multiple temp file
+ contexts. Files can be read immediately.
+ """
+ files: List[TextIOWrapper] = []
+
+ for filename, content in files_data:
+ file = open(self.path_in_temp_dir(filename), 'a+', encoding='utf-8')
+ file.write(content)
+ file.flush()
+ file.seek(0)
+ files.append(file)
+
+ yield files
+
+ for file in files:
+ file.close()
+
+ def touch_temp_files(
+ self, files_data: List[Tuple[Union[Path, str], str]]
+ ) -> None:
+ """Create several temp files in the temp dir, without context."""
+ with self.make_temp_files(files_data):
+ pass
+
+ @contextmanager
+ def open_temp_files(
+ self, files_data: List[Union[Path, str]]
+ ) -> Generator[List[TextIOWrapper], None, None]:
+ """Open several existing temp files in the test case's temp dir.
+
+ Provide a list of file names. Saves you the trouble of excessive
+ `with self.open_temp_file, self.open_temp_file...` nesting, and allows
+ programmatic definition of multiple temp file contexts.
+ """
+ files: List[TextIOWrapper] = []
+
+ for filename in files_data:
+ file = open(self.path_in_temp_dir(filename), 'r', encoding='utf-8')
+ files.append(file)
+
+ yield files
+
+ for file in files:
+ file.close()
+
+ def path_in_temp_dir(self, path: Union[Path, str]) -> Path:
+ """Place a path into the test case's temp dir.
+
+ This only works with a relative path; with an absolute path, this is a
+ no-op.
+ """
+ return self.temp_dir_path / path
+
+ def paths_in_temp_dir(self, *paths: Union[Path, str]) -> List[Path]:
+ """Place several paths into the test case's temp dir.
+
+ This only works with relative paths; with absolute paths, this is a
+ no-op.
+ """
+ return [self.path_in_temp_dir(path) for path in paths]
+
+
+class PwIdeTestCase(TempDirTestCase):
+ """A test case for testing `pw_ide`.
+
+ Provides a temp dir for testing file system actions and access to IDE
+ settings that wrap the temp dir.
+ """
+
+ def make_ide_settings(
+ self,
+ working_dir: Optional[Union[str, Path]] = None,
+ targets: Optional[List[str]] = None,
+ ) -> PigweedIdeSettings:
+ """Make settings that wrap provided paths in the temp path."""
+
+ if working_dir is not None:
+ working_dir_path = self.path_in_temp_dir(working_dir)
+ else:
+ working_dir_path = self.temp_dir_path
+
+ if targets is None:
+ targets = []
+
+ return PigweedIdeSettings(
+ False,
+ False,
+ False,
+ default_config={
+ 'working_dir': str(working_dir_path),
+ 'targets': targets,
+ },
+ )
diff --git a/pw_ide/py/vscode_test.py b/pw_ide/py/vscode_test.py
new file mode 100644
index 000000000..33e4f4216
--- /dev/null
+++ b/pw_ide/py/vscode_test.py
@@ -0,0 +1,79 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for pw_ide.editors"""
+
+import unittest
+
+from pw_ide.vscode import VscSettingsManager, VscSettingsType
+
+from test_cases import PwIdeTestCase
+
+
+class TestVscSettingsManager(PwIdeTestCase):
+ """Tests VscSettingsManager"""
+
+ def test_setup(self):
+ """Test realistic setup procedure. Success == doesn't raise."""
+ ide_settings = self.make_ide_settings()
+ manager = VscSettingsManager(ide_settings, self.temp_dir_path)
+
+ with manager.active(
+ VscSettingsType.SETTINGS
+ ).modify() as active_settings:
+ manager.default(VscSettingsType.SETTINGS).sync_to(active_settings)
+ manager.project(VscSettingsType.SETTINGS).sync_to(active_settings)
+ manager.user(VscSettingsType.SETTINGS).sync_to(active_settings)
+
+ with manager.active(VscSettingsType.TASKS).modify() as active_settings:
+ manager.default(VscSettingsType.TASKS).sync_to(active_settings)
+ manager.project(VscSettingsType.TASKS).sync_to(active_settings)
+ manager.user(VscSettingsType.TASKS).sync_to(active_settings)
+
+ with manager.active(
+ VscSettingsType.EXTENSIONS
+ ).modify() as active_settings:
+ manager.default(VscSettingsType.EXTENSIONS).sync_to(active_settings)
+ manager.project(VscSettingsType.EXTENSIONS).sync_to(active_settings)
+ manager.user(VscSettingsType.EXTENSIONS).sync_to(active_settings)
+
+ def test_json5(self):
+ """Test that we can parse JSON5 files."""
+ content = """{
+ // This is a comment, and this list has a trailing comma.
+ "_pw": [
+ "foo",
+ "bar",
+ "baz",
+ ]
+}
+ """
+
+ self.touch_temp_file('pw_project_settings.json', content)
+ ide_settings = self.make_ide_settings()
+ manager = VscSettingsManager(ide_settings, self.temp_dir_path)
+
+ with manager.active(
+ VscSettingsType.SETTINGS
+ ).modify() as active_settings:
+ manager.default(VscSettingsType.SETTINGS).sync_to(active_settings)
+ manager.project(VscSettingsType.SETTINGS).sync_to(active_settings)
+ manager.user(VscSettingsType.SETTINGS).sync_to(active_settings)
+
+ active_settings = manager.active(VscSettingsType.SETTINGS).get()
+ self.assertIn('_pw', active_settings.keys())
+ self.assertEqual(len(active_settings['_pw']), 3)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_interrupt/BUILD.gn b/pw_interrupt/BUILD.gn
index 37604bbe3..1114687fe 100644
--- a/pw_interrupt/BUILD.gn
+++ b/pw_interrupt/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
+import("$dir_pw_unit_test/test.gni")
import("backend.gni")
config("public_include_path") {
@@ -33,3 +34,6 @@ pw_facade("context") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_interrupt/CMakeLists.txt b/pw_interrupt/CMakeLists.txt
index f16272983..abd03306c 100644
--- a/pw_interrupt/CMakeLists.txt
+++ b/pw_interrupt/CMakeLists.txt
@@ -13,8 +13,11 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_interrupt/backend.cmake)
-pw_add_facade(pw_interrupt.context
+pw_add_facade(pw_interrupt.context INTERFACE
+ BACKEND
+ pw_interrupt.context_BACKEND
HEADERS
public/pw_interrupt/context.h
PUBLIC_INCLUDES
diff --git a/pw_interrupt/backend.cmake b/pw_interrupt/backend.cmake
new file mode 100644
index 000000000..4364be415
--- /dev/null
+++ b/pw_interrupt/backend.cmake
@@ -0,0 +1,19 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backends for the pw_interrupt module.
+pw_add_backend_variable(pw_interrupt.context_BACKEND)
diff --git a/pw_interrupt/backend.gni b/pw_interrupt/backend.gni
index 535648a2d..47a44cbd0 100644
--- a/pw_interrupt/backend.gni
+++ b/pw_interrupt/backend.gni
@@ -13,6 +13,6 @@
# the License.
declare_args() {
- # Backend for the pw_interrupt module.
+ # Backends for the pw_interrupt module.
pw_interrupt_CONTEXT_BACKEND = ""
}
diff --git a/pw_interrupt_cortex_m/BUILD.gn b/pw_interrupt_cortex_m/BUILD.gn
index fed8d29c6..954807979 100644
--- a/pw_interrupt_cortex_m/BUILD.gn
+++ b/pw_interrupt_cortex_m/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
include_dirs = [ "public" ]
@@ -56,3 +57,6 @@ group("context_armv8m") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_interrupt_cortex_m/CMakeLists.txt b/pw_interrupt_cortex_m/CMakeLists.txt
index 2845783ef..5142dd41a 100644
--- a/pw_interrupt_cortex_m/CMakeLists.txt
+++ b/pw_interrupt_cortex_m/CMakeLists.txt
@@ -14,9 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_interrupt_cortex_m.context
- IMPLEMENTS_FACADES
- pw_interrupt.context
+pw_add_library(pw_interrupt_cortex_m.context INTERFACE
HEADERS
public/pw_interrupt_cortex_m/context_inline.h
public_overrides/pw_interrupt_backend/context_inline.h
diff --git a/pw_interrupt_zephyr/BUILD.gn b/pw_interrupt_zephyr/BUILD.gn
index 9a6699a8e..1b11f77f5 100644
--- a/pw_interrupt_zephyr/BUILD.gn
+++ b/pw_interrupt_zephyr/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_interrupt_zephyr/CMakeLists.txt b/pw_interrupt_zephyr/CMakeLists.txt
index 9563dba34..52768a008 100644
--- a/pw_interrupt_zephyr/CMakeLists.txt
+++ b/pw_interrupt_zephyr/CMakeLists.txt
@@ -14,14 +14,19 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-if(NOT CONFIG_PIGWEED_INTERRUPT)
+if(NOT CONFIG_PIGWEED_INTERRUPT_CONTEXT)
return()
endif()
-pw_add_module_library(pw_interrupt_zephyr
- IMPLEMENTS_FACADES
- pw_interrupt
+pw_add_library(pw_interrupt_zephyr.context INTERFACE
+ HEADERS
+ public/pw_interrupt_zephyr/context_inline.h
+ public_overrides/pw_interrupt_backend/context_inline.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_interrupt.context.facade
)
-pw_set_backend(pw_interrupt pw_interrupt_zephyr)
-zephyr_link_interface(pw_interrupt_zephyr)
-zephyr_link_libraries(pw_interrupt_zephyr)
+zephyr_link_interface(pw_interrupt_zephyr.context)
+zephyr_link_libraries(pw_interrupt_zephyr.context)
diff --git a/pw_interrupt_zephyr/Kconfig b/pw_interrupt_zephyr/Kconfig
index 44626a939..cf51e3b59 100644
--- a/pw_interrupt_zephyr/Kconfig
+++ b/pw_interrupt_zephyr/Kconfig
@@ -12,5 +12,5 @@
# License for the specific language governing permissions and limitations under
# the License.
-config PIGWEED_INTERRUPT
- bool "Enable the Pigweed interrupt library (pw_interrupt)"
+config PIGWEED_INTERRUPT_CONTEXT
+ bool "Enable the Pigweed interrupt library (pw_interrupt.context)"
diff --git a/pw_interrupt_zephyr/public/pw_interrupt_zephyr/context_inline.h b/pw_interrupt_zephyr/public/pw_interrupt_zephyr/context_inline.h
index cf8fb7860..e2fda0302 100644
--- a/pw_interrupt_zephyr/public/pw_interrupt_zephyr/context_inline.h
+++ b/pw_interrupt_zephyr/public/pw_interrupt_zephyr/context_inline.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
namespace pw::interrupt {
diff --git a/pw_intrusive_ptr/BUILD.bazel b/pw_intrusive_ptr/BUILD.bazel
new file mode 100644
index 000000000..257c2a7d2
--- /dev/null
+++ b/pw_intrusive_ptr/BUILD.bazel
@@ -0,0 +1,61 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_intrusive_ptr",
+ srcs = ["ref_counted_base.cc"],
+ hdrs = [
+ "public/pw_intrusive_ptr/internal/ref_counted_base.h",
+ "public/pw_intrusive_ptr/intrusive_ptr.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":pw_recyclable",
+ "//pw_assert",
+ ],
+)
+
+pw_cc_library(
+ name = "pw_recyclable",
+ hdrs = [
+ "public/pw_intrusive_ptr/recyclable.h",
+ ],
+ includes = ["public"],
+)
+
+pw_cc_test(
+ name = "intrusive_ptr_test",
+ srcs = [
+ "intrusive_ptr_test.cc",
+ ],
+ deps = [":pw_intrusive_ptr"],
+)
+
+pw_cc_test(
+ name = "recyclable_test",
+ srcs = [
+ "recyclable_test.cc",
+ ],
+ deps = [":pw_intrusive_ptr"],
+)
diff --git a/pw_intrusive_ptr/BUILD.gn b/pw_intrusive_ptr/BUILD.gn
new file mode 100644
index 000000000..fee72c0d8
--- /dev/null
+++ b/pw_intrusive_ptr/BUILD.gn
@@ -0,0 +1,64 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("pw_intrusive_ptr") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "public/pw_intrusive_ptr/internal/ref_counted_base.h",
+ "public/pw_intrusive_ptr/intrusive_ptr.h",
+ ]
+ sources = [ "ref_counted_base.cc" ]
+ deps = [ "$dir_pw_assert" ]
+ public_deps = [ ":pw_recyclable" ]
+}
+
+pw_source_set("pw_recyclable") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_intrusive_ptr/recyclable.h" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":intrusive_ptr_test" ]
+}
+
+pw_test("intrusive_ptr_test") {
+ sources = [ "intrusive_ptr_test.cc" ]
+ deps = [ ":pw_intrusive_ptr" ]
+
+ # TODO(b/260624583): Fix this for //targets/rp2040
+ enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "pico_executable"
+}
+
+pw_test("recyclable_test") {
+ sources = [ "recyclable_test.cc" ]
+ deps = [ ":pw_intrusive_ptr" ]
+
+ # TODO(b/260624583): Fix this for //targets/rp2040
+ enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "pico_executable"
+}
diff --git a/pw_intrusive_ptr/CMakeLists.txt b/pw_intrusive_ptr/CMakeLists.txt
new file mode 100644
index 000000000..cc0eb6bba
--- /dev/null
+++ b/pw_intrusive_ptr/CMakeLists.txt
@@ -0,0 +1,37 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_intrusive_ptr STATIC
+ HEADERS
+ public/pw_intrusive_ptr/internal/ref_counted_base.h
+ public/pw_intrusive_ptr/intrusive_ptr.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ SOURCES
+ ref_counted_base.cc
+)
+
+pw_add_test(pw_intrusive_ptr.intrusive_ptr_test
+ SOURCES
+ intrusive_ptr_test.cc
+ PRIVATE_DEPS
+ pw_intrusive_ptr
+ GROUPS
+ modules
+ pw_intrusive_ptr
+)
diff --git a/pw_intrusive_ptr/OWNERS b/pw_intrusive_ptr/OWNERS
new file mode 100644
index 000000000..d96cbc68d
--- /dev/null
+++ b/pw_intrusive_ptr/OWNERS
@@ -0,0 +1 @@
+hepler@google.com
diff --git a/pw_intrusive_ptr/docs.rst b/pw_intrusive_ptr/docs.rst
new file mode 100644
index 000000000..59295b744
--- /dev/null
+++ b/pw_intrusive_ptr/docs.rst
@@ -0,0 +1,83 @@
+.. _module-pw_intrusive_ptr:
+
+----------------
+pw_intrusive_ptr
+----------------
+
+IntrusivePtr
+------------
+``pw::IntrusivePtr`` is a smart shared pointer that relies on the pointed-at
+object to do the reference counting. Its API is based on ``std::shared_ptr`` but
+requires the pointed-at class to provide ``AddRef()`` and ``ReleaseRef()``
+methods to do the reference counting. The easiest way to do that is to
+subclass ``pw::RefCounted``. Doing this will provide atomic reference counting
+and a ``Ptr`` type alias for the ``IntrusivePtr<T>``.
+
+``IntrusivePtr`` doesn't provide any weak pointer ability.
+
+``IntrusivePtr`` with a ``RefCounted``-based class always guarantees atomic
+operations on the reference counter, whereas ``std::shared_ptr`` falls back to a
+non-atomic control block when threading support is not enabled due to a design
+fault in the STL implementation.
+
+Similar to ``std::shared_ptr``, ``IntrusivePtr`` doesn't provide any
+thread-safety guarantees for the pointed-at object or for the pointer object
+itself. I.e. assigning and reading the same ``IntrusivePtr`` from multiple
+threads without external lock is not allowed.
+
+.. code-block:: cpp
+
+ class MyClass : public RefCounted<MyClass> {
+ // ...
+ };
+
+ // Empty pointer, equals to nullptr.
+ // MyClass::Ptr is the same as IntrusivePtr<MyClass>.
+ MyClass::Ptr empty_ptr = IntrusivePtr<MyClass>();
+
+ // Wrapping an externally created pointer.
+ MyClass raw_ptr = new MyClass();
+ MyClass::Ptr ptr_1 = MyClass::Ptr(raw_ptr);
+ // raw_ptr shouldn't be used after this line if ptr_1 can go out of scope.
+
+ // Using MakeRefCounted() helper.
+ auto ptr_2 = MakeRefCounted<MyClass>(/* ... */);
+
+``IntrusivePtr`` can be passed as an argument by either const reference or
+value. Const reference is more preferable because it does not cause unnecessary
+copies (which results in atomic operations on the ref count). Passing by value
+is used when this ``IntrusivePtr`` is immediately stored (e.g. constructor that
+stores ``IntrusivePtr`` in the object field). In this case passing by value and
+move is more explicit in terms of intentions. It is also the behavior that
+clang-tidy checks suggest.
+
+``IntrusivePtr`` should almost always be returned by value. The only case when
+it can be returned by const reference is the trivial getter for the object
+field. When returning locally created ``IntrusivePtr`` or a pointer that was
+casted to the base class it MUST be returned by value.
+
+Recyclable
+----------
+``pw::Recyclable`` is a mixin that can be used with supported smart pointers
+like ``pw::IntrusivePtr`` to specify a custom memory cleanup routine instead
+of `delete`. The cleanup routine is specified as a method with the signature
+``void pw_recycle()``. For example:
+
+.. code-block:: cpp
+
+ class Foo : public pw::Recyclable<Foo>, public pw::IntrusivePtr<Foo> {
+ public:
+ // public implementation here
+ private:
+ friend class pw::Recyclable<Foo>;
+ void pw_recycle() {
+ if (should_recycle())) {
+ do_recycle_stuff();
+ } else {
+ delete this;
+ }
+ }
+ };
+
+``Recyclable`` can be used to avoid heap allocation when using smart pointers,
+as the recycle routine can return memory to a memory pool.
diff --git a/pw_intrusive_ptr/intrusive_ptr_test.cc b/pw_intrusive_ptr/intrusive_ptr_test.cc
new file mode 100644
index 000000000..2f5dcefc3
--- /dev/null
+++ b/pw_intrusive_ptr/intrusive_ptr_test.cc
@@ -0,0 +1,459 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_intrusive_ptr/intrusive_ptr.h"
+
+#include <stdint.h>
+
+#include <utility>
+
+#include "gtest/gtest.h"
+
+namespace pw {
+namespace {
+
+class TestItem : public RefCounted<TestItem> {
+ public:
+ TestItem() { ++instance_counter; }
+
+ explicit TestItem(int32_t f) : TestItem() { first = f; }
+
+ explicit TestItem(int64_t s) : TestItem() { second = s; }
+
+ TestItem(int32_t f, int64_t s) : TestItem() {
+ first = f;
+ second = s;
+ }
+
+ TestItem(const TestItem&) : TestItem() {}
+ TestItem(TestItem&&) noexcept : TestItem() {}
+
+ TestItem& operator=(const TestItem& other) {
+ if (&other != this) {
+ ++instance_counter;
+ }
+ return *this;
+ }
+
+ TestItem& operator=(TestItem&& other) noexcept {
+ if (&other != this) {
+ ++instance_counter;
+ }
+ return *this;
+ }
+
+ virtual ~TestItem() { --instance_counter; }
+
+ inline static int32_t instance_counter = 0;
+
+ int32_t first = 0;
+ int64_t second = 1;
+};
+
+class TestItemDerived : public TestItem {
+ public:
+ TestItemDerived() { ++derived_instance_counter; }
+ TestItemDerived(const TestItemDerived&) : TestItemDerived() {}
+ TestItemDerived(TestItemDerived&&) noexcept : TestItemDerived() {}
+
+ TestItemDerived& operator=(const TestItemDerived& other) {
+ if (&other != this) {
+ ++derived_instance_counter;
+ }
+ return *this;
+ }
+
+ TestItemDerived& operator=(TestItemDerived&& other) noexcept {
+ if (&other != this) {
+ ++derived_instance_counter;
+ }
+ return *this;
+ }
+
+ ~TestItemDerived() override { --derived_instance_counter; }
+
+ inline static int32_t derived_instance_counter = 0;
+};
+
+struct FreeTestItem {
+ void AddRef() const { ++instance_counter; }
+
+ bool ReleaseRef() const { return --instance_counter < 1; }
+
+ mutable int32_t instance_counter = 0;
+};
+
+class IntrusivePtrTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ TestItem::instance_counter = 0;
+ TestItemDerived::derived_instance_counter = 0;
+ }
+};
+
+TEST_F(IntrusivePtrTest, DeletingLastPtrDeletesTheObject) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, AssigningToNullptrDeletesTheObject) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ ptr = nullptr;
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, AssigningToEmptyPtrDeletesTheObject) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ IntrusivePtr<TestItem> empty;
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ ptr = empty;
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, SwapWithNullptrKeepsTheObject) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ IntrusivePtr<TestItem> empty;
+ EXPECT_EQ(TestItem::instance_counter, 1);
+
+ ptr.swap(empty);
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(ptr, nullptr);
+
+ empty = nullptr;
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, CopyingPtrDoesntCreateNewObjects) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+
+ {
+ IntrusivePtr<TestItem> ptr_2(ptr);
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+
+ // We still have a ptr here.
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, MovingPtrDoesntCreateNewObjects) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+
+ {
+ IntrusivePtr<TestItem> ptr_2(std::move(ptr));
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+
+ // ptr was moved away, object should be deleted.
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, CopyAssigningPtrDoesntCreateNewObjects) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+
+ {
+ auto ptr_2 = ptr;
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+
+ // We still have a ptr here.
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, MoveAssigningPtrDoesntCreateNewObjects) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+
+ {
+ auto ptr_2 = std::move(ptr);
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+
+ // ptr was moved away, object should be deleted.
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, CopyingPtrToBaseClassPtrDoesntCreateNewObjects) {
+ {
+ IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+
+ {
+ IntrusivePtr<TestItem> ptr_2(ptr);
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+ }
+
+ // We still have a ptr here.
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, MovingPtrToBaseClassPtrDoesntCreateNewObjects) {
+ {
+ IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+
+ {
+ IntrusivePtr<TestItem> ptr_2(std::move(ptr));
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+ }
+
+ // ptr was moved away, object should be deleted.
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, CopyAssigningPtrToBaseClassPtrDoesntCreateNewObjects) {
+ {
+ IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+
+ {
+ IntrusivePtr<TestItem> ptr_2 = ptr;
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+ }
+
+ // We still have a ptr here.
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, MoveAssigningPtrToBaseClassPtrDoesntCreateNewObjects) {
+ {
+ IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+
+ {
+ IntrusivePtr<TestItem> ptr_2 = std::move(ptr);
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 1);
+ }
+
+ // ptr was moved away, object should be deleted.
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ EXPECT_EQ(TestItemDerived::derived_instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, CopyAssigningPtrDeletesOldObjectIfLast) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+
+ {
+ IntrusivePtr<TestItem> ptr_2(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 2);
+
+ ptr_2 = ptr;
+
+ // Old object in ptr_2 should be removed.
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+
+ // We still have a ptr here.
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+TEST_F(IntrusivePtrTest, MoveAssigningPtrDeletesOldObjectIfLast) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 1);
+
+ {
+ IntrusivePtr<TestItem> ptr_2(new TestItem());
+ EXPECT_EQ(TestItem::instance_counter, 2);
+
+ ptr_2 = std::move(ptr);
+
+ // Old object in ptr_2 should be removed.
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ }
+
+ // ptr was moved away, object should be deleted.
+ EXPECT_EQ(TestItem::instance_counter, 0);
+ }
+ EXPECT_EQ(TestItem::instance_counter, 0);
+}
+
+// Comparison tests use operators directly to cover == and != and both
+// argument orders.
+TEST_F(IntrusivePtrTest, PtrsWithDifferentObjectsAreNotEqual) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ IntrusivePtr<TestItem> ptr_2(new TestItem());
+
+ EXPECT_FALSE(ptr == ptr_2);
+ EXPECT_FALSE(ptr_2 == ptr);
+ EXPECT_TRUE(ptr != ptr_2);
+ EXPECT_TRUE(ptr_2 != ptr);
+}
+
+TEST_F(IntrusivePtrTest, PtrsWithSameObjectsAreEqual) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ auto ptr_2 = ptr;
+
+ EXPECT_TRUE(ptr == ptr_2);
+ EXPECT_TRUE(ptr_2 == ptr);
+ EXPECT_FALSE(ptr != ptr_2);
+ EXPECT_FALSE(ptr_2 != ptr);
+}
+
+TEST_F(IntrusivePtrTest, FilledPtrIsNotEqualToEmptyPtr) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ IntrusivePtr<TestItem> empty;
+
+ EXPECT_FALSE(ptr == empty);
+ EXPECT_FALSE(empty == ptr);
+ EXPECT_TRUE(ptr != empty);
+ EXPECT_TRUE(empty != ptr);
+}
+
+TEST_F(IntrusivePtrTest, FilledPtrIsNotEqualToNullptr) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+
+ EXPECT_FALSE(ptr == nullptr);
+ EXPECT_FALSE(nullptr == ptr);
+ EXPECT_TRUE(ptr != nullptr);
+ EXPECT_TRUE(nullptr != ptr);
+}
+
+TEST_F(IntrusivePtrTest, EmptyPtrIsEqualToNullptr) {
+ IntrusivePtr<TestItem> empty;
+
+ EXPECT_TRUE(empty == nullptr);
+ EXPECT_TRUE(nullptr == empty);
+ EXPECT_FALSE(empty != nullptr);
+ EXPECT_FALSE(nullptr != empty);
+}
+
+TEST_F(IntrusivePtrTest, PtrsWithDifferentObjectsReturnDifferentPointers) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ IntrusivePtr<TestItem> ptr_2(new TestItem());
+
+ EXPECT_NE(ptr.get(), ptr_2.get());
+}
+
+TEST_F(IntrusivePtrTest, PtrsWithSameObjectsReturnSamePointer) {
+ IntrusivePtr<TestItemDerived> ptr(new TestItemDerived());
+ auto ptr_2 = ptr;
+ IntrusivePtr<TestItem> ptr_3 = ptr;
+
+ EXPECT_EQ(ptr.get(), ptr_2.get());
+ EXPECT_EQ(static_cast<TestItem*>(ptr.get()), ptr_3.get());
+}
+
+TEST_F(IntrusivePtrTest, EmptyPtrReturnsNullptr) {
+ IntrusivePtr<TestItem> empty;
+
+ EXPECT_EQ(empty.get(), nullptr);
+}
+
+TEST_F(IntrusivePtrTest, ConstifyWorks) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ IntrusivePtr<const TestItem> ptr_2 = ptr;
+
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(ptr.get(), ptr_2.get());
+}
+
+TEST_F(IntrusivePtrTest, NonRefCountedObjectWorks) {
+ // Compilation test only.
+ IntrusivePtr<FreeTestItem> empty;
+ IntrusivePtr<FreeTestItem> free(new FreeTestItem);
+ IntrusivePtr<FreeTestItem> free_2(free);
+ IntrusivePtr<FreeTestItem> free_3(std::move(free));
+}
+
+TEST_F(IntrusivePtrTest, MakeRefCounted) {
+ auto ptr_1 = MakeRefCounted<TestItem>();
+ EXPECT_EQ(TestItem::instance_counter, 1);
+ EXPECT_EQ(ptr_1->first, 0);
+ EXPECT_EQ(ptr_1->second, 1);
+
+ auto ptr_2 = MakeRefCounted<TestItem>(int32_t(42));
+ EXPECT_EQ(TestItem::instance_counter, 2);
+ EXPECT_EQ(ptr_2->first, 42);
+ EXPECT_EQ(ptr_2->second, 1);
+
+ auto ptr_3 = MakeRefCounted<TestItem>(int64_t(2));
+ EXPECT_EQ(TestItem::instance_counter, 3);
+ EXPECT_EQ(ptr_3->first, 0);
+ EXPECT_EQ(ptr_3->second, 2);
+
+ auto ptr_4 = MakeRefCounted<TestItem>(42, 5);
+ EXPECT_EQ(TestItem::instance_counter, 4);
+ EXPECT_EQ(ptr_4->first, 42);
+ EXPECT_EQ(ptr_4->second, 5);
+}
+
+TEST_F(IntrusivePtrTest, UseCount) {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(ptr.use_count(), 1);
+ {
+ IntrusivePtr<TestItem> ptr_copy = ptr;
+ EXPECT_EQ(ptr.use_count(), 2);
+ }
+ EXPECT_EQ(ptr.use_count(), 1);
+}
+
+TEST_F(IntrusivePtrTest, UseCountForNullPtr) {
+ IntrusivePtr<TestItem> ptr;
+ EXPECT_EQ(ptr.use_count(), 0);
+
+ ptr = IntrusivePtr(new TestItem);
+ EXPECT_EQ(ptr.use_count(), 1);
+
+ ptr = nullptr;
+ EXPECT_EQ(ptr.use_count(), 0);
+}
+
+} // namespace
+} // namespace pw
diff --git a/pw_intrusive_ptr/public/pw_intrusive_ptr/internal/ref_counted_base.h b/pw_intrusive_ptr/public/pw_intrusive_ptr/internal/ref_counted_base.h
new file mode 100644
index 000000000..82bb3768c
--- /dev/null
+++ b/pw_intrusive_ptr/public/pw_intrusive_ptr/internal/ref_counted_base.h
@@ -0,0 +1,50 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <atomic>
+#include <cstdint>
+
+namespace pw::internal {
+
+// Base class for RefCounted. Separates ref count storage from the private
+// API accessible by the IntrusivePtr through the RefCounted.
+class RefCountedBase {
+ public:
+ RefCountedBase(const RefCountedBase&) = delete;
+ RefCountedBase(RefCountedBase&&) = delete;
+ RefCountedBase& operator=(const RefCountedBase&) = delete;
+ RefCountedBase& operator=(RefCountedBase&&) = delete;
+
+ protected:
+ constexpr RefCountedBase() = default;
+ ~RefCountedBase();
+
+ // Increments reference counter.
+ void AddRef() const;
+
+ // Decrements reference count and returns true if the object should be
+ // deleted.
+ [[nodiscard]] bool ReleaseRef() const;
+
+ // Returns current ref count value.
+ [[nodiscard]] int32_t ref_count() const {
+ return ref_count_.load(std::memory_order_relaxed);
+ }
+
+ private:
+ mutable std::atomic_int32_t ref_count_{0};
+};
+
+} // namespace pw::internal
diff --git a/pw_intrusive_ptr/public/pw_intrusive_ptr/intrusive_ptr.h b/pw_intrusive_ptr/public/pw_intrusive_ptr/intrusive_ptr.h
new file mode 100644
index 000000000..2e7ce6bc5
--- /dev/null
+++ b/pw_intrusive_ptr/public/pw_intrusive_ptr/intrusive_ptr.h
@@ -0,0 +1,209 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstddef>
+#include <type_traits>
+#include <utility>
+
+#include "pw_intrusive_ptr/internal/ref_counted_base.h"
+#include "pw_intrusive_ptr/recyclable.h"
+
+namespace pw {
+
+// Shared pointer that relies on the stored object for the refcounting.
+//
+// T should be either a subclass of `RefCounted` (preferred way) or
+// implement AddRef()/ReleaseRef() by itself.
+//
+// IntrusivePtr API follows the std::shared_ptr API, but doesn't provide weak
+// pointers and some of the functionality such as reset(), owner_before(),
+// operator[] or unique().
+//
+// Similar to the std::make_shared for the std::shared_ptr, IntrusivePtr
+// provides the MakeRefCounted() helper.
+//
+// IntrusivePtr by itself doesn't provide any thread-safety guarantees but if T
+// is a subclass from `RefCounted` - it is guaranteed to have atomic reference
+// counter operations.
+template <typename T>
+class IntrusivePtr final {
+ public:
+ using element_type = T;
+
+ // Constructs an empty IntrusivePtr.
+ constexpr IntrusivePtr() : ptr_(nullptr) {}
+
+ // Constructs an empty IntrusivePtr.
+ //
+ // NOLINTNEXTLINE(google-explicit-constructor)
+ constexpr IntrusivePtr(std::nullptr_t) : IntrusivePtr() {}
+
+ // Constructs an IntrusivePtr from already allocated pointer.
+ //
+ // IntrusivePtr owns this pointer after this wrapping. All operations with the
+ // pointer should be done through IntrusivePtr after the wrapping or while at
+ // least one IntrusivePtr object owning it is in scope.
+ //
+ // IntrusivePtr can be used with either heap-allocated pointers or
+ // stack/static allocated objects if T is Recyclable. An attempt to wrap a
+ // stack-allocated object with a non-Recyclable IntrusivePtr will result in a
+ // crash on destruction.
+ explicit IntrusivePtr(T* p) : ptr_(p) {
+ if (ptr_) {
+ ptr_->AddRef();
+ }
+ }
+
+ IntrusivePtr(const IntrusivePtr& other) : IntrusivePtr(other.ptr_) {}
+
+ IntrusivePtr(IntrusivePtr&& other) noexcept
+ : ptr_(std::exchange(other.ptr_, nullptr)) {}
+
+ template <typename U>
+ // NOLINTNEXTLINE(google-explicit-constructor)
+ IntrusivePtr(const IntrusivePtr<U>& other) : IntrusivePtr(other.ptr_) {
+ CheckConversionAllowed<U>();
+ }
+
+ template <typename U>
+ // NOLINTNEXTLINE(google-explicit-constructor)
+ IntrusivePtr(IntrusivePtr<U>&& other)
+ : ptr_(std::exchange(other.ptr_, nullptr)) {
+ CheckConversionAllowed<U>();
+ }
+
+ IntrusivePtr& operator=(const IntrusivePtr& other) {
+ if (&other == this) {
+ return *this;
+ }
+ IntrusivePtr(other).swap(*this);
+ return *this;
+ }
+
+ IntrusivePtr& operator=(IntrusivePtr&& other) noexcept {
+ if (&other == this) {
+ return *this;
+ }
+ IntrusivePtr(std::move(other)).swap(*this);
+ return *this;
+ }
+
+ ~IntrusivePtr() {
+ T* ptr = ptr_;
+ // Clear ptr_ to help detect re-entrancy in ~T.
+ ptr_ = nullptr;
+ if (ptr && ptr->ReleaseRef()) {
+ recycle_or_delete(ptr);
+ }
+ }
+
+ void swap(IntrusivePtr& other) { std::swap(ptr_, other.ptr_); }
+
+ T* get() const { return ptr_; }
+
+ int32_t use_count() const { return ptr_ ? ptr_->ref_count() : 0; }
+
+ T& operator*() const { return *ptr_; }
+
+ T* operator->() const { return ptr_; }
+
+ explicit operator bool() const { return ptr_; }
+
+ private:
+ template <typename U>
+ friend class IntrusivePtr;
+
+ // Compilation-time verification that we can convert from IntrusivePtr<U> to
+ // IntrusivePtr<T>.
+ template <typename U>
+ constexpr void CheckConversionAllowed() {
+ static_assert(
+ std::is_convertible_v<U*, T*> &&
+ (std::has_virtual_destructor_v<T> || std::is_same_v<T, const U>),
+ "Cannot convert IntrusivePtr<U> to IntrusivePtr<T> unless T has a "
+ "virtual destructor or T == const U.");
+ }
+
+ // Support Ts that inherit from the Recyclable mixin.
+ static void recycle_or_delete(T* ptr) {
+ if constexpr (::pw::internal::has_pw_recycle_v<T>) {
+ ::pw::internal::recycle<T>(ptr);
+ } else {
+ delete ptr;
+ }
+ }
+
+ T* ptr_;
+};
+
+// Base class to be used with the IntrusivePtr. Doesn't provide any public
+// methods.
+//
+// Provides an atomic-based reference counting. Atomics are used irrespective of
+// the settings, which makes it different from the std::shared_ptr (that relies
+// on the threading support settings to determine if atomics should be used for
+// the control block or not).
+//
+// RefCounted MUST never be used as a pointer type to store derived objects -
+// it doesn't provide a virtual destructor.
+template <typename T>
+class RefCounted : private internal::RefCountedBase {
+ public:
+ // Type alias for the IntrusivePtr of ref-counted type.
+ using Ptr = IntrusivePtr<T>;
+
+ private:
+ template <typename U>
+ friend class IntrusivePtr;
+};
+
+template <typename T, typename U>
+inline bool operator==(const IntrusivePtr<T>& lhs, const IntrusivePtr<U>& rhs) {
+ return lhs.get() == rhs.get();
+}
+
+template <typename T, typename U>
+inline bool operator!=(const IntrusivePtr<T>& lhs, const IntrusivePtr<U>& rhs) {
+ return !(lhs == rhs);
+}
+
+template <typename T>
+inline bool operator==(const IntrusivePtr<T>& ptr, std::nullptr_t) {
+ return ptr.get() == nullptr;
+}
+
+template <typename T>
+inline bool operator!=(const IntrusivePtr<T>& ptr, std::nullptr_t) {
+ return ptr.get() != nullptr;
+}
+
+template <typename T>
+inline bool operator==(std::nullptr_t, const IntrusivePtr<T>& ptr) {
+ return ptr.get() == nullptr;
+}
+
+template <typename T>
+inline bool operator!=(std::nullptr_t, const IntrusivePtr<T>& ptr) {
+ return ptr.get() != nullptr;
+}
+
+// Constructs an IntrusivePtr<T> with a given set of arguments for the T
+// constructor.
+template <typename T, typename... Args>
+IntrusivePtr<T> MakeRefCounted(Args&&... args) {
+ return IntrusivePtr(new T(std::forward<Args>(args)...));
+}
+
+} // namespace pw
diff --git a/pw_intrusive_ptr/public/pw_intrusive_ptr/recyclable.h b/pw_intrusive_ptr/public/pw_intrusive_ptr/recyclable.h
new file mode 100644
index 000000000..4c339c0c1
--- /dev/null
+++ b/pw_intrusive_ptr/public/pw_intrusive_ptr/recyclable.h
@@ -0,0 +1,135 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <type_traits>
+
+// pw::Recyclable<T>
+//
+// Notes:
+//
+// pw::Recyclable<T> is a mix-in class which allows users to control what
+// happens to objects when they reach the end of their lifecycle, as determined
+// by the Pigweed managed pointer classes.
+//
+// The general idea is as follows. A developer might have some sort of factory
+// pattern where they hand out unique_ptr<>s or IntrusivePtr<>s to objects which
+// they have created. When their user is done with the object and the managed
+// pointers let go of it, instead of executing the destructor and deleting the
+// object, the developer may want to "recycle" the object and use it for some
+// internal purpose. Examples include...
+//
+// 1) Putting the object on some sort of internal list to hand out again
+// of the object is re-usable and the cost of construction/destruction
+// is high.
+// 2) Putting the object into some form of deferred destruction queue
+// because users are either too high priority to pay the cost of
+// destruction when the object is released, or because the act of
+// destruction might involve operations which are not permitted when
+// the object is released (perhaps the object is released at IRQ time,
+// but the system needs to be running in a thread in order to properly
+// clean up the object)
+// 3) Re-using the object internally for something like bookkeeping
+// purposes.
+//
+// In order to make use of the feature, users need to do two things.
+//
+// 1) Derive from pw::Recyclable<T>.
+// 2) Implement a method with the signature "void pw_recycle()"
+//
+// When deriving from Recyclable<T>, T should be devoid of cv-qualifiers (even
+// if the managed pointers handed out by the user's code are const or volatile).
+// In addition, pw_recycle must be visible to pw::Recyclable<T>, either
+// because it is public or because the T is friends with pw::Recyclable<T>.
+//
+// :: Example ::
+//
+// Some code hands out unique pointers to const Foo objects and wishes to
+// have the chance to recycle them. The code would look something like
+// this...
+//
+// class Foo : public pw::Recyclable<Foo> {
+// public:
+// // public implementation here
+// private:
+// friend class pw::Recyclable<Foo>;
+// void pw_recycle() {
+// if (should_recycle())) {
+// do_recycle_stuff();
+// } else {
+// delete this;
+// }
+// }
+// };
+//
+// Note: the intention is to use this feature with managed pointers,
+// which will automatically detect and call the recycle method if
+// present. That said, there is nothing to stop users for manually
+// calling pw_recycle, provided that it is visible to the code which
+// needs to call it.
+
+namespace pw {
+
+// Default implementation of pw::Recyclable.
+//
+// Note: we provide a default implementation instead of just a fwd declaration
+// so we can add a static_assert which will give a user a more human readable
+// error in case they make the mistake of deriving from pw::Recyclable<const
+// Foo> instead of pw::Recyclable<Foo>
+template <typename T, typename = void>
+class Recyclable {
+ // Note: static assert must depend on T in order to trigger only when the
+ // template gets expanded. If it does not depend on any template parameters,
+ // eg static_assert(false), then it will always explode, regardless of whether
+ // or not the template is ever expanded.
+ static_assert(
+ std::is_same_v<T, T> == false,
+ "pw::Recyclable<T> objects must not specify cv-qualifiers for T. "
+ "Derive from pw::Recyclable<Foo>, not pw::Recyclable<const Foo>");
+};
+
+namespace internal {
+
+// Test to see if an object is recyclable. An object of type T is considered to
+// be recyclable if it derives from pw::Recyclable<T>
+template <typename T>
+inline constexpr bool has_pw_recycle_v =
+ std::is_base_of_v<::pw::Recyclable<std::remove_cv_t<T>>, T>;
+
+template <typename T>
+inline void recycle(T* ptr) {
+ static_assert(has_pw_recycle_v<T>, "T must derive from pw::Recyclable");
+ Recyclable<std::remove_cv_t<T>>::pw_recycle_thunk(
+ const_cast<std::remove_cv_t<T>*>(ptr));
+}
+
+} // namespace internal
+
+template <typename T>
+class Recyclable<T, std::enable_if_t<std::is_same_v<std::remove_cv_t<T>, T>>> {
+ private:
+ friend void ::pw::internal::recycle<T>(T*);
+ friend void ::pw::internal::recycle<const T>(const T*);
+
+ static void pw_recycle_thunk(T* ptr) {
+ static_assert(std::is_same_v<decltype(&T::pw_recycle), void (T::*)(void)>,
+ "pw_recycle() methods must be non-static member functions "
+ "with the signature 'void pw_recycle()', and be visible to "
+ "pw::Recyclable<T> (either because they are public, or "
+ "because of friendship).");
+ ptr->pw_recycle();
+ }
+};
+
+} // namespace pw
diff --git a/pw_intrusive_ptr/recyclable_test.cc b/pw_intrusive_ptr/recyclable_test.cc
new file mode 100644
index 000000000..9069d6265
--- /dev/null
+++ b/pw_intrusive_ptr/recyclable_test.cc
@@ -0,0 +1,89 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_intrusive_ptr/recyclable.h"
+
+#include <stdint.h>
+
+#include <utility>
+
+#include "gtest/gtest.h"
+#include "pw_intrusive_ptr/intrusive_ptr.h"
+
+namespace pw {
+namespace {
+
+class TestItem : public RefCounted<TestItem>, public Recyclable<TestItem> {
+ public:
+ TestItem() = default;
+
+ virtual ~TestItem() {}
+
+ inline static int32_t recycle_counter = 0;
+
+ private:
+ friend class Recyclable<TestItem>;
+ void pw_recycle() {
+ recycle_counter++;
+ delete this;
+ }
+};
+
+// Class that thas the pw_recyclable method, but does not derive from
+// Recyclable.
+class TestItemNonRecyclable : public RefCounted<TestItemNonRecyclable> {
+ public:
+ TestItemNonRecyclable() = default;
+
+ virtual ~TestItemNonRecyclable() {}
+
+ inline static int32_t recycle_counter = 0;
+
+ void pw_recycle() { recycle_counter++; }
+};
+
+class RecyclableTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ TestItem::recycle_counter = 0;
+ TestItemNonRecyclable::recycle_counter = 0;
+ }
+};
+
+TEST_F(RecyclableTest, DeletingLastPtrRecyclesTheObject) {
+ {
+ IntrusivePtr<TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::recycle_counter, 0);
+ }
+ EXPECT_EQ(TestItem::recycle_counter, 1);
+}
+
+TEST_F(RecyclableTest, ConstRecycle) {
+ {
+ IntrusivePtr<const TestItem> ptr(new TestItem());
+ EXPECT_EQ(TestItem::recycle_counter, 0);
+ }
+ EXPECT_EQ(TestItem::recycle_counter, 1);
+}
+
+TEST_F(RecyclableTest, NonRecyclableWithPwRecycleMethod) {
+ {
+ IntrusivePtr<TestItemNonRecyclable> ptr(new TestItemNonRecyclable());
+ EXPECT_EQ(TestItemNonRecyclable::recycle_counter, 0);
+ }
+ EXPECT_EQ(TestItemNonRecyclable::recycle_counter, 0);
+}
+
+} // namespace
+} // namespace pw
diff --git a/pw_intrusive_ptr/ref_counted_base.cc b/pw_intrusive_ptr/ref_counted_base.cc
new file mode 100644
index 000000000..089c9885b
--- /dev/null
+++ b/pw_intrusive_ptr/ref_counted_base.cc
@@ -0,0 +1,61 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_intrusive_ptr/internal/ref_counted_base.h"
+
+#include <atomic>
+#include <cstdint>
+
+#include "pw_assert/check.h"
+
+namespace pw::internal {
+
+RefCountedBase::~RefCountedBase() {
+ // Set the ref count to a poison value so that we have the best chance of
+ // catching a use-after-free situation.
+ //
+ // The value is chosen specifically to be negative when stored as an int32_t,
+ // and as far away from becoming positive (via either addition or subtraction)
+ // as possible.
+ ref_count_.store(static_cast<int32_t>(0xC0000000), std::memory_order_release);
+}
+
+void RefCountedBase::AddRef() const {
+ const auto refs = ref_count_.fetch_add(1, std::memory_order_relaxed);
+
+ // This assertion will fire if someone calls AddRef() on a ref-counted object
+ // that has reached ref_count_ == 0 but has not been destroyed yet. This could
+ // happen by manually calling AddRef(), or re-wrapping such a pointer with
+ // RefPtr<T>(T*) (which calls AddRef()).
+ PW_DCHECK(refs >= 0);
+}
+
+bool RefCountedBase::ReleaseRef() const {
+ // We don't follow the boost::intrusive_ptr/fit::RefPtr approach here with a
+ // release fetch_sub and acquire fence afterwards due to TSAN not supporting
+ // fences (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=97868).
+ //
+ // This approach is a bit less performant - it does the acquiring on each
+ // release, not only for the last ref - but otherwise works the same.
+ const auto refs = ref_count_.fetch_sub(1, std::memory_order_acq_rel);
+
+ // This assertion will fire if someone manually calls ReleaseRef()
+ // on a ref-counted object too many times, or if ReleaseRef is called
+ // before an object has been wrapped with RefPtr.
+ PW_DCHECK(refs >= 1);
+
+ return refs == 1;
+}
+
+} // namespace pw::internal
diff --git a/pw_kvs/BUILD.bazel b/pw_kvs/BUILD.bazel
index 41359d7b8..5b1fbfd1a 100644
--- a/pw_kvs/BUILD.bazel
+++ b/pw_kvs/BUILD.bazel
@@ -79,6 +79,12 @@ pw_cc_library(
)
pw_cc_library(
+ name = "flash_test_partition",
+ hdrs = ["public/pw_kvs/flash_test_partition.h"],
+ deps = [":pw_kvs"],
+)
+
+pw_cc_library(
name = "fake_flash",
srcs = [
"fake_flash_memory.cc",
@@ -117,6 +123,22 @@ pw_cc_library(
)
pw_cc_library(
+ name = "fake_flash_12_byte_partition",
+ srcs = ["fake_flash_test_partition.cc"],
+ hdrs = ["public/pw_kvs/flash_test_partition.h"],
+ defines = [
+ "PW_FLASH_TEST_SECTORS=3",
+ "PW_FLASH_TEST_SECTOR_SIZE=4",
+ "PW_FLASH_TEST_ALIGNMENT=4",
+ ],
+ deps = [
+ ":fake_flash",
+ ":flash_test_partition",
+ ":pw_kvs",
+ ],
+)
+
+pw_cc_library(
name = "fake_flash_16_aligned_partition",
srcs = [
"fake_flash_test_partition.cc",
@@ -374,7 +396,7 @@ pw_cc_test(
"//pw_log:facade",
"//pw_span",
"//pw_status",
- "//pw_string",
+ "//pw_string:builder",
"//pw_unit_test",
],
)
@@ -391,7 +413,7 @@ pw_cc_test(
"//pw_log:facade",
"//pw_span",
"//pw_status",
- "//pw_string",
+ "//pw_string:builder",
"//pw_unit_test",
],
)
@@ -408,7 +430,7 @@ pw_cc_test(
"//pw_log:facade",
"//pw_span",
"//pw_status",
- "//pw_string",
+ "//pw_string:builder",
"//pw_unit_test",
],
)
@@ -425,7 +447,7 @@ pw_cc_test(
"//pw_log:facade",
"//pw_span",
"//pw_status",
- "//pw_string",
+ "//pw_string:builder",
"//pw_unit_test",
],
)
@@ -442,7 +464,7 @@ pw_cc_test(
"//pw_log:facade",
"//pw_span",
"//pw_status",
- "//pw_string",
+ "//pw_string:builder",
"//pw_unit_test",
],
)
diff --git a/pw_kvs/BUILD.gn b/pw_kvs/BUILD.gn
index 10b054532..907769f46 100644
--- a/pw_kvs/BUILD.gn
+++ b/pw_kvs/BUILD.gn
@@ -65,9 +65,9 @@ pw_source_set("pw_kvs") {
dir_pw_assert,
dir_pw_bytes,
dir_pw_containers,
+ dir_pw_span,
dir_pw_status,
dir_pw_stream,
- dir_pw_string,
]
deps = [
":config",
@@ -88,6 +88,7 @@ pw_source_set("crc16") {
public_deps = [
":pw_kvs",
dir_pw_checksum,
+ dir_pw_span,
]
}
@@ -299,6 +300,7 @@ pw_source_set("key_value_store_initialized_test") {
":crc16",
":flash_test_partition",
":pw_kvs",
+ "$dir_pw_string:builder",
dir_pw_bytes,
dir_pw_checksum,
dir_pw_log,
@@ -309,9 +311,11 @@ pw_source_set("key_value_store_initialized_test") {
pw_source_set("key_value_store_fuzz_test") {
deps = [
+ ":config",
":crc16",
":flash_test_partition",
":pw_kvs",
+ "$dir_pw_string:builder",
dir_pw_bytes,
dir_pw_checksum,
dir_pw_log,
@@ -324,6 +328,7 @@ pw_source_set("test_key_value_store_test") {
deps = [
":pw_kvs",
":test_key_value_store",
+ "$dir_pw_string:builder",
"$dir_pw_sync:borrow",
dir_pw_unit_test,
]
@@ -353,7 +358,7 @@ pw_test_group("tests") {
if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
pw_toolchain_SCOPE.is_host_toolchain) {
- # TODO(pwbug/196): KVS tests are not compatible with device builds as they
+ # TODO(b/234883746): KVS tests are not compatible with device builds as they
# use features such as std::map and are computationally expensive. Solving
# this requires a more complex capabilities-based build and configuration
# system which allowing enabling specific tests for targets that support
@@ -479,9 +484,11 @@ pw_test("flash_partition_256_write_size_test") {
pw_test("key_value_store_test") {
deps = [
+ ":config",
":crc16",
":fake_flash",
":pw_kvs",
+ "$dir_pw_string:builder",
dir_pw_bytes,
dir_pw_checksum,
dir_pw_log,
@@ -566,6 +573,7 @@ pw_test("key_value_store_map_test") {
":fake_flash",
":pw_kvs",
":test_partition",
+ "$dir_pw_string:builder",
dir_pw_checksum,
]
sources = [ "key_value_store_map_test.cc" ]
@@ -599,13 +607,9 @@ pw_doc_group("docs") {
report_deps = [ ":kvs_size" ]
}
-pw_size_report("kvs_size") {
+pw_size_diff("kvs_size") {
title = "Pigweed KVS size report"
- # To see all the symbols, uncomment the following:
- # Note: The size report RST table won't be generated when full_report = true.
- # full_report = true
-
binaries = [
{
target = "size_report:with_kvs"
diff --git a/pw_kvs/CMakeLists.txt b/pw_kvs/CMakeLists.txt
index 8e20e5b13..1d3a2e7e8 100644
--- a/pw_kvs/CMakeLists.txt
+++ b/pw_kvs/CMakeLists.txt
@@ -16,27 +16,611 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_module_config(pw_kvs_CONFIG)
-pw_auto_add_simple_module(pw_kvs
+pw_add_library(pw_kvs.config INTERFACE
+ HEADERS
+ pw_kvs_private/config.h
PUBLIC_DEPS
+ ${pw_kvs_CONFIG}
+)
+
+pw_add_library(pw_kvs STATIC
+ HEADERS
+ public/pw_kvs/alignment.h
+ public/pw_kvs/checksum.h
+ public/pw_kvs/flash_memory.h
+ public/pw_kvs/flash_test_partition.h
+ public/pw_kvs/format.h
+ public/pw_kvs/io.h
+ public/pw_kvs/key.h
+ public/pw_kvs/key_value_store.h
+ public/pw_kvs/internal/entry.h
+ public/pw_kvs/internal/entry_cache.h
+ public/pw_kvs/internal/hash.h
+ public/pw_kvs/internal/key_descriptor.h
+ public/pw_kvs/internal/sectors.h
+ public/pw_kvs/internal/span_traits.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_bytes
pw_containers
+ pw_span
pw_status
+ pw_stream
+ SOURCES
+ alignment.cc
+ checksum.cc
+ entry.cc
+ entry_cache.cc
+ flash_memory.cc
+ format.cc
+ key_value_store.cc
+ sectors.cc
+ PRIVATE_DEPS
+ pw_checksum
+ pw_kvs.config
+ pw_log
+)
+
+pw_add_library(pw_kvs.crc16 INTERFACE
+ HEADERS
+ public/pw_kvs/crc16_checksum.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_checksum
+ pw_kvs
+ pw_span
+)
+
+pw_add_library(pw_kvs.flash_test_partition INTERFACE
+ HEADERS
+ public/pw_kvs/flash_test_partition.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs
+)
+
+pw_add_library(pw_kvs.test_key_value_store INTERFACE
+ HEADERS
+ public/pw_kvs/test_key_value_store.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs
pw_sync.borrow
+)
+
+pw_add_library(pw_kvs.fake_flash STATIC
+ HEADERS
+ public/pw_kvs/fake_flash_memory.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_containers
+ pw_kvs
+ pw_status
+ SOURCES
+ fake_flash_memory.cc
PRIVATE_DEPS
- pw_assert
+ pw_kvs.config
+ pw_log
+)
+
+pw_add_library(pw_kvs.fake_flash_12_byte_partition STATIC
+ HEADERS
+ public/pw_kvs/flash_test_partition.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs.flash_test_partition
+ SOURCES
+ fake_flash_test_partition.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_SECTORS=3
+ PW_FLASH_TEST_SECTOR_SIZE=4
+ PW_FLASH_TEST_ALIGNMENT=4
+)
+
+pw_add_library(pw_kvs.fake_flash_1_aligned_partition STATIC
+ HEADERS
+ public/pw_kvs/flash_test_partition.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs.flash_test_partition
+ SOURCES
+ fake_flash_test_partition.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_SECTORS=6U
+ PW_FLASH_TEST_SECTOR_SIZE=4096U
+ PW_FLASH_TEST_ALIGNMENT=1U
+)
+
+pw_add_library(pw_kvs.fake_flash_16_aligned_partition STATIC
+ HEADERS
+ public/pw_kvs/flash_test_partition.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs.flash_test_partition
+ SOURCES
+ fake_flash_test_partition.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_SECTORS=6U
+ PW_FLASH_TEST_SECTOR_SIZE=4096U
+ PW_FLASH_TEST_ALIGNMENT=16U
+)
+
+pw_add_library(pw_kvs.fake_flash_64_aligned_partition STATIC
+ HEADERS
+ public/pw_kvs/flash_test_partition.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs.flash_test_partition
+ SOURCES
+ fake_flash_test_partition.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_SECTORS=6U
+ PW_FLASH_TEST_SECTOR_SIZE=4096U
+ PW_FLASH_TEST_ALIGNMENT=64U
+)
+
+pw_add_library(pw_kvs.fake_flash_256_aligned_partition STATIC
+ HEADERS
+ public/pw_kvs/flash_test_partition.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs.flash_test_partition
+ SOURCES
+ fake_flash_test_partition.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_SECTORS=6U
+ PW_FLASH_TEST_SECTOR_SIZE=4096U
+ PW_FLASH_TEST_ALIGNMENT=256U
+)
+
+pw_add_library(pw_kvs.fake_flash_test_key_value_store STATIC
+ SOURCES
+ fake_flash_test_key_value_store.cc
+ PRIVATE_DEPS
+ pw_kvs
+ pw_kvs.crc16
+ pw_kvs.fake_flash
+ pw_kvs.test_key_value_store
+)
+
+pw_add_library(pw_kvs.flash_partition_stream_test STATIC
+ HEADERS
+ public/pw_kvs/flash_memory.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_sync.borrow
pw_bytes
- pw_checksum
- ${pw_kvs_CONFIG}
+ pw_kvs
+ pw_polyfill
+ pw_preprocessor
+ pw_status
+ pw_stream
+ SOURCES
+ flash_partition_stream_test.cc
+ PRIVATE_DEPS
+ pw_kvs.config
+ pw_kvs.fake_flash
+ pw_kvs.flash_test_partition
+ pw_kvs
pw_log
pw_random
- pw_stream
- pw_string
+ pw_unit_test
+)
+
+pw_add_library(pw_kvs.flash_partition_test_100_iterations STATIC
+ SOURCES
+ flash_partition_test.cc
+ PRIVATE_DEPS
+ pw_kvs.config
+ pw_kvs.flash_test_partition
+ pw_kvs
+ pw_log
+ pw_unit_test
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_ITERATIONS=100
+ PW_FLASH_TEST_WRITE_SIZE=1
+)
+
+pw_add_library(pw_kvs.flash_partition_test_2_iterations STATIC
+ SOURCES
+ flash_partition_test.cc
+ PRIVATE_DEPS
+ pw_kvs.config
+ pw_kvs.flash_test_partition
+ pw_kvs
+ pw_log
+ pw_unit_test
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_ITERATIONS=2
+ PW_FLASH_TEST_WRITE_SIZE=1
+)
+
+pw_add_library(pw_kvs.flash_partition_test_100_iterations_256_write STATIC
+ SOURCES
+ flash_partition_test.cc
+ PRIVATE_DEPS
+ pw_kvs.config
+ pw_kvs.flash_test_partition
+ pw_kvs
+ pw_log
+ pw_unit_test
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_ITERATIONS=100
+ PW_FLASH_TEST_WRITE_SIZE=256
+)
+
+pw_add_library(pw_kvs.flash_partition_test_2_iterations_256_write STATIC
+ SOURCES
+ flash_partition_test.cc
+ PRIVATE_DEPS
+ pw_kvs.config
+ pw_kvs.flash_test_partition
+ pw_kvs
+ pw_log
+ pw_unit_test
+ PRIVATE_DEFINES
+ PW_FLASH_TEST_ITERATIONS=2
+ PW_FLASH_TEST_WRITE_SIZE=256
+)
+
+pw_add_library(pw_kvs.key_value_store_initialized_test STATIC
+ SOURCES
+ key_value_store_initialized_test.cc
+ PRIVATE_DEPS
+ pw_kvs.crc16
+ pw_kvs.flash_test_partition
+ pw_kvs
+ pw_string.builder
+ pw_bytes
+ pw_checksum
+ pw_log
+ pw_unit_test
+)
+
+pw_add_library(pw_kvs.key_value_store_fuzz_test STATIC
+ SOURCES
+ key_value_store_fuzz_test.cc
+ PRIVATE_DEPS
+ pw_kvs.config
+ pw_kvs.crc16
+ pw_kvs.flash_test_partition
+ pw_kvs
+ pw_string.builder
+ pw_bytes
+ pw_checksum
+ pw_log
+ pw_unit_test
+)
+
+pw_add_library(pw_kvs.test_key_value_store_test STATIC
+ SOURCES
+ test_key_value_store_test.cc
+ PRIVATE_DEPS
+ pw_kvs
+ pw_kvs.test_key_value_store
+ pw_string.builder
+ pw_sync.borrow
+ pw_unit_test
+)
+
+pw_add_library(pw_kvs.test_partition STATIC
+ HEADERS
+ public/pw_kvs/flash_partition_with_stats.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_kvs
+ pw_log
+ pw_status
+ SOURCES
+ flash_partition_with_stats.cc
+ PRIVATE_DEPS
+ pw_kvs.config
+)
+
+pw_add_test(pw_kvs.alignment_test
+ SOURCES
+ alignment_test.cc
+ PRIVATE_DEPS
+ pw_kvs
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.checksum_test
+ SOURCES
+ checksum_test.cc
+ PRIVATE_DEPS
+ pw_kvs.crc16
+ pw_kvs
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.converts_to_span_test
+ SOURCES
+ converts_to_span_test.cc
+ PRIVATE_DEPS
+ pw_kvs
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.entry_test
+ SOURCES
+ entry_test.cc
+ PRIVATE_DEPS
+ pw_kvs.crc16
+ pw_kvs.fake_flash
+ pw_kvs
+ pw_bytes
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.entry_cache_test
+ SOURCES
+ entry_cache_test.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs
+ pw_bytes
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.flash_partition_1_stream_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.fake_flash_1_aligned_partition
+ pw_kvs.flash_partition_stream_test
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.flash_partition_1_alignment_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.fake_flash_1_aligned_partition
+ pw_kvs.flash_partition_test_100_iterations
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.flash_partition_16_alignment_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.fake_flash_16_aligned_partition
+ pw_kvs.flash_partition_test_100_iterations
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.flash_partition_64_alignment_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.fake_flash_64_aligned_partition
+ pw_kvs.flash_partition_test_100_iterations
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.flash_partition_256_alignment_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.fake_flash_256_aligned_partition
+ pw_kvs.flash_partition_test_100_iterations
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.flash_partition_256_write_size_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs.fake_flash_1_aligned_partition
+ pw_kvs.flash_partition_test_100_iterations_256_write
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_test
+ SOURCES
+ key_value_store_test.cc
+ PRIVATE_DEPS
+ pw_kvs.config
+ pw_kvs.crc16
+ pw_kvs.fake_flash
+ pw_kvs
+ pw_string.builder
+ pw_bytes
+ pw_checksum
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_1_alignment_flash_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash_1_aligned_partition
+ pw_kvs.key_value_store_initialized_test
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_16_alignment_flash_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash_16_aligned_partition
+ pw_kvs.key_value_store_initialized_test
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_64_alignment_flash_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash_64_aligned_partition
+ pw_kvs.key_value_store_initialized_test
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_256_alignment_flash_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash_256_aligned_partition
+ pw_kvs.key_value_store_initialized_test
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_fuzz_1_alignment_flash_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash_1_aligned_partition
+ pw_kvs.key_value_store_fuzz_test
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_fuzz_64_alignment_flash_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash_64_aligned_partition
+ pw_kvs.key_value_store_fuzz_test
+ GROUPS
+ modules
+ pw_kvs
)
-target_compile_definitions(
- pw_kvs
- PUBLIC PW_FLASH_TEST_SECTORS=6
- PUBLIC PW_FLASH_TEST_SECTOR_SIZE=4096
- PUBLIC PW_FLASH_TEST_ALIGNMENT=1
- PUBLIC PW_FLASH_TEST_ITERATIONS=2
- PUBLIC PW_FLASH_TEST_WRITE_SIZE=256
+pw_add_test(pw_kvs.key_value_store_binary_format_test
+ SOURCES
+ key_value_store_binary_format_test.cc
+ PRIVATE_DEPS
+ pw_kvs.crc16
+ pw_kvs.fake_flash
+ pw_kvs
+ pw_bytes
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_put_test
+ SOURCES
+ key_value_store_put_test.cc
+ PRIVATE_DEPS
+ pw_kvs.crc16
+ pw_kvs.fake_flash
+ pw_kvs
+ pw_kvs.test_partition
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.fake_flash_test_key_value_store_test
+ PRIVATE_DEPS
+ pw_kvs.fake_flash_test_key_value_store
+ pw_kvs.test_key_value_store_test
+ pw_sync.borrow
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_map_test
+ SOURCES
+ key_value_store_map_test.cc
+ PRIVATE_DEPS
+ pw_kvs.crc16
+ pw_kvs.fake_flash
+ pw_kvs
+ pw_kvs.test_partition
+ pw_string.builder
+ pw_checksum
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.sectors_test
+ SOURCES
+ sectors_test.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_test
+ SOURCES
+ key_test.cc
+ PRIVATE_DEPS
+ pw_kvs
+ GROUPS
+ modules
+ pw_kvs
+)
+
+pw_add_test(pw_kvs.key_value_store_wear_test
+ SOURCES
+ key_value_store_wear_test.cc
+ PRIVATE_DEPS
+ pw_kvs.fake_flash
+ pw_kvs
+ pw_kvs.test_partition
+ pw_log
+ GROUPS
+ modules
+ pw_kvs
)
diff --git a/pw_kvs/alignment.cc b/pw_kvs/alignment.cc
index 1192905ec..3259a9ace 100644
--- a/pw_kvs/alignment.cc
+++ b/pw_kvs/alignment.cc
@@ -18,7 +18,7 @@
namespace pw {
-StatusWithSize AlignedWriter::Write(std::span<const std::byte> data) {
+StatusWithSize AlignedWriter::Write(span<const std::byte> data) {
while (!data.empty()) {
size_t to_copy = std::min(write_size_ - bytes_in_buffer_, data.size());
diff --git a/pw_kvs/alignment_test.cc b/pw_kvs/alignment_test.cc
index 71d1a8ebd..81edbb911 100644
--- a/pw_kvs/alignment_test.cc
+++ b/pw_kvs/alignment_test.cc
@@ -125,11 +125,11 @@ constexpr std::string_view kData =
"123456789_123456789_123456789_123456789_123456789_" // 50
"123456789_123456789_123456789_123456789_123456789_"; // 100
-const std::span<const byte> kBytes = std::as_bytes(std::span(kData));
+const span<const byte> kBytes = as_bytes(span(kData));
// The output function checks that the data is properly aligned and matches
// the expected value (should always be 123456789_...).
-OutputToFunction check_against_data([](std::span<const byte> data) {
+OutputToFunction check_against_data([](span<const byte> data) {
EXPECT_EQ(data.size() % kAlignment, 0u);
EXPECT_EQ(kData.substr(0, data.size()),
std::string_view(reinterpret_cast<const char*>(data.data()),
@@ -168,15 +168,15 @@ TEST(AlignedWriter, DestructorFlushes) {
static size_t called_with_bytes;
called_with_bytes = 0;
- OutputToFunction output([](std::span<const byte> data) {
+ OutputToFunction output([](span<const byte> data) {
called_with_bytes += data.size();
return StatusWithSize(data.size());
});
{
AlignedWriterBuffer<64> writer(3, output);
- writer.Write(std::as_bytes(std::span("What is this?")))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(),
+ writer.Write(as_bytes(span("What is this?"))).status());
EXPECT_EQ(called_with_bytes, 0u); // Buffer not full; no output yet.
}
@@ -192,7 +192,7 @@ struct OutputWithErrorInjection final : public Output {
enum { kKeepGoing, kBreakOnNext, kBroken } state = kKeepGoing;
private:
- StatusWithSize DoWrite(std::span<const byte> data) override {
+ StatusWithSize DoWrite(span<const byte> data) override {
switch (state) {
case kKeepGoing:
return StatusWithSize(data.size());
@@ -212,13 +212,11 @@ TEST(AlignedWriter, Write_NoFurtherWritesOnFailure) {
{
AlignedWriterBuffer<4> writer(3, output);
- writer.Write(std::as_bytes(std::span("Everything is fine.")))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(),
+ writer.Write(as_bytes(span("Everything is fine."))).status());
output.state = OutputWithErrorInjection::kBreakOnNext;
EXPECT_EQ(Status::Unknown(),
- writer.Write(std::as_bytes(std::span("No more writes, okay?")))
- .status());
- writer.Flush().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ writer.Write(as_bytes(span("No more writes, okay?"))).status());
}
}
@@ -226,36 +224,34 @@ TEST(AlignedWriter, Write_ReturnsTotalBytesWritten) {
static Status return_status;
return_status = OkStatus();
- OutputToFunction output([](std::span<const byte> data) {
+ OutputToFunction output([](span<const byte> data) {
return StatusWithSize(return_status, data.size());
});
AlignedWriterBuffer<22> writer(10, output);
- StatusWithSize result =
- writer.Write(std::as_bytes(std::span("12345678901"sv)));
+ StatusWithSize result = writer.Write(as_bytes(span("12345678901"sv)));
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(0u, result.size()); // No writes; haven't filled buffer.
- result = writer.Write(std::as_bytes(std::span("2345678901"sv)));
+ result = writer.Write(as_bytes(span("2345678901"sv)));
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(20u, result.size());
return_status = Status::PermissionDenied();
- result = writer.Write(std::as_bytes(std::span("2345678901234567890"sv)));
+ result = writer.Write(as_bytes(span("2345678901234567890"sv)));
EXPECT_EQ(Status::PermissionDenied(), result.status());
EXPECT_EQ(40u, result.size());
}
TEST(AlignedWriter, Flush_Ok_ReturnsTotalBytesWritten) {
OutputToFunction output(
- [](std::span<const byte> data) { return StatusWithSize(data.size()); });
+ [](span<const byte> data) { return StatusWithSize(data.size()); });
AlignedWriterBuffer<4> writer(2, output);
- EXPECT_EQ(OkStatus(),
- writer.Write(std::as_bytes(std::span("12345678901"sv))).status());
+ EXPECT_EQ(OkStatus(), writer.Write(as_bytes(span("12345678901"sv))).status());
StatusWithSize result = writer.Flush();
EXPECT_EQ(OkStatus(), result.status());
@@ -263,13 +259,13 @@ TEST(AlignedWriter, Flush_Ok_ReturnsTotalBytesWritten) {
}
TEST(AlignedWriter, Flush_Error_ReturnsTotalBytesWritten) {
- OutputToFunction output([](std::span<const byte> data) {
+ OutputToFunction output([](span<const byte> data) {
return StatusWithSize::Aborted(data.size());
});
AlignedWriterBuffer<20> writer(10, output);
- EXPECT_EQ(0u, writer.Write(std::as_bytes(std::span("12345678901"sv))).size());
+ EXPECT_EQ(0u, writer.Write(as_bytes(span("12345678901"sv))).size());
StatusWithSize result = writer.Flush();
EXPECT_EQ(Status::Aborted(), result.status());
@@ -282,7 +278,7 @@ class InputWithErrorInjection final : public Input {
void BreakOnIndex(size_t index) { break_on_index_ = index; }
private:
- StatusWithSize DoRead(std::span<byte> data) override {
+ StatusWithSize DoRead(span<byte> data) override {
EXPECT_LE(index_ + data.size(), kBytes.size());
if (index_ + data.size() > kBytes.size()) {
diff --git a/pw_kvs/checksum.cc b/pw_kvs/checksum.cc
index f3e28f5f4..3b4bfc6b0 100644
--- a/pw_kvs/checksum.cc
+++ b/pw_kvs/checksum.cc
@@ -20,7 +20,7 @@ namespace pw::kvs {
using std::byte;
-Status ChecksumAlgorithm::Verify(std::span<const byte> checksum) const {
+Status ChecksumAlgorithm::Verify(span<const byte> checksum) const {
if (checksum.size() < size_bytes()) {
return Status::InvalidArgument();
}
diff --git a/pw_kvs/checksum_test.cc b/pw_kvs/checksum_test.cc
index 17e8f0120..90348a6fa 100644
--- a/pw_kvs/checksum_test.cc
+++ b/pw_kvs/checksum_test.cc
@@ -32,20 +32,19 @@ TEST(Checksum, UpdateAndVerify) {
ChecksumAlgorithm& algo = crc16_algo;
algo.Update(kString.data(), kString.size());
- EXPECT_EQ(OkStatus(), algo.Verify(std::as_bytes(std::span(&kStringCrc, 1))));
+ EXPECT_EQ(OkStatus(), algo.Verify(as_bytes(span(&kStringCrc, 1))));
}
TEST(Checksum, Verify_Failure) {
ChecksumCrc16 algo;
- EXPECT_EQ(Status::DataLoss(),
- algo.Verify(std::as_bytes(std::span(kString.data(), 2))));
+ EXPECT_EQ(Status::DataLoss(), algo.Verify(as_bytes(span(kString.data(), 2))));
}
TEST(Checksum, Verify_InvalidSize) {
ChecksumCrc16 algo;
EXPECT_EQ(Status::InvalidArgument(), algo.Verify({}));
EXPECT_EQ(Status::InvalidArgument(),
- algo.Verify(std::as_bytes(std::span(kString.substr(0, 1)))));
+ algo.Verify(as_bytes(span(kString.substr(0, 1)))));
}
TEST(Checksum, Verify_LargerState_ComparesToTruncatedData) {
@@ -53,17 +52,17 @@ TEST(Checksum, Verify_LargerState_ComparesToTruncatedData) {
ChecksumCrc16 algo;
ASSERT_GT(sizeof(crc), algo.size_bytes());
- algo.Update(std::as_bytes(std::span(kString)));
+ algo.Update(as_bytes(span(kString)));
EXPECT_EQ(OkStatus(), algo.Verify(crc));
}
TEST(Checksum, Reset) {
ChecksumCrc16 crc_algo;
- crc_algo.Update(std::as_bytes(std::span(kString)));
+ crc_algo.Update(as_bytes(span(kString)));
crc_algo.Reset();
- std::span state = crc_algo.Finish();
+ span state = crc_algo.Finish();
EXPECT_EQ(state[0], byte{0xFF});
EXPECT_EQ(state[1], byte{0xFF});
}
@@ -77,13 +76,13 @@ TEST(IgnoreChecksum, NeverUpdate_VerifyWithoutData) {
TEST(IgnoreChecksum, NeverUpdate_VerifyWithData) {
IgnoreChecksum checksum;
- EXPECT_EQ(OkStatus(), checksum.Verify(std::as_bytes(std::span(kString))));
+ EXPECT_EQ(OkStatus(), checksum.Verify(as_bytes(span(kString))));
}
TEST(IgnoreChecksum, AfterUpdate_Verify) {
IgnoreChecksum checksum;
- checksum.Update(std::as_bytes(std::span(kString)));
+ checksum.Update(as_bytes(span(kString)));
EXPECT_EQ(OkStatus(), checksum.Verify({}));
}
@@ -92,7 +91,7 @@ constexpr size_t kAlignment = 10;
constexpr std::string_view kData =
"123456789_123456789_123456789_123456789_123456789_" // 50
"123456789_123456789_123456789_123456789_123456789_"; // 100
-const std::span<const byte> kBytes = std::as_bytes(std::span(kData));
+const span<const byte> kBytes = as_bytes(span(kData));
class PickyChecksum final : public AlignedChecksum<kAlignment, 32> {
public:
@@ -102,7 +101,7 @@ class PickyChecksum final : public AlignedChecksum<kAlignment, 32> {
void FinalizeAligned() override { EXPECT_EQ(kData.size(), size_); }
- void UpdateAligned(std::span<const std::byte> data) override {
+ void UpdateAligned(span<const std::byte> data) override {
ASSERT_EQ(data.size() % kAlignment, 0u);
EXPECT_EQ(kData.substr(0, data.size()),
std::string_view(reinterpret_cast<const char*>(data.data()),
diff --git a/pw_kvs/converts_to_span_test.cc b/pw_kvs/converts_to_span_test.cc
index 33d59c6c6..0b83e9d8c 100644
--- a/pw_kvs/converts_to_span_test.cc
+++ b/pw_kvs/converts_to_span_test.cc
@@ -14,23 +14,22 @@
#include <array>
#include <cstddef>
-#include <span>
#include <string_view>
#include <vector>
#include "gtest/gtest.h"
#include "pw_kvs/internal/span_traits.h"
+#include "pw_span/span.h"
namespace pw::kvs {
namespace {
using internal::make_span;
+
using std::byte;
-using std::dynamic_extent;
-using std::span;
// Test that the ConvertsToSpan trait correctly idenitifies types that convert
-// to std::span.
+// to span.
// Basic types should not convert to span.
struct Foo {};
@@ -70,9 +69,13 @@ static_assert(ConvertsToSpan<const int (&&)[35]>());
struct FakeContainer {
const char* data() const { return nullptr; }
size_t size() const { return 0; }
+
+ const char* begin() const { return nullptr; }
+ const char* end() const { return nullptr; }
};
static_assert(ConvertsToSpan<FakeContainer>());
+
static_assert(ConvertsToSpan<FakeContainer&>());
static_assert(ConvertsToSpan<FakeContainer&&>());
static_assert(ConvertsToSpan<const FakeContainer>());
@@ -88,12 +91,12 @@ static_assert(ConvertsToSpan<const std::string_view&>());
static_assert(ConvertsToSpan<const std::string_view&&>());
// Spans should also convert to span.
-static_assert(ConvertsToSpan<std::span<int>>());
-static_assert(ConvertsToSpan<std::span<byte>>());
-static_assert(ConvertsToSpan<std::span<const int*>>());
-static_assert(ConvertsToSpan<std::span<bool>&&>());
-static_assert(ConvertsToSpan<const std::span<bool>&>());
-static_assert(ConvertsToSpan<std::span<bool>&&>());
+static_assert(ConvertsToSpan<span<int>>());
+static_assert(ConvertsToSpan<span<byte>>());
+static_assert(ConvertsToSpan<span<const int*>>());
+static_assert(ConvertsToSpan<span<bool>&&>());
+static_assert(ConvertsToSpan<const span<bool>&>());
+static_assert(ConvertsToSpan<span<bool>&&>());
// These tests for the make_span function were copied from Chromium:
// https://chromium.googlesource.com/chromium/src/+/main/base/containers/span_unittest.cc
@@ -108,7 +111,7 @@ TEST(SpanTest, MakeSpanFromDataAndSize) {
auto made_span = make_span(vector.data(), vector.size());
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == dynamic_extent, "");
+ static_assert(decltype(made_span)::extent == dynamic_extent);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -124,7 +127,7 @@ TEST(SpanTest, MakeSpanFromPointerPair) {
auto made_span = make_span(vector.data(), vector.data() + vector.size());
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == dynamic_extent, "");
+ static_assert(decltype(made_span)::extent == dynamic_extent);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -136,7 +139,7 @@ TEST(SpanTest, MakeSpanFromConstexprArray) {
constexpr auto made_span = make_span(kArray);
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == 5, "");
+ static_assert(decltype(made_span)::extent == 5);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -148,7 +151,7 @@ TEST(SpanTest, MakeSpanFromStdArray) {
auto made_span = make_span(kArray);
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == 5, "");
+ static_assert(decltype(made_span)::extent == 5);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -160,7 +163,7 @@ TEST(SpanTest, MakeSpanFromConstContainer) {
auto made_span = make_span(vector);
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == dynamic_extent, "");
+ static_assert(decltype(made_span)::extent == dynamic_extent);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -174,7 +177,7 @@ TEST(SpanTest, MakeStaticSpanFromConstContainer) {
auto made_span = make_span<5>(vector);
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == 5, "");
+ static_assert(decltype(made_span)::extent == 5);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -188,7 +191,7 @@ TEST(SpanTest, MakeSpanFromContainer) {
auto made_span = make_span(vector);
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == dynamic_extent, "");
+ static_assert(decltype(made_span)::extent == dynamic_extent);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -202,7 +205,7 @@ TEST(SpanTest, MakeStaticSpanFromContainer) {
auto made_span = make_span<5>(vector);
EXPECT_EQ(expected_span.data(), make_span<5>(vector).data());
EXPECT_EQ(expected_span.size(), make_span<5>(vector).size());
- static_assert(decltype(make_span<5>(vector))::extent == 5, "");
+ static_assert(decltype(make_span<5>(vector))::extent == 5);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -232,7 +235,7 @@ TEST(SpanTest, MakeSpanFromRValueContainer) {
auto made_span = make_span(static_cast<std::vector<int>&&>(vector));
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == dynamic_extent, "");
+ static_assert(decltype(made_span)::extent == dynamic_extent);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
@@ -250,7 +253,7 @@ TEST(SpanTest, MakeStaticSpanFromRValueContainer) {
auto made_span = make_span<5>(static_cast<std::vector<int>&&>(vector));
EXPECT_EQ(expected_span.data(), made_span.data());
EXPECT_EQ(expected_span.size(), made_span.size());
- static_assert(decltype(made_span)::extent == 5, "");
+ static_assert(decltype(made_span)::extent == 5);
static_assert(
std::is_same<decltype(expected_span), decltype(made_span)>::value,
"the type of made_span differs from expected_span!");
diff --git a/pw_kvs/docs.rst b/pw_kvs/docs.rst
index fd2199654..4f4452211 100644
--- a/pw_kvs/docs.rst
+++ b/pw_kvs/docs.rst
@@ -1,9 +1,8 @@
.. _module-pw_kvs:
-------
+======
pw_kvs
-------
-
+======
.. note::
The documentation for this module is currently under construction.
@@ -11,9 +10,9 @@ pw_kvs
persistent storage system with integrated wear-leveling that serves as a
relatively lightweight alternative to a file system.
+-------------
KeyValueStore
-=============
-
+-------------
The KVS system stores key and value data pairs. The key value pairs are stored
in `flash memory`_ as a `key-value entry`_ (KV entry) that consists of a
header/metadata, the key data, and value data. KV entries are accessed through
@@ -54,20 +53,19 @@ sector to be garbage collected to a different sector and then erasing the
sector.
Flash Memory
--------------
-
+============
The flash storage used by KVS is comprised of two layers, FlashMemory and
FlashPartition.
FlashMemory is the lower level that manages the raw read/write/erase of the
flash memory device.
-FlashPartition is a portion of a FlashMemory. A FlashMemory may have multiple
-FlashPartitions that represent different parts of the FlashMemory - such as
-partitions for KVS, OTA, snapshots/crashlogs, etc. Each FlashPartition has its
-own separate logical address space starting from zero to size of the partition.
-FlashPartition logical address does not always map directly to FlashMemory
-addresses due to partition encryption, sector headers, etc.
+FlashPartition is a subset of a FlashMemory. A FlashMemory may have one or
+multiple FlashPartitions that represent different parts of the FlashMemory -
+such as partitions for KVS, OTA, snapshots/crashlogs, etc. Each FlashPartition
+has its own separate logical address space starting from zero to size bytes of
+the partition. FlashPartition logical address does not always map directly to
+FlashMemory addresses due to partition encryption, sector headers, etc.
Writes to flash must have a start address that is a multiple of the flash
write alignment. Write size must also be a multiple of flash write alignment.
@@ -88,15 +86,14 @@ sectors into larger logical sectors.
FlashPartition supports access via NonSeekableWriter and SeekableReader.
Size report
------------
+===========
The following size report showcases the memory usage of the KVS and
FlashPartition.
.. include:: kvs_size
Storage Allocation
-------------------
-
+==================
KVS requires more storage space than the size of the key-value data stored.
This is due to the always free sector required for garbage collection and the
"write and garbage collect later" approach KVS uses.
@@ -111,8 +108,7 @@ The flash storage used by KVS is multiplied by `redundancy`_ used. A redundancy
of 2 will use twice the storage.
Key-Value Entry
----------------
-
+===============
Each key-value (KV) entry consists of a header/metadata, the key data, and
value data. Individual KV entries are contained within a single flash sector
(do not cross sector boundaries). Because of this the maximum KV entry size is
@@ -132,8 +128,7 @@ remains unaltered “on-disk” but is considered “stale”. It is garbage col
at some future time.
Redundancy
-----------
-
+==========
KVS supports storing redundant copies of KV entries. For a given redundancy
level (N), N total copies of each KV entry are stored. Redundant copies are
always stored in different sectors. This protects against corruption or even
@@ -143,8 +138,7 @@ Redundancy increases flash usage proportional to the redundancy level. The RAM
usage for KVS internal state has a small increase with redundancy.
Garbage Collection
-------------------
-
+==================
Storage space occupied by stale KV entries is reclaimed and made available
for reuse through a garbage collection process. The base garbage collection
operation is done to reclaim one sector at a time.
@@ -157,16 +151,16 @@ always free sector is rotated as part of the KVS wear leveling.
Full Maintenance does garbage collection of all sectors except those that have
current valid KV entries.
-Heavy Maintenance does garbage collection of all sectors. Use strong caution
-when doing Heavy Maintenance as it can, compared to Full Maintenance, result
-in a significant amount of moving valid entries,
+Heavy Maintenance does garbage collection of all sectors, including removing
+entries for deleted keys. Use strong caution when doing Heavy Maintenance as it
+can, compared to Full Maintenance, result in a significant amount of moving
+valid entries,
Garbage collection can be performed by request of higher level software or
automatically as needed to make space available to write new entries.
Flash wear management
----------------------
-
+=====================
Wear leveling is accomplished by cycling selection of the next sector to write
to. This cycling spreads flash wear across all free sectors so that no one
sector is prematurely worn out.
@@ -187,3 +181,16 @@ Wear leveling through cycling selection of next sector to write
* Sectors with already written key-values that are not modified will remain in
the original sector and not participate in wear-leveling, so long as the
key-values in the sector remain unchanged
+
+Configuration
+=============
+.. c:macro:: PW_KVS_MAX_FLASH_ALIGNMENT
+
+ The maximum flash alignment supported.
+
+.. c:macro:: PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
+
+ Whether to remove deleted keys in heavy maintanence. This feature costs some
+ code size (>1KB) and is only necessary if arbitrary key names are used.
+ Without this feature, deleted key entries can fill the KVS, making it
+ impossible to add more keys, even though most keys are deleted.
diff --git a/pw_kvs/entry.cc b/pw_kvs/entry.cc
index fc01b0c58..6e6fb0f4e 100644
--- a/pw_kvs/entry.cc
+++ b/pw_kvs/entry.cc
@@ -42,7 +42,7 @@ Status Entry::Read(FlashPartition& partition,
EntryHeader header;
PW_TRY(partition.Read(address, sizeof(header), &header));
- if (partition.AppearsErased(std::as_bytes(std::span(&header.magic, 1)))) {
+ if (partition.AppearsErased(as_bytes(span(&header.magic, 1)))) {
return Status::NotFound();
}
if (header.key_length_bytes > kMaxKeyLength) {
@@ -77,7 +77,7 @@ Entry::Entry(FlashPartition& partition,
Address address,
const EntryFormat& format,
Key key,
- std::span<const byte> value,
+ span<const byte> value,
uint16_t value_size_bytes,
uint32_t transaction_id)
: Entry(&partition,
@@ -91,20 +91,19 @@ Entry::Entry(FlashPartition& partition,
.value_size_bytes = value_size_bytes,
.transaction_id = transaction_id}) {
if (checksum_algo_ != nullptr) {
- std::span<const byte> checksum = CalculateChecksum(key, value);
+ span<const byte> checksum = CalculateChecksum(key, value);
std::memcpy(&header_.checksum,
checksum.data(),
std::min(checksum.size(), sizeof(header_.checksum)));
}
}
-StatusWithSize Entry::Write(Key key, std::span<const byte> value) const {
+StatusWithSize Entry::Write(Key key, span<const byte> value) const {
FlashPartition::Output flash(partition(), address_);
- return AlignedWrite<kWriteBufferSize>(flash,
- alignment_bytes(),
- {std::as_bytes(std::span(&header_, 1)),
- std::as_bytes(std::span(key)),
- value});
+ return AlignedWrite<kWriteBufferSize>(
+ flash,
+ alignment_bytes(),
+ {as_bytes(span(&header_, 1)), as_bytes(span(key)), value});
}
Status Entry::Update(const EntryFormat& new_format,
@@ -141,8 +140,7 @@ StatusWithSize Entry::Copy(Address new_address) const {
return writer.Flush();
}
-StatusWithSize Entry::ReadValue(std::span<byte> buffer,
- size_t offset_bytes) const {
+StatusWithSize Entry::ReadValue(span<byte> buffer, size_t offset_bytes) const {
if (offset_bytes > value_size()) {
return StatusWithSize::OutOfRange();
}
@@ -161,7 +159,7 @@ StatusWithSize Entry::ReadValue(std::span<byte> buffer,
return StatusWithSize(read_size);
}
-Status Entry::ValueMatches(std::span<const std::byte> value) const {
+Status Entry::ValueMatches(span<const std::byte> value) const {
if (value_size() != value.size_bytes()) {
return Status::NotFound();
}
@@ -173,7 +171,7 @@ Status Entry::ValueMatches(std::span<const std::byte> value) const {
std::array<std::byte, 2 * kMinAlignmentBytes> buffer;
while (address < end) {
const size_t read_size = std::min(size_t(end - address), buffer.size());
- PW_TRY(partition_->Read(address, std::span(buffer).first(read_size)));
+ PW_TRY(partition_->Read(address, span(buffer).first(read_size)));
if (std::memcmp(buffer.data(), value_ptr, read_size) != 0) {
return Status::NotFound();
@@ -186,7 +184,7 @@ Status Entry::ValueMatches(std::span<const std::byte> value) const {
return OkStatus();
}
-Status Entry::VerifyChecksum(Key key, std::span<const byte> value) const {
+Status Entry::VerifyChecksum(Key key, span<const byte> value) const {
if (checksum_algo_ == nullptr) {
return header_.checksum == 0 ? OkStatus() : Status::DataLoss();
}
@@ -257,8 +255,8 @@ void Entry::DebugLog() const {
PW_LOG_DEBUG(" Alignment = 0x%x", unsigned(alignment_bytes()));
}
-std::span<const byte> Entry::CalculateChecksum(
- const Key key, std::span<const byte> value) const {
+span<const byte> Entry::CalculateChecksum(const Key key,
+ span<const byte> value) const {
checksum_algo_->Reset();
{
@@ -266,7 +264,7 @@ std::span<const byte> Entry::CalculateChecksum(
header_for_checksum.checksum = 0;
checksum_algo_->Update(&header_for_checksum, sizeof(header_for_checksum));
- checksum_algo_->Update(std::as_bytes(std::span(key)));
+ checksum_algo_->Update(as_bytes(span(key)));
checksum_algo_->Update(value);
}
@@ -293,7 +291,7 @@ Status Entry::CalculateChecksumFromFlash() {
std::array<std::byte, 2 * kMinAlignmentBytes> buffer;
while (address < end) {
const size_t read_size = std::min(size_t(end - address), buffer.size());
- PW_TRY(partition_->Read(address, std::span(buffer).first(read_size)));
+ PW_TRY(partition_->Read(address, span(buffer).first(read_size)));
checksum_algo_->Update(buffer.data(), read_size);
address += read_size;
@@ -301,7 +299,7 @@ Status Entry::CalculateChecksumFromFlash() {
AddPaddingBytesToChecksum();
- std::span checksum = checksum_algo_->Finish();
+ span checksum = checksum_algo_->Finish();
std::memcpy(&header_.checksum,
checksum.data(),
std::min(checksum.size(), sizeof(header_.checksum)));
diff --git a/pw_kvs/entry_cache.cc b/pw_kvs/entry_cache.cc
index ece008b06..d066a1898 100644
--- a/pw_kvs/entry_cache.cc
+++ b/pw_kvs/entry_cache.cc
@@ -19,6 +19,7 @@
#include <cinttypes>
+#include "pw_assert/check.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/internal/entry.h"
#include "pw_kvs/internal/hash.h"
@@ -43,7 +44,7 @@ void EntryMetadata::RemoveAddress(Address address_to_remove) {
// Remove the back entry of the address list.
addresses_.back() = kNoAddress;
- addresses_ = std::span(addresses_.begin(), addresses_.size() - 1);
+ addresses_ = span(addresses_.begin(), addresses_.size() - 1);
break;
}
}
@@ -122,13 +123,39 @@ EntryMetadata EntryCache::AddNew(const KeyDescriptor& descriptor,
// TODO(hepler): DCHECK(!full());
Address* first_address = ResetAddresses(descriptors_.size(), address);
descriptors_.push_back(descriptor);
- return EntryMetadata(descriptors_.back(), std::span(first_address, 1));
+ return EntryMetadata(descriptors_.back(), span(first_address, 1));
}
-// TODO: This method is the trigger of the O(valid_entries * all_entries) time
-// complexity for reading. At some cost to memory, this could be optimized by
-// using a hash table instead of scanning, but in practice this should be fine
-// for a small number of keys
+// Removes an existing entry from the cache
+EntryCache::iterator EntryCache::RemoveEntry(iterator& entry_it) {
+ PW_DCHECK_PTR_EQ(entry_it.entry_cache_, this);
+
+ const unsigned int index_to_remove =
+ entry_it.metadata_.descriptor_ - &descriptors_.front();
+ const KeyDescriptor last_desc = descriptors_[descriptors_.size() - 1];
+
+ // Since order is not important, this copies the last descriptor into the
+ // deleted descriptor's space and then pops the last entry.
+ Address* addresses_at_end = first_address(descriptors_.size() - 1);
+
+ if (index_to_remove < descriptors_.size() - 1) {
+ Address* addresses_to_remove = first_address(index_to_remove);
+ for (unsigned int i = 0; i < redundancy_; i++) {
+ addresses_to_remove[i] = addresses_at_end[i];
+ }
+ descriptors_[index_to_remove] = last_desc;
+ }
+
+ // Erase the last entry since it was copied over the entry being deleted.
+ descriptors_.pop_back();
+
+ return {this, descriptors_.data() + index_to_remove};
+}
+
+// TODO(hepler): This method is the trigger of the O(valid_entries *
+// all_entries) time complexity for reading. At some cost to memory, this could
+// be optimized by using a hash table instead of scanning, but in practice this
+// should be fine for a small number of keys
Status EntryCache::AddNewOrUpdateExisting(const KeyDescriptor& descriptor,
Address address,
size_t sector_size_bytes) const {
@@ -213,8 +240,7 @@ void EntryCache::AddAddressIfRoom(size_t descriptor_index,
}
}
-std::span<EntryCache::Address> EntryCache::addresses(
- size_t descriptor_index) const {
+span<EntryCache::Address> EntryCache::addresses(size_t descriptor_index) const {
Address* const addresses = first_address(descriptor_index);
size_t size = 0;
@@ -222,7 +248,7 @@ std::span<EntryCache::Address> EntryCache::addresses(
size += 1;
}
- return std::span(addresses, size);
+ return span(addresses, size);
}
EntryCache::Address* EntryCache::ResetAddresses(size_t descriptor_index,
diff --git a/pw_kvs/entry_test.cc b/pw_kvs/entry_test.cc
index 76def5443..5dbe25aab 100644
--- a/pw_kvs/entry_test.cc
+++ b/pw_kvs/entry_test.cc
@@ -14,7 +14,6 @@
#include "pw_kvs/internal/entry.h"
-#include <span>
#include <string_view>
#include "gtest/gtest.h"
@@ -25,6 +24,7 @@
#include "pw_kvs/fake_flash_memory.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/format.h"
+#include "pw_span/span.h"
namespace pw::kvs::internal {
namespace {
@@ -50,8 +50,8 @@ TEST(Entry, Size_RoundsUpToAlignment) {
const size_t align = AlignUp(alignment_bytes, Entry::kMinAlignmentBytes);
for (size_t value : {size_t(0), align - 1, align, align + 1, 2 * align}) {
- Entry entry =
- Entry::Valid(partition, 0, kFormat, "k", {nullptr, value}, 0);
+ Entry entry = Entry::Valid(
+ partition, 0, kFormat, "k", {static_cast<byte*>(nullptr), value}, 0);
ASSERT_EQ(AlignUp(sizeof(EntryHeader) + 1 /* key */ + value, align),
entry.size());
@@ -66,8 +66,8 @@ TEST(Entry, Construct_ValidEntry) {
FakeFlashMemoryBuffer<64, 2> flash(16);
FlashPartition partition(&flash, 0, flash.sector_count());
- auto entry = Entry::Valid(
- partition, 1, kFormat, "k", std::as_bytes(std::span("123")), 9876);
+ auto entry =
+ Entry::Valid(partition, 1, kFormat, "k", as_bytes(span("123")), 9876);
EXPECT_FALSE(entry.deleted());
EXPECT_EQ(entry.magic(), kFormat.magic);
@@ -148,7 +148,7 @@ TEST_F(ValidEntryInFlash, ReadKey) {
TEST_F(ValidEntryInFlash, ReadValue) {
char value[32] = {};
- auto result = entry_.ReadValue(std::as_writable_bytes(std::span(value)));
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)));
ASSERT_EQ(OkStatus(), result.status());
EXPECT_EQ(result.size(), entry_.value_size());
@@ -157,7 +157,7 @@ TEST_F(ValidEntryInFlash, ReadValue) {
TEST_F(ValidEntryInFlash, ReadValue_BufferTooSmall) {
char value[3] = {};
- auto result = entry_.ReadValue(std::as_writable_bytes(std::span(value)));
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)));
ASSERT_EQ(Status::ResourceExhausted(), result.status());
EXPECT_EQ(3u, result.size());
@@ -168,7 +168,7 @@ TEST_F(ValidEntryInFlash, ReadValue_BufferTooSmall) {
TEST_F(ValidEntryInFlash, ReadValue_WithOffset) {
char value[3] = {};
- auto result = entry_.ReadValue(std::as_writable_bytes(std::span(value)), 3);
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)), 3);
ASSERT_EQ(OkStatus(), result.status());
EXPECT_EQ(3u, result.size());
@@ -179,7 +179,7 @@ TEST_F(ValidEntryInFlash, ReadValue_WithOffset) {
TEST_F(ValidEntryInFlash, ReadValue_WithOffset_BufferTooSmall) {
char value[1] = {};
- auto result = entry_.ReadValue(std::as_writable_bytes(std::span(value)), 4);
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)), 4);
ASSERT_EQ(Status::ResourceExhausted(), result.status());
EXPECT_EQ(1u, result.size());
@@ -188,7 +188,7 @@ TEST_F(ValidEntryInFlash, ReadValue_WithOffset_BufferTooSmall) {
TEST_F(ValidEntryInFlash, ReadValue_WithOffset_EmptyRead) {
char value[16] = {'?'};
- auto result = entry_.ReadValue(std::as_writable_bytes(std::span(value)), 6);
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)), 6);
ASSERT_EQ(OkStatus(), result.status());
EXPECT_EQ(0u, result.size());
@@ -197,7 +197,7 @@ TEST_F(ValidEntryInFlash, ReadValue_WithOffset_EmptyRead) {
TEST_F(ValidEntryInFlash, ReadValue_WithOffset_PastEnd) {
char value[16] = {};
- auto result = entry_.ReadValue(std::as_writable_bytes(std::span(value)), 7);
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)), 7);
EXPECT_EQ(Status::OutOfRange(), result.status());
EXPECT_EQ(0u, result.size());
@@ -265,7 +265,7 @@ TEST_F(TombstoneEntryInFlash, ReadKey) {
TEST_F(TombstoneEntryInFlash, ReadValue) {
char value[32] = {};
- auto result = entry_.ReadValue(std::as_writable_bytes(std::span(value)));
+ auto result = entry_.ReadValue(as_writable_bytes(span(value)));
ASSERT_EQ(OkStatus(), result.status());
EXPECT_EQ(0u, result.size());
@@ -379,7 +379,7 @@ TEST_F(ValidEntryInFlash, Copy_ReadError) {
EXPECT_EQ(0u, result.size());
}
-constexpr uint32_t ByteSum(std::span<const byte> bytes, uint32_t value = 0) {
+constexpr uint32_t ByteSum(span<const byte> bytes, uint32_t value = 0) {
for (byte b : bytes) {
value += unsigned(b);
}
@@ -389,12 +389,11 @@ constexpr uint32_t ByteSum(std::span<const byte> bytes, uint32_t value = 0) {
// Sums the bytes, adding one to each byte so that zeroes change the checksum.
class ChecksumSummation final : public ChecksumAlgorithm {
public:
- ChecksumSummation()
- : ChecksumAlgorithm(std::as_bytes(std::span(&sum_, 1))), sum_(0) {}
+ ChecksumSummation() : ChecksumAlgorithm(as_bytes(span(&sum_, 1))), sum_(0) {}
void Reset() override { sum_ = 0; }
- void Update(std::span<const byte> data) override {
+ void Update(span<const byte> data) override {
for (byte b : data) {
sum_ += unsigned(b) + 1; // Add 1 so zero-value bytes affect checksum.
}
@@ -456,7 +455,7 @@ TEST_F(ValidEntryInFlash, UpdateAndCopy_DifferentFormatSmallerAlignment) {
EXPECT_EQ(kTransactionId1 + 1, new_entry.transaction_id());
}
-TEST(ValidEntryInFlash, UpdateAndCopy_DifferentFormatSameAlignment) {
+TEST(Entry, UpdateAndCopy_DifferentFormatSameAlignment) {
// Use 32-bit alignment, the same as the original entry's alignment.
FakeFlashMemoryBuffer<1024, 4> flash(kEntry1);
FlashPartition partition(&flash, 0, 4, 32);
@@ -482,7 +481,7 @@ TEST(ValidEntryInFlash, UpdateAndCopy_DifferentFormatSameAlignment) {
EXPECT_EQ(kTransactionId1 + 1, new_entry.transaction_id());
}
-TEST(ValidEntryInFlash, UpdateAndCopy_DifferentFormatLargerAlignment) {
+TEST(Entry, UpdateAndCopy_DifferentFormatLargerAlignment) {
// Use 64-bit alignment, larger than the original entry's alignment.
FakeFlashMemoryBuffer<1024, 4> flash(kEntry1);
FlashPartition partition(&flash, 0, 4, 64);
diff --git a/pw_kvs/fake_flash_memory.cc b/pw_kvs/fake_flash_memory.cc
index 0f9b60159..fed4222dc 100644
--- a/pw_kvs/fake_flash_memory.cc
+++ b/pw_kvs/fake_flash_memory.cc
@@ -22,7 +22,7 @@
namespace pw::kvs {
-Status FlashError::Check(std::span<FlashError> errors,
+Status FlashError::Check(span<FlashError> errors,
FlashMemory::Address address,
size_t size) {
for (auto& error : errors) {
@@ -79,8 +79,7 @@ Status FakeFlashMemory::Erase(Address address, size_t num_sectors) {
return OkStatus();
}
-StatusWithSize FakeFlashMemory::Read(Address address,
- std::span<std::byte> output) {
+StatusWithSize FakeFlashMemory::Read(Address address, span<std::byte> output) {
if (address + output.size() >= sector_count() * size_bytes()) {
return StatusWithSize::OutOfRange();
}
@@ -92,7 +91,7 @@ StatusWithSize FakeFlashMemory::Read(Address address,
}
StatusWithSize FakeFlashMemory::Write(Address address,
- std::span<const std::byte> data) {
+ span<const std::byte> data) {
if (address % alignment_bytes() != 0 ||
data.size() % alignment_bytes() != 0) {
PW_LOG_ERROR("Unaligned write; address %x, size %u B, alignment %u",
diff --git a/pw_kvs/fake_flash_test_key_value_store.cc b/pw_kvs/fake_flash_test_key_value_store.cc
index e6f177816..840eb722f 100644
--- a/pw_kvs/fake_flash_test_key_value_store.cc
+++ b/pw_kvs/fake_flash_test_key_value_store.cc
@@ -72,7 +72,7 @@ sync::Borrowable<KeyValueStore> borrowable_kvs(test_kvs,
sync::Borrowable<KeyValueStore>& TestKvs() {
if (!test_kvs.initialized()) {
- test_kvs.Init().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ test_kvs.Init().IgnoreError(); // TODO(b/242598609): Handle Status properly
}
return borrowable_kvs;
diff --git a/pw_kvs/flash_memory.cc b/pw_kvs/flash_memory.cc
index 217ad16ef..a564d8f74 100644
--- a/pw_kvs/flash_memory.cc
+++ b/pw_kvs/flash_memory.cc
@@ -69,13 +69,13 @@ StatusWithSize FlashPartition::Reader::DoRead(ByteSpan data) {
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
-StatusWithSize FlashPartition::Output::DoWrite(std::span<const byte> data) {
+StatusWithSize FlashPartition::Output::DoWrite(span<const byte> data) {
PW_TRY_WITH_SIZE(flash_.Write(address_, data));
address_ += data.size();
return StatusWithSize(data.size());
}
-StatusWithSize FlashPartition::Input::DoRead(std::span<byte> data) {
+StatusWithSize FlashPartition::Input::DoRead(span<byte> data) {
StatusWithSize result = flash_.Read(address_, data);
address_ += result.size();
return result;
@@ -83,14 +83,14 @@ StatusWithSize FlashPartition::Input::DoRead(std::span<byte> data) {
FlashPartition::FlashPartition(
FlashMemory* flash,
- uint32_t start_sector_index,
- uint32_t sector_count,
+ uint32_t flash_start_sector_index,
+ uint32_t flash_sector_count,
uint32_t alignment_bytes, // Defaults to flash alignment
PartitionPermission permission)
: flash_(*flash),
- start_sector_index_(start_sector_index),
- sector_count_(sector_count),
+ flash_sector_count_(flash_sector_count),
+ flash_start_sector_index_(flash_start_sector_index),
alignment_bytes_(
alignment_bytes == 0
? flash_.alignment_bytes()
@@ -115,13 +115,12 @@ Status FlashPartition::Erase(Address address, size_t num_sectors) {
return flash_.Erase(PartitionToFlashAddress(address), num_sectors);
}
-StatusWithSize FlashPartition::Read(Address address, std::span<byte> output) {
+StatusWithSize FlashPartition::Read(Address address, span<byte> output) {
PW_TRY_WITH_SIZE(CheckBounds(address, output.size()));
return flash_.Read(PartitionToFlashAddress(address), output);
}
-StatusWithSize FlashPartition::Write(Address address,
- std::span<const byte> data) {
+StatusWithSize FlashPartition::Write(Address address, span<const byte> data) {
if (permission_ == PartitionPermission::kReadOnly) {
return StatusWithSize::PermissionDenied();
}
@@ -161,7 +160,7 @@ Status FlashPartition::IsRegionErased(Address source_flash_address,
PW_TRY(
Read(source_flash_address + offset, read_size, read_buffer).status());
- for (byte b : std::span(read_buffer, read_size)) {
+ for (byte b : span(read_buffer, read_size)) {
if (b != erased_byte) {
// Detected memory chunk is not entirely erased
return OkStatus();
@@ -175,7 +174,7 @@ Status FlashPartition::IsRegionErased(Address source_flash_address,
return OkStatus();
}
-bool FlashPartition::AppearsErased(std::span<const byte> data) const {
+bool FlashPartition::AppearsErased(span<const byte> data) const {
byte erased_content = flash_.erased_memory_content();
for (byte b : data) {
if (b != erased_content) {
diff --git a/pw_kvs/flash_partition_stream_test.cc b/pw_kvs/flash_partition_stream_test.cc
index 47fd974db..09c1376b3 100644
--- a/pw_kvs/flash_partition_stream_test.cc
+++ b/pw_kvs/flash_partition_stream_test.cc
@@ -16,7 +16,6 @@
#include <array>
#include <cstddef>
#include <cstring>
-#include <span>
#include "gtest/gtest.h"
#include "public/pw_kvs/flash_memory.h"
@@ -25,6 +24,7 @@
#include "pw_kvs_private/config.h"
#include "pw_log/log.h"
#include "pw_random/xor_shift.h"
+#include "pw_span/span.h"
#if PW_CXX_STANDARD_IS_SUPPORTED(17)
@@ -49,7 +49,7 @@ class FlashStreamTest : public ::testing::Test {
std::memset(buffer_span.data(),
static_cast<int>(flash_.erased_memory_content()),
buffer_span.size());
- ASSERT_EQ(OkStatus(), rng.Get(buffer_span).status());
+ rng.Get(buffer_span);
}
void VerifyFlash(ConstByteSpan verify_bytes, size_t offset = 0) {
@@ -75,8 +75,8 @@ class FlashStreamTest : public ::testing::Test {
}
void DoWriteInChunks(size_t chunk_write_size_bytes, uint64_t seed) {
- InitBufferToRandom(std::span(source_buffer_), seed);
- ConstByteSpan write_data = std::span(source_buffer_);
+ InitBufferToRandom(span(source_buffer_), seed);
+ ConstByteSpan write_data = span(source_buffer_);
ASSERT_EQ(OkStatus(), partition_.Erase());
@@ -96,7 +96,7 @@ class FlashStreamTest : public ::testing::Test {
ASSERT_EQ(writer.ConservativeWriteLimit(), write_data.size_bytes());
}
- VerifyFlashContent(std::span(source_buffer_));
+ VerifyFlashContent(span(source_buffer_));
}
void DoReadInChunks(size_t chunk_read_size_bytes,
@@ -119,7 +119,7 @@ class FlashStreamTest : public ::testing::Test {
size_t chunk_size = std::min(chunk_read_size_bytes, bytes_to_read);
- ByteSpan read_chunk = std::span(source_buffer_).first(chunk_size);
+ ByteSpan read_chunk = span(source_buffer_).first(chunk_size);
InitBufferToFill(read_chunk, 0);
ASSERT_EQ(read_chunk.size_bytes(), chunk_size);
@@ -211,7 +211,7 @@ TEST_F(FlashStreamTest, Read_Multiple_Seeks) {
ASSERT_EQ(reader.Seek(start_offset), OkStatus());
ASSERT_EQ(start_offset, reader.Tell());
- ByteSpan read_chunk = std::span(source_buffer_).first(kSeekReadSizeBytes);
+ ByteSpan read_chunk = span(source_buffer_).first(kSeekReadSizeBytes);
InitBufferToFill(read_chunk, 0);
auto result = reader.Read(read_chunk);
@@ -238,7 +238,7 @@ TEST_F(FlashStreamTest, Read_Seek_Forward_and_Back) {
ASSERT_EQ(reader.Seek(start_offset), OkStatus());
ASSERT_EQ(start_offset, reader.Tell());
- ByteSpan read_chunk = std::span(source_buffer_).first(kSeekReadSizeBytes);
+ ByteSpan read_chunk = span(source_buffer_).first(kSeekReadSizeBytes);
InitBufferToFill(read_chunk, 0);
auto result = reader.Read(read_chunk);
@@ -255,7 +255,7 @@ TEST_F(FlashStreamTest, Read_Seek_Forward_and_Back) {
ASSERT_EQ(start_offset, reader.Tell());
ASSERT_GE(reader.ConservativeReadLimit(), kSeekReadSizeBytes);
- ByteSpan read_chunk = std::span(source_buffer_).first(kSeekReadSizeBytes);
+ ByteSpan read_chunk = span(source_buffer_).first(kSeekReadSizeBytes);
InitBufferToFill(read_chunk, 0);
auto result = reader.Read(read_chunk);
@@ -273,8 +273,8 @@ TEST_F(FlashStreamTest, Read_Past_End) {
static const size_t kBytesForFinalRead = 50;
- ByteSpan read_chunk = std::span(source_buffer_)
- .first(source_buffer_.size() - kBytesForFinalRead);
+ ByteSpan read_chunk =
+ span(source_buffer_).first(source_buffer_.size() - kBytesForFinalRead);
auto result = reader.Read(read_chunk);
ASSERT_EQ(result.status(), OkStatus());
@@ -304,7 +304,7 @@ TEST_F(FlashStreamTest, Read_Past_End_After_Seek) {
ASSERT_EQ(start_offset, reader.Tell());
ASSERT_EQ(reader.ConservativeReadLimit(), kBytesForFinalRead);
- ByteSpan read_chunk = std::span(source_buffer_);
+ ByteSpan read_chunk = span(source_buffer_);
auto result = reader.Read(read_chunk);
ASSERT_EQ(result.status(), OkStatus());
ASSERT_EQ(result.value().size_bytes(), kBytesForFinalRead);
@@ -318,7 +318,7 @@ TEST_F(FlashStreamTest, Read_Out_Of_Range) {
InitBufferToRandom(flash_.buffer(), 0x531de176);
FlashPartition::Reader reader(partition_);
- ByteSpan read_chunk = std::span(source_buffer_);
+ ByteSpan read_chunk = span(source_buffer_);
auto result = reader.Read(read_chunk);
ASSERT_EQ(result.status(), OkStatus());
@@ -338,7 +338,7 @@ TEST_F(FlashStreamTest, Read_Out_Of_Range_After_Seek) {
InitBufferToRandom(flash_.buffer(), 0x8c94566);
FlashPartition::Reader reader(partition_);
- ByteSpan read_chunk = std::span(source_buffer_);
+ ByteSpan read_chunk = span(source_buffer_);
ASSERT_EQ(reader.Seek(flash_.buffer().size_bytes()), OkStatus());
ASSERT_EQ(reader.Tell(), flash_.buffer().size_bytes());
diff --git a/pw_kvs/flash_partition_test.cc b/pw_kvs/flash_partition_test.cc
index fbe4503c6..3d2cc680b 100644
--- a/pw_kvs/flash_partition_test.cc
+++ b/pw_kvs/flash_partition_test.cc
@@ -13,13 +13,13 @@
// the License.
#include <algorithm>
-#include <span>
#include "gtest/gtest.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/flash_test_partition.h"
#include "pw_kvs_private/config.h"
#include "pw_log/log.h"
+#include "pw_span/span.h"
namespace pw::kvs::PartitionTest {
namespace {
@@ -59,7 +59,7 @@ void WriteData(FlashPartition& partition, uint8_t fill_byte) {
for (size_t chunk_index = 0; chunk_index < chunks_per_sector;
chunk_index++) {
StatusWithSize status =
- partition.Write(address, as_bytes(std::span(test_data, write_size)));
+ partition.Write(address, as_bytes(span(test_data, write_size)));
ASSERT_EQ(OkStatus(), status.status());
ASSERT_EQ(write_size, status.size());
address += write_size;
@@ -145,7 +145,7 @@ TEST(FlashPartitionTest, EraseTest) {
const size_t block_size =
std::min(sizeof(test_data), test_partition.sector_size_bytes());
- auto data_span = std::span(test_data, block_size);
+ auto data_span = span(test_data, block_size);
ASSERT_EQ(OkStatus(), test_partition.Erase(0, test_partition.sector_count()));
@@ -206,7 +206,7 @@ TEST(FlashPartitionTest, AlignmentCheck) {
#define TESTING_CHECK_FAILURES_IS_SUPPORTED 0
#if TESTING_CHECK_FAILURES_IS_SUPPORTED
-// TODO: Ensure that this test triggers an assert.
+// TODO(davidrogers): Ensure that this test triggers an assert.
TEST(FlashPartitionTest, BadWriteAddressAlignment) {
FlashPartition& test_partition = FlashTestPartition();
@@ -219,7 +219,7 @@ TEST(FlashPartitionTest, BadWriteAddressAlignment) {
test_partition.Write(1, source_data);
}
-// TODO: Ensure that this test triggers an assert.
+// TODO(davidrogers): Ensure that this test triggers an assert.
TEST(FlashPartitionTest, BadWriteSizeAlignment) {
FlashPartition& test_partition = FlashTestPartition();
@@ -232,7 +232,7 @@ TEST(FlashPartitionTest, BadWriteSizeAlignment) {
test_partition.Write(0, source_data);
}
-// TODO: Ensure that this test triggers an assert.
+// TODO(davidrogers): Ensure that this test triggers an assert.
TEST(FlashPartitionTest, BadEraseAddressAlignment) {
FlashPartition& test_partition = FlashTestPartition();
@@ -264,7 +264,7 @@ TEST(FlashPartitionTest, IsErased) {
static const uint8_t fill_byte = 0x55;
uint8_t test_data[kMaxFlashAlignment];
memset(test_data, fill_byte, sizeof(test_data));
- auto data_span = std::span(test_data);
+ auto data_span = span(test_data);
// Write the chunk with fill byte.
StatusWithSize status = test_partition.Write(write_size, as_bytes(data_span));
diff --git a/pw_kvs/key_value_store.cc b/pw_kvs/key_value_store.cc
index 2b0bbe8dc..6dc570c00 100644
--- a/pw_kvs/key_value_store.cc
+++ b/pw_kvs/key_value_store.cc
@@ -39,7 +39,7 @@ constexpr bool InvalidKey(Key key) {
} // namespace
KeyValueStore::KeyValueStore(FlashPartition* partition,
- std::span<const EntryFormat> formats,
+ span<const EntryFormat> formats,
const Options& options,
size_t redundancy,
Vector<SectorDescriptor>& sector_descriptor_list,
@@ -79,7 +79,8 @@ Status KeyValueStore::Init() {
const size_t sector_size_bytes = partition_.sector_size_bytes();
- // TODO: investigate doing this as a static assert/compile-time check.
+ // TODO(davidrogers): investigate doing this as a static assert/compile-time
+ // check.
if (sector_size_bytes > SectorDescriptor::max_sector_size()) {
ERR("KVS init failed: sector_size_bytes (=%u) is greater than maximum "
"allowed sector size (=%u)",
@@ -386,7 +387,7 @@ Status KeyValueStore::ScanForEntry(const SectorDescriptor& sector,
address += Entry::kMinAlignmentBytes) {
uint32_t magic;
StatusWithSize read_result =
- partition_.Read(address, std::as_writable_bytes(std::span(&magic, 1)));
+ partition_.Read(address, as_writable_bytes(span(&magic, 1)));
if (!read_result.ok()) {
continue;
}
@@ -400,8 +401,42 @@ Status KeyValueStore::ScanForEntry(const SectorDescriptor& sector,
return Status::NotFound();
}
+#if PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
+
+Status KeyValueStore::RemoveDeletedKeyEntries() {
+ for (internal::EntryCache::iterator it = entry_cache_.begin();
+ it != entry_cache_.end();
+ ++it) {
+ EntryMetadata& entry_metadata = *it;
+
+ // The iterator we are given back from RemoveEntry could also be deleted,
+ // so loop until we find one that isn't deleted.
+ while (entry_metadata.state() == EntryState::kDeleted) {
+ // Read the original entry to get the size for sector accounting purposes.
+ Entry entry;
+ PW_TRY(ReadEntry(entry_metadata, entry));
+
+ for (Address address : entry_metadata.addresses()) {
+ sectors_.FromAddress(address).RemoveValidBytes(entry.size());
+ }
+
+ it = entry_cache_.RemoveEntry(it);
+
+ if (it == entry_cache_.end()) {
+ return OkStatus(); // new iterator is the end, bail
+ }
+
+ entry_metadata = *it; // set entry_metadata to check for deletion again
+ }
+ }
+
+ return OkStatus();
+}
+
+#endif // PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
+
StatusWithSize KeyValueStore::Get(Key key,
- std::span<byte> value_buffer,
+ span<byte> value_buffer,
size_t offset_bytes) const {
PW_TRY_WITH_SIZE(CheckReadOperation(key));
@@ -411,7 +446,7 @@ StatusWithSize KeyValueStore::Get(Key key,
return Get(key, metadata, value_buffer, offset_bytes);
}
-Status KeyValueStore::PutBytes(Key key, std::span<const byte> value) {
+Status KeyValueStore::PutBytes(Key key, span<const byte> value) {
PW_TRY(CheckWriteOperation(key));
DBG("Writing key/value; key length=%u, value length=%u",
unsigned(key.size()),
@@ -428,7 +463,7 @@ Status KeyValueStore::PutBytes(Key key, std::span<const byte> value) {
Status status = FindEntry(key, &metadata);
if (status.ok()) {
- // TODO: figure out logging how to support multiple addresses.
+ // TODO(davidrogers): figure out logging how to support multiple addresses.
DBG("Overwriting entry for key 0x%08x in %u sectors including %u",
unsigned(metadata.hash()),
unsigned(metadata.addresses().size()),
@@ -449,7 +484,7 @@ Status KeyValueStore::Delete(Key key) {
EntryMetadata metadata;
PW_TRY(FindExisting(key, &metadata));
- // TODO: figure out logging how to support multiple addresses.
+ // TODO(davidrogers): figure out logging how to support multiple addresses.
DBG("Writing tombstone for key 0x%08x in %u sectors including %u",
unsigned(metadata.hash()),
unsigned(metadata.addresses().size()),
@@ -463,7 +498,7 @@ void KeyValueStore::Item::ReadKey() {
Entry entry;
if (kvs_.ReadEntry(*iterator_, entry).ok()) {
entry.ReadKey(key_buffer_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
}
@@ -537,7 +572,7 @@ Status KeyValueStore::FindExisting(Key key, EntryMetadata* metadata_out) const {
StatusWithSize KeyValueStore::Get(Key key,
const EntryMetadata& metadata,
- std::span<std::byte> value_buffer,
+ span<std::byte> value_buffer,
size_t offset_bytes) const {
Entry entry;
@@ -584,7 +619,7 @@ Status KeyValueStore::FixedSizeGet(Key key,
}
StatusWithSize result =
- Get(key, metadata, std::span(static_cast<byte*>(value), size_bytes), 0);
+ Get(key, metadata, span(static_cast<byte*>(value), size_bytes), 0);
return result.status();
}
@@ -624,7 +659,7 @@ Status KeyValueStore::CheckReadOperation(Key key) const {
Status KeyValueStore::WriteEntryForExistingKey(EntryMetadata& metadata,
EntryState new_state,
Key key,
- std::span<const byte> value) {
+ span<const byte> value) {
// Read the original entry to get the size for sector accounting purposes.
Entry entry;
PW_TRY(ReadEntry(metadata, entry));
@@ -632,8 +667,22 @@ Status KeyValueStore::WriteEntryForExistingKey(EntryMetadata& metadata,
return WriteEntry(key, value, new_state, &metadata, &entry);
}
-Status KeyValueStore::WriteEntryForNewKey(Key key,
- std::span<const byte> value) {
+Status KeyValueStore::WriteEntryForNewKey(Key key, span<const byte> value) {
+ // If there is no room in the cache for a new entry, it is possible some cache
+ // entries could be freed by removing deleted keys. If deleted key removal is
+ // enabled and the KVS is configured to make all possible writes succeed,
+ // attempt heavy maintenance now.
+#if PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
+ if (options_.gc_on_write == GargbageCollectOnWrite::kAsManySectorsNeeded &&
+ entry_cache_.full()) {
+ Status maintenance_status = HeavyMaintenance();
+ if (!maintenance_status.ok()) {
+ WRN("KVS Maintenance failed for write: %s", maintenance_status.str());
+ return maintenance_status;
+ }
+ }
+#endif // PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
+
if (entry_cache_.full()) {
WRN("KVS full: trying to store a new entry, but can't. Have %u entries",
unsigned(entry_cache_.total_entries()));
@@ -644,7 +693,7 @@ Status KeyValueStore::WriteEntryForNewKey(Key key,
}
Status KeyValueStore::WriteEntry(Key key,
- std::span<const byte> value,
+ span<const byte> value,
EntryState new_state,
EntryMetadata* prior_metadata,
const Entry* prior_entry) {
@@ -721,8 +770,7 @@ Status KeyValueStore::GetAddressesForWrite(Address* write_addresses,
size_t write_size) {
for (size_t i = 0; i < redundancy(); i++) {
SectorDescriptor* sector;
- PW_TRY(
- GetSectorForWrite(&sector, write_size, std::span(write_addresses, i)));
+ PW_TRY(GetSectorForWrite(&sector, write_size, span(write_addresses, i)));
write_addresses[i] = sectors_.NextWritableAddress(*sector);
DBG("Found space for entry in sector %u at address %u",
@@ -741,7 +789,7 @@ Status KeyValueStore::GetAddressesForWrite(Address* write_addresses,
Status KeyValueStore::GetSectorForWrite(
SectorDescriptor** sector,
size_t entry_size,
- std::span<const Address> reserved_addresses) {
+ span<const Address> reserved_addresses) {
Status result = sectors_.FindSpace(sector, entry_size, reserved_addresses);
size_t gc_sector_count = 0;
@@ -795,7 +843,7 @@ Status KeyValueStore::MarkSectorCorruptIfNotOk(Status status,
Status KeyValueStore::AppendEntry(const Entry& entry,
Key key,
- std::span<const byte> value) {
+ span<const byte> value) {
const StatusWithSize result = entry.Write(key, value);
SectorDescriptor& sector = sectors_.FromAddress(entry.address());
@@ -829,7 +877,8 @@ StatusWithSize KeyValueStore::CopyEntryToSector(Entry& entry,
PW_TRY_WITH_SIZE(MarkSectorCorruptIfNotOk(
Entry::Read(partition_, new_address, formats_, &new_entry),
new_sector));
- // TODO: add test that catches doing the verify on the old entry.
+ // TODO(davidrogers): add test that catches doing the verify on the old
+ // entry.
PW_TRY_WITH_SIZE(MarkSectorCorruptIfNotOk(new_entry.VerifyChecksumInFlash(),
new_sector));
}
@@ -841,10 +890,9 @@ StatusWithSize KeyValueStore::CopyEntryToSector(Entry& entry,
return result;
}
-Status KeyValueStore::RelocateEntry(
- const EntryMetadata& metadata,
- KeyValueStore::Address& address,
- std::span<const Address> reserved_addresses) {
+Status KeyValueStore::RelocateEntry(const EntryMetadata& metadata,
+ KeyValueStore::Address& address,
+ span<const Address> reserved_addresses) {
Entry entry;
PW_TRY(ReadEntry(metadata, entry));
@@ -878,13 +926,15 @@ Status KeyValueStore::FullMaintenanceHelper(MaintenanceType maintenance_type) {
INF("Beginning full maintenance");
CheckForErrors();
+ // Step 1: Repair errors
if (error_detected_) {
PW_TRY(Repair());
}
+
+ // Step 2: Make sure all the entries are on the primary format.
StatusWithSize update_status = UpdateEntriesToPrimaryFormat();
Status overall_status = update_status.status();
- // Make sure all the entries are on the primary format.
if (!overall_status.ok()) {
ERR("Failed to update all entries to the primary format");
}
@@ -901,27 +951,52 @@ Status KeyValueStore::FullMaintenanceHelper(MaintenanceType maintenance_type) {
bool heavy = (maintenance_type == MaintenanceType::kHeavy);
bool force_gc = heavy || over_usage_threshold || (update_status.size() > 0);
- // TODO: look in to making an iterator method for cycling through sectors
- // starting from last_new_sector_.
- Status gc_status;
- for (size_t j = 0; j < sectors_.size(); j++) {
- sector += 1;
- if (sector == sectors_.end()) {
- sector = sectors_.begin();
- }
+ auto do_garbage_collect_pass = [&]() {
+ // TODO(drempel): look in to making an iterator method for cycling through
+ // sectors starting from last_new_sector_.
+ Status gc_status;
+ for (size_t j = 0; j < sectors_.size(); j++) {
+ sector += 1;
+ if (sector == sectors_.end()) {
+ sector = sectors_.begin();
+ }
- if (sector->RecoverableBytes(partition_.sector_size_bytes()) > 0 &&
- (force_gc || sector->valid_bytes() == 0)) {
- gc_status = GarbageCollectSector(*sector, {});
- if (!gc_status.ok()) {
- ERR("Failed to garbage collect all sectors");
- break;
+ if (sector->RecoverableBytes(partition_.sector_size_bytes()) > 0 &&
+ (force_gc || sector->valid_bytes() == 0)) {
+ gc_status = GarbageCollectSector(*sector, {});
+ if (!gc_status.ok()) {
+ ERR("Failed to garbage collect all sectors");
+ break;
+ }
}
}
- }
- if (overall_status.ok()) {
- overall_status = gc_status;
- }
+ if (overall_status.ok()) {
+ overall_status = gc_status;
+ }
+ };
+
+ // Step 3: Do full garbage collect pass for all sectors. This will erase all
+ // old/state entries from flash and leave only current/valid entries.
+ do_garbage_collect_pass();
+
+#if PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
+ // Step 4: (if heavy maintenance) garbage collect all the deleted keys.
+ if (heavy) {
+ // If enabled, remove deleted keys from the entry cache, including freeing
+ // sector bytes used by those keys. This must only be done directly after a
+ // full garbage collection, otherwise the current deleted entry could be
+ // garbage collected before the older stale entry producing a window for an
+ // invalid/corrupted KVS state if there was a power-fault, crash or other
+ // interruption.
+ overall_status.Update(RemoveDeletedKeyEntries());
+
+ // Do another garbage collect pass that will fully remove the deleted keys
+ // from flash. Garbage collect will only touch sectors that have something
+ // to garbage collect, which in this case is only sectors containing deleted
+ // keys.
+ do_garbage_collect_pass();
+ }
+#endif // PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
if (overall_status.ok()) {
INF("Full maintenance complete");
@@ -941,11 +1016,10 @@ Status KeyValueStore::PartialMaintenance() {
if (error_detected_ && options_.recovery != ErrorRecovery::kManual) {
PW_TRY(Repair());
}
- return GarbageCollect(std::span<const Address>());
+ return GarbageCollect(span<const Address>());
}
-Status KeyValueStore::GarbageCollect(
- std::span<const Address> reserved_addresses) {
+Status KeyValueStore::GarbageCollect(span<const Address> reserved_addresses) {
DBG("Garbage Collect a single sector");
for ([[maybe_unused]] Address address : reserved_addresses) {
DBG(" Avoid address %u", unsigned(address));
@@ -967,7 +1041,7 @@ Status KeyValueStore::GarbageCollect(
Status KeyValueStore::RelocateKeyAddressesInSector(
SectorDescriptor& sector_to_gc,
const EntryMetadata& metadata,
- std::span<const Address> reserved_addresses) {
+ span<const Address> reserved_addresses) {
for (FlashPartition::Address& address : metadata.addresses()) {
if (sectors_.AddressInSector(sector_to_gc, address)) {
DBG(" Relocate entry for Key 0x%08" PRIx32 ", sector %u",
@@ -978,11 +1052,10 @@ Status KeyValueStore::RelocateKeyAddressesInSector(
}
return OkStatus();
-};
+}
Status KeyValueStore::GarbageCollectSector(
- SectorDescriptor& sector_to_gc,
- std::span<const Address> reserved_addresses) {
+ SectorDescriptor& sector_to_gc, span<const Address> reserved_addresses) {
DBG(" Garbage Collect sector %u", sectors_.Index(sector_to_gc));
// Step 1: Move any valid entries in the GC sector to other sectors
@@ -1130,7 +1203,7 @@ Status KeyValueStore::EnsureFreeSectorExists() {
}
if (empty_sector_found == false) {
DBG(" No empty sector found, attempting to GC a free sector");
- Status sector_status = GarbageCollect(std::span<const Address, 0>());
+ Status sector_status = GarbageCollect(span<const Address, 0>());
if (repair_status.ok() && !sector_status.ok()) {
DBG(" Unable to free an empty sector");
repair_status = sector_status;
@@ -1210,14 +1283,14 @@ Status KeyValueStore::Repair() {
DBG("Reinitialize KVS metadata");
InitializeMetadata()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return FixErrors();
}
KeyValueStore::Entry KeyValueStore::CreateEntry(Address address,
Key key,
- std::span<const byte> value,
+ span<const byte> value,
EntryState state) {
// Always bump the transaction ID when creating a new entry.
//
@@ -1284,7 +1357,7 @@ void KeyValueStore::LogDebugInfo() const {
}
DBG(" ");
- // TODO: This should stop logging after some threshold.
+ // TODO(keir): This should stop logging after some threshold.
// size_t dumped_bytes = 0;
DBG("Sector raw data:");
for (size_t sector_id = 0; sector_id < sectors_.size(); ++sector_id) {
@@ -1309,7 +1382,7 @@ void KeyValueStore::LogDebugInfo() const {
static_cast<unsigned int>(raw_sector_data[i + 6]),
static_cast<unsigned int>(raw_sector_data[i + 7]));
- // TODO: Fix exit condition.
+ // TODO(keir): Fix exit condition.
if (i > 128) {
break;
}
diff --git a/pw_kvs/key_value_store_binary_format_test.cc b/pw_kvs/key_value_store_binary_format_test.cc
index dbaf44b34..4ce2d748a 100644
--- a/pw_kvs/key_value_store_binary_format_test.cc
+++ b/pw_kvs/key_value_store_binary_format_test.cc
@@ -33,7 +33,7 @@ using std::string_view;
constexpr size_t kMaxEntries = 256;
constexpr size_t kMaxUsableSectors = 256;
-constexpr uint32_t SimpleChecksum(std::span<const byte> data, uint32_t state) {
+constexpr uint32_t SimpleChecksum(span<const byte> data, uint32_t state) {
for (byte b : data) {
state += uint32_t(b);
}
@@ -43,19 +43,18 @@ constexpr uint32_t SimpleChecksum(std::span<const byte> data, uint32_t state) {
template <typename State>
class ChecksumFunction final : public ChecksumAlgorithm {
public:
- ChecksumFunction(State (&algorithm)(std::span<const byte>, State))
- : ChecksumAlgorithm(std::as_bytes(std::span(&state_, 1))),
- algorithm_(algorithm) {}
+ ChecksumFunction(State (&algorithm)(span<const byte>, State))
+ : ChecksumAlgorithm(as_bytes(span(&state_, 1))), algorithm_(algorithm) {}
void Reset() override { state_ = {}; }
- void Update(std::span<const byte> data) override {
+ void Update(span<const byte> data) override {
state_ = algorithm_(data, state_);
}
private:
State state_;
- State (&algorithm_)(std::span<const byte>, State);
+ State (&algorithm_)(span<const byte>, State);
};
ChecksumFunction<uint32_t> default_checksum(SimpleChecksum);
@@ -69,8 +68,7 @@ constexpr auto EntryPadding() {
}
// Creates a buffer containing a valid entry at compile time.
-template <uint32_t (*kChecksum)(std::span<const byte>,
- uint32_t) = &SimpleChecksum,
+template <uint32_t (*kChecksum)(span<const byte>, uint32_t) = &SimpleChecksum,
size_t kAlignmentBytes = sizeof(internal::EntryHeader),
size_t kKeyLengthWithNull,
size_t kValueSize>
@@ -88,7 +86,7 @@ constexpr auto MakeValidEntry(uint32_t magic,
uint16_t(kValueSize),
id,
bytes::String(key),
- std::span(value),
+ span(value),
EntryPadding<kAlignmentBytes, kKeyLength, kValueSize>());
// Calculate the checksum
@@ -102,8 +100,7 @@ constexpr auto MakeValidEntry(uint32_t magic,
}
// Creates a buffer containing a deleted entry at compile time.
-template <uint32_t (*kChecksum)(std::span<const byte>,
- uint32_t) = &SimpleChecksum,
+template <uint32_t (*kChecksum)(span<const byte>, uint32_t) = &SimpleChecksum,
size_t kAlignmentBytes = sizeof(internal::EntryHeader),
size_t kKeyLengthWithNull>
constexpr auto MakeDeletedEntry(uint32_t magic,
@@ -176,9 +173,8 @@ class KvsErrorHandling : public ::testing::Test {
partition_(&flash_),
kvs_(&partition_, default_format, kNoGcOptions) {}
- void InitFlashTo(std::span<const byte> contents) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ void InitFlashTo(span<const byte> contents) {
+ ASSERT_EQ(OkStatus(), partition_.Erase());
std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
}
@@ -341,7 +337,7 @@ TEST_F(KvsErrorHandling, Init_CorruptKey_RevertsToPreviousVersion) {
EXPECT_EQ(1u, kvs_.size());
- auto result = kvs_.Get("my_key", std::as_writable_bytes(std::span(buffer)));
+ auto result = kvs_.Get("my_key", as_writable_bytes(span(buffer)));
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(sizeof("version 7") - 1, result.size());
EXPECT_STREQ("version 7", buffer);
@@ -358,7 +354,7 @@ TEST_F(KvsErrorHandling, Put_WriteFailure_EntryNotAddedButBytesMarkedWritten) {
EXPECT_EQ(Status::Unavailable(), kvs_.Put("key1", bytes::String("value1")));
- EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", std::span<byte>()).status());
+ EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", span<byte>()).status());
ASSERT_TRUE(kvs_.empty());
auto stats = kvs_.GetStorageStats();
@@ -385,9 +381,8 @@ class KvsErrorRecovery : public ::testing::Test {
{.magic = kMagic, .checksum = &default_checksum},
kRecoveryNoGcOptions) {}
- void InitFlashTo(std::span<const byte> contents) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ void InitFlashTo(span<const byte> contents) {
+ ASSERT_EQ(OkStatus(), partition_.Erase());
std::memcpy(flash_.buffer().data(), contents.data(), contents.size());
}
@@ -562,7 +557,7 @@ TEST_F(KvsErrorRecovery, Init_CorruptKey_RevertsToPreviousVersion) {
EXPECT_EQ(1u, kvs_.size());
- auto result = kvs_.Get("my_key", std::as_writable_bytes(std::span(buffer)));
+ auto result = kvs_.Get("my_key", as_writable_bytes(span(buffer)));
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(sizeof("version 7") - 1, result.size());
EXPECT_STREQ("version 7", buffer);
@@ -580,7 +575,7 @@ TEST_F(KvsErrorRecovery, Put_WriteFailure_EntryNotAddedButBytesMarkedWritten) {
EXPECT_EQ(Status::Unavailable(), kvs_.Put("key1", bytes::String("value1")));
EXPECT_EQ(true, kvs_.error_detected());
- EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", std::span<byte>()).status());
+ EXPECT_EQ(Status::NotFound(), kvs_.Get("key1", span<byte>()).status());
ASSERT_TRUE(kvs_.empty());
auto stats = kvs_.GetStorageStats();
@@ -606,7 +601,7 @@ TEST_F(KvsErrorRecovery, Put_WriteFailure_EntryNotAddedButBytesMarkedWritten) {
// human readable 4 bytes. See pw_kvs/format.h for more information.
constexpr uint32_t kAltMagic = 0x41a2db83;
-constexpr uint32_t AltChecksum(std::span<const byte> data, uint32_t state) {
+constexpr uint32_t AltChecksum(span<const byte> data, uint32_t state) {
for (byte b : data) {
state = (state << 8) | uint32_t(byte(state >> 24) ^ b);
}
@@ -618,7 +613,7 @@ ChecksumFunction<uint32_t> alt_checksum(AltChecksum);
constexpr auto kAltEntry =
MakeValidEntry<AltChecksum>(kAltMagic, 32, "A Key", bytes::String("XD"));
-constexpr uint32_t NoChecksum(std::span<const byte>, uint32_t) { return 0; }
+constexpr uint32_t NoChecksum(span<const byte>, uint32_t) { return 0; }
// For KVS magic value always use a random 32 bit integer rather than a
// human readable 4 bytes. See pw_kvs/format.h for more information.
constexpr uint32_t kNoChecksumMagic = 0xd49ba138;
@@ -644,8 +639,7 @@ class InitializedRedundantMultiMagicKvs : public ::testing::Test {
{.magic = kNoChecksumMagic, .checksum = nullptr},
}},
kRecoveryNoGcOptions) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), partition_.Erase());
std::memcpy(flash_.buffer().data(),
kInitialContents.data(),
kInitialContents.size());
@@ -658,14 +652,13 @@ class InitializedRedundantMultiMagicKvs : public ::testing::Test {
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors, 2, 3> kvs_;
};
-#define ASSERT_CONTAINS_ENTRY(key, str_value) \
- do { \
- char val[sizeof(str_value)] = {}; \
- StatusWithSize stat = \
- kvs_.Get(key, std::as_writable_bytes(std::span(val))); \
- ASSERT_EQ(OkStatus(), stat.status()); \
- ASSERT_EQ(sizeof(str_value) - 1, stat.size()); \
- ASSERT_STREQ(str_value, val); \
+#define ASSERT_CONTAINS_ENTRY(key, str_value) \
+ do { \
+ char val[sizeof(str_value)] = {}; \
+ StatusWithSize stat = kvs_.Get(key, as_writable_bytes(span(val))); \
+ ASSERT_EQ(OkStatus(), stat.status()); \
+ ASSERT_EQ(sizeof(str_value) - 1, stat.size()); \
+ ASSERT_STREQ(str_value, val); \
} while (0)
TEST_F(InitializedRedundantMultiMagicKvs, AllEntriesArePresent) {
@@ -775,9 +768,8 @@ TEST_F(InitializedRedundantMultiMagicKvs, SingleWriteError) {
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
char val[20] = {};
- EXPECT_EQ(
- OkStatus(),
- kvs_.Get("new key", std::as_writable_bytes(std::span(val))).status());
+ EXPECT_EQ(OkStatus(),
+ kvs_.Get("new key", as_writable_bytes(span(val))).status());
EXPECT_EQ(OkStatus(), kvs_.FullMaintenance());
stats = kvs_.GetStorageStats();
@@ -787,9 +779,8 @@ TEST_F(InitializedRedundantMultiMagicKvs, SingleWriteError) {
EXPECT_EQ(stats.corrupt_sectors_recovered, 0u);
EXPECT_EQ(stats.missing_redundant_entries_recovered, 0u);
- EXPECT_EQ(
- OkStatus(),
- kvs_.Get("new key", std::as_writable_bytes(std::span(val))).status());
+ EXPECT_EQ(OkStatus(),
+ kvs_.Get("new key", as_writable_bytes(span(val))).status());
}
TEST_F(InitializedRedundantMultiMagicKvs, DataLossAfterLosingBothCopies) {
@@ -797,15 +788,15 @@ TEST_F(InitializedRedundantMultiMagicKvs, DataLossAfterLosingBothCopies) {
char val[20] = {};
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("key1", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("key1", as_writable_bytes(span(val))).status());
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("k2", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("k2", as_writable_bytes(span(val))).status());
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("k3y", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("k3y", as_writable_bytes(span(val))).status());
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("A Key", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("A Key", as_writable_bytes(span(val))).status());
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("kee", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("kee", as_writable_bytes(span(val))).status());
EXPECT_EQ(true, kvs_.error_detected());
@@ -841,14 +832,13 @@ TEST_F(InitializedRedundantMultiMagicKvs, PutExistingEntry_UsesFirstFormat) {
ASSERT_CONTAINS_ENTRY("A Key", "New value!");
}
-#define ASSERT_KVS_CONTAINS_ENTRY(kvs, key, str_value) \
- do { \
- char val[sizeof(str_value)] = {}; \
- StatusWithSize stat = \
- kvs.Get(key, std::as_writable_bytes(std::span(val))); \
- ASSERT_EQ(OkStatus(), stat.status()); \
- ASSERT_EQ(sizeof(str_value) - 1, stat.size()); \
- ASSERT_STREQ(str_value, val); \
+#define ASSERT_KVS_CONTAINS_ENTRY(kvs, key, str_value) \
+ do { \
+ char val[sizeof(str_value)] = {}; \
+ StatusWithSize stat = kvs.Get(key, as_writable_bytes(span(val))); \
+ ASSERT_EQ(OkStatus(), stat.status()); \
+ ASSERT_EQ(sizeof(str_value) - 1, stat.size()); \
+ ASSERT_STREQ(str_value, val); \
} while (0)
TEST_F(InitializedRedundantMultiMagicKvs, UpdateEntryFormat) {
@@ -883,8 +873,7 @@ class InitializedMultiMagicKvs : public ::testing::Test {
{.magic = kNoChecksumMagic, .checksum = nullptr},
}},
kRecoveryNoGcOptions) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), partition_.Erase());
std::memcpy(flash_.buffer().data(),
kInitialContents.data(),
kInitialContents.size());
@@ -937,8 +926,7 @@ class InitializedRedundantLazyRecoveryKvs : public ::testing::Test {
kvs_(&partition_,
{.magic = kMagic, .checksum = &default_checksum},
kRecoveryLazyGcOptions) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), partition_.Erase());
std::memcpy(flash_.buffer().data(),
kInitialContents.data(),
kInitialContents.size());
@@ -956,13 +944,13 @@ TEST_F(InitializedRedundantLazyRecoveryKvs, WriteAfterDataLoss) {
char val[20] = {};
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("key1", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("key1", as_writable_bytes(span(val))).status());
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("k2", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("k2", as_writable_bytes(span(val))).status());
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("k3y", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("k3y", as_writable_bytes(span(val))).status());
EXPECT_EQ(Status::DataLoss(),
- kvs_.Get("4k", std::as_writable_bytes(std::span(val))).status());
+ kvs_.Get("4k", as_writable_bytes(span(val))).status());
EXPECT_EQ(true, kvs_.error_detected());
@@ -1030,8 +1018,7 @@ class InitializedLazyRecoveryKvs : public ::testing::Test {
kvs_(&partition_,
{.magic = kMagic, .checksum = &default_checksum},
kRecoveryLazyGcOptions) {
- partition_.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), partition_.Erase());
std::memcpy(flash_.buffer().data(),
kInitialContents.data(),
kInitialContents.size());
diff --git a/pw_kvs/key_value_store_fuzz_test.cc b/pw_kvs/key_value_store_fuzz_test.cc
index d35a1286a..25ed31c9f 100644
--- a/pw_kvs/key_value_store_fuzz_test.cc
+++ b/pw_kvs/key_value_store_fuzz_test.cc
@@ -14,7 +14,6 @@
#include <array>
#include <cstring>
-#include <span>
#include "gtest/gtest.h"
#include "pw_checksum/crc16_ccitt.h"
@@ -22,8 +21,11 @@
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/flash_test_partition.h"
#include "pw_kvs/key_value_store.h"
+#include "pw_kvs_private/config.h"
#include "pw_log/log.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
+#include "pw_string/string_builder.h"
namespace pw::kvs {
namespace {
@@ -44,8 +46,7 @@ constexpr EntryFormat default_format{.magic = 0x749c361e,
TEST(KvsFuzz, FuzzTest) {
FlashPartition& test_partition = FlashTestPartition();
- test_partition.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), test_partition.Erase());
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_(&test_partition,
default_format);
@@ -55,7 +56,7 @@ TEST(KvsFuzz, FuzzTest) {
if (test_partition.sector_size_bytes() < 4 * 1024 ||
test_partition.sector_count() < 4) {
PW_LOG_INFO("Sectors too small, skipping test.");
- return; // TODO: Test could be generalized
+ return; // TODO(davidrogers): Test could be generalized
}
const char* key1 = "Buf1";
const char* key2 = "Buf2";
@@ -80,11 +81,12 @@ TEST(KvsFuzz, FuzzTest) {
// Rewrite a single key many times, can fill up a sector
ASSERT_EQ(OkStatus(), kvs_.Put("some_data", j));
}
- // Delete and re-add everything
+
+ // Delete and re-add everything except "some_data"
ASSERT_EQ(OkStatus(), kvs_.Delete(key1));
- ASSERT_EQ(OkStatus(), kvs_.Put(key1, std::span(buf1, size1)));
+ ASSERT_EQ(OkStatus(), kvs_.Put(key1, span(buf1, size1)));
ASSERT_EQ(OkStatus(), kvs_.Delete(key2));
- ASSERT_EQ(OkStatus(), kvs_.Put(key2, std::span(buf2, size2)));
+ ASSERT_EQ(OkStatus(), kvs_.Put(key2, span(buf2, size2)));
for (size_t j = 0; j < keys.size(); j++) {
ASSERT_EQ(OkStatus(), kvs_.Delete(keys[j]));
ASSERT_EQ(OkStatus(), kvs_.Put(keys[j], j));
@@ -93,9 +95,9 @@ TEST(KvsFuzz, FuzzTest) {
// Re-enable and verify
ASSERT_EQ(OkStatus(), kvs_.Init());
static byte buf[4 * 1024];
- ASSERT_EQ(OkStatus(), kvs_.Get(key1, std::span(buf, size1)).status());
+ ASSERT_EQ(OkStatus(), kvs_.Get(key1, span(buf, size1)).status());
ASSERT_EQ(std::memcmp(buf, buf1, size1), 0);
- ASSERT_EQ(OkStatus(), kvs_.Get(key2, std::span(buf, size2)).status());
+ ASSERT_EQ(OkStatus(), kvs_.Get(key2, span(buf, size2)).status());
ASSERT_EQ(std::memcmp(buf2, buf2, size2), 0);
for (size_t j = 0; j < keys.size(); j++) {
size_t ret = 1000;
@@ -105,4 +107,105 @@ TEST(KvsFuzz, FuzzTest) {
}
}
+TEST(KvsFuzz, FuzzTestWithGC) {
+ FlashPartition& test_partition = FlashTestPartition();
+ ASSERT_EQ(OkStatus(), test_partition.Erase());
+
+ KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_(&test_partition,
+ default_format);
+
+ ASSERT_EQ(OkStatus(), kvs_.Init());
+
+ if (test_partition.sector_size_bytes() < 4 * 1024 ||
+ test_partition.sector_count() < 4) {
+ PW_LOG_INFO("Sectors too small, skipping test.");
+ return; // TODO(drempel): Test could be generalized
+ }
+ const char* key1 = "Buf1";
+ const char* key2 = "Buf2";
+ const size_t kLargestBufSize = 3 * 1024;
+ static byte buf1[kLargestBufSize];
+ static byte buf2[kLargestBufSize];
+ std::memset(buf1, 1, sizeof(buf1));
+ std::memset(buf2, 2, sizeof(buf2));
+
+ // Start with things in KVS
+ ASSERT_EQ(OkStatus(), kvs_.Put(key1, buf1));
+ ASSERT_EQ(OkStatus(), kvs_.Put(key2, buf2));
+ for (size_t j = 0; j < keys.size(); j++) {
+ ASSERT_EQ(OkStatus(), kvs_.Put(keys[j], j));
+ }
+
+ for (size_t i = 0; i < 100; i++) {
+ // Vary two sizes
+ size_t size1 = (kLargestBufSize) / (i + 1);
+ size_t size2 = (kLargestBufSize) / (100 - i);
+ for (size_t j = 0; j < 50; j++) {
+ // Rewrite a single key many times, can fill up a sector
+ ASSERT_EQ(OkStatus(), kvs_.Put("some_data", j));
+ }
+
+ // Delete and re-add everything except "some_data".
+ ASSERT_EQ(OkStatus(), kvs_.Delete(key1));
+ ASSERT_EQ(OkStatus(), kvs_.Put(key1, span(buf1, size1)));
+ ASSERT_EQ(OkStatus(), kvs_.Delete(key2));
+
+ // Throw some heavy maintenance in the middle to trigger some GC before
+ // moving forward.
+ EXPECT_EQ(OkStatus(), kvs_.HeavyMaintenance());
+
+ // check for expected stats
+ KeyValueStore::StorageStats stats = kvs_.GetStorageStats();
+ EXPECT_GT(stats.sector_erase_count, 1u);
+ EXPECT_EQ(stats.reclaimable_bytes, 0u);
+
+ if (!PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE) {
+ PW_LOG_INFO(
+ "PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE is "
+ "disabled; skipping remainder of test");
+ return;
+ }
+
+ // Write out rotating keyvalue, read it, and delete kMaxEntries * 4.
+ // This tests whether garbage collection is working on write.
+ for (size_t j = 0; j < kMaxEntries * 4; j++) {
+ size_t readj;
+ StringBuffer<6> keyVal;
+ keyVal << j;
+ ASSERT_EQ(OkStatus(), kvs_.Put(keyVal.c_str(), j));
+ ASSERT_EQ(OkStatus(), kvs_.Get(keyVal.c_str(), &readj));
+ ASSERT_EQ(j, readj);
+ ASSERT_EQ(OkStatus(), kvs_.Delete(keyVal.c_str()));
+ ASSERT_EQ(Status::NotFound(), kvs_.Get(keyVal.c_str(), &readj));
+ }
+
+ // The KVS should contain key1, "some_data", and all of keys[].
+ ASSERT_EQ(kvs_.size(), 2u + keys.size());
+
+ ASSERT_EQ(OkStatus(), kvs_.Put(key2, span(buf2, size2)));
+ for (size_t j = 0; j < keys.size(); j++) {
+ ASSERT_EQ(OkStatus(), kvs_.Delete(keys[j]));
+ ASSERT_EQ(OkStatus(), kvs_.Put(keys[j], j));
+ }
+
+ // Do some more heavy maintenance, ensure we have the right number
+ // of keys.
+ EXPECT_EQ(OkStatus(), kvs_.HeavyMaintenance());
+ // The KVS should contain key1, key2, "some_data", and all of keys[].
+ ASSERT_EQ(kvs_.size(), 3u + keys.size());
+
+ // Re-enable and verify (final check on store).
+ ASSERT_EQ(OkStatus(), kvs_.Init());
+ static byte buf[4 * 1024];
+ ASSERT_EQ(OkStatus(), kvs_.Get(key1, span(buf, size1)).status());
+ ASSERT_EQ(std::memcmp(buf, buf1, size1), 0);
+ ASSERT_EQ(OkStatus(), kvs_.Get(key2, span(buf, size2)).status());
+ ASSERT_EQ(std::memcmp(buf2, buf2, size2), 0);
+ for (size_t j = 0; j < keys.size(); j++) {
+ size_t ret = 1000;
+ ASSERT_EQ(OkStatus(), kvs_.Get(keys[j], &ret));
+ ASSERT_EQ(ret, j);
+ }
+ }
+}
} // namespace pw::kvs
diff --git a/pw_kvs/key_value_store_initialized_test.cc b/pw_kvs/key_value_store_initialized_test.cc
index 431456458..2524a96dc 100644
--- a/pw_kvs/key_value_store_initialized_test.cc
+++ b/pw_kvs/key_value_store_initialized_test.cc
@@ -15,9 +15,9 @@
#include <array>
#include <cstdio>
#include <cstring>
-#include <span>
#include "gtest/gtest.h"
+#include "pw_assert/check.h"
#include "pw_bytes/array.h"
#include "pw_checksum/crc16_ccitt.h"
#include "pw_kvs/crc16_checksum.h"
@@ -26,6 +26,7 @@
#include "pw_kvs/internal/entry.h"
#include "pw_kvs/key_value_store.h"
#include "pw_log/log.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_string/string_builder.h"
@@ -82,9 +83,8 @@ constexpr EntryFormat default_format{.magic = 0x5b9a341e,
class EmptyInitializedKvs : public ::testing::Test {
protected:
EmptyInitializedKvs() : kvs_(&test_partition, default_format) {
- test_partition.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- ASSERT_EQ(OkStatus(), kvs_.Init());
+ EXPECT_EQ(OkStatus(), test_partition.Erase());
+ PW_CHECK_OK(kvs_.Init());
}
// Intention of this is to put and erase key-val to fill up sectors. It's a
@@ -110,9 +110,9 @@ class EmptyInitializedKvs : public ::testing::Test {
buffer[0] = static_cast<byte>(static_cast<uint8_t>(buffer[0]) + 1);
ASSERT_EQ(OkStatus(),
kvs_.Put(key,
- std::span(buffer.data(),
- chunk_len - kvs_attr.ChunkHeaderSize() -
- kvs_attr.KeySize())));
+ span(buffer.data(),
+ chunk_len - kvs_attr.ChunkHeaderSize() -
+ kvs_attr.KeySize())));
size_to_fill -= chunk_len;
chunk_len = std::min(size_to_fill, kMaxPutSize);
}
@@ -128,8 +128,7 @@ TEST_F(EmptyInitializedKvs, Put_SameKeySameValueRepeatedly_AlignedEntries) {
std::array<char, 8> value{'v', 'a', 'l', 'u', 'e', '6', '7', '\0'};
for (int i = 0; i < 1000; ++i) {
- ASSERT_EQ(OkStatus(),
- kvs_.Put("The Key!", std::as_bytes(std::span(value))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("The Key!", as_bytes(span(value))));
}
}
@@ -137,8 +136,7 @@ TEST_F(EmptyInitializedKvs, Put_SameKeySameValueRepeatedly_UnalignedEntries) {
std::array<char, 7> value{'v', 'a', 'l', 'u', 'e', '6', '\0'};
for (int i = 0; i < 1000; ++i) {
- ASSERT_EQ(OkStatus(),
- kvs_.Put("The Key!", std::as_bytes(std::span(value))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("The Key!", as_bytes(span(value))));
}
}
@@ -164,7 +162,7 @@ TEST_F(EmptyInitializedKvs, PutAndGetByValue_ConvertibleToSpan) {
TEST_F(EmptyInitializedKvs, PutAndGetByValue_Span) {
float input[] = {1.0, -3.5};
- ASSERT_EQ(OkStatus(), kvs_.Put("key", std::span(input)));
+ ASSERT_EQ(OkStatus(), kvs_.Put("key", span(input)));
float output[2] = {};
ASSERT_EQ(OkStatus(), kvs_.Get("key", &output));
@@ -188,48 +186,41 @@ TEST_F(EmptyInitializedKvs, PutAndGetByValue_NotConvertibleToSpan) {
}
TEST_F(EmptyInitializedKvs, Get_Simple) {
- ASSERT_EQ(OkStatus(),
- kvs_.Put("Charles", std::as_bytes(std::span("Mingus"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("Charles", as_bytes(span("Mingus"))));
char value[16];
- auto result = kvs_.Get("Charles", std::as_writable_bytes(std::span(value)));
+ auto result = kvs_.Get("Charles", as_writable_bytes(span(value)));
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(sizeof("Mingus"), result.size());
EXPECT_STREQ("Mingus", value);
}
TEST_F(EmptyInitializedKvs, Get_WithOffset) {
- ASSERT_EQ(OkStatus(),
- kvs_.Put("Charles", std::as_bytes(std::span("Mingus"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("Charles", as_bytes(span("Mingus"))));
char value[16];
- auto result =
- kvs_.Get("Charles", std::as_writable_bytes(std::span(value)), 4);
+ auto result = kvs_.Get("Charles", as_writable_bytes(span(value)), 4);
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(sizeof("Mingus") - 4, result.size());
EXPECT_STREQ("us", value);
}
TEST_F(EmptyInitializedKvs, Get_WithOffset_FillBuffer) {
- ASSERT_EQ(OkStatus(),
- kvs_.Put("Charles", std::as_bytes(std::span("Mingus"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("Charles", as_bytes(span("Mingus"))));
char value[4] = {};
- auto result =
- kvs_.Get("Charles", std::as_writable_bytes(std::span(value, 3)), 1);
+ auto result = kvs_.Get("Charles", as_writable_bytes(span(value, 3)), 1);
EXPECT_EQ(Status::ResourceExhausted(), result.status());
EXPECT_EQ(3u, result.size());
EXPECT_STREQ("ing", value);
}
TEST_F(EmptyInitializedKvs, Get_WithOffset_PastEnd) {
- ASSERT_EQ(OkStatus(),
- kvs_.Put("Charles", std::as_bytes(std::span("Mingus"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("Charles", as_bytes(span("Mingus"))));
char value[16];
- auto result = kvs_.Get("Charles",
- std::as_writable_bytes(std::span(value)),
- sizeof("Mingus") + 1);
+ auto result =
+ kvs_.Get("Charles", as_writable_bytes(span(value)), sizeof("Mingus") + 1);
EXPECT_EQ(Status::OutOfRange(), result.status());
EXPECT_EQ(0u, result.size());
}
@@ -259,7 +250,7 @@ TEST_F(EmptyInitializedKvs, GetValue_TooLarge) {
}
TEST_F(EmptyInitializedKvs, Delete_GetDeletedKey_ReturnsNotFound) {
- ASSERT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("123"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("123"))));
ASSERT_EQ(OkStatus(), kvs_.Delete("kEy"));
EXPECT_EQ(Status::NotFound(), kvs_.Get("kEy", {}).status());
@@ -267,10 +258,10 @@ TEST_F(EmptyInitializedKvs, Delete_GetDeletedKey_ReturnsNotFound) {
}
TEST_F(EmptyInitializedKvs, Delete_AddBackKey_PersistsAfterInitialization) {
- ASSERT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("123"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("123"))));
ASSERT_EQ(OkStatus(), kvs_.Delete("kEy"));
- EXPECT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("45678"))));
+ EXPECT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("45678"))));
char data[6] = {};
ASSERT_EQ(OkStatus(), kvs_.Get("kEy", &data));
EXPECT_STREQ(data, "45678");
@@ -280,14 +271,14 @@ TEST_F(EmptyInitializedKvs, Delete_AddBackKey_PersistsAfterInitialization) {
default_format);
ASSERT_EQ(OkStatus(), new_kvs.Init());
- EXPECT_EQ(OkStatus(), new_kvs.Put("kEy", std::as_bytes(std::span("45678"))));
+ EXPECT_EQ(OkStatus(), new_kvs.Put("kEy", as_bytes(span("45678"))));
char new_data[6] = {};
EXPECT_EQ(OkStatus(), new_kvs.Get("kEy", &new_data));
EXPECT_STREQ(data, "45678");
}
TEST_F(EmptyInitializedKvs, Delete_AllItems_KvsIsEmpty) {
- ASSERT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("123"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("123"))));
ASSERT_EQ(OkStatus(), kvs_.Delete("kEy"));
EXPECT_EQ(0u, kvs_.size());
@@ -346,7 +337,7 @@ TEST_F(EmptyInitializedKvs, Iteration_Empty_ByValue) {
}
TEST_F(EmptyInitializedKvs, Iteration_OneItem) {
- ASSERT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("123"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("123"))));
for (KeyValueStore::Item entry : kvs_) {
EXPECT_STREQ(entry.key(), "kEy"); // Make sure null-terminated.
@@ -358,11 +349,11 @@ TEST_F(EmptyInitializedKvs, Iteration_OneItem) {
}
TEST_F(EmptyInitializedKvs, Iteration_GetWithOffset) {
- ASSERT_EQ(OkStatus(), kvs_.Put("key", std::as_bytes(std::span("not bad!"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("key", as_bytes(span("not bad!"))));
for (KeyValueStore::Item entry : kvs_) {
char temp[5];
- auto result = entry.Get(std::as_writable_bytes(std::span(temp)), 4);
+ auto result = entry.Get(as_writable_bytes(span(temp)), 4);
EXPECT_EQ(OkStatus(), result.status());
EXPECT_EQ(5u, result.size());
EXPECT_STREQ("bad!", temp);
@@ -400,7 +391,7 @@ TEST_F(EmptyInitializedKvs, Iteration_GetValue_TooLarge) {
}
TEST_F(EmptyInitializedKvs, Iteration_EmptyAfterDeletion) {
- ASSERT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("123"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("123"))));
ASSERT_EQ(OkStatus(), kvs_.Delete("kEy"));
for (KeyValueStore::Item entry : kvs_) {
@@ -410,7 +401,7 @@ TEST_F(EmptyInitializedKvs, Iteration_EmptyAfterDeletion) {
}
TEST_F(EmptyInitializedKvs, Iterator) {
- ASSERT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("123"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("123"))));
for (KeyValueStore::iterator it = kvs_.begin(); it != kvs_.end(); ++it) {
EXPECT_STREQ(it->key(), "kEy");
@@ -422,7 +413,7 @@ TEST_F(EmptyInitializedKvs, Iterator) {
}
TEST_F(EmptyInitializedKvs, Iterator_PostIncrement) {
- ASSERT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("123"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("123"))));
KeyValueStore::iterator it = kvs_.begin();
EXPECT_EQ(it++, kvs_.begin());
@@ -430,7 +421,7 @@ TEST_F(EmptyInitializedKvs, Iterator_PostIncrement) {
}
TEST_F(EmptyInitializedKvs, Iterator_PreIncrement) {
- ASSERT_EQ(OkStatus(), kvs_.Put("kEy", std::as_bytes(std::span("123"))));
+ ASSERT_EQ(OkStatus(), kvs_.Put("kEy", as_bytes(span("123"))));
KeyValueStore::iterator it = kvs_.begin();
EXPECT_EQ(++it, kvs_.end());
@@ -440,9 +431,8 @@ TEST_F(EmptyInitializedKvs, Iterator_PreIncrement) {
TEST_F(EmptyInitializedKvs, Basic) {
// Add some data
uint8_t value1 = 0xDA;
- ASSERT_EQ(
- OkStatus(),
- kvs_.Put(keys[0], std::as_bytes(std::span(&value1, sizeof(value1)))));
+ ASSERT_EQ(OkStatus(),
+ kvs_.Put(keys[0], as_bytes(span(&value1, sizeof(value1)))));
uint32_t value2 = 0xBAD0301f;
ASSERT_EQ(OkStatus(), kvs_.Put(keys[1], value2));
@@ -462,15 +452,14 @@ TEST_F(EmptyInitializedKvs, Basic) {
// Verify it was erased
EXPECT_EQ(kvs_.Get(keys[0], &test1), Status::NotFound());
test2 = 0;
- ASSERT_EQ(OkStatus(),
- kvs_.Get(keys[1],
- std::span(reinterpret_cast<byte*>(&test2), sizeof(test2)))
- .status());
+ ASSERT_EQ(
+ OkStatus(),
+ kvs_.Get(keys[1], span(reinterpret_cast<byte*>(&test2), sizeof(test2)))
+ .status());
EXPECT_EQ(test2, value2);
// Delete other key
- kvs_.Delete(keys[1])
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), kvs_.Delete(keys[1]));
// Verify it was erased
EXPECT_EQ(kvs_.size(), 0u);
diff --git a/pw_kvs/key_value_store_map_test.cc b/pw_kvs/key_value_store_map_test.cc
index 45ced1459..289b30386 100644
--- a/pw_kvs/key_value_store_map_test.cc
+++ b/pw_kvs/key_value_store_map_test.cc
@@ -15,12 +15,13 @@
#include <cstdlib>
#include <random>
#include <set>
-#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
+#include "pw_span/span.h"
+
#define DUMP_KVS_CONTENTS 0
#if DUMP_KVS_CONTENTS
@@ -34,6 +35,7 @@
#include "pw_kvs/internal/entry.h"
#include "pw_kvs/key_value_store.h"
#include "pw_log/log.h"
+#include "pw_string/string_builder.h"
namespace pw::kvs {
namespace {
@@ -131,10 +133,10 @@ class KvsTester {
std::string key;
// Either add a new key or replace an existing one.
- // TODO: Using %2 (or any less than 16) fails with redundancy due to KVS
- // filling up and not being able to write the second redundant entry,
- // returning error. After re-init() the new key is picked up, resulting
- // in a mis-match between KVS and the test map.
+ // TODO(davidrogers): Using %2 (or any less than 16) fails with
+ // redundancy due to KVS filling up and not being able to write the
+ // second redundant entry, returning error. After re-init() the new key
+ // is picked up, resulting in a mis-match between KVS and the test map.
if (empty() || random_int() % 16 == 0) {
key = random_string(random_int() %
(internal::Entry::kMaxKeyLength + 1));
@@ -164,8 +166,9 @@ class KvsTester {
label << ((options == kReinitWithPartialGC) ? "PartialGC" : "");
label << ((kvs_.redundancy() > 1) ? "Redundant" : "");
- partition_.SaveStorageStats(kvs_, label.data())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ // Ignore error to allow test to pass on platforms where writing out the
+ // stats is not possible.
+ partition_.SaveStorageStats(kvs_, label.data()).IgnoreError();
}
}
@@ -250,7 +253,7 @@ class KvsTester {
char value[kMaxValueLength + 1] = {};
EXPECT_EQ(OkStatus(),
- item.Get(std::as_writable_bytes(std::span(value))).status());
+ item.Get(as_writable_bytes(span(value))).status());
EXPECT_EQ(map_entry->second, std::string(value));
}
}
@@ -263,7 +266,7 @@ class KvsTester {
StartOperation("Put", key);
EXPECT_LE(value.size(), kMaxValueLength);
- Status result = kvs_.Put(key, std::as_bytes(std::span(value)));
+ Status result = kvs_.Put(key, as_bytes(span(value)));
if (key.empty() || key.size() > internal::Entry::kMaxKeyLength) {
EXPECT_EQ(Status::InvalidArgument(), result);
@@ -414,11 +417,13 @@ FakeFlashMemoryBuffer<kParams.sector_size,
(kParams.sector_count * kParams.redundancy)>(
kParams.sector_alignment);
-#define _TEST(fixture, test, ...) \
- _TEST_VARIANT(fixture, test, test, __VA_ARGS__)
+#define _TEST(fixture, test, ...) \
+ _TEST_VARIANT(fixture, test, test, __VA_ARGS__); \
+ static_assert(true, "Macros must be terminated with a semicolon")
-#define _TEST_VARIANT(fixture, test, variant, ...) \
- TEST_F(fixture, test##variant) { tester_.Test_##test(__VA_ARGS__); }
+#define _TEST_VARIANT(fixture, test, variant, ...) \
+ TEST_F(fixture, test##variant) { tester_.Test_##test(__VA_ARGS__); } \
+ static_assert(true, "Macros must be terminated with a semicolon")
// Defines a test fixture that runs all tests against a flash with the specified
// parameters.
diff --git a/pw_kvs/key_value_store_put_test.cc b/pw_kvs/key_value_store_put_test.cc
index 5e69f3fdb..62e01a379 100644
--- a/pw_kvs/key_value_store_put_test.cc
+++ b/pw_kvs/key_value_store_put_test.cc
@@ -13,6 +13,7 @@
// the License.
#include "gtest/gtest.h"
+#include "pw_assert/check.h"
#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/fake_flash_memory.h"
#include "pw_kvs/flash_partition_with_stats.h"
@@ -43,9 +44,9 @@ class EmptyInitializedKvs : public ::testing::Test {
// human readable 4 bytes. See pw_kvs/format.h for more information.
EmptyInitializedKvs()
: kvs_(&test_partition, {.magic = 0x873a9b50, .checksum = &checksum}) {
- test_partition.Erase(0, test_partition.sector_count())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- ASSERT_EQ(OkStatus(), kvs_.Init());
+ EXPECT_EQ(OkStatus(),
+ test_partition.Erase(0, test_partition.sector_count()));
+ PW_CHECK_OK(kvs_.Init());
}
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_;
@@ -64,13 +65,15 @@ TEST_F(EmptyInitializedKvs, Put_VaryingKeysAndValues) {
for (unsigned value_size = 0; value_size < sizeof(value); ++value_size) {
ASSERT_EQ(OkStatus(),
kvs_.Put(std::string_view(value, key_size),
- std::as_bytes(std::span(value, value_size))));
+ as_bytes(span(value, value_size))));
}
}
}
+ // Ignore error to allow test to pass on platforms where writing out the stats
+ // is not possible.
test_partition.SaveStorageStats(kvs_, "Put_VaryingKeysAndValues")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
}
} // namespace
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
index 24f5daff4..11e410249 100644
--- a/pw_kvs/key_value_store_test.cc
+++ b/pw_kvs/key_value_store_test.cc
@@ -20,19 +20,22 @@
#include <array>
#include <cstdio>
#include <cstring>
-#include <span>
+
+#include "pw_span/span.h"
#if DUMP_KVS_STATE_TO_FILE
#include <vector>
#endif // DUMP_KVS_STATE_TO_FILE
#include "gtest/gtest.h"
+#include "pw_assert/check.h"
#include "pw_bytes/array.h"
#include "pw_checksum/crc16_ccitt.h"
#include "pw_kvs/crc16_checksum.h"
#include "pw_kvs/fake_flash_memory.h"
#include "pw_kvs/flash_memory.h"
#include "pw_kvs/internal/entry.h"
+#include "pw_kvs_private/config.h"
#include "pw_log/log.h"
#include "pw_log/shorter.h"
#include "pw_status/status.h"
@@ -67,7 +70,7 @@ struct FlashWithPartitionFake {
}
std::vector<std::byte> out_vec(memory.size_bytes());
Status status =
- memory.Read(0, std::span<std::byte>(out_vec.data(), out_vec.size()));
+ memory.Read(0, span<std::byte>(out_vec.data(), out_vec.size()));
if (status != OkStatus()) {
fclose(out_file);
return status;
@@ -203,8 +206,7 @@ TEST(InMemoryKvs, WriteOneKeyMultipleTimes) {
sizeof(fname_buf),
"WriteOneKeyMultipleTimes_%d.bin",
reload);
- flash.Dump(fname_buf)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), flash.Dump(fname_buf));
}
}
@@ -233,8 +235,7 @@ TEST(InMemoryKvs, WritingMultipleKeysIncreasesSize) {
EXPECT_OK(kvs.Put(key.view(), value));
EXPECT_EQ(kvs.size(), i + 1);
}
- flash.Dump("WritingMultipleKeysIncreasesSize.bin")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), flash.Dump("WritingMultipleKeysIncreasesSize.bin"));
}
TEST(InMemoryKvs, WriteAndReadOneKey) {
@@ -314,7 +315,7 @@ TEST(InMemoryKvs, Basic) {
// Add two entries with different keys and values.
uint8_t value1 = 0xDA;
- ASSERT_OK(kvs.Put(key1, std::as_bytes(std::span(&value1, sizeof(value1)))));
+ ASSERT_OK(kvs.Put(key1, as_bytes(span(&value1, sizeof(value1)))));
EXPECT_EQ(kvs.size(), 1u);
uint32_t value2 = 0xBAD0301f;
@@ -359,8 +360,8 @@ TEST(InMemoryKvs, CallingEraseTwice_NothingWrittenToFlash) {
class LargeEmptyInitializedKvs : public ::testing::Test {
protected:
LargeEmptyInitializedKvs() : kvs_(&large_test_partition, default_format) {
- ASSERT_EQ(OkStatus(), large_test_partition.Erase());
- ASSERT_EQ(OkStatus(), kvs_.Init());
+ PW_CHECK_OK(large_test_partition.Erase());
+ PW_CHECK_OK(kvs_.Init());
}
KeyValueStoreBuffer<kMaxEntries, kMaxUsableSectors> kvs_;
@@ -412,6 +413,68 @@ TEST_F(LargeEmptyInitializedKvs, FullMaintenance) {
EXPECT_EQ(stats.reclaimable_bytes, 0u);
}
+TEST_F(LargeEmptyInitializedKvs, KeyDeletionMaintenance) {
+ const uint8_t kValue1 = 0xDA;
+ const uint8_t kValue2 = 0x12;
+ uint8_t val = 0;
+
+ // Write and delete a key. The key should be gone, but the size should be 1.
+ ASSERT_EQ(OkStatus(), kvs_.Put(keys[0], kValue1));
+ ASSERT_EQ(kvs_.size(), 1u);
+ ASSERT_EQ(OkStatus(), kvs_.Delete(keys[0]));
+
+ // Ensure the key is indeed gone and the size is 1 before continuing.
+ ASSERT_EQ(kvs_.Get(keys[0], &val), Status::NotFound());
+ ASSERT_EQ(kvs_.size(), 0u);
+ ASSERT_EQ(kvs_.total_entries_with_deleted(), 1u);
+
+ KeyValueStore::StorageStats stats = kvs_.GetStorageStats();
+ EXPECT_EQ(stats.sector_erase_count, 0u);
+ EXPECT_GT(stats.reclaimable_bytes, 0u);
+
+ // Do aggressive FullMaintenance, which should GC the sector with valid data,
+ // resulting in no reclaimable bytes and an erased sector.
+ EXPECT_EQ(OkStatus(), kvs_.HeavyMaintenance());
+ stats = kvs_.GetStorageStats();
+ EXPECT_EQ(stats.reclaimable_bytes, 0u);
+ ASSERT_EQ(kvs_.size(), 0u);
+
+ if (PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE) {
+ EXPECT_GT(stats.sector_erase_count, 1u);
+ ASSERT_EQ(kvs_.total_entries_with_deleted(), 0u);
+ } else { // The deleted entries are only removed if that feature is enabled.
+ EXPECT_EQ(stats.sector_erase_count, 1u);
+ ASSERT_EQ(kvs_.total_entries_with_deleted(), 1u);
+ }
+
+ // Do it again but with 2 keys and keep one.
+ ASSERT_EQ(OkStatus(), kvs_.Put(keys[0], kValue1));
+ ASSERT_EQ(OkStatus(), kvs_.Put(keys[1], kValue2));
+ ASSERT_EQ(kvs_.size(), 2u);
+ ASSERT_EQ(OkStatus(), kvs_.Delete(keys[0]));
+
+ // Ensure the key is indeed gone and the size is 1 before continuing.
+ ASSERT_EQ(kvs_.Get(keys[0], &val), Status::NotFound());
+ ASSERT_EQ(kvs_.size(), 1u);
+ ASSERT_EQ(kvs_.total_entries_with_deleted(), 2u);
+
+ // Do aggressive FullMaintenance, which should GC the sector with valid data,
+ // resulting in no reclaimable bytes and an erased sector.
+ EXPECT_EQ(OkStatus(), kvs_.HeavyMaintenance());
+ stats = kvs_.GetStorageStats();
+ ASSERT_EQ(kvs_.size(), 1u);
+
+ if (PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE) {
+ ASSERT_EQ(kvs_.total_entries_with_deleted(), 1u);
+ } else { // The deleted entries are only removed if that feature is enabled.
+ ASSERT_EQ(kvs_.total_entries_with_deleted(), 2u);
+ }
+
+ // Read back the second key to make sure it is still valid.
+ ASSERT_EQ(kvs_.Get(keys[1], &val), OkStatus());
+ ASSERT_EQ(val, kValue2);
+}
+
TEST(InMemoryKvs, Put_MaxValueSize) {
// Create and erase the fake flash.
Flash flash;
@@ -433,7 +496,7 @@ TEST(InMemoryKvs, Put_MaxValueSize) {
// Use the large_test_flash as a big chunk of data for the Put statement.
ASSERT_GT(sizeof(large_test_flash), max_value_size + 2 * sizeof(EntryHeader));
- auto big_data = std::as_bytes(std::span(&large_test_flash, 1));
+ auto big_data = as_bytes(span(&large_test_flash, 1));
EXPECT_EQ(OkStatus(), kvs.Put("K", big_data.subspan(0, max_value_size)));
diff --git a/pw_kvs/key_value_store_wear_test.cc b/pw_kvs/key_value_store_wear_test.cc
index 94bb04207..c909697e8 100644
--- a/pw_kvs/key_value_store_wear_test.cc
+++ b/pw_kvs/key_value_store_wear_test.cc
@@ -65,7 +65,7 @@ TEST_F(WearTest, RepeatedLargeEntry) {
// written.
test_data[0]++;
- EXPECT_TRUE(kvs_.Put("large_entry", std::span(test_data)).ok());
+ EXPECT_TRUE(kvs_.Put("large_entry", span(test_data)).ok());
}
// Ensure every sector has been erased at several times due to garbage
@@ -73,8 +73,10 @@ TEST_F(WearTest, RepeatedLargeEntry) {
EXPECT_GE(partition_.min_erase_count(), 7u);
EXPECT_LE(partition_.max_erase_count(), partition_.min_erase_count() + 1u);
+ // Ignore error to allow test to pass on platforms where writing out the stats
+ // is not possible.
partition_.SaveStorageStats(kvs_, "WearTest RepeatedLargeEntry")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
}
// Test a KVS with a number of entries, several sectors that are nearly full
@@ -94,8 +96,7 @@ TEST_F(WearTest, TwoPassFillWithLargeAndLarger) {
EXPECT_EQ(
OkStatus(),
- kvs_.Put("key",
- std::as_bytes(std::span(test_data, sizeof(test_data) - 70))));
+ kvs_.Put("key", as_bytes(span(test_data, sizeof(test_data) - 70))));
}
// Add many copies of a differently sized entry that is larger than the
@@ -105,7 +106,7 @@ TEST_F(WearTest, TwoPassFillWithLargeAndLarger) {
// written.
test_data[0]++;
- printf("Add entry %zu\n", i);
+ PW_LOG_DEBUG("Add entry %zu\n", i);
EXPECT_EQ(OkStatus(), kvs_.Put("big_key", test_data));
}
diff --git a/pw_kvs/public/pw_kvs/alignment.h b/pw_kvs/public/pw_kvs/alignment.h
index 4965895f7..f8050d4ff 100644
--- a/pw_kvs/public/pw_kvs/alignment.h
+++ b/pw_kvs/public/pw_kvs/alignment.h
@@ -17,11 +17,11 @@
#include <cstddef>
#include <cstring>
#include <initializer_list>
-#include <span>
#include <utility>
#include "pw_bytes/span.h"
#include "pw_kvs/io.h"
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
namespace pw {
@@ -47,9 +47,7 @@ constexpr size_t Padding(size_t length, size_t alignment) {
// called or the AlignedWriter goes out of scope.
class AlignedWriter {
public:
- AlignedWriter(std::span<std::byte> buffer,
- size_t alignment_bytes,
- Output& writer)
+ AlignedWriter(span<std::byte> buffer, size_t alignment_bytes, Output& writer)
: buffer_(buffer.data()),
write_size_(AlignDown(buffer.size(), alignment_bytes)),
alignment_bytes_(alignment_bytes),
@@ -59,8 +57,11 @@ class AlignedWriter {
// TODO(hepler): Add DCHECK to ensure that buffer.size() >= alignment_bytes.
}
+ AlignedWriter(const AlignedWriter&) = delete;
+ AlignedWriter& operator=(const AlignedWriter&) = delete;
+
~AlignedWriter() {
- Flush().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ Flush().IgnoreError(); // TODO(b/242598609): Handle Status properly
}
// Writes bytes to the AlignedWriter. The output may be called if the internal
@@ -71,11 +72,11 @@ class AlignedWriter {
// successful and failed Write calls. On a failed write call, knowing the
// bytes attempted may be important when working with flash memory, since it
// can only be written once between erases.
- StatusWithSize Write(std::span<const std::byte> data);
+ StatusWithSize Write(span<const std::byte> data);
StatusWithSize Write(const void* data, size_t size) {
return Write(
- std::span<const std::byte>(static_cast<const std::byte*>(data), size));
+ span<const std::byte>(static_cast<const std::byte*>(data), size));
}
// Reads size bytes from the input and writes them to the output.
@@ -116,16 +117,16 @@ class AlignedWriterBuffer : public AlignedWriter {
template <size_t kBufferSize>
StatusWithSize AlignedWrite(Output& output,
size_t alignment_bytes,
- std::span<const std::span<const std::byte>> data) {
- // TODO: This should convert to PW_CHECK once that is available for use in
- // host tests.
+ span<const span<const std::byte>> data) {
+ // TODO(davidrogers): This should convert to PW_CHECK once that is available
+ // for use in host tests.
if (alignment_bytes > kBufferSize) {
return StatusWithSize::Internal();
}
AlignedWriterBuffer<kBufferSize> buffer(alignment_bytes, output);
- for (const std::span<const std::byte>& chunk : data) {
+ for (const span<const std::byte>& chunk : data) {
StatusWithSize result = buffer.Write(chunk);
if (!result.ok()) {
return result;
@@ -137,14 +138,13 @@ StatusWithSize AlignedWrite(Output& output,
// Calls AlignedWrite with an initializer list.
template <size_t kBufferSize>
-StatusWithSize AlignedWrite(
- Output& output,
- size_t alignment_bytes,
- std::initializer_list<std::span<const std::byte>> data) {
+StatusWithSize AlignedWrite(Output& output,
+ size_t alignment_bytes,
+ std::initializer_list<span<const std::byte>> data) {
return AlignedWrite<kBufferSize>(
output,
alignment_bytes,
- std::span<const ConstByteSpan>(data.begin(), data.size()));
+ span<const ConstByteSpan>(data.begin(), data.size()));
}
} // namespace pw
diff --git a/pw_kvs/public/pw_kvs/checksum.h b/pw_kvs/public/pw_kvs/checksum.h
index 9b6a3be81..9a307ea0e 100644
--- a/pw_kvs/public/pw_kvs/checksum.h
+++ b/pw_kvs/public/pw_kvs/checksum.h
@@ -14,9 +14,9 @@
#pragma once
#include <cstddef>
-#include <span>
#include "pw_kvs/alignment.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw {
@@ -28,19 +28,19 @@ class ChecksumAlgorithm {
virtual void Reset() = 0;
// Updates the checksum with the provided data.
- virtual void Update(std::span<const std::byte> data) = 0;
+ virtual void Update(span<const std::byte> data) = 0;
// Updates the checksum from a pointer and size.
void Update(const void* data, size_t size_bytes) {
- return Update(std::span<const std::byte>(
- static_cast<const std::byte*>(data), size_bytes));
+ return Update(
+ span<const std::byte>(static_cast<const std::byte*>(data), size_bytes));
}
// Returns the final result of the checksum. Update() can no longer be called
- // after this. The returned std::span is valid until a call to Reset().
+ // after this. The returned span is valid until a call to Reset().
//
// Finish MUST be called before calling Verify.
- std::span<const std::byte> Finish() {
+ span<const std::byte> Finish() {
Finalize(); // Implemented by derived classes, if required.
return state();
}
@@ -53,25 +53,24 @@ class ChecksumAlgorithm {
// size_bytes() are ignored.
//
// Finish MUST be called before calling Verify.
- Status Verify(std::span<const std::byte> checksum) const;
+ Status Verify(span<const std::byte> checksum) const;
protected:
- // A derived class provides a std::span of its state buffer.
- constexpr ChecksumAlgorithm(std::span<const std::byte> state)
- : state_(state) {}
+ // A derived class provides a span of its state buffer.
+ constexpr ChecksumAlgorithm(span<const std::byte> state) : state_(state) {}
// Protected destructor prevents deleting ChecksumAlgorithms from the base
// class, so that it is safe to have a non-virtual destructor.
~ChecksumAlgorithm() = default;
// Returns the current checksum state.
- constexpr std::span<const std::byte> state() const { return state_; }
+ constexpr span<const std::byte> state() const { return state_; }
private:
// Checksums that require finalizing operations may override this method.
virtual void Finalize() {}
- std::span<const std::byte> state_;
+ span<const std::byte> state_;
};
// A checksum algorithm for which Verify always passes. This can be used to
@@ -81,7 +80,7 @@ class IgnoreChecksum final : public ChecksumAlgorithm {
constexpr IgnoreChecksum() : ChecksumAlgorithm({}) {}
void Reset() override {}
- void Update(std::span<const std::byte>) override {}
+ void Update(span<const std::byte>) override {}
};
// Calculates a checksum in kAlignmentBytes chunks. Checksum classes can inherit
@@ -90,13 +89,13 @@ class IgnoreChecksum final : public ChecksumAlgorithm {
template <size_t kAlignmentBytes, size_t kBufferSize = kAlignmentBytes>
class AlignedChecksum : public ChecksumAlgorithm {
public:
- void Update(std::span<const std::byte> data) final {
+ void Update(span<const std::byte> data) final {
writer_.Write(data)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
protected:
- constexpr AlignedChecksum(std::span<const std::byte> state)
+ constexpr AlignedChecksum(span<const std::byte> state)
: ChecksumAlgorithm(state),
output_(*this),
writer_(kAlignmentBytes, output_) {}
@@ -107,11 +106,11 @@ class AlignedChecksum : public ChecksumAlgorithm {
static_assert(kBufferSize >= kAlignmentBytes);
void Finalize() final {
- writer_.Flush().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ writer_.Flush().IgnoreError(); // TODO(b/242598609): Handle Status properly
FinalizeAligned();
}
- virtual void UpdateAligned(std::span<const std::byte> data) = 0;
+ virtual void UpdateAligned(span<const std::byte> data) = 0;
virtual void FinalizeAligned() = 0;
@@ -120,7 +119,7 @@ class AlignedChecksum : public ChecksumAlgorithm {
constexpr CallUpdateAligned(AlignedChecksum& object) : object_(object) {}
private:
- StatusWithSize DoWrite(std::span<const std::byte> data) override {
+ StatusWithSize DoWrite(span<const std::byte> data) override {
object_.UpdateAligned(data);
return StatusWithSize(data.size());
}
diff --git a/pw_kvs/public/pw_kvs/crc16_checksum.h b/pw_kvs/public/pw_kvs/crc16_checksum.h
index 852e192ba..2ad63e523 100644
--- a/pw_kvs/public/pw_kvs/crc16_checksum.h
+++ b/pw_kvs/public/pw_kvs/crc16_checksum.h
@@ -13,21 +13,19 @@
// the License.
#pragma once
-#include <span>
-
#include "pw_checksum/crc16_ccitt.h"
#include "pw_kvs/checksum.h"
+#include "pw_span/span.h"
namespace pw::kvs {
class ChecksumCrc16 final : public ChecksumAlgorithm {
public:
- ChecksumCrc16()
- : ChecksumAlgorithm(std::as_bytes(std::span<uint16_t>(&crc_, 1))) {}
+ ChecksumCrc16() : ChecksumAlgorithm(as_bytes(span<uint16_t>(&crc_, 1))) {}
void Reset() override { crc_ = checksum::Crc16Ccitt::kInitialValue; }
- void Update(std::span<const std::byte> data) override {
+ void Update(span<const std::byte> data) override {
crc_ = checksum::Crc16Ccitt::Calculate(data, crc_);
}
diff --git a/pw_kvs/public/pw_kvs/fake_flash_memory.h b/pw_kvs/public/pw_kvs/fake_flash_memory.h
index 49e36d747..c15111b7f 100644
--- a/pw_kvs/public/pw_kvs/fake_flash_memory.h
+++ b/pw_kvs/public/pw_kvs/fake_flash_memory.h
@@ -17,10 +17,10 @@
#include <array>
#include <cstddef>
#include <cstring>
-#include <span>
#include "pw_containers/vector.h"
#include "pw_kvs/flash_memory.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::kvs {
@@ -50,7 +50,7 @@ class FlashError {
Status Check(FlashMemory::Address start_address, size_t size);
// Determines if any of a series of FlashErrors applies to the operation.
- static Status Check(std::span<FlashError> errors,
+ static Status Check(span<FlashError> errors,
FlashMemory::Address address,
size_t size);
@@ -85,7 +85,7 @@ class FakeFlashMemory : public FlashMemory {
static constexpr std::byte kErasedValue = std::byte{0xff};
- FakeFlashMemory(std::span<std::byte> buffer,
+ FakeFlashMemory(span<std::byte> buffer,
size_t sector_size,
size_t sector_count,
size_t alignment_bytes = kDefaultAlignmentBytes,
@@ -107,11 +107,10 @@ class FakeFlashMemory : public FlashMemory {
Status Erase(Address address, size_t num_sectors) override;
// Reads bytes from flash into buffer.
- StatusWithSize Read(Address address, std::span<std::byte> output) override;
+ StatusWithSize Read(Address address, span<std::byte> output) override;
// Writes bytes to flash.
- StatusWithSize Write(Address address,
- std::span<const std::byte> data) override;
+ StatusWithSize Write(Address address, span<const std::byte> data) override;
std::byte* FlashAddressToMcuAddress(Address) const override;
@@ -119,7 +118,7 @@ class FakeFlashMemory : public FlashMemory {
// Access the underlying buffer for testing purposes. Not part of the
// FlashMemory API.
- std::span<std::byte> buffer() const { return buffer_; }
+ span<std::byte> buffer() const { return buffer_; }
bool InjectReadError(const FlashError& error) {
if (read_errors_.full()) {
@@ -140,7 +139,7 @@ class FakeFlashMemory : public FlashMemory {
private:
static inline Vector<FlashError, 0> no_errors_;
- const std::span<std::byte> buffer_;
+ const span<std::byte> buffer_;
Vector<FlashError>& read_errors_;
Vector<FlashError>& write_errors_;
};
@@ -165,7 +164,7 @@ class FakeFlashMemoryBuffer : public FakeFlashMemory {
// Creates a flash memory initialized to the provided contents.
explicit FakeFlashMemoryBuffer(
- std::span<const std::byte> contents,
+ span<const std::byte> contents,
size_t alignment_bytes = kDefaultAlignmentBytes)
: FakeFlashMemoryBuffer(alignment_bytes) {
std::memcpy(buffer_.data(),
diff --git a/pw_kvs/public/pw_kvs/flash_memory.h b/pw_kvs/public/pw_kvs/flash_memory.h
index 073db84da..37b5cdc1a 100644
--- a/pw_kvs/public/pw_kvs/flash_memory.h
+++ b/pw_kvs/public/pw_kvs/flash_memory.h
@@ -16,11 +16,11 @@
#include <cstddef>
#include <cstdint>
#include <initializer_list>
-#include <span>
#include "pw_assert/assert.h"
#include "pw_kvs/alignment.h"
#include "pw_polyfill/standard.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -42,7 +42,7 @@ class FlashMemory {
// The flash address is in the range of: 0 to FlashSize.
typedef uint32_t Address;
- // TODO(pwbug/246): This can be constexpr when tokenized asserts are fixed.
+ // TODO(b/235149326): This can be constexpr when tokenized asserts are fixed.
FlashMemory(size_t sector_size,
size_t sector_count,
size_t alignment,
@@ -82,11 +82,10 @@ class FlashMemory {
// OK - success
// DEADLINE_EXCEEDED - timeout
// OUT_OF_RANGE - write does not fit in the flash memory
- virtual StatusWithSize Read(Address address, std::span<std::byte> output) = 0;
+ virtual StatusWithSize Read(Address address, span<std::byte> output) = 0;
StatusWithSize Read(Address address, void* buffer, size_t len) {
- return Read(address,
- std::span<std::byte>(static_cast<std::byte*>(buffer), len));
+ return Read(address, span<std::byte>(static_cast<std::byte*>(buffer), len));
}
// Writes bytes to flash. Blocking call. Returns:
@@ -96,14 +95,14 @@ class FlashMemory {
// INVALID_ARGUMENT - address or data size are not aligned
// OUT_OF_RANGE - write does not fit in the memory
virtual StatusWithSize Write(Address destination_flash_address,
- std::span<const std::byte> data) = 0;
+ span<const std::byte> data) = 0;
StatusWithSize Write(Address destination_flash_address,
const void* data,
size_t len) {
return Write(
destination_flash_address,
- std::span<const std::byte>(static_cast<const std::byte*>(data), len));
+ span<const std::byte>(static_cast<const std::byte*>(data), len));
}
// Convert an Address to an MCU pointer, this can be used for memory
@@ -155,7 +154,7 @@ class FlashPartition {
private:
Status DoWrite(ConstByteSpan data) override;
- size_t DoTell() const override { return position_; }
+ size_t DoTell() override { return position_; }
size_t ConservativeLimit(LimitType type) const override {
return type == LimitType::kWrite ? partition_.size_bytes() - position_
@@ -177,7 +176,7 @@ class FlashPartition {
private:
StatusWithSize DoRead(ByteSpan data) override;
- size_t DoTell() const override { return position_; }
+ size_t DoTell() override { return position_; }
Status DoSeek(ptrdiff_t offset, Whence origin) override {
return CalculateSeek(offset, origin, partition_.size_bytes(), position_);
@@ -199,7 +198,7 @@ class FlashPartition {
: flash_(flash), address_(address) {}
private:
- StatusWithSize DoWrite(std::span<const std::byte> data) override;
+ StatusWithSize DoWrite(span<const std::byte> data) override;
FlashPartition& flash_;
FlashPartition::Address address_;
@@ -212,7 +211,7 @@ class FlashPartition {
: flash_(flash), address_(address) {}
private:
- StatusWithSize DoRead(std::span<std::byte> data) override;
+ StatusWithSize DoRead(span<std::byte> data) override;
FlashPartition& flash_;
FlashPartition::Address address_;
@@ -220,8 +219,8 @@ class FlashPartition {
FlashPartition(
FlashMemory* flash,
- uint32_t start_sector_index,
- uint32_t sector_count,
+ uint32_t flash_start_sector_index,
+ uint32_t flash_sector_count,
uint32_t alignment_bytes = 0, // Defaults to flash alignment
PartitionPermission permission = PartitionPermission::kReadAndWrite);
@@ -257,11 +256,11 @@ class FlashPartition {
// TIMEOUT - on timeout.
// INVALID_ARGUMENT - address or length is invalid.
// UNKNOWN - HAL error
- virtual StatusWithSize Read(Address address, std::span<std::byte> output);
+ virtual StatusWithSize Read(Address address, span<std::byte> output);
StatusWithSize Read(Address address, size_t length, void* output) {
return Read(address,
- std::span<std::byte>(static_cast<std::byte*>(output), length));
+ span<std::byte>(static_cast<std::byte*>(output), length));
}
// Writes bytes to flash. Address and data.size_bytes() must both be a
@@ -272,8 +271,7 @@ class FlashPartition {
// INVALID_ARGUMENT - address or length is invalid.
// PERMISSION_DENIED - partition is read only.
// UNKNOWN - HAL error
- virtual StatusWithSize Write(Address address,
- std::span<const std::byte> data);
+ virtual StatusWithSize Write(Address address, span<const std::byte> data);
// Check to see if chunk of flash partition is erased. Address and len need to
// be aligned with FlashMemory. Returns:
@@ -282,7 +280,7 @@ class FlashPartition {
// TIMEOUT - on timeout.
// INVALID_ARGUMENT - address or length is invalid.
// UNKNOWN - HAL error
- // TODO: Result<bool>
+ // TODO(hepler): Result<bool>
virtual Status IsRegionErased(Address source_flash_address,
size_t length,
bool* is_erased);
@@ -296,22 +294,36 @@ class FlashPartition {
// Checks to see if the data appears to be erased. No reads or writes occur;
// the FlashPartition simply compares the data to
// flash_.erased_memory_content().
- bool AppearsErased(std::span<const std::byte> data) const;
+ bool AppearsErased(span<const std::byte> data) const;
- // Overridden by derived classes. The reported sector size is space available
- // to users of FlashPartition. It accounts for space reserved in the sector
- // for FlashPartition to store metadata.
+ // Optionally overridden by derived classes. The reported sector size is space
+ // available to users of FlashPartition. This reported size can be smaller or
+ // larger than the sector size of the backing FlashMemory.
+ //
+ // Possible reasons for size to be different from the backing FlashMemory
+ // could be due to space reserved in the sector for FlashPartition to store
+ // metadata or due to logical FlashPartition sectors that combine several
+ // FlashMemory sectors.
virtual size_t sector_size_bytes() const {
return flash_.sector_size_bytes();
}
+ // Optionally overridden by derived classes. The reported sector count is
+ // sectors available to users of FlashPartition. This reported count can be
+ // same or smaller than the given flash_sector_count of the backing
+ // FlashMemory for the same region of flash.
+ //
+ // Possible reasons for count to be different from the backing FlashMemory
+ // could be due to space reserved in the FlashPartition to store metadata or
+ // due to logical FlashPartition sectors that combine several FlashMemory
+ // sectors.
+ virtual size_t sector_count() const { return flash_sector_count_; }
+
size_t size_bytes() const { return sector_count() * sector_size_bytes(); }
// Alignment required for write address and write size.
size_t alignment_bytes() const { return alignment_bytes_; }
- size_t sector_count() const { return sector_count_; }
-
// Convert a FlashMemory::Address to an MCU pointer, this can be used for
// memory mapped reads. Return NULL if the memory is not memory mapped.
std::byte* PartitionAddressToMcuAddress(Address address) const {
@@ -323,7 +335,8 @@ class FlashPartition {
// address space may not be contiguous, and this conversion accounts for that.
virtual FlashMemory::Address PartitionToFlashAddress(Address address) const {
return flash_.start_address() +
- (start_sector_index_ - flash_.start_sector()) * sector_size_bytes() +
+ (flash_start_sector_index_ - flash_.start_sector()) *
+ flash_.sector_size_bytes() +
address;
}
@@ -335,17 +348,18 @@ class FlashPartition {
return flash_.erased_memory_content();
}
- uint32_t start_sector_index() const { return start_sector_index_; }
+ uint32_t start_sector_index() const { return flash_start_sector_index_; }
protected:
Status CheckBounds(Address address, size_t len) const;
FlashMemory& flash() const { return flash_; }
- private:
FlashMemory& flash_;
- const uint32_t start_sector_index_;
- const uint32_t sector_count_;
+ const uint32_t flash_sector_count_;
+
+ private:
+ const uint32_t flash_start_sector_index_;
const uint32_t alignment_bytes_;
const PartitionPermission permission_;
};
diff --git a/pw_kvs/public/pw_kvs/flash_partition_with_stats.h b/pw_kvs/public/pw_kvs/flash_partition_with_stats.h
index 4baca4ada..c914eae62 100644
--- a/pw_kvs/public/pw_kvs/flash_partition_with_stats.h
+++ b/pw_kvs/public/pw_kvs/flash_partition_with_stats.h
@@ -38,8 +38,8 @@ class FlashPartitionWithStats : public FlashPartition {
Status Erase(Address address, size_t num_sectors) override;
- std::span<size_t> sector_erase_counters() {
- return std::span(sector_counters_.data(), sector_counters_.size());
+ span<size_t> sector_erase_counters() {
+ return span(sector_counters_.data(), sector_counters_.size());
}
size_t min_erase_count() const {
@@ -73,13 +73,13 @@ class FlashPartitionWithStats : public FlashPartition {
FlashPartitionWithStats(
Vector<size_t>& sector_counters,
FlashMemory* flash,
- uint32_t start_sector_index,
- uint32_t sector_count,
+ uint32_t flash_start_sector_index,
+ uint32_t flash_sector_count,
uint32_t alignment_bytes = 0, // Defaults to flash alignment
PartitionPermission permission = PartitionPermission::kReadAndWrite)
: FlashPartition(flash,
- start_sector_index,
- sector_count,
+ flash_start_sector_index,
+ flash_sector_count,
alignment_bytes,
permission),
sector_counters_(sector_counters) {
@@ -95,14 +95,14 @@ class FlashPartitionWithStatsBuffer : public FlashPartitionWithStats {
public:
FlashPartitionWithStatsBuffer(
FlashMemory* flash,
- uint32_t start_sector_index,
- uint32_t sector_count,
+ uint32_t flash_start_sector_index,
+ uint32_t flash_sector_count,
uint32_t alignment_bytes = 0, // Defaults to flash alignment
PartitionPermission permission = PartitionPermission::kReadAndWrite)
: FlashPartitionWithStats(sector_counters_,
flash,
- start_sector_index,
- sector_count,
+ flash_start_sector_index,
+ flash_sector_count,
alignment_bytes,
permission) {}
diff --git a/pw_kvs/public/pw_kvs/format.h b/pw_kvs/public/pw_kvs/format.h
index 309f786dd..cac373c30 100644
--- a/pw_kvs/public/pw_kvs/format.h
+++ b/pw_kvs/public/pw_kvs/format.h
@@ -14,14 +14,39 @@
#pragma once
#include <cstdint>
-#include <span>
#include "pw_kvs/checksum.h"
+#include "pw_span/span.h"
namespace pw {
namespace kvs {
-struct EntryFormat;
+// The EntryFormat defines properties of KVS entries that use a particular magic
+// number.
+struct EntryFormat {
+ // Magic is a unique constant identifier for entries.
+ //
+ // Upon reading from an address in flash, the magic number facilitiates
+ // quickly differentiating between:
+ //
+ // - Reading erased data - typically 0xFF - from flash.
+ // - Reading corrupted data
+ // - Reading a valid entry
+ //
+ // When selecting a magic for your particular KVS, pick a random 32 bit
+ // integer rather than a human readable 4 bytes. This decreases the
+ // probability of a collision with a real string when scanning in the case of
+ // corruption. To generate such a number:
+ /*
+ $ python3 -c 'import random; print(hex(random.randint(0,2**32)))'
+ 0xaf741757
+ */
+ uint32_t magic;
+
+ // The checksum algorithm is used to calculate checksums for KVS entries. If
+ // it is null, no checksum is used.
+ ChecksumAlgorithm* checksum;
+};
namespace internal {
@@ -60,7 +85,7 @@ static_assert(sizeof(EntryHeader) == 16, "EntryHeader must not have padding");
// simultaneously supported formats.
class EntryFormats {
public:
- explicit constexpr EntryFormats(std::span<const EntryFormat> formats)
+ explicit constexpr EntryFormats(span<const EntryFormat> formats)
: formats_(formats) {}
explicit constexpr EntryFormats(const EntryFormat& format)
@@ -73,37 +98,9 @@ class EntryFormats {
const EntryFormat* Find(uint32_t magic) const;
private:
- const std::span<const EntryFormat> formats_;
+ const span<const EntryFormat> formats_;
};
} // namespace internal
-
-// The EntryFormat defines properties of KVS entries that use a particular magic
-// number.
-struct EntryFormat {
- // Magic is a unique constant identifier for entries.
- //
- // Upon reading from an address in flash, the magic number facilitiates
- // quickly differentiating between:
- //
- // - Reading erased data - typically 0xFF - from flash.
- // - Reading corrupted data
- // - Reading a valid entry
- //
- // When selecting a magic for your particular KVS, pick a random 32 bit
- // integer rather than a human readable 4 bytes. This decreases the
- // probability of a collision with a real string when scanning in the case of
- // corruption. To generate such a number:
- /*
- $ python3 -c 'import random; print(hex(random.randint(0,2**32)))'
- 0xaf741757
- */
- uint32_t magic;
-
- // The checksum algorithm is used to calculate checksums for KVS entries. If
- // it is null, no checksum is used.
- ChecksumAlgorithm* checksum;
-};
-
} // namespace kvs
} // namespace pw
diff --git a/pw_kvs/public/pw_kvs/internal/entry.h b/pw_kvs/public/pw_kvs/internal/entry.h
index aa94cbbda..1aaf1548e 100644
--- a/pw_kvs/public/pw_kvs/internal/entry.h
+++ b/pw_kvs/public/pw_kvs/internal/entry.h
@@ -19,7 +19,6 @@
#include <array>
#include <cstddef>
#include <cstdint>
-#include <span>
#include "pw_kvs/alignment.h"
#include "pw_kvs/checksum.h"
@@ -28,6 +27,7 @@
#include "pw_kvs/internal/hash.h"
#include "pw_kvs/internal/key_descriptor.h"
#include "pw_kvs/key.h"
+#include "pw_span/span.h"
namespace pw {
namespace kvs {
@@ -66,7 +66,7 @@ class Entry {
Address address,
const EntryFormat& format,
Key key,
- std::span<const std::byte> value,
+ span<const std::byte> value,
uint32_t transaction_id) {
return Entry(
partition, address, format, key, value, value.size(), transaction_id);
@@ -97,7 +97,7 @@ class Entry {
deleted() ? EntryState::kDeleted : EntryState::kValid};
}
- StatusWithSize Write(Key key, std::span<const std::byte> value) const;
+ StatusWithSize Write(Key key, span<const std::byte> value) const;
// Changes the format and transcation ID for this entry. In order to calculate
// the new checksum, the entire entry is read into a small stack-allocated
@@ -119,19 +119,19 @@ class Entry {
ReadKey(partition(), address_, key_length(), key.data()), key_length());
}
- StatusWithSize ReadValue(std::span<std::byte> buffer,
+ StatusWithSize ReadValue(span<std::byte> buffer,
size_t offset_bytes = 0) const;
- Status ValueMatches(std::span<const std::byte> value) const;
+ Status ValueMatches(span<const std::byte> value) const;
- Status VerifyChecksum(Key key, std::span<const std::byte> value) const;
+ Status VerifyChecksum(Key key, span<const std::byte> value) const;
Status VerifyChecksumInFlash() const;
// Calculates the total size of an entry, including padding.
static size_t size(const FlashPartition& partition,
Key key,
- std::span<const std::byte> value) {
+ span<const std::byte> value) {
return AlignUp(sizeof(EntryHeader) + key.size() + value.size(),
std::max(partition.alignment_bytes(), kMinAlignmentBytes));
}
@@ -177,7 +177,7 @@ class Entry {
Address address,
const EntryFormat& format,
Key key,
- std::span<const std::byte> value,
+ span<const std::byte> value,
uint16_t value_size_bytes,
uint32_t transaction_id);
@@ -199,12 +199,12 @@ class Entry {
return sizeof(EntryHeader) + key_length() + value_size();
}
- std::span<const std::byte> checksum_bytes() const {
- return std::as_bytes(std::span<const uint32_t>(&header_.checksum, 1));
+ span<const std::byte> checksum_bytes() const {
+ return as_bytes(span<const uint32_t>(&header_.checksum, 1));
}
- std::span<const std::byte> CalculateChecksum(
- Key key, std::span<const std::byte> value) const;
+ span<const std::byte> CalculateChecksum(Key key,
+ span<const std::byte> value) const;
Status CalculateChecksumFromFlash();
diff --git a/pw_kvs/public/pw_kvs/internal/entry_cache.h b/pw_kvs/public/pw_kvs/internal/entry_cache.h
index ecf709430..179a7ccd4 100644
--- a/pw_kvs/public/pw_kvs/internal/entry_cache.h
+++ b/pw_kvs/public/pw_kvs/internal/entry_cache.h
@@ -15,7 +15,6 @@
#include <cstddef>
#include <cstdint>
-#include <span>
#include <type_traits>
#include "pw_containers/vector.h"
@@ -24,6 +23,7 @@
#include "pw_kvs/internal/key_descriptor.h"
#include "pw_kvs/internal/sectors.h"
#include "pw_kvs/key.h"
+#include "pw_span/span.h"
namespace pw {
namespace kvs {
@@ -47,19 +47,23 @@ class EntryMetadata {
uint32_t first_address() const { return addresses_[0]; }
// All addresses for this entry, including redundant entries, if any.
- const std::span<Address>& addresses() const { return addresses_; }
+ const span<Address>& addresses() const { return addresses_; }
// True if the KeyDesctiptor's transaction ID is newer than the specified ID.
bool IsNewerThan(uint32_t other_transaction_id) const {
- // TODO: Consider handling rollover.
+ // TODO(hepler): Consider handling rollover.
return transaction_id() > other_transaction_id;
}
// Adds a new address to the entry metadata. MUST NOT be called more times
// than allowed by the redundancy.
void AddNewAddress(Address address) {
- addresses_[addresses_.size()] = address;
- addresses_ = std::span<Address>(addresses_.begin(), addresses_.size() + 1);
+ // Each descriptor is given sufficient space in an EntryCache's address
+ // buffer to meet the redundancy requirements of an EntryCache. This object
+ // isn't aware of required redundancy, so there's no strict checking that
+ // this contract is respected.
+ addresses_ = span<Address>(addresses_.begin(), addresses_.size() + 1);
+ addresses_[addresses_.size() - 1] = address;
}
// Remove an address from the entry metadata.
@@ -72,12 +76,11 @@ class EntryMetadata {
private:
friend class EntryCache;
- constexpr EntryMetadata(KeyDescriptor& descriptor,
- std::span<Address> addresses)
+ constexpr EntryMetadata(KeyDescriptor& descriptor, span<Address> addresses)
: descriptor_(&descriptor), addresses_(addresses) {}
KeyDescriptor* descriptor_;
- std::span<Address> addresses_;
+ span<Address> addresses_;
};
// Tracks entry metadata. Combines KeyDescriptors and with their associated
@@ -97,7 +100,12 @@ class EntryCache {
++metadata_.descriptor_;
return *this;
}
- Iterator& operator++(int) { return operator++(); }
+
+ Iterator operator++(int) {
+ Iterator original = *this;
+ operator++();
+ return original;
+ }
// Updates the internal EntryMetadata object.
value_type& operator*() const {
@@ -183,6 +191,10 @@ class EntryCache {
Address address,
size_t sector_size_bytes) const;
+ // Removes an existing entry from the cache. Returns an iterator to the
+ // next entry so that iteration can continue.
+ iterator RemoveEntry(iterator& entry_it);
+
// Returns a pointer to an array of redundancy() addresses for temporary use.
// This is used by the KeyValueStore to track reserved addresses when finding
// space for a new entry.
@@ -218,8 +230,8 @@ class EntryCache {
// address slot available.
void AddAddressIfRoom(size_t descriptor_index, Address address) const;
- // Returns a std::span of the valid addresses for the descriptor.
- std::span<Address> addresses(size_t descriptor_index) const;
+ // Returns a span of the valid addresses for the descriptor.
+ span<Address> addresses(size_t descriptor_index) const;
Address* first_address(size_t descriptor_index) const {
return &addresses_[descriptor_index * redundancy_];
diff --git a/pw_kvs/public/pw_kvs/internal/key_descriptor.h b/pw_kvs/public/pw_kvs/internal/key_descriptor.h
index 8e05b3f9b..91c12cdb0 100644
--- a/pw_kvs/public/pw_kvs/internal/key_descriptor.h
+++ b/pw_kvs/public/pw_kvs/internal/key_descriptor.h
@@ -27,7 +27,7 @@ struct KeyDescriptor {
uint32_t key_hash;
uint32_t transaction_id;
- EntryState state; // TODO: Pack into transaction ID? or something?
+ EntryState state; // TODO(hepler): Pack into transaction ID? or something?
};
} // namespace internal
diff --git a/pw_kvs/public/pw_kvs/internal/sectors.h b/pw_kvs/public/pw_kvs/internal/sectors.h
index 760952f72..3478330a5 100644
--- a/pw_kvs/public/pw_kvs/internal/sectors.h
+++ b/pw_kvs/public/pw_kvs/internal/sectors.h
@@ -16,10 +16,10 @@
#include <climits>
#include <cstddef>
#include <cstdint>
-#include <span>
#include "pw_containers/vector.h"
#include "pw_kvs/flash_memory.h"
+#include "pw_span/span.h"
namespace pw {
namespace kvs {
@@ -50,7 +50,7 @@ class SectorDescriptor {
// Removes valid bytes without updating the writable bytes.
void RemoveValidBytes(uint16_t bytes) {
if (bytes > valid_bytes()) {
- // TODO: use a DCHECK instead -- this is a programming error
+ // TODO(hepler): use a DCHECK instead -- this is a programming error
valid_bytes_ = 0;
} else {
valid_bytes_ -= bytes;
@@ -60,7 +60,7 @@ class SectorDescriptor {
// Removes writable bytes without updating the valid bytes.
void RemoveWritableBytes(uint16_t bytes) {
if (bytes > writable_bytes()) {
- // TODO: use a DCHECK instead -- this is a programming error
+ // TODO(hepler): use a DCHECK instead -- this is a programming error
tail_free_bytes_ = 0;
} else {
tail_free_bytes_ -= bytes;
@@ -146,7 +146,7 @@ class Sectors {
}
SectorDescriptor& FromAddress(Address address) const {
- // TODO: Add boundary checking once asserts are supported.
+ // TODO(hepler): Add boundary checking once asserts are supported.
// DCHECK_LT(index, sector_map_size_);`
return descriptors_[address / partition_.sector_size_bytes()];
}
@@ -161,7 +161,7 @@ class Sectors {
// least 1 empty sector. Addresses in reserved_addresses are avoided.
Status FindSpace(SectorDescriptor** found_sector,
size_t size,
- std::span<const Address> reserved_addresses) {
+ span<const Address> reserved_addresses) {
return Find(kAppendEntry, found_sector, size, {}, reserved_addresses);
}
@@ -170,8 +170,8 @@ class Sectors {
Status FindSpaceDuringGarbageCollection(
SectorDescriptor** found_sector,
size_t size,
- std::span<const Address> addresses_to_skip,
- std::span<const Address> reserved_addresses) {
+ span<const Address> addresses_to_skip,
+ span<const Address> reserved_addresses) {
return Find(kGarbageCollect,
found_sector,
size,
@@ -182,7 +182,7 @@ class Sectors {
// Finds a sector that is ready to be garbage collected. Returns nullptr if no
// sectors can / need to be garbage collected.
SectorDescriptor* FindSectorToGarbageCollect(
- std::span<const Address> reserved_addresses) const;
+ span<const Address> reserved_addresses) const;
// The number of sectors in use.
size_t size() const { return descriptors_.size(); }
@@ -212,8 +212,8 @@ class Sectors {
Status Find(FindMode find_mode,
SectorDescriptor** found_sector,
size_t size,
- std::span<const Address> addresses_to_skip,
- std::span<const Address> reserved_addresses);
+ span<const Address> addresses_to_skip,
+ span<const Address> reserved_addresses);
SectorDescriptor& WearLeveledSectorFromIndex(size_t idx) const;
diff --git a/pw_kvs/public/pw_kvs/internal/span_traits.h b/pw_kvs/public/pw_kvs/internal/span_traits.h
index afb052810..b12753018 100644
--- a/pw_kvs/public/pw_kvs/internal/span_traits.h
+++ b/pw_kvs/public/pw_kvs/internal/span_traits.h
@@ -16,6 +16,8 @@
#include <iterator>
#include <type_traits>
+#include "pw_span/span.h"
+
namespace pw {
namespace kvs {
namespace internal {
@@ -33,7 +35,7 @@ template <typename Iter>
using iter_reference_t = decltype(*std::declval<Iter&>());
template <typename T>
-struct ExtentImpl : std::integral_constant<size_t, std::dynamic_extent> {};
+struct ExtentImpl : std::integral_constant<size_t, dynamic_extent> {};
template <typename T, size_t N>
struct ExtentImpl<T[N]> : std::integral_constant<size_t, N> {};
@@ -42,7 +44,7 @@ template <typename T, size_t N>
struct ExtentImpl<std::array<T, N>> : std::integral_constant<size_t, N> {};
template <typename T, size_t N>
-struct ExtentImpl<std::span<T, N>> : std::integral_constant<size_t, N> {};
+struct ExtentImpl<span<T, N>> : std::integral_constant<size_t, N> {};
template <typename T>
using Extent = ExtentImpl<std::remove_cv_t<std::remove_reference_t<T>>>;
@@ -51,7 +53,7 @@ using Extent = ExtentImpl<std::remove_cv_t<std::remove_reference_t<T>>>;
template <int&... ExplicitArgumentBarrier, typename It, typename EndOrSize>
constexpr auto make_span(It it, EndOrSize end_or_size) noexcept {
using T = std::remove_reference_t<iter_reference_t<It>>;
- return std::span<T>(it, end_or_size);
+ return span<T>(it, end_or_size);
}
// make_span utility function that deduces both the span's value_type and extent
@@ -63,8 +65,7 @@ template <int&... ExplicitArgumentBarrier,
typename T = std::remove_pointer_t<
decltype(std::data(std::declval<Container>()))>>
constexpr auto make_span(Container&& container) noexcept {
- return std::span<T, Extent<Container>::value>(
- std::forward<Container>(container));
+ return span<T, Extent<Container>::value>(std::forward<Container>(container));
}
// The make_span functions above don't seem to work correctly with arrays of
@@ -76,8 +77,8 @@ constexpr bool ConvertsToSpan(int) {
return true;
}
-// If the expression std::span(T) fails, then the type can't be converted to a
-// std::span.
+// If the expression span(T) fails, then the type can't be converted to a
+// span.
template <typename T>
constexpr bool ConvertsToSpan(...) {
return false;
@@ -85,7 +86,7 @@ constexpr bool ConvertsToSpan(...) {
} // namespace internal
-// Traits class to detect if the type converts to a std::span.
+// Traits class to detect if the type converts to a span.
template <typename T>
struct ConvertsToSpan
: public std::bool_constant<
diff --git a/pw_kvs/public/pw_kvs/io.h b/pw_kvs/public/pw_kvs/io.h
index 58fe24da6..9fb81f112 100644
--- a/pw_kvs/public/pw_kvs/io.h
+++ b/pw_kvs/public/pw_kvs/io.h
@@ -14,9 +14,9 @@
#pragma once
#include <cstddef>
-#include <span>
#include <type_traits>
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
namespace pw {
@@ -34,55 +34,52 @@ struct FunctionTraits<ReturnType (T::*)(Args...)> {
} // namespace internal
// Writes bytes to an unspecified output. Provides a Write function that takes a
-// std::span of bytes and returns a Status.
+// span of bytes and returns a Status.
class Output {
public:
- StatusWithSize Write(std::span<const std::byte> data) {
- return DoWrite(data);
- }
+ StatusWithSize Write(span<const std::byte> data) { return DoWrite(data); }
// Convenience wrapper for writing data from a pointer and length.
StatusWithSize Write(const void* data, size_t size_bytes) {
- return Write(std::span<const std::byte>(static_cast<const std::byte*>(data),
- size_bytes));
+ return Write(
+ span<const std::byte>(static_cast<const std::byte*>(data), size_bytes));
}
protected:
~Output() = default;
private:
- virtual StatusWithSize DoWrite(std::span<const std::byte> data) = 0;
+ virtual StatusWithSize DoWrite(span<const std::byte> data) = 0;
};
class Input {
public:
- StatusWithSize Read(std::span<std::byte> data) { return DoRead(data); }
+ StatusWithSize Read(span<std::byte> data) { return DoRead(data); }
// Convenience wrapper for reading data from a pointer and length.
StatusWithSize Read(void* data, size_t size_bytes) {
- return Read(
- std::span<std::byte>(static_cast<std::byte*>(data), size_bytes));
+ return Read(span<std::byte>(static_cast<std::byte*>(data), size_bytes));
}
protected:
~Input() = default;
private:
- virtual StatusWithSize DoRead(std::span<std::byte> data) = 0;
+ virtual StatusWithSize DoRead(span<std::byte> data) = 0;
};
// Output adapter that calls a free function.
class OutputToFunction final : public Output {
public:
- OutputToFunction(StatusWithSize (*function)(std::span<const std::byte>))
+ OutputToFunction(StatusWithSize (*function)(span<const std::byte>))
: function_(function) {}
private:
- StatusWithSize DoWrite(std::span<const std::byte> data) override {
+ StatusWithSize DoWrite(span<const std::byte> data) override {
return function_(data);
}
- StatusWithSize (*function_)(std::span<const std::byte>);
+ StatusWithSize (*function_)(span<const std::byte>);
};
} // namespace pw
diff --git a/pw_kvs/public/pw_kvs/key.h b/pw_kvs/public/pw_kvs/key.h
index 789036eea..bf9a4281f 100644
--- a/pw_kvs/public/pw_kvs/key.h
+++ b/pw_kvs/public/pw_kvs/key.h
@@ -21,8 +21,6 @@
#include <string_view>
#endif // __cplusplus >= 201703L
-#include "pw_string/internal/length.h"
-
namespace pw {
namespace kvs {
@@ -30,13 +28,12 @@ namespace kvs {
// platforms without C++17.
class Key {
public:
+ using value_type = const char;
+
// Constructors
constexpr Key() : str_{nullptr}, length_{0} {}
constexpr Key(const Key&) = default;
- constexpr Key(const char* str)
- : str_{str},
- length_{string::internal::ClampedLength(
- str, std::numeric_limits<size_t>::max())} {}
+ constexpr Key(const char* str) : str_{str}, length_{CStringLength(str)} {}
constexpr Key(const char* str, size_t len) : str_{str}, length_{len} {}
Key(const std::string& str) : str_{str.data()}, length_{str.length()} {}
@@ -78,6 +75,15 @@ class Key {
}
private:
+ // constexpr version of strlen
+ static constexpr size_t CStringLength(const char* str) {
+ size_t length = 0;
+ while (str[length] != '\0') {
+ length += 1;
+ }
+ return length;
+ }
+
const char* str_;
size_t length_;
};
diff --git a/pw_kvs/public/pw_kvs/key_value_store.h b/pw_kvs/public/pw_kvs/key_value_store.h
index 086dcb8eb..47e2cc85d 100644
--- a/pw_kvs/public/pw_kvs/key_value_store.h
+++ b/pw_kvs/public/pw_kvs/key_value_store.h
@@ -16,7 +16,6 @@
#include <array>
#include <cstddef>
#include <cstdint>
-#include <span>
#include <type_traits>
#include "pw_containers/vector.h"
@@ -29,6 +28,7 @@
#include "pw_kvs/internal/sectors.h"
#include "pw_kvs/internal/span_traits.h"
#include "pw_kvs/key.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -118,12 +118,12 @@ class KeyValueStore {
// INVALID_ARGUMENT: key is empty or too long or value is too large
//
StatusWithSize Get(Key key,
- std::span<std::byte> value,
+ span<std::byte> value,
size_t offset_bytes = 0) const;
// This overload of Get accepts a pointer to a trivially copyable object.
// If the value is an array, call Get with
- // std::as_writable_bytes(std::span(array)), or pass a pointer to the array
+ // as_writable_bytes(span(array)), or pass a pointer to the array
// instead of the array itself.
template <typename Pointer,
typename = std::enable_if_t<std::is_pointer<Pointer>::value>>
@@ -136,7 +136,7 @@ class KeyValueStore {
// Adds a key-value entry to the KVS. If the key was already present, its
// value is overwritten.
//
- // The value may be a std::span of bytes or a trivially copyable object.
+ // The value may be a span of bytes or a trivially copyable object.
//
// In the current implementation, all keys in the KVS must have a unique hash.
// If Put is called with a key whose hash matches an existing key, nothing
@@ -153,14 +153,14 @@ class KeyValueStore {
template <typename T,
typename std::enable_if_t<ConvertsToSpan<T>::value>* = nullptr>
Status Put(const Key& key, const T& value) {
- return PutBytes(key, std::as_bytes(internal::make_span(value)));
+ return PutBytes(key, as_bytes(internal::make_span(value)));
}
template <typename T,
typename std::enable_if_t<!ConvertsToSpan<T>::value>* = nullptr>
Status Put(const Key& key, const T& value) {
CheckThatObjectCanBePutOrGet<T>();
- return PutBytes(key, std::as_bytes(std::span<const T>(&value, 1)));
+ return PutBytes(key, as_bytes(span<const T>(&value, 1)));
}
// Removes a key-value entry from the KVS.
@@ -224,7 +224,7 @@ class KeyValueStore {
// Gets the value referred to by this iterator. Equivalent to
// KeyValueStore::Get.
- StatusWithSize Get(std::span<std::byte> value_buffer,
+ StatusWithSize Get(span<std::byte> value_buffer,
size_t offset_bytes = 0) const {
return kvs_.Get(key(), *iterator_, value_buffer, offset_bytes);
}
@@ -246,8 +246,7 @@ class KeyValueStore {
constexpr Item(const KeyValueStore& kvs,
const internal::EntryCache::const_iterator& item_iterator)
- : kvs_(kvs), iterator_(item_iterator), key_buffer_ {}
- {}
+ : kvs_(kvs), iterator_(item_iterator), key_buffer_{} {}
void ReadKey();
@@ -305,6 +304,11 @@ class KeyValueStore {
// Returns the number of valid entries in the KeyValueStore.
size_t size() const { return entry_cache_.present_entries(); }
+ // Returns the number of valid entries and deleted entries yet to be collected
+ size_t total_entries_with_deleted() const {
+ return entry_cache_.total_entries();
+ }
+
size_t max_size() const { return entry_cache_.max_entries(); }
size_t empty() const { return size() == 0u; }
@@ -355,7 +359,7 @@ class KeyValueStore {
// In the future, will be able to provide additional EntryFormats for
// backwards compatibility.
KeyValueStore(FlashPartition* partition,
- std::span<const EntryFormat> formats,
+ span<const EntryFormat> formats,
const Options& options,
size_t redundancy,
Vector<SectorDescriptor>& sector_descriptor_list,
@@ -372,9 +376,9 @@ class KeyValueStore {
static_assert(
std::is_trivially_copyable<T>::value && !std::is_pointer<T>::value,
"Only trivially copyable, non-pointer objects may be Put and Get by "
- "value. Any value may be stored by converting it to a byte std::span "
- "with std::as_bytes(std::span(&value, 1)) or "
- "std::as_writable_bytes(std::span(&value, 1)).");
+ "value. Any value may be stored by converting it to a byte span "
+ "with as_bytes(span(&value, 1)) or "
+ "as_writable_bytes(span(&value, 1)).");
}
Status InitializeMetadata();
@@ -383,7 +387,15 @@ class KeyValueStore {
Address start_address,
Address* next_entry_address);
- Status PutBytes(Key key, std::span<const std::byte> value);
+ // Remove deleted keys from the entry cache, including freeing sector bytes
+ // used by those keys. This must only be done directly after a full garbage
+ // collection, otherwise the current deleted entry could be garbage
+ // collected before the older stale entry producing a window for an
+ // invalid/corrupted KVS state if there was a power-fault, crash or other
+ // interruption.
+ Status RemoveDeletedKeyEntries();
+
+ Status PutBytes(Key key, span<const std::byte> value);
StatusWithSize ValueSize(const EntryMetadata& metadata) const;
@@ -413,7 +425,7 @@ class KeyValueStore {
StatusWithSize Get(Key key,
const EntryMetadata& metadata,
- std::span<std::byte> value_buffer,
+ span<std::byte> value_buffer,
size_t offset_bytes) const;
Status FixedSizeGet(Key key, void* value, size_t size_bytes) const;
@@ -429,12 +441,12 @@ class KeyValueStore {
Status WriteEntryForExistingKey(EntryMetadata& metadata,
EntryState new_state,
Key key,
- std::span<const std::byte> value);
+ span<const std::byte> value);
- Status WriteEntryForNewKey(Key key, std::span<const std::byte> value);
+ Status WriteEntryForNewKey(Key key, span<const std::byte> value);
Status WriteEntry(Key key,
- std::span<const std::byte> value,
+ span<const std::byte> value,
EntryState new_state,
EntryMetadata* prior_metadata = nullptr,
const internal::Entry* prior_entry = nullptr);
@@ -453,13 +465,11 @@ class KeyValueStore {
Status GetSectorForWrite(SectorDescriptor** sector,
size_t entry_size,
- std::span<const Address> reserved_addresses);
+ span<const Address> reserved_addresses);
Status MarkSectorCorruptIfNotOk(Status status, SectorDescriptor* sector);
- Status AppendEntry(const Entry& entry,
- Key key,
- std::span<const std::byte> value);
+ Status AppendEntry(const Entry& entry, Key key, span<const std::byte> value);
StatusWithSize CopyEntryToSector(Entry& entry,
SectorDescriptor* new_sector,
@@ -467,7 +477,7 @@ class KeyValueStore {
Status RelocateEntry(const EntryMetadata& metadata,
KeyValueStore::Address& address,
- std::span<const Address> reserved_addresses);
+ span<const Address> reserved_addresses);
// Perform all maintenance possible, including all neeeded repairing of
// corruption and garbage collection of reclaimable space in the KVS. When
@@ -486,15 +496,14 @@ class KeyValueStore {
// Find and garbage collect a singe sector that does not include a reserved
// address.
- Status GarbageCollect(std::span<const Address> reserved_addresses);
+ Status GarbageCollect(span<const Address> reserved_addresses);
- Status RelocateKeyAddressesInSector(
- SectorDescriptor& sector_to_gc,
- const EntryMetadata& metadata,
- std::span<const Address> reserved_addresses);
+ Status RelocateKeyAddressesInSector(SectorDescriptor& sector_to_gc,
+ const EntryMetadata& metadata,
+ span<const Address> reserved_addresses);
Status GarbageCollectSector(SectorDescriptor& sector_to_gc,
- std::span<const Address> reserved_addresses);
+ span<const Address> reserved_addresses);
// Ensure that all entries are on the primary (first) format. Entries that are
// not on the primary format are rewritten.
@@ -516,7 +525,7 @@ class KeyValueStore {
internal::Entry CreateEntry(Address address,
Key key,
- std::span<const std::byte> value,
+ span<const std::byte> value,
EntryState state);
void LogSectors() const;
@@ -581,7 +590,7 @@ class KeyValueStoreBuffer : public KeyValueStore {
const Options& options = {})
: KeyValueStoreBuffer(
partition,
- std::span<const EntryFormat, kEntryFormats>(
+ span<const EntryFormat, kEntryFormats>(
reinterpret_cast<const EntryFormat (&)[1]>(format)),
options) {
static_assert(kEntryFormats == 1,
@@ -591,7 +600,7 @@ class KeyValueStoreBuffer : public KeyValueStore {
// Constructs a KeyValueStore on the partition. Supports multiple entry
// formats. The first EntryFormat is used for new entries.
KeyValueStoreBuffer(FlashPartition* partition,
- std::span<const EntryFormat, kEntryFormats> formats,
+ span<const EntryFormat, kEntryFormats> formats,
const Options& options = {})
: KeyValueStore(partition,
formats_,
@@ -600,7 +609,10 @@ class KeyValueStoreBuffer : public KeyValueStore {
sectors_,
temp_sectors_to_skip_,
key_descriptors_,
- addresses_) {
+ addresses_),
+ sectors_(),
+ key_descriptors_(),
+ formats_() {
std::copy(formats.begin(), formats.end(), formats_.begin());
}
diff --git a/pw_kvs/pw_kvs_private/config.h b/pw_kvs/pw_kvs_private/config.h
index fb9dc7999..6da354bf9 100644
--- a/pw_kvs/pw_kvs_private/config.h
+++ b/pw_kvs/pw_kvs_private/config.h
@@ -30,6 +30,14 @@
static_assert((PW_KVS_MAX_FLASH_ALIGNMENT >= 16UL),
"Max flash alignment is required to be at least 16");
+// Whether to remove deleted keys in heavy maintanence. This feature costs some
+// code size (>1KB) and is only necessary if arbitrary key names are used.
+// Without this feature, deleted key entries can fill the KVS, making it
+// impossible to add more keys, even though most keys are deleted.
+#ifndef PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
+#define PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE 1
+#endif // PW_KVS_REMOVE_DELETED_KEYS_IN_HEAVY_MAINTENANCE
+
namespace pw::kvs {
inline constexpr size_t kMaxFlashAlignment = PW_KVS_MAX_FLASH_ALIGNMENT;
diff --git a/pw_kvs/sectors.cc b/pw_kvs/sectors.cc
index 97d942357..25edcf676 100644
--- a/pw_kvs/sectors.cc
+++ b/pw_kvs/sectors.cc
@@ -24,7 +24,8 @@ namespace pw::kvs::internal {
namespace {
// Returns true if the container conatins the value.
-// TODO: At some point move this to pw_containers, along with adding tests.
+// TODO(hepler): At some point move this to pw_containers, along with adding
+// tests.
template <typename Container, typename T>
bool Contains(const Container& container, const T& value) {
return std::find(std::begin(container), std::end(container), value) !=
@@ -36,8 +37,8 @@ bool Contains(const Container& container, const T& value) {
Status Sectors::Find(FindMode find_mode,
SectorDescriptor** found_sector,
size_t size,
- std::span<const Address> addresses_to_skip,
- std::span<const Address> reserved_addresses) {
+ span<const Address> addresses_to_skip,
+ span<const Address> reserved_addresses) {
SectorDescriptor* first_empty_sector = nullptr;
bool at_least_two_empty_sectors = (find_mode == kGarbageCollect);
@@ -102,7 +103,7 @@ Status Sectors::Find(FindMode find_mode,
}
// Skip sectors in the skip list.
- if (Contains(std::span(temp_sectors_to_skip_, sectors_to_skip), sector)) {
+ if (Contains(span(temp_sectors_to_skip_, sectors_to_skip), sector)) {
continue;
}
@@ -162,9 +163,9 @@ SectorDescriptor& Sectors::WearLeveledSectorFromIndex(size_t idx) const {
return descriptors_[(Index(last_new_) + 1 + idx) % descriptors_.size()];
}
-// TODO: Consider breaking this function into smaller sub-chunks.
+// TODO(hepler): Consider breaking this function into smaller sub-chunks.
SectorDescriptor* Sectors::FindSectorToGarbageCollect(
- std::span<const Address> reserved_addresses) const {
+ span<const Address> reserved_addresses) const {
const size_t sector_size_bytes = partition_.sector_size_bytes();
SectorDescriptor* sector_candidate = nullptr;
size_t candidate_bytes = 0;
@@ -174,8 +175,7 @@ SectorDescriptor* Sectors::FindSectorToGarbageCollect(
temp_sectors_to_skip_[i] = &FromAddress(reserved_addresses[i]);
DBG(" Skip sector %u", Index(reserved_addresses[i]));
}
- const std::span sectors_to_skip(temp_sectors_to_skip_,
- reserved_addresses.size());
+ const span sectors_to_skip(temp_sectors_to_skip_, reserved_addresses.size());
// Step 1: Try to find a sectors with stale keys and no valid keys (no
// relocation needed). Use the first such sector found, as that will help the
diff --git a/pw_kvs/sectors_test.cc b/pw_kvs/sectors_test.cc
index c1d295fb5..9ce941053 100644
--- a/pw_kvs/sectors_test.cc
+++ b/pw_kvs/sectors_test.cc
@@ -79,7 +79,7 @@ TEST_F(SectorsTest, NextWritableAddress_PartiallyWrittenSector) {
EXPECT_EQ(123u, sectors_.NextWritableAddress(*sectors_.begin()));
}
-// TODO: Add tests for FindSpace, FindSpaceDuringGarbageCollection, and
+// TODO(hepler): Add tests for FindSpace, FindSpaceDuringGarbageCollection, and
// FindSectorToGarbageCollect.
} // namespace
diff --git a/pw_kvs/size_report/BUILD.gn b/pw_kvs/size_report/BUILD.gn
index e1c6774b6..00353c923 100644
--- a/pw_kvs/size_report/BUILD.gn
+++ b/pw_kvs/size_report/BUILD.gn
@@ -30,17 +30,17 @@ pw_executable("base") {
pw_executable("base_with_only_flash") {
sources = [ "base_with_only_flash.cc" ]
deps = _deps + [
- dir_pw_kvs,
- "$dir_pw_kvs:flash_test_partition",
"$dir_pw_kvs:fake_flash_12_byte_partition",
+ "$dir_pw_kvs:flash_test_partition",
+ dir_pw_kvs,
]
}
pw_executable("with_kvs") {
sources = [ "with_kvs.cc" ]
deps = _deps + [
- dir_pw_kvs,
- "$dir_pw_kvs:flash_test_partition",
"$dir_pw_kvs:fake_flash_12_byte_partition",
+ "$dir_pw_kvs:flash_test_partition",
+ dir_pw_kvs,
]
}
diff --git a/pw_kvs/size_report/base_with_only_flash.cc b/pw_kvs/size_report/base_with_only_flash.cc
index ab1fcbd30..f1fafa931 100644
--- a/pw_kvs/size_report/base_with_only_flash.cc
+++ b/pw_kvs/size_report/base_with_only_flash.cc
@@ -40,20 +40,20 @@ int main() {
is_set = (result != nullptr);
test_partition.Erase()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
std::memset((void*)working_buffer, 0x55, sizeof(working_buffer));
- test_partition.Write(0, std::as_bytes(std::span(working_buffer)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ test_partition.Write(0, pw::as_bytes(pw::span(working_buffer)))
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
bool tmp_bool;
test_partition.IsErased(&tmp_bool)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
is_erased = tmp_bool;
- test_partition.Read(0, as_writable_bytes(std::span(working_buffer)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ test_partition.Read(0, as_writable_bytes(pw::span(working_buffer)))
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return 0;
}
diff --git a/pw_kvs/size_report/with_kvs.cc b/pw_kvs/size_report/with_kvs.cc
index fccbab54b..eed3e4b8a 100644
--- a/pw_kvs/size_report/with_kvs.cc
+++ b/pw_kvs/size_report/with_kvs.cc
@@ -49,17 +49,17 @@ int main() {
std::memset((void*)working_buffer, 0x55, sizeof(working_buffer));
is_set = (result != nullptr);
- kvs.Init().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ kvs.Init().IgnoreError(); // TODO(b/242598609): Handle Status properly
unsigned kvs_value = 42;
kvs.Put("example_key", kvs_value)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
kvs_entry_count = kvs.size();
unsigned read_value = 0;
kvs.Get("example_key", &read_value)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return 0;
}
diff --git a/pw_libc/BUILD.bazel b/pw_libc/BUILD.bazel
index 0139ae7e7..172bcda1c 100644
--- a/pw_libc/BUILD.bazel
+++ b/pw_libc/BUILD.bazel
@@ -27,6 +27,7 @@ pw_cc_test(
"memset_test.cc",
],
deps = [
+ "//pw_containers",
"//pw_unit_test",
],
)
diff --git a/pw_libc/BUILD.gn b/pw_libc/BUILD.gn
index 9a55cef63..6b5c01f0e 100644
--- a/pw_libc/BUILD.gn
+++ b/pw_libc/BUILD.gn
@@ -28,6 +28,7 @@ pw_test_group("tests") {
pw_test("memset_test") {
sources = [ "memset_test.cc" ]
+ deps = [ "$dir_pw_containers" ]
}
pw_doc_group("docs") {
diff --git a/pw_libc/memset_test.cc b/pw_libc/memset_test.cc
index 5ab2a3f37..a4b46b5fd 100644
--- a/pw_libc/memset_test.cc
+++ b/pw_libc/memset_test.cc
@@ -25,6 +25,7 @@
#include <numeric>
#include "gtest/gtest.h"
+#include "pw_containers/algorithm.h"
namespace pw {
namespace {
@@ -51,8 +52,7 @@ TEST(Memset, EmptyCase) {
// Destination buffer untouched.
constexpr std::array<char, 5> kExpected{'h', 'e', 'l', 'l', 'o'};
- EXPECT_TRUE(
- std::equal(arr.begin(), arr.end(), kExpected.begin(), kExpected.end()));
+ EXPECT_TRUE(pw::containers::Equal(arr, kExpected));
}
TEST(Memset, OneCharacter) {
@@ -64,8 +64,7 @@ TEST(Memset, OneCharacter) {
// Ensure the destination buffer is untouched.
constexpr std::array<char, 5> kExpected{0, 'e', 'l', 'l', 'o'};
- EXPECT_TRUE(
- std::equal(arr.begin(), arr.end(), kExpected.begin(), kExpected.end()));
+ EXPECT_TRUE(pw::containers::Equal(arr, kExpected));
}
// Now do a detailed case with more values. Span both word sizes and alignments
diff --git a/pw_log/Android.bp b/pw_log/Android.bp
index fd4c7b14c..cd49e5f59 100644
--- a/pw_log/Android.bp
+++ b/pw_log/Android.bp
@@ -13,21 +13,15 @@
// the License.
package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "external_pigweed_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["external_pigweed_license"],
}
-cc_library {
- name: "libpw_log",
- vendor_available: true,
+cc_library_headers {
+ name: "pw_log_headers",
cpp_std: "c++2a",
- export_include_dirs: [
- "public",
- ],
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ host_supported: true,
}
android_library {
diff --git a/pw_log/BUILD.bazel b/pw_log/BUILD.bazel
index 3afe0afd3..b1bf3acbf 100644
--- a/pw_log/BUILD.bazel
+++ b/pw_log/BUILD.bazel
@@ -19,6 +19,7 @@ load(
"pw_cc_test",
)
load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -88,13 +89,25 @@ proto_library(
"log.proto",
],
import_prefix = "pw_log/proto",
- strip_import_prefix = "//pw_log",
+ strip_import_prefix = "/pw_log",
deps = [
- "//pw_protobuf:common_protos",
+ "//pw_protobuf:common_proto",
"//pw_tokenizer:tokenizer_proto",
],
)
+# TODO(b/241456982): Not expected to build yet.
+py_proto_library(
+ name = "log_proto_py_pb2",
+ tags = ["manual"],
+ deps = [":log_proto"],
+)
+
+java_lite_proto_library(
+ name = "log_java_proto_lite",
+ deps = [":log_proto"],
+)
+
pw_proto_library(
name = "log_proto_cc",
deps = [":log_proto"],
@@ -140,6 +153,7 @@ pw_cc_test(
":facade",
":log_proto_cc.pwpb",
":proto_utils",
+ "//pw_containers",
"//pw_preprocessor",
"//pw_protobuf",
"//pw_protobuf:bytes_utils",
diff --git a/pw_log/BUILD.gn b/pw_log/BUILD.gn
index 1fbeee110..3ff928ed2 100644
--- a/pw_log/BUILD.gn
+++ b/pw_log/BUILD.gn
@@ -85,6 +85,7 @@ pw_source_set("proto_utils") {
"$dir_pw_log:protos.pwpb",
"$dir_pw_log_tokenized:metadata",
"$dir_pw_result",
+ dir_pw_span,
]
deps = [ "$dir_pw_protobuf" ]
sources = [ "proto_utils.cc" ]
@@ -145,6 +146,7 @@ pw_test("proto_utils_test") {
deps = [
":proto_utils",
":pw_log.facade",
+ "$dir_pw_containers",
"$dir_pw_log:protos.pwpb",
"$dir_pw_preprocessor",
"$dir_pw_protobuf",
diff --git a/pw_log/CMakeLists.txt b/pw_log/CMakeLists.txt
index 9ee4eeaf8..887bbf8dd 100644
--- a/pw_log/CMakeLists.txt
+++ b/pw_log/CMakeLists.txt
@@ -13,13 +13,14 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_log/backend.cmake)
include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
pw_add_module_config(pw_log_CONFIG)
pw_add_module_config(pw_log_GLOG_ADAPTER_CONFIG)
-pw_add_module_library(pw_log.config
+pw_add_library(pw_log.config INTERFACE
HEADERS
public/pw_log/config.h
PUBLIC_INCLUDES
@@ -28,7 +29,9 @@ pw_add_module_library(pw_log.config
${pw_log_CONFIG}
)
-pw_add_facade(pw_log
+pw_add_facade(pw_log INTERFACE
+ BACKEND
+ pw_log_BACKEND
HEADERS
public/pw_log/levels.h
public/pw_log/log.h
@@ -41,7 +44,7 @@ pw_add_facade(pw_log
pw_log.config
)
-pw_add_module_library(pw_log.glog_adapter
+pw_add_library(pw_log.glog_adapter INTERFACE
HEADERS
public/pw_log/glog_adapter.h
public/pw_log/glog_adapter_config.h
@@ -57,7 +60,7 @@ pw_add_module_library(pw_log.glog_adapter
${pw_log_GLOG_ADAPTER_CONFIG}
)
-pw_add_module_library(pw_log.proto_utils
+pw_add_library(pw_log.proto_utils STATIC
HEADERS
public/pw_log/proto_utils.h
PUBLIC_INCLUDES
@@ -80,16 +83,16 @@ pw_proto_library(pw_log.protos
PREFIX
pw_log/proto
DEPS
- pw_protobuf.common_protos
+ pw_protobuf.common_proto
pw_tokenizer.proto
)
-if(NOT "${pw_log_BACKEND}" STREQUAL "pw_log.NO_BACKEND_SET")
+if(NOT "${pw_log_BACKEND}" STREQUAL "")
pw_add_test(pw_log.basic_log_test
SOURCES
basic_log_test.cc
basic_log_test_plain_c.c
- DEPS
+ PRIVATE_DEPS
pw_log
pw_preprocessor
GROUPS
@@ -100,7 +103,7 @@ if(NOT "${pw_log_BACKEND}" STREQUAL "pw_log.NO_BACKEND_SET")
pw_add_test(pw_log.glog_adapter_test
SOURCES
glog_adapter_test.cc
- DEPS
+ PRIVATE_DEPS
pw_log.glog_adapter
GROUPS
modules
@@ -110,13 +113,14 @@ if(NOT "${pw_log_BACKEND}" STREQUAL "pw_log.NO_BACKEND_SET")
pw_add_test(pw_log.proto_utils_test
SOURCES
proto_utils_test.cc
- DEPS
+ PRIVATE_DEPS
pw_log
pw_log.proto_utils
pw_log.protos.pwpb
pw_preprocessor
pw_protobuf
pw_protobuf.bytes_utils
+ pw_containers
GROUPS
modules
pw_log
diff --git a/pw_log/backend.cmake b/pw_log/backend.cmake
new file mode 100644
index 000000000..5bde0fc56
--- /dev/null
+++ b/pw_log/backend.cmake
@@ -0,0 +1,19 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_log module.
+pw_add_backend_variable(pw_log_BACKEND)
diff --git a/pw_log/basic_log_test.cc b/pw_log/basic_log_test.cc
index a7042a8c1..960f9beef 100644
--- a/pw_log/basic_log_test.cc
+++ b/pw_log/basic_log_test.cc
@@ -16,7 +16,7 @@
// compile the constructs promised by the logging facade; and that when run,
// there is no crash.
//
-// TODO(pwbug/88): Add verification of the actually logged statements.
+// TODO(b/235289499): Add verification of the actually logged statements.
// clang-format off
#define PW_LOG_MODULE_NAME "TST"
@@ -28,8 +28,8 @@
#include "gtest/gtest.h"
-// TODO(pwbug/86): Test unsigned integer logging (32 and 64 bit); test pointer
-// logging.
+// TODO(b/235291136): Test unsigned integer logging (32 and 64 bit); test
+// pointer logging.
void LoggingFromFunction() { PW_LOG_INFO("From a function!"); }
@@ -72,21 +72,46 @@ TEST(BasicLog, CriticalLevel) {
}
TEST(BasicLog, ManualLevel) {
- PW_LOG(PW_LOG_LEVEL_DEBUG, 0, "A manual DEBUG-level message");
- PW_LOG(PW_LOG_LEVEL_DEBUG, 1, "A manual DEBUG-level message; with a flag");
+ PW_LOG(PW_LOG_LEVEL_DEBUG,
+ PW_LOG_MODULE_NAME,
+ 0,
+ "A manual DEBUG-level message");
+ PW_LOG(PW_LOG_LEVEL_DEBUG,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual DEBUG-level message; with a flag");
- PW_LOG(PW_LOG_LEVEL_INFO, 0, "A manual INFO-level message");
- PW_LOG(PW_LOG_LEVEL_INFO, 1, "A manual INFO-level message; with a flag");
-
- PW_LOG(PW_LOG_LEVEL_WARN, 0, "A manual WARN-level message");
- PW_LOG(PW_LOG_LEVEL_WARN, 1, "A manual WARN-level message; with a flag");
-
- PW_LOG(PW_LOG_LEVEL_ERROR, 0, "A manual ERROR-level message");
- PW_LOG(PW_LOG_LEVEL_ERROR, 1, "A manual ERROR-level message; with a flag");
+ PW_LOG(
+ PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, 0, "A manual INFO-level message");
+ PW_LOG(PW_LOG_LEVEL_INFO,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual INFO-level message; with a flag");
- PW_LOG(PW_LOG_LEVEL_CRITICAL, 0, "A manual CRITICAL-level message");
PW_LOG(
- PW_LOG_LEVEL_CRITICAL, 1, "A manual CRITICAL-level message; with a flag");
+ PW_LOG_LEVEL_WARN, PW_LOG_MODULE_NAME, 0, "A manual WARN-level message");
+ PW_LOG(PW_LOG_LEVEL_WARN,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual WARN-level message; with a flag");
+
+ PW_LOG(PW_LOG_LEVEL_ERROR,
+ PW_LOG_MODULE_NAME,
+ 0,
+ "A manual ERROR-level message");
+ PW_LOG(PW_LOG_LEVEL_ERROR,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual ERROR-level message; with a flag");
+
+ PW_LOG(PW_LOG_LEVEL_CRITICAL,
+ PW_LOG_MODULE_NAME,
+ 0,
+ "A manual CRITICAL-level message");
+ PW_LOG(PW_LOG_LEVEL_CRITICAL,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual CRITICAL-level message; with a flag");
}
TEST(BasicLog, FromAFunction) { LoggingFromFunction(); }
@@ -94,11 +119,11 @@ TEST(BasicLog, FromAFunction) { LoggingFromFunction(); }
TEST(BasicLog, CustomLogLevels) {
// Log levels other than the standard ones work; what each backend does is
// implementation defined.
- PW_LOG(0, 0, "Custom log level: 0");
- PW_LOG(1, 0, "Custom log level: 1");
- PW_LOG(2, 0, "Custom log level: 2");
- PW_LOG(3, 0, "Custom log level: 3");
- PW_LOG(100, 0, "Custom log level: 100");
+ PW_LOG(0, "", 0, "Custom log level: 0");
+ PW_LOG(1, "", 0, "Custom log level: 1");
+ PW_LOG(2, "", 0, "Custom log level: 2");
+ PW_LOG(3, "", 0, "Custom log level: 3");
+ PW_LOG(100, "", 0, "Custom log level: 100");
}
#define TEST_FAILED_LOG "IF THIS MESSAGE WAS LOGGED, THE TEST FAILED"
@@ -122,12 +147,18 @@ TEST(BasicLog, FilteringByFlags) {
#define PW_LOG_SKIP_LOGS_WITH_FLAGS 1
// Flag is set so these should all get zapped.
- PW_LOG(PW_LOG_LEVEL_INFO, 1, TEST_FAILED_LOG);
- PW_LOG(PW_LOG_LEVEL_ERROR, 1, TEST_FAILED_LOG);
+ PW_LOG(PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, 1, TEST_FAILED_LOG);
+ PW_LOG(PW_LOG_LEVEL_ERROR, PW_LOG_MODULE_NAME, 1, TEST_FAILED_LOG);
// However, a different flag bit should still log.
- PW_LOG(PW_LOG_LEVEL_INFO, 1 << 1, "This flagged log is intended to appear");
- PW_LOG(PW_LOG_LEVEL_ERROR, 1 << 1, "This flagged log is intended to appear");
+ PW_LOG(PW_LOG_LEVEL_INFO,
+ PW_LOG_MODULE_NAME,
+ 1 << 1,
+ "This flagged log is intended to appear");
+ PW_LOG(PW_LOG_LEVEL_ERROR,
+ PW_LOG_MODULE_NAME,
+ 1 << 1,
+ "This flagged log is intended to appear");
#undef PW_LOG_SKIP_LOGS_WITH_FLAGS
#define PW_LOG_SKIP_LOGS_WITH_FLAGS 0
@@ -141,7 +172,7 @@ TEST(BasicLog, ChangingTheModuleName) {
}
TEST(BasicLog, ShortNames) {
- LOG(PW_LOG_LEVEL_INFO, 0, "Shrt lg");
+ LOG(PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, 0, "Shrt lg");
LOG_DEBUG("A debug log: %d", 1);
LOG_INFO("An info log: %d", 2);
LOG_WARN("A warning log: %d", 3);
@@ -150,7 +181,7 @@ TEST(BasicLog, ShortNames) {
}
TEST(BasicLog, UltraShortNames) {
- LOG(PW_LOG_LEVEL_INFO, 0, "Shrt lg");
+ LOG(PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, 0, "Shrt lg");
DBG("A debug log: %d", 1);
INF("An info log: %d", 2);
WRN("A warning log: %d", 3);
@@ -167,15 +198,17 @@ TEST(BasicLog, FromPlainC) { BasicLogTestPlainC(); }
// functions tests fail to compile, because the arguments end up out-of-order.
#undef PW_LOG
-#define PW_LOG(level, flags, message, ...) \
- DoNothingFakeFunction("%d/%d/%d: incoming transmission [" message "]", \
+#define PW_LOG(level, module, flags, message, ...) \
+ DoNothingFakeFunction(module, \
+ "%d/%d/%d: incoming transmission [" message "]", \
level, \
__LINE__, \
flags PW_COMMA_ARGS(__VA_ARGS__))
-void DoNothingFakeFunction(const char*, ...) PW_PRINTF_FORMAT(1, 2);
+void DoNothingFakeFunction(const char*, const char*, ...)
+ PW_PRINTF_FORMAT(2, 3);
-void DoNothingFakeFunction(const char*, ...) {}
+void DoNothingFakeFunction(const char*, const char*, ...) {}
TEST(CustomFormatString, DebugLevel) {
PW_LOG_DEBUG("This log statement should be at DEBUG level; no args");
diff --git a/pw_log/basic_log_test_plain_c.c b/pw_log/basic_log_test_plain_c.c
index 1b7a41d11..5f7d72d34 100644
--- a/pw_log/basic_log_test_plain_c.c
+++ b/pw_log/basic_log_test_plain_c.c
@@ -67,29 +67,54 @@ void BasicLogTestPlainC(void) {
PW_LOG_CRITICAL("This log is the last one; device should reboot");
// Core log macro, with manually specified level and flags.
- PW_LOG(PW_LOG_LEVEL_DEBUG, 0, "A manual DEBUG-level message");
- PW_LOG(PW_LOG_LEVEL_DEBUG, 1, "A manual DEBUG-level message; with a flag");
+ PW_LOG(PW_LOG_LEVEL_DEBUG,
+ PW_LOG_MODULE_NAME,
+ 0,
+ "A manual DEBUG-level message");
+ PW_LOG(PW_LOG_LEVEL_DEBUG,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual DEBUG-level message; with a flag");
- PW_LOG(PW_LOG_LEVEL_INFO, 0, "A manual INFO-level message");
- PW_LOG(PW_LOG_LEVEL_INFO, 1, "A manual INFO-level message; with a flag");
-
- PW_LOG(PW_LOG_LEVEL_WARN, 0, "A manual WARN-level message");
- PW_LOG(PW_LOG_LEVEL_WARN, 1, "A manual WARN-level message; with a flag");
-
- PW_LOG(PW_LOG_LEVEL_ERROR, 0, "A manual ERROR-level message");
- PW_LOG(PW_LOG_LEVEL_ERROR, 1, "A manual ERROR-level message; with a flag");
+ PW_LOG(
+ PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, 0, "A manual INFO-level message");
+ PW_LOG(PW_LOG_LEVEL_INFO,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual INFO-level message; with a flag");
- PW_LOG(PW_LOG_LEVEL_CRITICAL, 0, "A manual CRITICAL-level message");
PW_LOG(
- PW_LOG_LEVEL_CRITICAL, 1, "A manual CRITICAL-level message; with a flag");
+ PW_LOG_LEVEL_WARN, PW_LOG_MODULE_NAME, 0, "A manual WARN-level message");
+ PW_LOG(PW_LOG_LEVEL_WARN,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual WARN-level message; with a flag");
+
+ PW_LOG(PW_LOG_LEVEL_ERROR,
+ PW_LOG_MODULE_NAME,
+ 0,
+ "A manual ERROR-level message");
+ PW_LOG(PW_LOG_LEVEL_ERROR,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual ERROR-level message; with a flag");
+
+ PW_LOG(PW_LOG_LEVEL_CRITICAL,
+ PW_LOG_MODULE_NAME,
+ 0,
+ "A manual CRITICAL-level message");
+ PW_LOG(PW_LOG_LEVEL_CRITICAL,
+ PW_LOG_MODULE_NAME,
+ 1,
+ "A manual CRITICAL-level message; with a flag");
// Log levels other than the standard ones work; what each backend does is
// implementation defined.
- PW_LOG(0, PW_LOG_FLAGS, "Custom log level: 0");
- PW_LOG(1, PW_LOG_FLAGS, "Custom log level: 1");
- PW_LOG(2, PW_LOG_FLAGS, "Custom log level: 2");
- PW_LOG(3, PW_LOG_FLAGS, "Custom log level: 3");
- PW_LOG(100, PW_LOG_FLAGS, "Custom log level: 100");
+ PW_LOG(0, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, "Custom log level: 0");
+ PW_LOG(1, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, "Custom log level: 1");
+ PW_LOG(2, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, "Custom log level: 2");
+ PW_LOG(3, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, "Custom log level: 3");
+ PW_LOG(100, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, "Custom log level: 100");
// Logging from a function.
LoggingFromFunctionPlainC();
@@ -104,15 +129,20 @@ void BasicLogTestPlainC(void) {
}
#undef PW_LOG
-#define PW_LOG(level, flags, message, ...) \
- DoNothingFakeFunction("%d/%d/%d: incoming transmission [" message "]", \
+#define PW_LOG(level, module, flags, message, ...) \
+ DoNothingFakeFunction(module, \
+ "%d/%d/%d: incoming transmission [" message "]", \
level, \
__LINE__, \
flags PW_COMMA_ARGS(__VA_ARGS__))
-static void DoNothingFakeFunction(const char* f, ...) PW_PRINTF_FORMAT(1, 2);
+static void DoNothingFakeFunction(const char* module, const char* f, ...)
+ PW_PRINTF_FORMAT(2, 3);
-static void DoNothingFakeFunction(const char* f, ...) { (void)f; }
+static void DoNothingFakeFunction(const char* module, const char* f, ...) {
+ (void)module;
+ (void)f;
+}
static void CustomFormatStringTest(void) {
PW_LOG_DEBUG("Abc");
diff --git a/pw_log/docs.rst b/pw_log/docs.rst
index b3eac4376..3453c08e1 100644
--- a/pw_log/docs.rst
+++ b/pw_log/docs.rst
@@ -76,12 +76,15 @@ Logging macros
These are the primary macros for logging information about the functioning of a
system, intended to be used directly.
-.. cpp:function:: PW_LOG(level, flags, fmt, ...)
+.. c:macro:: PW_LOG(level, module, flags, fmt, ...)
This is the primary mechanism for logging.
*level* - An integer level as defined by ``pw_log/levels.h``.
+ *module* - A string literal for the module name. Defaults to
+ :c:macro:`PW_LOG_MODULE_NAME`.
+
*flags* - Arbitrary flags the backend can leverage. The semantics of these
flags are not defined in the facade, but are instead meant as a general
mechanism for communication bits of information to the logging backend.
@@ -104,8 +107,8 @@ system, intended to be used directly.
.. code-block:: cpp
- PW_LOG(PW_LOG_FLAGS, PW_LOG_LEVEL_INFO, "Temp is %d degrees", temp);
- PW_LOG(UNRELIABLE_DELIVERY, PW_LOG_LEVEL_ERROR, "It didn't work!");
+ PW_LOG(PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, "Temp is %d degrees", temp);
+ PW_LOG(PW_LOG_LEVEL_ERROR, PW_LOG_MODULE_NAME, UNRELIABLE_DELIVERY, "It didn't work!");
.. note::
@@ -116,13 +119,13 @@ system, intended to be used directly.
in the backend.
-.. cpp:function:: PW_LOG_DEBUG(fmt, ...)
-.. cpp:function:: PW_LOG_INFO(fmt, ...)
-.. cpp:function:: PW_LOG_WARN(fmt, ...)
-.. cpp:function:: PW_LOG_ERROR(fmt, ...)
-.. cpp:function:: PW_LOG_CRITICAL(fmt, ...)
+.. c:macro:: PW_LOG_DEBUG(fmt, ...)
+.. c:macro:: PW_LOG_INFO(fmt, ...)
+.. c:macro:: PW_LOG_WARN(fmt, ...)
+.. c:macro:: PW_LOG_ERROR(fmt, ...)
+.. c:macro:: PW_LOG_CRITICAL(fmt, ...)
- Shorthand for `PW_LOG(PW_LOG_FLAGS, <level>, fmt, ...)`.
+ Shorthand for ``PW_LOG(<level>, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, fmt, ...)``.
--------------------
Module configuration
@@ -158,7 +161,7 @@ This module defines macros that can be overridden to independently control the
behavior of ``pw_log`` statements for each C or C++ source file. To override
these macros, add ``#define`` statements for them before including headers.
-The option macro definitions must be visibile to ``pw_log/log.h`` the first time
+The option macro definitions must be visible to ``pw_log/log.h`` the first time
it is included. To handle potential transitive includes, place these
``#defines`` before all ``#include`` statements. This should only be done in
source files, not headers. For example:
diff --git a/pw_log/glog_adapter_test.cc b/pw_log/glog_adapter_test.cc
index f8265031f..d6691cea0 100644
--- a/pw_log/glog_adapter_test.cc
+++ b/pw_log/glog_adapter_test.cc
@@ -11,7 +11,7 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-// TODO(pwbug/88): Add verification of the actually logged statements.
+// TODO(b/235289499): Add verification of the actually logged statements.
// clang-format off
#define PW_LOG_MODULE_NAME "TST"
diff --git a/pw_log/log.proto b/pw_log/log.proto
index 611724a26..a1f4e7011 100644
--- a/pw_log/log.proto
+++ b/pw_log/log.proto
@@ -93,12 +93,12 @@ message LogEntry {
// Note: This packing saves two bytes per log message in most cases compared
// to having line and level separately; and is zero-cost if the log backend
// omits the line number.
- optional uint32 line_level = 2;
+ uint32 line_level = 2;
// Some log messages have flags to indicate attributes such as whether they
// are from an assert or if they contain PII. The particular flags are
// product- and implementation-dependent.
- optional uint32 flags = 3;
+ uint32 flags = 3;
// Timestamps are either specified with an absolute timestamp or relative to
// the previous log entry.
@@ -130,17 +130,17 @@ message LogEntry {
// When the log buffers are full but more logs come in, the logs are counted
// and a special log message is omitted with only counts for the number of
// messages dropped.
- optional uint32 dropped = 6;
+ uint32 dropped = 6;
// The PW_LOG_MODULE_NAME for this log message.
- optional bytes module = 7 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
+ bytes module = 7 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
// The file path where this log was created, if not encoded in the message.
- optional bytes file = 8 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
+ bytes file = 8 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
// The task or thread name that created the log message. If the log was not
// created on a thread, it should use a name appropriate to that context.
- optional bytes thread = 9 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
+ bytes thread = 9 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
// The following fields are planned but will not be added until they are
// needed. Protobuf field numbers over 15 use an extra byte, so these fields
@@ -148,22 +148,22 @@ message LogEntry {
// Represents the device from which the log originated. The meaning of this
// field is implementation defined
- // optional uint32 source_id = ?;
+ // uint32 source_id = ?;
// Some messages are associated with trace events, which may carry additional
// contextual data. This is a tuple of a data format string which could be
// used by the decoder to identify the data (e.g. printf-style tokens) and the
// data itself in bytes.
- // optional bytes data_format = ?
+ // bytes data_format = ?
// [(tokenizer.format) = TOKENIZATION_OPTIONAL];
- // optional bytes data = ?;
+ // bytes data = ?;
}
message LogRequest {}
message LogEntries {
repeated LogEntry entries = 1;
- optional uint32 first_entry_sequence_id = 2;
+ uint32 first_entry_sequence_id = 2;
}
// RPC service for accessing logs.
@@ -199,6 +199,9 @@ message FilterRule {
DROP = 2; // Drop the log entry if all conditions are met
};
Action action = 4;
+
+ // Condition 4: (thread_equals.size() == 0 || log.thread == thread_equals).
+ bytes thread_equals = 5 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
}
// A filter is a series of rules. First matching rule wins.
diff --git a/pw_log/proto_utils.cc b/pw_log/proto_utils.cc
index 23739aaf9..55c77e22d 100644
--- a/pw_log/proto_utils.cc
+++ b/pw_log/proto_utils.cc
@@ -14,13 +14,13 @@
#include "pw_log/proto_utils.h"
-#include <span>
#include <string_view>
#include "pw_bytes/endian.h"
#include "pw_log/levels.h"
#include "pw_log_tokenized/metadata.h"
#include "pw_protobuf/wire_format.h"
+#include "pw_span/span.h"
namespace pw::log {
@@ -34,14 +34,14 @@ Result<ConstByteSpan> EncodeLog(int level,
std::string_view message,
ByteSpan encode_buffer) {
// Encode message to the LogEntry protobuf.
- LogEntry::MemoryEncoder encoder(encode_buffer);
+ pwpb::LogEntry::MemoryEncoder encoder(encode_buffer);
if (message.empty()) {
return Status::InvalidArgument();
}
// Defer status checks until the end.
- Status status = encoder.WriteMessage(std::as_bytes(std::span(message)));
+ Status status = encoder.WriteMessage(as_bytes(span<const char>(message)));
status = encoder.WriteLineLevel(PackLineLevel(line_number, level));
if (flags != 0) {
status = encoder.WriteFlags(flags);
@@ -50,25 +50,25 @@ Result<ConstByteSpan> EncodeLog(int level,
// Module name and file name may or may not be present.
if (!module_name.empty()) {
- status = encoder.WriteModule(std::as_bytes(std::span(module_name)));
+ status = encoder.WriteModule(as_bytes(span<const char>(module_name)));
}
if (!file_name.empty()) {
- status = encoder.WriteFile(std::as_bytes(std::span(file_name)));
+ status = encoder.WriteFile(as_bytes(span<const char>(file_name)));
}
if (!thread_name.empty()) {
- status = encoder.WriteThread(std::as_bytes(std::span(thread_name)));
+ status = encoder.WriteThread(as_bytes(span<const char>(thread_name)));
}
PW_TRY(encoder.status());
return ConstByteSpan(encoder);
}
-LogEntry::MemoryEncoder CreateEncoderAndEncodeTokenizedLog(
+pwpb::LogEntry::MemoryEncoder CreateEncoderAndEncodeTokenizedLog(
pw::log_tokenized::Metadata metadata,
ConstByteSpan tokenized_data,
int64_t ticks_since_epoch,
ByteSpan encode_buffer) {
// Encode message to the LogEntry protobuf.
- LogEntry::MemoryEncoder encoder(encode_buffer);
+ pwpb::LogEntry::MemoryEncoder encoder(encode_buffer);
// Defer status checks until the end.
Status status = encoder.WriteMessage(tokenized_data);
@@ -80,9 +80,8 @@ LogEntry::MemoryEncoder CreateEncoderAndEncodeTokenizedLog(
status = encoder.WriteTimestamp(ticks_since_epoch);
if (metadata.module() != 0) {
const uint32_t little_endian_module =
- bytes::ConvertOrderTo(std::endian::little, metadata.module());
- status =
- encoder.WriteModule(std::as_bytes(std::span(&little_endian_module, 1)));
+ bytes::ConvertOrderTo(endian::little, metadata.module());
+ status = encoder.WriteModule(as_bytes(span(&little_endian_module, 1)));
}
return encoder;
}
diff --git a/pw_log/proto_utils_test.cc b/pw_log/proto_utils_test.cc
index 7b018006d..94ba45786 100644
--- a/pw_log/proto_utils_test.cc
+++ b/pw_log/proto_utils_test.cc
@@ -16,6 +16,7 @@
#include "gtest/gtest.h"
#include "pw_bytes/span.h"
+#include "pw_containers/algorithm.h"
#include "pw_log/levels.h"
#include "pw_log/proto/log.pwpb.h"
#include "pw_protobuf/bytes_utils.h"
@@ -31,16 +32,16 @@ void VerifyTokenizedLogEntry(pw::protobuf::Decoder& entry_decoder,
ConstByteSpan tokenized_data;
EXPECT_TRUE(entry_decoder.Next().ok()); // message [tokenized]
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::MESSAGE));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kMessage));
EXPECT_TRUE(entry_decoder.ReadBytes(&tokenized_data).ok());
- EXPECT_TRUE(std::memcmp(tokenized_data.begin(),
- expected_tokenized_data.begin(),
+ EXPECT_TRUE(std::memcmp(tokenized_data.data(),
+ expected_tokenized_data.data(),
expected_tokenized_data.size()) == 0);
uint32_t line_level;
EXPECT_TRUE(entry_decoder.Next().ok()); // line_level
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::LINE_LEVEL));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kLineLevel));
EXPECT_TRUE(entry_decoder.ReadUint32(&line_level).ok());
uint32_t line_number;
@@ -53,7 +54,7 @@ void VerifyTokenizedLogEntry(pw::protobuf::Decoder& entry_decoder,
uint32_t flags;
EXPECT_TRUE(entry_decoder.Next().ok()); // flags
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::FLAGS));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kFlags));
EXPECT_TRUE(entry_decoder.ReadUint32(&flags).ok());
EXPECT_EQ(expected_metadata.flags(), flags);
}
@@ -62,16 +63,17 @@ void VerifyTokenizedLogEntry(pw::protobuf::Decoder& entry_decoder,
EXPECT_TRUE(entry_decoder.Next().ok()); // timestamp
EXPECT_TRUE(
entry_decoder.FieldNumber() ==
- static_cast<uint32_t>(log::LogEntry::Fields::TIMESTAMP) ||
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kTimestamp) ||
entry_decoder.FieldNumber() ==
- static_cast<uint32_t>(log::LogEntry::Fields::TIME_SINCE_LAST_ENTRY));
+ static_cast<uint32_t>(
+ log::pwpb::LogEntry::Fields::kTimeSinceLastEntry));
EXPECT_TRUE(entry_decoder.ReadInt64(&timestamp).ok());
EXPECT_EQ(expected_timestamp, timestamp);
if (expected_metadata.module() != 0) {
EXPECT_TRUE(entry_decoder.Next().ok()); // module name
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::MODULE));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kModule));
const Result<uint32_t> module =
protobuf::DecodeBytesToUint32(entry_decoder);
ASSERT_TRUE(module.ok());
@@ -82,10 +84,10 @@ void VerifyTokenizedLogEntry(pw::protobuf::Decoder& entry_decoder,
ConstByteSpan tokenized_thread_name;
EXPECT_TRUE(entry_decoder.Next().ok()); // thread [tokenized]
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::THREAD));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kThread));
EXPECT_TRUE(entry_decoder.ReadBytes(&tokenized_thread_name).ok());
- EXPECT_TRUE(std::memcmp(tokenized_thread_name.begin(),
- expected_thread_name.begin(),
+ EXPECT_TRUE(std::memcmp(tokenized_thread_name.data(),
+ expected_thread_name.data(),
expected_thread_name.size()) == 0);
}
}
@@ -102,17 +104,14 @@ void VerifyLogEntry(pw::protobuf::Decoder& entry_decoder,
std::string_view message;
EXPECT_TRUE(entry_decoder.Next().ok()); // message
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::MESSAGE));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kMessage));
EXPECT_TRUE(entry_decoder.ReadString(&message).ok());
- EXPECT_TRUE(std::equal(message.begin(),
- message.end(),
- expected_message.begin(),
- expected_message.end()));
+ EXPECT_TRUE(pw::containers::Equal(message, expected_message));
uint32_t line_level;
EXPECT_TRUE(entry_decoder.Next().ok()); // line_level
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::LINE_LEVEL));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kLineLevel));
EXPECT_TRUE(entry_decoder.ReadUint32(&line_level).ok());
uint32_t line_number;
uint8_t level;
@@ -124,7 +123,7 @@ void VerifyLogEntry(pw::protobuf::Decoder& entry_decoder,
uint32_t flags;
EXPECT_TRUE(entry_decoder.Next().ok()); // flags
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::FLAGS));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kFlags));
EXPECT_TRUE(entry_decoder.ReadUint32(&flags).ok());
EXPECT_EQ(expected_flags, flags);
}
@@ -133,9 +132,10 @@ void VerifyLogEntry(pw::protobuf::Decoder& entry_decoder,
EXPECT_TRUE(entry_decoder.Next().ok()); // timestamp
EXPECT_TRUE(
entry_decoder.FieldNumber() ==
- static_cast<uint32_t>(log::LogEntry::Fields::TIMESTAMP) ||
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kTimestamp) ||
entry_decoder.FieldNumber() ==
- static_cast<uint32_t>(log::LogEntry::Fields::TIME_SINCE_LAST_ENTRY));
+ static_cast<uint32_t>(
+ log::pwpb::LogEntry::Fields::kTimeSinceLastEntry));
EXPECT_TRUE(entry_decoder.ReadInt64(&timestamp).ok());
EXPECT_EQ(expected_ticks_since_epoch, timestamp);
@@ -143,36 +143,27 @@ void VerifyLogEntry(pw::protobuf::Decoder& entry_decoder,
std::string_view module_name;
EXPECT_TRUE(entry_decoder.Next().ok()); // module
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::MODULE));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kModule));
EXPECT_TRUE(entry_decoder.ReadString(&module_name).ok());
- EXPECT_TRUE(std::equal(module_name.begin(),
- module_name.end(),
- expected_module.begin(),
- expected_module.end()));
+ EXPECT_TRUE(pw::containers::Equal(module_name, expected_module));
}
if (!expected_file_name.empty()) {
std::string_view file_name;
EXPECT_TRUE(entry_decoder.Next().ok()); // file
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::FILE));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kFile));
EXPECT_TRUE(entry_decoder.ReadString(&file_name).ok());
- EXPECT_TRUE(std::equal(file_name.begin(),
- file_name.end(),
- expected_file_name.begin(),
- expected_file_name.end()));
+ EXPECT_TRUE(pw::containers::Equal(file_name, expected_file_name));
}
if (!expected_thread_name.empty()) {
std::string_view thread_name;
EXPECT_TRUE(entry_decoder.Next().ok()); // file
EXPECT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::THREAD));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kThread));
EXPECT_TRUE(entry_decoder.ReadString(&thread_name).ok());
- EXPECT_TRUE(std::equal(thread_name.begin(),
- thread_name.end(),
- expected_thread_name.begin(),
- expected_thread_name.end()));
+ EXPECT_TRUE(pw::containers::Equal(thread_name, expected_thread_name));
}
}
diff --git a/pw_log/protobuf.rst b/pw_log/protobuf.rst
index 32f9ba2e0..ff7335f8c 100644
--- a/pw_log/protobuf.rst
+++ b/pw_log/protobuf.rst
@@ -89,18 +89,21 @@ Encoding logs to the ``log.proto`` format can be performed using the helpers
provided in the ``pw_log/proto_utils.h`` header. Separate helpers are provided
for encoding tokenized logs and string-based logs.
+The following example shows a :c:func:`pw_log_tokenized_HandleLog`
+implementation that encodes the results to a protobuf.
+
.. code-block:: cpp
#include "pw_log/proto_utils.h"
- extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload payload, const uint8_t data[], size_t size) {
+ extern "C" void pw_log_tokenized_HandleLog(
+ uint32_t payload, const uint8_t data[], size_t size) {
pw::log_tokenized::Metadata metadata(payload);
std::byte log_buffer[kLogBufferSize];
Result<ConstByteSpan> result = EncodeTokenizedLog(
metadata,
- std::as_bytes(std::span(data, size)),
+ pw::as_bytes(pw::span(data, size)),
log_buffer);
if (result.ok()) {
// This makes use of the encoded log proto and is custom per-product.
diff --git a/pw_log/public/pw_log/config.h b/pw_log/public/pw_log/config.h
index 340fbe4cb..8f2db4bd8 100644
--- a/pw_log/public/pw_log/config.h
+++ b/pw_log/public/pw_log/config.h
@@ -37,5 +37,5 @@
// This expression determines whether or not the statement is enabled and
// should be passed to the backend.
#ifndef PW_LOG_ENABLE_IF_DEFAULT
-#define PW_LOG_ENABLE_IF_DEFAULT(level, flags) ((level) >= PW_LOG_LEVEL)
+#define PW_LOG_ENABLE_IF_DEFAULT(level, module, flags) ((level) >= PW_LOG_LEVEL)
#endif // PW_LOG_ENABLE_IF_DEFAULT
diff --git a/pw_log/public/pw_log/internal/glog_adapter.h b/pw_log/public/pw_log/internal/glog_adapter.h
index 3e63b90df..38cd3a880 100644
--- a/pw_log/public/pw_log/internal/glog_adapter.h
+++ b/pw_log/public/pw_log/internal/glog_adapter.h
@@ -39,13 +39,16 @@ class GlogStreamingLog {
// Declares a unique GlogStreamingLog class definition with a destructor which
// matches the desired pw_log_level.
-#define _PW_LOG_GLOG_DECLARATION_PW_LOG(pw_log_level, unique) \
- class unique : public ::pw::log::internal::GlogStreamingLog { \
- public: \
- ~unique() { \
- PW_HANDLE_LOG( \
- pw_log_level, PW_LOG_FLAGS, "%s", string_builder_.c_str()); \
- } \
+#define _PW_LOG_GLOG_DECLARATION_PW_LOG(pw_log_level, unique) \
+ class unique : public ::pw::log::internal::GlogStreamingLog { \
+ public: \
+ ~unique() { \
+ PW_HANDLE_LOG(pw_log_level, \
+ PW_LOG_MODULE_NAME, \
+ PW_LOG_FLAGS, \
+ "%s", \
+ string_builder_.c_str()); \
+ } \
}
// Declares a unique GlogStreamingLog class definition with a destructor which
diff --git a/pw_log/public/pw_log/log.h b/pw_log/public/pw_log/log.h
index b9bda56b7..a22c6fb3b 100644
--- a/pw_log/public/pw_log/log.h
+++ b/pw_log/public/pw_log/log.h
@@ -65,11 +65,11 @@
// macro. The format string is not listed as a separate argument to avoid adding
// a comma after the format string when it has no arguments.
#ifndef PW_LOG
-#define PW_LOG(level, flags, /* format string and arguments */...) \
- do { \
- if (PW_LOG_ENABLE_IF(level, flags)) { \
- PW_HANDLE_LOG(level, flags, __VA_ARGS__); \
- } \
+#define PW_LOG(level, module, flags, /* format string and arguments */...) \
+ do { \
+ if (PW_LOG_ENABLE_IF(level, module, flags)) { \
+ PW_HANDLE_LOG(level, module, flags, __VA_ARGS__); \
+ } \
} while (0)
#endif // PW_LOG
@@ -77,24 +77,28 @@
// specialized versions, define the standard PW_LOG_<level>() macros in terms
// of the general PW_LOG().
#ifndef PW_LOG_DEBUG
-#define PW_LOG_DEBUG(...) PW_LOG(PW_LOG_LEVEL_DEBUG, PW_LOG_FLAGS, __VA_ARGS__)
+#define PW_LOG_DEBUG(...) \
+ PW_LOG(PW_LOG_LEVEL_DEBUG, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, __VA_ARGS__)
#endif // PW_LOG_DEBUG
#ifndef PW_LOG_INFO
-#define PW_LOG_INFO(...) PW_LOG(PW_LOG_LEVEL_INFO, PW_LOG_FLAGS, __VA_ARGS__)
+#define PW_LOG_INFO(...) \
+ PW_LOG(PW_LOG_LEVEL_INFO, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, __VA_ARGS__)
#endif // PW_LOG_INFO
#ifndef PW_LOG_WARN
-#define PW_LOG_WARN(...) PW_LOG(PW_LOG_LEVEL_WARN, PW_LOG_FLAGS, __VA_ARGS__)
+#define PW_LOG_WARN(...) \
+ PW_LOG(PW_LOG_LEVEL_WARN, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, __VA_ARGS__)
#endif // PW_LOG_WARN
#ifndef PW_LOG_ERROR
-#define PW_LOG_ERROR(...) PW_LOG(PW_LOG_LEVEL_ERROR, PW_LOG_FLAGS, __VA_ARGS__)
+#define PW_LOG_ERROR(...) \
+ PW_LOG(PW_LOG_LEVEL_ERROR, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, __VA_ARGS__)
#endif // PW_LOG_ERROR
#ifndef PW_LOG_CRITICAL
#define PW_LOG_CRITICAL(...) \
- PW_LOG(PW_LOG_LEVEL_CRITICAL, PW_LOG_FLAGS, __VA_ARGS__)
+ PW_LOG(PW_LOG_LEVEL_CRITICAL, PW_LOG_MODULE_NAME, PW_LOG_FLAGS, __VA_ARGS__)
#endif // PW_LOG_CRITICAL
// Default: Number of bits available for the log flags
diff --git a/pw_log/public/pw_log/options.h b/pw_log/public/pw_log/options.h
index 2d20bd6d6..87936f6db 100644
--- a/pw_log/public/pw_log/options.h
+++ b/pw_log/public/pw_log/options.h
@@ -68,7 +68,7 @@
#endif // PW_LOG_FLAGS
// DEPRECATED: Use PW_LOG_FLAGS.
-// TODO(pwbug/561): Remove this macro after migration.
+// TODO(b/234876701): Remove this macro after migration.
#ifndef PW_LOG_DEFAULT_FLAGS
#define PW_LOG_DEFAULT_FLAGS PW_LOG_FLAGS
#endif // PW_LOG_DEFAULT_FLAGS
@@ -78,5 +78,6 @@
// This expression determines whether or not the statement is enabled and
// should be passed to the backend.
#ifndef PW_LOG_ENABLE_IF
-#define PW_LOG_ENABLE_IF(level, flags) PW_LOG_ENABLE_IF_DEFAULT(level, flags)
+#define PW_LOG_ENABLE_IF(level, module, flags) \
+ PW_LOG_ENABLE_IF_DEFAULT(level, module, flags)
#endif // PW_LOG_ENABLE_IF
diff --git a/pw_log/public/pw_log/proto_utils.h b/pw_log/public/pw_log/proto_utils.h
index a9bada5d5..e6d390d58 100644
--- a/pw_log/public/pw_log/proto_utils.h
+++ b/pw_log/public/pw_log/proto_utils.h
@@ -65,7 +65,7 @@ Result<ConstByteSpan> EncodeLog(int level,
// Encodes tokenized message and metadata, with a timestamp as a log proto.
// Extra fields can be encoded into the returned encoder. The caller must check
// the encoder status.
-LogEntry::MemoryEncoder CreateEncoderAndEncodeTokenizedLog(
+pwpb::LogEntry::MemoryEncoder CreateEncoderAndEncodeTokenizedLog(
log_tokenized::Metadata metadata,
ConstByteSpan tokenized_data,
int64_t ticks_since_epoch,
@@ -83,7 +83,7 @@ inline Result<ConstByteSpan> EncodeTokenizedLog(
ConstByteSpan tokenized_data,
int64_t ticks_since_epoch,
ByteSpan encode_buffer) {
- LogEntry::MemoryEncoder encoder = CreateEncoderAndEncodeTokenizedLog(
+ pwpb::LogEntry::MemoryEncoder encoder = CreateEncoderAndEncodeTokenizedLog(
metadata, tokenized_data, ticks_since_epoch, encode_buffer);
PW_TRY(encoder.status());
return ConstByteSpan(encoder);
@@ -95,11 +95,10 @@ inline Result<ConstByteSpan> EncodeTokenizedLog(
size_t tokenized_data_size,
int64_t ticks_since_epoch,
ByteSpan encode_buffer) {
- return EncodeTokenizedLog(
- metadata,
- std::as_bytes(std::span(tokenized_data, tokenized_data_size)),
- ticks_since_epoch,
- encode_buffer);
+ return EncodeTokenizedLog(metadata,
+ as_bytes(span(tokenized_data, tokenized_data_size)),
+ ticks_since_epoch,
+ encode_buffer);
}
// Encodes tokenized message (passed as pointer and size), tokenized metadata,
@@ -116,9 +115,9 @@ inline Result<ConstByteSpan> EncodeTokenizedLog(
int64_t ticks_since_epoch,
ConstByteSpan thread_name,
ByteSpan encode_buffer) {
- LogEntry::MemoryEncoder encoder = CreateEncoderAndEncodeTokenizedLog(
+ pwpb::LogEntry::MemoryEncoder encoder = CreateEncoderAndEncodeTokenizedLog(
metadata,
- std::as_bytes(std::span(tokenized_data, tokenized_data_size)),
+ as_bytes(span(tokenized_data, tokenized_data_size)),
ticks_since_epoch,
encode_buffer);
if (!thread_name.empty()) {
@@ -141,7 +140,7 @@ inline Result<ConstByteSpan> EncodeTokenizedLog(
int64_t ticks_since_epoch,
ConstByteSpan thread_name,
ByteSpan encode_buffer) {
- LogEntry::MemoryEncoder encoder = CreateEncoderAndEncodeTokenizedLog(
+ pwpb::LogEntry::MemoryEncoder encoder = CreateEncoderAndEncodeTokenizedLog(
metadata, tokenized_data, ticks_since_epoch, encode_buffer);
if (!thread_name.empty()) {
encoder.WriteThread(thread_name).IgnoreError();
diff --git a/pw_log_android/Android.bp b/pw_log_android/Android.bp
index b54e8a85e..a3b18c1d7 100644
--- a/pw_log_android/Android.bp
+++ b/pw_log_android/Android.bp
@@ -13,11 +13,6 @@
// the License.
package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "external_pigweed_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["external_pigweed_license"],
}
@@ -29,10 +24,10 @@ cc_library {
"public",
"public_overrides",
],
- export_static_lib_headers: [
- "libpw_log",
+ export_header_lib_headers: [
+ "pw_log_headers",
],
- static_libs: [
- "libpw_log",
+ header_libs: [
+ "pw_log_headers",
],
}
diff --git a/pw_log_android/BUILD.gn b/pw_log_android/BUILD.gn
index 8316fd84a..4292b75df 100644
--- a/pw_log_android/BUILD.gn
+++ b/pw_log_android/BUILD.gn
@@ -12,4 +12,15 @@
# License for the specific language governing permissions and limitations under
# the License.
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
# Android only uses Soong Blueprints, so this file is empty.
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/third_party/embos/OWNERS b/pw_log_android/OWNERS
index 21d24bca7..21d24bca7 100644
--- a/third_party/embos/OWNERS
+++ b/pw_log_android/OWNERS
diff --git a/pw_log_android/docs.rst b/pw_log_android/docs.rst
new file mode 100644
index 000000000..84c5069ae
--- /dev/null
+++ b/pw_log_android/docs.rst
@@ -0,0 +1,9 @@
+.. _module-pw_log_android:
+
+==============
+pw_log_android
+==============
+
+.. warning::
+
+ This documentation is under construction.
diff --git a/pw_log_android/public/pw_log_android/log_android.h b/pw_log_android/public/pw_log_android/log_android.h
index 410f149e6..144a66cfe 100644
--- a/pw_log_android/public/pw_log_android/log_android.h
+++ b/pw_log_android/public/pw_log_android/log_android.h
@@ -39,5 +39,9 @@
// #define PW_LOG_LEVEL_FATAL 7
#define _PW_LOG_ANDROID_LEVEL_7(...) LOG_ALWAYS_FATAL(__VA_ARGS__)
-#define PW_HANDLE_LOG(level, flags, ...) \
+#define _PW_HANDLE_LOG(level, module, flags, ...) \
_PW_LOG_ANDROID_LEVEL_##level(__VA_ARGS__)
+
+// The indirection through _PW_HANDLE_LOG ensures the `level` argument is
+// expanded.
+#define PW_HANDLE_LOG(...) _PW_HANDLE_LOG(__VA_ARGS__)
diff --git a/pw_log_basic/BUILD.gn b/pw_log_basic/BUILD.gn
index a478ea21e..e8dd52a1e 100644
--- a/pw_log_basic/BUILD.gn
+++ b/pw_log_basic/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# The build target that overrides the default configuration options for this
@@ -75,3 +76,6 @@ pw_source_set("pw_log_basic.impl") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_log_basic/CMakeLists.txt b/pw_log_basic/CMakeLists.txt
index 64724a9d5..c9c37c3ab 100644
--- a/pw_log_basic/CMakeLists.txt
+++ b/pw_log_basic/CMakeLists.txt
@@ -16,11 +16,21 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_module_config(pw_log_basic_CONFIG)
-pw_auto_add_simple_module(pw_log_basic
- IMPLEMENTS_FACADE
- pw_log
+pw_add_library(pw_log_basic STATIC
+ HEADERS
+ public/pw_log_basic/log_basic.h
+ public_overrides/pw_log_backend/log_backend.h
+ pw_log_basic_private/config.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_preprocessor
+ SOURCES
+ log_basic.cc
PRIVATE_DEPS
pw_string
pw_sys_io
+ pw_log.facade
${pw_log_basic_CONFIG}
)
diff --git a/pw_log_basic/docs.rst b/pw_log_basic/docs.rst
index 7bdf8c5d2..153264f3c 100644
--- a/pw_log_basic/docs.rst
+++ b/pw_log_basic/docs.rst
@@ -18,9 +18,11 @@ calling ``pw::log_basic::SetOutput``.
Set the log output function, which defaults ``pw_sys_io::WriteLine``. This
function is called with each formatted log message.
-This module employs an internal buffer for formatting log strings, and currently
-has a fixed size of 150 bytes. Any final log statements that are larger than
-149 bytes (one byte used for a null terminator) will be truncated.
+This module employs an internal buffer for formatting log strings, whose size
+can be configured via the ``PW_LOG_BASIC_ENTRY_SIZE`` macro which defaults to
+150 bytes. Any final log statements that are larger than
+``PW_LOG_BASIC_ENTRY_SIZE - 1`` bytes (one byte used for a null terminator) will
+be truncated.
.. note::
The documentation for this module is currently incomplete.
diff --git a/pw_log_basic/log_basic.cc b/pw_log_basic/log_basic.cc
index 71a6197f0..cde9ededc 100644
--- a/pw_log_basic/log_basic.cc
+++ b/pw_log_basic/log_basic.cc
@@ -85,7 +85,7 @@ const char* GetFileBasename(const char* filename) {
void (*write_log)(std::string_view) = [](std::string_view log) {
sys_io::WriteLine(log)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
};
} // namespace
@@ -100,7 +100,7 @@ extern "C" void pw_Log(int level,
const char* message,
...) {
// Accumulate the log message in this buffer, then output it.
- pw::StringBuffer<150> buffer;
+ pw::StringBuffer<PW_LOG_BASIC_ENTRY_SIZE> buffer;
// Column: Timestamp
// Note that this macro method defaults to a no-op.
diff --git a/pw_log_basic/public/pw_log_basic/log_basic.h b/pw_log_basic/public/pw_log_basic/log_basic.h
index d16ef0bb1..a2c0e7889 100644
--- a/pw_log_basic/public/pw_log_basic/log_basic.h
+++ b/pw_log_basic/public/pw_log_basic/log_basic.h
@@ -38,16 +38,17 @@ PW_EXTERN_C_END
// arguments. Additionally, the use of the __FUNC__ macro adds a static const
// char[] variable inside functions with a log.
//
-// TODO(pwbug/87): Reconsider the naming of this module when more is in place.
-#define PW_HANDLE_LOG(level, flags, message, ...) \
- do { \
- pw_Log((level), \
- (flags), \
- PW_LOG_MODULE_NAME, \
- __FILE__, \
- __LINE__, \
- __func__, \
- message PW_COMMA_ARGS(__VA_ARGS__)); \
+// TODO(b/235289435): Reconsider the naming of this module when more is in
+// place.
+#define PW_HANDLE_LOG(level, module, flags, message, ...) \
+ do { \
+ pw_Log((level), \
+ (flags), \
+ module, \
+ __FILE__, \
+ __LINE__, \
+ __func__, \
+ message PW_COMMA_ARGS(__VA_ARGS__)); \
} while (0)
#ifdef __cplusplus
diff --git a/pw_log_basic/pw_log_basic_private/config.h b/pw_log_basic/pw_log_basic_private/config.h
index 7ed638256..bb6b28e29 100644
--- a/pw_log_basic/pw_log_basic_private/config.h
+++ b/pw_log_basic/pw_log_basic_private/config.h
@@ -67,3 +67,10 @@
do { \
} while (0)
#endif // PW_LOG_APPEND_TIMESTAMP
+
+// Maximum size of the encoded log message. Log messages that would result in a
+// larger entry are truncated to this size. The message buffer is allocated on
+// the stack on every log call.
+#ifndef PW_LOG_BASIC_ENTRY_SIZE
+#define PW_LOG_BASIC_ENTRY_SIZE 150
+#endif // PW_LOG_BASIC_ENTRY_SIZE
diff --git a/pw_log_null/Android.bp b/pw_log_null/Android.bp
new file mode 100644
index 000000000..ea7a1945c
--- /dev/null
+++ b/pw_log_null/Android.bp
@@ -0,0 +1,28 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_log_null_headers",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: [
+ "public",
+ "public_overrides",
+ ],
+ host_supported: true,
+}
diff --git a/pw_log_null/BUILD.bazel b/pw_log_null/BUILD.bazel
index 0e5fe5ac5..7feeec0b2 100644
--- a/pw_log_null/BUILD.bazel
+++ b/pw_log_null/BUILD.bazel
@@ -15,6 +15,7 @@
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
+ "pw_cc_test",
)
package(default_visibility = ["//visibility:public"])
@@ -23,8 +24,10 @@ licenses(["notice"])
pw_cc_library(
name = "headers",
- hdrs = [
+ srcs = [
"public/pw_log_null/log_null.h",
+ ],
+ hdrs = [
"public_overrides/pw_log_backend/log_backend.h",
],
includes = [
@@ -38,21 +41,17 @@ pw_cc_library(
pw_cc_library(
name = "pw_log_null",
- srcs = [
- "log_null.cc",
- ],
deps = [
"//pw_log:facade",
"//pw_log_null:headers",
- "//pw_string",
- "//pw_sys_io",
],
)
-pw_cc_library(
+pw_cc_test(
name = "test",
srcs = [
"test.cc",
"test_c.c",
],
+ deps = [":pw_log_null"],
)
diff --git a/pw_log_null/CMakeLists.txt b/pw_log_null/CMakeLists.txt
index 1f74b8dc9..8b63e1996 100644
--- a/pw_log_null/CMakeLists.txt
+++ b/pw_log_null/CMakeLists.txt
@@ -14,9 +14,24 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_auto_add_simple_module(pw_log_null
- IMPLEMENTS_FACADE
- pw_log
+pw_add_library(pw_log_null INTERFACE
+ HEADERS
+ public/pw_log_null/log_null.h
+ public_overrides/pw_log_backend/log_backend.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
PUBLIC_DEPS
pw_preprocessor
)
+
+pw_add_test(pw_log_null.test
+ SOURCES
+ test.cc
+ test_c.c
+ PRIVATE_DEPS
+ pw_log_null
+ GROUPS
+ modules
+ pw_log_null
+)
diff --git a/pw_log_null/public/pw_log_null/log_null.h b/pw_log_null/public/pw_log_null/log_null.h
index 4587e0960..4fda5a831 100644
--- a/pw_log_null/public/pw_log_null/log_null.h
+++ b/pw_log_null/public/pw_log_null/log_null.h
@@ -31,26 +31,23 @@ PW_EXTERN_C_START
// For compatibility with C and the printf compiler attribute, the declaration
// and definition must be separate and both marked inline.
static inline void pw_log_Ignored(int level,
- unsigned int flags,
const char* module_name,
+ unsigned int flags,
const char* message,
...) PW_PRINTF_FORMAT(4, 5);
static inline void pw_log_Ignored(int level,
- unsigned int flags,
const char* module_name,
+ unsigned int flags,
const char* message,
...) {
(void)level;
- (void)flags;
(void)module_name;
+ (void)flags;
(void)message;
}
PW_EXTERN_C_END
-#define PW_LOG(level, flags, message, ...) \
- pw_log_Ignored((level), \
- (flags), \
- PW_LOG_MODULE_NAME, \
- message PW_COMMA_ARGS(__VA_ARGS__))
+#define PW_HANDLE_LOG(level, module, flags, message, ...) \
+ pw_log_Ignored((level), module, (flags), message PW_COMMA_ARGS(__VA_ARGS__))
diff --git a/pw_log_null/test.cc b/pw_log_null/test.cc
index 6c656be53..3a86d48df 100644
--- a/pw_log_null/test.cc
+++ b/pw_log_null/test.cc
@@ -22,13 +22,19 @@ extern "C" bool CTest();
namespace {
TEST(LogNull, NoArguments) {
- PW_LOG(1, 2, "3");
- PW_LOG(1, 2, "whoa");
+ PW_HANDLE_LOG(1, PW_LOG_MODULE_NAME, 2, "3");
+ PW_HANDLE_LOG(1, PW_LOG_MODULE_NAME, 2, "whoa");
}
TEST(LogNull, WithArguments) {
- PW_LOG(1, 2, "%s", "hello");
- PW_LOG(1, 2, "%d + %s == %p", 1, "two", nullptr);
+ PW_HANDLE_LOG(1, PW_LOG_MODULE_NAME, 2, "%s", "hello");
+ PW_HANDLE_LOG(1,
+ PW_LOG_MODULE_NAME,
+ 2,
+ "%d + %s == %p",
+ 1,
+ "two",
+ static_cast<void*>(nullptr));
}
TEST(LogNull, ExpressionsAreEvaluated) {
@@ -37,10 +43,15 @@ TEST(LogNull, ExpressionsAreEvaluated) {
global = 0;
bool local = true;
- PW_LOG(1, 2, "You are number%s %d!", (local = false) ? "" : " not", []() {
- global = 1;
- return global;
- }());
+ PW_HANDLE_LOG(1,
+ PW_LOG_MODULE_NAME,
+ 2,
+ "You are number%s %d!",
+ (local = false) ? "" : " not",
+ []() {
+ global = 1;
+ return global;
+ }());
EXPECT_EQ(1, global);
EXPECT_FALSE(local);
diff --git a/pw_log_null/test_c.c b/pw_log_null/test_c.c
index 6d568d70e..755dce78b 100644
--- a/pw_log_null/test_c.c
+++ b/pw_log_null/test_c.c
@@ -24,19 +24,20 @@ static int global;
static int IncrementGlobal(void) { return ++global; }
bool CTest(void) {
- PW_LOG(1, 2, "3");
- PW_LOG(1, 2, "whoa");
- PW_LOG(1, 2, "%s", "hello");
- PW_LOG(1, 2, "%d + %s == %p", 1, "two", NULL);
+ PW_HANDLE_LOG(1, PW_LOG_MODULE_NAME, 2, "3");
+ PW_HANDLE_LOG(1, PW_LOG_MODULE_NAME, 2, "whoa");
+ PW_HANDLE_LOG(1, PW_LOG_MODULE_NAME, 2, "%s", "hello");
+ PW_HANDLE_LOG(1, PW_LOG_MODULE_NAME, 2, "%d + %s == %p", 1, "two", NULL);
global = 0;
bool local = true;
- PW_LOG(1,
- 2,
- "You are number%s %d!",
- (local = false) ? "" : " not",
- IncrementGlobal());
+ PW_HANDLE_LOG(1,
+ PW_LOG_MODULE_NAME,
+ 2,
+ "You are number%s %d!",
+ (local = false) ? "" : " not",
+ IncrementGlobal());
return global == 1 && !local;
}
diff --git a/pw_log_rpc/BUILD.bazel b/pw_log_rpc/BUILD.bazel
index dab90830d..b072dbf98 100644
--- a/pw_log_rpc/BUILD.bazel
+++ b/pw_log_rpc/BUILD.bazel
@@ -90,6 +90,7 @@ pw_cc_library(
deps = [
":log_filter",
"//pw_assert",
+ "//pw_chrono:system_clock",
"//pw_function",
"//pw_log:log_proto_cc.pwpb",
"//pw_log:log_proto_cc.raw_rpc",
@@ -127,8 +128,8 @@ pw_cc_library(
"//pw_bytes",
"//pw_containers:vector",
"//pw_log",
- "//pw_log:log_pwpb",
- "//pw_log_tokenized:metadata",
+ "//pw_log:log_proto_cc.pwpb",
+ "//pw_log_tokenized:headers",
"//pw_protobuf",
"//pw_protobuf:bytes_utils",
"//pw_unit_test",
@@ -193,7 +194,8 @@ pw_cc_test(
":rpc_log_drain",
":test_utils",
"//pw_bytes",
- "//pw_log:log_pwpb",
+ "//pw_log:log_proto_cc.pwpb",
+ "//pw_log:proto_utils",
"//pw_multisink",
"//pw_protobuf",
"//pw_rpc",
diff --git a/pw_log_rpc/BUILD.gn b/pw_log_rpc/BUILD.gn
index bfe2f6adf..5b62508b3 100644
--- a/pw_log_rpc/BUILD.gn
+++ b/pw_log_rpc/BUILD.gn
@@ -14,11 +14,19 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
+declare_args() {
+ # The build target that overrides the default configuration options for this
+ # module. This should point to a source set that provides defines through a
+ # public config (which may -include a file or add defines directly).
+ pw_log_rpc_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
+}
+
config("public_include_path") {
include_dirs = [ "public" ]
visibility = [ ":*" ]
@@ -27,6 +35,7 @@ config("public_include_path") {
pw_source_set("config") {
sources = [ "public/pw_log_rpc/internal/config.h" ]
public_configs = [ ":public_include_path" ]
+ public_deps = [ pw_log_rpc_CONFIG ]
visibility = [ "./*" ]
friend = [ "./*" ]
}
@@ -59,6 +68,7 @@ pw_source_set("log_filter_service") {
public = [ "public/pw_log_rpc/log_filter_service.h" ]
sources = [ "log_filter_service.cc" ]
deps = [
+ "$dir_pw_log",
"$dir_pw_log:protos.pwpb",
"$dir_pw_protobuf",
]
@@ -76,18 +86,17 @@ pw_source_set("log_filter") {
"public/pw_log_rpc/log_filter_map.h",
]
sources = [ "log_filter.cc" ]
- deps = [
- "$dir_pw_log",
- "$dir_pw_protobuf",
- ]
+ deps = [ "$dir_pw_log" ]
public_deps = [
":config",
"$dir_pw_assert",
"$dir_pw_bytes",
"$dir_pw_containers:vector",
+ "$dir_pw_log",
"$dir_pw_log:protos.pwpb",
"$dir_pw_protobuf",
"$dir_pw_status",
+ dir_pw_span,
]
}
@@ -112,6 +121,7 @@ pw_source_set("rpc_log_drain") {
"$dir_pw_status",
"$dir_pw_sync:lock_annotations",
"$dir_pw_sync:mutex",
+ dir_pw_span,
]
}
@@ -187,6 +197,7 @@ pw_test("log_filter_test") {
sources = [ "log_filter_test.cc" ]
deps = [
":log_filter",
+ "$dir_pw_log",
"$dir_pw_log:proto_utils",
"$dir_pw_log:protos.pwpb",
"$dir_pw_log_tokenized:metadata",
diff --git a/pw_log_rpc/CMakeLists.txt b/pw_log_rpc/CMakeLists.txt
new file mode 100644
index 000000000..c6bda839f
--- /dev/null
+++ b/pw_log_rpc/CMakeLists.txt
@@ -0,0 +1,233 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_module_config(pw_log_rpc_CONFIG)
+
+pw_add_library(pw_log_rpc.config INTERFACE
+ HEADERS
+ public/pw_log_rpc/internal/config.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ ${pw_log_rpc_CONFIG}
+)
+
+pw_add_library(pw_log_rpc.log_config INTERFACE
+ HEADERS
+ public/pw_log_rpc/internal/log_config.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_log_rpc.config
+)
+
+pw_add_library(pw_log_rpc.log_service STATIC
+ HEADERS
+ public/pw_log_rpc/log_service.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_log_rpc.rpc_log_drain
+ pw_log.protos.raw_rpc
+ SOURCES
+ log_service.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_log.protos.pwpb
+ pw_log_rpc.log_config
+)
+
+pw_add_library(pw_log_rpc.log_filter_service STATIC
+ HEADERS
+ public/pw_log_rpc/log_filter_service.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_log.protos.raw_rpc
+ pw_log_rpc.log_filter
+ pw_protobuf.bytes_utils
+ SOURCES
+ log_filter_service.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_log.protos.pwpb
+ pw_protobuf
+)
+
+pw_add_library(pw_log_rpc.log_filter STATIC
+ HEADERS
+ public/pw_log_rpc/log_filter.h
+ public/pw_log_rpc/log_filter_map.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_bytes
+ pw_containers.vector
+ pw_log.protos.pwpb
+ pw_log_rpc.config
+ pw_protobuf
+ pw_span
+ pw_status
+ SOURCES
+ log_filter.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_log.protos.pwpb
+)
+
+pw_add_library(pw_log_rpc.rpc_log_drain STATIC
+ HEADERS
+ public/pw_log_rpc/rpc_log_drain.h
+ public/pw_log_rpc/rpc_log_drain_map.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_chrono.system_clock
+ pw_function
+ pw_log.protos.pwpb
+ pw_log.protos.raw_rpc
+ pw_log_rpc.config
+ pw_log_rpc.log_filter
+ pw_multisink
+ pw_protobuf
+ pw_result
+ pw_span
+ pw_status
+ pw_sync.lock_annotations
+ pw_sync.mutex
+ SOURCES
+ rpc_log_drain.cc
+)
+
+pw_add_library(pw_log_rpc.rpc_log_drain_thread INTERFACE
+ HEADERS
+ public/pw_log_rpc/rpc_log_drain_thread.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_log_rpc.log_service
+ pw_log_rpc.rpc_log_drain
+ pw_multisink
+ pw_result
+ pw_rpc.raw.server_api
+ pw_status
+ pw_sync.timed_thread_notification
+ pw_thread.thread
+)
+
+pw_add_library(pw_log_rpc.test_utils STATIC
+ HEADERS
+ pw_log_rpc_private/test_utils.h
+ PUBLIC_DEPS
+ pw_bytes
+ pw_containers.vector
+ pw_log_tokenized.metadata
+ pw_protobuf
+ pw_unit_test
+ SOURCES
+ test_utils.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_log.protos.pwpb
+ pw_protobuf.bytes_utils
+)
+
+if(NOT "${pw_chrono.system_clock_BACKEND}" STREQUAL "")
+ pw_add_test(pw_log_rpc.log_service_test
+ SOURCES
+ log_service_test.cc
+ PRIVATE_DEPS
+ pw_containers.vector
+ pw_log
+ pw_log.proto_utils
+ pw_log.protos.pwpb
+ pw_log_rpc.log_filter
+ pw_log_rpc.log_service
+ pw_log_rpc.test_utils
+ pw_log_tokenized.metadata
+ pw_protobuf
+ pw_protobuf.bytes_utils
+ pw_result
+ pw_rpc.raw.test_method_context
+ pw_status
+ GROUPS
+ modules
+ pw_log_rpc
+ )
+endif()
+
+pw_add_test(pw_log_rpc.log_filter_service_test
+ SOURCES
+ log_filter_service_test.cc
+ PRIVATE_DEPS
+ pw_log.protos.pwpb
+ pw_log_rpc.log_filter
+ pw_log_rpc.log_filter_service
+ pw_protobuf
+ pw_protobuf.bytes_utils
+ pw_result
+ pw_rpc.raw.test_method_context
+ pw_status
+ GROUPS
+ modules
+ pw_log_rpc
+)
+
+pw_add_test(pw_log_rpc.log_filter_test
+ SOURCES
+ log_filter_test.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_log.proto_utils
+ pw_log.protos.pwpb
+ pw_log_rpc.log_filter
+ pw_log_tokenized.metadata
+ pw_result
+ pw_status
+ GROUPS
+ modules
+ pw_log_rpc
+)
+
+if(NOT "${pw_chrono.system_clock_BACKEND}" STREQUAL "")
+ pw_add_test(pw_log_rpc.rpc_log_drain_test
+ SOURCES
+ rpc_log_drain_test.cc
+ PRIVATE_DEPS
+ pw_bytes
+ pw_log.proto_utils
+ pw_log.protos.pwpb
+ pw_log_rpc.log_filter
+ pw_log_rpc.log_service
+ pw_log_rpc.rpc_log_drain
+ pw_log_rpc.test_utils
+ pw_log_tokenized.metadata
+ pw_multisink
+ pw_protobuf
+ pw_rpc.common
+ pw_rpc.raw.fake_channel_output
+ pw_rpc.raw.server_api
+ pw_rpc.raw.test_method_context
+ pw_status
+ pw_sync.mutex
+ GROUPS
+ modules
+ pw_log_rpc
+ )
+endif()
diff --git a/pw_log_rpc/docs.rst b/pw_log_rpc/docs.rst
index cf98d85e1..0b30356f0 100644
--- a/pw_log_rpc/docs.rst
+++ b/pw_log_rpc/docs.rst
@@ -28,9 +28,8 @@ Set up the :ref:`module-pw_log_tokenized` log backend.
3. Connect the tokenized logging handler to the MultiSink
---------------------------------------------------------
Create a :ref:`MultiSink <module-pw_multisink>` instance to buffer log entries.
-Then, make the log backend handler,
-``pw_tokenizer_HandleEncodedMessageWithPayload``, encode log entries in the
-``log::LogEntry`` format, and add them to the ``MultiSink``.
+Then, make the log backend handler, :c:func:`pw_log_tokenized_HandleLog`, encode
+log entries in the ``log::LogEntry`` format, and add them to the ``MultiSink``.
4. Create log drains and filters
--------------------------------
@@ -296,6 +295,9 @@ conditions must be met for the rule to be met.
- ``module_equals``: the condition is met if this byte array is empty, or the
log module equals the contents of this byte array.
+- ``thread_equals``: the condition is met if this byte array is empty or the
+ log thread equals the contents of this byte array.
+
Filter
------
Encapsulates a collection of zero or more ``Filter::Rule``\s and has
@@ -363,7 +365,6 @@ log drains and filters are set up.
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/lock_annotations.h"
#include "pw_sync/mutex.h"
- #include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
namespace foo::log {
namespace {
@@ -410,13 +411,13 @@ log drains and filters are set up.
},
}};
std::array<Filter, 2> filters{
- Filter(std::as_bytes(std::span("HOST", 4)), logs_to_host_filter_rules),
- Filter(std::as_bytes(std::span("WEB", 3)), logs_to_server_filter_rules),
+ Filter(pw::as_bytes(pw::span("HOST", 4)), logs_to_host_filter_rules),
+ Filter(pw::as_bytes(pw::span("WEB", 3)), logs_to_server_filter_rules),
};
pw::log_rpc::FilterMap filter_map(filters);
- extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload metadata, const uint8_t message[], size_t size_bytes) {
+ extern "C" void pw_log_tokenized_HandleLog(
+ uint32_t metadata, const uint8_t message[], size_t size_bytes) {
int64_t timestamp =
pw::chrono::SystemClock::now().time_since_epoch().count();
std::lock_guard lock(log_encode_lock);
diff --git a/pw_log_rpc/log_filter.cc b/pw_log_rpc/log_filter.cc
index 5385be4a1..4c29bc9cb 100644
--- a/pw_log_rpc/log_filter.cc
+++ b/pw_log_rpc/log_filter.cc
@@ -21,11 +21,15 @@
namespace pw::log_rpc {
namespace {
+namespace FilterRule = ::pw::log::pwpb::FilterRule;
+namespace LogEntry = ::pw::log::pwpb::LogEntry;
+
// Returns true if the provided log parameters match the given filter rule.
bool IsRuleMet(const Filter::Rule& rule,
uint32_t level,
ConstByteSpan module,
- uint32_t flags) {
+ uint32_t flags,
+ ConstByteSpan thread) {
if (level < static_cast<uint32_t>(rule.level_greater_than_or_equal)) {
return false;
}
@@ -38,6 +42,12 @@ bool IsRuleMet(const Filter::Rule& rule,
rule.module_equals.end())) {
return false;
}
+ if (!rule.thread_equals.empty() && !std::equal(thread.begin(),
+ thread.end(),
+ rule.thread_equals.begin(),
+ rule.thread_equals.end())) {
+ return false;
+ }
return true;
}
@@ -61,13 +71,12 @@ Status Filter::UpdateRulesFromProto(ConstByteSpan buffer) {
PW_TRY(decoder.ReadBytes(&rule_buffer));
protobuf::Decoder rule_decoder(rule_buffer);
while ((status = rule_decoder.Next()).ok()) {
- switch (
- static_cast<log::FilterRule::Fields>(rule_decoder.FieldNumber())) {
- case log::FilterRule::Fields::LEVEL_GREATER_THAN_OR_EQUAL:
+ switch (static_cast<FilterRule::Fields>(rule_decoder.FieldNumber())) {
+ case FilterRule::Fields::kLevelGreaterThanOrEqual:
PW_TRY(rule_decoder.ReadUint32(reinterpret_cast<uint32_t*>(
&rules_[i].level_greater_than_or_equal)));
break;
- case log::FilterRule::Fields::MODULE_EQUALS: {
+ case FilterRule::Fields::kModuleEquals: {
ConstByteSpan module;
PW_TRY(rule_decoder.ReadBytes(&module));
if (module.size() > rules_[i].module_equals.max_size()) {
@@ -75,13 +84,21 @@ Status Filter::UpdateRulesFromProto(ConstByteSpan buffer) {
}
rules_[i].module_equals.assign(module.begin(), module.end());
} break;
- case log::FilterRule::Fields::ANY_FLAGS_SET:
+ case FilterRule::Fields::kAnyFlagsSet:
PW_TRY(rule_decoder.ReadUint32(&rules_[i].any_flags_set));
break;
- case log::FilterRule::Fields::ACTION:
+ case FilterRule::Fields::kAction:
PW_TRY(rule_decoder.ReadUint32(
reinterpret_cast<uint32_t*>(&rules_[i].action)));
break;
+ case FilterRule::Fields::kThreadEquals: {
+ ConstByteSpan thread;
+ PW_TRY(rule_decoder.ReadBytes(&thread));
+ if (thread.size() > rules_[i].thread_equals.max_size()) {
+ return Status::InvalidArgument();
+ }
+ rules_[i].thread_equals.assign(thread.begin(), thread.end());
+ } break;
}
}
}
@@ -95,23 +112,25 @@ bool Filter::ShouldDropLog(ConstByteSpan entry) const {
uint32_t log_level = 0;
ConstByteSpan log_module;
+ ConstByteSpan log_thread;
uint32_t log_flags = 0;
protobuf::Decoder decoder(entry);
while (decoder.Next().ok()) {
- switch (static_cast<log::LogEntry::Fields>(decoder.FieldNumber())) {
- case log::LogEntry::Fields::LINE_LEVEL:
- if (decoder.ReadUint32(&log_level).ok()) {
- log_level &= PW_LOG_LEVEL_BITMASK;
- }
- break;
- case log::LogEntry::Fields::MODULE:
- decoder.ReadBytes(&log_module).IgnoreError();
- break;
- case log::LogEntry::Fields::FLAGS:
- decoder.ReadUint32(&log_flags).IgnoreError();
- break;
- default:
- break;
+ const auto field_num = static_cast<LogEntry::Fields>(decoder.FieldNumber());
+
+ if (field_num == LogEntry::Fields::kLineLevel) {
+ if (decoder.ReadUint32(&log_level).ok()) {
+ log_level &= PW_LOG_LEVEL_BITMASK;
+ }
+
+ } else if (field_num == LogEntry::Fields::kModule) {
+ decoder.ReadBytes(&log_module).IgnoreError();
+
+ } else if (field_num == LogEntry::Fields::kFlags) {
+ decoder.ReadUint32(&log_flags).IgnoreError();
+
+ } else if (field_num == LogEntry::Fields::kThread) {
+ decoder.ReadBytes(&log_thread).IgnoreError();
}
}
@@ -120,7 +139,7 @@ bool Filter::ShouldDropLog(ConstByteSpan entry) const {
if (rule.action == Filter::Rule::Action::kInactive) {
continue;
}
- if (IsRuleMet(rule, log_level, log_module, log_flags)) {
+ if (IsRuleMet(rule, log_level, log_module, log_flags, log_thread)) {
return rule.action == Filter::Rule::Action::kDrop;
}
}
diff --git a/pw_log_rpc/log_filter_service.cc b/pw_log_rpc/log_filter_service.cc
index 65e397411..eedb1d5c6 100644
--- a/pw_log_rpc/log_filter_service.cc
+++ b/pw_log_rpc/log_filter_service.cc
@@ -20,11 +20,15 @@
namespace pw::log_rpc {
+namespace GetFilterRequest = ::pw::log::pwpb::GetFilterRequest;
+namespace SetFilterRequest = ::pw::log::pwpb::SetFilterRequest;
+namespace FilterRule = ::pw::log::pwpb::FilterRule;
+
Status FilterService::SetFilterImpl(ConstByteSpan request) {
protobuf::Decoder decoder(request);
PW_TRY(decoder.Next());
- if (static_cast<log::SetFilterRequest::Fields>(decoder.FieldNumber()) !=
- log::SetFilterRequest::Fields::FILTER_ID) {
+ if (static_cast<SetFilterRequest::Fields>(decoder.FieldNumber()) !=
+ SetFilterRequest::Fields::kFilterId) {
return Status::InvalidArgument();
}
ConstByteSpan filter_id;
@@ -36,8 +40,8 @@ Status FilterService::SetFilterImpl(ConstByteSpan request) {
PW_TRY(decoder.Next());
ConstByteSpan filter_buffer;
- if (static_cast<log::SetFilterRequest::Fields>(decoder.FieldNumber()) !=
- log::SetFilterRequest::Fields::FILTER) {
+ if (static_cast<SetFilterRequest::Fields>(decoder.FieldNumber()) !=
+ SetFilterRequest::Fields::kFilter) {
return Status::InvalidArgument();
}
PW_TRY(decoder.ReadBytes(&filter_buffer));
@@ -49,8 +53,8 @@ StatusWithSize FilterService::GetFilterImpl(ConstByteSpan request,
ByteSpan response) {
protobuf::Decoder decoder(request);
PW_TRY_WITH_SIZE(decoder.Next());
- if (static_cast<log::GetFilterRequest::Fields>(decoder.FieldNumber()) !=
- log::GetFilterRequest::Fields::FILTER_ID) {
+ if (static_cast<GetFilterRequest::Fields>(decoder.FieldNumber()) !=
+ GetFilterRequest::Fields::kFilterId) {
return StatusWithSize::InvalidArgument();
}
ConstByteSpan filter_id;
@@ -60,15 +64,16 @@ StatusWithSize FilterService::GetFilterImpl(ConstByteSpan request,
return StatusWithSize::NotFound();
}
- log::Filter::MemoryEncoder encoder(response);
+ log::pwpb::Filter::MemoryEncoder encoder(response);
for (auto& rule : (*filter)->rules()) {
- log::FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
+ FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
rule_encoder.WriteLevelGreaterThanOrEqual(rule.level_greater_than_or_equal)
.IgnoreError();
rule_encoder.WriteModuleEquals(rule.module_equals).IgnoreError();
rule_encoder.WriteAnyFlagsSet(rule.any_flags_set).IgnoreError();
- rule_encoder.WriteAction(static_cast<log::FilterRule::Action>(rule.action))
+ rule_encoder.WriteAction(static_cast<FilterRule::Action>(rule.action))
.IgnoreError();
+ rule_encoder.WriteThreadEquals(rule.thread_equals).IgnoreError();
PW_TRY_WITH_SIZE(rule_encoder.status());
}
PW_TRY_WITH_SIZE(encoder.status());
@@ -77,7 +82,7 @@ StatusWithSize FilterService::GetFilterImpl(ConstByteSpan request,
}
StatusWithSize FilterService::ListFilterIdsImpl(ByteSpan response) {
- log::FilterIdListResponse::MemoryEncoder encoder(response);
+ log::pwpb::FilterIdListResponse::MemoryEncoder encoder(response);
for (auto& filter : filter_map_.filters()) {
PW_TRY_WITH_SIZE(encoder.WriteFilterId(filter.id()));
}
diff --git a/pw_log_rpc/log_filter_service_test.cc b/pw_log_rpc/log_filter_service_test.cc
index d0d9afc24..942d371e2 100644
--- a/pw_log_rpc/log_filter_service_test.cc
+++ b/pw_log_rpc/log_filter_service_test.cc
@@ -32,13 +32,16 @@
namespace pw::log_rpc {
namespace {
+namespace FilterRule = ::pw::log::pwpb::FilterRule;
+namespace GetFilterRequest = ::pw::log::pwpb::GetFilterRequest;
+namespace SetFilterRequest = ::pw::log::pwpb::SetFilterRequest;
+
class FilterServiceTest : public ::testing::Test {
public:
FilterServiceTest() : filter_map_(filters_) {}
protected:
- FilterMap filter_map_;
- static constexpr size_t kMaxFilterRules = 3;
+ static constexpr size_t kMaxFilterRules = 4;
std::array<Filter::Rule, kMaxFilterRules> rules1_;
std::array<Filter::Rule, kMaxFilterRules> rules2_;
std::array<Filter::Rule, kMaxFilterRules> rules3_;
@@ -54,6 +57,7 @@ class FilterServiceTest : public ::testing::Test {
Filter(filter_id2_, rules2_),
Filter(filter_id3_, rules3_),
};
+ FilterMap filter_map_;
};
TEST_F(FilterServiceTest, GetFilterIds) {
@@ -94,17 +98,19 @@ TEST_F(FilterServiceTest, GetFilterIds) {
}
Status EncodeFilterRule(const Filter::Rule& rule,
- log::FilterRule::StreamEncoder& encoder) {
+ FilterRule::StreamEncoder& encoder) {
PW_TRY(
encoder.WriteLevelGreaterThanOrEqual(rule.level_greater_than_or_equal));
PW_TRY(encoder.WriteModuleEquals(rule.module_equals));
PW_TRY(encoder.WriteAnyFlagsSet(rule.any_flags_set));
- return encoder.WriteAction(static_cast<log::FilterRule::Action>(rule.action));
+ PW_TRY(encoder.WriteThreadEquals(rule.thread_equals));
+ return encoder.WriteAction(static_cast<FilterRule::Action>(rule.action));
}
-Status EncodeFilter(const Filter& filter, log::Filter::StreamEncoder& encoder) {
+Status EncodeFilter(const Filter& filter,
+ log::pwpb::Filter::StreamEncoder& encoder) {
for (auto& rule : filter.rules()) {
- log::FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
+ FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
PW_TRY(EncodeFilterRule(rule, rule_encoder));
}
return OkStatus();
@@ -116,11 +122,10 @@ Result<ConstByteSpan> EncodeFilterRequest(const Filter& filter,
std::byte encode_buffer[256];
protobuf::StreamEncoder encoder(writer, encode_buffer);
PW_TRY(encoder.WriteBytes(
- static_cast<uint32_t>(log::SetFilterRequest::Fields::FILTER_ID),
- filter.id()));
+ static_cast<uint32_t>(SetFilterRequest::Fields::kFilterId), filter.id()));
{
- log::Filter::StreamEncoder filter_encoder = encoder.GetNestedEncoder(
- static_cast<uint32_t>(log::SetFilterRequest::Fields::FILTER));
+ log::pwpb::Filter::StreamEncoder filter_encoder = encoder.GetNestedEncoder(
+ static_cast<uint32_t>(SetFilterRequest::Fields::kFilter));
PW_TRY(EncodeFilter(filter, filter_encoder));
} // Let the StreamEncoder destructor finalize the data.
return ConstByteSpan(writer.data(), writer.bytes_written());
@@ -131,38 +136,52 @@ void VerifyRule(const Filter::Rule& rule, const Filter::Rule& expected_rule) {
expected_rule.level_greater_than_or_equal);
EXPECT_EQ(rule.module_equals, expected_rule.module_equals);
EXPECT_EQ(rule.any_flags_set, expected_rule.any_flags_set);
+ EXPECT_EQ(rule.thread_equals, expected_rule.thread_equals);
EXPECT_EQ(rule.action, expected_rule.action);
}
TEST_F(FilterServiceTest, SetFilterRules) {
- const std::array<Filter::Rule, 4> new_rules{{
+ const std::array<Filter::Rule, kMaxFilterRules> new_rules{{
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::DEBUG_LEVEL,
.any_flags_set = 0x0f,
.module_equals{std::byte(123)},
+ .thread_equals{std::byte('L'), std::byte('O'), std::byte('G')},
},
{
.action = Filter::Rule::Action::kInactive,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0xef,
.module_equals{},
+ .thread_equals{},
},
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = 0x1234,
.module_equals{std::byte(99)},
+ .thread_equals{std::byte('P'),
+ std::byte('O'),
+ std::byte('W'),
+ std::byte('E'),
+ std::byte('R')},
},
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0,
.module_equals{std::byte(4)},
+ .thread_equals{std::byte('P'),
+ std::byte('O'),
+ std::byte('W'),
+ std::byte('E'),
+ std::byte('R')},
},
}};
- const Filter new_filter(filters_[0].id(),
- const_cast<std::array<Filter::Rule, 4>&>(new_rules));
+ const Filter new_filter(
+ filters_[0].id(),
+ const_cast<std::array<Filter::Rule, kMaxFilterRules>&>(new_rules));
std::byte request_buffer[512];
const auto request = EncodeFilterRequest(new_filter, request_buffer);
@@ -180,35 +199,52 @@ TEST_F(FilterServiceTest, SetFilterRules) {
}
TEST_F(FilterServiceTest, SetFilterRulesWhenUsedByDrain) {
- const std::array<Filter::Rule, 4> new_filter_rules{{
+ const std::array<Filter::Rule, kMaxFilterRules> new_filter_rules{{
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::CRITICAL_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::CRITICAL_LEVEL,
.any_flags_set = 0xfd,
.module_equals{std::byte(543)},
+ .thread_equals{std::byte('M'),
+ std::byte('0'),
+ std::byte('L'),
+ std::byte('O'),
+ std::byte('G')},
},
{
.action = Filter::Rule::Action::kInactive,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0xca,
.module_equals{},
+ .thread_equals{},
},
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = 0xabcd,
.module_equals{std::byte(9000)},
+ .thread_equals{std::byte('P'),
+ std::byte('O'),
+ std::byte('W'),
+ std::byte('E'),
+ std::byte('R')},
},
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0,
.module_equals{std::byte(123)},
+ .thread_equals{std::byte('P'),
+ std::byte('O'),
+ std::byte('W'),
+ std::byte('E'),
+ std::byte('R')},
},
}};
Filter& filter = filters_[0];
const Filter new_filter(
- filter.id(), const_cast<std::array<Filter::Rule, 4>&>(new_filter_rules));
+ filter.id(),
+ const_cast<std::array<Filter::Rule, kMaxFilterRules>&>(new_filter_rules));
std::byte request_buffer[256];
const auto request = EncodeFilterRequest(new_filter, request_buffer);
@@ -236,35 +272,40 @@ TEST_F(FilterServiceTest, SetFilterRulesWhenUsedByDrain) {
}
// A new request for logs with a new filter updates filter.
- const std::array<Filter::Rule, 4> second_filter_rules{{
+ const std::array<Filter::Rule, kMaxFilterRules> second_filter_rules{{
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::DEBUG_LEVEL,
.any_flags_set = 0xab,
.module_equals{},
+ .thread_equals{},
},
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0x11,
.module_equals{std::byte(34)},
+ .thread_equals{std::byte('L'), std::byte('O'), std::byte('G')},
},
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0xef,
.module_equals{std::byte(23)},
+ .thread_equals{std::byte('R'), std::byte('P'), std::byte('C')},
},
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0x0f,
.module_equals{},
+ .thread_equals{std::byte('R'), std::byte('P'), std::byte('C')},
},
}};
const Filter second_filter(
filter.id(),
- const_cast<std::array<Filter::Rule, 4>&>(second_filter_rules));
+ const_cast<std::array<Filter::Rule, kMaxFilterRules>&>(
+ second_filter_rules));
std::memset(request_buffer, 0, sizeof(request_buffer));
const auto second_filter_request =
@@ -284,8 +325,10 @@ TEST_F(FilterServiceTest, SetFilterRulesWhenUsedByDrain) {
void VerifyFilterRule(protobuf::Decoder& decoder,
const Filter::Rule& expected_rule) {
ASSERT_TRUE(decoder.Next().ok());
- ASSERT_EQ(decoder.FieldNumber(), 1u); // level_greater_than_or_equal
- log::FilterRule::Level level_greater_than_or_equal;
+ ASSERT_EQ(
+ decoder.FieldNumber(),
+ static_cast<uint32_t>(FilterRule::Fields::kLevelGreaterThanOrEqual));
+ FilterRule::Level level_greater_than_or_equal;
ASSERT_EQ(decoder.ReadUint32(
reinterpret_cast<uint32_t*>(&level_greater_than_or_equal)),
OkStatus());
@@ -293,7 +336,8 @@ void VerifyFilterRule(protobuf::Decoder& decoder,
expected_rule.level_greater_than_or_equal);
ASSERT_TRUE(decoder.Next().ok());
- ASSERT_EQ(decoder.FieldNumber(), 2u); // module_equals
+ ASSERT_EQ(decoder.FieldNumber(),
+ static_cast<uint32_t>(FilterRule::Fields::kModuleEquals));
ConstByteSpan module_equals;
ASSERT_EQ(decoder.ReadBytes(&module_equals), OkStatus());
ASSERT_EQ(module_equals.size(), expected_rule.module_equals.size());
@@ -303,21 +347,34 @@ void VerifyFilterRule(protobuf::Decoder& decoder,
0);
ASSERT_TRUE(decoder.Next().ok());
- ASSERT_EQ(decoder.FieldNumber(), 3u); // any_flags_set
+ ASSERT_EQ(decoder.FieldNumber(),
+ static_cast<uint32_t>(FilterRule::Fields::kAnyFlagsSet));
uint32_t any_flags_set;
ASSERT_EQ(decoder.ReadUint32(&any_flags_set), OkStatus());
EXPECT_EQ(any_flags_set, expected_rule.any_flags_set);
ASSERT_TRUE(decoder.Next().ok());
- ASSERT_EQ(decoder.FieldNumber(), 4u); // action
+ ASSERT_EQ(decoder.FieldNumber(),
+ static_cast<uint32_t>(FilterRule::Fields::kAction));
Filter::Rule::Action action;
ASSERT_EQ(decoder.ReadUint32(reinterpret_cast<uint32_t*>(&action)),
OkStatus());
EXPECT_EQ(action, expected_rule.action);
+
+ ASSERT_TRUE(decoder.Next().ok());
+ ASSERT_EQ(decoder.FieldNumber(),
+ static_cast<uint32_t>(FilterRule::Fields::kThreadEquals));
+ ConstByteSpan thread;
+ ASSERT_EQ(decoder.ReadBytes(&thread), OkStatus());
+ ASSERT_EQ(thread.size(), expected_rule.thread_equals.size());
+ EXPECT_EQ(
+ std::memcmp(
+ thread.data(), expected_rule.thread_equals.data(), thread.size()),
+ 0);
}
void VerifyFilterRules(protobuf::Decoder& decoder,
- std::span<const Filter::Rule> expected_rules) {
+ span<const Filter::Rule> expected_rules) {
size_t rules_found = 0;
while (decoder.Next().ok()) {
ConstByteSpan rule;
@@ -337,7 +394,7 @@ TEST_F(FilterServiceTest, GetFilterRules) {
context(filter_map_);
std::byte request_buffer[64];
- log::GetFilterRequest::MemoryEncoder encoder(request_buffer);
+ GetFilterRequest::MemoryEncoder encoder(request_buffer);
ASSERT_EQ(OkStatus(), encoder.WriteFilterId(filter_id1_));
const auto request = ConstByteSpan(encoder);
context.call(request);
@@ -351,12 +408,15 @@ TEST_F(FilterServiceTest, GetFilterRules) {
// Partially populate rules.
rules1_[0].action = Filter::Rule::Action::kKeep;
- rules1_[0].level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL;
+ rules1_[0].level_greater_than_or_equal = FilterRule::Level::DEBUG_LEVEL;
rules1_[0].any_flags_set = 0xab;
const std::array<std::byte, 2> module1{std::byte(123), std::byte(0xab)};
rules1_[0].module_equals.assign(module1.begin(), module1.end());
+ const std::array<std::byte, 4> thread1{
+ std::byte('H'), std::byte('O'), std::byte('S'), std::byte('T')};
+ rules1_[0].thread_equals.assign(thread1.begin(), thread1.end());
rules1_[1].action = Filter::Rule::Action::kDrop;
- rules1_[1].level_greater_than_or_equal = log::FilterRule::Level::ERROR_LEVEL;
+ rules1_[1].level_greater_than_or_equal = FilterRule::Level::ERROR_LEVEL;
rules1_[1].any_flags_set = 0;
PW_RAW_TEST_METHOD_CONTEXT(FilterService, GetFilter, 1)
@@ -369,10 +429,13 @@ TEST_F(FilterServiceTest, GetFilterRules) {
// Modify the rest of the filter rules.
rules1_[2].action = Filter::Rule::Action::kKeep;
- rules1_[2].level_greater_than_or_equal = log::FilterRule::Level::FATAL_LEVEL;
+ rules1_[2].level_greater_than_or_equal = FilterRule::Level::FATAL_LEVEL;
rules1_[2].any_flags_set = 0xcd;
const std::array<std::byte, 2> module2{std::byte(1), std::byte(2)};
rules1_[2].module_equals.assign(module2.begin(), module2.end());
+ const std::array<std::byte, 3> thread2{
+ std::byte('A'), std::byte('P'), std::byte('P')};
+ rules1_[2].thread_equals.assign(thread2.begin(), thread2.end());
rules1_[3].action = Filter::Rule::Action::kInactive;
PW_RAW_TEST_METHOD_CONTEXT(FilterService, GetFilter, 1)
diff --git a/pw_log_rpc/log_filter_test.cc b/pw_log_rpc/log_filter_test.cc
index be1fcdd20..fea7ce08f 100644
--- a/pw_log_rpc/log_filter_test.cc
+++ b/pw_log_rpc/log_filter_test.cc
@@ -33,37 +33,43 @@
namespace pw::log_rpc {
namespace {
+namespace FilterRule = ::pw::log::pwpb::FilterRule;
+
constexpr uint32_t kSampleModule = 0x1234;
constexpr uint32_t kSampleFlags = 0x3;
+const std::array<std::byte, cfg::kMaxThreadNameBytes - 7> kSampleThread = {
+ std::byte('R'), std::byte('P'), std::byte('C')};
constexpr char kSampleMessage[] = "message";
constexpr auto kSampleModuleLittleEndian =
- bytes::CopyInOrder<uint32_t>(std::endian::little, kSampleModule);
+ bytes::CopyInOrder<uint32_t>(endian::little, kSampleModule);
// Creates and encodes a log entry in the provided buffer.
template <uintptr_t log_level, uintptr_t module, uintptr_t flags>
Result<ConstByteSpan> EncodeLogEntry(std::string_view message,
- ByteSpan buffer) {
+ ByteSpan buffer,
+ ConstByteSpan thread) {
auto metadata = log_tokenized::Metadata::Set<log_level, module, flags, 0>();
return log::EncodeTokenizedLog(metadata,
- std::as_bytes(std::span(message)),
+ as_bytes(span<const char>(message)),
/*ticks_since_epoch=*/0,
- /*thread_name=*/{},
+ thread,
buffer);
}
Status EncodeFilterRule(const Filter::Rule& rule,
- log::FilterRule::StreamEncoder& encoder) {
+ FilterRule::StreamEncoder& encoder) {
PW_TRY(
encoder.WriteLevelGreaterThanOrEqual(rule.level_greater_than_or_equal));
PW_TRY(encoder.WriteModuleEquals(rule.module_equals));
PW_TRY(encoder.WriteAnyFlagsSet(rule.any_flags_set));
- return encoder.WriteAction(static_cast<log::FilterRule::Action>(rule.action));
+ PW_TRY(encoder.WriteThreadEquals(rule.thread_equals));
+ return encoder.WriteAction(static_cast<FilterRule::Action>(rule.action));
}
Result<ConstByteSpan> EncodeFilter(const Filter& filter, ByteSpan buffer) {
- log::Filter::MemoryEncoder encoder(buffer);
+ log::pwpb::Filter::MemoryEncoder encoder(buffer);
for (auto& rule : filter.rules()) {
- log::FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
+ FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
PW_TRY(EncodeFilterRule(rule, rule_encoder));
}
return ConstByteSpan(encoder);
@@ -74,6 +80,7 @@ void VerifyRule(const Filter::Rule& rule, const Filter::Rule& expected_rule) {
expected_rule.level_greater_than_or_equal);
EXPECT_EQ(rule.module_equals, expected_rule.module_equals);
EXPECT_EQ(rule.any_flags_set, expected_rule.any_flags_set);
+ EXPECT_EQ(rule.thread_equals, expected_rule.thread_equals);
EXPECT_EQ(rule.action, expected_rule.action);
}
@@ -93,7 +100,7 @@ TEST(FilterMap, RetrieveFiltersById) {
FilterMap filter_map(filters);
// Check that each filters() element points to the same object provided.
- std::span<const Filter> filter_list = filter_map.filters();
+ span<const Filter> filter_list = filter_map.filters();
ASSERT_EQ(filter_list.size(), filters.size());
size_t i = 0;
for (auto& filter : filter_list) {
@@ -125,27 +132,35 @@ TEST(Filter, UpdateFilterRules) {
const std::array<Filter::Rule, 4> new_rules{{
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::DEBUG_LEVEL,
.any_flags_set = 0x0f,
.module_equals{std::byte(123)},
+ .thread_equals{},
},
{
.action = Filter::Rule::Action::kInactive,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0xef,
.module_equals{},
+ .thread_equals{std::byte('L'), std::byte('O'), std::byte('G')},
},
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = 0x1234,
.module_equals{std::byte(99)},
+ .thread_equals{},
},
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0,
.module_equals{std::byte(4)},
+ .thread_equals{std::byte('P'),
+ std::byte('O'),
+ std::byte('W'),
+ std::byte('E'),
+ std::byte('R')},
},
}};
@@ -170,9 +185,10 @@ TEST(Filter, UpdateFilterRules) {
EXPECT_EQ(filter.UpdateRulesFromProto(encode_result.value()), OkStatus());
const Filter::Rule empty_rule{
.action = Filter::Rule::Action::kInactive,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0,
.module_equals{},
+ .thread_equals{},
};
for (const auto& rule : filter.rules()) {
VerifyRule(rule, empty_rule);
@@ -183,15 +199,21 @@ TEST(Filter, UpdateFilterRules) {
const std::array<Filter::Rule, 2> few_rules{{
{
.action = Filter::Rule::Action::kInactive,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0xef,
.module_equals{},
+ .thread_equals{},
},
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = 0x1234,
.module_equals{std::byte(99)},
+ .thread_equals{std::byte('P'),
+ std::byte('O'),
+ std::byte('W'),
+ std::byte('E'),
+ std::byte('R')},
},
}};
const Filter filter_few_rules(
@@ -213,39 +235,53 @@ TEST(Filter, UpdateFilterRules) {
const std::array<Filter::Rule, 6> extra_rules{{
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::DEBUG_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::DEBUG_LEVEL,
.any_flags_set = 0x0f,
.module_equals{std::byte(123)},
+ .thread_equals{std::byte('P'),
+ std::byte('O'),
+ std::byte('W'),
+ std::byte('E'),
+ std::byte('R')},
},
{
.action = Filter::Rule::Action::kInactive,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0xef,
.module_equals{},
+ .thread_equals{},
},
{
.action = Filter::Rule::Action::kInactive,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0xef,
.module_equals{},
+ .thread_equals{},
},
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = 0x1234,
.module_equals{std::byte(99)},
+ .thread_equals{std::byte('L'), std::byte('O'), std::byte('G')},
},
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0,
.module_equals{std::byte(4)},
+ .thread_equals{std::byte('L'), std::byte('O'), std::byte('G')},
},
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = 0x1234,
- .module_equals{std::byte(99)},
+ .module_equals{std::byte('M'),
+ std::byte('0'),
+ std::byte('L'),
+ std::byte('O'),
+ std::byte('G')},
+ .thread_equals{},
},
}};
const Filter filter_extra_rules(
@@ -269,17 +305,19 @@ TEST(FilterTest, FilterLogsRuleDefaultDrop) {
const std::array<Filter::Rule, 2> rules{{
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = kSampleFlags,
.module_equals{kSampleModuleLittleEndian.begin(),
kSampleModuleLittleEndian.end()},
+ .thread_equals{kSampleThread.begin(), kSampleThread.end()},
},
// This rule catches all logs.
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
.any_flags_set = 0,
.module_equals = {},
+ .thread_equals{},
},
}};
const std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id{
@@ -290,34 +328,34 @@ TEST(FilterTest, FilterLogsRuleDefaultDrop) {
std::array<std::byte, 50> buffer;
const Result<ConstByteSpan> log_entry_info =
EncodeLogEntry<PW_LOG_LEVEL_INFO, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_info.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_info.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_debug =
EncodeLogEntry<PW_LOG_LEVEL_DEBUG, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_debug.status(), OkStatus());
EXPECT_TRUE(filter.ShouldDropLog(log_entry_debug.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_warn =
EncodeLogEntry<PW_LOG_LEVEL_WARN, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_warn.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_warn.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_error =
EncodeLogEntry<PW_LOG_LEVEL_ERROR, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_error.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_error.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_info_different =
- EncodeLogEntry<PW_LOG_LEVEL_INFO, 0, 0>(kSampleMessage, buffer);
+ EncodeLogEntry<PW_LOG_LEVEL_INFO, 0, 0>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_info_different.status(), OkStatus());
EXPECT_TRUE(filter.ShouldDropLog(log_entry_info_different.value()));
// Because the last rule catches all logs, the filter default action is not
@@ -329,15 +367,21 @@ TEST(FilterTest, FilterLogsRuleDefaultDrop) {
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_same_flags =
- EncodeLogEntry<0, 0, kSampleFlags>(kSampleMessage, buffer);
+ EncodeLogEntry<0, 0, kSampleFlags>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_same_flags.status(), OkStatus());
EXPECT_TRUE(filter.ShouldDropLog(log_entry_same_flags.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_same_module =
- EncodeLogEntry<0, kSampleModule, 0>(kSampleMessage, buffer);
+ EncodeLogEntry<0, kSampleModule, 0>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_same_module.status(), OkStatus());
EXPECT_TRUE(filter.ShouldDropLog(log_entry_same_module.value()));
+
+ buffer.fill(std::byte(0));
+ const Result<ConstByteSpan> log_entry_same_thread =
+ EncodeLogEntry<0, 0, 0>(kSampleMessage, buffer, kSampleThread);
+ ASSERT_EQ(log_entry_same_thread.status(), OkStatus());
+ EXPECT_TRUE(filter.ShouldDropLog(log_entry_same_thread.value()));
}
TEST(FilterTest, FilterLogsKeepLogsWhenNoRuleMatches) {
@@ -345,10 +389,11 @@ TEST(FilterTest, FilterLogsKeepLogsWhenNoRuleMatches) {
const std::array<Filter::Rule, 1> rules{{
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = kSampleFlags,
.module_equals = {kSampleModuleLittleEndian.begin(),
kSampleModuleLittleEndian.end()},
+ .thread_equals = {kSampleThread.begin(), kSampleThread.end()},
},
}};
@@ -362,48 +407,54 @@ TEST(FilterTest, FilterLogsKeepLogsWhenNoRuleMatches) {
std::array<std::byte, 50> buffer;
const Result<ConstByteSpan> log_entry_info =
EncodeLogEntry<PW_LOG_LEVEL_INFO, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_info.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_info.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_debug =
EncodeLogEntry<PW_LOG_LEVEL_DEBUG, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_debug.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_debug.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_warn =
EncodeLogEntry<PW_LOG_LEVEL_WARN, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_warn.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_warn.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_error =
EncodeLogEntry<PW_LOG_LEVEL_ERROR, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_error.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_error.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_info_different =
- EncodeLogEntry<PW_LOG_LEVEL_INFO, 0, 0>(kSampleMessage, buffer);
+ EncodeLogEntry<PW_LOG_LEVEL_INFO, 0, 0>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_info_different.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_info_different.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_same_flags =
- EncodeLogEntry<0, 0, kSampleFlags>(kSampleMessage, buffer);
+ EncodeLogEntry<0, 0, kSampleFlags>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_same_flags.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_same_flags.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_same_module =
- EncodeLogEntry<0, kSampleModule, 0>(kSampleMessage, buffer);
+ EncodeLogEntry<0, kSampleModule, 0>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_same_module.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_same_module.value()));
+
+ buffer.fill(std::byte(0));
+ const Result<ConstByteSpan> log_entry_same_thread =
+ EncodeLogEntry<0, 0, 0>(kSampleMessage, buffer, kSampleThread);
+ ASSERT_EQ(log_entry_same_thread.status(), OkStatus());
+ EXPECT_FALSE(filter.ShouldDropLog(log_entry_same_thread.value()));
}
TEST(FilterTest, FilterLogsKeepLogsWhenRulesEmpty) {
@@ -416,81 +467,91 @@ TEST(FilterTest, FilterLogsKeepLogsWhenRulesEmpty) {
std::array<std::byte, 50> buffer;
const Result<ConstByteSpan> log_entry_info =
EncodeLogEntry<PW_LOG_LEVEL_INFO, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_info.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_info.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_debug =
EncodeLogEntry<PW_LOG_LEVEL_DEBUG, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_debug.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_debug.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_warn =
EncodeLogEntry<PW_LOG_LEVEL_WARN, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_warn.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_warn.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_error =
EncodeLogEntry<PW_LOG_LEVEL_ERROR, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_error.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_error.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_info_different =
- EncodeLogEntry<PW_LOG_LEVEL_INFO, 0, 0>(kSampleMessage, buffer);
+ EncodeLogEntry<PW_LOG_LEVEL_INFO, 0, 0>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_info_different.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_info_different.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_same_flags =
- EncodeLogEntry<0, 0, kSampleFlags>(kSampleMessage, buffer);
+ EncodeLogEntry<0, 0, kSampleFlags>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_same_flags.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_same_flags.value()));
buffer.fill(std::byte(0));
const Result<ConstByteSpan> log_entry_same_module =
- EncodeLogEntry<0, kSampleModule, 0>(kSampleMessage, buffer);
+ EncodeLogEntry<0, kSampleModule, 0>(kSampleMessage, buffer, {});
ASSERT_EQ(log_entry_same_module.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_same_module.value()));
+
+ buffer.fill(std::byte(0));
+ const Result<ConstByteSpan> log_entry_same_thread =
+ EncodeLogEntry<0, 0, 0>(kSampleMessage, buffer, kSampleThread);
+ ASSERT_EQ(log_entry_same_thread.status(), OkStatus());
+ EXPECT_FALSE(filter.ShouldDropLog(log_entry_same_thread.value()));
}
TEST(FilterTest, FilterLogsFirstRuleWins) {
const std::array<Filter::Rule, 2> rules{{
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = kSampleFlags,
.module_equals = {kSampleModuleLittleEndian.begin(),
kSampleModuleLittleEndian.end()},
+ .thread_equals = {kSampleThread.begin(), kSampleThread.end()},
},
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = kSampleFlags,
.module_equals = {kSampleModuleLittleEndian.begin(),
kSampleModuleLittleEndian.end()},
+ .thread_equals = {kSampleThread.begin(), kSampleThread.end()},
},
}};
const std::array<Filter::Rule, 2> rules_reversed{{
{
.action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = kSampleFlags,
.module_equals = {kSampleModuleLittleEndian.begin(),
kSampleModuleLittleEndian.end()},
+ .thread_equals = {kSampleThread.begin(), kSampleThread.end()},
},
{
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = kSampleFlags,
.module_equals = {kSampleModuleLittleEndian.begin(),
kSampleModuleLittleEndian.end()},
+ .thread_equals = {kSampleThread.begin(), kSampleThread.end()},
},
}};
const std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id1{
@@ -505,11 +566,157 @@ TEST(FilterTest, FilterLogsFirstRuleWins) {
std::array<std::byte, 50> buffer;
const Result<ConstByteSpan> log_entry_info =
EncodeLogEntry<PW_LOG_LEVEL_INFO, kSampleModule, kSampleFlags>(
- kSampleMessage, buffer);
+ kSampleMessage, buffer, kSampleThread);
ASSERT_EQ(log_entry_info.status(), OkStatus());
EXPECT_FALSE(filter.ShouldDropLog(log_entry_info.value()));
EXPECT_TRUE(filter_reverse_rules.ShouldDropLog(log_entry_info.value()));
}
+TEST(FilterTest, DropFilterRuleDueToThreadName) {
+ const std::array<std::byte, cfg::kMaxThreadNameBytes - 7> kDropThread = {
+ std::byte('L'), std::byte('O'), std::byte('G')};
+ const std::array<Filter::Rule, 2> rules{{
+ {
+ .action = Filter::Rule::Action::kKeep,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
+ .any_flags_set = kSampleFlags,
+ .module_equals = {kSampleModuleLittleEndian.begin(),
+ kSampleModuleLittleEndian.end()},
+ .thread_equals = {kDropThread.begin(), kDropThread.end()},
+ },
+ {
+ .action = Filter::Rule::Action::kDrop,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
+ .any_flags_set = kSampleFlags,
+ .module_equals = {kSampleModuleLittleEndian.begin(),
+ kSampleModuleLittleEndian.end()},
+ .thread_equals = {kSampleThread.begin(), kSampleThread.end()},
+ },
+ }};
+
+ const std::array<Filter::Rule, 2> drop_rule{{
+ {
+ .action = Filter::Rule::Action::kDrop,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
+ .any_flags_set = kSampleFlags,
+ .module_equals = {kSampleModuleLittleEndian.begin(),
+ kSampleModuleLittleEndian.end()},
+ .thread_equals = {kDropThread.begin(), kDropThread.end()},
+ },
+ }};
+
+ const std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id1{
+ std::byte(0xba), std::byte(0x1d), std::byte(0xba), std::byte(0xb1)};
+ // A filter's thread_equals name that does and does not match the log's thread
+ // name.
+ const Filter filter(filter_id1,
+ const_cast<std::array<Filter::Rule, 2>&>(rules));
+ const std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id2{
+ std::byte(0), std::byte(0), std::byte(0), std::byte(2)};
+ // A filter's thread_equals name that does not match the log's thread name.
+ const Filter filter_with_unregistered_filter_rule(
+ filter_id2, const_cast<std::array<Filter::Rule, 2>&>(drop_rule));
+ std::array<std::byte, 50> buffer;
+ const Result<ConstByteSpan> log_entry_thread =
+ EncodeLogEntry<PW_LOG_LEVEL_INFO, kSampleModule, kSampleFlags>(
+ kSampleMessage, buffer, kSampleThread);
+ ASSERT_EQ(log_entry_thread.status(), OkStatus());
+ // Set filter rules to kDrop to showcase the output difference.
+ // Drop_rule not being dropped, while rules is dropped successfully.
+ EXPECT_TRUE(filter.ShouldDropLog(log_entry_thread.value()));
+ EXPECT_FALSE(filter_with_unregistered_filter_rule.ShouldDropLog(
+ log_entry_thread.value()));
+}
+
+TEST(FilterTest, UpdateFilterWithLargeThreadNamePasses) {
+ // Threads are limited to a size of kMaxThreadNameBytes.
+ // However, the excess bytes will not be in the updated rules.
+ const std::array<std::byte, cfg::kMaxThreadNameBytes + 1>
+ kThreadNameLongerThanAllowed = {
+ std::byte('L'),
+ std::byte('O'),
+ std::byte('C'),
+ std::byte('A'),
+ std::byte('L'),
+ std::byte('E'),
+ std::byte('G'),
+ std::byte('R'),
+ std::byte('E'),
+ std::byte('S'),
+ std::byte('S'),
+ };
+
+ const std::array<Filter::Rule, 2> rule{{
+ {
+ .action = Filter::Rule::Action::kKeep,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
+ .any_flags_set = kSampleFlags,
+ .module_equals = {kSampleModuleLittleEndian.begin(),
+ kSampleModuleLittleEndian.end()},
+ .thread_equals = {kThreadNameLongerThanAllowed.begin(),
+ kThreadNameLongerThanAllowed.end()},
+ },
+ {
+ .action = Filter::Rule::Action::kKeep,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
+ .any_flags_set = kSampleFlags,
+ .module_equals = {kSampleModuleLittleEndian.begin(),
+ kSampleModuleLittleEndian.end()},
+ .thread_equals = {kSampleThread.begin(), kSampleThread.end()},
+ },
+ }};
+
+ const std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id{
+ std::byte(0xba), std::byte(0x1d), std::byte(0xba), std::byte(0xb1)};
+ Filter filter(filter_id, const_cast<std::array<Filter::Rule, 2>&>(rule));
+ std::byte buffer[256];
+ auto encode_result = EncodeFilter(filter, buffer);
+ ASSERT_EQ(encode_result.status(), OkStatus());
+ EXPECT_EQ(filter.UpdateRulesFromProto(encode_result.value()), OkStatus());
+ size_t i = 0;
+ for (const auto& rules : filter.rules()) {
+ VerifyRule(rules, rule[i++]);
+ }
+}
+
+TEST(FilterTest, UpdateFilterWithLargeThreadNameFails) {
+ const std::array<Filter::Rule, 1> rule_with_more_than_ten_bytes{{{
+ .action = Filter::Rule::Action::kKeep,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
+ .any_flags_set = kSampleFlags,
+ .module_equals = {kSampleModuleLittleEndian.begin(),
+ kSampleModuleLittleEndian.end()},
+ .thread_equals = {kSampleThread.begin(), kSampleThread.end()},
+ }}};
+ const std::array<std::byte, cfg::kMaxFilterIdBytes> filter_id{
+ std::byte(0xba), std::byte(0x1d), std::byte(0xba), std::byte(0xb1)};
+ Filter filter(
+ filter_id,
+ const_cast<std::array<Filter::Rule, 1>&>(rule_with_more_than_ten_bytes));
+ std::byte buffer[256];
+ log::pwpb::Filter::MemoryEncoder encoder(buffer);
+ {
+ std::array<const std::byte, cfg::kMaxThreadNameBytes + 1>
+ kThreadNameLongerThanAllowed = {
+ std::byte('L'),
+ std::byte('O'),
+ std::byte('C'),
+ std::byte('A'),
+ std::byte('L'),
+ std::byte('E'),
+ std::byte('G'),
+ std::byte('R'),
+ std::byte('E'),
+ std::byte('S'),
+ std::byte('S'),
+ };
+ // Stream encoder writes to the buffer when it goes out of scope.
+ FilterRule::StreamEncoder rule_encoder = encoder.GetRuleEncoder();
+ ASSERT_EQ(rule_encoder.WriteThreadEquals(kThreadNameLongerThanAllowed),
+ OkStatus());
+ }
+ EXPECT_EQ(filter.UpdateRulesFromProto(ConstByteSpan(encoder)),
+ Status::InvalidArgument());
+}
} // namespace
} // namespace pw::log_rpc
diff --git a/pw_log_rpc/log_service_test.cc b/pw_log_rpc/log_service_test.cc
index 1072ec55b..e201e2382 100644
--- a/pw_log_rpc/log_service_test.cc
+++ b/pw_log_rpc/log_service_test.cc
@@ -41,6 +41,8 @@ namespace {
using log::pw_rpc::raw::Logs;
+namespace FilterRule = log::pwpb::FilterRule;
+
#define LOG_SERVICE_METHOD_CONTEXT \
PW_RAW_TEST_METHOD_CONTEXT(LogService, Listen, 10)
@@ -60,6 +62,11 @@ static_assert(sizeof(kLongMessage) < kMaxMessageSize);
static_assert(sizeof(kLongMessage) + RpcLogDrain::kMinEntrySizeWithoutPayload >
RpcLogDrain::kMinEntryBufferSize);
std::array<std::byte, 1> rpc_request_buffer;
+const std::array<std::byte, 5> kSampleThread = {std::byte('M'),
+ std::byte('0'),
+ std::byte('L'),
+ std::byte('O'),
+ std::byte('G')};
constexpr auto kSampleMetadata =
log_tokenized::Metadata::Set<PW_LOG_LEVEL_INFO, 123, 0x03, __LINE__>();
constexpr auto kDropMessageMetadata =
@@ -82,20 +89,22 @@ class LogServiceTest : public ::testing::Test {
void AddLogEntries(size_t log_count,
std::string_view message,
log_tokenized::Metadata metadata,
- int64_t timestamp) {
+ int64_t timestamp,
+ ConstByteSpan thread) {
for (size_t i = 0; i < log_count; ++i) {
- ASSERT_TRUE(AddLogEntry(message, metadata, timestamp).ok());
+ ASSERT_TRUE(AddLogEntry(message, metadata, timestamp, thread).ok());
}
}
StatusWithSize AddLogEntry(std::string_view message,
log_tokenized::Metadata metadata,
- int64_t timestamp) {
+ int64_t timestamp,
+ ConstByteSpan thread) {
Result<ConstByteSpan> encoded_log_result =
log::EncodeTokenizedLog(metadata,
- std::as_bytes(std::span(message)),
+ as_bytes(span<const char>(message)),
timestamp,
- /*thread_name=*/{},
+ thread,
entry_encode_buffer_);
PW_TRY_WITH_SIZE(encoded_log_result.status());
multisink_.HandleEntry(encoded_log_result.value());
@@ -107,7 +116,7 @@ class LogServiceTest : public ::testing::Test {
multisink::MultiSink multisink_;
RpcLogDrainMap drain_map_;
std::array<std::byte, kMaxLogEntrySize> entry_encode_buffer_;
- static constexpr size_t kMaxFilterRules = 3;
+ static constexpr size_t kMaxFilterRules = 4;
std::array<Filter::Rule, kMaxFilterRules> rules1_;
std::array<Filter::Rule, kMaxFilterRules> rules2_;
std::array<Filter::Rule, kMaxFilterRules> rules3_;
@@ -203,7 +212,7 @@ TEST_F(LogServiceTest, StartAndEndStream) {
// Add log entries.
const size_t total_entries = 10;
- AddLogEntries(total_entries, kMessage, kSampleMetadata, kSampleTimestamp);
+ AddLogEntries(total_entries, kMessage, kSampleMetadata, kSampleTimestamp, {});
// Request logs.
context.call(rpc_request_buffer);
@@ -221,10 +230,11 @@ TEST_F(LogServiceTest, StartAndEndStream) {
// Verify data in responses.
Vector<TestLogEntry, total_entries> expected_messages;
for (size_t i = 0; i < total_entries; ++i) {
- expected_messages.push_back({.metadata = kSampleMetadata,
- .timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(
- std::span(std::string_view(kMessage)))});
+ expected_messages.push_back(
+ {.metadata = kSampleMetadata,
+ .timestamp = kSampleTimestamp,
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = {}});
}
size_t entries_found = 0;
uint32_t drop_count_found = 0;
@@ -252,13 +262,17 @@ TEST_F(LogServiceTest, HandleDropped) {
const uint32_t total_drop_count = 2;
// Force a drop entry in between entries.
- AddLogEntries(
- entries_before_drop, kMessage, kSampleMetadata, kSampleTimestamp);
+ AddLogEntries(entries_before_drop,
+ kMessage,
+ kSampleMetadata,
+ kSampleTimestamp,
+ kSampleThread);
multisink_.HandleDropped(total_drop_count);
AddLogEntries(total_entries - entries_before_drop,
kMessage,
kSampleMetadata,
- kSampleTimestamp);
+ kSampleTimestamp,
+ kSampleThread);
// Request logs.
context.call(rpc_request_buffer);
@@ -271,21 +285,24 @@ TEST_F(LogServiceTest, HandleDropped) {
Vector<TestLogEntry, total_entries + 1> expected_messages;
size_t i = 0;
for (; i < entries_before_drop; ++i) {
- expected_messages.push_back({.metadata = kSampleMetadata,
- .timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(
- std::span(std::string_view(kMessage)))});
+ expected_messages.push_back(
+ {.metadata = kSampleMetadata,
+ .timestamp = kSampleTimestamp,
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread});
}
expected_messages.push_back(
{.metadata = kDropMessageMetadata,
.dropped = total_drop_count,
- .tokenized_data = std::as_bytes(
- std::span(std::string_view(RpcLogDrain::kIngressErrorMessage)))});
+ .tokenized_data =
+ as_bytes(span(std::string_view(RpcLogDrain::kIngressErrorMessage))),
+ .thread = {}});
for (; i < total_entries; ++i) {
- expected_messages.push_back({.metadata = kSampleMetadata,
- .timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(
- std::span(std::string_view(kMessage)))});
+ expected_messages.push_back(
+ {.metadata = kSampleMetadata,
+ .timestamp = kSampleTimestamp,
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread});
}
// Verify data in responses.
@@ -310,7 +327,7 @@ TEST_F(LogServiceTest, HandleDroppedBetweenFilteredOutLogs) {
context.set_channel_id(drain_channel_id);
// Set filter to drop INFO+ and keep DEBUG logs
rules1_[0].action = Filter::Rule::Action::kDrop;
- rules1_[0].level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL;
+ rules1_[0].level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL;
// Add log entries.
const size_t total_entries = 5;
@@ -320,14 +337,16 @@ TEST_F(LogServiceTest, HandleDroppedBetweenFilteredOutLogs) {
for (size_t i = 1; i < total_entries; ++i) {
ASSERT_EQ(
OkStatus(),
- AddLogEntry(kMessage, kSampleMetadata, kSampleTimestamp).status());
+ AddLogEntry(kMessage, kSampleMetadata, kSampleTimestamp, kSampleThread)
+ .status());
multisink_.HandleDropped(1);
}
// Add message that won't be filtered out.
constexpr auto metadata =
log_tokenized::Metadata::Set<PW_LOG_LEVEL_DEBUG, 0, 0, __LINE__>();
ASSERT_EQ(OkStatus(),
- AddLogEntry(kMessage, metadata, kSampleTimestamp).status());
+ AddLogEntry(kMessage, metadata, kSampleTimestamp, kSampleThread)
+ .status());
// Request logs.
context.call(rpc_request_buffer);
@@ -341,12 +360,14 @@ TEST_F(LogServiceTest, HandleDroppedBetweenFilteredOutLogs) {
expected_messages.push_back(
{.metadata = kDropMessageMetadata,
.dropped = total_drop_count,
- .tokenized_data = std::as_bytes(
- std::span(std::string_view(RpcLogDrain::kIngressErrorMessage)))});
+ .tokenized_data =
+ as_bytes(span(std::string_view(RpcLogDrain::kIngressErrorMessage))),
+ .thread = {}});
expected_messages.push_back(
{.metadata = metadata,
.timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(std::span(std::string_view(kMessage)))});
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread});
// Verify data in responses.
size_t entries_found = 0;
@@ -374,10 +395,15 @@ TEST_F(LogServiceTest, HandleSmallLogEntryBuffer) {
// one, since drop count messages are only sent when a log entry can be sent.
const size_t total_entries = 5;
const uint32_t total_drop_count = total_entries - 1;
- AddLogEntries(
- total_drop_count, kLongMessage, kSampleMetadata, kSampleTimestamp);
- EXPECT_EQ(OkStatus(),
- AddLogEntry(kMessage, kSampleMetadata, kSampleTimestamp).status());
+ AddLogEntries(total_drop_count,
+ kLongMessage,
+ kSampleMetadata,
+ kSampleTimestamp,
+ kSampleThread);
+ EXPECT_EQ(
+ OkStatus(),
+ AddLogEntry(kMessage, kSampleMetadata, kSampleTimestamp, kSampleThread)
+ .status());
// Request logs.
context.call(rpc_request_buffer);
@@ -390,12 +416,14 @@ TEST_F(LogServiceTest, HandleSmallLogEntryBuffer) {
expected_messages.push_back(
{.metadata = kDropMessageMetadata,
.dropped = total_drop_count,
- .tokenized_data = std::as_bytes(std::span(
- std::string_view(RpcLogDrain::kSmallStackBufferErrorMessage)))});
+ .tokenized_data = as_bytes(
+ span(std::string_view(RpcLogDrain::kSmallStackBufferErrorMessage))),
+ .thread = {}});
expected_messages.push_back(
{.metadata = kSampleMetadata,
.timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(std::span(std::string_view(kMessage)))});
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread});
// Expect one drop message with the total drop count, and the only message
// that fits the buffer.
@@ -421,7 +449,11 @@ TEST_F(LogServiceTest, FlushDrainWithoutMultisink) {
// Add log entries.
const size_t total_entries = 5;
- AddLogEntries(total_entries, kMessage, kSampleMetadata, kSampleTimestamp);
+ AddLogEntries(total_entries,
+ kMessage,
+ kSampleMetadata,
+ kSampleTimestamp,
+ kSampleThread);
// Request logs.
context.call(rpc_request_buffer);
EXPECT_EQ(detached_drain.Close(), OkStatus());
@@ -437,11 +469,12 @@ TEST_F(LogServiceTest, LargeLogEntry) {
(1 << PW_LOG_TOKENIZED_FLAG_BITS) - 1,
(1 << PW_LOG_TOKENIZED_LINE_BITS) - 1>(),
.timestamp = std::numeric_limits<int64_t>::max(),
- .tokenized_data = std::as_bytes(std::span(kMessage)),
+ .tokenized_data = as_bytes(span(kMessage)),
+ .thread = kSampleThread,
};
// Add entry to multisink.
- log::LogEntry::MemoryEncoder encoder(entry_encode_buffer_);
+ log::pwpb::LogEntry::MemoryEncoder encoder(entry_encode_buffer_);
ASSERT_EQ(encoder.WriteMessage(expected_entry.tokenized_data), OkStatus());
ASSERT_EQ(encoder.WriteLineLevel(
(expected_entry.metadata.level() & PW_LOG_LEVEL_BITMASK) |
@@ -450,11 +483,11 @@ TEST_F(LogServiceTest, LargeLogEntry) {
OkStatus());
ASSERT_EQ(encoder.WriteFlags(expected_entry.metadata.flags()), OkStatus());
ASSERT_EQ(encoder.WriteTimestamp(expected_entry.timestamp), OkStatus());
- const uint32_t little_endian_module = bytes::ConvertOrderTo(
- std::endian::little, expected_entry.metadata.module());
- ASSERT_EQ(
- encoder.WriteModule(std::as_bytes(std::span(&little_endian_module, 1))),
- OkStatus());
+ const uint32_t little_endian_module =
+ bytes::ConvertOrderTo(endian::little, expected_entry.metadata.module());
+ ASSERT_EQ(encoder.WriteModule(as_bytes(span(&little_endian_module, 1))),
+ OkStatus());
+ ASSERT_EQ(encoder.WriteThread(expected_entry.thread), OkStatus());
ASSERT_EQ(encoder.status(), OkStatus());
multisink_.HandleEntry(encoder);
@@ -489,11 +522,11 @@ TEST_F(LogServiceTest, InterruptedLogStreamSendsDropCount) {
const size_t max_packets = 10;
rpc::RawFakeChannelOutput<10, 512> output;
rpc::Channel channel(rpc::Channel::Create<drain_channel_id>(&output));
- rpc::Server server(std::span(&channel, 1));
+ rpc::Server server(span(&channel, 1));
// Add as many entries needed to have multiple packets send.
StatusWithSize status =
- AddLogEntry(kMessage, kSampleMetadata, kSampleTimestamp);
+ AddLogEntry(kMessage, kSampleMetadata, kSampleTimestamp, kSampleThread);
ASSERT_TRUE(status.ok());
const uint32_t max_messages_per_response =
@@ -504,7 +537,11 @@ TEST_F(LogServiceTest, InterruptedLogStreamSendsDropCount) {
const size_t max_entries = 50;
// Check we can test all these entries.
ASSERT_GE(max_entries, total_entries);
- AddLogEntries(total_entries - 1, kMessage, kSampleMetadata, kSampleTimestamp);
+ AddLogEntries(total_entries - 1,
+ kMessage,
+ kSampleMetadata,
+ kSampleTimestamp,
+ kSampleThread);
// Interrupt log stream with an error.
const uint32_t successful_packets_sent = packets_sent / 2;
@@ -524,10 +561,11 @@ TEST_F(LogServiceTest, InterruptedLogStreamSendsDropCount) {
// Verify data in responses.
Vector<TestLogEntry, max_entries> expected_messages;
for (size_t i = 0; i < total_entries; ++i) {
- expected_messages.push_back({.metadata = kSampleMetadata,
- .timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(
- std::span(std::string_view(kMessage)))});
+ expected_messages.push_back(
+ {.metadata = kSampleMetadata,
+ .timestamp = kSampleTimestamp,
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread});
}
size_t entries_found = 0;
uint32_t drop_count_found = 0;
@@ -560,16 +598,16 @@ TEST_F(LogServiceTest, InterruptedLogStreamSendsDropCount) {
expected_messages_after_reset.push_back(
{.metadata = kDropMessageMetadata,
.dropped = total_drop_count,
- .tokenized_data = std::as_bytes(
- std::span(std::string_view(RpcLogDrain::kWriterErrorMessage)))});
+ .tokenized_data =
+ as_bytes(span(std::string_view(RpcLogDrain::kWriterErrorMessage)))});
const uint32_t remaining_entries = total_entries - total_drop_count;
for (size_t i = 0; i < remaining_entries; ++i) {
expected_messages_after_reset.push_back(
{.metadata = kSampleMetadata,
.timestamp = kSampleTimestamp,
- .tokenized_data =
- std::as_bytes(std::span(std::string_view(kMessage)))});
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread});
}
size_t entries_found_after_reset = 0;
@@ -596,11 +634,11 @@ TEST_F(LogServiceTest, InterruptedLogStreamIgnoresErrors) {
const size_t max_packets = 20;
rpc::RawFakeChannelOutput<max_packets, 512> output;
rpc::Channel channel(rpc::Channel::Create<drain_channel_id>(&output));
- rpc::Server server(std::span(&channel, 1));
+ rpc::Server server(span(&channel, 1));
// Add as many entries needed to have multiple packets send.
StatusWithSize status =
- AddLogEntry(kMessage, kSampleMetadata, kSampleTimestamp);
+ AddLogEntry(kMessage, kSampleMetadata, kSampleTimestamp, kSampleThread);
ASSERT_TRUE(status.ok());
const uint32_t max_messages_per_response =
@@ -611,7 +649,11 @@ TEST_F(LogServiceTest, InterruptedLogStreamIgnoresErrors) {
const size_t max_entries = 50;
// Check we can test all these entries.
ASSERT_GT(max_entries, total_entries);
- AddLogEntries(total_entries - 1, kMessage, kSampleMetadata, kSampleTimestamp);
+ AddLogEntries(total_entries - 1,
+ kMessage,
+ kSampleMetadata,
+ kSampleTimestamp,
+ kSampleThread);
// Interrupt log stream with an error.
const uint32_t error_on_packet_count = packets_sent / 2;
@@ -640,10 +682,11 @@ TEST_F(LogServiceTest, InterruptedLogStreamIgnoresErrors) {
const uint32_t total_drop_count = total_entries - entries_found;
Vector<TestLogEntry, max_entries> expected_messages;
for (size_t i = 0; i < entries_found; ++i) {
- expected_messages.push_back({.metadata = kSampleMetadata,
- .timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(
- std::span(std::string_view(kMessage)))});
+ expected_messages.push_back(
+ {.metadata = kSampleMetadata,
+ .timestamp = kSampleTimestamp,
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread});
}
entries_found = 0;
@@ -687,37 +730,68 @@ TEST_F(LogServiceTest, FilterLogs) {
const uint32_t module = 0xcafe;
const uint32_t flags = 0x02;
const uint32_t line_number = 100;
+ const std::array<std::byte, 3> kNewThread = {
+ std::byte('A'), std::byte('P'), std::byte('P')};
+ const std::array<std::byte, 3> kInvalidThread = {
+ std::byte('C'), std::byte('D'), std::byte('C')};
const auto debug_metadata = log_tokenized::Metadata::
Set<PW_LOG_LEVEL_DEBUG, module, flags, line_number>();
- ASSERT_TRUE(AddLogEntry(kMessage, debug_metadata, kSampleTimestamp).ok());
+ ASSERT_TRUE(
+ AddLogEntry(kMessage, debug_metadata, kSampleTimestamp, kSampleThread)
+ .ok());
const auto info_metadata = log_tokenized::Metadata::
Set<PW_LOG_LEVEL_INFO, module, flags, line_number>();
- ASSERT_TRUE(AddLogEntry(kMessage, info_metadata, kSampleTimestamp).ok());
+ ASSERT_TRUE(
+ AddLogEntry(kMessage, info_metadata, kSampleTimestamp, kSampleThread)
+ .ok());
const auto warn_metadata = log_tokenized::Metadata::
Set<PW_LOG_LEVEL_WARN, module, flags, line_number>();
- ASSERT_TRUE(AddLogEntry(kMessage, warn_metadata, kSampleTimestamp).ok());
+ ASSERT_TRUE(
+ AddLogEntry(kMessage, warn_metadata, kSampleTimestamp, kSampleThread)
+ .ok());
const auto error_metadata = log_tokenized::Metadata::
Set<PW_LOG_LEVEL_ERROR, module, flags, line_number>();
- ASSERT_TRUE(AddLogEntry(kMessage, error_metadata, kSampleTimestamp).ok());
+ ASSERT_TRUE(
+ AddLogEntry(kMessage, error_metadata, kSampleTimestamp, kNewThread).ok());
const auto different_flags_metadata = log_tokenized::Metadata::
Set<PW_LOG_LEVEL_ERROR, module, 0x01, line_number>();
ASSERT_TRUE(
- AddLogEntry(kMessage, different_flags_metadata, kSampleTimestamp).ok());
+ AddLogEntry(
+ kMessage, different_flags_metadata, kSampleTimestamp, kSampleThread)
+ .ok());
const auto different_module_metadata = log_tokenized::Metadata::
Set<PW_LOG_LEVEL_ERROR, 0xabcd, flags, line_number>();
ASSERT_TRUE(
- AddLogEntry(kMessage, different_module_metadata, kSampleTimestamp).ok());
+ AddLogEntry(
+ kMessage, different_module_metadata, kSampleTimestamp, kSampleThread)
+ .ok());
+ const auto second_info_metadata = log_tokenized::Metadata::
+ Set<PW_LOG_LEVEL_INFO, module, flags, line_number>();
+ ASSERT_TRUE(
+ AddLogEntry(kMessage, second_info_metadata, kSampleTimestamp, kNewThread)
+ .ok());
+ const auto metadata = log_tokenized::Metadata::
+ Set<PW_LOG_LEVEL_INFO, module, flags, line_number>();
+ ASSERT_TRUE(
+ AddLogEntry(kMessage, metadata, kSampleTimestamp, kInvalidThread).ok());
- Vector<TestLogEntry, 3> expected_messages{
+ Vector<TestLogEntry, 4> expected_messages{
{.metadata = info_metadata,
.timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(std::span(std::string_view(kMessage)))},
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread},
{.metadata = warn_metadata,
.timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(std::span(std::string_view(kMessage)))},
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kSampleThread},
{.metadata = error_metadata,
.timestamp = kSampleTimestamp,
- .tokenized_data = std::as_bytes(std::span(std::string_view(kMessage)))},
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kNewThread},
+ {.metadata = second_info_metadata,
+ .timestamp = kSampleTimestamp,
+ .tokenized_data = as_bytes(span(std::string_view(kMessage))),
+ .thread = kNewThread},
};
// Set up filter rules for drain at drains_[1].
@@ -726,18 +800,30 @@ TEST_F(LogServiceTest, FilterLogs) {
rule = {};
}
const auto module_little_endian =
- bytes::CopyInOrder<uint32_t>(std::endian::little, module);
+ bytes::CopyInOrder<uint32_t>(endian::little, module);
rules2_[0] = {
.action = Filter::Rule::Action::kKeep,
- .level_greater_than_or_equal = log::FilterRule::Level::INFO_LEVEL,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
.any_flags_set = flags,
- .module_equals{module_little_endian.begin(), module_little_endian.end()}};
+ .module_equals{module_little_endian.begin(), module_little_endian.end()},
+ .thread_equals{kSampleThread.begin(), kSampleThread.end()}};
rules2_[1] = {
- .action = Filter::Rule::Action::kDrop,
- .level_greater_than_or_equal = log::FilterRule::Level::ANY_LEVEL,
- .any_flags_set = 0,
- .module_equals{},
- };
+ .action = Filter::Rule::Action::kKeep,
+ .level_greater_than_or_equal = FilterRule::Level::DEBUG_LEVEL,
+ .any_flags_set = flags,
+ .module_equals{module_little_endian.begin(), module_little_endian.end()},
+ .thread_equals{kNewThread.begin(), kNewThread.end()}};
+ rules2_[2] = {.action = Filter::Rule::Action::kDrop,
+ .level_greater_than_or_equal = FilterRule::Level::ANY_LEVEL,
+ .any_flags_set = 0,
+ .module_equals{},
+ .thread_equals{}};
+ rules2_[3] = {
+ .action = Filter::Rule::Action::kKeep,
+ .level_greater_than_or_equal = FilterRule::Level::INFO_LEVEL,
+ .any_flags_set = flags,
+ .module_equals{module_little_endian.begin(), module_little_endian.end()},
+ .thread_equals{kNewThread.begin(), kNewThread.end()}};
// Request logs.
LOG_SERVICE_METHOD_CONTEXT context(drain_map_);
@@ -755,7 +841,7 @@ TEST_F(LogServiceTest, FilterLogs) {
entries_found,
drop_count_found);
}
- EXPECT_EQ(entries_found, 3u);
+ EXPECT_EQ(entries_found, 4u);
EXPECT_EQ(drop_count_found, 0u);
}
@@ -767,7 +853,7 @@ TEST_F(LogServiceTest, ReopenClosedLogStreamWithAcquiredBuffer) {
LogService log_service(drain_map_);
rpc::RawFakeChannelOutput<10, 512> output;
rpc::Channel channel(rpc::Channel::Create<drain_channel_id>(&output));
- rpc::Server server(std::span(&channel, 1));
+ rpc::Server server(span(&channel, 1));
// Request logs.
rpc::RawServerWriter writer = rpc::RawServerWriter::Open<Logs::Listen>(
diff --git a/pw_log_rpc/public/pw_log_rpc/internal/config.h b/pw_log_rpc/public/pw_log_rpc/internal/config.h
index 9fbcfbfc7..1f378f526 100644
--- a/pw_log_rpc/public/pw_log_rpc/internal/config.h
+++ b/pw_log_rpc/public/pw_log_rpc/internal/config.h
@@ -25,6 +25,14 @@
#define PW_LOG_RPC_CONFIG_MAX_FILTER_RULE_MODULE_NAME_SIZE 4
#endif // PW_LOG_RPC_CONFIG_MAX_FILTER_RULE_MODULE_NAME_SIZE
+// Log filter threads are optionally tokenized,thus their backing on-device
+// container can have different sizes. A token may be represented by a 32-bit
+// integer, usually two. Default the max thread size to
+// 10 bytes.
+#ifndef PW_LOG_RPC_CONFIG_MAX_FILTER_RULE_THREAD_NAME_SIZE
+#define PW_LOG_RPC_CONFIG_MAX_FILTER_RULE_THREAD_NAME_SIZE 10
+#endif // PW_LOG_RPC_CONFIG_MAX_FILTER_RULE_THREAD_NAME_SIZE
+
// Log filter IDs are optionally tokenized, and thus their backing on-device
// container can have different sizes. A token may be represented by a 32-bit
// integer (though it is usually 2 bytes). Default the max module name size to
@@ -57,7 +65,7 @@
#define PW_LOG_RPC_SLOW_DRAIN_MSG "Slow drain"
#endif // PW_LOG_RPC_SLOW_DRAIN_MSG
-// Message for when a is too too large to fit in the outbound buffer, so it is
+// Message for when a log is too large to fit in the outbound buffer, so it is
// dropped.
#ifndef PW_LOG_RPC_SMALL_OUTBOUND_BUFFER_MSG
#define PW_LOG_RPC_SMALL_OUTBOUND_BUFFER_MSG "Outbound log buffer too small"
@@ -80,4 +88,7 @@ inline constexpr size_t kMaxModuleNameBytes =
inline constexpr size_t kMaxFilterIdBytes =
PW_LOG_RPC_CONFIG_MAX_FILTER_ID_SIZE;
+
+inline constexpr size_t kMaxThreadNameBytes =
+ PW_LOG_RPC_CONFIG_MAX_FILTER_RULE_THREAD_NAME_SIZE;
} // namespace pw::log_rpc::cfg
diff --git a/pw_log_rpc/public/pw_log_rpc/log_filter.h b/pw_log_rpc/public/pw_log_rpc/log_filter.h
index 56e01f75b..dc0a7daf2 100644
--- a/pw_log_rpc/public/pw_log_rpc/log_filter.h
+++ b/pw_log_rpc/public/pw_log_rpc/log_filter.h
@@ -16,13 +16,13 @@
#include <cstddef>
#include <cstring>
-#include <span>
#include "pw_assert/assert.h"
#include "pw_bytes/span.h"
#include "pw_containers/vector.h"
#include "pw_log/proto/log.pwpb.h"
#include "pw_log_rpc/internal/config.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::log_rpc {
@@ -42,17 +42,20 @@ class Filter {
// Checks if the log level is greater or equal to this value when it
// does not equal NOT_SET.
- log::FilterRule::Level level_greater_than_or_equal =
- log::FilterRule::Level::ANY_LEVEL;
+ log::pwpb::FilterRule::Level level_greater_than_or_equal =
+ log::pwpb::FilterRule::Level::ANY_LEVEL;
// Checks if the log entry has any flag is set when it doesn't equal 0.
uint32_t any_flags_set = 0;
// Checks if the log entry module equals this value when not empty.
Vector<std::byte, cfg::kMaxModuleNameBytes> module_equals{};
+
+ // Checks if the log entry thread equals this value when not empty.
+ Vector<std::byte, cfg::kMaxThreadNameBytes> thread_equals{};
};
- Filter(std::span<const std::byte> id, std::span<Rule> rules) : rules_(rules) {
+ Filter(span<const std::byte> id, span<Rule> rules) : rules_(rules) {
PW_ASSERT(!id.empty());
id_.assign(id.begin(), id.end());
}
@@ -62,7 +65,7 @@ class Filter {
Filter& operator=(const Filter&) = delete;
ConstByteSpan id() const { return ConstByteSpan(id_.data(), id_.size()); }
- std::span<const Rule> rules() const { return rules_; }
+ span<const Rule> rules() const { return rules_; }
// Verifies a log entry against the filter's rules in the order they were
// provided, stopping at the first rule that matches.
@@ -82,7 +85,7 @@ class Filter {
private:
Vector<std::byte, cfg::kMaxFilterIdBytes> id_;
- std::span<Rule> rules_;
+ span<Rule> rules_;
};
} // namespace pw::log_rpc
diff --git a/pw_log_rpc/public/pw_log_rpc/log_filter_map.h b/pw_log_rpc/public/pw_log_rpc/log_filter_map.h
index 5a1be6e6a..4ff70ebaf 100644
--- a/pw_log_rpc/public/pw_log_rpc/log_filter_map.h
+++ b/pw_log_rpc/public/pw_log_rpc/log_filter_map.h
@@ -15,18 +15,18 @@
#pragma once
#include <cstring>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_log_rpc/log_filter.h"
#include "pw_result/result.h"
+#include "pw_span/span.h"
namespace pw::log_rpc {
// Holds an inmutable Filter map, ordered by filter ID to facilitate set up.
class FilterMap {
public:
- explicit constexpr FilterMap(std::span<Filter> filters) : filters_(filters) {}
+ explicit constexpr FilterMap(span<Filter> filters) : filters_(filters) {}
// Not copyable nor movable.
FilterMap(FilterMap const&) = delete;
@@ -44,10 +44,10 @@ class FilterMap {
return Status::NotFound();
}
- const std::span<Filter>& filters() const { return filters_; }
+ const span<Filter>& filters() const { return filters_; }
protected:
- const std::span<Filter> filters_;
+ const span<Filter> filters_;
};
} // namespace pw::log_rpc
diff --git a/pw_log_rpc/public/pw_log_rpc/log_filter_service.h b/pw_log_rpc/public/pw_log_rpc/log_filter_service.h
index bb074afbd..3035141c6 100644
--- a/pw_log_rpc/public/pw_log_rpc/log_filter_service.h
+++ b/pw_log_rpc/public/pw_log_rpc/log_filter_service.h
@@ -37,14 +37,14 @@ class FilterService final
void GetFilter(ConstByteSpan request, rpc::RawUnaryResponder& responder) {
std::byte buffer[kFilterResponseBufferSize] = {};
StatusWithSize result = GetFilterImpl(request, buffer);
- responder.Finish(std::span(buffer).first(result.size()), result.status())
+ responder.Finish(span(buffer).first(result.size()), result.status())
.IgnoreError();
}
void ListFilterIds(ConstByteSpan, rpc::RawUnaryResponder& responder) {
std::byte buffer[kFilterIdsResponseBufferSize] = {};
StatusWithSize result = ListFilterIdsImpl(buffer);
- responder.Finish(std::span(buffer).first(result.size()), result.status())
+ responder.Finish(span(buffer).first(result.size()), result.status())
.IgnoreError();
}
@@ -52,21 +52,26 @@ class FilterService final
static constexpr size_t kMinSupportedFilters = 4;
static constexpr size_t kFilterResponseBufferSize =
- protobuf::FieldNumberSizeBytes(log::Filter::Fields::RULE) +
+ protobuf::TagSizeBytes(log::pwpb::Filter::Fields::kRule) +
protobuf::kMaxSizeOfLength +
kMinSupportedFilters *
(protobuf::SizeOfFieldEnum(
- log::FilterRule::Fields::LEVEL_GREATER_THAN_OR_EQUAL, 7) +
- protobuf::SizeOfFieldBytes(log::FilterRule::Fields::MODULE_EQUALS,
- 6) +
- protobuf::SizeOfFieldUint32(log::FilterRule::Fields::ANY_FLAGS_SET,
- 1) +
- protobuf::SizeOfFieldEnum(log::FilterRule::Fields::ACTION, 2));
+ log::pwpb::FilterRule::Fields::kLevelGreaterThanOrEqual, 7) +
+ protobuf::SizeOfFieldBytes(
+ log::pwpb::FilterRule::Fields::kModuleEquals,
+ cfg::kMaxModuleNameBytes) +
+ protobuf::SizeOfFieldUint32(
+ log::pwpb::FilterRule::Fields::kAnyFlagsSet, 1) +
+ protobuf::SizeOfFieldEnum(log::pwpb::FilterRule::Fields::kAction,
+ 2) +
+ protobuf::SizeOfFieldBytes(
+ log::pwpb::FilterRule::Fields::kThreadEquals,
+ cfg::kMaxThreadNameBytes));
static constexpr size_t kFilterIdsResponseBufferSize =
kMinSupportedFilters *
- protobuf::SizeOfFieldBytes(log::FilterIdListResponse::Fields::FILTER_ID,
- 4);
+ protobuf::SizeOfFieldBytes(
+ log::pwpb::FilterIdListResponse::Fields::kFilterId, 4);
Status SetFilterImpl(ConstByteSpan request);
StatusWithSize GetFilterImpl(ConstByteSpan request, ByteSpan response);
diff --git a/pw_log_rpc/public/pw_log_rpc/rpc_log_drain.h b/pw_log_rpc/public/pw_log_rpc/rpc_log_drain.h
index 76dd7e372..7fd1302a7 100644
--- a/pw_log_rpc/public/pw_log_rpc/rpc_log_drain.h
+++ b/pw_log_rpc/public/pw_log_rpc/rpc_log_drain.h
@@ -40,15 +40,16 @@ namespace pw::log_rpc {
// RpcLogDrain matches a MultiSink::Drain with with an RPC channel's writer. A
// RPC channel ID identifies this drain. The user must attach this drain
-// to a MultiSink that returns a log::LogEntry, and provide a buffer large
-// enough to hold the largest log::LogEntry transmittable. The user must call
-// Flush(), which, on every call, packs as many log::LogEntry items as possible
-// into a log::LogEntries message, writes the message to the provided writer,
-// then repeats the process until there are no more entries in the MultiSink or
-// the writer failed to write the outgoing package and error_handling is set to
-// `kCloseStreamOnWriterError`. When error_handling is `kIgnoreWriterErrors` the
-// drain will continue to retrieve log entries out of the MultiSink and attempt
-// to send them out ignoring the writer errors without sending a drop count.
+// to a MultiSink that returns a log::pwpb::LogEntry, and provide a buffer large
+// enough to hold the largest log::pwpb::LogEntry transmittable. The user must
+// call Flush(), which, on every call, packs as many log::pwpb::LogEntry items
+// as possible into a log::pwpb::LogEntries message, writes the message to the
+// provided writer, then repeats the process until there are no more entries in
+// the MultiSink or the writer failed to write the outgoing package and
+// error_handling is set to `kCloseStreamOnWriterError`. When error_handling is
+// `kIgnoreWriterErrors` the drain will continue to retrieve log entries out of
+// the MultiSink and attempt to send them out ignoring the writer errors without
+// sending a drop count.
// Note: the error handling and drop count reporting might change in the future.
// Log filtering is done using the rules of the Filter provided if any.
class RpcLogDrain : public multisink::MultiSink::Drain {
@@ -60,17 +61,18 @@ class RpcLogDrain : public multisink::MultiSink::Drain {
};
// The minimum buffer size, without the message payload or module sizes,
- // needed to retrieve a log::LogEntry from the attached MultiSink. The user
- // must account for the max message size to avoid log entry drops. The dropped
- // field is not accounted since a dropped message has all other fields unset.
+ // needed to retrieve a log::pwpb::LogEntry from the attached MultiSink. The
+ // user must account for the max message size to avoid log entry drops. The
+ // dropped field is not accounted since a dropped message has all other fields
+ // unset.
static constexpr size_t kMinEntrySizeWithoutPayload =
- protobuf::SizeOfFieldBytes(log::LogEntry::Fields::MESSAGE, 0) +
- protobuf::SizeOfFieldUint32(log::LogEntry::Fields::LINE_LEVEL) +
- protobuf::SizeOfFieldUint32(log::LogEntry::Fields::FLAGS) +
- protobuf::SizeOfFieldInt64(log::LogEntry::Fields::TIMESTAMP) +
- protobuf::SizeOfFieldBytes(log::LogEntry::Fields::MODULE, 0) +
- protobuf::SizeOfFieldBytes(log::LogEntry::Fields::FILE, 0) +
- protobuf::SizeOfFieldBytes(log::LogEntry::Fields::THREAD, 0);
+ protobuf::SizeOfFieldBytes(log::pwpb::LogEntry::Fields::kMessage, 0) +
+ protobuf::SizeOfFieldUint32(log::pwpb::LogEntry::Fields::kLineLevel) +
+ protobuf::SizeOfFieldUint32(log::pwpb::LogEntry::Fields::kFlags) +
+ protobuf::SizeOfFieldInt64(log::pwpb::LogEntry::Fields::kTimestamp) +
+ protobuf::SizeOfFieldBytes(log::pwpb::LogEntry::Fields::kModule, 0) +
+ protobuf::SizeOfFieldBytes(log::pwpb::LogEntry::Fields::kFile, 0) +
+ protobuf::SizeOfFieldBytes(log::pwpb::LogEntry::Fields::kThread, 0);
// Error messages sent when logs are dropped.
static constexpr std::string_view kIngressErrorMessage{
@@ -99,14 +101,14 @@ class RpcLogDrain : public multisink::MultiSink::Drain {
// bytes added to the encoded LogEntry. This constant and kMinEntryBufferSize
// can be used to calculate the minimum RPC ChannelOutput buffer size.
static constexpr size_t kLogEntriesEncodeFrameSize =
- protobuf::FieldNumberSizeBytes(log::LogEntries::Fields::ENTRIES) +
+ protobuf::TagSizeBytes(log::pwpb::LogEntries::Fields::kEntries) +
protobuf::kMaxSizeOfLength +
protobuf::SizeOfFieldUint32(
- log::LogEntries::Fields::FIRST_ENTRY_SEQUENCE_ID);
+ log::pwpb::LogEntries::Fields::kFirstEntrySequenceId);
// Creates a closed log stream with a writer that can be set at a later time.
// The provided buffer must be large enough to hold the largest transmittable
- // log::LogEntry or a drop count message at the very least. The user can
+ // log::pwpb::LogEntry or a drop count message at the very least. The user can
// choose to provide a unique mutex for the drain, or share it to save RAM as
// long as they are aware of contengency issues.
RpcLogDrain(
@@ -210,9 +212,9 @@ class RpcLogDrain : public multisink::MultiSink::Drain {
Status& encoding_status) PW_LOCKS_EXCLUDED(mutex_);
// Fills the outgoing buffer with as many entries as possible.
- LogDrainState EncodeOutgoingPacket(log::LogEntries::MemoryEncoder& encoder,
- uint32_t& packed_entry_count_out)
- PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+ LogDrainState EncodeOutgoingPacket(
+ log::pwpb::LogEntries::MemoryEncoder& encoder,
+ uint32_t& packed_entry_count_out) PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
const uint32_t channel_id_;
const LogDrainErrorHandling error_handling_;
diff --git a/pw_log_rpc/public/pw_log_rpc/rpc_log_drain_map.h b/pw_log_rpc/public/pw_log_rpc/rpc_log_drain_map.h
index d21c37bef..b5652867c 100644
--- a/pw_log_rpc/public/pw_log_rpc/rpc_log_drain_map.h
+++ b/pw_log_rpc/public/pw_log_rpc/rpc_log_drain_map.h
@@ -14,10 +14,9 @@
#pragma once
-#include <span>
-
#include "pw_log_rpc/rpc_log_drain.h"
#include "pw_result/result.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::log_rpc {
@@ -26,7 +25,7 @@ namespace pw::log_rpc {
// maintenance of all RPC log streams.
class RpcLogDrainMap {
public:
- explicit constexpr RpcLogDrainMap(std::span<RpcLogDrain> drains)
+ explicit constexpr RpcLogDrainMap(span<RpcLogDrain> drains)
: drains_(drains) {}
// Not copyable nor movable.
@@ -44,10 +43,10 @@ class RpcLogDrainMap {
return Status::NotFound();
}
- const std::span<RpcLogDrain>& drains() const { return drains_; }
+ const span<RpcLogDrain>& drains() const { return drains_; }
protected:
- const std::span<RpcLogDrain> drains_;
+ const span<RpcLogDrain> drains_;
};
} // namespace pw::log_rpc
diff --git a/pw_log_rpc/public/pw_log_rpc/rpc_log_drain_thread.h b/pw_log_rpc/public/pw_log_rpc/rpc_log_drain_thread.h
index 7b8600730..e61c71c7c 100644
--- a/pw_log_rpc/public/pw_log_rpc/rpc_log_drain_thread.h
+++ b/pw_log_rpc/public/pw_log_rpc/rpc_log_drain_thread.h
@@ -16,7 +16,6 @@
#include <cstddef>
#include <optional>
-#include <span>
#include "pw_chrono/system_clock.h"
#include "pw_log_rpc/log_service.h"
@@ -24,6 +23,7 @@
#include "pw_multisink/multisink.h"
#include "pw_result/result.h"
#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/try.h"
#include "pw_sync/timed_thread_notification.h"
@@ -40,7 +40,7 @@ class RpcLogDrainThread : public thread::ThreadCore,
public:
RpcLogDrainThread(multisink::MultiSink& multisink,
RpcLogDrainMap& drain_map,
- std::span<std::byte> encoding_buffer)
+ span<std::byte> encoding_buffer)
: drain_map_(drain_map),
multisink_(multisink),
encoding_buffer_(encoding_buffer) {}
@@ -98,7 +98,7 @@ class RpcLogDrainThread : public thread::ThreadCore,
sync::TimedThreadNotification ready_to_flush_notification_;
RpcLogDrainMap& drain_map_;
multisink::MultiSink& multisink_;
- std::span<std::byte> encoding_buffer_;
+ span<std::byte> encoding_buffer_;
};
template <size_t kEncodingBufferSizeBytes>
diff --git a/pw_log_rpc/rpc_log_drain.cc b/pw_log_rpc/rpc_log_drain.cc
index fe12ba0c8..68294f9a5 100644
--- a/pw_log_rpc/rpc_log_drain.cc
+++ b/pw_log_rpc/rpc_log_drain.cc
@@ -17,7 +17,6 @@
#include <limits>
#include <mutex>
#include <optional>
-#include <span>
#include <string_view>
#include "pw_assert/check.h"
@@ -25,6 +24,7 @@
#include "pw_log/proto/log.pwpb.h"
#include "pw_result/result.h"
#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/try.h"
@@ -33,14 +33,15 @@ namespace {
// Creates an encoded drop message on the provided buffer and adds it to the
// bulk log entries. Resets the drop count when successfull.
-void TryEncodeDropMessage(ByteSpan encoded_drop_message_buffer,
- std::string_view reason,
- uint32_t& drop_count,
- log::LogEntries::MemoryEncoder& entries_encoder) {
+void TryEncodeDropMessage(
+ ByteSpan encoded_drop_message_buffer,
+ std::string_view reason,
+ uint32_t& drop_count,
+ log::pwpb::LogEntries::MemoryEncoder& entries_encoder) {
// Encode drop count and reason, if any, in log proto.
- log::LogEntry::MemoryEncoder encoder(encoded_drop_message_buffer);
+ log::pwpb::LogEntry::MemoryEncoder encoder(encoded_drop_message_buffer);
if (!reason.empty()) {
- encoder.WriteMessage(std::as_bytes(std::span(reason))).IgnoreError();
+ encoder.WriteMessage(as_bytes(span<const char>(reason))).IgnoreError();
}
encoder.WriteDropped(drop_count).IgnoreError();
if (!encoder.status().ok()) {
@@ -51,7 +52,8 @@ void TryEncodeDropMessage(ByteSpan encoded_drop_message_buffer,
if (drop_message.size() + RpcLogDrain::kLogEntriesEncodeFrameSize <
entries_encoder.ConservativeWriteLimit()) {
PW_CHECK_OK(entries_encoder.WriteBytes(
- static_cast<uint32_t>(log::LogEntries::Fields::ENTRIES), drop_message));
+ static_cast<uint32_t>(log::pwpb::LogEntries::Fields::kEntries),
+ drop_message));
drop_count = 0;
}
}
@@ -113,7 +115,7 @@ RpcLogDrain::LogDrainState RpcLogDrain::SendLogs(size_t max_num_bundles,
// No reason to keep polling this drain until the writer is opened.
return LogDrainState::kCaughtUp;
}
- log::LogEntries::MemoryEncoder encoder(encoding_buffer);
+ log::pwpb::LogEntries::MemoryEncoder encoder(encoding_buffer);
uint32_t packed_entry_count = 0;
log_sink_state = EncodeOutgoingPacket(encoder, packed_entry_count);
@@ -123,7 +125,7 @@ RpcLogDrain::LogDrainState RpcLogDrain::SendLogs(size_t max_num_bundles,
}
encoder.WriteFirstEntrySequenceId(sequence_id_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
sequence_id_ += packed_entry_count;
const Status status = server_writer_.Write(encoder);
sent_bundle_count++;
@@ -141,7 +143,8 @@ RpcLogDrain::LogDrainState RpcLogDrain::SendLogs(size_t max_num_bundles,
}
RpcLogDrain::LogDrainState RpcLogDrain::EncodeOutgoingPacket(
- log::LogEntries::MemoryEncoder& encoder, uint32_t& packed_entry_count_out) {
+ log::pwpb::LogEntries::MemoryEncoder& encoder,
+ uint32_t& packed_entry_count_out) {
const size_t total_buffer_size = encoder.ConservativeWriteLimit();
do {
// Peek entry and get drop count from multisink.
@@ -245,7 +248,7 @@ RpcLogDrain::LogDrainState RpcLogDrain::EncodeOutgoingPacket(
// Encode the entry and remove it from multisink.
PW_CHECK_OK(encoder.WriteBytes(
- static_cast<uint32_t>(log::LogEntries::Fields::ENTRIES),
+ static_cast<uint32_t>(log::pwpb::LogEntries::Fields::kEntries),
possible_entry.value().entry()));
PW_CHECK_OK(PopEntry(possible_entry.value()));
++packed_entry_count_out;
diff --git a/pw_log_rpc/rpc_log_drain_test.cc b/pw_log_rpc/rpc_log_drain_test.cc
index 157e1158d..e7042895b 100644
--- a/pw_log_rpc/rpc_log_drain_test.cc
+++ b/pw_log_rpc/rpc_log_drain_test.cc
@@ -16,7 +16,6 @@
#include <array>
#include <cstdint>
-#include <span>
#include <string_view>
#include "gtest/gtest.h"
@@ -35,6 +34,7 @@
#include "pw_rpc/channel.h"
#include "pw_rpc/raw/fake_channel_output.h"
#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_sync/mutex.h"
@@ -98,7 +98,7 @@ TEST(RpcLogDrainMap, GetDrainsByIdFromDrainMap) {
ASSERT_TRUE(drain_result.ok());
EXPECT_EQ(drain_result.value(), &drains[channel_id]);
}
- const std::span<RpcLogDrain> mapped_drains = drain_map.drains();
+ const span<RpcLogDrain> mapped_drains = drain_map.drains();
ASSERT_EQ(mapped_drains.size(), kMaxDrains);
for (uint32_t channel_id = 0; channel_id < kMaxDrains; ++channel_id) {
EXPECT_EQ(&mapped_drains[channel_id], &drains[channel_id]);
@@ -123,7 +123,7 @@ TEST(RpcLogDrain, FlushingDrainWithOpenWriter) {
rpc::RawFakeChannelOutput<3> output;
rpc::Channel channel(rpc::Channel::Create<drain_id>(&output));
- rpc::Server server(std::span(&channel, 1));
+ rpc::Server server(span(&channel, 1));
// Attach drain to a MultiSink.
RpcLogDrain& drain = drains[0];
@@ -162,7 +162,7 @@ TEST(RpcLogDrain, TryReopenOpenedDrain) {
rpc::RawFakeChannelOutput<1> output;
rpc::Channel channel(rpc::Channel::Create<drain_id>(&output));
- rpc::Server server(std::span(&channel, 1));
+ rpc::Server server(span(&channel, 1));
// Open Drain and try to open with a new writer.
rpc::RawServerWriter writer =
@@ -200,14 +200,14 @@ class TrickleTest : public ::testing::Test {
log_service_(drain_map_),
output_(),
channel_(rpc::Channel::Create<kDrainChannelId>(&output_)),
- server_(std::span(&channel_, 1)) {}
+ server_(span(&channel_, 1)) {}
TestLogEntry BasicLog(std::string_view message) {
return {.metadata = kSampleMetadata,
.timestamp = kSampleTimestamp,
.dropped = 0,
- .tokenized_data = std::as_bytes(std::span(message)),
- .thread = std::as_bytes(std::span(kSampleThreadName))};
+ .tokenized_data = as_bytes(span<const char>(message)),
+ .thread = as_bytes(span(kSampleThreadName))};
}
void AttachDrain() { multisink_.AttachDrain(drains_[0]); }
@@ -244,18 +244,18 @@ class TrickleTest : public ::testing::Test {
static constexpr uint64_t kSampleTimestamp = 9000;
static constexpr std::string_view kSampleThreadName = "thread";
static constexpr size_t kBasicLogSizeWithoutPayload =
- protobuf::SizeOfFieldBytes(log::LogEntry::Fields::MESSAGE, 0) +
+ protobuf::SizeOfFieldBytes(log::pwpb::LogEntry::Fields::kMessage, 0) +
protobuf::SizeOfFieldUint32(
- log::LogEntry::Fields::LINE_LEVEL,
+ log::pwpb::LogEntry::Fields::kLineLevel,
log::PackLineLevel(kSampleMetadata.line_number(),
kSampleMetadata.level())) +
- protobuf::SizeOfFieldUint32(log::LogEntry::Fields::FLAGS,
+ protobuf::SizeOfFieldUint32(log::pwpb::LogEntry::Fields::kFlags,
kSampleMetadata.flags()) +
- protobuf::SizeOfFieldInt64(log::LogEntry::Fields::TIMESTAMP,
+ protobuf::SizeOfFieldInt64(log::pwpb::LogEntry::Fields::kTimestamp,
kSampleTimestamp) +
- protobuf::SizeOfFieldBytes(log::LogEntry::Fields::MODULE,
+ protobuf::SizeOfFieldBytes(log::pwpb::LogEntry::Fields::kModule,
sizeof(kSampleMetadata.module())) +
- protobuf::SizeOfFieldBytes(log::LogEntry::Fields::THREAD,
+ protobuf::SizeOfFieldBytes(log::pwpb::LogEntry::Fields::kThread,
kSampleThreadName.size());
static constexpr size_t kDrainEncodeBufferSize =
kBasicLogSizeWithoutPayload + kMaxMessageSize;
@@ -423,7 +423,7 @@ TEST(RpcLogDrain, OnOpenCallbackCalled) {
mutex,
RpcLogDrain::LogDrainErrorHandling::kCloseStreamOnWriterError,
nullptr);
- RpcLogDrainMap drain_map(std::span(&drain, 1));
+ RpcLogDrainMap drain_map(span(&drain, 1));
LogService log_service(drain_map);
std::array<std::byte, kBufferSize * 2> multisink_buffer;
multisink::MultiSink multisink(multisink_buffer);
@@ -432,7 +432,7 @@ TEST(RpcLogDrain, OnOpenCallbackCalled) {
// Create server writer.
rpc::RawFakeChannelOutput<3> output;
rpc::Channel channel(rpc::Channel::Create<drain_id>(&output));
- rpc::Server server(std::span(&channel, 1));
+ rpc::Server server(span(&channel, 1));
rpc::RawServerWriter writer =
rpc::RawServerWriter::Open<log::pw_rpc::raw::Logs::Listen>(
server, drain_id, log_service);
diff --git a/pw_log_rpc/test_utils.cc b/pw_log_rpc/test_utils.cc
index 3a06ec2b2..899382002 100644
--- a/pw_log_rpc/test_utils.cc
+++ b/pw_log_rpc/test_utils.cc
@@ -28,7 +28,7 @@
namespace pw::log_rpc {
namespace {
void VerifyOptionallyTokenizedField(protobuf::Decoder& entry_decoder,
- log::LogEntry::Fields field_number,
+ log::pwpb::LogEntry::Fields field_number,
ConstByteSpan expected_data) {
if (expected_data.empty()) {
return;
@@ -38,10 +38,10 @@ void VerifyOptionallyTokenizedField(protobuf::Decoder& entry_decoder,
ASSERT_EQ(entry_decoder.FieldNumber(), static_cast<uint32_t>(field_number));
ASSERT_EQ(entry_decoder.ReadBytes(&tokenized_data), OkStatus());
std::string_view data_as_string(
- reinterpret_cast<const char*>(tokenized_data.begin()),
+ reinterpret_cast<const char*>(tokenized_data.data()),
tokenized_data.size());
std::string_view expected_data_as_string(
- reinterpret_cast<const char*>(expected_data.begin()),
+ reinterpret_cast<const char*>(expected_data.data()),
expected_data.size());
EXPECT_EQ(data_as_string, expected_data_as_string);
}
@@ -53,12 +53,12 @@ void VerifyLogEntry(protobuf::Decoder& entry_decoder,
const TestLogEntry& expected_entry,
uint32_t& drop_count_out) {
VerifyOptionallyTokenizedField(entry_decoder,
- log::LogEntry::Fields::MESSAGE,
+ log::pwpb::LogEntry::Fields::kMessage,
expected_entry.tokenized_data);
if (expected_entry.metadata.level()) {
ASSERT_EQ(entry_decoder.Next(), OkStatus());
ASSERT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::LINE_LEVEL));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kLineLevel));
uint32_t line_level;
ASSERT_TRUE(entry_decoder.ReadUint32(&line_level).ok());
EXPECT_EQ(expected_entry.metadata.level(),
@@ -69,18 +69,19 @@ void VerifyLogEntry(protobuf::Decoder& entry_decoder,
if (expected_entry.metadata.flags()) {
ASSERT_EQ(entry_decoder.Next(), OkStatus());
ASSERT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::FLAGS));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kFlags));
uint32_t flags;
ASSERT_TRUE(entry_decoder.ReadUint32(&flags).ok());
EXPECT_EQ(expected_entry.metadata.flags(), flags);
}
if (expected_entry.timestamp) {
ASSERT_EQ(entry_decoder.Next(), OkStatus());
- ASSERT_TRUE(entry_decoder.FieldNumber() ==
- static_cast<uint32_t>(log::LogEntry::Fields::TIMESTAMP) ||
- entry_decoder.FieldNumber() ==
- static_cast<uint32_t>(
- log::LogEntry::Fields::TIME_SINCE_LAST_ENTRY));
+ ASSERT_TRUE(
+ entry_decoder.FieldNumber() ==
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kTimestamp) ||
+ entry_decoder.FieldNumber() ==
+ static_cast<uint32_t>(
+ log::pwpb::LogEntry::Fields::kTimeSinceLastEntry));
int64_t timestamp;
ASSERT_TRUE(entry_decoder.ReadInt64(&timestamp).ok());
EXPECT_EQ(expected_entry.timestamp, timestamp);
@@ -88,7 +89,7 @@ void VerifyLogEntry(protobuf::Decoder& entry_decoder,
if (expected_entry.dropped) {
ASSERT_EQ(entry_decoder.Next(), OkStatus());
ASSERT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::DROPPED));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kDropped));
uint32_t dropped = 0;
ASSERT_TRUE(entry_decoder.ReadUint32(&dropped).ok());
EXPECT_EQ(expected_entry.dropped, dropped);
@@ -97,16 +98,17 @@ void VerifyLogEntry(protobuf::Decoder& entry_decoder,
if (expected_entry.metadata.module()) {
ASSERT_EQ(entry_decoder.Next(), OkStatus());
ASSERT_EQ(entry_decoder.FieldNumber(),
- static_cast<uint32_t>(log::LogEntry::Fields::MODULE));
+ static_cast<uint32_t>(log::pwpb::LogEntry::Fields::kModule));
const Result<uint32_t> module =
protobuf::DecodeBytesToUint32(entry_decoder);
ASSERT_EQ(module.status(), OkStatus());
EXPECT_EQ(expected_entry.metadata.module(), module.value());
}
VerifyOptionallyTokenizedField(
- entry_decoder, log::LogEntry::Fields::FILE, expected_entry.file);
- VerifyOptionallyTokenizedField(
- entry_decoder, log::LogEntry::Fields::THREAD, expected_entry.thread);
+ entry_decoder, log::pwpb::LogEntry::Fields::kFile, expected_entry.file);
+ VerifyOptionallyTokenizedField(entry_decoder,
+ log::pwpb::LogEntry::Fields::kThread,
+ expected_entry.thread);
}
// Compares an encoded LogEntry's fields against the expected sequence ID and
@@ -120,9 +122,9 @@ void VerifyLogEntries(protobuf::Decoder& entries_decoder,
uint32_t& drop_count_out) {
size_t entry_index = entries_count_out;
while (entries_decoder.Next().ok()) {
- if (static_cast<pw::log::LogEntries::Fields>(
+ if (static_cast<log::pwpb::LogEntries::Fields>(
entries_decoder.FieldNumber()) ==
- log::LogEntries::Fields::ENTRIES) {
+ log::pwpb::LogEntries::Fields::kEntries) {
ConstByteSpan entry;
EXPECT_EQ(entries_decoder.ReadBytes(&entry), OkStatus());
protobuf::Decoder entry_decoder(entry);
@@ -141,9 +143,9 @@ void VerifyLogEntries(protobuf::Decoder& entries_decoder,
if (current_drop_count == 0) {
++entries_count_out;
}
- } else if (static_cast<pw::log::LogEntries::Fields>(
+ } else if (static_cast<log::pwpb::LogEntries::Fields>(
entries_decoder.FieldNumber()) ==
- log::LogEntries::Fields::FIRST_ENTRY_SEQUENCE_ID) {
+ log::pwpb::LogEntries::Fields::kFirstEntrySequenceId) {
uint32_t first_entry_sequence_id = 0;
EXPECT_EQ(entries_decoder.ReadUint32(&first_entry_sequence_id),
OkStatus());
@@ -155,9 +157,9 @@ void VerifyLogEntries(protobuf::Decoder& entries_decoder,
size_t CountLogEntries(protobuf::Decoder& entries_decoder) {
size_t entries_found = 0;
while (entries_decoder.Next().ok()) {
- if (static_cast<pw::log::LogEntries::Fields>(
+ if (static_cast<log::pwpb::LogEntries::Fields>(
entries_decoder.FieldNumber()) ==
- log::LogEntries::Fields::ENTRIES) {
+ log::pwpb::LogEntries::Fields::kEntries) {
++entries_found;
}
}
diff --git a/pw_log_string/BUILD.bazel b/pw_log_string/BUILD.bazel
index 74deb510e..e837734e7 100644
--- a/pw_log_string/BUILD.bazel
+++ b/pw_log_string/BUILD.bazel
@@ -53,3 +53,11 @@ pw_cc_library(
"@pigweed_config//:pw_log_string_handler_backend",
],
)
+
+pw_cc_library(
+ name = "handler_backend_multiplexer",
+ visibility = ["@pigweed_config//:__pkg__"],
+ # TODO(b/257539200): This probably should default to
+ # //pw_system:log_backend, but that target does not yet build in Bazel.
+ # deps = ["//pw_system:log_backend"],
+)
diff --git a/pw_log_string/BUILD.gn b/pw_log_string/BUILD.gn
index 0909a2a0e..130550c0d 100644
--- a/pw_log_string/BUILD.gn
+++ b/pw_log_string/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_build/error.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
import("backend.gni")
config("public_include_path") {
@@ -92,3 +93,6 @@ if (pw_log_string_HANDLER_BACKEND != "") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_log_string/CMakeLists.txt b/pw_log_string/CMakeLists.txt
index 87dfabdf6..fd3741f8e 100644
--- a/pw_log_string/CMakeLists.txt
+++ b/pw_log_string/CMakeLists.txt
@@ -13,10 +13,9 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_log_string/backend.cmake)
-pw_add_module_library(pw_log_string
- IMPLEMENTS_FACADES
- pw_log
+pw_add_library(pw_log_string INTERFACE
HEADERS
public/pw_log_string/log_string.h
public_overrides/pw_log_backend/log_backend.h
@@ -28,7 +27,9 @@ pw_add_module_library(pw_log_string
pw_log_string.handler
)
-pw_add_facade(pw_log_string.handler
+pw_add_facade(pw_log_string.handler STATIC
+ BACKEND
+ pw_log_string.handler_BACKEND
HEADERS
public/pw_log_string/handler.h
PUBLIC_INCLUDES
diff --git a/pw_log_string/backend.cmake b/pw_log_string/backend.cmake
new file mode 100644
index 000000000..1d34a9ea6
--- /dev/null
+++ b/pw_log_string/backend.cmake
@@ -0,0 +1,20 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# The pw_log_string.handler backend implements the pw_Log C API which is also
+# used by the pw_log_string module's implementation of the pw_log facade.
+pw_add_backend_variable(pw_log_string.handler_BACKEND)
diff --git a/pw_log_string/public/pw_log_string/log_string.h b/pw_log_string/public/pw_log_string/log_string.h
index 4f529198c..352541dad 100644
--- a/pw_log_string/public/pw_log_string/log_string.h
+++ b/pw_log_string/public/pw_log_string/log_string.h
@@ -22,11 +22,11 @@
// This is the log macro frontend that funnels everything into the C-based
// message hangler facade, i.e. pw_log_string_HandleMessage. It's not efficient
// at the callsite, since it passes many arguments.
-#define PW_HANDLE_LOG(level, flags, message, ...) \
+#define PW_HANDLE_LOG(level, module, flags, message, ...) \
do { \
pw_log_string_HandleMessage((level), \
(flags), \
- PW_LOG_MODULE_NAME, \
+ (module), \
__FILE__, \
__LINE__, \
message PW_COMMA_ARGS(__VA_ARGS__)); \
diff --git a/pw_log_tokenized/BUILD.bazel b/pw_log_tokenized/BUILD.bazel
index 5ac34a618..abcf0f1de 100644
--- a/pw_log_tokenized/BUILD.bazel
+++ b/pw_log_tokenized/BUILD.bazel
@@ -35,27 +35,64 @@ pw_cc_library(
"public_overrides",
],
deps = [
+ "//pw_log:facade",
"//pw_tokenizer",
],
)
pw_cc_library(
name = "pw_log_tokenized",
+ srcs = ["log_tokenized.cc"],
deps = [
+ ":handler",
":headers",
"//pw_log:facade",
],
)
pw_cc_library(
+ name = "handler_facade",
+ hdrs = ["public/pw_log_tokenized/handler.h"],
+ includes = ["public"],
+ deps = ["//pw_preprocessor"],
+)
+
+pw_cc_library(
+ name = "handler",
+ deps = [
+ ":handler_facade",
+ "@pigweed_config//:pw_log_tokenized_handler_backend",
+ ],
+)
+
+# There is no default backend for now.
+pw_cc_library(
+ name = "backend_multiplexer",
+ visibility = ["@pigweed_config//:__pkg__"],
+)
+
+# The compatibility library is not needed in Bazel.
+pw_cc_library(
+ name = "compatibility",
+ srcs = ["compatibility.cc"],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":handler_facade",
+ "//pw_tokenizer",
+ "//pw_tokenizer:global_handler_with_payload",
+ ],
+)
+
+pw_cc_library(
name = "base64_over_hdlc",
srcs = ["base64_over_hdlc.cc"],
hdrs = ["public/pw_log_tokenized/base64_over_hdlc.h"],
includes = ["public"],
deps = [
- "//pw_hdlc:encoder",
+ ":handler_facade",
+ "//pw_hdlc",
+ "//pw_stream:sys_io_stream",
"//pw_tokenizer:base64",
- "//pw_tokenizer:global_handler_with_payload.facade",
],
)
diff --git a/pw_log_tokenized/BUILD.gn b/pw_log_tokenized/BUILD.gn
index 21096734f..3e2b16181 100644
--- a/pw_log_tokenized/BUILD.gn
+++ b/pw_log_tokenized/BUILD.gn
@@ -14,10 +14,12 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/facade.gni")
import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_log/backend.gni")
+import("$dir_pw_log_tokenized/backend.gni")
import("$dir_pw_tokenizer/backend.gni")
import("$dir_pw_unit_test/test.gni")
@@ -40,19 +42,84 @@ config("backend_config") {
# This target provides the backend for pw_log.
pw_source_set("pw_log_tokenized") {
- public_configs = [
- ":backend_config",
- ":public_include_path",
+ public_configs = [ ":backend_config" ]
+ public_deps = [
+ ":handler.facade", # Depend on the facade to avoid circular dependencies.
+ ":headers",
]
+ public = [ "public_overrides/pw_log_backend/log_backend.h" ]
+
+ sources = [ "log_tokenized.cc" ]
+}
+
+config("backwards_compatibility_config") {
+ defines = [ "_PW_LOG_TOKENIZED_GLOBAL_HANDLER_BACKWARDS_COMPAT" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("headers") {
+ visibility = [ ":*" ]
+ public_configs = [ ":public_include_path" ]
public_deps = [
":config",
":metadata",
- "$dir_pw_tokenizer:global_handler_with_payload.facade",
+
+ # TODO(hepler): Remove this dependency when all projects have migrated to
+ # the new pw_log_tokenized handler.
+ "$dir_pw_tokenizer:global_handler_with_payload",
+ dir_pw_preprocessor,
+ dir_pw_tokenizer,
]
- public = [
- "public/pw_log_tokenized/log_tokenized.h",
- "public_overrides/pw_log_backend/log_backend.h",
+ public = [ "public/pw_log_tokenized/log_tokenized.h" ]
+}
+
+# The old pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND backend may still be
+# in use by projects that have not switched to the new pw_log_tokenized facade.
+# Use the old backend as a stand-in for the new backend if it is set.
+_old_backend_is_set = pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND != ""
+_new_backend_is_set = pw_log_tokenized_HANDLER_BACKEND != ""
+
+pw_facade("handler") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ # TODO(hepler): Remove this dependency when all projects have migrated to
+ # the new pw_log_tokenized handler.
+ "$dir_pw_tokenizer:global_handler_with_payload",
+ dir_pw_preprocessor,
]
+
+ public = [ "public/pw_log_tokenized/handler.h" ]
+
+ # If the global handler backend is set, redirect the new facade to the old
+ # facade. If no backend is set, the old facade may still be in use through
+ # link deps, so provide the compatibility layer.
+ #
+ # TODO(hepler): Remove these backwards compatibility workarounds when projects
+ # have migrated.
+ if (_old_backend_is_set || (!_old_backend_is_set && !_new_backend_is_set)) {
+ assert(pw_log_tokenized_HANDLER_BACKEND == "",
+ "pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND is deprecated; " +
+ "only pw_log_tokenized_HANDLER_BACKEND should be set")
+
+ backend = pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND
+
+ # There is only one pw_log_tokenized backend in Pigweed, and it has been
+ # updated to the new API.
+ if (_old_backend_is_set &&
+ get_label_info(pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND,
+ "label_no_toolchain") ==
+ get_label_info(":base64_over_hdlc", "label_no_toolchain")) {
+ defines = [ "PW_LOG_TOKENIZED_BACKEND_USES_NEW_API=1" ]
+ } else {
+ defines = [ "PW_LOG_TOKENIZED_BACKEND_USES_NEW_API=0" ]
+ }
+
+ public_configs += [ ":backwards_compatibility_config" ]
+ deps = [ dir_pw_tokenizer ]
+ sources = [ "compatibility.cc" ]
+ } else {
+ backend = pw_log_tokenized_HANDLER_BACKEND
+ }
}
pw_source_set("metadata") {
@@ -74,10 +141,11 @@ pw_source_set("config") {
# pw_log is so ubiquitous. These deps are kept separate so they can be
# depended on from elsewhere.
pw_source_set("pw_log_tokenized.impl") {
- deps = [
- ":pw_log_tokenized",
- "$dir_pw_tokenizer:global_handler_with_payload",
- ]
+ deps = [ ":pw_log_tokenized" ]
+
+ if (_new_backend_is_set || _old_backend_is_set) {
+ deps += [ ":handler" ]
+ }
}
# This target provides a backend for pw_tokenizer that encodes tokenized logs as
@@ -87,10 +155,11 @@ pw_source_set("base64_over_hdlc") {
public = [ "public/pw_log_tokenized/base64_over_hdlc.h" ]
sources = [ "base64_over_hdlc.cc" ]
deps = [
+ ":handler.facade",
"$dir_pw_hdlc:encoder",
"$dir_pw_stream:sys_io_stream",
"$dir_pw_tokenizer:base64",
- "$dir_pw_tokenizer:global_handler_with_payload.facade",
+ dir_pw_span,
]
}
@@ -108,7 +177,7 @@ pw_test("log_tokenized_test") {
"pw_log_tokenized_private/test_utils.h",
]
deps = [
- ":pw_log_tokenized",
+ ":headers",
dir_pw_preprocessor,
]
}
diff --git a/pw_log_tokenized/CMakeLists.txt b/pw_log_tokenized/CMakeLists.txt
index d051f3b2a..28b3c96a9 100644
--- a/pw_log_tokenized/CMakeLists.txt
+++ b/pw_log_tokenized/CMakeLists.txt
@@ -13,10 +13,11 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_log_tokenized/backend.cmake)
pw_add_module_config(pw_log_tokenized_CONFIG)
-pw_add_module_library(pw_log_tokenized.config
+pw_add_library(pw_log_tokenized.config INTERFACE
HEADERS
public/pw_log_tokenized/config.h
PUBLIC_INCLUDES
@@ -26,37 +27,45 @@ pw_add_module_library(pw_log_tokenized.config
${pw_log_tokenized_CONFIG}
)
-pw_add_module_library(pw_log_tokenized
- IMPLEMENTS_FACADES
- pw_log
+pw_add_library(pw_log_tokenized STATIC
HEADERS
public/pw_log_tokenized/log_tokenized.h
public_overrides/pw_log_backend/log_backend.h
PUBLIC_INCLUDES
public
+ public_overrides
PUBLIC_DEPS
pw_log_tokenized.config
+ pw_log_tokenized.handler
pw_log_tokenized.metadata
pw_tokenizer
- PRIVATE_DEPS
- pw_tokenizer.global_handler_with_payload
+ SOURCES
+ log_tokenized.cc
)
-pw_add_module_library(pw_log_tokenized.metadata
+pw_add_library(pw_log_tokenized.metadata INTERFACE
HEADERS
public/pw_log_tokenized/metadata.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
- pw_log.facade
pw_log_tokenized.config
)
+pw_add_facade(pw_log_tokenized.handler INTERFACE
+ BACKEND
+ pw_log_tokenized.handler_BACKEND
+ HEADERS
+ public/pw_log_tokenized/handler.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_preprocessor
+)
+
# This target provides a backend for pw_tokenizer that encodes tokenized logs as
# Base64, encodes them into HDLC frames, and writes them over sys_io.
-pw_add_module_library(pw_log_tokenized.base64_over_hdlc
- IMPLEMENTS_FACADES
- pw_tokenizer.global_handler_with_payload
+pw_add_library(pw_log_tokenized.base64_over_hdlc STATIC
HEADERS
public/pw_log_tokenized/base64_over_hdlc.h
PUBLIC_INCLUDES
@@ -65,27 +74,31 @@ pw_add_module_library(pw_log_tokenized.base64_over_hdlc
base64_over_hdlc.cc
PRIVATE_DEPS
pw_hdlc.encoder
+ pw_log_tokenized.handler
+ pw_span
pw_stream.sys_io_stream
pw_tokenizer.base64
)
-pw_add_test(pw_log_tokenized.log_tokenized_test
- SOURCES
- log_tokenized_test.cc
- log_tokenized_test_c.c
- pw_log_tokenized_private/test_utils.h
- DEPS
- pw_log_tokenized
- pw_preprocessor
- GROUPS
- modules
- pw_log_tokenized
-)
+if(NOT "${pw_tokenizer.global_handler_with_payload_BACKEND}" STREQUAL "")
+ pw_add_test(pw_log_tokenized.log_tokenized_test
+ SOURCES
+ log_tokenized_test.cc
+ log_tokenized_test_c.c
+ pw_log_tokenized_private/test_utils.h
+ PRIVATE_DEPS
+ pw_log_tokenized
+ pw_preprocessor
+ GROUPS
+ modules
+ pw_log_tokenized
+ )
+endif()
pw_add_test(pw_log_tokenized.metadata_test
SOURCES
metadata_test.cc
- DEPS
+ PRIVATE_DEPS
pw_log_tokenized.metadata
GROUPS
modules
diff --git a/pw_log_tokenized/backend.cmake b/pw_log_tokenized/backend.cmake
new file mode 100644
index 000000000..5218566b9
--- /dev/null
+++ b/pw_log_tokenized/backend.cmake
@@ -0,0 +1,19 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_log_tokenized handler.
+pw_add_backend_variable(pw_log_tokenized.handler_BACKEND)
diff --git a/pw_log_tokenized/backend.gni b/pw_log_tokenized/backend.gni
new file mode 100644
index 000000000..afa71ed51
--- /dev/null
+++ b/pw_log_tokenized/backend.gni
@@ -0,0 +1,18 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+declare_args() {
+ # Backend for the pw_log_tokenized log handler.
+ pw_log_tokenized_HANDLER_BACKEND = ""
+}
diff --git a/pw_log_tokenized/base64_over_hdlc.cc b/pw_log_tokenized/base64_over_hdlc.cc
index f7dc0e0fe..29e925ab7 100644
--- a/pw_log_tokenized/base64_over_hdlc.cc
+++ b/pw_log_tokenized/base64_over_hdlc.cc
@@ -17,12 +17,11 @@
#include "pw_log_tokenized/base64_over_hdlc.h"
-#include <span>
-
#include "pw_hdlc/encoder.h"
+#include "pw_log_tokenized/handler.h"
+#include "pw_span/span.h"
#include "pw_stream/sys_io_stream.h"
#include "pw_tokenizer/base64.h"
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
namespace pw::log_tokenized {
namespace {
@@ -32,19 +31,19 @@ stream::SysIoWriter writer;
} // namespace
// Base64-encodes tokenized logs and writes them to pw::sys_io as HDLC frames.
-extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload, // TODO(hepler): Use the metadata for filtering.
+extern "C" void pw_log_tokenized_HandleLog(
+ uint32_t, // TODO(hepler): Use the metadata for filtering.
const uint8_t log_buffer[],
size_t size_bytes) {
// Encode the tokenized message as Base64.
char base64_buffer[tokenizer::kDefaultBase64EncodedBufferSize];
const size_t base64_bytes = tokenizer::PrefixedBase64Encode(
- std::span(log_buffer, size_bytes), base64_buffer);
+ span(log_buffer, size_bytes), base64_buffer);
base64_buffer[base64_bytes] = '\0';
// HDLC-encode the Base64 string via a SysIoWriter.
hdlc::WriteUIFrame(PW_LOG_TOKENIZED_BASE64_LOG_HDLC_ADDRESS,
- std::as_bytes(std::span(base64_buffer, base64_bytes)),
+ as_bytes(span(base64_buffer, base64_bytes)),
writer);
}
diff --git a/pw_log_tokenized/compatibility.cc b/pw_log_tokenized/compatibility.cc
new file mode 100644
index 000000000..54060dbc7
--- /dev/null
+++ b/pw_log_tokenized/compatibility.cc
@@ -0,0 +1,60 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// If the project is still using pw_tokenizer's global handler with payload
+// facade, then define its functions as used by pw_log_tokenized.
+
+#include <cstdarg>
+
+#include "pw_log_tokenized/handler.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_tokenizer/encode_args.h"
+#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
+
+// If the new API is in use, define pw_tokenizer_HandleEncodedMessageWithPayload
+// to redirect to it, in case there are any direct calls to it. Only projects
+// that use the base64_over_hdlc backend will have been updated to the new API.
+#if PW_LOG_TOKENIZED_BACKEND_USES_NEW_API
+
+extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
+ uint32_t metadata, const uint8_t encoded_message[], size_t size_bytes) {
+ pw_log_tokenized_HandleLog(metadata, encoded_message, size_bytes);
+}
+
+#else // If the new API is not in use, implement it to redirect to the old API.
+
+extern "C" void pw_log_tokenized_HandleLog(uint32_t metadata,
+ const uint8_t encoded_message[],
+ size_t size_bytes) {
+ pw_tokenizer_HandleEncodedMessageWithPayload(
+ metadata, encoded_message, size_bytes);
+}
+
+#endif // PW_LOG_TOKENIZED_BACKEND_USES_NEW_API
+
+// Implement the global tokenized log handler function, which is identical
+// This function is the same as _pw_log_tokenized_EncodeTokenizedLog().
+extern "C" void _pw_tokenizer_ToGlobalHandlerWithPayload(
+ uint32_t metadata,
+ pw_tokenizer_Token token,
+ pw_tokenizer_ArgTypes types,
+ ...) {
+ va_list args;
+ va_start(args, types);
+ pw::tokenizer::EncodedMessage<> encoded_message(token, types, args);
+ va_end(args);
+
+ pw_log_tokenized_HandleLog(
+ metadata, encoded_message.data_as_uint8(), encoded_message.size());
+}
diff --git a/pw_log_tokenized/docs.rst b/pw_log_tokenized/docs.rst
index 1aa626765..24a6d8d4a 100644
--- a/pw_log_tokenized/docs.rst
+++ b/pw_log_tokenized/docs.rst
@@ -9,19 +9,19 @@ connects ``pw_log`` to ``pw_tokenizer``.
C++ backend
===========
``pw_log_tokenized`` provides a backend for ``pw_log`` that tokenizes log
-messages with the ``pw_tokenizer`` module. By default, log messages are
-tokenized with the ``PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD`` macro.
-The log level, 16-bit tokenized module name, and flags bits are passed through
-the payload argument. The macro eventually passes logs to the
-``pw_tokenizer_HandleEncodedMessageWithPayload`` function, which must be
-implemented by the application.
+messages with the ``pw_tokenizer`` module. The log level, 16-bit tokenized
+module name, and flags bits are passed through the payload argument. The macro
+eventually passes logs to the :c:func:`pw_log_tokenized_HandleLog` function,
+which must be implemented by the application.
+
+.. doxygenfunction:: pw_log_tokenized_HandleLog
Example implementation:
.. code-block:: cpp
- extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload payload, const uint8_t message[], size_t size) {
+ extern "C" void pw_log_tokenized_HandleLog(
+ uint32_t payload, const uint8_t message[], size_t size) {
// The metadata object provides the log level, module token, and flags.
// These values can be recorded and used for runtime filtering.
pw::log_tokenized::Metadata metadata(payload);
@@ -128,29 +128,52 @@ bits allocated is excluded from the log metadata.
Defaults to 16, which gives a ~1% probability of a collision with 37 module
names.
-Using a custom macro
---------------------
-Applications may use their own macro instead of
-``PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD`` by setting the
-``PW_LOG_TOKENIZED_ENCODE_MESSAGE`` config macro. This macro should take
-arguments equivalent to ``PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD``:
-
-.. c:macro:: PW_LOG_TOKENIZED_ENCODE_MESSAGE(log_metadata, message, ...)
+Creating and reading Metadata payloads
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+``pw_log_tokenized`` provides a C++ class to facilitate the creation and
+interpretation of packed log metadata payloads.
- :param log_metadata:
+.. doxygenclass:: pw::log_tokenized::GenericMetadata
+.. doxygentypedef:: pw::log_tokenized::Metadata
- Packed metadata for the log message. See the Metadata_ class for how to
- unpack the details.
+The following example shows that a ``Metadata`` object can be created from a
+``uint32_t`` log metadata payload.
- :type log_metadata: pw_tokenizer_Payload
+.. code-block:: cpp
- :param message: The log message format string (untokenized)
- :type message: :c:texpr:`const char*`
+ extern "C" void pw_log_tokenized_HandleLog(
+ uint32_t payload,
+ const uint8_t message[],
+ size_t size_bytes) {
+ pw::log_tokenized::Metadata metadata = payload;
+ // Check the log level to see if this log is a crash.
+ if (metadata.level() == PW_LOG_LEVEL_FATAL) {
+ HandleCrash(metadata, pw::ConstByteSpan(
+ reinterpret_cast<const std::byte*>(message), size_bytes));
+ PW_UNREACHABLE;
+ }
+ // ...
+ }
+
+It's also possible to get a ``uint32_t`` representation of a ``Metadata``
+object:
- .. _Metadata: https://cs.opensource.google/pigweed/pigweed/+/HEAD:pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h;l=113
+.. code-block:: cpp
-For instructions on how to implement a custom tokenization macro, see
-:ref:`module-pw_tokenizer-custom-macro`.
+ // Logs an explicitly created string token.
+ void LogToken(uint32_t token, int level, int line_number, int module) {
+ const uint32_t payload =
+ log_tokenized::Metadata(
+ level, module, PW_LOG_FLAGS, line_number)
+ .value();
+ std::array<std::byte, sizeof(token)> token_buffer =
+ pw::bytes::CopyInOrder(endian::little, token);
+
+ pw_log_tokenized_HandleLog(
+ payload,
+ reinterpret_cast<const uint8_t*>(token_buffer.data()),
+ token_buffer.size());
+ }
Build targets
-------------
@@ -158,8 +181,8 @@ The GN build for ``pw_log_tokenized`` has two targets: ``pw_log_tokenized`` and
``log_backend``. The ``pw_log_tokenized`` target provides the
``pw_log_tokenized/log_tokenized.h`` header. The ``log_backend`` target
implements the backend for the ``pw_log`` facade. ``pw_log_tokenized`` invokes
-the ``pw_tokenizer:global_handler_with_payload`` facade, which must be
-implemented by the user of ``pw_log_tokenized``.
+the ``pw_log_tokenized:handler`` facade, which must be implemented by the user
+of ``pw_log_tokenized``.
Python package
==============
diff --git a/pw_tokenizer/tokenize_to_global_handler_with_payload.cc b/pw_log_tokenized/log_tokenized.cc
index 44b02c643..25c00d90a 100644
--- a/pw_tokenizer/tokenize_to_global_handler_with_payload.cc
+++ b/pw_log_tokenized/log_tokenized.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// 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
@@ -12,26 +12,23 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
+#include "pw_log_tokenized/log_tokenized.h"
-#include "pw_tokenizer/encode_args.h"
+#include <cstdarg>
-namespace pw {
-namespace tokenizer {
+#include "pw_log_tokenized/handler.h"
+#include "pw_tokenizer/encode_args.h"
-extern "C" void _pw_tokenizer_ToGlobalHandlerWithPayload(
- const pw_tokenizer_Payload payload,
+extern "C" void _pw_log_tokenized_EncodeTokenizedLog(
+ uint32_t metadata,
pw_tokenizer_Token token,
pw_tokenizer_ArgTypes types,
...) {
va_list args;
va_start(args, types);
- EncodedMessage encoded(token, types, args);
+ pw::tokenizer::EncodedMessage<> encoded_message(token, types, args);
va_end(args);
- pw_tokenizer_HandleEncodedMessageWithPayload(
- payload, encoded.data_as_uint8(), encoded.size());
+ pw_log_tokenized_HandleLog(
+ metadata, encoded_message.data_as_uint8(), encoded_message.size());
}
-
-} // namespace tokenizer
-} // namespace pw
diff --git a/pw_log_tokenized/log_tokenized_test.cc b/pw_log_tokenized/log_tokenized_test.cc
index e926749f6..a8d4759b0 100644
--- a/pw_log_tokenized/log_tokenized_test.cc
+++ b/pw_log_tokenized/log_tokenized_test.cc
@@ -34,7 +34,8 @@ namespace pw::log_tokenized {
namespace {
TEST(LogTokenized, FormatString) {
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(63, 1023, "hello %d", 1);
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
+ 63, PW_LOG_MODULE_NAME, 1023, "hello %d", 1);
EXPECT_STREQ(last_log.format_string,
"■msg♦hello %d■module♦log module name!■file♦" __FILE__);
}
@@ -49,10 +50,11 @@ TEST(LogTokenized, LogMetadata_LevelTooLarge_Clamps) {
EXPECT_EQ(metadata.level(), 7u);
EXPECT_EQ(metadata.flags(), 0u);
EXPECT_EQ(metadata.module(), kModuleToken);
- EXPECT_TRUE(metadata.line_number() == 55u || metadata.line_number() == 45u);
+ EXPECT_EQ(metadata.line_number(), 1000u);
};
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(8, 0, "hello");
+#line 1000
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(8, PW_LOG_MODULE_NAME, 0, "");
check_metadata();
pw_log_tokenized_Test_LogMetadata_LevelTooLarge_Clamps();
@@ -65,10 +67,15 @@ TEST(LogTokenized, LogMetadata_TooManyFlags_Truncates) {
EXPECT_EQ(metadata.level(), 1u);
EXPECT_EQ(metadata.flags(), 0b11u);
EXPECT_EQ(metadata.module(), kModuleToken);
- EXPECT_TRUE(metadata.line_number() == 71u || metadata.line_number() == 49u);
+ EXPECT_EQ(metadata.line_number(), 1100u);
};
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(1, 0xFFFFFFFF, "hello");
+ // Keep statements on a single line, since GCC and Clang disagree about which
+ // line number to assign to multi-line macros.
+ // clang-format off
+#line 1100
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(1, PW_LOG_MODULE_NAME, 0xFFFFFFFF, "hello");
+ // clang-format on
check_metadata();
pw_log_tokenized_Test_LogMetadata_TooManyFlags_Truncates();
@@ -82,10 +89,13 @@ TEST(LogTokenized, LogMetadata_VariousValues) {
EXPECT_EQ(metadata.flags(), 3u);
EXPECT_EQ(metadata.module(), kModuleToken);
EXPECT_EQ(last_log.arg_count, 1u);
- EXPECT_TRUE(metadata.line_number() == 88u || metadata.line_number() == 53u);
+ EXPECT_EQ(metadata.line_number(), 1200u);
};
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(6, 3, "hello%s", "?");
+ // clang-format off
+#line 1200
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(6, PW_LOG_MODULE_NAME, 3, "hello%s", "?");
+ // clang-format on
check_metadata();
pw_log_tokenized_Test_LogMetadata_LogMetadata_VariousValues();
@@ -99,11 +109,11 @@ TEST(LogTokenized, LogMetadata_Zero) {
EXPECT_EQ(metadata.flags(), 0u);
EXPECT_EQ(metadata.module(), kModuleToken);
EXPECT_EQ(last_log.arg_count, 0u);
- EXPECT_TRUE(metadata.line_number() == 106u ||
- metadata.line_number() == 57u);
+ EXPECT_EQ(metadata.line_number(), 1300u);
};
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(0, 0, "hello");
+#line 1300
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(0, PW_LOG_MODULE_NAME, 0, "");
check_metadata();
pw_log_tokenized_Test_LogMetadata_LogMetadata_Zero();
@@ -112,27 +122,32 @@ TEST(LogTokenized, LogMetadata_Zero) {
TEST(LogTokenized, LogMetadata_MaxValues) {
#line 2047
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, 3, "hello %d", 1);
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, "name", 3, "hello %d", 1);
Metadata metadata = Metadata(last_log.metadata);
EXPECT_EQ(metadata.line_number(), 2047u);
EXPECT_EQ(metadata.level(), 7u);
EXPECT_EQ(metadata.flags(), 3u);
- EXPECT_EQ(metadata.module(), kModuleToken);
+ EXPECT_EQ(metadata.module(),
+ PW_TOKENIZER_STRING_TOKEN("name") &
+ ((1u << PW_LOG_TOKENIZED_MODULE_BITS) - 1));
EXPECT_EQ(last_log.arg_count, 1u);
}
TEST(LogTokenized, LogMetadata_LineNumberTooLarge_IsZero) {
#line 2048 // At 11 bits, the largest representable line is 2047
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, 3, "hello %d", 1);
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
+ 7, PW_LOG_MODULE_NAME, 3, "hello %d", 1);
EXPECT_EQ(Metadata(last_log.metadata).line_number(), 0u);
#line 2049
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, 3, "hello %d", 1);
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
+ 7, PW_LOG_MODULE_NAME, 3, "hello %d", 1);
EXPECT_EQ(Metadata(last_log.metadata).line_number(), 0u);
#line 99999
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(7, 3, "hello %d", 1);
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
+ 7, PW_LOG_MODULE_NAME, 3, "hello %d", 1);
EXPECT_EQ(Metadata(last_log.metadata).line_number(), 0u);
}
diff --git a/pw_log_tokenized/log_tokenized_test_c.c b/pw_log_tokenized/log_tokenized_test_c.c
index c0b28b79b..81667a7e0 100644
--- a/pw_log_tokenized/log_tokenized_test_c.c
+++ b/pw_log_tokenized/log_tokenized_test_c.c
@@ -42,17 +42,25 @@ void pw_log_tokenized_CaptureArgs(uintptr_t payload,
// These functions correspond to tests in log_tokenized_test.cc. The tests call
// these functions and check the results.
void pw_log_tokenized_Test_LogMetadata_LevelTooLarge_Clamps(void) {
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(8, 0, "hello");
+#line 1000
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(8, PW_LOG_MODULE_NAME, 0, "");
}
void pw_log_tokenized_Test_LogMetadata_TooManyFlags_Truncates(void) {
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(1, 0xFFFFFFFF, "hello");
+// clang-format off
+#line 1100
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(1, PW_LOG_MODULE_NAME, 0xFFFFFFFF, "hello");
+ // clang-format on
}
void pw_log_tokenized_Test_LogMetadata_LogMetadata_VariousValues(void) {
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(6, 3, "hello%s", "?");
+// clang-format off
+#line 1200
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(6, PW_LOG_MODULE_NAME, 3, "hello%s", "?");
+ // clang-format on
}
void pw_log_tokenized_Test_LogMetadata_LogMetadata_Zero(void) {
- PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(0, 0, "hello");
+#line 1300
+ PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD(0, PW_LOG_MODULE_NAME, 0, "");
}
diff --git a/pw_log_tokenized/metadata_test.cc b/pw_log_tokenized/metadata_test.cc
index 3a45ceeae..4d84c5aaf 100644
--- a/pw_log_tokenized/metadata_test.cc
+++ b/pw_log_tokenized/metadata_test.cc
@@ -20,7 +20,7 @@ namespace pw::log_tokenized {
namespace {
TEST(Metadata, NoLineBits) {
- using NoLineBits = internal::GenericMetadata<6, 0, 10, 16>;
+ using NoLineBits = GenericMetadata<6, 0, 10, 16>;
constexpr NoLineBits test1 = NoLineBits::Set<0, 0, 0>();
static_assert(test1.level() == 0);
@@ -42,7 +42,7 @@ TEST(Metadata, NoLineBits) {
}
TEST(Metadata, NoFlagBits) {
- using NoFlagBits = internal::GenericMetadata<3, 13, 0, 16>;
+ using NoFlagBits = GenericMetadata<3, 13, 0, 16>;
constexpr NoFlagBits test1 = NoFlagBits::Set<0, 0, 0, 0>();
static_assert(test1.level() == 0);
@@ -63,5 +63,50 @@ TEST(Metadata, NoFlagBits) {
static_assert(test3.line_number() == (1 << 13) - 1);
}
+TEST(Metadata, EncodedValue_Zero) {
+ constexpr Metadata test1 = Metadata::Set<0, 0, 0, 0>();
+ static_assert(test1.value() == 0);
+}
+
+TEST(Metadata, EncodedValue_Nonzero) {
+ constexpr size_t kExpectedLevel = 3;
+ constexpr size_t kExpectedLine = 2022;
+ constexpr size_t kExpectedFlags = 0b10;
+ constexpr size_t kExpectedModule = 1337;
+ constexpr size_t kExpectedValue =
+ (kExpectedLevel) | (kExpectedLine << PW_LOG_TOKENIZED_LEVEL_BITS) |
+ (kExpectedFlags << (PW_LOG_TOKENIZED_LEVEL_BITS +
+ PW_LOG_TOKENIZED_LINE_BITS)) |
+ (kExpectedModule << (PW_LOG_TOKENIZED_LEVEL_BITS +
+ PW_LOG_TOKENIZED_LINE_BITS +
+ PW_LOG_TOKENIZED_FLAG_BITS));
+ constexpr Metadata test = Metadata::
+ Set<kExpectedLevel, kExpectedModule, kExpectedFlags, kExpectedLine>();
+ static_assert(test.value() == kExpectedValue);
+}
+
+TEST(Metadata, EncodedValue_NonzeroConstructor) {
+ constexpr size_t kExpectedLevel = 1;
+ constexpr size_t kExpectedLine = 99;
+ constexpr size_t kExpectedFlags = 0b11;
+ constexpr size_t kExpectedModule = 8900;
+ constexpr size_t kExpectedValue =
+ (kExpectedLevel) | (kExpectedLine << PW_LOG_TOKENIZED_LEVEL_BITS) |
+ (kExpectedFlags << (PW_LOG_TOKENIZED_LEVEL_BITS +
+ PW_LOG_TOKENIZED_LINE_BITS)) |
+ (kExpectedModule << (PW_LOG_TOKENIZED_LEVEL_BITS +
+ PW_LOG_TOKENIZED_LINE_BITS +
+ PW_LOG_TOKENIZED_FLAG_BITS));
+ constexpr Metadata test =
+ Metadata(kExpectedLevel, kExpectedModule, kExpectedFlags, kExpectedLine);
+ static_assert(test.value() == kExpectedValue);
+}
+
+TEST(Metadata, EncodedValue_Overflow) {
+ constexpr size_t kExpectedLevel = 144;
+ constexpr Metadata test = Metadata(kExpectedLevel, 0, 0, 0);
+ static_assert(test.value() == 0);
+}
+
} // namespace
} // namespace pw::log_tokenized
diff --git a/pw_log_tokenized/public/pw_log_tokenized/config.h b/pw_log_tokenized/public/pw_log_tokenized/config.h
index de3351026..647598177 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/config.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/config.h
@@ -41,10 +41,6 @@
#define PW_LOG_TOKENIZED_LEVEL_BITS PW_LOG_LEVEL_BITS
#endif // PW_LOG_TOKENIZED_LEVEL_BITS
-// Bits to allocate for the line number. Defaults to 11 (up to line 2047). If
-// the line number is too large to be represented by this field, line is
-// reported as 0.
-//
// Including the line number can slightly increase code size. Without the line
// number, the log metadata argument is the same for all logs with the same
// level and flags. With the line number, each metadata value is unique and must
@@ -72,11 +68,3 @@
static_assert((PW_LOG_TOKENIZED_LEVEL_BITS + PW_LOG_TOKENIZED_LINE_BITS +
PW_LOG_TOKENIZED_FLAG_BITS + PW_LOG_TOKENIZED_MODULE_BITS) == 32,
"Log metadata fields must use 32 bits");
-
-// The macro to use to tokenize the log and its arguments. Defaults to
-// PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD. Projects may define their own
-// version of this macro that uses a different underlying function, if desired.
-#ifndef PW_LOG_TOKENIZED_ENCODE_MESSAGE
-#define PW_LOG_TOKENIZED_ENCODE_MESSAGE \
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD
-#endif // PW_LOG_TOKENIZED_ENCODE_MESSAGE
diff --git a/pw_log_tokenized/public/pw_log_tokenized/handler.h b/pw_log_tokenized/public/pw_log_tokenized/handler.h
new file mode 100644
index 000000000..40ac0f7d8
--- /dev/null
+++ b/pw_log_tokenized/public/pw_log_tokenized/handler.h
@@ -0,0 +1,31 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "pw_preprocessor/util.h"
+
+PW_EXTERN_C_START
+
+/// Function that is called for each log message. The metadata `uint32_t` can be
+/// converted to a @cpp_type{pw::log_tokenized::Metadata}. The message is passed
+/// as a pointer to a buffer and a size. The pointer is invalidated after this
+/// function returns, so the buffer must be copied.
+void pw_log_tokenized_HandleLog(uint32_t metadata,
+ const uint8_t encoded_message[],
+ size_t size_bytes);
+
+PW_EXTERN_C_END
diff --git a/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h b/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
index 110d5425f..61454a741 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/log_tokenized.h
@@ -16,30 +16,36 @@
#include <stdint.h>
#include "pw_log_tokenized/config.h"
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
+#include "pw_preprocessor/util.h"
+#include "pw_tokenizer/tokenize.h"
-// TODO(hepler): Remove this include.
+// TODO(hepler): Remove these includes.
#ifdef __cplusplus
#include "pw_log_tokenized/metadata.h"
#endif // __cplusplus
-// This macro implements PW_LOG using
-// PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD or an equivalent alternate macro
-// provided by PW_LOG_TOKENIZED_ENCODE_MESSAGE. The log level, module token, and
-// flags are packed into the payload argument.
+#ifdef _PW_LOG_TOKENIZED_GLOBAL_HANDLER_BACKWARDS_COMPAT
+#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
+#endif // _PW_LOG_TOKENIZED_GLOBAL_HANDLER_BACKWARDS_COMPAT
+
+#undef _PW_LOG_TOKENIZED_GLOBAL_HANDLER_BACKWARDS_COMPAT
+
+// This macro implements PW_LOG using pw_tokenizer. Users must implement
+// pw_log_tokenized_HandleLog(uint32_t metadata, uint8_t* buffer, size_t size).
+// The log level, module token, and flags are packed into the metadata argument.
//
// Two strings are tokenized in this macro:
//
// - The log format string, tokenized in the default tokenizer domain.
-// - PW_LOG_MODULE_NAME, masked to 16 bits and tokenized in the
+// - Log module name, masked to 16 bits and tokenized in the
// "pw_log_module_names" tokenizer domain.
//
-// To use this macro, implement pw_tokenizer_HandleEncodedMessageWithPayload,
-// which is defined in pw_tokenizer/tokenize.h. The log metadata can be accessed
-// using pw::log_tokenized::Metadata. For example:
+// To use this macro, implement pw_log_tokenized_HandleLog(), which is defined
+// in pw_log_tokenized/handler.h. The log metadata can be accessed using
+// pw::log_tokenized::Metadata. For example:
//
-// extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
-// pw_tokenizer_Payload payload, const uint8_t data[], size_t size) {
+// extern "C" void pw_log_tokenized_HandleLog(
+// uint32_t payload, const uint8_t data[], size_t size) {
// pw::log_tokenized::Metadata metadata(payload);
//
// if (metadata.level() >= kLogLevel && ModuleEnabled(metadata.module())) {
@@ -48,12 +54,12 @@
// }
//
#define PW_LOG_TOKENIZED_TO_GLOBAL_HANDLER_WITH_PAYLOAD( \
- level, flags, message, ...) \
+ level, module, flags, message, ...) \
do { \
_PW_TOKENIZER_CONST uintptr_t _pw_log_tokenized_module_token = \
PW_TOKENIZE_STRING_MASK("pw_log_module_names", \
((1u << PW_LOG_TOKENIZED_MODULE_BITS) - 1u), \
- PW_LOG_MODULE_NAME); \
+ module); \
const uintptr_t _pw_log_tokenized_level = level; \
PW_LOG_TOKENIZED_ENCODE_MESSAGE( \
(_PW_LOG_TOKENIZED_LEVEL(_pw_log_tokenized_level) | \
@@ -101,3 +107,22 @@
PW_LOG_TOKENIZED_LINE_BITS + \
PW_LOG_TOKENIZED_FLAG_BITS)))
#endif // PW_LOG_TOKENIZED_MODULE_BITS
+
+#define PW_LOG_TOKENIZED_ENCODE_MESSAGE(metadata, format, ...) \
+ do { \
+ PW_TOKENIZE_FORMAT_STRING( \
+ PW_TOKENIZER_DEFAULT_DOMAIN, UINT32_MAX, format, __VA_ARGS__); \
+ _pw_log_tokenized_EncodeTokenizedLog(metadata, \
+ _pw_tokenizer_token, \
+ PW_TOKENIZER_ARG_TYPES(__VA_ARGS__) \
+ PW_COMMA_ARGS(__VA_ARGS__)); \
+ } while (0)
+
+PW_EXTERN_C_START
+
+void _pw_log_tokenized_EncodeTokenizedLog(uint32_t metadata,
+ pw_tokenizer_Token token,
+ pw_tokenizer_ArgTypes types,
+ ...);
+
+PW_EXTERN_C_END
diff --git a/pw_log_tokenized/public/pw_log_tokenized/metadata.h b/pw_log_tokenized/public/pw_log_tokenized/metadata.h
index 86e6364bd..5d78e3b38 100644
--- a/pw_log_tokenized/public/pw_log_tokenized/metadata.h
+++ b/pw_log_tokenized/public/pw_log_tokenized/metadata.h
@@ -41,8 +41,17 @@ class BitField<T, 0, kShift> {
static constexpr T Shift(T) { return 0; }
};
+} // namespace internal
+
// This class, which is aliased to pw::log_tokenized::Metadata below, is used to
// access the log metadata packed into the tokenizer's payload argument.
+//
+/// `GenericMetadata` facilitates the creation and interpretation of packed
+/// log metadata payloads. The `GenericMetadata` class allows flags, log level,
+/// line number, and a module identifier to be packed into bit fields of
+/// configurable size.
+///
+/// Typically, the `Metadata` alias should be used instead.
template <unsigned kLevelBits,
unsigned kLineBits,
unsigned kFlagBits,
@@ -57,43 +66,62 @@ class GenericMetadata {
static_assert(flags < (1 << kFlagBits), "The flags are too large!");
static_assert(module < (1 << kModuleBits), "The module is too large!");
- return GenericMetadata(Level::Shift(log_level) | Module::Shift(module) |
- Flags::Shift(flags) | Line::Shift(line));
+ return GenericMetadata(BitsFromMetadata(log_level, module, flags, line));
}
- constexpr GenericMetadata(T value) : bits_(value) {}
+ /// Only use this constructor for creating metadata from runtime values. This
+ /// constructor is unable to warn at compilation when values will not fit in
+ /// the specified bit field widths.
+ constexpr GenericMetadata(T log_level, T module, T flags, T line)
+ : value_(BitsFromMetadata(log_level, module, flags, line)) {}
+
+ constexpr GenericMetadata(T value) : value_(value) {}
- // The log level of this message.
- constexpr T level() const { return Level::Get(bits_); }
+ /// The log level of this message.
+ constexpr T level() const { return Level::Get(value_); }
- // The line number of the log call. The first line in a file is 1. If the line
- // number is 0, it was too large to be stored.
- constexpr T line_number() const { return Line::Get(bits_); }
+ /// The line number of the log call. The first line in a file is 1. If the
+ /// line number is 0, it was too large to be stored.
+ constexpr T line_number() const { return Line::Get(value_); }
- // The flags provided to the log call.
- constexpr T flags() const { return Flags::Get(bits_); }
+ /// The flags provided to the log call.
+ constexpr T flags() const { return Flags::Get(value_); }
- // The 16 bit tokenized version of the module name (PW_LOG_MODULE_NAME).
- constexpr T module() const { return Module::Get(bits_); }
+ /// The 16-bit tokenized version of the module name
+ /// (@c_macro{PW_LOG_MODULE_NAME}).
+ constexpr T module() const { return Module::Get(value_); }
+
+ /// The underlying packed metadata.
+ constexpr T value() const { return value_; }
private:
- using Level = BitField<T, kLevelBits, 0>;
- using Line = BitField<T, kLineBits, kLevelBits>;
- using Flags = BitField<T, kFlagBits, kLevelBits + kLineBits>;
- using Module = BitField<T, kModuleBits, kLevelBits + kLineBits + kFlagBits>;
+ using Level = internal::BitField<T, kLevelBits, 0>;
+ using Line = internal::BitField<T, kLineBits, kLevelBits>;
+ using Flags = internal::BitField<T, kFlagBits, kLevelBits + kLineBits>;
+ using Module =
+ internal::BitField<T, kModuleBits, kLevelBits + kLineBits + kFlagBits>;
+
+ static constexpr T BitsFromMetadata(T log_level, T module, T flags, T line) {
+ return Level::Shift(log_level) | Module::Shift(module) |
+ Flags::Shift(flags) | Line::Shift(line);
+ }
- T bits_;
+ T value_;
static_assert(kLevelBits + kLineBits + kFlagBits + kModuleBits <=
- sizeof(bits_) * 8);
+ sizeof(value_) * 8);
};
-} // namespace internal
-
-using Metadata = internal::GenericMetadata<PW_LOG_TOKENIZED_LEVEL_BITS,
- PW_LOG_TOKENIZED_LINE_BITS,
- PW_LOG_TOKENIZED_FLAG_BITS,
- PW_LOG_TOKENIZED_MODULE_BITS>;
+/// The `Metadata` alias simplifies the bit field width templatization of
+/// `GenericMetadata` by pulling from this module's configuration options. In
+/// most cases, it's recommended to use `Metadata` to create or read metadata
+/// payloads.
+///
+/// A `Metadata` object can be created from a `uint32_t`.
+using Metadata = GenericMetadata<PW_LOG_TOKENIZED_LEVEL_BITS,
+ PW_LOG_TOKENIZED_LINE_BITS,
+ PW_LOG_TOKENIZED_FLAG_BITS,
+ PW_LOG_TOKENIZED_MODULE_BITS>;
} // namespace log_tokenized
} // namespace pw
diff --git a/pw_log_tokenized/py/BUILD.bazel b/pw_log_tokenized/py/BUILD.bazel
new file mode 100644
index 000000000..eca513e05
--- /dev/null
+++ b/pw_log_tokenized/py/BUILD.bazel
@@ -0,0 +1,34 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+py_library(
+ name = "pw_log_tokenized",
+ srcs = ["pw_log_tokenized/__init__.py"],
+ imports = ["."],
+ visibility = ["//visibility:public"],
+)
+
+py_test(
+ name = "format_string_test",
+ size = "small",
+ srcs = ["format_string_test.py"],
+ deps = [":pw_log_tokenized"],
+)
+
+py_test(
+ name = "metadata_test",
+ size = "small",
+ srcs = ["metadata_test.py"],
+ deps = [":pw_log_tokenized"],
+)
diff --git a/pw_log_tokenized/py/BUILD.gn b/pw_log_tokenized/py/BUILD.gn
index 8931cb2d2..c9e376eec 100644
--- a/pw_log_tokenized/py/BUILD.gn
+++ b/pw_log_tokenized/py/BUILD.gn
@@ -28,4 +28,5 @@ pw_python_package("py") {
"metadata_test.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_log_tokenized/py/format_string_test.py b/pw_log_tokenized/py/format_string_test.py
index af49bf200..cf04c788d 100644
--- a/pw_log_tokenized/py/format_string_test.py
+++ b/pw_log_tokenized/py/format_string_test.py
@@ -21,9 +21,11 @@ from pw_log_tokenized import FormatStringWithMetadata
class TestFormatStringWithMetadata(unittest.TestCase):
"""Tests extracting metadata from a pw_log_tokenized-style format string."""
+
def test_all_fields(self):
log = FormatStringWithMetadata(
- '■msg♦hello %d■file♦__FILE__■module♦log module name!')
+ '■msg♦hello %d■file♦__FILE__■module♦log module name!'
+ )
self.assertEqual(log.message, 'hello %d')
self.assertEqual(log.module, 'log module name!')
self.assertEqual(log.file, '__FILE__')
diff --git a/pw_log_tokenized/py/metadata_test.py b/pw_log_tokenized/py/metadata_test.py
index 1073a928d..83eb90421 100644
--- a/pw_log_tokenized/py/metadata_test.py
+++ b/pw_log_tokenized/py/metadata_test.py
@@ -20,6 +20,7 @@ from pw_log_tokenized import Metadata
class TestMetadata(unittest.TestCase):
"""Tests extracting fields from a pw_log_tokenized packed metadata value."""
+
def test_zero(self):
metadata = Metadata(0)
self.assertEqual(metadata.log_level, 0)
@@ -28,22 +29,22 @@ class TestMetadata(unittest.TestCase):
self.assertEqual(metadata.module_token, 0)
def test_various(self):
- metadata = Metadata(0xABCD << 16 | 1 << 14 | 1234 << 3 | 5,
- log_bits=3,
- line_bits=11,
- flag_bits=2,
- module_bits=16)
+ metadata = Metadata(
+ 0xABCD << 16 | 1 << 14 | 1234 << 3 | 5,
+ log_bits=3,
+ line_bits=11,
+ flag_bits=2,
+ module_bits=16,
+ )
self.assertEqual(metadata.log_level, 5)
self.assertEqual(metadata.line, 1234)
self.assertEqual(metadata.flags, 1)
self.assertEqual(metadata.module_token, 0xABCD)
def test_max(self):
- metadata = Metadata(0xFFFFFFFF,
- log_bits=3,
- line_bits=11,
- flag_bits=2,
- module_bits=16)
+ metadata = Metadata(
+ 0xFFFFFFFF, log_bits=3, line_bits=11, flag_bits=2, module_bits=16
+ )
self.assertEqual(metadata.log_level, 7)
self.assertEqual(metadata.line, 2047)
self.assertEqual(metadata.flags, 3)
diff --git a/pw_log_tokenized/py/pw_log_tokenized/__init__.py b/pw_log_tokenized/py/pw_log_tokenized/__init__.py
index d0cc1d14f..9fff100cc 100644
--- a/pw_log_tokenized/py/pw_log_tokenized/__init__.py
+++ b/pw_log_tokenized/py/pw_log_tokenized/__init__.py
@@ -13,9 +13,9 @@
# the License.
"""Tools for working with tokenized logs."""
-from dataclasses import dataclass
+from dataclasses import dataclass, asdict
import re
-from typing import Dict, Mapping
+from typing import Dict, Mapping, Iterator
def _mask(value: int, start: int, count: int) -> int:
@@ -23,33 +23,38 @@ def _mask(value: int, start: int, count: int) -> int:
return (value & (mask << start)) >> start
+@dataclass
class Metadata:
"""Parses the metadata payload used by pw_log_tokenized."""
- def __init__(self,
- value: int,
- *,
- log_bits: int = 3,
- line_bits: int = 11,
- flag_bits: int = 2,
- module_bits: int = 16) -> None:
- self.value = value
-
- self.log_level = _mask(value, 0, log_bits)
- self.line = _mask(value, log_bits, line_bits)
- self.flags = _mask(value, log_bits + line_bits, flag_bits)
- self.module_token = _mask(value, log_bits + line_bits + flag_bits,
- module_bits)
- def __repr__(self) -> str:
- return (f'{type(self).__name__}('
- f'log_level={self.log_level}, '
- f'line={self.line}, '
- f'flags={self.flags}, '
- f'module_token={self.module_token})')
+ value: int
+ log_bits: int = 3
+ line_bits: int = 11
+ flag_bits: int = 2
+ module_bits: int = 16
+
+ def __post_init__(self):
+ self.log_level = _mask(self.value, 0, self.log_bits)
+ self.line = _mask(self.value, self.log_bits, self.line_bits)
+ self.flags = _mask(
+ self.value, self.log_bits + self.line_bits, self.flag_bits
+ )
+ self.module_token = _mask(
+ self.value,
+ self.log_bits + self.line_bits + self.flag_bits,
+ self.module_bits,
+ )
+
+ def __iter__(self):
+ return iter(asdict(self).items())
+
+ def __dict__(self):
+ return asdict(self)
class FormatStringWithMetadata:
"""Parses metadata from a log format string with metadata fields."""
+
_FIELD_KEY = re.compile(r'■([a-zA-Z]\w*)♦', flags=re.ASCII)
def __init__(self, string: str) -> None:
diff --git a/pw_log_zephyr/BUILD.gn b/pw_log_zephyr/BUILD.gn
index 68ffb756b..7eccfebeb 100644
--- a/pw_log_zephyr/BUILD.gn
+++ b/pw_log_zephyr/BUILD.gn
@@ -12,4 +12,15 @@
# License for the specific language governing permissions and limitations under
# the License.
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
# Zephyr only uses CMake, so this file is empty.
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_log_zephyr/CMakeLists.txt b/pw_log_zephyr/CMakeLists.txt
index c4c71789e..3f39df97d 100644
--- a/pw_log_zephyr/CMakeLists.txt
+++ b/pw_log_zephyr/CMakeLists.txt
@@ -14,17 +14,34 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-if(NOT CONFIG_PIGWEED_LOG)
- return()
+if(CONFIG_PIGWEED_LOG_ZEPHYR)
+ pw_add_library(pw_log_zephyr STATIC
+ HEADERS
+ public/pw_log_zephyr/log_zephyr.h
+ public/pw_log_zephyr/config.h
+ public_overrides/pw_log_backend/log_backend.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_log.facade
+ zephyr_interface
+ SOURCES
+ log_zephyr.cc
+ PRIVATE_DEPS
+ pw_preprocessor
+ )
+ zephyr_link_libraries(pw_log_zephyr)
endif()
-pw_auto_add_simple_module(pw_log_zephyr
- IMPLEMENTS_FACADE
- pw_log
- PUBLIC_DEPS
- zephyr_interface
- PRIVATE_DEPS
- pw_preprocessor
-)
-pw_set_backend(pw_log pw_log_zephyr)
-zephyr_link_libraries(pw_log_zephyr)
+if(CONFIG_PIGWEED_LOG_TOKENIZED)
+ pw_add_library(pw_log_zephyr.tokenized_handler STATIC
+ SOURCES
+ pw_log_zephyr_tokenized_handler.cc
+ PRIVATE_DEPS
+ pw_log_tokenized.handler
+ pw_tokenizer
+ )
+ zephyr_link_libraries(pw_log pw_log_zephyr.tokenized_handler)
+ zephyr_include_directories(public_overrides)
+endif()
diff --git a/pw_log_zephyr/Kconfig b/pw_log_zephyr/Kconfig
index 472b54c64..59a971be9 100644
--- a/pw_log_zephyr/Kconfig
+++ b/pw_log_zephyr/Kconfig
@@ -12,17 +12,41 @@
# License for the specific language governing permissions and limitations under
# the License.
-menuconfig PIGWEED_LOG
- bool "Enable Pigweed logging library (pw_log)"
+choice PIGWEED_LOG
+ prompt "Logging backend used"
+ help
+ The type of Zephyr pw_log backend to use.
+
+config PIGWEED_LOG_ZEPHYR
+ bool "Zephyr logging for PW_LOG_* statements"
select PIGWEED_PREPROCESSOR
help
Once the Pigweed logging is enabled, all Pigweed logs via PW_LOG_*() will
- go to the "pigweed" Zephyr logging module.
+ be routed to the Zephyr logging system. This means that:
+ - PW_LOG_LEVEL_DEBUG maps to Zephyr's LOG_LEVEL_DBG
+ - PW_LOG_LEVEL_INFO maps to Zephyr's LOG_LEVEL_INF
+ - PW_LOG_LEVEL_WARN maps to Zephyr's LOG_LEVEL_WRN
+ - PW_LOG_LEVEL_ERROR maps to Zephyr's LOG_LEVEL_ERR
+ - PW_LOG_LEVEL_CRITICAL maps to Zephyr's LOG_LEVEL_ERR
+ - PW_LOG_LEVEL_FATAL maps to Zephyr's LOG_LEVEL_ERR
+
+config PIGWEED_LOG_TOKENIZED
+ bool "Maps all Zephyr log macros to tokenized PW_LOG_* macros"
+ select PIGWEED_PREPROCESSOR
+ select PIGWEED_TOKENIZER
+ select LOG_CUSTOM_HEADER
+ help
+ Map all the Zephyr log macros to use Pigweed's then use the
+ 'pw_log_tokenized' target as the logging backend in order to
+ automatically tokenize all the logging strings. This means that Pigweed
+ will also tokenize all of Zephyr's logging statements.
+
+endchoice
-if PIGWEED_LOG
+if PIGWEED_LOG_ZEPHYR || PIGWEED_LOG_TOKENIZED
module = PIGWEED
module-str = "pigweed"
source "subsys/logging/Kconfig.template.log_config"
-endif # PIGWEED_LOG
+endif # PIGWEED_LOG_ZEPHYR || PIGWEED_LOG_TOKENIZED
diff --git a/pw_log_zephyr/docs.rst b/pw_log_zephyr/docs.rst
index 84b8db9ac..beb6a691f 100644
--- a/pw_log_zephyr/docs.rst
+++ b/pw_log_zephyr/docs.rst
@@ -1,17 +1,31 @@
.. _module-pw_log_zephyr:
-================
+=============
pw_log_zephyr
-================
+=============
--------
Overview
--------
-This interrupt backend implements the ``pw_log`` facade. To enable, set
-``CONFIG_PIGWEED_LOG=y``. After that, logging can be controlled via the standard
-`Kconfig options <https://docs.zephyrproject.org/latest/reference/logging/index.html#global-kconfig-options>`_.
-All logs made through `PW_LOG_*` are logged to the Zephyr logging module
-``pigweed``.
+This interrupt backend implements the ``pw_log`` facade. Currently, two
+separate Pigweed backends are implemented. One that uses the plain Zephyr
+logging framework and routes Pigweed's logs to Zephyr. While another maps
+the Zephyr logging macros to Pigweed's tokenized logging.
+
+Using Zephyr logging
+--------------------
+To enable, set ``CONFIG_PIGWEED_LOG_ZEPHYR=y``. After that, logging can be
+controlled via the standard `Kconfig options`_. All logs made through
+`PW_LOG_*` are logged to the Zephyr logging module ``pigweed``. In this
+model, the Zephyr logging is set as ``pw_log``'s backend.
+
+Using Pigweed tokenized logging
+-------------------------------
+Using the pigweed logging can be done by enabling
+``CONFIG_PIGWEED_LOG_TOKENIZED=y``. At that point ``pw_log_tokenized`` is set
+as the backedn for ``pw_log`` and all Zephyr logs are routed to Pigweed's
+logging facade. This means that any logging statements made in Zephyr itself
+are also tokenized.
Setting the log level
---------------------
@@ -37,3 +51,5 @@ levels to their closest Zephyr counterparts:
Alternatively, it is also possible to set the Zephyr logging level directly via
``CONFIG_PIGWEED_LOG_LEVEL``.
+
+.. _`Kconfig options`: https://docs.zephyrproject.org/latest/reference/logging/index.html#global-kconfig-options
diff --git a/pw_log_zephyr/log_zephyr.cc b/pw_log_zephyr/log_zephyr.cc
index 053bf5403..3990d5d15 100644
--- a/pw_log_zephyr/log_zephyr.cc
+++ b/pw_log_zephyr/log_zephyr.cc
@@ -12,7 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include <logging/log.h>
+#include <zephyr/logging/log.h>
#include "pw_log_zephyr/config.h"
diff --git a/pw_log_zephyr/public/pw_log_zephyr/log_zephyr.h b/pw_log_zephyr/public/pw_log_zephyr/log_zephyr.h
index 5f0b88e7f..fbaba30e3 100644
--- a/pw_log_zephyr/public/pw_log_zephyr/log_zephyr.h
+++ b/pw_log_zephyr/public/pw_log_zephyr/log_zephyr.h
@@ -13,15 +13,11 @@
// the License.
#pragma once
-#include <logging/log.h>
-#include <logging/log_ctrl.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/logging/log_ctrl.h>
#include "pw_log_zephyr/config.h"
-#ifndef PW_LOG_MODULE_NAME
-#define PW_LOG_MODULE_NAME ""
-#endif
-
// If the consumer defined PW_LOG_LEVEL use it, otherwise fallback to the global
// CONFIG_PIGWEED_LOG_LEVEL set by Kconfig.
#ifdef PW_LOG_LEVEL
@@ -47,26 +43,26 @@
LOG_MODULE_DECLARE(PW_LOG_ZEPHYR_MODULE_NAME, LOG_LEVEL);
-#define PW_HANDLE_LOG(level, flags, ...) \
- do { \
- switch (level) { \
- case PW_LOG_LEVEL_INFO: \
- LOG_INF(PW_LOG_MODULE_NAME " " __VA_ARGS__); \
- break; \
- case PW_LOG_LEVEL_WARN: \
- LOG_WRN(PW_LOG_MODULE_NAME " " __VA_ARGS__); \
- break; \
- case PW_LOG_LEVEL_ERROR: \
- case PW_LOG_LEVEL_CRITICAL: \
- LOG_ERR(PW_LOG_MODULE_NAME " " __VA_ARGS__); \
- break; \
- case PW_LOG_LEVEL_FATAL: \
- LOG_ERR(PW_LOG_MODULE_NAME " " __VA_ARGS__); \
- LOG_PANIC(); \
- break; \
- case PW_LOG_LEVEL_DEBUG: \
- default: \
- LOG_DBG(PW_LOG_MODULE_NAME " " __VA_ARGS__); \
- break; \
- } \
+#define PW_HANDLE_LOG(level, module, flags, ...) \
+ do { \
+ switch (level) { \
+ case PW_LOG_LEVEL_INFO: \
+ LOG_INF(module " " __VA_ARGS__); \
+ break; \
+ case PW_LOG_LEVEL_WARN: \
+ LOG_WRN(module " " __VA_ARGS__); \
+ break; \
+ case PW_LOG_LEVEL_ERROR: \
+ case PW_LOG_LEVEL_CRITICAL: \
+ LOG_ERR(module " " __VA_ARGS__); \
+ break; \
+ case PW_LOG_LEVEL_FATAL: \
+ LOG_ERR(module " " __VA_ARGS__); \
+ LOG_PANIC(); \
+ break; \
+ case PW_LOG_LEVEL_DEBUG: \
+ default: \
+ LOG_DBG(module " " __VA_ARGS__); \
+ break; \
+ } \
} while (0)
diff --git a/pw_log_zephyr/public_overrides/zephyr_custom_log.h b/pw_log_zephyr/public_overrides/zephyr_custom_log.h
new file mode 100644
index 000000000..8485d5e02
--- /dev/null
+++ b/pw_log_zephyr/public_overrides/zephyr_custom_log.h
@@ -0,0 +1,35 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include <zephyr/sys/__assert.h>
+
+// If static_assert wasn't defined by zephyr/sys/__assert.h that means it's not
+// supported, just ignore it.
+#ifndef static_assert
+#define static_assert(...)
+#endif
+
+#include <pw_log/log.h>
+
+#undef LOG_DBG
+#undef LOG_INF
+#undef LOG_WRN
+#undef LOG_ERR
+
+#define LOG_DBG(format, ...) PW_LOG_DEBUG(format, ##__VA_ARGS__)
+#define LOG_INF(format, ...) PW_LOG_INFO(format, ##__VA_ARGS__)
+#define LOG_WRN(format, ...) PW_LOG_WARN(format, ##__VA_ARGS__)
+#define LOG_ERR(format, ...) PW_LOG_ERROR(format, ##__VA_ARGS__)
diff --git a/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc b/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc
new file mode 100644
index 000000000..bdc64ed49
--- /dev/null
+++ b/pw_log_zephyr/pw_log_zephyr_tokenized_handler.cc
@@ -0,0 +1,31 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 <zephyr/logging/log_backend.h>
+#include <zephyr/logging/log_msg.h>
+
+#include "pw_log_tokenized/handler.h"
+
+namespace pw::log_tokenized {
+
+extern "C" void pw_log_tokenized_HandleLog(uint32_t metadata,
+ const uint8_t log_buffer[],
+ size_t size_bytes) {
+ ARG_UNUSED(metadata);
+ ARG_UNUSED(log_buffer);
+ ARG_UNUSED(size_bytes);
+ // TODO(asemjonovs): implement this function
+}
+
+} // namespace pw::log_tokenized
diff --git a/pw_malloc/BUILD.gn b/pw_malloc/BUILD.gn
index 5a700a8b1..5af660ef3 100644
--- a/pw_malloc/BUILD.gn
+++ b/pw_malloc/BUILD.gn
@@ -19,6 +19,7 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_malloc/backend.gni")
import("$dir_pw_unit_test/test.gni")
+import("$dir_pw_unit_test/test.gni")
config("default_config") {
include_dirs = [ "public" ]
@@ -52,3 +53,6 @@ pw_facade("pw_malloc") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_malloc_freelist/BUILD.bazel b/pw_malloc_freelist/BUILD.bazel
index 889515d94..c0e6a8c9c 100644
--- a/pw_malloc_freelist/BUILD.bazel
+++ b/pw_malloc_freelist/BUILD.bazel
@@ -37,6 +37,18 @@ pw_cc_library(
srcs = [
"freelist_malloc.cc",
],
+ linkopts = [
+ # Link options that replace dynamic memory operations in standard
+ # library with the pigweed malloc.
+ "-Wl,--wrap=malloc",
+ "-Wl,--wrap=free",
+ "-Wl,--wrap=realloc",
+ "-Wl,--wrap=calloc",
+ "-Wl,--wrap=_malloc_r",
+ "-Wl,--wrap=_realloc_r",
+ "-Wl,--wrap=_free_r",
+ "-Wl,--wrap=_calloc_r",
+ ],
deps = [
":headers",
"//pw_allocator:block",
@@ -51,6 +63,8 @@ pw_cc_test(
srcs = [
"freelist_malloc_test.cc",
],
+ # TODO(b/257530518): Get this test to work with Bazel.
+ tags = ["manual"],
deps = [
":headers",
":pw_malloc_freelist",
diff --git a/pw_malloc_freelist/BUILD.gn b/pw_malloc_freelist/BUILD.gn
index dfb642b1c..117b820c6 100644
--- a/pw_malloc_freelist/BUILD.gn
+++ b/pw_malloc_freelist/BUILD.gn
@@ -31,6 +31,7 @@ pw_source_set("pw_malloc_freelist") {
"$dir_pw_allocator:freelist_heap",
"$dir_pw_malloc:facade",
"$dir_pw_preprocessor",
+ dir_pw_span,
]
sources = [ "freelist_malloc.cc" ]
}
@@ -44,6 +45,7 @@ pw_test("freelist_malloc_test") {
deps = [
"$dir_pw_allocator",
"$dir_pw_malloc",
+ dir_pw_span,
]
sources = [ "freelist_malloc_test.cc" ]
}
diff --git a/pw_malloc_freelist/freelist_malloc.cc b/pw_malloc_freelist/freelist_malloc.cc
index 73b44399f..7ce27dac1 100644
--- a/pw_malloc_freelist/freelist_malloc.cc
+++ b/pw_malloc_freelist/freelist_malloc.cc
@@ -12,12 +12,11 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include <span>
-
#include "pw_allocator/freelist_heap.h"
#include "pw_malloc/malloc.h"
#include "pw_preprocessor/compiler.h"
#include "pw_preprocessor/util.h"
+#include "pw_span/span.h"
namespace {
std::aligned_storage_t<sizeof(pw::allocator::FreeListHeapBuffer<>),
@@ -31,9 +30,9 @@ extern "C" {
#endif // __cplusplus
// Define the global heap variables.
void pw_MallocInit(uint8_t* heap_low_addr, uint8_t* heap_high_addr) {
- std::span<std::byte> pw_allocator_freelist_raw_heap =
- std::span(reinterpret_cast<std::byte*>(heap_low_addr),
- heap_high_addr - heap_low_addr);
+ pw::span<std::byte> pw_allocator_freelist_raw_heap =
+ pw::span(reinterpret_cast<std::byte*>(heap_low_addr),
+ heap_high_addr - heap_low_addr);
pw_freelist_heap = new (&buf)
pw::allocator::FreeListHeapBuffer(pw_allocator_freelist_raw_heap);
}
diff --git a/pw_malloc_freelist/freelist_malloc_test.cc b/pw_malloc_freelist/freelist_malloc_test.cc
index ec4bf77de..17c787b9f 100644
--- a/pw_malloc_freelist/freelist_malloc_test.cc
+++ b/pw_malloc_freelist/freelist_malloc_test.cc
@@ -15,10 +15,10 @@
#include "pw_malloc_freelist/freelist_malloc.h"
#include <memory>
-#include <span>
#include "gtest/gtest.h"
#include "pw_allocator/freelist_heap.h"
+#include "pw_span/span.h"
namespace pw::allocator {
diff --git a/pw_metric/BUILD.bazel b/pw_metric/BUILD.bazel
index 5c293f3cf..2eae45c18 100644
--- a/pw_metric/BUILD.bazel
+++ b/pw_metric/BUILD.bazel
@@ -17,6 +17,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -50,17 +52,76 @@ pw_cc_library(
],
)
+# Common MetricWalker/MetricWriter used by RPC service.
+pw_cc_library(
+ name = "metric_walker",
+ hdrs = ["pw_metric_private/metric_walker.h"],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":metric",
+ "//pw_assert",
+ "//pw_containers",
+ "//pw_status",
+ "//pw_tokenizer",
+ ],
+)
+
pw_cc_library(
name = "metric_service_nanopb",
srcs = ["metric_service_nanopb.cc"],
- hdrs = [
- "public/pw_metric/metric_service_nanopb.h",
+ hdrs = ["public/pw_metric/metric_service_nanopb.h"],
+ # TODO(b/258078909): Get this target to build.
+ tags = ["manual"],
+ deps = [
+ ":metric",
+ ":metric_proto_cc.nanopb_rpc",
+ ":metric_walker",
+ ],
+)
+
+pw_cc_library(
+ name = "metric_service_pwpb",
+ srcs = ["metric_service_pwpb.cc"],
+ hdrs = ["public/pw_metric/metric_service_pwpb.h"],
+ includes = [
+ "metric_proto_cc.pwpb.pb/pw_metric",
+ "metric_proto_cc.raw_rpc.pb/pw_metric",
],
deps = [
":metric",
+ ":metric_proto_cc.pwpb",
+ ":metric_proto_cc.pwpb_rpc",
+ ":metric_proto_cc.raw_rpc",
+ ":metric_walker",
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_containers",
+ "//pw_preprocessor",
+ "//pw_rpc/raw:server_api",
+ "//pw_span",
+ "//pw_status",
+ ],
+)
+
+proto_library(
+ name = "metric_proto",
+ srcs = [
+ "pw_metric_proto/metric_service.proto",
],
)
+# TODO(b/241456982): Not expected to build yet.
+py_proto_library(
+ name = "metric_proto_py_pb2",
+ tags = ["manual"],
+ deps = [":metric_proto"],
+)
+
+pw_proto_library(
+ name = "metric_proto_cc",
+ deps = [":metric_proto"],
+)
+
pw_cc_test(
name = "metric_test",
srcs = [
@@ -86,7 +147,21 @@ pw_cc_test(
srcs = [
"metric_service_nanopb_test.cc",
],
+ # TODO(b/258078909): Get this target to build.
+ tags = ["manual"],
deps = [
":metric_service_nanopb",
],
)
+
+pw_cc_test(
+ name = "metric_service_pwpb_test",
+ srcs = [
+ "metric_service_pwpb_test.cc",
+ ],
+ deps = [
+ ":metric_service_pwpb",
+ "//pw_rpc/pwpb:test_method_context",
+ "//pw_rpc/raw:test_method_context",
+ ],
+)
diff --git a/pw_metric/BUILD.gn b/pw_metric/BUILD.gn
index 3fc335c38..26b2fea85 100644
--- a/pw_metric/BUILD.gn
+++ b/pw_metric/BUILD.gn
@@ -36,6 +36,7 @@ pw_source_set("pw_metric") {
dir_pw_log,
dir_pw_tokenizer,
]
+ deps = [ dir_pw_span ]
}
# This gives access to the "PW_METRIC_GLOBAL()" macros, for globally-registered
@@ -60,16 +61,32 @@ pw_proto_library("metric_service_proto") {
# TODO(keir): Consider moving the nanopb service into the nanopb/ directory
# instead of having it directly inside pw_metric/.
+
+# Common MetricWalker/MetricWriter used by RPC service.
+pw_source_set("metric_walker") {
+ visibility = [ ":*" ]
+ public = [ "pw_metric_private/metric_walker.h" ]
+ deps = [
+ ":pw_metric",
+ "$dir_pw_assert:assert",
+ "$dir_pw_containers",
+ "$dir_pw_status",
+ "$dir_pw_tokenizer",
+ ]
+}
+
if (dir_pw_third_party_nanopb != "") {
pw_source_set("metric_service_nanopb") {
public_configs = [ ":default_config" ]
public_deps = [
":metric_service_proto.nanopb_rpc",
":pw_metric",
+ dir_pw_span,
]
public = [ "public/pw_metric/metric_service_nanopb.h" ]
deps = [
":metric_service_proto.nanopb_rpc",
+ ":metric_walker",
"$dir_pw_containers:vector",
dir_pw_tokenizer,
]
@@ -86,12 +103,47 @@ if (dir_pw_third_party_nanopb != "") {
}
}
+pw_source_set("metric_service_pwpb") {
+ public_configs = [ ":default_config" ]
+ public_deps = [
+ ":metric_service_proto.pwpb_rpc",
+ ":metric_service_proto.raw_rpc",
+ ":metric_walker",
+ ":pw_metric",
+ "$dir_pw_bytes",
+ "$dir_pw_containers",
+ "$dir_pw_rpc/raw:server_api",
+ ]
+ public = [ "public/pw_metric/metric_service_pwpb.h" ]
+ deps = [
+ ":metric_service_proto.pwpb",
+ ":metric_service_proto.raw_rpc",
+ "$dir_pw_assert",
+ "$dir_pw_containers:vector",
+ "$dir_pw_preprocessor",
+ "$dir_pw_span",
+ "$dir_pw_status",
+ ]
+ sources = [ "metric_service_pwpb.cc" ]
+}
+
+pw_test("metric_service_pwpb_test") {
+ deps = [
+ ":global",
+ ":metric_service_pwpb",
+ "$dir_pw_rpc/pwpb:test_method_context",
+ "$dir_pw_rpc/raw:test_method_context",
+ ]
+ sources = [ "metric_service_pwpb_test.cc" ]
+}
+
################################################################################
pw_test_group("tests") {
tests = [
":metric_test",
":global_test",
+ ":metric_service_pwpb_test",
]
if (dir_pw_third_party_nanopb != "") {
tests += [ ":metric_service_nanopb_test" ]
@@ -108,13 +160,9 @@ pw_test("global_test") {
deps = [ ":global" ]
}
-pw_size_report("metric_size_report") {
+pw_size_diff("metric_size_report") {
title = "Typical pw_metric use (no RPC service)"
- # To see all the symbols, uncomment the following:
- # Note: The size report RST table won't be generated when full_report = true.
- #full_report = true
-
binaries = [
{
target = "size_report:one_metric"
diff --git a/pw_metric/CMakeLists.txt b/pw_metric/CMakeLists.txt
new file mode 100644
index 000000000..0a4233437
--- /dev/null
+++ b/pw_metric/CMakeLists.txt
@@ -0,0 +1,64 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_metric STATIC
+ HEADERS
+ public/pw_metric/metric.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_tokenizer.base64
+ pw_assert
+ pw_containers
+ pw_log
+ pw_tokenizer
+ SOURCES
+ metric.cc
+ PRIVATE_DEPS
+ pw_span
+)
+
+pw_add_library(pw_metric.global STATIC
+ HEADERS
+ public/pw_metric/global.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_metric
+ pw_tokenizer
+ SOURCES
+ global.cc
+)
+
+pw_add_test(pw_metric.metric_test
+ SOURCES
+ metric_test.cc
+ PRIVATE_DEPS
+ pw_metric
+ GROUPS
+ modules
+ pw_metric
+)
+
+pw_add_test(pw_metric.global_test
+ SOURCES
+ global_test.cc
+ PRIVATE_DEPS
+ pw_metric.global
+ GROUPS
+ modules
+ pw_metric
+)
diff --git a/pw_metric/docs.rst b/pw_metric/docs.rst
index 2b9d20e87..93863658e 100644
--- a/pw_metric/docs.rst
+++ b/pw_metric/docs.rst
@@ -423,8 +423,8 @@ hypothetical global ``Uart`` object:
// Send/receive here...
private:
- std::span<std::byte> rx_buffer;
- std::span<std::byte> tx_buffer;
+ pw::span<std::byte> rx_buffer;
+ pw::span<std::byte> tx_buffer;
};
std::array<std::byte, 512> uart_rx_buffer;
@@ -454,8 +454,8 @@ might consider the following approach:
// Send/receive here which increment tx/rx_bytes.
private:
- std::span<std::byte> rx_buffer;
- std::span<std::byte> tx_buffer;
+ pw::span<std::byte> rx_buffer;
+ pw::span<std::byte> tx_buffer;
PW_METRIC(tx_bytes_, "tx_bytes", 0);
PW_METRIC(rx_bytes_, "rx_bytes", 0);
@@ -503,8 +503,8 @@ correctly, even when the objects are allocated globally:
// Send/receive here which increment tx/rx_bytes.
private:
- std::span<std::byte> rx_buffer;
- std::span<std::byte> tx_buffer;
+ pw::span<std::byte> rx_buffer;
+ pw::span<std::byte> tx_buffer;
PW_METRIC(tx_bytes_, "tx_bytes", 0);
PW_METRIC(rx_bytes_, "rx_bytes", 0);
@@ -632,8 +632,9 @@ Below is an example that **is incorrect**. Don't do what follows!
Exporting metrics
-----------------
Collecting metrics on a device is not useful without a mechanism to export
-those metrics for analysis and debugging. ``pw_metric`` offers an optional RPC
-service library (``:metric_service_nanopb``) that enables exporting a
+those metrics for analysis and debugging. ``pw_metric`` offers optional RPC
+service libraries (``:metric_service_nanopb`` based on nanopb, and
+``:metric_service_pwpb`` based on pw_protobuf) that enable exporting a
user-supplied set of on-device metrics via RPC. This facility is intended to
function from the early stages of device bringup through production in the
field.
@@ -738,6 +739,13 @@ metrics. This does not include the RPC service.
impact**. We are investigating why GCC is inserting large global static
constructors per group, when all the logic should be reused across objects.
+-------------
+Metric Parser
+-------------
+The metric_parser Python Module requests the system metrics via RPC, then parses the
+response while detokenizing the group and metrics names, and returns the metrics
+in a dictionary organized by group and value.
+
----------------
Design tradeoffs
----------------
diff --git a/pw_metric/metric.cc b/pw_metric/metric.cc
index 8a899f4f2..af23821e2 100644
--- a/pw_metric/metric.cc
+++ b/pw_metric/metric.cc
@@ -15,19 +15,19 @@
#include "pw_metric/metric.h"
#include <array>
-#include <span>
#include "pw_assert/check.h"
#include "pw_log/log.h"
+#include "pw_span/span.h"
#include "pw_tokenizer/base64.h"
namespace pw::metric {
namespace {
template <typename T>
-std::span<const std::byte> AsSpan(const T& t) {
- return std::span<const std::byte>(reinterpret_cast<const std::byte*>(&t),
- sizeof(t));
+span<const std::byte> AsSpan(const T& t) {
+ return span<const std::byte>(reinterpret_cast<const std::byte*>(&t),
+ sizeof(t));
}
// A convenience class to encode a token as base64 while managing the storage.
diff --git a/pw_metric/metric_service_nanopb.cc b/pw_metric/metric_service_nanopb.cc
index 0a1e7ccb3..5422be161 100644
--- a/pw_metric/metric_service_nanopb.cc
+++ b/pw_metric/metric_service_nanopb.cc
@@ -15,37 +15,40 @@
#include "pw_metric/metric_service_nanopb.h"
#include <cstring>
-#include <span>
#include "pw_assert/check.h"
#include "pw_containers/vector.h"
#include "pw_metric/metric.h"
+#include "pw_metric_private/metric_walker.h"
#include "pw_preprocessor/util.h"
+#include "pw_span/span.h"
namespace pw::metric {
namespace {
-class MetricWriter {
+class NanopbMetricWriter : public virtual internal::MetricWriter {
public:
- MetricWriter(
- MetricService::ServerWriter<pw_metric_MetricResponse>& response_writer)
- : response_(pw_metric_MetricResponse_init_zero),
+ NanopbMetricWriter(
+ MetricService::ServerWriter<pw_metric_proto_MetricResponse>&
+ response_writer)
+ : response_(pw_metric_proto_MetricResponse_init_zero),
response_writer_(response_writer) {}
// TODO(keir): Figure out a pw_rpc mechanism to fill a streaming packet based
// on transport MTU, rather than having this as a static knob. For example,
// some transports may be able to fit 30 metrics; others, only 5.
- void Write(const Metric& metric, const Vector<Token>& path) {
+ Status Write(const Metric& metric, const Vector<Token>& path) override {
// Nanopb doesn't offer an easy way to do bounds checking, so use span's
// type deduction magic to figure out the max size.
- std::span<pw_metric_Metric> metrics(response_.metrics);
+ span<pw_metric_proto_Metric> metrics(response_.metrics);
PW_CHECK_INT_LT(response_.metrics_count, metrics.size());
// Grab the next available Metric slot to write to in the response.
- pw_metric_Metric& proto_metric = response_.metrics[response_.metrics_count];
+ pw_metric_proto_Metric& proto_metric =
+ response_.metrics[response_.metrics_count];
// Copy the path.
- std::span<Token> proto_path(proto_metric.token_path);
+ span<Token> proto_path(proto_metric.token_path);
PW_CHECK_INT_LE(path.size(), proto_path.size());
std::copy(path.begin(), path.end(), proto_path.begin());
proto_metric.token_path_count = path.size();
@@ -53,10 +56,10 @@ class MetricWriter {
// Copy the metric value.
if (metric.is_float()) {
proto_metric.value.as_float = metric.as_float();
- proto_metric.which_value = pw_metric_Metric_as_float_tag;
+ proto_metric.which_value = pw_metric_proto_Metric_as_float_tag;
} else {
proto_metric.value.as_int = metric.as_int();
- proto_metric.which_value = pw_metric_Metric_as_int_tag;
+ proto_metric.which_value = pw_metric_proto_Metric_as_int_tag;
}
// Move write head to the next slot.
@@ -67,73 +70,32 @@ class MetricWriter {
if (response_.metrics_count == metrics.size()) {
Flush();
}
+
+ return OkStatus();
}
void Flush() {
if (response_.metrics_count) {
response_writer_.Write(response_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- response_ = pw_metric_MetricResponse_init_zero;
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ response_ = pw_metric_proto_MetricResponse_init_zero;
}
}
private:
- pw_metric_MetricResponse response_;
+ pw_metric_proto_MetricResponse response_;
// This RPC stream writer handle must be valid for the metric writer lifetime.
- MetricService::ServerWriter<pw_metric_MetricResponse>& response_writer_;
-};
-
-// Walk a metric tree recursively; passing metrics with their path (names) to a
-// metric writer which can consume them.
-//
-// TODO(keir): Generalize this to support a generic visitor.
-class MetricWalker {
- public:
- MetricWalker(MetricWriter& writer) : writer_(writer) {}
-
- void Walk(const IntrusiveList<Metric>& metrics) {
- for (const auto& m : metrics) {
- ScopedName scoped_name(m.name(), *this);
- writer_.Write(m, path_);
- }
- }
-
- void Walk(const IntrusiveList<Group>& groups) {
- for (const auto& g : groups) {
- Walk(g);
- }
- }
-
- void Walk(const Group& group) {
- ScopedName scoped_name(group.name(), *this);
- Walk(group.children());
- Walk(group.metrics());
- }
-
- private:
- // Exists to safely push/pop parent groups from the explicit stack.
- struct ScopedName {
- ScopedName(Token name, MetricWalker& rhs) : walker(rhs) {
- PW_CHECK_INT_LT(walker.path_.size(),
- walker.path_.capacity(),
- "Metrics are too deep; bump path_ capacity");
- walker.path_.push_back(name);
- }
- ~ScopedName() { walker.path_.pop_back(); }
- MetricWalker& walker;
- };
-
- Vector<Token, 4 /* max depth */> path_;
- MetricWriter& writer_;
+ MetricService::ServerWriter<pw_metric_proto_MetricResponse>& response_writer_;
};
} // namespace
-void MetricService::Get(const pw_metric_MetricRequest& /* request */,
- ServerWriter<pw_metric_MetricResponse>& response) {
+void MetricService::Get(
+ const pw_metric_proto_MetricRequest& /* request */,
+ ServerWriter<pw_metric_proto_MetricResponse>& response) {
// For now, ignore the request and just stream all the metrics back.
- MetricWriter writer(response);
- MetricWalker walker(writer);
+ NanopbMetricWriter writer(response);
+ internal::MetricWalker walker(writer);
// This will stream all the metrics in the span of this Get() method call.
// This will have the effect of blocking the RPC thread until all the metrics
@@ -142,8 +104,8 @@ void MetricService::Get(const pw_metric_MetricRequest& /* request */,
//
// In the future, this should be replaced with an optional async solution
// that puts the application in control of when the response batches are sent.
- walker.Walk(metrics_);
- walker.Walk(groups_);
+ walker.Walk(metrics_).IgnoreError();
+ walker.Walk(groups_).IgnoreError();
writer.Flush();
}
diff --git a/pw_metric/metric_service_nanopb_test.cc b/pw_metric/metric_service_nanopb_test.cc
index 631b1d030..267f8e22d 100644
--- a/pw_metric/metric_service_nanopb_test.cc
+++ b/pw_metric/metric_service_nanopb_test.cc
@@ -131,7 +131,7 @@ TEST(MetricService, NestedGroupsWithBatches) {
}
bool TokenPathsMatch(uint32_t expected_token_path[5],
- const pw_metric_Metric& metric) {
+ const pw_metric_proto_Metric& metric) {
// Calculate length of expected token & compare.
int expected_length = 0;
while (expected_token_path[expected_length]) {
diff --git a/pw_metric/metric_service_pwpb.cc b/pw_metric/metric_service_pwpb.cc
new file mode 100644
index 000000000..b52b6a1e5
--- /dev/null
+++ b/pw_metric/metric_service_pwpb.cc
@@ -0,0 +1,123 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_metric/metric_service_pwpb.h"
+
+#include <cstring>
+
+#include "pw_assert/check.h"
+#include "pw_containers/vector.h"
+#include "pw_metric/metric.h"
+#include "pw_metric_private/metric_walker.h"
+#include "pw_metric_proto/metric_service.pwpb.h"
+#include "pw_preprocessor/util.h"
+#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+
+namespace pw::metric {
+
+// TODO(amontanez): Make this follow the metric_service.options configuration.
+constexpr size_t kMaxNumPackedEntries = 3;
+
+namespace {
+
+class PwpbMetricWriter : public virtual internal::MetricWriter {
+ public:
+ PwpbMetricWriter(span<std::byte> response,
+ rpc::RawServerWriter& response_writer)
+ : response_(response),
+ response_writer_(response_writer),
+ encoder_(response) {}
+
+ // TODO(keir): Figure out a pw_rpc mechanism to fill a streaming packet based
+ // on transport MTU, rather than having this as a static knob. For example,
+ // some transports may be able to fit 30 metrics; others, only 5.
+ Status Write(const Metric& metric, const Vector<Token>& path) override {
+ { // Scope to control proto_encoder lifetime.
+
+ // Grab the next available Metric slot to write to in the response.
+ proto::pwpb::Metric::StreamEncoder proto_encoder =
+ encoder_.GetMetricsEncoder();
+ PW_TRY(proto_encoder.WriteTokenPath(path));
+ // Encode the metric value.
+ if (metric.is_float()) {
+ PW_TRY(proto_encoder.WriteAsFloat(metric.as_float()));
+ } else {
+ PW_TRY(proto_encoder.WriteAsInt(metric.as_int()));
+ }
+
+ metrics_count++;
+ }
+
+ if (metrics_count == kMaxNumPackedEntries) {
+ return Flush();
+ }
+ return OkStatus();
+ }
+
+ Status Flush() {
+ Status status;
+ if (metrics_count) {
+ status = response_writer_.Write(encoder_);
+ // Different way to clear MemoryEncoder. Copy constructor is disabled
+ // for memory encoder, and there is no "clear()" method.
+ encoder_.~MemoryEncoder();
+ new (&encoder_) proto::pwpb::MetricRequest::MemoryEncoder(response_);
+ metrics_count = 0;
+ }
+ return status;
+ }
+
+ private:
+ span<std::byte> response_;
+ // This RPC stream writer handle must be valid for the metric writer
+ // lifetime.
+ rpc::RawServerWriter& response_writer_;
+ proto::pwpb::MetricRequest::MemoryEncoder encoder_;
+ size_t metrics_count = 0;
+};
+} // namespace
+
+void MetricService::Get(ConstByteSpan /*request*/,
+ rpc::RawServerWriter& raw_response) {
+ // For now, ignore the request and just stream all the metrics back.
+ // TODO(amontanez): Make this follow the metric_service.options configuration.
+ constexpr size_t kSizeOfOneMetric =
+ pw::metric::proto::pwpb::MetricResponse::kMaxEncodedSizeBytes +
+ pw::metric::proto::pwpb::Metric::kMaxEncodedSizeBytes;
+ constexpr size_t kEncodeBufferSize = kMaxNumPackedEntries * kSizeOfOneMetric;
+
+ std::array<std::byte, kEncodeBufferSize> encode_buffer;
+
+ PwpbMetricWriter writer(encode_buffer, raw_response);
+ internal::MetricWalker walker(writer);
+
+ // This will stream all the metrics in the span of this Get() method call.
+ // This will have the effect of blocking the RPC thread until all the metrics
+ // are sent. That is likely to cause problems if there are many metrics, or
+ // if other RPCs are higher priority and should complete first.
+ //
+ // In the future, this should be replaced with an optional async solution
+ // that puts the application in control of when the response batches are sent.
+
+ // Propagate status through walker.
+ Status status;
+ status.Update(walker.Walk(metrics_));
+ status.Update(walker.Walk(groups_));
+ status.Update(writer.Flush());
+ raw_response.Finish(status).IgnoreError();
+}
+} // namespace pw::metric
diff --git a/pw_metric/metric_service_pwpb_test.cc b/pw_metric/metric_service_pwpb_test.cc
new file mode 100644
index 000000000..861463526
--- /dev/null
+++ b/pw_metric/metric_service_pwpb_test.cc
@@ -0,0 +1,218 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_metric/metric_service_pwpb.h"
+
+#include "gtest/gtest.h"
+#include "pw_log/log.h"
+#include "pw_metric_proto/metric_service.pwpb.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+#include "pw_rpc/raw/test_method_context.h"
+#include "pw_span/span.h"
+
+namespace pw::metric {
+namespace {
+
+#define MetricMethodContext \
+ PW_PWPB_TEST_METHOD_CONTEXT(MetricService, Get, 4, 256)
+
+size_t CountEncodedMetrics(ConstByteSpan serialized_path) {
+ protobuf::Decoder decoder(serialized_path);
+ size_t num_metrics = 0;
+ while (decoder.Next().ok()) {
+ switch (decoder.FieldNumber()) {
+ case static_cast<uint32_t>(
+ pw::metric::proto::pwpb::MetricResponse::Fields::kMetrics): {
+ num_metrics++;
+ }
+ }
+ }
+ return num_metrics;
+}
+
+size_t SumMetricInts(ConstByteSpan serialized_path) {
+ protobuf::Decoder decoder(serialized_path);
+ size_t metrics_sum = 0;
+ while (decoder.Next().ok()) {
+ switch (decoder.FieldNumber()) {
+ case static_cast<uint32_t>(
+ pw::metric::proto::pwpb::Metric::Fields::kAsInt): {
+ uint32_t metric_value;
+ EXPECT_EQ(OkStatus(), decoder.ReadUint32(&metric_value));
+ metrics_sum += metric_value;
+ }
+ }
+ }
+ return metrics_sum;
+}
+
+size_t GetMetricsSum(ConstByteSpan serialized_metric_buffer) {
+ protobuf::Decoder decoder(serialized_metric_buffer);
+ size_t metrics_sum = 0;
+ while (decoder.Next().ok()) {
+ switch (decoder.FieldNumber()) {
+ case static_cast<uint32_t>(
+ pw::metric::proto::pwpb::MetricResponse::Fields::kMetrics): {
+ ConstByteSpan metric_buffer;
+ EXPECT_EQ(OkStatus(), decoder.ReadBytes(&metric_buffer));
+ metrics_sum += SumMetricInts(metric_buffer);
+ }
+ }
+ }
+ return metrics_sum;
+}
+
+TEST(MetricService, EmptyGroupAndNoMetrics) {
+ // Empty root group.
+ PW_METRIC_GROUP(root, "/");
+
+ // Run the RPC and ensure it completes.
+
+ PW_RAW_TEST_METHOD_CONTEXT(MetricService, Get)
+ ctx{root.metrics(), root.children()};
+ ctx.call({});
+ EXPECT_TRUE(ctx.done());
+ EXPECT_EQ(OkStatus(), ctx.status());
+
+ // No metrics should be in the response.
+ EXPECT_EQ(0u, ctx.responses().size());
+}
+
+TEST(MetricService, OneGroupOneMetric) {
+ // One root group with one metric.
+ PW_METRIC_GROUP(root, "/");
+ PW_METRIC(root, a, "a", 3u);
+
+ // Run the RPC and ensure it completes.
+
+ PW_RAW_TEST_METHOD_CONTEXT(MetricService, Get)
+ ctx{root.metrics(), root.children()};
+ ctx.call({});
+ EXPECT_TRUE(ctx.done());
+ EXPECT_EQ(OkStatus(), ctx.status());
+
+ // One metric should be in the response.
+ EXPECT_EQ(1u, ctx.responses().size());
+
+ // Sum should be 3.
+ EXPECT_EQ(3u, GetMetricsSum(ctx.responses()[0]));
+}
+
+TEST(MetricService, OneGroupFiveMetrics) {
+ // One root group with five metrics.
+ PW_METRIC_GROUP(root, "/");
+ PW_METRIC(root, a, "a", 1u);
+ PW_METRIC(root, b, "b", 2u); // Note: Max # per response is 3.
+ PW_METRIC(root, c, "c", 3u);
+ PW_METRIC(root, x, "x", 4u);
+ PW_METRIC(root, y, "y", 5u);
+
+ // Run the RPC and ensure it completes.
+
+ PW_RAW_TEST_METHOD_CONTEXT(MetricService, Get)
+ ctx{root.metrics(), root.children()};
+ ctx.call({});
+ EXPECT_TRUE(ctx.done());
+ EXPECT_EQ(OkStatus(), ctx.status());
+
+ // Two metrics should be in the response.
+ EXPECT_EQ(2u, ctx.responses().size());
+ EXPECT_EQ(3u, CountEncodedMetrics(ctx.responses()[0]));
+ EXPECT_EQ(2u, CountEncodedMetrics(ctx.responses()[1]));
+
+ // The metrics are the numbers 1..5; sum them and compare.
+ EXPECT_EQ(
+ 15u,
+ GetMetricsSum(ctx.responses()[0]) + GetMetricsSum(ctx.responses()[1]));
+}
+
+TEST(MetricService, NestedGroupFiveMetrics) {
+ // Set up a nested group of metrics.
+ PW_METRIC_GROUP(root, "/");
+ PW_METRIC(root, a, "a", 1u);
+ PW_METRIC(root, b, "b", 2u);
+
+ PW_METRIC_GROUP(inner, "inner");
+ PW_METRIC(root, x, "x", 3u); // Note: Max # per response is 3.
+ PW_METRIC(inner, y, "y", 4u);
+ PW_METRIC(inner, z, "z", 5u);
+
+ root.Add(inner);
+
+ // Run the RPC and ensure it completes.
+
+ PW_RAW_TEST_METHOD_CONTEXT(MetricService, Get)
+ ctx{root.metrics(), root.children()};
+ ctx.call({});
+ EXPECT_TRUE(ctx.done());
+ EXPECT_EQ(OkStatus(), ctx.status());
+
+ // Two metrics should be in the response.
+ EXPECT_EQ(2u, ctx.responses().size());
+ EXPECT_EQ(3u, CountEncodedMetrics(ctx.responses()[0]));
+ EXPECT_EQ(2u, CountEncodedMetrics(ctx.responses()[1]));
+
+ EXPECT_EQ(
+ 15u,
+ GetMetricsSum(ctx.responses()[0]) + GetMetricsSum(ctx.responses()[1]));
+}
+
+TEST(MetricService, NestedGroupsWithBatches) {
+ // Set up a nested group of metrics that will not fit in a single batch.
+ PW_METRIC_GROUP(root, "/");
+ PW_METRIC(root, a, "a", 1u);
+ PW_METRIC(root, d, "d", 2u);
+ PW_METRIC(root, f, "f", 3u);
+
+ PW_METRIC_GROUP(inner_1, "inner1");
+ PW_METRIC(inner_1, x, "x", 4u);
+ PW_METRIC(inner_1, y, "y", 5u);
+ PW_METRIC(inner_1, z, "z", 6u);
+
+ PW_METRIC_GROUP(inner_2, "inner2");
+ PW_METRIC(inner_2, p, "p", 7u);
+ PW_METRIC(inner_2, q, "q", 8u);
+ PW_METRIC(inner_2, r, "r", 9u);
+ PW_METRIC(inner_2, s, "s", 10u); // Note: Max # per response is 3.
+ PW_METRIC(inner_2, t, "t", 11u);
+ PW_METRIC(inner_2, u, "u", 12u);
+
+ root.Add(inner_1);
+ root.Add(inner_2);
+
+ // Run the RPC and ensure it completes.
+ PW_RAW_TEST_METHOD_CONTEXT(MetricService, Get)
+ ctx{root.metrics(), root.children()};
+ ctx.call({});
+ EXPECT_TRUE(ctx.done());
+ EXPECT_EQ(OkStatus(), ctx.status());
+
+ // The response had to be split into four parts; check that they have the
+ // appropriate sizes.
+ EXPECT_EQ(4u, ctx.responses().size());
+ EXPECT_EQ(3u, CountEncodedMetrics(ctx.responses()[0]));
+ EXPECT_EQ(3u, CountEncodedMetrics(ctx.responses()[1]));
+ EXPECT_EQ(3u, CountEncodedMetrics(ctx.responses()[2]));
+ EXPECT_EQ(3u, CountEncodedMetrics(ctx.responses()[3]));
+
+ EXPECT_EQ(78u,
+ GetMetricsSum(ctx.responses()[0]) +
+ GetMetricsSum(ctx.responses()[1]) +
+ GetMetricsSum(ctx.responses()[2]) +
+ GetMetricsSum(ctx.responses()[3]));
+}
+
+} // namespace
+} // namespace pw::metric
diff --git a/pw_metric/metric_test.cc b/pw_metric/metric_test.cc
index 2677ad898..d4f353231 100644
--- a/pw_metric/metric_test.cc
+++ b/pw_metric/metric_test.cc
@@ -207,7 +207,7 @@ TEST(Metric, InlineConstructionWithGroups) {
// PW_METRIC_STATIC doesn't support class scopes, since a definition must be
// provided outside of the class body.
-// TODO(paulmathieu): add support for class scopes and enable this test
+// TODO(keir): add support for class scopes and enable this test
#if 0
class MetricTest: public ::testing::Test {
public:
diff --git a/pw_metric/public/pw_metric/metric.h b/pw_metric/public/pw_metric/metric.h
index 1ee998da8..5a6a3e030 100644
--- a/pw_metric/public/pw_metric/metric.h
+++ b/pw_metric/public/pw_metric/metric.h
@@ -298,12 +298,12 @@ class Group : public IntrusiveList<Group>::Item {
#define _PW_METRIC_GROUP_3(static_def, variable_name, group_name) \
static constexpr uint32_t variable_name##_token = \
PW_TOKENIZE_STRING_DOMAIN("metrics", group_name); \
- static_def ::pw::metric::Group variable_name = {variable_name##_token};
+ static_def ::pw::metric::Group variable_name = {variable_name##_token}
#define _PW_METRIC_GROUP_4(static_def, parent, variable_name, group_name) \
static constexpr uint32_t variable_name##_token = \
PW_TOKENIZE_STRING_DOMAIN("metrics", group_name); \
static_def ::pw::metric::Group variable_name = {variable_name##_token, \
- parent.children()};
+ parent.children()}
} // namespace pw::metric
diff --git a/pw_metric/public/pw_metric/metric_service_nanopb.h b/pw_metric/public/pw_metric/metric_service_nanopb.h
index c79f1dff9..ea6bb0d30 100644
--- a/pw_metric/public/pw_metric/metric_service_nanopb.h
+++ b/pw_metric/public/pw_metric/metric_service_nanopb.h
@@ -14,11 +14,11 @@
#pragma once
#include <cstring>
-#include <span>
#include "pw_log/log.h"
#include "pw_metric/metric.h"
#include "pw_metric_proto/metric_service.rpc.pb.h"
+#include "pw_span/span.h"
namespace pw::metric {
@@ -32,14 +32,14 @@ namespace pw::metric {
// future, we may switch to offering an async version where the Get() method
// returns immediately, and someone else is responsible for pumping the queue.
class MetricService final
- : public pw_rpc::nanopb::MetricService::Service<MetricService> {
+ : public proto::pw_rpc::nanopb::MetricService::Service<MetricService> {
public:
MetricService(const IntrusiveList<Metric>& metrics,
const IntrusiveList<Group>& groups)
: metrics_(metrics), groups_(groups) {}
- void Get(const pw_metric_MetricRequest& request,
- ServerWriter<pw_metric_MetricResponse>& response);
+ void Get(const pw_metric_proto_MetricRequest& request,
+ ServerWriter<pw_metric_proto_MetricResponse>& response);
private:
const IntrusiveList<Metric>& metrics_;
diff --git a/pw_metric/public/pw_metric/metric_service_pwpb.h b/pw_metric/public/pw_metric/metric_service_pwpb.h
new file mode 100644
index 000000000..2e36988fe
--- /dev/null
+++ b/pw_metric/public/pw_metric/metric_service_pwpb.h
@@ -0,0 +1,50 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstring>
+
+#include "pw_bytes/span.h"
+#include "pw_containers/intrusive_list.h"
+#include "pw_metric/metric.h"
+#include "pw_metric_proto/metric_service.raw_rpc.pb.h"
+#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
+
+namespace pw::metric {
+
+// The MetricService will send metrics when requested by Get(). For now, each
+// Get() request results in a stream of responses, containing the metrics from
+// the supplied list of groups and metrics. This includes recursive traversal
+// of subgroups. In the future, filtering will be supported.
+//
+// An important limitation of the current implementation is that the Get()
+// method is blocking, and sends all metrics at once (though batched). In the
+// future, we may switch to offering an async version where the Get() method
+// returns immediately, and someone else is responsible for pumping the queue.
+class MetricService final
+ : public proto::pw_rpc::raw::MetricService::Service<MetricService> {
+ public:
+ MetricService(const IntrusiveList<Metric>& metrics,
+ const IntrusiveList<Group>& groups)
+ : metrics_(metrics), groups_(groups) {}
+
+ void Get(ConstByteSpan request, rpc::RawServerWriter& response);
+
+ private:
+ const IntrusiveList<Metric>& metrics_;
+ const IntrusiveList<Group>& groups_;
+};
+
+} // namespace pw::metric
diff --git a/pw_metric/pw_metric_private/metric_walker.h b/pw_metric/pw_metric_private/metric_walker.h
new file mode 100644
index 000000000..a224312ba
--- /dev/null
+++ b/pw_metric/pw_metric_private/metric_walker.h
@@ -0,0 +1,75 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert/check.h"
+#include "pw_containers/intrusive_list.h"
+#include "pw_containers/vector.h"
+#include "pw_metric/metric.h"
+#include "pw_status/status.h"
+#include "pw_tokenizer/tokenize.h"
+
+namespace pw::metric::internal {
+
+class MetricWriter {
+ public:
+ virtual ~MetricWriter() = default;
+ virtual Status Write(const Metric& metric, const Vector<Token>& path) = 0;
+};
+
+// Walk a metric tree recursively; passing metrics with their path (names) to a
+// MetricWriter that can consume them.
+class MetricWalker {
+ public:
+ MetricWalker(MetricWriter& writer) : writer_(writer) {}
+
+ Status Walk(const IntrusiveList<Metric>& metrics) {
+ for (const auto& m : metrics) {
+ ScopedName scoped_name(m.name(), *this);
+ PW_TRY(writer_.Write(m, path_));
+ }
+ return OkStatus();
+ }
+
+ Status Walk(const IntrusiveList<Group>& groups) {
+ for (const auto& g : groups) {
+ PW_TRY(Walk(g));
+ }
+ return OkStatus();
+ }
+
+ Status Walk(const Group& group) {
+ ScopedName scoped_name(group.name(), *this);
+ PW_TRY(Walk(group.children()));
+ PW_TRY(Walk(group.metrics()));
+ return OkStatus();
+ }
+
+ private:
+ // Exists to safely push/pop parent groups from the explicit stack.
+ struct ScopedName {
+ ScopedName(Token name, MetricWalker& rhs) : walker(rhs) {
+ // Metrics are too deep; bump path_ capacity.
+ PW_ASSERT(walker.path_.size() < walker.path_.capacity());
+ walker.path_.push_back(name);
+ }
+ ~ScopedName() { walker.path_.pop_back(); }
+ MetricWalker& walker;
+ };
+
+ Vector<Token, /*capacity=*/4> path_;
+ MetricWriter& writer_;
+};
+
+} // namespace pw::metric::internal
diff --git a/pw_metric/pw_metric_proto/metric_service.options b/pw_metric/pw_metric_proto/metric_service.options
index 65eb3b349..28527a941 100644
--- a/pw_metric/pw_metric_proto/metric_service.options
+++ b/pw_metric/pw_metric_proto/metric_service.options
@@ -13,6 +13,6 @@
// the License.
// TODO(keir): Figure out appropriate options.
-pw.metric.Metric.token_path max_count:4
-pw.metric.MetricResponse.metrics max_count:10
+pw.metric.proto.Metric.token_path max_count:4
+pw.metric.proto.MetricResponse.metrics max_count:10
diff --git a/pw_metric/pw_metric_proto/metric_service.proto b/pw_metric/pw_metric_proto/metric_service.proto
index 9014463c3..354e25994 100644
--- a/pw_metric/pw_metric_proto/metric_service.proto
+++ b/pw_metric/pw_metric_proto/metric_service.proto
@@ -13,7 +13,7 @@
// the License.
syntax = "proto3";
-package pw.metric;
+package pw.metric.proto;
// A metric, described by the name (path + name), and the value.
//
diff --git a/pw_status/ts/BUILD.bazel b/pw_metric/py/BUILD.bazel
index fe368da9c..e0ea76184 100644
--- a/pw_status/ts/BUILD.bazel
+++ b/pw_metric/py/BUILD.bazel
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -12,25 +12,31 @@
# License for the specific language governing permissions and limitations under
# the License.
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@npm//@bazel/typescript:index.bzl", "ts_project")
+load("@rules_python//python:defs.bzl", "py_library", "py_test")
package(default_visibility = ["//visibility:public"])
-ts_project(
- name = "lib",
+licenses(["notice"])
+
+py_library(
+ name = "pw_metric",
srcs = [
- "index.ts",
- "status.ts",
+ "pw_metric/__init__.py",
+ "pw_metric/metric_parser.py",
+ ],
+ imports = ["."],
+ deps = [
+ "//pw_rpc/py:pw_rpc",
],
- declaration = True,
- source_map = True,
- deps = ["@npm//:node_modules"], # can't use fine-grained deps
)
-js_library(
- name = "pw_status",
- package_name = "@pigweed/pw_status",
- srcs = ["package.json"],
- deps = [":lib"],
+py_test(
+ name = "metric_parser_test",
+ size = "small",
+ srcs = [
+ "metric_parser_test.py",
+ ],
+ deps = [
+ ":pw_metric",
+ ],
)
diff --git a/pw_metric/py/BUILD.gn b/pw_metric/py/BUILD.gn
new file mode 100644
index 000000000..e7afa3480
--- /dev/null
+++ b/pw_metric/py/BUILD.gn
@@ -0,0 +1,38 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ generate_setup = {
+ metadata = {
+ name = "pw_metric"
+ version = "0.0.1"
+ }
+ }
+ sources = [
+ "pw_metric/__init__.py",
+ "pw_metric/metric_parser.py",
+ ]
+ tests = [ "metric_parser_test.py" ]
+ python_deps = [
+ "$dir_pw_rpc/py",
+ "$dir_pw_tokenizer/py",
+ ]
+ python_test_deps = [ "$dir_pw_metric:metric_service_proto.python" ]
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+}
diff --git a/pw_metric/py/metric_parser_test.py b/pw_metric/py/metric_parser_test.py
new file mode 100644
index 000000000..3b4800f7c
--- /dev/null
+++ b/pw_metric/py/metric_parser_test.py
@@ -0,0 +1,297 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for retreiving and parsing metrics."""
+from unittest import TestCase, mock, main
+from pw_metric.metric_parser import parse_metrics
+
+from pw_metric_proto import metric_service_pb2
+from pw_status import Status
+from pw_tokenizer import detokenize, tokens
+
+DATABASE = tokens.Database(
+ [
+ tokens.TokenizedStringEntry(0x01148A48, "total_dropped"),
+ tokens.TokenizedStringEntry(0x03796798, "min_queue_remaining"),
+ tokens.TokenizedStringEntry(0x22198280, "total_created"),
+ tokens.TokenizedStringEntry(0x534A42F4, "max_queue_used"),
+ tokens.TokenizedStringEntry(0x5D087463, "pw::work_queue::WorkQueue"),
+ tokens.TokenizedStringEntry(0xA7C43965, "log"),
+ ]
+)
+
+
+class TestParseMetrics(TestCase):
+ """Test parsing metrics received from RPCs"""
+
+ def setUp(self) -> None:
+ """Creating detokenizer and mocking RPC."""
+ self.detokenize = detokenize.Detokenizer(DATABASE)
+ self.rpc_timeout_s = 1
+ self.rpcs = mock.Mock()
+ self.rpcs.pw = mock.Mock()
+ self.rpcs.pw.metric = mock.Mock()
+ self.rpcs.pw.metric.proto = mock.Mock()
+ self.rpcs.pw.metric.proto.MetricService = mock.Mock()
+ self.rpcs.pw.metric.proto.MetricService.Get = mock.Mock()
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value = mock.Mock()
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.status = (
+ Status.OK
+ )
+ # Creating a group and metric name for better identification.
+ self.log = 0xA7C43965
+ self.total_created = 0x22198280
+ self.total_dropped = 0x01148A48
+ self.min_queue_remaining = 0x03796798
+ self.metric = [
+ metric_service_pb2.Metric(
+ token_path=[self.log, self.total_created],
+ string_path='N/A',
+ as_float=3.0,
+ ),
+ metric_service_pb2.Metric(
+ token_path=[self.log, self.total_dropped],
+ string_path='N/A',
+ as_float=4.0,
+ ),
+ ]
+
+ def test_invalid_detokenizer(self) -> None:
+ """Test invalid detokenizer was supplied."""
+ self.assertEqual(
+ {},
+ parse_metrics(self.rpcs, None, self.rpc_timeout_s),
+ msg='Valid detokenizer.',
+ )
+
+ def test_bad_stream_status(self) -> None:
+ """Test stream response has a status other than OK."""
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.status = (
+ Status.ABORTED
+ )
+ self.assertEqual(
+ {},
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s),
+ msg='Stream response was not aborted.',
+ )
+
+ def test_parse_metrics(self) -> None:
+ """Test metrics being parsed and recorded."""
+ # Loading metric into RPC.
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=self.metric)
+ ]
+ self.assertEqual(
+ {
+ 'log': {
+ 'total_created': 3.0,
+ 'total_dropped': 4.0,
+ }
+ },
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s),
+ msg='Metrics are not equal.',
+ )
+
+ def test_three_metric_names(self) -> None:
+ """Test creating a dictionary with three paths."""
+ # Creating another leaf.
+ self.metric.append(
+ metric_service_pb2.Metric(
+ token_path=[self.log, self.min_queue_remaining],
+ string_path='N/A',
+ as_float=1.0,
+ )
+ )
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=self.metric)
+ ]
+ self.assertEqual(
+ {
+ 'log': {
+ 'total_created': 3.0,
+ 'total_dropped': 4.0,
+ 'min_queue_remaining': 1.0,
+ },
+ },
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s),
+ msg='Metrics are not equal.',
+ )
+
+ def test_inserting_unknown_token(self) -> None:
+ # Inserting an unknown token as a group name.
+ self.metric.append(
+ metric_service_pb2.Metric(
+ token_path=[0x007, self.total_dropped],
+ string_path='N/A',
+ as_float=1.0,
+ )
+ )
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=self.metric)
+ ]
+ self.assertEqual(
+ {
+ 'log': {
+ 'total_created': 3.0,
+ 'total_dropped': 4.0,
+ },
+ '$': {'total_dropped': 1.0},
+ },
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s),
+ msg='Metrics are not equal.',
+ )
+
+ def test_multiple_metric_response(self) -> None:
+ """Tests multiple metric responses being handled."""
+ # Adding more than one MetricResponses.
+ metric = [
+ metric_service_pb2.Metric(
+ token_path=[0x007, self.total_dropped],
+ string_path='N/A',
+ as_float=1.0,
+ )
+ ]
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=self.metric),
+ metric_service_pb2.MetricResponse(metrics=metric),
+ ]
+ self.assertEqual(
+ {
+ 'log': {
+ 'total_created': 3.0,
+ 'total_dropped': 4.0,
+ },
+ '$': {
+ 'total_dropped': 1.0,
+ },
+ },
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s),
+ msg='Metrics are not equal.',
+ )
+
+ def test_paths_longer_than_two(self) -> None:
+ """Tests metric paths longer than two."""
+ # Path longer than two.
+ longest_metric = [
+ metric_service_pb2.Metric(
+ token_path=[
+ self.log,
+ self.total_created,
+ self.min_queue_remaining,
+ ],
+ string_path='N/A',
+ as_float=1.0,
+ ),
+ ]
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=longest_metric),
+ ]
+ self.assertEqual(
+ {
+ 'log': {
+ 'total_created': {'min_queue_remaining': 1.0},
+ }
+ },
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s),
+ msg='Metrics are not equal.',
+ )
+ # Create a new leaf in log.
+ longest_metric.append(
+ metric_service_pb2.Metric(
+ token_path=[self.log, self.total_dropped],
+ string_path='N/A',
+ as_float=3.0,
+ )
+ )
+ metric = [
+ metric_service_pb2.Metric(
+ token_path=[0x007, self.total_dropped],
+ string_path='N/A',
+ as_float=1.0,
+ ),
+ metric_service_pb2.Metric(
+ token_path=[0x007, self.total_created],
+ string_path='N/A',
+ as_float=2.0,
+ ),
+ ]
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=longest_metric),
+ metric_service_pb2.MetricResponse(metrics=metric),
+ ]
+ self.assertEqual(
+ {
+ 'log': {
+ 'total_created': {
+ 'min_queue_remaining': 1.0,
+ },
+ 'total_dropped': 3.0,
+ },
+ '$': {
+ 'total_dropped': 1.0,
+ 'total_created': 2.0,
+ },
+ },
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s),
+ msg='Metrics are not equal.',
+ )
+
+ def test_conflicting_keys(self) -> None:
+ """Tests conflicting key and value assignment."""
+ longest_metric = [
+ metric_service_pb2.Metric(
+ token_path=[
+ self.log,
+ self.total_created,
+ self.min_queue_remaining,
+ ],
+ string_path='N/A',
+ as_float=1.0,
+ ),
+ ]
+ # Creates a conflict at log/total_created, should throw an error.
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=longest_metric),
+ metric_service_pb2.MetricResponse(metrics=self.metric),
+ ]
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s)
+ self.assertRaises(ValueError, msg='Expected Value Error.')
+
+ def test_conflicting_logs(self) -> None:
+ """Tests conflicting loga being streamed."""
+ longest_metric = [
+ metric_service_pb2.Metric(
+ token_path=[self.log, self.total_created],
+ string_path='N/A',
+ as_float=1.0,
+ ),
+ ]
+ # Creates a duplicate metric for log/total_created.
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=longest_metric),
+ metric_service_pb2.MetricResponse(metrics=self.metric),
+ ]
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s)
+ self.assertRaises(ValueError, msg='Expected Value Error.')
+ # Duplicate metrics being loaded.
+ self.rpcs.pw.metric.proto.MetricService.Get.return_value.responses = [
+ metric_service_pb2.MetricResponse(metrics=self.metric),
+ metric_service_pb2.MetricResponse(metrics=self.metric),
+ ]
+ parse_metrics(self.rpcs, self.detokenize, self.rpc_timeout_s)
+ self.assertRaises(ValueError, msg='Expected Value Error.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_metric/py/pw_metric/__init__.py b/pw_metric/py/pw_metric/__init__.py
new file mode 100644
index 000000000..74efcfc3d
--- /dev/null
+++ b/pw_metric/py/pw_metric/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
diff --git a/pw_metric/py/pw_metric/metric_parser.py b/pw_metric/py/pw_metric/metric_parser.py
new file mode 100644
index 000000000..7b4ac94b2
--- /dev/null
+++ b/pw_metric/py/pw_metric/metric_parser.py
@@ -0,0 +1,80 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tools to retrieve and parse metrics."""
+from collections import defaultdict
+import json
+import logging
+from typing import Any, Optional
+from pw_tokenizer import detokenize
+
+_LOG = logging.getLogger(__name__)
+
+
+def _tree():
+ """Creates a key based on given input."""
+ return defaultdict(_tree)
+
+
+def _insert(metrics, path_names, value):
+ """Inserts any value in a leaf of the dictionary."""
+ for index, path_name in enumerate(path_names):
+ if index < len(path_names) - 1:
+ metrics = metrics[path_name]
+ elif path_name in metrics:
+ # the value in this position isn't a float or int,
+ # then collision occurs, throw an error.
+ assert ValueError(
+ 'Variable already exists: {p}'.format(p=path_name)
+ )
+ else:
+ metrics[path_name] = value
+
+
+def parse_metrics(
+ rpcs: Any,
+ detokenizer: Optional[detokenize.Detokenizer],
+ timeout_s: Optional[float],
+):
+ """Detokenizes metric names and retrieves their values."""
+ # Creates a defaultdict that can infinitely have other defaultdicts
+ # without a specified type.
+ metrics: defaultdict = _tree()
+ if not detokenizer:
+ _LOG.error('No metrics token database set.')
+ return metrics
+ stream_response = rpcs.pw.metric.proto.MetricService.Get(
+ pw_rpc_timeout_s=timeout_s
+ )
+ if not stream_response.status.ok():
+ _LOG.error('Unexpected status %s', stream_response.status)
+ return metrics
+ for metric_response in stream_response.responses:
+ for metric in metric_response.metrics:
+ path_names = []
+ for path in metric.token_path:
+ path_name = str(
+ detokenize.DetokenizedString(
+ path, detokenizer.lookup(path), b'', False
+ )
+ ).strip('"')
+ path_names.append(path_name)
+ value = (
+ metric.as_float
+ if metric.HasField('as_float')
+ else metric.as_int
+ )
+ # inserting path_names into metrics.
+ _insert(metrics, path_names, value)
+ # Converts default dict objects into standard dictionaries.
+ return json.loads(json.dumps(metrics))
diff --git a/pw_minimal_cpp_stdlib/BUILD.bazel b/pw_minimal_cpp_stdlib/BUILD.bazel
index 4deb4ceb6..8dd4536a8 100644
--- a/pw_minimal_cpp_stdlib/BUILD.bazel
+++ b/pw_minimal_cpp_stdlib/BUILD.bazel
@@ -64,14 +64,19 @@ pw_cc_library(
],
copts = ["-nostdinc++"],
includes = ["public"],
+ deps = [
+ "//pw_polyfill:standard_library",
+ ],
)
pw_cc_library(
name = "minimal_cpp_stdlib_isolated_test",
srcs = ["isolated_test.cc"],
copts = ["-nostdinc++"],
+ tags = ["manual"], # TODO(b/257529911): Fix build failures.
deps = [
":pw_minimal_cpp_stdlib",
+ "//pw_polyfill",
"//pw_preprocessor",
],
)
@@ -81,6 +86,7 @@ pw_cc_test(
srcs = [
"test.cc",
],
+ tags = ["manual"], # TODO(b/257529911): Fix build failures.
deps = [
":minimal_cpp_stdlib_isolated_test",
"//pw_unit_test",
diff --git a/pw_minimal_cpp_stdlib/BUILD.gn b/pw_minimal_cpp_stdlib/BUILD.gn
index bda41782c..779029cd2 100644
--- a/pw_minimal_cpp_stdlib/BUILD.gn
+++ b/pw_minimal_cpp_stdlib/BUILD.gn
@@ -18,16 +18,24 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
-config("include_dirs") {
+config("public_include_path") {
include_dirs = [ "public" ]
+ visibility = [ ":*" ]
}
config("no_cpp_includes") {
cflags = [ "-nostdinc++" ]
}
+config("use_minimal_cpp_stdlib") {
+ configs = [
+ ":public_include_path",
+ ":no_cpp_includes",
+ ]
+}
+
pw_source_set("pw_minimal_cpp_stdlib") {
- public_configs = [ ":include_dirs" ]
+ public_configs = [ ":public_include_path" ]
configs = [ ":no_cpp_includes" ]
public = [
"public/algorithm",
@@ -67,6 +75,7 @@ pw_source_set("pw_minimal_cpp_stdlib") {
"public/internal/type_traits.h",
"public/internal/utility.h",
]
+ public_deps = [ dir_pw_polyfill ]
}
pw_test_group("tests") {
@@ -84,6 +93,7 @@ pw_source_set("minimal_cpp_stdlib_isolated_test") {
configs = [ ":no_cpp_includes" ]
deps = [
":pw_minimal_cpp_stdlib",
+ dir_pw_polyfill,
dir_pw_preprocessor,
]
sources = [ "isolated_test.cc" ]
@@ -96,6 +106,10 @@ pw_test("minimal_cpp_stdlib_test") {
}
pw_test("standard_library_test") {
+ deps = [
+ dir_pw_polyfill,
+ dir_pw_preprocessor,
+ ]
sources = [
"isolated_test.cc",
"test.cc",
diff --git a/pw_minimal_cpp_stdlib/docs.rst b/pw_minimal_cpp_stdlib/docs.rst
index 29a89aa43..d48629b93 100644
--- a/pw_minimal_cpp_stdlib/docs.rst
+++ b/pw_minimal_cpp_stdlib/docs.rst
@@ -1,14 +1,26 @@
.. _module-pw_minimal_cpp_stdlib:
----------------------
+=====================
pw_minimal_cpp_stdlib
----------------------
+=====================
The ``pw_minimal_cpp_stdlib`` module provides an extremely limited
implementation of the C++ Standard Library. This module falls far, far short of
-providing a complete C++ Standard Library and should only be used in dire
-situations where you happen to be compiling with C++17 but don't have a C++
-Standard Library available to you.
+providing a complete C++ Standard Library and should only be used for testing
+and development when compiling with C++17 or newer without a C++ Standard
+Library. Production code should use a real C++ Standard Library implementation,
+such as `libc++ <https://libcxx.llvm.org/>`_ or
+`libstdc++ <https://gcc.gnu.org/onlinedocs/libstdc++/>`_.
+.. warning::
+
+ ``pw_minimal_cpp_stdlib`` was created for a very specific purpose. It is NOT a
+ general purpose C++ Standard Library implementation and should not be used as
+ one. Many features are missing, some features non-functioning stubs, and some
+ features may not match the C++ standard.
+
+-----------
+Code layout
+-----------
The C++ Standard Library headers (e.g. ``<cstdint>`` and ``<type_traits>``) are
defined in ``public/``. These files are symlinks to their implementations in
``public/internal/``.
@@ -22,8 +34,14 @@ defined in ``public/``. These files are symlinks to their implementations in
for f in $(ls internal/); do ln -s internal/$f ${f%.h}; done
+The top-level ``build_with_minimal_cpp_stdlib`` GN group builds a few supported
+modules with ``pw_minimal_cpp_stdlib`` swapped in for the C++ library at the
+toolchain level. Notably, ``pw_minimal_cpp_stdlib`` does not support
+``pw_unit_test``, so this group does not run any tests.
+
+------------
Requirements
-============
+------------
- C++17
- gcc or clang
- The C Standard Library
diff --git a/pw_minimal_cpp_stdlib/isolated_test.cc b/pw_minimal_cpp_stdlib/isolated_test.cc
index e577e3fba..8fb13b113 100644
--- a/pw_minimal_cpp_stdlib/isolated_test.cc
+++ b/pw_minimal_cpp_stdlib/isolated_test.cc
@@ -30,6 +30,7 @@
#include <type_traits>
#include <utility>
+#include "pw_polyfill/standard.h"
#include "pw_preprocessor/compiler.h"
namespace {
@@ -298,6 +299,25 @@ TEST(TypeTraits, Basic) {
static_assert(!std::is_same_v<char, unsigned char>);
}
+TEST(TypeTraits, LogicalTraits) {
+ static_assert(std::conjunction_v<>);
+ static_assert(!std::conjunction_v<std::false_type>);
+ static_assert(std::conjunction_v<std::true_type>);
+ static_assert(!std::conjunction_v<std::false_type, std::true_type>);
+ static_assert(std::conjunction_v<std::true_type, std::true_type>);
+ static_assert(!std::conjunction_v<std::false_type, std::false_type>);
+
+ static_assert(!std::disjunction_v<>);
+ static_assert(!std::disjunction_v<std::false_type>);
+ static_assert(std::disjunction_v<std::true_type>);
+ static_assert(std::disjunction_v<std::false_type, std::true_type>);
+ static_assert(std::disjunction_v<std::true_type, std::true_type>);
+ static_assert(!std::disjunction_v<std::false_type, std::false_type>);
+
+ static_assert(std::negation_v<std::false_type>);
+ static_assert(!std::negation_v<std::true_type>);
+}
+
struct MoveTester {
MoveTester(int value) : magic_value(value), moved(false) {}
@@ -325,6 +345,56 @@ TEST(Utility, Move) {
EXPECT_TRUE(moved.moved);
}
+TEST(Utility, MakeIntegerSequence) {
+ static_assert(std::is_same_v<std::make_integer_sequence<int, 0>,
+ std::integer_sequence<int>>);
+ static_assert(std::is_same_v<std::make_integer_sequence<int, 1>,
+ std::integer_sequence<int, 0>>);
+ static_assert(std::is_same_v<std::make_integer_sequence<int, 3>,
+ std::integer_sequence<int, 0, 1, 2>>);
+
+ static_assert(std::is_same_v<std::make_index_sequence<0>,
+ std::integer_sequence<size_t>>);
+ static_assert(std::is_same_v<std::make_index_sequence<1>,
+ std::integer_sequence<size_t, 0>>);
+ static_assert(std::is_same_v<std::make_index_sequence<3>,
+ std::integer_sequence<size_t, 0, 1, 2>>);
+}
+
+TEST(Iterator, Tags) {
+ static_assert(std::is_convertible_v<std::forward_iterator_tag,
+ std::input_iterator_tag>);
+
+ static_assert(std::is_convertible_v<std::bidirectional_iterator_tag,
+ std::input_iterator_tag>);
+ static_assert(std::is_convertible_v<std::bidirectional_iterator_tag,
+ std::forward_iterator_tag>);
+
+ static_assert(std::is_convertible_v<std::random_access_iterator_tag,
+ std::input_iterator_tag>);
+ static_assert(std::is_convertible_v<std::random_access_iterator_tag,
+ std::forward_iterator_tag>);
+ static_assert(std::is_convertible_v<std::random_access_iterator_tag,
+ std::bidirectional_iterator_tag>);
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(20)
+ static_assert(std::is_convertible_v<std::contiguous_iterator_tag,
+ std::input_iterator_tag>);
+ static_assert(std::is_convertible_v<std::contiguous_iterator_tag,
+ std::forward_iterator_tag>);
+ static_assert(std::is_convertible_v<std::contiguous_iterator_tag,
+ std::bidirectional_iterator_tag>);
+ static_assert(std::is_convertible_v<std::contiguous_iterator_tag,
+ std::random_access_iterator_tag>);
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(20)
+}
+
+TEST(TypeTrait, Basic) {
+ static_assert(std::is_same_v<const int, std::add_const_t<int>>);
+ static_assert(std::is_same_v<const int, std::add_const_t<const int>>);
+ static_assert(!std::is_same_v<int, std::add_const_t<int>>);
+}
+
} // namespace
namespace pw::minimal_cpp_stdlib {
diff --git a/pw_minimal_cpp_stdlib/public/internal/iterator.h b/pw_minimal_cpp_stdlib/public/internal/iterator.h
index 09b0c7020..4828cc5c3 100644
--- a/pw_minimal_cpp_stdlib/public/internal/iterator.h
+++ b/pw_minimal_cpp_stdlib/public/internal/iterator.h
@@ -15,6 +15,7 @@
#include <cstddef>
+#include "pw_polyfill/standard.h"
#include "pw_polyfill/standard_library/namespace.h"
_PW_POLYFILL_BEGIN_NAMESPACE_STD
@@ -70,6 +71,15 @@ constexpr decltype(sizeof(int)) size(const T (&)[kSize]) noexcept {
template <typename>
struct iterator_traits {};
+struct input_iterator_tag {};
+struct output_iterator_tag {};
+struct forward_iterator_tag : public input_iterator_tag {};
+struct bidirectional_iterator_tag : public forward_iterator_tag {};
+struct random_access_iterator_tag : public bidirectional_iterator_tag {};
+#if PW_CXX_STANDARD_IS_SUPPORTED(20)
+struct contiguous_iterator_tag : public random_access_iterator_tag {};
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(20)
+
// NOT IMPLEMENTED: Reverse iterators are not implemented.
template <typename>
struct reverse_iterator;
diff --git a/pw_minimal_cpp_stdlib/public/internal/type_traits.h b/pw_minimal_cpp_stdlib/public/internal/type_traits.h
index bb6ccb58f..d228f853d 100644
--- a/pw_minimal_cpp_stdlib/public/internal/type_traits.h
+++ b/pw_minimal_cpp_stdlib/public/internal/type_traits.h
@@ -19,7 +19,6 @@ _PW_POLYFILL_BEGIN_NAMESPACE_STD
#define __cpp_lib_transformation_trait_aliases 201304L
#define __cpp_lib_type_trait_variable_templates 201510L
-#define __cpp_lib_logical_traits 201510L
template <decltype(sizeof(0)) kLength,
decltype(sizeof(0)) kAlignment> // no default
@@ -209,12 +208,6 @@ struct is_void : is_same<void, typename remove_cv<T>::type> {};
template <typename T>
inline constexpr bool is_void_v = is_void<T>::value;
-template <typename T>
-struct negation : bool_constant<!bool(T::value)> {};
-
-template <typename T>
-inline constexpr bool negation_v = negation<T>::value;
-
template <bool kBool, typename TrueType, typename FalseType>
struct conditional {
using type = TrueType;
@@ -228,6 +221,46 @@ struct conditional<false, TrueType, FalseType> {
template <bool kBool, typename TrueType, typename FalseType>
using conditional_t = typename conditional<kBool, TrueType, FalseType>::type;
+#define __cpp_lib_logical_traits 201510L
+
+template <typename...>
+struct conjunction;
+
+template <>
+struct conjunction<> : true_type {};
+
+template <typename T>
+struct conjunction<T> : T {};
+
+template <typename First, typename... Others>
+struct conjunction<First, Others...>
+ : conditional_t<bool(First::value), conjunction<Others...>, First> {};
+
+template <typename... Types>
+inline constexpr bool conjunction_v = conjunction<Types...>::value;
+
+template <typename...>
+struct disjunction;
+
+template <>
+struct disjunction<> : false_type {};
+
+template <typename T>
+struct disjunction<T> : T {};
+
+template <typename First, typename... Others>
+struct disjunction<First, Others...>
+ : conditional_t<bool(First::value), First, disjunction<Others...>> {};
+
+template <typename... Types>
+inline constexpr bool disjunction_v = disjunction<Types...>::value;
+
+template <typename T>
+struct negation : bool_constant<!bool(T::value)> {};
+
+template <typename T>
+inline constexpr bool negation_v = negation<T>::value;
+
template <bool kEnable, typename T = void>
struct enable_if {
using type = T;
@@ -400,6 +433,60 @@ add_rvalue_reference_t<T> declval() noexcept;
namespace impl {
+template <class T>
+struct add_const {
+ typedef const T type;
+};
+
+} // namespace impl
+
+template <class T>
+using add_const_t = typename impl::add_const<T>::type;
+
+template <typename T>
+struct make_signed;
+
+template <typename T>
+struct make_unsigned;
+
+#define _PW_MAKE_SIGNED_SPECIALIZATION(base, signed_type, unsigned_type) \
+ template <> \
+ struct make_signed<base> { \
+ using type = signed_type; \
+ }; \
+ template <> \
+ struct make_unsigned<base> { \
+ using type = unsigned_type; \
+ }
+
+_PW_MAKE_SIGNED_SPECIALIZATION(char, signed char, unsigned char);
+_PW_MAKE_SIGNED_SPECIALIZATION(signed char, signed char, unsigned char);
+_PW_MAKE_SIGNED_SPECIALIZATION(unsigned char, signed char, unsigned char);
+
+_PW_MAKE_SIGNED_SPECIALIZATION(short, signed short, unsigned short);
+_PW_MAKE_SIGNED_SPECIALIZATION(unsigned short, signed short, unsigned short);
+
+_PW_MAKE_SIGNED_SPECIALIZATION(int, signed int, unsigned int);
+_PW_MAKE_SIGNED_SPECIALIZATION(unsigned int, signed int, unsigned int);
+
+_PW_MAKE_SIGNED_SPECIALIZATION(long, signed long, unsigned long);
+_PW_MAKE_SIGNED_SPECIALIZATION(unsigned long, signed long, unsigned long);
+
+_PW_MAKE_SIGNED_SPECIALIZATION(long long, signed short, unsigned short);
+_PW_MAKE_SIGNED_SPECIALIZATION(unsigned long long,
+ signed short,
+ unsigned short);
+
+// Skip specializations for char8_t, etc.
+
+template <typename T>
+using make_signed_t = typename make_signed<T>::type;
+
+template <typename T>
+using make_unsigned_t = typename make_unsigned<T>::type;
+
+namespace impl {
+
template <typename>
using templated_true = true_type;
diff --git a/pw_minimal_cpp_stdlib/public/internal/utility.h b/pw_minimal_cpp_stdlib/public/internal/utility.h
index c3862a778..31a704b41 100644
--- a/pw_minimal_cpp_stdlib/public/internal/utility.h
+++ b/pw_minimal_cpp_stdlib/public/internal/utility.h
@@ -31,4 +31,26 @@ struct tuple_element;
template <typename>
struct tuple_size;
+template <typename T, T... kSequence>
+class integer_sequence;
+
+template <size_t... kSequence>
+using index_sequence = integer_sequence<decltype(sizeof(int)), kSequence...>;
+
+template <typename T, T kEnd>
+#if __has_builtin(__make_integer_seq)
+using make_integer_sequence = __make_integer_seq<integer_sequence, T, kEnd>;
+#elif __has_builtin(__integer_pack)
+using make_integer_sequence = integer_sequence<T, __integer_pack(kEnd)...>;
+#endif // make_integer_sequence
+
+template <size_t kEnd>
+using make_index_sequence = make_integer_sequence<size_t, kEnd>;
+
+struct in_place_t {
+ explicit constexpr in_place_t() = default;
+};
+
+inline constexpr in_place_t in_place{};
+
_PW_POLYFILL_END_NAMESPACE_STD
diff --git a/pw_module/BUILD.gn b/pw_module/BUILD.gn
index 09696722b..45e4c14f1 100644
--- a/pw_module/BUILD.gn
+++ b/pw_module/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_module/docs.rst b/pw_module/docs.rst
index ec564eb7a..917da4cbb 100644
--- a/pw_module/docs.rst
+++ b/pw_module/docs.rst
@@ -12,17 +12,17 @@ Commands
.. _module-pw_module-module-check:
-``pw module-check``
+``pw module check``
^^^^^^^^^^^^^^^^^^^
-The ``pw module-check`` command exists to ensure that your module conforms to
+The ``pw module check`` command exists to ensure that your module conforms to
the Pigweed module norms.
-For example, at time of writing ``pw module-check pw_module`` is not passing
+For example, at time of writing ``pw module check pw_module`` is not passing
its own lint:
.. code-block:: none
- $ pw module-check pw_module
+ $ pw module check pw_module
▒█████▄ █▓ ▄███▒ ▒█ ▒█ ░▓████▒ ░▓████▒ ▒▓████▄
▒█░ █░ ░█▒ ██▒ ▀█▒ ▒█░ █ ▒█ ▒█ ▀ ▒█ ▀ ▒█ ▀█▌
@@ -35,3 +35,18 @@ its own lint:
20191205 17:05:19 ERR FAIL: Found errors when checking module pw_module
+.. _module-pw_module-module-create:
+
+``pw module create``
+^^^^^^^^^^^^^^^^^^^^
+The ``pw module create`` command is used to generate all of the required
+boilerplate for a new Pigweed module.
+
+.. note::
+
+ ``pw module create`` is still under construction and mostly experimental.
+ It is only usable in upstream Pigweed, and has limited feature support, with
+ a command-line API subject to change.
+
+ Once the command is more stable, it will be properly documented. For now,
+ running ``pw module create --help`` will display the current set of options.
diff --git a/pw_module/py/BUILD.gn b/pw_module/py/BUILD.gn
index 7a53ae7d3..876184043 100644
--- a/pw_module/py/BUILD.gn
+++ b/pw_module/py/BUILD.gn
@@ -24,8 +24,12 @@ pw_python_package("py") {
]
sources = [
"pw_module/__init__.py",
+ "pw_module/__main__.py",
"pw_module/check.py",
+ "pw_module/create.py",
]
+ python_deps = [ "$dir_pw_build/py" ]
tests = [ "check_test.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_module/py/check_test.py b/pw_module/py/check_test.py
index 9eb598e82..24bfc3e4a 100644
--- a/pw_module/py/check_test.py
+++ b/pw_module/py/check_test.py
@@ -26,6 +26,7 @@ _LOG = logging.getLogger(__name__)
class TestWithTempDirectory(unittest.TestCase):
"""Tests for pw_module.check."""
+
def setUp(self):
# Create a temporary directory for the test.
self.test_dir = tempfile.mkdtemp()
@@ -68,8 +69,9 @@ class TestWithTempDirectory(unittest.TestCase):
# Python files; no setup --> error.
self.create_file('pw_foo/py/pw_foo/__init__.py')
self.create_file('pw_foo/py/pw_foo/bar.py')
- self.assert_issue(pw_module.check.check_python_proper_module,
- 'setup.py')
+ self.assert_issue(
+ pw_module.check.check_python_proper_module, 'setup.py'
+ )
# Python files; have setup.py --> ok.
self.create_file('pw_foo/py/setup.py')
@@ -140,5 +142,6 @@ class TestWithTempDirectory(unittest.TestCase):
if __name__ == '__main__':
import sys
+
logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
unittest.main()
diff --git a/pw_module/py/pw_module/__main__.py b/pw_module/py/pw_module/__main__.py
new file mode 100644
index 000000000..5fd3a264d
--- /dev/null
+++ b/pw_module/py/pw_module/__main__.py
@@ -0,0 +1,44 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Utilities for managing modules."""
+
+import argparse
+
+import pw_module.check
+import pw_module.create
+
+
+def main() -> None:
+ """Entrypoint for the `pw module` plugin."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.set_defaults(func=lambda **_kwargs: parser.print_help())
+
+ subparsers = parser.add_subparsers(title='subcommands')
+
+ pw_module.check.register_subcommand(
+ subparsers.add_parser('check', help=pw_module.check.__doc__)
+ )
+ pw_module.create.register_subcommand(
+ subparsers.add_parser('create', help=pw_module.create.__doc__)
+ )
+
+ args = {**vars(parser.parse_args())}
+ func = args['func']
+ del args['func']
+ func(**args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_module/py/pw_module/check.py b/pw_module/py/pw_module/check.py
index 0ccc062e4..577d8fd4c 100644
--- a/pw_module/py/pw_module/check.py
+++ b/pw_module/py/pw_module/check.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -76,11 +76,13 @@ def check_module(module) -> bool:
# Try to make an error message that will help editors open the part
# of the module in question (e.g. vim's 'cerr' functionality).
components = [
- x for x in (
+ x
+ for x in (
issue.file,
issue.line_number,
issue.line_contents,
- ) if x
+ )
+ if x
]
editor_error_line = ':'.join(components)
if editor_error_line:
@@ -97,8 +99,9 @@ def check_module(module) -> bool:
# TODO(keir): Give this a proper ASCII art treatment.
if not found_any_warnings and not found_any_errors:
- _LOG.info('OK: Module %s looks good; no errors or warnings found',
- module)
+ _LOG.info(
+ 'OK: Module %s looks good; no errors or warnings found', module
+ )
if found_any_errors:
_LOG.error('FAIL: Found errors when checking module %s', module)
return False
@@ -147,8 +150,7 @@ def check_python_proper_module(directory):
@checker('PWCK002', 'If there are C++ files, there are C++ tests')
def check_have_cc_tests(directory):
module_cc_files = glob.glob(f'{directory}/**/*.cc', recursive=True)
- module_cc_test_files = glob.glob(f'{directory}/**/*test.cc',
- recursive=True)
+ module_cc_test_files = glob.glob(f'{directory}/**/*test.cc', recursive=True)
if module_cc_files and not module_cc_test_files:
yield Issue('C++ code present but no tests at all (you monster).')
@@ -156,8 +158,9 @@ def check_have_cc_tests(directory):
@checker('PWCK003', 'If there are Python files, there are Python tests')
def check_have_python_tests(directory):
module_py_files = glob.glob(f'{directory}/**/*.py', recursive=True)
- module_py_test_files = glob.glob(f'{directory}/**/*test*.py',
- recursive=True)
+ module_py_test_files = glob.glob(
+ f'{directory}/**/*test*.py', recursive=True
+ )
if module_py_files and not module_py_test_files:
yield Issue('Python code present but no tests (you monster).')
@@ -171,17 +174,19 @@ def check_has_readme(directory):
@checker('PWCK005', 'There is ReST documentation (*.rst)')
def check_has_rst_docs(directory):
if not glob.glob(f'{directory}/**/*.rst', recursive=True):
- yield Issue(
- 'Missing ReST documentation; need at least e.g. "docs.rst"')
+ yield Issue('Missing ReST documentation; need at least e.g. "docs.rst"')
-@checker('PWCK006', 'If C++, have <mod>/public/<mod>/*.h or '
- '<mod>/public_override/*.h')
+@checker(
+ 'PWCK006',
+ 'If C++, have <mod>/public/<mod>/*.h or ' '<mod>/public_override/*.h',
+)
def check_has_public_or_override_headers(directory):
- # TODO: Should likely have a decorator to check for C++ in a checker, or
- # other more useful and cachable mechanisms.
- if (not glob.glob(f'{directory}/**/*.cc', recursive=True)
- and not glob.glob(f'{directory}/**/*.h', recursive=True)):
+ # TODO(keir): Should likely have a decorator to check for C++ in a checker,
+ # or other more useful and cachable mechanisms.
+ if not glob.glob(f'{directory}/**/*.cc', recursive=True) and not glob.glob(
+ f'{directory}/**/*.h', recursive=True
+ ):
# No C++ files.
return
@@ -189,21 +194,25 @@ def check_has_public_or_override_headers(directory):
has_public_cpp_headers = glob.glob(f'{directory}/public/{module_name}/*.h')
has_public_cpp_override_headers = glob.glob(
- f'{directory}/public_overrides/**/*.h')
+ f'{directory}/public_overrides/**/*.h'
+ )
if not has_public_cpp_headers and not has_public_cpp_override_headers:
- yield Issue(f'Have C++ code but no public/{module_name}/*.h '
- 'found and no public_overrides/ found')
+ yield Issue(
+ f'Have C++ code but no public/{module_name}/*.h '
+ 'found and no public_overrides/ found'
+ )
multiple_public_directories = glob.glob(f'{directory}/public/*')
if len(multiple_public_directories) != 1:
- yield Issue(f'Have multiple directories under public/; there should '
- f'only be a single directory: "public/{module_name}". '
- 'Perhaps you were looking for public_overrides/?.')
+ yield Issue(
+ f'Have multiple directories under public/; there should '
+ f'only be a single directory: "public/{module_name}". '
+ 'Perhaps you were looking for public_overrides/?.'
+ )
-def main() -> None:
+def register_subcommand(parser: argparse.ArgumentParser) -> None:
"""Check that a module matches Pigweed's module guidelines."""
- parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('modules', nargs='+', help='The module to check')
- check_modules(**vars(parser.parse_args()))
+ parser.set_defaults(func=check_modules)
diff --git a/pw_module/py/pw_module/create.py b/pw_module/py/pw_module/create.py
new file mode 100644
index 000000000..d53dca03a
--- /dev/null
+++ b/pw_module/py/pw_module/create.py
@@ -0,0 +1,940 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Creates a new Pigweed module."""
+
+import abc
+import argparse
+import dataclasses
+from dataclasses import dataclass
+import datetime
+import logging
+import os
+from pathlib import Path
+import re
+import sys
+
+from typing import Any, Dict, Iterable, List, Optional, Type, Union
+
+from pw_build import generate_modules_lists
+
+_LOG = logging.getLogger(__name__)
+
+_PIGWEED_LICENSE = f"""
+# Copyright {datetime.datetime.now().year} The Pigweed Authors
+#
+# 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
+#
+# https://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.""".lstrip()
+
+_PIGWEED_LICENSE_CC = _PIGWEED_LICENSE.replace('#', '//')
+
+
+# TODO(frolv): Adapted from pw_protobuf. Consolidate them.
+class _OutputFile:
+ DEFAULT_INDENT_WIDTH = 2
+
+ def __init__(self, file: Path, indent_width: int = DEFAULT_INDENT_WIDTH):
+ self._file = file
+ self._content: List[str] = []
+ self._indent_width: int = indent_width
+ self._indentation = 0
+
+ def line(self, line: str = '') -> None:
+ if line:
+ self._content.append(' ' * self._indentation)
+ self._content.append(line)
+ self._content.append('\n')
+
+ def indent(
+ self,
+ width: Optional[int] = None,
+ ) -> '_OutputFile._IndentationContext':
+ """Increases the indentation level of the output."""
+ return self._IndentationContext(
+ self, width if width is not None else self._indent_width
+ )
+
+ @property
+ def path(self) -> Path:
+ return self._file
+
+ @property
+ def content(self) -> str:
+ return ''.join(self._content)
+
+ def write(self) -> None:
+ print(' create ' + str(self._file.relative_to(Path.cwd())))
+ self._file.write_text(self.content)
+
+ class _IndentationContext:
+ """Context that increases the output's indentation when it is active."""
+
+ def __init__(self, output: '_OutputFile', width: int):
+ self._output = output
+ self._width: int = width
+
+ def __enter__(self):
+ self._output._indentation += self._width
+
+ def __exit__(self, typ, value, traceback):
+ self._output._indentation -= self._width
+
+
+class _ModuleName:
+ _MODULE_NAME_REGEX = '(^[a-zA-Z]{2,})((_[a-zA-Z0-9]+)+)$'
+
+ def __init__(self, prefix: str, main: str) -> None:
+ self._prefix = prefix
+ self._main = main
+
+ @property
+ def full(self) -> str:
+ return f'{self._prefix}_{self._main}'
+
+ @property
+ def prefix(self) -> str:
+ return self._prefix
+
+ @property
+ def main(self) -> str:
+ return self._main
+
+ @property
+ def default_namespace(self) -> str:
+ return f'{self._prefix}::{self._main}'
+
+ def upper_camel_case(self) -> str:
+ return ''.join(s.capitalize() for s in self._main.split('_'))
+
+ def __str__(self) -> str:
+ return self.full
+
+ def __repr__(self) -> str:
+ return self.full
+
+ @classmethod
+ def parse(cls, name: str) -> Optional['_ModuleName']:
+ match = re.fullmatch(_ModuleName._MODULE_NAME_REGEX, name)
+ if not match:
+ return None
+
+ return cls(match.group(1), match.group(2)[1:])
+
+
+@dataclass
+class _ModuleContext:
+ name: _ModuleName
+ dir: Path
+ root_build_files: List['_BuildFile']
+ sub_build_files: List['_BuildFile']
+ build_systems: List[str]
+ is_upstream: bool
+
+ def build_files(self) -> Iterable['_BuildFile']:
+ yield from self.root_build_files
+ yield from self.sub_build_files
+
+ def add_docs_file(self, file: Path):
+ for build_file in self.root_build_files:
+ build_file.add_docs_source(str(file.relative_to(self.dir)))
+
+ def add_cc_target(self, target: '_BuildFile.CcTarget') -> None:
+ for build_file in self.root_build_files:
+ build_file.add_cc_target(target)
+
+ def add_cc_test(self, target: '_BuildFile.CcTarget') -> None:
+ for build_file in self.root_build_files:
+ build_file.add_cc_test(target)
+
+
+class _BuildFile:
+ """Abstract representation of a build file for a module."""
+
+ @dataclass
+ class Target:
+ name: str
+
+ # TODO(frolv): Shouldn't be a string list as that's build system
+ # specific. Figure out a way to resolve dependencies from targets.
+ deps: List[str] = dataclasses.field(default_factory=list)
+
+ @dataclass
+ class CcTarget(Target):
+ sources: List[Path] = dataclasses.field(default_factory=list)
+ headers: List[Path] = dataclasses.field(default_factory=list)
+
+ def rebased_sources(self, rebase_path: Path) -> Iterable[str]:
+ return (str(src.relative_to(rebase_path)) for src in self.sources)
+
+ def rebased_headers(self, rebase_path: Path) -> Iterable[str]:
+ return (str(hdr.relative_to(rebase_path)) for hdr in self.headers)
+
+ def __init__(self, path: Path, ctx: _ModuleContext):
+ self._path = path
+ self._ctx = ctx
+
+ self._docs_sources: List[str] = []
+ self._cc_targets: List[_BuildFile.CcTarget] = []
+ self._cc_tests: List[_BuildFile.CcTarget] = []
+
+ @property
+ def path(self) -> Path:
+ return self._path
+
+ @property
+ def dir(self) -> Path:
+ return self._path.parent
+
+ def add_docs_source(self, filename: str) -> None:
+ self._docs_sources.append(filename)
+
+ def add_cc_target(self, target: CcTarget) -> None:
+ self._cc_targets.append(target)
+
+ def add_cc_test(self, target: CcTarget) -> None:
+ self._cc_tests.append(target)
+
+ def write(self) -> None:
+ """Writes the contents of the build file to disk."""
+ file = _OutputFile(self._path, self._indent_width())
+
+ if self._ctx.is_upstream:
+ file.line(_PIGWEED_LICENSE)
+ file.line()
+
+ self._write_preamble(file)
+
+ for target in self._cc_targets:
+ file.line()
+ self._write_cc_target(file, target)
+
+ for target in self._cc_tests:
+ file.line()
+ self._write_cc_test(file, target)
+
+ if self._docs_sources:
+ file.line()
+ self._write_docs_target(file, self._docs_sources)
+
+ file.write()
+
+ @abc.abstractmethod
+ def _indent_width(self) -> int:
+ """Returns the default indent width for the build file's code style."""
+
+ @abc.abstractmethod
+ def _write_preamble(self, file: _OutputFile) -> None:
+ """Formats"""
+
+ @abc.abstractmethod
+ def _write_cc_target(
+ self,
+ file: _OutputFile,
+ target: '_BuildFile.CcTarget',
+ ) -> None:
+ """Defines a C++ library target within the build file."""
+
+ @abc.abstractmethod
+ def _write_cc_test(
+ self,
+ file: _OutputFile,
+ target: '_BuildFile.CcTarget',
+ ) -> None:
+ """Defines a C++ unit test target within the build file."""
+
+ @abc.abstractmethod
+ def _write_docs_target(
+ self,
+ file: _OutputFile,
+ docs_sources: List[str],
+ ) -> None:
+ """Defines a documentation target within the build file."""
+
+
+# TODO(frolv): The Dict here should be Dict[str, '_GnVal'] (i.e. _GnScope),
+# but mypy does not yet support recursive types:
+# https://github.com/python/mypy/issues/731
+_GnVal = Union[bool, int, str, List[str], Dict[str, Any]]
+_GnScope = Dict[str, _GnVal]
+
+
+class _GnBuildFile(_BuildFile):
+ _DEFAULT_FILENAME = 'BUILD.gn'
+ _INCLUDE_CONFIG_TARGET = 'public_include_path'
+
+ def __init__(
+ self,
+ directory: Path,
+ ctx: _ModuleContext,
+ filename: str = _DEFAULT_FILENAME,
+ ):
+ super().__init__(directory / filename, ctx)
+
+ def _indent_width(self) -> int:
+ return 2
+
+ def _write_preamble(self, file: _OutputFile) -> None:
+ # Upstream modules always require a tests target, even if it's empty.
+ has_tests = len(self._cc_tests) > 0 or self._ctx.is_upstream
+
+ imports = []
+
+ if self._cc_targets:
+ imports.append('$dir_pw_build/target_types.gni')
+
+ if has_tests:
+ imports.append('$dir_pw_unit_test/test.gni')
+
+ if self._docs_sources:
+ imports.append('$dir_pw_docgen/docs.gni')
+
+ file.line('import("//build_overrides/pigweed.gni")\n')
+ for imp in sorted(imports):
+ file.line(f'import("{imp}")')
+
+ if self._cc_targets:
+ file.line()
+ _GnBuildFile._target(
+ file,
+ 'config',
+ _GnBuildFile._INCLUDE_CONFIG_TARGET,
+ {
+ 'include_dirs': ['public'],
+ 'visibility': [':*'],
+ },
+ )
+
+ if has_tests:
+ file.line()
+ _GnBuildFile._target(
+ file,
+ 'pw_test_group',
+ 'tests',
+ {
+ 'tests': list(f':{test.name}' for test in self._cc_tests),
+ },
+ )
+
+ def _write_cc_target(
+ self,
+ file: _OutputFile,
+ target: _BuildFile.CcTarget,
+ ) -> None:
+ """Defines a GN source_set for a C++ target."""
+
+ target_vars: _GnScope = {}
+
+ if target.headers:
+ target_vars['public_configs'] = [
+ f':{_GnBuildFile._INCLUDE_CONFIG_TARGET}'
+ ]
+ target_vars['public'] = list(target.rebased_headers(self.dir))
+
+ if target.sources:
+ target_vars['sources'] = list(target.rebased_sources(self.dir))
+
+ if target.deps:
+ target_vars['deps'] = target.deps
+
+ _GnBuildFile._target(file, 'pw_source_set', target.name, target_vars)
+
+ def _write_cc_test(
+ self,
+ file: _OutputFile,
+ target: '_BuildFile.CcTarget',
+ ) -> None:
+ _GnBuildFile._target(
+ file,
+ 'pw_test',
+ target.name,
+ {
+ 'sources': list(target.rebased_sources(self.dir)),
+ 'deps': target.deps,
+ },
+ )
+
+ def _write_docs_target(
+ self,
+ file: _OutputFile,
+ docs_sources: List[str],
+ ) -> None:
+ """Defines a pw_doc_group for module documentation."""
+ _GnBuildFile._target(
+ file,
+ 'pw_doc_group',
+ 'docs',
+ {
+ 'sources': docs_sources,
+ },
+ )
+
+ @staticmethod
+ def _target(
+ file: _OutputFile,
+ target_type: str,
+ name: str,
+ args: _GnScope,
+ ) -> None:
+ """Formats a GN target."""
+
+ file.line(f'{target_type}("{name}") {{')
+
+ with file.indent():
+ _GnBuildFile._format_gn_scope(file, args)
+
+ file.line('}')
+
+ @staticmethod
+ def _format_gn_scope(file: _OutputFile, scope: _GnScope) -> None:
+ """Formats all of the variables within a GN scope to a file.
+
+ This function does not write the enclosing braces of the outer scope to
+ support use from multiple formatting contexts.
+ """
+ for key, val in scope.items():
+ if isinstance(val, int):
+ file.line(f'{key} = {val}')
+ continue
+
+ if isinstance(val, str):
+ file.line(f'{key} = {_GnBuildFile._gn_string(val)}')
+ continue
+
+ if isinstance(val, bool):
+ file.line(f'{key} = {str(val).lower()}')
+ continue
+
+ if isinstance(val, dict):
+ file.line(f'{key} = {{')
+ with file.indent():
+ _GnBuildFile._format_gn_scope(file, val)
+ file.line('}')
+ continue
+
+ # Format a list of strings.
+ # TODO(frolv): Lists of other types?
+ assert isinstance(val, list)
+
+ if not val:
+ file.line(f'{key} = []')
+ continue
+
+ if len(val) == 1:
+ file.line(f'{key} = [ {_GnBuildFile._gn_string(val[0])} ]')
+ continue
+
+ file.line(f'{key} = [')
+ with file.indent():
+ for string in sorted(val):
+ file.line(f'{_GnBuildFile._gn_string(string)},')
+ file.line(']')
+
+ @staticmethod
+ def _gn_string(string: str) -> str:
+ """Converts a Python string into a string literal within a GN file.
+
+ Accounts for the possibility of variable interpolation within GN,
+ removing quotes if unnecessary:
+
+ "string" -> "string"
+ "string" -> "string"
+ "$var" -> var
+ "$var2" -> var2
+ "$3var" -> "$3var"
+ "$dir_pw_foo" -> dir_pw_foo
+ "$dir_pw_foo:bar" -> "$dir_pw_foo:bar"
+ "$dir_pw_foo/baz" -> "$dir_pw_foo/baz"
+ "${dir_pw_foo}" -> dir_pw_foo
+
+ """
+
+ # Check if the entire string refers to a interpolated variable.
+ #
+ # Simple case: '$' followed a single word, e.g. "$my_variable".
+ # Note that identifiers can't start with a number.
+ if re.fullmatch(r'^\$[a-zA-Z_]\w*$', string):
+ return string[1:]
+
+ # GN permits wrapping an interpolated variable in braces.
+ # Check for strings of the format "${my_variable}".
+ if re.fullmatch(r'^\$\{[a-zA-Z_]\w*\}$', string):
+ return string[2:-1]
+
+ return f'"{string}"'
+
+
+class _BazelBuildFile(_BuildFile):
+ _DEFAULT_FILENAME = 'BUILD.bazel'
+
+ def __init__(
+ self,
+ directory: Path,
+ ctx: _ModuleContext,
+ filename: str = _DEFAULT_FILENAME,
+ ):
+ super().__init__(directory / filename, ctx)
+
+ def _indent_width(self) -> int:
+ return 4
+
+ def _write_preamble(self, file: _OutputFile) -> None:
+ imports = ['//pw_build:pigweed.bzl']
+ if self._cc_targets:
+ imports.append('pw_cc_library')
+
+ if self._cc_tests:
+ imports.append('pw_cc_test')
+
+ file.line('load(')
+ with file.indent():
+ for imp in sorted(imports):
+ file.line(f'"{imp}",')
+ file.line(')\n')
+
+ file.line('package(default_visibility = ["//visibility:public"])\n')
+ file.line('licenses(["notice"])')
+
+ def _write_cc_target(
+ self,
+ file: _OutputFile,
+ target: _BuildFile.CcTarget,
+ ) -> None:
+ _BazelBuildFile._target(
+ file,
+ 'pw_cc_library',
+ target.name,
+ {
+ 'srcs': list(target.rebased_sources(self.dir)),
+ 'hdrs': list(target.rebased_headers(self.dir)),
+ 'includes': ['public'],
+ },
+ )
+
+ def _write_cc_test(
+ self,
+ file: _OutputFile,
+ target: '_BuildFile.CcTarget',
+ ) -> None:
+ _BazelBuildFile._target(
+ file,
+ 'pw_cc_test',
+ target.name,
+ {
+ 'srcs': list(target.rebased_sources(self.dir)),
+ 'deps': target.deps,
+ },
+ )
+
+ def _write_docs_target(
+ self,
+ file: _OutputFile,
+ docs_sources: List[str],
+ ) -> None:
+ file.line('# Bazel does not yet support building docs.')
+ _BazelBuildFile._target(
+ file, 'filegroup', 'docs', {'srcs': docs_sources}
+ )
+
+ @staticmethod
+ def _target(
+ file: _OutputFile,
+ target_type: str,
+ name: str,
+ keys: Dict[str, List[str]],
+ ) -> None:
+ file.line(f'{target_type}(')
+
+ with file.indent():
+ file.line(f'name = "{name}",')
+
+ for k, vals in keys.items():
+ if len(vals) == 1:
+ file.line(f'{k} = ["{vals[0]}"],')
+ continue
+
+ file.line(f'{k} = [')
+ with file.indent():
+ for val in sorted(vals):
+ file.line(f'"{val}",')
+ file.line('],')
+
+ file.line(')')
+
+
+class _CmakeBuildFile(_BuildFile):
+ _DEFAULT_FILENAME = 'CMakeLists.txt'
+
+ def __init__(
+ self,
+ directory: Path,
+ ctx: _ModuleContext,
+ filename: str = _DEFAULT_FILENAME,
+ ):
+ super().__init__(directory / filename, ctx)
+
+ def _indent_width(self) -> int:
+ return 2
+
+ def _write_preamble(self, file: _OutputFile) -> None:
+ file.line('include($ENV{PW_ROOT}/pw_build/pigweed.cmake)')
+
+ def _write_cc_target(
+ self,
+ file: _OutputFile,
+ target: _BuildFile.CcTarget,
+ ) -> None:
+ if target.name == self._ctx.name.full:
+ target_name = target.name
+ else:
+ target_name = f'{self._ctx.name.full}.{target.name}'
+
+ _CmakeBuildFile._target(
+ file,
+ 'pw_add_module_library',
+ target_name,
+ {
+ 'sources': list(target.rebased_sources(self.dir)),
+ 'headers': list(target.rebased_headers(self.dir)),
+ 'public_includes': ['public'],
+ },
+ )
+
+ def _write_cc_test(
+ self,
+ file: _OutputFile,
+ target: '_BuildFile.CcTarget',
+ ) -> None:
+ _CmakeBuildFile._target(
+ file,
+ 'pw_auto_add_module_tests',
+ self._ctx.name.full,
+ {'private_deps': []},
+ )
+
+ def _write_docs_target(
+ self,
+ file: _OutputFile,
+ docs_sources: List[str],
+ ) -> None:
+ file.line('# CMake does not yet support building docs.')
+
+ @staticmethod
+ def _target(
+ file: _OutputFile,
+ target_type: str,
+ name: str,
+ keys: Dict[str, List[str]],
+ ) -> None:
+ file.line(f'{target_type}({name}')
+
+ with file.indent():
+ for k, vals in keys.items():
+ file.line(k.upper())
+ with file.indent():
+ for val in sorted(vals):
+ file.line(val)
+
+ file.line(')')
+
+
+class _LanguageGenerator:
+ """Generates files for a programming language in a new Pigweed module."""
+
+ def __init__(self, ctx: _ModuleContext) -> None:
+ self._ctx = ctx
+
+ @abc.abstractmethod
+ def create_source_files(self) -> None:
+ """Creates the boilerplate source files required by the language."""
+
+
+class _CcLanguageGenerator(_LanguageGenerator):
+ """Generates boilerplate source files for a C++ module."""
+
+ def __init__(self, ctx: _ModuleContext) -> None:
+ super().__init__(ctx)
+
+ self._public_dir = ctx.dir / 'public'
+ self._headers_dir = self._public_dir / ctx.name.full
+
+ def create_source_files(self) -> None:
+ self._headers_dir.mkdir(parents=True)
+
+ main_header = self._new_header(self._ctx.name.main)
+ main_source = self._new_source(self._ctx.name.main)
+ test_source = self._new_source(f'{self._ctx.name.main}_test')
+
+ # TODO(frolv): This could be configurable.
+ namespace = self._ctx.name.default_namespace
+
+ main_source.line(
+ f'#include "{main_header.path.relative_to(self._public_dir)}"\n'
+ )
+ main_source.line(f'namespace {namespace} {{\n')
+ main_source.line('int magic = 42;\n')
+ main_source.line(f'}} // namespace {namespace}')
+
+ main_header.line(f'namespace {namespace} {{\n')
+ main_header.line('extern int magic;\n')
+ main_header.line(f'}} // namespace {namespace}')
+
+ test_source.line(
+ f'#include "{main_header.path.relative_to(self._public_dir)}"\n'
+ )
+ test_source.line('#include "gtest/gtest.h"\n')
+ test_source.line(f'namespace {namespace} {{')
+ test_source.line('namespace {\n')
+ test_source.line(
+ f'TEST({self._ctx.name.upper_camel_case()}, GeneratesCorrectly) {{'
+ )
+ with test_source.indent():
+ test_source.line('EXPECT_EQ(magic, 42);')
+ test_source.line('}\n')
+ test_source.line('} // namespace')
+ test_source.line(f'}} // namespace {namespace}')
+
+ self._ctx.add_cc_target(
+ _BuildFile.CcTarget(
+ name=self._ctx.name.full,
+ sources=[main_source.path],
+ headers=[main_header.path],
+ )
+ )
+
+ self._ctx.add_cc_test(
+ _BuildFile.CcTarget(
+ name=f'{self._ctx.name.main}_test',
+ deps=[f':{self._ctx.name.full}'],
+ sources=[test_source.path],
+ )
+ )
+
+ main_header.write()
+ main_source.write()
+ test_source.write()
+
+ def _new_source(self, name: str) -> _OutputFile:
+ file = _OutputFile(self._ctx.dir / f'{name}.cc')
+
+ if self._ctx.is_upstream:
+ file.line(_PIGWEED_LICENSE_CC)
+ file.line()
+
+ return file
+
+ def _new_header(self, name: str) -> _OutputFile:
+ file = _OutputFile(self._headers_dir / f'{name}.h')
+
+ if self._ctx.is_upstream:
+ file.line(_PIGWEED_LICENSE_CC)
+
+ file.line('#pragma once\n')
+ return file
+
+
+_BUILD_FILES: Dict[str, Type[_BuildFile]] = {
+ 'bazel': _BazelBuildFile,
+ 'cmake': _CmakeBuildFile,
+ 'gn': _GnBuildFile,
+}
+
+_LANGUAGE_GENERATORS: Dict[str, Type[_LanguageGenerator]] = {
+ 'cc': _CcLanguageGenerator,
+}
+
+
+def _check_module_name(
+ module: str,
+ is_upstream: bool,
+) -> Optional[_ModuleName]:
+ """Checks whether a module name is valid."""
+
+ name = _ModuleName.parse(module)
+ if not name:
+ _LOG.error(
+ '"%s" does not conform to the Pigweed module name format', module
+ )
+ return None
+
+ if is_upstream and name.prefix != 'pw':
+ _LOG.error('Modules within Pigweed itself must start with "pw_"')
+ return None
+
+ return name
+
+
+def _create_main_docs_file(ctx: _ModuleContext) -> None:
+ """Populates the top-level docs.rst file within a new module."""
+
+ docs_file = _OutputFile(ctx.dir / 'docs.rst')
+ docs_file.line(f'.. _module-{ctx.name}:\n')
+
+ title = '=' * len(ctx.name.full)
+ docs_file.line(title)
+ docs_file.line(ctx.name.full)
+ docs_file.line(title)
+ docs_file.line(f'This is the main documentation file for {ctx.name}.')
+
+ ctx.add_docs_file(docs_file.path)
+
+ docs_file.write()
+
+
+def _basic_module_setup(
+ module_name: _ModuleName,
+ module_dir: Path,
+ build_systems: Iterable[str],
+ is_upstream: bool,
+) -> _ModuleContext:
+ """Creates the basic layout of a Pigweed module."""
+ module_dir.mkdir()
+
+ ctx = _ModuleContext(
+ name=module_name,
+ dir=module_dir,
+ root_build_files=[],
+ sub_build_files=[],
+ build_systems=list(build_systems),
+ is_upstream=is_upstream,
+ )
+
+ ctx.root_build_files.extend(
+ _BUILD_FILES[build](module_dir, ctx) for build in ctx.build_systems
+ )
+
+ _create_main_docs_file(ctx)
+
+ return ctx
+
+
+def _create_module(
+ module: str, languages: Iterable[str], build_systems: Iterable[str]
+) -> None:
+ project_root = Path(os.environ.get('PW_PROJECT_ROOT', ''))
+ assert project_root.is_dir()
+
+ is_upstream = os.environ.get('PW_ROOT') == str(project_root)
+
+ module_name = _check_module_name(module, is_upstream)
+ if not module_name:
+ sys.exit(1)
+
+ if not is_upstream:
+ _LOG.error(
+ '`pw module create` is experimental and does '
+ 'not yet support downstream projects.'
+ )
+ sys.exit(1)
+
+ module_dir = project_root / module
+
+ if module_dir.is_dir():
+ _LOG.error('Module %s already exists', module)
+ sys.exit(1)
+
+ if module_dir.is_file():
+ _LOG.error(
+ 'Cannot create module %s as a file of that name already exists',
+ module,
+ )
+ sys.exit(1)
+
+ ctx = _basic_module_setup(
+ module_name, module_dir, build_systems, is_upstream
+ )
+
+ try:
+ generators = list(_LANGUAGE_GENERATORS[lang](ctx) for lang in languages)
+ except KeyError as key:
+ _LOG.error('Unsupported language: %s', key)
+ sys.exit(1)
+
+ for generator in generators:
+ generator.create_source_files()
+
+ for build_file in ctx.build_files():
+ build_file.write()
+
+ if is_upstream:
+ modules_file = project_root / 'PIGWEED_MODULES'
+ if not modules_file.exists():
+ _LOG.error(
+ 'Could not locate PIGWEED_MODULES file; '
+ 'your repository may be in a bad state.'
+ )
+ return
+
+ modules_gni_file = (
+ project_root / 'pw_build' / 'generated_pigweed_modules_lists.gni'
+ )
+
+ # Cut off the extra newline at the end of the file.
+ modules_list = modules_file.read_text().split('\n')[:-1]
+ modules_list.append(module_name.full)
+ modules_list.sort()
+ modules_list.append('')
+ modules_file.write_text('\n'.join(modules_list))
+ print(' modify ' + str(modules_file.relative_to(Path.cwd())))
+
+ generate_modules_lists.main(
+ root=project_root,
+ modules_list=modules_file,
+ modules_gni_file=modules_gni_file,
+ mode=generate_modules_lists.Mode.UPDATE,
+ )
+ print(' modify ' + str(modules_gni_file.relative_to(Path.cwd())))
+
+ print()
+ _LOG.info(
+ 'Module %s created at %s',
+ module_name,
+ module_dir.relative_to(Path.cwd()),
+ )
+
+
+def register_subcommand(parser: argparse.ArgumentParser) -> None:
+ csv = lambda s: s.split(',')
+
+ parser.add_argument(
+ '--build-systems',
+ help=(
+ 'Comma-separated list of build systems the module supports. '
+ f'Options: {", ".join(_BUILD_FILES.keys())}'
+ ),
+ type=csv,
+ default=_BUILD_FILES.keys(),
+ metavar='BUILD[,BUILD,...]',
+ )
+ parser.add_argument(
+ '--languages',
+ help=(
+ 'Comma-separated list of languages the module will use. '
+ f'Options: {", ".join(_LANGUAGE_GENERATORS.keys())}'
+ ),
+ type=csv,
+ default=[],
+ metavar='LANG[,LANG,...]',
+ )
+ parser.add_argument(
+ 'module', help='Name of the module to create.', metavar='MODULE_NAME'
+ )
+ parser.set_defaults(func=_create_module)
diff --git a/pw_multisink/CMakeLists.txt b/pw_multisink/CMakeLists.txt
index 4d751805d..564e4b875 100644
--- a/pw_multisink/CMakeLists.txt
+++ b/pw_multisink/CMakeLists.txt
@@ -16,7 +16,7 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_module_config(pw_multisink_CONFIG)
-pw_add_module_library(pw_multisink.config
+pw_add_library(pw_multisink.config INTERFACE
HEADERS
public/pw_multisink/config.h
PUBLIC_INCLUDES
@@ -25,7 +25,7 @@ pw_add_module_library(pw_multisink.config
${pw_multisink_CONFIG}
)
-pw_add_module_library(pw_multisink
+pw_add_library(pw_multisink STATIC
HEADERS
public/pw_multisink/multisink.h
PUBLIC_INCLUDES
@@ -49,7 +49,7 @@ pw_add_module_library(pw_multisink
pw_varint
)
-pw_add_module_library(pw_multisink.util
+pw_add_library(pw_multisink.util STATIC
HEADERS
public/pw_multisink/util.h
PUBLIC_INCLUDES
@@ -65,7 +65,7 @@ pw_add_module_library(pw_multisink.util
pw_function
)
-pw_add_module_library(pw_multisink.test_thread
+pw_add_library(pw_multisink.test_thread INTERFACE
HEADERS
public/pw_multisink/test_thread.h
PUBLIC_INCLUDES
@@ -79,12 +79,13 @@ pw_add_module_library(pw_multisink.test_thread
# target that depends on this pw_add_module_library and a pw_add_module_library
# that provides the implementaiton of pw_multisink.test_thread. See
# pw_multisink.stl_multisink_test as an example.
-pw_add_module_library(pw_multisink.multisink_threaded_test
+pw_add_library(pw_multisink.multisink_threaded_test STATIC
SOURCES
multisink_threaded_test.cc
PRIVATE_DEPS
pw_multisink
pw_multisink.test_thread
+ pw_string
pw_thread.thread
pw_thread.yield
pw_unit_test
@@ -93,18 +94,17 @@ pw_add_module_library(pw_multisink.multisink_threaded_test
pw_add_test(pw_multisink.multisink_test
SOURCES
multisink_test.cc
- DEPS
+ PRIVATE_DEPS
pw_function
pw_multisink
- pw_polyfill.cstddef
- pw_polyfill.span
+ pw_span
pw_status
GROUPS
modules
pw_multisink
)
-pw_add_module_library(pw_multisink.stl_test_thread
+pw_add_library(pw_multisink.stl_test_thread STATIC
SOURCES
stl_test_thread.cc
PRIVATE_DEPS
@@ -115,9 +115,8 @@ pw_add_module_library(pw_multisink.stl_test_thread
if("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread")
pw_add_test(pw_multisink.stl_multisink_threaded_test
- DEPS
- pw_polyfill.cstddef
- pw_polyfill.span
+ PRIVATE_DEPS
+ pw_span
pw_multisink.multisink_threaded_test
pw_multisink.stl_test_thread
GROUPS
diff --git a/pw_multisink/multisink.cc b/pw_multisink/multisink.cc
index b1b95f769..b8704b297 100644
--- a/pw_multisink/multisink.cc
+++ b/pw_multisink/multisink.cc
@@ -66,8 +66,12 @@ Status MultiSink::PopEntry(Drain& drain, const Drain::PeekedEntry& entry) {
// still held, there shouldn't be any modifications to the multisink in
// between peeking and popping.
PW_CHECK_OK(drain.reader_.PopFront());
- drain.last_handled_sequence_id_ = next_entry_sequence_id;
}
+ // If the entry's sequence id is not the next one it means that the
+ // multisink advanced since PeekEntry() was called. Advance the last handled
+ // sequence id to the passed entry anyway to mark the fact that the dropped
+ // messages reported on PeekEntry() are handled.
+ drain.last_handled_sequence_id_ = entry.sequence_id();
return OkStatus();
}
@@ -75,12 +79,12 @@ Result<ConstByteSpan> MultiSink::PeekOrPopEntry(
Drain& drain,
ByteSpan buffer,
Request request,
- uint32_t& drop_count_out,
+ uint32_t& drain_drop_count_out,
uint32_t& ingress_drop_count_out,
uint32_t& entry_sequence_id_out) {
size_t bytes_read = 0;
entry_sequence_id_out = 0;
- drop_count_out = 0;
+ drain_drop_count_out = 0;
ingress_drop_count_out = 0;
std::lock_guard lock(lock_);
@@ -108,18 +112,19 @@ Result<ConstByteSpan> MultiSink::PeekOrPopEntry(
// current and last sequence IDs. Consecutive successful reads will always
// differ by one at least, so it is subtracted out. If the read was not
// successful, the difference is not adjusted.
- drop_count_out = entry_sequence_id_out - drain.last_handled_sequence_id_ -
- (peek_status.ok() ? 1 : 0);
+ drain_drop_count_out = entry_sequence_id_out -
+ drain.last_handled_sequence_id_ -
+ (peek_status.ok() ? 1 : 0);
// Only report the ingress drop count when the drain catches up to where the
// drop happened, accounting only for the drops found and no more, as
// indicated by the gap in sequence IDs.
- if (drop_count_out > 0) {
+ if (drain_drop_count_out > 0) {
ingress_drop_count_out =
- std::min(drop_count_out,
+ std::min(drain_drop_count_out,
total_ingress_drops_ - drain.last_handled_ingress_drop_count_);
- // Remove the ingress drop count duplicated in drop_count_out.
- drop_count_out -= ingress_drop_count_out;
+ // Remove the ingress drop count duplicated in drain_drop_count_out.
+ drain_drop_count_out -= ingress_drop_count_out;
// Check if all the ingress drops were reported.
drain.last_handled_ingress_drop_count_ =
total_ingress_drops_ > ingress_drop_count_out
@@ -138,7 +143,7 @@ Result<ConstByteSpan> MultiSink::PeekOrPopEntry(
PW_CHECK(drain.reader_.PopFront().ok());
drain.last_handled_sequence_id_ = entry_sequence_id_out;
}
- return std::as_bytes(buffer.first(bytes_read));
+ return as_bytes(buffer.first(bytes_read));
}
void MultiSink::AttachDrain(Drain& drain) {
@@ -225,7 +230,7 @@ Status MultiSink::Drain::PopEntry(const PeekedEntry& entry) {
Result<MultiSink::Drain::PeekedEntry> MultiSink::Drain::PeekEntry(
ByteSpan buffer,
- uint32_t& drop_count_out,
+ uint32_t& drain_drop_count_out,
uint32_t& ingress_drop_count_out) {
PW_DCHECK_NOTNULL(multisink_);
uint32_t entry_sequence_id_out;
@@ -233,7 +238,7 @@ Result<MultiSink::Drain::PeekedEntry> MultiSink::Drain::PeekEntry(
multisink_->PeekOrPopEntry(*this,
buffer,
Request::kPeek,
- drop_count_out,
+ drain_drop_count_out,
ingress_drop_count_out,
entry_sequence_id_out);
if (!peek_result.ok()) {
@@ -244,14 +249,14 @@ Result<MultiSink::Drain::PeekedEntry> MultiSink::Drain::PeekEntry(
Result<ConstByteSpan> MultiSink::Drain::PopEntry(
ByteSpan buffer,
- uint32_t& drop_count_out,
+ uint32_t& drain_drop_count_out,
uint32_t& ingress_drop_count_out) {
PW_DCHECK_NOTNULL(multisink_);
uint32_t entry_sequence_id_out;
return multisink_->PeekOrPopEntry(*this,
buffer,
Request::kPop,
- drop_count_out,
+ drain_drop_count_out,
ingress_drop_count_out,
entry_sequence_id_out);
}
diff --git a/pw_multisink/multisink_test.cc b/pw_multisink/multisink_test.cc
index 1f5bea91e..3489a0629 100644
--- a/pw_multisink/multisink_test.cc
+++ b/pw_multisink/multisink_test.cc
@@ -18,11 +18,11 @@
#include <cstdint>
#include <cstring>
#include <optional>
-#include <span>
#include <string_view>
#include "gtest/gtest.h"
#include "pw_function/function.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::multisink {
@@ -52,7 +52,7 @@ class MultiSinkTest : public ::testing::Test {
static constexpr size_t kEntryBufferSize = 1024;
static constexpr size_t kBufferSize = 5 * kEntryBufferSize;
- MultiSinkTest() : multisink_(buffer_) {}
+ MultiSinkTest() : buffer_{}, multisink_(buffer_) {}
// Expects the peeked or popped message to equal the provided non-empty
// message, and the drop count to match. If `expected_message` is empty, the
@@ -260,7 +260,7 @@ TEST_F(MultiSinkTest, TooSmallBuffer) {
// Attempting to acquire an entry with a small buffer should result in
// RESOURCE_EXHAUSTED and remove it.
Result<ConstByteSpan> result = drains_[0].PopEntry(
- std::span(entry_buffer_, 1), drop_count, ingress_drop_count);
+ span(entry_buffer_, 1), drop_count, ingress_drop_count);
EXPECT_EQ(result.status(), Status::ResourceExhausted());
VerifyPopEntry(drains_[0], std::nullopt, 1u, 1u);
@@ -428,6 +428,7 @@ TEST_F(MultiSinkTest, PeekReportsSlowDrainDropCount) {
std::array<std::byte, message_size> message;
std::memset(message.data(), 'a', message.size());
for (size_t i = 0; i < max_multisink_messages; ++i) {
+ message[0] = static_cast<std::byte>(i);
multisink_.HandleEntry(message);
}
@@ -436,15 +437,43 @@ TEST_F(MultiSinkTest, PeekReportsSlowDrainDropCount) {
// Account for that offset.
const size_t expected_drops = 5;
for (size_t i = 1; i < expected_drops; ++i) {
+ message[0] = static_cast<std::byte>(200 + i);
multisink_.HandleEntry(message);
}
uint32_t drop_count = 0;
uint32_t ingress_drop_count = 0;
+
auto peek_result =
drains_[0].PeekEntry(entry_buffer_, drop_count, ingress_drop_count);
+ // The message peeked is the 6th message added.
+ message[0] = static_cast<std::byte>(5);
VerifyPeekResult(
peek_result, drop_count, ingress_drop_count, message, expected_drops, 0);
+
+ // Add 3 more messages since we peeked the multisink, generating 2 more drops.
+ const size_t expected_drops2 = 2;
+ for (size_t i = 0; i < expected_drops2 + 1; ++i) {
+ message[0] = static_cast<std::byte>(220 + i);
+ multisink_.HandleEntry(message);
+ }
+
+ // Pop the 6th message now, even though it was already dropped.
+ EXPECT_EQ(drains_[0].PopEntry(peek_result.value()), OkStatus());
+
+ // A new peek would get the 9th message because two more messages were
+ // dropped. Given that PopEntry() was called with peek_result, all the dropped
+ // messages before peek_result should be considered handled and only the two
+ // new drops should be reported here.
+ auto peek_result2 =
+ drains_[0].PeekEntry(entry_buffer_, drop_count, ingress_drop_count);
+ message[0] = static_cast<std::byte>(8); // 9th message
+ VerifyPeekResult(peek_result2,
+ drop_count,
+ ingress_drop_count,
+ message,
+ expected_drops2,
+ 0);
}
TEST_F(MultiSinkTest, IngressDropCountOverflow) {
@@ -510,13 +539,13 @@ TEST(UnsafeIteration, NoLimit) {
MultiSink multisink(buffer);
for (std::string_view entry : kExpectedEntries) {
- multisink.HandleEntry(std::as_bytes(std::span(entry)));
+ multisink.HandleEntry(as_bytes(span<const char>(entry)));
}
size_t entry_count = 0;
struct {
size_t& entry_count;
- std::span<const std::string_view> expected_results;
+ span<const std::string_view> expected_results;
} ctx{entry_count, kExpectedEntries};
auto cb = [&ctx](ConstByteSpan data) {
std::string_view expected_entry = ctx.expected_results[ctx.entry_count];
@@ -541,13 +570,13 @@ TEST(UnsafeIteration, Subset) {
MultiSink multisink(buffer);
for (std::string_view entry : kExpectedEntries) {
- multisink.HandleEntry(std::as_bytes(std::span(entry)));
+ multisink.HandleEntry(as_bytes(span<const char>(entry)));
}
size_t entry_count = 0;
struct {
size_t& entry_count;
- std::span<const std::string_view> expected_results;
+ span<const std::string_view> expected_results;
} ctx{entry_count, kExpectedEntries};
auto cb = [&ctx](ConstByteSpan data) {
std::string_view expected_entry =
diff --git a/pw_multisink/multisink_threaded_test.cc b/pw_multisink/multisink_threaded_test.cc
index f9941b55a..30c553609 100644
--- a/pw_multisink/multisink_threaded_test.cc
+++ b/pw_multisink/multisink_threaded_test.cc
@@ -14,12 +14,12 @@
#include <cstddef>
#include <cstdint>
-#include <span>
#include "gtest/gtest.h"
#include "pw_containers/vector.h"
#include "pw_multisink/multisink.h"
#include "pw_multisink/test_thread.h"
+#include "pw_span/span.h"
#include "pw_string/string_builder.h"
#include "pw_thread/thread.h"
#include "pw_thread/yield.h"
@@ -30,7 +30,7 @@ constexpr size_t kEntryBufferSize = sizeof("message 000");
constexpr size_t kMaxMessageCount = 250;
constexpr size_t kBufferSize = kMaxMessageCount * kEntryBufferSize;
-using MessageSpan = std::span<const StringBuffer<kEntryBufferSize>>;
+using MessageSpan = span<const StringBuffer<kEntryBufferSize>>;
void CompareSentAndReceivedMessages(const MessageSpan& sent_messages,
const MessageSpan& received_messages) {
@@ -95,7 +95,7 @@ class LogPopReaderThread : public thread::ThreadCore {
void Run() override {
multisink_.AttachDrain(drain_);
ReadAllEntries();
- };
+ }
virtual void ReadAllEntries() {
do {
@@ -136,7 +136,7 @@ class LogPeekAndCommitReaderThread : public LogPopReaderThread {
uint32_t expected_message_and_drop_count)
: LogPopReaderThread(multisink, expected_message_and_drop_count) {}
- virtual void ReadAllEntries() {
+ void ReadAllEntries() override {
do {
uint32_t drop_count = 0;
uint32_t ingress_drop_count = 0;
@@ -171,11 +171,10 @@ class LogWriterThread : public thread::ThreadCore {
void Run() override {
for (const auto& message : message_stack_) {
- multisink_.HandleEntry(
- std::as_bytes(std::span(std::string_view(message))));
+ multisink_.HandleEntry(as_bytes(span(std::string_view(message))));
pw::this_thread::yield();
}
- };
+ }
private:
MultiSink& multisink_;
diff --git a/pw_multisink/public/pw_multisink/multisink.h b/pw_multisink/public/pw_multisink/multisink.h
index 5942304f4..c65d03dd3 100644
--- a/pw_multisink/public/pw_multisink/multisink.h
+++ b/pw_multisink/public/pw_multisink/multisink.h
@@ -70,7 +70,7 @@ class MultiSink {
// drop count in parallel.
//
// If the read operation was successful or returned OutOfRange (i.e. no
- // entries to read) then the `drop_count_out` is set to the number of
+ // entries to read) then the `drain_drop_count_out` is set to the number of
// entries that were dropped since the last call to PopEntry due to
// advancing the drain, and `ingress_drop_count_out` is set to the number of
// logs that were dropped before being added to the MultiSink. Otherwise,
@@ -133,8 +133,8 @@ class MultiSink {
// RESOURCE_EXHAUSTED - The provided buffer was not large enough to store
// the next available entry, which was discarded.
Result<ConstByteSpan> PopEntry(ByteSpan buffer,
- uint32_t& drop_count_out,
- uint32_t& ingress_drop_count)
+ uint32_t& drain_drop_count_out,
+ uint32_t& ingress_drop_count_out)
PW_LOCKS_EXCLUDED(multisink_->lock_);
// Overload that combines drop counts.
// TODO(cachinchilla): remove when downstream projects migrated to new API.
@@ -180,8 +180,8 @@ class MultiSink {
// count, without moving the drain forward, except if there is a
// RESOURCE_EXHAUSTED error when peeking, in which case the drain is
// automatically advanced.
- // The `drop_count_out` follows the same logic as `PopEntry`. The user must
- // call `PopEntry` once the data in peek was used successfully.
+ // The `drain_drop_count_out` follows the same logic as `PopEntry`. The user
+ // must call `PopEntry` once the data in peek was used successfully.
//
// Precondition: the buffer data must not be corrupt, otherwise there will
// be a crash.
@@ -193,8 +193,8 @@ class MultiSink {
// RESOURCE_EXHAUSTED - The provided buffer was not large enough to store
// the next available entry, which was discarded.
Result<PeekedEntry> PeekEntry(ByteSpan buffer,
- uint32_t& drop_count_out,
- uint32_t& ingress_drop_count)
+ uint32_t& drain_drop_count_out,
+ uint32_t& ingress_drop_count_out)
PW_LOCKS_EXCLUDED(multisink_->lock_);
// Drains are not copyable or movable.
@@ -313,7 +313,7 @@ class MultiSink {
MultiSink(ByteSpan buffer)
: ring_buffer_(true), sequence_id_(0), total_ingress_drops_(0) {
ring_buffer_.SetBuffer(buffer)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
AttachDrain(oldest_entry_drain_);
}
@@ -397,8 +397,8 @@ class MultiSink {
//
// Returns:
// OK - An entry was successfully read from the multisink. The
- // `drop_count_out` is set to the difference between the current sequence ID
- // and the last handled ID.
+ // `drain_drop_count_out` is set to the difference between the current
+ // sequence ID and the last handled ID.
// FAILED_PRECONDITION - The drain is not attached to
// a multisink.
// RESOURCE_EXHAUSTED - The provided buffer was not large enough to store
@@ -406,7 +406,7 @@ class MultiSink {
Result<ConstByteSpan> PeekOrPopEntry(Drain& drain,
ByteSpan buffer,
Request request,
- uint32_t& drop_count_out,
+ uint32_t& drain_drop_count_out,
uint32_t& ingress_drop_count_out,
uint32_t& entry_sequence_id_out)
PW_LOCKS_EXCLUDED(lock_);
diff --git a/pw_multisink/util.cc b/pw_multisink/util.cc
index 825701de6..b3ad6616e 100644
--- a/pw_multisink/util.cc
+++ b/pw_multisink/util.cc
@@ -25,8 +25,10 @@ Status UnsafeDumpMultiSinkLogs(MultiSink& sink,
pw::log::LogEntries::StreamEncoder& encoder,
size_t max_num_entries) {
auto callback = [&encoder](ConstByteSpan entry) {
- encoder.WriteBytes(
- static_cast<uint32_t>(pw::log::LogEntries::Fields::ENTRIES), entry);
+ encoder
+ .WriteBytes(
+ static_cast<uint32_t>(pw::log::LogEntries::Fields::kEntries), entry)
+ .IgnoreError();
};
return sink.UnsafeForEachEntry(callback, max_num_entries);
}
diff --git a/pw_package/BUILD.gn b/pw_package/BUILD.gn
index dd021e82f..a4d5ebe10 100644
--- a/pw_package/BUILD.gn
+++ b/pw_package/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_package/docs.rst b/pw_package/docs.rst
index 78d99b8f4..63a4e0eeb 100644
--- a/pw_package/docs.rst
+++ b/pw_package/docs.rst
@@ -21,7 +21,8 @@ In general, these should not be used by projects using Pigweed. Pigweed uses
these packages to avoid using submodules so downstream projects don't have
multiple copies of a given repository in their source tree. Projects using
Pigweed should use submodules instead of packages because submodules are
-supported by much more mature tooling: git.
+supported by much more mature tooling: git. To install these packages anyway,
+use ``--force`` on the command line or ``force=True`` in Python code.
-----
Usage
@@ -44,6 +45,9 @@ has several subcommands.
``pw package remove <package-name>``
Removes ``<package-name>``.
+By default ``pw package`` operates on the directory referenced by
+``PW_PACKAGE_ROOT``.
+
-----------
Configuring
-----------
diff --git a/pw_package/py/BUILD.gn b/pw_package/py/BUILD.gn
index 1af696e78..c180a50ac 100644
--- a/pw_package/py/BUILD.gn
+++ b/pw_package/py/BUILD.gn
@@ -31,6 +31,7 @@ pw_python_package("py") {
"pw_package/packages/boringssl.py",
"pw_package/packages/chromium_verifier.py",
"pw_package/packages/crlset.py",
+ "pw_package/packages/emboss.py",
"pw_package/packages/freertos.py",
"pw_package/packages/googletest.py",
"pw_package/packages/mbedtls.py",
@@ -43,6 +44,7 @@ pw_python_package("py") {
"pw_package/pigweed_packages.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
python_deps = [
"$dir_pw_arduino_build/py",
"$dir_pw_stm32cube_build/py",
diff --git a/pw_package/py/pw_package/git_repo.py b/pw_package/py/pw_package/git_repo.py
index 348775d25..f18b45f51 100644
--- a/pw_package/py/pw_package/git_repo.py
+++ b/pw_package/py/pw_package/git_repo.py
@@ -25,29 +25,31 @@ import pw_package.package_manager
PathOrStr = Union[pathlib.Path, str]
-def git_stdout(*args: PathOrStr,
- show_stderr=False,
- repo: PathOrStr = '.') -> str:
- return subprocess.run(['git', '-C', repo, *args],
- stdout=subprocess.PIPE,
- stderr=None if show_stderr else subprocess.DEVNULL,
- check=True).stdout.decode().strip()
-
-
-def git(*args: PathOrStr,
- repo: PathOrStr = '.') -> subprocess.CompletedProcess:
+def git_stdout(
+ *args: PathOrStr, show_stderr=False, repo: PathOrStr = '.'
+) -> str:
+ return (
+ subprocess.run(
+ ['git', '-C', repo, *args],
+ stdout=subprocess.PIPE,
+ stderr=None if show_stderr else subprocess.DEVNULL,
+ check=True,
+ )
+ .stdout.decode()
+ .strip()
+ )
+
+
+def git(*args: PathOrStr, repo: PathOrStr = '.') -> subprocess.CompletedProcess:
return subprocess.run(['git', '-C', repo, *args], check=True)
class GitRepo(pw_package.package_manager.Package):
"""Install and check status of Git repository-based packages."""
- def __init__(self,
- url,
- *args,
- commit='',
- tag='',
- sparse_list=None,
- **kwargs):
+
+ def __init__(
+ self, url, *args, commit='', tag='', sparse_list=None, **kwargs
+ ):
super().__init__(*args, **kwargs)
if not (commit or tag):
raise ValueError('git repo must specify a commit or tag')
@@ -56,8 +58,10 @@ class GitRepo(pw_package.package_manager.Package):
self._commit = commit
self._tag = tag
self._sparse_list = sparse_list
+ self._allow_use_in_downstream = False
def status(self, path: pathlib.Path) -> bool:
+ # TODO(tonymd): Check the correct SHA is checked out here.
if not os.path.isdir(path / '.git'):
return False
@@ -112,8 +116,7 @@ class GitRepo(pw_package.package_manager.Package):
git('clone', '--filter=blob:none', self._url, path)
git('reset', '--hard', self._commit, repo=path)
elif self._tag:
- git('clone', '-b', self._tag, '--filter=blob:none', self._url,
- path)
+ git('clone', '-b', self._tag, '--filter=blob:none', self._url, path)
def checkout_sparse(self, path: pathlib.Path) -> None:
# sparse checkout
@@ -131,6 +134,9 @@ class GitRepo(pw_package.package_manager.Package):
git('pull', '--depth=1', 'origin', target, repo=path)
def check_sparse_list(self, path: pathlib.Path) -> bool:
- sparse_list = git_stdout('sparse-checkout', 'list',
- repo=path).strip('\n').splitlines()
+ sparse_list = (
+ git_stdout('sparse-checkout', 'list', repo=path)
+ .strip('\n')
+ .splitlines()
+ )
return set(sparse_list) == set(self._sparse_list)
diff --git a/pw_package/py/pw_package/package_manager.py b/pw_package/py/pw_package/package_manager.py
index 125412112..6221798ec 100644
--- a/pw_package/py/pw_package/package_manager.py
+++ b/pw_package/py/pw_package/package_manager.py
@@ -19,7 +19,7 @@ import logging
import os
import pathlib
import shutil
-from typing import Dict, List, Sequence, Tuple
+from typing import Dict, List, Optional, Sequence, Tuple
_LOG: logging.Logger = logging.getLogger(__name__)
@@ -29,14 +29,22 @@ class Package:
Subclass this to implement installation of a specific package.
"""
+
def __init__(self, name):
self._name = name
+ self._allow_use_in_downstream = True
@property
def name(self):
return self._name
- def install(self, path: pathlib.Path) -> None: # pylint: disable=no-self-use
+ @property
+ def allow_use_in_downstream(self):
+ return self._allow_use_in_downstream
+
+ def install(
+ self, path: pathlib.Path
+ ) -> None: # pylint: disable=no-self-use
"""Install the package at path.
Install the package in path. Cannot assume this directory is empty—it
@@ -53,14 +61,22 @@ class Package:
if os.path.exists(path):
shutil.rmtree(path)
- def status(self, path: pathlib.Path) -> bool: # pylint: disable=no-self-use
+ def status( # pylint: disable=no-self-use
+ self,
+ path: pathlib.Path, # pylint: disable=unused-argument
+ ) -> bool:
"""Returns if package is installed at path and current.
This method will be skipped if the directory does not exist.
"""
+ return False
- def info(self, path: pathlib.Path) -> Sequence[str]: # pylint: disable=no-self-use
+ def info( # pylint: disable=no-self-use
+ self,
+ path: pathlib.Path, # pylint: disable=unused-argument
+ ) -> Sequence[str]:
"""Returns a short string explaining how to enable the package."""
+ return []
_PACKAGES: Dict[str, Package] = {}
@@ -78,14 +94,39 @@ class Packages:
available: Tuple[str, ...]
+class UpstreamOnlyPackageError(Exception):
+ def __init__(self, pkg_name):
+ super().__init__(
+ f'Package {pkg_name} is an upstream-only package--it should be '
+ 'imported as a submodule and not a package'
+ )
+
+
class PackageManager:
"""Install and remove optional packages."""
+
def __init__(self, root: pathlib.Path):
self._pkg_root = root
os.makedirs(root, exist_ok=True)
def install(self, package: str, force: bool = False) -> None:
+ """Install the named package.
+
+ Args:
+ package: The name of the package to install.
+ force: Install the package regardless of whether it's already
+ installed or if it's not "allowed" to be installed on this
+ project.
+ """
+
pkg = _PACKAGES[package]
+ if not pkg.allow_use_in_downstream:
+ if os.environ.get('PW_ROOT') != os.environ.get('PW_PROJECT_ROOT'):
+ if force:
+ _LOG.warning(str(UpstreamOnlyPackageError(pkg.name)))
+ else:
+ raise UpstreamOnlyPackageError(pkg.name)
+
if force:
self.remove(package)
pkg.install(self._pkg_root / pkg.name)
@@ -122,6 +163,7 @@ class PackageManager:
class PackageManagerCLI:
"""Command-line interface to PackageManager."""
+
def __init__(self):
self._mgr: PackageManager = None
@@ -171,15 +213,14 @@ class PackageManagerCLI:
return getattr(self, command)(**kwargs)
-def parse_args(argv: List[str] = None) -> argparse.Namespace:
+def parse_args(argv: Optional[List[str]] = None) -> argparse.Namespace:
parser = argparse.ArgumentParser("Manage packages.")
parser.add_argument(
'--package-root',
'-e',
dest='pkg_root',
type=pathlib.Path,
- default=(pathlib.Path(os.environ['_PW_ACTUAL_ENVIRONMENT_ROOT']) /
- 'packages'),
+ default=pathlib.Path(os.environ['PW_PACKAGE_ROOT']),
)
subparsers = parser.add_subparsers(dest='command', required=True)
install = subparsers.add_parser('install')
diff --git a/pw_package/py/pw_package/packages/arduino_core.py b/pw_package/py/pw_package/packages/arduino_core.py
index c630c939f..ecbbfe4d3 100644
--- a/pw_package/py/pw_package/packages/arduino_core.py
+++ b/pw_package/py/pw_package/packages/arduino_core.py
@@ -30,6 +30,7 @@ _LOG: logging.Logger = logging.getLogger(__name__)
class ArduinoCore(pw_package.package_manager.Package):
"""Install and check status of arduino cores."""
+
def __init__(self, core_name, *args, **kwargs):
super().__init__(*args, name=core_name, **kwargs)
@@ -49,8 +50,9 @@ class ArduinoCore(pw_package.package_manager.Package):
# Check if teensy cipd package is readable
- with tempfile.NamedTemporaryFile(prefix='cipd',
- delete=True) as temp_json:
+ with tempfile.NamedTemporaryFile(
+ prefix='cipd', delete=True
+ ) as temp_json:
cipd_acl_check_command = [
"cipd",
"acl-check",
@@ -67,18 +69,26 @@ class ArduinoCore(pw_package.package_manager.Package):
def _run_command(command):
_LOG.debug("Running: `%s`", " ".join(command))
result = subprocess.run(command, capture_output=True)
- _LOG.debug("Output:\n%s",
- result.stdout.decode() + result.stderr.decode())
+ _LOG.debug(
+ "Output:\n%s", result.stdout.decode() + result.stderr.decode()
+ )
_run_command(["cipd", "init", "-force", core_cache_path.as_posix()])
- _run_command([
- "cipd", "install", cipd_package_subpath, "-root",
- core_cache_path.as_posix(), "-force"
- ])
+ _run_command(
+ [
+ "cipd",
+ "install",
+ cipd_package_subpath,
+ "-root",
+ core_cache_path.as_posix(),
+ "-force",
+ ]
+ )
_LOG.debug(
"Available Cache Files:\n%s",
- "\n".join([p.as_posix() for p in core_cache_path.glob("*")]))
+ "\n".join([p.as_posix() for p in core_cache_path.glob("*")]),
+ )
def install(self, path: Path) -> None:
self.populate_download_cache_from_cipd(path)
@@ -86,8 +96,7 @@ class ArduinoCore(pw_package.package_manager.Package):
if self.status(path):
return
# Otherwise delete current version and reinstall
- core_installer.install_core(path.parent.resolve().as_posix(),
- self.name)
+ core_installer.install_core(path.parent.resolve().as_posix(), self.name)
def info(self, path: Path) -> Sequence[str]:
packages_root = path.parent.resolve()
@@ -102,18 +111,17 @@ class ArduinoCore(pw_package.package_manager.Package):
message_gn_args = [
'Enable by running "gn args out" and adding these lines:',
f' pw_arduino_build_CORE_PATH = "{packages_root}"',
- f' pw_arduino_build_CORE_NAME = "{self.name}"'
+ f' pw_arduino_build_CORE_NAME = "{self.name}"',
]
# Search for first valid 'package/version' directory
for hardware_dir in [
- path for path in (path / 'hardware').iterdir()
- if path.is_dir()
+ path for path in (path / 'hardware').iterdir() if path.is_dir()
]:
if path.name in ["arduino", "tools"]:
continue
for subdir in [
- path for path in hardware_dir.iterdir() if path.is_dir()
+ path for path in hardware_dir.iterdir() if path.is_dir()
]:
if subdir.name == 'avr' or re.match(r'[0-9.]+', subdir.name):
arduino_package_name = f'{hardware_dir.name}/{subdir.name}'
@@ -122,7 +130,7 @@ class ArduinoCore(pw_package.package_manager.Package):
if arduino_package_name:
message_gn_args += [
f' pw_arduino_build_PACKAGE_NAME = "{arduino_package_name}"',
- ' pw_arduino_build_BOARD = "BOARD_NAME"'
+ ' pw_arduino_build_BOARD = "BOARD_NAME"',
]
message += ["\n".join(message_gn_args)]
message += [
@@ -131,11 +139,12 @@ class ArduinoCore(pw_package.package_manager.Package):
'List available boards by running:\n'
' arduino_builder '
f'--arduino-package-path {arduino_package_path} '
- f'--arduino-package-name {arduino_package_name} list-boards'
+ f'--arduino-package-name {arduino_package_name} list-boards',
]
return message
for arduino_core_name in core_installer.supported_cores():
- pw_package.package_manager.register(ArduinoCore,
- core_name=arduino_core_name)
+ pw_package.package_manager.register(
+ ArduinoCore, core_name=arduino_core_name
+ )
diff --git a/pw_package/py/pw_package/packages/boringssl.py b/pw_package/py/pw_package/packages/boringssl.py
index f791ef0bb..1de38f8cf 100644
--- a/pw_package/py/pw_package/packages/boringssl.py
+++ b/pw_package/py/pw_package/packages/boringssl.py
@@ -27,15 +27,20 @@ def boringssl_repo_path(path: pathlib.Path) -> pathlib.Path:
class BoringSSL(pw_package.package_manager.Package):
"""Install and check status of BoringSSL and chromium verifier."""
+
def __init__(self, *args, **kwargs):
super().__init__(*args, name='boringssl', **kwargs)
self._boringssl = pw_package.git_repo.GitRepo(
name='boringssl',
- url=''.join([
- 'https://pigweed.googlesource.com',
- '/third_party/boringssl/boringssl'
- ]),
- commit='9f55d972854d0b34dae39c7cd3679d6ada3dfd5b')
+ url=''.join(
+ [
+ 'https://pigweed.googlesource.com',
+ '/third_party/boringssl/boringssl',
+ ]
+ ),
+ commit='9f55d972854d0b34dae39c7cd3679d6ada3dfd5b',
+ )
+ self._allow_use_in_downstream = False
def status(self, path: pathlib.Path) -> bool:
if not self._boringssl.status(boringssl_repo_path(path)):
diff --git a/pw_package/py/pw_package/packages/chromium_verifier.py b/pw_package/py/pw_package/packages/chromium_verifier.py
index 4474a6d1d..864a00ca8 100644
--- a/pw_package/py/pw_package/packages/chromium_verifier.py
+++ b/pw_package/py/pw_package/packages/chromium_verifier.py
@@ -57,30 +57,35 @@ CHROMIUM_VERIFIER_UNITTEST_SOURCES = [
'net/cert/internal/path_builder_unittest.cc',
]
-CHROMIUM_VERIFIER_SOURCES = CHROMIUM_VERIFIER_LIBRARY_SOURCES +\
- CHROMIUM_VERIFIER_UNITTEST_SOURCES
+CHROMIUM_VERIFIER_SOURCES = (
+ CHROMIUM_VERIFIER_LIBRARY_SOURCES + CHROMIUM_VERIFIER_UNITTEST_SOURCES
+)
def chromium_verifier_repo_path(
- chromium_verifier_install: pathlib.Path) -> pathlib.Path:
+ chromium_verifier_install: pathlib.Path,
+) -> pathlib.Path:
"""Return the sub-path for repo checkout of chromium verifier"""
return chromium_verifier_install / 'src'
def chromium_third_party_boringssl_repo_path(
- chromium_verifier_repo: pathlib.Path) -> pathlib.Path:
+ chromium_verifier_repo: pathlib.Path,
+) -> pathlib.Path:
"""Returns the path of third_party/boringssl library in chromium repo"""
return chromium_verifier_repo / 'third_party' / 'boringssl' / 'src'
def chromium_third_party_googletest_repo_path(
- chromium_verifier_repo: pathlib.Path) -> pathlib.Path:
+ chromium_verifier_repo: pathlib.Path,
+) -> pathlib.Path:
"""Returns the path of third_party/googletest in chromium repo"""
return chromium_verifier_repo / 'third_party' / 'googletest' / 'src'
class ChromiumVerifier(pw_package.package_manager.Package):
"""Install and check status of Chromium Verifier"""
+
def __init__(self, *args, **kwargs):
super().__init__(*args, name='chromium_verifier', **kwargs)
self._chromium_verifier = pw_package.git_repo.GitRepo(
@@ -96,25 +101,30 @@ class ChromiumVerifier(pw_package.package_manager.Package):
self._boringssl = pw_package.git_repo.GitRepo(
name='boringssl',
- url=''.join([
- 'https://pigweed.googlesource.com',
- '/third_party/boringssl/boringssl'
- ]),
+ url=''.join(
+ [
+ 'https://pigweed.googlesource.com',
+ '/third_party/boringssl/boringssl',
+ ]
+ ),
commit='9f55d972854d0b34dae39c7cd3679d6ada3dfd5b',
sparse_list=['include'],
)
self._googletest = pw_package.git_repo.GitRepo(
name='googletest',
- url=''.join([
- 'https://chromium.googlesource.com/',
- 'external/github.com/google/googletest.git',
- ]),
+ url=''.join(
+ [
+ 'https://chromium.googlesource.com/',
+ 'external/github.com/google/googletest.git',
+ ]
+ ),
commit='53495a2a7d6ba7e0691a7f3602e9a5324bba6e45',
sparse_list=[
'googletest/include',
'googlemock/include',
- ])
+ ],
+ )
def install(self, path: pathlib.Path) -> None:
# Checkout chromium verifier
@@ -122,13 +132,13 @@ class ChromiumVerifier(pw_package.package_manager.Package):
self._chromium_verifier.install(chromium_repo)
# Checkout third party boringssl headers
- boringssl_repo = chromium_third_party_boringssl_repo_path(
- chromium_repo)
+ boringssl_repo = chromium_third_party_boringssl_repo_path(chromium_repo)
self._boringssl.install(boringssl_repo)
# Checkout third party googletest headers
googletest_repo = chromium_third_party_googletest_repo_path(
- chromium_repo)
+ chromium_repo
+ )
self._googletest.install(googletest_repo)
def status(self, path: pathlib.Path) -> bool:
@@ -136,13 +146,13 @@ class ChromiumVerifier(pw_package.package_manager.Package):
if not self._chromium_verifier.status(chromium_repo):
return False
- boringssl_repo = chromium_third_party_boringssl_repo_path(
- chromium_repo)
+ boringssl_repo = chromium_third_party_boringssl_repo_path(chromium_repo)
if not self._boringssl.status(boringssl_repo):
return False
googletest_repo = chromium_third_party_googletest_repo_path(
- chromium_repo)
+ chromium_repo
+ )
if not self._googletest.status(googletest_repo):
return False
diff --git a/pw_package/py/pw_package/packages/crlset.py b/pw_package/py/pw_package/packages/crlset.py
index f17002a2b..e75839020 100644
--- a/pw_package/py/pw_package/packages/crlset.py
+++ b/pw_package/py/pw_package/packages/crlset.py
@@ -35,6 +35,7 @@ def crlset_file_path(path: pathlib.Path) -> pathlib.Path:
class CRLSet(pw_package.package_manager.Package):
"""Install and check status of CRLSet and downloaded CLRSet file."""
+
def __init__(self, *args, **kwargs):
super().__init__(*args, name='crlset', **kwargs)
self._crlset_tools = pw_package.git_repo.GitRepo(
@@ -62,10 +63,10 @@ class CRLSet(pw_package.package_manager.Package):
# Build the go tool
subprocess.run(
- ['go', 'build', '-o',
- crlset_exec_path(path), 'crlset.go'],
+ ['go', 'build', '-o', crlset_exec_path(path), 'crlset.go'],
check=True,
- cwd=crlset_tools_repo_path(path))
+ cwd=crlset_tools_repo_path(path),
+ )
crlset_tools_exec = crlset_exec_path(path)
if not os.path.exists(crlset_tools_exec):
@@ -73,9 +74,11 @@ class CRLSet(pw_package.package_manager.Package):
# Download the latest CRLSet with the go tool
with open(crlset_file_path(path), 'wb') as crlset_file:
- fetched = subprocess.run([crlset_exec_path(path), 'fetch'],
- capture_output=True,
- check=True).stdout
+ fetched = subprocess.run(
+ [crlset_exec_path(path), 'fetch'],
+ capture_output=True,
+ check=True,
+ ).stdout
crlset_file.write(fetched)
def info(self, path: pathlib.Path) -> Sequence[str]:
diff --git a/pw_package/py/pw_package/packages/emboss.py b/pw_package/py/pw_package/packages/emboss.py
new file mode 100644
index 000000000..090858f6d
--- /dev/null
+++ b/pw_package/py/pw_package/packages/emboss.py
@@ -0,0 +1,48 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Install and check status of Emboss."""
+
+import pathlib
+from typing import Sequence
+
+import pw_package.git_repo
+import pw_package.package_manager
+
+
+class Emboss(pw_package.git_repo.GitRepo):
+ """Install and check status of Emboss."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ *args,
+ name="emboss",
+ url="".join(
+ [
+ "https://fuchsia.googlesource.com",
+ "/third_party/github.com/google/emboss",
+ ]
+ ),
+ commit="b8e2750975aa241fb81f5723ecc4c50f71e6bd18",
+ **kwargs,
+ )
+
+ def info(self, path: pathlib.Path) -> Sequence[str]:
+ return (
+ f"{self.name} installed in: {path}",
+ "Enable by running 'gn args out' and adding this line:",
+ f' dir_pw_third_party_emboss = "{path}"',
+ )
+
+
+pw_package.package_manager.register(Emboss)
diff --git a/pw_package/py/pw_package/packages/freertos.py b/pw_package/py/pw_package/packages/freertos.py
index 1ab127075..db7b76947 100644
--- a/pw_package/py/pw_package/packages/freertos.py
+++ b/pw_package/py/pw_package/packages/freertos.py
@@ -22,12 +22,15 @@ import pw_package.package_manager
class FreeRtos(pw_package.git_repo.GitRepo):
"""Install and check status of FreeRTOS."""
+
def __init__(self, *args, **kwargs):
- super().__init__(*args,
- name='freertos',
- url='https://github.com/FreeRTOS/FreeRTOS-Kernel',
- commit='fa0f5c436ccd920be313313b7e08ba6a5513f93a',
- **kwargs)
+ super().__init__(
+ *args,
+ name='freertos',
+ url='https://github.com/FreeRTOS/FreeRTOS-Kernel',
+ commit='a4b28e35103d699edf074dfff4835921b481b301',
+ **kwargs,
+ )
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
diff --git a/pw_package/py/pw_package/packages/googletest.py b/pw_package/py/pw_package/packages/googletest.py
index adfb4e8d8..86da67cf2 100644
--- a/pw_package/py/pw_package/packages/googletest.py
+++ b/pw_package/py/pw_package/packages/googletest.py
@@ -22,12 +22,15 @@ import pw_package.package_manager
class Googletest(pw_package.git_repo.GitRepo):
"""Install and check status of googletest."""
+
def __init__(self, *args, **kwargs):
- super().__init__(*args,
- name='googletest',
- url='https://github.com/google/googletest',
- commit='073293463e1733c5e931313da1c3f1de044e1db3',
- **kwargs)
+ super().__init__(
+ *args,
+ name='googletest',
+ url='https://github.com/google/googletest',
+ commit='073293463e1733c5e931313da1c3f1de044e1db3',
+ **kwargs,
+ )
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
diff --git a/pw_package/py/pw_package/packages/mbedtls.py b/pw_package/py/pw_package/packages/mbedtls.py
index a320efbe4..96c49b3cf 100644
--- a/pw_package/py/pw_package/packages/mbedtls.py
+++ b/pw_package/py/pw_package/packages/mbedtls.py
@@ -22,15 +22,20 @@ import pw_package.package_manager
class MbedTLS(pw_package.git_repo.GitRepo):
"""Install and check status of MbedTLS."""
+
def __init__(self, *args, **kwargs):
- super().__init__(*args,
- name='mbedtls',
- url="".join([
- "https://pigweed.googlesource.com",
- "/third_party/github/ARMmbed/mbedtls",
- ]),
- commit='e483a77c85e1f9c1dd2eb1c5a8f552d2617fe400',
- **kwargs)
+ super().__init__(
+ *args,
+ name='mbedtls',
+ url="".join(
+ [
+ "https://pigweed.googlesource.com",
+ "/third_party/github/ARMmbed/mbedtls",
+ ]
+ ),
+ commit='e483a77c85e1f9c1dd2eb1c5a8f552d2617fe400',
+ **kwargs,
+ )
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
diff --git a/pw_package/py/pw_package/packages/micro_ecc.py b/pw_package/py/pw_package/packages/micro_ecc.py
index 9a5200330..a72ef433e 100644
--- a/pw_package/py/pw_package/packages/micro_ecc.py
+++ b/pw_package/py/pw_package/packages/micro_ecc.py
@@ -22,15 +22,20 @@ import pw_package.package_manager
class MicroECC(pw_package.git_repo.GitRepo):
"""Install and check the status of the Micro-ECC cryptography library."""
+
def __init__(self, *args, **kwargs):
- super().__init__(*args,
- name='micro-ecc',
- url="".join([
- "https://github.com",
- "/kmackay/micro-ecc.git",
- ]),
- commit='24c60e243580c7868f4334a1ba3123481fe1aa48',
- **kwargs)
+ super().__init__(
+ *args,
+ name='micro-ecc',
+ url="".join(
+ [
+ "https://github.com",
+ "/kmackay/micro-ecc.git",
+ ]
+ ),
+ commit='24c60e243580c7868f4334a1ba3123481fe1aa48',
+ **kwargs,
+ )
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
diff --git a/pw_package/py/pw_package/packages/nanopb.py b/pw_package/py/pw_package/packages/nanopb.py
index b5875d46c..a512fc8b0 100644
--- a/pw_package/py/pw_package/packages/nanopb.py
+++ b/pw_package/py/pw_package/packages/nanopb.py
@@ -22,14 +22,17 @@ import pw_package.package_manager
class NanoPB(pw_package.git_repo.GitRepo):
"""Install and check status of nanopb."""
+
def __init__(self, *args, **kwargs):
+ # pylint: disable=line-too-long
super().__init__(
*args,
name='nanopb',
- url=
- 'https://pigweed.googlesource.com/third_party/github/nanopb/nanopb',
+ url='https://pigweed.googlesource.com/third_party/github/nanopb/nanopb',
commit='2b48a361786dfb1f63d229840217a93aae064667',
- **kwargs)
+ **kwargs,
+ )
+ # pylint: enable=line-too-long
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
diff --git a/pw_package/py/pw_package/packages/pico_sdk.py b/pw_package/py/pw_package/packages/pico_sdk.py
index 2a68fc6e1..1014e0750 100644
--- a/pw_package/py/pw_package/packages/pico_sdk.py
+++ b/pw_package/py/pw_package/packages/pico_sdk.py
@@ -13,23 +13,47 @@
# the License.
"""Install and check status of the Raspberry Pi Pico SDK."""
-import pathlib
+from contextlib import contextmanager
+import os
+from pathlib import Path
from typing import Sequence
+import subprocess
import pw_package.git_repo
import pw_package.package_manager
-class PiPicoSdk(pw_package.git_repo.GitRepo):
+@contextmanager
+def change_working_dir(directory: Path):
+ original_dir = Path.cwd()
+ try:
+ os.chdir(directory)
+ yield directory
+ finally:
+ os.chdir(original_dir)
+
+
+class PiPicoSdk(pw_package.package_manager.Package):
"""Install and check status of the Raspberry Pi Pico SDK."""
+
def __init__(self, *args, **kwargs):
- super().__init__(*args,
- name='pico_sdk',
- url='https://github.com/raspberrypi/pico-sdk',
- commit='2062372d203b372849d573f252cf7c6dc2800c0a',
- **kwargs)
+ super().__init__(*args, name='pico_sdk', **kwargs)
+ self._pico_sdk = pw_package.git_repo.GitRepo(
+ name='pico_sdk',
+ url='https://github.com/raspberrypi/pico-sdk',
+ commit='2e6142b15b8a75c1227dd3edbe839193b2bf9041',
+ )
+
+ def install(self, path: Path) -> None:
+ self._pico_sdk.install(path)
+
+ # Run submodule update --init to fetch tinyusb.
+ with change_working_dir(path) as _pico_sdk_repo:
+ subprocess.run(
+ ['git', 'submodule', 'update', '--init'], capture_output=True
+ )
- def info(self, path: pathlib.Path) -> Sequence[str]:
+ def info(self, path: Path) -> Sequence[str]:
return (
f'{self.name} installed in: {path}',
"Enable by running 'gn args out' and adding this line:",
diff --git a/pw_package/py/pw_package/packages/protobuf.py b/pw_package/py/pw_package/packages/protobuf.py
index a06c493c9..25ba48677 100644
--- a/pw_package/py/pw_package/packages/protobuf.py
+++ b/pw_package/py/pw_package/packages/protobuf.py
@@ -22,12 +22,15 @@ import pw_package.package_manager
class Protobuf(pw_package.git_repo.GitRepo):
"""Install and check status of Protobuf."""
+
def __init__(self, *args, **kwargs):
- super().__init__(*args,
- name='protobuf',
- url="https://github.com/protocolbuffers/protobuf.git",
- commit='15c40c6cdac2f816a56640d24a5c4c3ec0f84b00',
- **kwargs)
+ super().__init__(
+ *args,
+ name='protobuf',
+ url="https://github.com/protocolbuffers/protobuf.git",
+ commit='15c40c6cdac2f816a56640d24a5c4c3ec0f84b00',
+ **kwargs,
+ )
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
diff --git a/pw_package/py/pw_package/packages/smartfusion_mss.py b/pw_package/py/pw_package/packages/smartfusion_mss.py
index b9dbe49c7..f42cfef9d 100644
--- a/pw_package/py/pw_package/packages/smartfusion_mss.py
+++ b/pw_package/py/pw_package/packages/smartfusion_mss.py
@@ -22,12 +22,15 @@ import pw_package.package_manager
class SmartfusionMss(pw_package.git_repo.GitRepo):
"""Install and check status of SmartFusion MSS."""
+
def __init__(self, *args, **kwargs):
- super().__init__(*args,
- name='smartfusion_mss',
- url='https://github.com/seank/smartfusion_mss',
- commit='9f47db73d3df786eab04d082645da5e735e63d28',
- **kwargs)
+ super().__init__(
+ *args,
+ name='smartfusion_mss',
+ url='https://github.com/seank/smartfusion_mss',
+ commit='bb22f26cc3a54df15bb901dc6c95662727158fed',
+ **kwargs,
+ )
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
diff --git a/pw_package/py/pw_package/packages/stm32cube.py b/pw_package/py/pw_package/packages/stm32cube.py
index e07b6395c..a2e704e04 100644
--- a/pw_package/py/pw_package/packages/stm32cube.py
+++ b/pw_package/py/pw_package/packages/stm32cube.py
@@ -20,66 +20,70 @@ import pw_stm32cube_build.gen_file_list
import pw_package.git_repo
import pw_package.package_manager
-# Compatible versions are listed on the README.md of each hal_driver repo
+# Compatible versions are listed in either of:
+# - For older releases, the README.md of each hal_driver, e.g.:
+# https://github.com/STMicroelectronics/stm32f4xx_hal_driver/blob/v1.8.0/README.md
+# - For newer releases, the Release_Notes.html file in STM32Cube release, e.g.:
+# https://github.com/STMicroelectronics/STM32CubeF4/blob/v1.27.1/Release_Notes.html
_STM32CUBE_VERSIONS = {
"f0": {
- "hal_driver_tag": "v1.7.5",
- "cmsis_device_tag": "v2.3.5",
+ "hal_driver_tag": "v1.7.6",
+ "cmsis_device_tag": "v2.3.6",
"cmsis_core_tag": "v5.4.0_cm0",
},
"f1": {
- "hal_driver_tag": "v1.1.7",
- "cmsis_device_tag": "v4.3.2",
+ "hal_driver_tag": "v1.1.8",
+ "cmsis_device_tag": "v4.3.3",
"cmsis_core_tag": "v5.4.0_cm3",
},
"f2": {
- "hal_driver_tag": "v1.2.6",
- "cmsis_device_tag": "v2.2.4",
+ "hal_driver_tag": "v1.2.7",
+ "cmsis_device_tag": "v2.2.5",
"cmsis_core_tag": "v5.4.0_cm3",
},
"f3": {
- "hal_driver_tag": "v1.5.5",
- "cmsis_device_tag": "v2.3.5",
+ "hal_driver_tag": "v1.5.6",
+ "cmsis_device_tag": "v2.3.6",
"cmsis_core_tag": "v5.4.0_cm4",
},
"f4": {
- "hal_driver_tag": "v1.7.12",
- "cmsis_device_tag": "v2.6.6",
+ "hal_driver_tag": "v1.8.0",
+ "cmsis_device_tag": "v2.6.8",
"cmsis_core_tag": "v5.4.0_cm4",
},
"f7": {
- "hal_driver_tag": "v1.2.9",
- "cmsis_device_tag": "v1.2.6",
+ "hal_driver_tag": "v1.3.0",
+ "cmsis_device_tag": "v1.2.8",
"cmsis_core_tag": "v5.4.0_cm7",
},
"g0": {
- "hal_driver_tag": "v1.4.1",
- "cmsis_device_tag": "v1.4.0",
+ "hal_driver_tag": "v1.4.5",
+ "cmsis_device_tag": "v1.4.3",
"cmsis_core_tag": "v5.6.0_cm0",
},
"g4": {
- "hal_driver_tag": "v1.2.1",
- "cmsis_device_tag": "v1.2.1",
+ "hal_driver_tag": "v1.2.2",
+ "cmsis_device_tag": "v1.2.2",
"cmsis_core_tag": "v5.6.0_cm4",
},
"h7": {
- "hal_driver_tag": "v1.10.0",
- "cmsis_device_tag": "v1.10.0",
+ "hal_driver_tag": "v1.11.0",
+ "cmsis_device_tag": "v1.10.2",
"cmsis_core_tag": "v5.6.0",
},
"l0": {
- "hal_driver_tag": "v1.10.4",
- "cmsis_device_tag": "v1.9.1",
- "cmsis_core_tag": "v4.5_cm0",
+ "hal_driver_tag": "v1.10.5",
+ "cmsis_device_tag": "v1.9.2",
+ "cmsis_core_tag": "v5.4.0_cm0",
},
"l1": {
- "hal_driver_tag": "v1.4.3",
- "cmsis_device_tag": "v2.3.1",
+ "hal_driver_tag": "v1.4.4",
+ "cmsis_device_tag": "v2.3.2",
"cmsis_core_tag": "v5.4.0_cm3",
},
"l4": {
- "hal_driver_tag": "v1.13.0",
- "cmsis_device_tag": "v1.7.1",
+ "hal_driver_tag": "v1.13.3",
+ "cmsis_device_tag": "v1.7.2",
"cmsis_core_tag": "v5.6.0_cm4",
},
"l5": {
@@ -88,13 +92,13 @@ _STM32CUBE_VERSIONS = {
"cmsis_core_tag": "v5.6.0_cm33",
},
"wb": {
- "hal_driver_tag": "v1.8.0",
- "cmsis_device_tag": "v1.8.0",
- "cmsis_core_tag": "v5.4.0_cm4",
+ "hal_driver_tag": "v1.11.0",
+ "cmsis_device_tag": "v1.11.0",
+ "cmsis_core_tag": "v5.6.0_cm4",
},
"wl": {
- "hal_driver_tag": "v1.0.0",
- "cmsis_device_tag": "v1.0.0",
+ "hal_driver_tag": "v1.1.0",
+ "cmsis_device_tag": "v1.1.0",
"cmsis_core_tag": "v5.6.0_cm4",
},
}
@@ -102,6 +106,7 @@ _STM32CUBE_VERSIONS = {
class Stm32Cube(pw_package.package_manager.Package):
"""Install and check status of stm32cube."""
+
def __init__(self, family, tags, *args, **kwargs):
super().__init__(*args, name=f'stm32cube_{family}', **kwargs)
@@ -133,12 +138,14 @@ class Stm32Cube(pw_package.package_manager.Package):
pw_stm32cube_build.gen_file_list.gen_file_list(path)
def status(self, path: pathlib.Path) -> bool:
- return all([
- self._hal_driver.status(path / self._hal_driver.name),
- self._cmsis_core.status(path / self._cmsis_core.name),
- self._cmsis_device.status(path / self._cmsis_device.name),
- (path / "files.txt").is_file(),
- ])
+ return all(
+ [
+ self._hal_driver.status(path / self._hal_driver.name),
+ self._cmsis_core.status(path / self._cmsis_core.name),
+ self._cmsis_device.status(path / self._cmsis_device.name),
+ (path / "files.txt").is_file(),
+ ]
+ )
def info(self, path: pathlib.Path) -> Sequence[str]:
return (
diff --git a/pw_package/py/pw_package/pigweed_packages.py b/pw_package/py/pw_package/pigweed_packages.py
index c60468c9b..dec3bcbc9 100644
--- a/pw_package/py/pw_package/pigweed_packages.py
+++ b/pw_package/py/pw_package/pigweed_packages.py
@@ -16,19 +16,24 @@
import sys
from pw_package import package_manager
-from pw_package.packages import arduino_core # pylint: disable=unused-import
-from pw_package.packages import boringssl # pylint: disable=unused-import
-from pw_package.packages import chromium_verifier # pylint: disable=unused-import
-from pw_package.packages import crlset # pylint: disable=unused-import
-from pw_package.packages import freertos # pylint: disable=unused-import
-from pw_package.packages import googletest # pylint: disable=unused-import
-from pw_package.packages import mbedtls # pylint: disable=unused-import
-from pw_package.packages import micro_ecc # pylint: disable=unused-import
+
+# pylint: disable=unused-import
+from pw_package.packages import arduino_core
+from pw_package.packages import boringssl
+from pw_package.packages import chromium_verifier
+from pw_package.packages import crlset
+from pw_package.packages import emboss
+from pw_package.packages import freertos
+from pw_package.packages import googletest
+from pw_package.packages import mbedtls
+from pw_package.packages import micro_ecc
from pw_package.packages import nanopb
-from pw_package.packages import pico_sdk # pylint: disable=unused-import
-from pw_package.packages import protobuf # pylint: disable=unused-import
-from pw_package.packages import smartfusion_mss # pylint: disable=unused-import
-from pw_package.packages import stm32cube # pylint: disable=unused-import
+from pw_package.packages import pico_sdk
+from pw_package.packages import protobuf
+from pw_package.packages import smartfusion_mss
+from pw_package.packages import stm32cube
+
+# pylint: enable=unused-import
def initialize():
diff --git a/pw_perf_test/BUILD.bazel b/pw_perf_test/BUILD.bazel
new file mode 100644
index 000000000..84d3cea5d
--- /dev/null
+++ b/pw_perf_test/BUILD.bazel
@@ -0,0 +1,212 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_facade",
+ "pw_cc_library",
+ "pw_cc_perf_test",
+ "pw_cc_test",
+)
+load(
+ "//pw_build:selects.bzl",
+ "TARGET_COMPATIBLE_WITH_HOST_SELECT",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "duration_unit",
+ hdrs = [
+ "public/pw_perf_test/internal/duration_unit.h",
+ ],
+ includes = ["public"],
+ visibility = ["//visibility:private"],
+)
+
+pw_cc_facade(
+ name = "timer_interface_facade",
+ hdrs = [
+ "public/pw_perf_test/internal/timer.h",
+ ],
+ includes = ["public"],
+ visibility = ["//visibility:private"],
+ deps = [
+ ":duration_unit",
+ ],
+)
+
+pw_cc_library(
+ name = "timer",
+ deps = [
+ ":timer_interface_facade",
+ "@pigweed_config//:pw_perf_test_timer_backend",
+ ],
+)
+
+pw_cc_library(
+ name = "timer_multiplexer",
+ visibility = ["@pigweed_config//:__pkg__"],
+ deps = select({
+ "//conditions:default": [":chrono_timer"],
+ }),
+)
+
+# EventHandler Configuraitions
+
+pw_cc_library(
+ name = "event_handler",
+ hdrs = ["public/pw_perf_test/event_handler.h"],
+ includes = ["public"],
+ deps = [":timer"],
+)
+
+pw_cc_library(
+ name = "pw_perf_test",
+ srcs = ["perf_test.cc"],
+ hdrs = ["public/pw_perf_test/perf_test.h"],
+ includes = ["public"],
+ deps = [
+ ":event_handler",
+ ":timer",
+ "//pw_assert",
+ "//pw_log",
+ ],
+)
+
+pw_cc_library(
+ name = "google_test_style_event_strings",
+ hdrs = ["public/pw_perf_test/googletest_style_event_handler.h"],
+)
+
+pw_cc_library(
+ name = "log_main_handler",
+ srcs = ["log_perf_handler.cc"],
+ hdrs = ["public/pw_perf_test/log_perf_handler.h"],
+ includes = [
+ "public",
+ ],
+ deps = [
+ ":event_handler",
+ ":google_test_style_event_strings",
+ "//pw_log",
+ ],
+)
+
+pw_cc_library(
+ name = "logging_main",
+ srcs = ["log_perf_handler_main.cc"],
+ deps = [
+ ":log_main_handler",
+ ":pw_perf_test",
+ ],
+)
+
+pw_cc_perf_test(
+ name = "generic_test",
+ srcs = ["performance_test_generic.cc"],
+)
+
+# Test Declarations
+
+pw_cc_test(
+ name = "perf_test_test",
+ srcs = ["perf_test_test.cc"],
+ deps = [":pw_perf_test"],
+)
+
+pw_cc_test(
+ name = "timer_test",
+ srcs = ["timer_test.cc"],
+ deps = [
+ ":timer",
+ "//pw_chrono:system_clock",
+ "//pw_thread:sleep",
+ ],
+)
+
+pw_cc_test(
+ name = "chrono_timer_test",
+ srcs = ["chrono_test.cc"],
+ deps = [
+ ":chrono_timer",
+ "//pw_chrono:system_clock",
+ "//pw_thread:sleep",
+ ],
+)
+
+pw_cc_test(
+ name = "state_test",
+ srcs = ["state_test.cc"],
+ deps = [":pw_perf_test"],
+)
+
+# Bazel does not yet support building docs.
+filegroup(
+ name = "docs",
+ srcs = ["docs.rst"],
+)
+
+# Chrono Implementation
+
+pw_cc_library(
+ name = "chrono_timer_headers",
+ hdrs = [
+ "chrono_public_overrides/pw_perf_test_timer_backend/timer.h",
+ "public/pw_perf_test/internal/chrono_timer_interface.h",
+ ],
+ includes = [
+ "chrono_public_overrides",
+ "public",
+ ],
+ deps = [
+ ":duration_unit",
+ "//pw_chrono:system_clock",
+ ],
+)
+
+pw_cc_library(
+ name = "chrono_timer",
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":chrono_timer_headers",
+ ":timer_interface_facade",
+ ],
+)
+
+# ARM Cortex Implementation
+
+pw_cc_library(
+ name = "arm_cortex_timer_headers",
+ hdrs = [
+ "arm_cortex_cyccnt_public_overrides/pw_perf_test_timer_backend/timer.h",
+ "public/pw_perf_test/internal/cyccnt_timer_interface.h",
+ ],
+ includes = [
+ "arm_cortex_cyccnt_public_overrides",
+ "public",
+ ],
+ deps = [":duration_unit"],
+)
+
+pw_cc_library(
+ name = "arm_cortex_timer",
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":arm_cortex_timer_headers",
+ ":timer_interface_facade",
+ ],
+)
diff --git a/pw_perf_test/BUILD.gn b/pw_perf_test/BUILD.gn
new file mode 100644
index 000000000..78839dd2b
--- /dev/null
+++ b/pw_perf_test/BUILD.gn
@@ -0,0 +1,187 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/facade.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_perf_test/perf_test.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+config("arm_config") {
+ include_dirs = [ "arm_cortex_cyccnt_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
+config("chrono_config") {
+ include_dirs = [ "chrono_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":perf_test_test",
+ ":timer_facade_test",
+ ":chrono_timer_test",
+ ":state_test",
+ ]
+}
+
+group("perf_test_tests_test") {
+ deps = [ ":generic_perf_test" ]
+}
+
+# Timing interface variables
+
+pw_source_set("duration_unit") {
+ public = [ "public/pw_perf_test/internal/duration_unit.h" ]
+ public_configs = [ ":public_include_path" ]
+ visibility = [ ":*" ]
+}
+
+pw_facade("timer_interface") {
+ backend = pw_perf_test_TIMER_INTERFACE_BACKEND
+ public = [ "public/pw_perf_test/internal/timer.h" ]
+ public_deps = [ ":duration_unit" ]
+ visibility = [ ":*" ]
+}
+
+# Event Handler Configurations
+
+pw_source_set("event_handler") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_perf_test/event_handler.h" ]
+}
+
+pw_source_set("pw_perf_test") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_perf_test/perf_test.h" ]
+ public_deps = [
+ ":event_handler",
+ ":timer_interface",
+ dir_pw_assert,
+ ]
+ deps = [ dir_pw_log ]
+ sources = [ "perf_test.cc" ]
+}
+
+pw_source_set("googletest_style_event_handler") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_perf_test/googletest_style_event_handler.h" ]
+}
+
+pw_source_set("log_perf_handler") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_perf_test/log_perf_handler.h" ]
+ public_deps = [
+ ":event_handler",
+ ":googletest_style_event_handler",
+ ":pw_perf_test",
+ ]
+ deps = [ dir_pw_log ]
+ sources = [ "log_perf_handler.cc" ]
+}
+
+pw_source_set("log_perf_handler_main") {
+ public_deps = [ ":log_perf_handler" ]
+ sources = [ "log_perf_handler_main.cc" ]
+}
+
+pw_perf_test("generic_perf_test") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ sources = [ "performance_test_generic.cc" ]
+}
+
+# Declaring module tests
+
+pw_test("perf_test_test") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ sources = [ "perf_test_test.cc" ]
+ deps = [ ":pw_perf_test" ]
+}
+
+pw_test("timer_facade_test") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ sources = [ "timer_test.cc" ]
+ public_deps = [ ":timer_interface" ]
+}
+
+pw_test("chrono_timer_test") {
+ enable_if = pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+ sources = [ "chrono_test.cc" ]
+ public_deps = [
+ ":chrono_timer",
+ "$dir_pw_chrono:system_timer",
+ "$dir_pw_thread:sleep",
+ ]
+}
+
+pw_test("state_test") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ sources = [ "state_test.cc" ]
+ public_deps = [ ":pw_perf_test" ]
+}
+
+# Documentation declaration
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+# Chrono Implementation
+pw_source_set("pw_perf_test_chrono") {
+ public_configs = [ ":chrono_config" ]
+ public = [ "chrono_public_overrides/pw_perf_test_timer_backend/timer.h" ]
+ public_deps = [ ":chrono_timer" ]
+}
+
+pw_source_set("chrono_timer") {
+ public_configs = [
+ ":public_include_path",
+ ":chrono_config",
+ ]
+ public = [ "public/pw_perf_test/internal/chrono_timer_interface.h" ]
+ public_deps = [
+ ":duration_unit",
+ "$dir_pw_chrono:system_clock",
+ ]
+ visibility = [ ":*" ]
+}
+
+# ARM Cortex Implementation
+
+pw_source_set("arm_cortex_timer") {
+ public_configs = [
+ ":public_include_path",
+ ":arm_config",
+ ]
+ public = [ "public/pw_perf_test/internal/cyccnt_timer_interface.h" ]
+ public_deps = [ ":duration_unit" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("pw_perf_test_arm_cortex") {
+ public_configs = [ ":arm_config" ]
+ public = [
+ "arm_cortex_cyccnt_public_overrides/pw_perf_test_timer_backend/timer.h",
+ ]
+ public_deps = [ ":arm_cortex_timer" ]
+}
diff --git a/pw_perf_test/CMakeLists.txt b/pw_perf_test/CMakeLists.txt
new file mode 100644
index 000000000..a6d7749f4
--- /dev/null
+++ b/pw_perf_test/CMakeLists.txt
@@ -0,0 +1,151 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_perf_test/backend.cmake)
+include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
+
+pw_add_library(pw_perf_test.duration_unit INTERFACE
+ HEADERS
+ public/pw_perf_test/internal/duration_unit.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_facade(pw_perf_test.timer INTERFACE
+ BACKEND
+ pw_perf_test.TIMER_INTERFACE_BACKEND
+ HEADERS
+ public/pw_perf_test/internal/timer.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_perf_test.duration_unit
+)
+
+pw_add_library(pw_perf_test.event_handler INTERFACE
+ HEADERS
+ public/pw_perf_test/event_handler.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_library(pw_perf_test STATIC
+ PUBLIC_INCLUDES
+ public
+ HEADERS
+ public/pw_perf_test/perf_test.h
+ PUBLIC_DEPS
+ pw_perf_test.timer
+ pw_perf_test.event_handler
+ PRIVATE_DEPS
+ pw_log
+ pw_assert
+ SOURCES
+ perf_test.cc
+)
+
+pw_add_library(pw_perf_test.googletest_style_event_handler INTERFACE
+ PUBLIC_INCLUDES
+ public
+ HEADERS
+ public/pw_perf_test/googletest_style_event_handler.h
+)
+
+pw_add_library(pw_perf_test.log_perf_handler STATIC
+ PUBLIC_INCLUDES
+ public
+ PRIVATE_DEPS
+ pw_log
+ PUBLIC_DEPS
+ pw_perf_test.event_handler
+ pw_perf_test.googletest_style_event_handler
+ pw_perf_test
+ HEADERS
+ public/pw_perf_test/log_perf_handler.h
+ SOURCES
+ log_perf_handler.cc
+)
+
+pw_add_library(pw_perf_test.log_perf_handler_main STATIC
+ PUBLIC_DEPS
+ pw_perf_test.log_perf_handler
+ SOURCES
+ log_perf_handler_main.cc
+)
+
+pw_add_library(pw_perf_test.chrono_timer INTERFACE
+ HEADERS
+ chrono_public_overrides/pw_perf_test_timer_backend/timer.h
+ public/pw_perf_test/internal/chrono_timer_interface.h
+ PUBLIC_INCLUDES
+ chrono_public_overrides
+ public
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_perf_test.duration_unit
+)
+
+if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}" STREQUAL "")
+ pw_add_test(pw_perf_test.timer_test
+ SOURCES
+ timer_test.cc
+ PRIVATE_DEPS
+ pw_perf_test.timer
+ pw_thread.sleep
+ pw_chrono.system_clock
+ GROUPS
+ modules
+ pw_perf_test
+ )
+endif()
+
+if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}"
+ STREQUAL "pw_chrono.SYSTEM_CLOCK_BACKEND.NO_BACKEND_SET")
+ pw_add_test(pw_perf_test.chrono_timer_test
+ SOURCES
+ chrono_test.cc
+ PRIVATE_DEPS
+ pw_perf_test.timer
+ pw_thread.sleep
+ pw_chrono.system_clock
+ GROUPS
+ modules
+ pw_perf_test
+ )
+endif()
+
+if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}" STREQUAL "")
+ pw_add_test(pw_perf_test.state_test
+ SOURCES
+ state_test.cc
+ PRIVATE_DEPS
+ pw_perf_test
+ GROUPS
+ modules
+ pw_perf_test
+ )
+endif()
+
+if(NOT "${pw_perf_test.TIMER_INTERFACE_BACKEND}" STREQUAL "")
+ pw_add_test(pw_perf_test.perf_test_test
+ SOURCES
+ perf_test_test.cc
+ PRIVATE_DEPS
+ pw_perf_test
+ GROUPS
+ modules
+ pw_perf_test
+ )
+endif()
diff --git a/pw_perf_test/arm_cortex_cyccnt_public_overrides/pw_perf_test_timer_backend/timer.h b/pw_perf_test/arm_cortex_cyccnt_public_overrides/pw_perf_test_timer_backend/timer.h
new file mode 100644
index 000000000..5ed897e88
--- /dev/null
+++ b/pw_perf_test/arm_cortex_cyccnt_public_overrides/pw_perf_test_timer_backend/timer.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_perf_test/internal/cyccnt_timer_interface.h"
diff --git a/pw_perf_test/backend.cmake b/pw_perf_test/backend.cmake
new file mode 100644
index 000000000..fa7a9b3ad
--- /dev/null
+++ b/pw_perf_test/backend.cmake
@@ -0,0 +1,18 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_backend_variable(pw_perf_test.TIMER_INTERFACE_BACKEND)
diff --git a/pw_perf_test/chrono_public_overrides/pw_perf_test_timer_backend/timer.h b/pw_perf_test/chrono_public_overrides/pw_perf_test_timer_backend/timer.h
new file mode 100644
index 000000000..1e44ac169
--- /dev/null
+++ b/pw_perf_test/chrono_public_overrides/pw_perf_test_timer_backend/timer.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_perf_test/internal/chrono_timer_interface.h"
diff --git a/pw_perf_test/chrono_test.cc b/pw_perf_test/chrono_test.cc
new file mode 100644
index 000000000..a708ede17
--- /dev/null
+++ b/pw_perf_test/chrono_test.cc
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_perf_test/internal/chrono_timer_interface.h"
+#include "pw_thread/sleep.h"
+
+namespace pw::perf_test::internal::backend {
+namespace {
+
+constexpr chrono::SystemClock::duration kArbitraryDuration =
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1));
+
+TEST(ChronoTest, DurationIsReasonable) {
+ Timestamp start = GetCurrentTimestamp();
+ this_thread::sleep_for(kArbitraryDuration);
+ Timestamp end = GetCurrentTimestamp();
+ int64_t duration = GetDuration(start, end);
+ EXPECT_GE(duration, 1000000);
+}
+
+} // namespace
+} // namespace pw::perf_test::internal::backend
diff --git a/pw_perf_test/docs.rst b/pw_perf_test/docs.rst
new file mode 100644
index 000000000..63d348b6e
--- /dev/null
+++ b/pw_perf_test/docs.rst
@@ -0,0 +1,325 @@
+.. _module-pw_perf_test:
+
+============
+pw_perf_test
+============
+Pigweed's perf test module provides an easy way to measure performance on
+any test setup. By using an API similar to GoogleTest, this module aims to bring
+a comprehensive and intuitive testing framework to our users, much like
+:ref:`module-pw_unit_test`.
+
+.. warning::
+ The PW_PERF_TEST macro is still under construction and should not be relied
+ upon yet
+
+-------------------
+Perf Test Interface
+-------------------
+The user experience of writing a performance test is intended to be as
+friction-less as possible. With the goal of being used for micro-benchmarking
+code, writing a performance test is as easy as:
+
+.. code-block:: cpp
+
+ void TestFunction(::pw::perf_test::State& state) {
+ // space to create any needed variables.
+ while (state.KeepRunning()){
+ // code to measure here
+ }
+ }
+ PW_PERF_TEST(PerformanceTestName, TestFunction);
+
+However, it is recommended to read this guide to understand and write tests that
+are suited towards your platform and the type of code you are trying to
+benchmark.
+
+State
+=====
+Within the testing framework, the state object is responsible for calling the
+timing interface and keeping track of testing iterations. It contains only one
+publicly accessible function, since the object is intended for internal use
+only. The ``KeepRunning()`` function collects timestamps to measure the code
+and ensures that only a certain number of iterations are run. To use the state
+object properly, pass it as an argument of the test function and pass in the
+``KeepRunning()`` function as the condition in a ``while()`` loop. The
+``KeepRunning()`` function collects timestamps to measure the code and ensures
+that only a certain number of iterations are run. Therefore the code to be
+measured should be in the body of the ``while()`` loop like so:
+
+.. code-block:: cpp
+
+ // The State object is injected into a performance test by including it as an
+ // argument to the function.
+ void TestFunction(::pw::perf_test::State& state_obj) {
+ while (state_obj.KeepRunning()) {
+ /*
+ Code to be measured here
+ */
+ }
+ }
+
+Macro Interface
+===============
+The test collection and registration process is done by a macro, much like
+:ref:`module-pw_unit_test`.
+
+.. c:macro:: PW_PERF_TEST(test_name, test_function, ...)
+
+ Registers a performance test. Any additional arguments are passed to the test
+ function.
+
+.. c:macro:: PW_PERF_TEST_SIMPLE(test_name, test_function, ...)
+
+ Like the original PW_PERF_TEST macro it registers a performance test. However
+ the test function does not need to have a state object. Internally this macro
+ runs all of the input function inside of its own state loop. Any additional
+ arguments are passed into the function to be tested.
+
+.. code-block:: cpp
+
+ // Declare performance test functions.
+ // The first argument is the state, which is passed in by the test framework.
+ void TestFunction(pw::perf_test::State& state) {
+ // Test set up code
+ Items a[] = {1, 2, 3};
+
+ // Tests a KeepRunning() function, similar to Fuchsia's Perftest.
+ while (state.KeepRunning()) {
+ // Code under test, ran for multiple iterations.
+ DoStuffToItems(a);
+ }
+ }
+
+ void TestFunctionWithArgs(pw::perf_test::State& state, int arg1, bool arg2) {
+ // Test set up code
+ Thing object_created_outside(arg1);
+
+ while (state.KeepRunning()) {
+ // Code under test, ran for multiple iterations.
+ object_created_outside.Do(arg2);
+ }
+ }
+
+ // Tests are declared with any callable object. This is similar to Benchmark's
+ // BENCMARK_CAPTURE() macro.
+ PW_PERF_TEST(Name1, [](pw::perf_test::State& state) {
+ TestFunctionWithArgs(1, false);
+ })
+
+ PW_PERF_TEST(Name2, TestFunctionWithArgs, 1, true);
+ PW_PERF_TEST(Name3, TestFunctionWithArgs, 2, false);
+
+ void Sum(int a, int b) {
+ return a + b;
+ }
+
+ PW_PERF_TEST_SIMPLE(SimpleExample, Sum, 4, 2);
+ PW_PERF_TEST_SIMPLE(Name4, MyExistingFunction, "input");
+
+.. warning::
+ Internally, the testing framework stores the testing function as a function
+ pointer. Therefore the test function argument must be converible to a function
+ pointer.
+
+Event Handler
+=============
+The performance testing framework relies heavily on the member functions of
+EventHandler to report iterations, the beginning of tests and other useful
+information. The ``EventHandler`` class is a virtual interface meant to be
+overridden, in order to provide flexibility on how data gets transferred.
+
+.. cpp:class:: pw::perf_test::EventHandler
+
+ Handles events from a performance test.
+
+ .. cpp:function:: virtual void RunAllTestsStart(const TestRunInfo& summary)
+
+ Called before all tests are run
+
+ .. cpp:function:: virtual void RunAllTestsEnd()
+
+ Called after all tests are run
+
+ .. cpp:function:: virtual void TestCaseStart(const TestCase& info)
+
+ Called when a new performance test is started
+
+ .. cpp:function:: virtual void TestCaseIteration(const IterationResult& result)
+
+ Called to output the results of an iteration
+
+ .. cpp:function:: virtual void TestCaseEnd(const TestCase& info, const Results& end_result)
+
+ Called after a performance test ends
+
+Logging Event Handler
+---------------------
+The default method of running performance tests is using the
+``LoggingEventHandler``. This event handler only logs the test results to the
+console and nothing more. It was chosen as the default method due to its
+portability and to cut down on the time it would take to implement other
+printing log handlers. Make sure to set a ``pw_log`` backend.
+
+Timing API
+==========
+In order to provide meaningful performance timings for given functions, events,
+etc a timing interface must be implemented from scratch to be able to provide
+for the testing needs. The timing API meets these needs by implementing either
+clock cycle record keeping or second based recordings.
+
+Time-Based Measurement
+----------------------
+For most host applications, pw_perf_test depends on :ref:`module-pw_chrono` for
+its timing needs. At the moment, the interface will only measure performance in
+terms of nanoseconds. To see more information about how pw_chrono works, see the
+module documentation.
+
+Cycle Count Measurement
+------------------------------------
+In the case of running tests on an embedded system, clock cycles may give more
+insight into the actual performance of the system. The timing API gives you this
+option by providing time measurements through a facade. In this case, by setting
+the ccynt timer as the backend, perf tests can be measured in clock cycles for
+ARM Cortex devices.
+
+This implementation directly accesses the registers of the Cortex, and therefore
+needs no operating system to function. This is achieved by enabling the
+`DWT register <https://developer.arm.com/documentation/ddi0337/e/System-Debug/DWT?lang=en>`_
+through the `DEMCR register <https://developer.arm.com/documentation/ddi0337/e/CEGHJDCF>`_.
+While this provides cycle counts directly from the CPU, notably it is vulnerable
+to rollover upon a duration of a test exceeding 2^32 clock cycles. This works
+out to a 43 second duration limit per iteration at 100 mhz.
+
+.. warning::
+ The interface only measures raw clock cycles and does not take into account
+ other possible sources of pollution such as LSUs, Sleeps and other registers.
+ `Read more on the DWT methods of counting instructions. <https://developer.arm.com/documentation/ka001499/1-0/>`_
+
+------------------------
+Build System Integration
+------------------------
+As of this moment, pw_perf_test provides build integration with Bazel and GN.
+Performance tests can be built in CMake, but must be built as regular
+executables.
+
+While each build system has their own names for their variables, each test must
+configure an ``EventHandler`` by choosing an associated ``main()`` function, and
+they must configure a ``timing interface``. At the moment, only a
+:ref:`module-pw_log` based event handler exists, timing is only supported
+where :ref:`module-pw_chrono` is supported, and cycle counts are only supported
+on ARM Cortex M series microcontrollers with a Data Watchpoint and Trace (DWT)
+unit.
+
+GN
+===
+To get tests building in GN, set the ``pw_perf_test_TIMER_INTERFACE_BACKEND``
+variable to whichever implementation is necessary for timings. Next, set the
+``pw_perf_test_MAIN_FUNCTION`` variable to the preferred event handler. Finally
+use the ``pw_perf_test`` template to register your code.
+
+.. code-block::
+
+ import("$dir_pw_perf_test/perf_test.gni")
+
+ pw_perf_test("foo_perf_test") {
+ sources = [ "foo_perf_test.cc" ]
+ }
+
+.. note::
+ If you use ``pw_watch``, the template is configured to build automatically
+ with ``pw_watch``. However you will still need to add your test group to the
+ pw_perf_tests group in the top level BUILD.gn.
+
+pw_perf_test template
+---------------------
+``pw_perf_test`` defines a single perf test suite. It creates two sub-targets.
+
+* ``<target_name>``: The test suite within a single binary. The test code is
+ linked against the target set in the build arg ``pw_unit_test_MAIN``.
+* ``<target_name>.lib``: The test sources without ``pw_unit_test_MAIN``.
+
+**Arguments**
+
+* All GN executable arguments are accepted and forwarded to the underlying
+ ``pw_executable``.
+* ``enable_if``: Boolean indicating whether the test should be built. If false,
+ replaces the test with an empty target. Default true.
+
+**Example**
+
+.. code::
+
+ import("$dir_pw_perf_test/perf_test.gni")
+
+ pw_perf_test("large_test") {
+ sources = [ "large_test.cc" ]
+ enable_if = device_has_1m_flash
+ }
+
+Grouping
+--------
+For grouping tests, no special template is required. Simply create a basic GN
+``group()`` and add each perf test as a dependency.
+
+**Example**
+
+.. code::
+
+ import("$dir_pw_perf_test/perf_test.gni")
+
+ pw_perf_test("foo_test") {
+ sources = [ "foo.cc" ]
+ }
+
+ pw_perf_test("bar_test") {
+ sources = [ "bar.cc" ]
+ }
+
+ group("my_perf_tests_collection") {
+ deps = [
+ ":foo_test",
+ ":bar_test",
+ ]
+ }
+
+Running
+-------
+To run perf tests from gn, locate the associated binaries from the ``out``
+directory and run/flash them manually.
+
+Bazel
+=====
+Bazel is a very efficient build system for running tests on host, needing very
+minimal setup to get tests running. To configure the timing interface, set the
+``pw_perf_test_timer_backend`` variable to use the preferred method of
+timekeeping. Right now, only the logging event handler is supported for Bazel.
+
+Template
+--------
+To use the ``pw_ccp_perf_test()`` template, load the ``pw_cc_perf_test``
+template from ``//pw_build:pigweed.bzl``.
+
+**Arguments**
+
+* All bazel executable arguments are accepted and forwarded to the underlying
+ ``native.cc_binary``.
+
+**Example**
+
+.. code::
+
+ load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_test",
+ )
+
+ pw_cc_perf_test(
+ name = "foo_test",
+ srcs = ["foo_perf_test.cc"],
+ )
+
+Running
+-------
+Running tests in Bazel is like running any other program. Use the default bazel
+run command: ``bazel run //path/to:target``.
+
diff --git a/pw_perf_test/log_perf_handler.cc b/pw_perf_test/log_perf_handler.cc
new file mode 100644
index 000000000..8bc72f313
--- /dev/null
+++ b/pw_perf_test/log_perf_handler.cc
@@ -0,0 +1,60 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+
+#include "pw_perf_test/log_perf_handler.h"
+
+#include "pw_log/log.h"
+#include "pw_perf_test/event_handler.h"
+#include "pw_perf_test/googletest_style_event_handler.h"
+#include "pw_perf_test/internal/timer.h"
+
+namespace pw::perf_test {
+
+void LoggingEventHandler::RunAllTestsStart(const TestRunInfo& summary) {
+ PW_LOG_INFO(PW_PERF_TEST_GOOGLESTYLE_RUN_ALL_TESTS_START);
+ PW_LOG_INFO(PW_PERF_TEST_GOOGLESTYLE_BEGINNING_SUMMARY,
+ summary.total_tests,
+ summary.default_iterations);
+}
+
+void LoggingEventHandler::TestCaseStart(const TestCase& info) {
+ PW_LOG_INFO(PW_PERF_TEST_GOOGLESTYLE_CASE_START, info.name);
+}
+
+void LoggingEventHandler::TestCaseEnd(const TestCase& info,
+ const Results& end_result) {
+ PW_LOG_INFO(PW_PERF_TEST_GOOGLESTYLE_CASE_RESULT,
+ static_cast<long>(end_result.mean),
+ internal::GetDurationUnitStr(),
+ static_cast<long>(end_result.min),
+ internal::GetDurationUnitStr(),
+ static_cast<long>(end_result.max),
+ internal::GetDurationUnitStr(),
+ end_result.iterations);
+ PW_LOG_INFO(PW_PERF_TEST_GOOGLESTYLE_CASE_END, info.name);
+}
+
+void LoggingEventHandler::RunAllTestsEnd() {
+ PW_LOG_INFO(PW_PERF_TEST_GOOGLESTYLE_RUN_ALL_TESTS_END);
+}
+
+void LoggingEventHandler::TestCaseIteration(const IterationResult& result) {
+ PW_LOG_DEBUG(PW_PERF_TEST_GOOGLESTYLE_ITERATION_REPORT,
+ static_cast<long>(result.number),
+ static_cast<long>(result.result),
+ internal::GetDurationUnitStr());
+}
+
+} // namespace pw::perf_test
diff --git a/pw_perf_test/log_perf_handler_main.cc b/pw_perf_test/log_perf_handler_main.cc
new file mode 100644
index 000000000..645ce6398
--- /dev/null
+++ b/pw_perf_test/log_perf_handler_main.cc
@@ -0,0 +1,22 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_perf_test/log_perf_handler.h"
+#include "pw_perf_test/perf_test.h"
+
+int main() {
+ pw::perf_test::LoggingEventHandler handler;
+ pw::perf_test::RunAllTests(handler);
+ return 0;
+}
diff --git a/pw_perf_test/perf_test.cc b/pw_perf_test/perf_test.cc
new file mode 100644
index 000000000..0e2f8d1ee
--- /dev/null
+++ b/pw_perf_test/perf_test.cc
@@ -0,0 +1,108 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#define PW_LOG_MODULE_NAME "pw_perf_test"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_INFO
+
+#include "pw_perf_test/perf_test.h"
+
+#include <cstdint>
+
+#include "pw_log/log.h"
+#include "pw_perf_test/event_handler.h"
+#include "pw_perf_test/internal/timer.h"
+
+namespace pw::perf_test {
+namespace internal {
+
+Framework Framework::framework_;
+
+int Framework::RunAllTests() {
+ if (!internal::TimerPrepare()) {
+ return false;
+ }
+
+ event_handler_->RunAllTestsStart(run_info_);
+
+ for (const TestInfo* test = tests_; test != nullptr; test = test->next()) {
+ State test_state =
+ CreateState(kDefaultIterations, *event_handler_, test->test_name());
+ test->Run(test_state);
+ }
+ internal::TimerCleanup();
+ event_handler_->RunAllTestsEnd();
+ return true;
+}
+
+void Framework::RegisterTest(TestInfo& new_test) {
+ ++run_info_.total_tests;
+ if (tests_ == nullptr) {
+ tests_ = &new_test;
+ return;
+ }
+ TestInfo* info = tests_;
+ for (; info->next() != nullptr; info = info->next()) {
+ }
+ info->SetNext(&new_test);
+}
+
+State CreateState(int durations,
+ EventHandler& event_handler,
+ const char* test_name) {
+ return State(durations, event_handler, test_name);
+}
+} // namespace internal
+
+bool State::KeepRunning() {
+ internal::Timestamp iteration_end = internal::GetCurrentTimestamp();
+ if (current_iteration_ == -1) {
+ ++current_iteration_;
+ event_handler_->TestCaseStart(test_info);
+ iteration_start_ = internal::GetCurrentTimestamp();
+ return true;
+ }
+ int64_t duration = internal::GetDuration(iteration_start_, iteration_end);
+ if (duration > max_) {
+ max_ = duration;
+ }
+ if (duration < min_) {
+ min_ = duration;
+ }
+ total_duration_ += duration;
+ ++current_iteration_;
+ PW_LOG_DEBUG("Iteration number: %d - Duration: %ld",
+ current_iteration_,
+ static_cast<long>(duration));
+ event_handler_->TestCaseIteration({current_iteration_, duration});
+ if (current_iteration_ == test_iterations_) {
+ PW_LOG_DEBUG("Total Duration: %ld Total Iterations: %d",
+ static_cast<long>(total_duration_),
+ test_iterations_);
+ mean_ = total_duration_ / test_iterations_;
+ PW_LOG_DEBUG("Mean: %ld: ", static_cast<long>(mean_));
+ PW_LOG_DEBUG("Minimum: %ld", static_cast<long>(min_));
+ PW_LOG_DEBUG("Maxmimum: %ld", static_cast<long>(max_));
+ event_handler_->TestCaseEnd(test_info,
+ Results{mean_, max_, min_, test_iterations_});
+ return false;
+ }
+ iteration_start_ = internal::GetCurrentTimestamp();
+ return true;
+}
+
+void RunAllTests(EventHandler& handler) {
+ internal::Framework::Get().RegisterEventHandler(handler);
+ internal::Framework::Get().RunAllTests();
+}
+
+} // namespace pw::perf_test
diff --git a/pw_perf_test/perf_test.gni b/pw_perf_test/perf_test.gni
new file mode 100644
index 000000000..9dd4e7201
--- /dev/null
+++ b/pw_perf_test/perf_test.gni
@@ -0,0 +1,84 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_compilation_testing/negative_compilation_test.gni")
+import("$dir_pw_toolchain/host_clang/toolchains.gni")
+import("$dir_pw_unit_test/test.gni")
+
+declare_args() {
+ # Chooses the backend for how the framework calculates time
+ pw_perf_test_TIMER_INTERFACE_BACKEND = ""
+
+ # Chooses the EventHandler for running the perf tests
+ pw_perf_test_MAIN_FUNCTION = "$dir_pw_perf_test:log_perf_handler_main"
+
+ # Chooses the executable template for performance tests
+ pw_perf_test_EXECUTABLE_TARGET_TYPE = "pw_executable"
+}
+
+# Creates a library and an executable target for a unit test with pw_unit_test.
+#
+# <target_name>.lib contains the provided test sources as a library, which can
+# then be linked into a test executable.
+# <target_name> is a standalone executable which contains only the test sources
+# specified in the pw_perf_test_template.
+#
+# Args:
+# - enable_if: (optional) Conditionally enables or disables this test. The
+# test target does nothing when the test is disabled. The
+# disabled test can still be built and run with the
+# <target_name>.DISABLED. Defaults to true (enable_if).
+# - All of the regular "executable" target args are accepted.
+#
+template("pw_perf_test") {
+ _test_target_name = target_name
+
+ _test_is_enabled = !defined(invoker.enable_if) || invoker.enable_if
+
+ _test_output_dir = "${target_out_dir}/test"
+ if (defined(invoker.output_dir)) {
+ _test_output_dir = invoker.output_dir
+ }
+
+ _test_main = pw_perf_test_MAIN_FUNCTION
+ if (defined(invoker.test_main)) {
+ _test_main = invoker.test_main
+ }
+
+ pw_internal_disableable_target("$target_name.lib") {
+ target_type = "pw_source_set"
+ enable_if = _test_is_enabled
+ forward_variables_from(invoker, "*", [ "metadata" ])
+
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ dir_pw_perf_test ]
+ }
+
+ pw_internal_disableable_target(_test_target_name) {
+ target_type = pw_perf_test_EXECUTABLE_TARGET_TYPE
+ enable_if = _test_is_enabled
+
+ deps = [ ":$_test_target_name.lib" ]
+ if (_test_main != "") {
+ deps += [ _test_main ]
+ }
+
+ output_dir = _test_output_dir
+ }
+}
diff --git a/pw_perf_test/perf_test_test.cc b/pw_perf_test/perf_test_test.cc
new file mode 100644
index 000000000..68bb10e24
--- /dev/null
+++ b/pw_perf_test/perf_test_test.cc
@@ -0,0 +1,44 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_perf_test/perf_test.h"
+
+#include "gtest/gtest.h"
+
+void TestingFunction(pw::perf_test::State& state) {
+ [[maybe_unused]] int p = 0;
+ while (state.KeepRunning()) {
+ ++p;
+ }
+}
+
+// This function is intentionally left blank
+void SimpleFunction() {}
+
+void SimpleFunctionWithArgs(int, bool) {}
+
+namespace pw::perf_test {
+namespace {
+
+PW_PERF_TEST(TestingComponentRegistration, TestingFunction);
+
+PW_PERF_TEST_SIMPLE(TestingSimpleRegistration, SimpleFunction);
+
+PW_PERF_TEST_SIMPLE(TestingSimpleRegistrationArgs,
+ SimpleFunctionWithArgs,
+ 123,
+ false);
+
+} // namespace
+} // namespace pw::perf_test
diff --git a/pw_perf_test/performance_test_generic.cc b/pw_perf_test/performance_test_generic.cc
new file mode 100644
index 000000000..5488a014d
--- /dev/null
+++ b/pw_perf_test/performance_test_generic.cc
@@ -0,0 +1,50 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_perf_test/perf_test.h"
+
+constexpr int kGlobalVariablePerfTest = 4;
+
+namespace pw::perf_test {
+namespace {
+
+void SimpleTestingFunction(pw::perf_test::State& state) {
+ [[maybe_unused]] int p = 0;
+ while (state.KeepRunning()) {
+ ++p;
+ }
+}
+
+void FunctionWithDelay(pw::perf_test::State& state, int a, int b) {
+ while (state.KeepRunning()) {
+ for (volatile int i = 0; i < a * b * 100000; i = i + 1) {
+ }
+ }
+}
+
+int TestSimple(int a, int b) { return a + b; }
+
+PW_PERF_TEST(IntialTest, SimpleTestingFunction);
+
+PW_PERF_TEST(FunctionWithParameters, FunctionWithDelay, 5, 5);
+
+PW_PERF_TEST(LambdaFunction, [](pw::perf_test::State& state_) {
+ FunctionWithDelay(state_, kGlobalVariablePerfTest, 4);
+});
+
+PW_PERF_TEST_SIMPLE(SimpleTest, TestSimple, 2, 4);
+PW_PERF_TEST_SIMPLE(
+ SimpleLambda, [](int a, int b) { return a + b; }, 1, 3);
+} // namespace
+} // namespace pw::perf_test
diff --git a/pw_perf_test/public/pw_perf_test/event_handler.h b/pw_perf_test/public/pw_perf_test/event_handler.h
new file mode 100644
index 000000000..22305e569
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/event_handler.h
@@ -0,0 +1,67 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+namespace pw::perf_test {
+
+// The data that will be reported on completion of an iteration.
+struct IterationResult {
+ int64_t number;
+ int64_t result;
+};
+
+// The data that will be reported upon the completion of an iteration.
+struct Results {
+ int64_t mean;
+ int64_t max;
+ int64_t min;
+ int iterations;
+};
+
+// Stores information on the upcoming collection of tests.
+struct TestRunInfo {
+ int total_tests;
+ int default_iterations;
+};
+
+struct TestCase {
+ const char* name;
+};
+
+// This is a declaration of the base EventHandler class. An EventHandler
+// collects and reports test results. Both the state and the framework classes
+// use their functions to report what happens at each stage
+class EventHandler {
+ public:
+ virtual ~EventHandler() = default;
+
+ // Called before all tests are run
+ virtual void RunAllTestsStart(const TestRunInfo& summary) = 0;
+
+ // Called after all tests are run
+ virtual void RunAllTestsEnd() = 0;
+
+ // Called when a new performance test is started
+ virtual void TestCaseStart(const TestCase& info) = 0;
+
+ // Called to output the results of an iteration
+ virtual void TestCaseIteration(const IterationResult& result) = 0;
+
+ // Called after a performance test ends
+ virtual void TestCaseEnd(const TestCase& info, const Results& end_result) = 0;
+};
+
+} // namespace pw::perf_test
diff --git a/pw_perf_test/public/pw_perf_test/googletest_style_event_handler.h b/pw_perf_test/public/pw_perf_test/googletest_style_event_handler.h
new file mode 100644
index 000000000..bf1a3e316
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/googletest_style_event_handler.h
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#define PW_PERF_TEST_GOOGLESTYLE_RUN_ALL_TESTS_START \
+ "[==========] Running all tests."
+#define PW_PERF_TEST_GOOGLESTYLE_BEGINNING_SUMMARY \
+ "[ PLANNING ] %d test(s) with %d run(s) each."
+#define PW_PERF_TEST_GOOGLESTYLE_RUN_ALL_TESTS_END \
+ "[==========] Done running all tests."
+
+#define PW_PERF_TEST_GOOGLESTYLE_CASE_START "[ RUN ] %s"
+#define PW_PERF_TEST_GOOGLESTYLE_CASE_RESULT \
+ "[ RESULT ] MEAN: %ld %s, MIN: %ld %s, MAX: %ld %s, ITERATIONS: %d"
+#define PW_PERF_TEST_GOOGLESTYLE_CASE_END "[ DONE ] %s"
+#define PW_PERF_TEST_GOOGLESTYLE_ITERATION_REPORT "[ Iteration ] #%ld: %ld %s"
diff --git a/pw_perf_test/public/pw_perf_test/internal/chrono_timer_interface.h b/pw_perf_test/public/pw_perf_test/internal/chrono_timer_interface.h
new file mode 100644
index 000000000..e366ba16c
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/internal/chrono_timer_interface.h
@@ -0,0 +1,39 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <chrono>
+#include <cstdbool>
+
+#include "pw_chrono/system_clock.h"
+#include "pw_perf_test/internal/duration_unit.h"
+
+namespace pw::perf_test::internal::backend {
+
+using Timestamp = chrono::SystemClock::time_point;
+
+inline constexpr DurationUnit kDurationUnit = DurationUnit::kNanoseconds;
+
+[[nodiscard]] inline bool TimerPrepare() { return true; }
+
+inline void TimerCleanup() {}
+
+inline Timestamp GetCurrentTimestamp() { return chrono::SystemClock::now(); }
+
+inline int64_t GetDuration(Timestamp begin, Timestamp end) {
+ return std::chrono::duration_cast<std::chrono::nanoseconds>(end - begin)
+ .count();
+}
+
+} // namespace pw::perf_test::internal::backend
diff --git a/pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h b/pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h
new file mode 100644
index 000000000..df547dbbb
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/internal/cyccnt_timer_interface.h
@@ -0,0 +1,62 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// This timing interface enables and targets ARM registers to enable clock cycle
+// counting. The documentation can be found here:
+// https://developer.arm.com/documentation/ddi0337/e/System-Debug/DWT?lang=en
+// https://developer.arm.com/documentation/ddi0337/e/CEGHJDCF
+
+#pragma once
+
+#include <cstdint>
+
+#include "pw_perf_test/internal/duration_unit.h"
+
+namespace pw::perf_test::internal::backend {
+
+// Creates a reference to the DWT Control unit
+inline volatile uint32_t& kDwtCtrl =
+ *reinterpret_cast<volatile uint32_t*>(0xE0001000);
+// Creates a reference to the memory address in which the DWT register stores
+// clock incrementations
+inline volatile uint32_t& kDwtCcynt =
+ *reinterpret_cast<volatile uint32_t*>(0xE0001004);
+// Creates a reference to the location of the Debug Exception and Monitor
+// Control register.
+inline volatile uint32_t& kDemcr =
+ *reinterpret_cast<volatile uint32_t*>(0xE000EDFC);
+
+using Timestamp = uint32_t;
+
+inline constexpr DurationUnit kDurationUnit = DurationUnit::kClockCycle;
+
+[[nodiscard]] inline bool TimerPrepare() {
+ kDemcr |= 0x01000000; // Enables the DWT control register
+ kDwtCtrl |= 0x00000001; // Enables the DWT clock
+ kDwtCcynt = 0x00000000; // Intializes the clock to 0
+ return (kDemcr & 0x01000000) && (kDwtCtrl & 0x00000001);
+}
+
+// Disables the DWT clock
+inline void TimerCleanup() { kDwtCtrl &= ~0x00000001; }
+
+inline Timestamp GetCurrentTimestamp() { return kDwtCcynt; }
+
+// Warning: this duration will overflow if the duration is greater that 2^32
+// clock cycles.
+inline int64_t GetDuration(Timestamp begin, Timestamp end) {
+ return static_cast<int64_t>(end - begin);
+}
+
+} // namespace pw::perf_test::internal::backend
diff --git a/pw_perf_test/public/pw_perf_test/internal/duration_unit.h b/pw_perf_test/public/pw_perf_test/internal/duration_unit.h
new file mode 100644
index 000000000..52f613791
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/internal/duration_unit.h
@@ -0,0 +1,24 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+namespace pw::perf_test::internal {
+
+// Time unit in which the test measurements will be conducted.
+enum class DurationUnit {
+ kNanoseconds,
+ kClockCycle,
+};
+
+} // namespace pw::perf_test::internal
diff --git a/pw_perf_test/public/pw_perf_test/internal/timer.h b/pw_perf_test/public/pw_perf_test/internal/timer.h
new file mode 100644
index 000000000..a36515b39
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/internal/timer.h
@@ -0,0 +1,53 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_perf_test/internal/duration_unit.h"
+#include "pw_perf_test_timer_backend/timer.h"
+
+namespace pw::perf_test::internal {
+
+using Timestamp = backend::Timestamp; // implementation-defined type
+
+// Returns true when the timer is ready to measure.
+[[nodiscard]] inline bool TimerPrepare() { return backend::TimerPrepare(); }
+
+// Performs any necessary cleanup for the timer.
+inline void TimerCleanup() { return backend::TimerCleanup(); }
+
+// Returns the current timestamp
+inline Timestamp GetCurrentTimestamp() {
+ return backend::GetCurrentTimestamp();
+}
+
+// Obtains the testing unit from the backend.
+inline constexpr DurationUnit kDurationUnit =
+ backend::kDurationUnit; // <cycles, ns, etc.>
+
+// Returns the duration in the specified unit.
+inline int64_t GetDuration(Timestamp begin, Timestamp end) {
+ return backend::GetDuration(begin, end);
+}
+
+constexpr const char* GetDurationUnitStr() {
+ switch (kDurationUnit) {
+ case DurationUnit::kNanoseconds:
+ return "ns";
+ case DurationUnit::kClockCycle:
+ return "clock cycles";
+ default:
+ return "unknown";
+ }
+}
+} // namespace pw::perf_test::internal
diff --git a/pw_perf_test/public/pw_perf_test/log_perf_handler.h b/pw_perf_test/public/pw_perf_test/log_perf_handler.h
new file mode 100644
index 000000000..8521d923f
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/log_perf_handler.h
@@ -0,0 +1,33 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+#include "pw_perf_test/event_handler.h"
+
+namespace pw::perf_test {
+
+// An event handler that depends on the pw_log module. This event handler acts
+// as the default for perf tests, and follows a GTEST-style format of messaging.
+class LoggingEventHandler : public EventHandler {
+ public:
+ void RunAllTestsStart(const TestRunInfo& summary) override;
+ void RunAllTestsEnd() override;
+ void TestCaseStart(const TestCase& info) override;
+ void TestCaseEnd(const TestCase& info, const Results& end_result) override;
+ void TestCaseIteration(const IterationResult& result) override;
+};
+
+} // namespace pw::perf_test
diff --git a/pw_perf_test/public/pw_perf_test/perf_test.h b/pw_perf_test/public/pw_perf_test/perf_test.h
new file mode 100644
index 000000000..e092087f9
--- /dev/null
+++ b/pw_perf_test/public/pw_perf_test/perf_test.h
@@ -0,0 +1,178 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+#include <limits>
+
+#include "pw_assert/assert.h"
+#include "pw_perf_test/event_handler.h"
+#include "pw_perf_test/internal/duration_unit.h"
+#include "pw_perf_test/internal/timer.h"
+#include "pw_preprocessor/arguments.h"
+
+#define PW_PERF_TEST(name, function, ...) \
+ const ::pw::perf_test::internal::TestInfo PwPerfTest_##name( \
+ #name, [](::pw::perf_test::State& pw_perf_test_state) { \
+ static_cast<void>( \
+ function(pw_perf_test_state PW_COMMA_ARGS(__VA_ARGS__))); \
+ })
+
+#define PW_PERF_TEST_SIMPLE(name, function, ...) \
+ PW_PERF_TEST( \
+ name, \
+ [](::pw::perf_test::State& pw_perf_test_simple_state, \
+ const auto&... args) { \
+ while (pw_perf_test_simple_state.KeepRunning()) { \
+ function(args...); \
+ } \
+ }, \
+ __VA_ARGS__)
+
+namespace pw::perf_test {
+
+class State;
+
+namespace internal {
+
+class TestInfo;
+
+// Allows access to the private State object constructor
+State CreateState(int durations,
+ EventHandler& event_handler,
+ const char* test_name);
+
+class Framework {
+ public:
+ constexpr Framework()
+ : event_handler_(nullptr),
+ tests_(nullptr),
+ run_info_{.total_tests = 0, .default_iterations = kDefaultIterations} {}
+
+ static Framework& Get() { return framework_; }
+
+ void RegisterEventHandler(EventHandler& event_handler) {
+ event_handler_ = &event_handler;
+ }
+
+ void RegisterTest(TestInfo&);
+
+ int RunAllTests();
+
+ private:
+ static constexpr int kDefaultIterations = 10;
+
+ EventHandler* event_handler_;
+
+ // Pointer to the list of tests
+ TestInfo* tests_;
+
+ TestRunInfo run_info_;
+
+ static Framework framework_;
+};
+
+class TestInfo {
+ public:
+ TestInfo(const char* test_name, void (*function_body)(State&))
+ : run_(function_body), test_name_(test_name) {
+ // Once a TestInfo object is created by the macro, this adds itself to the
+ // list of registered tests
+ Framework::Get().RegisterTest(*this);
+ }
+
+ // Returns the next registered test
+ TestInfo* next() const { return next_; }
+
+ void SetNext(TestInfo* next) { next_ = next; }
+
+ void Run(State& state) const { run_(state); }
+
+ const char* test_name() const { return test_name_; }
+
+ private:
+ // Function pointer to the code that will be measured
+ void (*run_)(State&);
+
+ // Intrusively linked list, this acts as a pointer to the next test
+ TestInfo* next_ = nullptr;
+
+ const char* test_name_;
+};
+
+} // namespace internal
+
+class State {
+ public:
+ // KeepRunning() should be called in a while loop. Responsible for managing
+ // iterations and timestamps.
+ bool KeepRunning();
+
+ private:
+ // Allows the framework to create state objects and unit tests for the state
+ // class
+ friend State internal::CreateState(int durations,
+ EventHandler& event_handler,
+ const char* test_name);
+
+ // Privated constructor to prevent unauthorized instances of the state class.
+ constexpr State(int iterations,
+ EventHandler& event_handler,
+ const char* test_name)
+ : mean_(-1),
+ test_iterations_(iterations),
+ total_duration_(0),
+ min_(std::numeric_limits<int64_t>::max()),
+ max_(std::numeric_limits<int64_t>::min()),
+ iteration_start_(),
+ current_iteration_(-1),
+ event_handler_(&event_handler),
+ test_info{.name = test_name} {
+ PW_ASSERT(test_iterations_ > 0);
+ }
+ // Set public after deciding how exactly to set user-defined iterations
+ void SetIterations(int iterations) {
+ PW_ASSERT(current_iteration_ == -1);
+ test_iterations_ = iterations;
+ PW_ASSERT(test_iterations_ > 0);
+ }
+
+ int64_t mean_;
+
+ // Stores the total number of iterations wanted
+ int test_iterations_;
+
+ // Stores the total duration of the tests.
+ int64_t total_duration_;
+
+ // Smallest value of the iterations
+ int64_t min_;
+
+ // Largest value of the iterations
+ int64_t max_;
+
+ // Time at the start of the iteration
+ internal::Timestamp iteration_start_;
+
+ // The current iteration
+ int current_iteration_;
+
+ EventHandler* event_handler_;
+
+ TestCase test_info;
+};
+
+void RunAllTests(pw::perf_test::EventHandler& handler);
+
+} // namespace pw::perf_test
diff --git a/pw_perf_test/state_test.cc b/pw_perf_test/state_test.cc
new file mode 100644
index 000000000..647e47a3b
--- /dev/null
+++ b/pw_perf_test/state_test.cc
@@ -0,0 +1,61 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_perf_test/event_handler.h"
+#include "pw_perf_test/perf_test.h"
+
+namespace pw::perf_test {
+namespace {
+
+class EmptyEventHandler : public EventHandler {
+ public:
+ void RunAllTestsStart(const TestRunInfo&) override {}
+ void TestCaseStart(const TestCase&) override {}
+ void TestCaseEnd(const TestCase&, const Results&) override {}
+ void TestCaseIteration(const IterationResult&) override {}
+ void RunAllTestsEnd() override {}
+};
+
+EmptyEventHandler handler;
+
+void TestFunction() {
+ for (volatile int i = 0; i < 100000; i = i + 1) {
+ }
+}
+
+TEST(StateTest, KeepRunningTest) {
+ constexpr int test_iterations = 10;
+ State state_obj = internal::CreateState(test_iterations, handler, "");
+ int total_iterations = 0;
+ while (state_obj.KeepRunning()) {
+ ++total_iterations;
+ TestFunction();
+ }
+ EXPECT_EQ(total_iterations, test_iterations);
+}
+
+TEST(StateTest, SingleTest) {
+ constexpr int test_iterations = 1;
+ State state_obj = internal::CreateState(test_iterations, handler, "");
+ int total_iterations = 0;
+ while (state_obj.KeepRunning()) {
+ ++total_iterations;
+ TestFunction();
+ }
+ EXPECT_EQ(total_iterations, test_iterations);
+}
+
+} // namespace
+} // namespace pw::perf_test
diff --git a/pw_perf_test/timer_test.cc b/pw_perf_test/timer_test.cc
new file mode 100644
index 000000000..ba65ffc01
--- /dev/null
+++ b/pw_perf_test/timer_test.cc
@@ -0,0 +1,32 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_perf_test/internal/timer.h"
+
+#include "gtest/gtest.h"
+
+namespace pw::perf_test::internal {
+namespace {
+
+TEST(TimerTest, DurationIsPositive) {
+ Timestamp start = GetCurrentTimestamp();
+ for (volatile int i = 0; i < 1000; i = i + 1) {
+ }
+ Timestamp end = GetCurrentTimestamp();
+ int64_t duration = GetDuration(start, end);
+ EXPECT_GT(duration, 0);
+}
+
+} // namespace
+} // namespace pw::perf_test::internal
diff --git a/pw_persistent_ram/BUILD.bazel b/pw_persistent_ram/BUILD.bazel
index 65f5551a2..e714450af 100644
--- a/pw_persistent_ram/BUILD.bazel
+++ b/pw_persistent_ram/BUILD.bazel
@@ -43,6 +43,8 @@ pw_cc_test(
srcs = [
"persistent_test.cc",
],
+ # The test contains intentional uninitialized memory access.
+ tags = ["nomsan"],
deps = [
":pw_persistent_ram",
"//pw_random",
@@ -55,6 +57,8 @@ pw_cc_test(
srcs = [
"persistent_buffer_test.cc",
],
+ # The test contains intentional uninitialized memory access.
+ tags = ["nomsan"],
deps = [
":pw_persistent_ram",
"//pw_random",
diff --git a/pw_persistent_ram/BUILD.gn b/pw_persistent_ram/BUILD.gn
index 7df09ca58..9930035a2 100644
--- a/pw_persistent_ram/BUILD.gn
+++ b/pw_persistent_ram/BUILD.gn
@@ -36,6 +36,7 @@ pw_source_set("pw_persistent_ram") {
dir_pw_bytes,
dir_pw_checksum,
dir_pw_preprocessor,
+ dir_pw_span,
dir_pw_stream,
]
}
@@ -68,13 +69,9 @@ pw_doc_group("docs") {
report_deps = [ ":persistent_size" ]
}
-pw_size_report("persistent_size") {
+pw_size_diff("persistent_size") {
title = "pw::persistent_ram::Persistent"
- # To see all the symbols, uncomment the following:
- # Note: The size report RST table won't be generated when full_report = true.
- # full_report = true
-
binaries = [
{
target = "size_report:persistent"
diff --git a/pw_persistent_ram/CMakeLists.txt b/pw_persistent_ram/CMakeLists.txt
index f9ba73ecd..59d69944e 100644
--- a/pw_persistent_ram/CMakeLists.txt
+++ b/pw_persistent_ram/CMakeLists.txt
@@ -14,7 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_persistent_ram
+pw_add_library(pw_persistent_ram STATIC
HEADERS
public/pw_persistent_ram/persistent.h
public/pw_persistent_ram/persistent_buffer.h
@@ -24,9 +24,8 @@ pw_add_module_library(pw_persistent_ram
pw_assert
pw_bytes
pw_checksum
- pw_polyfill.cstddef
- pw_polyfill.span
pw_preprocessor
+ pw_span
pw_stream
SOURCES
persistent_buffer.cc
@@ -35,7 +34,7 @@ pw_add_module_library(pw_persistent_ram
pw_add_test(pw_persistent_ram.persistent_test
SOURCES
persistent_test.cc
- DEPS
+ PRIVATE_DEPS
pw_persistent_ram
pw_random
GROUPS
@@ -46,7 +45,7 @@ pw_add_test(pw_persistent_ram.persistent_test
pw_add_test(pw_persistent_ram.persistent_buffer_test
SOURCES
persistent_buffer_test.cc
- DEPS
+ PRIVATE_DEPS
pw_persistent_ram
pw_random
GROUPS
diff --git a/pw_persistent_ram/persistent_buffer.cc b/pw_persistent_ram/persistent_buffer.cc
index 9cee8fcdf..2186f4dfd 100644
--- a/pw_persistent_ram/persistent_buffer.cc
+++ b/pw_persistent_ram/persistent_buffer.cc
@@ -35,7 +35,7 @@ Status PersistentBufferWriter::DoWrite(ConstByteSpan data) {
// Only checksum newly written data.
checksum_ = checksum::Crc16Ccitt::Calculate(
ByteSpan(buffer_.data() + size_, data.size_bytes()), checksum_);
- size_ += data.size_bytes();
+ size_ = size_ + data.size_bytes(); // += on a volatile is deprecated in C++20
return OkStatus();
}
diff --git a/pw_persistent_ram/persistent_buffer_test.cc b/pw_persistent_ram/persistent_buffer_test.cc
index 38c4fe9f8..397f7f9dd 100644
--- a/pw_persistent_ram/persistent_buffer_test.cc
+++ b/pw_persistent_ram/persistent_buffer_test.cc
@@ -14,12 +14,12 @@
#include "pw_persistent_ram/persistent_buffer.h"
#include <cstddef>
-#include <span>
#include <type_traits>
#include "gtest/gtest.h"
#include "pw_bytes/span.h"
#include "pw_random/xor_shift.h"
+#include "pw_span/span.h"
namespace pw::persistent_ram {
namespace {
@@ -33,9 +33,7 @@ class PersistentTest : public ::testing::Test {
void ZeroPersistentMemory() { memset(buffer_, 0, sizeof(buffer_)); }
void RandomFillMemory() {
random::XorShiftStarRng64 rng(0x9ad75);
- StatusWithSize sws = rng.Get(buffer_);
- ASSERT_TRUE(sws.ok());
- ASSERT_EQ(sws.size(), sizeof(buffer_));
+ rng.Get(buffer_);
}
PersistentBuffer<kBufferSize>& GetPersistentBuffer() {
@@ -59,8 +57,7 @@ TEST_F(PersistentTest, DefaultConstructionAndDestruction) {
auto writer = persistent.GetWriter();
EXPECT_EQ(persistent.size(), 0u);
- writer.Write(std::as_bytes(std::span(&kExpectedNumber, 1)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), writer.Write(as_bytes(span(&kExpectedNumber, 1))));
ASSERT_TRUE(persistent.has_value());
persistent.~PersistentBuffer(); // Emulate shutdown / global destructors.
@@ -89,15 +86,13 @@ TEST_F(PersistentTest, LongData) {
auto writer = persistent.GetWriter();
for (size_t i = 0; i < kTestString.length(); i += kWriteSize) {
- writer
- .Write(kTestString.data() + i,
- std::min(kWriteSize, kTestString.length() - i))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(),
+ writer.Write(kTestString.data() + i,
+ std::min(kWriteSize, kTestString.length() - i)));
}
// Need to manually write a null terminator since std::string_view doesn't
// include one in the string length.
- writer.Write(std::byte(0))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), writer.Write(std::byte(0)));
persistent.~PersistentBuffer(); // Emulate shutdown / global destructors.
}
@@ -133,8 +128,7 @@ TEST_F(PersistentTest, AppendingData) {
EXPECT_EQ(persistent.size(), 0u);
// Write an integer.
- writer.Write(std::as_bytes(std::span(&kTestNumber, 1)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), writer.Write(as_bytes(span(&kTestNumber, 1))));
ASSERT_TRUE(persistent.has_value());
persistent.~PersistentBuffer(); // Emulate shutdown / global destructors.
@@ -148,8 +142,8 @@ TEST_F(PersistentTest, AppendingData) {
// Write more data.
auto writer = persistent.GetWriter();
EXPECT_EQ(persistent.size(), sizeof(kTestNumber));
- writer.Write(std::as_bytes(std::span<const char>(kTestString)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(),
+ writer.Write(as_bytes(span<const char>(kTestString))));
persistent.~PersistentBuffer(); // Emulate shutdown / global destructors.
}
diff --git a/pw_persistent_ram/persistent_test.cc b/pw_persistent_ram/persistent_test.cc
index 8c667ec1d..69b26dcce 100644
--- a/pw_persistent_ram/persistent_test.cc
+++ b/pw_persistent_ram/persistent_test.cc
@@ -95,10 +95,8 @@ class MutablePersistentTest : public ::testing::Test {
void ZeroPersistentMemory() { memset(&buffer_, 0, sizeof(buffer_)); }
void RandomFillMemory() {
random::XorShiftStarRng64 rng(0x9ad75);
- StatusWithSize sws = rng.Get(std::span<std::byte>(
- reinterpret_cast<std::byte*>(&buffer_), sizeof(buffer_)));
- ASSERT_TRUE(sws.ok());
- ASSERT_EQ(sws.size(), sizeof(buffer_));
+ rng.Get(span<std::byte>(reinterpret_cast<std::byte*>(&buffer_),
+ sizeof(buffer_)));
}
// Allocate a chunk of aligned storage that can be independently controlled.
diff --git a/pw_persistent_ram/public/pw_persistent_ram/persistent.h b/pw_persistent_ram/public/pw_persistent_ram/persistent.h
index 29d2247b8..eb8e5a76e 100644
--- a/pw_persistent_ram/public/pw_persistent_ram/persistent.h
+++ b/pw_persistent_ram/public/pw_persistent_ram/persistent.h
@@ -15,13 +15,13 @@
#include <cstdint>
#include <cstring>
-#include <span>
#include <type_traits>
#include <utility>
#include "pw_assert/assert.h"
#include "pw_checksum/crc16_ccitt.h"
#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
namespace pw::persistent_ram {
@@ -48,7 +48,7 @@ PW_MODIFY_DIAGNOSTIC_GCC(ignored, "-Wmaybe-uninitialized");
// WARNING: Unlike a DoubleBufferedPersistent, a Persistent will be lost if a
// write/set operation is interrupted or otherwise not completed.
//
-// TODO(pwbug/348): Consider a different integrity check implementation which
+// TODO(b/235277454): Consider a different integrity check implementation which
// does not use a 512B lookup table.
template <typename T>
class Persistent {
@@ -160,7 +160,7 @@ class Persistent {
uint16_t CalculateCrc() const {
return checksum::Crc16Ccitt::Calculate(
- std::as_bytes(std::span(const_cast<const T*>(&contents_), 1)));
+ as_bytes(span(const_cast<const T*>(&contents_), 1)));
}
// Use unions to denote that these members are never initialized by design and
diff --git a/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h b/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h
index c4c159658..ab2e41eeb 100644
--- a/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h
+++ b/pw_persistent_ram/public/pw_persistent_ram/persistent_buffer.h
@@ -15,13 +15,13 @@
#include <cstdint>
#include <cstring>
-#include <span>
#include <type_traits>
#include <utility>
#include "pw_bytes/span.h"
#include "pw_checksum/crc16_ccitt.h"
#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_stream/stream.h"
diff --git a/pw_persistent_ram/size_report/BUILD.bazel b/pw_persistent_ram/size_report/BUILD.bazel
index 614cd3b63..799f16c94 100644
--- a/pw_persistent_ram/size_report/BUILD.bazel
+++ b/pw_persistent_ram/size_report/BUILD.bazel
@@ -25,11 +25,31 @@ pw_cc_binary(
name = "persistent",
srcs = [
"persistent.cc",
+ ],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_persistent_ram",
+ ],
+)
+
+pw_cc_binary(
+ name = "persistent_base",
+ srcs = [
"persistent_base.cc",
+ ],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_persistent_ram",
+ ],
+)
+
+pw_cc_binary(
+ name = "persistent_base_with_crc16",
+ srcs = [
"persistent_base_with_crc16.cc",
],
deps = [
"//pw_bloat:bloat_this_binary",
- "//pw_persistent_ram:persistent",
+ "//pw_persistent_ram",
],
)
diff --git a/pw_persistent_ram/size_report/persistent_base_with_crc16.cc b/pw_persistent_ram/size_report/persistent_base_with_crc16.cc
index a4e18e3a5..fe5153568 100644
--- a/pw_persistent_ram/size_report/persistent_base_with_crc16.cc
+++ b/pw_persistent_ram/size_report/persistent_base_with_crc16.cc
@@ -40,7 +40,7 @@ int main() {
// Use CRC16.
value = pw::checksum::Crc16Ccitt::Calculate(
- std::as_bytes(std::span(const_cast<uint32_t*>(&value), 1)));
+ pw::as_bytes(pw::span(const_cast<uint32_t*>(&value), 1)));
return 0;
}
diff --git a/pw_polyfill/Android.bp b/pw_polyfill/Android.bp
new file mode 100644
index 000000000..ff4677663
--- /dev/null
+++ b/pw_polyfill/Android.bp
@@ -0,0 +1,24 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_polyfill_headers",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ host_supported: true,
+}
diff --git a/pw_polyfill/BUILD.bazel b/pw_polyfill/BUILD.bazel
index 3fcef9b1b..473173a37 100644
--- a/pw_polyfill/BUILD.bazel
+++ b/pw_polyfill/BUILD.bazel
@@ -17,32 +17,11 @@ load(
"pw_cc_library",
"pw_cc_test",
)
-load("@rules_cc_toolchain//cc_toolchain:cc_toolchain_import.bzl", "cc_toolchain_import")
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
-cc_toolchain_import(
- name = "toolchain_polyfill_overrides",
- hdrs = [
- "public_overrides/bit",
- "public_overrides/cstddef",
- "public_overrides/iterator",
- "public_overrides/type_traits",
- "standard_library_public/pw_polyfill/standard_library/bit.h",
- "standard_library_public/pw_polyfill/standard_library/cstddef.h",
- "standard_library_public/pw_polyfill/standard_library/iterator.h",
- "standard_library_public/pw_polyfill/standard_library/namespace.h",
- "standard_library_public/pw_polyfill/standard_library/type_traits.h",
- ],
- includes = [
- "public",
- "public_overrides",
- "standard_library_public",
- ],
-)
-
pw_cc_library(
name = "pw_polyfill",
hdrs = [
@@ -52,85 +31,36 @@ pw_cc_library(
includes = ["public"],
)
-# TODO(pwbug/602): Deprecate this once all users have been migrated to targeted
-# polyfill deps.
-pw_cc_library(
- name = "overrides",
- deps = [
- ":bit",
- ":cstddef",
- ":iterator",
- ":span",
- ":type_traits",
- ],
-)
-
-# Provides <bit>'s std::endian.
-pw_cc_library(
- name = "bit",
- hdrs = [
- "public_overrides/bit",
- "standard_library_public/pw_polyfill/standard_library/bit.h",
- ],
- includes = [
- "public_overrides",
- "standard_library_public",
- ],
- deps = [":standard_library"],
-)
-
-# Provides <cstddef>'s std::byte.
+# Provides <cstddef>'s std::byte for C++14.
pw_cc_library(
name = "cstddef",
hdrs = [
- "public_overrides/cstddef",
+ "cstddef_public_overrides/cstddef",
"standard_library_public/pw_polyfill/standard_library/cstddef.h",
],
includes = [
"public_overrides",
"standard_library_public",
],
+ # Polyfills aren't supported in the Bazel build, so disallow use.
+ visibility = ["//visibility:private"],
deps = [":standard_library"],
)
-# TODO(pwbug/603): Remove this polyfill.
+# Provides <iterator>'s std::data and std::size for C++14.
pw_cc_library(
name = "iterator",
hdrs = [
- "public_overrides/iterator",
+ "iterator_public_overrides/iterator",
"standard_library_public/pw_polyfill/standard_library/iterator.h",
],
includes = [
"public_overrides",
"standard_library_public",
],
- deps = [
- ":standard_library",
- ":type_traits",
- ],
-)
-
-# Provides <span>.
-pw_cc_library(
- name = "span",
- deps = ["//pw_span"],
-)
-
-# TODO(pwbug/603): Remove this polyfill.
-pw_cc_library(
- name = "type_traits",
- hdrs = [
- "public_overrides/type_traits",
- "standard_library_public/pw_polyfill/standard_library/type_traits.h",
- ],
- includes = [
- "public_overrides",
- "standard_library_public",
- ],
- deps = [
- ":cstddef",
- ":standard_library",
- ],
+ # Polyfills aren't supported in the Bazel build, so disallow use.
+ visibility = ["//visibility:private"],
+ deps = [":standard_library"],
)
pw_cc_library(
@@ -139,15 +69,18 @@ pw_cc_library(
"standard_library_public/pw_polyfill/standard_library/namespace.h",
],
includes = ["standard_library_public"],
- visibility = ["//pw_span:__pkg__"],
+ visibility = [
+ "//pw_minimal_cpp_stdlib:__pkg__",
+ "//pw_span:__pkg__",
+ ],
)
pw_cc_test(
- name = "default_cpp_test",
- srcs = [
- "test.cc",
- ],
+ name = "test",
+ srcs = ["test.cc"],
deps = [
+ ":cstddef",
+ ":iterator",
":pw_polyfill",
":standard_library",
"//pw_unit_test",
diff --git a/pw_polyfill/BUILD.gn b/pw_polyfill/BUILD.gn
index 1c0cd4bd8..0302de39a 100644
--- a/pw_polyfill/BUILD.gn
+++ b/pw_polyfill/BUILD.gn
@@ -33,80 +33,45 @@ pw_source_set("pw_polyfill") {
]
}
-config("overrides_config") {
- include_dirs = [ "public_overrides" ]
+config("cstddef_overrides_config") {
+ include_dirs = [ "cstddef_public_overrides" ]
+ cflags = [ "-Wno-gnu-include-next" ]
visibility = [ ":*" ]
}
-# TODO(pwbug/602): Remove this overrides target by migrating all users to
-# explicitly depend on the polyfill(s) they require.
-group("overrides") {
- public_deps = [
- ":bit",
- ":cstddef",
- ":iterator",
- ":span",
- ":type_traits",
- ]
+config("iterator_overrides_config") {
+ include_dirs = [ "iterator_public_overrides" ]
+ cflags = [ "-Wno-gnu-include-next" ]
+ visibility = [ ":*" ]
}
config("standard_library_public") {
include_dirs = [ "standard_library_public" ]
}
-# Provides <bit>'s std::endian.
-pw_source_set("bit") {
- public_configs = [
- ":standard_library_public",
- ":overrides_config",
- ]
- public_deps = [ ":standard_library" ]
- remove_public_deps = [ "*" ]
- inputs = [ "public_overrides/bit" ]
- public = [ "standard_library_public/pw_polyfill/standard_library/bit.h" ]
-}
-
-# Provides <cstddef>'s std::byte.
+# Provides <cstddef>'s std::byte for C++14.
pw_source_set("cstddef") {
public_configs = [
":standard_library_public",
- ":overrides_config",
+ ":cstddef_overrides_config",
]
public_deps = [ ":standard_library" ]
remove_public_deps = [ "*" ]
- inputs = [ "public_overrides/cstddef" ]
- public = [ "standard_library_public/pw_polyfill/standard_library/cstddef.h" ]
+ public = [ "cstddef_public_overrides/cstddef" ]
+ sources = [ "standard_library_public/pw_polyfill/standard_library/cstddef.h" ]
}
-# TODO(pwbug/603): Remove this polyfill.
+# Provides <iterator>'s std::data and std::size for C++14.
pw_source_set("iterator") {
public_configs = [
":standard_library_public",
- ":overrides_config",
- ]
- public_deps = [ ":standard_library" ]
- remove_public_deps = [ "*" ]
- inputs = [ "public_overrides/iterator" ]
- public = [ "standard_library_public/pw_polyfill/standard_library/iterator.h" ]
-}
-
-# Provides <span>.
-pw_source_set("span") {
- remove_public_deps = [ "*" ]
- public_deps = [ "$dir_pw_span:polyfill" ]
-}
-
-# TODO(pwbug/603): Remove this polyfill.
-pw_source_set("type_traits") {
- public_configs = [
- ":standard_library_public",
- ":overrides_config",
+ ":iterator_overrides_config",
]
public_deps = [ ":standard_library" ]
remove_public_deps = [ "*" ]
- inputs = [ "public_overrides/type_traits" ]
- public =
- [ "standard_library_public/pw_polyfill/standard_library/type_traits.h" ]
+ public = [ "iterator_public_overrides/iterator" ]
+ sources =
+ [ "standard_library_public/pw_polyfill/standard_library/iterator.h" ]
}
pw_source_set("standard_library") {
@@ -118,23 +83,21 @@ pw_source_set("standard_library") {
}
pw_test_group("tests") {
- tests = [
- ":default_cpp_test",
- ":cpp14_test",
- ]
+ tests = [ ":test" ]
group_deps = [ "$dir_pw_span:tests" ]
}
-pw_test("default_cpp_test") {
+pw_test("test") {
deps = [ ":pw_polyfill" ]
- sources = [ "test.cc" ]
-}
-pw_test("cpp14_test") {
- remove_configs = [ "$dir_pw_build:cpp17" ]
- configs = [ "$dir_pw_build:cpp14" ]
- sources = [ "test.cc" ]
- deps = [ ":pw_polyfill" ]
+ # Do not depend on :cstddef and :iterator since they override library headers.
+ # Instead, add their include path and list them as sources.
+ configs = [ ":standard_library_public" ]
+ sources = [
+ "standard_library_public/pw_polyfill/standard_library/cstddef.h",
+ "standard_library_public/pw_polyfill/standard_library/iterator.h",
+ "test.cc",
+ ]
}
pw_doc_group("docs") {
diff --git a/pw_polyfill/CMakeLists.txt b/pw_polyfill/CMakeLists.txt
index c878dd35d..036857fee 100644
--- a/pw_polyfill/CMakeLists.txt
+++ b/pw_polyfill/CMakeLists.txt
@@ -14,7 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_polyfill
+pw_add_library(pw_polyfill INTERFACE
HEADERS
public/pw_polyfill/language_feature_macros.h
public/pw_polyfill/standard.h
@@ -25,75 +25,7 @@ if(Zephyr_FOUND AND CONFIG_PIGWEED_POLYFILL)
zephyr_link_libraries(pw_polyfill)
endif()
-# TODO(pwbug/602): Remove this overrides target by migrating all users to
-# explicitly depend on the polyfill(s) they require.
-pw_add_module_library(pw_polyfill.overrides
- PUBLIC_DEPS
- pw_polyfill.bit
- pw_polyfill.cstddef
- pw_polyfill.iterator
- pw_polyfill.span
- pw_polyfill.type_traits
-)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_POLYFILL_OVERRIDES)
- zephyr_link_libraries(pw_polyfill.overrides)
-endif()
-
-# Provides <bit>'s std::endian.
-pw_add_module_library(pw_polyfill.bit
- HEADERS
- public_overrides/bit
- standard_library_public/pw_polyfill/standard_library/bit.h
- PUBLIC_INCLUDES
- public_overrides
- standard_library_public
- PUBLIC_DEPS
- pw_polyfill.standard_library
-)
-
-# Provides <cstddef>'s std::byte.
-pw_add_module_library(pw_polyfill.cstddef
- HEADERS
- public_overrides/cstddef
- standard_library_public/pw_polyfill/standard_library/cstddef.h
- PUBLIC_INCLUDES
- public_overrides
- standard_library_public
- PUBLIC_DEPS
- pw_polyfill.standard_library
-)
-
-# TODO(pwbug/603): Remove this polyfill.
-pw_add_module_library(pw_polyfill.iterator
- HEADERS
- public_overrides/iterator
- standard_library_public/pw_polyfill/standard_library/iterator.h
- PUBLIC_INCLUDES
- public_overrides
- standard_library_public
- PUBLIC_DEPS
- pw_polyfill.standard_library
-)
-
-# Provides <span>.
-pw_add_module_library(pw_polyfill.span
- PUBLIC_DEPS
- pw_span
-)
-
-# TODO(pwbug/603): Remove this polyfill.
-pw_add_module_library(pw_polyfill.type_traits
- HEADERS
- public_overrides/type_traits
- standard_library_public/pw_polyfill/standard_library/type_traits.h
- PUBLIC_INCLUDES
- public_overrides
- standard_library_public
- PUBLIC_DEPS
- pw_polyfill.standard_library
-)
-
-pw_add_module_library(pw_polyfill.standard_library
+pw_add_library(pw_polyfill.standard_library INTERFACE
HEADERS
standard_library_public/pw_polyfill/standard_library/namespace.h
PUBLIC_INCLUDES
diff --git a/pw_polyfill/cstddef_public_overrides/cstddef b/pw_polyfill/cstddef_public_overrides/cstddef
new file mode 100644
index 000000000..b0cff3b14
--- /dev/null
+++ b/pw_polyfill/cstddef_public_overrides/cstddef
@@ -0,0 +1,22 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+static_assert(__cplusplus < 201703L,
+ "The <cstddef> polyfill header is only intended for C++14. "
+ "It cannot be used when building with C++17 or newer.");
+
+#include_next <cstddef>
+
+#include "pw_polyfill/standard_library/cstddef.h"
diff --git a/pw_polyfill/docs.rst b/pw_polyfill/docs.rst
index b998c8417..419ac22aa 100644
--- a/pw_polyfill/docs.rst
+++ b/pw_polyfill/docs.rst
@@ -3,75 +3,112 @@
===========
pw_polyfill
===========
-The ``pw_polyfill`` module backports new C++ features to C++14.
-
-------------------------------------------------
-Backport new C++ features to older C++ standards
-------------------------------------------------
-The main purpose of ``pw_polyfill`` is to bring new C++ library and language
-features to older C++ standards. No additional ``#include`` statements are
-required to use these features; simply write code assuming that the features are
-available. This implicit feature backporting is provided through the
-``overrides`` library in the ``pw_polyfill`` module. GN automatically adds this
-library as a dependency in ``pw_source_set``.
-
-``pw_polyfill`` backports C++ library features by wrapping the standard C++ and
-C headers. The wrapper headers include the original header using
-`#include_next <https://gcc.gnu.org/onlinedocs/cpp/Wrapper-Headers.html>`_, then
-add missing features. The backported features are only defined if they aren't
-provided by the standard header, so ``pw_polyfill`` is safe to use when
-compiling with any standard C++14 or newer.
-
-The wrapper headers are in ``public_overrides``. These are provided through the
-``"$dir_pw_polyfill"`` libraries, which the GN build adds as a
-dependency for all targets:
-
-* ``$dir_pw_polyfill:bit``
-* ``$dir_pw_polyfill:cstddef``
-* ``$dir_pw_polyfill:iterator``
-* ``$dir_pw_polyfill:span``
-* ``$dir_pw_polyfill:type_traits``
-
-To apply overrides in Bazel or CMake, depend on the targets you need such as
-``//pw_polyfill:span`` or ``pw_polyfill.span`` as an example.
-
-Backported features
-===================
-================== ================================ =============================== ========================================
-Header Feature Level of support Feature test macro
-================== ================================ =============================== ========================================
-<bit> std::endian full __cpp_lib_endian
-<cstdlib> std::byte full __cpp_lib_byte
-<iterator> std::data, std::size full __cpp_lib_nonmember_container_access
-<type_traits> std::bool_constant full __cpp_lib_bool_constant
-<type_traits> std::negation, etc. full __cpp_lib_logical_traits
-================== ================================ =============================== ========================================
+The ``pw_polyfill`` module supports compiling code against different C++
+standards. It also supports backporting a few C++17 features to C++14.
----------------------------------------------------
Adapt code to compile with different versions of C++
----------------------------------------------------
- ``pw_polyfill`` provides features for adapting to different C++ standards when
- ``pw_polyfill:overrides``'s automatic backporting is insufficient:
- - ``pw_polyfill/standard.h`` -- provides a macro for checking the C++ standard
- - ``pw_polyfill/language_feature_macros.h`` -- provides macros for adapting
- code to work with or without newer language features
+C++ standard macro
+==================
+``pw_polyfill/standard.h`` provides a macro for checking if a C++ standard is
+supported.
+
+.. c:macro:: PW_CXX_STANDARD_IS_SUPPORTED(standard)
+
+ Evaluates true if the provided C++ standard (98, 11, 14, 17, 20) is supported
+ by the compiler. This is a simpler, cleaner alternative to checking the value
+ of the ``__cplusplus`` macro.
Language feature macros
=======================
-====================== ================================ ======================================== ==========================
-Macro Feature Description Feature test macro
-====================== ================================ ======================================== ==========================
-PW_INLINE_VARIABLE inline variables inline if supported by the compiler __cpp_inline_variables
-PW_CONSTEXPR_CPP20 constexpr in C++20 constexpr if compiling for C++20 __cplusplus >= 202002L
-PW_CONSTEVAL consteval consteval if supported by the compiler __cpp_consteval
-PW_CONSTINIT constinit constinit in clang and GCC 10+ __cpp_constinit
-====================== ================================ ======================================== ==========================
+``pw_polyfill/language_feature_macros.h`` provides macros for adapting code to
+work with or without C++ language features.
+
+.. list-table::
+ :header-rows: 1
+
+ * - Macro
+ - Feature
+ - Description
+ - Feature test macro
+ * - ``PW_INLINE_VARIABLE``
+ - inline variables
+ - inline if supported by the compiler
+ - ``__cpp_inline_variables``
+ * - ``PW_CONSTEXPR_CPP20``
+ - ``constexpr``
+ - ``constexpr`` if compiling for C++20
+ - ``__cplusplus >= 202002L``
+ * - ``PW_CONSTEVAL``
+ - ``consteval``
+ - ``consteval`` if supported by the compiler
+ - ``__cpp_consteval``
+ * - ``PW_CONSTINIT``
+ - ``constinit``
+ - ``constinit`` in clang and GCC 10+
+ - ``__cpp_constinit``
In GN, Bazel, or CMake, depend on ``$dir_pw_polyfill``, ``//pw_polyfill``,
or ``pw_polyfill``, respectively, to access these features. In other build
-systems, add ``pw_polyfill/standard_library_public`` and
-``pw_polyfill/public_overrides`` as include paths.
+systems, add ``pw_polyfill/public`` as an include path.
+
+------------------------------------------------
+Backport new C++ features to older C++ standards
+------------------------------------------------
+Pigweed backports a few C++ features to older C++ standards. These features
+are provided in the ``pw`` namespace. If the features are provided by the
+toolchain, the ``pw`` versions are aliases of the ``std`` versions.
+
+``pw_polyfill`` also backports a few C++17 library features to C++14 by wrapping
+the standard C++ and C headers. The wrapper headers include the original header
+using `#include_next
+<https://gcc.gnu.org/onlinedocs/cpp/Wrapper-Headers.html>`_, then add missing
+features. The backported features are only defined if they aren't provided by
+the standard header and can only be used when compiling with C++14 in GN.
+
+Backported features
+===================
+.. list-table::
+ :header-rows: 1
+
+ * - Header
+ - Feature
+ - Feature test macro
+ - Module
+ - Polyfill header
+ - Polyfill name
+ * - ``<array>``
+ - ``std::to_array``
+ - ``__cpp_lib_to_array``
+ - :ref:`module-pw_containers`
+ - ``pw_containers/to_array.h``
+ - ``pw::containers::to_array``
+ * - ``<bit>``
+ - ``std::endian``
+ - ``__cpp_lib_endian``
+ - :ref:`module-pw_bytes`
+ - ``pw_bytes/bit.h``
+ - ``pw::endian``
+ * - ``<cstdlib>``
+ - ``std::byte``
+ - ``__cpp_lib_byte``
+ - pw_polyfill
+ - ``<cstdlib>``
+ - ``std::byte``
+ * - ``<iterator>``
+ - ``std::data``, ``std::size``
+ - ``__cpp_lib_nonmember_container_access``
+ - pw_polyfill
+ - ``<iterator>``
+ - ``std::data``, ``std::size``
+ * - ``<span>``
+ - ``std::span``
+ - ``__cpp_lib_span``
+ - :ref:`module-pw_span`
+ - ``pw_span/span.h``
+ - ``pw::span``
-------------
Compatibility
@@ -81,5 +118,4 @@ C++14
Zephyr
======
To enable ``pw_polyfill`` for Zephyr add ``CONFIG_PIGWEED_POLYFILL=y`` to the
-project's configuration. Similarly, to enable ``pw_polyfill.overrides``, add
-``CONFIG_PIGWEED_POLYFILL_OVERRIDES=y`` to the project's configuration.
+project's configuration.
diff --git a/pw_polyfill/iterator_public_overrides/iterator b/pw_polyfill/iterator_public_overrides/iterator
new file mode 100644
index 000000000..80394d868
--- /dev/null
+++ b/pw_polyfill/iterator_public_overrides/iterator
@@ -0,0 +1,22 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+static_assert(__cplusplus < 201703L,
+ "The <iterator> polyfill header is only intended for C++14. "
+ "It cannot be used when building with C++17 or newer.");
+
+#include_next <iterator>
+
+#include "pw_polyfill/standard_library/iterator.h"
diff --git a/pw_polyfill/public/pw_polyfill/language_feature_macros.h b/pw_polyfill/public/pw_polyfill/language_feature_macros.h
index 87dc4e59f..b4d9d2ace 100644
--- a/pw_polyfill/public/pw_polyfill/language_feature_macros.h
+++ b/pw_polyfill/public/pw_polyfill/language_feature_macros.h
@@ -40,7 +40,7 @@
#define PW_CONSTINIT constinit
#elif defined(__clang__)
#define PW_CONSTINIT [[clang::require_constant_initialization]]
-#elif __GNUC__ >= 10
+#elif defined(__GNUC__) && __GNUC__ >= 10
#define PW_CONSTINIT __constinit
#else
#define PW_CONSTINIT
diff --git a/pw_polyfill/public/pw_polyfill/standard.h b/pw_polyfill/public/pw_polyfill/standard.h
index d5e1b75a9..c1faa30e0 100644
--- a/pw_polyfill/public/pw_polyfill/standard.h
+++ b/pw_polyfill/public/pw_polyfill/standard.h
@@ -24,3 +24,4 @@
#define _PW_CXX_STANDARD_11() 201103L
#define _PW_CXX_STANDARD_14() 201402L
#define _PW_CXX_STANDARD_17() 201703L
+#define _PW_CXX_STANDARD_20() 202002L
diff --git a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h b/pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h
index e8dfc9d1b..fc6bfa26c 100644
--- a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h
+++ b/pw_polyfill/standard_library_public/pw_polyfill/standard_library/cstddef.h
@@ -61,12 +61,12 @@ constexpr byte& operator&=(byte& l, byte r) noexcept { return l = l & r; }
constexpr byte& operator^=(byte& l, byte r) noexcept { return l = l ^ r; }
template <typename I>
-constexpr inline byte& operator<<=(byte& b, I shift) noexcept {
+constexpr byte& operator<<=(byte& b, I shift) noexcept {
return b = b << shift;
}
template <typename I>
-constexpr inline byte& operator>>=(byte& b, I shift) noexcept {
+constexpr byte& operator>>=(byte& b, I shift) noexcept {
return b = b >> shift;
}
diff --git a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/type_traits.h b/pw_polyfill/standard_library_public/pw_polyfill/standard_library/type_traits.h
deleted file mode 100644
index 2cd602b59..000000000
--- a/pw_polyfill/standard_library_public/pw_polyfill/standard_library/type_traits.h
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-#pragma once
-
-#include <type_traits>
-
-#include "pw_polyfill/standard_library/namespace.h"
-
-_PW_POLYFILL_BEGIN_NAMESPACE_STD
-
-#ifndef __cpp_lib_bool_constant
-#define __cpp_lib_bool_constant 201505L
-template <bool kValue>
-using bool_constant = integral_constant<bool, kValue>;
-#endif // __cpp_lib_bool_constant
-
-#ifndef __cpp_lib_logical_traits
-#define __cpp_lib_logical_traits 201510L
-template <typename Value>
-struct negation : bool_constant<!bool(Value::value)> {};
-
-template <typename...>
-struct conjunction : std::true_type {};
-template <typename B1>
-struct conjunction<B1> : B1 {};
-template <typename B1, typename... Bn>
-struct conjunction<B1, Bn...>
- : std::conditional_t<bool(B1::value), conjunction<Bn...>, B1> {};
-
-template <typename...>
-struct disjunction : std::false_type {};
-template <typename B1>
-struct disjunction<B1> : B1 {};
-template <typename B1, typename... Bn>
-struct disjunction<B1, Bn...>
- : std::conditional_t<bool(B1::value), B1, disjunction<Bn...>> {};
-
-#endif // __cpp_lib_logical_traits
-
-_PW_POLYFILL_END_NAMESPACE_STD
diff --git a/pw_polyfill/test.cc b/pw_polyfill/test.cc
index b761d4e2d..d17475df4 100644
--- a/pw_polyfill/test.cc
+++ b/pw_polyfill/test.cc
@@ -17,10 +17,8 @@
#include "gtest/gtest.h"
#include "pw_polyfill/language_feature_macros.h"
#include "pw_polyfill/standard.h"
-#include "pw_polyfill/standard_library/bit.h"
#include "pw_polyfill/standard_library/cstddef.h"
#include "pw_polyfill/standard_library/iterator.h"
-#include "pw_polyfill/standard_library/type_traits.h"
namespace pw {
namespace polyfill {
@@ -40,15 +38,11 @@ static_assert(PW_CXX_STANDARD_IS_SUPPORTED(17), "C++17 must be not supported");
static_assert(!PW_CXX_STANDARD_IS_SUPPORTED(17), "C++17 must be supported");
#endif // __cplusplus >= 201703L
-TEST(Bit, Endian) {
- if (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) {
- EXPECT_EQ(std::endian::native, std::endian::big);
- } else if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) {
- EXPECT_EQ(std::endian::native, std::endian::little);
- } else {
- FAIL();
- }
-}
+#if __cplusplus >= 202002L
+static_assert(PW_CXX_STANDARD_IS_SUPPORTED(20), "C++20 must be supported");
+#else
+static_assert(!PW_CXX_STANDARD_IS_SUPPORTED(20), "C++20 must not be supported");
+#endif // __cplusplus >= 202002L
TEST(Cstddef, Byte_Operators) {
std::byte value = std::byte(0);
@@ -96,71 +90,6 @@ TEST(Constinit, ValueIsMutable) {
mutable_value = true;
}
-TEST(TypeTraits, Aliases) {
- static_assert(
- std::is_same<std::aligned_storage_t<40, 40>,
- typename std::aligned_storage<40, 40>::type>::value,
- "Alias must be defined");
-
- static_assert(std::is_same<std::common_type_t<int, bool>,
- typename std::common_type<int, bool>::type>::value,
- "Alias must be defined");
-
- static_assert(
- std::is_same<std::conditional_t<false, int, char>,
- typename std::conditional<false, int, char>::type>::value,
- "Alias must be defined");
-
- static_assert(
- std::is_same<std::decay_t<int>, typename std::decay<int>::type>::value,
- "Alias must be defined");
-
- static_assert(std::is_same<std::enable_if_t<true, int>,
- typename std::enable_if<true, int>::type>::value,
- "Alias must be defined");
-
- static_assert(std::is_same<std::make_signed_t<int>,
- typename std::make_signed<int>::type>::value,
- "Alias must be defined");
-
- static_assert(std::is_same<std::make_unsigned_t<int>,
- typename std::make_unsigned<int>::type>::value,
- "Alias must be defined");
-
- static_assert(std::is_same<std::remove_cv_t<int>,
- typename std::remove_cv<int>::type>::value,
- "Alias must be defined");
-
- static_assert(std::is_same<std::remove_pointer_t<int>,
- typename std::remove_pointer<int>::type>::value,
- "Alias must be defined");
-
- static_assert(std::is_same<std::remove_reference_t<int>,
- typename std::remove_reference<int>::type>::value,
- "Alias must be defined");
-}
-
-TEST(TypeTraits, LogicalTraits) {
- static_assert(std::conjunction<std::true_type, std::true_type>::value,
- "conjunction should be true");
- static_assert(!std::conjunction<std::true_type, std::false_type>::value,
- "conjunction should be false");
- static_assert(!std::conjunction<std::false_type, std::false_type>::value,
- "conjunction should be false");
-
- static_assert(std::disjunction<std::true_type, std::true_type>::value,
- "disjunction should be true");
- static_assert(std::disjunction<std::true_type, std::false_type>::value,
- "disjunction should be true");
- static_assert(!std::disjunction<std::false_type, std::false_type>::value,
- "disjunction should be false");
-
- static_assert(!std::negation<std::true_type>::value,
- "negation should be false");
- static_assert(std::negation<std::false_type>::value,
- "negation should be true");
-}
-
} // namespace
} // namespace polyfill
} // namespace pw
diff --git a/pw_preprocessor/Android.bp b/pw_preprocessor/Android.bp
new file mode 100644
index 000000000..a7503955a
--- /dev/null
+++ b/pw_preprocessor/Android.bp
@@ -0,0 +1,24 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_preprocessor_headers",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ host_supported: true,
+}
diff --git a/pw_preprocessor/CMakeLists.txt b/pw_preprocessor/CMakeLists.txt
index 1df11f96f..acf4a7c22 100644
--- a/pw_preprocessor/CMakeLists.txt
+++ b/pw_preprocessor/CMakeLists.txt
@@ -14,7 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_preprocessor
+pw_add_library(pw_preprocessor INTERFACE
HEADERS
public/pw_preprocessor/arguments.h
public/pw_preprocessor/boolean.h
@@ -22,11 +22,15 @@ pw_add_module_library(pw_preprocessor
public/pw_preprocessor/concat.h
public/pw_preprocessor/util.h
public/pw_preprocessor/internal/arg_count_impl.h
+ PUBLIC_INCLUDES
+ public
)
-pw_add_module_library(pw_preprocessor.arch
+pw_add_library(pw_preprocessor.arch INTERFACE
HEADERS
public/pw_preprocessor/arch.h
+ PUBLIC_INCLUDES
+ public
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_PREPROCESSOR)
diff --git a/pw_preprocessor/arguments_test.cc b/pw_preprocessor/arguments_test.cc
index 4196298a1..0a7b69ed3 100644
--- a/pw_preprocessor/arguments_test.cc
+++ b/pw_preprocessor/arguments_test.cc
@@ -19,7 +19,7 @@
#include <tuple>
-#include "pw_unit_test/framework.h"
+#include "gtest/gtest.h"
namespace pw {
namespace {
diff --git a/pw_preprocessor/boolean_test.cc b/pw_preprocessor/boolean_test.cc
index 40a00eb8f..decc85dd6 100644
--- a/pw_preprocessor/boolean_test.cc
+++ b/pw_preprocessor/boolean_test.cc
@@ -16,7 +16,7 @@
// passed. The TEST functions are used for organization only.
#include "pw_preprocessor/boolean.h"
-#include "pw_unit_test/framework.h"
+#include "gtest/gtest.h"
namespace pw {
namespace {
diff --git a/pw_preprocessor/concat_test.cc b/pw_preprocessor/concat_test.cc
index a43997f4e..a4be2b2df 100644
--- a/pw_preprocessor/concat_test.cc
+++ b/pw_preprocessor/concat_test.cc
@@ -14,8 +14,8 @@
#include "pw_preprocessor/concat.h"
+#include "gtest/gtest.h"
#include "pw_preprocessor/util.h"
-#include "pw_unit_test/framework.h"
namespace pw {
namespace {
diff --git a/pw_preprocessor/public/pw_preprocessor/arch.h b/pw_preprocessor/public/pw_preprocessor/arch.h
index 45b7ff42e..676a386c2 100644
--- a/pw_preprocessor/public/pw_preprocessor/arch.h
+++ b/pw_preprocessor/public/pw_preprocessor/arch.h
@@ -13,10 +13,10 @@
// the License.
#pragma once
-// TODO(pwbug/594): arch.h should be refactored out of pw_preprocessor as the
+// TODO(b/234887943): arch.h should be refactored out of pw_preprocessor as the
// scope is outside of the module. The intended scope of arch.h is only to
// provide architecture targeting and not any added utilities and capabilities.
-// Perhaps it should be placed under pw_compiler along with pwbug/593, e.g.
+// Perhaps it should be placed under pw_compiler along with b/234877280, e.g.
// pw_compiler/arch.h?
// Regardless, the arch defines should likely move to a trait system in Pigweed
// before making them public defines for others to use.
diff --git a/pw_preprocessor/public/pw_preprocessor/compiler.h b/pw_preprocessor/public/pw_preprocessor/compiler.h
index 690a061b9..44c2fa9f1 100644
--- a/pw_preprocessor/public/pw_preprocessor/compiler.h
+++ b/pw_preprocessor/public/pw_preprocessor/compiler.h
@@ -18,7 +18,7 @@
#include <assert.h>
-// TODO(pwbug/593): compiler.h should be refactored out of pw_preprocessor as
+// TODO(b/234877280): compiler.h should be refactored out of pw_preprocessor as
// the scope is outside of the module. Perhaps it should be split up and placed
// under pw_compiler, e.g. pw_compiler/attributes.h & pw_compiler/builtins.h.
diff --git a/pw_preprocessor/util_test.cc b/pw_preprocessor/util_test.cc
index 073b4cee7..855245286 100644
--- a/pw_preprocessor/util_test.cc
+++ b/pw_preprocessor/util_test.cc
@@ -16,7 +16,7 @@
#include <cstdint>
-#include "pw_unit_test/framework.h"
+#include "gtest/gtest.h"
namespace pw {
namespace {
diff --git a/pw_presubmit/BUILD.gn b/pw_presubmit/BUILD.gn
index 35fa9ffe2..8bda35a29 100644
--- a/pw_presubmit/BUILD.gn
+++ b/pw_presubmit/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
@@ -24,3 +25,6 @@ pw_doc_group("docs") {
"py/pw_presubmit/presubmit.py",
]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_presubmit/docs.rst b/pw_presubmit/docs.rst
index 899f29e98..5be527bc9 100644
--- a/pw_presubmit/docs.rst
+++ b/pw_presubmit/docs.rst
@@ -103,6 +103,80 @@ such as a quick program for local use and a full program for automated use. The
:ref:`example script <example-script>` uses ``pw_presubmit.Programs`` to define
``quick`` and ``full`` programs.
+``PresubmitContext`` has the following members:
+
+* ``root``: Source checkout root directory
+* ``repos``: Repositories (top-level and submodules) processed by
+ ``pw presubmit``
+* ``output_dir``: Output directory for this specific presubmit step
+* ``failure_summary_log``: File path where steps should write a brief summary
+ of any failures
+* ``paths``: Modified files for the presubmit step to check (often used in
+ formatting steps but ignored in compile steps)
+* ``all_paths``: All files in the repository tree.
+* ``package_root``: Root directory for ``pw package`` installations
+* ``override_gn_args``: Additional GN args processed by ``build.gn_gen()``
+* ``luci``: Information about the LUCI build or None if not running in LUCI
+* ``num_jobs``: Number of jobs to run in parallel
+* ``continue_after_build_error``: For steps that compile, don't exit on the
+ first compilation error
+
+The ``luci`` member is of type ``LuciContext`` and has the following members:
+
+* ``buildbucket_id``: The globally-unique buildbucket id of the build
+* ``build_number``: The builder-specific incrementing build number, if
+ configured for this builder
+* ``project``: The LUCI project under which this build is running (often
+ ``pigweed`` or ``pigweed-internal``)
+* ``bucket``: The LUCI bucket under which this build is running (often ends
+ with ``ci`` or ``try``)
+* ``builder``: The builder being run
+* ``swarming_server``: The swarming server on which this build is running
+* ``swarming_task_id``: The swarming task id of this build
+* ``cas_instance``: The CAS instance accessible from this build
+* ``pipeline``: Information about the build pipeline, if applicable.
+* ``triggers``: Information about triggering commits, if applicable.
+
+The ``pipeline`` member, if present, is of type ``LuciPipeline`` and has the
+following members:
+
+* ``round``: The zero-indexed round number.
+* ``builds_from_previous_iteration``: A list of the buildbucket ids from the
+ previous round, if any, encoded as strs.
+
+The ``triggers`` member is a sequence of ``LuciTrigger`` objects, which have the
+following members:
+
+* ``number``: The number of the change in Gerrit.
+* ``patchset``: The number of the patchset of the change.
+* ``remote``: The full URL of the remote.
+* ``branch``: The name of the branch on which this change is being/was
+ submitted.
+* ``ref``: The ``refs/changes/..`` path that can be used to reference the
+ patch for unsubmitted changes and the hash for submitted changes.
+* ``gerrit_name``: The name of the googlesource.com Gerrit host.
+* ``submitted``: Whether the change has been submitted or is still pending.
+
+Additional members can be added by subclassing ``PresubmitContext`` and
+``Presubmit``. Then override ``Presubmit._create_presubmit_context()`` to
+return the subclass of ``PresubmitContext``. Finally, add
+``presubmit_class=PresubmitSubClass`` when calling ``cli.run()``.
+
+Substeps
+--------
+Presubmit steps can define substeps that can run independently in other tooling.
+These steps should subclass ``SubStepCheck`` and must define a ``substeps()``
+method that yields ``SubStep`` objects. ``SubStep`` objects have the following
+members:
+
+* ``name``: Name of the substep
+* ``_func``: Substep code
+* ``args``: Positional arguments for ``_func``
+* ``kwargs``: Keyword arguments for ``_func``
+
+``SubStep`` objects must have unique names. For a detailed example of a
+``SubStepCheck`` subclass see ``GnGenNinja`` in ``build.py``.
+
Existing Presubmit Checks
-------------------------
A small number of presubmit checks are made available through ``pw_presubmit``
@@ -114,15 +188,152 @@ Formatting checks for a variety of languages are available from
``pw_presubmit.format_code``. These include C/C++, Java, Go, Python, GN, and
others. All of these checks can be included by adding
``pw_presubmit.format_code.presubmit_checks()`` to a presubmit program. These
-all use language-specific formatters like clang-format or yapf.
+all use language-specific formatters like clang-format or black.
These will suggest fixes using ``pw format --fix``.
+Options for code formatting can be specified in the ``pigweed.json`` file
+(see also :ref:`SEED-0101 <seed-0101>`). These apply to both ``pw presubmit``
+steps that check code formatting and ``pw format`` commands that either check
+or fix code formatting.
+
+* ``python_formatter``: Choice of Python formatter. Options are ``black`` (used
+ by Pigweed itself) and ``yapf`` (the default).
+* ``black_path``: If ``python_formatter`` is ``black``, use this as the
+ executable instead of ``black``.
+
+.. TODO(b/264578594) Add exclude to pigweed.json file.
+.. * ``exclude``: List of path regular expressions to ignore.
+
+Example section from a ``pigweed.json`` file:
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_presubmit": {
+ "format": {
+ "python_formatter": "black",
+ "black_path": "black"
+ }
+ }
+ }
+ }
+
+Sorted Blocks
+^^^^^^^^^^^^^
+Blocks of code can be required to be kept in sorted order using comments like
+the following:
+
+.. code-block::
+
+ # keep-sorted: start
+ bar
+ baz
+ foo
+ # keep-sorted: end
+
+This can be included by adding ``pw_presubmit.keep_sorted.presubmit_check`` to a
+presubmit program. Adding ``ignore-case`` to the start line will use
+case-insensitive sorting.
+
+By default, duplicates will be removed. Lines that are identical except in case
+are preserved, even with ``ignore-case``. To allow duplicates, add
+``allow-dupes`` to the start line.
+
+Prefixes can be ignored by adding ``ignore-prefix=`` followed by a
+comma-separated list of prefixes. The list below will be kept in this order.
+Neither commas nor whitespace are supported in prefixes.
+
+.. code-block::
+
+ # keep-sorted: start ignore-prefix=',"
+ 'bar',
+ "baz",
+ 'foo',
+ # keep-sorted: end
+
+Inline comments are assumed to be associated with the following line. For
+example, the following is already sorted. This can be disabled with
+``sticky-comments=no``.
+
+.. todo-check: disable
+
+.. code-block::
+
+ # keep-sorted: start
+ # TODO(b/1234) Fix this.
+ bar,
+ # TODO(b/5678) Also fix this.
+ foo,
+ # keep-sorted: end
+
+.. todo-check: enable
+
+By default, the prefix of the keep-sorted line is assumed to be the comment
+marker used by any inline comments. This can be overridden by adding lines like
+``sticky-comments=%,#`` to the start line.
+
+Lines indented more than the preceding line are assumed to be continuations.
+Thus, the following block is already sorted. keep-sorted blocks can not be
+nested, so there's no ability to add a keep-sorted block for the sub-items.
+
+.. code-block::
+
+ # keep-sorted: start
+ * abc
+ * xyz
+ * uvw
+ * def
+ # keep-sorted: end
+
+The presubmit check will suggest fixes using ``pw keep-sorted --fix``.
+
+Future versions may support additional multiline list items.
+
+.gitmodules
+^^^^^^^^^^^
+Various rules can be applied to .gitmodules files. This check can be included
+by adding ``pw_presubmit.gitmodules.create()`` to a presubmit program. This
+function takes an optional argument of type ``pw_presubmit.gitmodules.Config``.
+``Config`` objects have several properties.
+
+* ``allow_non_googlesource_hosts: bool = False`` — If false, all submodules URLs
+ must be on a Google-managed Gerrit server.
+* ``allowed_googlesource_hosts: Sequence[str] = ()`` — If set, any
+ Google-managed Gerrit URLs for submodules most be in this list. Entries
+ should be like ``pigweed`` for ``pigweed-review.googlesource.com``.
+* ``require_relative_urls: bool = False`` — If true, all submodules must be
+ relative to the superproject remote.
+* ``allow_sso: bool = True`` — If false, ``sso://`` and ``rpc://`` submodule
+ URLs are prohibited.
+* ``allow_git_corp_google_com: bool = True`` — If false, ``git.corp.google.com``
+ submodule URLs are prohibited.
+* ``require_branch: bool = False`` — If True, all submodules must reference a
+ branch.
+* ``validator: Callable[[PresubmitContext, Path, str, Dict[str, str]], None] = None``
+ — A function that can be used for arbitrary submodule validation. It's called
+ with the ``PresubmitContext``, the path to the ``.gitmodules`` file, the name
+ of the current submodule, and the properties of the current submodule.
+
#pragma once
^^^^^^^^^^^^
There's a ``pragma_once`` check that confirms the first non-comment line of
C/C++ headers is ``#pragma once``. This is enabled by adding
-``pw_presubmit.pragma_once`` to a presubmit program.
+``pw_presubmit.cpp_checks.pragma_once`` to a presubmit program.
+
+.. todo-check: disable
+
+TODO(b/###) Formatting
+^^^^^^^^^^^^^^^^^^^^^^^^^
+There's a check that confirms ``TODO`` lines match a given format. Upstream
+Pigweed expects these to look like ``TODO(b/###): Explanation``, but makes it
+easy for projects to define their own pattern instead.
+
+To use this check add ``todo_check.create(todo_check.BUGS_OR_USERNAMES)`` to a
+presubmit program.
+
+.. todo-check: enable
Python Checks
^^^^^^^^^^^^^
@@ -149,13 +360,49 @@ for entire blocks by using "inclusive-language: disable" before the block and
.. In case things get moved around in the previous paragraphs the enable line
.. is repeated here: inclusive-language: enable.
+OWNERS
+^^^^^^
+There's a check that requires folders matching specific patterns contain
+``OWNERS`` files. It can be included by adding
+``module_owners.presubmit_check()`` to a presubmit program. This function takes
+a callable as an argument that indicates, for a given file, where a controlling
+``OWNERS`` file should be, or returns None if no ``OWNERS`` file is necessary.
+Formatting of ``OWNERS`` files is handled similary to formatting of other
+source files and is discussed in `Code Formatting`.
+
+Source in Build
+^^^^^^^^^^^^^^^
+Pigweed provides checks that source files are configured as part of the build
+for GN, Bazel, and CMake. These can be included by adding
+``source_in_build.gn(filter)`` and similar functions to a presubmit check. The
+CMake check additionally requires a callable that invokes CMake with appropriate
+options.
+
pw_presubmit
------------
.. automodule:: pw_presubmit
- :members: filter_paths, call, PresubmitFailure, Programs
+ :members: filter_paths, FileFilter, call, PresubmitFailure, Programs
.. _example-script:
+
+Git hook
+--------
+You can run a presubmit program or step as a `git hook
+<https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks>`_ using
+``pw_presubmit.install_hook``. This can be used to run certain presubmit
+checks before a change is pushed to a remote.
+
+We strongly recommend that you only run fast (< 15 seconds) and trivial checks
+as push hooks, and perform slower or more complex ones in CI. This is because,
+
+* Running slow checks in the push hook will force you to wait longer for
+ ``git push`` to complete, and
+* If your change fails one of the checks at this stage, it will not yet be
+ uploaded to the remote, so you'll have a harder time debugging any failures
+ (sharing the change with your colleagues, linking to it from an issue
+ tracker, etc).
+
Example
=======
A simple example presubmit check script follows. This can be copied-and-pasted
@@ -173,7 +420,7 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example.
from pathlib import Path
import re
import sys
- from typing import List, Pattern
+ from typing import List, Optional, Pattern
try:
import pw_cli.log
@@ -210,13 +457,13 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example.
# Presubmit checks
#
def release_build(ctx: PresubmitContext):
- build.gn_gen(PROJECT_ROOT, ctx.output_dir, build_type='release')
- build.ninja(ctx.output_dir)
+ build.gn_gen(ctx, build_type='release')
+ build.ninja(ctx)
def host_tests(ctx: PresubmitContext):
- build.gn_gen(PROJECT_ROOT, ctx.output_dir, run_host_tests='true')
- build.ninja(ctx.output_dir)
+ build.gn_gen(ctx, run_host_tests='true')
+ build.ninja(ctx)
# Avoid running some checks on certain paths.
@@ -249,7 +496,7 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example.
# Use the upstream formatting checks, with custom path filters applied.
format_code.presubmit_checks(exclude=PATH_EXCLUSIONS),
# Include the upstream inclusive language check.
- inclusive_language.inclusive_language,
+ inclusive_language.presubmit_check,
# Include just the lint-related Python checks.
python_checks.gn_pylint.with_filter(exclude=PATH_EXCLUSIONS),
)
@@ -265,16 +512,35 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example.
PROGRAMS = pw_presubmit.Programs(other=OTHER, quick=QUICK, full=FULL)
- def run(install: bool, **presubmit_args) -> int:
+ #
+ # Allowlist of remote refs for presubmit. If the remote ref being pushed to
+ # matches any of these values (with regex matching), then the presubmits
+ # checks will be run before pushing.
+ #
+ PRE_PUSH_REMOTE_REF_ALLOWLIST = (
+ 'refs/for/main',
+ )
+
+
+ def run(install: bool, remote_ref: Optional[str], **presubmit_args) -> int:
"""Process the --install argument then invoke pw_presubmit."""
# Install the presubmit Git pre-push hook, if requested.
if install:
- install_hook(__file__, 'pre-push', ['--base', 'HEAD~'],
- git_repo.root())
+ # '$remote_ref' will be replaced by the actual value of the remote ref
+ # at runtime.
+ install_git_hook('pre-push', [
+ 'python', '-m', 'tools.presubmit_check', '--base', 'HEAD~',
+ '--remote-ref', '$remote_ref'
+ ])
return 0
- return cli.run(root=PROJECT_ROOT, **presubmit_args)
+ # Run the checks if either no remote_ref was passed, or if the remote ref
+ # matches anything in the allowlist.
+ if remote_ref is None or any(
+ re.search(pattern, remote_ref)
+ for pattern in PRE_PUSH_REMOTE_REF_ALLOWLIST):
+ return cli.run(root=PROJECT_ROOT, **presubmit_args)
def main() -> int:
@@ -288,6 +554,16 @@ See ``pigweed_presubmit.py`` for a more complex presubmit check script example.
action='store_true',
help='Install the presubmit as a Git pre-push hook and exit.')
+ # Define an optional flag to pass the remote ref into this script, if it
+ # is run as a pre-push hook. The destination variable in the parsed args
+ # will be `remote_ref`, as dashes are replaced with underscores to make
+ # valid variable names.
+ parser.add_argument(
+ '--remote-ref',
+ default=None,
+ nargs='?', # Make optional.
+ help='Remote ref of the push command, for use by the pre-push hook.')
+
return run(**vars(parser.parse_args()))
if __name__ == '__main__':
@@ -300,3 +576,44 @@ Code formatting tools
The ``pw_presubmit.format_code`` module formats supported source files using
external code format tools. The file ``format_code.py`` can be invoked directly
from the command line or from ``pw`` as ``pw format``.
+
+Example
+=======
+A simple example of adding support for a custom format. This code wraps the
+built in formatter to add a new format. It could also be used to replace
+a formatter or remove/disable a PigWeed supplied one.
+
+.. code-block:: python
+
+ #!/usr/bin/env python
+ """Formats files in repository. """
+
+ import logging
+ import sys
+
+ import pw_cli.log
+ from pw_presubmit import format_code
+ from your_project import presubmit_checks
+ from your_project import your_check
+
+ YOUR_CODE_FORMAT = CodeFormat('YourFormat',
+ filter=FileFilter(suffix=('.your', )),
+ check=your_check.check,
+ fix=your_check.fix)
+
+ CODE_FORMATS = (*format_code.CODE_FORMATS, YOUR_CODE_FORMAT)
+
+ def _run(exclude, **kwargs) -> int:
+ """Check and fix formatting for source files in the repo."""
+ return format_code.format_paths_in_repo(exclude=exclude,
+ code_formats=CODE_FORMATS,
+ **kwargs)
+
+
+ def main():
+ return _run(**vars(format_code.arguments(git_paths=True).parse_args()))
+
+
+ if __name__ == '__main__':
+ pw_cli.log.install(logging.INFO)
+ sys.exit(main())
diff --git a/pw_presubmit/py/BUILD.gn b/pw_presubmit/py/BUILD.gn
index 0fa08fcd6..0c29c5152 100644
--- a/pw_presubmit/py/BUILD.gn
+++ b/pw_presubmit/py/BUILD.gn
@@ -24,26 +24,45 @@ pw_python_package("py") {
]
sources = [
"pw_presubmit/__init__.py",
+ "pw_presubmit/bazel_parser.py",
"pw_presubmit/build.py",
"pw_presubmit/cli.py",
"pw_presubmit/cpp_checks.py",
"pw_presubmit/format_code.py",
"pw_presubmit/git_repo.py",
+ "pw_presubmit/gitmodules.py",
"pw_presubmit/inclusive_language.py",
"pw_presubmit/install_hook.py",
+ "pw_presubmit/keep_sorted.py",
+ "pw_presubmit/module_owners.py",
+ "pw_presubmit/ninja_parser.py",
+ "pw_presubmit/npm_presubmit.py",
+ "pw_presubmit/owners_checks.py",
"pw_presubmit/pigweed_presubmit.py",
"pw_presubmit/presubmit.py",
"pw_presubmit/python_checks.py",
+ "pw_presubmit/shell_checks.py",
+ "pw_presubmit/source_in_build.py",
+ "pw_presubmit/todo_check.py",
"pw_presubmit/tools.py",
]
tests = [
+ "bazel_parser_test.py",
+ "git_repo_test.py",
+ "gitmodules_test.py",
+ "keep_sorted_test.py",
+ "ninja_parser_test.py",
"presubmit_test.py",
+ "owners_checks_test.py",
+ "todo_check_test.py",
"tools_test.py",
]
+
python_deps = [
- "$dir_pw_build:python_lint",
"$dir_pw_cli/py",
+ "$dir_pw_env_setup/py",
"$dir_pw_package/py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_presubmit/py/bazel_parser_test.py b/pw_presubmit/py/bazel_parser_test.py
new file mode 100644
index 000000000..4c7d45e10
--- /dev/null
+++ b/pw_presubmit/py/bazel_parser_test.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for bazel_parser."""
+
+from pathlib import Path
+import tempfile
+import unittest
+
+from pw_presubmit import bazel_parser
+
+# This is a real Bazel failure, trimmed slightly.
+_REAL_TEST_INPUT = """
+Starting local Bazel server and connecting to it...
+WARNING: --verbose_explanations has no effect when --explain=<file> is not enabled
+Loading:
+Loading: 0 packages loaded
+Analyzing: 1362 targets (197 packages loaded)
+Analyzing: 1362 targets (197 packages loaded, 0 targets configured)
+INFO: Analyzed 1362 targets (304 packages loaded, 15546 targets configured).
+
+INFO: Found 1362 targets...
+[6 / 124] [Prepa] BazelWorkspaceStatusAction stable-status.txt
+[747 / 1,548] Compiling pw_kvs/entry.cc; 0s linux-sandbox ... (3 actions, ...
+ERROR: /usr/local/google/home/mohrr/pigweed/pigweed/pw_kvs/BUILD.bazel:25:14: Compiling pw_kvs/entry.cc failed: (Exit 1): gcc failed: error executing command
+ (cd /usr/local/google/home/mohrr/.cache/bazel/_bazel_mohrr/7e133e1f95b61... \
+ exec env - \
+ CPP_TOOL_PATH=external/clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_04/... \
+ GCC_TOOL_PATH=external/clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_04/... \
+ LD_TOOL_PATH=external/clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_04/... \
+ NM_TOOL_PATH=external/clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_04/... \
+ OBJDUMP_TOOL_PATH=external/clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_... \
+ PATH=/usr/local/google/home/mohrr/pigweed/pigweed/out/host/host_tools:... \
+ PWD=/proc/self/cwd \
+ external/rules_cc_toolchain/cc_toolchain/wrappers/posix/gcc -MD -MF bazel-out/k8-fastbuild/bin/pw_kvs/_objs/pw_kvs/entry.pic.d '-frandom-seed=bazel-out/k8-fastbuild/bin/pw_kvs/_objs/pw_kvs/entry.pic.o' -fPIC -iquote . -iquote bazel-out/k8-fastbuild/bin -isystem pw_kvs/public -isystem bazel-out/k8-fastbuild/bin/pw_kvs/public -isystem pw_assert/assert_compatibility_public_overrides -isystem bazel-out/k8-fastbuild/bin/pw_assert/assert_compatibility_public_overrides -isystem pw_assert/public -isystem bazel-out/k8-fastbuild/bin/pw_assert/public -isystem pw_preprocessor/public -isystem bazel-out/k8-fastbuild/bin/pw_preprocessor/public -isystem pw_assert_basic/public -isystem bazel-out/k8-fastbuild/bin/pw_assert_basic/public -isystem pw_assert_basic/public_overrides -isystem bazel-out/k8-fastbuild/bin/pw_assert_basic/public_overrides -isystem pw_string/public -isystem bazel-out/k8-fastbuild/bin/pw_string/public -isystem pw_span/public -isystem bazel-out/k8-fastbuild/bin/pw_span/public -isystem pw_polyfill/public -isystem bazel-out/k8-fastbuild/bin/pw_polyfill/public -isystem pw_status/public -isystem bazel-out/k8-fastbuild/bin/pw_status/public -isystem pw_result/public -isystem bazel-out/k8-fastbuild/bin/pw_result/public -isystem pw_sys_io/public -isystem bazel-out/k8-fastbuild/bin/pw_sys_io/public -isystem pw_bytes/public -isystem bazel-out/k8-fastbuild/bin/pw_bytes/public -isystem pw_containers/public -isystem bazel-out/k8-fastbuild/bin/pw_containers/public -isystem pw_checksum/public -isystem bazel-out/k8-fastbuild/bin/pw_checksum/public -isystem pw_compilation_testing/public -isystem bazel-out/k8-fastbuild/bin/pw_compilation_testing/public -isystem pw_log/public -isystem bazel-out/k8-fastbuild/bin/pw_log/public -isystem pw_log_basic/public -isystem bazel-out/k8-fastbuild/bin/pw_log_basic/public -isystem pw_log_basic/public_overrides -isystem bazel-out/k8-fastbuild/bin/pw_log_basic/public_overrides -isystem pw_stream/public -isystem bazel-out/k8-fastbuild/bin/pw_stream/public -nostdinc++ -nostdinc -isystemexternal/clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_04/include/c++/v1 -isystemexternal/debian_stretch_amd64_sysroot/usr/local/include -isystemexternal/debian_stretch_amd64_sysroot/usr/include/x86_64-linux-gnu -isystemexternal/debian_stretch_amd64_sysroot/usr/include -isystemexternal/clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_04/lib/clang/12.0.0 -isystemexternal/clang_llvm_12_00_x86_64_linux_gnu_ubuntu_16_04/lib/clang/12.0.0/include -fdata-sections -ffunction-sections -no-canonical-prefixes -Wno-builtin-macro-redefined '-D__DATE__="redacted"' '-D__TIMESTAMP__="redacted"' '-D__TIME__="redacted"' -xc++ --sysroot external/debian_stretch_amd64_sysroot -O0 -fPIC -g -fno-common -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra -Werror '-Wno-error=cpp' '-Wno-error=deprecated-declarations' -Wno-private-header '-std=c++17' -fno-rtti -Wnon-virtual-dtor -Wno-register -c pw_kvs/entry.cc -o bazel-out/k8-fastbuild/bin/pw_kvs/_objs/pw_kvs/entry.pic.o)
+# Configuration: 752863e407a197a5b9da05cfc572e7013efd6958e856cee61d2fa474ed...
+# Execution platform: @local_config_platform//:host
+
+Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging
+pw_kvs/entry.cc:49:20: error: no member named 'Dat' in 'pw::Status'
+ return Status::Dat aLoss();
+ ~~~~~~~~^
+pw_kvs/entry.cc:49:23: error: expected ';' after return statement
+ return Status::Dat aLoss();
+ ^
+ ;
+2 errors generated.
+INFO: Elapsed time: 5.662s, Critical Path: 1.01s
+INFO: 12 processes: 12 internal.
+FAILED: Build did NOT complete successfully
+FAILED: Build did NOT complete successfully
+"""
+
+_REAL_TEST_SUMMARY = """
+ERROR: /usr/local/google/home/mohrr/pigweed/pigweed/pw_kvs/BUILD.bazel:25:14: Compiling pw_kvs/entry.cc failed: (Exit 1): gcc failed: error executing command
+# Execution platform: @local_config_platform//:host
+pw_kvs/entry.cc:49:20: error: no member named 'Dat' in 'pw::Status'
+ return Status::Dat aLoss();
+ ~~~~~~~~^
+pw_kvs/entry.cc:49:23: error: expected ';' after return statement
+ return Status::Dat aLoss();
+ ^
+ ;
+2 errors generated.
+"""
+
+_STOP = 'INFO:\n'
+
+# pylint: disable=attribute-defined-outside-init
+
+
+class TestBazelParser(unittest.TestCase):
+ """Test bazel_parser."""
+
+ def _run(self, contents: str) -> None:
+ with tempfile.TemporaryDirectory() as tempdir:
+ path = Path(tempdir) / 'foo'
+
+ with path.open('w') as outs:
+ outs.write(contents)
+
+ self.output = bazel_parser.parse_bazel_stdout(path)
+
+ def test_simple(self) -> None:
+ error = 'ERROR: abc\nerror 1\nerror2\n'
+ self._run('[0/10] foo\n[1/10] bar\n' + error + _STOP)
+ self.assertEqual(error.strip(), self.output.strip())
+
+ def test_path(self) -> None:
+ error_in = 'ERROR: abc\n PATH=... \\\nerror 1\nerror2\n'
+ error_out = 'ERROR: abc\nerror 1\nerror2\n'
+ self._run('[0/10] foo\n[1/10] bar\n' + error_in + _STOP)
+ self.assertEqual(error_out.strip(), self.output.strip())
+
+ def test_unterminated(self) -> None:
+ error = 'ERROR: abc\nerror 1\nerror 2\n'
+ self._run('[0/10] foo\n[1/10] bar\n' + error)
+ self.assertEqual(error.strip(), self.output.strip())
+
+ def test_failure(self) -> None:
+ self._run(_REAL_TEST_INPUT)
+ self.assertEqual(_REAL_TEST_SUMMARY.strip(), self.output.strip())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_presubmit/py/git_repo_test.py b/pw_presubmit/py/git_repo_test.py
new file mode 100755
index 000000000..225701a05
--- /dev/null
+++ b/pw_presubmit/py/git_repo_test.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""git repo module tests"""
+
+from unittest import mock
+import re
+import pathlib
+import unittest
+
+from pw_presubmit import git_repo
+
+
+class TestGitRepo(unittest.TestCase):
+ """Tests for git_repo.py"""
+
+ GIT_ROOT = pathlib.Path("/dev/null/test")
+ SUBMODULES = [
+ pathlib.Path("third_party/pigweed"),
+ pathlib.Path("vendor/anycom/p1"),
+ pathlib.Path("vendor/anycom/p2"),
+ ]
+ GIT_SUBMODULES_OUT = "\n".join([str(x) for x in SUBMODULES])
+
+ def setUp(self) -> None:
+ self.git_stdout = mock.patch.object(
+ git_repo, "git_stdout", autospec=True
+ ).start()
+ self.git_stdout.return_value = self.GIT_SUBMODULES_OUT
+ self.root = mock.patch.object(git_repo, "root", autospec=True).start()
+ self.root.return_value = self.GIT_ROOT
+ super().setUp()
+
+ def tearDown(self) -> None:
+ mock.patch.stopall()
+ super().tearDown()
+
+ def test_mock_root(self):
+ """Ensure our mock works since so many of our tests depend upon it."""
+ self.assertEqual(git_repo.root(), self.GIT_ROOT)
+
+ def test_discover_submodules_1(self):
+ paths = git_repo.discover_submodules(superproject_dir=self.GIT_ROOT)
+ self.assertIn(self.GIT_ROOT, paths)
+
+ def test_discover_submodules_2(self):
+ paths = git_repo.discover_submodules(superproject_dir=self.GIT_ROOT)
+ self.assertIn(self.SUBMODULES[2], paths)
+
+ def test_discover_submodules_with_exclude_str(self):
+ paths = git_repo.discover_submodules(
+ superproject_dir=self.GIT_ROOT,
+ excluded_paths=(self.GIT_ROOT.as_posix(),),
+ )
+ self.assertNotIn(self.GIT_ROOT, paths)
+
+ def test_discover_submodules_with_exclude_regex(self):
+ paths = git_repo.discover_submodules(
+ superproject_dir=self.GIT_ROOT,
+ excluded_paths=(re.compile("third_party/.*"),),
+ )
+ self.assertNotIn(self.SUBMODULES[0], paths)
+
+ def test_discover_submodules_with_exclude_str_miss(self):
+ paths = git_repo.discover_submodules(
+ superproject_dir=self.GIT_ROOT,
+ excluded_paths=(re.compile("pigweed"),),
+ )
+ self.assertIn(self.SUBMODULES[-1], paths)
+
+ def test_discover_submodules_with_exclude_regex_miss_1(self):
+ paths = git_repo.discover_submodules(
+ superproject_dir=self.GIT_ROOT,
+ excluded_paths=(re.compile("foo/.*"),),
+ )
+ self.assertIn(self.GIT_ROOT, paths)
+ for module in self.SUBMODULES:
+ self.assertIn(module, paths)
+
+ def test_discover_submodules_with_exclude_regex_miss_2(self):
+ paths = git_repo.discover_submodules(
+ superproject_dir=self.GIT_ROOT,
+ excluded_paths=(re.compile("pigweed"),),
+ )
+ self.assertIn(self.GIT_ROOT, paths)
+ for module in self.SUBMODULES:
+ self.assertIn(module, paths)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_presubmit/py/gitmodules_test.py b/pw_presubmit/py/gitmodules_test.py
new file mode 100644
index 000000000..df7854546
--- /dev/null
+++ b/pw_presubmit/py/gitmodules_test.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for gitmodules."""
+
+from pathlib import Path
+import tempfile
+from typing import Optional
+import unittest
+from unittest.mock import MagicMock
+
+from pw_presubmit import gitmodules, PresubmitFailure
+
+
+def dotgitmodules(
+ name: str = 'foo',
+ url: Optional[str] = None,
+ host: Optional[str] = None,
+ branch: Optional[str] = 'main',
+):
+ cfg = f'[submodule "{name}"]\n'
+ cfg += f'path = {name}\n'
+ if url is None and host is None:
+ host = 'host'
+ if host:
+ cfg += f'url = https://{host}.googlesource.com/{name}\n'
+ else:
+ assert url
+ cfg += f'url = {url}\n'
+ if branch:
+ cfg += f'branch = {branch}\n'
+ return cfg
+
+
+class TestGitmodules(unittest.TestCase):
+ """Test gitmodules check."""
+
+ def setUp(self):
+ self.ctx: MagicMock = None
+
+ def _run(self, config: gitmodules.Config, contents: str) -> None:
+ self.ctx = MagicMock()
+ self.ctx.fail = MagicMock()
+
+ with tempfile.TemporaryDirectory() as tempdir:
+ path = Path(tempdir) / '.gitmodules'
+ with path.open('w') as outs:
+ outs.write(contents)
+
+ gitmodules.process_gitmodules(self.ctx, config, path)
+
+ def test_ok_default(self) -> None:
+ self._run(gitmodules.Config(), dotgitmodules(url='../foo'))
+ self.ctx.fail.assert_not_called()
+
+ def test_ok_restrictive(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(
+ allow_non_googlesource_hosts=False,
+ allowed_googlesource_hosts=('host',),
+ require_relative_urls=True,
+ allow_sso=False,
+ allow_git_corp_google_com=False,
+ require_branch=True,
+ )
+ self._run(cfg, dotgitmodules(url='../foo'))
+ self.ctx.fail.assert_not_called()
+
+ def test_validate_ok(self) -> None:
+ def validator(ctx, path, name, props) -> None:
+ _ = name
+ if 'bad' in props['url']:
+ ctx.fail('bad', path)
+
+ cfg: gitmodules.Config = gitmodules.Config(validator=validator)
+ self._run(cfg, dotgitmodules(host='host'))
+ self.ctx.fail.assert_not_called()
+
+ def test_validate_fail(self) -> None:
+ def validator(ctx, path, name, props) -> None:
+ _ = name
+ if 'bad' in props['url']:
+ ctx.fail('bad', path)
+
+ cfg: gitmodules.Config = gitmodules.Config(validator=validator)
+ self._run(cfg, dotgitmodules(host='badhost'))
+ self.ctx.fail.assert_called()
+
+ def test_non_google_ok(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(
+ allow_non_googlesource_hosts=True
+ )
+ self._run(cfg, dotgitmodules(url='https://github.com/foo/bar'))
+ self.ctx.fail.assert_not_called()
+
+ def test_non_google_fail(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(
+ allow_non_googlesource_hosts=False
+ )
+ self._run(cfg, dotgitmodules(url='https://github.com/foo/bar'))
+ self.ctx.fail.assert_called()
+
+ def test_bad_allowed_googlesource_hosts(self) -> None:
+ with self.assertRaises(PresubmitFailure):
+ cfg: gitmodules.Config = gitmodules.Config(
+ allowed_googlesource_hosts=('pigweed-review',)
+ )
+ self._run(cfg, dotgitmodules())
+
+ def test_bad_type_allowed_googlesource_hosts(self) -> None:
+ with self.assertRaises(AssertionError):
+ cfg: gitmodules.Config = gitmodules.Config(
+ allowed_googlesource_hosts=('pigweed')
+ )
+ self._run(cfg, dotgitmodules())
+
+ def test_allowed_googlesource_hosts_ok(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(
+ allowed_googlesource_hosts=(
+ 'pigweed',
+ 'pigweed-internal',
+ )
+ )
+ self._run(cfg, dotgitmodules(host='pigweed-internal'))
+ self.ctx.fail.assert_not_called()
+
+ def test_allowed_googlesource_hosts_fail(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(
+ allowed_googlesource_hosts=('pigweed-internal',)
+ )
+ self._run(cfg, dotgitmodules(host='pigweed'))
+ self.ctx.fail.assert_called()
+
+ def test_require_relative_urls_ok(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(require_relative_urls=False)
+ self._run(cfg, dotgitmodules(host='foo'))
+ self.ctx.fail.assert_not_called()
+
+ def test_require_relative_urls_fail(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(require_relative_urls=True)
+ self._run(cfg, dotgitmodules(host='foo'))
+ self.ctx.fail.assert_called()
+
+ def test_allow_sso_ok(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(allow_sso=True)
+ self._run(cfg, dotgitmodules(url='sso://host/foo'))
+ self.ctx.fail.assert_not_called()
+
+ def test_allow_sso_fail(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(allow_sso=False)
+ self._run(cfg, dotgitmodules(url='sso://host/foo'))
+ self.ctx.fail.assert_called()
+
+ def test_allow_git_corp_google_com_ok(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(
+ allow_git_corp_google_com=True
+ )
+ self._run(cfg, dotgitmodules(url='https://foo.git.corp.google.com/bar'))
+ self.ctx.fail.assert_not_called()
+
+ def test_allow_git_corp_google_com_fail(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(
+ allow_git_corp_google_com=False
+ )
+ self._run(cfg, dotgitmodules(url='https://foo.git.corp.google.com/bar'))
+ self.ctx.fail.assert_called()
+
+ def test_require_branch_ok(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(require_branch=False)
+ self._run(cfg, dotgitmodules(branch=None))
+ self.ctx.fail.assert_not_called()
+
+ def test_require_branch_fail(self) -> None:
+ cfg: gitmodules.Config = gitmodules.Config(require_branch=True)
+ self._run(cfg, dotgitmodules(branch=None))
+ self.ctx.fail.assert_called()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_presubmit/py/keep_sorted_test.py b/pw_presubmit/py/keep_sorted_test.py
new file mode 100644
index 000000000..8d1742a7e
--- /dev/null
+++ b/pw_presubmit/py/keep_sorted_test.py
@@ -0,0 +1,357 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for keep_sorted."""
+
+from pathlib import Path
+import tempfile
+import textwrap
+from typing import Dict, Sequence
+import unittest
+from unittest.mock import MagicMock
+
+from pw_presubmit import keep_sorted
+
+# Only include these literals here so keep_sorted doesn't try to reorder later
+# test lines.
+START = keep_sorted.START
+END = keep_sorted.END
+
+# pylint: disable=attribute-defined-outside-init
+# pylint: disable=too-many-public-methods
+
+
+class TestKeepSorted(unittest.TestCase):
+ """Test KeepSorted class"""
+
+ def _run(self, contents: str) -> None:
+ self.ctx = MagicMock()
+ self.ctx.fail = MagicMock()
+
+ with tempfile.TemporaryDirectory() as tempdir:
+ path = Path(tempdir) / 'foo'
+
+ with path.open('w') as outs:
+ outs.write(contents)
+
+ self.errors: Dict[Path, Sequence[str]] = {}
+
+ # pylint: disable=protected-access
+ self.sorter = keep_sorted._FileSorter(self.ctx, path, self.errors)
+
+ # pylint: enable=protected-access
+
+ self.sorter.sort()
+
+ # Truncate the file so it's obvious whether write() changed
+ # anything.
+ with path.open('w') as outs:
+ outs.write('')
+
+ self.sorter.write(path)
+ with path.open() as ins:
+ self.contents = ins.read()
+
+ def assert_errors(self):
+ self.assertTrue(self.errors)
+
+ def assert_no_errors(self):
+ self.assertFalse(self.errors)
+
+ def test_missing_end(self) -> None:
+ with self.assertRaises(keep_sorted.KeepSortedParsingError):
+ self._run(f'{START}\n')
+
+ def test_missing_start(self) -> None:
+ with self.assertRaises(keep_sorted.KeepSortedParsingError):
+ self._run(f'{END}: end\n')
+
+ def test_repeated_start(self) -> None:
+ with self.assertRaises(keep_sorted.KeepSortedParsingError):
+ self._run(f'{START}\n{START}\n')
+
+ def test_unrecognized_directive(self) -> None:
+ with self.assertRaises(keep_sorted.KeepSortedParsingError):
+ self._run(f'{START} foo bar baz\n2\n1\n{END}\n')
+
+ def test_repeated_valid_directive(self) -> None:
+ with self.assertRaises(keep_sorted.KeepSortedParsingError):
+ self._run(f'{START} ignore-case ignore-case\n2\n1\n{END}\n')
+
+ def test_already_sorted(self) -> None:
+ self._run(f'{START}\n1\n2\n3\n4\n{END}\n')
+ self.assert_no_errors()
+ self.assertEqual(self.contents, '')
+
+ def test_not_sorted(self) -> None:
+ self._run(f'{START}\n4\n3\n2\n1\n{END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'{START}\n1\n2\n3\n4\n{END}\n')
+
+ def test_prefix_sorted(self) -> None:
+ self._run(f'foo\nbar\n{START}\n1\n2\n{END}\n')
+ self.assert_no_errors()
+ self.assertEqual(self.contents, '')
+
+ def test_prefix_not_sorted(self) -> None:
+ self._run(f'foo\nbar\n{START}\n2\n1\n{END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'foo\nbar\n{START}\n1\n2\n{END}\n')
+
+ def test_suffix_sorted(self) -> None:
+ self._run(f'{START}\n1\n2\n{END}\nfoo\nbar\n')
+ self.assert_no_errors()
+ self.assertEqual(self.contents, '')
+
+ def test_suffix_not_sorted(self) -> None:
+ self._run(f'{START}\n2\n1\n{END}\nfoo\nbar\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'{START}\n1\n2\n{END}\nfoo\nbar\n')
+
+ def test_not_sorted_case_sensitive(self) -> None:
+ self._run(f'{START}\na\nD\nB\nc\n{END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'{START}\nB\nD\na\nc\n{END}\n')
+
+ def test_not_sorted_case_insensitive(self) -> None:
+ self._run(f'{START} ignore-case\na\nD\nB\nc\n{END}\n')
+ self.assert_errors()
+ self.assertEqual(
+ self.contents, f'{START} ignore-case\na\nB\nc\nD\n{END}\n'
+ )
+
+ def test_remove_dupes(self) -> None:
+ self._run(f'{START}\n1\n2\n2\n1\n{END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'{START}\n1\n2\n{END}\n')
+
+ def test_allow_dupes(self) -> None:
+ self._run(f'{START} allow-dupes\n1\n2\n2\n1\n{END}\n')
+ self.assert_errors()
+ self.assertEqual(
+ self.contents, f'{START} allow-dupes\n1\n1\n2\n2\n{END}\n'
+ )
+
+ def test_case_insensitive_dupes(self) -> None:
+ self._run(f'{START} ignore-case\na\nB\nA\n{END}\n')
+ self.assert_errors()
+ self.assertEqual(
+ self.contents, f'{START} ignore-case\nA\na\nB\n{END}\n'
+ )
+
+ def test_ignored_prefixes(self) -> None:
+ self._run(f'{START} ignore-prefix=foo,bar\na\nb\nfoob\nbarc\n{END}\n')
+ self.assert_no_errors()
+
+ def test_ignored_longest_prefixes(self) -> None:
+ self._run(f'{START} ignore-prefix=1,123\na\n123b\nb\n1c\n{END}\n')
+ self.assert_no_errors()
+
+ def test_ignored_prefixes_whitespace(self) -> None:
+ self._run(
+ f'{START} ignore-prefix=foo,bar\n' f' a\n b\n foob\n barc\n{END}\n'
+ )
+ self.assert_no_errors()
+
+ def test_ignored_prefixes_insensitive(self) -> None:
+ self._run(
+ f'{START} ignore-prefix=foo,bar ignore-case\n'
+ f'a\nB\nfooB\nbarc\n{END}\n'
+ )
+ self.assert_no_errors()
+
+ def test_python_comment_marks_sorted(self) -> None:
+ self._run(f'# {START}\n1\n2\n# {END}\n')
+ self.assert_no_errors()
+
+ def test_python_comment_marks_not_sorted(self) -> None:
+ self._run(f'# {START}\n2\n1\n# {END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'# {START}\n1\n2\n# {END}\n')
+
+ def test_python_comment_sticky_sorted(self) -> None:
+ self._run(f'# {START}\n# A\n1\n2\n# {END}\n')
+ self.assert_no_errors()
+
+ def test_python_comment_sticky_not_sorted(self) -> None:
+ self._run(f'# {START}\n2\n# A\n1\n# {END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'# {START}\n# A\n1\n2\n# {END}\n')
+
+ def test_python_comment_sticky_disabled(self) -> None:
+ self._run(f'# {START} sticky-comments=no\n1\n# B\n2\n# {END}\n')
+ self.assert_errors()
+ self.assertEqual(
+ self.contents, f'# {START} sticky-comments=no\n# B\n1\n2\n# {END}\n'
+ )
+
+ def test_cpp_comment_marks_sorted(self) -> None:
+ self._run(f'// {START}\n1\n2\n// {END}\n')
+ self.assert_no_errors()
+
+ def test_cpp_comment_marks_not_sorted(self) -> None:
+ self._run(f'// {START}\n2\n1\n// {END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'// {START}\n1\n2\n// {END}\n')
+
+ def test_cpp_comment_sticky_sorted(self) -> None:
+ self._run(f'// {START}\n1\n// B\n2\n// {END}\n')
+ self.assert_no_errors()
+
+ def test_cpp_comment_sticky_not_sorted(self) -> None:
+ self._run(f'// {START}\n// B\n2\n1\n// {END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'// {START}\n1\n// B\n2\n// {END}\n')
+
+ def test_cpp_comment_sticky_disabled(self) -> None:
+ self._run(f'// {START} sticky-comments=no\n1\n// B\n2\n// {END}\n')
+ self.assert_errors()
+ self.assertEqual(
+ self.contents,
+ f'// {START} sticky-comments=no\n// B\n1\n2\n// {END}\n',
+ )
+
+ def test_custom_comment_sticky_sorted(self) -> None:
+ self._run(f'{START} sticky-comments=%\n1\n% B\n2\n{END}\n')
+ self.assert_no_errors()
+
+ def test_custom_comment_sticky_not_sorted(self) -> None:
+ self._run(f'{START} sticky-comments=%\n% B\n2\n1\n{END}\n')
+ self.assert_errors()
+ self.assertEqual(
+ self.contents, f'{START} sticky-comments=%\n1\n% B\n2\n{END}\n'
+ )
+
+ def test_multiline_comment_sticky_sorted(self) -> None:
+ self._run(f'# {START}\n# B\n# A\n1\n2\n# {END}\n')
+ self.assert_no_errors()
+
+ def test_multiline_comment_sticky_not_sorted(self) -> None:
+ self._run(f'# {START}\n# B\n# A\n2\n1\n# {END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'# {START}\n1\n# B\n# A\n2\n# {END}\n')
+
+ def test_comment_sticky_sorted_fallback_sorted(self) -> None:
+ self._run(f'# {START}\n# A\n1\n# B\n1\n# {END}\n')
+ self.assert_no_errors()
+
+ def test_comment_sticky_sorted_fallback_not_sorted(self) -> None:
+ self._run(f'# {START}\n# B\n1\n# A\n1\n# {END}\n')
+ self.assert_errors()
+ self.assertEqual(self.contents, f'# {START}\n# A\n1\n# B\n1\n# {END}\n')
+
+ def test_comment_sticky_sorted_fallback_dupes(self) -> None:
+ self._run(f'# {START} allow-dupes\n# A\n1\n# A\n1\n# {END}\n')
+ self.assert_no_errors()
+
+ def test_different_comment_sticky_not_sorted(self) -> None:
+ self._run(f'# {START} sticky-comments=%\n% A\n1\n# B\n2\n# {END}\n')
+ self.assert_errors()
+ self.assertEqual(
+ self.contents,
+ f'# {START} sticky-comments=%\n# B\n% A\n1\n2\n# {END}\n',
+ )
+
+ def test_continuation_sorted(self) -> None:
+ initial = textwrap.dedent(
+ f"""
+ # {START}
+ baz
+ abc
+ foo
+ bar
+ # {END}
+ """.lstrip(
+ '\n'
+ )
+ )
+
+ self._run(initial)
+ self.assert_no_errors()
+
+ def test_continuation_not_sorted(self) -> None:
+ initial = textwrap.dedent(
+ f"""
+ # {START}
+ foo
+ bar
+ baz
+ abc
+ # {END}
+ """.lstrip(
+ '\n'
+ )
+ )
+
+ expected = textwrap.dedent(
+ f"""
+ # {START}
+ baz
+ abc
+ foo
+ bar
+ # {END}
+ """.lstrip(
+ '\n'
+ )
+ )
+
+ self._run(initial)
+ self.assert_errors()
+ self.assertEqual(self.contents, expected)
+
+ def test_indented_continuation_sorted(self) -> None:
+ # Intentionally not using textwrap.dedent().
+ initial = f"""
+ # {START}
+ baz
+ abc
+ foo
+ bar
+ # {END}""".lstrip(
+ '\n'
+ )
+
+ self._run(initial)
+ self.assert_no_errors()
+
+ def test_indented_continuation_not_sorted(self) -> None:
+ # Intentionally not using textwrap.dedent().
+ initial = f"""
+ # {START}
+ foo
+ bar
+ baz
+ abc
+ # {END}""".lstrip(
+ '\n'
+ )
+
+ expected = f"""
+ # {START}
+ baz
+ abc
+ foo
+ bar
+ # {END}""".lstrip(
+ '\n'
+ )
+
+ self._run(initial)
+ self.assert_errors()
+ self.assertEqual(self.contents, expected)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_presubmit/py/ninja_parser_test.py b/pw_presubmit/py/ninja_parser_test.py
new file mode 100644
index 000000000..4710f5ea3
--- /dev/null
+++ b/pw_presubmit/py/ninja_parser_test.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for ninja_parser."""
+
+from pathlib import Path
+import unittest
+from unittest.mock import MagicMock, mock_open, patch
+
+from pw_presubmit import ninja_parser
+
+_STOP = 'ninja: build stopped:\n'
+
+_REAL_TEST_INPUT = """
+[1168/1797] cp ../../pw_software_update/py/dev_sign_test.py python/gen/pw_software_update/py/py.generated_python_package/dev_sign_test.py
+[1169/1797] ACTION //pw_presubmit/py:py.lint.mypy(//pw_build/python_toolchain:python)
+FAILED: python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp
+python ../../pw_build/py/pw_build/python_runner.py --gn-root ../../ --current-path ../../pw_presubmit/py --default-toolchain=//pw_toolchain/default:default --current-toolchain=//pw_build/python_toolchain:python --env=MYPY_FORCE_COLOR=1 --touch python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp --capture-output --module mypy --python-virtualenv-config python/gen/pw_env_setup/pigweed_build_venv/venv_metadata.json --python-dep-list-files python/gen/pw_presubmit/py/py.lint.mypy_metadata_path_list.txt -- --pretty --show-error-codes ../../pw_presubmit/py/pw_presubmit/__init__.py ../../pw_presubmit/py/pw_presubmit/build.py ../../pw_presubmit/py/pw_presubmit/cli.py ../../pw_presubmit/py/pw_presubmit/cpp_checks.py ../../pw_presubmit/py/pw_presubmit/format_code.py ../../pw_presubmit/py/pw_presubmit/git_repo.py ../../pw_presubmit/py/pw_presubmit/inclusive_language.py ../../pw_presubmit/py/pw_presubmit/install_hook.py ../../pw_presubmit/py/pw_presubmit/keep_sorted.py ../../pw_presubmit/py/pw_presubmit/ninja_parser.py ../../pw_presubmit/py/pw_presubmit/npm_presubmit.py ../../pw_presubmit/py/pw_presubmit/pigweed_presubmit.py ../../pw_presubmit/py/pw_presubmit/presubmit.py ../../pw_presubmit/py/pw_presubmit/python_checks.py ../../pw_presubmit/py/pw_presubmit/shell_checks.py ../../pw_presubmit/py/pw_presubmit/todo_check.py ../../pw_presubmit/py/pw_presubmit/tools.py ../../pw_presubmit/py/git_repo_test.py ../../pw_presubmit/py/keep_sorted_test.py ../../pw_presubmit/py/ninja_parser_test.py ../../pw_presubmit/py/presubmit_test.py ../../pw_presubmit/py/tools_test.py ../../pw_presubmit/py/setup.py
+ Requirement already satisfied: pyserial in c:\\b\\s\\w\\ir\\x\\w\\co\\environment\\pigweed-venv\\lib\\site-packages (from pigweed==0.0.13+20230126212203) (3.5)
+../../pw_presubmit/py/presubmit_test.py:63: error: Module has no attribute
+"Filter" [attr-defined]
+ TestData(presubmit.Filter(suffix=('.a', '.b')), 'foo.c', False...
+ ^
+Found 1 error in 1 file (checked 23 source files)
+[1170/1797] stamp python/obj/pw_snapshot/metadata_proto.python._mirror_sources_to_out_dir.stamp
+[1171/1797] stamp python/obj/pw_software_update/py/py._mirror_sources_to_out_dir_dev_sign_test.py.stamp
+[1172/1797] ACTION //pw_log:protos.python(//pw_build/python_toolchain:python)
+[1173/1797] ACTION //pw_thread_freertos/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1174/1797] ACTION //pw_symbolizer/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1175/1797] ACTION //pw_symbolizer/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1176/1797] ACTION //pw_thread_freertos/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1177/1797] ACTION //pw_tls_client/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1178/1797] ACTION //pw_symbolizer/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1179/1797] ACTION //pw_console/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1180/1797] ACTION //pw_tls_client/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1181/1797] ACTION //pw_console/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1182/1797] ACTION //pw_console/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1183/1797] ACTION //pw_tls_client/py:py.lint.mypy(//pw_build/python_toolchain:python)
+[1184/1797] ACTION //pw_symbolizer/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1185/1797] ACTION //pw_thread_freertos/py:py.lint.pylint(//pw_build/python_toolchain:python)
+[1186/1797] ACTION //pw_tls_client/py:py.lint.pylint(//pw_build/python_toolchain:python)
+ninja: build stopped: subcommand failed.
+[FINISHED]
+"""
+
+_REAL_TEST_SUMMARY = """
+[1169/1797] ACTION //pw_presubmit/py:py.lint.mypy(//pw_build/python_toolchain:python)
+FAILED: python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp
+python ../../pw_build/py/pw_build/python_runner.py --gn-root ../../ --current-path ../../pw_presubmit/py --default-toolchain=//pw_toolchain/default:default --current-toolchain=//pw_build/python_toolchain:python --env=MYPY_FORCE_COLOR=1 --touch python/gen/pw_presubmit/py/py.lint.mypy.pw_pystamp --capture-output --module mypy --python-virtualenv-config python/gen/pw_env_setup/pigweed_build_venv/venv_metadata.json --python-dep-list-files python/gen/pw_presubmit/py/py.lint.mypy_metadata_path_list.txt -- --pretty --show-error-codes ../../pw_presubmit/py/pw_presubmit/__init__.py ../../pw_presubmit/py/pw_presubmit/build.py ../../pw_presubmit/py/pw_presubmit/cli.py ../../pw_presubmit/py/pw_presubmit/cpp_checks.py ../../pw_presubmit/py/pw_presubmit/format_code.py ../../pw_presubmit/py/pw_presubmit/git_repo.py ../../pw_presubmit/py/pw_presubmit/inclusive_language.py ../../pw_presubmit/py/pw_presubmit/install_hook.py ../../pw_presubmit/py/pw_presubmit/keep_sorted.py ../../pw_presubmit/py/pw_presubmit/ninja_parser.py ../../pw_presubmit/py/pw_presubmit/npm_presubmit.py ../../pw_presubmit/py/pw_presubmit/pigweed_presubmit.py ../../pw_presubmit/py/pw_presubmit/presubmit.py ../../pw_presubmit/py/pw_presubmit/python_checks.py ../../pw_presubmit/py/pw_presubmit/shell_checks.py ../../pw_presubmit/py/pw_presubmit/todo_check.py ../../pw_presubmit/py/pw_presubmit/tools.py ../../pw_presubmit/py/git_repo_test.py ../../pw_presubmit/py/keep_sorted_test.py ../../pw_presubmit/py/ninja_parser_test.py ../../pw_presubmit/py/presubmit_test.py ../../pw_presubmit/py/tools_test.py ../../pw_presubmit/py/setup.py
+../../pw_presubmit/py/presubmit_test.py:63: error: Module has no attribute
+"Filter" [attr-defined]
+ TestData(presubmit.Filter(suffix=('.a', '.b')), 'foo.c', False...
+ ^
+Found 1 error in 1 file (checked 23 source files)
+"""
+
+
+class TestNinjaParser(unittest.TestCase):
+ """Test ninja_parser."""
+
+ def _run(self, contents: str) -> str: # pylint: disable=no-self-use
+ path = MagicMock(spec=Path('foo/bar'))
+
+ def mocked_open_read(*args, **kwargs):
+ return mock_open(read_data=contents)(*args, **kwargs)
+
+ with patch.object(path, 'open', mocked_open_read):
+ return ninja_parser.parse_ninja_stdout(path)
+
+ def test_simple(self) -> None:
+ error = '[2/10] baz\nFAILED: something\nerror 1\nerror 2\n'
+ result = self._run('[0/10] foo\n[1/10] bar\n' + error + _STOP)
+ self.assertEqual(error.strip(), result.strip())
+
+ def test_short(self) -> None:
+ error = '[2/10] baz\nFAILED: something\n'
+ result = self._run('[0/10] foo\n[1/10] bar\n' + error + _STOP)
+ self.assertEqual(error.strip(), result.strip())
+
+ def test_unexpected(self) -> None:
+ error = '[2/10] baz\nERROR: something\nerror 1\n'
+ result = self._run('[0/10] foo\n[1/10] bar\n' + error)
+ self.assertEqual('', result.strip())
+
+ def test_real(self) -> None:
+ result = self._run(_REAL_TEST_INPUT)
+ self.assertEqual(_REAL_TEST_SUMMARY.strip(), result.strip())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_presubmit/py/owners_checks_test.py b/pw_presubmit/py/owners_checks_test.py
new file mode 100644
index 000000000..06194940b
--- /dev/null
+++ b/pw_presubmit/py/owners_checks_test.py
@@ -0,0 +1,448 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+#
+"""Unit tests for owners_checks.py."""
+from pathlib import Path
+import tempfile
+from typing import Iterable, Sequence, Tuple
+import unittest
+from unittest import mock
+from pw_presubmit import owners_checks
+
+# ===== Test data =====
+
+bad_duplicate = """\
+# Should raise OwnersDuplicateError.
+set noparent
+
+file:/foo/OWNERZ
+file:../OWNERS
+file:../OWNERS
+test1@example.com
+#Test 2 comment
+test2@example.com
+"""
+
+bad_duplicate_user = """\
+# Should raise OwnersDuplicateError.
+set noparent
+
+file:/foo/OWNERZ
+file:../OWNERS
+
+*
+test1@example.com
+#Test 2 comment
+test2@example.com
+ test1@example.com
+"""
+
+bad_duplicate_wildcard = """\
+# Should raise OwnersDuplicateError.
+set noparent
+*
+file:/foo/OWNERZ
+file:../OWNERS
+test1@example.com
+#Test 2 comment
+test2@example.com
+*
+"""
+
+bad_email = """\
+# Should raise OwnersInvalidLineError.
+set noparent
+*
+file:/foo/OWNERZ
+file:../OWNERS
+test1example.com
+#Test 2 comment
+test2@example.com
+*
+"""
+bad_grant_combo = """\
+# Should raise OwnersUserGrantError.
+
+file:/foo/OWNERZ
+file:../OWNERS
+
+test1@example.com
+#Test noparent comment
+set noparent
+test2@example.com
+
+*
+"""
+
+bad_ordering1 = """\
+# Tests formatter reorders groupings of lines into the right order.
+file:/foo/OWNERZ
+file:bar/OWNERZ
+
+test1@example.com
+#Test noparent comment
+set noparent
+test2@example.com
+"""
+
+bad_ordering1_fixed = """\
+#Test noparent comment
+set noparent
+
+# Tests formatter reorders groupings of lines into the right order.
+file:/foo/OWNERZ
+file:bar/OWNERZ
+
+test1@example.com
+test2@example.com
+"""
+
+bad_prohibited1 = """\
+# Should raise OwnersProhibitedError.
+set noparent
+
+file:/foo/OWNERZ
+file:../OWNERS
+
+test1@example.com
+#Test 2 comment
+test2@example.com
+
+include file1.txt
+
+per-file foo.txt=test3@example.com
+"""
+
+bad_moving_comments = """\
+# Test comments move with the rule that follows them.
+test2@example.com
+test1@example.com
+
+# foo comment
+file:/foo/OWNERZ
+# .. comment
+file:../OWNERS
+
+set noparent
+"""
+bad_moving_comments_fixed = """\
+set noparent
+
+# .. comment
+file:../OWNERS
+# foo comment
+file:/foo/OWNERZ
+
+test1@example.com
+# Test comments move with the rule that follows them.
+test2@example.com
+"""
+
+bad_whitespace = """\
+ set noparent
+
+
+
+file:/foo/OWNERZ
+
+ file:../OWNERS
+
+test1@example.com
+#Test 2 comment
+ test2@example.com
+
+"""
+
+bad_whitespace_fixed = """\
+set noparent
+
+file:../OWNERS
+file:/foo/OWNERZ
+
+test1@example.com
+#Test 2 comment
+test2@example.com
+"""
+
+no_dependencies = """\
+# Test no imports are found when there are none.
+set noparent
+
+test1@example.com
+#Test 2 comment
+test2@example.com
+"""
+
+has_dependencies_file = """\
+# Test if owners checks examine file: imports.
+set noparent
+
+file:foo_owners
+file:bar_owners
+
+test1@example.com
+#Test 2 comment
+test2@example.com
+"""
+
+has_dependencies_perfile = """\
+# Test if owners checks examine per-file imports.
+set noparent
+
+test1@example.com
+#Test 2 comment
+test2@example.com
+
+per-file *.txt=file:foo_owners
+per-file *.md=example.google.com
+"""
+
+has_dependencies_include = """\
+# Test if owners checks examine per-file imports.
+set noparent
+
+test1@example.com
+#Test 2 comment
+test2@example.com
+
+per-file *.txt=file:foo_owners
+per-file *.md=example.google.com
+"""
+
+dependencies_paths_relative = """\
+set noparent
+
+include foo/bar/../include_owners
+
+file:foo/bar/../file_owners
+
+*
+
+per-file *.txt=file:foo/bar/../perfile_owners
+"""
+
+dependencies_paths_absolute = """\
+set noparent
+
+include /test/include_owners
+
+file:/test/file_owners
+
+*
+
+per-file *.txt=file:/test/perfile_owners
+"""
+
+good1 = """\
+# Checks should fine this formatted correctly
+set noparent
+
+include good1_include
+
+file:good1_file
+
+test1@example.com
+#Test 2 comment
+test2@example.com #{LAST_RESORT_SUGGESTION}
+# LAST LINE
+"""
+
+good1_include = """\
+test1@example.comom
+"""
+
+good1_file = """\
+# Checks should fine this formatted correctly.
+test1@example.com
+"""
+
+foo_owners = """\
+test1@example.com
+"""
+
+bar_owners = """\
+test1@example.com
+"""
+
+BAD_TEST_FILES = (
+ ("bad_duplicate", owners_checks.OwnersDuplicateError),
+ ("bad_duplicate_user", owners_checks.OwnersDuplicateError),
+ ("bad_duplicate_wildcard", owners_checks.OwnersDuplicateError),
+ ("bad_email", owners_checks.OwnersInvalidLineError),
+ ("bad_grant_combo", owners_checks.OwnersUserGrantError),
+ ("bad_ordering1", owners_checks.OwnersStyleError),
+ ("bad_prohibited1", owners_checks.OwnersProhibitedError),
+)
+
+STYLING_CHECKS = (
+ ("bad_moving_comments", "bad_moving_comments_fixed"),
+ ("bad_ordering1", "bad_ordering1_fixed"),
+ ("bad_whitespace", "bad_whitespace_fixed"),
+)
+
+DEPENDENCY_TEST_CASES: Iterable[Tuple[str, Iterable[str]]] = (
+ ("no_dependencies", tuple()),
+ ("has_dependencies_file", ("foo_owners", "bar_owners")),
+ ("has_dependencies_perfile", ("foo_owners",)),
+ ("has_dependencies_include", ("foo_owners",)),
+)
+
+DEPENDENCY_PATH_TEST_CASES: Iterable[str] = (
+ "dependencies_paths_relative",
+ "dependencies_paths_absolute",
+)
+
+GOOD_TEST_CASES = (("good1", "good1_include", "good1_file"),)
+
+
+# ===== Unit Tests =====
+class TestOwnersChecks(unittest.TestCase):
+ """Unittest class for owners_checks.py."""
+
+ maxDiff = 2000
+
+ @staticmethod
+ def _create_temp_files(
+ temp_dir: str, file_list: Sequence[Tuple[str, str]]
+ ) -> Sequence[Path]:
+ real_files = []
+ temp_dir_path = Path(temp_dir)
+ for name, contents in file_list:
+ file_path = temp_dir_path / name
+ file_path.write_text(contents)
+ real_files.append(file_path)
+ return real_files
+
+ def test_bad_files(self):
+ # First test_file is the "primary" owners file followed by any needed
+ # "secondary" owners.
+ for test_file, expected_exception in BAD_TEST_FILES:
+ with self.subTest(
+ i=test_file
+ ), tempfile.TemporaryDirectory() as temp_dir, self.assertRaises(
+ expected_exception
+ ):
+ file_contents = globals()[test_file]
+ primary_file = self._create_temp_files(
+ temp_dir=temp_dir, file_list=((test_file, file_contents),)
+ )[0]
+ owners_file = owners_checks.OwnersFile(primary_file)
+ owners_file.look_for_owners_errors()
+ owners_file.check_style()
+
+ def test_good(self):
+ # First test_file is the "primary" owners file followed by any needed
+ # "secondary" owners.
+ for test_files in GOOD_TEST_CASES:
+ with self.subTest(
+ i=test_files[0]
+ ), tempfile.TemporaryDirectory() as temp_dir:
+ files = [
+ (file_name, globals()[file_name])
+ for file_name in test_files
+ ]
+ primary_file = self._create_temp_files(
+ temp_dir=temp_dir, file_list=files
+ )[0]
+ self.assertDictEqual(
+ {}, owners_checks.run_owners_checks(primary_file)
+ )
+
+ def test_style_proposals(self):
+ for unstyled_file, styled_file in STYLING_CHECKS:
+ with self.subTest(
+ i=unstyled_file
+ ), tempfile.TemporaryDirectory() as temp_dir:
+ unstyled_contents = globals()[unstyled_file]
+ styled_contents = globals()[styled_file]
+ unstyled_real_file = self._create_temp_files(
+ temp_dir=temp_dir,
+ file_list=((unstyled_file, unstyled_contents),),
+ )[0]
+ owners_file = owners_checks.OwnersFile(unstyled_real_file)
+ formatted_content = "\n".join(owners_file.formatted_lines)
+ self.assertEqual(styled_contents, formatted_content)
+
+ def test_dependency_discovery(self):
+ for file_under_test, expected_deps in DEPENDENCY_TEST_CASES:
+ # During test make the test file directory the "git root"
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_dir_path = Path(temp_dir).resolve()
+ with self.subTest(i=file_under_test), mock.patch(
+ "pw_presubmit.owners_checks.git_repo.root",
+ return_value=temp_dir_path,
+ ):
+ primary_file = (file_under_test, globals()[file_under_test])
+ deps_files = tuple(
+ (dep, (globals()[dep])) for dep in expected_deps
+ )
+
+ primary_file = self._create_temp_files(
+ temp_dir=temp_dir, file_list=(primary_file,)
+ )[0]
+ dep_files = self._create_temp_files(
+ temp_dir=temp_dir, file_list=deps_files
+ )
+
+ owners_file = owners_checks.OwnersFile(primary_file)
+
+ # get_dependencies is expected to resolve() files
+ found_deps = owners_file.get_dependencies()
+ expected_deps_path = [path.resolve() for path in dep_files]
+ expected_deps_path.sort()
+ found_deps.sort()
+ self.assertListEqual(expected_deps_path, found_deps)
+
+ def test_dependency_path_creation(self):
+ """Confirm paths care constructed for absolute and relative paths."""
+ for file_under_test in DEPENDENCY_PATH_TEST_CASES:
+ # During test make the test file directory the "git root"
+ with tempfile.TemporaryDirectory() as temp_dir:
+ temp_dir_path = Path(temp_dir).resolve()
+ with self.subTest(i=file_under_test), mock.patch(
+ "pw_presubmit.owners_checks.git_repo.root",
+ return_value=temp_dir_path,
+ ):
+ owners_file_path = (
+ temp_dir_path / "owners" / file_under_test
+ )
+ owners_file_path.parent.mkdir(parents=True)
+ owners_file_path.write_text(globals()[file_under_test])
+ owners_file = owners_checks.OwnersFile(owners_file_path)
+
+ # get_dependencies is expected to resolve() files
+ found_deps = owners_file.get_dependencies()
+
+ if "absolute" in file_under_test:
+ # Absolute paths start with the git/project root
+ expected_prefix = temp_dir_path / "test"
+ else:
+ # Relative paths start with owners file dir
+ expected_prefix = owners_file_path.parent / "foo"
+
+ expected_deps_path = [
+ (expected_prefix / filename).resolve()
+ for filename in (
+ "include_owners",
+ "file_owners",
+ "perfile_owners",
+ )
+ ]
+ expected_deps_path.sort()
+ found_deps.sort()
+ self.assertListEqual(expected_deps_path, found_deps)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/pw_presubmit/py/presubmit_test.py b/pw_presubmit/py/presubmit_test.py
index c4327d5e5..2ecbcde00 100755
--- a/pw_presubmit/py/presubmit_test.py
+++ b/pw_presubmit/py/presubmit_test.py
@@ -14,11 +14,73 @@
# the License.
"""Tests for presubmit tools."""
+import dataclasses
+import re
import unittest
from pw_presubmit import presubmit
+class TestFileFilter(unittest.TestCase):
+ """Test FileFilter class"""
+
+ @dataclasses.dataclass
+ class TestData:
+ filter: presubmit.FileFilter
+ value: str
+ expected: bool
+
+ test_scenarios = (
+ TestData(presubmit.FileFilter(endswith=('bar', 'foo')), 'foo', True),
+ TestData(presubmit.FileFilter(endswith=('bar', 'boo')), 'foo', False),
+ TestData(
+ presubmit.FileFilter(exclude=(re.compile('a/.+'),), name=('foo',)),
+ '/a/b/c/foo',
+ False,
+ ),
+ TestData(
+ presubmit.FileFilter(exclude=(re.compile('x/.+'),), name=('foo',)),
+ '/a/b/c/foo',
+ True,
+ ),
+ TestData(
+ presubmit.FileFilter(exclude=(re.compile('a+'), re.compile('b+'))),
+ 'cccc',
+ True,
+ ),
+ TestData(presubmit.FileFilter(name=('foo',)), 'foo', True),
+ TestData(presubmit.FileFilter(name=('foo',)), 'food', False),
+ TestData(presubmit.FileFilter(name=(re.compile('foo'),)), 'foo', True),
+ TestData(
+ presubmit.FileFilter(name=(re.compile('foo'),)), 'food', False
+ ),
+ TestData(presubmit.FileFilter(name=(re.compile('fo+'),)), 'foo', True),
+ TestData(presubmit.FileFilter(name=(re.compile('fo+'),)), 'fd', False),
+ TestData(
+ presubmit.FileFilter(suffix=('.exe',)), 'a/b.py/foo.exe', True
+ ),
+ TestData(
+ presubmit.FileFilter(suffix=('.py',)), 'a/b.py/foo.exe', False
+ ),
+ TestData(
+ presubmit.FileFilter(suffix=('.exe',)), 'a/b.py/foo.py.exe', True
+ ),
+ TestData(
+ presubmit.FileFilter(suffix=('.py',)), 'a/b.py/foo.py.exe', False
+ ),
+ TestData(presubmit.FileFilter(suffix=('.a', '.b')), 'foo.b', True),
+ TestData(presubmit.FileFilter(suffix=('.a', '.b')), 'foo.c', False),
+ )
+
+ def test_matches(self):
+ for test_num, test_data in enumerate(self.test_scenarios):
+ with self.subTest(i=test_num):
+ self.assertEqual(
+ test_data.filter.matches(test_data.value),
+ test_data.expected,
+ )
+
+
def _fake_function_1(_):
"""Fake presubmit function."""
@@ -27,35 +89,81 @@ def _fake_function_2(_):
"""Fake presubmit function."""
+def _all_substeps(program):
+ substeps = {}
+ for step in program:
+ # pylint: disable=protected-access
+ for sub in step.substeps():
+ substeps[sub.name or step.name] = sub._func
+ # pylint: enable=protected-access
+ return substeps
+
+
class ProgramsTest(unittest.TestCase):
"""Tests the presubmit Programs abstraction."""
+
def setUp(self):
self._programs = presubmit.Programs(
- first=[_fake_function_1, (), [(_fake_function_2, )]],
+ first=[_fake_function_1, (), [(_fake_function_2,)]],
second=[_fake_function_2],
)
def test_empty(self):
self.assertEqual({}, presubmit.Programs())
- def test_access_present_members(self):
+ def test_access_present_members_first(self):
self.assertEqual('first', self._programs['first'].name)
- self.assertEqual((_fake_function_1, _fake_function_2),
- tuple(self._programs['first']))
+ self.assertEqual(
+ ('_fake_function_1', '_fake_function_2'),
+ tuple(x.name for x in self._programs['first']),
+ )
+ self.assertEqual(2, len(self._programs['first']))
+ substeps = _all_substeps(
+ self._programs['first'] # pylint: disable=protected-access
+ ).values()
+ self.assertEqual(2, len(substeps))
+ self.assertEqual((_fake_function_1, _fake_function_2), tuple(substeps))
+
+ def test_access_present_members_second(self):
self.assertEqual('second', self._programs['second'].name)
- self.assertEqual((_fake_function_2, ), tuple(self._programs['second']))
+ self.assertEqual(
+ ('_fake_function_2',),
+ tuple(x.name for x in self._programs['second']),
+ )
+
+ self.assertEqual(1, len(self._programs['second']))
+ substeps = _all_substeps(
+ self._programs['second'] # pylint: disable=protected-access
+ ).values()
+ self.assertEqual(1, len(substeps))
+ self.assertEqual((_fake_function_2,), tuple(substeps))
def test_access_missing_member(self):
with self.assertRaises(KeyError):
_ = self._programs['not_there']
def test_all_steps(self):
- self.assertEqual(
- {
- '_fake_function_1': _fake_function_1,
- '_fake_function_2': _fake_function_2,
- }, self._programs.all_steps())
+ all_steps = self._programs.all_steps()
+ self.assertEqual(len(all_steps), 2)
+ all_substeps = _all_substeps(all_steps.values())
+ self.assertEqual(len(all_substeps), 2)
+
+ # pylint: disable=protected-access
+ self.assertEqual(all_substeps['_fake_function_1'], _fake_function_1)
+ self.assertEqual(all_substeps['_fake_function_2'], _fake_function_2)
+ # pylint: enable=protected-access
+
+
+class ContextTest(unittest.TestCase):
+ def test_presubmitcontext(self): # pylint: disable=no-self-use
+ _ = presubmit.PresubmitContext.create_for_testing()
+
+ def test_lucicontext(self): # pylint: disable=no-self-use
+ _ = presubmit.LuciContext.create_for_testing()
+
+ def test_lucitrigger(self): # pylint: disable=no-self-use
+ _ = presubmit.LuciTrigger.create_for_testing()
if __name__ == '__main__':
diff --git a/pw_presubmit/py/pw_presubmit/bazel_parser.py b/pw_presubmit/py/pw_presubmit/bazel_parser.py
new file mode 100644
index 000000000..39f42cdcd
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/bazel_parser.py
@@ -0,0 +1,63 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Extracts a concise error from a bazel log."""
+
+from pathlib import Path
+import re
+import sys
+
+
+def parse_bazel_stdout(bazel_stdout: Path) -> str:
+ """Extracts a concise error from a bazel log."""
+ seen_error = False
+ error_lines = []
+
+ with bazel_stdout.open() as ins:
+ for line in ins:
+ # Trailing whitespace isn't significant, as it doesn't affect the
+ # way the line shows up in the logs. However, leading whitespace may
+ # be significant, especially for compiler error messages.
+ line = line.rstrip()
+
+ if re.match(r'^ERROR:', line):
+ seen_error = True
+
+ if seen_error:
+ # Ignore long lines that just show the environment.
+ if re.search(r' +[\w_]*(PATH|PWD)=.* \\', line):
+ continue
+
+ # Ignore lines that only show bazel sandboxing.
+ if line.strip().startswith(('(cd /', 'exec env')):
+ continue
+
+ if line.strip().startswith('Use --sandbox_debug to see'):
+ continue
+
+ if line.strip().startswith('# Configuration'):
+ continue
+
+ # An "<ALLCAPS>:" line usually means compiler output is done
+ # and useful compiler errors are complete.
+ if re.match(r'^(?!ERROR)[A-Z]+:', line):
+ break
+
+ error_lines.append(line)
+
+ result = '\n'.join(error_lines)
+ return re.sub(r'\n+', '\n', result)
+
+
+if __name__ == '__main__':
+ print(parse_bazel_stdout(Path(sys.argv[1])))
diff --git a/pw_presubmit/py/pw_presubmit/build.py b/pw_presubmit/py/pw_presubmit/build.py
index 9da43e13f..6ef59e880 100644
--- a/pw_presubmit/py/pw_presubmit/build.py
+++ b/pw_presubmit/py/pw_presubmit/build.py
@@ -13,7 +13,6 @@
# the License.
"""Functions for building code during presubmit checks."""
-import collections
import contextlib
import itertools
import json
@@ -23,19 +22,40 @@ from pathlib import Path
import re
import subprocess
from shutil import which
-from typing import (Collection, Container, Dict, Iterable, List, Mapping, Set,
- Tuple, Union)
+import sys
+from typing import (
+ Any,
+ Callable,
+ Collection,
+ Container,
+ ContextManager,
+ Dict,
+ Iterable,
+ List,
+ Mapping,
+ Optional,
+ Sequence,
+ Set,
+ Tuple,
+ Union,
+)
-from pw_package import package_manager
from pw_presubmit import (
+ bazel_parser,
call,
Check,
+ FileFilter,
filter_paths,
format_code,
+ install_package,
+ Iterator,
log_run,
+ ninja_parser,
plural,
PresubmitContext,
PresubmitFailure,
+ PresubmitResult,
+ SubStep,
tools,
)
@@ -47,28 +67,60 @@ def bazel(ctx: PresubmitContext, cmd: str, *args: str) -> None:
Intended for use with bazel build and test. May not work with others.
"""
- call('bazel',
- cmd,
- '--verbose_failures',
- '--verbose_explanations',
- '--worker_verbose',
- f'--symlink_prefix={ctx.output_dir / ".bazel-"}',
- *args,
- cwd=ctx.root,
- env=env_with_clang_vars())
+
+ num_jobs: List[str] = []
+ if ctx.num_jobs is not None:
+ num_jobs.extend(('--jobs', str(ctx.num_jobs)))
+
+ keep_going: List[str] = []
+ if ctx.continue_after_build_error:
+ keep_going.append('--keep_going')
+
+ bazel_stdout = ctx.output_dir / 'bazel.stdout'
+ try:
+ with bazel_stdout.open('w') as outs:
+ call(
+ 'bazel',
+ cmd,
+ '--verbose_failures',
+ '--verbose_explanations',
+ '--worker_verbose',
+ f'--symlink_prefix={ctx.output_dir / ".bazel-"}',
+ *num_jobs,
+ *keep_going,
+ *args,
+ cwd=ctx.root,
+ env=env_with_clang_vars(),
+ tee=outs,
+ )
+
+ except PresubmitFailure as exc:
+ failure = bazel_parser.parse_bazel_stdout(bazel_stdout)
+ if failure:
+ with ctx.failure_summary_log.open('w') as outs:
+ outs.write(failure)
+
+ raise exc
-def install_package(root: Path, name: str) -> None:
- """Install package with given name in given path."""
- mgr = package_manager.PackageManager(root)
+def _gn_value(value) -> str:
+ if isinstance(value, bool):
+ return str(value).lower()
- if not mgr.list():
- raise PresubmitFailure(
- 'no packages configured, please import your pw_package '
- 'configuration module')
+ if (
+ isinstance(value, str)
+ and '"' not in value
+ and not value.startswith("{")
+ and not value.startswith("[")
+ ):
+ return f'"{value}"'
- if not mgr.status(name):
- mgr.install(name)
+ if isinstance(value, (list, tuple)):
+ return f'[{", ".join(_gn_value(a) for a in value)}]'
+
+ # Fall-back case handles integers as well as strings that already
+ # contain double quotation marks, or look like scopes or lists.
+ return str(value)
def gn_args(**kwargs) -> str:
@@ -81,16 +133,8 @@ def gn_args(**kwargs) -> str:
"""
transformed_args = []
for arg, val in kwargs.items():
- if isinstance(val, bool):
- transformed_args.append(f'{arg}={str(val).lower()}')
- continue
- if (isinstance(val, str) and '"' not in val and not val.startswith("{")
- and not val.startswith("[")):
- transformed_args.append(f'{arg}="{val}"')
- continue
- # Fall-back case handles integers as well as strings that already
- # contain double quotation marks, or look like scopes or lists.
- transformed_args.append(f'{arg}={val}')
+ transformed_args.append(f'{arg}={_gn_value(val)}')
+
# Use ccache if available for faster repeat presubmit runs.
if which('ccache'):
transformed_args.append('pw_command_launcher="ccache"')
@@ -98,20 +142,26 @@ def gn_args(**kwargs) -> str:
return '--args=' + ' '.join(transformed_args)
-def gn_gen(gn_source_dir: Path,
- gn_output_dir: Path,
- *args: str,
- gn_check: bool = True,
- gn_fail_on_unused: bool = True,
- export_compile_commands: Union[bool, str] = True,
- **gn_arguments) -> None:
+def gn_gen(
+ ctx: PresubmitContext,
+ *args: str,
+ gn_check: bool = True,
+ gn_fail_on_unused: bool = True,
+ export_compile_commands: Union[bool, str] = True,
+ preserve_args_gn: bool = False,
+ **gn_arguments,
+) -> None:
"""Runs gn gen in the specified directory with optional GN args."""
- args_option = gn_args(**gn_arguments)
+ all_gn_args = dict(gn_arguments)
+ all_gn_args.update(ctx.override_gn_args)
+ _LOG.debug('%r', all_gn_args)
+ args_option = gn_args(**all_gn_args)
- # Delete args.gn to ensure this is a clean build.
- args_gn = gn_output_dir / 'args.gn'
- if args_gn.is_file():
- args_gn.unlink()
+ if not preserve_args_gn:
+ # Delete args.gn to ensure this is a clean build.
+ args_gn = ctx.output_dir / 'args.gn'
+ if args_gn.is_file():
+ args_gn.unlink()
export_commands_arg = ''
if export_compile_commands:
@@ -119,69 +169,113 @@ def gn_gen(gn_source_dir: Path,
if isinstance(export_compile_commands, str):
export_commands_arg += f'={export_compile_commands}'
- call('gn',
- 'gen',
- gn_output_dir,
- '--color=always',
- *(['--fail-on-unused-args'] if gn_fail_on_unused else []),
- *([export_commands_arg] if export_commands_arg else []),
- *args,
- args_option,
- cwd=gn_source_dir)
+ call(
+ 'gn',
+ 'gen',
+ ctx.output_dir,
+ '--color=always',
+ *(['--fail-on-unused-args'] if gn_fail_on_unused else []),
+ *([export_commands_arg] if export_commands_arg else []),
+ *args,
+ *([args_option] if all_gn_args else []),
+ cwd=ctx.root,
+ )
if gn_check:
- call('gn',
- 'check',
- gn_output_dir,
- '--check-generated',
- '--check-system',
- cwd=gn_source_dir)
-
-
-def ninja(directory: Path,
- *args,
- save_compdb=True,
- save_graph=True,
- **kwargs) -> None:
+ call(
+ 'gn',
+ 'check',
+ ctx.output_dir,
+ '--check-generated',
+ '--check-system',
+ cwd=ctx.root,
+ )
+
+
+def ninja(
+ ctx: PresubmitContext, *args, save_compdb=True, save_graph=True, **kwargs
+) -> None:
"""Runs ninja in the specified directory."""
+
+ num_jobs: List[str] = []
+ if ctx.num_jobs is not None:
+ num_jobs.extend(('-j', str(ctx.num_jobs)))
+
+ keep_going: List[str] = []
+ if ctx.continue_after_build_error:
+ keep_going.extend(('-k', '0'))
+
if save_compdb:
proc = subprocess.run(
- ['ninja', '-C', directory, '-t', 'compdb', *args],
+ ['ninja', '-C', ctx.output_dir, '-t', 'compdb', *args],
capture_output=True,
- **kwargs)
- (directory / 'ninja.compdb').write_bytes(proc.stdout)
+ **kwargs,
+ )
+ (ctx.output_dir / 'ninja.compdb').write_bytes(proc.stdout)
if save_graph:
- proc = subprocess.run(['ninja', '-C', directory, '-t', 'graph', *args],
- capture_output=True,
- **kwargs)
- (directory / 'ninja.graph').write_bytes(proc.stdout)
+ proc = subprocess.run(
+ ['ninja', '-C', ctx.output_dir, '-t', 'graph', *args],
+ capture_output=True,
+ **kwargs,
+ )
+ (ctx.output_dir / 'ninja.graph').write_bytes(proc.stdout)
+
+ ninja_stdout = ctx.output_dir / 'ninja.stdout'
+ try:
+ with ninja_stdout.open('w') as outs:
+ if sys.platform == 'win32':
+ # Windows doesn't support pw-wrap-ninja.
+ ninja_command = ['ninja']
+ else:
+ ninja_command = ['pw-wrap-ninja', '--log-actions']
+
+ call(
+ *ninja_command,
+ '-C',
+ ctx.output_dir,
+ *num_jobs,
+ *keep_going,
+ *args,
+ tee=outs,
+ propagate_sigterm=True,
+ **kwargs,
+ )
- call('ninja', '-C', directory, *args, **kwargs)
- (directory / '.ninja_log').rename(directory / 'ninja.log')
+ except PresubmitFailure as exc:
+ failure = ninja_parser.parse_ninja_stdout(ninja_stdout)
+ if failure:
+ with ctx.failure_summary_log.open('w') as outs:
+ outs.write(failure)
+
+ raise exc
def get_gn_args(directory: Path) -> List[Dict[str, Dict[str, str]]]:
"""Dumps GN variables to JSON."""
- proc = subprocess.run(['gn', 'args', directory, '--list', '--json'],
- stdout=subprocess.PIPE)
+ proc = subprocess.run(
+ ['gn', 'args', directory, '--list', '--json'], stdout=subprocess.PIPE
+ )
return json.loads(proc.stdout)
-def cmake(source_dir: Path,
- output_dir: Path,
- *args: str,
- env: Mapping['str', 'str'] = None) -> None:
+def cmake(
+ ctx: PresubmitContext,
+ *args: str,
+ env: Optional[Mapping['str', 'str']] = None,
+) -> None:
"""Runs CMake for Ninja on the given source and output directories."""
- call('cmake',
- '-B',
- output_dir,
- '-S',
- source_dir,
- '-G',
- 'Ninja',
- *args,
- env=env)
+ call(
+ 'cmake',
+ '-B',
+ ctx.output_dir,
+ '-S',
+ ctx.root,
+ '-G',
+ 'Ninja',
+ *args,
+ env=env,
+ )
def env_with_clang_vars() -> Mapping[str, str]:
@@ -197,10 +291,14 @@ def _get_paths_from_command(source_dir: Path, *args, **kwargs) -> Set[Path]:
process = log_run(args, capture_output=True, cwd=source_dir, **kwargs)
if process.returncode:
- _LOG.error('Build invocation failed with return code %d!',
- process.returncode)
- _LOG.error('[COMMAND] %s\n%s\n%s', *tools.format_command(args, kwargs),
- process.stderr.decode())
+ _LOG.error(
+ 'Build invocation failed with return code %d!', process.returncode
+ )
+ _LOG.error(
+ '[COMMAND] %s\n%s\n%s',
+ *tools.format_command(args, kwargs),
+ process.stderr.decode(),
+ )
raise PresubmitFailure
files = set()
@@ -264,72 +362,144 @@ def check_compile_commands_for_files(
compiled = frozenset(
itertools.chain.from_iterable(
- compiled_files(cmds) for cmds in compile_commands))
+ compiled_files(cmds) for cmds in compile_commands
+ )
+ )
return [f for f in files if f not in compiled and f.suffix in extensions]
-def check_builds_for_files(
- bazel_extensions_to_check: Container[str],
- gn_extensions_to_check: Container[str],
- files: Iterable[Path],
- bazel_dirs: Iterable[Path] = (),
- gn_dirs: Iterable[Tuple[Path, Path]] = (),
- gn_build_files: Iterable[Path] = (),
-) -> Dict[str, List[Path]]:
- """Checks that source files are in the GN and Bazel builds.
+def check_bazel_build_for_files(
+ bazel_extensions_to_check: Container[str],
+ files: Iterable[Path],
+ bazel_dirs: Iterable[Path] = (),
+) -> List[Path]:
+ """Checks that source files are in the Bazel builds.
Args:
bazel_extensions_to_check: which file suffixes to look for in Bazel
- gn_extensions_to_check: which file suffixes to look for in GN
files: the files that should be checked
bazel_dirs: directories in which to run bazel query
- gn_dirs: (source_dir, output_dir) tuples with which to run gn desc
- gn_build_files: paths to BUILD.gn files to directly search for paths
Returns:
- a dictionary mapping build system ('Bazel' or 'GN' to a list of missing
- files; will be empty if there were no missing files
+ a list of missing files; will be empty if there were no missing files
"""
# Collect all paths in the Bazel builds.
bazel_builds: Set[Path] = set()
for directory in bazel_dirs:
bazel_builds.update(
- _get_paths_from_command(directory, 'bazel', 'query',
- 'kind("source file", //...:*)'))
+ _get_paths_from_command(
+ directory, 'bazel', 'query', 'kind("source file", //...:*)'
+ )
+ )
+
+ missing: List[Path] = []
+
+ if bazel_dirs:
+ for path in (p for p in files if p.suffix in bazel_extensions_to_check):
+ if path not in bazel_builds:
+ # TODO(b/234883555) Replace this workaround for fuzzers.
+ if 'fuzz' not in str(path):
+ missing.append(path)
+
+ if missing:
+ _LOG.warning(
+ '%s missing from the Bazel build:\n%s',
+ plural(missing, 'file', are=True),
+ '\n'.join(str(x) for x in missing),
+ )
+
+ return missing
+
+
+def check_gn_build_for_files(
+ gn_extensions_to_check: Container[str],
+ files: Iterable[Path],
+ gn_dirs: Iterable[Tuple[Path, Path]] = (),
+ gn_build_files: Iterable[Path] = (),
+) -> List[Path]:
+ """Checks that source files are in the GN build.
+
+ Args:
+ gn_extensions_to_check: which file suffixes to look for in GN
+ files: the files that should be checked
+ gn_dirs: (source_dir, output_dir) tuples with which to run gn desc
+ gn_build_files: paths to BUILD.gn files to directly search for paths
+
+ Returns:
+ a list of missing files; will be empty if there were no missing files
+ """
# Collect all paths in GN builds.
gn_builds: Set[Path] = set()
for source_dir, output_dir in gn_dirs:
gn_builds.update(
- _get_paths_from_command(source_dir, 'gn', 'desc', output_dir, '*'))
+ _get_paths_from_command(source_dir, 'gn', 'desc', output_dir, '*')
+ )
gn_builds.update(_search_files_for_paths(gn_build_files))
- missing: Dict[str, List[Path]] = collections.defaultdict(list)
-
- if bazel_dirs:
- for path in (p for p in files
- if p.suffix in bazel_extensions_to_check):
- if path not in bazel_builds:
- # TODO(pwbug/176) Replace this workaround for fuzzers.
- if 'fuzz' not in str(path):
- missing['Bazel'].append(path)
+ missing: List[Path] = []
if gn_dirs or gn_build_files:
for path in (p for p in files if p.suffix in gn_extensions_to_check):
if path not in gn_builds:
- missing['GN'].append(path)
+ missing.append(path)
- for builder, paths in missing.items():
- _LOG.warning('%s missing from the %s build:\n%s',
- plural(paths, 'file', are=True), builder,
- '\n'.join(str(x) for x in paths))
+ if missing:
+ _LOG.warning(
+ '%s missing from the GN build:\n%s',
+ plural(missing, 'file', are=True),
+ '\n'.join(str(x) for x in missing),
+ )
return missing
+def check_builds_for_files(
+ bazel_extensions_to_check: Container[str],
+ gn_extensions_to_check: Container[str],
+ files: Iterable[Path],
+ bazel_dirs: Iterable[Path] = (),
+ gn_dirs: Iterable[Tuple[Path, Path]] = (),
+ gn_build_files: Iterable[Path] = (),
+) -> Dict[str, List[Path]]:
+ """Checks that source files are in the GN and Bazel builds.
+
+ Args:
+ bazel_extensions_to_check: which file suffixes to look for in Bazel
+ gn_extensions_to_check: which file suffixes to look for in GN
+ files: the files that should be checked
+ bazel_dirs: directories in which to run bazel query
+ gn_dirs: (source_dir, output_dir) tuples with which to run gn desc
+ gn_build_files: paths to BUILD.gn files to directly search for paths
+
+ Returns:
+ a dictionary mapping build system ('Bazel' or 'GN' to a list of missing
+ files; will be empty if there were no missing files
+ """
+
+ bazel_missing = check_bazel_build_for_files(
+ bazel_extensions_to_check=bazel_extensions_to_check,
+ files=files,
+ bazel_dirs=bazel_dirs,
+ )
+ gn_missing = check_gn_build_for_files(
+ gn_extensions_to_check=gn_extensions_to_check,
+ files=files,
+ gn_dirs=gn_dirs,
+ gn_build_files=gn_build_files,
+ )
+
+ result = {}
+ if bazel_missing:
+ result['Bazel'] = bazel_missing
+ if gn_missing:
+ result['GN'] = gn_missing
+ return result
+
+
@contextlib.contextmanager
def test_server(executable: str, output_dir: Path):
"""Context manager that runs a test server executable.
@@ -350,10 +520,12 @@ def test_server(executable: str, output_dir: Path):
yield
finally:
- proc.terminate()
+ proc.terminate() # pylint: disable=used-before-assignment
-@filter_paths(endswith=('.bzl', '.bazel'))
+@filter_paths(
+ file_filter=FileFilter(endswith=('.bzl', '.bazel'), name=('WORKSPACE',))
+)
def bazel_lint(ctx: PresubmitContext):
"""Runs buildifier with lint on Bazel files.
@@ -375,5 +547,131 @@ def bazel_lint(ctx: PresubmitContext):
@Check
def gn_gen_check(ctx: PresubmitContext):
"""Runs gn gen --check to enforce correct header dependencies."""
- pw_project_root = Path(os.environ['PW_PROJECT_ROOT'])
- gn_gen(pw_project_root, ctx.output_dir, gn_check=True)
+ gn_gen(ctx, gn_check=True)
+
+
+_CtxMgrLambda = Callable[[PresubmitContext], ContextManager]
+_CtxMgrOrLambda = Union[ContextManager, _CtxMgrLambda]
+
+
+class GnGenNinja(Check):
+ """Thin wrapper of Check for steps that just call gn/ninja."""
+
+ def __init__(
+ self,
+ *args,
+ packages: Sequence[str] = (),
+ gn_args: Optional[ # pylint: disable=redefined-outer-name
+ Dict[str, Any]
+ ] = None,
+ ninja_contexts: Sequence[_CtxMgrOrLambda] = (),
+ ninja_targets: Union[str, Sequence[str], Sequence[Sequence[str]]] = (),
+ **kwargs,
+ ):
+ """Initializes a GnGenNinja object.
+
+ Args:
+ *args: Passed on to superclass.
+ packages: List of 'pw package' packages to install.
+ gn_args: Dict of GN args.
+ ninja_contexts: List of context managers to apply around ninja
+ calls.
+ ninja_targets: Single ninja target, list of Ninja targets, or list
+ of list of ninja targets. If a list of a list, ninja will be
+ called multiple times with the same build directory.
+ **kwargs: Passed on to superclass.
+ """
+ super().__init__(self._substeps(), *args, **kwargs)
+ self.packages: Sequence[str] = packages
+ self.gn_args: Dict[str, Any] = gn_args or {}
+ self.ninja_contexts: Tuple[_CtxMgrOrLambda, ...] = tuple(ninja_contexts)
+
+ if isinstance(ninja_targets, str):
+ ninja_targets = (ninja_targets,)
+ ninja_targets = list(ninja_targets)
+ all_strings = all(isinstance(x, str) for x in ninja_targets)
+ any_strings = any(isinstance(x, str) for x in ninja_targets)
+ if ninja_targets and all_strings != any_strings:
+ raise ValueError(repr(ninja_targets))
+
+ self.ninja_target_lists: Tuple[Tuple[str, ...], ...]
+ if all_strings:
+ targets: List[str] = []
+ for target in ninja_targets:
+ targets.append(target) # type: ignore
+ self.ninja_target_lists = (tuple(targets),)
+ else:
+ self.ninja_target_lists = tuple(tuple(x) for x in ninja_targets)
+
+ def _install_package( # pylint: disable=no-self-use
+ self,
+ ctx: PresubmitContext,
+ package: str,
+ ) -> PresubmitResult:
+ install_package(ctx, package)
+ return PresubmitResult.PASS
+
+ def _gn_gen(self, ctx: PresubmitContext) -> PresubmitResult:
+ Item = Union[int, str]
+ Value = Union[Item, Sequence[Item]]
+ ValueCallable = Callable[[PresubmitContext], Value]
+ InputItem = Union[Item, ValueCallable]
+ InputValue = Union[InputItem, Sequence[InputItem]]
+
+ # TODO(mohrr) Use typing.TypeGuard instead of "type: ignore"
+
+ def value(val: InputValue) -> Value:
+ if isinstance(val, (str, int)):
+ return val
+ if callable(val):
+ return val(ctx)
+
+ result: List[Item] = []
+ for item in val:
+ if callable(item):
+ call_result = item(ctx)
+ if isinstance(item, (int, str)):
+ result.append(call_result)
+ else: # Sequence.
+ result.extend(call_result) # type: ignore
+ elif isinstance(item, (int, str)):
+ result.append(item)
+ else: # Sequence.
+ result.extend(item)
+ return result
+
+ args = {k: value(v) for k, v in self.gn_args.items()}
+ gn_gen(ctx, **args) # type: ignore
+ return PresubmitResult.PASS
+
+ def _ninja(
+ self, ctx: PresubmitContext, targets: Sequence[str]
+ ) -> PresubmitResult:
+ with contextlib.ExitStack() as stack:
+ for mgr in self.ninja_contexts:
+ if isinstance(mgr, contextlib.AbstractContextManager):
+ stack.enter_context(mgr)
+ else:
+ stack.enter_context(mgr(ctx)) # type: ignore
+ ninja(ctx, *targets)
+ return PresubmitResult.PASS
+
+ def _substeps(self) -> Iterator[SubStep]:
+ for package in self.packages:
+ yield SubStep(
+ f'install {package} package',
+ self._install_package,
+ (package,),
+ )
+
+ yield SubStep('gn gen', self._gn_gen)
+
+ targets_parts = set()
+ for targets in self.ninja_target_lists:
+ targets_part = " ".join(targets)
+ maxlen = 70
+ if len(targets_part) > maxlen:
+ targets_part = f'{targets_part[0:maxlen-3]}...'
+ assert targets_part not in targets_parts
+ targets_parts.add(targets_part)
+ yield SubStep(f'ninja {targets_part}', self._ninja, (targets,))
diff --git a/pw_presubmit/py/pw_presubmit/cli.py b/pw_presubmit/py/pw_presubmit/cli.py
index c03f5cba2..d29dd1606 100644
--- a/pw_presubmit/py/pw_presubmit/cli.py
+++ b/pw_presubmit/py/pw_presubmit/cli.py
@@ -15,10 +15,12 @@
import argparse
import logging
+import os
from pathlib import Path
import re
import shutil
-from typing import Callable, Collection, Optional, Sequence
+import textwrap
+from typing import Callable, Collection, List, Optional, Sequence
from pw_presubmit import git_repo, presubmit
@@ -39,9 +41,12 @@ def add_path_arguments(parser) -> None:
'paths',
metavar='pathspec',
nargs='*',
- help=('Paths or patterns to which to restrict the checks. These are '
- 'interpreted as Git pathspecs. If --base is provided, only '
- 'paths changed since that commit are checked.'))
+ help=(
+ 'Paths or patterns to which to restrict the checks. These are '
+ 'interpreted as Git pathspecs. If --base is provided, only '
+ 'paths changed since that commit are checked.'
+ ),
+ )
base = parser.add_mutually_exclusive_group()
base.add_argument(
@@ -49,14 +54,20 @@ def add_path_arguments(parser) -> None:
'--base',
metavar='commit',
default=git_repo.TRACKING_BRANCH_ALIAS,
- help=('Git revision against which to diff for changed files. '
- 'Default is the tracking branch of the current branch.'))
+ help=(
+ 'Git revision against which to diff for changed files. '
+ 'Default is the tracking branch of the current branch.'
+ ),
+ )
+
base.add_argument(
+ '--all',
'--full',
dest='base',
action='store_const',
const=None,
- help='Run presubmit on all files, not just changed files.')
+ help='Run actions for all files, not just changed files.',
+ )
parser.add_argument(
'-e',
@@ -65,61 +76,138 @@ def add_path_arguments(parser) -> None:
default=[],
action='append',
type=re.compile,
- help=('Exclude paths matching any of these regular expressions, '
- "which are interpreted relative to each Git repository's root."))
+ help=(
+ 'Exclude paths matching any of these regular expressions, '
+ "which are interpreted relative to each Git repository's root."
+ ),
+ )
-def _add_programs_arguments(exclusive: argparse.ArgumentParser,
- programs: presubmit.Programs, default: str):
+def _add_programs_arguments(
+ parser: argparse.ArgumentParser, programs: presubmit.Programs, default: str
+):
def presubmit_program(arg: str) -> presubmit.Program:
if arg not in programs:
+ all_program_names = ', '.join(sorted(programs.keys()))
raise argparse.ArgumentTypeError(
- f'{arg} is not the name of a presubmit program')
+ f'{arg} is not the name of a presubmit program\n\n'
+ f'Valid Programs:\n{all_program_names}'
+ )
return programs[arg]
- exclusive.add_argument('-p',
- '--program',
- choices=programs.values(),
- type=presubmit_program,
- default=default,
- help='Which presubmit program to run')
+ # This argument is used to copy the default program into the argparse
+ # namespace argument. It's not intended to be set by users.
+ parser.add_argument(
+ '--default-program',
+ default=[presubmit_program(default)],
+ help=argparse.SUPPRESS,
+ )
+
+ parser.add_argument(
+ '-p',
+ '--program',
+ choices=programs.values(),
+ type=presubmit_program,
+ action='append',
+ default=[],
+ help='Which presubmit program to run',
+ )
+
+ parser.add_argument(
+ '--list-steps-file',
+ dest='list_steps_file',
+ type=Path,
+ help=argparse.SUPPRESS,
+ )
all_steps = programs.all_steps()
- # The step argument appends steps to a program. No "step" argument is
- # created on the resulting argparse.Namespace.
- class AddToCustomProgram(argparse.Action):
- def __call__(self, parser, namespace, values, unused_option=None):
- if not isinstance(namespace.program, list):
- namespace.program = []
+ def list_steps() -> None:
+ """List all available presubmit steps and their docstrings."""
+ for step in sorted(all_steps.values(), key=str):
+ _LOG.info('%s', step)
+ if step.doc:
+ first, *rest = step.doc.split('\n', 1)
+ _LOG.info(' %s', first)
+ if rest and _LOG.isEnabledFor(logging.DEBUG):
+ for line in textwrap.dedent(*rest).splitlines():
+ _LOG.debug(' %s', line)
- if values not in all_steps:
- raise parser.error(
- f'argument --step: {values} is not the name of a '
- 'presubmit check\n\nValid values for --step:\n'
- f'{{{",".join(sorted(all_steps))}}}')
+ parser.add_argument(
+ '--list-steps',
+ action='store_const',
+ const=list_steps,
+ default=None,
+ help='List all the available steps.',
+ )
- namespace.program.append(all_steps[values])
+ def presubmit_step(arg: str) -> presubmit.Check:
+ if arg not in all_steps:
+ all_step_names = ', '.join(sorted(all_steps.keys()))
+ raise argparse.ArgumentTypeError(
+ f'{arg} is not the name of a presubmit step\n\n'
+ f'Valid Steps:\n{all_step_names}'
+ )
+ return all_steps[arg]
- exclusive.add_argument(
+ parser.add_argument(
'--step',
- action=AddToCustomProgram,
- default=argparse.SUPPRESS, # Don't create a "step" argument.
- help='Provide explicit steps instead of running a predefined program.',
+ action='append',
+ choices=all_steps.values(),
+ default=[],
+ help='Run specific steps instead of running a full program.',
+ type=presubmit_step,
+ )
+
+ parser.add_argument(
+ '--substep',
+ action='store',
+ help=(
+ "Run a specific substep of a step. Only supported if there's only "
+ 'one --step argument and no --program arguments.'
+ ),
+ )
+
+ def gn_arg(argument):
+ key, value = argument.split('=', 1)
+ return (key, value)
+
+ # Recipe code for handling builds with pre-release toolchains requires the
+ # ability to pass through GN args. This ability is not expected to be used
+ # directly outside of this case, so the option is hidden. Values passed in
+ # to this argument should be of the form 'key=value'.
+ parser.add_argument(
+ '--override-gn-arg',
+ dest='override_gn_args',
+ action='append',
+ type=gn_arg,
+ help=argparse.SUPPRESS,
)
-def add_arguments(parser: argparse.ArgumentParser,
- programs: Optional[presubmit.Programs] = None,
- default: str = '') -> None:
+def add_arguments(
+ parser: argparse.ArgumentParser,
+ programs: Optional[presubmit.Programs] = None,
+ default: str = '',
+) -> None:
"""Adds common presubmit check options to an argument parser."""
add_path_arguments(parser)
- parser.add_argument('-k',
- '--keep-going',
- action='store_true',
- help='Continue instead of aborting when errors occur.')
+ parser.add_argument(
+ '-k',
+ '--keep-going',
+ action='store_true',
+ help='Continue running presubmit steps after a failure.',
+ )
+ parser.add_argument(
+ '--continue-after-build-error',
+ action='store_true',
+ help=(
+ 'Within presubmit steps, continue running build steps after a '
+ 'failure.'
+ ),
+ )
parser.add_argument(
'--output-directory',
type=Path,
@@ -128,7 +216,7 @@ def add_arguments(parser: argparse.ArgumentParser,
parser.add_argument(
'--package-root',
type=Path,
- help='Package root directory (default: <output directory>/packages)',
+ help='Package root directory (default: <env directory>/packages)',
)
exclusive = parser.add_mutually_exclusive_group()
@@ -154,20 +242,27 @@ def add_arguments(parser: argparse.ArgumentParser,
)
-def run(
- program: Sequence[Callable],
+def run( # pylint: disable=too-many-arguments
+ default_program: Optional[presubmit.Program],
+ program: Sequence[presubmit.Program],
+ step: Sequence[presubmit.Check],
+ substep: str,
output_directory: Optional[Path],
package_root: Path,
clear: bool,
- root: Path = None,
+ root: Optional[Path] = None,
repositories: Collection[Path] = (),
only_list_steps=False,
+ list_steps: Optional[Callable[[], None]] = None,
**other_args,
) -> int:
"""Processes arguments from add_arguments and runs the presubmit.
Args:
+ default_program: program to use if neither --program nor --step is used
program: from the --program option
+ step: from the --step option
+ substep: from the --substep option
output_directory: from --output-directory option
package_root: from --package-root option
clear: from the --clear option
@@ -177,10 +272,11 @@ def run(
defaults to the root of the current directory's repository
only_list_steps: list the steps that would be executed, one per line,
instead of executing them
+ list_steps: list the steps that would be executed with their docstrings
**other_args: remaining arguments defined by by add_arguments
Returns:
- exit code for sys.exit; 0 if succesful, 1 if an error occurred
+ exit code for sys.exit; 0 if successful, 1 if an error occurred
"""
if root is None:
root = git_repo.root()
@@ -193,10 +289,11 @@ def run(
output_directory.mkdir(parents=True, exist_ok=True)
output_directory.joinpath('README.txt').write_text(
- _OUTPUT_PATH_README.format(repo=root))
+ _OUTPUT_PATH_README.format(repo=root)
+ )
if not package_root:
- package_root = output_directory / 'packages'
+ package_root = Path(os.environ['PW_PACKAGE_ROOT'])
_LOG.debug('Using environment at %s', output_directory)
@@ -209,13 +306,37 @@ def run(
return 0
- if presubmit.run(program,
- root,
- repositories,
- only_list_steps=only_list_steps,
- output_directory=output_directory,
- package_root=package_root,
- **other_args):
+ if list_steps:
+ list_steps()
+ return 0
+
+ final_program: Optional[presubmit.Program] = None
+ if not program and not step:
+ assert default_program # Cast away Optional[].
+ final_program = default_program
+ elif len(program) == 1 and not step:
+ final_program = program[0]
+ else:
+ steps: List[presubmit.Check] = []
+ steps.extend(step)
+ for prog in program:
+ steps.extend(prog)
+ final_program = presubmit.Program('', steps)
+
+ if substep and len(final_program) > 1:
+ _LOG.error('--substep not supported if there are multiple steps')
+ return 1
+
+ if presubmit.run(
+ final_program,
+ root,
+ repositories,
+ only_list_steps=only_list_steps,
+ output_directory=output_directory,
+ package_root=package_root,
+ substep=substep,
+ **other_args,
+ ):
return 0
return 1
diff --git a/pw_presubmit/py/pw_presubmit/cpp_checks.py b/pw_presubmit/py/pw_presubmit/cpp_checks.py
index 5e406b901..849242455 100644
--- a/pw_presubmit/py/pw_presubmit/cpp_checks.py
+++ b/pw_presubmit/py/pw_presubmit/cpp_checks.py
@@ -13,58 +13,69 @@
# the License.
"""C++-related checks."""
+import logging
+
from pw_presubmit import (
build,
Check,
format_code,
PresubmitContext,
- PresubmitFailure,
filter_paths,
)
+_LOG: logging.Logger = logging.getLogger(__name__)
+
-@filter_paths(endswith=format_code.CPP_HEADER_EXTS, exclude=(r'\.pb\.h$', ))
+@filter_paths(endswith=format_code.CPP_HEADER_EXTS, exclude=(r'\.pb\.h$',))
def pragma_once(ctx: PresubmitContext) -> None:
"""Presubmit check that ensures all header files contain '#pragma once'."""
for path in ctx.paths:
+ _LOG.debug('Checking %s', path)
with open(path) as file:
for line in file:
if line.startswith('#pragma once'):
break
else:
- raise PresubmitFailure('#pragma once is missing!', path=path)
+ ctx.fail('#pragma once is missing!', path=path)
@Check
def asan(ctx: PresubmitContext) -> None:
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'asan')
+ """Test with the address sanitizer."""
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'asan')
@Check
def msan(ctx: PresubmitContext) -> None:
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'msan')
+ """Test with the memory sanitizer."""
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'msan')
@Check
def tsan(ctx: PresubmitContext) -> None:
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'tsan')
+ """Test with the thread sanitizer."""
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'tsan')
@Check
def ubsan(ctx: PresubmitContext) -> None:
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'ubsan')
+ """Test with the undefined behavior sanitizer."""
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'ubsan')
@Check
def runtime_sanitizers(ctx: PresubmitContext) -> None:
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'runtime_sanitizers')
+ """Test with the address, thread, and undefined behavior sanitizers."""
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'runtime_sanitizers')
def all_sanitizers():
- return [asan, msan, tsan, ubsan, runtime_sanitizers]
+ # TODO(b/234876100): msan will not work until the C++ standard library
+ # included in the sysroot has a variant built with msan.
+ return [asan, tsan, ubsan, runtime_sanitizers]
diff --git a/pw_presubmit/py/pw_presubmit/format_code.py b/pw_presubmit/py/pw_presubmit/format_code.py
index 623b5444d..9b073ea05 100755
--- a/pw_presubmit/py/pw_presubmit/format_code.py
+++ b/pw_presubmit/py/pw_presubmit/format_code.py
@@ -29,34 +29,58 @@ import re
import subprocess
import sys
import tempfile
-from typing import Callable, Collection, Dict, Iterable, List, NamedTuple
-from typing import Optional, Pattern, Tuple, Union
+from typing import (
+ Callable,
+ Collection,
+ Dict,
+ Iterable,
+ List,
+ NamedTuple,
+ Optional,
+ Pattern,
+ Sequence,
+ TextIO,
+ Tuple,
+ Union,
+)
try:
import pw_presubmit
except ImportError:
# Append the pw_presubmit package path to the module search path to allow
# running this module without installing the pw_presubmit package.
- sys.path.append(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__))))
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pw_presubmit
+import pw_cli.color
import pw_cli.env
-from pw_presubmit import cli, git_repo
+from pw_presubmit.presubmit import FileFilter
+from pw_presubmit import (
+ cli,
+ FormatContext,
+ FormatOptions,
+ git_repo,
+ owners_checks,
+ PresubmitContext,
+)
from pw_presubmit.tools import exclude_paths, file_summary, log_run, plural
_LOG: logging.Logger = logging.getLogger(__name__)
+_COLOR = pw_cli.color.colors()
+_DEFAULT_PATH = Path('out', 'format')
+
+_Context = Union[PresubmitContext, FormatContext]
def _colorize_diff_line(line: str) -> str:
if line.startswith('--- ') or line.startswith('+++ '):
- return pw_presubmit.color_bold_white(line)
+ return _COLOR.bold_white(line)
if line.startswith('-'):
- return pw_presubmit.color_red(line)
+ return _COLOR.red(line)
if line.startswith('+'):
- return pw_presubmit.color_green(line)
+ return _COLOR.green(line)
if line.startswith('@@ '):
- return pw_presubmit.color_aqua(line)
+ return _COLOR.cyan(line)
return line
@@ -69,11 +93,14 @@ def colorize_diff(lines: Iterable[str]) -> str:
def _diff(path, original: bytes, formatted: bytes) -> str:
- return colorize_diff(
+ return ''.join(
difflib.unified_diff(
original.decode(errors='replace').splitlines(True),
formatted.decode(errors='replace').splitlines(True),
- f'{path} (original)', f'{path} (reformatted)'))
+ f'{path} (original)',
+ f'{path} (reformatted)',
+ )
+ )
Formatter = Callable[[str, bytes], bytes]
@@ -100,40 +127,46 @@ def _check_files(files, formatter: Formatter) -> Dict[Path, str]:
return errors
-def _clang_format(*args: str, **kwargs) -> bytes:
- return log_run(['clang-format', '--style=file', *args],
- stdout=subprocess.PIPE,
- check=True,
- **kwargs).stdout
+def _clang_format(*args: Union[Path, str], **kwargs) -> bytes:
+ return log_run(
+ ['clang-format', '--style=file', *args],
+ stdout=subprocess.PIPE,
+ check=True,
+ **kwargs,
+ ).stdout
-def clang_format_check(files: Iterable[Path]) -> Dict[Path, str]:
+def clang_format_check(ctx: _Context) -> Dict[Path, str]:
"""Checks formatting; returns {path: diff} for files with bad formatting."""
- return _check_files(files, lambda path, _: _clang_format(path))
+ return _check_files(ctx.paths, lambda path, _: _clang_format(path))
-def clang_format_fix(files: Iterable) -> Dict[Path, str]:
+def clang_format_fix(ctx: _Context) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
- _clang_format('-i', *files)
+ _clang_format('-i', *ctx.paths)
return {}
-def check_gn_format(files: Iterable[Path]) -> Dict[Path, str]:
+def check_gn_format(ctx: _Context) -> Dict[Path, str]:
"""Checks formatting; returns {path: diff} for files with bad formatting."""
return _check_files(
- files, lambda _, data: log_run(['gn', 'format', '--stdin'],
- input=data,
- stdout=subprocess.PIPE,
- check=True).stdout)
+ ctx.paths,
+ lambda _, data: log_run(
+ ['gn', 'format', '--stdin'],
+ input=data,
+ stdout=subprocess.PIPE,
+ check=True,
+ ).stdout,
+ )
-def fix_gn_format(files: Iterable[Path]) -> Dict[Path, str]:
+def fix_gn_format(ctx: _Context) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
- log_run(['gn', 'format', *files], check=True)
+ log_run(['gn', 'format', *ctx.paths], check=True)
return {}
-def check_bazel_format(files: Iterable[Path]) -> Dict[Path, str]:
+def check_bazel_format(ctx: _Context) -> Dict[Path, str]:
"""Checks formatting; returns {path: diff} for files with bad formatting."""
errors: Dict[Path, str] = {}
@@ -141,7 +174,7 @@ def check_bazel_format(files: Iterable[Path]) -> Dict[Path, str]:
# buildifier doesn't have an option to output the changed file, so
# copy the file to a temp location, run buildifier on it, read that
# modified copy, and return its contents.
- with tempfile.TemporaryDirectory() as temp:
+ with tempfile.TemporaryDirectory(dir=ctx.output_dir) as temp:
build = Path(temp) / os.path.basename(path)
build.write_bytes(data)
@@ -152,46 +185,60 @@ def check_bazel_format(files: Iterable[Path]) -> Dict[Path, str]:
errors[Path(path)] = stderr
return build.read_bytes()
- result = _check_files(files, _format_temp)
+ result = _check_files(ctx.paths, _format_temp)
result.update(errors)
return result
-def fix_bazel_format(files: Iterable[Path]) -> Dict[Path, str]:
+def fix_bazel_format(ctx: _Context) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
errors = {}
- for path in files:
+ for path in ctx.paths:
proc = log_run(['buildifier', path], capture_output=True)
if proc.returncode:
errors[path] = proc.stderr.decode()
return errors
-def check_go_format(files: Iterable[Path]) -> Dict[Path, str]:
+def check_owners_format(ctx: _Context) -> Dict[Path, str]:
+ return owners_checks.run_owners_checks(ctx.paths)
+
+
+def fix_owners_format(ctx: _Context) -> Dict[Path, str]:
+ return owners_checks.format_owners_file(ctx.paths)
+
+
+def check_go_format(ctx: _Context) -> Dict[Path, str]:
"""Checks formatting; returns {path: diff} for files with bad formatting."""
return _check_files(
- files, lambda path, _: log_run(
- ['gofmt', path], stdout=subprocess.PIPE, check=True).stdout)
+ ctx.paths,
+ lambda path, _: log_run(
+ ['gofmt', path], stdout=subprocess.PIPE, check=True
+ ).stdout,
+ )
-def fix_go_format(files: Iterable[Path]) -> Dict[Path, str]:
+def fix_go_format(ctx: _Context) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
- log_run(['gofmt', '-w', *files], check=True)
+ log_run(['gofmt', '-w', *ctx.paths], check=True)
return {}
+# TODO(b/259595799) Remove yapf support.
def _yapf(*args, **kwargs) -> subprocess.CompletedProcess:
- return log_run(['python', '-m', 'yapf', '--parallel', *args],
- capture_output=True,
- **kwargs)
+ return log_run(
+ ['python', '-m', 'yapf', '--parallel', *args],
+ capture_output=True,
+ **kwargs,
+ )
_DIFF_START = re.compile(r'^--- (.*)\s+\(original\)$', flags=re.MULTILINE)
-def check_py_format(files: Iterable[Path]) -> Dict[Path, str]:
+def check_py_format_yapf(ctx: _Context) -> Dict[Path, str]:
"""Checks formatting; returns {path: diff} for files with bad formatting."""
- process = _yapf('--diff', *files)
+ process = _yapf('--diff', *ctx.paths)
errors: Dict[Path, str] = {}
@@ -200,23 +247,136 @@ def check_py_format(files: Iterable[Path]) -> Dict[Path, str]:
matches = tuple(_DIFF_START.finditer(raw_diff))
for start, end in zip(matches, (*matches[1:], None)):
- errors[Path(start.group(1))] = colorize_diff(
- raw_diff[start.start():end.start() if end else None])
+ errors[Path(start.group(1))] = raw_diff[
+ start.start() : end.start() if end else None
+ ]
if process.stderr:
- _LOG.error('yapf encountered an error:\n%s',
- process.stderr.decode(errors='replace').rstrip())
- errors.update({file: '' for file in files if file not in errors})
+ _LOG.error(
+ 'yapf encountered an error:\n%s',
+ process.stderr.decode(errors='replace').rstrip(),
+ )
+ errors.update({file: '' for file in ctx.paths if file not in errors})
return errors
-def fix_py_format(files: Iterable) -> Dict[Path, str]:
+def fix_py_format_yapf(ctx: _Context) -> Dict[Path, str]:
"""Fixes formatting for the provided files in place."""
- _yapf('--in-place', *files, check=True)
+ _yapf('--in-place', *ctx.paths, check=True)
return {}
+def _enumerate_black_configs() -> Iterable[Path]:
+ if directory := os.environ.get('PW_PROJECT_ROOT'):
+ yield Path(directory, '.black.toml')
+ yield Path(directory, 'pyproject.toml')
+
+ if directory := os.environ.get('PW_ROOT'):
+ yield Path(directory, '.black.toml')
+ yield Path(directory, 'pyproject.toml')
+
+
+def _black_config_args() -> Sequence[Union[str, Path]]:
+ config = None
+ for config_location in _enumerate_black_configs():
+ if config_location.is_file():
+ config = config_location
+ break
+
+ config_args: Sequence[Union[str, Path]] = ()
+ if config:
+ config_args = ('--config', config)
+ return config_args
+
+
+def _black_multiple_files(ctx: _Context) -> Tuple[str, ...]:
+ black = ctx.format_options.black_path
+ changed_paths: List[str] = []
+ for line in (
+ log_run(
+ [black, '--check', *_black_config_args(), *ctx.paths],
+ capture_output=True,
+ )
+ .stderr.decode()
+ .splitlines()
+ ):
+ if match := re.search(r'^would reformat (.*)\s*$', line):
+ changed_paths.append(match.group(1))
+ return tuple(changed_paths)
+
+
+def check_py_format_black(ctx: _Context) -> Dict[Path, str]:
+ """Checks formatting; returns {path: diff} for files with bad formatting."""
+ errors: Dict[Path, str] = {}
+
+ # Run black --check on the full list of paths and then only run black
+ # individually on the files that black found issue with.
+ paths: Tuple[str, ...] = _black_multiple_files(ctx)
+
+ def _format_temp(path: Union[Path, str], data: bytes) -> bytes:
+ # black doesn't have an option to output the changed file, so copy the
+ # file to a temp location, run buildifier on it, read that modified
+ # copy, and return its contents.
+ with tempfile.TemporaryDirectory(dir=ctx.output_dir) as temp:
+ build = Path(temp) / os.path.basename(path)
+ build.write_bytes(data)
+
+ proc = log_run(
+ [ctx.format_options.black_path, *_black_config_args(), build],
+ capture_output=True,
+ )
+ if proc.returncode:
+ stderr = proc.stderr.decode(errors='replace')
+ stderr = stderr.replace(str(build), str(path))
+ errors[Path(path)] = stderr
+ return build.read_bytes()
+
+ result = _check_files(
+ [x for x in ctx.paths if str(x).endswith(paths)],
+ _format_temp,
+ )
+ result.update(errors)
+ return result
+
+
+def fix_py_format_black(ctx: _Context) -> Dict[Path, str]:
+ """Fixes formatting for the provided files in place."""
+ errors: Dict[Path, str] = {}
+
+ # Run black --check on the full list of paths and then only run black
+ # individually on the files that black found issue with.
+ paths: Tuple[str, ...] = _black_multiple_files(ctx)
+
+ for path in ctx.paths:
+ if not str(path).endswith(paths):
+ continue
+
+ proc = log_run(
+ [ctx.format_options.black_path, *_black_config_args(), path],
+ capture_output=True,
+ )
+ if proc.returncode:
+ errors[path] = proc.stderr.decode()
+ return errors
+
+
+def check_py_format(ctx: _Context) -> Dict[Path, str]:
+ if ctx.format_options.python_formatter == 'black':
+ return check_py_format_black(ctx)
+ if ctx.format_options.python_formatter == 'yapf':
+ return check_py_format_yapf(ctx)
+ raise ValueError(ctx.format_options.python_formatter)
+
+
+def fix_py_format(ctx: _Context) -> Dict[Path, str]:
+ if ctx.format_options.python_formatter == 'black':
+ return fix_py_format_black(ctx)
+ if ctx.format_options.python_formatter == 'yapf':
+ return fix_py_format_yapf(ctx)
+ raise ValueError(ctx.format_options.python_formatter)
+
+
_TRAILING_SPACE = re.compile(rb'[ \t]+$', flags=re.MULTILINE)
@@ -239,147 +399,280 @@ def _check_trailing_space(paths: Iterable[Path], fix: bool) -> Dict[Path, str]:
return errors
-def check_trailing_space(files: Iterable[Path]) -> Dict[Path, str]:
- return _check_trailing_space(files, fix=False)
+def check_trailing_space(ctx: _Context) -> Dict[Path, str]:
+ return _check_trailing_space(ctx.paths, fix=False)
-def fix_trailing_space(files: Iterable[Path]) -> Dict[Path, str]:
- _check_trailing_space(files, fix=True)
+def fix_trailing_space(ctx: _Context) -> Dict[Path, str]:
+ _check_trailing_space(ctx.paths, fix=True)
return {}
-def print_format_check(errors: Dict[Path, str],
- show_fix_commands: bool) -> None:
+def print_format_check(
+ errors: Dict[Path, str],
+ show_fix_commands: bool,
+ show_summary: bool = True,
+ colors: Optional[bool] = None,
+ file: TextIO = sys.stdout,
+) -> None:
"""Prints and returns the result of a check_*_format function."""
if not errors:
# Don't print anything in the all-good case.
return
+ if colors is None:
+ colors = file == sys.stdout
+
# Show the format fixing diff suggested by the tooling (with colors).
- _LOG.warning('Found %d files with formatting errors. Format changes:',
- len(errors))
+ if show_summary:
+ _LOG.warning(
+ 'Found %d files with formatting errors. Format changes:',
+ len(errors),
+ )
for diff in errors.values():
- print(diff, end='')
+ if colors:
+ diff = colorize_diff(diff)
+ print(diff, end='', file=file)
# Show a copy-and-pastable command to fix the issues.
if show_fix_commands:
- def path_relative_to_cwd(path):
+ def path_relative_to_cwd(path: Path):
try:
return Path(path).resolve().relative_to(Path.cwd().resolve())
except ValueError:
return Path(path).resolve()
- message = (f' pw format --fix {path_relative_to_cwd(path)}'
- for path in errors)
+ message = (
+ f' pw format --fix {path_relative_to_cwd(path)}' for path in errors
+ )
_LOG.warning('To fix formatting, run:\n\n%s\n', '\n'.join(message))
class CodeFormat(NamedTuple):
language: str
- extensions: Collection[str]
- exclude: Collection[str]
- check: Callable[[Iterable], Dict[Path, str]]
- fix: Callable[[Iterable], Dict[Path, str]]
+ filter: FileFilter
+ check: Callable[[_Context], Dict[Path, str]]
+ fix: Callable[[_Context], Dict[Path, str]]
+
+ @property
+ def extensions(self):
+ # TODO(b/23842636): Switch calls of this to using 'filter' and remove.
+ return self.filter.endswith
-CPP_HEADER_EXTS = frozenset(
- ('.h', '.hpp', '.hxx', '.h++', '.hh', '.H', '.inc', '.inl'))
-CPP_SOURCE_EXTS = frozenset(('.c', '.cpp', '.cxx', '.c++', '.cc', '.C'))
+CPP_HEADER_EXTS = frozenset(('.h', '.hpp', '.hxx', '.h++', '.hh', '.H'))
+CPP_SOURCE_EXTS = frozenset(
+ ('.c', '.cpp', '.cxx', '.c++', '.cc', '.C', '.inc', '.inl')
+)
CPP_EXTS = CPP_HEADER_EXTS.union(CPP_SOURCE_EXTS)
+CPP_FILE_FILTER = FileFilter(
+ endswith=CPP_EXTS, exclude=(r'\.pb\.h$', r'\.pb\.c$')
+)
+
+C_FORMAT = CodeFormat(
+ 'C and C++', CPP_FILE_FILTER, clang_format_check, clang_format_fix
+)
-C_FORMAT: CodeFormat = CodeFormat('C and C++', CPP_EXTS,
- (r'\.pb\.h$', r'\.pb\.c$'),
- clang_format_check, clang_format_fix)
+PROTO_FORMAT: CodeFormat = CodeFormat(
+ 'Protocol buffer',
+ FileFilter(endswith=('.proto',)),
+ clang_format_check,
+ clang_format_fix,
+)
+
+JAVA_FORMAT: CodeFormat = CodeFormat(
+ 'Java',
+ FileFilter(endswith=('.java',)),
+ clang_format_check,
+ clang_format_fix,
+)
-PROTO_FORMAT: CodeFormat = CodeFormat('Protocol buffer', ('.proto', ), (),
- clang_format_check, clang_format_fix)
+JAVASCRIPT_FORMAT: CodeFormat = CodeFormat(
+ 'JavaScript',
+ FileFilter(endswith=('.js',)),
+ clang_format_check,
+ clang_format_fix,
+)
-JAVA_FORMAT: CodeFormat = CodeFormat('Java', ('.java', ), (),
- clang_format_check, clang_format_fix)
+GO_FORMAT: CodeFormat = CodeFormat(
+ 'Go', FileFilter(endswith=('.go',)), check_go_format, fix_go_format
+)
-JAVASCRIPT_FORMAT: CodeFormat = CodeFormat('JavaScript', ('.js', ), (),
- clang_format_check,
- clang_format_fix)
+PYTHON_FORMAT: CodeFormat = CodeFormat(
+ 'Python',
+ FileFilter(endswith=('.py',)),
+ check_py_format,
+ fix_py_format,
+)
-GO_FORMAT: CodeFormat = CodeFormat('Go', ('.go', ), (), check_go_format,
- fix_go_format)
+GN_FORMAT: CodeFormat = CodeFormat(
+ 'GN', FileFilter(endswith=('.gn', '.gni')), check_gn_format, fix_gn_format
+)
-PYTHON_FORMAT: CodeFormat = CodeFormat('Python', ('.py', ), (),
- check_py_format, fix_py_format)
+BAZEL_FORMAT: CodeFormat = CodeFormat(
+ 'Bazel',
+ FileFilter(endswith=('BUILD', '.bazel', '.bzl'), name=('WORKSPACE')),
+ check_bazel_format,
+ fix_bazel_format,
+)
-GN_FORMAT: CodeFormat = CodeFormat('GN', ('.gn', '.gni'), (), check_gn_format,
- fix_gn_format)
+COPYBARA_FORMAT: CodeFormat = CodeFormat(
+ 'Copybara',
+ FileFilter(endswith=('.bara.sky',)),
+ check_bazel_format,
+ fix_bazel_format,
+)
-# TODO(pwbug/191): Add real code formatting support for Bazel and CMake
-BAZEL_FORMAT: CodeFormat = CodeFormat('Bazel', ('BUILD', '.bazel', '.bzl'), (),
- check_bazel_format, fix_bazel_format)
+# TODO(b/234881054): Add real code formatting support for CMake
+CMAKE_FORMAT: CodeFormat = CodeFormat(
+ 'CMake',
+ FileFilter(endswith=('CMakeLists.txt', '.cmake')),
+ check_trailing_space,
+ fix_trailing_space,
+)
-CMAKE_FORMAT: CodeFormat = CodeFormat('CMake', ('CMakeLists.txt', '.cmake'),
- (), check_trailing_space,
- fix_trailing_space)
+RST_FORMAT: CodeFormat = CodeFormat(
+ 'reStructuredText',
+ FileFilter(endswith=('.rst',)),
+ check_trailing_space,
+ fix_trailing_space,
+)
-RST_FORMAT: CodeFormat = CodeFormat('reStructuredText', ('.rst', ), (),
- check_trailing_space, fix_trailing_space)
+MARKDOWN_FORMAT: CodeFormat = CodeFormat(
+ 'Markdown',
+ FileFilter(endswith=('.md',)),
+ check_trailing_space,
+ fix_trailing_space,
+)
-MARKDOWN_FORMAT: CodeFormat = CodeFormat('Markdown', ('.md', ), (),
- check_trailing_space,
- fix_trailing_space)
+OWNERS_CODE_FORMAT = CodeFormat(
+ 'OWNERS',
+ filter=FileFilter(name=('OWNERS',)),
+ check=check_owners_format,
+ fix=fix_owners_format,
+)
CODE_FORMATS: Tuple[CodeFormat, ...] = (
+ # keep-sorted: start
+ BAZEL_FORMAT,
+ CMAKE_FORMAT,
+ COPYBARA_FORMAT,
C_FORMAT,
- JAVA_FORMAT,
+ GN_FORMAT,
+ GO_FORMAT,
JAVASCRIPT_FORMAT,
+ JAVA_FORMAT,
+ MARKDOWN_FORMAT,
+ OWNERS_CODE_FORMAT,
PROTO_FORMAT,
- GO_FORMAT,
PYTHON_FORMAT,
- GN_FORMAT,
- BAZEL_FORMAT,
- CMAKE_FORMAT,
RST_FORMAT,
- MARKDOWN_FORMAT,
+ # keep-sorted: end
)
+# TODO(b/264578594) Remove these lines when these globals aren't referenced.
+CODE_FORMATS_WITH_BLACK: Tuple[CodeFormat, ...] = CODE_FORMATS
+CODE_FORMATS_WITH_YAPF: Tuple[CodeFormat, ...] = CODE_FORMATS
+
+
+def presubmit_check(
+ code_format: CodeFormat,
+ *,
+ exclude: Collection[Union[str, Pattern[str]]] = (),
+) -> Callable:
+ """Creates a presubmit check function from a CodeFormat object.
+
+ Args:
+ exclude: Additional exclusion regexes to apply.
+ """
-def presubmit_check(code_format: CodeFormat, **filter_paths_args) -> Callable:
- """Creates a presubmit check function from a CodeFormat object."""
- filter_paths_args.setdefault('endswith', code_format.extensions)
- filter_paths_args.setdefault('exclude', code_format.exclude)
+ # Make a copy of the FileFilter and add in any additional excludes.
+ file_filter = FileFilter(**vars(code_format.filter))
+ file_filter.exclude += tuple(re.compile(e) for e in exclude)
- @pw_presubmit.filter_paths(**filter_paths_args)
+ @pw_presubmit.filter_paths(file_filter=file_filter)
def check_code_format(ctx: pw_presubmit.PresubmitContext):
- errors = code_format.check(ctx.paths)
+ errors = code_format.check(ctx)
print_format_check(
errors,
# When running as part of presubmit, show the fix command help.
show_fix_commands=True,
)
- if errors:
- raise pw_presubmit.PresubmitFailure
+ if not errors:
+ return
+
+ with ctx.failure_summary_log.open('w') as outs:
+ print_format_check(
+ errors,
+ show_summary=False,
+ show_fix_commands=False,
+ file=outs,
+ )
+
+ raise pw_presubmit.PresubmitFailure
language = code_format.language.lower().replace('+', 'p').replace(' ', '_')
- check_code_format.__name__ = f'{language}_format'
+ check_code_format.name = f'{language}_format'
+ check_code_format.doc = f'Check the format of {code_format.language} files.'
return check_code_format
-def presubmit_checks(**filter_paths_args) -> Tuple[Callable, ...]:
- """Returns a tuple with all supported code format presubmit checks."""
- return tuple(
- presubmit_check(fmt, **filter_paths_args) for fmt in CODE_FORMATS)
+def presubmit_checks(
+ *,
+ exclude: Collection[Union[str, Pattern[str]]] = (),
+ code_formats: Collection[CodeFormat] = CODE_FORMATS,
+) -> Tuple[Callable, ...]:
+ """Returns a tuple with all supported code format presubmit checks.
+
+ Args:
+ exclude: Additional exclusion regexes to apply.
+ code_formats: A list of CodeFormat objects to run checks with.
+ """
+
+ return tuple(presubmit_check(fmt, exclude=exclude) for fmt in code_formats)
class CodeFormatter:
"""Checks or fixes the formatting of a set of files."""
- def __init__(self, files: Iterable[Path]):
+
+ def __init__(
+ self,
+ root: Optional[Path],
+ files: Iterable[Path],
+ output_dir: Path,
+ code_formats: Collection[CodeFormat] = CODE_FORMATS_WITH_YAPF,
+ package_root: Optional[Path] = None,
+ ):
+ self.root = root
self.paths = list(files)
self._formats: Dict[CodeFormat, List] = collections.defaultdict(list)
+ self.root_output_dir = output_dir
+ self.package_root = package_root or output_dir / 'packages'
for path in self.paths:
- for code_format in CODE_FORMATS:
- if any(path.as_posix().endswith(e)
- for e in code_format.extensions):
+ for code_format in code_formats:
+ if code_format.filter.matches(path):
+ _LOG.debug(
+ 'Formatting %s as %s', path, code_format.language
+ )
self._formats[code_format].append(path)
+ break
+ else:
+ _LOG.debug('No formatter found for %s', path)
+
+ def _context(self, code_format: CodeFormat):
+ outdir = self.root_output_dir / code_format.language.replace(' ', '_')
+ os.makedirs(outdir, exist_ok=True)
+
+ return FormatContext(
+ root=self.root,
+ output_dir=outdir,
+ paths=tuple(self._formats[code_format]),
+ package_root=self.package_root,
+ format_options=FormatOptions.load(),
+ )
def check(self) -> Dict[Path, str]:
"""Returns {path: diff} for files with incorrect formatting."""
@@ -387,7 +680,7 @@ class CodeFormatter:
for code_format, files in self._formats.items():
_LOG.debug('Checking %s', ', '.join(str(f) for f in files))
- errors.update(code_format.check(files))
+ errors.update(code_format.check(self._context(code_format)))
return collections.OrderedDict(sorted(errors.items()))
@@ -395,7 +688,7 @@ class CodeFormatter:
"""Fixes format errors for supported files in place."""
all_errors: Dict[Path, str] = {}
for code_format, files in self._formats.items():
- errors = code_format.fix(files)
+ errors = code_format.fix(self._context(code_format))
if errors:
for path, error in errors.items():
_LOG.error('Failed to format %s', path)
@@ -404,59 +697,111 @@ class CodeFormatter:
all_errors.update(errors)
continue
- _LOG.info('Formatted %s',
- plural(files, code_format.language + ' file'))
+ _LOG.info(
+ 'Formatted %s', plural(files, code_format.language + ' file')
+ )
return all_errors
def _file_summary(files: Iterable[Union[Path, str]], base: Path) -> List[str]:
try:
return file_summary(
- Path(f).resolve().relative_to(base.resolve()) for f in files)
+ Path(f).resolve().relative_to(base.resolve()) for f in files
+ )
except ValueError:
return []
-def format_paths_in_repo(paths: Collection[Union[Path, str]],
- exclude: Collection[Pattern[str]], fix: bool,
- base: str) -> int:
+def format_paths_in_repo(
+ paths: Collection[Union[Path, str]],
+ exclude: Collection[Pattern[str]],
+ fix: bool,
+ base: str,
+ code_formats: Collection[CodeFormat] = CODE_FORMATS,
+ output_directory: Optional[Path] = None,
+ package_root: Optional[Path] = None,
+) -> int:
"""Checks or fixes formatting for files in a Git repo."""
+
files = [Path(path).resolve() for path in paths if os.path.isfile(path)]
repo = git_repo.root() if git_repo.is_repo() else None
# Implement a graceful fallback in case the tracking branch isn't available.
- if (base == git_repo.TRACKING_BRANCH_ALIAS
- and not git_repo.tracking_branch(repo)):
+ if base == git_repo.TRACKING_BRANCH_ALIAS and not git_repo.tracking_branch(
+ repo
+ ):
_LOG.warning(
'Failed to determine the tracking branch, using --base HEAD~1 '
- 'instead of listing all files')
+ 'instead of listing all files'
+ )
base = 'HEAD~1'
# If this is a Git repo, list the original paths with git ls-files or diff.
if repo:
- project_root = Path(pw_cli.env.pigweed_environment().PW_PROJECT_ROOT)
+ project_root = pw_cli.env.pigweed_environment().PW_PROJECT_ROOT
_LOG.info(
'Formatting %s',
- git_repo.describe_files(repo, Path.cwd(), base, paths, exclude,
- project_root))
+ git_repo.describe_files(
+ repo, Path.cwd(), base, paths, exclude, project_root
+ ),
+ )
# Add files from Git and remove duplicates.
files = sorted(
set(exclude_paths(exclude, git_repo.list_files(base, paths)))
- | set(files))
+ | set(files)
+ )
elif base:
_LOG.critical(
- 'A base commit may only be provided if running from a Git repo')
+ 'A base commit may only be provided if running from a Git repo'
+ )
return 1
- return format_files(files, fix, repo=repo)
+ return format_files(
+ files,
+ fix,
+ repo=repo,
+ code_formats=code_formats,
+ output_directory=output_directory,
+ package_root=package_root,
+ )
+
+
+def format_files(
+ paths: Collection[Union[Path, str]],
+ fix: bool,
+ repo: Optional[Path] = None,
+ code_formats: Collection[CodeFormat] = CODE_FORMATS,
+ output_directory: Optional[Path] = None,
+ package_root: Optional[Path] = None,
+) -> int:
+ """Checks or fixes formatting for the specified files."""
+ root: Optional[Path] = None
-def format_files(paths: Collection[Union[Path, str]],
- fix: bool,
- repo: Optional[Path] = None) -> int:
- """Checks or fixes formatting for the specified files."""
- formatter = CodeFormatter(Path(p) for p in paths)
+ if git_repo.is_repo():
+ root = git_repo.root()
+ elif paths:
+ parent = Path(next(iter(paths))).parent
+ if git_repo.is_repo(parent):
+ root = git_repo.root(parent)
+
+ output_dir: Path
+ if output_directory:
+ output_dir = output_directory
+ elif root:
+ output_dir = root / _DEFAULT_PATH
+ else:
+ tempdir = tempfile.TemporaryDirectory()
+ output_dir = Path(tempdir.name)
+
+ formatter = CodeFormatter(
+ files=(Path(p) for p in paths),
+ code_formats=code_formats,
+ root=root,
+ output_dir=output_dir,
+ package_root=package_root,
+ )
_LOG.info('Checking formatting for %s', plural(formatter.paths, 'file'))
@@ -468,8 +813,9 @@ def format_files(paths: Collection[Union[Path, str]],
if check_errors:
if fix:
- _LOG.info('Applying formatting fixes to %d files',
- len(check_errors))
+ _LOG.info(
+ 'Applying formatting fixes to %d files', len(check_errors)
+ )
fix_errors = formatter.fix()
if fix_errors:
_LOG.info('Failed to apply formatting fixes')
@@ -499,19 +845,35 @@ def arguments(git_paths: bool) -> argparse.ArgumentParser:
path = Path(arg)
if not path.is_file():
raise argparse.ArgumentTypeError(
- f'{arg} is not a path to a file')
+ f'{arg} is not a path to a file'
+ )
return path
- parser.add_argument('paths',
- metavar='path',
- nargs='+',
- type=existing_path,
- help='File paths to check')
+ parser.add_argument(
+ 'paths',
+ metavar='path',
+ nargs='+',
+ type=existing_path,
+ help='File paths to check',
+ )
+
+ parser.add_argument(
+ '--fix', action='store_true', help='Apply formatting fixes in place.'
+ )
+
+ parser.add_argument(
+ '--output-directory',
+ type=Path,
+ help=f"Output directory (default: {'<repo root>' / _DEFAULT_PATH})",
+ )
+ parser.add_argument(
+ '--package-root',
+ type=Path,
+ default=Path(os.environ['PW_PACKAGE_ROOT']),
+ help='Package root directory',
+ )
- parser.add_argument('--fix',
- action='store_true',
- help='Apply formatting fixes in place.')
return parser
@@ -520,6 +882,19 @@ def main() -> int:
return format_paths_in_repo(**vars(arguments(git_paths=True).parse_args()))
+def _pigweed_upstream_main() -> int:
+ """Check and fix formatting for source files in upstream Pigweed.
+
+ Excludes third party sources.
+ """
+ args = arguments(git_paths=True).parse_args()
+
+ # Exclude paths with third party code from formatting.
+ args.exclude.append(re.compile('^third_party/fuchsia/repo/'))
+
+ return format_paths_in_repo(**vars(args))
+
+
if __name__ == '__main__':
try:
# If pw_cli is available, use it to initialize logs.
diff --git a/pw_presubmit/py/pw_presubmit/git_repo.py b/pw_presubmit/py/pw_presubmit/git_repo.py
index 25b3afa86..a3847f8ca 100644
--- a/pw_presubmit/py/pw_presubmit/git_repo.py
+++ b/pw_presubmit/py/pw_presubmit/git_repo.py
@@ -16,49 +16,64 @@
import logging
from pathlib import Path
import subprocess
-from typing import Collection, Iterable, Iterator, List, NamedTuple, Optional
-from typing import Pattern, Set, Tuple, Union
+from typing import Collection, Iterable, List, Optional, Pattern, Union
from pw_presubmit.tools import log_run, plural
_LOG = logging.getLogger(__name__)
PathOrStr = Union[Path, str]
+PatternOrStr = Union[Pattern, str]
TRACKING_BRANCH_ALIAS = '@{upstream}'
_TRACKING_BRANCH_ALIASES = TRACKING_BRANCH_ALIAS, '@{u}'
-def git_stdout(*args: PathOrStr,
- show_stderr=False,
- repo: PathOrStr = '.') -> str:
- return log_run(['git', '-C', repo, *args],
- stdout=subprocess.PIPE,
- stderr=None if show_stderr else subprocess.DEVNULL,
- check=True).stdout.decode().strip()
+def git_stdout(
+ *args: PathOrStr, show_stderr=False, repo: PathOrStr = '.'
+) -> str:
+ return (
+ log_run(
+ ['git', '-C', str(repo), *args],
+ stdout=subprocess.PIPE,
+ stderr=None if show_stderr else subprocess.DEVNULL,
+ check=True,
+ )
+ .stdout.decode()
+ .strip()
+ )
def _ls_files(args: Collection[PathOrStr], repo: Path) -> Iterable[Path]:
"""Returns results of git ls-files as absolute paths."""
git_root = repo.resolve()
for file in git_stdout('ls-files', '--', *args, repo=repo).splitlines():
- yield git_root / file
+ full_path = git_root / file
+ # Modified submodules will show up as directories and should be ignored.
+ if full_path.is_file():
+ yield full_path
-def _diff_names(commit: str, pathspecs: Collection[PathOrStr],
- repo: Path) -> Iterable[Path]:
+def _diff_names(
+ commit: str, pathspecs: Collection[PathOrStr], repo: Path
+) -> Iterable[Path]:
"""Returns absolute paths of files changed since the specified commit."""
git_root = root(repo)
- for file in git_stdout('diff',
- '--name-only',
- '--diff-filter=d',
- commit,
- '--',
- *pathspecs,
- repo=repo).splitlines():
- yield git_root / file
-
-
-def tracking_branch(repo_path: Path = None) -> Optional[str]:
+ for file in git_stdout(
+ 'diff',
+ '--name-only',
+ '--diff-filter=d',
+ commit,
+ '--',
+ *pathspecs,
+ repo=repo,
+ ).splitlines():
+ full_path = git_root / file
+ # Modified submodules will show up as directories and should be ignored.
+ if full_path.is_file():
+ yield full_path
+
+
+def tracking_branch(repo_path: Optional[Path] = None) -> Optional[str]:
"""Returns the tracking branch of the current branch.
Since most callers of this function can safely handle a return value of
@@ -81,19 +96,23 @@ def tracking_branch(repo_path: Path = None) -> Optional[str]:
# This command should only error out if there's no upstream branch set.
try:
- return git_stdout('rev-parse',
- '--abbrev-ref',
- '--symbolic-full-name',
- TRACKING_BRANCH_ALIAS,
- repo=repo_path)
+ return git_stdout(
+ 'rev-parse',
+ '--abbrev-ref',
+ '--symbolic-full-name',
+ TRACKING_BRANCH_ALIAS,
+ repo=repo_path,
+ )
except subprocess.CalledProcessError:
return None
-def list_files(commit: Optional[str] = None,
- pathspecs: Collection[PathOrStr] = (),
- repo_path: Optional[Path] = None) -> List[Path]:
+def list_files(
+ commit: Optional[str] = None,
+ pathspecs: Collection[PathOrStr] = (),
+ repo_path: Optional[Path] = None,
+) -> List[Path]:
"""Lists files with git ls-files or git diff --name-only.
Args:
@@ -116,7 +135,10 @@ def list_files(commit: Optional[str] = None,
except subprocess.CalledProcessError:
_LOG.warning(
'Error comparing with base revision %s of %s, listing all '
- 'files instead of just changed files', commit, repo_path)
+ 'files instead of just changed files',
+ commit,
+ repo_path,
+ )
return sorted(_ls_files(pathspecs, repo_path))
@@ -138,33 +160,44 @@ def has_uncommitted_changes(repo: Optional[Path] = None) -> bool:
retries = 6
for i in range(retries):
try:
- log_run(['git', '-C', repo, 'update-index', '-q', '--refresh'],
- capture_output=True,
- check=True)
+ log_run(
+ ['git', '-C', repo, 'update-index', '-q', '--refresh'],
+ capture_output=True,
+ check=True,
+ )
except subprocess.CalledProcessError as err:
if err.stderr or i == retries - 1:
raise
continue
# diff-index exits with 1 if there are uncommitted changes.
- return log_run(['git', '-C', repo, 'diff-index', '--quiet', 'HEAD',
- '--']).returncode == 1
-
-
-def _describe_constraints(git_root: Path, repo_path: Path,
- commit: Optional[str],
- pathspecs: Collection[PathOrStr],
- exclude: Collection[Pattern[str]]) -> Iterable[str]:
+ return (
+ log_run(
+ ['git', '-C', repo, 'diff-index', '--quiet', 'HEAD', '--']
+ ).returncode
+ == 1
+ )
+
+
+def _describe_constraints(
+ git_root: Path,
+ repo_path: Path,
+ commit: Optional[str],
+ pathspecs: Collection[PathOrStr],
+ exclude: Collection[Pattern[str]],
+) -> Iterable[str]:
if not git_root.samefile(repo_path):
yield (
f'under the {repo_path.resolve().relative_to(git_root.resolve())} '
- 'subdirectory')
+ 'subdirectory'
+ )
if commit in _TRACKING_BRANCH_ALIASES:
commit = tracking_branch(git_root)
if commit is None:
_LOG.warning(
'Attempted to list files changed since the remote tracking '
- 'branch, but the repo is not tracking a branch')
+ 'branch, but the repo is not tracking a branch'
+ )
if commit:
yield f'that have changed since {commit}'
@@ -174,19 +207,25 @@ def _describe_constraints(git_root: Path, repo_path: Path,
yield f'that match {plural(pathspecs, "pathspec")} ({paths_str})'
if exclude:
- yield (f'that do not match {plural(exclude, "pattern")} (' +
- ', '.join(p.pattern for p in exclude) + ')')
-
-
-def describe_files(git_root: Path,
- repo_path: Path,
- commit: Optional[str],
- pathspecs: Collection[PathOrStr],
- exclude: Collection[Pattern],
- project_root: Path = None) -> str:
+ yield (
+ f'that do not match {plural(exclude, "pattern")} ('
+ + ', '.join(p.pattern for p in exclude)
+ + ')'
+ )
+
+
+def describe_files(
+ git_root: Path,
+ repo_path: Path,
+ commit: Optional[str],
+ pathspecs: Collection[PathOrStr],
+ exclude: Collection[Pattern],
+ project_root: Optional[Path] = None,
+) -> str:
"""Completes 'Doing something to ...' for a set of files in a Git repo."""
constraints = list(
- _describe_constraints(git_root, repo_path, commit, pathspecs, exclude))
+ _describe_constraints(git_root, repo_path, commit, pathspecs, exclude)
+ )
name = git_root.name
if project_root and project_root != git_root:
@@ -214,10 +253,13 @@ def root(repo_path: PathOrStr = '.', *, show_stderr: bool = True) -> Path:
raise FileNotFoundError(f'{repo_path} does not exist')
return Path(
- git_stdout('rev-parse',
- '--show-toplevel',
- repo=repo_path if repo_path.is_dir() else repo_path.parent,
- show_stderr=show_stderr))
+ git_stdout(
+ 'rev-parse',
+ '--show-toplevel',
+ repo=repo_path if repo_path.is_dir() else repo_path.parent,
+ show_stderr=show_stderr,
+ )
+ )
def within_repo(repo_path: PathOrStr = '.') -> Optional[Path]:
@@ -233,92 +275,70 @@ def is_repo(repo_path: PathOrStr = '.') -> bool:
return within_repo(repo_path) is not None
-def path(repo_path: PathOrStr,
- *additional_repo_paths: PathOrStr,
- repo: PathOrStr = '.') -> Path:
+def path(
+ repo_path: PathOrStr,
+ *additional_repo_paths: PathOrStr,
+ repo: PathOrStr = '.',
+) -> Path:
"""Returns a path relative to a Git repository's root."""
return root(repo).joinpath(repo_path, *additional_repo_paths)
-class PythonPackage(NamedTuple):
- root: Path # Path to the file containing the setup.py
- package: Path # Path to the main package directory
- packaged_files: Tuple[Path, ...] # All sources in the main package dir
- other_files: Tuple[Path, ...] # Other Python files under root
-
- def all_files(self) -> Tuple[Path, ...]:
- return self.packaged_files + self.other_files
-
-
-def all_python_packages(repo: PathOrStr = '.') -> Iterator[PythonPackage]:
- """Finds all Python packages in the repo based on setup.py locations."""
- root_py_dirs = [
- file.parent
- for file in _ls_files(['setup.py', '*/setup.py'], Path(repo))
- ]
+def commit_message(commit: str = 'HEAD', repo: PathOrStr = '.') -> str:
+ return git_stdout('log', '--format=%B', '-n1', commit, repo=repo)
- for py_dir in root_py_dirs:
- all_packaged_files = _ls_files([py_dir / '*' / '*.py'], repo=py_dir)
- common_dir: Optional[str] = None
- # Make there is only one package directory with Python files in it.
- for file in all_packaged_files:
- package_dir = file.relative_to(py_dir).parts[0]
+def commit_author(commit: str = 'HEAD', repo: PathOrStr = '.') -> str:
+ return git_stdout('log', '--format=%ae', '-n1', commit, repo=repo)
- if common_dir is None:
- common_dir = package_dir
- elif common_dir != package_dir:
- _LOG.warning(
- 'There are multiple Python package directories in %s: %s '
- 'and %s. This is not supported by pw presubmit. Each '
- 'setup.py should correspond with a single Python package',
- py_dir, common_dir, package_dir)
- break
- if common_dir is not None:
- packaged_files = tuple(_ls_files(['*/*.py'], repo=py_dir))
- other_files = tuple(
- f for f in _ls_files(['*.py'], repo=py_dir)
- if f.name != 'setup.py' and f not in packaged_files)
+def commit_hash(
+ rev: str = 'HEAD', short: bool = True, repo: PathOrStr = '.'
+) -> str:
+ """Returns the commit hash of the revision."""
+ args = ['rev-parse']
+ if short:
+ args += ['--short']
+ args += [rev]
+ return git_stdout(*args, repo=repo)
- yield PythonPackage(py_dir, py_dir / common_dir, packaged_files,
- other_files)
+def discover_submodules(
+ superproject_dir: Path, excluded_paths: Collection[PatternOrStr] = ()
+) -> List[Path]:
+ """Query git and return a list of submodules in the current project.
-def python_packages_containing(
- python_paths: Iterable[Path],
- repo: PathOrStr = '.') -> Tuple[List[PythonPackage], List[Path]]:
- """Finds all Python packages containing the provided Python paths.
+ Args:
+ superproject_dir: Path object to directory under which we are looking
+ for submodules. This will also be included in list
+ returned unless excluded.
+ excluded_paths: Pattern or string that match submodules that should not
+ be returned. All matches are done on posix style paths.
Returns:
- ([packages], [files_not_in_packages])
+ List of "Path"s which were found but not excluded, this includes
+ superproject_dir unless excluded.
"""
- all_packages = list(all_python_packages(repo))
-
- packages: Set[PythonPackage] = set()
- files_not_in_packages: List[Path] = []
-
- for python_path in python_paths:
- for package in all_packages:
- if package.root in python_path.parents:
- packages.add(package)
- break
+ discovery_report = git_stdout(
+ 'submodule',
+ 'foreach',
+ '--quiet',
+ '--recursive',
+ 'echo $toplevel/$sm_path',
+ repo=superproject_dir,
+ )
+ module_dirs = [Path(line) for line in discovery_report.split()]
+ # The superproject is omitted in the prior scan.
+ module_dirs.append(superproject_dir)
+
+ for exclude in excluded_paths:
+ if isinstance(exclude, Pattern):
+ for module_dir in reversed(module_dirs):
+ if exclude.fullmatch(module_dir.as_posix()):
+ module_dirs.remove(module_dir)
else:
- files_not_in_packages.append(python_path)
-
- return list(packages), files_not_in_packages
-
-
-def commit_message(commit: str = 'HEAD', repo: PathOrStr = '.') -> str:
- return git_stdout('log', '--format=%B', '-n1', commit, repo=repo)
-
+ for module_dir in reversed(module_dirs):
+ if exclude == module_dir.as_posix():
+ module_dirs.remove(module_dir)
-def commit_hash(rev: str = 'HEAD',
- short: bool = True,
- repo: PathOrStr = '.') -> str:
- """Returns the commit hash of the revision."""
- args = ['rev-parse']
- if short:
- args += ['--short']
- args += [rev]
- return git_stdout(*args, repo=repo)
+ return module_dirs
diff --git a/pw_presubmit/py/pw_presubmit/gitmodules.py b/pw_presubmit/py/pw_presubmit/gitmodules.py
new file mode 100644
index 000000000..67e9854e6
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/gitmodules.py
@@ -0,0 +1,192 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Check various rules for .gitmodules files."""
+
+import dataclasses
+import logging
+from pathlib import Path
+from typing import Callable, Dict, Optional, Sequence
+import urllib.parse
+
+from pw_presubmit import (
+ git_repo,
+ PresubmitContext,
+ PresubmitFailure,
+ filter_paths,
+)
+
+_LOG: logging.Logger = logging.getLogger(__name__)
+
+
+@dataclasses.dataclass
+class Config:
+ # Allow direct references to non-Google hosts.
+ allow_non_googlesource_hosts: bool = False
+
+ # Allow a specific subset of googlesource.com hosts. If an empty list then
+ # all googlesource hosts are permitted.
+ allowed_googlesource_hosts: Sequence[str] = ()
+
+ # Require relative URLs, like those that start with "/" or "../".
+ require_relative_urls: bool = False
+
+ # Allow "sso://" URLs.
+ allow_sso: bool = True
+
+ # Allow use of "git.corp.google.com" URLs.
+ allow_git_corp_google_com: bool = True
+
+ # Require a branch for each submodule.
+ require_branch: bool = False
+
+ # Arbitrary validator. Gets invoked with the submodule name and a dict of
+ # the submodule properties. Should throw exceptions or call ctx.fail to
+ # register errors.
+ validator: Optional[
+ Callable[[PresubmitContext, Path, str, Dict[str, str]], None]
+ ] = None
+
+
+def _parse_gitmodules(path: Path) -> Dict[str, Dict[str, str]]:
+ raw_submodules: str = git_repo.git_stdout(
+ 'config', '--file', path, '--list'
+ )
+ submodules: Dict[str, Dict[str, str]] = {}
+ for line in raw_submodules.splitlines():
+ key: str
+ value: str
+ key, value = line.split('=', 1)
+ if not key.startswith('submodule.'):
+ raise PresubmitFailure(f'unexpected key {key!r}', path)
+ key = key.split('.', 1)[1]
+
+ submodule: str
+ param: str
+ submodule, param = key.rsplit('.', 1)
+
+ submodules.setdefault(submodule, {})
+ submodules[submodule][param] = value
+
+ return submodules
+
+
+_GERRIT_HOST_SUFFIXES = ('.googlesource.com', '.git.corp.google.com')
+
+
+def process_gitmodules(ctx: PresubmitContext, config: Config, path: Path):
+ """Check if a specific .gitmodules file passes the options in the config."""
+ _LOG.debug('Evaluating path %s', path)
+ submodules: Dict[str, Dict[str, str]] = _parse_gitmodules(path)
+
+ assert isinstance(config.allowed_googlesource_hosts, (list, tuple))
+ for allowed in config.allowed_googlesource_hosts:
+ if '.' in allowed or '-review' in allowed:
+ raise PresubmitFailure(
+ f'invalid googlesource requirement: {allowed}'
+ )
+
+ for name, submodule in submodules.items():
+ _LOG.debug('======================')
+ _LOG.debug('evaluating submodule %s', name)
+ _LOG.debug('%r', submodule)
+
+ if config.require_branch:
+ _LOG.debug('branch is required')
+ if 'branch' not in submodule:
+ ctx.fail(
+ f'submodule {name} does not have a branch set but '
+ 'branches are required'
+ )
+
+ url = submodule['url']
+
+ if config.validator:
+ config.validator(ctx, path, name, submodule)
+
+ if url.startswith(('/', '../')):
+ _LOG.debug('URL is relative, remaining checks are irrelevant')
+ continue
+
+ if config.require_relative_urls:
+ _LOG.debug('relative URLs required')
+ ctx.fail(
+ f'submodule {name} has non-relative url {url!r} but '
+ 'relative urls are required'
+ )
+ continue
+
+ parsed = urllib.parse.urlparse(url)
+
+ if not config.allow_sso:
+ _LOG.debug('sso not allowed')
+ if parsed.scheme in ('sso', 'rpc'):
+ ctx.fail(
+ f'submodule {name} has sso/rpc url {url!r} but '
+ 'sso/rpc urls are not allowed'
+ )
+ continue
+
+ if not config.allow_git_corp_google_com:
+ _LOG.debug('git.corp.google.com not allowed')
+ if '.git.corp.google.com' in parsed.netloc:
+ ctx.fail(
+ f'submodule {name} has git.corp.google.com url '
+ f'{url!r} but git.corp.google.com urls are not '
+ 'allowed'
+ )
+ continue
+
+ if not config.allow_non_googlesource_hosts:
+ _LOG.debug('non-google hosted repos not allowed')
+ if parsed.scheme not in (
+ 'sso',
+ 'rpc',
+ ) and not parsed.netloc.endswith(_GERRIT_HOST_SUFFIXES):
+ ctx.fail(
+ f'submodule {name} has prohibited non-Google url ' f'{url}'
+ )
+ continue
+
+ if config.allowed_googlesource_hosts:
+ _LOG.debug(
+ 'allowed googlesource hosts: %r',
+ config.allowed_googlesource_hosts,
+ )
+ _LOG.debug('raw url: %s', url)
+ host = parsed.netloc
+ if host.endswith(_GERRIT_HOST_SUFFIXES) or parsed.scheme in (
+ 'sso',
+ 'rpc',
+ ):
+ for suffix in _GERRIT_HOST_SUFFIXES:
+ host = host.replace(suffix, '')
+ _LOG.debug('host: %s', host)
+ if host not in config.allowed_googlesource_hosts:
+ ctx.fail(
+ f'submodule {name} is from prohibited Google '
+ f'Gerrit host {parsed.netloc}'
+ )
+ continue
+
+
+def create(config: Config = Config()):
+ """Create a gitmodules presubmit step with a given config."""
+
+ @filter_paths(endswith='.gitmodules')
+ def gitmodules(ctx: PresubmitContext):
+ """Check various rules for .gitmodules files."""
+ for path in ctx.paths:
+ process_gitmodules(ctx, config, path)
+
+ return gitmodules
diff --git a/pw_presubmit/py/pw_presubmit/inclusive_language.py b/pw_presubmit/py/pw_presubmit/inclusive_language.py
index b19f74af1..8ef0a9c00 100644
--- a/pw_presubmit/py/pw_presubmit/inclusive_language.py
+++ b/pw_presubmit/py/pw_presubmit/inclusive_language.py
@@ -26,7 +26,8 @@ from . import presubmit
NON_INCLUSIVE_WORDS = [
r'master',
r'slave',
- r'(white|gr[ae]y|black)\s*(list|hat)',
+ r'red[-\s]?line',
+ r'(white|gr[ae]y|black)[-\s]*(list|hat)',
r'craz(y|ie)',
r'insane',
r'crip+led?',
@@ -39,6 +40,7 @@ NON_INCLUSIVE_WORDS = [
r'her',
r'm[ae]n[-\s]*in[-\s]*the[-\s]*middle',
r'mitm',
+ r'first[-\s]?class[-\s]?citizen',
]
# inclusive-language: enable
@@ -66,11 +68,14 @@ def _process_inclusive_language(*words):
_ = re.compile(word)
word_boundary = (
- r'(\b|_|(?<=[a-z])(?=[A-Z])|(?<=[0-9])(?=\w)|(?<=\w)(?=[0-9]))')
+ r'(\b|_|(?<=[a-z])(?=[A-Z])|(?<=[0-9])(?=\w)|(?<=\w)(?=[0-9]))'
+ )
return re.compile(
- r"({b})(?i:{w})(e?[sd]{b}|{b})".format(w='|'.join(all_words),
- b=word_boundary), )
+ r"({b})(?i:{w})(e?[sd]{b}|{b})".format(
+ w='|'.join(all_words), b=word_boundary
+ ),
+ )
NON_INCLUSIVE_WORDS_REGEX = _process_inclusive_language()
@@ -100,8 +105,8 @@ class LineMatch:
return f'Found non-inclusive word "{self.word}" on line {self.line}'
-@presubmit.Check
-def inclusive_language(
+@presubmit.check(name='inclusive_language')
+def presubmit_check(
ctx: presubmit.PresubmitContext,
words_regex=NON_INCLUSIVE_WORDS_REGEX,
):
@@ -138,7 +143,8 @@ def inclusive_language(
if match:
found_words.setdefault(path, [])
found_words[path].append(
- LineMatch(i, match.group(0)))
+ LineMatch(i, match.group(0))
+ )
# Not using 'continue' so this line always executes.
prev = line
@@ -147,19 +153,25 @@ def inclusive_language(
# File is not text, like a gif.
pass
- for path, matches in found_words.items():
- print('=' * 40)
- print(path)
- for match in matches:
- print(match)
-
if found_words:
+ with open(ctx.failure_summary_log, 'w') as outs:
+ for i, (path, matches) in enumerate(found_words.items()):
+ if i:
+ print('=' * 40, file=outs)
+ print(path, file=outs)
+ for match in matches:
+ print(match, file=outs)
+
+ print(ctx.failure_summary_log.read_text(), end=None)
+
print()
- print("""
+ print(
+ """
Individual lines can be ignored with "inclusive-language: ignore". Blocks can be
ignored with "inclusive-language: disable" and reenabled with
"inclusive-language: enable".
-""".strip())
+""".strip()
+ )
# Re-enable just in case: inclusive-language: enable.
raise presubmit.PresubmitFailure
@@ -171,7 +183,8 @@ def inclusive_language_checker(*words):
regex = _process_inclusive_language(*words)
def inclusive_language( # pylint: disable=redefined-outer-name
- ctx: presubmit.PresubmitContext):
+ ctx: presubmit.PresubmitContext,
+ ):
globals()['inclusive_language'](ctx, regex)
return inclusive_language
diff --git a/pw_presubmit/py/pw_presubmit/install_hook.py b/pw_presubmit/py/pw_presubmit/install_hook.py
index 62363f952..1354dd0f7 100755
--- a/pw_presubmit/py/pw_presubmit/install_hook.py
+++ b/pw_presubmit/py/pw_presubmit/install_hook.py
@@ -21,25 +21,67 @@ from pathlib import Path
import re
import shlex
import subprocess
-from typing import Sequence, Union
+from typing import Optional, Sequence, Union
_LOG: logging.Logger = logging.getLogger(__name__)
def git_repo_root(path: Union[Path, str]) -> Path:
return Path(
- subprocess.run(['git', '-C', path, 'rev-parse', '--show-toplevel'],
- check=True,
- stdout=subprocess.PIPE).stdout.strip().decode())
+ subprocess.run(
+ ['git', '-C', path, 'rev-parse', '--show-toplevel'],
+ check=True,
+ stdout=subprocess.PIPE,
+ )
+ .stdout.strip()
+ .decode()
+ )
+
+
+def _stdin_args_for_hook(hook) -> Sequence[str]:
+ """Gives stdin arguments for each hook.
+
+ See https://git-scm.com/docs/githooks for more information.
+ """
+ if hook == 'pre-push':
+ return (
+ 'local_ref',
+ 'local_object_name',
+ 'remote_ref',
+ 'remote_object_name',
+ )
+ if hook in ('pre-receive', 'post-receive', 'reference-transaction'):
+ return ('old_value', 'new_value', 'ref_name')
+ if hook == 'post-rewrite':
+ return ('old_object_name', 'new_object_name')
+ return ()
+
+
+def _replace_arg_in_hook(arg: str, unquoted_args: Sequence[str]) -> str:
+ if arg in unquoted_args:
+ return arg
+ return shlex.quote(arg)
+
+
+def install_git_hook(
+ hook: str,
+ command: Sequence[Union[Path, str]],
+ repository: Union[Path, str] = '.',
+) -> None:
+ """Installs a simple Git hook that executes the provided command.
+
+ Args:
+ hook: Git hook to install, e.g. 'pre-push'.
+ command: Command to execute as the hook. The command is executed from the
+ root of the repo. Arguments are sanitised with `shlex.quote`, except
+ for any arguments are equal to f'${stdin_arg}' for some `stdin_arg`
+ that matches a standard-input argument to the git hook.
+ repository: Repository to install the hook in.
+ """
+ if not command:
+ raise ValueError('The command cannot be empty!')
-
-def install_hook(script,
- hook: str,
- args: Sequence[str] = (),
- repository: Union[Path, str] = '.') -> None:
- """Installs a simple Git hook that calls a script with arguments."""
root = git_repo_root(repository).resolve()
- script = os.path.relpath(script, root)
if root.joinpath('.git').is_dir():
hook_path = root.joinpath('.git', 'hooks', hook)
@@ -52,7 +94,13 @@ def install_hook(script,
hook_path.parent.mkdir(exist_ok=True)
- command = ' '.join(shlex.quote(arg) for arg in (script, *args))
+ hook_stdin_args = _stdin_args_for_hook(hook)
+ read_stdin_command = 'read ' + ' '.join(hook_stdin_args)
+
+ unquoted_args = [f'${arg}' for arg in hook_stdin_args]
+ args = (_replace_arg_in_hook(str(a), unquoted_args) for a in command[1:])
+
+ command_str = ' '.join([shlex.quote(str(command[0])), *args])
with hook_path.open('w') as file:
line = lambda *args: print(*args, file=file)
@@ -66,13 +114,20 @@ def install_hook(script,
line('# submodule hook.')
line('unset $(git rev-parse --local-env-vars)')
line()
- line(command)
+ line('# Read the stdin args for the hook, made available by git.')
+ line(read_stdin_command)
+ line()
+ line(command_str)
hook_path.chmod(0o755)
- logging.info('Created %s hook for %s at %s', hook, script, hook_path)
+ logging.info(
+ 'Installed %s hook for `%s` at %s', hook, command_str, hook_path
+ )
-def argument_parser(parser=None) -> argparse.ArgumentParser:
+def argument_parser(
+ parser: Optional[argparse.ArgumentParser] = None,
+) -> argparse.ArgumentParser:
if parser is None:
parser = argparse.ArgumentParser(description=__doc__)
@@ -87,22 +142,18 @@ def argument_parser(parser=None) -> argparse.ArgumentParser:
'--repository',
default='.',
type=path,
- help='Path to the repository in which to install the hook')
- parser.add_argument('--hook',
- required=True,
- help='Which type of Git hook to create')
- parser.add_argument('-s',
- '--script',
- required=True,
- type=path,
- help='Path to the script to execute in the hook')
- parser.add_argument('args',
- nargs='*',
- help='Arguments to provide to the commit hook')
+ help='Path to the repository in which to install the hook',
+ )
+ parser.add_argument(
+ '--hook', required=True, help='Which type of Git hook to create'
+ )
+ parser.add_argument(
+ 'command', nargs='*', help='Command to run in the commit hook'
+ )
return parser
if __name__ == '__main__':
logging.basicConfig(format='%(message)s', level=logging.INFO)
- install_hook(**vars(argument_parser().parse_args()))
+ install_git_hook(**vars(argument_parser().parse_args()))
diff --git a/pw_presubmit/py/pw_presubmit/keep_sorted.py b/pw_presubmit/py/pw_presubmit/keep_sorted.py
new file mode 100644
index 000000000..a1068f30c
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/keep_sorted.py
@@ -0,0 +1,497 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Keep specified lists sorted."""
+
+import argparse
+import dataclasses
+import difflib
+import logging
+import os
+from pathlib import Path
+import re
+import sys
+from typing import (
+ Callable,
+ Collection,
+ Dict,
+ List,
+ Optional,
+ Pattern,
+ Sequence,
+ Tuple,
+ Union,
+)
+
+import pw_cli
+from . import cli, format_code, git_repo, presubmit, tools
+
+DEFAULT_PATH = Path('out', 'presubmit', 'keep_sorted')
+
+_COLOR = pw_cli.color.colors()
+_LOG: logging.Logger = logging.getLogger(__name__)
+
+# Ignore a whole section. Please do not change the order of these lines.
+_START = re.compile(r'keep-sorted: (begin|start)', re.IGNORECASE)
+_END = re.compile(r'keep-sorted: (stop|end)', re.IGNORECASE)
+_IGNORE_CASE = re.compile(r'ignore-case', re.IGNORECASE)
+_ALLOW_DUPES = re.compile(r'allow-dupes', re.IGNORECASE)
+_IGNORE_PREFIX = re.compile(r'ignore-prefix=(\S+)', re.IGNORECASE)
+_STICKY_COMMENTS = re.compile(r'sticky-comments=(\S+)', re.IGNORECASE)
+
+# Only include these literals here so keep_sorted doesn't try to reorder later
+# test lines.
+(
+ START,
+ END,
+) = """
+keep-sorted: start
+keep-sorted: end
+""".strip().splitlines()
+
+
+@dataclasses.dataclass
+class KeepSortedContext:
+ paths: List[Path]
+ fix: bool
+ output_dir: Path
+ failure_summary_log: Path
+ failed: bool = False
+
+ def fail(
+ self,
+ description: str = '',
+ path: Optional[Path] = None,
+ line: Optional[int] = None,
+ ) -> None:
+ if not self.fix:
+ self.failed = True
+
+ line_part: str = ''
+ if line is not None:
+ line_part = f'{line}:'
+
+ log = _LOG.error
+ if self.fix:
+ log = _LOG.warning
+
+ if path:
+ log('%s:%s %s', path, line_part, description)
+ else:
+ log('%s', description)
+
+
+class KeepSortedParsingError(presubmit.PresubmitFailure):
+ pass
+
+
+@dataclasses.dataclass
+class _Line:
+ value: str = ''
+ sticky_comments: Sequence[str] = ()
+ continuations: Sequence[str] = ()
+
+ @property
+ def full(self):
+ return ''.join((*self.sticky_comments, self.value, *self.continuations))
+
+ def __lt__(self, other):
+ if not isinstance(other, _Line):
+ return NotImplemented
+ left = (self.value, self.continuations, self.sticky_comments)
+ right = (other.value, other.continuations, other.sticky_comments)
+ return left < right
+
+
+@dataclasses.dataclass
+class _Block:
+ ignore_case: bool = False
+ allow_dupes: bool = False
+ ignored_prefixes: Sequence[str] = dataclasses.field(default_factory=list)
+ sticky_comments: Tuple[str, ...] = ()
+ start_line_number: int = -1
+ start_line: str = ''
+ end_line: str = ''
+ lines: List[str] = dataclasses.field(default_factory=list)
+
+
+class _FileSorter:
+ def __init__(
+ self,
+ ctx: Union[presubmit.PresubmitContext, KeepSortedContext],
+ path: Path,
+ errors: Optional[Dict[Path, Sequence[str]]] = None,
+ ):
+ self.ctx = ctx
+ self.path: Path = path
+ self.all_lines: List[str] = []
+ self.changed: bool = False
+ self._errors: Dict[Path, Sequence[str]] = {}
+ if errors is not None:
+ self._errors = errors
+
+ def _process_block(self, block: _Block) -> Sequence[str]:
+ raw_lines: List[str] = block.lines
+ lines: List[_Line] = []
+
+ prefix = lambda x: len(x) - len(x.lstrip())
+
+ prev_prefix: Optional[int] = None
+ comments: List[str] = []
+ for raw_line in raw_lines:
+ curr_prefix: int = prefix(raw_line)
+ _LOG.debug('prev_prefix %r', prev_prefix)
+ _LOG.debug('curr_prefix %r', curr_prefix)
+ # A "sticky" comment is a comment in the middle of a list of
+ # non-comments. The keep-sorted check keeps this comment with the
+ # following item in the list. For more details see
+ # https://pigweed.dev/pw_presubmit/#sorted-blocks.
+ if block.sticky_comments and raw_line.lstrip().startswith(
+ block.sticky_comments
+ ):
+ _LOG.debug('found sticky %r', raw_line)
+ comments.append(raw_line)
+ elif prev_prefix is not None and curr_prefix > prev_prefix:
+ _LOG.debug('found continuation %r', raw_line)
+ lines[-1].continuations = (*lines[-1].continuations, raw_line)
+ _LOG.debug('modified line %s', lines[-1])
+ else:
+ _LOG.debug('non-sticky %r', raw_line)
+ line = _Line(raw_line, tuple(comments))
+ _LOG.debug('line %s', line)
+ lines.append(line)
+ comments = []
+ prev_prefix = curr_prefix
+ if comments:
+ self.ctx.fail(
+ f'sticky comment at end of block: {comments[0].strip()}',
+ self.path,
+ block.start_line_number,
+ )
+
+ if not block.allow_dupes:
+ lines = list({x.full: x for x in lines}.values())
+
+ StrLinePair = Tuple[str, _Line]
+ sort_key_funcs: List[Callable[[StrLinePair], StrLinePair]] = []
+
+ if block.ignored_prefixes:
+
+ def strip_ignored_prefixes(val):
+ """Remove one ignored prefix from val, if present."""
+ wo_white = val[0].lstrip()
+ white = val[0][0 : -len(wo_white)]
+ for prefix in block.ignored_prefixes:
+ if wo_white.startswith(prefix):
+ return (f'{white}{wo_white[len(prefix):]}', val[1])
+ return (val[0], val[1])
+
+ sort_key_funcs.append(strip_ignored_prefixes)
+
+ if block.ignore_case:
+ sort_key_funcs.append(lambda val: (val[0].lower(), val[1]))
+
+ def sort_key(line):
+ vals = (line.value, line)
+ for sort_key_func in sort_key_funcs:
+ vals = sort_key_func(vals)
+ return vals
+
+ for val in lines:
+ _LOG.debug('For sorting: %r => %r', val, sort_key(val))
+
+ sorted_lines = sorted(lines, key=sort_key)
+ raw_sorted_lines: List[str] = []
+ for line in sorted_lines:
+ raw_sorted_lines.extend(line.sticky_comments)
+ raw_sorted_lines.append(line.value)
+ raw_sorted_lines.extend(line.continuations)
+
+ if block.lines != raw_sorted_lines:
+ self.changed = True
+ diff = difflib.Differ()
+ diff_lines = ''.join(diff.compare(block.lines, raw_sorted_lines))
+
+ self._errors.setdefault(self.path, [])
+ self._errors[self.path] = (
+ f'@@ {block.start_line_number},{len(block.lines)+2} '
+ f'{block.start_line_number},{len(raw_sorted_lines)+2} @@\n'
+ f' {block.start_line}{diff_lines} {block.end_line}'
+ )
+
+ return raw_sorted_lines
+
+ def _parse_file(self, ins):
+ block: Optional[_Block] = None
+
+ for i, line in enumerate(ins, start=1):
+ if block:
+ if _START.search(line):
+ raise KeepSortedParsingError(
+ f'found {line.strip()!r} inside keep-sorted block',
+ self.path,
+ i,
+ )
+
+ if _END.search(line):
+ _LOG.debug('Found end line %d %r', i, line)
+ block.end_line = line
+ self.all_lines.extend(self._process_block(block))
+ block = None
+ self.all_lines.append(line)
+
+ else:
+ _LOG.debug('Adding to block line %d %r', i, line)
+ block.lines.append(line)
+
+ elif start_match := _START.search(line):
+ _LOG.debug('Found start line %d %r', i, line)
+
+ block = _Block()
+
+ block.ignore_case = bool(_IGNORE_CASE.search(line))
+ _LOG.debug('ignore_case: %s', block.ignore_case)
+
+ block.allow_dupes = bool(_ALLOW_DUPES.search(line))
+ _LOG.debug('allow_dupes: %s', block.allow_dupes)
+
+ match = _IGNORE_PREFIX.search(line)
+ if match:
+ block.ignored_prefixes = match.group(1).split(',')
+
+ # We want to check the longest prefixes first, in case one
+ # prefix is a prefix of another prefix.
+ block.ignored_prefixes.sort(key=lambda x: (-len(x), x))
+ _LOG.debug('ignored_prefixes: %r', block.ignored_prefixes)
+
+ match = _STICKY_COMMENTS.search(line)
+ if match:
+ if match.group(1) == 'no':
+ block.sticky_comments = ()
+ else:
+ block.sticky_comments = tuple(match.group(1).split(','))
+ else:
+ prefix = line[: start_match.start()].strip()
+ if prefix and len(prefix) <= 3:
+ block.sticky_comments = (prefix,)
+ _LOG.debug('sticky_comments: %s', block.sticky_comments)
+
+ block.start_line = line
+ block.start_line_number = i
+ self.all_lines.append(line)
+
+ remaining = line[start_match.end() :].strip()
+ remaining = _IGNORE_CASE.sub('', remaining, count=1).strip()
+ remaining = _ALLOW_DUPES.sub('', remaining, count=1).strip()
+ remaining = _IGNORE_PREFIX.sub('', remaining, count=1).strip()
+ remaining = _STICKY_COMMENTS.sub('', remaining, count=1).strip()
+ if remaining.strip():
+ raise KeepSortedParsingError(
+ f'unrecognized directive on keep-sorted line: '
+ f'{remaining}',
+ self.path,
+ i,
+ )
+
+ elif _END.search(line):
+ raise KeepSortedParsingError(
+ f'found {line.strip()!r} outside keep-sorted block',
+ self.path,
+ i,
+ )
+
+ else:
+ self.all_lines.append(line)
+
+ if block:
+ raise KeepSortedParsingError(
+ f'found EOF while looking for "{END}"', self.path
+ )
+
+ def sort(self) -> None:
+ """Check for unsorted keep-sorted blocks."""
+ _LOG.debug('Evaluating path %s', self.path)
+ try:
+ with self.path.open() as ins:
+ _LOG.debug('Processing %s', self.path)
+ self._parse_file(ins)
+
+ except UnicodeDecodeError:
+ # File is not text, like a gif.
+ _LOG.debug('File %s is not a text file', self.path)
+
+ def write(self, path: Optional[Path] = None) -> None:
+ if not self.changed:
+ return
+ if not path:
+ path = self.path
+ with path.open('w') as outs:
+ outs.writelines(self.all_lines)
+ _LOG.info('Applied keep-sorted changes to %s', path)
+
+
+def _print_howto_fix(paths: Sequence[Path]) -> None:
+ def path_relative_to_cwd(path):
+ try:
+ return Path(path).resolve().relative_to(Path.cwd().resolve())
+ except ValueError:
+ return Path(path).resolve()
+
+ message = (
+ f' pw keep-sorted --fix {path_relative_to_cwd(path)}' for path in paths
+ )
+ _LOG.warning('To sort these blocks, run:\n\n%s\n', '\n'.join(message))
+
+
+def _process_files(
+ ctx: Union[presubmit.PresubmitContext, KeepSortedContext]
+) -> Dict[Path, Sequence[str]]:
+ fix = getattr(ctx, 'fix', False)
+ errors: Dict[Path, Sequence[str]] = {}
+
+ for path in ctx.paths:
+ if path.is_symlink() or path.is_dir():
+ continue
+
+ try:
+ sorter = _FileSorter(ctx, path, errors)
+
+ sorter.sort()
+ if sorter.changed:
+ if fix:
+ sorter.write()
+
+ except KeepSortedParsingError as exc:
+ ctx.fail(str(exc))
+
+ if not errors:
+ return errors
+
+ ctx.fail(f'Found {len(errors)} files with keep-sorted errors:')
+
+ with ctx.failure_summary_log.open('w') as outs:
+ for path, diffs in errors.items():
+ diff = ''.join(
+ [
+ f'--- {path} (original)\n',
+ f'+++ {path} (sorted)\n',
+ *diffs,
+ ]
+ )
+
+ outs.write(diff)
+ print(format_code.colorize_diff(diff))
+
+ return errors
+
+
+@presubmit.check(name='keep_sorted')
+def presubmit_check(ctx: presubmit.PresubmitContext) -> None:
+ """Presubmit check that ensures specified lists remain sorted."""
+
+ errors = _process_files(ctx)
+
+ if errors:
+ _print_howto_fix(list(errors.keys()))
+
+
+def parse_args() -> argparse.Namespace:
+ """Creates an argument parser and parses arguments."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ cli.add_path_arguments(parser)
+ parser.add_argument(
+ '--fix', action='store_true', help='Apply fixes in place.'
+ )
+
+ parser.add_argument(
+ '--output-directory',
+ type=Path,
+ help=f'Output directory (default: {"<repo root>" / DEFAULT_PATH})',
+ )
+
+ return parser.parse_args()
+
+
+def keep_sorted_in_repo(
+ paths: Collection[Union[Path, str]],
+ fix: bool,
+ exclude: Collection[Pattern[str]],
+ base: str,
+ output_directory: Optional[Path],
+) -> int:
+ """Checks or fixes keep-sorted blocks for files in a Git repo."""
+
+ files = [Path(path).resolve() for path in paths if os.path.isfile(path)]
+ repo = git_repo.root() if git_repo.is_repo() else None
+
+ # Implement a graceful fallback in case the tracking branch isn't available.
+ if base == git_repo.TRACKING_BRANCH_ALIAS and not git_repo.tracking_branch(
+ repo
+ ):
+ _LOG.warning(
+ 'Failed to determine the tracking branch, using --base HEAD~1 '
+ 'instead of listing all files'
+ )
+ base = 'HEAD~1'
+
+ # If this is a Git repo, list the original paths with git ls-files or diff.
+ project_root = pw_cli.env.pigweed_environment().PW_PROJECT_ROOT
+ if repo:
+ _LOG.info(
+ 'Sorting %s',
+ git_repo.describe_files(
+ repo, Path.cwd(), base, paths, exclude, project_root
+ ),
+ )
+
+ # Add files from Git and remove duplicates.
+ files = sorted(
+ set(tools.exclude_paths(exclude, git_repo.list_files(base, paths)))
+ | set(files)
+ )
+ elif base:
+ _LOG.critical(
+ 'A base commit may only be provided if running from a Git repo'
+ )
+ return 1
+
+ outdir: Path
+ if output_directory:
+ outdir = output_directory
+ elif repo:
+ outdir = repo / DEFAULT_PATH
+ else:
+ outdir = project_root / DEFAULT_PATH
+
+ ctx = KeepSortedContext(
+ paths=files,
+ fix=fix,
+ output_dir=outdir,
+ failure_summary_log=outdir / 'failure-summary.log',
+ )
+ errors = _process_files(ctx)
+
+ if not fix and errors:
+ _print_howto_fix(list(errors.keys()))
+
+ return int(ctx.failed)
+
+
+def main() -> int:
+ return keep_sorted_in_repo(**vars(parse_args()))
+
+
+if __name__ == '__main__':
+ pw_cli.log.install(logging.INFO)
+ sys.exit(main())
diff --git a/pw_presubmit/py/pw_presubmit/module_owners.py b/pw_presubmit/py/pw_presubmit/module_owners.py
new file mode 100644
index 000000000..f16d6e3d4
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/module_owners.py
@@ -0,0 +1,82 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Ensure all modules have OWNERS files."""
+
+import logging
+from pathlib import Path
+from typing import Callable, Optional, Tuple
+
+from pw_presubmit import (
+ PresubmitContext,
+ PresubmitFailure,
+ presubmit,
+)
+
+_LOG: logging.Logger = logging.getLogger(__name__)
+
+
+def upstream_pigweed_applicability(
+ ctx: PresubmitContext,
+ path: Path,
+) -> Optional[Path]:
+ """Return a parent of path required to have an OWNERS file, or None."""
+ parts: Tuple[str, ...] = path.relative_to(ctx.root).parts
+
+ if len(parts) >= 2 and parts[0].startswith('pw_'):
+ return ctx.root / parts[0]
+ if len(parts) >= 3 and parts[0] in ('targets', 'third_party'):
+ return ctx.root / parts[0] / parts[1]
+
+ return None
+
+
+ApplicabilityFunc = Callable[[PresubmitContext, Path], Optional[Path]]
+
+
+def presubmit_check(
+ applicability: ApplicabilityFunc = upstream_pigweed_applicability,
+) -> presubmit.Check:
+ """Create a presubmit check for the presence of OWNERS files."""
+
+ @presubmit.check(name='module_owners')
+ def check(ctx: PresubmitContext) -> None:
+ """Presubmit check that ensures all modules have OWNERS files."""
+
+ modules_to_check = set()
+
+ for path in ctx.paths:
+ result = applicability(ctx, path)
+ if result:
+ modules_to_check.add(result)
+
+ errors = 0
+ for module in sorted(modules_to_check):
+ _LOG.debug('Checking module %s', module)
+ owners_path = module / 'OWNERS'
+ if not owners_path.is_file():
+ _LOG.error('%s is missing an OWNERS file', module)
+ errors += 1
+ continue
+
+ with owners_path.open() as ins:
+ contents = [x.strip() for x in ins.read().strip().splitlines()]
+ wo_comments = [x for x in contents if not x.startswith('#')]
+ owners = [x for x in wo_comments if 'per-file' not in x]
+ if len(owners) < 1:
+ _LOG.error('%s is too short: add owners', owners_path)
+
+ if errors:
+ raise PresubmitFailure
+
+ return check
diff --git a/pw_presubmit/py/pw_presubmit/ninja_parser.py b/pw_presubmit/py/pw_presubmit/ninja_parser.py
new file mode 100644
index 000000000..131137df6
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/ninja_parser.py
@@ -0,0 +1,64 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Inspired by
+# https://fuchsia.googlesource.com/infra/recipes/+/336933647862a1a9718b4ca18f0a67e89c2419f8/recipe_modules/ninja/resources/ninja_wrapper.py
+"""Extracts a concise error from a ninja log."""
+
+from pathlib import Path
+import re
+
+_RULE_RE = re.compile(r'^\s*\[\d+/\d+\] (\S+)')
+_FAILED_RE = re.compile(r'^\s*FAILED: (.*)$')
+_FAILED_END_RE = re.compile(r'^\s*ninja: build stopped:.*')
+
+
+def parse_ninja_stdout(ninja_stdout: Path) -> str:
+ """Extract an error summary from ninja output."""
+
+ failure_begins = False
+ failure_lines = []
+ last_line = ''
+
+ with ninja_stdout.open() as ins:
+ for line in ins:
+ # Trailing whitespace isn't significant, as it doesn't affect the
+ # way the line shows up in the logs. However, leading whitespace may
+ # be significant, especially for compiler error messages.
+ line = line.rstrip()
+ if failure_begins:
+ if not _RULE_RE.match(line) and not _FAILED_END_RE.match(line):
+ failure_lines.append(line)
+ else:
+ # Output of failed step ends, save its info.
+ failure_begins = False
+ else:
+ failed_nodes_match = _FAILED_RE.match(line)
+ failure_begins = False
+ if failed_nodes_match:
+ failure_begins = True
+ failure_lines.extend([last_line, line])
+ last_line = line
+
+ # Remove "Requirement already satisfied:" lines since many of those might
+ # be printed during Python installation, and they usually have no relevance
+ # to the actual error.
+ failure_lines = [
+ x
+ for x in failure_lines
+ if not x.lstrip().startswith('Requirement already satisfied:')
+ ]
+
+ result = '\n'.join(failure_lines)
+ return re.sub(r'\n+', '\n', result)
diff --git a/pw_presubmit/py/pw_presubmit/npm_presubmit.py b/pw_presubmit/py/pw_presubmit/npm_presubmit.py
new file mode 100644
index 000000000..565d6c91c
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/npm_presubmit.py
@@ -0,0 +1,22 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Presubmit to npm install and run tests"""
+
+from pw_presubmit import call, PresubmitContext
+
+
+def npm_test(ctx: PresubmitContext) -> None:
+ """Run npm install and npm test in Pigweed root to test all web modules"""
+ call('npm', "install", cwd=ctx.root)
+ call('npm', "test", cwd=ctx.root)
diff --git a/pw_presubmit/py/pw_presubmit/owners_checks.py b/pw_presubmit/py/pw_presubmit/owners_checks.py
new file mode 100644
index 000000000..d751ff9a7
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/owners_checks.py
@@ -0,0 +1,461 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+#
+"""OWNERS file checks."""
+
+import argparse
+import collections
+import dataclasses
+import difflib
+import enum
+import functools
+import logging
+import pathlib
+import re
+import sys
+from typing import (
+ Callable,
+ Collection,
+ DefaultDict,
+ Dict,
+ Iterable,
+ List,
+ OrderedDict,
+ Set,
+ Union,
+)
+from pw_presubmit import git_repo
+from pw_presubmit.presubmit import PresubmitFailure
+
+_LOG = logging.getLogger(__name__)
+
+
+class LineType(enum.Enum):
+ COMMENT = "comment"
+ WILDCARD = "wildcard"
+ FILE_LEVEL = "file_level"
+ FILE_RULE = "file_rule"
+ INCLUDE = "include"
+ PER_FILE = "per-file"
+ USER = "user"
+ # Special type to hold lines that don't get attached to another type
+ TRAILING_COMMENTS = "trailing-comments"
+
+
+_LINE_TYPERS: OrderedDict[
+ LineType, Callable[[str], bool]
+] = collections.OrderedDict(
+ (
+ (LineType.COMMENT, lambda x: x.startswith("#")),
+ (LineType.WILDCARD, lambda x: x == "*"),
+ (LineType.FILE_LEVEL, lambda x: x.startswith("set ")),
+ (LineType.FILE_RULE, lambda x: x.startswith("file:")),
+ (LineType.INCLUDE, lambda x: x.startswith("include ")),
+ (LineType.PER_FILE, lambda x: x.startswith("per-file ")),
+ (
+ LineType.USER,
+ lambda x: bool(re.match("^[a-zA-Z1-9.+-]+@[a-zA-Z0-9.-]+", x)),
+ ),
+ )
+)
+
+
+class OwnersError(Exception):
+ """Generic level OWNERS file error."""
+
+ def __init__(self, message: str, *args: object) -> None:
+ super().__init__(*args)
+ self.message = message
+
+
+class FormatterError(OwnersError):
+ """Errors where formatter doesn't know how to act."""
+
+
+class OwnersDuplicateError(OwnersError):
+ """Errors where duplicate lines are found in OWNERS files."""
+
+
+class OwnersUserGrantError(OwnersError):
+ """Invalid user grant, * is used with any other grant."""
+
+
+class OwnersProhibitedError(OwnersError):
+ """Any line that is prohibited by the owners syntax.
+
+ https://android-review.googlesource.com/plugins/code-owners/Documentation/backend-find-owners.html
+ """
+
+
+class OwnersDependencyError(OwnersError):
+ """OWNERS file tried to import file that does not exists."""
+
+
+class OwnersInvalidLineError(OwnersError):
+ """Line in OWNERS file does not match any 'line_typer'."""
+
+
+class OwnersStyleError(OwnersError):
+ """OWNERS file does not match style guide."""
+
+
+@dataclasses.dataclass
+class Line:
+ content: str
+ comments: List[str] = dataclasses.field(default_factory=list)
+
+
+class OwnersFile:
+ """Holds OWNERS file in easy to use parsed structure."""
+
+ path: pathlib.Path
+ original_lines: List[str]
+ sections: Dict[LineType, List[Line]]
+ formatted_lines: List[str]
+
+ def __init__(self, path: pathlib.Path) -> None:
+ if not path.exists():
+ error_msg = f"Tried to import {path} but it does not exists"
+ raise OwnersDependencyError(error_msg)
+ self.path = path
+
+ self.original_lines = self.load_owners_file(self.path)
+ cleaned_lines = self.clean_lines(self.original_lines)
+ self.sections = self.parse_owners(cleaned_lines)
+ self.formatted_lines = self.format_sections(self.sections)
+
+ @staticmethod
+ def load_owners_file(owners_file: pathlib.Path) -> List[str]:
+ return owners_file.read_text().split("\n")
+
+ @staticmethod
+ def clean_lines(dirty_lines: List[str]) -> List[str]:
+ """Removes extra whitespace from list of strings."""
+
+ cleaned_lines = []
+ for line in dirty_lines:
+ line = line.strip() # Remove initial and trailing whitespace
+
+ # Compress duplicated whitespace and remove tabs.
+ # Allow comment lines to break this rule as they may have initial
+ # whitespace for lining up text with neighboring lines.
+ if not line.startswith("#"):
+ line = re.sub(r"\s+", " ", line)
+ if line:
+ cleaned_lines.append(line)
+ return cleaned_lines
+
+ @staticmethod
+ def __find_line_type(line: str) -> LineType:
+ for line_type, type_matcher in _LINE_TYPERS.items():
+ if type_matcher(line):
+ return line_type
+
+ raise OwnersInvalidLineError(
+ f"Unrecognized OWNERS file line, '{line}'."
+ )
+
+ @staticmethod
+ def parse_owners(
+ cleaned_lines: List[str],
+ ) -> DefaultDict[LineType, List[Line]]:
+ """Converts text lines of OWNERS into structured object."""
+ sections: DefaultDict[LineType, List[Line]] = collections.defaultdict(
+ list
+ )
+ comment_buffer: List[str] = []
+
+ def add_line_to_sections(
+ sections, section: LineType, line: str, comment_buffer: List[str]
+ ):
+ if any(
+ seen_line.content == line for seen_line in sections[section]
+ ):
+ raise OwnersDuplicateError(f"Duplicate line '{line}'.")
+ line_obj = Line(content=line, comments=comment_buffer)
+ sections[section].append(line_obj)
+
+ for line in cleaned_lines:
+ line_type: LineType = OwnersFile.__find_line_type(line)
+ if line_type == LineType.COMMENT:
+ comment_buffer.append(line)
+ else:
+ add_line_to_sections(sections, line_type, line, comment_buffer)
+ comment_buffer = []
+
+ add_line_to_sections(
+ sections, LineType.TRAILING_COMMENTS, "", comment_buffer
+ )
+
+ return sections
+
+ @staticmethod
+ def format_sections(
+ sections: DefaultDict[LineType, List[Line]]
+ ) -> List[str]:
+ """Returns ideally styled OWNERS file.
+
+ The styling rules are
+ * Content will be sorted in the following orders with a blank line
+ separating
+ * "set noparent"
+ * "include" lines
+ * "file:" lines
+ * user grants (example, "*", foo@example.com)
+ * "per-file:" lines
+ * Do not combine user grants and "*"
+ * User grants should be sorted alphabetically (this assumes English
+ ordering)
+
+ Returns:
+ List of strings that make up a styled version of a OWNERS file.
+
+ Raises:
+ FormatterError: When formatter does not handle all lines of input.
+ This is a coding error in owners_checks.
+ """
+ all_sections = [
+ LineType.FILE_LEVEL,
+ LineType.INCLUDE,
+ LineType.FILE_RULE,
+ LineType.WILDCARD,
+ LineType.USER,
+ LineType.PER_FILE,
+ LineType.TRAILING_COMMENTS,
+ ]
+ formatted_lines: List[str] = []
+
+ def append_section(line_type):
+ # Add a line of separation if there was a previous section and our
+ # current section has any content. I.e. do not lead with padding and
+ # do not have multiple successive lines of padding.
+ if (
+ formatted_lines
+ and line_type != LineType.TRAILING_COMMENTS
+ and sections[line_type]
+ ):
+ formatted_lines.append("")
+
+ sections[line_type].sort(key=lambda line: line.content)
+ for line in sections[line_type]:
+ # Strip keep-sorted comments out since sorting is done by this
+ # script
+ formatted_lines.extend(
+ [
+ comment
+ for comment in line.comments
+ if not comment.startswith("# keep-sorted: ")
+ ]
+ )
+ formatted_lines.append(line.content)
+
+ for section in all_sections:
+ append_section(section)
+
+ if any(section_name not in all_sections for section_name in sections):
+ raise FormatterError("Formatter did not process all sections.")
+ return formatted_lines
+
+ def check_style(self) -> None:
+ """Checks styling of OWNERS file.
+
+ Enforce consistent style on OWNERS file. This also incidentally detects
+ a few classes of errors.
+
+ Raises:
+ OwnersStyleError: Indicates styled lines do not match original input.
+ """
+
+ if self.original_lines != self.formatted_lines:
+ print(
+ "\n".join(
+ difflib.unified_diff(
+ self.original_lines,
+ self.formatted_lines,
+ fromfile=str(self.path),
+ tofile="styled",
+ lineterm="",
+ )
+ )
+ )
+
+ raise OwnersStyleError(
+ "OWNERS file format does not follow styling."
+ )
+
+ def look_for_owners_errors(self) -> None:
+ """Scans owners files for invalid or useless content."""
+
+ # Confirm when using the wildcard("*") we don't also try to use
+ # individual user grants.
+ if self.sections[LineType.WILDCARD] and self.sections[LineType.USER]:
+ raise OwnersUserGrantError(
+ "Do not use '*' with individual user "
+ "grants, * already applies to all users."
+ )
+
+ # NOTE: Using the include keyword in combination with a per-file rule is
+ # not possible.
+ # https://android-review.googlesource.com/plugins/code-owners/Documentation/backend-find-owners.html#syntax:~:text=NOTE%3A%20Using%20the%20include%20keyword%20in%20combination%20with%20a%20per%2Dfile%20rule%20is%20not%20possible.
+ if self.sections[LineType.INCLUDE] and self.sections[LineType.PER_FILE]:
+ raise OwnersProhibitedError(
+ "'include' cannot be used with 'per-file'."
+ )
+
+ def __complete_path(self, sub_owners_file_path) -> pathlib.Path:
+ """Always return absolute path."""
+ # Absolute paths start with the git/project root
+ if sub_owners_file_path.startswith("/"):
+ root = git_repo.root(self.path)
+ full_path = root / sub_owners_file_path[1:]
+ else:
+ # Relative paths start with owners file dir
+ full_path = self.path.parent / sub_owners_file_path
+ return full_path.resolve()
+
+ def get_dependencies(self) -> List[pathlib.Path]:
+ """Finds owners files this file includes."""
+ dependencies = []
+ # All the includes
+ for include in self.sections.get(LineType.INCLUDE, []):
+ file_str = include.content[len("include ") :]
+ dependencies.append(self.__complete_path(file_str))
+
+ # all file: rules:
+ for file_rule in self.sections.get(LineType.FILE_RULE, []):
+ file_str = file_rule.content[len("file:") :]
+ if ":" in file_str:
+ _LOG.warning(
+ "TODO(b/254322931): This check does not yet support "
+ "<project> or <branch> in a file: rule"
+ )
+ _LOG.warning(
+ "It will not check line '%s' found in %s",
+ file_rule.content,
+ self.path,
+ )
+
+ dependencies.append(self.__complete_path(file_str))
+
+ # all the per-file rule includes
+ for per_file in self.sections.get(LineType.PER_FILE, []):
+ file_str = per_file.content[len("per-file ") :]
+ access_grant = file_str[file_str.index("=") + 1 :]
+ if access_grant.startswith("file:"):
+ dependencies.append(
+ self.__complete_path(access_grant[len("file:") :])
+ )
+
+ return dependencies
+
+ def write_formatted(self) -> None:
+ self.path.write_text("\n".join(self.formatted_lines))
+
+
+def resolve_owners_tree(root_owners: pathlib.Path) -> List[OwnersFile]:
+ """Given a starting OWNERS file return it and all of it's dependencies."""
+ found = []
+ todo = collections.deque((root_owners,))
+ checked: Set[pathlib.Path] = set()
+ while todo:
+ cur_file = todo.popleft()
+ checked.add(cur_file)
+ owners_obj = OwnersFile(cur_file)
+ found.append(owners_obj)
+ new_dependents = owners_obj.get_dependencies()
+ for new_dep in new_dependents:
+ if new_dep not in checked and new_dep not in todo:
+ todo.append(new_dep)
+ return found
+
+
+def _run_owners_checks(owners_obj: OwnersFile) -> None:
+ owners_obj.look_for_owners_errors()
+ owners_obj.check_style()
+
+
+def _format_owners_file(owners_obj: OwnersFile) -> None:
+ owners_obj.look_for_owners_errors()
+
+ if owners_obj.original_lines != owners_obj.formatted_lines:
+ owners_obj.write_formatted()
+
+
+def _list_unwrapper(
+ func, list_or_path: Union[Iterable[pathlib.Path], pathlib.Path]
+) -> Dict[pathlib.Path, str]:
+ """Decorator that accepts Paths or list of Paths and iterates as needed."""
+ errors: Dict[pathlib.Path, str] = {}
+ if isinstance(list_or_path, Iterable):
+ files = list_or_path
+ else:
+ files = (list_or_path,)
+
+ all_owners_obj: List[OwnersFile] = []
+ for file in files:
+ all_owners_obj.extend(resolve_owners_tree(file))
+
+ checked: Set[pathlib.Path] = set()
+ for current_owners in all_owners_obj:
+ # Ensure we don't check the same file twice
+ if current_owners.path in checked:
+ continue
+ checked.add(current_owners.path)
+ try:
+ func(current_owners)
+ except OwnersError as err:
+ errors[current_owners.path] = err.message
+ _LOG.error(
+ "%s: %s", str(current_owners.path.absolute()), err.message
+ )
+ return errors
+
+
+# This generates decorated versions of the functions that can used with both
+# formatter (which supplies files one at a time) and presubmits (which supplies
+# list of files).
+run_owners_checks = functools.partial(_list_unwrapper, _run_owners_checks)
+format_owners_file = functools.partial(_list_unwrapper, _format_owners_file)
+
+
+def presubmit_check(
+ files: Union[pathlib.Path, Collection[pathlib.Path]]
+) -> None:
+ errors = run_owners_checks(files)
+ if errors:
+ for file in errors:
+ _LOG.warning(" pw format --fix %s", file)
+ _LOG.warning("will automatically fix this.")
+ raise PresubmitFailure
+
+
+def main() -> int:
+ """Standalone test of styling."""
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--style", action="store_true")
+ parser.add_argument("--owners_file", required=True, type=str)
+ args = parser.parse_args()
+
+ try:
+ owners_obj = OwnersFile(pathlib.Path(args.owners_file))
+ owners_obj.look_for_owners_errors()
+ owners_obj.check_style()
+ except OwnersError as err:
+ _LOG.error("%s %s", err, err.message)
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
index d7b65d32c..1369fbc5e 100755
--- a/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/pigweed_presubmit.py
@@ -21,17 +21,17 @@ import logging
import os
from pathlib import Path
import re
+import shutil
import subprocess
import sys
-from typing import Sequence, IO, Tuple, Optional, Callable, List
+from typing import Callable, Iterable, List, Sequence, TextIO
try:
import pw_presubmit
except ImportError:
# Append the pw_presubmit package path to the module search path to allow
# running this module without installing the pw_presubmit package.
- sys.path.append(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__))))
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pw_presubmit
import pw_package.pigweed_packages
@@ -42,28 +42,48 @@ from pw_presubmit import (
cpp_checks,
format_code,
git_repo,
+ gitmodules,
call,
filter_paths,
inclusive_language,
+ keep_sorted,
+ module_owners,
+ npm_presubmit,
+ owners_checks,
plural,
+ presubmit,
PresubmitContext,
PresubmitFailure,
Programs,
python_checks,
+ shell_checks,
+ source_in_build,
+ todo_check,
)
-from pw_presubmit.install_hook import install_hook
+from pw_presubmit.install_hook import install_git_hook
_LOG = logging.getLogger(__name__)
pw_package.pigweed_packages.initialize()
# Trigger builds if files with these extensions change.
-_BUILD_EXTENSIONS = ('.py', '.rst', '.gn', '.gni',
- *format_code.C_FORMAT.extensions)
+_BUILD_FILE_FILTER = presubmit.FileFilter(
+ suffix=(
+ *format_code.C_FORMAT.extensions,
+ '.cfg',
+ '.py',
+ '.rst',
+ '.gn',
+ '.gni',
+ '.emb',
+ )
+)
+
+_OPTIMIZATION_LEVELS = 'debug', 'size_optimized', 'speed_optimized'
def _at_all_optimization_levels(target):
- for level in ('debug', 'size_optimized', 'speed_optimized'):
+ for level in _OPTIMIZATION_LEVELS:
yield f'{target}_{level}'
@@ -71,360 +91,448 @@ def _at_all_optimization_levels(target):
# Build presubmit checks
#
def gn_clang_build(ctx: PresubmitContext):
- build.gn_gen(ctx.root,
- ctx.output_dir,
- pw_RUN_INTEGRATION_TESTS=(sys.platform != 'win32'))
- build.ninja(ctx.output_dir, *_at_all_optimization_levels('host_clang'))
+ """Checks all compile targets that rely on LLVM tooling."""
+ build_targets = [
+ *_at_all_optimization_levels('host_clang'),
+ 'cpp14_compatibility',
+ 'cpp20_compatibility',
+ 'asan',
+ 'tsan',
+ 'ubsan',
+ 'runtime_sanitizers',
+ # TODO(b/234876100): msan will not work until the C++ standard library
+ # included in the sysroot has a variant built with msan.
+ ]
+ # clang-tidy doesn't run on Windows.
+ if sys.platform != 'win32':
+ build_targets.append('static_analysis')
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_gcc_build(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, *_at_all_optimization_levels('host_gcc'))
+ # QEMU doesn't run on Windows.
+ if sys.platform != 'win32':
+ # TODO(b/244604080): For the pw::InlineString tests, qemu_clang_debug
+ # and qemu_clang_speed_optimized produce a binary too large for the
+ # QEMU target's 256KB flash. Restore debug and speed optimized
+ # builds when this is fixed.
+ build_targets.append('qemu_clang_size_optimized')
+ # TODO(b/240982565): SocketStream currently requires Linux.
+ if sys.platform.startswith('linux'):
+ build_targets.append('integration_tests')
-_HOST_COMPILER = 'gcc' if sys.platform == 'win32' else 'clang'
+ build.gn_gen(ctx, pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS)
+ build.ninja(ctx, *build_targets)
-def gn_host_build(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir,
- *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'))
+_HOST_COMPILER = 'gcc' if sys.platform == 'win32' else 'clang'
-@filter_paths(endswith=_BUILD_EXTENSIONS)
+@_BUILD_FILE_FILTER.apply_to_check()
def gn_quick_build_check(ctx: PresubmitContext):
"""Checks the state of the GN build by running gn gen and gn check."""
- build.gn_gen(ctx.root, ctx.output_dir)
-
-
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_full_build_check(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, *_at_all_optimization_levels('stm32f429i'),
- *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
- 'python.tests', 'python.lint', 'docs', 'fuzzers',
- 'pw_env_setup:build_pigweed_python_source_tree')
+ build.gn_gen(ctx)
-@filter_paths(endswith=_BUILD_EXTENSIONS)
+@_BUILD_FILE_FILTER.apply_to_check()
def gn_full_qemu_check(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
+ build.gn_gen(ctx, pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS)
build.ninja(
- ctx.output_dir,
+ ctx,
*_at_all_optimization_levels('qemu_gcc'),
- # TODO(pwbug/321) Re-enable clang.
- # *_at_all_optimization_levels('qemu_clang'),
+ *_at_all_optimization_levels('qemu_clang'),
)
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_arm_build(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, *_at_all_optimization_levels('stm32f429i'))
+def _gn_combined_build_check_targets() -> Sequence[str]:
+ build_targets = [
+ 'check_modules',
+ *_at_all_optimization_levels('stm32f429i'),
+ *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
+ 'python.tests',
+ 'python.lint',
+ 'docs',
+ 'fuzzers',
+ 'pigweed_pypi_distribution',
+ ]
+ # TODO(b/234645359): Re-enable on Windows when compatibility tests build.
+ if sys.platform != 'win32':
+ build_targets.append('cpp14_compatibility')
+ build_targets.append('cpp20_compatibility')
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def stm32f429i(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir, pw_use_test_server=True)
- with build.test_server('stm32f429i_disc1_test_server', ctx.output_dir):
- build.ninja(ctx.output_dir, *_at_all_optimization_levels('stm32f429i'))
+ # clang-tidy doesn't run on Windows.
+ if sys.platform != 'win32':
+ build_targets.append('static_analysis')
+ # QEMU doesn't run on Windows.
+ if sys.platform != 'win32':
+ build_targets.extend(_at_all_optimization_levels('qemu_gcc'))
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_boringssl_build(ctx: PresubmitContext):
- build.install_package(ctx.package_root, 'boringssl')
- build.gn_gen(ctx.root,
- ctx.output_dir,
- dir_pw_third_party_boringssl='"{}"'.format(ctx.package_root /
- 'boringssl'))
- build.ninja(
- ctx.output_dir,
- *_at_all_optimization_levels('stm32f429i'),
- *_at_all_optimization_levels('host_clang'),
- )
+ # TODO(b/244604080): For the pw::InlineString tests, qemu_clang_debug
+ # and qemu_clang_speed_optimized produce a binary too large for the
+ # QEMU target's 256KB flash. Restore debug and speed optimized
+ # builds when this is fixed.
+ build_targets.append('qemu_clang_size_optimized')
+ # TODO(b/240982565): SocketStream currently requires Linux.
+ if sys.platform.startswith('linux'):
+ build_targets.append('integration_tests')
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_nanopb_build(ctx: PresubmitContext):
- build.install_package(ctx.package_root, 'nanopb')
- build.gn_gen(ctx.root,
- ctx.output_dir,
- dir_pw_third_party_nanopb='"{}"'.format(ctx.package_root /
- 'nanopb'))
- build.ninja(
- ctx.output_dir,
- *_at_all_optimization_levels('stm32f429i'),
- *_at_all_optimization_levels('host_clang'),
- )
+ return build_targets
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_crypto_mbedtls_build(ctx: PresubmitContext):
- build.install_package(ctx.package_root, 'mbedtls')
- build.gn_gen(
- ctx.root,
- ctx.output_dir,
- dir_pw_third_party_mbedtls='"{}"'.format(ctx.package_root / 'mbedtls'),
- pw_crypto_SHA256_BACKEND='"{}"'.format(ctx.root /
- 'pw_crypto:sha256_mbedtls'),
- pw_crypto_ECDSA_BACKEND='"{}"'.format(ctx.root /
- 'pw_crypto:ecdsa_mbedtls'))
- build.ninja(ctx.output_dir)
-
-
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_crypto_boringssl_build(ctx: PresubmitContext):
- build.install_package(ctx.package_root, 'boringssl')
- build.gn_gen(
- ctx.root,
- ctx.output_dir,
- dir_pw_third_party_boringssl='"{}"'.format(ctx.package_root /
- 'boringssl'),
- pw_crypto_SHA256_BACKEND='"{}"'.format(ctx.root /
- 'pw_crypto:sha256_boringssl'),
- pw_crypto_ECDSA_BACKEND='"{}"'.format(ctx.root /
- 'pw_crypto:ecdsa_boringssl'),
- )
- build.ninja(ctx.output_dir)
-
-
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_crypto_micro_ecc_build(ctx: PresubmitContext):
- build.install_package(ctx.package_root, 'micro-ecc')
- build.gn_gen(
- ctx.root,
- ctx.output_dir,
- dir_pw_third_party_micro_ecc='"{}"'.format(ctx.package_root /
- 'micro-ecc'),
- pw_crypto_ECDSA_BACKEND='"{}"'.format(ctx.root /
- 'pw_crypto:ecdsa_uecc'),
- )
- build.ninja(ctx.output_dir)
-
-
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_teensy_build(ctx: PresubmitContext):
- build.install_package(ctx.package_root, 'teensy')
- build.gn_gen(ctx.root,
- ctx.output_dir,
- pw_arduino_build_CORE_PATH='"{}"'.format(str(
- ctx.package_root)),
- pw_arduino_build_CORE_NAME='teensy',
- pw_arduino_build_PACKAGE_NAME='teensy/avr',
- pw_arduino_build_BOARD='teensy40')
- build.ninja(ctx.output_dir, *_at_all_optimization_levels('arduino'))
-
-
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_software_update_build(ctx: PresubmitContext):
- build.install_package(ctx.package_root, 'nanopb')
- build.install_package(ctx.package_root, 'protobuf')
- build.install_package(ctx.package_root, 'mbedtls')
- build.install_package(ctx.package_root, 'micro-ecc')
- build.gn_gen(
- ctx.root,
- ctx.output_dir,
- dir_pw_third_party_protobuf='"{}"'.format(ctx.package_root /
- 'protobuf'),
- dir_pw_third_party_nanopb='"{}"'.format(ctx.package_root / 'nanopb'),
- dir_pw_third_party_micro_ecc='"{}"'.format(ctx.package_root /
- 'micro-ecc'),
- pw_crypto_ECDSA_BACKEND='"{}"'.format(ctx.root /
- 'pw_crypto:ecdsa_uecc'),
- dir_pw_third_party_mbedtls='"{}"'.format(ctx.package_root / 'mbedtls'),
- pw_crypto_SHA256_BACKEND='"{}"'.format(ctx.root /
- 'pw_crypto:sha256_mbedtls'))
- build.ninja(
- ctx.output_dir,
- *_at_all_optimization_levels('host_clang'),
- )
+gn_combined_build_check = build.GnGenNinja(
+ name='gn_combined_build_check',
+ doc='Run most host and device (QEMU) tests.',
+ path_filter=_BUILD_FILE_FILTER,
+ gn_args=dict(pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS),
+ ninja_targets=_gn_combined_build_check_targets(),
+)
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_pw_system_demo_build(ctx: PresubmitContext):
- build.install_package(ctx.package_root, 'freertos')
- build.install_package(ctx.package_root, 'nanopb')
- build.install_package(ctx.package_root, 'stm32cube_f4')
- build.gn_gen(
- ctx.root,
- ctx.output_dir,
- dir_pw_third_party_freertos='"{}"'.format(ctx.package_root /
- 'freertos'),
- dir_pw_third_party_nanopb='"{}"'.format(ctx.package_root / 'nanopb'),
- dir_pw_third_party_stm32cube_f4='"{}"'.format(ctx.package_root /
- 'stm32cube_f4'),
- )
- build.ninja(ctx.output_dir, 'pw_system_demo')
+@_BUILD_FILE_FILTER.apply_to_check()
+def gn_arm_build(ctx: PresubmitContext):
+ build.gn_gen(ctx, pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS)
+ build.ninja(ctx, *_at_all_optimization_levels('stm32f429i'))
+
+
+stm32f429i = build.GnGenNinja(
+ name='stm32f429i',
+ path_filter=_BUILD_FILE_FILTER,
+ gn_args={
+ 'pw_use_test_server': True,
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_contexts=(
+ lambda ctx: build.test_server(
+ 'stm32f429i_disc1_test_server',
+ ctx.output_dir,
+ ),
+ ),
+ ninja_targets=_at_all_optimization_levels('stm32f429i'),
+)
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_qemu_build(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, *_at_all_optimization_levels('qemu_gcc'))
+gn_emboss_build = build.GnGenNinja(
+ name='gn_emboss_build',
+ packages=('emboss',),
+ gn_args=dict(
+ dir_pw_third_party_emboss=lambda ctx: '"{}"'.format(
+ ctx.package_root / 'emboss'
+ ),
+ pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
+ ),
+ ninja_targets=(*_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),),
+)
+
+gn_nanopb_build = build.GnGenNinja(
+ name='gn_nanopb_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('nanopb',),
+ gn_args=dict(
+ dir_pw_third_party_nanopb=lambda ctx: '"{}"'.format(
+ ctx.package_root / 'nanopb'
+ ),
+ pw_C_OPTIMIZATION_LEVELS=_OPTIMIZATION_LEVELS,
+ ),
+ ninja_targets=(
+ *_at_all_optimization_levels('stm32f429i'),
+ *_at_all_optimization_levels('host_clang'),
+ ),
+)
+gn_crypto_mbedtls_build = build.GnGenNinja(
+ name='gn_crypto_mbedtls_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('mbedtls',),
+ gn_args={
+ 'dir_pw_third_party_mbedtls': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'mbedtls'
+ ),
+ 'pw_crypto_SHA256_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'pw_crypto:sha256_mbedtls'
+ ),
+ 'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'pw_crypto:ecdsa_mbedtls'
+ ),
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=(
+ *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
+ # TODO(b/240982565): SocketStream currently requires Linux.
+ *(('integration_tests',) if sys.platform.startswith('linux') else ()),
+ ),
+)
-@filter_paths(endswith=_BUILD_EXTENSIONS)
-def gn_qemu_clang_build(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, *_at_all_optimization_levels('qemu_clang'))
+gn_crypto_boringssl_build = build.GnGenNinja(
+ name='gn_crypto_boringssl_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('boringssl',),
+ gn_args={
+ 'dir_pw_third_party_boringssl': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'boringssl'
+ ),
+ 'pw_crypto_SHA256_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'pw_crypto:sha256_boringssl'
+ ),
+ 'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'pw_crypto:ecdsa_boringssl'
+ ),
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=(
+ *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
+ # TODO(b/240982565): SocketStream currently requires Linux.
+ *(('integration_tests',) if sys.platform.startswith('linux') else ()),
+ ),
+)
+gn_crypto_micro_ecc_build = build.GnGenNinja(
+ name='gn_crypto_micro_ecc_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('micro-ecc',),
+ gn_args={
+ 'dir_pw_third_party_micro_ecc': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'micro-ecc'
+ ),
+ 'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'pw_crypto:ecdsa_uecc'
+ ),
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=(
+ *_at_all_optimization_levels(f'host_{_HOST_COMPILER}'),
+ # TODO(b/240982565): SocketStream currently requires Linux.
+ *(('integration_tests',) if sys.platform.startswith('linux') else ()),
+ ),
+)
-def gn_docs_build(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'docs')
+gn_teensy_build = build.GnGenNinja(
+ name='gn_teensy_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('teensy',),
+ gn_args={
+ 'pw_arduino_build_CORE_PATH': lambda ctx: '"{}"'.format(
+ str(ctx.package_root)
+ ),
+ 'pw_arduino_build_CORE_NAME': 'teensy',
+ 'pw_arduino_build_PACKAGE_NAME': 'teensy/avr',
+ 'pw_arduino_build_BOARD': 'teensy40',
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=_at_all_optimization_levels('arduino'),
+)
+gn_pico_build = build.GnGenNinja(
+ name='gn_pico_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('pico_sdk',),
+ gn_args={
+ 'PICO_SRC_DIR': lambda ctx: '"{}"'.format(
+ str(ctx.package_root / 'pico_sdk')
+ ),
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=('pi_pico',),
+)
-def gn_host_tools(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'host_tools')
+gn_software_update_build = build.GnGenNinja(
+ name='gn_software_update_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('nanopb', 'protobuf', 'mbedtls', 'micro-ecc'),
+ gn_args={
+ 'dir_pw_third_party_protobuf': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'protobuf'
+ ),
+ 'dir_pw_third_party_nanopb': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'nanopb'
+ ),
+ 'dir_pw_third_party_micro_ecc': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'micro-ecc'
+ ),
+ 'pw_crypto_ECDSA_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'pw_crypto:ecdsa_uecc'
+ ),
+ 'dir_pw_third_party_mbedtls': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'mbedtls'
+ ),
+ 'pw_crypto_SHA256_BACKEND': lambda ctx: '"{}"'.format(
+ ctx.root / 'pw_crypto:sha256_mbedtls'
+ ),
+ 'pw_C_OPTIMIZATION_LEVELS': _OPTIMIZATION_LEVELS,
+ },
+ ninja_targets=_at_all_optimization_levels('host_clang'),
+)
-@filter_paths(endswith=format_code.C_FORMAT.extensions)
-def oss_fuzz_build(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir, pw_toolchain_OSS_FUZZ_ENABLED=True)
- build.ninja(ctx.output_dir, "fuzzers")
+gn_pw_system_demo_build = build.GnGenNinja(
+ name='gn_pw_system_demo_build',
+ path_filter=_BUILD_FILE_FILTER,
+ packages=('freertos', 'nanopb', 'stm32cube_f4', 'pico_sdk'),
+ gn_args={
+ 'dir_pw_third_party_freertos': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'freertos'
+ ),
+ 'dir_pw_third_party_nanopb': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'nanopb'
+ ),
+ 'dir_pw_third_party_stm32cube_f4': lambda ctx: '"{}"'.format(
+ ctx.package_root / 'stm32cube_f4'
+ ),
+ 'PICO_SRC_DIR': lambda ctx: '"{}"'.format(
+ str(ctx.package_root / 'pico_sdk')
+ ),
+ },
+ ninja_targets=('pw_system_demo',),
+)
+
+gn_docs_build = build.GnGenNinja(name='gn_docs_build', ninja_targets=('docs',))
+
+gn_host_tools = build.GnGenNinja(
+ name='gn_host_tools',
+ ninja_targets=('host_tools',),
+)
def _run_cmake(ctx: PresubmitContext, toolchain='host_clang') -> None:
- build.install_package(ctx.package_root, 'nanopb')
+ build.install_package(ctx, 'nanopb')
env = None
if 'clang' in toolchain:
env = build.env_with_clang_vars()
toolchain_path = ctx.root / 'pw_toolchain' / toolchain / 'toolchain.cmake'
- build.cmake(ctx.root,
- ctx.output_dir,
- f'-DCMAKE_TOOLCHAIN_FILE={toolchain_path}',
- '-DCMAKE_EXPORT_COMPILE_COMMANDS=1',
- f'-Ddir_pw_third_party_nanopb={ctx.package_root / "nanopb"}',
- '-Dpw_third_party_nanopb_ADD_SUBDIRECTORY=ON',
- env=env)
+ build.cmake(
+ ctx,
+ f'-DCMAKE_TOOLCHAIN_FILE={toolchain_path}',
+ '-DCMAKE_EXPORT_COMPILE_COMMANDS=1',
+ f'-Ddir_pw_third_party_nanopb={ctx.package_root / "nanopb"}',
+ '-Dpw_third_party_nanopb_ADD_SUBDIRECTORY=ON',
+ env=env,
+ )
-@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.cmake',
- 'CMakeLists.txt'))
+@filter_paths(
+ endswith=(*format_code.C_FORMAT.extensions, '.cmake', 'CMakeLists.txt')
+)
def cmake_clang(ctx: PresubmitContext):
_run_cmake(ctx, toolchain='host_clang')
- build.ninja(ctx.output_dir, 'pw_apps', 'pw_run_tests.modules')
+ build.ninja(ctx, 'pw_apps', 'pw_run_tests.modules')
-@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.cmake',
- 'CMakeLists.txt'))
+@filter_paths(
+ endswith=(*format_code.C_FORMAT.extensions, '.cmake', 'CMakeLists.txt')
+)
def cmake_gcc(ctx: PresubmitContext):
_run_cmake(ctx, toolchain='host_gcc')
- build.ninja(ctx.output_dir, 'pw_apps', 'pw_run_tests.modules')
-
-
-# TODO(pwbug/180): Slowly add modules here that work with bazel until all
-# modules are added. Then replace with //...
-_MODULES_THAT_BUILD_WITH_BAZEL = [
- '//pw_allocator/...',
- '//pw_analog/...',
- '//pw_assert/...',
- '//pw_assert_basic/...',
- '//pw_assert_log/...',
- '//pw_base64/...',
- '//pw_bloat/...',
- '//pw_build/...',
- '//pw_checksum/...',
- '//pw_chrono_embos/...',
- '//pw_chrono_freertos/...',
- '//pw_chrono_stl/...',
- '//pw_chrono_threadx/...',
- '//pw_cli/...',
- '//pw_containers/...',
- '//pw_cpu_exception/...',
- '//pw_docgen/...',
- '//pw_doctor/...',
- '//pw_env_setup/...',
- '//pw_fuzzer/...',
- '//pw_hex_dump/...',
- '//pw_i2c/...',
- '//pw_interrupt/...',
- '//pw_interrupt_cortex_m/...',
- '//pw_libc/...',
- '//pw_log/...',
- '//pw_log_basic/...',
- '//pw_malloc/...',
- '//pw_malloc_freelist/...',
- '//pw_multisink/...',
- '//pw_polyfill/...',
- '//pw_preprocessor/...',
- '//pw_protobuf/...',
- '//pw_protobuf_compiler/...',
- '//pw_random/...',
- '//pw_result/...',
- '//pw_rpc/...',
- '//pw_span/...',
- '//pw_status/...',
- '//pw_stream/...',
- '//pw_string/...',
- '//pw_sync_baremetal/...',
- '//pw_sync_embos/...',
- '//pw_sync_freertos/...',
- '//pw_sync_stl/...',
- '//pw_sync_threadx/...',
- '//pw_sys_io/...',
- '//pw_sys_io_baremetal_lm3s6965evb/...',
- '//pw_sys_io_baremetal_stm32f429/...',
- '//pw_sys_io_stdio/...',
- '//pw_thread_stl/...',
- '//pw_tool/...',
- '//pw_toolchain/...',
- '//pw_transfer/...',
- '//pw_unit_test/...',
- '//pw_varint/...',
- '//pw_web_ui/...',
-]
-
-# TODO(pwbug/180): Slowly add modules here that work with bazel until all
-# modules are added. Then replace with //...
-_MODULES_THAT_TEST_WITH_BAZEL = [
- '//pw_allocator/...',
- '//pw_analog/...',
- '//pw_assert/...',
- '//pw_base64/...',
- '//pw_checksum/...',
- '//pw_cli/...',
- '//pw_containers/...',
- '//pw_hex_dump/...',
- '//pw_i2c/...',
- '//pw_libc/...',
- '//pw_log/...',
- '//pw_multisink/...',
- '//pw_polyfill/...',
- '//pw_preprocessor/...',
- '//pw_protobuf/...',
- '//pw_protobuf_compiler/...',
- '//pw_random/...',
- '//pw_result/...',
- '//pw_rpc/...',
- '//pw_span/...',
- '//pw_status/...',
- '//pw_stream/...',
- '//pw_string/...',
- '//pw_thread_stl/...',
- '//pw_unit_test/...',
- '//pw_varint/...',
- '//:buildifier_test',
-]
-
-
-@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.bazel', '.bzl',
- 'BUILD'))
+ build.ninja(ctx, 'pw_apps', 'pw_run_tests.modules')
+
+
+@filter_paths(
+ endswith=(*format_code.C_FORMAT.extensions, '.bazel', '.bzl', 'BUILD')
+)
def bazel_test(ctx: PresubmitContext) -> None:
- """Runs bazel test on each bazel compatible module"""
- build.bazel(ctx, 'test', *_MODULES_THAT_TEST_WITH_BAZEL,
- '--test_output=errors')
+ """Runs bazel test on the entire repo."""
+ build.bazel(
+ ctx,
+ 'test',
+ '--test_output=errors',
+ '--',
+ '//...',
+ )
-@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.bazel', '.bzl',
- 'BUILD'))
+@filter_paths(
+ endswith=(
+ *format_code.C_FORMAT.extensions,
+ '.bazel',
+ '.bzl',
+ '.py',
+ '.rs',
+ 'BUILD',
+ )
+)
def bazel_build(ctx: PresubmitContext) -> None:
- """Runs Bazel build on each Bazel compatible module."""
- build.bazel(ctx, 'build', *_MODULES_THAT_BUILD_WITH_BAZEL)
+ """Runs Bazel build for each supported platform."""
+ # Build everything with the default flags.
+ build.bazel(
+ ctx,
+ 'build',
+ '--',
+ '//...',
+ )
+
+ # Mapping from Bazel platforms to targets which should be built for those
+ # platforms.
+ targets_for_platform = {
+ "//pw_build/platforms:lm3s6965evb": [
+ "//pw_rust/examples/embedded_hello:hello",
+ ],
+ "//pw_build/platforms:microbit": [
+ "//pw_rust/examples/embedded_hello:hello",
+ ],
+ }
+
+ for cxxversion in ('c++17', 'c++20'):
+ # Explicitly build for each supported C++ version.
+ build.bazel(
+ ctx,
+ 'build',
+ f"--cxxopt=-std={cxxversion}",
+ '--',
+ '//...',
+ )
+
+ for platform, targets in targets_for_platform.items():
+ build.bazel(
+ ctx,
+ 'build',
+ f'--platforms={platform}',
+ f"--cxxopt='-std={cxxversion}'",
+ *targets,
+ )
+
+ # Provide some coverage of the FreeRTOS build.
+ #
+ # This is just a minimal presubmit intended to ensure we don't break what
+ # support we have.
+ #
+ # TODO(b/271465588): Eventually just build the entire repo for this
+ # platform.
+ build.bazel(
+ ctx,
+ 'build',
+ # Designated initializers produce a warning-treated-as-error when
+ # compiled with -std=c++17.
+ #
+ # TODO(b/271299438): Remove this.
+ '--copt=-Wno-pedantic',
+ '--platforms=//pw_build/platforms:testonly_freertos',
+ '//pw_sync/...',
+ '//pw_thread/...',
+ '//pw_thread_freertos/...',
+ )
+
+
+def pw_transfer_integration_test(ctx: PresubmitContext) -> None:
+ """Runs the pw_transfer cross-language integration test only.
+
+ This test is not part of the regular bazel build because it's slow and
+ intended to run in CI only.
+ """
+ build.bazel(
+ ctx,
+ 'test',
+ '//pw_transfer/integration_test:cross_language_small_test',
+ '//pw_transfer/integration_test:cross_language_medium_read_test',
+ '//pw_transfer/integration_test:cross_language_medium_write_test',
+ '//pw_transfer/integration_test:cross_language_large_read_test',
+ '//pw_transfer/integration_test:cross_language_large_write_test',
+ '//pw_transfer/integration_test:multi_transfer_test',
+ '//pw_transfer/integration_test:expected_errors_test',
+ '//pw_transfer/integration_test:legacy_binaries_test',
+ '--test_output=errors',
+ )
#
@@ -440,13 +548,17 @@ def _clang_system_include_paths(lang: str) -> List[str]:
"""
# Dump system include paths with preprocessor verbose.
command = [
- 'clang++', '-Xpreprocessor', '-v', '-x', f'{lang}', f'{os.devnull}',
- '-fsyntax-only'
+ 'clang++',
+ '-Xpreprocessor',
+ '-v',
+ '-x',
+ f'{lang}',
+ f'{os.devnull}',
+ '-fsyntax-only',
]
- process = subprocess.run(command,
- check=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ process = subprocess.run(
+ command, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
# Parse the command output to retrieve system include paths.
# The paths are listed one per line.
@@ -460,8 +572,9 @@ def _clang_system_include_paths(lang: str) -> List[str]:
return include_paths
-def edit_compile_commands(in_path: Path, out_path: Path,
- func: Callable[[str, str, str], str]) -> None:
+def edit_compile_commands(
+ in_path: Path, out_path: Path, func: Callable[[str, str, str], str]
+) -> None:
"""Edit the selected compile command file.
Calls the input callback on all triplets (file, directory, command) in
@@ -471,121 +584,121 @@ def edit_compile_commands(in_path: Path, out_path: Path,
with open(in_path) as in_file:
compile_commands = json.load(in_file)
for item in compile_commands:
- item['command'] = func(item['file'], item['directory'],
- item['command'])
+ item['command'] = func(
+ item['file'], item['directory'], item['command']
+ )
with open(out_path, 'w') as out_file:
json.dump(compile_commands, out_file, indent=2)
-# The first line must be regex because of the '20\d\d' date
-COPYRIGHT_FIRST_LINE = r'Copyright 20\d\d The Pigweed Authors'
-COPYRIGHT_COMMENTS = r'(#|//| \*|REM|::)'
-COPYRIGHT_BLOCK_COMMENTS = (
- # HTML comments
- (r'<!--', r'-->'),
- # Jinja comments
- (r'{#', r'#}'),
-)
-
-COPYRIGHT_FIRST_LINE_EXCEPTIONS = (
- '#!',
- '/*',
- '@echo off',
- '# -*-',
- ':',
-)
-
-COPYRIGHT_LINES = tuple("""\
-
-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
-
- https://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.
-""".splitlines())
-
_EXCLUDE_FROM_COPYRIGHT_NOTICE: Sequence[str] = (
# Configuration
- r'^(?:.+/)?\..+$',
+ # keep-sorted: start
+ r'\bDoxyfile$',
r'\bPW_PLUGINS$',
r'\bconstraint.list$',
+ r'^(?:.+/)?\..+$',
+ # keep-sorted: end
# Metadata
- r'^docker/tag$',
+ # keep-sorted: start
r'\bAUTHORS$',
r'\bLICENSE$',
r'\bOWNERS$',
r'\bPIGWEED_MODULES$',
- r'\brequirements.txt$',
r'\bgo.(mod|sum)$',
+ r'\bpackage-lock.json$',
r'\bpackage.json$',
+ r'\brequirements.txt$',
r'\byarn.lock$',
+ r'^docker/tag$',
+ # keep-sorted: end
# Data files
+ # keep-sorted: start
+ r'\.bin$',
+ r'\.csv$',
r'\.elf$',
r'\.gif$',
+ r'\.ico$',
r'\.jpg$',
r'\.json$',
r'\.png$',
r'\.svg$',
r'\.xml$',
+ # keep-sorted: end
# Documentation
+ # keep-sorted: start
r'\.md$',
r'\.rst$',
+ # keep-sorted: end
# Generated protobuf files
- r'\.pb\.h$',
+ # keep-sorted: start
r'\.pb\.c$',
+ r'\.pb\.h$',
r'\_pb2.pyi?$',
+ # keep-sorted: end
# Diff/Patch files
+ # keep-sorted: start
r'\.diff$',
r'\.patch$',
+ # keep-sorted: end
+ # Test data
+ # keep-sorted: start
+ r'\bpw_presubmit/py/test/owners_checks/',
+ # keep-sorted: end
)
+# Regular expression for the copyright comment. "\1" refers to the comment
+# characters and "\2" refers to space after the comment characters, if any.
+# All period characters are escaped using a replace call.
+# pylint: disable=line-too-long
+_COPYRIGHT = re.compile(
+ r"""(#|//|::| \*|)( ?)Copyright 2\d{3} The Pigweed Authors
+\1
+\1\2Licensed under the Apache License, Version 2.0 \(the "License"\); you may not
+\1\2use this file except in compliance with the License. You may obtain a copy of
+\1\2the License at
+\1
+\1(?:\2 |\t)https://www.apache.org/licenses/LICENSE-2.0
+\1
+\1\2Unless required by applicable law or agreed to in writing, software
+\1\2distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+\1\2WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+\1\2License for the specific language governing permissions and limitations under
+\1\2the License.
+""".replace(
+ '.', r'\.'
+ ),
+ re.MULTILINE,
+)
+# pylint: enable=line-too-long
+
+_SKIP_LINE_PREFIXES = (
+ '#!',
+ '@echo off',
+ ':<<',
+ '/*',
+ ' * @jest-environment jsdom',
+ ' */',
+ '{#', # Jinja comment block
+ '# -*- coding: utf-8 -*-',
+ '<!--',
+)
-def match_block_comment_start(line: str) -> Optional[str]:
- """Matches the start of a block comment and returns the end."""
- for block_comment in COPYRIGHT_BLOCK_COMMENTS:
- if re.match(block_comment[0], line):
- # Return the end of the block comment
- return block_comment[1]
- return None
+def _read_notice_lines(file: TextIO) -> Iterable[str]:
+ lines = iter(file)
+ try:
+ # Read until the first line of the copyright notice.
+ line = next(lines)
+ while line.isspace() or line.startswith(_SKIP_LINE_PREFIXES):
+ line = next(lines)
-def copyright_read_first_line(
- file: IO) -> Tuple[Optional[str], Optional[str], Optional[str]]:
- """Reads the file until it reads a valid first copyright line.
+ yield line
- Returns (comment, block_comment, line). comment and block_comment are
- mutually exclusive and refer to the comment character sequence and whether
- they form a block comment or a line comment. line is the first line of
- the copyright, and is used for error reporting.
- """
- line = file.readline()
- first_line_matcher = re.compile(COPYRIGHT_COMMENTS + ' ' +
- COPYRIGHT_FIRST_LINE)
- while line:
- end_block_comment = match_block_comment_start(line)
- if end_block_comment:
- next_line = file.readline()
- copyright_line = re.match(COPYRIGHT_FIRST_LINE, next_line)
- if not copyright_line:
- return (None, None, line)
- return (None, end_block_comment, line)
-
- first_line = first_line_matcher.match(line)
- if first_line:
- return (first_line.group(1), None, line)
-
- if (line.strip()
- and not line.startswith(COPYRIGHT_FIRST_LINE_EXCEPTIONS)):
- return (None, None, line)
-
- line = file.readline()
- return (None, None, None)
+ for _ in range(12): # The notice is 13 lines; read the remaining 12.
+ yield next(lines)
+ except StopIteration:
+ return
@filter_paths(exclude=_EXCLUDE_FROM_COPYRIGHT_NOTICE)
@@ -594,89 +707,41 @@ def copyright_notice(ctx: PresubmitContext):
errors = []
for path in ctx.paths:
-
if path.stat().st_size == 0:
continue # Skip empty files
- if path.is_dir():
- continue # Skip submodules which are included in ctx.paths.
-
with path.open() as file:
- (comment, end_block_comment,
- line) = copyright_read_first_line(file)
-
- if not line:
- _LOG.warning('%s: invalid first line', path)
+ if not _COPYRIGHT.match(''.join(_read_notice_lines(file))):
errors.append(path)
- continue
-
- if not (comment or end_block_comment):
- _LOG.warning('%s: invalid first line %r', path, line)
- errors.append(path)
- continue
-
- if end_block_comment:
- expected_lines = COPYRIGHT_LINES + (end_block_comment, )
- else:
- expected_lines = COPYRIGHT_LINES
-
- for expected, actual in zip(expected_lines, file):
- if end_block_comment:
- expected_line = expected + '\n'
- elif comment:
- expected_line = (comment + ' ' + expected).rstrip() + '\n'
-
- if expected_line != actual:
- _LOG.warning(' bad line: %r', actual)
- _LOG.warning(' expected: %r', expected_line)
- errors.append(path)
- break
if errors:
- _LOG.warning('%s with a missing or incorrect copyright notice:\n%s',
- plural(errors, 'file'), '\n'.join(str(e) for e in errors))
+ _LOG.warning(
+ '%s with a missing or incorrect copyright notice:\n%s',
+ plural(errors, 'file'),
+ '\n'.join(str(e) for e in errors),
+ )
raise PresubmitFailure
-_BAZEL_SOURCES_IN_BUILD = tuple(format_code.C_FORMAT.extensions)
-_GN_SOURCES_IN_BUILD = ('setup.cfg', '.toml', '.rst', '.py',
- *_BAZEL_SOURCES_IN_BUILD)
-
-
-@filter_paths(endswith=(*_GN_SOURCES_IN_BUILD, 'BUILD', '.bzl', '.gn', '.gni'),
- exclude=['zephyr.*/', 'android.*/'])
-def source_is_in_build_files(ctx: PresubmitContext):
- """Checks that source files are in the GN and Bazel builds."""
- missing = build.check_builds_for_files(
- _BAZEL_SOURCES_IN_BUILD,
- _GN_SOURCES_IN_BUILD,
- ctx.paths,
- bazel_dirs=[ctx.root],
- gn_build_files=git_repo.list_files(pathspecs=['BUILD.gn', '*BUILD.gn'],
- repo_path=ctx.root))
-
- if missing:
- _LOG.warning(
- 'All source files must appear in BUILD and BUILD.gn files')
- raise PresubmitFailure
+@filter_paths(endswith=format_code.CPP_SOURCE_EXTS)
+def source_is_in_cmake_build_warn_only(ctx: PresubmitContext):
+ """Checks that source files are in the CMake build."""
_run_cmake(ctx)
- cmake_missing = build.check_compile_commands_for_files(
+ missing = build.check_compile_commands_for_files(
ctx.output_dir / 'compile_commands.json',
- (f for f in ctx.paths if f.suffix in ('.c', '.cc')))
- if cmake_missing:
- _LOG.warning('The CMake build is missing %d files', len(cmake_missing))
- _LOG.warning('Files missing from CMake:\n%s',
- '\n'.join(str(f) for f in cmake_missing))
- # TODO(hepler): Many files are missing from the CMake build. Make this
- # check an error when the missing files are fixed.
- # raise PresubmitFailure
+ (f for f in ctx.paths if f.suffix in format_code.CPP_SOURCE_EXTS),
+ )
+ if missing:
+ _LOG.warning(
+ 'Files missing from CMake:\n%s',
+ '\n'.join(str(f) for f in missing),
+ )
def build_env_setup(ctx: PresubmitContext):
if 'PW_CARGO_SETUP' not in os.environ:
- _LOG.warning(
- 'Skipping build_env_setup since PW_CARGO_SETUP is not set')
+ _LOG.warning('Skipping build_env_setup since PW_CARGO_SETUP is not set')
return
tmpl = ctx.root.joinpath('pw_env_setup', 'py', 'pyoxidizer.bzl.tmpl')
@@ -690,8 +755,20 @@ def build_env_setup(ctx: PresubmitContext):
call('pyoxidizer', 'build', cwd=ctx.output_dir)
+def _valid_capitalization(word: str) -> bool:
+ """Checks that the word has a capital letter or is not a regular word."""
+ return bool(
+ any(c.isupper() for c in word) # Any capitalizatian (iTelephone)
+ or not word.isalpha() # Non-alphabetical (cool_stuff.exe)
+ or shutil.which(word)
+ ) # Matches an executable (clangd)
+
+
def commit_message_format(_: PresubmitContext):
"""Checks that the top commit's message is correctly formatted."""
+ if git_repo.commit_author().endswith('gserviceaccount.com'):
+ return
+
lines = git_repo.commit_message().splitlines()
# Show limits and current commit message in log.
@@ -699,50 +776,104 @@ def commit_message_format(_: PresubmitContext):
for line in lines:
_LOG.debug(line)
+ if not lines:
+ _LOG.error('The commit message is too short!')
+ raise PresubmitFailure
+
# Ignore Gerrit-generated reverts.
- if ('Revert' in lines[0]
- and 'This reverts commit ' in git_repo.commit_message()
- and 'Reason for revert: ' in git_repo.commit_message()):
+ if (
+ 'Revert' in lines[0]
+ and 'This reverts commit ' in git_repo.commit_message()
+ and 'Reason for revert: ' in git_repo.commit_message()
+ ):
_LOG.warning('Ignoring apparent Gerrit-generated revert')
return
- if not lines:
- _LOG.error('The commit message is too short!')
- raise PresubmitFailure
+ # Ignore Gerrit-generated relands
+ if (
+ 'Reland' in lines[0]
+ and 'This is a reland of ' in git_repo.commit_message()
+ and "Original change's description: " in git_repo.commit_message()
+ ):
+ _LOG.warning('Ignoring apparent Gerrit-generated reland')
+ return
errors = 0
if len(lines[0]) > 72:
- _LOG.warning("The commit message's first line must be no longer than "
- '72 characters.')
- _LOG.warning('The first line is %d characters:\n %s', len(lines[0]),
- lines[0])
+ _LOG.warning(
+ "The commit message's first line must be no longer than "
+ '72 characters.'
+ )
+ _LOG.warning(
+ 'The first line is %d characters:\n %s', len(lines[0]), lines[0]
+ )
errors += 1
if lines[0].endswith('.'):
_LOG.warning(
"The commit message's first line must not end with a period:\n %s",
- lines[0])
+ lines[0],
+ )
+ errors += 1
+
+ # Check that the first line matches the expected pattern.
+ match = re.match(
+ r'^(?:[\w*/]+(?:{[\w* ,]+})?[\w*/]*|SEED-\d+): (?P<desc>.+)$', lines[0]
+ )
+ if not match:
+ _LOG.warning('The first line does not match the expected format')
+ _LOG.warning(
+ 'Expected:\n\n module_or_target: The description\n\n'
+ 'Found:\n\n %s\n',
+ lines[0],
+ )
+ errors += 1
+ elif not _valid_capitalization(match.group('desc').split()[0]):
+ _LOG.warning(
+ 'The first word after the ":" in the first line ("%s") must be '
+ 'capitalized:\n %s',
+ match.group('desc').split()[0],
+ lines[0],
+ )
errors += 1
if len(lines) > 1 and lines[1]:
_LOG.warning("The commit message's second line must be blank.")
- _LOG.warning('The second line has %d characters:\n %s', len(lines[1]),
- lines[1])
+ _LOG.warning(
+ 'The second line has %d characters:\n %s', len(lines[1]), lines[1]
+ )
errors += 1
- # Check that the lines are 72 characters or less, but skip any lines that
- # might possibly have a URL, path, or metadata in them. Also skip any lines
- # with non-ASCII characters.
+ # Ignore the line length check for Copybara imports so they can include the
+ # commit hash and description for imported commits.
+ if not errors and (
+ 'Copybara import' in lines[0]
+ and 'GitOrigin-RevId:' in git_repo.commit_message()
+ ):
+ _LOG.warning('Ignoring Copybara import')
+ return
+
+ # Check that the lines are 72 characters or less.
for i, line in enumerate(lines[2:], 3):
- if any(c in line for c in ':/>') or not line.isascii():
+ # Skip any lines that might possibly have a URL, path, or metadata in
+ # them.
+ if any(c in line for c in ':/>'):
+ continue
+
+ # Skip any lines with non-ASCII characters.
+ if not line.isascii():
+ continue
+
+ # Skip any blockquoted lines.
+ if line.startswith(' '):
continue
if len(line) > 72:
_LOG.warning(
- 'Commit message lines must be no longer than 72 characters.')
- _LOG.warning('Line %d has %d characters:\n %s', i, len(line),
- line)
+ 'Commit message lines must be no longer than 72 characters.'
+ )
+ _LOG.warning('Line %d has %d characters:\n %s', i, len(line), line)
errors += 1
if errors:
@@ -753,13 +884,66 @@ def commit_message_format(_: PresubmitContext):
@filter_paths(endswith=(*format_code.C_FORMAT.extensions, '.py'))
def static_analysis(ctx: PresubmitContext):
"""Runs all available static analysis tools."""
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'python.lint', 'static_analysis')
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'python.lint', 'static_analysis')
+
+
+_EXCLUDE_FROM_TODO_CHECK = (
+ # keep-sorted: start
+ r'.bazelrc$',
+ r'.dockerignore$',
+ r'.gitignore$',
+ r'.pylintrc$',
+ r'\bdocs/build_system.rst',
+ r'\bpw_assert_basic/basic_handler.cc',
+ r'\bpw_assert_basic/public/pw_assert_basic/handler.h',
+ r'\bpw_blob_store/public/pw_blob_store/flat_file_system_entry.h',
+ r'\bpw_build/linker_script.gni',
+ r'\bpw_build/py/pw_build/copy_from_cipd.py',
+ r'\bpw_cpu_exception/basic_handler.cc',
+ r'\bpw_cpu_exception_cortex_m/entry.cc',
+ r'\bpw_cpu_exception_cortex_m/exception_entry_test.cc',
+ r'\bpw_doctor/py/pw_doctor/doctor.py',
+ r'\bpw_env_setup/util.sh',
+ r'\bpw_fuzzer/fuzzer.gni',
+ r'\bpw_i2c/BUILD.gn',
+ r'\bpw_i2c/public/pw_i2c/register_device.h',
+ r'\bpw_kvs/flash_memory.cc',
+ r'\bpw_kvs/key_value_store.cc',
+ r'\bpw_log_basic/log_basic.cc',
+ r'\bpw_package/py/pw_package/packages/chromium_verifier.py',
+ r'\bpw_protobuf/encoder.cc',
+ r'\bpw_rpc/docs.rst',
+ r'\bpw_watch/py/pw_watch/watch.py',
+ r'\btargets/mimxrt595_evk/BUILD.bazel',
+ r'\btargets/stm32f429i_disc1/boot.cc',
+ r'\bthird_party/chromium_verifier/BUILD.gn',
+ # keep-sorted: end
+)
+
+
+@filter_paths(exclude=_EXCLUDE_FROM_TODO_CHECK)
+def todo_check_with_exceptions(ctx: PresubmitContext):
+ """Check that non-legacy TODO lines are valid.""" # todo-check: ignore
+ todo_check.create(todo_check.BUGS_OR_USERNAMES)(ctx)
+
+
+@format_code.OWNERS_CODE_FORMAT.filter.apply_to_check()
+def owners_lint_checks(ctx: PresubmitContext):
+ """Runs OWNERS linter."""
+ owners_checks.presubmit_check(ctx.paths)
-def renode_check(ctx: PresubmitContext):
- """Placeholder for future check."""
- _LOG.info('%s %s', ctx.root, ctx.output_dir)
+SOURCE_FILES_FILTER = presubmit.FileFilter(
+ endswith=_BUILD_FILE_FILTER.endswith,
+ suffix=('.bazel', '.bzl', '.gn', '.gni', *_BUILD_FILE_FILTER.suffix),
+ exclude=(
+ r'zephyr.*',
+ r'android.*',
+ r'\.black.toml',
+ r'pyproject.toml',
+ ),
+)
#
@@ -767,44 +951,80 @@ def renode_check(ctx: PresubmitContext):
#
OTHER_CHECKS = (
- cpp_checks.all_sanitizers(),
- # Build that attempts to duplicate the build OSS-Fuzz does. Currently
- # failing.
- oss_fuzz_build,
- # TODO(pwbug/346): Enable all Bazel tests when they're fixed.
+ # keep-sorted: start
+ # TODO(b/235277910): Enable all Bazel tests when they're fixed.
bazel_test,
+ build.gn_gen_check,
cmake_clang,
cmake_gcc,
- gn_boringssl_build,
- build.gn_gen_check,
+ gitmodules.create(),
+ gn_clang_build,
+ gn_combined_build_check,
+ module_owners.presubmit_check(),
+ npm_presubmit.npm_test,
+ pw_transfer_integration_test,
+ # TODO(hepler): Many files are missing from the CMake build. Add this check
+ # to lintformat when the missing files are fixed.
+ source_in_build.cmake(SOURCE_FILES_FILTER, _run_cmake),
+ static_analysis,
+ stm32f429i,
+ todo_check.create(todo_check.BUGS_OR_USERNAMES),
+ # keep-sorted: end
+)
+
+# The misc program differs from other_checks in that checks in the misc
+# program block CQ on Linux.
+MISC = (
+ # keep-sorted: start
+ gn_emboss_build,
gn_nanopb_build,
- gn_crypto_mbedtls_build,
+ gn_pico_build,
+ gn_pw_system_demo_build,
+ gn_teensy_build,
+ # keep-sorted: end
+)
+
+SANITIZERS = (cpp_checks.all_sanitizers(),)
+
+SECURITY = (
+ # keep-sorted: start
gn_crypto_boringssl_build,
+ gn_crypto_mbedtls_build,
gn_crypto_micro_ecc_build,
gn_software_update_build,
- gn_full_build_check,
- gn_full_qemu_check,
- gn_clang_build,
- gn_gcc_build,
- gn_pw_system_demo_build,
- renode_check,
- stm32f429i,
+ # keep-sorted: end
)
+# Avoid running all checks on specific paths.
+PATH_EXCLUSIONS = (re.compile(r'\bthird_party/fuchsia/repo/'),)
+
_LINTFORMAT = (
commit_message_format,
copyright_notice,
format_code.presubmit_checks(),
- inclusive_language.inclusive_language.with_filter(
- exclude=(r'\byarn.lock$', )),
+ inclusive_language.presubmit_check.with_filter(
+ exclude=(
+ r'\byarn.lock$',
+ r'\bpackage-lock.json$',
+ )
+ ),
cpp_checks.pragma_once,
build.bazel_lint,
- source_is_in_build_files,
+ owners_lint_checks,
+ source_in_build.gn(SOURCE_FILES_FILTER),
+ source_is_in_cmake_build_warn_only,
+ shell_checks.shellcheck if shutil.which('shellcheck') else (),
+ keep_sorted.presubmit_check,
+ todo_check_with_exceptions,
)
LINTFORMAT = (
_LINTFORMAT,
- static_analysis,
+ # This check is excluded from _LINTFORMAT because it's not quick: it issues
+ # a bazel query that pulls in all of Pigweed's external dependencies
+ # (https://stackoverflow.com/q/71024130/1224002). These are cached, but
+ # after a roll it can be quite slow.
+ source_in_build.bazel(SOURCE_FILES_FILTER),
pw_presubmit.python_checks.check_python_versions,
pw_presubmit.python_checks.gn_python_lint,
)
@@ -812,7 +1032,7 @@ LINTFORMAT = (
QUICK = (
_LINTFORMAT,
gn_quick_build_check,
- # TODO(pwbug/141): Re-enable CMake and Bazel for Mac after we have fixed the
+ # TODO(b/34884583): Re-enable CMake and Bazel for Mac after we have fixed
# the clang issues. The problem is that all clang++ invocations need the
# two extra flags: "-nostdc++" and "${clang_prefix}/../lib/libc++.a".
cmake_clang if sys.platform != 'darwin' else (),
@@ -820,20 +1040,10 @@ QUICK = (
FULL = (
_LINTFORMAT,
- gn_host_build,
- gn_arm_build,
- gn_docs_build,
+ gn_combined_build_check,
gn_host_tools,
bazel_test if sys.platform == 'linux' else (),
bazel_build if sys.platform == 'linux' else (),
- # On Mac OS, system 'gcc' is a symlink to 'clang' by default, so skip GCC
- # host builds on Mac for now. Skip it on Windows too, since gn_host_build
- # already uses 'gcc' on Windows.
- gn_gcc_build if sys.platform not in ('darwin', 'win32') else (),
- # Windows doesn't support QEMU yet.
- gn_qemu_build if sys.platform != 'win32' else (),
- gn_qemu_clang_build if sys.platform != 'win32' else (),
- source_is_in_build_files,
python_checks.gn_python_check,
python_checks.gn_python_test_coverage,
build_env_setup,
@@ -843,10 +1053,15 @@ FULL = (
)
PROGRAMS = Programs(
+ # keep-sorted: start
full=FULL,
lintformat=LINTFORMAT,
+ misc=MISC,
other_checks=OTHER_CHECKS,
quick=QUICK,
+ sanitizers=SANITIZERS,
+ security=SECURITY,
+ # keep-sorted: end
)
@@ -858,23 +1073,32 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
'--install',
action='store_true',
- help='Install the presubmit as a Git pre-push hook and exit.')
+ help='Install the presubmit as a Git pre-push hook and exit.',
+ )
return parser.parse_args()
-def run(install: bool, **presubmit_args) -> int:
+def run(install: bool, exclude: list, **presubmit_args) -> int:
"""Entry point for presubmit."""
if install:
- # TODO(pwbug/209, pwbug/386) inclusive-language: disable
- install_hook(__file__, 'pre-push',
- ['--base', 'origin/master..HEAD', '--program', 'quick'],
- Path.cwd())
- # TODO(pwbug/209, pwbug/386) inclusive-language: enable
+ install_git_hook(
+ 'pre-push',
+ [
+ 'python',
+ '-m',
+ 'pw_presubmit.pigweed_presubmit',
+ '--base',
+ 'origin/main..HEAD',
+ '--program',
+ 'quick',
+ ],
+ )
return 0
- return cli.run(**presubmit_args)
+ exclude.extend(PATH_EXCLUSIONS)
+ return cli.run(exclude=exclude, **presubmit_args)
def main() -> int:
diff --git a/pw_presubmit/py/pw_presubmit/presubmit.py b/pw_presubmit/py/pw_presubmit/presubmit.py
index 08585d04e..ef10a3abe 100644
--- a/pw_presubmit/py/pw_presubmit/presubmit.py
+++ b/pw_presubmit/py/pw_presubmit/presubmit.py
@@ -41,33 +41,50 @@ from __future__ import annotations
import collections
import contextlib
+import copy
import dataclasses
import enum
from inspect import Parameter, signature
import itertools
+import json
import logging
import os
from pathlib import Path
import re
+import shutil
+import signal
import subprocess
+import sys
+import tempfile as tf
import time
-from typing import (Callable, Collection, Dict, Iterable, Iterator, List,
- NamedTuple, Optional, Pattern, Sequence, Set, Tuple, Union)
-
+import types
+from typing import (
+ Any,
+ Callable,
+ Collection,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Pattern,
+ Sequence,
+ Set,
+ Tuple,
+ Union,
+)
+import urllib
+
+import pw_cli.color
import pw_cli.env
+import pw_env_setup.config_file
+from pw_package import package_manager
from pw_presubmit import git_repo, tools
from pw_presubmit.tools import plural
_LOG: logging.Logger = logging.getLogger(__name__)
-color_red = tools.make_color(31)
-color_bold_red = tools.make_color(31, 1)
-color_black_on_red = tools.make_color(30, 41)
-color_yellow = tools.make_color(33, 1)
-color_green = tools.make_color(32)
-color_black_on_green = tools.make_color(30, 42)
-color_aqua = tools.make_color(36)
-color_bold_white = tools.make_color(37, 1)
+_COLOR = pw_cli.color.colors()
_SUMMARY_BOX = '══╦╗ ║║══╩╝'
_CHECK_UPPER = '━━━┓ '
@@ -93,34 +110,46 @@ def _format_time(time_s: float) -> str:
def _box(style, left, middle, right, box=tools.make_box('><>')) -> str:
- return box.format(*style,
- section1=left + ('' if left.endswith(' ') else ' '),
- width1=_LEFT,
- section2=' ' + middle,
- width2=WIDTH - _LEFT - _RIGHT - 4,
- section3=right + ' ',
- width3=_RIGHT)
+ return box.format(
+ *style,
+ section1=left + ('' if left.endswith(' ') else ' '),
+ width1=_LEFT,
+ section2=' ' + middle,
+ width2=WIDTH - _LEFT - _RIGHT - 4,
+ section3=right + ' ',
+ width3=_RIGHT,
+ )
class PresubmitFailure(Exception):
"""Optional exception to use for presubmit failures."""
- def __init__(self, description: str = '', path=None):
- super().__init__(f'{path}: {description}' if path else description)
-
-
-class _Result(enum.Enum):
+ def __init__(
+ self,
+ description: str = '',
+ path: Optional[Path] = None,
+ line: Optional[int] = None,
+ ):
+ line_part: str = ''
+ if line is not None:
+ line_part = f'{line}:'
+ super().__init__(
+ f'{path}:{line_part} {description}' if path else description
+ )
+
+
+class PresubmitResult(enum.Enum):
PASS = 'PASSED' # Check completed successfully.
FAIL = 'FAILED' # Check failed.
CANCEL = 'CANCEL' # Check didn't complete.
def colorized(self, width: int, invert: bool = False) -> str:
- if self is _Result.PASS:
- color = color_black_on_green if invert else color_green
- elif self is _Result.FAIL:
- color = color_black_on_red if invert else color_red
- elif self is _Result.CANCEL:
- color = color_yellow
+ if self is PresubmitResult.PASS:
+ color = _COLOR.black_on_green if invert else _COLOR.green
+ elif self is PresubmitResult.FAIL:
+ color = _COLOR.black_on_red if invert else _COLOR.red
+ elif self is PresubmitResult.CANCEL:
+ color = _COLOR.yellow
else:
color = lambda value: value
@@ -130,9 +159,18 @@ class _Result(enum.Enum):
class Program(collections.abc.Sequence):
"""A sequence of presubmit checks; basically a tuple with a name."""
+
def __init__(self, name: str, steps: Iterable[Callable]):
self.name = name
- self._steps = tuple({s: None for s in tools.flatten(steps)})
+
+ def ensure_check(step):
+ if isinstance(step, Check):
+ return step
+ return Check(step)
+
+ self._steps: tuple[Check, ...] = tuple(
+ {ensure_check(s): None for s in tools.flatten(steps)}
+ )
def __getitem__(self, i):
return self._steps[i]
@@ -152,6 +190,7 @@ class Programs(collections.abc.Mapping):
Use is optional. Helpful when managing multiple presubmit check programs.
"""
+
def __init__(self, **programs: Sequence):
"""Initializes a name: program mapping from the provided keyword args.
@@ -159,12 +198,11 @@ class Programs(collections.abc.Mapping):
contain nested sequences, which are flattened.
"""
self._programs: Dict[str, Program] = {
- name: Program(name, checks)
- for name, checks in programs.items()
+ name: Program(name, checks) for name, checks in programs.items()
}
- def all_steps(self) -> Dict[str, Callable]:
- return {c.__name__: c for c in itertools.chain(*self.values())}
+ def all_steps(self) -> Dict[str, Check]:
+ return {c.name: c for c in itertools.chain(*self.values())}
def __getitem__(self, item: str) -> Program:
return self._programs[item]
@@ -177,34 +215,474 @@ class Programs(collections.abc.Mapping):
@dataclasses.dataclass(frozen=True)
-class PresubmitContext:
- """Context passed into presubmit checks."""
+class FormatOptions:
+ python_formatter: Optional[str] = 'yapf'
+ black_path: Optional[str] = 'black'
+
+ # TODO(b/264578594) Add exclude to pigweed.json file.
+ # exclude: Sequence[re.Pattern] = dataclasses.field(default_factory=list)
+
+ @staticmethod
+ def load() -> 'FormatOptions':
+ config = pw_env_setup.config_file.load()
+ fmt = config.get('pw', {}).get('pw_presubmit', {}).get('format', {})
+ return FormatOptions(
+ python_formatter=fmt.get('python_formatter', 'yapf'),
+ black_path=fmt.get('black_path', 'black'),
+ # exclude=tuple(re.compile(x) for x in fmt.get('exclude', ())),
+ )
+
+
+@dataclasses.dataclass
+class LuciPipeline:
+ round: int
+ builds_from_previous_iteration: Sequence[str]
+
+ @staticmethod
+ def create(
+ bbid: int,
+ fake_pipeline_props: Optional[Dict[str, Any]] = None,
+ ) -> Optional['LuciPipeline']:
+ pipeline_props: Dict[str, Any]
+ if fake_pipeline_props is not None:
+ pipeline_props = fake_pipeline_props
+ else:
+ pipeline_props = (
+ get_buildbucket_info(bbid)
+ .get('input', {})
+ .get('properties', {})
+ .get('$pigweed/pipeline', {})
+ )
+ if not pipeline_props.get('inside_a_pipeline', False):
+ return None
+
+ return LuciPipeline(
+ round=int(pipeline_props['round']),
+ builds_from_previous_iteration=list(
+ pipeline_props['builds_from_previous_iteration']
+ ),
+ )
+
+
+def get_buildbucket_info(bbid) -> Dict[str, Any]:
+ if not bbid or not shutil.which('bb'):
+ return {}
+
+ output = subprocess.check_output(
+ ['bb', 'get', '-json', '-p', f'{bbid}'], text=True
+ )
+ return json.loads(output)
+
+
+def download_cas_artifact(
+ ctx: PresubmitContext, digest: str, output_dir: str
+) -> None:
+ """Downloads the given digest to the given outputdirectory
+
+ Args:
+ ctx: the presubmit context
+ digest:
+ a string digest in the form "<digest hash>/<size bytes>"
+ i.e 693a04e41374150d9d4b645fccb49d6f96e10b527c7a24b1e17b331f508aa73b/86
+ output_dir: the directory we want to download the artifacts to
+ """
+ if ctx.luci is None:
+ raise PresubmitFailure('Lucicontext is None')
+ cmd = [
+ 'cas',
+ 'download',
+ '-cas-instance',
+ ctx.luci.cas_instance,
+ '-digest',
+ digest,
+ '-dir',
+ output_dir,
+ ]
+ try:
+ subprocess.check_call(cmd)
+ except subprocess.CalledProcessError as failure:
+ raise PresubmitFailure('cas download failed') from failure
+
+
+def archive_cas_artifact(
+ ctx: PresubmitContext, root: str, upload_paths: List[str]
+) -> str:
+ """Uploads the given artifacts into cas
+
+ Args:
+ ctx: the presubmit context
+ root: root directory of archived tree, should be absolutepath.
+ paths: path to archived files/dirs, should be absolute path.
+ If empty, [root] will be used.
+
+ Returns:
+ A string digest in the form "<digest hash>/<size bytes>"
+ i.e 693a04e41374150d9d4b645fccb49d6f96e10b527c7a24b1e17b331f508aa73b/86
+ """
+ if ctx.luci is None:
+ raise PresubmitFailure('Lucicontext is None')
+ assert os.path.abspath(root)
+ if not upload_paths:
+ upload_paths = [root]
+ for path in upload_paths:
+ assert os.path.abspath(path)
+
+ with tf.NamedTemporaryFile(mode='w+t') as tmp_digest_file:
+ with tf.NamedTemporaryFile(mode='w+t') as tmp_paths_file:
+ json_paths = json.dumps(
+ [
+ [str(root), str(os.path.relpath(path, root))]
+ for path in upload_paths
+ ]
+ )
+ tmp_paths_file.write(json_paths)
+ tmp_paths_file.seek(0)
+ cmd = [
+ 'cas',
+ 'archive',
+ '-cas-instance',
+ ctx.luci.cas_instance,
+ '-paths-json',
+ tmp_paths_file.name,
+ '-dump-digest',
+ tmp_digest_file.name,
+ ]
+ try:
+ subprocess.check_call(cmd)
+ except subprocess.CalledProcessError as failure:
+ raise PresubmitFailure('cas archive failed') from failure
+
+ tmp_digest_file.seek(0)
+ uploaded_digest = tmp_digest_file.read()
+ return uploaded_digest
+
+
+@dataclasses.dataclass
+class LuciTrigger:
+ """Details the pending change or submitted commit triggering the build."""
+
+ number: int
+ remote: str
+ branch: str
+ ref: str
+ gerrit_name: str
+ submitted: bool
+
+ @property
+ def gerrit_url(self):
+ if not self.number:
+ return self.gitiles_url
+ return 'https://{}-review.googlesource.com/c/{}'.format(
+ self.gerrit_name, self.number
+ )
+
+ @property
+ def gitiles_url(self):
+ return '{}/+/{}'.format(self.remote, self.ref)
+
+ @staticmethod
+ def create_from_environment(
+ env: Optional[Dict[str, str]] = None,
+ ) -> Sequence['LuciTrigger']:
+ if not env:
+ env = os.environ.copy()
+ raw_path = env.get('TRIGGERING_CHANGES_JSON')
+ if not raw_path:
+ return ()
+ path = Path(raw_path)
+ if not path.is_file():
+ return ()
+
+ result = []
+ with open(path, 'r') as ins:
+ for trigger in json.load(ins):
+ keys = {
+ 'number',
+ 'remote',
+ 'branch',
+ 'ref',
+ 'gerrit_name',
+ 'submitted',
+ }
+ if keys <= trigger.keys():
+ result.append(LuciTrigger(**{x: trigger[x] for x in keys}))
+
+ return tuple(result)
+
+ @staticmethod
+ def create_for_testing():
+ change = {
+ 'number': 123456,
+ 'remote': 'https://pigweed.googlesource.com/pigweed/pigweed',
+ 'branch': 'main',
+ 'ref': 'refs/changes/56/123456/1',
+ 'gerrit_name': 'pigweed',
+ 'submitted': True,
+ }
+ with tf.TemporaryDirectory() as tempdir:
+ changes_json = Path(tempdir) / 'changes.json'
+ with changes_json.open('w') as outs:
+ json.dump([change], outs)
+ env = {'TRIGGERING_CHANGES_JSON': changes_json}
+ return LuciTrigger.create_from_environment(env)
+
+
+@dataclasses.dataclass
+class LuciContext:
+ """LUCI-specific information about the environment."""
+
+ buildbucket_id: int
+ build_number: int
+ project: str
+ bucket: str
+ builder: str
+ swarming_server: str
+ swarming_task_id: str
+ cas_instance: str
+ pipeline: Optional[LuciPipeline]
+ triggers: Sequence[LuciTrigger] = dataclasses.field(default_factory=tuple)
+
+ @staticmethod
+ def create_from_environment(
+ env: Optional[Dict[str, str]] = None,
+ fake_pipeline_props: Optional[Dict[str, Any]] = None,
+ ) -> Optional['LuciContext']:
+ """Create a LuciContext from the environment."""
+
+ if not env:
+ env = os.environ.copy()
+
+ luci_vars = [
+ 'BUILDBUCKET_ID',
+ 'BUILDBUCKET_NAME',
+ 'BUILD_NUMBER',
+ 'SWARMING_TASK_ID',
+ 'SWARMING_SERVER',
+ ]
+ if any(x for x in luci_vars if x not in env):
+ return None
+
+ project, bucket, builder = env['BUILDBUCKET_NAME'].split(':')
+
+ bbid: int = 0
+ pipeline: Optional[LuciPipeline] = None
+ try:
+ bbid = int(env['BUILDBUCKET_ID'])
+ pipeline = LuciPipeline.create(bbid, fake_pipeline_props)
+
+ except ValueError:
+ pass
+
+ # Logic to identify cas instance from swarming server is derived from
+ # https://chromium.googlesource.com/infra/luci/recipes-py/+/main/recipe_modules/cas/api.py
+ swarm_server = env['SWARMING_SERVER']
+ cas_project = urllib.parse.urlparse(swarm_server).netloc.split('.')[0]
+ cas_instance = f'projects/{cas_project}/instances/default_instance'
+
+ result = LuciContext(
+ buildbucket_id=bbid,
+ build_number=int(env['BUILD_NUMBER']),
+ project=project,
+ bucket=bucket,
+ builder=builder,
+ swarming_server=env['SWARMING_SERVER'],
+ swarming_task_id=env['SWARMING_TASK_ID'],
+ cas_instance=cas_instance,
+ pipeline=pipeline,
+ triggers=LuciTrigger.create_from_environment(env),
+ )
+ _LOG.debug('%r', result)
+ return result
+
+ @staticmethod
+ def create_for_testing():
+ env = {
+ 'BUILDBUCKET_ID': '881234567890',
+ 'BUILDBUCKET_NAME': 'pigweed:bucket.try:builder-name',
+ 'BUILD_NUMBER': '123',
+ 'SWARMING_SERVER': 'https://chromium-swarm.appspot.com',
+ 'SWARMING_TASK_ID': 'cd2dac62d2',
+ }
+ return LuciContext.create_from_environment(env, {})
+
+
+@dataclasses.dataclass
+class FormatContext:
+ """Context passed into formatting helpers.
+
+ This class is a subset of PresubmitContext containing only what's needed by
+ formatters.
+
+ For full documentation on the members see the PresubmitContext section of
+ pw_presubmit/docs.rst.
+
+ Args:
+ root: Source checkout root directory
+ output_dir: Output directory for this specific language
+ paths: Modified files for the presubmit step to check (often used in
+ formatting steps but ignored in compile steps)
+ package_root: Root directory for pw package installations
+ format_options: Formatting options, derived from pigweed.json
+ """
+
+ root: Optional[Path]
+ output_dir: Path
+ paths: Tuple[Path, ...]
+ package_root: Path
+ format_options: FormatOptions
+
+
+@dataclasses.dataclass
+class PresubmitContext: # pylint: disable=too-many-instance-attributes
+ """Context passed into presubmit checks.
+
+ For full documentation on the members see pw_presubmit/docs.rst.
+
+ Args:
+ root: Source checkout root directory
+ repos: Repositories (top-level and submodules) processed by
+ pw presubmit
+ output_dir: Output directory for this specific presubmit step
+ failure_summary_log: Path where steps should write a brief summary of
+ any failures encountered for use by other tooling.
+ paths: Modified files for the presubmit step to check (often used in
+ formatting steps but ignored in compile steps)
+ all_paths: All files in the tree.
+ package_root: Root directory for pw package installations
+ override_gn_args: Additional GN args processed by build.gn_gen()
+ luci: Information about the LUCI build or None if not running in LUCI
+ format_options: Formatting options, derived from pigweed.json
+ num_jobs: Number of jobs to run in parallel
+ continue_after_build_error: For steps that compile, don't exit on the
+ first compilation error
+ """
+
root: Path
repos: Tuple[Path, ...]
output_dir: Path
+ failure_summary_log: Path
paths: Tuple[Path, ...]
+ all_paths: Tuple[Path, ...]
package_root: Path
+ luci: Optional[LuciContext]
+ override_gn_args: Dict[str, str]
+ format_options: FormatOptions
+ num_jobs: Optional[int] = None
+ continue_after_build_error: bool = False
+ _failed: bool = False
- def relative_paths(self, start: Optional[Path] = None) -> Tuple[Path, ...]:
- return tuple(
- tools.relative_paths(self.paths, start if start else self.root))
+ @property
+ def failed(self) -> bool:
+ return self._failed
- def paths_by_repo(self) -> Dict[Path, List[Path]]:
- repos = collections.defaultdict(list)
+ def fail(
+ self,
+ description: str,
+ path: Optional[Path] = None,
+ line: Optional[int] = None,
+ ):
+ """Add a failure to this presubmit step.
+
+ If this is called at least once the step fails, but not immediately—the
+ check is free to continue and possibly call this method again.
+ """
+ _LOG.warning('%s', PresubmitFailure(description, path, line))
+ self._failed = True
+
+ @staticmethod
+ def create_for_testing():
+ parsed_env = pw_cli.env.pigweed_environment()
+ root = parsed_env.PW_PROJECT_ROOT
+ presubmit_root = root / 'out' / 'presubmit'
+ return PresubmitContext(
+ root=root,
+ repos=(root,),
+ output_dir=presubmit_root / 'test',
+ failure_summary_log=presubmit_root / 'failure-summary.log',
+ paths=(root / 'foo.cc', root / 'foo.py'),
+ all_paths=(root / 'BUILD.gn', root / 'foo.cc', root / 'foo.py'),
+ package_root=root / 'environment' / 'packages',
+ luci=None,
+ override_gn_args={},
+ format_options=FormatOptions(),
+ )
+
+
+class FileFilter:
+ """Allows checking if a path matches a series of filters.
+
+ Positive filters (e.g. the file name matches a regex) and negative filters
+ (path does not match a regular expression) may be applied.
+ """
+
+ _StrOrPattern = Union[Pattern, str]
+
+ def __init__(
+ self,
+ *,
+ exclude: Iterable[_StrOrPattern] = (),
+ endswith: Iterable[str] = (),
+ name: Iterable[_StrOrPattern] = (),
+ suffix: Iterable[str] = (),
+ ) -> None:
+ """Creates a FileFilter with the provided filters.
+
+ Args:
+ endswith: True if the end of the path is equal to any of the passed
+ strings
+ exclude: If any of the passed regular expresion match return False.
+ This overrides and other matches.
+ name: Regexs to match with file names(pathlib.Path.name). True if
+ the resulting regex matches the entire file name.
+ suffix: True if final suffix (as determined by pathlib.Path) is
+ matched by any of the passed str.
+ """
+ self.exclude = tuple(re.compile(i) for i in exclude)
+
+ self.endswith = tuple(endswith)
+ self.name = tuple(re.compile(i) for i in name)
+ self.suffix = tuple(suffix)
+
+ def matches(self, path: Union[str, Path]) -> bool:
+ """Returns true if the path matches any filter but not an exclude.
- for path in self.paths:
- repos[git_repo.root(path)].append(path)
+ If no positive filters are specified, any paths that do not match a
+ negative filter are considered to match.
+
+ If 'path' is a Path object it is rendered as a posix path (i.e.
+ using "/" as the path seperator) before testing with 'exclude' and
+ 'endswith'.
+ """
- return repos
+ posix_path = path.as_posix() if isinstance(path, Path) else path
+ if any(bool(exp.search(posix_path)) for exp in self.exclude):
+ return False
+ # If there are no positive filters set, accept all paths.
+ no_filters = not self.endswith and not self.name and not self.suffix
-class _Filter(NamedTuple):
- endswith: Tuple[str, ...] = ('', )
- exclude: Tuple[Pattern[str], ...] = ()
+ path_obj = Path(path)
+ return (
+ no_filters
+ or path_obj.suffix in self.suffix
+ or any(regex.fullmatch(path_obj.name) for regex in self.name)
+ or any(posix_path.endswith(end) for end in self.endswith)
+ )
- def matches(self, path: str) -> bool:
- return (any(path.endswith(end) for end in self.endswith)
- and not any(exp.search(path) for exp in self.exclude))
+ def filter(self, paths: Sequence[Union[str, Path]]) -> Sequence[Path]:
+ return [Path(x) for x in paths if self.matches(x)]
+
+ def apply_to_check(self, always_run: bool = False) -> Callable:
+ def wrapper(func: Callable) -> Check:
+ if isinstance(func, Check):
+ clone = copy.copy(func)
+ clone.filter = self
+ clone.always_run = clone.always_run or always_run
+ return clone
+
+ return Check(check=func, path_filter=self, always_run=always_run)
+
+ return wrapper
def _print_ui(*args) -> None:
@@ -212,29 +690,71 @@ def _print_ui(*args) -> None:
print(*args, flush=True)
+@dataclasses.dataclass
+class FilteredCheck:
+ check: Check
+ paths: Sequence[Path]
+ substep: Optional[str] = None
+
+ @property
+ def name(self) -> str:
+ return self.check.name
+
+ def run(self, ctx: PresubmitContext, count: int, total: int):
+ return self.check.run(ctx, count, total, self.substep)
+
+
class Presubmit:
"""Runs a series of presubmit checks on a list of files."""
- def __init__(self, root: Path, repos: Sequence[Path],
- output_directory: Path, paths: Sequence[Path],
- package_root: Path):
+
+ def __init__(
+ self,
+ root: Path,
+ repos: Sequence[Path],
+ output_directory: Path,
+ paths: Sequence[Path],
+ all_paths: Sequence[Path],
+ package_root: Path,
+ override_gn_args: Dict[str, str],
+ continue_after_build_error: bool,
+ ):
self._root = root.resolve()
self._repos = tuple(repos)
self._output_directory = output_directory.resolve()
self._paths = tuple(paths)
+ self._all_paths = tuple(all_paths)
self._relative_paths = tuple(
- tools.relative_paths(self._paths, self._root))
+ tools.relative_paths(self._paths, self._root)
+ )
self._package_root = package_root.resolve()
+ self._override_gn_args = override_gn_args
+ self._continue_after_build_error = continue_after_build_error
- def run(self, program: Program, keep_going: bool = False) -> bool:
+ def run(
+ self,
+ program: Program,
+ keep_going: bool = False,
+ substep: Optional[str] = None,
+ ) -> bool:
"""Executes a series of presubmit checks on the paths."""
checks = self.apply_filters(program)
+ if substep:
+ assert (
+ len(checks) == 1
+ ), 'substeps not supported with multiple steps'
+ checks[0].substep = substep
_LOG.debug('Running %s for %s', program.title(), self._root.name)
_print_ui(_title(f'{self._root.name}: {program.title()}'))
- _LOG.info('%d of %d checks apply to %s in %s', len(checks),
- len(program), plural(self._paths, 'file'), self._root)
+ _LOG.info(
+ '%d of %d checks apply to %s in %s',
+ len(checks),
+ len(program),
+ plural(self._paths, 'file'),
+ self._root,
+ )
_print_ui()
for line in tools.file_summary(self._relative_paths):
@@ -242,9 +762,9 @@ class Presubmit:
_print_ui()
if not self._paths:
- _print_ui(color_yellow('No files are being checked!'))
+ _print_ui(_COLOR.yellow('No files are being checked!'))
- _LOG.debug('Checks:\n%s', '\n'.join(c.name for c, _ in checks))
+ _LOG.debug('Checks:\n%s', '\n'.join(c.name for c in checks))
start_time: float = time.time()
passed, failed, skipped = self._execute_checks(checks, keep_going)
@@ -252,22 +772,25 @@ class Presubmit:
return not failed and not skipped
- def apply_filters(
- self,
- program: Sequence[Callable]) -> List[Tuple[Check, Sequence[Path]]]:
- """Returns list of (check, paths) for checks that should run."""
+ def apply_filters(self, program: Sequence[Callable]) -> List[FilteredCheck]:
+ """Returns list of FilteredCheck for checks that should run."""
checks = [c if isinstance(c, Check) else Check(c) for c in program]
- filter_to_checks: Dict[_Filter,
- List[Check]] = collections.defaultdict(list)
+ filter_to_checks: Dict[
+ FileFilter, List[Check]
+ ] = collections.defaultdict(list)
- for check in checks:
- filter_to_checks[check.filter].append(check)
+ for chk in checks:
+ filter_to_checks[chk.filter].append(chk)
check_to_paths = self._map_checks_to_paths(filter_to_checks)
- return [(c, check_to_paths[c]) for c in checks if c in check_to_paths]
+ return [
+ FilteredCheck(c, check_to_paths[c])
+ for c in checks
+ if c in check_to_paths
+ ]
def _map_checks_to_paths(
- self, filter_to_checks: Dict[_Filter, List[Check]]
+ self, filter_to_checks: Dict[FileFilter, List[Check]]
) -> Dict[Check, Sequence[Path]]:
checks_to_paths: Dict[Check, Sequence[Path]] = {}
@@ -275,19 +798,22 @@ class Presubmit:
for filt, checks in filter_to_checks.items():
filtered_paths = tuple(
- path for path, filter_path in zip(self._paths, posix_paths)
- if filt.matches(filter_path))
+ path
+ for path, filter_path in zip(self._paths, posix_paths)
+ if filt.matches(filter_path)
+ )
- for check in checks:
- if filtered_paths or check.always_run:
- checks_to_paths[check] = filtered_paths
+ for chk in checks:
+ if filtered_paths or chk.always_run:
+ checks_to_paths[chk] = filtered_paths
else:
- _LOG.debug('Skipping "%s": no relevant files', check.name)
+ _LOG.debug('Skipping "%s": no relevant files', chk.name)
return checks_to_paths
- def _log_summary(self, time_s: float, passed: int, failed: int,
- skipped: int) -> None:
+ def _log_summary(
+ self, time_s: float, passed: int, failed: int, skipped: int
+ ) -> None:
summary_items = []
if passed:
summary_items.append(f'{passed} passed')
@@ -297,58 +823,85 @@ class Presubmit:
summary_items.append(f'{skipped} not run')
summary = ', '.join(summary_items) or 'nothing was done'
- result = _Result.FAIL if failed or skipped else _Result.PASS
+ if failed or skipped:
+ result = PresubmitResult.FAIL
+ else:
+ result = PresubmitResult.PASS
total = passed + failed + skipped
- _LOG.debug('Finished running %d checks on %s in %.1f s', total,
- plural(self._paths, 'file'), time_s)
+ _LOG.debug(
+ 'Finished running %d checks on %s in %.1f s',
+ total,
+ plural(self._paths, 'file'),
+ time_s,
+ )
_LOG.debug('Presubmit checks %s: %s', result.value, summary)
_print_ui(
_box(
- _SUMMARY_BOX, result.colorized(_LEFT, invert=True),
+ _SUMMARY_BOX,
+ result.colorized(_LEFT, invert=True),
f'{total} checks on {plural(self._paths, "file")}: {summary}',
- _format_time(time_s)))
+ _format_time(time_s),
+ )
+ )
+
+ def _create_presubmit_context( # pylint: disable=no-self-use
+ self, **kwargs
+ ):
+ """Create a PresubmitContext. Override if needed in subclasses."""
+ return PresubmitContext(**kwargs)
@contextlib.contextmanager
- def _context(self, name: str, paths: Tuple[Path, ...]):
+ def _context(self, filtered_check: FilteredCheck):
# There are many characters banned from filenames on Windows. To
# simplify things, just strip everything that's not a letter, digit,
# or underscore.
- sanitized_name = re.sub(r'[\W_]+', '_', name).lower()
+ sanitized_name = re.sub(r'[\W_]+', '_', filtered_check.name).lower()
output_directory = self._output_directory.joinpath(sanitized_name)
os.makedirs(output_directory, exist_ok=True)
- handler = logging.FileHandler(output_directory.joinpath('step.log'),
- mode='w')
+ failure_summary_log = output_directory / 'failure-summary.log'
+ failure_summary_log.unlink(missing_ok=True)
+
+ handler = logging.FileHandler(
+ output_directory.joinpath('step.log'), mode='w'
+ )
handler.setLevel(logging.DEBUG)
try:
_LOG.addHandler(handler)
- yield PresubmitContext(
+ yield self._create_presubmit_context(
root=self._root,
repos=self._repos,
output_dir=output_directory,
- paths=paths,
+ failure_summary_log=failure_summary_log,
+ paths=filtered_check.paths,
+ all_paths=self._all_paths,
package_root=self._package_root,
+ override_gn_args=self._override_gn_args,
+ continue_after_build_error=self._continue_after_build_error,
+ luci=LuciContext.create_from_environment(),
+ format_options=FormatOptions.load(),
)
finally:
_LOG.removeHandler(handler)
- def _execute_checks(self, program,
- keep_going: bool) -> Tuple[int, int, int]:
+ def _execute_checks(
+ self, program: List[FilteredCheck], keep_going: bool
+ ) -> Tuple[int, int, int]:
"""Runs presubmit checks; returns (passed, failed, skipped) lists."""
passed = failed = 0
- for i, (check, paths) in enumerate(program, 1):
- with self._context(check.name, paths) as ctx:
- result = check.run(ctx, i, len(program))
+ for i, filtered_check in enumerate(program, 1):
+ with self._context(filtered_check) as ctx:
+ result = filtered_check.run(ctx, i, len(program))
- if result is _Result.PASS:
+ if result is PresubmitResult.PASS:
passed += 1
- elif result is _Result.CANCEL:
+ elif result is PresubmitResult.CANCEL:
break
else:
failed += 1
@@ -358,8 +911,9 @@ class Presubmit:
return passed, failed, len(program) - passed - failed
-def _process_pathspecs(repos: Iterable[Path],
- pathspecs: Iterable[str]) -> Dict[Path, List[str]]:
+def _process_pathspecs(
+ repos: Iterable[Path], pathspecs: Iterable[str]
+) -> Dict[Path, List[str]]:
pathspecs_by_repo: Dict[Path, List[str]] = {repo: [] for repo in repos}
repos_with_paths: Set[Path] = set()
@@ -371,7 +925,8 @@ def _process_pathspecs(repos: Iterable[Path],
repo = git_repo.within_repo(pathspec)
if repo not in pathspecs_by_repo:
raise ValueError(
- f'{pathspec} is not in a Git repository in this presubmit')
+ f'{pathspec} is not in a Git repository in this presubmit'
+ )
# Make the path relative to the repo's root.
pathspecs_by_repo[repo].append(os.path.relpath(pathspec, repo))
@@ -389,16 +944,23 @@ def _process_pathspecs(repos: Iterable[Path],
return pathspecs_by_repo
-def run(program: Sequence[Callable],
- root: Path,
- repos: Collection[Path] = (),
- base: Optional[str] = None,
- paths: Sequence[str] = (),
- exclude: Sequence[Pattern] = (),
- output_directory: Optional[Path] = None,
- package_root: Path = None,
- only_list_steps: bool = False,
- keep_going: bool = False) -> bool:
+def run( # pylint: disable=too-many-arguments,too-many-locals
+ program: Sequence[Check],
+ root: Path,
+ repos: Collection[Path] = (),
+ base: Optional[str] = None,
+ paths: Sequence[str] = (),
+ exclude: Sequence[Pattern] = (),
+ output_directory: Optional[Path] = None,
+ package_root: Optional[Path] = None,
+ only_list_steps: bool = False,
+ override_gn_args: Sequence[Tuple[str, str]] = (),
+ keep_going: bool = False,
+ continue_after_build_error: bool = False,
+ presubmit_class: type = Presubmit,
+ list_steps_file: Optional[Path] = None,
+ substep: Optional[str] = None,
+) -> bool:
"""Lists files in the current Git repo and runs a Presubmit with them.
This changes the directory to the root of the Git repository after listing
@@ -422,30 +984,72 @@ def run(program: Sequence[Callable],
output_directory: where to place output files
package_root: where to place package files
only_list_steps: print step names instead of running them
- keep_going: whether to continue running checks if an error occurs
+ override_gn_args: additional GN args to set on steps
+ keep_going: continue running presubmit steps after a step fails
+ continue_after_build_error: continue building if a build step fails
+ presubmit_class: class to use to run Presubmits, should inherit from
+ Presubmit class above
+ list_steps_file: File created by --only-list-steps, used to keep from
+ recalculating affected files.
+ substep: run only part of a single check
Returns:
True if all presubmit checks succeeded
"""
repos = [repo.resolve() for repo in repos]
+ non_empty_repos = []
for repo in repos:
- if git_repo.root(repo) != repo:
- raise ValueError(f'{repo} is not the root of a Git repo; '
- 'presubmit checks must be run from a Git repo')
+ if list(repo.iterdir()):
+ non_empty_repos.append(repo)
+ if git_repo.root(repo) != repo:
+ raise ValueError(
+ f'{repo} is not the root of a Git repo; '
+ 'presubmit checks must be run from a Git repo'
+ )
+ repos = non_empty_repos
pathspecs_by_repo = _process_pathspecs(repos, paths)
- files: List[Path] = []
-
- for repo, pathspecs in pathspecs_by_repo.items():
- files += tools.exclude_paths(
- exclude, git_repo.list_files(base, pathspecs, repo), root)
-
+ all_files: List[Path] = []
+ modified_files: List[Path] = []
+ list_steps_data: Dict[str, Any] = {}
+
+ if list_steps_file:
+ with list_steps_file.open() as ins:
+ list_steps_data = json.load(ins)
+ all_files.extend(list_steps_data['all_files'])
+ for step in list_steps_data['steps']:
+ modified_files.extend(Path(x) for x in step.get("paths", ()))
+ modified_files = sorted(set(modified_files))
_LOG.info(
- 'Checking %s',
- git_repo.describe_files(repo, repo, base, pathspecs, exclude,
- root))
+ 'Loaded %d paths from file %s',
+ len(modified_files),
+ list_steps_file,
+ )
+
+ else:
+ for repo, pathspecs in pathspecs_by_repo.items():
+ all_files_repo = tuple(
+ tools.exclude_paths(
+ exclude, git_repo.list_files(None, pathspecs, repo), root
+ )
+ )
+ all_files += all_files_repo
+
+ if base is None:
+ modified_files += all_files_repo
+ else:
+ modified_files += tools.exclude_paths(
+ exclude, git_repo.list_files(base, pathspecs, repo), root
+ )
+
+ _LOG.info(
+ 'Checking %s',
+ git_repo.describe_files(
+ repo, repo, base, pathspecs, exclude, root
+ ),
+ )
if output_directory is None:
output_directory = root / '.presubmit'
@@ -453,111 +1057,300 @@ def run(program: Sequence[Callable],
if package_root is None:
package_root = output_directory / 'packages'
- presubmit = Presubmit(
+ presubmit = presubmit_class(
root=root,
repos=repos,
output_directory=output_directory,
- paths=files,
+ paths=modified_files,
+ all_paths=all_files,
package_root=package_root,
+ override_gn_args=dict(override_gn_args or {}),
+ continue_after_build_error=continue_after_build_error,
)
if only_list_steps:
- for check, _ in presubmit.apply_filters(program):
- print(check.name)
+ steps: List[Dict] = []
+ for filtered_check in presubmit.apply_filters(program):
+ step = {
+ 'name': filtered_check.name,
+ 'paths': [str(x) for x in filtered_check.paths],
+ }
+ substeps = filtered_check.check.substeps()
+ if len(substeps) > 1:
+ step['substeps'] = [x.name for x in substeps]
+ steps.append(step)
+
+ list_steps_data = {
+ 'steps': steps,
+ 'all_files': [str(x) for x in all_files],
+ }
+ json.dump(list_steps_data, sys.stdout, indent=2)
+ sys.stdout.write('\n')
return True
if not isinstance(program, Program):
program = Program('', program)
- return presubmit.run(program, keep_going)
+ return presubmit.run(program, keep_going, substep=substep)
def _make_str_tuple(value: Union[Iterable[str], str]) -> Tuple[str, ...]:
return tuple([value] if isinstance(value, str) else value)
+def check(*args, **kwargs):
+ """Turn a function into a presubmit check.
+
+ Args:
+ *args: Passed through to function.
+ *kwargs: Passed through to function.
+
+ If only one argument is provided and it's a function, this function acts
+ as a decorator and creates a Check from the function. Example of this kind
+ of usage:
+
+ @check
+ def pragma_once(ctx: PresubmitContext):
+ pass
+
+ Otherwise, save the arguments, and return a decorator that turns a function
+ into a Check, but with the arguments added onto the Check constructor.
+ Example of this kind of usage:
+
+ @check(name='pragma_twice')
+ def pragma_once(ctx: PresubmitContext):
+ pass
+ """
+ if (
+ len(args) == 1
+ and isinstance(args[0], types.FunctionType)
+ and not kwargs
+ ):
+ # Called as a regular decorator.
+ return Check(args[0])
+
+ def decorator(check_function):
+ return Check(check_function, *args, **kwargs)
+
+ return decorator
+
+
+@dataclasses.dataclass
+class SubStep:
+ name: Optional[str]
+ _func: Callable[..., PresubmitResult]
+ args: Sequence[Any] = ()
+ kwargs: Dict[str, Any] = dataclasses.field(default_factory=lambda: {})
+
+ def __call__(self, ctx: PresubmitContext) -> PresubmitResult:
+ if self.name:
+ _LOG.info('%s', self.name)
+ return self._func(ctx, *self.args, **self.kwargs)
+
+
class Check:
"""Wraps a presubmit check function.
This class consolidates the logic for running and logging a presubmit check.
It also supports filtering the paths passed to the presubmit check.
"""
- def __init__(self,
- check_function: Callable,
- path_filter: _Filter = _Filter(),
- always_run: bool = True):
- _ensure_is_valid_presubmit_check_function(check_function)
-
- self._check: Callable = check_function
- self.filter: _Filter = path_filter
- self.always_run: bool = always_run
+ def __init__(
+ self,
+ check: Union[ # pylint: disable=redefined-outer-name
+ Callable, Iterable[SubStep]
+ ],
+ path_filter: FileFilter = FileFilter(),
+ always_run: bool = True,
+ name: Optional[str] = None,
+ doc: Optional[str] = None,
+ ) -> None:
# Since Check wraps a presubmit function, adopt that function's name.
- self.__name__ = self._check.__name__
+ self.name: str = ''
+ self.doc: str = ''
+ if isinstance(check, Check):
+ self.name = check.name
+ self.doc = check.doc
+ elif callable(check):
+ self.name = check.__name__
+ self.doc = check.__doc__ or ''
+
+ if name:
+ self.name = name
+ if doc:
+ self.doc = doc
+
+ if not self.name:
+ raise ValueError('no name for step')
+
+ self._substeps_raw: Iterable[SubStep]
+ if isinstance(check, collections.abc.Iterator):
+ self._substeps_raw = check
+ else:
+ assert callable(check)
+ _ensure_is_valid_presubmit_check_function(check)
+ self._substeps_raw = iter((SubStep(None, check),))
+ self._substeps_saved: Sequence[SubStep] = ()
+
+ self.filter = path_filter
+ self.always_run: bool = always_run
+
+ def substeps(self) -> Sequence[SubStep]:
+ """Return the SubSteps of the current step.
+
+ This is where the list of SubSteps is actually evaluated. It can't be
+ evaluated in the constructor because the Iterable passed into the
+ constructor might not be ready yet.
+ """
+ if not self._substeps_saved:
+ self._substeps_saved = tuple(self._substeps_raw)
+ return self._substeps_saved
+
+ def __repr__(self):
+ # This returns just the name so it's easy to show the entire list of
+ # steps with '--help'.
+ return self.name
+
+ def unfiltered(self) -> Check:
+ """Create a new check identical to this one, but without the filter."""
+ clone = copy.copy(self)
+ clone.filter = FileFilter()
+ return clone
def with_filter(
self,
*,
- endswith: Iterable[str] = '',
- exclude: Iterable[Union[Pattern[str], str]] = ()
+ endswith: Iterable[str] = (),
+ exclude: Iterable[Union[Pattern[str], str]] = (),
) -> Check:
- endswith = self.filter.endswith
- if endswith:
- endswith = endswith + _make_str_tuple(endswith)
- exclude = self.filter.exclude + tuple(re.compile(e) for e in exclude)
+ """Create a new check identical to this one, but with extra filters.
- return Check(check_function=self._check,
- path_filter=_Filter(endswith=endswith, exclude=exclude),
- always_run=self.always_run)
+ Add to the existing filter, perhaps to exclude an additional directory.
- @property
- def name(self):
- return self.__name__
+ Args:
+ endswith: Passed through to FileFilter.
+ exclude: Passed through to FileFilter.
+
+ Returns a new check.
+ """
+ return self.with_file_filter(
+ FileFilter(endswith=_make_str_tuple(endswith), exclude=exclude)
+ )
+
+ def with_file_filter(self, file_filter: FileFilter) -> Check:
+ """Create a new check identical to this one, but with extra filters.
+
+ Add to the existing filter, perhaps to exclude an additional directory.
+
+ Args:
+ file_filter: Additional filter rules.
- def run(self, ctx: PresubmitContext, count: int, total: int) -> _Result:
+ Returns a new check.
+ """
+ clone = copy.copy(self)
+ if clone.filter:
+ clone.filter.exclude = clone.filter.exclude + file_filter.exclude
+ clone.filter.endswith = clone.filter.endswith + file_filter.endswith
+ clone.filter.name = file_filter.name or clone.filter.name
+ clone.filter.suffix = clone.filter.suffix + file_filter.suffix
+ else:
+ clone.filter = file_filter
+ return clone
+
+ def run(
+ self,
+ ctx: PresubmitContext,
+ count: int,
+ total: int,
+ substep: Optional[str] = None,
+ ) -> PresubmitResult:
"""Runs the presubmit check on the provided paths."""
_print_ui(
- _box(_CHECK_UPPER, f'{count}/{total}', self.name,
- plural(ctx.paths, "file")))
-
- _LOG.debug('[%d/%d] Running %s on %s', count, total, self.name,
- plural(ctx.paths, "file"))
+ _box(
+ _CHECK_UPPER,
+ f'{count}/{total}',
+ self.name,
+ plural(ctx.paths, "file"),
+ )
+ )
+
+ substep_part = f'.{substep}' if substep else ''
+ _LOG.debug(
+ '[%d/%d] Running %s%s on %s',
+ count,
+ total,
+ self.name,
+ substep_part,
+ plural(ctx.paths, "file"),
+ )
start_time_s = time.time()
- result = self._call_function(ctx)
+ result: PresubmitResult
+ if substep:
+ result = self.run_substep(ctx, substep)
+ else:
+ result = self(ctx)
time_str = _format_time(time.time() - start_time_s)
_LOG.debug('%s %s', self.name, result.value)
_print_ui(
- _box(_CHECK_LOWER, result.colorized(_LEFT), self.name, time_str))
+ _box(_CHECK_LOWER, result.colorized(_LEFT), self.name, time_str)
+ )
_LOG.debug('%s duration:%s', self.name, time_str)
return result
- def _call_function(self, ctx: PresubmitContext) -> _Result:
+ def _try_call(
+ self,
+ func: Callable,
+ ctx,
+ *args,
+ **kwargs,
+ ) -> PresubmitResult:
try:
- self._check(ctx)
+ result = func(ctx, *args, **kwargs)
+ if ctx.failed:
+ return PresubmitResult.FAIL
+ if isinstance(result, PresubmitResult):
+ return result
+ return PresubmitResult.PASS
+
except PresubmitFailure as failure:
if str(failure):
_LOG.warning('%s', failure)
- return _Result.FAIL
- except Exception as failure: # pylint: disable=broad-except
+ return PresubmitResult.FAIL
+
+ except Exception as _failure: # pylint: disable=broad-except
_LOG.exception('Presubmit check %s failed!', self.name)
- return _Result.FAIL
+ return PresubmitResult.FAIL
+
except KeyboardInterrupt:
_print_ui()
- return _Result.CANCEL
+ return PresubmitResult.CANCEL
+
+ def run_substep(
+ self, ctx: PresubmitContext, name: Optional[str]
+ ) -> PresubmitResult:
+ for substep in self.substeps():
+ if substep.name == name:
+ return substep(ctx)
- return _Result.PASS
+ expected = ', '.join(repr(s.name) for s in self.substeps())
+ raise LookupError(f'bad substep name: {name!r} (expected: {expected})')
- def __call__(self, ctx: PresubmitContext, *args, **kwargs):
- """Calling a Check calls its underlying function directly.
+ def __call__(self, ctx: PresubmitContext) -> PresubmitResult:
+ """Calling a Check calls its underlying substeps directly.
This makes it possible to call functions wrapped by @filter_paths. The
prior filters are ignored, so new filters may be applied.
"""
- return self._check(ctx, *args, **kwargs)
+ result: PresubmitResult
+ for substep in self.substeps():
+ result = self._try_call(substep, ctx)
+ if result and result != PresubmitResult.PASS:
+ return result
+ return PresubmitResult.PASS
def _required_args(function: Callable) -> Iterable[Parameter]:
@@ -569,26 +1362,36 @@ def _required_args(function: Callable) -> Iterable[Parameter]:
yield param
-def _ensure_is_valid_presubmit_check_function(check: Callable) -> None:
+def _ensure_is_valid_presubmit_check_function(chk: Callable) -> None:
"""Checks if a Callable can be used as a presubmit check."""
try:
- required_args = tuple(_required_args(check))
+ required_args = tuple(_required_args(chk))
except (TypeError, ValueError):
- raise TypeError('Presubmit checks must be callable, but '
- f'{check!r} is a {type(check).__name__}')
+ raise TypeError(
+ 'Presubmit checks must be callable, but '
+ f'{chk!r} is a {type(chk).__name__}'
+ )
if len(required_args) != 1:
raise TypeError(
f'Presubmit check functions must have exactly one required '
f'positional argument (the PresubmitContext), but '
- f'{check.__name__} has {len(required_args)} required arguments' +
- (f' ({", ".join(a.name for a in required_args)})'
- if required_args else ''))
+ f'{chk.__name__} has {len(required_args)} required arguments'
+ + (
+ f' ({", ".join(a.name for a in required_args)})'
+ if required_args
+ else ''
+ )
+ )
-def filter_paths(endswith: Iterable[str] = '',
- exclude: Iterable[Union[Pattern[str], str]] = (),
- always_run: bool = False) -> Callable[[Callable], Check]:
+def filter_paths(
+ *,
+ endswith: Iterable[str] = (),
+ exclude: Iterable[Union[Pattern[str], str]] = (),
+ file_filter: Optional[FileFilter] = None,
+ always_run: bool = False,
+) -> Callable[[Callable], Check]:
"""Decorator for filtering the paths list for a presubmit check function.
Path filters only apply when the function is used as a presubmit check.
@@ -599,15 +1402,27 @@ def filter_paths(endswith: Iterable[str] = '',
Args:
endswith: str or iterable of path endings to include
exclude: regular expressions of paths to exclude
-
+ file_filter: FileFilter used to select files
+ always_run: Run check even when no files match
Returns:
a wrapped version of the presubmit function
"""
+
+ if file_filter:
+ real_file_filter = file_filter
+ if endswith or exclude:
+ raise ValueError(
+ 'Must specify either file_filter or '
+ 'endswith/exclude args, not both'
+ )
+ else:
+ # TODO(b/238426363): Remove these arguments and use FileFilter only.
+ real_file_filter = FileFilter(
+ endswith=_make_str_tuple(endswith), exclude=exclude
+ )
+
def filter_paths_for_function(function: Callable):
- return Check(function,
- _Filter(_make_str_tuple(endswith),
- tuple(re.compile(e) for e in exclude)),
- always_run=always_run)
+ return Check(function, real_file_filter, always_run=always_run)
return filter_paths_for_function
@@ -617,6 +1432,9 @@ def call(*args, **kwargs) -> None:
attributes, command = tools.format_command(args, kwargs)
_LOG.debug('[RUN] %s\n%s', attributes, command)
+ tee = kwargs.pop('tee', None)
+ propagate_sigterm = kwargs.pop('propagate_sigterm', False)
+
env = pw_cli.env.pigweed_environment()
kwargs['stdout'] = subprocess.PIPE
kwargs['stderr'] = subprocess.STDOUT
@@ -624,21 +1442,64 @@ def call(*args, **kwargs) -> None:
process = subprocess.Popen(args, **kwargs)
assert process.stdout
+ # Set up signal handler if requested.
+ signaled = False
+ if propagate_sigterm:
+
+ def signal_handler(_signal_number: int, _stack_frame: Any) -> None:
+ nonlocal signaled
+ signaled = True
+ process.terminate()
+
+ previous_signal_handler = signal.signal(signal.SIGTERM, signal_handler)
+
if env.PW_PRESUBMIT_DISABLE_SUBPROCESS_CAPTURE:
while True:
line = process.stdout.readline().decode(errors='backslashreplace')
if not line:
break
_LOG.info(line.rstrip())
+ if tee:
+ tee.write(line)
stdout, _ = process.communicate()
+ if tee:
+ tee.write(stdout.decode(errors='backslashreplace'))
logfunc = _LOG.warning if process.returncode else _LOG.debug
logfunc('[FINISHED]\n%s', command)
- logfunc('[RESULT] %s with return code %d',
- 'Failed' if process.returncode else 'Passed', process.returncode)
+ logfunc(
+ '[RESULT] %s with return code %d',
+ 'Failed' if process.returncode else 'Passed',
+ process.returncode,
+ )
if stdout:
logfunc('[OUTPUT]\n%s', stdout.decode(errors='backslashreplace'))
+ if propagate_sigterm:
+ signal.signal(signal.SIGTERM, previous_signal_handler)
+ if signaled:
+ _LOG.warning('Exiting due to SIGTERM.')
+ sys.exit(1)
+
if process.returncode:
raise PresubmitFailure
+
+
+def install_package(
+ ctx: Union[FormatContext, PresubmitContext],
+ name: str,
+ force: bool = False,
+) -> None:
+ """Install package with given name in given path."""
+ root = ctx.package_root
+ mgr = package_manager.PackageManager(root)
+
+ if not mgr.list():
+ raise PresubmitFailure(
+ 'no packages configured, please import your pw_package '
+ 'configuration module'
+ )
+
+ if not mgr.status(name) or force:
+ mgr.install(name, force=force)
diff --git a/pw_presubmit/py/pw_presubmit/python_checks.py b/pw_presubmit/py/pw_presubmit/python_checks.py
index c9c1b5de5..f694b354c 100644
--- a/pw_presubmit/py/pw_presubmit/python_checks.py
+++ b/pw_presubmit/py/pw_presubmit/python_checks.py
@@ -29,8 +29,7 @@ try:
except ImportError:
# Append the pw_presubmit package path to the module search path to allow
# running this module without installing the pw_presubmit package.
- sys.path.append(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__))))
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pw_presubmit
from pw_env_setup import python_packages
@@ -55,8 +54,8 @@ _PYTHON_IS_3_9_OR_HIGHER = sys.version_info >= (
@filter_paths(endswith=_PYTHON_EXTENSIONS)
def gn_python_check(ctx: PresubmitContext):
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'python.tests', 'python.lint')
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'python.tests', 'python.lint')
def _transform_lcov_file_paths(lcov_file: Path, repo_root: Path) -> str:
@@ -79,19 +78,30 @@ def _transform_lcov_file_paths(lcov_file: Path, repo_root: Path) -> str:
file_string = line[3:].rstrip()
source_file_path = Path(file_string)
+ # TODO(b/248257406) Remove once we drop support for Python 3.8.
+ def is_relative_to(path: Path, other: Path) -> bool:
+ try:
+ path.relative_to(other)
+ return True
+ except ValueError:
+ return False
+
# Attempt to map a generated Python package source file to the root
# source tree.
# pylint: disable=no-member
- if not source_file_path.is_relative_to( # type: ignore[attr-defined]
- repo_root):
+ if not is_relative_to(
+ source_file_path, repo_root # type: ignore[attr-defined]
+ ):
# pylint: enable=no-member
source_file_path = repo_root / str(source_file_path).replace(
- 'python/gen/', '').replace('py.generated_python_package/', '')
+ 'python/gen/', ''
+ ).replace('py.generated_python_package/', '')
# If mapping fails don't modify this line.
# pylint: disable=no-member
- if not source_file_path.is_relative_to( # type: ignore[attr-defined]
- repo_root):
+ if not is_relative_to(
+ source_file_path, repo_root # type: ignore[attr-defined]
+ ):
# pylint: enable=no-member
lcov_output += line + '\n'
continue
@@ -105,8 +115,8 @@ def _transform_lcov_file_paths(lcov_file: Path, repo_root: Path) -> str:
@filter_paths(endswith=_PYTHON_EXTENSIONS)
def gn_python_test_coverage(ctx: PresubmitContext):
"""Run Python tests with coverage and create reports."""
- build.gn_gen(ctx.root, ctx.output_dir, pw_build_PYTHON_TEST_COVERAGE=True)
- build.ninja(ctx.output_dir, 'python.tests')
+ build.gn_gen(ctx, pw_build_PYTHON_TEST_COVERAGE=True)
+ build.ninja(ctx, 'python.tests')
# Find coverage data files
coverage_data_files = list(ctx.output_dir.glob('**/*.coverage'))
@@ -120,7 +130,8 @@ def gn_python_test_coverage(ctx: PresubmitContext):
# Leave existing coverage files in place; by default they are deleted.
'--keep',
*coverage_data_files,
- cwd=ctx.output_dir)
+ cwd=ctx.output_dir,
+ )
combined_data_file = ctx.output_dir / '.coverage'
_LOG.info('Coverage data saved to: %s', combined_data_file.resolve())
@@ -129,7 +140,8 @@ def gn_python_test_coverage(ctx: PresubmitContext):
# Output coverage percentage summary to the terminal of changed files.
changed_python_files = list(
- str(p) for p in ctx.paths if str(p).endswith('.py'))
+ str(p) for p in ctx.paths if str(p).endswith('.py')
+ )
report_args = [
'coverage',
'report',
@@ -143,7 +155,8 @@ def gn_python_test_coverage(ctx: PresubmitContext):
call('coverage', 'lcov', coverage_omit_patterns, cwd=ctx.output_dir)
lcov_data_file = ctx.output_dir / 'coverage.lcov'
lcov_data_file.write_text(
- _transform_lcov_file_paths(lcov_data_file, repo_root=ctx.root))
+ _transform_lcov_file_paths(lcov_data_file, repo_root=ctx.root)
+ )
_LOG.info('Coverage lcov saved to: %s', lcov_data_file.resolve())
# Generate an html report
@@ -152,23 +165,29 @@ def gn_python_test_coverage(ctx: PresubmitContext):
_LOG.info('Coverage html report saved to: %s', html_report.resolve())
-@filter_paths(endswith=_PYTHON_EXTENSIONS + ('.pylintrc', ))
+@filter_paths(endswith=_PYTHON_EXTENSIONS + ('.pylintrc',))
def gn_python_lint(ctx: pw_presubmit.PresubmitContext) -> None:
- build.gn_gen(ctx.root, ctx.output_dir)
- build.ninja(ctx.output_dir, 'python.lint')
+ build.gn_gen(ctx)
+ build.ninja(ctx, 'python.lint')
@Check
def check_python_versions(ctx: PresubmitContext):
"""Checks that the list of installed packages is as expected."""
- build.gn_gen(ctx.root, ctx.output_dir)
+ build.gn_gen(ctx)
constraint_file: Optional[str] = None
+ requirement_file: Optional[str] = None
try:
for arg in build.get_gn_args(ctx.output_dir):
if arg['name'] == 'pw_build_PIP_CONSTRAINTS':
- constraint_file = json.loads(
- arg['current']['value'])[0].strip('/')
+ constraint_file = json.loads(arg['current']['value'])[0].strip(
+ '/'
+ )
+ if arg['name'] == 'pw_build_PIP_REQUIREMENTS':
+ requirement_file = json.loads(arg['current']['value'])[0].strip(
+ '/'
+ )
except json.JSONDecodeError:
_LOG.warning('failed to parse GN args json')
return
@@ -176,7 +195,15 @@ def check_python_versions(ctx: PresubmitContext):
if not constraint_file:
_LOG.warning('could not find pw_build_PIP_CONSTRAINTS GN arg')
return
-
- with (ctx.root / constraint_file).open('r') as ins:
- if python_packages.diff(ins) != 0:
- raise PresubmitFailure
+ ignored_requirements_arg = None
+ if requirement_file:
+ ignored_requirements_arg = [(ctx.root / requirement_file)]
+
+ if (
+ python_packages.diff(
+ expected=(ctx.root / constraint_file),
+ ignore_requirements_file=ignored_requirements_arg,
+ )
+ != 0
+ ):
+ raise PresubmitFailure
diff --git a/pw_presubmit/py/pw_presubmit/shell_checks.py b/pw_presubmit/py/pw_presubmit/shell_checks.py
new file mode 100644
index 000000000..510c09f06
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/shell_checks.py
@@ -0,0 +1,42 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Shell related checks."""
+
+import logging
+from pw_presubmit import (
+ Check,
+ PresubmitContext,
+ filter_paths,
+ tools,
+ PresubmitFailure,
+)
+
+_LOG = logging.getLogger(__name__)
+
+_SHELL_EXTENSIONS = ('.sh', '.bash')
+
+
+@filter_paths(endswith=_SHELL_EXTENSIONS)
+@Check
+def shellcheck(ctx: PresubmitContext) -> None:
+ """Run shell script static analiyzer on presubmit."""
+
+ _LOG.warning(
+ "The Pigweed project discourages use of shellscripts. "
+ "https://pigweed.dev/docs/faq.html"
+ )
+
+ result = tools.log_run(['shellcheck', *ctx.paths])
+ if result.returncode != 0:
+ raise PresubmitFailure('Shellcheck identifed issues.')
diff --git a/pw_presubmit/py/pw_presubmit/source_in_build.py b/pw_presubmit/py/pw_presubmit/source_in_build.py
new file mode 100644
index 000000000..f19dd1174
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/source_in_build.py
@@ -0,0 +1,168 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Checks that source files are listed in build files, such as BUILD.bazel."""
+
+import logging
+from typing import Callable, Sequence
+
+from pw_presubmit import build, format_code, git_repo
+from pw_presubmit.presubmit import (
+ Check,
+ FileFilter,
+ PresubmitContext,
+ PresubmitFailure,
+)
+
+_LOG: logging.Logger = logging.getLogger(__name__)
+
+# The filter is used twice for each source_is_in_* check. First to decide
+# whether the check should be run. Once it's running, we use ctx.all_paths
+# instead of ctx.paths since we want to check that all files are in the build,
+# not just changed files, but we need to run ctx.all_paths through the same
+# filter within the check or we won't properly ignore files that the caller
+# asked to be ignored.
+
+_DEFAULT_BAZEL_EXTENSIONS = (*format_code.C_FORMAT.extensions,)
+
+
+def bazel(
+ source_filter: FileFilter,
+ files_and_extensions_to_check: Sequence[str] = _DEFAULT_BAZEL_EXTENSIONS,
+) -> Check:
+ """Create a presubmit check that ensures source files are in Bazel files.
+
+ Args:
+ source_filter: filter that selects files that must be in the Bazel build
+ files_and_extensions_to_check: files and extensions to look for (the
+ source_filter might match build files that won't be in the build but
+ this should only match source files)
+ """
+
+ @source_filter.apply_to_check()
+ def source_is_in_bazel_build(ctx: PresubmitContext):
+ """Checks that source files are in the Bazel build."""
+
+ paths = source_filter.filter(ctx.all_paths)
+
+ missing = build.check_bazel_build_for_files(
+ files_and_extensions_to_check,
+ paths,
+ bazel_dirs=[ctx.root],
+ )
+
+ if missing:
+ with ctx.failure_summary_log.open('w') as outs:
+ print('Missing files:', file=outs)
+ for miss in missing:
+ print(miss, file=outs)
+
+ _LOG.warning('All source files must appear in BUILD.bazel files')
+ raise PresubmitFailure
+
+ return source_is_in_bazel_build
+
+
+_DEFAULT_GN_EXTENSIONS = (
+ 'setup.cfg',
+ '.toml',
+ '.rst',
+ '.py',
+ *format_code.C_FORMAT.extensions,
+)
+
+
+def gn( # pylint: disable=invalid-name
+ source_filter: FileFilter,
+ files_and_extensions_to_check: Sequence[str] = _DEFAULT_GN_EXTENSIONS,
+) -> Check:
+ """Create a presubmit check that ensures source files are in GN files.
+
+ Args:
+ source_filter: filter that selects files that must be in the GN build
+ files_and_extensions_to_check: files and extensions to look for (the
+ source_filter might match build files that won't be in the build but
+ this should only match source files)
+ """
+
+ @source_filter.apply_to_check()
+ def source_is_in_gn_build(ctx: PresubmitContext):
+ """Checks that source files are in the GN build."""
+
+ paths = source_filter.filter(ctx.all_paths)
+
+ missing = build.check_gn_build_for_files(
+ files_and_extensions_to_check,
+ paths,
+ gn_build_files=git_repo.list_files(
+ pathspecs=['BUILD.gn', '*BUILD.gn'], repo_path=ctx.root
+ ),
+ )
+
+ if missing:
+ with ctx.failure_summary_log.open('w') as outs:
+ print('Missing files:', file=outs)
+ for miss in missing:
+ print(miss, file=outs)
+
+ _LOG.warning('All source files must appear in BUILD.gn files')
+ raise PresubmitFailure
+
+ return source_is_in_gn_build
+
+
+_DEFAULT_CMAKE_EXTENSIONS = (*format_code.C_FORMAT.extensions,)
+
+
+def cmake(
+ source_filter: FileFilter,
+ run_cmake: Callable[[PresubmitContext], None],
+ files_and_extensions_to_check: Sequence[str] = _DEFAULT_CMAKE_EXTENSIONS,
+) -> Check:
+ """Create a presubmit check that ensures source files are in CMake files.
+
+ Args:
+ source_filter: filter that selects files that must be in the CMake build
+ run_cmake: callable that takes a PresubmitContext and invokes CMake
+ files_and_extensions_to_check: files and extensions to look for (the
+ source_filter might match build files that won't be in the build but
+ this should only match source files)
+ """
+
+ to_check = tuple(files_and_extensions_to_check)
+
+ @source_filter.apply_to_check()
+ def source_is_in_cmake_build(ctx: PresubmitContext):
+ """Checks that source files are in the CMake build."""
+
+ paths = source_filter.filter(ctx.all_paths)
+
+ run_cmake(ctx)
+ missing = build.check_compile_commands_for_files(
+ ctx.output_dir / 'compile_commands.json',
+ (f for f in paths if str(f).endswith(to_check)),
+ )
+
+ if missing:
+ with ctx.failure_summary_log.open('w') as outs:
+ print('Missing files:', file=outs)
+ for miss in missing:
+ print(miss, file=outs)
+
+ _LOG.warning(
+ 'Files missing from CMake:\n%s',
+ '\n'.join(str(f) for f in missing),
+ )
+ raise PresubmitFailure
+
+ return source_is_in_cmake_build
diff --git a/pw_presubmit/py/pw_presubmit/todo_check.py b/pw_presubmit/py/pw_presubmit/todo_check.py
new file mode 100644
index 000000000..783667f10
--- /dev/null
+++ b/pw_presubmit/py/pw_presubmit/todo_check.py
@@ -0,0 +1,108 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Check the formatting of TODOs."""
+
+import logging
+from pathlib import Path
+import re
+from typing import Iterable, Pattern, Sequence, Union
+
+from pw_presubmit import PresubmitContext, filter_paths
+
+_LOG: logging.Logger = logging.getLogger(__name__)
+
+EXCLUDE: Sequence[str] = (
+ # Metadata
+ r'^docker/tag$',
+ r'\byarn.lock$',
+ # Data files
+ r'\.bin$',
+ r'\.csv$',
+ r'\.elf$',
+ r'\.gif$',
+ r'\.jpg$',
+ r'\.json$',
+ r'\.png$',
+ r'\.svg$',
+ r'\.xml$',
+)
+
+# todo-check: disable
+BUGS_ONLY = re.compile(r'\bTODO\(b/\d+(?:, ?b/\d+)*\).*\w')
+BUGS_OR_USERNAMES = re.compile(
+ r'\bTODO\((?:b/\d+|[a-z]+)(?:, ?(?:b/\d+|[a-z]+))*\).*\w'
+)
+_TODO = re.compile(r'\bTODO\b')
+# todo-check: enable
+
+# If seen, ignore this line and the next.
+_IGNORE = 'todo-check: ignore'
+
+# Ignore a whole section. Please do not change the order of these lines.
+_DISABLE = 'todo-check: disable'
+_ENABLE = 'todo-check: enable'
+
+
+def _process_file(ctx: PresubmitContext, todo_pattern: re.Pattern, path: Path):
+ with path.open() as ins:
+ _LOG.debug('Evaluating path %s', path)
+ enabled = True
+ prev = ''
+
+ try:
+ summary = []
+ for i, line in enumerate(ins, 1):
+ if _DISABLE in line:
+ enabled = False
+ elif _ENABLE in line:
+ enabled = True
+
+ if not enabled or _IGNORE in line or _IGNORE in prev:
+ prev = line
+ continue
+
+ if _TODO.search(line):
+ if not todo_pattern.search(line):
+ # todo-check: ignore
+ ctx.fail(f'Bad TODO on line {i}:', path)
+ ctx.fail(f' {line.strip()}')
+ summary.append(
+ f'{path.relative_to(ctx.root)}:{i}:{line.strip()}'
+ )
+
+ prev = line
+
+ if summary:
+ with ctx.failure_summary_log.open('w') as outs:
+ for line in summary:
+ print(line, file=outs)
+
+ except UnicodeDecodeError:
+ # File is not text, like a gif.
+ _LOG.debug('File %s is not a text file', path)
+
+
+def create(
+ todo_pattern: re.Pattern = BUGS_ONLY,
+ exclude: Iterable[Union[Pattern[str], str]] = EXCLUDE,
+):
+ """Create a todo_check presubmit step that uses the given pattern."""
+
+ @filter_paths(exclude=exclude)
+ def todo_check(ctx: PresubmitContext):
+ """Check that TODO lines are valid.""" # todo-check: ignore
+ for path in ctx.paths:
+ _process_file(ctx, todo_pattern, path)
+
+ return todo_check
diff --git a/pw_presubmit/py/pw_presubmit/tools.py b/pw_presubmit/py/pw_presubmit/tools.py
index bff329ee2..bf184db73 100644
--- a/pw_presubmit/py/pw_presubmit/tools.py
+++ b/pw_presubmit/py/pw_presubmit/tools.py
@@ -20,17 +20,29 @@ import os
from pathlib import Path
import shlex
import subprocess
-from typing import Any, Dict, Iterable, Iterator, List, Sequence, Pattern, Tuple
+from typing import (
+ Any,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Sequence,
+ Pattern,
+ Tuple,
+)
_LOG: logging.Logger = logging.getLogger(__name__)
-def plural(items_or_count,
- singular: str,
- count_format='',
- these: bool = False,
- number: bool = True,
- are: bool = False) -> str:
+def plural(
+ items_or_count,
+ singular: str,
+ count_format='',
+ these: bool = False,
+ number: bool = True,
+ are: bool = False,
+) -> str:
"""Returns the singular or plural form of a word based on a count."""
try:
@@ -52,31 +64,39 @@ def plural(items_or_count,
return f'{prefix}{num}{result}{suffix}'
-def make_color(*codes: int):
- start = ''.join(f'\033[{code}m' for code in codes)
- return f'{start}{{}}\033[0m'.format if os.name == 'posix' else str
-
-
def make_box(section_alignments: Sequence[str]) -> str:
indices = [i + 1 for i in range(len(section_alignments))]
top_sections = '{2}'.join('{1:{1}^{width%d}}' % i for i in indices)
- mid_sections = '{5}'.join('{section%d:%s{width%d}}' %
- (i, section_alignments[i - 1], i)
- for i in indices)
+ mid_sections = '{5}'.join(
+ '{section%d:%s{width%d}}' % (i, section_alignments[i - 1], i)
+ for i in indices
+ )
bot_sections = '{9}'.join('{8:{8}^{width%d}}' % i for i in indices)
- return ''.join(['{0}', *top_sections, '{3}\n',
- '{4}', *mid_sections, '{6}\n',
- '{7}', *bot_sections, '{10}']) # yapf: disable
-
-
-def file_summary(paths: Iterable[Path],
- levels: int = 2,
- max_lines: int = 12,
- max_types: int = 3,
- pad: str = ' ',
- pad_start: str = ' ',
- pad_end: str = ' ') -> List[str]:
+ return ''.join(
+ [
+ '{0}',
+ *top_sections,
+ '{3}\n',
+ '{4}',
+ *mid_sections,
+ '{6}\n',
+ '{7}',
+ *bot_sections,
+ '{10}',
+ ]
+ )
+
+
+def file_summary(
+ paths: Iterable[Path],
+ levels: int = 2,
+ max_lines: int = 12,
+ max_types: int = 3,
+ pad: str = ' ',
+ pad_start: str = ' ',
+ pad_end: str = ' ',
+) -> List[str]:
"""Summarizes a list of files by the file types in each directory."""
# Count the file types in each directory.
@@ -88,11 +108,19 @@ def file_summary(paths: Iterable[Path],
# If there are too many lines, condense directories with the fewest files.
if len(all_counts) > max_lines:
- counts = sorted(all_counts.items(),
- key=lambda item: -sum(item[1].values()))
- counts, others = sorted(counts[:max_lines - 1]), counts[max_lines - 1:]
- counts.append((f'({plural(others, "other")})',
- sum((c for _, c in others), Counter())))
+ counts = sorted(
+ all_counts.items(), key=lambda item: -sum(item[1].values())
+ )
+ counts, others = (
+ sorted(counts[: max_lines - 1]),
+ counts[max_lines - 1 :],
+ )
+ counts.append(
+ (
+ f'({plural(others, "other")})',
+ sum((c for _, c in others), Counter()),
+ )
+ )
else:
counts = sorted(all_counts.items())
@@ -127,9 +155,11 @@ def relative_paths(paths: Iterable[Path], start: Path) -> Iterable[Path]:
yield Path(os.path.relpath(path, start))
-def exclude_paths(exclusions: Iterable[Pattern[str]],
- paths: Iterable[Path],
- relative_to: Path = None) -> Iterable[Path]:
+def exclude_paths(
+ exclusions: Iterable[Pattern[str]],
+ paths: Iterable[Path],
+ relative_to: Optional[Path] = None,
+) -> Iterable[Path]:
"""Excludes paths based on a series of regular expressions."""
if relative_to:
relpath = lambda path: Path(os.path.relpath(path, relative_to))
@@ -143,7 +173,7 @@ def exclude_paths(exclusions: Iterable[Pattern[str]],
def _truncate(value, length: int = 60) -> str:
value = str(value)
- return (value[:length - 5] + '[...]') if len(value) > length else value
+ return (value[: length - 5] + '[...]') if len(value) > length else value
def format_command(args: Sequence, kwargs: dict) -> Tuple[str, str]:
@@ -169,7 +199,8 @@ def flatten(*items) -> Iterator:
for item in items:
if isinstance(item, collections.abc.Iterable) and not isinstance(
- item, (str, bytes, bytearray)):
+ item, (str, bytes, bytearray)
+ ):
yield from flatten(*item)
else:
yield item
diff --git a/pw_presubmit/py/setup.cfg b/pw_presubmit/py/setup.cfg
index 14652af9d..5a71c4ff6 100644
--- a/pw_presubmit/py/setup.cfg
+++ b/pw_presubmit/py/setup.cfg
@@ -22,10 +22,8 @@ description = Presubmit tools and a presubmit script for Pigweed
packages = find:
zip_safe = False
install_requires =
- scan-build==2.0.19
- yapf==0.31.0
- pw_cli
- pw_package
+ yapf>=0.31.0
+ black>=23.1.0
[options.package_data]
pw_presubmit = py.typed
diff --git a/pw_presubmit/py/todo_check_test.py b/pw_presubmit/py/todo_check_test.py
new file mode 100644
index 000000000..c4b8d1b5a
--- /dev/null
+++ b/pw_presubmit/py/todo_check_test.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests for todo_check."""
+
+from pathlib import Path
+import re
+import unittest
+from unittest.mock import MagicMock, mock_open, patch
+
+from pw_presubmit import todo_check
+
+# pylint: disable=attribute-defined-outside-init
+# todo-check: disable
+
+
+class TestTodoCheck(unittest.TestCase):
+ """Test TODO checker."""
+
+ def _run(self, regex: re.Pattern, contents: str) -> None:
+ self.ctx = MagicMock()
+ self.ctx.fail = MagicMock()
+ path = MagicMock(spec=Path('foo/bar'))
+
+ def mocked_open_read(*args, **kwargs):
+ return mock_open(read_data=contents)(*args, **kwargs)
+
+ with patch.object(path, 'open', mocked_open_read):
+ # pylint: disable=protected-access
+ todo_check._process_file(self.ctx, regex, path)
+
+ # pylint: enable=protected-access
+
+ def _run_bugs_users(self, contents: str) -> None:
+ self._run(todo_check.BUGS_OR_USERNAMES, contents)
+
+ def _run_bugs(self, contents: str) -> None:
+ self._run(todo_check.BUGS_ONLY, contents)
+
+ def test_one_bug(self) -> None:
+ contents = 'TODO(b/123): foo\n'
+ self._run_bugs_users(contents)
+ self.ctx.fail.assert_not_called()
+ self._run_bugs(contents)
+ self.ctx.fail.assert_not_called()
+
+ def test_two_bugs(self) -> None:
+ contents = 'TODO(b/123, b/456): foo\n'
+ self._run_bugs_users(contents)
+ self.ctx.fail.assert_not_called()
+ self._run_bugs(contents)
+ self.ctx.fail.assert_not_called()
+
+ def test_three_bugs(self) -> None:
+ contents = 'TODO(b/123,b/456,b/789): foo\n'
+ self._run_bugs_users(contents)
+ self.ctx.fail.assert_not_called()
+ self._run_bugs(contents)
+ self.ctx.fail.assert_not_called()
+
+ def test_one_username(self) -> None:
+ self._run_bugs_users('TODO(usera): foo\n')
+ self.ctx.fail.assert_not_called()
+
+ def test_two_usernames(self) -> None:
+ self._run_bugs_users('TODO(usera, userb): foo\n')
+ self.ctx.fail.assert_not_called()
+
+ def test_three_usernames(self) -> None:
+ self._run_bugs_users('TODO(usera,userb,userc): foo\n')
+ self.ctx.fail.assert_not_called()
+
+ def test_username_not_allowed(self) -> None:
+ self._run_bugs('TODO(usera): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_after_todo_bugsonly(self) -> None:
+ self._run_bugs('TODO (b/123): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_after_todo_bugsusers(self) -> None:
+ self._run_bugs_users('TODO (b/123): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_before_bug_bugsonly(self) -> None:
+ self._run_bugs('TODO( b/123): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_before_bug_bugsusers(self) -> None:
+ self._run_bugs_users('TODO( b/123): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_after_bug_bugsonly(self) -> None:
+ self._run_bugs('TODO(b/123 ): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_space_after_bug_bugsusers(self) -> None:
+ self._run_bugs_users('TODO(b/123 ): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_missing_explanation_bugsonly(self) -> None:
+ self._run_bugs('TODO(b/123)\n')
+ self.ctx.fail.assert_called()
+
+ def test_missing_explanation_bugsusers(self) -> None:
+ self._run_bugs_users('TODO(b/123)\n')
+ self.ctx.fail.assert_called()
+
+ def test_not_a_bug_bugsonly(self) -> None:
+ self._run_bugs('TODO(cl/123): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_not_a_bug_bugsusers(self) -> None:
+ self._run_bugs_users('TODO(cl/123): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_but_not_bug_bugsonly(self) -> None:
+ self._run_bugs('TODO(b/123, cl/123): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_bug_not_bug_bugsusers(self) -> None:
+ self._run_bugs_users('TODO(b/123, cl/123): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_empty_bugsonly(self) -> None:
+ self._run_bugs('TODO(): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_empty_bugsusers(self) -> None:
+ self._run_bugs_users('TODO(): foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_bare_bugsonly(self) -> None:
+ self._run_bugs('TODO: foo\n')
+ self.ctx.fail.assert_called()
+
+ def test_bare_bugsusers(self) -> None:
+ self._run_bugs_users('TODO: foo\n')
+ self.ctx.fail.assert_called()
+
+
+if __name__ == '__main__':
+ unittest.main()
+
+# todo-check: enable
diff --git a/pw_presubmit/py/tools_test.py b/pw_presubmit/py/tools_test.py
index 5c9d165cd..af6d685ed 100755
--- a/pw_presubmit/py/tools_test.py
+++ b/pw_presubmit/py/tools_test.py
@@ -21,6 +21,7 @@ from pw_presubmit import tools
class FlattenTest(unittest.TestCase):
"""Tests the flatten function, which flattens iterables."""
+
def test_empty(self):
self.assertEqual([], list(tools.flatten()))
self.assertEqual([], list(tools.flatten([])))
@@ -28,21 +29,31 @@ class FlattenTest(unittest.TestCase):
self.assertEqual([], list(tools.flatten([[], (), [[]]], ((), []))))
def test_no_nesting(self):
- self.assertEqual(['a', 'bcd', 123, 45.6],
- list(tools.flatten('a', 'bcd', 123, 45.6)))
- self.assertEqual(['a', 'bcd', 123, 45.6],
- list(tools.flatten(['a', 'bcd', 123, 45.6])))
- self.assertEqual(['a', 'bcd', 123, 45.6],
- list(tools.flatten(['a', 'bcd'], [123, 45.6])))
+ self.assertEqual(
+ ['a', 'bcd', 123, 45.6], list(tools.flatten('a', 'bcd', 123, 45.6))
+ )
+ self.assertEqual(
+ ['a', 'bcd', 123, 45.6],
+ list(tools.flatten(['a', 'bcd', 123, 45.6])),
+ )
+ self.assertEqual(
+ ['a', 'bcd', 123, 45.6],
+ list(tools.flatten(['a', 'bcd'], [123, 45.6])),
+ )
def test_nesting(self):
- self.assertEqual(['a', 'bcd', 123, 45.6],
- list(tools.flatten('a', ['bcd'], [123], 45.6)))
- self.assertEqual(['a', 'bcd', 123, 45.6],
- list(tools.flatten([['a', ('bcd', [123])], 45.6])))
- self.assertEqual(['a', 'bcd', 123, 45.6],
- list(tools.flatten([('a', 'bcd')],
- [[[[123]]], 45.6])))
+ self.assertEqual(
+ ['a', 'bcd', 123, 45.6],
+ list(tools.flatten('a', ['bcd'], [123], 45.6)),
+ )
+ self.assertEqual(
+ ['a', 'bcd', 123, 45.6],
+ list(tools.flatten([['a', ('bcd', [123])], 45.6])),
+ )
+ self.assertEqual(
+ ['a', 'bcd', 123, 45.6],
+ list(tools.flatten([('a', 'bcd')], [[[[123]]], 45.6])),
+ )
if __name__ == '__main__':
diff --git a/pw_protobuf/Android.bp b/pw_protobuf/Android.bp
new file mode 100644
index 000000000..5ab8f35b7
--- /dev/null
+++ b/pw_protobuf/Android.bp
@@ -0,0 +1,107 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_protobuf",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ header_libs: [
+ "fuschia_sdk_lib_fit",
+ "fuschia_sdk_lib_stdcompat",
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_function_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
+ host_supported: true,
+ srcs: [
+ "decoder.cc",
+ "encoder.cc",
+ "find.cc",
+ "map_utils.cc",
+ "message.cc",
+ "stream_decoder.cc",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_containers",
+ "pw_status",
+ "pw_stream",
+ "pw_string",
+ "pw_varint",
+ ],
+}
+
+genrule {
+ name: "pw_protobuf_codegen_protos_py",
+ srcs: ["pw_protobuf_codegen_protos/codegen_options.proto"],
+ cmd: "$(location aprotoc) " +
+ "-I$$(dirname $(in)) " +
+ "--python_out=$(genDir) " +
+ "$(in)",
+ out: [
+ "codegen_options_pb2.py",
+ ],
+ tools: [
+ "aprotoc",
+ ],
+}
+
+python_library_host {
+ name: "pw_protobuf_codegen_protos_py_lib",
+ srcs: [
+ ":pw_protobuf_codegen_protos_py",
+ ],
+ pkg_path: "pw_protobuf_codegen_protos",
+}
+
+genrule {
+ name: "pw_protobuf_protos_py",
+ srcs: [
+ "pw_protobuf_protos/common.proto",
+ "pw_protobuf_protos/field_options.proto",
+ "pw_protobuf_protos/status.proto",
+ ":libprotobuf-internal-descriptor-proto",
+ ],
+ cmd: "$(location aprotoc) " +
+ "-I$$(dirname $(location pw_protobuf_protos/common.proto)) " +
+ "-Iexternal/protobuf/src/ " +
+ "--python_out=$(genDir) " +
+ "$(in)",
+ out: [
+ "common_pb2.py",
+ "field_options_pb2.py",
+ "status_pb2.py",
+ ],
+ tools: [
+ "aprotoc",
+ ],
+}
+
+python_library_host {
+ name: "pw_protobuf_protos_py_lib",
+ srcs: [
+ ":pw_protobuf_protos_py",
+ ],
+ pkg_path: "pw_protobuf_protos",
+} \ No newline at end of file
diff --git a/pw_protobuf/BUILD.bazel b/pw_protobuf/BUILD.bazel
index 316576e2b..33c68926e 100644
--- a/pw_protobuf/BUILD.bazel
+++ b/pw_protobuf/BUILD.bazel
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -16,9 +16,11 @@ load("@rules_proto//proto:defs.bzl", "proto_library")
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
+ "pw_cc_perf_test",
"pw_cc_test",
)
load("//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@rules_proto_grpc//:defs.bzl", "proto_plugin")
load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
@@ -46,6 +48,7 @@ pw_cc_library(
"public/pw_protobuf/decoder.h",
"public/pw_protobuf/encoder.h",
"public/pw_protobuf/find.h",
+ "public/pw_protobuf/internal/codegen.h",
"public/pw_protobuf/internal/proto_integer_base.h",
"public/pw_protobuf/map_utils.h",
"public/pw_protobuf/message.h",
@@ -58,14 +61,16 @@ pw_cc_library(
":config",
"//pw_assert",
"//pw_bytes",
+ "//pw_bytes:bit",
"//pw_containers:vector",
- "//pw_polyfill:bit",
- "//pw_polyfill:overrides",
+ "//pw_function",
+ "//pw_preprocessor",
"//pw_result",
"//pw_span",
"//pw_status",
"//pw_stream",
"//pw_stream:interval_reader",
+ "//pw_string:string",
"//pw_varint",
"//pw_varint:stream",
],
@@ -104,7 +109,23 @@ pw_cc_test(
pw_cc_fuzz_test(
name = "encoder_fuzz_test",
- srcs = ["encoder_fuzzer.cc"],
+ srcs = [
+ "encoder_fuzzer.cc",
+ "fuzz.h",
+ ],
+ deps = [
+ "//pw_fuzzer",
+ "//pw_protobuf",
+ "//pw_span",
+ ],
+)
+
+pw_cc_fuzz_test(
+ name = "decoder_fuzz_test",
+ srcs = [
+ "decoder_fuzzer.cc",
+ "fuzz.h",
+ ],
deps = [
"//pw_fuzzer",
"//pw_protobuf",
@@ -157,13 +178,80 @@ pw_cc_test(
],
)
+pw_cc_perf_test(
+ name = "encoder_perf_test",
+ srcs = ["encoder_perf_test.cc"],
+ deps = [
+ ":pw_protobuf",
+ "//pw_unit_test",
+ ],
+)
+
+proto_library(
+ name = "codegen_protos",
+ srcs = [
+ "pw_protobuf_codegen_protos/codegen_options.proto",
+ ],
+ strip_import_prefix = "/pw_protobuf",
+)
+
+py_proto_library(
+ name = "codegen_protos_pb2",
+ srcs = [
+ "pw_protobuf_codegen_protos/codegen_options.proto",
+ ],
+)
+
proto_library(
- name = "common_protos",
+ name = "common_proto",
srcs = [
"pw_protobuf_protos/common.proto",
+ ],
+ strip_import_prefix = "/pw_protobuf",
+)
+
+proto_library(
+ name = "field_options_proto",
+ srcs = [
+ "pw_protobuf_protos/field_options.proto",
+ ],
+ strip_import_prefix = "/pw_protobuf",
+ deps = [
+ "@com_google_protobuf//:descriptor_proto",
+ ],
+)
+
+py_proto_library(
+ name = "field_options_proto_pb2",
+ srcs = [
+ "pw_protobuf_protos/field_options.proto",
+ ],
+ deps = [
+ "@com_google_protobuf//:protobuf_python",
+ ],
+)
+
+proto_library(
+ name = "status_proto",
+ srcs = [
"pw_protobuf_protos/status.proto",
],
- strip_import_prefix = "//pw_protobuf",
+ strip_import_prefix = "/pw_protobuf",
+)
+
+py_proto_library(
+ name = "status_proto_pb2",
+ srcs = [
+ "pw_protobuf_protos/status.proto",
+ ],
+)
+
+proto_library(
+ name = "codegen_test_deps_protos",
+ srcs = [
+ "pw_protobuf_test_deps_protos/imported.proto",
+ ],
+ strip_import_prefix = "/pw_protobuf",
)
proto_library(
@@ -173,18 +261,23 @@ proto_library(
"pw_protobuf_test_protos/imported.proto",
"pw_protobuf_test_protos/importer.proto",
"pw_protobuf_test_protos/non_pw_package.proto",
+ "pw_protobuf_test_protos/optional.proto",
"pw_protobuf_test_protos/proto2.proto",
"pw_protobuf_test_protos/repeated.proto",
+ "pw_protobuf_test_protos/size_report.proto",
+ ],
+ strip_import_prefix = "/pw_protobuf",
+ deps = [
+ ":codegen_test_deps_protos",
+ ":common_proto",
],
- strip_import_prefix = "//pw_protobuf",
- deps = [":common_protos"],
)
pw_proto_library(
name = "codegen_test_proto_cc",
deps = [
":codegen_test_proto",
- ":common_protos",
+ ":common_proto",
],
)
@@ -214,6 +307,14 @@ pw_cc_test(
],
)
+# TODO(tpudlik): Figure out how to add options file support to Bazel.
+filegroup(
+ name = "codegen_message_test",
+ srcs = [
+ "codegen_message_test.cc",
+ ],
+)
+
# TODO(frolv): Figure out how to add facade tests to Bazel.
filegroup(
name = "varint_size_test",
diff --git a/pw_protobuf/BUILD.gn b/pw_protobuf/BUILD.gn
index 626e5cc5d..97f8c82fe 100644
--- a/pw_protobuf/BUILD.gn
+++ b/pw_protobuf/BUILD.gn
@@ -19,6 +19,7 @@ import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_fuzzer/fuzzer.gni")
+import("$dir_pw_perf_test/perf_test.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_unit_test/facade_test.gni")
import("$dir_pw_unit_test/test.gni")
@@ -45,13 +46,17 @@ pw_source_set("pw_protobuf") {
public_configs = [ ":public_include_path" ]
public_deps = [
":config",
+ "$dir_pw_bytes:bit",
"$dir_pw_containers:vector",
"$dir_pw_stream:interval_reader",
"$dir_pw_varint:stream",
dir_pw_assert,
dir_pw_bytes,
+ dir_pw_function,
dir_pw_log,
+ dir_pw_preprocessor,
dir_pw_result,
+ dir_pw_span,
dir_pw_status,
dir_pw_stream,
dir_pw_varint,
@@ -60,6 +65,7 @@ pw_source_set("pw_protobuf") {
"public/pw_protobuf/decoder.h",
"public/pw_protobuf/encoder.h",
"public/pw_protobuf/find.h",
+ "public/pw_protobuf/internal/codegen.h",
"public/pw_protobuf/internal/proto_integer_base.h",
"public/pw_protobuf/map_utils.h",
"public/pw_protobuf/message.h",
@@ -75,6 +81,7 @@ pw_source_set("pw_protobuf") {
"message.cc",
"stream_decoder.cc",
]
+ deps = [ "$dir_pw_string:string" ]
}
pw_source_set("bytes_utils") {
@@ -89,10 +96,20 @@ pw_source_set("bytes_utils") {
}
pw_doc_group("docs") {
- sources = [ "docs.rst" ]
+ sources = [
+ "docs.rst",
+ "size_report.rst",
+ ]
+ inputs = [
+ "pw_protobuf_test_protos/size_report.options",
+ "pw_protobuf_test_protos/size_report.proto",
+ ]
report_deps = [
- "size_report:decoder_full",
"size_report:decoder_incremental",
+ "size_report:decoder_partial",
+ "size_report:oneof_codegen_size_comparison",
+ "size_report:protobuf_overview",
+ "size_report:simple_codegen_size_comparison",
]
}
@@ -100,9 +117,11 @@ pw_test_group("tests") {
tests = [
":codegen_decoder_test",
":codegen_encoder_test",
+ ":codegen_message_test",
":decoder_test",
":encoder_test",
- ":encoder_fuzzer",
+ ":encoder_fuzzer_test",
+ ":decoder_fuzzer_test",
":find_test",
":map_utils_test",
":message_test",
@@ -112,6 +131,13 @@ pw_test_group("tests") {
]
}
+group("fuzzers") {
+ deps = [
+ ":decoder_fuzzer",
+ ":encoder_fuzzer",
+ ]
+}
+
pw_test("decoder_test") {
deps = [ ":pw_protobuf" ]
sources = [ "decoder_test.cc" ]
@@ -137,6 +163,14 @@ pw_test("codegen_encoder_test") {
sources = [ "codegen_encoder_test.cc" ]
}
+pw_test("codegen_message_test") {
+ deps = [
+ ":codegen_test_protos.pwpb",
+ dir_pw_string,
+ ]
+ sources = [ "codegen_message_test.cc" ]
+}
+
pw_test("serialized_size_test") {
deps = [ ":pw_protobuf" ]
sources = [ "serialized_size_test.cc" ]
@@ -157,6 +191,16 @@ pw_test("message_test") {
sources = [ "message_test.cc" ]
}
+group("perf_tests") {
+ deps = [ ":encoder_perf_test" ]
+}
+
+pw_perf_test("encoder_perf_test") {
+ enable_if = pw_perf_test_TIMER_INTERFACE_BACKEND != ""
+ deps = [ ":pw_protobuf" ]
+ sources = [ "encoder_perf_test.cc" ]
+}
+
config("one_byte_varint") {
defines = [ "PW_PROTOBUF_CFG_MAX_VARINT_SIZE=1" ]
visibility = [ ":*" ]
@@ -175,26 +219,80 @@ pw_facade_test("varint_size_test") {
sources = [ "varint_size_test.cc" ]
}
+pw_proto_library("codegen_protos") {
+ sources = [ "pw_protobuf_codegen_protos/codegen_options.proto" ]
+}
+
pw_proto_library("common_protos") {
sources = [
"pw_protobuf_protos/common.proto",
+ "pw_protobuf_protos/field_options.proto",
"pw_protobuf_protos/status.proto",
]
}
+pw_proto_library("codegen_test_deps_protos") {
+ sources = [ "pw_protobuf_test_deps_protos/imported.proto" ]
+ inputs = [ "pw_protobuf_test_deps_protos/imported.options" ]
+}
+
pw_proto_library("codegen_test_protos") {
sources = [
"pw_protobuf_test_protos/full_test.proto",
"pw_protobuf_test_protos/imported.proto",
"pw_protobuf_test_protos/importer.proto",
"pw_protobuf_test_protos/non_pw_package.proto",
+ "pw_protobuf_test_protos/optional.proto",
"pw_protobuf_test_protos/proto2.proto",
"pw_protobuf_test_protos/repeated.proto",
+ "pw_protobuf_test_protos/size_report.proto",
+ ]
+ inputs = [
+ "pw_protobuf_test_protos/full_test.options",
+ "pw_protobuf_test_protos/optional.options",
+ "pw_protobuf_test_protos/imported.options",
+ "pw_protobuf_test_protos/repeated.options",
+ ]
+ deps = [
+ ":codegen_test_deps_protos",
+ ":common_protos",
]
- deps = [ ":common_protos" ]
}
+# The tests below have a large amount of global and static data.
+# TODO(b/234883746): Replace this with a better size-based check.
+_small_executable_target_types = [
+ "stm32f429i_executable",
+ "lm3s6965evb_executable",
+]
+_supports_large_tests =
+ _small_executable_target_types + [ pw_build_EXECUTABLE_TARGET_TYPE ] -
+ _small_executable_target_types != []
+
pw_fuzzer("encoder_fuzzer") {
- sources = [ "encoder_fuzzer.cc" ]
- deps = [ ":pw_protobuf" ]
+ sources = [
+ "encoder_fuzzer.cc",
+ "fuzz.h",
+ ]
+ deps = [
+ ":pw_protobuf",
+ dir_pw_fuzzer,
+ dir_pw_span,
+ ]
+ enable_test_if = _supports_large_tests
+}
+
+pw_fuzzer("decoder_fuzzer") {
+ sources = [
+ "decoder_fuzzer.cc",
+ "fuzz.h",
+ ]
+ deps = [
+ ":pw_protobuf",
+ dir_pw_fuzzer,
+ dir_pw_span,
+ dir_pw_status,
+ dir_pw_stream,
+ ]
+ enable_test_if = _supports_large_tests
}
diff --git a/pw_protobuf/CMakeLists.txt b/pw_protobuf/CMakeLists.txt
index 73987db21..77d0ccf32 100644
--- a/pw_protobuf/CMakeLists.txt
+++ b/pw_protobuf/CMakeLists.txt
@@ -17,7 +17,7 @@ include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
pw_add_module_config(pw_protobuf_CONFIG)
-pw_add_module_library(pw_protobuf.config
+pw_add_library(pw_protobuf.config INTERFACE
HEADERS
public/pw_protobuf/config.h
PUBLIC_INCLUDES
@@ -26,11 +26,12 @@ pw_add_module_library(pw_protobuf.config
${pw_protobuf_CONFIG}
)
-pw_add_module_library(pw_protobuf
+pw_add_library(pw_protobuf STATIC
HEADERS
public/pw_protobuf/decoder.h
public/pw_protobuf/encoder.h
public/pw_protobuf/find.h
+ public/pw_protobuf/internal/codegen.h
public/pw_protobuf/internal/proto_integer_base.h
public/pw_protobuf/map_utils.h
public/pw_protobuf/message.h
@@ -42,16 +43,20 @@ pw_add_module_library(pw_protobuf
PUBLIC_DEPS
pw_assert
pw_bytes
+ pw_bytes.bit
pw_containers.vector
+ pw_function
+ pw_preprocessor
pw_protobuf.config
- pw_polyfill.span
- pw_polyfill.cstddef
pw_result
+ pw_span
pw_status
pw_stream
pw_stream.interval_reader
pw_varint
pw_varint.stream
+ PRIVATE_DEPS
+ pw_string.string
SOURCES
decoder.cc
encoder.cc
@@ -61,7 +66,7 @@ pw_add_module_library(pw_protobuf
stream_decoder.cc
)
-pw_add_module_library(pw_protobuf.bytes_utils
+pw_add_library(pw_protobuf.bytes_utils INTERFACE
HEADERS
public/pw_protobuf/bytes_utils.h
PUBLIC_INCLUDES
@@ -76,7 +81,7 @@ pw_add_module_library(pw_protobuf.bytes_utils
pw_add_test(pw_protobuf.decoder_test
SOURCES
decoder_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
GROUPS
modules
@@ -86,7 +91,7 @@ pw_add_test(pw_protobuf.decoder_test
pw_add_test(pw_protobuf.encoder_test
SOURCES
encoder_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
GROUPS
modules
@@ -96,7 +101,7 @@ pw_add_test(pw_protobuf.encoder_test
pw_add_test(pw_protobuf.find_test
SOURCES
find_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
GROUPS
modules
@@ -106,7 +111,7 @@ pw_add_test(pw_protobuf.find_test
pw_add_test(pw_protobuf.codegen_decoder_test
SOURCES
codegen_decoder_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
pw_protobuf.codegen_test_protos.pwpb
GROUPS
@@ -117,7 +122,7 @@ pw_add_test(pw_protobuf.codegen_decoder_test
pw_add_test(pw_protobuf.codegen_encoder_test
SOURCES
codegen_encoder_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
pw_protobuf.codegen_test_protos.pwpb
GROUPS
@@ -125,10 +130,22 @@ pw_add_test(pw_protobuf.codegen_encoder_test
pw_protobuf
)
+pw_add_test(pw_protobuf.codegen_message_test
+ SOURCES
+ codegen_message_test.cc
+ PRIVATE_DEPS
+ pw_protobuf
+ pw_protobuf.codegen_test_protos.pwpb
+ pw_string
+ GROUPS
+ modules
+ pw_protobuf
+)
+
pw_add_test(pw_protobuf.serialized_size_test
SOURCES
serialized_size_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
GROUPS
modules
@@ -138,7 +155,7 @@ pw_add_test(pw_protobuf.serialized_size_test
pw_add_test(pw_protobuf.stream_decoder_test
SOURCES
stream_decoder_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
GROUPS
modules
@@ -148,7 +165,7 @@ pw_add_test(pw_protobuf.stream_decoder_test
pw_add_test(pw_protobuf.map_utils_test
SOURCES
map_utils_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
GROUPS
modules
@@ -158,26 +175,56 @@ pw_add_test(pw_protobuf.map_utils_test
pw_add_test(pw_protobuf.message_test
SOURCES
message_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
GROUPS
modules
pw_protobuf
)
-pw_proto_library(pw_protobuf.common_protos
+pw_proto_library(pw_protobuf.common_proto
SOURCES
pw_protobuf_protos/common.proto
)
+pw_proto_library(pw_protobuf.status_proto
+ SOURCES
+ pw_protobuf_protos/status.proto
+)
+
+pw_proto_library(pw_protobuf.field_options_proto
+ SOURCES
+ pw_protobuf_protos/field_options.proto
+)
+
+pw_proto_library(pw_protobuf.codegen_protos
+ SOURCES
+ pw_protobuf_codegen_protos/codegen_options.proto
+)
+
+pw_proto_library(pw_protobuf.codegen_test_deps_protos
+ SOURCES
+ pw_protobuf_test_deps_protos/imported.proto
+ INPUTS
+ pw_protobuf_test_deps_protos/imported.options
+)
+
pw_proto_library(pw_protobuf.codegen_test_protos
SOURCES
pw_protobuf_test_protos/full_test.proto
pw_protobuf_test_protos/imported.proto
pw_protobuf_test_protos/importer.proto
pw_protobuf_test_protos/non_pw_package.proto
+ pw_protobuf_test_protos/optional.proto
pw_protobuf_test_protos/proto2.proto
pw_protobuf_test_protos/repeated.proto
+ INPUTS
+ pw_protobuf_test_protos/full_test.options
+ pw_protobuf_test_protos/imported.options
+ pw_protobuf_test_protos/optional.options
+ pw_protobuf_test_protos/repeated.options
DEPS
- pw_protobuf.common_protos
+ pw_protobuf.common_proto
+ pw_protobuf.status_proto
+ pw_protobuf.codegen_test_deps_protos
)
diff --git a/pw_protobuf/codegen_decoder_test.cc b/pw_protobuf/codegen_decoder_test.cc
index 657e6e9d0..09621a7c0 100644
--- a/pw_protobuf/codegen_decoder_test.cc
+++ b/pw_protobuf/codegen_decoder_test.cc
@@ -12,13 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
#include <array>
-#include <span>
#include <stdexcept>
#include <string_view>
#include "gtest/gtest.h"
#include "pw_bytes/span.h"
#include "pw_containers/vector.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_stream/memory_stream.h"
@@ -39,7 +39,20 @@
namespace pw::protobuf {
namespace {
-using namespace pw::protobuf::test;
+using test::pwpb::Bool;
+using test::pwpb::Enum;
+
+namespace DeviceInfo = test::pwpb::DeviceInfo;
+namespace KeyValuePair = test::pwpb::KeyValuePair;
+namespace Pigweed = test::pwpb::Pigweed;
+namespace Proto = test::pwpb::Proto;
+namespace RepeatedTest = test::pwpb::RepeatedTest;
+namespace TestResult = test::pwpb::TestResult;
+
+namespace imported {
+using ::pw::protobuf::test::imported::pwpb::IsValidStatus;
+using ::pw::protobuf::test::imported::pwpb::Status;
+} // namespace imported
TEST(Codegen, StreamDecoder) {
// clang-format off
@@ -118,17 +131,17 @@ TEST(Codegen, StreamDecoder) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
Pigweed::StreamDecoder pigweed(reader);
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::MAGIC_NUMBER);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kMagicNumber);
Result<uint32_t> magic_number = pigweed.ReadMagicNumber();
EXPECT_EQ(magic_number.status(), OkStatus());
EXPECT_EQ(magic_number.value(), 0x49u);
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::ZIGGY);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kZiggy);
Result<int32_t> ziggy = pigweed.ReadZiggy();
EXPECT_EQ(ziggy.status(), OkStatus());
EXPECT_EQ(ziggy.value(), -111);
@@ -136,7 +149,7 @@ TEST(Codegen, StreamDecoder) {
constexpr std::string_view kExpectedErrorMessage{"not a typewriter"};
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::ERROR_MESSAGE);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kErrorMessage);
std::array<char, 32> error_message{};
StatusWithSize error_message_status = pigweed.ReadErrorMessage(error_message);
EXPECT_EQ(error_message_status.status(), OkStatus());
@@ -147,20 +160,20 @@ TEST(Codegen, StreamDecoder) {
0);
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::BIN);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kBin);
Result<Pigweed::Protobuf::Binary> bin = pigweed.ReadBin();
EXPECT_EQ(bin.status(), OkStatus());
EXPECT_EQ(bin.value(), Pigweed::Protobuf::Binary::ZERO);
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::PIGWEED);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kPigweed);
{
Pigweed::Pigweed::StreamDecoder pigweed_pigweed =
pigweed.GetPigweedDecoder();
EXPECT_EQ(pigweed_pigweed.Next(), OkStatus());
EXPECT_EQ(pigweed_pigweed.Field().value(),
- Pigweed::Pigweed::Fields::STATUS);
+ Pigweed::Pigweed::Fields::kStatus);
Result<Bool> pigweed_status = pigweed_pigweed.ReadStatus();
EXPECT_EQ(pigweed_status.status(), OkStatus());
EXPECT_EQ(pigweed_status.value(), Bool::FILE_NOT_FOUND);
@@ -169,32 +182,32 @@ TEST(Codegen, StreamDecoder) {
}
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::PROTO);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kProto);
{
Proto::StreamDecoder proto = pigweed.GetProtoDecoder();
EXPECT_EQ(proto.Next(), OkStatus());
- EXPECT_EQ(proto.Field().value(), Proto::Fields::BIN);
+ EXPECT_EQ(proto.Field().value(), Proto::Fields::kBin);
Result<Proto::Binary> proto_bin = proto.ReadBin();
EXPECT_EQ(proto_bin.status(), OkStatus());
EXPECT_EQ(proto_bin.value(), Proto::Binary::OFF);
EXPECT_EQ(proto.Next(), OkStatus());
- EXPECT_EQ(proto.Field().value(), Proto::Fields::PIGWEED_PIGWEED_BIN);
+ EXPECT_EQ(proto.Field().value(), Proto::Fields::kPigweedPigweedBin);
Result<Pigweed::Pigweed::Binary> proto_pigweed_bin =
proto.ReadPigweedPigweedBin();
EXPECT_EQ(proto_pigweed_bin.status(), OkStatus());
EXPECT_EQ(proto_pigweed_bin.value(), Pigweed::Pigweed::Binary::ZERO);
EXPECT_EQ(proto.Next(), OkStatus());
- EXPECT_EQ(proto.Field().value(), Proto::Fields::PIGWEED_PROTOBUF_BIN);
+ EXPECT_EQ(proto.Field().value(), Proto::Fields::kPigweedProtobufBin);
Result<Pigweed::Protobuf::Binary> proto_protobuf_bin =
proto.ReadPigweedProtobufBin();
EXPECT_EQ(proto_protobuf_bin.status(), OkStatus());
EXPECT_EQ(proto_protobuf_bin.value(), Pigweed::Protobuf::Binary::ZERO);
EXPECT_EQ(proto.Next(), OkStatus());
- EXPECT_EQ(proto.Field().value(), Proto::Fields::META);
+ EXPECT_EQ(proto.Field().value(), Proto::Fields::kMeta);
{
Pigweed::Protobuf::Compiler::StreamDecoder meta = proto.GetMetaDecoder();
@@ -202,7 +215,7 @@ TEST(Codegen, StreamDecoder) {
EXPECT_EQ(meta.Next(), OkStatus());
EXPECT_EQ(meta.Field().value(),
- Pigweed::Protobuf::Compiler::Fields::FILE_NAME);
+ Pigweed::Protobuf::Compiler::Fields::kFileName);
std::array<char, 32> meta_file_name{};
StatusWithSize meta_file_name_status = meta.ReadFileName(meta_file_name);
EXPECT_EQ(meta_file_name_status.status(), OkStatus());
@@ -214,7 +227,7 @@ TEST(Codegen, StreamDecoder) {
EXPECT_EQ(meta.Next(), OkStatus());
EXPECT_EQ(meta.Field().value(),
- Pigweed::Protobuf::Compiler::Fields::STATUS);
+ Pigweed::Protobuf::Compiler::Fields::kStatus);
Result<Pigweed::Protobuf::Compiler::Status> meta_status =
meta.ReadStatus();
EXPECT_EQ(meta_status.status(), OkStatus());
@@ -225,14 +238,14 @@ TEST(Codegen, StreamDecoder) {
}
EXPECT_EQ(proto.Next(), OkStatus());
- EXPECT_EQ(proto.Field().value(), Proto::Fields::PIGWEED);
+ EXPECT_EQ(proto.Field().value(), Proto::Fields::kPigweed);
{
Pigweed::StreamDecoder proto_pigweed = proto.GetPigweedDecoder();
constexpr std::string_view kExpectedProtoErrorMessage{"here we go again"};
EXPECT_EQ(proto_pigweed.Next(), OkStatus());
- EXPECT_EQ(proto_pigweed.Field().value(), Pigweed::Fields::ERROR_MESSAGE);
+ EXPECT_EQ(proto_pigweed.Field().value(), Pigweed::Fields::kErrorMessage);
std::array<char, 32> proto_pigweed_error_message{};
StatusWithSize proto_pigweed_error_message_status =
proto_pigweed.ReadErrorMessage(proto_pigweed_error_message);
@@ -245,20 +258,20 @@ TEST(Codegen, StreamDecoder) {
0);
EXPECT_EQ(proto_pigweed.Next(), OkStatus());
- EXPECT_EQ(proto_pigweed.Field().value(), Pigweed::Fields::MAGIC_NUMBER);
+ EXPECT_EQ(proto_pigweed.Field().value(), Pigweed::Fields::kMagicNumber);
Result<uint32_t> proto_pigweed_magic_number =
proto_pigweed.ReadMagicNumber();
EXPECT_EQ(proto_pigweed_magic_number.status(), OkStatus());
EXPECT_EQ(proto_pigweed_magic_number.value(), 616u);
EXPECT_EQ(proto_pigweed.Next(), OkStatus());
- EXPECT_EQ(proto_pigweed.Field().value(), Pigweed::Fields::DEVICE_INFO);
+ EXPECT_EQ(proto_pigweed.Field().value(), Pigweed::Fields::kDeviceInfo);
{
DeviceInfo::StreamDecoder device_info =
proto_pigweed.GetDeviceInfoDecoder();
EXPECT_EQ(device_info.Next(), OkStatus());
- EXPECT_EQ(device_info.Field().value(), DeviceInfo::Fields::ATTRIBUTES);
+ EXPECT_EQ(device_info.Field().value(), DeviceInfo::Fields::kAttributes);
{
KeyValuePair::StreamDecoder key_value_pair =
device_info.GetAttributesDecoder();
@@ -267,7 +280,7 @@ TEST(Codegen, StreamDecoder) {
constexpr std::string_view kExpectedValue{"5.3.1"};
EXPECT_EQ(key_value_pair.Next(), OkStatus());
- EXPECT_EQ(key_value_pair.Field().value(), KeyValuePair::Fields::KEY);
+ EXPECT_EQ(key_value_pair.Field().value(), KeyValuePair::Fields::kKey);
std::array<char, 32> key{};
StatusWithSize key_status = key_value_pair.ReadKey(key);
EXPECT_EQ(key_status.status(), OkStatus());
@@ -278,7 +291,7 @@ TEST(Codegen, StreamDecoder) {
EXPECT_EQ(key_value_pair.Next(), OkStatus());
EXPECT_EQ(key_value_pair.Field().value(),
- KeyValuePair::Fields::VALUE);
+ KeyValuePair::Fields::kValue);
std::array<char, 32> value{};
StatusWithSize value_status = key_value_pair.ReadValue(value);
EXPECT_EQ(value_status.status(), OkStatus());
@@ -292,7 +305,7 @@ TEST(Codegen, StreamDecoder) {
}
EXPECT_EQ(device_info.Next(), OkStatus());
- EXPECT_EQ(device_info.Field().value(), DeviceInfo::Fields::ATTRIBUTES);
+ EXPECT_EQ(device_info.Field().value(), DeviceInfo::Fields::kAttributes);
{
KeyValuePair::StreamDecoder key_value_pair =
device_info.GetAttributesDecoder();
@@ -301,7 +314,7 @@ TEST(Codegen, StreamDecoder) {
constexpr std::string_view kExpectedValue{"left-soc"};
EXPECT_EQ(key_value_pair.Next(), OkStatus());
- EXPECT_EQ(key_value_pair.Field().value(), KeyValuePair::Fields::KEY);
+ EXPECT_EQ(key_value_pair.Field().value(), KeyValuePair::Fields::kKey);
std::array<char, 32> key{};
StatusWithSize key_status = key_value_pair.ReadKey(key);
EXPECT_EQ(key_status.status(), OkStatus());
@@ -312,7 +325,7 @@ TEST(Codegen, StreamDecoder) {
EXPECT_EQ(key_value_pair.Next(), OkStatus());
EXPECT_EQ(key_value_pair.Field().value(),
- KeyValuePair::Fields::VALUE);
+ KeyValuePair::Fields::kValue);
std::array<char, 32> value{};
StatusWithSize value_status = key_value_pair.ReadValue(value);
EXPECT_EQ(value_status.status(), OkStatus());
@@ -326,7 +339,7 @@ TEST(Codegen, StreamDecoder) {
}
EXPECT_EQ(device_info.Next(), OkStatus());
- EXPECT_EQ(device_info.Field().value(), DeviceInfo::Fields::STATUS);
+ EXPECT_EQ(device_info.Field().value(), DeviceInfo::Fields::kStatus);
Result<DeviceInfo::DeviceStatus> device_info_status =
device_info.ReadStatus();
EXPECT_EQ(device_info_status.status(), OkStatus());
@@ -343,12 +356,12 @@ TEST(Codegen, StreamDecoder) {
for (int i = 0; i < 5; ++i) {
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::ID);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kId);
Proto::ID::StreamDecoder id = pigweed.GetIdDecoder();
EXPECT_EQ(id.Next(), OkStatus());
- EXPECT_EQ(id.Field().value(), Proto::ID::Fields::ID);
+ EXPECT_EQ(id.Field().value(), Proto::ID::Fields::kId);
Result<uint32_t> id_id = id.ReadId();
EXPECT_EQ(id_id.status(), OkStatus());
EXPECT_EQ(id_id.value(), 5u * i * i + 3 * i + 49);
@@ -368,11 +381,11 @@ TEST(Codegen, ResourceExhausted) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
Pigweed::StreamDecoder pigweed(reader);
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::ERROR_MESSAGE);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kErrorMessage);
std::array<char, 8> error_message{};
StatusWithSize error_message_status = pigweed.ReadErrorMessage(error_message);
EXPECT_EQ(error_message_status.status(), Status::ResourceExhausted());
@@ -390,13 +403,13 @@ TEST(Codegen, BytesReader) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
Pigweed::StreamDecoder pigweed(reader);
constexpr std::string_view kExpectedErrorMessage{"not a typewriter"};
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::ERROR_MESSAGE);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kErrorMessage);
{
StreamDecoder::BytesReader bytes_reader = pigweed.GetErrorMessageReader();
EXPECT_EQ(bytes_reader.field_size(), kExpectedErrorMessage.size());
@@ -420,24 +433,34 @@ TEST(Codegen, BytesReader) {
TEST(Codegen, Enum) {
// clang-format off
constexpr uint8_t proto_data[] = {
- // pigweed.bin (value value)
+ // pigweed.bin (valid value)
0x40, 0x01,
+ // pigweed.bin (unknown value)
+ 0x40, 0x7f,
// pigweed.bin (invalid value)
0x40, 0xff,
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
Pigweed::StreamDecoder pigweed(reader);
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::BIN);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kBin);
Result<Pigweed::Protobuf::Binary> bin = pigweed.ReadBin();
EXPECT_EQ(bin.status(), OkStatus());
+ EXPECT_TRUE(Pigweed::Protobuf::IsValidBinary(bin.value()));
EXPECT_EQ(bin.value(), Pigweed::Protobuf::Binary::ZERO);
EXPECT_EQ(pigweed.Next(), OkStatus());
- EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::BIN);
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kBin);
+ bin = pigweed.ReadBin();
+ EXPECT_EQ(bin.status(), OkStatus());
+ EXPECT_FALSE(Pigweed::Protobuf::IsValidBinary(bin.value()));
+ EXPECT_EQ(static_cast<uint32_t>(bin.value()), 0x7fu);
+
+ EXPECT_EQ(pigweed.Next(), OkStatus());
+ EXPECT_EQ(pigweed.Field().value(), Pigweed::Fields::kBin);
bin = pigweed.ReadBin();
EXPECT_EQ(bin.status(), Status::DataLoss());
}
@@ -445,24 +468,34 @@ TEST(Codegen, Enum) {
TEST(Codegen, ImportedEnum) {
// clang-format off
constexpr uint8_t proto_data[] = {
- // result.status (value value)
+ // result.status (valid value)
0x08, 0x01,
+ // result.status (unknown value)
+ 0x08, 0x7f,
// result.status (invalid value)
0x08, 0xff,
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
TestResult::StreamDecoder test_result(reader);
EXPECT_EQ(test_result.Next(), OkStatus());
- EXPECT_EQ(test_result.Field().value(), TestResult::Fields::STATUS);
+ EXPECT_EQ(test_result.Field().value(), TestResult::Fields::kStatus);
Result<imported::Status> status = test_result.ReadStatus();
EXPECT_EQ(status.status(), OkStatus());
+ EXPECT_TRUE(imported::IsValidStatus(status.value()));
EXPECT_EQ(status.value(), imported::Status::NOT_OK);
EXPECT_EQ(test_result.Next(), OkStatus());
- EXPECT_EQ(test_result.Field().value(), TestResult::Fields::STATUS);
+ EXPECT_EQ(test_result.Field().value(), TestResult::Fields::kStatus);
+ status = test_result.ReadStatus();
+ EXPECT_EQ(status.status(), OkStatus());
+ EXPECT_FALSE(imported::IsValidStatus(status.value()));
+ EXPECT_EQ(static_cast<uint32_t>(status.value()), 0x7fu);
+
+ EXPECT_EQ(test_result.Next(), OkStatus());
+ EXPECT_EQ(test_result.Field().value(), TestResult::Fields::kStatus);
status = test_result.ReadStatus();
EXPECT_EQ(status.status(), Status::DataLoss());
}
@@ -483,12 +516,12 @@ TEST(CodegenRepeated, NonPackedScalar) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
for (int i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
Result<uint32_t> result = repeated_test.ReadUint32s();
EXPECT_EQ(result.status(), OkStatus());
@@ -497,7 +530,7 @@ TEST(CodegenRepeated, NonPackedScalar) {
for (int i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
Result<uint32_t> result = repeated_test.ReadFixed32s();
EXPECT_EQ(result.status(), OkStatus());
@@ -523,14 +556,14 @@ TEST(CodegenRepeated, NonPackedScalarVector) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
pw::Vector<uint32_t, 8> uint32s{};
for (int i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
Status status = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(status, OkStatus());
@@ -545,7 +578,7 @@ TEST(CodegenRepeated, NonPackedScalarVector) {
for (int i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
Status status = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(status, OkStatus());
@@ -570,25 +603,25 @@ TEST(CodegenRepeated, NonPackedVarintScalarVectorFull) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
pw::Vector<uint32_t, 2> uint32s{};
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
Status status = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(uint32s.size(), 1u);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
status = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(uint32s.size(), 2u);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
status = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(status, Status::ResourceExhausted());
EXPECT_EQ(uint32s.size(), 2u);
@@ -609,25 +642,25 @@ TEST(CodegenRepeated, NonPackedFixedScalarVectorFull) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
pw::Vector<uint32_t, 2> fixed32s{};
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
Status status = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(fixed32s.size(), 1u);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
status = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(fixed32s.size(), 2u);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
status = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(status, Status::ResourceExhausted());
EXPECT_EQ(fixed32s.size(), 2u);
@@ -655,11 +688,11 @@ TEST(CodegenRepeated, PackedScalar) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
std::array<uint32_t, 8> uint32s{};
StatusWithSize sws = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(sws.status(), OkStatus());
@@ -670,7 +703,7 @@ TEST(CodegenRepeated, PackedScalar) {
}
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
std::array<uint32_t, 8> fixed32s{};
sws = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(sws.status(), OkStatus());
@@ -695,11 +728,11 @@ TEST(CodegenRepeated, PackedVarintScalarExhausted) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
std::array<uint32_t, 2> uint32s{};
StatusWithSize sws = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(sws.status(), Status::ResourceExhausted());
@@ -722,11 +755,11 @@ TEST(CodegenRepeated, PackedFixedScalarExhausted) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
std::array<uint32_t, 2> fixed32s{};
StatusWithSize sws = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(sws.status(), Status::ResourceExhausted());
@@ -751,11 +784,11 @@ TEST(CodegenRepeated, PackedScalarVector) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
pw::Vector<uint32_t, 8> uint32s{};
Status status = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(status, OkStatus());
@@ -766,7 +799,7 @@ TEST(CodegenRepeated, PackedScalarVector) {
}
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
pw::Vector<uint32_t, 8> fixed32s{};
status = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(status, OkStatus());
@@ -791,11 +824,11 @@ TEST(CodegenRepeated, PackedVarintScalarVectorFull) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
pw::Vector<uint32_t, 2> uint32s{};
Status status = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(status, Status::ResourceExhausted());
@@ -818,11 +851,11 @@ TEST(CodegenRepeated, PackedFixedScalarVectorFull) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
pw::Vector<uint32_t, 2> fixed32s{};
Status status = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(status, Status::ResourceExhausted());
@@ -859,18 +892,18 @@ TEST(CodegenRepeated, PackedScalarVectorRepeated) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
pw::Vector<uint32_t, 8> uint32s{};
Status status = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(uint32s.size(), 4u);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::UINT32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kUint32s);
status = repeated_test.ReadUint32s(uint32s);
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(uint32s.size(), 8u);
@@ -880,14 +913,14 @@ TEST(CodegenRepeated, PackedScalarVectorRepeated) {
}
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
pw::Vector<uint32_t, 8> fixed32s{};
status = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(fixed32s.size(), 4u);
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::FIXED32S);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kFixed32s);
status = repeated_test.ReadFixed32s(fixed32s);
EXPECT_EQ(status, OkStatus());
EXPECT_EQ(fixed32s.size(), 8u);
@@ -910,7 +943,7 @@ TEST(CodegenRepeated, NonScalar) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(proto_data)));
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
RepeatedTest::StreamDecoder repeated_test(reader);
constexpr std::array<std::string_view, 4> kExpectedString{
@@ -918,7 +951,7 @@ TEST(CodegenRepeated, NonScalar) {
for (int i = 0; i < 4; ++i) {
EXPECT_EQ(repeated_test.Next(), OkStatus());
- EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::STRINGS);
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kStrings);
std::array<char, 32> string{};
StatusWithSize sws = repeated_test.ReadStrings(string);
EXPECT_EQ(sws.status(), OkStatus());
@@ -932,5 +965,65 @@ TEST(CodegenRepeated, NonScalar) {
EXPECT_EQ(repeated_test.Next(), Status::OutOfRange());
}
+TEST(CodegenRepeated, PackedEnum) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // enums[], v={RED, GREEN, AMBER, RED}
+ 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ EXPECT_EQ(repeated_test.Next(), OkStatus());
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kEnums);
+ std::array<Enum, 4> enums{};
+ StatusWithSize sws = repeated_test.ReadEnums(enums);
+ EXPECT_EQ(sws.status(), OkStatus());
+ ASSERT_EQ(sws.size(), 4u);
+
+ for (int i = 0; i < 4; ++i) {
+ EXPECT_TRUE(IsValidEnum(enums[i]));
+ }
+
+ EXPECT_EQ(enums[0], Enum::RED);
+ EXPECT_EQ(enums[1], Enum::GREEN);
+ EXPECT_EQ(enums[2], Enum::AMBER);
+ EXPECT_EQ(enums[3], Enum::RED);
+
+ EXPECT_EQ(repeated_test.Next(), Status::OutOfRange());
+}
+
+TEST(CodegenRepeated, PackedEnumVector) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // enums[], v={RED, GREEN, AMBER, RED}
+ 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ EXPECT_EQ(repeated_test.Next(), OkStatus());
+ EXPECT_EQ(repeated_test.Field().value(), RepeatedTest::Fields::kEnums);
+ pw::Vector<Enum, 4> enums{};
+ Status status = repeated_test.ReadEnums(enums);
+ EXPECT_EQ(status, OkStatus());
+ ASSERT_EQ(enums.size(), 4u);
+
+ for (int i = 0; i < 4; ++i) {
+ EXPECT_TRUE(IsValidEnum(enums[i]));
+ }
+
+ EXPECT_EQ(enums[0], Enum::RED);
+ EXPECT_EQ(enums[1], Enum::GREEN);
+ EXPECT_EQ(enums[2], Enum::AMBER);
+ EXPECT_EQ(enums[3], Enum::RED);
+
+ EXPECT_EQ(repeated_test.Next(), Status::OutOfRange());
+}
+
} // namespace
} // namespace pw::protobuf
diff --git a/pw_protobuf/codegen_encoder_test.cc b/pw_protobuf/codegen_encoder_test.cc
index 93bb02db3..de0a581ef 100644
--- a/pw_protobuf/codegen_encoder_test.cc
+++ b/pw_protobuf/codegen_encoder_test.cc
@@ -11,10 +11,9 @@
// 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 <span>
-
#include "gtest/gtest.h"
#include "pw_protobuf/encoder.h"
+#include "pw_span/span.h"
#include "pw_stream/memory_stream.h"
// These header files contain the code generated by the pw_protobuf plugin.
@@ -33,55 +32,67 @@
namespace pw::protobuf {
namespace {
-using namespace pw::protobuf::test;
+using test::pwpb::Bool;
+using test::pwpb::Enum;
+
+namespace Bar = test::pwpb::Bar;
+namespace BaseMessage = test::pwpb::BaseMessage;
+namespace Crate = test::pwpb::Crate;
+namespace DeviceInfo = test::pwpb::DeviceInfo;
+namespace Foo = test::pwpb::Foo;
+namespace IntegerMetadata = test::pwpb::IntegerMetadata;
+namespace KeyValuePair = test::pwpb::KeyValuePair;
+namespace Overlay = test::pwpb::Overlay;
+namespace Period = test::pwpb::Period;
+namespace Pigweed = test::pwpb::Pigweed;
+namespace Proto = test::pwpb::Proto;
+namespace RepeatedTest = test::pwpb::RepeatedTest;
+
+namespace imported {
+namespace Timestamp = ::pw::protobuf::test::imported::pwpb::Timestamp;
+} // namespace imported
TEST(Codegen, Codegen) {
- std::byte encode_buffer[512];
- std::byte temp_buffer[512];
+ std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes +
+ DeviceInfo::kMaxEncodedSizeBytes];
+ std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes +
+ DeviceInfo::kMaxEncodedSizeBytes];
stream::MemoryWriter writer(encode_buffer);
Pigweed::StreamEncoder pigweed(writer, temp_buffer);
- pigweed.WriteMagicNumber(73)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- pigweed.WriteZiggy(-111)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- pigweed.WriteErrorMessage("not a typewriter")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- pigweed.WriteBin(Pigweed::Protobuf::Binary::ZERO)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), pigweed.WriteMagicNumber(73));
+ ASSERT_EQ(OkStatus(), pigweed.WriteZiggy(-111));
+ ASSERT_EQ(OkStatus(), pigweed.WriteErrorMessage("not a typewriter"));
+ ASSERT_EQ(OkStatus(), pigweed.WriteBin(Pigweed::Protobuf::Binary::ZERO));
{
Pigweed::Pigweed::StreamEncoder pigweed_pigweed =
pigweed.GetPigweedEncoder();
- pigweed_pigweed.WriteStatus(Bool::FILE_NOT_FOUND)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), pigweed_pigweed.WriteStatus(Bool::FILE_NOT_FOUND));
ASSERT_EQ(pigweed_pigweed.status(), OkStatus());
}
{
Proto::StreamEncoder proto = pigweed.GetProtoEncoder();
- proto.WriteBin(Proto::Binary::OFF)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- proto.WritePigweedPigweedBin(Pigweed::Pigweed::Binary::ZERO)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- proto.WritePigweedProtobufBin(Pigweed::Protobuf::Binary::ZERO)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), proto.WriteBin(Proto::Binary::OFF));
+ ASSERT_EQ(OkStatus(),
+ proto.WritePigweedPigweedBin(Pigweed::Pigweed::Binary::ZERO));
+ ASSERT_EQ(OkStatus(),
+ proto.WritePigweedProtobufBin(Pigweed::Protobuf::Binary::ZERO));
{
Pigweed::Protobuf::Compiler::StreamEncoder meta = proto.GetMetaEncoder();
- meta.WriteFileName("/etc/passwd")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- meta.WriteStatus(Pigweed::Protobuf::Compiler::Status::FUBAR)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), meta.WriteFileName("/etc/passwd"));
+ ASSERT_EQ(OkStatus(),
+ meta.WriteStatus(Pigweed::Protobuf::Compiler::Status::FUBAR));
}
{
Pigweed::StreamEncoder nested_pigweed = proto.GetPigweedEncoder();
- nested_pigweed.WriteErrorMessage("here we go again")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- nested_pigweed.WriteMagicNumber(616)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(),
+ nested_pigweed.WriteErrorMessage("here we go again"));
+ ASSERT_EQ(OkStatus(), nested_pigweed.WriteMagicNumber(616));
{
DeviceInfo::StreamEncoder device_info =
@@ -90,31 +101,26 @@ TEST(Codegen, Codegen) {
{
KeyValuePair::StreamEncoder attributes =
device_info.GetAttributesEncoder();
- attributes.WriteKey("version")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- attributes.WriteValue("5.3.1")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), attributes.WriteKey("version"));
+ ASSERT_EQ(OkStatus(), attributes.WriteValue("5.3.1"));
}
{
KeyValuePair::StreamEncoder attributes =
device_info.GetAttributesEncoder();
- attributes.WriteKey("chip")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- attributes.WriteValue("left-soc")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), attributes.WriteKey("chip"));
+ ASSERT_EQ(OkStatus(), attributes.WriteValue("left-soc"));
}
- device_info.WriteStatus(DeviceInfo::DeviceStatus::PANIC)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(),
+ device_info.WriteStatus(DeviceInfo::DeviceStatus::PANIC));
}
}
}
for (int i = 0; i < 5; ++i) {
Proto::ID::StreamEncoder id = pigweed.GetIdEncoder();
- id.WriteId(5 * i * i + 3 * i + 49)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), id.WriteId(5 * i * i + 3 * i + 49));
}
// clang-format off
@@ -200,25 +206,23 @@ TEST(Codegen, Codegen) {
}
TEST(Codegen, RecursiveSubmessage) {
- std::byte encode_buffer[512];
+ // 12 here represents the longest name. Note that all field structure is taken
+ // care of, we just have to multiply by how many crates we're encoding, ie. 4.
+ std::byte encode_buffer[(Crate::kMaxEncodedSizeBytes + 12) * 4];
Crate::MemoryEncoder biggest_crate(encode_buffer);
- biggest_crate.WriteName("Huge crate")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), biggest_crate.WriteName("Huge crate"));
{
Crate::StreamEncoder medium_crate = biggest_crate.GetSmallerCratesEncoder();
- medium_crate.WriteName("Medium crate")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), medium_crate.WriteName("Medium crate"));
{
Crate::StreamEncoder small_crate = medium_crate.GetSmallerCratesEncoder();
- small_crate.WriteName("Small crate")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), small_crate.WriteName("Small crate"));
}
{
Crate::StreamEncoder tiny_crate = medium_crate.GetSmallerCratesEncoder();
- tiny_crate.WriteName("Tiny crate")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), tiny_crate.WriteName("Tiny crate"));
}
}
@@ -249,18 +253,16 @@ TEST(Codegen, RecursiveSubmessage) {
}
TEST(CodegenRepeated, NonPackedScalar) {
- std::byte encode_buffer[32];
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
stream::MemoryWriter writer(encode_buffer);
RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
for (int i = 0; i < 4; ++i) {
- repeated_test.WriteUint32s(i * 16)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), repeated_test.WriteUint32s(i * 16));
}
for (int i = 0; i < 4; ++i) {
- repeated_test.WriteFixed32s(i * 16)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), repeated_test.WriteFixed32s(i * 16));
}
// clang-format off
@@ -286,15 +288,13 @@ TEST(CodegenRepeated, NonPackedScalar) {
}
TEST(CodegenRepeated, PackedScalar) {
- std::byte encode_buffer[32];
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
stream::MemoryWriter writer(encode_buffer);
RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
constexpr uint32_t values[] = {0, 16, 32, 48};
- repeated_test.WriteUint32s(values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- repeated_test.WriteFixed32s(values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), repeated_test.WriteUint32s(values));
+ ASSERT_EQ(OkStatus(), repeated_test.WriteFixed32s(values));
// clang-format off
constexpr uint8_t expected_proto[] = {
@@ -321,13 +321,12 @@ TEST(CodegenRepeated, PackedScalar) {
}
TEST(CodegenRepeated, PackedBool) {
- std::byte encode_buffer[32];
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
stream::MemoryWriter writer(encode_buffer);
RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
constexpr bool values[] = {true, false, true, true, false};
- repeated_test.WriteBools(std::span(values))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), repeated_test.WriteBools(span(values)));
// clang-format off
constexpr uint8_t expected_proto[] = {
@@ -344,15 +343,13 @@ TEST(CodegenRepeated, PackedBool) {
}
TEST(CodegenRepeated, PackedScalarVector) {
- std::byte encode_buffer[32];
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
stream::MemoryWriter writer(encode_buffer);
RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
const pw::Vector<uint32_t, 4> values = {0, 16, 32, 48};
- repeated_test.WriteUint32s(values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- repeated_test.WriteFixed32s(values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), repeated_test.WriteUint32s(values));
+ ASSERT_EQ(OkStatus(), repeated_test.WriteFixed32s(values));
// clang-format off
constexpr uint8_t expected_proto[] = {
@@ -378,15 +375,59 @@ TEST(CodegenRepeated, PackedScalarVector) {
0);
}
+TEST(CodegenRepeated, PackedEnum) {
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
+ constexpr Enum values[] = {Enum::RED, Enum::GREEN, Enum::AMBER, Enum::RED};
+ ASSERT_EQ(repeated_test.WriteEnums(span(values)), OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // enums[], v={RED, GREEN, AMBER, RED}
+ 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ ASSERT_EQ(repeated_test.status(), OkStatus());
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenRepeated, PackedEnumVector) {
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
+ const pw::Vector<Enum, 4> values = {
+ Enum::RED, Enum::GREEN, Enum::AMBER, Enum::RED};
+ ASSERT_EQ(repeated_test.WriteEnums(values), OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // enums[], v={RED, GREEN, AMBER, RED}
+ 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ ASSERT_EQ(repeated_test.status(), OkStatus());
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
TEST(CodegenRepeated, NonScalar) {
- std::byte encode_buffer[32];
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
stream::MemoryWriter writer(encode_buffer);
RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
constexpr const char* strings[] = {"the", "quick", "brown", "fox"};
for (const char* s : strings) {
- repeated_test.WriteStrings(s)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), repeated_test.WriteStrings(s));
}
constexpr uint8_t expected_proto[] = {
@@ -400,15 +441,13 @@ TEST(CodegenRepeated, NonScalar) {
}
TEST(CodegenRepeated, Message) {
- std::byte encode_buffer[64];
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
RepeatedTest::MemoryEncoder repeated_test(encode_buffer);
for (int i = 0; i < 3; ++i) {
auto structs = repeated_test.GetStructsEncoder();
- structs.WriteOne(i * 1)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- structs.WriteTwo(i * 2)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), structs.WriteOne(i * 1));
+ ASSERT_EQ(OkStatus(), structs.WriteTwo(i * 2));
}
// clang-format off
@@ -425,17 +464,16 @@ TEST(CodegenRepeated, Message) {
}
TEST(Codegen, Proto2) {
- std::byte encode_buffer[64];
+ std::byte encode_buffer[Foo::kMaxEncodedSizeBytes];
Foo::MemoryEncoder foo(encode_buffer);
- foo.WriteInteger(3).IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), foo.WriteInteger(3));
{
constexpr std::byte data[] = {
std::byte(0xde), std::byte(0xad), std::byte(0xbe), std::byte(0xef)};
Bar::StreamEncoder bar = foo.GetBarEncoder();
- bar.WriteData(data)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), bar.WriteData(data));
}
constexpr uint8_t expected_proto[] = {
@@ -449,41 +487,90 @@ TEST(Codegen, Proto2) {
}
TEST(Codegen, Import) {
- std::byte encode_buffer[64];
+ std::byte encode_buffer[Period::kMaxEncodedSizeBytes];
Period::MemoryEncoder period(encode_buffer);
{
imported::Timestamp::StreamEncoder start = period.GetStartEncoder();
- start.WriteSeconds(1589501793)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- start.WriteNanoseconds(511613110)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), start.WriteSeconds(1589501793));
+ ASSERT_EQ(OkStatus(), start.WriteNanoseconds(511613110));
}
{
imported::Timestamp::StreamEncoder end = period.GetEndEncoder();
- end.WriteSeconds(1589501841)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- end.WriteNanoseconds(490367432)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), end.WriteSeconds(1589501841));
+ ASSERT_EQ(OkStatus(), end.WriteNanoseconds(490367432));
}
EXPECT_EQ(period.status(), OkStatus());
}
TEST(Codegen, NonPigweedPackage) {
- using namespace non::pigweed::package::name;
- std::byte encode_buffer[64];
+ namespace Packed = ::non::pigweed::package::name::pwpb::Packed;
+
+ std::byte encode_buffer[Packed::kMaxEncodedSizeBytes];
std::array<const int64_t, 2> repeated = {0, 1};
stream::MemoryWriter writer(encode_buffer);
Packed::StreamEncoder packed(writer, ByteSpan());
- packed.WriteRep(std::span<const int64_t>(repeated))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- packed.WritePacked("packed")
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), packed.WriteRep(span<const int64_t>(repeated)));
+ ASSERT_EQ(OkStatus(), packed.WritePacked("packed"));
EXPECT_EQ(packed.status(), OkStatus());
}
+TEST(Codegen, MemoryToStreamConversion) {
+ std::byte encode_buffer[IntegerMetadata::kMaxEncodedSizeBytes];
+ IntegerMetadata::MemoryEncoder metadata(encode_buffer);
+ IntegerMetadata::StreamEncoder& streamed_metadata = metadata;
+ EXPECT_EQ(streamed_metadata.WriteBits(3), OkStatus());
+
+ constexpr uint8_t expected_proto[] = {0x08, 0x03};
+
+ ConstByteSpan result(metadata);
+ ASSERT_EQ(metadata.status(), OkStatus());
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(Codegen, OverlayConversion) {
+ std::byte encode_buffer[BaseMessage::kMaxEncodedSizeBytes +
+ Overlay::kMaxEncodedSizeBytes];
+ BaseMessage::MemoryEncoder base(encode_buffer);
+ Overlay::StreamEncoder& overlay =
+ StreamEncoderCast<Overlay::StreamEncoder>(base);
+ EXPECT_EQ(overlay.WriteHeight(15), OkStatus());
+ EXPECT_EQ(base.WriteLength(7), OkStatus());
+
+ constexpr uint8_t expected_proto[] = {0x10, 0x0f, 0x08, 0x07};
+
+ ConstByteSpan result(base);
+ ASSERT_EQ(base.status(), OkStatus());
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(Codegen, EnumToString) {
+ EXPECT_STREQ(test::pwpb::BoolToString(test::pwpb::Bool::kTrue), "TRUE");
+ EXPECT_STREQ(test::pwpb::BoolToString(test::pwpb::Bool::kFalse), "FALSE");
+ EXPECT_STREQ(test::pwpb::BoolToString(test::pwpb::Bool::kFileNotFound),
+ "FILE_NOT_FOUND");
+ EXPECT_STREQ(test::pwpb::BoolToString(static_cast<test::pwpb::Bool>(12893)),
+ "");
+}
+
+TEST(Codegen, NestedEnumToString) {
+ EXPECT_STREQ(test::pwpb::Pigweed::Pigweed::BinaryToString(
+ test::pwpb::Pigweed::Pigweed::Binary::kZero),
+ "ZERO");
+ EXPECT_STREQ(test::pwpb::Pigweed::Pigweed::BinaryToString(
+ test::pwpb::Pigweed::Pigweed::Binary::kOne),
+ "ONE");
+ EXPECT_STREQ(test::pwpb::Pigweed::Pigweed::BinaryToString(
+ static_cast<test::pwpb::Pigweed::Pigweed::Binary>(12893)),
+ "");
+}
+
} // namespace
} // namespace pw::protobuf
diff --git a/pw_protobuf/codegen_message_test.cc b/pw_protobuf/codegen_message_test.cc
new file mode 100644
index 000000000..f049715ea
--- /dev/null
+++ b/pw_protobuf/codegen_message_test.cc
@@ -0,0 +1,2027 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 <array>
+#include <string_view>
+#include <tuple>
+
+#include "gtest/gtest.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_protobuf/internal/codegen.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_stream/memory_stream.h"
+
+// These header files contain the code generated by the pw_protobuf plugin.
+// They are re-generated every time the tests are built and are used by the
+// tests to ensure that the interface remains consistent.
+//
+// The purpose of the tests in this file is primarily to verify that the
+// generated C++ interface is valid rather than the correctness of the
+// low-level encoder.
+#include "pw_protobuf_test_protos/full_test.pwpb.h"
+#include "pw_protobuf_test_protos/importer.pwpb.h"
+#include "pw_protobuf_test_protos/optional.pwpb.h"
+#include "pw_protobuf_test_protos/repeated.pwpb.h"
+
+namespace pw::protobuf {
+namespace {
+
+using namespace ::pw::protobuf::test::pwpb;
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
+
+TEST(CodegenMessage, Equality) {
+ const Pigweed::Message one{
+ .magic_number = 0x49u,
+ .ziggy = -111,
+ .cycles = 0x40302010fecaaddeu,
+ .ratio = -1.42f,
+ .error_message = "not a typewriter",
+ .pigweed = {.status = Bool::FILE_NOT_FOUND},
+ .bin = Pigweed::Protobuf::Binary::ZERO,
+ .proto = {.bin = Proto::Binary::OFF,
+ .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO,
+ .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO,
+ .meta =
+ {
+ .file_name = "/etc/passwd",
+ .status = Pigweed::Protobuf::Compiler::Status::FUBAR,
+ .protobuf_bin = Pigweed::Protobuf::Binary::ONE,
+ .pigweed_bin = Pigweed::Pigweed::Binary::ONE,
+ }},
+ .data = {std::byte{0x10},
+ std::byte{0x20},
+ std::byte{0x30},
+ std::byte{0x40},
+ std::byte{0x50},
+ std::byte{0x60},
+ std::byte{0x70},
+ std::byte{0x80}},
+ .bungle = -111,
+ };
+
+ const Pigweed::Message two{
+ .magic_number = 0x49u,
+ .ziggy = -111,
+ .cycles = 0x40302010fecaaddeu,
+ .ratio = -1.42f,
+ .error_message = "not a typewriter",
+ .pigweed = {.status = Bool::FILE_NOT_FOUND},
+ .bin = Pigweed::Protobuf::Binary::ZERO,
+ .proto = {.bin = Proto::Binary::OFF,
+ .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO,
+ .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO,
+ .meta =
+ {
+ .file_name = "/etc/passwd",
+ .status = Pigweed::Protobuf::Compiler::Status::FUBAR,
+ .protobuf_bin = Pigweed::Protobuf::Binary::ONE,
+ .pigweed_bin = Pigweed::Pigweed::Binary::ONE,
+ }},
+ .data = {std::byte{0x10},
+ std::byte{0x20},
+ std::byte{0x30},
+ std::byte{0x40},
+ std::byte{0x50},
+ std::byte{0x60},
+ std::byte{0x70},
+ std::byte{0x80}},
+ .bungle = -111,
+ };
+
+ EXPECT_TRUE(one == two);
+}
+
+TEST(CodegenMessage, CopyEquality) {
+ Pigweed::Message one{
+ .magic_number = 0x49u,
+ .ziggy = -111,
+ .cycles = 0x40302010fecaaddeu,
+ .ratio = -1.42f,
+ .error_message = "not a typewriter",
+ .pigweed = {.status = Bool::FILE_NOT_FOUND},
+ .bin = Pigweed::Protobuf::Binary::ZERO,
+ .proto = {.bin = Proto::Binary::OFF,
+ .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO,
+ .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO,
+ .meta =
+ {
+ .file_name = "/etc/passwd",
+ .status = Pigweed::Protobuf::Compiler::Status::FUBAR,
+ .protobuf_bin = Pigweed::Protobuf::Binary::ONE,
+ .pigweed_bin = Pigweed::Pigweed::Binary::ONE,
+ }},
+ .data = {std::byte{0x10},
+ std::byte{0x20},
+ std::byte{0x30},
+ std::byte{0x40},
+ std::byte{0x50},
+ std::byte{0x60},
+ std::byte{0x70},
+ std::byte{0x80}},
+ .bungle = -111,
+ };
+ Pigweed::Message two = one;
+
+ EXPECT_TRUE(one == two);
+}
+
+TEST(CodegenMessage, EmptyEquality) {
+ const Pigweed::Message one{};
+ const Pigweed::Message two{};
+
+ EXPECT_TRUE(one == two);
+}
+
+TEST(CodegenMessage, Inequality) {
+ const Pigweed::Message one{
+ .magic_number = 0x49u,
+ .ziggy = -111,
+ .cycles = 0x40302010fecaaddeu,
+ .ratio = -1.42f,
+ .error_message = "not a typewriter",
+ .pigweed = {.status = Bool::FILE_NOT_FOUND},
+ .bin = Pigweed::Protobuf::Binary::ZERO,
+ .proto = {.bin = Proto::Binary::OFF,
+ .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO,
+ .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO,
+ .meta =
+ {
+ .file_name = "/etc/passwd",
+ .status = Pigweed::Protobuf::Compiler::Status::FUBAR,
+ .protobuf_bin = Pigweed::Protobuf::Binary::ONE,
+ .pigweed_bin = Pigweed::Pigweed::Binary::ONE,
+ }},
+ .data = {std::byte{0x10},
+ std::byte{0x20},
+ std::byte{0x30},
+ std::byte{0x40},
+ std::byte{0x50},
+ std::byte{0x60},
+ std::byte{0x70},
+ std::byte{0x80}},
+ .bungle = -111,
+ };
+
+ const Pigweed::Message two{
+ .magic_number = 0x43u,
+ .ziggy = 128,
+ .ratio = -1.42f,
+ .error_message = "not a typewriter",
+ .pigweed = {.status = Bool::TRUE},
+ .bin = Pigweed::Protobuf::Binary::ZERO,
+ .proto = {.bin = Proto::Binary::OFF,
+ .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO,
+ .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ONE,
+ .meta =
+ {
+ .file_name = "/etc/passwd",
+ .status = Pigweed::Protobuf::Compiler::Status::FUBAR,
+ .protobuf_bin = Pigweed::Protobuf::Binary::ONE,
+ .pigweed_bin = Pigweed::Pigweed::Binary::ONE,
+ }},
+ .data = {std::byte{0x20},
+ std::byte{0x30},
+ std::byte{0x40},
+ std::byte{0x50},
+ std::byte{0x60},
+ std::byte{0x70},
+ std::byte{0x80},
+ std::byte{0x90}},
+ };
+
+ EXPECT_FALSE(one == two);
+}
+
+TEST(CodegenMessage, TriviallyComparable) {
+ static_assert(IsTriviallyComparable<IntegerMetadata::Message>());
+ static_assert(IsTriviallyComparable<KeyValuePair::Message>());
+ static_assert(!IsTriviallyComparable<Pigweed::Message>());
+}
+
+TEST(CodegenMessage, ConstCopyable) {
+ const Pigweed::Message one{
+ .magic_number = 0x49u,
+ .ziggy = -111,
+ .cycles = 0x40302010fecaaddeu,
+ .ratio = -1.42f,
+ .error_message = "not a typewriter",
+ .pigweed = {.status = Bool::FILE_NOT_FOUND},
+ .bin = Pigweed::Protobuf::Binary::ZERO,
+ .proto = {.bin = Proto::Binary::OFF,
+ .pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO,
+ .pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO,
+ .meta =
+ {
+ .file_name = "/etc/passwd",
+ .status = Pigweed::Protobuf::Compiler::Status::FUBAR,
+ .protobuf_bin = Pigweed::Protobuf::Binary::ONE,
+ .pigweed_bin = Pigweed::Pigweed::Binary::ONE,
+ }},
+ .data = {std::byte{0x10},
+ std::byte{0x20},
+ std::byte{0x30},
+ std::byte{0x40},
+ std::byte{0x50},
+ std::byte{0x60},
+ std::byte{0x70},
+ std::byte{0x80}},
+ .bungle = -111,
+ };
+ Pigweed::Message two = one;
+
+ EXPECT_TRUE(one == two);
+}
+
+TEST(CodegenMessage, FixReservedIdentifiers) {
+ // This test checks that the code was generated as expected, so it will simply
+ // fail to compile if its expectations are not met.
+
+ // Make sure that the `signed` field was renamed to `signed_`.
+ std::ignore = IntegerMetadata::Message{
+ .bits = 32,
+ .signed_ = true,
+ .null = false,
+ };
+
+ // Make sure that the internal enum describing the struct's fields was
+ // generated as expected:
+ // - `BITS` doesn't need an underscore.
+ // - `SIGNED_` has an underscore to match the corresponding `signed_` field.
+ // - `NULL_` has an underscore to avoid a collision with `NULL` (even though
+ // the field `null` doesn't have or need an underscore).
+ std::ignore = IntegerMetadata::Fields::kBits;
+ std::ignore = IntegerMetadata::Fields::kSigned;
+ std::ignore = IntegerMetadata::Fields::kNull;
+
+ // Make sure that the `ReservedWord` enum values were renamed as expected.
+ // Specifically, only enum-value names that are reserved in UPPER_SNAKE_CASE
+ // should be modified. Names that are only reserved in lower_snake_case should
+ // be left alone since they'll never appear in that form in the generated
+ // code.
+ std::ignore = ReservedWord::NULL_; // Add underscore since NULL is a macro.
+ std::ignore = ReservedWord::kNull; // No underscore necessary.
+ std::ignore = ReservedWord::INT; // No underscore necessary.
+ std::ignore = ReservedWord::kInt; // No underscore necessary.
+ std::ignore = ReservedWord::RETURN; // No underscore necessary.
+ std::ignore = ReservedWord::kReturn; // No underscore necessary.
+ std::ignore = ReservedWord::BREAK; // No underscore necessary.
+ std::ignore = ReservedWord::kBreak; // No underscore necessary.
+ std::ignore = ReservedWord::FOR; // No underscore necessary.
+ std::ignore = ReservedWord::kFor; // No underscore necessary.
+ std::ignore = ReservedWord::DO; // No underscore necessary.
+ std::ignore = ReservedWord::kDo; // No underscore necessary.
+
+ // Instantiate an extremely degenerately named set of nested types in order to
+ // make sure that name conflicts with the codegen internals are properly
+ // prevented.
+ std::ignore = Function::Message{
+ .description =
+ Function::Message_::Message{
+ .content = "multiplication (mod 5)",
+ },
+ .domain_field = Function::Fields_::INTEGERS_MOD_5,
+ .codomain_field = Function::Fields_::INTEGERS_MOD_5,
+ };
+
+ // Check for expected values of `enum class Function::Fields`:
+ std::ignore = Function::Fields::kDescription;
+ std::ignore = Function::Fields::kDomainField;
+ std::ignore = Function::Fields::kCodomainField;
+
+ // Check for expected values of `enum class Function::Message_::Fields`:
+ std::ignore = Function::Message_::Fields::kContent;
+
+ // Check for expected values of `enum class Function::Fields_`:
+ std::ignore = Function::Fields_::NONE;
+ std::ignore = Function::Fields_::kNone;
+ std::ignore = Function::Fields_::COMPLEX_NUMBERS;
+ std::ignore = Function::Fields_::kComplexNumbers;
+ std::ignore = Function::Fields_::INTEGERS_MOD_5;
+ std::ignore = Function::Fields_::kIntegersMod5;
+ std::ignore = Function::Fields_::MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE;
+ std::ignore = Function::Fields_::kMeromorphicFunctionsOnComplexPlane;
+ std::ignore = Function::Fields_::OTHER;
+ std::ignore = Function::Fields_::kOther;
+}
+
+PW_MODIFY_DIAGNOSTICS_POP();
+
+TEST(CodegenMessage, Read) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.magic_number
+ 0x08, 0x49,
+ // pigweed.ziggy
+ 0x10, 0xdd, 0x01,
+ // pigweed.cycles
+ 0x19, 0xde, 0xad, 0xca, 0xfe, 0x10, 0x20, 0x30, 0x40,
+ // pigweed.ratio
+ 0x25, 0x8f, 0xc2, 0xb5, 0xbf,
+ // pigweed.error_message
+ 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ',
+ 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r',
+ // pigweed.bin
+ 0x40, 0x01,
+ // pigweed.bungle
+ 0x70, 0x91, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ Pigweed::Message message{};
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ constexpr std::string_view kExpectedErrorMessage{"not a typewriter"};
+
+ EXPECT_EQ(message.magic_number, 0x49u);
+ EXPECT_EQ(message.ziggy, -111);
+ EXPECT_EQ(message.cycles, 0x40302010fecaaddeu);
+ EXPECT_EQ(message.ratio, -1.42f);
+ EXPECT_EQ(message.error_message.size(), kExpectedErrorMessage.size());
+ EXPECT_EQ(std::memcmp(message.error_message.data(),
+ kExpectedErrorMessage.data(),
+ kExpectedErrorMessage.size()),
+ 0);
+ EXPECT_EQ(message.bin, Pigweed::Protobuf::Binary::ZERO);
+ EXPECT_EQ(message.bungle, -111);
+}
+
+TEST(CodegenMessage, ReadNonPackedScalar) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // uint32s[], v={0, 16, 32, 48}
+ 0x08, 0x00,
+ 0x08, 0x10,
+ 0x08, 0x20,
+ 0x08, 0x30,
+ // fixed32s[]. v={0, 16, 32, 48}
+ 0x35, 0x00, 0x00, 0x00, 0x00,
+ 0x35, 0x10, 0x00, 0x00, 0x00,
+ 0x35, 0x20, 0x00, 0x00, 0x00,
+ 0x35, 0x30, 0x00, 0x00, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ ASSERT_EQ(message.uint32s.size(), 4u);
+ for (int i = 0; i < 4; ++i) {
+ EXPECT_EQ(message.uint32s[i], i * 16u);
+ }
+
+ ASSERT_EQ(message.fixed32s.size(), 4u);
+ for (int i = 0; i < 4; ++i) {
+ EXPECT_EQ(message.fixed32s[i], i * 16u);
+ }
+}
+
+TEST(CodegenMessage, ReadPackedScalar) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // uint32s[], v={0, 16, 32, 48}
+ 0x0a, 0x04,
+ 0x00,
+ 0x10,
+ 0x20,
+ 0x30,
+ // fixed32s[]. v={0, 16, 32, 48}
+ 0x32, 0x10,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x10, 0x00, 0x00, 0x00,
+ 0x20, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ ASSERT_EQ(message.uint32s.size(), 4u);
+ for (int i = 0; i < 4; ++i) {
+ EXPECT_EQ(message.uint32s[i], i * 16u);
+ }
+
+ ASSERT_EQ(message.fixed32s.size(), 4u);
+ for (int i = 0; i < 4; ++i) {
+ EXPECT_EQ(message.fixed32s[i], i * 16u);
+ }
+}
+
+TEST(CodegenMessage, ReadPackedScalarRepeated) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // uint32s[], v={0, 16, 32, 48}
+ 0x0a, 0x04,
+ 0x00,
+ 0x10,
+ 0x20,
+ 0x30,
+ // uint32s[], v={64, 80, 96, 112}
+ 0x0a, 0x04,
+ 0x40,
+ 0x50,
+ 0x60,
+ 0x70,
+ // fixed32s[]. v={0, 16, 32, 48}
+ 0x32, 0x10,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x10, 0x00, 0x00, 0x00,
+ 0x20, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00,
+ // fixed32s[]. v={64, 80, 96, 112}
+ 0x32, 0x10,
+ 0x40, 0x00, 0x00, 0x00,
+ 0x50, 0x00, 0x00, 0x00,
+ 0x60, 0x00, 0x00, 0x00,
+ 0x70, 0x00, 0x00, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ ASSERT_EQ(message.uint32s.size(), 8u);
+ for (int i = 0; i < 8; ++i) {
+ EXPECT_EQ(message.uint32s[i], i * 16u);
+ }
+
+ ASSERT_EQ(message.fixed32s.size(), 8u);
+ for (int i = 0; i < 8; ++i) {
+ EXPECT_EQ(message.fixed32s[i], i * 16u);
+ }
+}
+
+TEST(CodegenMessage, ReadPackedScalarExhausted) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // uint32s[], v={0, 16, 32, 48, 64, 80, 96, 112, 128}
+ 0x0a, 0x09,
+ 0x00,
+ 0x10,
+ 0x20,
+ 0x30,
+ 0x40,
+ 0x50,
+ 0x60,
+ 0x70,
+ 0x80,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ // uint32s has max_size=8, so this will exhaust the vector.
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, Status::ResourceExhausted());
+}
+
+TEST(CodegenMessage, ReadPackedScalarCallback) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // sint32s[], v={-25, -1, 0, 1, 25}
+ 0x12, 0x05,
+ 0x31,
+ 0x01,
+ 0x00,
+ 0x02,
+ 0x32,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ // sint32s is a repeated field declared without max_count, so requirses a
+ // callback to be decoded.
+ RepeatedTest::Message message{};
+ message.sint32s.SetDecoder([](RepeatedTest::StreamDecoder& decoder) {
+ EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kSint32s);
+
+ pw::Vector<int32_t, 8> sint32s{};
+ const auto status = decoder.ReadSint32s(sint32s);
+ EXPECT_EQ(status, OkStatus());
+
+ EXPECT_EQ(sint32s.size(), 5u);
+ EXPECT_EQ(sint32s[0], -25);
+ EXPECT_EQ(sint32s[1], -1);
+ EXPECT_EQ(sint32s[2], 0);
+ EXPECT_EQ(sint32s[3], 1);
+ EXPECT_EQ(sint32s[4], 25);
+
+ return status;
+ });
+
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+}
+
+TEST(CodegenMessage, ReadPackedScalarFixedLength) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // uint64s[], v={1000, 2000, 3000, 4000}
+ 0x42, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f,
+ // doubles[], v={3.14159, 2.71828}
+ 0x22, 0x10,
+ 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40,
+ 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ EXPECT_EQ(message.uint64s[0], 1000u);
+ EXPECT_EQ(message.uint64s[1], 2000u);
+ EXPECT_EQ(message.uint64s[2], 3000u);
+ EXPECT_EQ(message.uint64s[3], 4000u);
+
+ EXPECT_EQ(message.doubles[0], 3.14159);
+ EXPECT_EQ(message.doubles[1], 2.71828);
+}
+
+TEST(CodegenMessage, ReadPackedScalarFixedLengthShort) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // uint64s[], v={1000, 2000}
+ 0x42, 0x04, 0xe8, 0x07, 0xd0, 0x0f,
+ // doubles[], v={3.14159}
+ 0x22, 0x08,
+ 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ EXPECT_EQ(message.uint64s[0], 1000u);
+ EXPECT_EQ(message.uint64s[1], 2000u);
+ EXPECT_EQ(message.uint64s[2], 0u);
+ EXPECT_EQ(message.uint64s[3], 0u);
+
+ EXPECT_EQ(message.doubles[0], 3.14159);
+ EXPECT_EQ(message.doubles[1], 0);
+}
+
+TEST(CodegenMessage, ReadPackedScalarVarintFixedLengthExhausted) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // uint64s[], v={0, 1000, 2000, 3000, 4000}
+ 0x42, 0x09, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, Status::ResourceExhausted());
+}
+
+TEST(CodegenMessage, ReadPackedScalarFixedLengthExhausted) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // doubles[], v={3.14159, 2.71828, 1.41429, 1.73205}
+ 0x22, 0x20,
+ 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40,
+ 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40,
+ 0x1b, 0xf5, 0x10, 0x8d, 0xee, 0xa0, 0xf6, 0x3f,
+ 0xbc, 0x96, 0x90, 0x0f, 0x7a, 0xb6, 0xfb, 0x3f,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, Status::ResourceExhausted());
+}
+
+TEST(CodegenMessage, ReadPackedEnum) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // enums[], v={RED, GREEN, AMBER, RED}
+ 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ ASSERT_EQ(message.enums.size(), 4u);
+ for (int i = 0; i < 4; ++i) {
+ EXPECT_TRUE(IsValidEnum(message.enums[i]));
+ }
+
+ EXPECT_EQ(message.enums[0], Enum::RED);
+ EXPECT_EQ(message.enums[1], Enum::GREEN);
+ EXPECT_EQ(message.enums[2], Enum::AMBER);
+ EXPECT_EQ(message.enums[3], Enum::RED);
+}
+
+TEST(CodegenMessage, ReadStringExhausted) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.error_message
+ 0x2a, 0xd3, 0x01, 'T', 'h', 'i', 's', ' ', 'l', 'a', 'b', 'e', 'l', ' ', 'i',
+ 's', ' ', 't', 'h', 'e', ' ', 't', 'a', 'r', 'g', 'e', 't', ' ', 'o', 'f',
+ ' ', 'a', ' ', 'g', 'o', 't', 'o', ' ', 'f', 'r', 'o', 'm', ' ', 'o', 'u',
+ 't', 's', 'i', 'd', 'e', ' ', 'o', 'f', ' ', 't', 'h', 'e', ' ', 'b', 'l',
+ 'o', 'c', 'k', ' ', 'c', 'o', 'n', 't', 'a', 'i', 'n', 'i', 'n', 'g', ' ',
+ 't', 'h', 'i', 's', ' ', 'l', 'a', 'b', 'e', 'l', ' ', 'A', 'N', 'D', ' ',
+ 't', 'h', 'i', 's', ' ', 'b', 'l', 'o', 'c', 'k', ' ', 'h', 'a', 's', ' ',
+ 'a', 'n', ' ', 'a', 'u', 't', 'o', 'm', 'a', 't', 'i', 'c', ' ', 'v', 'a',
+ 'r', 'i', 'a', 'b', 'l', 'e', ' ', 'w', 'i', 't', 'h', ' ', 'a', 'n', ' ',
+ 'i', 'n', 'i', 't', 'i', 'a', 'l', 'i', 'z', 'e', 'r', ' ', 'A', 'N', 'D',
+ ' ', 'y', 'o', 'u', 'r', ' ', 'w', 'i', 'n', 'd', 'o', 'w', ' ', 'w', 'a',
+ 's', 'n', '\'', 't', ' ', 'w', 'i', 'd', 'e', ' ', 'e', 'n', 'o', 'u', 'g',
+ 'h', ' ', 't', 'o', ' ', 'r', 'e', 'a', 'd', ' ', 't', 'h', 'i', 's', ' ',
+ 'w', 'h', 'o', 'l', 'e', ' ', 'e', 'r', 'r', 'o', 'r', ' ', 'm', 'e', 's',
+ 's', 'a', 'g', 'e'
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ Pigweed::Message message{};
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, Status::ResourceExhausted());
+}
+
+TEST(CodegenMessage, ReadStringCallback) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.description
+ 0x62, 0x5c, 'a', 'n', ' ', 'o', 'p', 'e', 'n', ' ', 's', 'o', 'u', 'r', 'c',
+ 'e', ' ', 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'o', 'f',
+ ' ', 'e', 'm', 'b', 'e', 'd', 'd', 'e', 'd', '-', 't', 'a', 'r', 'g', 'e',
+ 't', 'e', 'd', ' ', 'l', 'i', 'b', 'r', 'a', 'r', 'i', 'e', 's', '-', 'o',
+ 'r', ' ', 'a', 's', ' ', 'w', 'e', ' ', 'l', 'i', 'k', 'e', ' ', 't', 'o',
+ ' ', 'c', 'a', 'l', 'l', ' ', 't', 'h', 'e', 'm', ',', ' ', 'm', 'o', 'd',
+ 'u', 'l', 'e', 's'
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ // pigweed.description has no max_size specified so a callback must be
+ // set to read the value if present.
+ Pigweed::Message message{};
+ message.description.SetDecoder([](Pigweed::StreamDecoder& decoder) {
+ EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kDescription);
+
+ constexpr std::string_view kExpectedDescription{
+ "an open source collection of embedded-targeted libraries-or as we "
+ "like to call them, modules"};
+
+ std::array<char, 128> description{};
+ const auto sws = decoder.ReadDescription(description);
+ EXPECT_EQ(sws.status(), OkStatus());
+ EXPECT_EQ(sws.size(), kExpectedDescription.size());
+ EXPECT_EQ(std::memcmp(description.data(),
+ kExpectedDescription.data(),
+ kExpectedDescription.size()),
+ 0);
+
+ return sws.status();
+ });
+
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, OkStatus());
+}
+
+TEST(CodegenMessage, ReadMultipleString) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.error_message
+ 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ',
+ 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r',
+ // pigweed.error_message
+ 0x02a, 0x07, 'o', 'n', ' ', 'f', 'i', 'r', 'e'
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ Pigweed::Message message{};
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ constexpr std::string_view kExpectedErrorMessage{"on fire"};
+
+ EXPECT_EQ(message.error_message.size(), kExpectedErrorMessage.size());
+ EXPECT_EQ(std::memcmp(message.error_message.data(),
+ kExpectedErrorMessage.data(),
+ kExpectedErrorMessage.size()),
+ 0);
+}
+
+TEST(CodegenMessage, ReadRepeatedStrings) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // repeated.strings
+ 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ',
+ 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ',
+ 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n',
+ // repeated.strings
+ 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e',
+ 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',',
+ ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g',
+ // repeated.strings
+ 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ',
+ 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd',
+ ' ', 's', 'o', ' ', 'd', 'i', 'e',
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ // Repeated strings require a callback to avoid forcing multi-dimensional
+ // arrays upon the caller.
+ RepeatedTest::Message message{};
+ int i = 0;
+ message.strings.SetDecoder([&i](RepeatedTest::StreamDecoder& decoder) {
+ EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStrings);
+
+ constexpr std::string_view kExpectedStrings[] = {
+ {"if music be the food of love, play on"},
+ {"give me excess of it, that, surfeiting"},
+ {"the appetite may sicken, and so die"}};
+
+ std::array<char, 40> strings{};
+ const StatusWithSize sws = decoder.ReadStrings(strings);
+ EXPECT_EQ(sws.status(), OkStatus());
+ EXPECT_EQ(sws.size(), kExpectedStrings[i].size());
+ EXPECT_EQ(std::memcmp(strings.data(),
+ kExpectedStrings[i].data(),
+ kExpectedStrings[i].size()),
+ 0);
+
+ ++i;
+ return sws.status();
+ });
+
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+}
+
+TEST(CodegenMessage, ReadForcedCallback) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.special_property
+ 0x68, 0x2a,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ // pigweed.special_property has use_callback=true to force the use of a
+ // callback even though it's a simple scalar.
+ Pigweed::Message message{};
+ message.special_property.SetDecoder([](Pigweed::StreamDecoder& decoder) {
+ EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kSpecialProperty);
+
+ pw::Result<uint32_t> result = decoder.ReadSpecialProperty();
+ EXPECT_EQ(result.status(), OkStatus());
+ EXPECT_EQ(result.value(), 42u);
+
+ return result.status();
+ });
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, OkStatus());
+}
+
+TEST(CodegenMessage, ReadMissingCallback) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // repeated.strings
+ 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ',
+ 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ',
+ 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n',
+ // repeated.strings
+ 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e',
+ 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',',
+ ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g',
+ // repeated.strings
+ 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ',
+ 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd',
+ ' ', 's', 'o', ' ', 'd', 'i', 'e',
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ // Failing to set a callback will give a DataLoss error if that field is
+ // present in the decoded data.
+ RepeatedTest::Message message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, Status::DataLoss());
+}
+
+TEST(CodegenMessage, ReadFixedLength) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.data
+ 0x5a, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ Pigweed::Message message{};
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ EXPECT_EQ(message.data[0], std::byte{0x01});
+ EXPECT_EQ(message.data[1], std::byte{0x02});
+ EXPECT_EQ(message.data[2], std::byte{0x03});
+ EXPECT_EQ(message.data[3], std::byte{0x04});
+ EXPECT_EQ(message.data[4], std::byte{0x05});
+ EXPECT_EQ(message.data[5], std::byte{0x06});
+ EXPECT_EQ(message.data[6], std::byte{0x07});
+ EXPECT_EQ(message.data[7], std::byte{0x08});
+}
+
+TEST(CodegenMessage, ReadFixedLengthShort) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.data
+ 0x5a, 0x04, 0x01, 0x02, 0x03, 0x04
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ Pigweed::Message message{};
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ EXPECT_EQ(message.data[0], std::byte{0x01});
+ EXPECT_EQ(message.data[1], std::byte{0x02});
+ EXPECT_EQ(message.data[2], std::byte{0x03});
+ EXPECT_EQ(message.data[3], std::byte{0x04});
+ // Remaining bytes are whatever you initialized them to.
+ EXPECT_EQ(message.data[4], std::byte{0x00});
+ EXPECT_EQ(message.data[5], std::byte{0x00});
+ EXPECT_EQ(message.data[6], std::byte{0x00});
+ EXPECT_EQ(message.data[7], std::byte{0x00});
+}
+
+TEST(CodegenMessage, ReadFixedLengthExhausted) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.data
+ 0x5a, 0x0c, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0a, 0x0b, 0x0c
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ Pigweed::Message message{};
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, Status::ResourceExhausted());
+}
+
+TEST(CodegenMessage, ReadNested) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.magic_number
+ 0x08, 0x49,
+ // pigweed.pigweed
+ 0x3a, 0x02,
+ // pigweed.pigweed.status
+ 0x08, 0x02,
+ // pigweed.ziggy
+ 0x10, 0xdd, 0x01,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ Pigweed::Message message{};
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ EXPECT_EQ(message.magic_number, 0x49u);
+ EXPECT_EQ(message.pigweed.status, Bool::FILE_NOT_FOUND);
+ EXPECT_EQ(message.ziggy, -111);
+}
+
+TEST(CodegenMessage, ReadNestedImported) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // period.start
+ 0x0a, 0x08,
+ // period.start.seconds v=1517949900
+ 0x08, 0xcc, 0xa7, 0xe8, 0xd3, 0x05,
+ // period.start.nanoseconds v=0
+ 0x10, 0x00,
+ // period.end
+ 0x12, 0x08,
+ // period.end.seconds, v=1517950378
+ 0x08, 0xaa, 0xab, 0xe8, 0xd3, 0x05,
+ // period.end.nanoseconds, v=0
+ 0x10, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Period::StreamDecoder period(reader);
+
+ // Messages imported from another file can be directly embedded in a message.
+ Period::Message message{};
+ const auto status = period.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ EXPECT_EQ(message.start.seconds, 1517949900u);
+ EXPECT_EQ(message.start.nanoseconds, 0u);
+ EXPECT_EQ(message.end.seconds, 1517950378u);
+ EXPECT_EQ(message.end.nanoseconds, 0u);
+}
+
+TEST(CodegenMessage, ReadNestedRepeated) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // repeated.structs
+ 0x2a, 0x04,
+ // repeated.structs.one v=16
+ 0x08, 0x10,
+ // repeated.structs.two v=32
+ 0x10, 0x20,
+ // repeated.structs
+ 0x2a, 0x04,
+ // repeated.structs.one v=48
+ 0x08, 0x30,
+ // repeated.structs.two v=64
+ 0x10, 0x40,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ // Repeated nested messages require a callback since there would otherwise be
+ // no way to set callbacks on the nested message.
+ RepeatedTest::Message message{};
+ int i = 0;
+ message.structs.SetDecoder([&i](RepeatedTest::StreamDecoder& decoder) {
+ EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStructs);
+
+ Struct::Message structs_message{};
+ auto structs_decoder = decoder.GetStructsDecoder();
+ const auto status = structs_decoder.Read(structs_message);
+ EXPECT_EQ(status, OkStatus());
+
+ EXPECT_LT(i, 2);
+ EXPECT_EQ(structs_message.one, i * 32 + 16u);
+ EXPECT_EQ(structs_message.two, i * 32 + 32u);
+ ++i;
+
+ return status;
+ });
+
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+}
+
+TEST(CodegenMessage, ReadNestedForcedCallback) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // pigweed.device_info
+ 0x32, 0x0e,
+ // pigweed.device_info.device_name
+ 0x0a, 0x05, 'p', 'i', 'x', 'e', 'l',
+ // pigweed.device_info.device_id
+ 0x15, 0x08, 0x08, 0x08, 0x08,
+ // pigweed.device_info.status
+ 0x18, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ Pigweed::StreamDecoder pigweed(reader);
+
+ // pigweed.device_info has use_callback=true to force the use of a callback.
+ Pigweed::Message message{};
+ message.device_info.SetDecoder([](Pigweed::StreamDecoder& decoder) {
+ EXPECT_EQ(decoder.Field().value(), Pigweed::Fields::kDeviceInfo);
+
+ DeviceInfo::Message device_info{};
+ DeviceInfo::StreamDecoder device_info_decoder =
+ decoder.GetDeviceInfoDecoder();
+ const auto status = device_info_decoder.Read(device_info);
+ EXPECT_EQ(status, OkStatus());
+
+ constexpr std::string_view kExpectedDeviceName{"pixel"};
+
+ EXPECT_EQ(device_info.device_name.size(), kExpectedDeviceName.size());
+ EXPECT_EQ(std::memcmp(device_info.device_name.data(),
+ kExpectedDeviceName.data(),
+ kExpectedDeviceName.size()),
+ 0);
+ EXPECT_EQ(device_info.device_id, 0x08080808u);
+ EXPECT_EQ(device_info.status, DeviceInfo::DeviceStatus::OK);
+
+ return status;
+ });
+ const auto status = pigweed.Read(message);
+ ASSERT_EQ(status, OkStatus());
+}
+
+TEST(CodegenMessage, ReadOptionalPresent) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // optional.sometimes_present_fixed
+ 0x0d, 0x2a, 0x00, 0x00, 0x00,
+ // optional.sometimes_present_varint
+ 0x10, 0x2a,
+ // optional.explicitly_present_fixed
+ 0x1d, 0x45, 0x00, 0x00, 0x00,
+ // optional.explicitly_present_varint
+ 0x20, 0x45,
+ // optional.sometimes_empty_fixed
+ 0x2a, 0x04, 0x63, 0x00, 0x00, 0x00,
+ // optional.sometimes_empty_varint
+ 0x32, 0x01, 0x63,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ OptionalTest::StreamDecoder optional_test(reader);
+
+ OptionalTest::Message message{};
+ const auto status = optional_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ EXPECT_EQ(message.sometimes_present_fixed, 0x2a);
+ EXPECT_EQ(message.sometimes_present_varint, 0x2a);
+ EXPECT_TRUE(message.explicitly_present_fixed);
+ EXPECT_EQ(*message.explicitly_present_fixed, 0x45);
+ EXPECT_TRUE(message.explicitly_present_varint);
+ EXPECT_EQ(*message.explicitly_present_varint, 0x45);
+ EXPECT_FALSE(message.sometimes_empty_fixed.empty());
+ EXPECT_EQ(message.sometimes_empty_fixed.size(), 1u);
+ EXPECT_EQ(message.sometimes_empty_fixed[0], 0x63);
+ EXPECT_FALSE(message.sometimes_empty_varint.empty());
+ EXPECT_EQ(message.sometimes_empty_varint.size(), 1u);
+ EXPECT_EQ(message.sometimes_empty_varint[0], 0x63);
+}
+
+TEST(CodegenMessage, ReadOptionalNotPresent) {
+ constexpr std::array<std::byte, 0> proto_data{};
+
+ stream::MemoryReader reader(proto_data);
+ OptionalTest::StreamDecoder optional_test(reader);
+
+ OptionalTest::Message message{};
+ const auto status = optional_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // Non-optional fields have their default value.
+ EXPECT_EQ(message.sometimes_present_fixed, 0);
+ EXPECT_EQ(message.sometimes_present_varint, 0);
+ EXPECT_TRUE(message.sometimes_empty_fixed.empty());
+ EXPECT_TRUE(message.sometimes_empty_varint.empty());
+
+ // Optional fields are explicitly not present.
+ EXPECT_FALSE(message.explicitly_present_fixed);
+ EXPECT_FALSE(message.explicitly_present_varint);
+}
+
+TEST(CodegenMessage, ReadOptionalPresentDefaults) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // optional.sometimes_present_fixed
+ 0x0d, 0x00, 0x00, 0x00, 0x00,
+ // optional.sometimes_present_varint
+ 0x10, 0x00,
+ // optional.explicitly_present_fixed
+ 0x1d, 0x00, 0x00, 0x00, 0x00,
+ // optional.explicitly_present_varint
+ 0x20, 0x00,
+ // optional.sometimes_empty_fixed
+ 0x2a, 0x04, 0x00, 0x00, 0x00, 0x00,
+ // optional.sometimes_empty_varint
+ 0x32, 0x01, 0x00,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ OptionalTest::StreamDecoder optional_test(reader);
+
+ OptionalTest::Message message{};
+ const auto status = optional_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // Non-optional fields have their default value and aren't meaningfully
+ // different from missing.
+ EXPECT_EQ(message.sometimes_present_fixed, 0x00);
+ EXPECT_EQ(message.sometimes_present_varint, 0x00);
+
+ // Optional fields are explicitly present with a default value.
+ EXPECT_TRUE(message.explicitly_present_fixed);
+ EXPECT_EQ(*message.explicitly_present_fixed, 0x00);
+ EXPECT_TRUE(message.explicitly_present_varint);
+ EXPECT_EQ(*message.explicitly_present_varint, 0x00);
+
+ // Repeated fields with a default value are meaningfully non-empty.
+ EXPECT_FALSE(message.sometimes_empty_fixed.empty());
+ EXPECT_EQ(message.sometimes_empty_fixed.size(), 1u);
+ EXPECT_EQ(message.sometimes_empty_fixed[0], 0x00);
+ EXPECT_FALSE(message.sometimes_empty_varint.empty());
+ EXPECT_EQ(message.sometimes_empty_varint.size(), 1u);
+ EXPECT_EQ(message.sometimes_empty_varint[0], 0x00);
+}
+
+TEST(CodegenMessage, ReadImportedOptions) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // notice
+ 0x0a, 0x0f,
+ // notice.message
+ 0x0a, 0x0d, 'P', 'r', 'e', 's', 's', ' ', 'a', 'n', 'y', ' ', 'k', 'e', 'y'
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ TestMessage::StreamDecoder test_message(reader);
+
+ // The options file for the imported proto is applied, making the string
+ // field a vector rather than requiring a callback.
+ TestMessage::Message message{};
+ const auto status = test_message.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ constexpr std::string_view kExpectedMessage{"Press any key"};
+
+ EXPECT_EQ(message.notice.message.size(), kExpectedMessage.size());
+ EXPECT_EQ(std::memcmp(message.notice.message.data(),
+ kExpectedMessage.data(),
+ kExpectedMessage.size()),
+ 0);
+}
+
+TEST(CodegenMessage, ReadImportedFromDepsOptions) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // debug
+ 0x12, 0x0f,
+ // debug.message
+ 0x0a, 0x0d, 'P', 'r', 'e', 's', 's', ' ', 'a', 'n', 'y', ' ', 'k', 'e', 'y'
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ TestMessage::StreamDecoder test_message(reader);
+
+ // The options file for the imported proto is applied, making the string
+ // field a vector rather than requiring a callback.
+ TestMessage::Message message{};
+ const auto status = test_message.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ constexpr std::string_view kExpectedMessage{"Press any key"};
+
+ EXPECT_EQ(message.debug.message.size(), kExpectedMessage.size());
+ EXPECT_EQ(std::memcmp(message.debug.message.data(),
+ kExpectedMessage.data(),
+ kExpectedMessage.size()),
+ 0);
+}
+
+class BreakableDecoder : public KeyValuePair::StreamDecoder {
+ public:
+ constexpr BreakableDecoder(stream::Reader& reader) : StreamDecoder(reader) {}
+
+ Status Read(KeyValuePair::Message& message,
+ span<const internal::MessageField> table) {
+ return ::pw::protobuf::StreamDecoder::Read(
+ as_writable_bytes(span(&message, 1)), table);
+ }
+};
+
+TEST(CodegenMessage, DISABLED_ReadDoesNotOverrun) {
+ // Deliberately construct a message table that attempts to violate the bounds
+ // of the structure. We're not testing that a developer can't do this, rather
+ // that the protobuf decoder can't be exploited in this way.
+ constexpr internal::MessageField kMessageFields[] = {
+ {1,
+ WireType::kDelimited,
+ sizeof(std::byte),
+ static_cast<internal::VarintType>(0),
+ false,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ sizeof(KeyValuePair::Message) * 2,
+ {}},
+ };
+
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // id=1, len=9,
+ 0x0a, 0x08, 'd', 'o', 'n', 't', 'e', 'a', 't', 'm', 'e',
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ BreakableDecoder decoder(reader);
+
+ KeyValuePair::Message message{};
+ // ASSERT_CRASH
+ std::ignore = decoder.Read(message, kMessageFields);
+}
+
+TEST(CodegenMessage, Write) {
+ constexpr uint8_t pigweed_data[] = {
+ 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80};
+
+ Pigweed::Message message{};
+ message.magic_number = 0x49u;
+ message.ziggy = -111;
+ message.cycles = 0x40302010fecaaddeu;
+ message.ratio = -1.42f;
+ message.error_message = "not a typewriter";
+ message.pigweed.status = Bool::FILE_NOT_FOUND;
+ message.bin = Pigweed::Protobuf::Binary::ZERO;
+ message.bungle = -111;
+ message.proto.bin = Proto::Binary::OFF;
+ message.proto.pigweed_pigweed_bin = Pigweed::Pigweed::Binary::ZERO;
+ message.proto.pigweed_protobuf_bin = Pigweed::Protobuf::Binary::ZERO;
+ message.proto.meta.file_name = "/etc/passwd";
+ message.proto.meta.status = Pigweed::Protobuf::Compiler::Status::FUBAR;
+ message.proto.meta.protobuf_bin = Pigweed::Protobuf::Binary::ONE;
+ message.proto.meta.pigweed_bin = Pigweed::Pigweed::Binary::ONE;
+ std::memcpy(message.data.data(), pigweed_data, sizeof(pigweed_data));
+
+ std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes];
+ std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ Pigweed::StreamEncoder pigweed(writer, temp_buffer);
+
+ const auto status = pigweed.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // pigweed.magic_number
+ 0x08, 0x49,
+ // pigweed.ziggy
+ 0x10, 0xdd, 0x01,
+ // pigweed.cycles
+ 0x19, 0xde, 0xad, 0xca, 0xfe, 0x10, 0x20, 0x30, 0x40,
+ // pigweed.ratio
+ 0x25, 0x8f, 0xc2, 0xb5, 0xbf,
+ // pigweed.error_message
+ 0x2a, 0x10, 'n', 'o', 't', ' ', 'a', ' ',
+ 't', 'y', 'p', 'e', 'w', 'r', 'i', 't', 'e', 'r',
+ // pigweed.pigweed
+ 0x3a, 0x02,
+ // pigweed.pigweed.status
+ 0x08, 0x02,
+ // pigweed.bin
+ 0x40, 0x01,
+ // pigweed.proto
+ 0x4a, 0x15,
+ // pigweed.proto.pigweed_protobuf_bin
+ 0x20, 0x01,
+ // pigweed.proto.meta
+ 0x2a, 0x11,
+ // pigweed.proto.meta.file_name
+ 0x0a, 0x0b, '/', 'e', 't', 'c', '/', 'p', 'a', 's', 's', 'w', 'd',
+ // pigweed.proto.meta.status
+ 0x10, 0x02,
+ // pigweed.proto.meta.pigweed_bin
+ 0x20, 0x01,
+ // pigweed.bytes
+ 0x5a, 0x08, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80,
+ // pigweed.bungle
+ 0x70, 0x91, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WriteDefaults) {
+ Pigweed::Message message{};
+
+ std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes];
+ std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ Pigweed::StreamEncoder pigweed(writer, temp_buffer);
+
+ const auto status = pigweed.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // Since all fields are at their default, the output should be zero sized.
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), 0u);
+}
+
+TEST(CodegenMessage, WritePackedScalar) {
+ RepeatedTest::Message message{};
+ for (int i = 0; i < 4; ++i) {
+ message.uint32s.push_back(i * 16u);
+ message.fixed32s.push_back(i * 16u);
+ }
+
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
+
+ const auto status = repeated_test.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // uint32s[], v={0, 16, 32, 48}
+ 0x0a, 0x04,
+ 0x00,
+ 0x10,
+ 0x20,
+ 0x30,
+ // fixed32s[]. v={0, 16, 32, 48}
+ 0x32, 0x10,
+ 0x00, 0x00, 0x00, 0x00,
+ 0x10, 0x00, 0x00, 0x00,
+ 0x20, 0x00, 0x00, 0x00,
+ 0x30, 0x00, 0x00, 0x00,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WritePackedScalarFixedLength) {
+ RepeatedTest::Message message{};
+ for (int i = 0; i < 4; ++i) {
+ message.uint64s[i] = (i + 1) * 1000u;
+ }
+ message.doubles[0] = 3.14159;
+ message.doubles[1] = 2.71828;
+
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
+
+ const auto status = repeated_test.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // doubles[], v={3.14159, 2.71828}
+ 0x22, 0x10,
+ 0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40,
+ 0x90, 0xf7, 0xaa, 0x95, 0x09, 0xbf, 0x05, 0x40,
+ // uint64s[], v={1000, 2000, 3000, 4000}
+ 0x42, 0x08, 0xe8, 0x07, 0xd0, 0x0f, 0xb8, 0x17, 0xa0, 0x1f,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WritePackedScalarCallback) {
+ RepeatedTest::Message message{};
+ message.sint32s.SetEncoder([](RepeatedTest::StreamEncoder& encoder) {
+ constexpr int32_t sint32s[] = {-25, -1, 0, 1, 25};
+ return encoder.WriteSint32s(sint32s);
+ });
+
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes +
+ varint::kMaxVarint32SizeBytes * 5];
+
+ stream::MemoryWriter writer(encode_buffer);
+ RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
+
+ const auto status = repeated_test.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // sint32s[], v={-25, -1, 0, 1, 25}
+ 0x12, 0x05,
+ 0x31,
+ 0x01,
+ 0x00,
+ 0x02,
+ 0x32,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WritePackedEnum) {
+ RepeatedTest::Message message{};
+ message.enums.push_back(Enum::RED);
+ message.enums.push_back(Enum::GREEN);
+ message.enums.push_back(Enum::AMBER);
+ message.enums.push_back(Enum::RED);
+
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ RepeatedTest::StreamEncoder repeated_test(writer, ByteSpan());
+
+ const auto status = repeated_test.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // enums[], v={RED, GREEN, AMBER, RED}
+ 0x4a, 0x04, 0x00, 0x02, 0x01, 0x00,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WriteStringCallback) {
+ Pigweed::Message message{};
+ // pigweed.description has no max_size specified so a callback must be
+ // set to write the value.
+ message.description.SetEncoder([](Pigweed::StreamEncoder& encoder) {
+ return encoder.WriteDescription(
+ "an open source collection of embedded-targeted "
+ "libraries-or as we like to call them, modules");
+ });
+
+ std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes + 92];
+ std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ Pigweed::StreamEncoder pigweed(writer, temp_buffer);
+
+ const auto status = pigweed.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // pigweed.description
+ 0x62, 0x5c, 'a', 'n', ' ', 'o', 'p', 'e', 'n', ' ', 's', 'o', 'u', 'r', 'c',
+ 'e', ' ', 'c', 'o', 'l', 'l', 'e', 'c', 't', 'i', 'o', 'n', ' ', 'o', 'f',
+ ' ', 'e', 'm', 'b', 'e', 'd', 'd', 'e', 'd', '-', 't', 'a', 'r', 'g', 'e',
+ 't', 'e', 'd', ' ', 'l', 'i', 'b', 'r', 'a', 'r', 'i', 'e', 's', '-', 'o',
+ 'r', ' ', 'a', 's', ' ', 'w', 'e', ' ', 'l', 'i', 'k', 'e', ' ', 't', 'o',
+ ' ', 'c', 'a', 'l', 'l', ' ', 't', 'h', 'e', 'm', ',', ' ', 'm', 'o', 'd',
+ 'u', 'l', 'e', 's',
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WriteForcedCallback) {
+ Pigweed::Message message{};
+ // pigweed.special_property has use_callback=true to force the use of a
+ // callback even though it's a simple scalar.
+ message.special_property.SetEncoder([](Pigweed::StreamEncoder& encoder) {
+ return encoder.WriteSpecialProperty(42u);
+ });
+
+ std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes];
+ std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ Pigweed::StreamEncoder pigweed(writer, temp_buffer);
+
+ const auto status = pigweed.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // pigweed.special_property
+ 0x68, 0x2a,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WriteNestedImported) {
+ Period::Message message{};
+ message.start.seconds = 1517949900u;
+ message.end.seconds = 1517950378u;
+
+ std::byte encode_buffer[Period::kMaxEncodedSizeBytes];
+ std::byte temp_buffer[Period::kScratchBufferSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ Period::StreamEncoder period(writer, temp_buffer);
+
+ const auto status = period.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // period.start
+ 0x0a, 0x06,
+ // period.start.seconds v=1517949900
+ 0x08, 0xcc, 0xa7, 0xe8, 0xd3, 0x05,
+ // period.end
+ 0x12, 0x06,
+ // period.end.seconds, v=1517950378
+ 0x08, 0xaa, 0xab, 0xe8, 0xd3, 0x05,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WriteNestedRepeated) {
+ RepeatedTest::Message message{};
+ // Repeated nested messages require a callback since there would otherwise be
+ // no way to set callbacks on the nested message.
+ message.structs.SetEncoder([](RepeatedTest::StreamEncoder& encoder) {
+ for (int i = 0; i < 2; ++i) {
+ Struct::Message struct_message{};
+ struct_message.one = i * 32 + 16u;
+ struct_message.two = i * 32 + 32u;
+
+ const auto status = encoder.GetStructsEncoder().Write(struct_message);
+ EXPECT_EQ(status, OkStatus());
+ }
+ return OkStatus();
+ });
+
+ std::byte encode_buffer[RepeatedTest::kMaxEncodedSizeBytes +
+ Struct::kMaxEncodedSizeBytes * 2];
+ std::byte temp_buffer[RepeatedTest::kScratchBufferSizeBytes +
+ Struct::kMaxEncodedSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ RepeatedTest::StreamEncoder repeated_test(writer, temp_buffer);
+
+ const auto status = repeated_test.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // repeated.structs
+ 0x2a, 0x04,
+ // repeated.structs.one v=16
+ 0x08, 0x10,
+ // repeated.structs.two v=32
+ 0x10, 0x20,
+ // repeated.structs
+ 0x2a, 0x04,
+ // repeated.structs.one v=48
+ 0x08, 0x30,
+ // repeated.structs.two v=64
+ 0x10, 0x40,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WriteNestedForcedCallback) {
+ Pigweed::Message message{};
+ // pigweed.device_info has use_callback=true to force the use of a callback.
+ message.device_info.SetEncoder([](Pigweed::StreamEncoder& encoder) {
+ DeviceInfo::Message device_info{};
+ device_info.device_name = "pixel";
+ device_info.device_id = 0x08080808u;
+ device_info.status = DeviceInfo::DeviceStatus::OK;
+
+ // Use the callback to set nested callbacks.
+ device_info.attributes.SetEncoder(
+ [](DeviceInfo::StreamEncoder& device_info_encoder) {
+ KeyValuePair::Message attribute{};
+
+ attribute.key = "version";
+ attribute.value = "5.3.1";
+ PW_TRY(device_info_encoder.GetAttributesEncoder().Write(attribute));
+
+ attribute.key = "chip";
+ attribute.value = "left-soc";
+ PW_TRY(device_info_encoder.GetAttributesEncoder().Write(attribute));
+
+ return OkStatus();
+ });
+
+ return encoder.GetDeviceInfoEncoder().Write(device_info);
+ });
+
+ std::byte encode_buffer[Pigweed::kMaxEncodedSizeBytes +
+ DeviceInfo::kMaxEncodedSizeBytes];
+ std::byte temp_buffer[Pigweed::kScratchBufferSizeBytes +
+ DeviceInfo::kMaxEncodedSizeBytes];
+
+ stream::MemoryWriter writer(encode_buffer);
+ Pigweed::StreamEncoder pigweed(writer, temp_buffer);
+
+ const auto status = pigweed.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // pigweed.device_info
+ 0x32, 0x30,
+ // pigweed.device_info.device_name
+ 0x0a, 0x05, 'p', 'i', 'x', 'e', 'l',
+ // pigweed.device_info.device_id
+ 0x15, 0x08, 0x08, 0x08, 0x08,
+ // pigweed.device_info.attributes[0]
+ 0x22, 0x10,
+ // pigweed.device_info.attributes[0].key
+ 0x0a, 0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n',
+ // pigweed.device_info.attributes[0].value
+ 0x12, 0x05, '5', '.', '3', '.', '1',
+ // pigweed.device_info.attributes[1]
+ 0x22, 0x10,
+ // pigweed.device_info.attributes[1].key
+ 0x0a, 0x04, 'c', 'h', 'i', 'p',
+ // pigweed.device_info.attributes[1].value
+ 0x12, 0x08, 'l', 'e', 'f', 't', '-', 's', 'o', 'c',
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, EnumAliases) {
+ // Unprefixed enum.
+ EXPECT_EQ(Bool::kTrue, Bool::TRUE);
+ EXPECT_EQ(Bool::kFalse, Bool::FALSE);
+ EXPECT_EQ(Bool::kFileNotFound, Bool::FILE_NOT_FOUND);
+
+ // Prefixed enum has the prefix removed.
+ EXPECT_EQ(Error::kNone, Error::ERROR_NONE);
+ EXPECT_EQ(Error::kNotFound, Error::ERROR_NOT_FOUND);
+ EXPECT_EQ(Error::kUnknown, Error::ERROR_UNKNOWN);
+
+ // Single-value enum.
+ EXPECT_EQ(AlwaysBlue::kBlue, AlwaysBlue::BLUE);
+}
+
+TEST(CodegenMessage, WriteOptionalPresent) {
+ OptionalTest::Message message{};
+ message.sometimes_present_fixed = 0x2a;
+ message.sometimes_present_varint = 0x2a;
+ message.explicitly_present_fixed = 0x45;
+ message.explicitly_present_varint = 0x45;
+ message.sometimes_empty_fixed.push_back(0x63);
+ message.sometimes_empty_varint.push_back(0x63);
+
+ std::byte encode_buffer[512];
+
+ stream::MemoryWriter writer(encode_buffer);
+ OptionalTest::StreamEncoder optional_test(writer, ByteSpan());
+
+ const auto status = optional_test.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // optional.sometimes_present_fixed
+ 0x0d, 0x2a, 0x00, 0x00, 0x00,
+ // optional.sometimes_present_varint
+ 0x10, 0x2a,
+ // optional.explicitly_present_fixed
+ 0x1d, 0x45, 0x00, 0x00, 0x00,
+ // optional.explicitly_present_varint
+ 0x20, 0x45,
+ // optional.sometimes_empty_fixed
+ 0x2a, 0x04, 0x63, 0x00, 0x00, 0x00,
+ // optional.sometimes_empty_varint
+ 0x32, 0x01, 0x63,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+TEST(CodegenMessage, WriteOptionalNotPresent) {
+ OptionalTest::Message message{};
+
+ std::byte encode_buffer[512];
+
+ stream::MemoryWriter writer(encode_buffer);
+ OptionalTest::StreamEncoder optional_test(writer, ByteSpan());
+
+ const auto status = optional_test.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // The expected proto is empty; no bytes should be written.
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_TRUE(result.empty());
+}
+
+TEST(CodegenMessage, WriteOptionalPresentDefaults) {
+ OptionalTest::Message message{};
+ // Non-optional fields with a default value are not explicitly encoded, so
+ // aren't meaningfully different from one that's just ommitted.
+ message.sometimes_present_fixed = 0x00;
+ message.sometimes_present_varint = 0x00;
+ // Optional fields, even with a default value, are explicitly encoded.
+ message.explicitly_present_fixed = 0x00;
+ message.explicitly_present_varint = 0x00;
+ // Repeated fields with a default value are meaningfully non-empty.
+ message.sometimes_empty_fixed.push_back(0x00);
+ message.sometimes_empty_varint.push_back(0x00);
+
+ std::byte encode_buffer[512];
+
+ stream::MemoryWriter writer(encode_buffer);
+ OptionalTest::StreamEncoder optional_test(writer, ByteSpan());
+
+ const auto status = optional_test.Write(message);
+ ASSERT_EQ(status, OkStatus());
+
+ // clang-format off
+ constexpr uint8_t expected_proto[] = {
+ // optional.explicitly_present_fixed
+ 0x1d, 0x00, 0x00, 0x00, 0x00,
+ // optional.explicitly_present_varint
+ 0x20, 0x00,
+ // optional.sometimes_empty_fixed
+ 0x2a, 0x04, 0x00, 0x00, 0x00, 0x00,
+ // optional.sometimes_empty_varint
+ 0x32, 0x01, 0x00,
+ };
+ // clang-format on
+
+ ConstByteSpan result = writer.WrittenData();
+ EXPECT_EQ(result.size(), sizeof(expected_proto));
+ EXPECT_EQ(std::memcmp(result.data(), expected_proto, sizeof(expected_proto)),
+ 0);
+}
+
+class BreakableEncoder : public KeyValuePair::MemoryEncoder {
+ public:
+ constexpr BreakableEncoder(ByteSpan buffer)
+ : KeyValuePair::MemoryEncoder(buffer) {}
+
+ Status Write(const KeyValuePair::Message& message,
+ span<const internal::MessageField> table) {
+ return ::pw::protobuf::StreamEncoder::Write(as_bytes(span(&message, 1)),
+ table);
+ }
+};
+
+TEST(CodegenMessage, DISABLED_WriteDoesNotOverrun) {
+ // Deliberately construct a message table that attempts to violate the bounds
+ // of the structure. We're not testing that a developer can't do this, rather
+ // that the protobuf encoder can't be exploited in this way.
+ constexpr internal::MessageField kMessageFields[] = {
+ {1,
+ WireType::kDelimited,
+ sizeof(std::byte),
+ static_cast<internal::VarintType>(0),
+ false,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ sizeof(KeyValuePair::Message) * 2,
+ {}},
+ };
+
+ std::byte encode_buffer[64];
+
+ BreakableEncoder encoder(encode_buffer);
+ KeyValuePair::Message message{};
+ // ASSERT_CRASH
+ std::ignore = encoder.Write(message, kMessageFields);
+}
+
+// The following tests cover using the codegen struct Message and callbacks in
+// different ways.
+
+// Check that the callback function object is large enough to implement a
+// "call a function on this" lambda.
+class StringChecker {
+ public:
+ StringChecker() = default;
+ ~StringChecker() = default;
+
+ Status Check(RepeatedTest::StreamDecoder& repeated_test) {
+ RepeatedTest::Message message{};
+ message.strings.SetDecoder([this](RepeatedTest::StreamDecoder& decoder) {
+ return this->CheckOne(decoder);
+ });
+ return repeated_test.Read(message);
+ }
+
+ private:
+ Status CheckOne(RepeatedTest::StreamDecoder& decoder) {
+ EXPECT_EQ(decoder.Field().value(), RepeatedTest::Fields::kStrings);
+
+ std::array<char, 40> strings{};
+ const StatusWithSize sws = decoder.ReadStrings(strings);
+ EXPECT_EQ(sws.status(), OkStatus());
+ EXPECT_EQ(sws.size(), kExpectedStrings[i_].size());
+ EXPECT_EQ(std::memcmp(strings.data(),
+ kExpectedStrings[i_].data(),
+ kExpectedStrings[i_].size()),
+ 0);
+
+ ++i_;
+ return sws.status();
+ }
+
+ int i_ = 0;
+ constexpr static std::string_view kExpectedStrings[] = {
+ {"if music be the food of love, play on"},
+ {"give me excess of it, that, surfeiting"},
+ {"the appetite may sicken, and so die"}};
+};
+
+TEST(CodegenMessage, CallbackInClass) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // repeated.strings
+ 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ',
+ 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ',
+ 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n',
+ // repeated.strings
+ 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e',
+ 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',',
+ ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g',
+ // repeated.strings
+ 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ',
+ 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd',
+ ' ', 's', 'o', ' ', 'd', 'i', 'e',
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ StringChecker checker{};
+ const auto status = checker.Check(repeated_test);
+ ASSERT_EQ(status, OkStatus());
+}
+
+// Check that we can create a custom subclass of the message struct that sets
+// its own callbacks to member functions that populate fields added in the
+// subclass.
+struct CustomMessage : RepeatedTest::Message {
+ CustomMessage() : RepeatedTest::Message() {
+ strings.SetDecoder([this](RepeatedTest::StreamDecoder& decoder) {
+ return this->ParseStrings(decoder);
+ });
+ }
+
+ pw::Vector<std::array<char, 40>, 8> all_strings{};
+
+ private:
+ Status ParseStrings(RepeatedTest::StreamDecoder& decoder) {
+ PW_ASSERT(decoder.Field().value() == RepeatedTest::Fields::kStrings);
+
+ std::array<char, 40> one_strings{};
+ const auto sws = decoder.ReadStrings(one_strings);
+ if (!sws.ok()) {
+ return sws.status();
+ }
+
+ one_strings[sws.size()] = '\0';
+ all_strings.push_back(one_strings);
+
+ return OkStatus();
+ }
+};
+
+TEST(CodegenMessage, CallbackInSubclass) {
+ // clang-format off
+ constexpr uint8_t proto_data[] = {
+ // repeated.strings
+ 0x1a, 0x25, 'i', 'f', ' ', 'm', 'u', 's', 'i', 'c', ' ', 'b', 'e', ' ',
+ 't', 'h', 'e', ' ', 'f', 'o', 'o', 'd', ' ', 'o', 'f', ' ',
+ 'l', 'o', 'v', 'e', ',', ' ', 'p', 'l', 'a', 'y', ' ', 'o', 'n',
+ // repeated.strings
+ 0x1a, 0x26, 'g', 'i', 'v', 'e', ' ', 'm', 'e', ' ', 'e', 'x', 'c', 'e',
+ 's', 's', ' ', 'o', 'f', ' ', 'i', 't', ',', ' ', 't', 'h', 'a', 't', ',',
+ ' ', 's', 'u', 'r', 'f', 'e', 'i', 't', 'i', 'n', 'g',
+ // repeated.strings
+ 0x1a, 0x23, 't', 'h', 'e', ' ', 'a', 'p', 'p', 'e', 't', 'i', 't', 'e', ' ',
+ 'm', 'a', 'y', ' ', 's', 'i', 'c', 'k', 'e', 'n', ',', ' ', 'a', 'n', 'd',
+ ' ', 's', 'o', ' ', 'd', 'i', 'e',
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(proto_data)));
+ RepeatedTest::StreamDecoder repeated_test(reader);
+
+ CustomMessage message{};
+ const auto status = repeated_test.Read(message);
+ ASSERT_EQ(status, OkStatus());
+
+ constexpr static std::string_view kExpectedStrings[] = {
+ {"if music be the food of love, play on"},
+ {"give me excess of it, that, surfeiting"},
+ {"the appetite may sicken, and so die"}};
+
+ EXPECT_EQ(message.all_strings.size(), 3u);
+ for (int i = 0; i < 3; ++i) {
+ EXPECT_EQ(std::memcmp(message.all_strings[i].data(),
+ kExpectedStrings[i].data(),
+ kExpectedStrings[i].size()),
+ 0);
+ EXPECT_EQ(message.all_strings[i].data()[kExpectedStrings[i].size()], '\0');
+ }
+}
+
+} // namespace
+} // namespace pw::protobuf
diff --git a/pw_protobuf/decoder.cc b/pw_protobuf/decoder.cc
index 7f8c8cf39..2f58eb501 100644
--- a/pw_protobuf/decoder.cc
+++ b/pw_protobuf/decoder.cc
@@ -103,7 +103,7 @@ Status Decoder::ReadBool(bool* out) {
}
Status Decoder::ReadString(std::string_view* out) {
- std::span<const std::byte> bytes;
+ span<const std::byte> bytes;
Status status = ReadDelimited(&bytes);
if (!status.ok()) {
return status;
@@ -120,7 +120,7 @@ size_t Decoder::FieldSize() const {
return 0;
}
- std::span<const std::byte> remainder = proto_.subspan(key_size);
+ span<const std::byte> remainder = proto_.subspan(key_size);
uint64_t value = 0;
size_t expected_size = 0;
@@ -212,7 +212,7 @@ Status Decoder::ReadFixed(std::byte* out, size_t size) {
return OkStatus();
}
-Status Decoder::ReadDelimited(std::span<const std::byte>* out) {
+Status Decoder::ReadDelimited(span<const std::byte>* out) {
Status status = ConsumeKey(WireType::kDelimited);
if (!status.ok()) {
return status;
@@ -236,7 +236,7 @@ Status Decoder::ReadDelimited(std::span<const std::byte>* out) {
return OkStatus();
}
-Status CallbackDecoder::Decode(std::span<const std::byte> proto) {
+Status CallbackDecoder::Decode(span<const std::byte> proto) {
if (handler_ == nullptr || state_ != kReady) {
return Status::FailedPrecondition();
}
diff --git a/pw_protobuf/decoder_fuzzer.cc b/pw_protobuf/decoder_fuzzer.cc
new file mode 100644
index 000000000..7866b4381
--- /dev/null
+++ b/pw_protobuf/decoder_fuzzer.cc
@@ -0,0 +1,219 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include "fuzz.h"
+#include "pw_fuzzer/fuzzed_data_provider.h"
+#include "pw_protobuf/stream_decoder.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_stream/memory_stream.h"
+#include "pw_stream/stream.h"
+
+namespace pw::protobuf::fuzz {
+namespace {
+
+void recursive_fuzzed_decode(FuzzedDataProvider& provider,
+ StreamDecoder& decoder,
+ uint32_t depth = 0) {
+ constexpr size_t kMaxRepeatedRead = 1024;
+ constexpr size_t kMaxDepth = 3;
+
+ if (depth > kMaxDepth) {
+ return;
+ }
+ while (provider.remaining_bytes() != 0 && decoder.Next().ok()) {
+ FieldType field_type = provider.ConsumeEnum<FieldType>();
+ switch (field_type) {
+ case kUint32:
+ if (!decoder.ReadUint32().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedUint32: {
+ uint32_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedUint32(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kUint64:
+ if (!decoder.ReadUint64().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedUint64: {
+ uint64_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedUint64(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kInt32:
+ if (!decoder.ReadInt32().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedInt32: {
+ int32_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedInt32(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kInt64:
+ if (!decoder.ReadInt64().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedInt64: {
+ int64_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedInt64(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kSint32:
+ if (!decoder.ReadSint32().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedSint32: {
+ int32_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedSint32(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kSint64:
+ if (!decoder.ReadSint64().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedSint64: {
+ int64_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedSint64(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kBool:
+ if (!decoder.ReadBool().status().ok()) {
+ return;
+ }
+ break;
+ case kFixed32:
+ if (!decoder.ReadFixed32().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedFixed32: {
+ uint32_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedFixed32(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kFixed64:
+ if (!decoder.ReadFixed64().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedFixed64: {
+ uint64_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedFixed64(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kSfixed32:
+ if (!decoder.ReadSfixed32().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedSfixed32: {
+ int32_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedSfixed32(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kSfixed64:
+ if (!decoder.ReadSfixed64().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedSfixed64: {
+ int64_t packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedSfixed64(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kFloat:
+ if (!decoder.ReadFloat().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedFloat: {
+ float packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedFloat(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kDouble:
+ if (!decoder.ReadDouble().status().ok()) {
+ return;
+ }
+ break;
+ case kPackedDouble: {
+ double packed[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadPackedDouble(packed).status().ok()) {
+ return;
+ }
+ } break;
+ case kBytes: {
+ std::byte bytes[kMaxRepeatedRead] = {std::byte{0}};
+ if (!decoder.ReadBytes(bytes).status().ok()) {
+ return;
+ }
+ } break;
+ case kString: {
+ char str[kMaxRepeatedRead] = {0};
+ if (!decoder.ReadString(str).status().ok()) {
+ return;
+ }
+ } break;
+ case kPush: {
+ StreamDecoder nested_decoder = decoder.GetNestedDecoder();
+ recursive_fuzzed_decode(provider, nested_decoder, depth + 1);
+
+ } break;
+ }
+ }
+}
+
+void TestOneInput(FuzzedDataProvider& provider) {
+ constexpr size_t kMaxFuzzedProtoSize = 4096;
+ std::vector<std::byte> proto_message_data = provider.ConsumeBytes<std::byte>(
+ provider.ConsumeIntegralInRange<size_t>(0, kMaxFuzzedProtoSize));
+ stream::MemoryReader memory_reader(proto_message_data);
+ StreamDecoder decoder(memory_reader);
+ recursive_fuzzed_decode(provider, decoder);
+}
+
+} // namespace
+} // namespace pw::protobuf::fuzz
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ FuzzedDataProvider provider(data, size);
+ pw::protobuf::fuzz::TestOneInput(provider);
+ return 0;
+}
diff --git a/pw_protobuf/decoder_test.cc b/pw_protobuf/decoder_test.cc
index d006312ac..1111b1a5e 100644
--- a/pw_protobuf/decoder_test.cc
+++ b/pw_protobuf/decoder_test.cc
@@ -14,6 +14,8 @@
#include "pw_protobuf/decoder.h"
+#include <cstring>
+
#include "gtest/gtest.h"
#include "pw_preprocessor/util.h"
@@ -28,28 +30,22 @@ class TestDecodeHandler : public DecodeHandler {
switch (field_number) {
case 1:
- decoder.ReadInt32(&test_int32)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), decoder.ReadInt32(&test_int32));
break;
case 2:
- decoder.ReadSint32(&test_sint32)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), decoder.ReadSint32(&test_sint32));
break;
case 3:
- decoder.ReadBool(&test_bool)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), decoder.ReadBool(&test_bool));
break;
case 4:
- decoder.ReadDouble(&test_double)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), decoder.ReadDouble(&test_double));
break;
case 5:
- decoder.ReadFixed32(&test_fixed32)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), decoder.ReadFixed32(&test_fixed32));
break;
case 6:
- decoder.ReadString(&str)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), decoder.ReadString(&str));
std::memcpy(test_string, str.data(), str.size());
test_string[str.size()] = '\0';
break;
@@ -86,7 +82,7 @@ TEST(Decoder, Decode) {
};
// clang-format on
- Decoder decoder(std::as_bytes(std::span(encoded_proto)));
+ Decoder decoder(as_bytes(span(encoded_proto)));
int32_t v1 = 0;
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -148,7 +144,7 @@ TEST(Decoder, Decode_SkipsUnusedFields) {
};
// clang-format on
- Decoder decoder(std::as_bytes(std::span(encoded_proto)));
+ Decoder decoder(as_bytes(span(encoded_proto)));
// Don't process any fields except for the fourth. Next should still iterate
// correctly despite field values not being consumed.
@@ -174,7 +170,7 @@ TEST(Decoder, Decode_BadFieldNumber) {
};
// clang-format on
- Decoder decoder(std::as_bytes(std::span(encoded_proto)));
+ Decoder decoder(as_bytes(span(encoded_proto)));
int32_t value;
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -210,8 +206,7 @@ TEST(CallbackDecoder, Decode) {
// clang-format on
decoder.set_handler(&handler);
- EXPECT_EQ(decoder.Decode(std::as_bytes(std::span(encoded_proto))),
- OkStatus());
+ EXPECT_EQ(decoder.Decode(as_bytes(span(encoded_proto))), OkStatus());
EXPECT_TRUE(handler.called);
EXPECT_EQ(handler.test_int32, 42);
EXPECT_EQ(handler.test_sint32, -13);
@@ -237,8 +232,7 @@ TEST(CallbackDecoder, Decode_OverridesDuplicateFields) {
// clang-format on
decoder.set_handler(&handler);
- EXPECT_EQ(decoder.Decode(std::as_bytes(std::span(encoded_proto))),
- OkStatus());
+ EXPECT_EQ(decoder.Decode(as_bytes(span(encoded_proto))), OkStatus());
EXPECT_TRUE(handler.called);
EXPECT_EQ(handler.test_int32, 44);
}
@@ -248,7 +242,7 @@ TEST(CallbackDecoder, Decode_Empty) {
TestDecodeHandler handler;
decoder.set_handler(&handler);
- EXPECT_EQ(decoder.Decode(std::span<std::byte>()), OkStatus());
+ EXPECT_EQ(decoder.Decode(span<std::byte>()), OkStatus());
EXPECT_FALSE(handler.called);
EXPECT_EQ(handler.test_int32, 0);
EXPECT_EQ(handler.test_sint32, 0);
@@ -262,8 +256,7 @@ TEST(CallbackDecoder, Decode_BadData) {
uint8_t encoded_proto[] = {0x08};
decoder.set_handler(&handler);
- EXPECT_EQ(decoder.Decode(std::as_bytes(std::span(encoded_proto))),
- Status::DataLoss());
+ EXPECT_EQ(decoder.Decode(as_bytes(span(encoded_proto))), Status::DataLoss());
}
// Only processes fields numbered 1 or 3.
@@ -317,8 +310,7 @@ TEST(CallbackDecoder, Decode_SkipsUnprocessedFields) {
// clang-format on
decoder.set_handler(&handler);
- EXPECT_EQ(decoder.Decode(std::as_bytes(std::span(encoded_proto))),
- OkStatus());
+ EXPECT_EQ(decoder.Decode(as_bytes(span(encoded_proto))), OkStatus());
EXPECT_TRUE(handler.called);
EXPECT_EQ(handler.field_one, 42);
EXPECT_EQ(handler.field_three, 99);
@@ -367,8 +359,7 @@ TEST(CallbackDecoder, Decode_StopsOnNonOkStatus) {
// clang-format on
decoder.set_handler(&handler);
- EXPECT_EQ(decoder.Decode(std::as_bytes(std::span(encoded_proto))),
- Status::Cancelled());
+ EXPECT_EQ(decoder.Decode(as_bytes(span(encoded_proto))), Status::Cancelled());
EXPECT_EQ(handler.field_one, 42);
EXPECT_EQ(handler.field_three, 1111);
}
diff --git a/pw_protobuf/docs.rst b/pw_protobuf/docs.rst
index ecc3f5593..021d03fde 100644
--- a/pw_protobuf/docs.rst
+++ b/pw_protobuf/docs.rst
@@ -3,30 +3,612 @@
===========
pw_protobuf
===========
-The protobuf module provides a lightweight interface for encoding and decoding
-the Protocol Buffer wire format.
+The protobuf module provides an expressive interface for encoding and decoding
+the Protocol Buffer wire format with a lightweight code and data footprint.
.. note::
The protobuf module is a work in progress. Wire format encoding and decoding
is supported, though the APIs are not final. C++ code generation exists for
- encoding and decoding, but does not cover all message types.
-
-------
-Design
-------
-Unlike other protobuf libraries, which typically provide in-memory data
-structures to represent protobuf messages, ``pw_protobuf`` operates directly on
-the wire format and leaves data storage to the user. This has a few benefits.
-The primary one is that it allows the library to be incredibly small, with the
-encoder and decoder each having a code size of around 1.5K and negligible RAM
-usage. Users can choose the tradeoffs most suitable for their product on top of
-this core implementation.
-
-``pw_protobuf`` also provides zero-overhead C++ code generation which wraps its
-low-level wire format operations with a user-friendly API for processing
-specific protobuf messages. The code generation integrates with Pigweed's GN
-build system.
+ encoding and decoding, but not yet optimized for in-memory decoding.
+
+--------
+Overview
+--------
+Unlike protobuf libraries which require protobuf messages be represented by
+in-memory data structures, ``pw_protobuf`` provides a progressive flexible API
+that allows the user to choose the data storage format and tradeoffs most
+suitable for their product on top of the implementation.
+
+The API is designed in three layers, which can be freely intermixed with each
+other in your code, depending on point of use requirements:
+
+ 1. Message Structures,
+ 2. Per-Field Writers and Readers,
+ 3. Direct Writers and Readers.
+
+This has a few benefits. The primary one is that it allows the core proto
+serialization and deserialization libraries to be relatively small.
+
+.. include:: size_report/protobuf_overview
+
+To demonstrate these layers, we use the following protobuf message definition
+in the examples:
+
+.. code::
+
+ message Customer {
+ enum Status {
+ NEW = 1;
+ ACTIVE = 2;
+ INACTIVE = 3;
+ }
+ int32 age = 1;
+ string name = 2;
+ Status status = 3;
+ }
+
+And the following accompanying options file:
+
+.. code::
+
+ Customer.name max_size:32
+
+.. toctree::
+ :maxdepth: 1
+
+ size_report
+
+Message Structures
+==================
+The highest level API is based around message structures created through C++
+code generation, integrated with Pigweed's build system.
+
+This results in the following generated structure:
+
+.. code:: c++
+
+ enum class Customer::Status : uint32_t {
+ NEW = 1,
+ ACTIVE = 2,
+ INACTIVE = 3,
+
+ kNew = NEW,
+ kActive = ACTIVE,
+ kInactive = INACTIVE,
+ };
+
+ struct Customer::Message {
+ int32_t age;
+ pw::InlineString<32> name;
+ Customer::Status status;
+ };
+
+Which can be encoded with the code:
+
+.. code:: c++
+
+ #include "example_protos/customer.pwpb.h"
+
+ pw::Status EncodeCustomer(Customer::StreamEncoder& encoder) {
+ return encoder.Write({
+ age = 33,
+ name = "Joe Bloggs",
+ status = Customer::Status::INACTIVE
+ });
+ }
+
+And decoded into a struct with the code:
+
+.. code:: c++
+
+ #include "example_protos/customer.pwpb.h"
+
+ pw::Status DecodeCustomer(Customer::StreamDecoder& decoder) {
+ Customer::Message customer{};
+ PW_TRY(decoder.Read(customer));
+ // Read fields from customer
+ return pw::OkStatus();
+ }
+
+These structures can be moved, copied, and compared with each other for
+equality.
+
+The encoder and decoder code is generic and implemented in the core C++ module.
+A small overhead for each message type used in your code describes the structure
+to the generic encoder and decoders.
+
+Message comparison
+------------------
+Message structures implement ``operator==`` and ``operator!=`` for equality and
+inequality comparisons. However, these can only work on trivial scalar fields
+and fixed-size strings within the message. Fields using a callback are not
+considered in the comparison.
+
+To check if the equality operator of a generated message covers all fields,
+``pw_protobuf`` provides an ``IsTriviallyComparable`` function.
+
+.. code-block:: c++
+
+ template <typename Message>
+ constexpr bool IsTriviallyComparable<Message>();
+
+For example, given the following protobuf definitions:
+
+.. code-block::
+
+ message Point {
+ int32 x = 1;
+ int32 y = 2;
+ }
+
+ message Label {
+ Point point = 1;
+ string label = 2;
+ }
+
+And the accompanying options file:
+
+.. code-block::
+
+ Label.label use_callback:true
+
+The ``Point`` message can be fully compared for equality, but ``Label`` cannot.
+``Label`` still defines an ``operator==``, but it ignores the ``label`` string.
+
+.. code-block:: c++
+
+ Point::Message one = {.x = 5, .y = 11};
+ Point::Message two = {.x = 5, .y = 11};
+
+ static_assert(pw::protobuf::IsTriviallyComparable<Point::Message>());
+ ASSERT_EQ(one, two);
+ static_assert(!pw::protobuf::IsTriviallyComparable<Label::Message>());
+
+Buffer Sizes
+------------
+Initializing a ``MemoryEncoder`` requires that you specify the size of the
+buffer to encode to. The code generation includes a ``kMaxEncodedSizeBytes``
+constant that represents the maximum encoded size of the protobuf message,
+excluding the contents of any field values which require a callback.
+
+.. code:: c++
+
+ #include "example_protos/customer.pwpb.h"
+
+ std::byte buffer[Customer::kMaxEncodedSizeBytes];
+ Customer::MemoryEncoder encoder(buffer);
+ const auto status = encoder.Write({
+ age = 22,
+ name = "Wolfgang Bjornson",
+ status = Customer::Status::ACTIVE
+ });
+
+ // Always check the encoder status or return values from Write calls.
+ if (!status.ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
+ }
+
+In the above example, because the ``name`` field has a ``max_size`` specified
+in the accompanying options file, ``kMaxEncodedSizeBytes`` includes the maximum
+length of the value for that field.
+
+Where the maximum length of a field value is not known, indicated by the
+structure requiring a callback for that field, the constant includes
+all relevant overhead and only requires that you add the length of the field
+values.
+
+For example if a ``bytes`` field length is not specified in the options file,
+but is known to your code (``kMaxImageDataSize`` in this example being a
+constant in your own code), you can simply add it to the generated constant:
+
+.. code:: c++
+
+ #include "example_protos/store.pwpb.h"
+
+ const std::byte image_data[kMaxImageDataSize] = { ... };
+
+ Store::Message store{};
+ // Calling SetEncoder means we must always extend the buffer size.
+ store.image_data.SetEncoder([](Store::StreamEncoder& encoder) {
+ return encoder.WriteImageData(image_data);
+ });
+
+ std::byte buffer[Store::kMaxEncodedSizeBytes + kMaxImageDataSize];
+ Store::MemoryEncoder encoder(buffer);
+ const auto status = encoder.Write(store);
+
+ // Always check the encoder status or return values from Write calls.
+ if (!status.ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
+ }
+
+Or when using a variable number of repeated submessages, where the maximum
+number is known to your code but not to the proto, you can add the constants
+from one message type to another:
+
+.. code:: c++
+
+ #include "example_protos/person.pwpb.h"
+
+ Person::Message grandchild{};
+ // Calling SetEncoder means we must always extend the buffer size.
+ grandchild.grandparent.SetEncoder([](Person::StreamEncoder& encoder) {
+ PW_TRY(encoder.GetGrandparentEncoder().Write(maternal_grandma));
+ PW_TRY(encoder.GetGrandparentEncoder().Write(maternal_grandpa));
+ PW_TRY(encoder.GetGrandparentEncoder().Write(paternal_grandma));
+ PW_TRY(encoder.GetGrandparentEncoder().Write(paternal_grandpa));
+ return pw::OkStatus();
+ });
+
+ std::byte buffer[Person::kMaxEncodedSizeBytes +
+ Grandparent::kMaxEncodedSizeBytes * 4];
+ Person::MemoryEncoder encoder(buffer);
+ const auto status = encoder.Write(grandchild);
+
+ // Always check the encoder status or return values from Write calls.
+ if (!status.ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
+ }
+
+.. warning::
+ Encoding to a buffer that is insufficiently large will return
+ ``Status::ResourceExhausted()`` from ``Write`` calls, and from the
+ encoder's ``status()`` call. Always check the status of calls or the encoder,
+ as in the case of error, the encoded data will be invalid.
+
+Per-Field Writers and Readers
+=============================
+The middle level API is based around typed methods to write and read each
+field of the message directly to the final serialized form, again created
+through C++ code generation.
+
+Encoding
+--------
+Given the same message structure, in addition to the ``Write()`` method that
+accepts a message structure, the following additional methods are also
+generated in the typed ``StreamEncoder`` class.
+
+There are lightweight wrappers around the core implementation, calling the
+underlying methods with the correct field numbers and value types, and result
+in no additional binary code over correctly using the core implementation.
+
+.. code:: c++
+
+ class Customer::StreamEncoder : pw::protobuf::StreamEncoder {
+ public:
+ // Message Structure Writer.
+ pw::Status Write(const Customer::Message&);
+
+ // Per-Field Typed Writers.
+ pw::Status WriteAge(int32_t);
+
+ pw::Status WriteName(std::string_view);
+ pw::Status WriteName(const char*, size_t);
+
+ pw::Status WriteStatus(Customer::Status);
+ };
+
+So the same encoding method could be written as:
+
+.. code:: c++
+
+ #include "example_protos/customer.pwpb.h"
+
+ Status EncodeCustomer(Customer::StreamEncoder& encoder) {
+ PW_TRY(encoder.WriteAge(33));
+ PW_TRY(encoder.WriteName("Joe Bloggs"sv));
+ PW_TRY(encoder.WriteStatus(Customer::Status::INACTIVE));
+ }
+
+Pigweed's protobuf encoders encode directly to the wire format of a proto rather
+than staging information to a mutable datastructure. This means any writes of a
+value are final, and can't be referenced or modified as a later step in the
+encode process.
+
+Casting between generated StreamEncoder types
+=============================================
+pw_protobuf guarantees that all generated ``StreamEncoder`` classes can be
+converted among each other. It's also safe to convert any ``MemoryEncoder`` to
+any other ``StreamEncoder``.
+
+This guarantee exists to facilitate usage of protobuf overlays. Protobuf
+overlays are protobuf message definitions that deliberately ensure that
+fields defined in one message will not conflict with fields defined in other
+messages.
+
+For example:
+
+.. code::
+
+ // The first half of the overlaid message.
+ message BaseMessage {
+ uint32 length = 1;
+ reserved 2; // Reserved for Overlay
+ }
+
+ // OK: The second half of the overlaid message.
+ message Overlay {
+ reserved 1; // Reserved for BaseMessage
+ uint32 height = 2;
+ }
+
+ // OK: A message that overlays and bundles both types together.
+ message Both {
+ uint32 length = 1; // Defined independently by BaseMessage
+ uint32 height = 2; // Defined independently by Overlay
+ }
+
+ // BAD: Diverges from BaseMessage's definition, and can cause decode
+ // errors/corruption.
+ message InvalidOverlay {
+ fixed32 length = 1;
+ }
+
+The ``StreamEncoderCast<>()`` helper template reduces very messy casting into
+a much easier to read syntax:
+
+.. code:: c++
+
+ #include "pw_protobuf/encoder.h"
+ #include "pw_protobuf_test_protos/full_test.pwpb.h"
+
+ Result<ConstByteSpan> EncodeOverlaid(uint32_t height,
+ uint32_t length,
+ ConstByteSpan encode_buffer) {
+ BaseMessage::MemoryEncoder base(encode_buffer);
+
+ // Without StreamEncoderCast<>(), this line would be:
+ // Overlay::StreamEncoder& overlay =
+ // *static_cast<Overlay::StreamEncoder*>(
+ // static_cast<pw::protobuf::StreamEncoder*>(&base)
+ Overlay::StreamEncoder& overlay =
+ StreamEncoderCast<Overlay::StreamEncoder>(base);
+ if (!overlay.WriteHeight(height).ok()) {
+ return overlay.status();
+ }
+ if (!base.WriteLength(length).ok()) {
+ return base.status();
+ }
+ return ConstByteSpan(base);
+ }
+
+While this use case is somewhat uncommon, it's a core supported use case of
+pw_protobuf.
+
+.. warning::
+
+ Using this to convert one stream encoder to another when the messages
+ themselves do not safely overlay will result in corrupt protos. Be careful
+ when doing this as there's no compile-time way to detect whether or not two
+ messages are meant to overlay.
+
+Decoding
+--------
+For decoding, in addition to the ``Read()`` method that populates a message
+structure, the following additional methods are also generated in the typed
+``StreamDecoder`` class.
+
+.. code:: c++
+
+ class Customer::StreamDecoder : pw::protobuf::StreamDecoder {
+ public:
+ // Message Structure Reader.
+ pw::Status Read(Customer::Message&);
+
+ // Returns the identity of the current field.
+ ::pw::Result<Fields> Field();
+
+ // Per-Field Typed Readers.
+ pw::Result<int32_t> ReadAge();
+
+ pw::StatusWithSize ReadName(pw::span<char>);
+ BytesReader GetNameReader(); // Read name as a stream of bytes.
+
+ pw::Result<Customer::Status> ReadStatus();
+ };
+
+Complete and correct decoding requires looping through the fields, so is more
+complex than encoding or using the message structure.
+
+.. code:: c++
+
+ pw::Status DecodeCustomer(Customer::StreamDecoder& decoder) {
+ uint32_t age;
+ char name[32];
+ Customer::Status status;
+
+ while ((status = decoder.Next()).ok()) {
+ switch (decoder.Field().value()) {
+ case Customer::Fields::kAge: {
+ PW_TRY_ASSIGN(age, decoder.ReadAge());
+ break;
+ }
+ case Customer::Fields::kName: {
+ PW_TRY(decoder.ReadName(name));
+ break;
+ }
+ case Customer::Fields::kStatus: {
+ PW_TRY_ASSIGN(status, decoder.ReadStatus());
+ break;
+ }
+ }
+ }
+
+ return status.IsOutOfRange() ? OkStatus() : status;
+ }
+
+.. warning:: ``Fields::SNAKE_CASE`` is deprecated. Use ``Fields::kCamelCase``.
+
+ Transitional support for ``Fields::SNAKE_CASE`` will soon only be available by
+ explicitly setting the following GN variable in your project:
+ ``pw_protobuf_compiler_GENERATE_LEGACY_ENUM_SNAKE_CASE_NAMES=true``
+
+ This support will be removed after downstream projects have been migrated.
+
+
+Direct Writers and Readers
+==========================
+The lowest level API is provided by the core C++ implementation, and requires
+the caller to provide the correct field number and value types for encoding, or
+check the same when decoding.
+
+Encoding
+--------
+The two fundamental classes are ``MemoryEncoder`` which directly encodes a proto
+to an in-memory buffer, and ``StreamEncoder`` that operates on
+``pw::stream::Writer`` objects to serialize proto data.
+
+``StreamEncoder`` allows you encode a proto to something like ``pw::sys_io``
+without needing to build the complete message in memory
+
+To encode the same message we've used in the examples thus far, we would use
+the following parts of the core API:
+
+.. code:: c++
+
+ class pw::protobuf::StreamEncoder {
+ public:
+ Status WriteInt32(uint32_t field_number, int32_t);
+ Status WriteUint32(uint32_t field_number, uint32_t);
+
+ Status WriteString(uint32_t field_number, std::string_view);
+ Status WriteString(uint32_t field_number, const char*, size_t);
+
+ // And many other methods, see pw_protobuf/encoder.h
+ };
+
+Encoding the same message requires that we specify the field numbers, which we
+can hardcode, or supplement using the C++ code generated ``Fields`` enum, and
+cast the enumerated type.
+
+.. code:: c++
+
+ #include "pw_protobuf/encoder.h"
+ #include "example_protos/customer.pwpb.h"
+
+ Status EncodeCustomer(pw::protobuf::StreamEncoder& encoder) {
+ PW_TRY(encoder.WriteInt32(static_cast<uint32_t>(Customer::Fields::kAge),
+ 33));
+ PW_TRY(encoder.WriteString(static_cast<uint32_t>(Customer::Fields::kName),
+ "Joe Bloggs"sv));
+ PW_TRY(encoder.WriteUint32(
+ static_cast<uint32_t>(Customer::Fields::kStatus),
+ static_cast<uint32_t>(Customer::Status::INACTIVE)));
+ }
+
+Decoding
+--------
+``StreamDecoder`` reads data from a ``pw::stream::Reader`` and mirrors the API
+of the encoders.
+
+To decode the same message we would use the following parts of the core API:
+
+.. code:: c++
+
+ class pw::protobuf::StreamDecoder {
+ public:
+ // Returns the identity of the current field.
+ ::pw::Result<uint32_t> FieldNumber();
+
+ Result<int32_t> ReadInt32();
+ Result<uint32_t> ReadUint32();
+
+ StatusWithSize ReadString(pw::span<char>);
+
+ // And many other methods, see pw_protobuf/stream_decoder.h
+ };
+
+As with the typed per-field API, complete and correct decoding requires looping
+through the fields and checking the field numbers, along with casting types.
+
+.. code:: c++
+
+ pw::Status DecodeCustomer(pw::protobuf::StreamDecoder& decoder) {
+ uint32_t age;
+ char name[32];
+ Customer::Status status;
+
+ while ((status = decoder.Next()).ok()) {
+ switch (decoder.FieldNumber().value()) {
+ case static_cast<uint32_t>(Customer::Fields::kAge): {
+ PW_TRY_ASSIGN(age, decoder.ReadInt32());
+ break;
+ }
+ case static_cast<uint32_t>(Customer::Fields::kName): {
+ PW_TRY(decoder.ReadString(name));
+ break;
+ }
+ case static_cast<uint32_t>(Customer::Fields::kStatus): {
+ uint32_t status_value;
+ PW_TRY_ASSIGN(status_value, decoder.ReadUint32());
+ status = static_cast<Customer::Status>(status_value);
+ break;
+ }
+ }
+ }
+
+ return status.IsOutOfRange() ? OkStatus() : status;
+ }
+
+
+Handling of packages
+====================
+
+Package declarations in ``.proto`` files are converted to namespace
+declarations. Unlike ``protoc``'s native C++ codegen, pw_protobuf appends an
+additional ``::pwpb`` namespace after the user-specified package name: for
+example, ``package my.cool.project`` becomes ``namespace
+my::cool::project::pwpb``. We emit a different package name than stated, in
+order to avoid clashes for projects that link against multiple C++ proto
+libraries in the same library.
+
+..
+ TODO(b/258832150) Remove this section, if possible
+
+In some cases, pw_protobuf codegen may encounter external message references
+during parsing, where it is unable to resolve the package name of the message.
+In these situations, the codegen is instead forced to emit the package name as
+``pw::pwpb_xxx::my::cool::project``, where "pwpb_xxx" is the name of some
+unspecified private namespace. Users are expected to manually identify the
+intended namespace name of that symbol, as described above, and must not rely
+on any such private namespaces, even if they appear in codegen output.
+
+-------
+Codegen
+-------
+pw_protobuf codegen integration is supported in GN, Bazel, and CMake.
+
+This module's codegen is available through the ``*.pwpb`` sub-target of a
+``pw_proto_library`` in GN, CMake, and Bazel. See :ref:`pw_protobuf_compiler's
+documentation <module-pw_protobuf_compiler>` for more information on build
+system integration for pw_protobuf codegen.
+
+Example ``BUILD.gn``:
+
+.. code::
+
+ import("//build_overrides/pigweed.gni")
+
+ import("$dir_pw_build/target_types.gni")
+ import("$dir_pw_protobuf_compiler/proto.gni")
+
+ # This target controls where the *.pwpb.h headers end up on the include path.
+ # In this example, it's at "pet_daycare_protos/client.pwpb.h".
+ pw_proto_library("pet_daycare_protos") {
+ sources = [
+ "pet_daycare_protos/client.proto",
+ ]
+ }
+
+ pw_source_set("example_client") {
+ sources = [ "example_client.cc" ]
+ deps = [
+ ":pet_daycare_protos.pwpb",
+ dir_pw_bytes,
+ dir_pw_stream,
+ ]
+ }
-------------
Configuration
@@ -55,62 +637,644 @@ Configuration
| 5 bytes | 4,294,967,295 or < 4GiB (max uint32_t) |
+-------------------+----------------------------------------+
+Field Options
+=============
+``pw_protobuf`` supports the following field options for specifying
+protocol-level limitations, rather than code generation parameters (although
+they do influence code generation):
+
+
+* ``max_count``:
+ Maximum number of entries for repeated fields.
+
+* ``max_size``:
+ Maximum size of `bytes` or `string` fields.
+
+Even though other proto codegen implementations do not respect these field
+options, they can still compile protos which use these options. This is
+especially useful for host builds using upstream protoc code generation, where
+host software can use the reflection API to query for the options and validate
+messages comply with the specified limitations.
+
+.. code::
+
+ import "pw_protobuf_protos/field_options.proto";
+
+ message Demo {
+ string size_limited_string = 1 [(pw.protobuf.pwpb).max_size = 16];
+ };
+
+Options Files
+=============
+Code generation can be configured using a separate ``.options`` file placed
+alongside the relevant ``.proto`` file.
+
+The format of this file is a series of fully qualified field names, or patterns,
+followed by one or more options. Lines starting with ``#`` or ``//`` are
+comments, and blank lines are ignored.
+
+Example:
+
+.. code::
+
+ // Set an option for a specific field.
+ fuzzy_friends.Client.visit_dates max_count:16
+
+ // Set options for multiple fields by wildcard matching.
+ fuzzy_friends.Pet.* max_size:32
+
+ // Set multiple options in one go.
+ fuzzy_friends.Dog.paws max_count:4 fixed_count:true
+
+Options files should be listed as ``inputs`` when defining ``pw_proto_library``,
+e.g.
+
+.. code::
+
+ pw_proto_library("pet_daycare_protos") {
+ sources = [
+ "pet_daycare_protos/client.proto",
+ ]
+ inputs = [
+ "pet_daycare_protos/client.options",
+ ]
+ }
+
+Valid options are:
+
+* ``max_count``:
+ Maximum number of entries for repeated fields. When set, repeated scalar
+ fields will use the ``pw::Vector`` container type instead of a callback.
+
+* ``fixed_count``:
+ Specified with ``max_count`` to use a fixed length ``std::array`` container
+ instead of ``pw::Vector``.
+
+* ``max_size``:
+ Maximum size of `bytes` or `string` fields. When set, `bytes` fields use
+ ``pw::Vector`` and `string` fields use ``pw::InlineString`` instead of a
+ callback.
+
+* ``fixed_size``:
+ Specified with ``max_size`` to use a fixed length ``std::array`` container
+ instead of ``pw::Vector`` for `bytes` fields.
+
+* ``use_callback``:
+ Replaces the structure member for the field with a callback function even
+ where a simpler type could be used. This can be useful to ignore fields, to
+ stop decoding of complex structures if certain values are not as expected, or
+ to provide special handling for nested messages.
+
+.. admonition:: Rationale
+
+ The choice of a separate options file, over embedding options within the proto
+ file, are driven by the need for proto files to be shared across multiple
+ contexts.
+
+ A typical product would require the same proto be used on a hardware
+ component, running Pigweed; a server-side component, running on a cloud
+ platform; and an app component, running on a Phone OS.
+
+ While related, each of these will likely have different source projects and
+ build systems.
+
+ Were the Pigweed options embedded in the protos, it would be necessary for
+ both the cloud platform and Phone OS to be able to ``"import pigweed"`` ---
+ and equivalently for options relevant to their platforms in the embedded
+ software project.
+
+------------------
+Message Structures
+------------------
+The C++ code generator creates a ``struct Message`` for each protobuf message
+that can hold the set of values encoded by it, following these rules.
+
+* Scalar fields are represented by their appropriate C++ type.
+
+ .. code::
+
+ message Customer {
+ int32 age = 1;
+ uint32 birth_year = 2;
+ sint64 rating = 3;
+ bool is_active = 4;
+ }
+
+ .. code:: c++
+
+ struct Customer::Message {
+ int32_t age;
+ uint32_t birth_year;
+ int64_t rating;
+ bool is_active;
+ };
+
+* Enumerations are represented by a code generated namespaced proto enum.
+
+ .. code::
+
+ message Award {
+ enum Service {
+ BRONZE = 1;
+ SILVER = 2;
+ GOLD = 3;
+ }
+ Service service = 1;
+ }
+
+ .. code:: c++
+
+ enum class Award::Service : uint32_t {
+ BRONZE = 1,
+ SILVER = 2,
+ GOLD = 3,
+
+ kBronze = BRONZE,
+ kSilver = SILVER,
+ kGold = GOLD,
+ };
+
+ struct Award::Message {
+ Award::Service service;
+ };
+
+ Aliases to the enum values are also included in the "constant" style to match
+ your preferred coding style. These aliases have any common prefix to the
+ enumeration values removed, such that:
+
+ .. code::
+
+ enum Activity {
+ ACTIVITY_CYCLING = 1;
+ ACTIVITY_RUNNING = 2;
+ ACTIVITY_SWIMMING = 3;
+ }
+
+ .. code:: c++
+
+ enum class Activity : uint32_t {
+ ACTIVITY_CYCLING = 1,
+ ACTIVITY_RUNNING = 2,
+ ACTIVITY_SWIMMING = 3,
+
+ kCycling = ACTIVITY_CYCLING,
+ kRunning = ACTIVITY_RUNNING,
+ kSwimming = ACTIVITY_SWIMMING,
+ };
+
+
+* Nested messages are represented by their own ``struct Message`` provided that
+ a reference cycle does not exist.
+
+ .. code::
+
+ message Sale {
+ Customer customer = 1;
+ Product product = 2;
+ }
+
+ .. code:: c++
+
+ struct Sale::Message {
+ Customer::Message customer;
+ Product::Message product;
+ };
+
+* Optional scalar fields are represented by the appropriate C++ type wrapped in
+ ``std::optional``. Optional fields are not encoded when the value is not
+ present.
+
+ .. code::
+
+ message Loyalty {
+ optional int32 points = 1;
+ }
+
+ .. code:: c++
+
+ struct Loyalty::Message {
+ std::optional<int32_t> points;
+ };
+
+* Repeated scalar fields are represented by ``pw::Vector`` when the
+ ``max_count`` option is set for that field, or by ``std::array`` when both
+ ``max_count`` and ``fixed_count:true`` are set.
+
+ .. code::
+
+ message Register {
+ repeated int32 cash_in = 1;
+ repeated int32 cash_out = 2;
+ }
+
+ .. code::
+
+ Register.cash_in max_count:32 fixed_count:true
+ Register.cash_out max_count:64
+
+ .. code:: c++
+
+ struct Register::Message {
+ std::array<int32_t, 32> cash_in;
+ pw::Vector<int32_t, 64> cash_out;
+ };
+
+* `bytes` fields are represented by ``pw::Vector`` when the ``max_size`` option
+ is set for that field, or by ``std::array`` when both ``max_size`` and
+ ``fixed_size:true`` are set.
+
+ .. code::
+
+ message Product {
+ bytes sku = 1;
+ bytes serial_number = 2;
+ }
+
+ .. code::
+
+ Product.sku max_size:8 fixed_size:true
+ Product.serial_number max_size:64
+
+ .. code:: c++
+
+ struct Product::Message {
+ std::array<std::byte, 8> sku;
+ pw::Vector<std::byte, 64> serial_number;
+ };
+
+* `string` fields are represented by a :cpp:type:`pw::InlineString` when the
+ ``max_size`` option is set for that field. The string can hold up to
+ ``max_size`` characters, and is always null terminated. The null terminator is
+ not counted in ``max_size``.
+
+ .. code::
+
+ message Employee {
+ string name = 1;
+ }
+
+ .. code::
+
+ Employee.name max_size:128
+
+ .. code:: c++
+
+ struct Employee::Message {
+ pw::InlineString<128> name;
+ };
+
+* Nested messages with a dependency cycle, repeated scalar fields without a
+ ``max_count`` option set, `bytes` and `strings` fields without a ``max_size``
+ option set, and repeated nested messages, repeated `bytes`, and repeated
+ `strings` fields, are represented by a callback.
+
+ You set the callback to a custom function for encoding or decoding
+ before passing the structure to ``Write()`` or ``Read()`` appropriately.
+
+ .. code::
+
+ message Store {
+ Store nearest_store = 1;
+ repeated int32 employee_numbers = 2;
+ string driections = 3;
+ repeated string address = 4;
+ repeated Employee employees = 5;
+ }
+
+ .. code::
+
+ // No options set.
+
+ .. code:: c++
+
+ struct Store::Message {
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> nearest_store;
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> employee_numbers;
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> directions;
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> address;
+ pw::protobuf::Callback<Store::StreamEncoder, Store::StreamDecoder> employees;
+ };
+
+Message structures can be copied, but doing so will clear any assigned
+callbacks. To preserve functions applied to callbacks, ensure that the message
+structure is moved.
+
+Message structures can also be compared with each other for equality. This
+includes all repeated and nested fields represented by value types, but does not
+compare any field represented by a callback.
+
+Reserved-Word Conflicts
+=======================
+Generated symbols whose names conflict with reserved C++ keywords or
+standard-library macros are suffixed with underscores to avoid compilation
+failures. This can be seen below in ``Channel.operator``, which is mapped to
+``Channel::Message::operator_`` to avoid conflicting with the ``operator``
+keyword.
+
+.. code::
+
+ message Channel {
+ int32 bitrate = 1;
+ float signal_to_noise_ratio = 2;
+ Company operator = 3;
+ }
+
+.. code:: c++
+
+ struct Channel::Message {
+ int32_t bitrate;
+ float signal_to_noise_ratio;
+ Company::Message operator_;
+ };
+
+Similarly, as shown in the example below, some POSIX-signal names conflict with
+macros defined by the standard-library header ``<csignal>`` and therefore
+require underscore suffixes in the generated code. Note, however, that some
+signal names are left alone. This is because ``<csignal>`` only defines a subset
+of the POSIX signals as macros; the rest are perfectly valid identifiers that
+won't cause any problems unless the user defines custom macros for them. Any
+naming conflicts caused by user-defined macros are the user's responsibility
+(https://google.github.io/styleguide/cppguide.html#Preprocessor_Macros).
+
+.. code::
+
+ enum PosixSignal {
+ NONE = 0;
+ SIGHUP = 1;
+ SIGINT = 2;
+ SIGQUIT = 3;
+ SIGILL = 4;
+ SIGTRAP = 5;
+ SIGABRT = 6;
+ SIGFPE = 8;
+ SIGKILL = 9;
+ SIGSEGV = 11;
+ SIGPIPE = 13;
+ SIGALRM = 14;
+ SIGTERM = 15;
+ }
+
+.. code:: c++
+
+ enum class PosixSignal : uint32_t {
+ NONE = 0,
+ SIGHUP = 1,
+ SIGINT_ = 2,
+ SIGQUIT = 3,
+ SIGILL_ = 4,
+ SIGTRAP = 5,
+ SIGABRT_ = 6,
+ SIGFPE_ = 8,
+ SIGKILL = 9,
+ SIGSEGV_ = 11,
+ SIGPIPE = 13,
+ SIGALRM = 14,
+ SIGTERM_ = 15,
+
+ kNone = NONE,
+ kSighup = SIGHUP,
+ kSigint = SIGINT_,
+ kSigquit = SIGQUIT,
+ kSigill = SIGILL_,
+ kSigtrap = SIGTRAP,
+ kSigabrt = SIGABRT_,
+ kSigfpe = SIGFPE_,
+ kSigkill = SIGKILL,
+ kSigsegv = SIGSEGV_,
+ kSigpipe = SIGPIPE,
+ kSigalrm = SIGALRM,
+ kSigterm = SIGTERM_,
+ };
+
+Much like reserved words and macros, the names ``Message`` and ``Fields`` are
+suffixed with underscores in generated C++ code. This is to prevent name
+conflicts with the codegen internals if they're used in a nested context as in
+the example below.
+
+.. code::
+
+ message Function {
+ message Message {
+ string content = 1;
+ }
+
+ enum Fields {
+ NONE = 0;
+ COMPLEX_NUMBERS = 1;
+ INTEGERS_MOD_5 = 2;
+ MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE = 3;
+ OTHER = 4;
+ }
+
+ Message description = 1;
+ Fields domain = 2;
+ Fields codomain = 3;
+ }
+
+.. code::
+
+ Function.Message.content max_size:128
+
+.. code:: c++
+
+ struct Function::Message_::Message {
+ pw::InlineString<128> content;
+ };
+
+ enum class Function::Message_::Fields : uint32_t {
+ CONTENT = 1,
+ };
+
+ enum class Function::Fields_ uint32_t {
+ NONE = 0,
+ COMPLEX_NUMBERS = 1,
+ INTEGERS_MOD_5 = 2,
+ MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE = 3,
+ OTHER = 4,
+
+ kNone = NONE,
+ kComplexNumbers = COMPLEX_NUMBERS,
+ kIntegersMod5 = INTEGERS_MOD_5,
+ kMeromorphicFunctionsOnComplexPlane =
+ MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE,
+ kOther = OTHER,
+ };
+
+ struct Function::Message {
+ Function::Message_::Message description;
+ Function::Fields_ domain;
+ Function::Fields_ codomain;
+ };
+
+ enum class Function::Fields : uint32_t {
+ DESCRIPTION = 1,
+ DOMAIN = 2,
+ CODOMAIN = 3,
+ };
+
+.. warning::
+ Note that the C++ spec also reserves two categories of identifiers for the
+ compiler to use in ways that may conflict with generated code:
+
+ * Any identifier that contains two consecutive underscores anywhere in it.
+
+ * Any identifier that starts with an underscore followed by a capital letter.
+
+ Appending underscores to symbols in these categories wouldn't change the fact
+ that they match patterns reserved for the compiler, so the codegen does not
+ currently attempt to fix them. Such names will therefore result in
+ non-portable code that may or may not work depending on the compiler. These
+ naming patterns are of course strongly discouraged in any protobufs that will
+ be used with ``pw_protobuf`` codegen.
+
+Overhead
+========
+A single encoder and decoder is used for these structures, with a one-time code
+cost. When the code generator creates the ``struct Message``, it also creates
+a description of this structure that the shared encoder and decoder use.
+
+The cost of this description is a shared table for each protobuf message
+definition used, with four words per field within the protobuf message, and an
+addition word to store the size of the table.
+
--------
Encoding
--------
+The simplest way to use ``MemoryEncoder`` to encode a proto is from its code
+generated ``Message`` structure into an in-memory buffer.
-Usage
-=====
-Pigweed's protobuf encoders encode directly to the wire format of a proto rather
-than staging information to a mutable datastructure. This means any writes of a
-value are final, and can't be referenced or modified as a later step in the
-encode process.
+.. code:: c++
-MemoryEncoder
-=============
-A MemoryEncoder directly encodes a proto to an in-memory buffer.
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_bytes/span.h"
+ #include "pw_protobuf/encoder.h"
+ #include "pw_status/status_with_size.h"
+
+ // Writes a proto response to the provided buffer, returning the encode
+ // status and number of bytes written.
+ pw::StatusWithSize WriteProtoResponse(pw::ByteSpan response) {
+ MyProto::Message message{}
+ message.magic_number = 0x1a1a2b2b;
+ message.favorite_food = "cookies";
+ message.calories = 600;
+
+ // All proto writes are directly written to the `response` buffer.
+ MyProto::MemoryEncoder encoder(response);
+ encoder.Write(message);
+
+ return pw::StatusWithSize(encoder.status(), encoder.size());
+ }
+
+All fields of a message are written, including those initialized to their
+default values.
+
+Alternatively, for example if only a subset of fields are required to be
+encoded, fields can be written a field at a time through the code generated
+or lower-level APIs. This can be more convenient if finer grained control or
+other custom handling is required.
+
+.. code:: c++
-.. Code:: cpp
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_bytes/span.h"
+ #include "pw_protobuf/encoder.h"
+ #include "pw_status/status_with_size.h"
// Writes a proto response to the provided buffer, returning the encode
// status and number of bytes written.
- StatusWithSize WriteProtoResponse(ByteSpan response) {
+ pw::StatusWithSize WriteProtoResponse(pw::ByteSpan response) {
// All proto writes are directly written to the `response` buffer.
- MemoryEncoder encoder(response);
- encoder.WriteUint32(kMagicNumberField, 0x1a1a2b2b);
- encoder.WriteString(kFavoriteFood, "cookies");
- return StatusWithSize(encoder.status(), encoder.size());
+ MyProto::MemoryEncoder encoder(response);
+ encoder.WriteMagicNumber(0x1a1a2b2b);
+ encoder.WriteFavoriteFood("cookies");
+ // Only conditionally write calories.
+ if (on_diet) {
+ encoder.WriteCalories(600);
+ }
+ return pw::StatusWithSize(encoder.status(), encoder.size());
}
StreamEncoder
=============
-pw_protobuf's StreamEncoder class operates on pw::stream::Writer objects to
-serialized proto data. This means you can directly encode a proto to something
-like pw::sys_io without needing to build the complete message in memory first.
+``StreamEncoder`` is constructed with the destination stream, and a scratch
+buffer used to handle nested submessages.
-.. Code:: cpp
+.. code:: c++
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_bytes/span.h"
#include "pw_protobuf/encoder.h"
#include "pw_stream/sys_io_stream.h"
- #include "pw_bytes/span.h"
pw::stream::SysIoWriter sys_io_writer;
- pw::protobuf::StreamEncoder my_proto_encoder(sys_io_writer,
- pw::ByteSpan());
+ MyProto::StreamEncoder encoder(sys_io_writer, pw::ByteSpan());
// Once this line returns, the field has been written to the Writer.
- my_proto_encoder.WriteInt64(kTimestampFieldNumber, system::GetUnixEpoch());
+ encoder.WriteTimestamp(system::GetUnixEpoch());
// There's no intermediate buffering when writing a string directly to a
// StreamEncoder.
- my_proto_encoder.WriteString(kWelcomeMessageFieldNumber,
- "Welcome to Pigweed!");
- if (!my_proto_encoder.status().ok()) {
- PW_LOG_INFO("Failed to encode proto; %s", my_proto_encoder.status().str());
+ encoder.WriteWelcomeMessage("Welcome to Pigweed!");
+
+ if (!encoder.status().ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", encoder.status().str());
}
+Callbacks
+=========
+When using the ``Write()`` method with a ``struct Message``, certain fields may
+require a callback function be set to encode the values for those fields.
+Otherwise the values will be treated as an empty repeated field and not encoded.
+
+The callback is called with the cursor at the field in question, and passed
+a reference to the typed encoder that can write the required values to the
+stream or buffer.
+
+Callback implementations may use any level of API. For example a callback for a
+nested submessage (with a dependency cycle, or repeated) can be implemented by
+calling ``Write()`` on a nested encoder.
+
+.. code:: c++
+
+ Store::Message store{};
+ store.employees.SetEncoder([](Store::StreamEncoder& encoder) {
+ Employee::Message employee{};
+ // Populate `employee`.
+ return encoder.GetEmployeesEncoder().Write(employee);
+ ));
+
Nested submessages
==================
+Code generated ``GetFieldEncoder`` methods are provided that return a correctly
+typed ``StreamEncoder`` or ``MemoryEncoder`` for the message.
+
+.. code::
+
+ message Owner {
+ Animal pet = 1;
+ }
+
+Note that the accessor method is named for the field, while the returned encoder
+is named for the message type.
+
+.. cpp:function:: Animal::StreamEncoder Owner::StreamEncoder::GetPetEncoder()
+
+A lower-level API method returns an untyped encoder, which only provides the
+lower-level API methods. This can be cast to a typed encoder if needed.
+
+.. cpp:function:: pw::protobuf::StreamEncoder pw::protobuf::StreamEncoder::GetNestedEncoder(uint32_t field_number, EmptyEncoderBehavior empty_encoder_behavior = EmptyEncoderBehavior::kWriteFieldNumber)
+
+(The optional `empty_encoder_behavior` parameter allows the user to disable
+writing the tag number for the nested encoder, if no data was written to
+that nested decoder.)
+
+.. warning::
+ When a nested submessage is created, any use of the parent encoder that
+ created the nested encoder will trigger a crash. To resume using the parent
+ encoder, destroy the submessage encoder first.
+
+Buffering
+---------
Writing proto messages with nested submessages requires buffering due to
limitations of the proto format. Every proto submessage must know the size of
the submessage before its final serialization can begin. A streaming encoder can
@@ -119,71 +1283,240 @@ submessage data is buffered to this scratch buffer until the submessage is
finalized. Note that the contents of this scratch buffer is not necessarily
valid proto data, so don't try to use it directly.
-MemoryEncoder objects use the final destination buffer rather than relying on a
-scratch buffer. Note that this means your destination buffer might need
-additional space for overhead incurred by nesting submessages. The
-``MaxScratchBufferSize()`` helper function can be useful in estimating how much
-space to allocate to account for nested submessage encoding overhead.
+The code generation includes a ``kScratchBufferSizeBytes`` constant that
+represents the size of the largest submessage and all necessary overhead,
+excluding the contents of any field values which require a callback.
-.. Code:: cpp
+If a submessage field requires a callback, due to a dependency cycle, or a
+repeated field of unknown length, the size of the submessage cannot be included
+in the ``kScratchBufferSizeBytes`` constant. If you encode a submessage of this
+type (which you'll know you're doing because you set an encoder callback for it)
+simply add the appropriate structure's ``kMaxEncodedSizeBytes`` constant to the
+scratch buffer size to guarantee enough space.
+When calculating yourself, the ``MaxScratchBufferSize()`` helper function can
+also be useful in estimating how much space to allocate to account for nested
+submessage encoding overhead.
+
+.. code:: c++
+
+ #include "my_protos/pets.pwpb.h"
+ #include "pw_bytes/span.h"
#include "pw_protobuf/encoder.h"
#include "pw_stream/sys_io_stream.h"
- #include "pw_bytes/span.h"
pw::stream::SysIoWriter sys_io_writer;
// The scratch buffer should be at least as big as the largest nested
// submessage. It's a good idea to be a little generous.
- std::byte submessage_scratch_buffer[64];
+ std::byte submessage_scratch_buffer[Owner::kScratchBufferSizeBytes];
// Provide the scratch buffer to the proto encoder. The buffer's lifetime must
// match the lifetime of the encoder.
- pw::protobuf::StreamEncoder my_proto_encoder(sys_io_writer,
- submessage_scratch_buffer);
+ Owner::StreamEncoder owner_encoder(sys_io_writer, submessage_scratch_buffer);
{
- // Note that the parent encoder, my_proto_encoder, cannot be used until the
- // nested encoder, nested_encoder, has been destroyed.
- StreamEncoder nested_encoder =
- my_proto_encoder.GetNestedEncoder(kPetsFieldNumber);
+ // Note that the parent encoder, owner_encoder, cannot be used until the
+ // nested encoder, pet_encoder, has been destroyed.
+ Animal::StreamEncoder pet_encoder = owner_encoder.GetPetEncoder();
// There's intermediate buffering when writing to a nested encoder.
- nested_encoder.WriteString(kNameFieldNumber, "Spot");
- nested_encoder.WriteString(kPetTypeFieldNumber, "dog");
+ pet_encoder.WriteName("Spot");
+ pet_encoder.WriteType(Pet::Type::DOG);
// When this scope ends, the nested encoder is serialized to the Writer.
- // In addition, the parent encoder, my_proto_encoder, can be used again.
+ // In addition, the parent encoder, owner_encoder, can be used again.
}
// If an encode error occurs when encoding the nested messages, it will be
// reflected at the root encoder.
- if (!my_proto_encoder.status().ok()) {
- PW_LOG_INFO("Failed to encode proto; %s", my_proto_encoder.status().str());
+ if (!owner_encoder.status().ok()) {
+ PW_LOG_INFO("Failed to encode proto; %s", owner_encoder.status().str());
}
+MemoryEncoder objects use the final destination buffer rather than relying on a
+scratch buffer. The ``kMaxEncodedSizeBytes`` constant takes into account the
+overhead required for nesting submessages. If you calculate the buffer size
+yourself, your destination buffer might need additional space.
+
.. warning::
- When a nested submessage is created, any use of the parent encoder that
- created the nested encoder will trigger a crash. To resume using the parent
- encoder, destroy the submessage encoder first.
+ If the scratch buffer size is not sufficient, the encoding will fail with
+ ``Status::ResourceExhausted()``. Always check the results of ``Write`` calls
+ or the encoder status to ensure success, as otherwise the encoded data will
+ be invalid.
+
+Scalar Fields
+=============
+As shown, scalar fields are written using code generated ``WriteFoo``
+methods that accept the appropriate type and automatically writes the correct
+field number.
+
+.. cpp:function:: Status MyProto::StreamEncoder::WriteFoo(T)
+
+These can be freely intermixed with the lower-level API that provides a method
+per field type, requiring that the field number be passed in. The code
+generation includes a ``Fields`` enum to provide the field number values.
+
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteUint64(uint32_t field_number, uint64_t)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteSint64(uint32_t field_number, int64_t)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteInt64(uint32_t field_number, int64_t)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteUint32(uint32_t field_number, uint32_t)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteSint32(uint32_t field_number, int32_t)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteInt32(uint32_t field_number, int32_t)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteFixed64(uint32_t field_number, uint64_t)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteFixed32(uint32_t field_number, uint64_t)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteDouble(uint32_t field_number, double)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteFloat(uint32_t field_number, float)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteBool(uint32_t field_number, bool)
+
+The following two method calls are equivalent, where the first is using the
+code generated API, and the second implemented by hand.
+
+.. code:: c++
+
+ my_proto_encoder.WriteAge(42);
+ my_proto_encoder.WriteInt32(static_cast<uint32_t>(MyProto::Fields::kAge), 42);
Repeated Fields
-===============
-Repeated fields can be encoded a value at a time by repeatedly calling
-`WriteInt32` etc., or as a packed field by calling e.g. `WritePackedInt32` with
-a `std::span<Type>` or `WriteRepeatedInt32` with a `pw::Vector<Type>` (see
-:ref:`module-pw_containers` for details).
+---------------
+For repeated scalar fields, multiple code generated ``WriteFoos`` methods
+are provided.
+
+.. cpp:function:: Status MyProto::StreamEncoder::WriteFoos(T)
+
+ This writes a single unpacked value.
+
+.. cpp:function:: Status MyProto::StreamEncoder::WriteFoos(pw::span<const T>)
+.. cpp:function:: Status MyProto::StreamEncoder::WriteFoos(const pw::Vector<T>&)
+
+ These write a packed field containing all of the values in the provided span
+ or vector.
+
+These too can be freely intermixed with the lower-level API methods, both to
+write a single value, or to write packed values from either a ``pw::span`` or
+``pw::Vector`` source.
+
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedUint64(uint32_t field_number, pw::span<const uint64_t>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedUint64(uint32_t field_number, const pw::Vector<uint64_t>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedSint64(uint32_t field_number, pw::span<const int64_t>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedSint64(uint32_t field_number, const pw::Vector<int64_t>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedInt64(uint32_t field_number, pw::span<const int64_t>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedInt64(uint32_t field_number, const pw::Vector<int64_t>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedUint32(uint32_t field_number, pw::span<const uint32_t>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedUint32(uint32_t field_number, const pw::Vector<uint32_t>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedSint32(uint32_t field_number, pw::span<const int32_t>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedSint32(uint32_t field_number, const pw::Vector<int32_t>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedInt32(uint32_t field_number, pw::span<const int32_t>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedInt32(uint32_t field_number, const pw::Vector<int32_t>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedFixed64(uint32_t field_number, pw::span<const uint64_t>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedFixed64(uint32_t field_number, const pw::Vector<uint64_t>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedFixed32(uint32_t field_number, pw::span<const uint64_t>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedFixed32(uint32_t field_number, const pw::Vector<uint64_t>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedDouble(uint32_t field_number, pw::span<const double>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedDouble(uint32_t field_number, const pw::Vector<double>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedFloat(uint32_t field_number, pw::span<const float>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedFloat(uint32_t field_number, const pw::Vector<float>&)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WritePackedBool(uint32_t field_number, pw::span<const bool>)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteRepeatedBool(uint32_t field_number, const pw::Vector<bool>&)
+
+The following two method calls are equivalent, where the first is using the
+code generated API, and the second implemented by hand.
+
+.. code:: c++
+
+ constexpr std::array<int32_t, 5> numbers = { 4, 8, 15, 16, 23, 42 };
+
+ my_proto_encoder.WriteNumbers(numbers);
+ my_proto_encoder.WritePackedInt32(
+ static_cast<uint32_t>(MyProto::Fields::kNumbers),
+ numbers);
+
+Enumerations
+============
+Enumerations are written using code generated ``WriteEnum`` methods that
+accept the code generated enumeration as the appropriate type and automatically
+writes both the correct field number and corresponding value.
+
+.. cpp:function:: Status MyProto::StreamEncoder::WriteEnum(MyProto::Enum)
+
+To write enumerations with the lower-level API, you would need to cast both
+the field number and value to the ``uint32_t`` type.
+
+The following two methods are equivalent, where the first is code generated,
+and the second implemented by hand.
+
+.. code:: c++
+
+ my_proto_encoder.WriteAward(MyProto::Award::SILVER);
+ my_proto_encoder.WriteUint32(
+ static_cast<uint32_t>(MyProto::Fields::kAward),
+ static_cast<uint32_t>(MyProto::Award::SILVER));
+
+Repeated Fields
+---------------
+For repeated enum fields, multiple code generated ``WriteEnums`` methods
+are provided.
+
+.. cpp:function:: Status MyProto::StreamEncoder::WriteEnums(MyProto::Enums)
+
+ This writes a single unpacked value.
+
+.. cpp:function:: Status MyProto::StreamEncoder::WriteEnums(pw::span<const MyProto::Enums>)
+.. cpp:function:: Status MyProto::StreamEncoder::WriteEnums(const pw::Vector<MyProto::Enums>&)
+
+ These write a packed field containing all of the values in the provided span
+ or vector.
+
+Their use is as scalar fields.
+
+Strings
+=======
+Strings fields have multiple code generated methods provided.
+
+.. cpp:function:: Status MyProto::StreamEncoder::WriteName(std::string_view)
+.. cpp:function:: Status MyProto::StreamEncoder::WriteName(const char*, size_t)
+
+These can be freely intermixed with the lower-level API methods.
+
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteString(uint32_t field_number, std::string_view)
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteString(uint32_t field_number, const char*, size_t)
+
+A lower level API method is provided that can write a string from another
+stream.
+
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteStringFromStream(uint32_t field_number, stream::Reader& bytes_reader, size_t num_bytes, ByteSpan stream_pipe_buffer)
+
+ The payload for the value is provided through the stream::Reader
+ ``bytes_reader``. The method reads a chunk of the data from the reader using
+ the ``stream_pipe_buffer`` and writes it to the encoder.
+
+Bytes
+=====
+Bytes fields provide the ``WriteData`` code generated method.
+
+.. cpp:function:: Status MyProto::StreamEncoder::WriteData(ConstByteSpan)
+
+This can be freely intermixed with the lower-level API method.
+
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteBytes(uint32_t field_number, ConstByteSpan)
+
+And with the API method that can write bytes from another stream.
+
+.. cpp:function:: Status pw::protobuf::StreamEncoder::WriteBytesFromStream(uint32_t field_number, stream::Reader& bytes_reader, size_t num_bytes, ByteSpan stream_pipe_buffer)
+
+ The payload for the value is provided through the stream::Reader
+ ``bytes_reader``. The method reads a chunk of the data from the reader using
+ the ``stream_pipe_buffer`` and writes it to the encoder.
Error Handling
==============
-While individual write calls on a proto encoder return pw::Status objects, the
-encoder tracks all status returns and "latches" onto the first error
+While individual write calls on a proto encoder return ``pw::Status`` objects,
+the encoder tracks all status returns and "latches" onto the first error
encountered. This status can be accessed via ``StreamEncoder::status()``.
Proto map encoding utils
========================
-
Some additional helpers for encoding more complex but common protobuf
-submessages (e.g. map<string, bytes>) are provided in
+submessages (e.g. ``map<string, bytes>``) are provided in
``pw_protobuf/map_utils.h``.
.. Note::
@@ -192,49 +1525,76 @@ submessages (e.g. map<string, bytes>) are provided in
--------
Decoding
--------
-``pw_protobuf`` provides three decoder implementations, which are described
-below.
+The simplest way to use ``StreamDecoder`` is to decode a proto from the stream
+into its code generated ``Message`` structure.
-Decoder
-=======
-The ``Decoder`` class operates on an protobuf message located in a buffer in
-memory. It provides an iterator-style API for processing a message. Calling
-``Next()`` advances the decoder to the next proto field, which can then be read
-by calling the appropriate ``Read*`` function for the field number.
+.. code:: c++
-When reading ``bytes`` and ``string`` fields, the decoder returns a view of that
-field within the buffer; no data is copied out.
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_protobuf/stream_decoder.h"
+ #include "pw_status/status.h"
+ #include "pw_stream/stream.h"
-.. note::
+ pw::Status DecodeProtoFromStream(pw::stream::Reader& reader) {
+ MyProto::Message message{};
+ MyProto::StreamDecoder decoder(reader);
+ decoder.Read(message);
+ return decoder.status();
+ }
- ``pw::protobuf::Decoder`` will soon be renamed ``pw::protobuf::MemoryDecoder``
- for clarity and consistency.
+In the case of errors, the decoding will stop and return with the cursor on the
+field that caused the error. It is valid in some cases to inspect the error and
+continue decoding by calling ``Read()`` again on the same structure, or fall
+back to using the lower-level APIs.
-.. code-block:: c++
+Unknown fields in the wire encoding are skipped.
- #include "pw_protobuf/decoder.h"
+If finer-grained control is required, the ``StreamDecoder`` class provides an
+iterator-style API for processing a message a field at a time where calling
+``Next()`` advances the decoder to the next proto field.
+
+.. cpp:function:: Status pw::protobuf::StreamDecoder::Next()
+
+In the code generated classes the ``Field()`` method returns the current field
+as a typed ``Fields`` enumeration member, while the lower-level API provides a
+``FieldNumber()`` method that returns the number of the field.
+
+.. cpp:function:: Result<MyProto::Fields> MyProto::StreamDecoder::Field()
+.. cpp:function:: Result<uint32_t> pw::protobuf::StreamDecoder::FieldNumber()
+
+.. code:: c++
+
+ #include "my_protos/my_proto.pwpb.h"
+ #include "pw_protobuf/strema_decoder.h"
+ #include "pw_status/status.h"
#include "pw_status/try.h"
+ #include "pw_stream/stream.h"
- pw::Status DecodeProtoFromBuffer(std::span<const std::byte> buffer) {
- pw::protobuf::Decoder decoder(buffer);
+ pw::Status DecodeProtoFromStream(pw::stream::Reader& reader) {
+ MyProto::StreamDecoder decoder(reader);
pw::Status status;
- uint32_t uint32_field;
- std::string_view string_field;
+ uint32_t age;
+ char name[16];
// Iterate over the fields in the message. A return value of OK indicates
// that a valid field has been found and can be read. When the decoder
// reaches the end of the message, Next() will return OUT_OF_RANGE.
// Other return values indicate an error trying to decode the message.
while ((status = decoder.Next()).ok()) {
- switch (decoder.FieldNumber()) {
- case 1:
- PW_TRY(decoder.ReadUint32(&uint32_field));
+ // Field() returns a Result<Fields> as it may fail sometimes.
+ // However, Field() is guaranteed to be valid after a call to Next()
+ // that returns OK, so the value can be used directly here.
+ switch (decoder.Field().value()) {
+ case MyProto::Fields::kAge: {
+ PW_TRY_ASSIGN(age, decoder.ReadAge());
break;
- case 2:
- // The passed-in string_view will point to the contents of the string
- // field within the buffer.
- PW_TRY(decoder.ReadString(&string_field));
+ }
+ case MyProto::Fields::kName:
+ // The string field is copied into the provided buffer. If the buffer
+ // is too small to fit the string, RESOURCE_EXHAUSTED is returned and
+ // the decoder is not advanced, allowing the field to be re-read.
+ PW_TRY(decoder.ReadName(name));
break;
}
}
@@ -244,130 +1604,372 @@ field within the buffer; no data is copied out.
return status.IsOutOfRange() ? OkStatus() : status;
}
-StreamDecoder
-=============
-Sometimes, a serialized protobuf message may be too large to fit into an
-in-memory buffer. To faciliate working with that type of data, ``pw_protobuf``
-provides a ``StreamDecoder`` which reads data from a ``pw::stream::Reader``.
+Callbacks
+=========
+When using the ``Read()`` method with a ``struct Message``, certain fields may
+require a callback function be set, otherwise a ``DataLoss`` error will be
+returned should that field be encountered in the wire encoding.
-.. admonition:: When to use a stream decoder
+The callback is called with the cursor at the field in question, and passed
+a reference to the typed decoder that can examine the field and be used to
+decode it.
- The ``StreamDecoder`` should only be used in cases where the protobuf data
- cannot be read directly from a buffer. It is unadvisable to use a
- ``StreamDecoder`` with a ``MemoryStream`` --- the decoding operations will be
- far less efficient than the ``Decoder``, which is optimized for in-memory
- messages.
+Callback implementations may use any level of API. For example a callback for a
+nested submessage (with a dependency cycle, or repeated) can be implemented by
+calling ``Read()`` on a nested decoder.
-The general usage of a ``StreamDecoder`` is similar to the basic ``Decoder``,
-with the exception of ``bytes`` and ``string`` fields, which must be copied out
-of the stream into a provided buffer.
+.. code:: c++
-.. code-block:: c++
+ Store::Message store{};
+ store.employees.SetDecoder([](Store::StreamDecoder& decoder) {
+ PW_ASSERT(decoder.Field().value() == Store::Fields::kEmployees);
- #include "pw_protobuf/decoder.h"
- #include "pw_status/try.h"
+ Employee::Message employee{};
+ // Set any callbacks on `employee`.
+ PW_TRY(decoder.GetEmployeesDecoder().Read(employee));
+ // Do things with `employee`.
+ return OkStatus();
+ ));
- pw::Status DecodeProtoFromStream(pw::stream::Reader& reader) {
- pw::protobuf::StreamDecoder decoder(reader);
- pw::Status status;
+Nested submessages
+==================
+Code generated ``GetFieldDecoder`` methods are provided that return a correctly
+typed ``StreamDecoder`` for the message.
- uint32_t uint32_field;
- char string_field[16];
+.. code::
- // Iterate over the fields in the message. A return value of OK indicates
- // that a valid field has been found and can be read. When the decoder
- // reaches the end of the message, Next() will return OUT_OF_RANGE.
- // Other return values indicate an error trying to decode the message.
- while ((status = decoder.Next()).ok()) {
- // FieldNumber() returns a Result<uint32_t> as it may fail sometimes.
- // However, FieldNumber() is guaranteed to be valid after a call to Next()
- // that returns OK, so the value can be used directly here.
- switch (decoder.FieldNumber().value()) {
- case 1: {
- Result<uint32_t> result = decoder.ReadUint32();
- if (result.ok()) {
- uint32_field = result.value();
- }
- break;
- }
+ message Owner {
+ Animal pet = 1;
+ }
- case 2:
- // The string field is copied into the provided buffer. If the buffer
- // is too small to fit the string, RESOURCE_EXHAUSTED is returned and
- // the decoder is not advanced, allowing the field to be re-read.
- PW_TRY(decoder.ReadString(string_field));
- break;
+As with encoding, note that the accessor method is named for the field, while
+the returned decoder is named for the message type.
+
+.. cpp:function:: Animal::StreamDecoder Owner::StreamDecoder::GetPetDecoder()
+
+A lower-level API method returns an untyped decoder, which only provides the
+lower-level API methods. This can be moved to a typed decoder later.
+
+.. cpp:function:: pw::protobuf::StreamDecoder pw::protobuf::StreamDecoder::GetNestedDecoder()
+
+.. warning::
+ When a nested submessage is being decoded, any use of the parent decoder that
+ created the nested decoder will trigger a crash. To resume using the parent
+ decoder, destroy the submessage decoder first.
+
+
+.. code:: c++
+
+ case Owner::Fields::kPet: {
+ // Note that the parent decoder, owner_decoder, cannot be used until the
+ // nested decoder, pet_decoder, has been destroyed.
+ Animal::StreamDecoder pet_decoder = owner_decoder.GetPetDecoder();
+
+ while ((status = pet_decoder.Next()).ok()) {
+ switch (pet_decoder.Field().value()) {
+ // Decode pet fields...
}
}
- // Do something with the fields...
+ // When this scope ends, the nested decoder is destroyed and the
+ // parent decoder, owner_decoder, can be used again.
+ break;
+ }
- return status.IsOutOfRange() ? OkStatus() : status;
+Scalar Fields
+=============
+Scalar fields are read using code generated ``ReadFoo`` methods that return the
+appropriate type and assert that the correct field number ie being read.
+
+.. cpp:function:: Result<T> MyProto::StreamDecoder::ReadFoo()
+
+These can be freely intermixed with the lower-level API that provides a method
+per field type, requiring that the caller first check the field number.
+
+.. cpp:function:: Result<uint64_t> pw::protobuf::StreamDecoder::ReadUint64()
+.. cpp:function:: Result<int64_t> pw::protobuf::StreamDecoder::ReadSint64()
+.. cpp:function:: Result<int64_t> pw::protobuf::StreamDecoder::ReadInt64()
+.. cpp:function:: Result<uint32_t> pw::protobuf::StreamDecoder::ReadUint32()
+.. cpp:function:: Result<int32_t> pw::protobuf::StreamDecoder::ReadSint32()
+.. cpp:function:: Result<int32_t> pw::protobuf::StreamDecoder::ReadInt32()
+.. cpp:function:: Result<uint64_t> pw::protobuf::StreamDecoder::ReadFixed64()
+.. cpp:function:: Result<uint64_t> pw::protobuf::StreamDecoder::ReadFixed32()
+.. cpp:function:: Result<double> pw::protobuf::StreamDecoder::ReadDouble()
+.. cpp:function:: Result<float> pw::protobuf::StreamDecoder::ReadFloat()
+.. cpp:function:: Result<bool> pw::protobuf::StreamDecoder::ReadBool()
+
+The following two code snippets are equivalent, where the first uses the code
+generated API, and the second implemented by hand.
+
+.. code:: c++
+
+ pw::Result<int32_t> age = my_proto_decoder.ReadAge();
+
+.. code:: c++
+
+ PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
+ static_cast<uint32_t>(MyProto::Fields::kAge));
+ pw::Result<int32_t> my_proto_decoder.ReadInt32();
+
+Repeated Fields
+---------------
+For repeated scalar fields, multiple code generated ``ReadFoos`` methods
+are provided.
+
+.. cpp:function:: Result<T> MyProto::StreamDecoder::ReadFoos()
+
+ This reads a single unpacked value.
+
+.. cpp:function:: StatusWithSize MyProto::StreamDecoder::ReadFoos(pw::span<T>)
+
+ This reads a packed field containing all of the values into the provided span.
+
+.. cpp:function:: Status MyProto::StreamDecoder::ReadFoos(pw::Vector<T>&)
+
+ Protobuf encoders are permitted to choose either repeating single unpacked
+ values, or a packed field, including splitting repeated fields up into
+ multiple packed fields.
+
+ This method supports either format, appending values to the provided
+ ``pw::Vector``.
+
+These too can be freely intermixed with the lower-level API methods, to read a
+single value, a field of packed values into a ``pw::span``, or support both
+formats appending to a ``pw::Vector`` source.
+
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedUint64(pw::span<uint64_t>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedUint64(pw::Vector<uint64_t>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedSint64(pw::span<int64_t>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedSint64(pw::Vector<int64_t>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedInt64(pw::span<int64_t>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedInt64(pw::Vector<int64_t>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedUint32(pw::span<uint32_t>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedUint32(pw::Vector<uint32_t>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedSint32(pw::span<int32_t>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedSint32(pw::Vector<int32_t>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedInt32(pw::span<int32_t>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedInt32(pw::Vector<int32_t>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedFixed64(pw::span<uint64_t>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedFixed64(pw::Vector<uint64_t>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedFixed32(pw::span<uint64_t>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedFixed32(pw::Vector<uint64_t>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedDouble(pw::span<double>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedDouble(pw::Vector<double>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedFloat(pw::span<float>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedFloat(pw::Vector<float>&)
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadPackedBool(pw::span<bool>)
+.. cpp:function:: Status pw::protobuf::StreamDecoder::ReadRepeatedBool(pw::Vector<bool>&)
+
+The following two code blocks are equivalent, where the first uses the code
+generated API, and the second is implemented by hand.
+
+.. code:: c++
+
+ pw::Vector<int32_t, 8> numbers;
+
+ my_proto_decoder.ReadNumbers(numbers);
+
+.. code:: c++
+
+ pw::Vector<int32_t, 8> numbers;
+
+ PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
+ static_cast<uint32_t>(MyProto::Fields::kNumbers));
+ my_proto_decoder.ReadRepeatedInt32(numbers);
+
+Enumerations
+============
+``pw_protobuf`` generates a few functions for working with enumerations.
+Most importantly, enumerations are read using generated ``ReadEnum`` methods
+that return the enumeration as the appropriate generated type.
+
+.. cpp:function:: Result<MyProto::Enum> MyProto::StreamDecoder::ReadEnum()
+
+ Decodes an enum from the stream.
+
+.. cpp:function:: constexpr bool MyProto::IsValidEnum(MyProto::Enum value)
+
+ Validates the value encoded in the wire format against the known set of
+ enumerates.
+
+.. cpp:function:: constexpr const char* MyProto::EnumToString(MyProto::Enum value)
+
+ Returns the string representation of the enum value. For example,
+ ``FooToString(Foo::kBarBaz)`` returns ``"BAR_BAZ"``. Returns the empty string
+ if the value is not a valid value.
+
+To read enumerations with the lower-level API, you would need to cast the
+retured value from the ``uint32_t``.
+
+The following two code blocks are equivalent, where the first is using the code
+generated API, and the second implemented by hand.
+
+.. code-block:: c++
+
+ pw::Result<MyProto::Award> award = my_proto_decoder.ReadAward();
+ if (!MyProto::IsValidAward(award)) {
+ PW_LOG_DBG("Unknown award");
}
+.. code-block:: c++
+
+ PW_ASSERT(my_proto_decoder.FieldNumber().value() ==
+ static_cast<uint32_t>(MyProto::Fields::kAward));
+ pw::Result<uint32_t> award_value = my_proto_decoder.ReadUint32();
+ if (award_value.ok()) {
+ MyProto::Award award = static_cast<MyProto::Award>(award_value);
+ }
+
+Repeated Fields
+---------------
+For repeated enum fields, multiple code generated ``ReadEnums`` methods
+are provided.
+
+.. cpp:function:: Result<MyProto::Enums> MyProto::StreamDecoder::ReadEnums()
+
+ This reads a single unpacked value.
+
+.. cpp:function:: StatusWithSize MyProto::StreamDecoder::ReadEnums(pw::span<MyProto::Enums>)
+
+ This reads a packed field containing all of the checked values into the
+ provided span.
+
+.. cpp:function:: Status MyProto::StreamDecoder::ReadEnums(pw::Vector<MyProto::Enums>&)
+
+ This method supports either repeated unpacked or packed formats, appending
+ checked values to the provided ``pw::Vector``.
+
+Their use is as scalar fields.
+
+Strings
+=======
+Strings fields provide a code generated method to read the string into the
+provided span. Since the span is updated with the size of the string, the string
+is not automatically null-terminated. :ref:`module-pw_string` provides utility
+methods to copy string data from spans into other targets.
+
+.. cpp:function:: StatusWithSize MyProto::StreamDecoder::ReadName(pw::span<char>)
+
+An additional code generated method is provided to return a nested
+``BytesReader`` to access the data as a stream. As with nested submessage
+decoders, any use of the parent decoder that created the bytes reader will
+trigger a crash. To resume using the parent decoder, destroy the bytes reader
+first.
+
+.. cpp:function:: pw::protobuf::StreamDecoder::BytesReader MyProto::StreamDecoder::GetNameReader()
+
+These can be freely intermixed with the lower-level API method:
+
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadString(pw::span<char>)
+
+The lower-level ``GetBytesReader()`` method can also be used to read string data
+as bytes.
+
+Bytes
+=====
+Bytes fields provide the ``WriteData`` code generated method to read the bytes
+into the provided span.
+
+.. cpp:function:: StatusWithSize MyProto::StreamDecoder::ReadData(ByteSpan)
+
+An additional code generated method is provided to return a nested
+``BytesReader`` to access the data as a stream. As with nested submessage
+decoders, any use of the parent decoder that created the bytes reader will
+trigger a crash. To resume using the parent decoder, destroy the bytes reader
+first.
+
+.. cpp:function:: pw::protobuf::StreamDecoder::BytesReader MyProto::StreamDecoder::GetDataReader()
+
+These can be freely intermixed with the lower-level API methods.
+
+.. cpp:function:: StatusWithSize pw::protobuf::StreamDecoder::ReadBytes(ByteSpan)
+.. cpp:function:: pw::protobuf::StreamDecoder::BytesReader pw::protobuf::StreamDecoder::GetBytesReader()
+
+The ``BytesReader`` supports seeking only if the ``StreamDecoder``'s reader
+supports seeking.
+
+Error Handling
+==============
+While individual read calls on a proto decoder return ``pw::Result``,
+``pw::StatusWithSize``, or ``pw::Status`` objects, the decoder tracks all status
+returns and "latches" onto the first error encountered. This status can be
+accessed via ``StreamDecoder::status()``.
+
+Length Limited Decoding
+=======================
Where the length of the protobuf message is known in advance, the decoder can
be prevented from reading from the stream beyond the known bounds by specifying
the known length to the decoder:
-.. code-block:: c++
+.. code:: c++
pw::protobuf::StreamDecoder decoder(reader, message_length);
When a decoder constructed in this way goes out of scope, it will consume any
-remaining bytes in `message_length` allowing the next `Read` to be data after
-the protobuf, even when it was not fully parsed.
+remaining bytes in ``message_length`` allowing the next ``Read()`` on the stream
+to be data after the protobuf, even when it was not fully parsed.
+
+-----------------
+In-memory Decoder
+-----------------
+The separate ``Decoder`` class operates on an protobuf message located in a
+buffer in memory. It is more efficient than the ``StreamDecoder`` in cases
+where the complete protobuf data can be stored in memory. The tradeoff of this
+efficiency is that no code generation is provided, so all decoding must be
+performed by hand.
+
+As ``StreamDecoder``, it provides an iterator-style API for processing a
+message. Calling ``Next()`` advances the decoder to the next proto field, which
+can then be read by calling the appropriate ``Read*`` function for the field
+number.
-The ``StreamDecoder`` can also return a ``StreamDecoder::BytesReader`` for
-reading bytes fields, avoiding the need to copy data out directly.
-
-.. code-block:: c++
-
- if (decoder.FieldNumber() == 3) {
- // bytes my_bytes_field = 3;
- pw::protobuf::StreamDecoder::BytesReader bytes_reader =
- decoder.GetBytesReader();
-
- // Read data incrementally through the bytes_reader. While the reader is
- // active, any attempts to use the decoder will result in a crash. When the
- // reader goes out of scope, it will close itself and reactive the decoder.
- }
+When reading ``bytes`` and ``string`` fields, the decoder returns a view of that
+field within the buffer; no data is copied out.
-This reader supports seeking only if the ``StreamDecoder``'s reader supports
-seeking.
+.. code:: c++
-If the current field is a nested protobuf message, the ``StreamDecoder`` can
-provide a decoder for the nested message. While the nested decoder is active,
-its parent decoder cannot be used.
+ #include "pw_protobuf/decoder.h"
+ #include "pw_status/try.h"
-.. code-block:: c++
+ pw::Status DecodeProtoFromBuffer(pw::span<const std::byte> buffer) {
+ pw::protobuf::Decoder decoder(buffer);
+ pw::Status status;
- if (decoder.FieldNumber() == 4) {
- pw::protobuf::StreamDecoder nested_decoder = decoder.GetNestedDecoder();
+ uint32_t uint32_field;
+ std::string_view string_field;
- while (nested_decoder.Next().ok()) {
- // Process the nested message.
+ // Iterate over the fields in the message. A return value of OK indicates
+ // that a valid field has been found and can be read. When the decoder
+ // reaches the end of the message, Next() will return OUT_OF_RANGE.
+ // Other return values indicate an error trying to decode the message.
+ while ((status = decoder.Next()).ok()) {
+ switch (decoder.FieldNumber()) {
+ case 1:
+ PW_TRY(decoder.ReadUint32(&uint32_field));
+ break;
+ case 2:
+ // The passed-in string_view will point to the contents of the string
+ // field within the buffer.
+ PW_TRY(decoder.ReadString(&string_field));
+ break;
+ }
}
- // Once the nested decoder goes out of scope, it closes itself, and the
- // parent decoder can be used again.
+ // Do something with the fields...
+
+ return status.IsOutOfRange() ? OkStatus() : status;
}
-Repeated Fields
---------------
-The ``StreamDecoder`` supports two encoded forms of repeated fields: value at a
-time, by repeatedly calling `ReadInt32` etc., and packed fields by calling
-e.g. `ReadPackedInt32`.
+Message Decoder
+---------------
-Since protobuf encoders are permitted to choose either format, including
-splitting repeated fields up into multiple packed fields, ``StreamDecoder``
-also provides method `ReadRepeatedInt32` etc. methods that accept a
-``pw::Vector`` (see :ref:`module-pw_containers` for details). These methods
-correctly extend the vector for either encoding.
+.. note::
-Message
-=======
+ ``pw::protobuf::Message`` is unrelated to the codegen ``struct Message``
+ used with ``StreamDecoder``.
-The module implements a message parsing class ``Message``, in
+The module implements a message parsing helper class ``Message``, in
``pw_protobuf/message.h``, to faciliate proto message parsing and field access.
The class provides interfaces for searching fields in a proto message and
creating helper classes for it according to its interpreted field type, i.e.
@@ -376,7 +1978,7 @@ uint32, bytes, string, map<>, repeated etc. The class works on top of
message access. The following gives examples for using the class to process
different fields in a proto message:
-.. code-block:: c++
+.. code:: c++
// Consider the proto messages defined as follows:
//
@@ -501,7 +2103,7 @@ is recommended to use the following for-range style to iterate and process
single fields directly.
-.. code-block:: c++
+.. code:: c++
for (Message::Field field : message) {
// Check status
@@ -528,166 +2130,6 @@ single fields directly.
.. Note::
The helper API are currently in-development and may not remain stable.
--------
-Codegen
--------
-
-pw_protobuf codegen integration is supported in GN, Bazel, and CMake.
-The codegen is a light wrapper around the ``StreamEncoder``, ``MemoryEncoder``,
-and ``StreamDecoder`` objects, providing named helper functions to write and
-read proto fields rather than requiring that field numbers are directly passed
-to an encoder.
-
-All generated messages provide a ``Fields`` enum that can be used directly for
-out-of-band encoding, or with the ``pw::protobuf::Decoder``.
-
-This module's codegen is available through the ``*.pwpb`` sub-target of a
-``pw_proto_library`` in GN, CMake, and Bazel. See :ref:`pw_protobuf_compiler's
-documentation <module-pw_protobuf_compiler>` for more information on build
-system integration for pw_protobuf codegen.
-
-Example ``BUILD.gn``:
-
-.. Code:: none
-
- import("//build_overrides/pigweed.gni")
-
- import("$dir_pw_build/target_types.gni")
- import("$dir_pw_protobuf_compiler/proto.gni")
-
- # This target controls where the *.pwpb.h headers end up on the include path.
- # In this example, it's at "pet_daycare_protos/client.pwpb.h".
- pw_proto_library("pet_daycare_protos") {
- sources = [
- "pet_daycare_protos/client.proto",
- ]
- }
-
- pw_source_set("example_client") {
- sources = [ "example_client.cc" ]
- deps = [
- ":pet_daycare_protos.pwpb",
- dir_pw_bytes,
- dir_pw_stream,
- ]
- }
-
- pw_source_set("example_server") {
- sources = [ "example_server.cc" ]
- deps = [
- ":pet_daycare_protos.pwpb",
- dir_pw_bytes,
- dir_pw_stream,
- ]
- }
-
-Example ``pet_daycare_protos/client.proto``:
-
-.. Code:: none
-
- syntax = "proto3";
- // The proto package controls the namespacing of the codegen. If this package
- // were fuzzy.friends, the namespace for codegen would be fuzzy::friends::*.
- package fuzzy_friends;
-
- message Pet {
- string name = 1;
- string pet_type = 2;
- }
-
- message Client {
- repeated Pet pets = 1;
- }
-
-Example ``example_client.cc``:
-
-.. Code:: cpp
-
- #include "pet_daycare_protos/client.pwpb.h"
- #include "pw_protobuf/encoder.h"
- #include "pw_stream/sys_io_stream.h"
- #include "pw_bytes/span.h"
-
- pw::stream::SysIoWriter sys_io_writer;
- std::byte submessage_scratch_buffer[64];
- // The constructor is the same as a pw::protobuf::StreamEncoder.
- fuzzy_friends::Client::StreamEncoder client(sys_io_writer,
- submessage_scratch_buffer);
- {
- fuzzy_friends::Pet::StreamEncoder pet1 = client.GetPetsEncoder();
- pet1.WriteName("Spot");
- pet1.WritePetType("dog");
- }
-
- {
- fuzzy_friends::Pet::StreamEncoder pet2 = client.GetPetsEncoder();
- pet2.WriteName("Slippers");
- pet2.WritePetType("rabbit");
- }
-
- if (!client.status().ok()) {
- PW_LOG_INFO("Failed to encode proto; %s", client.status().str());
- }
-
-Example ``example_server.cc``:
-
-.. Code:: cpp
-
- #include "pet_daycare_protos/client.pwpb.h"
- #include "pw_protobuf/stream_decoder.h"
- #include "pw_stream/sys_io_stream.h"
- #include "pw_bytes/span.h"
-
- pw::stream::SysIoReader sys_io_reader;
- // The constructor is the same as a pw::protobuf::StreamDecoder.
- fuzzy_friends::Client::StreamDecoder client(sys_io_reader);
- while (client.Next().ok()) {
- switch (client.Field().value) {
- case fuzzy_friends::Client::Fields::PET: {
- std::array<char, 32> name{};
- std::array<char, 32> pet_type{};
-
- fuzzy_friends::Pet::StreamDecoder pet = client.GetPetsDecoder();
- while (pet.Next().ok()) {
- switch (pet.Field().value) {
- case fuzzy_friends::Pet::NAME:
- pet.ReadName(name);
- break;
- case fuzzy_friends::Pet::TYPE:
- pet.ReadPetType(pet_type);
- break;
- }
- }
-
- break;
- }
- }
- }
-
- if (!client.status().ok()) {
- PW_LOG_INFO("Failed to decode proto; %s", client.status().str());
- }
-
-Enums
-=====
-Namespaced proto enums are generated, and used as the arguments when writing
-enum fields of a proto message. When reading enum fields of a proto message,
-the enum value is validated and returned as the correct type, or
-``Status::DataLoss()`` if the decoded enum value was not given in the proto.
-
-Repeated Fields
-===============
-For encoding, the wrappers provide a `WriteFieldName` method with three
-signatures. One that encodes a single value at a time, one that encodes a packed
-field from a `std::span<Type>`, and one that encodes a packed field from a
-`pw::Vector<Type>`. All three return `Status`.
-
-For decoding, the wrappers provide a `ReadFieldName` method with three
-signatures. One that reads a single value at a time, returning a `Result<Type>`,
-one that reads a packed field into a `std::span<Type>` and returning a
-`StatusWithSize`, and one that supports all formats reading into a
-`pw::Vector<Type>` and returning `Status`.
-
-----------
Size report
-----------
@@ -699,7 +2141,7 @@ This report demonstrates the size of using the entire decoder with all of its
decode methods and a decode callback for a proto message containing each of the
protobuf field types.
-.. include:: size_report/decoder_full
+.. include:: size_report/decoder_partial
Incremental size report
@@ -722,6 +2164,10 @@ the specified type, given a particular key and, for variable length fields
(varint or delimited), a value. The ``SizeOf*Field`` functions calculate the
encoded size of fields with a particular wire format (delimited, varint).
+In the rare event that you need to know the serialized size of a field's tag
+(field number and wire type), you can use ``TagSizeBytes()`` to calculate the
+tag size for a given field number.
+
--------------------------
Available protobuf modules
--------------------------
@@ -742,7 +2188,7 @@ Contains the enum for pw::Status.
file. Instead, the StatusCodes should be converted to the Status type in the
language. In C++, this would be:
- .. code-block:: c++
+ .. code:: c++
// Reading from a proto
pw::Status status = static_cast<pw::Status::Code>(proto.status_field));
diff --git a/pw_protobuf/encoder.cc b/pw_protobuf/encoder.cc
index 156469af4..7c8491d55 100644
--- a/pw_protobuf/encoder.cc
+++ b/pw_protobuf/encoder.cc
@@ -14,23 +14,31 @@
#include "pw_protobuf/encoder.h"
+#include <algorithm>
#include <cstddef>
#include <cstring>
-#include <span>
+#include <optional>
#include "pw_assert/check.h"
#include "pw_bytes/span.h"
+#include "pw_protobuf/internal/codegen.h"
#include "pw_protobuf/serialized_size.h"
+#include "pw_protobuf/stream_decoder.h"
#include "pw_protobuf/wire_format.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/try.h"
#include "pw_stream/memory_stream.h"
#include "pw_stream/stream.h"
+#include "pw_string/string.h"
#include "pw_varint/varint.h"
namespace pw::protobuf {
-StreamEncoder StreamEncoder::GetNestedEncoder(uint32_t field_number) {
+using internal::VarintType;
+
+StreamEncoder StreamEncoder::GetNestedEncoder(uint32_t field_number,
+ bool write_when_empty) {
PW_CHECK(!nested_encoder_open());
PW_CHECK(ValidFieldNumber(field_number));
@@ -57,10 +65,10 @@ StreamEncoder StreamEncoder::GetNestedEncoder(uint32_t field_number) {
} else {
nested_buffer = ByteSpan();
}
- return StreamEncoder(*this, nested_buffer);
+ return StreamEncoder(*this, nested_buffer, write_when_empty);
}
-StreamEncoder::~StreamEncoder() {
+void StreamEncoder::CloseEncoder() {
// If this was an invalidated StreamEncoder which cannot be used, permit the
// object to be cleanly destructed by doing nothing.
if (nested_field_number_ == kFirstReservedNumber) {
@@ -104,6 +112,10 @@ void StreamEncoder::CloseNestedMessage(StreamEncoder& nested) {
return;
}
+ if (!nested.memory_writer_.bytes_written() && !nested.write_when_empty_) {
+ return;
+ }
+
status_ = WriteLengthDelimitedField(temp_field_number,
nested.memory_writer_.WrittenData());
}
@@ -113,7 +125,7 @@ Status StreamEncoder::WriteVarintField(uint32_t field_number, uint64_t value) {
field_number, WireType::kVarint, varint::EncodedSize(value)));
WriteVarint(FieldKey(field_number, WireType::kVarint))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return WriteVarint(value);
}
@@ -168,7 +180,7 @@ Status StreamEncoder::WriteFixed(uint32_t field_number, ConstByteSpan data) {
PW_TRY(UpdateStatusForWrite(field_number, type, data.size()));
WriteVarint(FieldKey(field_number, type))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
if (Status status = writer_.Write(data); !status.ok()) {
status_ = status;
}
@@ -176,7 +188,7 @@ Status StreamEncoder::WriteFixed(uint32_t field_number, ConstByteSpan data) {
}
Status StreamEncoder::WritePackedFixed(uint32_t field_number,
- std::span<const std::byte> values,
+ span<const std::byte> values,
size_t elem_size) {
if (values.empty()) {
return status_;
@@ -188,21 +200,21 @@ Status StreamEncoder::WritePackedFixed(uint32_t field_number,
PW_TRY(UpdateStatusForWrite(
field_number, WireType::kDelimited, values.size_bytes()));
WriteVarint(FieldKey(field_number, WireType::kDelimited))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
WriteVarint(values.size_bytes())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
for (auto val_start = values.begin(); val_start != values.end();
val_start += elem_size) {
// Allocates 8 bytes so both 4-byte and 8-byte types can be encoded as
// little-endian for serialization.
std::array<std::byte, sizeof(uint64_t)> data;
- if (std::endian::native == std::endian::little) {
+ if (endian::native == endian::little) {
std::copy(val_start, val_start + elem_size, std::begin(data));
} else {
std::reverse_copy(val_start, val_start + elem_size, std::begin(data));
}
- status_.Update(writer_.Write(std::span(data).first(elem_size)));
+ status_.Update(writer_.Write(span(data).first(elem_size)));
PW_TRY(status_);
}
return status_;
@@ -229,4 +241,323 @@ Status StreamEncoder::UpdateStatusForWrite(uint32_t field_number,
return status_;
}
+Status StreamEncoder::Write(span<const std::byte> message,
+ span<const internal::MessageField> table) {
+ PW_CHECK(!nested_encoder_open());
+ PW_TRY(status_);
+
+ for (const auto& field : table) {
+ // Calculate the span of bytes corresponding to the structure field to
+ // read from.
+ const auto values =
+ message.subspan(field.field_offset(), field.field_size());
+ PW_CHECK(values.begin() >= message.begin() &&
+ values.end() <= message.end());
+
+ // If the field is using callbacks, interpret the input field accordingly
+ // and allow the caller to provide custom handling.
+ if (field.use_callback()) {
+ const Callback<StreamEncoder, StreamDecoder>* callback =
+ reinterpret_cast<const Callback<StreamEncoder, StreamDecoder>*>(
+ values.data());
+ PW_TRY(callback->Encode(*this));
+ continue;
+ }
+
+ switch (field.wire_type()) {
+ case WireType::kFixed64:
+ case WireType::kFixed32: {
+ // Fixed fields call WriteFixed() for singular case and
+ // WritePackedFixed() for repeated fields.
+ PW_CHECK(field.elem_size() == (field.wire_type() == WireType::kFixed32
+ ? sizeof(uint32_t)
+ : sizeof(uint64_t)),
+ "Mismatched message field type and size");
+ if (field.is_fixed_size()) {
+ PW_CHECK(field.is_repeated(), "Non-repeated fixed size field");
+ if (static_cast<size_t>(
+ std::count(values.begin(), values.end(), std::byte{0})) <
+ values.size()) {
+ PW_TRY(WritePackedFixed(
+ field.field_number(), values, field.elem_size()));
+ }
+ } else if (field.is_repeated()) {
+ // The struct member for this field is a vector of a type
+ // corresponding to the field element size. Cast to the correct
+ // vector type so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed).
+ if (field.elem_size() == sizeof(uint64_t)) {
+ const auto* vector =
+ reinterpret_cast<const pw::Vector<const uint64_t>*>(
+ values.data());
+ if (!vector->empty()) {
+ PW_TRY(WritePackedFixed(
+ field.field_number(),
+ as_bytes(span(vector->data(), vector->size())),
+ field.elem_size()));
+ }
+ } else if (field.elem_size() == sizeof(uint32_t)) {
+ const auto* vector =
+ reinterpret_cast<const pw::Vector<const uint32_t>*>(
+ values.data());
+ if (!vector->empty()) {
+ PW_TRY(WritePackedFixed(
+ field.field_number(),
+ as_bytes(span(vector->data(), vector->size())),
+ field.elem_size()));
+ }
+ }
+ } else if (field.is_optional()) {
+ // The struct member for this field is a std::optional of a type
+ // corresponding to the field element size. Cast to the correct
+ // optional type so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed), and write from
+ // a temporary.
+ if (field.elem_size() == sizeof(uint64_t)) {
+ const auto* optional =
+ reinterpret_cast<const std::optional<uint64_t>*>(values.data());
+ if (optional->has_value()) {
+ uint64_t value = optional->value();
+ PW_TRY(
+ WriteFixed(field.field_number(), as_bytes(span(&value, 1))));
+ }
+ } else if (field.elem_size() == sizeof(uint32_t)) {
+ const auto* optional =
+ reinterpret_cast<const std::optional<uint32_t>*>(values.data());
+ if (optional->has_value()) {
+ uint32_t value = optional->value();
+ PW_TRY(
+ WriteFixed(field.field_number(), as_bytes(span(&value, 1))));
+ }
+ }
+ } else {
+ PW_CHECK(values.size() == field.elem_size(),
+ "Mismatched message field type and size");
+ if (static_cast<size_t>(
+ std::count(values.begin(), values.end(), std::byte{0})) <
+ values.size()) {
+ PW_TRY(WriteFixed(field.field_number(), values));
+ }
+ }
+ break;
+ }
+ case WireType::kVarint: {
+ // Varint fields call WriteVarintField() for singular case and
+ // WritePackedVarints() for repeated fields.
+ PW_CHECK(field.elem_size() == sizeof(uint64_t) ||
+ field.elem_size() == sizeof(uint32_t) ||
+ field.elem_size() == sizeof(bool),
+ "Mismatched message field type and size");
+ if (field.is_fixed_size()) {
+ // The struct member for this field is an array of type corresponding
+ // to the field element size. Cast to a span of the correct type over
+ // the array so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed).
+ PW_CHECK(field.is_repeated(), "Non-repeated fixed size field");
+ if (static_cast<size_t>(
+ std::count(values.begin(), values.end(), std::byte{0})) ==
+ values.size()) {
+ continue;
+ }
+ if (field.elem_size() == sizeof(uint64_t)) {
+ PW_TRY(WritePackedVarints(
+ field.field_number(),
+ span(reinterpret_cast<const uint64_t*>(values.data()),
+ values.size() / field.elem_size()),
+ field.varint_type()));
+ } else if (field.elem_size() == sizeof(uint32_t)) {
+ PW_TRY(WritePackedVarints(
+ field.field_number(),
+ span(reinterpret_cast<const uint32_t*>(values.data()),
+ values.size() / field.elem_size()),
+ field.varint_type()));
+ } else if (field.elem_size() == sizeof(bool)) {
+ static_assert(sizeof(bool) == sizeof(uint8_t),
+ "bool must be same size as uint8_t");
+ PW_TRY(WritePackedVarints(
+ field.field_number(),
+ span(reinterpret_cast<const uint8_t*>(values.data()),
+ values.size() / field.elem_size()),
+ field.varint_type()));
+ }
+ } else if (field.is_repeated()) {
+ // The struct member for this field is a vector of a type
+ // corresponding to the field element size. Cast to the correct
+ // vector type so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed).
+ if (field.elem_size() == sizeof(uint64_t)) {
+ const auto* vector =
+ reinterpret_cast<const pw::Vector<const uint64_t>*>(
+ values.data());
+ if (!vector->empty()) {
+ PW_TRY(WritePackedVarints(field.field_number(),
+ span(vector->data(), vector->size()),
+ field.varint_type()));
+ }
+ } else if (field.elem_size() == sizeof(uint32_t)) {
+ const auto* vector =
+ reinterpret_cast<const pw::Vector<const uint32_t>*>(
+ values.data());
+ if (!vector->empty()) {
+ PW_TRY(WritePackedVarints(field.field_number(),
+ span(vector->data(), vector->size()),
+ field.varint_type()));
+ }
+ } else if (field.elem_size() == sizeof(bool)) {
+ static_assert(sizeof(bool) == sizeof(uint8_t),
+ "bool must be same size as uint8_t");
+ const auto* vector =
+ reinterpret_cast<const pw::Vector<const uint8_t>*>(
+ values.data());
+ if (!vector->empty()) {
+ PW_TRY(WritePackedVarints(field.field_number(),
+ span(vector->data(), vector->size()),
+ field.varint_type()));
+ }
+ }
+ } else if (field.is_optional()) {
+ // The struct member for this field is a std::optional of a type
+ // corresponding to the field element size. Cast to the correct
+ // optional type so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed), and write from
+ // a temporary.
+ uint64_t value = 0;
+ if (field.elem_size() == sizeof(uint64_t)) {
+ if (field.varint_type() == VarintType::kUnsigned) {
+ const auto* optional =
+ reinterpret_cast<const std::optional<uint64_t>*>(
+ values.data());
+ if (!optional->has_value()) {
+ continue;
+ }
+ value = optional->value();
+ } else {
+ const auto* optional =
+ reinterpret_cast<const std::optional<int64_t>*>(
+ values.data());
+ if (!optional->has_value()) {
+ continue;
+ }
+ value = field.varint_type() == VarintType::kZigZag
+ ? varint::ZigZagEncode(optional->value())
+ : optional->value();
+ }
+ } else if (field.elem_size() == sizeof(uint32_t)) {
+ if (field.varint_type() == VarintType::kUnsigned) {
+ const auto* optional =
+ reinterpret_cast<const std::optional<uint32_t>*>(
+ values.data());
+ if (!optional->has_value()) {
+ continue;
+ }
+ value = optional->value();
+ } else {
+ const auto* optional =
+ reinterpret_cast<const std::optional<int32_t>*>(
+ values.data());
+ if (!optional->has_value()) {
+ continue;
+ }
+ value = field.varint_type() == VarintType::kZigZag
+ ? varint::ZigZagEncode(optional->value())
+ : optional->value();
+ }
+ } else if (field.elem_size() == sizeof(bool)) {
+ const auto* optional =
+ reinterpret_cast<const std::optional<bool>*>(values.data());
+ if (!optional->has_value()) {
+ continue;
+ }
+ value = optional->value();
+ }
+ PW_TRY(WriteVarintField(field.field_number(), value));
+ } else {
+ // The struct member for this field is a scalar of a type
+ // corresponding to the field element size. Cast to the correct
+ // type to retrieve the value before passing to WriteVarintField()
+ // so we're not performing type aliasing (except for unsigned vs
+ // signed which is explicitly allowed).
+ PW_CHECK(values.size() == field.elem_size(),
+ "Mismatched message field type and size");
+ uint64_t value = 0;
+ if (field.elem_size() == sizeof(uint64_t)) {
+ if (field.varint_type() == VarintType::kZigZag) {
+ value = varint::ZigZagEncode(
+ *reinterpret_cast<const int64_t*>(values.data()));
+ } else if (field.varint_type() == VarintType::kNormal) {
+ value = *reinterpret_cast<const int64_t*>(values.data());
+ } else {
+ value = *reinterpret_cast<const uint64_t*>(values.data());
+ }
+ if (!value) {
+ continue;
+ }
+ } else if (field.elem_size() == sizeof(uint32_t)) {
+ if (field.varint_type() == VarintType::kZigZag) {
+ value = varint::ZigZagEncode(
+ *reinterpret_cast<const int32_t*>(values.data()));
+ } else if (field.varint_type() == VarintType::kNormal) {
+ value = *reinterpret_cast<const int32_t*>(values.data());
+ } else {
+ value = *reinterpret_cast<const uint32_t*>(values.data());
+ }
+ if (!value) {
+ continue;
+ }
+ } else if (field.elem_size() == sizeof(bool)) {
+ value = *reinterpret_cast<const bool*>(values.data());
+ if (!value) {
+ continue;
+ }
+ }
+ PW_TRY(WriteVarintField(field.field_number(), value));
+ }
+ break;
+ }
+ case WireType::kDelimited: {
+ // Delimited fields are always a singular case because of the
+ // inability to cast to a generic vector with an element of a certain
+ // size (we always need a type).
+ PW_CHECK(!field.is_repeated(),
+ "Repeated delimited messages always require a callback");
+ if (field.nested_message_fields()) {
+ // Nested Message. Struct member is an embedded struct for the
+ // nested field. Obtain a nested encoder and recursively call Write()
+ // using the fields table pointer from this field.
+ auto nested_encoder = GetNestedEncoder(field.field_number(),
+ /*write_when_empty=*/false);
+ PW_TRY(nested_encoder.Write(values, *field.nested_message_fields()));
+ } else if (field.is_fixed_size()) {
+ // Fixed-length bytes field. Struct member is a std::array<std::byte>.
+ // Call WriteLengthDelimitedField() to output it to the stream.
+ PW_CHECK(field.elem_size() == sizeof(std::byte),
+ "Mismatched message field type and size");
+ if (static_cast<size_t>(
+ std::count(values.begin(), values.end(), std::byte{0})) <
+ values.size()) {
+ PW_TRY(WriteLengthDelimitedField(field.field_number(), values));
+ }
+ } else {
+ // bytes or string field with a maximum size. Struct member is
+ // pw::Vector<std::byte> for bytes or pw::InlineString<> for string.
+ // Use the contents as a span and call WriteLengthDelimitedField() to
+ // output it to the stream.
+ PW_CHECK(field.elem_size() == sizeof(std::byte),
+ "Mismatched message field type and size");
+ if (field.is_string()) {
+ PW_TRY(WriteStringOrBytes<const InlineString<>>(
+ field.field_number(), values.data()));
+ } else {
+ PW_TRY(WriteStringOrBytes<const Vector<const std::byte>>(
+ field.field_number(), values.data()));
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ return status_;
+}
+
} // namespace pw::protobuf
diff --git a/pw_protobuf/encoder_fuzzer.cc b/pw_protobuf/encoder_fuzzer.cc
index 421af6575..ebd0984be 100644
--- a/pw_protobuf/encoder_fuzzer.cc
+++ b/pw_protobuf/encoder_fuzzer.cc
@@ -15,116 +15,79 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
-#include <span>
#include <vector>
+#include "fuzz.h"
#include "pw_fuzzer/asan_interface.h"
#include "pw_fuzzer/fuzzed_data_provider.h"
#include "pw_protobuf/encoder.h"
+#include "pw_span/span.h"
+namespace pw::protobuf::fuzz {
namespace {
-// Encodable values. The fuzzer will iteratively choose different field types to
-// generate and encode.
-enum FieldType : uint8_t {
- kUint32 = 0,
- kPackedUint32,
- kUint64,
- kPackedUint64,
- kInt32,
- kPackedInt32,
- kInt64,
- kPackedInt64,
- kSint32,
- kPackedSint32,
- kSint64,
- kPackedSint64,
- kBool,
- kFixed32,
- kPackedFixed32,
- kFixed64,
- kPackedFixed64,
- kSfixed32,
- kPackedSfixed32,
- kSfixed64,
- kPackedSfixed64,
- kFloat,
- kPackedFloat,
- kDouble,
- kPackedDouble,
- kBytes,
- kString,
- kPush,
- kMaxValue = kPush,
-};
-
-// TODO(pwbug/181): Move this to pw_fuzzer/fuzzed_data_provider.h
+// TODO(b/235289495): Move this to pw_fuzzer/fuzzed_data_provider.h
// Uses the given |provider| to pick and return a number between 0 and the
// maximum numbers of T that can be generated from the remaining input data.
template <typename T>
-size_t ConsumeSize(FuzzedDataProvider* provider) {
- size_t max = provider->remaining_bytes() / sizeof(T);
- return provider->ConsumeIntegralInRange<size_t>(0, max);
+size_t ConsumeSize(FuzzedDataProvider& provider) {
+ size_t max = provider.remaining_bytes() / sizeof(T);
+ return provider.ConsumeIntegralInRange<size_t>(0, max);
}
// Uses the given |provider| to generate several instances of T, store them in
-// |data|, and then return a std::span to them. It is the caller's responsbility
-// to ensure |data| remains in scope as long as the returned std::span.
+// |data|, and then return a span to them. It is the caller's responsbility
+// to ensure |data| remains in scope as long as the returned span.
template <typename T>
-std::span<const T> ConsumeSpan(FuzzedDataProvider* provider,
- std::vector<T>* data) {
+span<const T> ConsumeSpan(FuzzedDataProvider& provider, std::vector<T>* data) {
size_t num = ConsumeSize<T>(provider);
size_t off = data->size();
data->reserve(off + num);
for (size_t i = 0; i < num; ++i) {
if constexpr (std::is_floating_point<T>::value) {
- data->push_back(provider->ConsumeFloatingPoint<T>());
+ data->push_back(provider.ConsumeFloatingPoint<T>());
} else {
- data->push_back(provider->ConsumeIntegral<T>());
+ data->push_back(provider.ConsumeIntegral<T>());
}
}
- return std::span(&((*data)[off]), num);
+ return span(&((*data)[off]), num);
}
// Uses the given |provider| to generate a string, store it in |data|, and
// return a C-style representation. It is the caller's responsbility to
// ensure |data| remains in scope as long as the returned char*.
-const char* ConsumeString(FuzzedDataProvider* provider,
+const char* ConsumeString(FuzzedDataProvider& provider,
std::vector<std::string>* data) {
size_t off = data->size();
// OSS-Fuzz's clang doesn't have the zero-parameter version of
// ConsumeRandomLengthString yet.
size_t max_length = std::numeric_limits<size_t>::max();
- data->push_back(provider->ConsumeRandomLengthString(max_length));
+ data->push_back(provider.ConsumeRandomLengthString(max_length));
return (*data)[off].c_str();
}
// Uses the given |provider| to generate non-arithmetic bytes, store them in
-// |data|, and return a std::span to them. It is the caller's responsbility to
-// ensure |data| remains in scope as long as the returned std::span.
-std::span<const std::byte> ConsumeBytes(FuzzedDataProvider* provider,
- std::vector<std::byte>* data) {
+// |data|, and return a span to them. It is the caller's responsbility to
+// ensure |data| remains in scope as long as the returned span.
+span<const std::byte> ConsumeBytes(FuzzedDataProvider& provider,
+ std::vector<std::byte>* data) {
size_t num = ConsumeSize<std::byte>(provider);
- auto added = provider->ConsumeBytes<std::byte>(num);
+ auto added = provider.ConsumeBytes<std::byte>(num);
size_t off = data->size();
num = added.size();
data->insert(data->end(), added.begin(), added.end());
- return std::span(&((*data)[off]), num);
+ return span(&((*data)[off]), num);
}
-} // namespace
-
-extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+void TestOneInput(FuzzedDataProvider& provider) {
static std::byte buffer[65536];
- FuzzedDataProvider provider(data, size);
-
// Pick a subset of the buffer that the fuzzer is allowed to use, and poison
// the rest.
size_t unpoisoned_length =
provider.ConsumeIntegralInRange<size_t>(0, sizeof(buffer));
- std::span<std::byte> unpoisoned(buffer, unpoisoned_length);
+ ByteSpan unpoisoned(buffer, unpoisoned_length);
void* poisoned = &buffer[unpoisoned_length];
size_t poisoned_length = sizeof(buffer) - unpoisoned_length;
ASAN_POISON_MEMORY_REGION(poisoned, poisoned_length);
@@ -152,163 +115,163 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
encoder
.WriteUint32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<uint32_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedUint32:
encoder
.WritePackedUint32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<uint32_t>(&provider, &u32s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<uint32_t>(provider, &u32s))
+ .IgnoreError();
break;
case kUint64:
encoder
.WriteUint64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<uint64_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedUint64:
encoder
.WritePackedUint64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<uint64_t>(&provider, &u64s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<uint64_t>(provider, &u64s))
+ .IgnoreError();
break;
case kInt32:
encoder
.WriteInt32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int32_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedInt32:
encoder
.WritePackedInt32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int32_t>(&provider, &s32s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<int32_t>(provider, &s32s))
+ .IgnoreError();
break;
case kInt64:
encoder
.WriteInt64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int64_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedInt64:
encoder
.WritePackedInt64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int64_t>(&provider, &s64s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<int64_t>(provider, &s64s))
+ .IgnoreError();
break;
case kSint32:
encoder
.WriteSint32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int32_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedSint32:
encoder
.WritePackedSint32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int32_t>(&provider, &s32s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<int32_t>(provider, &s32s))
+ .IgnoreError();
break;
case kSint64:
encoder
.WriteSint64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int64_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedSint64:
encoder
.WritePackedSint64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int64_t>(&provider, &s64s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<int64_t>(provider, &s64s))
+ .IgnoreError();
break;
case kBool:
encoder
.WriteBool(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeBool())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kFixed32:
encoder
.WriteFixed32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<uint32_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedFixed32:
encoder
.WritePackedFixed32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<uint32_t>(&provider, &u32s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<uint32_t>(provider, &u32s))
+ .IgnoreError();
break;
case kFixed64:
encoder
.WriteFixed64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<uint64_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedFixed64:
encoder
.WritePackedFixed64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<uint64_t>(&provider, &u64s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<uint64_t>(provider, &u64s))
+ .IgnoreError();
break;
case kSfixed32:
encoder
.WriteSfixed32(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int32_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedSfixed32:
encoder
.WritePackedSfixed32(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int32_t>(&provider, &s32s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<int32_t>(provider, &s32s))
+ .IgnoreError();
break;
case kSfixed64:
encoder
.WriteSfixed64(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeIntegral<int64_t>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedSfixed64:
encoder
.WritePackedSfixed64(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<int64_t>(&provider, &s64s))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<int64_t>(provider, &s64s))
+ .IgnoreError();
break;
case kFloat:
encoder
.WriteFloat(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeFloatingPoint<float>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedFloat:
encoder
.WritePackedFloat(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<float>(&provider, &floats))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<float>(provider, &floats))
+ .IgnoreError();
break;
case kDouble:
encoder
.WriteDouble(provider.ConsumeIntegral<uint32_t>(),
provider.ConsumeFloatingPoint<double>())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError();
break;
case kPackedDouble:
encoder
.WritePackedDouble(provider.ConsumeIntegral<uint32_t>(),
- ConsumeSpan<double>(&provider, &doubles))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeSpan<double>(provider, &doubles))
+ .IgnoreError();
break;
case kBytes:
encoder
.WriteBytes(provider.ConsumeIntegral<uint32_t>(),
- ConsumeBytes(&provider, &bytes))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeBytes(provider, &bytes))
+ .IgnoreError();
break;
case kString:
encoder
.WriteString(provider.ConsumeIntegral<uint32_t>(),
- ConsumeString(&provider, &strings))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ConsumeString(provider, &strings))
+ .IgnoreError();
break;
case kPush:
// Special "field". The marks the start of a nested message.
@@ -319,5 +282,13 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
// Don't forget to unpoison for the next iteration!
ASAN_UNPOISON_MEMORY_REGION(poisoned, poisoned_length);
+}
+
+} // namespace
+} // namespace pw::protobuf::fuzz
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ FuzzedDataProvider provider(data, size);
+ pw::protobuf::fuzz::TestOneInput(provider);
return 0;
}
diff --git a/pw_protobuf/encoder_perf_test.cc b/pw_protobuf/encoder_perf_test.cc
new file mode 100644
index 000000000..9b4671a17
--- /dev/null
+++ b/pw_protobuf/encoder_perf_test.cc
@@ -0,0 +1,38 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bytes/span.h"
+#include "pw_perf_test/perf_test.h"
+#include "pw_protobuf/encoder.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_stream/memory_stream.h"
+
+namespace pw::protobuf {
+namespace {
+
+void BasicIntegerPerformance(pw::perf_test::State& state, int64_t value) {
+ std::byte encode_buffer[30];
+
+ while (state.KeepRunning()) {
+ MemoryEncoder encoder(encode_buffer);
+ encoder.WriteUint32(1, value).IgnoreError();
+ }
+}
+
+PW_PERF_TEST(SmallIntegerEncoding, BasicIntegerPerformance, 1);
+PW_PERF_TEST(LargerIntegerEncoding, BasicIntegerPerformance, 4000000000);
+
+} // namespace
+} // namespace pw::protobuf
diff --git a/pw_protobuf/encoder_test.cc b/pw_protobuf/encoder_test.cc
index 6417f699f..754e45b04 100644
--- a/pw_protobuf/encoder_test.cc
+++ b/pw_protobuf/encoder_test.cc
@@ -14,10 +14,9 @@
#include "pw_protobuf/encoder.h"
-#include <span>
-
#include "gtest/gtest.h"
#include "pw_bytes/span.h"
+#include "pw_span/span.h"
#include "pw_stream/memory_stream.h"
namespace pw::protobuf {
@@ -109,7 +108,7 @@ TEST(StreamEncoder, EncodePrimitives) {
OkStatus());
const std::string_view kReaderMessage = "byreader";
- stream::MemoryReader msg_reader(std::as_bytes(std::span(kReaderMessage)));
+ stream::MemoryReader msg_reader(as_bytes(span(kReaderMessage)));
std::byte stream_pipe_buffer[1];
EXPECT_EQ(encoder.WriteStringFromStream(kTestProtoPayloadFromStreamField,
msg_reader,
@@ -262,8 +261,7 @@ TEST(StreamEncoder, RepeatedField) {
// repeated uint32 values = 1;
constexpr uint32_t values[] = {0, 50, 100, 150, 200};
for (int i = 0; i < 5; ++i) {
- encoder.WriteUint32(1, values[i])
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WriteUint32(1, values[i]));
}
constexpr uint8_t encoded_proto[] = {
@@ -282,8 +280,7 @@ TEST(StreamEncoder, PackedVarint) {
// repeated uint32 values = 1;
constexpr uint32_t values[] = {0, 50, 100, 150, 200};
- encoder.WritePackedUint32(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WritePackedUint32(1, values));
constexpr uint8_t encoded_proto[] = {
0x0a, 0x07, 0x00, 0x32, 0x64, 0x96, 0x01, 0xc8, 0x01};
@@ -301,8 +298,7 @@ TEST(StreamEncoder, PackedVarintInsufficientSpace) {
MemoryEncoder encoder(encode_buffer);
constexpr uint32_t values[] = {0, 50, 100, 150, 200};
- encoder.WritePackedUint32(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(Status::ResourceExhausted(), encoder.WritePackedUint32(1, values));
EXPECT_EQ(encoder.status(), Status::ResourceExhausted());
}
@@ -313,8 +309,7 @@ TEST(StreamEncoder, PackedVarintVector) {
// repeated uint32 values = 1;
const pw::Vector<uint32_t, 5> values = {0, 50, 100, 150, 200};
- encoder.WriteRepeatedUint32(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WriteRepeatedUint32(1, values));
constexpr uint8_t encoded_proto[] = {
0x0a, 0x07, 0x00, 0x32, 0x64, 0x96, 0x01, 0xc8, 0x01};
@@ -332,8 +327,8 @@ TEST(StreamEncoder, PackedVarintVectorInsufficientSpace) {
MemoryEncoder encoder(encode_buffer);
const pw::Vector<uint32_t, 5> values = {0, 50, 100, 150, 200};
- encoder.WriteRepeatedUint32(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(Status::ResourceExhausted(),
+ encoder.WriteRepeatedUint32(1, values));
EXPECT_EQ(encoder.status(), Status::ResourceExhausted());
}
@@ -344,8 +339,7 @@ TEST(StreamEncoder, PackedBool) {
// repeated bool values = 1;
constexpr bool values[] = {true, false, true, true, false};
- encoder.WritePackedBool(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WritePackedBool(1, values));
constexpr uint8_t encoded_proto[] = {
0x0a, 0x05, 0x01, 0x00, 0x01, 0x01, 0x00};
@@ -364,13 +358,11 @@ TEST(StreamEncoder, PackedFixed) {
// repeated fixed32 values = 1;
constexpr uint32_t values[] = {0, 50, 100, 150, 200};
- encoder.WritePackedFixed32(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WritePackedFixed32(1, values));
// repeated fixed64 values64 = 2;
constexpr uint64_t values64[] = {0x0102030405060708};
- encoder.WritePackedFixed64(2, values64)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WritePackedFixed64(2, values64));
constexpr uint8_t encoded_proto[] = {
0x0a, 0x14, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x64,
@@ -390,13 +382,11 @@ TEST(StreamEncoder, PackedFixedVector) {
// repeated fixed32 values = 1;
const pw::Vector<uint32_t, 5> values = {0, 50, 100, 150, 200};
- encoder.WriteRepeatedFixed32(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WriteRepeatedFixed32(1, values));
// repeated fixed64 values64 = 2;
const pw::Vector<uint64_t, 1> values64 = {0x0102030405060708};
- encoder.WriteRepeatedFixed64(2, values64)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WriteRepeatedFixed64(2, values64));
constexpr uint8_t encoded_proto[] = {
0x0a, 0x14, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x64,
@@ -410,14 +400,37 @@ TEST(StreamEncoder, PackedFixedVector) {
0);
}
+TEST(StreamEncoder, PackedSfixedVector) {
+ std::byte encode_buffer[32];
+ MemoryEncoder encoder(encode_buffer);
+
+ // repeated fixed32 values = 1;
+ const pw::Vector<int32_t, 5> values = {0, 50, 100, 150, 200};
+ ASSERT_EQ(OkStatus(), encoder.WriteRepeatedSfixed32(1, values));
+
+ // repeated fixed64 values64 = 2;
+ const pw::Vector<int64_t, 1> values64 = {-2};
+ ASSERT_EQ(OkStatus(), encoder.WriteRepeatedSfixed64(2, values64));
+
+ constexpr uint8_t encoded_proto[] = {
+ 0x0a, 0x14, 0x00, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x64,
+ 0x00, 0x00, 0x00, 0x96, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x00, 0x00,
+ 0x12, 0x08, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
+
+ ASSERT_EQ(encoder.status(), OkStatus());
+ ConstByteSpan result(encoder);
+ EXPECT_EQ(result.size(), sizeof(encoded_proto));
+ EXPECT_EQ(std::memcmp(result.data(), encoded_proto, sizeof(encoded_proto)),
+ 0);
+}
+
TEST(StreamEncoder, PackedZigzag) {
std::byte encode_buffer[32];
MemoryEncoder encoder(encode_buffer);
// repeated sint32 values = 1;
constexpr int32_t values[] = {-100, -25, -1, 0, 1, 25, 100};
- encoder.WritePackedSint32(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WritePackedSint32(1, values));
constexpr uint8_t encoded_proto[] = {
0x0a, 0x09, 0xc7, 0x01, 0x31, 0x01, 0x00, 0x02, 0x32, 0xc8, 0x01};
@@ -435,8 +448,7 @@ TEST(StreamEncoder, PackedZigzagVector) {
// repeated sint32 values = 1;
const pw::Vector<int32_t, 7> values = {-100, -25, -1, 0, 1, 25, 100};
- encoder.WriteRepeatedSint32(1, values)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), encoder.WriteRepeatedSint32(1, values));
constexpr uint8_t encoded_proto[] = {
0x0a, 0x09, 0xc7, 0x01, 0x31, 0x01, 0x00, 0x02, 0x32, 0xc8, 0x01};
@@ -503,5 +515,18 @@ TEST(StreamEncoder, NestedStatusPropagates) {
ASSERT_EQ(parent.status(), Status::InvalidArgument());
}
+TEST(StreamEncoder, ManualCloseEncoderWrites) {
+ std::byte encode_buffer[32];
+ MemoryEncoder parent(encode_buffer);
+ StreamEncoder child = parent.GetNestedEncoder(kTestProtoNestedField);
+ child.CloseEncoder();
+ ASSERT_EQ(parent.status(), OkStatus());
+ const size_t kExpectedSize =
+ varint::EncodedSize(
+ FieldKey(kTestProtoNestedField, WireType::kDelimited)) +
+ varint::EncodedSize(0);
+ ASSERT_EQ(parent.size(), kExpectedSize);
+}
+
} // namespace
} // namespace pw::protobuf
diff --git a/pw_protobuf/find.cc b/pw_protobuf/find.cc
index 0082726c7..c5bd64f78 100644
--- a/pw_protobuf/find.cc
+++ b/pw_protobuf/find.cc
@@ -28,7 +28,7 @@ Status FindDecodeHandler::ProcessField(CallbackDecoder& decoder,
return Status::Cancelled();
}
- std::span<const std::byte> submessage;
+ span<const std::byte> submessage;
if (Status status = decoder.ReadBytes(&submessage); !status.ok()) {
return status;
}
diff --git a/pw_protobuf/find_test.cc b/pw_protobuf/find_test.cc
index 669e7d04f..bacac86ca 100644
--- a/pw_protobuf/find_test.cc
+++ b/pw_protobuf/find_test.cc
@@ -45,8 +45,7 @@ TEST(FindDecodeHandler, SingleLevel_FindsExistingField) {
FindDecodeHandler finder(3);
decoder.set_handler(&finder);
- decoder.Decode(std::as_bytes(std::span(encoded_proto)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(Status::Cancelled(), decoder.Decode(as_bytes(span(encoded_proto))));
EXPECT_TRUE(finder.found());
EXPECT_TRUE(decoder.cancelled());
@@ -57,8 +56,7 @@ TEST(FindDecodeHandler, SingleLevel_DoesntFindNonExistingField) {
FindDecodeHandler finder(8);
decoder.set_handler(&finder);
- decoder.Decode(std::as_bytes(std::span(encoded_proto)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), decoder.Decode(as_bytes(span(encoded_proto))));
EXPECT_FALSE(finder.found());
EXPECT_FALSE(decoder.cancelled());
@@ -70,8 +68,7 @@ TEST(FindDecodeHandler, MultiLevel_FindsExistingNestedField) {
FindDecodeHandler finder(7, &nested_finder);
decoder.set_handler(&finder);
- decoder.Decode(std::as_bytes(std::span(encoded_proto)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(Status::Cancelled(), decoder.Decode(as_bytes(span(encoded_proto))));
EXPECT_TRUE(finder.found());
EXPECT_TRUE(nested_finder.found());
@@ -84,8 +81,7 @@ TEST(FindDecodeHandler, MultiLevel_DoesntFindNonExistingNestedField) {
FindDecodeHandler finder(7, &nested_finder);
decoder.set_handler(&finder);
- decoder.Decode(std::as_bytes(std::span(encoded_proto)))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), decoder.Decode(as_bytes(span(encoded_proto))));
EXPECT_TRUE(finder.found());
EXPECT_FALSE(nested_finder.found());
diff --git a/pw_protobuf/fuzz.h b/pw_protobuf/fuzz.h
new file mode 100644
index 000000000..cc95db65c
--- /dev/null
+++ b/pw_protobuf/fuzz.h
@@ -0,0 +1,54 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+namespace pw::protobuf::fuzz {
+
+// Encodable values. The fuzzer will iteratively choose different field types to
+// generate and encode.
+enum FieldType : uint8_t {
+ kUint32 = 0,
+ kPackedUint32,
+ kUint64,
+ kPackedUint64,
+ kInt32,
+ kPackedInt32,
+ kInt64,
+ kPackedInt64,
+ kSint32,
+ kPackedSint32,
+ kSint64,
+ kPackedSint64,
+ kBool,
+ kFixed32,
+ kPackedFixed32,
+ kFixed64,
+ kPackedFixed64,
+ kSfixed32,
+ kPackedSfixed32,
+ kSfixed64,
+ kPackedSfixed64,
+ kFloat,
+ kPackedFloat,
+ kDouble,
+ kPackedDouble,
+ kBytes,
+ kString,
+ kPush,
+ kMaxValue = kPush,
+};
+
+} // namespace pw::protobuf::fuzz
diff --git a/pw_protobuf/map_utils_test.cc b/pw_protobuf/map_utils_test.cc
index d1da109ca..56a33d085 100644
--- a/pw_protobuf/map_utils_test.cc
+++ b/pw_protobuf/map_utils_test.cc
@@ -81,8 +81,8 @@ TEST(ProtoHelper, WriteProtoStringToBytesMapEntry) {
std::byte stream_pipe_buffer[1];
for (auto ele : kMapData) {
- stream::MemoryReader key_reader(std::as_bytes(std::span{ele.key}));
- stream::MemoryReader value_reader(std::as_bytes(std::span{ele.value}));
+ stream::MemoryReader key_reader(as_bytes(span<const char>{ele.key}));
+ stream::MemoryReader value_reader(as_bytes(span<const char>{ele.value}));
ASSERT_OK(WriteProtoStringToBytesMapEntry(ele.field_number,
key_reader,
ele.key.size(),
@@ -119,8 +119,8 @@ TEST(ProtoHelper, WriteProtoStringToBytesMapEntryExceedsWriteLimit) {
constexpr uint32_t kFieldNumber = 1;
std::string_view key = "key_bar";
std::string_view value = "bar_a";
- stream::MemoryReader key_reader(std::as_bytes(std::span{key}));
- stream::MemoryReader value_reader(std::as_bytes(std::span{value}));
+ stream::MemoryReader key_reader(as_bytes(span<const char>{key}));
+ stream::MemoryReader value_reader(as_bytes(span<const char>{value}));
std::byte stream_pipe_buffer[1];
ASSERT_EQ(
WriteProtoStringToBytesMapEntry(kFieldNumber,
@@ -138,8 +138,8 @@ TEST(ProtoHelper, WriteProtoStringToBytesMapEntryInvalidArgument) {
stream::MemoryWriter writer(encode_buffer);
std::string_view key = "key_bar";
std::string_view value = "bar_a";
- stream::MemoryReader key_reader(std::as_bytes(std::span{key}));
- stream::MemoryReader value_reader(std::as_bytes(std::span{value}));
+ stream::MemoryReader key_reader(as_bytes(span<const char>{key}));
+ stream::MemoryReader value_reader(as_bytes(span<const char>{value}));
std::byte stream_pipe_buffer[1];
ASSERT_EQ(
diff --git a/pw_protobuf/message.cc b/pw_protobuf/message.cc
index 01286277b..bc4f31bdf 100644
--- a/pw_protobuf/message.cc
+++ b/pw_protobuf/message.cc
@@ -136,7 +136,7 @@ Result<bool> Bytes::Equal(ConstByteSpan bytes) {
}
Result<bool> String::Equal(std::string_view str) {
- return Bytes::Equal(std::as_bytes(std::span{str}));
+ return Bytes::Equal(as_bytes(span<const char>{str}));
}
Message::iterator& Message::iterator::operator++() {
diff --git a/pw_protobuf/message_test.cc b/pw_protobuf/message_test.cc
index 3dab07599..b4dc9bd30 100644
--- a/pw_protobuf/message_test.cc
+++ b/pw_protobuf/message_test.cc
@@ -33,7 +33,7 @@ TEST(ProtoHelper, IterateMessage) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
uint32_t count = 0;
@@ -58,7 +58,7 @@ TEST(ProtoHelper, MessageIterator) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
Message::iterator iter = parser.begin();
@@ -97,7 +97,7 @@ TEST(ProtoHelper, MessageIteratorMalformedProto) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
Message::iterator iter = parser.begin();
@@ -160,7 +160,7 @@ TEST(ProtoHelper, AsProtoInteger) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
{
@@ -259,7 +259,7 @@ TEST(ProtoHelper, AsString) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
constexpr uint32_t kFieldNumber = 1;
@@ -308,7 +308,7 @@ TEST(ProtoHelper, AsRepeatedStrings) {
constexpr uint32_t kMsgBFieldNumber = 2;
constexpr uint32_t kNonExistFieldNumber = 3;
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
// Field 'msg_a'
@@ -377,7 +377,7 @@ TEST(ProtoHelper, RepeatedFieldIterator) {
// clang-format on
constexpr uint32_t kFieldNumber = 1;
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
RepeatedStrings repeated_str = parser.AsRepeatedStrings(kFieldNumber);
@@ -418,7 +418,7 @@ TEST(ProtoHelper, RepeatedFieldIteratorMalformedFieldID) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
RepeatedStrings repeated_str = parser.AsRepeatedStrings(1);
@@ -449,7 +449,7 @@ TEST(ProtoHelper, RepeatedFieldIteratorMalformedFieldIDBeginning) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
RepeatedStrings repeated_str = parser.AsRepeatedStrings(1);
@@ -478,7 +478,7 @@ TEST(ProtoHelper, RepeatedFieldIteratorMalformedDataLoss) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
RepeatedStrings repeated_str = parser.AsRepeatedStrings(1);
@@ -514,7 +514,7 @@ TEST(ProtoHelper, AsMessage) {
constexpr uint32_t kNumberFieldNumber = 1;
constexpr uint32_t kEmailFieldNumber = 2;
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
Message info = parser.AsMessage(kInfoFieldNumber);
@@ -560,7 +560,7 @@ TEST(ProtoHelper, AsRepeatedMessages) {
constexpr uint32_t kNumberFieldNumber = 1;
constexpr uint32_t kEmailFieldNumber = 2;
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
RepeatedMessages messages = parser.AsRepeatedMessages(kInfoFieldNumber);
@@ -623,7 +623,7 @@ TEST(ProtoHelper, AsStringToBytesMap) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
{
@@ -699,7 +699,7 @@ TEST(ProtoHelper, AsStringToMessageMap) {
constexpr uint32_t kNumberFieldId = 1;
constexpr uint32_t kEmailFieldId = 2;
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
StringMapParser<Message> staffs = parser.AsStringToMessageMap(kStaffsFieldId);
@@ -751,7 +751,7 @@ TEST(ProtoHelper, AsStringToBytesMapMalformed) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
Message parser = Message(reader, sizeof(encoded_proto));
// Parse field 'map_a'
diff --git a/pw_protobuf/public/pw_protobuf/bytes_utils.h b/pw_protobuf/public/pw_protobuf/bytes_utils.h
index d7c90bfd1..25c8c3291 100644
--- a/pw_protobuf/public/pw_protobuf/bytes_utils.h
+++ b/pw_protobuf/public/pw_protobuf/bytes_utils.h
@@ -32,7 +32,7 @@ namespace pw::protobuf {
// // HANDLE ERROR.
// }
// if (static_cast<MyProtoMessage::Fields>(decoder.FieldNumber()) !=
-// MyProtoMessage::Fields::MY_FIELD) {
+// MyProtoMessage::Fields::kMyFields) {
// // HANDLE ERROR.
// }
// Result<uint32_t> result = DecodeBytesToUint32(decoder);
@@ -52,7 +52,7 @@ inline Result<uint32_t> DecodeBytesToUint32(Decoder& decoder) {
return Status::InvalidArgument();
}
uint32_t value;
- if (!bytes::ReadInOrder(std::endian::little, bytes_read, value)) {
+ if (!bytes::ReadInOrder(endian::little, bytes_read, value)) {
return Status::Internal();
}
return value;
diff --git a/pw_protobuf/public/pw_protobuf/decoder.h b/pw_protobuf/public/pw_protobuf/decoder.h
index 6f3c944a6..28cd7ad5c 100644
--- a/pw_protobuf/public/pw_protobuf/decoder.h
+++ b/pw_protobuf/public/pw_protobuf/decoder.h
@@ -13,10 +13,10 @@
// the License.
#pragma once
-#include <span>
#include <string_view>
#include "pw_protobuf/wire_format.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_varint/varint.h"
@@ -45,7 +45,7 @@ namespace pw::protobuf {
// TODO(frolv): Rename this to MemoryDecoder to match the encoder naming.
class Decoder {
public:
- constexpr Decoder(std::span<const std::byte> proto)
+ constexpr Decoder(span<const std::byte> proto)
: proto_(proto), previous_field_consumed_(true) {}
Decoder(const Decoder& other) = delete;
@@ -134,14 +134,12 @@ class Decoder {
Status ReadString(std::string_view* out);
// Reads a proto bytes value from the current cursor and returns a view of it
- // in `out`. The raw protobuf data must outlive the `out` std::span. If the
+ // in `out`. The raw protobuf data must outlive the `out` span. If the
// bytes field is invalid, `out` is not modified.
- Status ReadBytes(std::span<const std::byte>* out) {
- return ReadDelimited(out);
- }
+ Status ReadBytes(span<const std::byte>* out) { return ReadDelimited(out); }
// Resets the decoder to start reading a new proto message.
- void Reset(std::span<const std::byte> proto) {
+ void Reset(span<const std::byte> proto) {
proto_ = proto;
previous_field_consumed_ = true;
}
@@ -169,9 +167,9 @@ class Decoder {
return ReadFixed(reinterpret_cast<std::byte*>(out), sizeof(T));
}
- Status ReadDelimited(std::span<const std::byte>* out);
+ Status ReadDelimited(span<const std::byte>* out);
- std::span<const std::byte> proto_;
+ span<const std::byte> proto_;
bool previous_field_consumed_;
};
@@ -206,7 +204,7 @@ class DecodeHandler;
// unsigned int baz;
// };
//
-// void DecodeFooProto(std::span<std::byte> raw_proto) {
+// void DecodeFooProto(span<std::byte> raw_proto) {
// Decoder decoder;
// FooProtoHandler handler;
//
@@ -231,7 +229,7 @@ class CallbackDecoder {
// Decodes the specified protobuf data. The registered handler's ProcessField
// function is called on each field found in the data.
- Status Decode(std::span<const std::byte> proto);
+ Status Decode(span<const std::byte> proto);
// Reads a proto int32 value from the current cursor.
Status ReadInt32(int32_t* out) { return decoder_.ReadInt32(out); }
@@ -278,13 +276,13 @@ class CallbackDecoder {
Status ReadString(std::string_view* out) { return decoder_.ReadString(out); }
// Reads a proto bytes value from the current cursor and returns a view of it
- // in `out`. The raw protobuf data must outlive the `out` std::span. If the
+ // in `out`. The raw protobuf data must outlive the `out` span. If the
// bytes field is invalid, `out` is not modified.
- Status ReadBytes(std::span<const std::byte>* out) {
+ Status ReadBytes(span<const std::byte>* out) {
return decoder_.ReadBytes(out);
}
- bool cancelled() const { return state_ == kDecodeCancelled; };
+ bool cancelled() const { return state_ == kDecodeCancelled; }
private:
enum State {
diff --git a/pw_protobuf/public/pw_protobuf/encoder.h b/pw_protobuf/public/pw_protobuf/encoder.h
index f5b427382..21b12cb1f 100644
--- a/pw_protobuf/public/pw_protobuf/encoder.h
+++ b/pw_protobuf/public/pw_protobuf/encoder.h
@@ -15,18 +15,19 @@
#include <algorithm>
#include <array>
-#include <bit>
#include <cstddef>
#include <cstring>
-#include <span>
#include <string_view>
#include "pw_assert/assert.h"
+#include "pw_bytes/bit.h"
#include "pw_bytes/endian.h"
#include "pw_bytes/span.h"
#include "pw_containers/vector.h"
#include "pw_protobuf/config.h"
+#include "pw_protobuf/internal/codegen.h"
#include "pw_protobuf/wire_format.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/try.h"
#include "pw_stream/memory_stream.h"
@@ -66,7 +67,7 @@ inline Status WriteVarint(uint64_t value, stream::Writer& writer) {
std::array<std::byte, varint::kMaxVarint64SizeBytes> varint_encode_buffer;
const size_t varint_size =
pw::varint::EncodeLittleEndianBase128(value, varint_encode_buffer);
- return writer.Write(std::span(varint_encode_buffer).first(varint_size));
+ return writer.Write(span(varint_encode_buffer).first(varint_size));
}
// Write the field key and length prefix for a length-delimited field. It is
@@ -117,6 +118,7 @@ class StreamEncoder {
// provide a zero-length scratch buffer.
constexpr StreamEncoder(stream::Writer& writer, ByteSpan scratch_buffer)
: status_(OkStatus()),
+ write_when_empty_(true),
parent_(nullptr),
nested_field_number_(0),
memory_writer_(scratch_buffer),
@@ -126,7 +128,7 @@ class StreamEncoder {
//
// Postcondition: If this encoder is a nested one, the parent encoder is
// unlocked and proto encoding may resume on the parent.
- ~StreamEncoder();
+ ~StreamEncoder() { CloseEncoder(); }
// Disallow copy/assign to avoid confusion about who owns the buffer.
StreamEncoder& operator=(const StreamEncoder& other) = delete;
@@ -136,6 +138,19 @@ class StreamEncoder {
// parent_ pointer to become invalid.
StreamEncoder& operator=(StreamEncoder&& other) = delete;
+ // Closes this encoder, finalizing its output.
+ //
+ // This method is called automatically by `StreamEncoder`'s destructor, but
+ // may be invoked manually in order to close an encoder before the end of its
+ // lexical scope.
+ //
+ // Precondition: Encoder has no active child encoder.
+ //
+ // Postcondition: If this encoder is a nested one, the parent encoder is
+ // unlocked and proto encoding may resume on the parent. No more writes
+ // to this encoder may be performed.
+ void CloseEncoder();
+
// Forwards the conservative write limit of the underlying
// pw::stream::Writer.
//
@@ -145,6 +160,8 @@ class StreamEncoder {
return writer_.ConservativeWriteLimit();
}
+ enum class EmptyEncoderBehavior { kWriteFieldNumber, kWriteNothing };
+
// Creates a nested encoder with the provided field number. Once this is
// called, the parent encoder is locked and not available for use until the
// nested encoder is finalized (either explicitly or through destruction).
@@ -153,7 +170,13 @@ class StreamEncoder {
//
// Postcondition: Until the nested child encoder has been destroyed, this
// encoder cannot be used.
- StreamEncoder GetNestedEncoder(uint32_t field_number);
+ StreamEncoder GetNestedEncoder(uint32_t field_number,
+ EmptyEncoderBehavior empty_encoder_behavior =
+ EmptyEncoderBehavior::kWriteFieldNumber) {
+ return GetNestedEncoder(
+ field_number, /*write_when_empty=*/
+ empty_encoder_behavior == EmptyEncoderBehavior::kWriteFieldNumber);
+ }
// Returns the current encoder's status.
//
@@ -173,9 +196,9 @@ class StreamEncoder {
// Writes a repeated uint32 using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedUint32(uint32_t field_number,
- std::span<const uint32_t> values) {
- return WritePackedVarints(field_number, values, VarintEncodeType::kNormal);
+ Status WritePackedUint32(uint32_t field_number, span<const uint32_t> values) {
+ return WritePackedVarints(
+ field_number, values, internal::VarintType::kNormal);
}
// Writes a repeated uint32 using packed encoding.
@@ -184,8 +207,8 @@ class StreamEncoder {
Status WriteRepeatedUint32(uint32_t field_number,
const pw::Vector<uint32_t>& values) {
return WritePackedVarints(field_number,
- std::span(values.data(), values.size()),
- VarintEncodeType::kNormal);
+ span(values.data(), values.size()),
+ internal::VarintType::kNormal);
}
// Writes a proto uint64 key-value pair.
@@ -198,9 +221,9 @@ class StreamEncoder {
// Writes a repeated uint64 using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedUint64(uint64_t field_number,
- std::span<const uint64_t> values) {
- return WritePackedVarints(field_number, values, VarintEncodeType::kNormal);
+ Status WritePackedUint64(uint64_t field_number, span<const uint64_t> values) {
+ return WritePackedVarints(
+ field_number, values, internal::VarintType::kNormal);
}
// Writes a repeated uint64 using packed encoding.
@@ -209,8 +232,8 @@ class StreamEncoder {
Status WriteRepeatedUint64(uint32_t field_number,
const pw::Vector<uint64_t>& values) {
return WritePackedVarints(field_number,
- std::span(values.data(), values.size()),
- VarintEncodeType::kNormal);
+ span(values.data(), values.size()),
+ internal::VarintType::kNormal);
}
// Writes a proto int32 key-value pair.
@@ -223,13 +246,11 @@ class StreamEncoder {
// Writes a repeated int32 using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedInt32(uint32_t field_number,
- std::span<const int32_t> values) {
+ Status WritePackedInt32(uint32_t field_number, span<const int32_t> values) {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint32_t*>(values.data()),
- values.size()),
- VarintEncodeType::kNormal);
+ span(reinterpret_cast<const uint32_t*>(values.data()), values.size()),
+ internal::VarintType::kNormal);
}
// Writes a repeated int32 using packed encoding.
@@ -239,9 +260,8 @@ class StreamEncoder {
const pw::Vector<int32_t>& values) {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint32_t*>(values.data()),
- values.size()),
- VarintEncodeType::kNormal);
+ span(reinterpret_cast<const uint32_t*>(values.data()), values.size()),
+ internal::VarintType::kNormal);
}
// Writes a proto int64 key-value pair.
@@ -254,13 +274,11 @@ class StreamEncoder {
// Writes a repeated int64 using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedInt64(uint32_t field_number,
- std::span<const int64_t> values) {
+ Status WritePackedInt64(uint32_t field_number, span<const int64_t> values) {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint64_t*>(values.data()),
- values.size()),
- VarintEncodeType::kNormal);
+ span(reinterpret_cast<const uint64_t*>(values.data()), values.size()),
+ internal::VarintType::kNormal);
}
// Writes a repeated int64 using packed encoding.
@@ -270,9 +288,8 @@ class StreamEncoder {
const pw::Vector<int64_t>& values) {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint64_t*>(values.data()),
- values.size()),
- VarintEncodeType::kNormal);
+ span(reinterpret_cast<const uint64_t*>(values.data()), values.size()),
+ internal::VarintType::kNormal);
}
// Writes a proto sint32 key-value pair.
@@ -285,13 +302,11 @@ class StreamEncoder {
// Writes a repeated sint32 using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedSint32(uint32_t field_number,
- std::span<const int32_t> values) {
+ Status WritePackedSint32(uint32_t field_number, span<const int32_t> values) {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint32_t*>(values.data()),
- values.size()),
- VarintEncodeType::kZigZag);
+ span(reinterpret_cast<const uint32_t*>(values.data()), values.size()),
+ internal::VarintType::kZigZag);
}
// Writes a repeated sint32 using packed encoding.
@@ -301,9 +316,8 @@ class StreamEncoder {
const pw::Vector<int32_t>& values) {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint32_t*>(values.data()),
- values.size()),
- VarintEncodeType::kZigZag);
+ span(reinterpret_cast<const uint32_t*>(values.data()), values.size()),
+ internal::VarintType::kZigZag);
}
// Writes a proto sint64 key-value pair.
@@ -316,13 +330,11 @@ class StreamEncoder {
// Writes a repeated sint64 using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedSint64(uint32_t field_number,
- std::span<const int64_t> values) {
+ Status WritePackedSint64(uint32_t field_number, span<const int64_t> values) {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint64_t*>(values.data()),
- values.size()),
- VarintEncodeType::kZigZag);
+ span(reinterpret_cast<const uint64_t*>(values.data()), values.size()),
+ internal::VarintType::kZigZag);
}
// Writes a repeated sint64 using packed encoding.
@@ -332,9 +344,8 @@ class StreamEncoder {
const pw::Vector<int64_t>& values) {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint64_t*>(values.data()),
- values.size()),
- VarintEncodeType::kZigZag);
+ span(reinterpret_cast<const uint64_t*>(values.data()), values.size()),
+ internal::VarintType::kZigZag);
}
// Writes a proto bool key-value pair.
@@ -347,14 +358,13 @@ class StreamEncoder {
// Writes a repeated bool using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedBool(uint32_t field_number, std::span<const bool> values) {
+ Status WritePackedBool(uint32_t field_number, span<const bool> values) {
static_assert(sizeof(bool) == sizeof(uint8_t),
"bool must be same size as uint8_t");
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint8_t*>(values.data()),
- values.size()),
- VarintEncodeType::kNormal);
+ span(reinterpret_cast<const uint8_t*>(values.data()), values.size()),
+ internal::VarintType::kNormal);
}
// Writes a repeated bool using packed encoding.
@@ -367,9 +377,8 @@ class StreamEncoder {
return WritePackedVarints(
field_number,
- std::span(reinterpret_cast<const uint8_t*>(values.data()),
- values.size()),
- VarintEncodeType::kNormal);
+ span(reinterpret_cast<const uint8_t*>(values.data()), values.size()),
+ internal::VarintType::kNormal);
}
// Writes a proto fixed32 key-value pair.
@@ -377,7 +386,7 @@ class StreamEncoder {
// Precondition: Encoder has no active child encoder.
Status WriteFixed32(uint32_t field_number, uint32_t value) {
std::array<std::byte, sizeof(value)> data =
- bytes::CopyInOrder(std::endian::little, value);
+ bytes::CopyInOrder(endian::little, value);
return WriteFixed(field_number, data);
}
@@ -385,9 +394,8 @@ class StreamEncoder {
//
// Precondition: Encoder has no active child encoder.
Status WritePackedFixed32(uint32_t field_number,
- std::span<const uint32_t> values) {
- return WritePackedFixed(
- field_number, std::as_bytes(values), sizeof(uint32_t));
+ span<const uint32_t> values) {
+ return WritePackedFixed(field_number, as_bytes(values), sizeof(uint32_t));
}
// Writes a repeated fixed32 field using packed encoding.
@@ -395,10 +403,9 @@ class StreamEncoder {
// Precondition: Encoder has no active child encoder.
Status WriteRepeatedFixed32(uint32_t field_number,
const pw::Vector<uint32_t>& values) {
- return WritePackedFixed(
- field_number,
- std::as_bytes(std::span(values.data(), values.size())),
- sizeof(uint32_t));
+ return WritePackedFixed(field_number,
+ as_bytes(span(values.data(), values.size())),
+ sizeof(uint32_t));
}
// Writes a proto fixed64 key-value pair.
@@ -406,7 +413,7 @@ class StreamEncoder {
// Precondition: Encoder has no active child encoder.
Status WriteFixed64(uint32_t field_number, uint64_t value) {
std::array<std::byte, sizeof(value)> data =
- bytes::CopyInOrder(std::endian::little, value);
+ bytes::CopyInOrder(endian::little, value);
return WriteFixed(field_number, data);
}
@@ -414,9 +421,8 @@ class StreamEncoder {
//
// Precondition: Encoder has no active child encoder.
Status WritePackedFixed64(uint32_t field_number,
- std::span<const uint64_t> values) {
- return WritePackedFixed(
- field_number, std::as_bytes(values), sizeof(uint64_t));
+ span<const uint64_t> values) {
+ return WritePackedFixed(field_number, as_bytes(values), sizeof(uint64_t));
}
// Writes a repeated fixed64 field using packed encoding.
@@ -424,10 +430,9 @@ class StreamEncoder {
// Precondition: Encoder has no active child encoder.
Status WriteRepeatedFixed64(uint32_t field_number,
const pw::Vector<uint64_t>& values) {
- return WritePackedFixed(
- field_number,
- std::as_bytes(std::span(values.data(), values.size())),
- sizeof(uint64_t));
+ return WritePackedFixed(field_number,
+ as_bytes(span(values.data(), values.size())),
+ sizeof(uint64_t));
}
// Writes a proto sfixed32 key-value pair.
@@ -441,9 +446,8 @@ class StreamEncoder {
//
// Precondition: Encoder has no active child encoder.
Status WritePackedSfixed32(uint32_t field_number,
- std::span<const int32_t> values) {
- return WritePackedFixed(
- field_number, std::as_bytes(values), sizeof(int32_t));
+ span<const int32_t> values) {
+ return WritePackedFixed(field_number, as_bytes(values), sizeof(int32_t));
}
// Writes a repeated fixed32 field using packed encoding.
@@ -451,10 +455,9 @@ class StreamEncoder {
// Precondition: Encoder has no active child encoder.
Status WriteRepeatedSfixed32(uint32_t field_number,
const pw::Vector<int32_t>& values) {
- return WritePackedFixed(
- field_number,
- std::as_bytes(std::span(values.data(), values.size())),
- sizeof(int32_t));
+ return WritePackedFixed(field_number,
+ as_bytes(span(values.data(), values.size())),
+ sizeof(int32_t));
}
// Writes a proto sfixed64 key-value pair.
@@ -468,20 +471,18 @@ class StreamEncoder {
//
// Precondition: Encoder has no active child encoder.
Status WritePackedSfixed64(uint32_t field_number,
- std::span<const int64_t> values) {
- return WritePackedFixed(
- field_number, std::as_bytes(values), sizeof(int64_t));
+ span<const int64_t> values) {
+ return WritePackedFixed(field_number, as_bytes(values), sizeof(int64_t));
}
// Writes a repeated fixed64 field using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WriteRepeatedFixed64(uint32_t field_number,
- const pw::Vector<int64_t>& values) {
- return WritePackedFixed(
- field_number,
- std::as_bytes(std::span(values.data(), values.size())),
- sizeof(int64_t));
+ Status WriteRepeatedSfixed64(uint32_t field_number,
+ const pw::Vector<int64_t>& values) {
+ return WritePackedFixed(field_number,
+ as_bytes(span(values.data(), values.size())),
+ sizeof(int64_t));
}
// Writes a proto float key-value pair.
@@ -493,16 +494,15 @@ class StreamEncoder {
uint32_t integral_value;
std::memcpy(&integral_value, &value, sizeof(value));
std::array<std::byte, sizeof(value)> data =
- bytes::CopyInOrder(std::endian::little, integral_value);
+ bytes::CopyInOrder(endian::little, integral_value);
return WriteFixed(field_number, data);
}
// Writes a repeated float field using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedFloat(uint32_t field_number,
- std::span<const float> values) {
- return WritePackedFixed(field_number, std::as_bytes(values), sizeof(float));
+ Status WritePackedFloat(uint32_t field_number, span<const float> values) {
+ return WritePackedFixed(field_number, as_bytes(values), sizeof(float));
}
// Writes a repeated float field using packed encoding.
@@ -510,10 +510,9 @@ class StreamEncoder {
// Precondition: Encoder has no active child encoder.
Status WriteRepeatedFloat(uint32_t field_number,
const pw::Vector<float>& values) {
- return WritePackedFixed(
- field_number,
- std::as_bytes(std::span(values.data(), values.size())),
- sizeof(float));
+ return WritePackedFixed(field_number,
+ as_bytes(span(values.data(), values.size())),
+ sizeof(float));
}
// Writes a proto double key-value pair.
@@ -525,17 +524,15 @@ class StreamEncoder {
uint64_t integral_value;
std::memcpy(&integral_value, &value, sizeof(value));
std::array<std::byte, sizeof(value)> data =
- bytes::CopyInOrder(std::endian::little, integral_value);
+ bytes::CopyInOrder(endian::little, integral_value);
return WriteFixed(field_number, data);
}
// Writes a repeated double field using packed encoding.
//
// Precondition: Encoder has no active child encoder.
- Status WritePackedDouble(uint32_t field_number,
- std::span<const double> values) {
- return WritePackedFixed(
- field_number, std::as_bytes(values), sizeof(double));
+ Status WritePackedDouble(uint32_t field_number, span<const double> values) {
+ return WritePackedFixed(field_number, as_bytes(values), sizeof(double));
}
// Writes a repeated double field using packed encoding.
@@ -543,10 +540,9 @@ class StreamEncoder {
// Precondition: Encoder has no active child encoder.
Status WriteRepeatedDouble(uint32_t field_number,
const pw::Vector<double>& values) {
- return WritePackedFixed(
- field_number,
- std::as_bytes(std::span(values.data(), values.size())),
- sizeof(double));
+ return WritePackedFixed(field_number,
+ as_bytes(span(values.data(), values.size())),
+ sizeof(double));
}
// Writes a proto `bytes` field as a key-value pair. This can also be used to
@@ -586,14 +582,14 @@ class StreamEncoder {
//
// Precondition: Encoder has no active child encoder.
Status WriteString(uint32_t field_number, std::string_view value) {
- return WriteBytes(field_number, std::as_bytes(std::span(value)));
+ return WriteBytes(field_number, as_bytes(span<const char>(value)));
}
// Writes a proto string key-value pair.
//
// Precondition: Encoder has no active child encoder.
Status WriteString(uint32_t field_number, const char* value, size_t len) {
- return WriteBytes(field_number, std::as_bytes(std::span(value, len)));
+ return WriteBytes(field_number, as_bytes(span(value, len)));
}
// Writes a proto 'string' field from the stream bytes_reader.
@@ -627,6 +623,7 @@ class StreamEncoder {
// acts like a parent encoder with an active child encoder.
constexpr StreamEncoder(StreamEncoder&& other)
: status_(other.status_),
+ write_when_empty_(true),
parent_(other.parent_),
nested_field_number_(other.nested_field_number_),
memory_writer_(std::move(other.memory_writer_)),
@@ -639,17 +636,29 @@ class StreamEncoder {
other.parent_ = nullptr;
}
+ // Writes proto values to the stream from the structure contained within
+ // message, according to the description of fields in table.
+ //
+ // This is called by codegen subclass Write() functions that accept a typed
+ // struct Message reference, using the appropriate codegen MessageField table
+ // corresponding to that type.
+ Status Write(span<const std::byte> message,
+ span<const internal::MessageField> table);
+
+ // Protected method to create a nested encoder, specifying whether the field
+ // should be written when no fields were added to the nested encoder. Exposed
+ // using an enum in the public API, for better readability.
+ StreamEncoder GetNestedEncoder(uint32_t field_number, bool write_when_empty);
+
private:
friend class MemoryEncoder;
- enum class VarintEncodeType {
- kNormal,
- kZigZag,
- };
-
- constexpr StreamEncoder(StreamEncoder& parent, ByteSpan scratch_buffer)
+ constexpr StreamEncoder(StreamEncoder& parent,
+ ByteSpan scratch_buffer,
+ bool write_when_empty = true)
: status_(scratch_buffer.empty() ? Status::ResourceExhausted()
: OkStatus()),
+ write_when_empty_(write_when_empty),
parent_(&parent),
nested_field_number_(0),
memory_writer_(scratch_buffer),
@@ -692,8 +701,8 @@ class StreamEncoder {
// Writes a list of varints to the buffer in length-delimited packed encoding.
template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
Status WritePackedVarints(uint32_t field_number,
- std::span<T> values,
- VarintEncodeType encode_type) {
+ span<T> values,
+ internal::VarintType encode_type) {
static_assert(std::is_same<T, const uint8_t>::value ||
std::is_same<T, const uint32_t>::value ||
std::is_same<T, const int32_t>::value ||
@@ -704,7 +713,7 @@ class StreamEncoder {
size_t payload_size = 0;
for (T val : values) {
- if (encode_type == VarintEncodeType::kZigZag) {
+ if (encode_type == internal::VarintType::kZigZag) {
int64_t integer =
static_cast<int64_t>(static_cast<std::make_signed_t<T>>(val));
payload_size += varint::EncodedSize(varint::ZigZagEncode(integer));
@@ -720,16 +729,16 @@ class StreamEncoder {
}
WriteVarint(FieldKey(field_number, WireType::kDelimited))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
WriteVarint(payload_size)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
for (T value : values) {
- if (encode_type == VarintEncodeType::kZigZag) {
+ if (encode_type == internal::VarintType::kZigZag) {
WriteZigzagVarint(static_cast<std::make_signed_t<T>>(value))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
} else {
WriteVarint(value)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
}
@@ -740,9 +749,19 @@ class StreamEncoder {
// packed encoding. Only float, double, uint32_t, int32_t, uint64_t, and
// int64_t are permitted
Status WritePackedFixed(uint32_t field_number,
- std::span<const std::byte> values,
+ span<const std::byte> values,
size_t elem_size);
+ template <typename Container>
+ Status WriteStringOrBytes(uint32_t field_number,
+ const std::byte* raw_container) {
+ const auto& container = *reinterpret_cast<Container*>(raw_container);
+ if (container.empty()) {
+ return OkStatus();
+ }
+ return WriteLengthDelimitedField(field_number, as_bytes(span(container)));
+ }
+
// Checks if a write is invalid or will cause the encoder to enter an error
// state, and preemptively sets this encoder's status to that error to block
// the write. Only the first error encountered is tracked.
@@ -764,6 +783,10 @@ class StreamEncoder {
// encoder enters an error state.
Status status_;
+ // Checked by the parent when the nested encoder is closed, and if no bytes
+ // were written, the field is not written.
+ bool write_when_empty_;
+
// If this is a nested encoder, this points to the encoder that created it.
// For user-created MemoryEncoders, parent_ points to this object as an
// optimization for the MemoryEncoder and nested encoders to use the same
@@ -819,9 +842,65 @@ class MemoryEncoder : public StreamEncoder {
const std::byte* data() const { return memory_writer_.data(); }
size_t size() const { return memory_writer_.bytes_written(); }
+ const std::byte* begin() const { return data(); }
+ const std::byte* end() const { return data() + size(); }
+
protected:
// This is needed by codegen.
MemoryEncoder(MemoryEncoder&& other) = default;
};
+// pw_protobuf guarantees that all generated StreamEncoder classes can be
+// converted among each other. It's also safe to convert any MemoryEncoder to
+// any other StreamEncoder.
+//
+// This guarantee exists to facilitate usage of protobuf overlays. Protobuf
+// overlays are protobuf message definitions that deliberately ensure that
+// fields defined in one message will not conflict with fields defined in other
+// messages.
+//
+// Example:
+//
+// // The first half of the overlaid message.
+// message BaseMessage {
+// uint32 length = 1;
+// reserved 2; // Reserved for Overlay
+// }
+//
+// // OK: The second half of the overlaid message.
+// message Overlay {
+// reserved 1; // Reserved for BaseMessage
+// uint32 height = 2;
+// }
+//
+// // OK: A message that overlays and bundles both types together.
+// message Both {
+// uint32 length = 1; // Defined independently by BaseMessage
+// uint32 height = 2; // Defined independently by Overlay
+// }
+//
+// // BAD: Diverges from BaseMessage's definition, and can cause decode
+// // errors/corruption.
+// message InvalidOverlay {
+// fixed32 length = 1;
+// }
+//
+// While this use case is somewhat uncommon, it's a core supported use case of
+// pw_protobuf.
+//
+// Warning: Using this to convert one stream encoder to another when the
+// messages themselves do not safely overlay will result in corrupt protos.
+// Be careful when doing this as there's no compile-time way to detect whether
+// or not two messages are meant to overlay.
+template <typename ToStreamEncoder, typename FromStreamEncoder>
+inline ToStreamEncoder& StreamEncoderCast(FromStreamEncoder& encoder) {
+ static_assert(std::is_base_of<StreamEncoder, FromStreamEncoder>::value,
+ "Provided argument is not a derived class of "
+ "pw::protobuf::StreamEncoder");
+ static_assert(std::is_base_of<StreamEncoder, ToStreamEncoder>::value,
+ "Cannot cast to a type that is not a derived class of "
+ "pw::protobuf::StreamEncoder");
+ return static_cast<ToStreamEncoder&>(static_cast<StreamEncoder&>(encoder));
+}
+
} // namespace pw::protobuf
diff --git a/pw_protobuf/public/pw_protobuf/internal/codegen.h b/pw_protobuf/public/pw_protobuf/internal/codegen.h
new file mode 100644
index 000000000..39e6ccc5c
--- /dev/null
+++ b/pw_protobuf/public/pw_protobuf/internal/codegen.h
@@ -0,0 +1,245 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+#include "pw_function/function.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_protobuf/wire_format.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+// TODO(b/259746255): Remove this manual application of -Wconversion when all of
+// Pigweed builds with it.
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(error, "-Wconversion");
+
+namespace pw::protobuf {
+namespace internal {
+
+// Varints can be encoded as an unsigned type, a signed type with normal
+// encoding, or a signed type with zigzag encoding.
+enum class VarintType {
+ kUnsigned = 0,
+ kNormal = 1,
+ kZigZag = 2,
+};
+
+// Represents a field in a code generated message struct that can be the target
+// for decoding or source of encoding.
+//
+// An instance of this class exists for every field in every protobuf in the
+// binary, thus it is size critical to ensure efficiency while retaining enough
+// information to describe the layout of the generated message struct.
+//
+// Limitations imposed:
+// - Element size of a repeated fields must be no larger than 15 bytes.
+// (8 byte int64/fixed64/double is the largest supported element).
+// - Individual field size (including repeated and nested messages) must be no
+// larger than 64 KB. (This is already the maximum size of pw::Vector).
+//
+// A complete codegen struct is represented by a span<MessageField>,
+// holding a pointer to the MessageField members themselves, and the number of
+// fields in the struct. These spans are global data, one span per protobuf
+// message (including the size), and one MessageField per field in the message.
+//
+// Nested messages are handled with a pointer from the MessageField in the
+// parent to a pointer to the (global data) span. Since the size of the nested
+// message is stored as part of the global span, the cost of a nested message
+// is only the size of a pointer to that span.
+class MessageField {
+ public:
+ static constexpr unsigned int kMaxFieldSize = (1u << 16) - 1;
+
+ constexpr MessageField(uint32_t field_number,
+ WireType wire_type,
+ size_t elem_size,
+ VarintType varint_type,
+ bool is_string,
+ bool is_fixed_size,
+ bool is_repeated,
+ bool is_optional,
+ bool use_callback,
+ size_t field_offset,
+ size_t field_size,
+ const span<const MessageField>* nested_message_fields)
+ : field_number_(field_number),
+ field_info_(static_cast<uint32_t>(wire_type) << kWireTypeShift |
+ static_cast<uint32_t>(elem_size) << kElemSizeShift |
+ static_cast<uint32_t>(varint_type) << kVarintTypeShift |
+ static_cast<uint32_t>(is_string) << kIsStringShift |
+ static_cast<uint32_t>(is_fixed_size) << kIsFixedSizeShift |
+ static_cast<uint32_t>(is_repeated) << kIsRepeatedShift |
+ static_cast<uint32_t>(is_optional) << kIsOptionalShift |
+ static_cast<uint32_t>(use_callback) << kUseCallbackShift |
+ static_cast<uint32_t>(field_size) << kFieldSizeShift),
+ field_offset_(field_offset),
+ nested_message_fields_(nested_message_fields) {}
+
+ constexpr uint32_t field_number() const { return field_number_; }
+ constexpr WireType wire_type() const {
+ return static_cast<WireType>((field_info_ >> kWireTypeShift) &
+ kWireTypeMask);
+ }
+ constexpr size_t elem_size() const {
+ return (field_info_ >> kElemSizeShift) & kElemSizeMask;
+ }
+ constexpr VarintType varint_type() const {
+ return static_cast<VarintType>((field_info_ >> kVarintTypeShift) &
+ kVarintTypeMask);
+ }
+ constexpr bool is_string() const {
+ return (field_info_ >> kIsStringShift) & 1;
+ }
+ constexpr bool is_fixed_size() const {
+ return (field_info_ >> kIsFixedSizeShift) & 1;
+ }
+ constexpr bool is_repeated() const {
+ return (field_info_ >> kIsRepeatedShift) & 1;
+ }
+ constexpr bool is_optional() const {
+ return (field_info_ >> kIsOptionalShift) & 1;
+ }
+ constexpr bool use_callback() const {
+ return (field_info_ >> kUseCallbackShift) & 1;
+ }
+ constexpr size_t field_offset() const { return field_offset_; }
+ constexpr size_t field_size() const {
+ return (field_info_ >> kFieldSizeShift) & kFieldSizeMask;
+ }
+ constexpr const span<const MessageField>* nested_message_fields() const {
+ return nested_message_fields_;
+ }
+
+ constexpr bool operator==(uint32_t field_number) const {
+ return field_number == field_number_;
+ }
+
+ private:
+ // field_info_ packs multiple fields into a single word as follows:
+ //
+ // wire_type : 3
+ // varint_type : 2
+ // is_string : 1
+ // is_fixed_size : 1
+ // is_repeated : 1
+ // use_callback : 1
+ // -
+ // elem_size : 4
+ // is_optional : 1
+ // [unused space] : 2
+ // -
+ // field_size : 16
+ //
+ // The protobuf field type is spread among a few fields (wire_type,
+ // varint_type, is_string, elem_size). The exact field type (e.g. int32, bool,
+ // message, etc.), from which all of that information can be derived, can be
+ // represented in 4 bits. If more bits are needed in the future, these could
+ // be consolidated into a single field type enum.
+ static constexpr unsigned int kWireTypeShift = 29u;
+ static constexpr unsigned int kWireTypeMask = (1u << 3) - 1;
+ static constexpr unsigned int kVarintTypeShift = 27u;
+ static constexpr unsigned int kVarintTypeMask = (1u << 2) - 1;
+ static constexpr unsigned int kIsStringShift = 26u;
+ static constexpr unsigned int kIsFixedSizeShift = 25u;
+ static constexpr unsigned int kIsRepeatedShift = 24u;
+ static constexpr unsigned int kUseCallbackShift = 23u;
+ static constexpr unsigned int kElemSizeShift = 19u;
+ static constexpr unsigned int kElemSizeMask = (1u << 4) - 1;
+ static constexpr unsigned int kIsOptionalShift = 16u;
+ static constexpr unsigned int kFieldSizeShift = 0u;
+ static constexpr unsigned int kFieldSizeMask = kMaxFieldSize;
+
+ uint32_t field_number_;
+ uint32_t field_info_;
+ size_t field_offset_;
+ // TODO(b/234875722): Could be replaced by a class MessageDescriptor*
+ const span<const MessageField>* nested_message_fields_;
+};
+static_assert(sizeof(MessageField) <= sizeof(size_t) * 4,
+ "MessageField should be four words or less");
+
+template <typename...>
+constexpr std::false_type kInvalidMessageStruct{};
+
+} // namespace internal
+
+// Callback for a structure member that cannot be represented by a data type.
+// Holds either a callback for encoding a field, or a callback for decoding
+// a field.
+template <typename StreamEncoder, typename StreamDecoder>
+union Callback {
+ constexpr Callback() : encode_() {}
+ ~Callback() { encode_ = nullptr; }
+
+ // Set the encoder callback.
+ void SetEncoder(Function<Status(StreamEncoder& encoder)>&& encode) {
+ encode_ = std::move(encode);
+ }
+
+ // Set the decoder callback.
+ void SetDecoder(Function<Status(StreamDecoder& decoder)>&& decode) {
+ decode_ = std::move(decode);
+ }
+
+ // Allow moving of callbacks by moving the member.
+ constexpr Callback(Callback&& other) = default;
+ constexpr Callback& operator=(Callback&& other) = default;
+
+ // Copying a callback does not copy the functions.
+ constexpr Callback(const Callback&) : encode_() {}
+ constexpr Callback& operator=(const Callback&) {
+ encode_ = nullptr;
+ return *this;
+ }
+
+ private:
+ friend StreamDecoder;
+ friend StreamEncoder;
+
+ // Called by StreamEncoder to encode the structure member.
+ // Returns OkStatus() if this has not been set by the caller, the default
+ // behavior of a field without an encoder is the same as default-initialized
+ // field.
+ Status Encode(StreamEncoder& encoder) const {
+ if (encode_) {
+ return encode_(encoder);
+ }
+ return OkStatus();
+ }
+
+ // Called by StreamDecoder to decode the structure member when the field
+ // is present. Returns DataLoss() if this has not been set by the caller.
+ Status Decode(StreamDecoder& decoder) const {
+ if (decode_) {
+ return decode_(decoder);
+ }
+ return Status::DataLoss();
+ }
+
+ Function<Status(StreamEncoder& encoder)> encode_;
+ Function<Status(StreamDecoder& decoder)> decode_;
+};
+
+template <typename T>
+constexpr bool IsTriviallyComparable() {
+ static_assert(internal::kInvalidMessageStruct<T>,
+ "Not a generated message struct");
+ return false;
+}
+
+} // namespace pw::protobuf
+
+PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_protobuf/public/pw_protobuf/internal/proto_integer_base.h b/pw_protobuf/public/pw_protobuf/internal/proto_integer_base.h
index 2e81f3c4f..97f77f6bd 100644
--- a/pw_protobuf/public/pw_protobuf/internal/proto_integer_base.h
+++ b/pw_protobuf/public/pw_protobuf/internal/proto_integer_base.h
@@ -29,8 +29,6 @@ class ProtoIntegerBase {
constexpr ProtoIntegerBase(Result<Integer> value) : value_(value) {}
constexpr ProtoIntegerBase(Status status) : value_(status) {}
- // TODO(pwbug/363): Migrate this to Result<> once we have StatusOr like
- // support.
bool ok() { return value_.ok(); }
Status status() { return value_.status(); }
Integer value() { return value_.value(); }
diff --git a/pw_protobuf/public/pw_protobuf/message.h b/pw_protobuf/public/pw_protobuf/message.h
index b3a2ac5ac..c3b0e1066 100644
--- a/pw_protobuf/public/pw_protobuf/message.h
+++ b/pw_protobuf/public/pw_protobuf/message.h
@@ -113,14 +113,10 @@ class Bytes {
Bytes(stream::IntervalReader reader) : reader_(reader) {}
stream::IntervalReader GetBytesReader() { return reader_; }
- // TODO(pwbug/363): Migrate this to Result<> once we have StatusOr like
- // support.
bool ok() { return reader_.ok(); }
Status status() { return reader_.status(); }
// Check whether the bytes value equals the given `bytes`.
- // TODO(pwbug/456): Should this return `bool`? In the case of error, is it ok
- // to just return false?
Result<bool> Equal(ConstByteSpan bytes);
private:
@@ -397,8 +393,6 @@ class Message {
// message.
Bytes ToBytes() { return Bytes(reader_.Reset()); }
- // TODO(pwbug/363): Migrate this to Result<> once we have StatusOr like
- // support.
bool ok() { return reader_.ok(); }
Status status() { return reader_.status(); }
@@ -552,8 +546,6 @@ class RepeatedFieldParser {
RepeatedFieldParser(Status status) : message_(status) {}
- // TODO(pwbug/363): Migrate this to Result<> once we have StatusOr like
- // support.
bool ok() { return message_.ok(); }
Status status() { return message_.status(); }
diff --git a/pw_protobuf/public/pw_protobuf/serialized_size.h b/pw_protobuf/public/pw_protobuf/serialized_size.h
index 920bf9777..a2d368ca1 100644
--- a/pw_protobuf/public/pw_protobuf/serialized_size.h
+++ b/pw_protobuf/public/pw_protobuf/serialized_size.h
@@ -46,19 +46,17 @@ inline constexpr size_t kMaxSizeOfFieldNumber = varint::kMaxVarint32SizeBytes;
inline constexpr size_t kMaxSizeOfLength = varint::kMaxVarint32SizeBytes;
-// Calculate the size of a proto field key in wire format, including the key
-// field number + wire type.
-// type).
+// Calculate the serialized size of a proto tag (field number + wire type).
//
// Args:
// field_number: The field number for the field.
//
// Returns:
-// The size of the field key.
+// The size of the field's encoded tag.
//
// Precondition: The field_number must be a ValidFieldNumber.
template <typename T>
-constexpr size_t FieldNumberSizeBytes(T field_number) {
+constexpr size_t TagSizeBytes(T field_number) {
static_assert((std::is_enum<T>() || std::is_integral<T>()) &&
sizeof(T) <= sizeof(uint32_t),
"Field numbers must be 32-bit enums or integers");
@@ -71,7 +69,7 @@ constexpr size_t FieldNumberSizeBytes(T field_number) {
// Calculates the size of a varint field (uint32/64, int32/64, sint32/64, enum).
template <typename T, typename U>
constexpr size_t SizeOfVarintField(T field_number, U value) {
- return FieldNumberSizeBytes(field_number) + varint::EncodedSize(value);
+ return TagSizeBytes(field_number) + varint::EncodedSize(value);
}
// Calculates the size of a delimited field (string, bytes, nested message,
@@ -82,7 +80,7 @@ template <typename T>
constexpr size_t SizeOfDelimitedFieldWithoutValue(
T field_number,
uint32_t length_bytes = std::numeric_limits<uint32_t>::max()) {
- return FieldNumberSizeBytes(field_number) + varint::EncodedSize(length_bytes);
+ return TagSizeBytes(field_number) + varint::EncodedSize(length_bytes);
}
// Calculates the total size of a delimited field (string, bytes, nested
@@ -115,7 +113,7 @@ constexpr size_t SizeOfField(T field_number,
if (type == WireType::kDelimited) {
return SizeOfDelimitedField(field_number, data_size_bytes);
}
- return FieldNumberSizeBytes(field_number) + data_size_bytes;
+ return TagSizeBytes(field_number) + data_size_bytes;
}
// Functions for calculating the size of each type of protobuf field. Varint
@@ -123,11 +121,11 @@ constexpr size_t SizeOfField(T field_number,
// largest-to-encode value for the type.
template <typename T>
constexpr size_t SizeOfFieldFloat(T field_number) {
- return FieldNumberSizeBytes(field_number) + sizeof(float);
+ return TagSizeBytes(field_number) + sizeof(float);
}
template <typename T>
constexpr size_t SizeOfFieldDouble(T field_number) {
- return FieldNumberSizeBytes(field_number) + sizeof(double);
+ return TagSizeBytes(field_number) + sizeof(double);
}
template <typename T>
constexpr size_t SizeOfFieldInt32(T field_number, int32_t value = -1) {
@@ -159,23 +157,23 @@ constexpr size_t SizeOfFieldUint64(
}
template <typename T>
constexpr size_t SizeOfFieldFixed32(T field_number) {
- return FieldNumberSizeBytes(field_number) + sizeof(uint32_t);
+ return TagSizeBytes(field_number) + sizeof(uint32_t);
}
template <typename T>
constexpr size_t SizeOfFieldFixed64(T field_number) {
- return FieldNumberSizeBytes(field_number) + sizeof(uint64_t);
+ return TagSizeBytes(field_number) + sizeof(uint64_t);
}
template <typename T>
constexpr size_t SizeOfFieldSfixed32(T field_number) {
- return FieldNumberSizeBytes(field_number) + sizeof(uint32_t);
+ return TagSizeBytes(field_number) + sizeof(uint32_t);
}
template <typename T>
constexpr size_t SizeOfFieldSfixed64(T field_number) {
- return FieldNumberSizeBytes(field_number) + sizeof(uint64_t);
+ return TagSizeBytes(field_number) + sizeof(uint64_t);
}
template <typename T>
constexpr size_t SizeOfFieldBool(T field_number) {
- return FieldNumberSizeBytes(field_number) + 1;
+ return TagSizeBytes(field_number) + 1;
}
template <typename T>
constexpr size_t SizeOfFieldString(T field_number, uint32_t length_bytes) {
diff --git a/pw_protobuf/public/pw_protobuf/stream_decoder.h b/pw_protobuf/public/pw_protobuf/stream_decoder.h
index 37869b3a4..edbb8779b 100644
--- a/pw_protobuf/public/pw_protobuf/stream_decoder.h
+++ b/pw_protobuf/public/pw_protobuf/stream_decoder.h
@@ -16,12 +16,13 @@
#include <array>
#include <cstring>
#include <limits>
-#include <span>
#include <type_traits>
#include "pw_assert/assert.h"
#include "pw_containers/vector.h"
+#include "pw_protobuf/internal/codegen.h"
#include "pw_protobuf/wire_format.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_stream/stream.h"
@@ -72,7 +73,7 @@ class StreamDecoder {
// will also not.
class BytesReader : public stream::RelativeSeekableReader {
public:
- ~BytesReader() { decoder_.CloseBytesReader(*this); }
+ ~BytesReader() override { decoder_.CloseBytesReader(*this); }
constexpr size_t field_size() const { return end_offset_ - start_offset_; }
@@ -158,7 +159,7 @@ class StreamDecoder {
// Reads a proto int32 value from the current position.
Result<int32_t> ReadInt32() {
- return ReadVarintField<int32_t>(VarintDecodeType::kNormal);
+ return ReadVarintField<int32_t>(internal::VarintType::kNormal);
}
// Reads repeated int32 values from the current position using packed
@@ -166,21 +167,20 @@ class StreamDecoder {
//
// Returns the number of values read. In the case of error, the return value
// indicates the number of values successfully read, in addition to the error.
- StatusWithSize ReadPackedInt32(std::span<int32_t> out) {
- return ReadPackedVarintField(std::as_writable_bytes(out),
- sizeof(int32_t),
- VarintDecodeType::kNormal);
+ StatusWithSize ReadPackedInt32(span<int32_t> out) {
+ return ReadPackedVarintField(
+ as_writable_bytes(out), sizeof(int32_t), internal::VarintType::kNormal);
}
// Reads repeated int32 values from the current position into the vector,
// supporting either repeated single field elements or packed encoding.
Status ReadRepeatedInt32(pw::Vector<int32_t>& out) {
- return ReadRepeatedVarintField<int32_t>(out, VarintDecodeType::kNormal);
+ return ReadRepeatedVarintField<int32_t>(out, internal::VarintType::kNormal);
}
// Reads a proto uint32 value from the current position.
Result<uint32_t> ReadUint32() {
- return ReadVarintField<uint32_t>(VarintDecodeType::kUnsigned);
+ return ReadVarintField<uint32_t>(internal::VarintType::kUnsigned);
}
// Reads repeated uint32 values from the current position using packed
@@ -188,21 +188,22 @@ class StreamDecoder {
//
// Returns the number of values read. In the case of error, the return value
// indicates the number of values successfully read, in addition to the error.
- StatusWithSize ReadPackedUint32(std::span<uint32_t> out) {
- return ReadPackedVarintField(std::as_writable_bytes(out),
+ StatusWithSize ReadPackedUint32(span<uint32_t> out) {
+ return ReadPackedVarintField(as_writable_bytes(out),
sizeof(uint32_t),
- VarintDecodeType::kUnsigned);
+ internal::VarintType::kUnsigned);
}
// Reads repeated uint32 values from the current position into the vector,
// supporting either repeated single field elements or packed encoding.
Status ReadRepeatedUint32(pw::Vector<uint32_t>& out) {
- return ReadRepeatedVarintField<uint32_t>(out, VarintDecodeType::kUnsigned);
+ return ReadRepeatedVarintField<uint32_t>(out,
+ internal::VarintType::kUnsigned);
}
// Reads a proto int64 value from the current position.
Result<int64_t> ReadInt64() {
- return ReadVarintField<int64_t>(VarintDecodeType::kNormal);
+ return ReadVarintField<int64_t>(internal::VarintType::kNormal);
}
// Reads repeated int64 values from the current position using packed
@@ -211,21 +212,20 @@ class StreamDecoder {
// Returns the number of values read. In the case of error, the return value
// indicates the number of values successfully read, in addition to the
// error.
- StatusWithSize ReadPackedInt64(std::span<int64_t> out) {
- return ReadPackedVarintField(std::as_writable_bytes(out),
- sizeof(int64_t),
- VarintDecodeType::kNormal);
+ StatusWithSize ReadPackedInt64(span<int64_t> out) {
+ return ReadPackedVarintField(
+ as_writable_bytes(out), sizeof(int64_t), internal::VarintType::kNormal);
}
// Reads repeated int64 values from the current position into the vector,
// supporting either repeated single field elements or packed encoding.
Status ReadRepeatedInt64(pw::Vector<int64_t>& out) {
- return ReadRepeatedVarintField<int64_t>(out, VarintDecodeType::kNormal);
+ return ReadRepeatedVarintField<int64_t>(out, internal::VarintType::kNormal);
}
// Reads a proto uint64 value from the current position.
Result<uint64_t> ReadUint64() {
- return ReadVarintField<uint64_t>(VarintDecodeType::kUnsigned);
+ return ReadVarintField<uint64_t>(internal::VarintType::kUnsigned);
}
// Reads repeated uint64 values from the current position using packed
@@ -234,21 +234,22 @@ class StreamDecoder {
// Returns the number of values read. In the case of error, the return value
// indicates the number of values successfully read, in addition to the
// error.
- StatusWithSize ReadPackedUint64(std::span<uint64_t> out) {
- return ReadPackedVarintField(std::as_writable_bytes(out),
+ StatusWithSize ReadPackedUint64(span<uint64_t> out) {
+ return ReadPackedVarintField(as_writable_bytes(out),
sizeof(uint64_t),
- VarintDecodeType::kUnsigned);
+ internal::VarintType::kUnsigned);
}
// Reads repeated uint64 values from the current position into the vector,
// supporting either repeated single field elements or packed encoding.
Status ReadRepeatedUint64(pw::Vector<uint64_t>& out) {
- return ReadRepeatedVarintField<uint64_t>(out, VarintDecodeType::kUnsigned);
+ return ReadRepeatedVarintField<uint64_t>(out,
+ internal::VarintType::kUnsigned);
}
// Reads a proto sint32 value from the current position.
Result<int32_t> ReadSint32() {
- return ReadVarintField<int32_t>(VarintDecodeType::kZigZag);
+ return ReadVarintField<int32_t>(internal::VarintType::kZigZag);
}
// Reads repeated sint32 values from the current position using packed
@@ -257,21 +258,20 @@ class StreamDecoder {
// Returns the number of values read. In the case of error, the return value
// indicates the number of values successfully read, in addition to the
// error.
- StatusWithSize ReadPackedSint32(std::span<int32_t> out) {
- return ReadPackedVarintField(std::as_writable_bytes(out),
- sizeof(int32_t),
- VarintDecodeType::kZigZag);
+ StatusWithSize ReadPackedSint32(span<int32_t> out) {
+ return ReadPackedVarintField(
+ as_writable_bytes(out), sizeof(int32_t), internal::VarintType::kZigZag);
}
// Reads repeated sint32 values from the current position into the vector,
// supporting either repeated single field elements or packed encoding.
Status ReadRepeatedSint32(pw::Vector<int32_t>& out) {
- return ReadRepeatedVarintField<int32_t>(out, VarintDecodeType::kZigZag);
+ return ReadRepeatedVarintField<int32_t>(out, internal::VarintType::kZigZag);
}
// Reads a proto sint64 value from the current position.
Result<int64_t> ReadSint64() {
- return ReadVarintField<int64_t>(VarintDecodeType::kZigZag);
+ return ReadVarintField<int64_t>(internal::VarintType::kZigZag);
}
// Reads repeated int64 values from the current position using packed
@@ -280,21 +280,20 @@ class StreamDecoder {
// Returns the number of values read. In the case of error, the return value
// indicates the number of values successfully read, in addition to the
// error.
- StatusWithSize ReadPackedSint64(std::span<int64_t> out) {
- return ReadPackedVarintField(std::as_writable_bytes(out),
- sizeof(int64_t),
- VarintDecodeType::kZigZag);
+ StatusWithSize ReadPackedSint64(span<int64_t> out) {
+ return ReadPackedVarintField(
+ as_writable_bytes(out), sizeof(int64_t), internal::VarintType::kZigZag);
}
// Reads repeated sint64 values from the current position into the vector,
// supporting either repeated single field elements or packed encoding.
Status ReadRepeatedSint64(pw::Vector<int64_t>& out) {
- return ReadRepeatedVarintField<int64_t>(out, VarintDecodeType::kZigZag);
+ return ReadRepeatedVarintField<int64_t>(out, internal::VarintType::kZigZag);
}
// Reads a proto bool value from the current position.
Result<bool> ReadBool() {
- return ReadVarintField<bool>(VarintDecodeType::kUnsigned);
+ return ReadVarintField<bool>(internal::VarintType::kUnsigned);
}
// Reads repeated bool values from the current position using packed
@@ -303,15 +302,15 @@ class StreamDecoder {
// Returns the number of values read. In the case of error, the return value
// indicates the number of values successfully read, in addition to the
// error.
- StatusWithSize ReadPackedBool(std::span<bool> out) {
+ StatusWithSize ReadPackedBool(span<bool> out) {
return ReadPackedVarintField(
- std::as_writable_bytes(out), sizeof(bool), VarintDecodeType::kUnsigned);
+ as_writable_bytes(out), sizeof(bool), internal::VarintType::kUnsigned);
}
// Reads repeated bool values from the current position into the vector,
// supporting either repeated single field elements or packed encoding.
Status ReadRepeatedBool(pw::Vector<bool>& out) {
- return ReadRepeatedVarintField<bool>(out, VarintDecodeType::kUnsigned);
+ return ReadRepeatedVarintField<bool>(out, internal::VarintType::kUnsigned);
}
// Reads a proto fixed32 value from the current position.
@@ -321,8 +320,8 @@ class StreamDecoder {
// encoding.
//
// Returns the number of values read.
- StatusWithSize ReadPackedFixed32(std::span<uint32_t> out) {
- return ReadPackedFixedField(std::as_writable_bytes(out), sizeof(uint32_t));
+ StatusWithSize ReadPackedFixed32(span<uint32_t> out) {
+ return ReadPackedFixedField(as_writable_bytes(out), sizeof(uint32_t));
}
// Reads repeated fixed32 values from the current position into the vector,
@@ -338,8 +337,8 @@ class StreamDecoder {
// encoding.
//
// Returns the number of values read.
- StatusWithSize ReadPackedFixed64(std::span<uint64_t> out) {
- return ReadPackedFixedField(std::as_writable_bytes(out), sizeof(uint64_t));
+ StatusWithSize ReadPackedFixed64(span<uint64_t> out) {
+ return ReadPackedFixedField(as_writable_bytes(out), sizeof(uint64_t));
}
// Reads repeated fixed64 values from the current position into the vector,
@@ -355,8 +354,8 @@ class StreamDecoder {
// encoding.
//
// Returns the number of values read.
- StatusWithSize ReadPackedSfixed32(std::span<int32_t> out) {
- return ReadPackedFixedField(std::as_writable_bytes(out), sizeof(int32_t));
+ StatusWithSize ReadPackedSfixed32(span<int32_t> out) {
+ return ReadPackedFixedField(as_writable_bytes(out), sizeof(int32_t));
}
// Reads repeated sfixed32 values from the current position into the vector,
@@ -372,8 +371,8 @@ class StreamDecoder {
// encoding.
//
// Returns the number of values read.
- StatusWithSize ReadPackedSfixed64(std::span<int64_t> out) {
- return ReadPackedFixedField(std::as_writable_bytes(out), sizeof(int64_t));
+ StatusWithSize ReadPackedSfixed64(span<int64_t> out) {
+ return ReadPackedFixedField(as_writable_bytes(out), sizeof(int64_t));
}
// Reads repeated sfixed64 values from the current position into the vector,
@@ -393,10 +392,10 @@ class StreamDecoder {
// encoding.
//
// Returns the number of values read.
- StatusWithSize ReadPackedFloat(std::span<float> out) {
+ StatusWithSize ReadPackedFloat(span<float> out) {
static_assert(sizeof(float) == sizeof(uint32_t),
"Float and uint32_t must be the same size for protobufs");
- return ReadPackedFixedField(std::as_writable_bytes(out), sizeof(float));
+ return ReadPackedFixedField(as_writable_bytes(out), sizeof(float));
}
// Reads repeated float values from the current position into the vector,
@@ -416,10 +415,10 @@ class StreamDecoder {
// encoding.
//
// Returns the number of values read.
- StatusWithSize ReadPackedDouble(std::span<double> out) {
+ StatusWithSize ReadPackedDouble(span<double> out) {
static_assert(sizeof(double) == sizeof(uint64_t),
"Double and uint64_t must be the same size for protobufs");
- return ReadPackedFixedField(std::as_writable_bytes(out), sizeof(double));
+ return ReadPackedFixedField(as_writable_bytes(out), sizeof(double));
}
// Reads repeated double values from the current position into the vector,
@@ -436,8 +435,8 @@ class StreamDecoder {
// If the buffer is too small to fit the string value, RESOURCE_EXHAUSTED is
// returned and no data is read. The decoder's position remains on the
// string field.
- StatusWithSize ReadString(std::span<char> out) {
- return ReadBytes(std::as_writable_bytes(out));
+ StatusWithSize ReadString(span<char> out) {
+ return ReadBytes(as_writable_bytes(out));
}
// Reads a proto bytes value from the current position. The value is copied
@@ -449,7 +448,7 @@ class StreamDecoder {
//
// For larger bytes values that won't fit into memory, use GetBytesReader()
// to acquire a stream::Reader to the bytes instead.
- StatusWithSize ReadBytes(std::span<std::byte> out) {
+ StatusWithSize ReadBytes(span<std::byte> out) {
return ReadDelimitedField(out);
}
@@ -529,15 +528,18 @@ class StreamDecoder {
other.status_ = pw::Status::Cancelled();
}
+ // Reads proto values from the stream and decodes them into the structure
+ // contained within message according to the description of fields in table.
+ //
+ // This is called by codegen subclass Read() functions that accept a typed
+ // struct Message reference, using the appropriate codegen MessageField table
+ // corresponding to that type.
+ Status Read(span<std::byte> message,
+ span<const internal::MessageField> table);
+
private:
friend class BytesReader;
- enum class VarintDecodeType {
- kUnsigned,
- kNormal,
- kZigZag,
- };
-
// The FieldKey class can't store an invalid key, so pick a random large key
// to set as the initial value. This will be overwritten the first time Next()
// is called, and FieldKey() fails if Next() is not called first -- ensuring
@@ -580,20 +582,25 @@ class StreamDecoder {
Status Advance(size_t end_position);
+ size_t RemainingBytes() {
+ return stream_bounds_.high < std::numeric_limits<size_t>::max()
+ ? stream_bounds_.high - position_
+ : std::numeric_limits<size_t>::max();
+ }
+
void CloseBytesReader(BytesReader& reader);
void CloseNestedDecoder(StreamDecoder& nested);
Status ReadFieldKey();
Status SkipField();
- Status ReadVarintField(std::span<std::byte> out,
- VarintDecodeType decode_type);
+ Status ReadVarintField(span<std::byte> out, internal::VarintType decode_type);
- StatusWithSize ReadOneVarint(std::span<std::byte> out,
- VarintDecodeType decode_type);
+ StatusWithSize ReadOneVarint(span<std::byte> out,
+ internal::VarintType decode_type);
template <typename T>
- Result<T> ReadVarintField(VarintDecodeType decode_type) {
+ Result<T> ReadVarintField(internal::VarintType decode_type) {
static_assert(
std::is_same_v<T, bool> || std::is_same_v<T, uint32_t> ||
std::is_same_v<T, int32_t> || std::is_same_v<T, uint64_t> ||
@@ -602,8 +609,8 @@ class StreamDecoder {
"or int64_t");
T result;
- if (Status status = ReadVarintField(
- std::as_writable_bytes(std::span(&result, 1)), decode_type);
+ if (Status status =
+ ReadVarintField(as_writable_bytes(span(&result, 1)), decode_type);
!status.ok()) {
return status;
}
@@ -611,7 +618,7 @@ class StreamDecoder {
return result;
}
- Status ReadFixedField(std::span<std::byte> out);
+ Status ReadFixedField(span<std::byte> out);
template <typename T>
Result<T> ReadFixedField() {
@@ -620,8 +627,7 @@ class StreamDecoder {
"Protobuf fixed-size fields must be 32- or 64-bit");
T result;
- if (Status status =
- ReadFixedField(std::as_writable_bytes(std::span(&result, 1)));
+ if (Status status = ReadFixedField(as_writable_bytes(span(&result, 1)));
!status.ok()) {
return status;
}
@@ -629,14 +635,13 @@ class StreamDecoder {
return result;
}
- StatusWithSize ReadDelimitedField(std::span<std::byte> out);
+ StatusWithSize ReadDelimitedField(span<std::byte> out);
- StatusWithSize ReadPackedFixedField(std::span<std::byte> out,
- size_t elem_size);
+ StatusWithSize ReadPackedFixedField(span<std::byte> out, size_t elem_size);
- StatusWithSize ReadPackedVarintField(std::span<std::byte> out,
+ StatusWithSize ReadPackedVarintField(span<std::byte> out,
size_t elem_size,
- VarintDecodeType decode_type);
+ internal::VarintType decode_type);
template <typename T>
Status ReadRepeatedFixedField(pw::Vector<T>& out) {
@@ -647,15 +652,14 @@ class StreamDecoder {
if (current_field_.wire_type() == WireType::kDelimited) {
out.resize(out.capacity());
const auto sws = ReadPackedFixedField(
- std::as_writable_bytes(
- std::span(out.data() + old_size, out.size() - old_size)),
+ as_writable_bytes(span(out.data() + old_size, out.size() - old_size)),
sizeof(T));
out.resize(old_size + sws.size());
return sws.status();
} else {
out.resize(old_size + 1);
- const auto status = ReadFixedField(std::as_writable_bytes(
- std::span(out.data() + old_size, out.size() - old_size)));
+ const auto status = ReadFixedField(as_writable_bytes(
+ span(out.data() + old_size, out.size() - old_size)));
if (!status.ok()) {
out.resize(old_size);
}
@@ -665,7 +669,7 @@ class StreamDecoder {
template <typename T>
Status ReadRepeatedVarintField(pw::Vector<T>& out,
- VarintDecodeType decode_type) {
+ internal::VarintType decode_type) {
if (out.full()) {
return Status::ResourceExhausted();
}
@@ -673,18 +677,16 @@ class StreamDecoder {
if (current_field_.wire_type() == WireType::kDelimited) {
out.resize(out.capacity());
const auto sws = ReadPackedVarintField(
- std::as_writable_bytes(
- std::span(out.data() + old_size, out.size() - old_size)),
+ as_writable_bytes(span(out.data() + old_size, out.size() - old_size)),
sizeof(T),
decode_type);
out.resize(old_size + sws.size());
return sws.status();
} else {
out.resize(old_size + 1);
- const auto status =
- ReadVarintField(std::as_writable_bytes(std::span(
- out.data() + old_size, out.size() - old_size)),
- decode_type);
+ const auto status = ReadVarintField(
+ as_writable_bytes(span(out.data() + old_size, out.size() - old_size)),
+ decode_type);
if (!status.ok()) {
out.resize(old_size);
}
@@ -692,6 +694,18 @@ class StreamDecoder {
}
}
+ template <typename Container>
+ Status ReadStringOrBytesField(std::byte* raw_container) {
+ auto& container = *reinterpret_cast<Container*>(raw_container);
+ if (container.capacity() < delimited_field_size_) {
+ return Status::ResourceExhausted();
+ }
+ container.resize(container.capacity());
+ const auto sws = ReadDelimitedField(as_writable_bytes(span(container)));
+ container.resize(sws.size());
+ return sws.status();
+ }
+
Status CheckOkToRead(WireType type);
stream::Reader& reader_;
diff --git a/pw_protobuf/pw_protobuf_codegen_protos/codegen_options.proto b/pw_protobuf/pw_protobuf_codegen_protos/codegen_options.proto
new file mode 100644
index 000000000..6a2773beb
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_codegen_protos/codegen_options.proto
@@ -0,0 +1,34 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+syntax = "proto3";
+
+package pw.protobuf;
+
+message CodegenOptions {
+ // Maximum number of entries for repeated fields.
+ int32 max_count = 1;
+
+ // Use a fixed length std::array for repeated fields instead of pw::Vector.
+ bool fixed_count = 2;
+
+ // Maximum size for bytes and string fields.
+ int32 max_size = 3;
+
+ // Use a fixed size std::array for bytes fields instead of pw::Vector.
+ bool fixed_size = 4;
+
+ // Force the use of a callback function for the field.
+ bool use_callback = 5;
+}
diff --git a/pw_protobuf/pw_protobuf_protos/field_options.proto b/pw_protobuf/pw_protobuf_protos/field_options.proto
new file mode 100644
index 000000000..5d1567703
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_protos/field_options.proto
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+syntax = "proto3";
+
+import "google/protobuf/descriptor.proto";
+
+package pw.protobuf;
+
+// Options supported for inline usage in .proto files.
+//
+// These options are for use when there are true limitations intended to be part
+// of the protocol specification and are not specific to any particular protobuf
+// code generator.
+message FieldOptions {
+ // Maximum number of entries for repeated fields.
+ optional int32 max_count = 1;
+
+ // Maximum size for bytes and string fields (excludes null terminator).
+ optional int32 max_size = 2;
+}
+
+extend google.protobuf.FieldOptions {
+ // Extension 1155 is reserved upstream for PW protobuf compiler.
+ // https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md
+ optional FieldOptions pwpb = 1155;
+}
diff --git a/pw_protobuf/pw_protobuf_test_deps_protos/imported.options b/pw_protobuf/pw_protobuf_test_deps_protos/imported.options
new file mode 100644
index 000000000..621fe59b0
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_deps_protos/imported.options
@@ -0,0 +1,15 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+pw.protobuf.test.imported.Debug.message max_size:64
diff --git a/pw_protobuf/pw_protobuf_test_deps_protos/imported.proto b/pw_protobuf/pw_protobuf_test_deps_protos/imported.proto
new file mode 100644
index 000000000..faea08c69
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_deps_protos/imported.proto
@@ -0,0 +1,20 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+syntax = "proto3";
+
+package pw.protobuf.test.imported;
+
+message Debug {
+ string message = 1;
+}
diff --git a/pw_protobuf/pw_protobuf_test_protos/full_test.options b/pw_protobuf/pw_protobuf_test_protos/full_test.options
new file mode 100644
index 000000000..3d9f4b08b
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_protos/full_test.options
@@ -0,0 +1,26 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+pw.protobuf.test.Pigweed.error_message max_size:64
+pw.protobuf.test.Pigweed.data max_size:8 fixed_size:true
+pw.protobuf.test.Pigweed.special_property use_callback:true
+
+pw.protobuf.test.Pigweed.device_info use_callback:true
+pw.protobuf.test.DeviceInfo.device_name max_size:32
+
+pw.protobuf.test.Pigweed.Protobuf.Compiler.file_name max_size:48
+
+pw.protobuf.test.Function.Message.content max_size:128
+
+pw.protobuf.test.KeyValuePair.* max_size:32
diff --git a/pw_protobuf/pw_protobuf_test_protos/full_test.proto b/pw_protobuf/pw_protobuf_test_protos/full_test.proto
index b8a80fcd4..fa144e365 100644
--- a/pw_protobuf/pw_protobuf_test_protos/full_test.proto
+++ b/pw_protobuf/pw_protobuf_test_protos/full_test.proto
@@ -24,6 +24,18 @@ enum Bool {
FILE_NOT_FOUND = 2;
}
+// Prefixed enum
+enum Error {
+ ERROR_NONE = 0;
+ ERROR_NOT_FOUND = 1;
+ ERROR_UNKNOWN = 2;
+}
+
+// Single-value enum
+enum AlwaysBlue {
+ BLUE = 0;
+}
+
// A message!
message Pigweed {
// Nested messages and enums.
@@ -74,6 +86,13 @@ message Pigweed {
Proto proto = 9;
repeated Proto.ID id = 10;
+
+ // Fixed-length bytes field, a string with no maximum size specified in
+ // full_test.options, and a scalar with a forced callback.
+ bytes data = 11;
+ string description = 12;
+ uint32 special_property = 13;
+ int32 bungle = 14;
}
// Another message.
@@ -120,8 +139,75 @@ message Crate {
repeated Crate smaller_crates = 2;
}
+// Two messages that should properly overlay without collisions.
+message BaseMessage {
+ uint32 length = 1;
+ reserved 2;
+}
+
+message Overlay {
+ reserved 1;
+ uint32 height = 2;
+}
+
+// Ensure that reserved words are suffixed with underscores.
+message IntegerMetadata {
+ int32 bits = 1;
+ bool signed = 2;
+ bool null = 3;
+}
+
+// Enum values are handled differently from normal identifiers because they are
+// automatically case-converted in the generated code. Therefore, we append an
+// underscore if the output format exactly matches a reserved word or a
+// standard-library macro. This enum tests that underscores are appended in
+// cases where they're actually necessary but not when they can be skipped due
+// to case conversion.
+enum ReservedWord {
+ NULL = 0;
+ int = 1;
+ return = 2;
+ break = 3;
+ for // The linter wants a line break here.
+ = 4;
+ do // The linter wants a line break here.
+ = 5;
+ // Note: This obviously isn't anywhere near a complete list of C++ keywords
+ // and standard-library macros.
+}
+
+// Messages and enums named either `Message` or `Fields` should be appended with
+// underscores in generated C++ code so that they don't conflict with the
+// codegen internals. This is only a problem if such types are defined within
+// the scopes of other messages, as generated message structs automatically have
+// nested types `struct Message` and `enum class Fields`. However, we simply
+// apply the transformation in all contexts for consistency and simplicity.
+message Function {
+ message Message {
+ string content = 1;
+ }
+
+ enum Fields {
+ NONE = 0;
+ COMPLEX_NUMBERS = 1;
+ INTEGERS_MOD_5 = 2;
+ MEROMORPHIC_FUNCTIONS_ON_COMPLEX_PLANE = 3;
+ OTHER = 4;
+ }
+
+ Message description = 1;
+ Fields domain_field = 2;
+ Fields codomain_field = 3;
+}
+
// This might be useful.
message KeyValuePair {
string key = 1;
string value = 2;
}
+
+// Corner cases of code generation.
+message CornerCases {
+ // Generates ReadUint32() and WriteUint32() that call the parent definition.
+ uint32 _uint32 = 1;
+}
diff --git a/pw_protobuf/pw_protobuf_test_protos/imported.options b/pw_protobuf/pw_protobuf_test_protos/imported.options
new file mode 100644
index 000000000..cbf447cc1
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_protos/imported.options
@@ -0,0 +1,15 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+pw.protobuf.test.imported.Notice.message max_size:64
diff --git a/pw_protobuf/pw_protobuf_test_protos/imported.proto b/pw_protobuf/pw_protobuf_test_protos/imported.proto
index 1ba1403e2..7520a6365 100644
--- a/pw_protobuf/pw_protobuf_test_protos/imported.proto
+++ b/pw_protobuf/pw_protobuf_test_protos/imported.proto
@@ -25,3 +25,7 @@ enum Status {
NOT_OK = 1;
OK = 2;
}
+
+message Notice {
+ string message = 1;
+}
diff --git a/pw_protobuf/pw_protobuf_test_protos/importer.proto b/pw_protobuf/pw_protobuf_test_protos/importer.proto
index 00bec36cd..c8b50672b 100644
--- a/pw_protobuf/pw_protobuf_test_protos/importer.proto
+++ b/pw_protobuf/pw_protobuf_test_protos/importer.proto
@@ -13,6 +13,7 @@
// the License.
syntax = "proto3";
+import 'pw_protobuf_test_deps_protos/imported.proto';
import 'pw_protobuf_test_protos/imported.proto';
import 'pw_protobuf_protos/common.proto';
@@ -30,3 +31,10 @@ message Nothing {
message TestResult {
imported.Status status = 1;
}
+
+message TestMessage {
+ oneof level {
+ imported.Notice notice = 1;
+ imported.Debug debug = 2;
+ }
+}
diff --git a/pw_protobuf/pw_protobuf_test_protos/optional.options b/pw_protobuf/pw_protobuf_test_protos/optional.options
new file mode 100644
index 000000000..722e699f4
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_protos/optional.options
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+pw.protobuf.test.OptionalTest.sometimes_empty_fixed max_count:4
+pw.protobuf.test.OptionalTest.sometimes_empty_varint max_count:4
diff --git a/pw_protobuf/pw_protobuf_test_protos/optional.proto b/pw_protobuf/pw_protobuf_test_protos/optional.proto
new file mode 100644
index 000000000..b75cd178a
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_protos/optional.proto
@@ -0,0 +1,25 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+syntax = "proto3";
+
+package pw.protobuf.test;
+
+message OptionalTest {
+ sfixed32 sometimes_present_fixed = 1;
+ int32 sometimes_present_varint = 2;
+ optional sfixed32 explicitly_present_fixed = 3;
+ optional int32 explicitly_present_varint = 4;
+ repeated sfixed32 sometimes_empty_fixed = 5;
+ repeated int32 sometimes_empty_varint = 6;
+};
diff --git a/pw_protobuf/pw_protobuf_test_protos/repeated.options b/pw_protobuf/pw_protobuf_test_protos/repeated.options
new file mode 100644
index 000000000..67e699635
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_protos/repeated.options
@@ -0,0 +1,19 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+pw.protobuf.test.RepeatedTest.uint32s max_count:8
+pw.protobuf.test.RepeatedTest.fixed32s max_count:8
+pw.protobuf.test.RepeatedTest.doubles max_count:2 fixed_count:true
+pw.protobuf.test.RepeatedTest.uint64s max_count:4 fixed_count:true
+pw.protobuf.test.RepeatedTest.enums max_count:4
diff --git a/pw_protobuf/pw_protobuf_test_protos/repeated.proto b/pw_protobuf/pw_protobuf_test_protos/repeated.proto
index 590d2e00c..98502e85b 100644
--- a/pw_protobuf/pw_protobuf_test_protos/repeated.proto
+++ b/pw_protobuf/pw_protobuf_test_protos/repeated.proto
@@ -23,9 +23,17 @@ message RepeatedTest {
repeated Struct structs = 5;
repeated fixed32 fixed32s = 6;
repeated bool bools = 7;
+ repeated uint64 uint64s = 8;
+ repeated Enum enums = 9;
};
message Struct {
uint32 one = 1;
uint32 two = 2;
}
+
+enum Enum {
+ RED = 0;
+ AMBER = 1;
+ GREEN = 2;
+}
diff --git a/pw_protobuf/pw_protobuf_test_protos/size_report.options b/pw_protobuf/pw_protobuf_test_protos/size_report.options
new file mode 100644
index 000000000..2b63e6939
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_protos/size_report.options
@@ -0,0 +1,15 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+pw.protobuf_size_report.Reponse.responses max_count:4
diff --git a/pw_protobuf/pw_protobuf_test_protos/size_report.proto b/pw_protobuf/pw_protobuf_test_protos/size_report.proto
new file mode 100644
index 000000000..55783f20a
--- /dev/null
+++ b/pw_protobuf/pw_protobuf_test_protos/size_report.proto
@@ -0,0 +1,57 @@
+// Copyright 2019 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+syntax = "proto3";
+
+package pw.protobuf_size_report;
+
+message ItemInfo {
+ enum Access {
+ NONE = 0;
+ READ = 1;
+ WRITE = 2;
+ READ_AND_WRITE = 3;
+ }
+ uint64 offset = 1;
+ uint32 size = 2;
+ Access access_level = 3;
+}
+
+message ResponseInfo {
+ oneof key {
+ string key_string = 1;
+ fixed32 key_token = 2;
+ }
+
+ optional int64 timestamp = 3;
+ optional bool has_value = 4;
+ ItemInfo item_info = 5;
+}
+
+message Response {
+ repeated ResponseInfo responses = 1;
+}
+
+message LookupRequest {
+ message AuthInfo {
+ optional uint32 id = 1;
+ optional uint32 token = 2;
+ }
+ oneof key {
+ string key_string = 1;
+ fixed32 key_token = 2;
+ }
+ optional uint32 items_per_response = 3;
+ AuthInfo auth_info = 4;
+ bool add_timestamp = 5;
+}
diff --git a/pw_protobuf/py/Android.bp b/pw_protobuf/py/Android.bp
new file mode 100644
index 000000000..0e2439c6f
--- /dev/null
+++ b/pw_protobuf/py/Android.bp
@@ -0,0 +1,44 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+python_binary_host {
+ name: "pw_protobuf_plugin_py",
+ main: "pw_protobuf/plugin.py",
+ srcs: [
+ "pw_protobuf/plugin.py",
+ ],
+ libs: [
+ "libprotobuf-python",
+ "pw_protobuf_plugin_py_lib",
+ ],
+}
+
+python_library_host {
+ name: "pw_protobuf_plugin_py_lib",
+ srcs: [
+ "pw_protobuf/*.py",
+ ],
+ exclude_srcs: [
+ "pw_protobuf/plugin.py",
+ ],
+ libs: [
+ "libprotobuf-python",
+ "pw_protobuf_codegen_protos_py_lib",
+ "pw_protobuf_protos_py_lib",
+ ],
+}
diff --git a/pw_protobuf/py/BUILD.bazel b/pw_protobuf/py/BUILD.bazel
index c288141ea..c3668335e 100644
--- a/pw_protobuf/py/BUILD.bazel
+++ b/pw_protobuf/py/BUILD.bazel
@@ -23,9 +23,11 @@ filegroup(
srcs = [
"pw_protobuf/__init__.py",
"pw_protobuf/codegen_pwpb.py",
+ "pw_protobuf/options.py",
"pw_protobuf/output_file.py",
"pw_protobuf/plugin.py",
"pw_protobuf/proto_tree.py",
+ "pw_protobuf/symbol_name_mapping.py",
],
)
@@ -33,7 +35,11 @@ py_library(
name = "plugin_library",
srcs = [":pw_protobuf_common_sources"],
imports = ["."],
- deps = ["@com_google_protobuf//:protobuf_python"],
+ deps = [
+ "//pw_protobuf:codegen_protos_pb2",
+ "//pw_protobuf:field_options_proto_pb2",
+ "@com_google_protobuf//:protobuf_python",
+ ],
)
py_binary(
diff --git a/pw_protobuf/py/BUILD.gn b/pw_protobuf/py/BUILD.gn
index f920ef1bf..c9e40192d 100644
--- a/pw_protobuf/py/BUILD.gn
+++ b/pw_protobuf/py/BUILD.gn
@@ -25,10 +25,17 @@ pw_python_package("py") {
sources = [
"pw_protobuf/__init__.py",
"pw_protobuf/codegen_pwpb.py",
+ "pw_protobuf/options.py",
"pw_protobuf/output_file.py",
"pw_protobuf/plugin.py",
"pw_protobuf/proto_tree.py",
+ "pw_protobuf/symbol_name_mapping.py",
+ ]
+ python_deps = [
+ "$dir_pw_cli/py",
+ "..:codegen_protos.python",
+ "..:common_protos.python",
]
- python_deps = [ "$dir_pw_cli/py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
index fc28fba72..dd862645b 100644
--- a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
+++ b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
@@ -14,11 +14,14 @@
"""This module defines the generated code for pw_protobuf C++ classes."""
import abc
-from datetime import datetime
import enum
+
+# Type ignore here for graphlib-backport on Python 3.8
+from graphlib import CycleError, TopologicalSorter # type: ignore
+from itertools import takewhile
import os
import sys
-from typing import Dict, Iterable, List, Tuple
+from typing import Dict, Iterable, List, Optional, Tuple
from typing import cast
from google.protobuf import descriptor_pb2
@@ -27,6 +30,7 @@ from pw_protobuf.output_file import OutputFile
from pw_protobuf.proto_tree import ProtoEnum, ProtoMessage, ProtoMessageField
from pw_protobuf.proto_tree import ProtoNode
from pw_protobuf.proto_tree import build_node_tree
+from pw_protobuf.proto_tree import EXTERNAL_SYMBOL_WORKAROUND_NAMESPACE
PLUGIN_NAME = 'pw_protobuf'
PLUGIN_VERSION = '0.1.0'
@@ -35,10 +39,12 @@ PROTO_H_EXTENSION = '.pwpb.h'
PROTO_CC_EXTENSION = '.pwpb.cc'
PROTOBUF_NAMESPACE = '::pw::protobuf'
+_INTERNAL_NAMESPACE = '::pw::protobuf::internal'
class ClassType(enum.Enum):
"""Type of class."""
+
MEMORY_ENCODER = 1
STREAMING_ENCODER = 2
# MEMORY_DECODER = 3
@@ -83,15 +89,13 @@ def debug_print(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
-class ProtoMethod(abc.ABC):
- """Base class for a C++ method for a field in a protobuf message."""
+class ProtoMember(abc.ABC):
+ """Base class for a C++ class member for a field in a protobuf message."""
+
def __init__(
- self,
- field: ProtoMessageField,
- scope: ProtoNode,
- root: ProtoNode,
+ self, field: ProtoMessageField, scope: ProtoNode, root: ProtoNode
):
- """Creates an instance of a method.
+ """Creates an instance of a class member.
Args:
field: the ProtoMessageField to which the method belongs.
@@ -103,7 +107,47 @@ class ProtoMethod(abc.ABC):
@abc.abstractmethod
def name(self) -> str:
- """Returns the name of the method, e.g. DoSomething."""
+ """Returns the name of the member, e.g. DoSomething."""
+
+ @abc.abstractmethod
+ def should_appear(self) -> bool: # pylint: disable=no-self-use
+ """Whether the member should be generated."""
+
+ def field_cast(self) -> str:
+ return 'static_cast<uint32_t>(Fields::{})'.format(
+ self._field.enum_name()
+ )
+
+ def _relative_type_namespace(self, from_root: bool = False) -> str:
+ """Returns relative namespace between member's scope and field type."""
+ scope = self._root if from_root else self._scope
+ type_node = self._field.type_node()
+ assert type_node is not None
+
+ # If a class method is referencing its class, the namespace provided
+ # must be from the root or it will be empty.
+ if type_node == scope:
+ scope = self._root
+
+ ancestor = scope.common_ancestor(type_node)
+ namespace = type_node.cpp_namespace(ancestor)
+
+ assert namespace
+ return namespace
+
+
+class ProtoMethod(ProtoMember):
+ """Base class for a C++ method for a field in a protobuf message."""
+
+ def __init__(
+ self,
+ field: ProtoMessageField,
+ scope: ProtoNode,
+ root: ProtoNode,
+ base_class: str,
+ ):
+ super().__init__(field, scope, root)
+ self._base_class: str = base_class
@abc.abstractmethod
def params(self) -> List[Tuple[str, str]]:
@@ -149,26 +193,6 @@ class ProtoMethod(abc.ABC):
def param_string(self) -> str:
return ', '.join([f'{type} {name}' for type, name in self.params()])
- def field_cast(self) -> str:
- return 'static_cast<uint32_t>(Fields::{})'.format(
- self._field.enum_name())
-
- def _relative_type_namespace(self, from_root: bool = False) -> str:
- """Returns relative namespace between method's scope and field type."""
- scope = self._root if from_root else self._scope
- type_node = self._field.type_node()
- assert type_node is not None
-
- # If a class method is referencing its class, the namespace provided
- # must be from the root or it will be empty.
- if type_node == scope:
- scope = self._root
-
- ancestor = scope.common_ancestor(type_node)
- namespace = type_node.cpp_namespace(ancestor)
- assert namespace
- return namespace
-
class WriteMethod(ProtoMethod):
"""Base class representing an encoder write method.
@@ -180,6 +204,7 @@ class WriteMethod(ProtoMethod):
}
"""
+
def name(self) -> str:
return 'Write{}'.format(self._field.name())
@@ -188,8 +213,9 @@ class WriteMethod(ProtoMethod):
def body(self) -> List[str]:
params = ', '.join([pair[1] for pair in self.params()])
- line = 'return {}({}, {});'.format(self._encoder_fn(),
- self.field_cast(), params)
+ line = 'return {}::{}({}, {});'.format(
+ self._base_class, self._encoder_fn(), self.field_cast(), params
+ )
return [line]
def params(self) -> List[Tuple[str, str]]:
@@ -214,6 +240,7 @@ class PackedWriteMethod(WriteMethod):
Same as a WriteMethod, but is only generated for repeated fields.
"""
+
def should_appear(self) -> bool:
return self._field.is_repeated()
@@ -229,11 +256,13 @@ class ReadMethod(ProtoMethod):
Result<{ctype}> ReadFoo({params...}) {
Result<uint32_t> field_number = FieldNumber();
PW_ASSERT(field_number.ok());
- PW_ASSERT(field_number.value() == static_cast<uint32_t>(Fields::FOO));
+ PW_ASSERT(field_number.value() ==
+ static_cast<uint32_t>(Fields::kFoo));
return decoder_->Read{type}({params...});
}
"""
+
def name(self) -> str:
return 'Read{}'.format(self._field.name())
@@ -245,7 +274,7 @@ class ReadMethod(ProtoMethod):
Defined in subclasses.
- e.g. 'uint32_t', 'std::span<std::byte>', etc.
+ e.g. 'uint32_t', 'pw::span<std::byte>', etc.
"""
raise NotImplementedError()
@@ -262,7 +291,9 @@ class ReadMethod(ProtoMethod):
def _decoder_body(self) -> List[str]:
"""Returns the decoder body part as a list of source code lines."""
params = ', '.join([pair[1] for pair in self.params()])
- line = 'return {}({});'.format(self._decoder_fn(), params)
+ line = 'return {}::{}({});'.format(
+ self._base_class, self._decoder_fn(), params
+ )
return [line]
def _decoder_fn(self) -> str:
@@ -287,6 +318,7 @@ class PackedReadMethod(ReadMethod):
Same as ReadMethod, but is only generated for repeated fields.
"""
+
def should_appear(self) -> bool:
return self._field.is_repeated()
@@ -294,7 +326,7 @@ class PackedReadMethod(ReadMethod):
return '::pw::StatusWithSize'
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<{}>'.format(self._result_type()), 'out')]
+ return [('pw::span<{}>'.format(self._result_type()), 'out')]
class PackedReadVectorMethod(ReadMethod):
@@ -303,6 +335,7 @@ class PackedReadVectorMethod(ReadMethod):
An alternative to ReadMethod for repeated fields that appends values into
a pw::Vector.
"""
+
def should_appear(self) -> bool:
return self._field.is_repeated()
@@ -313,6 +346,179 @@ class PackedReadVectorMethod(ReadMethod):
return [('::pw::Vector<{}>&'.format(self._result_type()), 'out')]
+class MessageProperty(ProtoMember):
+ """Base class for a C++ property for a field in a protobuf message."""
+
+ def name(self) -> str:
+ return self._field.field_name()
+
+ def should_appear(self) -> bool:
+ return True
+
+ @abc.abstractmethod
+ def type_name(self, from_root: bool = False) -> str:
+ """Returns the type of the property, e.g. uint32_t."""
+
+ @abc.abstractmethod
+ def wire_type(self) -> str:
+ """Returns the wire type of the property, e.g. kVarint."""
+
+ def varint_decode_type(self) -> str:
+ """Returns the varint decoding type of the property, e.g. kZigZag.
+
+ Defined in subclasses that return kVarint for wire_type().
+ """
+ raise NotImplementedError()
+
+ def is_string(self) -> bool: # pylint: disable=no-self-use
+ """True if this field is a string field (as opposed to bytes)."""
+ return False
+
+ @staticmethod
+ def repeated_field_container(type_name: str, max_size: int) -> str:
+ """Returns the container type used for repeated fields.
+
+ Defaults to ::pw::Vector<type, max_size>. String fields use
+ ::pw::InlineString<max_size> instead.
+ """
+ return f'::pw::Vector<{type_name}, {max_size}>'
+
+ def use_callback(self) -> bool: # pylint: disable=no-self-use
+ """Returns whether the decoder should use a callback."""
+ options = self._field.options()
+ assert options is not None
+ return options.use_callback or (
+ self._field.is_repeated() and self.max_size() == 0
+ )
+
+ def is_optional(self) -> bool:
+ """Returns whether the decoder should use std::optional."""
+ return (
+ self._field.is_optional()
+ and self.max_size() == 0
+ and self.wire_type() != 'kDelimited'
+ )
+
+ def is_repeated(self) -> bool:
+ return self._field.is_repeated()
+
+ def max_size(self) -> int:
+ """Returns the maximum size of the field."""
+ if self._field.is_repeated():
+ options = self._field.options()
+ assert options is not None
+ return options.max_count
+
+ return 0
+
+ def is_fixed_size(self) -> bool:
+ """Returns whether the decoder should use a fixed sized field."""
+ if self._field.is_repeated():
+ options = self._field.options()
+ assert options is not None
+ return options.fixed_count
+
+ return False
+
+ def sub_table(self) -> str: # pylint: disable=no-self-use
+ return '{}'
+
+ def struct_member(self, from_root: bool = False) -> Tuple[str, str]:
+ """Returns the structure member."""
+ if self.use_callback():
+ return (
+ f'{PROTOBUF_NAMESPACE}::Callback'
+ '<StreamEncoder, StreamDecoder>',
+ self.name(),
+ )
+
+ # Optional fields are wrapped in std::optional
+ if self.is_optional():
+ return (
+ 'std::optional<{}>'.format(self.type_name(from_root)),
+ self.name(),
+ )
+
+ # Non-repeated fields have a member of just the type name.
+ max_size = self.max_size()
+ if max_size == 0:
+ return (self.type_name(from_root), self.name())
+
+ # Fixed size fields use std::array.
+ if self.is_fixed_size():
+ return (
+ 'std::array<{}, {}>'.format(
+ self.type_name(from_root), max_size
+ ),
+ self.name(),
+ )
+
+ # Otherwise prefer pw::Vector for repeated fields.
+ return (
+ self.repeated_field_container(self.type_name(from_root), max_size),
+ self.name(),
+ )
+
+ def _varint_type_table_entry(self) -> str:
+ if self.wire_type() == 'kVarint':
+ return '{}::VarintType::{}'.format(
+ _INTERNAL_NAMESPACE, self.varint_decode_type()
+ )
+
+ return f'static_cast<{_INTERNAL_NAMESPACE}::VarintType>(0)'
+
+ def _wire_type_table_entry(self) -> str:
+ return '{}::WireType::{}'.format(PROTOBUF_NAMESPACE, self.wire_type())
+
+ def _elem_size_table_entry(self) -> str:
+ return 'sizeof({})'.format(self.type_name())
+
+ def _bool_attr(self, attr: str) -> str:
+ """C++ string for a bool argument that includes the argument name."""
+ return f'/*{attr}=*/{bool(getattr(self, attr)())}'.lower()
+
+ def table_entry(self) -> List[str]:
+ """Table entry."""
+ return [
+ self.field_cast(),
+ self._wire_type_table_entry(),
+ self._elem_size_table_entry(),
+ self._varint_type_table_entry(),
+ self._bool_attr('is_string'),
+ self._bool_attr('is_fixed_size'),
+ self._bool_attr('is_repeated'),
+ self._bool_attr('is_optional'),
+ self._bool_attr('use_callback'),
+ 'offsetof(Message, {})'.format(self.name()),
+ 'sizeof(Message::{})'.format(self.name()),
+ self.sub_table(),
+ ]
+
+ @abc.abstractmethod
+ def _size_fn(self) -> str:
+ """Returns the name of the field size function."""
+
+ def _size_length(self) -> Optional[str]: # pylint: disable=no-self-use
+ """Returns the length to add to the maximum encoded size."""
+ return None
+
+ def max_encoded_size(self) -> str:
+ """Returns a constant expression for field's maximum encoded size."""
+ size_call = '{}::{}({})'.format(
+ PROTOBUF_NAMESPACE, self._size_fn(), self.field_cast()
+ )
+
+ size_length: Optional[str] = self._size_length()
+ if size_length is None:
+ return size_call
+
+ return f'{size_call} + {size_length}'
+
+ def include_in_scratch_size(self) -> bool: # pylint: disable=no-self-use
+ """Returns whether the field contributes to the scratch buffer size."""
+ return False
+
+
#
# The following code defines write and read methods for each of the
# complex protobuf types.
@@ -321,19 +527,22 @@ class PackedReadVectorMethod(ReadMethod):
class SubMessageEncoderMethod(ProtoMethod):
"""Method which returns a sub-message encoder."""
+
def name(self) -> str:
return 'Get{}Encoder'.format(self._field.name())
def return_type(self, from_root: bool = False) -> str:
return '{}::StreamEncoder'.format(
- self._relative_type_namespace(from_root))
+ self._relative_type_namespace(from_root)
+ )
def params(self) -> List[Tuple[str, str]]:
return []
def body(self) -> List[str]:
- line = 'return {}::StreamEncoder(GetNestedEncoder({}));'.format(
- self._relative_type_namespace(), self.field_cast())
+ line = 'return {}::StreamEncoder({}::GetNestedEncoder({}));'.format(
+ self._relative_type_namespace(), self._base_class, self.field_cast()
+ )
return [line]
# Submessage methods are not defined within the class itself because the
@@ -344,16 +553,19 @@ class SubMessageEncoderMethod(ProtoMethod):
class SubMessageDecoderMethod(ReadMethod):
"""Method which returns a sub-message decoder."""
+
def name(self) -> str:
return 'Get{}Decoder'.format(self._field.name())
def return_type(self, from_root: bool = False) -> str:
return '{}::StreamDecoder'.format(
- self._relative_type_namespace(from_root))
+ self._relative_type_namespace(from_root)
+ )
def _decoder_body(self) -> List[str]:
line = 'return {}::StreamDecoder(GetNestedDecoder());'.format(
- self._relative_type_namespace())
+ self._relative_type_namespace()
+ )
return [line]
# Submessage methods are not defined within the class itself because the
@@ -362,13 +574,77 @@ class SubMessageDecoderMethod(ReadMethod):
return False
+class SubMessageProperty(MessageProperty):
+ """Property which contains a sub-message."""
+
+ def _dependency_removed(self) -> bool:
+ """Returns true if the message dependency was removed to break a cycle.
+
+ Proto allows cycles between messages, but C++ doesn't allow cycles
+ between class references. So when we're forced to break one, the
+ struct member is replaced with a callback.
+ """
+ type_node = self._field.type_node()
+ assert type_node is not None
+ return type_node in cast(ProtoMessage, self._scope).dependency_cycles()
+
+ def _elem_size_table_entry(self) -> str:
+ # Since messages can't be repeated (as we couldn't set callbacks),
+ # only field size is used. Set elem_size to 0 so space can be saved by
+ # not using more than 4 bits for it.
+ return '0'
+
+ def type_name(self, from_root: bool = False) -> str:
+ return '{}::Message'.format(self._relative_type_namespace(from_root))
+
+ def use_callback(self) -> bool:
+ # Always use a callback for a message dependency removed to break a
+ # cycle, and for repeated fields, since in both cases there's no way
+ # to handle the size of nested field.
+ options = self._field.options()
+ assert options is not None
+ return (
+ options.use_callback
+ or self._dependency_removed()
+ or self._field.is_repeated()
+ )
+
+ def wire_type(self) -> str:
+ return 'kDelimited'
+
+ def sub_table(self) -> str:
+ if self.use_callback():
+ return 'nullptr'
+
+ return '&{}::kMessageFields'.format(self._relative_type_namespace())
+
+ def _size_fn(self) -> str:
+ # This uses the WithoutValue method to ensure that the maximum length
+ # of the delimited field size varint is used. This is because the nested
+ # message might include callbacks and be longer than we expect, and to
+ # account for scratch overhead when used with MemoryEncoder.
+ return 'SizeOfDelimitedFieldWithoutValue'
+
+ def _size_length(self) -> Optional[str]:
+ if self.use_callback():
+ return None
+
+ return '{}::kMaxEncodedSizeBytes'.format(
+ self._relative_type_namespace()
+ )
+
+ def include_in_scratch_size(self) -> bool:
+ return True
+
+
class BytesReaderMethod(ReadMethod):
"""Method which returns a bytes reader."""
+
def name(self) -> str:
return 'Get{}Reader'.format(self._field.name())
def return_type(self, from_root: bool = False) -> str:
- return '::pw::protobuf::StreamDecoder::BytesReader'
+ return f'{PROTOBUF_NAMESPACE}::StreamDecoder::BytesReader'
def _decoder_fn(self) -> str:
return 'GetBytesReader'
@@ -382,6 +658,7 @@ class BytesReaderMethod(ReadMethod):
class DoubleWriteMethod(WriteMethod):
"""Method which writes a proto double value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('double', 'value')]
@@ -391,8 +668,9 @@ class DoubleWriteMethod(WriteMethod):
class PackedDoubleWriteMethod(PackedWriteMethod):
"""Method which writes a packed list of doubles."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const double>', 'values')]
+ return [('pw::span<const double>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedDouble'
@@ -400,6 +678,7 @@ class PackedDoubleWriteMethod(PackedWriteMethod):
class PackedDoubleWriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of doubles."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<double>&', 'values')]
@@ -409,6 +688,7 @@ class PackedDoubleWriteVectorMethod(PackedWriteMethod):
class DoubleReadMethod(ReadMethod):
"""Method which reads a proto double value."""
+
def _result_type(self) -> str:
return 'double'
@@ -418,6 +698,7 @@ class DoubleReadMethod(ReadMethod):
class PackedDoubleReadMethod(PackedReadMethod):
"""Method which reads packed double values."""
+
def _result_type(self) -> str:
return 'double'
@@ -427,6 +708,7 @@ class PackedDoubleReadMethod(PackedReadMethod):
class PackedDoubleReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed double values."""
+
def _result_type(self) -> str:
return 'double'
@@ -434,8 +716,22 @@ class PackedDoubleReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedDouble'
+class DoubleProperty(MessageProperty):
+ """Property which holds a proto double value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'double'
+
+ def wire_type(self) -> str:
+ return 'kFixed64'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldDouble'
+
+
class FloatWriteMethod(WriteMethod):
"""Method which writes a proto float value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('float', 'value')]
@@ -445,8 +741,9 @@ class FloatWriteMethod(WriteMethod):
class PackedFloatWriteMethod(PackedWriteMethod):
"""Method which writes a packed list of floats."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const float>', 'values')]
+ return [('pw::span<const float>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedFloat'
@@ -454,6 +751,7 @@ class PackedFloatWriteMethod(PackedWriteMethod):
class PackedFloatWriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of floats."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<float>&', 'values')]
@@ -463,6 +761,7 @@ class PackedFloatWriteVectorMethod(PackedWriteMethod):
class FloatReadMethod(ReadMethod):
"""Method which reads a proto float value."""
+
def _result_type(self) -> str:
return 'float'
@@ -472,6 +771,7 @@ class FloatReadMethod(ReadMethod):
class PackedFloatReadMethod(PackedReadMethod):
"""Method which reads packed float values."""
+
def _result_type(self) -> str:
return 'float'
@@ -481,6 +781,7 @@ class PackedFloatReadMethod(PackedReadMethod):
class PackedFloatReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed float values."""
+
def _result_type(self) -> str:
return 'float'
@@ -488,8 +789,22 @@ class PackedFloatReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedFloat'
+class FloatProperty(MessageProperty):
+ """Property which holds a proto float value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'float'
+
+ def wire_type(self) -> str:
+ return 'kFixed32'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldFloat'
+
+
class Int32WriteMethod(WriteMethod):
"""Method which writes a proto int32 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('int32_t', 'value')]
@@ -499,8 +814,9 @@ class Int32WriteMethod(WriteMethod):
class PackedInt32WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of int32."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const int32_t>', 'values')]
+ return [('pw::span<const int32_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedInt32'
@@ -508,6 +824,7 @@ class PackedInt32WriteMethod(PackedWriteMethod):
class PackedInt32WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of int32."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<int32_t>&', 'values')]
@@ -517,6 +834,7 @@ class PackedInt32WriteVectorMethod(PackedWriteMethod):
class Int32ReadMethod(ReadMethod):
"""Method which reads a proto int32 value."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -526,6 +844,7 @@ class Int32ReadMethod(ReadMethod):
class PackedInt32ReadMethod(PackedReadMethod):
"""Method which reads packed int32 values."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -535,6 +854,7 @@ class PackedInt32ReadMethod(PackedReadMethod):
class PackedInt32ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed int32 values."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -542,8 +862,25 @@ class PackedInt32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedInt32'
+class Int32Property(MessageProperty):
+ """Property which holds a proto int32 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'int32_t'
+
+ def wire_type(self) -> str:
+ return 'kVarint'
+
+ def varint_decode_type(self) -> str:
+ return 'kNormal'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldInt32'
+
+
class Sint32WriteMethod(WriteMethod):
"""Method which writes a proto sint32 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('int32_t', 'value')]
@@ -553,8 +890,9 @@ class Sint32WriteMethod(WriteMethod):
class PackedSint32WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of sint32."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const int32_t>', 'values')]
+ return [('pw::span<const int32_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedSint32'
@@ -562,6 +900,7 @@ class PackedSint32WriteMethod(PackedWriteMethod):
class PackedSint32WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of sint32."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<int32_t>&', 'values')]
@@ -571,6 +910,7 @@ class PackedSint32WriteVectorMethod(PackedWriteMethod):
class Sint32ReadMethod(ReadMethod):
"""Method which reads a proto sint32 value."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -580,6 +920,7 @@ class Sint32ReadMethod(ReadMethod):
class PackedSint32ReadMethod(PackedReadMethod):
"""Method which reads packed sint32 values."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -589,6 +930,7 @@ class PackedSint32ReadMethod(PackedReadMethod):
class PackedSint32ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed sint32 values."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -596,8 +938,25 @@ class PackedSint32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedSint32'
+class Sint32Property(MessageProperty):
+ """Property which holds a proto sint32 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'int32_t'
+
+ def wire_type(self) -> str:
+ return 'kVarint'
+
+ def varint_decode_type(self) -> str:
+ return 'kZigZag'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldSint32'
+
+
class Sfixed32WriteMethod(WriteMethod):
"""Method which writes a proto sfixed32 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('int32_t', 'value')]
@@ -607,8 +966,9 @@ class Sfixed32WriteMethod(WriteMethod):
class PackedSfixed32WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of sfixed32."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const int32_t>', 'values')]
+ return [('pw::span<const int32_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedSfixed32'
@@ -616,6 +976,7 @@ class PackedSfixed32WriteMethod(PackedWriteMethod):
class PackedSfixed32WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of sfixed32."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<int32_t>&', 'values')]
@@ -625,6 +986,7 @@ class PackedSfixed32WriteVectorMethod(PackedWriteMethod):
class Sfixed32ReadMethod(ReadMethod):
"""Method which reads a proto sfixed32 value."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -634,6 +996,7 @@ class Sfixed32ReadMethod(ReadMethod):
class PackedSfixed32ReadMethod(PackedReadMethod):
"""Method which reads packed sfixed32 values."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -643,6 +1006,7 @@ class PackedSfixed32ReadMethod(PackedReadMethod):
class PackedSfixed32ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed sfixed32 values."""
+
def _result_type(self) -> str:
return 'int32_t'
@@ -650,8 +1014,22 @@ class PackedSfixed32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedSfixed32'
+class Sfixed32Property(MessageProperty):
+ """Property which holds a proto sfixed32 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'int32_t'
+
+ def wire_type(self) -> str:
+ return 'kFixed32'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldSfixed32'
+
+
class Int64WriteMethod(WriteMethod):
"""Method which writes a proto int64 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('int64_t', 'value')]
@@ -661,8 +1039,9 @@ class Int64WriteMethod(WriteMethod):
class PackedInt64WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of int64."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const int64_t>', 'values')]
+ return [('pw::span<const int64_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedInt64'
@@ -670,6 +1049,7 @@ class PackedInt64WriteMethod(PackedWriteMethod):
class PackedInt64WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of int64."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<int64_t>&', 'values')]
@@ -679,6 +1059,7 @@ class PackedInt64WriteVectorMethod(PackedWriteMethod):
class Int64ReadMethod(ReadMethod):
"""Method which reads a proto int64 value."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -688,6 +1069,7 @@ class Int64ReadMethod(ReadMethod):
class PackedInt64ReadMethod(PackedReadMethod):
"""Method which reads packed int64 values."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -697,6 +1079,7 @@ class PackedInt64ReadMethod(PackedReadMethod):
class PackedInt64ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed int64 values."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -704,8 +1087,25 @@ class PackedInt64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedInt64'
+class Int64Property(MessageProperty):
+ """Property which holds a proto int64 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'int64_t'
+
+ def wire_type(self) -> str:
+ return 'kVarint'
+
+ def varint_decode_type(self) -> str:
+ return 'kNormal'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldInt64'
+
+
class Sint64WriteMethod(WriteMethod):
"""Method which writes a proto sint64 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('int64_t', 'value')]
@@ -715,8 +1115,9 @@ class Sint64WriteMethod(WriteMethod):
class PackedSint64WriteMethod(PackedWriteMethod):
"""Method which writes a packst list of sint64."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const int64_t>', 'values')]
+ return [('pw::span<const int64_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedSint64'
@@ -724,6 +1125,7 @@ class PackedSint64WriteMethod(PackedWriteMethod):
class PackedSint64WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of sint64."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<int64_t>&', 'values')]
@@ -733,6 +1135,7 @@ class PackedSint64WriteVectorMethod(PackedWriteMethod):
class Sint64ReadMethod(ReadMethod):
"""Method which reads a proto sint64 value."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -742,6 +1145,7 @@ class Sint64ReadMethod(ReadMethod):
class PackedSint64ReadMethod(PackedReadMethod):
"""Method which reads packed sint64 values."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -751,6 +1155,7 @@ class PackedSint64ReadMethod(PackedReadMethod):
class PackedSint64ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed sint64 values."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -758,8 +1163,25 @@ class PackedSint64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedSint64'
+class Sint64Property(MessageProperty):
+ """Property which holds a proto sint64 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'int64_t'
+
+ def wire_type(self) -> str:
+ return 'kVarint'
+
+ def varint_decode_type(self) -> str:
+ return 'kZigZag'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldSint64'
+
+
class Sfixed64WriteMethod(WriteMethod):
"""Method which writes a proto sfixed64 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('int64_t', 'value')]
@@ -769,8 +1191,9 @@ class Sfixed64WriteMethod(WriteMethod):
class PackedSfixed64WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of sfixed64."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const int64_t>', 'values')]
+ return [('pw::span<const int64_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedSfixed4'
@@ -778,6 +1201,7 @@ class PackedSfixed64WriteMethod(PackedWriteMethod):
class PackedSfixed64WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of sfixed64."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<int64_t>&', 'values')]
@@ -787,6 +1211,7 @@ class PackedSfixed64WriteVectorMethod(PackedWriteMethod):
class Sfixed64ReadMethod(ReadMethod):
"""Method which reads a proto sfixed64 value."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -796,6 +1221,7 @@ class Sfixed64ReadMethod(ReadMethod):
class PackedSfixed64ReadMethod(PackedReadMethod):
"""Method which reads packed sfixed64 values."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -805,6 +1231,7 @@ class PackedSfixed64ReadMethod(PackedReadMethod):
class PackedSfixed64ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed sfixed64 values."""
+
def _result_type(self) -> str:
return 'int64_t'
@@ -812,8 +1239,22 @@ class PackedSfixed64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedSfixed64'
+class Sfixed64Property(MessageProperty):
+ """Property which holds a proto sfixed64 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'int64_t'
+
+ def wire_type(self) -> str:
+ return 'kFixed64'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldSfixed64'
+
+
class Uint32WriteMethod(WriteMethod):
"""Method which writes a proto uint32 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('uint32_t', 'value')]
@@ -823,8 +1264,9 @@ class Uint32WriteMethod(WriteMethod):
class PackedUint32WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of uint32."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const uint32_t>', 'values')]
+ return [('pw::span<const uint32_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedUint32'
@@ -832,6 +1274,7 @@ class PackedUint32WriteMethod(PackedWriteMethod):
class PackedUint32WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of uint32."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<uint32_t>&', 'values')]
@@ -841,6 +1284,7 @@ class PackedUint32WriteVectorMethod(PackedWriteMethod):
class Uint32ReadMethod(ReadMethod):
"""Method which reads a proto uint32 value."""
+
def _result_type(self) -> str:
return 'uint32_t'
@@ -850,6 +1294,7 @@ class Uint32ReadMethod(ReadMethod):
class PackedUint32ReadMethod(PackedReadMethod):
"""Method which reads packed uint32 values."""
+
def _result_type(self) -> str:
return 'uint32_t'
@@ -859,6 +1304,7 @@ class PackedUint32ReadMethod(PackedReadMethod):
class PackedUint32ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed uint32 values."""
+
def _result_type(self) -> str:
return 'uint32_t'
@@ -866,8 +1312,25 @@ class PackedUint32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedUint32'
+class Uint32Property(MessageProperty):
+ """Property which holds a proto uint32 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'uint32_t'
+
+ def wire_type(self) -> str:
+ return 'kVarint'
+
+ def varint_decode_type(self) -> str:
+ return 'kUnsigned'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldUint32'
+
+
class Fixed32WriteMethod(WriteMethod):
"""Method which writes a proto fixed32 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('uint32_t', 'value')]
@@ -877,8 +1340,9 @@ class Fixed32WriteMethod(WriteMethod):
class PackedFixed32WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of fixed32."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const uint32_t>', 'values')]
+ return [('pw::span<const uint32_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedFixed32'
@@ -886,6 +1350,7 @@ class PackedFixed32WriteMethod(PackedWriteMethod):
class PackedFixed32WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of fixed32."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<uint32_t>&', 'values')]
@@ -895,6 +1360,7 @@ class PackedFixed32WriteVectorMethod(PackedWriteMethod):
class Fixed32ReadMethod(ReadMethod):
"""Method which reads a proto fixed32 value."""
+
def _result_type(self) -> str:
return 'uint32_t'
@@ -904,6 +1370,7 @@ class Fixed32ReadMethod(ReadMethod):
class PackedFixed32ReadMethod(PackedReadMethod):
"""Method which reads packed fixed32 values."""
+
def _result_type(self) -> str:
return 'uint32_t'
@@ -913,6 +1380,7 @@ class PackedFixed32ReadMethod(PackedReadMethod):
class PackedFixed32ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed fixed32 values."""
+
def _result_type(self) -> str:
return 'uint32_t'
@@ -920,8 +1388,22 @@ class PackedFixed32ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedFixed32'
+class Fixed32Property(MessageProperty):
+ """Property which holds a proto fixed32 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'uint32_t'
+
+ def wire_type(self) -> str:
+ return 'kFixed32'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldFixed32'
+
+
class Uint64WriteMethod(WriteMethod):
"""Method which writes a proto uint64 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('uint64_t', 'value')]
@@ -931,8 +1413,9 @@ class Uint64WriteMethod(WriteMethod):
class PackedUint64WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of uint64."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const uint64_t>', 'values')]
+ return [('pw::span<const uint64_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedUint64'
@@ -940,6 +1423,7 @@ class PackedUint64WriteMethod(PackedWriteMethod):
class PackedUint64WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of uint64."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<uint64_t>&', 'values')]
@@ -949,6 +1433,7 @@ class PackedUint64WriteVectorMethod(PackedWriteMethod):
class Uint64ReadMethod(ReadMethod):
"""Method which reads a proto uint64 value."""
+
def _result_type(self) -> str:
return 'uint64_t'
@@ -958,6 +1443,7 @@ class Uint64ReadMethod(ReadMethod):
class PackedUint64ReadMethod(PackedReadMethod):
"""Method which reads packed uint64 values."""
+
def _result_type(self) -> str:
return 'uint64_t'
@@ -967,6 +1453,7 @@ class PackedUint64ReadMethod(PackedReadMethod):
class PackedUint64ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed uint64 values."""
+
def _result_type(self) -> str:
return 'uint64_t'
@@ -974,8 +1461,25 @@ class PackedUint64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedUint64'
+class Uint64Property(MessageProperty):
+ """Property which holds a proto uint64 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'uint64_t'
+
+ def wire_type(self) -> str:
+ return 'kVarint'
+
+ def varint_decode_type(self) -> str:
+ return 'kUnsigned'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldUint64'
+
+
class Fixed64WriteMethod(WriteMethod):
"""Method which writes a proto fixed64 value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('uint64_t', 'value')]
@@ -985,8 +1489,9 @@ class Fixed64WriteMethod(WriteMethod):
class PackedFixed64WriteMethod(PackedWriteMethod):
"""Method which writes a packed list of fixed64."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const uint64_t>', 'values')]
+ return [('pw::span<const uint64_t>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedFixed64'
@@ -994,6 +1499,7 @@ class PackedFixed64WriteMethod(PackedWriteMethod):
class PackedFixed64WriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed list of fixed64."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<uint64_t>&', 'values')]
@@ -1003,6 +1509,7 @@ class PackedFixed64WriteVectorMethod(PackedWriteMethod):
class Fixed64ReadMethod(ReadMethod):
"""Method which reads a proto fixed64 value."""
+
def _result_type(self) -> str:
return 'uint64_t'
@@ -1012,6 +1519,7 @@ class Fixed64ReadMethod(ReadMethod):
class PackedFixed64ReadMethod(PackedReadMethod):
"""Method which reads packed fixed64 values."""
+
def _result_type(self) -> str:
return 'uint64_t'
@@ -1021,6 +1529,7 @@ class PackedFixed64ReadMethod(PackedReadMethod):
class PackedFixed64ReadVectorMethod(PackedReadVectorMethod):
"""Method which reads packed fixed64 values."""
+
def _result_type(self) -> str:
return 'uint64_t'
@@ -1028,8 +1537,22 @@ class PackedFixed64ReadVectorMethod(PackedReadVectorMethod):
return 'ReadRepeatedFixed64'
+class Fixed64Property(MessageProperty):
+ """Property which holds a proto fixed64 value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'uint64_t'
+
+ def wire_type(self) -> str:
+ return 'kFixed64'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldFixed64'
+
+
class BoolWriteMethod(WriteMethod):
"""Method which writes a proto bool value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('bool', 'value')]
@@ -1039,8 +1562,9 @@ class BoolWriteMethod(WriteMethod):
class PackedBoolWriteMethod(PackedWriteMethod):
"""Method which writes a packed list of bools."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const bool>', 'values')]
+ return [('pw::span<const bool>', 'values')]
def _encoder_fn(self) -> str:
return 'WritePackedBool'
@@ -1048,6 +1572,7 @@ class PackedBoolWriteMethod(PackedWriteMethod):
class PackedBoolWriteVectorMethod(PackedWriteMethod):
"""Method which writes a packed vector of bools."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const ::pw::Vector<bool>&', 'values')]
@@ -1057,6 +1582,7 @@ class PackedBoolWriteVectorMethod(PackedWriteMethod):
class BoolReadMethod(ReadMethod):
"""Method which reads a proto bool value."""
+
def _result_type(self) -> str:
return 'bool'
@@ -1066,6 +1592,7 @@ class BoolReadMethod(ReadMethod):
class PackedBoolReadMethod(PackedReadMethod):
"""Method which reads packed bool values."""
+
def _result_type(self) -> str:
return 'bool'
@@ -1073,10 +1600,27 @@ class PackedBoolReadMethod(PackedReadMethod):
return 'ReadPackedBool'
+class BoolProperty(MessageProperty):
+ """Property which holds a proto bool value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'bool'
+
+ def wire_type(self) -> str:
+ return 'kVarint'
+
+ def varint_decode_type(self) -> str:
+ return 'kUnsigned'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldBool'
+
+
class BytesWriteMethod(WriteMethod):
"""Method which writes a proto bytes value."""
+
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<const std::byte>', 'value')]
+ return [('pw::span<const std::byte>', 'value')]
def _encoder_fn(self) -> str:
return 'WriteBytes'
@@ -1084,18 +1628,60 @@ class BytesWriteMethod(WriteMethod):
class BytesReadMethod(ReadMethod):
"""Method which reads a proto bytes value."""
+
def return_type(self, from_root: bool = False) -> str:
return '::pw::StatusWithSize'
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<std::byte>', 'out')]
+ return [('pw::span<std::byte>', 'out')]
def _decoder_fn(self) -> str:
return 'ReadBytes'
+class BytesProperty(MessageProperty):
+ """Property which holds a proto bytes value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'std::byte'
+
+ def use_callback(self) -> bool:
+ return self.max_size() == 0
+
+ def max_size(self) -> int:
+ if not self._field.is_repeated():
+ options = self._field.options()
+ assert options is not None
+ return options.max_size
+
+ return 0
+
+ def is_fixed_size(self) -> bool:
+ if not self._field.is_repeated():
+ options = self._field.options()
+ assert options is not None
+ return options.fixed_size
+
+ return False
+
+ def wire_type(self) -> str:
+ return 'kDelimited'
+
+ def _size_fn(self) -> str:
+ # This uses the WithoutValue method to ensure that the maximum length
+ # of the delimited field size varint is used. This accounts for scratch
+ # overhead when used with MemoryEncoder.
+ return 'SizeOfDelimitedFieldWithoutValue'
+
+ def _size_length(self) -> Optional[str]:
+ if self.use_callback():
+ return None
+ return f'{self.max_size()}'
+
+
class StringLenWriteMethod(WriteMethod):
"""Method which writes a proto string value with length."""
+
def params(self) -> List[Tuple[str, str]]:
return [('const char*', 'value'), ('size_t', 'len')]
@@ -1105,6 +1691,7 @@ class StringLenWriteMethod(WriteMethod):
class StringWriteMethod(WriteMethod):
"""Method which writes a proto string value."""
+
def params(self) -> List[Tuple[str, str]]:
return [('std::string_view', 'value')]
@@ -1114,24 +1701,72 @@ class StringWriteMethod(WriteMethod):
class StringReadMethod(ReadMethod):
"""Method which reads a proto string value."""
+
def return_type(self, from_root: bool = False) -> str:
return '::pw::StatusWithSize'
def params(self) -> List[Tuple[str, str]]:
- return [('std::span<char>', 'out')]
+ return [('pw::span<char>', 'out')]
def _decoder_fn(self) -> str:
return 'ReadString'
+class StringProperty(MessageProperty):
+ """Property which holds a proto string value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return 'char'
+
+ def use_callback(self) -> bool:
+ return self.max_size() == 0
+
+ def max_size(self) -> int:
+ if not self._field.is_repeated():
+ options = self._field.options()
+ assert options is not None
+ return options.max_size
+
+ return 0
+
+ def is_fixed_size(self) -> bool:
+ return False
+
+ def wire_type(self) -> str:
+ return 'kDelimited'
+
+ def is_string(self) -> bool:
+ return True
+
+ @staticmethod
+ def repeated_field_container(type_name: str, max_size: int) -> str:
+ return f'::pw::InlineBasicString<{type_name}, {max_size}>'
+
+ def _size_fn(self) -> str:
+ # This uses the WithoutValue method to ensure that the maximum length
+ # of the delimited field size varint is used. This accounts for scratch
+ # overhead when used with MemoryEncoder.
+ return 'SizeOfDelimitedFieldWithoutValue'
+
+ def _size_length(self) -> Optional[str]:
+ if self.use_callback():
+ return None
+ return f'{self.max_size()}'
+
+
class EnumWriteMethod(WriteMethod):
"""Method which writes a proto enum value."""
+
def params(self) -> List[Tuple[str, str]]:
return [(self._relative_type_namespace(), 'value')]
def body(self) -> List[str]:
- line = 'return WriteUint32(' \
- '{}, static_cast<uint32_t>(value));'.format(self.field_cast())
+ line = (
+ 'return {}::WriteUint32({}, '
+ 'static_cast<uint32_t>(value));'.format(
+ self._base_class, self.field_cast()
+ )
+ )
return [line]
def in_class_definition(self) -> bool:
@@ -1141,8 +1776,50 @@ class EnumWriteMethod(WriteMethod):
raise NotImplementedError()
+class PackedEnumWriteMethod(PackedWriteMethod):
+ """Method which writes a packed list of enum."""
+
+ def params(self) -> List[Tuple[str, str]]:
+ return [
+ (
+ 'pw::span<const {}>'.format(self._relative_type_namespace()),
+ 'values',
+ )
+ ]
+
+ def body(self) -> List[str]:
+ value_param = self.params()[0][1]
+ line = (
+ f'return {self._base_class}::WritePackedUint32('
+ f'{self.field_cast()}, pw::span(reinterpret_cast<const uint32_t*>('
+ f'{value_param}.data()), {value_param}.size()));'
+ )
+ return [line]
+
+ def in_class_definition(self) -> bool:
+ return True
+
+ def _encoder_fn(self) -> str:
+ raise NotImplementedError()
+
+
+class PackedEnumWriteVectorMethod(PackedEnumWriteMethod):
+ """Method which writes a packed vector of enum."""
+
+ def params(self) -> List[Tuple[str, str]]:
+ return [
+ (
+ 'const ::pw::Vector<{}>&'.format(
+ self._relative_type_namespace()
+ ),
+ 'values',
+ )
+ ]
+
+
class EnumReadMethod(ReadMethod):
"""Method which reads a proto enum value."""
+
def _result_type(self):
return self._relative_type_namespace()
@@ -1153,121 +1830,251 @@ class EnumReadMethod(ReadMethod):
lines += [' return value.status();']
lines += ['}']
- name_parts = self._relative_type_namespace().split('::')
- enum_name = name_parts.pop()
- function_name = '::'.join(name_parts + [f'Get{enum_name}'])
-
- lines += [f'return {function_name}(value.value());']
+ lines += [f'return static_cast<{self._result_type()}>(value.value());']
return lines
+class PackedEnumReadMethod(PackedReadMethod):
+ """Method which reads packed enum values."""
+
+ def _result_type(self):
+ return self._relative_type_namespace()
+
+ def _decoder_body(self) -> List[str]:
+ value_param = self.params()[0][1]
+ return [
+ f'return ReadPackedUint32('
+ f'pw::span(reinterpret_cast<uint32_t*>({value_param}.data()), '
+ f'{value_param}.size()));'
+ ]
+
+
+class PackedEnumReadVectorMethod(PackedReadVectorMethod):
+ """Method which reads packed enum values."""
+
+ def _result_type(self):
+ return self._relative_type_namespace()
+
+ def _decoder_body(self) -> List[str]:
+ value_param = self.params()[0][1]
+ return [
+ f'return ReadRepeatedUint32('
+ f'*reinterpret_cast<pw::Vector<uint32_t>*>(&{value_param}));'
+ ]
+
+
+class EnumProperty(MessageProperty):
+ """Property which holds a proto enum value."""
+
+ def type_name(self, from_root: bool = False) -> str:
+ return self._relative_type_namespace(from_root=from_root)
+
+ def wire_type(self) -> str:
+ return 'kVarint'
+
+ def varint_decode_type(self) -> str:
+ return 'kUnsigned'
+
+ def _size_fn(self) -> str:
+ return 'SizeOfFieldEnum'
+
+
# Mapping of protobuf field types to their method definitions.
PROTO_FIELD_WRITE_METHODS: Dict[int, List] = {
descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE: [
- DoubleWriteMethod, PackedDoubleWriteMethod,
- PackedDoubleWriteVectorMethod
+ DoubleWriteMethod,
+ PackedDoubleWriteMethod,
+ PackedDoubleWriteVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT: [
+ FloatWriteMethod,
+ PackedFloatWriteMethod,
+ PackedFloatWriteVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT32: [
+ Int32WriteMethod,
+ PackedInt32WriteMethod,
+ PackedInt32WriteVectorMethod,
],
- descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT:
- [FloatWriteMethod, PackedFloatWriteMethod, PackedFloatWriteVectorMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_INT32:
- [Int32WriteMethod, PackedInt32WriteMethod, PackedInt32WriteVectorMethod],
descriptor_pb2.FieldDescriptorProto.TYPE_SINT32: [
- Sint32WriteMethod, PackedSint32WriteMethod,
- PackedSint32WriteVectorMethod
+ Sint32WriteMethod,
+ PackedSint32WriteMethod,
+ PackedSint32WriteVectorMethod,
],
descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED32: [
- Sfixed32WriteMethod, PackedSfixed32WriteMethod,
- PackedSfixed32WriteVectorMethod
+ Sfixed32WriteMethod,
+ PackedSfixed32WriteMethod,
+ PackedSfixed32WriteVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT64: [
+ Int64WriteMethod,
+ PackedInt64WriteMethod,
+ PackedInt64WriteVectorMethod,
],
- descriptor_pb2.FieldDescriptorProto.TYPE_INT64:
- [Int64WriteMethod, PackedInt64WriteMethod, PackedInt64WriteVectorMethod],
descriptor_pb2.FieldDescriptorProto.TYPE_SINT64: [
- Sint64WriteMethod, PackedSint64WriteMethod,
- PackedSint64WriteVectorMethod
+ Sint64WriteMethod,
+ PackedSint64WriteMethod,
+ PackedSint64WriteVectorMethod,
],
descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED64: [
- Sfixed64WriteMethod, PackedSfixed64WriteMethod,
- PackedSfixed64WriteVectorMethod
+ Sfixed64WriteMethod,
+ PackedSfixed64WriteMethod,
+ PackedSfixed64WriteVectorMethod,
],
descriptor_pb2.FieldDescriptorProto.TYPE_UINT32: [
- Uint32WriteMethod, PackedUint32WriteMethod,
- PackedUint32WriteVectorMethod
+ Uint32WriteMethod,
+ PackedUint32WriteMethod,
+ PackedUint32WriteVectorMethod,
],
descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32: [
- Fixed32WriteMethod, PackedFixed32WriteMethod,
- PackedFixed32WriteVectorMethod
+ Fixed32WriteMethod,
+ PackedFixed32WriteMethod,
+ PackedFixed32WriteVectorMethod,
],
descriptor_pb2.FieldDescriptorProto.TYPE_UINT64: [
- Uint64WriteMethod, PackedUint64WriteMethod,
- PackedUint64WriteVectorMethod
+ Uint64WriteMethod,
+ PackedUint64WriteMethod,
+ PackedUint64WriteVectorMethod,
],
descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64: [
- Fixed64WriteMethod, PackedFixed64WriteMethod,
- PackedFixed64WriteVectorMethod
+ Fixed64WriteMethod,
+ PackedFixed64WriteMethod,
+ PackedFixed64WriteVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BOOL: [
+ BoolWriteMethod,
+ PackedBoolWriteMethod,
+ PackedBoolWriteVectorMethod,
],
- descriptor_pb2.FieldDescriptorProto.TYPE_BOOL:
- [BoolWriteMethod, PackedBoolWriteMethod, PackedBoolWriteVectorMethod],
descriptor_pb2.FieldDescriptorProto.TYPE_BYTES: [BytesWriteMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_STRING:
- [StringLenWriteMethod, StringWriteMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE:
- [SubMessageEncoderMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: [EnumWriteMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_STRING: [
+ StringLenWriteMethod,
+ StringWriteMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE: [SubMessageEncoderMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: [
+ EnumWriteMethod,
+ PackedEnumWriteMethod,
+ PackedEnumWriteVectorMethod,
+ ],
}
PROTO_FIELD_READ_METHODS: Dict[int, List] = {
- descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE:
- [DoubleReadMethod, PackedDoubleReadMethod, PackedDoubleReadVectorMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT:
- [FloatReadMethod, PackedFloatReadMethod, PackedFloatReadVectorMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_INT32:
- [Int32ReadMethod, PackedInt32ReadMethod, PackedInt32ReadVectorMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_SINT32:
- [Sint32ReadMethod, PackedSint32ReadMethod, PackedSint32ReadVectorMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE: [
+ DoubleReadMethod,
+ PackedDoubleReadMethod,
+ PackedDoubleReadVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT: [
+ FloatReadMethod,
+ PackedFloatReadMethod,
+ PackedFloatReadVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT32: [
+ Int32ReadMethod,
+ PackedInt32ReadMethod,
+ PackedInt32ReadVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT32: [
+ Sint32ReadMethod,
+ PackedSint32ReadMethod,
+ PackedSint32ReadVectorMethod,
+ ],
descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED32: [
- Sfixed32ReadMethod, PackedSfixed32ReadMethod,
- PackedSfixed32ReadVectorMethod
+ Sfixed32ReadMethod,
+ PackedSfixed32ReadMethod,
+ PackedSfixed32ReadVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT64: [
+ Int64ReadMethod,
+ PackedInt64ReadMethod,
+ PackedInt64ReadVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT64: [
+ Sint64ReadMethod,
+ PackedSint64ReadMethod,
+ PackedSint64ReadVectorMethod,
],
- descriptor_pb2.FieldDescriptorProto.TYPE_INT64:
- [Int64ReadMethod, PackedInt64ReadMethod, PackedInt64ReadVectorMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_SINT64:
- [Sint64ReadMethod, PackedSint64ReadMethod, PackedSint64ReadVectorMethod],
descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED64: [
- Sfixed64ReadMethod, PackedSfixed64ReadMethod,
- PackedSfixed64ReadVectorMethod
+ Sfixed64ReadMethod,
+ PackedSfixed64ReadMethod,
+ PackedSfixed64ReadVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT32: [
+ Uint32ReadMethod,
+ PackedUint32ReadMethod,
+ PackedUint32ReadVectorMethod,
],
- descriptor_pb2.FieldDescriptorProto.TYPE_UINT32:
- [Uint32ReadMethod, PackedUint32ReadMethod, PackedUint32ReadVectorMethod],
descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32: [
- Fixed32ReadMethod, PackedFixed32ReadMethod,
- PackedFixed32ReadVectorMethod
+ Fixed32ReadMethod,
+ PackedFixed32ReadMethod,
+ PackedFixed32ReadVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT64: [
+ Uint64ReadMethod,
+ PackedUint64ReadMethod,
+ PackedUint64ReadVectorMethod,
],
- descriptor_pb2.FieldDescriptorProto.TYPE_UINT64:
- [Uint64ReadMethod, PackedUint64ReadMethod, PackedUint64ReadVectorMethod],
descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64: [
- Fixed64ReadMethod, PackedFixed64ReadMethod,
- PackedFixed64ReadVectorMethod
+ Fixed64ReadMethod,
+ PackedFixed64ReadMethod,
+ PackedFixed64ReadVectorMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BOOL: [
+ BoolReadMethod,
+ PackedBoolReadMethod,
],
- descriptor_pb2.FieldDescriptorProto.TYPE_BOOL:
- [BoolReadMethod, PackedBoolReadMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_BYTES:
- [BytesReadMethod, BytesReaderMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_STRING:
- [StringReadMethod, BytesReaderMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE:
- [SubMessageDecoderMethod],
- descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: [EnumReadMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BYTES: [
+ BytesReadMethod,
+ BytesReaderMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_STRING: [
+ StringReadMethod,
+ BytesReaderMethod,
+ ],
+ descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE: [SubMessageDecoderMethod],
+ descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: [
+ EnumReadMethod,
+ PackedEnumReadMethod,
+ PackedEnumReadVectorMethod,
+ ],
+}
+
+PROTO_FIELD_PROPERTIES: Dict[int, List] = {
+ descriptor_pb2.FieldDescriptorProto.TYPE_DOUBLE: [DoubleProperty],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FLOAT: [FloatProperty],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT32: [Int32Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT32: [Sint32Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED32: [Sfixed32Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_INT64: [Int64Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SINT64: [Sint64Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_SFIXED64: [Sfixed32Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT32: [Uint32Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FIXED32: [Fixed32Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_UINT64: [Uint64Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_FIXED64: [Fixed64Property],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BOOL: [BoolProperty],
+ descriptor_pb2.FieldDescriptorProto.TYPE_BYTES: [BytesProperty],
+ descriptor_pb2.FieldDescriptorProto.TYPE_STRING: [StringProperty],
+ descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE: [SubMessageProperty],
+ descriptor_pb2.FieldDescriptorProto.TYPE_ENUM: [EnumProperty],
}
def proto_field_methods(class_type: ClassType, field_type: int) -> List:
- return (PROTO_FIELD_WRITE_METHODS[field_type] if class_type.is_encoder()
- else PROTO_FIELD_READ_METHODS[field_type])
+ return (
+ PROTO_FIELD_WRITE_METHODS[field_type]
+ if class_type.is_encoder()
+ else PROTO_FIELD_READ_METHODS[field_type]
+ )
-def generate_class_for_message(message: ProtoMessage, root: ProtoNode,
- output: OutputFile,
- class_type: ClassType) -> None:
+def generate_class_for_message(
+ message: ProtoMessage,
+ root: ProtoNode,
+ output: OutputFile,
+ class_type: ClassType,
+) -> None:
"""Creates a C++ class to encode or decoder a protobuf message."""
assert message.type() == ProtoNode.Type.MESSAGE
@@ -1278,7 +2085,7 @@ def generate_class_for_message(message: ProtoMessage, root: ProtoNode,
# and use its constructor.
base_class = f'{PROTOBUF_NAMESPACE}::{base_class_name}'
output.write_line(
- f'class {message.cpp_namespace(root)}::{class_name} ' \
+ f'class {message.cpp_namespace(root=root)}::{class_name} '
f': public {base_class} {{'
)
output.write_line(' public:')
@@ -1288,14 +2095,17 @@ def generate_class_for_message(message: ProtoMessage, root: ProtoNode,
output.write_line(f'using {base_class}::{base_class_name};')
# Declare a move constructor that takes a base class.
- output.write_line(f'constexpr {class_name}({base_class}&& parent) '
- f': {base_class}(std::move(parent)) {{}}')
+ output.write_line(
+ f'constexpr {class_name}({base_class}&& parent) '
+ f': {base_class}(std::move(parent)) {{}}'
+ )
# Allow MemoryEncoder& to be converted to StreamEncoder&.
if class_type == ClassType.MEMORY_ENCODER:
stream_type = (
f'::{message.cpp_namespace()}::'
- f'{ClassType.STREAMING_ENCODER.codegen_class_name()}')
+ f'{ClassType.STREAMING_ENCODER.codegen_class_name()}'
+ )
output.write_line(
f'operator {stream_type}&() '
f' {{ return static_cast<{stream_type}&>('
@@ -1307,27 +2117,52 @@ def generate_class_for_message(message: ProtoMessage, root: ProtoNode,
output.write_line()
output.write_line('::pw::Result<Fields> Field() {')
with output.indent():
- output.write_line('::pw::Result<uint32_t> result '
- '= FieldNumber();')
+ output.write_line(
+ '::pw::Result<uint32_t> result ' '= FieldNumber();'
+ )
output.write_line('if (!result.ok()) {')
with output.indent():
output.write_line('return result.status();')
output.write_line('}')
+ output.write_line('return static_cast<Fields>(result.value());')
+ output.write_line('}')
+
+ # Generate entry for message table read or write methods.
+ if class_type == ClassType.STREAMING_DECODER:
+ output.write_line()
+ output.write_line('::pw::Status Read(Message& message) {')
+ with output.indent():
+ output.write_line(
+ f'return {base_class}::Read('
+ 'pw::as_writable_bytes(pw::span(&message, 1)), '
+ 'kMessageFields);'
+ )
+ output.write_line('}')
+ elif class_type in (
+ ClassType.STREAMING_ENCODER,
+ ClassType.MEMORY_ENCODER,
+ ):
+ output.write_line()
+ output.write_line('::pw::Status Write(const Message& message) {')
+ with output.indent():
output.write_line(
- 'return static_cast<Fields>(result.value());')
+ f'return {base_class}::Write('
+ 'pw::as_bytes(pw::span(&message, 1)), kMessageFields);'
+ )
output.write_line('}')
# Generate methods for each of the message's fields.
for field in message.fields():
for method_class in proto_field_methods(class_type, field.type()):
- method = method_class(field, message, root)
+ method = method_class(field, message, root, base_class)
if not method.should_appear():
continue
output.write_line()
method_signature = (
f'{method.return_type()} '
- f'{method.name()}({method.param_string()})')
+ f'{method.name()}({method.param_string()})'
+ )
if not method.in_class_definition():
# Method will be defined outside of the class at the end of
@@ -1344,24 +2179,33 @@ def generate_class_for_message(message: ProtoMessage, root: ProtoNode,
output.write_line('};')
-def define_not_in_class_methods(message: ProtoMessage, root: ProtoNode,
- output: OutputFile,
- class_type: ClassType) -> None:
+def define_not_in_class_methods(
+ message: ProtoMessage,
+ root: ProtoNode,
+ output: OutputFile,
+ class_type: ClassType,
+) -> None:
"""Defines methods for a message class that were previously declared."""
assert message.type() == ProtoNode.Type.MESSAGE
+ base_class_name = class_type.base_class_name()
+ base_class = f'{PROTOBUF_NAMESPACE}::{base_class_name}'
+
for field in message.fields():
for method_class in proto_field_methods(class_type, field.type()):
- method = method_class(field, message, root)
+ method = method_class(field, message, root, base_class)
if not method.should_appear() or method.in_class_definition():
continue
output.write_line()
- class_name = (f'{message.cpp_namespace(root)}::'
- f'{class_type.codegen_class_name()}')
+ class_name = (
+ f'{message.cpp_namespace(root=root)}::'
+ f'{class_type.codegen_class_name()}'
+ )
method_signature = (
f'inline {method.return_type(from_root=True)} '
- f'{class_name}::{method.name()}({method.param_string()})')
+ f'{class_name}::{method.name()}({method.param_string()})'
+ )
output.write_line(f'{method_signature} {{')
with output.indent():
for line in method.body():
@@ -1369,52 +2213,138 @@ def define_not_in_class_methods(message: ProtoMessage, root: ProtoNode,
output.write_line('}')
-def generate_code_for_enum(proto_enum: ProtoEnum, root: ProtoNode,
- output: OutputFile) -> None:
+def _common_value_prefix(proto_enum: ProtoEnum) -> str:
+ """Calculate the common prefix of all enum values.
+
+ Given an enumeration:
+ enum Thing {
+ THING_ONE = 1;
+ THING_TWO = 2;
+ THING_THREE = 3;
+ }
+
+ If will return 'THING_', resulting in generated "style" aliases of
+ 'kOne', 'kTwo', and 'kThree'.
+
+ The prefix is walked back to the last _, so that the enumeration:
+ enum Activity {
+ ACTIVITY_RUN = 1;
+ ACTIVITY_ROW = 2;
+ }
+
+ Returns 'ACTIVITY_' and not 'ACTIVITY_R'.
+ """
+ if len(proto_enum.values()) <= 1:
+ return ''
+
+ common_prefix = "".join(
+ ch[0]
+ for ch in takewhile(
+ lambda ch: all(ch[0] == c for c in ch),
+ zip(*[name for name, _ in proto_enum.values()]),
+ )
+ )
+ (left, under, _) = common_prefix.rpartition('_')
+ return left + under
+
+
+def generate_code_for_enum(
+ proto_enum: ProtoEnum, root: ProtoNode, output: OutputFile
+) -> None:
"""Creates a C++ enum for a proto enum."""
assert proto_enum.type() == ProtoNode.Type.ENUM
- output.write_line(f'enum class {proto_enum.cpp_namespace(root)} {{')
+ common_prefix = _common_value_prefix(proto_enum)
+ output.write_line(
+ f'enum class {proto_enum.cpp_namespace(root=root)} ' f': uint32_t {{'
+ )
with output.indent():
for name, number in proto_enum.values():
output.write_line(f'{name} = {number},')
+
+ style_name = 'k' + ProtoMessageField.upper_camel_case(
+ name[len(common_prefix) :]
+ )
+ if style_name != name:
+ output.write_line(f'{style_name} = {name},')
+
output.write_line('};')
-def generate_function_for_enum(proto_enum: ProtoEnum, root: ProtoNode,
- output: OutputFile) -> None:
- """Creates a C++ validation function for for a proto enum."""
+def generate_function_for_enum(
+ proto_enum: ProtoEnum, root: ProtoNode, output: OutputFile
+) -> None:
+ """Creates a C++ validation function for a proto enum."""
assert proto_enum.type() == ProtoNode.Type.ENUM
- enum_name = proto_enum.cpp_namespace(root)
+ enum_name = proto_enum.cpp_namespace(root=root)
output.write_line(
- f'constexpr ::pw::Result<{enum_name}> Get{enum_name}(uint32_t value) {{'
+ f'constexpr bool IsValid{enum_name}({enum_name} value) {{'
)
with output.indent():
output.write_line('switch (value) {')
with output.indent():
- for name, number in proto_enum.values():
- output.write_line(
- f'case {number}: return {enum_name}::{name};')
- output.write_line('default: return ::pw::Status::DataLoss();')
+ for name, _ in proto_enum.values():
+ output.write_line(f'case {enum_name}::{name}: return true;')
+ output.write_line('default: return false;')
output.write_line('}')
output.write_line('}')
-def forward_declare(node: ProtoMessage, root: ProtoNode,
- output: OutputFile) -> None:
+def generate_to_string_for_enum(
+ proto_enum: ProtoEnum, root: ProtoNode, output: OutputFile
+) -> None:
+ """Creates a C++ to string function for a proto enum."""
+ assert proto_enum.type() == ProtoNode.Type.ENUM
+
+ enum_name = proto_enum.cpp_namespace(root=root)
+ output.write_line(
+ f'// Returns string names for {enum_name}; '
+ 'returns "" for invalid enum values.'
+ )
+ output.write_line(
+ f'constexpr const char* {enum_name}ToString({enum_name} value) {{'
+ )
+ with output.indent():
+ output.write_line('switch (value) {')
+ with output.indent():
+ for name, _ in proto_enum.values():
+ output.write_line(f'case {enum_name}::{name}: return "{name}";')
+ output.write_line('default: return "";')
+ output.write_line('}')
+ output.write_line('}')
+
+
+def forward_declare(
+ node: ProtoMessage,
+ root: ProtoNode,
+ output: OutputFile,
+ exclude_legacy_snake_case_field_name_enums: bool,
+) -> None:
"""Generates code forward-declaring entities in a message's namespace."""
- namespace = node.cpp_namespace(root)
+ namespace = node.cpp_namespace(root=root)
output.write_line()
output.write_line(f'namespace {namespace} {{')
# Define an enum defining each of the message's fields and their numbers.
- output.write_line('enum class Fields {')
+ output.write_line('enum class Fields : uint32_t {')
with output.indent():
for field in node.fields():
output.write_line(f'{field.enum_name()} = {field.number()},')
+
+ # Migration support from SNAKE_CASE to kConstantCase.
+ if not exclude_legacy_snake_case_field_name_enums:
+ for field in node.fields():
+ output.write_line(
+ f'{field.legacy_enum_name()} = {field.number()},'
+ )
+
output.write_line('};')
+ # Declare the message's message struct.
+ output.write_line()
+ output.write_line('struct Message;')
+
# Declare the message's encoder classes.
output.write_line()
output.write_line('class StreamEncoder;')
@@ -1431,25 +2361,207 @@ def forward_declare(node: ProtoMessage, root: ProtoNode,
generate_code_for_enum(cast(ProtoEnum, child), node, output)
output.write_line()
generate_function_for_enum(cast(ProtoEnum, child), node, output)
+ output.write_line()
+ generate_to_string_for_enum(cast(ProtoEnum, child), node, output)
output.write_line(f'}} // namespace {namespace}')
-def generate_class_wrappers(package: ProtoNode, class_type: ClassType,
- output: OutputFile):
- # Run through all messages in the file, generating a class for each.
- for node in package:
- if node.type() == ProtoNode.Type.MESSAGE:
- output.write_line()
- generate_class_for_message(cast(ProtoMessage, node), package,
- output, class_type)
+def generate_struct_for_message(
+ message: ProtoMessage, root: ProtoNode, output: OutputFile
+) -> None:
+ """Creates a C++ struct to hold a protobuf message values."""
+ assert message.type() == ProtoNode.Type.MESSAGE
- # Run a second pass through the classes, this time defining all of the
- # methods which were previously only declared.
- for node in package:
- if node.type() == ProtoNode.Type.MESSAGE:
- define_not_in_class_methods(cast(ProtoMessage, node), package,
- output, class_type)
+ output.write_line(f'struct {message.cpp_namespace(root=root)}::Message {{')
+
+ # Generate members for each of the message's fields.
+ with output.indent():
+ cmp: List[str] = []
+ for field in message.fields():
+ for property_class in PROTO_FIELD_PROPERTIES[field.type()]:
+ prop = property_class(field, message, root)
+ if not prop.should_appear():
+ continue
+
+ (type_name, name) = prop.struct_member()
+ output.write_line(f'{type_name} {name};')
+
+ if not prop.use_callback():
+ cmp.append(f'{name} == other.{name}')
+
+ # Equality operator
+ output.write_line()
+ output.write_line('bool operator==(const Message& other) const {')
+ with output.indent():
+ if len(cmp) > 0:
+ output.write_line(f'return {" && ".join(cmp)};')
+ else:
+ output.write_line('static_cast<void>(other);')
+ output.write_line('return true;')
+ output.write_line('}')
+ output.write_line(
+ 'bool operator!=(const Message& other) const '
+ '{ return !(*this == other); }'
+ )
+
+ output.write_line('};')
+
+
+def generate_table_for_message(
+ message: ProtoMessage, root: ProtoNode, output: OutputFile
+) -> None:
+ """Creates a C++ array to hold a protobuf message description."""
+ assert message.type() == ProtoNode.Type.MESSAGE
+
+ namespace = message.cpp_namespace(root=root)
+ output.write_line(f'namespace {namespace} {{')
+
+ properties = []
+ for field in message.fields():
+ for property_class in PROTO_FIELD_PROPERTIES[field.type()]:
+ prop = property_class(field, message, root)
+ if prop.should_appear():
+ properties.append(prop)
+
+ output.write_line('PW_MODIFY_DIAGNOSTICS_PUSH();')
+ output.write_line('PW_MODIFY_DIAGNOSTIC(ignored, "-Winvalid-offsetof");')
+
+ # Generate static_asserts to fail at compile-time if the structure cannot
+ # be converted into a table.
+ for idx, prop in enumerate(properties):
+ if idx > 0:
+ output.write_line(
+ 'static_assert(offsetof(Message, {}) > 0);'.format(prop.name())
+ )
+ output.write_line(
+ 'static_assert(sizeof(Message::{}) <= '
+ '{}::MessageField::kMaxFieldSize);'.format(
+ prop.name(), _INTERNAL_NAMESPACE
+ )
+ )
+
+ # Zero-length C arrays are not permitted by the C++ standard, so only
+ # generate the message fields array if it is non-empty. Zero-length
+ # std::arrays are valid, but older toolchains may not support constexpr
+ # std::arrays, even with -std=c++17.
+ #
+ # The kMessageFields span is generated whether the message has fields or
+ # not. Only the span is referenced elsewhere.
+ if properties:
+ output.write_line(
+ f'inline constexpr {_INTERNAL_NAMESPACE}::MessageField '
+ ' _kMessageFields[] = {'
+ )
+
+ # Generate members for each of the message's fields.
+ with output.indent():
+ for prop in properties:
+ table = ', '.join(prop.table_entry())
+ output.write_line(f'{{{table}}},')
+
+ output.write_line('};')
+ output.write_line('PW_MODIFY_DIAGNOSTICS_POP();')
+
+ output.write_line(
+ f'inline constexpr pw::span<const {_INTERNAL_NAMESPACE}::'
+ 'MessageField> kMessageFields = _kMessageFields;'
+ )
+
+ member_list = ', '.join(
+ [f'message.{prop.struct_member()[1]}' for prop in properties]
+ )
+
+ # Generate std::tuple for Message fields.
+ output.write_line(
+ 'inline constexpr auto ToTuple(const Message &message) {'
+ )
+ output.write_line(f' return std::tie({member_list});')
+ output.write_line('}')
+
+ # Generate mutable std::tuple for Message fields.
+ output.write_line(
+ 'inline constexpr auto ToMutableTuple(Message &message) {'
+ )
+ output.write_line(f' return std::tie({member_list});')
+ output.write_line('}')
+ else:
+ output.write_line(
+ f'inline constexpr pw::span<const {_INTERNAL_NAMESPACE}::'
+ 'MessageField> kMessageFields;'
+ )
+
+ output.write_line(f'}} // namespace {namespace}')
+
+
+def generate_sizes_for_message(
+ message: ProtoMessage, root: ProtoNode, output: OutputFile
+) -> None:
+ """Creates C++ constants for the encoded sizes of a protobuf message."""
+ assert message.type() == ProtoNode.Type.MESSAGE
+
+ namespace = message.cpp_namespace(root=root)
+ output.write_line(f'namespace {namespace} {{')
+
+ property_sizes: List[str] = []
+ scratch_sizes: List[str] = []
+ for field in message.fields():
+ for property_class in PROTO_FIELD_PROPERTIES[field.type()]:
+ prop = property_class(field, message, root)
+ if not prop.should_appear():
+ continue
+
+ property_sizes.append(prop.max_encoded_size())
+ if prop.include_in_scratch_size():
+ scratch_sizes.append(prop.max_encoded_size())
+
+ output.write_line('inline constexpr size_t kMaxEncodedSizeBytes =')
+ with output.indent():
+ if len(property_sizes) == 0:
+ output.write_line('0;')
+ while len(property_sizes) > 0:
+ property_size = property_sizes.pop(0)
+ if len(property_sizes) > 0:
+ output.write_line(f'{property_size} +')
+ else:
+ output.write_line(f'{property_size};')
+
+ output.write_line()
+ output.write_line(
+ 'inline constexpr size_t kScratchBufferSizeBytes = '
+ + ('std::max({' if len(scratch_sizes) > 0 else '0;')
+ )
+ with output.indent():
+ for scratch_size in scratch_sizes:
+ output.write_line(f'{scratch_size},')
+ if len(scratch_sizes) > 0:
+ output.write_line('});')
+
+ output.write_line(f'}} // namespace {namespace}')
+
+
+def generate_is_trivially_comparable_specialization(
+ message: ProtoMessage, root: ProtoNode, output: OutputFile
+) -> None:
+ is_trivially_comparable = True
+ for field in message.fields():
+ for property_class in PROTO_FIELD_PROPERTIES[field.type()]:
+ prop = property_class(field, message, root)
+ if not prop.should_appear():
+ continue
+
+ if prop.use_callback():
+ is_trivially_comparable = False
+ break
+
+ qualified_message = f'{message.cpp_namespace()}::Message'
+
+ output.write_line('template <>')
+ output.write_line(
+ 'constexpr bool IsTriviallyComparable' f'<{qualified_message}>() {{'
+ )
+ output.write_line(f' return {str(is_trivially_comparable).lower()};')
+ output.write_line('}')
def _proto_filename_to_generated_header(proto_file: str) -> str:
@@ -1457,27 +2569,71 @@ def _proto_filename_to_generated_header(proto_file: str) -> str:
return os.path.splitext(proto_file)[0] + PROTO_H_EXTENSION
-def generate_code_for_package(file_descriptor_proto, package: ProtoNode,
- output: OutputFile) -> None:
+def dependency_sorted_messages(package: ProtoNode):
+ """Yields the messages in the package sorted after their dependencies."""
+
+ # Build the graph of dependencies between messages.
+ graph: Dict[ProtoMessage, List[ProtoMessage]] = {}
+ for node in package:
+ if node.type() == ProtoNode.Type.MESSAGE:
+ message = cast(ProtoMessage, node)
+ graph[message] = message.dependencies()
+
+ # Repeatedly prepare a topological sort of the dependency graph, removing
+ # a dependency each time a cycle is a detected, until we're left with a
+ # fully directed graph.
+ tsort: TopologicalSorter
+ while True:
+ tsort = TopologicalSorter(graph)
+ try:
+ tsort.prepare()
+ break
+ except CycleError as err:
+ dependency, message = err.args[1][0], err.args[1][1]
+ message.remove_dependency_cycle(dependency)
+ graph[message] = message.dependencies()
+
+ # Yield the messages from the sorted graph.
+ while tsort.is_active():
+ messages = tsort.get_ready()
+ yield from messages
+ tsort.done(*messages)
+
+
+def generate_code_for_package(
+ file_descriptor_proto,
+ package: ProtoNode,
+ output: OutputFile,
+ suppress_legacy_namespace: bool,
+ exclude_legacy_snake_case_field_name_enums: bool,
+) -> None:
"""Generates code for a single .pb.h file corresponding to a .proto file."""
assert package.type() == ProtoNode.Type.PACKAGE
- output.write_line(f'// {os.path.basename(output.name())} automatically '
- f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}')
- output.write_line(f'// on {datetime.now()}')
+ output.write_line(
+ f'// {os.path.basename(output.name())} automatically '
+ f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}'
+ )
output.write_line('#pragma once\n')
+ output.write_line('#include <algorithm>')
+ output.write_line('#include <array>')
output.write_line('#include <cstddef>')
output.write_line('#include <cstdint>')
- output.write_line('#include <span>')
+ output.write_line('#include <optional>')
output.write_line('#include <string_view>\n')
output.write_line('#include "pw_assert/assert.h"')
output.write_line('#include "pw_containers/vector.h"')
+ output.write_line('#include "pw_preprocessor/compiler.h"')
output.write_line('#include "pw_protobuf/encoder.h"')
+ output.write_line('#include "pw_protobuf/internal/codegen.h"')
+ output.write_line('#include "pw_protobuf/serialized_size.h"')
output.write_line('#include "pw_protobuf/stream_decoder.h"')
output.write_line('#include "pw_result/result.h"')
+ output.write_line('#include "pw_span/span.h"')
output.write_line('#include "pw_status/status.h"')
output.write_line('#include "pw_status/status_with_size.h"')
+ output.write_line('#include "pw_string/string.h"')
for imported_file in file_descriptor_proto.dependency:
generated_header = _proto_filename_to_generated_header(imported_file)
@@ -1492,7 +2648,12 @@ def generate_code_for_package(file_descriptor_proto, package: ProtoNode,
for node in package:
if node.type() == ProtoNode.Type.MESSAGE:
- forward_declare(cast(ProtoMessage, node), package, output)
+ forward_declare(
+ cast(ProtoMessage, node),
+ package,
+ output,
+ exclude_legacy_snake_case_field_name_enums,
+ )
# Define all top-level enums.
for node in package.children():
@@ -1501,27 +2662,114 @@ def generate_code_for_package(file_descriptor_proto, package: ProtoNode,
generate_code_for_enum(cast(ProtoEnum, node), package, output)
output.write_line()
generate_function_for_enum(cast(ProtoEnum, node), package, output)
-
- generate_class_wrappers(package, ClassType.STREAMING_ENCODER, output)
- generate_class_wrappers(package, ClassType.MEMORY_ENCODER, output)
-
- generate_class_wrappers(package, ClassType.STREAMING_DECODER, output)
+ output.write_line()
+ generate_to_string_for_enum(cast(ProtoEnum, node), package, output)
+
+ # Run through all messages, generating structs and classes for each.
+ messages = []
+ for message in dependency_sorted_messages(package):
+ output.write_line()
+ generate_struct_for_message(message, package, output)
+ output.write_line()
+ generate_table_for_message(message, package, output)
+ output.write_line()
+ generate_sizes_for_message(message, package, output)
+ output.write_line()
+ generate_class_for_message(
+ message, package, output, ClassType.STREAMING_ENCODER
+ )
+ output.write_line()
+ generate_class_for_message(
+ message, package, output, ClassType.MEMORY_ENCODER
+ )
+ output.write_line()
+ generate_class_for_message(
+ message, package, output, ClassType.STREAMING_DECODER
+ )
+ messages.append(message)
+
+ # Run a second pass through the messages, this time defining all of the
+ # methods which were previously only declared.
+ for message in messages:
+ define_not_in_class_methods(
+ message, package, output, ClassType.STREAMING_ENCODER
+ )
+ define_not_in_class_methods(
+ message, package, output, ClassType.MEMORY_ENCODER
+ )
+ define_not_in_class_methods(
+ message, package, output, ClassType.STREAMING_DECODER
+ )
if package.cpp_namespace():
output.write_line(f'\n}} // namespace {package.cpp_namespace()}')
+ # Aliasing namespaces aren't needed if `package.cpp_namespace()` is
+ # empty (since everyone can see the global namespace). It shouldn't
+ # ever be empty, though.
-def process_proto_file(proto_file) -> Iterable[OutputFile]:
+ if not suppress_legacy_namespace:
+ output.write_line()
+ output.write_line(
+ '// Aliases for legacy pwpb codegen interface. '
+ 'Please use the'
+ )
+ output.write_line('// `::pwpb`-suffixed names in new code.')
+ legacy_namespace = package.cpp_namespace(codegen_subnamespace=None)
+ output.write_line(f'namespace {legacy_namespace} {{')
+ output.write_line(f'using namespace ::{package.cpp_namespace()};')
+ output.write_line(f'}} // namespace {legacy_namespace}')
+
+ # TODO(b/250945489) Remove this if possible
+ output.write_line()
+ output.write_line(
+ '// Codegen implementation detail; do not use this namespace!'
+ )
+
+ external_lookup_namespace = "{}::{}".format(
+ EXTERNAL_SYMBOL_WORKAROUND_NAMESPACE,
+ package.cpp_namespace(codegen_subnamespace=None),
+ )
+
+ output.write_line(f'namespace {external_lookup_namespace} {{')
+ output.write_line(f'using namespace ::{package.cpp_namespace()};')
+ output.write_line(f'}} // namespace {external_lookup_namespace}')
+
+ if messages:
+ proto_namespace = PROTOBUF_NAMESPACE.lstrip(':')
+ output.write_line()
+ output.write_line(f'namespace {proto_namespace} {{')
+
+ for message in messages:
+ generate_is_trivially_comparable_specialization(
+ message, package, output
+ )
+
+ output.write_line(f'}} // namespace {proto_namespace}')
+
+
+def process_proto_file(
+ proto_file,
+ proto_options,
+ suppress_legacy_namespace: bool,
+ exclude_legacy_snake_case_field_name_enums: bool,
+) -> Iterable[OutputFile]:
"""Generates code for a single .proto file."""
# Two passes are made through the file. The first builds the tree of all
# message/enum nodes, then the second creates the fields in each. This is
# done as non-primitive fields need pointers to their types, which requires
# the entire tree to have been parsed into memory.
- _, package_root = build_node_tree(proto_file)
+ _, package_root = build_node_tree(proto_file, proto_options=proto_options)
output_filename = _proto_filename_to_generated_header(proto_file.name)
output_file = OutputFile(output_filename)
- generate_code_for_package(proto_file, package_root, output_file)
+ generate_code_for_package(
+ proto_file,
+ package_root,
+ output_file,
+ suppress_legacy_namespace,
+ exclude_legacy_snake_case_field_name_enums,
+ )
return [output_file]
diff --git a/pw_protobuf/py/pw_protobuf/options.py b/pw_protobuf/py/pw_protobuf/options.py
new file mode 100644
index 000000000..61cb6f1e9
--- /dev/null
+++ b/pw_protobuf/py/pw_protobuf/options.py
@@ -0,0 +1,114 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Options file parsing for proto generation."""
+
+from fnmatch import fnmatchcase
+from pathlib import Path
+import re
+from typing import List, Tuple
+
+from google.protobuf import text_format
+
+from pw_protobuf_codegen_protos.codegen_options_pb2 import CodegenOptions
+from pw_protobuf_protos.field_options_pb2 import FieldOptions
+
+_MULTI_LINE_COMMENT_RE = re.compile(r'/\*.*?\*/', flags=re.MULTILINE)
+_SINGLE_LINE_COMMENT_RE = re.compile(r'//.*?$', flags=re.MULTILINE)
+_SHELL_STYLE_COMMENT_RE = re.compile(r'#.*?$', flags=re.MULTILINE)
+
+# A list of (proto field path, CodegenOptions) tuples.
+ParsedOptions = List[Tuple[str, CodegenOptions]]
+
+
+def load_options_from(options: ParsedOptions, options_file_name: Path):
+ """Loads a single .options file for the given .proto"""
+ with open(options_file_name) as options_file:
+ # Read the options file and strip all styles of comments before parsing.
+ options_data = options_file.read()
+ options_data = _MULTI_LINE_COMMENT_RE.sub('', options_data)
+ options_data = _SINGLE_LINE_COMMENT_RE.sub('', options_data)
+ options_data = _SHELL_STYLE_COMMENT_RE.sub('', options_data)
+
+ for line in options_data.split('\n'):
+ parts = line.strip().split(None, 1)
+ if len(parts) < 2:
+ continue
+
+ # Parse as a name glob followed by a protobuf text format.
+ try:
+ opts = CodegenOptions()
+ text_format.Merge(parts[1], opts)
+ options.append((parts[0], opts))
+ except: # pylint: disable=bare-except
+ continue
+
+
+def load_options(
+ include_paths: List[Path], proto_file_name: Path
+) -> ParsedOptions:
+ """Loads the .options for the given .proto."""
+ options: ParsedOptions = []
+
+ for include_path in include_paths:
+ options_file_name = include_path / proto_file_name.with_suffix(
+ '.options'
+ )
+ if options_file_name.exists():
+ load_options_from(options, options_file_name)
+
+ return options
+
+
+def match_options(name: str, options: ParsedOptions) -> CodegenOptions:
+ """Return the matching options for a name."""
+ matched = CodegenOptions()
+ for name_glob, mask_options in options:
+ if fnmatchcase(name, name_glob):
+ matched.MergeFrom(mask_options)
+
+ return matched
+
+
+def create_from_field_options(
+ field_options: FieldOptions,
+) -> CodegenOptions:
+ """Create a CodegenOptions from a FieldOptions."""
+ codegen_options = CodegenOptions()
+
+ if field_options.HasField('max_count'):
+ codegen_options.max_count = field_options.max_count
+
+ if field_options.HasField('max_size'):
+ codegen_options.max_size = field_options.max_size
+
+ return codegen_options
+
+
+def merge_field_and_codegen_options(
+ field_options: CodegenOptions, codegen_options: CodegenOptions
+) -> CodegenOptions:
+ """Merge inline field_options and options file codegen_options."""
+ # The field options specify protocol-level requirements. Therefore, any
+ # codegen options should not violate those protocol-level requirements.
+ if field_options.max_count > 0 and codegen_options.max_count > 0:
+ assert field_options.max_count == codegen_options.max_count
+
+ if field_options.max_size > 0 and codegen_options.max_size > 0:
+ assert field_options.max_size == codegen_options.max_size
+
+ merged_options = CodegenOptions()
+ merged_options.CopyFrom(field_options)
+ merged_options.MergeFrom(codegen_options)
+
+ return merged_options
diff --git a/pw_protobuf/py/pw_protobuf/output_file.py b/pw_protobuf/py/pw_protobuf/output_file.py
index 04bd0ee8b..cc4c378d5 100644
--- a/pw_protobuf/py/pw_protobuf/output_file.py
+++ b/pw_protobuf/py/pw_protobuf/output_file.py
@@ -69,6 +69,7 @@ class OutputFile:
class _IndentationContext:
"""Context that increases the output's indentation when it is active."""
+
def __init__(self, output: 'OutputFile', amount: int):
self._output = output
self._amount: int = amount
diff --git a/pw_protobuf/py/pw_protobuf/plugin.py b/pw_protobuf/py/pw_protobuf/plugin.py
index c584a9a73..bc3c405ec 100755
--- a/pw_protobuf/py/pw_protobuf/plugin.py
+++ b/pw_protobuf/py/pw_protobuf/plugin.py
@@ -19,14 +19,62 @@ protobuf messages in the pw_protobuf format.
"""
import sys
+from argparse import ArgumentParser, Namespace
+from pathlib import Path
+from shlex import shlex
from google.protobuf.compiler import plugin_pb2
-from pw_protobuf import codegen_pwpb
+from pw_protobuf import codegen_pwpb, options
-def process_proto_request(req: plugin_pb2.CodeGeneratorRequest,
- res: plugin_pb2.CodeGeneratorResponse) -> None:
+def parse_parameter_options(parameter: str) -> Namespace:
+ """Parses parameters passed through from protoc.
+
+ These parameters come in via passing `--${NAME}_out` parameters to protoc,
+ where protoc-gen-${NAME} is the supplied name of the plugin. At time of
+ writing, Blaze uses --pwpb_opt, whereas the script for GN uses --custom_opt.
+ """
+ parser = ArgumentParser()
+ parser.add_argument(
+ '-I',
+ '--include-path',
+ dest='include_paths',
+ metavar='DIR',
+ action='append',
+ default=[],
+ type=Path,
+ help='Append DIR to options file search path',
+ )
+ parser.add_argument(
+ '--no-legacy-namespace',
+ dest='no_legacy_namespace',
+ action='store_true',
+ help='If set, suppresses `using namespace` declarations, which '
+ 'disallows use of the legacy non-prefixed namespace',
+ )
+ parser.add_argument(
+ '--exclude-legacy-snake-case-field-name-enums',
+ dest='exclude_legacy_snake_case_field_name_enums',
+ action='store_true',
+ help='Do not generate legacy SNAKE_CASE names for field name enums.',
+ )
+
+ # protoc passes the custom arguments in shell quoted form, separated by
+ # commas. Use shlex to split them, correctly handling quoted sections, with
+ # equivalent options to IFS=","
+ lex = shlex(parameter)
+ lex.whitespace_split = True
+ lex.whitespace = ','
+ lex.commenters = ''
+ args = list(lex)
+
+ return parser.parse_args(args)
+
+
+def process_proto_request(
+ req: plugin_pb2.CodeGeneratorRequest, res: plugin_pb2.CodeGeneratorResponse
+) -> None:
"""Handles a protoc CodeGeneratorRequest message.
Generates code for the files in the request and writes the output to the
@@ -36,8 +84,19 @@ def process_proto_request(req: plugin_pb2.CodeGeneratorRequest,
req: A CodeGeneratorRequest for a proto compilation.
res: A CodeGeneratorResponse to populate with the plugin's output.
"""
+ args = parse_parameter_options(req.parameter)
for proto_file in req.proto_file:
- output_files = codegen_pwpb.process_proto_file(proto_file)
+ proto_options = options.load_options(
+ args.include_paths, Path(proto_file.name)
+ )
+ output_files = codegen_pwpb.process_proto_file(
+ proto_file,
+ proto_options,
+ suppress_legacy_namespace=args.no_legacy_namespace,
+ exclude_legacy_snake_case_field_name_enums=(
+ args.exclude_legacy_snake_case_field_name_enums
+ ),
+ )
for output_file in output_files:
fd = res.file.add()
fd.name = output_file.name()
@@ -57,7 +116,8 @@ def main() -> int:
# Declare that this plugin supports optional fields in proto3.
response.supported_features |= ( # type: ignore[attr-defined]
- response.FEATURE_PROTO3_OPTIONAL) # type: ignore[attr-defined]
+ response.FEATURE_PROTO3_OPTIONAL
+ ) # type: ignore[attr-defined]
sys.stdout.buffer.write(response.SerializeToString())
return 0
diff --git a/pw_protobuf/py/pw_protobuf/proto_tree.py b/pw_protobuf/py/pw_protobuf/proto_tree.py
index 9f8885527..15bfb732a 100644
--- a/pw_protobuf/py/pw_protobuf/proto_tree.py
+++ b/pw_protobuf/py/pw_protobuf/proto_tree.py
@@ -16,14 +16,39 @@
import abc
import collections
import enum
-
-from typing import Callable, Dict, Iterator, List, Optional, Tuple, TypeVar
-from typing import cast
+import itertools
+
+from typing import (
+ Callable,
+ Dict,
+ Iterator,
+ List,
+ Optional,
+ Tuple,
+ TypeVar,
+ cast,
+)
from google.protobuf import descriptor_pb2
+from pw_protobuf import options, symbol_name_mapping
+from pw_protobuf_codegen_protos.codegen_options_pb2 import CodegenOptions
+from pw_protobuf_protos.field_options_pb2 import pwpb as pwpb_field_options
+
T = TypeVar('T') # pylint: disable=invalid-name
+# Currently, protoc does not do a traversal to look up the package name of all
+# messages that are referenced in the file. For such "external" message names,
+# we are unable to find where the "::pwpb" subnamespace would be inserted by our
+# codegen. This namespace provides us with an alternative, more verbose
+# namespace that the codegen can use as a fallback in these cases. For example,
+# for the symbol name `my.external.package.ProtoMsg.SubMsg`, we would use
+# `::pw::pwpb_codegen_private::my::external::package:ProtoMsg::SubMsg` to refer
+# to the pw_protobuf generated code, when package name info is not available.
+#
+# TODO(b/258832150) Explore removing this if possible
+EXTERNAL_SYMBOL_WORKAROUND_NAMESPACE = 'pw::pwpb_codegen_private'
+
class ProtoNode(abc.ABC):
"""A ProtoNode represents a C++ scope mapping of an entity in a .proto file.
@@ -31,6 +56,7 @@ class ProtoNode(abc.ABC):
Nodes form a tree beginning at a top-level (global) scope, descending into a
hierarchy of .proto packages and the messages and enums defined within them.
"""
+
class Type(enum.Enum):
"""The type of a ProtoNode.
@@ -40,6 +66,7 @@ class ProtoNode(abc.ABC):
EXTERNAL represents a node defined within a different compilation unit.
SERVICE represents an RPC service definition.
"""
+
PACKAGE = 1
MESSAGE = 2
ENUM = 3
@@ -58,23 +85,144 @@ class ProtoNode(abc.ABC):
def children(self) -> List['ProtoNode']:
return list(self._children.values())
+ def parent(self) -> Optional['ProtoNode']:
+ return self._parent
+
def name(self) -> str:
return self._name
def cpp_name(self) -> str:
"""The name of this node in generated C++ code."""
- return self._name.replace('.', '::')
+ return symbol_name_mapping.fix_cc_identifier(self._name).replace(
+ '.', '::'
+ )
+
+ def _package_or_external(self) -> 'ProtoNode':
+ """Returns this node's deepest package or external ancestor node.
+
+ This method may need to return an external node, as a fallback for
+ external names that are referenced, but not processed into a more
+ regular proto tree. This is because there is no way to find the package
+ name of a node referring to an external symbol.
+ """
+ node: Optional['ProtoNode'] = self
+ while (
+ node
+ and node.type() != ProtoNode.Type.PACKAGE
+ and node.type() != ProtoNode.Type.EXTERNAL
+ ):
+ node = node.parent()
+
+ assert node, 'proto tree was built without a root'
+ return node
+
+ def cpp_namespace(
+ self,
+ root: Optional['ProtoNode'] = None,
+ codegen_subnamespace: Optional[str] = 'pwpb',
+ ) -> str:
+ """C++ namespace of the node, up to the specified root.
- def cpp_namespace(self, root: Optional['ProtoNode'] = None) -> str:
- """C++ namespace of the node, up to the specified root."""
- return '::'.join(name for name in self._attr_hierarchy(
- lambda node: node.cpp_name(), root) if name)
+ Args:
+ root: Namespace from which this ProtoNode is referred. If this
+ ProtoNode has `root` as an ancestor namespace, then the ancestor
+ namespace scopes above `root` are omitted.
+
+ codegen_subnamespace: A subnamespace that is appended to the package
+ declared in the .proto file. It is appended to the declared package,
+ but before any namespaces that are needed for messages etc. This
+ feature can be used to allow different codegen tools to output
+ different, non-conflicting symbols for the same protos.
+
+ By default, this is "pwpb", which reflects the default behaviour
+ of the pwpb codegen.
+ """
+ self_pkg_or_ext = self._package_or_external()
+ root_pkg_or_ext = (
+ root._package_or_external() # pylint: disable=protected-access
+ if root is not None
+ else None
+ )
+ if root_pkg_or_ext:
+ assert root_pkg_or_ext.type() != ProtoNode.Type.EXTERNAL
+
+ def compute_hierarchy() -> Iterator[str]:
+ same_package = True
+
+ if self_pkg_or_ext.type() == ProtoNode.Type.EXTERNAL:
+ # Can't figure out where the namespace cutoff is. Punt to using
+ # the external symbol workaround.
+ #
+ # TODO(b/250945489) Investigate removing this limitation / hack
+ return itertools.chain(
+ [EXTERNAL_SYMBOL_WORKAROUND_NAMESPACE],
+ self._attr_hierarchy(ProtoNode.cpp_name, root=None),
+ )
+
+ if root is None or root_pkg_or_ext is None: # extra check for mypy
+ # TODO(b/250945489): maybe elide "::{codegen_subnamespace}"
+ # here, if this node doesn't have any package?
+ same_package = False
+ else:
+ paired_hierarchy = itertools.zip_longest(
+ self_pkg_or_ext._attr_hierarchy( # pylint: disable=protected-access
+ ProtoNode.cpp_name, root=None
+ ),
+ root_pkg_or_ext._attr_hierarchy( # pylint: disable=protected-access
+ ProtoNode.cpp_name, root=None
+ ),
+ )
+ for str_a, str_b in paired_hierarchy:
+ if str_a != str_b:
+ same_package = False
+ break
+
+ if same_package:
+ # This ProtoNode and the requested root are in the same package,
+ # so the `codegen_subnamespace` should be omitted.
+ hierarchy = self._attr_hierarchy(ProtoNode.cpp_name, root)
+ return hierarchy
+
+ # The given root is either effectively nonexistent (common ancestor
+ # is ""), or is only a partial match for the package of this node.
+ # Either way, we will have to insert `codegen_subnamespace` after
+ # the relevant package string.
+ package_hierarchy = self_pkg_or_ext._attr_hierarchy( # pylint: disable=protected-access
+ ProtoNode.cpp_name, root
+ )
+ maybe_subnamespace = (
+ [codegen_subnamespace] if codegen_subnamespace else []
+ )
+ inside_hierarchy = self._attr_hierarchy(
+ ProtoNode.cpp_name, self_pkg_or_ext
+ )
+
+ hierarchy = itertools.chain(
+ package_hierarchy, maybe_subnamespace, inside_hierarchy
+ )
+ return hierarchy
+
+ joined_namespace = '::'.join(
+ name for name in compute_hierarchy() if name
+ )
+
+ return (
+ '' if joined_namespace == codegen_subnamespace else joined_namespace
+ )
def proto_path(self) -> str:
"""Fully-qualified package path of the node."""
path = '.'.join(self._attr_hierarchy(lambda node: node.name(), None))
return path.lstrip('.')
+ def pwpb_struct(self) -> str:
+ """Name of the pw_protobuf struct for this proto."""
+ return '::' + self.cpp_namespace() + '::Message'
+
+ def pwpb_table(self) -> str:
+ """Name of the pw_protobuf table constant for this proto."""
+ return '::' + self.cpp_namespace() + '::kMessageFields'
+
def nanopb_fields(self) -> str:
"""Name of the Nanopb variable that represents the proto fields."""
return self._nanopb_name() + '_fields'
@@ -137,8 +285,10 @@ class ProtoNode(abc.ABC):
ValueError: This node does not allow nesting the given type of child.
"""
if not self._supports_child(child):
- raise ValueError('Invalid child %s for node of type %s' %
- (child.type(), self.type()))
+ raise ValueError(
+ 'Invalid child %s for node of type %s'
+ % (child.type(), self.type())
+ )
# pylint: disable=protected-access
if child._parent is not None:
@@ -149,7 +299,11 @@ class ProtoNode(abc.ABC):
# pylint: enable=protected-access
def find(self, path: str) -> Optional['ProtoNode']:
- """Finds a node within this node's subtree."""
+ """Finds a node within this node's subtree.
+
+ Args:
+ path: The path to the sought node.
+ """
node = self
# pylint: disable=protected-access
@@ -162,9 +316,6 @@ class ProtoNode(abc.ABC):
return node
- def parent(self) -> Optional['ProtoNode']:
- return self._parent
-
def __iter__(self) -> Iterator['ProtoNode']:
"""Iterates depth-first through all nodes in this node's subtree."""
yield self
@@ -172,8 +323,11 @@ class ProtoNode(abc.ABC):
for child in child_iterator:
yield child
- def _attr_hierarchy(self, attr_accessor: Callable[['ProtoNode'], T],
- root: Optional['ProtoNode']) -> Iterator[T]:
+ def _attr_hierarchy(
+ self,
+ attr_accessor: Callable[['ProtoNode'], T],
+ root: Optional['ProtoNode'],
+ ) -> Iterator[T]:
"""Fetches node attributes at each level of the tree from the root.
Args:
@@ -198,6 +352,7 @@ class ProtoNode(abc.ABC):
class ProtoPackage(ProtoNode):
"""A protobuf package."""
+
def type(self) -> ProtoNode.Type:
return ProtoNode.Type.PACKAGE
@@ -207,6 +362,7 @@ class ProtoPackage(ProtoNode):
class ProtoEnum(ProtoNode):
"""Representation of an enum in a .proto file."""
+
def __init__(self, name: str):
super().__init__(name)
self._values: List[Tuple[str, int]] = []
@@ -218,7 +374,14 @@ class ProtoEnum(ProtoNode):
return list(self._values)
def add_value(self, name: str, value: int) -> None:
- self._values.append((ProtoMessageField.upper_snake_case(name), value))
+ self._values.append(
+ (
+ ProtoMessageField.upper_snake_case(
+ symbol_name_mapping.fix_cc_enum_value_name(name)
+ ),
+ value,
+ )
+ )
def _supports_child(self, child: ProtoNode) -> bool:
# Enums cannot have nested children.
@@ -227,9 +390,12 @@ class ProtoEnum(ProtoNode):
class ProtoMessage(ProtoNode):
"""Representation of a message in a .proto file."""
+
def __init__(self, name: str):
super().__init__(name)
self._fields: List['ProtoMessageField'] = []
+ self._dependencies: Optional[List['ProtoMessage']] = None
+ self._dependency_cycles: List['ProtoMessage'] = []
def type(self) -> ProtoNode.Type:
return ProtoNode.Type.MESSAGE
@@ -241,12 +407,40 @@ class ProtoMessage(ProtoNode):
self._fields.append(field)
def _supports_child(self, child: ProtoNode) -> bool:
- return (child.type() == self.Type.ENUM
- or child.type() == self.Type.MESSAGE)
+ return (
+ child.type() == self.Type.ENUM or child.type() == self.Type.MESSAGE
+ )
+
+ def dependencies(self) -> List['ProtoMessage']:
+ if self._dependencies is None:
+ self._dependencies = []
+ for field in self._fields:
+ if (
+ field.type()
+ != descriptor_pb2.FieldDescriptorProto.TYPE_MESSAGE
+ ):
+ continue
+
+ type_node = field.type_node()
+ assert type_node is not None
+ if type_node.type() == ProtoNode.Type.MESSAGE:
+ self._dependencies.append(cast(ProtoMessage, type_node))
+
+ return list(self._dependencies)
+
+ def dependency_cycles(self) -> List['ProtoMessage']:
+ return list(self._dependency_cycles)
+
+ def remove_dependency_cycle(self, dependency: 'ProtoMessage'):
+ assert self._dependencies is not None
+ assert dependency in self._dependencies
+ self._dependencies.remove(dependency)
+ self._dependency_cycles.append(dependency)
class ProtoService(ProtoNode):
"""Representation of a service in a .proto file."""
+
def __init__(self, name: str):
super().__init__(name)
self._methods: List['ProtoServiceMethod'] = []
@@ -273,6 +467,7 @@ class ProtoExternal(ProtoNode):
within the node graph is to provide namespace resolution between compile
units.
"""
+
def type(self) -> ProtoNode.Type:
return ProtoNode.Type.EXTERNAL
@@ -284,23 +479,38 @@ class ProtoExternal(ProtoNode):
# Fields belong to proto messages and are processed separately.
class ProtoMessageField:
"""Representation of a field within a protobuf message."""
- def __init__(self,
- field_name: str,
- field_number: int,
- field_type: int,
- type_node: Optional[ProtoNode] = None,
- repeated: bool = False):
- self._field_name = field_name
+
+ def __init__(
+ self,
+ field_name: str,
+ field_number: int,
+ field_type: int,
+ type_node: Optional[ProtoNode] = None,
+ optional: bool = False,
+ repeated: bool = False,
+ codegen_options: Optional[CodegenOptions] = None,
+ ):
+ self._field_name = symbol_name_mapping.fix_cc_identifier(field_name)
self._number: int = field_number
self._type: int = field_type
self._type_node: Optional[ProtoNode] = type_node
+ self._optional: bool = optional
self._repeated: bool = repeated
+ self._options: Optional[CodegenOptions] = codegen_options
def name(self) -> str:
return self.upper_camel_case(self._field_name)
+ def field_name(self) -> str:
+ return self._field_name
+
def enum_name(self) -> str:
- return self.upper_snake_case(self._field_name)
+ return 'k' + self.name()
+
+ def legacy_enum_name(self) -> str:
+ return self.upper_snake_case(
+ symbol_name_mapping.fix_cc_enum_value_name(self._field_name)
+ )
def number(self) -> int:
return self._number
@@ -311,16 +521,20 @@ class ProtoMessageField:
def type_node(self) -> Optional[ProtoNode]:
return self._type_node
+ def is_optional(self) -> bool:
+ return self._optional
+
def is_repeated(self) -> bool:
return self._repeated
+ def options(self) -> Optional[CodegenOptions]:
+ return self._options
+
@staticmethod
def upper_camel_case(field_name: str) -> str:
"""Converts a field name to UpperCamelCase."""
name_components = field_name.split('_')
- for i, _ in enumerate(name_components):
- name_components[i] = name_components[i].lower().capitalize()
- return ''.join(name_components)
+ return ''.join([word.lower().capitalize() for word in name_components])
@staticmethod
def upper_snake_case(field_name: str) -> str:
@@ -330,6 +544,7 @@ class ProtoMessageField:
class ProtoServiceMethod:
"""A method defined in a protobuf service."""
+
class Type(enum.Enum):
UNARY = 'kUnary'
SERVER_STREAMING = 'kServerStreaming'
@@ -340,8 +555,14 @@ class ProtoServiceMethod:
"""Returns the pw_rpc MethodType C++ enum for this method type."""
return '::pw::rpc::MethodType::' + self.value
- def __init__(self, service: ProtoService, name: str, method_type: Type,
- request_type: ProtoNode, response_type: ProtoNode):
+ def __init__(
+ self,
+ service: ProtoService,
+ name: str,
+ method_type: Type,
+ request_type: ProtoNode,
+ response_type: ProtoNode,
+ ):
self._service = service
self._name = name
self._type = method_type
@@ -358,12 +579,16 @@ class ProtoServiceMethod:
return self._type
def server_streaming(self) -> bool:
- return self._type in (self.Type.SERVER_STREAMING,
- self.Type.BIDIRECTIONAL_STREAMING)
+ return self._type in (
+ self.Type.SERVER_STREAMING,
+ self.Type.BIDIRECTIONAL_STREAMING,
+ )
def client_streaming(self) -> bool:
- return self._type in (self.Type.CLIENT_STREAMING,
- self.Type.BIDIRECTIONAL_STREAMING)
+ return self._type in (
+ self.Type.CLIENT_STREAMING,
+ self.Type.BIDIRECTIONAL_STREAMING,
+ )
def request_type(self) -> ProtoNode:
return self._request_type
@@ -395,8 +620,9 @@ def _create_external_nodes(root: ProtoNode, path: str) -> ProtoNode:
return node
-def _find_or_create_node(global_root: ProtoNode, package_root: ProtoNode,
- path: str) -> ProtoNode:
+def _find_or_create_node(
+ global_root: ProtoNode, package_root: ProtoNode, path: str
+) -> ProtoNode:
"""Searches the proto tree for a node by path, creating it if not found."""
if path[0] == '.':
@@ -417,8 +643,13 @@ def _find_or_create_node(global_root: ProtoNode, package_root: ProtoNode,
return node
-def _add_message_fields(global_root: ProtoNode, package_root: ProtoNode,
- message: ProtoNode, proto_message) -> None:
+def _add_message_fields(
+ global_root: ProtoNode,
+ package_root: ProtoNode,
+ message: ProtoNode,
+ proto_message,
+ proto_options,
+) -> None:
"""Adds fields from a protobuf message descriptor to a message node."""
assert message.type() == ProtoNode.Type.MESSAGE
message = cast(ProtoMessage, message)
@@ -430,25 +661,63 @@ def _add_message_fields(global_root: ProtoNode, package_root: ProtoNode,
# The "type_name" member contains the global .proto path of the
# field's type object, for example ".pw.protobuf.test.KeyValuePair".
# Try to find the node for this object within the current context.
- type_node = _find_or_create_node(global_root, package_root,
- field.type_name)
+ type_node = _find_or_create_node(
+ global_root, package_root, field.type_name
+ )
else:
type_node = None
- repeated = \
+ optional = field.proto3_optional
+ repeated = (
field.label == descriptor_pb2.FieldDescriptorProto.LABEL_REPEATED
+ )
+
+ codegen_options = (
+ options.match_options(
+ '.'.join((message.proto_path(), field.name)), proto_options
+ )
+ if proto_options is not None
+ else None
+ )
+
+ field_options = (
+ options.create_from_field_options(
+ field.options.Extensions[pwpb_field_options]
+ )
+ if field.options.HasExtension(pwpb_field_options)
+ else None
+ )
+
+ merged_options = None
+
+ if field_options and codegen_options:
+ merged_options = options.merge_field_and_codegen_options(
+ field_options, codegen_options
+ )
+ elif field_options:
+ merged_options = field_options
+ elif codegen_options:
+ merged_options = codegen_options
+
message.add_field(
ProtoMessageField(
field.name,
field.number,
field.type,
type_node,
+ optional,
repeated,
- ))
+ merged_options,
+ )
+ )
-def _add_service_methods(global_root: ProtoNode, package_root: ProtoNode,
- service: ProtoNode, proto_service) -> None:
+def _add_service_methods(
+ global_root: ProtoNode,
+ package_root: ProtoNode,
+ service: ProtoNode,
+ proto_service,
+) -> None:
assert service.type() == ProtoNode.Type.SERVICE
service = cast(ProtoService, service)
@@ -462,22 +731,33 @@ def _add_service_methods(global_root: ProtoNode, package_root: ProtoNode,
else:
method_type = ProtoServiceMethod.Type.UNARY
- request_node = _find_or_create_node(global_root, package_root,
- method.input_type)
- response_node = _find_or_create_node(global_root, package_root,
- method.output_type)
+ request_node = _find_or_create_node(
+ global_root, package_root, method.input_type
+ )
+ response_node = _find_or_create_node(
+ global_root, package_root, method.output_type
+ )
service.add_method(
- ProtoServiceMethod(service, method.name, method_type, request_node,
- response_node))
-
-
-def _populate_fields(proto_file, global_root: ProtoNode,
- package_root: ProtoNode) -> None:
+ ProtoServiceMethod(
+ service, method.name, method_type, request_node, response_node
+ )
+ )
+
+
+def _populate_fields(
+ proto_file: descriptor_pb2.FileDescriptorProto,
+ global_root: ProtoNode,
+ package_root: ProtoNode,
+ proto_options: Optional[options.ParsedOptions],
+) -> None:
"""Traverses a proto file, adding all message and enum fields to a tree."""
+
def populate_message(node, message):
"""Recursively populates nested messages and enums."""
- _add_message_fields(global_root, package_root, node, message)
+ _add_message_fields(
+ global_root, package_root, node, message, proto_options
+ )
for proto_enum in message.enum_type:
_add_enum_fields(node.find(proto_enum.name), proto_enum)
@@ -499,7 +779,9 @@ def _populate_fields(proto_file, global_root: ProtoNode,
_add_service_methods(global_root, package_root, service_node, service)
-def _build_hierarchy(proto_file):
+def _build_hierarchy(
+ proto_file: descriptor_pb2.FileDescriptorProto,
+) -> Tuple[ProtoPackage, ProtoPackage]:
"""Creates a ProtoNode hierarchy from a proto file descriptor."""
root = ProtoPackage('')
@@ -531,12 +813,17 @@ def _build_hierarchy(proto_file):
return root, package_root
-def build_node_tree(file_descriptor_proto) -> Tuple[ProtoNode, ProtoNode]:
+def build_node_tree(
+ file_descriptor_proto: descriptor_pb2.FileDescriptorProto,
+ proto_options: Optional[options.ParsedOptions] = None,
+) -> Tuple[ProtoNode, ProtoNode]:
"""Constructs a tree of proto nodes from a file descriptor.
Returns the root node of the entire proto package tree and the node
representing the file's package.
"""
global_root, package_root = _build_hierarchy(file_descriptor_proto)
- _populate_fields(file_descriptor_proto, global_root, package_root)
+ _populate_fields(
+ file_descriptor_proto, global_root, package_root, proto_options
+ )
return global_root, package_root
diff --git a/pw_protobuf/py/pw_protobuf/symbol_name_mapping.py b/pw_protobuf/py/pw_protobuf/symbol_name_mapping.py
new file mode 100755
index 000000000..ffe3c9451
--- /dev/null
+++ b/pw_protobuf/py/pw_protobuf/symbol_name_mapping.py
@@ -0,0 +1,613 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Fixes identifiers that would cause compiler errors in generated C++ code."""
+
+from typing import Set
+
+# Set of words that can't be used as identifiers in the generated code. Many of
+# these are valid identifiers in proto syntax, but they need special handling in
+# the generated C++ code.
+#
+# Note: This is primarily used for "if x in y" operations, hence the use of a
+# set rather than a list.
+PW_PROTO_CODEGEN_RESERVED_WORDS: Set[str] = {
+ # Identifiers that conflict with the codegen internals when used in certain
+ # contexts:
+ "Fields",
+ "Message",
+ # C++20 keywords (https://en.cppreference.com/w/cpp/keyword):
+ "alignas",
+ "alignof",
+ "and",
+ "and_eq",
+ "asm",
+ "atomic_cancel",
+ "atomic_commit",
+ "atomic_noexcept",
+ "auto",
+ "bitand",
+ "bitor",
+ "bool",
+ "break",
+ "case",
+ "catch",
+ "char",
+ "char8_t",
+ "char16_t",
+ "char32_t",
+ "class",
+ "compl",
+ "concept",
+ "const",
+ "consteval",
+ "constexpr",
+ "constinit",
+ "const_cast",
+ "continue",
+ "co_await",
+ "co_return",
+ "co_yield",
+ "decltype",
+ "default",
+ "delete",
+ "do",
+ "double",
+ "dynamic_cast",
+ "else",
+ "enum",
+ "explicit",
+ "export",
+ "extern",
+ "false",
+ "float",
+ "for",
+ "friend",
+ "goto",
+ "if",
+ "inline",
+ "int",
+ "long",
+ "mutable",
+ "namespace",
+ "new",
+ "noexcept",
+ "not",
+ "not_eq",
+ "nullptr",
+ "operator",
+ "or",
+ "or_eq",
+ "private",
+ "protected",
+ "public",
+ "reflexpr",
+ "register",
+ "reinterpret_cast",
+ "requires",
+ "return",
+ "short",
+ "signed",
+ "sizeof",
+ "static",
+ "static_assert",
+ "static_cast",
+ "struct",
+ "switch",
+ "synchronized",
+ "template",
+ "this",
+ "thread_local",
+ "throw",
+ "true",
+ "try",
+ "typedef",
+ "typeid",
+ "typename",
+ "union",
+ "unsigned",
+ "using",
+ "virtual",
+ "void",
+ "volatile",
+ "wchar_t",
+ "while",
+ "xor",
+ "xor_eq",
+ # C++20 macros (https://en.cppreference.com/w/cpp/symbol_index/macro),
+ # excluding the following:
+ # - Function-like macros, which have unambiguous syntax and thus won't
+ # conflict with generated symbols.
+ # - Macros that couldn't be made valid by appending underscores, namely
+ # those containing "__" or starting with "_[A-Z]". C++ reserves all such
+ # identifiers for the compiler, and appending underscores wouldn't change
+ # that.
+ "ATOMIC_BOOL_LOCK_FREE",
+ "ATOMIC_CHAR_LOCK_FREE",
+ "ATOMIC_CHAR16_T_LOCK_FREE",
+ "ATOMIC_CHAR32_T_LOCK_FREE",
+ "ATOMIC_CHAR8_T_LOCK_FREE",
+ "ATOMIC_FLAG_INIT",
+ "ATOMIC_INT_LOCK_FREE",
+ "ATOMIC_LLONG_LOCK_FREE",
+ "ATOMIC_LONG_LOCK_FREE",
+ "ATOMIC_POINTER_LOCK_FREE",
+ "ATOMIC_SHORT_LOCK_FREE",
+ "ATOMIC_WCHAR_T_LOCK_FREE",
+ "BUFSIZ",
+ "CHAR_BIT",
+ "CHAR_MAX",
+ "CHAR_MIN",
+ "CLOCKS_PER_SEC",
+ "DBL_DECIMAL_DIG",
+ "DBL_DIG",
+ "DBL_EPSILON",
+ "DBL_HAS_SUBNORM",
+ "DBL_MANT_DIG",
+ "DBL_MAX",
+ "DBL_MAX_10_EXP",
+ "DBL_MAX_EXP",
+ "DBL_MIN",
+ "DBL_MIN_10_EXP",
+ "DBL_MIN_EXP",
+ "DBL_TRUE_MIN",
+ "DECIMAL_DIG",
+ "E2BIG",
+ "EACCES",
+ "EADDRINUSE",
+ "EADDRNOTAVAIL",
+ "EAFNOSUPPORT",
+ "EAGAIN",
+ "EALREADY",
+ "EBADF",
+ "EBADMSG",
+ "EBUSY",
+ "ECANCELED",
+ "ECHILD",
+ "ECONNABORTED",
+ "ECONNREFUSED",
+ "ECONNRESET",
+ "EDEADLK",
+ "EDESTADDRREQ",
+ "EDOM",
+ "EEXIST",
+ "EFAULT",
+ "EFBIG",
+ "EHOSTUNREACH",
+ "EIDRM",
+ "EILSEQ",
+ "EINPROGRESS",
+ "EINTR",
+ "EINVAL",
+ "EIO",
+ "EISCONN",
+ "EISDIR",
+ "ELOOP",
+ "EMFILE",
+ "EMLINK",
+ "EMSGSIZE",
+ "ENAMETOOLONG",
+ "ENETDOWN",
+ "ENETRESET",
+ "ENETUNREACH",
+ "ENFILE",
+ "ENOBUFS",
+ "ENODATA",
+ "ENODEV",
+ "ENOENT",
+ "ENOEXEC",
+ "ENOLCK",
+ "ENOLINK",
+ "ENOMEM",
+ "ENOMSG",
+ "ENOPROTOOPT",
+ "ENOSPC",
+ "ENOSR",
+ "ENOSTR",
+ "ENOSYS",
+ "ENOTCONN",
+ "ENOTDIR",
+ "ENOTEMPTY",
+ "ENOTRECOVERABLE",
+ "ENOTSOCK",
+ "ENOTSUP",
+ "ENOTTY",
+ "ENXIO",
+ "EOF",
+ "EOPNOTSUPP",
+ "EOVERFLOW",
+ "EOWNERDEAD",
+ "EPERM",
+ "EPIPE",
+ "EPROTO",
+ "EPROTONOSUPPORT",
+ "EPROTOTYPE",
+ "ERANGE",
+ "EROFS",
+ "errno",
+ "ESPIPE",
+ "ESRCH",
+ "ETIME",
+ "ETIMEDOUT",
+ "ETXTBSY",
+ "EWOULDBLOCK",
+ "EXDEV",
+ "EXIT_FAILURE",
+ "EXIT_SUCCESS",
+ "FE_ALL_EXCEPT",
+ "FE_DFL_ENV",
+ "FE_DIVBYZERO",
+ "FE_DOWNWARD",
+ "FE_INEXACT",
+ "FE_INVALID",
+ "FE_OVERFLOW",
+ "FE_TONEAREST",
+ "FE_TOWARDZERO",
+ "FE_UNDERFLOW",
+ "FE_UPWARD",
+ "FILENAME_MAX",
+ "FLT_DECIMAL_DIG",
+ "FLT_DIG",
+ "FLT_EPSILON",
+ "FLT_EVAL_METHOD",
+ "FLT_HAS_SUBNORM",
+ "FLT_MANT_DIG",
+ "FLT_MAX",
+ "FLT_MAX_10_EXP",
+ "FLT_MAX_EXP",
+ "FLT_MIN",
+ "FLT_MIN_10_EXP",
+ "FLT_MIN_EXP",
+ "FLT_RADIX",
+ "FLT_ROUNDS",
+ "FLT_TRUE_MIN",
+ "FOPEN_MAX",
+ "FP_FAST_FMA",
+ "FP_FAST_FMAF",
+ "FP_FAST_FMAL",
+ "FP_ILOGB0",
+ "FP_ILOGBNAN",
+ "FP_SUBNORMAL",
+ "FP_ZERO",
+ "FP_INFINITE",
+ "FP_NAN",
+ "FP_NORMAL",
+ "HUGE_VAL",
+ "HUGE_VALF",
+ "HUGE_VALL",
+ "INFINITY",
+ "INT_FAST16_MAX",
+ "INT_FAST16_MIN",
+ "INT_FAST32_MAX",
+ "INT_FAST32_MIN",
+ "INT_FAST64_MAX",
+ "INT_FAST64_MIN",
+ "INT_FAST8_MAX",
+ "INT_FAST8_MIN",
+ "INT_LEAST16_MAX",
+ "INT_LEAST16_MIN",
+ "INT_LEAST32_MAX",
+ "INT_LEAST32_MIN",
+ "INT_LEAST64_MAX",
+ "INT_LEAST64_MIN",
+ "INT_LEAST8_MAX",
+ "INT_LEAST8_MIN",
+ "INT_MAX",
+ "INT_MIN",
+ "INT16_MAX",
+ "INT16_MIN",
+ "INT32_MAX",
+ "INT32_MIN",
+ "INT64_MAX",
+ "INT64_MIN",
+ "INT8_MAX",
+ "INT8_MIN",
+ "INTMAX_MAX",
+ "INTMAX_MIN",
+ "INTPTR_MAX",
+ "INTPTR_MIN",
+ "L_tmpnam",
+ "LC_ALL",
+ "LC_COLLATE",
+ "LC_CTYPE",
+ "LC_MONETARY",
+ "LC_NUMERIC",
+ "LC_TIME",
+ "LDBL_DECIMAL_DIG",
+ "LDBL_DIG",
+ "LDBL_EPSILON",
+ "LDBL_HAS_SUBNORM",
+ "LDBL_MANT_DIG",
+ "LDBL_MAX",
+ "LDBL_MAX_10_EXP",
+ "LDBL_MAX_EXP",
+ "LDBL_MIN",
+ "LDBL_MIN_10_EXP",
+ "LDBL_MIN_EXP",
+ "LDBL_TRUE_MIN",
+ "LLONG_MAX",
+ "LLONG_MIN",
+ "LONG_MAX",
+ "LONG_MIN",
+ "MATH_ERREXCEPT",
+ "math_errhandling",
+ "MATH_ERRNO",
+ "MB_CUR_MAX",
+ "MB_LEN_MAX",
+ "NAN",
+ "NULL",
+ "ONCE_FLAG_INIT",
+ "PRId16",
+ "PRId32",
+ "PRId64",
+ "PRId8",
+ "PRIdFAST16",
+ "PRIdFAST32",
+ "PRIdFAST64",
+ "PRIdFAST8",
+ "PRIdLEAST16",
+ "PRIdLEAST32",
+ "PRIdLEAST64",
+ "PRIdLEAST8",
+ "PRIdMAX",
+ "PRIdPTR",
+ "PRIi16",
+ "PRIi32",
+ "PRIi64",
+ "PRIi8",
+ "PRIiFAST16",
+ "PRIiFAST32",
+ "PRIiFAST64",
+ "PRIiFAST8",
+ "PRIiLEAST16",
+ "PRIiLEAST32",
+ "PRIiLEAST64",
+ "PRIiLEAST8",
+ "PRIiMAX",
+ "PRIiPTR",
+ "PRIo16",
+ "PRIo32",
+ "PRIo64",
+ "PRIo8",
+ "PRIoFAST16",
+ "PRIoFAST32",
+ "PRIoFAST64",
+ "PRIoFAST8",
+ "PRIoLEAST16",
+ "PRIoLEAST32",
+ "PRIoLEAST64",
+ "PRIoLEAST8",
+ "PRIoMAX",
+ "PRIoPTR",
+ "PRIu16",
+ "PRIu32",
+ "PRIu64",
+ "PRIu8",
+ "PRIuFAST16",
+ "PRIuFAST32",
+ "PRIuFAST64",
+ "PRIuFAST8",
+ "PRIuLEAST16",
+ "PRIuLEAST32",
+ "PRIuLEAST64",
+ "PRIuLEAST8",
+ "PRIuMAX",
+ "PRIuPTR",
+ "PRIx16",
+ "PRIX16",
+ "PRIx32",
+ "PRIX32",
+ "PRIx64",
+ "PRIX64",
+ "PRIx8",
+ "PRIX8",
+ "PRIxFAST16",
+ "PRIXFAST16",
+ "PRIxFAST32",
+ "PRIXFAST32",
+ "PRIxFAST64",
+ "PRIXFAST64",
+ "PRIxFAST8",
+ "PRIXFAST8",
+ "PRIxLEAST16",
+ "PRIXLEAST16",
+ "PRIxLEAST32",
+ "PRIXLEAST32",
+ "PRIxLEAST64",
+ "PRIXLEAST64",
+ "PRIxLEAST8",
+ "PRIXLEAST8",
+ "PRIxMAX",
+ "PRIXMAX",
+ "PRIxPTR",
+ "PRIXPTR",
+ "PTRDIFF_MAX",
+ "PTRDIFF_MIN",
+ "RAND_MAX",
+ "SCHAR_MAX",
+ "SCHAR_MIN",
+ "SCNd16",
+ "SCNd32",
+ "SCNd64",
+ "SCNd8",
+ "SCNdFAST16",
+ "SCNdFAST32",
+ "SCNdFAST64",
+ "SCNdFAST8",
+ "SCNdLEAST16",
+ "SCNdLEAST32",
+ "SCNdLEAST64",
+ "SCNdLEAST8",
+ "SCNdMAX",
+ "SCNdPTR",
+ "SCNi16",
+ "SCNi32",
+ "SCNi64",
+ "SCNi8",
+ "SCNiFAST16",
+ "SCNiFAST32",
+ "SCNiFAST64",
+ "SCNiFAST8",
+ "SCNiLEAST16",
+ "SCNiLEAST32",
+ "SCNiLEAST64",
+ "SCNiLEAST8",
+ "SCNiMAX",
+ "SCNiPTR",
+ "SCNo16",
+ "SCNo32",
+ "SCNo64",
+ "SCNo8",
+ "SCNoFAST16",
+ "SCNoFAST32",
+ "SCNoFAST64",
+ "SCNoFAST8",
+ "SCNoLEAST16",
+ "SCNoLEAST32",
+ "SCNoLEAST64",
+ "SCNoLEAST8",
+ "SCNoMAX",
+ "SCNoPTR",
+ "SCNu16",
+ "SCNu32",
+ "SCNu64",
+ "SCNu8",
+ "SCNuFAST16",
+ "SCNuFAST32",
+ "SCNuFAST64",
+ "SCNuFAST8",
+ "SCNuLEAST16",
+ "SCNuLEAST32",
+ "SCNuLEAST64",
+ "SCNuLEAST8",
+ "SCNuMAX",
+ "SCNuPTR",
+ "SCNx16",
+ "SCNx32",
+ "SCNx64",
+ "SCNx8",
+ "SCNxFAST16",
+ "SCNxFAST32",
+ "SCNxFAST64",
+ "SCNxFAST8",
+ "SCNxLEAST16",
+ "SCNxLEAST32",
+ "SCNxLEAST64",
+ "SCNxLEAST8",
+ "SCNxMAX",
+ "SCNxPTR",
+ "SEEK_CUR",
+ "SEEK_END",
+ "SEEK_SET",
+ "SHRT_MAX",
+ "SHRT_MIN",
+ "SIG_ATOMIC_MAX",
+ "SIG_ATOMIC_MIN",
+ "SIG_DFL",
+ "SIG_ERR",
+ "SIG_IGN",
+ "SIGABRT",
+ "SIGFPE",
+ "SIGILL",
+ "SIGINT",
+ "SIGSEGV",
+ "SIGTERM",
+ "SIZE_MAX",
+ "stderr",
+ "stdin",
+ "stdout",
+ "TIME_UTC",
+ "TMP_MAX",
+ "UCHAR_MAX",
+ "UINT_FAST16_MAX",
+ "UINT_FAST32_MAX",
+ "UINT_FAST64_MAX",
+ "UINT_FAST8_MAX",
+ "UINT_LEAST16_MAX",
+ "UINT_LEAST32_MAX",
+ "UINT_LEAST64_MAX",
+ "UINT_LEAST8_MAX",
+ "UINT_MAX",
+ "UINT16_MAX",
+ "UINT32_MAX",
+ "UINT64_MAX",
+ "UINT8_MAX",
+ "UINTMAX_MAX",
+ "UINTPTR_MAX",
+ "ULLONG_MAX",
+ "ULONG_MAX",
+ "USHRT_MAX",
+ "WCHAR_MAX",
+ "WCHAR_MIN",
+ "WEOF",
+ "WINT_MAX",
+ "WINT_MIN",
+}
+
+
+def _transform_invalid_identifier(invalid_identifier: str) -> str:
+ """Applies a transformation to an invalid C++ identifier to make it valid.
+
+ Currently, this simply appends an underscore. This addresses the vast
+ majority of realistic cases, but there are some caveats; see
+ `fix_cc_identifier` function documentation for details.
+ """
+ return f"{invalid_identifier}_"
+
+
+def fix_cc_identifier(proto_identifier: str) -> str:
+ """Returns an adjusted form of the identifier for use in generated C++ code.
+
+ If the given identifier is already valid for use in the generated C++ code,
+ it will be returned as-is. If the identifier is a C++ keyword or a
+ preprocessor macro from the standard library, the returned identifier will
+ be modified slightly in order to avoid compiler errors.
+
+ Currently, this simply appends an underscore if necessary. This handles the
+ vast majority of realistic cases, though it doesn't attempt to fix
+ identifiers that the C++ spec reserves for the compiler's use.
+
+ For reference, C++ reserves two categories of identifiers for the compiler:
+ - Any identifier that contains the substring "__" anywhere in it.
+ - Any identifier with an underscore for the first character and a capital
+ letter for the second character.
+ """
+ return (
+ _transform_invalid_identifier(proto_identifier) #
+ if proto_identifier in PW_PROTO_CODEGEN_RESERVED_WORDS #
+ else proto_identifier
+ )
+
+
+def fix_cc_enum_value_name(proto_enum_entry: str) -> str:
+ """Returns an adjusted form of the enum-value name for use in generated C++.
+
+ Generates an UPPER_SNAKE_CASE variant of the given enum-value name and then
+ checks it for collisions with C++ keywords and standard-library macros.
+ Returns a potentially modified version of the input in order to fix
+ collisions if any are found.
+
+ Note that, although the code generation also creates enum-value aliases in
+ kHungarianNotationPascalCase, symbols of that form never conflict with
+ keywords or standard-library macros in C++20. Therefore, only the
+ UPPER_SNAKE_CASE versions need to be checked for conflicts.
+
+ See `fix_cc_identifier` for further details.
+ """
+ upper_snake_case = proto_enum_entry.upper()
+ return (
+ _transform_invalid_identifier(proto_enum_entry) #
+ if upper_snake_case in PW_PROTO_CODEGEN_RESERVED_WORDS #
+ else proto_enum_entry
+ )
diff --git a/pw_protobuf/py/setup.cfg b/pw_protobuf/py/setup.cfg
index d4bc72fb4..883edded4 100644
--- a/pw_protobuf/py/setup.cfg
+++ b/pw_protobuf/py/setup.cfg
@@ -21,7 +21,10 @@ description = Lightweight streaming protobuf implementation
[options]
packages = find:
zip_safe = False
-install_requires = protobuf; pw_cli
+install_requires =
+ protobuf>=3.20.1
+ googleapis-common-protos>=1.56.2
+ graphlib-backport;python_version<'3.9'
[options.entry_points]
console_scripts =
diff --git a/pw_protobuf/size_report.rst b/pw_protobuf/size_report.rst
new file mode 100644
index 000000000..1adf6d2af
--- /dev/null
+++ b/pw_protobuf/size_report.rst
@@ -0,0 +1,62 @@
+.. _module-pw_protobuf-size_report:
+
+================================
+pw_protobuf extended size report
+================================
+pw_protobuf can impact binary size very differently depending on how it's used.
+A series of examples are provided below to illustrate how much certain use cases
+affect binary size.
+
+--------
+Overview
+--------
+This module includes a proto encoder, two different proto decoders (one that
+operates on a ``pw::stream::StreamReader`` and another that operates on an in-
+memory buffer), codegen for direct wire-format encoders/decoders, and a
+table-based codegen system for constructing proto messages as in-memory structs.
+
+Here's a brief overview of the different encoder/decoder costs:
+
+.. include:: size_report/protobuf_overview
+
+.. note::
+
+ There's some overhead involved in ensuring all of the encoder/decoder
+ functionality is pulled in. Check the per-symbol breakdown for more details.
+
+--------------------------------
+Encoder/decoder codegen overhead
+--------------------------------
+The different proto serialization/deserialization codegen methods have different
+overhead. Some have a higher up-front cost, but lower complexity (and therefore
+smaller compiler generated code) at the sites of usage. Others trade lower
+up-front code size cost for more complexity at the proto construction and read
+sites.
+
+This example uses the following proto message to construct a variety of use
+cases to illustrate how code and memory requirements can change depending on
+the complexity of the proto message being encoded/decoded.
+
+.. literalinclude:: pw_protobuf_test_protos/size_report.proto
+ :language: protobuf
+ :lines: 14-
+
+This proto is configured with the following options file:
+
+.. literalinclude:: pw_protobuf_test_protos/size_report.options
+ :lines: 14-
+
+Trivial proto
+=============
+This is a size report for encoding/decoding the ``pw.protobuf.test.ItemInfo``
+message. This is a pretty trivial message with just a few integers.
+
+.. include:: size_report/simple_codegen_size_comparison
+
+Optional and oneof
+==================
+This is a size report for encoding/decoding the
+``pw.protobuf.test.ResponseInfo`` message. This is slightly more complex message
+that has a few explicitly optional fields, a oneof, and a submessage.
+
+.. include:: size_report/oneof_codegen_size_comparison
diff --git a/pw_protobuf/size_report/BUILD.bazel b/pw_protobuf/size_report/BUILD.bazel
new file mode 100644
index 000000000..3e4d7c0ca
--- /dev/null
+++ b/pw_protobuf/size_report/BUILD.bazel
@@ -0,0 +1,161 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_binary",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_binary(
+ name = "decoder_partial",
+ srcs = [
+ "decoder_partial.cc",
+ ],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_preprocessor",
+ "//pw_protobuf",
+ ],
+)
+
+pw_cc_binary(
+ name = "decoder_incremental",
+ srcs = [
+ "decoder_incremental.cc",
+ ],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_preprocessor",
+ "//pw_protobuf",
+ ],
+)
+
+pw_cc_library(
+ name = "proto_bloat",
+ srcs = [
+ "proto_bloat.cc",
+ ],
+ hdrs = ["proto_bloat.h"],
+ deps = [
+ "//pw_containers",
+ "//pw_preprocessor",
+ "//pw_protobuf",
+ "//pw_status",
+ "//pw_stream",
+ ],
+)
+
+pw_cc_binary(
+ name = "proto_base",
+ srcs = [
+ "proto_base.cc",
+ ],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ ],
+)
+
+pw_cc_binary(
+ name = "encode_decode_core",
+ srcs = [
+ "encode_decode_core.cc",
+ ],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ ],
+)
+
+pw_cc_binary(
+ name = "message_core",
+ srcs = [
+ "message_core.cc",
+ ],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ ],
+)
+
+pw_cc_binary(
+ name = "messages_no_codegen",
+ srcs = ["simple_codegen_comparison.cc"],
+ defines = ["_PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN=1"],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ "//pw_protobuf:codegen_test_proto_cc.pwpb",
+ ],
+)
+
+pw_cc_binary(
+ name = "messages_wire_format",
+ srcs = ["simple_codegen_comparison.cc"],
+ defines = ["_PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT=1"],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ "//pw_protobuf:codegen_test_proto_cc.pwpb",
+ ],
+)
+
+pw_cc_binary(
+ name = "messages_message",
+ srcs = ["simple_codegen_comparison.cc"],
+ defines = ["_PW_PROTOBUF_SIZE_REPORT_MESSAGE=1"],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ "//pw_protobuf:codegen_test_proto_cc.pwpb",
+ ],
+)
+
+pw_cc_binary(
+ name = "oneof_no_codegen",
+ srcs = ["oneof_codegen_comparison.cc"],
+ defines = ["_PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN=1"],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ "//pw_protobuf:codegen_test_proto_cc.pwpb",
+ ],
+)
+
+pw_cc_binary(
+ name = "oneof_wire_format",
+ srcs = ["oneof_codegen_comparison.cc"],
+ defines = ["_PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT=1"],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ "//pw_protobuf:codegen_test_proto_cc.pwpb",
+ ],
+)
+
+pw_cc_binary(
+ name = "oneof_message",
+ srcs = ["oneof_codegen_comparison.cc"],
+ defines = ["_PW_PROTOBUF_SIZE_REPORT_MESSAGE=1"],
+ deps = [
+ ":proto_bloat",
+ "//pw_bloat:bloat_this_binary",
+ "//pw_protobuf:codegen_test_proto_cc.pwpb",
+ ],
+)
diff --git a/pw_protobuf/size_report/BUILD.gn b/pw_protobuf/size_report/BUILD.gn
index 4906c86ee..a28f2da10 100644
--- a/pw_protobuf/size_report/BUILD.gn
+++ b/pw_protobuf/size_report/BUILD.gn
@@ -15,24 +15,121 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_bloat/bloat.gni")
+import("$dir_pw_build/target_types.gni")
-_decoder_full = {
+_decoder_partial = {
deps = [
"$dir_pw_bloat:bloat_this_binary",
"$dir_pw_preprocessor",
- "$dir_pw_protobuf:pw_protobuf",
+ "$dir_pw_protobuf",
]
- sources = [ "decoder_full.cc" ]
+ sources = [ "decoder_partial.cc" ]
}
-pw_toolchain_size_report("decoder_full") {
+pw_source_set("proto_bloat") {
+ public = [ "proto_bloat.h" ]
+ deps = [
+ "$dir_pw_containers",
+ "$dir_pw_preprocessor",
+ "$dir_pw_protobuf",
+ "$dir_pw_status",
+ "$dir_pw_stream",
+ ]
+ sources = [ "proto_bloat.cc" ]
+}
+
+pw_executable("proto_base") {
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ ]
+ sources = [ "proto_base.cc" ]
+}
+
+pw_executable("encode_decode_core") {
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ ]
+ sources = [ "encode_decode_core.cc" ]
+}
+
+pw_executable("message_core") {
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ ]
+ sources = [ "message_core.cc" ]
+}
+
+pw_executable("messages_no_codegen") {
+ defines = [ "_PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN=1" ]
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:codegen_test_protos.pwpb",
+ ]
+ sources = [ "simple_codegen_comparison.cc" ]
+}
+
+pw_executable("messages_wire_format") {
+ defines = [ "_PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT=1" ]
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:codegen_test_protos.pwpb",
+ ]
+ sources = [ "simple_codegen_comparison.cc" ]
+}
+
+pw_executable("messages_message") {
+ defines = [ "_PW_PROTOBUF_SIZE_REPORT_MESSAGE=1" ]
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:codegen_test_protos.pwpb",
+ ]
+ sources = [ "simple_codegen_comparison.cc" ]
+}
+
+pw_executable("oneof_no_codegen") {
+ defines = [ "_PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN=1" ]
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:codegen_test_protos.pwpb",
+ ]
+ sources = [ "oneof_codegen_comparison.cc" ]
+}
+
+pw_executable("oneof_wire_format") {
+ defines = [ "_PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT=1" ]
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:codegen_test_protos.pwpb",
+ ]
+ sources = [ "oneof_codegen_comparison.cc" ]
+}
+
+pw_executable("oneof_message") {
+ defines = [ "_PW_PROTOBUF_SIZE_REPORT_MESSAGE=1" ]
+ deps = [
+ ":proto_bloat",
+ "$dir_pw_bloat:bloat_this_binary",
+ "..:codegen_test_protos.pwpb",
+ ]
+ sources = [ "oneof_codegen_comparison.cc" ]
+}
+
+pw_toolchain_size_diff("decoder_partial") {
base_executable = pw_bloat_empty_base
- diff_executable = _decoder_full
+ diff_executable = _decoder_partial
title = "Size of all decoder methods"
}
-pw_toolchain_size_report("decoder_incremental") {
- base_executable = _decoder_full
+pw_toolchain_size_diff("decoder_incremental") {
+ base_executable = _decoder_partial
diff_executable = {
deps = [
"$dir_pw_bloat:bloat_this_binary",
@@ -43,3 +140,80 @@ pw_toolchain_size_report("decoder_incremental") {
}
title = "Adding more fields to decode callback"
}
+
+pw_size_diff("simple_codegen_size_comparison") {
+ title = "Pigweed protobuf codegen size report"
+ source_filter = "pw::protobuf_size_report::*"
+ binaries = [
+ {
+ target = ":messages_no_codegen"
+ base = ":message_core"
+ label = "Direct wire-format proto encoder"
+ },
+ {
+ target = ":messages_wire_format"
+ base = ":message_core"
+ label = "Generated wrapped wire-format encoder"
+ },
+ {
+ target = ":messages_message"
+ base = ":message_core"
+ label = "Generated message encoder"
+ },
+ ]
+}
+
+pw_size_diff("oneof_codegen_size_comparison") {
+ title = "Pigweed protobuf codegen size report"
+ source_filter = "pw::protobuf_size_report::*"
+ binaries = [
+ {
+ target = ":oneof_no_codegen"
+ base = ":message_core"
+ label = "Direct wire-format proto encoder"
+ },
+ {
+ target = ":oneof_wire_format"
+ base = ":message_core"
+ label = "Generated wrapped wire-format encoder"
+ },
+ {
+ target = ":oneof_message"
+ base = ":message_core"
+ label = "Generated message encoder"
+ },
+ ]
+}
+
+pw_size_diff("message_size_report") {
+ title = "Pigweed protobuf message size report"
+ binaries = [
+ {
+ target = ":one_message_struct_write_vs_base"
+ base = ":proto_base"
+ label = "Message encoder flash cost (incl. wire-format encoder)"
+ },
+ {
+ target = ":one_message_struct_write_vs_encoder"
+ base = ":encoder_full"
+ label = "Message encoder flash cost (excl. wire-format encoder)"
+ },
+ ]
+}
+
+pw_size_diff("protobuf_overview") {
+ title = "Pigweed protobuf encoder size report"
+ source_filter = "pw::protobuf::*|section .code"
+ binaries = [
+ {
+ target = ":encode_decode_core"
+ base = ":proto_base"
+ label = "Full wire-format proto encode/decode library"
+ },
+ {
+ target = ":message_core"
+ base = ":proto_base"
+ label = "Including table-based `Message` encoder and decoder"
+ },
+ ]
+}
diff --git a/pw_protobuf/size_report/decoder_incremental.cc b/pw_protobuf/size_report/decoder_incremental.cc
index 9f4029ffd..064f57931 100644
--- a/pw_protobuf/size_report/decoder_incremental.cc
+++ b/pw_protobuf/size_report/decoder_incremental.cc
@@ -39,7 +39,7 @@ int main() {
double d;
uint32_t uint;
- pw::protobuf::Decoder decoder(std::as_bytes(std::span(encoded_proto)));
+ pw::protobuf::Decoder decoder(pw::as_bytes(pw::span(encoded_proto)));
while (decoder.Next().ok()) {
switch (decoder.FieldNumber()) {
case 1:
diff --git a/pw_protobuf/size_report/decoder_full.cc b/pw_protobuf/size_report/decoder_partial.cc
index 075981e81..561335e26 100644
--- a/pw_protobuf/size_report/decoder_full.cc
+++ b/pw_protobuf/size_report/decoder_partial.cc
@@ -38,7 +38,7 @@ int main() {
float f;
double d;
- pw::protobuf::Decoder decoder(std::as_bytes(std::span(encoded_proto)));
+ pw::protobuf::Decoder decoder(pw::as_bytes(pw::span(encoded_proto)));
while (decoder.Next().ok()) {
switch (decoder.FieldNumber()) {
case 1:
diff --git a/pw_protobuf/size_report/encode_decode_core.cc b/pw_protobuf/size_report/encode_decode_core.cc
new file mode 100644
index 000000000..4a8540438
--- /dev/null
+++ b/pw_protobuf/size_report/encode_decode_core.cc
@@ -0,0 +1,25 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "proto_bloat.h"
+#include "pw_bloat/bloat_this_binary.h"
+
+int main() {
+ pw::bloat::BloatThisBinary();
+ pw::protobuf_size_report::BloatWithBase();
+ pw::protobuf_size_report::BloatWithEncoder();
+ pw::protobuf_size_report::BloatWithDecoder();
+ pw::protobuf_size_report::BloatWithStreamDecoder();
+ return 0;
+}
diff --git a/pw_protobuf/size_report/message_core.cc b/pw_protobuf/size_report/message_core.cc
new file mode 100644
index 000000000..a5e275930
--- /dev/null
+++ b/pw_protobuf/size_report/message_core.cc
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "proto_bloat.h"
+#include "pw_bloat/bloat_this_binary.h"
+
+int main() {
+ pw::bloat::BloatThisBinary();
+ pw::protobuf_size_report::BloatWithBase();
+ pw::protobuf_size_report::BloatWithEncoder();
+ pw::protobuf_size_report::BloatWithDecoder();
+ pw::protobuf_size_report::BloatWithStreamDecoder();
+ pw::protobuf_size_report::BloatWithTableEncoder();
+ pw::protobuf_size_report::BloatWithTableDecoder();
+ return 0;
+}
diff --git a/pw_protobuf/size_report/oneof_codegen_comparison.cc b/pw_protobuf/size_report/oneof_codegen_comparison.cc
new file mode 100644
index 000000000..27317db0d
--- /dev/null
+++ b/pw_protobuf/size_report/oneof_codegen_comparison.cc
@@ -0,0 +1,390 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "proto_bloat.h"
+#include "pw_bloat/bloat_this_binary.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_protobuf/encoder.h"
+#include "pw_protobuf/stream_decoder.h"
+#include "pw_protobuf_test_protos/size_report.pwpb.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+#ifndef _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN
+#define _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN 0
+#endif // _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN
+
+#ifndef _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT
+#define _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT 0
+#endif // _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT
+
+#ifndef _PW_PROTOBUF_SIZE_REPORT_MESSAGE
+#define _PW_PROTOBUF_SIZE_REPORT_MESSAGE 0
+#endif // _PW_PROTOBUF_SIZE_REPORT_MESSAGE
+
+namespace pw::protobuf_size_report {
+namespace {
+
+namespace ItemInfo = pwpb::ItemInfo;
+namespace ResponseInfo = pwpb::ResponseInfo;
+
+template <typename T>
+PW_NO_INLINE void ConsumeValue(T val) {
+ [[maybe_unused]] volatile T no_optimize = val;
+}
+
+#if _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN
+
+std::array<std::byte, ItemInfo::kMaxEncodedSizeBytes> encode_buffer;
+pw::protobuf::MemoryEncoder encoder(encode_buffer);
+
+PW_NO_INLINE void BasicEncode() {
+ pw::Status status;
+ volatile enum KeyType : uint32_t {
+ NONE = 0,
+ KEY_STRING = 1,
+ KEY_TOKEN = 2,
+ } which_key = KeyType::KEY_STRING;
+ volatile bool has_timestamp = true;
+ volatile bool has_has_value = false;
+ if (which_key == KeyType::KEY_STRING) {
+ encoder.WriteString(1, "test");
+ } else if (which_key == KeyType::KEY_TOKEN) {
+ encoder.WriteFixed32(2, 99999);
+ }
+
+ if (has_timestamp) {
+ encoder.WriteInt64(3, 1663003467);
+ }
+
+ if (has_has_value) {
+ encoder.WriteBool(4, true);
+ }
+
+ {
+ pw::protobuf::StreamEncoder submessage_encoder =
+ encoder.GetNestedEncoder(5);
+ status.Update(submessage_encoder.WriteInt64(1, 0x5001DBADFEEDBEE5));
+ status.Update(submessage_encoder.WriteInt32(2, 128));
+ status.Update(submessage_encoder.WriteInt32(3, 2));
+ }
+ ConsumeValue(status);
+}
+
+std::array<std::byte, ItemInfo::kMaxEncodedSizeBytes> decode_buffer;
+pw::protobuf::Decoder decoder(decode_buffer);
+
+PW_NO_INLINE void DecodeItemInfo(pw::ConstByteSpan data) {
+ pw::protobuf::Decoder submessage_decoder(data);
+ while (submessage_decoder.Next().ok()) {
+ switch (submessage_decoder.FieldNumber()) {
+ case static_cast<uint32_t>(ItemInfo::Fields::kOffset): {
+ uint64_t value;
+ if (submessage_decoder.ReadUint64(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case static_cast<uint32_t>(ItemInfo::Fields::kSize): {
+ uint32_t value;
+ if (submessage_decoder.ReadUint32(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case static_cast<uint32_t>(ItemInfo::Fields::kAccessLevel): {
+ uint32_t value;
+
+ if (submessage_decoder.ReadUint32(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ }
+ }
+}
+
+PW_NO_INLINE void BasicDecode() {
+ volatile enum KeyType : uint32_t {
+ NONE = 0,
+ KEY_STRING = 1,
+ KEY_TOKEN = 2,
+ } which_key = KeyType::NONE;
+ volatile bool has_timestamp = false;
+ volatile bool has_has_value = false;
+
+ while (decoder.Next().ok()) {
+ switch (decoder.FieldNumber()) {
+ case static_cast<uint32_t>(ResponseInfo::Fields::kKeyString): {
+ which_key = KeyType::KEY_STRING;
+ std::string_view value;
+ if (decoder.ReadString(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case static_cast<uint32_t>(ResponseInfo::Fields::kKeyToken): {
+ which_key = KeyType::KEY_TOKEN;
+ uint32_t value;
+ if (decoder.ReadUint32(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case static_cast<uint32_t>(ResponseInfo::Fields::kTimestamp): {
+ uint64_t value;
+ has_timestamp = true;
+ if (decoder.ReadUint64(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case static_cast<uint32_t>(ResponseInfo::Fields::kHasValue): {
+ bool value;
+ has_has_value = true;
+ if (decoder.ReadBool(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case static_cast<uint32_t>(ResponseInfo::Fields::kItemInfo): {
+ pw::ConstByteSpan value;
+ if (decoder.ReadBytes(&value).ok()) {
+ DecodeItemInfo(value);
+ }
+ break;
+ }
+ }
+ }
+ ConsumeValue(which_key);
+ ConsumeValue(has_timestamp);
+ ConsumeValue(has_has_value);
+}
+
+#endif // _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN
+
+#if _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT
+
+std::array<std::byte, ResponseInfo::kMaxEncodedSizeBytes> encode_buffer;
+ResponseInfo::MemoryEncoder encoder(encode_buffer);
+
+PW_NO_INLINE void BasicEncode() {
+ pw::Status status;
+ volatile enum KeyType : uint32_t {
+ NONE = 0,
+ KEY_STRING = 1,
+ KEY_TOKEN = 2,
+ } which_key = KeyType::KEY_STRING;
+ volatile bool has_timestamp = true;
+ volatile bool has_has_value = false;
+ if (which_key == KeyType::KEY_STRING) {
+ encoder.WriteKeyString("test");
+ } else if (which_key == KeyType::KEY_TOKEN) {
+ encoder.WriteKeyToken(99999);
+ }
+
+ if (has_timestamp) {
+ encoder.WriteTimestamp(1663003467);
+ }
+
+ if (has_has_value) {
+ encoder.WriteHasValue(true);
+ }
+
+ {
+ ItemInfo::StreamEncoder submessage_encoder = encoder.GetItemInfoEncoder();
+ status.Update(submessage_encoder.WriteOffset(0x5001DBADFEEDBEE5));
+ status.Update(submessage_encoder.WriteSize(128));
+ status.Update(submessage_encoder.WriteAccessLevel(ItemInfo::Access::WRITE));
+ }
+ ConsumeValue(status);
+}
+
+std::array<std::byte, ResponseInfo::kMaxEncodedSizeBytes> decode_buffer;
+pw::stream::MemoryReader reader(decode_buffer);
+ResponseInfo::StreamDecoder decoder(reader);
+
+PW_NO_INLINE void DecodeItemInfo(ItemInfo::StreamDecoder& submessage_decoder) {
+ while (submessage_decoder.Next().ok()) {
+ pw::Result<ItemInfo::Fields> field = submessage_decoder.Field();
+ if (!field.ok()) {
+ ConsumeValue(field.status());
+ return;
+ }
+
+ switch (field.value()) {
+ case ItemInfo::Fields::kOffset: {
+ pw::Result<uint64_t> value = submessage_decoder.ReadOffset();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case ItemInfo::Fields::kSize: {
+ pw::Result<uint32_t> value = submessage_decoder.ReadSize();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case ItemInfo::Fields::kAccessLevel: {
+ pw::Result<ItemInfo::Access> value =
+ submessage_decoder.ReadAccessLevel();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ }
+ }
+}
+
+PW_NO_INLINE void BasicDecode() {
+ volatile enum KeyType : uint32_t {
+ NONE = 0,
+ KEY_STRING = 1,
+ KEY_TOKEN = 2,
+ } which_key = KeyType::NONE;
+ volatile bool has_timestamp = false;
+ volatile bool has_has_value = false;
+
+ while (decoder.Next().ok()) {
+ while (decoder.Next().ok()) {
+ pw::Result<ResponseInfo::Fields> field = decoder.Field();
+ if (!field.ok()) {
+ ConsumeValue(field.status());
+ return;
+ }
+
+ switch (field.value()) {
+ case ResponseInfo::Fields::kKeyString: {
+ which_key = KeyType::KEY_STRING;
+ std::array<char, 8> value;
+ pw::StatusWithSize status = decoder.ReadKeyString(value);
+ if (status.ok()) {
+ ConsumeValue(pw::span(value));
+ }
+ break;
+ }
+ case ResponseInfo::Fields::kKeyToken: {
+ which_key = KeyType::KEY_TOKEN;
+ pw::Result<uint32_t> value = decoder.ReadKeyToken();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case ResponseInfo::Fields::kTimestamp: {
+ has_timestamp = true;
+ pw::Result<int64_t> value = decoder.ReadTimestamp();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case ResponseInfo::Fields::kHasValue: {
+ has_has_value = true;
+ pw::Result<bool> value = decoder.ReadHasValue();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case ResponseInfo::Fields::kItemInfo: {
+ ItemInfo::StreamDecoder submessage_decoder =
+ decoder.GetItemInfoDecoder();
+ DecodeItemInfo(submessage_decoder);
+ break;
+ }
+ }
+ }
+ }
+ ConsumeValue(which_key);
+ ConsumeValue(has_timestamp);
+ ConsumeValue(has_has_value);
+}
+#endif // _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT
+
+#if _PW_PROTOBUF_SIZE_REPORT_MESSAGE
+
+ResponseInfo::Message message;
+
+std::array<std::byte, ResponseInfo::kMaxEncodedSizeBytes> encode_buffer;
+ResponseInfo::MemoryEncoder encoder(encode_buffer);
+
+PW_NO_INLINE void BasicEncode() {
+ volatile enum KeyType : uint32_t {
+ NONE = 0,
+ KEY_STRING = 1,
+ KEY_TOKEN = 2,
+ } which_key = KeyType::KEY_STRING;
+ volatile bool has_timestamp = true;
+ volatile bool has_has_value = false;
+ if (which_key == KeyType::KEY_STRING) {
+ message.key_string.SetEncoder(
+ [](ResponseInfo::StreamEncoder& key_string_encoder) -> pw::Status {
+ key_string_encoder.WriteKeyString("test");
+ return pw::OkStatus();
+ });
+ } else if (which_key == KeyType::KEY_TOKEN) {
+ message.key_token = 99999;
+ }
+ message.timestamp =
+ has_timestamp ? std::optional<uint32_t>(1663003467) : std::nullopt;
+ message.has_value = has_has_value ? std::optional<bool>(false) : std::nullopt;
+
+ message.item_info.offset = 0x5001DBADFEEDBEE5;
+ message.item_info.size = 128;
+ message.item_info.access_level = ItemInfo::Access::WRITE;
+ ConsumeValue(encoder.Write(message));
+}
+
+std::array<std::byte, ResponseInfo::kMaxEncodedSizeBytes> decode_buffer;
+pw::stream::MemoryReader reader(decode_buffer);
+ResponseInfo::StreamDecoder decoder(reader);
+
+PW_NO_INLINE void BasicDecode() {
+ volatile enum KeyType : uint32_t {
+ NONE = 0,
+ KEY_STRING = 1,
+ KEY_TOKEN = 2,
+ } which_key = KeyType::NONE;
+ volatile bool has_timestamp = false;
+ volatile bool has_has_value = false;
+ if (pw::Status status = decoder.Read(message); status.ok()) {
+ ConsumeValue(status);
+ has_timestamp = message.timestamp.has_value();
+ has_has_value = message.has_value.has_value();
+ }
+ ConsumeValue(which_key);
+ ConsumeValue(has_timestamp);
+ ConsumeValue(has_has_value);
+}
+#endif // _PW_PROTOBUF_SIZE_REPORT_MESSAGE
+
+} // namespace
+} // namespace pw::protobuf_size_report
+
+int main() {
+ pw::bloat::BloatThisBinary();
+ pw::protobuf_size_report::BloatWithBase();
+ pw::protobuf_size_report::BloatWithEncoder();
+ pw::protobuf_size_report::BloatWithStreamDecoder();
+ pw::protobuf_size_report::BloatWithDecoder();
+ pw::protobuf_size_report::BloatWithTableEncoder();
+ pw::protobuf_size_report::BloatWithTableDecoder();
+ pw::protobuf_size_report::BasicEncode();
+ pw::protobuf_size_report::BasicDecode();
+ return 0;
+}
diff --git a/pw_assert_zephyr/assert_zephyr.cc b/pw_protobuf/size_report/proto_base.cc
index 1deae5d11..49be2a694 100644
--- a/pw_assert_zephyr/assert_zephyr.cc
+++ b/pw_protobuf/size_report/proto_base.cc
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,12 +12,11 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include <kernel.h>
+#include "proto_bloat.h"
+#include "pw_bloat/bloat_this_binary.h"
-#include "pw_assert/assert.h"
-#include "pw_preprocessor/compiler.h"
-
-extern "C" void pw_assert_HandleFailure(void) {
- k_panic();
- PW_UNREACHABLE;
+int main() {
+ pw::bloat::BloatThisBinary();
+ pw::protobuf_size_report::BloatWithBase();
+ return 0;
}
diff --git a/pw_protobuf/size_report/proto_bloat.cc b/pw_protobuf/size_report/proto_bloat.cc
new file mode 100644
index 000000000..9d5884422
--- /dev/null
+++ b/pw_protobuf/size_report/proto_bloat.cc
@@ -0,0 +1,350 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "proto_bloat.h"
+
+#include <array>
+#include <cstdint>
+#include <string_view>
+
+#include "pw_containers/vector.h"
+#include "pw_preprocessor/concat.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_protobuf/encoder.h"
+#include "pw_protobuf/stream_decoder.h"
+#include "pw_status/status.h"
+#include "pw_stream/null_stream.h"
+#include "pw_stream/stream.h"
+
+namespace pw::protobuf_size_report {
+namespace {
+
+template <typename T>
+constexpr std::array<T, 4> GetIntegerArray() {
+ return std::array<T, 4>{958736, 2085792374, 0, 42};
+}
+
+template <typename T>
+constexpr Vector<T, 4> GetIntegerVector() {
+ return Vector<T, 4>{958736, 2085792374, 0, 42};
+}
+
+constexpr std::array<bool, 5> GetBoolArray() {
+ return std::array<bool, 5>{true, false, false, true, true};
+}
+
+Vector<bool, 5> GetBoolVector() {
+ return Vector<bool, 5>{true, false, false, true, true};
+}
+
+constexpr std::array<float, 5> GetFloatArray() {
+ return std::array<float, 5>{1.2f, 3.4f, 5.6e20f, 0.0789f, 0.0f};
+}
+
+Vector<float, 5> GetFloatVector() {
+ return Vector<float, 5>{1.2f, 3.4f, 5.6e20f, 0.0789f, 0.0f};
+}
+
+constexpr std::array<double, 5> GetDoubleArray() {
+ return std::array<double, 5>{1.2, 3.4, 5.6e20, 0.0789, 0.0};
+}
+
+Vector<double, 5> GetDoubleVector() {
+ return Vector<double, 5>{1.2, 3.4, 5.6e20, 0.0789, 0.0};
+}
+
+constexpr std::string_view kTestString("I eat chips too often");
+
+constexpr protobuf::internal::MessageField kFakeTable[] = {
+ {4567,
+ protobuf::WireType::kDelimited,
+ 234567,
+ protobuf::internal::VarintType::kNormal,
+ false,
+ true,
+ true,
+ true,
+ true,
+ 260,
+ 840245,
+ nullptr},
+ {4567,
+ protobuf::WireType::kDelimited,
+ 234567,
+ protobuf::internal::VarintType::kNormal,
+ false,
+ true,
+ true,
+ true,
+ true,
+ 260,
+ 840245,
+ nullptr}};
+
+class FakeMessageEncoder : public protobuf::StreamEncoder {
+ public:
+ FakeMessageEncoder(stream::Writer& writer)
+ : protobuf::StreamEncoder(writer, ByteSpan()) {}
+ void DoBloat() { Write(ByteSpan(), kFakeTable); }
+};
+
+class FakeMessageDecoder : public protobuf::StreamDecoder {
+ public:
+ FakeMessageDecoder(stream::Reader& reader)
+ : protobuf::StreamDecoder(reader) {}
+ void DoBloat() { Read(ByteSpan(), kFakeTable); }
+};
+
+void CodeToSetUpSizeReportEnvironment() {
+ [[maybe_unused]] volatile auto arr1 = GetIntegerArray<uint32_t>();
+ [[maybe_unused]] volatile auto arr2 = GetIntegerArray<int32_t>();
+ [[maybe_unused]] volatile auto arr3 = GetIntegerArray<uint64_t>();
+ [[maybe_unused]] volatile auto arr4 = GetIntegerArray<int64_t>();
+
+ [[maybe_unused]] volatile auto vec1 = GetIntegerVector<uint32_t>();
+ [[maybe_unused]] volatile auto vec2 = GetIntegerVector<int32_t>();
+ [[maybe_unused]] volatile auto vec3 = GetIntegerVector<uint64_t>();
+ [[maybe_unused]] volatile auto vec4 = GetIntegerVector<int64_t>();
+
+ [[maybe_unused]] volatile auto bool1 = GetBoolArray();
+ [[maybe_unused]] volatile auto bool2 = GetBoolVector();
+
+ [[maybe_unused]] volatile auto float1 = GetFloatArray();
+ [[maybe_unused]] volatile auto float2 = GetFloatVector();
+
+ [[maybe_unused]] volatile auto double1 = GetDoubleArray();
+ [[maybe_unused]] volatile auto double2 = GetDoubleVector();
+
+ [[maybe_unused]] volatile std::string_view test_string = kTestString;
+
+ [[maybe_unused]] volatile stream::NullStream null_stream;
+}
+
+void Dependencies() {
+ std::array<std::byte, 2> buffer;
+ stream::NullStream null_stream;
+ stream::MemoryWriter memory_writer(buffer);
+ memory_writer.Write(buffer);
+ null_stream.Write(buffer);
+ stream::MemoryReader memory_reader(buffer);
+ memory_reader.Read(buffer).IgnoreError();
+}
+
+void CodeToPullInProtoEncoder() {
+ std::array<std::byte, 1024> buffer;
+ protobuf::MemoryEncoder encoder(buffer);
+
+ encoder.WriteUint32(1, 1);
+ encoder.WritePackedUint32(1, GetIntegerArray<uint32_t>());
+ encoder.WriteRepeatedUint32(1, GetIntegerVector<uint32_t>());
+
+ encoder.WriteInt32(1, 1);
+ encoder.WritePackedInt32(1, GetIntegerArray<int32_t>());
+ encoder.WriteRepeatedInt32(1, GetIntegerVector<int32_t>());
+
+ encoder.WriteUint64(1, 1);
+ encoder.WritePackedUint64(1, GetIntegerArray<uint64_t>());
+ encoder.WriteRepeatedUint64(1, GetIntegerVector<uint64_t>());
+
+ encoder.WriteInt64(1, 1);
+ encoder.WritePackedInt64(1, GetIntegerArray<int64_t>());
+ encoder.WriteRepeatedInt64(1, GetIntegerVector<int64_t>());
+
+ encoder.WriteSint32(1, 1);
+ encoder.WritePackedSint32(1, GetIntegerArray<int32_t>());
+ encoder.WriteRepeatedSint32(1, GetIntegerVector<int32_t>());
+
+ encoder.WriteSint64(1, 1);
+ encoder.WritePackedSint64(1, GetIntegerArray<int64_t>());
+ encoder.WriteRepeatedSint64(1, GetIntegerVector<int64_t>());
+
+ encoder.WriteFixed32(1, 1);
+ encoder.WritePackedFixed32(1, GetIntegerArray<uint32_t>());
+ encoder.WriteRepeatedFixed32(1, GetIntegerVector<uint32_t>());
+
+ encoder.WriteFixed64(1, 1);
+ encoder.WritePackedFixed64(1, GetIntegerArray<uint64_t>());
+ encoder.WriteRepeatedFixed64(1, GetIntegerVector<uint64_t>());
+
+ encoder.WriteSfixed32(1, 1);
+ encoder.WritePackedSfixed32(1, GetIntegerArray<int32_t>());
+ encoder.WriteRepeatedSfixed32(1, GetIntegerVector<int32_t>());
+
+ encoder.WriteSfixed64(1, 1);
+ encoder.WritePackedSfixed64(1, GetIntegerArray<int64_t>());
+ encoder.WriteRepeatedSfixed64(1, GetIntegerVector<int64_t>());
+
+ {
+ protobuf::StreamEncoder child = encoder.GetNestedEncoder(0xc01dfee7);
+
+ child.WriteFloat(234, 3.14f);
+ child.WritePackedFloat(234, GetFloatArray());
+ child.WriteRepeatedFloat(234, GetFloatVector());
+
+ child.WriteFloat(234, 3.14);
+ child.WritePackedDouble(234, GetDoubleArray());
+ child.WriteRepeatedDouble(234, GetDoubleVector());
+
+ child.WriteBool(7, true);
+ child.WritePackedBool(8, GetBoolArray());
+ child.WriteRepeatedBool(8, GetBoolVector());
+
+ encoder.WriteBytes(93, as_bytes(span<const double>(GetDoubleArray())));
+ encoder.WriteString(21343, kTestString);
+ }
+
+ stream::NullStream null_stream;
+ protobuf::StreamEncoder stream_encoder(null_stream, buffer);
+ stream_encoder.WriteBytesFromStream(3636, null_stream, 10824, buffer);
+}
+
+void CodeToPullInTableEncoder() {
+ stream::NullStream stream;
+ FakeMessageEncoder fake_encoder(stream);
+ fake_encoder.DoBloat();
+}
+
+void CodeToPullInTableDecoder() {
+ stream::NullStream stream;
+ FakeMessageDecoder fake_decoder(stream);
+ fake_decoder.DoBloat();
+}
+
+#define _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(type_camel_case, underlying_type) \
+ do { \
+ Status status; \
+ Vector<underlying_type, 3> vec; \
+ span<underlying_type> packed_span; \
+ status.Update(decoder.PW_CONCAT(Read, type_camel_case)().status()); \
+ status.Update( \
+ decoder.PW_CONCAT(ReadPacked, type_camel_case)(packed_span).status()); \
+ status.Update(decoder.PW_CONCAT(ReadRepeated, type_camel_case)(vec)); \
+ [[maybe_unused]] volatile bool ok = status.ok(); \
+ } while (0)
+
+void CodeToPullInProtoStreamDecoder() {
+ stream::NullStream null_stream;
+ protobuf::StreamDecoder decoder(null_stream);
+ decoder.Next().IgnoreError();
+
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Int32, int32_t);
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Uint32, uint32_t);
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Int64, int64_t);
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Uint64, uint64_t);
+
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Sint32, int32_t);
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Sint64, int64_t);
+
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Bool, bool);
+
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Fixed32, uint32_t);
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Fixed64, uint64_t);
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Sfixed32, int32_t);
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Sfixed64, int64_t);
+
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Float, float);
+ _PW_USE_FUNCTIONS_FOR_STREAM_DECODER(Double, double);
+
+ {
+ Status status;
+ span<char> str_out;
+ span<std::byte> bytes_out;
+ status.Update(decoder.ReadString(str_out).status());
+ status.Update(decoder.ReadBytes(bytes_out).status());
+ status.Update(decoder.GetLengthDelimitedPayloadBounds().status());
+ [[maybe_unused]] volatile Result<uint32_t> field_number =
+ decoder.FieldNumber();
+ [[maybe_unused]] volatile protobuf::StreamDecoder::BytesReader
+ bytes_reader = decoder.GetBytesReader();
+ [[maybe_unused]] volatile bool ok = status.ok();
+ }
+}
+
+#define _PW_USE_FUNCTIONS_FOR_DECODER(type_camel_case, underlying_type) \
+ do { \
+ Status status; \
+ underlying_type val; \
+ status.Update(decoder.PW_CONCAT(Read, type_camel_case)(&val)); \
+ [[maybe_unused]] volatile bool ok = status.ok(); \
+ } while (0)
+
+void CodeToPullInProtoDecoder() {
+ std::array<std::byte, 3> buffer = {
+ std::byte(0x01), std::byte(0xff), std::byte(0x08)};
+ protobuf::Decoder decoder(buffer);
+ decoder.Next().IgnoreError();
+
+ _PW_USE_FUNCTIONS_FOR_DECODER(Int32, int32_t);
+ _PW_USE_FUNCTIONS_FOR_DECODER(Uint32, uint32_t);
+ _PW_USE_FUNCTIONS_FOR_DECODER(Int64, int64_t);
+ _PW_USE_FUNCTIONS_FOR_DECODER(Uint64, uint64_t);
+
+ _PW_USE_FUNCTIONS_FOR_DECODER(Sint32, int32_t);
+ _PW_USE_FUNCTIONS_FOR_DECODER(Sint64, int64_t);
+
+ _PW_USE_FUNCTIONS_FOR_DECODER(Bool, bool);
+
+ _PW_USE_FUNCTIONS_FOR_DECODER(Fixed32, uint32_t);
+ _PW_USE_FUNCTIONS_FOR_DECODER(Fixed64, uint64_t);
+ _PW_USE_FUNCTIONS_FOR_DECODER(Sfixed32, int32_t);
+ _PW_USE_FUNCTIONS_FOR_DECODER(Sfixed64, int64_t);
+
+ _PW_USE_FUNCTIONS_FOR_DECODER(Float, float);
+ _PW_USE_FUNCTIONS_FOR_DECODER(Double, double);
+
+ {
+ Status status;
+ std::string_view str_out;
+ span<const std::byte> bytes_out;
+ status.Update(decoder.ReadString(&str_out));
+ status.Update(decoder.ReadBytes(&bytes_out));
+ decoder.Reset(buffer);
+ [[maybe_unused]] volatile uint32_t field_number = decoder.FieldNumber();
+ [[maybe_unused]] volatile bool ok = status.ok();
+ }
+}
+
+} // namespace
+
+void BloatWithBase() {
+ CodeToSetUpSizeReportEnvironment();
+ Dependencies();
+}
+
+void BloatWithEncoder() {
+ BloatWithBase();
+ CodeToPullInProtoEncoder();
+}
+
+void BloatWithTableEncoder() {
+ BloatWithBase();
+ CodeToPullInTableEncoder();
+}
+
+void BloatWithTableDecoder() {
+ BloatWithBase();
+ CodeToPullInTableDecoder();
+}
+
+void BloatWithStreamDecoder() {
+ BloatWithBase();
+ CodeToPullInProtoStreamDecoder();
+}
+
+void BloatWithDecoder() {
+ BloatWithBase();
+ CodeToPullInProtoDecoder();
+}
+
+} // namespace pw::protobuf_size_report
diff --git a/pw_protobuf/size_report/proto_bloat.h b/pw_protobuf/size_report/proto_bloat.h
new file mode 100644
index 000000000..51654d22a
--- /dev/null
+++ b/pw_protobuf/size_report/proto_bloat.h
@@ -0,0 +1,45 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// This namespace is NOT in pw::protobuf to allow filtering of symbols not
+// intended to be reflected in the size report.
+namespace pw::protobuf_size_report {
+
+// Includes ambient glue-related code that is required to set up size reports.
+// Include this in base size reports to prevent irrelevant symbols from showing
+// up in the final diffs.
+void BloatWithBase();
+
+// Adds pw_protobuf's StreamEncoder and MemoryEncoders to the size report.
+void BloatWithEncoder();
+
+// Adds pw_protobuf's StreamDecoder to the size report. This does not include
+// the memory-buffer-only Decoder class, as the implementation is very
+// different.
+void BloatWithStreamDecoder();
+
+// Adds pw_protobuf's Decoder to the size report. This does not include the
+// StreamDecoder class, as the implementation is very different.
+void BloatWithDecoder();
+
+// Adds pw_protobuf's table-based Message encoder to the size report in addition
+// to the StreamEncoder/MemoryEncoder.
+void BloatWithTableEncoder();
+
+// Adds pw_protobuf's table-based Message decoder to the size report in addition
+// to the StreamDecoder.
+void BloatWithTableDecoder();
+
+} // namespace pw::protobuf_size_report
diff --git a/pw_protobuf/size_report/simple_codegen_comparison.cc b/pw_protobuf/size_report/simple_codegen_comparison.cc
new file mode 100644
index 000000000..bbad6909c
--- /dev/null
+++ b/pw_protobuf/size_report/simple_codegen_comparison.cc
@@ -0,0 +1,181 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "proto_bloat.h"
+#include "pw_bloat/bloat_this_binary.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_protobuf/encoder.h"
+#include "pw_protobuf/stream_decoder.h"
+#include "pw_protobuf_test_protos/size_report.pwpb.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+#ifndef _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN
+#define _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN 0
+#endif // _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN
+
+#ifndef _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT
+#define _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT 0
+#endif // _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT
+
+#ifndef _PW_PROTOBUF_SIZE_REPORT_MESSAGE
+#define _PW_PROTOBUF_SIZE_REPORT_MESSAGE 0
+#endif // _PW_PROTOBUF_SIZE_REPORT_MESSAGE
+
+namespace pw::protobuf_size_report {
+namespace {
+
+template <typename T>
+PW_NO_INLINE void ConsumeValue(T val) {
+ [[maybe_unused]] volatile T no_optimize = val;
+}
+
+#if _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN
+
+std::array<std::byte, pwpb::ItemInfo::kMaxEncodedSizeBytes> encode_buffer;
+pw::protobuf::MemoryEncoder generic_encoder(encode_buffer);
+
+PW_NO_INLINE void BasicEncode() {
+ pw::Status status;
+ status.Update(generic_encoder.WriteInt64(1, 0x5001DBADFEEDBEE5));
+ status.Update(generic_encoder.WriteInt32(2, 128));
+ status.Update(generic_encoder.WriteInt32(3, 2));
+ ConsumeValue(status);
+}
+
+std::array<std::byte, pwpb::ItemInfo::kMaxEncodedSizeBytes> decode_buffer;
+pw::protobuf::Decoder generic_decoder(decode_buffer);
+
+PW_NO_INLINE void BasicDecode() {
+ while (generic_decoder.Next().ok()) {
+ switch (generic_decoder.FieldNumber()) {
+ case static_cast<uint32_t>(pwpb::ItemInfo::Fields::kOffset): {
+ uint64_t value;
+ if (generic_decoder.ReadUint64(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case static_cast<uint32_t>(pwpb::ItemInfo::Fields::kSize): {
+ uint32_t value;
+ if (generic_decoder.ReadUint32(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case static_cast<uint32_t>(pwpb::ItemInfo::Fields::kAccessLevel): {
+ uint32_t value;
+
+ if (generic_decoder.ReadUint32(&value).ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ }
+ }
+}
+#endif // _PW_PROTOBUF_SIZE_REPORT_NO_CODEGEN
+
+#if _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT
+
+std::array<std::byte, pwpb::ItemInfo::kMaxEncodedSizeBytes> encode_buffer;
+pwpb::ItemInfo::MemoryEncoder encoder(encode_buffer);
+
+PW_NO_INLINE void BasicEncode() {
+ pw::Status status;
+ status.Update(encoder.WriteOffset(0x5001DBADFEEDBEE5));
+ status.Update(encoder.WriteSize(128));
+ status.Update(encoder.WriteAccessLevel(pwpb::ItemInfo::Access::WRITE));
+ ConsumeValue(status);
+}
+
+std::array<std::byte, pwpb::ItemInfo::kMaxEncodedSizeBytes> decode_buffer;
+pw::stream::MemoryReader reader(decode_buffer);
+pwpb::ItemInfo::StreamDecoder decoder(reader);
+
+PW_NO_INLINE void BasicDecode() {
+ while (decoder.Next().ok()) {
+ pw::Result<pwpb::ItemInfo::Fields> field = decoder.Field();
+ if (!field.ok()) {
+ ConsumeValue(field.status());
+ return;
+ }
+
+ switch (field.value()) {
+ case pwpb::ItemInfo::Fields::kOffset: {
+ pw::Result<uint64_t> value = decoder.ReadOffset();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case pwpb::ItemInfo::Fields::kSize: {
+ pw::Result<uint32_t> value = decoder.ReadSize();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ case pwpb::ItemInfo::Fields::kAccessLevel: {
+ pw::Result<pwpb::ItemInfo::Access> value = decoder.ReadAccessLevel();
+ if (value.ok()) {
+ ConsumeValue(value);
+ }
+ break;
+ }
+ }
+ }
+}
+#endif // _PW_PROTOBUF_SIZE_REPORT_WIRE_FORMAT
+
+#if _PW_PROTOBUF_SIZE_REPORT_MESSAGE
+
+pwpb::ItemInfo::Message message;
+
+std::array<std::byte, pwpb::ItemInfo::kMaxEncodedSizeBytes> encode_buffer;
+pwpb::ItemInfo::MemoryEncoder encoder(encode_buffer);
+
+PW_NO_INLINE void BasicEncode() {
+ message.offset = 0x5001DBADFEEDBEE5;
+ message.size = 128;
+ message.access_level = pwpb::ItemInfo::Access::WRITE;
+ ConsumeValue(encoder.Write(message));
+}
+
+std::array<std::byte, pwpb::ItemInfo::kMaxEncodedSizeBytes> decode_buffer;
+pw::stream::MemoryReader reader(decode_buffer);
+pwpb::ItemInfo::StreamDecoder decoder(reader);
+
+PW_NO_INLINE void BasicDecode() {
+ if (pw::Status status = decoder.Read(message); status.ok()) {
+ ConsumeValue(status);
+ }
+}
+#endif // _PW_PROTOBUF_SIZE_REPORT_MESSAGE
+
+} // namespace
+} // namespace pw::protobuf_size_report
+
+int main() {
+ pw::bloat::BloatThisBinary();
+ pw::protobuf_size_report::BloatWithBase();
+ pw::protobuf_size_report::BloatWithEncoder();
+ pw::protobuf_size_report::BloatWithStreamDecoder();
+ pw::protobuf_size_report::BloatWithDecoder();
+ pw::protobuf_size_report::BloatWithTableEncoder();
+ pw::protobuf_size_report::BloatWithTableDecoder();
+ pw::protobuf_size_report::BasicEncode();
+ pw::protobuf_size_report::BasicDecode();
+ return 0;
+}
diff --git a/pw_protobuf/stream_decoder.cc b/pw_protobuf/stream_decoder.cc
index e6a88aea0..c94b8e88e 100644
--- a/pw_protobuf/stream_decoder.cc
+++ b/pw_protobuf/stream_decoder.cc
@@ -15,20 +15,31 @@
#include "pw_protobuf/stream_decoder.h"
#include <algorithm>
-#include <bit>
#include <cstdint>
#include <cstring>
#include <limits>
+#include <optional>
+#include "pw_assert/assert.h"
#include "pw_assert/check.h"
+#include "pw_bytes/bit.h"
+#include "pw_containers/vector.h"
+#include "pw_function/function.h"
+#include "pw_protobuf/encoder.h"
+#include "pw_protobuf/internal/codegen.h"
+#include "pw_protobuf/wire_format.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_status/try.h"
+#include "pw_string/string.h"
#include "pw_varint/stream.h"
#include "pw_varint/varint.h"
namespace pw::protobuf {
+using internal::VarintType;
+
Status StreamDecoder::BytesReader::DoSeek(ptrdiff_t offset, Whence origin) {
PW_TRY(status_);
if (!decoder_.reader_.seekable()) {
@@ -163,7 +174,7 @@ Status StreamDecoder::Advance(size_t end_position) {
while (position_ < end_position) {
std::byte b;
- PW_TRY(reader_.Read(std::span(&b, 1)));
+ PW_TRY(reader_.Read(span(&b, 1)));
position_++;
}
return OkStatus();
@@ -202,7 +213,8 @@ Status StreamDecoder::ReadFieldKey() {
PW_DCHECK(field_consumed_);
uint64_t varint = 0;
- PW_TRY_ASSIGN(size_t bytes_read, varint::Read(reader_, &varint));
+ PW_TRY_ASSIGN(size_t bytes_read,
+ varint::Read(reader_, &varint, RemainingBytes()));
position_ += bytes_read;
if (!FieldKey::IsValidKey(varint)) {
@@ -214,8 +226,16 @@ Status StreamDecoder::ReadFieldKey() {
if (current_field_.wire_type() == WireType::kDelimited) {
// Read the length varint of length-delimited fields immediately to simplify
// later processing of the field.
- PW_TRY_ASSIGN(bytes_read, varint::Read(reader_, &varint));
- position_ += bytes_read;
+ StatusWithSize sws = varint::Read(reader_, &varint, RemainingBytes());
+ if (sws.IsOutOfRange()) {
+ // Out of range indicates the end of the stream. As a value is expected
+ // here, report it as a data loss and terminate the decode operation.
+ return Status::DataLoss();
+ }
+ if (!sws.ok()) {
+ return sws.status();
+ }
+ position_ += sws.size();
if (varint > std::numeric_limits<uint32_t>::max()) {
return Status::DataLoss();
@@ -246,7 +266,8 @@ Status StreamDecoder::SkipField() {
switch (current_field_.wire_type()) {
case WireType::kVarint: {
// Consume the varint field; nothing more to skip afterward.
- PW_TRY_ASSIGN(size_t bytes_read, varint::Read(reader_, &value));
+ PW_TRY_ASSIGN(size_t bytes_read,
+ varint::Read(reader_, &value, RemainingBytes()));
position_ += bytes_read;
break;
}
@@ -272,6 +293,11 @@ Status StreamDecoder::SkipField() {
return status_;
}
+ if (RemainingBytes() < bytes_to_skip) {
+ status_ = Status::DataLoss();
+ return status_;
+ }
+
PW_TRY(Advance(position_ + bytes_to_skip));
}
@@ -279,8 +305,8 @@ Status StreamDecoder::SkipField() {
return OkStatus();
}
-Status StreamDecoder::ReadVarintField(std::span<std::byte> out,
- VarintDecodeType decode_type) {
+Status StreamDecoder::ReadVarintField(span<std::byte> out,
+ VarintType decode_type) {
PW_CHECK(out.size() == sizeof(bool) || out.size() == sizeof(uint32_t) ||
out.size() == sizeof(uint64_t),
"Protobuf varints must only be used with bool, int32_t, uint32_t, "
@@ -293,10 +319,10 @@ Status StreamDecoder::ReadVarintField(std::span<std::byte> out,
return sws.status();
}
-StatusWithSize StreamDecoder::ReadOneVarint(std::span<std::byte> out,
- VarintDecodeType decode_type) {
+StatusWithSize StreamDecoder::ReadOneVarint(span<std::byte> out,
+ VarintType decode_type) {
uint64_t value;
- StatusWithSize sws = varint::Read(reader_, &value);
+ StatusWithSize sws = varint::Read(reader_, &value, RemainingBytes());
if (sws.IsOutOfRange()) {
// Out of range indicates the end of the stream. As a value is expected
// here, report it as a data loss and terminate the decode operation.
@@ -310,32 +336,32 @@ StatusWithSize StreamDecoder::ReadOneVarint(std::span<std::byte> out,
position_ += sws.size();
if (out.size() == sizeof(uint64_t)) {
- if (decode_type == VarintDecodeType::kUnsigned) {
+ if (decode_type == VarintType::kUnsigned) {
std::memcpy(out.data(), &value, out.size());
} else {
- const int64_t signed_value = decode_type == VarintDecodeType::kZigZag
+ const int64_t signed_value = decode_type == VarintType::kZigZag
? varint::ZigZagDecode(value)
: static_cast<int64_t>(value);
std::memcpy(out.data(), &signed_value, out.size());
}
} else if (out.size() == sizeof(uint32_t)) {
- if (decode_type == VarintDecodeType::kUnsigned) {
+ if (decode_type == VarintType::kUnsigned) {
if (value > std::numeric_limits<uint32_t>::max()) {
- return StatusWithSize(Status::OutOfRange(), sws.size());
+ return StatusWithSize(Status::FailedPrecondition(), sws.size());
}
std::memcpy(out.data(), &value, out.size());
} else {
- const int64_t signed_value = decode_type == VarintDecodeType::kZigZag
+ const int64_t signed_value = decode_type == VarintType::kZigZag
? varint::ZigZagDecode(value)
: static_cast<int64_t>(value);
if (signed_value > std::numeric_limits<int32_t>::max() ||
signed_value < std::numeric_limits<int32_t>::min()) {
- return StatusWithSize(Status::OutOfRange(), sws.size());
+ return StatusWithSize(Status::FailedPrecondition(), sws.size());
}
std::memcpy(out.data(), &signed_value, out.size());
}
} else if (out.size() == sizeof(bool)) {
- PW_CHECK(decode_type == VarintDecodeType::kUnsigned,
+ PW_CHECK(decode_type == VarintType::kUnsigned,
"Protobuf bool can never be signed");
std::memcpy(out.data(), &value, out.size());
}
@@ -343,7 +369,7 @@ StatusWithSize StreamDecoder::ReadOneVarint(std::span<std::byte> out,
return sws;
}
-Status StreamDecoder::ReadFixedField(std::span<std::byte> out) {
+Status StreamDecoder::ReadFixedField(span<std::byte> out) {
WireType expected_wire_type =
out.size() == sizeof(uint32_t) ? WireType::kFixed32 : WireType::kFixed64;
PW_TRY(CheckOkToRead(expected_wire_type));
@@ -353,18 +379,23 @@ Status StreamDecoder::ReadFixedField(std::span<std::byte> out) {
return status_;
}
+ if (RemainingBytes() < out.size()) {
+ status_ = Status::DataLoss();
+ return status_;
+ }
+
PW_TRY(reader_.Read(out));
position_ += out.size();
field_consumed_ = true;
- if (std::endian::native != std::endian::little) {
+ if (endian::native != endian::little) {
std::reverse(out.begin(), out.end());
}
return OkStatus();
}
-StatusWithSize StreamDecoder::ReadDelimitedField(std::span<std::byte> out) {
+StatusWithSize StreamDecoder::ReadDelimitedField(span<std::byte> out) {
if (Status status = CheckOkToRead(WireType::kDelimited); !status.ok()) {
return StatusWithSize(status, 0);
}
@@ -391,7 +422,7 @@ StatusWithSize StreamDecoder::ReadDelimitedField(std::span<std::byte> out) {
return StatusWithSize(result.value().size());
}
-StatusWithSize StreamDecoder::ReadPackedFixedField(std::span<std::byte> out,
+StatusWithSize StreamDecoder::ReadPackedFixedField(span<std::byte> out,
size_t elem_size) {
if (Status status = CheckOkToRead(WireType::kDelimited); !status.ok()) {
return StatusWithSize(status, 0);
@@ -418,7 +449,7 @@ StatusWithSize StreamDecoder::ReadPackedFixedField(std::span<std::byte> out,
field_consumed_ = true;
// Decode little-endian serialized packed fields.
- if (std::endian::native != std::endian::little) {
+ if (endian::native != endian::little) {
for (auto out_start = out.begin(); out_start != out.end();
out_start += elem_size) {
std::reverse(out_start, out_start + elem_size);
@@ -428,8 +459,9 @@ StatusWithSize StreamDecoder::ReadPackedFixedField(std::span<std::byte> out,
return StatusWithSize(result.value().size() / elem_size);
}
-StatusWithSize StreamDecoder::ReadPackedVarintField(
- std::span<std::byte> out, size_t elem_size, VarintDecodeType decode_type) {
+StatusWithSize StreamDecoder::ReadPackedVarintField(span<std::byte> out,
+ size_t elem_size,
+ VarintType decode_type) {
PW_CHECK(elem_size == sizeof(bool) || elem_size == sizeof(uint32_t) ||
elem_size == sizeof(uint64_t),
"Protobuf varints must only be used with bool, int32_t, uint32_t, "
@@ -483,4 +515,192 @@ Status StreamDecoder::CheckOkToRead(WireType type) {
return status_;
}
+Status StreamDecoder::Read(span<std::byte> message,
+ span<const internal::MessageField> table) {
+ PW_TRY(status_);
+
+ while (Next().ok()) {
+ // Find the field in the table,
+ // TODO(b/234876102): Finding the field can be made more efficient.
+ const auto field =
+ std::find(table.begin(), table.end(), current_field_.field_number());
+ if (field == table.end()) {
+ // If the field is not found, skip to the next one.
+ // TODO(b/234873295): Provide a way to allow the caller to inspect unknown
+ // fields, and serialize them back out later.
+ continue;
+ }
+
+ // Calculate the span of bytes corresponding to the structure field to
+ // output into.
+ const auto out =
+ message.subspan(field->field_offset(), field->field_size());
+ PW_CHECK(out.begin() >= message.begin() && out.end() <= message.end());
+
+ // If the field is using callbacks, interpret the output field accordingly
+ // and allow the caller to provide custom handling.
+ if (field->use_callback()) {
+ const Callback<StreamEncoder, StreamDecoder>* callback =
+ reinterpret_cast<const Callback<StreamEncoder, StreamDecoder>*>(
+ out.data());
+ PW_TRY(callback->Decode(*this));
+ continue;
+ }
+
+ // Switch on the expected wire type of the field, not the actual, to ensure
+ // the remote encoder doesn't influence our decoding unexpectedly.
+ switch (field->wire_type()) {
+ case WireType::kFixed64:
+ case WireType::kFixed32: {
+ // Fixed fields call ReadFixedField() for singular case, and either
+ // ReadPackedFixedField() or ReadRepeatedFixedField() for repeated
+ // fields.
+ PW_CHECK(field->elem_size() == (field->wire_type() == WireType::kFixed32
+ ? sizeof(uint32_t)
+ : sizeof(uint64_t)),
+ "Mismatched message field type and size");
+ if (field->is_fixed_size()) {
+ PW_CHECK(field->is_repeated(), "Non-repeated fixed size field");
+ PW_TRY(ReadPackedFixedField(out, field->elem_size()));
+ } else if (field->is_repeated()) {
+ // The struct member for this field is a vector of a type
+ // corresponding to the field element size. Cast to the correct
+ // vector type so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed).
+ if (field->elem_size() == sizeof(uint64_t)) {
+ auto* vector = reinterpret_cast<pw::Vector<uint64_t>*>(out.data());
+ PW_TRY(ReadRepeatedFixedField(*vector));
+ } else if (field->elem_size() == sizeof(uint32_t)) {
+ auto* vector = reinterpret_cast<pw::Vector<uint32_t>*>(out.data());
+ PW_TRY(ReadRepeatedFixedField(*vector));
+ }
+ } else if (field->is_optional()) {
+ // The struct member for this field is a std::optional of a type
+ // corresponding to the field element size. Cast to the correct
+ // optional type so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed), and assign through
+ // a temporary.
+ if (field->elem_size() == sizeof(uint64_t)) {
+ uint64_t value = 0;
+ PW_TRY(ReadFixedField(as_writable_bytes(span(&value, 1))));
+ auto* optional =
+ reinterpret_cast<std::optional<uint64_t>*>(out.data());
+ *optional = value;
+ } else if (field->elem_size() == sizeof(uint32_t)) {
+ uint32_t value = 0;
+ PW_TRY(ReadFixedField(as_writable_bytes(span(&value, 1))));
+ auto* optional =
+ reinterpret_cast<std::optional<uint32_t>*>(out.data());
+ *optional = value;
+ }
+ } else {
+ PW_CHECK(out.size() == field->elem_size(),
+ "Mismatched message field type and size");
+ PW_TRY(ReadFixedField(out));
+ }
+ break;
+ }
+ case WireType::kVarint: {
+ // Varint fields call ReadVarintField() for singular case, and either
+ // ReadPackedVarintField() or ReadRepeatedVarintField() for repeated
+ // fields.
+ PW_CHECK(field->elem_size() == sizeof(uint64_t) ||
+ field->elem_size() == sizeof(uint32_t) ||
+ field->elem_size() == sizeof(bool),
+ "Mismatched message field type and size");
+ if (field->is_fixed_size()) {
+ PW_CHECK(field->is_repeated(), "Non-repeated fixed size field");
+ PW_TRY(ReadPackedVarintField(
+ out, field->elem_size(), field->varint_type()));
+ } else if (field->is_repeated()) {
+ // The struct member for this field is a vector of a type
+ // corresponding to the field element size. Cast to the correct
+ // vector type so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed).
+ if (field->elem_size() == sizeof(uint64_t)) {
+ auto* vector = reinterpret_cast<pw::Vector<uint64_t>*>(out.data());
+ PW_TRY(ReadRepeatedVarintField(*vector, field->varint_type()));
+ } else if (field->elem_size() == sizeof(uint32_t)) {
+ auto* vector = reinterpret_cast<pw::Vector<uint32_t>*>(out.data());
+ PW_TRY(ReadRepeatedVarintField(*vector, field->varint_type()));
+ } else if (field->elem_size() == sizeof(bool)) {
+ auto* vector = reinterpret_cast<pw::Vector<bool>*>(out.data());
+ PW_TRY(ReadRepeatedVarintField(*vector, field->varint_type()));
+ }
+ } else if (field->is_optional()) {
+ // The struct member for this field is a std::optional of a type
+ // corresponding to the field element size. Cast to the correct
+ // optional type so we're not performing type aliasing (except for
+ // unsigned vs signed which is explicitly allowed), and assign through
+ // a temporary.
+ if (field->elem_size() == sizeof(uint64_t)) {
+ uint64_t value = 0;
+ PW_TRY(ReadVarintField(as_writable_bytes(span(&value, 1)),
+ field->varint_type()));
+ auto* optional =
+ reinterpret_cast<std::optional<uint64_t>*>(out.data());
+ *optional = value;
+ } else if (field->elem_size() == sizeof(uint32_t)) {
+ uint32_t value = 0;
+ PW_TRY(ReadVarintField(as_writable_bytes(span(&value, 1)),
+ field->varint_type()));
+ auto* optional =
+ reinterpret_cast<std::optional<uint32_t>*>(out.data());
+ *optional = value;
+ } else if (field->elem_size() == sizeof(bool)) {
+ bool value = false;
+ PW_TRY(ReadVarintField(as_writable_bytes(span(&value, 1)),
+ field->varint_type()));
+ auto* optional = reinterpret_cast<std::optional<bool>*>(out.data());
+ *optional = value;
+ }
+ } else {
+ PW_CHECK(out.size() == field->elem_size(),
+ "Mismatched message field type and size");
+ PW_TRY(ReadVarintField(out, field->varint_type()));
+ }
+ break;
+ }
+ case WireType::kDelimited: {
+ // Delimited fields are always a singular case because of the inability
+ // to cast to a generic vector with an element of a certain size (we
+ // always need a type).
+ PW_CHECK(!field->is_repeated(),
+ "Repeated delimited messages always require a callback");
+ if (field->nested_message_fields()) {
+ // Nested Message. Struct member is an embedded struct for the
+ // nested field. Obtain a nested decoder and recursively call Read()
+ // using the fields table pointer from this field.
+ auto nested_decoder = GetNestedDecoder();
+ PW_TRY(nested_decoder.Read(out, *field->nested_message_fields()));
+ } else if (field->is_fixed_size()) {
+ // Fixed-length bytes field. Struct member is a std::array<std::byte>.
+ // Call ReadDelimitedField() to populate it from the stream.
+ PW_CHECK(field->elem_size() == sizeof(std::byte),
+ "Mismatched message field type and size");
+ PW_TRY(ReadDelimitedField(out));
+ } else {
+ // bytes or string field with a maximum size. The struct member is
+ // pw::Vector<std::byte> for bytes or pw::InlineString<> for string.
+ PW_CHECK(field->elem_size() == sizeof(std::byte),
+ "Mismatched message field type and size");
+ if (field->is_string()) {
+ PW_TRY(ReadStringOrBytesField<pw::InlineString<>>(out.data()));
+ } else {
+ PW_TRY(ReadStringOrBytesField<pw::Vector<std::byte>>(out.data()));
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ // Reaching the end of the encoded protobuf is not an error.
+ if (status_ == Status::OutOfRange()) {
+ return OkStatus();
+ }
+
+ return status_;
+}
+
} // namespace pw::protobuf
diff --git a/pw_protobuf/stream_decoder_test.cc b/pw_protobuf/stream_decoder_test.cc
index 0cff426b8..d28141cf0 100644
--- a/pw_protobuf/stream_decoder_test.cc
+++ b/pw_protobuf/stream_decoder_test.cc
@@ -35,7 +35,7 @@ class NonSeekableMemoryReader : public stream::NonSeekableReader {
const std::byte* data() const { return reader_.data(); }
private:
- virtual StatusWithSize DoRead(ByteSpan destination) override {
+ StatusWithSize DoRead(ByteSpan destination) override {
const pw::Result<pw::ByteSpan> result = reader_.Read(destination);
if (!result.ok()) {
return StatusWithSize(result.status(), 0);
@@ -70,7 +70,7 @@ TEST(StreamDecoder, Decode) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -150,7 +150,7 @@ TEST(StreamDecoder, Decode_SkipsUnusedFields) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
// Don't process any fields except for the fourth. Next should still iterate
@@ -184,7 +184,7 @@ TEST(StreamDecoder, Decode_NonSeekable_SkipsUnusedFields) {
// clang-format on
// Test with a non-seekable memory reader
- stream::MemoryReader wrapped_reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader wrapped_reader(as_bytes(span(encoded_proto)));
NonSeekableMemoryReader reader(wrapped_reader);
StreamDecoder decoder(reader);
@@ -210,7 +210,7 @@ TEST(StreamDecoder, Decode_BadData) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -226,6 +226,75 @@ TEST(StreamDecoder, Decode_BadData) {
EXPECT_EQ(decoder.Next(), Status::DataLoss());
}
+TEST(StreamDecoder, Decode_MissingDelimitedLength) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // type=int32, k=1, v=42
+ 0x08, 0x2a,
+ // Submessage (bytes) key=8, length=... missing
+ 0x32,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+ Result<int32_t> int32 = decoder.ReadInt32();
+ ASSERT_EQ(int32.status(), OkStatus());
+ EXPECT_EQ(int32.value(), 42);
+
+ EXPECT_EQ(decoder.Next(), Status::DataLoss());
+}
+
+TEST(StreamDecoder, Decode_VarintTooBig) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // type=uint32, k=1, v=>uint32_t::max
+ 0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f,
+ // type=int32, k=2, v=>int32_t::max
+ 0x10, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f,
+ // type=int32, k=3, v<=int32_t::min
+ 0x18, 0x80, 0x80, 0x80, 0x80, 0x80, 0xff, 0xff, 0xff, 0xff, 0x01,
+ // type=sint32, k=4, v=>int32_t::max
+ 0x20, 0xfe, 0xff, 0xff, 0xff, 0xff, 0x0f,
+ // type=sint32, k=5, v<=int32_t::max
+ 0x28, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+ Result<uint32_t> uint32 = decoder.ReadUint32();
+ ASSERT_EQ(uint32.status(), Status::FailedPrecondition());
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 2u);
+ Result<int32_t> int32 = decoder.ReadInt32();
+ ASSERT_EQ(int32.status(), Status::FailedPrecondition());
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 3u);
+ int32 = decoder.ReadInt32();
+ ASSERT_EQ(int32.status(), Status::FailedPrecondition());
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 4u);
+ Result<int32_t> sint32 = decoder.ReadSint32();
+ ASSERT_EQ(sint32.status(), Status::FailedPrecondition());
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 5u);
+ sint32 = decoder.ReadSint32();
+ ASSERT_EQ(sint32.status(), Status::FailedPrecondition());
+
+ EXPECT_EQ(decoder.Next(), Status::OutOfRange());
+}
+
TEST(Decoder, Decode_SkipsBadFieldNumbers) {
// clang-format off
constexpr uint8_t encoded_proto[] = {
@@ -238,7 +307,7 @@ TEST(Decoder, Decode_SkipsBadFieldNumbers) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -273,7 +342,7 @@ TEST(StreamDecoder, Decode_Nested) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -330,7 +399,7 @@ TEST(StreamDecoder, Decode_Nested_SeeksToNextFieldOnDestruction) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -370,7 +439,7 @@ TEST(StreamDecoder,
// clang-format on
// Test with a non-seekable memory reader
- stream::MemoryReader wrapped_reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader wrapped_reader(as_bytes(span(encoded_proto)));
NonSeekableMemoryReader reader(wrapped_reader);
StreamDecoder decoder(reader);
@@ -406,7 +475,7 @@ TEST(StreamDecoder, Decode_Nested_LastField) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -438,7 +507,7 @@ TEST(StreamDecoder, Decode_Nested_MultiLevel) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -476,7 +545,7 @@ TEST(StreamDecoder, Decode_Nested_InvalidField) {
// Oops. No data!
};
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -489,6 +558,233 @@ TEST(StreamDecoder, Decode_Nested_InvalidField) {
EXPECT_EQ(decoder.Next(), Status::DataLoss());
}
+TEST(StreamDecoder, Decode_Nested_InvalidFieldKey) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // Submessage key=1, length=2
+ 0x0a, 0x02,
+ // type=invalid...
+ 0xff, 0xff,
+ // End submessage
+
+ // type=sint32, k=2, v=-13
+ 0x10, 0x19,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+
+ {
+ StreamDecoder nested = decoder.GetNestedDecoder();
+ EXPECT_EQ(nested.Next(), Status::DataLoss());
+
+ // Make sure that the nested decoder didn't run off the end of the
+ // submessage.
+ ASSERT_EQ(reader.Tell(), 4u);
+ }
+}
+
+TEST(StreamDecoder, Decode_Nested_MissingDelimitedLength) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // Submessage key=1, length=1
+ 0x0a, 0x01,
+ // Delimited field (bytes) key=1, length=missing...
+ 0x0a,
+ // End submessage
+
+ // type=sint32, k=2, v=-13
+ 0x10, 0x19,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+
+ {
+ StreamDecoder nested = decoder.GetNestedDecoder();
+ EXPECT_EQ(nested.Next(), Status::DataLoss());
+
+ // Make sure that the nested decoder didn't run off the end of the
+ // submessage.
+ ASSERT_EQ(reader.Tell(), 3u);
+ }
+}
+
+TEST(StreamDecoder, Decode_Nested_InvalidDelimitedLength) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // Submessage key=1, length=2
+ 0x0a, 0x02,
+ // Delimited field (bytes) key=1, length=invalid...
+ 0x0a, 0xff,
+ // End submessage
+
+ // type=sint32, k=2, v=-13
+ 0x10, 0x19,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+
+ {
+ StreamDecoder nested = decoder.GetNestedDecoder();
+ EXPECT_EQ(nested.Next(), Status::DataLoss());
+
+ // Make sure that the nested decoder didn't run off the end of the
+ // submessage.
+ ASSERT_EQ(reader.Tell(), 4u);
+ }
+}
+
+TEST(StreamDecoder, Decode_Nested_InvalidVarint) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // Submessage key=1, length=2
+ 0x0a, 0x02,
+ // type=uint32 key=1, value=invalid...
+ 0x08, 0xff,
+ // End submessage
+
+ // type=sint32, k=2, v=-13
+ 0x10, 0x19,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+
+ {
+ StreamDecoder nested = decoder.GetNestedDecoder();
+ EXPECT_EQ(nested.Next(), OkStatus());
+ ASSERT_EQ(*nested.FieldNumber(), 1u);
+
+ Result<uint32_t> uint32 = nested.ReadUint32();
+ EXPECT_EQ(uint32.status(), Status::DataLoss());
+
+ // Make sure that the nested decoder didn't run off the end of the
+ // submessage.
+ ASSERT_EQ(reader.Tell(), 4u);
+ }
+}
+
+TEST(StreamDecoder, Decode_Nested_SkipInvalidVarint) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // Submessage key=1, length=2
+ 0x0a, 0x02,
+ // type=uint32 key=1, value=invalid...
+ 0x08, 0xff,
+ // End submessage
+
+ // type=sint32, k=2, v=-13
+ 0x10, 0x19,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+
+ {
+ StreamDecoder nested = decoder.GetNestedDecoder();
+ EXPECT_EQ(nested.Next(), OkStatus());
+ ASSERT_EQ(*nested.FieldNumber(), 1u);
+
+ // Skip without reading.
+ EXPECT_EQ(nested.Next(), Status::DataLoss());
+
+ // Make sure that the nested decoder didn't run off the end of the
+ // submessage.
+ ASSERT_EQ(reader.Tell(), 4u);
+ }
+}
+
+TEST(StreamDecoder, Decode_Nested_TruncatedFixed) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // Submessage key=1, length=2
+ 0x0a, 0x03,
+ // type=fixed32 key=1, value=truncated...
+ 0x0d, 0x42, 0x00,
+ // End submessage
+
+ // type=sint32, k=2, v=-13
+ 0x10, 0x19,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+
+ {
+ StreamDecoder nested = decoder.GetNestedDecoder();
+ EXPECT_EQ(nested.Next(), OkStatus());
+ ASSERT_EQ(*nested.FieldNumber(), 1u);
+
+ Result<uint32_t> uint32 = nested.ReadFixed32();
+ EXPECT_EQ(uint32.status(), Status::DataLoss());
+
+ // Make sure that the nested decoder didn't run off the end of the
+ // submessage. Note that this will not read the data at all in this case.
+ ASSERT_EQ(reader.Tell(), 3u);
+ }
+}
+
+TEST(StreamDecoder, Decode_Nested_SkipTruncatedFixed) {
+ // clang-format off
+ constexpr uint8_t encoded_proto[] = {
+ // Submessage key=1, length=2
+ 0x0a, 0x03,
+ // type=fixed32 key=1, value=truncated...
+ 0x0d, 0x42, 0x00,
+ // End submessage
+
+ // type=sint32, k=2, v=-13
+ 0x10, 0x19,
+ };
+ // clang-format on
+
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
+ StreamDecoder decoder(reader);
+
+ EXPECT_EQ(decoder.Next(), OkStatus());
+ ASSERT_EQ(*decoder.FieldNumber(), 1u);
+
+ {
+ StreamDecoder nested = decoder.GetNestedDecoder();
+ EXPECT_EQ(nested.Next(), OkStatus());
+ ASSERT_EQ(*nested.FieldNumber(), 1u);
+
+ // Skip without reading.
+ EXPECT_EQ(nested.Next(), Status::DataLoss());
+
+ // Make sure that the nested decoder didn't run off the end of the
+ // submessage. Note that this will be unable to skip the field without
+ // exceeding the range of the nested decoder, so it won't move the cursor.
+ ASSERT_EQ(reader.Tell(), 3u);
+ }
+}
+
TEST(StreamDecoder, Decode_BytesReader) {
// clang-format off
constexpr uint8_t encoded_proto[] = {
@@ -502,7 +798,7 @@ TEST(StreamDecoder, Decode_BytesReader) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -537,7 +833,7 @@ TEST(StreamDecoder, Decode_BytesReader_Seek) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -594,7 +890,7 @@ TEST(StreamDecoder, Decode_BytesReader_Close) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -633,7 +929,7 @@ TEST(StreamDecoder, Decode_BytesReader_NonSeekable_Close) {
// clang-format on
// Test with a non-seekable memory reader
- stream::MemoryReader wrapped_reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader wrapped_reader(as_bytes(span(encoded_proto)));
NonSeekableMemoryReader reader(wrapped_reader);
StreamDecoder decoder(reader);
@@ -664,7 +960,7 @@ TEST(StreamDecoder, Decode_BytesReader_InvalidField) {
// Oops. No data!
};
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -697,7 +993,7 @@ TEST(StreamDecoder, GetLengthDelimitedPayloadBounds) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
ASSERT_EQ(OkStatus(), decoder.Next());
@@ -722,7 +1018,7 @@ TEST(StreamDecoder, ReadDelimitedField_DoesntOverConsume) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
ASSERT_EQ(OkStatus(), decoder.Next());
@@ -751,7 +1047,7 @@ TEST(StreamDecoder, Decode_WithLength) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader, /*length=*/2u);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -774,7 +1070,7 @@ TEST(StreamDecoder, Decode_WithLength_SkipsToEnd) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
{
StreamDecoder decoder(reader, /*length=*/13u);
@@ -803,7 +1099,7 @@ TEST(StreamDecoder, RepeatedField) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -855,7 +1151,7 @@ TEST(StreamDecoder, RepeatedFieldVector) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
pw::Vector<uint32_t, 8> uint32{};
@@ -914,7 +1210,7 @@ TEST(StreamDecoder, RepeatedFieldVectorFull) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
pw::Vector<uint32_t, 2> uint32{};
@@ -953,7 +1249,7 @@ TEST(StreamDecoder, PackedVarint) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -983,7 +1279,7 @@ TEST(StreamDecoder, PackedVarintInsufficientSpace) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -1011,7 +1307,7 @@ TEST(StreamDecoder, PackedVarintVector) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -1041,7 +1337,7 @@ TEST(StreamDecoder, PackedVarintVectorFull) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -1071,7 +1367,7 @@ TEST(StreamDecoder, PackedZigZag) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -1105,7 +1401,7 @@ TEST(StreamDecoder, PackedZigZagVector) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -1156,7 +1452,7 @@ TEST(StreamDecoder, PackedFixed) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -1237,7 +1533,7 @@ TEST(StreamDecoder, PackedFixedInsufficientSpace) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -1260,7 +1556,7 @@ TEST(StreamDecoder, PackedFixedVector) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
@@ -1292,7 +1588,7 @@ TEST(StreamDecoder, PackedFixedVectorFull) {
};
// clang-format on
- stream::MemoryReader reader(std::as_bytes(std::span(encoded_proto)));
+ stream::MemoryReader reader(as_bytes(span(encoded_proto)));
StreamDecoder decoder(reader);
EXPECT_EQ(decoder.Next(), OkStatus());
diff --git a/pw_protobuf_compiler/BUILD.bazel b/pw_protobuf_compiler/BUILD.bazel
index 045285e84..25728e84c 100644
--- a/pw_protobuf_compiler/BUILD.bazel
+++ b/pw_protobuf_compiler/BUILD.bazel
@@ -11,19 +11,25 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
+load("@rules_proto//proto:defs.bzl", "proto_library")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
-load("@rules_proto_grpc//js:defs.bzl", "js_proto_library")
+load("//pw_build:pigweed.bzl", "pw_cc_test")
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
-# TODO(frolv): Figure out how to support nanopb codegen in Bazel.
+# TODO(frolv): Figure out how to support nanopb and pwpb codegen in Bazel.
filegroup(
name = "nanopb_test",
srcs = ["nanopb_test.cc"],
)
+filegroup(
+ name = "pwpb_test",
+ srcs = ["pwpb_test.cc"],
+)
+
py_proto_library(
name = "pw_protobuf_compiler_protos",
srcs = [
@@ -40,7 +46,15 @@ proto_library(
],
)
-js_proto_library(
- name = "test_protos_tspb",
- protos = ["//pw_protobuf_compiler:test_protos"],
+pw_cc_test(
+ name = "nested_packages_test",
+ srcs = ["nested_packages_test.cc"],
+ deps = [
+ "//pw_protobuf_compiler/pw_nested_packages:aggregate_pw_proto.pwpb",
+ "//pw_protobuf_compiler/pw_nested_packages:aggregate_wrapper_pw_proto.pwpb",
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/id:id_pw_proto.pwpb",
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/thing:thing_pw_proto.pwpb",
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/thing:type_of_thing_pw_proto.pwpb",
+ "//pw_unit_test",
+ ],
)
diff --git a/pw_protobuf_compiler/BUILD.gn b/pw_protobuf_compiler/BUILD.gn
index 62f2819cc..22f097c84 100644
--- a/pw_protobuf_compiler/BUILD.gn
+++ b/pw_protobuf_compiler/BUILD.gn
@@ -25,7 +25,11 @@ pw_doc_group("docs") {
}
pw_test_group("tests") {
- tests = [ ":nanopb_test" ]
+ tests = [
+ ":nanopb_test",
+ ":pwpb_test",
+ ":nested_packages_test",
+ ]
}
pw_test("nanopb_test") {
@@ -42,6 +46,17 @@ pw_proto_library("nanopb_test_protos") {
}
}
+pw_test("pwpb_test") {
+ deps = [ ":pwpb_test_protos.pwpb" ]
+ sources = [ "pwpb_test.cc" ]
+}
+
+pw_proto_library("pwpb_test_protos") {
+ sources = [ "pw_protobuf_compiler_pwpb_protos/pwpb_test.proto" ]
+ inputs = [ "pw_protobuf_compiler_pwpb_protos/pwpb_test.options" ]
+ deps = [ "$dir_pw_protobuf:common_protos" ]
+}
+
pw_proto_library("test_protos") {
sources = [
"pw_protobuf_compiler_protos/nested/more_nesting/test.proto",
@@ -49,12 +64,7 @@ pw_proto_library("test_protos") {
]
}
-# PyPI Requirements needed to install Python protobuf packages.
-pw_python_requirements("protobuf_requirements") {
- requirements = [
- # NOTE: mypy needs to stay in sync with mypy-protobuf
- # Currently using mypy 0.910 and mypy-protobuf 2.9
- # This must also be specified in //pw_protobuf_compiler/py/setup.cfg
- "mypy-protobuf==2.9",
- ]
+pw_test("nested_packages_test") {
+ deps = [ "pw_nested_packages:aggregate_wrapper.pwpb" ]
+ sources = [ "nested_packages_test.cc" ]
}
diff --git a/pw_protobuf_compiler/CMakeLists.txt b/pw_protobuf_compiler/CMakeLists.txt
index 4b5a3b62a..eb84e94cc 100644
--- a/pw_protobuf_compiler/CMakeLists.txt
+++ b/pw_protobuf_compiler/CMakeLists.txt
@@ -15,6 +15,22 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
+pw_proto_library(pw_protobuf_compiler.pwpb_test_protos
+ SOURCES
+ pw_protobuf_compiler_pwpb_protos/pwpb_test.proto
+ INPUTS
+ pw_protobuf_compiler_pwpb_protos/pwpb_test.options
+)
+
+pw_add_test(pw_protobuf_compiler.pwpb_test
+ SOURCES
+ pwpb_test.cc
+ PRIVATE_DEPS
+ pw_protobuf_compiler.pwpb_test_protos.pwpb
+ GROUPS
+ pw_protobuf_compiler
+)
+
if(NOT "${dir_pw_third_party_nanopb}" STREQUAL "")
pw_proto_library(pw_protobuf_compiler.nanopb_test_protos
SOURCES
@@ -26,7 +42,7 @@ if(NOT "${dir_pw_third_party_nanopb}" STREQUAL "")
pw_add_test(pw_protobuf_compiler.nanopb_test
SOURCES
nanopb_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf_compiler.nanopb_test_protos.nanopb
GROUPS
pw_protobuf_compiler
diff --git a/pw_protobuf_compiler/docs.rst b/pw_protobuf_compiler/docs.rst
index 31160e865..7644625c9 100644
--- a/pw_protobuf_compiler/docs.rst
+++ b/pw_protobuf_compiler/docs.rst
@@ -1,11 +1,15 @@
.. _module-pw_protobuf_compiler:
---------------------
+====================
pw_protobuf_compiler
---------------------
+====================
The Protobuf compiler module provides build system integration and wrapper
scripts for generating source code for Protobuf definitions.
+--------------------
+Protobuf compilation
+--------------------
+
Generator support
=================
Protobuf code generation is currently supported for the following generators:
@@ -15,6 +19,9 @@ Protobuf code generation is currently supported for the following generators:
+-------------+----------------+-----------------------------------------------+
| pw_protobuf | ``pwpb`` | Compiles using ``pw_protobuf``. |
+-------------+----------------+-----------------------------------------------+
+| pw_protobuf | ``pwpb_rpc`` | Compiles pw_rpc service and client code for |
+| RPC | | ``pw_protobuf``. |
++-------------+----------------+-----------------------------------------------+
| Nanopb | ``nanopb`` | Compiles using Nanopb. The build argument |
| | | ``dir_pw_third_party_nanopb`` must be set to |
| | | point to a local nanopb installation. |
@@ -81,6 +88,7 @@ GN supports the following compiled proto libraries via the specified
sub-targets generated by a ``pw_proto_library``.
* ``${target_name}.pwpb`` - Generated C++ pw_protobuf code
+* ``${target_name}.pwpb_rpc`` - Generated C++ pw_protobuf pw_rpc code
* ``${target_name}.nanopb`` - Generated C++ nanopb code (requires Nanopb)
* ``${target_name}.nanopb_rpc`` - Generated C++ Nanopb pw_rpc code (requires
Nanopb)
@@ -93,6 +101,7 @@ sub-targets generated by a ``pw_proto_library``.
* ``sources``: List of input .proto files.
* ``deps``: List of other pw_proto_library dependencies.
+* ``other_deps``: List of other non-proto dependencies.
* ``inputs``: Other files on which the protos depend (e.g. nanopb ``.options``
files).
* ``prefix``: A prefix to add to the source protos prior to compilation. For
@@ -102,6 +111,14 @@ sub-targets generated by a ``pw_proto_library``.
input files must be nested under this path.
* ``python_package``: Label of Python package to which to add the proto modules.
The .python subtarget will redirect to this package.
+* ``enabled_targets``: List of sub-targets to enable (see Supported Codegen),
+ e.g. ``["pwpb", "raw_rpc"]``. By default, all sub-targets are enabled. The
+ enabled sub-targets are built only as requested by the build system, but it
+ may be necessary to explicitly disable an unused sub-target if it conflicts
+ with another target in the same package. (For example, ``nanopb`` codegen can
+ conflict with the default C++ codegen provided by ``protoc``.)
+ TODO(b/235132083): Remove this argument once we've removed the file-name
+ conflict between nanopb and protoc code generators.
**Example**
@@ -155,9 +172,7 @@ From Python, ``baz.proto`` is imported as follows:
Proto file structure
--------------------
Protobuf source files must be nested under another directory when they are
-compiled. This ensures that they can be packaged properly in Python. The first
-directory is used as the Python package name, so must be unique across the
-build. The ``prefix`` option may be used to set this directory.
+compiled. This ensures that they can be packaged properly in Python.
Using ``prefix`` and ``strip_prefix`` together allows remapping proto files to
a completely different path. This can be useful when working with protos defined
@@ -330,17 +345,24 @@ CMake supports the following compiled proto libraries via the specified
sub-targets generated by a ``pw_proto_library``.
* ``${NAME}.pwpb`` - Generated C++ pw_protobuf code
+* ``${NAME}.pwpb_rpc`` - Generated C++ pw_protobuf pw_rpc code
* ``${NAME}.nanopb`` - Generated C++ nanopb code (requires Nanopb)
* ``${NAME}.nanopb_rpc`` - Generated C++ Nanopb pw_rpc code (requires Nanopb)
* ``${NAME}.raw_rpc`` - Generated C++ raw pw_rpc code (no protobuf library)
Bazel
=====
-Bazel provides a ``pw_proto_library`` rule with similar features as the
-GN template. The Bazel build only supports building firmware code, so
-``pw_proto_library`` does not generate a Python package. The Bazel rules differ
-slightly compared to the GN build to be more in line with what would be
-considered idiomatic in Bazel.
+In Bazel we provide a set rules with similar features to the GN templates:
+
+* ``pwpb_proto_library`` - Generated C++ pw_protobuf code
+* ``pwpb_rpc_proto_library`` - Generated C++ pw_protobuf pw_rpc code
+* ``raw_rpc_proto_library`` - Generated C++ raw pw_rpc code (no protobuf library)
+* ``nanopb_proto_library`` - Generated C++ nanopb code
+* ``nanopb_rpc_proto_library`` - Generated C++ Nanopb pw_rpc code
+
+These rules build the corresponding firmware code; there are no rules for
+generating Python libraries. The Bazel rules differ slightly compared to the GN
+build to be more in line with what would be considered idiomatic in Bazel.
To use Pigweeds Protobuf rules you must first pull in the required dependencies
into your Bazel WORKSPACE file. e.g.
@@ -358,7 +380,12 @@ compile them. e.g.
# BUILD ...
load("@rules_proto//proto:defs.bzl", "proto_library")
- load("@pigweed//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+ load("@pigweed//pw_protobuf_compiler:proto.bzl",
+ "nanopb_proto_library",
+ "nanopb_rpc_proto_library",
+ "pwpb_proto_library",
+ "raw_rpc_proto_library",
+ )
# Manages proto sources and dependencies.
proto_library(
@@ -369,9 +396,25 @@ compile them. e.g.
]
)
- # Compiles dependant protos to C++.
- pw_proto_library(
- name = "my_cc_proto",
+ # Compiles dependent protos to C++.
+ pwpb_proto_library(
+ name = "my_proto_pwpb",
+ deps = [":my_proto"],
+ )
+
+ nanopb_proto_library(
+ name = "my_proto_nanopb",
+ deps = [":my_proto"],
+ )
+
+ raw_rpc_proto_library(
+ name = "my_proto_raw_rpc",
+ deps = [":my_proto"],
+ )
+
+ nanopb_rpc_proto_library(
+ name = "my_proto_nanopb_rpc",
+ nanopb_proto_library_deps = [":my_proto_nanopb"],
deps = [":my_proto"],
)
@@ -379,14 +422,14 @@ compile them. e.g.
pw_cc_library(
name = "my_proto_only_lib",
srcs = ["my/proto_only.cc"],
- deps = [":my_cc_proto.pwpb"],
+ deps = [":my_proto_pwpb"],
)
# Library that depends on only Nanopb generated proto targets.
pw_cc_library(
name = "my_nanopb_only_lib",
srcs = ["my/nanopb_only.cc"],
- deps = [":my_cc_proto.nanopb"],
+ deps = [":my_proto_nanopb"],
)
# Library that depends on pw_protobuf and pw_rpc/raw.
@@ -394,33 +437,19 @@ compile them. e.g.
name = "my_raw_rpc_lib",
srcs = ["my/raw_rpc.cc"],
deps = [
- ":my_cc_proto.pwpb",
- ":my_cc_proto.raw_rpc",
+ ":my_proto_pwpb",
+ ":my_proto_raw_rpc",
],
)
pw_cc_library(
name = "my_nanopb_rpc_lib",
srcs = ["my/proto_only.cc"],
deps = [
- ":my_cc_proto.nanopb_rpc",
+ ":my_proto_nanopb_rpc",
],
)
- # Library that depends on generated proto targets. Prefer to depend only on
- # those generated targets ("my_lib.pwpb", "my_lib.nanopb") that are actually
- # required. Note that the .nanopb target may not compile for some proto
- # messages, e.g. self-referring messages;
- # see https://github.com/nanopb/nanopb/issues/433.
- pw_cc_library(
- name = "my_lib",
- srcs = ["my/lib.cc"],
- # This target depends on all generated proto targets
- # e.g. name.{pwpb, nanopb, raw_rpc, nanopb_rpc}
- deps = [":my_cc_proto"],
- )
-
-
From ``my/lib.cc`` you can now include the generated headers.
e.g.
@@ -433,15 +462,37 @@ e.g.
#include "my_protos/bar.nanopb_rpc.pb.h"
-
-**Supported Codegen**
-
-Bazel supports the following compiled proto libraries via the specified
-sub-targets generated by a ``pw_proto_library``.
-
-* ``${NAME}.pwpb`` - Generated C++ pw_protobuf code
-* ``${NAME}.nanopb`` - Generated C++ nanopb code
-* ``${NAME}.raw_rpc`` - Generated C++ raw pw_rpc code (no protobuf library)
-* ``${NAME}.nanopb_rpc`` - Generated C++ Nanopb pw_rpc code
-
-
+Why isn't there one rule to generate all the code?
+--------------------------------------------------
+There is! Like in GN, it's called ``pw_proto_library``, and has subtargets
+corresponding to the different codegen flavors. However, *we recommend against
+using this target*. It is deprecated, and will be removed in the future.
+
+The ``pw_proto_library`` target has a number of disadvantages:
+
+#. As a general bazel style rule, macros should produce exactly one target for
+ external use, named according to the invocation's name argument. ``BUILD``
+ files are easier to follow when the name specified in the macro call
+ actually matches the name of the generated target. This is not possible if a
+ single macro is generating multiple targets, as ``pw_proto_library`` does.
+#. If you depend directly on the ``pw_proto_library``, rather than the
+ appropriate subtargets, you will build code you don't actually use. You may
+ even fetch dependencies you don't need, like nanopb.
+#. The subtargets you don't depend on are still added to your BUILD files by
+ the ``pw_proto_library`` macro, and bazel will attempt to build them when
+ you run ``bazel build //...``. This may cause build breakages, and has
+ forced us to implement `awkward workarounds
+ <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/96980>`_.
+
+----------------------
+Python proto libraries
+----------------------
+``pw_protobuf_compiler`` includes utilties for working with protocol buffers
+in Python. The tools facilitate using protos from their package names
+(``my.pkg.Message()``) rather than their generated module names
+(``proto_source_file_pb2.Message()``).
+
+``python_protos`` module
+========================
+.. automodule:: pw_protobuf_compiler.python_protos
+ :members: proto_repr, Library
diff --git a/pw_protobuf_compiler/nested_packages_test.cc b/pw_protobuf_compiler/nested_packages_test.cc
new file mode 100644
index 000000000..1b1dff592
--- /dev/null
+++ b/pw_protobuf_compiler/nested_packages_test.cc
@@ -0,0 +1,72 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "proto_root/aggregate.pwpb.h"
+#include "proto_root/aggregate_wrapper.pwpb.h"
+#include "proto_root/data_type/id/id.pwpb.h"
+#include "proto_root/data_type/thing/thing.pwpb.h"
+#include "proto_root/data_type/thing/type_of_thing.pwpb.h"
+
+namespace pw::protobuf_compiler {
+namespace {
+
+TEST(NestedPackages, CompilesProtobufs) {
+ using Aggregate = proto_root::pwpb::Aggregate::Message;
+ using AggregateWrapper = proto_root::pwpb::AggregateWrapper::Message;
+ using Id = proto_root::data_type::id::pwpb::Id::Message;
+ using Thing = proto_root::data_type::thing::pwpb::Thing::Message;
+ using proto_root::data_type::thing::pwpb::TypeOfThing;
+
+ AggregateWrapper wrapper = {
+ Id{0u, {1, 2}},
+ TypeOfThing::kOther,
+
+ Aggregate{
+ Id{1u, {2, 3}},
+ TypeOfThing::kOther,
+
+ Thing{Id{2u, {3, 4}}, TypeOfThing::kOrganism},
+ Thing{Id{3u, {4, 5}}, TypeOfThing::kSubstance},
+ Thing{Id{4u, {5, 6}}, TypeOfThing::kObject},
+ }};
+
+ EXPECT_EQ(wrapper.id.id, 0u);
+ EXPECT_EQ(wrapper.id.impl.foo, 1);
+ EXPECT_EQ(wrapper.id.impl.bar, 2);
+ EXPECT_EQ(wrapper.type, TypeOfThing::kOther);
+
+ EXPECT_EQ(wrapper.aggregate.id.id, 1u);
+ EXPECT_EQ(wrapper.aggregate.id.impl.foo, 2);
+ EXPECT_EQ(wrapper.aggregate.id.impl.bar, 3);
+ EXPECT_EQ(wrapper.aggregate.type, TypeOfThing::kOther);
+
+ EXPECT_EQ(wrapper.aggregate.alice.id.id, 2u);
+ EXPECT_EQ(wrapper.aggregate.alice.id.impl.foo, 3);
+ EXPECT_EQ(wrapper.aggregate.alice.id.impl.bar, 4);
+ EXPECT_EQ(wrapper.aggregate.alice.type, TypeOfThing::kOrganism);
+
+ EXPECT_EQ(wrapper.aggregate.neodymium.id.id, 3u);
+ EXPECT_EQ(wrapper.aggregate.neodymium.id.impl.foo, 4);
+ EXPECT_EQ(wrapper.aggregate.neodymium.id.impl.bar, 5);
+ EXPECT_EQ(wrapper.aggregate.neodymium.type, TypeOfThing::kSubstance);
+
+ EXPECT_EQ(wrapper.aggregate.mountain.id.id, 4u);
+ EXPECT_EQ(wrapper.aggregate.mountain.id.impl.foo, 5);
+ EXPECT_EQ(wrapper.aggregate.mountain.id.impl.bar, 6);
+ EXPECT_EQ(wrapper.aggregate.mountain.type, TypeOfThing::kObject);
+}
+
+} // namespace
+} // namespace pw::protobuf_compiler
diff --git a/pw_protobuf_compiler/proto.bzl b/pw_protobuf_compiler/proto.bzl
index effe3de70..4b07323ca 100644
--- a/pw_protobuf_compiler/proto.bzl
+++ b/pw_protobuf_compiler/proto.bzl
@@ -15,8 +15,18 @@
load(
"//third_party/rules_proto_grpc:internal_proto.bzl",
+ _nanopb_proto_library = "nanopb_proto_library",
+ _nanopb_rpc_proto_library = "nanopb_rpc_proto_library",
_pw_proto_library = "pw_proto_library",
+ _pwpb_proto_library = "pwpb_proto_library",
+ _pwpb_rpc_proto_library = "pwpb_rpc_proto_library",
+ _raw_rpc_proto_library = "raw_rpc_proto_library",
)
# Export internal symbols.
+nanopb_proto_library = _nanopb_proto_library
+nanopb_rpc_proto_library = _nanopb_rpc_proto_library
pw_proto_library = _pw_proto_library
+pwpb_proto_library = _pwpb_proto_library
+pwpb_rpc_proto_library = _pwpb_rpc_proto_library
+raw_rpc_proto_library = _raw_rpc_proto_library
diff --git a/pw_protobuf_compiler/proto.cmake b/pw_protobuf_compiler/proto.cmake
index dbe4fe2b6..8ed7519aa 100644
--- a/pw_protobuf_compiler/proto.cmake
+++ b/pw_protobuf_compiler/proto.cmake
@@ -13,6 +13,8 @@
# the License.
include_guard(GLOBAL)
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
# Declares a protocol buffers library. This function creates a library for each
# supported protocol buffer implementation:
#
@@ -21,9 +23,9 @@ include_guard(GLOBAL)
#
# This function also creates libraries for generating pw_rpc code:
#
+# ${NAME}.pwpb_rpc - generates pw_protobuf pw_rpc code
# ${NAME}.nanopb_rpc - generates Nanopb pw_rpc code
# ${NAME}.raw_rpc - generates raw pw_rpc (no protobuf library) code
-# ${NAME}.pwpb_rpc - (Not implemented) generates pw_protobuf pw_rpc code
#
# Args:
#
@@ -36,14 +38,19 @@ include_guard(GLOBAL)
# .options files)
#
function(pw_proto_library NAME)
- cmake_parse_arguments(PARSE_ARGV 1 arg "" "STRIP_PREFIX;PREFIX"
- "SOURCES;INPUTS;DEPS")
-
- if("${arg_SOURCES}" STREQUAL "")
- message(FATAL_ERROR
- "pw_proto_library requires at least one .proto file in SOURCES. No "
- "SOURCES were listed for ${NAME}.")
- endif()
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ STRIP_PREFIX
+ PREFIX
+ MULTI_VALUE_ARGS
+ SOURCES
+ INPUTS
+ DEPS
+ REQUIRED_ARGS
+ SOURCES
+ )
set(out_dir "${CMAKE_CURRENT_BINARY_DIR}/${NAME}")
@@ -52,9 +59,12 @@ function(pw_proto_library NAME)
set(include_deps "${arg_DEPS}")
list(TRANSFORM include_deps APPEND ._includes)
- add_library("${NAME}._includes" INTERFACE)
- target_include_directories("${NAME}._includes" INTERFACE "${out_dir}/sources")
- target_link_libraries("${NAME}._includes" INTERFACE ${include_deps})
+ pw_add_library_generic("${NAME}._includes" INTERFACE
+ PUBLIC_INCLUDES
+ "${out_dir}/sources"
+ PUBLIC_DEPS
+ ${include_deps}
+ )
# Generate a file with all include paths needed by protoc. Use the include
# directory paths and replace ; with \n.
@@ -76,9 +86,11 @@ function(pw_proto_library NAME)
# Mirror the sources to the output directory with the specified prefix.
_pw_rebase_paths(
- sources "${out_dir}/sources/${arg_PREFIX}" "${arg_STRIP_PREFIX}" "${arg_SOURCES}" "")
+ sources "${out_dir}/sources/${arg_PREFIX}" "${arg_STRIP_PREFIX}"
+ "${arg_SOURCES}" "")
_pw_rebase_paths(
- inputs "${out_dir}/sources/${arg_PREFIX}" "${arg_STRIP_PREFIX}" "${arg_INPUTS}" "")
+ inputs "${out_dir}/sources/${arg_PREFIX}" "${arg_STRIP_PREFIX}"
+ "${arg_INPUTS}" "")
add_custom_command(
COMMAND
@@ -103,14 +115,66 @@ function(pw_proto_library NAME)
endif()
# Create a protobuf target for each supported protobuf library.
- _pw_pwpb_library(
- "${NAME}" "${sources}" "${inputs}" "${arg_DEPS}" "${include_file}" "${out_dir}")
- _pw_raw_rpc_library(
- "${NAME}" "${sources}" "${inputs}" "${arg_DEPS}" "${include_file}" "${out_dir}")
- _pw_nanopb_library(
- "${NAME}" "${sources}" "${inputs}" "${arg_DEPS}" "${include_file}" "${out_dir}")
- _pw_nanopb_rpc_library(
- "${NAME}" "${sources}" "${inputs}" "${arg_DEPS}" "${include_file}" "${out_dir}")
+ _pw_pwpb_library("${NAME}"
+ SOURCES
+ ${sources}
+ INPUTS
+ ${inputs}
+ DEPS
+ ${arg_DEPS}
+ INCLUDE_FILE
+ "${include_file}"
+ OUT_DIR
+ "${out_dir}"
+ )
+ _pw_pwpb_rpc_library("${NAME}"
+ SOURCES
+ ${sources}
+ INPUTS
+ ${inputs}
+ DEPS
+ ${arg_DEPS}
+ INCLUDE_FILE
+ "${include_file}"
+ OUT_DIR
+ "${out_dir}"
+ )
+ _pw_raw_rpc_library("${NAME}"
+ SOURCES
+ ${sources}
+ INPUTS
+ ${inputs}
+ DEPS
+ ${arg_DEPS}
+ INCLUDE_FILE
+ "${include_file}"
+ OUT_DIR
+ "${out_dir}"
+ )
+ _pw_nanopb_library("${NAME}"
+ SOURCES
+ ${sources}
+ INPUTS
+ ${inputs}
+ DEPS
+ ${arg_DEPS}
+ INCLUDE_FILE
+ "${include_file}"
+ OUT_DIR
+ "${out_dir}"
+ )
+ _pw_nanopb_rpc_library("${NAME}"
+ SOURCES
+ ${sources}
+ INPUTS
+ ${inputs}
+ DEPS
+ ${arg_DEPS}
+ INCLUDE_FILE
+ "${include_file}"
+ OUT_DIR
+ "${out_dir}"
+ )
endfunction(pw_proto_library)
function(_pw_rebase_paths VAR OUT_DIR ROOT FILES EXTENSIONS)
@@ -133,19 +197,35 @@ function(_pw_rebase_paths VAR OUT_DIR ROOT FILES EXTENSIONS)
endfunction(_pw_rebase_paths)
# Internal function that invokes protoc through generate_protos.py.
-function(_pw_generate_protos
- TARGET LANGUAGE PLUGIN OUTPUT_EXTS INCLUDE_FILE OUT_DIR SOURCES INPUTS DEPS)
+function(_pw_generate_protos TARGET LANGUAGE)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 2
+ ONE_VALUE_ARGS
+ PLUGIN
+ INCLUDE_FILE
+ OUT_DIR
+ MULTI_VALUE_ARGS
+ OUTPUT_EXTS
+ SOURCES
+ INPUTS
+ DEPENDS
+ )
+
# Determine the names of the compiled output files.
_pw_rebase_paths(outputs
- "${OUT_DIR}/${LANGUAGE}" "${OUT_DIR}/sources" "${SOURCES}" "${OUTPUT_EXTS}")
+ "${arg_OUT_DIR}/${LANGUAGE}" "${arg_OUT_DIR}/sources" "${arg_SOURCES}"
+ "${arg_OUTPUT_EXTS}")
# Export the output files to the caller's scope so it can use them if needed.
set(generated_outputs "${outputs}" PARENT_SCOPE)
if("${CMAKE_HOST_SYSTEM_NAME}" STREQUAL "Windows")
- get_filename_component(dir "${source_file}" DIRECTORY)
- get_filename_component(name "${source_file}" NAME_WE)
- set(PLUGIN "${dir}/${name}.bat")
+ foreach(source_file IN LISTS SOURCES)
+ get_filename_component(dir "${source_file}" DIRECTORY)
+ get_filename_component(name "${source_file}" NAME_WE)
+ set(arg_PLUGIN "${dir}/${name}.bat")
+ endforeach()
endif()
set(script "$ENV{PW_ROOT}/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py")
@@ -154,16 +234,16 @@ function(_pw_generate_protos
python3
"${script}"
--language "${LANGUAGE}"
- --plugin-path "${PLUGIN}"
- --include-file "${INCLUDE_FILE}"
- --compile-dir "${OUT_DIR}/sources"
- --out-dir "${OUT_DIR}/${LANGUAGE}"
- --sources ${SOURCES}
+ --plugin-path "${arg_PLUGIN}"
+ --include-file "${arg_INCLUDE_FILE}"
+ --compile-dir "${arg_OUT_DIR}/sources"
+ --out-dir "${arg_OUT_DIR}/${LANGUAGE}"
+ --sources ${arg_SOURCES}
DEPENDS
${script}
- ${SOURCES}
- ${INPUTS}
- ${DEPS}
+ ${arg_SOURCES}
+ ${arg_INPUTS}
+ ${arg_DEPENDS}
OUTPUT
${outputs}
)
@@ -172,108 +252,215 @@ function(_pw_generate_protos
endfunction(_pw_generate_protos)
# Internal function that creates a pwpb proto library.
-function(_pw_pwpb_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
- list(TRANSFORM DEPS APPEND .pwpb)
+function(_pw_pwpb_library NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ INCLUDE_FILE
+ OUT_DIR
+ MULTI_VALUE_ARGS
+ SOURCES
+ INPUTS
+ DEPS
+ )
- _pw_generate_protos("${NAME}"
- pwpb
+ list(TRANSFORM arg_DEPS APPEND .pwpb)
+
+ _pw_generate_protos("${NAME}" pwpb
+ PLUGIN
"$ENV{PW_ROOT}/pw_protobuf/py/pw_protobuf/plugin.py"
+ OUTPUT_EXTS
".pwpb.h"
- "${INCLUDE_FILE}"
- "${OUT_DIR}"
- "${SOURCES}"
- "${INPUTS}"
- "${DEPS}"
+ INCLUDE_FILE
+ "${arg_INCLUDE_FILE}"
+ OUT_DIR
+ "${arg_OUT_DIR}"
+ SOURCES
+ ${arg_SOURCES}
+ INPUTS
+ ${arg_INPUTS}
+ DEPENDS
+ ${arg_DEPS}
)
# Create the library with the generated source files.
- add_library("${NAME}.pwpb" INTERFACE)
- target_include_directories("${NAME}.pwpb" INTERFACE "${OUT_DIR}/pwpb")
- target_link_libraries("${NAME}.pwpb"
- INTERFACE
+ pw_add_library_generic("${NAME}.pwpb" INTERFACE
+ PUBLIC_INCLUDES
+ "${arg_OUT_DIR}/pwpb"
+ PUBLIC_DEPS
pw_build
- pw_polyfill.cstddef
- pw_polyfill.span
pw_protobuf
- ${DEPS}
+ pw_span
+ pw_string.string
+ ${arg_DEPS}
)
add_dependencies("${NAME}.pwpb" "${NAME}._generate.pwpb")
endfunction(_pw_pwpb_library)
+# Internal function that creates a pwpb_rpc library.
+function(_pw_pwpb_rpc_library NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ INCLUDE_FILE
+ OUT_DIR
+ MULTI_VALUE_ARGS
+ SOURCES
+ INPUTS
+ DEPS
+ )
+
+ # Determine the names of the output files.
+ list(TRANSFORM arg_DEPS APPEND .pwpb_rpc)
+
+ _pw_generate_protos("${NAME}" pwpb_rpc
+ PLUGIN
+ "$ENV{PW_ROOT}/pw_rpc/py/pw_rpc/plugin_pwpb.py"
+ OUTPUT_EXTS
+ ".rpc.pwpb.h"
+ INCLUDE_FILE
+ "${arg_INCLUDE_FILE}"
+ OUT_DIR
+ "${arg_OUT_DIR}"
+ SOURCES
+ ${arg_SOURCES}
+ INPUTS
+ ${arg_INPUTS}
+ DEPENDS
+ ${arg_DEPS}
+ "$ENV{PW_ROOT}/pw_rpc/py/pw_rpc/plugin.py"
+ )
+
+ # Create the library with the generated source files.
+ pw_add_library_generic("${NAME}.pwpb_rpc" INTERFACE
+ PUBLIC_INCLUDES
+ "${arg_OUT_DIR}/pwpb_rpc"
+ PUBLIC_DEPS
+ "${NAME}.pwpb"
+ pw_build
+ pw_rpc.pwpb.client_api
+ pw_rpc.pwpb.server_api
+ pw_rpc.server
+ ${arg_DEPS}
+ )
+ add_dependencies("${NAME}.pwpb_rpc" "${NAME}._generate.pwpb_rpc")
+endfunction(_pw_pwpb_rpc_library)
+
# Internal function that creates a raw_rpc proto library.
-function(_pw_raw_rpc_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
- list(TRANSFORM DEPS APPEND .raw_rpc)
+function(_pw_raw_rpc_library NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ INCLUDE_FILE
+ OUT_DIR
+ MULTI_VALUE_ARGS
+ SOURCES
+ INPUTS
+ DEPS
+ )
- _pw_generate_protos("${NAME}"
- raw_rpc
+ list(TRANSFORM arg_DEPS APPEND .raw_rpc)
+
+ _pw_generate_protos("${NAME}" raw_rpc
+ PLUGIN
"$ENV{PW_ROOT}/pw_rpc/py/pw_rpc/plugin_raw.py"
+ OUTPUT_EXTS
".raw_rpc.pb.h"
- "${INCLUDE_FILE}"
- "${OUT_DIR}"
- "${SOURCES}"
- "${INPUTS}"
- "${DEPS}"
+ INCLUDE_FILE
+ "${arg_INCLUDE_FILE}"
+ OUT_DIR
+ "${arg_OUT_DIR}"
+ SOURCES
+ ${arg_SOURCES}
+ INPUTS
+ ${arg_INPUTS}
+ DEPENDS
+ ${arg_DEPS}
+ "$ENV{PW_ROOT}/pw_rpc/py/pw_rpc/plugin.py"
)
# Create the library with the generated source files.
- add_library("${NAME}.raw_rpc" INTERFACE)
- target_include_directories("${NAME}.raw_rpc" INTERFACE "${OUT_DIR}/raw_rpc")
- target_link_libraries("${NAME}.raw_rpc"
- INTERFACE
+ pw_add_library_generic("${NAME}.raw_rpc" INTERFACE
+ PUBLIC_INCLUDES
+ "${arg_OUT_DIR}/raw_rpc"
+ PUBLIC_DEPS
pw_build
- pw_rpc.raw
+ pw_rpc.raw.server_api
+ pw_rpc.raw.client_api
pw_rpc.server
- ${DEPS}
+ ${arg_DEPS}
)
add_dependencies("${NAME}.raw_rpc" "${NAME}._generate.raw_rpc")
endfunction(_pw_raw_rpc_library)
# Internal function that creates a nanopb proto library.
-function(_pw_nanopb_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
- list(TRANSFORM DEPS APPEND .nanopb)
+function(_pw_nanopb_library NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ INCLUDE_FILE
+ OUT_DIR
+ MULTI_VALUE_ARGS
+ SOURCES
+ INPUTS
+ DEPS
+ )
+
+ list(TRANSFORM arg_DEPS APPEND .nanopb)
if("${dir_pw_third_party_nanopb}" STREQUAL "")
- add_custom_target("${NAME}._generate.nanopb"
- "${CMAKE_COMMAND}" -E echo
- ERROR: Attempting to use pw_proto_library, but
- dir_pw_third_party_nanopb is not set. Set dir_pw_third_party_nanopb
- to the path to the Nanopb repository.
- COMMAND
- "${CMAKE_COMMAND}" -E false
- DEPENDS
- ${DEPS}
- SOURCES
- ${SOURCES}
+ add_custom_target("${NAME}._generate.nanopb") # Nothing to do
+ pw_add_error_target("${NAME}.nanopb"
+ MESSAGE
+ "Attempting to use pw_proto_library, but dir_pw_third_party_nanopb is "
+ "not set. Set dir_pw_third_party_nanopb to the path to the Nanopb "
+ "repository."
)
- set(generated_outputs $<TARGET_PROPERTY:pw_build.empty,SOURCES>)
else()
# When compiling with the Nanopb plugin, the nanopb.proto file is already
# compiled internally, so skip recompiling it with protoc.
- if("${SOURCES}" MATCHES "nanopb\\.proto")
+ if("${arg_SOURCES}" MATCHES "nanopb\\.proto")
add_custom_target("${NAME}._generate.nanopb") # Nothing to do
- add_library("${NAME}.nanopb" INTERFACE)
- target_link_libraries("${NAME}.nanopb"
- INTERFACE
+ pw_add_library_generic("${NAME}.nanopb" INTERFACE
+ PUBLIC_DEPS
pw_build
pw_third_party.nanopb
- ${DEPS}
+ ${arg_DEPS}
)
else()
- _pw_generate_protos("${NAME}"
- nanopb
+ _pw_generate_protos("${NAME}" nanopb
+ PLUGIN
"${dir_pw_third_party_nanopb}/generator/protoc-gen-nanopb"
- ".pb.h;.pb.c"
- "${INCLUDE_FILE}"
- "${OUT_DIR}"
- "${SOURCES}"
- "${INPUTS}"
- "${DEPS}"
+ OUTPUT_EXTS
+ ".pb.h"
+ ".pb.c"
+ INCLUDE_FILE
+ "${arg_INCLUDE_FILE}"
+ OUT_DIR
+ "${arg_OUT_DIR}"
+ SOURCES
+ ${arg_SOURCES}
+ INPUTS
+ ${arg_INPUTS}
+ DEPENDS
+ ${arg_DEPS}
)
# Create the library with the generated source files.
- add_library("${NAME}.nanopb" EXCLUDE_FROM_ALL ${generated_outputs})
- target_include_directories("${NAME}.nanopb" PUBLIC "${OUT_DIR}/nanopb")
- target_link_libraries("${NAME}.nanopb" PUBLIC pw_build pw_third_party.nanopb ${DEPS})
+ pw_add_library_generic("${NAME}.nanopb" STATIC
+ SOURCES
+ ${generated_outputs}
+ PUBLIC_INCLUDES
+ "${arg_OUT_DIR}/nanopb"
+ PUBLIC_DEPS
+ pw_build
+ pw_third_party.nanopb
+ ${arg_DEPS}
+ )
endif()
add_dependencies("${NAME}.nanopb" "${NAME}._generate.nanopb")
@@ -286,35 +473,51 @@ function(_pw_nanopb_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
endfunction(_pw_nanopb_library)
# Internal function that creates a nanopb_rpc library.
-function(_pw_nanopb_rpc_library NAME SOURCES INPUTS DEPS INCLUDE_FILE OUT_DIR)
+function(_pw_nanopb_rpc_library NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ ONE_VALUE_ARGS
+ INCLUDE_FILE
+ OUT_DIR
+ MULTI_VALUE_ARGS
+ SOURCES
+ INPUTS
+ DEPS
+ )
+
# Determine the names of the output files.
- list(TRANSFORM DEPS APPEND .nanopb_rpc)
+ list(TRANSFORM arg_DEPS APPEND .nanopb_rpc)
- _pw_generate_protos("${NAME}"
- nanopb_rpc
+ _pw_generate_protos("${NAME}" nanopb_rpc
+ PLUGIN
"$ENV{PW_ROOT}/pw_rpc/py/pw_rpc/plugin_nanopb.py"
+ OUTPUT_EXTS
".rpc.pb.h"
- "${INCLUDE_FILE}"
- "${OUT_DIR}"
- "${SOURCES}"
- "${INPUTS}"
- "${DEPS}"
+ INCLUDE_FILE
+ "${arg_INCLUDE_FILE}"
+ OUT_DIR
+ "${arg_OUT_DIR}"
+ SOURCES
+ ${arg_SOURCES}
+ INPUTS
+ ${arg_INPUTS}
+ DEPENDS
+ ${arg_DEPS}
+ "$ENV{PW_ROOT}/pw_rpc/py/pw_rpc/plugin.py"
)
# Create the library with the generated source files.
- add_library("${NAME}.nanopb_rpc" INTERFACE)
- target_include_directories("${NAME}.nanopb_rpc"
- INTERFACE
- "${OUT_DIR}/nanopb_rpc"
- )
- target_link_libraries("${NAME}.nanopb_rpc"
- INTERFACE
+ pw_add_library_generic("${NAME}.nanopb_rpc" INTERFACE
+ PUBLIC_INCLUDES
+ "${arg_OUT_DIR}/nanopb_rpc"
+ PUBLIC_DEPS
"${NAME}.nanopb"
pw_build
- pw_rpc.nanopb.client
- pw_rpc.nanopb.method_union
+ pw_rpc.nanopb.client_api
+ pw_rpc.nanopb.server_api
pw_rpc.server
- ${DEPS}
+ ${arg_DEPS}
)
add_dependencies("${NAME}.nanopb_rpc" "${NAME}._generate.nanopb_rpc")
endfunction(_pw_nanopb_rpc_library)
diff --git a/pw_protobuf_compiler/proto.gni b/pw_protobuf_compiler/proto.gni
index e6fd4a8b6..8a628010c 100644
--- a/pw_protobuf_compiler/proto.gni
+++ b/pw_protobuf_compiler/proto.gni
@@ -19,6 +19,7 @@ import("$dir_pw_build/input_group.gni")
import("$dir_pw_build/mirror_tree.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/python_action.gni")
+import("$dir_pw_build/python_gn_args.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_third_party/nanopb/nanopb.gni")
import("toolchain.gni")
@@ -56,7 +57,17 @@ template("_pw_invoke_protoc") {
script =
"$dir_pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py"
- python_deps = [ "$dir_pw_protobuf_compiler/py" ]
+ # NOTE: A python_dep on "$dir_pw_protobuf_compiler/py" should not be
+ # included building that Python package which requires the build venv to
+ # be created. Instead, use python_metadata_deps to only add
+ # pw_protobuf_compiler to the PYTHONPATH.
+ python_deps = []
+
+ # Add pw_protobuf_compiler and its dependencies to the PYTHONPATH when
+ # running this action.
+ python_metadata_deps = [ "$dir_pw_protobuf_compiler/py" ]
+
+ python_deps = []
if (defined(invoker.python_deps)) {
python_deps += invoker.python_deps
}
@@ -74,17 +85,21 @@ template("_pw_invoke_protoc") {
deps += invoker.other_deps
}
- args = [
- "--language",
- invoker.language,
- "--include-file",
- _includes[0],
- "--compile-dir",
- rebase_path(invoker.compile_dir, root_build_dir),
- "--out-dir",
- rebase_path(_out_dir, root_build_dir),
- "--sources",
- ] + rebase_path(invoker.sources, root_build_dir)
+ args = []
+ if (!pw_protobuf_compiler_GENERATE_LEGACY_ENUM_SNAKE_CASE_NAMES) {
+ args += [ "--exclude-pwpb-legacy-snake-case-field-name-enums" ]
+ }
+ args += [
+ "--language",
+ invoker.language,
+ "--include-file",
+ _includes[0],
+ "--compile-dir",
+ rebase_path(invoker.compile_dir, root_build_dir),
+ "--out-dir",
+ rebase_path(_out_dir, root_build_dir),
+ "--sources",
+ ] + rebase_path(invoker.sources, root_build_dir)
if (defined(invoker.plugin)) {
inputs = [ invoker.plugin ]
@@ -136,6 +151,38 @@ template("_pw_invoke_protoc") {
# Generates pw_protobuf C++ code for proto files, creating a source_set of the
# generated files. This is internal and should not be used outside of this file.
# Use pw_proto_library instead.
+template("_pw_pwpb_rpc_proto_library") {
+ # Create a target which runs protoc configured with the pwpb_rpc plugin to
+ # generate the C++ proto RPC headers.
+ _pw_invoke_protoc(target_name) {
+ forward_variables_from(invoker, "*", _forwarded_vars)
+ language = "pwpb_rpc"
+ plugin = "$dir_pw_rpc/py/pw_rpc/plugin_pwpb.py"
+ python_deps = [ "$dir_pw_rpc/py" ]
+ }
+
+ # Create a library with the generated source files.
+ config("$target_name._include_path") {
+ include_dirs = [ "${invoker.base_out_dir}/pwpb_rpc" ]
+ visibility = [ ":*" ]
+ }
+
+ pw_source_set(target_name) {
+ forward_variables_from(invoker, _forwarded_vars)
+ public_configs = [ ":$target_name._include_path" ]
+ deps = [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
+ public_deps = [
+ ":${invoker.base_target}.pwpb",
+ "$dir_pw_protobuf",
+ "$dir_pw_rpc:server",
+ "$dir_pw_rpc/pwpb:client_api",
+ "$dir_pw_rpc/pwpb:server_api",
+ ] + invoker.deps
+ public = invoker.outputs
+ check_includes = false
+ }
+}
+
template("_pw_pwpb_proto_library") {
_pw_invoke_protoc(target_name) {
forward_variables_from(invoker, "*", _forwarded_vars)
@@ -156,7 +203,10 @@ template("_pw_pwpb_proto_library") {
deps = [ ":$target_name._gen($pw_protobuf_compiler_TOOLCHAIN)" ]
public_deps = [
"$dir_pw_containers:vector",
+ "$dir_pw_string:string",
dir_pw_assert,
+ dir_pw_function,
+ dir_pw_preprocessor,
dir_pw_protobuf,
dir_pw_result,
dir_pw_status,
@@ -311,7 +361,6 @@ template("_pw_python_proto_library") {
_pw_invoke_protoc(target_name) {
forward_variables_from(invoker, "*", _forwarded_vars + [ "python_package" ])
language = "python"
- python_deps = [ "$dir_pw_protobuf_compiler:protobuf_requirements" ]
if (defined(invoker.python_package)) {
python_package = invoker.python_package
@@ -337,9 +386,24 @@ template("_pw_python_proto_library") {
# Create a Python package with the generated source files.
pw_python_package(target_name) {
forward_variables_from(invoker, _forwarded_vars)
+ _target_dir =
+ get_path_info(get_label_info(":${invoker.base_target}", "dir"),
+ "abspath")
generate_setup = {
metadata = {
- name = invoker._package_dir
+ # Default to a package name that include the full source path to avoid
+ # conflicts with other packages when collecting all the .whl files
+ # with pw_python_wheels().
+ name =
+ string_replace(string_replace(_target_dir, "//", ""), "/", "_") +
+ "_" + invoker.base_target
+
+ # The package name should match where the __init__.py lives. If
+ # module_as_package is specified use that for the Python package name.
+ if (defined(invoker.module_as_package) &&
+ invoker.module_as_package != "") {
+ name = invoker.module_as_package
+ }
version =
"0.0.1" # TODO(hepler): Need to be able to set this verison.
}
@@ -375,6 +439,7 @@ template("_pw_python_proto_library") {
# Args:
# sources: List of input .proto files.
# deps: List of other pw_proto_library dependencies.
+# other_deps: List of other non-proto dependencies.
# inputs: Other files on which the protos depend (e.g. nanopb .options files).
# prefix: A prefix to add to the source protos prior to compilation. For
# example, a source called "foo.proto" with prefix = "nested" will be
@@ -416,7 +481,7 @@ template("pw_proto_library") {
_prefix = ""
}
- _package_dir = ""
+ _root_dir_name = ""
_source_names = []
# Determine the Python package name to use for these protos. If there is no
@@ -425,10 +490,10 @@ template("pw_proto_library") {
_path_components = []
_path_components = string_split(source, "/")
- if (_package_dir == "") {
- _package_dir = _path_components[0]
+ if (_root_dir_name == "") {
+ _root_dir_name = _path_components[0]
} else {
- assert(_prefix != "" || _path_components[0] == _package_dir,
+ assert(_prefix != "" || _path_components[0] == _root_dir_name,
"Unless 'prefix' is supplied, all .proto sources in a " +
"pw_proto_library must be in the same directory tree")
}
@@ -440,30 +505,13 @@ template("pw_proto_library") {
# If the 'prefix' was supplied, use that for the package directory.
if (_prefix != "") {
_prefix_path_components = string_split(_prefix, "/")
- _package_dir = _prefix_path_components[0]
+ _root_dir_name = _prefix_path_components[0]
}
- assert(_package_dir != "" && _package_dir != "." && _package_dir != "..",
- "Either a 'prefix' must be specified or all sources must be nested " +
- "under a common directory")
-
- # Define an action that is never executed to prevent duplicate proto packages
- # from being declared. The target name and the output file include only the
- # package directory, so different targets that use the same proto package name
- # will conflict.
- action("pw_proto_library.$_package_dir") {
- script = "$dir_pw_build/py/pw_build/nop.py"
- visibility = []
-
- # Place an error message in the output path (which is never created). If the
- # package name conflicts occur in different BUILD.gn files, this results in
- # an otherwise cryptic Ninja error, rather than a GN error.
- outputs = [ "$root_out_dir/ " +
- "ERROR - Multiple pw_proto_library targets create the " +
- "'$_package_dir' package. Change the package name by setting " +
- "the \"prefix\" arg or move the protos to a different " +
- "directory, then re-run gn gen." ]
- }
+ assert(
+ _root_dir_name != "" && _root_dir_name != "." && _root_dir_name != "..",
+ "Either a 'prefix' must be specified or all sources must be nested " +
+ "under a common directory")
if (defined(invoker.deps)) {
_deps = invoker.deps
@@ -489,7 +537,7 @@ template("pw_proto_library") {
sources += [ "$compile_dir/$_prefix/$file" ]
}
- package = _package_dir
+ package = _root_dir_name
}
# For each proto target, create a file which collects the base directories of
@@ -517,6 +565,9 @@ template("pw_proto_library") {
pw_mirror_tree("$target_name._sources") {
source_root = _source_root
sources = invoker.sources
+ if (defined(invoker.other_deps)) {
+ deps = invoker.other_deps
+ }
if (defined(invoker.inputs)) {
sources += invoker.inputs
@@ -525,11 +576,31 @@ template("pw_proto_library") {
directory = "${_common.compile_dir}/$_prefix"
}
} else {
- not_needed(invoker, [ "inputs" ])
+ not_needed(invoker,
+ [
+ "inputs",
+ "other_deps",
+ ])
}
# Enumerate all of the protobuf generator targets.
+ _pw_pwpb_rpc_proto_library("$target_name.pwpb_rpc") {
+ forward_variables_from(invoker, _forwarded_vars)
+ forward_variables_from(_common, "*")
+
+ deps = []
+ foreach(dep, _deps) {
+ _base = get_label_info(dep, "label_no_toolchain")
+ deps += [ "$_base.pwpb_rpc(" + get_label_info(dep, "toolchain") + ")" ]
+ }
+
+ outputs = []
+ foreach(name, _source_names) {
+ outputs += [ "$base_out_dir/pwpb_rpc/$_prefix/${name}.rpc.pwpb.h" ]
+ }
+ }
+
_pw_pwpb_proto_library("$target_name.pwpb") {
forward_variables_from(invoker, _forwarded_vars)
forward_variables_from(_common, "*")
@@ -650,6 +721,7 @@ template("pw_proto_library") {
# All supported pw_protobuf generators.
_protobuf_generators = [
"pwpb",
+ "pwpb_rpc",
"nanopb",
"nanopb_rpc",
"raw_rpc",
diff --git a/pw_protobuf_compiler/pw_nanopb_cc_library.bzl b/pw_protobuf_compiler/pw_nanopb_cc_library.bzl
index a5ae2d26a..c2bda47c3 100644
--- a/pw_protobuf_compiler/pw_nanopb_cc_library.bzl
+++ b/pw_protobuf_compiler/pw_nanopb_cc_library.bzl
@@ -16,7 +16,7 @@
Nanopb C++ library generating targets.
"""
-# TODO(pwbug/621) Enable unused variable check.
+# TODO(b/234873954) Enable unused variable check.
# buildifier: disable=unused-variable
def pw_nanopb_cc_library(
name,
diff --git a/pw_protobuf_compiler/pw_nested_packages/BUILD.bazel b/pw_protobuf_compiler/pw_nested_packages/BUILD.bazel
new file mode 100644
index 000000000..92a3073c3
--- /dev/null
+++ b/pw_protobuf_compiler/pw_nested_packages/BUILD.bazel
@@ -0,0 +1,56 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+
+package(default_visibility = ["//visibility:public"])
+
+proto_library(
+ name = "aggregate",
+ srcs = ["aggregate.proto"],
+ import_prefix = "proto_root",
+ strip_import_prefix = "/pw_protobuf_compiler/pw_nested_packages",
+ deps = [
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/id",
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/thing",
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/thing:type_of_thing",
+ ],
+)
+
+pw_proto_library(
+ name = "aggregate_pw_proto",
+ deps = [
+ ":aggregate",
+ ],
+)
+
+proto_library(
+ name = "aggregate_wrapper",
+ srcs = ["aggregate_wrapper.proto"],
+ import_prefix = "proto_root",
+ strip_import_prefix = "/pw_protobuf_compiler/pw_nested_packages",
+ deps = [
+ ":aggregate",
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/id",
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/thing:type_of_thing",
+ ],
+)
+
+pw_proto_library(
+ name = "aggregate_wrapper_pw_proto",
+ deps = [
+ ":aggregate_wrapper",
+ ],
+)
diff --git a/pw_protobuf_compiler/pw_nested_packages/BUILD.gn b/pw_protobuf_compiler/pw_nested_packages/BUILD.gn
new file mode 100644
index 000000000..27c378404
--- /dev/null
+++ b/pw_protobuf_compiler/pw_nested_packages/BUILD.gn
@@ -0,0 +1,39 @@
+# Copyright 2019 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_protobuf_compiler/proto.gni")
+
+pw_proto_library("aggregate") {
+ sources = [ "aggregate.proto" ]
+ strip_prefix = "$dir_pw_protobuf_compiler/pw_nested_packages"
+ prefix = "proto_root"
+ deps = [
+ "$dir_pw_protobuf_compiler/pw_nested_packages/data_type/id",
+ "$dir_pw_protobuf_compiler/pw_nested_packages/data_type/thing",
+ "$dir_pw_protobuf_compiler/pw_nested_packages/data_type/thing:type_of_thing",
+ ]
+}
+
+pw_proto_library("aggregate_wrapper") {
+ sources = [ "aggregate_wrapper.proto" ]
+ strip_prefix = "$dir_pw_protobuf_compiler/pw_nested_packages"
+ prefix = "proto_root"
+ deps = [
+ ":aggregate",
+ "$dir_pw_protobuf_compiler/pw_nested_packages/data_type/id",
+ "$dir_pw_protobuf_compiler/pw_nested_packages/data_type/thing:type_of_thing",
+ ]
+}
diff --git a/pw_protobuf_compiler/pw_nested_packages/aggregate.proto b/pw_protobuf_compiler/pw_nested_packages/aggregate.proto
new file mode 100644
index 000000000..d2603f5c8
--- /dev/null
+++ b/pw_protobuf_compiler/pw_nested_packages/aggregate.proto
@@ -0,0 +1,30 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+syntax = "proto3";
+
+package pw.protobuf_compiler.proto_root;
+
+import "proto_root/data_type/id/id.proto";
+import "proto_root/data_type/thing/thing.proto";
+import "proto_root/data_type/thing/type_of_thing.proto";
+
+message Aggregate {
+ data_type.id.Id id = 1;
+ data_type.thing.TypeOfThing type = 2;
+
+ data_type.thing.Thing alice = 3;
+ data_type.thing.Thing neodymium = 4;
+ data_type.thing.Thing mountain = 5;
+}
diff --git a/pw_protobuf_compiler/pw_nested_packages/aggregate_wrapper.proto b/pw_protobuf_compiler/pw_nested_packages/aggregate_wrapper.proto
new file mode 100644
index 000000000..b07c4cc0f
--- /dev/null
+++ b/pw_protobuf_compiler/pw_nested_packages/aggregate_wrapper.proto
@@ -0,0 +1,28 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+syntax = "proto3";
+
+package pw.protobuf_compiler.proto_root;
+
+import "proto_root/aggregate.proto";
+import "proto_root/data_type/id/id.proto";
+import "proto_root/data_type/thing/type_of_thing.proto";
+
+message AggregateWrapper {
+ data_type.id.Id id = 1;
+ data_type.thing.TypeOfThing type = 2;
+
+ Aggregate aggregate = 3;
+}
diff --git a/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel b/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel
new file mode 100644
index 000000000..9d9fa4186
--- /dev/null
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.bazel
@@ -0,0 +1,35 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+
+package(default_visibility = ["//visibility:public"])
+
+proto_library(
+ name = "id",
+ srcs = [
+ "id.proto",
+ "internal/id_internal.proto",
+ ],
+ import_prefix = "proto_root",
+ strip_import_prefix = "/pw_protobuf_compiler/pw_nested_packages",
+)
+
+pw_proto_library(
+ name = "id_pw_proto",
+ deps = [
+ ":id",
+ ],
+)
diff --git a/pw_fuzzer/oss_fuzz.gni b/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.gn
index 3a2143822..974eb2860 100644
--- a/pw_fuzzer/oss_fuzz.gni
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/id/BUILD.gn
@@ -12,13 +12,15 @@
# License for the specific language governing permissions and limitations under
# the License.
-# TODO(aarongreen): Do some minimal parsing on the environment variables to
-# identify conflicting configs.
-oss_fuzz_extra_cflags_c = string_split(getenv("CFLAGS"))
-oss_fuzz_extra_cflags_cc = string_split(getenv("CXXFLAGS"))
-oss_fuzz_extra_ldflags = string_split(getenv("LDFLAGS"))
+import("//build_overrides/pigweed.gni")
-# TODO(pwbug/184): OSS-Fuzz sets -stdlib=libc++, but pw_minimal_cpp_stdlib
-# sets -nostdinc++. Find a more flexible mechanism to achieve this and
-# similar needs (like removing -fno-rtti fro UBSan).
-oss_fuzz_extra_cflags_cc += [ "-Wno-unused-command-line-argument" ]
+import("$dir_pw_protobuf_compiler/proto.gni")
+
+pw_proto_library("id") {
+ sources = [
+ "id.proto",
+ "internal/id_internal.proto",
+ ]
+ strip_prefix = "$dir_pw_protobuf_compiler/pw_nested_packages"
+ prefix = "proto_root"
+}
diff --git a/pw_protobuf_compiler/pw_nested_packages/data_type/id/id.proto b/pw_protobuf_compiler/pw_nested_packages/data_type/id/id.proto
new file mode 100644
index 000000000..6d4473047
--- /dev/null
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/id/id.proto
@@ -0,0 +1,24 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+syntax = "proto3";
+
+package pw.protobuf_compiler.proto_root.data_type.id;
+
+import "proto_root/data_type/id/internal/id_internal.proto";
+
+message Id {
+ uint32 id = 1;
+ internal.IdImplementationDetails impl = 2;
+}
diff --git a/pw_polyfill/public_overrides/iterator b/pw_protobuf_compiler/pw_nested_packages/data_type/id/internal/id_internal.proto
index a386ee801..11c80a73a 100644
--- a/pw_polyfill/public_overrides/iterator
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/id/internal/id_internal.proto
@@ -11,8 +11,12 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-#pragma once
-#include_next <iterator>
+syntax = "proto3";
-#include "pw_polyfill/standard_library/iterator.h"
+package pw.protobuf_compiler.proto_root.data_type.id.internal;
+
+message IdImplementationDetails {
+ int32 foo = 1;
+ int32 bar = 2;
+}
diff --git a/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel
new file mode 100644
index 000000000..12c7aadd2
--- /dev/null
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.bazel
@@ -0,0 +1,50 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+
+package(default_visibility = ["//visibility:public"])
+
+proto_library(
+ name = "type_of_thing",
+ srcs = ["type_of_thing.proto"],
+ import_prefix = "proto_root",
+ strip_import_prefix = "/pw_protobuf_compiler/pw_nested_packages",
+)
+
+pw_proto_library(
+ name = "type_of_thing_pw_proto",
+ deps = [
+ ":type_of_thing",
+ ],
+)
+
+proto_library(
+ name = "thing",
+ srcs = ["thing.proto"],
+ import_prefix = "proto_root",
+ strip_import_prefix = "/pw_protobuf_compiler/pw_nested_packages",
+ deps = [
+ ":type_of_thing",
+ "//pw_protobuf_compiler/pw_nested_packages/data_type/id",
+ ],
+)
+
+pw_proto_library(
+ name = "thing_pw_proto",
+ deps = [
+ ":thing",
+ ],
+)
diff --git a/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.gn b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.gn
new file mode 100644
index 000000000..ddb56cc9f
--- /dev/null
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/BUILD.gn
@@ -0,0 +1,33 @@
+# Copyright 2019 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_protobuf_compiler/proto.gni")
+
+pw_proto_library("type_of_thing") {
+ sources = [ "type_of_thing.proto" ]
+ strip_prefix = "$dir_pw_protobuf_compiler/pw_nested_packages"
+ prefix = "proto_root"
+}
+
+pw_proto_library("thing") {
+ sources = [ "thing.proto" ]
+ strip_prefix = "$dir_pw_protobuf_compiler/pw_nested_packages"
+ prefix = "proto_root"
+ deps = [
+ ":type_of_thing",
+ "$dir_pw_protobuf_compiler/pw_nested_packages/data_type/id",
+ ]
+}
diff --git a/pw_web_ui/src/frontend/index.tsx b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/thing.proto
index ef96a3eee..61b28f62a 100644
--- a/pw_web_ui/src/frontend/index.tsx
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/thing.proto
@@ -12,10 +12,14 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser */
-import * as React from 'react';
-import * as ReactDOM from 'react-dom';
-import {App} from './app';
+syntax = "proto3";
-// Bootstrap the app and append it to the DOM
-ReactDOM.render(<App />, document.getElementById('react-root'));
+package pw.protobuf_compiler.proto_root.data_type.thing;
+
+import "proto_root/data_type/id/id.proto";
+import "proto_root/data_type/thing/type_of_thing.proto";
+
+message Thing {
+ .pw.protobuf_compiler.proto_root.data_type.id.Id id = 1;
+ TypeOfThing type = 2;
+}
diff --git a/pw_polyfill/public_overrides/bit b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/type_of_thing.proto
index 3d5460a15..c9d32e984 100644
--- a/pw_polyfill/public_overrides/bit
+++ b/pw_protobuf_compiler/pw_nested_packages/data_type/thing/type_of_thing.proto
@@ -11,10 +11,15 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-#pragma once
-#if __has_include_next(<bit>)
-#include_next <bit>
-#endif // __has_include_next(<bit>)
+syntax = "proto3";
-#include "pw_polyfill/standard_library/bit.h"
+package pw.protobuf_compiler.proto_root.data_type.thing;
+
+enum TypeOfThing {
+ NONE = 0;
+ ORGANISM = 1;
+ SUBSTANCE = 2;
+ OBJECT = 3;
+ OTHER = 4;
+}
diff --git a/pw_protobuf_compiler/pw_proto_library.bzl b/pw_protobuf_compiler/pw_proto_library.bzl
index f9a304846..58d07bdc9 100644
--- a/pw_protobuf_compiler/pw_proto_library.bzl
+++ b/pw_protobuf_compiler/pw_proto_library.bzl
@@ -22,11 +22,11 @@ of rules_proto_grpc. However, the version checked in here does not yet support,
In addition, nanopb proto files are not yet generated.
-TODO(pwbug/621): Close these gaps and start using this implementation.
+TODO(b/234873954): Close these gaps and start using this implementation.
# Overview of implementation
-(If you just want to use pw_proto_library, see its docstring; this section is
+(If you just want to use the macros, see their docstrings; this section is
intended to orient future maintainers.)
Proto code generation is carried out by the _pw_proto_library,
@@ -43,27 +43,151 @@ files at C++ compile time.
Although we have a separate rule for each protocol compiler plugin
(_pw_proto_library, _pw_raw_rpc_proto_library, _pw_nanopb_rpc_proto_library),
they actually share an implementation (_pw _impl_pw_proto_library) and use
-similar aspects, all generated by _proto_compiler_aspect. The only difference
-between the rules are captured in the PIGWEED_PLUGIN dictonary and the aspect
-instantiations (_pw_proto_compiler_aspect, etc).
-
+similar aspects, all generated by _proto_compiler_aspect.
"""
load("//pw_build:pigweed.bzl", "pw_cc_library")
load("@rules_proto//proto:defs.bzl", "ProtoInfo")
-load("//pw_protobuf_compiler:pw_nanopb_cc_library", "pw_nanopb_cc_library")
+load("//pw_protobuf_compiler:pw_nanopb_cc_library.bzl", "pw_nanopb_cc_library")
+
+def pwpb_proto_library(name, deps, tags = None, visibility = None):
+ """A C++ proto library generated using pw_protobuf.
+
+ Attributes:
+ deps: proto_library targets for which to generate this library.
+ """
+ name_pb = name + ".pb"
+
+ _pw_proto_library(
+ name = name_pb,
+ deps = deps,
+ )
+
+ pw_cc_library(
+ name = name,
+ hdrs = [":" + name_pb],
+ deps = [
+ "//pw_assert:facade",
+ "//pw_containers:vector",
+ "//pw_preprocessor",
+ "//pw_protobuf",
+ "//pw_result",
+ "//pw_span",
+ "//pw_status",
+ "//pw_string:string",
+ ],
+ linkstatic = 1,
+ tags = tags,
+ visibility = visibility,
+ )
+
+def pwpb_rpc_proto_library(name, deps, pwpb_proto_library_deps, tags = None, visibility = None):
+ """A pwpb_rpc proto library target.
+
+ Attributes:
+ deps: proto_library targets for which to generate this library.
+ pwpb_proto_library_deps: A pwpb_proto_library generated
+ from the same proto_library. Required.
+ """
+ name_pb = name + ".pb"
+
+ _pw_pwpb_rpc_proto_library(
+ name = name_pb,
+ deps = deps,
+ )
+
+ pw_cc_library(
+ name = name,
+ hdrs = [":" + name_pb],
+ deps = [
+ "//pw_protobuf",
+ "//pw_rpc",
+ "//pw_rpc/pwpb:client_api",
+ "//pw_rpc/pwpb:server_api",
+ ] + pwpb_proto_library_deps,
+ linkstatic = 1,
+ tags = tags,
+ visibility = visibility,
+ )
+
+def raw_rpc_proto_library(name, deps, tags = None, visibility = None):
+ """A raw C++ RPC proto library."""
+ name_pb = name + ".pb"
+
+ _pw_raw_rpc_proto_library(
+ name = name_pb,
+ deps = deps,
+ )
+
+ pw_cc_library(
+ name = name,
+ hdrs = [":" + name_pb],
+ deps = [
+ "//pw_rpc",
+ "//pw_rpc/raw:client_api",
+ "//pw_rpc/raw:server_api",
+ ],
+ linkstatic = 1,
+ tags = tags,
+ visibility = visibility,
+ )
+
+def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, tags = None, visibility = None):
+ """A C++ RPC proto library using nanopb.
+
+ Attributes:
+ deps: proto_library targets for which to generate this library.
+ nanopb_proto_library_deps: A pw_nanopb_cc_library generated
+ from the same proto_library. Required.
+ """
+ name_pb = name + ".pb"
-def pw_proto_library(name = "", deps = [], nanopb_options = None):
+ _pw_nanopb_rpc_proto_library(
+ name = name_pb,
+ deps = deps,
+ )
+
+ pw_cc_library(
+ name = name,
+ hdrs = [":" + name_pb],
+ deps = [
+ "//pw_rpc",
+ "//pw_rpc/nanopb:client_api",
+ "//pw_rpc/nanopb:server_api",
+ ] + nanopb_proto_library_deps,
+ linkstatic = 1,
+ tags = tags,
+ visibility = visibility,
+ )
+
+def pw_proto_library(
+ name,
+ deps,
+ visibility = None,
+ tags = None,
+ nanopb_options = None,
+ enabled_targets = None):
"""Generate Pigweed proto C++ code.
- This is the only public symbol in this file: everything else is
- implementation details.
+ DEPRECATED. This macro is deprecated and will be removed in a future
+ Pigweed version. Please use the single-target macros above.
Args:
name: The name of the target.
deps: proto_library targets from which to generate Pigweed C++.
+ visibility: The visibility of the target. See
+ https://bazel.build/concepts/visibility.
+ tags: Tags for the target. See
+ https://bazel.build/reference/be/common-definitions#common-attributes.
nanopb_options: path to file containing nanopb options, if any
(https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options).
+ enabled_targets: Specifies which libraries should be generated. Libraries
+ will only be generated as needed, but unnecessary outputs may conflict
+ with other build rules and thus cause build failures. This filter allows
+ manual selection of which libraries should be supported by this build
+ target in order to prevent such conflicts. The argument, if provided,
+ should be a subset of ["pwpb", "nanopb", "raw_rpc", "nanopb_rpc"]. All
+ are enabled by default. Note that "nanopb_rpc" relies on "nanopb".
Example usage:
@@ -88,6 +212,8 @@ def pw_proto_library(name = "", deps = [], nanopb_options = None):
The pw_proto_library generates the following targets in this example:
"benchmark_pw_proto.pwpb": C++ library exposing the "benchmark.pwpb.h" header.
+ "benchmark_pw_proto.pwpb_rpc": C++ library exposing the
+ "benchmark.rpc.pwpb.h" header.
"benchmark_pw_proto.raw_rpc": C++ library exposing the "benchmark.raw_rpc.h"
header.
"benchmark_pw_proto.nanopb": C++ library exposing the "benchmark.pb.h"
@@ -96,29 +222,52 @@ def pw_proto_library(name = "", deps = [], nanopb_options = None):
"benchmark.rpc.pb.h" header.
"""
- # Use nanopb to generate the pb.h and pb.c files, and the target exposing
- # them.
- pw_nanopb_cc_library(name + ".nanopb", deps, options = nanopb_options)
+ def is_plugin_enabled(plugin):
+ return (enabled_targets == None or plugin in enabled_targets)
+
+ if is_plugin_enabled("nanopb"):
+ # Use nanopb to generate the pb.h and pb.c files, and the target
+ # exposing them.
+ pw_nanopb_cc_library(
+ name = name + ".nanopb",
+ deps = deps,
+ visibility = visibility,
+ tags = tags,
+ options = nanopb_options,
+ )
+
+ if is_plugin_enabled("pwpb"):
+ pwpb_proto_library(
+ name = name + ".pwpb",
+ deps = deps,
+ tags = tags,
+ visibility = visibility,
+ )
+
+ if is_plugin_enabled("pwpb_rpc"):
+ pwpb_rpc_proto_library(
+ name = name + ".pwpb_rpc",
+ deps = deps,
+ pwpb_proto_library_deps = [":" + name + ".pwpb"],
+ tags = tags,
+ visibility = visibility,
+ )
- # Use Pigweed proto plugins to generate the remaining files and targets.
- for plugin_name, info in PIGWEED_PLUGIN.items():
- name_pb = name + "_pb." + plugin_name
- info["compiler"](
- name = name_pb,
+ if is_plugin_enabled("raw_rpc"):
+ raw_rpc_proto_library(
+ name = name + ".raw_rpc",
deps = deps,
+ tags = tags,
+ visibility = visibility,
)
- # The rpc.pb.h header depends on the generated nanopb code.
- if info["include_nanopb_dep"]:
- lib_deps = info["deps"] + [":" + name + ".nanopb"]
- else:
- lib_deps = info["deps"]
-
- pw_cc_library(
- name = name + "." + plugin_name,
- hdrs = [name_pb],
- deps = lib_deps,
- linkstatic = 1,
+ if is_plugin_enabled("nanopb_rpc"):
+ nanopb_rpc_proto_library(
+ name = name + ".nanopb_rpc",
+ deps = deps,
+ nanopb_proto_library_deps = [":" + name + ".nanopb"],
+ tags = tags,
+ visibility = visibility,
)
PwProtoInfo = provider(
@@ -128,6 +277,16 @@ PwProtoInfo = provider(
},
)
+PwProtoOptionsInfo = provider(
+ "Allows `pw_proto_filegroup` targets to pass along `.options` files " +
+ "without polluting the `DefaultInfo` provider, which means they can " +
+ "still be used in the `srcs` of `proto_library` targets.",
+ fields = {
+ "options_files": (".options file(s) associated with a proto_library " +
+ "for Pigweed codegen."),
+ },
+)
+
def _get_short_path(source):
return source.short_path
@@ -137,23 +296,47 @@ def _get_path(file):
def _proto_compiler_aspect_impl(target, ctx):
# List the files we will generate for this proto_library target.
genfiles = []
+
for src in target[ProtoInfo].direct_sources:
path = src.basename[:-len("proto")] + ctx.attr._extension
genfiles.append(ctx.actions.declare_file(path, sibling = src))
+ # List the `.options` files from any `pw_proto_filegroup` targets listed
+ # under this target's `srcs`.
+ options_files = [
+ options_file
+ for src in ctx.rule.attr.srcs
+ if PwProtoOptionsInfo in src
+ for options_file in src[PwProtoOptionsInfo].options_files.to_list()
+ ]
+
+ # Convert include paths to a depset and back to deduplicate entries.
+ # Note that this will probably evaluate to either [] or ["."] in most cases.
+ options_file_include_paths = depset([
+ "." if options_file.root.path == "" else options_file.root.path
+ for options_file in options_files
+ ]).to_list()
+
args = ctx.actions.args()
args.add("--plugin=protoc-gen-pwpb={}".format(ctx.executable._protoc_plugin.path))
+ for options_file_include_path in options_file_include_paths:
+ args.add("--pwpb_opt=-I{}".format(options_file_include_path))
+ args.add("--pwpb_opt=--no-legacy-namespace")
args.add("--pwpb_out={}".format(ctx.bin_dir.path))
args.add_joined(
"--descriptor_set_in",
target[ProtoInfo].transitive_descriptor_sets,
- join_with = ctx.host_configuration.host_path_separator,
+ join_with = ctx.configuration.host_path_separator,
map_each = _get_path,
)
+
args.add_all(target[ProtoInfo].direct_sources, map_each = _get_short_path)
ctx.actions.run(
- inputs = depset(target[ProtoInfo].transitive_sources.to_list(), transitive = [target[ProtoInfo].transitive_descriptor_sets]),
+ inputs = depset(
+ target[ProtoInfo].transitive_sources.to_list() + options_files,
+ transitive = [target[ProtoInfo].transitive_descriptor_sets],
+ ),
progress_message = "Generating %s C++ files for %s" % (ctx.attr._extension, ctx.label.name),
tools = [ctx.executable._protoc_plugin],
outputs = genfiles,
@@ -187,15 +370,16 @@ def _proto_compiler_aspect(extension, protoc_plugin):
"_protoc": attr.label(
default = Label("@com_google_protobuf//:protoc"),
executable = True,
- cfg = "host",
+ cfg = "exec",
),
"_protoc_plugin": attr.label(
default = Label(protoc_plugin),
executable = True,
- cfg = "host",
+ cfg = "exec",
),
},
implementation = _proto_compiler_aspect_impl,
+ provides = [PwProtoInfo],
)
def _impl_pw_proto_library(ctx):
@@ -214,7 +398,7 @@ def _impl_pw_proto_library(ctx):
# in srcs. We don't perform layering_check in Pigweed, so this is not a big
# deal.
#
- # TODO(pwbug/621): Tidy this up.
+ # TODO(b/234873954): Tidy this up.
all_genfiles = []
for dep in ctx.attr.deps:
for f in dep[PwProtoInfo].genfiles:
@@ -235,6 +419,18 @@ _pw_proto_library = rule(
},
)
+_pw_pwpb_rpc_proto_compiler_aspect = _proto_compiler_aspect("rpc.pwpb.h", "//pw_rpc/py:plugin_pwpb")
+
+_pw_pwpb_rpc_proto_library = rule(
+ implementation = _impl_pw_proto_library,
+ attrs = {
+ "deps": attr.label_list(
+ providers = [ProtoInfo],
+ aspects = [_pw_pwpb_rpc_proto_compiler_aspect],
+ ),
+ },
+)
+
_pw_raw_rpc_proto_compiler_aspect = _proto_compiler_aspect("raw_rpc.pb.h", "//pw_rpc/py:plugin_raw")
_pw_raw_rpc_proto_library = rule(
@@ -259,31 +455,70 @@ _pw_nanopb_rpc_proto_library = rule(
},
)
-PIGWEED_PLUGIN = {
- "pwpb": {
- "compiler": _pw_proto_library,
- "deps": [
- "//pw_span",
- "//pw_protobuf:pw_protobuf",
- ],
- "include_nanopb_dep": False,
- },
- "raw_rpc": {
- "compiler": _pw_raw_rpc_proto_library,
- "deps": [
- "//pw_rpc",
- "//pw_rpc/raw:client_api",
- "//pw_rpc/raw:server_api",
- ],
- "include_nanopb_dep": False,
- },
- "nanopb_rpc": {
- "compiler": _pw_nanopb_rpc_proto_library,
- "deps": [
- "//pw_rpc",
- "//pw_rpc/nanopb:client_api",
- "//pw_rpc/nanopb:server_api",
- ],
- "include_nanopb_dep": True,
+def _pw_proto_filegroup_impl(ctx):
+ source_files = list()
+ options_files = list()
+
+ for src in ctx.attr.srcs:
+ source_files += src.files.to_list()
+
+ for options_src in ctx.attr.options_files:
+ for file in options_src.files.to_list():
+ if file.extension == "options":
+ options_files.append(file)
+ else:
+ fail((
+ "Files provided as `options_files` to a " +
+ "`pw_proto_filegroup` must have the `.options` " +
+ "extension; the file `{}` was provided."
+ ).format(file.basename))
+
+ return [
+ DefaultInfo(files = depset(source_files)),
+ PwProtoOptionsInfo(options_files = depset(options_files)),
+ ]
+
+pw_proto_filegroup = rule(
+ doc = (
+ "Acts like a `filegroup`, but with an additional `options_files` " +
+ "attribute that accepts a list of `.options` files. These `.options` " +
+ "files should typically correspond to `.proto` files provided under " +
+ "the `srcs` attribute." +
+ "\n\n" +
+ "A `pw_proto_filegroup` is intended to be passed into the `srcs` of " +
+ "a `proto_library` target as if it were a normal `filegroup` " +
+ "containing only `.proto` files. For the purposes of the " +
+ "`proto_library` itself, the `pw_proto_filegroup` does indeed act " +
+ "just like a normal `filegroup`; the `options_files` attribute is " +
+ "ignored. However, if that `proto_library` target is then passed " +
+ "(directly or transitively) into the `deps` of a `pw_proto_library` " +
+ "for code generation, the `pw_proto_library` target will have access " +
+ "to the provided `.options` files and will pass them to the code " +
+ "generator." +
+ "\n\n" +
+ "Note that, in order for a `pw_proto_filegroup` to be a valid `srcs` " +
+ "entry for a `proto_library`, it must meet the same conditions " +
+ "required of a standard `filegroup` in that context. Namely, its " +
+ "`srcs` must provide at least one `.proto` (or `.protodevel`) file. " +
+ "Put simply, a `pw_proto_filegroup` cannot be used as a vector for " +
+ "injecting solely `.options` files; it must contain at least one " +
+ "proto as well (generally one associated with an included `.options` " +
+ "file in the interest of clarity)." +
+ "\n\n" +
+ "Regarding the somewhat unusual usage, this feature's design was " +
+ "mostly preordained by the combination of Bazel's strict access " +
+ "controls, the restrictions imposed on inputs to the `proto_library` " +
+ "rule, and the need to support `.options` files from transitive " +
+ "dependencies."
+ ),
+ implementation = _pw_proto_filegroup_impl,
+ attrs = {
+ "srcs": attr.label_list(
+ allow_files = True,
+ ),
+ "options_files": attr.label_list(
+ allow_files = True,
+ ),
},
-}
+ provides = [PwProtoOptionsInfo],
+)
diff --git a/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel b/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel
new file mode 100644
index 000000000..280c01fd5
--- /dev/null
+++ b/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/BUILD.bazel
@@ -0,0 +1,50 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load(
+ "//pw_protobuf_compiler:pw_proto_library.bzl",
+ "pw_proto_filegroup",
+ # TODO(b/234873954): Also load `pw_proto_library` from here when possible.
+ # A somewhat different Starlark macro with the same name is currently loaded
+ # from `proto.bzl` (below), but we need to switch to the implementation from
+ # `pw_proto_library.bzl` when possible.
+)
+load(
+ "//pw_protobuf_compiler:proto.bzl",
+ "pw_proto_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+pw_proto_filegroup(
+ name = "pwpb_test_proto_and_options_files",
+ srcs = ["pwpb_test.proto"],
+ options_files = ["pwpb_test.options"],
+)
+
+proto_library(
+ name = "pwpb_test_proto",
+ srcs = [":pwpb_test_proto_and_options_files"],
+ deps = [
+ "//pw_protobuf:field_options_proto",
+ ],
+)
+
+pw_proto_library(
+ name = "pwpb_test_pw_proto",
+ deps = [
+ ":pwpb_test_proto",
+ ],
+)
diff --git a/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/pwpb_test.options b/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/pwpb_test.options
new file mode 100644
index 000000000..f7e21b940
--- /dev/null
+++ b/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/pwpb_test.options
@@ -0,0 +1,21 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+pw.protobuf_compiler.Point.name max_size:16
+
+pw.protobuf_compiler.OptionsFileExample.thirty_two_chars max_size:32
+pw.protobuf_compiler.OptionsFileExample.forty_two_chars max_size:42
+// The `max_size` of the string `unspecified_length` is of course unspecified.
+
+pw.protobuf_compiler.InlineOptionsExample.ten_chars_inline fixed_size:True
diff --git a/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/pwpb_test.proto b/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/pwpb_test.proto
new file mode 100644
index 000000000..f93f5eeed
--- /dev/null
+++ b/pw_protobuf_compiler/pw_protobuf_compiler_pwpb_protos/pwpb_test.proto
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+syntax = "proto3";
+
+package pw.protobuf_compiler;
+
+import "pw_protobuf_protos/field_options.proto";
+
+message Point {
+ uint32 x = 1;
+ uint32 y = 2;
+ string name = 3;
+};
+
+// Check that `.options` files work properly. Compare three strings, two of them
+// with different `max_size` values defined in the `.options` file and the third
+// with no specified `max_size`.
+message OptionsFileExample {
+ string thirty_two_chars = 1;
+ string forty_two_chars = 2;
+ string unspecified_length = 3;
+};
+
+message InlineOptionsExample {
+ string ten_chars_inline = 1 [(pw.protobuf.pwpb).max_size = 10];
+}
diff --git a/pw_protobuf_compiler/pwpb_test.cc b/pw_protobuf_compiler/pwpb_test.cc
new file mode 100644
index 000000000..ae5ffa7f8
--- /dev/null
+++ b/pw_protobuf_compiler/pwpb_test.cc
@@ -0,0 +1,61 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_protobuf_compiler_pwpb_protos/pwpb_test.pwpb.h"
+#include "pw_string/string.h"
+
+namespace pw::protobuf_compiler {
+namespace {
+
+TEST(Pwpb, CompilesProtobufs) {
+ pwpb::Point::Message point = {4, 8, "point"};
+ EXPECT_EQ(point.x, 4u);
+ EXPECT_EQ(point.y, 8u);
+ EXPECT_EQ(point.name.size(), 5u);
+ EXPECT_EQ(point.name, "point");
+}
+
+TEST(Pwpb, OptionsFilesAreApplied) {
+ pwpb::OptionsFileExample::Message string_options_comparison;
+
+ static_assert(
+ std::is_same_v<decltype(string_options_comparison.thirty_two_chars),
+ pw::InlineString<32>>,
+ "Field `thirty_two_chars` should be a `pw::InlineString<32>`.");
+
+ static_assert(
+ std::is_same_v<decltype(string_options_comparison.forty_two_chars),
+ pw::InlineString<42>>,
+ "Field `forty_two_chars` should be a `pw::InlineString<42>`.");
+
+ static_assert(
+ std::is_same_v<
+ decltype(string_options_comparison.unspecified_length),
+ pw::protobuf::Callback<pwpb::OptionsFileExample::StreamEncoder,
+ pwpb::OptionsFileExample::StreamDecoder>>,
+ "The field `unspecified_length` should be a `pw::protobuf::Callback`.");
+}
+
+TEST(Pwpb, InlineOptionsAppliedAndOverridden) {
+ pw::protobuf_compiler::InlineOptionsExample::Message inline_options_example;
+
+ static_assert(
+ std::is_same_v<decltype(inline_options_example.ten_chars_inline),
+ pw::InlineString<10>>,
+ "Field `ten_chars_inline` should be a `pw::InlineString<10>`.");
+}
+
+} // namespace
+} // namespace pw::protobuf_compiler
diff --git a/pw_protobuf_compiler/py/Android.bp b/pw_protobuf_compiler/py/Android.bp
new file mode 100644
index 000000000..d51246ac7
--- /dev/null
+++ b/pw_protobuf_compiler/py/Android.bp
@@ -0,0 +1,32 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+python_library_host {
+ name: "pw_protobuf_compiler_py_lib",
+ srcs: [
+ "pw_protobuf_compiler/*.py",
+ ],
+}
+
+python_binary_host {
+ name: "pw_protobuf_compiler_py",
+ main: "pw_protobuf_compiler/generate_protos.py",
+ srcs: [
+ "pw_protobuf_compiler/generate_protos.py",
+ ],
+} \ No newline at end of file
diff --git a/pw_protobuf_compiler/py/BUILD.gn b/pw_protobuf_compiler/py/BUILD.gn
index 0d8321e4e..4616364d6 100644
--- a/pw_protobuf_compiler/py/BUILD.gn
+++ b/pw_protobuf_compiler/py/BUILD.gn
@@ -36,6 +36,7 @@ pw_python_package("py") {
python_deps = [ "$dir_pw_cli/py" ]
python_test_deps = [ "..:test_protos.python" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
# If Nanopb is available, test protos that import nanopb.proto.
if (dir_pw_third_party_nanopb != "") {
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
index add698f9c..da19c59d0 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/generate_protos.py
@@ -31,42 +31,111 @@ except ImportError:
_LOG = logging.getLogger(__name__)
-_COMMON_FLAGS = ('--experimental_allow_proto3_optional', )
-
def _argument_parser() -> argparse.ArgumentParser:
"""Registers the script's arguments on an argument parser."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--language',
- required=True,
- choices=DEFAULT_PROTOC_ARGS,
- help='Output language')
- parser.add_argument('--plugin-path',
- type=Path,
- help='Path to the protoc plugin')
- parser.add_argument('--include-file',
- type=argparse.FileType('r'),
- help='File containing additional protoc include paths')
- parser.add_argument('--out-dir',
- type=Path,
- required=True,
- help='Output directory for generated code')
- parser.add_argument('--compile-dir',
- type=Path,
- required=True,
- help='Root path for compilation')
- parser.add_argument('--sources',
- type=Path,
- nargs='+',
- help='Input protobuf files')
+ parser.add_argument(
+ '--language',
+ required=True,
+ choices=DEFAULT_PROTOC_ARGS,
+ help='Output language',
+ )
+ parser.add_argument(
+ '--plugin-path', type=Path, help='Path to the protoc plugin'
+ )
+ parser.add_argument(
+ '--proto-path',
+ type=Path,
+ help='Additional protoc include paths',
+ action='append',
+ )
+ parser.add_argument(
+ '--include-file',
+ type=argparse.FileType('r'),
+ help='File containing additional protoc include paths',
+ )
+ parser.add_argument(
+ '--out-dir',
+ type=Path,
+ required=True,
+ help='Output directory for generated code',
+ )
+ parser.add_argument(
+ '--compile-dir',
+ type=Path,
+ required=True,
+ help='Root path for compilation',
+ )
+ parser.add_argument(
+ '--sources', type=Path, nargs='+', help='Input protobuf files'
+ )
+ parser.add_argument(
+ '--protoc', type=Path, default='protoc', help='Path to protoc'
+ )
+ parser.add_argument(
+ '--no-experimental-proto3-optional',
+ dest='experimental_proto3_optional',
+ action='store_false',
+ help='Do not invoke protoc with --experimental_allow_proto3_optional',
+ )
+ parser.add_argument(
+ '--no-generate-type-hints',
+ dest='generate_type_hints',
+ action='store_false',
+ help='Do not generate pyi files for python',
+ )
+ parser.add_argument(
+ '--exclude-pwpb-legacy-snake-case-field-name-enums',
+ dest='exclude_pwpb_legacy_snake_case_field_name_enums',
+ action='store_true',
+ help=(
+ 'If set, generates legacy SNAKE_CASE names for field name enums '
+ 'in PWPB.'
+ ),
+ )
return parser
-def protoc_cc_args(args: argparse.Namespace) -> Tuple[str, ...]:
- return _COMMON_FLAGS + (
+def protoc_common_args(args: argparse.Namespace) -> Tuple[str, ...]:
+ flags: Tuple[str, ...] = ()
+ if args.experimental_proto3_optional:
+ flags += ('--experimental_allow_proto3_optional',)
+ return flags
+
+
+def protoc_pwpb_args(
+ args: argparse.Namespace, include_paths: List[str]
+) -> Tuple[str, ...]:
+ out_args = [
+ '--plugin',
+ f'protoc-gen-custom={args.plugin_path}',
+ f'--custom_opt=-I{args.compile_dir}',
+ *[f'--custom_opt=-I{include_path}' for include_path in include_paths],
+ ]
+
+ if args.exclude_pwpb_legacy_snake_case_field_name_enums:
+ out_args.append(
+ '--custom_opt=--exclude-legacy-snake-case-field-name-enums'
+ )
+
+ out_args.extend(
+ [
+ '--custom_out',
+ args.out_dir,
+ ]
+ )
+
+ return tuple(out_args)
+
+
+def protoc_pwpb_rpc_args(
+ args: argparse.Namespace, _include_paths: List[str]
+) -> Tuple[str, ...]:
+ return (
'--plugin',
f'protoc-gen-custom={args.plugin_path}',
'--custom_out',
@@ -74,16 +143,20 @@ def protoc_cc_args(args: argparse.Namespace) -> Tuple[str, ...]:
)
-def protoc_go_args(args: argparse.Namespace) -> Tuple[str, ...]:
- return _COMMON_FLAGS + (
+def protoc_go_args(
+ args: argparse.Namespace, _include_paths: List[str]
+) -> Tuple[str, ...]:
+ return (
'--go_out',
f'plugins=grpc:{args.out_dir}',
)
-def protoc_nanopb_args(args: argparse.Namespace) -> Tuple[str, ...]:
+def protoc_nanopb_args(
+ args: argparse.Namespace, _include_paths: List[str]
+) -> Tuple[str, ...]:
# nanopb needs to know of the include path to parse *.options files
- return _COMMON_FLAGS + (
+ return (
'--plugin',
f'protoc-gen-nanopb={args.plugin_path}',
# nanopb_opt provides the flags to use for nanopb_out. Windows doesn't
@@ -95,8 +168,10 @@ def protoc_nanopb_args(args: argparse.Namespace) -> Tuple[str, ...]:
)
-def protoc_nanopb_rpc_args(args: argparse.Namespace) -> Tuple[str, ...]:
- return _COMMON_FLAGS + (
+def protoc_nanopb_rpc_args(
+ args: argparse.Namespace, _include_paths: List[str]
+) -> Tuple[str, ...]:
+ return (
'--plugin',
f'protoc-gen-custom={args.plugin_path}',
'--custom_out',
@@ -104,8 +179,10 @@ def protoc_nanopb_rpc_args(args: argparse.Namespace) -> Tuple[str, ...]:
)
-def protoc_raw_rpc_args(args: argparse.Namespace) -> Tuple[str, ...]:
- return _COMMON_FLAGS + (
+def protoc_raw_rpc_args(
+ args: argparse.Namespace, _include_paths: List[str]
+) -> Tuple[str, ...]:
+ return (
'--plugin',
f'protoc-gen-custom={args.plugin_path}',
'--custom_out',
@@ -113,26 +190,37 @@ def protoc_raw_rpc_args(args: argparse.Namespace) -> Tuple[str, ...]:
)
-def protoc_python_args(args: argparse.Namespace) -> Tuple[str, ...]:
- return _COMMON_FLAGS + (
+def protoc_python_args(
+ args: argparse.Namespace, _include_paths: List[str]
+) -> Tuple[str, ...]:
+ flags: Tuple[str, ...] = (
'--python_out',
args.out_dir,
- '--mypy_out',
- args.out_dir,
)
+ if args.generate_type_hints:
+ flags += (
+ '--mypy_out',
+ args.out_dir,
+ )
+
+ return flags
-_DefaultArgsFunction = Callable[[argparse.Namespace], Tuple[str, ...]]
+
+_DefaultArgsFunction = Callable[
+ [argparse.Namespace, List[str]], Tuple[str, ...]
+]
# Default additional protoc arguments for each supported language.
# TODO(frolv): Make these overridable with a command-line argument.
DEFAULT_PROTOC_ARGS: Dict[str, _DefaultArgsFunction] = {
- 'pwpb': protoc_cc_args,
'go': protoc_go_args,
'nanopb': protoc_nanopb_args,
'nanopb_rpc': protoc_nanopb_rpc_args,
- 'raw_rpc': protoc_raw_rpc_args,
+ 'pwpb': protoc_pwpb_args,
+ 'pwpb_rpc': protoc_pwpb_rpc_args,
'python': protoc_python_args,
+ 'raw_rpc': protoc_raw_rpc_args,
}
# Languages that protoc internally supports.
@@ -147,13 +235,16 @@ def main() -> int:
if args.plugin_path is None and args.language not in BUILTIN_PROTOC_LANGS:
parser.error(
- f'--plugin-path is required for --language {args.language}')
+ f'--plugin-path is required for --language {args.language}'
+ )
args.out_dir.mkdir(parents=True, exist_ok=True)
include_paths: List[str] = []
if args.include_file:
- include_paths = [f'-I{line.strip()}' for line in args.include_file]
+ include_paths.extend(line.strip() for line in args.include_file)
+ if args.proto_path:
+ include_paths.extend(str(path) for path in args.proto_path)
wrapper_script: Optional[Path] = None
@@ -164,32 +255,36 @@ def main() -> int:
args.plugin_path = args.plugin_path.with_suffix('.bat')
_LOG.debug('Using Batch plugin %s', args.plugin_path)
else:
- with tempfile.NamedTemporaryFile('w', suffix='.bat',
- delete=False) as file:
+ with tempfile.NamedTemporaryFile(
+ 'w', suffix='.bat', delete=False
+ ) as file:
file.write(f'@echo off\npython {args.plugin_path.resolve()}\n')
args.plugin_path = wrapper_script = Path(file.name)
_LOG.debug('Using generated plugin wrapper %s', args.plugin_path)
cmd: Tuple[Union[str, Path], ...] = (
- 'protoc',
+ args.protoc,
f'-I{args.compile_dir}',
- *include_paths,
- *DEFAULT_PROTOC_ARGS[args.language](args),
+ *[f'-I{include_path}' for include_path in include_paths],
+ *protoc_common_args(args),
+ *DEFAULT_PROTOC_ARGS[args.language](args, include_paths),
*args.sources,
)
try:
- process = subprocess.run(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ process = subprocess.run(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
finally:
if wrapper_script:
wrapper_script.unlink()
if process.returncode != 0:
- _LOG.error('Protocol buffer compilation failed!\n%s',
- ' '.join(str(c) for c in cmd))
+ _LOG.error(
+ 'Protocol buffer compilation failed!\n%s',
+ ' '.join(str(c) for c in cmd),
+ )
sys.stderr.buffer.write(process.stdout)
sys.stderr.flush()
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/proto_target_invalid.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/proto_target_invalid.py
index 72894b4e2..1877fb146 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/proto_target_invalid.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/proto_target_invalid.py
@@ -19,14 +19,11 @@ import sys
from typing import Optional
-from pw_cli.color import colors
-import pw_cli.log
-
_LOG = logging.getLogger(__name__)
def argument_parser(
- parser: Optional[argparse.ArgumentParser] = None
+ parser: Optional[argparse.ArgumentParser] = None,
) -> argparse.ArgumentParser:
"""Registers the script's arguments on an argument parser."""
@@ -36,10 +33,12 @@ def argument_parser(
parser.add_argument('--dir', required=True, help='Target directory')
parser.add_argument('--root', required=True, help='GN root')
parser.add_argument('--target', required=True, help='Build target')
- parser.add_argument('generators',
- metavar='GEN',
- nargs='+',
- help='Supported protobuf generators')
+ parser.add_argument(
+ 'generators',
+ metavar='GEN',
+ nargs='+',
+ help='Supported protobuf generators',
+ )
return parser
@@ -48,11 +47,10 @@ def main() -> int:
"""Prints an error message."""
args = argument_parser().parse_args()
- relative_dir = args.dir[len(args.root):].rstrip('/')
+ relative_dir = args.dir[len(args.root) :].rstrip('/')
_LOG.error('')
- _LOG.error('The target %s is not a compiled protobuf library.',
- colors().bold_white(args.target))
+ _LOG.error('The target %s is not a compiled protobuf library.', args.target)
_LOG.error('')
_LOG.error('A different target is generated for each active generator.')
_LOG.error('Depend on one of the following targets instead:')
@@ -65,5 +63,13 @@ def main() -> int:
if __name__ == '__main__':
- pw_cli.log.install()
+ try:
+ # If pw_cli is available, use it to initialize logs.
+ from pw_cli import log
+
+ log.install(logging.INFO)
+ except ImportError:
+ # If pw_cli isn't available, display log messages like a simple print.
+ logging.basicConfig(format='%(message)s', level=logging.INFO)
+
sys.exit(main())
diff --git a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
index 905fe0d13..5d379cbb6 100644
--- a/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
+++ b/pw_protobuf_compiler/py/pw_protobuf_compiler/python_protos.py
@@ -22,8 +22,30 @@ import subprocess
import shlex
import tempfile
from types import ModuleType
-from typing import (Dict, Generic, Iterable, Iterator, List, NamedTuple, Set,
- Tuple, TypeVar, Union)
+from typing import (
+ Dict,
+ Generic,
+ Iterable,
+ Iterator,
+ List,
+ NamedTuple,
+ Optional,
+ Set,
+ Tuple,
+ TypeVar,
+ Union,
+)
+
+try:
+ # pylint: disable=wrong-import-position
+ import black
+
+ black_mode: Optional[black.Mode] = black.Mode(string_normalization=False)
+
+ # pylint: enable=wrong-import-position
+except ImportError:
+ black = None # type: ignore
+ black_mode = None
_LOG = logging.getLogger(__name__)
@@ -33,7 +55,8 @@ PathOrStr = Union[Path, str]
def compile_protos(
output_dir: PathOrStr,
proto_files: Iterable[PathOrStr],
- includes: Iterable[PathOrStr] = ()) -> None:
+ includes: Iterable[PathOrStr] = (),
+) -> None:
"""Compiles proto files for Python by invoking the protobuf compiler.
Proto files not covered by one of the provided include paths will have their
@@ -59,9 +82,11 @@ def compile_protos(
process = subprocess.run(cmd, capture_output=True)
if process.returncode:
- _LOG.error('protoc invocation failed!\n%s\n%s',
- ' '.join(shlex.quote(str(c)) for c in cmd),
- process.stderr.decode())
+ _LOG.error(
+ 'protoc invocation failed!\n%s\n%s',
+ ' '.join(shlex.quote(str(c)) for c in cmd),
+ process.stderr.decode(),
+ )
process.check_returncode()
@@ -84,13 +109,17 @@ def import_modules(directory: PathOrStr) -> Iterator:
name, ext = os.path.splitext(file)
if ext == '.py':
- yield _import_module(f'{".".join(path_parts)}.{name}',
- os.path.join(dirpath, file))
+ yield _import_module(
+ f'{".".join(path_parts)}.{name}',
+ os.path.join(dirpath, file),
+ )
-def compile_and_import(proto_files: Iterable[PathOrStr],
- includes: Iterable[PathOrStr] = (),
- output_dir: PathOrStr = None) -> Iterator:
+def compile_and_import(
+ proto_files: Iterable[PathOrStr],
+ includes: Iterable[PathOrStr] = (),
+ output_dir: Optional[PathOrStr] = None,
+) -> Iterator:
"""Compiles protos and imports their modules; yields the proto modules.
Args:
@@ -112,16 +141,20 @@ def compile_and_import(proto_files: Iterable[PathOrStr],
yield from import_modules(tempdir)
-def compile_and_import_file(proto_file: PathOrStr,
- includes: Iterable[PathOrStr] = (),
- output_dir: PathOrStr = None):
+def compile_and_import_file(
+ proto_file: PathOrStr,
+ includes: Iterable[PathOrStr] = (),
+ output_dir: Optional[PathOrStr] = None,
+):
"""Compiles and imports the module for a single .proto file."""
return next(iter(compile_and_import([proto_file], includes, output_dir)))
-def compile_and_import_strings(contents: Iterable[str],
- includes: Iterable[PathOrStr] = (),
- output_dir: PathOrStr = None) -> Iterator:
+def compile_and_import_strings(
+ contents: Iterable[str],
+ includes: Iterable[PathOrStr] = (),
+ output_dir: Optional[PathOrStr] = None,
+) -> Iterator:
"""Compiles protos in one or more strings."""
if isinstance(contents, str):
@@ -145,6 +178,7 @@ T = TypeVar('T')
class _NestedPackage(Generic[T]):
"""Facilitates navigating protobuf packages as attributes."""
+
def __init__(self, package: str):
self._packages: Dict[str, _NestedPackage[T]] = {}
self._items: List[T] = []
@@ -167,7 +201,8 @@ class _NestedPackage(Generic[T]):
return getattr(item, attr)
raise AttributeError(
- f'Proto package "{self._package}" does not contain "{attr}"')
+ f'Proto package "{self._package}" does not contain "{attr}"'
+ )
def __getitem__(self, subpackage: str) -> '_NestedPackage[T]':
"""Support accessing nested packages by name."""
@@ -186,7 +221,8 @@ class _NestedPackage(Generic[T]):
for attr, value in vars(item).items():
# Exclude private variables and modules from dir().
if not attr.startswith('_') and not isinstance(
- value, ModuleType):
+ value, ModuleType
+ ):
attributes.append(attr)
return attributes
@@ -199,7 +235,8 @@ class _NestedPackage(Generic[T]):
msg = [f'ProtoPackage({self._package!r}']
public_members = [
- i for i in vars(self)
+ i
+ for i in vars(self)
if i not in self._packages and not i.startswith('_')
]
if public_members:
@@ -216,12 +253,14 @@ class _NestedPackage(Generic[T]):
class Packages(NamedTuple):
"""Items in a protobuf package structure; returned from as_package."""
+
items_by_package: Dict[str, List]
packages: _NestedPackage
-def as_packages(items: Iterable[Tuple[str, T]],
- packages: Packages = None) -> Packages:
+def as_packages(
+ items: Iterable[Tuple[str, T]], packages: Optional[Packages] = None
+) -> Packages:
"""Places items in a proto-style package structure navigable by attributes.
Args:
@@ -240,8 +279,9 @@ def as_packages(items: Iterable[Tuple[str, T]],
# pylint: disable=protected-access
for i, subpackage in enumerate(subpackages, 1):
if subpackage not in entry._packages:
- entry._add_package(subpackage,
- _NestedPackage('.'.join(subpackages[:i])))
+ entry._add_package(
+ subpackage, _NestedPackage('.'.join(subpackages[:i]))
+ )
entry = entry._packages[subpackage]
@@ -272,6 +312,7 @@ class Library:
the list of modules in a particular package, and the modules() generator
for iterating over all modules.
"""
+
@classmethod
def from_paths(cls, protos: Iterable[PathOrModule]) -> 'Library':
"""Creates a Library from paths to proto files or proto modules."""
@@ -289,10 +330,12 @@ class Library:
return Library(modules)
@classmethod
- def from_strings(cls,
- contents: Iterable[str],
- includes: Iterable[PathOrStr] = (),
- output_dir: PathOrStr = None) -> 'Library':
+ def from_strings(
+ cls,
+ contents: Iterable[str],
+ includes: Iterable[PathOrStr] = (),
+ output_dir: Optional[PathOrStr] = None,
+ ) -> 'Library':
"""Creates a proto library from protos in the provided strings."""
return cls(compile_and_import_strings(contents, includes, output_dir))
@@ -306,7 +349,8 @@ class Library:
"""
self.modules_by_package, self.packages = as_packages(
(m.DESCRIPTOR.package, m) # type: ignore[attr-defined]
- for m in modules)
+ for m in modules
+ )
def modules(self) -> Iterable:
"""Iterates over all protobuf modules in this library."""
@@ -317,7 +361,8 @@ class Library:
"""Iterates over all protobuf messages in this library."""
for module in self.modules():
yield from _nested_messages(
- module, module.DESCRIPTOR.message_types_by_name)
+ module, module.DESCRIPTOR.message_types_by_name
+ )
def _nested_messages(scope, message_names: Iterable[str]) -> Iterator:
@@ -373,8 +418,10 @@ def _proto_repr(message) -> Iterator[str]:
continue
except ValueError:
# Skip default-valued fields that don't support HasField.
- if (field.label != field.LABEL_REPEATED
- and value == field.default_value):
+ if (
+ field.label != field.LABEL_REPEATED
+ and value == field.default_value
+ ):
continue
if field.label == field.LABEL_REPEATED:
@@ -385,7 +432,8 @@ def _proto_repr(message) -> Iterator[str]:
key_desc, value_desc = field.message_type.fields
values = ', '.join(
f'{_field_repr(key_desc, k)}: {_field_repr(value_desc, v)}'
- for k, v in value.items())
+ for k, v in value.items()
+ )
yield f'{field.name}={{{values}}}'
else:
values = ', '.join(_field_repr(field, v) for v in value)
@@ -394,6 +442,21 @@ def _proto_repr(message) -> Iterator[str]:
yield f'{field.name}={_field_repr(field, value)}'
-def proto_repr(message) -> str:
- """Creates a repr-like string for a protobuf."""
- return f'{message.DESCRIPTOR.full_name}({", ".join(_proto_repr(message))})'
+def proto_repr(message, *, wrap: bool = True) -> str:
+ """Creates a repr-like string for a protobuf.
+
+ In an interactive console that imports proto objects into the namespace, the
+ output of proto_repr() can be used as Python source to create a proto
+ object.
+
+ Args:
+ message: The protobuf message to format
+ wrap: If true and black is available, the output is wrapped according to
+ PEP8 using black.
+ """
+ raw = f'{message.DESCRIPTOR.full_name}({", ".join(_proto_repr(message))})'
+
+ if wrap and black is not None and black_mode is not None:
+ return black.format_str(raw, mode=black_mode).strip()
+
+ return raw
diff --git a/pw_protobuf_compiler/py/python_protos_test.py b/pw_protobuf_compiler/py/python_protos_test.py
index a6afaf964..819bede33 100755
--- a/pw_protobuf_compiler/py/python_protos_test.py
+++ b/pw_protobuf_compiler/py/python_protos_test.py
@@ -101,6 +101,7 @@ message NestingMessage {
class TestCompileAndImport(unittest.TestCase):
"""Test compiling and importing."""
+
def setUp(self):
self._proto_dir = tempfile.TemporaryDirectory(prefix='proto_test')
self._protos = []
@@ -122,7 +123,8 @@ class TestCompileAndImport(unittest.TestCase):
# Make sure the protobuf modules contain what we expect them to.
mod = modules['test_1.proto']
self.assertEqual(
- 4, len(mod.DESCRIPTOR.services_by_name['PublicService'].methods))
+ 4, len(mod.DESCRIPTOR.services_by_name['PublicService'].methods)
+ )
mod = modules['test_2.proto']
self.assertEqual(mod.Request(magic_number=1.5).magic_number, 1.5)
@@ -134,10 +136,12 @@ class TestCompileAndImport(unittest.TestCase):
class TestProtoLibrary(TestCompileAndImport):
"""Tests the Library class."""
+
def setUp(self):
super().setUp()
self._library = python_protos.Library(
- python_protos.compile_and_import(self._protos))
+ python_protos.compile_and_import(self._protos)
+ )
def test_packages_can_access_messages(self):
msg = self._library.packages.pw.protobuf_compiler.test1.SomeMessage
@@ -191,12 +195,18 @@ class TestProtoLibrary(TestCompileAndImport):
self.assertEqual(val, 0)
def test_access_nested_packages_by_name(self):
- self.assertIs(self._library.packages['pw.protobuf_compiler.test1'],
- self._library.packages.pw.protobuf_compiler.test1)
- self.assertIs(self._library.packages.pw['protobuf_compiler.test1'],
- self._library.packages.pw.protobuf_compiler.test1)
- self.assertIs(self._library.packages.pw.protobuf_compiler['test1'],
- self._library.packages.pw.protobuf_compiler.test1)
+ self.assertIs(
+ self._library.packages['pw.protobuf_compiler.test1'],
+ self._library.packages.pw.protobuf_compiler.test1,
+ )
+ self.assertIs(
+ self._library.packages.pw['protobuf_compiler.test1'],
+ self._library.packages.pw.protobuf_compiler.test1,
+ )
+ self.assertIs(
+ self._library.packages.pw.protobuf_compiler['test1'],
+ self._library.packages.pw.protobuf_compiler.test1,
+ )
def test_access_nested_packages_by_name_unknown_package(self):
with self.assertRaises(KeyError):
@@ -217,7 +227,8 @@ class TestProtoLibrary(TestCompileAndImport):
def test_messages(self):
protos = self._library.packages.pw.protobuf_compiler
self.assertEqual(
- set(self._library.messages()), {
+ set(self._library.messages()),
+ {
protos.test1.SomeMessage,
protos.test1.AnotherMessage,
protos.test2.Request,
@@ -226,7 +237,8 @@ class TestProtoLibrary(TestCompileAndImport):
protos.test2.NestingMessage,
protos.test2.NestingMessage.NestedMessage,
protos.test2.NestingMessage.NestedMessage.NestedNestedMessage,
- })
+ },
+ )
PROTO_FOR_REPR = """\
@@ -267,7 +279,7 @@ message Message {
oneof oneof_test {
string oneof_1 = 15;
int32 oneof_2 = 16;
- float oneof_3 = 17;
+ Nested oneof_3 = 17;
}
map<string, Nested> mapping = 18;
@@ -277,6 +289,7 @@ message Message {
class TestProtoRepr(unittest.TestCase):
"""Tests printing protobufs."""
+
def setUp(self):
protos = python_protos.Library.from_strings(PROTO_FOR_REPR)
self.enum = protos.packages.pw.test3.Enum
@@ -294,9 +307,12 @@ class TestProtoRepr(unittest.TestCase):
'optional_int=-1, '
'repeated_int=[0, 1, 2])',
proto_repr(
- self.message(repeated_int=[0, 1, 2],
- regular_int=999,
- optional_int=-1)))
+ self.message(
+ repeated_int=[0, 1, 2], regular_int=999, optional_int=-1
+ ),
+ wrap=False,
+ ),
+ )
def test_bytes_fields(self):
self.assertEqual(
@@ -309,7 +325,10 @@ class TestProtoRepr(unittest.TestCase):
regular_bytes=b'\xfe\xed\xbe\xef',
optional_bytes=b'',
repeated_bytes=[b"Hello'''"],
- )))
+ ),
+ wrap=False,
+ ),
+ )
def test_string_fields(self):
self.assertEqual(
@@ -322,30 +341,42 @@ class TestProtoRepr(unittest.TestCase):
regular_string='hi',
optional_string='',
repeated_string=[b"'"],
- )))
+ ),
+ wrap=False,
+ ),
+ )
def test_enum_fields(self):
- self.assertEqual('pw.test3.Nested(an_enum=pw.test3.Enum.ONE)',
- proto_repr(self.nested(an_enum=1)))
- self.assertEqual('pw.test3.Message(optional_enum=pw.test3.Enum.ONE)',
- proto_repr(self.message(optional_enum=self.enum.ONE)))
+ self.assertEqual(
+ 'pw.test3.Nested(an_enum=pw.test3.Enum.ONE)',
+ proto_repr(self.nested(an_enum=1)),
+ )
+ self.assertEqual(
+ 'pw.test3.Message(optional_enum=pw.test3.Enum.ONE)',
+ proto_repr(self.message(optional_enum=self.enum.ONE)),
+ )
self.assertEqual(
'pw.test3.Message(repeated_enum='
'[pw.test3.Enum.ONE, pw.test3.Enum.ONE, pw.test3.Enum.ZERO])',
- proto_repr(self.message(repeated_enum=[1, 1, 0])))
+ proto_repr(self.message(repeated_enum=[1, 1, 0]), wrap=False),
+ )
def test_message_fields(self):
self.assertEqual(
'pw.test3.Message(message=pw.test3.Nested(value=[123]))',
- proto_repr(self.message(message=self.nested(value=[123]))))
+ proto_repr(self.message(message=self.nested(value=[123]))),
+ )
self.assertEqual(
'pw.test3.Message('
'repeated_message=[pw.test3.Nested(value=[123]), '
'pw.test3.Nested()])',
proto_repr(
self.message(
- repeated_message=[self.nested(
- value=[123]), self.nested()])))
+ repeated_message=[self.nested(value=[123]), self.nested()]
+ ),
+ wrap=False,
+ ),
+ )
def test_optional_shown_if_set_to_default(self):
self.assertEqual(
@@ -353,18 +384,32 @@ class TestProtoRepr(unittest.TestCase):
"optional_int=0, optional_bytes=b'', optional_string='', "
"optional_enum=pw.test3.Enum.ZERO)",
proto_repr(
- self.message(optional_int=0,
- optional_bytes=b'',
- optional_string='',
- optional_enum=0)))
+ self.message(
+ optional_int=0,
+ optional_bytes=b'',
+ optional_string='',
+ optional_enum=0,
+ ),
+ wrap=False,
+ ),
+ )
def test_oneof(self):
- self.assertEqual(proto_repr(self.message(oneof_1='test')),
- "pw.test3.Message(oneof_1='test')")
- self.assertEqual(proto_repr(self.message(oneof_2=123)),
- "pw.test3.Message(oneof_2=123)")
- self.assertEqual(proto_repr(self.message(oneof_3=123)),
- "pw.test3.Message(oneof_3=123.0)")
+ self.assertEqual(
+ proto_repr(self.message(oneof_1='test')),
+ "pw.test3.Message(oneof_1='test')",
+ )
+ self.assertEqual(
+ proto_repr(self.message(oneof_2=123)),
+ "pw.test3.Message(oneof_2=123)",
+ )
+ self.assertEqual(
+ proto_repr(
+ self.message(oneof_3=self.nested(an_enum=self.enum.ONE))
+ ),
+ 'pw.test3.Message('
+ 'oneof_3=pw.test3.Nested(an_enum=pw.test3.Enum.ONE))',
+ )
msg = self.message(oneof_1='test')
msg.oneof_2 = 99
@@ -374,24 +419,59 @@ class TestProtoRepr(unittest.TestCase):
msg = self.message()
msg.mapping['zero'].MergeFrom(self.nested())
msg.mapping['one'].MergeFrom(
- self.nested(an_enum=self.enum.ONE, value=[1]))
+ self.nested(an_enum=self.enum.ONE, value=[1])
+ )
- result = proto_repr(msg)
+ result = proto_repr(msg, wrap=False)
self.assertRegex(result, r'^pw.test3.Message\(mapping={.*}\)$')
self.assertIn("'zero': pw.test3.Nested()", result)
self.assertIn(
"'one': pw.test3.Nested(value=[1], an_enum=pw.test3.Enum.ONE)",
- result)
+ result,
+ )
def test_bytes_repr(self):
- self.assertEqual(bytes_repr(b'\xfe\xed\xbe\xef'),
- r"b'\xFE\xED\xBE\xEF'")
- self.assertEqual(bytes_repr(b'\xfe\xed\xbe\xef123'),
- r"b'\xFE\xED\xBE\xEF\x31\x32\x33'")
- self.assertEqual(bytes_repr(b'\xfe\xed\xbe\xef1234'),
- r"b'\xFE\xED\xBE\xEF1234'")
- self.assertEqual(bytes_repr(b'\xfe\xed\xbe\xef12345'),
- r"b'\xFE\xED\xBE\xEF12345'")
+ self.assertEqual(
+ bytes_repr(b'\xfe\xed\xbe\xef'), r"b'\xFE\xED\xBE\xEF'"
+ )
+ self.assertEqual(
+ bytes_repr(b'\xfe\xed\xbe\xef123'),
+ r"b'\xFE\xED\xBE\xEF\x31\x32\x33'",
+ )
+ self.assertEqual(
+ bytes_repr(b'\xfe\xed\xbe\xef1234'), r"b'\xFE\xED\xBE\xEF1234'"
+ )
+ self.assertEqual(
+ bytes_repr(b'\xfe\xed\xbe\xef12345'), r"b'\xFE\xED\xBE\xEF12345'"
+ )
+
+ def test_wrap_multiple_lines(self):
+ self.assertEqual(
+ """\
+pw.test3.Message(
+ optional_int=0,
+ optional_bytes=b'',
+ optional_string='',
+ optional_enum=pw.test3.Enum.ZERO,
+)""",
+ proto_repr(
+ self.message(
+ optional_int=0,
+ optional_bytes=b'',
+ optional_string='',
+ optional_enum=0,
+ ),
+ wrap=True,
+ ),
+ )
+
+ def test_wrap_one_line(self):
+ self.assertEqual(
+ "pw.test3.Message(optional_int=0, optional_bytes=b'')",
+ proto_repr(
+ self.message(optional_int=0, optional_bytes=b''), wrap=True
+ ),
+ )
if __name__ == '__main__':
diff --git a/pw_protobuf_compiler/py/setup.cfg b/pw_protobuf_compiler/py/setup.cfg
index 09b0f7b14..8e3a6eca0 100644
--- a/pw_protobuf_compiler/py/setup.cfg
+++ b/pw_protobuf_compiler/py/setup.cfg
@@ -22,13 +22,13 @@ description = Pigweed protoc wrapper
packages = find:
zip_safe = False
install_requires =
- # NOTE: mypy needs to stay in sync with mypy-protobuf
- # Currently using mypy 0.910 and mypy-protobuf 2.9
- # This must also be specified in //pw_protobuf_compiler/BUILD.gn
- mypy-protobuf==2.9
- protobuf
- pw_cli
- types-protobuf
+ # NOTE: protobuf needs to stay in sync with mypy-protobuf
+ # Currently using mypy protobuf 3.20.1 and mypy-protobuf 3.3.0 (see
+ # constraint.list). These requirements should stay as >= the lowest version
+ # we support.
+ mypy-protobuf>=3.2.0
+ protobuf>=3.20.1
+ types-protobuf>=3.19.22
[options.package_data]
pw_protobuf_compiler = py.typed
diff --git a/pw_protobuf_compiler/toolchain.gni b/pw_protobuf_compiler/toolchain.gni
index 606e69d39..806a96367 100644
--- a/pw_protobuf_compiler/toolchain.gni
+++ b/pw_protobuf_compiler/toolchain.gni
@@ -20,4 +20,10 @@ declare_args() {
# in a single toolchain to avoid unnecessary duplication in the build.
pw_protobuf_compiler_TOOLCHAIN =
"$dir_pw_protobuf_compiler/toolchain:protocol_buffer"
+
+ # pwpb previously generated field enum names in SNAKE_CASE rather than
+ # kConstantCase. Set this variable to temporarily enable legacy SNAKE_CASE
+ # support while you migrate your codebase to kConstantCase.
+ # b/266298474
+ pw_protobuf_compiler_GENERATE_LEGACY_ENUM_SNAKE_CASE_NAMES = true
}
diff --git a/pw_protobuf_compiler/ts/BUILD.bazel b/pw_protobuf_compiler/ts/BUILD.bazel
deleted file mode 100644
index 020da8fe4..000000000
--- a/pw_protobuf_compiler/ts/BUILD.bazel
+++ /dev/null
@@ -1,70 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
-load("@npm//@bazel/typescript:index.bzl", "ts_library", "ts_project")
-load("ts_proto_collection.bzl", "ts_proto_collection")
-
-package(default_visibility = ["//visibility:public"])
-
-filegroup(
- name = "ts_proto_collection_template",
- srcs = ["ts_proto_collection.template.ts"],
-)
-
-ts_project(
- name = "lib",
- srcs = [
- "index.ts",
- "proto_collection.ts",
- ],
- declaration = True,
- source_map = True,
- deps = ["@npm//:node_modules"], # can't use fine-grained deps
-)
-
-js_library(
- name = "pw_protobuf_compiler",
- package_name = "@pigweed/pw_protobuf_compiler",
- srcs = ["package.json"],
- deps = [":lib"],
-)
-
-ts_proto_collection(
- name = "test_proto_collection",
- js_proto_library = "//pw_protobuf_compiler:test_protos_tspb",
- proto_library = "//pw_protobuf_compiler:test_protos",
-)
-
-ts_library(
- name = "ts_proto_collection_test_lib",
- srcs = [
- "ts_proto_collection_test.ts",
- ],
- deps = [
- ":test_proto_collection",
- "//pw_protobuf_compiler:test_protos_tspb",
- "//pw_rpc/ts:packet_proto_tspb",
- "@npm//@types/google-protobuf",
- "@npm//@types/jasmine",
- ],
-)
-
-jasmine_node_test(
- name = "ts_proto_collection_test",
- srcs = [
- ":ts_proto_collection_test_lib",
- ],
-)
diff --git a/pw_protobuf_compiler/ts/build.ts b/pw_protobuf_compiler/ts/build.ts
new file mode 100644
index 000000000..2baa7e641
--- /dev/null
+++ b/pw_protobuf_compiler/ts/build.ts
@@ -0,0 +1,98 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {exec, ExecException} from 'child_process';
+import fs from 'fs';
+import path from 'path';
+import generateTemplate from './codegen/template_replacement';
+// eslint-disable-next-line node/no-extraneous-import
+import * as argModule from 'arg';
+const arg = argModule.default;
+
+const googProtobufPath = require.resolve('google-protobuf');
+const googProtobufModule = fs.readFileSync(googProtobufPath, 'utf-8');
+
+const args = arg({
+ // Types
+ '--proto': [String],
+ '--out': String,
+
+ // Aliases
+ '-p': '--proto',
+});
+
+const protos = args['--proto'];
+const outDir = args['--out'] || 'protos';
+
+fs.mkdirSync(outDir, {recursive: true});
+
+const run = function (executable: string, args: string[]) {
+ return new Promise<void>(resolve => {
+ exec(`${executable} ${args.join(" ")}`, {cwd: process.cwd()}, (error: ExecException | null, stdout: string | Buffer) => {
+ if (error) {
+ throw error;
+ }
+
+ console.log(stdout);
+ resolve();
+ });
+ });
+};
+
+const protoc = async function (protos: string[], outDir: string) {
+ const PROTOC_GEN_TS_PATH = path.resolve(
+ path.dirname(require.resolve('ts-protoc-gen/generate.js')),
+ '..',
+ '.bin',
+ 'protoc-gen-ts'
+ );
+
+ await run('protoc', [
+ `--plugin="protoc-gen-ts=${PROTOC_GEN_TS_PATH}"`,
+ `--descriptor_set_out=${outDir}/descriptor.bin`,
+ `--js_out=import_style=commonjs,binary:${outDir}`,
+ `--ts_out=${outDir}`,
+ `--proto_path=${process.cwd()}`,
+ ...protos,
+ ]);
+
+ // ES6 workaround: Replace google-protobuf imports with entire library.
+ protos.forEach(protoPath => {
+ const outPath = path.join(outDir, protoPath.replace('.proto', '_pb.js'));
+
+ if (fs.existsSync(outPath)) {
+ let data = fs.readFileSync(outPath, 'utf8');
+ data = data.replace("var jspb = require('google-protobuf');", googProtobufModule);
+ data = data.replace('var goog = jspb;', '');
+ fs.writeFileSync(outPath, data);
+ }
+ });
+};
+
+const makeProtoCollection = function (
+ descriptorBinPath: string,
+ protoPath: string,
+ importPath: string
+) {
+ const outputCollectionName = path.extname(require.resolve("./ts_proto_collection.template")) === ".ts" ? "collection.ts" : "collection.js";
+ generateTemplate(`${protoPath}/${outputCollectionName}`, descriptorBinPath, require.resolve("./ts_proto_collection.template"), importPath)
+};
+
+protoc(protos, outDir).then(() => {
+ makeProtoCollection(
+ path.join(outDir, 'descriptor.bin'),
+ outDir,
+ 'pigweedjs/protos'
+ );
+});
diff --git a/pw_protobuf_compiler/ts/codegen/template_replacement.bzl b/pw_protobuf_compiler/ts/codegen/template_replacement.bzl
deleted file mode 100644
index 41d62d066..000000000
--- a/pw_protobuf_compiler/ts/codegen/template_replacement.bzl
+++ /dev/null
@@ -1,61 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-
-"""Utility for generating TS code with string replacement."""
-
-load("@build_bazel_rules_nodejs//:providers.bzl", "run_node")
-
-def _template_replacement_impl(ctx):
- output_file = ctx.actions.declare_file(ctx.attr.output_file)
- descriptor_data = ctx.files.descriptor_data[0]
- template_file = ctx.files.template_file[0]
-
- run_node(
- ctx,
- executable = "_template_replacement_bin",
- inputs = [descriptor_data, template_file],
- outputs = [output_file],
- arguments = [
- "--template",
- template_file.path,
- "--descriptor_data",
- descriptor_data.path,
- "--output",
- output_file.path,
- "--proto_root_dir",
- ctx.attr.proto_root_dir,
- ],
- )
-
- return [DefaultInfo(files = depset([output_file]))]
-
-template_replacement = rule(
- implementation = _template_replacement_impl,
- attrs = {
- "_template_replacement_bin": attr.label(
- executable = True,
- cfg = "exec",
- default = Label("@//pw_protobuf_compiler/ts/codegen:template_replacement_bin"),
- ),
- "descriptor_data": attr.label(
- allow_files = [".proto.bin"],
- ),
- "proto_root_dir": attr.string(mandatory = True),
- "output_file": attr.string(mandatory = True),
- "template_file": attr.label(
- allow_files = [".ts"],
- default = Label("@//pw_protobuf_compiler/ts:ts_proto_collection_template"),
- ),
- },
-)
diff --git a/pw_protobuf_compiler/ts/codegen/template_replacement.ts b/pw_protobuf_compiler/ts/codegen/template_replacement.ts
index 31186512a..7f2ef0517 100644
--- a/pw_protobuf_compiler/ts/codegen/template_replacement.ts
+++ b/pw_protobuf_compiler/ts/codegen/template_replacement.ts
@@ -1,5 +1,4 @@
-#!/usr/bin/env node
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -14,66 +13,43 @@
// the License.
import * as fs from 'fs';
-import {ArgumentParser} from 'argparse';
import {FileDescriptorSet} from 'google-protobuf/google/protobuf/descriptor_pb';
-// import {Message} from 'google-protobuf';
-
-const parser = new ArgumentParser({});
-parser.add_argument('--output', {
- action: 'store',
- required: true,
- type: String,
-});
-parser.add_argument('--descriptor_data', {
- action: 'store',
- required: true,
- type: String,
-});
-parser.add_argument('--template', {
- action: 'store',
- required: true,
- type: String,
-});
-parser.add_argument('--proto_root_dir', {
- action: 'store',
- required: true,
- type: String,
-});
-
-const args = parser.parse_args();
-let template = fs.readFileSync(args.template).toString();
function buildModulePath(rootDir: string, fileName: string): string {
const name = `${rootDir}/${fileName}`;
return name.replace(/\.proto$/, '_pb');
}
-const descriptorSetBinary = fs.readFileSync(args.descriptor_data);
-const base64DescriptorSet = descriptorSetBinary.toString('base64');
-const fileDescriptorSet = FileDescriptorSet.deserializeBinary(
- new Buffer(descriptorSetBinary)
-);
-
-const imports = [];
-const moduleDictionary = [];
-const fileList = fileDescriptorSet.getFileList();
-for (let i = 0; i < fileList.length; i++) {
- const file = fileList[i];
- const modulePath = buildModulePath(args.proto_root_dir, file.getName()!);
- const moduleName = 'proto_' + i;
- imports.push(`import * as ${moduleName} from '${modulePath}';`);
- const key = file.getName()!;
- moduleDictionary.push(`['${key}', ${moduleName}],`);
+export default function generateTemplate(outputPath: string, descriptorDataPath: string, templatePath: string, protoRootDir: string) {
+ let template = fs.readFileSync(templatePath).toString();
+
+ const descriptorSetBinary = fs.readFileSync(descriptorDataPath);
+ const base64DescriptorSet = descriptorSetBinary.toString('base64');
+ const fileDescriptorSet = FileDescriptorSet.deserializeBinary(
+ Buffer.from(descriptorSetBinary)
+ );
+
+ const imports = [];
+ const moduleDictionary = [];
+ const fileList = fileDescriptorSet.getFileList();
+ for (let i = 0; i < fileList.length; i++) {
+ const file = fileList[i];
+ const modulePath = buildModulePath(".", file.getName()!);
+ const moduleName = 'proto_' + i;
+ imports.push(`import * as ${moduleName} from '${modulePath}';`);
+ const key = file.getName()!;
+ moduleDictionary.push(`['${key}', ${moduleName}],`);
+ }
+
+ template = template.replace(
+ '{TEMPLATE_descriptor_binary}',
+ base64DescriptorSet
+ );
+ template = template.replace('// TEMPLATE_proto_imports', imports.join('\n'));
+ template = template.replace(
+ '// TEMPLATE_module_map',
+ moduleDictionary.join('\n')
+ );
+
+ fs.writeFileSync(outputPath, template);
}
-
-template = template.replace(
- '{TEMPLATE_descriptor_binary}',
- base64DescriptorSet
-);
-template = template.replace('// TEMPLATE_proto_imports', imports.join('\n'));
-template = template.replace(
- '// TEMPLATE_module_map',
- moduleDictionary.join('\n')
-);
-
-fs.writeFileSync(args.output, template);
diff --git a/pw_protobuf_compiler/ts/package.json b/pw_protobuf_compiler/ts/package.json
deleted file mode 100644
index 8cc2e0840..000000000
--- a/pw_protobuf_compiler/ts/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "@pigweed/pw_protobuf_compiler",
- "version": "1.0.0",
- "main": "index.js",
- "license": "Apache-2.0",
- "dependencies": {
- "@bazel/jasmine": "^4.1.0",
- "@types/argparse": "^2.0.10",
- "@types/google-protobuf": "^3.15.5",
- "@types/jasmine": "^3.9.0",
- "@types/node": "^16.10.2",
- "argparse": "^2.0.1",
- "base64-js": "^1.5.1",
- "google-protobuf": "^3.19.0",
- "jasmine": "^3.9.0",
- "jasmine-core": "^3.9.0"
- }
-}
diff --git a/pw_protobuf_compiler/ts/proto_collection.ts b/pw_protobuf_compiler/ts/proto_collection.ts
index 74a936583..fb31b3a96 100644
--- a/pw_protobuf_compiler/ts/proto_collection.ts
+++ b/pw_protobuf_compiler/ts/proto_collection.ts
@@ -15,41 +15,42 @@
/** Tools for compiling and importing Javascript protos on the fly. */
import {Message} from 'google-protobuf';
-import {FileDescriptorSet} from 'google-protobuf/google/protobuf/descriptor_pb';
+import {DescriptorProto, FileDescriptorSet} from 'google-protobuf/google/protobuf/descriptor_pb';
export type MessageCreator = new () => Message;
class MessageMap extends Map<string, MessageCreator> {}
+class MessageDescriptorMap extends Map<string, DescriptorProto> { }
export class ModuleMap extends Map<string, any> {}
/**
* A wrapper class of protocol buffer modules to provide convenience methods.
*/
export class ProtoCollection {
- private messages: MessageMap;
+ private messages: MessageMap = new MessageMap();
+ private messageDescriptors: MessageDescriptorMap = new MessageDescriptorMap();
constructor(
readonly fileDescriptorSet: FileDescriptorSet,
modules: ModuleMap
) {
- this.messages = this.mapMessages(fileDescriptorSet, modules);
+ this.mapMessages(fileDescriptorSet, modules);
}
/**
* Creates a map between message identifier "{packageName}.{messageName}"
- * and the Message class.
+ * and the Message class and also the associated DescriptorProto.
*/
- private mapMessages(set: FileDescriptorSet, mods: ModuleMap): MessageMap {
- const messages = new MessageMap();
+ private mapMessages(set: FileDescriptorSet, mods: ModuleMap): void {
for (const fileDescriptor of set.getFileList()) {
const mod = mods.get(fileDescriptor.getName()!)!;
for (const messageType of fileDescriptor.getMessageTypeList()) {
const fullName =
fileDescriptor.getPackage()! + '.' + messageType.getName();
const message = mod[messageType.getName()!];
- messages.set(fullName, message);
+ this.messages.set(fullName, message);
+ this.messageDescriptors.set(fullName, messageType);
}
}
- return messages;
}
/**
@@ -61,4 +62,14 @@ export class ProtoCollection {
getMessageCreator(identifier: string): MessageCreator | undefined {
return this.messages.get(identifier);
}
+
+ /**
+ * Finds the DescriptorProto referenced by the identifier.
+ *
+ * @param identifier String identifier of the form
+ * "{packageName}.{messageName}" i.e: "pw.rpc.test.NewMessage".
+ */
+ getDescriptorProto(identifier: string): DescriptorProto | undefined {
+ return this.messageDescriptors.get(identifier);
+ }
}
diff --git a/pw_protobuf_compiler/ts/ts_proto_collection.bzl b/pw_protobuf_compiler/ts/ts_proto_collection.bzl
deleted file mode 100644
index fe9af959e..000000000
--- a/pw_protobuf_compiler/ts/ts_proto_collection.bzl
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-
-"""Builds a proto collection."""
-
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@//pw_protobuf_compiler/ts/codegen:template_replacement.bzl", "template_replacement")
-
-def _lib(name, proto_library, js_proto_library):
- js_proto_library_name = Label(js_proto_library).name
- proto_root_dir = js_proto_library_name + "/" + js_proto_library_name + "_pb"
- template_name = name + "_template"
- template_replacement(
- name = template_name,
- descriptor_data = proto_library,
- proto_root_dir = proto_root_dir,
- output_file = "generated/ts_proto_collection.ts",
- )
-
- ts_library(
- name = name + "_lib",
- package_name = name,
- module_name = name,
- srcs = [template_name],
- deps = [
- js_proto_library,
- "@//pw_protobuf_compiler/ts:pw_protobuf_compiler",
- "@//pw_rpc/ts:packet_proto_tspb",
- "@npm//@types/google-protobuf",
- "@npm//@types/node",
- "@npm//base64-js",
- ],
- )
-
- native.filegroup(
- name = name + "_esm",
- srcs = [name + "_lib"],
- output_group = "es6_sources",
- )
-
-def ts_proto_collection(name, proto_library, js_proto_library):
- _lib(name, proto_library, js_proto_library)
- js_library(
- name = name,
- package_name = "@pigweed/" + name + "/pw_protobuf_compiler",
- deps = [name + "_lib", name + "_esm"],
- )
diff --git a/pw_protobuf_compiler/ts/ts_proto_collection.template.ts b/pw_protobuf_compiler/ts/ts_proto_collection.template.ts
index 7ec60fdf6..ff0fcefcb 100644
--- a/pw_protobuf_compiler/ts/ts_proto_collection.template.ts
+++ b/pw_protobuf_compiler/ts/ts_proto_collection.template.ts
@@ -17,7 +17,7 @@
import {
ProtoCollection as Base,
ModuleMap,
-} from '@pigweed/pw_protobuf_compiler';
+} from 'pigweedjs/pw_protobuf_compiler';
import {FileDescriptorSet} from 'google-protobuf/google/protobuf/descriptor_pb';
import * as base64 from 'base64-js';
diff --git a/pw_protobuf_compiler/ts/ts_proto_collection_test.ts b/pw_protobuf_compiler/ts/ts_proto_collection_test.ts
index c20d766c8..d7206c027 100644
--- a/pw_protobuf_compiler/ts/ts_proto_collection_test.ts
+++ b/pw_protobuf_compiler/ts/ts_proto_collection_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,12 +12,11 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
-import 'jasmine';
+/* eslint-env browser */
-import {Message} from 'test_protos_tspb/test_protos_tspb_pb/pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test_pb';
+import {Message} from 'pigweedjs/protos/pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test_pb';
-import {ProtoCollection} from 'test_proto_collection/generated/ts_proto_collection';
+import {ProtoCollection} from 'pigweedjs/protos/collection';
describe('ProtoCollection', () => {
it('getMessageType returns message', () => {
@@ -35,4 +34,20 @@ describe('ProtoCollection', () => {
fetched = lib.getMessageCreator('pw.test1.Garbage');
expect(fetched).toBeUndefined();
});
+
+ it('getDescriptorProto returns descriptor', () => {
+ const lib = new ProtoCollection();
+
+ const fetched = lib.getDescriptorProto('pw.protobuf_compiler.test.Message');
+ expect(fetched.getFieldList()[0].getName()).toEqual("field");
+ });
+
+ it('getDescriptorProto for invalid identifier returns undefined', () => {
+ const lib = new ProtoCollection();
+
+ let fetched = lib.getMessageCreator('pw');
+ expect(fetched).toBeUndefined();
+ fetched = lib.getMessageCreator('pw.test1.Garbage');
+ expect(fetched).toBeUndefined();
+ });
});
diff --git a/pw_protobuf_compiler/ts/tsconfig.json b/pw_protobuf_compiler/ts/tsconfig.json
deleted file mode 100644
index 4ddd637e9..000000000
--- a/pw_protobuf_compiler/ts/tsconfig.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "compilerOptions": {
- "allowUnreachableCode": false,
- "allowUnusedLabels": false,
- "declaration": true,
- "forceConsistentCasingInFileNames": true,
- "lib": [
- "es2018",
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "module": "commonjs",
- "noEmitOnError": true,
- "noFallthroughCasesInSwitch": true,
- "noImplicitReturns": true,
- "pretty": true,
- "sourceMap": true,
- "strict": true,
- "target": "es2018",
- "jsx": "react",
- "plugins": [
- {
- "name": "@bazel/tsetse",
- "disabledRules": [
- "must-use-promises"
- ]
- }
- ]
- },
- "exclude": [
- "node_modules"
- ]
-}
diff --git a/pw_protobuf_compiler/ts/yarn.lock b/pw_protobuf_compiler/ts/yarn.lock
deleted file mode 100644
index 50afb2b90..000000000
--- a/pw_protobuf_compiler/ts/yarn.lock
+++ /dev/null
@@ -1,497 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@bazel/jasmine@^4.1.0":
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-4.4.0.tgz#4f3ec3e1cc62824c9ed222e937c6ad368be22b28"
- integrity sha512-GkpRvD6Z880g6AsLzoc/UPTPdaXsGzAUGWGefGHqWsO+tBCWmi2V8WL0yc0N6edu6QLi4EI0fxN9obH1FsU0MQ==
- dependencies:
- c8 "~7.5.0"
- jasmine-reporters "~2.4.0"
-
-"@bcoe/v8-coverage@^0.2.3":
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
- integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
-
-"@istanbuljs/schema@^0.1.2":
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
- integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
-
-"@types/argparse@^2.0.10":
- version "2.0.10"
- resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-2.0.10.tgz#664e84808accd1987548d888b9d21b3e9c996a6c"
- integrity sha512-C4wahC3gz3vQtvPazrJ5ONwmK1zSDllQboiWvpMM/iOswCYfBREFnjFbq/iWKIVOCl8+m5Pk6eva6/ZSsDuIGA==
-
-"@types/google-protobuf@^3.15.5":
- version "3.15.5"
- resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.5.tgz#644b2be0f5613b1f822c70c73c6b0e0b5b5fa2ad"
- integrity sha512-6bgv24B+A2bo9AfzReeg5StdiijKzwwnRflA8RLd1V4Yv995LeTmo0z69/MPbBDFSiZWdZHQygLo/ccXhMEDgw==
-
-"@types/is-windows@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-1.0.0.tgz#1011fa129d87091e2f6faf9042d6704cdf2e7be0"
- integrity sha512-tJ1rq04tGKuIJoWIH0Gyuwv4RQ3+tIu7wQrC0MV47raQ44kIzXSSFKfrxFUOWVRvesoF7mrTqigXmqoZJsXwTg==
-
-"@types/istanbul-lib-coverage@^2.0.1":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
- integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
-
-"@types/jasmine@^3.9.0":
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.10.0.tgz#b55e7184102ef320c4e8fb3d64ebfac309f60bf7"
- integrity sha512-sPHWB05cYGt7GXFkkn+03VL1533abxiA5bE8PKdr0nS3cEsOXCGjMk0sgqVwY6xkiwajoAiN3zc/7zDeXip3Pw==
-
-"@types/node@^16.10.2":
- version "16.11.3"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.3.tgz#fad0b069ec205b0e81429c805d306d2c12e26be1"
- integrity sha512-aIYL9Eemcecs1y77XzFGiSc+FdfN58k4J23UEe6+hynf4Wd9g4DzQPwIKL080vSMuubFqy2hWwOzCtJdc6vFKw==
-
-ansi-regex@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
- integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
- dependencies:
- color-convert "^2.0.1"
-
-argparse@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
- integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
-
-balanced-match@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
- integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-js@^1.5.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
- integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-c8@~7.5.0:
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/c8/-/c8-7.5.0.tgz#a69439ab82848f344a74bb25dc5dd4e867764481"
- integrity sha512-GSkLsbvDr+FIwjNSJ8OwzWAyuznEYGTAd1pzb/Kr0FMLuV4vqYJTyjboDTwmlUNAG6jAU3PFWzqIdKrOt1D8tw==
- dependencies:
- "@bcoe/v8-coverage" "^0.2.3"
- "@istanbuljs/schema" "^0.1.2"
- find-up "^5.0.0"
- foreground-child "^2.0.0"
- furi "^2.0.0"
- istanbul-lib-coverage "^3.0.0"
- istanbul-lib-report "^3.0.0"
- istanbul-reports "^3.0.2"
- rimraf "^3.0.0"
- test-exclude "^6.0.0"
- v8-to-istanbul "^7.1.0"
- yargs "^16.0.0"
- yargs-parser "^20.0.0"
-
-cliui@^7.0.2:
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
- integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^7.0.0"
-
-color-convert@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
- integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
- dependencies:
- color-name "~1.1.4"
-
-color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-
-convert-source-map@^1.6.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
- integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
- dependencies:
- safe-buffer "~5.1.1"
-
-cross-spawn@^7.0.0:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
- integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
- dependencies:
- path-key "^3.1.0"
- shebang-command "^2.0.0"
- which "^2.0.1"
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-escalade@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
- integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-find-up@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
- integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
- dependencies:
- locate-path "^6.0.0"
- path-exists "^4.0.0"
-
-foreground-child@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53"
- integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==
- dependencies:
- cross-spawn "^7.0.0"
- signal-exit "^3.0.2"
-
-fs.realpath@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
- integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-
-furi@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/furi/-/furi-2.0.0.tgz#13d85826a1af21acc691da6254b3888fc39f0b4a"
- integrity sha512-uKuNsaU0WVaK/vmvj23wW1bicOFfyqSsAIH71bRZx8kA4Xj+YCHin7CJKJJjkIsmxYaPFLk9ljmjEyB7xF7WvQ==
- dependencies:
- "@types/is-windows" "^1.0.0"
- is-windows "^1.0.2"
-
-get-caller-file@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
- integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
- integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-google-protobuf@^3.19.0:
- version "3.19.0"
- resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.0.tgz#97f474323c92f19fd6737af1bb792e396991e0b8"
- integrity sha512-qXGAiv3OOlaJXJNeKOBKxbBAwjsxzhx+12ZdKOkZTsqsRkyiQRmr/nBkAkqnuQ8cmA9X5NVXvObQTpHVnXE2DQ==
-
-has-flag@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
- integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-html-escaper@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
- integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
-
-inflight@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
- integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
- dependencies:
- once "^1.3.0"
- wrappy "1"
-
-inherits@2:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-windows@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
- integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
-
-isexe@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
- integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
-
-istanbul-lib-coverage@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
- integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
-
-istanbul-lib-report@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
- integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==
- dependencies:
- istanbul-lib-coverage "^3.0.0"
- make-dir "^3.0.0"
- supports-color "^7.1.0"
-
-istanbul-reports@^3.0.2:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.5.tgz#a2580107e71279ea6d661ddede929ffc6d693384"
- integrity sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ==
- dependencies:
- html-escaper "^2.0.0"
- istanbul-lib-report "^3.0.0"
-
-jasmine-core@^3.9.0, jasmine-core@~3.10.0:
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.10.0.tgz#1e3e4053d954eb6d0bfbb3eb859bb21a5655de45"
- integrity sha512-XWGaJ25RUdOQnjGiLoQa9QG/R4u1e9Bk4uhLdn9F4JCBco84L4SKM52bxci4vWTSUzhmhuHNAkAHFN/6Cox9wQ==
-
-jasmine-reporters@~2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.4.0.tgz#708c17ae70ba6671e3a930bb1b202aab80a31409"
- integrity sha512-jxONSrBLN1vz/8zCx5YNWQSS8iyDAlXQ5yk1LuqITe4C6iXCDx5u6Q0jfNtkKhL4qLZPe69fL+AWvXFt9/x38w==
- dependencies:
- mkdirp "^0.5.1"
- xmldom "^0.5.0"
-
-jasmine@^3.9.0:
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.10.0.tgz#acd3cd560a9d20d8fdad6bd2dd05867d188503f3"
- integrity sha512-2Y42VsC+3CQCTzTwJezOvji4qLORmKIE0kwowWC+934Krn6ZXNQYljiwK5st9V3PVx96BSiDYXSB60VVah3IlQ==
- dependencies:
- glob "^7.1.6"
- jasmine-core "~3.10.0"
-
-locate-path@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
- integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
- dependencies:
- p-locate "^5.0.0"
-
-make-dir@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
- integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
- dependencies:
- semver "^6.0.0"
-
-minimatch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimist@^1.2.5:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
- integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
-
-mkdirp@^0.5.1:
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
- integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
- dependencies:
- minimist "^1.2.5"
-
-once@^1.3.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
- integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
- dependencies:
- wrappy "1"
-
-p-limit@^3.0.2:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
- integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
- dependencies:
- yocto-queue "^0.1.0"
-
-p-locate@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
- integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
- dependencies:
- p-limit "^3.0.2"
-
-path-exists@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
- integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-is-absolute@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
- integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-
-path-key@^3.1.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
- integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-
-require-directory@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
- integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
-
-rimraf@^3.0.0:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
-safe-buffer@~5.1.1:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
- integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
-semver@^6.0.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
- integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-shebang-command@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
- integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
- dependencies:
- shebang-regex "^3.0.0"
-
-shebang-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
- integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-
-signal-exit@^3.0.2:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
- integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
-
-source-map@^0.7.3:
- version "0.7.3"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
- integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
-
-string-width@^4.1.0, string-width@^4.2.0:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-supports-color@^7.1.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
- integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
- dependencies:
- has-flag "^4.0.0"
-
-test-exclude@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
- integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
- dependencies:
- "@istanbuljs/schema" "^0.1.2"
- glob "^7.1.4"
- minimatch "^3.0.4"
-
-v8-to-istanbul@^7.1.0:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1"
- integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==
- dependencies:
- "@types/istanbul-lib-coverage" "^2.0.1"
- convert-source-map "^1.6.0"
- source-map "^0.7.3"
-
-which@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
- integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
- dependencies:
- isexe "^2.0.0"
-
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrappy@1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
- integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-
-xmldom@^0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e"
- integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==
-
-y18n@^5.0.5:
- version "5.0.8"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
- integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yargs-parser@^20.0.0, yargs-parser@^20.2.2:
- version "20.2.9"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
- integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
-yargs@^16.0.0:
- version "16.2.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
- integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
- dependencies:
- cliui "^7.0.2"
- escalade "^3.1.1"
- get-caller-file "^2.0.5"
- require-directory "^2.1.1"
- string-width "^4.2.0"
- y18n "^5.0.5"
- yargs-parser "^20.2.2"
-
-yocto-queue@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
- integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
diff --git a/pw_random/BUILD.bazel b/pw_random/BUILD.bazel
index e41d610df..c347a903a 100644
--- a/pw_random/BUILD.bazel
+++ b/pw_random/BUILD.bazel
@@ -29,6 +29,9 @@ pw_cc_library(
"public/pw_random/xor_shift.h",
],
includes = ["public"],
+ deps = [
+ "//pw_assert",
+ ],
)
pw_cc_test(
@@ -36,6 +39,7 @@ pw_cc_test(
srcs = ["xor_shift_test.cc"],
deps = [
":pw_random",
+ "//pw_assert",
"//pw_bytes",
"//pw_unit_test",
],
diff --git a/pw_random/BUILD.gn b/pw_random/BUILD.gn
index 8a440f8b7..0563fc2e2 100644
--- a/pw_random/BUILD.gn
+++ b/pw_random/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_fuzzer/fuzzer.gni")
import("$dir_pw_unit_test/test.gni")
config("default_config") {
@@ -29,20 +30,48 @@ pw_source_set("pw_random") {
"public/pw_random/xor_shift.h",
]
public_deps = [
+ dir_pw_assert,
dir_pw_bytes,
+ dir_pw_span,
dir_pw_status,
]
}
+pw_source_set("fuzzer_generator") {
+ public = [ "public/pw_random/fuzzer.h" ]
+ public_deps = [
+ ":pw_random",
+ dir_pw_fuzzer,
+ ]
+}
+
pw_test_group("tests") {
- tests = [ ":xor_shift_star_test" ]
+ tests = [
+ ":xor_shift_star_test",
+ ":get_int_bounded_fuzzer_test",
+ ]
+}
+
+group("fuzzers") {
+ deps = [ ":get_int_bounded_fuzzer" ]
}
pw_test("xor_shift_star_test") {
- deps = [ ":pw_random" ]
+ deps = [
+ ":pw_random",
+ dir_pw_assert,
+ ]
sources = [ "xor_shift_test.cc" ]
}
+pw_fuzzer("get_int_bounded_fuzzer") {
+ sources = [ "get_int_bounded_fuzzer.cc" ]
+ deps = [
+ ":fuzzer_generator",
+ ":pw_random",
+ ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_random/CMakeLists.txt b/pw_random/CMakeLists.txt
index 776cccdac..c258e3dc9 100644
--- a/pw_random/CMakeLists.txt
+++ b/pw_random/CMakeLists.txt
@@ -14,7 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_random
+pw_add_library(pw_random INTERFACE
HEADERS
public/pw_random/random.h
public/pw_random/xor_shift.h
@@ -22,15 +22,17 @@ pw_add_module_library(pw_random
public
PUBLIC_DEPS
pw_bytes
- pw_polyfill.cstddef
- pw_polyfill.span
+ pw_span
pw_status
+ PRIVATE_DEPS
+ pw_assert
)
pw_add_test(pw_random.xor_shift_star_test
SOURCES
xor_shift_test.cc
- DEPS
+ PRIVATE_DEPS
+ pw_assert
pw_random
GROUPS
modules
diff --git a/pw_random/docs.rst b/pw_random/docs.rst
index fd4ba167e..c6b5fe260 100644
--- a/pw_random/docs.rst
+++ b/pw_random/docs.rst
@@ -20,13 +20,14 @@ easier to manage.
Using RandomGenerator
=====================
There's two sides to a RandomGenerator; the input, and the output. The outputs
-are relatively straightforward; ``GetInt()`` randomizes the passed integer
-reference, and ``Get()`` dumps random values into a the passed span. The inputs
-are in the form of the ``InjectEntropy*()`` functions. These functions are used
-to "seed" the random generator. In some implementations, this can simply be
-resetting the seed of a PRNG, while in others it might directly populate a
-limited buffer of random data. In all cases, entropy injection is used to
-improve the randomness of calls to ``Get*()``.
+are relatively straightforward; ``GetInt(T&)`` randomizes the passed integer
+reference, ``GetInt(T&, T exclusive_upper_bound)`` produces a random integer
+less than ``exclusive_upper_bound``, and ``Get()`` dumps random values into the
+passed span. The inputs are in the form of the ``InjectEntropy*()`` functions.
+These functions are used to "seed" the random generator. In some
+implementations, this can simply be resetting the seed of a PRNG, while in
+others it might directly populate a limited buffer of random data. In all cases,
+entropy injection is used to improve the randomness of calls to ``Get*()``.
It might not be easy to find sources of entropy in a system, but in general a
few bits of noise from ADCs or other highly variable inputs can be accumulated
diff --git a/pw_random/get_int_bounded_fuzzer.cc b/pw_random/get_int_bounded_fuzzer.cc
new file mode 100644
index 000000000..1fdf5afcc
--- /dev/null
+++ b/pw_random/get_int_bounded_fuzzer.cc
@@ -0,0 +1,59 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <cstddef>
+#include <cstdint>
+#include <limits>
+
+#include "pw_fuzzer/fuzzed_data_provider.h"
+#include "pw_random/fuzzer.h"
+
+namespace {
+enum class IntegerType : uint8_t {
+ kUint8,
+ kUint16,
+ kUint32,
+ kUint64,
+ kMaxValue = kUint64,
+};
+
+template <typename T>
+void FuzzGetInt(FuzzedDataProvider* provider) {
+ pw::random::FuzzerRandomGenerator rng(provider);
+ T value = 0;
+ T bound =
+ provider->ConsumeIntegralInRange<T>(1, std::numeric_limits<T>::max());
+ rng.GetInt(value, bound);
+ PW_CHECK(value < bound);
+}
+} // namespace
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ FuzzedDataProvider provider(data, size);
+ switch (provider.ConsumeEnum<IntegerType>()) {
+ case IntegerType::kUint8:
+ FuzzGetInt<uint8_t>(&provider);
+ break;
+ case IntegerType::kUint16:
+ FuzzGetInt<uint16_t>(&provider);
+ break;
+ case IntegerType::kUint32:
+ FuzzGetInt<uint32_t>(&provider);
+ break;
+ case IntegerType::kUint64:
+ FuzzGetInt<uint64_t>(&provider);
+ break;
+ }
+ return 0;
+}
diff --git a/pw_random/public/pw_random/fuzzer.h b/pw_random/public/pw_random/fuzzer.h
new file mode 100644
index 000000000..098d86d30
--- /dev/null
+++ b/pw_random/public/pw_random/fuzzer.h
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_fuzzer/fuzzed_data_provider.h"
+#include "pw_random/random.h"
+
+namespace pw::random {
+
+// FuzzerRandomGenerator is intended to be used as the generator in fuzz tests.
+// It returns data from the fuzzer input.
+class FuzzerRandomGenerator : public RandomGenerator {
+ public:
+ FuzzerRandomGenerator(FuzzedDataProvider* provider) : provider_(provider) {}
+ void Get(ByteSpan dest) override {
+ provider_->ConsumeData(dest.data(), dest.size());
+ }
+
+ void InjectEntropyBits(uint32_t /*data*/,
+ uint_fast8_t /*num_bits*/) override {}
+
+ private:
+ FuzzedDataProvider* provider_;
+};
+
+} // namespace pw::random
diff --git a/pw_random/public/pw_random/random.h b/pw_random/public/pw_random/random.h
index 388d88a50..4be398c05 100644
--- a/pw_random/public/pw_random/random.h
+++ b/pw_random/public/pw_random/random.h
@@ -13,11 +13,14 @@
// the License.
#pragma once
+#include <climits>
#include <cstddef>
#include <cstdint>
-#include <span>
+#include <limits>
+#include "pw_assert/check.h"
#include "pw_bytes/span.h"
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
namespace pw::random {
@@ -25,25 +28,57 @@ namespace pw::random {
// A random generator uses injected entropy to generate random values. Many of
// the guarantees for this interface are provided at the level of the
// implementations. In general:
+// * DO assume a generator will always succeed.
// * DO NOT assume a generator is cryptographically secure.
// * DO NOT assume uniformity of generated data.
-// * DO assume a generator can be exhausted.
class RandomGenerator {
public:
virtual ~RandomGenerator() = default;
template <class T>
- StatusWithSize GetInt(T& dest) {
+ void GetInt(T& dest) {
static_assert(std::is_integral<T>::value,
"Use Get() for non-integral types");
- return Get({reinterpret_cast<std::byte*>(&dest), sizeof(T)});
+ Get({reinterpret_cast<std::byte*>(&dest), sizeof(T)});
}
- // Populates the destination buffer with a randomly generated value. Returns:
- // OK - Successfully filled the destination buffer with random data.
- // RESOURCE_EXHAUSTED - Filled the buffer with the returned number of bytes.
- // The returned size is number of complete bytes with random data.
- virtual StatusWithSize Get(ByteSpan dest) = 0;
+ // Calculate a uniformly distributed random number in the range [0,
+ // exclusive_upper_bound). This avoids modulo biasing. Uniformity is only
+ // guaranteed if the underlying generator generates uniform data. Uniformity
+ // is achieved by generating new random numbers until one is generated in the
+ // desired range (with optimizations).
+ template <class T>
+ void GetInt(T& dest, const T& exclusive_upper_bound) {
+ static_assert(std::is_unsigned_v<T>, "T must be an unsigned integer");
+ PW_DCHECK(exclusive_upper_bound != 0);
+
+ if (exclusive_upper_bound < 2) {
+ dest = 0;
+ return;
+ }
+
+ const uint8_t leading_zeros_in_upper_bound =
+ CountLeadingZeros(exclusive_upper_bound);
+
+ // Create a mask that discards the higher order bits of the random number.
+ const T mask =
+ std::numeric_limits<T>::max() >> leading_zeros_in_upper_bound;
+
+ // This loop should end fairly soon. It discards all the values that aren't
+ // below exclusive_upper_bound. The probability of values being greater or
+ // equal than exclusive_upper_bound is less than 1/2, which means that the
+ // expected amount of iterations is less than 2.
+ while (true) {
+ GetInt(dest);
+ dest &= mask;
+ if (dest < exclusive_upper_bound) {
+ return;
+ }
+ }
+ }
+
+ // Populates the destination buffer with a randomly generated value.
+ virtual void Get(ByteSpan dest) = 0;
// Injects entropy into the pool. `data` may have up to 32 bits of random
// entropy. If the number of bits of entropy is less than 32, entropy is
@@ -56,6 +91,24 @@ class RandomGenerator {
InjectEntropyBits(std::to_integer<uint32_t>(b), /*num_bits=*/8);
}
}
+
+ private:
+ template <class T>
+ uint8_t CountLeadingZeros(T value) {
+ if constexpr (std::is_same_v<T, unsigned>) {
+ return static_cast<uint8_t>(__builtin_clz(value));
+ } else if constexpr (std::is_same_v<T, unsigned long>) {
+ return static_cast<uint8_t>(__builtin_clzl(value));
+ } else if constexpr (std::is_same_v<T, unsigned long long>) {
+ return static_cast<uint8_t>(__builtin_clzll(value));
+ } else {
+ static_assert(sizeof(T) < sizeof(unsigned));
+ // __builtin_clz returns the count of leading zeros in an unsigned , so we
+ // need to subtract the size difference of T in bits.
+ return static_cast<uint8_t>(__builtin_clz(value) -
+ ((sizeof(unsigned) - sizeof(T)) * CHAR_BIT));
+ }
+ }
};
} // namespace pw::random
diff --git a/pw_random/public/pw_random/xor_shift.h b/pw_random/public/pw_random/xor_shift.h
index 4b303194f..2e98e9b65 100644
--- a/pw_random/public/pw_random/xor_shift.h
+++ b/pw_random/public/pw_random/xor_shift.h
@@ -13,12 +13,13 @@
// the License.
#pragma once
+#include <algorithm>
#include <cstdint>
#include <cstring>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_random/random.h"
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
namespace pw::random {
@@ -37,16 +38,13 @@ class XorShiftStarRng64 : public RandomGenerator {
// This generator uses entropy-seeded PRNG to never exhaust its random number
// pool.
- StatusWithSize Get(ByteSpan dest) final {
- const size_t bytes_written = dest.size_bytes();
+ void Get(ByteSpan dest) final {
while (!dest.empty()) {
uint64_t random = Regenerate();
size_t copy_size = std::min(dest.size_bytes(), sizeof(state_));
std::memcpy(dest.data(), &random, copy_size);
dest = dest.subspan(copy_size);
}
-
- return StatusWithSize(bytes_written);
}
// Entropy is injected by rotating the state by the number of entropy bits
@@ -64,7 +62,8 @@ class XorShiftStarRng64 : public RandomGenerator {
uint64_t untouched_state = state_ >> (kNumStateBits - num_bits);
state_ = untouched_state | (state_ << num_bits);
// Zero-out all irrelevant bits, then XOR entropy into state.
- uint32_t mask = (1 << num_bits) - 1;
+ uint32_t mask =
+ static_cast<uint32_t>((static_cast<uint64_t>(1) << num_bits) - 1);
state_ ^= (data & mask);
}
diff --git a/pw_random/xor_shift_test.cc b/pw_random/xor_shift_test.cc
index a65358427..6140b8c5d 100644
--- a/pw_random/xor_shift_test.cc
+++ b/pw_random/xor_shift_test.cc
@@ -17,8 +17,10 @@
#include <cstddef>
#include <cstdint>
#include <cstdio>
+#include <limits>
#include "gtest/gtest.h"
+#include "pw_assert/config.h"
namespace pw::random {
namespace {
@@ -44,7 +46,7 @@ TEST(XorShiftStarRng64, ValidateSeries1) {
XorShiftStarRng64 rng(seed1);
for (size_t i = 0; i < result1_count; ++i) {
uint64_t val = 0;
- EXPECT_EQ(rng.GetInt(val).status(), OkStatus());
+ rng.GetInt(val);
EXPECT_EQ(val, result1[i]);
}
}
@@ -53,7 +55,7 @@ TEST(XorShiftStarRng64, ValidateSeries2) {
XorShiftStarRng64 rng(seed2);
for (size_t i = 0; i < result2_count; ++i) {
uint64_t val = 0;
- EXPECT_EQ(rng.GetInt(val).status(), OkStatus());
+ rng.GetInt(val);
EXPECT_EQ(val, result2[i]);
}
}
@@ -62,24 +64,32 @@ TEST(XorShiftStarRng64, InjectEntropyBits) {
XorShiftStarRng64 rng(seed1);
uint64_t val = 0;
rng.InjectEntropyBits(0x1, 1);
- EXPECT_EQ(rng.GetInt(val).status(), OkStatus());
+ rng.GetInt(val);
EXPECT_NE(val, result1[0]);
}
+TEST(XorShiftStarRng64, Inject32BitsEntropy) {
+ XorShiftStarRng64 rng_1(seed1);
+ uint64_t first_val = 0;
+ rng_1.InjectEntropyBits(0x12345678, 32);
+ rng_1.GetInt(first_val);
+ EXPECT_NE(first_val, result1[0]);
+}
+
// Ensure injecting the same entropy integer, but different bit counts causes
// the randomly generated number to differ.
TEST(XorShiftStarRng64, EntropyBitCount) {
XorShiftStarRng64 rng_1(seed1);
uint64_t first_val = 0;
rng_1.InjectEntropyBits(0x1, 1);
- EXPECT_EQ(rng_1.GetInt(first_val).status(), OkStatus());
+ rng_1.GetInt(first_val);
// Use the same starting seed.
XorShiftStarRng64 rng_2(seed1);
uint64_t second_val = 0;
// Use a different number of entropy bits.
rng_2.InjectEntropyBits(0x1, 2);
- EXPECT_EQ(rng_2.GetInt(second_val).status(), OkStatus());
+ rng_2.GetInt(second_val);
EXPECT_NE(first_val, second_val);
}
@@ -91,7 +101,7 @@ TEST(XorShiftStarRng64, IncrementalEntropy) {
XorShiftStarRng64 rng_1(seed1);
uint64_t first_val = 0;
rng_1.InjectEntropyBits(0x6, 3);
- EXPECT_EQ(rng_1.GetInt(first_val).status(), OkStatus());
+ rng_1.GetInt(first_val);
// Use the same starting seed.
XorShiftStarRng64 rng_2(seed1);
@@ -100,7 +110,7 @@ TEST(XorShiftStarRng64, IncrementalEntropy) {
rng_2.InjectEntropyBits(0x1, 1);
rng_2.InjectEntropyBits(0x1, 1);
rng_2.InjectEntropyBits(0x0, 1);
- EXPECT_EQ(rng_2.GetInt(second_val).status(), OkStatus());
+ rng_2.GetInt(second_val);
EXPECT_EQ(first_val, second_val);
}
@@ -114,9 +124,176 @@ TEST(XorShiftStarRng64, InjectEntropy) {
std::byte(0x17),
std::byte(0x02)};
rng.InjectEntropy(entropy);
- EXPECT_EQ(rng.GetInt(val).status(), OkStatus());
+ rng.GetInt(val);
EXPECT_NE(val, result1[0]);
}
+TEST(XorShiftStarRng64, GetIntBoundedUint8) {
+ XorShiftStarRng64 rng(seed1);
+
+ constexpr uint8_t upper_bound = 150;
+
+ constexpr uint8_t result[] = {
+ 116,
+ 116,
+ 121,
+ 17,
+ 46,
+ 137,
+ 121,
+ 114,
+ 44,
+ };
+ constexpr int result_count = sizeof(result) / sizeof(result[0]);
+
+ uint8_t val8 = 0;
+ for (int i = 0; i < result_count; i++) {
+ rng.GetInt(val8, upper_bound);
+ EXPECT_EQ(val8, result[i]);
+ }
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedUint16) {
+ XorShiftStarRng64 rng(seed1);
+
+ constexpr uint16_t upper_bound = 400;
+
+ constexpr uint16_t result[] = {
+ 116,
+ 116,
+ 121,
+ 17,
+ 302,
+ 137,
+ 121,
+ 370,
+ 300,
+ };
+ constexpr int result_count = sizeof(result) / sizeof(result[0]);
+
+ uint16_t val16 = 0;
+ for (int i = 0; i < result_count; i++) {
+ rng.GetInt(val16, upper_bound);
+ EXPECT_EQ(val16, result[i]);
+ }
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedUint32) {
+ XorShiftStarRng64 rng(seed1);
+
+ constexpr uint32_t upper_bound = 3'000'000'000;
+
+ constexpr uint32_t result[] = {
+ 1'605'596'276,
+ 2'712'329'332,
+ 156'990'481,
+ 2'474'818'862,
+ 1'767'009'929,
+ 1'239'843'961,
+ 2'490'623'346,
+ };
+ constexpr int result_count = sizeof(result) / sizeof(result[0]);
+
+ uint32_t val32 = 0;
+ for (int i = 0; i < result_count; i++) {
+ rng.GetInt(val32, upper_bound);
+ EXPECT_EQ(val32, result[i]);
+ }
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedUint64) {
+ XorShiftStarRng64 rng(seed1);
+
+ constexpr uint64_t upper_bound = 10'000'000'000;
+
+ constexpr uint64_t result[] = {
+ 1'605'596'276,
+ 7'007'296'628,
+ 4'116'273'785,
+ 6'061'977'225,
+ 1'239'843'961,
+ 6'785'590'642,
+ 4'181'236'647,
+ };
+ constexpr int result_count = sizeof(result) / sizeof(result[0]);
+
+ uint64_t val64 = 0;
+ for (int i = 0; i < result_count; i++) {
+ rng.GetInt(val64, upper_bound);
+ EXPECT_EQ(val64, result[i]);
+ }
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedAt0) {
+ if (!PW_ASSERT_ENABLE_DEBUG) {
+ XorShiftStarRng64 rng(seed1);
+ uint64_t val64 = 0;
+ rng.GetInt(val64, static_cast<uint64_t>(0));
+ EXPECT_EQ(val64, 0u);
+ }
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedWith1IsAlways0) {
+ XorShiftStarRng64 rng(seed1);
+ uint64_t val64 = 0;
+ for (int i = 0; i < 100; ++i) {
+ rng.GetInt(val64, static_cast<uint64_t>(1));
+ EXPECT_EQ(val64, 0u);
+ }
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedWithBoundOf2MightBeOneOrZero) {
+ XorShiftStarRng64 rng(seed1);
+ bool values[] = {false, false, false};
+ for (int i = 0; i < 250; ++i) {
+ size_t values_index = 0;
+ rng.GetInt(values_index, static_cast<size_t>(2));
+ values[values_index] |= true;
+ }
+
+ EXPECT_TRUE(values[0]);
+ EXPECT_TRUE(values[1]);
+ EXPECT_FALSE(values[2]);
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedIsLowerThanPowersOfTwo) {
+ XorShiftStarRng64 rng(seed1);
+ for (uint64_t pow_of_2 = 0; pow_of_2 < 64; ++pow_of_2) {
+ uint64_t upper_bound = static_cast<uint64_t>(1) << pow_of_2;
+ uint64_t value = 0;
+ for (int i = 0; i < 256; ++i) {
+ rng.GetInt(value, upper_bound);
+ EXPECT_LT(value, upper_bound);
+ }
+ }
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedUint64IsLowerThanSomeNumbers) {
+ XorShiftStarRng64 rng(seed1);
+ uint64_t bounds[] = {7, 13, 51, 233, 181, 1025, 50323, 546778};
+ size_t bounds_size = sizeof(bounds) / sizeof(bounds[0]);
+
+ for (size_t i = 0; i < bounds_size; ++i) {
+ for (int j = 0; j < 256; ++j) {
+ uint64_t value = 0;
+ rng.GetInt(value, bounds[i]);
+ EXPECT_LT(value, bounds[i]);
+ }
+ }
+}
+
+TEST(XorShiftStarRng64, GetIntBoundedHasHighBitSetSometimes) {
+ XorShiftStarRng64 rng(seed1);
+ bool high_bit = false;
+
+ for (int i = 0; i < 256; ++i) {
+ uint64_t value = 0;
+ rng.GetInt(value, std::numeric_limits<uint64_t>::max());
+ high_bit |= value & (1ULL << 63);
+ }
+
+ EXPECT_TRUE(high_bit);
+}
+
} // namespace
} // namespace pw::random
diff --git a/pw_result/Android.bp b/pw_result/Android.bp
new file mode 100644
index 000000000..72c58a5ce
--- /dev/null
+++ b/pw_result/Android.bp
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_result_headers",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: [
+ "public",
+ ],
+ host_supported: true,
+}
diff --git a/pw_result/BUILD.gn b/pw_result/BUILD.gn
index 1fac1ad03..8bb7ebdbc 100644
--- a/pw_result/BUILD.gn
+++ b/pw_result/BUILD.gn
@@ -54,7 +54,7 @@ pw_doc_group("docs") {
report_deps = [ ":result_size" ]
}
-pw_size_report("result_size") {
+pw_size_diff("result_size") {
title = "pw::Result vs. pw::Status and out pointer"
binaries = [
@@ -73,5 +73,20 @@ pw_size_report("result_size") {
base = "size_report:pointer_read"
label = "Returning a larger object (std::span)"
},
+ {
+ target = "size_report:monadic_and_then"
+ base = "size_report:ladder_and_then"
+ label = "Using and_then instead of if ladder"
+ },
+ {
+ target = "size_report:monadic_or_else"
+ base = "size_report:ladder_or_else"
+ label = "Using or_else instead of if ladder"
+ },
+ {
+ target = "size_report:monadic_transform"
+ base = "size_report:ladder_transform"
+ label = "Using transform instead of if ladder"
+ },
]
}
diff --git a/pw_result/CMakeLists.txt b/pw_result/CMakeLists.txt
index 300d12b8d..7f1aa9bce 100644
--- a/pw_result/CMakeLists.txt
+++ b/pw_result/CMakeLists.txt
@@ -14,7 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_result
+pw_add_library(pw_result INTERFACE
HEADERS
public/pw_result/result.h
PUBLIC_INCLUDES
@@ -31,7 +31,7 @@ endif()
pw_add_test(pw_result.result_test
SOURCES
result_test.cc
- DEPS
+ PRIVATE_DEPS
pw_result
pw_status
GROUPS
diff --git a/pw_result/docs.rst b/pw_result/docs.rst
index 09f9e9852..fa4aa9ec1 100644
--- a/pw_result/docs.rst
+++ b/pw_result/docs.rst
@@ -89,6 +89,146 @@ and attempting to access the value is an error.
This module is experimental. Its impact on code size and stack usage has not
yet been profiled. Use at your own risk.
+Monadic Operations
+==================
+``pw::Result<T>`` also supports monadic operations, similar to the additions
+made to ``std::optional<T>`` in C++23. These operations allow functions to be
+applied to a ``pw::Result<T>`` that would perform additional computation.
+
+These operations do not incur any additional FLASH or RAM cost compared to a
+traditional if/else ladder, as can be seen in the `Size report`_.
+
+.. code-block:: cpp
+
+ // Without monads
+ pw::Result<Image> GetCuteCat(const Image& img) {
+ pw::Result<Image> cropped = CropToCat(img);
+ if (!cropped.ok()) {
+ return cropped.status();
+ }
+ pw::Result<Image> with_tie = AddBowTie(*cropped);
+ if (!with_tie.ok()) {
+ return with_tie.status();
+ }
+ pw::Result<Image> with_sparkles = MakeEyesSparkle(*with_tie);
+ if (!with_sparkles.ok()) {
+ return with_parkes.status();
+ }
+ return AddRainbow(MakeSmaller(*with_sparkles));
+ }
+
+ // With monads
+ pw::Result<Image> GetCuteCat(const Image& img) {
+ return CropToCat(img)
+ .and_then(AddBoeTie)
+ .and_then(MakeEyesSparkle)
+ .transform(MakeSmaller)
+ .transform(AddRainbow);
+ }
+
+``pw::Result<T>::and_then``
+---------------------------
+The ``pw::Result<T>::and_then`` member function will return the result of the
+invocation of the provided function on the contained value if it exists.
+Otherwise, returns the contained status in a ``pw::Result<U>``, which is the
+return type of provided function.
+
+.. code-block:: cpp
+
+ // Expositional prototype of and_then:
+ template <typename T>
+ class Result {
+ template <typename U>
+ Result<U> and_then(Function<Result<U>(T)> func);
+ };
+
+ Result<Foo> CreateFoo();
+ Result<Bar> CreateBarFromFoo(const Foo& foo);
+
+ Result<Bar> bar = CreateFoo().and_then(CreateBarFromFoo);
+
+``pw::Result<T>::or_else``
+--------------------------
+The ``pw::Result<T>::or_else`` member function will return ``*this`` if it
+contains a value. Otherwise, it will return the result of the provided function.
+The function must return a type convertible to a ``pw::Result<T>`` or ``void``.
+This is particularly useful for handling errors.
+
+.. code-block:: cpp
+
+ // Expositional prototype of or_else:
+ template <typename T>
+ class Result {
+ template <typename U>
+ requires std::is_convertible_v<U, Result<T>>
+ Result<T> or_else(Function<U(Status)> func);
+
+ Result<T> or_else(Function<void(Status)> func);
+ };
+
+ // Without or_else:
+ Result<Image> GetCuteCat(const Image& image) {
+ Result<Image> cropped = CropToCat(image);
+ if (!cropped.ok()) {
+ PW_LOG_ERROR("Failed to crop cat: %d", cropped.status().code());
+ return cropped.status();
+ }
+ return cropped;
+ }
+
+ // With or_else:
+ Result<Image> GetCuteCat(const Image& image) {
+ return CropToCat(image).or_else(
+ [](Status s) { PW_LOG_ERROR("Failed to crop cat: %d", s.code()); });
+ }
+
+Another useful scenario for ``pw::Result<T>::or_else`` is providing a default
+value that is expensive to compute. Typically, default values are provided by
+using ``pw::Result<T>::value_or``, but that requires the default value to be
+constructed regardless of whether we actually need it.
+
+.. code-block:: cpp
+
+ // With value_or:
+ Image GetCuteCat(const Image& image) {
+ // GenerateCuteCat() must execute regardless of the success of CropToCat
+ return CropToCat(image).value_or(GenerateCuteCat());
+ }
+
+ // With or_else:
+ Image GetCuteCat(const Image& image) {
+ // GenerateCuteCat() only executes if CropToCat fails.
+ return *CropToCat(image).or_else([](Status) { return GenerateCuteCat(); });
+ }
+
+``pw::Result<T>::transform``
+----------------------------
+The ``pw::Result<T>::transform`` member method will return a ``pw::Result<U>``
+which contains the result of the invocation of the given function if ``*this``
+contains a value. Otherwise, it returns a ``pw::Result<U>`` with the same
+``pw::Status`` value as ``*this``.
+
+The monadic methods for ``and_then`` and ``transform`` are fairly similar. The
+primary difference is that ``and_then`` requires the provided function to return
+a ``pw::Result``, whereas ``transform`` functions can return any type. Users
+should be aware that if they provide a function that returns a ``pw::Result`` to
+``transform``, this will return a ``pw::Result<pw::Result<U>>``.
+
+.. code-block:: cpp
+
+ // Expositional prototype of transform:
+ template <typename T>
+ class Result {
+ template <typename U>
+ Result<U> transform(Function<U(T)> func);
+ };
+
+ Result<int> ConvertStringToInteger(std::string_view);
+ int MultiplyByTwo(int x);
+
+ Result<int> x = ConvertStringToInteger("42")
+ .transform(MultiplyByTwo);
+
-----------
Size report
-----------
diff --git a/pw_result/public/pw_result/internal/result_internal.h b/pw_result/public/pw_result/internal/result_internal.h
index 895bfd934..f87eb02d8 100644
--- a/pw_result/public/pw_result/internal/result_internal.h
+++ b/pw_result/public/pw_result/internal/result_internal.h
@@ -124,6 +124,18 @@ using IsForwardingAssignmentValid = std::disjunction<
std::remove_cv_t<std::remove_reference_t<U>>>,
IsForwardingAssignmentAmbiguous<T, U>>>>;
+// This trait is for determining if a given type is a Result.
+template <typename T>
+constexpr bool IsResult = false;
+template <typename T>
+constexpr bool IsResult<Result<T>> = true;
+
+// This trait determines the return type of a given function without const,
+// volatile or reference qualifiers.
+template <typename Fn, typename T>
+using InvokeResultType =
+ std::remove_cv_t<std::remove_reference_t<std::invoke_result_t<Fn, T>>>;
+
PW_MODIFY_DIAGNOSTICS_PUSH();
PW_MODIFY_DIAGNOSTIC_GCC(ignored, "-Wmaybe-uninitialized");
@@ -271,7 +283,8 @@ class StatusOrData;
template <typename... Arg> \
void MakeValue(Arg&&... arg) { \
internal_result::PlacementNew<T>(&unused_, std::forward<Arg>(arg)...); \
- }
+ } \
+ static_assert(true, "Macros must be terminated with a semicolon")
template <typename T>
class StatusOrData<T, true> {
diff --git a/pw_result/public/pw_result/result.h b/pw_result/public/pw_result/result.h
index fe504dc76..79b4a0005 100644
--- a/pw_result/public/pw_result/result.h
+++ b/pw_result/public/pw_result/result.h
@@ -35,6 +35,7 @@
#pragma once
#include <exception>
+#include <functional>
#include <initializer_list>
#include <new>
#include <string>
@@ -549,6 +550,163 @@ class Result : private internal_result::StatusOrData<T>,
return this->data_;
}
+ // Result<T>::and_then()
+ //
+ // template <typename U>
+ // Result<U> and_then(Function<Result<U>(T)> func);
+ //
+ // Returns the Result from the invocation of the function on the contained
+ // value if it exists. Otherwise, returns the contained status in the Result.
+ //
+ // Result<Foo> CreateFoo();
+ // Result<Bar> CreateBarFromFoo(const Foo& foo);
+ //
+ // Result<Bar> bar = CreateFoo().and_then(CreateBarFromFoo);
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, T&>,
+ std::enable_if_t<std::is_copy_constructible_v<Ret>, int> = 0>
+ constexpr Ret and_then(Fn&& function) & {
+ static_assert(internal_result::IsResult<Ret>,
+ "Fn must return a pw::Result");
+ return ok() ? std::invoke(std::forward<Fn>(function), value())
+ : Ret(status());
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, T&&>,
+ std::enable_if_t<std::is_move_constructible_v<Ret>, int> = 0>
+ constexpr auto and_then(Fn&& function) && {
+ static_assert(internal_result::IsResult<Ret>,
+ "Fn must return a pw::Result");
+ return ok() ? std::invoke(std::forward<Fn>(function), std::move(value()))
+ : Ret(status());
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, const T&>,
+ std::enable_if_t<std::is_copy_constructible_v<Ret>, int> = 0>
+ constexpr auto and_then(Fn&& function) const& {
+ static_assert(internal_result::IsResult<Ret>,
+ "Fn must return a pw::Result");
+ return ok() ? std::invoke(std::forward<Fn>(function), value())
+ : Ret(status());
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, const T&&>,
+ std::enable_if_t<std::is_move_constructible_v<Ret>, int> = 0>
+ constexpr auto and_then(Fn&& function) const&& {
+ static_assert(internal_result::IsResult<Ret>,
+ "Fn must return a pw::Result");
+ return ok() ? std::invoke(std::forward<Fn>(function), std::move(value()))
+ : Ret(status());
+ }
+
+ // Result<T>::or_else()
+ //
+ // template <typename U>
+ // requires std::is_convertible_v<U, Result<T>>
+ // Result<T> or_else(Function<U(Status)> func);
+ //
+ // Result<T> or_else(Function<void(Status)> func);
+ //
+ // Returns a Result if it has a value, otherwise it invokes the given
+ // function. The function must return a type convertible to a Result<T> or a
+ // void.
+ //
+ // Result<Foo> CreateFoo();
+ //
+ // Result<Foo> foo = CreateFoo().or_else(
+ // [](Status s) { PW_LOG_ERROR("Status: %d", s.code()); });
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, const Status&>,
+ std::enable_if_t<!std::is_void_v<Ret>, int> = 0>
+ constexpr Result<T> or_else(Fn&& function) const& {
+ static_assert(std::is_convertible_v<Ret, Result<T>>,
+ "Fn must be convertible to a pw::Result");
+ return ok() ? *this : std::invoke(std::forward<Fn>(function), status());
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, const Status&>,
+ std::enable_if_t<std::is_void_v<Ret>, int> = 0>
+ constexpr Result<T> or_else(Fn&& function) const& {
+ if (ok()) {
+ return *this;
+ }
+ std::invoke(std::forward<Fn>(function), status());
+ return *this;
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, Status&&>,
+ std::enable_if_t<!std::is_void_v<Ret>, int> = 0>
+ constexpr Result<T> or_else(Fn&& function) && {
+ static_assert(std::is_convertible_v<Ret, Result<T>>,
+ "Fn must be convertible to a pw::Result");
+ return ok() ? std::move(*this)
+ : std::invoke(std::forward<Fn>(function), std::move(status()));
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, Status&&>,
+ std::enable_if_t<std::is_void_v<Ret>, int> = 0>
+ constexpr Result<T> or_else(Fn&& function) && {
+ if (ok()) {
+ return *this;
+ }
+ std::invoke(std::forward<Fn>(function), status());
+ return std::move(*this);
+ }
+
+ // Result<T>::transform()
+ //
+ // template <typename U>
+ // Result<U> transform(Function<U(T)> func);
+ //
+ // Returns a Result<U> which contains the result of the invocation of the
+ // given function if *this contains a value. Otherwise, it returns a Result<U>
+ // with the same Status as *this.
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, T&>,
+ std::enable_if_t<std::is_copy_constructible_v<Ret>, int> = 0>
+ constexpr Result<Ret> transform(Fn&& function) & {
+ if (!ok()) {
+ return status();
+ }
+ return std::invoke(std::forward<Fn>(function), value());
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, T&&>,
+ std::enable_if_t<std::is_move_constructible_v<Ret>, int> = 0>
+ constexpr Result<Ret> transform(Fn&& function) && {
+ if (!ok()) {
+ return std::move(status());
+ }
+ return std::invoke(std::forward<Fn>(function), std::move(value()));
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, T&>,
+ std::enable_if_t<std::is_copy_constructible_v<Ret>, int> = 0>
+ constexpr Result<Ret> transform(Fn&& function) const& {
+ if (!ok()) {
+ return status();
+ }
+ return std::invoke(std::forward<Fn>(function), value());
+ }
+
+ template <typename Fn,
+ typename Ret = internal_result::InvokeResultType<Fn, T&&>,
+ std::enable_if_t<std::is_move_constructible_v<Ret>, int> = 0>
+ constexpr Result<Ret> transform(Fn&& function) const&& {
+ if (!ok()) {
+ return std::move(status());
+ }
+ return std::invoke(std::forward<Fn>(function), std::move(value()));
+ }
+
private:
using Base::Assign;
template <typename U>
diff --git a/pw_result/result_test.cc b/pw_result/result_test.cc
index b65a3cf70..c465bdb39 100644
--- a/pw_result/result_test.cc
+++ b/pw_result/result_test.cc
@@ -23,6 +23,7 @@
#include "pw_result/result.h"
#include "gtest/gtest.h"
+#include "pw_status/status.h"
#include "pw_status/try.h"
namespace pw {
@@ -50,8 +51,8 @@ TEST(Result, ValueOr) {
TEST(Result, Deref) {
struct Tester {
- constexpr bool True() { return true; };
- constexpr bool False() { return false; };
+ constexpr bool True() { return true; }
+ constexpr bool False() { return false; }
};
auto tester = Result<Tester>(Tester());
@@ -66,8 +67,8 @@ TEST(Result, Deref) {
TEST(Result, ConstDeref) {
struct Tester {
- constexpr bool True() const { return true; };
- constexpr bool False() const { return false; };
+ constexpr bool True() const { return true; }
+ constexpr bool False() const { return false; }
};
const auto tester = Result<Tester>(Tester());
@@ -124,10 +125,6 @@ Status TryResultAssign(Result<bool> result) {
return result.status();
}
-// TODO(pwbug/363): Once pw::Result has been refactored to properly support
-// non-default move and/or copy assignment operators and/or constructors, we
-// should add explicit tests to confirm this is properly handled by
-// PW_TRY_ASSIGN.
TEST(Result, TryAssign) {
EXPECT_EQ(TryResultAssign(Status::Cancelled()), Status::Cancelled());
EXPECT_EQ(TryResultAssign(Status::DataLoss()), Status::DataLoss());
@@ -180,5 +177,367 @@ TEST(Result, ConstexprNotOkCopy) {
static_assert(std::move(kResultCopy).value_or(Value{99}).number == 99);
}
+auto multiply = [](int x) -> Result<int> { return x * 2; };
+auto add_two = [](int x) -> Result<int> { return x + 2; };
+auto fail_unknown = [](int) -> Result<int> { return Status::Unknown(); };
+
+TEST(Result, AndThenNonConstLValueRefInvokeSuccess) {
+ Result<int> r = 32;
+ auto ret = r.and_then(multiply);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 64);
+}
+
+TEST(Result, AndThenNonConstLValueRefInvokeFail) {
+ Result<int> r = 32;
+ auto ret = r.and_then(fail_unknown);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Unknown());
+}
+
+TEST(Result, AndThenNonConstLValueRefSkips) {
+ Result<int> r = Status::NotFound();
+ auto ret = r.and_then(multiply);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, AndThenNonConstRvalueRefInvokeSuccess) {
+ Result<int> r = 32;
+ auto ret = std::move(r).and_then(multiply);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 64);
+}
+
+TEST(Result, AndThenNonConstRvalueRefInvokeFails) {
+ Result<int> r = 64;
+ auto ret = std::move(r).and_then(fail_unknown);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Unknown());
+}
+
+TEST(Result, AndThenNonConstRvalueRefSkips) {
+ Result<int> r = Status::NotFound();
+ auto ret = std::move(r).and_then(multiply);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, AndThenConstLValueRefInvokeSuccess) {
+ const Result<int> r = 32;
+ auto ret = r.and_then(multiply);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 64);
+}
+
+TEST(Result, AndThenConstLValueRefInvokeFail) {
+ const Result<int> r = 32;
+ auto ret = r.and_then(fail_unknown);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Unknown());
+}
+
+TEST(Result, AndThenConstLValueRefSkips) {
+ const Result<int> r = Status::NotFound();
+ auto ret = r.and_then(multiply);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, AndThenConstRValueRefInvokeSuccess) {
+ const Result<int> r = 32;
+ auto ret = std::move(r).and_then(multiply);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 64);
+}
+
+TEST(Result, AndThenConstRValueRefInvokeFail) {
+ const Result<int> r = 32;
+ auto ret = std::move(r).and_then(fail_unknown);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Unknown());
+}
+
+TEST(Result, AndThenConstRValueRefSkips) {
+ const Result<int> r = Status::NotFound();
+ auto ret = std::move(r).and_then(multiply);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, AndThenMultipleChained) {
+ Result<int> r = 32;
+ auto ret = r.and_then(multiply).and_then(add_two).and_then(multiply);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 132);
+}
+
+auto return_status = [](Status) { return Status::Unknown(); };
+auto return_result = [](Status) { return Result<int>(Status::Internal()); };
+
+TEST(Result, OrElseNonConstLValueRefSkips) {
+ Result<int> r = 32;
+ auto ret = r.or_else(return_status);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 32);
+}
+
+TEST(Result, OrElseNonConstLValueRefStatusInvokes) {
+ Result<int> r = Status::NotFound();
+ auto ret = r.or_else(return_status);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Unknown());
+}
+
+TEST(Result, OrElseNonConstLValueRefResultInvokes) {
+ Result<int> r = Status::NotFound();
+ auto ret = r.or_else(return_result);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Internal());
+}
+
+TEST(Result, OrElseNonConstLValueRefVoidSkips) {
+ Result<int> r = 32;
+ bool invoked = false;
+ auto ret = r.or_else([&invoked](Status) { invoked = true; });
+ EXPECT_FALSE(invoked);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 32);
+}
+
+TEST(Result, OrElseNonConstLValueRefVoidInvokes) {
+ Result<int> r = Status::NotFound();
+ bool invoked = false;
+ auto ret = r.or_else([&invoked](Status) { invoked = true; });
+ EXPECT_TRUE(invoked);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, OrElseNonConstRValueRefSkips) {
+ Result<int> r = 32;
+ auto ret = std::move(r).or_else(return_status);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 32);
+}
+
+TEST(Result, OrElseNonConstRValueRefStatusInvokes) {
+ Result<int> r = Status::NotFound();
+ auto ret = std::move(r).or_else(return_status);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Unknown());
+}
+
+TEST(Result, OrElseNonConstRValueRefResultInvokes) {
+ Result<int> r = Status::NotFound();
+ auto ret = std::move(r).or_else(return_result);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Internal());
+}
+
+TEST(Result, OrElseNonConstRValueRefVoidSkips) {
+ Result<int> r = 32;
+ bool invoked = false;
+ auto ret = std::move(r).or_else([&invoked](Status) { invoked = true; });
+ EXPECT_FALSE(invoked);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 32);
+}
+
+TEST(Result, OrElseNonConstRValueRefVoidInvokes) {
+ Result<int> r = Status::NotFound();
+ bool invoked = false;
+ auto ret = std::move(r).or_else([&invoked](Status) { invoked = true; });
+ EXPECT_TRUE(invoked);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, OrElseConstLValueRefSkips) {
+ const Result<int> r = 32;
+ auto ret = r.or_else(return_status);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 32);
+}
+
+TEST(Result, OrElseConstLValueRefStatusInvokes) {
+ const Result<int> r = Status::NotFound();
+ auto ret = r.or_else(return_status);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Unknown());
+}
+
+TEST(Result, OrElseConstLValueRefResultInvokes) {
+ const Result<int> r = Status::NotFound();
+ auto ret = r.or_else(return_result);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Internal());
+}
+
+TEST(Result, OrElseConstLValueRefVoidSkips) {
+ const Result<int> r = 32;
+ bool invoked = false;
+ auto ret = r.or_else([&invoked](Status) { invoked = true; });
+ EXPECT_FALSE(invoked);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 32);
+}
+
+TEST(Result, OrElseConstLValueRefVoidInvokes) {
+ const Result<int> r = Status::NotFound();
+ bool invoked = false;
+ auto ret = r.or_else([&invoked](Status) { invoked = true; });
+ EXPECT_TRUE(invoked);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, OrElseConstRValueRefSkips) {
+ const Result<int> r = 32;
+ auto ret = std::move(r).or_else(return_status);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 32);
+}
+
+TEST(Result, OrElseConstRValueRefStatusInvokes) {
+ const Result<int> r = Status::NotFound();
+ auto ret = std::move(r).or_else(return_status);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Unknown());
+}
+
+TEST(Result, OrElseConstRValueRefResultInvokes) {
+ const Result<int> r = Status::NotFound();
+ auto ret = std::move(r).or_else(return_result);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Internal());
+}
+
+TEST(Result, OrElseConstRValueRefVoidSkips) {
+ const Result<int> r = 32;
+ bool invoked = false;
+ auto ret = std::move(r).or_else([&invoked](Status) { invoked = true; });
+ EXPECT_FALSE(invoked);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 32);
+}
+
+TEST(Result, OrElseConstRValueRefVoidInvokes) {
+ const Result<int> r = Status::NotFound();
+ bool invoked = false;
+ auto ret = std::move(r).or_else([&invoked](Status) { invoked = true; });
+ EXPECT_TRUE(invoked);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, OrElseMultipleChained) {
+ Result<int> r = Status::NotFound();
+ bool invoked = false;
+ auto ret =
+ r.or_else(return_result).or_else([&invoked](Status) { invoked = true; });
+ EXPECT_TRUE(invoked);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::Internal());
+}
+
+auto multiply_int = [](int x) { return x * 2; };
+auto add_two_int = [](int x) { return x + 2; };
+auto make_value = [](int x) { return Value{.number = x}; };
+
+TEST(Result, TransformNonConstLValueRefInvokeSuccess) {
+ Result<int> r = 32;
+ auto ret = r.transform(multiply_int);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 64);
+}
+
+TEST(Result, TransformNonConstLValueRefInvokeDifferentType) {
+ Result<int> r = 32;
+ auto ret = r.transform(make_value);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(ret->number, 32);
+}
+
+TEST(Result, TransformNonConstLValueRefSkips) {
+ Result<int> r = Status::NotFound();
+ auto ret = r.transform(multiply_int);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, TransformNonConstRValueRefInvokeSuccess) {
+ Result<int> r = 32;
+ auto ret = std::move(r).transform(multiply_int);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 64);
+}
+
+TEST(Result, TransformNonConstRValueRefInvokeDifferentType) {
+ Result<int> r = 32;
+ auto ret = std::move(r).transform(make_value);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(ret->number, 32);
+}
+
+TEST(Result, TransformNonConstRValueRefSkips) {
+ Result<int> r = Status::NotFound();
+ auto ret = std::move(r).transform(multiply_int);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, TransformConstLValueRefInvokeSuccess) {
+ const Result<int> r = 32;
+ auto ret = r.transform(multiply_int);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 64);
+}
+
+TEST(Result, TransformConstLValueRefInvokeDifferentType) {
+ const Result<int> r = 32;
+ auto ret = r.transform(make_value);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(ret->number, 32);
+}
+
+TEST(Result, TransformConstLValueRefSkips) {
+ const Result<int> r = Status::NotFound();
+ auto ret = r.transform(multiply_int);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, TransformConstRValueRefInvokeSuccess) {
+ const Result<int> r = 32;
+ auto ret = std::move(r).transform(multiply_int);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(*ret, 64);
+}
+
+TEST(Result, TransformConstRValueRefInvokeDifferentType) {
+ const Result<int> r = 32;
+ auto ret = std::move(r).transform(make_value);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(ret->number, 32);
+}
+
+TEST(Result, TransformConstRValueRefSkips) {
+ const Result<int> r = Status::NotFound();
+ auto ret = std::move(r).transform(multiply_int);
+ ASSERT_FALSE(ret.ok());
+ EXPECT_EQ(ret.status(), Status::NotFound());
+}
+
+TEST(Result, TransformMultipleChained) {
+ Result<int> r = 32;
+ auto ret = r.transform(multiply_int)
+ .transform(add_two_int)
+ .transform(multiply_int)
+ .transform(make_value);
+ ASSERT_TRUE(ret.ok());
+ EXPECT_EQ(ret->number, 132);
+}
+
} // namespace
} // namespace pw
diff --git a/pw_result/size_report/BUILD.bazel b/pw_result/size_report/BUILD.bazel
index 6853da24e..f8a1dc3a3 100644
--- a/pw_result/size_report/BUILD.bazel
+++ b/pw_result/size_report/BUILD.bazel
@@ -78,3 +78,39 @@ pw_cc_binary(
"//pw_span",
],
)
+
+pw_cc_binary(
+ name = "ladder_and_then",
+ srcs = ["ladder_and_then.cc"],
+ deps = ["//pw_result"],
+)
+
+pw_cc_binary(
+ name = "monadic_and_then",
+ srcs = ["monadic_and_then.cc"],
+ deps = ["//pw_result"],
+)
+
+pw_cc_binary(
+ name = "ladder_or_else",
+ srcs = ["ladder_or_else.cc"],
+ deps = ["//pw_result"],
+)
+
+pw_cc_binary(
+ name = "monadic_or_else",
+ srcs = ["monadic_or_else.cc"],
+ deps = ["//pw_result"],
+)
+
+pw_cc_binary(
+ name = "ladder_transform",
+ srcs = ["ladder_transform.cc"],
+ deps = ["//pw_result"],
+)
+
+pw_cc_binary(
+ name = "monadic_transform",
+ srcs = ["monadic_transform.cc"],
+ deps = ["//pw_result"],
+)
diff --git a/pw_result/size_report/BUILD.gn b/pw_result/size_report/BUILD.gn
index 441151fce..964d217c1 100644
--- a/pw_result/size_report/BUILD.gn
+++ b/pw_result/size_report/BUILD.gn
@@ -69,3 +69,33 @@ pw_executable("result_read") {
dir_pw_preprocessor,
]
}
+
+pw_executable("ladder_and_then") {
+ sources = [ "ladder_and_then.cc" ]
+ deps = [ ".." ]
+}
+
+pw_executable("monadic_and_then") {
+ sources = [ "monadic_and_then.cc" ]
+ deps = [ ".." ]
+}
+
+pw_executable("ladder_or_else") {
+ sources = [ "ladder_or_else.cc" ]
+ deps = [ ".." ]
+}
+
+pw_executable("monadic_or_else") {
+ sources = [ "monadic_or_else.cc" ]
+ deps = [ ".." ]
+}
+
+pw_executable("ladder_transform") {
+ sources = [ "ladder_transform.cc" ]
+ deps = [ ".." ]
+}
+
+pw_executable("monadic_transform") {
+ sources = [ "monadic_transform.cc" ]
+ deps = [ ".." ]
+}
diff --git a/pw_result/size_report/ladder_and_then.cc b/pw_result/size_report/ladder_and_then.cc
new file mode 100644
index 000000000..5921ab964
--- /dev/null
+++ b/pw_result/size_report/ladder_and_then.cc
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_result/result.h"
+
+pw::Result<int> Multiply(int a, int b) { return a * b; }
+
+pw::Result<int> AddTwo(int x) { return x + 2; }
+
+int volatile* unoptimizable;
+
+int main() {
+ pw::Result multiplied = Multiply(*unoptimizable, *unoptimizable);
+
+ if (!multiplied.ok()) {
+ return -1;
+ }
+
+ pw::Result and_two = AddTwo(*multiplied);
+
+ if (!and_two.ok()) {
+ return -1;
+ }
+
+ return *and_two;
+}
diff --git a/pw_result/size_report/ladder_or_else.cc b/pw_result/size_report/ladder_or_else.cc
new file mode 100644
index 000000000..b6e402247
--- /dev/null
+++ b/pw_result/size_report/ladder_or_else.cc
@@ -0,0 +1,36 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <cstdio>
+
+#include "pw_result/result.h"
+
+pw::Result<float> Divide(float a, float b) {
+ if (b == 0) {
+ return pw::Status::InvalidArgument();
+ }
+ return a / b;
+}
+
+float volatile* unoptimizable;
+
+int main() {
+ pw::Result result = Divide(*unoptimizable, *unoptimizable);
+
+ if (!result.ok()) {
+ printf("result failed with %d", result.status().code());
+ }
+
+ return 0;
+}
diff --git a/pw_result/size_report/ladder_transform.cc b/pw_result/size_report/ladder_transform.cc
new file mode 100644
index 000000000..322b546c9
--- /dev/null
+++ b/pw_result/size_report/ladder_transform.cc
@@ -0,0 +1,33 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_result/result.h"
+
+pw::Result<int> Multiply(int a, int b) { return a * b; }
+
+int AddTwo(int x) { return x + 2; }
+
+int volatile* unoptimizable;
+
+int main() {
+ pw::Result multiplied = Multiply(*unoptimizable, *unoptimizable);
+
+ if (!multiplied.ok()) {
+ return -1;
+ }
+
+ int and_two = AddTwo(*multiplied);
+
+ return and_two;
+}
diff --git a/pw_result/size_report/monadic_and_then.cc b/pw_result/size_report/monadic_and_then.cc
new file mode 100644
index 000000000..17075faa8
--- /dev/null
+++ b/pw_result/size_report/monadic_and_then.cc
@@ -0,0 +1,31 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_result/result.h"
+
+pw::Result<int> Multiply(int a, int b) { return a * b; }
+
+pw::Result<int> AddTwo(int x) { return x + 2; }
+
+int volatile* unoptimizable;
+
+int main() {
+ pw::Result result = Multiply(*unoptimizable, *unoptimizable).and_then(AddTwo);
+
+ if (!result.ok()) {
+ return -1;
+ }
+
+ return *result;
+}
diff --git a/pw_result/size_report/monadic_or_else.cc b/pw_result/size_report/monadic_or_else.cc
new file mode 100644
index 000000000..e1050890c
--- /dev/null
+++ b/pw_result/size_report/monadic_or_else.cc
@@ -0,0 +1,36 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <cstdio>
+
+#include "pw_result/result.h"
+
+pw::Result<float> Divide(float a, float b) {
+ if (b == 0) {
+ return pw::Status::InvalidArgument();
+ }
+ return a / b;
+}
+
+float volatile* unoptimizable;
+
+int main() {
+ pw::Result result =
+ Divide(*unoptimizable, *unoptimizable).or_else([](pw::Status status) {
+ printf("result failed with %d", status.code());
+ });
+ result.IgnoreError();
+
+ return 0;
+}
diff --git a/pw_result/size_report/monadic_transform.cc b/pw_result/size_report/monadic_transform.cc
new file mode 100644
index 000000000..62aef7148
--- /dev/null
+++ b/pw_result/size_report/monadic_transform.cc
@@ -0,0 +1,32 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_result/result.h"
+
+pw::Result<int> Multiply(int a, int b) { return a * b; }
+
+int AddTwo(int x) { return x + 2; }
+
+int volatile* unoptimizable;
+
+int main() {
+ pw::Result result =
+ Multiply(*unoptimizable, *unoptimizable).transform(AddTwo);
+
+ if (!result.ok()) {
+ return -1;
+ }
+
+ return *result;
+}
diff --git a/pw_result/size_report/pointer_read.cc b/pw_result/size_report/pointer_read.cc
index 130cb5655..2ce96aafe 100644
--- a/pw_result/size_report/pointer_read.cc
+++ b/pw_result/size_report/pointer_read.cc
@@ -13,11 +13,11 @@
// the License.
#include <cstring>
-#include <span>
#include "pw_bytes/array.h"
#include "pw_log/log.h"
#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace {
@@ -32,12 +32,12 @@ constexpr auto kArray = pw::bytes::Array<
PW_NO_INLINE pw::Status Read(size_t offset,
size_t size,
- std::span<const std::byte>* out) {
+ pw::span<const std::byte>* out) {
if (offset + size >= std::size(kArray)) {
return pw::Status::OutOfRange();
}
- *out = std::span<const std::byte>(std::data(kArray) + offset, size);
+ *out = pw::span<const std::byte>(std::data(kArray) + offset, size);
return pw::OkStatus();
}
@@ -46,7 +46,7 @@ PW_NO_INLINE pw::Status Read(size_t offset,
size_t volatile* unoptimizable;
int main() {
- std::span<const std::byte> data;
+ pw::span<const std::byte> data;
pw::Status status = Read(*unoptimizable, *unoptimizable, &data);
if (!status.ok()) {
return 1;
diff --git a/pw_result/size_report/result_read.cc b/pw_result/size_report/result_read.cc
index 351be041a..cfe5a447c 100644
--- a/pw_result/size_report/result_read.cc
+++ b/pw_result/size_report/result_read.cc
@@ -13,12 +13,12 @@
// the License.
#include <cstring>
-#include <span>
#include "pw_bytes/array.h"
#include "pw_log/log.h"
#include "pw_preprocessor/compiler.h"
#include "pw_result/result.h"
+#include "pw_span/span.h"
namespace {
@@ -30,13 +30,13 @@ constexpr auto kArray = pw::bytes::Array<
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01>();
// clang-format on
-PW_NO_INLINE pw::Result<std::span<const std::byte>> Read(size_t offset,
- size_t size) {
+PW_NO_INLINE pw::Result<pw::span<const std::byte>> Read(size_t offset,
+ size_t size) {
if (offset + size >= std::size(kArray)) {
return pw::Status::OutOfRange();
}
- return std::span<const std::byte>(std::data(kArray) + offset, size);
+ return pw::span<const std::byte>(std::data(kArray) + offset, size);
}
} // namespace
diff --git a/pw_result/statusor_test.cc b/pw_result/statusor_test.cc
index 62dab32a9..93a14f954 100644
--- a/pw_result/statusor_test.cc
+++ b/pw_result/statusor_test.cc
@@ -101,7 +101,7 @@ class Base2 {
class Derived : public Base1, public Base2 {
public:
- virtual ~Derived() {}
+ ~Derived() override {}
int evenmorepad;
};
@@ -120,8 +120,8 @@ pw::Result<std::unique_ptr<int>> ReturnUniquePtr() {
}
TEST(Result, ElementType) {
- static_assert(std::is_same<pw::Result<int>::value_type, int>(), "");
- static_assert(std::is_same<pw::Result<char>::value_type, char>(), "");
+ static_assert(std::is_same<pw::Result<int>::value_type, int>());
+ static_assert(std::is_same<pw::Result<char>::value_type, char>());
}
TEST(Result, TestMoveOnlyInitialization) {
@@ -198,7 +198,7 @@ TEST(Result, StatusCtorForwards) {
}
#define EXPECT_DEATH_OR_THROW(statement, status) \
- EXPECT_DEATH_IF_SUPPORTED(statement, status.str());
+ EXPECT_DEATH_IF_SUPPORTED(statement, ".*");
TEST(ResultDeathTest, TestDefaultCtorValue) {
pw::Result<int> thing;
@@ -243,7 +243,7 @@ TEST(ResultDeathTest, TestStatusCtorStatusOk) {
EXPECT_FALSE(thing.ok());
EXPECT_EQ(thing.status().code(), pw::Status::Internal().code());
},
- "An OK status is not a valid constructor argument");
+ ".*");
}
TEST(ResultDeathTest, TestPointerStatusCtorStatusOk) {
@@ -255,7 +255,7 @@ TEST(ResultDeathTest, TestPointerStatusCtorStatusOk) {
EXPECT_FALSE(thing.ok());
EXPECT_EQ(thing.status().code(), pw::Status::Internal().code());
},
- "An OK status is not a valid constructor argument");
+ ".*");
}
#endif
@@ -1345,7 +1345,7 @@ TEST(Result, TestPointerValueConst) {
TEST(Result, ResultVectorOfUniquePointerCanReserveAndResize) {
using EvilType = std::vector<std::unique_ptr<int>>;
- static_assert(std::is_copy_constructible<EvilType>::value, "");
+ static_assert(std::is_copy_constructible<EvilType>::value);
std::vector<::pw::Result<EvilType>> v(5);
v.reserve(v.capacity() + 10);
v.resize(v.capacity() + 10);
diff --git a/pw_ring_buffer/BUILD.gn b/pw_ring_buffer/BUILD.gn
index 88db5bdb6..0116a68f8 100644
--- a/pw_ring_buffer/BUILD.gn
+++ b/pw_ring_buffer/BUILD.gn
@@ -28,6 +28,7 @@ pw_source_set("pw_ring_buffer") {
public_deps = [
"$dir_pw_containers",
"$dir_pw_result",
+ "$dir_pw_span",
"$dir_pw_status",
]
sources = [ "prefixed_entry_ring_buffer.cc" ]
@@ -55,7 +56,7 @@ pw_doc_group("docs") {
report_deps = [ ":ring_buffer_size" ]
}
-pw_size_report("ring_buffer_size") {
+pw_size_diff("ring_buffer_size") {
title = "pw::ring_buffer::PrefixedEntryRingBuffer"
binaries = [
diff --git a/pw_ring_buffer/CMakeLists.txt b/pw_ring_buffer/CMakeLists.txt
index b866f0ad7..62d9bb1f5 100644
--- a/pw_ring_buffer/CMakeLists.txt
+++ b/pw_ring_buffer/CMakeLists.txt
@@ -14,16 +14,15 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_ring_buffer
+pw_add_library(pw_ring_buffer STATIC
HEADERS
public/pw_ring_buffer/prefixed_entry_ring_buffer.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
pw_containers
- pw_polyfill.cstddef
- pw_polyfill.span
pw_result
+ pw_span
pw_status
SOURCES
prefixed_entry_ring_buffer.cc
@@ -35,7 +34,7 @@ pw_add_module_library(pw_ring_buffer
pw_add_test(pw_ring_buffer.prefixed_entry_ring_buffer_test
SOURCES
prefixed_entry_ring_buffer_test.cc
- DEPS
+ PRIVATE_DEPS
pw_ring_buffer
pw_assert
GROUPS
diff --git a/pw_ring_buffer/docs.rst b/pw_ring_buffer/docs.rst
index a0efbfec4..673861042 100644
--- a/pw_ring_buffer/docs.rst
+++ b/pw_ring_buffer/docs.rst
@@ -27,8 +27,9 @@ entries in the provided buffer.
// Setting up buffers and attaching a reader.
std::byte buffer[1024];
std::byte read_buffer[256];
- PrefixedEntryRingBuffer ring_buffer(buffer);
+ PrefixedEntryRingBuffer ring_buffer;
PrefixedEntryRingBuffer::Reader reader;
+ ring_buffer.SetBuffer(buffer);
ring_buffer.AttachReader(reader);
// Insert some entries and process some entries.
@@ -43,7 +44,8 @@ entries in the provided buffer.
// You can use a range-based for-loop to walk through all entries.
for (auto entry : ring_buffer) {
- PW_LOG_WARN("Read entry of size: %lu", entry.size());
+ PW_LOG_WARN("Read entry of size: %u",
+ static_cast<unsigned>(entry.buffer.size()));
}
In cases where a crash has caused the ring buffer to have corrupted data, the
@@ -60,7 +62,8 @@ indicating the reason the iterator reached it's end.
// Hold the iterator outside any loops to inspect it later.
iterator it = ring_buffer.begin();
for (; it != it.end(); ++it) {
- PW_LOG_WARN("Read entry of size: %lu", it->size());
+ PW_LOG_WARN("Read entry of size: %u",
+ static_cast<unsigned>(it->buffer.size()));
}
// Warn if there was a failure during iteration.
diff --git a/pw_ring_buffer/prefixed_entry_ring_buffer.cc b/pw_ring_buffer/prefixed_entry_ring_buffer.cc
index f3c32b202..93f70f780 100644
--- a/pw_ring_buffer/prefixed_entry_ring_buffer.cc
+++ b/pw_ring_buffer/prefixed_entry_ring_buffer.cc
@@ -38,7 +38,7 @@ void PrefixedEntryRingBufferMulti::Clear() {
}
}
-Status PrefixedEntryRingBufferMulti::SetBuffer(std::span<byte> buffer) {
+Status PrefixedEntryRingBufferMulti::SetBuffer(span<byte> buffer) {
if ((buffer.data() == nullptr) || //
(buffer.size_bytes() == 0) || //
(buffer.size_bytes() > kMaxBufferBytes)) {
@@ -83,7 +83,7 @@ Status PrefixedEntryRingBufferMulti::DetachReader(Reader& reader) {
}
Status PrefixedEntryRingBufferMulti::InternalPushBack(
- std::span<const byte> data,
+ span<const byte> data,
uint32_t user_preamble_data,
bool pop_front_if_needed) {
if (buffer_ == nullptr) {
@@ -99,7 +99,7 @@ Status PrefixedEntryRingBufferMulti::InternalPushBack(
varint::Encode<uint32_t>(user_preamble_data, preamble_buf);
}
size_t length_bytes = varint::Encode<uint32_t>(
- data.size_bytes(), std::span(preamble_buf).subspan(user_preamble_bytes));
+ data.size_bytes(), span(preamble_buf).subspan(user_preamble_bytes));
size_t total_write_bytes =
user_preamble_bytes + length_bytes + data.size_bytes();
if (buffer_bytes_ < total_write_bytes) {
@@ -118,7 +118,7 @@ Status PrefixedEntryRingBufferMulti::InternalPushBack(
}
// Write the new entry into the ring buffer.
- RawWrite(std::span(preamble_buf, user_preamble_bytes + length_bytes));
+ RawWrite(span(preamble_buf, user_preamble_bytes + length_bytes));
RawWrite(data);
// Update all readers of the new count.
@@ -128,8 +128,8 @@ Status PrefixedEntryRingBufferMulti::InternalPushBack(
return OkStatus();
}
-auto GetOutput(std::span<byte> data_out, size_t* write_index) {
- return [data_out, write_index](std::span<const byte> src) -> Status {
+auto GetOutput(span<byte> data_out, size_t* write_index) {
+ return [data_out, write_index](span<const byte> src) -> Status {
size_t copy_size = std::min(data_out.size_bytes(), src.size_bytes());
memcpy(data_out.data() + *write_index, src.data(), copy_size);
@@ -141,7 +141,7 @@ auto GetOutput(std::span<byte> data_out, size_t* write_index) {
}
Status PrefixedEntryRingBufferMulti::InternalPeekFront(
- const Reader& reader, std::span<byte> data, size_t* bytes_read_out) const {
+ const Reader& reader, span<byte> data, size_t* bytes_read_out) const {
*bytes_read_out = 0;
return InternalRead(reader, GetOutput(data, bytes_read_out), false);
}
@@ -152,7 +152,7 @@ Status PrefixedEntryRingBufferMulti::InternalPeekFront(
}
Status PrefixedEntryRingBufferMulti::InternalPeekFrontWithPreamble(
- const Reader& reader, std::span<byte> data, size_t* bytes_read_out) const {
+ const Reader& reader, span<byte> data, size_t* bytes_read_out) const {
*bytes_read_out = 0;
return InternalRead(reader, GetOutput(data, bytes_read_out), true);
}
@@ -173,9 +173,9 @@ Status PrefixedEntryRingBufferMulti::InternalPeekFrontPreamble(
return OkStatus();
}
-// TODO(pwbug/339): Consider whether this internal templating is required, or if
-// we can simply promote GetOutput to a static function and remove the template.
-// T should be similar to Status (*read_output)(std::span<const byte>)
+// TODO(b/235351046): Consider whether this internal templating is required, or
+// if we can simply promote GetOutput to a static function and remove the
+// template. T should be similar to Status (*read_output)(span<const byte>)
template <typename T>
Status PrefixedEntryRingBufferMulti::InternalRead(
const Reader& reader,
@@ -205,12 +205,11 @@ Status PrefixedEntryRingBufferMulti::InternalRead(
// Read bytes, stopping at the end of the buffer if this entry wraps.
size_t bytes_until_wrap = buffer_bytes_ - data_read_idx;
size_t bytes_to_copy = std::min(read_bytes, bytes_until_wrap);
- Status status =
- read_output(std::span(buffer_ + data_read_idx, bytes_to_copy));
+ Status status = read_output(span(buffer_ + data_read_idx, bytes_to_copy));
// If the entry wrapped, read the remaining bytes.
if (status.ok() && (bytes_to_copy < read_bytes)) {
- status = read_output(std::span(buffer_, read_bytes - bytes_to_copy));
+ status = read_output(span(buffer_, read_bytes - bytes_to_copy));
}
return status;
}
@@ -228,7 +227,7 @@ void PrefixedEntryRingBufferMulti::InternalPopFrontAll() {
for (Reader& reader : readers_) {
if (reader.entry_count_ == entry_count) {
reader.PopFront()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
}
}
@@ -263,7 +262,7 @@ Status PrefixedEntryRingBufferMulti::InternalDering(Reader& dering_reader) {
return Status::FailedPrecondition();
}
- auto buffer_span = std::span(buffer_, buffer_bytes_);
+ auto buffer_span = span(buffer_, buffer_bytes_);
std::rotate(buffer_span.begin(),
buffer_span.begin() + dering_reader.read_idx_,
buffer_span.end());
@@ -391,7 +390,7 @@ size_t PrefixedEntryRingBufferMulti::RawAvailableBytes() const {
return buffer_bytes_;
}
-void PrefixedEntryRingBufferMulti::RawWrite(std::span<const std::byte> source) {
+void PrefixedEntryRingBufferMulti::RawWrite(span<const std::byte> source) {
if (source.size_bytes() == 0) {
return;
}
@@ -439,7 +438,7 @@ size_t PrefixedEntryRingBufferMulti::IncrementIndex(size_t index,
}
Status PrefixedEntryRingBufferMulti::Reader::PeekFrontWithPreamble(
- std::span<byte> data,
+ span<byte> data,
uint32_t& user_preamble_out,
size_t& entry_bytes_read_out) const {
entry_bytes_read_out = 0;
@@ -487,7 +486,7 @@ const Entry& iterator::operator*() const {
PW_DCHECK_OK(info.status());
entry_ = {
- .buffer = std::span<const byte>(
+ .buffer = span<const byte>(
ring_buffer_->buffer_ + read_idx_ + info.value().preamble_bytes,
info.value().data_bytes),
.preamble = info.value().user_preamble,
diff --git a/pw_ring_buffer/prefixed_entry_ring_buffer_test.cc b/pw_ring_buffer/prefixed_entry_ring_buffer_test.cc
index a37245573..f9325551e 100644
--- a/pw_ring_buffer/prefixed_entry_ring_buffer_test.cc
+++ b/pw_ring_buffer/prefixed_entry_ring_buffer_test.cc
@@ -16,10 +16,11 @@
#include <cstddef>
#include <cstdint>
+#include <cstring>
+#include "gtest/gtest.h"
#include "pw_assert/check.h"
#include "pw_containers/vector.h"
-#include "pw_unit_test/framework.h"
using std::byte;
@@ -36,9 +37,9 @@ TEST(PrefixedEntryRingBuffer, NoBuffer) {
size_t count;
EXPECT_EQ(ring.EntryCount(), 0u);
- EXPECT_EQ(ring.SetBuffer(std::span<byte>(nullptr, 10u)),
+ EXPECT_EQ(ring.SetBuffer(span<byte>(static_cast<byte*>(nullptr), 10u)),
Status::InvalidArgument());
- EXPECT_EQ(ring.SetBuffer(std::span(buf, 0u)), Status::InvalidArgument());
+ EXPECT_EQ(ring.SetBuffer(span(buf, 0u)), Status::InvalidArgument());
EXPECT_EQ(ring.FrontEntryDataSizeBytes(), 0u);
EXPECT_EQ(ring.PushBack(buf), Status::FailedPrecondition());
@@ -91,9 +92,8 @@ void SingleEntryWriteReadTest(bool user_data) {
EXPECT_EQ(ring.EntryCount(), 0u);
EXPECT_EQ(ring.PopFront(), Status::OutOfRange());
EXPECT_EQ(ring.EntryCount(), 0u);
- EXPECT_EQ(
- ring.PushBack(std::span(single_entry_data, sizeof(test_buffer) + 5)),
- Status::OutOfRange());
+ EXPECT_EQ(ring.PushBack(span(single_entry_data, sizeof(test_buffer) + 5)),
+ Status::OutOfRange());
EXPECT_EQ(ring.EntryCount(), 0u);
EXPECT_EQ(ring.PeekFront(read_buffer, &read_size), Status::OutOfRange());
EXPECT_EQ(read_size, 0u);
@@ -118,9 +118,8 @@ void SingleEntryWriteReadTest(bool user_data) {
// retain a static `single_entry_buffer_size` during the test. Single
// bytes are varint-encoded to the same value.
uint32_t preamble_byte = i % 128;
- ASSERT_EQ(
- ring.PushBack(std::span(single_entry_data, data_size), preamble_byte),
- OkStatus());
+ ASSERT_EQ(ring.PushBack(span(single_entry_data, data_size), preamble_byte),
+ OkStatus());
ASSERT_EQ(ring.FrontEntryDataSizeBytes(), data_size);
ASSERT_EQ(ring.FrontEntryTotalSizeBytes(), single_entry_total_size);
@@ -128,12 +127,12 @@ void SingleEntryWriteReadTest(bool user_data) {
ASSERT_EQ(ring.PeekFront(read_buffer, &read_size), OkStatus());
ASSERT_EQ(read_size, data_size);
- // ASSERT_THAT(std::span(expect_buffer).last(data_size),
- // testing::ElementsAreArray(std::span(read_buffer, data_size)));
- ASSERT_EQ(memcmp(std::span(expect_buffer).last(data_size).data(),
- read_buffer,
- data_size),
- 0);
+ // ASSERT_THAT(span(expect_buffer).last(data_size),
+ // testing::ElementsAreArray(span(read_buffer, data_size)));
+ ASSERT_EQ(
+ memcmp(
+ span(expect_buffer).last(data_size).data(), read_buffer, data_size),
+ 0);
read_size = 500U;
ASSERT_EQ(ring.PeekFrontWithPreamble(read_buffer, &read_size), OkStatus());
@@ -143,8 +142,8 @@ void SingleEntryWriteReadTest(bool user_data) {
expect_buffer[0] = byte(preamble_byte);
}
- // ASSERT_THAT(std::span(expect_buffer),
- // testing::ElementsAreArray(std::span(read_buffer)));
+ // ASSERT_THAT(span(expect_buffer),
+ // testing::ElementsAreArray(span(read_buffer)));
ASSERT_EQ(memcmp(expect_buffer, read_buffer, single_entry_total_size), 0);
if (user_data) {
@@ -154,7 +153,7 @@ void SingleEntryWriteReadTest(bool user_data) {
OkStatus());
ASSERT_EQ(read_size, data_size);
ASSERT_EQ(user_preamble, preamble_byte);
- ASSERT_EQ(memcmp(std::span(expect_buffer).last(data_size).data(),
+ ASSERT_EQ(memcmp(span(expect_buffer).last(data_size).data(),
read_buffer,
data_size),
0);
@@ -172,7 +171,7 @@ TEST(PrefixedEntryRingBuffer, SingleEntryWriteReadYesUserData) {
SingleEntryWriteReadTest(true);
}
-// TODO(pwbug/196): Increase this to 5000 once we have a way to detect targets
+// TODO(b/234883746): Increase this to 5000 once we have a way to detect targets
// with more computation and memory oomph.
constexpr size_t kOuterCycles = 50u;
constexpr size_t kCountingUpMaxExpectedEntries =
@@ -242,7 +241,7 @@ void SingleEntryWriteReadWithSectionWriterTest(bool user_data) {
EXPECT_EQ(ring.SetBuffer(test_buffer), OkStatus());
- auto output = [](std::span<const byte> src) -> Status {
+ auto output = [](span<const byte> src) -> Status {
for (byte b : src) {
read_buffer.push_back(b);
}
@@ -265,9 +264,8 @@ void SingleEntryWriteReadWithSectionWriterTest(bool user_data) {
// retain a static `single_entry_buffer_size` during the test. Single
// bytes are varint-encoded to the same value.
uint32_t preamble_byte = i % 128;
- ASSERT_EQ(
- ring.PushBack(std::span(single_entry_data, data_size), preamble_byte),
- OkStatus());
+ ASSERT_EQ(ring.PushBack(span(single_entry_data, data_size), preamble_byte),
+ OkStatus());
ASSERT_EQ(ring.FrontEntryDataSizeBytes(), data_size);
ASSERT_EQ(ring.FrontEntryTotalSizeBytes(), single_entry_total_size);
@@ -275,7 +273,7 @@ void SingleEntryWriteReadWithSectionWriterTest(bool user_data) {
ASSERT_EQ(ring.PeekFront(output), OkStatus());
ASSERT_EQ(read_buffer.size(), data_size);
- ASSERT_EQ(memcmp(std::span(expect_buffer).last(data_size).data(),
+ ASSERT_EQ(memcmp(span(expect_buffer).last(data_size).data(),
read_buffer.data(),
data_size),
0);
@@ -320,11 +318,11 @@ void DeringTest(bool preload) {
// Entry data is entry size - preamble (single byte in this case).
byte single_entry_buffer[kEntrySizeBytes - 1u];
- auto entry_data = std::span(single_entry_buffer);
+ auto entry_data = span(single_entry_buffer);
size_t i;
- // TODO(pwbug/196): Increase this to 500 once we have a way to detect targets
- // with more computation and memory oomph.
+ // TODO(b/234883746): Increase this to 500 once we have a way to detect
+ // targets with more computation and memory oomph.
size_t loop_goal = preload ? 50 : 1;
for (size_t main_loop_count = 0; main_loop_count < loop_goal;
@@ -334,8 +332,7 @@ void DeringTest(bool preload) {
// wrapped.
for (i = 0; i < (kTotalEntryCount * (main_loop_count % 64u)); i++) {
memset(single_entry_buffer, i, sizeof(single_entry_buffer));
- ring.PushBack(single_entry_buffer)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), ring.PushBack(single_entry_buffer));
}
}
@@ -353,8 +350,7 @@ void DeringTest(bool preload) {
}
// The ring buffer internally pushes the varint size byte.
- ring.PushBack(single_entry_buffer)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), ring.PushBack(single_entry_buffer));
}
// Check values before doing the dering.
@@ -369,7 +365,7 @@ void DeringTest(bool preload) {
// Read out the entries of the ring buffer.
actual_result.clear();
- auto output = [](std::span<const byte> src) -> Status {
+ auto output = [](span<const byte> src) -> Status {
for (byte b : src) {
actual_result.push_back(b);
}
@@ -438,7 +434,7 @@ T PeekFront(PrefixedEntryRingBufferMulti::Reader& reader,
}
template <typename T>
-T GetEntry(std::span<const std::byte> lhs) {
+T GetEntry(span<const std::byte> lhs) {
union {
std::array<byte, sizeof(T)> buffer;
T item;
@@ -453,7 +449,7 @@ void EmptyDataPushBackTest(bool user_data) {
EXPECT_EQ(ring.SetBuffer(test_buffer), OkStatus());
// Push back an empty span and a non-empty span.
- EXPECT_EQ(ring.PushBack(std::span<std::byte>(), 1u), OkStatus());
+ EXPECT_EQ(ring.PushBack(span<std::byte>(), 1u), OkStatus());
EXPECT_EQ(ring.EntryCount(), 1u);
EXPECT_EQ(ring.PushBack(single_entry_data, 2u), OkStatus());
EXPECT_EQ(ring.EntryCount(), 2u);
diff --git a/pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h b/pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h
index a4e3490df..a1209c256 100644
--- a/pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h
+++ b/pw_ring_buffer/public/pw_ring_buffer/prefixed_entry_ring_buffer.h
@@ -14,10 +14,11 @@
#pragma once
#include <cstddef>
-#include <span>
+#include <limits>
#include "pw_containers/intrusive_list.h"
#include "pw_result/result.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw {
@@ -40,7 +41,7 @@ namespace ring_buffer {
// around as needed.
class PrefixedEntryRingBufferMulti {
public:
- typedef Status (*ReadOutput)(std::span<const std::byte>);
+ typedef Status (*ReadOutput)(span<const std::byte>);
// A reader that provides a single-reader interface into the multi-reader ring
// buffer it has been attached to via AttachReader(). Readers maintain their
@@ -63,13 +64,13 @@ class PrefixedEntryRingBufferMulti {
public:
constexpr Reader() : buffer_(nullptr), read_idx_(0), entry_count_(0) {}
- // TODO(pwbug/344): Add locking to the internal functions. Who owns the
+ // TODO(b/235351035): Add locking to the internal functions. Who owns the
// lock? This class? Does this class need a lock if it's not a multi-reader?
// (One doesn't exist today but presumably nothing prevents push + pop
// operations from happening on two different threads).
// Read the oldest stored data chunk of data from the ring buffer to
- // the provided destination std::span. The number of bytes read is written
+ // the provided destination span. The number of bytes read is written
// to bytes_read
//
// Precondition: the buffer data must not be corrupt, otherwise there will
@@ -79,11 +80,11 @@ class PrefixedEntryRingBufferMulti {
// OK - Data successfully read from the ring buffer.
// FAILED_PRECONDITION - Buffer not initialized.
// OUT_OF_RANGE - No entries in ring buffer to read.
- // RESOURCE_EXHAUSTED - Destination data std::span was smaller number of
+ // RESOURCE_EXHAUSTED - Destination data span was smaller number of
// bytes than the data size of the data chunk being read. Available
// destination bytes were filled, remaining bytes of the data chunk were
// ignored.
- Status PeekFront(std::span<std::byte> data, size_t* bytes_read_out) const {
+ Status PeekFront(span<std::byte> data, size_t* bytes_read_out) const {
return buffer_->InternalPeekFront(*this, data, bytes_read_out);
}
@@ -101,13 +102,14 @@ class PrefixedEntryRingBufferMulti {
// Same as PeekFront but includes the entry's preamble of optional user
// value and the varint of the data size.
- // TODO(pwbug/341): Move all other APIs to passing bytes_read by reference,
- // as it is required to determine the length populated in the span.
- Status PeekFrontWithPreamble(std::span<std::byte> data,
+ // TODO(b/235351847): Move all other APIs to passing bytes_read by
+ // reference, as it is required to determine the length populated in the
+ // span.
+ Status PeekFrontWithPreamble(span<std::byte> data,
uint32_t& user_preamble_out,
size_t& entry_bytes_read_out) const;
- Status PeekFrontWithPreamble(std::span<std::byte> data,
+ Status PeekFrontWithPreamble(span<std::byte> data,
size_t* bytes_read_out) const {
return buffer_->InternalPeekFrontWithPreamble(
*this, data, bytes_read_out);
@@ -174,7 +176,7 @@ class PrefixedEntryRingBufferMulti {
// An entry returned by the iterator containing the byte span of the entry
// and preamble data (if the ring buffer was configured with a preamble).
struct Entry {
- std::span<const std::byte> buffer;
+ span<const std::byte> buffer;
uint32_t preamble;
};
@@ -219,7 +221,7 @@ class PrefixedEntryRingBufferMulti {
private:
static constexpr Entry kEndEntry = {
- .buffer = std::span<const std::byte>(),
+ .buffer = span<const std::byte>(),
.preamble = 0,
};
@@ -248,8 +250,8 @@ class PrefixedEntryRingBufferMulti {
const_iterator cbegin() { return begin(); }
const_iterator cend() { return end(); }
- // TODO(pwbug/340): Consider changing bool to an enum, to explicitly enumerate
- // what this variable means in clients.
+ // TODO(b/235351861): Consider changing bool to an enum, to explicitly
+ // enumerate what this variable means in clients.
PrefixedEntryRingBufferMulti(bool user_preamble = false)
: buffer_(nullptr),
buffer_bytes_(0),
@@ -261,7 +263,7 @@ class PrefixedEntryRingBufferMulti {
// Return values:
// OK - successfully set the raw buffer.
// INVALID_ARGUMENT - Argument was nullptr, size zero, or too large.
- Status SetBuffer(std::span<std::byte> buffer);
+ Status SetBuffer(span<std::byte> buffer);
// Determines if the ring buffer has corrupted entries.
//
@@ -308,15 +310,13 @@ class PrefixedEntryRingBufferMulti {
// OK - Data successfully written to the ring buffer.
// FAILED_PRECONDITION - Buffer not initialized.
// OUT_OF_RANGE - Size of data is greater than buffer size.
- Status PushBack(std::span<const std::byte> data,
- uint32_t user_preamble_data = 0) {
+ Status PushBack(span<const std::byte> data, uint32_t user_preamble_data = 0) {
return InternalPushBack(data, user_preamble_data, true);
}
// [Deprecated] An implementation of PushBack that accepts a single-byte as
// preamble data. Clients should migrate to passing uint32_t preamble data.
- Status PushBack(std::span<const std::byte> data,
- std::byte user_preamble_data) {
+ Status PushBack(span<const std::byte> data, std::byte user_preamble_data) {
return PushBack(data, static_cast<uint32_t>(user_preamble_data));
}
@@ -336,15 +336,14 @@ class PrefixedEntryRingBufferMulti {
// OUT_OF_RANGE - Size of data is greater than buffer size.
// RESOURCE_EXHAUSTED - The ring buffer doesn't have space for the data
// without popping off existing elements.
- Status TryPushBack(std::span<const std::byte> data,
+ Status TryPushBack(span<const std::byte> data,
uint32_t user_preamble_data = 0) {
return InternalPushBack(data, user_preamble_data, false);
}
// [Deprecated] An implementation of TryPushBack that accepts a single-byte as
// preamble data. Clients should migrate to passing uint32_t preamble data.
- Status TryPushBack(std::span<const std::byte> data,
- std::byte user_preamble_data) {
+ Status TryPushBack(span<const std::byte> data, std::byte user_preamble_data) {
return TryPushBack(data, static_cast<uint32_t>(user_preamble_data));
}
@@ -352,6 +351,9 @@ class PrefixedEntryRingBufferMulti {
// including preamble and data chunk.
size_t TotalUsedBytes() const { return buffer_bytes_ - RawAvailableBytes(); }
+ // Returns total size of ring buffer in bytes.
+ size_t TotalSizeBytes() const { return buffer_bytes_; }
+
// Dering the buffer by reordering entries internally in the buffer by
// rotating to have the oldest entry is at the lowest address/index with
// newest entry at the highest address. If no readers are attached, the buffer
@@ -364,7 +366,7 @@ class PrefixedEntryRingBufferMulti {
private:
// Read the oldest stored data chunk of data from the ring buffer to
- // the provided destination std::span. The number of bytes read is written to
+ // the provided destination span. The number of bytes read is written to
// `bytes_read_out`.
//
// Precondition: the buffer data must not be corrupt, otherwise there will
@@ -374,11 +376,11 @@ class PrefixedEntryRingBufferMulti {
// OK - Data successfully read from the ring buffer.
// FAILED_PRECONDITION - Buffer not initialized.
// OUT_OF_RANGE - No entries in ring buffer to read.
- // RESOURCE_EXHAUSTED - Destination data std::span was smaller number of bytes
+ // RESOURCE_EXHAUSTED - Destination data span was smaller number of bytes
// than the data size of the data chunk being read. Available destination
// bytes were filled, remaining bytes of the data chunk were ignored.
Status InternalPeekFront(const Reader& reader,
- std::span<std::byte> data,
+ span<std::byte> data,
size_t* bytes_read_out) const;
Status InternalPeekFront(const Reader& reader, ReadOutput output) const;
@@ -387,7 +389,7 @@ class PrefixedEntryRingBufferMulti {
// Same as Read but includes the entry's preamble of optional user value and
// the varint of the data size
Status InternalPeekFrontWithPreamble(const Reader& reader,
- std::span<std::byte> data,
+ span<std::byte> data,
size_t* bytes_read_out) const;
Status InternalPeekFrontWithPreamble(const Reader& reader,
ReadOutput output) const;
@@ -437,7 +439,7 @@ class PrefixedEntryRingBufferMulti {
// Push back implementation, which optionally discards front elements to fit
// the incoming element.
- Status InternalPushBack(std::span<const std::byte> data,
+ Status InternalPushBack(span<const std::byte> data,
uint32_t user_preamble_data,
bool pop_front_if_needed);
@@ -480,7 +482,7 @@ class PrefixedEntryRingBufferMulti {
// Do the basic write of the specified number of bytes starting at the last
// write index of the ring buffer to the destination, handing any wrap-around
// of the ring buffer. This is basic, raw operation with no safety checks.
- void RawWrite(std::span<const std::byte> source);
+ void RawWrite(span<const std::byte> source);
// Do the basic read of the specified number of bytes starting at the given
// index of the ring buffer to the destination, handing any wrap-around of
@@ -512,7 +514,7 @@ class PrefixedEntryRingBuffer : public PrefixedEntryRingBufferMulti,
PrefixedEntryRingBuffer(bool user_preamble = false)
: PrefixedEntryRingBufferMulti(user_preamble) {
AttachReader(*this)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
};
diff --git a/pw_ring_buffer/size_report/BUILD.bazel b/pw_ring_buffer/size_report/BUILD.bazel
index b05b576d0..b919b9929 100644
--- a/pw_ring_buffer/size_report/BUILD.bazel
+++ b/pw_ring_buffer/size_report/BUILD.bazel
@@ -24,9 +24,17 @@ licenses(["notice"])
pw_cc_binary(
name = "ring_buffer_simple",
srcs = ["ring_buffer_simple.cc"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_ring_buffer",
+ ],
)
pw_cc_binary(
name = "ring_buffer_multi",
srcs = ["ring_buffer_multi.cc"],
+ deps = [
+ "//pw_bloat:bloat_this_binary",
+ "//pw_ring_buffer",
+ ],
)
diff --git a/pw_ring_buffer/size_report/ring_buffer_multi.cc b/pw_ring_buffer/size_report/ring_buffer_multi.cc
index 557046921..c4e9232d1 100644
--- a/pw_ring_buffer/size_report/ring_buffer_multi.cc
+++ b/pw_ring_buffer/size_report/ring_buffer_multi.cc
@@ -36,7 +36,7 @@ int main() {
pw::ring_buffer::PrefixedEntryRingBufferMulti::Reader readers[kReaderCount];
for (auto& reader : readers) {
ring.AttachReader(reader)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
// Push entries until the buffer is full.
@@ -94,7 +94,7 @@ int main() {
for (auto& reader : readers) {
ring.DetachReader(reader)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
ring.Clear();
return 0;
diff --git a/pw_router/BUILD.bazel b/pw_router/BUILD.bazel
index 9047d674d..5e5cc3f73 100644
--- a/pw_router/BUILD.bazel
+++ b/pw_router/BUILD.bazel
@@ -47,6 +47,7 @@ pw_cc_library(
pw_cc_library(
name = "packet_parser",
hdrs = ["public/pw_router/packet_parser.h"],
+ includes = ["public"],
deps = ["//pw_bytes"],
)
diff --git a/pw_router/BUILD.gn b/pw_router/BUILD.gn
index 503ff416c..4af8f1760 100644
--- a/pw_router/BUILD.gn
+++ b/pw_router/BUILD.gn
@@ -30,6 +30,7 @@ pw_source_set("static_router") {
":egress",
":packet_parser",
dir_pw_metric,
+ dir_pw_span,
]
public = [ "public/pw_router/static_router.h" ]
sources = [ "static_router.cc" ]
@@ -41,13 +42,17 @@ pw_source_set("egress") {
public_deps = [
":packet_parser",
dir_pw_bytes,
+ dir_pw_span,
]
}
pw_source_set("packet_parser") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_router/packet_parser.h" ]
- public_deps = [ dir_pw_bytes ]
+ public_deps = [
+ dir_pw_bytes,
+ dir_pw_span,
+ ]
}
pw_source_set("egress_function") {
@@ -56,6 +61,7 @@ pw_source_set("egress_function") {
public_deps = [
":egress",
dir_pw_function,
+ dir_pw_span,
]
}
@@ -76,7 +82,7 @@ pw_test("static_router_test") {
sources = [ "static_router_test.cc" ]
}
-pw_size_report("static_router_size") {
+pw_size_diff("static_router_size") {
title = "pw::router::StaticRouter size report"
binaries = [
{
diff --git a/pw_router/CMakeLists.txt b/pw_router/CMakeLists.txt
index b9e17069b..0bc82fca3 100644
--- a/pw_router/CMakeLists.txt
+++ b/pw_router/CMakeLists.txt
@@ -14,14 +14,18 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_router.static_router
- SOURCES
- static_router.cc
+pw_add_library(pw_router.static_router STATIC
+ HEADERS
+ public/pw_router/static_router.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_metric
pw_router.egress
pw_router.packet_parser
- pw_sync.mutex
+ pw_span
+ SOURCES
+ static_router.cc
PRIVATE_DEPS
pw_log
)
@@ -29,34 +33,54 @@ if(Zephyr_FOUND AND CONFIG_PIGWEED_ROUTER_STATIC_ROUTER)
zephyr_link_libraries(pw_router.static_router)
endif()
-pw_add_module_library(pw_router.egress
+pw_add_library(pw_router.egress INTERFACE
+ HEADERS
+ public/pw_router/egress.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_bytes
pw_router.packet_parser
+ pw_span
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_ROUTER_EGRESS)
zephyr_link_libraries(pw_router.egress)
endif()
-pw_add_module_library(pw_router.packet_parser
+pw_add_library(pw_router.packet_parser INTERFACE
+ HEADERS
+ public/pw_router/packet_parser.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_bytes
+ pw_span
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_ROUTER_PACKET_PARSER)
zephyr_link_libraries(pw_router.packet_parser)
endif()
-pw_add_module_library(pw_router.egress_function
+pw_add_library(pw_router.egress_function INTERFACE
+ HEADERS
+ public/pw_router/egress_function.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_function
pw_router.egress
+ pw_span
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_ROUTER_EGRESS_FUNCTION)
zephyr_link_libraries(pw_router.egress_function)
endif()
-pw_auto_add_module_tests(pw_router
+pw_add_test(pw_router.static_router_test
+ SOURCES
+ static_router_test.cc
PRIVATE_DEPS
pw_router.egress_function
pw_router.static_router
+ GROUPS
+ modules
+ pw_router
)
diff --git a/pw_router/Kconfig b/pw_router/Kconfig
index 090e34e24..da0149796 100644
--- a/pw_router/Kconfig
+++ b/pw_router/Kconfig
@@ -12,11 +12,6 @@
# License for the specific language governing permissions and limitations under
# the License.
-menuconfig PIGWEED_ROUTER
- bool "Enable the Pigweed router library (pw_router)"
-
-if PIGWEED_ROUTER
-
config PIGWEED_ROUTER_STATIC_ROUTER
bool "Enable the Pigweed static router library (pw_router.static_router)"
select PIGWEED_METRIC
@@ -36,5 +31,3 @@ config PIGWEED_ROUTER_PACKET_PARSER
config PIGWEED_ROUTER_EGRESS_FUNCTION
bool "Enable the Pigweed router egress function library (pw_router.egress_function)"
select PIGWEED_RPC_EGRESS
-
-endif # PIGWEED_ROUTER
diff --git a/pw_router/public/pw_router/egress.h b/pw_router/public/pw_router/egress.h
index cf7613b3e..a870dee65 100644
--- a/pw_router/public/pw_router/egress.h
+++ b/pw_router/public/pw_router/egress.h
@@ -14,10 +14,10 @@
#pragma once
#include <optional>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_router/packet_parser.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::router {
diff --git a/pw_router/public/pw_router/egress_function.h b/pw_router/public/pw_router/egress_function.h
index 83620aaff..b8c7df285 100644
--- a/pw_router/public/pw_router/egress_function.h
+++ b/pw_router/public/pw_router/egress_function.h
@@ -13,10 +13,9 @@
// the License.
#pragma once
-#include <span>
-
#include "pw_function/function.h"
#include "pw_router/egress.h"
+#include "pw_span/span.h"
namespace pw::router {
diff --git a/pw_router/public/pw_router/packet_parser.h b/pw_router/public/pw_router/packet_parser.h
index c4ecc8af4..937d5084c 100644
--- a/pw_router/public/pw_router/packet_parser.h
+++ b/pw_router/public/pw_router/packet_parser.h
@@ -13,10 +13,11 @@
// the License.
#pragma once
+#include <cstdint>
#include <optional>
-#include <span>
#include "pw_bytes/span.h"
+#include "pw_span/span.h"
namespace pw::router {
diff --git a/pw_router/public/pw_router/static_router.h b/pw_router/public/pw_router/static_router.h
index 1d2a07bfb..223c8481c 100644
--- a/pw_router/public/pw_router/static_router.h
+++ b/pw_router/public/pw_router/static_router.h
@@ -13,12 +13,11 @@
// the License.
#pragma once
-#include <span>
-
#include "pw_bytes/span.h"
#include "pw_metric/metric.h"
#include "pw_router/egress.h"
#include "pw_router/packet_parser.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::router {
@@ -38,7 +37,7 @@ class StaticRouter {
Egress& egress;
};
- StaticRouter(std::span<const Route> routes) : routes_(routes) {}
+ StaticRouter(span<const Route> routes) : routes_(routes) {}
StaticRouter(const StaticRouter&) = delete;
StaticRouter(StaticRouter&&) = delete;
@@ -63,7 +62,7 @@ class StaticRouter {
Status RoutePacket(ConstByteSpan packet, PacketParser& parser);
private:
- const std::span<const Route> routes_;
+ const span<const Route> routes_;
PW_METRIC_GROUP(metrics_, "static_router");
PW_METRIC(metrics_, parser_errors_, "parser_errors", 0u);
PW_METRIC(metrics_, route_errors_, "route_errors", 0u);
diff --git a/pw_router/size_report/BUILD.gn b/pw_router/size_report/BUILD.gn
index 6f283c015..d9e2e85d4 100644
--- a/pw_router/size_report/BUILD.gn
+++ b/pw_router/size_report/BUILD.gn
@@ -31,7 +31,7 @@ pw_executable("base") {
pw_executable("static_router_with_one_route") {
sources = [ "static_router_with_one_route.cc" ]
deps = _common_deps + [
- "..:static_router",
"..:egress_function",
+ "..:static_router",
]
}
diff --git a/pw_router/static_router_test.cc b/pw_router/static_router_test.cc
index e800f4933..8239e5840 100644
--- a/pw_router/static_router_test.cc
+++ b/pw_router/static_router_test.cc
@@ -30,7 +30,7 @@ struct BasicPacket {
constexpr BasicPacket(uint32_t addr, uint32_t prio, uint64_t data)
: magic(kMagic), address(addr), priority(prio), payload(data) {}
- ConstByteSpan data() const { return std::as_bytes(std::span(this, 1)); }
+ ConstByteSpan data() const { return as_bytes(span(this, 1)); }
uint32_t magic;
uint32_t address;
@@ -55,7 +55,7 @@ class BasicPacketParser : public PacketParser {
uint32_t priority() const {
PW_DCHECK_NOTNULL(packet_);
return packet_->priority;
- };
+ }
private:
const BasicPacket* packet_;
diff --git a/pw_rpc/Android.bp b/pw_rpc/Android.bp
index fbd80605b..9abba4d79 100644
--- a/pw_rpc/Android.bp
+++ b/pw_rpc/Android.bp
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -13,11 +13,6 @@
// the License.
package {
- // See: http://go/android-license-faq
- // A large-scale-change added 'default_applicable_licenses' to import
- // all of the 'license_kinds' from "external_pigweed_license"
- // to get the below license kinds:
- // SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["external_pigweed_license"],
}
@@ -59,3 +54,226 @@ java_library_static {
srcs: ["echo.proto"],
sdk_version: "current",
}
+
+filegroup {
+ name: "pw_rpc_src_files",
+ srcs: [
+ "call.cc",
+ "channel.cc",
+ "channel_list.cc",
+ "client.cc",
+ "client_call.cc",
+ "client_server.cc",
+ "endpoint.cc",
+ "packet.cc",
+ "packet_meta.cc",
+ "server.cc",
+ "server_call.cc",
+ "service.cc",
+ ],
+}
+
+cc_library_headers {
+ name: "pw_rpc_include_dirs",
+ export_include_dirs: [
+ "public",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+// This rule must be instantiated, i.e.
+//
+// cc_library_static {
+// name: "pw_rpc_<instance_name>",
+// defaults: [
+// "pw_rpc_cflags_<instance_name>",
+// "pw_rpc_defaults",
+// ],
+// }
+//
+// where pw_rpc_cflags_<instance_name> defines your flags, i.e.
+//
+// cc_defaults {
+// name: "pw_rpc_cflags_<instance_name>",
+// cflags: [
+// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
+// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_DYNAMIC_ALLOCATION",
+// ],
+// }
+//
+// see pw_rpc_nanopb_defaults, pw_rpc_raw_defaults
+cc_defaults {
+ name: "pw_rpc_defaults",
+ cpp_std: "c++2a",
+ header_libs: [
+ "fuschia_sdk_lib_fit",
+ "fuschia_sdk_lib_stdcompat",
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_function_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_rpc_include_dirs",
+ "pw_span_headers",
+ "pw_sync_baremetal_headers",
+ "pw_sync_headers",
+ "pw_toolchain",
+ ],
+ export_header_lib_headers: [
+ "fuschia_sdk_lib_fit",
+ "fuschia_sdk_lib_stdcompat",
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_function_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_rpc_include_dirs",
+ "pw_span_headers",
+ "pw_sync_baremetal_headers",
+ "pw_sync_headers",
+ "pw_toolchain",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_containers",
+ "pw_protobuf",
+ "pw_status",
+ "pw_stream",
+ "pw_string",
+ "pw_varint",
+ ],
+ export_static_lib_headers: [
+ "pw_bytes",
+ "pw_containers",
+ "pw_protobuf",
+ "pw_status",
+ "pw_stream",
+ "pw_string",
+ "pw_varint",
+ ],
+ generated_headers: [
+ "pw_rpc_internal_packet_pwpb_h",
+ ],
+ export_generated_headers: [
+ "pw_rpc_internal_packet_pwpb_h",
+ ],
+ srcs: [
+ ":pw_rpc_src_files"
+ ],
+ host_supported: true,
+ vendor_available: true,
+}
+
+genrule {
+ name: "pw_rpc_internal_packet_pwpb_h",
+ srcs: ["internal/packet.proto"],
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--out-dir=$$(dirname $(location pw_rpc/internal/packet.pwpb.h)) " +
+ "--plugin-path=$(location pw_protobuf_plugin_py) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--sources $(in) " +
+ "--language pwpb " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc) ",
+ out: [
+ "pw_rpc/internal/packet.pwpb.h",
+ ],
+ tools: [
+ "aprotoc",
+ "pw_protobuf_plugin_py",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+genrule {
+ name: "pw_rpc_internal_packet_py",
+ srcs: ["internal/packet.proto"],
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--sources $(in) " +
+ "--language python " +
+ "--no-generate-type-hints " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ out: [
+ "packet_pb2.py",
+ ],
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Generate cc and header nanopb files.
+// The output file names are based on the srcs file name with a .pb.c / .pb.h extension.
+genrule_defaults {
+ name: "pw_rpc_generate_nanopb_proto",
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location protoc-gen-nanopb) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--language nanopb " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "protoc-gen-nanopb",
+ "pw_protobuf_compiler_py",
+ ],
+}
+
+// Generate the header nanopb RPC file.
+// The output file name is based on the srcs file name with a .rpc.pb.h extension.
+genrule_defaults {
+ name: "pw_rpc_generate_nanopb_rpc_header",
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_nanopb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--language nanopb_rpc " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_nanopb_py",
+ ],
+}
+
+// Generate the header raw RPC file.
+// The output file name is based on the srcs file name with a .raw_rpc.pb.h extension.
+genrule_defaults {
+ name: "pw_rpc_generate_raw_rpc_header",
+ cmd: "python3 $(location pw_protobuf_compiler_py) " +
+ "--plugin-path=$(location pw_rpc_plugin_rawpb_py) " +
+ "--out-dir=$(genDir) " +
+ "--compile-dir=$$(dirname $(in)) " +
+ "--language raw_rpc " +
+ "--sources $(in) " +
+ "--no-experimental-proto3-optional " +
+ "--protoc=$(location aprotoc)",
+ tools: [
+ "aprotoc",
+ "pw_protobuf_compiler_py",
+ "pw_rpc_plugin_rawpb_py",
+ ],
+}
+
+python_library_host {
+ name: "pw_rpc_internal_packet_py_lib",
+ srcs: [
+ ":pw_rpc_internal_packet_py",
+ ],
+ pkg_path: "pw_rpc/internal",
+}
diff --git a/pw_rpc/BUILD.bazel b/pw_rpc/BUILD.bazel
index e29ff15eb..8574e9f53 100644
--- a/pw_rpc/BUILD.bazel
+++ b/pw_rpc/BUILD.bazel
@@ -14,6 +14,7 @@
load("//pw_build:pigweed.bzl", "pw_cc_library", "pw_cc_test")
load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_protobuf_compiler:pw_proto_library.bzl", "pw_proto_filegroup")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_proto_grpc//:defs.bzl", "proto_plugin")
@@ -45,7 +46,7 @@ pw_cc_library(
],
)
-# TODO(pwbug/507): Build this as a cc_binary and use it in integration tests.
+# TODO(b/242059613): Build this as a cc_binary and use it in integration tests.
filegroup(
name = "test_rpc_server",
srcs = ["test_rpc_server.cc"],
@@ -73,12 +74,14 @@ pw_cc_library(
"client_call.cc",
"endpoint.cc",
"packet.cc",
+ "packet_meta.cc",
"public/pw_rpc/internal/call.h",
"public/pw_rpc/internal/call_context.h",
"public/pw_rpc/internal/channel.h",
"public/pw_rpc/internal/channel_list.h",
"public/pw_rpc/internal/client_call.h",
"public/pw_rpc/internal/config.h",
+ "public/pw_rpc/internal/encoding_buffer.h",
"public/pw_rpc/internal/endpoint.h",
"public/pw_rpc/internal/hash.h",
"public/pw_rpc/internal/lock.h",
@@ -89,6 +92,7 @@ pw_cc_library(
"public/pw_rpc/internal/method_union.h",
"public/pw_rpc/internal/packet.h",
"public/pw_rpc/internal/server_call.h",
+ "public/pw_rpc/method_info.h",
"public/pw_rpc/method_type.h",
"public/pw_rpc/writer.h",
"server.cc",
@@ -99,36 +103,97 @@ pw_cc_library(
"public/pw_rpc/channel.h",
"public/pw_rpc/client.h",
"public/pw_rpc/internal/service_client.h",
+ "public/pw_rpc/method_id.h",
+ "public/pw_rpc/method_info.h",
+ "public/pw_rpc/packet_meta.h",
"public/pw_rpc/server.h",
"public/pw_rpc/service.h",
+ "public/pw_rpc/service_id.h",
],
includes = ["public"],
deps = [
":internal_packet_cc.pwpb",
"//pw_assert",
"//pw_bytes",
- "//pw_containers",
"//pw_containers:intrusive_list",
"//pw_function",
"//pw_log",
+ "//pw_preprocessor",
"//pw_result",
"//pw_span",
"//pw_status",
"//pw_sync:lock_annotations",
+ "//pw_sync:mutex",
+ "//pw_thread:sleep",
+ "//pw_toolchain:no_destructor",
],
)
pw_cc_library(
- name = "thread_testing",
- hdrs = ["public/pw_rpc/thread_testing.h"],
+ name = "synchronous_client_api",
+ hdrs = [
+ "public/pw_rpc/synchronous_call.h",
+ "public/pw_rpc/synchronous_call_result.h",
+ ],
includes = ["public"],
deps = [
+ ":pw_rpc",
+ "//pw_chrono:system_clock",
+ "//pw_sync:timed_thread_notification",
+ ],
+)
+
+pw_cc_library(
+ name = "client_server_testing",
+ hdrs = ["public/pw_rpc/internal/client_server_testing.h"],
+ includes = ["public"],
+ deps = [
+ ":client_server",
":internal_test_utils",
+ "//pw_bytes",
+ "//pw_result",
+ ],
+)
+
+pw_cc_library(
+ name = "client_server_testing_threaded",
+ hdrs = ["public/pw_rpc/internal/client_server_testing_threaded.h"],
+ includes = ["public"],
+ deps = [
+ ":client_server_testing",
+ "//pw_bytes",
+ "//pw_result",
+ "//pw_sync:binary_semaphore",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:mutex",
+ "//pw_thread:thread",
+ ],
+)
+
+pw_cc_library(
+ name = "test_helpers",
+ hdrs = ["public/pw_rpc/test_helpers.h"],
+ includes = ["public"],
+ deps = [
+ ":internal_test_utils",
+ ":pw_rpc",
"//pw_assert",
+ "//pw_chrono:system_clock",
+ "//pw_status",
"//pw_sync:counting_semaphore",
+ "//pw_thread:yield",
],
)
+# thread_testing target is kept for backward compatibility.
+# New code should use test_helpers instead.
+pw_cc_library(
+ name = "thread_testing",
+ hdrs = ["public/pw_rpc/thread_testing.h"],
+ includes = ["public"],
+ deps = [":test_helpers"],
+)
+
pw_cc_library(
name = "internal_test_utils",
srcs = ["fake_channel_output.cc"],
@@ -160,29 +225,28 @@ pw_cc_library(
],
)
-# TODO(pwbug/507): Enable this library when logging_event_handler can be used.
-filegroup(
+pw_cc_library(
name = "integration_testing",
srcs = [
"integration_testing.cc",
- # ],
- # hdrs = [
+ ],
+ hdrs = [
"public/pw_rpc/integration_test_socket_client.h",
"public/pw_rpc/integration_testing.h",
],
- #deps = [
- # ":client",
- # "//pw_assert",
- # "//pw_hdlc:pw_rpc",
- # "//pw_hdlc:rpc_channel_output",
- # "//pw_log",
- # "//pw_stream:socket_stream",
- # "//pw_unit_test",
- # "//pw_unit_test:logging_event_handler",
- #],
+ deps = [
+ ":pw_rpc",
+ "//pw_assert",
+ "//pw_hdlc:pw_rpc",
+ "//pw_hdlc:rpc_channel_output",
+ "//pw_log",
+ "//pw_stream:socket_stream",
+ "//pw_unit_test",
+ "//pw_unit_test:logging_event_handler",
+ ],
)
-# TODO(pwbug/507): Add the client integration test to the build.
+# TODO(b/242059613): Add the client integration test to the build.
filegroup(
name = "client_integration_test",
srcs = ["client_integration_test.cc"],
@@ -200,6 +264,21 @@ pw_cc_test(
)
pw_cc_test(
+ name = "callback_test",
+ srcs = ["callback_test.cc"],
+ deps = [
+ ":pw_rpc",
+ ":pw_rpc_test_cc.raw_rpc",
+ "//pw_rpc/raw:client_testing",
+ "//pw_sync:binary_semaphore",
+ "//pw_thread:sleep",
+ "//pw_thread:test_threads_header",
+ "//pw_thread:yield",
+ "//pw_thread_stl:test_threads",
+ ],
+)
+
+pw_cc_test(
name = "channel_test",
srcs = ["channel_test.cc"],
deps = [
@@ -228,6 +307,16 @@ pw_cc_test(
)
pw_cc_test(
+ name = "packet_meta_test",
+ srcs = [
+ "packet_meta_test.cc",
+ ],
+ deps = [
+ ":pw_rpc",
+ ],
+)
+
+pw_cc_test(
name = "client_server_test",
srcs = ["client_server_test.cc"],
deps = [
@@ -267,6 +356,13 @@ pw_cc_test(
deps = [":internal_test_utils"],
)
+# TODO(b/234874064): Add test_helpers_test when it is possible to use .options
+# in bazel build.
+filegroup(
+ name = "test_helpers_test",
+ srcs = ["test_helpers_test.cc"],
+)
+
proto_library(
name = "internal_packet_proto",
srcs = ["internal/packet.proto"],
@@ -290,8 +386,11 @@ pw_proto_library(
proto_library(
name = "pw_rpc_test_proto",
- srcs = ["pw_rpc_test_protos/test.proto"],
- strip_import_prefix = "//pw_rpc",
+ srcs = [
+ "pw_rpc_test_protos/no_package.proto",
+ "pw_rpc_test_protos/test.proto",
+ ],
+ strip_import_prefix = "/pw_rpc",
)
pw_proto_library(
@@ -336,11 +435,31 @@ proto_plugin(
visibility = ["//visibility:public"],
)
+proto_plugin(
+ name = "pw_cc_plugin_pwpb_rpc",
+ outputs = [
+ "{protopath}.rpc.pwpb.h",
+ ],
+ protoc_plugin_name = "pwpb_rpc",
+ tool = "@pigweed//pw_rpc/py:plugin_pwpb",
+ use_built_in_shell_environment = True,
+ visibility = ["//visibility:public"],
+)
+
+pw_proto_filegroup(
+ name = "echo_proto_and_options",
+ srcs = ["echo.proto"],
+ options_files = ["echo.options"],
+)
+
proto_library(
name = "echo_proto",
- srcs = [
- "echo.proto",
- ],
+ srcs = [":echo_proto_and_options"],
+)
+
+py_proto_library(
+ name = "echo_py_pb2",
+ srcs = ["echo.proto"],
)
pw_proto_library(
diff --git a/pw_rpc/BUILD.gn b/pw_rpc/BUILD.gn
index 14539c593..18c8778e3 100644
--- a/pw_rpc/BUILD.gn
+++ b/pw_rpc/BUILD.gn
@@ -18,10 +18,13 @@ import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/python.gni")
import("$dir_pw_build/python_action.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_compilation_testing/negative_compilation_test.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_sync/backend.gni")
import("$dir_pw_third_party/nanopb/nanopb.gni")
+import("$dir_pw_thread/backend.gni")
import("$dir_pw_unit_test/test.gni")
import("config.gni")
import("internal/integration_test_ports.gni")
@@ -31,13 +34,27 @@ config("public_include_path") {
visibility = [ ":*" ]
}
+config("disable_global_mutex_config") {
+ defines = [
+ "PW_RPC_USE_GLOBAL_MUTEX=0",
+ "PW_RPC_YIELD_MODE=PW_RPC_YIELD_MODE_BUSY_LOOP",
+ ]
+ visibility = [ ":*" ]
+}
+
+# Set pw_rpc_CONFIG to this to disable the global mutex. If additional options
+# are needed, a config target that sets those can depend on this.
+group("disable_global_mutex") {
+ public_configs = [ ":disable_global_mutex_config" ]
+}
+
config("global_mutex_config") {
defines = [ "PW_RPC_USE_GLOBAL_MUTEX=1" ]
visibility = [ ":*" ]
}
-# Set pw_rpc_CONFIG to this to enable the global mutex. If additional options
-# are needed, a config target that sets those can depend on this.
+# Set pw_rpc_CONFIG to this to always enable the global mutex. The mutex is
+# enabled by default, so this is a no-op.
group("use_global_mutex") {
public_configs = [ ":global_mutex_config" ]
}
@@ -88,10 +105,12 @@ pw_source_set("client") {
public_deps = [
":common",
dir_pw_result,
+ dir_pw_span,
]
deps = [
":log_config",
dir_pw_log,
+ dir_pw_preprocessor,
]
public = [
"public/pw_rpc/client.h",
@@ -115,6 +134,20 @@ pw_source_set("client_server") {
sources = [ "client_server.cc" ]
}
+pw_source_set("synchronous_client_api") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":client",
+ ":common",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_sync:timed_thread_notification",
+ ]
+ public = [
+ "public/pw_rpc/synchronous_call.h",
+ "public/pw_rpc/synchronous_call_result.h",
+ ]
+}
+
# Classes shared by the server and client.
pw_source_set("common") {
public_configs = [ ":public_include_path" ]
@@ -123,9 +156,11 @@ pw_source_set("common") {
":protos.pwpb",
"$dir_pw_containers:intrusive_list",
"$dir_pw_sync:lock_annotations",
+ "$dir_pw_toolchain:no_destructor",
dir_pw_assert,
dir_pw_bytes,
dir_pw_function,
+ dir_pw_span,
dir_pw_status,
]
@@ -137,8 +172,22 @@ pw_source_set("common") {
":log_config",
dir_pw_log,
]
+
+ # pw_rpc needs a way to yield the current thread. Depending on its
+ # configuration, it may need either pw_thread:sleep or pw_thread:yield.
+ if (pw_thread_SLEEP_BACKEND != "") {
+ deps += [ "$dir_pw_thread:sleep" ]
+ }
+ if (pw_thread_YIELD_BACKEND != "") {
+ deps += [ "$dir_pw_thread:yield" ]
+ }
+
public = [
"public/pw_rpc/channel.h",
+ "public/pw_rpc/method_id.h",
+ "public/pw_rpc/method_info.h",
+ "public/pw_rpc/packet_meta.h",
+ "public/pw_rpc/service_id.h",
"public/pw_rpc/writer.h",
]
sources = [
@@ -147,10 +196,12 @@ pw_source_set("common") {
"channel_list.cc",
"endpoint.cc",
"packet.cc",
+ "packet_meta.cc",
"public/pw_rpc/internal/call.h",
"public/pw_rpc/internal/call_context.h",
"public/pw_rpc/internal/channel.h",
"public/pw_rpc/internal/channel_list.h",
+ "public/pw_rpc/internal/encoding_buffer.h",
"public/pw_rpc/internal/endpoint.h",
"public/pw_rpc/internal/lock.h",
"public/pw_rpc/internal/method_info.h",
@@ -188,15 +239,52 @@ pw_source_set("fake_channel_output") {
visibility = [ "./*" ]
}
-pw_source_set("thread_testing") {
- public = [ "public/pw_rpc/thread_testing.h" ]
+pw_source_set("client_server_testing") {
+ public = [ "public/pw_rpc/internal/client_server_testing.h" ]
+ public_configs = [ ":public_include_path" ]
public_deps = [
+ ":client_server",
":fake_channel_output",
+ "$dir_pw_bytes",
+ "$dir_pw_result",
+ ]
+ visibility = [ "./*" ]
+}
+
+pw_source_set("client_server_testing_threaded") {
+ public = [ "public/pw_rpc/internal/client_server_testing_threaded.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":client_server_testing",
+ "$dir_pw_bytes",
+ "$dir_pw_result",
+ "$dir_pw_sync:binary_semaphore",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_thread:thread",
+ ]
+ visibility = [ "./*" ]
+}
+
+pw_source_set("test_helpers") {
+ public = [ "public/pw_rpc/test_helpers.h" ]
+ public_deps = [
+ ":fake_channel_output",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_status",
"$dir_pw_sync:counting_semaphore",
+ "$dir_pw_thread:yield",
dir_pw_assert,
]
}
+# thread_testing target is kept for backward compatibility.
+# New code should use test_helpers instead.
+pw_source_set("thread_testing") {
+ public = [ "public/pw_rpc/thread_testing.h" ]
+ public_deps = [ ":test_helpers" ]
+}
+
pw_source_set("test_utils") {
public = [
"public/pw_rpc/internal/fake_channel_output.h",
@@ -233,6 +321,7 @@ pw_source_set("integration_testing") {
"$dir_pw_stream:socket_stream",
"$dir_pw_unit_test:logging_event_handler",
dir_pw_assert,
+ dir_pw_function,
dir_pw_unit_test,
]
deps = [ dir_pw_log ]
@@ -260,6 +349,8 @@ pw_executable("client_integration_test") {
dir_pw_unit_test,
]
+ deps += [ "pwpb:client_integration_test" ]
+
if (dir_pw_third_party_nanopb != "") {
deps += [ "nanopb:client_integration_test" ]
}
@@ -293,7 +384,6 @@ pw_proto_library("protos") {
"echo.options",
"benchmark.options",
]
- deps = [ "$dir_pw_protobuf:common_protos" ]
python_package = "py"
prefix = "pw_rpc"
}
@@ -320,13 +410,14 @@ pw_doc_group("docs") {
]
group_deps = [
"nanopb:docs",
+ "pwpb:docs",
"py:docs",
"ts:docs",
]
report_deps = [ ":server_size" ]
}
-pw_size_report("server_size") {
+pw_size_diff("server_size") {
title = "Pigweed RPC server size report"
binaries = [
@@ -351,23 +442,31 @@ pw_size_report("server_size") {
pw_test_group("tests") {
tests = [
":call_test",
+ ":callback_test",
":channel_test",
":client_server_test",
+ ":test_helpers_test",
":fake_channel_output_test",
":method_test",
":ids_test",
":packet_test",
+ ":packet_meta_test",
":server_test",
":service_test",
]
group_deps = [
+ "fuzz:tests",
"nanopb:tests",
+ "pwpb:tests",
"raw:tests",
]
}
pw_proto_library("test_protos") {
- sources = [ "pw_rpc_test_protos/test.proto" ]
+ sources = [
+ "pw_rpc_test_protos/no_package.proto",
+ "pw_rpc_test_protos/test.proto",
+ ]
inputs = [ "pw_rpc_test_protos/test.options" ]
visibility = [ "./*" ]
}
@@ -380,6 +479,22 @@ pw_test("call_test") {
sources = [ "call_test.cc" ]
}
+pw_test("callback_test") {
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ deps = [
+ ":client",
+ ":server",
+ ":test_protos.raw_rpc",
+ "$dir_pw_sync:binary_semaphore",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread:yield",
+ "$dir_pw_thread_stl:test_threads",
+ "raw:client_testing",
+ ]
+ sources = [ "callback_test.cc" ]
+}
+
pw_test("channel_test") {
deps = [
":server",
@@ -416,6 +531,14 @@ pw_test("packet_test") {
sources = [ "packet_test.cc" ]
}
+pw_test("packet_meta_test") {
+ deps = [
+ ":server",
+ dir_pw_bytes,
+ ]
+ sources = [ "packet_meta_test.cc" ]
+}
+
pw_test("service_test") {
deps = [
":protos.pwpb",
@@ -456,3 +579,21 @@ pw_test("fake_channel_output_test") {
deps = [ ":test_utils" ]
sources = [ "fake_channel_output_test.cc" ]
}
+
+pw_test("test_helpers_test") {
+ deps = [
+ ":test_helpers",
+ "$dir_pw_result",
+ "$dir_pw_status",
+ "$dir_pw_sync:interrupt_spin_lock",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:timed_thread_notification",
+ "pwpb:client_testing",
+ "pwpb:echo_service",
+ "pwpb:server_api",
+ ]
+ sources = [ "test_helpers_test.cc" ]
+ enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != "" &&
+ pw_sync_COUNTING_SEMAPHORE_BACKEND != "" &&
+ pw_chrono_SYSTEM_CLOCK_BACKEND != ""
+}
diff --git a/pw_rpc/CMakeLists.txt b/pw_rpc/CMakeLists.txt
index 7d5b2cf47..37b7cb0c9 100644
--- a/pw_rpc/CMakeLists.txt
+++ b/pw_rpc/CMakeLists.txt
@@ -15,17 +15,43 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
-if(NOT "${dir_pw_third_party_nanopb}" STREQUAL "")
- add_subdirectory(nanopb)
-endif()
-
+add_subdirectory(nanopb)
+add_subdirectory(pwpb)
add_subdirectory(raw)
add_subdirectory(system_server)
-pw_add_module_library(pw_rpc.server
+pw_add_module_config(pw_rpc_CONFIG)
+
+pw_add_library(pw_rpc.config INTERFACE
+ HEADERS
+ public/pw_rpc/internal/config.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ ${pw_rpc_CONFIG}
+)
+
+pw_add_library(pw_rpc.log_config INTERFACE
+ HEADERS
+ public/pw_rpc/internal/log_config.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.config
+)
+
+pw_add_library(pw_rpc.server STATIC
+ HEADERS
+ public/pw_rpc/server.h
+ public/pw_rpc/service.h
+ public/pw_rpc/internal/hash.h
+ public/pw_rpc/internal/method.h
+ public/pw_rpc/internal/method_lookup.h
+ public/pw_rpc/internal/method_union.h
+ public/pw_rpc/internal/server_call.h
+ PUBLIC_INCLUDES
+ public
SOURCES
- call.cc
- endpoint.cc
server.cc
server_call.cc
service.cc
@@ -33,90 +59,380 @@ pw_add_module_library(pw_rpc.server
pw_rpc.common
PRIVATE_DEPS
pw_log
+ pw_rpc.log_config
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_SERVER)
zephyr_link_libraries(pw_rpc.server)
endif()
-pw_add_module_library(pw_rpc.client
+pw_add_library(pw_rpc.client STATIC
+ HEADERS
+ public/pw_rpc/client.h
+ public/pw_rpc/internal/client_call.h
+ public/pw_rpc/internal/service_client.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_result
+ pw_rpc.common
+ pw_span
SOURCES
client.cc
client_call.cc
- PUBLIC_DEPS
- pw_rpc.common
- pw_result
PRIVATE_DEPS
pw_log
+ pw_rpc.log_config
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_CLIENT)
zephyr_link_libraries(pw_rpc.client)
endif()
-pw_add_module_library(pw_rpc.client_server
- SOURCES
- client_server.cc
+pw_add_library(pw_rpc.client_server STATIC
+ HEADERS
+ public/pw_rpc/client_server.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_rpc.client
pw_rpc.server
+ SOURCES
+ client_server.cc
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_CLIENT_SERVER)
zephyr_link_libraries(pw_rpc.client_server)
endif()
-pw_add_module_library(pw_rpc.common
- SOURCES
- channel.cc
- channel_list.cc
- packet.cc
+pw_add_library(pw_rpc.synchronous_client_api INTERFACE
+ HEADERS
+ public/pw_rpc/synchronous_call.h
+ public/pw_rpc/synchronous_call_result.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_rpc.client
+ pw_rpc.common
+ pw_sync.timed_thread_notification
+)
+
+pw_add_library(pw_rpc.common STATIC
+ HEADERS
+ public/pw_rpc/channel.h
+ public/pw_rpc/internal/call.h
+ public/pw_rpc/internal/call_context.h
+ public/pw_rpc/internal/channel.h
+ public/pw_rpc/internal/channel_list.h
+ public/pw_rpc/internal/encoding_buffer.h
+ public/pw_rpc/internal/endpoint.h
+ public/pw_rpc/internal/lock.h
+ public/pw_rpc/internal/method_info.h
+ public/pw_rpc/internal/packet.h
+ public/pw_rpc/method_id.h
+ public/pw_rpc/method_info.h
+ public/pw_rpc/method_type.h
+ public/pw_rpc/packet_meta.h
+ public/pw_rpc/service_id.h
+ public/pw_rpc/writer.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_assert
pw_bytes
- pw_containers
+ pw_containers.intrusive_list
pw_function
+ pw_rpc.config
+ pw_rpc.protos.pwpb
pw_span
pw_status
pw_sync.lock_annotations
- pw_rpc.protos.pwpb
+ pw_toolchain.no_destructor
+ SOURCES
+ call.cc
+ channel.cc
+ channel_list.cc
+ endpoint.cc
+ packet.cc
+ packet_meta.cc
PRIVATE_DEPS
pw_log
+ pw_preprocessor
+ pw_rpc.log_config
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_COMMON)
- zephyr_link_libraries(pw_rpc.common)
+if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
+ pw_target_link_targets(pw_rpc.common PUBLIC pw_sync.mutex)
+endif()
+
+if(NOT "${pw_thread.sleep_BACKEND}" STREQUAL "")
+ pw_target_link_targets(pw_rpc.common PUBLIC pw_thread.sleep)
endif()
-if (NOT "${pw_sync.mutex_BACKEND}" STREQUAL "pw_sync.mutex.NO_BACKEND_SET" AND
- NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
- target_link_libraries(pw_rpc.common PUBLIC pw_sync.mutex)
+if(NOT "${pw_thread.yield_BACKEND}" STREQUAL "")
+ pw_target_link_targets(pw_rpc.common PUBLIC pw_thread.yield)
endif()
-pw_add_module_library(pw_rpc.test_utils
+if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_COMMON)
+ zephyr_link_libraries(pw_rpc.common)
+endif()
+
+pw_add_library(pw_rpc.fake_channel_output STATIC
+ HEADERS
+ public/pw_rpc/internal/fake_channel_output.h
+ public/pw_rpc/payloads_view.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_bytes
+ pw_containers.filtered_view
+ pw_containers.vector
+ pw_containers.wrapped_iterator
+ pw_function
+ pw_rpc.common
+ pw_sync.mutex
SOURCES
fake_channel_output.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_rpc.log_config
+)
+
+pw_add_library(pw_rpc.client_server_testing INTERFACE
+ HEADERS
+ public/pw_rpc/internal/client_server_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_result
+ pw_rpc.client_server
+ pw_rpc.fake_channel_output
+)
+
+pw_add_library(pw_rpc.client_server_testing_threaded INTERFACE
+ HEADERS
+ public/pw_rpc/internal/client_server_testing_threaded.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_result
+ pw_rpc.client_server_testing
+ pw_sync.binary_semaphore
+ pw_sync.lock_annotations
+ pw_sync.mutex
+ pw_thread.thread
+)
+
+pw_add_library(pw_rpc.test_helpers INTERFACE
+ HEADERS
+ public/pw_rpc/test_helpers.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_chrono.system_clock
+ pw_rpc.fake_channel_output
+ pw_status
+ pw_sync.counting_semaphore
+ pw_thread.yield
+)
+
+# thread_testing target is kept for backward compatibility.
+# New code should use pw_rpc.test_helpers instead.
+pw_add_library(pw_rpc.thread_testing INTERFACE
+ HEADERS
+ public/pw_rpc/thread_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.test_helpers
+)
+
+pw_add_library(pw_rpc.test_utils INTERFACE
+ HEADERS
+ public/pw_rpc/internal/fake_channel_output.h
+ public/pw_rpc/internal/method_impl_tester.h
+ public/pw_rpc/internal/method_info_tester.h
+ public/pw_rpc/internal/test_method.h
+ public/pw_rpc/internal/test_method_context.h
+ public/pw_rpc/internal/test_utils.h
+ pw_rpc_private/fake_server_reader_writer.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_assert
pw_bytes
pw_rpc.client
pw_rpc.server
+ pw_containers.vector
+ pw_rpc.raw.fake_channel_output
+ pw_rpc.raw.server_api
+)
+
+pw_add_library(pw_rpc.integration_testing STATIC
+ HEADERS
+ public/pw_rpc/integration_test_socket_client.h
+ public/pw_rpc/integration_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_function
+ pw_hdlc.pw_rpc
+ pw_hdlc.rpc_channel_output
+ pw_rpc.client
+ pw_stream.socket_stream
+ pw_unit_test
+ pw_unit_test.logging_event_handler
+ SOURCES
+ integration_testing.cc
+ PRIVATE_DEPS
+ pw_log
)
-target_include_directories(pw_rpc.test_utils PUBLIC .)
pw_proto_library(pw_rpc.protos
SOURCES
+ benchmark.proto
internal/packet.proto
echo.proto
INPUTS
echo.options
+ benchmark.options
PREFIX
pw_rpc
)
pw_proto_library(pw_rpc.test_protos
SOURCES
+ pw_rpc_test_protos/no_package.proto
pw_rpc_test_protos/test.proto
+ INPUTS
+ pw_rpc_test_protos/test.options
)
-pw_auto_add_module_tests(pw_rpc
+# Set pw_rpc_CONFIG to this to disable the global mutex.
+pw_add_library(pw_rpc.disable_global_mutex_config INTERFACE
+ PUBLIC_DEFINES
+ PW_RPC_USE_GLOBAL_MUTEX=0
+)
+
+pw_add_test(pw_rpc.call_test
+ SOURCES
+ call_test.cc
PRIVATE_DEPS
- pw_rpc.client
pw_rpc.server
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.channel_test
+ SOURCES
+ channel_test.cc
+ PRIVATE_DEPS
+ pw_rpc.server
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.packet_test
+ SOURCES
+ packet_test.cc
+ PRIVATE_DEPS
+ pw_bytes
+ pw_protobuf
+ pw_rpc.server
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.packet_meta_test
+ SOURCES
+ packet_meta_test.cc
+ PRIVATE_DEPS
+ pw_bytes
+ pw_rpc.server
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.service_test
+ SOURCES
+ service_test.cc
+ PRIVATE_DEPS
+ pw_assert
+ pw_rpc.protos.pwpb
+ pw_rpc.server
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.client_server_test
+ SOURCES
+ client_server_test.cc
+ PRIVATE_DEPS
+ pw_rpc.client_server
+ pw_rpc.test_utils
+ pw_rpc.raw.server_api
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.method_test
+ SOURCES
+ method_test.cc
+ PRIVATE_DEPS
+ pw_rpc.server
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.server_test
+ SOURCES
+ server_test.cc
+ PRIVATE_DEPS
+ pw_assert
+ pw_rpc.protos.pwpb
+ pw_rpc.server
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.fake_channel_output_test
+ SOURCES
+ fake_channel_output_test.cc
+ PRIVATE_DEPS
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc
+)
+
+pw_add_test(pw_rpc.test_helpers_test
+ SOURCES
+ test_helpers_test.cc
+ PRIVATE_DEPS
+ pw_result
+ pw_rpc.pwpb.client_testing
+ pw_rpc.pwpb.echo_service
+ pw_rpc.pwpb.server_api
+ pw_rpc.test_helpers
+ pw_status
+ pw_sync.interrupt_spin_lock
+ pw_sync.lock_annotations
+ pw_sync.timed_thread_notification
+ GROUPS
+ modules
+ pw_rpc
)
diff --git a/pw_rpc/Kconfig b/pw_rpc/Kconfig
index 903e320e9..f01276ff7 100644
--- a/pw_rpc/Kconfig
+++ b/pw_rpc/Kconfig
@@ -12,11 +12,6 @@
# License for the specific language governing permissions and limitations under
# the License.
-menuconfig PIGWEED_RPC
- bool "Pigweed RPC submodule"
-
-if PIGWEED_RPC
-
rsource "nanopb/Kconfig"
config PIGWEED_RPC_SERVER
@@ -44,5 +39,4 @@ config PIGWEED_RPC_COMMON
select PIGWEED_SPAN
select PIGWEED_STATUS
select PIGWEED_LOG
-
-endif # PIGWEED_RPC
+ select PIGWEED_SYNC_MUTEX
diff --git a/pw_rpc/benchmark.cc b/pw_rpc/benchmark.cc
index b906e0c48..dfa11b64e 100644
--- a/pw_rpc/benchmark.cc
+++ b/pw_rpc/benchmark.cc
@@ -35,7 +35,7 @@ void BenchmarkService::UnaryEcho(ConstByteSpan request,
RawUnaryResponder& responder) {
std::byte response[32];
StatusWithSize result = CopyBuffer(request, response);
- responder.Finish(std::span(response).first(result.size()), result.status())
+ responder.Finish(span(response).first(result.size()), result.status())
.IgnoreError();
}
@@ -47,7 +47,7 @@ void BenchmarkService::BidirectionalEcho(
Status status = reader_writer_.Write(request);
if (!status.ok()) {
reader_writer_.Finish(status)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
});
}
diff --git a/pw_rpc/benchmark.rst b/pw_rpc/benchmark.rst
index 6e97ac1d0..5ecc136a8 100644
--- a/pw_rpc/benchmark.rst
+++ b/pw_rpc/benchmark.rst
@@ -49,3 +49,23 @@ Example
server.RegisterService(benchmark_service);
}
+Stress Test
+===========
+.. attention::
+ This section is experimental and liable to change.
+
+The Benchmark service is also used as part of a stress test of the ``pw_rpc``
+module. This stress test is implemented as an unguided fuzzer that uses
+multiple worker threads to perform generated sequences of actions using RPC
+``Call`` objects. The test is included as an integration test, and can found and
+be run locally using GN:
+
+.. code-block:: bash
+
+ $ gn desc out //:integration_tests deps | grep fuzz
+ //pw_rpc/fuzz:cpp_client_server_fuzz_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)
+
+ $ gn outputs out '//pw_rpc/fuzz:cpp_client_server_fuzz_test(//targets/host/pigweed_internal:pw_strict_host_clang_debug)'
+ pw_strict_host_clang_debug/gen/pw_rpc/fuzz/cpp_client_server_fuzz_test.pw_pystamp
+
+ $ ninja -C out pw_strict_host_clang_debug/gen/pw_rpc/fuzz/cpp_client_server_fuzz_test.pw_pystamp
diff --git a/pw_rpc/call.cc b/pw_rpc/call.cc
index e4afcb5de..ffb94150b 100644
--- a/pw_rpc/call.cc
+++ b/pw_rpc/call.cc
@@ -15,54 +15,126 @@
#include "pw_rpc/internal/call.h"
#include "pw_assert/check.h"
+#include "pw_log/log.h"
+#include "pw_preprocessor/util.h"
#include "pw_rpc/client.h"
+#include "pw_rpc/internal/encoding_buffer.h"
#include "pw_rpc/internal/endpoint.h"
#include "pw_rpc/internal/method.h"
#include "pw_rpc/server.h"
+// If the callback timeout is enabled, count the number of iterations of the
+// waiting loop and crash if it exceeds PW_RPC_CALLBACK_TIMEOUT_TICKS.
+#if PW_RPC_CALLBACK_TIMEOUT_TICKS > 0
+#define PW_RPC_CHECK_FOR_DEADLOCK(timeout_source, call) \
+ iterations += 1; \
+ PW_CHECK( \
+ iterations < PW_RPC_CALLBACK_TIMEOUT_TICKS, \
+ "A callback for RPC %u:%08x/%08x has not finished after " \
+ PW_STRINGIFY(PW_RPC_CALLBACK_TIMEOUT_TICKS) \
+ " ticks. This may indicate that an RPC callback attempted to " \
+ timeout_source \
+ " its own call object, which is not permitted. Fix this condition or " \
+ "change the value of PW_RPC_CALLBACK_TIMEOUT_TICKS to avoid this " \
+ "crash. See https://pigweed.dev/pw_rpc" \
+ "#destructors-moves-wait-for-callbacks-to-complete for details.", \
+ static_cast<unsigned>((call).channel_id_), \
+ static_cast<unsigned>((call).service_id_), \
+ static_cast<unsigned>((call).method_id_))
+#else
+#define PW_RPC_CHECK_FOR_DEADLOCK(timeout_source, call) \
+ static_cast<void>(iterations)
+#endif // PW_RPC_CALLBACK_TIMEOUT_TICKS > 0
+
namespace pw::rpc::internal {
+using pwpb::PacketType;
+
+// Creates an active server-side Call.
+Call::Call(const LockedCallContext& context, CallProperties properties)
+ : Call(context.server().ClaimLocked(),
+ context.call_id(),
+ context.channel_id(),
+ UnwrapServiceId(context.service().service_id()),
+ context.method().id(),
+ properties) {}
+
// Creates an active client-side call, assigning it a new ID.
-Call::Call(Endpoint& client,
+Call::Call(LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
- MethodType type)
+ CallProperties properties)
: Call(client,
client.NewCallId(),
channel_id,
service_id,
method_id,
- type,
- kClientCall) {}
+ properties) {}
-Call::Call(Endpoint& endpoint_ref,
+Call::Call(LockedEndpoint& endpoint_ref,
uint32_t call_id,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
- MethodType type,
- CallType call_type)
+ CallProperties properties)
: endpoint_(&endpoint_ref),
channel_id_(channel_id),
id_(call_id),
service_id_(service_id),
method_id_(method_id),
- rpc_state_(kActive),
- type_(type),
- call_type_(call_type),
- client_stream_state_(HasClientStream(type) ? kClientStreamActive
- : kClientStreamInactive) {
+ state_(kActive | (HasClientStream(properties.method_type())
+ ? static_cast<uint8_t>(kClientStreamActive)
+ : 0u)),
+ awaiting_cleanup_(OkStatus().code()),
+ callbacks_executing_(0),
+ properties_(properties) {
+ PW_CHECK_UINT_NE(channel_id,
+ Channel::kUnassignedChannelId,
+ "Calls cannot be created with channel ID 0 "
+ "(Channel::kUnassignedChannelId)");
endpoint().RegisterCall(*this);
}
+Call::~Call() {
+ // Note: this explicit deregistration is necessary to ensure that
+ // modifications to the endpoint call list occur while holding rpc_lock.
+ // Removing this explicit registration would result in unsynchronized
+ // modification of the endpoint call list via the destructor of the
+ // superclass `IntrusiveList<Call>::Item`.
+ RpcLockGuard lock;
+
+ // This `active_locked()` guard is necessary to ensure that `endpoint()` is
+ // still valid.
+ if (active_locked()) {
+ endpoint().UnregisterCall(*this);
+ }
+
+ do {
+ int iterations = 0;
+ while (CallbacksAreRunning()) {
+ PW_RPC_CHECK_FOR_DEADLOCK("destroy", *this);
+ YieldRpcLock();
+ }
+
+ } while (CleanUpIfRequired());
+
+ // Help prevent dangling references in callbacks by waiting for callbacks to
+ // complete before deleting this call.
+}
+
void Call::MoveFrom(Call& other) {
PW_DCHECK(!active_locked());
+ PW_DCHECK(!awaiting_cleanup() && !other.awaiting_cleanup());
if (!other.active_locked()) {
return; // Nothing else to do; this call is already closed.
}
+ // An active call with an executing callback cannot be moved. Derived call
+ // classes must wait for callbacks to finish before calling MoveFrom.
+ PW_DCHECK(!other.CallbacksAreRunning());
+
// Copy all members from the other call.
endpoint_ = other.endpoint_;
channel_id_ = other.channel_id_;
@@ -70,29 +142,73 @@ void Call::MoveFrom(Call& other) {
service_id_ = other.service_id_;
method_id_ = other.method_id_;
- rpc_state_ = other.rpc_state_;
- type_ = other.type_;
- call_type_ = other.call_type_;
- client_stream_state_ = other.client_stream_state_;
+ state_ = other.state_;
+
+ // No need to move awaiting_cleanup_, since it is 0 in both calls here.
+
+ properties_ = other.properties_;
+
+ // callbacks_executing_ is not moved since it is associated with the object in
+ // memory, not the call.
on_error_ = std::move(other.on_error_);
on_next_ = std::move(other.on_next_);
// Mark the other call inactive, unregister it, and register this one.
- other.rpc_state_ = kInactive;
- other.client_stream_state_ = kClientStreamInactive;
+ other.MarkClosed();
endpoint().UnregisterCall(other);
endpoint().RegisterUniqueCall(*this);
}
+void Call::WaitUntilReadyForMove(Call& destination, Call& source) {
+ do {
+ // Wait for the source's callbacks to finish if it is active.
+ int iterations = 0;
+ while (source.active_locked() && source.CallbacksAreRunning()) {
+ PW_RPC_CHECK_FOR_DEADLOCK("move", source);
+ YieldRpcLock();
+ }
+
+ // At this point, no callbacks are running in the source call. If cleanup
+ // is required for the destination call, perform it and retry since
+ // cleanup releases and reacquires the RPC lock.
+ } while (source.CleanUpIfRequired() || destination.CleanUpIfRequired());
+}
+
+void Call::CallOnError(Status error) {
+ auto on_error_local = std::move(on_error_);
+
+ CallbackStarted();
+
+ rpc_lock().unlock();
+ if (on_error_local) {
+ on_error_local(error);
+ }
+
+ // This mutex lock could be avoided by making callbacks_executing_ atomic.
+ RpcLockGuard lock;
+ CallbackFinished();
+}
+
+bool Call::CleanUpIfRequired() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ if (!awaiting_cleanup()) {
+ return false;
+ }
+ endpoint_->CleanUpCall(*this);
+ rpc_lock().lock();
+ return true;
+}
+
Status Call::SendPacket(PacketType type, ConstByteSpan payload, Status status) {
if (!active_locked()) {
+ encoding_buffer.ReleaseIfAllocated();
return Status::FailedPrecondition();
}
Channel* channel = endpoint_->GetInternalChannel(channel_id_);
if (channel == nullptr) {
+ encoding_buffer.ReleaseIfAllocated();
return Status::Unavailable();
}
return channel->Send(MakePacket(type, payload, status));
@@ -107,11 +223,67 @@ Status Call::CloseAndSendFinalPacketLocked(PacketType type,
}
Status Call::WriteLocked(ConstByteSpan payload) {
- return SendPacket(call_type_ == kServerCall ? PacketType::SERVER_STREAM
- : PacketType::CLIENT_STREAM,
+ return SendPacket(properties_.call_type() == kServerCall
+ ? PacketType::SERVER_STREAM
+ : PacketType::CLIENT_STREAM,
payload);
}
+// This definition is in the .cc file because the Endpoint class is not defined
+// in the Call header, due to circular dependencies between the two.
+void Call::CloseAndMarkForCleanup(Status error) {
+ endpoint_->CloseCallAndMarkForCleanup(*this, error);
+}
+
+void Call::HandlePayload(ConstByteSpan payload) {
+ // pw_rpc only supports handling packets for a particular RPC one at a time.
+ // Check if any callbacks are running and drop the packet if they are.
+ //
+ // The on_next callback cannot support multiple packets at once since it is
+ // moved before it is invoked. on_error and on_completed are only called
+ // after the call is closed.
+ if (CallbacksAreRunning()) {
+ PW_LOG_WARN(
+ "Received stream packet for %u:%08x/%08x before the callback for a "
+ "previous packet completed! This packet will be dropped. This can be "
+ "avoided by handling packets for a particular RPC on only one thread.",
+ static_cast<unsigned>(channel_id_),
+ static_cast<unsigned>(service_id_),
+ static_cast<unsigned>(method_id_));
+ rpc_lock().unlock();
+ return;
+ }
+
+ if (on_next_ == nullptr) {
+ rpc_lock().unlock();
+ return;
+ }
+
+ const uint32_t original_id = id();
+ auto on_next_local = std::move(on_next_);
+ CallbackStarted();
+
+ if (hold_lock_while_invoking_callback_with_payload()) {
+ on_next_local(payload);
+ } else {
+ rpc_lock().unlock();
+ on_next_local(payload);
+ rpc_lock().lock();
+ }
+
+ CallbackFinished();
+
+ // Restore the original callback if the original call is still active and
+ // the callback has not been replaced.
+ // NOLINTNEXTLINE(bugprone-use-after-move)
+ if (active_locked() && id() == original_id && on_next_ == nullptr) {
+ on_next_ = std::move(on_next_local);
+ }
+
+ // Clean up calls in case decoding failed.
+ endpoint_->CleanUpCalls();
+}
+
void Call::UnregisterAndMarkClosed() {
if (active_locked()) {
endpoint().UnregisterCall(*this);
diff --git a/pw_rpc/call_test.cc b/pw_rpc/call_test.cc
index 208342dc5..b2cb44bfb 100644
--- a/pw_rpc/call_test.cc
+++ b/pw_rpc/call_test.cc
@@ -18,6 +18,7 @@
#include <array>
#include <cstdint>
#include <cstring>
+#include <optional>
#include "gtest/gtest.h"
#include "pw_rpc/internal/test_method.h"
@@ -37,211 +38,251 @@ class TestService : public Service {
namespace internal {
namespace {
-constexpr Packet kPacket(PacketType::REQUEST, 99, 16, 8);
-
-using pw::rpc::internal::test::FakeServerWriter;
-using std::byte;
-
-TEST(ServerWriter, ConstructWithContext_StartsOpen) {
- ServerContextForTest<TestService> context(TestService::method.method());
+constexpr Packet kPacket(pwpb::PacketType::REQUEST, 99, 16, 8);
+
+using ::pw::rpc::internal::test::FakeServerReader;
+using ::pw::rpc::internal::test::FakeServerReaderWriter;
+using ::pw::rpc::internal::test::FakeServerWriter;
+using ::std::byte;
+using ::testing::Test;
+
+static_assert(sizeof(Call) ==
+ // IntrusiveList::Item pointer
+ sizeof(IntrusiveList<Call>::Item) +
+ // Endpoint pointer
+ sizeof(Endpoint*) +
+ // call_id, channel_id, service_id, method_id
+ 4 * sizeof(uint32_t) +
+ // Packed state and properties
+ sizeof(void*) +
+ // on_error and on_next callbacks
+ 2 * sizeof(Function<void(Status)>),
+ "Unexpected padding in Call!");
+
+static_assert(sizeof(CallProperties) == sizeof(uint8_t));
+
+TEST(CallProperties, ValuesMatch) {
+ constexpr CallProperties props_1(
+ MethodType::kBidirectionalStreaming, kClientCall, kRawProto);
+ static_assert(props_1.method_type() == MethodType::kBidirectionalStreaming);
+ static_assert(props_1.call_type() == kClientCall);
+ static_assert(props_1.callback_proto_type() == kRawProto);
+
+ constexpr CallProperties props_2(
+ MethodType::kClientStreaming, kServerCall, kProtoStruct);
+ static_assert(props_2.method_type() == MethodType::kClientStreaming);
+ static_assert(props_2.call_type() == kServerCall);
+ static_assert(props_2.callback_proto_type() == kProtoStruct);
+
+ constexpr CallProperties props_3(
+ MethodType::kUnary, kClientCall, kProtoStruct);
+ static_assert(props_3.method_type() == MethodType::kUnary);
+ static_assert(props_3.call_type() == kClientCall);
+ static_assert(props_3.callback_proto_type() == kProtoStruct);
+}
- FakeServerWriter writer(context.get());
+class ServerWriterTest : public Test {
+ public:
+ ServerWriterTest() : context_(TestService::method.method()) {
+ rpc_lock().lock();
+ FakeServerWriter writer_temp(context_.get().ClaimLocked());
+ rpc_lock().unlock();
+ writer_ = std::move(writer_temp);
+ }
+
+ ServerContextForTest<TestService> context_;
+ FakeServerWriter writer_;
+};
- EXPECT_TRUE(writer.active());
+TEST_F(ServerWriterTest, ConstructWithContext_StartsOpen) {
+ EXPECT_TRUE(writer_.active());
}
-TEST(ServerWriter, Move_ClosesOriginal) {
- ServerContextForTest<TestService> context(TestService::method.method());
-
- FakeServerWriter moved(context.get());
- FakeServerWriter writer(std::move(moved));
+TEST_F(ServerWriterTest, Move_ClosesOriginal) {
+ FakeServerWriter moved(std::move(writer_));
#ifndef __clang_analyzer__
- EXPECT_FALSE(moved.active());
+ EXPECT_FALSE(writer_.active());
#endif // ignore use-after-move
- EXPECT_TRUE(writer.active());
+ EXPECT_TRUE(moved.active());
}
-TEST(ServerWriter, DefaultConstruct_Closed) {
+TEST_F(ServerWriterTest, DefaultConstruct_Closed) {
FakeServerWriter writer;
-
EXPECT_FALSE(writer.active());
}
-TEST(ServerWriter, Construct_RegistersWithServer) PW_NO_LOCK_SAFETY_ANALYSIS {
- ServerContextForTest<TestService> context(TestService::method.method());
- FakeServerWriter writer(context.get());
-
- Call* call = context.server().FindCall(kPacket);
- ASSERT_NE(call, nullptr);
- EXPECT_EQ(static_cast<void*>(call), static_cast<void*>(&writer));
+TEST_F(ServerWriterTest, Construct_RegistersWithServer) {
+ RpcLockGuard lock;
+ IntrusiveList<Call>::iterator call = context_.server().FindCall(kPacket);
+ ASSERT_NE(call, context_.server().calls_end());
+ EXPECT_EQ(static_cast<void*>(&*call), static_cast<void*>(&writer_));
}
-TEST(ServerWriter, Destruct_RemovesFromServer) PW_NO_LOCK_SAFETY_ANALYSIS {
- ServerContextForTest<TestService> context(TestService::method.method());
- { FakeServerWriter writer(context.get()); }
-
- EXPECT_EQ(context.server().FindCall(kPacket), nullptr);
+TEST_F(ServerWriterTest, Destruct_RemovesFromServer) {
+ {
+ // Note `lock_guard` cannot be used here, because while the constructor
+ // of `FakeServerWriter` requires the lock be held, the destructor acquires
+ // it!
+ rpc_lock().lock();
+ FakeServerWriter writer(context_.get().ClaimLocked());
+ rpc_lock().unlock();
+ }
+
+ RpcLockGuard lock;
+ EXPECT_EQ(context_.server().FindCall(kPacket), context_.server().calls_end());
}
-TEST(ServerWriter, Finish_RemovesFromServer) PW_NO_LOCK_SAFETY_ANALYSIS {
- ServerContextForTest<TestService> context(TestService::method.method());
- FakeServerWriter writer(context.get());
-
- EXPECT_EQ(OkStatus(), writer.Finish());
-
- EXPECT_EQ(context.server().FindCall(kPacket), nullptr);
+TEST_F(ServerWriterTest, Finish_RemovesFromServer) {
+ EXPECT_EQ(OkStatus(), writer_.Finish());
+ RpcLockGuard lock;
+ EXPECT_EQ(context_.server().FindCall(kPacket), context_.server().calls_end());
}
-TEST(ServerWriter, Finish_SendsResponse) {
- ServerContextForTest<TestService> context(TestService::method.method());
- FakeServerWriter writer(context.get());
-
- EXPECT_EQ(OkStatus(), writer.Finish());
+TEST_F(ServerWriterTest, Finish_SendsResponse) {
+ EXPECT_EQ(OkStatus(), writer_.Finish());
- ASSERT_EQ(context.output().total_packets(), 1u);
- const Packet& packet = context.output().last_packet();
- EXPECT_EQ(packet.type(), PacketType::RESPONSE);
- EXPECT_EQ(packet.channel_id(), context.channel_id());
- EXPECT_EQ(packet.service_id(), context.service_id());
- EXPECT_EQ(packet.method_id(), context.get().method().id());
+ ASSERT_EQ(context_.output().total_packets(), 1u);
+ const Packet& packet = context_.output().last_packet();
+ EXPECT_EQ(packet.type(), pwpb::PacketType::RESPONSE);
+ EXPECT_EQ(packet.channel_id(), context_.channel_id());
+ EXPECT_EQ(packet.service_id(), context_.service_id());
+ EXPECT_EQ(packet.method_id(), context_.get().method().id());
EXPECT_TRUE(packet.payload().empty());
EXPECT_EQ(packet.status(), OkStatus());
}
-TEST(ServerWriter, Finish_ReturnsStatusFromChannelSend) {
- ServerContextForTest<TestService> context(TestService::method.method());
- FakeServerWriter writer(context.get());
- context.output().set_send_status(Status::Unauthenticated());
+TEST_F(ServerWriterTest, Finish_ReturnsStatusFromChannelSend) {
+ context_.output().set_send_status(Status::Unauthenticated());
// All non-OK statuses are remapped to UNKNOWN.
- EXPECT_EQ(Status::Unknown(), writer.Finish());
+ EXPECT_EQ(Status::Unknown(), writer_.Finish());
}
-TEST(ServerWriter, Finish) {
- ServerContextForTest<TestService> context(TestService::method.method());
- FakeServerWriter writer(context.get());
-
- ASSERT_TRUE(writer.active());
- EXPECT_EQ(OkStatus(), writer.Finish());
- EXPECT_FALSE(writer.active());
- EXPECT_EQ(Status::FailedPrecondition(), writer.Finish());
+TEST_F(ServerWriterTest, Finish) {
+ ASSERT_TRUE(writer_.active());
+ EXPECT_EQ(OkStatus(), writer_.Finish());
+ EXPECT_FALSE(writer_.active());
+ EXPECT_EQ(Status::FailedPrecondition(), writer_.Finish());
}
-TEST(ServerWriter, Open_SendsPacketWithPayload) {
- ServerContextForTest<TestService> context(TestService::method.method());
- FakeServerWriter writer(context.get());
-
+TEST_F(ServerWriterTest, Open_SendsPacketWithPayload) {
constexpr byte data[] = {byte{0xf0}, byte{0x0d}};
- ASSERT_EQ(OkStatus(), writer.Write(data));
+ ASSERT_EQ(OkStatus(), writer_.Write(data));
byte encoded[64];
- auto result = context.server_stream(data).Encode(encoded);
+ auto result = context_.server_stream(data).Encode(encoded);
ASSERT_EQ(OkStatus(), result.status());
- ConstByteSpan payload = context.output().last_packet().payload();
+ ConstByteSpan payload = context_.output().last_packet().payload();
EXPECT_EQ(sizeof(data), payload.size());
EXPECT_EQ(0, std::memcmp(data, payload.data(), sizeof(data)));
}
-TEST(ServerWriter, Closed_IgnoresFinish) {
- ServerContextForTest<TestService> context(TestService::method.method());
- FakeServerWriter writer(context.get());
-
- EXPECT_EQ(OkStatus(), writer.Finish());
- EXPECT_EQ(Status::FailedPrecondition(), writer.Finish());
+TEST_F(ServerWriterTest, Closed_IgnoresFinish) {
+ EXPECT_EQ(OkStatus(), writer_.Finish());
+ EXPECT_EQ(Status::FailedPrecondition(), writer_.Finish());
}
-TEST(ServerWriter, DefaultConstructor_NoClientStream) {
+TEST_F(ServerWriterTest, DefaultConstructor_NoClientStream) {
FakeServerWriter writer;
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
EXPECT_FALSE(writer.as_server_call().has_client_stream());
EXPECT_FALSE(writer.as_server_call().client_stream_open());
}
-TEST(ServerWriter, Open_NoClientStream) {
- ServerContextForTest<TestService> context(TestService::method.method());
- FakeServerWriter writer(context.get());
-
- LockGuard lock(rpc_lock());
- EXPECT_FALSE(writer.as_server_call().has_client_stream());
- EXPECT_FALSE(writer.as_server_call().client_stream_open());
+TEST_F(ServerWriterTest, Open_NoClientStream) {
+ RpcLockGuard lock;
+ EXPECT_FALSE(writer_.as_server_call().has_client_stream());
+ EXPECT_FALSE(writer_.as_server_call().client_stream_open());
}
-TEST(ServerReader, DefaultConstructor_ClientStreamClosed) {
- test::FakeServerReader reader;
+class ServerReaderTest : public Test {
+ public:
+ ServerReaderTest() : context_(TestService::method.method()) {
+ rpc_lock().lock();
+ FakeServerReader reader_temp(context_.get().ClaimLocked());
+ rpc_lock().unlock();
+ reader_ = std::move(reader_temp);
+ }
+
+ ServerContextForTest<TestService> context_;
+ FakeServerReader reader_;
+};
+
+TEST_F(ServerReaderTest, DefaultConstructor_ClientStreamClosed) {
+ FakeServerReader reader;
EXPECT_FALSE(reader.as_server_call().active());
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
EXPECT_FALSE(reader.as_server_call().client_stream_open());
}
-TEST(ServerReader, Open_ClientStreamStartsOpen) {
- ServerContextForTest<TestService> context(TestService::method.method());
- test::FakeServerReader reader(context.get());
-
- LockGuard lock(rpc_lock());
- EXPECT_TRUE(reader.as_server_call().has_client_stream());
- EXPECT_TRUE(reader.as_server_call().client_stream_open());
+TEST_F(ServerReaderTest, Open_ClientStreamStartsOpen) {
+ RpcLockGuard lock;
+ EXPECT_TRUE(reader_.as_server_call().has_client_stream());
+ EXPECT_TRUE(reader_.as_server_call().client_stream_open());
}
-TEST(ServerReader, Close_ClosesClientStream) {
- ServerContextForTest<TestService> context(TestService::method.method());
- test::FakeServerReader reader(context.get());
-
- EXPECT_TRUE(reader.as_server_call().active());
+TEST_F(ServerReaderTest, Close_ClosesClientStream) {
+ EXPECT_TRUE(reader_.as_server_call().active());
rpc_lock().lock();
- EXPECT_TRUE(reader.as_server_call().client_stream_open());
+ EXPECT_TRUE(reader_.as_server_call().client_stream_open());
rpc_lock().unlock();
EXPECT_EQ(OkStatus(),
- reader.as_server_call().CloseAndSendResponse(OkStatus()));
+ reader_.as_server_call().CloseAndSendResponse(OkStatus()));
- EXPECT_FALSE(reader.as_server_call().active());
- LockGuard lock(rpc_lock());
- EXPECT_FALSE(reader.as_server_call().client_stream_open());
+ EXPECT_FALSE(reader_.as_server_call().active());
+ RpcLockGuard lock;
+ EXPECT_FALSE(reader_.as_server_call().client_stream_open());
}
-TEST(ServerReader, EndClientStream_OnlyClosesClientStream) {
- ServerContextForTest<TestService> context(TestService::method.method());
- test::FakeServerReader reader(context.get());
-
- EXPECT_TRUE(reader.active());
+TEST_F(ServerReaderTest, EndClientStream_OnlyClosesClientStream) {
+ EXPECT_TRUE(reader_.active());
rpc_lock().lock();
- EXPECT_TRUE(reader.as_server_call().client_stream_open());
- reader.as_server_call().HandleClientStreamEnd();
+ EXPECT_TRUE(reader_.as_server_call().client_stream_open());
+ reader_.as_server_call().HandleClientStreamEnd();
- EXPECT_TRUE(reader.active());
- LockGuard lock(rpc_lock());
- EXPECT_FALSE(reader.as_server_call().client_stream_open());
+ EXPECT_TRUE(reader_.active());
+ RpcLockGuard lock;
+ EXPECT_FALSE(reader_.as_server_call().client_stream_open());
}
-TEST(ServerReaderWriter, Move_MaintainsClientStream) {
- ServerContextForTest<TestService> context(TestService::method.method());
- test::FakeServerReaderWriter reader_writer(context.get());
- test::FakeServerReaderWriter destination;
+class ServerReaderWriterTest : public Test {
+ public:
+ ServerReaderWriterTest() : context_(TestService::method.method()) {
+ rpc_lock().lock();
+ FakeServerReaderWriter reader_writer_temp(context_.get().ClaimLocked());
+ rpc_lock().unlock();
+ reader_writer_ = std::move(reader_writer_temp);
+ }
+
+ ServerContextForTest<TestService> context_;
+ FakeServerReaderWriter reader_writer_;
+};
+
+TEST_F(ServerReaderWriterTest, Move_MaintainsClientStream) {
+ FakeServerReaderWriter destination;
rpc_lock().lock();
EXPECT_FALSE(destination.as_server_call().client_stream_open());
rpc_lock().unlock();
- destination = std::move(reader_writer);
- LockGuard lock(rpc_lock());
+ destination = std::move(reader_writer_);
+ RpcLockGuard lock;
EXPECT_TRUE(destination.as_server_call().has_client_stream());
EXPECT_TRUE(destination.as_server_call().client_stream_open());
}
-TEST(ServerReaderWriter, Move_MovesCallbacks) {
- ServerContextForTest<TestService> context(TestService::method.method());
- test::FakeServerReaderWriter reader_writer(context.get());
-
+TEST_F(ServerReaderWriterTest, Move_MovesCallbacks) {
int calls = 0;
- reader_writer.set_on_error([&calls](Status) { calls += 1; });
- reader_writer.set_on_next([&calls](ConstByteSpan) { calls += 1; });
+ reader_writer_.set_on_error([&calls](Status) { calls += 1; });
+ reader_writer_.set_on_next([&calls](ConstByteSpan) { calls += 1; });
#if PW_RPC_CLIENT_STREAM_END_CALLBACK
- reader_writer.set_on_client_stream_end([&calls]() { calls += 1; });
+ reader_writer_.set_on_client_stream_end([&calls]() { calls += 1; });
#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
- test::FakeServerReaderWriter destination(std::move(reader_writer));
+ FakeServerReaderWriter destination(std::move(reader_writer_));
rpc_lock().lock();
destination.as_server_call().HandlePayload({});
rpc_lock().lock();
@@ -252,6 +293,81 @@ TEST(ServerReaderWriter, Move_MovesCallbacks) {
EXPECT_EQ(calls, 2 + PW_RPC_CLIENT_STREAM_END_CALLBACK);
}
+TEST_F(ServerReaderWriterTest, Move_ClearsCallAndChannelId) {
+ rpc_lock().lock();
+ reader_writer_.set_id(999);
+ EXPECT_NE(reader_writer_.channel_id_locked(), 0u);
+ rpc_lock().unlock();
+
+ FakeServerReaderWriter destination(std::move(reader_writer_));
+
+ RpcLockGuard lock;
+ EXPECT_EQ(reader_writer_.id(), 0u);
+ EXPECT_EQ(reader_writer_.channel_id_locked(), 0u);
+}
+
+TEST_F(ServerReaderWriterTest, Move_SourceAwaitingCleanup_CleansUpCalls) {
+ std::optional<Status> on_error_cb;
+ reader_writer_.set_on_error([&on_error_cb](Status error) {
+ ASSERT_FALSE(on_error_cb.has_value());
+ on_error_cb = error;
+ });
+
+ rpc_lock().lock();
+ context_.server().CloseCallAndMarkForCleanup(reader_writer_.as_server_call(),
+ Status::NotFound());
+ rpc_lock().unlock();
+
+ FakeServerReaderWriter destination(std::move(reader_writer_));
+
+ EXPECT_EQ(Status::NotFound(), on_error_cb);
+}
+
+TEST_F(ServerReaderWriterTest, Move_BothAwaitingCleanup_CleansUpCalls) {
+ rpc_lock().lock();
+ // Use call ID 123 so this call is distinct from the other.
+ FakeServerReaderWriter destination(context_.get(123).ClaimLocked());
+ rpc_lock().unlock();
+
+ std::optional<Status> destination_on_error_cb;
+ destination.set_on_error([&destination_on_error_cb](Status error) {
+ ASSERT_FALSE(destination_on_error_cb.has_value());
+ destination_on_error_cb = error;
+ });
+
+ std::optional<Status> source_on_error_cb;
+ reader_writer_.set_on_error([&source_on_error_cb](Status error) {
+ ASSERT_FALSE(source_on_error_cb.has_value());
+ source_on_error_cb = error;
+ });
+
+ // Simulate these two calls being closed by another thread.
+ rpc_lock().lock();
+ context_.server().CloseCallAndMarkForCleanup(destination.as_server_call(),
+ Status::NotFound());
+ context_.server().CloseCallAndMarkForCleanup(reader_writer_.as_server_call(),
+ Status::Unauthenticated());
+ rpc_lock().unlock();
+
+ destination = std::move(reader_writer_);
+
+ EXPECT_EQ(Status::NotFound(), destination_on_error_cb);
+ EXPECT_EQ(Status::Unauthenticated(), source_on_error_cb);
+}
+
+TEST_F(ServerReaderWriterTest, Close_ClearsCallAndChannelId) {
+ rpc_lock().lock();
+ reader_writer_.set_id(999);
+ EXPECT_NE(reader_writer_.channel_id_locked(), 0u);
+ rpc_lock().unlock();
+
+ EXPECT_EQ(OkStatus(), reader_writer_.Finish());
+
+ RpcLockGuard lock;
+ EXPECT_EQ(reader_writer_.id(), 0u);
+ EXPECT_EQ(reader_writer_.channel_id_locked(), 0u);
+}
+
} // namespace
} // namespace internal
} // namespace pw::rpc
diff --git a/pw_rpc/callback_test.cc b/pw_rpc/callback_test.cc
new file mode 100644
index 000000000..76f15a2cc
--- /dev/null
+++ b/pw_rpc/callback_test.cc
@@ -0,0 +1,248 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_rpc/raw/client_testing.h"
+#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/sleep.h"
+#include "pw_thread/test_threads.h"
+#include "pw_thread/thread.h"
+#include "pw_thread/yield.h"
+
+namespace pw::rpc {
+namespace {
+
+using namespace std::chrono_literals;
+
+using test::pw_rpc::raw::TestService;
+
+void YieldToOtherThread() {
+ // Sleep for a while and then yield just to be sure the other thread runs.
+ this_thread::sleep_for(100ms);
+ this_thread::yield();
+}
+
+class CallbacksTest : public ::testing::Test {
+ protected:
+ CallbacksTest()
+ : callback_thread_(
+ thread::test::TestOptionsThread0(),
+ [](void* arg) {
+ static_cast<CallbacksTest*>(arg)->SendResponseAfterSemaphore();
+ },
+ this) {}
+
+ ~CallbacksTest() override {
+ EXPECT_FALSE(callback_thread_.joinable()); // Tests must join the thread!
+ }
+
+ void RespondToCall(const RawClientReaderWriter& call) {
+ respond_to_call_ = &call;
+ }
+
+ RawClientTestContext<> context_;
+ sync::BinarySemaphore callback_thread_sem_;
+ sync::BinarySemaphore main_thread_sem_;
+
+ thread::Thread callback_thread_;
+
+ // Must be set to true by the RPC callback in each test.
+ volatile int callback_executed_ = 0;
+
+ // Variables optionally used by tests. These are in this object so lambads
+ // only need to capture [this] to access them.
+ volatile bool call_is_in_scope_ = false;
+
+ RawClientReaderWriter call_1_;
+ RawClientReaderWriter call_2_;
+
+ private:
+ void SendResponseAfterSemaphore() {
+ // Wait until the main thread says to send the response.
+ callback_thread_sem_.acquire();
+
+ context_.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ {}, respond_to_call_->id());
+ }
+
+ const RawClientReaderWriter* respond_to_call_ = &call_1_;
+};
+
+TEST_F(CallbacksTest, DestructorWaitsUntilCallbacksComplete) {
+ // Skip this test if locks are disabled because the thread can't yield.
+ if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
+ callback_thread_sem_.release();
+ callback_thread_.join();
+ GTEST_SKIP();
+ }
+
+ {
+ RawClientReaderWriter local_call = TestService::TestBidirectionalStreamRpc(
+ context_.client(), context_.channel().id());
+ RespondToCall(local_call);
+
+ call_is_in_scope_ = true;
+
+ local_call.set_on_next([this](ConstByteSpan) {
+ main_thread_sem_.release();
+
+ // Wait for a while so the main thread tries to destroy the call.
+ YieldToOtherThread();
+
+ // Now, make sure the call is still in scope. The main thread should
+ // block in the call's destructor until this callback completes.
+ EXPECT_TRUE(call_is_in_scope_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread so it can invoke the callback.
+ callback_thread_sem_.release();
+
+ // Wait until the callback thread starts.
+ main_thread_sem_.acquire();
+ }
+
+ // The callback thread will sleep for a bit. Meanwhile, let the call go out
+ // of scope, and mark it as such.
+ call_is_in_scope_ = false;
+
+ // Wait for the callback thread to finish.
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+}
+
+TEST_F(CallbacksTest, MoveActiveCall_WaitsForCallbackToComplete) {
+ // Skip this test if locks are disabled because the thread can't yield.
+ if (PW_RPC_USE_GLOBAL_MUTEX == 0) {
+ callback_thread_sem_.release();
+ callback_thread_.join();
+ GTEST_SKIP();
+ }
+
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(), context_.channel().id(), [this](ConstByteSpan) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ YieldToOtherThread();
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread so it can invoke the callback.
+ callback_thread_sem_.release();
+
+ // Confirm that the callback thread started.
+ main_thread_sem_.acquire();
+
+ // Move the call object. This thread should wait until the on_completed
+ // callback is done.
+ EXPECT_TRUE(call_1_.active());
+ call_2_ = std::move(call_1_);
+
+ // The callback should already have finished. This thread should have waited
+ // for it to finish during the move.
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_FALSE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ callback_thread_.join();
+}
+
+TEST_F(CallbacksTest, MoveOtherCallIntoOwnCallInCallback) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(), context_.channel().id(), [this](ConstByteSpan) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ call_1_ = std::move(call_2_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ call_2_ = TestService::TestBidirectionalStreamRpc(context_.client(),
+ context_.channel().id());
+
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ // Start the callback thread and wait for it to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_FALSE(call_2_.active());
+}
+
+TEST_F(CallbacksTest, MoveOwnCallInCallback) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(), context_.channel().id(), [this](ConstByteSpan) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ // Cancel this call first, or the move will deadlock, since the moving
+ // thread will wait for the callback thread (both this thread) to
+ // terminate if the call is active.
+ EXPECT_EQ(OkStatus(), call_1_.Cancel());
+ call_2_ = std::move(call_1_);
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ call_2_ = TestService::TestBidirectionalStreamRpc(context_.client(),
+ context_.channel().id());
+
+ EXPECT_TRUE(call_1_.active());
+ EXPECT_TRUE(call_2_.active());
+
+ // Start the callback thread and wait for it to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+ EXPECT_FALSE(call_1_.active());
+ EXPECT_FALSE(call_2_.active());
+}
+
+TEST_F(CallbacksTest, PacketDroppedIfOnNextIsBusy) {
+ call_1_ = TestService::TestBidirectionalStreamRpc(
+ context_.client(), context_.channel().id(), [this](ConstByteSpan) {
+ main_thread_sem_.release(); // Confirm that this thread started
+
+ callback_thread_sem_.acquire(); // Wait for the main thread to release
+
+ callback_executed_ = callback_executed_ + 1;
+ });
+
+ // Start the callback thread.
+ callback_thread_sem_.release();
+
+ main_thread_sem_.acquire(); // Confirm that the callback is running
+
+ // Handle a few packets for this call, which should be dropped.
+ for (int i = 0; i < 5; ++i) {
+ context_.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ {}, call_1_.id());
+ }
+
+ // Wait for the callback thread to finish.
+ callback_thread_sem_.release();
+ callback_thread_.join();
+
+ EXPECT_EQ(callback_executed_, 1);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/channel.cc b/pw_rpc/channel.cc
index ea74168cc..dfc802a6e 100644
--- a/pw_rpc/channel.cc
+++ b/pw_rpc/channel.cc
@@ -22,31 +22,21 @@
#include "pw_log/log.h"
#include "pw_protobuf/decoder.h"
#include "pw_rpc/internal/config.h"
+#include "pw_rpc/internal/encoding_buffer.h"
namespace pw::rpc {
-namespace {
-
-// TODO(pwbug/615): Dynamically allocate this buffer if
-// PW_RPC_DYNAMIC_ALLOCATION is enabled.
-std::array<std::byte, cfg::kEncodingBufferSizeBytes> encoding_buffer
- PW_GUARDED_BY(internal::rpc_lock());
-
-} // namespace
Result<uint32_t> ExtractChannelId(ConstByteSpan packet) {
protobuf::Decoder decoder(packet);
while (decoder.Next().ok()) {
- switch (static_cast<internal::RpcPacket::Fields>(decoder.FieldNumber())) {
- case internal::RpcPacket::Fields::CHANNEL_ID: {
- uint32_t channel_id;
- PW_TRY(decoder.ReadUint32(&channel_id));
- return channel_id;
- }
-
- default:
- continue;
+ if (static_cast<internal::pwpb::RpcPacket::Fields>(decoder.FieldNumber()) !=
+ internal::pwpb::RpcPacket::Fields::kChannelId) {
+ continue;
}
+ uint32_t channel_id;
+ PW_TRY(decoder.ReadUint32(&channel_id));
+ return channel_id;
}
return Status::DataLoss();
@@ -54,15 +44,12 @@ Result<uint32_t> ExtractChannelId(ConstByteSpan packet) {
namespace internal {
-ByteSpan GetPayloadBuffer() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return ByteSpan(encoding_buffer)
- .subspan(Packet::kMinEncodedSizeWithoutPayload);
-}
-
Status Channel::Send(const Packet& packet) {
- Result encoded = packet.Encode(encoding_buffer);
+ ByteSpan buffer = encoding_buffer.GetPacketBuffer(packet.payload().size());
+ Result encoded = packet.Encode(buffer);
if (!encoded.ok()) {
+ encoding_buffer.Release();
PW_LOG_ERROR(
"Failed to encode RPC packet type %u to channel %u buffer, status %u",
static_cast<unsigned>(packet.type()),
@@ -72,6 +59,7 @@ Status Channel::Send(const Packet& packet) {
}
Status sent = output().Send(encoded.value());
+ encoding_buffer.Release();
if (!sent.ok()) {
PW_LOG_DEBUG("Channel %u failed to send packet with status %u",
diff --git a/pw_rpc/channel_test.cc b/pw_rpc/channel_test.cc
index b61fd8d75..e96a7bd9b 100644
--- a/pw_rpc/channel_test.cc
+++ b/pw_rpc/channel_test.cc
@@ -27,7 +27,7 @@ TEST(ChannelOutput, Name) {
class NameTester : public ChannelOutput {
public:
NameTester(const char* name) : ChannelOutput(name) {}
- Status Send(std::span<const std::byte>) override { return OkStatus(); }
+ Status Send(span<const std::byte>) override { return OkStatus(); }
};
EXPECT_STREQ("hello_world", NameTester("hello_world").name());
@@ -35,7 +35,7 @@ TEST(ChannelOutput, Name) {
}
constexpr Packet kTestPacket(
- PacketType::RESPONSE, 23, 42, 100, 0, {}, Status::NotFound());
+ pwpb::PacketType::RESPONSE, 23, 42, 100, 0, {}, Status::NotFound());
const size_t kReservedSize = 2 /* type */ + 2 /* channel */ + 5 /* service */ +
5 /* method */ + 2 /* payload key */ +
2 /* status (if not OK) */;
@@ -45,6 +45,69 @@ enum class ChannelId {
kTwo = 2,
};
+TEST(Channel, MaxSafePayload) {
+ constexpr size_t kUint32Max = std::numeric_limits<uint32_t>::max();
+ constexpr size_t kMaxPayloadSize = 64;
+
+ constexpr size_t kTestPayloadSize = MaxSafePayloadSize(kMaxPayloadSize);
+
+ // Because it's impractical to test a payload that nears the limits of a
+ // uint32 varint, calculate the difference when using a smaller payload.
+ constexpr size_t kPayloadSizeTestLimitations =
+ varint::EncodedSize(kUint32Max) - varint::EncodedSize(kTestPayloadSize);
+
+ // The buffer to use for encoding the RPC packet.
+ std::array<std::byte, kMaxPayloadSize - kPayloadSizeTestLimitations> buffer;
+
+ std::array<std::byte, kTestPayloadSize> payload;
+ for (size_t i = 0; i < payload.size(); i++) {
+ payload[i] = std::byte(i % std::numeric_limits<uint8_t>::max());
+ }
+
+ Packet packet(pwpb::PacketType::SERVER_STREAM,
+ /*channel_id=*/kUint32Max, // Varint, needs to be uint32_t max.
+ /*service_id=*/42, // Fixed-width. Value doesn't matter.
+ /*method_id=*/100, // Fixed-width. Value doesn't matter.
+ /*call_id=*/kUint32Max, // Varint, needs to be uint32_t max.
+ payload,
+ Status::Unauthenticated());
+
+ Result<ConstByteSpan> result = packet.Encode(buffer);
+ ASSERT_EQ(OkStatus(), result.status());
+}
+
+TEST(Channel, MaxSafePayload_OffByOne) {
+ constexpr size_t kUint32Max = std::numeric_limits<uint32_t>::max();
+ constexpr size_t kMaxPayloadSize = 64;
+
+ constexpr size_t kTestPayloadSize = MaxSafePayloadSize(kMaxPayloadSize);
+
+ // Because it's impractical to test a payload that nears the limits of a
+ // uint32 varint, calculate the difference when using a smaller payload.
+ constexpr size_t kPayloadSizeTestLimitations =
+ varint::EncodedSize(kUint32Max) - varint::EncodedSize(kTestPayloadSize);
+
+ // The buffer to use for encoding the RPC packet.
+ std::array<std::byte, kMaxPayloadSize - kPayloadSizeTestLimitations - 1>
+ buffer;
+
+ std::array<std::byte, kTestPayloadSize> payload;
+ for (size_t i = 0; i < payload.size(); i++) {
+ payload[i] = std::byte(i % std::numeric_limits<uint8_t>::max());
+ }
+
+ Packet packet(pwpb::PacketType::SERVER_STREAM,
+ /*channel_id=*/kUint32Max, // Varint, needs to be uint32_t max.
+ /*service_id=*/42, // Fixed-width. Value doesn't matter.
+ /*method_id=*/100, // Fixed-width. Value doesn't matter.
+ /*call_id=*/kUint32Max, // Varint, needs to be uint32_t max.
+ payload,
+ Status::Unauthenticated());
+
+ Result<ConstByteSpan> result = packet.Encode(buffer);
+ ASSERT_EQ(Status::ResourceExhausted(), result.status());
+}
+
TEST(Channel, Create_FromEnum) {
constexpr rpc::Channel one = Channel::Create<ChannelId::kOne>(nullptr);
constexpr rpc::Channel two = Channel::Create<ChannelId::kTwo>(nullptr);
diff --git a/pw_rpc/client.cc b/pw_rpc/client.cc
index 21a950d0a..006264220 100644
--- a/pw_rpc/client.cc
+++ b/pw_rpc/client.cc
@@ -27,7 +27,7 @@ namespace pw::rpc {
namespace {
using internal::Packet;
-using internal::PacketType;
+using internal::pwpb::PacketType;
} // namespace
@@ -36,8 +36,7 @@ Status Client::ProcessPacket(ConstByteSpan data) {
// Find an existing call for this RPC, if any.
internal::rpc_lock().lock();
- internal::ClientCall* call =
- static_cast<internal::ClientCall*>(FindCall(packet));
+ IntrusiveList<internal::Call>::iterator call = FindCall(packet);
internal::Channel* channel = GetInternalChannel(packet.channel_id());
@@ -47,7 +46,7 @@ Status Client::ProcessPacket(ConstByteSpan data) {
return Status::Unavailable();
}
- if (call == nullptr || call->id() != packet.call_id()) {
+ if (call == calls_end()) {
// The call for the packet does not exist. If the packet is a server stream
// message, notify the server so that it can kill the stream. Otherwise,
// silently drop the packet (as it would terminate the RPC anyway).
@@ -85,6 +84,11 @@ Status Client::ProcessPacket(ConstByteSpan data) {
PW_LOG_DEBUG("Received SERVER_STREAM for RPC without a server stream");
}
break;
+
+ case PacketType::REQUEST:
+ case PacketType::CLIENT_STREAM:
+ case PacketType::CLIENT_ERROR:
+ case PacketType::CLIENT_STREAM_END:
default:
internal::rpc_lock().unlock();
PW_LOG_WARN("pw_rpc client unable to handle packet of type %u",
diff --git a/pw_rpc/client_call.cc b/pw_rpc/client_call.cc
index d8e57e89a..70ca4344a 100644
--- a/pw_rpc/client_call.cc
+++ b/pw_rpc/client_call.cc
@@ -23,4 +23,49 @@ void ClientCall::CloseClientCall() {
UnregisterAndMarkClosed();
}
+void ClientCall::MoveClientCallFrom(ClientCall& other)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ WaitUntilReadyForMove(*this, other);
+ CloseClientCall();
+ MoveFrom(other);
+}
+
+void UnaryResponseClientCall::HandleCompleted(
+ ConstByteSpan response, Status status) PW_NO_LOCK_SAFETY_ANALYSIS {
+ UnregisterAndMarkClosed();
+
+ auto on_completed_local = std::move(on_completed_);
+ CallbackStarted();
+
+ // The lock is only released when calling into user code. If the callback is
+ // wrapped, this on_completed is an internal function that expects the lock to
+ // be held, and releases it before invoking user code.
+ if (!hold_lock_while_invoking_callback_with_payload()) {
+ rpc_lock().unlock();
+ }
+
+ if (on_completed_local) {
+ on_completed_local(response, status);
+ }
+
+ // This mutex lock could be avoided by making callbacks_executing_ atomic.
+ RpcLockGuard lock;
+ CallbackFinished();
+}
+
+void StreamResponseClientCall::HandleCompleted(Status status) {
+ UnregisterAndMarkClosed();
+ auto on_completed_local = std::move(on_completed_);
+ CallbackStarted();
+ rpc_lock().unlock();
+
+ if (on_completed_local) {
+ on_completed_local(status);
+ }
+
+ // This mutex lock could be avoided by making callbacks_executing_ atomic.
+ RpcLockGuard lock;
+ CallbackFinished();
+}
+
} // namespace pw::rpc::internal
diff --git a/pw_rpc/client_integration_test.cc b/pw_rpc/client_integration_test.cc
index 94ce8070c..53bff084a 100644
--- a/pw_rpc/client_integration_test.cc
+++ b/pw_rpc/client_integration_test.cc
@@ -12,6 +12,8 @@
// License for the specific language governing permissions and limitations under
// the License.
+#include <sys/socket.h>
+
#include <cstring>
#include "gtest/gtest.h"
@@ -26,6 +28,10 @@ namespace {
constexpr int kIterations = 3;
+// This client configures a socket read timeout to allow the RPC dispatch thread
+// to exit gracefully.
+constexpr timeval kSocketReadTimeout = {.tv_sec = 1, .tv_usec = 0};
+
using namespace std::chrono_literals;
using pw::ByteSpan;
using pw::ConstByteSpan;
@@ -69,7 +75,7 @@ TEST(RawRpcIntegrationTest, Unary) {
for (int i = 0; i < kIterations; ++i) {
StringReceiver receiver;
pw::rpc::RawUnaryReceiver call = kServiceClient.UnaryEcho(
- std::as_bytes(std::span("hello")), receiver.UnaryOnCompleted());
+ pw::as_bytes(pw::span("hello")), receiver.UnaryOnCompleted());
EXPECT_STREQ(receiver.Wait(), "hello");
}
}
@@ -80,10 +86,10 @@ TEST(RawRpcIntegrationTest, BidirectionalStreaming) {
pw::rpc::RawClientReaderWriter call =
kServiceClient.BidirectionalEcho(receiver.OnNext());
- ASSERT_EQ(OkStatus(), call.Write(std::as_bytes(std::span("Yello"))));
+ ASSERT_EQ(OkStatus(), call.Write(pw::as_bytes(pw::span("Yello"))));
EXPECT_STREQ(receiver.Wait(), "Yello");
- ASSERT_EQ(OkStatus(), call.Write(std::as_bytes(std::span("Dello"))));
+ ASSERT_EQ(OkStatus(), call.Write(pw::as_bytes(pw::span("Dello"))));
EXPECT_STREQ(receiver.Wait(), "Dello");
ASSERT_EQ(OkStatus(), call.Cancel());
@@ -97,5 +103,22 @@ int main(int argc, char* argv[]) {
if (!pw::rpc::integration_test::InitializeClient(argc, argv).ok()) {
return 1;
}
- return RUN_ALL_TESTS();
+
+ // Set read timout on socket to allow
+ // pw::rpc::integration_test::TerminateClient() to complete.
+ int retval = setsockopt(pw::rpc::integration_test::GetClientSocketFd(),
+ SOL_SOCKET,
+ SO_RCVTIMEO,
+ &rpc_test::kSocketReadTimeout,
+ sizeof(rpc_test::kSocketReadTimeout));
+ PW_CHECK_INT_EQ(retval,
+ 0,
+ "Failed to configure socket receive timeout with errno=%d",
+ errno);
+
+ int test_retval = RUN_ALL_TESTS();
+
+ pw::rpc::integration_test::TerminateClient();
+
+ return test_retval;
}
diff --git a/pw_rpc/client_server.cc b/pw_rpc/client_server.cc
index 1268b17af..e6d37b2ec 100644
--- a/pw_rpc/client_server.cc
+++ b/pw_rpc/client_server.cc
@@ -16,9 +16,8 @@
namespace pw::rpc {
-Status ClientServer::ProcessPacket(ConstByteSpan packet,
- ChannelOutput* interface) {
- Status status = server_.ProcessPacket(packet, interface);
+Status ClientServer::ProcessPacket(ConstByteSpan packet) {
+ Status status = server_.ProcessPacket(packet);
if (status.IsInvalidArgument()) {
// INVALID_ARGUMENT indicates the packet is intended for a client.
status = client_.ProcessPacket(packet);
diff --git a/pw_rpc/client_server_test.cc b/pw_rpc/client_server_test.cc
index bddcea849..bb2c72b0d 100644
--- a/pw_rpc/client_server_test.cc
+++ b/pw_rpc/client_server_test.cc
@@ -51,12 +51,12 @@ TEST(ClientServer, ProcessPacket_CallsServer) {
client_server.server().RegisterService(service);
Packet packet(
- PacketType::REQUEST, kFakeChannelId, kFakeServiceId, kFakeMethodId);
+ pwpb::PacketType::REQUEST, kFakeChannelId, kFakeServiceId, kFakeMethodId);
std::array<std::byte, 32> buffer;
Result result = packet.Encode(buffer);
EXPECT_EQ(result.status(), OkStatus());
- EXPECT_EQ(client_server.ProcessPacket(result.value(), output), OkStatus());
+ EXPECT_EQ(client_server.ProcessPacket(result.value()), OkStatus());
}
TEST(ClientServer, ProcessPacket_CallsClient) {
@@ -65,20 +65,22 @@ TEST(ClientServer, ProcessPacket_CallsClient) {
// Same packet as above, but type RESPONSE will skip the server and call into
// the client.
- Packet packet(
- PacketType::RESPONSE, kFakeChannelId, kFakeServiceId, kFakeMethodId);
+ Packet packet(pwpb::PacketType::RESPONSE,
+ kFakeChannelId,
+ kFakeServiceId,
+ kFakeMethodId);
std::array<std::byte, 32> buffer;
Result result = packet.Encode(buffer);
EXPECT_EQ(result.status(), OkStatus());
// No calls are registered on the client, so nothing should happen. The
// ProcessPacket call still returns OK since the client handled it.
- EXPECT_EQ(client_server.ProcessPacket(result.value(), output), OkStatus());
+ EXPECT_EQ(client_server.ProcessPacket(result.value()), OkStatus());
}
TEST(ClientServer, ProcessPacket_BadData) {
ClientServer client_server(channels);
- EXPECT_EQ(client_server.ProcessPacket({}, output), Status::DataLoss());
+ EXPECT_EQ(client_server.ProcessPacket({}), Status::DataLoss());
}
} // namespace
diff --git a/pw_rpc/docs.rst b/pw_rpc/docs.rst
index 25554f05a..83bd6d558 100644
--- a/pw_rpc/docs.rst
+++ b/pw_rpc/docs.rst
@@ -1,8 +1,8 @@
.. _module-pw_rpc:
-------
+======
pw_rpc
-------
+======
The ``pw_rpc`` module provides a system for defining and invoking remote
procedure calls (RPCs) on a device.
@@ -26,8 +26,9 @@ documents:
This documentation is under construction. Many sections are outdated or
incomplete. The content needs to be reorgnanized.
+---------------
Implementations
-===============
+---------------
Pigweed provides several client and server implementations of ``pw_rpc``.
.. list-table::
@@ -43,11 +44,11 @@ Pigweed provides several client and server implementations of ``pw_rpc``.
- ✅
- ✅
* - C++ (pw_protobuf)
- - planned
- - planned
+ - ✅
+ - ✅
* - Java
-
- - in development
+ - ✅
* - Python
-
- ✅
@@ -55,28 +56,53 @@ Pigweed provides several client and server implementations of ``pw_rpc``.
-
- in development
+-------------
RPC semantics
-=============
+-------------
The semantics of ``pw_rpc`` are similar to `gRPC
<https://grpc.io/docs/what-is-grpc/core-concepts/>`_.
RPC call lifecycle
-------------------
-In ``pw_rpc``, an RPC begins when the client sends a request packet. The server
-receives the request, looks up the relevant service method, then calls into the
-RPC function. The RPC is considered active until the server sends a response
-packet with the RPC's status. The client may terminate an ongoing RPC by
-cancelling it.
-
-``pw_rpc`` supports only one RPC invocation per service/method/channel. If a
-client calls an ongoing RPC on the same channel, the server cancels the ongoing
-call and reinvokes the RPC with the new request. This applies to unary and
-streaming RPCs, though the server may not have an opportunity to cancel a
-synchronously handled unary RPC before it completes. The same RPC may be invoked
-multiple times simultaneously if the invocations are on different channels.
+==================
+In ``pw_rpc``, an RPC begins when the client sends an initial packet. The server
+receives the packet, looks up the relevant service method, then calls into the
+RPC function. The RPC is considered active until the server sends a status to
+finish the RPC. The client may terminate an ongoing RPC by cancelling it.
+
+Depending the type of RPC, the client and server exchange zero or more protobuf
+request or response payloads. There are four RPC types:
+
+* **Unary**. The client sends one request and the server sends one
+ response with a status.
+* **Server streaming**. The client sends one request and the server sends zero
+ or more responses followed by a status.
+* **Client streaming**. The client sends zero or more requests and the server
+ sends one response with a status.
+* **Bidirectional streaming**. The client sends zero or more requests and the
+ server sends zero or more responses followed by a status.
+
+Events
+------
+The key events in the RPC lifecycle are:
+
+* **Start**. The client initiates the RPC. The server's RPC body executes.
+* **Finish**. The server sends a status and completes the RPC. The client calls
+ a callback.
+* **Request**. The client sends a request protobuf. The server calls a callback
+ when it receives it. In unary and server streaming RPCs, there is only one
+ request and it is handled when the RPC starts.
+* **Response**. The server sends a response protobuf. The client calls a
+ callback when it receives it. In unary and client streaming RPCs, there is
+ only one response and it is handled when the RPC completes.
+* **Error**. The server or client terminates the RPC abnormally with a status.
+ The receiving endpoint calls a callback.
+* **Client stream end**. The client sends a message that it has finished sending
+ requests. The server calls a callback when it receives it. Some servers may
+ ignore the client stream end. The client stream end is only relevant for
+ client and bidirectional streaming RPCs.
Status codes
-------------
+============
``pw_rpc`` call objects (``ClientReaderWriter``, ``ServerReaderWriter``, etc.)
use certain status codes to indicate what occurred. These codes are returned
from functions like ``Write()`` or ``Finish()``.
@@ -88,7 +114,7 @@ from functions like ``Write()`` or ``Finish()``.
:cpp:func:`pw::rpc::ChannelOutput::Send` error.
Unrequested responses
----------------------
+=====================
``pw_rpc`` supports sending responses to RPCs that have not yet been invoked by
a client. This is useful in testing and in situations like an RPC that triggers
reboot. After the reboot, the device opens the writer object and sends its
@@ -112,11 +138,12 @@ appropriate reader/writer class must be used.
// Finish the RPC.
CHECK_OK(writer.Finish(OkStatus()));
+---------------
Creating an RPC
-===============
+---------------
1. RPC service declaration
---------------------------
+==========================
Pigweed RPCs are declared in a protocol buffer service definition.
* `Protocol Buffer service documentation
@@ -166,10 +193,11 @@ This protocol buffer is declared in a ``BUILD.gn`` file as follows:
If you need to distinguish between a default-valued field and a missing field,
mark the field as ``optional``. The presence of the field can be detected
- with a ``HasField(name)`` or ``has_<field>`` member, depending on the library.
+ with ``std::optional``, a ``HasField(name)``, or ``has_<field>`` member,
+ depending on the library.
- Optional fields have some overhead --- default-valued fields are included in
- the encoded proto, and, if using Nanopb, the proto structs have a
+ Optional fields have some overhead --- if using Nanopb, default-valued fields
+ are included in the encoded proto, and the proto structs have a
``has_<field>`` flag for each optional field. Use plain fields if field
presence detection is not needed.
@@ -186,7 +214,7 @@ This protocol buffer is declared in a ``BUILD.gn`` file as follows:
}
2. RPC code generation
-----------------------
+======================
``pw_rpc`` generates a C++ header file for each ``.proto`` file. This header is
generated in the build output directory. Its exact location varies by build
system and toolchain, but the C++ include path always matches the sources
@@ -207,12 +235,12 @@ For example, the generated RPC header for ``"foo_bar/the_service.proto"`` is
The generated header defines a base class for each RPC service declared in the
``.proto`` file. A service named ``TheService`` in package ``foo.bar`` would
-generate the following base class for Nanopb:
+generate the following base class for pw_protobuf:
-.. cpp:class:: template <typename Implementation> foo::bar::pw_rpc::nanopb::TheService::Service
+.. cpp:class:: template <typename Implementation> foo::bar::pw_rpc::pwpb::TheService::Service
3. RPC service definition
--------------------------
+=========================
The serivce class is implemented by inheriting from the generated RPC service
base class and defining a method for each RPC. The methods must match the name
and function signature for one of the supported protobuf implementations.
@@ -230,7 +258,7 @@ Services may mix and match protobuf implementations within one service.
.. code-block:: sh
- find out/ -name <proto_name>.rpc.pb.h
+ find out/ -name <proto_name>.rpc.pwpb.h
#. Scroll to the bottom of the generated RPC header.
#. Copy the stub class declaration to a header file.
@@ -239,32 +267,33 @@ Services may mix and match protobuf implementations within one service.
#. List these files in a build target with a dependency on the
``pw_proto_library``.
-A Nanopb implementation of this service would be as follows:
+A pw_protobuf implementation of this service would be as follows:
.. code-block:: cpp
- #include "foo_bar/the_service.rpc.pb.h"
+ #include "foo_bar/the_service.rpc.pwpb.h"
namespace foo::bar {
- class TheService : public pw_rpc::nanopb::TheService::Service<TheService> {
+ class TheService : public pw_rpc::pwpb::TheService::Service<TheService> {
public:
- pw::Status MethodOne(const foo_bar_Request& request,
- foo_bar_Response& response) {
+ pw::Status MethodOne(const Request::Message& request,
+ Response::Message& response) {
// implementation
+ response.number = 123;
return pw::OkStatus();
}
- void MethodTwo(const foo_bar_Request& request,
- ServerWriter<foo_bar_Response>& response) {
+ void MethodTwo(const Request::Message& request,
+ ServerWriter<Response::Message>& response) {
// implementation
- response.Write(foo_bar_Response{.number = 123});
+ response.Write({.number = 123});
}
};
} // namespace foo::bar
-The Nanopb implementation would be declared in a ``BUILD.gn``:
+The pw_protobuf implementation would be declared in a ``BUILD.gn``:
.. code-block:: python
@@ -275,16 +304,11 @@ The Nanopb implementation would be declared in a ``BUILD.gn``:
pw_source_set("the_service") {
public_configs = [ ":public" ]
public = [ "public/foo_bar/service.h" ]
- public_deps = [ ":the_service_proto.nanopb_rpc" ]
+ public_deps = [ ":the_service_proto.pwpb_rpc" ]
}
-.. attention::
-
- pw_rpc's generated classes will support using ``pw_protobuf`` or raw buffers
- (no protobuf library) in the future.
-
4. Register the service with a server
--------------------------------------
+=====================================
This example code sets up an RPC server with an :ref:`HDLC<module-pw_hdlc>`
channel output and the example service.
@@ -294,9 +318,12 @@ channel output and the example service.
// pw_rpc server to use HDLC over UART; projects not using UART and HDLC must
// adapt this as necessary.
pw::stream::SysIoWriter writer;
- pw::rpc::RpcChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
+ pw::rpc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
writer, pw::hdlc::kDefaultRpcAddress, "HDLC output");
+ // Allocate an array of channels for the server to use. If dynamic allocation
+ // is enabled (PW_RPC_DYNAMIC_ALLOCATION=1), the server can be initialized
+ // without any channels, and they can be added later.
pw::rpc::Channel channels[] = {
pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
@@ -319,12 +346,12 @@ channel output and the example service.
std::array<std::byte, kMaxTransmissionUnit> input_buffer;
PW_LOG_INFO("Starting pw_rpc server");
- pw::hdlc::ReadAndProcessPackets(
- server, hdlc_channel_output, input_buffer);
+ pw::hdlc::ReadAndProcessPackets(server, input_buffer);
}
+--------
Channels
-========
+--------
``pw_rpc`` sends all of its packets over channels. These are logical,
application-layer routes used to tell the RPC system where a packet should go.
@@ -366,7 +393,7 @@ output.
}
Adding and removing channels
-----------------------------
+============================
New channels may be registered with the ``OpenChannel`` function. If dynamic
allocation is enabled (:c:macro:`PW_RPC_DYNAMIC_ALLOCATION` is 1), any number of
channels may be registered. If dynamic allocation is disabled, new channels may
@@ -384,8 +411,9 @@ with the ``ABORTED`` status.
// on_error callbacks with ABORTED status.
client->CloseChannel(1);
+--------
Services
-========
+--------
A service is a logical grouping of RPCs defined within a .proto file. ``pw_rpc``
uses these .proto definitions to generate code for a base service, from which
user-defined RPCs are implemented.
@@ -393,18 +421,25 @@ user-defined RPCs are implemented.
``pw_rpc`` supports multiple protobuf libraries, and the generated code API
depends on which is used.
+Services must be registered with a server in order to call their methods.
+Services may later be unregistered, which aborts calls for methods in that
+service and prevents future calls to them, until the service is re-registered.
+
.. _module-pw_rpc-protobuf-library-apis:
+---------------------
Protobuf library APIs
-=====================
+---------------------
.. toctree::
:maxdepth: 1
+ pwpb/docs
nanopb/docs
+----------------------------
Testing a pw_rpc integration
-============================
+----------------------------
After setting up a ``pw_rpc`` server in your project, you can test that it is
working as intended by registering the provided ``EchoService``, defined in
``echo.proto``, which echoes back a message that it receives.
@@ -413,14 +448,14 @@ working as intended by registering the provided ``EchoService``, defined in
:language: protobuf
:lines: 14-
-For example, in C++ with nanopb:
+For example, in C++ with pw_protobuf:
.. code:: c++
#include "pw_rpc/server.h"
// Include the apporpriate header for your protobuf library.
- #include "pw_rpc/echo_service_nanopb.h"
+ #include "pw_rpc/echo_service_pwpb.h"
constexpr pw::rpc::Channel kChannels[] = { /* ... */ };
static pw::rpc::Server server(kChannels);
@@ -432,7 +467,7 @@ For example, in C++ with nanopb:
}
Benchmarking and stress testing
--------------------------------
+===============================
.. toctree::
:maxdepth: 1
@@ -443,11 +478,12 @@ Benchmarking and stress testing
``pw_rpc`` provides an RPC service and Python module for stress testing and
benchmarking a ``pw_rpc`` deployment. See :ref:`module-pw_rpc-benchmark`.
+------
Naming
-======
+------
Reserved names
---------------
+==============
``pw_rpc`` reserves a few service method names so they can be used for generated
classes. The following names cannnot be used for service methods:
@@ -459,7 +495,7 @@ classes. The following names cannnot be used for service methods:
reserved words in supported languages applies.
Service naming style
---------------------
+====================
``pw_rpc`` service names should use capitalized camel case and should not use
the term "Service". Appending "Service" to a service name is redundant, similar
to appending "Class" or "Function" to a class or function name. The
@@ -491,20 +527,58 @@ The C++ service implementation class may append "Service" to the name.
void List(ConstByteSpan request, RawServerWriter& writer);
};
- }
+ } // namespace pw::file
For upstream Pigweed services, this naming style is a requirement. Note that
some services created before this was established may use non-compliant
names. For Pigweed users, this naming style is a suggestion.
+------------------------------
+C++ payload sizing limitations
+------------------------------
+The individual size of each sent RPC request or response is limited by
+``pw_rpc``'s ``PW_RPC_ENCODING_BUFFER_SIZE_BYTES`` configuration option when
+using Pigweed's C++ implementation. While multiple RPC messages can be enqueued
+(as permitted by the underlying transport), if a single individual sent message
+exceeds the limitations of the statically allocated encode buffer, the packet
+will fail to encode and be dropped.
+
+This applies to all C++ RPC service implementations (nanopb, raw, and pwpb),
+so it's important to ensure request and response message sizes do not exceed
+this limitation.
+
+As ``pw_rpc`` has some additional encoding overhead, a helper,
+``pw::rpc::MaxSafePayloadSize()`` is provided to expose the practical max RPC
+message payload size.
+
+.. code-block:: cpp
+
+ #include "pw_file/file.raw_rpc.pb.h"
+ #include "pw_rpc/channel.h"
+
+ namespace pw::file {
+
+ class FileSystemService : public pw_rpc::raw::FileSystem::Service<FileSystemService> {
+ public:
+ void List(ConstByteSpan request, RawServerWriter& writer);
+
+ private:
+ // Allocate a buffer for building proto responses.
+ static constexpr size_t kEncodeBufferSize = pw::rpc::MaxSafePayloadSize();
+ std::array<std::byte, kEncodeBufferSize> encode_buffer_;
+ };
+
+ } // namespace pw::file
+
+--------------------
Protocol description
-====================
+--------------------
Pigweed RPC servers and clients communicate using ``pw_rpc`` packets. These
packets are used to send requests and responses, control streams, cancel ongoing
RPCs, and report errors.
Packet format
--------------
+=============
Pigweed RPC packets consist of a type and a set of fields. The packets are
encoded as protocol buffers. The full packet format is described in
``pw_rpc/pw_rpc/internal/packet.proto``.
@@ -518,7 +592,7 @@ packet. Each packet type is only sent by either the client or the server.
These tables describe the meaning of and fields included with each packet type.
Client-to-server packets
-^^^^^^^^^^^^^^^^^^^^^^^^
+------------------------
+-------------------+-------------------------------------+
| packet type | description |
+===================+=====================================+
@@ -587,7 +661,7 @@ status codes result in the same action by the server: aborting the RPC.
* ``UNAVAILABLE`` -- Received a packet for an unknown channel.
Server-to-client packets
-^^^^^^^^^^^^^^^^^^^^^^^^
+------------------------
+-------------------+-------------------------------------+
| packet type | description |
+===================+=====================================+
@@ -649,7 +723,7 @@ status field indicates the type of error.
* ``UNAVAILABLE`` -- Received a packet for an unknown channel.
Inovking a service method
--------------------------
+=========================
Calling an RPC requires a specific sequence of packets. This section describes
the protocol for calling service methods of each type: unary, server streaming,
client streaming, and bidirectional streaming.
@@ -670,7 +744,7 @@ packet with status ``CANCELLED``. The server may finish an ongoing RPC at any
time by sending the ``RESPONSE`` packet.
Unary RPC
-^^^^^^^^^
+---------
In a unary RPC, the client sends a single request and the server sends a single
response.
@@ -684,7 +758,7 @@ sends the response), it may not be possible to cancel the RPC.
.. image:: unary_rpc_cancelled.svg
Server streaming RPC
-^^^^^^^^^^^^^^^^^^^^
+--------------------
In a server streaming RPC, the client sends a single request and the server
sends any number of ``SERVER_STREAM`` packets followed by a ``RESPONSE`` packet.
@@ -696,7 +770,7 @@ packet with status ``CANCELLED``. The server sends no response.
.. image:: server_streaming_rpc_cancelled.svg
Client streaming RPC
-^^^^^^^^^^^^^^^^^^^^
+--------------------
In a client streaming RPC, the client starts the RPC by sending a ``REQUEST``
packet with no payload. It then sends any number of messages in
``CLIENT_STREAM`` packets, followed by a ``CLIENT_STREAM_END``. The server sends
@@ -712,7 +786,7 @@ terminate the RPC at any time by sending a ``CLIENT_ERROR`` packet with status
.. image:: client_streaming_rpc_cancelled.svg
Bidirectional streaming RPC
-^^^^^^^^^^^^^^^^^^^^^^^^^^^
+--------------------^^^^^^^
In a bidirectional streaming RPC, the client sends any number of requests and
the server sends any number of responses. The client invokes the RPC by sending
a ``REQUEST`` with no payload. It sends a ``CLIENT_STREAM_END`` packet when it
@@ -728,6 +802,10 @@ terminate the RPC at any time by sending a ``CLIENT_ERROR`` packet with status
.. image:: bidirectional_streaming_rpc_cancelled.svg
+-------
+C++ API
+-------
+
RPC server
==========
Declare an instance of ``rpc::Server`` and register services with it.
@@ -771,12 +849,12 @@ Packet flow
^^^^^^^^^^^
Requests
-~~~~~~~~
+........
.. image:: request_packets.svg
Responses
-~~~~~~~~~
+.........
.. image:: response_packets.svg
@@ -811,6 +889,14 @@ client, the client's ``ProcessPacket`` function is called with the packet data.
my_client.ProcessPacket(packet);
}
+Note that client processing such as callbacks will be invoked within
+the body of ``ProcessPacket``.
+
+If certain packets need to be filtered out, or if certain client processing
+needs to be invoked from a specific thread or context, the ``PacketMeta`` class
+can be used to determine which service or channel a packet is targeting. After
+filtering, ``ProcessPacket`` can be called from the appropriate environment.
+
.. _module-pw_rpc-making-calls:
Making RPC calls
@@ -824,13 +910,12 @@ please refer to the
service client implementations can exist simulatenously and share the same
``Client`` class.
-When a call is made, a ``pw::rpc::ClientCall`` object is returned to the caller.
-This object tracks the ongoing RPC call, and can be used to manage it. An RPC
-call is only active as long as its ``ClientCall`` object is alive.
+When a call is made, a call object is returned to the caller. This object tracks
+the ongoing RPC call, and can be used to manage it. An RPC call is only active
+as long as its call object is alive.
.. tip::
- Use ``std::move`` when passing around ``ClientCall`` objects to keep RPCs
- alive.
+ Use ``std::move`` when passing around call objects to keep RPCs alive.
Example
^^^^^^^
@@ -842,10 +927,11 @@ Example
// Generated clients are namespaced with their proto library.
using EchoClient = pw_rpc::nanopb::EchoService::Client;
- // RPC channel ID on which to make client calls.
+ // RPC channel ID on which to make client calls. RPC calls cannot be made on
+ // channel 0 (Channel::kUnassignedChannelId).
constexpr uint32_t kDefaultChannelId = 1;
- EchoClient::EchoCall echo_call;
+ pw::rpc::NanopbUnaryReceiver<pw_rpc_EchoMessage> echo_call;
// Callback invoked when a response is received. This is called synchronously
// from Client::ProcessPacket.
@@ -865,10 +951,10 @@ Example
// Create a client to call the EchoService.
EchoClient echo_client(my_rpc_client, kDefaultChannelId);
- pw_rpc_EchoMessage request = pw_rpc_EchoMessage_init_default;
+ pw_rpc_EchoMessage request{};
pw::string::Copy(message, request.msg);
- // By assigning the returned ClientCall to the global echo_call, the RPC
+ // By assigning the returned call to the global echo_call, the RPC
// call is kept alive until it completes. When a response is received, it
// will be logged by the handler function and the call will complete.
echo_call = echo_client.Echo(request, EchoResponse);
@@ -878,20 +964,311 @@ Example
}
}
-Client implementation details
------------------------------
+Call objects
+============
+An RPC call is represented by a call object. Server and client calls use the
+same base call class in C++, but the public API is different depending on the
+type of call (see `RPC call lifecycle`_) and whether it is being used by the
+server or client.
+
+The public call types are as follows:
+
+.. list-table::
+ :header-rows: 1
+
+ * - RPC Type
+ - Server call
+ - Client call
+ * - Unary
+ - ``(Raw|Nanopb|Pwpb)UnaryResponder``
+ - ``(Raw|Nanopb|Pwpb)UnaryReceiver``
+ * - Server streaming
+ - ``(Raw|Nanopb|Pwpb)ServerWriter``
+ - ``(Raw|Nanopb|Pwpb)ClientReader``
+ * - Client streaming
+ - ``(Raw|Nanopb|Pwpb)ServerReader``
+ - ``(Raw|Nanopb|Pwpb)ClientWriter``
+ * - Bidirectional streaming
+ - ``(Raw|Nanopb|Pwpb)ServerReaderWriter``
+ - ``(Raw|Nanopb|Pwpb)ClientReaderWriter``
+
+Client call API
+---------------
+Client call objects provide a few common methods.
+
+.. cpp:class:: pw::rpc::ClientCallType
+
+ The ``ClientCallType`` will be one of the following types:
+
+ - ``(Raw|Nanopb|Pwpb)UnaryReceiver`` for unary
+ - ``(Raw|Nanopb|Pwpb)ClientReader`` for server streaming
+ - ``(Raw|Nanopb|Pwpb)ClientWriter`` for client streaming
+ - ``(Raw|Nanopb|Pwpb)ClientReaderWriter`` for bidirectional streaming
+
+ .. cpp:function:: bool active() const
+
+ Returns true if the call is active.
+
+ .. cpp:function:: uint32_t channel_id() const
+
+ Returns the channel ID of this call, which is 0 if the call is inactive.
+
+ .. cpp:function:: uint32_t id() const
+
+ Returns the call ID, a unique identifier for this call.
+
+ .. cpp:function:: void Write(RequestType)
+
+ Only available on client and bidirectional streaming calls. Sends a stream
+ request. Returns:
+
+ - ``OK`` - the request was successfully sent
+ - ``FAILED_PRECONDITION`` - the writer is closed
+ - ``INTERNAL`` - pw_rpc was unable to encode message; does not apply to raw
+ calls
+ - other errors - the :cpp:class:`ChannelOutput` failed to send the packet;
+ the error codes are determined by the :cpp:class:`ChannelOutput`
+ implementation
+
+ .. cpp:function:: pw::Status CloseClientStream()
+
+ Only available on client and bidirectional streaming calls. Notifies the
+ server that no further client stream messages will be sent.
+
+ .. cpp:function:: pw::Status Cancel()
+
+ Cancels this RPC. Closes the call and sends a ``CANCELLED`` error to the
+ server. Return statuses are the same as :cpp:func:`Write`.
+
+ .. cpp:function:: void Abandon()
+
+ Closes this RPC locally. Sends a ``CLIENT_STREAM_END``, but no cancellation
+ packet. Future packets for this RPC are dropped, and the client sends a
+ ``FAILED_PRECONDITION`` error in response because the call is not active.
+
+ .. cpp:function:: void set_on_completed(pw::Function<void(ResponseTypeIfUnaryOnly, pw::Status)>)
+
+ Sets the callback that is called when the RPC completes normally. The
+ signature depends on whether the call has a unary or stream response.
+
+ .. cpp:function:: void set_on_error(pw::Function<void(pw::Status)>)
+
+ Sets the callback that is called when the RPC is terminated due to an error.
+
+ .. cpp:function:: void set_on_next(pw::Function<void(ResponseType)>)
+
+ Only available on server and bidirectional streaming calls. Sets the callback
+ that is called for each stream response.
+
+Callbacks
+---------
+The C++ call objects allow users to set callbacks that are invoked when RPC
+`events`_ occur.
+
+.. list-table::
+ :header-rows: 1
+
+ * - Name
+ - Stream signature
+ - Non-stream signature
+ - Server
+ - Client
+ * - ``on_error``
+ - ``void(pw::Status)``
+ - ``void(pw::Status)``
+ - ✅
+ - ✅
+ * - ``on_next``
+ - n/a
+ - ``void(const PayloadType&)``
+ - ✅
+ - ✅
+ * - ``on_completed``
+ - ``void(pw::Status)``
+ - ``void(const PayloadType&, pw::Status)``
+ -
+ - ✅
+ * - ``on_client_stream_end``
+ - ``void()``
+ - n/a
+ - ✅ (:c:macro:`optional <PW_RPC_CLIENT_STREAM_END_CALLBACK>`)
+ -
+
+Limitations and restrictions
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+RPC callbacks are free to perform most actions, including invoking new RPCs or
+cancelling pending calls. However, the C++ implementation imposes some
+limitations and restrictions that must be observed.
+
+Destructors & moves wait for callbacks to complete
+...................................................
+* Callbacks must not destroy their call object. Attempting to do so will result
+ in deadlock.
+* Other threads may destroy a call while its callback is running, but that
+ thread will block until all callbacks complete.
+* Callbacks must not move their call object if it the call is still active. They
+ may move their call object after it has terminated. Callbacks may move a
+ different call into their call object, since moving closes the destination
+ call.
+* Other threads may move a call object while it has a callback running, but they
+ will block until the callback completes if the call is still active.
+
+.. warning::
+
+ Deadlocks or crashes occur if a callback:
+
+ - attempts to destroy its call object
+ - attempts to move its call object while the call is still active
+ - never returns
+
+ If ``pw_rpc`` a callback violates these restrictions, a crash may occur,
+ depending on the value of :c:macro:`PW_RPC_CALLBACK_TIMEOUT_TICKS`. These
+ crashes have a message like the following:
+
+ .. code-block:: text
-The ClientCall class
-^^^^^^^^^^^^^^^^^^^^
-``ClientCall`` stores the context of an active RPC, and serves as the user's
-interface to the RPC client. The core RPC library provides a base ``ClientCall``
-class with common functionality, which is then extended for RPC client
-implementations tied to different protobuf libraries to provide convenient
-interfaces for working with RPCs.
+ A callback for RPC 1:cc0f6de0/31e616ce has not finished after 10000 ticks.
+ This may indicate that an RPC callback attempted to destroy or move its own
+ call object, which is not permitted. Fix this condition or change the value of
+ PW_RPC_CALLBACK_TIMEOUT_TICKS to avoid this crash.
-The RPC server stores a list of all of active ``ClientCall`` objects. When an
-incoming packet is recieved, it dispatches to one of its active calls, which
-then decodes the payload and presents it to the user.
+ See https://pigweed.dev/pw_rpc#destructors-moves-wait-for-callbacks-to-complete
+ for details.
+
+Only one thread at a time may execute ``on_next``
+.................................................
+Only one thread may execute the ``on_next`` callback for a specific service
+method at a time. If a second thread calls ``ProcessPacket()`` with a stream
+packet before the ``on_next`` callback for the previous packet completes, the
+second packet will be dropped. The RPC endpoint logs a warning when this occurs.
+
+Example warning for a dropped stream packet:
+
+.. code-block:: text
+
+ WRN Received stream packet for 1:cc0f6de0/31e616ce before the callback for
+ a previous packet completed! This packet will be dropped. This can be
+ avoided by handling packets for a particular RPC on only one thread.
+
+RPC calls introspection
+=======================
+``pw_rpc`` provides ``pw_rpc/method_info.h`` header that allows to obtain
+information about the generated RPC method in compile time.
+
+For now it provides only two types: ``MethodRequestType<RpcMethod>`` and
+``MethodResponseType<RpcMethod>``. They are aliases to the types that are used
+as a request and response respectively for the given RpcMethod.
+
+Example
+-------
+We have an RPC service ``SpecialService`` with ``MyMethod`` method:
+
+.. code-block:: protobuf
+
+ package some.package;
+ service SpecialService {
+ rpc MyMethod(MyMethodRequest) returns (MyMethodResponse) {}
+ }
+
+We also have a templated Storage type alias:
+
+.. code-block:: c++
+
+ template <auto kMethod>
+ using Storage =
+ std::pair<MethodRequestType<kMethod>, MethodResponseType<kMethod>>;
+
+``Storage<some::package::pw_rpc::pwpb::SpecialService::MyMethod>`` will
+instantiate as:
+
+.. code-block:: c++
+
+ std::pair<some::package::MyMethodRequest::Message,
+ some::package::MyMethodResponse::Message>;
+
+.. note::
+
+ Only nanopb and pw_protobuf have real types as
+ ``MethodRequestType<RpcMethod>``/``MethodResponseType<RpcMethod>``. Raw has
+ them both set as ``void``. In reality, they are ``pw::ConstByteSpan``. Any
+ helper/trait that wants to use this types for raw methods should do a custom
+ implementation that copies the bytes under the span instead of copying just
+ the span.
+
+Client Synchronous Call wrappers
+================================
+If synchronous behavior is desired when making client calls, users can use one
+of the ``SynchronousCall<RpcMethod>`` wrapper functions to make their RPC call.
+These wrappers effectively wrap the asynchronous Client RPC call with a timed
+thread notification and return once a result is known or a timeout has occurred.
+These return a ``SynchronousCallResult<Response>`` object, which can be queried
+to determine whether any error scenarios occurred and, if not, access the
+response.
+
+``SynchronousCall<RpcMethod>`` will block indefinitely, whereas
+``SynchronousCallFor<RpcMethod>`` and ``SynchronousCallUntil<RpcMethod>`` will
+block for a given timeout or until a deadline, respectively. All wrappers work
+with both the standalone static RPC functions and the generated Client member
+methods.
+
+.. note:: Use of the SynchronousCall wrappers requires a TimedThreadNotification
+ backend.
+.. note:: Only nanopb and pw_protobuf Unary RPC methods are supported.
+
+Example
+-------
+.. code-block:: c++
+
+ #include "pw_rpc/synchronous_call.h"
+
+ void InvokeUnaryRpc() {
+ pw::rpc::Client client;
+ pw::rpc::Channel channel;
+
+ RoomInfoRequest request;
+ SynchronousCallResult<RoomInfoResponse> result =
+ SynchronousCall<Chat::GetRoomInformation>(client, channel.id(), request);
+
+ if (result.is_rpc_error()) {
+ ShutdownClient(client);
+ } else if (result.is_server_error()) {
+ HandleServerError(result.status());
+ } else if (result.is_timeout()) {
+ // SynchronousCall will block indefinitely, so we should never get here.
+ PW_UNREACHABLE();
+ }
+ HandleRoomInformation(std::move(result).response());
+ }
+
+ void AnotherExample() {
+ pw_rpc::nanopb::Chat::Client chat_client(client, channel);
+ constexpr auto kTimeout = pw::chrono::SystemClock::for_at_least(500ms);
+
+ RoomInfoRequest request;
+ auto result = SynchronousCallFor<Chat::GetRoomInformation>(
+ chat_client, request, kTimeout);
+
+ if (result.is_timeout()) {
+ RetryRoomRequest();
+ } else {
+ ...
+ }
+ }
+
+The ``SynchronousCallResult<Response>`` is also compatible with the PW_TRY
+family of macros, but users should be aware that their use will lose information
+about the type of error. This should only be used if the caller will handle all
+error scenarios the same.
+
+.. code-block:: c++
+
+ pw::Status SyncRpc() {
+ const RoomInfoRequest request;
+ PW_TRY_ASSIGN(const RoomInfoResponse& response,
+ SynchronousCall<Chat::GetRoomInformation>(client, request));
+ HandleRoomInformation(response);
+ return pw::OkStatus();
+ }
ClientServer
============
@@ -923,15 +1300,18 @@ Testing
Client unit testing in C++
--------------------------
``pw_rpc`` supports invoking RPCs, simulating server responses, and checking
-what packets are sent by an RPC client in tests. Both raw and Nanopb interfaces
-are supported. Code that uses the raw API may be tested with the Nanopb test
-helpers, and vice versa.
+what packets are sent by an RPC client in tests. Raw, Nanopb and Pwpb interfaces
+are supported. Code that uses the raw API may be tested with the raw test
+helpers, and vice versa. The Nanopb and Pwpb APIs also provides a test helper
+with a real client-server pair that supports testing of asynchronous messaging.
-To test code that invokes RPCs, declare a ``RawClientTestContext`` or
-``NanopbClientTestContext``. These test context objects provide a
-preconfigured RPC client, channel, server fake, and buffer for encoding packets.
-These test classes are defined in ``pw_rpc/raw/client_testing.h`` and
-``pw_rpc/nanopb/client_testing.h``.
+To test sychronous code that invokes RPCs, declare a ``RawClientTestContext``,
+``PwpbClientTestContext``, or ``NanopbClientTestContext``. These test context
+objects provide a preconfigured RPC client, channel, server fake, and buffer for
+encoding packets.
+
+These test classes are defined in ``pw_rpc/raw/client_testing.h``,
+``pw_rpc/pwpb/client_testing.h``, or ``pw_rpc/nanopb/client_testing.h``.
Use the context's ``client()`` and ``channel()`` to invoke RPCs. Use the
context's ``server()`` to simulate responses. To verify that the client sent the
@@ -944,12 +1324,12 @@ the expected data was sent and then simulates a response from the server.
#include "pw_rpc/raw/client_testing.h"
- class ThingThatCallsRpcs {
+ class ClientUnderTest {
public:
// To support injecting an RPC client for testing, classes that make RPC
// calls should take an RPC client and channel ID or an RPC service client
// (e.g. pw_rpc::raw::MyService::Client).
- ThingThatCallsRpcs(pw::rpc::Client& client, uint32_t channel_id);
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
void DoSomethingThatInvokesAnRpc();
@@ -958,7 +1338,7 @@ the expected data was sent and then simulates a response from the server.
TEST(TestAThing, InvokesRpcAndHandlesResponse) {
RawClientTestContext context;
- ThingThatCallsRpcs thing(context.client(), context.channel().id());
+ ClientUnderTest thing(context.client(), context.channel().id());
// Execute the code that invokes the MyService.TheMethod RPC.
things.DoSomethingThatInvokesAnRpc();
@@ -979,111 +1359,176 @@ the expected data was sent and then simulates a response from the server.
EXPECT_TRUE(thing.SetToTrueWhenRpcCompletes());
}
-Integration testing with ``pw_rpc``
------------------------------------
-``pw_rpc`` provides utilities to simplify writing integration tests for systems
-that communicate with ``pw_rpc``. The integration test utitilies set up a socket
-to use for IPC between an RPC server and client process.
+To test client code that uses asynchronous responses, encapsulates multiple
+rpc calls to one or more services, or uses a custom service implemenation,
+declare a ``NanopbClientServerTestContextThreaded`` or
+``PwpbClientServerTestContextThreaded``. These test object are defined in
+``pw_rpc/nanopb/client_server_testing_threaded.h`` and
+``pw_rpc/pwpb/client_server_testing_threaded.h``.
-The server binary uses the system RPC server facade defined
-``pw_rpc_system_server/rpc_server.h``. The client binary uses the functions
-defined in ``pw_rpc/integration_testing.h``:
+Use the context's ``server()`` to register a ``Service`` implementation, and
+``client()`` and ``channel()`` to invoke RPCs. Create a ``Thread`` using the
+context as a ``ThreadCore`` to have it asycronously forward request/responses or
+call ``ForwardNewPackets`` to synchronously process all messages. To verify that
+the client/server sent the expected data, use the context's
+``request(uint32_t index)`` and ``response(uint32_t index)`` to retrieve the
+ordered messages.
-.. cpp:var:: constexpr uint32_t kChannelId
+For example, the following tests a class that invokes an RPC and blocks till a
+response is received. It verifies that expected data was both sent and received.
- The RPC channel for integration test RPCs.
+.. code-block:: cpp
-.. cpp:function:: pw::rpc::Client& pw::rpc::integration_test::Client()
+ #include "my_library_protos/my_service.rpc.pb.h"
+ #include "pw_rpc/nanopb/client_server_testing_threaded.h"
+ #include "pw_thread_stl/options.h"
- Returns the global RPC client for integration test use.
+ class ClientUnderTest {
+ public:
+ // To support injecting an RPC client for testing, classes that make RPC
+ // calls should take an RPC client and channel ID or an RPC service client
+ // (e.g. pw_rpc::raw::MyService::Client).
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
-.. cpp:function:: pw::Status pw::rpc::integration_test::InitializeClient(int argc, char* argv[], const char* usage_args = "PORT")
+ Status BlockOnResponse(uint32_t value);
+ };
- Initializes logging and the global RPC client for integration testing. Starts
- a background thread that processes incoming.
-Module Configuration Options
-============================
-The following configurations can be adjusted via compile-time configuration of
-this module, see the
-:ref:`module documentation <module-structure-compile-time-configuration>` for
-more details.
+ class TestService final : public MyService<TestService> {
+ public:
+ Status TheMethod(const pw_rpc_test_TheMethod& request,
+ pw_rpc_test_TheMethod& response) {
+ response.value = request.integer + 1;
+ return pw::OkStatus();
+ }
+ };
-.. c:macro:: PW_RPC_CLIENT_STREAM_END_CALLBACK
+ TEST(TestServiceTest, ReceivesUnaryRpcReponse) {
+ NanopbClientServerTestContextThreaded<> ctx(pw::thread::stl::Options{});
+ TestService service;
+ ctx.server().RegisterService(service);
+ ClientUnderTest client(ctx.client(), ctx.channel().id());
- In client and bidirectional RPCs, pw_rpc clients may signal that they have
- finished sending requests with a CLIENT_STREAM_END packet. While this can be
- useful in some circumstances, it is often not necessary.
+ // Execute the code that invokes the MyService.TheMethod RPC.
+ constexpr uint32_t value = 1;
+ const auto result = client.BlockOnResponse(value);
+ const auto request = ctx.request<MyService::TheMethod>(0);
+ const auto response = ctx.resonse<MyService::TheMethod>(0);
+
+ // Verify content of messages
+ EXPECT_EQ(result, pw::OkStatus());
+ EXPECT_EQ(request.value, value);
+ EXPECT_EQ(response.value, value + 1);
+ }
- This option controls whether or not include a callback that is called when
- the client stream ends. The callback is included in all ServerReader/Writer
- objects as a pw::Function, so may have a significant cost.
+Synchronous versions of these test contexts also exist that may be used on
+non-threaded systems ``NanopbClientServerTestContext`` and
+``PwpbClientServerTestContext``. While these do not allow for asynchronous
+messaging they support the use of service implemenations and use a similar
+syntax. When these are used ``.ForwardNewPackets()`` should be called after each
+rpc call to trigger sending of queued messages.
- This is disabled by default.
+For example, the following tests a class that invokes an RPC that is responded
+to with a test service implemenation.
-.. c:macro:: PW_RPC_NANOPB_STRUCT_MIN_BUFFER_SIZE
+.. code-block:: cpp
- The Nanopb-based pw_rpc implementation allocates memory to use for Nanopb
- structs for the request and response protobufs. The template function that
- allocates these structs rounds struct sizes up to this value so that
- different structs can be allocated with the same function. Structs with sizes
- larger than this value cause an extra function to be created, which slightly
- increases code size.
+ #include "my_library_protos/my_service.rpc.pb.h"
+ #include "pw_rpc/nanopb/client_server_testing.h"
- Ideally, this value will be set to the size of the largest Nanopb struct used
- as an RPC request or response. The buffer can be stack or globally allocated
- (see ``PW_RPC_NANOPB_STRUCT_BUFFER_STACK_ALLOCATE``).
+ class ClientUnderTest {
+ public:
+ ClientUnderTest(pw::rpc::Client& client, uint32_t channel_id);
- This defaults to 64 Bytes.
+ Status SendRpcCall(uint32_t value);
+ };
-.. c:macro:: PW_RPC_USE_GLOBAL_MUTEX
- Enable global synchronization for RPC calls. If this is set, a backend must
- be configured for pw_sync:mutex.
+ class TestService final : public MyService<TestService> {
+ public:
+ Status TheMethod(const pw_rpc_test_TheMethod& request,
+ pw_rpc_test_TheMethod& response) {
+ response.value = request.integer + 1;
+ return pw::OkStatus();
+ }
+ };
- This is disabled by default.
+ TEST(TestServiceTest, ReceivesUnaryRpcReponse) {
+ NanopbClientServerTestContext<> ctx();
+ TestService service;
+ ctx.server().RegisterService(service);
+ ClientUnderTest client(ctx.client(), ctx.channel().id());
-.. c:macro:: PW_RPC_DYNAMIC_ALLOCATION
+ // Execute the code that invokes the MyService.TheMethod RPC.
+ constexpr uint32_t value = 1;
+ const auto result = client.SendRpcCall(value);
+ // Needed after ever RPC call to trigger forward of packets
+ ctx.ForwardNewPackets();
+ const auto request = ctx.request<MyService::TheMethod>(0);
+ const auto response = ctx.resonse<MyService::TheMethod>(0);
+
+ // Verify content of messages
+ EXPECT_EQ(result, pw::OkStatus());
+ EXPECT_EQ(request.value, value);
+ EXPECT_EQ(response.value, value + 1);
+ }
- Whether pw_rpc should use dynamic memory allocation internally. If enabled,
- pw_rpc dynamically allocates channels and its encoding buffers. RPC users may
- use dynamic allocation independently of this option (e.g. to allocate pw_rpc
- call objects).
+SendResponseIfCalled() helper
+-----------------------------
+``SendResponseIfCalled()`` function waits on ``*ClientTestContext*`` output to
+have a call for the specified method and then responses to it. It supports
+timeout for the waiting part (default timeout is 100ms).
+
+.. code:: c++
+
+ #include "pw_rpc/test_helpers.h"
+
+ pw::rpc::PwpbClientTestContext client_context;
+ other::pw_rpc::pwpb::OtherService::Client other_service_client(
+ client_context.client(), client_context.channel().id());
+
+ PW_PWPB_TEST_METHOD_CONTEXT(MyService, GetData)
+ context(other_service_client);
+ context.call({});
+
+ ASSERT_OK(pw::rpc::test::SendResponseIfCalled<
+ other::pw_rpc::pwpb::OtherService::GetPart>(
+ client_context, {.value = 42}));
- The semantics for allocating and initializing channels change depending on
- this option. If dynamic allocation is disabled, pw_rpc endpoints (servers or
- clients) use an externally-allocated, fixed-size array of channels.
- That array must include unassigned channels or existing channels must be
- closed to add new channels.
+ // At this point MyService::GetData handler received the GetPartResponse.
- If dynamic allocation is enabled, an span of channels may be passed to the
- endpoint at construction, but these channels are only used to initialize its
- internal std::vector of channels. External channel objects are NOT used by
- the endpoint cannot be updated if dynamic allocation is enabled. No
- unassigned channels should be passed to the endpoint; they will be ignored.
- Any number of channels may be added to the endpoint, without closing existing
- channels, but adding channels will use more memory.
+Integration testing with ``pw_rpc``
+-----------------------------------
+``pw_rpc`` provides utilities to simplify writing integration tests for systems
+that communicate with ``pw_rpc``. The integration test utitilies set up a socket
+to use for IPC between an RPC server and client process.
-.. c:macro:: PW_RPC_CONFIG_LOG_LEVEL
+The server binary uses the system RPC server facade defined
+``pw_rpc_system_server/rpc_server.h``. The client binary uses the functions
+defined in ``pw_rpc/integration_testing.h``:
- The log level to use for this module. Logs below this level are omitted.
+.. cpp:var:: constexpr uint32_t kChannelId
- This defaults to ``PW_LOG_LEVEL_INFO``.
+ The RPC channel for integration test RPCs.
-.. c:macro:: PW_RPC_CONFIG_LOG_MODULE_NAME
+.. cpp:function:: pw::rpc::Client& pw::rpc::integration_test::Client()
- The log module name to use for this module.
+ Returns the global RPC client for integration test use.
- This defaults to ``"PW_RPC"``.
+.. cpp:function:: pw::Status pw::rpc::integration_test::InitializeClient(int argc, char* argv[], const char* usage_args = "PORT")
-.. c:macro:: PW_RPC_NANOPB_STRUCT_BUFFER_STACK_ALLOCATE
+ Initializes logging and the global RPC client for integration testing. Starts
+ a background thread that processes incoming.
- This option determines whether to allocate the Nanopb structs on the stack or
- in a global variable. Globally allocated structs are NOT thread safe, but
- work fine when the RPC server's ProcessPacket function is only called from
- one thread.
+Module Configuration Options
+============================
+The following configurations can be adjusted via compile-time configuration of
+this module, see the
+:ref:`module documentation <module-structure-compile-time-configuration>` for
+more details.
- This is enabled by default.
+.. doxygenfile:: pw_rpc/public/pw_rpc/internal/config.h
+ :sections: define
Sharing server and client code
==============================
@@ -1115,7 +1560,9 @@ minimal overhead, ``pw_rpc`` uses a single, global mutex (when
Because ``pw_rpc`` uses a global mutex, it also uses a global buffer to encode
outgoing packets. The size of the buffer is set with
-``PW_RPC_ENCODING_BUFFER_SIZE``, which defaults to 512 B.
+``PW_RPC_ENCODING_BUFFER_SIZE_BYTES``, which defaults to 512 B. If dynamic
+allocation is enabled, this size does not affect how large RPC messages can be,
+but it is still used for sizing buffers in test utilities.
Users of ``pw_rpc`` must implement the :cpp:class:`pw::rpc::ChannelOutput`
interface.
@@ -1140,7 +1587,7 @@ interface.
returns :cpp:member:`kUnlimited`, which indicates that there is no MTU
limit.
- .. cpp:function:: virtual pw::Status Send(std::span<std::byte> packet)
+ .. cpp:function:: virtual pw::Status Send(span<std::byte> packet)
Sends an encoded RPC packet. Returns OK if further packets may be sent, even
if the current packet could not be sent. Returns any other status if the
diff --git a/pw_rpc/endpoint.cc b/pw_rpc/endpoint.cc
index c0e5079b2..dd6cd53c8 100644
--- a/pw_rpc/endpoint.cc
+++ b/pw_rpc/endpoint.cc
@@ -21,22 +21,71 @@
#include "pw_log/log.h"
#include "pw_rpc/internal/lock.h"
-namespace pw::rpc::internal {
+#if PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_BUSY_LOOP
-RpcLock& rpc_lock() {
- static RpcLock lock;
- return lock;
-}
+static_assert(
+ PW_RPC_USE_GLOBAL_MUTEX == 0,
+ "The RPC global mutex is enabled, but no pw_rpc yield mode is selected! "
+ "Because the global mutex is in use, pw_rpc may be used from multiple "
+ "threads. This could result in thread starvation. To fix this, set "
+ "PW_RPC_YIELD to PW_RPC_YIELD_MODE_SLEEP and add a dependency on "
+ "pw_thread:sleep.");
-Endpoint::~Endpoint() {
- // Since the calls remove themselves from the Endpoint in
- // CloseAndSendResponse(), close responders until no responders remain.
- while (!calls_.empty()) {
- calls_.front().CloseAndSendResponse(OkStatus()).IgnoreError();
- }
+#elif PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_SLEEP
+
+#include <chrono>
+
+#if !__has_include("pw_thread/sleep.h")
+
+static_assert(false,
+ "PW_RPC_YIELD_MODE is PW_RPC_YIELD_MODE_SLEEP "
+ "(pw::this_thread::sleep_for()), but no backend is set for "
+ "pw_thread:sleep. Set a pw_thread:sleep backend or use a "
+ "different PW_RPC_YIELD_MODE setting.");
+
+#endif // !__has_include("pw_thread/sleep.h")
+
+#include "pw_thread/sleep.h"
+
+#elif PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_YIELD
+
+#if !__has_include("pw_thread/yield.h")
+
+static_assert(false,
+ "PW_RPC_YIELD_MODE is PW_RPC_YIELD_MODE_YIELD "
+ "(pw::this_thread::yield()), but no backend is set for "
+ "pw_thread:yield. Set a pw_thread:yield backend or use a "
+ "different PW_RPC_YIELD_MODE setting.");
+
+#endif // !__has_include("pw_thread/yield.h")
+
+#include "pw_thread/yield.h"
+
+#else
+
+static_assert(
+ false,
+ "PW_RPC_YIELD_MODE macro must be set to PW_RPC_YIELD_MODE_BUSY_LOOP, "
+ "PW_RPC_YIELD_MODE_SLEEP (pw::this_thread::sleep_for()), or "
+ "PW_RPC_YIELD_MODE_YIELD (pw::this_thread::yield())");
+
+#endif // PW_RPC_YIELD_MODE
+
+namespace pw::rpc::internal {
+
+void YieldRpcLock() {
+ rpc_lock().unlock();
+#if PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_SLEEP
+ static constexpr chrono::SystemClock::duration kSleepDuration =
+ PW_RPC_YIELD_SLEEP_DURATION;
+ this_thread::sleep_for(kSleepDuration);
+#elif PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_YIELD
+ this_thread::yield();
+#endif // PW_RPC_YIELD_MODE
+ rpc_lock().lock();
}
-Result<Packet> Endpoint::ProcessPacket(std::span<const std::byte> data,
+Result<Packet> Endpoint::ProcessPacket(span<const std::byte> data,
Packet::Destination destination) {
Result<Packet> result = Packet::FromBuffer(data);
@@ -60,57 +109,116 @@ Result<Packet> Endpoint::ProcessPacket(std::span<const std::byte> data,
return result;
}
-void Endpoint::RegisterCall(Call& call) {
- Call* const existing_call = FindCallById(
- call.channel_id_locked(), call.service_id(), call.method_id());
-
- RegisterUniqueCall(call);
-
- if (existing_call != nullptr) {
- // TODO(pwbug/597): Ensure call object is locked when calling callback. For
- // on_error, could potentially move the callback and call it after the
- // lock is released.
- existing_call->HandleError(Status::Cancelled());
- rpc_lock().lock();
+void Endpoint::RegisterCall(Call& new_call) {
+ // Mark any exisitng duplicate calls as cancelled.
+ auto [before_call, call] = FindIteratorsForCall(new_call);
+ if (call != calls_.end()) {
+ CloseCallAndMarkForCleanup(before_call, call, Status::Cancelled());
}
+
+ // Register the new call.
+ calls_.push_front(new_call);
}
-Call* Endpoint::FindCallById(uint32_t channel_id,
- uint32_t service_id,
- uint32_t method_id) {
- for (Call& call : calls_) {
- if (channel_id == call.channel_id_locked() &&
- service_id == call.service_id() && method_id == call.method_id()) {
- return &call;
+std::tuple<IntrusiveList<Call>::iterator, IntrusiveList<Call>::iterator>
+Endpoint::FindIteratorsForCall(uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ uint32_t call_id) {
+ auto previous = calls_.before_begin();
+ auto call = calls_.begin();
+
+ while (call != calls_.end()) {
+ if (channel_id == call->channel_id_locked() &&
+ service_id == call->service_id() && method_id == call->method_id()) {
+ if (call_id == call->id() || call_id == kOpenCallId) {
+ break;
+ }
+ if (call->id() == kOpenCallId) {
+ // Calls with ID of `kOpenCallId` were unrequested, and
+ // are updated to have the call ID of the first matching request.
+ call->set_id(call_id);
+ break;
+ }
}
+ previous = call;
+ ++call;
}
- return nullptr;
+
+ return {previous, call};
}
Status Endpoint::CloseChannel(uint32_t channel_id) {
- LockGuard lock(rpc_lock());
+ rpc_lock().lock();
Channel* channel = channels_.Get(channel_id);
if (channel == nullptr) {
+ rpc_lock().unlock();
return Status::NotFound();
}
channel->Close();
// Close pending calls on the channel that's going away.
+ AbortCalls(AbortIdType::kChannel, channel_id);
+
+ CleanUpCalls();
+
+ return OkStatus();
+}
+
+void Endpoint::AbortCalls(AbortIdType type, uint32_t id) {
auto previous = calls_.before_begin();
auto current = calls_.begin();
while (current != calls_.end()) {
- if (channel_id == current->channel_id_locked()) {
- current->HandleChannelClose();
- current = calls_.erase_after(previous); // previous stays the same
+ if (id == (type == AbortIdType::kChannel ? current->channel_id_locked()
+ : current->service_id())) {
+ current =
+ CloseCallAndMarkForCleanup(previous, current, Status::Aborted());
} else {
previous = current;
++current;
}
}
+}
- return OkStatus();
+void Endpoint::CleanUpCalls() {
+ if (to_cleanup_.empty()) {
+ rpc_lock().unlock();
+ return;
+ }
+
+ // Drain the to_cleanup_ list. This while loop is structured to avoid
+ // unnecessarily acquiring the lock after popping the last call.
+ while (true) {
+ Call& call = to_cleanup_.front();
+ to_cleanup_.pop_front();
+
+ const bool done = to_cleanup_.empty();
+
+ call.CleanUpFromEndpoint();
+
+ if (done) {
+ return;
+ }
+
+ rpc_lock().lock();
+ }
+}
+
+void Endpoint::RemoveAllCalls() {
+ RpcLockGuard lock;
+
+ // Close all calls without invoking on_error callbacks, since the calls should
+ // have been closed before the Endpoint was deleted.
+ while (!calls_.empty()) {
+ calls_.front().CloseFromDeletedEndpoint();
+ calls_.pop_front();
+ }
+ while (!to_cleanup_.empty()) {
+ to_cleanup_.front().CloseFromDeletedEndpoint();
+ to_cleanup_.pop_front();
+ }
}
} // namespace pw::rpc::internal
diff --git a/pw_rpc/fake_channel_output.cc b/pw_rpc/fake_channel_output.cc
index 530a03a6d..6404e68d6 100644
--- a/pw_rpc/fake_channel_output.cc
+++ b/pw_rpc/fake_channel_output.cc
@@ -26,14 +26,14 @@
namespace pw::rpc::internal::test {
void FakeChannelOutput::clear() {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
payloads_.clear();
packets_.clear();
send_status_ = OkStatus();
return_after_packet_count_ = -1;
}
-Status FakeChannelOutput::HandlePacket(std::span<const std::byte> buffer) {
+Status FakeChannelOutput::HandlePacket(span<const std::byte> buffer) {
// If the buffer is empty, this is just releasing an unused buffer.
if (buffer.empty()) {
return OkStatus();
@@ -63,26 +63,23 @@ Status FakeChannelOutput::HandlePacket(std::span<const std::byte> buffer) {
CopyPayloadToBuffer(packet);
switch (packet.type()) {
- case PacketType::REQUEST:
+ case pwpb::PacketType::REQUEST:
return OkStatus();
- case PacketType::RESPONSE:
+ case pwpb::PacketType::RESPONSE:
total_response_packets_ += 1;
return OkStatus();
- case PacketType::CLIENT_STREAM:
+ case pwpb::PacketType::CLIENT_STREAM:
return OkStatus();
- case PacketType::DEPRECATED_SERVER_STREAM_END:
- PW_CRASH("Deprecated PacketType %d", static_cast<int>(packet.type()));
- case PacketType::CLIENT_ERROR:
+ case pwpb::PacketType::CLIENT_ERROR:
PW_LOG_WARN("FakeChannelOutput received client error: %s",
packet.status().str());
return OkStatus();
- case PacketType::SERVER_ERROR:
+ case pwpb::PacketType::SERVER_ERROR:
PW_LOG_WARN("FakeChannelOutput received server error: %s",
packet.status().str());
return OkStatus();
- case PacketType::DEPRECATED_CANCEL:
- case PacketType::SERVER_STREAM:
- case PacketType::CLIENT_STREAM_END:
+ case pwpb::PacketType::SERVER_STREAM:
+ case pwpb::PacketType::CLIENT_STREAM_END:
return OkStatus();
}
PW_CRASH("Unhandled PacketType %d", static_cast<int>(result.value().type()));
@@ -104,11 +101,11 @@ void FakeChannelOutput::CopyPayloadToBuffer(Packet& packet) {
const size_t start = payloads_.size();
payloads_.resize(payloads_.size() + payload.size());
std::memcpy(&payloads_[start], payload.data(), payload.size());
- packet.set_payload(std::span(&payloads_[start], payload.size()));
+ packet.set_payload(span(&payloads_[start], payload.size()));
}
void FakeChannelOutput::LogPackets() const {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
PW_LOG_INFO("%u packets have been sent through this FakeChannelOutput",
static_cast<unsigned>(packets_.size()));
diff --git a/pw_rpc/fake_channel_output_test.cc b/pw_rpc/fake_channel_output_test.cc
index 202be7e43..7d892545d 100644
--- a/pw_rpc/fake_channel_output_test.cc
+++ b/pw_rpc/fake_channel_output_test.cc
@@ -50,13 +50,13 @@ TEST(FakeChannelOutput, SendAndClear) {
constexpr MethodType type = MethodType::kServerStreaming;
TestFakeChannelOutput output;
Channel channel(kChannelId, &output);
- const internal::Packet server_stream_packet(PacketType::SERVER_STREAM,
+ const internal::Packet server_stream_packet(pwpb::PacketType::SERVER_STREAM,
kChannelId,
kServiceId,
kMethodId,
kCallId,
kPayload);
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
ASSERT_EQ(channel.Send(server_stream_packet), OkStatus());
ASSERT_EQ(output.last_response(type).size(), kPayload.size());
EXPECT_EQ(
@@ -77,13 +77,13 @@ TEST(FakeChannelOutput, SendAndFakeFutureResults) {
constexpr MethodType type = MethodType::kUnary;
TestFakeChannelOutput output;
Channel channel(kChannelId, &output);
- const internal::Packet response_packet(PacketType::RESPONSE,
+ const internal::Packet response_packet(pwpb::PacketType::RESPONSE,
kChannelId,
kServiceId,
kMethodId,
kCallId,
kPayload);
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
EXPECT_EQ(channel.Send(response_packet), OkStatus());
EXPECT_EQ(output.total_payloads(type), 1u);
EXPECT_EQ(output.total_packets(), 1u);
@@ -103,7 +103,7 @@ TEST(FakeChannelOutput, SendAndFakeFutureResults) {
EXPECT_EQ(output.total_payloads(type), 2u);
EXPECT_EQ(output.total_packets(), 2u);
- const internal::Packet server_stream_packet(PacketType::SERVER_STREAM,
+ const internal::Packet server_stream_packet(pwpb::PacketType::SERVER_STREAM,
kChannelId,
kServiceId,
kMethodId,
@@ -124,7 +124,7 @@ TEST(FakeChannelOutput, SendAndFakeSingleResult) {
constexpr MethodType type = MethodType::kUnary;
TestFakeChannelOutput output;
Channel channel(kChannelId, &output);
- const internal::Packet response_packet(PacketType::RESPONSE,
+ const internal::Packet response_packet(pwpb::PacketType::RESPONSE,
kChannelId,
kServiceId,
kMethodId,
@@ -133,7 +133,7 @@ TEST(FakeChannelOutput, SendAndFakeSingleResult) {
// Multiple calls will return the same error status.
const int packet_count_fail = 4;
output.set_send_status(Status::Unknown(), packet_count_fail);
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
for (int i = 0; i < packet_count_fail; ++i) {
EXPECT_EQ(channel.Send(response_packet), OkStatus());
@@ -158,13 +158,13 @@ TEST(FakeChannelOutput, SendAndFakeSingleResult) {
TEST(FakeChannelOutput, SendResponseUpdated) {
TestFakeChannelOutput output;
Channel channel(kChannelId, &output);
- const internal::Packet response_packet(PacketType::RESPONSE,
+ const internal::Packet response_packet(pwpb::PacketType::RESPONSE,
kChannelId,
kServiceId,
kMethodId,
kCallId,
kPayload);
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
ASSERT_EQ(channel.Send(response_packet), OkStatus());
ASSERT_EQ(output.last_response(MethodType::kUnary).size(), kPayload.size());
EXPECT_EQ(std::memcmp(output.last_response(MethodType::kUnary).data(),
@@ -176,15 +176,19 @@ TEST(FakeChannelOutput, SendResponseUpdated) {
EXPECT_TRUE(output.done());
output.clear();
- const internal::Packet packet_empty_payload(
- PacketType::RESPONSE, kChannelId, kServiceId, kMethodId, kCallId, {});
+ const internal::Packet packet_empty_payload(pwpb::PacketType::RESPONSE,
+ kChannelId,
+ kServiceId,
+ kMethodId,
+ kCallId,
+ {});
EXPECT_EQ(channel.Send(packet_empty_payload), OkStatus());
EXPECT_EQ(output.last_response(MethodType::kUnary).size(), 0u);
EXPECT_EQ(output.total_payloads(MethodType::kUnary), 1u);
EXPECT_EQ(output.total_packets(), 1u);
EXPECT_TRUE(output.done());
- const internal::Packet server_stream_packet(PacketType::SERVER_STREAM,
+ const internal::Packet server_stream_packet(pwpb::PacketType::SERVER_STREAM,
kChannelId,
kServiceId,
kMethodId,
diff --git a/pw_rpc/fuzz/BUILD.gn b/pw_rpc/fuzz/BUILD.gn
new file mode 100644
index 000000000..bec42fe44
--- /dev/null
+++ b/pw_rpc/fuzz/BUILD.gn
@@ -0,0 +1,140 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_rpc/internal/integration_test_ports.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public_include_path") {
+ include_dirs = [
+ "public",
+ "$dir_pw_rpc/public",
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("alarm_timer") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_rpc/fuzz/alarm_timer.h" ]
+ public_deps = [
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_chrono:system_timer",
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_test("alarm_timer_test") {
+ enable_if = pw_chrono_SYSTEM_TIMER_BACKEND != ""
+ sources = [ "alarm_timer_test.cc" ]
+ deps = [
+ ":alarm_timer",
+ "$dir_pw_sync:binary_semaphore",
+ ]
+}
+
+pw_source_set("argparse") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_rpc/fuzz/argparse.h" ]
+ sources = [ "argparse.cc" ]
+ public_deps = [
+ "$dir_pw_containers:vector",
+ dir_pw_status,
+ ]
+ deps = [
+ "$dir_pw_string:builder",
+ dir_pw_assert,
+ dir_pw_log,
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_test("argparse_test") {
+ sources = [ "argparse_test.cc" ]
+ deps = [ ":argparse" ]
+}
+
+pw_source_set("engine") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_rpc/fuzz/engine.h" ]
+ sources = [ "engine.cc" ]
+ public_deps = [
+ ":alarm_timer",
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_rpc:benchmark",
+ "$dir_pw_rpc:log_config",
+ "$dir_pw_rpc:protos.raw_rpc",
+ "$dir_pw_string:format",
+ "$dir_pw_sync:condition_variable",
+ "$dir_pw_sync:timed_mutex",
+ "$dir_pw_thread:thread",
+ dir_pw_random,
+ ]
+ deps = [ "$dir_pw_rpc:client" ]
+ visibility = [ ":*" ]
+}
+
+pw_test("engine_test") {
+ enable_if =
+ pw_chrono_SYSTEM_TIMER_BACKEND == "$dir_pw_chrono_stl:system_timer" &&
+ pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
+ sources = [ "engine_test.cc" ]
+ deps = [
+ ":engine",
+ "$dir_pw_rpc:client_server_testing_threaded",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread_stl:test_threads",
+ dir_pw_log,
+ pw_chrono_SYSTEM_TIMER_BACKEND,
+ ]
+}
+
+pw_executable("client_fuzzer") {
+ sources = [ "client_fuzzer.cc" ]
+ deps = [
+ ":argparse",
+ ":engine",
+ "$dir_pw_rpc:client",
+ "$dir_pw_rpc:integration_testing",
+ ]
+}
+
+pw_python_action("cpp_client_server_fuzz_test") {
+ script = "../py/pw_rpc/testing.py"
+ args = [
+ "--server",
+ "<TARGET_FILE($dir_pw_rpc:test_rpc_server)>",
+ "--client",
+ "<TARGET_FILE(:client_fuzzer)>",
+ "--",
+ "$pw_rpc_CPP_CLIENT_FUZZER_TEST_PORT",
+ ]
+ deps = [
+ ":client_fuzzer",
+ "$dir_pw_rpc:test_rpc_server",
+ ]
+
+ stamp = true
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":argparse_test",
+ ":alarm_timer_test",
+ ":engine_test",
+ ]
+}
diff --git a/pw_rpc/fuzz/alarm_timer_test.cc b/pw_rpc/fuzz/alarm_timer_test.cc
new file mode 100644
index 000000000..ce8395a6b
--- /dev/null
+++ b/pw_rpc/fuzz/alarm_timer_test.cc
@@ -0,0 +1,64 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/fuzz/alarm_timer.h"
+
+#include <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_sync/binary_semaphore.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+using namespace std::chrono_literals;
+
+TEST(AlarmTimerTest, Start) {
+ sync::BinarySemaphore sem;
+ AlarmTimer timer([&sem](chrono::SystemClock::time_point) { sem.release(); });
+ timer.Start(10ms);
+ sem.acquire();
+}
+
+TEST(AlarmTimerTest, Restart) {
+ sync::BinarySemaphore sem;
+ AlarmTimer timer([&sem](chrono::SystemClock::time_point) { sem.release(); });
+ timer.Start(50ms);
+ for (size_t i = 0; i < 10; ++i) {
+ timer.Restart();
+ EXPECT_FALSE(sem.try_acquire_for(10us));
+ }
+ sem.acquire();
+}
+
+TEST(AlarmTimerTest, Cancel) {
+ sync::BinarySemaphore sem;
+ AlarmTimer timer([&sem](chrono::SystemClock::time_point) { sem.release(); });
+ timer.Start(50ms);
+ timer.Cancel();
+ EXPECT_FALSE(sem.try_acquire_for(100us));
+}
+
+TEST(AlarmTimerTest, Destroy) {
+ sync::BinarySemaphore sem;
+ {
+ AlarmTimer timer(
+ [&sem](chrono::SystemClock::time_point) { sem.release(); });
+ timer.Start(50ms);
+ }
+ EXPECT_FALSE(sem.try_acquire_for(100us));
+}
+
+} // namespace
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/argparse.cc b/pw_rpc/fuzz/argparse.cc
new file mode 100644
index 000000000..39a5dd694
--- /dev/null
+++ b/pw_rpc/fuzz/argparse.cc
@@ -0,0 +1,259 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/fuzz/argparse.h"
+
+#include <cctype>
+#include <cstring>
+
+#include "pw_assert/check.h"
+#include "pw_log/log.h"
+#include "pw_string/string_builder.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+// Visitor to `ArgVariant` used by `ParseArgs` below.
+struct ParseVisitor {
+ std::string_view arg0;
+ std::string_view arg1;
+
+ template <typename Parser>
+ ParseStatus operator()(Parser& parser) {
+ return parser.Parse(arg0, arg1);
+ }
+};
+
+// Visitor to `ArgVariant` used by `GetArg` below.
+struct ValueVisitor {
+ std::string_view name;
+
+ template <typename Parser>
+ std::optional<ArgVariant> operator()(Parser& parser) {
+ std::optional<ArgVariant> result;
+ if (parser.short_name() == name || parser.long_name() == name) {
+ result.emplace(parser.value());
+ }
+ return result;
+ }
+};
+
+// Visitor to `ArgVariant` used by `PrintUsage` below.
+const size_t kMaxUsageLen = 256;
+struct UsageVisitor {
+ StringBuffer<kMaxUsageLen>* buffer;
+
+ void operator()(const BoolParser& parser) const {
+ auto short_name = parser.short_name();
+ auto long_name = parser.long_name();
+ *buffer << " [" << short_name << "|--[no-]" << long_name.substr(2) << "]";
+ }
+
+ template <typename T>
+ void operator()(const UnsignedParser<T>& parser) const {
+ auto short_name = parser.short_name();
+ auto long_name = parser.long_name();
+ *buffer << " ";
+ if (!parser.positional()) {
+ *buffer << "[";
+ if (!short_name.empty()) {
+ *buffer << short_name << "|";
+ }
+ *buffer << long_name << " ";
+ }
+ for (const auto& c : long_name) {
+ *buffer << static_cast<char>(toupper(c));
+ }
+ if (!parser.positional()) {
+ *buffer << "]";
+ }
+ }
+};
+
+// Visitor to `ArgVariant` used by `ResetArg` below.
+struct ResetVisitor {
+ std::string_view name;
+
+ template <typename Parser>
+ bool operator()(Parser& parser) {
+ if (parser.short_name() != name && parser.long_name() != name) {
+ return false;
+ }
+ parser.Reset();
+ return true;
+ }
+};
+
+} // namespace
+
+ArgParserBase::ArgParserBase(std::string_view name) : long_name_(name) {
+ PW_CHECK(!name.empty());
+ PW_CHECK(name != "--");
+ positional_ =
+ name[0] != '-' || (name.size() > 2 && name.substr(0, 2) != "--");
+}
+
+ArgParserBase::ArgParserBase(std::string_view shortopt,
+ std::string_view longopt)
+ : short_name_(shortopt), long_name_(longopt) {
+ PW_CHECK(shortopt.size() == 2);
+ PW_CHECK(shortopt[0] == '-');
+ PW_CHECK(shortopt != "--");
+ PW_CHECK(longopt.size() > 2);
+ PW_CHECK(longopt.substr(0, 2) == "--");
+ positional_ = false;
+}
+
+bool ArgParserBase::Match(std::string_view arg) {
+ if (arg.empty()) {
+ return false;
+ }
+ if (!positional_) {
+ return arg == short_name_ || arg == long_name_;
+ }
+ if (!std::holds_alternative<std::monostate>(value_)) {
+ return false;
+ }
+ if ((arg.size() == 2 && arg[0] == '-') ||
+ (arg.size() > 2 && arg.substr(0, 2) == "--")) {
+ PW_LOG_WARN("Argument parsed for '%s' appears to be a flag: '%s'",
+ long_name_.data(),
+ arg.data());
+ }
+ return true;
+}
+
+const ArgVariant& ArgParserBase::GetValue() const {
+ return std::holds_alternative<std::monostate>(value_) ? initial_ : value_;
+}
+
+BoolParser::BoolParser(std::string_view name) : ArgParserBase(name) {}
+BoolParser::BoolParser(std::string_view shortopt, std::string_view longopt)
+ : ArgParserBase(shortopt, longopt) {}
+
+BoolParser& BoolParser::set_default(bool value) {
+ set_initial(value);
+ return *this;
+}
+
+ParseStatus BoolParser::Parse(std::string_view arg0,
+ [[maybe_unused]] std::string_view arg1) {
+ if (Match(arg0)) {
+ set_value(true);
+ return kParsedOne;
+ }
+ if (arg0.size() > 5 && arg0.substr(0, 5) == "--no-" &&
+ arg0.substr(5) == long_name().substr(2)) {
+ set_value(false);
+ return kParsedOne;
+ }
+ return kParseMismatch;
+}
+
+UnsignedParserBase::UnsignedParserBase(std::string_view name)
+ : ArgParserBase(name) {}
+UnsignedParserBase::UnsignedParserBase(std::string_view shortopt,
+ std::string_view longopt)
+ : ArgParserBase(shortopt, longopt) {}
+
+ParseStatus UnsignedParserBase::Parse(std::string_view arg0,
+ std::string_view arg1,
+ uint64_t max) {
+ auto result = kParsedOne;
+ if (!Match(arg0)) {
+ return kParseMismatch;
+ }
+ if (!positional()) {
+ if (arg1.empty()) {
+ PW_LOG_ERROR("Missing value for flag '%s'", arg0.data());
+ return kParseFailure;
+ }
+ arg0 = arg1;
+ result = kParsedTwo;
+ }
+ char* endptr;
+ auto value = strtoull(arg0.data(), &endptr, 0);
+ if (*endptr) {
+ PW_LOG_ERROR("Failed to parse number from '%s'", arg0.data());
+ return kParseFailure;
+ }
+ if (value > max) {
+ PW_LOG_ERROR("Parsed value is too large: %llu", value);
+ return kParseFailure;
+ }
+ set_value(value);
+ return result;
+}
+
+Status ParseArgs(Vector<ArgParserVariant>& parsers, int argc, char** argv) {
+ for (int i = 1; i < argc; ++i) {
+ auto arg0 = std::string_view(argv[i]);
+ auto arg1 =
+ i == (argc - 1) ? std::string_view() : std::string_view(argv[i + 1]);
+ bool parsed = false;
+ for (auto& parser : parsers) {
+ switch (std::visit(ParseVisitor{.arg0 = arg0, .arg1 = arg1}, parser)) {
+ case kParsedOne:
+ break;
+ case kParsedTwo:
+ ++i;
+ break;
+ case kParseMismatch:
+ continue;
+ case kParseFailure:
+ PW_LOG_ERROR("Failed to parse '%s'", arg0.data());
+ return Status::InvalidArgument();
+ }
+ parsed = true;
+ break;
+ }
+ if (!parsed) {
+ PW_LOG_ERROR("Unrecognized argument: '%s'", arg0.data());
+ return Status::InvalidArgument();
+ }
+ }
+ return OkStatus();
+}
+
+void PrintUsage(const Vector<ArgParserVariant>& parsers,
+ std::string_view argv0) {
+ StringBuffer<kMaxUsageLen> buffer;
+ buffer << "usage: " << argv0;
+ for (auto& parser : parsers) {
+ std::visit(UsageVisitor{.buffer = &buffer}, parser);
+ }
+ PW_LOG_INFO("%s", buffer.c_str());
+}
+
+std::optional<ArgVariant> GetArg(const Vector<ArgParserVariant>& parsers,
+ std::string_view name) {
+ for (auto& parser : parsers) {
+ if (auto result = std::visit(ValueVisitor{.name = name}, parser);
+ result.has_value()) {
+ return result;
+ }
+ }
+ return std::optional<ArgVariant>();
+}
+
+Status ResetArg(Vector<ArgParserVariant>& parsers, std::string_view name) {
+ for (auto& parser : parsers) {
+ if (std::visit(ResetVisitor{.name = name}, parser)) {
+ return OkStatus();
+ }
+ }
+ return Status::InvalidArgument();
+}
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/argparse_test.cc b/pw_rpc/fuzz/argparse_test.cc
new file mode 100644
index 000000000..6f0ab3889
--- /dev/null
+++ b/pw_rpc/fuzz/argparse_test.cc
@@ -0,0 +1,196 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/fuzz/argparse.h"
+
+#include <cstdint>
+#include <limits>
+
+#include "gtest/gtest.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+TEST(ArgsParseTest, ParseBoolFlag) {
+ auto parser1 = BoolParser("-t", "--true").set_default(true);
+ auto parser2 = BoolParser("-f").set_default(false);
+ EXPECT_TRUE(parser1.value());
+ EXPECT_FALSE(parser2.value());
+
+ EXPECT_EQ(parser1.Parse("-t"), ParseStatus::kParsedOne);
+ EXPECT_EQ(parser2.Parse("-t"), ParseStatus::kParseMismatch);
+ EXPECT_TRUE(parser1.value());
+ EXPECT_FALSE(parser2.value());
+
+ EXPECT_EQ(parser1.Parse("--true"), ParseStatus::kParsedOne);
+ EXPECT_EQ(parser2.Parse("--true"), ParseStatus::kParseMismatch);
+ EXPECT_TRUE(parser1.value());
+ EXPECT_FALSE(parser2.value());
+
+ EXPECT_EQ(parser1.Parse("--no-true"), ParseStatus::kParsedOne);
+ EXPECT_EQ(parser2.Parse("--no-true"), ParseStatus::kParseMismatch);
+ EXPECT_FALSE(parser1.value());
+ EXPECT_FALSE(parser2.value());
+
+ EXPECT_EQ(parser1.Parse("-f"), ParseStatus::kParseMismatch);
+ EXPECT_EQ(parser2.Parse("-f"), ParseStatus::kParsedOne);
+ EXPECT_FALSE(parser1.value());
+ EXPECT_TRUE(parser2.value());
+}
+
+template <typename T>
+void ParseUnsignedFlag() {
+ auto parser = UnsignedParser<T>("-u", "--unsigned").set_default(137);
+ EXPECT_EQ(parser.value(), 137u);
+
+ // Wrong name.
+ EXPECT_EQ(parser.Parse("-s"), ParseStatus::kParseMismatch);
+ EXPECT_EQ(parser.Parse("--signed"), ParseStatus::kParseMismatch);
+ EXPECT_EQ(parser.value(), 137u);
+
+ // Missing values.
+ EXPECT_EQ(parser.Parse("-u"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.Parse("--unsigned"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), 137u);
+
+ // Non-numeric values.
+ EXPECT_EQ(parser.Parse("-u", "foo"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.Parse("--unsigned", "bar"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), 137u);
+
+ // Minimum values.
+ EXPECT_EQ(parser.Parse("-u", "0"), ParseStatus::kParsedTwo);
+ EXPECT_EQ(parser.Parse("--unsigned", "0"), ParseStatus::kParsedTwo);
+ EXPECT_EQ(parser.value(), 0u);
+
+ // Maximum values.
+ T max = std::numeric_limits<T>::max();
+ StringBuffer<32> buf;
+ buf << max;
+ EXPECT_EQ(parser.Parse("-u", buf.c_str()), ParseStatus::kParsedTwo);
+ EXPECT_EQ(parser.value(), max);
+ EXPECT_EQ(parser.Parse("--unsigned", buf.c_str()), ParseStatus::kParsedTwo);
+ EXPECT_EQ(parser.value(), max);
+
+ // Out of-range value.
+ if (max < std::numeric_limits<uint64_t>::max()) {
+ buf.clear();
+ buf << (max + 1ULL);
+ EXPECT_EQ(parser.Parse("-u", buf.c_str()), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.Parse("--unsigned", buf.c_str()),
+ ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), max);
+ }
+}
+
+TEST(ArgsParseTest, ParseUnsignedFlags) {
+ ParseUnsignedFlag<uint8_t>();
+ ParseUnsignedFlag<uint16_t>();
+ ParseUnsignedFlag<uint32_t>();
+ ParseUnsignedFlag<uint64_t>();
+}
+
+TEST(ArgsParseTest, ParsePositional) {
+ auto parser = UnsignedParser<size_t>("positional").set_default(1);
+ EXPECT_EQ(parser.Parse("-p", "2"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), 1u);
+
+ EXPECT_EQ(parser.Parse("--positional", "2"), ParseStatus::kParseFailure);
+ EXPECT_EQ(parser.value(), 1u);
+
+ // Second arg is ignored..
+ EXPECT_EQ(parser.Parse("2", "3"), ParseStatus::kParsedOne);
+ EXPECT_EQ(parser.value(), 2u);
+
+ // Positional only matches once.
+ EXPECT_EQ(parser.Parse("3"), ParseStatus::kParseMismatch);
+ EXPECT_EQ(parser.value(), 2u);
+}
+
+TEST(ArgsParseTest, PrintUsage) {
+ // Just verify it compiles and runs.
+ Vector<ArgParserVariant, 3> parsers = {
+ BoolParser("-v", "--verbose").set_default(false),
+ UnsignedParser<size_t>("-r", "--runs").set_default(1000),
+ UnsignedParser<size_t>("port").set_default(11111),
+ };
+ PrintUsage(parsers, "test-bin");
+}
+
+void CheckArgs(Vector<ArgParserVariant>& parsers,
+ bool verbose,
+ size_t runs,
+ uint16_t port) {
+ bool actual_verbose;
+ EXPECT_EQ(GetArg(parsers, "--verbose", &actual_verbose), OkStatus());
+ EXPECT_EQ(verbose, actual_verbose);
+ EXPECT_EQ(ResetArg(parsers, "--verbose"), OkStatus());
+
+ size_t actual_runs;
+ EXPECT_EQ(GetArg(parsers, "--runs", &actual_runs), OkStatus());
+ EXPECT_EQ(runs, actual_runs);
+ EXPECT_EQ(ResetArg(parsers, "--runs"), OkStatus());
+
+ uint16_t actual_port;
+ EXPECT_EQ(GetArg(parsers, "port", &actual_port), OkStatus());
+ EXPECT_EQ(port, actual_port);
+ EXPECT_EQ(ResetArg(parsers, "port"), OkStatus());
+}
+
+TEST(ArgsParseTest, ParseArgs) {
+ Vector<ArgParserVariant, 3> parsers{
+ BoolParser("-v", "--verbose").set_default(false),
+ UnsignedParser<size_t>("-r", "--runs").set_default(1000),
+ UnsignedParser<uint16_t>("port").set_default(11111),
+ };
+
+ char const* argv1[] = {"test-bin"};
+ EXPECT_EQ(ParseArgs(parsers, 1, const_cast<char**>(argv1)), OkStatus());
+ CheckArgs(parsers, false, 1000, 11111);
+
+ char const* argv2[] = {"test-bin", "22222"};
+ EXPECT_EQ(ParseArgs(parsers, 2, const_cast<char**>(argv2)), OkStatus());
+ CheckArgs(parsers, false, 1000, 22222);
+
+ // Out of range argument.
+ char const* argv3[] = {"test-bin", "65536"};
+ EXPECT_EQ(ParseArgs(parsers, 2, const_cast<char**>(argv3)),
+ Status::InvalidArgument());
+
+ // Extra argument.
+ char const* argv4[] = {"test-bin", "1", "2"};
+ EXPECT_EQ(ParseArgs(parsers, 3, const_cast<char**>(argv4)),
+ Status::InvalidArgument());
+ EXPECT_EQ(ResetArg(parsers, "port"), OkStatus());
+
+ // Flag missing value.
+ char const* argv5[] = {"test-bin", "--runs"};
+ EXPECT_EQ(ParseArgs(parsers, 2, const_cast<char**>(argv5)),
+ Status::InvalidArgument());
+
+ char const* argv6[] = {"test-bin", "-v", "33333", "--runs", "300"};
+ EXPECT_EQ(ParseArgs(parsers, 5, const_cast<char**>(argv6)), OkStatus());
+ CheckArgs(parsers, true, 300, 33333);
+
+ char const* argv7[] = {"test-bin", "-r", "400", "--verbose"};
+ EXPECT_EQ(ParseArgs(parsers, 4, const_cast<char**>(argv7)), OkStatus());
+ CheckArgs(parsers, true, 400, 11111);
+
+ char const* argv8[] = {"test-bin", "--no-verbose", "-r", "5000", "55555"};
+ EXPECT_EQ(ParseArgs(parsers, 5, const_cast<char**>(argv8)), OkStatus());
+ CheckArgs(parsers, false, 5000, 55555);
+}
+
+} // namespace
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/client_fuzzer.cc b/pw_rpc/fuzz/client_fuzzer.cc
new file mode 100644
index 000000000..c5086becf
--- /dev/null
+++ b/pw_rpc/fuzz/client_fuzzer.cc
@@ -0,0 +1,111 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// clang-format off
+#include "pw_rpc/internal/log_config.h" // PW_LOG_* macros must be first.
+// clang-format on
+
+#include <sys/socket.h>
+
+#include <cstring>
+
+#include "pw_log/log.h"
+#include "pw_rpc/fuzz/argparse.h"
+#include "pw_rpc/fuzz/engine.h"
+#include "pw_rpc/integration_testing.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+// This client configures a socket read timeout to allow the RPC dispatch thread
+// to exit gracefully.
+constexpr timeval kSocketReadTimeout = {.tv_sec = 1, .tv_usec = 0};
+
+int FuzzClient(int argc, char** argv) {
+ // TODO(aarongreen): Incorporate descriptions into usage message.
+ Vector<ArgParserVariant, 5> parsers{
+ // Enables additional logging.
+ BoolParser("-v", "--verbose").set_default(false),
+
+ // The number of actions to perform as part of the test. A value of 0 runs
+ // indefinitely.
+ UnsignedParser<size_t>("-n", "--num-actions").set_default(256),
+
+ // The seed value for the PRNG. A value of 0 generates a seed.
+ UnsignedParser<uint64_t>("-s", "--seed").set_default(0),
+
+ // The time, in milliseconds, that can elapse without triggering an error.
+ UnsignedParser<size_t>("-t", "--timeout").set_default(5000),
+
+ // The port use to connect to the `test_rpc_server`.
+ UnsignedParser<uint16_t>("port").set_default(48000)};
+
+ if (!ParseArgs(parsers, argc, argv).ok()) {
+ PrintUsage(parsers, argv[0]);
+ return 1;
+ }
+
+ bool verbose;
+ size_t num_actions;
+ uint64_t seed;
+ size_t timeout_ms;
+ uint16_t port;
+ if (!GetArg(parsers, "--verbose", &verbose).ok() ||
+ !GetArg(parsers, "--num-actions", &num_actions).ok() ||
+ !GetArg(parsers, "--seed", &seed).ok() ||
+ !GetArg(parsers, "--timeout", &timeout_ms).ok() ||
+ !GetArg(parsers, "port", &port).ok()) {
+ return 1;
+ }
+
+ if (!seed) {
+ seed = chrono::SystemClock::now().time_since_epoch().count();
+ }
+
+ if (auto status = integration_test::InitializeClient(port); !status.ok()) {
+ PW_LOG_ERROR("Failed to initialize client: %s", pw_StatusString(status));
+ return 1;
+ }
+
+ // Set read timout on socket to allow
+ // pw::rpc::integration_test::TerminateClient() to complete.
+ int fd = integration_test::GetClientSocketFd();
+ if (setsockopt(fd,
+ SOL_SOCKET,
+ SO_RCVTIMEO,
+ &kSocketReadTimeout,
+ sizeof(kSocketReadTimeout)) != 0) {
+ PW_LOG_ERROR("Failed to configure socket receive timeout with errno=%d",
+ errno);
+ return 1;
+ }
+
+ if (num_actions == 0) {
+ num_actions = std::numeric_limits<size_t>::max();
+ }
+
+ Fuzzer fuzzer(integration_test::client(), integration_test::kChannelId);
+ fuzzer.set_verbose(verbose);
+ fuzzer.set_timeout(std::chrono::milliseconds(timeout_ms));
+ fuzzer.Run(seed, num_actions);
+ integration_test::TerminateClient();
+ return 0;
+}
+
+} // namespace
+} // namespace pw::rpc::fuzz
+
+int main(int argc, char** argv) {
+ return pw::rpc::fuzz::FuzzClient(argc, argv);
+}
diff --git a/pw_rpc/fuzz/engine.cc b/pw_rpc/fuzz/engine.cc
new file mode 100644
index 000000000..b19dfa398
--- /dev/null
+++ b/pw_rpc/fuzz/engine.cc
@@ -0,0 +1,553 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// clang-format off
+#include "pw_rpc/internal/log_config.h" // PW_LOG_* macros must be first.
+
+#include "pw_rpc/fuzz/engine.h"
+// clang-format on
+
+#include <algorithm>
+#include <cctype>
+#include <chrono>
+#include <cinttypes>
+#include <limits>
+#include <mutex>
+
+#include "pw_assert/check.h"
+#include "pw_bytes/span.h"
+#include "pw_log/log.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_string/format.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+using namespace std::chrono_literals;
+
+// Maximum number of bytes written in a single unary or stream request.
+constexpr size_t kMaxWriteLen = MaxSafePayloadSize();
+static_assert(kMaxWriteLen * 0x7E <= std::numeric_limits<uint16_t>::max());
+
+struct ActiveVisitor final {
+ using result_type = bool;
+ result_type operator()(std::monostate&) { return false; }
+ result_type operator()(pw::rpc::RawUnaryReceiver& call) {
+ return call.active();
+ }
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ return call.active();
+ }
+};
+
+struct CloseClientStreamVisitor final {
+ using result_type = void;
+ result_type operator()(std::monostate&) {}
+ result_type operator()(pw::rpc::RawUnaryReceiver&) {}
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ call.CloseClientStream().IgnoreError();
+ }
+};
+
+struct WriteVisitor final {
+ using result_type = bool;
+ result_type operator()(std::monostate&) { return false; }
+ result_type operator()(pw::rpc::RawUnaryReceiver&) { return false; }
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ if (!call.active()) {
+ return false;
+ }
+ call.Write(data).IgnoreError();
+ return true;
+ }
+ ConstByteSpan data;
+};
+
+struct CancelVisitor final {
+ using result_type = void;
+ result_type operator()(std::monostate&) {}
+ result_type operator()(pw::rpc::RawUnaryReceiver& call) {
+ call.Cancel().IgnoreError();
+ }
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ call.Cancel().IgnoreError();
+ }
+};
+
+struct AbandonVisitor final {
+ using result_type = void;
+ result_type operator()(std::monostate&) {}
+ result_type operator()(pw::rpc::RawUnaryReceiver& call) { call.Abandon(); }
+ result_type operator()(pw::rpc::RawClientReaderWriter& call) {
+ call.Abandon();
+ }
+};
+
+} // namespace
+
+// `Action` methods.
+
+Action::Action(uint32_t encoded) {
+ // The first byte is used to determine the operation. The ranges used set the
+ // relative likelihood of each result, e.g. `kWait` is more likely than
+ // `kAbandon`.
+ uint32_t raw = encoded & 0xFF;
+ if (raw == 0) {
+ op = kSkip;
+ } else if (raw < 0x60) {
+ op = kWait;
+ } else if (raw < 0x80) {
+ op = kWriteUnary;
+ } else if (raw < 0xA0) {
+ op = kWriteStream;
+ } else if (raw < 0xC0) {
+ op = kCloseClientStream;
+ } else if (raw < 0xD0) {
+ op = kCancel;
+ } else if (raw < 0xE0) {
+ op = kAbandon;
+ } else if (raw < 0xF0) {
+ op = kSwap;
+ } else {
+ op = kDestroy;
+ }
+ target = ((encoded & 0xFF00) >> 8) % Fuzzer::kMaxConcurrentCalls;
+ value = encoded >> 16;
+}
+
+Action::Action(Op op_, size_t target_, uint16_t value_)
+ : op(op_), target(target_), value(value_) {}
+
+Action::Action(Op op_, size_t target_, char val, size_t len)
+ : op(op_), target(target_) {
+ PW_ASSERT(op == kWriteUnary || op == kWriteStream);
+ value = static_cast<uint16_t>(((val % 0x80) * kMaxWriteLen) +
+ (len % kMaxWriteLen));
+}
+
+char Action::DecodeWriteValue(uint16_t value) {
+ return static_cast<char>((value / kMaxWriteLen) % 0x7F);
+}
+
+size_t Action::DecodeWriteLength(uint16_t value) {
+ return value % kMaxWriteLen;
+}
+
+uint32_t Action::Encode() const {
+ uint32_t encoded = 0;
+ switch (op) {
+ case kSkip:
+ encoded = 0x00;
+ break;
+ case kWait:
+ encoded = 0x5F;
+ break;
+ case kWriteUnary:
+ encoded = 0x7F;
+ break;
+ case kWriteStream:
+ encoded = 0x9F;
+ break;
+ case kCloseClientStream:
+ encoded = 0xBF;
+ break;
+ case kCancel:
+ encoded = 0xCF;
+ break;
+ case kAbandon:
+ encoded = 0xDF;
+ break;
+ case kSwap:
+ encoded = 0xEF;
+ break;
+ case kDestroy:
+ encoded = 0xFF;
+ break;
+ }
+ encoded |=
+ ((target < Fuzzer::kMaxConcurrentCalls ? target
+ : Fuzzer::kMaxConcurrentCalls) %
+ 0xFF)
+ << 8;
+ encoded |= (static_cast<uint32_t>(value) << 16);
+ return encoded;
+}
+
+void Action::Log(bool verbose, size_t num_actions, const char* fmt, ...) const {
+ if (!verbose) {
+ return;
+ }
+ char s1[16];
+ auto result = callback_id < Fuzzer::kMaxConcurrentCalls
+ ? string::Format(s1, "%-3zu", callback_id)
+ : string::Format(s1, "n/a");
+ va_list ap;
+ va_start(ap, fmt);
+ char s2[128];
+ if (result.ok()) {
+ result = string::FormatVaList(s2, fmt, ap);
+ }
+ va_end(ap);
+ if (result.ok()) {
+ PW_LOG_INFO("#%-12zu\tthread: %zu\tcallback for: %s\ttarget call: %zu\t%s",
+ num_actions,
+ thread_id,
+ s1,
+ target,
+ s2);
+ } else {
+ LogFailure(verbose, num_actions, result.status());
+ }
+}
+
+void Action::LogFailure(bool verbose, size_t num_actions, Status status) const {
+ if (verbose && !status.ok()) {
+ PW_LOG_INFO("#%-12zu\tthread: %zu\tFailed to log action: %s",
+ num_actions,
+ thread_id,
+ pw_StatusString(status));
+ }
+}
+
+// FuzzyCall methods.
+
+void FuzzyCall::RecordWrite(size_t num, bool append) {
+ std::lock_guard lock(mutex_);
+ if (append) {
+ last_write_ += num;
+ } else {
+ last_write_ = num;
+ }
+ total_written_ += num;
+ pending_ = true;
+}
+
+void FuzzyCall::Await() {
+ std::unique_lock<sync::Mutex> lock(mutex_);
+ cv_.wait(lock, [this]() PW_NO_LOCK_SAFETY_ANALYSIS { return !pending_; });
+}
+
+void FuzzyCall::Notify() {
+ if (pending_.exchange(false)) {
+ cv_.notify_all();
+ }
+}
+
+void FuzzyCall::Swap(FuzzyCall& other) {
+ if (index_ == other.index_) {
+ return;
+ }
+ // Manually acquire locks in an order based on call IDs to prevent deadlock.
+ if (index_ < other.index_) {
+ mutex_.lock();
+ other.mutex_.lock();
+ } else {
+ other.mutex_.lock();
+ mutex_.lock();
+ }
+ call_.swap(other.call_);
+ std::swap(id_, other.id_);
+ pending_ = other.pending_.exchange(pending_);
+ std::swap(last_write_, other.last_write_);
+ std::swap(total_written_, other.total_written_);
+ mutex_.unlock();
+ other.mutex_.unlock();
+ cv_.notify_all();
+ other.cv_.notify_all();
+}
+
+void FuzzyCall::Reset(Variant call) {
+ {
+ std::lock_guard lock(mutex_);
+ call_ = std::move(call);
+ }
+ cv_.notify_all();
+}
+
+void FuzzyCall::Log() {
+ if (mutex_.try_lock_for(100ms)) {
+ PW_LOG_INFO("call %zu:", index_);
+ PW_LOG_INFO(" active: %s",
+ std::visit(ActiveVisitor(), call_) ? "true" : "false");
+ PW_LOG_INFO(" request pending: %s ", pending_ ? "true" : "false");
+ PW_LOG_INFO(" last write: %zu bytes", last_write_);
+ PW_LOG_INFO(" total written: %zu bytes", total_written_);
+ mutex_.unlock();
+ } else {
+ PW_LOG_WARN("call %zu: failed to acquire lock", index_);
+ }
+}
+
+// `Fuzzer` methods.
+
+#define FUZZ_LOG_VERBOSE(...) \
+ if (verbose_) { \
+ PW_LOG_INFO(__VA_ARGS__); \
+ }
+
+Fuzzer::Fuzzer(Client& client, uint32_t channel_id)
+ : client_(client, channel_id),
+ timer_([this](chrono::SystemClock::time_point) {
+ PW_LOG_ERROR(
+ "Workers performed %zu actions before timing out without an "
+ "update.",
+ num_actions_.load());
+ PW_LOG_INFO("Additional call details:");
+ for (auto& call : fuzzy_calls_) {
+ call.Log();
+ }
+ PW_CRASH("Fuzzer found a fatal error condition: TIMEOUT.");
+ }) {
+ for (size_t index = 0; index < kMaxConcurrentCalls; ++index) {
+ fuzzy_calls_.emplace_back(index);
+ indices_.push_back(index);
+ contexts_.push_back(CallbackContext{.id = index, .fuzzer = this});
+ }
+}
+
+void Fuzzer::Run(uint64_t seed, size_t num_actions) {
+ FUZZ_LOG_VERBOSE("Fuzzing RPC client with:");
+ FUZZ_LOG_VERBOSE(" num_actions: %zu", num_actions);
+ FUZZ_LOG_VERBOSE(" seed: %" PRIu64, seed);
+ num_actions_.store(0);
+ random::XorShiftStarRng64 rng(seed);
+ while (true) {
+ {
+ size_t actions_done = num_actions_.load();
+ if (actions_done >= num_actions) {
+ FUZZ_LOG_VERBOSE("Fuzzing complete; %zu actions performed.",
+ actions_done);
+ break;
+ }
+ FUZZ_LOG_VERBOSE("%zu actions remaining.", num_actions - actions_done);
+ }
+ FUZZ_LOG_VERBOSE("Generating %zu random actions.", kMaxActions);
+ pw::Vector<uint32_t, kMaxActions> actions;
+ for (size_t i = 0; i < kNumThreads; ++i) {
+ size_t num_actions_for_thread;
+ rng.GetInt(num_actions_for_thread, kMaxActionsPerThread + 1);
+ for (size_t j = 0; j < num_actions_for_thread; ++j) {
+ uint32_t encoded = 0;
+ while (!encoded) {
+ rng.GetInt(encoded);
+ }
+ actions.push_back(encoded);
+ }
+ actions.push_back(0);
+ }
+ Run(actions);
+ }
+}
+
+void Fuzzer::Run(const pw::Vector<uint32_t>& actions) {
+ FUZZ_LOG_VERBOSE("Starting %zu threads to perform %zu actions:",
+ kNumThreads - 1,
+ actions.size());
+ FUZZ_LOG_VERBOSE(" timeout: %lldms", timer_.timeout() / 1ms);
+ auto iter = actions.begin();
+ timer_.Restart();
+ for (size_t thread_id = 0; thread_id < kNumThreads; ++thread_id) {
+ pw::Vector<uint32_t, kMaxActionsPerThread> thread_actions;
+ while (thread_actions.size() < kMaxActionsPerThread &&
+ iter != actions.end()) {
+ uint32_t encoded = *iter++;
+ if (!encoded) {
+ break;
+ }
+ thread_actions.push_back(encoded);
+ }
+ if (thread_id == 0) {
+ std::lock_guard lock(mutex_);
+ callback_actions_ = std::move(thread_actions);
+ callback_iterator_ = callback_actions_.begin();
+ } else {
+ threads_.emplace_back(
+ [this, thread_id, actions = std::move(thread_actions)]() {
+ for (const auto& encoded : actions) {
+ Action action(encoded);
+ action.set_thread_id(thread_id);
+ Perform(action);
+ }
+ });
+ }
+ }
+ for (auto& t : threads_) {
+ t.join();
+ }
+ for (auto& fuzzy_call : fuzzy_calls_) {
+ fuzzy_call.Reset();
+ }
+ timer_.Cancel();
+}
+
+void Fuzzer::Perform(const Action& action) {
+ FuzzyCall& fuzzy_call = FindCall(action.target);
+ switch (action.op) {
+ case Action::kSkip: {
+ if (action.thread_id == 0) {
+ action.Log(verbose_, ++num_actions_, "Callback chain completed");
+ }
+ break;
+ }
+ case Action::kWait: {
+ if (action.callback_id == action.target) {
+ // Don't wait in a callback of the target call.
+ break;
+ }
+ if (fuzzy_call.pending()) {
+ action.Log(verbose_, ++num_actions_, "Waiting for call.");
+ fuzzy_call.Await();
+ }
+ break;
+ }
+ case Action::kWriteUnary:
+ case Action::kWriteStream: {
+ if (action.callback_id == action.target) {
+ // Don't create a new call from the call's own callback.
+ break;
+ }
+ char buf[kMaxWriteLen];
+ char val = Action::DecodeWriteValue(action.value);
+ size_t len = Action::DecodeWriteLength(action.value);
+ memset(buf, val, len);
+ if (verbose_) {
+ char msg_buf[64];
+ span msg(msg_buf);
+ auto result = string::Format(
+ msg,
+ "Writing %s request of ",
+ action.op == Action::kWriteUnary ? "unary" : "stream");
+ if (result.ok()) {
+ size_t off = result.size();
+ result = string::Format(
+ msg.subspan(off),
+ isprint(val) ? "['%c'; %zu]." : "['\\x%02x'; %zu].",
+ val,
+ len);
+ }
+ size_t num_actions = ++num_actions_;
+ if (result.ok()) {
+ action.Log(verbose_, num_actions, "%s", msg.data());
+ } else if (verbose_) {
+ action.LogFailure(verbose_, num_actions, result.status());
+ }
+ }
+ bool append = false;
+ if (action.op == Action::kWriteUnary) {
+ // Send a unary request.
+ fuzzy_call.Reset(client_.UnaryEcho(
+ as_bytes(span(buf, len)),
+ /* on completed */
+ [context = GetContext(action.target)](ConstByteSpan, Status) {
+ context->fuzzer->OnCompleted(context->id);
+ },
+ /* on error */
+ [context = GetContext(action.target)](Status status) {
+ context->fuzzer->OnError(context->id, status);
+ }));
+
+ } else if (fuzzy_call.Visit(
+ WriteVisitor{.data = as_bytes(span(buf, len))})) {
+ // Append to an existing stream
+ append = true;
+ } else {
+ // .Open a new stream.
+ fuzzy_call.Reset(client_.BidirectionalEcho(
+ /* on next */
+ [context = GetContext(action.target)](ConstByteSpan) {
+ context->fuzzer->OnNext(context->id);
+ },
+ /* on completed */
+ [context = GetContext(action.target)](Status) {
+ context->fuzzer->OnCompleted(context->id);
+ },
+ /* on error */
+ [context = GetContext(action.target)](Status status) {
+ context->fuzzer->OnError(context->id, status);
+ }));
+ }
+ fuzzy_call.RecordWrite(len, append);
+ break;
+ }
+ case Action::kCloseClientStream:
+ action.Log(verbose_, ++num_actions_, "Closing stream.");
+ fuzzy_call.Visit(CloseClientStreamVisitor());
+ break;
+ case Action::kCancel:
+ action.Log(verbose_, ++num_actions_, "Canceling call.");
+ fuzzy_call.Visit(CancelVisitor());
+ break;
+ case Action::kAbandon: {
+ action.Log(verbose_, ++num_actions_, "Abandoning call.");
+ fuzzy_call.Visit(AbandonVisitor());
+ break;
+ }
+ case Action::kSwap: {
+ size_t other_target = action.value % kMaxConcurrentCalls;
+ if (action.callback_id == action.target ||
+ action.callback_id == other_target) {
+ // Don't move a call from within its own callback.
+ break;
+ }
+ action.Log(verbose_,
+ ++num_actions_,
+ "Swapping call with call %zu.",
+ other_target);
+ std::lock_guard lock(mutex_);
+ FuzzyCall& other = FindCallLocked(other_target);
+ std::swap(indices_[fuzzy_call.id()], indices_[other.id()]);
+ fuzzy_call.Swap(other);
+ break;
+ }
+ case Action::kDestroy: {
+ if (action.callback_id == action.target) {
+ // Don't destroy a call from within its own callback.
+ break;
+ }
+ action.Log(verbose_, ++num_actions_, "Destroying call.");
+ fuzzy_call.Reset();
+ break;
+ }
+ default:
+ break;
+ }
+ timer_.Restart();
+}
+
+void Fuzzer::OnNext(size_t callback_id) { FindCall(callback_id).Notify(); }
+
+void Fuzzer::OnCompleted(size_t callback_id) {
+ uint32_t encoded = 0;
+ {
+ std::lock_guard lock(mutex_);
+ if (callback_iterator_ != callback_actions_.end()) {
+ encoded = *callback_iterator_++;
+ }
+ }
+ Action action(encoded);
+ action.set_callback_id(callback_id);
+ Perform(action);
+ FindCall(callback_id).Notify();
+}
+
+void Fuzzer::OnError(size_t callback_id, Status status) {
+ FuzzyCall& call = FindCall(callback_id);
+ PW_LOG_WARN("Call %zu received an error from the server: %s",
+ call.id(),
+ pw_StatusString(status));
+ call.Notify();
+}
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/engine_test.cc b/pw_rpc/fuzz/engine_test.cc
new file mode 100644
index 000000000..5ac1a49a1
--- /dev/null
+++ b/pw_rpc/fuzz/engine_test.cc
@@ -0,0 +1,264 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/fuzz/engine.h"
+
+#include <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_containers/vector.h"
+#include "pw_log/log.h"
+#include "pw_rpc/benchmark.h"
+#include "pw_rpc/internal/client_server_testing_threaded.h"
+#include "pw_rpc/internal/fake_channel_output.h"
+#include "pw_thread/test_threads.h"
+
+namespace pw::rpc::fuzz {
+namespace {
+
+using namespace std::literals::chrono_literals;
+
+// Maximum time, in milliseconds, that can elapse without a call completing or
+// being dropped in some way..
+const chrono::SystemClock::duration kTimeout = 5s;
+
+// These are fairly tight constraints in order to fit within the default
+// `PW_UNIT_TEST_CONFIG_MEMORY_POOL_SIZE`.
+constexpr size_t kMaxPackets = 128;
+constexpr size_t kMaxPayloadSize = 64;
+
+using BufferedChannelOutputBase =
+ internal::test::FakeChannelOutputBuffer<kMaxPackets, kMaxPayloadSize>;
+
+/// Channel output backed by a fixed buffer.
+class BufferedChannelOutput : public BufferedChannelOutputBase {
+ public:
+ BufferedChannelOutput() : BufferedChannelOutputBase() {}
+};
+
+using FuzzerChannelOutputBase =
+ internal::WatchableChannelOutput<BufferedChannelOutput,
+ kMaxPayloadSize,
+ kMaxPackets,
+ kMaxPayloadSize>;
+
+/// Channel output that can be waited on by the server.
+class FuzzerChannelOutput : public FuzzerChannelOutputBase {
+ public:
+ FuzzerChannelOutput() : FuzzerChannelOutputBase() {}
+};
+
+using FuzzerContextBase =
+ internal::ClientServerTestContextThreaded<FuzzerChannelOutput,
+ kMaxPayloadSize,
+ kMaxPackets,
+ kMaxPayloadSize>;
+class FuzzerContext : public FuzzerContextBase {
+ public:
+ static constexpr uint32_t kChannelId = 1;
+
+ FuzzerContext() : FuzzerContextBase(thread::test::TestOptionsThread0()) {}
+};
+
+class RpcFuzzTestingTest : public testing::Test {
+ protected:
+ void SetUp() override { context_.server().RegisterService(service_); }
+
+ void Add(Action::Op op, size_t target, uint16_t value) {
+ actions_.push_back(Action(op, target, value).Encode());
+ }
+
+ void Add(Action::Op op, size_t target, char val, size_t len) {
+ actions_.push_back(Action(op, target, val, len).Encode());
+ }
+
+ void NextThread() { actions_.push_back(0); }
+
+ void Run() {
+ Fuzzer fuzzer(context_.client(), FuzzerContext::kChannelId);
+ fuzzer.set_verbose(true);
+ fuzzer.set_timeout(kTimeout);
+ fuzzer.Run(actions_);
+ }
+
+ private:
+ FuzzerContext context_;
+ BenchmarkService service_;
+ Vector<uint32_t, Fuzzer::kMaxActions> actions_;
+};
+
+TEST_F(RpcFuzzTestingTest, SequentialRequests) {
+ // Callback thread
+ Add(Action::kWriteStream, 1, 'B', 1);
+ Add(Action::kSkip, 0, 0);
+ Add(Action::kWriteStream, 2, 'B', 2);
+ Add(Action::kSkip, 0, 0);
+ Add(Action::kWriteStream, 3, 'B', 3);
+ Add(Action::kSkip, 0, 0);
+ NextThread();
+
+ // Thread 1
+ Add(Action::kWriteStream, 0, 'A', 2);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWriteStream, 1, 'A', 4);
+ NextThread();
+
+ // Thread 2
+ NextThread();
+ Add(Action::kWait, 2, 0);
+ Add(Action::kWriteStream, 2, 'A', 6);
+
+ // Thread 3
+ NextThread();
+ Add(Action::kWait, 3, 0);
+
+ Run();
+}
+
+// TODO(b/274437709): Re-enable.
+TEST_F(RpcFuzzTestingTest, DISABLED_SimultaneousRequests) {
+ // Callback thread
+ NextThread();
+
+ // Thread 1
+ Add(Action::kWriteUnary, 1, 'A', 1);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+
+ // Thread 2
+ Add(Action::kWriteUnary, 2, 'B', 2);
+ Add(Action::kWait, 3, 0);
+ NextThread();
+
+ // Thread 3
+ Add(Action::kWriteUnary, 3, 'C', 3);
+ Add(Action::kWait, 1, 0);
+ NextThread();
+
+ Run();
+}
+
+// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// channel. It will be re-enabled when the underlying stream is swapped for
+// a pw_ring_buffer-based approach.
+TEST_F(RpcFuzzTestingTest, DISABLED_CanceledRequests) {
+ // Callback thread
+ NextThread();
+
+ // Thread 1
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kWriteUnary, i % 3, 'A', i);
+ }
+ Add(Action::kWait, 0, 0);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+
+ // Thread 2
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kCancel, i % 3, 0);
+ }
+ NextThread();
+
+ // Thread 3
+ NextThread();
+
+ Run();
+}
+
+// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// channel. It will be re-enabled when the underlying stream is swapped for
+// a pw_ring_buffer-based approach.
+TEST_F(RpcFuzzTestingTest, DISABLED_AbandonedRequests) {
+ // Callback thread
+ NextThread();
+
+ // Thread 1
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kWriteUnary, i % 3, 'A', i);
+ }
+ Add(Action::kWait, 0, 0);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+
+ // Thread 2
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kAbandon, i % 3, 0);
+ }
+ NextThread();
+
+ // Thread 3
+ NextThread();
+
+ Run();
+}
+
+// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// channel. It will be re-enabled when the underlying stream is swapped for
+// a pw_ring_buffer-based approach.
+TEST_F(RpcFuzzTestingTest, DISABLED_SwappedRequests) {
+ Vector<uint32_t, Fuzzer::kMaxActions> actions;
+ // Callback thread
+ NextThread();
+ // Thread 1
+ for (size_t i = 0; i < 10; ++i) {
+ Add(Action::kWriteUnary, i % 3, 'A', i);
+ }
+ Add(Action::kWait, 0, 0);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+ // Thread 2
+ for (size_t i = 0; i < 100; ++i) {
+ auto j = i % 3;
+ Add(Action::kSwap, j, j + 1);
+ }
+ NextThread();
+ // Thread 3
+ NextThread();
+
+ Run();
+}
+
+// TODO(b/274437709) This test currently does not pass as it exhausts the fake
+// channel. It will be re-enabled when the underlying stream is swapped for
+// a pw_ring_buffer-based approach.
+TEST_F(RpcFuzzTestingTest, DISABLED_DestroyedRequests) {
+ // Callback thread
+ NextThread();
+
+ // Thread 1
+ for (size_t i = 0; i < 100; ++i) {
+ Add(Action::kWriteUnary, i % 3, 'A', i);
+ }
+ Add(Action::kWait, 0, 0);
+ Add(Action::kWait, 1, 0);
+ Add(Action::kWait, 2, 0);
+ NextThread();
+
+ // Thread 2
+ for (size_t i = 0; i < 100; ++i) {
+ Add(Action::kDestroy, i % 3, 0);
+ }
+ NextThread();
+
+ // Thread 3
+ NextThread();
+
+ Run();
+}
+
+} // namespace
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h
new file mode 100644
index 000000000..9ccd7ce0b
--- /dev/null
+++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/alarm_timer.h
@@ -0,0 +1,56 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_chrono/system_clock.h"
+#include "pw_chrono/system_timer.h"
+
+namespace pw::rpc::fuzz {
+
+/// Represents a timer that invokes a callback on timeout. Once started, it will
+/// invoke the callback after a provided duration unless it is restarted,
+/// canceled, or destroyed.
+class AlarmTimer {
+ public:
+ AlarmTimer(chrono::SystemTimer::ExpiryCallback&& on_timeout)
+ : timer_(std::move(on_timeout)) {}
+
+ chrono::SystemClock::duration timeout() const { return timeout_; }
+
+ /// "Arms" the timer. The callback will be invoked if `timeout` elapses
+ /// without a call to `Restart`, `Cancel`, or the destructor. Calling `Start`
+ /// again restarts the timer, possibly with a different `timeout` value.
+ void Start(chrono::SystemClock::duration timeout) {
+ timeout_ = timeout;
+ Restart();
+ }
+
+ /// Restarts the timer. This is equivalent to calling `Start` with the same
+ /// `timeout` as passed previously. Does nothing if `Start` has not been
+ /// called.
+ void Restart() {
+ Cancel();
+ timer_.InvokeAfter(timeout_);
+ }
+
+ /// "Disarms" the timer. The callback will not be invoked unless `Start` is
+ /// called again. Does nothing if `Start` has not been called.
+ void Cancel() { timer_.Cancel(); }
+
+ private:
+ chrono::SystemTimer timer_;
+ chrono::SystemClock::duration timeout_;
+};
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
new file mode 100644
index 000000000..05a7633e6
--- /dev/null
+++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/argparse.h
@@ -0,0 +1,230 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+/// Command line argument parsing.
+///
+/// The objects defined below can be used to parse command line arguments of
+/// different types. These objects are "just enough" defined for current use
+/// cases, but the design is intended to be extensible as new types and traits
+/// are needed.
+///
+/// Example:
+///
+/// Given a boolean flag "verbose", a numerical flag "runs", and a positional
+/// "port" argument to be parsed, we can create a vector of parsers. In this
+/// example, we modify the parsers during creation to set default values:
+///
+/// @code
+/// Vector<ArgParserVariant, 3> parsers = {
+/// BoolParser("-v", "--verbose").set_default(false),
+/// UnsignedParser<size_t>("-r", "--runs").set_default(1000),
+/// UnsignedParser<uint16_t>("port").set_default(11111),
+/// };
+/// @endcode
+///
+/// With this vector, we can then parse command line arguments and extract
+/// the values of arguments that were set, e.g.:
+///
+/// @code
+/// if (!ParseArgs(parsers, argc, argv).ok()) {
+/// PrintUsage(parsers, argv[0]);
+/// return 1;
+/// }
+/// bool verbose;
+/// size_t runs;
+/// uint16_t port;
+/// if (!GetArg(parsers, "--verbose", &verbose).ok() ||
+/// !GetArg(parsers, "--runs", &runs).ok() ||
+/// !GetArg(parsers, "port", &port).ok()) {
+/// // Shouldn't happen unless names do not match.
+/// return 1;
+/// }
+///
+/// // Do stuff with `verbose`, `runs`, and `port`...
+/// @endcode
+
+#include <cstddef>
+#include <cstdint>
+#include <string_view>
+#include <variant>
+
+#include "pw_containers/vector.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc::fuzz {
+
+/// Enumerates the results of trying to parse a specific command line argument
+/// with a particular parsers.
+enum ParseStatus {
+ /// The argument matched the parser and was successfully parsed without a
+ /// value.
+ kParsedOne,
+
+ /// The argument matched the parser and was successfully parsed with a value.
+ kParsedTwo,
+
+ /// The argument did not match the parser. This is not necessarily an error;
+ /// the argument may match a different parser.
+ kParseMismatch,
+
+ /// The argument matched a parser, but could not be parsed. This may be due to
+ /// a missing value for a flag, a value of the wrong type, a provided value
+ /// being out of range, etc. Parsers should log additional details before
+ /// returning this value.
+ kParseFailure,
+};
+
+/// Holds parsed argument values of different types.
+using ArgVariant = std::variant<std::monostate, bool, uint64_t>;
+
+/// Base class for argument parsers.
+class ArgParserBase {
+ public:
+ virtual ~ArgParserBase() = default;
+
+ std::string_view short_name() const { return short_name_; }
+ std::string_view long_name() const { return long_name_; }
+ bool positional() const { return positional_; }
+
+ /// Clears the value. Typically, command line arguments are only parsed once,
+ /// but this method is useful for testing.
+ void Reset() { value_ = std::monostate(); }
+
+ protected:
+ /// Defines an argument parser with a single name. This may be a positional
+ /// argument or a flag.
+ ArgParserBase(std::string_view name);
+
+ /// Defines an argument parser for a flag with short and long names.
+ ArgParserBase(std::string_view shortopt, std::string_view longopt);
+
+ void set_initial(ArgVariant initial) { initial_ = initial; }
+ void set_value(ArgVariant value) { value_ = value; }
+
+ /// Examines if the given `arg` matches this parser. A parser for a flag can
+ /// match the short name (e.g. '-f') if set, or the long name (e.g. '--foo').
+ /// A parser for a positional argument will match anything until it has a
+ /// value set.
+ bool Match(std::string_view arg);
+
+ /// Returns the parsed value.
+ template <typename T>
+ T Get() const {
+ return std::get<T>(GetValue());
+ }
+
+ private:
+ const ArgVariant& GetValue() const;
+
+ std::string_view short_name_;
+ std::string_view long_name_;
+ bool positional_;
+
+ ArgVariant initial_;
+ ArgVariant value_;
+};
+
+// Argument parsers for boolean arguments. These arguments are always flags, and
+// can be specified as, e.g. "-f" (true), "--foo" (true) or "--no-foo" (false).
+class BoolParser : public ArgParserBase {
+ public:
+ BoolParser(std::string_view optname);
+ BoolParser(std::string_view shortopt, std::string_view longopt);
+
+ bool value() const { return Get<bool>(); }
+ BoolParser& set_default(bool value);
+
+ ParseStatus Parse(std::string_view arg0,
+ std::string_view arg1 = std::string_view());
+};
+
+// Type-erasing argument parser for unsigned integer arguments. This object
+// always parses values as `uint64_t`s and should not be used directly.
+// Instead, use `UnsignedParser<T>` with a type to explicitly narrow to.
+class UnsignedParserBase : public ArgParserBase {
+ protected:
+ UnsignedParserBase(std::string_view name);
+ UnsignedParserBase(std::string_view shortopt, std::string_view longopt);
+
+ ParseStatus Parse(std::string_view arg0, std::string_view arg1, uint64_t max);
+};
+
+// Argument parser for unsigned integer arguments. These arguments may be flags
+// or positional arguments.
+template <typename T, typename std::enable_if_t<std::is_unsigned_v<T>, int> = 0>
+class UnsignedParser : public UnsignedParserBase {
+ public:
+ UnsignedParser(std::string_view name) : UnsignedParserBase(name) {}
+ UnsignedParser(std::string_view shortopt, std::string_view longopt)
+ : UnsignedParserBase(shortopt, longopt) {}
+
+ T value() const { return static_cast<T>(Get<uint64_t>()); }
+
+ UnsignedParser& set_default(T value) {
+ set_initial(static_cast<uint64_t>(value));
+ return *this;
+ }
+
+ ParseStatus Parse(std::string_view arg0,
+ std::string_view arg1 = std::string_view()) {
+ return UnsignedParserBase::Parse(arg0, arg1, std::numeric_limits<T>::max());
+ }
+};
+
+// Holds argument parsers of different types.
+using ArgParserVariant =
+ std::variant<BoolParser, UnsignedParser<uint16_t>, UnsignedParser<size_t>>;
+
+// Parses the command line arguments and sets the values of the given `parsers`.
+Status ParseArgs(Vector<ArgParserVariant>& parsers, int argc, char** argv);
+
+// Logs a usage message based on the given `parsers` and the program name given
+// by `argv0`.
+void PrintUsage(const Vector<ArgParserVariant>& parsers,
+ std::string_view argv0);
+
+// Attempts to find the parser in `parsers` with the given `name`, and returns
+// its value if found.
+std::optional<ArgVariant> GetArg(const Vector<ArgParserVariant>& parsers,
+ std::string_view name);
+
+inline void GetArgValue(const ArgVariant& arg, bool* out) {
+ *out = std::get<bool>(arg);
+}
+
+template <typename T, typename std::enable_if_t<std::is_unsigned_v<T>, int> = 0>
+void GetArgValue(const ArgVariant& arg, T* out) {
+ *out = static_cast<T>(std::get<uint64_t>(arg));
+}
+
+// Like `GetArgVariant` above, but extracts the typed value from the variant
+// into `out`. Returns an error if no parser exists in `parsers` with the given
+// `name`.
+template <typename T>
+Status GetArg(const Vector<ArgParserVariant>& parsers,
+ std::string_view name,
+ T* out) {
+ const auto& arg = GetArg(parsers, name);
+ if (!arg.has_value()) {
+ return Status::InvalidArgument();
+ }
+ GetArgValue(*arg, out);
+ return OkStatus();
+}
+
+// Resets the parser with the given name. Returns an error if not found.
+Status ResetArg(Vector<ArgParserVariant>& parsers, std::string_view name);
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h b/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h
new file mode 100644
index 000000000..34e92c003
--- /dev/null
+++ b/pw_rpc/fuzz/public/pw_rpc/fuzz/engine.h
@@ -0,0 +1,339 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <atomic>
+#include <cstdarg>
+#include <cstddef>
+#include <cstdint>
+#include <thread>
+#include <variant>
+
+#include "pw_containers/vector.h"
+#include "pw_random/xor_shift.h"
+#include "pw_rpc/benchmark.h"
+#include "pw_rpc/benchmark.raw_rpc.pb.h"
+#include "pw_rpc/fuzz/alarm_timer.h"
+#include "pw_sync/condition_variable.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+#include "pw_sync/timed_mutex.h"
+
+namespace pw::rpc::fuzz {
+
+/// Describes an action a fuzzing thread can perform on a call.
+struct Action {
+ enum Op : uint8_t {
+ /// No-op.
+ kSkip,
+
+ /// Waits for the call indicated by `target` to complete.
+ kWait,
+
+ /// Makes a new unary request using the call indicated by `target`. The data
+ /// written is derived from `value`.
+ kWriteUnary,
+
+ /// Writes to a stream request using the call indicated by `target`, or
+ /// makes
+ /// a new one if not currently a stream call. The data written is derived
+ /// from `value`.
+ kWriteStream,
+
+ /// Closes the stream if the call indicated by `target` is a stream call.
+ kCloseClientStream,
+
+ /// Cancels the call indicated by `target`.
+ kCancel,
+
+ /// Abandons the call indicated by `target`.
+ kAbandon,
+
+ /// Swaps the call indicated by `target` with a call indicated by `value`.
+ kSwap,
+
+ /// Sets the call indicated by `target` to an initial, unset state.
+ kDestroy,
+ };
+
+ constexpr Action() = default;
+ Action(uint32_t encoded);
+ Action(Op op, size_t target, uint16_t value);
+ Action(Op op, size_t target, char val, size_t len);
+ ~Action() = default;
+
+ void set_thread_id(size_t thread_id_) {
+ thread_id = thread_id_;
+ callback_id = std::numeric_limits<size_t>::max();
+ }
+
+ void set_callback_id(size_t callback_id_) {
+ thread_id = 0;
+ callback_id = callback_id_;
+ }
+
+ // For a write action's value, returns the character value to be written.
+ static char DecodeWriteValue(uint16_t value);
+
+ // For a write action's value, returns the number of characters to be written.
+ static size_t DecodeWriteLength(uint16_t value);
+
+ /// Returns a value that represents the fields of an action. Constructing an
+ /// `Action` with this value will produce the same fields.
+ uint32_t Encode() const;
+
+ /// Records details of the action being performed if verbose logging is
+ /// enabled.
+ void Log(bool verbose, size_t num_actions, const char* fmt, ...) const;
+
+ /// Records an encountered when trying to log an action.
+ void LogFailure(bool verbose, size_t num_actions, Status status) const;
+
+ Op op = kSkip;
+ size_t target = 0;
+ uint16_t value = 0;
+
+ size_t thread_id = 0;
+ size_t callback_id = std::numeric_limits<size_t>::max();
+};
+
+/// Wraps an RPC call that may be either a `RawUnaryReceiver` or
+/// `RawClientReaderWriter`. Allows applying `Action`s to each possible
+/// type of call.
+class FuzzyCall {
+ public:
+ using Variant =
+ std::variant<std::monostate, RawUnaryReceiver, RawClientReaderWriter>;
+
+ explicit FuzzyCall(size_t index) : index_(index), id_(index) {}
+ ~FuzzyCall() = default;
+
+ size_t id() {
+ std::lock_guard lock(mutex_);
+ return id_;
+ }
+
+ bool pending() {
+ std::lock_guard lock(mutex_);
+ return pending_;
+ }
+
+ /// Applies the given visitor to the call variant. If the action taken by the
+ /// visitor is expected to complete the call, it will notify any threads
+ /// waiting for the call to complete. This version of the method does not
+ /// return the result of the visiting the variant.
+ template <typename Visitor,
+ typename std::enable_if_t<
+ std::is_same_v<typename Visitor::result_type, void>,
+ int> = 0>
+ typename Visitor::result_type Visit(Visitor visitor, bool completes = true) {
+ {
+ std::lock_guard lock(mutex_);
+ std::visit(std::move(visitor), call_);
+ }
+ if (completes && pending_.exchange(false)) {
+ cv_.notify_all();
+ }
+ }
+
+ /// Applies the given visitor to the call variant. If the action taken by the
+ /// visitor is expected to complete the call, it will notify any threads
+ /// waiting for the call to complete. This version of the method returns the
+ /// result of the visiting the variant.
+ template <typename Visitor,
+ typename std::enable_if_t<
+ !std::is_same_v<typename Visitor::result_type, void>,
+ int> = 0>
+ typename Visitor::result_type Visit(Visitor visitor, bool completes = true) {
+ typename Visitor::result_type result;
+ {
+ std::lock_guard lock(mutex_);
+ result = std::visit(std::move(visitor), call_);
+ }
+ if (completes && pending_.exchange(false)) {
+ cv_.notify_all();
+ }
+ return result;
+ }
+
+ // Records the number of bytes written as part of a request. If `append` is
+ // true, treats the write as a continuation of a streaming request.
+ void RecordWrite(size_t num, bool append = false);
+
+ /// Waits to be notified that a callback has been invoked.
+ void Await() PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Completes the call, notifying any waiters.
+ void Notify() PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Exchanges the call represented by this object with another.
+ void Swap(FuzzyCall& other);
+
+ /// Resets the call wrapped by this object with a new one. Destorys the
+ /// previous call.
+ void Reset(Variant call = Variant()) PW_LOCKS_EXCLUDED(mutex_);
+
+ // Reports the state of this object.
+ void Log() PW_LOCKS_EXCLUDED(mutex_);
+
+ private:
+ /// This represents the index in the engine's list of calls. It is used to
+ /// ensure a consistent order of locking multiple calls.
+ const size_t index_;
+
+ sync::TimedMutex mutex_;
+ sync::ConditionVariable cv_;
+
+ /// An identifier that can be used find this object, e.g. by a callback, even
+ /// when it has been swapped with another call.
+ size_t id_ PW_GUARDED_BY(mutex_);
+
+ /// Holds the actual pw::rpc::Call object, when present.
+ Variant call_ PW_GUARDED_BY(mutex_);
+
+ /// Set when a request is sent, and cleared when a callback is invoked.
+ std::atomic_bool pending_ = false;
+
+ /// Bytes sent in the last unary request or stream write.
+ size_t last_write_ PW_GUARDED_BY(mutex_) = 0;
+
+ /// Total bytes sent using this call object.
+ size_t total_written_ PW_GUARDED_BY(mutex_) = 0;
+};
+
+/// The main RPC fuzzing engine.
+///
+/// This class takes or generates a sequence of actions, and dsitributes them to
+/// a number of threads that can perform them using an RPC client. Passing the
+/// same seed to the engine at construction will allow it to generate the same
+/// sequence of actions.
+class Fuzzer {
+ public:
+ /// Number of fuzzing threads. The first thread counted is the RPC dispatch
+ /// thread.
+ static constexpr size_t kNumThreads = 4;
+
+ /// Maximum number of actions that a single thread will try to perform before
+ /// exiting.
+ static constexpr size_t kMaxActionsPerThread = 255;
+
+ /// The number of call objects available to be used for fuzzing.
+ static constexpr size_t kMaxConcurrentCalls = 8;
+
+ /// The mxiumum number of individual fuzzing actions that the fuzzing threads
+ /// can perform. The `+ 1` is to allow the inclusion of a special `0` action
+ /// to separate each thread's actions when concatenated into a single list.
+ static constexpr size_t kMaxActions =
+ kNumThreads * (kMaxActionsPerThread + 1);
+
+ explicit Fuzzer(Client& client, uint32_t channel_id);
+
+ /// The fuzzer engine should remain pinned in memory since it is referenced by
+ /// the `CallbackContext`s.
+ Fuzzer(const Fuzzer&) = delete;
+ Fuzzer(Fuzzer&&) = delete;
+ Fuzzer& operator=(const Fuzzer&) = delete;
+ Fuzzer& operator=(Fuzzer&&) = delete;
+
+ void set_verbose(bool verbose) { verbose_ = verbose; }
+
+ /// Sets the timeout and starts the timer.
+ void set_timeout(chrono::SystemClock::duration timeout) {
+ timer_.Start(timeout);
+ }
+
+ /// Generates encoded actions from the RNG and `Run`s them.
+ void Run(uint64_t seed, size_t num_actions);
+
+ /// Splits the provided `actions` between the fuzzing threads and runs them to
+ /// completion.
+ void Run(const Vector<uint32_t>& actions);
+
+ private:
+ /// Information passed to the RPC callbacks, including the index of the
+ /// associated call and a pointer to the fuzzer object.
+ struct CallbackContext {
+ size_t id;
+ Fuzzer* fuzzer;
+ };
+
+ /// Restarts the alarm timer, delaying it from detecting a timeout. This is
+ /// called whenever actions complete and indicates progress is still being
+ /// made.
+ void ResetTimerLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
+
+ /// Decodes the `encoded` action and performs it. The `thread_id` is used for
+ /// verbose diagnostics. When invoked from `PerformCallback` the `callback_id`
+ /// will be set to the index of the associated call. This allows avoiding
+ /// specific, prohibited actions, e.g. destroying a call from its own
+ /// callback.
+ void Perform(const Action& action) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Returns the call with the matching `id`.
+ FuzzyCall& FindCall(size_t id) PW_LOCKS_EXCLUDED(mutex_) {
+ std::lock_guard lock(mutex_);
+ return FindCallLocked(id);
+ }
+
+ FuzzyCall& FindCallLocked(size_t id) PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
+ return fuzzy_calls_[indices_[id]];
+ }
+
+ /// Returns a pointer to callback context for the given call index.
+ CallbackContext* GetContext(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_) {
+ std::lock_guard lock(mutex_);
+ return &contexts_[callback_id];
+ }
+
+ /// Callback for stream write made by the call with the given `callback_id`.
+ void OnNext(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Callback for completed request for the call with the given `callback_id`.
+ void OnCompleted(size_t callback_id) PW_LOCKS_EXCLUDED(mutex_);
+
+ /// Callback for an error for the call with the given `callback_id`.
+ void OnError(size_t callback_id, Status status) PW_LOCKS_EXCLUDED(mutex_);
+
+ bool verbose_ = false;
+ pw_rpc::raw::Benchmark::Client client_;
+ BenchmarkService service_;
+
+ /// Alarm thread that detects when no workers have made recent progress.
+ AlarmTimer timer_;
+
+ sync::Mutex mutex_;
+
+ /// Worker threads. The first thread is the RPC response dispatcher.
+ Vector<std::thread, kNumThreads> threads_;
+
+ /// RPC call objects.
+ Vector<FuzzyCall, kMaxConcurrentCalls> fuzzy_calls_;
+
+ /// Maps each call's IDs to its index. Since calls may be move before their
+ /// callbacks are invoked, this list can be used to find the original call.
+ Vector<size_t, kMaxConcurrentCalls> indices_ PW_GUARDED_BY(mutex_);
+
+ /// Context objects used to reference the engine and call.
+ Vector<CallbackContext, kMaxConcurrentCalls> contexts_ PW_GUARDED_BY(mutex_);
+
+ /// Set of actions performed as callbacks from other calls.
+ Vector<uint32_t, kMaxActionsPerThread> callback_actions_
+ PW_GUARDED_BY(mutex_);
+ Vector<uint32_t>::iterator callback_iterator_ PW_GUARDED_BY(mutex_);
+
+ /// Total actions performed by all workers.
+ std::atomic<size_t> num_actions_ = 0;
+};
+
+} // namespace pw::rpc::fuzz
diff --git a/pw_rpc/integration_testing.cc b/pw_rpc/integration_testing.cc
index 4212d4b00..d0b18fada 100644
--- a/pw_rpc/integration_testing.cc
+++ b/pw_rpc/integration_testing.cc
@@ -24,23 +24,27 @@
namespace pw::rpc::integration_test {
namespace {
-SocketClientContext<512> context;
+// Hard-coded to 1055 bytes, which is enough to fit 512-byte payloads when using
+// HDLC framing.
+SocketClientContext<1055> context;
unit_test::LoggingEventHandler log_test_events;
} // namespace
Client& client() { return context.client(); }
-Status InitializeClient(int argc, char* argv[], const char* usage_args) {
- unit_test::RegisterEventHandler(&log_test_events);
+int GetClientSocketFd() { return context.GetSocketFd(); }
- if (argc < 2) {
- PW_LOG_INFO("Usage: %s %s", argv[0], usage_args);
- return Status::InvalidArgument();
- }
+void SetEgressChannelManipulator(ChannelManipulator* new_channel_manipulator) {
+ context.SetEgressChannelManipulator(new_channel_manipulator);
+}
- const int port = std::atoi(argv[1]);
+void SetIngressChannelManipulator(ChannelManipulator* new_channel_manipulator) {
+ context.SetIngressChannelManipulator(new_channel_manipulator);
+}
+Status InitializeClient(int port) {
+ unit_test::RegisterEventHandler(&log_test_events);
if (port <= 0 || port > std::numeric_limits<uint16_t>::max()) {
PW_LOG_CRITICAL("Port numbers must be between 1 and 65535; %d is invalid",
port);
@@ -51,4 +55,16 @@ Status InitializeClient(int argc, char* argv[], const char* usage_args) {
return context.Start(port);
}
+void TerminateClient() { context.Terminate(); }
+
+Status InitializeClient(int argc, char* argv[], const char* usage_args) {
+ if (argc < 2) {
+ PW_LOG_INFO("Usage: %s %s", argv[0], usage_args);
+ return Status::InvalidArgument();
+ }
+
+ const int port = std::atoi(argv[1]);
+ return InitializeClient(port);
+}
+
} // namespace pw::rpc::integration_test
diff --git a/pw_rpc/internal/integration_test_ports.gni b/pw_rpc/internal/integration_test_ports.gni
index 0e4436e88..5197aab46 100644
--- a/pw_rpc/internal/integration_test_ports.gni
+++ b/pw_rpc/internal/integration_test_ports.gni
@@ -16,6 +16,5 @@
# in one place to prevent accidental conflicts between tests.
pw_rpc_PYTHON_CLIENT_CPP_SERVER_TEST_PORT = 30576
pw_rpc_CPP_CLIENT_INTEGRATION_TEST_PORT = 30577
-pw_transfer_PYTHON_CPP_TRANSFER_TEST_PORT = 30578
-pw_transfer_CPP_CPP_TRANSFER_TEST_PORT = 30579
+pw_rpc_CPP_CLIENT_FUZZER_TEST_PORT = 30578
pw_unit_test_RPC_SERVICE_TEST_PORT = 30580
diff --git a/pw_rpc/internal/packet.proto b/pw_rpc/internal/packet.proto
index 2862eb402..606efb9ef 100644
--- a/pw_rpc/internal/packet.proto
+++ b/pw_rpc/internal/packet.proto
@@ -33,10 +33,6 @@ enum PacketType {
// The client received a packet for an RPC it did not request.
CLIENT_ERROR = 4;
- // Deprecated, do not use. Send a CLIENT_ERROR with status CANCELLED instead.
- // TODO(pwbug/512): Remove this packet type.
- DEPRECATED_CANCEL = 6;
-
// A client stream has completed.
CLIENT_STREAM_END = 8;
@@ -45,16 +41,15 @@ enum PacketType {
// The RPC has finished.
RESPONSE = 1;
- // Deprecated, do not use. Formerly was used as the last packet in a server
- // stream.
- // TODO(pwbug/512): Remove this packet type.
- DEPRECATED_SERVER_STREAM_END = 3;
-
// The server was unable to process a request.
SERVER_ERROR = 5;
// A message in a server stream.
SERVER_STREAM = 7;
+
+ // Reserve field numbers for deprecated PacketTypes.
+ reserved 3; // SERVER_STREAM_END (equivalent to RESPONSE now)
+ reserved 6; // CANCEL (replaced by CLIENT_ERROR with status CANCELLED)
}
message RpcPacket {
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/AbstractCall.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/AbstractCall.java
new file mode 100644
index 000000000..116fa096d
--- /dev/null
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/AbstractCall.java
@@ -0,0 +1,145 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.MessageLite;
+import dev.pigweed.pw_log.Logger;
+import dev.pigweed.pw_rpc.Call.ClientStreaming;
+import java.util.Locale;
+import javax.annotation.Nullable;
+
+/**
+ * Partial implementation of the Call interface.
+ *
+ * Call objects never manipulate their own state through public functions. They only manipulate
+ * state through functions called by the RpcManager class.
+ */
+abstract class AbstractCall<RequestT extends MessageLite, ResponseT extends MessageLite>
+ implements Call, ClientStreaming<RequestT> {
+ private static final Logger logger = Logger.forClass(StreamObserverCall.class);
+
+ private final Endpoint endpoint;
+ private final PendingRpc rpc;
+
+ @Nullable private Status status = null;
+ @Nullable private Status error = null;
+
+ AbstractCall(Endpoint endpoint, PendingRpc rpc) {
+ this.endpoint = endpoint;
+ this.rpc = rpc;
+ }
+
+ @Nullable
+ @Override
+ public final Status status() {
+ return status;
+ }
+
+ @Nullable
+ @Override
+ public final Status error() {
+ return error;
+ }
+
+ @Override
+ public final boolean cancel() throws ChannelOutputException {
+ return endpoint.cancel(this);
+ }
+
+ @Override
+ public final boolean abandon() {
+ return endpoint.abandon(this);
+ }
+
+ @Override
+ public final boolean write(RequestT request) throws ChannelOutputException {
+ return endpoint.clientStream(this, request);
+ }
+
+ @Override
+ public final boolean finish() throws ChannelOutputException {
+ return endpoint.clientStreamEnd(this);
+ }
+
+ final int getChannelId() {
+ return rpc.channel().id();
+ }
+
+ final void sendPacket(byte[] packet) throws ChannelOutputException {
+ rpc.channel().send(packet);
+ }
+
+ final PendingRpc rpc() {
+ return rpc;
+ }
+
+ // The following functions change the call's state and may ONLY be called by the RpcManager!
+
+ final void handleNext(ByteString payload) {
+ ResponseT response = parseResponse(payload);
+ if (response != null) {
+ doHandleNext(response);
+ }
+ }
+
+ abstract void doHandleNext(ResponseT response);
+
+ final void handleStreamCompleted(Status status) {
+ this.status = status;
+ doHandleCompleted();
+ }
+
+ final void handleUnaryCompleted(ByteString payload, Status status) {
+ this.status = status;
+ handleNext(payload);
+ doHandleCompleted();
+ }
+
+ abstract void doHandleCompleted();
+
+ final void handleError(Status error) {
+ this.error = error;
+ doHandleError();
+ }
+
+ abstract void doHandleError();
+
+ void handleExceptionOnInitialPacket(ChannelOutputException e) throws ChannelOutputException {
+ throw e;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Nullable
+ private ResponseT parseResponse(ByteString payload) {
+ try {
+ return (ResponseT) rpc.method().decodeResponsePayload(payload);
+ } catch (InvalidProtocolBufferException e) {
+ logger.atWarning().withCause(e).log(
+ "Failed to decode response for method %s; skipping packet", rpc.method().name());
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ENGLISH,
+ "RpcCall[%s|channel=%d|%s]",
+ rpc.method(),
+ rpc.channel().id(),
+ active() ? "active" : "inactive");
+ }
+}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/BUILD.bazel b/pw_rpc/java/main/dev/pigweed/pw_rpc/BUILD.bazel
index 4747b7f86..d7923d7d4 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/BUILD.bazel
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/BUILD.bazel
@@ -17,17 +17,24 @@
java_library(
name = "client",
srcs = [
+ "AbstractCall.java",
"Call.java",
"Channel.java",
"ChannelOutputException.java",
"Client.java",
+ "Endpoint.java",
+ "FutureCall.java",
"Ids.java",
+ "InvalidRpcChannelException.java",
+ "InvalidRpcServiceException.java",
+ "InvalidRpcServiceMethodException.java",
+ "InvalidRpcStateException.java",
"Method.java",
"MethodClient.java",
"Packets.java",
"PendingRpc.java",
"RpcError.java",
- "RpcManager.java",
+ "RpcKey.java",
"Service.java",
"Status.java",
"StreamObserver.java",
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Call.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Call.java
index 364ec9667..1465a4b11 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Call.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Call.java
@@ -18,10 +18,29 @@ import com.google.common.util.concurrent.ListenableFuture;
import com.google.protobuf.MessageLite;
import javax.annotation.Nullable;
-/** Represents an ongoing RPC call. */
+/**
+ * Represents an ongoing RPC call.
+ *
+ * Methods that send packets may throw a ChannelOutputException if sending failed.
+ */
public interface Call {
- /** Cancels the RPC. Sends a cancellation packet to the server and sets error() to CANCELLED. */
- void cancel() throws ChannelOutputException;
+ /**
+ * Cancels the RPC.
+ *
+ * Sends a cancellation packet to the server and sets error() to CANCELLED. Does nothing if the
+ * call is inactive.
+ *
+ * @return true if the call was active; false if the call was inactive so nothing was done
+ * @throws ChannelOutputException the Channel.Output was unable to send the cancellation packet
+ */
+ boolean cancel() throws ChannelOutputException;
+
+ /**
+ * Cancels the RPC as in cancel(), but does not send a cancellation packet to the server.
+ *
+ * @return true if the call was active; false if the call was inactive so nothing was done
+ */
+ boolean abandon();
/** True if the RPC has not yet completed. */
default boolean active() {
@@ -46,17 +65,34 @@ public interface Call {
/** Represents a call to a client or bidirectional streaming RPC. */
interface ClientStreaming<RequestT extends MessageLite> extends Call {
/**
- * Sends a request to a pending client streaming RPC.
+ * Sends a request to a pending client or bidirectional streaming RPC.
*
- * <p>The semantics of send() reflect the Channel.Output implementation for this channel.
+ * Sends a client stream packet to the server. Does nothing if the call is inactive.
*
+ * @return true if the packet was sent; false if the call was inactive so nothing was done
* @throws ChannelOutputException the Channel.Output was unable to send the request
- * @throws RpcError the RPC is not currently active
*/
- void send(RequestT request) throws ChannelOutputException, RpcError;
+ boolean write(RequestT request) throws ChannelOutputException;
+
+ /**
+ * send() was renamed to write() for consistency with other pw_rpc clients.
+ *
+ * @deprecated Renamed to write(); call write() instead
+ */
+ @Deprecated
+ default void send(RequestT request) throws ChannelOutputException {
+ write(request);
+ }
- /** Signals to the server that the client stream has completed. */
- void finish() throws ChannelOutputException;
+ /**
+ * Signals to the server that the client stream has completed.
+ *
+ * Sends a client stream end packet to the server. Does nothing if the call is inactive.
+ *
+ * @return true if the packet was sent; false if the call was inactive so nothing was done
+ * @throws ChannelOutputException the Channel.Output was unable to send the request
+ */
+ boolean finish() throws ChannelOutputException;
}
/** Represents a call to a client streaming RPC that uses a future. */
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Channel.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Channel.java
index 6cb224dd9..d49395137 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Channel.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Channel.java
@@ -38,7 +38,12 @@ public class Channel {
private final int id;
private final Output output;
+ /** Creates a new Channel with the provided ID, which must be positive. */
public Channel(int id, Output output) {
+ if (id <= 0) {
+ throw new IllegalArgumentException("The channel ID must be positive: " + id + " is invalid");
+ }
+
this.id = id;
this.output = output;
}
@@ -47,7 +52,7 @@ public class Channel {
return id;
}
- public void send(byte[] data) throws ChannelOutputException {
+ void send(byte[] data) throws ChannelOutputException {
output.send(data);
}
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
index a62d77d76..cdbfc07de 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Client.java
@@ -18,12 +18,12 @@ import com.google.protobuf.ExtensionRegistryLite;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
import dev.pigweed.pw_log.Logger;
-import dev.pigweed.pw_rpc.internal.Packet.PacketType;
import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -35,13 +35,12 @@ import javax.annotation.Nullable;
public class Client {
private static final Logger logger = Logger.forClass(Client.class);
- private final Map<Integer, Channel> channels;
private final Map<Integer, Service> services;
+ private final Endpoint endpoint;
- private final Map<PendingRpc, MethodClient> methodClients;
- private final RpcManager rpcs;
+ private final Map<RpcKey, MethodClient> methodClients = new HashMap<>();
- private final Function<PendingRpc, StreamObserver<MessageLite>> defaultObserverFactory;
+ private final Function<RpcKey, StreamObserver<MessageLite>> defaultObserverFactory;
/**
* Creates a new RPC client.
@@ -51,12 +50,9 @@ public class Client {
*/
private Client(List<Channel> channels,
List<Service> services,
- Function<PendingRpc, StreamObserver<MessageLite>> defaultObserverFactory) {
- this.channels = channels.stream().collect(Collectors.toMap(Channel::id, c -> c));
+ Function<RpcKey, StreamObserver<MessageLite>> defaultObserverFactory) {
this.services = services.stream().collect(Collectors.toMap(Service::id, s -> s));
-
- this.methodClients = new HashMap<>();
- this.rpcs = new RpcManager();
+ this.endpoint = new Endpoint(channels);
this.defaultObserverFactory = defaultObserverFactory;
}
@@ -71,11 +67,13 @@ public class Client {
*/
public static Client create(List<Channel> channels,
List<Service> services,
- Function<PendingRpc, StreamObserver<MessageLite>> defaultObserverFactory) {
+ Function<RpcKey, StreamObserver<MessageLite>> defaultObserverFactory) {
return new Client(channels, services, defaultObserverFactory);
}
- /** Creates a new pw_rpc client that logs responses when no observer is provided to calls. */
+ /**
+ * Creates a new pw_rpc client that logs responses when no observer is provided to calls.
+ */
public static Client create(List<Channel> channels, List<Service> services) {
return create(channels, services, (rpc) -> new StreamObserver<MessageLite>() {
@Override
@@ -96,6 +94,25 @@ public class Client {
}
/**
+ * Adds a new channel to this RPC client.
+ *
+ * @throws InvalidRpcChannelException if the channel's ID is already in use
+ */
+ public void openChannel(Channel channel) {
+ endpoint.openChannel(channel);
+ }
+
+ /**
+ * Closes a channel and aborts and RPCs using it.
+ *
+ * @param id the channel ID to close
+ * @return true if the channel was closed; false if the channel was not found
+ */
+ public boolean closeChannel(int id) {
+ return endpoint.closeChannel(id);
+ }
+
+ /**
* Returns a MethodClient with the given name for the provided channelID
*
* @param channelId the ID for the channel through which to invoke the RPC
@@ -118,36 +135,65 @@ public class Client {
* and "Method".
*/
public MethodClient method(int channelId, String fullServiceName, String methodName) {
- try {
- return method(channelId, Ids.calculate(fullServiceName), Ids.calculate(methodName));
- } catch (IllegalArgumentException e) {
- // Rethrow the exception with the service and method name instead of the ID.
- throw new IllegalArgumentException("Unknown RPC " + fullServiceName + '/' + methodName, e);
+ return method(channelId, Ids.calculate(fullServiceName), Ids.calculate(methodName));
+ }
+
+ /**
+ * Returns a MethodClient instance from a Method instance.
+ */
+ public MethodClient method(int channelId, Method serviceMethod) {
+ return method(channelId, serviceMethod.service().id(), serviceMethod.id());
+ }
+
+ /**
+ * Returns a MethodClient with the provided service and method IDs.
+ */
+ synchronized MethodClient method(int channelId, int serviceId, int methodId) {
+ Method method = getMethod(serviceId, methodId);
+
+ RpcKey rpc = RpcKey.create(channelId, method);
+ if (!methodClients.containsKey(rpc)) {
+ methodClients.put(
+ rpc, new MethodClient(this, channelId, method, defaultObserverFactory.apply(rpc)));
}
+ return methodClients.get(rpc);
+ }
+
+ synchronized<CallT extends AbstractCall<?, ?>> CallT invokeRpc(int channelId,
+ Method method,
+ BiFunction<Endpoint, PendingRpc, CallT> createCall,
+ @Nullable MessageLite request) throws ChannelOutputException {
+ return endpoint.invokeRpc(channelId, checkMethod(method), createCall, request);
}
- /** Returns a MethodClient with the provided service and method IDs. */
- public MethodClient method(int channelId, int serviceId, int methodId) {
- Channel channel = channels.get(channelId);
- if (channel == null) {
- throw new IllegalArgumentException("Unknown channel ID " + channelId);
+ synchronized<CallT extends AbstractCall<?, ?>> CallT openRpc(
+ int channelId, Method method, BiFunction<Endpoint, PendingRpc, CallT> createCall) {
+ return endpoint.openRpc(channelId, checkMethod(method), createCall);
+ }
+
+ private Method checkMethod(Method method) {
+ // Check that the method on this service object matches the method this client is for.
+ // If the service was swapped out, the method could be different.
+ Method foundMethod = getMethod(method.service().id(), method.id());
+ if (!method.equals(foundMethod)) {
+ throw new InvalidRpcServiceMethodException(foundMethod);
}
+ return foundMethod;
+ }
+ private synchronized Method getMethod(int serviceId, int methodId) {
+ // Make sure the service is still present on the class.
Service service = services.get(serviceId);
if (service == null) {
- throw new IllegalArgumentException("Unknown service ID " + serviceId);
+ throw new InvalidRpcServiceException(serviceId);
}
Method method = service.methods().get(methodId);
if (method == null) {
- throw new IllegalArgumentException("Unknown method ID " + methodId);
+ throw new InvalidRpcServiceMethodException(service, methodId);
}
- PendingRpc rpc = PendingRpc.create(channel, service, method);
- if (!methodClients.containsKey(rpc)) {
- methodClients.put(rpc, new MethodClient(rpcs, rpc, defaultObserverFactory.apply(rpc)));
- }
- return methodClients.get(rpc);
+ return method;
}
/**
@@ -181,92 +227,12 @@ public class Client {
return false;
}
- Channel channel = channels.get(packet.getChannelId());
- if (channel == null) {
- logger.atWarning().log("Received packet for unrecognized channel %d", packet.getChannelId());
- return false;
- }
-
- PendingRpc rpc = lookupRpc(channel, packet);
- if (rpc == null) {
- logger.atInfo().log("Ignoring packet for unknown service method");
- sendError(channel, packet, Status.NOT_FOUND);
- return true; // true since the packet was handled, even though it was invalid.
- }
-
- // Any packet type other than SERVER_STREAM indicates that this is the last packet for this RPC.
- StreamObserverCall<?, ?> call =
- packet.getType().equals(PacketType.SERVER_STREAM) ? rpcs.getPending(rpc) : rpcs.clear(rpc);
- if (call == null) {
- logger.atFine().log(
- "Ignoring packet for %s, which isn't pending. Pending RPCs are %s", rpc, rpcs);
- sendError(channel, packet, Status.FAILED_PRECONDITION);
- return true;
- }
-
- switch (packet.getType()) {
- case SERVER_ERROR: {
- Status status = decodeStatus(packet);
- logger.atWarning().log("%s failed with error %s", rpc, status);
- call.onError(status);
- break;
- }
- case RESPONSE: {
- Status status = decodeStatus(packet);
- // Server streaming an unary RPCs include a payload with their response packet.
- if (!rpc.method().isServerStreaming()) {
- logger.atFiner().log("%s completed with status %s and %d B payload",
- rpc,
- status,
- packet.getPayload().size());
- call.onNext(packet.getPayload());
- } else {
- logger.atFiner().log("%s completed with status %s", rpc, status);
- }
- call.onCompleted(status);
- break;
- }
- case SERVER_STREAM:
- logger.atFiner().log(
- "%s received server stream with %d B payload", rpc, packet.getPayload().size());
- call.onNext(packet.getPayload());
- break;
- default:
- logger.atWarning().log(
- "%s received unexpected PacketType %d", rpc, packet.getType().getNumber());
- }
-
- return true;
- }
-
- private static void sendError(Channel channel, RpcPacket packet, Status status) {
+ Method method;
try {
- channel.send(Packets.error(packet, status));
- } catch (ChannelOutputException e) {
- logger.atWarning().withCause(e).log("Failed to send error packet");
- }
- }
-
- @Nullable
- private PendingRpc lookupRpc(Channel channel, RpcPacket packet) {
- Service service = services.get(packet.getServiceId());
- if (service != null) {
- Method method = service.methods().get(packet.getMethodId());
- if (method != null) {
- return PendingRpc.create(channel, service, method);
- }
- }
-
- return null;
- }
-
- private static Status decodeStatus(RpcPacket packet) {
- Status status = Status.fromCode(packet.getStatus());
- if (status == null) {
- logger.atWarning().log(
- "Illegal status code %d in packet; using Status.UNKNOWN ", packet.getStatus());
- return Status.UNKNOWN;
+ method = getMethod(packet.getServiceId(), packet.getMethodId());
+ } catch (InvalidRpcStateException e) {
+ method = null;
}
- return status;
+ return endpoint.processClientPacket(method, packet);
}
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Endpoint.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Endpoint.java
new file mode 100644
index 000000000..974a13204
--- /dev/null
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Endpoint.java
@@ -0,0 +1,311 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.MessageLite;
+import dev.pigweed.pw_log.Logger;
+import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.function.BiFunction;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+/**
+ * Tracks the state of service method invocations.
+ *
+ * The RPC endpoint handles all RPC-related events and actions. It synchronizes interactions between
+ * the endpoint and any threads interacting with RPC call objects.
+ *
+ * The Endpoint's intrinsic lock is held when updating the channels or pending calls lists. Call
+ * objects only make updates to their own state through function calls made from the Endpoint, which
+ * ensures their states are also guarded by the Endpoint's lock. Updates to call objects are
+ * enqueued while the lock is held and processed after releasing the lock. This ensures updates
+ * occur in order without needing to hold the Endpoint's lock while possibly executing user code.
+ */
+class Endpoint {
+ private static final Logger logger = Logger.forClass(Endpoint.class);
+
+ private final Map<Integer, Channel> channels;
+ private final Map<PendingRpc, AbstractCall<?, ?>> pending = new HashMap<>();
+ private final BlockingQueue<Runnable> callUpdates = new LinkedBlockingQueue<>();
+
+ public Endpoint(List<Channel> channels) {
+ this.channels = channels.stream().collect(Collectors.toMap(Channel::id, c -> c));
+ }
+
+ /**
+ * Creates an RPC call object and invokes the RPC
+ *
+ * @param channelId the channel to use
+ * @param method the service method to invoke
+ * @param createCall function that creates the call object
+ * @param request the request proto; null if this is a client streaming RPC
+ * @throws InvalidRpcChannelException if channelId is invalid
+ */
+ <RequestT extends MessageLite, CallT extends AbstractCall<?, ?>> CallT invokeRpc(int channelId,
+ Method method,
+ BiFunction<Endpoint, PendingRpc, CallT> createCall,
+ @Nullable RequestT request) throws ChannelOutputException {
+ CallT call = createCall(channelId, method, createCall);
+
+ // Attempt to start the call.
+ logger.atFiner().log("Starting %s", call);
+
+ try {
+ // If sending the packet fails, the RPC is never considered pending.
+ call.rpc().channel().send(Packets.request(call.rpc(), request));
+ } catch (ChannelOutputException e) {
+ call.handleExceptionOnInitialPacket(e);
+ }
+ registerCall(call);
+ return call;
+ }
+
+ /**
+ * Starts listening to responses for an RPC locally, but does not send any packets.
+ *
+ * <p>The RPC remains open until it is closed by the server (either from a response or error
+ * packet) or cancelled.
+ */
+ <CallT extends AbstractCall<?, ?>> CallT openRpc(
+ int channelId, Method method, BiFunction<Endpoint, PendingRpc, CallT> createCall) {
+ CallT call = createCall(channelId, method, createCall);
+ logger.atFiner().log("Opening %s", call);
+ registerCall(call);
+ return call;
+ }
+
+ private <CallT extends AbstractCall<?, ?>> CallT createCall(
+ int channelId, Method method, BiFunction<Endpoint, PendingRpc, CallT> createCall) {
+ Channel channel = channels.get(channelId);
+ if (channel == null) {
+ throw InvalidRpcChannelException.unknown(channelId);
+ }
+
+ return createCall.apply(this, PendingRpc.create(channel, method));
+ }
+
+ private void registerCall(AbstractCall<?, ?> call) {
+ // TODO(hepler): Use call_id to support simultaneous calls for the same RPC on one channel.
+ //
+ // Originally, only one call per service/method/channel was supported. With this restriction,
+ // the original call should have been aborted here, but was not. The client will be updated to
+ // support multiple simultaneous calls instead of aborting the call.
+ pending.put(call.rpc(), call);
+ }
+
+ /** Enqueues call object updates to make after release the Endpoint's lock. */
+ private void enqueueCallUpdate(Runnable callUpdate) {
+ while (!callUpdates.add(callUpdate)) {
+ // Retry until added successfully
+ }
+ }
+
+ /** Processes all enqueued call updates; the lock must NOT be held when this is called. */
+ private void processCallUpdates() {
+ while (true) {
+ Runnable callUpdate = callUpdates.poll();
+ if (callUpdate == null) {
+ break;
+ }
+ callUpdate.run();
+ }
+ }
+
+ /** Cancels an ongoing RPC */
+ public boolean cancel(AbstractCall<?, ?> call) throws ChannelOutputException {
+ try {
+ synchronized (this) {
+ if (pending.remove(call.rpc()) == null) {
+ return false;
+ }
+
+ enqueueCallUpdate(() -> call.handleError(Status.CANCELLED));
+ call.sendPacket(Packets.cancel(call.rpc()));
+ }
+ } finally {
+ logger.atFiner().log("Cancelling %s", call);
+ processCallUpdates();
+ }
+ return true;
+ }
+
+ /** Cancels an ongoing RPC without sending a cancellation packet. */
+ public boolean abandon(AbstractCall<?, ?> call) {
+ synchronized (this) {
+ if (pending.remove(call.rpc()) == null) {
+ return false;
+ }
+ enqueueCallUpdate(() -> call.handleError(Status.CANCELLED));
+ }
+ logger.atFiner().log("Abandoning %s", call);
+ processCallUpdates();
+ return true;
+ }
+
+ public synchronized boolean clientStream(AbstractCall<?, ?> call, MessageLite payload)
+ throws ChannelOutputException {
+ return sendPacket(call, Packets.clientStream(call.rpc(), payload));
+ }
+
+ public synchronized boolean clientStreamEnd(AbstractCall<?, ?> call)
+ throws ChannelOutputException {
+ return sendPacket(call, Packets.clientStreamEnd(call.rpc()));
+ }
+
+ private boolean sendPacket(AbstractCall<?, ?> call, byte[] packet) throws ChannelOutputException {
+ if (!pending.containsKey(call.rpc())) {
+ return false;
+ }
+ // TODO(hepler): Consider aborting the call if sending the packet fails.
+ call.sendPacket(packet);
+ return true;
+ }
+
+ public synchronized void openChannel(Channel channel) {
+ if (channels.putIfAbsent(channel.id(), channel) != null) {
+ throw InvalidRpcChannelException.duplicate(channel.id());
+ }
+ }
+
+ public boolean closeChannel(int id) {
+ synchronized (this) {
+ if (channels.remove(id) == null) {
+ return false;
+ }
+ pending.values().stream().filter(call -> call.getChannelId() == id).forEach(call -> {
+ enqueueCallUpdate(() -> call.handleError(Status.ABORTED));
+ });
+ }
+ processCallUpdates();
+ return true;
+ }
+
+ private boolean handleNext(PendingRpc rpc, ByteString payload) {
+ AbstractCall<?, ?> call = pending.get(rpc);
+ if (call == null) {
+ return false;
+ }
+ logger.atFiner().log("%s received server stream with %d B payload", call, payload.size());
+ enqueueCallUpdate(() -> call.handleNext(payload));
+ return true;
+ }
+
+ private boolean handleUnaryCompleted(PendingRpc rpc, ByteString payload, Status status) {
+ AbstractCall<?, ?> call = pending.remove(rpc);
+ if (call == null) {
+ return false;
+ }
+ logger.atFiner().log(
+ "%s completed with status %s and %d B payload", call, status, payload.size());
+ enqueueCallUpdate(() -> call.handleUnaryCompleted(payload, status));
+ return true;
+ }
+
+ private boolean handleStreamCompleted(PendingRpc rpc, Status status) {
+ AbstractCall<?, ?> call = pending.remove(rpc);
+ if (call == null) {
+ return false;
+ }
+ logger.atFiner().log("%s completed with status %s", call, status);
+ enqueueCallUpdate(() -> call.handleStreamCompleted(status));
+ return true;
+ }
+
+ private boolean handleError(PendingRpc rpc, Status status) {
+ AbstractCall<?, ?> call = pending.remove(rpc);
+ if (call == null) {
+ return false;
+ }
+ logger.atFiner().log("%s failed with error %s", call, status);
+ enqueueCallUpdate(() -> call.handleError(status));
+ return true;
+ }
+
+ public boolean processClientPacket(@Nullable Method method, RpcPacket packet) {
+ synchronized (this) {
+ Channel channel = channels.get(packet.getChannelId());
+ if (channel == null) {
+ logger.atWarning().log(
+ "Received packet for unrecognized channel %d", packet.getChannelId());
+ return false;
+ }
+
+ if (method == null) {
+ logger.atFine().log("Ignoring packet for unknown service method");
+ sendError(channel, packet, Status.NOT_FOUND);
+ return true; // true since the packet was handled, even though it was invalid.
+ }
+
+ PendingRpc rpc = PendingRpc.create(channel, method);
+ if (!updateCall(packet, rpc)) {
+ logger.atFine().log("Ignoring packet for %s, which isn't pending", rpc);
+ sendError(channel, packet, Status.FAILED_PRECONDITION);
+ return true;
+ }
+ }
+
+ processCallUpdates();
+ return true;
+ }
+
+ /** Returns true if the packet was forwarded to an active RPC call; false if no call was found. */
+ private boolean updateCall(RpcPacket packet, PendingRpc rpc) {
+ switch (packet.getType()) {
+ case SERVER_ERROR: {
+ Status status = decodeStatus(packet);
+ return handleError(rpc, status);
+ }
+ case RESPONSE: {
+ Status status = decodeStatus(packet);
+ // Client streaming and unary RPCs include a payload with their response packet.
+ if (rpc.method().isServerStreaming()) {
+ return handleStreamCompleted(rpc, status);
+ }
+ return handleUnaryCompleted(rpc, packet.getPayload(), status);
+ }
+ case SERVER_STREAM:
+ return handleNext(rpc, packet.getPayload());
+ default:
+ logger.atWarning().log(
+ "%s received unexpected PacketType %d", rpc, packet.getType().getNumber());
+ }
+
+ return true;
+ }
+
+ private static void sendError(Channel channel, RpcPacket packet, Status status) {
+ try {
+ channel.send(Packets.error(packet, status));
+ } catch (ChannelOutputException e) {
+ logger.atWarning().withCause(e).log("Failed to send error packet");
+ }
+ }
+
+ private static Status decodeStatus(RpcPacket packet) {
+ Status status = Status.fromCode(packet.getStatus());
+ if (status == null) {
+ logger.atWarning().log(
+ "Illegal status code %d in packet; using Status.UNKNOWN ", packet.getStatus());
+ return Status.UNKNOWN;
+ }
+ return status;
+ }
+}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/FutureCall.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/FutureCall.java
new file mode 100644
index 000000000..c6953487f
--- /dev/null
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/FutureCall.java
@@ -0,0 +1,163 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.protobuf.MessageLite;
+import dev.pigweed.pw_log.Logger;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+
+/**
+ * Call implementation that represents the call as a ListenableFuture.
+ *
+ * This class suppresses ShouldNotSubclass warnings from ListenableFuture. It implements
+ * ListenableFuture only because it cannot extend AbstractFuture since multiple inheritance is not
+ * supported. No Future funtionality is duplicated; FutureCall uses SettableFuture internally.
+ */
+@SuppressWarnings("ShouldNotSubclass")
+abstract class FutureCall<RequestT extends MessageLite, ResponseT extends MessageLite, ResultT>
+ extends AbstractCall<RequestT, ResponseT> implements ListenableFuture<ResultT> {
+ private static final Logger logger = Logger.forClass(FutureCall.class);
+
+ private final SettableFuture<ResultT> future = SettableFuture.create();
+
+ private FutureCall(Endpoint endpoint, PendingRpc rpc) {
+ super(endpoint, rpc);
+ }
+
+ // Implement the ListenableFuture interface by forwarding the internal SettableFuture.
+
+ @Override
+ public final void addListener(Runnable runnable, Executor executor) {
+ future.addListener(runnable, executor);
+ }
+
+ /** Cancellation means that a cancel() or cancel(boolean) call succeeded. */
+ @Override
+ public final boolean isCancelled() {
+ return error() == Status.CANCELLED;
+ }
+
+ @Override
+ public final boolean isDone() {
+ return future.isDone();
+ }
+
+ @Override
+ public final ResultT get() throws InterruptedException, ExecutionException {
+ return future.get();
+ }
+
+ @Override
+ public final ResultT get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ return future.get(timeout, unit);
+ }
+
+ @Override
+ public final boolean cancel(boolean mayInterruptIfRunning) {
+ try {
+ return this.cancel();
+ } catch (ChannelOutputException e) {
+ logger.atWarning().withCause(e).log("Failed to send cancellation packet for %s", rpc());
+ return true; // handleError() was already called, so the future was cancelled
+ }
+ }
+
+ /** Used by derived classes to access the future instance. */
+ final SettableFuture<ResultT> future() {
+ return future;
+ }
+
+ @Override
+ void handleExceptionOnInitialPacket(ChannelOutputException e) {
+ // Stash the exception in the future and abort the call.
+ future.setException(e);
+
+ // Set the status to mark the call completed. doHandleError() will have no effect since the
+ // exception was already set.
+ handleError(Status.ABORTED);
+ }
+
+ @Override
+ public void doHandleError() {
+ future.setException(new RpcError(rpc(), error()));
+ }
+
+ /** Future-based Call class for unary and client streaming RPCs. */
+ static class UnaryResponseFuture<RequestT extends MessageLite, ResponseT extends MessageLite>
+ extends FutureCall<RequestT, ResponseT, UnaryResult<ResponseT>>
+ implements ClientStreamingFuture<RequestT, ResponseT> {
+ @Nullable ResponseT response = null;
+
+ UnaryResponseFuture(Endpoint endpoint, PendingRpc rpc) {
+ super(endpoint, rpc);
+ }
+
+ @Override
+ public void doHandleNext(ResponseT value) {
+ if (response == null) {
+ response = value;
+ } else {
+ future().setException(new IllegalStateException("Unary RPC received multiple responses."));
+ }
+ }
+
+ @Override
+ public void doHandleCompleted() {
+ if (response == null) {
+ future().setException(
+ new IllegalStateException("Unary RPC completed without a response payload"));
+ } else {
+ future().set(UnaryResult.create(response, status()));
+ }
+ }
+ }
+
+ /** Future-based Call class for server and bidirectional streaming RPCs. */
+ static class StreamResponseFuture<RequestT extends MessageLite, ResponseT extends MessageLite>
+ extends FutureCall<RequestT, ResponseT, Status>
+ implements BidirectionalStreamingFuture<RequestT> {
+ private final Consumer<ResponseT> onNext;
+
+ static <RequestT extends MessageLite, ResponseT extends MessageLite>
+ BiFunction<Endpoint, PendingRpc, StreamResponseFuture<RequestT, ResponseT>> getFactory(
+ Consumer<ResponseT> onNext) {
+ return (rpcManager, pendingRpc) -> new StreamResponseFuture<>(rpcManager, pendingRpc, onNext);
+ }
+
+ private StreamResponseFuture(Endpoint endpoint, PendingRpc rpc, Consumer<ResponseT> onNext) {
+ super(endpoint, rpc);
+ this.onNext = onNext;
+ }
+
+ @Override
+ public void doHandleNext(ResponseT value) {
+ onNext.accept(value);
+ }
+
+ @Override
+ public void doHandleCompleted() {
+ future().set(status());
+ }
+ }
+}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcChannelException.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcChannelException.java
new file mode 100644
index 000000000..5916907cc
--- /dev/null
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcChannelException.java
@@ -0,0 +1,29 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+public class InvalidRpcChannelException extends InvalidRpcStateException {
+ static InvalidRpcChannelException unknown(int channelId) {
+ return new InvalidRpcChannelException("Invalid or closed RPC channel " + channelId);
+ }
+
+ static InvalidRpcChannelException duplicate(int channelId) {
+ return new InvalidRpcChannelException("A channel with ID " + channelId + " already exists!");
+ }
+
+ private InvalidRpcChannelException(String message) {
+ super(message);
+ }
+}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcServiceException.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcServiceException.java
new file mode 100644
index 000000000..601984e97
--- /dev/null
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcServiceException.java
@@ -0,0 +1,21 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+public class InvalidRpcServiceException extends InvalidRpcStateException {
+ InvalidRpcServiceException(int serviceId) {
+ super("The service with ID " + serviceId + " is not available");
+ }
+}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcServiceMethodException.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcServiceMethodException.java
new file mode 100644
index 000000000..e1d48141e
--- /dev/null
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcServiceMethodException.java
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+/** Attempted to access an unknown method in a known service. */
+public class InvalidRpcServiceMethodException extends InvalidRpcStateException {
+ InvalidRpcServiceMethodException(Method method) {
+ super("Service " + method.service().name() + " has no match for the " + method.name()
+ + " method");
+ }
+
+ InvalidRpcServiceMethodException(Service service, int methodId) {
+ super("Service " + service.name() + " has no method with ID " + methodId);
+ }
+}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcStateException.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcStateException.java
new file mode 100644
index 000000000..3ad5d1ac8
--- /dev/null
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/InvalidRpcStateException.java
@@ -0,0 +1,22 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+/** Base class for RPC state exceptions. */
+public class InvalidRpcStateException extends RuntimeException {
+ InvalidRpcStateException(String message) {
+ super(message);
+ }
+}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
index e32d9550e..ff80cbb61 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Method.java
@@ -41,7 +41,7 @@ public abstract class Method {
public abstract Class<? extends MessageLite> response();
- public final int id() {
+ final int id() {
return Ids.calculate(name());
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
index 010b8eaea..6d7fb2b42 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/MethodClient.java
@@ -15,42 +15,47 @@
package dev.pigweed.pw_rpc;
import com.google.protobuf.MessageLite;
-import dev.pigweed.pw_rpc.StreamObserverCall.StreamResponseFuture;
-import dev.pigweed.pw_rpc.StreamObserverCall.UnaryResponseFuture;
+import dev.pigweed.pw_rpc.FutureCall.StreamResponseFuture;
+import dev.pigweed.pw_rpc.FutureCall.UnaryResponseFuture;
+import java.util.function.BiFunction;
import java.util.function.Consumer;
+import javax.annotation.Nullable;
/**
* Represents a method ready to be invoked on a particular RPC channel.
*
- * <p>The Client has the concrete MethodClient as a type parameter. This allows implementations to
- * fully define the interface and semantics for RPC calls.
+ * Invoking an RPC with a method client may throw exceptions:
+ *
+ * TODO(hepler): This class should be split into four types -- one for each method type. The call
+ * type checks should be done when the object is created. Also, the client should be typed on
+ * the request/response.
*/
public class MethodClient {
- protected final StreamObserver<? extends MessageLite> defaultObserver;
- private final RpcManager rpcs;
- private final PendingRpc rpc;
-
- MethodClient(RpcManager rpcs, PendingRpc rpc, StreamObserver<MessageLite> defaultObserver) {
- this.rpcs = rpcs;
- this.rpc = rpc;
+ private final Client client;
+ private final int channelId;
+ private final Method method;
+ private final StreamObserver<? extends MessageLite> defaultObserver;
+
+ MethodClient(
+ Client client, int channelId, Method method, StreamObserver<MessageLite> defaultObserver) {
+ this.client = client;
+ this.channelId = channelId;
+ this.method = method;
this.defaultObserver = defaultObserver;
}
public final Method method() {
- return rpc.method();
- }
-
- /** Gives implementations access to the RpcManager shared with the Client. */
- protected final RpcManager rpcs() {
- return rpcs;
- }
-
- /** Gives implementations access to the PendingRpc this MethodClient represents. */
- protected final PendingRpc rpc() {
- return rpc;
+ return method;
}
- /** Invokes a unary RPC. Uses the default StreamObserver for RPC events. */
+ /**
+ * Invokes a unary RPC. Uses the default StreamObserver for RPC events.
+ *
+ * @throws InvalidRpcChannelException the client has no channel with this ID
+ * @throws InvalidRpcServiceException if the service was removed from the client
+ * @throws InvalidRpcServiceMethodException if the method was removed or changed since this
+ * MethodClient was created
+ */
public Call invokeUnary(MessageLite request) throws ChannelOutputException {
return invokeUnary(request, defaultObserver());
}
@@ -59,26 +64,26 @@ public class MethodClient {
public Call invokeUnary(MessageLite request, StreamObserver<? extends MessageLite> observer)
throws ChannelOutputException {
checkCallType(Method.Type.UNARY);
- return StreamObserverCall.start(rpcs(), rpc(), observer, request);
+ return client.invokeRpc(channelId, method, StreamObserverCall.getFactory(observer), request);
}
/** Invokes a unary RPC with a future that collects the response. */
public <ResponseT extends MessageLite> Call.UnaryFuture<ResponseT> invokeUnaryFuture(
MessageLite request) {
checkCallType(Method.Type.UNARY);
- return new UnaryResponseFuture<>(rpcs(), rpc(), request);
+ return invokeFuture(UnaryResponseFuture::new, request);
}
/**
- * Starts a unary RPC, ignoring any errors that occur when opening. This can be used to start
- * listening to responses to an RPC before the RPC server is available.
+ * Creates a call object for a unary RPC without starting the RPC on the server. This can be used
+ * to start listening to responses to an RPC before the RPC server is available.
*
* <p>The RPC remains open until it is completed by the server with a response or error packet or
* cancelled.
*/
- public Call openUnary(MessageLite request, StreamObserver<? extends MessageLite> observer) {
+ public Call openUnary(StreamObserver<? extends MessageLite> observer) {
checkCallType(Method.Type.UNARY);
- return StreamObserverCall.open(rpcs(), rpc(), observer, request);
+ return client.openRpc(channelId, method, StreamObserverCall.getFactory(observer));
}
/** Invokes a server streaming RPC. Uses the default StreamObserver for RPC events. */
@@ -90,27 +95,26 @@ public class MethodClient {
public Call invokeServerStreaming(MessageLite request,
StreamObserver<? extends MessageLite> observer) throws ChannelOutputException {
checkCallType(Method.Type.SERVER_STREAMING);
- return StreamObserverCall.start(rpcs(), rpc(), observer, request);
+ return client.invokeRpc(channelId, method, StreamObserverCall.getFactory(observer), request);
}
/** Invokes a server streaming RPC with a future that collects the responses. */
public Call.ServerStreamingFuture invokeServerStreamingFuture(
MessageLite request, Consumer<? extends MessageLite> onNext) {
checkCallType(Method.Type.SERVER_STREAMING);
- return new StreamResponseFuture<>(rpcs(), rpc(), onNext, request);
+ return invokeFuture(StreamResponseFuture.getFactory(onNext), request);
}
/**
- * Starts a server streaming RPC, ignoring any errors that occur when opening. This can be used to
- * start listening to responses to an RPC before the RPC server is available.
+ * Creates a call object for a server streaming RPC without starting the RPC on the server. This
+ * can be used to start listening to responses to an RPC before the RPC server is available.
*
* <p>The RPC remains open until it is completed by the server with a response or error packet or
* cancelled.
*/
- public Call openServerStreaming(
- MessageLite request, StreamObserver<? extends MessageLite> observer) {
+ public Call openServerStreaming(StreamObserver<? extends MessageLite> observer) {
checkCallType(Method.Type.SERVER_STREAMING);
- return StreamObserverCall.open(rpcs(), rpc(), observer, request);
+ return client.openRpc(channelId, method, StreamObserverCall.getFactory(observer));
}
/** Invokes a client streaming RPC. Uses the default StreamObserver for RPC events. */
@@ -123,19 +127,19 @@ public class MethodClient {
public <RequestT extends MessageLite> Call.ClientStreaming<RequestT> invokeClientStreaming(
StreamObserver<? extends MessageLite> observer) throws ChannelOutputException {
checkCallType(Method.Type.CLIENT_STREAMING);
- return StreamObserverCall.start(rpcs(), rpc(), observer, null);
+ return client.invokeRpc(channelId, method, StreamObserverCall.getFactory(observer), null);
}
/** Invokes a client streaming RPC with a future that collects the response. */
public <RequestT extends MessageLite> Call.ClientStreaming<RequestT>
invokeClientStreamingFuture() {
checkCallType(Method.Type.CLIENT_STREAMING);
- return new UnaryResponseFuture<>(rpcs(), rpc(), null);
+ return invokeFuture(UnaryResponseFuture::new, null);
}
/**
- * Starts a client streaming RPC, ignoring any errors that occur when opening. This can be used to
- * start listening to responses to an RPC before the RPC server is available.
+ * Creates a call object for a client streaming RPC without starting the RPC on the server. This
+ * can be used to start listening to responses to an RPC before the RPC server is available.
*
* <p>The RPC remains open until it is completed by the server with a response or error packet or
* cancelled.
@@ -143,7 +147,7 @@ public class MethodClient {
public <RequestT extends MessageLite> Call.ClientStreaming<RequestT> openClientStreaming(
StreamObserver<? extends MessageLite> observer) {
checkCallType(Method.Type.CLIENT_STREAMING);
- return StreamObserverCall.open(rpcs(), rpc(), observer, null);
+ return client.openRpc(channelId, method, StreamObserverCall.getFactory(observer));
}
/** Invokes a bidirectional streaming RPC. Uses the default StreamObserver for RPC events. */
@@ -156,7 +160,7 @@ public class MethodClient {
public <RequestT extends MessageLite> Call.ClientStreaming<RequestT> invokeBidirectionalStreaming(
StreamObserver<? extends MessageLite> observer) throws ChannelOutputException {
checkCallType(Method.Type.BIDIRECTIONAL_STREAMING);
- return StreamObserverCall.start(rpcs(), rpc(), observer, null);
+ return client.invokeRpc(channelId, method, StreamObserverCall.getFactory(observer), null);
}
/** Invokes a bidirectional streaming RPC with a future that finishes when the RPC finishes. */
@@ -164,12 +168,12 @@ public class MethodClient {
Call.BidirectionalStreamingFuture<RequestT> invokeBidirectionalStreamingFuture(
Consumer<ResponseT> onNext) {
checkCallType(Method.Type.BIDIRECTIONAL_STREAMING);
- return new StreamResponseFuture<>(rpcs(), rpc(), onNext, null);
+ return invokeFuture(StreamResponseFuture.getFactory(onNext), null);
}
/**
- * Starts a bidirectional streaming RPC, ignoring any errors that occur when opening. This can be
- * used to start listening to responses to an RPC before the RPC server is available.
+ * Creates a call object for a bidirectional streaming RPC without starting the RPC on the server.
+ * This can be used to start listening to responses to an RPC before the RPC server is available.
*
* <p>The RPC remains open until it is completed by the server with a response or error packet or
* cancelled.
@@ -177,7 +181,7 @@ public class MethodClient {
public <RequestT extends MessageLite> Call.ClientStreaming<RequestT> openBidirectionalStreaming(
StreamObserver<? extends MessageLite> observer) {
checkCallType(Method.Type.BIDIRECTIONAL_STREAMING);
- return StreamObserverCall.open(rpcs(), rpc(), observer, null);
+ return client.openRpc(channelId, method, StreamObserverCall.getFactory(observer));
}
@SuppressWarnings("unchecked")
@@ -185,13 +189,23 @@ public class MethodClient {
return (StreamObserver<ResponseT>) defaultObserver;
}
+ public <RequestT extends MessageLite, ResponseT extends MessageLite, CallT
+ extends FutureCall<RequestT, ResponseT, ?>> CallT
+ invokeFuture(BiFunction<Endpoint, PendingRpc, CallT> createCall, @Nullable MessageLite request) {
+ try {
+ return client.invokeRpc(channelId, method, createCall, request);
+ } catch (ChannelOutputException e) {
+ throw new AssertionError("Starting a future-based RPC call should never throw", e);
+ }
+ }
+
private void checkCallType(Method.Type expected) {
- if (!rpc().method().type().equals(expected)) {
+ if (!method.type().equals(expected)) {
throw new UnsupportedOperationException(String.format(
"%s is a %s method, but it was invoked as a %s method. RPCs must be invoked by the"
+ " appropriate invoke function.",
- method().fullName(),
- method().type().sentenceName(),
+ method.fullName(),
+ method.type().sentenceName(),
expected.sentenceName()));
}
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/PendingRpc.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/PendingRpc.java
index 59e210d8a..56ed5b2ed 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/PendingRpc.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/PendingRpc.java
@@ -17,21 +17,27 @@ package dev.pigweed.pw_rpc;
import com.google.auto.value.AutoValue;
import java.util.Locale;
-/** Represents a unique RPC invocation: channel + service + method. */
+/**
+ * Represents an active RPC invocation: channel + service + method.
+ *
+ * TODO(hepler): Use call ID to support multiple simultaneous calls to the same RPC on one channel.
+ */
@AutoValue
-public abstract class PendingRpc {
- public static PendingRpc create(Channel channel, Service service, Method method) {
- return new AutoValue_PendingRpc(channel, service, method);
+abstract class PendingRpc {
+ static PendingRpc create(Channel channel, Method method) {
+ return new AutoValue_PendingRpc(channel, method);
}
public abstract Channel channel();
- public abstract Service service();
+ public final Service service() {
+ return method().service();
+ }
public abstract Method method();
@Override
public final String toString() {
- return String.format(Locale.ENGLISH, "RpcCall[%s channel=%d]", method(), channel().id());
+ return String.format(Locale.ENGLISH, "PendingRpc[%s|channel=%d]", method(), channel().id());
}
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/RpcKey.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/RpcKey.java
new file mode 100644
index 000000000..d78747e50
--- /dev/null
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/RpcKey.java
@@ -0,0 +1,41 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+import com.google.auto.value.AutoValue;
+import java.util.Locale;
+
+/**
+ * Represents a potential RPC invocation: channel ID, service, and method.
+ */
+@AutoValue
+public abstract class RpcKey {
+ static RpcKey create(int channelId, Method method) {
+ return new AutoValue_RpcKey(channelId, method);
+ }
+
+ public abstract int channelId();
+
+ public final Service service() {
+ return method().service();
+ }
+
+ public abstract Method method();
+
+ @Override
+ public final String toString() {
+ return String.format(Locale.ENGLISH, "RpcKey[%s|channel=%d]", method(), channelId());
+ }
+}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/RpcManager.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/RpcManager.java
deleted file mode 100644
index bc690bb28..000000000
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/RpcManager.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-package dev.pigweed.pw_rpc;
-
-import com.google.protobuf.MessageLite;
-import dev.pigweed.pw_log.Logger;
-import java.util.HashMap;
-import java.util.Map;
-import javax.annotation.Nullable;
-
-/** Tracks the state of service method invocations. */
-public class RpcManager {
- private static final Logger logger = Logger.forClass(RpcManager.class);
-
- private final Map<PendingRpc, StreamObserverCall<?, ?>> pending = new HashMap<>();
-
- /**
- * Invokes an RPC.
- *
- * @param rpc channel / service / method tuple that unique identifies this RPC
- * @param call object for this RPC
- * @param payload the request
- */
- @Nullable
- public synchronized StreamObserverCall<?, ?> start(
- PendingRpc rpc, StreamObserverCall<?, ?> call, @Nullable MessageLite payload)
- throws ChannelOutputException {
- logger.atFine().log("%s starting", rpc);
- rpc.channel().send(Packets.request(rpc, payload));
- return pending.put(rpc, call);
- }
-
- /**
- * Invokes an RPC, but ignores errors and keeps the RPC active if the invocation fails.
- *
- * <p>The RPC remains open until it is closed by the server (either with a response or error
- * packet) or cancelled.
- */
- @Nullable
- public synchronized StreamObserverCall<?, ?> open(
- PendingRpc rpc, StreamObserverCall<?, ?> call, @Nullable MessageLite payload) {
- logger.atFine().log("%s opening", rpc);
- try {
- rpc.channel().send(Packets.request(rpc, payload));
- } catch (ChannelOutputException e) {
- logger.atFiner().withCause(e).log(
- "Ignoring error opening %s; listening for unrequested responses", rpc);
- }
- return pending.put(rpc, call);
- }
-
- /** Cancels an ongoing RPC */
- @Nullable
- public synchronized StreamObserverCall<?, ?> cancel(PendingRpc rpc)
- throws ChannelOutputException {
- StreamObserverCall<?, ?> call = pending.remove(rpc);
- if (call != null) {
- logger.atFine().log("%s was cancelled", rpc);
- rpc.channel().send(Packets.cancel(rpc));
- }
- return call;
- }
-
- @Nullable
- public synchronized StreamObserverCall<?, ?> clientStream(PendingRpc rpc, MessageLite payload)
- throws ChannelOutputException {
- StreamObserverCall<?, ?> call = pending.get(rpc);
- if (call != null) {
- rpc.channel().send(Packets.clientStream(rpc, payload));
- }
- return call;
- }
-
- @Nullable
- public synchronized StreamObserverCall<?, ?> clientStreamEnd(PendingRpc rpc)
- throws ChannelOutputException {
- StreamObserverCall<?, ?> call = pending.get(rpc);
- if (call != null) {
- logger.atFiner().log("%s client stream closed", rpc);
- rpc.channel().send(Packets.clientStreamEnd(rpc));
- }
- return call;
- }
-
- @Nullable
- public synchronized StreamObserverCall<?, ?> clear(PendingRpc rpc) {
- return pending.remove(rpc);
- }
-
- @Nullable
- public synchronized StreamObserverCall<?, ?> getPending(PendingRpc rpc) {
- return pending.get(rpc);
- }
-
- @Override
- public synchronized String toString() {
- return "RpcManager{"
- + "pending=" + pending + '}';
- }
-}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
index 311ff6f10..eeffd2af8 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/Service.java
@@ -38,7 +38,7 @@ public class Service {
return name;
}
- public int id() {
+ int id() {
return id;
}
diff --git a/pw_rpc/java/main/dev/pigweed/pw_rpc/StreamObserverCall.java b/pw_rpc/java/main/dev/pigweed/pw_rpc/StreamObserverCall.java
index 537362c6c..64a442727 100644
--- a/pw_rpc/java/main/dev/pigweed/pw_rpc/StreamObserverCall.java
+++ b/pw_rpc/java/main/dev/pigweed/pw_rpc/StreamObserverCall.java
@@ -14,14 +14,8 @@
package dev.pigweed.pw_rpc;
-import com.google.common.util.concurrent.AbstractFuture;
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
-import dev.pigweed.pw_log.Logger;
-import dev.pigweed.pw_rpc.Call.ClientStreaming;
-import java.util.function.Consumer;
-import javax.annotation.Nullable;
+import java.util.function.BiFunction;
/**
* Represents an ongoing RPC call.
@@ -33,233 +27,34 @@ import javax.annotation.Nullable;
* @param <RequestT> request type of the RPC; used for client or bidirectional streaming RPCs
* @param <ResponseT> response type of the RPC; used for all types of RPCs
*/
-class StreamObserverCall<RequestT extends MessageLite, ResponseT extends MessageLite>
- implements ClientStreaming<RequestT> {
- private static final Logger logger = Logger.forClass(StreamObserverCall.class);
-
- private final RpcManager rpcs;
- private final PendingRpc rpc;
+final class StreamObserverCall<RequestT extends MessageLite, ResponseT extends MessageLite>
+ extends AbstractCall<RequestT, ResponseT> {
private final StreamObserver<ResponseT> observer;
- @Nullable private Status status = null;
- @Nullable private Status error = null;
-
- /** Base class for a Call that is a ListenableFuture. */
- private abstract static class StreamObserverFutureCall<RequestT extends MessageLite, ResponseT
- extends MessageLite, ResultT>
- extends AbstractFuture<ResultT>
- implements ClientStreaming<RequestT>, StreamObserver<ResponseT> {
- private final StreamObserverCall<RequestT, ResponseT> call;
-
- private StreamObserverFutureCall(RpcManager rpcs, PendingRpc rpc) {
- call = new StreamObserverCall<>(rpcs, rpc, this);
- }
-
- void start(@Nullable RequestT request) {
- try {
- call.rpcs.start(call.rpc, call, request);
- } catch (ChannelOutputException e) {
- call.error = Status.UNKNOWN;
- setException(e);
- }
- }
-
- @Override
- public boolean cancel(boolean mayInterruptIfRunning) {
- boolean result = super.cancel(mayInterruptIfRunning);
- try {
- call.cancel();
- } catch (ChannelOutputException e) {
- setException(e);
- }
- return result;
- }
-
- @Override
- public void cancel() throws ChannelOutputException {
- cancel(true);
- }
-
- @Nullable
- @Override
- public Status status() {
- return call.status();
- }
-
- @Nullable
- @Override
- public Status error() {
- return call.error();
- }
-
- @Override
- public void send(RequestT request) throws ChannelOutputException, RpcError {
- call.send(request);
- }
-
- @Override
- public void finish() throws ChannelOutputException {
- call.finish();
- }
-
- @Override
- public void onError(Status status) {
- setException(new RpcError(call.rpc, status));
- }
- }
-
- /** Future-based Call class for unary and client streaming RPCs. */
- static class UnaryResponseFuture<RequestT extends MessageLite, ResponseT extends MessageLite>
- extends StreamObserverFutureCall<RequestT, ResponseT, UnaryResult<ResponseT>>
- implements Call.ClientStreamingFuture<RequestT, ResponseT> {
- @Nullable ResponseT response = null;
-
- UnaryResponseFuture(RpcManager rpcs, PendingRpc rpc, @Nullable RequestT request) {
- super(rpcs, rpc);
- start(request);
- }
-
- @Override
- public void onNext(ResponseT value) {
- if (response == null) {
- response = value;
- } else {
- setException(new IllegalStateException("Unary RPC received multiple responses."));
- }
- }
-
- @Override
- public void onCompleted(Status status) {
- if (response == null) {
- setException(new IllegalStateException("Unary RPC completed without a response payload"));
- } else {
- set(UnaryResult.create(response, status));
- }
- }
- }
-
- /** Future-based Call class for server and bidirectional streaming RPCs. */
- static class StreamResponseFuture<RequestT extends MessageLite, ResponseT extends MessageLite>
- extends StreamObserverFutureCall<RequestT, ResponseT, Status>
- implements Call.BidirectionalStreamingFuture<RequestT> {
- private final Consumer<ResponseT> onNext;
-
- StreamResponseFuture(
- RpcManager rpcs, PendingRpc rpc, Consumer<ResponseT> onNext, @Nullable RequestT request) {
- super(rpcs, rpc);
- this.onNext = onNext;
- start(request);
- }
-
- @Override
- public void onNext(ResponseT value) {
- onNext.accept(value);
- }
-
- @Override
- public void onCompleted(Status status) {
- set(status);
- }
- }
-
- /** Invokes the specified RPC. */
- static <RequestT extends MessageLite, ResponseT extends MessageLite>
- StreamObserverCall<RequestT, ResponseT> start(RpcManager rpcs,
- PendingRpc rpc,
- StreamObserver<ResponseT> observer,
- @Nullable MessageLite request) throws ChannelOutputException {
- StreamObserverCall<RequestT, ResponseT> call = new StreamObserverCall<>(rpcs, rpc, observer);
- rpcs.start(rpc, call, request);
- return call;
- }
-
- /** Invokes the specified RPC, ignoring errors that occur when the RPC is invoked. */
static <RequestT extends MessageLite, ResponseT extends MessageLite>
- StreamObserverCall<RequestT, ResponseT> open(RpcManager rpcs,
- PendingRpc rpc,
- StreamObserver<ResponseT> observer,
- @Nullable MessageLite request) {
- StreamObserverCall<RequestT, ResponseT> call = new StreamObserverCall<>(rpcs, rpc, observer);
- rpcs.open(rpc, call, request);
- return call;
+ BiFunction<Endpoint, PendingRpc, StreamObserverCall<RequestT, ResponseT>> getFactory(
+ StreamObserver<ResponseT> observer) {
+ return (endpoint, pendingRpc) -> new StreamObserverCall<>(endpoint, pendingRpc, observer);
}
- private StreamObserverCall(RpcManager rpcs, PendingRpc rpc, StreamObserver<ResponseT> observer) {
- this.rpcs = rpcs;
- this.rpc = rpc;
+ private StreamObserverCall(
+ Endpoint endpoint, PendingRpc rpc, StreamObserver<ResponseT> observer) {
+ super(endpoint, rpc);
this.observer = observer;
}
@Override
- public void cancel() throws ChannelOutputException {
- if (active()) {
- error = Status.CANCELLED;
- rpcs.cancel(rpc);
- }
- }
-
- @Override
- @Nullable
- public Status status() {
- return status;
- }
-
- @Nullable
- @Override
- public Status error() {
- return error;
+ void doHandleNext(ResponseT response) {
+ observer.onNext(response);
}
@Override
- public void send(RequestT request) throws ChannelOutputException, RpcError {
- if (error != null) {
- throw new RpcError(rpc, error);
- }
- if (status != null) {
- throw new RpcError(rpc, Status.FAILED_PRECONDITION);
- }
- rpcs.clientStream(rpc, request);
+ void doHandleCompleted() {
+ observer.onCompleted(status());
}
@Override
- public void finish() throws ChannelOutputException {
- if (active()) {
- rpcs.clientStreamEnd(rpc);
- }
- }
-
- void onNext(ByteString payload) {
- if (active()) {
- ResponseT message = parseResponse(payload);
- if (message != null) {
- observer.onNext(message);
- }
- }
- }
-
- void onCompleted(Status status) {
- if (active()) {
- this.status = status;
- observer.onCompleted(status);
- }
- }
-
- void onError(Status status) {
- if (active()) {
- this.error = status;
- observer.onError(status);
- }
- }
-
- @SuppressWarnings("unchecked")
- @Nullable
- private ResponseT parseResponse(ByteString payload) {
- try {
- return (ResponseT) rpc.method().decodeResponsePayload(payload);
- } catch (InvalidProtocolBufferException e) {
- logger.atWarning().withCause(e).log(
- "Failed to decode response for method %s; skipping packet", rpc.method().name());
- return null;
- }
+ void doHandleError() {
+ observer.onError(error());
}
}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel b/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
index 3f63f7a7b..532255da9 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/BUILD.bazel
@@ -20,7 +20,10 @@ java_library(
name = "test_client",
testonly = True,
srcs = ["TestClient.java"],
- visibility = ["__pkg__"],
+ visibility = [
+ "__pkg__",
+ "//pw_transfer/java/test/dev/pigweed/pw_transfer:__pkg__",
+ ],
deps = [
"//pw_rpc:packet_proto_java_lite",
"//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
@@ -48,44 +51,60 @@ java_test(
)
java_test(
- name = "IdsTest",
+ name = "EndpointTest",
size = "small",
- srcs = ["IdsTest.java"],
- test_class = "dev.pigweed.pw_rpc.IdsTest",
+ srcs = ["EndpointTest.java"],
+ test_class = "dev.pigweed.pw_rpc.EndpointTest",
deps = [
+ ":test_proto_java_proto_lite",
+ "//pw_rpc:packet_proto_java_lite",
"//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
+ "@com_google_protobuf//java/lite",
"@maven//:com_google_flogger_flogger_system_backend",
"@maven//:com_google_truth_truth",
+ "@maven//:org_mockito_mockito_core",
],
)
java_test(
- name = "PacketsTest",
+ name = "FutureCallTest",
size = "small",
- srcs = ["PacketsTest.java"],
- test_class = "dev.pigweed.pw_rpc.PacketsTest",
+ srcs = ["FutureCallTest.java"],
+ test_class = "dev.pigweed.pw_rpc.FutureCallTest",
deps = [
+ ":test_proto_java_proto_lite",
"//pw_rpc:packet_proto_java_lite",
"//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
"@com_google_protobuf//java/lite",
"@maven//:com_google_flogger_flogger_system_backend",
"@maven//:com_google_truth_truth",
+ "@maven//:org_mockito_mockito_core",
],
)
java_test(
- name = "RpcManagerTest",
+ name = "IdsTest",
size = "small",
- srcs = ["RpcManagerTest.java"],
- test_class = "dev.pigweed.pw_rpc.RpcManagerTest",
+ srcs = ["IdsTest.java"],
+ test_class = "dev.pigweed.pw_rpc.IdsTest",
+ deps = [
+ "//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
+ "@maven//:com_google_flogger_flogger_system_backend",
+ "@maven//:com_google_truth_truth",
+ ],
+)
+
+java_test(
+ name = "PacketsTest",
+ size = "small",
+ srcs = ["PacketsTest.java"],
+ test_class = "dev.pigweed.pw_rpc.PacketsTest",
deps = [
- ":test_proto_java_proto_lite",
"//pw_rpc:packet_proto_java_lite",
"//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
"@com_google_protobuf//java/lite",
"@maven//:com_google_flogger_flogger_system_backend",
"@maven//:com_google_truth_truth",
- "@maven//:org_mockito_mockito_core",
],
)
@@ -111,6 +130,7 @@ java_test(
test_class = "dev.pigweed.pw_rpc.StreamObserverMethodClientTest",
deps = [
":test_proto_java_proto_lite",
+ "//pw_rpc:packet_proto_java_lite",
"//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
"@com_google_protobuf//java/lite",
"@maven//:com_google_flogger_flogger_system_backend",
@@ -123,9 +143,10 @@ test_suite(
name = "pw_rpc",
tests = [
":ClientTest",
+ ":EndpointTest",
+ ":FutureCallTest",
":IdsTest",
":PacketsTest",
- ":RpcManagerTest",
":StreamObserverCallTest",
":StreamObserverMethodClientTest",
],
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
index affffa598..b3e31984b 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/ClientTest.java
@@ -20,6 +20,7 @@ import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
@@ -33,7 +34,6 @@ import java.util.List;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
@@ -123,22 +123,40 @@ public final class ClientTest {
}
@Test
- public void method_unknownMethod() {
+ public void method_invalidFormat() {
assertThrows(IllegalArgumentException.class, () -> client.method(CHANNEL_ID, ""));
assertThrows(IllegalArgumentException.class, () -> client.method(CHANNEL_ID, "one"));
assertThrows(IllegalArgumentException.class, () -> client.method(CHANNEL_ID, "hello"));
+ }
+
+ @Test
+ public void method_unknownService() {
assertThrows(
- IllegalArgumentException.class, () -> client.method(CHANNEL_ID, "abc.Service/Method"));
- assertThrows(IllegalArgumentException.class,
- () -> client.method(CHANNEL_ID, "pw.rpc.test1.TheTestService/NotAnRpc").method());
+ InvalidRpcServiceException.class, () -> client.method(CHANNEL_ID, "abc.Service/Method"));
+
+ Service service = new Service("throwaway.NotRealService",
+ Service.unaryMethod("NotAnRpc", SomeMessage.class, AnotherMessage.class));
+ assertThrows(InvalidRpcServiceException.class,
+ () -> client.method(CHANNEL_ID, service.method("NotAnRpc")));
+ }
+
+ @Test
+ public void method_unknownMethodInKnownService() {
+ assertThrows(InvalidRpcServiceMethodException.class,
+ () -> client.method(CHANNEL_ID, "pw.rpc.test1.TheTestService/NotAnRpc"));
+ assertThrows(InvalidRpcServiceMethodException.class,
+ () -> client.method(CHANNEL_ID, "pw.rpc.test1.TheTestService", "NotAnRpc"));
}
@Test
public void method_unknownChannel() {
- assertThrows(IllegalArgumentException.class,
- () -> client.method(0, "pw.rpc.test1.TheTestService/SomeUnary"));
- assertThrows(IllegalArgumentException.class,
- () -> client.method(999, "pw.rpc.test1.TheTestService/SomeUnary"));
+ MethodClient methodClient0 = client.method(0, "pw.rpc.test1.TheTestService/SomeUnary");
+ assertThrows(InvalidRpcChannelException.class,
+ () -> methodClient0.invokeUnary(SomeMessage.getDefaultInstance()));
+
+ MethodClient methodClient999 = client.method(999, "pw.rpc.test1.TheTestService/SomeUnary");
+ assertThrows(InvalidRpcChannelException.class,
+ () -> methodClient999.invokeUnary(SomeMessage.getDefaultInstance()));
}
@Test
@@ -178,13 +196,22 @@ public final class ClientTest {
}
@Test
+ public void method_accessFromMethodInstance() {
+ assertThat(client.method(CHANNEL_ID, UNARY_METHOD).method()).isSameInstanceAs(UNARY_METHOD);
+ assertThat(client.method(CHANNEL_ID, SERVER_STREAMING_METHOD).method())
+ .isSameInstanceAs(SERVER_STREAMING_METHOD);
+ assertThat(client.method(CHANNEL_ID, CLIENT_STREAMING_METHOD).method())
+ .isSameInstanceAs(CLIENT_STREAMING_METHOD);
+ }
+
+ @Test
public void processPacket_emptyPacket_isNotProcessed() {
assertThat(client.processPacket(new byte[] {})).isFalse();
}
@Test
public void processPacket_invalidPacket_isNotProcessed() {
- assertThat(client.processPacket("This is definitely not a packet!".getBytes(UTF_8))).isFalse();
+ assertThat(client.processPacket("\uffff\uffff\uffffNot a packet!".getBytes(UTF_8))).isFalse();
}
@Test
@@ -323,7 +350,7 @@ public final class ClientTest {
}
@Test
- @SuppressWarnings("unchecked") // No idea why, but this test causes "unchecked" warnings
+ @SuppressWarnings("unchecked")
public void streamObserverClient_create_invokeMethod() throws Exception {
Channel.Output mockChannelOutput = Mockito.mock(Channel.Output.class);
Client client = Client.create(ImmutableList.of(new Channel(1, mockChannelOutput)),
@@ -335,4 +362,59 @@ public final class ClientTest {
verify(mockChannelOutput)
.send(requestPacket("pw.rpc.test1.TheTestService", "SomeUnary", payload).toByteArray());
}
+
+ @Test
+ public void closeChannel_abortsExisting() throws Exception {
+ MethodClient serverStreamMethod =
+ client.method(CHANNEL_ID, "pw.rpc.test1.TheTestService", "SomeServerStreaming");
+
+ Call call1 = serverStreamMethod.invokeServerStreaming(REQUEST_PAYLOAD, observer);
+ Call call2 = client.method(CHANNEL_ID, "pw.rpc.test1.TheTestService", "SomeClientStreaming")
+ .invokeClientStreaming(observer);
+ assertThat(call1.active()).isTrue();
+ assertThat(call2.active()).isTrue();
+
+ assertThat(client.closeChannel(CHANNEL_ID)).isTrue();
+
+ assertThat(call1.active()).isFalse();
+ assertThat(call2.active()).isFalse();
+
+ verify(observer, times(2)).onError(Status.ABORTED);
+
+ assertThrows(InvalidRpcChannelException.class,
+ () -> serverStreamMethod.invokeServerStreaming(REQUEST_PAYLOAD, observer));
+ }
+
+ @Test
+ public void closeChannel_noCalls() {
+ assertThat(client.closeChannel(CHANNEL_ID)).isTrue();
+ }
+
+ @Test
+ public void closeChannel_knownChannel() {
+ assertThat(client.closeChannel(CHANNEL_ID + 100)).isFalse();
+ }
+
+ @Test
+ public void openChannel_uniqueChannel() throws Exception {
+ int newChannelId = CHANNEL_ID + 100;
+ Channel.Output channelOutput = Mockito.mock(Channel.Output.class);
+ client.openChannel(new Channel(newChannelId, channelOutput));
+
+ client.method(newChannelId, "pw.rpc.test1.TheTestService", "SomeUnary")
+ .invokeUnary(REQUEST_PAYLOAD, observer);
+
+ verify(channelOutput)
+ .send(requestPacket("pw.rpc.test1.TheTestService", "SomeUnary", REQUEST_PAYLOAD)
+ .toBuilder()
+ .setChannelId(newChannelId)
+ .build()
+ .toByteArray());
+ }
+
+ @Test
+ public void openChannel_alreadyExists_throwsException() {
+ assertThrows(InvalidRpcChannelException.class,
+ () -> client.openChannel(new Channel(CHANNEL_ID, packet -> {})));
+ }
}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java
new file mode 100644
index 000000000..91722ee70
--- /dev/null
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/EndpointTest.java
@@ -0,0 +1,209 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.MessageLite;
+import dev.pigweed.pw_rpc.internal.Packet.PacketType;
+import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public final class EndpointTest {
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
+ Service.unaryMethod("SomeUnary", SomeMessage.class, SomeMessage.class),
+ Service.serverStreamingMethod("SomeServerStreaming", SomeMessage.class, SomeMessage.class),
+ Service.clientStreamingMethod("SomeClientStreaming", SomeMessage.class, SomeMessage.class),
+ Service.bidirectionalStreamingMethod(
+ "SomeBidiStreaming", SomeMessage.class, SomeMessage.class));
+
+ private static final Method METHOD = SERVICE.method("SomeUnary");
+
+ private static final SomeMessage REQUEST_PAYLOAD =
+ SomeMessage.newBuilder().setMagicNumber(1337).build();
+ private static final byte[] REQUEST = request(REQUEST_PAYLOAD);
+ private static final AnotherMessage RESPONSE_PAYLOAD =
+ AnotherMessage.newBuilder().setPayload("hello").build();
+ private static final int CHANNEL_ID = 555;
+
+ @Mock private Channel.Output mockOutput;
+ @Mock private StreamObserver<MessageLite> callEvents;
+
+ private final Channel channel = new Channel(CHANNEL_ID, bytes -> mockOutput.send(bytes));
+ private final Endpoint endpoint = new Endpoint(ImmutableList.of(channel));
+
+ private static byte[] request(MessageLite payload) {
+ return packetBuilder()
+ .setType(PacketType.REQUEST)
+ .setPayload(payload.toByteString())
+ .build()
+ .toByteArray();
+ }
+
+ private static byte[] cancel() {
+ return packetBuilder()
+ .setType(PacketType.CLIENT_ERROR)
+ .setStatus(Status.CANCELLED.code())
+ .build()
+ .toByteArray();
+ }
+
+ private static RpcPacket.Builder packetBuilder() {
+ return RpcPacket.newBuilder()
+ .setChannelId(CHANNEL_ID)
+ .setServiceId(SERVICE.id())
+ .setMethodId(METHOD.id());
+ }
+
+ private AbstractCall<MessageLite, MessageLite> createCall(Endpoint endpoint, PendingRpc rpc) {
+ return StreamObserverCall.getFactory(callEvents).apply(endpoint, rpc);
+ }
+
+ @Test
+ public void start_succeeds_rpcIsPending() throws Exception {
+ AbstractCall<MessageLite, MessageLite> call =
+ endpoint.invokeRpc(CHANNEL_ID, METHOD, this::createCall, REQUEST_PAYLOAD);
+
+ verify(mockOutput).send(REQUEST);
+ assertThat(endpoint.abandon(call)).isTrue();
+ }
+
+ @Test
+ public void start_sendingFails_callsHandleError() throws Exception {
+ doThrow(new ChannelOutputException()).when(mockOutput).send(any());
+
+ assertThrows(ChannelOutputException.class,
+ () -> endpoint.invokeRpc(CHANNEL_ID, METHOD, this::createCall, REQUEST_PAYLOAD));
+
+ verify(mockOutput).send(REQUEST);
+ }
+
+ @Test
+ public void abandon_rpcNoLongerPending() throws Exception {
+ AbstractCall<MessageLite, MessageLite> call =
+ endpoint.invokeRpc(CHANNEL_ID, METHOD, this::createCall, REQUEST_PAYLOAD);
+ assertThat(endpoint.abandon(call)).isTrue();
+
+ assertThat(endpoint.abandon(call)).isFalse();
+ }
+
+ @Test
+ public void abandon_sendsNoPackets() throws Exception {
+ AbstractCall<MessageLite, MessageLite> call =
+ endpoint.invokeRpc(CHANNEL_ID, METHOD, this::createCall, REQUEST_PAYLOAD);
+ verify(mockOutput).send(REQUEST);
+ verifyNoMoreInteractions(mockOutput);
+
+ assertThat(endpoint.abandon(call)).isTrue();
+ }
+
+ @Test
+ public void cancel_rpcNoLongerPending() throws Exception {
+ AbstractCall<MessageLite, MessageLite> call =
+ endpoint.invokeRpc(CHANNEL_ID, METHOD, this::createCall, REQUEST_PAYLOAD);
+ assertThat(endpoint.cancel(call)).isTrue();
+
+ assertThat(endpoint.abandon(call)).isFalse();
+ }
+
+ @Test
+ public void cancel_sendsCancelPacket() throws Exception {
+ AbstractCall<MessageLite, MessageLite> call =
+ endpoint.invokeRpc(CHANNEL_ID, METHOD, this::createCall, REQUEST_PAYLOAD);
+ assertThat(endpoint.cancel(call)).isTrue();
+
+ verify(mockOutput).send(cancel());
+ }
+
+ @Test
+ public void open_sendsNoPacketsButRpcIsPending() {
+ AbstractCall<MessageLite, MessageLite> call =
+ endpoint.openRpc(CHANNEL_ID, METHOD, this::createCall);
+
+ assertThat(call.active()).isTrue();
+ assertThat(endpoint.abandon(call)).isTrue();
+ verifyNoInteractions(mockOutput);
+ }
+
+ @Test
+ public void ignoresActionsIfCallIsNotPending() throws Exception {
+ AbstractCall<MessageLite, MessageLite> call =
+ createCall(endpoint, PendingRpc.create(channel, METHOD));
+
+ assertThat(endpoint.cancel(call)).isFalse();
+ assertThat(endpoint.abandon(call)).isFalse();
+ assertThat(endpoint.clientStream(call, REQUEST_PAYLOAD)).isFalse();
+ assertThat(endpoint.clientStreamEnd(call)).isFalse();
+ }
+
+ @Test
+ public void ignoresPacketsIfCallIsNotPending() throws Exception {
+ AbstractCall<MessageLite, MessageLite> call =
+ createCall(endpoint, PendingRpc.create(channel, METHOD));
+
+ assertThat(endpoint.cancel(call)).isFalse();
+ assertThat(endpoint.abandon(call)).isFalse();
+
+ assertThat(endpoint.processClientPacket(call.rpc().method(),
+ packetBuilder()
+ .setType(PacketType.SERVER_STREAM)
+ .setPayload(RESPONSE_PAYLOAD.toByteString())
+ .build()))
+ .isTrue();
+ assertThat(endpoint.processClientPacket(call.rpc().method(),
+ packetBuilder()
+ .setType(PacketType.RESPONSE)
+ .setPayload(RESPONSE_PAYLOAD.toByteString())
+ .build()))
+ .isTrue();
+ assertThat(endpoint.processClientPacket(call.rpc().method(),
+ packetBuilder()
+ .setType(PacketType.SERVER_ERROR)
+ .setStatus(Status.ABORTED.code())
+ .build()))
+ .isTrue();
+
+ assertThat(endpoint.processClientPacket(call.rpc().method(),
+ packetBuilder()
+ .setType(PacketType.CLIENT_STREAM)
+ .setPayload(REQUEST_PAYLOAD.toByteString())
+ .build()))
+ .isTrue();
+ assertThat(endpoint.processClientPacket(call.rpc().method(),
+ packetBuilder().setType(PacketType.CLIENT_STREAM_END).build()))
+ .isTrue();
+ assertThat(endpoint.processClientPacket(call.rpc().method(),
+ packetBuilder()
+ .setType(PacketType.CLIENT_ERROR)
+ .setStatus(Status.ABORTED.code())
+ .build()))
+ .isTrue();
+
+ verifyNoInteractions(callEvents);
+ }
+}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java
new file mode 100644
index 000000000..a1f11997a
--- /dev/null
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/FutureCallTest.java
@@ -0,0 +1,217 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_rpc;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+import dev.pigweed.pw_rpc.Call.UnaryFuture;
+import dev.pigweed.pw_rpc.FutureCall.StreamResponseFuture;
+import dev.pigweed.pw_rpc.FutureCall.UnaryResponseFuture;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public final class FutureCallTest {
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
+ Service.unaryMethod("SomeUnary", SomeMessage.class, AnotherMessage.class),
+ Service.clientStreamingMethod("SomeClient", SomeMessage.class, AnotherMessage.class),
+ Service.bidirectionalStreamingMethod(
+ "SomeBidirectional", SomeMessage.class, AnotherMessage.class));
+ private static final Method METHOD = SERVICE.method("SomeUnary");
+ private static final int CHANNEL_ID = 555;
+
+ @Mock private Channel.Output mockOutput;
+
+ private final Channel channel = new Channel(CHANNEL_ID, packet -> mockOutput.send(packet));
+ private final Endpoint endpoint = new Endpoint(ImmutableList.of(channel));
+ private final PendingRpc rpc = PendingRpc.create(channel, METHOD);
+
+ @Test
+ public void unaryFuture_response_setsValue() throws Exception {
+ UnaryResponseFuture<SomeMessage, AnotherMessage> call = endpoint.invokeRpc(
+ CHANNEL_ID, METHOD, UnaryResponseFuture::new, SomeMessage.getDefaultInstance());
+
+ AnotherMessage response = AnotherMessage.newBuilder().setResultValue(1138).build();
+ call.handleUnaryCompleted(response.toByteString(), Status.CANCELLED);
+
+ assertThat(call.isDone()).isTrue();
+ assertThat(call.get()).isEqualTo(UnaryResult.create(response, Status.CANCELLED));
+ }
+
+ @Test
+ public void unaryFuture_serverError_setsException() throws Exception {
+ UnaryResponseFuture<SomeMessage, AnotherMessage> call = endpoint.invokeRpc(
+ CHANNEL_ID, METHOD, UnaryResponseFuture::new, SomeMessage.getDefaultInstance());
+
+ call.handleError(Status.NOT_FOUND);
+
+ assertThat(call.isDone()).isTrue();
+ ExecutionException exception = assertThrows(ExecutionException.class, call::get);
+ assertThat(exception).hasCauseThat().isInstanceOf(RpcError.class);
+
+ RpcError error = (RpcError) exception.getCause();
+ assertThat(error).isNotNull();
+ assertThat(error.rpc()).isEqualTo(rpc);
+ assertThat(error.status()).isEqualTo(Status.NOT_FOUND);
+ }
+
+ @Test
+ public void unaryFuture_cancelOnCall_cancelsTheCallAndFuture() throws Exception {
+ UnaryFuture<SomeMessage> call = endpoint.invokeRpc(
+ CHANNEL_ID, METHOD, UnaryResponseFuture::new, SomeMessage.getDefaultInstance());
+ assertThat(call.cancel()).isTrue();
+ assertThat(call.isCancelled()).isTrue();
+
+ ExecutionException exception = assertThrows(ExecutionException.class, call::get);
+ assertThat(exception).hasCauseThat().isInstanceOf(RpcError.class);
+
+ RpcError error = (RpcError) exception.getCause();
+ assertThat(error).isNotNull();
+ assertThat(error.rpc()).isEqualTo(rpc);
+ assertThat(error.status()).isEqualTo(Status.CANCELLED);
+ }
+
+ @Test
+ public void unaryFuture_cancelOnFuture_cancelsTheCallAndFuture() throws Exception {
+ UnaryFuture<SomeMessage> call = endpoint.invokeRpc(
+ CHANNEL_ID, METHOD, UnaryResponseFuture::new, SomeMessage.getDefaultInstance());
+ assertThat(call.cancel(true)).isTrue();
+ assertThat(call.isCancelled()).isTrue();
+
+ ExecutionException exception = assertThrows(ExecutionException.class, call::get);
+ assertThat(exception).hasCauseThat().isInstanceOf(RpcError.class);
+
+ RpcError error = (RpcError) exception.getCause();
+ assertThat(error).isNotNull();
+ assertThat(error.rpc()).isEqualTo(rpc);
+ assertThat(error.status()).isEqualTo(Status.CANCELLED);
+ }
+
+ @Test
+ public void unaryFuture_cancelOnFutureSendFails_cancelsTheCallAndFuture() throws Exception {
+ UnaryFuture<SomeMessage> call = endpoint.invokeRpc(
+ CHANNEL_ID, METHOD, UnaryResponseFuture::new, SomeMessage.getDefaultInstance());
+
+ doThrow(new ChannelOutputException()).when(mockOutput).send(any());
+
+ assertThat(call.cancel(true)).isTrue();
+ assertThat(call.isCancelled()).isTrue();
+
+ ExecutionException exception = assertThrows(ExecutionException.class, call::get);
+ assertThat(exception).hasCauseThat().isInstanceOf(RpcError.class);
+
+ RpcError error = (RpcError) exception.getCause();
+ assertThat(error).isNotNull();
+ assertThat(error.rpc()).isEqualTo(rpc);
+ assertThat(error.status()).isEqualTo(Status.CANCELLED);
+ }
+
+ @Test
+ public void unaryFuture_multipleResponses_setsException() throws Exception {
+ UnaryResponseFuture<SomeMessage, AnotherMessage> call = endpoint.invokeRpc(
+ CHANNEL_ID, METHOD, UnaryResponseFuture::new, SomeMessage.getDefaultInstance());
+
+ AnotherMessage response = AnotherMessage.newBuilder().setResultValue(1138).build();
+ call.doHandleNext(response);
+ call.handleUnaryCompleted(ByteString.EMPTY, Status.OK);
+
+ assertThat(call.isDone()).isTrue();
+ ExecutionException exception = assertThrows(ExecutionException.class, call::get);
+ assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class);
+ }
+
+ @Test
+ public void unaryFuture_addListener_calledOnCompletion() throws Exception {
+ UnaryResponseFuture<SomeMessage, AnotherMessage> call = endpoint.invokeRpc(
+ CHANNEL_ID, METHOD, UnaryResponseFuture::new, SomeMessage.getDefaultInstance());
+
+ Runnable listener = mock(Runnable.class);
+ call.addListener(listener, directExecutor());
+
+ AnotherMessage response = AnotherMessage.newBuilder().setResultValue(1138).build();
+ call.handleUnaryCompleted(response.toByteString(), Status.OK);
+
+ verify(listener, times(1)).run();
+ }
+
+ @Test
+ public void unaryFuture_exceptionDuringStart() throws Exception {
+ ChannelOutputException exceptionToThrow = new ChannelOutputException();
+ doThrow(exceptionToThrow).when(mockOutput).send(any());
+
+ UnaryResponseFuture<SomeMessage, AnotherMessage> call = endpoint.invokeRpc(
+ CHANNEL_ID, METHOD, UnaryResponseFuture::new, SomeMessage.getDefaultInstance());
+
+ assertThat(call.error()).isEqualTo(Status.ABORTED);
+ ExecutionException exception = assertThrows(ExecutionException.class, call::get);
+ assertThat(exception).hasCauseThat().isInstanceOf(ChannelOutputException.class);
+
+ assertThat(exception.getCause()).isSameInstanceAs(exceptionToThrow);
+ }
+
+ @Test
+ public void bidirectionalStreamingFuture_responses_setsValue() throws Exception {
+ List<AnotherMessage> responses = new ArrayList<>();
+ StreamResponseFuture<SomeMessage, AnotherMessage> call = endpoint.invokeRpc(CHANNEL_ID,
+ METHOD,
+ StreamResponseFuture.getFactory(responses::add),
+ SomeMessage.getDefaultInstance());
+
+ AnotherMessage message = AnotherMessage.newBuilder().setResultValue(1138).build();
+ call.doHandleNext(message);
+ call.doHandleNext(message);
+ assertThat(call.isDone()).isFalse();
+ call.handleStreamCompleted(Status.OK);
+
+ assertThat(call.isDone()).isTrue();
+ assertThat(call.get()).isEqualTo(Status.OK);
+ assertThat(responses).containsExactly(message, message);
+ }
+
+ @Test
+ public void bidirectionalStreamingFuture_serverError_setsException() throws Exception {
+ StreamResponseFuture<SomeMessage, AnotherMessage> call = endpoint.invokeRpc(CHANNEL_ID,
+ METHOD,
+ StreamResponseFuture.getFactory(msg -> {}),
+ SomeMessage.getDefaultInstance());
+
+ call.handleError(Status.NOT_FOUND);
+
+ assertThat(call.isDone()).isTrue();
+ ExecutionException exception = assertThrows(ExecutionException.class, call::get);
+ assertThat(exception).hasCauseThat().isInstanceOf(RpcError.class);
+
+ RpcError error = (RpcError) exception.getCause();
+ assertThat(error).isNotNull();
+ assertThat(error.rpc()).isEqualTo(rpc);
+ assertThat(error.status()).isEqualTo(Status.NOT_FOUND);
+ }
+}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/IdsTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/IdsTest.java
index 1440c6dbf..f8f99762b 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/IdsTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/IdsTest.java
@@ -17,7 +17,6 @@ package dev.pigweed.pw_rpc;
import static com.google.common.truth.Truth.assertThat;
import org.junit.Test;
-import org.junit.runner.RunWith;
public final class IdsTest {
@Test
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/PacketsTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/PacketsTest.java
index 6a1e771f8..5f75aacf1 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/PacketsTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/PacketsTest.java
@@ -20,14 +20,13 @@ import com.google.protobuf.ExtensionRegistryLite;
import dev.pigweed.pw_rpc.internal.Packet.PacketType;
import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
import org.junit.Test;
-import org.junit.runner.RunWith;
public final class PacketsTest {
private static final Service SERVICE =
new Service("Greetings", Service.unaryMethod("Hello", RpcPacket.class, RpcPacket.class));
private static final PendingRpc RPC =
- PendingRpc.create(new Channel(123, null), SERVICE, SERVICE.method("Hello"));
+ PendingRpc.create(new Channel(123, null), SERVICE.method("Hello"));
private static final RpcPacket PACKET = RpcPacket.newBuilder()
.setChannelId(123)
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/RpcManagerTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/RpcManagerTest.java
deleted file mode 100644
index f25c66283..000000000
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/RpcManagerTest.java
+++ /dev/null
@@ -1,149 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-package dev.pigweed.pw_rpc;
-
-import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-import com.google.protobuf.MessageLite;
-import dev.pigweed.pw_rpc.internal.Packet.PacketType;
-import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-public final class RpcManagerTest {
- @Rule public final MockitoRule mockito = MockitoJUnit.rule();
-
- private static final Service SERVICE = new Service("pw.rpc.test1.TheTestService",
- Service.unaryMethod("SomeUnary", SomeMessage.class, SomeMessage.class),
- Service.serverStreamingMethod("SomeServerStreaming", SomeMessage.class, SomeMessage.class),
- Service.clientStreamingMethod("SomeClientStreaming", SomeMessage.class, SomeMessage.class),
- Service.bidirectionalStreamingMethod(
- "SomeBidiStreaming", SomeMessage.class, SomeMessage.class));
-
- private static final Method METHOD = SERVICE.method("SomeUnary");
-
- private static final SomeMessage REQUEST_PAYLOAD =
- SomeMessage.newBuilder().setMagicNumber(1337).build();
- private static final byte[] REQUEST = request(REQUEST_PAYLOAD);
- private static final int CHANNEL_ID = 555;
-
- @Mock private Channel.Output mockOutput;
- @Mock private StreamObserverCall<MessageLite, MessageLite> call;
-
- private PendingRpc rpc;
- private RpcManager manager;
-
- @Before
- public void setup() {
- rpc = PendingRpc.create(new Channel(CHANNEL_ID, mockOutput), SERVICE, METHOD);
- manager = new RpcManager();
- }
-
- private static byte[] request(MessageLite payload) {
- return packetBuilder()
- .setType(PacketType.REQUEST)
- .setPayload(payload.toByteString())
- .build()
- .toByteArray();
- }
-
- private static byte[] cancel() {
- return packetBuilder()
- .setType(PacketType.CLIENT_ERROR)
- .setStatus(Status.CANCELLED.code())
- .build()
- .toByteArray();
- }
-
- private static RpcPacket.Builder packetBuilder() {
- return RpcPacket.newBuilder()
- .setChannelId(CHANNEL_ID)
- .setServiceId(SERVICE.id())
- .setMethodId(METHOD.id());
- }
-
- @Test
- public void start_sendingFails_rpcNotPending() throws Exception {
- doThrow(new ChannelOutputException()).when(mockOutput).send(any());
-
- assertThrows(ChannelOutputException.class, () -> manager.start(rpc, call, REQUEST_PAYLOAD));
-
- verify(mockOutput).send(REQUEST);
- assertThat(manager.getPending(rpc)).isNull();
- }
-
- @Test
- public void start_succeeds_rpcIsPending() throws Exception {
- assertThat(manager.start(rpc, call, REQUEST_PAYLOAD)).isNull();
-
- assertThat(manager.getPending(rpc)).isSameInstanceAs(call);
- }
-
- @Test
- public void startThenCancel_rpcNotPending() throws Exception {
- assertThat(manager.start(rpc, call, REQUEST_PAYLOAD)).isNull();
- assertThat(manager.cancel(rpc)).isSameInstanceAs(call);
-
- assertThat(manager.getPending(rpc)).isNull();
- }
-
- @Test
- public void startThenCancel_sendsCancelPacket() throws Exception {
- assertThat(manager.start(rpc, call, REQUEST_PAYLOAD)).isNull();
- assertThat(manager.cancel(rpc)).isEqualTo(call);
-
- verify(mockOutput).send(cancel());
- }
-
- @Test
- public void startThenClear_sendsNothing() throws Exception {
- verifyNoMoreInteractions(mockOutput);
-
- assertThat(manager.start(rpc, call, REQUEST_PAYLOAD)).isNull();
- assertThat(manager.clear(rpc)).isEqualTo(call);
- }
-
- @Test
- public void clear_notPending_returnsNull() {
- assertThat(manager.clear(rpc)).isNull();
- }
-
- @Test
- public void open_sendingFails_rpcIsPending() throws Exception {
- doThrow(new ChannelOutputException()).when(mockOutput).send(any());
-
- assertThat(manager.open(rpc, call, REQUEST_PAYLOAD)).isNull();
-
- verify(mockOutput).send(REQUEST);
- assertThat(manager.getPending(rpc)).isSameInstanceAs(call);
- }
-
- @Test
- public void open_success_rpcIsPending() {
- assertThat(manager.open(rpc, call, REQUEST_PAYLOAD)).isNull();
-
- assertThat(manager.getPending(rpc)).isSameInstanceAs(call);
- }
-}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
index f4eba688b..76d86c162 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverCallTest.java
@@ -15,22 +15,15 @@
package dev.pigweed.pw_rpc;
import static com.google.common.truth.Truth.assertThat;
-import static org.junit.Assert.assertThrows;
-import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import dev.pigweed.pw_rpc.StreamObserverCall.StreamResponseFuture;
-import dev.pigweed.pw_rpc.StreamObserverCall.UnaryResponseFuture;
+import com.google.common.collect.ImmutableList;
import dev.pigweed.pw_rpc.internal.Packet.PacketType;
import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -49,9 +42,9 @@ public final class StreamObserverCallTest {
@Mock private StreamObserver<AnotherMessage> observer;
@Mock private Channel.Output mockOutput;
- private final RpcManager rpcManager = new RpcManager();
+ private final Channel channel = new Channel(CHANNEL_ID, packet -> mockOutput.send(packet));
+ private final Endpoint endpoint = new Endpoint(ImmutableList.of(channel));
private StreamObserverCall<SomeMessage, AnotherMessage> streamObserverCall;
- private PendingRpc rpc;
private static byte[] cancel() {
return packetBuilder()
@@ -70,9 +63,8 @@ public final class StreamObserverCallTest {
@Before
public void createCall() throws Exception {
- rpc = PendingRpc.create(new Channel(CHANNEL_ID, mockOutput), SERVICE, METHOD);
- streamObserverCall = StreamObserverCall.start(rpcManager, rpc, observer, null);
- rpcManager.start(rpc, streamObserverCall, SomeMessage.getDefaultInstance());
+ streamObserverCall =
+ endpoint.invokeRpc(CHANNEL_ID, METHOD, StreamObserverCall.getFactory(observer), null);
}
@Test
@@ -100,9 +92,23 @@ public final class StreamObserverCallTest {
}
@Test
+ public void abandon_doesNotSendCancelPacket() throws Exception {
+ streamObserverCall.abandon();
+
+ verify(mockOutput, never()).send(cancel());
+ }
+
+ @Test
+ public void abandon_deactivates() {
+ streamObserverCall.abandon();
+
+ assertThat(streamObserverCall.active()).isFalse();
+ }
+
+ @Test
public void send_sendsClientStreamPacket() throws Exception {
SomeMessage request = SomeMessage.newBuilder().setMagicNumber(123).build();
- streamObserverCall.send(request);
+ streamObserverCall.write(request);
verify(mockOutput)
.send(packetBuilder()
@@ -116,9 +122,7 @@ public final class StreamObserverCallTest {
public void send_raisesExceptionIfClosed() throws Exception {
streamObserverCall.cancel();
- RpcError thrown = assertThrows(
- RpcError.class, () -> streamObserverCall.send(SomeMessage.getDefaultInstance()));
- assertThat(thrown.status()).isSameInstanceAs(Status.CANCELLED);
+ assertThat(streamObserverCall.write(SomeMessage.getDefaultInstance())).isFalse();
}
@Test
@@ -131,29 +135,21 @@ public final class StreamObserverCallTest {
@Test
public void onNext_callsObserverIfActive() {
- streamObserverCall.onNext(AnotherMessage.getDefaultInstance().toByteString());
+ streamObserverCall.handleNext(AnotherMessage.getDefaultInstance().toByteString());
verify(observer).onNext(AnotherMessage.getDefaultInstance());
}
@Test
- public void onNext_ignoresIfNotActive() throws Exception {
- streamObserverCall.cancel();
- streamObserverCall.onNext(AnotherMessage.getDefaultInstance().toByteString());
-
- verify(observer, never()).onNext(any());
- }
-
- @Test
public void callDispatcher_onCompleted_callsObserver() {
- streamObserverCall.onCompleted(Status.ABORTED);
+ streamObserverCall.handleStreamCompleted(Status.ABORTED);
verify(observer).onCompleted(Status.ABORTED);
}
@Test
public void callDispatcher_onCompleted_setsActiveAndStatus() {
- streamObserverCall.onCompleted(Status.ABORTED);
+ streamObserverCall.handleStreamCompleted(Status.ABORTED);
verify(observer).onCompleted(Status.ABORTED);
assertThat(streamObserverCall.active()).isFalse();
@@ -162,109 +158,17 @@ public final class StreamObserverCallTest {
@Test
public void callDispatcher_onError_callsObserver() {
- streamObserverCall.onError(Status.NOT_FOUND);
+ streamObserverCall.handleError(Status.NOT_FOUND);
verify(observer).onError(Status.NOT_FOUND);
}
@Test
public void callDispatcher_onError_deactivates() {
- streamObserverCall.onError(Status.ABORTED);
+ streamObserverCall.handleError(Status.ABORTED);
verify(observer).onError(Status.ABORTED);
assertThat(streamObserverCall.active()).isFalse();
assertThat(streamObserverCall.status()).isNull();
}
-
- @Test
- public void unaryFuture_response_setsValue() throws Exception {
- UnaryResponseFuture<SomeMessage, AnotherMessage> call =
- new UnaryResponseFuture<>(rpcManager, rpc, SomeMessage.getDefaultInstance());
-
- AnotherMessage response = AnotherMessage.newBuilder().setResultValue(1138).build();
- call.onNext(response);
- assertThat(call.isDone()).isFalse();
- call.onCompleted(Status.CANCELLED);
-
- assertThat(call.isDone()).isTrue();
- assertThat(call.get()).isEqualTo(UnaryResult.create(response, Status.CANCELLED));
- }
-
- @Test
- public void unaryFuture_serverError_setsException() throws Exception {
- UnaryResponseFuture<SomeMessage, AnotherMessage> call =
- new UnaryResponseFuture<>(rpcManager, rpc, SomeMessage.getDefaultInstance());
-
- call.onError(Status.NOT_FOUND);
-
- assertThat(call.isDone()).isTrue();
- ExecutionException exception = assertThrows(ExecutionException.class, call::get);
- assertThat(exception).hasCauseThat().isInstanceOf(RpcError.class);
-
- RpcError error = (RpcError) exception.getCause();
- assertThat(error).isNotNull();
- assertThat(error.rpc()).isEqualTo(rpc);
- assertThat(error.status()).isEqualTo(Status.NOT_FOUND);
- }
-
- @Test
- public void unaryFuture_noMessage_setsException() throws Exception {
- UnaryResponseFuture<SomeMessage, AnotherMessage> call =
- new UnaryResponseFuture<>(rpcManager, rpc, SomeMessage.getDefaultInstance());
-
- call.onCompleted(Status.OK);
-
- assertThat(call.isDone()).isTrue();
- ExecutionException exception = assertThrows(ExecutionException.class, call::get);
- assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class);
- }
-
- @Test
- public void unaryFuture_multipleResponses_setsException() throws Exception {
- UnaryResponseFuture<SomeMessage, AnotherMessage> call =
- new UnaryResponseFuture<>(rpcManager, rpc, SomeMessage.getDefaultInstance());
-
- AnotherMessage response = AnotherMessage.newBuilder().setResultValue(1138).build();
- call.onNext(response);
- call.onNext(response);
- call.onCompleted(Status.OK);
-
- assertThat(call.isDone()).isTrue();
- ExecutionException exception = assertThrows(ExecutionException.class, call::get);
- assertThat(exception).hasCauseThat().isInstanceOf(IllegalStateException.class);
- }
-
- @Test
- public void bidirectionalStreamingfuture_responses_setsValue() throws Exception {
- List<AnotherMessage> responses = new ArrayList<>();
- StreamResponseFuture<SomeMessage, AnotherMessage> call =
- new StreamResponseFuture<>(rpcManager, rpc, responses::add, null);
-
- AnotherMessage message = AnotherMessage.newBuilder().setResultValue(1138).build();
- call.onNext(message);
- call.onNext(message);
- assertThat(call.isDone()).isFalse();
- call.onCompleted(Status.OK);
-
- assertThat(call.isDone()).isTrue();
- assertThat(call.get()).isEqualTo(Status.OK);
- assertThat(responses).containsExactly(message, message);
- }
-
- @Test
- public void bidirectionalStreamingfuture_serverError_setsException() throws Exception {
- StreamResponseFuture<SomeMessage, AnotherMessage> call =
- new StreamResponseFuture<>(rpcManager, rpc, (msg) -> {}, null);
-
- call.onError(Status.NOT_FOUND);
-
- assertThat(call.isDone()).isTrue();
- ExecutionException exception = assertThrows(ExecutionException.class, call::get);
- assertThat(exception).hasCauseThat().isInstanceOf(RpcError.class);
-
- RpcError error = (RpcError) exception.getCause();
- assertThat(error).isNotNull();
- assertThat(error.rpc()).isEqualTo(rpc);
- assertThat(error.status()).isEqualTo(Status.NOT_FOUND);
- }
}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
index 55fd88aa5..36102f447 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/StreamObserverMethodClientTest.java
@@ -16,14 +16,18 @@ package dev.pigweed.pw_rpc;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
-import static org.mockito.Mockito.mock;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import com.google.common.collect.ImmutableList;
import com.google.protobuf.MessageLite;
+import dev.pigweed.pw_rpc.internal.Packet.PacketType;
+import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
@@ -36,22 +40,24 @@ public final class StreamObserverMethodClientTest {
Service.bidirectionalStreamingMethod(
"SomeBidirectionalStreaming", SomeMessage.class, AnotherMessage.class));
- private static final Channel CHANNEL = new Channel(1, (bytes) -> {});
-
- private static final PendingRpc UNARY_RPC =
- PendingRpc.create(CHANNEL, SERVICE, SERVICE.method("SomeUnary"));
- private static final PendingRpc SERVER_STREAMING_RPC =
- PendingRpc.create(CHANNEL, SERVICE, SERVICE.method("SomeServerStreaming"));
- private static final PendingRpc CLIENT_STREAMING_RPC =
- PendingRpc.create(CHANNEL, SERVICE, SERVICE.method("SomeClientStreaming"));
- private static final PendingRpc BIDIRECTIONAL_STREAMING_RPC =
- PendingRpc.create(CHANNEL, SERVICE, SERVICE.method("SomeBidirectionalStreaming"));
-
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Mock private StreamObserver<MessageLite> defaultObserver;
+ @Mock private StreamObserver<AnotherMessage> observer;
+ @Mock private Channel.Output channelOutput;
+
+ // Wrap Channel.Output since channelOutput will be null when the channel is initialized.
+ private final Channel channel = new Channel(1, bytes -> channelOutput.send(bytes));
- private final RpcManager rpcManager = new RpcManager();
+ private final PendingRpc unary_rpc = PendingRpc.create(channel, SERVICE.method("SomeUnary"));
+ private final PendingRpc server_streaming_rpc =
+ PendingRpc.create(channel, SERVICE.method("SomeServerStreaming"));
+ private final PendingRpc client_streaming_rpc =
+ PendingRpc.create(channel, SERVICE.method("SomeClientStreaming"));
+ private final PendingRpc bidirectional_streaming_rpc =
+ PendingRpc.create(channel, SERVICE.method("SomeBidirectionalStreaming"));
+
+ private final Client client = Client.create(ImmutableList.of(channel), ImmutableList.of(SERVICE));
private MethodClient unaryMethodClient;
private MethodClient serverStreamingMethodClient;
private MethodClient clientStreamingMethodClient;
@@ -59,33 +65,30 @@ public final class StreamObserverMethodClientTest {
@Before
public void createMethodClient() {
- unaryMethodClient = new MethodClient(rpcManager, UNARY_RPC, defaultObserver);
+ unaryMethodClient = new MethodClient(client, channel.id(), unary_rpc.method(), defaultObserver);
serverStreamingMethodClient =
- new MethodClient(rpcManager, SERVER_STREAMING_RPC, defaultObserver);
+ new MethodClient(client, channel.id(), server_streaming_rpc.method(), defaultObserver);
clientStreamingMethodClient =
- new MethodClient(rpcManager, CLIENT_STREAMING_RPC, defaultObserver);
- bidirectionalStreamingMethodClient =
- new MethodClient(rpcManager, BIDIRECTIONAL_STREAMING_RPC, defaultObserver);
+ new MethodClient(client, channel.id(), client_streaming_rpc.method(), defaultObserver);
+ bidirectionalStreamingMethodClient = new MethodClient(
+ client, channel.id(), bidirectional_streaming_rpc.method(), defaultObserver);
}
@Test
public void invokeWithNoObserver_usesDefaultObserver() throws Exception {
unaryMethodClient.invokeUnary(SomeMessage.getDefaultInstance());
AnotherMessage reply = AnotherMessage.newBuilder().setPayload("yo").build();
- rpcManager.getPending(UNARY_RPC).onNext(reply.toByteString());
+
+ assertThat(client.processPacket(responsePacket(unary_rpc, reply))).isTrue();
verify(defaultObserver).onNext(reply);
}
@Test
public void invoke_usesProvidedObserver() throws Exception {
- @SuppressWarnings("unchecked")
- StreamObserver<AnotherMessage> observer =
- (StreamObserver<AnotherMessage>) mock(StreamObserver.class);
-
unaryMethodClient.invokeUnary(SomeMessage.getDefaultInstance(), observer);
AnotherMessage reply = AnotherMessage.newBuilder().setPayload("yo").build();
- rpcManager.getPending(UNARY_RPC).onNext(reply.toByteString());
+ assertThat(client.processPacket(responsePacket(unary_rpc, reply))).isTrue();
verify(observer).onNext(reply);
}
@@ -93,75 +96,86 @@ public final class StreamObserverMethodClientTest {
@Test
public void invokeUnary_startsRpc() throws Exception {
Call call = unaryMethodClient.invokeUnary(SomeMessage.getDefaultInstance());
- assertThat(rpcManager.getPending(UNARY_RPC)).isSameInstanceAs(call);
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, times(1)).send(any());
}
@Test
- public void openUnary_startsRpc() {
- Call call = unaryMethodClient.openUnary(SomeMessage.getDefaultInstance(), defaultObserver);
- assertThat(rpcManager.getPending(UNARY_RPC)).isSameInstanceAs(call);
+ public void openUnary_startsRpc() throws Exception {
+ Call call = unaryMethodClient.openUnary(defaultObserver);
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, never()).send(any());
}
@Test
public void invokeServerStreaming_startsRpc() throws Exception {
Call call = serverStreamingMethodClient.invokeServerStreaming(SomeMessage.getDefaultInstance());
- assertThat(rpcManager.getPending(SERVER_STREAMING_RPC)).isSameInstanceAs(call);
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, times(1)).send(any());
}
@Test
- public void openServerStreaming_startsRpc() {
- Call call = serverStreamingMethodClient.openServerStreaming(
- SomeMessage.getDefaultInstance(), defaultObserver);
- assertThat(rpcManager.getPending(SERVER_STREAMING_RPC)).isSameInstanceAs(call);
+ public void openServerStreaming_startsRpc() throws Exception {
+ Call call = serverStreamingMethodClient.openServerStreaming(defaultObserver);
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, never()).send(any());
}
@Test
public void invokeClientStreaming_startsRpc() throws Exception {
Call call = clientStreamingMethodClient.invokeClientStreaming();
- assertThat(rpcManager.getPending(CLIENT_STREAMING_RPC)).isSameInstanceAs(call);
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, times(1)).send(any());
}
@Test
- public void openClientStreaming_startsRpc() {
+ public void openClientStreaming_startsRpc() throws Exception {
Call call = clientStreamingMethodClient.openClientStreaming(defaultObserver);
- assertThat(rpcManager.getPending(CLIENT_STREAMING_RPC)).isSameInstanceAs(call);
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, never()).send(any());
}
@Test
public void invokeBidirectionalStreaming_startsRpc() throws Exception {
Call call = bidirectionalStreamingMethodClient.invokeBidirectionalStreaming();
- assertThat(rpcManager.getPending(BIDIRECTIONAL_STREAMING_RPC)).isSameInstanceAs(call);
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, times(1)).send(any());
}
@Test
- public void openBidirectionalStreaming_startsRpc() {
+ public void openBidirectionalStreaming_startsRpc() throws Exception {
Call call = bidirectionalStreamingMethodClient.openBidirectionalStreaming(defaultObserver);
- assertThat(rpcManager.getPending(BIDIRECTIONAL_STREAMING_RPC)).isSameInstanceAs(call);
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, never()).send(any());
}
@Test
- public void invokeUnaryFuture_startsRpc() {
- unaryMethodClient.invokeUnaryFuture(SomeMessage.getDefaultInstance());
- assertThat(rpcManager.getPending(UNARY_RPC)).isNotNull();
+ public void invokeUnaryFuture_startsRpc() throws Exception {
+ Call call = unaryMethodClient.invokeUnaryFuture(SomeMessage.getDefaultInstance());
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, times(1)).send(any());
}
@Test
- public void invokeServerStreamingFuture_startsRpc() {
- serverStreamingMethodClient.invokeServerStreamingFuture(
+ public void invokeServerStreamingFuture_startsRpc() throws Exception {
+ Call call = serverStreamingMethodClient.invokeServerStreamingFuture(
SomeMessage.getDefaultInstance(), (msg) -> {});
- assertThat(rpcManager.getPending(SERVER_STREAMING_RPC)).isNotNull();
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, times(1)).send(any());
}
@Test
- public void invokeClientStreamingFuture_startsRpc() {
- clientStreamingMethodClient.invokeClientStreamingFuture();
- assertThat(rpcManager.getPending(CLIENT_STREAMING_RPC)).isNotNull();
+ public void invokeClientStreamingFuture_startsRpc() throws Exception {
+ Call call = clientStreamingMethodClient.invokeClientStreamingFuture();
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, times(1)).send(any());
}
@Test
- public void invokeBidirectionalStreamingFuture_startsRpc() {
- bidirectionalStreamingMethodClient.invokeBidirectionalStreamingFuture((msg) -> {});
- assertThat(rpcManager.getPending(BIDIRECTIONAL_STREAMING_RPC)).isNotNull();
+ public void invokeBidirectionalStreamingFuture_startsRpc() throws Exception {
+ Call call = bidirectionalStreamingMethodClient.invokeBidirectionalStreamingFuture((msg) -> {});
+ assertThat(call.active()).isTrue();
+ verify(channelOutput, times(1)).send(any());
}
@Test
@@ -187,4 +201,50 @@ public final class StreamObserverMethodClientTest {
assertThrows(UnsupportedOperationException.class,
() -> clientStreamingMethodClient.invokeBidirectionalStreaming());
}
+
+ @Test
+ public void invalidChannel_throwsException() {
+ MethodClient methodClient =
+ new MethodClient(client, 999, client_streaming_rpc.method(), defaultObserver);
+ assertThrows(InvalidRpcChannelException.class, methodClient::invokeClientStreaming);
+ }
+
+ @Test
+ public void invalidService_throwsException() {
+ Service otherService = new Service("something.Else",
+ Service.clientStreamingMethod("ClientStream", SomeMessage.class, AnotherMessage.class));
+
+ MethodClient methodClient = new MethodClient(
+ client, channel.id(), otherService.method("ClientStream"), defaultObserver);
+ assertThrows(InvalidRpcServiceException.class, methodClient::invokeClientStreaming);
+ }
+
+ @Test
+ public void invalidMethod_throwsException() {
+ Service serviceWithDifferentUnaryMethod = new Service("pw.rpc.test1.TheTestService",
+ Service.unaryMethod("SomeUnary", AnotherMessage.class, AnotherMessage.class),
+ Service.serverStreamingMethod(
+ "SomeServerStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.clientStreamingMethod(
+ "SomeClientStreaming", SomeMessage.class, AnotherMessage.class),
+ Service.bidirectionalStreamingMethod(
+ "SomeBidirectionalStreaming", SomeMessage.class, AnotherMessage.class));
+
+ MethodClient methodClient = new MethodClient(
+ client, 999, serviceWithDifferentUnaryMethod.method("SomeUnary"), defaultObserver);
+ assertThrows(InvalidRpcServiceMethodException.class,
+ () -> methodClient.invokeUnary(AnotherMessage.getDefaultInstance()));
+ }
+
+ private static byte[] responsePacket(PendingRpc rpc, MessageLite payload) {
+ return RpcPacket.newBuilder()
+ .setChannelId(1)
+ .setServiceId(rpc.service().id())
+ .setMethodId(rpc.method().id())
+ .setType(PacketType.RESPONSE)
+ .setStatus(Status.OK.code())
+ .setPayload(payload.toByteString())
+ .build()
+ .toByteArray();
+ }
}
diff --git a/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java b/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
index 6688e2422..a58d51901 100644
--- a/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
+++ b/pw_rpc/java/test/dev/pigweed/pw_rpc/TestClient.java
@@ -14,17 +14,19 @@
package dev.pigweed.pw_rpc;
-import static java.util.Arrays.stream;
-
import com.google.common.collect.ImmutableList;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
+import com.google.protobuf.MessageLiteOrBuilder;
import dev.pigweed.pw_rpc.internal.Packet.PacketType;
import dev.pigweed.pw_rpc.internal.Packet.RpcPacket;
+import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
+import java.util.Queue;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@@ -38,29 +40,35 @@ public class TestClient {
private final Client client;
private final List<RpcPacket> sentPackets = new ArrayList<>();
- private final List<RpcPacket> enqueuedPackets = new ArrayList<>();
- private int receiveEnqueuedPacketsAfter = 1;
- final Map<PacketType, Integer> sentPayloadIndices = new EnumMap<>(PacketType.class);
+ private final Queue<EnqueuedPackets> enqueuedPackets = new ArrayDeque<>();
+ private final Map<PacketType, Integer> sentPayloadIndices = new EnumMap<>(PacketType.class);
+
+ @Nullable private ChannelOutputException channelOutputException = null;
+
+ private static class EnqueuedPackets {
+ private int processAfterSentPackets;
+ private final List<RpcPacket> packets;
- @Nullable ChannelOutputException channelOutputException = null;
+ private EnqueuedPackets(int processAfterSentPackets, List<RpcPacket> packets) {
+ this.processAfterSentPackets = processAfterSentPackets;
+ this.packets = packets;
+ }
+
+ private boolean shouldProcessEnqueuedPackets() {
+ return processAfterSentPackets-- <= 1;
+ }
+ }
public TestClient(List<Service> services) {
Channel.Output channelOutput = packet -> {
- if (channelOutputException == null) {
- sentPackets.add(parsePacket(packet));
- } else {
+ if (channelOutputException != null) {
throw channelOutputException;
}
+ sentPackets.add(parsePacket(packet));
- // Process any enqueued packets.
- if (receiveEnqueuedPacketsAfter > 1) {
- receiveEnqueuedPacketsAfter -= 1;
- return;
- }
- if (!enqueuedPackets.isEmpty()) {
- List<RpcPacket> packetsToProcess = new ArrayList<>(enqueuedPackets);
- enqueuedPackets.clear();
- packetsToProcess.forEach(this::processPacket);
+ if (!enqueuedPackets.isEmpty() && enqueuedPackets.peek().shouldProcessEnqueuedPackets()) {
+ // Process any enqueued packets.
+ enqueuedPackets.remove().packets.forEach(this::processPacket);
}
};
client = Client.create(ImmutableList.of(new Channel(CHANNEL_ID, channelOutput)), services);
@@ -86,52 +94,34 @@ public class TestClient {
}
/** Simulates receiving SERVER_STREAM packets from the server. */
- public void receiveServerStream(String service, String method, MessageLite... payloads) {
+ public void receiveServerStream(String service, String method, MessageLiteOrBuilder... payloads) {
RpcPacket base = startPacket(service, method, PacketType.SERVER_STREAM).build();
- for (MessageLite payload : payloads) {
- processPacket(RpcPacket.newBuilder(base).setPayload(payload.toByteString()));
+ for (MessageLiteOrBuilder payload : payloads) {
+ processPacket(RpcPacket.newBuilder(base).setPayload(getMessage(payload).toByteString()));
}
}
- public void receiveServerStream(String service, String method, MessageLite.Builder... builders) {
- receiveServerStream(service,
- method,
- stream(builders).map(MessageLite.Builder::build).toArray(MessageLite[] ::new));
- }
-
/**
* Enqueues a SERVER_STREAM packet so that the client receives it after a packet is sent.
*
+ * This function may be called multiple times to create a queue of packets to process as different
+ * packets are sent.
+ *
* @param afterPackets Wait until this many packets have been sent before the client receives
- * these stream packets. The minimum value (and the default) is 1.
+ * these stream packets. The minimum value is 1. If multiple stream packets are queued,
+ * afterPackets is counted from the packet before it in the queue.
*/
public void enqueueServerStream(
- String service, String method, int afterPackets, MessageLite... payloads) {
+ String service, String method, int afterPackets, MessageLiteOrBuilder... payloads) {
if (afterPackets < 1) {
throw new IllegalArgumentException("afterPackets must be at least 1");
}
- if (afterPackets != 1 && receiveEnqueuedPacketsAfter != 1) {
- throw new AssertionError(
- "May only set afterPackets once before enqueued packets are processed");
- }
- receiveEnqueuedPacketsAfter = afterPackets;
RpcPacket base = startPacket(service, method, PacketType.SERVER_STREAM).build();
- for (MessageLite payload : payloads) {
- enqueuedPackets.add(RpcPacket.newBuilder(base).setPayload(payload.toByteString()).build());
- }
- }
-
- public void enqueueServerStream(String service, String method, MessageLite... payloads) {
- enqueueServerStream(service, method, 1, payloads);
- }
-
- public void enqueueServerStream(
- String service, String method, int afterPackets, MessageLite.Builder... builders) {
- enqueueServerStream(service,
- method,
- afterPackets,
- stream(builders).map(MessageLite.Builder::build).toArray(MessageLite[] ::new));
+ enqueuedPackets.add(new EnqueuedPackets(afterPackets,
+ Arrays.stream(payloads)
+ .map(m -> RpcPacket.newBuilder(base).setPayload(getMessage(m).toByteString()).build())
+ .collect(Collectors.toList())));
}
/** Simulates receiving a SERVER_ERROR packet from the server. */
@@ -192,4 +182,14 @@ public class TestClient {
throw new AssertionError("Decoding sent packet payload failed", e);
}
}
+
+ private MessageLite getMessage(MessageLiteOrBuilder messageOrBuilder) {
+ if (messageOrBuilder instanceof MessageLite.Builder) {
+ return ((MessageLite.Builder) messageOrBuilder).build();
+ }
+ if (messageOrBuilder instanceof MessageLite) {
+ return (MessageLite) messageOrBuilder;
+ }
+ throw new AssertionError("Unexpected MessageLiteOrBuilder class");
+ }
}
diff --git a/pw_rpc/method_test.cc b/pw_rpc/method_test.cc
index 597af2f5c..b1403d8b0 100644
--- a/pw_rpc/method_test.cc
+++ b/pw_rpc/method_test.cc
@@ -56,7 +56,7 @@ TEST(Method, Invoke) {
} channel_output;
Channel channel(123, &channel_output);
- Server server(std::span(static_cast<rpc::Channel*>(&channel), 1));
+ Server server(span(static_cast<rpc::Channel*>(&channel), 1));
TestService service;
const CallContext context(server, channel.id(), service, kTestMethod, 0);
diff --git a/pw_rpc/nanopb/Android.bp b/pw_rpc/nanopb/Android.bp
new file mode 100644
index 000000000..d1f749c4c
--- /dev/null
+++ b/pw_rpc/nanopb/Android.bp
@@ -0,0 +1,84 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+filegroup {
+ name: "pw_rpc_nanopb_src_files",
+ srcs: [
+ "common.cc",
+ "method.cc",
+ "server_reader_writer.cc",
+ ],
+}
+
+cc_library_headers {
+ name: "pw_rpc_nanopb_include_dirs",
+ export_include_dirs: [
+ "public",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+// This rule must be instantiated, i.e.
+//
+// cc_library_static {
+// name: "pw_rpc_nanopb_<instance_name>",
+// defaults: [
+// "pw_rpc_cflags_<instance_name>",
+// "pw_rpc_nanopb_defaults",
+// ],
+// static_libs: [
+// "pw_rpc_raw_<instance_name>",
+// "pw_rpc_<instance_name>",
+// ],
+// export_static_lib_headers: [
+// "pw_rpc_raw_<instance_name>",
+// "pw_rpc_<instance_name>",
+// ],
+// }
+//
+// where pw_rpc_cflags_<instance_name> defines your flags, i.e.
+//
+// cc_defaults {
+// name: "pw_rpc_cflags_<instance_name>",
+// cflags: [
+// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
+// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_DYNAMIC_ALLOCATION",
+// ],
+// }
+//
+// see pw_rpc_defaults, pw_rpc_raw_defaults
+cc_defaults {
+ name: "pw_rpc_nanopb_defaults",
+ cpp_std: "c++2a",
+ static_libs: [
+ "libprotobuf-c-nano",
+ ],
+ header_libs: [
+ "pw_rpc_nanopb_include_dirs",
+ ],
+ export_header_lib_headers: [
+ "pw_rpc_nanopb_include_dirs",
+ ],
+ srcs: [
+ ":pw_rpc_nanopb_src_files",
+ ],
+ host_supported: true,
+ vendor_available: true,
+} \ No newline at end of file
diff --git a/pw_rpc/nanopb/BUILD.bazel b/pw_rpc/nanopb/BUILD.bazel
index f5a87f6ea..32c0573bb 100644
--- a/pw_rpc/nanopb/BUILD.bazel
+++ b/pw_rpc/nanopb/BUILD.bazel
@@ -17,6 +17,10 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load(
+ "//pw_build:selects.bzl",
+ "TARGET_COMPATIBLE_WITH_HOST_SELECT",
+)
package(default_visibility = ["//visibility:public"])
@@ -42,9 +46,7 @@ pw_cc_library(
pw_cc_library(
name = "client_api",
- hdrs = [
- "public/pw_rpc/nanopb/client_reader_writer.h",
- ],
+ hdrs = ["public/pw_rpc/nanopb/client_reader_writer.h"],
includes = ["public"],
deps = [
":common",
@@ -92,6 +94,30 @@ pw_cc_library(
)
pw_cc_library(
+ name = "client_server_testing",
+ hdrs = [
+ "public/pw_rpc/nanopb/client_server_testing.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:client_server_testing",
+ ],
+)
+
+pw_cc_library(
+ name = "client_server_testing_threaded",
+ hdrs = [
+ "public/pw_rpc/nanopb/client_server_testing_threaded.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:client_server_testing_threaded",
+ ],
+)
+
+pw_cc_library(
name = "internal_test_utils",
hdrs = ["pw_rpc_nanopb_private/internal_test_utils.h"],
deps = ["//pw_rpc:internal_test_utils"],
@@ -105,7 +131,7 @@ pw_cc_library(
],
)
-# TODO(pwbug/507): Enable this library when logging_event_handler can be used.
+# TODO(b/242059613): Enable this library when logging_event_handler can be used.
filegroup(
name = "client_integration_test",
srcs = [
@@ -144,6 +170,34 @@ pw_cc_test(
)
pw_cc_test(
+ name = "client_server_context_test",
+ srcs = [
+ "client_server_context_test.cc",
+ ],
+ deps = [
+ ":client_api",
+ ":client_server_testing",
+ "//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "client_server_context_threaded_test",
+ srcs = [
+ "client_server_context_threaded_test.cc",
+ ],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":client_api",
+ ":client_server_testing_threaded",
+ "//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
+ "//pw_sync:binary_semaphore",
+ "//pw_thread:test_threads_header",
+ "//pw_thread_stl:test_threads",
+ ],
+)
+
+pw_cc_test(
name = "codegen_test",
srcs = [
"codegen_test.cc",
@@ -175,6 +229,7 @@ pw_cc_test(
deps = [
":internal_test_utils",
":server_api",
+ "//pw_containers",
"//pw_rpc",
"//pw_rpc:internal_test_utils",
"//pw_rpc:pw_rpc_test_cc.nanopb",
@@ -212,7 +267,7 @@ pw_cc_test(
],
)
-# TODO(pwbug/628): Requires nanopb options file support to compile.
+# TODO(b/234874064): Requires nanopb options file support to compile.
filegroup(
name = "echo_service_test",
srcs = ["echo_service_test.cc"],
@@ -260,3 +315,16 @@ pw_cc_test(
"//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
],
)
+
+pw_cc_test(
+ name = "synchronous_call_test",
+ srcs = ["synchronous_call_test.cc"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:pw_rpc_test_cc.nanopb_rpc",
+ "//pw_rpc:synchronous_client_api",
+ "//pw_work_queue",
+ "//pw_work_queue:stl_test_thread",
+ "//pw_work_queue:test_thread_header",
+ ],
+)
diff --git a/pw_rpc/nanopb/BUILD.gn b/pw_rpc/nanopb/BUILD.gn
index a2ec00126..d04fa8923 100644
--- a/pw_rpc/nanopb/BUILD.gn
+++ b/pw_rpc/nanopb/BUILD.gn
@@ -16,7 +16,9 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
import("$dir_pw_third_party/nanopb/nanopb.gni")
+import("$dir_pw_thread/backend.gni")
import("$dir_pw_unit_test/test.gni")
config("public") {
@@ -41,6 +43,7 @@ pw_source_set("server_api") {
"..:config",
"..:server",
dir_pw_bytes,
+ dir_pw_span,
]
deps = [
"..:log_config",
@@ -101,9 +104,25 @@ pw_source_set("client_testing") {
]
}
+pw_source_set("client_server_testing") {
+ public = [ "public/pw_rpc/nanopb/client_server_testing.h" ]
+ public_deps = [
+ ":test_method_context",
+ "..:client_server_testing",
+ ]
+}
+
+pw_source_set("client_server_testing_threaded") {
+ public = [ "public/pw_rpc/nanopb/client_server_testing_threaded.h" ]
+ public_deps = [
+ ":test_method_context",
+ "..:client_server_testing_threaded",
+ ]
+}
+
pw_source_set("internal_test_utils") {
public = [ "pw_rpc_nanopb_private/internal_test_utils.h" ]
- public_deps = []
+ public_deps = [ dir_pw_span ]
if (dir_pw_third_party_nanopb != "") {
public_deps += [ "$dir_pw_third_party/nanopb" ]
}
@@ -135,6 +154,8 @@ pw_test_group("tests") {
tests = [
":client_call_test",
":client_reader_writer_test",
+ ":client_server_context_test",
+ ":client_server_context_threaded_test",
":codegen_test",
":echo_service_test",
":fake_channel_output_test",
@@ -146,6 +167,7 @@ pw_test_group("tests") {
":server_reader_writer_test",
":serde_test",
":stub_generation_test",
+ ":synchronous_call_test",
]
}
@@ -170,6 +192,34 @@ pw_test("client_reader_writer_test") {
enable_if = dir_pw_third_party_nanopb != ""
}
+pw_test("client_server_context_test") {
+ deps = [
+ ":client_api",
+ ":client_server_testing",
+ "..:test_protos.nanopb_rpc",
+ ]
+ sources = [ "client_server_context_test.cc" ]
+ enable_if = dir_pw_third_party_nanopb != ""
+}
+
+_stl_threading_and_nanopb_enabled =
+ pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread" &&
+ pw_sync_BINARY_SEMAPHORE_BACKEND != "" && pw_sync_MUTEX_BACKEND != "" &&
+ dir_pw_third_party_nanopb != ""
+
+pw_test("client_server_context_threaded_test") {
+ deps = [
+ ":client_api",
+ ":client_server_testing_threaded",
+ "$dir_pw_sync:binary_semaphore",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread_stl:test_threads",
+ "..:test_protos.nanopb_rpc",
+ ]
+ sources = [ "client_server_context_threaded_test.cc" ]
+ enable_if = _stl_threading_and_nanopb_enabled
+}
+
pw_test("codegen_test") {
deps = [
":client_api",
@@ -197,7 +247,7 @@ pw_test("method_test") {
deps = [
":internal_test_utils",
":server_api",
- ":server_api",
+ "$dir_pw_containers",
"..:test_protos.nanopb",
"..:test_utils",
]
@@ -282,3 +332,17 @@ pw_test("stub_generation_test") {
sources = [ "stub_generation_test.cc" ]
enable_if = dir_pw_third_party_nanopb != ""
}
+
+pw_test("synchronous_call_test") {
+ deps = [
+ ":test_method_context",
+ "$dir_pw_work_queue:pw_work_queue",
+ "$dir_pw_work_queue:stl_test_thread",
+ "$dir_pw_work_queue:test_thread",
+ "..:synchronous_client_api",
+ "..:test_protos.nanopb_rpc",
+ ]
+ sources = [ "synchronous_call_test.cc" ]
+ enable_if = dir_pw_third_party_nanopb != "" &&
+ pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != ""
+}
diff --git a/pw_rpc/nanopb/CMakeLists.txt b/pw_rpc/nanopb/CMakeLists.txt
index 2b5d36cf7..a3a73e5db 100644
--- a/pw_rpc/nanopb/CMakeLists.txt
+++ b/pw_rpc/nanopb/CMakeLists.txt
@@ -14,56 +14,131 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_rpc.nanopb.method
- SOURCES
- method.cc
- server_reader_writer.cc
+pw_add_library(pw_rpc.nanopb.server_api STATIC
+ HEADERS
+ public/pw_rpc/nanopb/internal/method.h
+ public/pw_rpc/nanopb/internal/method_union.h
+ public/pw_rpc/nanopb/server_reader_writer.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
+ pw_bytes
+ pw_rpc.config
pw_rpc.nanopb.common
+ pw_rpc.raw.server_api
pw_rpc.server
+ pw_span
+ SOURCES
+ method.cc
+ server_reader_writer.cc
PRIVATE_DEPS
pw_log
+ pw_rpc.log_config
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_METHOD)
zephyr_link_libraries(pw_rpc.nanopb.method)
endif()
-pw_add_module_library(pw_rpc.nanopb.method_union
+# TODO(hepler): Deprecate this once no one depends on it.
+pw_add_library(pw_rpc.nanopb.method_union INTERFACE
PUBLIC_DEPS
- pw_rpc.nanopb.method
- pw_rpc.raw
- pw_rpc.server
- PRIVATE_DEPS
- pw_log
+ pw_rpc.nanopb.server_api
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_METHOD_UNION)
zephyr_link_libraries(pw_rpc.nanopb.method_union)
endif()
-pw_add_module_library(pw_rpc.nanopb.client
+pw_add_library(pw_rpc.nanopb.client_api INTERFACE
+ HEADERS
+ public/pw_rpc/nanopb/client_reader_writer.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_function
+ pw_rpc.client
pw_rpc.nanopb.common
- pw_rpc.common
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_CLIENT)
- zephyr_link_libraries(pw_rpc.nanopb.client)
+ zephyr_link_libraries(pw_rpc.nanopb.client_api)
endif()
-pw_add_module_library(pw_rpc.nanopb.common
- SOURCES
- common.cc
+pw_add_library(pw_rpc.nanopb.common STATIC
+ HEADERS
+ public/pw_rpc/nanopb/internal/common.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_bytes
- pw_log
pw_rpc.common
pw_third_party.nanopb
+ SOURCES
+ common.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_rpc.client
+ pw_rpc.log_config
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_COMMON)
zephyr_link_libraries(pw_rpc.nanopb.common)
endif()
-pw_add_module_library(pw_rpc.nanopb.echo_service
+pw_add_library(pw_rpc.nanopb.test_method_context INTERFACE
+ HEADERS
+ public/pw_rpc/nanopb/fake_channel_output.h
+ public/pw_rpc/nanopb/test_method_context.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_containers
+ pw_rpc.nanopb.server_api
+ pw_rpc.test_utils
+)
+
+pw_add_library(pw_rpc.nanopb.client_testing INTERFACE
+ HEADERS
+ public/pw_rpc/nanopb/client_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.client
+ pw_rpc.raw.client_testing
+)
+
+pw_add_library(pw_rpc.nanopb.client_server_testing INTERFACE
+ HEADERS
+ public/pw_rpc/nanopb/client_server_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.client_server_testing
+)
+
+pw_add_library(pw_rpc.nanopb.client_server_testing_threaded INTERFACE
+ HEADERS
+ public/pw_rpc/nanopb/client_server_testing_threaded.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.client_server_testing_threaded
+)
+
+pw_add_library(pw_rpc.nanopb.internal_test_utils INTERFACE
+ HEADERS
+ pw_rpc_nanopb_private/internal_test_utils.h
+ PUBLIC_DEPS
+ pw_span
+ pw_third_party.nanopb
+)
+
+pw_add_library(pw_rpc.nanopb.echo_service INTERFACE
+ HEADERS
+ public/pw_rpc/echo_service_nanopb.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_rpc.protos.nanopb_rpc
)
@@ -71,13 +146,228 @@ if(Zephyr_FOUND AND CONFIG_PIGWEED_RPC_NANOPB_ECHO_SERVICE)
zephyr_link_libraries(pw_rpc.nanopb.echo_service)
endif()
-pw_auto_add_module_tests(pw_rpc.nanopb
+pw_add_library(pw_rpc.nanopb.client_integration_test STATIC
+ SOURCES
+ client_integration_test.cc
PRIVATE_DEPS
- pw_rpc.client
- pw_rpc.raw
- pw_rpc.server
- pw_rpc.nanopb.common
+ pw_assert
+ pw_rpc.integration_testing
pw_rpc.protos.nanopb_rpc
+ pw_sync.binary_semaphore
+ pw_unit_test
+)
+
+pw_add_test(pw_rpc.nanopb.client_call_test
+ SOURCES
+ client_call_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.client_api
+ pw_rpc.nanopb.internal_test_utils
+ pw_rpc.test_protos.nanopb
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.client_reader_writer_test
+ SOURCES
+ client_reader_writer_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.client_api
+ pw_rpc.nanopb.client_testing
pw_rpc.test_protos.nanopb_rpc
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.client_server_context_test
+ SOURCES
+ client_server_context_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.client_api
+ pw_rpc.nanopb.client_server_testing
+ pw_rpc.test_protos.nanopb_rpc
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
+ (NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "") AND
+ (NOT "${pw_sync.mutex_BACKEND}" STREQUAL ""))
+ pw_add_test(pw_rpc.nanopb.client_server_context_threaded_test
+ SOURCES
+ client_server_context_threaded_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.client_api
+ pw_rpc.nanopb.client_server_testing_threaded
+ pw_rpc.test_protos.nanopb_rpc
+ pw_sync.binary_semaphore
+ pw_thread.test_threads
+ pw_thread.thread
+ pw_thread_stl.test_threads
+ GROUPS
+ modules
+ pw_rpc.nanopb
+ )
+endif()
+
+pw_add_test(pw_rpc.nanopb.codegen_test
+ SOURCES
+ codegen_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.client_api
+ pw_rpc.nanopb.internal_test_utils
+ pw_rpc.nanopb.server_api
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.test_protos.nanopb_rpc
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.fake_channel_output_test
+ SOURCES
+ fake_channel_output_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.server_api
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.test_protos.nanopb_rpc
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.method_test
+ SOURCES
+ method_test.cc
+ PRIVATE_DEPS
+ pw_containers
+ pw_rpc.nanopb.internal_test_utils
+ pw_rpc.nanopb.server_api
+ pw_rpc.test_protos.nanopb
pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.nanopb
)
+
+pw_add_test(pw_rpc.nanopb.method_info_test
+ SOURCES
+ method_info_test.cc
+ PRIVATE_DEPS
+ pw_rpc.common
+ pw_rpc.test_protos.nanopb_rpc
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.method_lookup_test
+ SOURCES
+ method_lookup_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.server_api
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.test_protos.nanopb_rpc
+ pw_rpc.test_utils
+ pw_rpc.raw.test_method_context
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.method_union_test
+ SOURCES
+ method_union_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.internal_test_utils
+ pw_rpc.nanopb.server_api
+ pw_rpc.test_protos.nanopb
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.echo_service_test
+ SOURCES
+ echo_service_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.echo_service
+ pw_rpc.nanopb.server_api
+ pw_rpc.nanopb.test_method_context
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.serde_test
+ SOURCES
+ serde_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.server_api
+ pw_rpc.test_protos.nanopb
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.server_callback_test
+ SOURCES
+ server_callback_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.server_api
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.test_protos.nanopb_rpc
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.server_reader_writer_test
+ SOURCES
+ server_reader_writer_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.server_api
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.test_protos.nanopb_rpc
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+pw_add_test(pw_rpc.nanopb.stub_generation_test
+ SOURCES
+ stub_generation_test.cc
+ PRIVATE_DEPS
+ pw_rpc.test_protos.nanopb_rpc
+ GROUPS
+ modules
+ pw_rpc.nanopb
+)
+
+# TODO(b/231950909) Test disabled as pw_work_queue lacks CMakeLists.txt
+if((TARGET pw_work_queue.pw_work_queue) AND
+ ("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
+ (NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL ""))
+ pw_add_test(pw_rpc.nanopb.synchronous_call_test
+ SOURCES
+ synchronous_call_test.cc
+ PRIVATE_DEPS
+ pw_rpc.nanopb.test_method_context
+ pw_rpc.synchronous_client_api
+ pw_rpc.test_protos.nanopb_rpc
+ pw_thread.thread
+ pw_work_queue.pw_work_queue
+ pw_work_queue.stl_test_thread
+ pw_work_queue.test_thread
+ GROUPS
+ modules
+ pw_rpc.nanopb
+ )
+endif()
diff --git a/pw_rpc/nanopb/client_call_test.cc b/pw_rpc/nanopb/client_call_test.cc
index 837620557..19d7c8678 100644
--- a/pw_rpc/nanopb/client_call_test.cc
+++ b/pw_rpc/nanopb/client_call_test.cc
@@ -198,7 +198,7 @@ TEST_F(UnaryClientCall, InvokesErrorCallbackOnServerError) {
[this](Status status) { last_error_ = status; });
EXPECT_EQ(OkStatus(),
- context.SendPacket(internal::PacketType::SERVER_ERROR,
+ context.SendPacket(internal::pwpb::PacketType::SERVER_ERROR,
Status::NotFound()));
EXPECT_EQ(responses_received_, 0);
@@ -252,7 +252,6 @@ TEST_F(UnaryClientCall, OnlyReceivesOneResponse) {
class ServerStreamingClientCall : public ::testing::Test {
protected:
- bool active_ = true;
std::optional<Status> stream_status_;
std::optional<Status> rpc_error_;
int responses_received_ = 0;
@@ -290,26 +289,23 @@ TEST_F(ServerStreamingClientCall, InvokesCallbackOnValidResponse) {
++responses_received_;
last_response_number_ = response.number;
},
- [this](Status status) {
- active_ = false;
- stream_status_ = status;
- });
+ [this](Status status) { stream_status_ = status; });
PW_ENCODE_PB(pw_rpc_test_TestStreamResponse, r1, .chunk = {}, .number = 11u);
EXPECT_EQ(OkStatus(), context.SendServerStream(r1));
- EXPECT_TRUE(active_);
+ EXPECT_TRUE(call.active());
EXPECT_EQ(responses_received_, 1);
EXPECT_EQ(last_response_number_, 11);
PW_ENCODE_PB(pw_rpc_test_TestStreamResponse, r2, .chunk = {}, .number = 22u);
EXPECT_EQ(OkStatus(), context.SendServerStream(r2));
- EXPECT_TRUE(active_);
+ EXPECT_TRUE(call.active());
EXPECT_EQ(responses_received_, 2);
EXPECT_EQ(last_response_number_, 22);
PW_ENCODE_PB(pw_rpc_test_TestStreamResponse, r3, .chunk = {}, .number = 33u);
EXPECT_EQ(OkStatus(), context.SendServerStream(r3));
- EXPECT_TRUE(active_);
+ EXPECT_TRUE(call.active());
EXPECT_EQ(responses_received_, 3);
EXPECT_EQ(last_response_number_, 33);
}
@@ -325,30 +321,27 @@ TEST_F(ServerStreamingClientCall, InvokesStreamEndOnFinish) {
++responses_received_;
last_response_number_ = response.number;
},
- [this](Status status) {
- active_ = false;
- stream_status_ = status;
- });
+ [this](Status status) { stream_status_ = status; });
PW_ENCODE_PB(pw_rpc_test_TestStreamResponse, r1, .chunk = {}, .number = 11u);
EXPECT_EQ(OkStatus(), context.SendServerStream(r1));
- EXPECT_TRUE(active_);
+ EXPECT_TRUE(call.active());
PW_ENCODE_PB(pw_rpc_test_TestStreamResponse, r2, .chunk = {}, .number = 22u);
EXPECT_EQ(OkStatus(), context.SendServerStream(r2));
- EXPECT_TRUE(active_);
+ EXPECT_TRUE(call.active());
// Close the stream.
EXPECT_EQ(OkStatus(), context.SendResponse(Status::NotFound()));
PW_ENCODE_PB(pw_rpc_test_TestStreamResponse, r3, .chunk = {}, .number = 33u);
EXPECT_EQ(OkStatus(), context.SendServerStream(r3));
- EXPECT_FALSE(active_);
+ EXPECT_FALSE(call.active());
EXPECT_EQ(responses_received_, 2);
}
-TEST_F(ServerStreamingClientCall, InvokesErrorCallbackOnInvalidResponses) {
+TEST_F(ServerStreamingClientCall, ParseErrorTerminatesCallWithDataLoss) {
ClientContextForTest<128, 99, kServiceId, kServerStreamingMethodId> context;
auto call = FakeGeneratedServiceClient::TestServerStreamRpc(
@@ -364,28 +357,16 @@ TEST_F(ServerStreamingClientCall, InvokesErrorCallbackOnInvalidResponses) {
PW_ENCODE_PB(pw_rpc_test_TestStreamResponse, r1, .chunk = {}, .number = 11u);
EXPECT_EQ(OkStatus(), context.SendServerStream(r1));
- EXPECT_TRUE(active_);
+ EXPECT_TRUE(call.active());
EXPECT_EQ(responses_received_, 1);
EXPECT_EQ(last_response_number_, 11);
constexpr std::byte bad_payload[]{
std::byte{0xab}, std::byte{0xcd}, std::byte{0xef}};
EXPECT_EQ(OkStatus(), context.SendServerStream(bad_payload));
+ EXPECT_FALSE(call.active());
EXPECT_EQ(responses_received_, 1);
- ASSERT_TRUE(rpc_error_.has_value());
EXPECT_EQ(rpc_error_, Status::DataLoss());
-
- PW_ENCODE_PB(pw_rpc_test_TestStreamResponse, r2, .chunk = {}, .number = 22u);
- EXPECT_EQ(OkStatus(), context.SendServerStream(r2));
- EXPECT_TRUE(active_);
- EXPECT_EQ(responses_received_, 2);
- EXPECT_EQ(last_response_number_, 22);
-
- EXPECT_EQ(OkStatus(),
- context.SendPacket(internal::PacketType::SERVER_ERROR,
- Status::NotFound()));
- EXPECT_EQ(responses_received_, 2);
- EXPECT_EQ(rpc_error_, Status::NotFound());
}
} // namespace
diff --git a/pw_rpc/nanopb/client_integration_test.cc b/pw_rpc/nanopb/client_integration_test.cc
index 07d3ca963..6163ba7a0 100644
--- a/pw_rpc/nanopb/client_integration_test.cc
+++ b/pw_rpc/nanopb/client_integration_test.cc
@@ -114,6 +114,7 @@ TEST(NanopbRpcIntegrationTest, BidirectionalStreaming_MoveCalls) {
pw::rpc::NanopbClientReaderWriter<pw_rpc_Payload, pw_rpc_Payload> new_call =
std::move(call);
+ // NOLINTNEXTLINE(bugprone-use-after-move)
EXPECT_EQ(Status::FailedPrecondition(), call.Write(Payload("Dello")));
ASSERT_EQ(OkStatus(), new_call.Write(Payload("Dello")));
@@ -121,6 +122,7 @@ TEST(NanopbRpcIntegrationTest, BidirectionalStreaming_MoveCalls) {
call = std::move(new_call);
+ // NOLINTNEXTLINE(bugprone-use-after-move)
EXPECT_EQ(Status::FailedPrecondition(), new_call.Write(Payload("Dello")));
ASSERT_EQ(OkStatus(), call.Write(Payload("???")));
diff --git a/pw_rpc/nanopb/client_server_context_test.cc b/pw_rpc/nanopb/client_server_context_test.cc
new file mode 100644
index 000000000..b023fdf4f
--- /dev/null
+++ b/pw_rpc/nanopb/client_server_context_test.cc
@@ -0,0 +1,116 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_rpc/nanopb/client_server_testing.h"
+#include "pw_rpc_test_protos/test.rpc.pb.h"
+
+namespace pw::rpc {
+namespace {
+
+using GeneratedService = ::pw::rpc::test::pw_rpc::nanopb::TestService;
+
+class TestService final : public GeneratedService::Service<TestService> {
+ public:
+ Status TestUnaryRpc(const pw_rpc_test_TestRequest& request,
+ pw_rpc_test_TestResponse& response) {
+ response.value = request.integer + 1;
+ return static_cast<Status::Code>(request.status_code);
+ }
+
+ void TestAnotherUnaryRpc(const pw_rpc_test_TestRequest&,
+ NanopbUnaryResponder<pw_rpc_test_TestResponse>&) {}
+
+ static void TestServerStreamRpc(
+ const pw_rpc_test_TestRequest&,
+ ServerWriter<pw_rpc_test_TestStreamResponse>&) {}
+
+ void TestClientStreamRpc(
+ ServerReader<pw_rpc_test_TestRequest, pw_rpc_test_TestStreamResponse>&) {}
+
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>&) {}
+};
+
+TEST(NanopbClientServerTestContext, ReceivesUnaryRpcReponse) {
+ NanopbClientServerTestContext<> ctx;
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ pw_rpc_test_TestResponse response pw_rpc_test_TestResponse_init_default;
+ auto handler = [&response](const pw_rpc_test_TestResponse& server_response,
+ pw::Status) { response = server_response; };
+
+ pw_rpc_test_TestRequest request{.integer = 1,
+ .status_code = OkStatus().code()};
+ auto call = GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request, handler);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ const auto sent_request =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto sent_response =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+
+ EXPECT_EQ(response.value, sent_response.value);
+ EXPECT_EQ(response.value, request.integer + 1);
+ EXPECT_EQ(request.integer, sent_request.integer);
+}
+
+TEST(NanopbClientServerTestContext, ReceivesMultipleReponses) {
+ NanopbClientServerTestContext<> ctx;
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ pw_rpc_test_TestResponse response1 pw_rpc_test_TestResponse_init_default;
+ pw_rpc_test_TestResponse response2 pw_rpc_test_TestResponse_init_default;
+ auto handler1 = [&response1](const pw_rpc_test_TestResponse& server_response,
+ pw::Status) { response1 = server_response; };
+ auto handler2 = [&response2](const pw_rpc_test_TestResponse& server_response,
+ pw::Status) { response2 = server_response; };
+
+ pw_rpc_test_TestRequest request1{.integer = 1,
+ .status_code = OkStatus().code()};
+ pw_rpc_test_TestRequest request2{.integer = 2,
+ .status_code = OkStatus().code()};
+ const auto call1 = GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request1, handler1);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+ const auto call2 = GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request2, handler2);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ const auto sent_request1 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto sent_request2 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+ const auto sent_response1 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto sent_response2 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(response1.value, request1.integer + 1);
+ EXPECT_EQ(response2.value, request2.integer + 1);
+ EXPECT_EQ(response1.value, sent_response1.value);
+ EXPECT_EQ(response2.value, sent_response2.value);
+ EXPECT_EQ(request1.integer, sent_request1.integer);
+ EXPECT_EQ(request2.integer, sent_request2.integer);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/client_server_context_threaded_test.cc b/pw_rpc/nanopb/client_server_context_threaded_test.cc
new file mode 100644
index 000000000..68be69960
--- /dev/null
+++ b/pw_rpc/nanopb/client_server_context_threaded_test.cc
@@ -0,0 +1,117 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_rpc/nanopb/client_server_testing_threaded.h"
+#include "pw_rpc_test_protos/test.rpc.pb.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/test_threads.h"
+
+namespace pw::rpc {
+namespace {
+
+using GeneratedService = ::pw::rpc::test::pw_rpc::nanopb::TestService;
+
+class TestService final : public GeneratedService::Service<TestService> {
+ public:
+ Status TestUnaryRpc(const pw_rpc_test_TestRequest& request,
+ pw_rpc_test_TestResponse& response) {
+ response.value = request.integer + 1;
+ return static_cast<Status::Code>(request.status_code);
+ }
+
+ void TestAnotherUnaryRpc(const pw_rpc_test_TestRequest&,
+ NanopbUnaryResponder<pw_rpc_test_TestResponse>&) {}
+
+ static void TestServerStreamRpc(
+ const pw_rpc_test_TestRequest&,
+ ServerWriter<pw_rpc_test_TestStreamResponse>&) {}
+
+ void TestClientStreamRpc(
+ ServerReader<pw_rpc_test_TestRequest, pw_rpc_test_TestStreamResponse>&) {}
+
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>&) {}
+};
+
+class RpcCaller {
+ public:
+ void BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
+ pw_rpc_test_TestRequest request{.integer = i,
+ .status_code = OkStatus().code()};
+ auto call = GeneratedService::TestUnaryRpc(
+ client,
+ channel_id,
+ request,
+ [this](const pw_rpc_test_TestResponse&, Status) {
+ semaphore_.release();
+ },
+ [](Status) {});
+
+ semaphore_.acquire();
+ }
+
+ private:
+ pw::sync::BinarySemaphore semaphore_;
+};
+
+TEST(NanopbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
+ NanopbClientServerTestContextThreaded<> ctx(
+ thread::test::TestOptionsThread0());
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ constexpr auto value = 1;
+ caller.BlockOnResponse(value, ctx.client(), ctx.channel().id());
+
+ const auto request =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto response =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+
+ EXPECT_EQ(value, request.integer);
+ EXPECT_EQ(value + 1, response.value);
+}
+
+TEST(NanopbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
+ NanopbClientServerTestContextThreaded<> ctx(
+ thread::test::TestOptionsThread0());
+ TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ constexpr auto value1 = 1;
+ constexpr auto value2 = 2;
+ caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id());
+ caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id());
+
+ const auto request1 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto request2 =
+ ctx.request<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+ const auto response1 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(0);
+ const auto response2 =
+ ctx.response<test::pw_rpc::nanopb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(value1, request1.integer);
+ EXPECT_EQ(value2, request2.integer);
+ EXPECT_EQ(value1 + 1, response1.value);
+ EXPECT_EQ(value2 + 1, response2.value);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/codegen_test.cc b/pw_rpc/nanopb/codegen_test.cc
index 5d27c1d56..8a6b26cb1 100644
--- a/pw_rpc/nanopb/codegen_test.cc
+++ b/pw_rpc/nanopb/codegen_test.cc
@@ -81,7 +81,8 @@ using internal::ClientContextForTest;
TEST(NanopbCodegen, CompilesProperly) {
test::TestService service;
- EXPECT_EQ(service.id(), internal::Hash("pw.rpc.test.TestService"));
+ EXPECT_EQ(internal::UnwrapServiceId(service.service_id()),
+ internal::Hash("pw.rpc.test.TestService"));
EXPECT_STREQ(service.name(), "TestService");
}
diff --git a/pw_rpc/nanopb/common.cc b/pw_rpc/nanopb/common.cc
index a571ebe27..34aea7614 100644
--- a/pw_rpc/nanopb/common.cc
+++ b/pw_rpc/nanopb/common.cc
@@ -24,6 +24,7 @@
#include "pw_log/log.h"
#include "pw_result/result.h"
#include "pw_rpc/internal/client_call.h"
+#include "pw_rpc/internal/encoding_buffer.h"
#include "pw_rpc/nanopb/server_reader_writer.h"
#include "pw_status/try.h"
@@ -43,16 +44,6 @@ struct NanopbTraits<bool(pb_istream_t*, FieldsType, void*)> {
using Fields = typename NanopbTraits<decltype(pb_decode)>::Fields;
-Result<ByteSpan> EncodeToPayloadBuffer(const void* payload, NanopbSerde serde)
- PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- ByteSpan payload_buffer = GetPayloadBuffer();
- StatusWithSize result = serde.Encode(payload, payload_buffer);
- if (!result.ok()) {
- return result.status();
- }
- return payload_buffer.first(result.size());
-}
-
} // namespace
// PB_NO_ERRMSG is used in pb_decode.h and pb_encode.h to enable or disable the
@@ -87,14 +78,15 @@ StatusWithSize NanopbSerde::EncodedSizeBytes(const void* proto_struct) const {
: StatusWithSize::Unknown();
}
-bool NanopbSerde::Decode(ConstByteSpan buffer, void* proto_struct) const {
+Status NanopbSerde::Decode(ConstByteSpan buffer, void* proto_struct) const {
auto input = pb_istream_from_buffer(
reinterpret_cast<const pb_byte_t*>(buffer.data()), buffer.size());
bool result = pb_decode(&input, static_cast<Fields>(fields_), proto_struct);
if (!result) {
PW_RPC_LOG_NANOPB_FAILURE("Nanopb protobuf decode failed", input);
+ return Status::DataLoss();
}
- return result;
+ return OkStatus();
}
#undef PW_RPC_LOG_NANOPB_FAILURE
@@ -109,17 +101,20 @@ void NanopbSendInitialRequest(ClientCall& call,
if (result.ok()) {
call.SendInitialClientRequest(*result);
} else {
- call.HandleError(result.status());
+ call.CloseAndMarkForCleanup(result.status());
}
}
-Status NanopbSendStream(Call& call, const void* payload, NanopbSerde serde) {
- LockGuard lock(rpc_lock());
+Status NanopbSendStream(Call& call,
+ const void* payload,
+ const NanopbMethodSerde* serde) {
if (!call.active_locked()) {
return Status::FailedPrecondition();
}
- Result<ByteSpan> result = EncodeToPayloadBuffer(payload, serde);
+ Result<ByteSpan> result = EncodeToPayloadBuffer(
+ payload,
+ call.type() == kClientCall ? serde->request() : serde->response());
PW_TRY(result.status());
return call.WriteLocked(*result);
@@ -128,7 +123,7 @@ Status NanopbSendStream(Call& call, const void* payload, NanopbSerde serde) {
Status SendFinalResponse(NanopbServerCall& call,
const void* payload,
const Status status) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
if (!call.active_locked()) {
return Status::FailedPrecondition();
}
diff --git a/pw_rpc/nanopb/docs.rst b/pw_rpc/nanopb/docs.rst
index 5ed7010f1..993f7c5fb 100644
--- a/pw_rpc/nanopb/docs.rst
+++ b/pw_rpc/nanopb/docs.rst
@@ -127,15 +127,11 @@ Once a ``ServerWriter`` has been closed, all future ``Write`` calls will fail.
Client streaming RPC
^^^^^^^^^^^^^^^^^^^^
-.. attention::
-
- ``pw_rpc`` does not yet support client streaming RPCs.
+.. attention:: Supported, but the documentation is still under construction.
Bidirectional streaming RPC
^^^^^^^^^^^^^^^^^^^^^^^^^^^
-.. attention::
-
- ``pw_rpc`` does not yet support bidirectional streaming RPCs.
+.. attention:: Supported, but the documentation is still under construction.
Client-side
-----------
@@ -155,7 +151,7 @@ which they will send requests, and the channel ID they will use.
public:
Client(::pw::rpc::Client& client, uint32_t channel_id);
- GetRoomInformationCall GetRoomInformation(
+ pw::rpc::NanopbUnaryReceiver<RoomInfoResponse> GetRoomInformation(
const RoomInfoRequest& request,
::pw::Function<void(Status, const RoomInfoResponse&)> on_response,
::pw::Function<void(Status)> on_rpc_error = nullptr);
@@ -167,16 +163,14 @@ RPCs can also be invoked individually as free functions:
.. code-block:: c++
- GetRoomInformationCall call = pw_rpc::nanopb::Chat::GetRoomInformation(
+ pw::rpc::NanopbUnaryReceiver<RoomInfoResponse> call = pw_rpc::nanopb::Chat::GetRoomInformation(
client, channel_id, request, on_response, on_rpc_error);
The client class has member functions for each method defined within the
service's protobuf descriptor. The arguments to these methods vary depending on
-the type of RPC. Each method returns a ``NanopbClientCall`` object which stores
-the context of the ongoing RPC call. For more information on ``ClientCall``
-objects, refer to the :ref:`core RPC docs <module-pw_rpc-making-calls>`. The
-type of the returned object is complex, so it is aliased using the method
-name.
+the type of RPC. Each method returns a client call object which stores the
+context of the ongoing RPC call. For more information on call objects, refer to
+the :ref:`core RPC docs <module-pw_rpc-making-calls>`.
.. admonition:: Callback invocation
@@ -196,7 +190,7 @@ An optional second callback can be provided to handle internal errors.
.. code-block:: c++
- GetRoomInformationCall GetRoomInformation(
+ pw::rpc::NanopbUnaryReceiver<RoomInfoResponse> GetRoomInformation(
const RoomInfoRequest& request,
::pw::Function<void(const RoomInfoResponse&, Status)> on_response,
::pw::Function<void(Status)> on_rpc_error = nullptr);
@@ -211,12 +205,20 @@ An optional third callback can be provided to handle internal errors.
.. code-block:: c++
- ListUsersInRoomCall ListUsersInRoom(
+ pw::rpc::NanopbClientReader<ListUsersResponse> ListUsersInRoom(
const ListUsersRequest& request,
::pw::Function<void(const ListUsersResponse&)> on_response,
::pw::Function<void(Status)> on_stream_end,
::pw::Function<void(Status)> on_rpc_error = nullptr);
+Client streaming RPC
+~~~~~~~~~~~~~~~~~~~~
+.. attention:: Supported, but the documentation is still under construction.
+
+Bidirectional streaming RPC
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. attention:: Supported, but the documentation is still under construction.
+
Example usage
^^^^^^^^^^^^^
The following example demonstrates how to call an RPC method using a nanopb
diff --git a/pw_rpc/nanopb/fake_channel_output_test.cc b/pw_rpc/nanopb/fake_channel_output_test.cc
index f21b5dc45..dcdf0c096 100644
--- a/pw_rpc/nanopb/fake_channel_output_test.cc
+++ b/pw_rpc/nanopb/fake_channel_output_test.cc
@@ -35,21 +35,21 @@ TEST(NanopbFakeChannelOutput, Requests) {
std::byte payload_buffer[32] = {};
constexpr Info::Request request{.integer = -100, .status_code = 5};
const StatusWithSize payload =
- Info::serde().EncodeRequest(&request, payload_buffer);
+ Info::serde().request().Encode(&request, payload_buffer);
ASSERT_TRUE(payload.ok());
std::array<std::byte, 128> buffer;
- auto packet = Packet(PacketType::REQUEST,
+ auto packet = Packet(pwpb::PacketType::REQUEST,
1,
Info::kServiceId,
Info::kMethodId,
999,
- std::span(payload_buffer, payload.size()))
+ span(payload_buffer, payload.size()))
.Encode(buffer);
ASSERT_TRUE(packet.ok());
- ASSERT_EQ(OkStatus(), output.Send(std::span(buffer).first(packet->size())));
+ ASSERT_EQ(OkStatus(), output.Send(span(buffer).first(packet->size())));
ASSERT_TRUE(output.responses<TestService::TestUnaryRpc>().empty());
ASSERT_EQ(output.requests<TestService::TestUnaryRpc>().size(), 1u);
@@ -65,21 +65,21 @@ TEST(NanopbFakeChannelOutput, Responses) {
std::byte payload_buffer[32] = {};
constexpr Info::Response response{.value = -9876, .repeated_field = {}};
const StatusWithSize payload =
- Info::serde().EncodeResponse(&response, payload_buffer);
+ Info::serde().response().Encode(&response, payload_buffer);
ASSERT_TRUE(payload.ok());
std::array<std::byte, 128> buffer;
- auto packet = Packet(PacketType::RESPONSE,
+ auto packet = Packet(pwpb::PacketType::RESPONSE,
1,
Info::kServiceId,
Info::kMethodId,
999,
- std::span(payload_buffer, payload.size()))
+ span(payload_buffer, payload.size()))
.Encode(buffer);
ASSERT_TRUE(packet.ok());
- ASSERT_EQ(OkStatus(), output.Send(std::span(buffer).first(packet->size())));
+ ASSERT_EQ(OkStatus(), output.Send(span(buffer).first(packet->size())));
ASSERT_EQ(output.responses<TestService::TestUnaryRpc>().size(), 1u);
ASSERT_TRUE(output.requests<TestService::TestUnaryRpc>().empty());
diff --git a/pw_rpc/nanopb/method.cc b/pw_rpc/nanopb/method.cc
index ed38f39b5..0a850808e 100644
--- a/pw_rpc/nanopb/method.cc
+++ b/pw_rpc/nanopb/method.cc
@@ -36,7 +36,7 @@ void NanopbMethod::CallSynchronousUnary(const CallContext& context,
return;
}
- NanopbServerCall responder(context, MethodType::kUnary);
+ NanopbServerCall responder(context.ClaimLocked(), MethodType::kUnary);
rpc_lock().unlock();
const Status status = function_.synchronous_unary(
context.service(), request_struct, response_struct);
@@ -52,7 +52,7 @@ void NanopbMethod::CallUnaryRequest(const CallContext& context,
return;
}
- NanopbServerCall server_writer(context, type);
+ NanopbServerCall server_writer(context.ClaimLocked(), type);
rpc_lock().unlock();
function_.unary_request(context.service(), request_struct, server_writer);
}
@@ -60,7 +60,7 @@ void NanopbMethod::CallUnaryRequest(const CallContext& context,
bool NanopbMethod::DecodeRequest(const CallContext& context,
const Packet& request,
void* proto_struct) const {
- if (serde_.DecodeRequest(request.payload(), proto_struct)) {
+ if (serde_.request().Decode(request.payload(), proto_struct).ok()) {
return true;
}
diff --git a/pw_rpc/nanopb/method_test.cc b/pw_rpc/nanopb/method_test.cc
index cf19fe051..9b84e1730 100644
--- a/pw_rpc/nanopb/method_test.cc
+++ b/pw_rpc/nanopb/method_test.cc
@@ -17,6 +17,7 @@
#include <array>
#include "gtest/gtest.h"
+#include "pw_containers/algorithm.h"
#include "pw_rpc/internal/lock.h"
#include "pw_rpc/internal/method_impl_tester.h"
#include "pw_rpc/internal/test_utils.h"
@@ -237,7 +238,7 @@ TEST(NanopbMethod, SyncUnaryRpc_InvalidPayload_SendsError) {
kSyncUnary.Invoke(context.get(), context.request(bad_payload));
const Packet& packet = context.output().last_packet();
- EXPECT_EQ(PacketType::SERVER_ERROR, packet.type());
+ EXPECT_EQ(pwpb::PacketType::SERVER_ERROR, packet.type());
EXPECT_EQ(Status::DataLoss(), packet.status());
EXPECT_EQ(context.service_id(), packet.service_id());
EXPECT_EQ(kSyncUnary.id(), packet.method_id());
@@ -255,7 +256,7 @@ TEST(NanopbMethod, AsyncUnaryRpc_ResponseEncodingFails_SendsInternalError) {
kAsyncUnary.Invoke(context.get(), context.request(request));
const Packet& packet = context.output().last_packet();
- EXPECT_EQ(PacketType::SERVER_ERROR, packet.type());
+ EXPECT_EQ(pwpb::PacketType::SERVER_ERROR, packet.type());
EXPECT_EQ(Status::Internal(), packet.status());
EXPECT_EQ(context.service_id(), packet.service_id());
EXPECT_EQ(kAsyncUnary.id(), packet.method_id());
@@ -290,10 +291,7 @@ TEST(NanopbMethod, ServerWriter_SendsResponse) {
ASSERT_EQ(OkStatus(), encoded.status());
ConstByteSpan sent_payload = context.output().last_packet().payload();
- EXPECT_TRUE(std::equal(payload.begin(),
- payload.end(),
- sent_payload.begin(),
- sent_payload.end()));
+ EXPECT_TRUE(pw::containers::Equal(payload, sent_payload));
}
TEST(NanopbMethod, ServerWriter_WriteWhenClosed_ReturnsFailedPrecondition) {
@@ -357,8 +355,7 @@ TEST(NanopbMethod, ServerReader_HandlesRequests) {
std::array<byte, 128> encoded_request = {};
auto encoded = context.client_stream(request).Encode(encoded_request);
ASSERT_EQ(OkStatus(), encoded.status());
- ASSERT_EQ(OkStatus(),
- context.server().ProcessPacket(*encoded, context.output()));
+ ASSERT_EQ(OkStatus(), context.server().ProcessPacket(*encoded));
EXPECT_EQ(request_struct.integer, 1 << 30);
EXPECT_EQ(request_struct.status_code, 9u);
@@ -379,10 +376,7 @@ TEST(NanopbMethod, ServerReaderWriter_WritesResponses) {
ASSERT_EQ(OkStatus(), encoded.status());
ConstByteSpan sent_payload = context.output().last_packet().payload();
- EXPECT_TRUE(std::equal(payload.begin(),
- payload.end(),
- sent_payload.begin(),
- sent_payload.end()));
+ EXPECT_TRUE(pw::containers::Equal(payload, sent_payload));
}
TEST(NanopbMethod, ServerReaderWriter_HandlesRequests) {
@@ -402,8 +396,7 @@ TEST(NanopbMethod, ServerReaderWriter_HandlesRequests) {
std::array<byte, 128> encoded_request = {};
auto encoded = context.client_stream(request).Encode(encoded_request);
ASSERT_EQ(OkStatus(), encoded.status());
- ASSERT_EQ(OkStatus(),
- context.server().ProcessPacket(*encoded, context.output()));
+ ASSERT_EQ(OkStatus(), context.server().ProcessPacket(*encoded));
EXPECT_EQ(request_struct.integer, 1 << 29);
EXPECT_EQ(request_struct.status_code, 8u);
diff --git a/pw_rpc/nanopb/method_union_test.cc b/pw_rpc/nanopb/method_union_test.cc
index 6eacb5978..3b9c2d114 100644
--- a/pw_rpc/nanopb/method_union_test.cc
+++ b/pw_rpc/nanopb/method_union_test.cc
@@ -25,8 +25,6 @@
namespace pw::rpc::internal {
namespace {
-using std::byte;
-
template <typename Implementation>
class FakeGeneratedService : public Service {
public:
@@ -119,7 +117,7 @@ TEST(NanopbMethodUnion, Raw_CallsServerStreamingMethod) {
EXPECT_TRUE(context.service().last_raw_writer.active());
EXPECT_EQ(OkStatus(), context.service().last_raw_writer.Finish());
- EXPECT_EQ(context.output().last_packet().type(), PacketType::RESPONSE);
+ EXPECT_EQ(context.output().last_packet().type(), pwpb::PacketType::RESPONSE);
}
TEST(NanopbMethodUnion, Nanopb_CallsUnaryMethod) {
@@ -162,7 +160,7 @@ TEST(NanopbMethodUnion, Nanopb_CallsServerStreamingMethod) {
EXPECT_TRUE(context.service().last_writer.active());
EXPECT_EQ(OkStatus(), context.service().last_writer.Finish());
- EXPECT_EQ(context.output().last_packet().type(), PacketType::RESPONSE);
+ EXPECT_EQ(context.output().last_packet().type(), pwpb::PacketType::RESPONSE);
}
} // namespace
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
index 61f5a7304..3aac5a576 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_reader_writer.h
@@ -37,11 +37,13 @@ class NanopbUnaryResponseClientCall : public UnaryResponseClientCall {
const NanopbMethodSerde& serde,
Function<void(const Response&, Status)>&& on_completed,
Function<void(Status)>&& on_error,
- const Request&... request) {
+ const Request&... request)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
rpc_lock().lock();
- CallType call(client, channel_id, service_id, method_id, serde);
+ CallType call(
+ client.ClaimLocked(), channel_id, service_id, method_id, serde);
- call.set_on_completed_locked(std::move(on_completed));
+ call.set_nanopb_on_completed_locked(std::move(on_completed));
call.set_on_error_locked(std::move(on_error));
if constexpr (sizeof...(Request) == 0u) {
@@ -49,20 +51,22 @@ class NanopbUnaryResponseClientCall : public UnaryResponseClientCall {
} else {
NanopbSendInitialRequest(call, serde.request(), &request...);
}
+ client.CleanUpCalls();
return call;
}
protected:
constexpr NanopbUnaryResponseClientCall() = default;
- NanopbUnaryResponseClientCall(internal::Endpoint& client,
+ NanopbUnaryResponseClientCall(LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
MethodType type,
const NanopbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: UnaryResponseClientCall(
- client, channel_id, service_id, method_id, type),
+ client, channel_id, service_id, method_id, StructCallProps(type)),
serde_(&serde) {}
NanopbUnaryResponseClientCall(NanopbUnaryResponseClientCall&& other)
@@ -72,51 +76,42 @@ class NanopbUnaryResponseClientCall : public UnaryResponseClientCall {
NanopbUnaryResponseClientCall& operator=(
NanopbUnaryResponseClientCall&& other) PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
MoveUnaryResponseClientCallFrom(other);
serde_ = other.serde_;
- set_on_completed_locked(std::move(other.nanopb_on_completed_));
+ set_nanopb_on_completed_locked(std::move(other.nanopb_on_completed_));
return *this;
}
void set_on_completed(
Function<void(const Response& response, Status)>&& on_completed)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
- set_on_completed_locked(std::move(on_completed));
+ RpcLockGuard lock;
+ set_nanopb_on_completed_locked(std::move(on_completed));
}
Status SendClientStream(const void* payload) PW_LOCKS_EXCLUDED(rpc_lock()) {
- if (!active()) {
- return Status::FailedPrecondition();
- }
- return NanopbSendStream(*this, payload, serde_->request());
+ RpcLockGuard lock;
+ return NanopbSendStream(*this, payload, serde_);
}
private:
- void set_on_completed_locked(
+ void set_nanopb_on_completed_locked(
Function<void(const Response& response, Status)>&& on_completed)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
nanopb_on_completed_ = std::move(on_completed);
UnaryResponseClientCall::set_on_completed_locked(
- [this](ConstByteSpan payload, Status status) {
- if (nanopb_on_completed_) {
- Response response_struct{};
- if (serde_->DecodeResponse(payload, &response_struct)) {
- nanopb_on_completed_(response_struct, status);
- } else {
- // TODO(hepler): This should send a DATA_LOSS error and call the
- // error callback.
- rpc_lock().lock();
- CallOnError(Status::DataLoss());
- }
- }
- });
+ [this](ConstByteSpan payload, Status status)
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ DecodeToStructAndInvokeOnCompleted(
+ payload, serde_->response(), nanopb_on_completed_, status);
+ });
}
- const NanopbMethodSerde* serde_;
- Function<void(const Response&, Status)> nanopb_on_completed_;
+ const NanopbMethodSerde* serde_ PW_GUARDED_BY(rpc_lock());
+ Function<void(const Response&, Status)> nanopb_on_completed_
+ PW_GUARDED_BY(rpc_lock());
};
// Base class for server and bidirectional streaming calls.
@@ -132,11 +127,13 @@ class NanopbStreamResponseClientCall : public StreamResponseClientCall {
Function<void(const Response&)>&& on_next,
Function<void(Status)>&& on_completed,
Function<void(Status)>&& on_error,
- const Request&... request) {
+ const Request&... request)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
rpc_lock().lock();
- CallType call(client, channel_id, service_id, method_id, serde);
+ CallType call(
+ client.ClaimLocked(), channel_id, service_id, method_id, serde);
- call.set_on_next_locked(std::move(on_next));
+ call.set_nanopb_on_next_locked(std::move(on_next));
call.set_on_completed_locked(std::move(on_completed));
call.set_on_error_locked(std::move(on_error));
@@ -145,6 +142,7 @@ class NanopbStreamResponseClientCall : public StreamResponseClientCall {
} else {
NanopbSendInitialRequest(call, serde.request(), &request...);
}
+ client.CleanUpCalls();
return call;
}
@@ -158,58 +156,50 @@ class NanopbStreamResponseClientCall : public StreamResponseClientCall {
NanopbStreamResponseClientCall& operator=(
NanopbStreamResponseClientCall&& other) PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
MoveStreamResponseClientCallFrom(other);
serde_ = other.serde_;
- set_on_next_locked(std::move(other.nanopb_on_next_));
+ set_nanopb_on_next_locked(std::move(other.nanopb_on_next_));
return *this;
}
- NanopbStreamResponseClientCall(internal::Endpoint& client,
+ NanopbStreamResponseClientCall(LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
MethodType type,
const NanopbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: StreamResponseClientCall(
- client, channel_id, service_id, method_id, type),
+ client, channel_id, service_id, method_id, StructCallProps(type)),
serde_(&serde) {}
- Status SendClientStream(const void* payload) {
- if (!active()) {
- return Status::FailedPrecondition();
- }
- return NanopbSendStream(*this, payload, serde_->request());
+ Status SendClientStream(const void* payload) PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ return NanopbSendStream(*this, payload, serde_);
}
void set_on_next(Function<void(const Response& response)>&& on_next)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
- set_on_next_locked(std::move(on_next));
+ RpcLockGuard lock;
+ set_nanopb_on_next_locked(std::move(on_next));
}
private:
- void set_on_next_locked(Function<void(const Response& response)>&& on_next)
+ void set_nanopb_on_next_locked(
+ Function<void(const Response& response)>&& on_next)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
nanopb_on_next_ = std::move(on_next);
- internal::Call::set_on_next_locked([this](ConstByteSpan payload) {
- if (nanopb_on_next_) {
- Response response_struct{};
- if (serde_->DecodeResponse(payload, &response_struct)) {
- nanopb_on_next_(response_struct);
- } else {
- // TODO(hepler): This should send a DATA_LOSS error and call the
- // error callback.
- rpc_lock().lock();
- CallOnError(Status::DataLoss());
- }
- }
- });
+ Call::set_on_next_locked(
+ [this](ConstByteSpan payload) PW_NO_LOCK_SAFETY_ANALYSIS {
+ DecodeToStructAndInvokeOnNext(
+ payload, serde_->response(), nanopb_on_next_);
+ });
}
- const NanopbMethodSerde* serde_;
- Function<void(const Response&)> nanopb_on_next_;
+ const NanopbMethodSerde* serde_ PW_GUARDED_BY(rpc_lock());
+ Function<void(const Response&)> nanopb_on_next_ PW_GUARDED_BY(rpc_lock());
};
} // namespace internal
@@ -243,8 +233,17 @@ class NanopbClientReaderWriter
&request);
}
+ // Notifies the server that no further client stream messages will be sent.
+ using internal::ClientCall::CloseClientStream;
+
+ // Cancels this RPC. Closes the call locally and sends a CANCELLED error to
+ // the server.
using internal::Call::Cancel;
- using internal::Call::CloseClientStream;
+
+ // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
+ // packet. Future packets for this RPC are dropped, and the client sends a
+ // FAILED_PRECONDITION error in response because the call is not active.
+ using internal::ClientCall::Abandon;
// Functions for setting RPC event callbacks.
using internal::Call::set_on_error;
@@ -254,11 +253,12 @@ class NanopbClientReaderWriter
private:
friend class internal::NanopbStreamResponseClientCall<Response>;
- NanopbClientReaderWriter(internal::Endpoint& client,
+ NanopbClientReaderWriter(internal::LockedEndpoint& client,
uint32_t channel_id_value,
uint32_t service_id,
uint32_t method_id,
const internal::NanopbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: internal::NanopbStreamResponseClientCall<Response>(
client,
channel_id_value,
@@ -290,15 +290,17 @@ class NanopbClientReader
using internal::StreamResponseClientCall::set_on_completed;
using internal::Call::Cancel;
+ using internal::ClientCall::Abandon;
private:
friend class internal::NanopbStreamResponseClientCall<Response>;
- NanopbClientReader(internal::Endpoint& client,
+ NanopbClientReader(internal::LockedEndpoint& client,
uint32_t channel_id_value,
uint32_t service_id,
uint32_t method_id,
const internal::NanopbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: internal::NanopbStreamResponseClientCall<Response>(
client,
channel_id_value,
@@ -334,15 +336,17 @@ class NanopbClientWriter
using internal::Call::Cancel;
using internal::Call::CloseClientStream;
+ using internal::ClientCall::Abandon;
private:
friend class internal::NanopbUnaryResponseClientCall<Response>;
- NanopbClientWriter(internal::Endpoint& client,
+ NanopbClientWriter(internal::LockedEndpoint& client,
uint32_t channel_id_value,
uint32_t service_id,
uint32_t method_id,
const internal::NanopbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: internal::NanopbUnaryResponseClientCall<Response>(
client,
channel_id_value,
@@ -372,15 +376,17 @@ class NanopbUnaryReceiver
using internal::Call::set_on_error;
using internal::Call::Cancel;
+ using internal::ClientCall::Abandon;
private:
friend class internal::NanopbUnaryResponseClientCall<Response>;
- NanopbUnaryReceiver(internal::Endpoint& client,
+ NanopbUnaryReceiver(internal::LockedEndpoint& client,
uint32_t channel_id_value,
uint32_t service_id,
uint32_t method_id,
const internal::NanopbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: internal::NanopbUnaryResponseClientCall<Response>(client,
channel_id_value,
service_id,
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h
new file mode 100644
index 000000000..429cb719c
--- /dev/null
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing.h
@@ -0,0 +1,110 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cinttypes>
+
+#include "pw_rpc/internal/client_server_testing.h"
+#include "pw_rpc/nanopb/fake_channel_output.h"
+
+namespace pw::rpc {
+namespace internal {
+
+template <size_t kOutputSize,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class NanopbForwardingChannelOutput final
+ : public ForwardingChannelOutput<
+ NanopbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ template <auto kMethod>
+ using Response = typename MethodInfo<kMethod>::Response;
+ template <auto kMethod>
+ using Request = typename MethodInfo<kMethod>::Request;
+
+ using Base = ForwardingChannelOutput<
+ NanopbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ constexpr NanopbForwardingChannelOutput() = default;
+
+ template <auto kMethod>
+ Response<kMethod> response(uint32_t channel_id, uint32_t index) {
+ PW_ASSERT(Base::PacketCount() >= index);
+ return Base::output_.template responses<kMethod>(channel_id)[index];
+ }
+
+ template <auto kMethod>
+ Request<kMethod> request(uint32_t channel_id, uint32_t index) {
+ PW_ASSERT(Base::PacketCount() >= index);
+ return Base::output_.template requests<kMethod>(channel_id)[index];
+ }
+};
+
+} // namespace internal
+
+template <size_t kOutputSize = 128,
+ size_t kMaxPackets = 16,
+ size_t kPayloadsBufferSizeBytes = 128>
+class NanopbClientServerTestContext final
+ : public internal::ClientServerTestContext<
+ internal::NanopbForwardingChannelOutput<kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ template <auto kMethod>
+ using Response = typename MethodInfo<kMethod>::Response;
+ template <auto kMethod>
+ using Request = typename MethodInfo<kMethod>::Request;
+
+ using Base = internal::ClientServerTestContext<
+ internal::NanopbForwardingChannelOutput<kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ NanopbClientServerTestContext() = default;
+
+ // Retrieve copy of request indexed by order of occurance
+ template <auto kMethod>
+ Request<kMethod> request(uint32_t index) {
+ return Base::channel_output_.template request<kMethod>(Base::channel().id(),
+ index);
+ }
+
+ // Retrieve copy of resonse indexed by order of occurance
+ template <auto kMethod>
+ Response<kMethod> response(uint32_t index) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index);
+ }
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h
new file mode 100644
index 000000000..0e08f0b38
--- /dev/null
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_server_testing_threaded.h
@@ -0,0 +1,115 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cinttypes>
+
+#include "pw_rpc/internal/client_server_testing_threaded.h"
+#include "pw_rpc/nanopb/fake_channel_output.h"
+
+namespace pw::rpc {
+namespace internal {
+
+template <size_t kOutputSize,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class NanopbWatchableChannelOutput final
+ : public WatchableChannelOutput<
+ NanopbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ template <auto kMethod>
+ using Response = typename MethodInfo<kMethod>::Response;
+ template <auto kMethod>
+ using Request = typename MethodInfo<kMethod>::Request;
+
+ using Base = WatchableChannelOutput<
+ NanopbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ constexpr NanopbWatchableChannelOutput() = default;
+
+ template <auto kMethod>
+ Response<kMethod> response(uint32_t channel_id, uint32_t index)
+ PW_LOCKS_EXCLUDED(Base::mutex_) {
+ std::lock_guard lock(Base::mutex_);
+ PW_ASSERT(Base::PacketCount() >= index);
+ return Base::output_.template responses<kMethod>(channel_id)[index];
+ }
+
+ template <auto kMethod>
+ Request<kMethod> request(uint32_t channel_id, uint32_t index)
+ PW_LOCKS_EXCLUDED(Base::mutex_) {
+ std::lock_guard lock(Base::mutex_);
+ PW_ASSERT(Base::PacketCount() >= index);
+ return Base::output_.template requests<kMethod>(channel_id)[index];
+ }
+};
+
+} // namespace internal
+
+template <size_t kOutputSize = 128,
+ size_t kMaxPackets = 16,
+ size_t kPayloadsBufferSizeBytes = 128>
+class NanopbClientServerTestContextThreaded final
+ : public internal::ClientServerTestContextThreaded<
+ internal::NanopbWatchableChannelOutput<kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ template <auto kMethod>
+ using Response = typename MethodInfo<kMethod>::Response;
+ template <auto kMethod>
+ using Request = typename MethodInfo<kMethod>::Request;
+
+ using Base = internal::ClientServerTestContextThreaded<
+ internal::NanopbWatchableChannelOutput<kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ NanopbClientServerTestContextThreaded(const thread::Options& options)
+ : Base(options) {}
+
+ // Retrieve copy of request indexed by order of occurance
+ template <auto kMethod>
+ Request<kMethod> request(uint32_t index) {
+ return Base::channel_output_.template request<kMethod>(Base::channel().id(),
+ index);
+ }
+
+ // Retrieve copy of resonse indexed by order of occurance
+ template <auto kMethod>
+ Response<kMethod> response(uint32_t index) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index);
+ }
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
index d0e0898dc..a06d53798 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/client_testing.h
@@ -24,7 +24,7 @@
namespace pw::rpc {
-// TODO(pwbug/477): Document the client testing APIs.
+// TODO(b/234878467): Document the client testing APIs.
// Sends packets to an RPC client as if it were a pw_rpc server. Accepts
// payloads as Nanopb structs.
@@ -67,9 +67,10 @@ class NanopbFakeServer : public FakeServer {
template <auto kMethod>
static ConstByteSpan EncodeResponse(const void* payload, ByteSpan buffer) {
const StatusWithSize result =
- internal::MethodInfo<kMethod>::serde().EncodeResponse(payload, buffer);
+ internal::MethodInfo<kMethod>::serde().response().Encode(payload,
+ buffer);
PW_ASSERT(result.ok());
- return std::span(buffer).first(result.size());
+ return span(buffer).first(result.size());
}
};
@@ -83,7 +84,7 @@ class NanopbClientTestContext {
public:
constexpr NanopbClientTestContext()
: channel_(Channel::Create<kDefaultChannelId>(&channel_output_)),
- client_(std::span(&channel_, 1)),
+ client_(span(&channel_, 1)),
packet_buffer_{},
fake_server_(
channel_output_, client_, kDefaultChannelId, packet_buffer_) {}
@@ -98,6 +99,7 @@ class NanopbClientTestContext {
Client& client() { return client_; }
const auto& output() const { return channel_output_; }
+ auto& output() { return channel_output_; }
private:
static constexpr uint32_t kDefaultChannelId = 1;
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
index 360cfc1d5..f71c87795 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/fake_channel_output.h
@@ -15,16 +15,28 @@
#include <cstddef>
#include <cstdint>
+#include <mutex>
#include "pw_assert/assert.h"
#include "pw_bytes/span.h"
#include "pw_containers/vector.h"
#include "pw_containers/wrapped_iterator.h"
#include "pw_rpc/internal/fake_channel_output.h"
+#include "pw_rpc/internal/lock.h"
#include "pw_rpc/nanopb/internal/common.h"
#include "pw_rpc/nanopb/internal/method.h"
namespace pw::rpc {
+namespace internal {
+
+// Forward declare for a friend statement.
+template <typename, size_t, size_t, size_t>
+class ForwardingChannelOutput;
+
+} // namespace internal
+} // namespace pw::rpc
+
+namespace pw::rpc {
namespace internal::test::nanopb {
// Forward declare for a friend statement.
@@ -44,7 +56,7 @@ class NanopbPayloadsView {
// Access the payload (rather than packet) with operator*.
Payload operator*() const {
Payload payload{};
- PW_ASSERT(serde_.Decode(Base::value(), &payload));
+ PW_ASSERT_OK(serde_.Decode(Base::value(), payload));
return payload;
}
@@ -60,7 +72,7 @@ class NanopbPayloadsView {
Payload operator[](size_t index) const {
Payload payload{};
- PW_ASSERT(serde_.Decode(view_[index], &payload));
+ PW_ASSERT_OK(serde_.Decode(view_[index], payload));
return payload;
}
@@ -112,11 +124,12 @@ class NanopbFakeChannelOutput final
// thread accesses the FakeChannelOutput.
template <auto kMethod>
NanopbPayloadsView<Request<kMethod>> requests(
- uint32_t channel_id = Channel::kUnassignedChannelId) const {
- constexpr internal::PacketType packet_type =
+ uint32_t channel_id = Channel::kUnassignedChannelId) const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ constexpr internal::pwpb::PacketType packet_type =
HasClientStream(internal::MethodInfo<kMethod>::kType)
- ? internal::PacketType::CLIENT_STREAM
- : internal::PacketType::REQUEST;
+ ? internal::pwpb::PacketType::CLIENT_STREAM
+ : internal::pwpb::PacketType::REQUEST;
return NanopbPayloadsView<Request<kMethod>>(
internal::MethodInfo<kMethod>::serde().request(),
Base::packets(),
@@ -136,11 +149,12 @@ class NanopbFakeChannelOutput final
// thread accesses the FakeChannelOutput.
template <auto kMethod>
NanopbPayloadsView<Response<kMethod>> responses(
- uint32_t channel_id = Channel::kUnassignedChannelId) const {
- constexpr internal::PacketType packet_type =
+ uint32_t channel_id = Channel::kUnassignedChannelId) const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ constexpr internal::pwpb::PacketType packet_type =
HasServerStream(internal::MethodInfo<kMethod>::kType)
- ? internal::PacketType::SERVER_STREAM
- : internal::PacketType::RESPONSE;
+ ? internal::pwpb::PacketType::SERVER_STREAM
+ : internal::pwpb::PacketType::RESPONSE;
return NanopbPayloadsView<Response<kMethod>>(
internal::MethodInfo<kMethod>::serde().response(),
Base::packets(),
@@ -153,6 +167,7 @@ class NanopbFakeChannelOutput final
template <auto kMethod>
Response<kMethod> last_response() const {
+ std::lock_guard lock(internal::test::FakeChannelOutput::mutex());
NanopbPayloadsView<Response<kMethod>> payloads = responses<kMethod>();
PW_ASSERT(!payloads.empty());
return payloads.back();
@@ -161,6 +176,8 @@ class NanopbFakeChannelOutput final
private:
template <typename, auto, uint32_t, size_t, size_t>
friend class internal::test::nanopb::NanopbInvocationContext;
+ template <typename, size_t, size_t, size_t>
+ friend class internal::ForwardingChannelOutput;
using Base =
internal::test::FakeChannelOutputBuffer<kMaxPackets,
@@ -168,12 +185,18 @@ class NanopbFakeChannelOutput final
using internal::test::FakeChannelOutput::last_packet;
+ // !!! WARNING !!!
+ //
+ // Access to the FakeChannelOutput through the NanopbPayloadsView is NOT
+ // synchronized! The NanopbPayloadsView is immediately invalidated if any
+ // thread accesses the FakeChannelOutput.
template <typename T>
NanopbPayloadsView<T> payload_structs(const internal::NanopbSerde& serde,
MethodType type,
uint32_t channel_id,
uint32_t service_id,
- uint32_t method_id) const {
+ uint32_t method_id) const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
return NanopbPayloadsView<T>(
serde, Base::packets(), type, channel_id, service_id, method_id);
}
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/common.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/common.h
index ca69c8f79..f20550eca 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/common.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/common.h
@@ -50,7 +50,12 @@ class NanopbSerde {
StatusWithSize EncodedSizeBytes(const void* proto_struct) const;
// Decodes a serialized protobuf to a Nanopb struct.
- bool Decode(ConstByteSpan buffer, void* proto_struct) const;
+ template <typename T>
+ Status Decode(ConstByteSpan buffer, T& proto_struct) const {
+ return Decode(buffer, static_cast<void*>(&proto_struct));
+ }
+
+ Status Decode(ConstByteSpan buffer, void* proto_struct) const;
private:
NanopbMessageDescriptor fields_;
@@ -67,22 +72,6 @@ class NanopbMethodSerde {
NanopbMethodSerde(const NanopbMethodSerde&) = delete;
NanopbMethodSerde& operator=(const NanopbMethodSerde&) = delete;
- StatusWithSize EncodeRequest(const void* proto_struct,
- ByteSpan buffer) const {
- return request_fields_.Encode(proto_struct, buffer);
- }
- StatusWithSize EncodeResponse(const void* proto_struct,
- ByteSpan buffer) const {
- return response_fields_.Encode(proto_struct, buffer);
- }
-
- bool DecodeRequest(ConstByteSpan buffer, void* proto_struct) const {
- return request_fields_.Decode(buffer, proto_struct);
- }
- bool DecodeResponse(ConstByteSpan buffer, void* proto_struct) const {
- return response_fields_.Decode(buffer, proto_struct);
- }
-
const NanopbSerde& request() const { return request_fields_; }
const NanopbSerde& response() const { return response_fields_; }
@@ -103,12 +92,14 @@ class NanopbServerCall;
void NanopbSendInitialRequest(ClientCall& call,
NanopbSerde serde,
const void* payload)
- PW_UNLOCK_FUNCTION(rpc_lock());
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
// [Client/Server] Encodes and sends a client or server stream message.
// active() must be true.
-Status NanopbSendStream(Call& call, const void* payload, NanopbSerde serde)
- PW_LOCKS_EXCLUDED(rpc_lock());
+Status NanopbSendStream(Call& call,
+ const void* payload,
+ const NanopbMethodSerde* serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
// [Server] Encodes and sends the final response message.
// Returns Status::FailedPrecondition if active() is false.
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method.h
index fb84fc123..ef7bdd929 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method.h
@@ -16,7 +16,6 @@
#include <algorithm>
#include <cstddef>
#include <cstdint>
-#include <span>
#include <type_traits>
#include "pw_function/function.h"
@@ -26,6 +25,7 @@
#include "pw_rpc/method_type.h"
#include "pw_rpc/nanopb/internal/common.h"
#include "pw_rpc/nanopb/server_reader_writer.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -173,7 +173,7 @@ class NanopbMethod : public Method {
// generically in the Function union, defined below.
//
// In optimized builds, the compiler inlines the user-defined function into
- // this wrapper, elminating any overhead.
+ // this wrapper, eliminating any overhead.
constexpr SynchronousUnaryFunction wrapper =
[](Service& service, const void* req, void* resp) {
return CallMethodImplFunction<kMethod>(
@@ -198,7 +198,7 @@ class NanopbMethod : public Method {
// generically in the Function union, defined below.
//
// In optimized builds, the compiler inlines the user-defined function into
- // this wrapper, elminating any overhead.
+ // this wrapper, eliminating any overhead.
constexpr UnaryRequestFunction wrapper =
[](Service& service, const void* req, NanopbServerCall& resp) {
return CallMethodImplFunction<kMethod>(
@@ -384,7 +384,7 @@ class NanopbMethod : public Method {
template <typename Request>
static void ClientStreamingInvoker(const CallContext& context, const Packet&)
PW_UNLOCK_FUNCTION(rpc_lock()) {
- BaseNanopbServerReader<Request> reader(context,
+ BaseNanopbServerReader<Request> reader(context.ClaimLocked(),
MethodType::kClientStreaming);
rpc_lock().unlock();
static_cast<const NanopbMethod&>(context.method())
@@ -397,7 +397,7 @@ class NanopbMethod : public Method {
const Packet&)
PW_UNLOCK_FUNCTION(rpc_lock()) {
BaseNanopbServerReader<Request> reader_writer(
- context, MethodType::kBidirectionalStreaming);
+ context.ClaimLocked(), MethodType::kBidirectionalStreaming);
rpc_lock().unlock();
static_cast<const NanopbMethod&>(context.method())
.function_.stream_request(context.service(), reader_writer);
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method_union.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method_union.h
index aa2a6729d..10c5aa197 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method_union.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/internal/method_union.h
@@ -53,6 +53,6 @@ constexpr auto GetNanopbOrRawMethodFor(
} else {
return InvalidMethod<kMethod, kType, RawMethod>(id);
}
-};
+}
} // namespace pw::rpc::internal
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
index b52c25f51..06bb538a4 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/server_reader_writer.h
@@ -39,18 +39,22 @@ class InvocationContext;
} // namespace test
-class NanopbServerCall : public internal::ServerCall {
+class NanopbServerCall : public ServerCall {
public:
constexpr NanopbServerCall() : serde_(nullptr) {}
- NanopbServerCall(const CallContext& context, MethodType type);
+ NanopbServerCall(const LockedCallContext& context, MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
Status SendUnaryResponse(const void* payload, Status status)
PW_LOCKS_EXCLUDED(rpc_lock()) {
return SendFinalResponse(*this, payload, status);
}
- const NanopbMethodSerde& serde() const { return *serde_; }
+ const NanopbMethodSerde& serde() const
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return *serde_;
+ }
protected:
NanopbServerCall(NanopbServerCall&& other) PW_LOCKS_EXCLUDED(rpc_lock()) {
@@ -59,7 +63,7 @@ class NanopbServerCall : public internal::ServerCall {
NanopbServerCall& operator=(NanopbServerCall&& other)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- internal::LockGuard lock(internal::rpc_lock());
+ RpcLockGuard lock;
MoveNanopbServerCallFrom(other);
return *this;
}
@@ -70,14 +74,13 @@ class NanopbServerCall : public internal::ServerCall {
serde_ = other.serde_;
}
- Status SendServerStream(const void* payload) PW_LOCKS_EXCLUDED(rpc_lock());
-
- bool DecodeRequest(ConstByteSpan payload, void* request_struct) const {
- return serde_->DecodeRequest(payload, request_struct);
+ Status SendServerStream(const void* payload) PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ return NanopbSendStream(*this, payload, serde_);
}
private:
- const NanopbMethodSerde* serde_;
+ const NanopbMethodSerde* serde_ PW_GUARDED_BY(rpc_lock());
};
// The BaseNanopbServerReader serves as the base for the ServerReader and
@@ -86,7 +89,8 @@ class NanopbServerCall : public internal::ServerCall {
template <typename Request>
class BaseNanopbServerReader : public NanopbServerCall {
public:
- BaseNanopbServerReader(const internal::CallContext& context, MethodType type)
+ BaseNanopbServerReader(const LockedCallContext& context, MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: NanopbServerCall(context, type) {}
protected:
@@ -99,34 +103,32 @@ class BaseNanopbServerReader : public NanopbServerCall {
BaseNanopbServerReader& operator=(BaseNanopbServerReader&& other)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- internal::LockGuard lock(internal::rpc_lock());
+ RpcLockGuard lock;
MoveNanopbServerCallFrom(other);
- set_on_next_locked(std::move(other.nanopb_on_next_));
+ set_nanopb_on_next_locked(std::move(other.nanopb_on_next_));
return *this;
}
void set_on_next(Function<void(const Request& request)>&& on_next)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- internal::LockGuard lock(internal::rpc_lock());
- set_on_next_locked(std::move(on_next));
+ RpcLockGuard lock;
+ set_nanopb_on_next_locked(std::move(on_next));
}
private:
- void set_on_next_locked(Function<void(const Request& request)>&& on_next)
+ void set_nanopb_on_next_locked(
+ Function<void(const Request& request)>&& on_next)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
nanopb_on_next_ = std::move(on_next);
- internal::Call::set_on_next_locked([this](ConstByteSpan payload) {
- if (nanopb_on_next_) {
- Request request_struct{};
- if (DecodeRequest(payload, &request_struct)) {
- nanopb_on_next_(request_struct);
- }
- }
- });
+ Call::set_on_next_locked(
+ [this](ConstByteSpan payload) PW_NO_LOCK_SAFETY_ANALYSIS {
+ DecodeToStructAndInvokeOnNext(
+ payload, serde().request(), nanopb_on_next_);
+ });
}
- Function<void(const Request&)> nanopb_on_next_;
+ Function<void(const Request&)> nanopb_on_next_ PW_GUARDED_BY(rpc_lock());
};
} // namespace internal
@@ -146,7 +148,8 @@ class NanopbServerReaderWriter
template <auto kMethod, typename ServiceImpl>
[[nodiscard]] static NanopbServerReaderWriter Open(Server& server,
uint32_t channel_id,
- ServiceImpl& service) {
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
using Info = internal::MethodInfo<kMethod>;
static_assert(std::is_same_v<Request, typename Info::Request>,
"The request type of a NanopbServerReaderWriter must match "
@@ -154,12 +157,13 @@ class NanopbServerReaderWriter
static_assert(std::is_same_v<Response, typename Info::Response>,
"The response type of a NanopbServerReaderWriter must match "
"the method.");
- internal::LockGuard lock(internal::rpc_lock());
- return {server.OpenContext<kMethod, MethodType::kBidirectionalStreaming>(
+ return server.OpenCall<NanopbServerReaderWriter<Request, Response>,
+ kMethod,
+ MethodType::kBidirectionalStreaming>(
channel_id,
service,
internal::MethodLookup::GetNanopbMethod<ServiceImpl,
- Info::kMethodId>())};
+ Info::kMethodId>());
}
constexpr NanopbServerReaderWriter() = default;
@@ -193,11 +197,13 @@ class NanopbServerReaderWriter
private:
friend class internal::NanopbMethod;
+ friend class Server;
template <typename, typename, uint32_t>
friend class internal::test::InvocationContext;
- NanopbServerReaderWriter(const internal::CallContext& context)
+ NanopbServerReaderWriter(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: internal::BaseNanopbServerReader<Request>(
context, MethodType::kBidirectionalStreaming) {}
};
@@ -213,7 +219,8 @@ class NanopbServerReader : private internal::BaseNanopbServerReader<Request> {
template <auto kMethod, typename ServiceImpl>
[[nodiscard]] static NanopbServerReader Open(Server& server,
uint32_t channel_id,
- ServiceImpl& service) {
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
using Info = internal::MethodInfo<kMethod>;
static_assert(
std::is_same_v<Request, typename Info::Request>,
@@ -221,12 +228,13 @@ class NanopbServerReader : private internal::BaseNanopbServerReader<Request> {
static_assert(
std::is_same_v<Response, typename Info::Response>,
"The response type of a NanopbServerReader must match the method.");
- internal::LockGuard lock(internal::rpc_lock());
- return {server.OpenContext<kMethod, MethodType::kClientStreaming>(
+ return server.OpenCall<NanopbServerReader<Request, Response>,
+ kMethod,
+ MethodType::kClientStreaming>(
channel_id,
service,
internal::MethodLookup::GetNanopbMethod<ServiceImpl,
- Info::kMethodId>())};
+ Info::kMethodId>());
}
// Allow default construction so that users can declare a variable into which
@@ -250,11 +258,13 @@ class NanopbServerReader : private internal::BaseNanopbServerReader<Request> {
private:
friend class internal::NanopbMethod;
+ friend class Server;
template <typename, typename, uint32_t>
friend class internal::test::InvocationContext;
- NanopbServerReader(const internal::CallContext& context)
+ NanopbServerReader(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: internal::BaseNanopbServerReader<Request>(
context, MethodType::kClientStreaming) {}
};
@@ -270,17 +280,19 @@ class NanopbServerWriter : private internal::NanopbServerCall {
template <auto kMethod, typename ServiceImpl>
[[nodiscard]] static NanopbServerWriter Open(Server& server,
uint32_t channel_id,
- ServiceImpl& service) {
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
using Info = internal::MethodInfo<kMethod>;
static_assert(
std::is_same_v<Response, typename Info::Response>,
"The response type of a NanopbServerWriter must match the method.");
- internal::LockGuard lock(internal::rpc_lock());
- return {server.OpenContext<kMethod, MethodType::kServerStreaming>(
+ return server.OpenCall<NanopbServerWriter<Response>,
+ kMethod,
+ MethodType::kServerStreaming>(
channel_id,
service,
internal::MethodLookup::GetNanopbMethod<ServiceImpl,
- Info::kMethodId>())};
+ Info::kMethodId>());
}
// Allow default construction so that users can declare a variable into which
@@ -314,11 +326,13 @@ class NanopbServerWriter : private internal::NanopbServerCall {
private:
friend class internal::NanopbMethod;
+ friend class Server;
template <typename, typename, uint32_t>
friend class internal::test::InvocationContext;
- NanopbServerWriter(const internal::CallContext& context)
+ NanopbServerWriter(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: internal::NanopbServerCall(context, MethodType::kServerStreaming) {}
};
@@ -331,17 +345,18 @@ class NanopbUnaryResponder : private internal::NanopbServerCall {
template <auto kMethod, typename ServiceImpl>
[[nodiscard]] static NanopbUnaryResponder Open(Server& server,
uint32_t channel_id,
- ServiceImpl& service) {
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
using Info = internal::MethodInfo<kMethod>;
static_assert(
std::is_same_v<Response, typename Info::Response>,
"The response type of a NanopbUnaryResponder must match the method.");
- internal::LockGuard lock(internal::rpc_lock());
- return {server.OpenContext<kMethod, MethodType::kUnary>(
- channel_id,
- service,
- internal::MethodLookup::GetNanopbMethod<ServiceImpl,
- Info::kMethodId>())};
+ return server
+ .OpenCall<NanopbUnaryResponder<Response>, kMethod, MethodType::kUnary>(
+ channel_id,
+ service,
+ internal::MethodLookup::GetNanopbMethod<ServiceImpl,
+ Info::kMethodId>());
}
// Allow default construction so that users can declare a variable into which
@@ -371,15 +386,14 @@ class NanopbUnaryResponder : private internal::NanopbServerCall {
private:
friend class internal::NanopbMethod;
+ friend class Server;
template <typename, typename, uint32_t>
friend class internal::test::InvocationContext;
- NanopbUnaryResponder(const internal::CallContext& context)
+ NanopbUnaryResponder(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: internal::NanopbServerCall(context, MethodType::kUnary) {}
};
-// TODO(hepler): "pw::rpc::ServerWriter" should not be specific to Nanopb.
-template <typename T>
-using ServerWriter = NanopbServerWriter<T>;
} // namespace pw::rpc
diff --git a/pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h b/pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h
index 972cb62a3..1f98369dd 100644
--- a/pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h
+++ b/pw_rpc/nanopb/public/pw_rpc/nanopb/test_method_context.h
@@ -122,8 +122,8 @@ class NanopbInvocationContext
// Gives access to the RPC's most recent response.
Response response() const {
Response response{};
- PW_ASSERT(kMethodInfo.serde().DecodeResponse(Base::responses().back(),
- &response));
+ PW_ASSERT_OK(kMethodInfo.serde().response().Decode(Base::responses().back(),
+ response));
return response;
}
@@ -131,8 +131,8 @@ class NanopbInvocationContext
// to parse the nanopb. Use this version when you need to set pb_callback_t
// fields in the Response object before parsing.
void response(Response& response) const {
- PW_ASSERT(kMethodInfo.serde().DecodeResponse(Base::responses().back(),
- &response));
+ PW_ASSERT_OK(kMethodInfo.serde().response().Decode(Base::responses().back(),
+ response));
}
NanopbPayloadsView<Response> responses() const {
@@ -140,7 +140,7 @@ class NanopbInvocationContext
kMethodInfo.serde().response(),
MethodTraits<decltype(kMethod)>::kType,
Base::channel_id(),
- Base::service().id(),
+ internal::UnwrapServiceId(Base::service().service_id()),
kMethodId);
}
@@ -154,8 +154,8 @@ class NanopbInvocationContext
template <size_t kEncodingBufferSizeBytes = 128>
void SendClientStream(const Request& request) PW_LOCKS_EXCLUDED(rpc_lock()) {
std::array<std::byte, kEncodingBufferSizeBytes> buffer;
- Base::SendClientStream(std::span(buffer).first(
- kMethodInfo.serde().EncodeRequest(&request, buffer).size()));
+ Base::SendClientStream(span(buffer).first(
+ kMethodInfo.serde().request().Encode(&request, buffer).size()));
}
private:
diff --git a/pw_rpc/nanopb/pw_rpc_nanopb_private/internal_test_utils.h b/pw_rpc/nanopb/pw_rpc_nanopb_private/internal_test_utils.h
index 40efe8549..ab63bfef8 100644
--- a/pw_rpc/nanopb/pw_rpc_nanopb_private/internal_test_utils.h
+++ b/pw_rpc/nanopb/pw_rpc_nanopb_private/internal_test_utils.h
@@ -13,10 +13,9 @@
// the License.
#pragma once
-#include <span>
-
#include "pb_decode.h"
#include "pb_encode.h"
+#include "pw_span/span.h"
namespace pw::rpc::internal {
@@ -33,28 +32,27 @@ namespace pw::rpc::internal {
#define _PW_ENCODE_PB_IMPL(proto, result, unique, ...) \
std::array<pb_byte_t, 2 * sizeof(proto)> _pb_buffer_##unique{}; \
- const std::span result = \
+ const span result = \
::pw::rpc::internal::EncodeProtobuf<proto, proto##_fields>( \
proto{__VA_ARGS__}, _pb_buffer_##unique)
template <typename T, auto kFields>
-std::span<const std::byte> EncodeProtobuf(const T& protobuf,
- std::span<pb_byte_t> buffer) {
+span<const std::byte> EncodeProtobuf(const T& protobuf,
+ span<pb_byte_t> buffer) {
auto output = pb_ostream_from_buffer(buffer.data(), buffer.size());
EXPECT_TRUE(pb_encode(&output, kFields, &protobuf));
- return std::as_bytes(buffer.first(output.bytes_written));
+ return as_bytes(buffer.first(output.bytes_written));
}
// Decodes a protobuf to a nanopb struct named by result.
-#define PW_DECODE_PB(proto, result, buffer) \
- proto result; \
- ::pw::rpc::internal::DecodeProtobuf<proto, proto##_fields>( \
- std::span(reinterpret_cast<const pb_byte_t*>(buffer.data()), \
- buffer.size()), \
+#define PW_DECODE_PB(proto, result, buffer) \
+ proto result; \
+ ::pw::rpc::internal::DecodeProtobuf<proto, proto##_fields>( \
+ span(reinterpret_cast<const pb_byte_t*>(buffer.data()), buffer.size()), \
result);
template <typename T, auto kFields>
-void DecodeProtobuf(std::span<const pb_byte_t> buffer, T& protobuf) {
+void DecodeProtobuf(span<const pb_byte_t> buffer, T& protobuf) {
auto input = pb_istream_from_buffer(buffer.data(), buffer.size());
EXPECT_TRUE(pb_decode(&input, kFields, &protobuf));
}
diff --git a/pw_rpc/nanopb/serde_test.cc b/pw_rpc/nanopb/serde_test.cc
index aa0ee87b3..ebc337459 100644
--- a/pw_rpc/nanopb/serde_test.cc
+++ b/pw_rpc/nanopb/serde_test.cc
@@ -47,7 +47,7 @@ TEST(NanopbSerde, Decode) {
constexpr std::byte buffer[]{std::byte{1} << 3, std::byte{3}};
pw_rpc_test_TestRequest proto = {};
- EXPECT_TRUE(kTestRequest.Decode(buffer, &proto));
+ EXPECT_EQ(OkStatus(), kTestRequest.Decode(buffer, &proto));
EXPECT_EQ(3, proto.integer);
EXPECT_EQ(0u, proto.status_code);
diff --git a/pw_rpc/nanopb/server_reader_writer.cc b/pw_rpc/nanopb/server_reader_writer.cc
index f0517bc3e..92b01246b 100644
--- a/pw_rpc/nanopb/server_reader_writer.cc
+++ b/pw_rpc/nanopb/server_reader_writer.cc
@@ -18,16 +18,10 @@
namespace pw::rpc::internal {
-NanopbServerCall::NanopbServerCall(const CallContext& context, MethodType type)
- : internal::ServerCall(context, type),
- serde_(&static_cast<const internal::NanopbMethod&>(context.method())
- .serde()) {}
-
-Status NanopbServerCall::SendServerStream(const void* payload) {
- if (!active()) {
- return Status::FailedPrecondition();
- }
- return NanopbSendStream(*this, payload, serde_->response());
-}
+NanopbServerCall::NanopbServerCall(const LockedCallContext& context,
+ MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : ServerCall(context, CallProperties(type, kServerCall, kProtoStruct)),
+ serde_(&static_cast<const NanopbMethod&>(context.method()).serde()) {}
} // namespace pw::rpc::internal
diff --git a/pw_rpc/nanopb/server_reader_writer_test.cc b/pw_rpc/nanopb/server_reader_writer_test.cc
index 98a3b52bc..909063de0 100644
--- a/pw_rpc/nanopb/server_reader_writer_test.cc
+++ b/pw_rpc/nanopb/server_reader_writer_test.cc
@@ -14,6 +14,8 @@
#include "pw_rpc/nanopb/server_reader_writer.h"
+#include <optional>
+
#include "gtest/gtest.h"
#include "pw_rpc/nanopb/fake_channel_output.h"
#include "pw_rpc/nanopb/test_method_context.h"
@@ -54,7 +56,7 @@ struct ReaderWriterTestContext {
ReaderWriterTestContext()
: channel(Channel::Create<kChannelId>(&output)),
- server(std::span(&channel, 1)) {}
+ server(span(&channel, 1)) {}
TestServiceImpl service;
NanopbFakeChannelOutput<4> output;
@@ -266,6 +268,32 @@ TEST(RawServerReaderWriter, Open_UnknownChannel) {
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
}
+TEST(RawServerReaderWriter, Open_MultipleTimes_CancelsPrevious) {
+ ReaderWriterTestContext<TestService::TestBidirectionalStreamRpc> ctx;
+
+ NanopbServerReaderWriter one =
+ NanopbServerReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>::
+ Open<TestService::TestBidirectionalStreamRpc>(
+ ctx.server, ctx.kChannelId, ctx.service);
+
+ std::optional<Status> error;
+ one.set_on_error([&error](Status status) { error = status; });
+
+ ASSERT_TRUE(one.active());
+
+ NanopbServerReaderWriter two =
+ NanopbServerReaderWriter<pw_rpc_test_TestRequest,
+ pw_rpc_test_TestStreamResponse>::
+ Open<TestService::TestBidirectionalStreamRpc>(
+ ctx.server, ctx.kChannelId, ctx.service);
+
+ EXPECT_FALSE(one.active());
+ EXPECT_TRUE(two.active());
+
+ EXPECT_EQ(Status::Cancelled(), error);
+}
+
TEST(NanopbServerReader, CallbacksMoveCorrectly) {
PW_NANOPB_TEST_METHOD_CONTEXT(TestServiceImpl, TestClientStreamRpc) ctx;
diff --git a/pw_rpc/nanopb/synchronous_call_test.cc b/pw_rpc/nanopb/synchronous_call_test.cc
new file mode 100644
index 000000000..4ea8402d6
--- /dev/null
+++ b/pw_rpc/nanopb/synchronous_call_test.cc
@@ -0,0 +1,235 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/synchronous_call.h"
+
+#include <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/nanopb/fake_channel_output.h"
+#include "pw_rpc_test_protos/test.rpc.pb.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_thread/thread.h"
+#include "pw_work_queue/test_thread.h"
+#include "pw_work_queue/work_queue.h"
+
+namespace pw::rpc::test {
+namespace {
+
+using pw::rpc::test::pw_rpc::nanopb::TestService;
+using MethodInfo = internal::MethodInfo<TestService::TestUnaryRpc>;
+
+class SynchronousCallTest : public ::testing::Test {
+ public:
+ SynchronousCallTest()
+ : channels_({{Channel::Create<42>(&fake_output_)}}), client_(channels_) {}
+
+ void SetUp() override {
+ work_thread_ =
+ thread::Thread(work_queue::test::WorkQueueThreadOptions(), work_queue_);
+ }
+
+ void TearDown() override {
+ work_queue_.RequestStop();
+ work_thread_.join();
+ }
+
+ protected:
+ using FakeChannelOutput = NanopbFakeChannelOutput<2>;
+
+ void OnSend(span<const std::byte> buffer, Status status) {
+ if (!status.ok()) {
+ return;
+ }
+ auto result = internal::Packet::FromBuffer(buffer);
+ EXPECT_TRUE(result.ok());
+ request_packet_ = *result;
+
+ EXPECT_TRUE(work_queue_.PushWork([this]() { SendResponse(); }).ok());
+ }
+
+ void SendResponse() {
+ std::array<std::byte, 256> buffer;
+ std::array<std::byte, 32> payload_buffer;
+
+ StatusWithSize size_status =
+ MethodInfo::serde().response().Encode(&response_, payload_buffer);
+ EXPECT_TRUE(size_status.ok());
+
+ auto response =
+ internal::Packet::Response(request_packet_, response_status_);
+ response.set_payload({payload_buffer.data(), size_status.size()});
+ EXPECT_TRUE(client_.ProcessPacket(response.Encode(buffer).value()).ok());
+ }
+
+ void set_response(const pw_rpc_test_TestResponse& response,
+ Status response_status = OkStatus()) {
+ response_ = response;
+ response_status_ = response_status;
+ output().set_on_send([this](span<const std::byte> buffer, Status status) {
+ OnSend(buffer, status);
+ });
+ }
+
+ MethodInfo::GeneratedClient generated_client() {
+ return MethodInfo::GeneratedClient(client(), channel().id());
+ }
+
+ FakeChannelOutput& output() { return fake_output_; }
+ const Channel& channel() const { return channels_.front(); }
+ Client& client() { return client_; }
+
+ private:
+ FakeChannelOutput fake_output_;
+ std::array<Channel, 1> channels_;
+ Client client_;
+ thread::Thread work_thread_;
+ work_queue::WorkQueueWithBuffer<1> work_queue_;
+ pw_rpc_test_TestResponse response_{};
+ Status response_status_ = OkStatus();
+ internal::Packet request_packet_;
+};
+
+TEST_F(SynchronousCallTest, SynchronousCallSuccess) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+ pw_rpc_test_TestResponse response{.value = 42, .repeated_field{}};
+
+ set_response(response, OkStatus());
+
+ auto result = SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), request);
+ EXPECT_TRUE(result.ok());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+TEST_F(SynchronousCallTest, SynchronousCallServerError) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+ pw_rpc_test_TestResponse response{.value = 42, .repeated_field{}};
+
+ set_response(response, Status::Internal());
+
+ auto result = SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), request);
+ EXPECT_TRUE(result.is_error());
+ EXPECT_EQ(result.status(), Status::Internal());
+
+ // We should still receive the response
+ EXPECT_TRUE(result.is_server_response());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+TEST_F(SynchronousCallTest, SynchronousCallRpcError) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+
+ // Internally, if Channel receives a non-ok status from the
+ // ChannelOutput::Send, it will always return Unknown.
+ output().set_send_status(Status::Unknown());
+
+ auto result = SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), request);
+ EXPECT_TRUE(result.is_rpc_error());
+ EXPECT_EQ(result.status(), Status::Unknown());
+}
+
+TEST_F(SynchronousCallTest, SynchronousCallForTimeoutError) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+
+ auto result = SynchronousCallFor<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ request,
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1)));
+
+ EXPECT_TRUE(result.is_timeout());
+ EXPECT_EQ(result.status(), Status::DeadlineExceeded());
+}
+
+TEST_F(SynchronousCallTest, SynchronousCallUntilTimeoutError) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+
+ auto result = SynchronousCallUntil<TestService::TestUnaryRpc>(
+ client(), channel().id(), request, chrono::SystemClock::now());
+
+ EXPECT_TRUE(result.is_timeout());
+ EXPECT_EQ(result.status(), Status::DeadlineExceeded());
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallSuccess) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+ pw_rpc_test_TestResponse response{.value = 42, .repeated_field{}};
+
+ set_response(response, OkStatus());
+
+ auto result =
+ SynchronousCall<TestService::TestUnaryRpc>(generated_client(), request);
+ EXPECT_TRUE(result.ok());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallServerError) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+ pw_rpc_test_TestResponse response{.value = 42, .repeated_field{}};
+
+ set_response(response, Status::Internal());
+
+ auto result =
+ SynchronousCall<TestService::TestUnaryRpc>(generated_client(), request);
+ EXPECT_TRUE(result.is_error());
+ EXPECT_EQ(result.status(), Status::Internal());
+
+ // We should still receive the response
+ EXPECT_TRUE(result.is_server_response());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallRpcError) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+
+ // Internally, if Channel receives a non-ok status from the
+ // ChannelOutput::Send, it will always return Unknown.
+ output().set_send_status(Status::Unknown());
+
+ auto result =
+ SynchronousCall<TestService::TestUnaryRpc>(generated_client(), request);
+ EXPECT_TRUE(result.is_rpc_error());
+ EXPECT_EQ(result.status(), Status::Unknown());
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallForTimeoutError) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+
+ auto result = SynchronousCallFor<TestService::TestUnaryRpc>(
+ generated_client(),
+ request,
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1)));
+
+ EXPECT_TRUE(result.is_timeout());
+ EXPECT_EQ(result.status(), Status::DeadlineExceeded());
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallUntilTimeoutError) {
+ pw_rpc_test_TestRequest request{.integer = 5, .status_code = 0};
+
+ auto result = SynchronousCallUntil<TestService::TestUnaryRpc>(
+ generated_client(), request, chrono::SystemClock::now());
+
+ EXPECT_TRUE(result.is_timeout());
+ EXPECT_EQ(result.status(), Status::DeadlineExceeded());
+}
+} // namespace
+} // namespace pw::rpc::test
diff --git a/pw_rpc/packet.cc b/pw_rpc/packet.cc
index de37ab1fc..3e9803c23 100644
--- a/pw_rpc/packet.cc
+++ b/pw_rpc/packet.cc
@@ -18,6 +18,10 @@
namespace pw::rpc::internal {
+using pwpb::PacketType;
+
+namespace RpcPacket = pwpb::RpcPacket;
+
Result<Packet> Packet::FromBuffer(ConstByteSpan data) {
Packet packet;
Status status;
@@ -28,7 +32,7 @@ Result<Packet> Packet::FromBuffer(ConstByteSpan data) {
static_cast<RpcPacket::Fields>(decoder.FieldNumber());
switch (field) {
- case RpcPacket::Fields::TYPE: {
+ case RpcPacket::Fields::kType: {
uint32_t value;
// A decode error will propagate from Next() and terminate the loop.
decoder.ReadUint32(&value).IgnoreError();
@@ -36,27 +40,27 @@ Result<Packet> Packet::FromBuffer(ConstByteSpan data) {
break;
}
- case RpcPacket::Fields::CHANNEL_ID:
+ case RpcPacket::Fields::kChannelId:
// A decode error will propagate from Next() and terminate the loop.
decoder.ReadUint32(&packet.channel_id_).IgnoreError();
break;
- case RpcPacket::Fields::SERVICE_ID:
+ case RpcPacket::Fields::kServiceId:
// A decode error will propagate from Next() and terminate the loop.
decoder.ReadFixed32(&packet.service_id_).IgnoreError();
break;
- case RpcPacket::Fields::METHOD_ID:
+ case RpcPacket::Fields::kMethodId:
// A decode error will propagate from Next() and terminate the loop.
decoder.ReadFixed32(&packet.method_id_).IgnoreError();
break;
- case RpcPacket::Fields::PAYLOAD:
+ case RpcPacket::Fields::kPayload:
// A decode error will propagate from Next() and terminate the loop.
decoder.ReadBytes(&packet.payload_).IgnoreError();
break;
- case RpcPacket::Fields::STATUS: {
+ case RpcPacket::Fields::kStatus: {
uint32_t value;
// A decode error will propagate from Next() and terminate the loop.
decoder.ReadUint32(&value).IgnoreError();
@@ -64,7 +68,7 @@ Result<Packet> Packet::FromBuffer(ConstByteSpan data) {
break;
}
- case RpcPacket::Fields::CALL_ID:
+ case RpcPacket::Fields::kCallId:
// A decode error will propagate from Next() and terminate the loop.
decoder.ReadUint32(&packet.call_id_).IgnoreError();
break;
@@ -75,12 +79,6 @@ Result<Packet> Packet::FromBuffer(ConstByteSpan data) {
return status;
}
- // TODO(pwbug/512): CANCEL is equivalent to CLIENT_ERROR with status
- // CANCELLED. Remove this workaround when CANCEL is removed.
- if (packet.type() == PacketType::DEPRECATED_CANCEL) {
- packet.set_status(Status::Cancelled());
- }
-
return packet;
}
diff --git a/pw_rpc/packet_meta.cc b/pw_rpc/packet_meta.cc
new file mode 100644
index 000000000..5de0ba4b9
--- /dev/null
+++ b/pw_rpc/packet_meta.cc
@@ -0,0 +1,39 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// clang-format off
+#include "pw_rpc/internal/log_config.h" // PW_LOG_* macros must be first.
+
+#include "pw_rpc/packet_meta.h"
+// clang-format on
+
+#include "pw_log/log.h"
+#include "pw_rpc/internal/channel.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_span/span.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::rpc {
+
+Result<PacketMeta> PacketMeta::FromBuffer(ConstByteSpan data) {
+ PW_TRY_ASSIGN(internal::Packet packet, internal::Packet::FromBuffer(data));
+ if (packet.channel_id() == internal::Channel::kUnassignedChannelId ||
+ packet.service_id() == 0 || packet.method_id() == 0) {
+ PW_LOG_WARN("Received malformed pw_rpc packet");
+ return Status::DataLoss();
+ }
+ return PacketMeta(packet);
+}
+
+} // namespace pw::rpc \ No newline at end of file
diff --git a/pw_rpc/packet_meta_test.cc b/pw_rpc/packet_meta_test.cc
new file mode 100644
index 000000000..d21f3a48d
--- /dev/null
+++ b/pw_rpc/packet_meta_test.cc
@@ -0,0 +1,57 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/packet_meta.h"
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/packet.h"
+
+namespace pw::rpc {
+namespace {
+
+TEST(PacketMeta, FromBufferDecodesValidMinimalPacket) {
+ const uint32_t kChannelId = 12;
+ const ServiceId kServiceId = internal::WrapServiceId(0xdeadbeef);
+ const uint32_t kMethodId = 44;
+
+ internal::Packet packet;
+ packet.set_channel_id(kChannelId);
+ packet.set_service_id(internal::UnwrapServiceId(kServiceId));
+ packet.set_type(internal::pwpb::PacketType::RESPONSE);
+ packet.set_method_id(kMethodId);
+
+ std::byte buffer[128];
+ Result<ConstByteSpan> encode_result = packet.Encode(buffer);
+ ASSERT_EQ(encode_result.status(), OkStatus());
+
+ Result<PacketMeta> decode_result = PacketMeta::FromBuffer(*encode_result);
+ ASSERT_EQ(decode_result.status(), OkStatus());
+ EXPECT_EQ(decode_result->channel_id(), kChannelId);
+ EXPECT_EQ(decode_result->service_id(), kServiceId);
+ EXPECT_TRUE(decode_result->destination_is_client());
+}
+
+TEST(PacketMeta, FromBufferFailsOnIncompletePacket) {
+ internal::Packet packet;
+
+ std::byte buffer[128];
+ Result<ConstByteSpan> encode_result = packet.Encode(buffer);
+ ASSERT_EQ(encode_result.status(), OkStatus());
+
+ Result<PacketMeta> decode_result = PacketMeta::FromBuffer(*encode_result);
+ ASSERT_EQ(decode_result.status(), Status::DataLoss());
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/packet_test.cc b/pw_rpc/packet_test.cc
index 1edff7a33..a0d7da3b6 100644
--- a/pw_rpc/packet_test.cc
+++ b/pw_rpc/packet_test.cc
@@ -22,6 +22,7 @@ namespace pw::rpc::internal {
namespace {
using protobuf::FieldKey;
+using ::pw::rpc::internal::pwpb::PacketType;
using std::byte;
constexpr auto kPayload = bytes::Array<0x82, 0x02, 0xff, 0xff>();
@@ -130,7 +131,7 @@ TEST(Packet, EncodeDecode) {
Result result = packet.Encode(buffer);
ASSERT_EQ(result.status(), OkStatus());
- std::span<byte> packet_data(buffer, result.value().size());
+ span<byte> packet_data(buffer, result.value().size());
auto decode_result = Packet::FromBuffer(packet_data);
ASSERT_TRUE(decode_result.ok());
diff --git a/pw_rpc/public/pw_rpc/channel.h b/pw_rpc/public/pw_rpc/channel.h
index 849595602..19c65b428 100644
--- a/pw_rpc/public/pw_rpc/channel.h
+++ b/pw_rpc/public/pw_rpc/channel.h
@@ -15,13 +15,14 @@
#include <cstdint>
#include <limits>
-#include <span>
#include <type_traits>
#include "pw_assert/assert.h"
#include "pw_bytes/span.h"
#include "pw_result/result.h"
#include "pw_rpc/internal/lock.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::rpc {
@@ -30,6 +31,18 @@ namespace pw::rpc {
// packet is corrupt and the channel ID could not be found.
Result<uint32_t> ExtractChannelId(ConstByteSpan packet);
+// Returns the maximum size of the payload of an RPC packet. This can be used
+// when allocating response encode buffers for RPC services.
+// If the RPC encode buffer is too small to fit RPC packet headers, this will
+// return zero.
+constexpr size_t MaxSafePayloadSize(
+ size_t encode_buffer_size = cfg::kEncodingBufferSizeBytes) {
+ return encode_buffer_size > internal::Packet::kMinEncodedSizeWithoutPayload
+ ? encode_buffer_size -
+ internal::Packet::kMinEncodedSizeWithoutPayload
+ : 0;
+}
+
class ChannelOutput {
public:
// Returned from MaximumTransmissionUnit() to indicate that this ChannelOutput
@@ -68,7 +81,7 @@ class ChannelOutput {
// The buffer provided in packet must NOT be accessed outside of this
// function. It must be sent immediately or copied elsewhere before the
// function returns.
- virtual Status Send(std::span<const std::byte> buffer)
+ virtual Status Send(span<const std::byte> buffer)
PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock()) = 0;
private:
@@ -104,7 +117,7 @@ class Channel {
// Creates a dynamically assignable channel without a set ID or output.
constexpr Channel() : id_(kUnassignedChannelId), output_(nullptr) {}
- // TODO(pwbug/620): Remove the Configure and set_channel_output functions.
+ // TODO(b/234876441): Remove the Configure and set_channel_output functions.
// Users should call CloseChannel() / OpenChannel() to change a channel.
// This ensures calls are properly update and works consistently between
// static and dynamic channel allocation.
diff --git a/pw_rpc/public/pw_rpc/client.h b/pw_rpc/public/pw_rpc/client.h
index db782989a..bcaf1fdff 100644
--- a/pw_rpc/public/pw_rpc/client.h
+++ b/pw_rpc/public/pw_rpc/client.h
@@ -14,21 +14,27 @@
#pragma once
#include <cstddef>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_rpc/channel.h"
#include "pw_rpc/internal/channel.h"
#include "pw_rpc/internal/endpoint.h"
#include "pw_rpc/internal/lock.h"
+#include "pw_span/span.h"
namespace pw::rpc {
class Client : public internal::Endpoint {
public:
+ // If dynamic allocation is supported, it is not necessary to preallocate a
+ // channels list.
+#if PW_RPC_DYNAMIC_ALLOCATION
+ _PW_RPC_CONSTEXPR Client() = default;
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
// Creates a client that uses a set of RPC channels. Channels can be shared
- // between a client and a server, but not between multiple clients.
- _PW_RPC_CONSTEXPR Client(std::span<Channel> channels) : Endpoint(channels) {}
+ // between multiple clients and servers.
+ _PW_RPC_CONSTEXPR Client(span<Channel> channels) : Endpoint(channels) {}
// Processes an incoming RPC packet. The packet may be an RPC response or a
// control packet, the result of which is processed in this function. Returns
@@ -45,6 +51,8 @@ class Client : public internal::Endpoint {
private:
// Remove these internal::Endpoint functions from the public interface.
using Endpoint::active_call_count;
+ using Endpoint::ClaimLocked;
+ using Endpoint::CleanUpCalls;
using Endpoint::GetInternalChannel;
};
diff --git a/pw_rpc/public/pw_rpc/client_server.h b/pw_rpc/public/pw_rpc/client_server.h
index 92f99da81..3164f17bd 100644
--- a/pw_rpc/public/pw_rpc/client_server.h
+++ b/pw_rpc/public/pw_rpc/client_server.h
@@ -22,27 +22,22 @@ namespace pw::rpc {
// a device needs to function as both.
class ClientServer {
public:
- _PW_RPC_CONSTEXPR ClientServer(std::span<Channel> channels)
+ // If dynamic allocation is supported, it is not necessary to preallocate a
+ // channels list.
+#if PW_RPC_DYNAMIC_ALLOCATION
+ _PW_RPC_CONSTEXPR ClientServer() = default;
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+ _PW_RPC_CONSTEXPR ClientServer(span<Channel> channels)
: client_(channels), server_(channels) {}
// Sends a packet to either the client or the server, depending on its type.
- //
- // ProcessPacket optionally accepts a ChannelOutput as a second argument. If
- // provided, the server will be able to dynamically assign channels as
- // requests come in instead of requiring channels to be known at compile time.
- Status ProcessPacket(ConstByteSpan packet) {
- return ProcessPacket(packet, nullptr);
- }
- Status ProcessPacket(ConstByteSpan packet, ChannelOutput& interface) {
- return ProcessPacket(packet, &interface);
- }
+ Status ProcessPacket(ConstByteSpan packet);
constexpr Client& client() { return client_; }
constexpr Server& server() { return server_; }
private:
- Status ProcessPacket(ConstByteSpan packet, ChannelOutput* interface);
-
Client client_;
Server server_;
};
diff --git a/pw_rpc/public/pw_rpc/integration_test_socket_client.h b/pw_rpc/public/pw_rpc/integration_test_socket_client.h
index 40db2aea4..f8847874b 100644
--- a/pw_rpc/public/pw_rpc/integration_test_socket_client.h
+++ b/pw_rpc/public/pw_rpc/integration_test_socket_client.h
@@ -13,13 +13,16 @@
// the License.
#pragma once
+#include <atomic>
#include <cstdint>
-#include <span>
+#include <optional>
#include <thread>
+#include "pw_hdlc/encoded_size.h"
#include "pw_hdlc/rpc_channel.h"
#include "pw_hdlc/rpc_packets.h"
#include "pw_rpc/integration_testing.h"
+#include "pw_span/span.h"
#include "pw_status/try.h"
#include "pw_stream/socket_stream.h"
@@ -31,9 +34,12 @@ template <size_t kMaxTransmissionUnit>
class SocketClientContext {
public:
constexpr SocketClientContext()
- : channel_output_(stream_, hdlc::kDefaultRpcAddress, "socket"),
- channel_(Channel::Create<kChannelId>(&channel_output_)),
- client_(std::span(&channel_, 1)) {}
+ : rpc_dispatch_thread_handle_(std::nullopt),
+ channel_output_(stream_, hdlc::kDefaultRpcAddress, "socket"),
+ channel_output_with_manipulator_(channel_output_),
+ channel_(
+ Channel::Create<kChannelId>(&channel_output_with_manipulator_)),
+ client_(span(&channel_, 1)) {}
Client& client() { return client_; }
@@ -41,39 +47,120 @@ class SocketClientContext {
// packets from the socket.
Status Start(const char* host, uint16_t port) {
PW_TRY(stream_.Connect(host, port));
- std::thread{&SocketClientContext::ProcessPackets, this}.detach();
+ rpc_dispatch_thread_handle_.emplace(&SocketClientContext::ProcessPackets,
+ this);
return OkStatus();
}
+ // Terminates the client, joining the RPC dispatch thread.
+ //
+ // WARNING: This may block forever if the socket is configured to block
+ // indefinitely on reads. Configuring the client socket's `SO_RCVTIMEO` to a
+ // nonzero timeout will allow the dispatch thread to always return.
+ void Terminate() {
+ PW_ASSERT(rpc_dispatch_thread_handle_.has_value());
+ should_terminate_.test_and_set();
+ rpc_dispatch_thread_handle_->join();
+ }
+
+ int GetSocketFd() { return stream_.connection_fd(); }
+
+ void SetEgressChannelManipulator(
+ ChannelManipulator* new_channel_manipulator) {
+ channel_output_with_manipulator_.set_channel_manipulator(
+ new_channel_manipulator);
+ }
+
+ void SetIngressChannelManipulator(
+ ChannelManipulator* new_channel_manipulator) {
+ if (new_channel_manipulator != nullptr) {
+ new_channel_manipulator->set_send_packet([&](ConstByteSpan payload) {
+ return client_.ProcessPacket(payload);
+ });
+ }
+ ingress_channel_manipulator_ = new_channel_manipulator;
+ }
+
// Calls Start for localhost.
Status Start(uint16_t port) { return Start("localhost", port); }
private:
void ProcessPackets();
+ class ChannelOutputWithManipulator : public ChannelOutput {
+ public:
+ ChannelOutputWithManipulator(ChannelOutput& actual_output)
+ : ChannelOutput(actual_output.name()),
+ actual_output_(actual_output),
+ channel_manipulator_(nullptr) {}
+
+ void set_channel_manipulator(ChannelManipulator* new_channel_manipulator) {
+ if (new_channel_manipulator != nullptr) {
+ new_channel_manipulator->set_send_packet(
+ ChannelManipulator::SendCallback([&](
+ ConstByteSpan
+ payload) __attribute__((no_thread_safety_analysis)) {
+ return actual_output_.Send(payload);
+ }));
+ }
+ channel_manipulator_ = new_channel_manipulator;
+ }
+
+ size_t MaximumTransmissionUnit() override {
+ return actual_output_.MaximumTransmissionUnit();
+ }
+ Status Send(span<const std::byte> buffer) override
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock()) {
+ if (channel_manipulator_ != nullptr) {
+ return channel_manipulator_->ProcessAndSend(buffer);
+ }
+
+ return actual_output_.Send(buffer);
+ }
+
+ private:
+ ChannelOutput& actual_output_;
+ ChannelManipulator* channel_manipulator_;
+ };
+
+ std::atomic_flag should_terminate_ = ATOMIC_FLAG_INIT;
+ std::optional<std::thread> rpc_dispatch_thread_handle_;
stream::SocketStream stream_;
- hdlc::RpcChannelOutput channel_output_;
+ hdlc::FixedMtuChannelOutput<kMaxTransmissionUnit> channel_output_;
+ ChannelOutputWithManipulator channel_output_with_manipulator_;
+ ChannelManipulator* ingress_channel_manipulator_;
Channel channel_;
Client client_;
};
template <size_t kMaxTransmissionUnit>
void SocketClientContext<kMaxTransmissionUnit>::ProcessPackets() {
- std::byte decode_buffer[kMaxTransmissionUnit];
+ constexpr size_t kDecoderBufferSize =
+ hdlc::Decoder::RequiredBufferSizeForFrameSize(kMaxTransmissionUnit);
+ std::array<std::byte, kDecoderBufferSize> decode_buffer;
hdlc::Decoder decoder(decode_buffer);
while (true) {
std::byte byte[1];
Result<ByteSpan> read = stream_.Read(byte);
- if (!read.ok() || read->size() == 0u) {
+ if (should_terminate_.test()) {
+ return;
+ }
+
+ if (!read.ok() || read->empty()) {
continue;
}
if (auto result = decoder.Process(*byte); result.ok()) {
hdlc::Frame& frame = result.value();
if (frame.address() == hdlc::kDefaultRpcAddress) {
- PW_ASSERT(client_.ProcessPacket(frame.data()).ok());
+ if (ingress_channel_manipulator_ != nullptr) {
+ PW_ASSERT(
+ ingress_channel_manipulator_->ProcessAndSend(frame.data()).ok());
+ } else {
+ PW_ASSERT(client_.ProcessPacket(frame.data()).ok());
+ }
}
}
}
diff --git a/pw_rpc/public/pw_rpc/integration_testing.h b/pw_rpc/public/pw_rpc/integration_testing.h
index 3f8004907..8d6dd17c8 100644
--- a/pw_rpc/public/pw_rpc/integration_testing.h
+++ b/pw_rpc/public/pw_rpc/integration_testing.h
@@ -15,6 +15,7 @@
#include <cstdint>
+#include "pw_function/function.h"
#include "pw_rpc/client.h"
#include "pw_status/status.h"
@@ -23,13 +24,84 @@ namespace pw::rpc::integration_test {
// The RPC channel for integration test RPCs.
inline constexpr uint32_t kChannelId = 1;
+// An injectable pipe interface that may manipulate packets before they're sent
+// to the final destination.
+//
+// ``ChannelManipulator``s allow application-specific packet handling to be
+// injected into the packet processing pipeline for an ingress or egress
+// channel-like pathway. This is particularly useful for integration testing
+// resilience to things like packet loss on a usually-reliable transport. RPC
+// server integrations may provide an opportunity to inject a
+// ``ChannelManipulator`` for this use case.
+//
+// A ``ChannelManipulator`` should not set send_packet_, as the consumer of a
+// ``ChannelManipulator`` will use ``send_packet`` to insert the provided
+// ``ChannelManipulator`` into a packet processing path.
+class ChannelManipulator {
+ public:
+ // The expected function signature of the send callback used by a
+ // ChannelManipulator to forward packets to the final destination.
+ //
+ // The only argument is a byte span containing the RPC packet that should
+ // be sent.
+ //
+ // Returns:
+ // OK - Packet successfully sent.
+ // Other - Failed to send packet.
+ using SendCallback = Function<Status(span<const std::byte>)>;
+
+ ChannelManipulator()
+ : send_packet_(
+ [](span<const std::byte>) { return Status::Unimplemented(); }) {}
+ virtual ~ChannelManipulator() {}
+
+ // Sets the true send callback that a ChannelManipulator will use to forward
+ // packets to the final destination.
+ //
+ // This should not be used by a ChannelManipulator. The consumer of a
+ // ChannelManipulator will set the send callback.
+ void set_send_packet(SendCallback&& send) { send_packet_ = std::move(send); }
+
+ // Processes an incoming packet before optionally sending it.
+ //
+ // Implementations of this method may send the processed packet, multiple
+ // packets, or no packets at all via the registered `send_packet()`
+ // handler.
+ virtual Status ProcessAndSend(span<const std::byte> packet) = 0;
+
+ protected:
+ Status send_packet(span<const std::byte> payload) {
+ return send_packet_(payload);
+ }
+
+ private:
+ Function<Status(span<const std::byte>)> send_packet_;
+};
+
+void SetEgressChannelManipulator(ChannelManipulator* new_channel_manipulator);
+
+void SetIngressChannelManipulator(ChannelManipulator* new_channel_manipulator);
+
// Returns the global RPC client for integration test use.
Client& client();
+// The file descriptor for the socket associated with the client. This may be
+// used to configure socket options.
+int GetClientSocketFd();
+
// Initializes logging and the global RPC client for integration testing. Starts
// a background thread that processes incoming.
Status InitializeClient(int argc,
char* argv[],
const char* usage_args = "PORT");
+Status InitializeClient(int port);
+
+// Terminates the client, joining the RPC dispatch thread.
+//
+// WARNING: This may block forever if the socket is configured to block
+// indefinitely on reads. Configuring the client socket's `SO_RCVTIMEO` to a
+// nonzero timeout will allow the dispatch thread to always return.
+void TerminateClient();
+
} // namespace pw::rpc::integration_test
diff --git a/pw_rpc/public/pw_rpc/internal/call.h b/pw_rpc/public/pw_rpc/internal/call.h
index 8edff6fb0..ca9597a04 100644
--- a/pw_rpc/public/pw_rpc/internal/call.h
+++ b/pw_rpc/public/pw_rpc/internal/call.h
@@ -15,7 +15,7 @@
#include <cassert>
#include <cstddef>
-#include <span>
+#include <limits>
#include <utility>
#include "pw_containers/intrusive_list.h"
@@ -27,6 +27,7 @@
#include "pw_rpc/internal/packet.h"
#include "pw_rpc/method_type.h"
#include "pw_rpc/service.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_sync/lock_annotations.h"
@@ -37,8 +38,61 @@ class Writer;
namespace internal {
class Endpoint;
+class LockedEndpoint;
class Packet;
+// Whether a call object is associated with a server or a client.
+enum CallType : bool { kServerCall, kClientCall };
+
+// Whether callbacks that take a proto use the raw data directly or decode it
+// to a struct. The RPC lock is held when invoking callbacks that decode to a
+// struct.
+enum CallbackProtoType : bool { kRawProto, kProtoStruct };
+
+// Immutable properties of a call object. These do not change after an active
+// call is initialized.
+//
+// Bits
+// 0-1: MethodType
+// 2: CallType
+// 3: Bool for whether callbacks decode to proto structs
+//
+class CallProperties {
+ public:
+ constexpr CallProperties() : bits_(0u) {}
+
+ constexpr CallProperties(MethodType method_type,
+ CallType call_type,
+ CallbackProtoType callback_proto_type)
+ : bits_((static_cast<uint8_t>(method_type) << 0) |
+ (static_cast<uint8_t>(call_type) << 2) |
+ (static_cast<uint8_t>(callback_proto_type) << 3)) {}
+
+ constexpr CallProperties(const CallProperties&) = default;
+
+ constexpr CallProperties& operator=(const CallProperties&) = default;
+
+ constexpr MethodType method_type() const {
+ return static_cast<MethodType>(bits_ & 0b0011u);
+ }
+
+ constexpr CallType call_type() const {
+ return static_cast<CallType>((bits_ & 0b0100u) >> 2);
+ }
+
+ constexpr CallbackProtoType callback_proto_type() const {
+ return static_cast<CallbackProtoType>((bits_ & 0b1000u) >> 3);
+ }
+
+ private:
+ uint8_t bits_;
+};
+
+// Unrequested RPCs always use this call ID. When a subsequent request
+// or response is sent with a matching channel + service + method,
+// it will match a calls with this ID if one exists.
+inline constexpr uint32_t kOpenCallId = std::numeric_limits<uint32_t>::max();
+
// Internal RPC Call class. The Call is used to respond to any type of RPC.
// Public classes like ServerWriters inherit from it with private inheritance
// and provide a public API for their use case. The Call's public API is used by
@@ -58,46 +112,65 @@ class Call : public IntrusiveList<Call>::Item {
Call& operator=(const Call&) = delete;
Call& operator=(Call&&) = delete;
+ ~Call() PW_LOCKS_EXCLUDED(rpc_lock());
+
// True if the Call is active and ready to send responses.
[[nodiscard]] bool active() const PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
return active_locked();
}
[[nodiscard]] bool active_locked() const
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return rpc_state_ == kActive;
+ return (state_ & kActive) != 0;
+ }
+
+ [[nodiscard]] bool awaiting_cleanup() const
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return awaiting_cleanup_ != OkStatus().code();
}
uint32_t id() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) { return id_; }
+ void set_id(uint32_t id) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) { id_ = id; }
+
+ // Public function for accessing the channel ID of this call. Set to 0 when
+ // the call is closed.
uint32_t channel_id() const PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
return channel_id_locked();
}
+
uint32_t channel_id_locked() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
return channel_id_;
}
+
uint32_t service_id() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
return service_id_;
}
+
uint32_t method_id() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
return method_id_;
}
+ // Return whether this is a server or client call.
+ CallType type() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return properties_.call_type();
+ }
+
// Closes the Call and sends a RESPONSE packet, if it is active. Returns the
// status from sending the packet, or FAILED_PRECONDITION if the Call is not
// active.
Status CloseAndSendResponse(ConstByteSpan response, Status status)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
return CloseAndSendResponseLocked(response, status);
}
Status CloseAndSendResponseLocked(ConstByteSpan response, Status status)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
return CloseAndSendFinalPacketLocked(
- PacketType::RESPONSE, response, status);
+ pwpb::PacketType::RESPONSE, response, status);
}
Status CloseAndSendResponse(Status status) PW_LOCKS_EXCLUDED(rpc_lock()) {
@@ -106,24 +179,25 @@ class Call : public IntrusiveList<Call>::Item {
Status CloseAndSendServerErrorLocked(Status error)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return CloseAndSendFinalPacketLocked(PacketType::SERVER_ERROR, {}, error);
+ return CloseAndSendFinalPacketLocked(
+ pwpb::PacketType::SERVER_ERROR, {}, error);
}
- // Public call that ends the client stream for a client call.
+ // Public function that ends the client stream for a client call.
Status CloseClientStream() PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
return CloseClientStreamLocked();
}
- // Internal call that closes the client stream.
+ // Internal function that closes the client stream.
Status CloseClientStreamLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- client_stream_state_ = kClientStreamInactive;
- return SendPacket(PacketType::CLIENT_STREAM_END, {}, {});
+ MarkClientStreamCompleted();
+ return SendPacket(pwpb::PacketType::CLIENT_STREAM_END, {}, {});
}
// Sends a payload in either a server or client stream packet.
Status Write(ConstByteSpan payload) PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
return WriteLocked(payload);
}
@@ -133,30 +207,20 @@ class Call : public IntrusiveList<Call>::Item {
// Sends the initial request for a client call. If the request fails, the call
// is closed.
void SendInitialClientRequest(ConstByteSpan payload)
- PW_UNLOCK_FUNCTION(rpc_lock()) {
- // TODO(pwbug/597): Ensure the call object is locked before releasing the
- // RPC mutex.
- if (const Status status = SendPacket(PacketType::REQUEST, payload);
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ if (const Status status = SendPacket(pwpb::PacketType::REQUEST, payload);
!status.ok()) {
- HandleError(status);
- } else {
- rpc_lock().unlock();
+ CloseAndMarkForCleanup(status);
}
}
+ void CloseAndMarkForCleanup(Status error)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
// Whenever a payload arrives (in a server/client stream or in a response),
// call the on_next_ callback.
// Precondition: rpc_lock() must be held.
- void HandlePayload(ConstByteSpan message) const
- PW_UNLOCK_FUNCTION(rpc_lock()) {
- const bool invoke = on_next_ != nullptr;
- // TODO(pwbug/597): Ensure on_next_ is properly guarded.
- rpc_lock().unlock();
-
- if (invoke) {
- on_next_(message);
- }
- }
+ void HandlePayload(ConstByteSpan payload) PW_UNLOCK_FUNCTION(rpc_lock());
// Handles an error condition for the call. This closes the call and calls the
// on_error callback, if set.
@@ -165,38 +229,41 @@ class Call : public IntrusiveList<Call>::Item {
CallOnError(status);
}
- // Aborts the RPC because its channel was closed. Does NOT unregister the
- // call! The calls are removed when iterating over the list in the endpoint.
- void HandleChannelClose() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- // Locking here is problematic because CallOnError releases rpc_lock().
- //
- // pwbug/597 must be addressed before the locking here can be cleaned up.
+ // Closes the RPC, but does NOT unregister the call or call on_error. The
+ // call must be moved to the endpoint's to_cleanup_ list and have its
+ // CleanUp() method called at a later time. Only for use by the Endpoint.
+ void CloseAndMarkForCleanupFromEndpoint(Status error)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
MarkClosed();
+ awaiting_cleanup_ = error.code();
+ }
- CallOnError(Status::Aborted());
-
- // Re-lock rpc_lock().
- rpc_lock().lock();
+ // Clears the awaiting_cleanup_ variable and calls the on_error callback. Only
+ // for use by the Endpoint, which will unlist the call.
+ void CleanUpFromEndpoint() PW_UNLOCK_FUNCTION(rpc_lock()) {
+ const Status status(static_cast<Status::Code>(awaiting_cleanup_));
+ awaiting_cleanup_ = OkStatus().code();
+ CallOnError(status);
}
bool has_client_stream() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return HasClientStream(type_);
+ return HasClientStream(properties_.method_type());
}
bool has_server_stream() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return HasServerStream(type_);
+ return HasServerStream(properties_.method_type());
}
bool client_stream_open() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return client_stream_state_ == kClientStreamActive;
+ return (state_ & kClientStreamActive) != 0;
}
- // Keep this public so the Nanopb implementation can set it from a helper
- // function.
- void set_on_next(Function<void(ConstByteSpan)>&& on_next)
- PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
- set_on_next_locked(std::move(on_next));
+ // Closes a call without doing anything else. Called from the Endpoint
+ // destructor.
+ void CloseFromDeletedEndpoint() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ MarkClosed();
+ awaiting_cleanup_ = OkStatus().code();
+ endpoint_ = nullptr;
}
protected:
@@ -207,28 +274,29 @@ class Call : public IntrusiveList<Call>::Item {
id_{},
service_id_{},
method_id_{},
- rpc_state_{},
- type_{},
- call_type_{},
- client_stream_state_ {}
- {}
+ state_{},
+ awaiting_cleanup_{},
+ callbacks_executing_{},
+ properties_{} {}
// Creates an active server-side Call.
- Call(const CallContext& context, MethodType type)
- : Call(context.server(),
- context.call_id(),
- context.channel_id(),
- context.service().id(),
- context.method().id(),
- type,
- kServerCall) {}
+ Call(const LockedCallContext& context, CallProperties properties)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
// Creates an active client-side Call.
- Call(Endpoint& client,
+ Call(LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
- MethodType type);
+ CallProperties properties) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
+ void CallbackStarted() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ callbacks_executing_ += 1;
+ }
+
+ void CallbackFinished() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ callbacks_executing_ -= 1;
+ }
// This call must be in a closed state when this is called.
void MoveFrom(Call& other) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
@@ -237,48 +305,47 @@ class Call : public IntrusiveList<Call>::Item {
return *endpoint_;
}
+ // Public function that sets the on_next function in the raw API.
+ void set_on_next(Function<void(ConstByteSpan)>&& on_next)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ set_on_next_locked(std::move(on_next));
+ }
+
+ // Internal function that sets on_next.
void set_on_next_locked(Function<void(ConstByteSpan)>&& on_next)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
on_next_ = std::move(on_next);
}
+ // Public function that sets the on_error callback.
void set_on_error(Function<void(Status)>&& on_error)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
set_on_error_locked(std::move(on_error));
}
+ // Internal function that sets on_error.
void set_on_error_locked(Function<void(Status)>&& on_error)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
on_error_ = std::move(on_error);
}
- // Calls the on_error callback without closing the RPC. This is used when the
- // call has already completed.
- void CallOnError(Status error) PW_UNLOCK_FUNCTION(rpc_lock()) {
- const bool invoke = on_error_ != nullptr;
-
- // TODO(pwbug/597): Ensure on_error_ is properly guarded.
- rpc_lock().unlock();
- if (invoke) {
- on_error_(error);
- }
- }
-
void MarkClientStreamCompleted() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- client_stream_state_ = kClientStreamInactive;
+ state_ &= ~kClientStreamActive;
}
Status CloseAndSendResponseLocked(Status status)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return CloseAndSendFinalPacketLocked(PacketType::RESPONSE, {}, status);
+ return CloseAndSendFinalPacketLocked(
+ pwpb::PacketType::RESPONSE, {}, status);
}
- // Cancels an RPC. For client calls only.
+ // Cancels an RPC. Public function for client calls only.
Status Cancel() PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
return CloseAndSendFinalPacketLocked(
- PacketType::CLIENT_ERROR, {}, Status::Cancelled());
+ pwpb::PacketType::CLIENT_ERROR, {}, Status::Cancelled());
}
// Unregisters the RPC from the endpoint & marks as closed. The call may be
@@ -290,19 +357,100 @@ class Call : public IntrusiveList<Call>::Item {
constexpr operator Writer&();
constexpr operator const Writer&() const;
+ // Indicates if the on_next and unary on_completed callbacks are internal
+ // wrappers that decode the raw proto before invoking the user's callback. If
+ // they are, the lock must be held when they are invoked.
+ bool hold_lock_while_invoking_callback_with_payload() const
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return properties_.callback_proto_type() == kProtoStruct;
+ }
+
+ // Decodes a raw protobuf into a proto struct (pwpb or Nanopb) and invokes the
+ // pwpb or Nanopb version of the on_next callback.
+ //
+ // This must ONLY be called from derived classes the wrap the on_next
+ // callback. These classes MUST indicate that they call calls in their
+ // constructor.
+ template <typename Decoder, typename ProtoStruct>
+ void DecodeToStructAndInvokeOnNext(
+ ConstByteSpan payload,
+ const Decoder& decoder,
+ Function<void(const ProtoStruct&)>& proto_on_next)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ if (proto_on_next == nullptr) {
+ return;
+ }
+
+ ProtoStruct proto_struct{};
+
+ if (!decoder.Decode(payload, proto_struct).ok()) {
+ CloseAndMarkForCleanup(Status::DataLoss());
+ return;
+ }
+
+ const uint32_t original_id = id();
+ auto proto_on_next_local = std::move(proto_on_next);
+
+ rpc_lock().unlock();
+ proto_on_next_local(proto_struct);
+ rpc_lock().lock();
+
+ // Restore the original callback if the original call is still active and
+ // the callback has not been replaced.
+ // NOLINTNEXTLINE(bugprone-use-after-move)
+ if (active_locked() && id() == original_id && proto_on_next == nullptr) {
+ proto_on_next = std::move(proto_on_next_local);
+ }
+ }
+
+ // The call is already unregistered and closed.
+ template <typename Decoder, typename ProtoStruct>
+ void DecodeToStructAndInvokeOnCompleted(
+ ConstByteSpan payload,
+ const Decoder& decoder,
+ Function<void(const ProtoStruct&, Status)>& proto_on_completed,
+ Status status) PW_UNLOCK_FUNCTION(rpc_lock()) {
+ // Always move proto_on_completed so it goes out of scope in this function.
+ auto proto_on_completed_local = std::move(proto_on_completed);
+
+ // Move on_error in case an error occurs.
+ auto on_error_local = std::move(on_error_);
+
+ // Release the lock before decoding, since decoder is a global.
+ rpc_lock().unlock();
+
+ if (proto_on_completed_local == nullptr) {
+ return;
+ }
+
+ ProtoStruct proto_struct{};
+ if (decoder.Decode(payload, proto_struct).ok()) {
+ proto_on_completed_local(proto_struct, status);
+ } else if (on_error_local != nullptr) {
+ on_error_local(Status::DataLoss());
+ }
+ }
+
+ // An active call cannot be moved if its callbacks are running. This function
+ // must be called on the call being moved before updating any state.
+ static void WaitUntilReadyForMove(Call& destination, Call& source)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
private:
- enum CallType : bool { kServerCall, kClientCall };
+ enum State : uint8_t {
+ kActive = 0b01,
+ kClientStreamActive = 0b10,
+ };
// Common constructor for server & client calls.
- Call(Endpoint& endpoint,
+ Call(LockedEndpoint& endpoint,
uint32_t id,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
- MethodType type,
- CallType call_type);
+ CallProperties properties);
- Packet MakePacket(PacketType type,
+ Packet MakePacket(pwpb::PacketType type,
ConstByteSpan payload,
Status status = OkStatus()) const
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
@@ -315,46 +463,70 @@ class Call : public IntrusiveList<Call>::Item {
status);
}
+ // Marks a call object closed without doing anything else. The call is not
+ // removed from the calls list and no callbacks are called.
void MarkClosed() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
channel_id_ = Channel::kUnassignedChannelId;
- rpc_state_ = kInactive;
- client_stream_state_ = kClientStreamInactive;
+ id_ = 0;
+ state_ = 0;
}
+ // Calls the on_error callback without closing the RPC. This is used when the
+ // call has already completed.
+ void CallOnError(Status error) PW_UNLOCK_FUNCTION(rpc_lock());
+
+ // If required, removes this call from the endpoint's to_cleanup_ list and
+ // calls CleanUp(). Returns true if cleanup was required, which means the lock
+ // was released.
+ bool CleanUpIfRequired() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
// Sends a payload with the specified type. The payload may either be in a
// previously acquired buffer or in a standalone buffer.
//
// Returns FAILED_PRECONDITION if the call is not active().
- Status SendPacket(PacketType type,
+ Status SendPacket(pwpb::PacketType type,
ConstByteSpan payload,
Status status = OkStatus())
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
- Status CloseAndSendFinalPacketLocked(PacketType type,
+ Status CloseAndSendFinalPacketLocked(pwpb::PacketType type,
ConstByteSpan response,
Status status)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
- internal::Endpoint* endpoint_ PW_GUARDED_BY(rpc_lock());
+ bool CallbacksAreRunning() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return callbacks_executing_ != 0u;
+ }
+
+ Endpoint* endpoint_ PW_GUARDED_BY(rpc_lock());
uint32_t channel_id_ PW_GUARDED_BY(rpc_lock());
uint32_t id_ PW_GUARDED_BY(rpc_lock());
uint32_t service_id_ PW_GUARDED_BY(rpc_lock());
uint32_t method_id_ PW_GUARDED_BY(rpc_lock());
- enum : bool { kInactive, kActive } rpc_state_ PW_GUARDED_BY(rpc_lock());
- MethodType type_ PW_GUARDED_BY(rpc_lock());
- CallType call_type_ PW_GUARDED_BY(rpc_lock());
- enum : bool {
- kClientStreamInactive,
- kClientStreamActive,
- } client_stream_state_ PW_GUARDED_BY(rpc_lock());
+ // State of call and client stream.
+ //
+ // bit 0: call is active
+ // bit 1: client stream is active
+ //
+ uint8_t state_ PW_GUARDED_BY(rpc_lock());
+
+ // If non-OK, indicates that the call was closed and needs to have its
+ // on_error called with this Status code. Uses a uint8_t for compactness.
+ uint8_t awaiting_cleanup_ PW_GUARDED_BY(rpc_lock());
+
+ // Tracks how many of this call's callbacks are running. Must be 0 for the
+ // call to be destroyed.
+ uint8_t callbacks_executing_ PW_GUARDED_BY(rpc_lock());
+
+ CallProperties properties_ PW_GUARDED_BY(rpc_lock());
// Called when the RPC is terminated due to an error.
- Function<void(Status error)> on_error_;
+ Function<void(Status error)> on_error_ PW_GUARDED_BY(rpc_lock());
// Called when a request is received. Only used for RPCs with client streams.
// The raw payload buffer is passed to the callback.
- Function<void(ConstByteSpan payload)> on_next_;
+ Function<void(ConstByteSpan payload)> on_next_ PW_GUARDED_BY(rpc_lock());
};
} // namespace internal
diff --git a/pw_rpc/public/pw_rpc/internal/call_context.h b/pw_rpc/public/pw_rpc/internal/call_context.h
index cc15cbdcf..666abf008 100644
--- a/pw_rpc/public/pw_rpc/internal/call_context.h
+++ b/pw_rpc/public/pw_rpc/internal/call_context.h
@@ -17,6 +17,7 @@
#include <cstdint>
#include "pw_rpc/internal/channel.h"
+#include "pw_rpc/internal/lock.h"
namespace pw::rpc {
@@ -25,6 +26,8 @@ class Service;
namespace internal {
class Endpoint;
+class LockedCallContext;
+class LockedEndpoint;
class Method;
// The Server creates a CallContext object to represent a method invocation. The
@@ -42,6 +45,17 @@ class CallContext {
method_(method),
call_id_(call_id) {}
+ // Claims that `rpc_lock()` is held, returning a wrapped context.
+ //
+ // This function should only be called in contexts in which it is clear that
+ // `rpc_lock()` is held. When calling this function from a constructor, the
+ // lock annotation will not result in errors, so care should be taken to
+ // ensure that `rpc_lock()` is held.
+ const LockedCallContext& ClaimLocked() const
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
+ LockedCallContext& ClaimLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
constexpr Endpoint& server() const { return server_; }
constexpr const uint32_t& channel_id() const { return channel_id_; }
@@ -63,5 +77,27 @@ class CallContext {
uint32_t call_id_;
};
+// A `CallContext` indicating that `rpc_lock()` is held.
+//
+// This is used as a constructor argument to supplement
+// `PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())`. Current compilers do not enforce
+// lock annotations on constructors; no warnings or errors are produced when
+// calling an annotated constructor without holding `rpc_lock()`.
+class LockedCallContext : public CallContext {
+ public:
+ friend class CallContext;
+ // No public constructor: this is created only via the `ClaimLocked` method on
+ // `CallContext`.
+ constexpr LockedCallContext() = delete;
+};
+
+inline const LockedCallContext& CallContext::ClaimLocked() const {
+ return *static_cast<const LockedCallContext*>(this);
+}
+
+inline LockedCallContext& CallContext::ClaimLocked() {
+ return *static_cast<LockedCallContext*>(this);
+}
+
} // namespace internal
} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/internal/channel.h b/pw_rpc/public/pw_rpc/internal/channel.h
index 2d62cc623..a3161d05d 100644
--- a/pw_rpc/public/pw_rpc/internal/channel.h
+++ b/pw_rpc/public/pw_rpc/internal/channel.h
@@ -13,10 +13,6 @@
// the License.
#pragma once
-#include <span>
-
-#include "pw_assert/assert.h"
-#include "pw_bytes/span.h"
#include "pw_rpc/channel.h"
#include "pw_rpc/internal/lock.h"
#include "pw_rpc/internal/packet.h"
@@ -24,10 +20,6 @@
namespace pw::rpc::internal {
-// Returns a portion of the encoding buffer that may be used to encode an
-// outgoing payload.
-ByteSpan GetPayloadBuffer() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
-
class Channel : public rpc::Channel {
public:
Channel() = delete;
diff --git a/pw_rpc/public/pw_rpc/internal/channel_list.h b/pw_rpc/public/pw_rpc/internal/channel_list.h
index c21224b73..eff61b561 100644
--- a/pw_rpc/public/pw_rpc/internal/channel_list.h
+++ b/pw_rpc/public/pw_rpc/internal/channel_list.h
@@ -13,24 +13,43 @@
// the License.
#pragma once
-#include <span>
-#include <vector>
-
#include "pw_rpc/internal/channel.h"
#include "pw_rpc/internal/config.h"
+#include "pw_span/span.h"
-namespace pw::rpc::internal {
+// With dynamic allocation enabled, include the specified header and don't
+// require the constructor to be constexpr.
+#if PW_RPC_DYNAMIC_ALLOCATION
+
+#include PW_RPC_DYNAMIC_CONTAINER_INCLUDE
-#if PW_RPC_DYNAMIC_ALLOCATION && !defined(__cpp_lib_constexpr_vector)
#define _PW_RPC_CONSTEXPR
-#else
+
+#else // Otherwise, channels are stored in a constexpr constructible span.
+
#define _PW_RPC_CONSTEXPR constexpr
-#endif // PW_RPC_DYNAMIC_ALLOCATION && !defined(__cpp_lib_constexpr_vector)
+
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+namespace pw::rpc::internal {
class ChannelList {
public:
- _PW_RPC_CONSTEXPR ChannelList(std::span<Channel> channels)
- : channels_(channels.begin(), channels.end()) {}
+ _PW_RPC_CONSTEXPR ChannelList() = default;
+
+ _PW_RPC_CONSTEXPR ChannelList(span<Channel> channels)
+ // If dynamic allocation is enabled, channels aren't typically allocated
+ // beforehand, though they can be. If they are, push them one-by-one to the
+ // vector to avoid requiring a constructor that does that.
+#if PW_RPC_DYNAMIC_ALLOCATION
+ {
+ for (const Channel& channel : channels) {
+ channels_.emplace_back(channel);
+ }
+#else // Without dynamic allocation, simply initialize the span.
+ : channels_(channels) {
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+ }
// Returns the first channel with the matching ID or nullptr if none match.
// Except for Channel::kUnassignedChannelId, there should be no duplicate
@@ -60,9 +79,9 @@ class ChannelList {
Status Remove(uint32_t channel_id);
#if PW_RPC_DYNAMIC_ALLOCATION
- std::vector<Channel> channels_;
+ PW_RPC_DYNAMIC_CONTAINER(Channel) channels_;
#else
- std::span<Channel> channels_;
+ span<Channel> channels_;
#endif // PW_RPC_DYNAMIC_ALLOCATION
};
diff --git a/pw_rpc/public/pw_rpc/internal/client_call.h b/pw_rpc/public/pw_rpc/internal/client_call.h
index c4d9c1073..793d581e4 100644
--- a/pw_rpc/public/pw_rpc/internal/client_call.h
+++ b/pw_rpc/public/pw_rpc/internal/client_call.h
@@ -18,6 +18,7 @@
#include "pw_bytes/span.h"
#include "pw_function/function.h"
#include "pw_rpc/internal/call.h"
+#include "pw_rpc/internal/endpoint.h"
#include "pw_rpc/internal/lock.h"
namespace pw::rpc::internal {
@@ -25,37 +26,52 @@ namespace pw::rpc::internal {
// A Call object, as used by an RPC client.
class ClientCall : public Call {
public:
- ~ClientCall() PW_LOCKS_EXCLUDED(rpc_lock()) {
- rpc_lock().lock();
- CloseClientCall();
- rpc_lock().unlock();
+ ~ClientCall() PW_LOCKS_EXCLUDED(rpc_lock()) { Abandon(); }
+
+ uint32_t id() const PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ return Call::id();
}
protected:
+ // Initializes CallProperties for a struct-based client call impl.
+ static constexpr CallProperties StructCallProps(MethodType type) {
+ return CallProperties(type, kClientCall, kProtoStruct);
+ }
+
+ // Initializes CallProperties for a raw client call.
+ static constexpr CallProperties RawCallProps(MethodType type) {
+ return CallProperties(type, kClientCall, kRawProto);
+ }
+
constexpr ClientCall() = default;
- ClientCall(Endpoint& client,
+ ClientCall(LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
- MethodType type)
- : Call(client, channel_id, service_id, method_id, type) {}
+ CallProperties properties) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : Call(client, channel_id, service_id, method_id, properties) {}
+
+ // Public function that closes a call client-side without cancelling it on the
+ // server.
+ void Abandon() PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ CloseClientCall();
+ }
- // Sends CLIENT_STREAM_END if applicable, releases any held payload buffer,
- // and marks the call as closed.
+ // Sends CLIENT_STREAM_END if applicable and marks the call as closed.
void CloseClientCall() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
void MoveClientCallFrom(ClientCall& other)
- PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- CloseClientCall();
- MoveFrom(other);
- }
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
};
// Unary response client calls receive both a payload and the status in their
// on_completed callback. The on_next callback is not used.
class UnaryResponseClientCall : public ClientCall {
public:
+ // Start call for raw unary response RPCs.
template <typename CallType>
static CallType Start(Endpoint& client,
uint32_t channel_id,
@@ -65,34 +81,28 @@ class UnaryResponseClientCall : public ClientCall {
Function<void(Status)>&& on_error,
ConstByteSpan request) PW_LOCKS_EXCLUDED(rpc_lock()) {
rpc_lock().lock();
- CallType call(client, channel_id, service_id, method_id);
+ CallType call(client.ClaimLocked(), channel_id, service_id, method_id);
call.set_on_completed_locked(std::move(on_completed));
call.set_on_error_locked(std::move(on_error));
call.SendInitialClientRequest(request);
+ client.CleanUpCalls();
return call;
}
void HandleCompleted(ConstByteSpan response, Status status)
- PW_UNLOCK_FUNCTION(rpc_lock()) {
- const bool invoke_callback = on_completed_ != nullptr;
- UnregisterAndMarkClosed();
-
- rpc_lock().unlock();
- if (invoke_callback) {
- on_completed_(response, status);
- }
- }
+ PW_UNLOCK_FUNCTION(rpc_lock());
protected:
constexpr UnaryResponseClientCall() = default;
- UnaryResponseClientCall(Endpoint& client,
+ UnaryResponseClientCall(LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
- MethodType type)
- : ClientCall(client, channel_id, service_id, method_id, type) {}
+ CallProperties properties)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : ClientCall(client, channel_id, service_id, method_id, properties) {}
UnaryResponseClientCall(UnaryResponseClientCall&& other) {
*this = std::move(other);
@@ -100,7 +110,7 @@ class UnaryResponseClientCall : public ClientCall {
UnaryResponseClientCall& operator=(UnaryResponseClientCall&& other)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
MoveUnaryResponseClientCallFrom(other);
return *this;
}
@@ -113,8 +123,7 @@ class UnaryResponseClientCall : public ClientCall {
void set_on_completed(Function<void(ConstByteSpan, Status)>&& on_completed)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- // TODO(pwbug/597): Ensure on_completed_ is properly guarded.
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
set_on_completed_locked(std::move(on_completed));
}
@@ -127,13 +136,14 @@ class UnaryResponseClientCall : public ClientCall {
private:
using internal::ClientCall::set_on_next; // Not used in unary response calls.
- Function<void(ConstByteSpan, Status)> on_completed_;
+ Function<void(ConstByteSpan, Status)> on_completed_ PW_GUARDED_BY(rpc_lock());
};
// Stream response client calls only receive the status in their on_completed
// callback. Payloads are sent through the on_next callback.
class StreamResponseClientCall : public ClientCall {
public:
+ // Start call for raw stream response RPCs.
template <typename CallType>
static CallType Start(Endpoint& client,
uint32_t channel_id,
@@ -144,37 +154,29 @@ class StreamResponseClientCall : public ClientCall {
Function<void(Status)>&& on_error,
ConstByteSpan request) {
rpc_lock().lock();
- CallType call(client, channel_id, service_id, method_id);
+ CallType call(client.ClaimLocked(), channel_id, service_id, method_id);
call.set_on_next_locked(std::move(on_next));
call.set_on_completed_locked(std::move(on_completed));
call.set_on_error_locked(std::move(on_error));
call.SendInitialClientRequest(request);
+ client.CleanUpCalls();
return call;
}
- void HandleCompleted(Status status) PW_UNLOCK_FUNCTION(rpc_lock()) {
- const bool invoke_callback = on_completed_ != nullptr;
-
- UnregisterAndMarkClosed();
- rpc_lock().unlock();
-
- // TODO(pwbug/597): Ensure on_completed_ is properly guarded.
- if (invoke_callback) {
- on_completed_(status);
- }
- }
+ void HandleCompleted(Status status) PW_UNLOCK_FUNCTION(rpc_lock());
protected:
constexpr StreamResponseClientCall() = default;
- StreamResponseClientCall(Endpoint& client,
+ StreamResponseClientCall(LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
- MethodType type)
- : ClientCall(client, channel_id, service_id, method_id, type) {}
+ CallProperties properties)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : ClientCall(client, channel_id, service_id, method_id, properties) {}
StreamResponseClientCall(StreamResponseClientCall&& other) {
*this = std::move(other);
@@ -182,7 +184,7 @@ class StreamResponseClientCall : public ClientCall {
StreamResponseClientCall& operator=(StreamResponseClientCall&& other)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
MoveStreamResponseClientCallFrom(other);
return *this;
}
@@ -195,8 +197,7 @@ class StreamResponseClientCall : public ClientCall {
void set_on_completed(Function<void(Status)>&& on_completed)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- // TODO(pwbug/597): Ensure on_completed_ is properly guarded.
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
set_on_completed_locked(std::move(on_completed));
}
@@ -206,7 +207,7 @@ class StreamResponseClientCall : public ClientCall {
}
private:
- Function<void(Status)> on_completed_;
+ Function<void(Status)> on_completed_ PW_GUARDED_BY(rpc_lock());
};
} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/client_server_testing.h b/pw_rpc/public/pw_rpc/internal/client_server_testing.h
new file mode 100644
index 000000000..01cfd16a3
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/client_server_testing.h
@@ -0,0 +1,114 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cinttypes>
+#include <mutex>
+
+#include "pw_rpc/channel.h"
+#include "pw_rpc/client_server.h"
+#include "pw_rpc/internal/fake_channel_output.h"
+#include "pw_rpc/internal/lock.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc {
+namespace internal {
+
+// Expands on a Fake Channel Output implementation to allow for forwarding of
+// packets.
+template <typename FakeChannelOutputImpl,
+ size_t kOutputSize,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class ForwardingChannelOutput : public ChannelOutput {
+ public:
+ size_t MaximumTransmissionUnit() override {
+ return output_.MaximumTransmissionUnit();
+ }
+
+ Status Send(span<const std::byte> buffer) override {
+ return output_.Send(buffer);
+ }
+
+ // Returns true if new packets were available to forward
+ bool ForwardNextPacket(ClientServer& client_server) {
+ std::array<std::byte, kOutputSize> packet_buffer;
+ Result<ConstByteSpan> result = EncodeNextUnsentPacket(packet_buffer);
+ if (!result.ok()) {
+ return false;
+ }
+ ++sent_packets_;
+ const auto process_result = client_server.ProcessPacket(*result);
+ PW_ASSERT(process_result.ok());
+ return true;
+ }
+
+ protected:
+ constexpr ForwardingChannelOutput()
+ : ChannelOutput("testing::FakeChannelOutput") {}
+
+ FakeChannelOutputImpl output_;
+
+ // Functions are virtual to allow for their override in threaded version, so
+ // threading protection can be added.
+ virtual size_t PacketCount() const { return output_.total_packets(); }
+
+ virtual Result<ConstByteSpan> EncodeNextUnsentPacket(
+ std::array<std::byte, kPayloadsBufferSizeBytes>& packet_buffer) {
+ std::lock_guard lock(output_.mutex_);
+ const auto& packets = output_.packets();
+ if (packets.size() <= sent_packets_) {
+ return Status::NotFound();
+ }
+ return packets[sent_packets_].Encode(packet_buffer);
+ }
+
+ uint16_t sent_packets_ = 0;
+};
+
+// Provides a testing context with a real client and server
+template <typename ForwardingChannelOutputImpl,
+ size_t kOutputSize = 128,
+ size_t kMaxPackets = 16,
+ size_t kPayloadsBufferSizeBytes = 128>
+class ClientServerTestContext {
+ public:
+ const pw::rpc::Channel& channel() { return channel_; }
+ Client& client() { return client_server_.client(); }
+ Server& server() { return client_server_.server(); }
+
+ // Should be called after each rpc call to synchronously forward all queued
+ // messages. Otherwise this function can be ignored.
+ void ForwardNewPackets() {
+ while (channel_output_.ForwardNextPacket(client_server_)) {
+ }
+ }
+
+ protected:
+ explicit ClientServerTestContext()
+ : channel_(Channel::Create<1>(&channel_output_)),
+ client_server_({&channel_, 1}) {}
+
+ ~ClientServerTestContext() = default;
+
+ ForwardingChannelOutputImpl channel_output_;
+
+ private:
+ pw::rpc::Channel channel_;
+ ClientServer client_server_;
+};
+
+} // namespace internal
+} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h b/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h
new file mode 100644
index 000000000..ff838a28a
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/client_server_testing_threaded.h
@@ -0,0 +1,137 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cinttypes>
+
+#include "pw_rpc/channel.h"
+#include "pw_rpc/client_server.h"
+#include "pw_rpc/internal/client_server_testing.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_sync/mutex.h"
+#include "pw_thread/thread.h"
+
+namespace pw::rpc {
+namespace internal {
+
+// Expands on a Forwarding Channel Output implementation to allow for
+// observation of packets.
+template <typename FakeChannelOutputImpl,
+ size_t kOutputSize,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class WatchableChannelOutput
+ : public ForwardingChannelOutput<FakeChannelOutputImpl,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ using Base = ForwardingChannelOutput<FakeChannelOutputImpl,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ size_t MaximumTransmissionUnit() PW_LOCKS_EXCLUDED(mutex_) override {
+ std::lock_guard lock(mutex_);
+ return Base::MaximumTransmissionUnit();
+ }
+
+ Status Send(span<const std::byte> buffer) PW_LOCKS_EXCLUDED(mutex_) override {
+ Status status;
+ mutex_.lock();
+ status = Base::Send(buffer);
+ mutex_.unlock();
+ output_semaphore_.release();
+ return status;
+ }
+
+ // Returns true if should continue waiting for additional output
+ bool WaitForOutput() PW_LOCKS_EXCLUDED(mutex_) {
+ output_semaphore_.acquire();
+ std::lock_guard lock(mutex_);
+ return should_wait_;
+ }
+
+ void StopWaitingForOutput() PW_LOCKS_EXCLUDED(mutex_) {
+ std::lock_guard lock(mutex_);
+ should_wait_ = false;
+ output_semaphore_.release();
+ }
+
+ protected:
+ constexpr WatchableChannelOutput() = default;
+
+ size_t PacketCount() const PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_) override {
+ return Base::PacketCount();
+ }
+
+ sync::Mutex mutex_;
+
+ private:
+ Result<ConstByteSpan> EncodeNextUnsentPacket(
+ std::array<std::byte, kPayloadsBufferSizeBytes>& packet_buffer)
+ PW_LOCKS_EXCLUDED(mutex_) override {
+ std::lock_guard lock(mutex_);
+ return Base::EncodeNextUnsentPacket(packet_buffer);
+ }
+ sync::BinarySemaphore output_semaphore_;
+ bool should_wait_ PW_GUARDED_BY(mutex_) = true;
+};
+
+// Provides a testing context with a real client and server
+template <typename WatchableChannelOutputImpl,
+ size_t kOutputSize = 128,
+ size_t kMaxPackets = 16,
+ size_t kPayloadsBufferSizeBytes = 128>
+class ClientServerTestContextThreaded
+ : public ClientServerTestContext<WatchableChannelOutputImpl,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ using Instance = ClientServerTestContextThreaded<WatchableChannelOutputImpl,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+ using Base = ClientServerTestContext<WatchableChannelOutputImpl,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ ~ClientServerTestContextThreaded() {
+ Base::channel_output_.StopWaitingForOutput();
+ thread_.join();
+ }
+
+ protected:
+ explicit ClientServerTestContextThreaded(const thread::Options& options)
+ : thread_(options, Instance::Run, this) {}
+
+ private:
+ using Base::ForwardNewPackets;
+ static void Run(void* arg) {
+ auto& ctx = *static_cast<Instance*>(arg);
+ while (ctx.channel_output_.WaitForOutput()) {
+ ctx.ForwardNewPackets();
+ }
+ }
+ thread::Thread thread_;
+};
+
+} // namespace internal
+} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/internal/config.h b/pw_rpc/public/pw_rpc/internal/config.h
index 42e4ebb80..24a227159 100644
--- a/pw_rpc/public/pw_rpc/internal/config.h
+++ b/pw_rpc/public/pw_rpc/internal/config.h
@@ -18,76 +18,186 @@
#include <cstddef>
#include <type_traits>
-// In client and bidirectional RPCs, pw_rpc clients may signal that they have
-// finished sending requests with a CLIENT_STREAM_END packet. While this can be
-// useful in some circumstances, it is often not necessary.
-//
-// This option controls whether or not include a callback that is called when
-// the client stream ends. The callback is included in all ServerReader/Writer
-// objects as a pw::Function, so may have a significant cost.
+/// In client and bidirectional RPCs, pw_rpc clients may signal that they have
+/// finished sending requests with a `CLIENT_STREAM_END` packet. While this can
+/// be useful in some circumstances, it is often not necessary.
+///
+/// This option controls whether or not include a callback that is called when
+/// the client stream ends. The callback is included in all ServerReader/Writer
+/// objects as a @cpp_type{pw::Function}, so may have a significant cost.
+///
+/// This is disabled by default.
#ifndef PW_RPC_CLIENT_STREAM_END_CALLBACK
#define PW_RPC_CLIENT_STREAM_END_CALLBACK 0
#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
-// The Nanopb-based pw_rpc implementation allocates memory to use for Nanopb
-// structs for the request and response protobufs. The template function that
-// allocates these structs rounds struct sizes up to this value so that
-// different structs can be allocated with the same function. Structs with sizes
-// larger than this value cause an extra function to be created, which slightly
-// increases code size.
-//
-// Ideally, this value will be set to the size of the largest Nanopb struct used
-// as an RPC request or response. The buffer can be stack or globally allocated
-// (see PW_RPC_NANOPB_STRUCT_BUFFER_STACK_ALLOCATE).
+/// The Nanopb-based pw_rpc implementation allocates memory to use for Nanopb
+/// structs for the request and response protobufs. The template function that
+/// allocates these structs rounds struct sizes up to this value so that
+/// different structs can be allocated with the same function. Structs with
+/// sizes larger than this value cause an extra function to be created, which
+/// slightly increases code size.
+///
+/// Ideally, this value will be set to the size of the largest Nanopb struct
+/// used as an RPC request or response. The buffer can be stack or globally
+/// allocated (see @c_macro{PW_RPC_NANOPB_STRUCT_BUFFER_STACK_ALLOCATE}).
+///
+/// This defaults to 64 bytes.
#ifndef PW_RPC_NANOPB_STRUCT_MIN_BUFFER_SIZE
#define PW_RPC_NANOPB_STRUCT_MIN_BUFFER_SIZE 64
#endif // PW_RPC_NANOPB_STRUCT_MIN_BUFFER_SIZE
-// Enable global synchronization for RPC calls. If this is set, a backend must
-// be configured for pw_sync:mutex.
+/// Enable global synchronization for RPC calls. If this is set, a backend must
+/// be configured for pw_sync:mutex.
+///
+/// This is enabled by default.
#ifndef PW_RPC_USE_GLOBAL_MUTEX
-#define PW_RPC_USE_GLOBAL_MUTEX 0
+#define PW_RPC_USE_GLOBAL_MUTEX 1
#endif // PW_RPC_USE_GLOBAL_MUTEX
-// Whether pw_rpc should use dynamic memory allocation internally. If enabled,
-// pw_rpc dynamically allocates channels and its encoding buffers. RPC users may
-// use dynamic allocation independently of this option (e.g. to allocate pw_rpc
-// call objects).
-//
-// The semantics for allocating and initializing channels change depending on
-// this option. If dynamic allocation is disabled, pw_rpc endpoints (servers or
-// clients) use an externally-allocated, fixed-size array of channels.
-// That array must include unassigned channels or existing channels must be
-// closed to add new channels.
-//
-// If dynamic allocation is enabled, an span of channels may be passed to the
-// endpoint at construction, but these channels are only used to initialize its
-// internal std::vector of channels. External channel objects are NOT used by
-// the endpoint cannot be updated if dynamic allocation is enabled. No
-// unassigned channels should be passed to the endpoint; they will be ignored.
-// Any number of channels may be added to the endpoint, without closing existing
-// channels, but adding channels will use more memory.
+/// pw_rpc must yield the current thread when waiting for a callback to complete
+/// in a different thread. PW_RPC_YIELD_MODE determines how to yield. There are
+/// three supported settings:
+///
+/// - @c_macro{PW_RPC_YIELD_MODE_BUSY_LOOP} - Do nothing. Release and
+/// reacquire the RPC lock in a busy loop. @c_macro{PW_RPC_USE_GLOBAL_MUTEX}
+/// must be 0.
+/// - @c_macro{PW_RPC_YIELD_MODE_SLEEP} - Yield with 1-tick calls to
+/// @cpp_func{pw::this_thread::sleep_for()}. A backend must be configured
+/// for pw_thread:sleep.
+/// - @c_macro{PW_RPC_YIELD_MODE_YIELD} - Yield with
+/// @cpp_func{pw::this_thread::yield()}. A backend must be configured for
+/// pw_thread:yield. IMPORTANT: On some platforms,
+/// @cpp_func{pw::this_thread::yield()} does not yield to lower priority
+/// tasks and should not be used here.
+///
+#ifndef PW_RPC_YIELD_MODE
+#if PW_RPC_USE_GLOBAL_MUTEX == 0
+#define PW_RPC_YIELD_MODE PW_RPC_YIELD_MODE_BUSY_LOOP
+#else
+#define PW_RPC_YIELD_MODE PW_RPC_YIELD_MODE_SLEEP
+#endif // PW_RPC_USE_GLOBAL_MUTEX == 0
+#endif // PW_RPC_YIELD_MODE
+
+/// @def PW_RPC_YIELD_MODE_BUSY_LOOP
+/// @def PW_RPC_YIELD_MODE_SLEEP
+/// @def PW_RPC_YIELD_MODE_YIELD
+///
+/// Supported configuration values for @c_macro{PW_RPC_YIELD_MODE}.
+#define PW_RPC_YIELD_MODE_BUSY_LOOP 100
+#define PW_RPC_YIELD_MODE_SLEEP 101
+#define PW_RPC_YIELD_MODE_YIELD 102
+
+/// If `PW_RPC_YIELD_MODE == PW_RPC_YIELD_MODE_SLEEP`,
+/// `PW_RPC_YIELD_SLEEP_DURATION` sets how long to sleep during each iteration
+/// of the yield loop. The value must be a constant expression that converts to
+/// a @cpp_type{pw::chrono::SystemClock::duration}.
+#ifndef PW_RPC_YIELD_SLEEP_DURATION
+
+// When building for a desktop operating system, use a 1ms sleep by default.
+// 1-tick duration sleeps can result in spurious timeouts.
+#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
+#define PW_RPC_YIELD_SLEEP_DURATION std::chrono::milliseconds(1)
+#else
+#define PW_RPC_YIELD_SLEEP_DURATION pw::chrono::SystemClock::duration(1)
+#endif // defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
+
+#endif // PW_RPC_YIELD_SLEEP_DURATION
+
+// PW_RPC_YIELD_SLEEP_DURATION is not needed for non-sleep yield modes.
+#if PW_RPC_YIELD_MODE != PW_RPC_YIELD_MODE_SLEEP
+#undef PW_RPC_YIELD_SLEEP_DURATION
+#endif // PW_RPC_YIELD_MODE != PW_RPC_YIELD_MODE_SLEEP
+
+/// pw_rpc call objects wait for their callbacks to complete before they are
+/// moved or destoyed. Deadlocks occur if a callback:
+///
+/// - attempts to destroy its call object,
+/// - attempts to move its call object while the call is still active, or
+/// - never returns.
+///
+/// If `PW_RPC_CALLBACK_TIMEOUT_TICKS` is greater than 0, then `PW_CRASH` is
+/// invoked if a thread waits for an RPC callback to complete for more than the
+/// specified tick count.
+///
+/// A "tick" in this context is one iteration of a loop that yields releases the
+/// RPC lock and yields the thread according to @c_macro{PW_RPC_YIELD_MODE}. By
+/// default, the thread yields with a 1-tick call to
+/// @cpp_func{pw::this_thread::sleep_for()}.
+#ifndef PW_RPC_CALLBACK_TIMEOUT_TICKS
+#define PW_RPC_CALLBACK_TIMEOUT_TICKS 10000
+#endif // PW_RPC_CALLBACK_TIMEOUT_TICKS
+
+/// Whether pw_rpc should use dynamic memory allocation internally. If enabled,
+/// pw_rpc dynamically allocates channels and its encoding buffer. RPC users may
+/// use dynamic allocation independently of this option (e.g. to allocate pw_rpc
+/// call objects).
+///
+/// The semantics for allocating and initializing channels change depending on
+/// this option. If dynamic allocation is disabled, pw_rpc endpoints (servers or
+/// clients) use an externally-allocated, fixed-size array of channels. That
+/// array must include unassigned channels or existing channels must be closed
+/// to add new channels.
+///
+/// If dynamic allocation is enabled, an span of channels may be passed to the
+/// endpoint at construction, but these channels are only used to initialize its
+/// internal channels container. External channel objects are NOT used by the
+/// endpoint and cannot be updated if dynamic allocation is enabled. No
+/// unassigned channels should be passed to the endpoint; they will be ignored.
+/// Any number of channels may be added to the endpoint, without closing
+/// existing channels, but adding channels will use more memory.
#ifndef PW_RPC_DYNAMIC_ALLOCATION
#define PW_RPC_DYNAMIC_ALLOCATION 0
#endif // PW_RPC_DYNAMIC_ALLOCATION
-#if PW_RPC_DYNAMIC_ALLOCATION && defined(PW_RPC_ENCODING_BUFFER_SIZE_BYTES)
-static_assert(false,
- "PW_RPC_ENCODING_BUFFER_SIZE_BYTES cannot be set if "
- "PW_RPC_DYNAMIC_ALLOCATION is enabled");
-#endif // PW_RPC_DYNAMIC_ALLOCATION && PW_RPC_ENCODING_BUFFER_SIZE_BYTES
-
-// Size of the global RPC packet encoding buffer in bytes.
+#if defined(PW_RPC_DYNAMIC_CONTAINER) || \
+ defined(PW_RPC_DYNAMIC_CONTAINER_INCLUDE)
+static_assert(
+ PW_RPC_DYNAMIC_ALLOCATION == 1,
+ "PW_RPC_DYNAMIC_ALLOCATION is disabled, so PW_RPC_DYNAMIC_CONTAINER and "
+ "PW_RPC_DYNAMIC_CONTAINER_INCLUDE have no effect and should not be set.");
+#endif // PW_RPC_DYNAMIC_CONTAINER || PW_RPC_DYNAMIC_CONTAINER_INCLUDE
+
+/// If @c_macro{PW_RPC_DYNAMIC_ALLOCATION} is enabled, this macro must expand to
+/// a container capable of storing objects of the provided type. This container
+/// will be used internally by pw_rpc to allocate the channels list and encoding
+/// buffer. Defaults to `std::vector<type>`, but may be set to any type that
+/// supports the following `std::vector` operations:
+///
+/// - Default construction
+/// - `emplace_back()`
+/// - `pop_back()`
+/// - `back()`
+/// - `resize()`
+/// - `clear()`
+/// - Range-based for loop iteration (`begin()`, `end()`)
+///
+#ifndef PW_RPC_DYNAMIC_CONTAINER
+#define PW_RPC_DYNAMIC_CONTAINER(type) std::vector<type>
+#endif // PW_RPC_DYNAMIC_CONTAINER
+
+/// If @c_macro{PW_RPC_DYNAMIC_ALLOCATION} is enabled, this header file is
+/// included in files that use @c_macro{PW_RPC_DYNAMIC_CONTAINER}. Defaults to
+/// `<vector>`, but may be set in conjunction with
+/// @c_macro{PW_RPC_DYNAMIC_CONTAINER} to use a different container type for
+/// dynamic allocations in pw_rpc.
+#ifndef PW_RPC_DYNAMIC_CONTAINER_INCLUDE
+#define PW_RPC_DYNAMIC_CONTAINER_INCLUDE <vector>
+#endif // PW_RPC_DYNAMIC_CONTAINER_INCLUDE
+
+/// Size of the global RPC packet encoding buffer in bytes. If dynamic
+/// allocation is enabled, this value is only used for test helpers that
+/// allocate RPC encoding buffers.
#ifndef PW_RPC_ENCODING_BUFFER_SIZE_BYTES
#define PW_RPC_ENCODING_BUFFER_SIZE_BYTES 512
#endif // PW_RPC_ENCODING_BUFFER_SIZE_BYTES
-// The log level to use for this module. Logs below this level are omitted.
+/// The log level to use for this module. Logs below this level are omitted.
#ifndef PW_RPC_CONFIG_LOG_LEVEL
#define PW_RPC_CONFIG_LOG_LEVEL PW_LOG_LEVEL_INFO
#endif // PW_RPC_CONFIG_LOG_LEVEL
-// The log module name to use for this module.
+/// The log module name to use for this module.
#ifndef PW_RPC_CONFIG_LOG_MODULE_NAME
#define PW_RPC_CONFIG_LOG_MODULE_NAME "PW_RPC"
#endif // PW_RPC_CONFIG_LOG_MODULE_NAME
@@ -113,14 +223,15 @@ inline constexpr size_t kEncodingBufferSizeBytes =
} // namespace pw::rpc::cfg
-// This option determines whether to allocate the Nanopb structs on the stack or
-// in a global variable. Globally allocated structs are NOT thread safe, but
-// work fine when the RPC server's ProcessPacket function is only called from
-// one thread.
+/// This option determines whether to allocate the Nanopb structs on the stack
+/// or in a global variable. Globally allocated structs are NOT thread safe, but
+/// work fine when the RPC server's ProcessPacket function is only called from
+/// one thread.
#ifndef PW_RPC_NANOPB_STRUCT_BUFFER_STACK_ALLOCATE
#define PW_RPC_NANOPB_STRUCT_BUFFER_STACK_ALLOCATE 1
#endif // PW_RPC_NANOPB_STRUCT_BUFFER_STACK_ALLOCATE
+/// @private Internal macro for declaring the Nanopb struct; do not use.
#if PW_RPC_NANOPB_STRUCT_BUFFER_STACK_ALLOCATE
#define _PW_RPC_NANOPB_STRUCT_STORAGE_CLASS
#else
diff --git a/pw_rpc/public/pw_rpc/internal/encoding_buffer.h b/pw_rpc/public/pw_rpc/internal/encoding_buffer.h
new file mode 100644
index 000000000..8ae752f12
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/internal/encoding_buffer.h
@@ -0,0 +1,146 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// Definitions for the static and dynamic versions of the pw_rpc encoding
+// buffer. Both version are compiled rot, but only one is instantiated,
+// depending on the PW_RPC_DYNAMIC_ALLOCATION config option.
+#pragma once
+
+#include <array>
+
+#include "pw_assert/assert.h"
+#include "pw_bytes/span.h"
+#include "pw_rpc/internal/config.h"
+#include "pw_rpc/internal/lock.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_status/status_with_size.h"
+
+#if PW_RPC_DYNAMIC_ALLOCATION
+
+#include PW_RPC_DYNAMIC_CONTAINER_INCLUDE
+
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+namespace pw::rpc::internal {
+
+constexpr ByteSpan ResizeForPayload(ByteSpan buffer) {
+ return buffer.subspan(Packet::kMinEncodedSizeWithoutPayload);
+}
+
+// Wraps a statically allocated encoding buffer.
+class StaticEncodingBuffer {
+ public:
+ constexpr StaticEncodingBuffer() : buffer_{} {}
+
+ ByteSpan AllocatePayloadBuffer() { return ResizeForPayload(buffer_); }
+ ByteSpan GetPacketBuffer(size_t /* payload_size */) { return buffer_; }
+
+ void Release() {}
+ void ReleaseIfAllocated() {}
+
+ private:
+ static_assert(MaxSafePayloadSize() > 0,
+ "pw_rpc's encode buffer is too small to fit any data");
+
+ std::array<std::byte, cfg::kEncodingBufferSizeBytes> buffer_;
+};
+
+#if PW_RPC_DYNAMIC_ALLOCATION
+
+// Wraps a dynamically allocated encoding buffer.
+class DynamicEncodingBuffer {
+ public:
+ DynamicEncodingBuffer() = default;
+
+ ~DynamicEncodingBuffer() { PW_DASSERT(buffer_.empty()); }
+
+ // Allocates a new buffer and returns a portion to use to encode the payload.
+ ByteSpan AllocatePayloadBuffer(size_t payload_size) {
+ Allocate(payload_size);
+ return ResizeForPayload(buffer_);
+ }
+
+ // Returns the buffer into which to encode the packet, allocating a new buffer
+ // if necessary.
+ ByteSpan GetPacketBuffer(size_t payload_size) {
+ if (buffer_.empty()) {
+ Allocate(payload_size);
+ }
+ return buffer_;
+ }
+
+ // Frees the payload buffer, which MUST have been allocated previously.
+ void Release() {
+ PW_DASSERT(!buffer_.empty());
+ buffer_.clear();
+ }
+
+ // Frees the payload buffer, if one was allocated.
+ void ReleaseIfAllocated() {
+ if (!buffer_.empty()) {
+ Release();
+ }
+ }
+
+ private:
+ void Allocate(size_t payload_size) {
+ const size_t buffer_size =
+ payload_size + Packet::kMinEncodedSizeWithoutPayload;
+ PW_DASSERT(buffer_.empty());
+ buffer_.resize(buffer_size);
+ }
+
+ PW_RPC_DYNAMIC_CONTAINER(std::byte) buffer_;
+};
+
+using EncodingBuffer = DynamicEncodingBuffer;
+
+#else
+
+using EncodingBuffer = StaticEncodingBuffer;
+
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+// Instantiate the global encoding buffer variable, depending on whether dynamic
+// allocation is enabled or not.
+inline EncodingBuffer encoding_buffer PW_GUARDED_BY(rpc_lock());
+
+// Successful calls to EncodeToPayloadBuffer MUST send the returned buffer,
+// without releasing the RPC lock.
+template <typename Proto, typename Encoder>
+static Result<ByteSpan> EncodeToPayloadBuffer(Proto& payload,
+ const Encoder& encoder)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ // If dynamic allocation is enabled, calculate the size of the encoded
+ // protobuf and allocate a buffer for it.
+#if PW_RPC_DYNAMIC_ALLOCATION
+ StatusWithSize payload_size = encoder.EncodedSizeBytes(payload);
+ if (!payload_size.ok()) {
+ return Status::Internal();
+ }
+
+ ByteSpan buffer = encoding_buffer.AllocatePayloadBuffer(payload_size.size());
+#else
+ ByteSpan buffer = encoding_buffer.AllocatePayloadBuffer();
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+ StatusWithSize result = encoder.Encode(payload, buffer);
+ if (!result.ok()) {
+ encoding_buffer.ReleaseIfAllocated();
+ return result.status();
+ }
+ return buffer.first(result.size());
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/endpoint.h b/pw_rpc/public/pw_rpc/internal/endpoint.h
index cbbb93848..93a8239a0 100644
--- a/pw_rpc/public/pw_rpc/internal/endpoint.h
+++ b/pw_rpc/public/pw_rpc/internal/endpoint.h
@@ -13,8 +13,9 @@
// the License.
#pragma once
-#include <span>
+#include <tuple>
+#include "pw_assert/assert.h"
#include "pw_containers/intrusive_list.h"
#include "pw_result/result.h"
#include "pw_rpc/internal/call.h"
@@ -22,10 +23,13 @@
#include "pw_rpc/internal/channel_list.h"
#include "pw_rpc/internal/lock.h"
#include "pw_rpc/internal/packet.h"
+#include "pw_span/span.h"
#include "pw_sync/lock_annotations.h"
namespace pw::rpc::internal {
+class LockedEndpoint;
+
// Manages a list of channels and a list of ongoing calls for either a server or
// client.
//
@@ -36,7 +40,11 @@ namespace pw::rpc::internal {
// Server or Client object, which derive from Endpoint.
class Endpoint {
public:
- ~Endpoint();
+ // If an endpoint is deleted, all calls using it are closed without notifying
+ // the other endpoint.
+ ~Endpoint() PW_LOCKS_EXCLUDED(rpc_lock()) { RemoveAllCalls(); }
+
+ // Public functions
// Creates a channel with the provided ID and ChannelOutput, if a channel slot
// is available or can be allocated (if PW_RPC_DYNAMIC_ALLOCATION is enabled).
@@ -50,7 +58,7 @@ class Endpoint {
//
Status OpenChannel(uint32_t id, ChannelOutput& interface)
PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
return channels_.Add(id, interface);
}
@@ -59,42 +67,114 @@ class Endpoint {
// called with the ABORTED status.
Status CloseChannel(uint32_t channel_id) PW_LOCKS_EXCLUDED(rpc_lock());
- // For internal use only: returns the number calls in the RPC calls list.
+ // Internal functions, hidden by the Client and Server classes
+
+ // Returns the number calls in the RPC calls list.
size_t active_call_count() const PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
return calls_.size();
}
- // For internal use only: finds an internal::Channel with this ID or nullptr
- // if none matches.
+ // Claims that `rpc_lock()` is held, returning a wrapped endpoint.
+ //
+ // This function should only be called in contexts in which it is clear that
+ // `rpc_lock()` is held. When calling this function from a constructor, the
+ // lock annotation will not result in errors, so care should be taken to
+ // ensure that `rpc_lock()` is held.
+ LockedEndpoint& ClaimLocked() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
+ // Finds an internal::Channel with this ID or nullptr if none matches.
Channel* GetInternalChannel(uint32_t channel_id)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
return channels_.Get(channel_id);
}
+ // Loops until the list of calls to clean up is empty. Releases the RPC lock.
+ //
+ // This must be called after operations that potentially put calls in the
+ // awaiting cleanup state:
+ //
+ // - Creating a new call object, either from handling a request on the server
+ // or starting a new call on the client.
+ // - Processing a stream message, since decoding to Nanopb or pwpb could fail,
+ // and the RPC mutex should not be released yet.
+ // - Calls to CloseChannel() or UnregisterService(), which may need to cancel
+ // multiple calls before the mutex is released.
+ //
+ void CleanUpCalls() PW_UNLOCK_FUNCTION(rpc_lock());
+
protected:
- _PW_RPC_CONSTEXPR Endpoint(std::span<rpc::Channel> channels)
- : channels_(std::span(static_cast<internal::Channel*>(channels.data()),
- channels.size())),
- next_call_id_(0) {}
+ _PW_RPC_CONSTEXPR Endpoint() = default;
+
+ // Initializes the endpoint from a span of channels.
+ _PW_RPC_CONSTEXPR Endpoint(span<rpc::Channel> channels)
+ : channels_(span(static_cast<internal::Channel*>(channels.data()),
+ channels.size())) {}
// Parses an RPC packet and sets ongoing_call to the matching call, if any.
// Returns the parsed packet or an error.
- Result<Packet> ProcessPacket(std::span<const std::byte> data,
+ Result<Packet> ProcessPacket(span<const std::byte> data,
Packet::Destination destination)
PW_LOCKS_EXCLUDED(rpc_lock());
// Finds a call object for an ongoing call associated with this packet, if
- // any. Returns nullptr if no matching call exists.
- Call* FindCall(const Packet& packet) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- return FindCallById(
- packet.channel_id(), packet.service_id(), packet.method_id());
+ // any. The iterator will be calls_end() if no match was found.
+ IntrusiveList<Call>::iterator FindCall(const Packet& packet)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return std::get<1>(FindIteratorsForCall(packet.channel_id(),
+ packet.service_id(),
+ packet.method_id(),
+ packet.call_id()));
+ }
+
+ // Used to check if a call iterator is valid or not.
+ IntrusiveList<Call>::const_iterator calls_end() const
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return calls_.end();
+ }
+
+ // Aborts calls associated with a particular service. Calls to
+ // AbortCallsForService() must be followed by a call to CleanUpCalls().
+ void AbortCallsForService(const Service& service)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ AbortCalls(AbortIdType::kService, UnwrapServiceId(service.service_id()));
+ }
+
+ // Marks an active call as awaiting cleanup, moving it from the active calls_
+ // list to the to_cleanup_ list.
+ //
+ // This method is protected so it can be exposed in tests.
+ void CloseCallAndMarkForCleanup(Call& call, Status error)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ call.CloseAndMarkForCleanupFromEndpoint(error);
+ calls_.remove(call);
+ to_cleanup_.push_front(call);
+ }
+
+ // Iterator version of CloseCallAndMarkForCleanup. Returns the iterator to the
+ // item after the closed call.
+ IntrusiveList<Call>::iterator CloseCallAndMarkForCleanup(
+ IntrusiveList<Call>::iterator before_call,
+ IntrusiveList<Call>::iterator call_iterator,
+ Status error) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ Call& call = *call_iterator;
+ call.CloseAndMarkForCleanupFromEndpoint(error);
+ auto next = calls_.erase_after(before_call);
+ to_cleanup_.push_front(call);
+ return next;
}
private:
// Give Call access to the register/unregister functions.
friend class Call;
+ enum class AbortIdType : bool { kChannel, kService };
+
+ // Aborts calls for a particular channel or service and enqueues them for
+ // cleanup. AbortCalls() must be followed by a call to CleanUpCalls().
+ void AbortCalls(AbortIdType type, uint32_t id)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
// Returns an ID that can be assigned to a new call.
uint32_t NewCallId() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
// Call IDs are varint encoded. Limit the varint size to 2 bytes (14 usable
@@ -104,7 +184,7 @@ class Endpoint {
}
// Adds a call to the internal call registry. If a matching call already
- // exists, it is cancelled locally (on_error called, no packet sent).
+ // exists, it is cancelled. CleanUpCalls() must be called after RegisterCall.
void RegisterCall(Call& call) PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
// Registers a call that is known to be unique. The calls list is NOT checked
@@ -113,21 +193,77 @@ class Endpoint {
calls_.push_front(call);
}
+ void CleanUpCall(Call& call) PW_UNLOCK_FUNCTION(rpc_lock()) {
+ const bool removed_call_to_cleanup = to_cleanup_.remove(call);
+ PW_DASSERT(removed_call_to_cleanup); // Should have been awaiting cleanup
+ call.CleanUpFromEndpoint();
+ }
+
// Removes the provided call from the call registry.
void UnregisterCall(const Call& call)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
- calls_.remove(call);
+ bool closed_call_was_in_list = calls_.remove(call);
+ PW_DASSERT(closed_call_was_in_list);
}
- Call* FindCallById(uint32_t channel_id,
- uint32_t service_id,
- uint32_t method_id)
+ std::tuple<IntrusiveList<Call>::iterator, IntrusiveList<Call>::iterator>
+ FindIteratorsForCall(uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ uint32_t call_id)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+ std::tuple<IntrusiveList<Call>::iterator, IntrusiveList<Call>::iterator>
+ FindIteratorsForCall(const Call& call)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return FindIteratorsForCall(call.channel_id_locked(),
+ call.service_id(),
+ call.method_id(),
+ call.id());
+ }
+
+ // Silently closes all calls. Called by the destructor. This is a
+ // non-destructor function so that Clang's lock safety analysis applies.
+ //
+ // Endpoints are not deleted in normal RPC use, and especially would not be
+ // deleted before the calls that use them. To handle this unusual case, all
+ // calls are closed without invoking on_error callbacks. If cleanup tasks are
+ // required, users should perform them before deleting the Endpoint. Cleanup
+ // could be done individually for each call or by closing channels with
+ // CloseChannel.
+ void RemoveAllCalls() PW_LOCKS_EXCLUDED(rpc_lock());
+
ChannelList channels_ PW_GUARDED_BY(rpc_lock());
+
+ // List of all active calls associated with this endpoint. Calls are added to
+ // this list when they start and removed from it when they finish.
IntrusiveList<Call> calls_ PW_GUARDED_BY(rpc_lock());
- uint32_t next_call_id_ PW_GUARDED_BY(rpc_lock());
+ // List of all inactive calls that need to have their on_error callbacks
+ // called. Calling on_error requires releasing the RPC lock, so calls are
+ // added to this list in situations where releasing the mutex could be
+ // problematic.
+ IntrusiveList<Call> to_cleanup_ PW_GUARDED_BY(rpc_lock());
+
+ uint32_t next_call_id_ PW_GUARDED_BY(rpc_lock()) = 0;
};
+// An `Endpoint` indicating that `rpc_lock()` is held.
+//
+// This is used as a constructor argument to supplement
+// `PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())`. Current compilers do not enforce
+// lock annotations on constructors; no warnings or errors are produced when
+// calling an annotated constructor without holding `rpc_lock()`.
+class LockedEndpoint : public Endpoint {
+ public:
+ friend class Endpoint;
+ // No public constructor: this is created only via the `ClaimLocked` method on
+ // `Endpoint`.
+ constexpr LockedEndpoint() = delete;
+};
+
+inline LockedEndpoint& Endpoint::ClaimLocked() {
+ return *static_cast<LockedEndpoint*>(this);
+}
+
} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/fake_channel_output.h b/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
index cef049d4f..b60f5c598 100644
--- a/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
+++ b/pw_rpc/public/pw_rpc/internal/fake_channel_output.h
@@ -16,6 +16,7 @@
#include <cstddef>
#include <iterator>
#include <limits>
+#include <mutex>
#include "pw_bytes/span.h"
#include "pw_containers/vector.h"
@@ -29,6 +30,16 @@
#include "pw_sync/lock_annotations.h"
namespace pw::rpc {
+namespace internal {
+
+// Forward declare for a friend statement.
+template <class, size_t, size_t, size_t>
+class ForwardingChannelOutput;
+
+} // namespace internal
+} // namespace pw::rpc
+
+namespace pw::rpc {
class FakeServer;
@@ -44,7 +55,7 @@ class FakeChannelOutput : public ChannelOutput {
FakeChannelOutput& operator=(FakeChannelOutput&&) = delete;
Status last_status() const PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
PW_ASSERT(total_response_packets_ > 0);
return packets_.back().status();
}
@@ -59,7 +70,7 @@ class FakeChannelOutput : public ChannelOutput {
template <auto kMethod>
PayloadsView payloads(uint32_t channel_id = Channel::kUnassignedChannelId)
const PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
return PayloadsView(packets_,
MethodInfo<kMethod>::kType,
channel_id,
@@ -71,10 +82,33 @@ class FakeChannelOutput : public ChannelOutput {
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id) const PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
return PayloadsView(packets_, type, channel_id, service_id, method_id);
}
+ // Returns a number of the payloads seen for this RPC.
+ template <auto kMethod>
+ size_t total_payloads(uint32_t channel_id = Channel::kUnassignedChannelId)
+ const PW_LOCKS_EXCLUDED(mutex_) {
+ std::lock_guard lock(mutex_);
+ return PayloadsView(packets_,
+ MethodInfo<kMethod>::kType,
+ channel_id,
+ MethodInfo<kMethod>::kServiceId,
+ MethodInfo<kMethod>::kMethodId)
+ .size();
+ }
+
+ // Returns a number of the payloads seen for this RPC.
+ size_t total_payloads(MethodType type,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id) const PW_LOCKS_EXCLUDED(mutex_) {
+ std::lock_guard lock(mutex_);
+ return PayloadsView(packets_, type, channel_id, service_id, method_id)
+ .size();
+ }
+
// Returns a view of the final statuses seen for this RPC. Only relevant for
// checking packets sent by a server.
//
@@ -86,10 +120,10 @@ class FakeChannelOutput : public ChannelOutput {
template <auto kMethod>
StatusView completions(uint32_t channel_id = Channel::kUnassignedChannelId)
const PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
return StatusView(packets_,
- internal::PacketType::RESPONSE,
- internal::PacketType::RESPONSE,
+ internal::pwpb::PacketType::RESPONSE,
+ internal::pwpb::PacketType::RESPONSE,
channel_id,
MethodInfo<kMethod>::kServiceId,
MethodInfo<kMethod>::kMethodId);
@@ -105,10 +139,10 @@ class FakeChannelOutput : public ChannelOutput {
template <auto kMethod>
StatusView errors(uint32_t channel_id = Channel::kUnassignedChannelId) const
PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
return StatusView(packets_,
- internal::PacketType::CLIENT_ERROR,
- internal::PacketType::SERVER_ERROR,
+ internal::pwpb::PacketType::CLIENT_ERROR,
+ internal::pwpb::PacketType::SERVER_ERROR,
channel_id,
MethodInfo<kMethod>::kServiceId,
MethodInfo<kMethod>::kMethodId);
@@ -120,12 +154,12 @@ class FakeChannelOutput : public ChannelOutput {
size_t client_stream_end_packets(
uint32_t channel_id = Channel::kUnassignedChannelId) const
PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
return internal::test::PacketsView(
packets_,
internal::test::PacketFilter(
- internal::PacketType::CLIENT_STREAM_END,
- internal::PacketType::CLIENT_STREAM_END,
+ internal::pwpb::PacketType::CLIENT_STREAM_END,
+ internal::pwpb::PacketType::CLIENT_STREAM_END,
channel_id,
MethodInfo<kMethod>::kServiceId,
MethodInfo<kMethod>::kMethodId))
@@ -135,19 +169,19 @@ class FakeChannelOutput : public ChannelOutput {
// The maximum number of packets this FakeChannelOutput can store. Attempting
// to store more packets than this is an error.
size_t max_packets() const PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
return packets_.max_size();
}
// The total number of packets that have been sent.
size_t total_packets() const PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
return packets_.size();
}
// Set to true if a RESPONSE packet is seen.
bool done() const PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
return total_response_packets_ > 0;
}
@@ -157,7 +191,7 @@ class FakeChannelOutput : public ChannelOutput {
// Returns `status` for all future Send calls. Enables packet processing if
// `status` is OK.
void set_send_status(Status status) PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
send_status_ = status;
return_after_packet_count_ = status.ok() ? -1 : 0;
}
@@ -165,7 +199,7 @@ class FakeChannelOutput : public ChannelOutput {
// Returns `status` once after the specified positive number of packets.
void set_send_status(Status status, int return_after_packet_count)
PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
PW_ASSERT(!status.ok());
PW_ASSERT(return_after_packet_count > 0);
send_status_ = status;
@@ -181,7 +215,7 @@ class FakeChannelOutput : public ChannelOutput {
// When equals 0, returns `send_status_` in all future calls,
// When negative, ignores `send_status_` processes buffer.
Status Send(ConstByteSpan buffer) final PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
const Status status = HandlePacket(buffer);
if (on_send_ != nullptr) {
on_send_(buffer, status);
@@ -192,7 +226,7 @@ class FakeChannelOutput : public ChannelOutput {
// Gives access to the last received internal::Packet. This is hidden by the
// raw/Nanopb implementations, since it gives access to an internal class.
const Packet& last_packet() const PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
PW_ASSERT(!packets_.empty());
return packets_.back();
}
@@ -205,7 +239,7 @@ class FakeChannelOutput : public ChannelOutput {
// deadlocks.
void set_on_send(Function<void(ConstByteSpan, Status)>&& on_send)
PW_LOCKS_EXCLUDED(mutex_) {
- LockGuard lock(mutex_);
+ std::lock_guard lock(mutex_);
on_send_ = std::move(on_send);
}
@@ -215,10 +249,16 @@ class FakeChannelOutput : public ChannelOutput {
packets_(packets),
payloads_(payloads) {}
- const Vector<Packet>& packets() const { return packets_; }
+ const Vector<Packet>& packets() const PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
+ return packets_;
+ }
+
+ RpcLock& mutex() const { return mutex_; }
private:
friend class rpc::FakeServer;
+ template <class, size_t, size_t, size_t>
+ friend class internal::ForwardingChannelOutput;
Status HandlePacket(ConstByteSpan buffer) PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
void CopyPayloadToBuffer(Packet& packet) PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_);
@@ -239,8 +279,7 @@ template <size_t kMaxPackets, size_t kPayloadsBufferSizeBytes>
class FakeChannelOutputBuffer : public FakeChannelOutput {
protected:
FakeChannelOutputBuffer()
- : FakeChannelOutput(packets_array_, payloads_array_), payloads_array_ {}
- {}
+ : FakeChannelOutput(packets_array_, payloads_array_), payloads_array_{} {}
Vector<std::byte, kPayloadsBufferSizeBytes> payloads_array_;
Vector<Packet, kMaxPackets> packets_array_;
diff --git a/pw_rpc/public/pw_rpc/internal/lock.h b/pw_rpc/public/pw_rpc/internal/lock.h
index 50c53d5c3..3961eeee6 100644
--- a/pw_rpc/public/pw_rpc/internal/lock.h
+++ b/pw_rpc/public/pw_rpc/internal/lock.h
@@ -15,11 +15,10 @@
#include "pw_rpc/internal/config.h"
#include "pw_sync/lock_annotations.h"
+#include "pw_toolchain/no_destructor.h"
#if PW_RPC_USE_GLOBAL_MUTEX
-#include <mutex>
-
#include "pw_sync/mutex.h" // nogncheck
#endif // PW_RPC_USE_GLOBAL_MUTEX
@@ -29,7 +28,6 @@ namespace pw::rpc::internal {
#if PW_RPC_USE_GLOBAL_MUTEX
using RpcLock = sync::Mutex;
-using LockGuard = std::lock_guard<RpcLock>;
#else
@@ -39,18 +37,21 @@ class PW_LOCKABLE("pw::rpc::internal::RpcLock") RpcLock {
constexpr void unlock() PW_UNLOCK_FUNCTION() {}
};
-class PW_SCOPED_LOCKABLE LockGuard {
+#endif // PW_RPC_USE_GLOBAL_MUTEX
+
+inline RpcLock& rpc_lock() {
+ static NoDestructor<RpcLock> lock;
+ return *lock;
+}
+
+class PW_SCOPED_LOCKABLE RpcLockGuard {
public:
- // [[maybe_unused]] needs to be after the parameter to workaround a gcc bug
- // context: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=81429
- constexpr LockGuard(RpcLock& mutex [[maybe_unused]])
- PW_EXCLUSIVE_LOCK_FUNCTION(mutex) {}
+ RpcLockGuard() PW_EXCLUSIVE_LOCK_FUNCTION(rpc_lock()) { rpc_lock().lock(); }
- ~LockGuard() PW_UNLOCK_FUNCTION() = default;
+ ~RpcLockGuard() PW_UNLOCK_FUNCTION(rpc_lock()) { rpc_lock().unlock(); }
};
-#endif // PW_RPC_USE_GLOBAL_MUTEX
-
-RpcLock& rpc_lock();
+// Releases the RPC lock, yields, and reacquires it.
+void YieldRpcLock() PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/method_info_tester.h b/pw_rpc/public/pw_rpc/internal/method_info_tester.h
index 6e800993a..1312ca2d3 100644
--- a/pw_rpc/public/pw_rpc/internal/method_info_tester.h
+++ b/pw_rpc/public/pw_rpc/internal/method_info_tester.h
@@ -13,8 +13,11 @@
// the License.
#pragma once
+#include <type_traits>
+
#include "pw_rpc/internal/hash.h"
#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/method_info.h"
namespace pw::rpc::internal {
@@ -23,7 +26,8 @@ template <typename GeneratedClass, typename ServiceImpl>
class MethodInfoTests {
public:
constexpr bool Pass() const {
- return Ids().Pass() && MethodFunction().Pass();
+ return Ids().Pass() && MethodFunction().Pass() &&
+ MethodRequestResponseTypes().Pass();
}
private:
@@ -68,6 +72,25 @@ class MethodInfoTests {
PW_RPC_TEST_METHOD_INFO_FUNCTION(TestBidirectionalStreamRpc);
#undef PW_RPC_TEST_METHOD_INFO_FUNCTION
};
+
+ struct MethodRequestResponseTypes {
+ constexpr bool Pass() const { return true; }
+
+#define PW_RPC_TEST_PUBLIC_METHOD_INFO_TYPES(function) \
+ static_assert( \
+ std::is_same_v<typename MethodInfo<GeneratedClass::function>::Request, \
+ ::pw::rpc::MethodRequestType<GeneratedClass::function>>); \
+ static_assert( \
+ std::is_same_v<typename MethodInfo<GeneratedClass::function>::Response, \
+ ::pw::rpc::MethodResponseType<GeneratedClass::function>>)
+
+ PW_RPC_TEST_PUBLIC_METHOD_INFO_TYPES(TestUnaryRpc);
+ PW_RPC_TEST_PUBLIC_METHOD_INFO_TYPES(TestAnotherUnaryRpc);
+ PW_RPC_TEST_PUBLIC_METHOD_INFO_TYPES(TestServerStreamRpc);
+ PW_RPC_TEST_PUBLIC_METHOD_INFO_TYPES(TestClientStreamRpc);
+ PW_RPC_TEST_PUBLIC_METHOD_INFO_TYPES(TestBidirectionalStreamRpc);
+#undef PW_RPC_TEST_PUBLIC_METHOD_INFO_TYPES
+ };
};
} // namespace pw::rpc::internal
diff --git a/pw_rpc/public/pw_rpc/internal/method_lookup.h b/pw_rpc/public/pw_rpc/internal/method_lookup.h
index be0745582..232e0c4db 100644
--- a/pw_rpc/public/pw_rpc/internal/method_lookup.h
+++ b/pw_rpc/public/pw_rpc/internal/method_lookup.h
@@ -35,6 +35,13 @@ class MethodLookup {
}
template <typename Service, uint32_t kMethodId>
+ static constexpr const auto& GetPwpbMethod() {
+ const auto& method = GetMethodUnion<Service, kMethodId>().pwpb_method();
+ static_assert(method.id() == kMethodId, "Incorrect method implementation");
+ return method;
+ }
+
+ template <typename Service, uint32_t kMethodId>
static constexpr const auto& GetNanopbMethod() {
const auto& method = GetMethodUnion<Service, kMethodId>().nanopb_method();
static_assert(method.id() == kMethodId, "Incorrect method implementation");
diff --git a/pw_rpc/public/pw_rpc/internal/packet.h b/pw_rpc/public/pw_rpc/internal/packet.h
index 279c4877a..64fe6b2e4 100644
--- a/pw_rpc/public/pw_rpc/internal/packet.h
+++ b/pw_rpc/public/pw_rpc/internal/packet.h
@@ -15,11 +15,11 @@
#include <cstddef>
#include <cstdint>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_protobuf/serialized_size.h"
#include "pw_rpc/internal/packet.pwpb.h"
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
namespace pw::rpc::internal {
@@ -28,16 +28,20 @@ class Packet {
public:
static constexpr uint32_t kUnassignedId = 0;
+ // TODO(b/236156534): This can use the pwpb generated
+ // pw::rpc::internal::pwpb::RpcPacket::kMaxEncodedSizeBytes once the max value
+ // of enums is properly accounted for and when `status` is changed from a
+ // uint32 to a StatusCode.
static constexpr size_t kMinEncodedSizeWithoutPayload =
- protobuf::SizeOfFieldEnum(RpcPacket::Fields::TYPE, 7) +
- protobuf::SizeOfFieldUint32(RpcPacket::Fields::CHANNEL_ID) +
- protobuf::SizeOfFieldUint32(RpcPacket::Fields::SERVICE_ID) +
- protobuf::SizeOfFieldUint32(RpcPacket::Fields::METHOD_ID) +
- protobuf::SizeOfDelimitedFieldWithoutValue(RpcPacket::Fields::PAYLOAD) +
- protobuf::SizeOfFieldUint32(RpcPacket::Fields::STATUS,
+ protobuf::SizeOfFieldEnum(pwpb::RpcPacket::Fields::kType, 7) +
+ protobuf::SizeOfFieldUint32(pwpb::RpcPacket::Fields::kChannelId) +
+ protobuf::SizeOfFieldFixed32(pwpb::RpcPacket::Fields::kServiceId) +
+ protobuf::SizeOfFieldFixed32(pwpb::RpcPacket::Fields::kMethodId) +
+ protobuf::SizeOfDelimitedFieldWithoutValue(
+ pwpb::RpcPacket::Fields::kPayload) +
+ protobuf::SizeOfFieldUint32(pwpb::RpcPacket::Fields::kStatus,
Status::Unauthenticated().code()) +
- protobuf::SizeOfFieldUint32(RpcPacket::Fields::CALL_ID);
- ;
+ protobuf::SizeOfFieldUint32(pwpb::RpcPacket::Fields::kCallId);
// Parses a packet from a protobuf message. Missing or malformed fields take
// their default values.
@@ -47,7 +51,7 @@ class Packet {
// provided packet.
static constexpr Packet Response(const Packet& request,
Status status = OkStatus()) {
- return Packet(PacketType::RESPONSE,
+ return Packet(pwpb::PacketType::RESPONSE,
request.channel_id(),
request.service_id(),
request.method_id(),
@@ -59,7 +63,7 @@ class Packet {
// Creates a SERVER_ERROR packet with the channel, service, and method ID of
// the provided packet.
static constexpr Packet ServerError(const Packet& packet, Status status) {
- return Packet(PacketType::SERVER_ERROR,
+ return Packet(pwpb::PacketType::SERVER_ERROR,
packet.channel_id(),
packet.service_id(),
packet.method_id(),
@@ -71,7 +75,7 @@ class Packet {
// Creates a CLIENT_ERROR packet with the channel, service, and method ID of
// the provided packet.
static constexpr Packet ClientError(const Packet& packet, Status status) {
- return Packet(PacketType::CLIENT_ERROR,
+ return Packet(pwpb::PacketType::CLIENT_ERROR,
packet.channel_id(),
packet.service_id(),
packet.method_id(),
@@ -82,9 +86,10 @@ class Packet {
// Creates an empty packet.
constexpr Packet()
- : Packet(PacketType{}, kUnassignedId, kUnassignedId, kUnassignedId) {}
+ : Packet(
+ pwpb::PacketType{}, kUnassignedId, kUnassignedId, kUnassignedId) {}
- constexpr Packet(PacketType type,
+ constexpr Packet(pwpb::PacketType type,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
@@ -116,7 +121,7 @@ class Packet {
return static_cast<int>(type_) % 2 == 0 ? kServer : kClient;
}
- constexpr PacketType type() const { return type_; }
+ constexpr pwpb::PacketType type() const { return type_; }
constexpr uint32_t channel_id() const { return channel_id_; }
constexpr uint32_t service_id() const { return service_id_; }
constexpr uint32_t method_id() const { return method_id_; }
@@ -124,7 +129,7 @@ class Packet {
constexpr const ConstByteSpan& payload() const { return payload_; }
constexpr const Status& status() const { return status_; }
- constexpr void set_type(PacketType type) { type_ = type; }
+ constexpr void set_type(pwpb::PacketType type) { type_ = type; }
constexpr void set_channel_id(uint32_t channel_id) {
channel_id_ = channel_id;
}
@@ -137,7 +142,7 @@ class Packet {
constexpr void set_status(Status status) { status_ = status; }
private:
- PacketType type_;
+ pwpb::PacketType type_;
uint32_t channel_id_;
uint32_t service_id_;
uint32_t method_id_;
diff --git a/pw_rpc/public/pw_rpc/internal/server_call.h b/pw_rpc/public/pw_rpc/internal/server_call.h
index c7a9c5311..65cb7375d 100644
--- a/pw_rpc/public/pw_rpc/internal/server_call.h
+++ b/pw_rpc/public/pw_rpc/internal/server_call.h
@@ -25,14 +25,20 @@ class ServerCall : public Call {
public:
void HandleClientStreamEnd() PW_UNLOCK_FUNCTION(rpc_lock()) {
MarkClientStreamCompleted();
- // TODO(pwbug/597): Ensure on_client_stream_end_ is properly guarded.
- rpc_lock().unlock();
#if PW_RPC_CLIENT_STREAM_END_CALLBACK
- if (on_client_stream_end_) {
- on_client_stream_end_();
+ auto on_client_stream_end_local = std::move(on_client_stream_end_);
+ CallbackStarted();
+ rpc_lock().unlock();
+
+ if (on_client_stream_end_local) {
+ on_client_stream_end_local();
}
+
+ rpc_lock().lock();
+ CallbackFinished();
#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
+ rpc_lock().unlock();
}
protected:
@@ -40,14 +46,14 @@ class ServerCall : public Call {
ServerCall(ServerCall&& other) { *this = std::move(other); }
- ~ServerCall() {
+ ~ServerCall() PW_LOCKS_EXCLUDED(rpc_lock()) {
// Any errors are logged in Channel::Send.
CloseAndSendResponse(OkStatus()).IgnoreError();
}
// Version of operator= used by the raw call classes.
ServerCall& operator=(ServerCall&& other) PW_LOCKS_EXCLUDED(rpc_lock()) {
- LockGuard lock(rpc_lock());
+ RpcLockGuard lock;
MoveServerCallFrom(other);
return *this;
}
@@ -55,21 +61,23 @@ class ServerCall : public Call {
void MoveServerCallFrom(ServerCall& other)
PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
- ServerCall(const CallContext& context, MethodType type)
- : Call(context, type) {}
+ ServerCall(const LockedCallContext& context, CallProperties properties)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : Call(context, properties) {}
// set_on_client_stream_end is templated so that it can be conditionally
// disabled with a helpful static_assert message.
template <typename UnusedType = void>
void set_on_client_stream_end(
- [[maybe_unused]] Function<void()>&& on_client_stream_end) {
- // TODO(pwbug/597): Ensure on_client_stream_end_ is properly guarded.
+ [[maybe_unused]] Function<void()>&& on_client_stream_end)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
static_assert(
cfg::kClientStreamEndCallbackEnabled<UnusedType>,
"The client stream end callback is disabled, so "
"set_on_client_stream_end cannot be called. To enable the client end "
"callback, set PW_RPC_CLIENT_STREAM_END_CALLBACK to 1.");
#if PW_RPC_CLIENT_STREAM_END_CALLBACK
+ RpcLockGuard lock;
on_client_stream_end_ = std::move(on_client_stream_end);
#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
}
@@ -77,7 +85,7 @@ class ServerCall : public Call {
private:
#if PW_RPC_CLIENT_STREAM_END_CALLBACK
// Called when a client stream completes.
- Function<void()> on_client_stream_end_;
+ Function<void()> on_client_stream_end_ PW_GUARDED_BY(rpc_lock());
#endif // PW_RPC_CLIENT_STREAM_END_CALLBACK
};
diff --git a/pw_rpc/public/pw_rpc/internal/test_method.h b/pw_rpc/public/pw_rpc/internal/test_method.h
index bdf143580..cf06bb338 100644
--- a/pw_rpc/public/pw_rpc/internal/test_method.h
+++ b/pw_rpc/public/pw_rpc/internal/test_method.h
@@ -15,7 +15,6 @@
#include <cstdint>
#include <cstring>
-#include <span>
#include "pw_rpc/internal/lock.h"
#include "pw_rpc/internal/method.h"
@@ -23,6 +22,8 @@
#include "pw_rpc/internal/packet.h"
#include "pw_rpc/internal/server_call.h"
#include "pw_rpc/method_type.h"
+#include "pw_rpc/server.h"
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
namespace pw::rpc::internal {
@@ -34,8 +35,9 @@ class TestMethod : public Method {
class FakeServerCall : public ServerCall {
public:
constexpr FakeServerCall() = default;
- FakeServerCall(const CallContext& context, MethodType type)
- : ServerCall(context, type) {}
+ FakeServerCall(const LockedCallContext& context, MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : ServerCall(context, CallProperties(type, kServerCall, kRawProto)) {}
FakeServerCall(FakeServerCall&&) = default;
FakeServerCall& operator=(FakeServerCall&&) = default;
@@ -70,9 +72,9 @@ class TestMethod : public Method {
test_method.invocations_ += 1;
// Create a call object so it registers / unregisters with the server.
- FakeServerCall fake_call(context, kType);
+ FakeServerCall fake_call(context.ClaimLocked(), kType);
- rpc_lock().unlock();
+ context.server().CleanUpCalls();
if (test_method.move_to_call_ != nullptr) {
*test_method.move_to_call_ = std::move(fake_call);
@@ -100,7 +102,7 @@ class TestMethod : public Method {
mutable size_t invocations_;
mutable FakeServerCall* move_to_call_;
- std::span<const std::byte> response_;
+ span<const std::byte> response_;
Status response_status_;
};
diff --git a/pw_rpc/public/pw_rpc/internal/test_method_context.h b/pw_rpc/public/pw_rpc/internal/test_method_context.h
index a8e546194..2cc49953c 100644
--- a/pw_rpc/public/pw_rpc/internal/test_method_context.h
+++ b/pw_rpc/public/pw_rpc/internal/test_method_context.h
@@ -17,6 +17,7 @@
#include "pw_assert/assert.h"
#include "pw_rpc/channel.h"
+#include "pw_rpc/internal/call.h"
#include "pw_rpc/internal/fake_channel_output.h"
#include "pw_rpc/internal/method.h"
#include "pw_rpc/internal/packet.h"
@@ -59,8 +60,10 @@ class InvocationContext {
// responses is responses().max_size(). responses().back() is always the most
// recent response, even if total_responses() > responses().max_size().
auto responses() const {
- return output().payloads(
- method_type_, channel_.id(), service().id(), kMethodId);
+ return output().payloads(method_type_,
+ channel_.id(),
+ UnwrapServiceId(service().service_id()),
+ kMethodId);
}
// True if the RPC has completed.
@@ -73,18 +76,19 @@ class InvocationContext {
}
void SendClientError(Status error) {
+ using PacketType = ::pw::rpc::internal::pwpb::PacketType;
+
std::byte packet[kNoPayloadPacketSizeBytes];
PW_ASSERT(server_
.ProcessPacket(Packet(PacketType::CLIENT_ERROR,
channel_.id(),
- service_.id(),
+ UnwrapServiceId(service_.service_id()),
kMethodId,
0,
{},
error)
.Encode(packet)
- .value(),
- output_)
+ .value())
.ok());
}
@@ -101,7 +105,7 @@ class InvocationContext {
ServiceArgs&&... service_args)
: method_type_(method_type),
channel_(123, &output_),
- server_(std::span(static_cast<rpc::Channel*>(&channel_), 1)),
+ server_(span(static_cast<rpc::Channel*>(&channel_), 1)),
service_(std::forward<ServiceArgs>(service_args)...),
context_(server_, channel_.id(), service_, method, 0) {
server_.RegisterService(service_);
@@ -111,36 +115,38 @@ class InvocationContext {
template <size_t kMaxPayloadSize = 32>
void SendClientStream(ConstByteSpan payload) {
+ using PacketType = ::pw::rpc::internal::pwpb::PacketType;
+
std::byte packet[kNoPayloadPacketSizeBytes + 3 + kMaxPayloadSize];
PW_ASSERT(server_
.ProcessPacket(Packet(PacketType::CLIENT_STREAM,
channel_.id(),
- service_.id(),
+ UnwrapServiceId(service_.service_id()),
kMethodId,
0,
payload)
.Encode(packet)
- .value(),
- output_)
+ .value())
.ok());
}
void SendClientStreamEnd() {
+ using PacketType = ::pw::rpc::internal::pwpb::PacketType;
+
std::byte packet[kNoPayloadPacketSizeBytes];
PW_ASSERT(server_
.ProcessPacket(Packet(PacketType::CLIENT_STREAM_END,
channel_.id(),
- service_.id(),
+ UnwrapServiceId(service_.service_id()),
kMethodId)
.Encode(packet)
- .value(),
- output_)
+ .value())
.ok());
}
// Invokes the RPC, optionally with a request argument.
template <auto kMethod, typename T, typename... RequestArg>
- void call(RequestArg&&... request) {
+ void call(RequestArg&&... request) PW_LOCKS_EXCLUDED(rpc_lock()) {
static_assert(sizeof...(request) <= 1);
output_.clear();
T responder = GetResponder<T>();
@@ -149,8 +155,9 @@ class InvocationContext {
}
template <typename T>
- T GetResponder() {
- return T(call_context());
+ T GetResponder() PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ return T(call_context().ClaimLocked());
}
const internal::CallContext& call_context() const { return context_; }
diff --git a/pw_rpc/public/pw_rpc/internal/test_utils.h b/pw_rpc/public/pw_rpc/internal/test_utils.h
index e3f8be5f5..773b18e42 100644
--- a/pw_rpc/public/pw_rpc/internal/test_utils.h
+++ b/pw_rpc/public/pw_rpc/internal/test_utils.h
@@ -19,7 +19,6 @@
#include <array>
#include <cstddef>
#include <cstdint>
-#include <span>
#include "gtest/gtest.h"
#include "pw_assert/assert.h"
@@ -29,12 +28,15 @@
#include "pw_rpc/internal/packet.h"
#include "pw_rpc/raw/fake_channel_output.h"
#include "pw_rpc/server.h"
+#include "pw_span/span.h"
namespace pw::rpc::internal {
// Version of the Server with extra methods exposed for testing.
class TestServer : public Server {
public:
+ using Server::calls_end;
+ using Server::CloseCallAndMarkForCleanup;
using Server::FindCall;
};
@@ -46,16 +48,15 @@ class ServerContextForTest {
ServerContextForTest(const internal::Method& method)
: channel_(Channel::Create<kChannelId>(&output_)),
- server_(std::span(&channel_, 1)),
+ server_(span(&channel_, 1)),
service_(kServiceId),
- context_(
- static_cast<Server&>(server_), channel_.id(), service_, method, 0) {
+ context_(server_, channel_.id(), service_, method, 0) {
server_.RegisterService(service_);
}
// Create packets for this context's channel, service, and method.
- internal::Packet request(std::span<const std::byte> payload) const {
- return internal::Packet(internal::PacketType::REQUEST,
+ internal::Packet request(span<const std::byte> payload) const {
+ return internal::Packet(internal::pwpb::PacketType::REQUEST,
kChannelId,
kServiceId,
context_.method().id(),
@@ -64,8 +65,8 @@ class ServerContextForTest {
}
internal::Packet response(Status status,
- std::span<const std::byte> payload = {}) const {
- return internal::Packet(internal::PacketType::RESPONSE,
+ span<const std::byte> payload = {}) const {
+ return internal::Packet(internal::pwpb::PacketType::RESPONSE,
kChannelId,
kServiceId,
context_.method().id(),
@@ -74,8 +75,8 @@ class ServerContextForTest {
status);
}
- internal::Packet server_stream(std::span<const std::byte> payload) const {
- return internal::Packet(internal::PacketType::SERVER_STREAM,
+ internal::Packet server_stream(span<const std::byte> payload) const {
+ return internal::Packet(internal::pwpb::PacketType::SERVER_STREAM,
kChannelId,
kServiceId,
context_.method().id(),
@@ -83,8 +84,8 @@ class ServerContextForTest {
payload);
}
- internal::Packet client_stream(std::span<const std::byte> payload) const {
- return internal::Packet(internal::PacketType::CLIENT_STREAM,
+ internal::Packet client_stream(span<const std::byte> payload) const {
+ return internal::Packet(internal::pwpb::PacketType::CLIENT_STREAM,
kChannelId,
kServiceId,
context_.method().id(),
@@ -92,7 +93,14 @@ class ServerContextForTest {
payload);
}
- const internal::CallContext& get() { return context_; }
+ CallContext get(uint32_t id = 0) const {
+ return CallContext(context_.server(),
+ context_.channel_id(),
+ context_.service(),
+ context_.method(),
+ id);
+ }
+
internal::test::FakeChannelOutput& output() { return output_; }
TestServer& server() { return static_cast<TestServer&>(server_); }
Service& service() { return service_; }
@@ -118,7 +126,7 @@ class ClientContextForTest {
ClientContextForTest()
: channel_(Channel::Create<kChannelId>(&output_)),
- client_(std::span(&channel_, 1)) {}
+ client_(span(&channel_, 1)) {}
const internal::test::FakeChannelOutput& output() const { return output_; }
Channel& channel() { return static_cast<Channel&>(channel_); }
@@ -126,9 +134,9 @@ class ClientContextForTest {
// Sends a packet to be processed by the client. Returns the client's
// ProcessPacket status.
- Status SendPacket(internal::PacketType type,
+ Status SendPacket(internal::pwpb::PacketType type,
Status status = OkStatus(),
- std::span<const std::byte> payload = {}) {
+ span<const std::byte> payload = {}) {
uint32_t call_id =
output().total_packets() > 0 ? output().last_packet().call_id() : 0;
@@ -140,12 +148,13 @@ class ClientContextForTest {
return client_.ProcessPacket(result.value_or(ConstByteSpan()));
}
- Status SendResponse(Status status, std::span<const std::byte> payload = {}) {
- return SendPacket(internal::PacketType::RESPONSE, status, payload);
+ Status SendResponse(Status status, span<const std::byte> payload = {}) {
+ return SendPacket(internal::pwpb::PacketType::RESPONSE, status, payload);
}
- Status SendServerStream(std::span<const std::byte> payload) {
- return SendPacket(internal::PacketType::SERVER_STREAM, OkStatus(), payload);
+ Status SendServerStream(span<const std::byte> payload) {
+ return SendPacket(
+ internal::pwpb::PacketType::SERVER_STREAM, OkStatus(), payload);
}
private:
diff --git a/pw_rpc/public/pw_rpc/method_id.h b/pw_rpc/public/pw_rpc/method_id.h
new file mode 100644
index 000000000..8527aba19
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/method_id.h
@@ -0,0 +1,75 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+#include <functional>
+
+// NOTE: These wrappers exist in order to provide future compatibility for
+// different internal representations of method identifiers.
+
+namespace pw::rpc {
+
+class MethodId;
+
+namespace internal {
+constexpr MethodId WrapMethodId(uint32_t id);
+constexpr uint32_t UnwrapMethodId(MethodId id);
+} // namespace internal
+
+// An identifier for a method.
+class MethodId {
+ private:
+ constexpr explicit MethodId(uint32_t id) : id_(id) {}
+ friend constexpr MethodId internal::WrapMethodId(uint32_t id);
+ friend constexpr uint32_t internal::UnwrapMethodId(MethodId id);
+ uint32_t id_;
+};
+
+constexpr bool operator==(MethodId lhs, MethodId rhs) {
+ return internal::UnwrapMethodId(lhs) == internal::UnwrapMethodId(rhs);
+}
+
+constexpr bool operator!=(MethodId lhs, MethodId rhs) { return !(lhs == rhs); }
+
+// Comparisons are provided to enable sorting by `MethodId`.
+
+constexpr bool operator<(MethodId lhs, MethodId rhs) {
+ return internal::UnwrapMethodId(lhs) < internal::UnwrapMethodId(rhs);
+}
+
+constexpr bool operator>(MethodId lhs, MethodId rhs) { return rhs < lhs; }
+
+constexpr bool operator<=(MethodId lhs, MethodId rhs) { return !(lhs > rhs); }
+
+constexpr bool operator>=(MethodId lhs, MethodId rhs) { return !(lhs < rhs); }
+
+namespace internal {
+
+constexpr MethodId WrapMethodId(uint32_t id) { return MethodId(id); }
+constexpr uint32_t UnwrapMethodId(MethodId id) { return id.id_; }
+
+} // namespace internal
+} // namespace pw::rpc
+
+namespace std {
+
+template <>
+struct hash<pw::rpc::MethodId> {
+ size_t operator()(const pw::rpc::MethodId& id) const {
+ return hash<uint32_t>{}(::pw::rpc::internal::UnwrapMethodId(id));
+ }
+};
+
+} // namespace std
diff --git a/pw_rpc/public/pw_rpc/method_info.h b/pw_rpc/public/pw_rpc/method_info.h
new file mode 100644
index 000000000..e11dd6115
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/method_info.h
@@ -0,0 +1,76 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/method_id.h"
+#include "pw_rpc/service_id.h"
+
+namespace pw::rpc {
+
+// Collection of various helpers for RPC calls introspection. For now contains
+// only MethodRequestType/MethodResponseTypes types to obtain information about
+// RPC methods request/response types.
+//
+// Example. We have an RPC service:
+//
+// package some.package;
+// service SpecialService {
+// rpc MyMethod(MyMethodRequest) returns (MyMethodResponse) {}
+// }
+//
+// We also have a templated Storage type alias:
+//
+// template <auto kMethod>
+// using Storage =
+// std::pair<MethodRequestType<kMethod>, MethodResponseType<kMethod>>;
+//
+// Storage<some::package::pw_rpc::pwpb::SpecialService::MyMethod> will
+// instantiate as:
+//
+// std::pair<some::package::MyMethodRequest::Message,
+// some::package::MyMethodResponse::Message>;
+
+// Request type for given kMethod.
+template <auto kMethod>
+using MethodRequestType = typename internal::MethodInfo<kMethod>::Request;
+
+// Response type for given kMethod.
+template <auto kMethod>
+using MethodResponseType = typename internal::MethodInfo<kMethod>::Response;
+
+// Function which returns a serializer for given kMethod.
+// For e.g. `pwpb` methods, this returns a `const PwpbMethodSerde&`.
+template <auto kMethod>
+constexpr const auto& MethodSerde() {
+ return internal::MethodInfo<kMethod>::serde();
+}
+
+// Returns the identifier for this particular method.
+//
+// Identifiers are not guaranteed to be unique across services, so this should
+// be paired with a service ID when checking against packets which could target
+// different services.
+template <auto kMethod>
+constexpr MethodId GetMethodId() {
+ return internal::WrapMethodId(internal::MethodInfo<kMethod>::kMethodId);
+}
+
+// Returns the identifier for the service this method belongs to.
+template <auto kMethod>
+constexpr ServiceId GetServiceIdForMethod() {
+ return internal::WrapServiceId(internal::MethodInfo<kMethod>::kServiceId);
+}
+
+} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/packet_meta.h b/pw_rpc/public/pw_rpc/packet_meta.h
new file mode 100644
index 000000000..efa5e5dba
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/packet_meta.h
@@ -0,0 +1,61 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/method_id.h"
+#include "pw_rpc/service_id.h"
+#include "pw_span/span.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::rpc {
+
+// Metadata about a `pw_rpc` packet.
+//
+// For now, this metadata structure only includes a limited set of information
+// about the contents of a packet, but it may be extended in the future.
+class PacketMeta {
+ public:
+ // Parses the metadata from a serialized packet.
+ static Result<PacketMeta> FromBuffer(ConstByteSpan data);
+ constexpr uint32_t channel_id() const { return channel_id_; }
+ constexpr ServiceId service_id() const { return service_id_; }
+ constexpr MethodId method_id() const { return method_id_; }
+ constexpr bool destination_is_client() const {
+ return destination_ == internal::Packet::kClient;
+ }
+ constexpr bool destination_is_server() const {
+ return destination_ == internal::Packet::kServer;
+ }
+ // Note: this `payload` is only valid so long as the original `data` buffer
+ // passed to `PacketMeta::FromBuffer` remains valid.
+ constexpr ConstByteSpan payload() const { return payload_; }
+
+ private:
+ constexpr explicit PacketMeta(const internal::Packet packet)
+ : channel_id_(packet.channel_id()),
+ service_id_(internal::WrapServiceId(packet.service_id())),
+ method_id_(internal::WrapMethodId(packet.method_id())),
+ destination_(packet.destination()),
+ payload_(packet.payload()) {}
+ uint32_t channel_id_;
+ ServiceId service_id_;
+ MethodId method_id_;
+ internal::Packet::Destination destination_;
+ ConstByteSpan payload_;
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/payloads_view.h b/pw_rpc/public/pw_rpc/payloads_view.h
index 3d4934f88..9d003ddca 100644
--- a/pw_rpc/public/pw_rpc/payloads_view.h
+++ b/pw_rpc/public/pw_rpc/payloads_view.h
@@ -39,8 +39,8 @@ class FakeChannelOutput;
class PacketFilter {
public:
// Use Channel::kUnassignedChannelId to ignore the channel.
- constexpr PacketFilter(PacketType packet_type_1,
- PacketType packet_type_2,
+ constexpr PacketFilter(internal::pwpb::PacketType packet_type_1,
+ internal::pwpb::PacketType packet_type_2,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id)
@@ -62,8 +62,8 @@ class PacketFilter {
private:
// Support filtering on two packet types to handle reading both client and
// server streams for bidirectional streams.
- PacketType packet_type_1_;
- PacketType packet_type_2_;
+ internal::pwpb::PacketType packet_type_1_;
+ internal::pwpb::PacketType packet_type_2_;
uint32_t channel_id_;
uint32_t service_id_;
uint32_t method_id_;
@@ -124,10 +124,13 @@ class PayloadsView {
template <typename>
friend class NanopbPayloadsView;
+ template <typename>
+ friend class PwpbPayloadsView;
+
template <auto kMethod>
using MethodInfo = internal::MethodInfo<kMethod>;
- using PacketType = internal::PacketType;
+ using PacketType = internal::pwpb::PacketType;
template <auto kMethod>
static constexpr PayloadsView For(const Vector<internal::Packet>& packets,
@@ -240,7 +243,7 @@ class StatusView {
template <auto kMethod>
using MethodInfo = internal::MethodInfo<kMethod>;
- using PacketType = internal::PacketType;
+ using PacketType = internal::pwpb::PacketType;
constexpr StatusView(const Vector<internal::Packet>& packets,
PacketType packet_type_1,
diff --git a/pw_rpc/public/pw_rpc/server.h b/pw_rpc/public/pw_rpc/server.h
index 69eae07fc..19c9333c5 100644
--- a/pw_rpc/public/pw_rpc/server.h
+++ b/pw_rpc/public/pw_rpc/server.h
@@ -14,11 +14,11 @@
#pragma once
#include <cstddef>
-#include <span>
#include <tuple>
#include "pw_containers/intrusive_list.h"
#include "pw_rpc/channel.h"
+#include "pw_rpc/internal/call.h"
#include "pw_rpc/internal/channel.h"
#include "pw_rpc/internal/endpoint.h"
#include "pw_rpc/internal/lock.h"
@@ -26,13 +26,22 @@
#include "pw_rpc/internal/method_info.h"
#include "pw_rpc/internal/server_call.h"
#include "pw_rpc/service.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::rpc {
class Server : public internal::Endpoint {
public:
- _PW_RPC_CONSTEXPR Server(std::span<Channel> channels) : Endpoint(channels) {}
+ // If dynamic allocation is supported, it is not necessary to preallocate a
+ // channels list.
+#if PW_RPC_DYNAMIC_ALLOCATION
+ _PW_RPC_CONSTEXPR Server() = default;
+#endif // PW_RPC_DYNAMIC_ALLOCATION
+
+ // Creates a client that uses a set of RPC channels. Channels can be shared
+ // between multiple clients and servers.
+ _PW_RPC_CONSTEXPR Server(span<Channel> channels) : Endpoint(channels) {}
// Registers one or more services with the server. This should not be called
// directly with a Service; instead, use a generated class which inherits
@@ -44,7 +53,7 @@ class Server : public internal::Endpoint {
template <typename... OtherServices>
void RegisterService(Service& service, OtherServices&... services)
PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
- internal::LockGuard lock(internal::rpc_lock());
+ internal::RpcLockGuard lock;
services_.push_front(service); // Register the first service
// Register any additional services by expanding the parameter pack. This
@@ -52,6 +61,31 @@ class Server : public internal::Endpoint {
(services_.push_front(services), ...);
}
+ // Returns whether a service is registered.
+ //
+ // Calling RegisterService with a registered service will assert. So depending
+ // on your logic you might want to check if a service is currently registered.
+ bool IsServiceRegistered(const Service& service) const
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ internal::RpcLockGuard lock;
+
+ for (const Service& svc : services_) {
+ if (&svc == &service) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ template <typename... OtherServices>
+ void UnregisterService(Service& service, OtherServices&... services)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ internal::rpc_lock().lock();
+ UnregisterServiceLocked(service, static_cast<Service&>(services)...);
+ CleanUpCalls();
+ }
+
// Processes an RPC packet. The packet may contain an RPC request or a control
// packet, the result of which is processed in this function. Returns whether
// the packet was able to be processed:
@@ -60,24 +94,13 @@ class Server : public internal::Endpoint {
// DATA_LOSS - Failed to decode the packet.
// INVALID_ARGUMENT - The packet is intended for a client, not a server.
// UNAVAILABLE - No RPC channel with the requested ID was found.
- //
- // ProcessPacket optionally accepts a ChannelOutput as a second argument. If
- // provided, the server respond on that interface if an unknown channel is
- // requested.
Status ProcessPacket(ConstByteSpan packet_data)
- PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
- return ProcessPacket(packet_data, nullptr);
- }
- Status ProcessPacket(ConstByteSpan packet_data, ChannelOutput& interface)
- PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
- return ProcessPacket(packet_data, &interface);
- }
+ PW_LOCKS_EXCLUDED(internal::rpc_lock());
private:
friend class internal::Call;
- friend class ClientServer;
- // Give call classes access to OpenContext.
+ // Give call classes access to OpenCall.
friend class RawServerReaderWriter;
friend class RawServerWriter;
friend class RawServerReader;
@@ -92,16 +115,32 @@ class Server : public internal::Endpoint {
template <typename>
friend class NanopbUnaryResponder;
- // Creates a call context for a particular RPC. Unlike the CallContext
- // constructor, this function checks the type of RPC at compile time.
- template <auto kMethod,
+ template <typename, typename>
+ friend class PwpbServerReaderWriter;
+ template <typename>
+ friend class PwpbServerWriter;
+ template <typename, typename>
+ friend class PwpbServerReader;
+ template <typename>
+ friend class PwpbUnaryResponder;
+
+ // Opens a call object for an unrequested RPC. Calls created with OpenCall
+ // use a special call ID and will adopt the call ID from the first packet for
+ // their channel, service, and method. Only one call object may be opened in
+ // this fashion at a time.
+ //
+ // This function checks the type of RPC at compile time.
+ template <typename CallType,
+ auto kMethod,
MethodType kExpected,
typename ServiceImpl,
typename MethodImpl>
- internal::CallContext OpenContext(uint32_t channel_id,
- ServiceImpl& service,
- const MethodImpl& method)
- PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock()) {
+ [[nodiscard]] CallType OpenCall(uint32_t channel_id,
+ ServiceImpl& service,
+ const MethodImpl& method)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ internal::rpc_lock().lock();
+
using Info = internal::MethodInfo<kMethod>;
if constexpr (kExpected == MethodType::kUnary) {
static_assert(
@@ -121,28 +160,36 @@ class Server : public internal::Endpoint {
"streaming RPCs.");
}
- // Unrequested RPCs always use 0 as the call ID. When an actual request is
- // sent, the call will be replaced with its real ID.
- constexpr uint32_t kOpenCallId = 0;
-
- return internal::CallContext(
- *this, channel_id, service, method, kOpenCallId);
+ CallType call(internal::CallContext(
+ *this, channel_id, service, method, internal::kOpenCallId)
+ .ClaimLocked());
+ CleanUpCalls();
+ return call;
}
- Status ProcessPacket(ConstByteSpan packet_data, ChannelOutput* interface)
- PW_LOCKS_EXCLUDED(internal::rpc_lock());
-
std::tuple<Service*, const internal::Method*> FindMethod(
const internal::Packet& packet)
PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock());
void HandleClientStreamPacket(const internal::Packet& packet,
internal::Channel& channel,
- internal::ServerCall* call) const
- PW_UNLOCK_FUNCTION(internal::rpc_lock());
+ IntrusiveList<internal::Call>::iterator call)
+ const PW_UNLOCK_FUNCTION(internal::rpc_lock());
+
+ template <typename... OtherServices>
+ void UnregisterServiceLocked(Service& service, OtherServices&... services)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock()) {
+ services_.remove(service);
+ UnregisterServiceLocked(services...);
+ AbortCallsForService(service);
+ }
+
+ void UnregisterServiceLocked() {} // Base case; nothing left to do.
// Remove these internal::Endpoint functions from the public interface.
using Endpoint::active_call_count;
+ using Endpoint::ClaimLocked;
+ using Endpoint::CleanUpCalls;
using Endpoint::GetInternalChannel;
IntrusiveList<Service> services_ PW_GUARDED_BY(internal::rpc_lock());
diff --git a/pw_rpc/public/pw_rpc/service.h b/pw_rpc/public/pw_rpc/service.h
index a4f2dbffb..5f4776e5a 100644
--- a/pw_rpc/public/pw_rpc/service.h
+++ b/pw_rpc/public/pw_rpc/service.h
@@ -15,12 +15,13 @@
#include <cstdint>
#include <limits>
-#include <span>
#include "pw_containers/intrusive_list.h"
#include "pw_preprocessor/compiler.h"
#include "pw_rpc/internal/method.h"
#include "pw_rpc/internal/method_union.h"
+#include "pw_rpc/service_id.h"
+#include "pw_span/span.h"
namespace pw::rpc {
@@ -38,9 +39,13 @@ class Service : public IntrusiveList<Service>::Item {
Service& operator=(const Service&) = delete;
Service& operator=(Service&&) = delete;
- uint32_t id() const { return id_; }
+ ServiceId service_id() const { return internal::WrapServiceId(id_); }
protected:
+ // Note: despite being non-`::internal` and non-`private`, this constructor
+ // is not considered part of pigweed's public API: calling it requires
+ // constructing `methods`, which must have a `.data()` accessor which returns
+ // a `const internal::MethodUnion*`.
template <typename T, size_t kMethodCount>
constexpr Service(uint32_t id, const std::array<T, kMethodCount>& methods)
: id_(id),
@@ -55,6 +60,9 @@ class Service : public IntrusiveList<Service>::Item {
}
// For use by tests with only one method.
+ //
+ // Note: This constructor is not for direct use outside of pigweed, and
+ // is not considered part of the public API.
template <typename T>
constexpr Service(uint32_t id, const T& method)
: id_(id), methods_(&method), method_size_(sizeof(T)), method_count_(1) {}
diff --git a/pw_rpc/public/pw_rpc/service_id.h b/pw_rpc/public/pw_rpc/service_id.h
new file mode 100644
index 000000000..0bbd151c3
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/service_id.h
@@ -0,0 +1,80 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+#include <functional>
+
+// NOTE: These wrappers exist in order to provide future compatibility for
+// different internal representations of service and method identifiers.
+
+namespace pw::rpc {
+
+class ServiceId;
+
+namespace internal {
+constexpr ServiceId WrapServiceId(uint32_t id);
+constexpr uint32_t UnwrapServiceId(ServiceId id);
+} // namespace internal
+
+// An identifier for a service.
+//
+// Note: this does not identify instances of a service (Servers), only the
+// service itself.
+class ServiceId {
+ private:
+ constexpr explicit ServiceId(uint32_t id) : id_(id) {}
+ friend constexpr ServiceId internal::WrapServiceId(uint32_t id);
+ friend constexpr uint32_t internal::UnwrapServiceId(ServiceId id);
+ uint32_t id_;
+};
+
+constexpr bool operator==(ServiceId lhs, ServiceId rhs) {
+ return internal::UnwrapServiceId(lhs) == internal::UnwrapServiceId(rhs);
+}
+
+constexpr bool operator!=(ServiceId lhs, ServiceId rhs) {
+ return !(lhs == rhs);
+}
+
+// Comparisons are provided to enable sorting by `ServiceId`.
+
+constexpr bool operator<(ServiceId lhs, ServiceId rhs) {
+ return internal::UnwrapServiceId(lhs) < internal::UnwrapServiceId(rhs);
+}
+
+constexpr bool operator>(ServiceId lhs, ServiceId rhs) { return rhs < lhs; }
+
+constexpr bool operator<=(ServiceId lhs, ServiceId rhs) { return !(lhs > rhs); }
+
+constexpr bool operator>=(ServiceId lhs, ServiceId rhs) { return !(lhs < rhs); }
+
+namespace internal {
+
+constexpr ServiceId WrapServiceId(uint32_t id) { return ServiceId(id); }
+constexpr uint32_t UnwrapServiceId(ServiceId id) { return id.id_; }
+
+} // namespace internal
+} // namespace pw::rpc
+
+namespace std {
+
+template <>
+struct hash<pw::rpc::ServiceId> {
+ size_t operator()(const pw::rpc::ServiceId& id) const {
+ return hash<uint32_t>{}(::pw::rpc::internal::UnwrapServiceId(id));
+ }
+};
+
+} // namespace std
diff --git a/pw_rpc/public/pw_rpc/synchronous_call.h b/pw_rpc/public/pw_rpc/synchronous_call.h
new file mode 100644
index 000000000..5dc58b1c8
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/synchronous_call.h
@@ -0,0 +1,295 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <utility>
+
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc/client.h"
+#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/synchronous_call_result.h"
+#include "pw_sync/timed_thread_notification.h"
+
+// Synchronous Call wrappers
+//
+// Wraps an asynchronous RPC client call, converting it to a synchronous
+// interface.
+//
+// WARNING! This should not be called from any context that cannot be blocked!
+// This method will block the calling thread until the RPC completes, and
+// translate the response into a pw::rpc::SynchronousCallResult that contains
+// the error type and status or the proto response.
+//
+// Example:
+//
+// pw_rpc_EchoMessage request{.msg = "hello" };
+// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
+// pw::rpc::SynchronousCall<EchoService::Echo>(rpc_client,
+// channel_id,
+// request);
+// if (result.ok()) {
+// printf("%s", result.response().msg);
+// }
+//
+// Note: The above example will block indefinitely. If you'd like to include a
+// timeout for how long the call should block for, use the
+// `SynchronousCallFor()` or `SynchronousCallUntil()` variants.
+//
+// Additionally, the use of a generated Client object is supported:
+//
+// pw_rpc::nanopb::EchoService::client client;
+// pw_rpc_EchoMessage request{.msg = "hello" };
+// pw::rpc::SynchronousCallResult<pw_rpc_EchoMessage> result =
+// pw::rpc::SynchronousCall<EchoService::Echo>(client, request);
+//
+// if (result.ok()) {
+// printf("%s", result.response().msg);
+// }
+
+namespace pw::rpc {
+namespace internal {
+
+template <typename Response>
+struct SynchronousCallState {
+ auto OnCompletedCallback() {
+ return [this](const Response& response, Status status) {
+ result = SynchronousCallResult<Response>(status, response);
+ notify.release();
+ };
+ }
+
+ auto OnRpcErrorCallback() {
+ return [this](Status status) {
+ result = SynchronousCallResult<Response>::RpcError(status);
+ notify.release();
+ };
+ }
+
+ SynchronousCallResult<Response> result;
+ sync::TimedThreadNotification notify;
+};
+
+} // namespace internal
+
+// SynchronousCall
+//
+// Template arguments:
+// kRpcMethod: The RPC Method to invoke
+//
+// Arguments:
+// client: The pw::rpc::Client to use for the call
+// channel_id: The ID of the RPC channel to make the call on
+// request: The proto struct to send as the request
+template <auto kRpcMethod>
+SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
+SynchronousCall(
+ Client& client,
+ uint32_t channel_id,
+ const typename internal::MethodInfo<kRpcMethod>::Request& request) {
+ using Info = internal::MethodInfo<kRpcMethod>;
+ using Response = typename Info::Response;
+ static_assert(Info::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ internal::SynchronousCallState<Response> call_state;
+
+ auto call = kRpcMethod(client,
+ channel_id,
+ request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+
+ call_state.notify.acquire();
+
+ return std::move(call_state.result);
+}
+
+// SynchronousCall
+//
+// Template arguments:
+// kRpcMethod: The RPC Method to invoke
+//
+// Arguments:
+// client: The service Client to use for the call
+// request: The proto struct to send as the request
+template <auto kRpcMethod>
+SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
+SynchronousCall(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ const typename internal::MethodInfo<kRpcMethod>::Request& request) {
+ using Info = internal::MethodInfo<kRpcMethod>;
+ using Response = typename Info::Response;
+ static_assert(Info::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ constexpr auto Function =
+ Info::template Function<typename Info::GeneratedClient>();
+
+ internal::SynchronousCallState<Response> call_state;
+
+ auto call = (client.*Function)(request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+
+ call_state.notify.acquire();
+
+ return std::move(call_state.result);
+}
+
+// SynchronousCallFor
+//
+// Template arguments:
+// kRpcMethod: The RPC Method to invoke
+//
+// Arguments:
+// client: The pw::rpc::Client to use for the call
+// channel_id: The ID of the RPC channel to make the call on
+// request: The proto struct to send as the request
+// timeout: Duration to block for before returning with Timeout
+template <auto kRpcMethod>
+SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
+SynchronousCallFor(
+ Client& client,
+ uint32_t channel_id,
+ const typename internal::MethodInfo<kRpcMethod>::Request& request,
+ chrono::SystemClock::duration timeout) {
+ using Info = internal::MethodInfo<kRpcMethod>;
+ using Response = typename Info::Response;
+ static_assert(Info::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ internal::SynchronousCallState<Response> call_state;
+
+ auto call = kRpcMethod(client,
+ channel_id,
+ request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+
+ if (!call_state.notify.try_acquire_for(timeout)) {
+ return SynchronousCallResult<Response>::Timeout();
+ }
+
+ return std::move(call_state.result);
+}
+
+// SynchronousCallFor
+//
+// Template arguments:
+// kRpcMethod: The RPC Method to invoke
+//
+// Arguments:
+// client: The service Client to use for the call
+// request: The proto struct to send as the request
+// timeout: Duration to block for before returning with Timeout
+template <auto kRpcMethod>
+SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
+SynchronousCallFor(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ const typename internal::MethodInfo<kRpcMethod>::Request& request,
+ chrono::SystemClock::duration timeout) {
+ using Info = internal::MethodInfo<kRpcMethod>;
+ using Response = typename Info::Response;
+ static_assert(Info::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ constexpr auto Function =
+ Info::template Function<typename Info::GeneratedClient>();
+
+ internal::SynchronousCallState<Response> call_state;
+
+ auto call = (client.*Function)(request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+
+ if (!call_state.notify.try_acquire_for(timeout)) {
+ return SynchronousCallResult<Response>::Timeout();
+ }
+
+ return std::move(call_state.result);
+}
+
+// SynchronousCallUntil
+//
+// Template arguments:
+// kRpcMethod: The RPC Method to invoke
+//
+// Arguments:
+// client: The pw::rpc::Client to use for the call
+// channel_id: The ID of the RPC channel to make the call on
+// request: The proto struct to send as the request
+// deadline: Timepoint to block until before returning with Timeout
+template <auto kRpcMethod>
+SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
+SynchronousCallUntil(
+ Client& client,
+ uint32_t channel_id,
+ const typename internal::MethodInfo<kRpcMethod>::Request& request,
+ chrono::SystemClock::time_point deadline) {
+ using Info = internal::MethodInfo<kRpcMethod>;
+ using Response = typename Info::Response;
+ static_assert(Info::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ internal::SynchronousCallState<Response> call_state;
+
+ auto call = kRpcMethod(client,
+ channel_id,
+ request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+
+ if (!call_state.notify.try_acquire_until(deadline)) {
+ return SynchronousCallResult<Response>::Timeout();
+ }
+
+ return std::move(call_state.result);
+}
+
+// SynchronousCallUntil
+//
+// Template arguments:
+// kRpcMethod: The RPC Method to invoke
+//
+// Arguments:
+// client: The service Client to use for the call
+// request: The proto struct to send as the request
+// deadline: Timepoint to block until before returning with Timeout
+template <auto kRpcMethod>
+SynchronousCallResult<typename internal::MethodInfo<kRpcMethod>::Response>
+SynchronousCallUntil(
+ const typename internal::MethodInfo<kRpcMethod>::GeneratedClient& client,
+ const typename internal::MethodInfo<kRpcMethod>::Request& request,
+ chrono::SystemClock::time_point deadline) {
+ using Info = internal::MethodInfo<kRpcMethod>;
+ using Response = typename Info::Response;
+ static_assert(Info::kType == MethodType::kUnary,
+ "Only unary methods can be used with synchronous calls");
+
+ constexpr auto Function =
+ Info::template Function<typename Info::GeneratedClient>();
+
+ internal::SynchronousCallState<Response> call_state;
+
+ auto call = (client.*Function)(request,
+ call_state.OnCompletedCallback(),
+ call_state.OnRpcErrorCallback());
+
+ if (!call_state.notify.try_acquire_until(deadline)) {
+ return SynchronousCallResult<Response>::Timeout();
+ }
+
+ return std::move(call_state.result);
+}
+} // namespace pw::rpc
diff --git a/pw_rpc/public/pw_rpc/synchronous_call_result.h b/pw_rpc/public/pw_rpc/synchronous_call_result.h
new file mode 100644
index 000000000..9b4f5e0fb
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/synchronous_call_result.h
@@ -0,0 +1,271 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert/assert.h"
+#include "pw_status/status.h"
+
+namespace pw {
+namespace rpc {
+
+// A `SynchronousCallResult<Response>` is an object that contains the result of
+// a `SynchronousCall`. When synchronous calls are made, errors could occur for
+// multiple reasons. There could have been an error at either the RPC layer or
+// Server; or the client-specified timeout might have occurred. A user can query
+// this object to determine what type of error occurred, so that they can handle
+// it appropriately. If the server responded, the response() and dereference
+// operators will provide access to the Response.
+//
+// Example:
+//
+// SynchronousCallResult<MyResponse> result =
+// SynchronousCallFor<MyService::Method>(client, request, timeout);
+// if (result.is_rpc_error()) {
+// ShutdownClient(client);
+// } else if (result.is_timeout()) {
+// RetryCall(client, request);
+// } else if (result.is_server_error()) {
+// return result.status();
+// }
+// PW_ASSERT(result.ok());
+// return std::move(result).response();
+//
+// For some RPCs, the server could have responded with a non-Ok Status but with
+// a valid Response object. For example, if the server was ran out of space in a
+// buffer, it might return a Status of ResourceExhausted, but the response
+// contains as much data as could fit. In this situation, users should be
+// careful not to treat the error as fatal.
+//
+// Example:
+//
+// SynchronousCallResult<BufferResponse> result =
+// SynchronousCall<MyService::Read>(client, request);
+// if (result.is_rpc_error()) {
+// ShutdownClient(client);
+// }
+// PW_ASSERT(result.is_server_response());
+// HandleServerResponse(result.status(), result.response());
+//
+
+namespace internal {
+enum class SynchronousCallStatus {
+ kInvalid,
+ kTimeout,
+ kRpc,
+ kServer,
+};
+} // namespace internal
+
+template <typename Response>
+class SynchronousCallResult {
+ public:
+ // Error Constructors
+ constexpr static SynchronousCallResult Timeout();
+ constexpr static SynchronousCallResult RpcError(Status status);
+
+ // Server Response Constructor
+ constexpr explicit SynchronousCallResult(Status status, Response response)
+ : call_status_(internal::SynchronousCallStatus::kServer),
+ status_(status),
+ response_(std::move(response)) {}
+
+ constexpr SynchronousCallResult() = default;
+ ~SynchronousCallResult() = default;
+
+ // Copyable if `Response` is copyable.
+ constexpr SynchronousCallResult(const SynchronousCallResult&) = default;
+ constexpr SynchronousCallResult& operator=(const SynchronousCallResult&) =
+ default;
+
+ // Movable if `Response` is movable.
+ constexpr SynchronousCallResult(SynchronousCallResult&&) = default;
+ constexpr SynchronousCallResult& operator=(SynchronousCallResult&&) = default;
+
+ // Returns true if there was a timeout, an rpc error or the server returned a
+ // non-Ok status.
+ [[nodiscard]] constexpr bool is_error() const;
+
+ // Returns true if the server returned a response with an Ok status.
+ [[nodiscard]] constexpr bool ok() const;
+
+ // Returns true if the server responded with a non-Ok status.
+ [[nodiscard]] constexpr bool is_server_error() const;
+
+ [[nodiscard]] constexpr bool is_timeout() const;
+ [[nodiscard]] constexpr bool is_rpc_error() const;
+ [[nodiscard]] constexpr bool is_server_response() const;
+
+ [[nodiscard]] constexpr Status status() const;
+
+ // SynchronousCallResult<Response>::response()
+ // SynchronousCallResult<Response>::operator*()
+ // SynchronousCallResult<Response>::operator->()
+ //
+ // Accessors to the held value if `this->is_server_response()`. Otherwise,
+ // terminates the process.
+ constexpr const Response& response() const&;
+ constexpr Response& response() &;
+ constexpr const Response&& response() const&&;
+ constexpr Response&& response() &&;
+
+ constexpr const Response& operator*() const&;
+ constexpr Response& operator*() &;
+ constexpr const Response&& operator*() const&&;
+ constexpr Response&& operator*() &&;
+
+ constexpr const Response* operator->() const;
+ constexpr Response* operator->();
+
+ private:
+ // This constructor is private to protect against invariants that might occur
+ // when constructing with a SynchronousCallStatus.
+ constexpr explicit SynchronousCallResult(
+ internal::SynchronousCallStatus call_status, Status status)
+ : call_status_(call_status), status_(status) {}
+
+ internal::SynchronousCallStatus call_status_ =
+ internal::SynchronousCallStatus::kInvalid;
+ Status status_{};
+ Response response_{};
+};
+
+// Implementations
+
+template <typename Response>
+constexpr SynchronousCallResult<Response>
+SynchronousCallResult<Response>::Timeout() {
+ return SynchronousCallResult(internal::SynchronousCallStatus::kTimeout,
+ Status::DeadlineExceeded());
+}
+
+template <typename Response>
+constexpr SynchronousCallResult<Response>
+SynchronousCallResult<Response>::RpcError(Status status) {
+ return SynchronousCallResult(internal::SynchronousCallStatus::kRpc, status);
+}
+
+template <typename Response>
+constexpr bool SynchronousCallResult<Response>::is_error() const {
+ return !ok();
+}
+
+template <typename Response>
+constexpr bool SynchronousCallResult<Response>::ok() const {
+ return is_server_response() && status_.ok();
+}
+
+template <typename Response>
+constexpr bool SynchronousCallResult<Response>::is_server_error() const {
+ return is_server_response() && !status_.ok();
+}
+
+template <typename Response>
+constexpr bool SynchronousCallResult<Response>::is_timeout() const {
+ return call_status_ == internal::SynchronousCallStatus::kTimeout;
+}
+
+template <typename Response>
+constexpr bool SynchronousCallResult<Response>::is_rpc_error() const {
+ return call_status_ == internal::SynchronousCallStatus::kRpc;
+}
+
+template <typename Response>
+constexpr bool SynchronousCallResult<Response>::is_server_response() const {
+ return call_status_ == internal::SynchronousCallStatus::kServer;
+}
+
+template <typename Response>
+constexpr Status SynchronousCallResult<Response>::status() const {
+ PW_ASSERT(call_status_ != internal::SynchronousCallStatus::kInvalid);
+ return status_;
+}
+
+template <typename Response>
+constexpr const Response& SynchronousCallResult<Response>::response() const& {
+ PW_ASSERT(is_server_response());
+ return response_;
+}
+
+template <typename Response>
+constexpr Response& SynchronousCallResult<Response>::response() & {
+ PW_ASSERT(is_server_response());
+ return response_;
+}
+
+template <typename Response>
+constexpr const Response&& SynchronousCallResult<Response>::response() const&& {
+ PW_ASSERT(is_server_response());
+ return std::move(response_);
+}
+
+template <typename Response>
+constexpr Response&& SynchronousCallResult<Response>::response() && {
+ PW_ASSERT(is_server_response());
+ return std::move(response_);
+}
+
+template <typename Response>
+constexpr const Response& SynchronousCallResult<Response>::operator*() const& {
+ PW_ASSERT(is_server_response());
+ return response_;
+}
+
+template <typename Response>
+constexpr Response& SynchronousCallResult<Response>::operator*() & {
+ PW_ASSERT(is_server_response());
+ return response_;
+}
+
+template <typename Response>
+constexpr const Response&& SynchronousCallResult<Response>::operator*()
+ const&& {
+ PW_ASSERT(is_server_response());
+ return std::move(response_);
+}
+
+template <typename Response>
+constexpr Response&& SynchronousCallResult<Response>::operator*() && {
+ PW_ASSERT(is_server_response());
+ return std::move(response_);
+}
+
+template <typename Response>
+constexpr const Response* SynchronousCallResult<Response>::operator->() const {
+ PW_ASSERT(is_server_response());
+ return &response_;
+}
+
+template <typename Response>
+constexpr Response* SynchronousCallResult<Response>::operator->() {
+ PW_ASSERT(is_server_response());
+ return &response_;
+}
+
+} // namespace rpc
+
+// Conversion functions for usage with PW_TRY and PW_TRY_ASSIGN.
+namespace internal {
+
+template <typename T>
+constexpr Status ConvertToStatus(const rpc::SynchronousCallResult<T>& result) {
+ return result.status();
+}
+
+template <typename T>
+constexpr T ConvertToValue(rpc::SynchronousCallResult<T>& result) {
+ return std::move(result.response());
+}
+
+} // namespace internal
+} // namespace pw
diff --git a/pw_rpc/public/pw_rpc/test_helpers.h b/pw_rpc/public/pw_rpc/test_helpers.h
new file mode 100644
index 000000000..487bc5374
--- /dev/null
+++ b/pw_rpc/public/pw_rpc/test_helpers.h
@@ -0,0 +1,104 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <chrono>
+
+#include "pw_assert/assert.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc/internal/fake_channel_output.h"
+#include "pw_rpc/method_info.h"
+#include "pw_status/status.h"
+#include "pw_sync/counting_semaphore.h"
+#include "pw_thread/yield.h"
+
+namespace pw::rpc::test {
+
+// Wait until the provided RawFakeChannelOutput, NanopbFakeChannelOutput or
+// PwpbFakeChannelOutput receives the specified number of packets.
+template <unsigned kTimeoutSeconds = 10, typename Function>
+void WaitForPackets(internal::test::FakeChannelOutput& output,
+ int count,
+ Function&& run_before) {
+ sync::CountingSemaphore sem;
+ output.set_on_send([&sem](ConstByteSpan, Status) { sem.release(); });
+
+ run_before();
+
+ for (int i = 0; i < count; ++i) {
+ PW_ASSERT(sem.try_acquire_for(std::chrono::seconds(kTimeoutSeconds)));
+ }
+
+ output.set_on_send(nullptr);
+}
+
+// Checks that kMethod was called in client_context (which is a
+// PwpbClientTestContext or a NanopbClientTestContext) and sends the response
+// and status back.
+//
+// If no kMethod was called in timeout duration - returns
+// DEADLINE_EXCEEDED. Otherwise returns OK.
+//
+// Example: Let's say we are testing an RPC service (my::MyService) that as part
+// of the call (GetData) handling does another RPC call to a different service
+// (other::OtherService::GetPart). my::MyService constructor accepts the
+// other::OtherService::Client as an argument. To be able to test it we need to
+// provide a prepared response when request is sent.
+//
+// pw::rpc::PwpbClientTestContext client_context;
+// other::pw_rpc::pwpb::OtherService::Client other_service_client(
+// client_context.client(), client_context.channel().id());
+//
+// PW_PWPB_TEST_METHOD_CONTEXT(MyService, GetData)
+// context(other_service_client);
+// context.call({});
+//
+// ASSERT_EQ(pw::rpc::test::SendResponseIfCalled<
+// other::pw_rpc::pwpb::OtherService::GetPart>(client_context,
+// {.value = 42}),
+// pw::OkStatus());
+//
+// // At this point we have GetData handler received the response for GetPart.
+template <auto kMethod, typename Context>
+Status SendResponseIfCalled(
+ Context& client_context,
+ const MethodResponseType<kMethod>& response,
+ Status status = OkStatus(),
+ chrono::SystemClock::duration timeout =
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(100))) {
+ const auto start_time = chrono::SystemClock::now();
+ while (chrono::SystemClock::now() - start_time < timeout) {
+ const auto count =
+ client_context.output().template total_payloads<kMethod>();
+ if (count > 0) {
+ client_context.server().template SendResponse<kMethod>(response, status);
+ return OkStatus();
+ }
+ this_thread::yield();
+ }
+ return Status::DeadlineExceeded();
+}
+
+// Shortcut for SendResponseIfCalled(client_context, {}, status, timeout).
+template <auto kMethod, typename Context>
+Status SendResponseIfCalled(
+ Context& client_context,
+ Status status = OkStatus(),
+ chrono::SystemClock::duration timeout =
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(100))) {
+ return SendResponseIfCalled<kMethod, Context>(
+ client_context, {}, status, timeout);
+}
+
+} // namespace pw::rpc::test
diff --git a/pw_rpc/public/pw_rpc/thread_testing.h b/pw_rpc/public/pw_rpc/thread_testing.h
index eb723a2cf..e19949bde 100644
--- a/pw_rpc/public/pw_rpc/thread_testing.h
+++ b/pw_rpc/public/pw_rpc/thread_testing.h
@@ -13,30 +13,7 @@
// the License.
#pragma once
-#include <chrono>
+// This file is kept for backward compatibility. New code should use
+// "pw_rpc/test_helpers.h" instead.
-#include "pw_assert/assert.h"
-#include "pw_rpc/internal/fake_channel_output.h"
-#include "pw_sync/counting_semaphore.h"
-
-namespace pw::rpc::test {
-
-// Wait until the provided RawFakeChannelOutput or NanopbFakeChannelOutput
-// receives the specified number of packets.
-template <unsigned kTimeoutSeconds = 10, typename Function>
-void WaitForPackets(internal::test::FakeChannelOutput& output,
- int count,
- Function&& run_before) {
- sync::CountingSemaphore sem;
- output.set_on_send([&sem](ConstByteSpan, Status) { sem.release(); });
-
- run_before();
-
- for (int i = 0; i < count; ++i) {
- PW_ASSERT(sem.try_acquire_for(std::chrono::seconds(kTimeoutSeconds)));
- }
-
- output.set_on_send(nullptr);
-}
-
-} // namespace pw::rpc::test
+#include "pw_rpc/test_helpers.h"
diff --git a/pw_rpc/pw_rpc_private/fake_server_reader_writer.h b/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
index 44c359b2f..89c5d91c3 100644
--- a/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
+++ b/pw_rpc/pw_rpc_private/fake_server_reader_writer.h
@@ -36,19 +36,24 @@ namespace pw::rpc::internal::test {
//
// Call's public API is intended for rpc::Server, so hide the public methods
// with private inheritance.
-class FakeServerReaderWriter : private internal::ServerCall {
+class FakeServerReaderWriter : private ServerCall {
public:
constexpr FakeServerReaderWriter() = default;
// On a real reader/writer, this constructor would not be exposed.
- FakeServerReaderWriter(const CallContext& context,
+ FakeServerReaderWriter(const LockedCallContext& context,
MethodType type = MethodType::kBidirectionalStreaming)
- : ServerCall(context, type) {}
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : ServerCall(context, CallProperties(type, kServerCall, kRawProto)) {}
FakeServerReaderWriter(FakeServerReaderWriter&&) = default;
FakeServerReaderWriter& operator=(FakeServerReaderWriter&&) = default;
// Pull in protected functions from the hidden Call base as needed.
+ //
+ // Note: these functions all acquire `rpc_lock()`. However, the
+ // `PW_LOCKS_EXCLUDED(rpc_lock())` on their original definitions does not
+ // appear to carry through here.
using Call::active;
using Call::set_on_error;
using Call::set_on_next;
@@ -62,15 +67,20 @@ class FakeServerReaderWriter : private internal::ServerCall {
// Expose a few additional methods for test use.
ServerCall& as_server_call() { return *this; }
+ using Call::channel_id_locked;
+ using Call::id;
+ using Call::set_id;
};
class FakeServerWriter : private FakeServerReaderWriter {
public:
constexpr FakeServerWriter() = default;
- FakeServerWriter(const CallContext& context)
+ FakeServerWriter(const LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: FakeServerReaderWriter(context, MethodType::kServerStreaming) {}
FakeServerWriter(FakeServerWriter&&) = default;
+ FakeServerWriter& operator=(FakeServerWriter&&) = default;
// Common reader/writer functions.
using FakeServerReaderWriter::active;
@@ -86,10 +96,12 @@ class FakeServerReader : private FakeServerReaderWriter {
public:
constexpr FakeServerReader() = default;
- FakeServerReader(const CallContext& context)
+ FakeServerReader(const LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
: FakeServerReaderWriter(context, MethodType::kClientStreaming) {}
FakeServerReader(FakeServerReader&&) = default;
+ FakeServerReader& operator=(FakeServerReader&&) = default;
using FakeServerReaderWriter::active;
using FakeServerReaderWriter::as_server_call;
diff --git a/pw_rpc/pw_rpc_test_protos/no_package.proto b/pw_rpc/pw_rpc_test_protos/no_package.proto
new file mode 100644
index 000000000..92fca61b6
--- /dev/null
+++ b/pw_rpc/pw_rpc_test_protos/no_package.proto
@@ -0,0 +1,44 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+syntax = "proto3";
+
+// No package
+
+message PwRpcTestRequest {
+ int64 integer = 1;
+ uint32 status_code = 2;
+}
+
+message PwRpcTestResponse {
+ int32 value = 1;
+ repeated uint32 repeated_field = 2;
+}
+
+message PwRpcTestStreamResponse {
+ bytes chunk = 1;
+ uint32 number = 2;
+}
+
+message PwRpcEmpty {}
+
+service PwRpcTestService {
+ rpc TestUnaryRpc(PwRpcTestRequest) returns (PwRpcTestResponse);
+ rpc TestAnotherUnaryRpc(PwRpcTestRequest) returns (PwRpcTestResponse);
+ rpc TestServerStreamRpc(PwRpcTestRequest)
+ returns (stream PwRpcTestStreamResponse);
+ rpc TestClientStreamRpc(stream PwRpcTestRequest)
+ returns (PwRpcTestStreamResponse);
+ rpc TestBidirectionalStreamRpc(stream PwRpcTestRequest)
+ returns (stream PwRpcTestStreamResponse);
+}
diff --git a/pw_rpc/pwpb/BUILD.bazel b/pw_rpc/pwpb/BUILD.bazel
new file mode 100644
index 000000000..786b03cd0
--- /dev/null
+++ b/pw_rpc/pwpb/BUILD.bazel
@@ -0,0 +1,333 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+load(
+ "//pw_build:selects.bzl",
+ "TARGET_COMPATIBLE_WITH_HOST_SELECT",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "server_api",
+ srcs = [
+ "server_reader_writer.cc",
+ ],
+ hdrs = [
+ "public/pw_rpc/pwpb/internal/method.h",
+ "public/pw_rpc/pwpb/internal/method_union.h",
+ "public/pw_rpc/pwpb/server_reader_writer.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":common",
+ "//pw_rpc/raw:server_api",
+ ],
+)
+
+pw_cc_library(
+ name = "client_api",
+ hdrs = [
+ "public/pw_rpc/pwpb/client_reader_writer.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":common",
+ ],
+)
+
+pw_cc_library(
+ name = "common",
+ hdrs = [
+ "public/pw_rpc/pwpb/internal/common.h",
+ "public/pw_rpc/pwpb/serde.h",
+ "public/pw_rpc/pwpb/server_reader_writer.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_rpc",
+ ],
+)
+
+pw_cc_library(
+ name = "test_method_context",
+ hdrs = [
+ "public/pw_rpc/pwpb/fake_channel_output.h",
+ "public/pw_rpc/pwpb/test_method_context.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":common",
+ ":server_api",
+ "//pw_assert",
+ "//pw_containers",
+ "//pw_rpc:internal_test_utils",
+ "//pw_span",
+ ],
+)
+
+pw_cc_library(
+ name = "client_testing",
+ hdrs = [
+ "public/pw_rpc/pwpb/client_testing.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc",
+ "//pw_rpc/raw:client_testing",
+ ],
+)
+
+pw_cc_library(
+ name = "client_server_testing",
+ hdrs = [
+ "public/pw_rpc/pwpb/client_server_testing.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:client_server_testing",
+ ],
+)
+
+pw_cc_library(
+ name = "client_server_testing_threaded",
+ hdrs = [
+ "public/pw_rpc/pwpb/client_server_testing_threaded.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:client_server_testing_threaded",
+ ],
+)
+
+pw_cc_library(
+ name = "internal_test_utils",
+ hdrs = ["pw_rpc_pwpb_private/internal_test_utils.h"],
+ deps = ["//pw_rpc:internal_test_utils"],
+)
+
+pw_cc_library(
+ name = "echo_service",
+ hdrs = ["public/pw_rpc/echo_service_pwpb.h"],
+ deps = [
+ "//pw_rpc:echo_cc.pwpb_rpc",
+ ],
+)
+
+# TODO(b/242059613): Enable this library when logging_event_handler can be used.
+filegroup(
+ name = "client_integration_test",
+ srcs = [
+ "client_integration_test.cc",
+ ],
+ #deps = [
+ # "//pw_rpc:integration_testing",
+ # "//pw_sync:binary_semaphore",
+ # "//pw_rpc:benchmark_cc.pwpb_rpc",
+ #]
+)
+
+pw_cc_test(
+ name = "client_call_test",
+ srcs = [
+ "client_call_test.cc",
+ ],
+ deps = [
+ ":client_api",
+ ":internal_test_utils",
+ "//pw_rpc",
+ "//pw_rpc:pw_rpc_test_cc.pwpb",
+ ],
+)
+
+pw_cc_test(
+ name = "client_reader_writer_test",
+ srcs = [
+ "client_reader_writer_test.cc",
+ ],
+ deps = [
+ ":client_api",
+ ":client_testing",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "client_server_context_test",
+ srcs = [
+ "client_server_context_test.cc",
+ ],
+ deps = [
+ ":client_api",
+ ":client_server_testing",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "client_server_context_threaded_test",
+ srcs = [
+ "client_server_context_threaded_test.cc",
+ ],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":client_api",
+ ":client_server_testing_threaded",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ "//pw_sync:binary_semaphore",
+ "//pw_thread:test_threads_header",
+ "//pw_thread_stl:test_threads",
+ ],
+)
+
+pw_cc_test(
+ name = "codegen_test",
+ srcs = [
+ "codegen_test.cc",
+ ],
+ deps = [
+ ":internal_test_utils",
+ ":test_method_context",
+ "//pw_preprocessor",
+ "//pw_rpc:internal_test_utils",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "fake_channel_output_test",
+ srcs = ["fake_channel_output_test.cc"],
+ deps = [
+ ":common",
+ ":server_api",
+ ":test_method_context",
+ "//pw_rpc:internal_test_utils",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "method_test",
+ srcs = ["method_test.cc"],
+ deps = [
+ ":internal_test_utils",
+ ":server_api",
+ "//pw_containers",
+ "//pw_rpc",
+ "//pw_rpc:internal_test_utils",
+ "//pw_rpc:pw_rpc_test_cc.pwpb",
+ ],
+)
+
+pw_cc_test(
+ name = "method_info_test",
+ srcs = ["method_info_test.cc"],
+ deps = [
+ "//pw_rpc",
+ "//pw_rpc:internal_test_utils",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "method_lookup_test",
+ srcs = ["method_lookup_test.cc"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ "//pw_rpc/raw:test_method_context",
+ ],
+)
+
+pw_cc_test(
+ name = "method_union_test",
+ srcs = ["method_union_test.cc"],
+ deps = [
+ ":internal_test_utils",
+ ":server_api",
+ "//pw_rpc:internal_test_utils",
+ "//pw_rpc:pw_rpc_test_cc.pwpb",
+ ],
+)
+
+# TODO(b/234874064): Requires pwpb options file support to compile.
+filegroup(
+ name = "echo_service_test",
+ srcs = ["echo_service_test.cc"],
+ # deps = [
+ # ":echo_service",
+ # ":test_method_context",
+ # ],
+)
+
+pw_cc_test(
+ name = "server_reader_writer_test",
+ srcs = ["server_reader_writer_test.cc"],
+ deps = [
+ ":server_api",
+ ":test_method_context",
+ "//pw_rpc",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "serde_test",
+ srcs = ["serde_test.cc"],
+ deps = [
+ ":common",
+ "//pw_rpc:pw_rpc_test_cc.pwpb",
+ ],
+)
+
+pw_cc_test(
+ name = "server_callback_test",
+ srcs = ["server_callback_test.cc"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "stub_generation_test",
+ srcs = ["stub_generation_test.cc"],
+ deps = [
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ ],
+)
+
+pw_cc_test(
+ name = "synchronous_call_test",
+ srcs = ["synchronous_call_test.cc"],
+ deps = [
+ ":test_method_context",
+ "//pw_rpc:pw_rpc_test_cc.pwpb_rpc",
+ "//pw_rpc:synchronous_client_api",
+ "//pw_work_queue",
+ "//pw_work_queue:stl_test_thread",
+ "//pw_work_queue:test_thread_header",
+ ],
+)
diff --git a/pw_rpc/pwpb/BUILD.gn b/pw_rpc/pwpb/BUILD.gn
new file mode 100644
index 000000000..0c611bb99
--- /dev/null
+++ b/pw_rpc/pwpb/BUILD.gn
@@ -0,0 +1,329 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
+
+config("public") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("server_api") {
+ public_configs = [ ":public" ]
+ public = [
+ "public/pw_rpc/pwpb/internal/method.h",
+ "public/pw_rpc/pwpb/internal/method_union.h",
+ "public/pw_rpc/pwpb/server_reader_writer.h",
+ ]
+ sources = [ "server_reader_writer.cc" ]
+ public_deps = [
+ ":common",
+ "$dir_pw_rpc/raw:server_api",
+ "..:config",
+ "..:server",
+ dir_pw_bytes,
+ ]
+ deps = [
+ "..:log_config",
+ dir_pw_log,
+ ]
+ allow_circular_includes_from = [ ":common" ]
+}
+
+pw_source_set("client_api") {
+ public_configs = [ ":public" ]
+ public_deps = [
+ ":common",
+ "..:client",
+ dir_pw_function,
+ ]
+ public = [ "public/pw_rpc/pwpb/client_reader_writer.h" ]
+}
+
+pw_source_set("common") {
+ public_deps = [
+ "..:common",
+ dir_pw_bytes,
+ dir_pw_span,
+ ]
+ public_configs = [ ":public" ]
+ deps = [
+ "..:client",
+ "..:log_config",
+ "..:server",
+ dir_pw_log,
+ dir_pw_stream,
+ ]
+ public = [
+ "public/pw_rpc/pwpb/internal/common.h",
+ "public/pw_rpc/pwpb/serde.h",
+ ]
+}
+
+pw_source_set("test_method_context") {
+ public_configs = [ ":public" ]
+ public = [
+ "public/pw_rpc/pwpb/fake_channel_output.h",
+ "public/pw_rpc/pwpb/test_method_context.h",
+ ]
+ public_deps = [
+ ":server_api",
+ "..:test_utils",
+ dir_pw_assert,
+ dir_pw_containers,
+ dir_pw_span,
+ ]
+}
+
+pw_source_set("client_testing") {
+ public = [ "public/pw_rpc/pwpb/client_testing.h" ]
+ public_deps = [
+ ":test_method_context",
+ "..:client",
+ "../raw:client_testing",
+ ]
+}
+
+pw_source_set("client_server_testing") {
+ public = [ "public/pw_rpc/pwpb/client_server_testing.h" ]
+ public_deps = [
+ ":test_method_context",
+ "..:client_server_testing",
+ ]
+}
+
+pw_source_set("client_server_testing_threaded") {
+ public = [ "public/pw_rpc/pwpb/client_server_testing_threaded.h" ]
+ public_deps = [
+ ":test_method_context",
+ "..:client_server_testing_threaded",
+ ]
+}
+
+pw_source_set("internal_test_utils") {
+ public = [ "pw_rpc_pwpb_private/internal_test_utils.h" ]
+ public_deps = [
+ dir_pw_status,
+ dir_pw_stream,
+ ]
+}
+
+pw_source_set("echo_service") {
+ public_configs = [ ":public" ]
+ public_deps = [ "..:protos.pwpb_rpc" ]
+ sources = [ "public/pw_rpc/echo_service_pwpb.h" ]
+}
+
+pw_source_set("client_integration_test") {
+ public_configs = [ ":public" ]
+ public_deps = [
+ "$dir_pw_sync:binary_semaphore",
+ "..:integration_testing",
+ "..:protos.pwpb_rpc",
+ dir_pw_assert,
+ dir_pw_unit_test,
+ ]
+ sources = [ "client_integration_test.cc" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+ tests = [
+ ":client_call_test",
+ ":client_reader_writer_test",
+ ":client_server_context_test",
+ ":client_server_context_threaded_test",
+ ":codegen_test",
+ ":echo_service_test",
+ ":fake_channel_output_test",
+ ":method_lookup_test",
+ ":method_test",
+ ":method_info_test",
+ ":method_union_test",
+ ":server_callback_test",
+ ":server_reader_writer_test",
+ ":serde_test",
+ ":stub_generation_test",
+ ":synchronous_call_test",
+ ]
+}
+
+pw_test("client_call_test") {
+ deps = [
+ ":client_api",
+ ":internal_test_utils",
+ "..:test_protos.pwpb",
+ "..:test_utils",
+ ]
+ sources = [ "client_call_test.cc" ]
+}
+
+pw_test("client_reader_writer_test") {
+ deps = [
+ ":client_api",
+ ":client_testing",
+ "..:test_protos.pwpb_rpc",
+ ]
+ sources = [ "client_reader_writer_test.cc" ]
+}
+
+pw_test("client_server_context_test") {
+ deps = [
+ ":client_api",
+ ":client_server_testing",
+ "..:test_protos.pwpb_rpc",
+ ]
+ sources = [ "client_server_context_test.cc" ]
+}
+
+_stl_threading_enabled =
+ pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread" &&
+ pw_sync_BINARY_SEMAPHORE_BACKEND != "" && pw_sync_MUTEX_BACKEND != ""
+
+pw_test("client_server_context_threaded_test") {
+ deps = [
+ ":client_api",
+ ":client_server_testing_threaded",
+ "$dir_pw_sync:binary_semaphore",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread_stl:test_threads",
+ "..:test_protos.pwpb_rpc",
+ ]
+ sources = [ "client_server_context_threaded_test.cc" ]
+ enable_if = _stl_threading_enabled
+}
+
+pw_test("codegen_test") {
+ deps = [
+ ":client_api",
+ ":internal_test_utils",
+ ":server_api",
+ ":test_method_context",
+ "..:test_protos.pwpb_rpc",
+ "..:test_utils",
+ ]
+ sources = [ "codegen_test.cc" ]
+}
+
+pw_test("echo_service_test") {
+ deps = [
+ ":echo_service",
+ ":server_api",
+ ":test_method_context",
+ ]
+ sources = [ "echo_service_test.cc" ]
+}
+
+pw_test("fake_channel_output_test") {
+ deps = [
+ ":server_api",
+ ":test_method_context",
+ "..:test_protos.pwpb_rpc",
+ ]
+ sources = [ "fake_channel_output_test.cc" ]
+}
+
+pw_test("method_test") {
+ deps = [
+ ":internal_test_utils",
+ ":server_api",
+ "$dir_pw_containers",
+ "..:test_protos.pwpb",
+ "..:test_utils",
+ ]
+ sources = [ "method_test.cc" ]
+}
+
+pw_test("method_info_test") {
+ deps = [
+ "..:common",
+ "..:test_protos.pwpb_rpc",
+ "..:test_utils",
+ ]
+ sources = [ "method_info_test.cc" ]
+}
+
+pw_test("method_lookup_test") {
+ deps = [
+ ":server_api",
+ ":test_method_context",
+ "..:test_protos.pwpb_rpc",
+ "..:test_utils",
+ "../raw:test_method_context",
+ ]
+ sources = [ "method_lookup_test.cc" ]
+}
+
+pw_test("method_union_test") {
+ deps = [
+ ":internal_test_utils",
+ ":server_api",
+ "..:test_protos.pwpb",
+ "..:test_utils",
+ ]
+ sources = [ "method_union_test.cc" ]
+}
+
+pw_test("serde_test") {
+ deps = [
+ ":server_api",
+ "..:test_protos.pwpb",
+ ]
+ sources = [ "serde_test.cc" ]
+}
+
+pw_test("server_callback_test") {
+ deps = [
+ ":server_api",
+ ":test_method_context",
+ "..:test_protos.pwpb_rpc",
+ ]
+ sources = [ "server_callback_test.cc" ]
+}
+
+pw_test("server_reader_writer_test") {
+ deps = [
+ ":server_api",
+ ":test_method_context",
+ "..:test_protos.pwpb_rpc",
+ ]
+ sources = [ "server_reader_writer_test.cc" ]
+}
+
+pw_test("stub_generation_test") {
+ deps = [ "..:test_protos.pwpb_rpc" ]
+ sources = [ "stub_generation_test.cc" ]
+}
+
+pw_test("synchronous_call_test") {
+ deps = [
+ ":test_method_context",
+ "$dir_pw_work_queue:pw_work_queue",
+ "$dir_pw_work_queue:stl_test_thread",
+ "$dir_pw_work_queue:test_thread",
+ "..:synchronous_client_api",
+ "..:test_protos.pwpb_rpc",
+ ]
+ sources = [ "synchronous_call_test.cc" ]
+ enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND != ""
+}
diff --git a/pw_rpc/pwpb/CMakeLists.txt b/pw_rpc/pwpb/CMakeLists.txt
new file mode 100644
index 000000000..ce75e479c
--- /dev/null
+++ b/pw_rpc/pwpb/CMakeLists.txt
@@ -0,0 +1,354 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_rpc.pwpb.server_api STATIC
+ HEADERS
+ public/pw_rpc/pwpb/internal/method.h
+ public/pw_rpc/pwpb/internal/method_union.h
+ public/pw_rpc/pwpb/server_reader_writer.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_rpc.config
+ pw_rpc.pwpb.common
+ pw_rpc.raw.server_api
+ pw_rpc.server
+ SOURCES
+ server_reader_writer.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_rpc.log_config
+)
+
+pw_add_library(pw_rpc.pwpb.client_api INTERFACE
+ HEADERS
+ public/pw_rpc/pwpb/client_reader_writer.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_function
+ pw_rpc.pwpb.common
+ pw_rpc.common
+)
+
+pw_add_library(pw_rpc.pwpb.common INTERFACE
+ HEADERS
+ public/pw_rpc/pwpb/internal/common.h
+ public/pw_rpc/pwpb/serde.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert.assert
+ pw_bytes
+ pw_log
+ pw_protobuf
+ pw_rpc.client
+ pw_rpc.common
+ pw_rpc.log_config
+ pw_rpc.server
+ pw_span
+ pw_status
+)
+
+pw_add_library(pw_rpc.pwpb.test_method_context INTERFACE
+ HEADERS
+ public/pw_rpc/pwpb/fake_channel_output.h
+ public/pw_rpc/pwpb/test_method_context.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_containers
+ pw_rpc.pwpb.server_api
+ pw_rpc.test_utils
+ pw_span
+)
+
+pw_add_library(pw_rpc.pwpb.client_testing INTERFACE
+ HEADERS
+ public/pw_rpc/pwpb/client_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.client
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.raw.client_testing
+)
+
+pw_add_library(pw_rpc.pwpb.client_server_testing INTERFACE
+ HEADERS
+ public/pw_rpc/pwpb/client_server_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.client_server_testing
+)
+
+pw_add_library(pw_rpc.pwpb.client_server_testing_threaded INTERFACE
+ HEADERS
+ public/pw_rpc/pwpb/client_server_testing_threaded.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.client_server_testing_threaded
+)
+
+pw_add_library(pw_rpc.pwpb.internal_test_utils INTERFACE
+ HEADERS
+ pw_rpc_pwpb_private/internal_test_utils.h
+ PUBLIC_DEPS
+ pw_status
+ pw_stream
+)
+
+pw_add_library(pw_rpc.pwpb.echo_service INTERFACE
+ HEADERS
+ public/pw_rpc/echo_service_pwpb.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.protos.pwpb_rpc
+)
+
+pw_add_library(pw_rpc.pwpb.client_integration_test STATIC
+ SOURCES
+ client_integration_test.cc
+ PRIVATE_DEPS
+ pw_assert
+ pw_rpc.integration_testing
+ pw_rpc.protos.pwpb_rpc
+ pw_sync.binary_semaphore
+ pw_unit_test
+)
+
+pw_add_test(pw_rpc.pwpb.client_call_test
+ SOURCES
+ client_call_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.client_api
+ pw_rpc.pwpb.internal_test_utils
+ pw_rpc.test_protos.pwpb
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.client_reader_writer_test
+ SOURCES
+ client_reader_writer_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.client_api
+ pw_rpc.pwpb.client_testing
+ pw_rpc.test_protos.pwpb_rpc
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.client_server_context_test
+ SOURCES
+ client_server_context_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.client_api
+ pw_rpc.pwpb.client_server_testing
+ pw_rpc.test_protos.pwpb_rpc
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
+ (NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "") AND
+ (NOT "${pw_sync.mutex_BACKEND}" STREQUAL ""))
+ pw_add_test(pw_rpc.pwpb.client_server_context_threaded_test
+ SOURCES
+ client_server_context_threaded_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.client_api
+ pw_rpc.pwpb.client_server_testing_threaded
+ pw_rpc.test_protos.pwpb_rpc
+ pw_sync.binary_semaphore
+ pw_thread.test_threads
+ pw_thread.thread
+ pw_thread_stl.test_threads
+ GROUPS
+ modules
+ pw_rpc.pwpb
+ )
+endif()
+
+pw_add_test(pw_rpc.pwpb.codegen_test
+ SOURCES
+ codegen_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.client_api
+ pw_rpc.pwpb.internal_test_utils
+ pw_rpc.pwpb.server_api
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.test_protos.pwpb_rpc
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.echo_service_test
+ SOURCES
+ echo_service_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.echo_service
+ pw_rpc.pwpb.server_api
+ pw_rpc.pwpb.test_method_context
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.fake_channel_output_test
+ SOURCES
+ fake_channel_output_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.server_api
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.test_protos.pwpb_rpc
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.method_test
+ SOURCES
+ method_test.cc
+ PRIVATE_DEPS
+ pw_containers
+ pw_rpc.pwpb.internal_test_utils
+ pw_rpc.pwpb.server_api
+ pw_rpc.test_protos.pwpb
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.method_info_test
+ SOURCES
+ method_info_test.cc
+ PRIVATE_DEPS
+ pw_rpc.common
+ pw_rpc.test_protos.pwpb_rpc
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.method_lookup_test
+ SOURCES
+ method_lookup_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.server_api
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.raw.test_method_context
+ pw_rpc.test_protos.pwpb_rpc
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.method_union_test
+ SOURCES
+ method_union_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.internal_test_utils
+ pw_rpc.pwpb.server_api
+ pw_rpc.test_protos.pwpb
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.serde_test
+ SOURCES
+ serde_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.server_api
+ pw_rpc.test_protos.pwpb
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.server_callback_test
+ SOURCES
+ server_callback_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.server_api
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.test_protos.pwpb_rpc
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.server_reader_writer_test
+ SOURCES
+ server_reader_writer_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.server_api
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.test_protos.pwpb_rpc
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+pw_add_test(pw_rpc.pwpb.stub_generation_test
+ SOURCES
+ stub_generation_test.cc
+ PRIVATE_DEPS
+ pw_rpc.test_protos.pwpb_rpc
+ GROUPS
+ modules
+ pw_rpc.pwpb
+)
+
+# TODO(b/231950909) Test disabled as pw_work_queue lacks CMakeLists.txt
+if((TARGET pw_work_queue.pw_work_queue) AND
+ ("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
+ (NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL
+ "pw_sync.timed_thread_notification.NO_BACKEND_SET"))
+ pw_add_test(pw_rpc.pwpb.synchronous_call_test
+ SOURCES
+ synchronous_call_test.cc
+ PRIVATE_DEPS
+ pw_rpc.pwpb.test_method_context
+ pw_rpc.synchronous_client_api
+ pw_rpc.test_protos.pwpb_rpc
+ pw_thread.thread
+ pw_work_queue.pw_work_queue
+ pw_work_queue.stl_test_thread
+ pw_work_queue.test_thread
+ GROUPS
+ modules
+ pw_rpc.pwpb
+ )
+endif()
diff --git a/pw_rpc/pwpb/client_call_test.cc b/pw_rpc/pwpb/client_call_test.cc
new file mode 100644
index 000000000..e382ea82e
--- /dev/null
+++ b/pw_rpc/pwpb/client_call_test.cc
@@ -0,0 +1,376 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <optional>
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/test_utils.h"
+#include "pw_rpc/pwpb/client_reader_writer.h"
+#include "pw_rpc_pwpb_private/internal_test_utils.h"
+#include "pw_rpc_test_protos/test.pwpb.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
+
+namespace pw::rpc {
+namespace {
+
+using internal::ClientContextForTest;
+using internal::pwpb::PacketType;
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+constexpr uint32_t kServiceId = 16;
+constexpr uint32_t kUnaryMethodId = 111;
+constexpr uint32_t kServerStreamingMethodId = 112;
+
+class FakeGeneratedServiceClient {
+ public:
+ static PwpbUnaryReceiver<TestResponse::Message> TestUnaryRpc(
+ Client& client,
+ uint32_t channel_id,
+ const TestRequest::Message& request,
+ Function<void(const TestResponse::Message&, Status)> on_response,
+ Function<void(Status)> on_error = nullptr) {
+ return internal::PwpbUnaryResponseClientCall<TestResponse::Message>::Start<
+ PwpbUnaryReceiver<TestResponse::Message>>(
+ client,
+ channel_id,
+ kServiceId,
+ kUnaryMethodId,
+ internal::kPwpbMethodSerde<&TestRequest::kMessageFields,
+ &TestResponse::kMessageFields>,
+ std::move(on_response),
+ std::move(on_error),
+ request);
+ }
+
+ static PwpbUnaryReceiver<TestResponse::Message> TestAnotherUnaryRpc(
+ Client& client,
+ uint32_t channel_id,
+ const TestRequest::Message& request,
+ Function<void(const TestResponse::Message&, Status)> on_response,
+ Function<void(Status)> on_error = nullptr) {
+ return internal::PwpbUnaryResponseClientCall<TestResponse::Message>::Start<
+ PwpbUnaryReceiver<TestResponse::Message>>(
+ client,
+ channel_id,
+ kServiceId,
+ kUnaryMethodId,
+ internal::kPwpbMethodSerde<&TestRequest::kMessageFields,
+ &TestResponse::kMessageFields>,
+ std::move(on_response),
+ std::move(on_error),
+ request);
+ }
+
+ static PwpbClientReader<TestStreamResponse::Message> TestServerStreamRpc(
+ Client& client,
+ uint32_t channel_id,
+ const TestRequest::Message& request,
+ Function<void(const TestStreamResponse::Message&)> on_response,
+ Function<void(Status)> on_stream_end,
+ Function<void(Status)> on_error = nullptr) {
+ return internal::PwpbStreamResponseClientCall<TestStreamResponse::Message>::
+ Start<PwpbClientReader<TestStreamResponse::Message>>(
+ client,
+ channel_id,
+ kServiceId,
+ kServerStreamingMethodId,
+ internal::kPwpbMethodSerde<&TestRequest::kMessageFields,
+ &TestStreamResponse::kMessageFields>,
+ std::move(on_response),
+ std::move(on_stream_end),
+ std::move(on_error),
+ request);
+ }
+};
+
+TEST(PwpbClientCall, Unary_SendsRequestPacket) {
+ ClientContextForTest context;
+
+ auto call = FakeGeneratedServiceClient::TestUnaryRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ nullptr);
+
+ EXPECT_EQ(context.output().total_packets(), 1u);
+ auto packet = context.output().last_packet();
+ EXPECT_EQ(packet.channel_id(), context.channel().id());
+ EXPECT_EQ(packet.service_id(), kServiceId);
+ EXPECT_EQ(packet.method_id(), kUnaryMethodId);
+
+ PW_DECODE_PB(TestRequest, sent_proto, packet.payload());
+ EXPECT_EQ(sent_proto.integer, 123);
+}
+
+class UnaryClientCall : public ::testing::Test {
+ protected:
+ std::optional<Status> last_status_;
+ std::optional<Status> last_error_;
+ int responses_received_ = 0;
+ int last_response_value_ = 0;
+};
+
+TEST_F(UnaryClientCall, InvokesCallbackOnValidResponse) {
+ ClientContextForTest context;
+
+ auto call = FakeGeneratedServiceClient::TestUnaryRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ [this](const TestResponse::Message& response, Status status) {
+ ++responses_received_;
+ last_status_ = status;
+ last_response_value_ = response.value;
+ });
+
+ PW_ENCODE_PB(TestResponse, response, .value = 42);
+ EXPECT_EQ(OkStatus(), context.SendResponse(OkStatus(), response));
+
+ ASSERT_EQ(responses_received_, 1);
+ EXPECT_EQ(last_status_, OkStatus());
+ EXPECT_EQ(last_response_value_, 42);
+}
+
+TEST_F(UnaryClientCall, DoesNothingOnNullCallback) {
+ ClientContextForTest context;
+
+ auto call = FakeGeneratedServiceClient::TestUnaryRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ nullptr);
+
+ PW_ENCODE_PB(TestResponse, response, .value = 42);
+ EXPECT_EQ(OkStatus(), context.SendResponse(OkStatus(), response));
+
+ ASSERT_EQ(responses_received_, 0);
+}
+
+TEST_F(UnaryClientCall, InvokesErrorCallbackOnInvalidResponse) {
+ ClientContextForTest context;
+
+ auto call = FakeGeneratedServiceClient::TestUnaryRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ [this](const TestResponse::Message& response, Status status) {
+ ++responses_received_;
+ last_status_ = status;
+ last_response_value_ = response.value;
+ },
+ [this](Status status) { last_error_ = status; });
+
+ constexpr std::byte bad_payload[]{
+ std::byte{0xab}, std::byte{0xcd}, std::byte{0xef}};
+ EXPECT_EQ(OkStatus(), context.SendResponse(OkStatus(), bad_payload));
+
+ EXPECT_EQ(responses_received_, 0);
+ ASSERT_TRUE(last_error_.has_value());
+ EXPECT_EQ(last_error_, Status::DataLoss());
+}
+
+TEST_F(UnaryClientCall, InvokesErrorCallbackOnServerError) {
+ ClientContextForTest context;
+
+ auto call = FakeGeneratedServiceClient::TestUnaryRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ [this](const TestResponse::Message& response, Status status) {
+ ++responses_received_;
+ last_status_ = status;
+ last_response_value_ = response.value;
+ },
+ [this](Status status) { last_error_ = status; });
+
+ EXPECT_EQ(OkStatus(),
+ context.SendPacket(PacketType::SERVER_ERROR, Status::NotFound()));
+
+ EXPECT_EQ(responses_received_, 0);
+ EXPECT_EQ(last_error_, Status::NotFound());
+}
+
+TEST_F(UnaryClientCall, DoesNothingOnErrorWithoutCallback) {
+ ClientContextForTest context;
+
+ auto call = FakeGeneratedServiceClient::TestUnaryRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ [this](const TestResponse::Message& response, Status status) {
+ ++responses_received_;
+ last_status_ = status;
+ last_response_value_ = response.value;
+ });
+
+ constexpr std::byte bad_payload[]{
+ std::byte{0xab}, std::byte{0xcd}, std::byte{0xef}};
+ EXPECT_EQ(OkStatus(), context.SendResponse(OkStatus(), bad_payload));
+
+ EXPECT_EQ(responses_received_, 0);
+}
+
+TEST_F(UnaryClientCall, OnlyReceivesOneResponse) {
+ ClientContextForTest context;
+
+ auto call = FakeGeneratedServiceClient::TestUnaryRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ [this](const TestResponse::Message& response, Status status) {
+ ++responses_received_;
+ last_status_ = status;
+ last_response_value_ = response.value;
+ });
+
+ PW_ENCODE_PB(TestResponse, r1, .value = 42);
+ EXPECT_EQ(OkStatus(), context.SendResponse(Status::Unimplemented(), r1));
+ PW_ENCODE_PB(TestResponse, r2, .value = 44);
+ EXPECT_EQ(OkStatus(), context.SendResponse(Status::OutOfRange(), r2));
+ PW_ENCODE_PB(TestResponse, r3, .value = 46);
+ EXPECT_EQ(OkStatus(), context.SendResponse(Status::Internal(), r3));
+
+ EXPECT_EQ(responses_received_, 1);
+ EXPECT_EQ(last_status_, Status::Unimplemented());
+ EXPECT_EQ(last_response_value_, 42);
+}
+
+class ServerStreamingClientCall : public ::testing::Test {
+ protected:
+ std::optional<Status> stream_status_;
+ std::optional<Status> rpc_error_;
+ int responses_received_ = 0;
+ int last_response_number_ = 0;
+};
+
+TEST_F(ServerStreamingClientCall, SendsRequestPacket) {
+ ClientContextForTest<128, 99, kServiceId, kServerStreamingMethodId> context;
+
+ auto call = FakeGeneratedServiceClient::TestServerStreamRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 71, .status_code = 0},
+ nullptr,
+ nullptr);
+
+ EXPECT_EQ(context.output().total_packets(), 1u);
+ auto packet = context.output().last_packet();
+ EXPECT_EQ(packet.channel_id(), context.channel().id());
+ EXPECT_EQ(packet.service_id(), kServiceId);
+ EXPECT_EQ(packet.method_id(), kServerStreamingMethodId);
+
+ PW_DECODE_PB(TestRequest, sent_proto, packet.payload());
+ EXPECT_EQ(sent_proto.integer, 71);
+}
+
+TEST_F(ServerStreamingClientCall, InvokesCallbackOnValidResponse) {
+ ClientContextForTest<128, 99, kServiceId, kServerStreamingMethodId> context;
+
+ auto call = FakeGeneratedServiceClient::TestServerStreamRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 71, .status_code = 0},
+ [this](const TestStreamResponse::Message& response) {
+ ++responses_received_;
+ last_response_number_ = response.number;
+ },
+ [this](Status status) { stream_status_ = status; });
+
+ PW_ENCODE_PB(TestStreamResponse, r1, .chunk = {}, .number = 11u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(r1));
+ EXPECT_TRUE(call.active());
+ EXPECT_EQ(responses_received_, 1);
+ EXPECT_EQ(last_response_number_, 11);
+
+ PW_ENCODE_PB(TestStreamResponse, r2, .chunk = {}, .number = 22u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(r2));
+ EXPECT_TRUE(call.active());
+ EXPECT_EQ(responses_received_, 2);
+ EXPECT_EQ(last_response_number_, 22);
+
+ PW_ENCODE_PB(TestStreamResponse, r3, .chunk = {}, .number = 33u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(r3));
+ EXPECT_TRUE(call.active());
+ EXPECT_EQ(responses_received_, 3);
+ EXPECT_EQ(last_response_number_, 33);
+}
+
+TEST_F(ServerStreamingClientCall, InvokesStreamEndOnFinish) {
+ ClientContextForTest<128, 99, kServiceId, kServerStreamingMethodId> context;
+
+ auto call = FakeGeneratedServiceClient::TestServerStreamRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 71, .status_code = 0},
+ [this](const TestStreamResponse::Message& response) {
+ ++responses_received_;
+ last_response_number_ = response.number;
+ },
+ [this](Status status) { stream_status_ = status; });
+
+ PW_ENCODE_PB(TestStreamResponse, r1, .chunk = {}, .number = 11u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(r1));
+ EXPECT_TRUE(call.active());
+
+ PW_ENCODE_PB(TestStreamResponse, r2, .chunk = {}, .number = 22u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(r2));
+ EXPECT_TRUE(call.active());
+
+ // Close the stream.
+ EXPECT_EQ(OkStatus(), context.SendResponse(Status::NotFound()));
+
+ PW_ENCODE_PB(TestStreamResponse, r3, .chunk = {}, .number = 33u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(r3));
+ EXPECT_FALSE(call.active());
+
+ EXPECT_EQ(responses_received_, 2);
+}
+
+TEST_F(ServerStreamingClientCall, ParseErrorTerminatesCallWithDataLoss) {
+ ClientContextForTest<128, 99, kServiceId, kServerStreamingMethodId> context;
+
+ auto call = FakeGeneratedServiceClient::TestServerStreamRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 71, .status_code = 0},
+ [this](const TestStreamResponse::Message& response) {
+ ++responses_received_;
+ last_response_number_ = response.number;
+ },
+ nullptr,
+ [this](Status error) { rpc_error_ = error; });
+
+ PW_ENCODE_PB(TestStreamResponse, r1, .chunk = {}, .number = 11u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(r1));
+ EXPECT_TRUE(call.active());
+ EXPECT_EQ(responses_received_, 1);
+ EXPECT_EQ(last_response_number_, 11);
+
+ constexpr std::byte bad_payload[]{
+ std::byte{0xab}, std::byte{0xcd}, std::byte{0xef}};
+ EXPECT_EQ(OkStatus(), context.SendServerStream(bad_payload));
+ EXPECT_FALSE(call.active());
+ EXPECT_EQ(responses_received_, 1);
+ EXPECT_EQ(rpc_error_, Status::DataLoss());
+}
+
+} // namespace
+} // namespace pw::rpc
+
+PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_rpc/pwpb/client_integration_test.cc b/pw_rpc/pwpb/client_integration_test.cc
new file mode 100644
index 000000000..af5bfc745
--- /dev/null
+++ b/pw_rpc/pwpb/client_integration_test.cc
@@ -0,0 +1,160 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_assert/check.h"
+#include "pw_rpc/benchmark.rpc.pwpb.h"
+#include "pw_rpc/integration_testing.h"
+#include "pw_sync/binary_semaphore.h"
+
+namespace pwpb_rpc_test {
+namespace {
+
+using namespace std::chrono_literals;
+using pw::ByteSpan;
+using pw::ConstByteSpan;
+using pw::Function;
+using pw::OkStatus;
+using pw::Status;
+
+using pw::rpc::pw_rpc::pwpb::Benchmark;
+
+constexpr int kIterations = 10;
+
+class PayloadReceiver {
+ public:
+ const char* Wait() {
+ PW_CHECK(sem_.try_acquire_for(1500ms));
+ return reinterpret_cast<const char*>(payload_.payload.data());
+ }
+
+ Function<void(const pw::rpc::Payload::Message&, Status)> UnaryOnCompleted() {
+ return [this](const pw::rpc::Payload::Message& data, Status) {
+ CopyPayload(data);
+ };
+ }
+
+ Function<void(const pw::rpc::Payload::Message&)> OnNext() {
+ return [this](const pw::rpc::Payload::Message& data) { CopyPayload(data); };
+ }
+
+ private:
+ void CopyPayload(const pw::rpc::Payload::Message& data) {
+ payload_ = data;
+ sem_.release();
+ }
+
+ pw::sync::BinarySemaphore sem_;
+ pw::rpc::Payload::Message payload_ = {};
+};
+
+template <size_t kSize>
+pw::rpc::Payload::Message Payload(const char (&string)[kSize]) {
+ static_assert(kSize <= sizeof(pw::rpc::Payload::Message::payload));
+ pw::rpc::Payload::Message payload{};
+ payload.payload.resize(kSize);
+ std::memcpy(payload.payload.data(), string, kSize);
+ return payload;
+}
+
+const Benchmark::Client kClient(pw::rpc::integration_test::client(),
+ pw::rpc::integration_test::kChannelId);
+
+TEST(PwpbRpcIntegrationTest, Unary) {
+ char value[] = {"hello, world!"};
+
+ for (int i = 0; i < kIterations; ++i) {
+ PayloadReceiver receiver;
+
+ value[0] = static_cast<char>(i);
+ pw::rpc::PwpbUnaryReceiver call =
+ kClient.UnaryEcho(Payload(value), receiver.UnaryOnCompleted());
+ ASSERT_STREQ(receiver.Wait(), value);
+ }
+}
+
+TEST(PwpbRpcIntegrationTest, Unary_ReuseCall) {
+ pw::rpc::PwpbUnaryReceiver<pw::rpc::Payload::Message> call;
+ char value[] = {"O_o "};
+
+ for (int i = 0; i < kIterations; ++i) {
+ PayloadReceiver receiver;
+
+ value[sizeof(value) - 2] = static_cast<char>(i);
+ call = kClient.UnaryEcho(Payload(value), receiver.UnaryOnCompleted());
+ ASSERT_STREQ(receiver.Wait(), value);
+ }
+}
+
+TEST(PwpbRpcIntegrationTest, Unary_DiscardCalls) {
+ constexpr int iterations = PW_RPC_USE_GLOBAL_MUTEX ? 10000 : 1;
+ for (int i = 0; i < iterations; ++i) {
+ kClient.UnaryEcho(Payload("O_o"));
+ }
+}
+
+TEST(PwpbRpcIntegrationTest, BidirectionalStreaming_MoveCalls) {
+ for (int i = 0; i < kIterations; ++i) {
+ PayloadReceiver receiver;
+ pw::rpc::PwpbClientReaderWriter call =
+ kClient.BidirectionalEcho(receiver.OnNext());
+
+ ASSERT_EQ(OkStatus(), call.Write(Payload("Yello")));
+ ASSERT_STREQ(receiver.Wait(), "Yello");
+
+ pw::rpc::PwpbClientReaderWriter<pw::rpc::Payload::Message,
+ pw::rpc::Payload::Message>
+ new_call = std::move(call);
+
+ // NOLINTNEXTLINE(bugprone-use-after-move)
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write(Payload("Dello")));
+
+ ASSERT_EQ(OkStatus(), new_call.Write(Payload("Dello")));
+ ASSERT_STREQ(receiver.Wait(), "Dello");
+
+ call = std::move(new_call);
+
+ // NOLINTNEXTLINE(bugprone-use-after-move)
+ EXPECT_EQ(Status::FailedPrecondition(), new_call.Write(Payload("Dello")));
+
+ ASSERT_EQ(OkStatus(), call.Write(Payload("???")));
+ ASSERT_STREQ(receiver.Wait(), "???");
+
+ EXPECT_EQ(OkStatus(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), new_call.Cancel());
+ }
+}
+
+TEST(PwpbRpcIntegrationTest, BidirectionalStreaming_ReuseCall) {
+ pw::rpc::PwpbClientReaderWriter<pw::rpc::Payload::Message,
+ pw::rpc::Payload::Message>
+ call;
+
+ for (int i = 0; i < kIterations; ++i) {
+ PayloadReceiver receiver;
+ call = kClient.BidirectionalEcho(receiver.OnNext());
+
+ ASSERT_EQ(OkStatus(), call.Write(Payload("Yello")));
+ ASSERT_STREQ(receiver.Wait(), "Yello");
+
+ ASSERT_EQ(OkStatus(), call.Write(Payload("Dello")));
+ ASSERT_STREQ(receiver.Wait(), "Dello");
+
+ ASSERT_EQ(OkStatus(), call.Write(Payload("???")));
+ ASSERT_STREQ(receiver.Wait(), "???");
+ }
+}
+
+} // namespace
+} // namespace pwpb_rpc_test
diff --git a/pw_rpc/pwpb/client_reader_writer_test.cc b/pw_rpc/pwpb/client_reader_writer_test.cc
new file mode 100644
index 000000000..540f0106d
--- /dev/null
+++ b/pw_rpc/pwpb/client_reader_writer_test.cc
@@ -0,0 +1,239 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/pwpb/client_reader_writer.h"
+
+#include <optional>
+
+#include "gtest/gtest.h"
+#include "pw_rpc/pwpb/client_testing.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
+
+namespace pw::rpc {
+namespace {
+
+using test::pw_rpc::pwpb::TestService;
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+void FailIfCalled(Status) { FAIL(); }
+template <typename T>
+void FailIfOnNextCalled(const T&) {
+ FAIL();
+}
+template <typename T>
+void FailIfOnCompletedCalled(const T&, Status) {
+ FAIL();
+}
+
+TEST(PwpbUnaryReceiver, DefaultConstructed) {
+ PwpbUnaryReceiver<TestResponse::Message> call;
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+
+ call.set_on_completed([](const TestResponse::Message&, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientWriter, DefaultConstructed) {
+ PwpbClientWriter<TestRequest::Message, TestStreamResponse::Message> call;
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+
+ call.set_on_completed([](const TestStreamResponse::Message&, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientReader, DefaultConstructed) {
+ PwpbClientReader<TestStreamResponse::Message> call;
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const TestStreamResponse::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientReaderWriter, DefaultConstructed) {
+ PwpbClientReaderWriter<TestRequest::Message, TestStreamResponse::Message>
+ call;
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const TestStreamResponse::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbUnaryReceiver, Closed) {
+ PwpbClientTestContext ctx;
+ PwpbUnaryReceiver<TestResponse::Message> call =
+ TestService::TestUnaryRpc(ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnCompletedCalled<TestResponse::Message>,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.Cancel());
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+
+ call.set_on_completed([](const TestResponse::Message&, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientWriter, Closed) {
+ PwpbClientTestContext ctx;
+ PwpbClientWriter<TestRequest::Message, TestStreamResponse::Message> call =
+ TestService::TestClientStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnCompletedCalled<TestStreamResponse::Message>,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.Cancel());
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+
+ call.set_on_completed([](const TestStreamResponse::Message&, Status) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientReader, Closed) {
+ PwpbClientTestContext ctx;
+ PwpbClientReader<TestStreamResponse::Message> call =
+ TestService::TestServerStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnNextCalled<TestStreamResponse::Message>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.Cancel());
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const TestStreamResponse::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbClientReaderWriter, Closed) {
+ PwpbClientTestContext ctx;
+ PwpbClientReaderWriter<TestRequest::Message, TestStreamResponse::Message>
+ call = TestService::TestBidirectionalStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ FailIfOnNextCalled<TestStreamResponse::Message>,
+ FailIfCalled,
+ FailIfCalled);
+ ASSERT_EQ(OkStatus(), call.Cancel());
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+
+ call.set_on_completed([](Status) {});
+ call.set_on_next([](const TestStreamResponse::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbUnaryReceiver, CallbacksMoveCorrectly) {
+ PwpbClientTestContext ctx;
+
+ struct {
+ TestResponse::Message payload = {.value = 12345678};
+ std::optional<Status> status;
+ } reply;
+
+ PwpbUnaryReceiver<TestResponse::Message> call_2;
+ {
+ PwpbUnaryReceiver call_1 = TestService::TestUnaryRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ {},
+ [&reply](const TestResponse::Message& response, Status status) {
+ reply.payload = response;
+ reply.status = status;
+ });
+
+ call_2 = std::move(call_1);
+ }
+
+ ctx.server().SendResponse<TestService::TestUnaryRpc>({.value = 9000},
+ Status::NotFound());
+ EXPECT_EQ(reply.payload.value, 9000);
+ EXPECT_EQ(reply.status, Status::NotFound());
+}
+
+TEST(PwpbClientReaderWriter, CallbacksMoveCorrectly) {
+ PwpbClientTestContext ctx;
+
+ TestStreamResponse::Message payload = {.chunk = {}, .number = 13579};
+
+ PwpbClientReaderWriter<TestRequest::Message, TestStreamResponse::Message>
+ call_2;
+ {
+ PwpbClientReaderWriter call_1 = TestService::TestBidirectionalStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ [&payload](const TestStreamResponse::Message& response) {
+ payload = response;
+ });
+
+ call_2 = std::move(call_1);
+ }
+
+ ctx.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ {.chunk = {}, .number = 5050});
+ EXPECT_EQ(payload.number, 5050u);
+}
+
+} // namespace
+} // namespace pw::rpc
+
+PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_rpc/pwpb/client_server_context_test.cc b/pw_rpc/pwpb/client_server_context_test.cc
new file mode 100644
index 000000000..bd45154ef
--- /dev/null
+++ b/pw_rpc/pwpb/client_server_context_test.cc
@@ -0,0 +1,123 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_rpc/pwpb/client_server_testing.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+
+namespace pw::rpc {
+namespace {
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+} // namespace
+
+namespace test {
+
+using GeneratedService = ::pw::rpc::test::pw_rpc::pwpb::TestService;
+
+class TestService final : public GeneratedService::Service<TestService> {
+ public:
+ Status TestUnaryRpc(const TestRequest::Message& request,
+ TestResponse::Message& response) {
+ response.value = request.integer + 1;
+ return static_cast<Status::Code>(request.status_code);
+ }
+
+ void TestAnotherUnaryRpc(const TestRequest::Message&,
+ PwpbUnaryResponder<TestResponse::Message>&) {}
+
+ static void TestServerStreamRpc(const TestRequest::Message&,
+ ServerWriter<TestStreamResponse::Message>&) {}
+
+ void TestClientStreamRpc(
+ ServerReader<TestRequest::Message, TestStreamResponse::Message>&) {}
+
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<TestRequest::Message, TestStreamResponse::Message>&) {}
+};
+
+} // namespace test
+
+namespace {
+
+TEST(PwpbClientServerTestContext, ReceivesUnaryRpcReponse) {
+ PwpbClientServerTestContext<> ctx;
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ TestResponse::Message response = {};
+ auto handler = [&response](const TestResponse::Message& server_response,
+ pw::Status) { response = server_response; };
+
+ TestRequest::Message request{.integer = 1, .status_code = OkStatus().code()};
+ auto call = test::GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request, handler);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ const auto sent_request =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto sent_response =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+
+ EXPECT_EQ(response.value, sent_response.value);
+ EXPECT_EQ(response.value, request.integer + 1);
+ EXPECT_EQ(request.integer, sent_request.integer);
+}
+
+TEST(PwpbClientServerTestContext, ReceivesMultipleReponses) {
+ PwpbClientServerTestContext<> ctx;
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ TestResponse::Message response1 = {};
+ TestResponse::Message response2 = {};
+ auto handler1 = [&response1](const TestResponse::Message& server_response,
+ pw::Status) { response1 = server_response; };
+ auto handler2 = [&response2](const TestResponse::Message& server_response,
+ pw::Status) { response2 = server_response; };
+
+ TestRequest::Message request1{.integer = 1, .status_code = OkStatus().code()};
+ TestRequest::Message request2{.integer = 2, .status_code = OkStatus().code()};
+ const auto call1 = test::GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request1, handler1);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+ const auto call2 = test::GeneratedService::TestUnaryRpc(
+ ctx.client(), ctx.channel().id(), request2, handler2);
+ // Force manual forwarding of packets as context is not threaded
+ ctx.ForwardNewPackets();
+
+ const auto sent_request1 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto sent_request2 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+ const auto sent_response1 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto sent_response2 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(response1.value, request1.integer + 1);
+ EXPECT_EQ(response2.value, request2.integer + 1);
+ EXPECT_EQ(response1.value, sent_response1.value);
+ EXPECT_EQ(response2.value, sent_response2.value);
+ EXPECT_EQ(request1.integer, sent_request1.integer);
+ EXPECT_EQ(request2.integer, sent_request2.integer);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/client_server_context_threaded_test.cc b/pw_rpc/pwpb/client_server_context_threaded_test.cc
new file mode 100644
index 000000000..ae1306df3
--- /dev/null
+++ b/pw_rpc/pwpb/client_server_context_threaded_test.cc
@@ -0,0 +1,123 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_rpc/pwpb/client_server_testing_threaded.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/test_threads.h"
+
+namespace pw::rpc {
+namespace {
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+} // namespace
+
+namespace test {
+
+using GeneratedService = ::pw::rpc::test::pw_rpc::pwpb::TestService;
+
+class TestService final : public GeneratedService::Service<TestService> {
+ public:
+ Status TestUnaryRpc(const TestRequest::Message& request,
+ TestResponse::Message& response) {
+ response.value = request.integer + 1;
+ return static_cast<Status::Code>(request.status_code);
+ }
+
+ void TestAnotherUnaryRpc(const TestRequest::Message&,
+ PwpbUnaryResponder<TestResponse::Message>&) {}
+
+ static void TestServerStreamRpc(const TestRequest::Message&,
+ ServerWriter<TestStreamResponse::Message>&) {}
+
+ void TestClientStreamRpc(
+ ServerReader<TestRequest::Message, TestStreamResponse::Message>&) {}
+
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<TestRequest::Message, TestStreamResponse::Message>&) {}
+};
+
+} // namespace test
+
+namespace {
+
+class RpcCaller {
+ public:
+ void BlockOnResponse(uint32_t i, Client& client, uint32_t channel_id) {
+ TestRequest::Message request{.integer = i,
+ .status_code = OkStatus().code()};
+ auto call = test::GeneratedService::TestUnaryRpc(
+ client,
+ channel_id,
+ request,
+ [this](const TestResponse::Message&, Status) { semaphore_.release(); },
+ [](Status) {});
+
+ semaphore_.acquire();
+ }
+
+ private:
+ pw::sync::BinarySemaphore semaphore_;
+};
+
+TEST(PwpbClientServerTestContextThreaded, ReceivesUnaryRpcReponseThreaded) {
+ PwpbClientServerTestContextThreaded<> ctx(thread::test::TestOptionsThread0());
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ constexpr auto value = 1;
+ caller.BlockOnResponse(value, ctx.client(), ctx.channel().id());
+
+ const auto request =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto response =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+
+ EXPECT_EQ(value, request.integer);
+ EXPECT_EQ(value + 1, response.value);
+}
+
+TEST(PwpbClientServerTestContextThreaded, ReceivesMultipleReponsesThreaded) {
+ PwpbClientServerTestContextThreaded<> ctx(thread::test::TestOptionsThread0());
+ test::TestService service;
+ ctx.server().RegisterService(service);
+
+ RpcCaller caller;
+ constexpr auto value1 = 1;
+ constexpr auto value2 = 2;
+ caller.BlockOnResponse(value1, ctx.client(), ctx.channel().id());
+ caller.BlockOnResponse(value2, ctx.client(), ctx.channel().id());
+
+ const auto request1 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto request2 =
+ ctx.request<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+ const auto response1 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(0);
+ const auto response2 =
+ ctx.response<test::pw_rpc::pwpb::TestService::TestUnaryRpc>(1);
+
+ EXPECT_EQ(value1, request1.integer);
+ EXPECT_EQ(value2, request2.integer);
+ EXPECT_EQ(value1 + 1, response1.value);
+ EXPECT_EQ(value2 + 1, response2.value);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/codegen_test.cc b/pw_rpc/pwpb/codegen_test.cc
new file mode 100644
index 000000000..d85760350
--- /dev/null
+++ b/pw_rpc/pwpb/codegen_test.cc
@@ -0,0 +1,399 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_rpc/internal/hash.h"
+#include "pw_rpc/internal/test_utils.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+#include "pw_rpc_pwpb_private/internal_test_utils.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
+
+namespace pw::rpc {
+namespace test {
+
+class TestService final
+ : public pw_rpc::pwpb::TestService::Service<TestService> {
+ public:
+ Status TestUnaryRpc(const pwpb::TestRequest::Message& request,
+ pwpb::TestResponse::Message& response) {
+ response.value = request.integer + 1;
+ return static_cast<Status::Code>(request.status_code);
+ }
+
+ void TestAnotherUnaryRpc(
+ const pwpb::TestRequest::Message& request,
+ PwpbUnaryResponder<pwpb::TestResponse::Message>& responder) {
+ pwpb::TestResponse::Message response{};
+ EXPECT_EQ(OkStatus(),
+ responder.Finish(response, TestUnaryRpc(request, response)));
+ }
+
+ static void TestServerStreamRpc(
+ const pwpb::TestRequest::Message& request,
+ ServerWriter<pwpb::TestStreamResponse::Message>& writer) {
+ for (int i = 0; i < request.integer; ++i) {
+ EXPECT_EQ(
+ OkStatus(),
+ writer.Write({.chunk = {}, .number = static_cast<uint32_t>(i)}));
+ }
+
+ EXPECT_EQ(OkStatus(),
+ writer.Finish(static_cast<Status::Code>(request.status_code)));
+ }
+
+ void TestClientStreamRpc(
+ ServerReader<pwpb::TestRequest::Message,
+ pwpb::TestStreamResponse::Message>& new_reader) {
+ reader = std::move(new_reader);
+ }
+
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<pwpb::TestRequest::Message,
+ pwpb::TestStreamResponse::Message>&
+ new_reader_writer) {
+ reader_writer = std::move(new_reader_writer);
+ }
+
+ ServerReader<pwpb::TestRequest::Message, pwpb::TestStreamResponse::Message>
+ reader;
+ ServerReaderWriter<pwpb::TestRequest::Message,
+ pwpb::TestStreamResponse::Message>
+ reader_writer;
+};
+
+} // namespace test
+
+namespace {
+
+using internal::ClientContextForTest;
+
+TEST(PwpbCodegen, CompilesProperly) {
+ test::TestService service;
+ EXPECT_EQ(internal::UnwrapServiceId(service.service_id()),
+ internal::Hash("pw.rpc.test.TestService"));
+ EXPECT_STREQ(service.name(), "TestService");
+}
+
+TEST(PwpbCodegen, Server_InvokeUnaryRpc) {
+ PW_PWPB_TEST_METHOD_CONTEXT(test::TestService, TestUnaryRpc) context;
+
+ EXPECT_EQ(OkStatus(),
+ context.call({.integer = 123, .status_code = OkStatus().code()}));
+
+ EXPECT_EQ(124, context.response().value);
+
+ EXPECT_EQ(Status::InvalidArgument(),
+ context.call({.integer = 999,
+ .status_code = Status::InvalidArgument().code()}));
+ EXPECT_EQ(1000, context.response().value);
+}
+
+TEST(PwpbCodegen, Server_InvokeAsyncUnaryRpc) {
+ PW_PWPB_TEST_METHOD_CONTEXT(test::TestService, TestAnotherUnaryRpc) context;
+
+ context.call({.integer = 123, .status_code = OkStatus().code()});
+
+ EXPECT_EQ(OkStatus(), context.status());
+ EXPECT_EQ(124, context.response().value);
+
+ context.call(
+ {.integer = 999, .status_code = Status::InvalidArgument().code()});
+ EXPECT_EQ(Status::InvalidArgument(), context.status());
+ EXPECT_EQ(1000, context.response().value);
+}
+
+TEST(PwpbCodegen, Server_InvokeServerStreamingRpc) {
+ PW_PWPB_TEST_METHOD_CONTEXT(test::TestService, TestServerStreamRpc) context;
+
+ context.call({.integer = 0, .status_code = Status::Aborted().code()});
+
+ EXPECT_EQ(Status::Aborted(), context.status());
+ EXPECT_TRUE(context.done());
+ EXPECT_EQ(context.total_responses(), 0u);
+
+ context.call({.integer = 4, .status_code = OkStatus().code()});
+
+ ASSERT_EQ(4u, context.responses().size());
+
+ for (size_t i = 0; i < context.responses().size(); ++i) {
+ EXPECT_EQ(context.responses()[i].number, i);
+ }
+
+ EXPECT_EQ(OkStatus().code(), context.status());
+}
+
+TEST(PwpbCodegen, Server_InvokeServerStreamingRpc_ManualWriting) {
+ PW_PWPB_TEST_METHOD_CONTEXT(test::TestService, TestServerStreamRpc, 4)
+ context;
+
+ ASSERT_EQ(4u, context.max_packets());
+
+ auto writer = context.writer();
+
+ EXPECT_EQ(OkStatus(), writer.Write({.chunk = {}, .number = 3}));
+ EXPECT_EQ(OkStatus(), writer.Write({.chunk = {}, .number = 6}));
+ EXPECT_EQ(OkStatus(), writer.Write({.chunk = {}, .number = 9}));
+
+ EXPECT_FALSE(context.done());
+
+ EXPECT_EQ(OkStatus(), writer.Finish(Status::Cancelled()));
+ ASSERT_TRUE(context.done());
+ EXPECT_EQ(Status::Cancelled(), context.status());
+
+ ASSERT_EQ(3u, context.responses().size());
+
+ EXPECT_EQ(context.responses()[0].number, 3u);
+ EXPECT_EQ(context.responses()[1].number, 6u);
+ EXPECT_EQ(context.responses()[2].number, 9u);
+}
+
+TEST(PwpbCodegen, Server_InvokeClientStreamingRpc) {
+ PW_PWPB_TEST_METHOD_CONTEXT(test::TestService, TestClientStreamRpc) context;
+
+ context.call();
+
+ test::pwpb::TestRequest::Message request = {};
+ context.service().reader.set_on_next(
+ [&request](const test::pwpb::TestRequest::Message& req) {
+ request = req;
+ });
+
+ context.SendClientStream({.integer = -99, .status_code = 10});
+ EXPECT_EQ(request.integer, -99);
+ EXPECT_EQ(request.status_code, 10u);
+
+ ASSERT_EQ(OkStatus(),
+ context.service().reader.Finish({.chunk = {}, .number = 3},
+ Status::Unimplemented()));
+ EXPECT_EQ(Status::Unimplemented(), context.status());
+ EXPECT_EQ(context.response().number, 3u);
+}
+
+TEST(PwpbCodegen, Server_InvokeBidirectionalStreamingRpc) {
+ PW_PWPB_TEST_METHOD_CONTEXT(test::TestService, TestBidirectionalStreamRpc)
+ context;
+
+ context.call();
+
+ test::pwpb::TestRequest::Message request = {};
+ context.service().reader_writer.set_on_next(
+ [&request](const test::pwpb::TestRequest::Message& req) {
+ request = req;
+ });
+
+ context.SendClientStream({.integer = -99, .status_code = 10});
+ EXPECT_EQ(request.integer, -99);
+ EXPECT_EQ(request.status_code, 10u);
+
+ ASSERT_EQ(OkStatus(),
+ context.service().reader_writer.Write({.chunk = {}, .number = 2}));
+ EXPECT_EQ(context.responses()[0].number, 2u);
+
+ ASSERT_EQ(OkStatus(),
+ context.service().reader_writer.Finish(Status::NotFound()));
+ EXPECT_EQ(Status::NotFound(), context.status());
+}
+
+TEST(PwpbCodegen, ClientCall_DefaultConstructor) {
+ PwpbUnaryReceiver<test::pwpb::TestResponse::Message> unary_call;
+ PwpbClientReader<test::pwpb::TestStreamResponse::Message>
+ server_streaming_call;
+}
+
+using TestServiceClient = test::pw_rpc::pwpb::TestService::Client;
+
+TEST(PwpbCodegen, Client_InvokesUnaryRpcWithCallback) {
+ constexpr uint32_t kServiceId = internal::Hash("pw.rpc.test.TestService");
+ constexpr uint32_t kMethodId = internal::Hash("TestUnaryRpc");
+
+ ClientContextForTest<128, 99, kServiceId, kMethodId> context;
+
+ TestServiceClient test_client(context.client(), context.channel().id());
+
+ struct {
+ Status last_status = Status::Unknown();
+ int response_value = -1;
+ } result;
+
+ auto call = test_client.TestUnaryRpc(
+ {.integer = 123, .status_code = 0},
+ [&result](const test::pwpb::TestResponse::Message& response,
+ Status status) {
+ result.last_status = status;
+ result.response_value = response.value;
+ });
+
+ EXPECT_TRUE(call.active());
+
+ EXPECT_EQ(context.output().total_packets(), 1u);
+ auto packet =
+ static_cast<const internal::test::FakeChannelOutput&>(context.output())
+ .last_packet();
+ EXPECT_EQ(packet.channel_id(), context.channel().id());
+ EXPECT_EQ(packet.service_id(), kServiceId);
+ EXPECT_EQ(packet.method_id(), kMethodId);
+ PW_DECODE_PB(test::pwpb::TestRequest, sent_proto, packet.payload());
+ EXPECT_EQ(sent_proto.integer, 123);
+
+ PW_ENCODE_PB(test::pwpb::TestResponse, response, .value = 42);
+ EXPECT_EQ(OkStatus(), context.SendResponse(OkStatus(), response));
+ EXPECT_EQ(result.last_status, OkStatus());
+ EXPECT_EQ(result.response_value, 42);
+
+ EXPECT_FALSE(call.active());
+}
+
+TEST(PwpbCodegen, Client_InvokesServerStreamingRpcWithCallback) {
+ constexpr uint32_t kServiceId = internal::Hash("pw.rpc.test.TestService");
+ constexpr uint32_t kMethodId = internal::Hash("TestServerStreamRpc");
+
+ ClientContextForTest<128, 99, kServiceId, kMethodId> context;
+
+ TestServiceClient test_client(context.client(), context.channel().id());
+
+ struct {
+ bool active = true;
+ Status stream_status = Status::Unknown();
+ int response_value = -1;
+ } result;
+
+ auto call = test_client.TestServerStreamRpc(
+ {.integer = 123, .status_code = 0},
+ [&result](const test::pwpb::TestStreamResponse::Message& response) {
+ result.active = true;
+ result.response_value = response.number;
+ },
+ [&result](Status status) {
+ result.active = false;
+ result.stream_status = status;
+ });
+
+ EXPECT_TRUE(call.active());
+
+ EXPECT_EQ(context.output().total_packets(), 1u);
+ auto packet =
+ static_cast<const internal::test::FakeChannelOutput&>(context.output())
+ .last_packet();
+ EXPECT_EQ(packet.channel_id(), context.channel().id());
+ EXPECT_EQ(packet.service_id(), kServiceId);
+ EXPECT_EQ(packet.method_id(), kMethodId);
+ PW_DECODE_PB(test::pwpb::TestRequest, sent_proto, packet.payload());
+ EXPECT_EQ(sent_proto.integer, 123);
+
+ PW_ENCODE_PB(
+ test::pwpb::TestStreamResponse, response, .chunk = {}, .number = 11u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(response));
+ EXPECT_TRUE(result.active);
+ EXPECT_EQ(result.response_value, 11);
+
+ EXPECT_EQ(OkStatus(), context.SendResponse(Status::NotFound()));
+ EXPECT_FALSE(result.active);
+ EXPECT_EQ(result.stream_status, Status::NotFound());
+}
+
+TEST(PwpbCodegen, Client_StaticMethod_InvokesUnaryRpcWithCallback) {
+ constexpr uint32_t kServiceId = internal::Hash("pw.rpc.test.TestService");
+ constexpr uint32_t kMethodId = internal::Hash("TestUnaryRpc");
+
+ ClientContextForTest<128, 99, kServiceId, kMethodId> context;
+
+ struct {
+ Status last_status = Status::Unknown();
+ int response_value = -1;
+ } result;
+
+ auto call = test::pw_rpc::pwpb::TestService::TestUnaryRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ [&result](const test::pwpb::TestResponse::Message& response,
+ Status status) {
+ result.last_status = status;
+ result.response_value = response.value;
+ });
+
+ EXPECT_TRUE(call.active());
+
+ EXPECT_EQ(context.output().total_packets(), 1u);
+ auto packet =
+ static_cast<const internal::test::FakeChannelOutput&>(context.output())
+ .last_packet();
+ EXPECT_EQ(packet.channel_id(), context.channel().id());
+ EXPECT_EQ(packet.service_id(), kServiceId);
+ EXPECT_EQ(packet.method_id(), kMethodId);
+ PW_DECODE_PB(test::pwpb::TestRequest, sent_proto, packet.payload());
+ EXPECT_EQ(sent_proto.integer, 123);
+
+ PW_ENCODE_PB(test::pwpb::TestResponse, response, .value = 42);
+ EXPECT_EQ(OkStatus(), context.SendResponse(OkStatus(), response));
+ EXPECT_EQ(result.last_status, OkStatus());
+ EXPECT_EQ(result.response_value, 42);
+}
+
+TEST(PwpbCodegen, Client_StaticMethod_InvokesServerStreamingRpcWithCallback) {
+ constexpr uint32_t kServiceId = internal::Hash("pw.rpc.test.TestService");
+ constexpr uint32_t kMethodId = internal::Hash("TestServerStreamRpc");
+
+ ClientContextForTest<128, 99, kServiceId, kMethodId> context;
+
+ struct {
+ bool active = true;
+ Status stream_status = Status::Unknown();
+ int response_value = -1;
+ } result;
+
+ auto call = test::pw_rpc::pwpb::TestService::TestServerStreamRpc(
+ context.client(),
+ context.channel().id(),
+ {.integer = 123, .status_code = 0},
+ [&result](const test::pwpb::TestStreamResponse::Message& response) {
+ result.active = true;
+ result.response_value = response.number;
+ },
+ [&result](Status status) {
+ result.active = false;
+ result.stream_status = status;
+ });
+
+ EXPECT_TRUE(call.active());
+
+ EXPECT_EQ(context.output().total_packets(), 1u);
+ auto packet =
+ static_cast<const internal::test::FakeChannelOutput&>(context.output())
+ .last_packet();
+ EXPECT_EQ(packet.channel_id(), context.channel().id());
+ EXPECT_EQ(packet.service_id(), kServiceId);
+ EXPECT_EQ(packet.method_id(), kMethodId);
+ PW_DECODE_PB(test::pwpb::TestRequest, sent_proto, packet.payload());
+ EXPECT_EQ(sent_proto.integer, 123);
+
+ PW_ENCODE_PB(
+ test::pwpb::TestStreamResponse, response, .chunk = {}, .number = 11u);
+ EXPECT_EQ(OkStatus(), context.SendServerStream(response));
+ EXPECT_TRUE(result.active);
+ EXPECT_EQ(result.response_value, 11);
+
+ EXPECT_EQ(OkStatus(), context.SendResponse(Status::NotFound()));
+ EXPECT_FALSE(result.active);
+ EXPECT_EQ(result.stream_status, Status::NotFound());
+}
+
+} // namespace
+} // namespace pw::rpc
+
+PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_rpc/pwpb/docs.rst b/pw_rpc/pwpb/docs.rst
new file mode 100644
index 000000000..a1332a5a7
--- /dev/null
+++ b/pw_rpc/pwpb/docs.rst
@@ -0,0 +1,261 @@
+.. _module-pw_rpc_pw_protobuf:
+
+-----------
+pw_protobuf
+-----------
+``pw_rpc`` can generate services which encode/decode RPC requests and responses
+as ``pw_protobuf`` message structs
+
+Usage
+=====
+Define a ``pw_proto_library`` containing the .proto file defining your service
+(and optionally other related protos), then depend on the ``pwpb_rpc``
+version of that library in the code implementing the service.
+
+.. code::
+
+ # chat/BUILD.gn
+
+ import("$dir_pw_build/target_types.gni")
+ import("$dir_pw_protobuf_compiler/proto.gni")
+
+ pw_proto_library("chat_protos") {
+ sources = [ "chat_protos/chat_service.proto" ]
+ }
+
+ # Library that implements the Chat service.
+ pw_source_set("chat_service") {
+ sources = [
+ "chat_service.cc",
+ "chat_service.h",
+ ]
+ public_deps = [ ":chat_protos.pwpb_rpc" ]
+ }
+
+A C++ header file is generated for each input .proto file, with the ``.proto``
+extension replaced by ``.rpc.pwpb.h``. For example, given the input file
+``chat_protos/chat_service.proto``, the generated header file will be placed
+at the include path ``"chat_protos/chat_service.rpc.pwpb.h"``.
+
+Generated code API
+==================
+All examples in this document use the following RPC service definition.
+
+.. code:: protobuf
+
+ // chat/chat_protos/chat_service.proto
+
+ syntax = "proto3";
+
+ service Chat {
+ // Returns information about a chatroom.
+ rpc GetRoomInformation(RoomInfoRequest) returns (RoomInfoResponse) {}
+
+ // Lists all of the users in a chatroom. The response is streamed as there
+ // may be a large amount of users.
+ rpc ListUsersInRoom(ListUsersRequest) returns (stream ListUsersResponse) {}
+
+ // Uploads a file, in chunks, to a chatroom.
+ rpc UploadFile(stream UploadFileRequest) returns (UploadFileResponse) {}
+
+ // Sends messages to a chatroom while receiving messages from other users.
+ rpc Chat(stream ChatMessage) returns (stream ChatMessage) {}
+ }
+
+Server-side
+-----------
+A C++ class is generated for each service in the .proto file. The class is
+located within a special ``pw_rpc::pwpb`` sub-namespace of the file's package.
+
+The generated class is a base class which must be derived to implement the
+service's methods. The base class is templated on the derived class.
+
+.. code:: c++
+
+ #include "chat_protos/chat_service.rpc.pwpb.h"
+
+ class ChatService final : public pw_rpc::pwpb::Chat::Service<ChatService> {
+ public:
+ // Implementations of the service's RPC methods; see below.
+ };
+
+Unary RPC
+^^^^^^^^^
+A unary RPC is implemented as a function which takes in the RPC's request struct
+and populates a response struct to send back, with a status indicating whether
+the request succeeded.
+
+.. code:: c++
+
+ pw::Status GetRoomInformation(const RoomInfoRequest::Message& request,
+ RoomInfoResponse::Message& response);
+
+Server streaming RPC
+^^^^^^^^^^^^^^^^^^^^
+A server streaming RPC receives the client's request message alongside a
+``ServerWriter``, used to stream back responses.
+
+.. code:: c++
+
+ void ListUsersInRoom(const ListUsersRequest::Message& request,
+ pw::rpc::ServerWriter<ListUsersResponse::Message>& writer);
+
+The ``ServerWriter`` object is movable, and remains active until it is manually
+closed or goes out of scope. The writer has a simple API to return responses:
+
+.. cpp:function:: Status PwpbServerWriter::Write(const T::Message& response)
+
+ Writes a single response message to the stream. The returned status indicates
+ whether the write was successful.
+
+.. cpp:function:: void PwpbServerWriter::Finish(Status status = OkStatus())
+
+ Closes the stream and sends back the RPC's overall status to the client.
+
+Once a ``ServerWriter`` has been closed, all future ``Write`` calls will fail.
+
+.. attention::
+
+ Make sure to use ``std::move`` when passing the ``ServerWriter`` around to
+ avoid accidentally closing it and ending the RPC.
+
+Client streaming RPC
+^^^^^^^^^^^^^^^^^^^^
+.. attention:: Supported, but the documentation is still under construction.
+
+Bidirectional streaming RPC
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. attention:: Supported, but the documentation is still under construction.
+
+Client-side
+-----------
+A corresponding client class is generated for every service defined in the proto
+file. To allow multiple types of clients to exist, it is placed under the
+``pw_rpc::pwpb`` namespace. The ``Client`` class is nested under
+``pw_rpc::pwpb::ServiceName``. For example, the ``Chat`` service would create
+``pw_rpc::pwpb::Chat::Client``.
+
+Service clients are instantiated with a reference to the RPC client through
+which they will send requests, and the channel ID they will use.
+
+.. code-block:: c++
+
+ // Nested under pw_rpc::pwpb::ServiceName.
+ class Client {
+ public:
+ Client(::pw::rpc::Client& client, uint32_t channel_id);
+
+ pw::rpc::PwpbUnaryReceiver<RoomInfoResponse::Message> GetRoomInformation(
+ const RoomInfoRequest::Message& request,
+ ::pw::Function<void(Status, const RoomInfoResponse::Message&)> on_response,
+ ::pw::Function<void(Status)> on_rpc_error = nullptr);
+
+ // ...and more (see below).
+ };
+
+RPCs can also be invoked individually as free functions:
+
+.. code-block:: c++
+
+ pw::rpc::PwpbUnaryReceiver<RoomInfoResponse::Message> call = pw_rpc::pwpb::Chat::GetRoomInformation(
+ client, channel_id, request, on_response, on_rpc_error);
+
+The client class has member functions for each method defined within the
+service's protobuf descriptor. The arguments to these methods vary depending on
+the type of RPC. Each method returns a client call object which stores the
+context of the ongoing RPC call. For more information on call objects, refer to
+the :ref:`core RPC docs <module-pw_rpc-making-calls>`.
+
+.. admonition:: Callback invocation
+
+ RPC callbacks are invoked synchronously from ``Client::ProcessPacket``.
+
+Method APIs
+^^^^^^^^^^^
+The arguments provided when invoking a method depend on its type.
+
+Unary RPC
+~~~~~~~~~
+A unary RPC call takes the request struct and a callback to invoke when a
+response is received. The callback receives the RPC's status and response
+struct.
+
+An optional second callback can be provided to handle internal errors.
+
+.. code-block:: c++
+
+ pw::rpc::PwpbUnaryReceiver<RoomInfoResponse::Message> GetRoomInformation(
+ const RoomInfoRequest::Message& request,
+ ::pw::Function<void(const RoomInfoResponse::Message&, Status)> on_response,
+ ::pw::Function<void(Status)> on_rpc_error = nullptr);
+
+Server streaming RPC
+~~~~~~~~~~~~~~~~~~~~
+A server streaming RPC call takes the initial request struct and two callbacks.
+The first is invoked on every stream response received, and the second is
+invoked once the stream is complete with its overall status.
+
+An optional third callback can be provided to handle internal errors.
+
+.. code-block:: c++
+
+ pw::rpc::PwpbClientReader<ListUsersResponse::Message> ListUsersInRoom(
+ const ListUsersRequest::Message& request,
+ ::pw::Function<void(const ListUsersResponse::Message&)> on_response,
+ ::pw::Function<void(Status)> on_stream_end,
+ ::pw::Function<void(Status)> on_rpc_error = nullptr);
+
+Client streaming RPC
+~~~~~~~~~~~~~~~~~~~~
+.. attention:: Supported, but the documentation is still under construction.
+
+Bidirectional streaming RPC
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+.. attention:: Supported, but the documentation is still under construction.
+
+Example usage
+^^^^^^^^^^^^^
+The following example demonstrates how to call an RPC method using a pw_protobuf
+service client and receive the response.
+
+.. code-block:: c++
+
+ #include "chat_protos/chat_service.rpc.pwpb.h"
+
+ namespace {
+
+ using ChatClient = pw_rpc::pwpb::Chat::Client;
+
+ MyChannelOutput output;
+ pw::rpc::Channel channels[] = {pw::rpc::Channel::Create<1>(&output)};
+ pw::rpc::Client client(channels);
+
+ // Callback function for GetRoomInformation.
+ void LogRoomInformation(const RoomInfoResponse::Message& response,
+ Status status);
+
+ } // namespace
+
+ void InvokeSomeRpcs() {
+ // Instantiate a service client to call Chat service methods on channel 1.
+ ChatClient chat_client(client, 1);
+
+ // The RPC will remain active as long as `call` is alive.
+ auto call = chat_client.GetRoomInformation(
+ {.room = "pigweed"}, LogRoomInformation);
+ if (!call.active()) {
+ // The invocation may fail. This could occur due to an invalid channel ID,
+ // for example. The failure status is forwarded to the to call's
+ // on_rpc_error callback.
+ return;
+ }
+
+ // For simplicity, block until the call completes. An actual implementation
+ // would likely std::move the call somewhere to keep it active while doing
+ // other work.
+ while (call.active()) {
+ Wait();
+ }
+
+ // Do other stuff now that we have the room information.
+ }
diff --git a/pw_rpc/pwpb/echo_service_test.cc b/pw_rpc/pwpb/echo_service_test.cc
new file mode 100644
index 000000000..80651c65c
--- /dev/null
+++ b/pw_rpc/pwpb/echo_service_test.cc
@@ -0,0 +1,41 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_rpc/echo_service_pwpb.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+
+namespace pw::rpc {
+namespace {
+
+TEST(EchoService, Echo_EchoesRequestMessage) {
+ PW_PWPB_TEST_METHOD_CONTEXT(EchoService, Echo) context;
+ ASSERT_EQ(context.call({"Hello, world"}), OkStatus());
+ EXPECT_EQ(std::string_view(context.response().msg.data(),
+ context.response().msg.size()),
+ "Hello, world");
+}
+
+TEST(EchoService, Echo_EmptyRequest) {
+ PW_PWPB_TEST_METHOD_CONTEXT(EchoService, Echo) context;
+ ASSERT_EQ(context.call({}), OkStatus());
+ EXPECT_EQ(std::string_view(context.response().msg.data(),
+ context.response().msg.size()),
+ "");
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/fake_channel_output_test.cc b/pw_rpc/pwpb/fake_channel_output_test.cc
new file mode 100644
index 000000000..039092b9f
--- /dev/null
+++ b/pw_rpc/pwpb/fake_channel_output_test.cc
@@ -0,0 +1,98 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/pwpb/fake_channel_output.h"
+
+#include <array>
+#include <cstddef>
+#include <memory>
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/channel.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
+
+namespace pw::rpc::internal::test {
+namespace {
+
+using ::pw::rpc::internal::pwpb::PacketType;
+using ::pw::rpc::test::pw_rpc::pwpb::TestService;
+using Info = ::pw::rpc::internal::MethodInfo<TestService::TestUnaryRpc>;
+
+TEST(PwpbFakeChannelOutput, Requests) {
+ PwpbFakeChannelOutput<1> output;
+
+ std::byte payload_buffer[32] = {};
+ constexpr Info::Request request{.integer = -100, .status_code = 5};
+ const StatusWithSize payload =
+ Info::serde().request().Encode(request, payload_buffer);
+ ASSERT_TRUE(payload.ok());
+
+ std::array<std::byte, 128> buffer;
+
+ auto packet = Packet(PacketType::REQUEST,
+ 1,
+ Info::kServiceId,
+ Info::kMethodId,
+ 999,
+ span(payload_buffer, payload.size()))
+ .Encode(buffer);
+ ASSERT_TRUE(packet.ok());
+
+ ASSERT_EQ(OkStatus(), output.Send(span(buffer).first(packet->size())));
+
+ ASSERT_TRUE(output.responses<TestService::TestUnaryRpc>().empty());
+ ASSERT_EQ(output.requests<TestService::TestUnaryRpc>().size(), 1u);
+
+ Info::Request sent = output.requests<TestService::TestUnaryRpc>().front();
+ EXPECT_EQ(sent.integer, -100);
+ EXPECT_EQ(sent.status_code, 5u);
+}
+
+TEST(PwpbFakeChannelOutput, Responses) {
+ PwpbFakeChannelOutput<1> output;
+
+ std::byte payload_buffer[32] = {};
+ const Info::Response response{.value = -9876};
+ const StatusWithSize payload =
+ Info::serde().response().Encode(response, payload_buffer);
+ ASSERT_TRUE(payload.ok());
+
+ std::array<std::byte, 128> buffer;
+
+ auto packet = Packet(PacketType::RESPONSE,
+ 1,
+ Info::kServiceId,
+ Info::kMethodId,
+ 999,
+ span(payload_buffer, payload.size()))
+ .Encode(buffer);
+ ASSERT_TRUE(packet.ok());
+
+ ASSERT_EQ(OkStatus(), output.Send(span(buffer).first(packet->size())));
+
+ ASSERT_EQ(output.responses<TestService::TestUnaryRpc>().size(), 1u);
+ ASSERT_TRUE(output.requests<TestService::TestUnaryRpc>().empty());
+
+ Info::Response sent = output.responses<TestService::TestUnaryRpc>().front();
+ EXPECT_EQ(sent.value, -9876);
+}
+
+} // namespace
+} // namespace pw::rpc::internal::test
+
+PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_rpc/pwpb/method_info_test.cc b/pw_rpc/pwpb/method_info_test.cc
new file mode 100644
index 000000000..8102be561
--- /dev/null
+++ b/pw_rpc/pwpb/method_info_test.cc
@@ -0,0 +1,62 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/internal/method_info.h"
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/method_info_tester.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+#include "pw_status/status.h"
+
+namespace pw::rpc {
+
+namespace test {
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+} // namespace test
+
+namespace {
+
+class TestService final
+ : public test::pw_rpc::pwpb::TestService::Service<TestService> {
+ public:
+ Status TestUnaryRpc(const test::TestRequest::Message&,
+ test::TestResponse::Message&) {
+ return OkStatus();
+ }
+
+ void TestAnotherUnaryRpc(const test::TestRequest::Message&,
+ PwpbUnaryResponder<test::TestResponse::Message>&) {}
+
+ static void TestServerStreamRpc(
+ const test::TestRequest::Message&,
+ ServerWriter<test::TestStreamResponse::Message>&) {}
+
+ void TestClientStreamRpc(ServerReader<test::TestRequest::Message,
+ test::TestStreamResponse::Message>&) {}
+
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<test::TestRequest::Message,
+ test::TestStreamResponse::Message>&) {}
+};
+
+static_assert(
+ internal::MethodInfoTests<test::pw_rpc::pwpb::TestService, TestService>()
+ .Pass());
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/method_lookup_test.cc b/pw_rpc/pwpb/method_lookup_test.cc
new file mode 100644
index 000000000..d0a578e6a
--- /dev/null
+++ b/pw_rpc/pwpb/method_lookup_test.cc
@@ -0,0 +1,161 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+#include "pw_rpc/raw/test_method_context.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+
+namespace pw::rpc {
+namespace {
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+class MixedService1
+ : public test::pw_rpc::pwpb::TestService::Service<MixedService1> {
+ public:
+ void TestUnaryRpc(ConstByteSpan, RawUnaryResponder& responder) {
+ std::byte response[5] = {};
+ ASSERT_EQ(OkStatus(), responder.Finish(response, OkStatus()));
+ }
+
+ void TestAnotherUnaryRpc(const TestRequest::Message&,
+ PwpbUnaryResponder<TestResponse::Message>&) {
+ called_async_unary_method = true;
+ }
+
+ void TestServerStreamRpc(const TestRequest::Message&,
+ ServerWriter<TestStreamResponse::Message>&) {
+ called_server_streaming_method = true;
+ }
+
+ void TestClientStreamRpc(RawServerReader&) {
+ called_client_streaming_method = true;
+ }
+
+ void TestBidirectionalStreamRpc(
+ ServerReaderWriter<TestRequest::Message, TestStreamResponse::Message>&) {
+ called_bidirectional_streaming_method = true;
+ }
+
+ bool called_async_unary_method = false;
+ bool called_server_streaming_method = false;
+ bool called_client_streaming_method = false;
+ bool called_bidirectional_streaming_method = false;
+};
+
+class MixedService2
+ : public test::pw_rpc::pwpb::TestService::Service<MixedService2> {
+ public:
+ Status TestUnaryRpc(const TestRequest::Message&, TestResponse::Message&) {
+ return Status::Unauthenticated();
+ }
+
+ void TestAnotherUnaryRpc(ConstByteSpan, RawUnaryResponder&) {
+ called_async_unary_method = true;
+ }
+
+ void TestServerStreamRpc(ConstByteSpan, RawServerWriter&) {
+ called_server_streaming_method = true;
+ }
+
+ void TestClientStreamRpc(
+ ServerReader<TestRequest::Message, TestStreamResponse::Message>&) {
+ called_client_streaming_method = true;
+ }
+
+ void TestBidirectionalStreamRpc(RawServerReaderWriter&) {
+ called_bidirectional_streaming_method = true;
+ }
+
+ bool called_async_unary_method = false;
+ bool called_server_streaming_method = false;
+ bool called_client_streaming_method = false;
+ bool called_bidirectional_streaming_method = false;
+};
+
+TEST(MixedService1, CallRawMethod_SyncUnary) {
+ PW_RAW_TEST_METHOD_CONTEXT(MixedService1, TestUnaryRpc) context;
+ context.call({});
+ EXPECT_EQ(OkStatus(), context.status());
+ EXPECT_EQ(5u, context.response().size());
+}
+
+TEST(MixedService1, CallPwpbMethod_AsyncUnary) {
+ PW_PWPB_TEST_METHOD_CONTEXT(MixedService1, TestAnotherUnaryRpc) context;
+ ASSERT_FALSE(context.service().called_async_unary_method);
+ context.call({});
+ EXPECT_TRUE(context.service().called_async_unary_method);
+}
+
+TEST(MixedService1, CallPwpbMethod_ServerStreaming) {
+ PW_PWPB_TEST_METHOD_CONTEXT(MixedService1, TestServerStreamRpc) context;
+ ASSERT_FALSE(context.service().called_server_streaming_method);
+ context.call({});
+ EXPECT_TRUE(context.service().called_server_streaming_method);
+}
+
+TEST(MixedService1, CallRawMethod_ClientStreaming) {
+ PW_RAW_TEST_METHOD_CONTEXT(MixedService1, TestClientStreamRpc) context;
+ ASSERT_FALSE(context.service().called_client_streaming_method);
+ context.call();
+ EXPECT_TRUE(context.service().called_client_streaming_method);
+}
+
+TEST(MixedService1, CallPwpbMethod_BidirectionalStreaming) {
+ PW_PWPB_TEST_METHOD_CONTEXT(MixedService1, TestBidirectionalStreamRpc)
+ context;
+ ASSERT_FALSE(context.service().called_bidirectional_streaming_method);
+ context.call();
+ EXPECT_TRUE(context.service().called_bidirectional_streaming_method);
+}
+
+TEST(MixedService2, CallPwpbMethod_SyncUnary) {
+ PW_PWPB_TEST_METHOD_CONTEXT(MixedService2, TestUnaryRpc) context;
+ Status status = context.call({});
+ EXPECT_EQ(Status::Unauthenticated(), status);
+}
+
+TEST(MixedService2, CallRawMethod_AsyncUnary) {
+ PW_RAW_TEST_METHOD_CONTEXT(MixedService2, TestAnotherUnaryRpc) context;
+ ASSERT_FALSE(context.service().called_async_unary_method);
+ context.call({});
+ EXPECT_TRUE(context.service().called_async_unary_method);
+}
+
+TEST(MixedService2, CallRawMethod_ServerStreaming) {
+ PW_RAW_TEST_METHOD_CONTEXT(MixedService2, TestServerStreamRpc) context;
+ ASSERT_FALSE(context.service().called_server_streaming_method);
+ context.call({});
+ EXPECT_TRUE(context.service().called_server_streaming_method);
+}
+
+TEST(MixedService2, CallPwpbMethod_ClientStreaming) {
+ PW_PWPB_TEST_METHOD_CONTEXT(MixedService2, TestClientStreamRpc) context;
+ ASSERT_FALSE(context.service().called_client_streaming_method);
+ context.call();
+ EXPECT_TRUE(context.service().called_client_streaming_method);
+}
+
+TEST(MixedService2, CallRawMethod_BidirectionalStreaming) {
+ PW_RAW_TEST_METHOD_CONTEXT(MixedService2, TestBidirectionalStreamRpc) context;
+ ASSERT_FALSE(context.service().called_bidirectional_streaming_method);
+ context.call();
+ EXPECT_TRUE(context.service().called_bidirectional_streaming_method);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/method_test.cc b/pw_rpc/pwpb/method_test.cc
new file mode 100644
index 000000000..76f728f59
--- /dev/null
+++ b/pw_rpc/pwpb/method_test.cc
@@ -0,0 +1,425 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/pwpb/internal/method.h"
+
+#include <array>
+
+#include "gtest/gtest.h"
+#include "pw_containers/algorithm.h"
+#include "pw_rpc/internal/lock.h"
+#include "pw_rpc/internal/method_impl_tester.h"
+#include "pw_rpc/internal/test_utils.h"
+#include "pw_rpc/pwpb/internal/method_union.h"
+#include "pw_rpc/service.h"
+#include "pw_rpc_pwpb_private/internal_test_utils.h"
+#include "pw_rpc_test_protos/test.pwpb.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
+
+namespace pw::rpc::internal {
+namespace {
+
+using std::byte;
+
+struct FakePb {};
+
+// Create a fake service for use with the MethodImplTester.
+class TestPwpbService final : public Service {
+ public:
+ // Unary signatures
+
+ Status Unary(const FakePb&, FakePb&) { return Status(); }
+
+ static Status StaticUnary(const FakePb&, FakePb&) { return Status(); }
+
+ void AsyncUnary(const FakePb&, PwpbUnaryResponder<FakePb>&) {}
+
+ static void StaticAsyncUnary(const FakePb&, PwpbUnaryResponder<FakePb>&) {}
+
+ Status UnaryWrongArg(FakePb&, FakePb&) { return Status(); }
+
+ static void StaticUnaryVoidReturn(const FakePb&, FakePb&) {}
+
+ // Server streaming signatures
+
+ void ServerStreaming(const FakePb&, PwpbServerWriter<FakePb>&) {}
+
+ static void StaticServerStreaming(const FakePb&, PwpbServerWriter<FakePb>&) {}
+
+ int ServerStreamingBadReturn(const FakePb&, PwpbServerWriter<FakePb>&) {
+ return 5;
+ }
+
+ static void StaticServerStreamingMissingArg(PwpbServerWriter<FakePb>&) {}
+
+ // Client streaming signatures
+
+ void ClientStreaming(PwpbServerReader<FakePb, FakePb>&) {}
+
+ static void StaticClientStreaming(PwpbServerReader<FakePb, FakePb>&) {}
+
+ int ClientStreamingBadReturn(PwpbServerReader<FakePb, FakePb>&) { return 0; }
+
+ static void StaticClientStreamingMissingArg() {}
+
+ // Bidirectional streaming signatures
+
+ void BidirectionalStreaming(PwpbServerReaderWriter<FakePb, FakePb>&) {}
+
+ static void StaticBidirectionalStreaming(
+ PwpbServerReaderWriter<FakePb, FakePb>&) {}
+
+ int BidirectionalStreamingBadReturn(PwpbServerReaderWriter<FakePb, FakePb>&) {
+ return 0;
+ }
+
+ static void StaticBidirectionalStreamingMissingArg() {}
+};
+
+struct WrongPb;
+
+// Test matches() rejects incorrect request/response types.
+// clang-format off
+static_assert(!PwpbMethod::template matches<&TestPwpbService::Unary, WrongPb, FakePb>());
+static_assert(!PwpbMethod::template matches<&TestPwpbService::Unary, FakePb, WrongPb>());
+static_assert(!PwpbMethod::template matches<&TestPwpbService::Unary, WrongPb, WrongPb>());
+static_assert(!PwpbMethod::template matches<&TestPwpbService::StaticUnary, FakePb, WrongPb>());
+
+static_assert(!PwpbMethod::template matches<&TestPwpbService::ServerStreaming, WrongPb, FakePb>());
+static_assert(!PwpbMethod::template matches<&TestPwpbService::StaticServerStreaming, FakePb, WrongPb>());
+
+static_assert(!PwpbMethod::template matches<&TestPwpbService::ClientStreaming, WrongPb, FakePb>());
+static_assert(!PwpbMethod::template matches<&TestPwpbService::StaticClientStreaming, FakePb, WrongPb>());
+
+static_assert(!PwpbMethod::template matches<&TestPwpbService::BidirectionalStreaming, WrongPb, FakePb>());
+static_assert(!PwpbMethod::template matches<&TestPwpbService::StaticBidirectionalStreaming, FakePb, WrongPb>());
+// clang-format on
+
+static_assert(MethodImplTests<PwpbMethod, TestPwpbService>().Pass(
+ MatchesTypes<FakePb, FakePb>(),
+ std::tuple<const PwpbMethodSerde&>(kPwpbMethodSerde<nullptr, nullptr>)));
+
+template <typename Impl>
+class FakeServiceBase : public Service {
+ public:
+ FakeServiceBase(uint32_t id) : Service(id, kMethods) {}
+
+ static constexpr std::array<PwpbMethodUnion, 5> kMethods = {
+ PwpbMethod::SynchronousUnary<&Impl::DoNothing>(
+ 10u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::Empty::kMessageFields,
+ &pw::rpc::test::pwpb::Empty::kMessageFields>),
+ PwpbMethod::AsynchronousUnary<&Impl::AddFive>(
+ 11u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::TestRequest::kMessageFields,
+ &pw::rpc::test::pwpb::TestResponse::kMessageFields>),
+ PwpbMethod::ServerStreaming<&Impl::StartStream>(
+ 12u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::TestRequest::kMessageFields,
+ &pw::rpc::test::pwpb::TestResponse::kMessageFields>),
+ PwpbMethod::ClientStreaming<&Impl::ClientStream>(
+ 13u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::TestRequest::kMessageFields,
+ &pw::rpc::test::pwpb::TestResponse::kMessageFields>),
+ PwpbMethod::BidirectionalStreaming<&Impl::BidirectionalStream>(
+ 14u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::TestRequest::kMessageFields,
+ &pw::rpc::test::pwpb::TestResponse::kMessageFields>),
+ };
+};
+
+class FakeService : public FakeServiceBase<FakeService> {
+ public:
+ FakeService(uint32_t id) : FakeServiceBase(id) {}
+
+ Status DoNothing(const pw::rpc::test::pwpb::Empty::Message&,
+ pw::rpc::test::pwpb::Empty::Message&) {
+ return Status::Unknown();
+ }
+
+ void AddFive(const pw::rpc::test::pwpb::TestRequest::Message& request,
+ PwpbUnaryResponder<pw::rpc::test::pwpb::TestResponse::Message>&
+ responder) {
+ last_request = request;
+
+ if (fail_to_encode_async_unary_response) {
+ pw::rpc::test::pwpb::TestResponse::Message response = {};
+ response.repeated_field.SetEncoder(
+ [](const pw::rpc::test::pwpb::TestResponse::StreamEncoder&) {
+ return Status::Internal();
+ });
+ ASSERT_EQ(OkStatus(), responder.Finish(response, Status::NotFound()));
+ } else {
+ ASSERT_EQ(
+ OkStatus(),
+ responder.Finish({.value = static_cast<int32_t>(request.integer + 5)},
+ Status::Unauthenticated()));
+ }
+ }
+
+ void StartStream(
+ const pw::rpc::test::pwpb::TestRequest::Message& request,
+ PwpbServerWriter<pw::rpc::test::pwpb::TestResponse::Message>& writer) {
+ last_request = request;
+ last_writer = std::move(writer);
+ }
+
+ void ClientStream(
+ PwpbServerReader<pw::rpc::test::pwpb::TestRequest::Message,
+ pw::rpc::test::pwpb::TestResponse::Message>& reader) {
+ last_reader = std::move(reader);
+ }
+
+ void BidirectionalStream(
+ PwpbServerReaderWriter<pw::rpc::test::pwpb::TestRequest::Message,
+ pw::rpc::test::pwpb::TestResponse::Message>&
+ reader_writer) {
+ last_reader_writer = std::move(reader_writer);
+ }
+
+ bool fail_to_encode_async_unary_response = false;
+
+ pw::rpc::test::pwpb::TestRequest::Message last_request;
+ PwpbServerWriter<pw::rpc::test::pwpb::TestResponse::Message> last_writer;
+ PwpbServerReader<pw::rpc::test::pwpb::TestRequest::Message,
+ pw::rpc::test::pwpb::TestResponse::Message>
+ last_reader;
+ PwpbServerReaderWriter<pw::rpc::test::pwpb::TestRequest::Message,
+ pw::rpc::test::pwpb::TestResponse::Message>
+ last_reader_writer;
+};
+
+constexpr const PwpbMethod& kSyncUnary =
+ std::get<0>(FakeServiceBase<FakeService>::kMethods).pwpb_method();
+constexpr const PwpbMethod& kAsyncUnary =
+ std::get<1>(FakeServiceBase<FakeService>::kMethods).pwpb_method();
+constexpr const PwpbMethod& kServerStream =
+ std::get<2>(FakeServiceBase<FakeService>::kMethods).pwpb_method();
+constexpr const PwpbMethod& kClientStream =
+ std::get<3>(FakeServiceBase<FakeService>::kMethods).pwpb_method();
+constexpr const PwpbMethod& kBidirectionalStream =
+ std::get<4>(FakeServiceBase<FakeService>::kMethods).pwpb_method();
+
+TEST(PwpbMethod, AsyncUnaryRpc_SendsResponse) {
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestRequest,
+ request,
+ .integer = 123,
+ .status_code = 0);
+
+ ServerContextForTest<FakeService> context(kAsyncUnary);
+ rpc_lock().lock();
+ kAsyncUnary.Invoke(context.get(), context.request(request));
+
+ const Packet& response = context.output().last_packet();
+ EXPECT_EQ(response.status(), Status::Unauthenticated());
+
+ // Field 1 (encoded as 1 << 3) with 128 as the value.
+ constexpr std::byte expected[]{
+ std::byte{0x08}, std::byte{0x80}, std::byte{0x01}};
+
+ EXPECT_EQ(sizeof(expected), response.payload().size());
+ EXPECT_EQ(0,
+ std::memcmp(expected, response.payload().data(), sizeof(expected)));
+
+ EXPECT_EQ(123, context.service().last_request.integer);
+}
+
+TEST(PwpbMethod, SyncUnaryRpc_InvalidPayload_SendsError) {
+ std::array<byte, 8> bad_payload{byte{0xFF}, byte{0xAA}, byte{0xDD}};
+
+ ServerContextForTest<FakeService> context(kSyncUnary);
+ rpc_lock().lock();
+ kSyncUnary.Invoke(context.get(), context.request(bad_payload));
+
+ const Packet& packet = context.output().last_packet();
+ EXPECT_EQ(pwpb::PacketType::SERVER_ERROR, packet.type());
+ EXPECT_EQ(Status::DataLoss(), packet.status());
+ EXPECT_EQ(context.service_id(), packet.service_id());
+ EXPECT_EQ(kSyncUnary.id(), packet.method_id());
+}
+
+TEST(PwpbMethod, AsyncUnaryRpc_ResponseEncodingFails_SendsInternalError) {
+ constexpr int64_t value = 0x7FFFFFFF'FFFFFF00ll;
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestRequest,
+ request,
+ .integer = value,
+ .status_code = 0);
+
+ ServerContextForTest<FakeService> context(kAsyncUnary);
+ context.service().fail_to_encode_async_unary_response = true;
+
+ rpc_lock().lock();
+ kAsyncUnary.Invoke(context.get(), context.request(request));
+
+ const Packet& packet = context.output().last_packet();
+ EXPECT_EQ(pwpb::PacketType::SERVER_ERROR, packet.type());
+ EXPECT_EQ(Status::Internal(), packet.status());
+ EXPECT_EQ(context.service_id(), packet.service_id());
+ EXPECT_EQ(kAsyncUnary.id(), packet.method_id());
+
+ EXPECT_EQ(value, context.service().last_request.integer);
+}
+
+TEST(PwpbMethod, ServerStreamingRpc_SendsNothingWhenInitiallyCalled) {
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestRequest,
+ request,
+ .integer = 555,
+ .status_code = 0);
+
+ ServerContextForTest<FakeService> context(kServerStream);
+
+ rpc_lock().lock();
+ kServerStream.Invoke(context.get(), context.request(request));
+
+ EXPECT_EQ(0u, context.output().total_packets());
+ EXPECT_EQ(555, context.service().last_request.integer);
+}
+
+TEST(PwpbMethod, ServerWriter_SendsResponse) {
+ ServerContextForTest<FakeService> context(kServerStream);
+
+ rpc_lock().lock();
+ kServerStream.Invoke(context.get(), context.request({}));
+
+ EXPECT_EQ(OkStatus(), context.service().last_writer.Write({.value = 100}));
+
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestResponse, payload, .value = 100);
+ std::array<byte, 128> encoded_response = {};
+ auto encoded = context.server_stream(payload).Encode(encoded_response);
+ ASSERT_EQ(OkStatus(), encoded.status());
+
+ ConstByteSpan sent_payload = context.output().last_packet().payload();
+ EXPECT_TRUE(pw::containers::Equal(payload, sent_payload));
+}
+
+TEST(PwpbMethod, ServerWriter_WriteWhenClosed_ReturnsFailedPrecondition) {
+ ServerContextForTest<FakeService> context(kServerStream);
+
+ rpc_lock().lock();
+ kServerStream.Invoke(context.get(), context.request({}));
+
+ EXPECT_EQ(OkStatus(), context.service().last_writer.Finish());
+ EXPECT_TRUE(context.service()
+ .last_writer.Write({.value = 100})
+ .IsFailedPrecondition());
+}
+
+TEST(PwpbMethod, ServerWriter_WriteAfterMoved_ReturnsFailedPrecondition) {
+ ServerContextForTest<FakeService> context(kServerStream);
+
+ rpc_lock().lock();
+ kServerStream.Invoke(context.get(), context.request({}));
+ PwpbServerWriter<pw::rpc::test::pwpb::TestResponse::Message> new_writer =
+ std::move(context.service().last_writer);
+
+ EXPECT_EQ(OkStatus(), new_writer.Write({.value = 100}));
+
+ EXPECT_EQ(Status::FailedPrecondition(),
+ context.service().last_writer.Write({.value = 100}));
+ EXPECT_EQ(Status::FailedPrecondition(),
+ context.service().last_writer.Finish());
+
+ EXPECT_EQ(OkStatus(), new_writer.Finish());
+}
+
+TEST(PwpbMethod, ServerStreamingRpc_ResponseEncodingFails_InternalError) {
+ ServerContextForTest<FakeService> context(kServerStream);
+
+ rpc_lock().lock();
+ kServerStream.Invoke(context.get(), context.request({}));
+
+ EXPECT_EQ(OkStatus(), context.service().last_writer.Write({}));
+
+ pw::rpc::test::pwpb::TestResponse::Message response = {};
+ response.repeated_field.SetEncoder(
+ [](const pw::rpc::test::pwpb::TestResponse::StreamEncoder&) {
+ return Status::Internal();
+ });
+ EXPECT_EQ(Status::Internal(), context.service().last_writer.Write(response));
+}
+
+TEST(PwpbMethod, ServerReader_HandlesRequests) {
+ ServerContextForTest<FakeService> context(kClientStream);
+
+ rpc_lock().lock();
+ kClientStream.Invoke(context.get(), context.request({}));
+
+ pw::rpc::test::pwpb::TestRequest::Message request_struct{};
+ context.service().last_reader.set_on_next(
+ [&request_struct](const pw::rpc::test::pwpb::TestRequest::Message& req) {
+ request_struct = req;
+ });
+
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestRequest,
+ request,
+ .integer = 1 << 30,
+ .status_code = 9);
+ std::array<byte, 128> encoded_request = {};
+ auto encoded = context.client_stream(request).Encode(encoded_request);
+ ASSERT_EQ(OkStatus(), encoded.status());
+ ASSERT_EQ(OkStatus(), context.server().ProcessPacket(*encoded));
+
+ EXPECT_EQ(request_struct.integer, 1 << 30);
+ EXPECT_EQ(request_struct.status_code, 9u);
+}
+
+TEST(PwpbMethod, ServerReaderWriter_WritesResponses) {
+ ServerContextForTest<FakeService> context(kBidirectionalStream);
+
+ rpc_lock().lock();
+ kBidirectionalStream.Invoke(context.get(), context.request({}));
+
+ EXPECT_EQ(OkStatus(),
+ context.service().last_reader_writer.Write({.value = 100}));
+
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestResponse, payload, .value = 100);
+ std::array<byte, 128> encoded_response = {};
+ auto encoded = context.server_stream(payload).Encode(encoded_response);
+ ASSERT_EQ(OkStatus(), encoded.status());
+
+ ConstByteSpan sent_payload = context.output().last_packet().payload();
+ EXPECT_TRUE(pw::containers::Equal(payload, sent_payload));
+}
+
+TEST(PwpbMethod, ServerReaderWriter_HandlesRequests) {
+ ServerContextForTest<FakeService> context(kBidirectionalStream);
+
+ rpc_lock().lock();
+ kBidirectionalStream.Invoke(context.get(), context.request({}));
+
+ pw::rpc::test::pwpb::TestRequest::Message request_struct{};
+ context.service().last_reader_writer.set_on_next(
+ [&request_struct](const pw::rpc::test::pwpb::TestRequest::Message& req) {
+ request_struct = req;
+ });
+
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestRequest,
+ request,
+ .integer = 1 << 29,
+ .status_code = 8);
+ std::array<byte, 128> encoded_request = {};
+ auto encoded = context.client_stream(request).Encode(encoded_request);
+ ASSERT_EQ(OkStatus(), encoded.status());
+ ASSERT_EQ(OkStatus(), context.server().ProcessPacket(*encoded));
+
+ EXPECT_EQ(request_struct.integer, 1 << 29);
+ EXPECT_EQ(request_struct.status_code, 8u);
+}
+
+} // namespace
+} // namespace pw::rpc::internal
+
+PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_rpc/pwpb/method_union_test.cc b/pw_rpc/pwpb/method_union_test.cc
new file mode 100644
index 000000000..f474f15e1
--- /dev/null
+++ b/pw_rpc/pwpb/method_union_test.cc
@@ -0,0 +1,175 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/pwpb/internal/method_union.h"
+
+#include <array>
+
+#include "gtest/gtest.h"
+#include "pw_rpc/internal/test_utils.h"
+#include "pw_rpc/service.h"
+#include "pw_rpc_pwpb_private/internal_test_utils.h"
+#include "pw_rpc_test_protos/test.pwpb.h"
+
+namespace pw::rpc::internal {
+namespace {
+
+template <typename Implementation>
+class FakeGeneratedService : public Service {
+ public:
+ constexpr FakeGeneratedService(uint32_t id) : Service(id, kMethods) {}
+
+ static constexpr std::array<PwpbMethodUnion, 4> kMethods = {
+ GetPwpbOrRawMethodFor<&Implementation::DoNothing,
+ MethodType::kUnary,
+ pw::rpc::test::pwpb::Empty::Message,
+ pw::rpc::test::pwpb::Empty::Message>(
+ 10u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::Empty::kMessageFields,
+ &pw::rpc::test::pwpb::Empty::kMessageFields>),
+ GetPwpbOrRawMethodFor<&Implementation::RawStream,
+ MethodType::kServerStreaming,
+ pw::rpc::test::pwpb::TestRequest::Message,
+ pw::rpc::test::pwpb::TestResponse::Message>(
+ 11u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::TestRequest::kMessageFields,
+ &pw::rpc::test::pwpb::TestResponse::kMessageFields>),
+ GetPwpbOrRawMethodFor<&Implementation::AddFive,
+ MethodType::kUnary,
+ pw::rpc::test::pwpb::TestRequest::Message,
+ pw::rpc::test::pwpb::TestResponse::Message>(
+ 12u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::TestRequest::kMessageFields,
+ &pw::rpc::test::pwpb::TestResponse::kMessageFields>),
+ GetPwpbOrRawMethodFor<&Implementation::StartStream,
+ MethodType::kServerStreaming,
+ pw::rpc::test::pwpb::TestRequest::Message,
+ pw::rpc::test::pwpb::TestResponse::Message>(
+ 13u,
+ kPwpbMethodSerde<&pw::rpc::test::pwpb::TestRequest::kMessageFields,
+ &pw::rpc::test::pwpb::TestResponse::kMessageFields>),
+ };
+};
+
+class FakeGeneratedServiceImpl
+ : public FakeGeneratedService<FakeGeneratedServiceImpl> {
+ public:
+ FakeGeneratedServiceImpl(uint32_t id) : FakeGeneratedService(id) {}
+
+ Status AddFive(const pw::rpc::test::pwpb::TestRequest::Message& request,
+ pw::rpc::test::pwpb::TestResponse::Message& response) {
+ last_request = request;
+ response.value = request.integer + 5;
+ return Status::Unauthenticated();
+ }
+
+ void DoNothing(ConstByteSpan, RawUnaryResponder& responder) {
+ ASSERT_EQ(OkStatus(), responder.Finish({}, Status::Unknown()));
+ }
+
+ void RawStream(ConstByteSpan, RawServerWriter& writer) {
+ last_raw_writer = std::move(writer);
+ }
+
+ void StartStream(
+ const pw::rpc::test::pwpb::TestRequest::Message& request,
+ PwpbServerWriter<pw::rpc::test::pwpb::TestResponse::Message>& writer) {
+ last_request = request;
+ last_writer = std::move(writer);
+ }
+
+ pw::rpc::test::pwpb::TestRequest::Message last_request;
+ PwpbServerWriter<pw::rpc::test::pwpb::TestResponse::Message> last_writer;
+ RawServerWriter last_raw_writer;
+};
+
+TEST(PwpbMethodUnion, Raw_CallsUnaryMethod) {
+ const Method& method =
+ std::get<0>(FakeGeneratedServiceImpl::kMethods).method();
+ ServerContextForTest<FakeGeneratedServiceImpl> context(method);
+ rpc_lock().lock();
+ method.Invoke(context.get(), context.request({}));
+
+ const Packet& response = context.output().last_packet();
+ EXPECT_EQ(response.status(), Status::Unknown());
+}
+
+TEST(PwpbMethodUnion, Raw_CallsServerStreamingMethod) {
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestRequest,
+ request,
+ .integer = 555,
+ .status_code = 0);
+
+ const Method& method =
+ std::get<1>(FakeGeneratedServiceImpl::kMethods).method();
+ ServerContextForTest<FakeGeneratedServiceImpl> context(method);
+
+ rpc_lock().lock();
+ method.Invoke(context.get(), context.request(request));
+
+ EXPECT_TRUE(context.service().last_raw_writer.active());
+ EXPECT_EQ(OkStatus(), context.service().last_raw_writer.Finish());
+ EXPECT_EQ(context.output().last_packet().type(), pwpb::PacketType::RESPONSE);
+}
+
+TEST(PwpbMethodUnion, Pwpb_CallsUnaryMethod) {
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestRequest,
+ request,
+ .integer = 123,
+ .status_code = 3);
+
+ const Method& method =
+ std::get<2>(FakeGeneratedServiceImpl::kMethods).method();
+ ServerContextForTest<FakeGeneratedServiceImpl> context(method);
+ rpc_lock().lock();
+ method.Invoke(context.get(), context.request(request));
+
+ const Packet& response = context.output().last_packet();
+ EXPECT_EQ(response.status(), Status::Unauthenticated());
+
+ // Field 1 (encoded as 1 << 3) with 128 as the value.
+ constexpr std::byte expected[]{
+ std::byte{0x08}, std::byte{0x80}, std::byte{0x01}};
+
+ EXPECT_EQ(sizeof(expected), response.payload().size());
+ EXPECT_EQ(0,
+ std::memcmp(expected, response.payload().data(), sizeof(expected)));
+
+ EXPECT_EQ(123, context.service().last_request.integer);
+ EXPECT_EQ(3u, context.service().last_request.status_code);
+}
+
+TEST(PwpbMethodUnion, Pwpb_CallsServerStreamingMethod) {
+ PW_ENCODE_PB(pw::rpc::test::pwpb::TestRequest,
+ request,
+ .integer = 555,
+ .status_code = 0);
+
+ const Method& method =
+ std::get<3>(FakeGeneratedServiceImpl::kMethods).method();
+ ServerContextForTest<FakeGeneratedServiceImpl> context(method);
+
+ rpc_lock().lock();
+ method.Invoke(context.get(), context.request(request));
+
+ EXPECT_EQ(555, context.service().last_request.integer);
+ EXPECT_TRUE(context.service().last_writer.active());
+
+ EXPECT_EQ(OkStatus(), context.service().last_writer.Finish());
+ EXPECT_EQ(context.output().last_packet().type(), pwpb::PacketType::RESPONSE);
+}
+
+} // namespace
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/pwpb/public/pw_rpc/echo_service_pwpb.h b/pw_rpc/pwpb/public/pw_rpc/echo_service_pwpb.h
new file mode 100644
index 000000000..bb0e0f807
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/echo_service_pwpb.h
@@ -0,0 +1,30 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_rpc/echo.rpc.pwpb.h"
+
+namespace pw::rpc {
+
+class EchoService final
+ : public pw_rpc::pwpb::EchoService::Service<EchoService> {
+ public:
+ Status Echo(const pwpb::EchoMessage::Message& request,
+ pwpb::EchoMessage::Message& response) {
+ response.msg = request.msg;
+ return OkStatus();
+ }
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h
new file mode 100644
index 000000000..e66c42092
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_reader_writer.h
@@ -0,0 +1,473 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// This file defines the ClientReaderWriter, ClientReader, ClientWriter,
+// and UnaryReceiver classes for the pw_protobuf RPC interface. These classes
+// are used for bidirectional, client, and server streaming, and unary RPCs.
+#pragma once
+
+#include "pw_bytes/span.h"
+#include "pw_function/function.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/internal/client_call.h"
+#include "pw_rpc/pwpb/internal/common.h"
+
+namespace pw::rpc {
+namespace internal {
+
+// internal::PwpbUnaryResponseClientCall extends
+// internal::UnaryResponseClientCall by adding a method serializer/deserializer
+// passed in to Start(), typed request messages to the Start() call, and an
+// on_completed callback templated on the response type.
+template <typename Response>
+class PwpbUnaryResponseClientCall : public UnaryResponseClientCall {
+ public:
+ // Start() can be called with zero or one request objects.
+ template <typename CallType, typename... Request>
+ static CallType Start(Endpoint& client,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&, Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ rpc_lock().lock();
+ CallType call(
+ client.ClaimLocked(), channel_id, service_id, method_id, serde);
+
+ call.set_pwpb_on_completed_locked(std::move(on_completed));
+ call.set_on_error_locked(std::move(on_error));
+
+ if constexpr (sizeof...(Request) == 0u) {
+ call.SendInitialClientRequest({});
+ } else {
+ PwpbSendInitialRequest(call, serde.request(), request...);
+ }
+
+ client.CleanUpCalls();
+ return call;
+ }
+
+ protected:
+ // Derived classes allow default construction so that users can declare a
+ // variable into which to move client reader/writers from RPC calls.
+ constexpr PwpbUnaryResponseClientCall() = default;
+
+ PwpbUnaryResponseClientCall(LockedEndpoint& client,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ MethodType type,
+ const PwpbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : UnaryResponseClientCall(
+ client, channel_id, service_id, method_id, StructCallProps(type)),
+ serde_(&serde) {}
+
+ // Allow derived classes to be constructed moving another instance.
+ PwpbUnaryResponseClientCall(PwpbUnaryResponseClientCall&& other)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ *this = std::move(other);
+ }
+
+ // Allow derived classes to use move assignment from another instance.
+ PwpbUnaryResponseClientCall& operator=(PwpbUnaryResponseClientCall&& other)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ MovePwpbUnaryResponseClientCallFrom(other);
+ return *this;
+ }
+
+ // Implement moving by copying the serde pointer and on_completed function.
+ void MovePwpbUnaryResponseClientCallFrom(PwpbUnaryResponseClientCall& other)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ MoveUnaryResponseClientCallFrom(other);
+ serde_ = other.serde_;
+ set_pwpb_on_completed_locked(std::move(other.pwpb_on_completed_));
+ }
+
+ void set_on_completed(
+ Function<void(const Response& response, Status)>&& on_completed)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ set_pwpb_on_completed_locked(std::move(on_completed));
+ }
+
+ // Sends a streamed request.
+ // Returns the following Status codes:
+ //
+ // OK - the request was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf protobuf
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ template <typename Request>
+ Status SendStreamRequest(const Request& request)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ return PwpbSendStream(*this, request, serde_);
+ }
+
+ private:
+ void set_pwpb_on_completed_locked(
+ Function<void(const Response& response, Status)>&& on_completed)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ pwpb_on_completed_ = std::move(on_completed);
+
+ UnaryResponseClientCall::set_on_completed_locked(
+ [this](ConstByteSpan payload, Status status)
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ DecodeToStructAndInvokeOnCompleted(
+ payload, serde_->response(), pwpb_on_completed_, status);
+ });
+ }
+
+ const PwpbMethodSerde* serde_ PW_GUARDED_BY(rpc_lock());
+ Function<void(const Response&, Status)> pwpb_on_completed_
+ PW_GUARDED_BY(rpc_lock());
+};
+
+// internal::PwpbStreamResponseClientCall extends
+// internal::StreamResponseClientCall by adding a method serializer/deserializer
+// passed in to Start(), typed request messages to the Start() call, and an
+// on_next callback templated on the response type.
+template <typename Response>
+class PwpbStreamResponseClientCall : public StreamResponseClientCall {
+ public:
+ // Start() can be called with zero or one request objects.
+ template <typename CallType, typename... Request>
+ static CallType Start(Endpoint& client,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde,
+ Function<void(const Response&)>&& on_next,
+ Function<void(Status)>&& on_completed,
+ Function<void(Status)>&& on_error,
+ const Request&... request)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ rpc_lock().lock();
+ CallType call(
+ client.ClaimLocked(), channel_id, service_id, method_id, serde);
+
+ call.set_pwpb_on_next_locked(std::move(on_next));
+ call.set_on_completed_locked(std::move(on_completed));
+ call.set_on_error_locked(std::move(on_error));
+
+ if constexpr (sizeof...(Request) == 0u) {
+ call.SendInitialClientRequest({});
+ } else {
+ PwpbSendInitialRequest(call, serde.request(), request...);
+ }
+ client.CleanUpCalls();
+ return call;
+ }
+
+ protected:
+ // Derived classes allow default construction so that users can declare a
+ // variable into which to move client reader/writers from RPC calls.
+ constexpr PwpbStreamResponseClientCall() = default;
+
+ PwpbStreamResponseClientCall(LockedEndpoint& client,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ MethodType type,
+ const PwpbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : StreamResponseClientCall(
+ client, channel_id, service_id, method_id, StructCallProps(type)),
+ serde_(&serde) {}
+
+ // Allow derived classes to be constructed moving another instance.
+ PwpbStreamResponseClientCall(PwpbStreamResponseClientCall&& other)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ *this = std::move(other);
+ }
+
+ // Allow derived classes to use move assignment from another instance.
+ PwpbStreamResponseClientCall& operator=(PwpbStreamResponseClientCall&& other)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ MovePwpbStreamResponseClientCallFrom(other);
+ return *this;
+ }
+
+ // Implement moving by copying the serde pointer and on_next function.
+ void MovePwpbStreamResponseClientCallFrom(PwpbStreamResponseClientCall& other)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ MoveStreamResponseClientCallFrom(other);
+ serde_ = other.serde_;
+ set_pwpb_on_next_locked(std::move(other.pwpb_on_next_));
+ }
+
+ void set_on_next(Function<void(const Response& response)>&& on_next)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ set_pwpb_on_next_locked(std::move(on_next));
+ }
+
+ // Sends a streamed request.
+ // Returns the following Status codes:
+ //
+ // OK - the request was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf protobuf
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ template <typename Request>
+ Status SendStreamRequest(const Request& request)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ return PwpbSendStream(*this, request, serde_);
+ }
+
+ private:
+ void set_pwpb_on_next_locked(
+ Function<void(const Response& response)>&& on_next)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ pwpb_on_next_ = std::move(on_next);
+
+ Call::set_on_next_locked(
+ [this](ConstByteSpan payload) PW_NO_LOCK_SAFETY_ANALYSIS {
+ DecodeToStructAndInvokeOnNext(
+ payload, serde_->response(), pwpb_on_next_);
+ });
+ }
+
+ const PwpbMethodSerde* serde_ PW_GUARDED_BY(rpc_lock());
+ Function<void(const Response&)> pwpb_on_next_ PW_GUARDED_BY(rpc_lock());
+};
+
+} // namespace internal
+
+// The PwpbClientReaderWriter is used to send and receive typed messages in a
+// pw_protobuf bidirectional streaming RPC.
+//
+// These classes use private inheritance to hide the internal::Call API while
+// allow direct use of its public and protected functions.
+template <typename Request, typename Response>
+class PwpbClientReaderWriter
+ : private internal::PwpbStreamResponseClientCall<Response> {
+ public:
+ // Allow default construction so that users can declare a variable into
+ // which to move client reader/writers from RPC calls.
+ constexpr PwpbClientReaderWriter() = default;
+
+ PwpbClientReaderWriter(PwpbClientReaderWriter&&) = default;
+ PwpbClientReaderWriter& operator=(PwpbClientReaderWriter&&) = default;
+
+ using internal::Call::active;
+ using internal::Call::channel_id;
+
+ // Writes a request. Returns the following Status codes:
+ //
+ // OK - the request was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf message
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ Status Write(const Request& request) {
+ return internal::PwpbStreamResponseClientCall<Response>::SendStreamRequest(
+ request);
+ }
+
+ // Notifies the server that no further client stream messages will be sent.
+ using internal::ClientCall::CloseClientStream;
+
+ // Cancels this RPC. Closes the call locally and sends a CANCELLED error to
+ // the server.
+ using internal::Call::Cancel;
+
+ // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
+ // packet. Future packets for this RPC are dropped, and the client sends a
+ // FAILED_PRECONDITION error in response because the call is not active.
+ using internal::ClientCall::Abandon;
+
+ // Functions for setting RPC event callbacks.
+ using internal::PwpbStreamResponseClientCall<Response>::set_on_next;
+ using internal::StreamResponseClientCall::set_on_completed;
+ using internal::StreamResponseClientCall::set_on_error;
+
+ protected:
+ friend class internal::PwpbStreamResponseClientCall<Response>;
+
+ PwpbClientReaderWriter(internal::LockedEndpoint& client,
+ uint32_t channel_id_v,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : internal::PwpbStreamResponseClientCall<Response>(
+ client,
+ channel_id_v,
+ service_id,
+ method_id,
+ MethodType::kBidirectionalStreaming,
+ serde) {}
+};
+
+// The PwpbClientReader is used to receive typed messages and send a typed
+// response in a pw_protobuf client streaming RPC.
+//
+// These classes use private inheritance to hide the internal::Call API while
+// allow direct use of its public and protected functions.
+template <typename Response>
+class PwpbClientReader
+ : private internal::PwpbStreamResponseClientCall<Response> {
+ public:
+ // Allow default construction so that users can declare a variable into
+ // which to move client reader/writers from RPC calls.
+ constexpr PwpbClientReader() = default;
+
+ PwpbClientReader(PwpbClientReader&&) = default;
+ PwpbClientReader& operator=(PwpbClientReader&&) = default;
+
+ using internal::StreamResponseClientCall::active;
+ using internal::StreamResponseClientCall::channel_id;
+
+ using internal::Call::Cancel;
+ using internal::ClientCall::Abandon;
+
+ // Functions for setting RPC event callbacks.
+ using internal::PwpbStreamResponseClientCall<Response>::set_on_next;
+ using internal::StreamResponseClientCall::set_on_completed;
+ using internal::StreamResponseClientCall::set_on_error;
+
+ private:
+ friend class internal::PwpbStreamResponseClientCall<Response>;
+
+ PwpbClientReader(internal::LockedEndpoint& client,
+ uint32_t channel_id_v,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : internal::PwpbStreamResponseClientCall<Response>(
+ client,
+ channel_id_v,
+ service_id,
+ method_id,
+ MethodType::kServerStreaming,
+ serde) {}
+};
+
+// The PwpbClientWriter is used to send typed responses in a pw_protobuf server
+// streaming RPC.
+//
+// These classes use private inheritance to hide the internal::Call API while
+// allow direct use of its public and protected functions.
+template <typename Request, typename Response>
+class PwpbClientWriter
+ : private internal::PwpbUnaryResponseClientCall<Response> {
+ public:
+ // Allow default construction so that users can declare a variable into
+ // which to move client reader/writers from RPC calls.
+ constexpr PwpbClientWriter() = default;
+
+ PwpbClientWriter(PwpbClientWriter&&) = default;
+ PwpbClientWriter& operator=(PwpbClientWriter&&) = default;
+
+ using internal::UnaryResponseClientCall::active;
+ using internal::UnaryResponseClientCall::channel_id;
+
+ // Writes a request. Returns the following Status codes:
+ //
+ // OK - the request was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf message
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ Status Write(const Request& request) {
+ return internal::PwpbUnaryResponseClientCall<Response>::SendStreamRequest(
+ request);
+ }
+
+ using internal::Call::Cancel;
+ using internal::Call::CloseClientStream;
+ using internal::ClientCall::Abandon;
+
+ // Functions for setting RPC event callbacks.
+ using internal::PwpbUnaryResponseClientCall<Response>::set_on_completed;
+ using internal::UnaryResponseClientCall::set_on_error;
+
+ private:
+ friend class internal::PwpbUnaryResponseClientCall<Response>;
+
+ PwpbClientWriter(internal::LockedEndpoint& client,
+ uint32_t channel_id_v,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+
+ : internal::PwpbUnaryResponseClientCall<Response>(
+ client,
+ channel_id_v,
+ service_id,
+ method_id,
+ MethodType::kClientStreaming,
+ serde) {}
+};
+
+// The PwpbUnaryReceiver is used to handle a typed response to a pw_protobuf
+// unary RPC.
+//
+// These classes use private inheritance to hide the internal::Call API while
+// allow direct use of its public and protected functions.
+template <typename Response>
+class PwpbUnaryReceiver
+ : private internal::PwpbUnaryResponseClientCall<Response> {
+ public:
+ // Allow default construction so that users can declare a variable into
+ // which to move client reader/writers from RPC calls.
+ constexpr PwpbUnaryReceiver() = default;
+
+ PwpbUnaryReceiver(PwpbUnaryReceiver&&) = default;
+ PwpbUnaryReceiver& operator=(PwpbUnaryReceiver&&) = default;
+
+ using internal::Call::active;
+ using internal::Call::channel_id;
+
+ // Functions for setting RPC event callbacks.
+ using internal::Call::set_on_error;
+ using internal::PwpbUnaryResponseClientCall<Response>::set_on_completed;
+
+ using internal::Call::Cancel;
+ using internal::ClientCall::Abandon;
+
+ private:
+ friend class internal::PwpbUnaryResponseClientCall<Response>;
+
+ PwpbUnaryReceiver(internal::LockedEndpoint& client,
+ uint32_t channel_id_v,
+ uint32_t service_id,
+ uint32_t method_id,
+ const PwpbMethodSerde& serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : internal::PwpbUnaryResponseClientCall<Response>(client,
+ channel_id_v,
+ service_id,
+ method_id,
+ MethodType::kUnary,
+ serde) {}
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h
new file mode 100644
index 000000000..394c49f0d
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing.h
@@ -0,0 +1,110 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cinttypes>
+
+#include "pw_rpc/internal/client_server_testing.h"
+#include "pw_rpc/pwpb/fake_channel_output.h"
+
+namespace pw::rpc {
+namespace internal {
+
+template <size_t kOutputSize,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class PwpbForwardingChannelOutput final
+ : public ForwardingChannelOutput<
+ PwpbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ template <auto kMethod>
+ using Response = typename MethodInfo<kMethod>::Response;
+ template <auto kMethod>
+ using Request = typename MethodInfo<kMethod>::Request;
+
+ using Base = ForwardingChannelOutput<
+ PwpbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ constexpr PwpbForwardingChannelOutput() = default;
+
+ template <auto kMethod>
+ Response<kMethod> response(uint32_t channel_id, uint32_t index) {
+ PW_ASSERT(Base::PacketCount() >= index);
+ return Base::output_.template responses<kMethod>(channel_id)[index];
+ }
+
+ template <auto kMethod>
+ Request<kMethod> request(uint32_t channel_id, uint32_t index) {
+ PW_ASSERT(Base::PacketCount() >= index);
+ return Base::output_.template requests<kMethod>(channel_id)[index];
+ }
+};
+
+} // namespace internal
+
+template <size_t kOutputSize = 128,
+ size_t kMaxPackets = 16,
+ size_t kPayloadsBufferSizeBytes = 128>
+class PwpbClientServerTestContext final
+ : public internal::ClientServerTestContext<
+ internal::PwpbForwardingChannelOutput<kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ template <auto kMethod>
+ using Response = typename MethodInfo<kMethod>::Response;
+ template <auto kMethod>
+ using Request = typename MethodInfo<kMethod>::Request;
+
+ using Base = internal::ClientServerTestContext<
+ internal::PwpbForwardingChannelOutput<kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ PwpbClientServerTestContext() = default;
+
+ // Retrieve copy of request indexed by order of occurance
+ template <auto kMethod>
+ Request<kMethod> request(uint32_t index) {
+ return Base::channel_output_.template request<kMethod>(Base::channel().id(),
+ index);
+ }
+
+ // Retrieve copy of resonse indexed by order of occurance
+ template <auto kMethod>
+ Response<kMethod> response(uint32_t index) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index);
+ }
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h
new file mode 100644
index 000000000..f37d3ecf0
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_server_testing_threaded.h
@@ -0,0 +1,115 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cinttypes>
+
+#include "pw_rpc/internal/client_server_testing_threaded.h"
+#include "pw_rpc/pwpb/fake_channel_output.h"
+
+namespace pw::rpc {
+namespace internal {
+
+template <size_t kOutputSize,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class PwpbWatchableChannelOutput final
+ : public WatchableChannelOutput<
+ PwpbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ template <auto kMethod>
+ using Response = typename MethodInfo<kMethod>::Response;
+ template <auto kMethod>
+ using Request = typename MethodInfo<kMethod>::Request;
+
+ using Base = WatchableChannelOutput<
+ PwpbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ constexpr PwpbWatchableChannelOutput() = default;
+
+ template <auto kMethod>
+ Response<kMethod> response(uint32_t channel_id, uint32_t index)
+ PW_LOCKS_EXCLUDED(Base::mutex_) {
+ std::lock_guard lock(Base::mutex_);
+ PW_ASSERT(Base::PacketCount() >= index);
+ return Base::output_.template responses<kMethod>(channel_id)[index];
+ }
+
+ template <auto kMethod>
+ Request<kMethod> request(uint32_t channel_id, uint32_t index)
+ PW_LOCKS_EXCLUDED(Base::mutex_) {
+ std::lock_guard lock(Base::mutex_);
+ PW_ASSERT(Base::PacketCount() >= index);
+ return Base::output_.template requests<kMethod>(channel_id)[index];
+ }
+};
+
+} // namespace internal
+
+template <size_t kOutputSize = 128,
+ size_t kMaxPackets = 16,
+ size_t kPayloadsBufferSizeBytes = 128>
+class PwpbClientServerTestContextThreaded final
+ : public internal::ClientServerTestContextThreaded<
+ internal::PwpbWatchableChannelOutput<kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ template <auto kMethod>
+ using Response = typename MethodInfo<kMethod>::Response;
+ template <auto kMethod>
+ using Request = typename MethodInfo<kMethod>::Request;
+
+ using Base = internal::ClientServerTestContextThreaded<
+ internal::PwpbWatchableChannelOutput<kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ kOutputSize,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ PwpbClientServerTestContextThreaded(const thread::Options& options)
+ : Base(options) {}
+
+ // Retrieve copy of request indexed by order of occurance
+ template <auto kMethod>
+ Request<kMethod> request(uint32_t index) {
+ return Base::channel_output_.template request<kMethod>(Base::channel().id(),
+ index);
+ }
+
+ // Retrieve copy of resonse indexed by order of occurance
+ template <auto kMethod>
+ Response<kMethod> response(uint32_t index) {
+ return Base::channel_output_.template response<kMethod>(
+ Base::channel().id(), index);
+ }
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h
new file mode 100644
index 000000000..d91ebc6d3
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/client_testing.h
@@ -0,0 +1,115 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+#include "pw_bytes/span.h"
+#include "pw_rpc/client.h"
+#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/pwpb/fake_channel_output.h"
+#include "pw_rpc/raw/client_testing.h"
+
+namespace pw::rpc {
+
+// TODO(b/234878467): Document the client testing APIs.
+
+// Sends packets to an RPC client as if it were a pw_rpc server. Accepts
+// payloads as pw_protobuf message structs.
+class PwpbFakeServer : public FakeServer {
+ private:
+ template <auto kMethod>
+ using Response = typename internal::MethodInfo<kMethod>::Response;
+
+ public:
+ using FakeServer::FakeServer;
+
+ // Sends a response packet for a server or bidirectional streaming RPC to the
+ // client.
+ template <auto kMethod>
+ void SendResponse(Status status) const {
+ FakeServer::SendResponse<kMethod>(status);
+ }
+
+ // Sends a response packet for a unary or client streaming streaming RPC to
+ // the client.
+ template <auto kMethod,
+ size_t kEncodeBufferSizeBytes = 2 * sizeof(Response<kMethod>)>
+ void SendResponse(const Response<kMethod>& payload, Status status) const {
+ std::byte buffer[kEncodeBufferSizeBytes] = {};
+ FakeServer::SendResponse<kMethod>(EncodeResponse<kMethod>(payload, buffer),
+ status);
+ }
+
+ // Sends a stream packet for a server or bidirectional streaming RPC to the
+ // client.
+ template <auto kMethod,
+ size_t kEncodeBufferSizeBytes = 2 * sizeof(Response<kMethod>)>
+ void SendServerStream(const Response<kMethod>& payload) const {
+ std::byte buffer[kEncodeBufferSizeBytes] = {};
+ FakeServer::SendServerStream<kMethod>(
+ EncodeResponse<kMethod>(payload, buffer));
+ }
+
+ private:
+ template <auto kMethod>
+ static ConstByteSpan EncodeResponse(const Response<kMethod>& payload,
+ ByteSpan buffer) {
+ const StatusWithSize result =
+ internal::MethodInfo<kMethod>::serde().response().Encode(payload,
+ buffer);
+ PW_ASSERT(result.ok());
+ return span(buffer).first(result.size());
+ }
+};
+
+// Instantiates a PwpbFakeServer, Client, Channel, and PwpbFakeChannelOutput
+// for testing RPC client calls. These components may be used individually, but
+// are instantiated together for convenience.
+template <size_t kMaxPackets = 10,
+ size_t kPacketEncodeBufferSizeBytes = 128,
+ size_t kPayloadsBufferSizeBytes = 256>
+class PwpbClientTestContext {
+ public:
+ constexpr PwpbClientTestContext()
+ : channel_(Channel::Create<kDefaultChannelId>(&channel_output_)),
+ client_(span(&channel_, 1)),
+ packet_buffer_{},
+ fake_server_(
+ channel_output_, client_, kDefaultChannelId, packet_buffer_) {}
+
+ const Channel& channel() const { return channel_; }
+ Channel& channel() { return channel_; }
+
+ const PwpbFakeServer& server() const { return fake_server_; }
+ PwpbFakeServer& server() { return fake_server_; }
+
+ const Client& client() const { return client_; }
+ Client& client() { return client_; }
+
+ const auto& output() const { return channel_output_; }
+ auto& output() { return channel_output_; }
+
+ private:
+ static constexpr uint32_t kDefaultChannelId = 1;
+
+ PwpbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes> channel_output_;
+ Channel channel_;
+ Client client_;
+ std::byte packet_buffer_[kPacketEncodeBufferSizeBytes];
+ PwpbFakeServer fake_server_;
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h
new file mode 100644
index 000000000..c164f689a
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/fake_channel_output.h
@@ -0,0 +1,211 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstddef>
+#include <mutex>
+
+#include "pw_containers/wrapped_iterator.h"
+#include "pw_rpc/internal/fake_channel_output.h"
+#include "pw_rpc/internal/lock.h"
+#include "pw_rpc/pwpb/internal/common.h"
+#include "pw_rpc/pwpb/internal/method.h"
+
+namespace pw::rpc {
+namespace internal {
+
+// Forward declare for a friend statement.
+template <typename, size_t, size_t, size_t>
+class ForwardingChannelOutput;
+
+} // namespace internal
+} // namespace pw::rpc
+
+namespace pw::rpc {
+namespace internal::test::pwpb {
+
+// Forward declare for a friend statement.
+template <typename, auto, uint32_t, size_t, size_t>
+class PwpbInvocationContext;
+
+} // namespace internal::test::pwpb
+
+// PwpbPayloadsView supports iterating over payloads as decoded pw_protobuf
+// request or response message structs.
+template <typename Payload>
+class PwpbPayloadsView {
+ public:
+ class iterator : public containers::WrappedIterator<iterator,
+ PayloadsView::iterator,
+ Payload> {
+ public:
+ // Access the payload (rather than packet) with operator*.
+ Payload operator*() const {
+ Payload payload{};
+ PW_ASSERT(serde_
+ .Decode(containers::WrappedIterator<iterator,
+ PayloadsView::iterator,
+ Payload>::value(),
+ payload)
+ .ok());
+ return payload;
+ }
+
+ private:
+ friend class PwpbPayloadsView;
+
+ constexpr iterator(const PayloadsView::iterator& it, const PwpbSerde& serde)
+ : containers::
+ WrappedIterator<iterator, PayloadsView::iterator, Payload>(it),
+ serde_(serde) {}
+
+ PwpbSerde serde_;
+ };
+
+ Payload operator[](size_t index) const {
+ Payload payload{};
+ PW_ASSERT(serde_.Decode(view_[index], payload).ok());
+ return payload;
+ }
+
+ size_t size() const { return view_.size(); }
+ bool empty() const { return view_.empty(); }
+
+ // Returns the first/last payload for the RPC. size() must be > 0.
+ Payload front() const { return *begin(); }
+ Payload back() const { return *std::prev(end()); }
+
+ iterator begin() const { return iterator(view_.begin(), serde_); }
+ iterator end() const { return iterator(view_.end(), serde_); }
+
+ private:
+ template <size_t, size_t>
+ friend class PwpbFakeChannelOutput;
+
+ template <typename... Args>
+ PwpbPayloadsView(const PwpbSerde& serde, Args&&... args)
+ : view_(args...), serde_(serde) {}
+
+ PayloadsView view_;
+ PwpbSerde serde_;
+};
+
+// A ChannelOutput implementation that stores the outgoing payloads and status.
+template <size_t kMaxPackets, size_t kPayloadsBufferSizeBytes = 128>
+class PwpbFakeChannelOutput final
+ : public internal::test::FakeChannelOutputBuffer<kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ template <auto kMethod>
+ using Request = typename internal::MethodInfo<kMethod>::Request;
+ template <auto kMethod>
+ using Response = typename internal::MethodInfo<kMethod>::Response;
+
+ public:
+ PwpbFakeChannelOutput() = default;
+
+ // Iterates over request payloads from request or client stream packets.
+ //
+ // !!! WARNING !!!
+ //
+ // Access to the FakeChannelOutput through the PwpbPayloadsView is NOT
+ // synchronized! The PwpbPayloadsView is immediately invalidated if any
+ // thread accesses the FakeChannelOutput.
+ template <auto kMethod>
+ PwpbPayloadsView<Request<kMethod>> requests(
+ uint32_t channel_id = Channel::kUnassignedChannelId) const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ constexpr internal::pwpb::PacketType packet_type =
+ HasClientStream(internal::MethodInfo<kMethod>::kType)
+ ? internal::pwpb::PacketType::CLIENT_STREAM
+ : internal::pwpb::PacketType::REQUEST;
+ return PwpbPayloadsView<Request<kMethod>>(
+ internal::MethodInfo<kMethod>::serde().request(),
+ internal::test::FakeChannelOutputBuffer<
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>::packets(),
+ packet_type,
+ packet_type,
+ channel_id,
+ internal::MethodInfo<kMethod>::kServiceId,
+ internal::MethodInfo<kMethod>::kMethodId);
+ }
+
+ // Iterates over response payloads from response or server stream packets.
+ //
+ // !!! WARNING !!!
+ //
+ // Access to the FakeChannelOutput through the PwpbPayloadsView is NOT
+ // synchronized! The PwpbPayloadsView is immediately invalidated if any
+ // thread accesses the FakeChannelOutput.
+ template <auto kMethod>
+ PwpbPayloadsView<Response<kMethod>> responses(
+ uint32_t channel_id = Channel::kUnassignedChannelId) const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ constexpr internal::pwpb::PacketType packet_type =
+ HasServerStream(internal::MethodInfo<kMethod>::kType)
+ ? internal::pwpb::PacketType::SERVER_STREAM
+ : internal::pwpb::PacketType::RESPONSE;
+ return PwpbPayloadsView<Response<kMethod>>(
+ internal::MethodInfo<kMethod>::serde().response(),
+ internal::test::FakeChannelOutputBuffer<
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>::packets(),
+ packet_type,
+ packet_type,
+ channel_id,
+ internal::MethodInfo<kMethod>::kServiceId,
+ internal::MethodInfo<kMethod>::kMethodId);
+ }
+
+ template <auto kMethod>
+ Response<kMethod> last_response() const {
+ std::lock_guard lock(internal::test::FakeChannelOutput::mutex());
+ PwpbPayloadsView<Response<kMethod>> payloads = responses<kMethod>();
+ PW_ASSERT(!payloads.empty());
+ return payloads.back();
+ }
+
+ private:
+ template <typename, auto, uint32_t, size_t, size_t>
+ friend class internal::test::pwpb::PwpbInvocationContext;
+ template <typename, size_t, size_t, size_t>
+ friend class internal::ForwardingChannelOutput;
+
+ using internal::test::FakeChannelOutput::last_packet;
+
+ // !!! WARNING !!!
+ //
+ // Access to the FakeChannelOutput through the PwpbPayloadsView is NOT
+ // synchronized! The PwpbPayloadsView is immediately invalidated if any
+ // thread accesses the FakeChannelOutput.
+ template <typename T>
+ PwpbPayloadsView<T> payload_structs(const PwpbSerde& serde,
+ MethodType type,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id) const
+ PW_NO_LOCK_SAFETY_ANALYSIS {
+ return PwpbPayloadsView<T>(serde,
+ internal::test::FakeChannelOutputBuffer<
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>::packets(),
+ type,
+ channel_id,
+ service_id,
+ method_id);
+ }
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/common.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/common.h
new file mode 100644
index 000000000..03babf84b
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/common.h
@@ -0,0 +1,74 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_assert/assert.h"
+#include "pw_bytes/span.h"
+#include "pw_protobuf/encoder.h"
+#include "pw_protobuf/internal/codegen.h"
+#include "pw_protobuf/stream_decoder.h"
+#include "pw_rpc/internal/client_call.h"
+#include "pw_rpc/internal/encoding_buffer.h"
+#include "pw_rpc/internal/server_call.h"
+#include "pw_rpc/pwpb/serde.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::rpc::internal {
+
+// Forward declaration to avoid circular include.
+class PwpbServerCall;
+
+// Defines per-message struct type instance of the serializer/deserializer.
+template <PwpbMessageDescriptor kRequest, PwpbMessageDescriptor kResponse>
+constexpr PwpbMethodSerde kPwpbMethodSerde(kRequest, kResponse);
+
+// [Client] Encodes and sends the initial request message for the call.
+// active() must be true.
+template <typename Request>
+void PwpbSendInitialRequest(ClientCall& call,
+ PwpbSerde serde,
+ const Request& request)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ PW_ASSERT(call.active_locked());
+
+ Result<ByteSpan> buffer = EncodeToPayloadBuffer(request, serde);
+ if (buffer.ok()) {
+ call.SendInitialClientRequest(*buffer);
+ } else {
+ call.CloseAndMarkForCleanup(buffer.status());
+ }
+}
+
+// [Client/Server] Encodes and sends a client or server stream message.
+// Returns FAILED_PRECONDITION if active() is false.
+template <typename Payload>
+Status PwpbSendStream(Call& call,
+ const Payload& payload,
+ const PwpbMethodSerde* serde)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ if (!call.active_locked()) {
+ return Status::FailedPrecondition();
+ }
+
+ Result<ByteSpan> buffer = EncodeToPayloadBuffer(
+ payload,
+ call.type() == kClientCall ? serde->request() : serde->response());
+ PW_TRY(buffer);
+
+ return call.WriteLocked(*buffer);
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h
new file mode 100644
index 000000000..a0b484d59
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method.h
@@ -0,0 +1,461 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <type_traits>
+
+#include "pw_bytes/span.h"
+#include "pw_rpc/internal/call_context.h"
+#include "pw_rpc/internal/lock.h"
+#include "pw_rpc/internal/method.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/method_type.h"
+#include "pw_rpc/pwpb/internal/common.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+#include "pw_rpc/service.h"
+#include "pw_span/span.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::rpc::internal {
+
+// Expected function signatures for user-implemented RPC functions.
+template <typename Request, typename Response>
+using PwpbSynchronousUnary = Status(const Request&, Response&);
+
+template <typename Request, typename Response>
+using PwpbAsynchronousUnary = void(const Request&,
+ PwpbUnaryResponder<Response>&);
+
+template <typename Request, typename Response>
+using PwpbServerStreaming = void(const Request&, PwpbServerWriter<Response>&);
+
+template <typename Request, typename Response>
+using PwpbClientStreaming = void(PwpbServerReader<Request, Response>&);
+
+template <typename Request, typename Response>
+using PwpbBidirectionalStreaming =
+ void(PwpbServerReaderWriter<Request, Response>&);
+
+// The PwpbMethod class invokes user-defined service methods. When a
+// pw::rpc::Server receives an RPC request packet, it looks up the matching
+// PwpbMethod instance and calls its Invoke method, which eventually calls into
+// the user-defined RPC function.
+//
+// A PwpbMethod instance is created for each user-defined RPC in the pw_rpc
+// generated code. The PwpbMethod stores a pointer to the RPC function,
+// a pointer to an "invoker" function that calls that function, and a
+// reference to a serializer/deserializer initiiated with the message struct
+// tables used to encode and decode request and response message structs.
+class PwpbMethod : public Method {
+ public:
+ template <auto kMethod, typename RequestType, typename ResponseType>
+ static constexpr bool matches() {
+ return std::conjunction_v<
+ std::is_same<MethodImplementation<kMethod>, PwpbMethod>,
+ std::is_same<RequestType, Request<kMethod>>,
+ std::is_same<ResponseType, Response<kMethod>>>;
+ }
+
+ // Creates a PwpbMethod for a synchronous unary RPC.
+ // TODO(b/234874001): Find a way to reduce the number of monomorphized copies
+ // of this method.
+ template <auto kMethod>
+ static constexpr PwpbMethod SynchronousUnary(uint32_t id,
+ const PwpbMethodSerde& serde) {
+ // Define a wrapper around the user-defined function that takes the
+ // request and response protobuf structs as byte spans, and calls the
+ // implementation with the correct type.
+ //
+ // This wrapper is stored generically in the Function union, defined below.
+ // In optimized builds, the compiler inlines the user-defined function into
+ // this wrapper, eliminating any overhead.
+ constexpr SynchronousUnaryFunction wrapper =
+ [](Service& service, const void* request, void* response) {
+ return CallMethodImplFunction<kMethod>(
+ service,
+ *reinterpret_cast<const Request<kMethod>*>(request),
+ *reinterpret_cast<Response<kMethod>*>(response));
+ };
+ return PwpbMethod(
+ id,
+ SynchronousUnaryInvoker<Request<kMethod>, Response<kMethod>>,
+ Function{.synchronous_unary = wrapper},
+ serde);
+ }
+
+ // Creates a PwpbMethod for an asynchronous unary RPC.
+ // TODO(b/234874001): Find a way to reduce the number of monomorphized copies
+ // of this method.
+ template <auto kMethod>
+ static constexpr PwpbMethod AsynchronousUnary(uint32_t id,
+ const PwpbMethodSerde& serde) {
+ // Define a wrapper around the user-defined function that takes the
+ // request struct as a byte span, the response as a server call, and calls
+ // the implementation with the correct types.
+ //
+ // This wrapper is stored generically in the Function union, defined below.
+ // In optimized builds, the compiler inlines the user-defined function into
+ // this wrapper, eliminating any overhead.
+ constexpr UnaryRequestFunction wrapper =
+ [](Service& service,
+ const void* request,
+ internal::PwpbServerCall& writer) {
+ return CallMethodImplFunction<kMethod>(
+ service,
+ *reinterpret_cast<const Request<kMethod>*>(request),
+ static_cast<PwpbUnaryResponder<Response<kMethod>>&>(writer));
+ };
+ return PwpbMethod(id,
+ AsynchronousUnaryInvoker<Request<kMethod>>,
+ Function{.unary_request = wrapper},
+ serde);
+ }
+
+ // Creates a PwpbMethod for a server-streaming RPC.
+ template <auto kMethod>
+ static constexpr PwpbMethod ServerStreaming(uint32_t id,
+ const PwpbMethodSerde& serde) {
+ // Define a wrapper around the user-defined function that takes the
+ // request struct as a byte span, the response as a server call, and calls
+ // the implementation with the correct types.
+ //
+ // This wrapper is stored generically in the Function union, defined below.
+ // In optimized builds, the compiler inlines the user-defined function into
+ // this wrapper, eliminating any overhead.
+ constexpr UnaryRequestFunction wrapper =
+ [](Service& service,
+ const void* request,
+ internal::PwpbServerCall& writer) {
+ return CallMethodImplFunction<kMethod>(
+ service,
+ *reinterpret_cast<const Request<kMethod>*>(request),
+ static_cast<PwpbServerWriter<Response<kMethod>>&>(writer));
+ };
+ return PwpbMethod(id,
+ ServerStreamingInvoker<Request<kMethod>>,
+ Function{.unary_request = wrapper},
+ serde);
+ }
+
+ // Creates a PwpbMethod for a client-streaming RPC.
+ template <auto kMethod>
+ static constexpr PwpbMethod ClientStreaming(uint32_t id,
+ const PwpbMethodSerde& serde) {
+ // Define a wrapper around the user-defined function that takes the
+ // request as a server call, and calls the implementation with the correct
+ // types.
+ //
+ // This wrapper is stored generically in the Function union, defined below.
+ // In optimized builds, the compiler inlines the user-defined function into
+ // this wrapper, eliminating any overhead.
+ constexpr StreamRequestFunction wrapper = [](Service& service,
+ internal::PwpbServerCall&
+ reader) {
+ return CallMethodImplFunction<kMethod>(
+ service,
+ static_cast<PwpbServerReader<Request<kMethod>, Response<kMethod>>&>(
+ reader));
+ };
+ return PwpbMethod(id,
+ ClientStreamingInvoker<Request<kMethod>>,
+ Function{.stream_request = wrapper},
+ serde);
+ }
+
+ // Creates a PwpbMethod for a bidirectional-streaming RPC.
+ template <auto kMethod>
+ static constexpr PwpbMethod BidirectionalStreaming(
+ uint32_t id, const PwpbMethodSerde& serde) {
+ // Define a wrapper around the user-defined function that takes the
+ // request and response as a server call, and calls the implementation with
+ // the correct types.
+ //
+ // This wrapper is stored generically in the Function union, defined below.
+ // In optimized builds, the compiler inlines the user-defined function into
+ // this wrapper, eliminating any overhead.
+ constexpr StreamRequestFunction wrapper =
+ [](Service& service, internal::PwpbServerCall& reader_writer) {
+ return CallMethodImplFunction<kMethod>(
+ service,
+ static_cast<
+ PwpbServerReaderWriter<Request<kMethod>, Response<kMethod>>&>(
+ reader_writer));
+ };
+ return PwpbMethod(id,
+ BidirectionalStreamingInvoker<Request<kMethod>>,
+ Function{.stream_request = wrapper},
+ serde);
+ }
+
+ // Represents an invalid method. Used to reduce error message verbosity.
+ static constexpr PwpbMethod Invalid() {
+ return {0, InvalidInvoker, {}, PwpbMethodSerde(nullptr, nullptr)};
+ }
+
+ // Give access to the serializer/deserializer object for converting requests
+ // and responses between the wire format and pw_protobuf structs.
+ const PwpbMethodSerde& serde() const { return serde_; }
+
+ private:
+ // Generic function signature for synchronous unary RPCs.
+ using SynchronousUnaryFunction = Status (*)(Service&,
+ const void* request,
+ void* response);
+
+ // Generic function signature for asynchronous unary and server streaming
+ // RPCs.
+ using UnaryRequestFunction = void (*)(Service&,
+ const void* request,
+ internal::PwpbServerCall& writer);
+
+ // Generic function signature for client and bidirectional streaming RPCs.
+ using StreamRequestFunction =
+ void (*)(Service&, internal::PwpbServerCall& reader_writer);
+
+ // The Function union stores a pointer to a generic version of the
+ // user-defined RPC function. Using a union instead of void* avoids
+ // reinterpret_cast, which keeps this class fully constexpr.
+ union Function {
+ SynchronousUnaryFunction synchronous_unary;
+ UnaryRequestFunction unary_request;
+ StreamRequestFunction stream_request;
+ };
+
+ constexpr PwpbMethod(uint32_t id,
+ Invoker invoker,
+ Function function,
+ const PwpbMethodSerde& serde)
+ : Method(id, invoker), function_(function), serde_(serde) {}
+
+ template <typename Request, typename Response>
+ void CallSynchronousUnary(const CallContext& context,
+ const Packet& request,
+ Request& request_struct,
+ Response& response_struct) const
+ PW_UNLOCK_FUNCTION(rpc_lock()) {
+ if (!DecodeRequest(context, request, request_struct).ok()) {
+ context.server().CleanUpCalls();
+ return;
+ }
+
+ internal::PwpbServerCall responder(context.ClaimLocked(),
+ MethodType::kUnary);
+ context.server().CleanUpCalls();
+ const Status status = function_.synchronous_unary(
+ context.service(), &request_struct, &response_struct);
+ responder.SendUnaryResponse(response_struct, status).IgnoreError();
+ }
+
+ template <typename Request>
+ void CallUnaryRequest(const CallContext& context,
+ MethodType method_type,
+ const Packet& request,
+ Request& request_struct) const
+ PW_UNLOCK_FUNCTION(rpc_lock()) {
+ if (!DecodeRequest(context, request, request_struct).ok()) {
+ context.server().CleanUpCalls();
+ return;
+ }
+
+ internal::PwpbServerCall server_writer(context.ClaimLocked(), method_type);
+ context.server().CleanUpCalls();
+ function_.unary_request(context.service(), &request_struct, server_writer);
+ }
+
+ // Decodes a request protobuf into the provided buffer. Sends an error packet
+ // if the request failed to decode.
+ template <typename Request>
+ Status DecodeRequest(const CallContext& context,
+ const Packet& request,
+ Request& request_struct) const
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ const auto status =
+ serde_.request().Decode(request.payload(), request_struct);
+ if (status.ok()) {
+ return status;
+ }
+
+ // The channel is known to exist. It was found when the request was
+ // processed and the lock has been held since, so GetInternalChannel cannot
+ // fail.
+ context.server()
+ .GetInternalChannel(context.channel_id())
+ ->Send(Packet::ServerError(request, Status::DataLoss()))
+ .IgnoreError();
+ return status;
+ }
+
+ // Invoker function for synchronous unary RPCs.
+ template <typename Request, typename Response>
+ static void SynchronousUnaryInvoker(const CallContext& context,
+ const Packet& request)
+ PW_UNLOCK_FUNCTION(rpc_lock()) {
+ Request request_struct{};
+ Response response_struct{};
+
+ static_cast<const PwpbMethod&>(context.method())
+ .CallSynchronousUnary(
+ context, request, request_struct, response_struct);
+ }
+
+ // Invoker function for asynchronous unary RPCs.
+ template <typename Request>
+ static void AsynchronousUnaryInvoker(const CallContext& context,
+ const Packet& request)
+ PW_UNLOCK_FUNCTION(rpc_lock()) {
+ Request request_struct{};
+
+ static_cast<const PwpbMethod&>(context.method())
+ .CallUnaryRequest(context, MethodType::kUnary, request, request_struct);
+ }
+
+ // Invoker function for server streaming RPCs.
+ template <typename Request>
+ static void ServerStreamingInvoker(const CallContext& context,
+ const Packet& request)
+ PW_UNLOCK_FUNCTION(rpc_lock()) {
+ Request request_struct{};
+
+ static_cast<const PwpbMethod&>(context.method())
+ .CallUnaryRequest(
+ context, MethodType::kServerStreaming, request, request_struct);
+ }
+
+ // Invoker function for client streaming RPCs.
+ template <typename Request>
+ static void ClientStreamingInvoker(const CallContext& context, const Packet&)
+ PW_UNLOCK_FUNCTION(rpc_lock()) {
+ internal::BasePwpbServerReader<Request> reader(
+ context.ClaimLocked(), MethodType::kClientStreaming);
+ context.server().CleanUpCalls();
+ static_cast<const PwpbMethod&>(context.method())
+ .function_.stream_request(context.service(), reader);
+ }
+
+ // Invoker function for bidirectional streaming RPCs.
+ template <typename Request>
+ static void BidirectionalStreamingInvoker(const CallContext& context,
+ const Packet&)
+ PW_UNLOCK_FUNCTION(rpc_lock()) {
+ internal::BasePwpbServerReader<Request> reader_writer(
+ context.ClaimLocked(), MethodType::kBidirectionalStreaming);
+ context.server().CleanUpCalls();
+ static_cast<const PwpbMethod&>(context.method())
+ .function_.stream_request(context.service(), reader_writer);
+ }
+
+ // Stores the user-defined RPC in a generic wrapper.
+ Function function_;
+
+ // Serde used to encode and decode pw_protobuf structs.
+ const PwpbMethodSerde& serde_;
+};
+
+// MethodTraits specialization for a static synchronous unary method.
+// TODO(b/234874320): Further qualify this (and nanopb) definition so that they
+// can co-exist in the same project.
+template <typename Req, typename Res>
+struct MethodTraits<PwpbSynchronousUnary<Req, Res>*> {
+ using Implementation = PwpbMethod;
+ using Request = Req;
+ using Response = Res;
+
+ static constexpr MethodType kType = MethodType::kUnary;
+ static constexpr bool kSynchronous = true;
+
+ static constexpr bool kServerStreaming = false;
+ static constexpr bool kClientStreaming = false;
+};
+
+// MethodTraits specialization for a synchronous raw unary method.
+template <typename T, typename Req, typename Res>
+struct MethodTraits<PwpbSynchronousUnary<Req, Res>(T::*)>
+ : MethodTraits<PwpbSynchronousUnary<Req, Res>*> {
+ using Service = T;
+};
+
+// MethodTraits specialization for a static asynchronous unary method.
+template <typename Req, typename Resp>
+struct MethodTraits<PwpbAsynchronousUnary<Req, Resp>*>
+ : MethodTraits<PwpbSynchronousUnary<Req, Resp>*> {
+ static constexpr bool kSynchronous = false;
+};
+
+// MethodTraits specialization for an asynchronous unary method.
+template <typename T, typename Req, typename Resp>
+struct MethodTraits<PwpbAsynchronousUnary<Req, Resp>(T::*)>
+ : MethodTraits<PwpbSynchronousUnary<Req, Resp>(T::*)> {
+ static constexpr bool kSynchronous = false;
+};
+
+// MethodTraits specialization for a static server streaming method.
+template <typename Req, typename Resp>
+struct MethodTraits<PwpbServerStreaming<Req, Resp>*> {
+ using Implementation = PwpbMethod;
+ using Request = Req;
+ using Response = Resp;
+
+ static constexpr MethodType kType = MethodType::kServerStreaming;
+ static constexpr bool kServerStreaming = true;
+ static constexpr bool kClientStreaming = false;
+};
+
+// MethodTraits specialization for a server streaming method.
+template <typename T, typename Req, typename Resp>
+struct MethodTraits<PwpbServerStreaming<Req, Resp>(T::*)>
+ : MethodTraits<PwpbServerStreaming<Req, Resp>*> {
+ using Service = T;
+};
+
+// MethodTraits specialization for a static server streaming method.
+template <typename Req, typename Resp>
+struct MethodTraits<PwpbClientStreaming<Req, Resp>*> {
+ using Implementation = PwpbMethod;
+ using Request = Req;
+ using Response = Resp;
+
+ static constexpr MethodType kType = MethodType::kClientStreaming;
+ static constexpr bool kServerStreaming = false;
+ static constexpr bool kClientStreaming = true;
+};
+
+// MethodTraits specialization for a server streaming method.
+template <typename T, typename Req, typename Resp>
+struct MethodTraits<PwpbClientStreaming<Req, Resp>(T::*)>
+ : MethodTraits<PwpbClientStreaming<Req, Resp>*> {
+ using Service = T;
+};
+
+// MethodTraits specialization for a static server streaming method.
+template <typename Req, typename Resp>
+struct MethodTraits<PwpbBidirectionalStreaming<Req, Resp>*> {
+ using Implementation = PwpbMethod;
+ using Request = Req;
+ using Response = Resp;
+
+ static constexpr MethodType kType = MethodType::kBidirectionalStreaming;
+ static constexpr bool kServerStreaming = true;
+ static constexpr bool kClientStreaming = true;
+};
+
+// MethodTraits specialization for a server streaming method.
+template <typename T, typename Req, typename Resp>
+struct MethodTraits<PwpbBidirectionalStreaming<Req, Resp>(T::*)>
+ : MethodTraits<PwpbBidirectionalStreaming<Req, Resp>*> {
+ using Service = T;
+};
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method_union.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method_union.h
new file mode 100644
index 000000000..9e92d446a
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/internal/method_union.h
@@ -0,0 +1,57 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_rpc/internal/method_union.h"
+#include "pw_rpc/pwpb/internal/common.h"
+#include "pw_rpc/pwpb/internal/method.h"
+#include "pw_rpc/raw/internal/method_union.h"
+
+namespace pw::rpc::internal {
+
+// MethodUnion which holds a pw_protobuf method or a raw method.
+class PwpbMethodUnion : public MethodUnion {
+ public:
+ constexpr PwpbMethodUnion(RawMethod&& method)
+ : impl_({.raw = std::move(method)}) {}
+ constexpr PwpbMethodUnion(PwpbMethod&& method)
+ : impl_({.pwpb = std::move(method)}) {}
+
+ constexpr const Method& method() const { return impl_.method; }
+ constexpr const RawMethod& raw_method() const { return impl_.raw; }
+ constexpr const PwpbMethod& pwpb_method() const { return impl_.pwpb; }
+
+ private:
+ union {
+ Method method;
+ RawMethod raw;
+ PwpbMethod pwpb;
+ } impl_;
+};
+
+// Deduces the type of an implemented service method from its signature, and
+// returns the appropriate MethodUnion object to invoke it.
+template <auto kMethod, MethodType kType, typename Request, typename Response>
+constexpr auto GetPwpbOrRawMethodFor(uint32_t id,
+ const PwpbMethodSerde& serde) {
+ if constexpr (RawMethod::matches<kMethod>()) {
+ return GetMethodFor<kMethod, RawMethod, kType>(id);
+ } else if constexpr (PwpbMethod::matches<kMethod, Request, Response>()) {
+ return GetMethodFor<kMethod, PwpbMethod, kType>(id, serde);
+ } else {
+ return InvalidMethod<kMethod, kType, RawMethod>(id);
+ }
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h
new file mode 100644
index 000000000..92572de84
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/serde.h
@@ -0,0 +1,119 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <array>
+
+#include "pw_protobuf/encoder.h"
+#include "pw_protobuf/internal/codegen.h"
+#include "pw_protobuf/stream_decoder.h"
+#include "pw_span/span.h"
+#include "pw_stream/null_stream.h"
+
+namespace pw::rpc {
+
+using PwpbMessageDescriptor =
+ const span<const protobuf::internal::MessageField>*;
+
+// Serializer/deserializer for a pw_protobuf message.
+class PwpbSerde {
+ public:
+ explicit constexpr PwpbSerde(PwpbMessageDescriptor table) : table_(table) {}
+
+ PwpbSerde(const PwpbSerde&) = default;
+ PwpbSerde& operator=(const PwpbSerde&) = default;
+
+ // Encodes a pw_protobuf struct to the serialized wire format.
+ template <typename Message>
+ StatusWithSize Encode(const Message& message, ByteSpan buffer) const {
+ return Encoder(buffer).Write(as_bytes(span(&message, 1)), table_);
+ }
+
+ // Calculates the encoded size of the provided protobuf struct without
+ // actually encoding it.
+ template <typename Message>
+ StatusWithSize EncodedSizeBytes(const Message& message) const {
+ // TODO(b/269515470): Use kScratchBufferSizeBytes instead of a fixed size.
+ std::array<std::byte, 64> scratch_buffer;
+
+ stream::CountingNullStream output;
+ StreamEncoder encoder(output, scratch_buffer);
+ const Status result = encoder.Write(as_bytes(span(&message, 1)), *table_);
+
+ // TODO(b/269633514): Add 1 to the encoded size because pw_protobuf
+ // sometimes fails to encode to buffers that exactly fit the output.
+ return StatusWithSize(result, output.bytes_written() + 1);
+ }
+
+ // Decodes a serialized protobuf into a pw_protobuf message struct.
+ template <typename Message>
+ Status Decode(ConstByteSpan buffer, Message& message) const {
+ return Decoder(buffer).Read(as_writable_bytes(span(&message, 1)), table_);
+ }
+
+ private:
+ class Encoder : public protobuf::MemoryEncoder {
+ public:
+ constexpr Encoder(ByteSpan buffer) : protobuf::MemoryEncoder(buffer) {}
+
+ StatusWithSize Write(ConstByteSpan message, PwpbMessageDescriptor table) {
+ const Status status = protobuf::MemoryEncoder::Write(message, *table);
+ return StatusWithSize(status, size());
+ }
+ };
+
+ class StreamEncoder : public protobuf::StreamEncoder {
+ public:
+ constexpr StreamEncoder(stream::Writer& writer, ByteSpan buffer)
+ : protobuf::StreamEncoder(writer, buffer) {}
+
+ using protobuf::StreamEncoder::Write; // Make this method public
+ };
+
+ class Decoder : public protobuf::StreamDecoder {
+ public:
+ constexpr Decoder(ConstByteSpan buffer)
+ : protobuf::StreamDecoder(reader_), reader_(buffer) {}
+
+ Status Read(ByteSpan message, PwpbMessageDescriptor table) {
+ return protobuf::StreamDecoder::Read(message, *table);
+ }
+
+ private:
+ stream::MemoryReader reader_;
+ };
+
+ PwpbMessageDescriptor table_;
+};
+
+// Serializer/deserializer for pw_protobuf request and response message structs
+// within an RPC method.
+class PwpbMethodSerde {
+ public:
+ constexpr PwpbMethodSerde(PwpbMessageDescriptor request_table,
+ PwpbMessageDescriptor response_table)
+ : request_serde_(request_table), response_serde_(response_table) {}
+
+ PwpbMethodSerde(const PwpbMethodSerde&) = delete;
+ PwpbMethodSerde& operator=(const PwpbMethodSerde&) = delete;
+
+ const PwpbSerde& request() const { return request_serde_; }
+ const PwpbSerde& response() const { return response_serde_; }
+
+ private:
+ PwpbSerde request_serde_;
+ PwpbSerde response_serde_;
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h
new file mode 100644
index 000000000..4d80c7176
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/server_reader_writer.h
@@ -0,0 +1,469 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// This file defines the ServerReaderWriter, ServerReader, ServerWriter, and
+// UnaryResponder classes for the pw_protobuf RPC interface. These classes are
+// used for bidirectional, client, and server streaming, and unary RPCs.
+#pragma once
+
+#include "pw_bytes/span.h"
+#include "pw_function/function.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/internal/lock.h"
+#include "pw_rpc/internal/method_info.h"
+#include "pw_rpc/internal/method_lookup.h"
+#include "pw_rpc/internal/server_call.h"
+#include "pw_rpc/method_type.h"
+#include "pw_rpc/pwpb/internal/common.h"
+#include "pw_rpc/server.h"
+
+namespace pw::rpc {
+namespace internal {
+
+// Forward declarations for internal classes needed in friend statements.
+namespace test {
+template <typename, typename, uint32_t>
+class InvocationContext;
+} // namespace test
+
+class PwpbMethod;
+
+// internal::PwpbServerCall extends internal::ServerCall by adding a method
+// serializer/deserializer that is initialized based on the method context.
+class PwpbServerCall : public ServerCall {
+ public:
+ // Allow construction using a call context and method type which creates
+ // a working server call.
+ PwpbServerCall(const LockedCallContext& context, MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock());
+
+ // Sends a unary response.
+ // Returns the following Status codes:
+ //
+ // OK - the response was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf protobuf
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ template <typename Response>
+ Status SendUnaryResponse(const Response& response, Status status = OkStatus())
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ if (!active_locked()) {
+ return Status::FailedPrecondition();
+ }
+
+ Result<ByteSpan> buffer =
+ EncodeToPayloadBuffer(response, serde_->response());
+ if (!buffer.ok()) {
+ return CloseAndSendServerErrorLocked(Status::Internal());
+ }
+
+ return CloseAndSendResponseLocked(*buffer, status);
+ }
+
+ protected:
+ // Derived classes allow default construction so that users can declare a
+ // variable into which to move server reader/writers from RPC calls.
+ constexpr PwpbServerCall() : serde_(nullptr) {}
+
+ // Give access to the serializer/deserializer object for converting requests
+ // and responses between the wire format and pw_protobuf structs.
+ const PwpbMethodSerde& serde() const PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ return *serde_;
+ }
+
+ // Allow derived classes to be constructed moving another instance.
+ PwpbServerCall(PwpbServerCall&& other) PW_LOCKS_EXCLUDED(rpc_lock()) {
+ *this = std::move(other);
+ }
+
+ // Allow derived classes to use move assignment from another instance.
+ PwpbServerCall& operator=(PwpbServerCall&& other)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ MovePwpbServerCallFrom(other);
+ return *this;
+ }
+
+ // Implement moving by copying the serde pointer.
+ void MovePwpbServerCallFrom(PwpbServerCall& other)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ MoveServerCallFrom(other);
+ serde_ = other.serde_;
+ }
+
+ // Sends a streamed response.
+ // Returns the following Status codes:
+ //
+ // OK - the response was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf protobuf
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ template <typename Response>
+ Status SendStreamResponse(const Response& response)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ return PwpbSendStream(*this, response, serde_);
+ }
+
+ private:
+ const PwpbMethodSerde* serde_ PW_GUARDED_BY(rpc_lock());
+};
+
+// internal::BasePwpbServerReader extends internal::PwpbServerCall further by
+// adding an on_next callback templated on the request type.
+template <typename Request>
+class BasePwpbServerReader : public PwpbServerCall {
+ public:
+ BasePwpbServerReader(const LockedCallContext& context, MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock())
+ : PwpbServerCall(context, type) {}
+
+ protected:
+ // Allow default construction so that users can declare a variable into
+ // which to move server reader/writers from RPC calls.
+ constexpr BasePwpbServerReader() = default;
+
+ // Allow derived classes to be constructed moving another instance.
+ BasePwpbServerReader(BasePwpbServerReader&& other)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ *this = std::move(other);
+ }
+
+ // Allow derived classes to use move assignment from another instance.
+ BasePwpbServerReader& operator=(BasePwpbServerReader&& other)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ MoveBasePwpbServerReaderFrom(other);
+ return *this;
+ }
+
+ // Implement moving by copying the on_next function.
+ void MoveBasePwpbServerReaderFrom(BasePwpbServerReader& other)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ MovePwpbServerCallFrom(other);
+ set_pwpb_on_next_locked(std::move(other.pwpb_on_next_));
+ }
+
+ void set_on_next(Function<void(const Request& request)>&& on_next)
+ PW_LOCKS_EXCLUDED(rpc_lock()) {
+ RpcLockGuard lock;
+ set_pwpb_on_next_locked(std::move(on_next));
+ }
+
+ private:
+ void set_pwpb_on_next_locked(Function<void(const Request& request)>&& on_next)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(rpc_lock()) {
+ pwpb_on_next_ = std::move(on_next);
+
+ Call::set_on_next_locked(
+ [this](ConstByteSpan payload) PW_NO_LOCK_SAFETY_ANALYSIS {
+ DecodeToStructAndInvokeOnNext(
+ payload, serde().request(), pwpb_on_next_);
+ });
+ }
+
+ Function<void(const Request&)> pwpb_on_next_ PW_GUARDED_BY(rpc_lock());
+};
+
+} // namespace internal
+
+// The PwpbServerReaderWriter is used to send and receive typed messages in a
+// pw_protobuf bidirectional streaming RPC.
+//
+// These classes use private inheritance to hide the internal::Call API while
+// allow direct use of its public and protected functions.
+template <typename Request, typename Response>
+class PwpbServerReaderWriter : private internal::BasePwpbServerReader<Request> {
+ public:
+ // Creates a PwpbServerReaderWriter that is ready to send responses for a
+ // particular RPC. This can be used for testing or to send responses to an RPC
+ // that has not been started by a client.
+ template <auto kMethod, typename ServiceImpl>
+ [[nodiscard]] static PwpbServerReaderWriter Open(Server& server,
+ uint32_t channel_id,
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ static_assert(std::is_same_v<Request, typename MethodInfo::Request>,
+ "The request type of a PwpbServerReaderWriter must match "
+ "the method.");
+ static_assert(std::is_same_v<Response, typename MethodInfo::Response>,
+ "The response type of a PwpbServerReaderWriter must match "
+ "the method.");
+ return server.OpenCall<PwpbServerReaderWriter,
+ kMethod,
+ MethodType::kBidirectionalStreaming>(
+ channel_id,
+ service,
+ internal::MethodLookup::GetPwpbMethod<ServiceImpl,
+ MethodInfo::kMethodId>());
+ }
+
+ // Allow default construction so that users can declare a variable into
+ // which to move server reader/writers from RPC calls.
+ constexpr PwpbServerReaderWriter() = default;
+
+ PwpbServerReaderWriter(PwpbServerReaderWriter&&) = default;
+ PwpbServerReaderWriter& operator=(PwpbServerReaderWriter&&) = default;
+
+ using internal::Call::active;
+ using internal::Call::channel_id;
+
+ // Functions for setting RPC event callbacks.
+ using internal::Call::set_on_error;
+ using internal::BasePwpbServerReader<Request>::set_on_next;
+ using internal::ServerCall::set_on_client_stream_end;
+
+ // Writes a response. Returns the following Status codes:
+ //
+ // OK - the response was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf message
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ Status Write(const Response& response) {
+ return internal::PwpbServerCall::SendStreamResponse(response);
+ }
+
+ Status Finish(Status status = OkStatus()) {
+ return internal::Call::CloseAndSendResponse(status);
+ }
+
+ private:
+ friend class internal::PwpbMethod;
+ friend class Server;
+
+ template <typename, typename, uint32_t>
+ friend class internal::test::InvocationContext;
+
+ PwpbServerReaderWriter(const internal::LockedCallContext& context,
+ MethodType type = MethodType::kBidirectionalStreaming)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : internal::BasePwpbServerReader<Request>(context, type) {}
+};
+
+// The PwpbServerReader is used to receive typed messages and send a typed
+// response in a pw_protobuf client streaming RPC.
+//
+// These classes use private inheritance to hide the internal::Call API while
+// allow direct use of its public and protected functions.
+template <typename Request, typename Response>
+class PwpbServerReader : private internal::BasePwpbServerReader<Request> {
+ public:
+ // Creates a PwpbServerReader that is ready to send a response to a particular
+ // RPC. This can be used for testing or to finish an RPC that has not been
+ // started by the client.
+ template <auto kMethod, typename ServiceImpl>
+ [[nodiscard]] static PwpbServerReader Open(Server& server,
+ uint32_t channel_id,
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ static_assert(std::is_same_v<Request, typename MethodInfo::Request>,
+ "The request type of a PwpbServerReader must match "
+ "the method.");
+ static_assert(std::is_same_v<Response, typename MethodInfo::Response>,
+ "The response type of a PwpbServerReader must match "
+ "the method.");
+ return server
+ .OpenCall<PwpbServerReader, kMethod, MethodType::kClientStreaming>(
+ channel_id,
+ service,
+ internal::MethodLookup::GetPwpbMethod<ServiceImpl,
+ MethodInfo::kMethodId>());
+ }
+
+ // Allow default construction so that users can declare a variable into
+ // which to move server reader/writers from RPC calls.
+ constexpr PwpbServerReader() = default;
+
+ PwpbServerReader(PwpbServerReader&&) = default;
+ PwpbServerReader& operator=(PwpbServerReader&&) = default;
+
+ using internal::Call::active;
+ using internal::Call::channel_id;
+
+ // Functions for setting RPC event callbacks.
+ using internal::Call::set_on_error;
+ using internal::BasePwpbServerReader<Request>::set_on_next;
+ using internal::ServerCall::set_on_client_stream_end;
+
+ // Sends the response. Returns the following Status codes:
+ //
+ // OK - the response was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf message
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ Status Finish(const Response& response, Status status = OkStatus()) {
+ return internal::PwpbServerCall::SendUnaryResponse(response, status);
+ }
+
+ private:
+ friend class internal::PwpbMethod;
+ friend class Server;
+
+ template <typename, typename, uint32_t>
+ friend class internal::test::InvocationContext;
+
+ PwpbServerReader(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : internal::BasePwpbServerReader<Request>(context,
+ MethodType::kClientStreaming) {}
+};
+
+// The PwpbServerWriter is used to send typed responses in a pw_protobuf server
+// streaming RPC.
+//
+// These classes use private inheritance to hide the internal::Call API while
+// allow direct use of its public and protected functions.
+template <typename Response>
+class PwpbServerWriter : private internal::PwpbServerCall {
+ public:
+ // Creates a PwpbServerWriter that is ready to send responses for a particular
+ // RPC. This can be used for testing or to send responses to an RPC that has
+ // not been started by a client.
+ template <auto kMethod, typename ServiceImpl>
+ [[nodiscard]] static PwpbServerWriter Open(Server& server,
+ uint32_t channel_id,
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ static_assert(std::is_same_v<Response, typename MethodInfo::Response>,
+ "The response type of a PwpbServerWriter must match "
+ "the method.");
+ return server
+ .OpenCall<PwpbServerWriter, kMethod, MethodType::kServerStreaming>(
+ channel_id,
+ service,
+ internal::MethodLookup::GetPwpbMethod<ServiceImpl,
+ MethodInfo::kMethodId>());
+ }
+
+ // Allow default construction so that users can declare a variable into
+ // which to move server reader/writers from RPC calls.
+ constexpr PwpbServerWriter() = default;
+
+ PwpbServerWriter(PwpbServerWriter&&) = default;
+ PwpbServerWriter& operator=(PwpbServerWriter&&) = default;
+
+ using internal::Call::active;
+ using internal::Call::channel_id;
+
+ // Functions for setting RPC event callbacks.
+ using internal::Call::set_on_error;
+ using internal::ServerCall::set_on_client_stream_end;
+
+ // Writes a response. Returns the following Status codes:
+ //
+ // OK - the response was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf message
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ Status Write(const Response& response) {
+ return internal::PwpbServerCall::SendStreamResponse(response);
+ }
+
+ Status Finish(Status status = OkStatus()) {
+ return internal::Call::CloseAndSendResponse(status);
+ }
+
+ private:
+ friend class internal::PwpbMethod;
+ friend class Server;
+
+ template <typename, typename, uint32_t>
+ friend class internal::test::InvocationContext;
+
+ PwpbServerWriter(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : internal::PwpbServerCall(context, MethodType::kServerStreaming) {}
+};
+
+// The PwpbUnaryResponder is used to send a typed response in a pw_protobuf
+// unary RPC.
+//
+// These classes use private inheritance to hide the internal::Call API while
+// allow direct use of its public and protected functions.
+template <typename Response>
+class PwpbUnaryResponder : private internal::PwpbServerCall {
+ public:
+ // Creates a PwpbUnaryResponder that is ready to send responses for a
+ // particular RPC. This can be used for testing or to send responses to an
+ // RPC that has not been started by a client.
+ template <auto kMethod, typename ServiceImpl>
+ [[nodiscard]] static PwpbUnaryResponder Open(Server& server,
+ uint32_t channel_id,
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ using MethodInfo = internal::MethodInfo<kMethod>;
+ static_assert(std::is_same_v<Response, typename MethodInfo::Response>,
+ "The response type of a PwpbUnaryResponder must match "
+ "the method.");
+ return server
+ .OpenCall<PwpbUnaryResponder<Response>, kMethod, MethodType::kUnary>(
+ channel_id,
+ service,
+ internal::MethodLookup::GetPwpbMethod<ServiceImpl,
+ MethodInfo::kMethodId>());
+ }
+
+ // Allow default construction so that users can declare a variable into
+ // which to move server reader/writers from RPC calls.
+ constexpr PwpbUnaryResponder() = default;
+
+ PwpbUnaryResponder(PwpbUnaryResponder&&) = default;
+ PwpbUnaryResponder& operator=(PwpbUnaryResponder&&) = default;
+
+ using internal::ServerCall::active;
+ using internal::ServerCall::channel_id;
+
+ // Functions for setting RPC event callbacks.
+ using internal::Call::set_on_error;
+ using internal::ServerCall::set_on_client_stream_end;
+
+ // Sends the response. Returns the following Status codes:
+ //
+ // OK - the response was successfully sent
+ // FAILED_PRECONDITION - the writer is closed
+ // INTERNAL - pw_rpc was unable to encode the pw_protobuf message
+ // other errors - the ChannelOutput failed to send the packet; the error
+ // codes are determined by the ChannelOutput implementation
+ //
+ Status Finish(const Response& response, Status status = OkStatus()) {
+ return internal::PwpbServerCall::SendUnaryResponse(response, status);
+ }
+
+ private:
+ friend class internal::PwpbMethod;
+ friend class Server;
+
+ template <typename, typename, uint32_t>
+ friend class internal::test::InvocationContext;
+
+ PwpbUnaryResponder(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : internal::PwpbServerCall(context, MethodType::kUnary) {}
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/public/pw_rpc/pwpb/test_method_context.h b/pw_rpc/pwpb/public/pw_rpc/pwpb/test_method_context.h
new file mode 100644
index 000000000..958fd5de3
--- /dev/null
+++ b/pw_rpc/pwpb/public/pw_rpc/pwpb/test_method_context.h
@@ -0,0 +1,397 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <tuple>
+#include <utility>
+
+#include "pw_preprocessor/arguments.h"
+#include "pw_rpc/internal/hash.h"
+#include "pw_rpc/internal/method_lookup.h"
+#include "pw_rpc/internal/test_method_context.h"
+#include "pw_rpc/pwpb/fake_channel_output.h"
+#include "pw_rpc/pwpb/internal/method.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+#include "pw_span/span.h"
+
+namespace pw::rpc {
+
+// Declares a context object that may be used to invoke an RPC. The context is
+// declared with the name of the implemented service and the method to invoke.
+// The RPC can then be invoked with the call method.
+//
+// For a unary RPC, context.call(request) returns the status, and the response
+// struct can be accessed via context.response().
+//
+// PW_PWPB_TEST_METHOD_CONTEXT(my::CoolService, TheMethod) context;
+// EXPECT_EQ(OkStatus(), context.call({.some_arg = 123}).status());
+// EXPECT_EQ(500, context.response().some_response_value);
+//
+// For a unary RPC with repeated fields in the response, pw_protobuf uses a
+// callback field called when parsing the response as many times as the
+// field is present in the protobuf. To set the callback create the Response
+// struct and pass it to the response method:
+//
+// PW_PWPB_TEST_METHOD_CONTEXT(my::CoolService, TheMethod) context;
+// EXPECT_EQ(OkStatus(), context.call({.some_arg = 123}).status());
+//
+// TheMethodResponse::Message response{};
+// response.repeated_field.SetDecoder([](TheMethod::StreamDecoder& decoder) {
+// PW_TRY_ASSIGN(const auto value, decoder.ReadValue());
+// EXPECT_EQ(value, 123);
+// return OkStatus();
+// });
+// context.response(response); // Callbacks called from here.
+//
+// For a server streaming RPC, context.call(request) invokes the method. As in a
+// normal RPC, the method completes when the ServerWriter's Finish method is
+// called (or it goes out of scope).
+//
+// PW_PWPB_TEST_METHOD_CONTEXT(my::CoolService, TheStreamingMethod) context;
+// context.call({.some_arg = 123});
+//
+// EXPECT_TRUE(context.done()); // Check that the RPC completed
+// EXPECT_EQ(OkStatus(), context.status()); // Check the status
+//
+// EXPECT_EQ(3u, context.responses().size());
+// EXPECT_EQ(123, context.responses()[0].value); // check individual responses
+//
+// for (const MyResponse& response : context.responses()) {
+// // iterate over the responses
+// }
+//
+// PW_PWPB_TEST_METHOD_CONTEXT forwards its constructor arguments to the
+// underlying service. For example:
+//
+// PW_PWPB_TEST_METHOD_CONTEXT(MyService, Go) context(service, args);
+//
+// PW_PWPB_TEST_METHOD_CONTEXT takes one optional argument:
+//
+// size_t kMaxPackets: maximum packets to store
+//
+// Example:
+//
+// PW_PWPB_TEST_METHOD_CONTEXT(MyService, BestMethod, 3, 256) context;
+// ASSERT_EQ(3u, context.responses().max_size());
+//
+#define PW_PWPB_TEST_METHOD_CONTEXT(service, method, ...) \
+ ::pw::rpc::PwpbTestMethodContext<service, \
+ &service::method, \
+ ::pw::rpc::internal::Hash(#method) \
+ PW_COMMA_ARGS(__VA_ARGS__)>
+
+template <typename Service,
+ auto kMethod,
+ uint32_t kMethodId,
+ size_t kMaxPackets = 6,
+ size_t kPayloadsBufferSizeBytes = 256>
+class PwpbTestMethodContext;
+
+namespace internal::test::pwpb {
+
+// Collects everything needed to invoke a particular RPC.
+template <typename Service,
+ auto kMethod,
+ uint32_t kMethodId,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class PwpbInvocationContext
+ : public InvocationContext<
+ PwpbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ Service,
+ kMethodId> {
+ private:
+ using Base = InvocationContext<
+ PwpbFakeChannelOutput<kMaxPackets, kPayloadsBufferSizeBytes>,
+ Service,
+ kMethodId>;
+
+ public:
+ using Request = internal::Request<kMethod>;
+ using Response = internal::Response<kMethod>;
+
+ // Gives access to the RPC's most recent response.
+ Response response() const {
+ Response response{};
+ PW_ASSERT(kMethodInfo.serde()
+ .response()
+ .Decode(Base::responses().back(), response)
+ .ok());
+ return response;
+ }
+
+ // Gives access to the RPC's most recent response using passed Response object
+ // to parse using pw_protobuf. Use this version when you need to set callback
+ // fields in the Response object before parsing.
+ void response(Response& response) const {
+ PW_ASSERT(kMethodInfo.serde()
+ .response()
+ .Decode(Base::responses().back(), response)
+ .ok());
+ }
+
+ PwpbPayloadsView<Response> responses() const {
+ return Base::output().template payload_structs<Response>(
+ kMethodInfo.serde().response(),
+ MethodTraits<decltype(kMethod)>::kType,
+ Base::channel_id(),
+ internal::UnwrapServiceId(Base::service().service_id()),
+ kMethodId);
+ }
+
+ protected:
+ template <typename... Args>
+ PwpbInvocationContext(Args&&... args)
+ : Base(kMethodInfo,
+ MethodTraits<decltype(kMethod)>::kType,
+ std::forward<Args>(args)...) {}
+
+ template <size_t kEncodingBufferSizeBytes = 128>
+ void SendClientStream(const Request& request) PW_LOCKS_EXCLUDED(rpc_lock()) {
+ std::array<std::byte, kEncodingBufferSizeBytes> buffer;
+ // Clang 10.0.1 issue requires separate span variable declaration.
+ span buffer_span(buffer);
+ Base::SendClientStream(buffer_span.first(
+ kMethodInfo.serde().request().Encode(request, buffer).size()));
+ }
+
+ private:
+ static constexpr PwpbMethod kMethodInfo =
+ MethodLookup::GetPwpbMethod<Service, kMethodId>();
+};
+
+// Method invocation context for a unary RPC. Returns the status in
+// call_context() and provides the response through the response() method.
+template <typename Service,
+ auto kMethod,
+ uint32_t kMethodId,
+ size_t kPayloadsBufferSizeBytes>
+class UnaryContext : public PwpbInvocationContext<Service,
+ kMethod,
+ kMethodId,
+ 1,
+ kPayloadsBufferSizeBytes> {
+ private:
+ using Base = PwpbInvocationContext<Service,
+ kMethod,
+ kMethodId,
+ 1,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ using Request = typename Base::Request;
+ using Response = typename Base::Response;
+
+ template <typename... Args>
+ UnaryContext(Args&&... args) : Base(std::forward<Args>(args)...) {}
+
+ // Invokes the RPC with the provided request. Returns the status.
+ auto call(const Request& request) {
+ if constexpr (MethodTraits<decltype(kMethod)>::kSynchronous) {
+ Base::output().clear();
+
+ PwpbUnaryResponder<Response> responder =
+ Base::template GetResponder<PwpbUnaryResponder<Response>>();
+ Response response = {};
+ Status status =
+ CallMethodImplFunction<kMethod>(Base::service(), request, response);
+ PW_ASSERT(responder.Finish(response, status).ok());
+ return status;
+
+ } else {
+ Base::template call<kMethod, PwpbUnaryResponder<Response>>(request);
+ }
+ }
+};
+
+// Method invocation context for a server streaming RPC.
+template <typename Service,
+ auto kMethod,
+ uint32_t kMethodId,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class ServerStreamingContext
+ : public PwpbInvocationContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ using Base = PwpbInvocationContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ using Request = typename Base::Request;
+ using Response = typename Base::Response;
+
+ template <typename... Args>
+ ServerStreamingContext(Args&&... args) : Base(std::forward<Args>(args)...) {}
+
+ // Invokes the RPC with the provided request.
+ void call(const Request& request) {
+ Base::template call<kMethod, PwpbServerWriter<Response>>(request);
+ }
+
+ // Returns a server writer which writes responses into the context's buffer.
+ // This should not be called alongside call(); use one or the other.
+ PwpbServerWriter<Response> writer() {
+ return Base::template GetResponder<PwpbServerWriter<Response>>();
+ }
+};
+
+// Method invocation context for a client streaming RPC.
+template <typename Service,
+ auto kMethod,
+ uint32_t kMethodId,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class ClientStreamingContext
+ : public PwpbInvocationContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ using Base = PwpbInvocationContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ using Request = typename Base::Request;
+ using Response = typename Base::Response;
+
+ template <typename... Args>
+ ClientStreamingContext(Args&&... args) : Base(std::forward<Args>(args)...) {}
+
+ // Invokes the RPC.
+ void call() {
+ Base::template call<kMethod, PwpbServerReader<Request, Response>>();
+ }
+
+ // Returns a server reader which writes responses into the context's buffer.
+ // This should not be called alongside call(); use one or the other.
+ PwpbServerReader<Request, Response> reader() {
+ return Base::template GetResponder<PwpbServerReader<Request, Response>>();
+ }
+
+ // Allow sending client streaming packets.
+ using Base::SendClientStream;
+ using Base::SendClientStreamEnd;
+};
+
+// Method invocation context for a bidirectional streaming RPC.
+template <typename Service,
+ auto kMethod,
+ uint32_t kMethodId,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class BidirectionalStreamingContext
+ : public PwpbInvocationContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ private:
+ using Base = PwpbInvocationContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>;
+
+ public:
+ using Request = typename Base::Request;
+ using Response = typename Base::Response;
+
+ template <typename... Args>
+ BidirectionalStreamingContext(Args&&... args)
+ : Base(std::forward<Args>(args)...) {}
+
+ // Invokes the RPC.
+ void call() {
+ Base::template call<kMethod, PwpbServerReaderWriter<Request, Response>>();
+ }
+
+ // Returns a server reader which writes responses into the context's buffer.
+ // This should not be called alongside call(); use one or the other.
+ PwpbServerReaderWriter<Request, Response> reader_writer() {
+ return Base::template GetResponder<
+ PwpbServerReaderWriter<Request, Response>>();
+ }
+
+ // Allow sending client streaming packets.
+ using Base::SendClientStream;
+ using Base::SendClientStreamEnd;
+};
+
+// Alias to select the type of the context object to use based on which type of
+// RPC it is for.
+template <typename Service,
+ auto kMethod,
+ uint32_t kMethodId,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+using Context = std::tuple_element_t<
+ static_cast<size_t>(internal::MethodTraits<decltype(kMethod)>::kType),
+ std::tuple<
+ UnaryContext<Service, kMethod, kMethodId, kPayloadsBufferSizeBytes>,
+ ServerStreamingContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ ClientStreamingContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>,
+ BidirectionalStreamingContext<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>>>;
+
+} // namespace internal::test::pwpb
+
+template <typename Service,
+ auto kMethod,
+ uint32_t kMethodId,
+ size_t kMaxPackets,
+ size_t kPayloadsBufferSizeBytes>
+class PwpbTestMethodContext
+ : public internal::test::pwpb::Context<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes> {
+ public:
+ // Forwards constructor arguments to the service class.
+ template <typename... ServiceArgs>
+ PwpbTestMethodContext(ServiceArgs&&... service_args)
+ : internal::test::pwpb::Context<Service,
+ kMethod,
+ kMethodId,
+ kMaxPackets,
+ kPayloadsBufferSizeBytes>(
+ std::forward<ServiceArgs>(service_args)...) {}
+};
+
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/pw_rpc_pwpb_private/internal_test_utils.h b/pw_rpc/pwpb/pw_rpc_pwpb_private/internal_test_utils.h
new file mode 100644
index 000000000..7208841f0
--- /dev/null
+++ b/pw_rpc/pwpb/pw_rpc_pwpb_private/internal_test_utils.h
@@ -0,0 +1,69 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <array>
+#include <cstddef>
+
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_stream/memory_stream.h"
+
+namespace pw::rpc::internal {
+
+// Encodes a protobuf to a local span named by result from a list of pw_protobuf
+// struct initializers. Note that the proto namespace is passed, not the name
+// of the struct --- ie. exclude the "::Message" suffix.
+//
+// PW_ENCODE_PB(pw::rpc::TestProto, encoded, .value = 42);
+//
+#define PW_ENCODE_PB(proto, result, ...) \
+ _PW_ENCODE_PB_EXPAND(proto, result, __LINE__, __VA_ARGS__)
+
+#define _PW_ENCODE_PB_EXPAND(proto, result, unique, ...) \
+ _PW_ENCODE_PB_IMPL(proto, result, unique, __VA_ARGS__)
+
+#define _PW_ENCODE_PB_IMPL(proto, result, unique, ...) \
+ std::array<std::byte, 2 * sizeof(proto::Message)> _pb_buffer_##unique{}; \
+ const span result = \
+ ::pw::rpc::internal::EncodeProtobuf<proto::Message, \
+ proto::MemoryEncoder>( \
+ proto::Message{__VA_ARGS__}, _pb_buffer_##unique)
+
+template <typename Message, typename MemoryEncoder>
+span<const std::byte> EncodeProtobuf(const Message& message,
+ span<std::byte> buffer) {
+ MemoryEncoder encoder(buffer);
+ EXPECT_EQ(encoder.Write(message), OkStatus());
+ return buffer.first(encoder.size());
+}
+
+// Decodes a protobuf to a pw_protobuf struct named by result. Note that the
+// proto namespace is passed, not the name of the struct --- ie. exclude the
+// "::Message" suffix.
+//
+// PW_DECODE_PB(pw::rpc::TestProto, decoded, buffer);
+//
+#define PW_DECODE_PB(proto, result, buffer) \
+ proto::Message result; \
+ ::pw::rpc::internal::DecodeProtobuf<proto::Message, proto::StreamDecoder>( \
+ buffer, result);
+
+template <typename Message, typename StreamDecoder>
+void DecodeProtobuf(span<const std::byte> buffer, Message& message) {
+ stream::MemoryReader reader(buffer);
+ EXPECT_EQ(StreamDecoder(reader).Read(message), OkStatus());
+}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/pwpb/serde_test.cc b/pw_rpc/pwpb/serde_test.cc
new file mode 100644
index 000000000..d9da19749
--- /dev/null
+++ b/pw_rpc/pwpb/serde_test.cc
@@ -0,0 +1,56 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_rpc/pwpb/internal/common.h"
+#include "pw_rpc_test_protos/test.pwpb.h"
+#include "pw_span/span.h"
+#include "pw_status/status_with_size.h"
+
+namespace pw::rpc::internal {
+namespace {
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+
+constexpr PwpbSerde kTestRequest(&TestRequest::kMessageFields);
+constexpr TestRequest::Message kProto{.integer = 3, .status_code = 0};
+
+TEST(PwpbSerde, Encode) {
+ std::byte buffer[32] = {};
+
+ StatusWithSize result = kTestRequest.Encode(kProto, buffer);
+ EXPECT_EQ(OkStatus(), result.status());
+ EXPECT_EQ(result.size(), 2u);
+ EXPECT_EQ(buffer[0], std::byte{1} << 3);
+ EXPECT_EQ(buffer[1], std::byte{3});
+}
+
+TEST(PwpbSerde, Encode_TooSmall) {
+ std::byte buffer[1] = {};
+ EXPECT_EQ(Status::ResourceExhausted(),
+ kTestRequest.Encode(kProto, buffer).status());
+}
+
+TEST(PwpbSerde, Decode) {
+ constexpr std::byte buffer[]{std::byte{1} << 3, std::byte{3}};
+ TestRequest::Message proto = {};
+
+ EXPECT_EQ(OkStatus(), kTestRequest.Decode(buffer, proto));
+
+ EXPECT_EQ(3, proto.integer);
+ EXPECT_EQ(0u, proto.status_code);
+}
+
+} // namespace
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/pwpb/server_callback_test.cc b/pw_rpc/pwpb/server_callback_test.cc
new file mode 100644
index 000000000..780279a3d
--- /dev/null
+++ b/pw_rpc/pwpb/server_callback_test.cc
@@ -0,0 +1,95 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <array>
+
+#include "gtest/gtest.h"
+#include "pw_containers/vector.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+#include "pw_rpc/service.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+
+namespace pw::rpc {
+namespace {
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+class TestServiceImpl final
+ : public test::pw_rpc::pwpb::TestService::Service<TestServiceImpl> {
+ public:
+ Status TestUnaryRpc(const TestRequest::Message&,
+ TestResponse::Message& response) {
+ response.value = 42;
+ return OkStatus();
+ }
+
+ Status TestAnotherUnaryRpc(const TestRequest::Message&,
+ TestResponse::Message& response) {
+ response.value = 42;
+ response.repeated_field.SetEncoder(
+ [](TestResponse::StreamEncoder& encoder) {
+ constexpr std::array<uint32_t, 3> kValues = {7, 8, 9};
+ return encoder.WriteRepeatedField(kValues);
+ });
+ return OkStatus();
+ }
+
+ void TestServerStreamRpc(const TestRequest::Message&,
+ PwpbServerWriter<TestStreamResponse::Message>&) {}
+
+ void TestClientStreamRpc(
+ PwpbServerReader<TestRequest::Message, TestStreamResponse::Message>&) {}
+
+ void TestBidirectionalStreamRpc(
+ PwpbServerReaderWriter<TestRequest::Message,
+ TestStreamResponse::Message>&) {}
+};
+
+TEST(PwpbTestMethodContext, ResponseWithoutCallbacks) {
+ // Calling response() without an argument returns a Response struct without
+ // any callbacks set.
+ PW_PWPB_TEST_METHOD_CONTEXT(TestServiceImpl, TestUnaryRpc) ctx;
+ ASSERT_EQ(ctx.call({}), OkStatus());
+
+ TestResponse::Message response = ctx.response();
+ EXPECT_EQ(42, response.value);
+}
+
+TEST(PwpbTestMethodContext, ResponseWithCallbacks) {
+ PW_PWPB_TEST_METHOD_CONTEXT(TestServiceImpl, TestAnotherUnaryRpc) ctx;
+ ASSERT_EQ(ctx.call({}), OkStatus());
+
+ // To decode a response object that requires to set callbacks, pass it to the
+ // response() method as a parameter.
+ pw::Vector<uint32_t, 4> values{};
+
+ TestResponse::Message response{};
+ response.repeated_field.SetDecoder(
+ [&values](TestResponse::StreamDecoder& decoder) {
+ return decoder.ReadRepeatedField(values);
+ });
+ ctx.response(response);
+
+ EXPECT_EQ(42, response.value);
+
+ EXPECT_EQ(3u, values.size());
+ EXPECT_EQ(7u, values[0]);
+ EXPECT_EQ(8u, values[1]);
+ EXPECT_EQ(9u, values[2]);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/server_reader_writer.cc b/pw_rpc/pwpb/server_reader_writer.cc
new file mode 100644
index 000000000..365403dd7
--- /dev/null
+++ b/pw_rpc/pwpb/server_reader_writer.cc
@@ -0,0 +1,26 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/pwpb/server_reader_writer.h"
+
+#include "pw_rpc/pwpb/internal/method.h"
+
+namespace pw::rpc::internal {
+
+PwpbServerCall::PwpbServerCall(const LockedCallContext& context,
+ MethodType type)
+ : ServerCall(context, CallProperties(type, kServerCall, kProtoStruct)),
+ serde_(&static_cast<const PwpbMethod&>(context.method()).serde()) {}
+
+} // namespace pw::rpc::internal
diff --git a/pw_rpc/pwpb/server_reader_writer_test.cc b/pw_rpc/pwpb/server_reader_writer_test.cc
new file mode 100644
index 000000000..07b96c826
--- /dev/null
+++ b/pw_rpc/pwpb/server_reader_writer_test.cc
@@ -0,0 +1,318 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/pwpb/server_reader_writer.h"
+
+#include <optional>
+
+#include "gtest/gtest.h"
+#include "pw_rpc/pwpb/fake_channel_output.h"
+#include "pw_rpc/pwpb/test_method_context.h"
+#include "pw_rpc/service.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+
+namespace pw::rpc {
+namespace {
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
+class TestServiceImpl final
+ : public test::pw_rpc::pwpb::TestService::Service<TestServiceImpl> {
+ public:
+ Status TestUnaryRpc(const TestRequest::Message&, TestResponse::Message&) {
+ return OkStatus();
+ }
+
+ void TestAnotherUnaryRpc(const TestRequest::Message&,
+ PwpbUnaryResponder<TestResponse::Message>&) {}
+
+ void TestServerStreamRpc(const TestRequest::Message&,
+ PwpbServerWriter<TestStreamResponse::Message>&) {}
+
+ void TestClientStreamRpc(
+ PwpbServerReader<TestRequest::Message, TestStreamResponse::Message>&) {}
+
+ void TestBidirectionalStreamRpc(
+ PwpbServerReaderWriter<TestRequest::Message,
+ TestStreamResponse::Message>&) {}
+};
+
+template <auto kMethod>
+struct ReaderWriterTestContext {
+ using Info = internal::MethodInfo<kMethod>;
+
+ static constexpr uint32_t kChannelId = 1;
+
+ ReaderWriterTestContext()
+ : channel(Channel::Create<kChannelId>(&output)),
+ server(span(&channel, 1)) {}
+
+ TestServiceImpl service;
+ PwpbFakeChannelOutput<4> output;
+ Channel channel;
+ Server server;
+};
+
+using test::pw_rpc::pwpb::TestService;
+
+TEST(PwpbUnaryResponder, DefaultConstructed) {
+ PwpbUnaryResponder<TestResponse::Message> call;
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Finish({}, OkStatus()));
+
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbServerWriter, DefaultConstructed) {
+ PwpbServerWriter<TestStreamResponse::Message> call;
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Finish(OkStatus()));
+
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbServerReader, DefaultConstructed) {
+ PwpbServerReader<TestRequest::Message, TestStreamResponse::Message> call;
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Finish({}, OkStatus()));
+
+ call.set_on_next([](const TestRequest::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbServerReaderWriter, DefaultConstructed) {
+ PwpbServerReaderWriter<TestRequest::Message, TestStreamResponse::Message>
+ call;
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Finish(OkStatus()));
+
+ call.set_on_next([](const TestRequest::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbUnaryResponder, Closed) {
+ ReaderWriterTestContext<TestService::TestUnaryRpc> ctx;
+ PwpbUnaryResponder call = PwpbUnaryResponder<TestResponse::Message>::Open<
+ TestService::TestUnaryRpc>(ctx.server, ctx.channel.id(), ctx.service);
+ ASSERT_EQ(OkStatus(), call.Finish({}, OkStatus()));
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Finish({}, OkStatus()));
+
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbServerWriter, Closed) {
+ ReaderWriterTestContext<TestService::TestServerStreamRpc> ctx;
+ PwpbServerWriter call = PwpbServerWriter<TestStreamResponse::Message>::Open<
+ TestService::TestServerStreamRpc>(
+ ctx.server, ctx.channel.id(), ctx.service);
+ ASSERT_EQ(OkStatus(), call.Finish(OkStatus()));
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Finish(OkStatus()));
+
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbServerReader, Closed) {
+ ReaderWriterTestContext<TestService::TestClientStreamRpc> ctx;
+ PwpbServerReader call =
+ PwpbServerReader<TestRequest::Message, TestStreamResponse::Message>::Open<
+ TestService::TestClientStreamRpc>(
+ ctx.server, ctx.channel.id(), ctx.service);
+ ASSERT_EQ(OkStatus(), call.Finish({}, OkStatus()));
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Finish({}, OkStatus()));
+
+ call.set_on_next([](const TestRequest::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbServerReaderWriter, Closed) {
+ ReaderWriterTestContext<TestService::TestBidirectionalStreamRpc> ctx;
+ PwpbServerReaderWriter call =
+ PwpbServerReaderWriter<TestRequest::Message,
+ TestStreamResponse::Message>::
+ Open<TestService::TestBidirectionalStreamRpc>(
+ ctx.server, ctx.channel.id(), ctx.service);
+ ASSERT_EQ(OkStatus(), call.Finish(OkStatus()));
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Finish(OkStatus()));
+
+ call.set_on_next([](const TestRequest::Message&) {});
+ call.set_on_error([](Status) {});
+}
+
+TEST(PwpbUnaryResponder, Open_ReturnsUsableResponder) {
+ ReaderWriterTestContext<TestService::TestUnaryRpc> ctx;
+ PwpbUnaryResponder responder =
+ PwpbUnaryResponder<TestResponse::Message>::Open<
+ TestService::TestUnaryRpc>(ctx.server, ctx.channel.id(), ctx.service);
+
+ ASSERT_EQ(OkStatus(),
+ responder.Finish({.value = 4321, .repeated_field = {}}));
+
+ EXPECT_EQ(ctx.output.last_response<TestService::TestUnaryRpc>().value, 4321);
+ EXPECT_EQ(ctx.output.last_status(), OkStatus());
+}
+
+TEST(PwpbServerWriter, Open_ReturnsUsableWriter) {
+ ReaderWriterTestContext<TestService::TestServerStreamRpc> ctx;
+ PwpbServerWriter responder =
+ PwpbServerWriter<TestStreamResponse::Message>::Open<
+ TestService::TestServerStreamRpc>(
+ ctx.server, ctx.channel.id(), ctx.service);
+
+ ASSERT_EQ(OkStatus(), responder.Write({.chunk = {}, .number = 321}));
+ ASSERT_EQ(OkStatus(), responder.Finish());
+
+ EXPECT_EQ(ctx.output.last_response<TestService::TestServerStreamRpc>().number,
+ 321u);
+ EXPECT_EQ(ctx.output.last_status(), OkStatus());
+}
+
+TEST(PwpbServerReader, Open_ReturnsUsableReader) {
+ ReaderWriterTestContext<TestService::TestClientStreamRpc> ctx;
+ PwpbServerReader responder =
+ PwpbServerReader<TestRequest::Message, TestStreamResponse::Message>::Open<
+ TestService::TestClientStreamRpc>(
+ ctx.server, ctx.channel.id(), ctx.service);
+
+ ASSERT_EQ(OkStatus(), responder.Finish({.chunk = {}, .number = 321}));
+
+ EXPECT_EQ(ctx.output.last_response<TestService::TestClientStreamRpc>().number,
+ 321u);
+}
+
+TEST(PwpbServerReaderWriter, Open_ReturnsUsableReaderWriter) {
+ ReaderWriterTestContext<TestService::TestBidirectionalStreamRpc> ctx;
+ PwpbServerReaderWriter responder =
+ PwpbServerReaderWriter<TestRequest::Message,
+ TestStreamResponse::Message>::
+ Open<TestService::TestBidirectionalStreamRpc>(
+ ctx.server, ctx.channel.id(), ctx.service);
+
+ ASSERT_EQ(OkStatus(), responder.Write({.chunk = {}, .number = 321}));
+ ASSERT_EQ(OkStatus(), responder.Finish(Status::NotFound()));
+
+ EXPECT_EQ(ctx.output.last_response<TestService::TestBidirectionalStreamRpc>()
+ .number,
+ 321u);
+ EXPECT_EQ(ctx.output.last_status(), Status::NotFound());
+}
+
+TEST(RawServerReaderWriter, Open_UnknownChannel) {
+ ReaderWriterTestContext<TestService::TestBidirectionalStreamRpc> ctx;
+ ASSERT_EQ(OkStatus(), ctx.server.CloseChannel(ctx.kChannelId));
+
+ PwpbServerReaderWriter call =
+ PwpbServerReaderWriter<TestRequest::Message,
+ TestStreamResponse::Message>::
+ Open<TestService::TestBidirectionalStreamRpc>(
+ ctx.server, ctx.kChannelId, ctx.service);
+
+ EXPECT_TRUE(call.active());
+ EXPECT_EQ(call.channel_id(), ctx.kChannelId);
+ EXPECT_EQ(Status::Unavailable(), call.Write({}));
+
+ ASSERT_EQ(OkStatus(), ctx.server.OpenChannel(ctx.kChannelId, ctx.output));
+
+ EXPECT_EQ(OkStatus(), call.Write({}));
+ EXPECT_TRUE(call.active());
+
+ EXPECT_EQ(OkStatus(), call.Finish());
+ EXPECT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+}
+
+TEST(RawServerReaderWriter, Open_MultipleTimes_CancelsPrevious) {
+ ReaderWriterTestContext<TestService::TestBidirectionalStreamRpc> ctx;
+
+ PwpbServerReaderWriter one =
+ PwpbServerReaderWriter<TestRequest::Message,
+ TestStreamResponse::Message>::
+ Open<TestService::TestBidirectionalStreamRpc>(
+ ctx.server, ctx.kChannelId, ctx.service);
+
+ std::optional<Status> error;
+ one.set_on_error([&error](Status status) { error = status; });
+
+ ASSERT_TRUE(one.active());
+
+ PwpbServerReaderWriter two =
+ PwpbServerReaderWriter<TestRequest::Message,
+ TestStreamResponse::Message>::
+ Open<TestService::TestBidirectionalStreamRpc>(
+ ctx.server, ctx.kChannelId, ctx.service);
+
+ EXPECT_FALSE(one.active());
+ EXPECT_TRUE(two.active());
+
+ EXPECT_EQ(Status::Cancelled(), error);
+}
+
+TEST(PwpbServerReader, CallbacksMoveCorrectly) {
+ PW_PWPB_TEST_METHOD_CONTEXT(TestServiceImpl, TestClientStreamRpc) ctx;
+
+ PwpbServerReader call_1 = ctx.reader();
+
+ ASSERT_TRUE(call_1.active());
+
+ TestRequest::Message received_request = {.integer = 12345678,
+ .status_code = 1};
+
+ call_1.set_on_next([&received_request](const TestRequest::Message& value) {
+ received_request = value;
+ });
+
+ PwpbServerReader<TestRequest::Message, TestStreamResponse::Message> call_2;
+ call_2 = std::move(call_1);
+
+ constexpr TestRequest::Message request{.integer = 600613, .status_code = 2};
+ ctx.SendClientStream(request);
+ EXPECT_EQ(request.integer, received_request.integer);
+ EXPECT_EQ(request.status_code, received_request.status_code);
+}
+
+} // namespace
+} // namespace pw::rpc
diff --git a/pw_rpc/pwpb/stub_generation_test.cc b/pw_rpc/pwpb/stub_generation_test.cc
new file mode 100644
index 000000000..cf1919ff8
--- /dev/null
+++ b/pw_rpc/pwpb/stub_generation_test.cc
@@ -0,0 +1,29 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// This macro is used to remove the generated stubs from the proto files. Define
+// so that the generated stubs can be tested.
+#define _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS
+
+#include "gtest/gtest.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+
+namespace {
+
+TEST(PwpbServiceStub, GeneratedStubCompiles) {
+ ::pw::rpc::test::TestService test_service;
+ EXPECT_STREQ(test_service.name(), "TestService");
+}
+
+} // namespace
diff --git a/pw_rpc/pwpb/synchronous_call_test.cc b/pw_rpc/pwpb/synchronous_call_test.cc
new file mode 100644
index 000000000..33a639658
--- /dev/null
+++ b/pw_rpc/pwpb/synchronous_call_test.cc
@@ -0,0 +1,238 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/synchronous_call.h"
+
+#include <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/pwpb/fake_channel_output.h"
+#include "pw_rpc_test_protos/test.rpc.pwpb.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_thread/thread.h"
+#include "pw_work_queue/test_thread.h"
+#include "pw_work_queue/work_queue.h"
+
+namespace pw::rpc::test {
+namespace {
+
+using pw::rpc::test::pw_rpc::pwpb::TestService;
+using MethodInfo = internal::MethodInfo<TestService::TestUnaryRpc>;
+
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+
+class SynchronousCallTest : public ::testing::Test {
+ public:
+ SynchronousCallTest()
+ : channels_({{Channel::Create<42>(&fake_output_)}}), client_(channels_) {}
+
+ void SetUp() override {
+ work_thread_ =
+ thread::Thread(work_queue::test::WorkQueueThreadOptions(), work_queue_);
+ }
+
+ void TearDown() override {
+ work_queue_.RequestStop();
+ work_thread_.join();
+ }
+
+ protected:
+ using FakeChannelOutput = PwpbFakeChannelOutput<2>;
+
+ void OnSend(span<const std::byte> buffer, Status status) {
+ if (!status.ok()) {
+ return;
+ }
+ auto result = internal::Packet::FromBuffer(buffer);
+ EXPECT_TRUE(result.ok());
+ request_packet_ = *result;
+
+ EXPECT_TRUE(work_queue_.PushWork([this]() { SendResponse(); }).ok());
+ }
+
+ void SendResponse() {
+ std::array<std::byte, 256> buffer;
+ std::array<std::byte, 32> payload_buffer;
+
+ StatusWithSize size_status =
+ MethodInfo::serde().response().Encode(response_, payload_buffer);
+ EXPECT_TRUE(size_status.ok());
+
+ auto response =
+ internal::Packet::Response(request_packet_, response_status_);
+ response.set_payload({payload_buffer.data(), size_status.size()});
+ EXPECT_TRUE(client_.ProcessPacket(response.Encode(buffer).value()).ok());
+ }
+
+ void set_response(const TestResponse::Message& response,
+ Status response_status = OkStatus()) {
+ response_ = response;
+ response_status_ = response_status;
+ output().set_on_send([this](span<const std::byte> buffer, Status status) {
+ OnSend(buffer, status);
+ });
+ }
+
+ MethodInfo::GeneratedClient generated_client() {
+ return MethodInfo::GeneratedClient(client(), channel().id());
+ }
+
+ FakeChannelOutput& output() { return fake_output_; }
+ const Channel& channel() const { return channels_.front(); }
+ Client& client() { return client_; }
+
+ private:
+ FakeChannelOutput fake_output_;
+ std::array<Channel, 1> channels_;
+ Client client_;
+ thread::Thread work_thread_;
+ work_queue::WorkQueueWithBuffer<1> work_queue_;
+ TestResponse::Message response_{};
+ Status response_status_ = OkStatus();
+ internal::Packet request_packet_;
+};
+
+TEST_F(SynchronousCallTest, SynchronousCallSuccess) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+ TestResponse::Message response{.value = 42, .repeated_field{}};
+
+ set_response(response, OkStatus());
+
+ auto result = SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), request);
+ EXPECT_TRUE(result.ok());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+TEST_F(SynchronousCallTest, SynchronousCallServerError) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+ TestResponse::Message response{.value = 42, .repeated_field{}};
+
+ set_response(response, Status::Internal());
+
+ auto result = SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), request);
+ EXPECT_TRUE(result.is_error());
+ EXPECT_EQ(result.status(), Status::Internal());
+
+ // We should still receive the response
+ EXPECT_TRUE(result.is_server_response());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+TEST_F(SynchronousCallTest, SynchronousCallRpcError) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+
+ // Internally, if Channel receives a non-ok status from the
+ // ChannelOutput::Send, it will always return Unknown.
+ output().set_send_status(Status::Unknown());
+
+ auto result = SynchronousCall<TestService::TestUnaryRpc>(
+ client(), channel().id(), request);
+ EXPECT_TRUE(result.is_rpc_error());
+ EXPECT_EQ(result.status(), Status::Unknown());
+}
+
+TEST_F(SynchronousCallTest, SynchronousCallForTimeoutError) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+
+ auto result = SynchronousCallFor<TestService::TestUnaryRpc>(
+ client(),
+ channel().id(),
+ request,
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1)));
+
+ EXPECT_TRUE(result.is_timeout());
+ EXPECT_EQ(result.status(), Status::DeadlineExceeded());
+}
+
+TEST_F(SynchronousCallTest, SynchronousCallUntilTimeoutError) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+
+ auto result = SynchronousCallUntil<TestService::TestUnaryRpc>(
+ client(), channel().id(), request, chrono::SystemClock::now());
+
+ EXPECT_TRUE(result.is_timeout());
+ EXPECT_EQ(result.status(), Status::DeadlineExceeded());
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallSuccess) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+ TestResponse::Message response{.value = 42, .repeated_field{}};
+
+ set_response(response, OkStatus());
+
+ auto result =
+ SynchronousCall<TestService::TestUnaryRpc>(generated_client(), request);
+ EXPECT_TRUE(result.ok());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallServerError) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+ TestResponse::Message response{.value = 42, .repeated_field{}};
+
+ set_response(response, Status::Internal());
+
+ auto result =
+ SynchronousCall<TestService::TestUnaryRpc>(generated_client(), request);
+ EXPECT_TRUE(result.is_error());
+ EXPECT_EQ(result.status(), Status::Internal());
+
+ // We should still receive the response
+ EXPECT_TRUE(result.is_server_response());
+ EXPECT_EQ(result.response().value, 42);
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallRpcError) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+
+ // Internally, if Channel receives a non-ok status from the
+ // ChannelOutput::Send, it will always return Unknown.
+ output().set_send_status(Status::Unknown());
+
+ auto result =
+ SynchronousCall<TestService::TestUnaryRpc>(generated_client(), request);
+ EXPECT_TRUE(result.is_rpc_error());
+ EXPECT_EQ(result.status(), Status::Unknown());
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallForTimeoutError) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+
+ auto result = SynchronousCallFor<TestService::TestUnaryRpc>(
+ generated_client(),
+ request,
+ chrono::SystemClock::for_at_least(std::chrono::milliseconds(1)));
+
+ EXPECT_TRUE(result.is_timeout());
+ EXPECT_EQ(result.status(), Status::DeadlineExceeded());
+}
+
+TEST_F(SynchronousCallTest, GeneratedClientSynchronousCallUntilTimeoutError) {
+ TestRequest::Message request{.integer = 5, .status_code = 0};
+
+ auto result = SynchronousCallUntil<TestService::TestUnaryRpc>(
+ generated_client(), request, chrono::SystemClock::now());
+
+ EXPECT_TRUE(result.is_timeout());
+ EXPECT_EQ(result.status(), Status::DeadlineExceeded());
+}
+} // namespace
+} // namespace pw::rpc::test
diff --git a/pw_rpc/py/Android.bp b/pw_rpc/py/Android.bp
new file mode 100644
index 000000000..10b3aedbe
--- /dev/null
+++ b/pw_rpc/py/Android.bp
@@ -0,0 +1,47 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+python_binary_host {
+ name: "pw_rpc_plugin_nanopb_py",
+ main: "pw_rpc/plugin_nanopb.py",
+ srcs: [
+ "pw_rpc/**/*.py",
+ ],
+ libs: [
+ "libprotobuf-python",
+ "pw_protobuf_compiler_py_lib",
+ "pw_protobuf_plugin_py_lib",
+ "pw_rpc_internal_packet_py_lib",
+ "pw_status_py_lib",
+ ],
+}
+
+python_binary_host {
+ name: "pw_rpc_plugin_rawpb_py",
+ main: "pw_rpc/plugin_raw.py",
+ srcs: [
+ "pw_rpc/**/*.py",
+ ],
+ libs: [
+ "libprotobuf-python",
+ "pw_protobuf_compiler_py_lib",
+ "pw_protobuf_plugin_py_lib",
+ "pw_rpc_internal_packet_py_lib",
+ "pw_status_py_lib",
+ ],
+} \ No newline at end of file
diff --git a/pw_rpc/py/BUILD.bazel b/pw_rpc/py/BUILD.bazel
index 4d9d454fe..64c9c8b29 100644
--- a/pw_rpc/py/BUILD.bazel
+++ b/pw_rpc/py/BUILD.bazel
@@ -27,6 +27,7 @@ filegroup(
"pw_rpc/callback_client/impl.py",
"pw_rpc/codegen.py",
"pw_rpc/codegen_nanopb.py",
+ "pw_rpc/codegen_pwpb.py",
"pw_rpc/codegen_raw.py",
"pw_rpc/console_tools/__init__.py",
"pw_rpc/console_tools/console.py",
@@ -37,6 +38,7 @@ filegroup(
"pw_rpc/packets.py",
"pw_rpc/plugin.py",
"pw_rpc/plugin_nanopb.py",
+ "pw_rpc/plugin_pwpb.py",
"pw_rpc/plugin_raw.py",
],
)
@@ -69,6 +71,20 @@ py_binary(
],
)
+py_binary(
+ name = "plugin_pwpb",
+ srcs = [":pw_rpc_common_sources"],
+ imports = ["."],
+ main = "pw_rpc/plugin_pwpb.py",
+ python_version = "PY3",
+ deps = [
+ "//pw_protobuf/py:plugin_library",
+ "//pw_protobuf_compiler/py:pw_protobuf_compiler",
+ "//pw_status/py:pw_status",
+ "@com_google_protobuf//:protobuf_python",
+ ],
+)
+
py_library(
name = "pw_rpc",
srcs = [
@@ -76,45 +92,77 @@ py_library(
"pw_rpc/client.py",
":pw_rpc_common_sources",
],
- # TODO(tonymd): Figure out the right way to handle generated protos
- # data = [
- # # Copy packet_pb2.py
- # # From: //pw_rpc/internal/packet_pb2.py
- # # To: //pw_rpc/py/pw_rpc/internal/packet_pb2.py
- # ":copy_packet_pb2",
- # ],
imports = ["."],
deps = [
"//pw_protobuf/py:pw_protobuf",
"//pw_protobuf_compiler/py:pw_protobuf_compiler",
+ "//pw_rpc:internal_packet_proto_pb2",
+ "//pw_status/py:pw_status",
+ ],
+)
+
+py_test(
+ name = "callback_client_test",
+ size = "small",
+ srcs = [
+ "tests/callback_client_test.py",
+ ],
+ deps = [
+ ":pw_rpc",
+ "//pw_protobuf_compiler:pw_protobuf_compiler_protos",
+ "//pw_rpc:internal_packet_proto_pb2",
+ "//pw_status/py:pw_status",
+ ],
+)
+
+py_test(
+ name = "client_test",
+ size = "small",
+ srcs = [
+ "tests/client_test.py",
+ ],
+ deps = [
+ ":pw_rpc",
+ "//pw_rpc:internal_packet_proto_pb2",
"//pw_status/py:pw_status",
],
)
-# TODO(tonymd): Figure out the right way to handle generated protos
-# load("@bazel_skylib//rules:copy_file.bzl", "copy_file")
-# copy_file(
-# name = "copy_packet_pb2",
-# src = "//pw_rpc:internal_packet_proto_pb2",
-# out = "pw_rpc/internal/packet_pb2.py",
-# allow_symlink = True,
-# )
+py_test(
+ name = "descriptors_test",
+ size = "small",
+ srcs = [
+ "tests/descriptors_test.py",
+ ],
+ deps = [
+ ":pw_rpc",
+ "//pw_protobuf_compiler:pw_protobuf_compiler_protos",
+ "@com_google_protobuf//:protobuf_python",
+ ],
+)
+
+py_test(
+ name = "ids_test",
+ size = "small",
+ srcs = [
+ "tests/ids_test.py",
+ ],
+ deps = [
+ ":pw_rpc",
+ "//pw_build/py:pw_build",
+ "//pw_protobuf_compiler:pw_protobuf_compiler_protos",
+ ],
+)
-# TODO(tonymd): Figure out the right way to handle generated protos
-# py_test(
-# name = "client_test",
-# size = "small",
-# srcs = [
-# "tests/client_test.py",
-# ],
-# data = [
-# # Copy packet_pb2.py
-# # From: //pw_rpc/internal/packet_pb2.py
-# # To: //pw_rpc/py/pw_rpc/internal/packet_pb2.py
-# ":copy_packet_pb2",
-# ],
-# deps = [
-# ":pw_rpc",
-# "//pw_rpc:internal_packet_proto_pb2",
-# ],
-# )
+py_test(
+ name = "packets_test",
+ size = "small",
+ srcs = [
+ "tests/packets_test.py",
+ ],
+ deps = [
+ ":pw_rpc",
+ "//pw_rpc:internal_packet_proto_pb2",
+ "//pw_status/py:pw_status",
+ ],
+)
diff --git a/pw_rpc/py/BUILD.gn b/pw_rpc/py/BUILD.gn
index c9080f8d7..2c091a70f 100644
--- a/pw_rpc/py/BUILD.gn
+++ b/pw_rpc/py/BUILD.gn
@@ -38,6 +38,7 @@ pw_python_package("py") {
"pw_rpc/client.py",
"pw_rpc/codegen.py",
"pw_rpc/codegen_nanopb.py",
+ "pw_rpc/codegen_pwpb.py",
"pw_rpc/codegen_raw.py",
"pw_rpc/console_tools/__init__.py",
"pw_rpc/console_tools/console.py",
@@ -45,9 +46,11 @@ pw_python_package("py") {
"pw_rpc/console_tools/watchdog.py",
"pw_rpc/descriptors.py",
"pw_rpc/ids.py",
+ "pw_rpc/lossy_channel.py",
"pw_rpc/packets.py",
"pw_rpc/plugin.py",
"pw_rpc/plugin_nanopb.py",
+ "pw_rpc/plugin_pwpb.py",
"pw_rpc/plugin_raw.py",
"pw_rpc/testing.py",
]
@@ -66,8 +69,13 @@ pw_python_package("py") {
"$dir_pw_protobuf_compiler/py",
"$dir_pw_status/py",
]
- python_test_deps = [ "$dir_pw_build/py" ]
+ python_test_deps = [
+ "$dir_pw_build/py",
+ "$dir_pw_log:protos.python",
+ "$dir_pw_tokenizer/py:test_proto.python",
+ ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
proto_library = "..:protos"
}
@@ -80,10 +88,15 @@ pw_python_script("python_client_cpp_server_test") {
sources = [ "tests/python_client_cpp_server_test.py" ]
python_deps = [
":py",
+ "$dir_pw_build/py",
"$dir_pw_hdlc/py",
+ "$dir_pw_log:protos.python",
"$dir_pw_status/py",
+ "$dir_pw_tokenizer/py:test_proto.python",
]
+
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
action = {
args = [
diff --git a/pw_rpc/py/pw_rpc/__init__.py b/pw_rpc/py/pw_rpc/__init__.py
index ff1f8713b..45948b19b 100644
--- a/pw_rpc/py/pw_rpc/__init__.py
+++ b/pw_rpc/py/pw_rpc/__init__.py
@@ -13,5 +13,9 @@
# the License.
"""Package for calling Pigweed RPCs from Python."""
+from pkgutil import extend_path # type: ignore
+
+__path__ = extend_path(__path__, __name__) # type: ignore
+
from pw_rpc.client import Client
from pw_rpc.descriptors import Channel, ChannelManipulator
diff --git a/pw_rpc/py/pw_rpc/callback_client/call.py b/pw_rpc/py/pw_rpc/callback_client/call.py
index abe108385..6cb09aaef 100644
--- a/pw_rpc/py/pw_rpc/callback_client/call.py
+++ b/pw_rpc/py/pw_rpc/callback_client/call.py
@@ -17,8 +17,17 @@ import enum
import logging
import math
import queue
-from typing import (Any, Callable, Iterable, Iterator, NamedTuple, Union,
- Optional, Sequence, TypeVar)
+from typing import (
+ Any,
+ Callable,
+ Iterable,
+ Iterator,
+ NamedTuple,
+ Union,
+ Optional,
+ Sequence,
+ TypeVar,
+)
from pw_protobuf_compiler.python_protos import proto_repr
from pw_status import Status
@@ -33,45 +42,61 @@ _LOG = logging.getLogger(__package__)
class UseDefault(enum.Enum):
"""Marker for args that should use a default value, when None is valid."""
+
VALUE = 0
-CallType = TypeVar('CallType', 'UnaryCall', 'ServerStreamingCall',
- 'ClientStreamingCall', 'BidirectionalStreamingCall')
+CallTypeT = TypeVar(
+ 'CallTypeT',
+ 'UnaryCall',
+ 'ServerStreamingCall',
+ 'ClientStreamingCall',
+ 'BidirectionalStreamingCall',
+)
-OnNextCallback = Callable[[CallType, Any], Any]
-OnCompletedCallback = Callable[[CallType, Any], Any]
-OnErrorCallback = Callable[[CallType, Any], Any]
+OnNextCallback = Callable[[CallTypeT, Any], Any]
+OnCompletedCallback = Callable[[CallTypeT, Any], Any]
+OnErrorCallback = Callable[[CallTypeT, Any], Any]
OptionalTimeout = Union[UseDefault, float, None]
class UnaryResponse(NamedTuple):
"""Result from a unary or client streaming RPC: status and response."""
+
status: Status
response: Any
def __repr__(self) -> str:
- return f'({self.status}, {proto_repr(self.response)})'
+ reply = proto_repr(self.response) if self.response else self.response
+ return f'({self.status}, {reply})'
class StreamResponse(NamedTuple):
"""Results from a server or bidirectional streaming RPC."""
+
status: Status
responses: Sequence[Any]
def __repr__(self) -> str:
- return (f'({self.status}, '
- f'[{", ".join(proto_repr(r) for r in self.responses)}])')
+ return (
+ f'({self.status}, '
+ f'[{", ".join(proto_repr(r) for r in self.responses)}])'
+ )
class Call:
"""Represents an in-progress or completed RPC call."""
- def __init__(self, rpcs: PendingRpcs, rpc: PendingRpc,
- default_timeout_s: Optional[float],
- on_next: Optional[OnNextCallback],
- on_completed: Optional[OnCompletedCallback],
- on_error: Optional[OnErrorCallback]) -> None:
+
+ def __init__(
+ self,
+ rpcs: PendingRpcs,
+ rpc: PendingRpc,
+ default_timeout_s: Optional[float],
+ on_next: Optional[OnNextCallback],
+ on_completed: Optional[OnCompletedCallback],
+ on_error: Optional[OnErrorCallback],
+ ) -> None:
self._rpcs = rpcs
self._rpc = rpc
self.default_timeout_s = default_timeout_s
@@ -88,16 +113,23 @@ class Call:
def _invoke(self, request: Optional[Message], ignore_errors: bool) -> None:
"""Calls the RPC. This must be called immediately after __init__."""
- previous = self._rpcs.send_request(self._rpc,
- request,
- self,
- ignore_errors=ignore_errors,
- override_pending=True)
+ previous = self._rpcs.send_request(
+ self._rpc,
+ request,
+ self,
+ ignore_errors=ignore_errors,
+ override_pending=True,
+ )
# TODO(hepler): Remove the cancel_duplicate_calls option.
- if (self._rpcs.cancel_duplicate_calls and # type: ignore[attr-defined]
- previous is not None and not previous.completed()):
- previous._handle_error(Status.CANCELLED) # pylint: disable=protected-access
+ if (
+ self._rpcs.cancel_duplicate_calls # type: ignore[attr-defined]
+ and previous is not None
+ and not previous.completed()
+ ):
+ previous._handle_error( # pylint: disable=protected-access
+ Status.CANCELLED
+ )
def _default_response(self, response: Message) -> None:
_LOG.debug('%s received response: %s', self._rpc, response)
@@ -116,8 +148,9 @@ class Call:
"""True if the RPC call has completed, successfully or from an error."""
return self.status is not None or self.error is not None
- def _send_client_stream(self, request_proto: Optional[Message],
- request_fields: dict) -> None:
+ def _send_client_stream(
+ self, request_proto: Optional[Message], request_fields: dict
+ ) -> None:
"""Sends a client to the server in the client stream.
Sending a client stream packet on a closed RPC raises an exception.
@@ -128,7 +161,8 @@ class Call:
raise RpcError(self._rpc, Status.FAILED_PRECONDITION)
self._rpcs.send_client_stream(
- self._rpc, self.method.get_request(request_proto, request_fields))
+ self._rpc, self.method.get_request(request_proto, request_fields)
+ )
def _finish_client_stream(self, requests: Iterable[Message]) -> None:
for request in requests:
@@ -153,10 +187,9 @@ class Call:
assert self.status is not None
return StreamResponse(self.status, self._responses)
- def _get_responses(self,
- *,
- count: int = None,
- timeout_s: OptionalTimeout) -> Iterator:
+ def _get_responses(
+ self, *, count: Optional[int] = None, timeout_s: OptionalTimeout
+ ) -> Iterator:
"""Returns an iterator of stream responses.
Args:
@@ -233,8 +266,10 @@ class Call:
try:
callback(self, arg)
except Exception as callback_exception: # pylint: disable=broad-except
- msg = (f'The {callback_name} callback ({callback}) for '
- f'{self._rpc} raised an exception')
+ msg = (
+ f'The {callback_name} callback ({callback}) for '
+ f'{self._rpc} raised an exception'
+ )
_LOG.exception(msg)
self._callback_exception = RuntimeError(msg)
@@ -252,30 +287,35 @@ class Call:
class UnaryCall(Call):
"""Tracks the state of a unary RPC call."""
+
@property
def response(self) -> Any:
return self._responses[-1] if self._responses else None
- def wait(self,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> UnaryResponse:
+ def wait(
+ self, timeout_s: OptionalTimeout = UseDefault.VALUE
+ ) -> UnaryResponse:
return self._unary_wait(timeout_s)
class ServerStreamingCall(Call):
"""Tracks the state of a server streaming RPC call."""
+
@property
def responses(self) -> Sequence:
return self._responses
- def wait(self,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> StreamResponse:
+ def wait(
+ self, timeout_s: OptionalTimeout = UseDefault.VALUE
+ ) -> StreamResponse:
return self._stream_wait(timeout_s)
def get_responses(
- self,
- *,
- count: int = None,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> Iterator:
+ self,
+ *,
+ count: Optional[int] = None,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
+ ) -> Iterator:
return self._get_responses(count=count, timeout_s=timeout_s)
def __iter__(self) -> Iterator:
@@ -284,23 +324,25 @@ class ServerStreamingCall(Call):
class ClientStreamingCall(Call):
"""Tracks the state of a client streaming RPC call."""
+
@property
def response(self) -> Any:
return self._responses[-1] if self._responses else None
# TODO(hepler): Use / to mark the first arg as positional-only
# when when Python 3.7 support is no longer required.
- def send(self,
- _rpc_request_proto: Message = None,
- **request_fields) -> None:
+ def send(
+ self, _rpc_request_proto: Optional[Message] = None, **request_fields
+ ) -> None:
"""Sends client stream request to the server."""
self._send_client_stream(_rpc_request_proto, request_fields)
def finish_and_wait(
- self,
- requests: Iterable[Message] = (),
- *,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> UnaryResponse:
+ self,
+ requests: Iterable[Message] = (),
+ *,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
+ ) -> UnaryResponse:
"""Ends the client stream and waits for the RPC to complete."""
self._finish_client_stream(requests)
return self._unary_wait(timeout_s)
@@ -308,32 +350,35 @@ class ClientStreamingCall(Call):
class BidirectionalStreamingCall(Call):
"""Tracks the state of a bidirectional streaming RPC call."""
+
@property
def responses(self) -> Sequence:
return self._responses
# TODO(hepler): Use / to mark the first arg as positional-only
# when when Python 3.7 support is no longer required.
- def send(self,
- _rpc_request_proto: Message = None,
- **request_fields) -> None:
+ def send(
+ self, _rpc_request_proto: Optional[Message] = None, **request_fields
+ ) -> None:
"""Sends a message to the server in the client stream."""
self._send_client_stream(_rpc_request_proto, request_fields)
def finish_and_wait(
- self,
- requests: Iterable[Message] = (),
- *,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> StreamResponse:
+ self,
+ requests: Iterable[Message] = (),
+ *,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
+ ) -> StreamResponse:
"""Ends the client stream and waits for the RPC to complete."""
self._finish_client_stream(requests)
return self._stream_wait(timeout_s)
def get_responses(
- self,
- *,
- count: int = None,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> Iterator:
+ self,
+ *,
+ count: Optional[int] = None,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
+ ) -> Iterator:
return self._get_responses(count=count, timeout_s=timeout_s)
def __iter__(self) -> Iterator:
diff --git a/pw_rpc/py/pw_rpc/callback_client/errors.py b/pw_rpc/py/pw_rpc/callback_client/errors.py
index 0e898e661..96138596c 100644
--- a/pw_rpc/py/pw_rpc/callback_client/errors.py
+++ b/pw_rpc/py/pw_rpc/callback_client/errors.py
@@ -23,7 +23,8 @@ from pw_rpc.client import PendingRpc
class RpcTimeout(Exception):
def __init__(self, rpc: PendingRpc, timeout: Optional[float]):
super().__init__(
- f'No response received for {rpc.method} after {timeout} s')
+ f'No response received for {rpc.method} after {timeout} s'
+ )
self.rpc = rpc
self.timeout = timeout
diff --git a/pw_rpc/py/pw_rpc/callback_client/impl.py b/pw_rpc/py/pw_rpc/callback_client/impl.py
index 59a185e32..474757373 100644
--- a/pw_rpc/py/pw_rpc/callback_client/impl.py
+++ b/pw_rpc/py/pw_rpc/callback_client/impl.py
@@ -28,7 +28,7 @@ from pw_rpc.descriptors import Channel, Method, Service
from pw_rpc.callback_client.call import (
UseDefault,
OptionalTimeout,
- CallType,
+ CallTypeT,
UnaryResponse,
StreamResponse,
Call,
@@ -46,9 +46,15 @@ _LOG = logging.getLogger(__package__)
class _MethodClient:
"""A method that can be invoked for a particular channel."""
- def __init__(self, client_impl: 'Impl', rpcs: PendingRpcs,
- channel: Channel, method: Method,
- default_timeout_s: Optional[float]) -> None:
+
+ def __init__(
+ self,
+ client_impl: 'Impl',
+ rpcs: PendingRpcs,
+ channel: Channel,
+ method: Method,
+ default_timeout_s: Optional[float],
+ ) -> None:
self._impl = client_impl
self._rpcs = rpcs
self._rpc = PendingRpc(channel, method.service, method)
@@ -95,41 +101,47 @@ class _MethodClient:
f'{function_call}'
f'{arg_sep.join(descriptors.field_help(self.method.request_type))})'
f'\n\n{textwrap.indent(docstring, " ")}\n\n'
- f' Returns {annotation}.')
-
- def _start_call(self,
- call_type: Type[CallType],
- request: Optional[Message],
- timeout_s: OptionalTimeout,
- on_next: Optional[OnNextCallback],
- on_completed: Optional[OnCompletedCallback],
- on_error: Optional[OnErrorCallback],
- ignore_errors: bool = False) -> CallType:
+ f' Returns {annotation}.'
+ )
+
+ def _start_call(
+ self,
+ call_type: Type[CallTypeT],
+ request: Optional[Message],
+ timeout_s: OptionalTimeout,
+ on_next: Optional[OnNextCallback],
+ on_completed: Optional[OnCompletedCallback],
+ on_error: Optional[OnErrorCallback],
+ ignore_errors: bool = False,
+ ) -> CallTypeT:
"""Creates the Call object and invokes the RPC using it."""
if timeout_s is UseDefault.VALUE:
timeout_s = self.default_timeout_s
- call = call_type(self._rpcs, self._rpc, timeout_s, on_next,
- on_completed, on_error)
+ call = call_type(
+ self._rpcs, self._rpc, timeout_s, on_next, on_completed, on_error
+ )
call._invoke(request, ignore_errors) # pylint: disable=protected-access
return call
- def _client_streaming_call_type(self,
- base: Type[CallType]) -> Type[CallType]:
+ def _client_streaming_call_type(
+ self, base: Type[CallTypeT]
+ ) -> Type[CallTypeT]:
"""Creates a client or bidirectional stream call type.
Applies the signature from the request protobuf to the send method.
"""
- def send(self,
- _rpc_request_proto: Message = None,
- **request_fields) -> None:
- ClientStreamingCall.send(self, _rpc_request_proto,
- **request_fields)
+
+ def send(
+ self, _rpc_request_proto: Optional[Message] = None, **request_fields
+ ) -> None:
+ ClientStreamingCall.send(self, _rpc_request_proto, **request_fields)
_apply_protobuf_signature(self.method, send)
- return type(f'{self.method.name}_{base.__name__}', (base, ),
- dict(send=send))
+ return type(
+ f'{self.method.name}_{base.__name__}', (base,), dict(send=send)
+ )
def _function_docstring(method: Method) -> str:
@@ -161,139 +173,198 @@ def _apply_protobuf_signature(method: Method, function: Callable) -> None:
params = [next(iter(sig.parameters.values()))] # Get the "self" parameter
params += method.request_parameters()
params.append(
- inspect.Parameter('pw_rpc_timeout_s', inspect.Parameter.KEYWORD_ONLY))
+ inspect.Parameter('pw_rpc_timeout_s', inspect.Parameter.KEYWORD_ONLY)
+ )
function.__signature__ = sig.replace( # type: ignore[attr-defined]
- parameters=params)
+ parameters=params
+ )
class _UnaryMethodClient(_MethodClient):
- def invoke(self,
- request: Message = None,
- on_next: OnNextCallback = None,
- on_completed: OnCompletedCallback = None,
- on_error: OnErrorCallback = None,
- *,
- request_args: Dict[str, Any] = None,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> UnaryCall:
+ def invoke(
+ self,
+ request: Optional[Message] = None,
+ on_next: Optional[OnNextCallback] = None,
+ on_completed: Optional[OnCompletedCallback] = None,
+ on_error: Optional[OnErrorCallback] = None,
+ *,
+ request_args: Optional[Dict[str, Any]] = None,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
+ ) -> UnaryCall:
"""Invokes the unary RPC and returns a call object."""
- return self._start_call(UnaryCall,
- self.method.get_request(request, request_args),
- timeout_s, on_next, on_completed, on_error)
-
- def open(self,
- request: Message = None,
- on_next: OnNextCallback = None,
- on_completed: OnCompletedCallback = None,
- on_error: OnErrorCallback = None,
- *,
- request_args: Dict[str, Any] = None) -> UnaryCall:
+ return self._start_call(
+ UnaryCall,
+ self.method.get_request(request, request_args),
+ timeout_s,
+ on_next,
+ on_completed,
+ on_error,
+ )
+
+ def open(
+ self,
+ request: Optional[Message] = None,
+ on_next: Optional[OnNextCallback] = None,
+ on_completed: Optional[OnCompletedCallback] = None,
+ on_error: Optional[OnErrorCallback] = None,
+ *,
+ request_args: Optional[Dict[str, Any]] = None,
+ ) -> UnaryCall:
"""Invokes the unary RPC and returns a call object."""
- return self._start_call(UnaryCall,
- self.method.get_request(request, request_args),
- None, on_next, on_completed, on_error, True)
+ return self._start_call(
+ UnaryCall,
+ self.method.get_request(request, request_args),
+ None,
+ on_next,
+ on_completed,
+ on_error,
+ True,
+ )
class _ServerStreamingMethodClient(_MethodClient):
def invoke(
- self,
- request: Message = None,
- on_next: OnNextCallback = None,
- on_completed: OnCompletedCallback = None,
- on_error: OnErrorCallback = None,
- *,
- request_args: Dict[str, Any] = None,
- timeout_s: OptionalTimeout = UseDefault.VALUE
+ self,
+ request: Optional[Message] = None,
+ on_next: Optional[OnNextCallback] = None,
+ on_completed: Optional[OnCompletedCallback] = None,
+ on_error: Optional[OnErrorCallback] = None,
+ *,
+ request_args: Optional[Dict[str, Any]] = None,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
) -> ServerStreamingCall:
"""Invokes the server streaming RPC and returns a call object."""
- return self._start_call(ServerStreamingCall,
- self.method.get_request(request, request_args),
- timeout_s, on_next, on_completed, on_error)
-
- def open(self,
- request: Message = None,
- on_next: OnNextCallback = None,
- on_completed: OnCompletedCallback = None,
- on_error: OnErrorCallback = None,
- *,
- request_args: Dict[str, Any] = None) -> ServerStreamingCall:
+ return self._start_call(
+ ServerStreamingCall,
+ self.method.get_request(request, request_args),
+ timeout_s,
+ on_next,
+ on_completed,
+ on_error,
+ )
+
+ def open(
+ self,
+ request: Optional[Message] = None,
+ on_next: Optional[OnNextCallback] = None,
+ on_completed: Optional[OnCompletedCallback] = None,
+ on_error: Optional[OnErrorCallback] = None,
+ *,
+ request_args: Optional[Dict[str, Any]] = None,
+ ) -> ServerStreamingCall:
"""Returns a call object for the RPC, even if the RPC cannot be invoked.
Can be used to listen for responses from an RPC server that may yet be
available.
"""
- return self._start_call(ServerStreamingCall,
- self.method.get_request(request, request_args),
- None, on_next, on_completed, on_error, True)
+ return self._start_call(
+ ServerStreamingCall,
+ self.method.get_request(request, request_args),
+ None,
+ on_next,
+ on_completed,
+ on_error,
+ True,
+ )
class _ClientStreamingMethodClient(_MethodClient):
def invoke(
- self,
- on_next: OnNextCallback = None,
- on_completed: OnCompletedCallback = None,
- on_error: OnErrorCallback = None,
- *,
- timeout_s: OptionalTimeout = UseDefault.VALUE
+ self,
+ on_next: Optional[OnNextCallback] = None,
+ on_completed: Optional[OnCompletedCallback] = None,
+ on_error: Optional[OnErrorCallback] = None,
+ *,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
) -> ClientStreamingCall:
"""Invokes the client streaming RPC and returns a call object"""
return self._start_call(
- self._client_streaming_call_type(ClientStreamingCall), None,
- timeout_s, on_next, on_completed, on_error, True)
-
- def open(self,
- on_next: OnNextCallback = None,
- on_completed: OnCompletedCallback = None,
- on_error: OnErrorCallback = None) -> ClientStreamingCall:
+ self._client_streaming_call_type(ClientStreamingCall),
+ None,
+ timeout_s,
+ on_next,
+ on_completed,
+ on_error,
+ True,
+ )
+
+ def open(
+ self,
+ on_next: Optional[OnNextCallback] = None,
+ on_completed: Optional[OnCompletedCallback] = None,
+ on_error: Optional[OnErrorCallback] = None,
+ ) -> ClientStreamingCall:
"""Returns a call object for the RPC, even if the RPC cannot be invoked.
Can be used to listen for responses from an RPC server that may yet be
available.
"""
return self._start_call(
- self._client_streaming_call_type(ClientStreamingCall), None, None,
- on_next, on_completed, on_error, True)
+ self._client_streaming_call_type(ClientStreamingCall),
+ None,
+ None,
+ on_next,
+ on_completed,
+ on_error,
+ True,
+ )
def __call__(
- self,
- requests: Iterable[Message] = (),
- *,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> UnaryResponse:
+ self,
+ requests: Iterable[Message] = (),
+ *,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
+ ) -> UnaryResponse:
return self.invoke().finish_and_wait(requests, timeout_s=timeout_s)
class _BidirectionalStreamingMethodClient(_MethodClient):
def invoke(
self,
- on_next: OnNextCallback = None,
- on_completed: OnCompletedCallback = None,
- on_error: OnErrorCallback = None,
+ on_next: Optional[OnNextCallback] = None,
+ on_completed: Optional[OnCompletedCallback] = None,
+ on_error: Optional[OnErrorCallback] = None,
*,
- timeout_s: OptionalTimeout = UseDefault.VALUE
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
) -> BidirectionalStreamingCall:
"""Invokes the bidirectional streaming RPC and returns a call object."""
return self._start_call(
- self._client_streaming_call_type(BidirectionalStreamingCall), None,
- timeout_s, on_next, on_completed, on_error)
-
- def open(self,
- on_next: OnNextCallback = None,
- on_completed: OnCompletedCallback = None,
- on_error: OnErrorCallback = None) -> BidirectionalStreamingCall:
+ self._client_streaming_call_type(BidirectionalStreamingCall),
+ None,
+ timeout_s,
+ on_next,
+ on_completed,
+ on_error,
+ )
+
+ def open(
+ self,
+ on_next: Optional[OnNextCallback] = None,
+ on_completed: Optional[OnCompletedCallback] = None,
+ on_error: Optional[OnErrorCallback] = None,
+ ) -> BidirectionalStreamingCall:
"""Returns a call object for the RPC, even if the RPC cannot be invoked.
Can be used to listen for responses from an RPC server that may yet be
available.
"""
return self._start_call(
- self._client_streaming_call_type(BidirectionalStreamingCall), None,
- None, on_next, on_completed, on_error, True)
+ self._client_streaming_call_type(BidirectionalStreamingCall),
+ None,
+ None,
+ on_next,
+ on_completed,
+ on_error,
+ True,
+ )
def __call__(
- self,
- requests: Iterable[Message] = (),
- *,
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> StreamResponse:
+ self,
+ requests: Iterable[Message] = (),
+ *,
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
+ ) -> StreamResponse:
return self.invoke().finish_and_wait(requests, timeout_s=timeout_s)
@@ -308,10 +379,13 @@ asynchronously using the invoke method.
class Impl(client.ClientImpl):
"""Callback-based ClientImpl, for use with pw_rpc.Client."""
- def __init__(self,
- default_unary_timeout_s: float = None,
- default_stream_timeout_s: float = None,
- cancel_duplicate_calls: bool = True) -> None:
+
+ def __init__(
+ self,
+ default_unary_timeout_s: Optional[float] = None,
+ default_stream_timeout_s: Optional[float] = None,
+ cancel_duplicate_calls: Optional[bool] = True,
+ ) -> None:
super().__init__()
self._default_unary_timeout_s = default_unary_timeout_s
self._default_stream_timeout_s = default_stream_timeout_s
@@ -335,113 +409,150 @@ class Impl(client.ClientImpl):
# Temporarily attach the cancel_duplicate_calls option to the
# PendingRpcs object.
# TODO(hepler): Remove this workaround.
+ assert self.rpcs
self.rpcs.cancel_duplicate_calls = ( # type: ignore[attr-defined]
- self._cancel_duplicate_calls)
+ self._cancel_duplicate_calls
+ )
if method.type is Method.Type.UNARY:
return self._create_unary_method_client(
- channel, method, self.default_unary_timeout_s)
+ channel, method, self.default_unary_timeout_s
+ )
if method.type is Method.Type.SERVER_STREAMING:
return self._create_server_streaming_method_client(
- channel, method, self.default_stream_timeout_s)
+ channel, method, self.default_stream_timeout_s
+ )
if method.type is Method.Type.CLIENT_STREAMING:
- return self._create_method_client(_ClientStreamingMethodClient,
- channel, method,
- self.default_unary_timeout_s)
+ return self._create_method_client(
+ _ClientStreamingMethodClient,
+ channel,
+ method,
+ self.default_unary_timeout_s,
+ )
if method.type is Method.Type.BIDIRECTIONAL_STREAMING:
return self._create_method_client(
- _BidirectionalStreamingMethodClient, channel, method,
- self.default_stream_timeout_s)
+ _BidirectionalStreamingMethodClient,
+ channel,
+ method,
+ self.default_stream_timeout_s,
+ )
raise AssertionError(f'Unknown method type {method.type}')
- def _create_method_client(self, base: type, channel: Channel,
- method: Method,
- default_timeout_s: Optional[float], **fields):
+ def _create_method_client(
+ self,
+ base: type,
+ channel: Channel,
+ method: Method,
+ default_timeout_s: Optional[float],
+ **fields,
+ ):
"""Creates a _MethodClient derived class customized for the method."""
method_client_type = type(
- f'{method.name}{base.__name__}', (base, ),
- dict(__doc__=_method_client_docstring(method), **fields))
- return method_client_type(self, self.rpcs, channel, method,
- default_timeout_s)
+ f'{method.name}{base.__name__}',
+ (base,),
+ dict(__doc__=_method_client_docstring(method), **fields),
+ )
+ return method_client_type(
+ self, self.rpcs, channel, method, default_timeout_s
+ )
def _create_unary_method_client(
- self, channel: Channel, method: Method,
- default_timeout_s: Optional[float]) -> _UnaryMethodClient:
+ self,
+ channel: Channel,
+ method: Method,
+ default_timeout_s: Optional[float],
+ ) -> _UnaryMethodClient:
"""Creates a _UnaryMethodClient with a customized __call__ method."""
# TODO(hepler): Use / to mark the first arg as positional-only
# when when Python 3.7 support is no longer required.
- def call(self: _UnaryMethodClient,
- _rpc_request_proto: Message = None,
- *,
- pw_rpc_timeout_s: OptionalTimeout = UseDefault.VALUE,
- **request_fields) -> UnaryResponse:
+ def call(
+ self: _UnaryMethodClient,
+ _rpc_request_proto: Optional[Message] = None,
+ *,
+ pw_rpc_timeout_s: OptionalTimeout = UseDefault.VALUE,
+ **request_fields,
+ ) -> UnaryResponse:
return self.invoke(
- self.method.get_request(_rpc_request_proto,
- request_fields)).wait(pw_rpc_timeout_s)
+ self.method.get_request(_rpc_request_proto, request_fields)
+ ).wait(pw_rpc_timeout_s)
_update_call_method(method, call)
- return self._create_method_client(_UnaryMethodClient,
- channel,
- method,
- default_timeout_s,
- __call__=call)
+ return self._create_method_client(
+ _UnaryMethodClient,
+ channel,
+ method,
+ default_timeout_s,
+ __call__=call,
+ )
def _create_server_streaming_method_client(
- self, channel: Channel, method: Method,
- default_timeout_s: Optional[float]
+ self,
+ channel: Channel,
+ method: Method,
+ default_timeout_s: Optional[float],
) -> _ServerStreamingMethodClient:
"""Creates _ServerStreamingMethodClient with custom __call__ method."""
# TODO(hepler): Use / to mark the first arg as positional-only
# when when Python 3.7 support is no longer required.
- def call(self: _ServerStreamingMethodClient,
- _rpc_request_proto: Message = None,
- *,
- pw_rpc_timeout_s: OptionalTimeout = UseDefault.VALUE,
- **request_fields) -> StreamResponse:
+ def call(
+ self: _ServerStreamingMethodClient,
+ _rpc_request_proto: Optional[Message] = None,
+ *,
+ pw_rpc_timeout_s: OptionalTimeout = UseDefault.VALUE,
+ **request_fields,
+ ) -> StreamResponse:
return self.invoke(
- self.method.get_request(_rpc_request_proto,
- request_fields)).wait(pw_rpc_timeout_s)
+ self.method.get_request(_rpc_request_proto, request_fields)
+ ).wait(pw_rpc_timeout_s)
_update_call_method(method, call)
- return self._create_method_client(_ServerStreamingMethodClient,
- channel,
- method,
- default_timeout_s,
- __call__=call)
-
- def handle_response(self,
- rpc: PendingRpc,
- context: Call,
- payload,
- *,
- args: tuple = (),
- kwargs: dict = None) -> None:
+ return self._create_method_client(
+ _ServerStreamingMethodClient,
+ channel,
+ method,
+ default_timeout_s,
+ __call__=call,
+ )
+
+ def handle_response(
+ self,
+ rpc: PendingRpc,
+ context: Call,
+ payload,
+ *,
+ args: tuple = (),
+ kwargs: Optional[dict] = None,
+ ) -> None:
"""Invokes the callback associated with this RPC."""
assert not args and not kwargs, 'Forwarding args & kwargs not supported'
context._handle_response(payload) # pylint: disable=protected-access
- def handle_completion(self,
- rpc: PendingRpc,
- context: Call,
- status: Status,
- *,
- args: tuple = (),
- kwargs: dict = None):
+ def handle_completion(
+ self,
+ rpc: PendingRpc,
+ context: Call,
+ status: Status,
+ *,
+ args: tuple = (),
+ kwargs: Optional[dict] = None,
+ ):
assert not args and not kwargs, 'Forwarding args & kwargs not supported'
context._handle_completion(status) # pylint: disable=protected-access
- def handle_error(self,
- rpc: PendingRpc,
- context: Call,
- status: Status,
- *,
- args: tuple = (),
- kwargs: dict = None) -> None:
+ def handle_error(
+ self,
+ rpc: PendingRpc,
+ context: Call,
+ status: Status,
+ *,
+ args: tuple = (),
+ kwargs: Optional[dict] = None,
+ ) -> None:
assert not args and not kwargs, 'Forwarding args & kwargs not supported'
context._handle_error(status) # pylint: disable=protected-access
diff --git a/pw_rpc/py/pw_rpc/client.py b/pw_rpc/py/pw_rpc/client.py
index 182434d6b..d8c3390d7 100644
--- a/pw_rpc/py/pw_rpc/client.py
+++ b/pw_rpc/py/pw_rpc/client.py
@@ -16,8 +16,16 @@
import abc
from dataclasses import dataclass
import logging
-from typing import (Any, Callable, Collection, Dict, Iterable, Iterator,
- NamedTuple, Optional)
+from typing import (
+ Any,
+ Callable,
+ Collection,
+ Dict,
+ Iterable,
+ Iterator,
+ NamedTuple,
+ Optional,
+)
from google.protobuf.message import DecodeError, Message
from pw_status import Status
@@ -35,6 +43,7 @@ class Error(Exception):
class PendingRpc(NamedTuple):
"""Uniquely identifies an RPC call."""
+
channel: Channel
service: Service
method: Method
@@ -50,26 +59,31 @@ class _PendingRpcMetadata:
class PendingRpcs:
"""Tracks pending RPCs and encodes outgoing RPC packets."""
+
def __init__(self):
self._pending: Dict[PendingRpc, _PendingRpcMetadata] = {}
- def request(self,
- rpc: PendingRpc,
- request: Optional[Message],
- context: object,
- override_pending: bool = True) -> bytes:
+ def request(
+ self,
+ rpc: PendingRpc,
+ request: Optional[Message],
+ context: object,
+ override_pending: bool = True,
+ ) -> bytes:
"""Starts the provided RPC and returns the encoded packet to send."""
# Ensure that every context is a unique object by wrapping it in a list.
self.open(rpc, context, override_pending)
return packets.encode_request(rpc, request)
- def send_request(self,
- rpc: PendingRpc,
- request: Optional[Message],
- context: object,
- *,
- ignore_errors: bool = False,
- override_pending: bool = False) -> Any:
+ def send_request(
+ self,
+ rpc: PendingRpc,
+ request: Optional[Message],
+ context: object,
+ *,
+ ignore_errors: bool = False,
+ override_pending: bool = False,
+ ) -> Any:
"""Starts the provided RPC and sends the request packet to the channel.
Returns:
@@ -90,10 +104,9 @@ class PendingRpcs:
return previous
- def open(self,
- rpc: PendingRpc,
- context: object,
- override_pending: bool = False) -> Any:
+ def open(
+ self, rpc: PendingRpc, context: object, override_pending: bool = False
+ ) -> Any:
"""Creates a context for an RPC, but does not invoke it.
open() can be used to receive streaming responses to an RPC that was not
@@ -113,32 +126,36 @@ class PendingRpcs:
if self._pending.setdefault(rpc, metadata) is not metadata:
# If the context was not added, the RPC was already pending.
- raise Error(f'Sent request for {rpc}, but it is already pending! '
- 'Cancel the RPC before invoking it again')
+ raise Error(
+ f'Sent request for {rpc}, but it is already pending! '
+ 'Cancel the RPC before invoking it again'
+ )
return None
def send_client_stream(self, rpc: PendingRpc, message: Message) -> None:
if rpc not in self._pending:
- raise Error(
- f'Attempt to send client stream for inactive RPC {rpc}')
+ raise Error(f'Attempt to send client stream for inactive RPC {rpc}')
rpc.channel.output( # type: ignore
- packets.encode_client_stream(rpc, message))
+ packets.encode_client_stream(rpc, message)
+ )
def send_client_stream_end(self, rpc: PendingRpc) -> None:
if rpc not in self._pending:
raise Error(
- f'Attempt to send client stream end for inactive RPC {rpc}')
+ f'Attempt to send client stream end for inactive RPC {rpc}'
+ )
rpc.channel.output( # type: ignore
- packets.encode_client_stream_end(rpc))
+ packets.encode_client_stream_end(rpc)
+ )
- def cancel(self, rpc: PendingRpc) -> Optional[bytes]:
- """Cancels the RPC. Returns the CANCEL packet to send.
+ def cancel(self, rpc: PendingRpc) -> bytes:
+ """Cancels the RPC.
Returns:
- True if the RPC was cancelled; False if it was not pending
+ The CLIENT_ERROR packet to send.
Raises:
KeyError if the RPC is not pending
@@ -146,9 +163,6 @@ class PendingRpcs:
_LOG.debug('Cancelling %s', rpc)
del self._pending[rpc]
- if rpc.method.type is Method.Type.UNARY:
- return None
-
return packets.encode_cancel(rpc)
def send_cancel(self, rpc: PendingRpc) -> bool:
@@ -178,22 +192,25 @@ class ClientImpl(abc.ABC):
This interface defines the semantics for invoking an RPC on a particular
client.
"""
+
def __init__(self):
- self.client: 'Client' = None
- self.rpcs: PendingRpcs = None
+ self.client: Optional['Client'] = None
+ self.rpcs: Optional[PendingRpcs] = None
@abc.abstractmethod
def method_client(self, channel: Channel, method: Method) -> Any:
"""Returns an object that invokes a method using the given channel."""
@abc.abstractmethod
- def handle_response(self,
- rpc: PendingRpc,
- context: Any,
- payload: Any,
- *,
- args: tuple = (),
- kwargs: dict = None) -> Any:
+ def handle_response(
+ self,
+ rpc: PendingRpc,
+ context: Any,
+ payload: Any,
+ *,
+ args: tuple = (),
+ kwargs: Optional[dict] = None,
+ ) -> Any:
"""Handles a response from the RPC server.
Args:
@@ -204,13 +221,15 @@ class ClientImpl(abc.ABC):
"""
@abc.abstractmethod
- def handle_completion(self,
- rpc: PendingRpc,
- context: Any,
- status: Status,
- *,
- args: tuple = (),
- kwargs: dict = None) -> Any:
+ def handle_completion(
+ self,
+ rpc: PendingRpc,
+ context: Any,
+ status: Status,
+ *,
+ args: tuple = (),
+ kwargs: Optional[dict] = None,
+ ) -> Any:
"""Handles the successful completion of an RPC.
Args:
@@ -221,13 +240,15 @@ class ClientImpl(abc.ABC):
"""
@abc.abstractmethod
- def handle_error(self,
- rpc: PendingRpc,
- context,
- status: Status,
- *,
- args: tuple = (),
- kwargs: dict = None):
+ def handle_error(
+ self,
+ rpc: PendingRpc,
+ context,
+ status: Status,
+ *,
+ args: tuple = (),
+ kwargs: Optional[dict] = None,
+ ):
"""Handles the abnormal termination of an RPC.
args:
@@ -240,22 +261,27 @@ class ClientImpl(abc.ABC):
class ServiceClient(descriptors.ServiceAccessor):
"""Navigates the methods in a service provided by a ChannelClient."""
- def __init__(self, client_impl: ClientImpl, channel: Channel,
- service: Service):
+
+ def __init__(
+ self, client_impl: ClientImpl, channel: Channel, service: Service
+ ):
super().__init__(
{
method: client_impl.method_client(channel, method)
for method in service.methods
},
- as_attrs='members')
+ as_attrs='members',
+ )
self._channel = channel
self._service = service
def __repr__(self) -> str:
- return (f'Service({self._service.full_name!r}, '
- f'methods={[m.name for m in self._service.methods]}, '
- f'channel={self._channel.id})')
+ return (
+ f'Service({self._service.full_name!r}, '
+ f'methods={[m.name for m in self._service.methods]}, '
+ f'channel={self._channel.id})'
+ )
def __str__(self) -> str:
return str(self._service)
@@ -263,19 +289,23 @@ class ServiceClient(descriptors.ServiceAccessor):
class Services(descriptors.ServiceAccessor[ServiceClient]):
"""Navigates the services provided by a ChannelClient."""
- def __init__(self, client_impl, channel: Channel,
- services: Collection[Service]):
+
+ def __init__(
+ self, client_impl, channel: Channel, services: Collection[Service]
+ ):
super().__init__(
- {s: ServiceClient(client_impl, channel, s)
- for s in services},
- as_attrs='packages')
+ {s: ServiceClient(client_impl, channel, s) for s in services},
+ as_attrs='packages',
+ )
self._channel = channel
self._services = services
def __repr__(self) -> str:
- return (f'Services(channel={self._channel.id}, '
- f'services={[s.full_name for s in self._services]})')
+ return (
+ f'Services(channel={self._channel.id}, '
+ f'services={[s.full_name for s in self._services]})'
+ )
def _decode_status(rpc: PendingRpc, packet) -> Optional[Status]:
@@ -327,6 +357,7 @@ class ChannelClient:
synchronous RPC client might return a callable object, so an RPC could be
invoked directly (e.g. rpc(field1=123, field2=b'456')).
"""
+
client: 'Client'
channel: Channel
rpcs: Services
@@ -352,23 +383,20 @@ class ChannelClient:
yield from service_client
def __repr__(self) -> str:
- return (f'ChannelClient(channel={self.channel.id}, '
- f'services={[str(s) for s in self.services()]})')
+ return (
+ f'ChannelClient(channel={self.channel.id}, '
+ f'services={[str(s) for s in self.services()]})'
+ )
-def _update_for_backwards_compatibility(rpc: PendingRpc,
- packet: RpcPacket) -> None:
+def _update_for_backwards_compatibility(
+ rpc: PendingRpc, packet: RpcPacket
+) -> None:
"""Adapts server streaming RPC packets to the updated protocol if needed."""
# The protocol changes only affect server streaming RPCs.
if rpc.method.type is not Method.Type.SERVER_STREAMING:
return
- # SERVER_STREAM_END packets are deprecated. They are equivalent to a
- # RESPONSE packet.
- if packet.type == PacketType.DEPRECATED_SERVER_STREAM_END:
- packet.type = PacketType.RESPONSE
- return
-
# Prior to the introduction of SERVER_STREAM packets, RESPONSE packets with
# a payload were used instead. If a non-zero payload is present, assume this
# RESPONSE is equivalent to a SERVER_STREAM packet.
@@ -389,16 +417,27 @@ class Client:
Users may set an optional response_callback that is called before processing
every response or server stream RPC packet.
"""
+
@classmethod
- def from_modules(cls, impl: ClientImpl, channels: Iterable[Channel],
- modules: Iterable):
+ def from_modules(
+ cls, impl: ClientImpl, channels: Iterable[Channel], modules: Iterable
+ ):
return cls(
- impl, channels,
- (Service.from_descriptor(service) for module in modules
- for service in module.DESCRIPTOR.services_by_name.values()))
-
- def __init__(self, impl: ClientImpl, channels: Iterable[Channel],
- services: Iterable[Service]):
+ impl,
+ channels,
+ (
+ Service.from_descriptor(service)
+ for module in modules
+ for service in module.DESCRIPTOR.services_by_name.values()
+ ),
+ )
+
+ def __init__(
+ self,
+ impl: ClientImpl,
+ channels: Iterable[Channel],
+ services: Iterable[Service],
+ ):
self._impl = impl
self._impl.client = self
self._impl.rpcs = PendingRpcs()
@@ -406,17 +445,18 @@ class Client:
self.services = descriptors.Services(services)
self._channels_by_id = {
- channel.id:
- ChannelClient(self, channel,
- Services(self._impl, channel, self.services))
+ channel.id: ChannelClient(
+ self, channel, Services(self._impl, channel, self.services)
+ )
for channel in channels
}
# Optional function called before processing every non-error RPC packet.
- self.response_callback: Optional[Callable[
- [PendingRpc, Any, Optional[Status]], Any]] = None
+ self.response_callback: Optional[
+ Callable[[PendingRpc, Any, Optional[Status]], Any]
+ ] = None
- def channel(self, channel_id: int = None) -> ChannelClient:
+ def channel(self, channel_id: Optional[int] = None) -> ChannelClient:
"""Returns a ChannelClient, which is used to call RPCs on a channel.
If no channel is provided, the first channel is used.
@@ -447,8 +487,9 @@ class Client:
for service in self.services:
yield from service.methods
- def process_packet(self, pw_rpc_raw_packet_data: bytes, *impl_args,
- **impl_kwargs) -> Status:
+ def process_packet(
+ self, pw_rpc_raw_packet_data: bytes, *impl_args, **impl_kwargs
+ ) -> Status:
"""Processes an incoming packet.
Args:
@@ -487,8 +528,11 @@ class Client:
_update_for_backwards_compatibility(rpc, packet)
- if packet.type not in (PacketType.RESPONSE, PacketType.SERVER_STREAM,
- PacketType.SERVER_ERROR):
+ if packet.type not in (
+ PacketType.RESPONSE,
+ PacketType.SERVER_STREAM,
+ PacketType.SERVER_ERROR,
+ ):
_LOG.error('%s: unexpected PacketType %s', rpc, packet.type)
_LOG.debug('Packet:\n%s', packet)
return Status.OK
@@ -499,9 +543,12 @@ class Client:
payload = _decode_payload(rpc, packet)
except DecodeError as err:
_send_client_error(channel_client, packet, Status.DATA_LOSS)
- _LOG.warning('Failed to decode %s response for %s: %s',
- rpc.method.response_type.DESCRIPTOR.full_name,
- rpc.method.full_name, err)
+ _LOG.warning(
+ 'Failed to decode %s response for %s: %s',
+ rpc.method.response_type.DESCRIPTOR.full_name,
+ rpc.method.full_name,
+ err,
+ )
_LOG.debug('Raw payload: %s', packet.payload)
# Make this an error packet so the error handler is called.
@@ -510,65 +557,74 @@ class Client:
# If set, call the response callback with non-error packets.
if self.response_callback and packet.type != PacketType.SERVER_ERROR:
- self.response_callback(rpc, payload, status) # pylint: disable=not-callable
+ self.response_callback( # pylint: disable=not-callable
+ rpc, payload, status
+ )
try:
+ assert self._impl.rpcs
context = self._impl.rpcs.get_pending(rpc, status)
except KeyError:
- _send_client_error(channel_client, packet,
- Status.FAILED_PRECONDITION)
+ _send_client_error(
+ channel_client, packet, Status.FAILED_PRECONDITION
+ )
_LOG.debug('Discarding response for %s, which is not pending', rpc)
return Status.OK
if packet.type == PacketType.SERVER_ERROR:
assert status is not None and not status.ok()
_LOG.warning('%s: invocation failed with %s', rpc, status)
- self._impl.handle_error(rpc,
- context,
- status,
- args=impl_args,
- kwargs=impl_kwargs)
+ self._impl.handle_error(
+ rpc, context, status, args=impl_args, kwargs=impl_kwargs
+ )
return Status.OK
if payload is not None:
- self._impl.handle_response(rpc,
- context,
- payload,
- args=impl_args,
- kwargs=impl_kwargs)
+ self._impl.handle_response(
+ rpc, context, payload, args=impl_args, kwargs=impl_kwargs
+ )
if status is not None:
- self._impl.handle_completion(rpc,
- context,
- status,
- args=impl_args,
- kwargs=impl_kwargs)
+ self._impl.handle_completion(
+ rpc, context, status, args=impl_args, kwargs=impl_kwargs
+ )
return Status.OK
def _look_up_service_and_method(
- self, packet: RpcPacket,
- channel_client: ChannelClient) -> PendingRpc:
+ self, packet: RpcPacket, channel_client: ChannelClient
+ ) -> PendingRpc:
+ # Protobuf is sometimes silly so the 32 bit python bindings return
+ # signed values from `fixed32` fields. Let's convert back to unsigned.
+ # b/239712573
+ service_id = packet.service_id & 0xFFFFFFFF
try:
- service = self.services[packet.service_id]
+ service = self.services[service_id]
except KeyError:
- raise ValueError(f'Unrecognized service ID {packet.service_id}')
+ raise ValueError(f'Unrecognized service ID {service_id}')
+ # See above, also for b/239712573
+ method_id = packet.method_id & 0xFFFFFFFF
try:
- method = service.methods[packet.method_id]
+ method = service.methods[method_id]
except KeyError:
raise ValueError(
- f'No method ID {packet.method_id} in service {service.name}')
+ f'No method ID {method_id} in service {service.name}'
+ )
return PendingRpc(channel_client.channel, service, method)
def __repr__(self) -> str:
- return (f'pw_rpc.Client(channels={list(self._channels_by_id)}, '
- f'services={[s.full_name for s in self.services]})')
+ return (
+ f'pw_rpc.Client(channels={list(self._channels_by_id)}, '
+ f'services={[s.full_name for s in self.services]})'
+ )
-def _send_client_error(client: ChannelClient, packet: RpcPacket,
- error: Status) -> None:
+def _send_client_error(
+ client: ChannelClient, packet: RpcPacket, error: Status
+) -> None:
# Never send responses to SERVER_ERRORs.
if packet.type != PacketType.SERVER_ERROR:
client.channel.output( # type: ignore
- packets.encode_client_error(packet, error))
+ packets.encode_client_error(packet, error)
+ )
diff --git a/pw_rpc/py/pw_rpc/codegen.py b/pw_rpc/py/pw_rpc/codegen.py
index 44b10afe8..dba811bca 100644
--- a/pw_rpc/py/pw_rpc/codegen.py
+++ b/pw_rpc/py/pw_rpc/codegen.py
@@ -27,19 +27,26 @@ PLUGIN_VERSION = '0.3.0'
RPC_NAMESPACE = '::pw::rpc'
+# todo-check: disable
STUB_REQUEST_TODO = (
- '// TODO: Read the request as appropriate for your application')
+ '// TODO: Read the request as appropriate for your application'
+)
STUB_RESPONSE_TODO = (
- '// TODO: Fill in the response as appropriate for your application')
+ '// TODO: Fill in the response as appropriate for your application'
+)
STUB_WRITER_TODO = (
'// TODO: Send responses with the writer as appropriate for your '
- 'application')
+ 'application'
+)
STUB_READER_TODO = (
'// TODO: Set the client stream callback and send a response as '
- 'appropriate for your application')
+ 'appropriate for your application'
+)
STUB_READER_WRITER_TODO = (
'// TODO: Set the client stream callback and send responses as '
- 'appropriate for your application')
+ 'appropriate for your application'
+)
+# todo-check: enable
def get_id(item: Union[ProtoService, ProtoServiceMethod]) -> str:
@@ -65,6 +72,7 @@ def client_call_type(method: ProtoServiceMethod, prefix: str) -> str:
class CodeGenerator(abc.ABC):
"""Generates RPC code for services and clients."""
+
def __init__(self, output_filename: str) -> None:
self.output = OutputFile(output_filename)
@@ -122,13 +130,16 @@ class CodeGenerator(abc.ABC):
"""Additions to the private section of the outer generated class."""
-def generate_package(file_descriptor_proto, proto_package: ProtoNode,
- gen: CodeGenerator) -> None:
+def generate_package(
+ file_descriptor_proto, proto_package: ProtoNode, gen: CodeGenerator
+) -> None:
"""Generates service and client code for a package."""
assert proto_package.type() == ProtoNode.Type.PACKAGE
- gen.line(f'// {os.path.basename(gen.output.name())} automatically '
- f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}')
+ gen.line(
+ f'// {os.path.basename(gen.output.name())} automatically '
+ f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}'
+ )
gen.line(f'// on {datetime.now().isoformat()}')
gen.line('// clang-format off')
gen.line('#pragma once\n')
@@ -143,6 +154,7 @@ def generate_package(file_descriptor_proto, proto_package: ProtoNode,
'#include "pw_rpc/internal/service_client.h"',
'#include "pw_rpc/method_type.h"',
'#include "pw_rpc/service.h"',
+ '#include "pw_rpc/service_id.h"',
]
include_lines += gen.includes(file_descriptor_proto.name)
@@ -151,8 +163,8 @@ def generate_package(file_descriptor_proto, proto_package: ProtoNode,
gen.line()
- if proto_package.cpp_namespace():
- file_namespace = proto_package.cpp_namespace()
+ if proto_package.cpp_namespace(codegen_subnamespace=None):
+ file_namespace = proto_package.cpp_namespace(codegen_subnamespace=None)
if file_namespace.startswith('::'):
file_namespace = file_namespace[2:]
@@ -164,7 +176,8 @@ def generate_package(file_descriptor_proto, proto_package: ProtoNode,
gen.line()
services = [
- cast(ProtoService, node) for node in proto_package
+ cast(ProtoService, node)
+ for node in proto_package
if node.type() == ProtoNode.Type.SERVICE
]
@@ -178,16 +191,21 @@ def generate_package(file_descriptor_proto, proto_package: ProtoNode,
gen.line('} // namespace ' + file_namespace)
gen.line()
- gen.line('// Specialize MethodInfo for each RPC to provide metadata at '
- 'compile time.')
+ gen.line(
+ '// Specialize MethodInfo for each RPC to provide metadata at '
+ 'compile time.'
+ )
for service in services:
_generate_info(gen, file_namespace, service)
-def _generate_service_and_client(gen: CodeGenerator,
- service: ProtoService) -> None:
- gen.line('// Wrapper class that namespaces server and client code for '
- 'this RPC service.')
+def _generate_service_and_client(
+ gen: CodeGenerator, service: ProtoService
+) -> None:
+ gen.line(
+ '// Wrapper class that namespaces server and client code for '
+ 'this RPC service.'
+ )
gen.line(f'class {service.name()} final {{')
gen.line(' public:')
@@ -195,6 +213,12 @@ def _generate_service_and_client(gen: CodeGenerator,
gen.line(f'{service.name()}() = delete;')
gen.line()
+ gen.line('static constexpr ::pw::rpc::ServiceId service_id() {')
+ with gen.indent():
+ gen.line('return ::pw::rpc::internal::WrapServiceId(kServiceId);')
+ gen.line('}')
+ gen.line()
+
_generate_service(gen, service)
gen.line()
@@ -211,23 +235,40 @@ def _generate_service_and_client(gen: CodeGenerator,
def _check_method_name(method: ProtoServiceMethod) -> None:
- if method.name() in ('Service', 'Client'):
+ # Methods with the same name as their enclosing service will fail
+ # to compile because the generated method will be indistinguishable
+ # from a constructor.
+ if method.name() == method.service().name():
+ raise ValueError(
+ f'Attempted to compile `pw_rpc` for proto with method '
+ f'`{method.name()}` inside a service of the same name. '
+ '`pw_rpc` does not yet support methods with the same name as their '
+ 'enclosing service.'
+ )
+ if method.name() in ('Service', 'ServiceInfo', 'Client'):
raise ValueError(
f'"{method.service().proto_path()}.{method.name()}" is not a '
f'valid method name! The name "{method.name()}" is reserved '
- 'for internal use by pw_rpc.')
+ 'for internal use by pw_rpc.'
+ )
def _generate_client(gen: CodeGenerator, service: ProtoService) -> None:
gen.line('// The Client is used to invoke RPCs for this service.')
- gen.line(f'class Client final : public {RPC_NAMESPACE}::internal::'
- 'ServiceClient {')
+ gen.line(
+ f'class Client final : public {RPC_NAMESPACE}::internal::'
+ 'ServiceClient {'
+ )
gen.line(' public:')
with gen.indent():
- gen.line(f'constexpr Client({RPC_NAMESPACE}::Client& client,'
- ' uint32_t channel_id)')
+ gen.line(
+ f'constexpr Client({RPC_NAMESPACE}::Client& client,'
+ ' uint32_t channel_id)'
+ )
gen.line(' : ServiceClient(client, channel_id) {}')
+ gen.line()
+ gen.line(f'using ServiceInfo = {service.name()};')
for method in service.methods():
gen.line()
@@ -236,33 +277,43 @@ def _generate_client(gen: CodeGenerator, service: ProtoService) -> None:
gen.line('};')
gen.line()
- gen.line('// Static functions for invoking RPCs on a pw_rpc server. '
- 'These functions are ')
- gen.line('// equivalent to instantiating a Client and calling the '
- 'corresponding RPC.')
+ gen.line(
+ '// Static functions for invoking RPCs on a pw_rpc server. '
+ 'These functions are '
+ )
+ gen.line(
+ '// equivalent to instantiating a Client and calling the '
+ 'corresponding RPC.'
+ )
for method in service.methods():
_check_method_name(method)
gen.client_static_function(method)
gen.line()
-def _generate_info(gen: CodeGenerator, namespace: str,
- service: ProtoService) -> None:
+def _generate_info(
+ gen: CodeGenerator, namespace: str, service: ProtoService
+) -> None:
"""Generates MethodInfo for each method."""
service_id = get_id(service)
info = f'struct {RPC_NAMESPACE.lstrip(":")}::internal::MethodInfo'
for method in service.methods():
gen.line('template <>')
- gen.line(f'{info}<{namespace}::pw_rpc::{gen.name()}::'
- f'{service.name()}::{method.name()}> {{')
+ gen.line(
+ f'{info}<{namespace}::pw_rpc::{gen.name()}::'
+ f'{service.name()}::{method.name()}> {{'
+ )
with gen.indent():
gen.line(f'static constexpr uint32_t kServiceId = {service_id};')
- gen.line(f'static constexpr uint32_t kMethodId = '
- f'{get_id(method)};')
- gen.line(f'static constexpr {RPC_NAMESPACE}::MethodType kType = '
- f'{method.type().cc_enum()};')
+ gen.line(
+ f'static constexpr uint32_t kMethodId = ' f'{get_id(method)};'
+ )
+ gen.line(
+ f'static constexpr {RPC_NAMESPACE}::MethodType kType = '
+ f'{method.type().cc_enum()};'
+ )
gen.line()
gen.line('template <typename ServiceImpl>')
@@ -273,6 +324,12 @@ def _generate_info(gen: CodeGenerator, namespace: str,
gen.line('}')
+ gen.line(
+ 'using GeneratedClient = '
+ f'{"::" + namespace if namespace else ""}'
+ f'::pw_rpc::{gen.name()}::{service.name()}::Client;'
+ )
+
gen.method_info_specialization(method)
gen.line('};')
@@ -295,16 +352,21 @@ def _generate_service(gen: CodeGenerator, service: ProtoService) -> None:
gen.service_aliases()
gen.line()
- gen.line(f'static constexpr const char* name() '
- f'{{ return "{service.name()}"; }}')
-
+ gen.line(
+ f'static constexpr const char* name() '
+ f'{{ return "{service.name()}"; }}'
+ )
+ gen.line()
+ gen.line(f'using ServiceInfo = {service.name()};')
gen.line()
gen.line(' protected:')
with gen.indent():
- gen.line('constexpr Service() : '
- f'{base_class}(kServiceId, kPwRpcMethods) {{}}')
+ gen.line(
+ 'constexpr Service() : '
+ f'{base_class}(kServiceId, kPwRpcMethods) {{}}'
+ )
gen.line()
gen.line(' private:')
@@ -314,9 +376,11 @@ def _generate_service(gen: CodeGenerator, service: ProtoService) -> None:
gen.line()
# Generate the method table
- gen.line('static constexpr std::array<'
- f'{RPC_NAMESPACE}::internal::{gen.method_union_name()},'
- f' {len(service.methods())}> kPwRpcMethods = {{')
+ gen.line(
+ 'static constexpr std::array<'
+ f'{RPC_NAMESPACE}::internal::{gen.method_union_name()},'
+ f' {len(service.methods())}> kPwRpcMethods = {{'
+ )
with gen.indent(4):
for method in service.methods():
@@ -332,8 +396,10 @@ def _generate_service(gen: CodeGenerator, service: ProtoService) -> None:
def _method_lookup_table(gen: CodeGenerator, service: ProtoService) -> None:
"""Generates array of method IDs for looking up methods at compile time."""
- gen.line('static constexpr std::array<uint32_t, '
- f'{len(service.methods())}> kPwRpcMethodIds = {{')
+ gen.line(
+ 'static constexpr std::array<uint32_t, '
+ f'{len(service.methods())}> kPwRpcMethodIds = {{'
+ )
with gen.indent(4):
for method in service.methods():
@@ -344,23 +410,26 @@ def _method_lookup_table(gen: CodeGenerator, service: ProtoService) -> None:
class StubGenerator(abc.ABC):
"""Generates stub method implementations that can be copied-and-pasted."""
+
@abc.abstractmethod
def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
"""Returns the signature of this unary method."""
@abc.abstractmethod
- def unary_stub(self, method: ProtoServiceMethod,
- output: OutputFile) -> None:
+ def unary_stub(
+ self, method: ProtoServiceMethod, output: OutputFile
+ ) -> None:
"""Returns the stub for this unary method."""
@abc.abstractmethod
- def server_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
+ def server_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
"""Returns the signature of this server streaming method."""
def server_streaming_stub( # pylint: disable=no-self-use
- self, unused_method: ProtoServiceMethod,
- output: OutputFile) -> None:
+ self, unused_method: ProtoServiceMethod, output: OutputFile
+ ) -> None:
"""Returns the stub for this server streaming method."""
output.write_line(STUB_REQUEST_TODO)
output.write_line('static_cast<void>(request);')
@@ -368,25 +437,27 @@ class StubGenerator(abc.ABC):
output.write_line('static_cast<void>(writer);')
@abc.abstractmethod
- def client_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
+ def client_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
"""Returns the signature of this client streaming method."""
def client_streaming_stub( # pylint: disable=no-self-use
- self, unused_method: ProtoServiceMethod,
- output: OutputFile) -> None:
+ self, unused_method: ProtoServiceMethod, output: OutputFile
+ ) -> None:
"""Returns the stub for this client streaming method."""
output.write_line(STUB_READER_TODO)
output.write_line('static_cast<void>(reader);')
@abc.abstractmethod
- def bidirectional_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
+ def bidirectional_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
"""Returns the signature of this bidirectional streaming method."""
def bidirectional_streaming_stub( # pylint: disable=no-self-use
- self, unused_method: ProtoServiceMethod,
- output: OutputFile) -> None:
+ self, unused_method: ProtoServiceMethod, output: OutputFile
+ ) -> None:
"""Returns the stub for this bidirectional streaming method."""
output.write_line(STUB_READER_WRITER_TODO)
output.write_line('static_cast<void>(reader_writer);')
@@ -403,8 +474,10 @@ def _select_stub_methods(gen: StubGenerator, method: ProtoServiceMethod):
return gen.client_streaming_signature, gen.client_streaming_stub
if method.type() is ProtoServiceMethod.Type.BIDIRECTIONAL_STREAMING:
- return (gen.bidirectional_streaming_signature,
- gen.bidirectional_streaming_stub)
+ return (
+ gen.bidirectional_streaming_signature,
+ gen.bidirectional_streaming_stub,
+ )
raise NotImplementedError(f'Unrecognized method type {method.type()}')
@@ -430,11 +503,12 @@ _STUBS_COMMENT = r'''
'''
-def package_stubs(proto_package: ProtoNode, gen: CodeGenerator,
- stub_generator: StubGenerator) -> None:
+def package_stubs(
+ proto_package: ProtoNode, gen: CodeGenerator, stub_generator: StubGenerator
+) -> None:
"""Generates the RPC stubs for a package."""
- if proto_package.cpp_namespace():
- file_ns = proto_package.cpp_namespace()
+ if proto_package.cpp_namespace(codegen_subnamespace=None):
+ file_ns = proto_package.cpp_namespace(codegen_subnamespace=None)
if file_ns.startswith('::'):
file_ns = file_ns[2:]
@@ -444,7 +518,8 @@ def package_stubs(proto_package: ProtoNode, gen: CodeGenerator,
start_ns = finish_ns = lambda: None
services = [
- cast(ProtoService, node) for node in proto_package
+ cast(ProtoService, node)
+ for node in proto_package
if node.type() == ProtoNode.Type.SERVICE
]
@@ -473,11 +548,14 @@ def package_stubs(proto_package: ProtoNode, gen: CodeGenerator,
gen.line('#endif // _PW_RPC_COMPILE_GENERATED_SERVICE_STUBS')
-def _service_declaration_stub(service: ProtoService, gen: CodeGenerator,
- stub_generator: StubGenerator) -> None:
+def _service_declaration_stub(
+ service: ProtoService, gen: CodeGenerator, stub_generator: StubGenerator
+) -> None:
gen.line(f'// Implementation class for {service.proto_path()}.')
- gen.line(f'class {service.name()} : public pw_rpc::{gen.name()}::'
- f'{service.name()}::Service<{service.name()}> {{')
+ gen.line(
+ f'class {service.name()} : public pw_rpc::{gen.name()}::'
+ f'{service.name()}::Service<{service.name()}> {{'
+ )
gen.line(' public:')
@@ -497,8 +575,9 @@ def _service_declaration_stub(service: ProtoService, gen: CodeGenerator,
gen.line('};\n')
-def _service_definition_stub(service: ProtoService, gen: CodeGenerator,
- stub_generator: StubGenerator) -> None:
+def _service_definition_stub(
+ service: ProtoService, gen: CodeGenerator, stub_generator: StubGenerator
+) -> None:
gen.line(f'// Method definitions for {service.proto_path()}.')
blank_line = False
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
index 463de4fc1..35a119418 100644
--- a/pw_rpc/py/pw_rpc/codegen_nanopb.py
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -20,8 +20,12 @@ from pw_protobuf.output_file import OutputFile
from pw_protobuf.proto_tree import ProtoServiceMethod
from pw_protobuf.proto_tree import build_node_tree
from pw_rpc import codegen
-from pw_rpc.codegen import (client_call_type, get_id, CodeGenerator,
- RPC_NAMESPACE)
+from pw_rpc.codegen import (
+ client_call_type,
+ get_id,
+ CodeGenerator,
+ RPC_NAMESPACE,
+)
PROTO_H_EXTENSION = '.pb.h'
PROTO_CC_EXTENSION = '.pb.cc'
@@ -30,9 +34,11 @@ NANOPB_H_EXTENSION = '.pb.h'
def _serde(method: ProtoServiceMethod) -> str:
"""Returns the NanopbMethodSerde for this method."""
- return (f'{RPC_NAMESPACE}::internal::kNanopbMethodSerde<'
- f'{method.request_type().nanopb_fields()}, '
- f'{method.response_type().nanopb_fields()}>')
+ return (
+ f'{RPC_NAMESPACE}::internal::kNanopbMethodSerde<'
+ f'{method.request_type().nanopb_fields()}, '
+ f'{method.response_type().nanopb_fields()}>'
+ )
def _proto_filename_to_nanopb_header(proto_file: str) -> str:
@@ -71,14 +77,17 @@ def _user_args(method: ProtoServiceMethod) -> Iterable[str]:
yield f'::pw::Function<void(const {response}&)>&& on_next = nullptr'
yield '::pw::Function<void(::pw::Status)>&& on_completed = nullptr'
else:
- yield (f'::pw::Function<void(const {response}&, ::pw::Status)>&& '
- 'on_completed = nullptr')
+ yield (
+ f'::pw::Function<void(const {response}&, ::pw::Status)>&& '
+ 'on_completed = nullptr'
+ )
yield '::pw::Function<void(::pw::Status)>&& on_error = nullptr'
class NanopbCodeGenerator(CodeGenerator):
"""Generates an RPC service and client using the Nanopb API."""
+
def name(self) -> str:
return 'nanopb'
@@ -98,22 +107,29 @@ class NanopbCodeGenerator(CodeGenerator):
def service_aliases(self) -> None:
self.line('template <typename Response>')
- self.line('using ServerWriter = '
- f'{RPC_NAMESPACE}::NanopbServerWriter<Response>;')
+ self.line(
+ 'using ServerWriter = '
+ f'{RPC_NAMESPACE}::NanopbServerWriter<Response>;'
+ )
self.line('template <typename Request, typename Response>')
- self.line('using ServerReader = '
- f'{RPC_NAMESPACE}::NanopbServerReader<Request, Response>;')
+ self.line(
+ 'using ServerReader = '
+ f'{RPC_NAMESPACE}::NanopbServerReader<Request, Response>;'
+ )
self.line('template <typename Request, typename Response>')
self.line(
'using ServerReaderWriter = '
- f'{RPC_NAMESPACE}::NanopbServerReaderWriter<Request, Response>;')
+ f'{RPC_NAMESPACE}::NanopbServerReaderWriter<Request, Response>;'
+ )
def method_descriptor(self, method: ProtoServiceMethod) -> None:
- self.line(f'{RPC_NAMESPACE}::internal::'
- f'GetNanopbOrRawMethodFor<&Implementation::{method.name()}, '
- f'{method.type().cc_enum()}, '
- f'{method.request_type().nanopb_struct()}, '
- f'{method.response_type().nanopb_struct()}>(')
+ self.line(
+ f'{RPC_NAMESPACE}::internal::'
+ f'GetNanopbOrRawMethodFor<&Implementation::{method.name()}, '
+ f'{method.type().cc_enum()}, '
+ f'{method.request_type().nanopb_struct()}, '
+ f'{method.response_type().nanopb_struct()}>('
+ )
with self.indent(4):
self.line(f'{get_id(method)}, // Hash of "{method.name()}"')
self.line(f'{_serde(method)}),')
@@ -127,10 +143,12 @@ class NanopbCodeGenerator(CodeGenerator):
with self.indent():
client_call = _client_call(method)
base = 'Stream' if method.server_streaming() else 'Unary'
- self.line(f'return {RPC_NAMESPACE}::internal::'
- f'Nanopb{base}ResponseClientCall<'
- f'{method.response_type().nanopb_struct()}>::'
- f'Start<{client_call}>(')
+ self.line(
+ f'return {RPC_NAMESPACE}::internal::'
+ f'Nanopb{base}ResponseClientCall<'
+ f'{method.response_type().nanopb_struct()}>::'
+ f'Start<{client_call}>('
+ )
service_client = RPC_NAMESPACE + '::internal::ServiceClient'
@@ -156,10 +174,12 @@ class NanopbCodeGenerator(CodeGenerator):
def client_static_function(self, method: ProtoServiceMethod) -> None:
self.line(f'static {_function(method)}(')
- self.indented_list(f'{RPC_NAMESPACE}::Client& client',
- 'uint32_t channel_id',
- *_user_args(method),
- end=') {')
+ self.indented_list(
+ f'{RPC_NAMESPACE}::Client& client',
+ 'uint32_t channel_id',
+ *_user_args(method),
+ end=') {',
+ )
with self.indent():
self.line(f'return Client(client, channel_id).{method.name()}(')
@@ -172,21 +192,24 @@ class NanopbCodeGenerator(CodeGenerator):
if method.server_streaming():
args.append('std::move(on_next)')
- self.indented_list(*args,
- 'std::move(on_completed)',
- 'std::move(on_error)',
- end=');')
+ self.indented_list(
+ *args,
+ 'std::move(on_completed)',
+ 'std::move(on_error)',
+ end=');',
+ )
self.line('}')
def method_info_specialization(self, method: ProtoServiceMethod) -> None:
self.line()
self.line(f'using Request = {method.request_type().nanopb_struct()};')
- self.line(
- f'using Response = {method.response_type().nanopb_struct()};')
+ self.line(f'using Response = {method.response_type().nanopb_struct()};')
self.line()
- self.line(f'static constexpr const {RPC_NAMESPACE}::internal::'
- 'NanopbMethodSerde& serde() {')
+ self.line(
+ f'static constexpr const {RPC_NAMESPACE}::internal::'
+ 'NanopbMethodSerde& serde() {'
+ )
with self.indent():
self.line(f'return {_serde(method)};')
self.line('}')
@@ -208,37 +231,49 @@ class _CallbackFunction(NamedTuple):
class StubGenerator(codegen.StubGenerator):
"""Generates Nanopb RPC stubs."""
+
def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
- return (f'::pw::Status {prefix}{method.name()}( '
- f'const {method.request_type().nanopb_struct()}& request, '
- f'{method.response_type().nanopb_struct()}& response)')
+ return (
+ f'::pw::Status {prefix}{method.name()}( '
+ f'const {method.request_type().nanopb_struct()}& request, '
+ f'{method.response_type().nanopb_struct()}& response)'
+ )
- def unary_stub(self, method: ProtoServiceMethod,
- output: OutputFile) -> None:
+ def unary_stub(
+ self, method: ProtoServiceMethod, output: OutputFile
+ ) -> None:
output.write_line(codegen.STUB_REQUEST_TODO)
output.write_line('static_cast<void>(request);')
output.write_line(codegen.STUB_RESPONSE_TODO)
output.write_line('static_cast<void>(response);')
output.write_line('return ::pw::Status::Unimplemented();')
- def server_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
+ def server_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
return (
f'void {prefix}{method.name()}( '
f'const {method.request_type().nanopb_struct()}& request, '
- f'ServerWriter<{method.response_type().nanopb_struct()}>& writer)')
-
- def client_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
- return (f'void {prefix}{method.name()}( '
- f'ServerReader<{method.request_type().nanopb_struct()}, '
- f'{method.response_type().nanopb_struct()}>& reader)')
-
- def bidirectional_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
- return (f'void {prefix}{method.name()}( '
- f'ServerReaderWriter<{method.request_type().nanopb_struct()}, '
- f'{method.response_type().nanopb_struct()}>& reader_writer)')
+ f'ServerWriter<{method.response_type().nanopb_struct()}>& writer)'
+ )
+
+ def client_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
+ return (
+ f'void {prefix}{method.name()}( '
+ f'ServerReader<{method.request_type().nanopb_struct()}, '
+ f'{method.response_type().nanopb_struct()}>& reader)'
+ )
+
+ def bidirectional_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
+ return (
+ f'void {prefix}{method.name()}( '
+ f'ServerReaderWriter<{method.request_type().nanopb_struct()}, '
+ f'{method.response_type().nanopb_struct()}>& reader_writer)'
+ )
def process_proto_file(proto_file) -> Iterable[OutputFile]:
diff --git a/pw_rpc/py/pw_rpc/codegen_pwpb.py b/pw_rpc/py/pw_rpc/codegen_pwpb.py
new file mode 100644
index 000000000..a9bf2383b
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/codegen_pwpb.py
@@ -0,0 +1,277 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""This module generates the code for pw_protobuf pw_rpc services."""
+
+import os
+from typing import Iterable
+
+from pw_protobuf.output_file import OutputFile
+from pw_protobuf.proto_tree import ProtoServiceMethod
+from pw_protobuf.proto_tree import build_node_tree
+from pw_rpc import codegen
+from pw_rpc.codegen import (
+ client_call_type,
+ get_id,
+ CodeGenerator,
+ RPC_NAMESPACE,
+)
+
+PROTO_H_EXTENSION = '.pwpb.h'
+PWPB_H_EXTENSION = '.pwpb.h'
+
+
+def _proto_filename_to_pwpb_header(proto_file: str) -> str:
+ """Returns the generated pwpb header name for a .proto file."""
+ filename = os.path.splitext(proto_file)[0]
+ return f'{filename}{PWPB_H_EXTENSION}'
+
+
+def _proto_filename_to_generated_header(proto_file: str) -> str:
+ """Returns the generated C++ RPC header name for a .proto file."""
+ filename = os.path.splitext(proto_file)[0]
+ return f'{filename}.rpc{PROTO_H_EXTENSION}'
+
+
+def _serde(method: ProtoServiceMethod) -> str:
+ """Returns the PwpbMethodSerde for this method."""
+ return (
+ f'{RPC_NAMESPACE}::internal::kPwpbMethodSerde<'
+ f'&{method.request_type().pwpb_table()}, '
+ f'&{method.response_type().pwpb_table()}>'
+ )
+
+
+def _client_call(method: ProtoServiceMethod) -> str:
+ template_args = []
+
+ if method.client_streaming():
+ template_args.append(method.request_type().pwpb_struct())
+
+ template_args.append(method.response_type().pwpb_struct())
+
+ return f'{client_call_type(method, "Pwpb")}<{", ".join(template_args)}>'
+
+
+def _function(method: ProtoServiceMethod) -> str:
+ return f'{_client_call(method)} {method.name()}'
+
+
+def _user_args(method: ProtoServiceMethod) -> Iterable[str]:
+ if not method.client_streaming():
+ yield f'const {method.request_type().pwpb_struct()}& request'
+
+ response = method.response_type().pwpb_struct()
+
+ if method.server_streaming():
+ yield f'::pw::Function<void(const {response}&)>&& on_next = nullptr'
+ yield '::pw::Function<void(::pw::Status)>&& on_completed = nullptr'
+ else:
+ yield (
+ f'::pw::Function<void(const {response}&, ::pw::Status)>&& '
+ 'on_completed = nullptr'
+ )
+
+ yield '::pw::Function<void(::pw::Status)>&& on_error = nullptr'
+
+
+class PwpbCodeGenerator(CodeGenerator):
+ """Generates an RPC service and client using the pw_protobuf API."""
+
+ def name(self) -> str:
+ return 'pwpb'
+
+ def method_union_name(self) -> str:
+ return 'PwpbMethodUnion'
+
+ def includes(self, proto_file_name: str) -> Iterable[str]:
+ yield '#include "pw_rpc/pwpb/client_reader_writer.h"'
+ yield '#include "pw_rpc/pwpb/internal/method_union.h"'
+ yield '#include "pw_rpc/pwpb/server_reader_writer.h"'
+
+ # Include the corresponding pwpb header file for this proto file, in
+ # which the file's messages and enums are generated. All other files
+ # imported from the .proto file are #included in there.
+ pwpb_header = _proto_filename_to_pwpb_header(proto_file_name)
+ yield f'#include "{pwpb_header}"'
+
+ def service_aliases(self) -> None:
+ self.line('template <typename Response>')
+ self.line(
+ 'using ServerWriter = '
+ f'{RPC_NAMESPACE}::PwpbServerWriter<Response>;'
+ )
+ self.line('template <typename Request, typename Response>')
+ self.line(
+ 'using ServerReader = '
+ f'{RPC_NAMESPACE}::PwpbServerReader<Request, Response>;'
+ )
+ self.line('template <typename Request, typename Response>')
+ self.line(
+ 'using ServerReaderWriter = '
+ f'{RPC_NAMESPACE}::PwpbServerReaderWriter<Request, Response>;'
+ )
+
+ def method_descriptor(self, method: ProtoServiceMethod) -> None:
+ impl_method = f'&Implementation::{method.name()}'
+
+ self.line(
+ f'{RPC_NAMESPACE}::internal::GetPwpbOrRawMethodFor<{impl_method}, '
+ f'{method.type().cc_enum()}, '
+ f'{method.request_type().pwpb_struct()}, '
+ f'{method.response_type().pwpb_struct()}>('
+ )
+ with self.indent(4):
+ self.line(f'{get_id(method)}, // Hash of "{method.name()}"')
+ self.line(f'{_serde(method)}),')
+
+ def client_member_function(self, method: ProtoServiceMethod) -> None:
+ """Outputs client code for a single RPC method."""
+
+ self.line(f'{_function(method)}(')
+ self.indented_list(*_user_args(method), end=') const {')
+
+ with self.indent():
+ client_call = _client_call(method)
+ base = 'Stream' if method.server_streaming() else 'Unary'
+ self.line(
+ f'return {RPC_NAMESPACE}::internal::'
+ f'Pwpb{base}ResponseClientCall<'
+ f'{method.response_type().pwpb_struct()}>::'
+ f'Start<{client_call}>('
+ )
+
+ service_client = RPC_NAMESPACE + '::internal::ServiceClient'
+
+ args = [
+ f'{service_client}::client()',
+ f'{service_client}::channel_id()',
+ 'kServiceId',
+ get_id(method),
+ _serde(method),
+ ]
+ if method.server_streaming():
+ args.append('std::move(on_next)')
+
+ args.append('std::move(on_completed)')
+ args.append('std::move(on_error)')
+
+ if not method.client_streaming():
+ args.append('request')
+
+ self.indented_list(*args, end=');')
+
+ self.line('}')
+
+ def client_static_function(self, method: ProtoServiceMethod) -> None:
+ self.line(f'static {_function(method)}(')
+ self.indented_list(
+ f'{RPC_NAMESPACE}::Client& client',
+ 'uint32_t channel_id',
+ *_user_args(method),
+ end=') {',
+ )
+
+ with self.indent():
+ self.line(f'return Client(client, channel_id).{method.name()}(')
+
+ args = []
+
+ if not method.client_streaming():
+ args.append('request')
+
+ if method.server_streaming():
+ args.append('std::move(on_next)')
+
+ self.indented_list(
+ *args,
+ 'std::move(on_completed)',
+ 'std::move(on_error)',
+ end=');',
+ )
+
+ self.line('}')
+
+ def method_info_specialization(self, method: ProtoServiceMethod) -> None:
+ self.line()
+ self.line(f'using Request = {method.request_type().pwpb_struct()};')
+ self.line(f'using Response = {method.response_type().pwpb_struct()};')
+ self.line()
+ self.line(
+ f'static constexpr const {RPC_NAMESPACE}::'
+ 'PwpbMethodSerde& serde() {'
+ )
+ with self.indent():
+ self.line(f'return {_serde(method)};')
+ self.line('}')
+
+
+class StubGenerator(codegen.StubGenerator):
+ """Generates pw_protobuf RPC stubs."""
+
+ def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
+ return (
+ f'::pw::Status {prefix}{method.name()}( '
+ f'const {method.request_type().pwpb_struct()}& request, '
+ f'{method.response_type().pwpb_struct()}& response)'
+ )
+
+ def unary_stub(
+ self, method: ProtoServiceMethod, output: OutputFile
+ ) -> None:
+ output.write_line(codegen.STUB_REQUEST_TODO)
+ output.write_line('static_cast<void>(request);')
+ output.write_line(codegen.STUB_RESPONSE_TODO)
+ output.write_line('static_cast<void>(response);')
+ output.write_line('return ::pw::Status::Unimplemented();')
+
+ def server_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
+ return (
+ f'void {prefix}{method.name()}( '
+ f'const {method.request_type().pwpb_struct()}& request, '
+ f'ServerWriter<{method.response_type().pwpb_struct()}>& writer)'
+ )
+
+ def client_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
+ return (
+ f'void {prefix}{method.name()}( '
+ f'ServerReader<{method.request_type().pwpb_struct()}, '
+ f'{method.response_type().pwpb_struct()}>& reader)'
+ )
+
+ def bidirectional_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
+ return (
+ f'void {prefix}{method.name()}( '
+ f'ServerReaderWriter<{method.request_type().pwpb_struct()}, '
+ f'{method.response_type().pwpb_struct()}>& reader_writer)'
+ )
+
+
+def process_proto_file(proto_file) -> Iterable[OutputFile]:
+ """Generates code for a single .proto file."""
+
+ _, package_root = build_node_tree(proto_file)
+ output_filename = _proto_filename_to_generated_header(proto_file.name)
+
+ generator = PwpbCodeGenerator(output_filename)
+ codegen.generate_package(proto_file, package_root, generator)
+
+ codegen.package_stubs(package_root, generator, StubGenerator())
+
+ return [generator.output]
diff --git a/pw_rpc/py/pw_rpc/codegen_raw.py b/pw_rpc/py/pw_rpc/codegen_raw.py
index 15bc15ba3..01e03466a 100644
--- a/pw_rpc/py/pw_rpc/codegen_raw.py
+++ b/pw_rpc/py/pw_rpc/codegen_raw.py
@@ -20,8 +20,12 @@ from pw_protobuf.output_file import OutputFile
from pw_protobuf.proto_tree import ProtoServiceMethod
from pw_protobuf.proto_tree import build_node_tree
from pw_rpc import codegen
-from pw_rpc.codegen import (client_call_type, get_id, CodeGenerator,
- RPC_NAMESPACE)
+from pw_rpc.codegen import (
+ client_call_type,
+ get_id,
+ CodeGenerator,
+ RPC_NAMESPACE,
+)
PROTO_H_EXTENSION = '.pb.h'
@@ -50,14 +54,17 @@ def _user_args(method: ProtoServiceMethod) -> Iterable[str]:
yield '::pw::Function<void(::pw::ConstByteSpan)>&& on_next = nullptr'
yield '::pw::Function<void(::pw::Status)>&& on_completed = nullptr'
else:
- yield ('::pw::Function<void(::pw::ConstByteSpan, ::pw::Status)>&& '
- 'on_completed = nullptr')
+ yield (
+ '::pw::Function<void(::pw::ConstByteSpan, ::pw::Status)>&& '
+ 'on_completed = nullptr'
+ )
yield '::pw::Function<void(::pw::Status)>&& on_error = nullptr'
class RawCodeGenerator(CodeGenerator):
"""Generates an RPC service and client using the raw buffers API."""
+
def name(self) -> str:
return 'raw'
@@ -72,14 +79,18 @@ class RawCodeGenerator(CodeGenerator):
def service_aliases(self) -> None:
self.line(f'using RawServerWriter = {RPC_NAMESPACE}::RawServerWriter;')
self.line(f'using RawServerReader = {RPC_NAMESPACE}::RawServerReader;')
- self.line('using RawServerReaderWriter = '
- f'{RPC_NAMESPACE}::RawServerReaderWriter;')
+ self.line(
+ 'using RawServerReaderWriter = '
+ f'{RPC_NAMESPACE}::RawServerReaderWriter;'
+ )
def method_descriptor(self, method: ProtoServiceMethod) -> None:
impl_method = f'&Implementation::{method.name()}'
- self.line(f'{RPC_NAMESPACE}::internal::GetRawMethodFor<{impl_method}, '
- f'{method.type().cc_enum()}>(')
+ self.line(
+ f'{RPC_NAMESPACE}::internal::GetRawMethodFor<{impl_method}, '
+ f'{method.type().cc_enum()}>('
+ )
self.line(f' {get_id(method)}), // Hash of "{method.name()}"')
def client_member_function(self, method: ProtoServiceMethod) -> None:
@@ -88,9 +99,11 @@ class RawCodeGenerator(CodeGenerator):
with self.indent():
base = 'Stream' if method.server_streaming() else 'Unary'
- self.line(f'return {RPC_NAMESPACE}::internal::'
- f'{base}ResponseClientCall::'
- f'Start<{client_call_type(method, "Raw")}>(')
+ self.line(
+ f'return {RPC_NAMESPACE}::internal::'
+ f'{base}ResponseClientCall::'
+ f'Start<{client_call_type(method, "Raw")}>('
+ )
service_client = RPC_NAMESPACE + '::internal::ServiceClient'
arg = ['std::move(on_next)'] if method.server_streaming() else []
@@ -104,16 +117,19 @@ class RawCodeGenerator(CodeGenerator):
'std::move(on_completed)',
'std::move(on_error)',
'{}' if method.client_streaming() else 'request',
- end=');')
+ end=');',
+ )
self.line('}')
def client_static_function(self, method: ProtoServiceMethod) -> None:
self.line(f'static {_function(method)}(')
- self.indented_list(f'{RPC_NAMESPACE}::Client& client',
- 'uint32_t channel_id',
- *_user_args(method),
- end=') {')
+ self.indented_list(
+ f'{RPC_NAMESPACE}::Client& client',
+ 'uint32_t channel_id',
+ *_user_args(method),
+ end=') {',
+ )
with self.indent():
self.line(f'return Client(client, channel_id).{method.name()}(')
@@ -126,40 +142,66 @@ class RawCodeGenerator(CodeGenerator):
if method.server_streaming():
args.append('std::move(on_next)')
- self.indented_list(*args,
- 'std::move(on_completed)',
- 'std::move(on_error)',
- end=');')
+ self.indented_list(
+ *args,
+ 'std::move(on_completed)',
+ 'std::move(on_error)',
+ end=');',
+ )
self.line('}')
+ def method_info_specialization(self, method: ProtoServiceMethod) -> None:
+ self.line()
+ # We have Request/Response as voids to mark raw as a special case.
+ # Raw operates in ConstByteSpans, which won't be copied by copying the
+ # span itself and without special treatment will lead to dangling
+ # pointers.
+ #
+ # Helpers/traits that want to use Request/Response and should support
+ # raw are required to do a special implementation for them instead that
+ # will copy the actual data.
+ self.line('using Request = void;')
+ self.line('using Response = void;')
+
class StubGenerator(codegen.StubGenerator):
- def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
- return (f'void {prefix}{method.name()}(pw::ConstByteSpan request, '
- 'pw::rpc::RawUnaryResponder& responder)')
+ """TODO(frolv) Add docstring."""
- def unary_stub(self, method: ProtoServiceMethod,
- output: OutputFile) -> None:
+ def unary_signature(self, method: ProtoServiceMethod, prefix: str) -> str:
+ return (
+ f'void {prefix}{method.name()}(pw::ConstByteSpan request, '
+ 'pw::rpc::RawUnaryResponder& responder)'
+ )
+
+ def unary_stub(
+ self, method: ProtoServiceMethod, output: OutputFile
+ ) -> None:
output.write_line(codegen.STUB_REQUEST_TODO)
output.write_line('static_cast<void>(request);')
output.write_line(codegen.STUB_RESPONSE_TODO)
output.write_line('static_cast<void>(responder);')
- def server_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
-
- return (f'void {prefix}{method.name()}('
- 'pw::ConstByteSpan request, RawServerWriter& writer)')
-
- def client_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
+ def server_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
+ return (
+ f'void {prefix}{method.name()}('
+ 'pw::ConstByteSpan request, RawServerWriter& writer)'
+ )
+
+ def client_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
return f'void {prefix}{method.name()}(RawServerReader& reader)'
- def bidirectional_streaming_signature(self, method: ProtoServiceMethod,
- prefix: str) -> str:
- return (f'void {prefix}{method.name()}('
- 'RawServerReaderWriter& reader_writer)')
+ def bidirectional_streaming_signature(
+ self, method: ProtoServiceMethod, prefix: str
+ ) -> str:
+ return (
+ f'void {prefix}{method.name()}('
+ 'RawServerReaderWriter& reader_writer)'
+ )
def process_proto_file(proto_file) -> Iterable[OutputFile]:
diff --git a/pw_rpc/py/pw_rpc/console_tools/__init__.py b/pw_rpc/py/pw_rpc/console_tools/__init__.py
index 0732ac3f6..510041322 100644
--- a/pw_rpc/py/pw_rpc/console_tools/__init__.py
+++ b/pw_rpc/py/pw_rpc/console_tools/__init__.py
@@ -13,8 +13,12 @@
# the License.
"""Utilities for building tools that interact with pw_rpc."""
-from pw_rpc.console_tools.console import (Context, CommandHelper, ClientInfo,
- flattened_rpc_completions,
- alias_deprecated_command)
+from pw_rpc.console_tools.console import (
+ Context,
+ CommandHelper,
+ ClientInfo,
+ flattened_rpc_completions,
+ alias_deprecated_command,
+)
from pw_rpc.console_tools.functions import help_as_repr
from pw_rpc.console_tools.watchdog import Watchdog
diff --git a/pw_rpc/py/pw_rpc/console_tools/console.py b/pw_rpc/py/pw_rpc/console_tools/console.py
index 2dcdad66b..51544f6f9 100644
--- a/pw_rpc/py/pw_rpc/console_tools/console.py
+++ b/pw_rpc/py/pw_rpc/console_tools/console.py
@@ -19,7 +19,15 @@ from itertools import chain
import inspect
import textwrap
import types
-from typing import Any, Collection, Dict, Iterable, Mapping, NamedTuple
+from typing import (
+ Any,
+ Collection,
+ Dict,
+ Iterable,
+ Mapping,
+ NamedTuple,
+ Optional,
+)
import pw_status
from pw_protobuf_compiler import python_protos
@@ -33,20 +41,24 @@ _INDENT = ' '
class CommandHelper:
"""Used to implement a help command in an RPC console."""
+
@classmethod
- def from_methods(cls,
- methods: Iterable[Method],
- variables: Mapping[str, object],
- header: str,
- footer: str = '') -> 'CommandHelper':
- return cls({m.full_name: m
- for m in methods}, variables, header, footer)
-
- def __init__(self,
- methods: Mapping[str, object],
- variables: Mapping[str, object],
- header: str,
- footer: str = ''):
+ def from_methods(
+ cls,
+ methods: Iterable[Method],
+ variables: Mapping[str, object],
+ header: str,
+ footer: str = '',
+ ) -> 'CommandHelper':
+ return cls({m.full_name: m for m in methods}, variables, header, footer)
+
+ def __init__(
+ self,
+ methods: Mapping[str, object],
+ variables: Mapping[str, object],
+ header: str,
+ footer: str = '',
+ ):
self._methods = methods
self._variables = variables
self.header = header
@@ -58,11 +70,13 @@ class CommandHelper:
if item is None:
all_vars = '\n'.join(sorted(self._variables_without_methods()))
all_rpcs = '\n'.join(self._methods)
- return (f'{self.header}\n\n'
- f'All variables:\n\n{textwrap.indent(all_vars, _INDENT)}'
- '\n\n'
- f'All commands:\n\n{textwrap.indent(all_rpcs, _INDENT)}'
- f'\n\n{self.footer}'.strip())
+ return (
+ f'{self.header}\n\n'
+ f'All variables:\n\n{textwrap.indent(all_vars, _INDENT)}'
+ '\n\n'
+ f'All commands:\n\n{textwrap.indent(all_rpcs, _INDENT)}'
+ f'\n\n{self.footer}'.strip()
+ )
# If item is a string, find commands matching that.
if isinstance(item, str):
@@ -75,17 +89,20 @@ class CommandHelper:
return f'{name}\n\n{inspect.getdoc(method)}'
return f'Multiple matches for {item!r}:\n\n' + textwrap.indent(
- '\n'.join(matches), _INDENT)
+ '\n'.join(matches), _INDENT
+ )
return inspect.getdoc(item) or f'No documentation for {item!r}.'
def _variables_without_methods(self) -> Mapping[str, object]:
packages = frozenset(
- n.split('.', 1)[0] for n in self._methods if '.' in n)
+ n.split('.', 1)[0] for n in self._methods if '.' in n
+ )
return {
name: var
- for name, var in self._variables.items() if name not in packages
+ for name, var in self._variables.items()
+ if name not in packages
}
def __call__(self, item: object = None) -> None:
@@ -99,6 +116,7 @@ class CommandHelper:
class ClientInfo(NamedTuple):
"""Information about an RPC client as it appears in the console."""
+
# The name to use in the console to refer to this client.
name: str
@@ -110,7 +128,8 @@ class ClientInfo(NamedTuple):
def flattened_rpc_completions(
- client_info_list: Collection[ClientInfo], ) -> Dict[str, str]:
+ client_info_list: Collection[ClientInfo],
+) -> Dict[str, str]:
"""Create a flattened list of rpc commands for repl auto-completion.
This gathers all rpc commands from a set of ClientInfo variables and
@@ -131,15 +150,18 @@ def flattened_rpc_completions(
}
"""
rpc_list = list(
- chain.from_iterable([
- '{}.rpcs.{}'.format(c.name, a.full_name)
- for a in c.rpc_client.methods()
- ] for c in client_info_list))
+ chain.from_iterable(
+ [
+ '{}.rpcs.{}'.format(c.name, a.full_name)
+ for a in c.rpc_client.methods()
+ ]
+ for c in client_info_list
+ )
+ )
# Dict should contain completion text as keys and descriptions as values.
custom_word_completions = {
- flattened_rpc_name: 'RPC'
- for flattened_rpc_name in rpc_list
+ flattened_rpc_name: 'RPC' for flattened_rpc_name in rpc_list
}
return custom_word_completions
@@ -159,12 +181,15 @@ class Context:
IPython.terminal.embed.InteractiveShellEmbed().mainloop(
module=types.SimpleNamespace(**context.variables()))
"""
- def __init__(self,
- client_info: Collection[ClientInfo],
- default_client: Any,
- protos: python_protos.Library,
- *,
- help_header: str = '') -> None:
+
+ def __init__(
+ self,
+ client_info: Collection[ClientInfo],
+ default_client: Any,
+ protos: python_protos.Library,
+ *,
+ help_header: str = '',
+ ) -> None:
"""Creates an RPC console context.
Protos and RPC services are accessible by their proto package and name.
@@ -185,7 +210,8 @@ class Context:
# Store objects with references to RPC services, sorted by package.
self._services: Dict[str, types.SimpleNamespace] = defaultdict(
- types.SimpleNamespace)
+ types.SimpleNamespace
+ )
self._variables: Dict[str, object] = dict(
Status=pw_status.Status,
@@ -199,18 +225,24 @@ class Context:
# Make the proto package hierarchy directly available in the console.
for package in self.protos.packages:
- self._variables[package._package] = package # pylint: disable=protected-access
+ self._variables[
+ package._package
+ ] = package # pylint: disable=protected-access
# Monkey patch the message types to use an improved repr function.
for message_type in self.protos.messages():
message_type.__repr__ = python_protos.proto_repr
# Set up the 'help' command.
- all_methods = chain.from_iterable(c.rpc_client.methods()
- for c in self.client_info)
+ all_methods = chain.from_iterable(
+ c.rpc_client.methods() for c in self.client_info
+ )
self._helper = CommandHelper.from_methods(
- all_methods, self._variables, help_header,
- 'Type a command and hit Enter to see detailed help information.')
+ all_methods,
+ self._variables,
+ help_header,
+ 'Type a command and hit Enter to see detailed help information.',
+ )
self._variables['help'] = self._helper
@@ -225,9 +257,9 @@ class Context:
"""Returns a mapping of names to variables for use in an RPC console."""
return self._variables
- def set_target(self,
- selected_client: object,
- channel_id: int = None) -> None:
+ def set_target(
+ self, selected_client: object, channel_id: Optional[int] = None
+ ) -> None:
"""Sets the default target for commands."""
# Make sure the variable is one of the client variables.
name = ''
@@ -238,8 +270,10 @@ class Context:
print('CURRENT RPC TARGET:', name)
break
else:
- raise ValueError('Supported targets :' +
- ', '.join(c.name for c in self.client_info))
+ raise ValueError(
+ 'Supported targets :'
+ + ', '.join(c.name for c in self.client_info)
+ )
# Update the RPC services to use the newly selected target.
for service_client in rpc_client.channel(channel_id).rpcs:
@@ -248,19 +282,25 @@ class Context:
method.request_type.__repr__ = python_protos.proto_repr
method.response_type.__repr__ = python_protos.proto_repr
- service = service_client._service # pylint: disable=protected-access
- setattr(self._services[service.package], service.name,
- service_client)
+ service = (
+ service_client._service # pylint: disable=protected-access
+ )
+ setattr(
+ self._services[service.package], service.name, service_client
+ )
# Add the RPC methods to their proto packages.
for package_name, rpcs in self._services.items():
- self.protos.packages[package_name]._add_item(rpcs) # pylint: disable=protected-access
+ # pylint: disable=protected-access
+ self.protos.packages[package_name]._add_item(rpcs)
+ # pylint: enable=protected-access
self.current_client = selected_client
def _create_command_alias(command: Any, name: str, message: str) -> object:
"""Wraps __call__, __getattr__, and __repr__ to print a message."""
+
@functools.wraps(command.__call__)
def print_message_and_call(_, *args, **kwargs):
print(message)
@@ -272,10 +312,14 @@ def _create_command_alias(command: Any, name: str, message: str) -> object:
return attr
return type(
- name, (),
- dict(__call__=print_message_and_call,
- __getattr__=getattr_and_print_message,
- __repr__=lambda _: message))()
+ name,
+ (),
+ dict(
+ __call__=print_message_and_call,
+ __getattr__=getattr_and_print_message,
+ __repr__=lambda _: message,
+ ),
+ )()
def _access_in_dict_or_namespace(item, name: str, create_if_missing: bool):
@@ -305,21 +349,24 @@ def _access_names(item, names: Iterable[str], create_if_missing: bool):
return item
-def alias_deprecated_command(variables: Any, old_name: str,
- new_name: str) -> None:
+def alias_deprecated_command(
+ variables: Any, old_name: str, new_name: str
+) -> None:
"""Adds an alias for an old command that redirects to the new command.
The deprecated command prints a message then invokes the new command.
"""
# Get the new command.
- item = _access_names(variables,
- new_name.split('.'),
- create_if_missing=False)
+ item = _access_names(
+ variables, new_name.split('.'), create_if_missing=False
+ )
# Create a wrapper to the new comamnd with the old name.
wrapper = _create_command_alias(
- item, old_name,
- f'WARNING: {old_name} is DEPRECATED; use {new_name} instead')
+ item,
+ old_name,
+ f'WARNING: {old_name} is DEPRECATED; use {new_name} instead',
+ )
# Add the wrapper to the variables with the old command's name.
name_parts = old_name.split('.')
diff --git a/pw_rpc/py/pw_rpc/console_tools/functions.py b/pw_rpc/py/pw_rpc/console_tools/functions.py
index dea625171..edc7835a6 100644
--- a/pw_rpc/py/pw_rpc/console_tools/functions.py
+++ b/pw_rpc/py/pw_rpc/console_tools/functions.py
@@ -53,12 +53,14 @@ def format_signature(name: str, signature: inspect.Signature) -> str:
Does not yet handle / and * markers.
"""
params = ', '.join(
- format_parameter(arg) for arg in signature.parameters.values())
+ format_parameter(arg) for arg in signature.parameters.values()
+ )
if signature.return_annotation is signature.empty:
return_annotation = ''
else:
return_annotation = ' -> ' + _annotation_name(
- signature.return_annotation)
+ signature.return_annotation
+ )
return f'{name}({params}){return_annotation}'
@@ -66,7 +68,8 @@ def format_signature(name: str, signature: inspect.Signature) -> str:
def format_function_help(function: Callable) -> str:
"""Formats a help string with a declaration and docstring."""
signature = format_signature(
- function.__name__, inspect.signature(function, follow_wrapped=False))
+ function.__name__, inspect.signature(function, follow_wrapped=False)
+ )
docs = inspect.getdoc(function) or '(no docstring)'
return f'{signature}:\n\n{textwrap.indent(docs, " ")}'
@@ -80,11 +83,16 @@ def help_as_repr(function: Callable) -> Callable:
with the full function signature, type annotations, and docstring when the
function is wrapped with help_as_repr.
"""
+
def display_help(_):
return format_function_help(function)
return type(
- function.__name__, (),
- dict(__call__=staticmethod(function),
- __doc__=format_function_help(function),
- __repr__=display_help))()
+ function.__name__,
+ (),
+ dict(
+ __call__=staticmethod(function),
+ __doc__=format_function_help(function),
+ __repr__=display_help,
+ ),
+ )()
diff --git a/pw_rpc/py/pw_rpc/console_tools/watchdog.py b/pw_rpc/py/pw_rpc/console_tools/watchdog.py
index c4b9e3dc4..8fa1cfce6 100644
--- a/pw_rpc/py/pw_rpc/console_tools/watchdog.py
+++ b/pw_rpc/py/pw_rpc/console_tools/watchdog.py
@@ -14,7 +14,7 @@
"""Simple watchdog class."""
import threading
-from typing import Any, Callable
+from typing import Any, Callable, Optional
class Watchdog:
@@ -23,12 +23,15 @@ class Watchdog:
This class could be used, for example, to track a device's connection state
for devices that send a periodic heartbeat packet.
"""
- def __init__(self,
- on_reset: Callable[[], Any],
- on_expiration: Callable[[], Any],
- while_expired: Callable[[], Any] = lambda: None,
- timeout_s: float = 1,
- expired_timeout_s: float = None):
+
+ def __init__(
+ self,
+ on_reset: Callable[[], Any],
+ on_expiration: Callable[[], Any],
+ while_expired: Callable[[], Any] = lambda: None,
+ timeout_s: float = 1,
+ expired_timeout_s: Optional[float] = None,
+ ):
"""Creates a watchdog; start() must be called to start it.
Args:
@@ -61,7 +64,8 @@ class Watchdog:
self._watchdog.cancel()
self._watchdog = threading.Timer(
self.expired_timeout_s if self.expired else self.timeout_s,
- self._timeout_expired)
+ self._timeout_expired,
+ )
self._watchdog.daemon = True
self._watchdog.start()
diff --git a/pw_rpc/py/pw_rpc/descriptors.py b/pw_rpc/py/pw_rpc/descriptors.py
index 57ba98458..f3daf8787 100644
--- a/pw_rpc/py/pw_rpc/descriptors.py
+++ b/pw_rpc/py/pw_rpc/descriptors.py
@@ -17,12 +17,26 @@ import abc
from dataclasses import dataclass
import enum
from inspect import Parameter
-from typing import (Any, Callable, Collection, Dict, Generic, Iterable,
- Iterator, Optional, Tuple, TypeVar, Union)
+from typing import (
+ Any,
+ Callable,
+ Collection,
+ Dict,
+ Generic,
+ Iterable,
+ Iterator,
+ Optional,
+ Tuple,
+ TypeVar,
+ Union,
+)
from google.protobuf import descriptor_pb2, message_factory
-from google.protobuf.descriptor import (FieldDescriptor, MethodDescriptor,
- ServiceDescriptor)
+from google.protobuf.descriptor import (
+ FieldDescriptor,
+ MethodDescriptor,
+ ServiceDescriptor,
+)
from google.protobuf.message import Message
from pw_protobuf_compiler import python_protos
@@ -73,6 +87,7 @@ class ChannelManipulator(abc.ABC):
# Create a RPC client.
client = HdlcRpcClient(socket.read, protos, channels, stdout)
"""
+
def __init__(self):
self.send_packet: Callable[[bytes], Any] = lambda _: None
@@ -92,6 +107,7 @@ class ChannelManipulator(abc.ABC):
@dataclass(frozen=True, eq=False)
class Service:
"""Describes an RPC service."""
+
_descriptor: ServiceDescriptor
id: int
methods: 'Methods'
@@ -110,13 +126,19 @@ class Service:
@classmethod
def from_descriptor(cls, descriptor: ServiceDescriptor) -> 'Service':
- service = cls(descriptor, ids.calculate(descriptor.full_name),
- None) # type: ignore[arg-type]
+ service = cls(
+ descriptor,
+ ids.calculate(descriptor.full_name),
+ None, # type: ignore[arg-type]
+ )
object.__setattr__(
- service, 'methods',
+ service,
+ 'methods',
Methods(
Method.from_descriptor(method_descriptor, service)
- for method_descriptor in descriptor.methods))
+ for method_descriptor in descriptor.methods
+ ),
+ )
return service
@@ -136,7 +158,9 @@ def _streaming_attributes(method) -> Tuple[bool, bool]:
file_pb = descriptor_pb2.FileDescriptorProto()
file_pb.MergeFromString(service.file.serialized_pb)
- method_pb = file_pb.service[service.index].method[method.index] # pylint: disable=no-member
+ method_pb = file_pb.service[service.index].method[
+ method.index
+ ] # pylint: disable=no-member
return method_pb.server_streaming, method_pb.client_streaming
@@ -167,7 +191,8 @@ def _field_type_annotation(field: FieldDescriptor):
"""Creates a field type annotation to use in the help message only."""
if field.type == FieldDescriptor.TYPE_MESSAGE:
annotation = message_factory.MessageFactory(
- field.message_type.file.pool).GetPrototype(field.message_type)
+ field.message_type.file.pool
+ ).GetPrototype(field.message_type)
else:
annotation = _PROTO_FIELD_TYPES.get(field.type, Parameter.empty)
@@ -200,8 +225,10 @@ def _message_is_type(proto, expected_type) -> bool:
# new, unique generated proto class. Any generated classes for a particular
# proto message share the same MessageDescriptor instance and are
# interchangeable, so check the descriptors in addition to the types.
- return isinstance(proto, expected_type) or (isinstance(
- proto, Message) and proto.DESCRIPTOR is expected_type.DESCRIPTOR)
+ return isinstance(proto, expected_type) or (
+ isinstance(proto, Message)
+ and proto.DESCRIPTOR is expected_type.DESCRIPTOR
+ )
@dataclass(frozen=True, eq=False)
@@ -219,9 +246,11 @@ class Method:
@classmethod
def from_descriptor(cls, descriptor: MethodDescriptor, service: Service):
input_factory = message_factory.MessageFactory(
- descriptor.input_type.file.pool)
+ descriptor.input_type.file.pool
+ )
output_factory = message_factory.MessageFactory(
- descriptor.output_type.file.pool)
+ descriptor.output_type.file.pool
+ )
return Method(
descriptor,
service,
@@ -238,7 +267,9 @@ class Method:
BIDIRECTIONAL_STREAMING = 3
def sentence_name(self) -> str:
- return self.name.lower().replace('_', ' ') # pylint: disable=no-member
+ return self.name.lower().replace(
+ '_', ' '
+ ) # pylint: disable=no-member
@property
def name(self) -> str:
@@ -265,8 +296,9 @@ class Method:
return self.Type.UNARY
- def get_request(self, proto: Optional[Message],
- proto_kwargs: Optional[Dict[str, Any]]) -> Message:
+ def get_request(
+ self, proto: Optional[Message], proto_kwargs: Optional[Dict[str, Any]]
+ ) -> Message:
"""Returns a request_type protobuf message.
The client implementation may use this to support providing a request
@@ -281,7 +313,8 @@ class Method:
raise TypeError(
'Requests must be provided either as a message object or a '
'series of keyword args, but both were provided '
- f"({proto_str} and {proto_kwargs!r})")
+ f"({proto_str} and {proto_kwargs!r})"
+ )
if proto is None:
return self.request_type(**proto_kwargs)
@@ -292,9 +325,11 @@ class Method:
except AttributeError:
bad_type = type(proto).__name__
- raise TypeError(f'Expected a message of type '
- f'{self.request_type.DESCRIPTOR.full_name}, '
- f'got {bad_type}')
+ raise TypeError(
+ f'Expected a message of type '
+ f'{self.request_type.DESCRIPTOR.full_name}, '
+ f'got {bad_type}'
+ )
return proto
@@ -304,10 +339,12 @@ class Method:
This can be used to make function signatures match the request proto.
"""
for field in self.request_type.DESCRIPTOR.fields:
- yield Parameter(field.name,
- Parameter.KEYWORD_ONLY,
- annotation=_field_type_annotation(field),
- default=field.default_value)
+ yield Parameter(
+ field.name,
+ Parameter.KEYWORD_ONLY,
+ annotation=_field_type_annotation(field),
+ default=field.default_value,
+ )
def __repr__(self) -> str:
req = self._method_parameter(self.request_type, self.client_streaming)
@@ -336,12 +373,14 @@ def _name(item: Union[Service, Method]) -> str:
class _AccessByName(Generic[T]):
"""Wrapper for accessing types by name within a proto package structure."""
+
def __init__(self, name: str, item: T):
setattr(self, name, item)
class ServiceAccessor(Collection[T]):
"""Navigates RPC services by name or ID."""
+
def __init__(self, members, as_attrs: str = ''):
"""Creates accessor from an {item: value} dict or [values] iterable."""
# If the members arg was passed as a [values] iterable, convert it to
@@ -357,8 +396,8 @@ class ServiceAccessor(Collection[T]):
setattr(self, name, member)
elif as_attrs == 'packages':
for package in python_protos.as_packages(
- (m.package, _AccessByName(m.name, members[m]))
- for m in members).packages:
+ (m.package, _AccessByName(m.name, members[m])) for m in members
+ ).packages:
setattr(self, str(package), package)
elif as_attrs:
raise ValueError(f'Unexpected value {as_attrs!r} for as_attrs')
@@ -393,12 +432,14 @@ def _id(handle: Union[str, int]) -> int:
class Methods(ServiceAccessor[Method]):
"""A collection of Method descriptors in a Service."""
+
def __init__(self, method: Iterable[Method]):
super().__init__(method)
class Services(ServiceAccessor[Service]):
"""A collection of Service descriptors."""
+
def __init__(self, services: Iterable[Service]):
super().__init__(services)
diff --git a/pw_rpc/py/pw_rpc/lossy_channel.py b/pw_rpc/py/pw_rpc/lossy_channel.py
new file mode 100644
index 000000000..db8908a07
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/lossy_channel.py
@@ -0,0 +1,253 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""A channel adapter layer that introduces lossy behavior to a channel."""
+
+import abc
+import collections
+import copy
+import logging
+from typing import Callable, Deque, Optional, Tuple
+import random
+import time
+
+import pw_rpc
+
+_LOG = logging.getLogger(__name__)
+
+
+# TODO(amontanez): The surface are of this API could be reduced significantly
+# with a few changes to LossyChannel.
+class LossController(abc.ABC):
+ """Interface for driving loss/corruption decisions of a LossyChannel."""
+
+ @abc.abstractmethod
+ def next_packet_duplicated(self) -> bool:
+ """Returns true if the next packet should be duplicated."""
+
+ @abc.abstractmethod
+ def next_packet_out_of_order(self) -> bool:
+ """Returns true if the next packet should be a re-ordered packet."""
+
+ @abc.abstractmethod
+ def next_packet_delayed(self) -> bool:
+ """Returns true if a delay should occur before sending a packet."""
+
+ @abc.abstractmethod
+ def next_packet_dropped(self) -> bool:
+ """Returns true if the next incoming packet should be dropped."""
+
+ @abc.abstractmethod
+ def next_packet_delay(self) -> int:
+ """The delay before sending the next packet, in milliseconds."""
+
+ @abc.abstractmethod
+ def next_num_dupes(self) -> int:
+ """Returns how many times the next packet should be duplicated."""
+
+ @abc.abstractmethod
+ def choose_out_of_order_packet(self, max_idx) -> int:
+ """Returns the index of the next reordered packet.
+
+ A return value of 0 represents the newest packet, while max_idx
+ represents the oldest packet.
+ """
+
+
+class ManualPacketFilter(LossController):
+ """Determines if a packet should be kept or dropped for testing purposes."""
+
+ _Action = Callable[[int], Tuple[bool, bool]]
+ _KEEP = lambda _: (True, False)
+ _DROP = lambda _: (False, False)
+
+ def __init__(self) -> None:
+ self.packet_count = 0
+ self._actions: Deque[ManualPacketFilter._Action] = collections.deque()
+
+ def reset(self) -> None:
+ self.packet_count = 0
+ self._actions.clear()
+
+ def keep(self, count: int) -> None:
+ """Keeps the next count packets."""
+ self._actions.extend(ManualPacketFilter._KEEP for _ in range(count))
+
+ def drop(self, count: int) -> None:
+ """Drops the next count packets."""
+ self._actions.extend(ManualPacketFilter._DROP for _ in range(count))
+
+ def drop_every(self, every: int) -> None:
+ """Drops every Nth packet forever."""
+ self._actions.append(lambda count: (count % every != 0, True))
+
+ def randomly_drop(self, one_in: int, gen: random.Random) -> None:
+ """Drops packets randomly forever."""
+ self._actions.append(lambda _: (gen.randrange(one_in) != 0, True))
+
+ def keep_packet(self) -> bool:
+ """Returns whether the provided packet should be kept or dropped."""
+ self.packet_count += 1
+
+ if not self._actions:
+ return True
+
+ keep, repeat = self._actions[0](self.packet_count)
+
+ if not repeat:
+ self._actions.popleft()
+
+ return keep
+
+ def next_packet_duplicated(self) -> bool:
+ return False
+
+ def next_packet_out_of_order(self) -> bool:
+ return False
+
+ def next_packet_delayed(self) -> bool:
+ return False
+
+ def next_packet_dropped(self) -> bool:
+ return not self.keep_packet()
+
+ def next_packet_delay(self) -> int:
+ return 0
+
+ def next_num_dupes(self) -> int:
+ return 0
+
+ def choose_out_of_order_packet(self, max_idx) -> int:
+ return 0
+
+
+class RandomLossGenerator(LossController):
+ """Parametrized random number generator that drives a LossyChannel."""
+
+ def __init__(
+ self,
+ duplicated_packet_probability: float,
+ max_duplications_per_packet: int,
+ out_of_order_probability: float,
+ delayed_packet_probability: float,
+ delayed_packet_range_ms: Tuple[int, int],
+ dropped_packet_probability: float,
+ seed: Optional[int] = None,
+ ):
+ self.duplicated_packet_probability = duplicated_packet_probability
+ self.max_duplications_per_packet = max_duplications_per_packet
+ self.out_of_order_probability = out_of_order_probability
+ self.delayed_packet_probability = delayed_packet_probability
+ self.delayed_packet_range_ms = delayed_packet_range_ms
+ self.dropped_packet_probability = dropped_packet_probability
+ self._rng = random.Random(seed)
+
+ def next_packet_duplicated(self) -> bool:
+ return self.duplicated_packet_probability > self._rng.uniform(0.0, 1.0)
+
+ def next_packet_out_of_order(self) -> bool:
+ return self.out_of_order_probability > self._rng.uniform(0.0, 1.0)
+
+ def next_packet_delayed(self) -> bool:
+ return self.delayed_packet_probability > self._rng.uniform(0.0, 1.0)
+
+ def next_packet_dropped(self) -> bool:
+ return self.dropped_packet_probability > self._rng.uniform(0.0, 1.0)
+
+ def next_packet_delay(self) -> int:
+ return self._rng.randint(*self.delayed_packet_range_ms)
+
+ def next_num_dupes(self) -> int:
+ return self._rng.randint(1, self.max_duplications_per_packet)
+
+ def choose_out_of_order_packet(self, max_idx) -> int:
+ return self._rng.randint(0, max_idx)
+
+
+class LossyChannel(pw_rpc.ChannelManipulator):
+ """Introduces lossy behaviors into a channel."""
+
+ class _Packet:
+ """Container class to keep track of incoming packet sequence number."""
+
+ def __init__(self, sequence_number: int, payload: bytes):
+ self.sequence_number = sequence_number
+ self.payload = payload
+
+ def __init__(
+ self, name, loss_generator: LossController, max_num_old_packets=24
+ ):
+ super().__init__()
+ self.name = name
+ self._packets: Deque[LossyChannel._Packet] = collections.deque()
+ self._old_packets: Deque[LossyChannel._Packet] = collections.deque()
+ self._max_old_packet_window_size = max_num_old_packets
+ self.unique_packet_count = 0
+ self._rng = loss_generator
+
+ def _enqueue_old_packet(self, packet: _Packet):
+ if len(self._old_packets) >= self._max_old_packet_window_size:
+ self._old_packets.popleft()
+ self._old_packets.append(packet)
+
+ def _enqueue_packet(self, payload: bytes):
+ # Generate duplicate packets on ingress.
+ packet = self._Packet(self.unique_packet_count, payload)
+ self.unique_packet_count += 1
+
+ self._packets.append(packet)
+ self._enqueue_old_packet(packet)
+ if self._rng.next_packet_duplicated():
+ num_dupes = self._rng.next_num_dupes()
+ _LOG.debug('[%s] Duplicating packet %d times', self.name, num_dupes)
+ for _ in range(num_dupes):
+ self._packets.append(packet)
+
+ def _send_packets(self):
+ while self._packets:
+ packet = None
+
+ if self._rng.next_packet_out_of_order():
+ idx = self._rng.choose_out_of_order_packet(
+ len(self._old_packets) - 1
+ )
+ _LOG.debug(
+ '[%s] Selecting out of order packet at index %d',
+ self.name,
+ idx,
+ )
+ packet = copy.copy(self._old_packets[idx])
+ del self._old_packets[idx]
+ else:
+ packet = self._packets.popleft()
+
+ if self._rng.next_packet_delayed():
+ delay = self._rng.next_packet_delay()
+ _LOG.debug('[%s] Delaying channel by %d ms', self.name, delay)
+ time.sleep(delay / 1000)
+
+ action_msg = 'Dropped'
+ if not self._rng.next_packet_dropped():
+ action_msg = 'Sent'
+ self.send_packet(packet.payload)
+ _LOG.debug(
+ '[%s] %s packet #%d: %s',
+ self.name,
+ action_msg,
+ packet.sequence_number,
+ str(packet.payload),
+ )
+
+ def process_and_send(self, packet: bytes):
+ self._enqueue_packet(packet)
+ self._send_packets()
diff --git a/pw_rpc/py/pw_rpc/packets.py b/pw_rpc/py/pw_rpc/packets.py
index d3e25b78b..ddcc03e74 100644
--- a/pw_rpc/py/pw_rpc/packets.py
+++ b/pw_rpc/py/pw_rpc/packets.py
@@ -41,11 +41,13 @@ def encode_request(rpc: tuple, request: Optional[message.Message]) -> bytes:
channel, service, method = _ids(rpc)
payload = request.SerializeToString() if request is not None else bytes()
- return packet_pb2.RpcPacket(type=packet_pb2.PacketType.REQUEST,
- channel_id=channel,
- service_id=service,
- method_id=method,
- payload=payload).SerializeToString()
+ return packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.REQUEST,
+ channel_id=channel,
+ service_id=service,
+ method_id=method,
+ payload=payload,
+ ).SerializeToString()
def encode_response(rpc: tuple, response: message.Message) -> bytes:
@@ -56,7 +58,8 @@ def encode_response(rpc: tuple, response: message.Message) -> bytes:
channel_id=channel,
service_id=service,
method_id=method,
- payload=response.SerializeToString()).SerializeToString()
+ payload=response.SerializeToString(),
+ ).SerializeToString()
def encode_client_stream(rpc: tuple, request: message.Message) -> bytes:
@@ -67,33 +70,40 @@ def encode_client_stream(rpc: tuple, request: message.Message) -> bytes:
channel_id=channel,
service_id=service,
method_id=method,
- payload=request.SerializeToString()).SerializeToString()
+ payload=request.SerializeToString(),
+ ).SerializeToString()
def encode_client_error(packet: packet_pb2.RpcPacket, status: Status) -> bytes:
- return packet_pb2.RpcPacket(type=packet_pb2.PacketType.CLIENT_ERROR,
- channel_id=packet.channel_id,
- service_id=packet.service_id,
- method_id=packet.method_id,
- status=status.value).SerializeToString()
+ return packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.CLIENT_ERROR,
+ channel_id=packet.channel_id,
+ service_id=packet.service_id,
+ method_id=packet.method_id,
+ status=status.value,
+ ).SerializeToString()
def encode_cancel(rpc: tuple) -> bytes:
channel, service, method = _ids(rpc)
- return packet_pb2.RpcPacket(type=packet_pb2.PacketType.CLIENT_ERROR,
- status=Status.CANCELLED.value,
- channel_id=channel,
- service_id=service,
- method_id=method).SerializeToString()
+ return packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.CLIENT_ERROR,
+ status=Status.CANCELLED.value,
+ channel_id=channel,
+ service_id=service,
+ method_id=method,
+ ).SerializeToString()
def encode_client_stream_end(rpc: tuple) -> bytes:
channel, service, method = _ids(rpc)
- return packet_pb2.RpcPacket(type=packet_pb2.PacketType.CLIENT_STREAM_END,
- channel_id=channel,
- service_id=service,
- method_id=method).SerializeToString()
+ return packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.CLIENT_STREAM_END,
+ channel_id=channel,
+ service_id=service,
+ method_id=method,
+ ).SerializeToString()
def for_server(packet: packet_pb2.RpcPacket) -> bool:
diff --git a/pw_rpc/py/pw_rpc/plugin.py b/pw_rpc/py/pw_rpc/plugin.py
index 30d616045..62f8bac01 100644
--- a/pw_rpc/py/pw_rpc/plugin.py
+++ b/pw_rpc/py/pw_rpc/plugin.py
@@ -19,17 +19,21 @@ import sys
from google.protobuf.compiler import plugin_pb2
from pw_rpc import codegen_nanopb
+from pw_rpc import codegen_pwpb
from pw_rpc import codegen_raw
class Codegen(enum.Enum):
RAW = 0
NANOPB = 1
+ PWPB = 2
-def process_proto_request(codegen: Codegen,
- req: plugin_pb2.CodeGeneratorRequest,
- res: plugin_pb2.CodeGeneratorResponse) -> None:
+def process_proto_request(
+ codegen: Codegen,
+ req: plugin_pb2.CodeGeneratorRequest,
+ res: plugin_pb2.CodeGeneratorResponse,
+) -> None:
"""Handles a protoc CodeGeneratorRequest message.
Generates code for the files in the request and writes the output to the
@@ -44,6 +48,8 @@ def process_proto_request(codegen: Codegen,
output_files = codegen_raw.process_proto_file(proto_file)
elif codegen is Codegen.NANOPB:
output_files = codegen_nanopb.process_proto_file(proto_file)
+ elif codegen is Codegen.PWPB:
+ output_files = codegen_pwpb.process_proto_file(proto_file)
else:
raise NotImplementedError(f'Unknown codegen type {codegen}')
@@ -67,7 +73,8 @@ def main(codegen: Codegen) -> int:
# Declare that this plugin supports optional fields in proto3. No proto
# message code is generated, so optional in proto3 is supported trivially.
response.supported_features |= ( # type: ignore[attr-defined]
- response.FEATURE_PROTO3_OPTIONAL) # type: ignore[attr-defined]
+ response.FEATURE_PROTO3_OPTIONAL
+ ) # type: ignore[attr-defined]
sys.stdout.buffer.write(response.SerializeToString())
return 0
diff --git a/pw_rpc/py/pw_rpc/plugin_pwpb.py b/pw_rpc/py/pw_rpc/plugin_pwpb.py
new file mode 100755
index 000000000..a99163d66
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/plugin_pwpb.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_rpc pw_protobuf protoc plugin."""
+
+import sys
+
+from pw_rpc import plugin
+
+
+def main() -> int:
+ return plugin.main(plugin.Codegen.PWPB)
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_rpc/py/pw_rpc/testing.py b/pw_rpc/py/pw_rpc/testing.py
index a9ab4d373..de8ab6223 100644
--- a/pw_rpc/py/pw_rpc/testing.py
+++ b/pw_rpc/py/pw_rpc/testing.py
@@ -14,6 +14,7 @@
"""Utilities for testing pw_rpc."""
import argparse
+import shlex
import subprocess
import sys
import tempfile
@@ -24,25 +25,34 @@ TEMP_DIR_MARKER = '(pw_rpc:CREATE_TEMP_DIR)'
def parse_test_server_args(
- parser: argparse.ArgumentParser = None) -> argparse.Namespace:
+ parser: Optional[argparse.ArgumentParser] = None,
+) -> argparse.Namespace:
"""Parses arguments for running a Python-based integration test."""
if parser is None:
parser = argparse.ArgumentParser(
- description=sys.modules['__main__'].__doc__)
+ description=sys.modules['__main__'].__doc__
+ )
- parser.add_argument('--test-server-command',
- nargs='+',
- required=True,
- help='Command that starts the test server.')
+ parser.add_argument(
+ '--test-server-command',
+ nargs='+',
+ required=True,
+ help='Command that starts the test server.',
+ )
parser.add_argument(
'--port',
type=int,
required=True,
- help=('The port to use to connect to the test server. This value is '
- 'passed to the test server as the last argument.'))
- parser.add_argument('unittest_args',
- nargs=argparse.REMAINDER,
- help='Arguments after "--" are passed to unittest.')
+ help=(
+ 'The port to use to connect to the test server. This value is '
+ 'passed to the test server as the last argument.'
+ ),
+ )
+ parser.add_argument(
+ 'unittest_args',
+ nargs=argparse.REMAINDER,
+ help='Arguments after "--" are passed to unittest.',
+ )
args = parser.parse_args()
@@ -57,15 +67,33 @@ def parse_test_server_args(
def _parse_subprocess_integration_test_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
- description='Executes a test between two subprocesses')
- parser.add_argument('--client', required=True, help='Client binary to run')
- parser.add_argument('--server', required=True, help='Server binary to run')
+ description='Executes a test between two subprocesses'
+ )
+ parser.add_argument(
+ '--client',
+ required=True,
+ help=(
+ 'Client command to run. '
+ 'Use quotes and whitespace to pass client-specifc arguments.'
+ ),
+ )
+ parser.add_argument(
+ '--server',
+ required=True,
+ help=(
+ 'Server command to run. '
+ 'Use quotes and whitespace to pass client-specifc arguments.'
+ ),
+ )
parser.add_argument(
'common_args',
metavar='-- ...',
nargs=argparse.REMAINDER,
- help=('Arguments to pass to both the server and client; '
- f'pass {TEMP_DIR_MARKER} to generate a temporary directory'))
+ help=(
+ 'Arguments to pass to both the server and client; '
+ f'pass {TEMP_DIR_MARKER} to generate a temporary directory'
+ ),
+ )
args = parser.parse_args()
@@ -77,10 +105,13 @@ def _parse_subprocess_integration_test_args() -> argparse.Namespace:
return args
-def execute_integration_test(server: str,
- client: str,
- common_args: Sequence[str],
- setup_time_s: float = 0.2) -> int:
+def execute_integration_test(
+ server: str,
+ client: str,
+ common_args: Sequence[str],
+ setup_time_s: float = 0.2,
+) -> int:
+ """Runs an RPC server and client as part of an integration test."""
temp_dir: Optional[tempfile.TemporaryDirectory] = None
if TEMP_DIR_MARKER in common_args:
@@ -90,11 +121,17 @@ def execute_integration_test(server: str,
]
try:
- server_process = subprocess.Popen([server, *common_args])
- # TODO(pwbug/508): Replace this delay with some sort of IPC.
+ server_cmdline = shlex.split(server)
+ client_cmdline = shlex.split(client)
+ if common_args:
+ server_cmdline += [*common_args]
+ client_cmdline += [*common_args]
+
+ server_process = subprocess.Popen(server_cmdline)
+ # TODO(b/234879791): Replace this delay with some sort of IPC.
time.sleep(setup_time_s)
- result = subprocess.run([client, *common_args]).returncode
+ result = subprocess.run(client_cmdline).returncode
server_process.terminate()
server_process.communicate()
@@ -108,4 +145,6 @@ def execute_integration_test(server: str,
if __name__ == '__main__':
sys.exit(
execute_integration_test(
- **vars(_parse_subprocess_integration_test_args())))
+ **vars(_parse_subprocess_integration_test_args())
+ )
+ )
diff --git a/pw_rpc/py/tests/callback_client_test.py b/pw_rpc/py/tests/callback_client_test.py
index b06939f25..406182983 100755
--- a/pw_rpc/py/tests/callback_client_test.py
+++ b/pw_rpc/py/tests/callback_client_test.py
@@ -59,13 +59,16 @@ def _message_bytes(msg) -> bytes:
class _CallbackClientImplTestBase(unittest.TestCase):
"""Supports writing tests that require responses from an RPC server."""
+
def setUp(self) -> None:
self._protos = python_protos.Library.from_strings(TEST_PROTO_1)
self._request = self._protos.packages.pw.test1.SomeMessage
self._client = client.Client.from_modules(
- callback_client.Impl(), [client.Channel(1, self._handle_packet)],
- self._protos.modules())
+ callback_client.Impl(),
+ [client.Channel(1, self._handle_packet)],
+ self._protos.modules(),
+ )
self._service = self._client.channel(1).rpcs.pw.test1.PublicService
self.requests: List[packet_pb2.RpcPacket] = []
@@ -78,14 +81,16 @@ class _CallbackClientImplTestBase(unittest.TestCase):
assert self.requests
return self.requests[-1]
- def _enqueue_response(self,
- channel_id: int,
- method=None,
- status: Status = Status.OK,
- payload=b'',
- *,
- ids: Tuple[int, int] = None,
- process_status=Status.OK) -> None:
+ def _enqueue_response(
+ self,
+ channel_id: int,
+ method=None,
+ status: Status = Status.OK,
+ payload=b'',
+ *,
+ ids: Optional[Tuple[int, int]] = None,
+ process_status=Status.OK,
+ ) -> None:
if method:
assert ids is None
service_id, method_id = method.service.id, method.id
@@ -93,40 +98,58 @@ class _CallbackClientImplTestBase(unittest.TestCase):
assert ids is not None and method is None
service_id, method_id = ids
- self._next_packets.append((packet_pb2.RpcPacket(
- type=packet_pb2.PacketType.RESPONSE,
- channel_id=channel_id,
- service_id=service_id,
- method_id=method_id,
- status=status.value,
- payload=_message_bytes(payload)).SerializeToString(),
- process_status))
-
- def _enqueue_server_stream(self,
- channel_id: int,
- method,
- response,
- process_status=Status.OK) -> None:
- self._next_packets.append((packet_pb2.RpcPacket(
- type=packet_pb2.PacketType.SERVER_STREAM,
- channel_id=channel_id,
- service_id=method.service.id,
- method_id=method.id,
- payload=_message_bytes(response)).SerializeToString(),
- process_status))
-
- def _enqueue_error(self,
- channel_id: int,
- service,
- method,
- status: Status,
- process_status=Status.OK) -> None:
- self._next_packets.append((packet_pb2.RpcPacket(
- type=packet_pb2.PacketType.SERVER_ERROR,
- channel_id=channel_id,
- service_id=service if isinstance(service, int) else service.id,
- method_id=method if isinstance(method, int) else method.id,
- status=status.value).SerializeToString(), process_status))
+ self._next_packets.append(
+ (
+ packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.RESPONSE,
+ channel_id=channel_id,
+ service_id=service_id,
+ method_id=method_id,
+ status=status.value,
+ payload=_message_bytes(payload),
+ ).SerializeToString(),
+ process_status,
+ )
+ )
+
+ def _enqueue_server_stream(
+ self, channel_id: int, method, response, process_status=Status.OK
+ ) -> None:
+ self._next_packets.append(
+ (
+ packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.SERVER_STREAM,
+ channel_id=channel_id,
+ service_id=method.service.id,
+ method_id=method.id,
+ payload=_message_bytes(response),
+ ).SerializeToString(),
+ process_status,
+ )
+ )
+
+ def _enqueue_error(
+ self,
+ channel_id: int,
+ service,
+ method,
+ status: Status,
+ process_status=Status.OK,
+ ) -> None:
+ self._next_packets.append(
+ (
+ packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.SERVER_ERROR,
+ channel_id=channel_id,
+ service_id=service
+ if isinstance(service, int)
+ else service.id,
+ method_id=method if isinstance(method, int) else method.id,
+ status=status.value,
+ ).SerializeToString(),
+ process_status,
+ )
+ )
def _handle_packet(self, data: bytes) -> None:
if self.output_exception:
@@ -160,6 +183,7 @@ class _CallbackClientImplTestBase(unittest.TestCase):
class CallbackClientImplTest(_CallbackClientImplTestBase):
"""Tests the callback_client.Impl client implementation."""
+
def test_callback_exceptions_suppressed(self) -> None:
stub = self._service.SomeUnary
@@ -167,8 +191,9 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
exception_msg = 'YOU BROKE IT O-]-<'
with self.assertLogs(callback_client.__package__, 'ERROR') as logs:
- stub.invoke(self._request(),
- mock.Mock(side_effect=Exception(exception_msg)))
+ stub.invoke(
+ self._request(), mock.Mock(side_effect=Exception(exception_msg))
+ )
self.assertIn(exception_msg, ''.join(logs.output))
@@ -184,18 +209,19 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
# Unknown channel
self._enqueue_response(999, method, process_status=Status.NOT_FOUND)
# Bad service
- self._enqueue_response(1,
- ids=(999, method.id),
- process_status=Status.OK)
+ self._enqueue_response(
+ 1, ids=(999, method.id), process_status=Status.OK
+ )
# Bad method
- self._enqueue_response(1,
- ids=(service_id, 999),
- process_status=Status.OK)
+ self._enqueue_response(
+ 1, ids=(service_id, 999), process_status=Status.OK
+ )
# For RPC not pending (is Status.OK because the packet is processed)
- self._enqueue_response(1,
- ids=(service_id,
- self._service.SomeBidiStreaming.method.id),
- process_status=Status.OK)
+ self._enqueue_response(
+ 1,
+ ids=(service_id, self._service.SomeBidiStreaming.method.id),
+ process_status=Status.OK,
+ )
self._enqueue_response(1, method, process_status=Status.OK)
@@ -208,19 +234,24 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
service_id = method.service.id
# Unknown channel
- self._enqueue_error(999,
- service_id,
- method,
- Status.NOT_FOUND,
- process_status=Status.NOT_FOUND)
+ self._enqueue_error(
+ 999,
+ service_id,
+ method,
+ Status.NOT_FOUND,
+ process_status=Status.NOT_FOUND,
+ )
# Bad service
self._enqueue_error(1, 999, method.id, Status.INVALID_ARGUMENT)
# Bad method
self._enqueue_error(1, service_id, 999, Status.INVALID_ARGUMENT)
# For RPC not pending
- self._enqueue_error(1, service_id,
- self._service.SomeBidiStreaming.method.id,
- Status.NOT_FOUND)
+ self._enqueue_error(
+ 1,
+ service_id,
+ self._service.SomeBidiStreaming.method.id,
+ Status.NOT_FOUND,
+ )
self._process_enqueued_packets()
@@ -229,11 +260,9 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
def test_exception_if_payload_fails_to_decode(self) -> None:
method = self._service.SomeUnary.method
- self._enqueue_response(1,
- method,
- Status.OK,
- b'INVALID DATA!!!',
- process_status=Status.OK)
+ self._enqueue_response(
+ 1, method, Status.OK, b'INVALID DATA!!!', process_status=Status.OK
+ )
with self.assertRaises(callback_client.RpcError) as context:
self._service.SomeUnary(magic_number=6)
@@ -251,34 +280,44 @@ class CallbackClientImplTest(_CallbackClientImplTestBase):
self.assertEqual(impl.default_stream_timeout_s, 1.5)
def test_default_timeouts_set_for_all_rpcs(self) -> None:
- rpc_client = client.Client.from_modules(callback_client.Impl(
- 99, 100), [client.Channel(1, lambda *a, **b: None)],
- self._protos.modules())
+ rpc_client = client.Client.from_modules(
+ callback_client.Impl(99, 100),
+ [client.Channel(1, lambda *a, **b: None)],
+ self._protos.modules(),
+ )
rpcs = rpc_client.channel(1).rpcs
self.assertEqual(
- rpcs.pw.test1.PublicService.SomeUnary.default_timeout_s, 99)
+ rpcs.pw.test1.PublicService.SomeUnary.default_timeout_s, 99
+ )
self.assertEqual(
rpcs.pw.test1.PublicService.SomeServerStreaming.default_timeout_s,
- 100)
+ 100,
+ )
self.assertEqual(
rpcs.pw.test1.PublicService.SomeClientStreaming.default_timeout_s,
- 99)
+ 99,
+ )
self.assertEqual(
- rpcs.pw.test1.PublicService.SomeBidiStreaming.default_timeout_s,
- 100)
+ rpcs.pw.test1.PublicService.SomeBidiStreaming.default_timeout_s, 100
+ )
def test_rpc_provides_request_type(self) -> None:
- self.assertIs(self._service.SomeUnary.request,
- self._service.SomeUnary.method.request_type)
+ self.assertIs(
+ self._service.SomeUnary.request,
+ self._service.SomeUnary.method.request_type,
+ )
def test_rpc_provides_response_type(self) -> None:
- self.assertIs(self._service.SomeUnary.request,
- self._service.SomeUnary.method.request_type)
+ self.assertIs(
+ self._service.SomeUnary.request,
+ self._service.SomeUnary.method.request_type,
+ )
class UnaryTest(_CallbackClientImplTestBase):
"""Tests for invoking a unary RPC."""
+
def setUp(self) -> None:
super().setUp()
self.rpc = self._service.SomeUnary
@@ -286,64 +325,85 @@ class UnaryTest(_CallbackClientImplTestBase):
def test_blocking_call(self) -> None:
for _ in range(3):
- self._enqueue_response(1, self.method, Status.ABORTED,
- self.method.response_type(payload='0_o'))
+ self._enqueue_response(
+ 1,
+ self.method,
+ Status.ABORTED,
+ self.method.response_type(payload='0_o'),
+ )
status, response = self._service.SomeUnary(
- self.method.request_type(magic_number=6))
+ self.method.request_type(magic_number=6)
+ )
self.assertEqual(
- 6,
- self._sent_payload(self.method.request_type).magic_number)
+ 6, self._sent_payload(self.method.request_type).magic_number
+ )
self.assertIs(Status.ABORTED, status)
self.assertEqual('0_o', response.payload)
def test_nonblocking_call(self) -> None:
for _ in range(3):
- self._enqueue_response(1, self.method, Status.ABORTED,
- self.method.response_type(payload='0_o'))
+ self._enqueue_response(
+ 1,
+ self.method,
+ Status.ABORTED,
+ self.method.response_type(payload='0_o'),
+ )
callback = mock.Mock()
- call = self.rpc.invoke(self._request(magic_number=5), callback,
- callback)
+ call = self.rpc.invoke(
+ self._request(magic_number=5), callback, callback
+ )
- callback.assert_has_calls([
- mock.call(call, self.method.response_type(payload='0_o')),
- mock.call(call, Status.ABORTED)
- ])
+ callback.assert_has_calls(
+ [
+ mock.call(call, self.method.response_type(payload='0_o')),
+ mock.call(call, Status.ABORTED),
+ ]
+ )
self.assertEqual(
- 5,
- self._sent_payload(self.method.request_type).magic_number)
+ 5, self._sent_payload(self.method.request_type).magic_number
+ )
def test_open(self) -> None:
self.output_exception = IOError('something went wrong sending!')
for _ in range(3):
- self._enqueue_response(1, self.method, Status.ABORTED,
- self.method.response_type(payload='0_o'))
+ self._enqueue_response(
+ 1,
+ self.method,
+ Status.ABORTED,
+ self.method.response_type(payload='0_o'),
+ )
callback = mock.Mock()
- call = self.rpc.open(self._request(magic_number=5), callback,
- callback)
+ call = self.rpc.open(
+ self._request(magic_number=5), callback, callback
+ )
self.assertEqual(self.requests, [])
self._process_enqueued_packets()
- callback.assert_has_calls([
- mock.call(call, self.method.response_type(payload='0_o')),
- mock.call(call, Status.ABORTED)
- ])
+ callback.assert_has_calls(
+ [
+ mock.call(call, self.method.response_type(payload='0_o')),
+ mock.call(call, Status.ABORTED),
+ ]
+ )
def test_blocking_server_error(self) -> None:
for _ in range(3):
- self._enqueue_error(1, self.method.service, self.method,
- Status.NOT_FOUND)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.NOT_FOUND
+ )
with self.assertRaises(callback_client.RpcError) as context:
self._service.SomeUnary(
- self.method.request_type(magic_number=6))
+ self.method.request_type(magic_number=6)
+ )
self.assertIs(context.exception.status, Status.NOT_FOUND)
@@ -352,7 +412,8 @@ class UnaryTest(_CallbackClientImplTestBase):
for _ in range(3):
call = self._service.SomeUnary.invoke(
- self._request(magic_number=55), callback)
+ self._request(magic_number=55), callback
+ )
self.assertGreater(len(self.requests), 0)
self.requests.clear()
@@ -360,15 +421,18 @@ class UnaryTest(_CallbackClientImplTestBase):
self.assertTrue(call.cancel())
self.assertFalse(call.cancel()) # Already cancelled, returns False
- # Unary RPCs do not send a cancel request to the server.
- self.assertFalse(self.requests)
+ self.assertEqual(
+ self.last_request().type, packet_pb2.PacketType.CLIENT_ERROR
+ )
+ self.assertEqual(self.last_request().status, Status.CANCELLED.value)
callback.assert_not_called()
def test_nonblocking_with_request_args(self) -> None:
self.rpc.invoke(request_args=dict(magic_number=1138))
self.assertEqual(
- self._sent_payload(self.rpc.request).magic_number, 1138)
+ self._sent_payload(self.rpc.request).magic_number, 1138
+ )
def test_blocking_timeout_as_argument(self) -> None:
with self.assertRaises(callback_client.RpcTimeout):
@@ -401,9 +465,21 @@ class UnaryTest(_CallbackClientImplTestBase):
self.assertEqual(context.exception.__cause__, exception)
+ def test_unary_response(self) -> None:
+ proto = self._protos.packages.pw.test1.SomeMessage(magic_number=123)
+ self.assertEqual(
+ repr(callback_client.UnaryResponse(Status.ABORTED, proto)),
+ '(Status.ABORTED, pw.test1.SomeMessage(magic_number=123))',
+ )
+ self.assertEqual(
+ repr(callback_client.UnaryResponse(Status.OK, None)),
+ '(Status.OK, None)',
+ )
+
class ServerStreamingTest(_CallbackClientImplTestBase):
"""Tests for server streaming RPCs."""
+
def setUp(self) -> None:
super().setUp()
self.rpc = self._service.SomeServerStreaming
@@ -420,38 +496,12 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
self.assertEqual(
[rep1, rep2],
- self._service.SomeServerStreaming(magic_number=4).responses)
-
- self.assertEqual(
- 4,
- self._sent_payload(self.method.request_type).magic_number)
-
- def test_deprecated_packet_format(self) -> None:
- rep1 = self.method.response_type(payload='!!!')
- rep2 = self.method.response_type(payload='?')
-
- for _ in range(3):
- # The original packet format used RESPONSE packets for the server
- # stream and a SERVER_STREAM_END packet as the last packet. These
- # are converted to SERVER_STREAM packets followed by a RESPONSE.
- self._enqueue_response(1, self.method, payload=rep1)
- self._enqueue_response(1, self.method, payload=rep2)
-
- self._next_packets.append((packet_pb2.RpcPacket(
- type=packet_pb2.PacketType.DEPRECATED_SERVER_STREAM_END,
- channel_id=1,
- service_id=self.method.service.id,
- method_id=self.method.id,
- status=Status.INVALID_ARGUMENT.value).SerializeToString(),
- Status.OK))
-
- status, replies = self._service.SomeServerStreaming(magic_number=4)
- self.assertEqual([rep1, rep2], replies)
- self.assertIs(status, Status.INVALID_ARGUMENT)
+ self._service.SomeServerStreaming(magic_number=4).responses,
+ )
self.assertEqual(
- 4,
- self._sent_payload(self.method.request_type).magic_number)
+ 4, self._sent_payload(self.method.request_type).magic_number
+ )
def test_nonblocking_call(self) -> None:
rep1 = self.method.response_type(payload='!!!')
@@ -463,18 +513,21 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
self._enqueue_response(1, self.method, Status.ABORTED)
callback = mock.Mock()
- call = self.rpc.invoke(self._request(magic_number=3), callback,
- callback)
-
- callback.assert_has_calls([
- mock.call(call, self.method.response_type(payload='!!!')),
- mock.call(call, self.method.response_type(payload='?')),
- mock.call(call, Status.ABORTED),
- ])
+ call = self.rpc.invoke(
+ self._request(magic_number=3), callback, callback
+ )
+
+ callback.assert_has_calls(
+ [
+ mock.call(call, self.method.response_type(payload='!!!')),
+ mock.call(call, self.method.response_type(payload='?')),
+ mock.call(call, Status.ABORTED),
+ ]
+ )
self.assertEqual(
- 3,
- self._sent_payload(self.method.request_type).magic_number)
+ 3, self._sent_payload(self.method.request_type).magic_number
+ )
def test_open(self) -> None:
self.output_exception = IOError('something went wrong sending!')
@@ -487,17 +540,20 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
self._enqueue_response(1, self.method, Status.ABORTED)
callback = mock.Mock()
- call = self.rpc.open(self._request(magic_number=3), callback,
- callback)
+ call = self.rpc.open(
+ self._request(magic_number=3), callback, callback
+ )
self.assertEqual(self.requests, [])
self._process_enqueued_packets()
- callback.assert_has_calls([
- mock.call(call, self.method.response_type(payload='!!!')),
- mock.call(call, self.method.response_type(payload='?')),
- mock.call(call, Status.ABORTED),
- ])
+ callback.assert_has_calls(
+ [
+ mock.call(call, self.method.response_type(payload='!!!')),
+ mock.call(call, self.method.response_type(payload='?')),
+ mock.call(call, Status.ABORTED),
+ ]
+ )
def test_nonblocking_cancel(self) -> None:
resp = self.rpc.method.response_type(payload='!!!')
@@ -506,32 +562,38 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
callback = mock.Mock()
call = self.rpc.invoke(self._request(magic_number=3), callback)
callback.assert_called_once_with(
- call, self.rpc.method.response_type(payload='!!!'))
+ call, self.rpc.method.response_type(payload='!!!')
+ )
callback.reset_mock()
call.cancel()
- self.assertEqual(self.last_request().type,
- packet_pb2.PacketType.CLIENT_ERROR)
+ self.assertEqual(
+ self.last_request().type, packet_pb2.PacketType.CLIENT_ERROR
+ )
self.assertEqual(self.last_request().status, Status.CANCELLED.value)
# Ensure the RPC can be called after being cancelled.
self._enqueue_server_stream(1, self.method, resp)
self._enqueue_response(1, self.method, Status.OK)
- call = self.rpc.invoke(self._request(magic_number=3), callback,
- callback)
+ call = self.rpc.invoke(
+ self._request(magic_number=3), callback, callback
+ )
- callback.assert_has_calls([
- mock.call(call, self.method.response_type(payload='!!!')),
- mock.call(call, Status.OK),
- ])
+ callback.assert_has_calls(
+ [
+ mock.call(call, self.method.response_type(payload='!!!')),
+ mock.call(call, Status.OK),
+ ]
+ )
def test_nonblocking_with_request_args(self) -> None:
self.rpc.invoke(request_args=dict(magic_number=1138))
self.assertEqual(
- self._sent_payload(self.rpc.request).magic_number, 1138)
+ self._sent_payload(self.rpc.request).magic_number, 1138
+ )
def test_blocking_timeout(self) -> None:
with self.assertRaises(callback_client.RpcTimeout):
@@ -578,6 +640,7 @@ class ServerStreamingTest(_CallbackClientImplTestBase):
class ClientStreamingTest(_CallbackClientImplTestBase):
"""Tests for client streaming RPCs."""
+
def setUp(self) -> None:
super().setUp()
self.rpc = self._service.SomeClientStreaming
@@ -602,8 +665,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
requests = [self.method.request_type(magic_number=123)]
# Send after len(requests) and the client stream end packet.
- self._enqueue_error(1, self.method.service, self.method,
- Status.NOT_FOUND)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.NOT_FOUND
+ )
with self.assertRaises(callback_client.RpcError) as context:
self.rpc(requests)
@@ -619,22 +683,24 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
stream.send(magic_number=31)
- self.assertIs(packet_pb2.PacketType.CLIENT_STREAM,
- self.last_request().type)
+ self.assertIs(
+ packet_pb2.PacketType.CLIENT_STREAM, self.last_request().type
+ )
self.assertEqual(
- 31,
- self._sent_payload(self.method.request_type).magic_number)
+ 31, self._sent_payload(self.method.request_type).magic_number
+ )
self.assertFalse(stream.completed())
# Enqueue the server response to be sent after the next message.
self._enqueue_response(1, self.method, Status.OK, payload_1)
stream.send(magic_number=32)
- self.assertIs(packet_pb2.PacketType.CLIENT_STREAM,
- self.last_request().type)
+ self.assertIs(
+ packet_pb2.PacketType.CLIENT_STREAM, self.last_request().type
+ )
self.assertEqual(
- 32,
- self._sent_payload(self.method.request_type).magic_number)
+ 32, self._sent_payload(self.method.request_type).magic_number
+ )
self.assertTrue(stream.completed())
self.assertIs(Status.OK, stream.status)
@@ -654,10 +720,12 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self._process_enqueued_packets()
- callback.assert_has_calls([
- mock.call(call, payload),
- mock.call(call, Status.OK),
- ])
+ callback.assert_has_calls(
+ [
+ mock.call(call, payload),
+ mock.call(call, Status.OK),
+ ]
+ )
def test_nonblocking_finish(self) -> None:
"""Tests a client streaming RPC ended by the client."""
@@ -668,19 +736,22 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
stream.send(magic_number=37)
- self.assertIs(packet_pb2.PacketType.CLIENT_STREAM,
- self.last_request().type)
+ self.assertIs(
+ packet_pb2.PacketType.CLIENT_STREAM, self.last_request().type
+ )
self.assertEqual(
- 37,
- self._sent_payload(self.method.request_type).magic_number)
+ 37, self._sent_payload(self.method.request_type).magic_number
+ )
self.assertFalse(stream.completed())
# Enqueue the server response to be sent after the next message.
self._enqueue_response(1, self.method, Status.OK, payload_1)
stream.finish_and_wait()
- self.assertIs(packet_pb2.PacketType.CLIENT_STREAM_END,
- self.last_request().type)
+ self.assertIs(
+ packet_pb2.PacketType.CLIENT_STREAM_END,
+ self.last_request().type,
+ )
self.assertTrue(stream.completed())
self.assertIs(Status.OK, stream.status)
@@ -693,8 +764,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
stream.send(magic_number=37)
self.assertTrue(stream.cancel())
- self.assertIs(packet_pb2.PacketType.CLIENT_ERROR,
- self.last_request().type)
+ self.assertIs(
+ packet_pb2.PacketType.CLIENT_ERROR, self.last_request().type
+ )
self.assertIs(Status.CANCELLED.value, self.last_request().status)
self.assertFalse(stream.cancel())
@@ -705,8 +777,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
for _ in range(3):
stream = self._service.SomeClientStreaming.invoke()
- self._enqueue_error(1, self.method.service, self.method,
- Status.INVALID_ARGUMENT)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ )
stream.send(magic_number=2**32 - 1)
with self.assertRaises(callback_client.RpcError) as context:
@@ -719,8 +792,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
stream = self._service.SomeClientStreaming.invoke()
# Error will be sent in response to the CLIENT_STREAM_END packet.
- self._enqueue_error(1, self.method.service, self.method,
- Status.INVALID_ARGUMENT)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ )
with self.assertRaises(callback_client.RpcError) as context:
stream.finish_and_wait()
@@ -748,8 +822,9 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
self.assertEqual(result, call.finish_and_wait())
def test_nonblocking_finish_after_error(self) -> None:
- self._enqueue_error(1, self.method.service, self.method,
- Status.UNAVAILABLE)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.UNAVAILABLE
+ )
call = self.rpc.invoke()
@@ -773,6 +848,7 @@ class ClientStreamingTest(_CallbackClientImplTestBase):
class BidirectionalStreamingTest(_CallbackClientImplTestBase):
"""Tests for bidirectional streaming RPCs."""
+
def setUp(self) -> None:
super().setUp()
self.rpc = self._service.SomeBidiStreaming
@@ -796,8 +872,9 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
requests = [self.method.request_type(magic_number=123)]
# Send after len(requests) and the client stream end packet.
- self._enqueue_error(1, self.method.service, self.method,
- Status.NOT_FOUND)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.NOT_FOUND
+ )
with self.assertRaises(callback_client.RpcError) as context:
self.rpc(requests)
@@ -812,15 +889,17 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
for _ in range(3):
responses: list = []
stream = self._service.SomeBidiStreaming.invoke(
- lambda _, res, responses=responses: responses.append(res))
+ lambda _, res, responses=responses: responses.append(res)
+ )
self.assertFalse(stream.completed())
stream.send(magic_number=55)
- self.assertIs(packet_pb2.PacketType.CLIENT_STREAM,
- self.last_request().type)
+ self.assertIs(
+ packet_pb2.PacketType.CLIENT_STREAM, self.last_request().type
+ )
self.assertEqual(
- 55,
- self._sent_payload(self.method.request_type).magic_number)
+ 55, self._sent_payload(self.method.request_type).magic_number
+ )
self.assertFalse(stream.completed())
self.assertEqual([], responses)
@@ -828,11 +907,12 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self._enqueue_server_stream(1, self.method, rep2)
stream.send(magic_number=66)
- self.assertIs(packet_pb2.PacketType.CLIENT_STREAM,
- self.last_request().type)
+ self.assertIs(
+ packet_pb2.PacketType.CLIENT_STREAM, self.last_request().type
+ )
self.assertEqual(
- 66,
- self._sent_payload(self.method.request_type).magic_number)
+ 66, self._sent_payload(self.method.request_type).magic_number
+ )
self.assertFalse(stream.completed())
self.assertEqual([rep1, rep2], responses)
@@ -861,11 +941,13 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self._process_enqueued_packets()
- callback.assert_has_calls([
- mock.call(call, self.method.response_type(payload='!!!')),
- mock.call(call, self.method.response_type(payload='?')),
- mock.call(call, Status.OK),
- ])
+ callback.assert_has_calls(
+ [
+ mock.call(call, self.method.response_type(payload='!!!')),
+ mock.call(call, self.method.response_type(payload='?')),
+ mock.call(call, Status.OK),
+ ]
+ )
@mock.patch('pw_rpc.callback_client.call.Call._default_response')
def test_nonblocking(self, callback) -> None:
@@ -883,7 +965,8 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
for _ in range(3):
responses: list = []
stream = self._service.SomeBidiStreaming.invoke(
- lambda _, res, responses=responses: responses.append(res))
+ lambda _, res, responses=responses: responses.append(res)
+ )
self.assertFalse(stream.completed())
self._enqueue_server_stream(1, self.method, rep1)
@@ -892,8 +975,9 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self.assertFalse(stream.completed())
self.assertEqual([rep1], responses)
- self._enqueue_error(1, self.method.service, self.method,
- Status.OUT_OF_RANGE)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.OUT_OF_RANGE
+ )
stream.send(magic_number=99999)
self.assertTrue(stream.completed())
@@ -911,8 +995,9 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
stream = self._service.SomeBidiStreaming.invoke()
# Error will be sent in response to the CLIENT_STREAM_END packet.
- self._enqueue_error(1, self.method.service, self.method,
- Status.INVALID_ARGUMENT)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.INVALID_ARGUMENT
+ )
with self.assertRaises(callback_client.RpcError) as context:
stream.finish_and_wait()
@@ -943,8 +1028,9 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
def test_nonblocking_finish_after_error(self) -> None:
reply = self.method.response_type(payload='!?')
self._enqueue_server_stream(1, self.method, reply)
- self._enqueue_error(1, self.method.service, self.method,
- Status.UNAVAILABLE)
+ self._enqueue_error(
+ 1, self.method.service, self.method, Status.UNAVAILABLE
+ )
call = self.rpc.invoke()
@@ -965,6 +1051,18 @@ class BidirectionalStreamingTest(_CallbackClientImplTestBase):
self.assertIs(first_call.error, Status.CANCELLED)
self.assertFalse(second_call.completed())
+ def test_stream_response(self) -> None:
+ proto = self._protos.packages.pw.test1.SomeMessage(magic_number=123)
+ self.assertEqual(
+ repr(callback_client.StreamResponse(Status.ABORTED, [proto] * 2)),
+ '(Status.ABORTED, [pw.test1.SomeMessage(magic_number=123), '
+ 'pw.test1.SomeMessage(magic_number=123)])',
+ )
+ self.assertEqual(
+ repr(callback_client.StreamResponse(Status.OK, [])),
+ '(Status.OK, [])',
+ )
+
if __name__ == '__main__':
unittest.main()
diff --git a/pw_rpc/py/tests/client_test.py b/pw_rpc/py/tests/client_test.py
index e07125287..92a1f8236 100755
--- a/pw_rpc/py/tests/client_test.py
+++ b/pw_rpc/py/tests/client_test.py
@@ -78,46 +78,58 @@ def _test_setup(output=None):
protos = python_protos.Library.from_strings([TEST_PROTO_1, TEST_PROTO_2])
return protos, client.Client.from_modules(
callback_client.Impl(),
- [client.Channel(1, output),
- client.Channel(2, lambda _: None)], protos.modules())
+ [client.Channel(1, output), client.Channel(2, lambda _: None)],
+ protos.modules(),
+ )
class ChannelClientTest(unittest.TestCase):
"""Tests the ChannelClient."""
+
def setUp(self) -> None:
self._channel_client = _test_setup()[1].channel(1)
def test_access_service_client_as_attribute_or_index(self) -> None:
- self.assertIs(self._channel_client.rpcs.pw.test1.PublicService,
- self._channel_client.rpcs['pw.test1.PublicService'])
self.assertIs(
self._channel_client.rpcs.pw.test1.PublicService,
- self._channel_client.rpcs[pw_rpc.ids.calculate(
- 'pw.test1.PublicService')])
+ self._channel_client.rpcs['pw.test1.PublicService'],
+ )
+ self.assertIs(
+ self._channel_client.rpcs.pw.test1.PublicService,
+ self._channel_client.rpcs[
+ pw_rpc.ids.calculate('pw.test1.PublicService')
+ ],
+ )
def test_access_method_client_as_attribute_or_index(self) -> None:
- self.assertIs(self._channel_client.rpcs.pw.test2.Alpha.Unary,
- self._channel_client.rpcs['pw.test2.Alpha']['Unary'])
self.assertIs(
self._channel_client.rpcs.pw.test2.Alpha.Unary,
- self._channel_client.rpcs['pw.test2.Alpha'][pw_rpc.ids.calculate(
- 'Unary')])
+ self._channel_client.rpcs['pw.test2.Alpha']['Unary'],
+ )
+ self.assertIs(
+ self._channel_client.rpcs.pw.test2.Alpha.Unary,
+ self._channel_client.rpcs['pw.test2.Alpha'][
+ pw_rpc.ids.calculate('Unary')
+ ],
+ )
def test_service_name(self) -> None:
self.assertEqual(
- self._channel_client.rpcs.pw.test2.Alpha.Unary.service.name,
- 'Alpha')
+ self._channel_client.rpcs.pw.test2.Alpha.Unary.service.name, 'Alpha'
+ )
self.assertEqual(
self._channel_client.rpcs.pw.test2.Alpha.Unary.service.full_name,
- 'pw.test2.Alpha')
+ 'pw.test2.Alpha',
+ )
def test_method_name(self) -> None:
self.assertEqual(
- self._channel_client.rpcs.pw.test2.Alpha.Unary.method.name,
- 'Unary')
+ self._channel_client.rpcs.pw.test2.Alpha.Unary.method.name, 'Unary'
+ )
self.assertEqual(
self._channel_client.rpcs.pw.test2.Alpha.Unary.method.full_name,
- 'pw.test2.Alpha.Unary')
+ 'pw.test2.Alpha.Unary',
+ )
def test_iterate_over_all_methods(self) -> None:
channel_client = self._channel_client
@@ -133,8 +145,10 @@ class ChannelClientTest(unittest.TestCase):
def test_check_for_presence_of_services(self) -> None:
self.assertIn('pw.test1.PublicService', self._channel_client.rpcs)
- self.assertIn(pw_rpc.ids.calculate('pw.test1.PublicService'),
- self._channel_client.rpcs)
+ self.assertIn(
+ pw_rpc.ids.calculate('pw.test1.PublicService'),
+ self._channel_client.rpcs,
+ )
def test_check_for_presence_of_missing_services(self) -> None:
self.assertNotIn('PublicService', self._channel_client.rpcs)
@@ -153,14 +167,19 @@ class ChannelClientTest(unittest.TestCase):
self.assertNotIn(12345, service)
def test_method_fully_qualified_name(self) -> None:
- self.assertIs(self._channel_client.method('pw.test2.Alpha/Unary'),
- self._channel_client.rpcs.pw.test2.Alpha.Unary)
- self.assertIs(self._channel_client.method('pw.test2.Alpha.Unary'),
- self._channel_client.rpcs.pw.test2.Alpha.Unary)
+ self.assertIs(
+ self._channel_client.method('pw.test2.Alpha/Unary'),
+ self._channel_client.rpcs.pw.test2.Alpha.Unary,
+ )
+ self.assertIs(
+ self._channel_client.method('pw.test2.Alpha.Unary'),
+ self._channel_client.rpcs.pw.test2.Alpha.Unary,
+ )
class ClientTest(unittest.TestCase):
"""Tests the pw_rpc Client independently of the ClientImpl."""
+
def setUp(self) -> None:
self._last_packet_sent_bytes: Optional[bytes] = None
self._protos, self._client = _test_setup(self._save_packet)
@@ -200,11 +219,17 @@ class ClientTest(unittest.TestCase):
def test_method_present(self) -> None:
self.assertIs(
- self._client.method('pw.test1.PublicService.SomeUnary'), self.
- _client.services['pw.test1.PublicService'].methods['SomeUnary'])
+ self._client.method('pw.test1.PublicService.SomeUnary'),
+ self._client.services['pw.test1.PublicService'].methods[
+ 'SomeUnary'
+ ],
+ )
self.assertIs(
- self._client.method('pw.test1.PublicService/SomeUnary'), self.
- _client.services['pw.test1.PublicService'].methods['SomeUnary'])
+ self._client.method('pw.test1.PublicService/SomeUnary'),
+ self._client.services['pw.test1.PublicService'].methods[
+ 'SomeUnary'
+ ],
+ )
def test_method_invalid_format(self) -> None:
with self.assertRaises(ValueError):
@@ -218,37 +243,48 @@ class ClientTest(unittest.TestCase):
self._client.method('nothing.Good')
def test_process_packet_invalid_proto_data(self) -> None:
- self.assertIs(self._client.process_packet(b'NOT a packet!'),
- Status.DATA_LOSS)
+ self.assertIs(
+ self._client.process_packet(b'NOT a packet!'), Status.DATA_LOSS
+ )
def test_process_packet_not_for_client(self) -> None:
self.assertIs(
self._client.process_packet(
- RpcPacket(type=PacketType.REQUEST).SerializeToString()),
- Status.INVALID_ARGUMENT)
+ RpcPacket(type=PacketType.REQUEST).SerializeToString()
+ ),
+ Status.INVALID_ARGUMENT,
+ )
def test_process_packet_unrecognized_channel(self) -> None:
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (123, 456, 789),
- self._protos.packages.pw.test2.Request())),
- Status.NOT_FOUND)
+ (123, 456, 789), self._protos.packages.pw.test2.Request()
+ )
+ ),
+ Status.NOT_FOUND,
+ )
def test_process_packet_unrecognized_service(self) -> None:
self.assertIs(
self._client.process_packet(
packets.encode_response(
- (1, 456, 789), self._protos.packages.pw.test2.Request())),
- Status.OK)
+ (1, 456, 789), self._protos.packages.pw.test2.Request()
+ )
+ ),
+ Status.OK,
+ )
self.assertEqual(
self._last_packet_sent(),
- RpcPacket(type=PacketType.CLIENT_ERROR,
- channel_id=1,
- service_id=456,
- method_id=789,
- status=Status.NOT_FOUND.value))
+ RpcPacket(
+ type=PacketType.CLIENT_ERROR,
+ channel_id=1,
+ service_id=456,
+ method_id=789,
+ status=Status.NOT_FOUND.value,
+ ),
+ )
def test_process_packet_unrecognized_method(self) -> None:
service = next(iter(self._client.services))
@@ -257,15 +293,22 @@ class ClientTest(unittest.TestCase):
self._client.process_packet(
packets.encode_response(
(1, service.id, 789),
- self._protos.packages.pw.test2.Request())), Status.OK)
+ self._protos.packages.pw.test2.Request(),
+ )
+ ),
+ Status.OK,
+ )
self.assertEqual(
self._last_packet_sent(),
- RpcPacket(type=PacketType.CLIENT_ERROR,
- channel_id=1,
- service_id=service.id,
- method_id=789,
- status=Status.NOT_FOUND.value))
+ RpcPacket(
+ type=PacketType.CLIENT_ERROR,
+ channel_id=1,
+ service_id=service.id,
+ method_id=789,
+ status=Status.NOT_FOUND.value,
+ ),
+ )
def test_process_packet_non_pending_method(self) -> None:
service = next(iter(self._client.services))
@@ -275,26 +318,36 @@ class ClientTest(unittest.TestCase):
self._client.process_packet(
packets.encode_response(
(1, service.id, method.id),
- self._protos.packages.pw.test2.Request())), Status.OK)
+ self._protos.packages.pw.test2.Request(),
+ )
+ ),
+ Status.OK,
+ )
self.assertEqual(
self._last_packet_sent(),
- RpcPacket(type=PacketType.CLIENT_ERROR,
- channel_id=1,
- service_id=service.id,
- method_id=method.id,
- status=Status.FAILED_PRECONDITION.value))
+ RpcPacket(
+ type=PacketType.CLIENT_ERROR,
+ channel_id=1,
+ service_id=service.id,
+ method_id=method.id,
+ status=Status.FAILED_PRECONDITION.value,
+ ),
+ )
def test_process_packet_non_pending_calls_response_callback(self) -> None:
method = self._client.method('pw.test1.PublicService.SomeUnary')
reply = method.response_type(payload='hello')
- def response_callback(rpc: client.PendingRpc, message,
- status: Optional[Status]) -> None:
+ def response_callback(
+ rpc: client.PendingRpc, message, status: Optional[Status]
+ ) -> None:
self.assertEqual(
rpc,
client.PendingRpc(
- self._client.channel(1).channel, method.service, method))
+ self._client.channel(1).channel, method.service, method
+ ),
+ )
self.assertEqual(message, reply)
self.assertIs(status, Status.OK)
@@ -302,8 +355,10 @@ class ClientTest(unittest.TestCase):
self.assertIs(
self._client.process_packet(
- packets.encode_response((1, method.service, method), reply)),
- Status.OK)
+ packets.encode_response((1, method.service, method), reply)
+ ),
+ Status.OK,
+ )
if __name__ == '__main__':
diff --git a/pw_rpc/py/tests/console_tools/console_tools_test.py b/pw_rpc/py/tests/console_tools/console_tools_test.py
index 36285db71..3ddc08380 100755
--- a/pw_rpc/py/tests/console_tools/console_tools_test.py
+++ b/pw_rpc/py/tests/console_tools/console_tools_test.py
@@ -15,6 +15,7 @@
"""Tests the pw_rpc.console_tools.console module."""
import types
+from typing import Optional
import unittest
import pw_status
@@ -22,16 +23,21 @@ import pw_status
from pw_protobuf_compiler import python_protos
import pw_rpc
from pw_rpc import callback_client
-from pw_rpc.console_tools.console import (CommandHelper, Context, ClientInfo,
- alias_deprecated_command)
+from pw_rpc.console_tools.console import (
+ CommandHelper,
+ Context,
+ ClientInfo,
+ alias_deprecated_command,
+)
class TestCommandHelper(unittest.TestCase):
def setUp(self) -> None:
self._commands = {'command_a': 'A', 'command_B': 'B'}
self._variables = {'hello': 1, 'world': 2}
- self._helper = CommandHelper(self._commands, self._variables,
- 'The header', 'The footer')
+ self._helper = CommandHelper(
+ self._commands, self._variables, 'The header', 'The footer'
+ )
def test_help_contents(self) -> None:
help_contents = self._helper.help()
@@ -71,20 +77,27 @@ service Service {
class TestConsoleContext(unittest.TestCase):
"""Tests console_tools.console.Context."""
+
def setUp(self) -> None:
self._protos = python_protos.Library.from_strings(_PROTO)
self._info = ClientInfo(
- 'the_client', object(),
- pw_rpc.Client.from_modules(callback_client.Impl(), [
- pw_rpc.Channel(1, lambda _: None),
- pw_rpc.Channel(2, lambda _: None),
- ], self._protos.modules()))
+ 'the_client',
+ object(),
+ pw_rpc.Client.from_modules(
+ callback_client.Impl(),
+ [
+ pw_rpc.Channel(1, lambda _: None),
+ pw_rpc.Channel(2, lambda _: None),
+ ],
+ self._protos.modules(),
+ ),
+ )
def test_sets_expected_variables(self) -> None:
- variables = Context([self._info],
- default_client=self._info.client,
- protos=self._protos).variables()
+ variables = Context(
+ [self._info], default_client=self._info.client, protos=self._protos
+ ).variables()
self.assertIn('set_target', variables)
@@ -98,34 +111,46 @@ class TestConsoleContext(unittest.TestCase):
client_2_channel = pw_rpc.Channel(99, lambda _: None)
info_2 = ClientInfo(
- 'other_client', object(),
- pw_rpc.Client.from_modules(callback_client.Impl(),
- [client_2_channel],
- self._protos.modules()))
-
- context = Context([self._info, info_2],
- default_client=self._info.client,
- protos=self._protos)
+ 'other_client',
+ object(),
+ pw_rpc.Client.from_modules(
+ callback_client.Impl(),
+ [client_2_channel],
+ self._protos.modules(),
+ ),
+ )
+
+ context = Context(
+ [self._info, info_2],
+ default_client=self._info.client,
+ protos=self._protos,
+ )
# Make sure the RPC service switches from one client to the other.
- self.assertIs(context.variables()['the'].pkg.Service.Unary.channel,
- client_1_channel)
+ self.assertIs(
+ context.variables()['the'].pkg.Service.Unary.channel,
+ client_1_channel,
+ )
context.set_target(info_2.client)
- self.assertIs(context.variables()['the'].pkg.Service.Unary.channel,
- client_2_channel)
+ self.assertIs(
+ context.variables()['the'].pkg.Service.Unary.channel,
+ client_2_channel,
+ )
def test_default_client_must_be_in_clients(self) -> None:
with self.assertRaises(ValueError):
- Context([self._info],
- default_client='something else',
- protos=self._protos)
+ Context(
+ [self._info],
+ default_client='something else',
+ protos=self._protos,
+ )
def test_set_target_invalid_channel(self) -> None:
- context = Context([self._info],
- default_client=self._info.client,
- protos=self._protos)
+ context = Context(
+ [self._info], default_client=self._info.client, protos=self._protos
+ )
with self.assertRaises(KeyError):
context.set_target(self._info.client, 100)
@@ -134,9 +159,9 @@ class TestConsoleContext(unittest.TestCase):
channel_1 = self._info.rpc_client.channel(1).channel
channel_2 = self._info.rpc_client.channel(2).channel
- context = Context([self._info],
- default_client=self._info.client,
- protos=self._protos)
+ context = Context(
+ [self._info], default_client=self._info.client, protos=self._protos
+ )
variables = context.variables()
self.assertIs(variables['the'].pkg.Service.Unary.channel, channel_1)
@@ -149,9 +174,9 @@ class TestConsoleContext(unittest.TestCase):
context.set_target(self._info.client, 100)
def test_set_target_requires_client_object(self) -> None:
- context = Context([self._info],
- default_client=self._info.client,
- protos=self._protos)
+ context = Context(
+ [self._info], default_client=self._info.client, protos=self._protos
+ )
with self.assertRaises(ValueError):
context.set_target(self._info.rpc_client)
@@ -162,15 +187,19 @@ class TestConsoleContext(unittest.TestCase):
called_derived_set_target = False
class DerivedContext(Context):
- def set_target(self,
- unused_selected_client,
- unused_channel_id: int = None) -> None:
+ def set_target(
+ self,
+ unused_selected_client,
+ unused_channel_id: Optional[int] = None,
+ ) -> None:
nonlocal called_derived_set_target
called_derived_set_target = True
- variables = DerivedContext(client_info=[self._info],
- default_client=self._info.client,
- protos=self._protos).variables()
+ variables = DerivedContext(
+ client_info=[self._info],
+ default_client=self._info.client,
+ protos=self._protos,
+ ).variables()
variables['set_target'](self._info.client)
self.assertTrue(called_derived_set_target)
diff --git a/pw_rpc/py/tests/console_tools/functions_test.py b/pw_rpc/py/tests/console_tools/functions_test.py
index 85f352a53..c2c2a56b9 100644
--- a/pw_rpc/py/tests/console_tools/functions_test.py
+++ b/pw_rpc/py/tests/console_tools/functions_test.py
@@ -18,7 +18,9 @@ import unittest
from pw_rpc.console_tools import functions
-def func(one, two: int, *a: bool, three=3, four: 'int' = 4, **kw) -> None: # pylint: disable=unused-argument
+def func( # pylint: disable=unused-argument
+ one, two: int, *a: bool, three=3, four: 'int' = 4, **kw
+) -> None:
"""This is the docstring.
More stuff.
@@ -38,8 +40,10 @@ class TestFunctions(unittest.TestCase):
def simple_function():
pass
- self.assertEqual(functions.format_function_help(simple_function),
- 'simple_function():\n\n (no docstring)')
+ self.assertEqual(
+ functions.format_function_help(simple_function),
+ 'simple_function():\n\n (no docstring)',
+ )
def test_format_complex_function_help(self) -> None:
self.assertEqual(functions.format_function_help(func), _EXPECTED_HELP)
diff --git a/pw_rpc/py/tests/console_tools/watchdog_test.py b/pw_rpc/py/tests/console_tools/watchdog_test.py
index 9bc203c1e..f891defc7 100644
--- a/pw_rpc/py/tests/console_tools/watchdog_test.py
+++ b/pw_rpc/py/tests/console_tools/watchdog_test.py
@@ -22,13 +22,15 @@ from pw_rpc.console_tools import Watchdog
class TestWatchdog(unittest.TestCase):
"""Tests the Watchdog class."""
+
def setUp(self) -> None:
self._reset = mock.Mock()
self._expiration = mock.Mock()
self._while_expired = mock.Mock()
- self._watchdog = Watchdog(self._reset, self._expiration,
- self._while_expired, 99999)
+ self._watchdog = Watchdog(
+ self._reset, self._expiration, self._while_expired, 99999
+ )
def _trigger_timeout(self) -> None:
# Don't wait for the timeout -- that's too flaky. Call the internal
diff --git a/pw_rpc/py/tests/descriptors_test.py b/pw_rpc/py/tests/descriptors_test.py
index 9b3bf9330..b328559a8 100644
--- a/pw_rpc/py/tests/descriptors_test.py
+++ b/pw_rpc/py/tests/descriptors_test.py
@@ -51,20 +51,24 @@ service PublicService {
class MethodTest(unittest.TestCase):
"""Tests pw_rpc.Method."""
+
def setUp(self):
- module, = python_protos.compile_and_import_strings([TEST_PROTO])
+ (module,) = python_protos.compile_and_import_strings([TEST_PROTO])
service = descriptors.Service.from_descriptor(
- module.DESCRIPTOR.services_by_name['PublicService'])
+ module.DESCRIPTOR.services_by_name['PublicService']
+ )
self._method = service.methods['SomeUnary']
def test_get_request_with_both_message_and_kwargs(self):
with self.assertRaisesRegex(TypeError, r'either'):
- self._method.get_request(self._method.request_type(),
- {'magic_number': 1})
+ self._method.get_request(
+ self._method.request_type(), {'magic_number': 1}
+ )
def test_get_request_neither_message_nor_kwargs(self):
- self.assertEqual(self._method.request_type(),
- self._method.get_request(None, None))
+ self.assertEqual(
+ self._method.request_type(), self._method.get_request(None, None)
+ )
def test_get_request_with_wrong_type(self):
with self.assertRaisesRegex(TypeError, r'pw\.test1\.SomeMessage'):
@@ -77,8 +81,8 @@ class MethodTest(unittest.TestCase):
def test_get_request_with_different_copy_of_same_message_class(self):
some_message_clone = MessageFactory(
- self._method.request_type.DESCRIPTOR.file.pool).GetPrototype(
- self._method.request_type.DESCRIPTOR)
+ self._method.request_type.DESCRIPTOR.file.pool
+ ).GetPrototype(self._method.request_type.DESCRIPTOR)
msg = some_message_clone()
diff --git a/pw_rpc/py/tests/ids_test.py b/pw_rpc/py/tests/ids_test.py
index 28f812f45..fed5b787d 100755
--- a/pw_rpc/py/tests/ids_test.py
+++ b/pw_rpc/py/tests/ids_test.py
@@ -21,19 +21,21 @@ from pw_build.generated_tests import Context, TestGenerator
from pw_build import generated_tests
from pw_rpc import ids
-_TESTS = TestGenerator([
- 'Empty string',
- (0x00000000, ''),
- 'Single character strings',
- (0x00000001, '\0'),
- (0x00010040, '\1'),
- (0x003F0F82, '?'),
- 'Non-printable strings',
- (0xD3556087, '\0\0\0\1\1\1\1'),
- 'General strings',
- (0x63D43D8C, 'Pigweed?'),
- (0x79AB6494, 'Pigweed!Pigweed!Pigweed!Pigweed!Pigweed!Pigweed!'),
-])
+_TESTS = TestGenerator(
+ [
+ 'Empty string',
+ (0x00000000, ''),
+ 'Single character strings',
+ (0x00000001, '\0'),
+ (0x00010040, '\1'),
+ (0x003F0F82, '?'),
+ 'Non-printable strings',
+ (0xD3556087, '\0\0\0\1\1\1\1'),
+ 'General strings',
+ (0x63D43D8C, 'Pigweed?'),
+ (0x79AB6494, 'Pigweed!Pigweed!Pigweed!Pigweed!Pigweed!Pigweed!'),
+ ]
+)
def _define_py_test(ctx: Context):
@@ -69,7 +71,6 @@ def _cc_test(ctx: Context) -> Iterator[str]:
if __name__ == '__main__':
args = generated_tests.parse_test_generation_args()
if args.generate_cc_test:
- _TESTS.cc_tests(args.generate_cc_test, _cc_test, _CC_HEADER,
- _CC_FOOTER)
+ _TESTS.cc_tests(args.generate_cc_test, _cc_test, _CC_HEADER, _CC_FOOTER)
else:
unittest.main()
diff --git a/pw_rpc/py/tests/packets_test.py b/pw_rpc/py/tests/packets_test.py
index 452ee90b0..d6fa87935 100755
--- a/pw_rpc/py/tests/packets_test.py
+++ b/pw_rpc/py/tests/packets_test.py
@@ -21,15 +21,18 @@ from pw_status import Status
from pw_rpc.internal.packet_pb2 import PacketType, RpcPacket
from pw_rpc import packets
-_TEST_REQUEST = RpcPacket(type=PacketType.REQUEST,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString())
+_TEST_REQUEST = RpcPacket(
+ type=PacketType.REQUEST,
+ channel_id=1,
+ service_id=2,
+ method_id=3,
+ payload=RpcPacket(status=321).SerializeToString(),
+)
class PacketsTest(unittest.TestCase):
"""Tests for packet encoding and decoding."""
+
def test_encode_request(self):
data = packets.encode_request((1, 2, 3), RpcPacket(status=321))
packet = RpcPacket()
@@ -38,11 +41,13 @@ class PacketsTest(unittest.TestCase):
self.assertEqual(_TEST_REQUEST, packet)
def test_encode_response(self):
- response = RpcPacket(type=PacketType.RESPONSE,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString())
+ response = RpcPacket(
+ type=PacketType.RESPONSE,
+ channel_id=1,
+ service_id=2,
+ method_id=3,
+ payload=RpcPacket(status=321).SerializeToString(),
+ )
data = packets.encode_response((1, 2, 3), RpcPacket(status=321))
packet = RpcPacket()
@@ -58,11 +63,14 @@ class PacketsTest(unittest.TestCase):
self.assertEqual(
packet,
- RpcPacket(type=PacketType.CLIENT_ERROR,
- channel_id=9,
- service_id=8,
- method_id=7,
- status=Status.CANCELLED.value))
+ RpcPacket(
+ type=PacketType.CLIENT_ERROR,
+ channel_id=9,
+ service_id=8,
+ method_id=7,
+ status=Status.CANCELLED.value,
+ ),
+ )
def test_encode_client_error(self):
data = packets.encode_client_error(_TEST_REQUEST, Status.NOT_FOUND)
@@ -72,26 +80,34 @@ class PacketsTest(unittest.TestCase):
self.assertEqual(
packet,
- RpcPacket(type=PacketType.CLIENT_ERROR,
- channel_id=1,
- service_id=2,
- method_id=3,
- status=Status.NOT_FOUND.value))
+ RpcPacket(
+ type=PacketType.CLIENT_ERROR,
+ channel_id=1,
+ service_id=2,
+ method_id=3,
+ status=Status.NOT_FOUND.value,
+ ),
+ )
def test_decode(self):
- self.assertEqual(_TEST_REQUEST,
- packets.decode(_TEST_REQUEST.SerializeToString()))
+ self.assertEqual(
+ _TEST_REQUEST, packets.decode(_TEST_REQUEST.SerializeToString())
+ )
def test_for_server(self):
self.assertTrue(packets.for_server(_TEST_REQUEST))
self.assertFalse(
packets.for_server(
- RpcPacket(type=PacketType.RESPONSE,
- channel_id=1,
- service_id=2,
- method_id=3,
- payload=RpcPacket(status=321).SerializeToString())))
+ RpcPacket(
+ type=PacketType.RESPONSE,
+ channel_id=1,
+ service_id=2,
+ method_id=3,
+ payload=RpcPacket(status=321).SerializeToString(),
+ )
+ )
+ )
if __name__ == '__main__':
diff --git a/pw_rpc/py/tests/python_client_cpp_server_test.py b/pw_rpc/py/tests/python_client_cpp_server_test.py
index db9d35cbe..ea180b2eb 100755
--- a/pw_rpc/py/tests/python_client_cpp_server_test.py
+++ b/pw_rpc/py/tests/python_client_cpp_server_test.py
@@ -26,12 +26,14 @@ ITERATIONS = 50
class RpcIntegrationTest(unittest.TestCase):
"""Calls RPCs on an RPC server through a socket."""
+
test_server_command: Tuple[str, ...] = ()
port: int
def setUp(self) -> None:
self._context = pw_hdlc.rpc.HdlcRpcLocalServerAndClient(
- self.test_server_command, self.port, [benchmark_pb2])
+ self.test_server_command, self.port, [benchmark_pb2]
+ )
self.rpcs = self._context.client.channel(1).rpcs
def tearDown(self) -> None:
@@ -41,7 +43,8 @@ class RpcIntegrationTest(unittest.TestCase):
for i in range(ITERATIONS):
payload = f'O_o #{i}'.encode()
status, reply = self.rpcs.pw.rpc.Benchmark.UnaryEcho(
- payload=payload)
+ payload=payload
+ )
self.assertIs(status, Status.OK)
self.assertEqual(reply.payload, payload)
@@ -61,26 +64,31 @@ class RpcIntegrationTest(unittest.TestCase):
for _ in range(ITERATIONS):
first_call = rpc.invoke()
first_call.send(payload=b'abc')
- self.assertEqual(next(iter(first_call)),
- rpc.response(payload=b'abc'))
+ self.assertEqual(
+ next(iter(first_call)), rpc.response(payload=b'abc')
+ )
self.assertFalse(first_call.completed())
second_call = rpc.invoke()
second_call.send(payload=b'123')
- self.assertEqual(next(iter(second_call)),
- rpc.response(payload=b'123'))
+ self.assertEqual(
+ next(iter(second_call)), rpc.response(payload=b'123')
+ )
self.assertIs(first_call.error, Status.CANCELLED)
- self.assertEqual(first_call.responses,
- [rpc.response(payload=b'abc')])
+ self.assertEqual(
+ first_call.responses, [rpc.response(payload=b'abc')]
+ )
self.assertFalse(second_call.completed())
- self.assertEqual(second_call.responses,
- [rpc.response(payload=b'123')])
+ self.assertEqual(
+ second_call.responses, [rpc.response(payload=b'123')]
+ )
-def _main(test_server_command: List[str], port: int,
- unittest_args: List[str]) -> None:
+def _main(
+ test_server_command: List[str], port: int, unittest_args: List[str]
+) -> None:
RpcIntegrationTest.test_server_command = tuple(test_server_command)
RpcIntegrationTest.port = port
unittest.main(argv=unittest_args)
diff --git a/pw_rpc/raw/Android.bp b/pw_rpc/raw/Android.bp
new file mode 100644
index 000000000..0b48ffc88
--- /dev/null
+++ b/pw_rpc/raw/Android.bp
@@ -0,0 +1,74 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+filegroup {
+ name: "pw_rpc_raw_src_files",
+ srcs: [
+ "method.cc",
+ ],
+}
+
+cc_library_headers {
+ name: "pw_rpc_raw_include_dirs",
+ export_include_dirs: [
+ "public",
+ ],
+ vendor_available: true,
+ host_supported: true,
+}
+
+// This rule must be instantiated, i.e.
+//
+// cc_library_static {
+// name: "pw_rpc_raw_<instance_name>",
+// defaults: [
+// "pw_rpc_cflags_<instance_name>",
+// "pw_rpc_raw_defaults",
+// ],
+// static_libs: [
+// "pw_rpc_<instance_name>",
+// ],
+// }
+//
+// where pw_rpc_cflags_<instance_name> defines your flags, i.e.
+//
+// cc_defaults {
+// name: "pw_rpc_cflags_<instance_name>",
+// cflags: [
+// "-DPW_RPC_USE_GLOBAL_MUTEX=0",
+// "-DPW_RPC_CLIENT_STREAM_END_CALLBACK",
+// "-DPW_RPC_DYNAMIC_ALLOCATION",
+// ],
+// }
+//
+// see pw_rpc_defaults, pw_rpc_defaults
+cc_defaults {
+ name: "pw_rpc_raw_defaults",
+ cpp_std: "c++2a",
+ header_libs: [
+ "pw_rpc_raw_include_dirs",
+ ],
+ export_header_lib_headers: [
+ "pw_rpc_raw_include_dirs",
+ ],
+ srcs: [
+ ":pw_rpc_raw_src_files",
+ ],
+ host_supported: true,
+ vendor_available: true,
+}
diff --git a/pw_rpc/raw/BUILD.bazel b/pw_rpc/raw/BUILD.bazel
index fcad0a628..ac9c10e8c 100644
--- a/pw_rpc/raw/BUILD.bazel
+++ b/pw_rpc/raw/BUILD.bazel
@@ -137,6 +137,7 @@ pw_cc_test(
],
deps = [
":server_api",
+ "//pw_containers",
"//pw_protobuf",
"//pw_rpc:internal_test_utils",
"//pw_rpc:pw_rpc_test_cc.pwpb",
@@ -178,6 +179,14 @@ pw_cc_test(
],
)
+# Negative compilation testing is not supported by Bazel. Build this as a
+# regular unit for now test.
+pw_cc_test(
+ name = "service_nc_test",
+ srcs = ["service_nc_test.cc"],
+ deps = ["//pw_rpc:pw_rpc_test_cc.raw_rpc"],
+)
+
pw_cc_test(
name = "stub_generation_test",
srcs = ["stub_generation_test.cc"],
diff --git a/pw_rpc/raw/BUILD.gn b/pw_rpc/raw/BUILD.gn
index cad3bd6b9..48b35bf50 100644
--- a/pw_rpc/raw/BUILD.gn
+++ b/pw_rpc/raw/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_compilation_testing/negative_compilation_test.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
@@ -119,6 +120,8 @@ pw_test("client_reader_writer_test") {
pw_test("method_test") {
deps = [
":server_api",
+ ":service_nc_test", # Pull in the service NC test through this test
+ "$dir_pw_containers",
"..:test_protos.pwpb",
"..:test_protos.raw_rpc",
"..:test_utils",
@@ -159,3 +162,8 @@ pw_test("stub_generation_test") {
deps = [ "..:test_protos.raw_rpc" ]
sources = [ "stub_generation_test.cc" ]
}
+
+pw_cc_negative_compilation_test("service_nc_test") {
+ sources = [ "service_nc_test.cc" ]
+ deps = [ "..:test_protos.raw_rpc" ]
+}
diff --git a/pw_rpc/raw/CMakeLists.txt b/pw_rpc/raw/CMakeLists.txt
index 8a9c7ad91..a610d42b3 100644
--- a/pw_rpc/raw/CMakeLists.txt
+++ b/pw_rpc/raw/CMakeLists.txt
@@ -14,14 +14,138 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_auto_add_simple_module(pw_rpc.raw
+pw_add_library(pw_rpc.raw.server_api STATIC
+ HEADERS
+ public/pw_rpc/raw/internal/method.h
+ public/pw_rpc/raw/internal/method_union.h
+ public/pw_rpc/raw/server_reader_writer.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
- pw_log
- pw_rpc.client
- pw_rpc.common
pw_rpc.server
- TEST_DEPS
+ pw_bytes
+ SOURCES
+ method.cc
+)
+
+pw_add_library(pw_rpc.raw.client_api INTERFACE
+ HEADERS
+ public/pw_rpc/raw/client_reader_writer.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.client
+ pw_bytes
+)
+
+pw_add_library(pw_rpc.raw.fake_channel_output INTERFACE
+ HEADERS
+ public/pw_rpc/raw/fake_channel_output.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.fake_channel_output
+)
+
+pw_add_library(pw_rpc.raw.test_method_context INTERFACE
+ HEADERS
+ public/pw_rpc/raw/test_method_context.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_containers
+ pw_rpc.raw.fake_channel_output
+ pw_rpc.raw.server_api
+ pw_rpc.test_utils
+)
+
+pw_add_library(pw_rpc.raw.client_testing STATIC
+ HEADERS
+ public/pw_rpc/raw/client_testing.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.fake_channel_output
+ SOURCES
+ client_testing.cc
+ PRIVATE_DEPS
+ pw_log
+ pw_rpc.log_config
+)
+
+pw_add_test(pw_rpc.raw.client_test
+ SOURCES
+ client_test.cc
+ PRIVATE_DEPS
+ pw_rpc.raw.client_api
+ pw_rpc.raw.client_testing
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.raw
+)
+
+pw_add_test(pw_rpc.raw.client_reader_writer_test
+ SOURCES
+ client_reader_writer_test.cc
+ PRIVATE_DEPS
+ pw_rpc.raw.client_api
+ pw_rpc.raw.client_testing
+ pw_rpc.test_protos.raw_rpc
+ GROUPS
+ modules
+ pw_rpc.raw
+)
+
+pw_add_test(pw_rpc.raw.method_test
+ SOURCES
+ method_test.cc
+ PRIVATE_DEPS
+ pw_containers
+ pw_protobuf
+ pw_rpc.raw.server_api
pw_rpc.test_protos.pwpb
pw_rpc.test_protos.raw_rpc
pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.raw
+)
+
+pw_add_test(pw_rpc.raw.method_info_test
+ SOURCES
+ method_info_test.cc
+ PRIVATE_DEPS
+ pw_rpc.common
+ pw_rpc.test_protos.raw_rpc
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.raw
+)
+
+pw_add_test(pw_rpc.raw.method_union_test
+ SOURCES
+ method_union_test.cc
+ PRIVATE_DEPS
+ pw_protobuf
+ pw_rpc.raw.server_api
+ pw_rpc.test_protos.pwpb
+ pw_rpc.test_utils
+ GROUPS
+ modules
+ pw_rpc.raw
+)
+
+pw_add_test(pw_rpc.raw.server_reader_writer_test
+ SOURCES
+ server_reader_writer_test.cc
+ PRIVATE_DEPS
+ pw_rpc.raw.server_api
+ pw_rpc.raw.test_method_context
+ pw_rpc.test_protos.raw_rpc
+ GROUPS
+ modules
+ pw_rpc.raw
)
diff --git a/pw_rpc/raw/client_reader_writer_test.cc b/pw_rpc/raw/client_reader_writer_test.cc
index df62de26f..8c9553c09 100644
--- a/pw_rpc/raw/client_reader_writer_test.cc
+++ b/pw_rpc/raw/client_reader_writer_test.cc
@@ -14,6 +14,8 @@
#include "pw_rpc/raw/client_reader_writer.h"
+#include <optional>
+
#include "gtest/gtest.h"
#include "pw_rpc/raw/client_testing.h"
#include "pw_rpc/writer.h"
@@ -82,7 +84,7 @@ TEST(RawClientReaderWriter, DefaultConstructed) {
call.set_on_error([](Status) {});
}
-TEST(RawUnaryReceiver, Closed) {
+TEST(RawUnaryReceiver, Cancel) {
RawClientTestContext ctx;
RawUnaryReceiver call = TestService::TestUnaryRpc(ctx.client(),
ctx.channel().id(),
@@ -91,6 +93,10 @@ TEST(RawUnaryReceiver, Closed) {
FailIfCalled);
ASSERT_EQ(OkStatus(), call.Cancel());
+ // Additional calls should do nothing and return FAILED_PRECONDITION.
+ ASSERT_EQ(Status::FailedPrecondition(), call.Cancel());
+ ASSERT_EQ(Status::FailedPrecondition(), call.Cancel());
+
ASSERT_FALSE(call.active());
EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
@@ -98,9 +104,11 @@ TEST(RawUnaryReceiver, Closed) {
call.set_on_completed([](ConstByteSpan, Status) {});
call.set_on_error([](Status) {});
+
+ EXPECT_EQ(ctx.output().total_packets(), 2u); // request & cancellation only
}
-TEST(RawClientWriter, Closed) {
+TEST(RawClientWriter, Cancel) {
RawClientTestContext ctx;
RawClientWriter call = TestService::TestClientStreamRpc(
ctx.client(), ctx.channel().id(), FailIfOnCompletedCalled, FailIfCalled);
@@ -115,9 +123,11 @@ TEST(RawClientWriter, Closed) {
call.set_on_completed([](ConstByteSpan, Status) {});
call.set_on_error([](Status) {});
+
+ EXPECT_EQ(ctx.output().total_packets(), 2u); // request & cancellation only
}
-TEST(RawClientReader, Closed) {
+TEST(RawClientReader, Cancel) {
RawClientTestContext ctx;
RawClientReader call = TestService::TestServerStreamRpc(ctx.client(),
ctx.channel().id(),
@@ -135,9 +145,11 @@ TEST(RawClientReader, Closed) {
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
call.set_on_error([](Status) {});
+
+ EXPECT_EQ(ctx.output().total_packets(), 2u); // request & cancellation only
}
-TEST(RawClientReaderWriter, Closed) {
+TEST(RawClientReaderWriter, Cancel) {
RawClientTestContext ctx;
RawClientReaderWriter call =
TestService::TestBidirectionalStreamRpc(ctx.client(),
@@ -157,6 +169,79 @@ TEST(RawClientReaderWriter, Closed) {
call.set_on_completed([](Status) {});
call.set_on_next([](ConstByteSpan) {});
call.set_on_error([](Status) {});
+
+ EXPECT_EQ(ctx.output().total_packets(), 2u); // request & cancellation only
+}
+
+TEST(RawUnaryReceiver, Abandon) {
+ RawClientTestContext ctx;
+ RawUnaryReceiver call = TestService::TestUnaryRpc(ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnCompletedCalled,
+ FailIfCalled);
+ call.Abandon();
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+
+ EXPECT_EQ(ctx.output().total_packets(), 1u); // request only
+}
+
+TEST(RawClientWriter, Abandon) {
+ RawClientTestContext ctx;
+ RawClientWriter call = TestService::TestClientStreamRpc(
+ ctx.client(), ctx.channel().id(), FailIfOnCompletedCalled, FailIfCalled);
+ call.Abandon();
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+
+ EXPECT_EQ(ctx.output().total_packets(), 2u); // request & client stream end
+}
+
+TEST(RawClientReader, Abandon) {
+ RawClientTestContext ctx;
+ RawClientReader call = TestService::TestServerStreamRpc(ctx.client(),
+ ctx.channel().id(),
+ {},
+ FailIfOnNextCalled,
+ FailIfCalled,
+ FailIfCalled);
+ call.Abandon();
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+
+ EXPECT_EQ(ctx.output().total_packets(), 1u); // request only
+}
+
+TEST(RawClientReaderWriter, Abandon) {
+ RawClientTestContext ctx;
+ RawClientReaderWriter call =
+ TestService::TestBidirectionalStreamRpc(ctx.client(),
+ ctx.channel().id(),
+ FailIfOnNextCalled,
+ FailIfCalled,
+ FailIfCalled);
+ call.Abandon();
+
+ ASSERT_FALSE(call.active());
+ EXPECT_EQ(call.channel_id(), Channel::kUnassignedChannelId);
+
+ EXPECT_EQ(Status::FailedPrecondition(), call.Write({}));
+ EXPECT_EQ(Status::FailedPrecondition(), call.Cancel());
+ EXPECT_EQ(Status::FailedPrecondition(), call.CloseClientStream());
+
+ EXPECT_EQ(ctx.output().total_packets(), 2u); // request & client stream end
}
TEST(RawClientReaderWriter, Move_InactiveToActive_EndsClientStream) {
@@ -231,25 +316,16 @@ TEST(RawUnaryReceiver, Move_ActiveToActive) {
EXPECT_TRUE(active_call_2.active());
}
-TEST(RawClientReaderWriter, NewCallCancelsPreviousAndCallsErrorCallback) {
+TEST(RawUnaryReceiver, InvalidChannelId) {
RawClientTestContext ctx;
-
- Status error;
- RawClientReaderWriter active_call_1 = TestService::TestBidirectionalStreamRpc(
- ctx.client(),
- ctx.channel().id(),
- FailIfOnNextCalled,
- FailIfCalled,
- [&error](Status status) { error = status; });
-
- ASSERT_TRUE(active_call_1.active());
-
- RawClientReaderWriter active_call_2 =
- TestService::TestBidirectionalStreamRpc(ctx.client(), ctx.channel().id());
-
- EXPECT_FALSE(active_call_1.active());
- EXPECT_TRUE(active_call_2.active());
- EXPECT_EQ(error, Status::Cancelled());
+ std::optional<Status> error;
+
+ RawUnaryReceiver call = TestService::TestUnaryRpc(
+ ctx.client(), 1290341, {}, {}, [&error](Status status) {
+ error = status;
+ });
+ EXPECT_FALSE(call.active());
+ EXPECT_EQ(error, Status::Unavailable());
}
TEST(RawClientReader, NoClientStream_OutOfScope_SilentlyCloses) {
@@ -292,7 +368,7 @@ void WriteAsWriter(Writer& writer) {
ASSERT_TRUE(writer.active());
ASSERT_EQ(writer.channel_id(), RawClientTestContext<>::kDefaultChannelId);
- EXPECT_EQ(OkStatus(), writer.Write(std::as_bytes(std::span(kWriterData))));
+ EXPECT_EQ(OkStatus(), writer.Write(as_bytes(span(kWriterData))));
}
TEST(RawClientWriter, UsableAsWriter) {
@@ -329,5 +405,49 @@ TEST(RawClientReaderWriter, UsableAsWriter) {
kWriterData);
}
+const char* span_as_cstr(ConstByteSpan span) {
+ return reinterpret_cast<const char*>(span.data());
+}
+
+TEST(RawClientReaderWriter,
+ MultipleCallsToSameMethodOkAndReceiveSeparateResponses) {
+ RawClientTestContext ctx;
+
+ ConstByteSpan data_1 = as_bytes(span("data_1_unset"));
+ ConstByteSpan data_2 = as_bytes(span("data_2_unset"));
+
+ Status error;
+ auto set_error = [&error](Status status) { error.Update(status); };
+ RawClientReaderWriter active_call_1 = TestService::TestBidirectionalStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ [&data_1](ConstByteSpan payload) { data_1 = payload; },
+ FailIfCalled,
+ set_error);
+
+ EXPECT_TRUE(active_call_1.active());
+
+ RawClientReaderWriter active_call_2 = TestService::TestBidirectionalStreamRpc(
+ ctx.client(),
+ ctx.channel().id(),
+ [&data_2](ConstByteSpan payload) { data_2 = payload; },
+ FailIfCalled,
+ set_error);
+
+ EXPECT_TRUE(active_call_1.active());
+ EXPECT_TRUE(active_call_2.active());
+ EXPECT_EQ(error, OkStatus());
+
+ ConstByteSpan message_1 = as_bytes(span("hello_1"));
+ ConstByteSpan message_2 = as_bytes(span("hello_2"));
+
+ ctx.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ message_2, active_call_2.id());
+ EXPECT_STREQ(span_as_cstr(data_2), span_as_cstr(message_2));
+ ctx.server().SendServerStream<TestService::TestBidirectionalStreamRpc>(
+ message_1, active_call_1.id());
+ EXPECT_STREQ(span_as_cstr(data_1), span_as_cstr(message_1));
+}
+
} // namespace
} // namespace pw::rpc
diff --git a/pw_rpc/raw/client_test.cc b/pw_rpc/raw/client_test.cc
index 6175c788e..a7009d649 100644
--- a/pw_rpc/raw/client_test.cc
+++ b/pw_rpc/raw/client_test.cc
@@ -43,29 +43,42 @@ struct internal::MethodInfo<BidirectionalStreamMethod> {
namespace {
template <auto kMethod, typename Call, typename Context>
-Call MakeCall(Context& context) {
- return Call(context.client(),
- context.channel().id(),
- internal::MethodInfo<kMethod>::kServiceId,
- internal::MethodInfo<kMethod>::kMethodId,
- internal::MethodInfo<kMethod>::kType);
+Call StartCall(Context& context,
+ std::optional<uint32_t> channel_id = std::nullopt)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ internal::rpc_lock().lock();
+ Call call(static_cast<internal::Endpoint&>(context.client()).ClaimLocked(),
+ channel_id.value_or(context.channel().id()),
+ internal::MethodInfo<kMethod>::kServiceId,
+ internal::MethodInfo<kMethod>::kMethodId,
+ internal::MethodInfo<kMethod>::kType);
+ call.SendInitialClientRequest({});
+ // As in the real implementations, immediately clean up aborted calls.
+ static_cast<internal::Endpoint&>(context.client()).CleanUpCalls();
+ return call;
}
class TestStreamCall : public internal::StreamResponseClientCall {
public:
- TestStreamCall(Client& client,
+ TestStreamCall(internal::LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: StreamResponseClientCall(
- client, channel_id, service_id, method_id, type),
+ client,
+ channel_id,
+ service_id,
+ method_id,
+ internal::CallProperties(
+ type, internal::kClientCall, internal::kRawProto)),
payload(nullptr) {
- set_on_next([this](ConstByteSpan string) {
+ set_on_next_locked([this](ConstByteSpan string) {
payload = reinterpret_cast<const char*>(string.data());
});
- set_on_completed([this](Status status) { completed = status; });
- set_on_error([this](Status status) { error = status; });
+ set_on_completed_locked([this](Status status) { completed = status; });
+ set_on_error_locked([this](Status status) { error = status; });
}
const char* payload;
@@ -75,21 +88,32 @@ class TestStreamCall : public internal::StreamResponseClientCall {
class TestUnaryCall : public internal::UnaryResponseClientCall {
public:
- TestUnaryCall(Client& client,
+ TestUnaryCall() = default;
+
+ TestUnaryCall(internal::LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id,
MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: UnaryResponseClientCall(
- client, channel_id, service_id, method_id, type),
+ client,
+ channel_id,
+ service_id,
+ method_id,
+ internal::CallProperties(
+ type, internal::kClientCall, internal::kRawProto)),
payload(nullptr) {
- set_on_completed([this](ConstByteSpan string, Status status) {
+ set_on_completed_locked([this](ConstByteSpan string, Status status) {
payload = reinterpret_cast<const char*>(string.data());
completed = status;
});
- set_on_error([this](Status status) { error = status; });
+ set_on_error_locked([this](Status status) { error = status; });
}
+ using Call::set_on_error;
+ using UnaryResponseClientCall::set_on_completed;
+
const char* payload;
std::optional<Status> completed;
std::optional<Status> error;
@@ -97,28 +121,37 @@ class TestUnaryCall : public internal::UnaryResponseClientCall {
TEST(Client, ProcessPacket_InvokesUnaryCallbacks) {
RawClientTestContext context;
- TestUnaryCall call = MakeCall<UnaryMethod, TestUnaryCall>(context);
- internal::rpc_lock().lock();
- call.SendInitialClientRequest({});
+ TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(context);
ASSERT_NE(call.completed, OkStatus());
- context.server().SendResponse<UnaryMethod>(
- std::as_bytes(std::span("you nary?!?")), OkStatus());
+ context.server().SendResponse<UnaryMethod>(as_bytes(span("you nary?!?")),
+ OkStatus());
ASSERT_NE(call.payload, nullptr);
EXPECT_STREQ(call.payload, "you nary?!?");
EXPECT_EQ(call.completed, OkStatus());
+ EXPECT_FALSE(call.active());
+}
+
+TEST(Client, ProcessPacket_NoCallbackSet) {
+ RawClientTestContext context;
+ TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(context);
+ call.set_on_completed(nullptr);
+
+ ASSERT_NE(call.completed, OkStatus());
+
+ context.server().SendResponse<UnaryMethod>(as_bytes(span("you nary?!?")),
+ OkStatus());
+ EXPECT_FALSE(call.active());
}
TEST(Client, ProcessPacket_InvokesStreamCallbacks) {
RawClientTestContext context;
- auto call = MakeCall<BidirectionalStreamMethod, TestStreamCall>(context);
- internal::rpc_lock().lock();
- call.SendInitialClientRequest({});
+ auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
context.server().SendServerStream<BidirectionalStreamMethod>(
- std::as_bytes(std::span("<=>")));
+ as_bytes(span("<=>")));
ASSERT_NE(call.payload, nullptr);
EXPECT_STREQ(call.payload, "<=>");
@@ -128,11 +161,26 @@ TEST(Client, ProcessPacket_InvokesStreamCallbacks) {
EXPECT_EQ(call.completed, Status::NotFound());
}
+TEST(Client, ProcessPacket_UnassignedChannelId_ReturnsDataLoss) {
+ RawClientTestContext context;
+ auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
+
+ std::byte encoded[64];
+ Result<span<const std::byte>> result =
+ internal::Packet(
+ internal::pwpb::PacketType::kResponse,
+ Channel::kUnassignedChannelId,
+ internal::MethodInfo<BidirectionalStreamMethod>::kServiceId,
+ internal::MethodInfo<BidirectionalStreamMethod>::kMethodId)
+ .Encode(encoded);
+ ASSERT_TRUE(result.ok());
+
+ EXPECT_EQ(context.client().ProcessPacket(*result), Status::DataLoss());
+}
+
TEST(Client, ProcessPacket_InvokesErrorCallback) {
RawClientTestContext context;
- auto call = MakeCall<BidirectionalStreamMethod, TestStreamCall>(context);
- internal::rpc_lock().lock();
- call.SendInitialClientRequest({});
+ auto call = StartCall<BidirectionalStreamMethod, TestStreamCall>(context);
context.server().SendServerError<BidirectionalStreamMethod>(
Status::Aborted());
@@ -170,15 +218,16 @@ TEST(Client, ProcessPacket_ReturnsInvalidArgumentOnServerPacket) {
RawClientTestContext context;
std::byte encoded[64];
- Result<std::span<const std::byte>> result =
- internal::Packet(internal::PacketType::REQUEST, 1, 2, 3).Encode(encoded);
+ Result<span<const std::byte>> result =
+ internal::Packet(internal::pwpb::PacketType::REQUEST, 1, 2, 3)
+ .Encode(encoded);
ASSERT_TRUE(result.ok());
EXPECT_EQ(context.client().ProcessPacket(*result), Status::InvalidArgument());
}
const Channel* GetChannel(internal::Endpoint& endpoint, uint32_t id) {
- internal::LockGuard lock(internal::rpc_lock());
+ internal::RpcLockGuard lock;
return endpoint.GetInternalChannel(id);
}
@@ -198,9 +247,7 @@ TEST(Client, CloseChannel_UnknownChannel) {
TEST(Client, CloseChannel_CallsErrorCallback) {
RawClientTestContext ctx;
- TestUnaryCall call = MakeCall<UnaryMethod, TestUnaryCall>(ctx);
- internal::rpc_lock().lock();
- call.SendInitialClientRequest({});
+ TestUnaryCall call = StartCall<UnaryMethod, TestUnaryCall>(ctx);
ASSERT_NE(call.completed, OkStatus());
ASSERT_EQ(1u,
@@ -213,6 +260,66 @@ TEST(Client, CloseChannel_CallsErrorCallback) {
ASSERT_EQ(call.error, Status::Aborted()); // set by the on_error callback
}
+TEST(Client, CloseChannel_ErrorCallbackReusesCallObjectForCallOnClosedChannel) {
+ struct {
+ RawClientTestContext<> ctx;
+ TestUnaryCall call;
+ } context;
+
+ context.call = StartCall<UnaryMethod, TestUnaryCall>(context.ctx);
+ context.call.set_on_error([&context](Status error) {
+ context.call = StartCall<UnaryMethod, TestUnaryCall>(context.ctx, 1);
+ context.call.error = error;
+ });
+
+ EXPECT_EQ(OkStatus(), context.ctx.client().CloseChannel(1));
+ EXPECT_EQ(context.call.error, Status::Aborted());
+
+ EXPECT_FALSE(context.call.active());
+ EXPECT_EQ(0u,
+ static_cast<internal::Endpoint&>(context.ctx.client())
+ .active_call_count());
+}
+
+TEST(Client, CloseChannel_ErrorCallbackReusesCallObjectForActiveCall) {
+ class ContextWithTwoChannels {
+ public:
+ ContextWithTwoChannels()
+ : channels_{Channel::Create<1>(&channel_output_),
+ Channel::Create<2>(&channel_output_)},
+ client_(channels_),
+ packet_buffer{},
+ fake_server_(channel_output_, client_, 1, packet_buffer) {}
+
+ Channel& channel() { return channels_[0]; }
+ Client& client() { return client_; }
+ TestUnaryCall& call() { return call_; }
+
+ private:
+ RawFakeChannelOutput<10, 256> channel_output_;
+ Channel channels_[2];
+ Client client_;
+ std::byte packet_buffer[64];
+ FakeServer fake_server_;
+
+ TestUnaryCall call_;
+ } context;
+
+ context.call() = StartCall<UnaryMethod, TestUnaryCall>(context, 1);
+ context.call().set_on_error([&context](Status error) {
+ context.call() = StartCall<UnaryMethod, TestUnaryCall>(context, 2);
+ context.call().error = error;
+ });
+
+ EXPECT_EQ(OkStatus(), context.client().CloseChannel(1));
+ EXPECT_EQ(context.call().error, Status::Aborted());
+
+ EXPECT_TRUE(context.call().active());
+ EXPECT_EQ(
+ 1u,
+ static_cast<internal::Endpoint&>(context.client()).active_call_count());
+}
+
TEST(Client, OpenChannel_UnusedSlot) {
RawClientTestContext ctx;
ASSERT_EQ(OkStatus(), ctx.client().CloseChannel(1));
diff --git a/pw_rpc/raw/client_testing.cc b/pw_rpc/raw/client_testing.cc
index e3b8f5363..bb5dc50e1 100644
--- a/pw_rpc/raw/client_testing.cc
+++ b/pw_rpc/raw/client_testing.cc
@@ -18,19 +18,23 @@
#include "pw_rpc/raw/client_testing.h"
// clang-format on
+#include <mutex>
+
#include "pw_assert/check.h"
#include "pw_log/log.h"
#include "pw_rpc/client.h"
+#include "pw_rpc/internal/lock.h"
namespace pw::rpc {
-void FakeServer::CheckProcessPacket(internal::PacketType type,
+void FakeServer::CheckProcessPacket(internal::pwpb::PacketType type,
uint32_t service_id,
uint32_t method_id,
+ std::optional<uint32_t> call_id,
ConstByteSpan payload,
Status status) const {
if (Status process_packet_status =
- ProcessPacket(type, service_id, method_id, payload, status);
+ ProcessPacket(type, service_id, method_id, call_id, payload, status);
!process_packet_status.ok()) {
PW_LOG_CRITICAL("Failed to process packet in pw::rpc::FakeServer");
PW_LOG_CRITICAL(
@@ -46,25 +50,36 @@ void FakeServer::CheckProcessPacket(internal::PacketType type,
}
}
-Status FakeServer::ProcessPacket(internal::PacketType type,
+Status FakeServer::ProcessPacket(internal::pwpb::PacketType type,
uint32_t service_id,
uint32_t method_id,
+ std::optional<uint32_t> call_id,
ConstByteSpan payload,
Status status) const {
- auto view = internal::test::PacketsView(
- output_.packets(),
- internal::test::PacketFilter(internal::PacketType::REQUEST,
- internal::PacketType::RESPONSE,
- channel_id_,
- service_id,
- method_id));
+ if (!call_id.has_value()) {
+ std::lock_guard lock(output_.mutex_);
+ auto view = internal::test::PacketsView(
+ output_.packets(),
+ internal::test::PacketFilter(internal::pwpb::PacketType::REQUEST,
+ internal::pwpb::PacketType::RESPONSE,
+ channel_id_,
+ service_id,
+ method_id));
- // Re-use the call ID of the most recent packet for this RPC.
- uint32_t call_id = view.empty() ? 0 : view.back().call_id();
+ // Re-use the call ID of the most recent packet for this RPC.
+ if (!view.empty()) {
+ call_id = view.back().call_id();
+ }
+ }
auto packet_encoding_result =
- internal::Packet(
- type, channel_id_, service_id, method_id, call_id, payload, status)
+ internal::Packet(type,
+ channel_id_,
+ service_id,
+ method_id,
+ call_id.value_or(internal::Packet::kUnassignedId),
+ payload,
+ status)
.Encode(packet_buffer_);
PW_CHECK_OK(packet_encoding_result.status());
return client_.ProcessPacket(*packet_encoding_result);
diff --git a/pw_rpc/raw/codegen_test.cc b/pw_rpc/raw/codegen_test.cc
index 3318ac45d..765dfdcda 100644
--- a/pw_rpc/raw/codegen_test.cc
+++ b/pw_rpc/raw/codegen_test.cc
@@ -19,15 +19,20 @@
#include "pw_rpc/internal/hash.h"
#include "pw_rpc/raw/client_testing.h"
#include "pw_rpc/raw/test_method_context.h"
+#include "pw_rpc_test_protos/no_package.raw_rpc.pb.h"
#include "pw_rpc_test_protos/test.pwpb.h"
#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
namespace pw::rpc {
namespace {
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
+namespace TestStreamResponse = ::pw::rpc::test::pwpb::TestStreamResponse;
+
Vector<std::byte, 64> EncodeRequest(int integer, Status status) {
Vector<std::byte, 64> buffer(64);
- test::TestRequest::MemoryEncoder test_request(buffer);
+ TestRequest::MemoryEncoder test_request(buffer);
EXPECT_EQ(OkStatus(), test_request.WriteInteger(integer));
EXPECT_EQ(OkStatus(), test_request.WriteStatusCode(status.code()));
@@ -39,7 +44,7 @@ Vector<std::byte, 64> EncodeRequest(int integer, Status status) {
Vector<std::byte, 64> EncodeResponse(int number) {
Vector<std::byte, 64> buffer(64);
- test::TestStreamResponse::MemoryEncoder test_response(buffer);
+ TestStreamResponse::MemoryEncoder test_response(buffer);
EXPECT_EQ(OkStatus(), test_response.WriteNumber(number));
@@ -69,9 +74,9 @@ class TestService final
TestResponse::MemoryEncoder test_response(response);
EXPECT_EQ(OkStatus(), test_response.WriteValue(integer + 1));
- ASSERT_EQ(OkStatus(),
- responder.Finish(std::span(response).first(test_response.size()),
- status));
+ ASSERT_EQ(
+ OkStatus(),
+ responder.Finish(span(response).first(test_response.size()), status));
}
void TestAnotherUnaryRpc(ConstByteSpan request,
@@ -128,10 +133,10 @@ class TestService final
protobuf::Decoder decoder(request);
while (decoder.Next().ok()) {
switch (static_cast<TestRequest::Fields>(decoder.FieldNumber())) {
- case TestRequest::Fields::INTEGER:
+ case TestRequest::Fields::kInteger:
EXPECT_EQ(OkStatus(), decoder.ReadUint32(&integer));
break;
- case TestRequest::Fields::STATUS_CODE:
+ case TestRequest::Fields::kStatusCode:
break;
default:
ADD_FAILURE();
@@ -151,12 +156,12 @@ class TestService final
while (decoder.Next().ok()) {
switch (static_cast<TestRequest::Fields>(decoder.FieldNumber())) {
- case TestRequest::Fields::INTEGER:
+ case TestRequest::Fields::kInteger:
decode_status = decoder.ReadInt64(&integer);
EXPECT_EQ(OkStatus(), decode_status);
has_integer = decode_status.ok();
break;
- case TestRequest::Fields::STATUS_CODE: {
+ case TestRequest::Fields::kStatusCode: {
uint32_t status_code;
decode_status = decoder.ReadUint32(&status_code);
EXPECT_EQ(OkStatus(), decode_status);
@@ -176,13 +181,29 @@ class TestService final
RawServerReaderWriter last_reader_writer_;
};
+// Test that code generation succeeds when no proto package is specified.
+class NoPackageTestService final
+ : public ::pw_rpc::raw::PwRpcTestService::Service<NoPackageTestService> {
+ public:
+ static void TestUnaryRpc(ConstByteSpan, RawUnaryResponder&) {}
+
+ void TestAnotherUnaryRpc(ConstByteSpan, RawUnaryResponder&) {}
+
+ void TestServerStreamRpc(ConstByteSpan, RawServerWriter&) {}
+
+ void TestClientStreamRpc(RawServerReader&) {}
+
+ void TestBidirectionalStreamRpc(RawServerReaderWriter&) {}
+};
+
} // namespace test
namespace {
TEST(RawCodegen, Server_CompilesProperly) {
test::TestService service;
- EXPECT_EQ(service.id(), internal::Hash("pw.rpc.test.TestService"));
+ EXPECT_EQ(internal::UnwrapServiceId(service.service_id()),
+ internal::Hash("pw.rpc.test.TestService"));
EXPECT_STREQ(service.name(), "TestService");
}
@@ -195,14 +216,14 @@ TEST(RawCodegen, Server_InvokeUnaryRpc) {
protobuf::Decoder decoder(context.response());
while (decoder.Next().ok()) {
- switch (static_cast<test::TestResponse::Fields>(decoder.FieldNumber())) {
- case test::TestResponse::Fields::VALUE: {
+ switch (static_cast<TestResponse::Fields>(decoder.FieldNumber())) {
+ case TestResponse::Fields::kValue: {
int32_t value;
EXPECT_EQ(OkStatus(), decoder.ReadInt32(&value));
EXPECT_EQ(value, 124);
break;
}
- case test::TestResponse::Fields::REPEATED_FIELD:
+ case TestResponse::Fields::kRepeatedField:
break; // Ignore this field
}
}
@@ -217,14 +238,14 @@ TEST(RawCodegen, Server_InvokeAsyncUnaryRpc) {
protobuf::Decoder decoder(context.response());
while (decoder.Next().ok()) {
- switch (static_cast<test::TestResponse::Fields>(decoder.FieldNumber())) {
- case test::TestResponse::Fields::VALUE: {
+ switch (static_cast<TestResponse::Fields>(decoder.FieldNumber())) {
+ case TestResponse::Fields::kValue: {
int32_t value;
EXPECT_EQ(OkStatus(), decoder.ReadInt32(&value));
EXPECT_EQ(value, 124);
break;
}
- case test::TestResponse::Fields::REPEATED_FIELD:
+ case TestResponse::Fields::kRepeatedField:
break; // Ignore this field
}
}
@@ -304,15 +325,14 @@ TEST(RawCodegen, Server_InvokeServerStreamingRpc) {
protobuf::Decoder decoder(context.responses().back());
while (decoder.Next().ok()) {
- switch (
- static_cast<test::TestStreamResponse::Fields>(decoder.FieldNumber())) {
- case test::TestStreamResponse::Fields::NUMBER: {
+ switch (static_cast<TestStreamResponse::Fields>(decoder.FieldNumber())) {
+ case TestStreamResponse::Fields::kNumber: {
int32_t value;
EXPECT_EQ(OkStatus(), decoder.ReadInt32(&value));
EXPECT_EQ(value, 4);
break;
}
- case test::TestStreamResponse::Fields::CHUNK:
+ case TestStreamResponse::Fields::kChunk:
FAIL();
break;
}
@@ -323,12 +343,12 @@ int32_t ReadResponseNumber(ConstByteSpan data) {
int32_t value = -1;
protobuf::Decoder decoder(data);
while (decoder.Next().ok()) {
- switch (
- static_cast<test::TestStreamResponse::Fields>(decoder.FieldNumber())) {
- case test::TestStreamResponse::Fields::NUMBER: {
+ switch (static_cast<TestStreamResponse::Fields>(decoder.FieldNumber())) {
+ case TestStreamResponse::Fields::kNumber: {
EXPECT_EQ(OkStatus(), decoder.ReadInt32(&value));
break;
}
+ case TestStreamResponse::Fields::kChunk:
default:
ADD_FAILURE();
break;
@@ -427,12 +447,12 @@ TEST_F(RawCodegenClientTest, InvokeUnaryRpc_Ok) {
RawUnaryReceiver call = test::pw_rpc::raw::TestService::TestUnaryRpc(
context_.client(),
context_.channel().id(),
- std::as_bytes(std::span("This is the request")),
+ as_bytes(span("This is the request")),
UnaryOnCompleted(),
OnError());
context_.server().SendResponse<test::pw_rpc::raw::TestService::TestUnaryRpc>(
- std::as_bytes(std::span("(ㆆ_ㆆ)")), OkStatus());
+ as_bytes(span("(ㆆ_ㆆ)")), OkStatus());
ASSERT_TRUE(payload_.has_value());
EXPECT_STREQ(payload_.value(), "(ㆆ_ㆆ)");
@@ -442,9 +462,7 @@ TEST_F(RawCodegenClientTest, InvokeUnaryRpc_Ok) {
TEST_F(RawCodegenClientTest, InvokeUnaryRpc_Error) {
RawUnaryReceiver call = service_client_.TestUnaryRpc(
- std::as_bytes(std::span("This is the request")),
- UnaryOnCompleted(),
- OnError());
+ as_bytes(span("This is the request")), UnaryOnCompleted(), OnError());
context_.server()
.SendServerError<test::pw_rpc::raw::TestService::TestUnaryRpc>(
@@ -459,21 +477,21 @@ TEST_F(RawCodegenClientTest, InvokeServerStreamRpc_Ok) {
RawClientReader call = test::pw_rpc::raw::TestService::TestServerStreamRpc(
context_.client(),
context_.channel().id(),
- std::as_bytes(std::span("This is the request")),
+ as_bytes(span("This is the request")),
OnNext(),
OnCompleted(),
OnError());
context_.server()
.SendServerStream<test::pw_rpc::raw::TestService::TestServerStreamRpc>(
- std::as_bytes(std::span("(⌐□_□)")));
+ as_bytes(span("(⌐□_□)")));
ASSERT_TRUE(payload_.has_value());
EXPECT_STREQ(payload_.value(), "(⌐□_□)");
context_.server()
.SendServerStream<test::pw_rpc::raw::TestService::TestServerStreamRpc>(
- std::as_bytes(std::span("(o_O)")));
+ as_bytes(span("(o_O)")));
EXPECT_STREQ(payload_.value(), "(o_O)");
@@ -486,11 +504,11 @@ TEST_F(RawCodegenClientTest, InvokeServerStreamRpc_Ok) {
}
TEST_F(RawCodegenClientTest, InvokeServerStreamRpc_Error) {
- RawClientReader call = service_client_.TestServerStreamRpc(
- std::as_bytes(std::span("This is the request")),
- OnNext(),
- OnCompleted(),
- OnError());
+ RawClientReader call =
+ service_client_.TestServerStreamRpc(as_bytes(span("This is the request")),
+ OnNext(),
+ OnCompleted(),
+ OnError());
context_.server()
.SendServerError<test::pw_rpc::raw::TestService::TestServerStreamRpc>(
@@ -508,7 +526,7 @@ TEST_F(RawCodegenClientTest, InvokeClientStreamRpc_Ok) {
UnaryOnCompleted(),
OnError());
- EXPECT_EQ(OkStatus(), call.Write(std::as_bytes(std::span("(•‿•)"))));
+ EXPECT_EQ(OkStatus(), call.Write(as_bytes(span("(•‿•)"))));
EXPECT_STREQ(
reinterpret_cast<const char*>(
context_.output()
@@ -519,7 +537,7 @@ TEST_F(RawCodegenClientTest, InvokeClientStreamRpc_Ok) {
context_.server()
.SendResponse<test::pw_rpc::raw::TestService::TestClientStreamRpc>(
- std::as_bytes(std::span("(⌐□_□)")), Status::InvalidArgument());
+ as_bytes(span("(⌐□_□)")), Status::InvalidArgument());
ASSERT_TRUE(payload_.has_value());
EXPECT_STREQ(payload_.value(), "(⌐□_□)");
@@ -591,7 +609,7 @@ TEST_F(RawCodegenClientTest, InvokeBidirectionalStreamRpc_Ok) {
OnCompleted(),
OnError());
- EXPECT_EQ(OkStatus(), call.Write(std::as_bytes(std::span("(•‿•)"))));
+ EXPECT_EQ(OkStatus(), call.Write(as_bytes(span("(•‿•)"))));
EXPECT_STREQ(
reinterpret_cast<const char*>(
context_.output()
@@ -604,7 +622,7 @@ TEST_F(RawCodegenClientTest, InvokeBidirectionalStreamRpc_Ok) {
context_.server()
.SendServerStream<
test::pw_rpc::raw::TestService::TestBidirectionalStreamRpc>(
- std::as_bytes(std::span("(⌐□_□)")));
+ as_bytes(span("(⌐□_□)")));
ASSERT_TRUE(payload_.has_value());
EXPECT_STREQ(payload_.value(), "(⌐□_□)");
diff --git a/pw_rpc/raw/method.cc b/pw_rpc/raw/method.cc
index 11ae69642..ac7206930 100644
--- a/pw_rpc/raw/method.cc
+++ b/pw_rpc/raw/method.cc
@@ -21,29 +21,10 @@
namespace pw::rpc::internal {
-void RawMethod::SynchronousUnaryInvoker(const CallContext& context,
- const Packet& request) {
- RawUnaryResponder responder(context);
- rpc_lock().unlock();
- // TODO(hepler): Remove support for raw synchronous unary methods. Unlike
- // synchronous Nanopb methods, they provide little value compared to
- // asynchronous unary methods. For now, just provide a fixed buffer on the
- // stack.
- std::byte payload_buffer[64] = {};
-
- StatusWithSize sws =
- static_cast<const RawMethod&>(context.method())
- .function_.synchronous_unary(
- context.service(), request.payload(), std::span(payload_buffer));
-
- responder.Finish(std::span(payload_buffer, sws.size()), sws.status())
- .IgnoreError();
-}
-
void RawMethod::AsynchronousUnaryInvoker(const CallContext& context,
const Packet& request) {
- RawUnaryResponder responder(context);
- rpc_lock().unlock();
+ RawUnaryResponder responder(context.ClaimLocked());
+ context.server().CleanUpCalls();
static_cast<const RawMethod&>(context.method())
.function_.asynchronous_unary(
context.service(), request.payload(), responder);
@@ -51,8 +32,8 @@ void RawMethod::AsynchronousUnaryInvoker(const CallContext& context,
void RawMethod::ServerStreamingInvoker(const CallContext& context,
const Packet& request) {
- RawServerWriter server_writer(context);
- rpc_lock().unlock();
+ RawServerWriter server_writer(context.ClaimLocked());
+ context.server().CleanUpCalls();
static_cast<const RawMethod&>(context.method())
.function_.server_streaming(
context.service(), request.payload(), server_writer);
@@ -60,16 +41,16 @@ void RawMethod::ServerStreamingInvoker(const CallContext& context,
void RawMethod::ClientStreamingInvoker(const CallContext& context,
const Packet&) {
- RawServerReader reader(context);
- rpc_lock().unlock();
+ RawServerReader reader(context.ClaimLocked());
+ context.server().CleanUpCalls();
static_cast<const RawMethod&>(context.method())
.function_.stream_request(context.service(), reader);
}
void RawMethod::BidirectionalStreamingInvoker(const CallContext& context,
const Packet&) {
- RawServerReaderWriter reader_writer(context);
- rpc_lock().unlock();
+ RawServerReaderWriter reader_writer(context.ClaimLocked());
+ context.server().CleanUpCalls();
static_cast<const RawMethod&>(context.method())
.function_.stream_request(context.service(), reader_writer);
}
diff --git a/pw_rpc/raw/method_test.cc b/pw_rpc/raw/method_test.cc
index 43382dcbf..f2facba54 100644
--- a/pw_rpc/raw/method_test.cc
+++ b/pw_rpc/raw/method_test.cc
@@ -18,6 +18,7 @@
#include "gtest/gtest.h"
#include "pw_bytes/array.h"
+#include "pw_containers/algorithm.h"
#include "pw_protobuf/decoder.h"
#include "pw_protobuf/encoder.h"
#include "pw_rpc/internal/config.h"
@@ -30,8 +31,8 @@
namespace pw::rpc::internal {
namespace {
-namespace TestRequest = ::pw::rpc::test::TestRequest;
-namespace TestResponse = ::pw::rpc::test::TestResponse;
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
// Create a fake service for use with the MethodImplTester.
class TestRawService final : public Service {
@@ -116,7 +117,7 @@ class FakeService : public FakeServiceBase<FakeService> {
ConstByteSpan payload(test_response);
ASSERT_EQ(OkStatus(),
- responder.Finish(std::span(response).first(payload.size()),
+ responder.Finish(span(response).first(payload.size()),
Status::Unauthenticated()));
}
@@ -141,15 +142,15 @@ class FakeService : public FakeServiceBase<FakeService> {
static_cast<TestRequest::Fields>(decoder.FieldNumber());
switch (field) {
- case TestRequest::Fields::INTEGER:
+ case TestRequest::Fields::kInteger:
ASSERT_EQ(OkStatus(), decoder.ReadInt64(&last_request.integer));
break;
- case TestRequest::Fields::STATUS_CODE:
+ case TestRequest::Fields::kStatusCode:
ASSERT_EQ(OkStatus(), decoder.ReadUint32(&last_request.status_code));
break;
}
}
- };
+ }
struct {
int64_t integer;
@@ -203,7 +204,7 @@ TEST(RawMethod, AsyncUnaryRpc0_SendsResponse) {
kAsyncUnary0.Invoke(context.get(), context.request({}));
const Packet& packet = context.output().last_packet();
- EXPECT_EQ(PacketType::RESPONSE, packet.type());
+ EXPECT_EQ(pwpb::PacketType::RESPONSE, packet.type());
EXPECT_EQ(Status::Unknown(), packet.status());
EXPECT_EQ(context.service_id(), packet.service_id());
EXPECT_EQ(kAsyncUnary0.id(), packet.method_id());
@@ -238,11 +239,10 @@ TEST(RawMethod, ServerReader_HandlesRequests) {
constexpr const char kRequestValue[] = "This is a request payload!!!";
std::array<std::byte, 128> encoded_request = {};
- auto encoded = context.client_stream(std::as_bytes(std::span(kRequestValue)))
+ auto encoded = context.client_stream(as_bytes(span(kRequestValue)))
.Encode(encoded_request);
ASSERT_EQ(OkStatus(), encoded.status());
- ASSERT_EQ(OkStatus(),
- context.server().ProcessPacket(*encoded, context.output()));
+ ASSERT_EQ(OkStatus(), context.server().ProcessPacket(*encoded));
EXPECT_STREQ(reinterpret_cast<const char*>(request.data()), kRequestValue);
}
@@ -253,7 +253,7 @@ TEST(RawMethod, ServerReaderWriter_WritesResponses) {
kBidirectionalStream.Invoke(context.get(), context.request({}));
constexpr const char kRequestValue[] = "O_o";
- const auto kRequestBytes = std::as_bytes(std::span(kRequestValue));
+ const auto kRequestBytes = as_bytes(span(kRequestValue));
EXPECT_EQ(OkStatus(),
context.service().last_reader_writer.Write(kRequestBytes));
@@ -262,10 +262,7 @@ TEST(RawMethod, ServerReaderWriter_WritesResponses) {
ASSERT_EQ(OkStatus(), encoded.status());
ConstByteSpan sent_payload = context.output().last_packet().payload();
- EXPECT_TRUE(std::equal(kRequestBytes.begin(),
- kRequestBytes.end(),
- sent_payload.begin(),
- sent_payload.end()));
+ EXPECT_TRUE(pw::containers::Equal(kRequestBytes, sent_payload));
}
TEST(RawServerWriter, Write_SendsPayload) {
@@ -277,7 +274,7 @@ TEST(RawServerWriter, Write_SendsPayload) {
EXPECT_EQ(context.service().last_writer.Write(data), OkStatus());
const internal::Packet& packet = context.output().last_packet();
- EXPECT_EQ(packet.type(), internal::PacketType::SERVER_STREAM);
+ EXPECT_EQ(packet.type(), pwpb::PacketType::SERVER_STREAM);
EXPECT_EQ(packet.channel_id(), context.channel_id());
EXPECT_EQ(packet.service_id(), context.service_id());
EXPECT_EQ(packet.method_id(), context.get().method().id());
@@ -293,7 +290,7 @@ TEST(RawServerWriter, Write_EmptyBuffer) {
ASSERT_EQ(context.service().last_writer.Write({}), OkStatus());
const internal::Packet& packet = context.output().last_packet();
- EXPECT_EQ(packet.type(), internal::PacketType::SERVER_STREAM);
+ EXPECT_EQ(packet.type(), pwpb::PacketType::SERVER_STREAM);
EXPECT_EQ(packet.channel_id(), context.channel_id());
EXPECT_EQ(packet.service_id(), context.service_id());
EXPECT_EQ(packet.method_id(), context.get().method().id());
@@ -313,6 +310,12 @@ TEST(RawServerWriter, Write_Closed_ReturnsFailedPrecondition) {
}
TEST(RawServerWriter, Write_PayloadTooLargeForEncodingBuffer_ReturnsInternal) {
+ // The payload is never too large for the encoding buffer when dynamic
+ // allocation is enabled.
+#if PW_RPC_DYNAMIC_ALLOCATION
+ GTEST_SKIP();
+#endif // !PW_RPC_DYNAMIC_ALLOCATION
+
ServerContextForTest<FakeService> context(kServerStream);
rpc_lock().lock();
kServerStream.Invoke(context.get(), context.request({}));
diff --git a/pw_rpc/raw/method_union_test.cc b/pw_rpc/raw/method_union_test.cc
index a46478783..12428dcbb 100644
--- a/pw_rpc/raw/method_union_test.cc
+++ b/pw_rpc/raw/method_union_test.cc
@@ -27,8 +27,8 @@
namespace pw::rpc::internal {
namespace {
-namespace TestRequest = ::pw::rpc::test::TestRequest;
-namespace TestResponse = ::pw::rpc::test::TestResponse;
+namespace TestRequest = ::pw::rpc::test::pwpb::TestRequest;
+namespace TestResponse = ::pw::rpc::test::pwpb::TestResponse;
template <typename Implementation>
class FakeGeneratedService : public Service {
@@ -58,7 +58,7 @@ class FakeGeneratedServiceImpl
ASSERT_EQ(OkStatus(), test_response.WriteValue(last_request.integer + 5));
ASSERT_EQ(OkStatus(),
- responder.Finish(std::span(response).first(test_response.size()),
+ responder.Finish(span(response).first(test_response.size()),
Status::Unauthenticated()));
}
@@ -82,13 +82,11 @@ class FakeGeneratedServiceImpl
static_cast<TestRequest::Fields>(decoder.FieldNumber());
switch (field) {
- case TestRequest::Fields::INTEGER:
- decoder.ReadInt64(&last_request.integer)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ case TestRequest::Fields::kInteger:
+ ASSERT_EQ(OkStatus(), decoder.ReadInt64(&last_request.integer));
break;
- case TestRequest::Fields::STATUS_CODE:
- decoder.ReadUint32(&last_request.status_code)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ case TestRequest::Fields::kStatusCode:
+ ASSERT_EQ(OkStatus(), decoder.ReadUint32(&last_request.status_code));
break;
}
}
@@ -99,10 +97,8 @@ TEST(RawMethodUnion, InvokesUnary) {
std::byte buffer[16];
TestRequest::MemoryEncoder test_request(buffer);
- test_request.WriteInteger(456)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- test_request.WriteStatusCode(7)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), test_request.WriteInteger(456));
+ ASSERT_EQ(OkStatus(), test_request.WriteStatusCode(7));
const Method& method =
std::get<1>(FakeGeneratedServiceImpl::kMethods).method();
@@ -127,10 +123,8 @@ TEST(RawMethodUnion, InvokesServerStreaming) {
std::byte buffer[16];
TestRequest::MemoryEncoder test_request(buffer);
- test_request.WriteInteger(777)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- test_request.WriteStatusCode(2)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), test_request.WriteInteger(777));
+ ASSERT_EQ(OkStatus(), test_request.WriteStatusCode(2));
const Method& method =
std::get<2>(FakeGeneratedServiceImpl::kMethods).method();
diff --git a/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h b/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
index 5e21a59b6..2d780279b 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/client_reader_writer.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -38,6 +38,8 @@ class RawClientReaderWriter : private internal::StreamResponseClientCall {
using internal::Call::active;
using internal::Call::channel_id;
+ using internal::ClientCall::id;
+
// Functions for setting the callbacks.
using internal::StreamResponseClientCall::set_on_completed;
using internal::StreamResponseClientCall::set_on_error;
@@ -47,25 +49,35 @@ class RawClientReaderWriter : private internal::StreamResponseClientCall {
using internal::Call::Write;
// Notifies the server that no further client stream messages will be sent.
- using internal::Call::CloseClientStream;
+ using internal::ClientCall::CloseClientStream;
- // Cancels this RPC.
+ // Cancels this RPC. Closes the call locally and sends a CANCELLED error to
+ // the server.
using internal::Call::Cancel;
+ // Closes this RPC locally. Sends a CLIENT_STREAM_END, but no cancellation
+ // packet. Future packets for this RPC are dropped, and the client sends a
+ // FAILED_PRECONDITION error in response because the call is not active.
+ using internal::ClientCall::Abandon;
+
// Allow use as a generic RPC Writer.
using internal::Call::operator Writer&;
using internal::Call::operator const Writer&;
- protected:
+ private:
friend class internal::StreamResponseClientCall;
- RawClientReaderWriter(internal::Endpoint& client,
+ RawClientReaderWriter(internal::LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
- uint32_t method_id,
- MethodType type = MethodType::kBidirectionalStreaming)
+ uint32_t method_id)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: StreamResponseClientCall(
- client, channel_id, service_id, method_id, type) {}
+ client,
+ channel_id,
+ service_id,
+ method_id,
+ RawCallProps(MethodType::kBidirectionalStreaming)) {}
};
// Handles responses for a server streaming RPC.
@@ -84,19 +96,21 @@ class RawClientReader : private internal::StreamResponseClientCall {
using internal::StreamResponseClientCall::set_on_next;
using internal::Call::Cancel;
+ using internal::ClientCall::Abandon;
private:
friend class internal::StreamResponseClientCall;
- RawClientReader(internal::Endpoint& client,
+ RawClientReader(internal::LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: StreamResponseClientCall(client,
channel_id,
service_id,
method_id,
- MethodType::kServerStreaming) {}
+ RawCallProps(MethodType::kServerStreaming)) {}
};
// Sends requests and handles the response for a client streaming RPC.
@@ -116,6 +130,7 @@ class RawClientWriter : private internal::UnaryResponseClientCall {
using internal::Call::Cancel;
using internal::Call::CloseClientStream;
using internal::Call::Write;
+ using internal::ClientCall::Abandon;
// Allow use as a generic RPC Writer.
using internal::Call::operator Writer&;
@@ -124,15 +139,16 @@ class RawClientWriter : private internal::UnaryResponseClientCall {
private:
friend class internal::UnaryResponseClientCall;
- RawClientWriter(internal::Endpoint& client,
+ RawClientWriter(internal::LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: UnaryResponseClientCall(client,
channel_id,
service_id,
method_id,
- MethodType::kClientStreaming) {}
+ RawCallProps(MethodType::kClientStreaming)) {}
};
// Handles the response for to unary RPC.
@@ -149,17 +165,22 @@ class RawUnaryReceiver : private internal::UnaryResponseClientCall {
using internal::UnaryResponseClientCall::set_on_completed;
using internal::UnaryResponseClientCall::set_on_error;
+ using internal::ClientCall::Abandon;
using internal::UnaryResponseClientCall::Cancel;
private:
friend class internal::UnaryResponseClientCall;
- RawUnaryReceiver(internal::Endpoint& client,
+ RawUnaryReceiver(internal::LockedEndpoint& client,
uint32_t channel_id,
uint32_t service_id,
uint32_t method_id)
- : UnaryResponseClientCall(
- client, channel_id, service_id, method_id, MethodType::kUnary) {}
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : UnaryResponseClientCall(client,
+ channel_id,
+ service_id,
+ method_id,
+ RawCallProps(MethodType::kUnary)) {}
};
} // namespace pw::rpc
diff --git a/pw_rpc/raw/public/pw_rpc/raw/client_testing.h b/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
index 436e59e8e..dbda24d58 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/client_testing.h
@@ -26,7 +26,7 @@
namespace pw::rpc {
-// TODO(pwbug/477): Document the client testing APIs.
+// TODO(b/234878467): Document the client testing APIs.
// Sends packets to an RPC client as if it were a pw_rpc server.
class FakeServer {
@@ -45,8 +45,10 @@ class FakeServer {
template <auto kMethod,
typename = std::enable_if_t<
HasServerStream(internal::MethodInfo<kMethod>::kType)>>
- void SendResponse(Status status) const {
- SendPacket<kMethod>(internal::PacketType::RESPONSE, {}, status);
+ void SendResponse(Status status,
+ std::optional<uint32_t> call_id = std::nullopt) const {
+ SendPacket<kMethod>(
+ internal::pwpb::PacketType::RESPONSE, {}, status, call_id);
}
// Sends a response packet for a unary or client streaming streaming RPC to
@@ -54,45 +56,57 @@ class FakeServer {
template <auto kMethod,
typename = std::enable_if_t<
!HasServerStream(internal::MethodInfo<kMethod>::kType)>>
- void SendResponse(ConstByteSpan payload, Status status) const {
- SendPacket<kMethod>(internal::PacketType::RESPONSE, payload, status);
+ void SendResponse(ConstByteSpan payload,
+ Status status,
+ std::optional<uint32_t> call_id = std::nullopt) const {
+ SendPacket<kMethod>(
+ internal::pwpb::PacketType::RESPONSE, payload, status, call_id);
}
// Sends a stream packet for a server or bidirectional streaming RPC to the
// client.
template <auto kMethod>
- void SendServerStream(ConstByteSpan payload) const {
+ void SendServerStream(ConstByteSpan payload,
+ std::optional<uint32_t> call_id = std::nullopt) const {
static_assert(HasServerStream(internal::MethodInfo<kMethod>::kType),
"Only server and bidirectional streaming methods can receive "
"server stream packets");
- SendPacket<kMethod>(internal::PacketType::SERVER_STREAM, payload);
+ SendPacket<kMethod>(internal::pwpb::PacketType::SERVER_STREAM,
+ payload,
+ OkStatus(),
+ call_id);
}
// Sends a server error packet to the client.
template <auto kMethod>
- void SendServerError(Status error) const {
- SendPacket<kMethod>(internal::PacketType::SERVER_ERROR, {}, error);
+ void SendServerError(Status error,
+ std::optional<uint32_t> call_id = std::nullopt) const {
+ SendPacket<kMethod>(
+ internal::pwpb::PacketType::SERVER_ERROR, {}, error, call_id);
}
private:
template <auto kMethod>
- void SendPacket(internal::PacketType type,
- ConstByteSpan payload = {},
- Status status = OkStatus()) const {
+ void SendPacket(internal::pwpb::PacketType type,
+ ConstByteSpan payload,
+ Status status,
+ std::optional<uint32_t> call_id) const {
using Info = internal::MethodInfo<kMethod>;
CheckProcessPacket(
- type, Info::kServiceId, Info::kMethodId, payload, status);
+ type, Info::kServiceId, Info::kMethodId, call_id, payload, status);
}
- void CheckProcessPacket(internal::PacketType type,
+ void CheckProcessPacket(internal::pwpb::PacketType type,
uint32_t service_id,
uint32_t method_id,
+ std::optional<uint32_t> call_id,
ConstByteSpan payload,
Status status) const;
- Status ProcessPacket(internal::PacketType type,
+ Status ProcessPacket(internal::pwpb::PacketType type,
uint32_t service_id,
uint32_t method_id,
+ std::optional<uint32_t> call_id,
ConstByteSpan payload,
Status status) const;
@@ -114,7 +128,7 @@ class RawClientTestContext {
constexpr RawClientTestContext()
: channel_(Channel::Create<kDefaultChannelId>(&channel_output_)),
- client_(std::span(&channel_, 1)),
+ client_(span(&channel_, 1)),
packet_buffer_{},
fake_server_(
channel_output_, client_, kDefaultChannelId, packet_buffer_) {}
diff --git a/pw_rpc/raw/public/pw_rpc/raw/internal/method.h b/pw_rpc/raw/public/pw_rpc/raw/internal/method.h
index 0ab022222..0d02d4a1c 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/internal/method.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/internal/method.h
@@ -115,10 +115,6 @@ class RawMethod : public Method {
constexpr RawMethod(uint32_t id, Invoker invoker, Function function)
: Method(id, invoker), function_(function) {}
- static void SynchronousUnaryInvoker(const CallContext& context,
- const Packet& request)
- PW_UNLOCK_FUNCTION(rpc_lock());
-
static void AsynchronousUnaryInvoker(const CallContext& context,
const Packet& request)
PW_UNLOCK_FUNCTION(rpc_lock());
diff --git a/pw_rpc/raw/public/pw_rpc/raw/internal/method_union.h b/pw_rpc/raw/public/pw_rpc/raw/internal/method_union.h
index d3eb2bc88..4158b0be7 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/internal/method_union.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/internal/method_union.h
@@ -45,6 +45,6 @@ constexpr RawMethod GetRawMethodFor(uint32_t id) {
} else {
return InvalidMethod<kMethod, kType, RawMethod>(id);
}
-};
+}
} // namespace pw::rpc::internal
diff --git a/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h b/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
index 8ff018e55..4de87fc03 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/server_reader_writer.h
@@ -57,14 +57,16 @@ class RawServerReaderWriter : private internal::ServerCall {
template <auto kMethod, typename ServiceImpl>
[[nodiscard]] static RawServerReaderWriter Open(Server& server,
uint32_t channel_id,
- ServiceImpl& service) {
- internal::LockGuard lock(internal::rpc_lock());
- return {server.OpenContext<kMethod, MethodType::kBidirectionalStreaming>(
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ return server.OpenCall<RawServerReaderWriter,
+ kMethod,
+ MethodType::kBidirectionalStreaming>(
channel_id,
service,
internal::MethodLookup::GetRawMethod<
ServiceImpl,
- internal::MethodInfo<kMethod>::kMethodId>())};
+ internal::MethodInfo<kMethod>::kMethodId>());
}
using internal::Call::active;
@@ -87,17 +89,26 @@ class RawServerReaderWriter : private internal::ServerCall {
using internal::Call::operator const Writer&;
protected:
- RawServerReaderWriter(const internal::CallContext& context,
- MethodType type = MethodType::kBidirectionalStreaming)
- : internal::ServerCall(context, type) {}
+ RawServerReaderWriter(const internal::LockedCallContext& context,
+ MethodType type)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
+ : internal::ServerCall(
+ context,
+ internal::CallProperties(
+ type, internal::kServerCall, internal::kRawProto)) {}
using internal::Call::CloseAndSendResponse;
private:
friend class internal::RawMethod; // Needed to construct
+ friend class Server;
template <typename, typename, uint32_t>
friend class internal::test::InvocationContext;
+
+ // Private constructor for test use
+ RawServerReaderWriter(const internal::LockedCallContext& context)
+ : RawServerReaderWriter(context, MethodType::kBidirectionalStreaming) {}
};
// The RawServerReader is used to receive messages and send a response in a
@@ -110,14 +121,15 @@ class RawServerReader : private RawServerReaderWriter {
template <auto kMethod, typename ServiceImpl>
[[nodiscard]] static RawServerReader Open(Server& server,
uint32_t channel_id,
- ServiceImpl& service) {
- internal::LockGuard lock(internal::rpc_lock());
- return {server.OpenContext<kMethod, MethodType::kClientStreaming>(
- channel_id,
- service,
- internal::MethodLookup::GetRawMethod<
- ServiceImpl,
- internal::MethodInfo<kMethod>::kMethodId>())};
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ return server
+ .OpenCall<RawServerReader, kMethod, MethodType::kClientStreaming>(
+ channel_id,
+ service,
+ internal::MethodLookup::GetRawMethod<
+ ServiceImpl,
+ internal::MethodInfo<kMethod>::kMethodId>());
}
constexpr RawServerReader() = default;
@@ -138,11 +150,13 @@ class RawServerReader : private RawServerReaderWriter {
private:
friend class internal::RawMethod; // Needed for conversions from ReaderWriter
+ friend class Server;
template <typename, typename, uint32_t>
friend class internal::test::InvocationContext;
- RawServerReader(const internal::CallContext& context)
+ RawServerReader(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: RawServerReaderWriter(context, MethodType::kClientStreaming) {}
};
@@ -155,14 +169,15 @@ class RawServerWriter : private RawServerReaderWriter {
template <auto kMethod, typename ServiceImpl>
[[nodiscard]] static RawServerWriter Open(Server& server,
uint32_t channel_id,
- ServiceImpl& service) {
- internal::LockGuard lock(internal::rpc_lock());
- return {server.OpenContext<kMethod, MethodType::kServerStreaming>(
- channel_id,
- service,
- internal::MethodLookup::GetRawMethod<
- ServiceImpl,
- internal::MethodInfo<kMethod>::kMethodId>())};
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ return server
+ .OpenCall<RawServerWriter, kMethod, MethodType::kServerStreaming>(
+ channel_id,
+ service,
+ internal::MethodLookup::GetRawMethod<
+ ServiceImpl,
+ internal::MethodInfo<kMethod>::kMethodId>());
}
constexpr RawServerWriter() = default;
@@ -183,12 +198,14 @@ class RawServerWriter : private RawServerReaderWriter {
using internal::Call::operator const Writer&;
private:
+ friend class internal::RawMethod;
+ friend class Server;
+
template <typename, typename, uint32_t>
friend class internal::test::InvocationContext;
- friend class internal::RawMethod;
-
- RawServerWriter(const internal::CallContext& context)
+ RawServerWriter(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: RawServerReaderWriter(context, MethodType::kServerStreaming) {}
};
@@ -201,14 +218,14 @@ class RawUnaryResponder : private RawServerReaderWriter {
template <auto kMethod, typename ServiceImpl>
[[nodiscard]] static RawUnaryResponder Open(Server& server,
uint32_t channel_id,
- ServiceImpl& service) {
- internal::LockGuard lock(internal::rpc_lock());
- return {server.OpenContext<kMethod, MethodType::kUnary>(
+ ServiceImpl& service)
+ PW_LOCKS_EXCLUDED(internal::rpc_lock()) {
+ return server.OpenCall<RawUnaryResponder, kMethod, MethodType::kUnary>(
channel_id,
service,
internal::MethodLookup::GetRawMethod<
ServiceImpl,
- internal::MethodInfo<kMethod>::kMethodId>())};
+ internal::MethodInfo<kMethod>::kMethodId>());
}
constexpr RawUnaryResponder() = default;
@@ -226,12 +243,14 @@ class RawUnaryResponder : private RawServerReaderWriter {
}
private:
+ friend class internal::RawMethod;
+ friend class Server;
+
template <typename, typename, uint32_t>
friend class internal::test::InvocationContext;
- friend class internal::RawMethod;
-
- RawUnaryResponder(const internal::CallContext& context)
+ RawUnaryResponder(const internal::LockedCallContext& context)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(internal::rpc_lock())
: RawServerReaderWriter(context, MethodType::kUnary) {}
};
diff --git a/pw_rpc/raw/public/pw_rpc/raw/test_method_context.h b/pw_rpc/raw/public/pw_rpc/raw/test_method_context.h
index e91306604..fca027b5d 100644
--- a/pw_rpc/raw/public/pw_rpc/raw/test_method_context.h
+++ b/pw_rpc/raw/public/pw_rpc/raw/test_method_context.h
@@ -137,10 +137,9 @@ class UnaryContext
auto responder = Base::template GetResponder<RawUnaryResponder>();
std::byte response[kSynchronousResponseBufferSizeBytes] = {};
auto sws = CallMethodImplFunction<kMethod>(
- Base::service(), request, std::span(response));
- PW_ASSERT(
- responder.Finish(std::span(response).first(sws.size()), sws.status())
- .ok());
+ Base::service(), request, span(response));
+ PW_ASSERT(responder.Finish(span(response).first(sws.size()), sws.status())
+ .ok());
return sws;
} else {
Base::template call<kMethod, RawUnaryResponder>(request);
diff --git a/pw_rpc/raw/server_reader_writer_test.cc b/pw_rpc/raw/server_reader_writer_test.cc
index d6a35f803..cadc3d81f 100644
--- a/pw_rpc/raw/server_reader_writer_test.cc
+++ b/pw_rpc/raw/server_reader_writer_test.cc
@@ -14,6 +14,8 @@
#include "pw_rpc/raw/server_reader_writer.h"
+#include <optional>
+
#include "gtest/gtest.h"
#include "pw_rpc/internal/lock.h"
#include "pw_rpc/raw/fake_channel_output.h"
@@ -42,7 +44,7 @@ struct ReaderWriterTestContext {
ReaderWriterTestContext()
: channel(Channel::Create<kChannelId>(&output)),
- server(std::span(&channel, 1)) {}
+ server(span(&channel, 1)) {}
TestServiceImpl service;
RawFakeChannelOutput<4> output;
@@ -169,8 +171,7 @@ TEST(RawUnaryResponder, Open_ReturnsUsableResponder) {
ctx.server, ctx.channel.id(), ctx.service);
EXPECT_EQ(call.channel_id(), ctx.channel.id());
- EXPECT_EQ(OkStatus(),
- call.Finish(std::as_bytes(std::span("hello from pw_rpc"))));
+ EXPECT_EQ(OkStatus(), call.Finish(as_bytes(span("hello from pw_rpc"))));
EXPECT_STREQ(
reinterpret_cast<const char*>(
@@ -206,6 +207,9 @@ TEST(RawUnaryResponder, Open_MultipleTimes_CancelsPrevious) {
RawUnaryResponder one = RawUnaryResponder::Open<TestService::TestUnaryRpc>(
ctx.server, ctx.channel.id(), ctx.service);
+ std::optional<Status> error;
+ one.set_on_error([&error](Status status) { error = status; });
+
ASSERT_TRUE(one.active());
RawUnaryResponder two = RawUnaryResponder::Open<TestService::TestUnaryRpc>(
@@ -213,6 +217,8 @@ TEST(RawUnaryResponder, Open_MultipleTimes_CancelsPrevious) {
ASSERT_FALSE(one.active());
ASSERT_TRUE(two.active());
+
+ EXPECT_EQ(Status::Cancelled(), error);
}
TEST(RawServerWriter, Open_ReturnsUsableWriter) {
@@ -222,7 +228,7 @@ TEST(RawServerWriter, Open_ReturnsUsableWriter) {
ctx.server, ctx.channel.id(), ctx.service);
EXPECT_EQ(call.channel_id(), ctx.channel.id());
- EXPECT_EQ(OkStatus(), call.Write(std::as_bytes(std::span("321"))));
+ EXPECT_EQ(OkStatus(), call.Write(as_bytes(span("321"))));
EXPECT_STREQ(reinterpret_cast<const char*>(
ctx.output.payloads<TestService::TestServerStreamRpc>()
@@ -238,8 +244,7 @@ TEST(RawServerReader, Open_ReturnsUsableReader) {
ctx.server, ctx.channel.id(), ctx.service);
EXPECT_EQ(call.channel_id(), ctx.channel.id());
- EXPECT_EQ(OkStatus(),
- call.Finish(std::as_bytes(std::span("This is a message"))));
+ EXPECT_EQ(OkStatus(), call.Finish(as_bytes(span("This is a message"))));
EXPECT_STREQ(reinterpret_cast<const char*>(
ctx.output.payloads<TestService::TestClientStreamRpc>()
@@ -255,7 +260,7 @@ TEST(RawServerReaderWriter, Open_ReturnsUsableReaderWriter) {
ctx.server, ctx.channel.id(), ctx.service);
EXPECT_EQ(call.channel_id(), ctx.channel.id());
- EXPECT_EQ(OkStatus(), call.Write(std::as_bytes(std::span("321"))));
+ EXPECT_EQ(OkStatus(), call.Write(as_bytes(span("321"))));
EXPECT_STREQ(
reinterpret_cast<const char*>(
@@ -318,9 +323,9 @@ TEST(RawUnaryResponder, ReplaceActiveCall_DoesNotFinishCall) {
ASSERT_TRUE(ctx.output.completions<TestService::TestUnaryRpc>().empty());
constexpr const char kData[] = "Some data!";
- EXPECT_EQ(OkStatus(),
- active_call.Finish(std::as_bytes(std::span(kData)),
- Status::InvalidArgument()));
+ EXPECT_EQ(
+ OkStatus(),
+ active_call.Finish(as_bytes(span(kData)), Status::InvalidArgument()));
EXPECT_STREQ(
reinterpret_cast<const char*>(
@@ -378,7 +383,7 @@ TEST(RawServerWriter, ReplaceActiveCall_DoesNotFinishCall) {
ctx.output.completions<TestService::TestServerStreamRpc>().empty());
constexpr const char kData[] = "Some data!";
- EXPECT_EQ(OkStatus(), active_call.Write(std::as_bytes(std::span(kData))));
+ EXPECT_EQ(OkStatus(), active_call.Write(as_bytes(span(kData))));
EXPECT_STREQ(reinterpret_cast<const char*>(
ctx.output.payloads<TestService::TestServerStreamRpc>()
@@ -393,7 +398,7 @@ void WriteAsWriter(Writer& writer) {
ASSERT_TRUE(writer.active());
ASSERT_EQ(writer.channel_id(), ReaderWriterTestContext::kChannelId);
- EXPECT_EQ(OkStatus(), writer.Write(std::as_bytes(std::span(kWriterData))));
+ EXPECT_EQ(OkStatus(), writer.Write(as_bytes(span(kWriterData))));
}
TEST(RawServerWriter, UsableAsWriter) {
diff --git a/pw_rpc/raw/service_nc_test.cc b/pw_rpc/raw/service_nc_test.cc
new file mode 100644
index 000000000..695f807be
--- /dev/null
+++ b/pw_rpc/raw/service_nc_test.cc
@@ -0,0 +1,38 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_compilation_testing/negative_compilation.h"
+#include "pw_rpc_test_protos/test.raw_rpc.pb.h"
+
+namespace pw::rpc {
+namespace test {
+
+#if PW_NC_TEST(NoMethods)
+PW_NC_EXPECT("TestUnaryRpc");
+
+class TestService final
+ : public pw_rpc::raw::TestService::Service<TestService> {
+ public:
+};
+
+#else
+
+class TestService {};
+
+#endif // PW_NC_TEST
+
+TestService test_service;
+
+} // namespace test
+} // namespace pw::rpc
diff --git a/pw_rpc/server.cc b/pw_rpc/server.cc
index d7634ab6b..4af9ff4e6 100644
--- a/pw_rpc/server.cc
+++ b/pw_rpc/server.cc
@@ -23,23 +23,21 @@
#include "pw_log/log.h"
#include "pw_rpc/internal/endpoint.h"
#include "pw_rpc/internal/packet.h"
+#include "pw_rpc/service_id.h"
namespace pw::rpc {
namespace {
using internal::Packet;
-using internal::PacketType;
+using internal::pwpb::PacketType;
} // namespace
-Status Server::ProcessPacket(ConstByteSpan packet_data,
- ChannelOutput* interface) {
+Status Server::ProcessPacket(ConstByteSpan packet_data) {
PW_TRY_ASSIGN(Packet packet,
Endpoint::ProcessPacket(packet_data, Packet::kServer));
internal::rpc_lock().lock();
- internal::ServerCall* const call =
- static_cast<internal::ServerCall*>(FindCall(packet));
// Verbose log for debugging.
// PW_LOG_DEBUG("RPC server received packet type %u for %u:%08x/%08x",
@@ -50,15 +48,6 @@ Status Server::ProcessPacket(ConstByteSpan packet_data,
internal::Channel* channel = GetInternalChannel(packet.channel_id());
if (channel == nullptr) {
- // If an interface was provided, respond with a SERVER_ERROR to indicate
- // that the channel is not available on this server. Don't send responses to
- // error messages, though, to avoid potential infinite cycles.
- if (interface != nullptr && packet.type() != PacketType::CLIENT_ERROR) {
- internal::Channel(packet.channel_id(), interface)
- .Send(Packet::ServerError(packet, Status::Unavailable()))
- .IgnoreError();
- }
-
internal::rpc_lock().unlock();
PW_LOG_WARN("RPC server received packet for unknown channel %u",
static_cast<unsigned>(packet.channel_id()));
@@ -74,24 +63,30 @@ Status Server::ProcessPacket(ConstByteSpan packet_data,
.IgnoreError();
}
internal::rpc_lock().unlock();
+ PW_LOG_DEBUG("Received packet on channel %u for unknown RPC %08x/%08x",
+ static_cast<unsigned>(packet.channel_id()),
+ static_cast<unsigned>(packet.service_id()),
+ static_cast<unsigned>(packet.method_id()));
return OkStatus(); // OK since the packet was handled.
}
+ // Handle request packets separately to avoid an unnecessary call lookup. The
+ // Call constructor looks up and cancels any duplicate calls.
+ if (packet.type() == PacketType::REQUEST) {
+ const internal::CallContext context(
+ *this, packet.channel_id(), *service, *method, packet.call_id());
+ method->Invoke(context, packet);
+ return OkStatus();
+ }
+
+ IntrusiveList<internal::Call>::iterator call = FindCall(packet);
+
switch (packet.type()) {
- case PacketType::REQUEST: {
- // If the REQUEST is for an ongoing RPC, the existing call will be
- // cancelled when the new call object is created.
- const internal::CallContext context(
- *this, channel->id(), *service, *method, packet.call_id());
- method->Invoke(context, packet);
- break;
- }
case PacketType::CLIENT_STREAM:
HandleClientStreamPacket(packet, *channel, call);
break;
case PacketType::CLIENT_ERROR:
- case PacketType::DEPRECATED_CANCEL:
- if (call != nullptr && call->id() == packet.call_id()) {
+ if (call != calls_end()) {
call->HandleError(packet.status());
} else {
internal::rpc_lock().unlock();
@@ -100,6 +95,10 @@ Status Server::ProcessPacket(ConstByteSpan packet_data,
case PacketType::CLIENT_STREAM_END:
HandleClientStreamPacket(packet, *channel, call);
break;
+ case PacketType::REQUEST: // Handled above
+ case PacketType::RESPONSE:
+ case PacketType::SERVER_ERROR:
+ case PacketType::SERVER_STREAM:
default:
internal::rpc_lock().unlock();
PW_LOG_WARN("pw_rpc server unable to handle packet of type %u",
@@ -113,7 +112,7 @@ std::tuple<Service*, const internal::Method*> Server::FindMethod(
const internal::Packet& packet) {
// Packets always include service and method IDs.
auto service = std::find_if(services_.begin(), services_.end(), [&](auto& s) {
- return s.id() == packet.service_id();
+ return internal::UnwrapServiceId(s.service_id()) == packet.service_id();
});
if (service == services_.end()) {
@@ -123,10 +122,11 @@ std::tuple<Service*, const internal::Method*> Server::FindMethod(
return {&(*service), service->FindMethod(packet.method_id())};
}
-void Server::HandleClientStreamPacket(const internal::Packet& packet,
- internal::Channel& channel,
- internal::ServerCall* call) const {
- if (call == nullptr || call->id() != packet.call_id()) {
+void Server::HandleClientStreamPacket(
+ const internal::Packet& packet,
+ internal::Channel& channel,
+ IntrusiveList<internal::Call>::iterator call) const {
+ if (call == calls_end()) {
channel.Send(Packet::ServerError(packet, Status::FailedPrecondition()))
.IgnoreError(); // Errors are logged in Channel::Send.
internal::rpc_lock().unlock();
@@ -142,6 +142,12 @@ void Server::HandleClientStreamPacket(const internal::Packet& packet,
channel.Send(Packet::ServerError(packet, Status::InvalidArgument()))
.IgnoreError(); // Errors are logged in Channel::Send.
internal::rpc_lock().unlock();
+ PW_LOG_DEBUG(
+ "Received client stream packet for %u:%08x/%08x, which doesn't have a "
+ "client stream",
+ static_cast<unsigned>(packet.channel_id()),
+ static_cast<unsigned>(packet.service_id()),
+ static_cast<unsigned>(packet.method_id()));
return;
}
@@ -149,13 +155,19 @@ void Server::HandleClientStreamPacket(const internal::Packet& packet,
channel.Send(Packet::ServerError(packet, Status::FailedPrecondition()))
.IgnoreError(); // Errors are logged in Channel::Send.
internal::rpc_lock().unlock();
+ PW_LOG_DEBUG(
+ "Received client stream packet for %u:%08x/%08x, but its client stream "
+ "is closed",
+ static_cast<unsigned>(packet.channel_id()),
+ static_cast<unsigned>(packet.service_id()),
+ static_cast<unsigned>(packet.method_id()));
return;
}
if (packet.type() == PacketType::CLIENT_STREAM) {
call->HandlePayload(packet.payload());
} else { // Handle PacketType::CLIENT_STREAM_END.
- call->HandleClientStreamEnd();
+ static_cast<internal::ServerCall&>(*call).HandleClientStreamEnd();
}
}
diff --git a/pw_rpc/server_call.cc b/pw_rpc/server_call.cc
index 602ad8357..c665595ba 100644
--- a/pw_rpc/server_call.cc
+++ b/pw_rpc/server_call.cc
@@ -17,6 +17,8 @@
namespace pw::rpc::internal {
void ServerCall::MoveServerCallFrom(ServerCall& other) {
+ WaitUntilReadyForMove(*this, other);
+
// If this call is active, finish it first.
if (active_locked()) {
CloseAndSendResponseLocked(OkStatus()).IgnoreError();
diff --git a/pw_rpc/server_test.cc b/pw_rpc/server_test.cc
index e51c3aec6..20e91e954 100644
--- a/pw_rpc/server_test.cc
+++ b/pw_rpc/server_test.cc
@@ -19,6 +19,7 @@
#include "gtest/gtest.h"
#include "pw_assert/check.h"
+#include "pw_rpc/internal/call.h"
#include "pw_rpc/internal/method.h"
#include "pw_rpc/internal/packet.h"
#include "pw_rpc/internal/test_method.h"
@@ -32,9 +33,9 @@ namespace {
using std::byte;
using internal::Packet;
-using internal::PacketType;
using internal::TestMethod;
using internal::TestMethodUnion;
+using internal::pwpb::PacketType;
class TestService : public Service {
public:
@@ -67,6 +68,8 @@ class EmptyService : public Service {
static constexpr std::array<TestMethodUnion, 0> methods_ = {};
};
+uint32_t kDefaultCallId = 24601;
+
class BasicServer : public ::testing::Test {
protected:
static constexpr byte kDefaultPayload[] = {
@@ -84,37 +87,40 @@ class BasicServer : public ::testing::Test {
server_.RegisterService(service_1_, service_42_, empty_service_);
}
- std::span<const byte> EncodePacket(
- PacketType type,
- uint32_t channel_id,
- uint32_t service_id,
- uint32_t method_id,
- std::span<const byte> payload = kDefaultPayload,
- Status status = OkStatus()) {
- auto result =
- Packet(type, channel_id, service_id, method_id, 0, payload, status)
- .Encode(request_buffer_);
- EXPECT_EQ(OkStatus(), result.status());
- return result.value_or(ConstByteSpan());
+ span<const byte> EncodePacket(PacketType type,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ uint32_t call_id = kDefaultCallId) {
+ return EncodePacketWithBody(type,
+ channel_id,
+ service_id,
+ method_id,
+ call_id,
+ kDefaultPayload,
+ OkStatus());
}
- std::span<const byte> EncodeCancel(uint32_t channel_id = 1,
- uint32_t service_id = 42,
- uint32_t method_id = 100) {
- return EncodePacket(PacketType::CLIENT_ERROR,
- channel_id,
- service_id,
- method_id,
- {},
- Status::Cancelled());
+ span<const byte> EncodeCancel(uint32_t channel_id = 1,
+ uint32_t service_id = 42,
+ uint32_t method_id = 100,
+ uint32_t call_id = kDefaultCallId) {
+ return EncodePacketWithBody(PacketType::CLIENT_ERROR,
+ channel_id,
+ service_id,
+ method_id,
+ call_id,
+ {},
+ Status::Cancelled());
}
template <typename T = ConstByteSpan>
ConstByteSpan PacketForRpc(PacketType type,
Status status = OkStatus(),
- T&& payload = {}) {
- return EncodePacket(
- type, 1, 42, 100, std::as_bytes(std::span(payload)), status);
+ T&& payload = {},
+ uint32_t call_id = kDefaultCallId) {
+ return EncodePacketWithBody(
+ type, 1, 42, 100, call_id, as_bytes(span(payload)), status);
}
RawFakeChannelOutput<2> output_;
@@ -126,12 +132,33 @@ class BasicServer : public ::testing::Test {
private:
byte request_buffer_[64];
+
+ span<const byte> EncodePacketWithBody(PacketType type,
+ uint32_t channel_id,
+ uint32_t service_id,
+ uint32_t method_id,
+ uint32_t call_id,
+ span<const byte> payload,
+ Status status) {
+ auto result =
+ Packet(
+ type, channel_id, service_id, method_id, call_id, payload, status)
+ .Encode(request_buffer_);
+ EXPECT_EQ(OkStatus(), result.status());
+ return result.value_or(ConstByteSpan());
+ }
};
+TEST_F(BasicServer, IsServiceRegistered) {
+ TestService unregisteredService(0);
+ EXPECT_FALSE(server_.IsServiceRegistered(unregisteredService));
+ EXPECT_TRUE(server_.IsServiceRegistered(service_1_));
+}
+
TEST_F(BasicServer, ProcessPacket_ValidMethodInService1_InvokesMethod) {
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 1, 100),
- output_));
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 1, 100)));
const TestMethod& method = service_1_.method(100);
EXPECT_EQ(1u, method.last_channel_id());
@@ -143,9 +170,9 @@ TEST_F(BasicServer, ProcessPacket_ValidMethodInService1_InvokesMethod) {
}
TEST_F(BasicServer, ProcessPacket_ValidMethodInService42_InvokesMethod) {
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 200),
- output_));
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 200)));
const TestMethod& method = service_42_.method(200);
EXPECT_EQ(1u, method.last_channel_id());
@@ -156,49 +183,82 @@ TEST_F(BasicServer, ProcessPacket_ValidMethodInService42_InvokesMethod) {
0);
}
+TEST_F(BasicServer, UnregisterService_CannotCallMethod) {
+ const uint32_t kCallId = 8675309;
+ server_.UnregisterService(service_1_, service_42_);
+
+ EXPECT_EQ(OkStatus(),
+ server_.ProcessPacket(
+ EncodePacket(PacketType::REQUEST, 1, 1, 100, kCallId)));
+
+ const Packet& packet =
+ static_cast<internal::test::FakeChannelOutput&>(output_).last_packet();
+ EXPECT_EQ(packet.type(), PacketType::SERVER_ERROR);
+ EXPECT_EQ(packet.channel_id(), 1u);
+ EXPECT_EQ(packet.service_id(), 1u);
+ EXPECT_EQ(packet.method_id(), 100u);
+ EXPECT_EQ(packet.call_id(), kCallId);
+ EXPECT_EQ(packet.status(), Status::NotFound());
+}
+
+TEST_F(BasicServer, UnregisterService_AlreadyUnregistered_DoesNothing) {
+ server_.UnregisterService(service_42_, service_42_, service_42_);
+ server_.UnregisterService(service_42_);
+
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 1, 100)));
+
+ const TestMethod& method = service_1_.method(100);
+ EXPECT_EQ(1u, method.last_channel_id());
+ ASSERT_EQ(sizeof(kDefaultPayload), method.last_request().payload().size());
+ EXPECT_EQ(std::memcmp(kDefaultPayload,
+ method.last_request().payload().data(),
+ method.last_request().payload().size()),
+ 0);
+}
+
TEST_F(BasicServer, ProcessPacket_IncompletePacket_NothingIsInvoked) {
+ EXPECT_EQ(
+ Status::DataLoss(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 0, 42, 101)));
+ EXPECT_EQ(
+ Status::DataLoss(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 0, 101)));
EXPECT_EQ(Status::DataLoss(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 0, 42, 101),
- output_));
- EXPECT_EQ(Status::DataLoss(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 0, 101),
- output_));
- EXPECT_EQ(Status::DataLoss(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 0),
- output_));
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 0)));
EXPECT_EQ(0u, service_42_.method(100).last_channel_id());
EXPECT_EQ(0u, service_42_.method(200).last_channel_id());
}
TEST_F(BasicServer, ProcessPacket_NoChannel_SendsNothing) {
- EXPECT_EQ(Status::DataLoss(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 0, 42, 101),
- output_));
+ EXPECT_EQ(
+ Status::DataLoss(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 0, 42, 101)));
EXPECT_EQ(output_.total_packets(), 0u);
}
TEST_F(BasicServer, ProcessPacket_NoService_SendsNothing) {
- EXPECT_EQ(Status::DataLoss(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 0, 101),
- output_));
+ EXPECT_EQ(
+ Status::DataLoss(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 0, 101)));
EXPECT_EQ(output_.total_packets(), 0u);
}
TEST_F(BasicServer, ProcessPacket_NoMethod_SendsNothing) {
EXPECT_EQ(Status::DataLoss(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 0),
- output_));
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 0)));
EXPECT_EQ(output_.total_packets(), 0u);
}
TEST_F(BasicServer, ProcessPacket_InvalidMethod_NothingIsInvoked) {
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 101),
- output_));
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 101)));
EXPECT_EQ(0u, service_42_.method(100).last_channel_id());
EXPECT_EQ(0u, service_42_.method(200).last_channel_id());
@@ -207,15 +267,15 @@ TEST_F(BasicServer, ProcessPacket_InvalidMethod_NothingIsInvoked) {
TEST_F(BasicServer, ProcessPacket_ClientErrorWithInvalidMethod_NoResponse) {
EXPECT_EQ(OkStatus(),
server_.ProcessPacket(
- EncodePacket(PacketType::CLIENT_ERROR, 1, 42, 101), output_));
+ EncodePacket(PacketType::CLIENT_ERROR, 1, 42, 101)));
EXPECT_EQ(0u, output_.total_packets());
}
TEST_F(BasicServer, ProcessPacket_InvalidMethod_SendsError) {
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 27),
- output_));
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 27)));
const Packet& packet =
static_cast<internal::test::FakeChannelOutput&>(output_).last_packet();
@@ -227,9 +287,9 @@ TEST_F(BasicServer, ProcessPacket_InvalidMethod_SendsError) {
}
TEST_F(BasicServer, ProcessPacket_InvalidService_SendsError) {
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 43, 27),
- output_));
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 43, 27)));
const Packet& packet =
static_cast<internal::test::FakeChannelOutput&>(output_).last_packet();
@@ -246,43 +306,25 @@ TEST_F(BasicServer, ProcessPacket_UnassignedChannel) {
EncodePacket(PacketType::REQUEST, /*channel_id=*/99, 42, 27)));
}
-TEST_F(BasicServer,
- ProcessPacket_UnassignedChannel_SendsUnavailableToProvidedInterface) {
- EXPECT_EQ(Status::Unavailable(),
- server_.ProcessPacket(
- EncodePacket(PacketType::REQUEST, /*channel_id=*/99, 42, 27),
- output_));
-
- const Packet& packet =
- static_cast<internal::test::FakeChannelOutput&>(output_).last_packet();
- EXPECT_EQ(packet.status(), Status::Unavailable());
- EXPECT_EQ(packet.channel_id(), 99u);
- EXPECT_EQ(packet.service_id(), 42u);
- EXPECT_EQ(packet.method_id(), 27u);
-}
-
TEST_F(BasicServer, ProcessPacket_ClientErrorOnUnassignedChannel_NoResponse) {
channels_[2] = Channel::Create<3>(&output_); // Occupy only available channel
- EXPECT_EQ(
- Status::Unavailable(),
- server_.ProcessPacket(
- EncodePacket(PacketType::CLIENT_ERROR, /*channel_id=*/99, 42, 27),
- output_));
+ EXPECT_EQ(Status::Unavailable(),
+ server_.ProcessPacket(EncodePacket(
+ PacketType::CLIENT_ERROR, /*channel_id=*/99, 42, 27)));
EXPECT_EQ(0u, output_.total_packets());
}
TEST_F(BasicServer, ProcessPacket_Cancel_MethodNotActive_SendsNothing) {
// Set up a fake ServerWriter representing an ongoing RPC.
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodeCancel(1, 42, 100), output_));
+ EXPECT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel(1, 42, 100)));
EXPECT_EQ(output_.total_packets(), 0u);
}
const Channel* GetChannel(internal::Endpoint& endpoint, uint32_t id) {
- internal::LockGuard lock(internal::rpc_lock());
+ internal::RpcLockGuard lock;
return endpoint.GetInternalChannel(id);
}
@@ -305,9 +347,9 @@ TEST_F(BasicServer, CloseChannel_PendingCall) {
internal::TestMethod::FakeServerCall call;
service_42_.method(100).keep_call_active(call);
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 100),
- output_));
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(EncodePacket(PacketType::REQUEST, 1, 42, 100)));
Status on_error_status;
call.set_on_error(
@@ -327,11 +369,11 @@ TEST_F(BasicServer, CloseChannel_PendingCall) {
}
TEST_F(BasicServer, OpenChannel_UnusedSlot) {
- const std::span request = EncodePacket(PacketType::REQUEST, 9, 42, 100);
- EXPECT_EQ(Status::Unavailable(), server_.ProcessPacket(request, output_));
+ const span request = EncodePacket(PacketType::REQUEST, 9, 42, 100);
+ EXPECT_EQ(Status::Unavailable(), server_.ProcessPacket(request));
EXPECT_EQ(OkStatus(), server_.OpenChannel(9, output_));
- EXPECT_EQ(OkStatus(), server_.ProcessPacket(request, output_));
+ EXPECT_EQ(OkStatus(), server_.ProcessPacket(request));
const Packet& packet =
static_cast<internal::test::FakeChannelOutput&>(output_).last_packet();
@@ -356,19 +398,26 @@ TEST_F(BasicServer, OpenChannel_AdditionalSlot) {
class BidiMethod : public BasicServer {
protected:
- BidiMethod()
- : responder_(internal::CallContext(server_,
- channels_[0].id(),
- service_42_,
- service_42_.method(100),
- 0)) {
- ASSERT_TRUE(responder_.active());
+ BidiMethod() {
+ internal::rpc_lock().lock();
+ internal::CallContext context(server_,
+ channels_[0].id(),
+ service_42_,
+ service_42_.method(100),
+ kDefaultCallId);
+ // A local temporary is required since the constructor requires a lock,
+ // but the *move* constructor takes out the lock.
+ internal::test::FakeServerReaderWriter responder_temp(
+ context.ClaimLocked());
+ internal::rpc_lock().unlock();
+ responder_ = std::move(responder_temp);
+ PW_CHECK(responder_.active());
}
internal::test::FakeServerReaderWriter responder_;
};
-TEST_F(BidiMethod, DuplicateCall_CancelsExistingThenCallsAgain) {
+TEST_F(BidiMethod, DuplicateCallId_CancelsExistingThenCallsAgain) {
int cancelled = 0;
responder_.set_on_error([&cancelled](Status error) {
if (error.IsCancelled()) {
@@ -380,28 +429,86 @@ TEST_F(BidiMethod, DuplicateCall_CancelsExistingThenCallsAgain) {
ASSERT_EQ(method.invocations(), 0u);
EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::REQUEST), output_));
+ server_.ProcessPacket(PacketForRpc(PacketType::REQUEST)));
EXPECT_EQ(cancelled, 1);
EXPECT_EQ(method.invocations(), 1u);
}
+TEST_F(BidiMethod, DuplicateMethodDifferentCallId_NotCancelled) {
+ int cancelled = 0;
+ responder_.set_on_error([&cancelled](Status error) {
+ if (error.IsCancelled()) {
+ cancelled += 1;
+ }
+ });
+
+ const uint32_t kSecondCallId = 1625;
+ EXPECT_EQ(OkStatus(),
+ server_.ProcessPacket(PacketForRpc(
+ PacketType::REQUEST, OkStatus(), {}, kSecondCallId)));
+
+ EXPECT_EQ(cancelled, 0);
+}
+
+const char* span_as_cstr(ConstByteSpan span) {
+ return reinterpret_cast<const char*>(span.data());
+}
+
+TEST_F(BidiMethod, DuplicateMethodDifferentCallIdEachCallGetsSeparateResponse) {
+ const uint32_t kSecondCallId = 1625;
+
+ internal::rpc_lock().lock();
+ internal::test::FakeServerReaderWriter responder_2(
+ internal::CallContext(server_,
+ channels_[0].id(),
+ service_42_,
+ service_42_.method(100),
+ kSecondCallId)
+ .ClaimLocked());
+ internal::rpc_lock().unlock();
+
+ ConstByteSpan data_1 = as_bytes(span("data_1_unset"));
+ responder_.set_on_next(
+ [&data_1](ConstByteSpan payload) { data_1 = payload; });
+
+ ConstByteSpan data_2 = as_bytes(span("data_2_unset"));
+ responder_2.set_on_next(
+ [&data_2](ConstByteSpan payload) { data_2 = payload; });
+
+ const char* kMessage1 = "hello_1";
+ const char* kMessage2 = "hello_2";
+
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(PacketForRpc(
+ PacketType::CLIENT_STREAM, OkStatus(), "hello_2", kSecondCallId)));
+
+ EXPECT_STREQ(span_as_cstr(data_2), kMessage2);
+
+ EXPECT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(PacketForRpc(
+ PacketType::CLIENT_STREAM, OkStatus(), "hello_1", kDefaultCallId)));
+
+ EXPECT_STREQ(span_as_cstr(data_1), kMessage1);
+}
+
TEST_F(BidiMethod, Cancel_ClosesServerWriter) {
- EXPECT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel(), output_));
+ EXPECT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel()));
EXPECT_FALSE(responder_.active());
}
TEST_F(BidiMethod, Cancel_SendsNoResponse) {
- EXPECT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel(), output_));
+ EXPECT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel()));
EXPECT_EQ(output_.total_packets(), 0u);
}
TEST_F(BidiMethod, ClientError_ClosesServerWriterWithoutResponse) {
- ASSERT_EQ(
- OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_ERROR), output_));
+ ASSERT_EQ(OkStatus(),
+ server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_ERROR)));
EXPECT_FALSE(responder_.active());
EXPECT_EQ(output_.total_packets(), 0u);
@@ -413,8 +520,7 @@ TEST_F(BidiMethod, ClientError_CallsOnErrorCallback) {
ASSERT_EQ(OkStatus(),
server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_ERROR,
- Status::Unauthenticated()),
- output_));
+ Status::Unauthenticated())));
EXPECT_EQ(status, Status::Unauthenticated());
}
@@ -423,42 +529,92 @@ TEST_F(BidiMethod, Cancel_CallsOnErrorCallback) {
Status status = Status::Unknown();
responder_.set_on_error([&status](Status error) { status = error; });
- ASSERT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel(), output_));
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel()));
EXPECT_EQ(status, Status::Cancelled());
}
TEST_F(BidiMethod, Cancel_IncorrectChannel_SendsNothing) {
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodeCancel(2, 42, 100), output_));
+ EXPECT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel(2, 42, 100)));
EXPECT_EQ(output_.total_packets(), 0u);
EXPECT_TRUE(responder_.active());
}
TEST_F(BidiMethod, Cancel_IncorrectService_SendsNothing) {
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodeCancel(1, 43, 100), output_));
+ EXPECT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel(1, 43, 100)));
EXPECT_EQ(output_.total_packets(), 0u);
EXPECT_TRUE(responder_.active());
}
TEST_F(BidiMethod, Cancel_IncorrectMethod_SendsNothing) {
- EXPECT_EQ(OkStatus(),
- server_.ProcessPacket(EncodeCancel(1, 42, 101), output_));
+ EXPECT_EQ(OkStatus(), server_.ProcessPacket(EncodeCancel(1, 42, 101)));
EXPECT_EQ(output_.total_packets(), 0u);
EXPECT_TRUE(responder_.active());
}
TEST_F(BidiMethod, ClientStream_CallsCallback) {
- ConstByteSpan data = std::as_bytes(std::span("?"));
+ ConstByteSpan data = as_bytes(span("?"));
responder_.set_on_next([&data](ConstByteSpan payload) { data = payload; });
ASSERT_EQ(OkStatus(),
server_.ProcessPacket(
- PacketForRpc(PacketType::CLIENT_STREAM, {}, "hello"), output_));
+ PacketForRpc(PacketType::CLIENT_STREAM, {}, "hello")));
EXPECT_EQ(output_.total_packets(), 0u);
- EXPECT_STREQ(reinterpret_cast<const char*>(data.data()), "hello");
+ EXPECT_STREQ(span_as_cstr(data), "hello");
+}
+
+TEST_F(BidiMethod, ClientStream_CallsCallbackOnCallWithOpenId) {
+ ConstByteSpan data = as_bytes(span("?"));
+ responder_.set_on_next([&data](ConstByteSpan payload) { data = payload; });
+
+ ASSERT_EQ(
+ OkStatus(),
+ server_.ProcessPacket(PacketForRpc(
+ PacketType::CLIENT_STREAM, {}, "hello", internal::kOpenCallId)));
+
+ EXPECT_EQ(output_.total_packets(), 0u);
+ EXPECT_STREQ(span_as_cstr(data), "hello");
+}
+
+TEST_F(BidiMethod, ClientStream_CallsOpenIdOnCallWithDifferentId) {
+ const uint32_t kSecondCallId = 1625;
+ internal::CallContext context(server_,
+ channels_[0].id(),
+ service_42_,
+ service_42_.method(100),
+ internal::kOpenCallId);
+ internal::rpc_lock().lock();
+ auto temp_responder =
+ internal::test::FakeServerReaderWriter(context.ClaimLocked());
+ internal::rpc_lock().unlock();
+ responder_ = std::move(temp_responder);
+
+ ConstByteSpan data = as_bytes(span("?"));
+ responder_.set_on_next([&data](ConstByteSpan payload) { data = payload; });
+
+ ASSERT_EQ(OkStatus(),
+ server_.ProcessPacket(PacketForRpc(
+ PacketType::CLIENT_STREAM, {}, "hello", kSecondCallId)));
+
+ EXPECT_EQ(output_.total_packets(), 0u);
+ EXPECT_STREQ(span_as_cstr(data), "hello");
+
+ internal::RpcLockGuard lock;
+ EXPECT_EQ(responder_.as_server_call().id(), kSecondCallId);
+}
+
+TEST_F(BidiMethod, UnregsiterService_AbortsActiveCalls) {
+ ASSERT_TRUE(responder_.active());
+
+ Status on_error_status = OkStatus();
+ responder_.set_on_error(
+ [&on_error_status](Status status) { on_error_status = status; });
+
+ server_.UnregisterService(service_42_);
+
+ EXPECT_FALSE(responder_.active());
+ EXPECT_EQ(Status::Aborted(), on_error_status);
}
#if PW_RPC_CLIENT_STREAM_END_CALLBACK
@@ -468,8 +624,7 @@ TEST_F(BidiMethod, ClientStreamEnd_CallsCallback) {
responder_.set_on_client_stream_end([&called]() { called = true; });
ASSERT_EQ(OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM_END),
- output_));
+ server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM_END)));
EXPECT_EQ(output_.total_packets(), 0u);
EXPECT_TRUE(called);
@@ -477,12 +632,12 @@ TEST_F(BidiMethod, ClientStreamEnd_CallsCallback) {
TEST_F(BidiMethod, ClientStreamEnd_ErrorWhenClosed) {
const auto end = PacketForRpc(PacketType::CLIENT_STREAM_END);
- ASSERT_EQ(OkStatus(), server_.ProcessPacket(end, output_));
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
bool called = false;
responder_.set_on_client_stream_end([&called]() { called = true; });
- ASSERT_EQ(OkStatus(), server_.ProcessPacket(end, output_));
+ ASSERT_EQ(OkStatus(), server_.ProcessPacket(end));
ASSERT_EQ(output_.total_packets(), 1u);
const Packet& packet =
@@ -495,24 +650,25 @@ TEST_F(BidiMethod, ClientStreamEnd_ErrorWhenClosed) {
class ServerStreamingMethod : public BasicServer {
protected:
- ServerStreamingMethod()
- : call_(server_,
- channels_[0].id(),
- service_42_,
- service_42_.method(100),
- 0),
- responder_(call_) {
- ASSERT_TRUE(responder_.active());
+ ServerStreamingMethod() {
+ internal::CallContext context(server_,
+ channels_[0].id(),
+ service_42_,
+ service_42_.method(100),
+ kDefaultCallId);
+ internal::rpc_lock().lock();
+ internal::test::FakeServerWriter responder_temp(context.ClaimLocked());
+ internal::rpc_lock().unlock();
+ responder_ = std::move(responder_temp);
+ PW_CHECK(responder_.active());
}
- internal::CallContext call_;
internal::test::FakeServerWriter responder_;
};
TEST_F(ServerStreamingMethod, ClientStream_InvalidArgumentError) {
- ASSERT_EQ(
- OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM), output_));
+ ASSERT_EQ(OkStatus(),
+ server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM)));
ASSERT_EQ(output_.total_packets(), 1u);
const Packet& packet =
@@ -523,8 +679,7 @@ TEST_F(ServerStreamingMethod, ClientStream_InvalidArgumentError) {
TEST_F(ServerStreamingMethod, ClientStreamEnd_InvalidArgumentError) {
ASSERT_EQ(OkStatus(),
- server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM_END),
- output_));
+ server_.ProcessPacket(PacketForRpc(PacketType::CLIENT_STREAM_END)));
ASSERT_EQ(output_.total_packets(), 1u);
const Packet& packet =
diff --git a/pw_rpc/size_report/base.cc b/pw_rpc/size_report/base.cc
index 76043ab0f..5248c8034 100644
--- a/pw_rpc/size_report/base.cc
+++ b/pw_rpc/size_report/base.cc
@@ -28,9 +28,9 @@ int main() {
std::byte packet_buffer[128];
pw::sys_io::ReadBytes(packet_buffer)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
pw::sys_io::WriteBytes(packet_buffer)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return static_cast<int>(packet_buffer[92]);
}
diff --git a/pw_rpc/size_report/server_only.cc b/pw_rpc/size_report/server_only.cc
index aa466e6a5..a8644f778 100644
--- a/pw_rpc/size_report/server_only.cc
+++ b/pw_rpc/size_report/server_only.cc
@@ -24,7 +24,7 @@ class Output : public pw::rpc::ChannelOutput {
public:
Output() : ChannelOutput("output") {}
- pw::Status Send(std::span<const std::byte> buffer) override {
+ pw::Status Send(pw::span<const std::byte> buffer) override {
return pw::sys_io::WriteBytes(buffer).status();
}
};
@@ -46,12 +46,12 @@ int main() {
std::byte packet_buffer[128];
pw::sys_io::ReadBytes(packet_buffer)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
pw::sys_io::WriteBytes(packet_buffer)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
- my_product::server.ProcessPacket(packet_buffer, my_product::output)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ my_product::server.ProcessPacket(packet_buffer)
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return static_cast<int>(packet_buffer[92]);
}
diff --git a/pw_rpc/size_report/server_with_echo_service.cc b/pw_rpc/size_report/server_with_echo_service.cc
index 79e7ca9f8..05052610a 100644
--- a/pw_rpc/size_report/server_with_echo_service.cc
+++ b/pw_rpc/size_report/server_with_echo_service.cc
@@ -27,7 +27,7 @@ class Output : public pw::rpc::ChannelOutput {
public:
Output() : ChannelOutput("output") {}
- pw::Status Send(std::span<const std::byte> buffer) override {
+ pw::Status Send(pw::span<const std::byte> buffer) override {
return pw::sys_io::WriteBytes(buffer).status();
}
};
@@ -79,7 +79,7 @@ int main() {
pw::sys_io::WriteBytes(packet_buffer);
my_product::server.RegisterService(my_product::echo_service);
- my_product::server.ProcessPacket(packet_buffer, my_product::output);
+ my_product::server.ProcessPacket(packet_buffer);
return static_cast<int>(packet_buffer[92]);
}
diff --git a/pw_rpc/system_server/BUILD.bazel b/pw_rpc/system_server/BUILD.bazel
index f89e97f8f..407c6da73 100644
--- a/pw_rpc/system_server/BUILD.bazel
+++ b/pw_rpc/system_server/BUILD.bazel
@@ -52,8 +52,8 @@ pw_cc_library(
name = "system_server_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "//pw_build/constraints/board:stm32f429i-disc1": ["//targets/stm32f429i_disc1:system_rpc_server"],
- "//pw_build/constraints/board:mimxrt595_evk": ["//targets/mimxrt595_evk:system_rpc_server"],
+ "//pw_build/constraints/board:stm32f429i-disc1": ["//pw_hdlc:hdlc_sys_io_system_server"],
+ "//pw_build/constraints/board:mimxrt595_evk": ["//pw_hdlc:hdlc_sys_io_system_server"],
"//conditions:default": ["//targets/host:system_rpc_server"],
}),
)
diff --git a/pw_rpc/system_server/BUILD.gn b/pw_rpc/system_server/BUILD.gn
index e6721d634..ba0ca5383 100644
--- a/pw_rpc/system_server/BUILD.gn
+++ b/pw_rpc/system_server/BUILD.gn
@@ -26,7 +26,12 @@ config("public_includes") {
pw_facade("system_server") {
backend = pw_rpc_system_server_BACKEND
public_configs = [ ":public_includes" ]
- public_deps = [ "$dir_pw_rpc:server" ]
+ public_deps = [
+ "$dir_pw_rpc:server",
+ dir_pw_bytes,
+ dir_pw_function,
+ dir_pw_result,
+ ]
public = [ "public/pw_rpc_system_server/rpc_server.h" ]
}
diff --git a/pw_rpc/system_server/CMakeLists.txt b/pw_rpc/system_server/CMakeLists.txt
index 12f9cb690..b6b3fe857 100644
--- a/pw_rpc/system_server/CMakeLists.txt
+++ b/pw_rpc/system_server/CMakeLists.txt
@@ -13,9 +13,27 @@
# the License.
include("$ENV{PW_ROOT}/pw_build/pigweed.cmake")
+include($ENV{PW_ROOT}/pw_rpc/system_server/backend.cmake)
-pw_add_facade(pw_rpc.system_server
+pw_add_facade(pw_rpc.system_server INTERFACE
+ BACKEND
+ pw_rpc.system_server_BACKEND
+ HEADERS
+ public/pw_rpc_system_server/rpc_server.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_rpc.server
pw_stream
+ pw_function
+ pw_result
+)
+
+pw_add_library(pw_rpc.system_server.socket INTERFACE
+ HEADERS
+ public/pw_rpc_system_server/socket.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_rpc.system_server
)
diff --git a/pw_rpc/system_server/backend.cmake b/pw_rpc/system_server/backend.cmake
new file mode 100644
index 000000000..cf69f74ad
--- /dev/null
+++ b/pw_rpc/system_server/backend.cmake
@@ -0,0 +1,19 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_rpc_system_server module.
+pw_add_backend_variable(pw_rpc.system_server_BACKEND)
diff --git a/pw_rpc/system_server/public/pw_rpc_system_server/rpc_server.h b/pw_rpc/system_server/public/pw_rpc_system_server/rpc_server.h
index d062610bf..4aab3dbcd 100644
--- a/pw_rpc/system_server/public/pw_rpc_system_server/rpc_server.h
+++ b/pw_rpc/system_server/public/pw_rpc_system_server/rpc_server.h
@@ -17,12 +17,12 @@
namespace pw::rpc::system_server {
-// Initialization.
-void Init();
-
// Get the reference of RPC Server instance.
pw::rpc::Server& Server();
+// Initialization.
+void Init();
+
// Start the server and processing packets. May not return.
Status Start();
diff --git a/pw_rpc/system_server/public/pw_rpc_system_server/socket.h b/pw_rpc/system_server/public/pw_rpc_system_server/socket.h
index 647633564..dc34e9335 100644
--- a/pw_rpc/system_server/public/pw_rpc_system_server/socket.h
+++ b/pw_rpc/system_server/public/pw_rpc_system_server/socket.h
@@ -20,4 +20,8 @@ namespace pw::rpc::system_server {
// Sets the port to use for pw::rpc::system_server backends that use sockets.
void set_socket_port(uint16_t port);
+// The file descriptor for the socket associated with the server. This may be
+// used to configure socket options.
+int GetServerSocketFd();
+
} // namespace pw::rpc::system_server
diff --git a/pw_rpc/test_helpers_test.cc b/pw_rpc/test_helpers_test.cc
new file mode 100644
index 000000000..7b4b6e2a9
--- /dev/null
+++ b/pw_rpc/test_helpers_test.cc
@@ -0,0 +1,199 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_rpc/test_helpers.h"
+
+#include <mutex>
+
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_containers/vector.h"
+#include "pw_result/result.h"
+#include "pw_rpc/echo.pwpb.h"
+#include "pw_rpc/echo.rpc.pwpb.h"
+#include "pw_rpc/pwpb/client_testing.h"
+#include "pw_rpc/pwpb/server_reader_writer.h"
+#include "pw_status/status.h"
+#include "pw_sync/interrupt_spin_lock.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/timed_thread_notification.h"
+
+namespace pw::rpc::test {
+namespace {
+using namespace std::chrono_literals;
+
+constexpr auto kWaitForEchoTimeout =
+ pw::chrono::SystemClock::for_at_least(100ms);
+
+// Class that we want to test.
+//
+// It's main purpose is to ask EchoService for Echo and provide its result
+// through WaitForEcho/LastEcho pair to the user.
+class EntityUnderTest {
+ public:
+ explicit EntityUnderTest(pw_rpc::pwpb::EchoService::Client& echo_client)
+ : echo_client_(echo_client) {}
+
+ void AskForEcho() {
+ call_ = echo_client_.Echo(
+ pwpb::EchoMessage::Message{},
+ [this](const pwpb::EchoMessage::Message& response, pw::Status status) {
+ lock_.lock();
+ if (status.ok()) {
+ last_echo_ = response.msg;
+ } else {
+ last_echo_ = status;
+ }
+ lock_.unlock();
+ notifier_.release();
+ },
+ [this](pw::Status status) {
+ lock_.lock();
+ last_echo_ = status;
+ lock_.unlock();
+ notifier_.release();
+ });
+ }
+
+ bool WaitForEcho(pw::chrono::SystemClock::duration duration) {
+ return notifier_.try_acquire_for(duration);
+ }
+
+ pw::Result<pw::InlineString<64>> LastEcho() const {
+ std::lock_guard<pw::sync::InterruptSpinLock> lock(lock_);
+ return last_echo_;
+ }
+
+ private:
+ pw_rpc::pwpb::EchoService::Client& echo_client_;
+ PwpbUnaryReceiver<pwpb::EchoMessage::Message> call_;
+ pw::sync::TimedThreadNotification notifier_;
+ pw::Result<pw::InlineString<64>> last_echo_ PW_GUARDED_BY(lock_);
+ mutable pw::sync::InterruptSpinLock lock_;
+};
+
+TEST(RpcTestHelpersTest, SendResponseIfCalledOk) {
+ PwpbClientTestContext client_context;
+ pw_rpc::pwpb::EchoService::Client client(client_context.client(),
+ client_context.channel().id());
+ EntityUnderTest entity(client);
+
+ // We need to call the function that will initiate the request before we can
+ // send the response back.
+ entity.AskForEcho();
+
+ // SendResponseIfCalled blocks until request is received by the service (it is
+ // sent by AskForEcho to EchoService in this case) and responds to it with the
+ // response.
+ //
+ // SendResponseIfCalled will timeout if no request were sent in the `timeout`
+ // interval (see SendResponseIfCalledWithoutRequest test for the example).
+ ASSERT_EQ(SendResponseIfCalled<pw_rpc::pwpb::EchoService::Echo>(
+ client_context, {.msg = "Hello"}),
+ OkStatus());
+
+ // After SendResponseIfCalled returned OkStatus client should have received
+ // the response back in the RPC thread, so we can check it here. Because it is
+ // a separate thread we still need to wait with the timeout.
+ ASSERT_TRUE(entity.WaitForEcho(kWaitForEchoTimeout));
+
+ pw::Result<pw::InlineString<64>> result = entity.LastEcho();
+ ASSERT_TRUE(result.ok());
+ EXPECT_EQ(result.value(), "Hello");
+}
+
+TEST(RpcTestHelpersTest, SendResponseIfCalledNotOk) {
+ PwpbClientTestContext client_context;
+ pw_rpc::pwpb::EchoService::Client client(client_context.client(),
+ client_context.channel().id());
+ EntityUnderTest entity(client);
+
+ // We need to call the function that will initiate the request before we can
+ // send the response back.
+ entity.AskForEcho();
+
+ // SendResponseIfCalled also can be used to respond with failures. In this
+ // case we are sending back pw::Status::InvalidArgument and expect to see it
+ // on the client side.
+ //
+ // SendResponseIfCalled result status is not the same status as it sends to
+ // the client, so we still are expecting the OkStatus here.
+ ASSERT_EQ(SendResponseIfCalled<pw_rpc::pwpb::EchoService::Echo>(
+ client_context, {}, pw::Status::InvalidArgument()),
+ OkStatus());
+
+ // After SendResponseIfCalled returned OkStatus client should have received
+ // the response back in the RPC thread, so we can check it here. Because it is
+ // a separate thread we still need to wait with the timeout.
+ ASSERT_TRUE(entity.WaitForEcho(kWaitForEchoTimeout));
+
+ EXPECT_EQ(entity.LastEcho().status(), Status::InvalidArgument());
+}
+
+TEST(RpcTestHelpersTest, SendResponseIfCalledNotOkShortcut) {
+ PwpbClientTestContext client_context;
+ pw_rpc::pwpb::EchoService::Client client(client_context.client(),
+ client_context.channel().id());
+ EntityUnderTest entity(client);
+
+ // We need to call the function that will initiate the request before we can
+ // send the response back.
+ entity.AskForEcho();
+
+ // SendResponseIfCalled shortcut version exists to respond with failures. It
+ // works exactly the same, but doesn't have the response argument. In this
+ // case we are sending back pw::Status::InvalidArgument and expect to see it
+ // on the client side.
+ //
+ // SendResponseIfCalled result status is not the same status as it sends to
+ // the client, so we still are expecting the OkStatus here.
+ ASSERT_EQ(SendResponseIfCalled<pw_rpc::pwpb::EchoService::Echo>(
+ client_context, pw::Status::InvalidArgument()),
+ OkStatus());
+
+ // After SendResponseIfCalled returned OkStatus client should have received
+ // the response back in the RPC thread, so we can check it here. Because it is
+ // a separate thread we still need to wait with the timeout.
+ ASSERT_TRUE(entity.WaitForEcho(kWaitForEchoTimeout));
+
+ EXPECT_EQ(entity.LastEcho().status(), Status::InvalidArgument());
+}
+
+TEST(RpcTestHelpersTest, SendResponseIfCalledWithoutRequest) {
+ PwpbClientTestContext client_context;
+ pw_rpc::pwpb::EchoService::Client client(client_context.client(),
+ client_context.channel().id());
+
+ // We don't send any request in this test and SendResponseIfCalled is expected
+ // to fail on waiting for the request with pw::Status::FailedPrecondition.
+
+ const auto start_time = pw::chrono::SystemClock::now();
+ auto status = SendResponseIfCalled<pw_rpc::pwpb::EchoService::Echo>(
+ client_context,
+ {.msg = "Hello"},
+ pw::OkStatus(),
+ /*timeout=*/pw::chrono::SystemClock::for_at_least(10ms));
+
+ // We set our timeout for SendResponseIfCalled to 10ms, so it should be at
+ // least 10ms since we called the SendResponseIfCalled.
+ EXPECT_GE(pw::chrono::SystemClock::now() - start_time,
+ pw::chrono::SystemClock::for_at_least(10ms));
+
+ // We expect SendResponseIfCalled to fail, because there were no request sent
+ // for the given method.
+ EXPECT_EQ(status, Status::DeadlineExceeded());
+}
+
+} // namespace
+} // namespace pw::rpc::test
diff --git a/pw_rpc/ts/BUILD.bazel b/pw_rpc/ts/BUILD.bazel
deleted file mode 100644
index ef3bc6496..000000000
--- a/pw_rpc/ts/BUILD.bazel
+++ /dev/null
@@ -1,106 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@npm//@bazel/typescript:index.bzl", "ts_library", "ts_project")
-load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
-load("@rules_proto_grpc//js:defs.bzl", "js_proto_library")
-load("//pw_protobuf_compiler/ts:ts_proto_collection.bzl", "ts_proto_collection")
-
-package(default_visibility = ["//visibility:public"])
-
-ts_project(
- name = "lib",
- srcs = [
- "call.ts",
- "client.ts",
- "descriptors.ts",
- "hash.ts",
- "index.ts",
- "method.ts",
- "packets.ts",
- "rpc_classes.ts",
- ],
- declaration = True,
- source_map = True,
- deps = [
- ":packet_proto_tspb",
- "//pw_protobuf_compiler/ts:pw_protobuf_compiler",
- "//pw_status/ts:pw_status",
- "@npm//@types/google-protobuf",
- "@npm//wait-queue",
- ],
-)
-
-js_library(
- name = "pw_rpc",
- package_name = "@pigweed/pw_rpc",
- srcs = ["package.json"],
- deps = [":lib"],
-)
-
-ts_proto_collection(
- name = "rpc_proto_collection",
- js_proto_library = "@pigweed//pw_rpc/ts:test_protos_tspb",
- proto_library = "@pigweed//pw_rpc/ts:test_protos",
-)
-
-ts_library(
- name = "rpc_test_lib",
- srcs = [
- "call_test.ts",
- "client_test.ts",
- "descriptors_test.ts",
- "packets_test.ts",
- ],
- data = [
- ":test_protos",
- ],
- deps = [
- ":lib",
- ":packet_proto_tspb",
- ":rpc_proto_collection",
- ":test_protos_tspb",
- "//pw_protobuf_compiler/ts:pw_protobuf_compiler",
- "//pw_status/ts:pw_status",
- "@npm//@types/google-protobuf",
- "@npm//@types/jasmine",
- "@npm//@types/node",
- ],
-)
-
-jasmine_node_test(
- name = "rpc_test",
- srcs = [
- ":rpc_test_lib",
- ],
-)
-
-proto_library(
- name = "test_protos",
- srcs = [
- "test.proto",
- "test2.proto",
- ],
-)
-
-js_proto_library(
- name = "test_protos_tspb",
- protos = [":test_protos"],
-)
-
-js_proto_library(
- name = "packet_proto_tspb",
- protos = ["//pw_rpc:internal_packet_proto"],
-)
diff --git a/pw_rpc/ts/call.ts b/pw_rpc/ts/call.ts
index e084fbfd7..11894dc8c 100644
--- a/pw_rpc/ts/call.ts
+++ b/pw_rpc/ts/call.ts
@@ -12,10 +12,10 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Status} from '@pigweed/pw_status';
+import {Status} from 'pigweedjs/pw_status';
import {Message} from 'google-protobuf';
-import WaitQueue = require('wait-queue');
+import WaitQueue from "./queue";
import {PendingCalls, Rpc} from './rpc_classes';
diff --git a/pw_rpc/ts/call_test.ts b/pw_rpc/ts/call_test.ts
index 36d72b1c1..47af6dab8 100644
--- a/pw_rpc/ts/call_test.ts
+++ b/pw_rpc/ts/call_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,10 +12,9 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
-import 'jasmine';
+/* eslint-env browser */
-import {SomeMessage} from 'test_protos_tspb/test_protos_tspb_pb/pw_rpc/ts/test2_pb';
+import {SomeMessage} from 'pigweedjs/protos/pw_rpc/ts/test2_pb';
import {Call} from './call';
import {Channel, Method, Service} from './descriptors';
@@ -34,9 +33,8 @@ describe('Call', () => {
let call: Call;
beforeEach(() => {
- const noop = () => {};
+ const noop = () => { };
const pendingCalls = new PendingCalls();
- const channel = jasmine.createSpy();
const rpc = new FakeRpc();
call = new Call(pendingCalls, rpc, noop, noop, noop);
});
@@ -60,11 +58,11 @@ describe('Call', () => {
let responses = call.getResponses(2);
expect((await responses.next()).value).toEqual(message1);
expect((await responses.next()).value).toEqual(message2);
- expect((await responses.next()).done).toBeTrue();
+ expect((await responses.next()).done).toEqual(true);
responses = call.getResponses(1);
expect((await responses.next()).value).toEqual(message3);
- expect((await responses.next()).done).toBeTrue();
+ expect((await responses.next()).done).toEqual(true);
});
it('getResponse early returns on stream end.', async () => {
@@ -76,10 +74,11 @@ describe('Call', () => {
call.handleCompletion(0);
expect((await responses.next()).value).toEqual(message);
- expect((await responses.next()).done).toBeTrue();
+ expect((await responses.next()).done).toEqual(true);
});
it('getResponse promise is rejected on stream error.', async () => {
+ expect.assertions(2);
const message = newMessage();
const responses = call.getResponses(3);
@@ -91,7 +90,9 @@ describe('Call', () => {
// Promise is rejected as soon as an error is received, even if there is a
// response in the queue.
- await expectAsync(responses.next()).toBeRejected();
+ responses.next().catch((e: Error) => {
+ expect(e.name).toEqual('TypeError');
+ });
});
it('getResponse waits if queue is empty', async () => {
@@ -104,13 +105,13 @@ describe('Call', () => {
call.handleResponse(message1);
call.handleResponse(message2);
call.handleCompletion(0);
- expect(call.completed).toBeTrue();
+ expect(call.completed).toEqual(true);
}, 200);
- expect(call.completed).toBeFalse();
+ expect(call.completed).toEqual(false);
expect((await responses.next()).value).toEqual(message1);
expect((await responses.next()).value).toEqual(message2);
- expect((await responses.next()).done).toBeTrue();
+ expect((await responses.next()).done).toEqual(true);
});
it('getResponse without count fetches all results', async () => {
@@ -124,11 +125,11 @@ describe('Call', () => {
setTimeout(() => {
call.handleResponse(message2);
call.handleCompletion(0);
- expect(call.completed).toBeTrue();
+ expect(call.completed).toEqual(true);
}, 200);
- expect(call.completed).toBeFalse();
+ expect(call.completed).toEqual(false);
expect((await responses.next()).value).toEqual(message2);
- expect((await responses.next()).done).toBeTrue();
+ expect((await responses.next()).done).toEqual(true);
});
});
diff --git a/pw_rpc/ts/client.ts b/pw_rpc/ts/client.ts
index 0e44b4d6e..e7a97176d 100644
--- a/pw_rpc/ts/client.ts
+++ b/pw_rpc/ts/client.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -14,13 +14,13 @@
/** Provides a pw_rpc client for TypeScript. */
-import {ProtoCollection} from '@pigweed/pw_protobuf_compiler';
-import {Status} from '@pigweed/pw_status';
+import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler';
+import {Status} from 'pigweedjs/pw_status';
import {Message} from 'google-protobuf';
import {
PacketType,
RpcPacket,
-} from 'packet_proto_tspb/packet_proto_tspb_pb/pw_rpc/internal/packet_pb';
+} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
import {Channel, Service} from './descriptors';
import {MethodStub, methodStubFactory} from './method';
@@ -33,7 +33,7 @@ import {PendingCalls, Rpc} from './rpc_classes';
export class ServiceClient {
private service: Service;
private methods: MethodStub[] = [];
- private methodsByName = new Map<string, MethodStub>();
+ methodsByName = new Map<string, MethodStub>();
constructor(client: Client, channel: Channel, service: Service) {
this.service = service;
@@ -52,6 +52,10 @@ export class ServiceClient {
get id(): number {
return this.service.id;
}
+
+ get name(): string {
+ return this.service.name;
+ }
}
/**
@@ -59,7 +63,7 @@ export class ServiceClient {
*/
export class ChannelClient {
readonly channel: Channel;
- private services = new Map<string, ServiceClient>();
+ services = new Map<string, ServiceClient>();
constructor(client: Client, channel: Channel, services: Service[]) {
this.channel = channel;
diff --git a/pw_rpc/ts/client_test.ts b/pw_rpc/ts/client_test.ts
index e040e8602..0dfed21b1 100644
--- a/pw_rpc/ts/client_test.ts
+++ b/pw_rpc/ts/client_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,21 +12,20 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
-import 'jasmine';
+/* eslint-env browser */
-import {Status} from '@pigweed/pw_status';
-import {MessageCreator} from '@pigweed/pw_protobuf_compiler';
+import {Status} from 'pigweedjs/pw_status';
+import {MessageCreator} from 'pigweedjs/pw_protobuf_compiler';
import {Message} from 'google-protobuf';
import {
PacketType,
RpcPacket,
-} from 'packet_proto_tspb/packet_proto_tspb_pb/pw_rpc/internal/packet_pb';
-import {ProtoCollection} from 'rpc_proto_collection/generated/ts_proto_collection';
+} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
+import {ProtoCollection} from 'pigweedjs/protos/collection';
import {
Request,
Response,
-} from 'test_protos_tspb/test_protos_tspb_pb/pw_rpc/ts/test_pb';
+} from 'pigweedjs/protos/pw_rpc/ts/test_pb';
import {Client} from './client';
import {Channel, Method} from './descriptors';
@@ -160,7 +159,7 @@ describe('RPC', () => {
beforeEach(async () => {
protoCollection = new ProtoCollection();
- const channels = [new Channel(1, handlePacket), new Channel(2, () => {})];
+ const channels = [new Channel(1, handlePacket), new Channel(2, () => { })];
client = Client.fromProtoSet(channels, protoCollection);
lastPacketSent = undefined;
requests = [];
@@ -302,9 +301,9 @@ describe('RPC', () => {
const response = newResponse('hello world');
enqueueResponse(1, unaryStub.method, Status.ABORTED, response);
- const onNext = jasmine.createSpy();
- const onCompleted = jasmine.createSpy();
- const onError = jasmine.createSpy();
+ const onNext = jest.fn();
+ const onCompleted = jest.fn();
+ const onError = jest.fn();
const call = unaryStub.invoke(
newRequest(5),
onNext,
@@ -313,9 +312,9 @@ describe('RPC', () => {
);
expect(sentPayload(Request).getMagicNumber()).toEqual(5);
- expect(onNext).toHaveBeenCalledOnceWith(response);
+ expect(onNext).toHaveBeenCalledWith(response);
expect(onError).not.toHaveBeenCalled();
- expect(onCompleted).toHaveBeenCalledOnceWith(Status.ABORTED);
+ expect(onCompleted).toHaveBeenCalledWith(Status.ABORTED);
}
});
@@ -326,17 +325,17 @@ describe('RPC', () => {
const response = newResponse('hello world');
enqueueResponse(1, unaryStub.method, Status.ABORTED, response);
- const onNext = jasmine.createSpy();
- const onCompleted = jasmine.createSpy();
- const onError = jasmine.createSpy();
+ const onNext = jest.fn();
+ const onCompleted = jest.fn();
+ const onError = jest.fn();
unaryStub.open(newRequest(5), onNext, onCompleted, onError);
- expect(requests).toHaveSize(0);
+ expect(requests).toHaveLength(0);
processEnqueuedPackets();
- expect(onNext).toHaveBeenCalledOnceWith(response);
+ expect(onNext).toHaveBeenCalledWith(response);
expect(onError).not.toHaveBeenCalled();
- expect(onCompleted).toHaveBeenCalledOnceWith(Status.ABORTED);
+ expect(onCompleted).toHaveBeenCalledWith(Status.ABORTED);
}
});
@@ -355,14 +354,17 @@ describe('RPC', () => {
it('nonblocking call cancel', () => {
for (let i = 0; i < 3; i++) {
- const onNext = jasmine.createSpy();
+ const onNext = jest.fn();
const call = unaryStub.invoke(newRequest(), onNext);
expect(requests.length).toBeGreaterThan(0);
requests = [];
- expect(call.cancel()).toBeTrue();
- expect(call.cancel()).toBeFalse();
+ expect(call.cancel()).toBe(true);
+ expect(lastRequest().getType()).toEqual(PacketType.CLIENT_ERROR);
+ expect(lastRequest().getStatus()).toEqual(Status.CANCELLED);
+
+ expect(call.cancel()).toBe(false);
expect(onNext).not.toHaveBeenCalled();
}
});
@@ -378,11 +380,11 @@ describe('RPC', () => {
it('nonblocking duplicate calls first is cancelled', () => {
const firstCall = unaryStub.invoke(newRequest());
- expect(firstCall.completed).toBeFalse();
+ expect(firstCall.completed).toBe(false);
const secondCall = unaryStub.invoke(newRequest());
expect(firstCall.error).toEqual(Status.CANCELLED);
- expect(secondCall.completed).toBeFalse();
+ expect(secondCall.completed).toBe(false);
});
it('nonblocking exception in callback', () => {
@@ -417,15 +419,15 @@ describe('RPC', () => {
enqueueServerStream(1, serverStreaming.method, response2);
enqueueResponse(1, serverStreaming.method, Status.ABORTED);
- const onNext = jasmine.createSpy();
- const onCompleted = jasmine.createSpy();
- const onError = jasmine.createSpy();
+ const onNext = jest.fn();
+ const onCompleted = jest.fn();
+ const onError = jest.fn();
serverStreaming.invoke(newRequest(4), onNext, onCompleted, onError);
expect(onNext).toHaveBeenCalledWith(response1);
expect(onNext).toHaveBeenCalledWith(response2);
expect(onError).not.toHaveBeenCalled();
- expect(onCompleted).toHaveBeenCalledOnceWith(Status.ABORTED);
+ expect(onCompleted).toHaveBeenCalledWith(Status.ABORTED);
expect(
sentPayload(serverStreaming.method.requestType).getMagicNumber()
@@ -443,9 +445,9 @@ describe('RPC', () => {
enqueueServerStream(1, serverStreaming.method, response2);
enqueueResponse(1, serverStreaming.method, Status.ABORTED);
- const onNext = jasmine.createSpy();
- const onCompleted = jasmine.createSpy();
- const onError = jasmine.createSpy();
+ const onNext = jest.fn();
+ const onCompleted = jest.fn();
+ const onError = jest.fn();
const call = serverStreaming.open(
newRequest(3),
onNext,
@@ -453,13 +455,13 @@ describe('RPC', () => {
onError
);
- expect(requests).toHaveSize(0);
+ expect(requests).toHaveLength(0);
processEnqueuedPackets();
expect(onNext).toHaveBeenCalledWith(response1);
expect(onNext).toHaveBeenCalledWith(response2);
expect(onError).not.toHaveBeenCalled();
- expect(onCompleted).toHaveBeenCalledOnceWith(Status.ABORTED);
+ expect(onCompleted).toHaveBeenCalledWith(Status.ABORTED);
}
});
@@ -476,13 +478,13 @@ describe('RPC', () => {
const testResponse = newResponse('!!!');
enqueueServerStream(1, serverStreaming.method, testResponse);
- const onNext = jasmine.createSpy();
- const onCompleted = jasmine.createSpy();
- const onError = jasmine.createSpy();
+ const onNext = jest.fn();
+ const onCompleted = jest.fn();
+ const onError = jest.fn();
let call = serverStreaming.invoke(newRequest(3), onNext);
- expect(onNext).toHaveBeenCalledOnceWith(testResponse);
+ expect(onNext).toHaveBeenNthCalledWith(1, testResponse);
- onNext.calls.reset();
+ // onNext.calls.reset();
call.cancel();
expect(lastRequest().getType()).toEqual(PacketType.CLIENT_ERROR);
@@ -492,9 +494,9 @@ describe('RPC', () => {
enqueueServerStream(1, serverStreaming.method, testResponse);
enqueueResponse(1, serverStreaming.method, Status.OK);
call = serverStreaming.invoke(newRequest(), onNext, onCompleted, onError);
- expect(onNext).toHaveBeenCalledWith(testResponse);
+ expect(onNext).toHaveBeenNthCalledWith(2, testResponse);
expect(onError).not.toHaveBeenCalled();
- expect(onCompleted).toHaveBeenCalledOnceWith(Status.OK);
+ expect(onCompleted).toHaveBeenCalledWith(Status.OK);
});
});
@@ -513,14 +515,14 @@ describe('RPC', () => {
const testResponse = newResponse('-.-');
for (let i = 0; i < 3; i++) {
- const onNext = jasmine.createSpy();
+ const onNext = jest.fn();
const stream = clientStreaming.invoke(onNext);
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
stream.send(newRequest(31));
expect(lastRequest().getType()).toEqual(PacketType.CLIENT_STREAM);
expect(sentPayload(Request).getMagicNumber()).toEqual(31);
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
// Enqueue the server response to be sent after the next message.
enqueueResponse(1, clientStreaming.method, Status.OK, testResponse);
@@ -529,8 +531,8 @@ describe('RPC', () => {
expect(lastRequest().getType()).toEqual(PacketType.CLIENT_STREAM);
expect(sentPayload(Request).getMagicNumber()).toEqual(32);
- expect(onNext).toHaveBeenCalledOnceWith(testResponse);
- expect(stream.completed).toBeTrue();
+ expect(onNext).toHaveBeenCalledWith(testResponse);
+ expect(stream.completed).toBe(true);
expect(stream.status).toEqual(Status.OK);
expect(stream.error).toBeUndefined();
}
@@ -543,17 +545,17 @@ describe('RPC', () => {
for (let i = 0; i < 3; i++) {
enqueueResponse(1, clientStreaming.method, Status.OK, response);
- const onNext = jasmine.createSpy();
- const onCompleted = jasmine.createSpy();
- const onError = jasmine.createSpy();
+ const onNext = jest.fn();
+ const onCompleted = jest.fn();
+ const onError = jest.fn();
const call = clientStreaming.open(onNext, onCompleted, onError);
- expect(requests).toHaveSize(0);
+ expect(requests).toHaveLength(0);
processEnqueuedPackets();
expect(onNext).toHaveBeenCalledWith(response);
expect(onError).not.toHaveBeenCalled();
- expect(onCompleted).toHaveBeenCalledOnceWith(Status.OK);
+ expect(onCompleted).toHaveBeenCalledWith(Status.OK);
}
});
@@ -569,14 +571,14 @@ describe('RPC', () => {
it('non-blocking call ended by client', () => {
const testResponse = newResponse('0.o');
for (let i = 0; i < 3; i++) {
- const onNext = jasmine.createSpy();
+ const onNext = jest.fn();
const stream = clientStreaming.invoke(onNext);
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
stream.send(newRequest(31));
expect(lastRequest().getType()).toEqual(PacketType.CLIENT_STREAM);
expect(sentPayload(Request).getMagicNumber()).toEqual(31);
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
// Enqueue the server response to be sent after the next message.
enqueueResponse(1, clientStreaming.method, Status.OK, testResponse);
@@ -584,8 +586,8 @@ describe('RPC', () => {
stream.finishAndWait();
expect(lastRequest().getType()).toEqual(PacketType.CLIENT_STREAM_END);
- expect(onNext).toHaveBeenCalledOnceWith(testResponse);
- expect(stream.completed).toBeTrue();
+ expect(onNext).toHaveBeenCalledWith(testResponse);
+ expect(stream.completed).toBe(true);
expect(stream.status).toEqual(Status.OK);
expect(stream.error).toBeUndefined();
}
@@ -596,11 +598,11 @@ describe('RPC', () => {
const stream = clientStreaming.invoke();
stream.send(newRequest());
- expect(stream.cancel()).toBeTrue();
+ expect(stream.cancel()).toBe(true);
expect(lastRequest().getType()).toEqual(PacketType.CLIENT_ERROR);
expect(lastRequest().getStatus()).toEqual(Status.CANCELLED);
- expect(stream.cancel()).toBeFalse();
- expect(stream.completed).toBeTrue();
+ expect(stream.cancel()).toBe(false);
+ expect(stream.completed).toBe(true);
expect(stream.error).toEqual(Status.CANCELLED);
}
});
@@ -651,12 +653,20 @@ describe('RPC', () => {
});
it('non-blocking call send after cancelled', () => {
+ expect.assertions(2);
const stream = clientStreaming.invoke();
- expect(stream.cancel()).toBeTrue();
+ expect(stream.cancel()).toBe(true);
- expect(() => stream.send(newRequest())).toThrowMatching(
- error => error.status === Status.CANCELLED
- );
+ try {
+ stream.send(newRequest());
+ }
+ catch (e) {
+ console.log(e);
+ expect(e.status).toEqual(Status.CANCELLED);
+ }
+ // expect(() => stream.send(newRequest())).toThrowError(
+ // error => error.status === Status.CANCELLED
+ // );
});
it('non-blocking finish after completed', async () => {
@@ -696,11 +706,11 @@ describe('RPC', () => {
it('non-blocking duplicate calls first is cancelled', () => {
const firstCall = clientStreaming.invoke();
- expect(firstCall.completed).toBeFalse();
+ expect(firstCall.completed).toBe(false);
const secondCall = clientStreaming.invoke();
expect(firstCall.error).toEqual(Status.CANCELLED);
- expect(secondCall.completed).toBeFalse();
+ expect(secondCall.completed).toBe(false);
});
});
@@ -749,12 +759,12 @@ describe('RPC', () => {
const stream = bidiStreaming.invoke(response => {
testResponses.push(response);
});
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
stream.send(newRequest(55));
expect(lastRequest().getType()).toEqual(PacketType.CLIENT_STREAM);
expect(sentPayload(Request).getMagicNumber()).toEqual(55);
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
expect(testResponses).toEqual([]);
enqueueServerStream(1, bidiStreaming.method, rep1);
@@ -763,13 +773,13 @@ describe('RPC', () => {
stream.send(newRequest(66));
expect(lastRequest().getType()).toEqual(PacketType.CLIENT_STREAM);
expect(sentPayload(Request).getMagicNumber()).toEqual(66);
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
expect(testResponses).toEqual([rep1, rep2]);
enqueueResponse(1, bidiStreaming.method, Status.OK);
stream.send(newRequest(77));
- expect(stream.completed).toBeTrue();
+ expect(stream.completed).toBe(true);
expect(testResponses).toEqual([rep1, rep2]);
expect(stream.status).toEqual(Status.OK);
expect(stream.error).toBeUndefined();
@@ -786,18 +796,18 @@ describe('RPC', () => {
enqueueServerStream(1, bidiStreaming.method, response2);
enqueueResponse(1, bidiStreaming.method, Status.OK);
- const onNext = jasmine.createSpy();
- const onCompleted = jasmine.createSpy();
- const onError = jasmine.createSpy();
+ const onNext = jest.fn();
+ const onCompleted = jest.fn();
+ const onError = jest.fn();
const call = bidiStreaming.open(onNext, onCompleted, onError);
- expect(requests).toHaveSize(0);
+ expect(requests).toHaveLength(0);
processEnqueuedPackets();
expect(onNext).toHaveBeenCalledWith(response1);
expect(onNext).toHaveBeenCalledWith(response2);
expect(onError).not.toHaveBeenCalled();
- expect(onCompleted).toHaveBeenCalledOnceWith(Status.OK);
+ expect(onCompleted).toHaveBeenCalledWith(Status.OK);
}
});
@@ -818,18 +828,18 @@ describe('RPC', () => {
const stream = bidiStreaming.invoke(response => {
testResponses.push(response);
});
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
enqueueServerStream(1, bidiStreaming.method, response);
stream.send(newRequest(55));
- expect(stream.completed).toBeFalse();
+ expect(stream.completed).toBe(false);
expect(testResponses).toEqual([response]);
enqueueError(1, bidiStreaming.method, Status.OUT_OF_RANGE, Status.OK);
stream.send(newRequest(999));
- expect(stream.completed).toBeTrue();
+ expect(stream.completed).toBe(true);
expect(testResponses).toEqual([response]);
expect(stream.status).toBeUndefined();
expect(stream.error).toEqual(Status.OUT_OF_RANGE);
@@ -869,7 +879,7 @@ describe('RPC', () => {
it('non-blocking send after cancelled', async () => {
const stream = bidiStreaming.invoke();
- expect(stream.cancel()).toBeTrue();
+ expect(stream.cancel()).toBe(true);
try {
stream.send(newRequest());
@@ -913,10 +923,10 @@ describe('RPC', () => {
});
it('non-blocking duplicate calls first is cancelled', () => {
const firstCall = bidiStreaming.invoke();
- expect(firstCall.completed).toBeFalse();
+ expect(firstCall.completed).toBe(false);
const secondCall = bidiStreaming.invoke();
expect(firstCall.error).toEqual(Status.CANCELLED);
- expect(secondCall.completed).toBeFalse();
+ expect(secondCall.completed).toBe(false);
});
});
});
diff --git a/pw_rpc/ts/descriptors.ts b/pw_rpc/ts/descriptors.ts
index e36bee1cc..eb4581f3e 100644
--- a/pw_rpc/ts/descriptors.ts
+++ b/pw_rpc/ts/descriptors.ts
@@ -12,7 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {ProtoCollection} from '@pigweed/pw_protobuf_compiler';
+import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler';
import {
MethodDescriptorProto,
ServiceDescriptorProto,
@@ -78,6 +78,7 @@ export class Method {
readonly serverStreaming: boolean;
readonly requestType: any;
readonly responseType: any;
+ readonly descriptor: MethodDescriptorProto;
constructor(
descriptor: MethodDescriptorProto,
@@ -87,6 +88,7 @@ export class Method {
this.name = descriptor.getName()!;
this.id = hash(this.name);
this.service = service;
+ this.descriptor = descriptor;
this.serverStreaming = descriptor.getServerStreaming()!;
this.clientStreaming = descriptor.getClientStreaming()!;
diff --git a/pw_rpc/ts/descriptors_test.ts b/pw_rpc/ts/descriptors_test.ts
index 8af4f10b7..a6b11e26a 100644
--- a/pw_rpc/ts/descriptors_test.ts
+++ b/pw_rpc/ts/descriptors_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,14 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
-import 'jasmine';
+/* eslint-env browser */
-import {ProtoCollection} from 'rpc_proto_collection/generated/ts_proto_collection';
+import {ProtoCollection} from 'pigweedjs/protos/collection';
import {
Request,
Response,
-} from 'test_protos_tspb/test_protos_tspb_pb/pw_rpc/ts/test_pb';
+} from 'pigweedjs/protos/pw_rpc/ts/test_pb';
import * as descriptors from './descriptors';
@@ -28,7 +27,8 @@ const TEST_PROTO_PATH = 'pw_rpc/ts/test_protos-descriptor-set.proto.bin';
describe('Descriptors', () => {
it('parses from ServiceDescriptor binary', async () => {
const protoCollection = new ProtoCollection();
- const fd = protoCollection.fileDescriptorSet.getFileList()[0];
+ const fd = protoCollection.fileDescriptorSet.getFileList()
+ .find((file: any) => file.array[1].indexOf("pw.rpc.test1") !== -1);
const sd = fd.getServiceList()[0];
const service = new descriptors.Service(
sd,
@@ -41,16 +41,16 @@ describe('Descriptors', () => {
const unaryMethod = service.methodsByName.get('SomeUnary')!;
expect(unaryMethod.name).toEqual('SomeUnary');
- expect(unaryMethod.clientStreaming).toBeFalse();
- expect(unaryMethod.serverStreaming).toBeFalse();
+ expect(unaryMethod.clientStreaming).toBe(false);
+ expect(unaryMethod.serverStreaming).toBe(false);
expect(unaryMethod.service).toEqual(service);
expect(unaryMethod.requestType).toEqual(Request);
expect(unaryMethod.responseType).toEqual(Response);
const someBidiStreaming = service.methodsByName.get('SomeBidiStreaming')!;
expect(someBidiStreaming.name).toEqual('SomeBidiStreaming');
- expect(someBidiStreaming.clientStreaming).toBeTrue();
- expect(someBidiStreaming.serverStreaming).toBeTrue();
+ expect(someBidiStreaming.clientStreaming).toBe(true);
+ expect(someBidiStreaming.serverStreaming).toBe(true);
expect(someBidiStreaming.service).toEqual(service);
expect(someBidiStreaming.requestType).toEqual(Request);
expect(someBidiStreaming.responseType).toEqual(Response);
diff --git a/pw_rpc/ts/docs.rst b/pw_rpc/ts/docs.rst
index 8d719fce2..7d7a2c7c0 100644
--- a/pw_rpc/ts/docs.rst
+++ b/pw_rpc/ts/docs.rst
@@ -1,12 +1,12 @@
.. _module-pw_rpc-ts:
-------------------------
-pw_rpc Typescript package
+pw_rpc Web Module
-------------------------
-The ``pw_rpc`` Typescript package makes it possible to call Pigweed RPCs from
-Typescript. The package includes client library to facilitate handling RPCs.
+The ``pw_rpc`` module makes it possible to call Pigweed RPCs from
+TypeScript or JavaScript. The module includes client library to facilitate handling RPCs.
-This package is currently a work in progress.
+This module is currently a work in progress.
Creating an RPC Client
======================
@@ -14,38 +14,27 @@ The RPC client is instantiated from a list of channels and a set of protos.
.. code-block:: typescript
- const testProtoPath = 'pw_rpc/ts/test_protos-descriptor-set.proto.bin';
- const lib = await Library.fromFileDescriptorSet(
- testProtoPath, 'test_protos_tspb');
+ import { ProtoCollection } from 'pigweedjs/protos/collection';
+
const channels = [new Channel(1, savePacket), new Channel(5)];
- const client = Client.fromProtoSet(channels, lib);
+ const client = Client.fromProtoSet(channels, new ProtoCollection());
function savePacket(packetBytes: Uint8Array): void {
const packet = RpcPacket.deserializeBinary(packetBytes);
...
}
-The proto library must match the proto build rules. The first argument
-corresponds with the location of the ``proto_library`` build rule that generates
-a descriptor set for all src protos. The second argument corresponds with the
-name of the ``js_proto_library`` build rule that generates javascript based on
-the descriptor set. For instance, the previous example corresponds with the
-following build file: ``pw_rpc/ts/BUILD.bazel``.
-
-.. code-block::
-
- proto_library(
- name = "test_protos",
- srcs = [
- "test.proto",
- "test2.proto",
- ],
- )
-
- js_proto_library(
- name = "test_protos_tspb",
- protos = [":test_protos"],
- )
+To generate a ProtoSet/ProtoCollection from your own ``.proto`` files, use
+``pw_proto_compiler`` in your ``package.json`` like this:
+
+.. code-block:: javascript
+
+ ...
+ "scripts": {
+ "build-protos": "pw_proto_compiler -p protos/rpc1.proto -p protos/rpc2.proto --out dist/protos",
+
+This will generate a `collection.js` file which can be used similar to above
+example.
Finding an RPC Method
=====================
diff --git a/pw_rpc/ts/method.ts b/pw_rpc/ts/method.ts
index 7f10287e9..18cf6db07 100644
--- a/pw_rpc/ts/method.ts
+++ b/pw_rpc/ts/method.ts
@@ -12,7 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-import {Status} from '@pigweed/pw_status';
+import {Status} from 'pigweedjs/pw_status';
import {Message} from 'google-protobuf';
import {
diff --git a/pw_rpc/ts/package.json b/pw_rpc/ts/package.json
deleted file mode 100644
index 7648957ac..000000000
--- a/pw_rpc/ts/package.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "@pigweed/pw_rpc",
- "version": "1.0.0",
- "main": "index.js",
- "license": "Apache-2.0",
- "dependencies": {
- "@bazel/jasmine": "^4.1.0",
- "@pigweed/pw_protobuf_compiler": "link:../../pw_protobuf_compiler/ts",
- "@pigweed/pw_status": "link:../../pw_status/ts",
- "@types/google-protobuf": "^3.15.5",
- "@types/jasmine": "^3.9.0",
- "@types/node": "^16.10.2",
- "jasmine": "^3.9.0",
- "jasmine-core": "^3.9.0",
- "wait-queue": "^1.1.4"
- }
-}
diff --git a/pw_rpc/ts/packets.ts b/pw_rpc/ts/packets.ts
index 4cd414b51..68cf80393 100644
--- a/pw_rpc/ts/packets.ts
+++ b/pw_rpc/ts/packets.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -16,8 +16,8 @@
import {Message} from 'google-protobuf';
import {MethodDescriptorProto} from 'google-protobuf/google/protobuf/descriptor_pb';
-import * as packetPb from 'packet_proto_tspb/packet_proto_tspb_pb/pw_rpc/internal/packet_pb';
-import {Status} from '@pigweed/pw_status';
+import * as packetPb from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
+import {Status} from 'pigweedjs/pw_status';
// Channel, Service, Method
type idSet = [number, number, number];
diff --git a/pw_rpc/ts/packets_test.ts b/pw_rpc/ts/packets_test.ts
index d214518d7..b399e2578 100644
--- a/pw_rpc/ts/packets_test.ts
+++ b/pw_rpc/ts/packets_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,14 +12,12 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
-import 'jasmine';
-
+/* eslint-env browser */
import {
PacketType,
RpcPacket,
-} from 'packet_proto_tspb/packet_proto_tspb_pb/pw_rpc/internal/packet_pb';
-import {Status} from '@pigweed/pw_status';
+} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
+import {Status} from 'pigweedjs/pw_status';
import * as packets from './packets';
@@ -33,7 +31,7 @@ function addTestData(packet: RpcPacket) {
}
describe('Packets', () => {
- beforeEach(() => {});
+ beforeEach(() => { });
it('encodeRequest sets packet fields', () => {
const goldenRequest = new RpcPacket();
@@ -107,7 +105,7 @@ describe('Packets', () => {
const response = new RpcPacket();
response.setType(PacketType.RESPONSE);
- expect(packets.forServer(request)).toBeTrue();
- expect(packets.forServer(response)).toBeFalse();
+ expect(packets.forServer(request)).toBe(true);
+ expect(packets.forServer(response)).toBe(false);
});
});
diff --git a/pw_rpc/ts/queue.ts b/pw_rpc/ts/queue.ts
new file mode 100644
index 000000000..0bd2b8c6c
--- /dev/null
+++ b/pw_rpc/ts/queue.ts
@@ -0,0 +1,59 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/**
+ * Provides a simple array-based queue that will block caller on await
+ * queue.shift() if the queue is empty, until a new item is pushed to the
+ * queue. */
+
+export default class Queue<T> {
+ queue = Array<T>();
+ elementListeners = Array<() => void>();
+
+ get length(): number {
+ return this.queue.length;
+ }
+
+ push(...items: T[]): number {
+ this.queue.push(...items);
+ this._checkListeners();
+ return this.length;
+ }
+
+ shift(): Promise<T> {
+ return new Promise(resolve => {
+ if (this.length > 0) {
+ return resolve(this.queue.shift()!);
+ } else {
+ this.elementListeners.push(() => {
+ return resolve(this.queue.shift()!);
+ });
+ }
+ });
+ }
+
+ _checkListeners() {
+ if (this.length > 0 && this.elementListeners.length > 0) {
+ const listener = this.elementListeners.shift()!;
+ listener.call(this);
+ this._checkListeners();
+ }
+ }
+
+ unshift(...items: T[]): number {
+ this.queue.unshift(...items);
+ this._checkListeners();
+ return this.length;
+ }
+}
diff --git a/pw_rpc/ts/rpc_classes.ts b/pw_rpc/ts/rpc_classes.ts
index e29362dd5..703612e19 100644
--- a/pw_rpc/ts/rpc_classes.ts
+++ b/pw_rpc/ts/rpc_classes.ts
@@ -13,7 +13,7 @@
// the License.
import {Message} from 'google-protobuf';
-import {Status} from '@pigweed/pw_status';
+import {Status} from 'pigweedjs/pw_status';
import {Call} from './call';
import {Channel, Method, Service} from './descriptors';
@@ -112,13 +112,10 @@ export class PendingCalls {
rpc.channel.send(packets.encodeClientStreamEnd(rpc.idSet));
}
- /** Cancels the RPC. Returns the CANCEL packet to send. */
- cancel(rpc: Rpc): Uint8Array | undefined {
+ /** Cancels the RPC. Returns the CLIENT_ERROR packet to send. */
+ cancel(rpc: Rpc): Uint8Array {
console.debug(`Cancelling ${rpc}`);
this.pending.delete(rpc.idString);
- if (rpc.method.clientStreaming && rpc.method.serverStreaming) {
- return undefined;
- }
return packets.encodeCancel(rpc.idSet);
}
diff --git a/pw_rpc/ts/tsconfig.json b/pw_rpc/ts/tsconfig.json
deleted file mode 100644
index 4ddd637e9..000000000
--- a/pw_rpc/ts/tsconfig.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "compilerOptions": {
- "allowUnreachableCode": false,
- "allowUnusedLabels": false,
- "declaration": true,
- "forceConsistentCasingInFileNames": true,
- "lib": [
- "es2018",
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "module": "commonjs",
- "noEmitOnError": true,
- "noFallthroughCasesInSwitch": true,
- "noImplicitReturns": true,
- "pretty": true,
- "sourceMap": true,
- "strict": true,
- "target": "es2018",
- "jsx": "react",
- "plugins": [
- {
- "name": "@bazel/tsetse",
- "disabledRules": [
- "must-use-promises"
- ]
- }
- ]
- },
- "exclude": [
- "node_modules"
- ]
-}
diff --git a/pw_rpc/ts/yarn.lock b/pw_rpc/ts/yarn.lock
deleted file mode 100644
index f38b2fbb6..000000000
--- a/pw_rpc/ts/yarn.lock
+++ /dev/null
@@ -1,510 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@bazel/jasmine@^4.1.0":
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-4.3.0.tgz#d2dd29deb56cffae2b3bd7be706fb1b3dd532fc7"
- integrity sha512-lROo6iAdyqmqVNe8M5or6Vkzcn5wyBSI4MJBqqLJVjejhlhU6Mg27j1xC+VJPlnQkiEyeHLV5WNndp50ROivSw==
- dependencies:
- c8 "~7.5.0"
- jasmine-reporters "~2.4.0"
-
-"@bcoe/v8-coverage@^0.2.3":
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
- integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
-
-"@istanbuljs/schema@^0.1.2":
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
- integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
-
-"@pigweed/pw_protobuf_compiler@link:../../pw_protobuf_compiler/ts":
- version "0.0.0"
- uid ""
-
-"@pigweed/pw_status@link:../../pw_status/ts":
- version "0.0.0"
- uid ""
-
-"@types/argparse@^2.0.10":
- version "2.0.10"
- resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-2.0.10.tgz#664e84808accd1987548d888b9d21b3e9c996a6c"
- integrity sha512-C4wahC3gz3vQtvPazrJ5ONwmK1zSDllQboiWvpMM/iOswCYfBREFnjFbq/iWKIVOCl8+m5Pk6eva6/ZSsDuIGA==
-
-"@types/google-protobuf@^3.15.5":
- version "3.15.5"
- resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.5.tgz#644b2be0f5613b1f822c70c73c6b0e0b5b5fa2ad"
- integrity sha512-6bgv24B+A2bo9AfzReeg5StdiijKzwwnRflA8RLd1V4Yv995LeTmo0z69/MPbBDFSiZWdZHQygLo/ccXhMEDgw==
-
-"@types/is-windows@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-1.0.0.tgz#1011fa129d87091e2f6faf9042d6704cdf2e7be0"
- integrity sha512-tJ1rq04tGKuIJoWIH0Gyuwv4RQ3+tIu7wQrC0MV47raQ44kIzXSSFKfrxFUOWVRvesoF7mrTqigXmqoZJsXwTg==
-
-"@types/istanbul-lib-coverage@^2.0.1":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
- integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
-
-"@types/jasmine@^3.9.0":
- version "3.9.1"
- resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.9.1.tgz#94c65ee8bf9d24d9e1d84abaed57b6e0da8b49de"
- integrity sha512-PVpjh8S8lqKFKurWSKdFATlfBHGPzgy0PoDdzQ+rr78jTQ0aacyh9YndzZcAUPxhk4kRujItFFGQdUJ7flHumw==
-
-"@types/node@^16.10.2":
- version "16.10.2"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.2.tgz#5764ca9aa94470adb4e1185fe2e9f19458992b2e"
- integrity sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==
-
-ansi-regex@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
- integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
- dependencies:
- color-convert "^2.0.1"
-
-argparse@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
- integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
-
-balanced-match@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
- integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-js@^1.5.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
- integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-c8@~7.5.0:
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/c8/-/c8-7.5.0.tgz#a69439ab82848f344a74bb25dc5dd4e867764481"
- integrity sha512-GSkLsbvDr+FIwjNSJ8OwzWAyuznEYGTAd1pzb/Kr0FMLuV4vqYJTyjboDTwmlUNAG6jAU3PFWzqIdKrOt1D8tw==
- dependencies:
- "@bcoe/v8-coverage" "^0.2.3"
- "@istanbuljs/schema" "^0.1.2"
- find-up "^5.0.0"
- foreground-child "^2.0.0"
- furi "^2.0.0"
- istanbul-lib-coverage "^3.0.0"
- istanbul-lib-report "^3.0.0"
- istanbul-reports "^3.0.2"
- rimraf "^3.0.0"
- test-exclude "^6.0.0"
- v8-to-istanbul "^7.1.0"
- yargs "^16.0.0"
- yargs-parser "^20.0.0"
-
-cliui@^7.0.2:
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
- integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^7.0.0"
-
-color-convert@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
- integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
- dependencies:
- color-name "~1.1.4"
-
-color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-
-convert-source-map@^1.6.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
- integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
- dependencies:
- safe-buffer "~5.1.1"
-
-cross-spawn@^7.0.0:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
- integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
- dependencies:
- path-key "^3.1.0"
- shebang-command "^2.0.0"
- which "^2.0.1"
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-escalade@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
- integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-find-up@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
- integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
- dependencies:
- locate-path "^6.0.0"
- path-exists "^4.0.0"
-
-foreground-child@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53"
- integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==
- dependencies:
- cross-spawn "^7.0.0"
- signal-exit "^3.0.2"
-
-fs.realpath@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
- integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-
-furi@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/furi/-/furi-2.0.0.tgz#13d85826a1af21acc691da6254b3888fc39f0b4a"
- integrity sha512-uKuNsaU0WVaK/vmvj23wW1bicOFfyqSsAIH71bRZx8kA4Xj+YCHin7CJKJJjkIsmxYaPFLk9ljmjEyB7xF7WvQ==
- dependencies:
- "@types/is-windows" "^1.0.0"
- is-windows "^1.0.2"
-
-get-caller-file@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
- integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
- integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-google-protobuf@^3.19.0:
- version "3.19.0"
- resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.0.tgz#97f474323c92f19fd6737af1bb792e396991e0b8"
- integrity sha512-qXGAiv3OOlaJXJNeKOBKxbBAwjsxzhx+12ZdKOkZTsqsRkyiQRmr/nBkAkqnuQ8cmA9X5NVXvObQTpHVnXE2DQ==
-
-has-flag@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
- integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-html-escaper@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
- integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
-
-inflight@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
- integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
- dependencies:
- once "^1.3.0"
- wrappy "1"
-
-inherits@2:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-windows@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
- integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
-
-isexe@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
- integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
-
-istanbul-lib-coverage@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.1.tgz#e8900b3ed6069759229cf30f7067388d148aeb5e"
- integrity sha512-GvCYYTxaCPqwMjobtVcVKvSHtAGe48MNhGjpK8LtVF8K0ISX7hCKl85LgtuaSneWVyQmaGcW3iXVV3GaZSLpmQ==
-
-istanbul-lib-report@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
- integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==
- dependencies:
- istanbul-lib-coverage "^3.0.0"
- make-dir "^3.0.0"
- supports-color "^7.1.0"
-
-istanbul-reports@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.2.tgz#d593210e5000683750cb09fc0644e4b6e27fd53b"
- integrity sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==
- dependencies:
- html-escaper "^2.0.0"
- istanbul-lib-report "^3.0.0"
-
-jasmine-core@^3.9.0, jasmine-core@~3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.9.0.tgz#09a3c8169fe98ec69440476d04a0e4cb4d59e452"
- integrity sha512-Tv3kVbPCGVrjsnHBZ38NsPU3sDOtNa0XmbG2baiyJqdb5/SPpDO6GVwJYtUryl6KB4q1Ssckwg612ES9Z0dreQ==
-
-jasmine-reporters@~2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.4.0.tgz#708c17ae70ba6671e3a930bb1b202aab80a31409"
- integrity sha512-jxONSrBLN1vz/8zCx5YNWQSS8iyDAlXQ5yk1LuqITe4C6iXCDx5u6Q0jfNtkKhL4qLZPe69fL+AWvXFt9/x38w==
- dependencies:
- mkdirp "^0.5.1"
- xmldom "^0.5.0"
-
-jasmine@^3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.9.0.tgz#286c4f9f88b69defc24acf3989af5533d5c6a0e6"
- integrity sha512-JgtzteG7xnqZZ51fg7N2/wiQmXon09szkALcRMTgCMX4u/m17gVJFjObnvw5FXkZOWuweHPaPRVB6DI2uN0wVA==
- dependencies:
- glob "^7.1.6"
- jasmine-core "~3.9.0"
-
-locate-path@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
- integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
- dependencies:
- p-locate "^5.0.0"
-
-make-dir@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
- integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
- dependencies:
- semver "^6.0.0"
-
-minimatch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimist@^1.2.5:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
- integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
-
-mkdirp@^0.5.1:
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
- integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
- dependencies:
- minimist "^1.2.5"
-
-once@^1.3.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
- integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
- dependencies:
- wrappy "1"
-
-p-limit@^3.0.2:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
- integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
- dependencies:
- yocto-queue "^0.1.0"
-
-p-locate@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
- integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
- dependencies:
- p-limit "^3.0.2"
-
-path-exists@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
- integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-is-absolute@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
- integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-
-path-key@^3.1.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
- integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-
-require-directory@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
- integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
-
-rimraf@^3.0.0:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
-safe-buffer@~5.1.1:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
- integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
-semver@^6.0.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
- integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-shebang-command@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
- integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
- dependencies:
- shebang-regex "^3.0.0"
-
-shebang-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
- integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-
-signal-exit@^3.0.2:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
- integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
-
-source-map@^0.7.3:
- version "0.7.3"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
- integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
-
-string-width@^4.1.0, string-width@^4.2.0:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-supports-color@^7.1.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
- integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
- dependencies:
- has-flag "^4.0.0"
-
-test-exclude@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
- integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
- dependencies:
- "@istanbuljs/schema" "^0.1.2"
- glob "^7.1.4"
- minimatch "^3.0.4"
-
-v8-to-istanbul@^7.1.0:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1"
- integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==
- dependencies:
- "@types/istanbul-lib-coverage" "^2.0.1"
- convert-source-map "^1.6.0"
- source-map "^0.7.3"
-
-wait-queue@^1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/wait-queue/-/wait-queue-1.1.4.tgz#344f9bdd6e011ddc0bb1e3252eeb41234f7a8a85"
- integrity sha512-/VdMghiBDG/Ch43ZRp3d8OSd8A0dx8hfkBO7AfWCDzMn2blHquMf+3gqHHhYcggSBpKf7VTzA939bb0DevYKBA==
-
-which@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
- integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
- dependencies:
- isexe "^2.0.0"
-
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrappy@1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
- integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-
-xmldom@^0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e"
- integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==
-
-y18n@^5.0.5:
- version "5.0.8"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
- integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yargs-parser@^20.0.0, yargs-parser@^20.2.2:
- version "20.2.9"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
- integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
-yargs@^16.0.0:
- version "16.2.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
- integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
- dependencies:
- cliui "^7.0.2"
- escalade "^3.1.1"
- get-caller-file "^2.0.5"
- require-directory "^2.1.1"
- string-width "^4.2.0"
- y18n "^5.0.5"
- yargs-parser "^20.2.2"
-
-yocto-queue@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
- integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
diff --git a/pw_rust/BUILD.bazel b/pw_rust/BUILD.bazel
new file mode 100644
index 000000000..74efcfc3d
--- /dev/null
+++ b/pw_rust/BUILD.bazel
@@ -0,0 +1,13 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
diff --git a/pw_rust/BUILD.gn b/pw_rust/BUILD.gn
new file mode 100644
index 000000000..ab4a0d832
--- /dev/null
+++ b/pw_rust/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_rust/docs.rst b/pw_rust/docs.rst
new file mode 100644
index 000000000..b917b54ce
--- /dev/null
+++ b/pw_rust/docs.rst
@@ -0,0 +1,63 @@
+.. _module-pw_rust:
+
+=======
+pw_rust
+=======
+Rust support in pigweed is **highly** experimental. Currently functionality
+is split between Bazel and GN support.
+
+-----
+Bazel
+-----
+Bazel support is based on `rules_rust <https://github.com/bazelbuild/rules_rust>`_
+and supports a rich set of targets for both host and target builds.
+
+Building and Running the Embedded Example
+=========================================
+The ``embedded_hello`` example can be built for both the ``lm3s6965evb``
+and ``microbit`` QEMU machines. The example can be built and run using
+the following commands where ``PLATFORM`` is one of ``lm3s6965evb`` or
+``microbit``.
+
+.. code:: bash
+
+ $ bazel build //pw_rust/examples/embedded_hello:hello \
+ --platforms //pw_build/platforms:${PLATFORM} \
+
+ $ qemu-system-arm \
+ -machine ${PLATFORM} \
+ -nographic \
+ -semihosting-config enable=on,target=native \
+ -kernel ./bazel-bin/pw_rust/examples/embedded_hello/hello
+ Hello, Pigweed!
+
+--
+GN
+--
+In GN, currently only building a single host binary using the standard
+libraries is supported. Windows builds are currently unsupported.
+
+Building
+========
+To build the sample rust targets, you need to enable
+``pw_rust_ENABLE_EXPERIMENTAL_BUILD``:
+
+.. code:: bash
+
+ $ gn gen out --args="pw_rust_ENABLE_EXPERIMENTAL_BUILD=true"
+
+Once that is set, you can build and run the ``hello`` example:
+
+.. code:: bash
+
+ $ ninja -C out host_clang_debug/obj/pw_rust/example/bin/hello
+ $ ./out/host_clang_debug/obj/pw_rust/examples/host_executable/bin/hello
+ Hello, Pigweed!
+
+------------------
+Third Party Crates
+------------------
+Thrid party crates are vendored in the
+`third_party/rust_crates <https://pigweed.googlesource.com/third_party/rust_crates>`_
+respository. Currently referencing these is only supported through the bazel
+build.
diff --git a/pw_rust/examples/embedded_hello/BUILD.bazel b/pw_rust/examples/embedded_hello/BUILD.bazel
new file mode 100644
index 000000000..e8309654c
--- /dev/null
+++ b/pw_rust/examples/embedded_hello/BUILD.bazel
@@ -0,0 +1,39 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_rust//rust:defs.bzl", "rust_binary")
+
+package(default_visibility = ["//visibility:public"])
+
+rust_binary(
+ name = "hello",
+ srcs = ["src/main.rs"],
+ edition = "2021",
+ linker_script = select({
+ "//pw_build/constraints/board:microbit": "qemu-rust-nrf51822.ld",
+ "//pw_build/constraints/chipset:lm3s6965evb": "qemu-rust-lm3s6965.ld",
+ }),
+ target_compatible_with = select({
+ "@platforms//os:linux": ["@platforms//:incompatible"],
+ "@platforms//os:macos": ["@platforms//:incompatible"],
+ "//pw_build/constraints/board:microbit": [],
+ "//pw_build/constraints/chipset:lm3s6965evb": [],
+ }),
+ deps = [
+ "@rust_crates//crates:cortex-m",
+ "@rust_crates//crates:cortex-m-rt",
+ "@rust_crates//crates:cortex-m-semihosting",
+ "@rust_crates//crates:panic-halt",
+ ],
+)
diff --git a/pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld b/pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld
new file mode 100644
index 000000000..7e65d2509
--- /dev/null
+++ b/pw_rust/examples/embedded_hello/qemu-rust-lm3s6965.ld
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2023 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/* This relatively simplified linker script will work with many ARMv7-M and
+ * ARMv8-M cores that have on-board memory-mapped RAM and FLASH. For more
+ * complex projects and devices, it's possible this linker script will not be
+ * sufficient as-is.
+ *
+ * This linker script is likely not suitable for a project with a bootloader.
+ */
+
+/* Note: This technically doesn't set the firmware's entry point. Setting the
+ * firmware entry point is done by setting vector_table[1]
+ * (Reset_Handler). However, this DOES tell the compiler how to optimize
+ * when --gc-sections is enabled.
+ */
+ENTRY(Reset)
+
+MEMORY
+{
+ /* TODO(b/234892223): Make it possible for projects to freely customize
+ * memory regions.
+ */
+
+ /* Vector Table (typically in flash) */
+ VECTOR_TABLE(rx) : ORIGIN = 0x00000000, LENGTH = 1024
+ /* Internal Flash */
+ FLASH(rx) : ORIGIN = 0x00000400, LENGTH = 255K
+ /* Internal SRAM */
+ RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 64K
+
+ /* Each memory region above has an associated .*.unused_space section that
+ * overlays the unused space at the end of the memory segment. These segments
+ * are used by pw_bloat.bloaty_config to create the utilization data source
+ * for bloaty size reports.
+ *
+ * These sections MUST be located immediately after the last section that is
+ * placed in the respective memory region or lld will issue a warning like:
+ *
+ * warning: ignoring memory region assignment for non-allocatable section
+ * '.VECTOR_TABLE.unused_space'
+ *
+ * If this warning occurs, it's also likely that LLD will have created quite
+ * large padded regions in the ELF file due to bad cursor operations. This
+ * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
+ * megabytes.
+ *
+ * Attempting to add sections to the memory region AFTER the unused_space
+ * section will cause the region to overflow.
+ */
+}
+
+SECTIONS
+{
+ /* This is the link-time vector table. If used, the VTOR (Vector Table Offset
+ * Register) MUST point to this memory location in order to be used. This can
+ * be done by ensuring this section exists at the default location of the VTOR
+ * so it's used on reset, or by explicitly setting the VTOR in a bootloader
+ * manually to point to &pw_boot_vector_table_addr before interrupts are
+ * enabled.
+ *
+ * The ARMv7-M architecture requires this is at least aligned to 128 bytes,
+ * and aligned to a power of two that is greater than 4 times the number of
+ * supported exceptions. 512 has been selected as it accommodates most
+ * devices' vector tables.
+ */
+ .vector_table : ALIGN(512)
+ {
+ LONG(pw_boot_stack_high_addr);
+ pw_boot_vector_table_addr = .;
+ KEEP(*(.vector_table))
+ KEEP(*(.vector_table.reset_vector))
+ KEEP(*(.vector_table.exceptions))
+ KEEP(*(.vector_table.interrupts))
+ } >VECTOR_TABLE
+
+ /* Represents unused space in the VECTOR_TABLE segment. This MUST be the last
+ * section assigned to the VECTOR_TABLE region.
+ */
+ .VECTOR_TABLE.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE));
+ } >VECTOR_TABLE
+
+ /* Main executable code. */
+ .code : ALIGN(4)
+ {
+ . = ALIGN(4);
+ /* cortex-m-rt expects these to be nearby each other because short branches
+ * are used.
+ */
+ *(.PreResetTrampoline);
+ *(.Reset);
+ *(.HardFaultTrampoline);
+ *(.HardFault.*);
+
+ /* Application code. */
+ *(.text)
+ *(.text*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+
+ . = ALIGN(4);
+ /* Constants.*/
+ *(.rodata)
+ *(.rodata*)
+
+ /* .preinit_array, .init_array, .fini_array are used by libc.
+ * Each section is a list of function pointers that are called pre-main and
+ * post-exit for object initialization and tear-down.
+ * Since the region isn't explicitly referenced, specify KEEP to prevent
+ * link-time garbage collection. SORT is used for sections that have strict
+ * init/de-init ordering requirements. */
+ . = ALIGN(4);
+ PROVIDE_HIDDEN(__preinit_array_start = .);
+ KEEP(*(.preinit_array*))
+ PROVIDE_HIDDEN(__preinit_array_end = .);
+
+ PROVIDE_HIDDEN(__init_array_start = .);
+ KEEP(*(SORT(.init_array.*)))
+ KEEP(*(.init_array*))
+ PROVIDE_HIDDEN(__init_array_end = .);
+
+ PROVIDE_HIDDEN(__fini_array_start = .);
+ KEEP(*(SORT(.fini_array.*)))
+ KEEP(*(.fini_array*))
+ PROVIDE_HIDDEN(__fini_array_end = .);
+ } >FLASH
+
+ /* Used by unwind-arm/ */
+ .ARM : ALIGN(4) {
+ __exidx_start = .;
+ *(.ARM.exidx*)
+ __exidx_end = .;
+ } >FLASH
+
+ /* Explicitly initialized global and static data. (.data)*/
+ .static_init_ram : ALIGN(4)
+ {
+ *(.data)
+ *(.data*)
+ . = ALIGN(4);
+ } >RAM AT> FLASH
+
+ /* Represents unused space in the FLASH segment. This MUST be the last section
+ * assigned to the FLASH region.
+ */
+ .FLASH.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(FLASH) + LENGTH(FLASH));
+ } >FLASH
+
+ /* The .zero_init_ram, .heap, and .stack sections below require (NOLOAD)
+ * annotations for LLVM lld, but not GNU ld, because LLVM's lld intentionally
+ * interprets the linker file differently from ld:
+ *
+ * https://discourse.llvm.org/t/lld-vs-ld-section-type-progbits-vs-nobits/5999/3
+ *
+ * Zero initialized global/static data (.bss) is initialized in
+ * pw_boot_Entry() via memset(), so the section doesn't need to be loaded from
+ * flash. The .heap and .stack sections don't require any initialization,
+ * as they only represent allocated memory regions, so they also do not need
+ * to be loaded.
+ */
+ .zero_init_ram (NOLOAD) : ALIGN(4)
+ {
+ *(.bss)
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ } >RAM
+
+ .heap (NOLOAD) : ALIGN(4)
+ {
+ pw_boot_heap_low_addr = .;
+ . = . + 0;
+ . = ALIGN(4);
+ pw_boot_heap_high_addr = .;
+ } >RAM
+
+ /* Link-time check for stack overlaps.
+ *
+ * The ARMv7-M architecture may require 8-byte alignment of the stack pointer
+ * rather than 4 in some contexts and implementations, so this region is
+ * 8-byte aligned (see ARMv7-M Architecture Reference Manual DDI0403E
+ * section B1.5.7).
+ */
+ .stack (NOLOAD) : ALIGN(8)
+ {
+ /* Set the address that the main stack pointer should be initialized to. */
+ pw_boot_stack_low_addr = .;
+ HIDDEN(_stack_size = ORIGIN(RAM) + LENGTH(RAM) - .);
+ /* Align the stack to a lower address to ensure it isn't out of range. */
+ HIDDEN(_stack_high = (. + _stack_size) & ~0x7);
+ ASSERT(_stack_high - . >= 1K,
+ "Error: Not enough RAM for desired minimum stack size.");
+ . = _stack_high;
+ pw_boot_stack_high_addr = .;
+ } >RAM
+
+ /* Represents unused space in the RAM segment. This MUST be the last section
+ * assigned to the RAM region.
+ */
+ .RAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(RAM) + LENGTH(RAM));
+ } >RAM
+
+ /* Discard unwind info. */
+ .ARM.extab 0x0 (INFO) :
+ {
+ KEEP(*(.ARM.extab*))
+ }
+}
+
+/* Symbols used by core_init.c: */
+/* Start of .static_init_ram in FLASH. */
+_pw_static_init_flash_start = LOADADDR(.static_init_ram);
+
+/* Region of .static_init_ram in RAM. */
+_pw_static_init_ram_start = ADDR(.static_init_ram);
+_pw_static_init_ram_end = _pw_static_init_ram_start + SIZEOF(.static_init_ram);
+
+/* Region of .zero_init_ram. */
+_pw_zero_init_ram_start = ADDR(.zero_init_ram);
+_pw_zero_init_ram_end = _pw_zero_init_ram_start + SIZEOF(.zero_init_ram);
+
+/* Symbols needed for the Rust cortex-m-rt crate. */
+__sbss = _pw_zero_init_ram_start;
+__ebss = _pw_zero_init_ram_end;
+__sdata = _pw_static_init_ram_start;
+__edata = _pw_static_init_ram_end;
+__sidata = LOADADDR(.static_init_ram);
+__pre_init = DefaultPreInit;
+DefaultHandler = DefaultHandler_;
+NonMaskableInt = DefaultHandler;
+MemoryManagement = DefaultHandler;
+BusFault = DefaultHandler;
+UsageFault = DefaultHandler;
+SVCall = DefaultHandler;
+DebugMonitor = DefaultHandler;
+PendSV = DefaultHandler;
+SysTick = DefaultHandler;
+HardFault = HardFault_;
+
+/* arm-none-eabi expects `end` symbol to point to start of heap for sbrk. */
+PROVIDE(end = _pw_zero_init_ram_end);
+
+/* These symbols are used by pw_bloat.bloaty_config to create the memoryregions
+ * data source for bloaty in this format (where the optional _N defaults to 0):
+ * pw_bloat_config_memory_region_NAME_{start,end}{_N,} */
+pw_bloat_config_memory_region_VECTOR_TABLE_start = ORIGIN(VECTOR_TABLE);
+pw_bloat_config_memory_region_VECTOR_TABLE_end =
+ ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE);
+pw_bloat_config_memory_region_FLASH_start = ORIGIN(FLASH);
+pw_bloat_config_memory_region_FLASH_end = ORIGIN(FLASH) + LENGTH(FLASH);
+pw_bloat_config_memory_region_RAM_start = ORIGIN(RAM);
+pw_bloat_config_memory_region_RAM_end = ORIGIN(RAM) + LENGTH(RAM);
diff --git a/pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld b/pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld
new file mode 100644
index 000000000..f8366acdd
--- /dev/null
+++ b/pw_rust/examples/embedded_hello/qemu-rust-nrf51822.ld
@@ -0,0 +1,271 @@
+/*
+ * Copyright 2023 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/* This relatively simplified linker script will work with many ARMv7-M and
+ * ARMv8-M cores that have on-board memory-mapped RAM and FLASH. For more
+ * complex projects and devices, it's possible this linker script will not be
+ * sufficient as-is.
+ *
+ * This linker script is likely not suitable for a project with a bootloader.
+ */
+
+/* Note: This technically doesn't set the firmware's entry point. Setting the
+ * firmware entry point is done by setting vector_table[1]
+ * (Reset_Handler). However, this DOES tell the compiler how to optimize
+ * when --gc-sections is enabled.
+ */
+ENTRY(Reset)
+
+MEMORY
+{
+ /* TODO(b/234892223): Make it possible for projects to freely customize
+ * memory regions.
+ */
+
+ /* Vector Table (typically in flash) */
+ VECTOR_TABLE(rx) : ORIGIN = 0x00000000, LENGTH = 1024
+ /* Internal Flash */
+ FLASH(rx) : ORIGIN = 0x00000400, LENGTH = 255K
+ /* Internal SRAM */
+ RAM(rwx) : ORIGIN = 0x20000000, LENGTH = 16K
+
+ /* Each memory region above has an associated .*.unused_space section that
+ * overlays the unused space at the end of the memory segment. These segments
+ * are used by pw_bloat.bloaty_config to create the utilization data source
+ * for bloaty size reports.
+ *
+ * These sections MUST be located immediately after the last section that is
+ * placed in the respective memory region or lld will issue a warning like:
+ *
+ * warning: ignoring memory region assignment for non-allocatable section
+ * '.VECTOR_TABLE.unused_space'
+ *
+ * If this warning occurs, it's also likely that LLD will have created quite
+ * large padded regions in the ELF file due to bad cursor operations. This
+ * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
+ * megabytes.
+ *
+ * Attempting to add sections to the memory region AFTER the unused_space
+ * section will cause the region to overflow.
+ */
+}
+
+SECTIONS
+{
+ /* This is the link-time vector table. If used, the VTOR (Vector Table Offset
+ * Register) MUST point to this memory location in order to be used. This can
+ * be done by ensuring this section exists at the default location of the VTOR
+ * so it's used on reset, or by explicitly setting the VTOR in a bootloader
+ * manually to point to &pw_boot_vector_table_addr before interrupts are
+ * enabled.
+ *
+ * The ARMv7-M architecture requires this is at least aligned to 128 bytes,
+ * and aligned to a power of two that is greater than 4 times the number of
+ * supported exceptions. 512 has been selected as it accommodates most
+ * devices' vector tables.
+ */
+ .vector_table : ALIGN(512)
+ {
+ LONG(pw_boot_stack_high_addr);
+ pw_boot_vector_table_addr = .;
+ KEEP(*(.vector_table))
+ KEEP(*(.vector_table.reset_vector))
+ KEEP(*(.vector_table.exceptions))
+ KEEP(*(.vector_table.interrupts))
+ } >VECTOR_TABLE
+
+ /* Represents unused space in the VECTOR_TABLE segment. This MUST be the last
+ * section assigned to the VECTOR_TABLE region.
+ */
+ .VECTOR_TABLE.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE));
+ } >VECTOR_TABLE
+
+ /* Main executable code. */
+ .code : ALIGN(4)
+ {
+ . = ALIGN(4);
+ /* cortex-m-rt expects these to be nearby each other because short branches
+ * are used.
+ */
+ *(.PreResetTrampoline);
+ *(.Reset);
+ *(.HardFaultTrampoline);
+ *(.HardFault.*);
+
+ /* Application code. */
+ *(.text)
+ *(.text*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+
+ . = ALIGN(4);
+ /* Constants.*/
+ *(.rodata)
+ *(.rodata*)
+
+ /* .preinit_array, .init_array, .fini_array are used by libc.
+ * Each section is a list of function pointers that are called pre-main and
+ * post-exit for object initialization and tear-down.
+ * Since the region isn't explicitly referenced, specify KEEP to prevent
+ * link-time garbage collection. SORT is used for sections that have strict
+ * init/de-init ordering requirements. */
+ . = ALIGN(4);
+ PROVIDE_HIDDEN(__preinit_array_start = .);
+ KEEP(*(.preinit_array*))
+ PROVIDE_HIDDEN(__preinit_array_end = .);
+
+ PROVIDE_HIDDEN(__init_array_start = .);
+ KEEP(*(SORT(.init_array.*)))
+ KEEP(*(.init_array*))
+ PROVIDE_HIDDEN(__init_array_end = .);
+
+ PROVIDE_HIDDEN(__fini_array_start = .);
+ KEEP(*(SORT(.fini_array.*)))
+ KEEP(*(.fini_array*))
+ PROVIDE_HIDDEN(__fini_array_end = .);
+ } >FLASH
+
+ /* Used by unwind-arm/ */
+ .ARM : ALIGN(4) {
+ __exidx_start = .;
+ *(.ARM.exidx*)
+ __exidx_end = .;
+ } >FLASH
+
+ /* Explicitly initialized global and static data. (.data)*/
+ .static_init_ram : ALIGN(4)
+ {
+ *(.data)
+ *(.data*)
+ . = ALIGN(4);
+ } >RAM AT> FLASH
+
+ /* Represents unused space in the FLASH segment. This MUST be the last section
+ * assigned to the FLASH region.
+ */
+ .FLASH.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(FLASH) + LENGTH(FLASH));
+ } >FLASH
+
+ /* The .zero_init_ram, .heap, and .stack sections below require (NOLOAD)
+ * annotations for LLVM lld, but not GNU ld, because LLVM's lld intentionally
+ * interprets the linker file differently from ld:
+ *
+ * https://discourse.llvm.org/t/lld-vs-ld-section-type-progbits-vs-nobits/5999/3
+ *
+ * Zero initialized global/static data (.bss) is initialized in
+ * pw_boot_Entry() via memset(), so the section doesn't need to be loaded from
+ * flash. The .heap and .stack sections don't require any initialization,
+ * as they only represent allocated memory regions, so they also do not need
+ * to be loaded.
+ */
+ .zero_init_ram (NOLOAD) : ALIGN(4)
+ {
+ *(.bss)
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ } >RAM
+
+ .heap (NOLOAD) : ALIGN(4)
+ {
+ pw_boot_heap_low_addr = .;
+ . = . + 0;
+ . = ALIGN(4);
+ pw_boot_heap_high_addr = .;
+ } >RAM
+
+ /* Link-time check for stack overlaps.
+ *
+ * The ARMv7-M architecture may require 8-byte alignment of the stack pointer
+ * rather than 4 in some contexts and implementations, so this region is
+ * 8-byte aligned (see ARMv7-M Architecture Reference Manual DDI0403E
+ * section B1.5.7).
+ */
+ .stack (NOLOAD) : ALIGN(8)
+ {
+ /* Set the address that the main stack pointer should be initialized to. */
+ pw_boot_stack_low_addr = .;
+ HIDDEN(_stack_size = ORIGIN(RAM) + LENGTH(RAM) - .);
+ /* Align the stack to a lower address to ensure it isn't out of range. */
+ HIDDEN(_stack_high = (. + _stack_size) & ~0x7);
+ ASSERT(_stack_high - . >= 1K,
+ "Error: Not enough RAM for desired minimum stack size.");
+ . = _stack_high;
+ pw_boot_stack_high_addr = .;
+ } >RAM
+
+ /* Represents unused space in the RAM segment. This MUST be the last section
+ * assigned to the RAM region.
+ */
+ .RAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(RAM) + LENGTH(RAM));
+ } >RAM
+
+ /* Discard unwind info. */
+ .ARM.extab 0x0 (INFO) :
+ {
+ KEEP(*(.ARM.extab*))
+ }
+}
+
+/* Symbols used by core_init.c: */
+/* Start of .static_init_ram in FLASH. */
+_pw_static_init_flash_start = LOADADDR(.static_init_ram);
+
+/* Region of .static_init_ram in RAM. */
+_pw_static_init_ram_start = ADDR(.static_init_ram);
+_pw_static_init_ram_end = _pw_static_init_ram_start + SIZEOF(.static_init_ram);
+
+/* Region of .zero_init_ram. */
+_pw_zero_init_ram_start = ADDR(.zero_init_ram);
+_pw_zero_init_ram_end = _pw_zero_init_ram_start + SIZEOF(.zero_init_ram);
+
+/* Symbols needed for the Rust cortex-m-rt crate. */
+__sbss = _pw_zero_init_ram_start;
+__ebss = _pw_zero_init_ram_end;
+__sdata = _pw_static_init_ram_start;
+__edata = _pw_static_init_ram_end;
+__sidata = LOADADDR(.static_init_ram);
+__pre_init = DefaultPreInit;
+DefaultHandler = DefaultHandler_;
+NonMaskableInt = DefaultHandler;
+MemoryManagement = DefaultHandler;
+BusFault = DefaultHandler;
+UsageFault = DefaultHandler;
+SVCall = DefaultHandler;
+DebugMonitor = DefaultHandler;
+PendSV = DefaultHandler;
+SysTick = DefaultHandler;
+HardFault = HardFault_;
+
+/* arm-none-eabi expects `end` symbol to point to start of heap for sbrk. */
+PROVIDE(end = _pw_zero_init_ram_end);
+
+/* These symbols are used by pw_bloat.bloaty_config to create the memoryregions
+ * data source for bloaty in this format (where the optional _N defaults to 0):
+ * pw_bloat_config_memory_region_NAME_{start,end}{_N,} */
+pw_bloat_config_memory_region_VECTOR_TABLE_start = ORIGIN(VECTOR_TABLE);
+pw_bloat_config_memory_region_VECTOR_TABLE_end =
+ ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE);
+pw_bloat_config_memory_region_FLASH_start = ORIGIN(FLASH);
+pw_bloat_config_memory_region_FLASH_end = ORIGIN(FLASH) + LENGTH(FLASH);
+pw_bloat_config_memory_region_RAM_start = ORIGIN(RAM);
+pw_bloat_config_memory_region_RAM_end = ORIGIN(RAM) + LENGTH(RAM);
diff --git a/pw_rust/examples/embedded_hello/src/main.rs b/pw_rust/examples/embedded_hello/src/main.rs
new file mode 100644
index 000000000..51028c47d
--- /dev/null
+++ b/pw_rust/examples/embedded_hello/src/main.rs
@@ -0,0 +1,32 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#![no_main]
+#![no_std]
+
+// Panic handler that halts the CPU on panic.
+use panic_halt as _;
+
+// Cortex M runtime entry macro.
+use cortex_m_rt::entry;
+
+// Semihosting support which is well supported for QEMU targets.
+use cortex_m_semihosting::{debug, hprintln};
+
+#[entry]
+fn main() -> ! {
+ hprintln!("Hello, Pigweed!");
+ debug::exit(debug::EXIT_SUCCESS);
+ loop {}
+}
diff --git a/pw_rust/examples/host_executable/BUILD.gn b/pw_rust/examples/host_executable/BUILD.gn
new file mode 100644
index 000000000..fca1f74e1
--- /dev/null
+++ b/pw_rust/examples/host_executable/BUILD.gn
@@ -0,0 +1,48 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+pw_rust_executable("hello") {
+ sources = [
+ "main.rs",
+ "other.rs",
+ ]
+
+ deps = [
+ ":a",
+ ":c",
+ ]
+}
+
+# The dep chain hello->a->b will exercise the functionality of both direct and
+# transitive deps for A
+pw_rust_library("a") {
+ crate_root = "a/lib.rs"
+ sources = [ "a/lib.rs" ]
+ deps = [ ":b" ]
+}
+
+pw_rust_library("b") {
+ crate_root = "b/lib.rs"
+ sources = [ "b/lib.rs" ]
+ deps = [ ":c" ]
+}
+
+pw_rust_library("c") {
+ crate_root = "c/lib.rs"
+ sources = [ "c/lib.rs" ]
+}
diff --git a/pw_rust/examples/host_executable/a/lib.rs b/pw_rust/examples/host_executable/a/lib.rs
new file mode 100644
index 000000000..e49ddb40a
--- /dev/null
+++ b/pw_rust/examples/host_executable/a/lib.rs
@@ -0,0 +1,22 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#![warn(clippy::all)]
+
+use b::RequiredB;
+
+#[derive(Copy, Clone, Default)]
+pub struct RequiredA {
+ pub required_b: RequiredB,
+}
diff --git a/pw_polyfill/public_overrides/type_traits b/pw_rust/examples/host_executable/b/lib.rs
index 3c9860b5f..6e32a1425 100644
--- a/pw_polyfill/public_overrides/type_traits
+++ b/pw_rust/examples/host_executable/b/lib.rs
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2023 The Pigweed Authors
//
// 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
@@ -11,8 +11,10 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-#pragma once
-#include_next <type_traits>
+#![warn(clippy::all)]
-#include "pw_polyfill/standard_library/type_traits.h"
+#[derive(Copy, Clone, Debug, Default)]
+pub struct RequiredB {
+ pub value: i32,
+}
diff --git a/pw_rust/examples/host_executable/c/lib.rs b/pw_rust/examples/host_executable/c/lib.rs
new file mode 100644
index 000000000..3c2cbff78
--- /dev/null
+++ b/pw_rust/examples/host_executable/c/lib.rs
@@ -0,0 +1,19 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#![warn(clippy::all)]
+
+pub fn value() -> i32 {
+ 1
+}
diff --git a/pw_rust/examples/host_executable/main.rs b/pw_rust/examples/host_executable/main.rs
new file mode 100644
index 000000000..79d72df70
--- /dev/null
+++ b/pw_rust/examples/host_executable/main.rs
@@ -0,0 +1,42 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#![warn(clippy::all)]
+
+mod other;
+
+fn main() {
+ println!("Hello, Pigweed!");
+
+ // ensure we can run code from other modules in the main crate
+ println!("{}", other::foo());
+
+ // ensure we can run code from dependent libraries
+ println!("{}", a::RequiredA::default().required_b.value);
+ println!("{}", c::value());
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn test_simple() {
+ let x = 3.14;
+ assert!(x > 0.0);
+ }
+
+ #[test]
+ fn test_other_module() {
+ assert!(a::RequiredA::default().required_b.value == 0);
+ }
+}
diff --git a/pw_rust/examples/host_executable/other.rs b/pw_rust/examples/host_executable/other.rs
new file mode 100644
index 000000000..fd571096f
--- /dev/null
+++ b/pw_rust/examples/host_executable/other.rs
@@ -0,0 +1,18 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#[allow(unused)]
+pub fn foo() -> i32 {
+ return 42;
+}
diff --git a/pw_rust/rust.gni b/pw_rust/rust.gni
new file mode 100644
index 000000000..8ae2ba64a
--- /dev/null
+++ b/pw_rust/rust.gni
@@ -0,0 +1,20 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+declare_args() {
+ # Enables compiling Pigweed's Rust libraries.
+ #
+ # WARNING: This is experimental and *not* guaranteed to work.
+ pw_rust_ENABLE_EXPERIMENTAL_BUILD = false
+}
diff --git a/pw_snapshot/BUILD.bazel b/pw_snapshot/BUILD.bazel
index 38816e9d9..ae98aec55 100644
--- a/pw_snapshot/BUILD.bazel
+++ b/pw_snapshot/BUILD.bazel
@@ -17,6 +17,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -32,7 +34,7 @@ pw_cc_library(
],
includes = ["public"],
deps = [
- ":metadata_proto",
+ ":metadata_proto_cc.pwpb",
"//pw_bytes",
"//pw_protobuf",
"//pw_status",
@@ -44,6 +46,18 @@ proto_library(
srcs = [
"pw_snapshot_protos/snapshot_metadata.proto",
],
+ import_prefix = "pw_snapshot_metadata_proto",
+ strip_import_prefix = "/pw_snapshot/pw_snapshot_protos",
+ deps = [
+ "//pw_tokenizer:tokenizer_proto",
+ ],
+)
+
+# TODO(b/241456982): Not expected to build yet.
+py_proto_library(
+ name = "metadata_proto_py_pb2",
+ tags = ["manual"],
+ deps = [":metadata_proto"],
)
proto_library(
@@ -51,17 +65,43 @@ proto_library(
srcs = [
"pw_snapshot_protos/snapshot.proto",
],
+ import_prefix = "pw_snapshot_protos",
+ strip_import_prefix = "/pw_snapshot/pw_snapshot_protos",
deps = [
":metadata_proto",
+ "//pw_chrono:chrono_proto",
+ "//pw_cpu_exception_cortex_m:cpu_state_protos",
+ "//pw_log:log_proto",
+ "//pw_thread:thread_proto",
],
)
-# TODO(pwbug/366): pw_protobuf codegen doesn't work for Bazel yet.
-filegroup(
+# TODO(b/241456982): Not expected to build yet.
+py_proto_library(
+ name = "snapshot_proto_py_pb2",
+ tags = ["manual"],
+ deps = [":snapshot_proto"],
+)
+
+pw_proto_library(
+ name = "metadata_proto_cc",
+ deps = [":snapshot_proto"],
+)
+
+pw_proto_library(
+ name = "snapshot_proto_cc",
+ deps = [":snapshot_proto"],
+)
+
+pw_cc_test(
name = "cpp_compile_test",
srcs = [
"cpp_compile_test.cc",
],
+ deps = [
+ ":snapshot_proto_cc.pwpb",
+ "//pw_unit_test",
+ ],
)
pw_cc_test(
@@ -70,7 +110,7 @@ pw_cc_test(
"uuid_test.cc",
],
deps = [
- ":metadata_proto",
+ ":metadata_proto_cc.pwpb",
":uuid",
"//pw_bytes",
"//pw_protobuf",
diff --git a/pw_snapshot/BUILD.gn b/pw_snapshot/BUILD.gn
index 4a6c8c140..14db8f1fe 100644
--- a/pw_snapshot/BUILD.gn
+++ b/pw_snapshot/BUILD.gn
@@ -30,6 +30,7 @@ pw_source_set("uuid") {
public_deps = [
dir_pw_bytes,
dir_pw_result,
+ dir_pw_span,
dir_pw_status,
]
deps = [
@@ -62,6 +63,7 @@ pw_proto_library("snapshot_proto") {
sources = [ "pw_snapshot_protos/snapshot.proto" ]
deps = [
":metadata_proto",
+ "$dir_pw_chrono:protos",
"$dir_pw_cpu_exception_cortex_m:cpu_state_protos",
"$dir_pw_log:protos",
"$dir_pw_thread:protos",
diff --git a/pw_snapshot/CMakeLists.txt b/pw_snapshot/CMakeLists.txt
index 1ba335551..7b414cbce 100644
--- a/pw_snapshot/CMakeLists.txt
+++ b/pw_snapshot/CMakeLists.txt
@@ -15,16 +15,15 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
-pw_add_module_library(pw_snapshot.uuid
+pw_add_library(pw_snapshot.uuid STATIC
HEADERS
public/pw_snapshot/uuid.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
- pw_polyfill.span
- pw_polyfill.cstddef
- pw_result
pw_bytes
+ pw_result
+ pw_span
SOURCES
uuid.cc
PRIVATE_DEPS
@@ -32,12 +31,6 @@ pw_add_module_library(pw_snapshot.uuid
pw_snapshot.metadata_proto.pwpb
)
-pw_add_module_library(pw_snapshot
- PUBLIC_DEPS
- pw_snapshot.metadata_proto
- pw_snapshot.snapshot_proto
-)
-
# This proto library only contains the snapshot_metadata.proto. Typically this
# should be a dependency of snapshot-like protos.
pw_proto_library(pw_snapshot.metadata_proto
@@ -58,16 +51,17 @@ pw_proto_library(pw_snapshot.snapshot_proto
SOURCES
pw_snapshot_protos/snapshot.proto
DEPS
- pw_snapshot.metadata_proto
+ pw_chrono.protos
pw_cpu_exception_cortex_m.cpu_state_protos
pw_log.protos
+ pw_snapshot.metadata_proto
pw_thread.protos
)
pw_add_test(pw_snapshot.cpp_compile_test
SOURCES
cpp_compile_test.cc
- DEPS
+ PRIVATE_DEPS
pw_protobuf
pw_snapshot.snapshot_proto.pwpb
GROUPS
@@ -78,10 +72,9 @@ pw_add_test(pw_snapshot.cpp_compile_test
pw_add_test(pw_snapshot.uuid_test
SOURCES
uuid_test.cc
- DEPS
+ PRIVATE_DEPS
pw_bytes
- pw_polyfill.cstddef
- pw_polyfill.span
+ pw_span
pw_protobuf
pw_result
pw_snapshot.metadata_proto.pwpb
diff --git a/pw_snapshot/cpp_compile_test.cc b/pw_snapshot/cpp_compile_test.cc
index f9bec8797..681c3324d 100644
--- a/pw_snapshot/cpp_compile_test.cc
+++ b/pw_snapshot/cpp_compile_test.cc
@@ -12,11 +12,10 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include <span>
-
#include "gtest/gtest.h"
#include "pw_protobuf/encoder.h"
#include "pw_snapshot_protos/snapshot.pwpb.h"
+#include "pw_span/span.h"
namespace pw::snapshot {
namespace {
@@ -27,20 +26,19 @@ TEST(Status, CompileTest) {
std::byte submessage_buffer[kMaxProtoSize];
stream::MemoryWriter writer(encode_buffer);
- Snapshot::StreamEncoder snapshot_encoder(writer, submessage_buffer);
+ pwpb::Snapshot::StreamEncoder snapshot_encoder(writer, submessage_buffer);
{
- Metadata::StreamEncoder metadata_encoder =
+ pwpb::Metadata::StreamEncoder metadata_encoder =
snapshot_encoder.GetMetadataEncoder();
- metadata_encoder
- .WriteReason(
- std::as_bytes(std::span("It just died, I didn't do anything")))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- metadata_encoder.WriteFatal(true)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- metadata_encoder.WriteProjectName(std::as_bytes(std::span("smart-shoe")))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- metadata_encoder.WriteDeviceName(std::as_bytes(std::span("smart-shoe-p1")))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(),
+ metadata_encoder.WriteReason(
+ as_bytes(span("It just died, I didn't do anything"))));
+ ASSERT_EQ(OkStatus(), metadata_encoder.WriteFatal(true));
+ ASSERT_EQ(OkStatus(),
+ metadata_encoder.WriteProjectName(as_bytes(span("smart-shoe"))));
+ ASSERT_EQ(
+ OkStatus(),
+ metadata_encoder.WriteDeviceName(as_bytes(span("smart-shoe-p1"))));
}
ASSERT_TRUE(snapshot_encoder.status().ok());
}
diff --git a/pw_snapshot/docs.rst b/pw_snapshot/docs.rst
index dd4c3e938..c6df8532f 100644
--- a/pw_snapshot/docs.rst
+++ b/pw_snapshot/docs.rst
@@ -30,7 +30,7 @@ on-demand system state capturing.
Life of a Snapshot
------------------
A "snapshot" is just a `proto message
-<https://cs.opensource.google/pigweed/pigweed/+/HEAD:pw_snapshot/pw_snapshot_protos/snapshot.proto>`_
+<https://cs.pigweed.dev/pigweed/+/HEAD:pw_snapshot/pw_snapshot_protos/snapshot.proto>`_
with many optional fields that describe a device's state at the time the
snapshot was captured. The serialized proto can then be stored and transfered
like a file so it can be analyzed at a later time.
diff --git a/pw_snapshot/module_usage.rst b/pw_snapshot/module_usage.rst
index f020fd722..422113274 100644
--- a/pw_snapshot/module_usage.rst
+++ b/pw_snapshot/module_usage.rst
@@ -44,9 +44,9 @@ write a few fields in a snapshot, you can do so with minimal memory overhead.
snapshot_encoder.GetMetadataEncoder();
metadata_encoder.WriteReason(EncodeReasonLog(crash_info));
metadata_encoder.WriteFatal(true);
- metadata_encoder.WriteProjectName(std::as_bytes(std::span("smart-shoe")));
+ metadata_encoder.WriteProjectName(pw::as_bytes(pw::span("smart-shoe")));
metadata_encoder.WriteDeviceName(
- std::as_bytes(std::span("smart-shoe-p1")));
+ pw::as_bytes(pw::span("smart-shoe-p1")));
}
return proto_encoder.status();
}
@@ -135,6 +135,9 @@ optionally be passed to the tool to detokenize applicable fields.
Stack info
Stack used: 0x2001b000 - 0x2001ae20 (480 bytes)
Stack limits: 0x2001b000 - 0x???????? (size unknown)
+ Raw Stack
+ 00caadde
+
Thread (RUNNING): Idle
Est CPU usage: unknown
diff --git a/pw_snapshot/public/pw_snapshot/uuid.h b/pw_snapshot/public/pw_snapshot/uuid.h
index 9946a36a7..dadeb8339 100644
--- a/pw_snapshot/public/pw_snapshot/uuid.h
+++ b/pw_snapshot/public/pw_snapshot/uuid.h
@@ -14,10 +14,10 @@
#pragma once
#include <cstddef>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_result/result.h"
+#include "pw_span/span.h"
namespace pw::snapshot {
@@ -26,8 +26,8 @@ namespace pw::snapshot {
// Note this is not strictly enforced anywhere, this is pure for convenience.
inline constexpr size_t kUuidSizeBytes = 16;
-using UuidSpan = std::span<std::byte, kUuidSizeBytes>;
-using ConstUuidSpan = std::span<const std::byte, kUuidSizeBytes>;
+using UuidSpan = span<std::byte, kUuidSizeBytes>;
+using ConstUuidSpan = span<const std::byte, kUuidSizeBytes>;
// Reads the snapshot UUID from an in memory snapshot, if present, and returns
// the subspan of `output` that contains the read snapshot.
diff --git a/pw_snapshot/pw_snapshot_protos/snapshot.proto b/pw_snapshot/pw_snapshot_protos/snapshot.proto
index 309dfaaa0..62b33ed56 100644
--- a/pw_snapshot/pw_snapshot_protos/snapshot.proto
+++ b/pw_snapshot/pw_snapshot_protos/snapshot.proto
@@ -18,6 +18,7 @@ package pw.snapshot;
option java_package = "pw.snapshot.proto";
option java_outer_classname = "Snapshot";
+import "pw_chrono_protos/chrono.proto";
import "pw_cpu_exception_cortex_m_protos/cpu_state.proto";
import "pw_log/proto/log.proto";
import "pw_thread_protos/thread.proto";
@@ -67,7 +68,7 @@ message Snapshot {
// entries added to it during a decode.
map<string, string> tags = 17;
- repeated pw.thread.Thread threads = 18;
+ repeated pw.thread.proto.Thread threads = 18;
// If a device has multiple cores, it may be useful to collect an associated
// snapshot for attached cores when a snapshot collection is triggered on one
@@ -81,9 +82,14 @@ message Snapshot {
// pw_trace_tokenized buffer format for stored data.
bytes trace_data = 21;
+ // Timestamps that mark when this snapshot occurred. This field is repeated
+ // to accommodate wall-clock time, time since boot, and/or the raw system
+ // clock value.
+ repeated pw.chrono.TimePoint timestamps = 22;
+
// RESERVED FOR PIGWEED. Downstream projects may NOT write to these fields.
// Encodes to two bytes of tag overhead.
- reserved 22 to 1031;
+ reserved 23 to 1031;
// RESERVED FOR USERS. Encodes to two or more bytes of tag overhead.
reserved 1032 to max;
diff --git a/pw_snapshot/pw_snapshot_protos/snapshot_metadata.proto b/pw_snapshot/pw_snapshot_protos/snapshot_metadata.proto
index dabf49b0a..12b3ffbd9 100644
--- a/pw_snapshot/pw_snapshot_protos/snapshot_metadata.proto
+++ b/pw_snapshot/pw_snapshot_protos/snapshot_metadata.proto
@@ -48,8 +48,11 @@ message Metadata {
bytes project_name = 3 [(tokenizer.format) = TOKENIZATION_OPTIONAL];
// Version characters must be alphanumeric, punctuation, and space. This
- // string is case-sensitive. This should either be human readable text, or
- // tokenized data.
+ // string is case-sensitive. This should always be human readable text, and
+ // does not support tokenization by design. If this field was tokenized, it's
+ // possible that the token could be lost (e.g. generated by a local developer
+ // build and not uploaded anywhere) and a firmware version running on a device
+ // in the field would be left entirely unidentifiable.
//
// Examples:
// "codename-local-[build_id]"
diff --git a/pw_snapshot/py/BUILD.bazel b/pw_snapshot/py/BUILD.bazel
new file mode 100644
index 000000000..eeebb252b
--- /dev/null
+++ b/pw_snapshot/py/BUILD.bazel
@@ -0,0 +1,65 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+package(default_visibility = ["//visibility:public"])
+
+# TODO(b/241456982): Not expected to build yet due to the dependency on
+# snapshot_proto_py_pb2.
+py_library(
+ name = "pw_snapshot",
+ srcs = [
+ "pw_snapshot/__init__.py",
+ "pw_snapshot/processor.py",
+ ],
+ tags = ["manual"],
+ deps = [
+ ":pw_snapshot_metadata",
+ "//pw_build_info/py:pw_build_info",
+ "//pw_chrono/py:pw_chrono",
+ "//pw_cpu_exception_cortex_m/py:exception_analyzer",
+ "//pw_snapshot:snapshot_proto_py_pb2",
+ "//pw_symbolizer/py:pw_symbolizer",
+ "//pw_thread/py:pw_thread",
+ "//pw_tokenizer/py:pw_tokenizer",
+ ],
+)
+
+# TODO(b/241456982): Not expected to build yet due to the dependency on
+# metadata_proto_py_pb2.
+py_library(
+ name = "pw_snapshot_metadata",
+ srcs = [
+ "pw_snapshot_metadata/__init__.py",
+ "pw_snapshot_metadata/metadata.py",
+ ],
+ tags = ["manual"],
+ deps = [
+ "//pw_log_tokenized/py:pw_log_tokenized",
+ "//pw_snapshot:metadata_proto_py_pb2",
+ "//pw_tokenizer/py:pw_tokenizer",
+ ],
+)
+
+# TODO(b/241456982): Not expected to build yet due to the dependency on
+# snapshot_proto_py_pb2.
+py_test(
+ name = "metadata_test",
+ srcs = ["metadata_test.py"],
+ tags = ["manual"],
+ deps = [
+ ":pw_snapshot_metadata",
+ "//pw_snapshot:snapshot_proto_py_pb2",
+ "//pw_tokenizer/py:pw_tokenizer",
+ ],
+)
diff --git a/pw_snapshot/py/BUILD.gn b/pw_snapshot/py/BUILD.gn
index 51cb6839a..5349b52c3 100644
--- a/pw_snapshot/py/BUILD.gn
+++ b/pw_snapshot/py/BUILD.gn
@@ -15,7 +15,6 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python.gni")
-import("$dir_pw_docgen/docs.gni")
pw_python_package("pw_snapshot_metadata") {
generate_setup = {
@@ -35,6 +34,7 @@ pw_python_package("pw_snapshot_metadata") {
"..:metadata_proto.python",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
pw_python_package("pw_snapshot") {
@@ -53,6 +53,8 @@ pw_python_package("pw_snapshot") {
python_deps = [
":pw_snapshot_metadata",
"$dir_pw_build_info/py",
+ "$dir_pw_chrono:protos.python",
+ "$dir_pw_chrono/py",
"$dir_pw_cpu_exception_cortex_m/py",
"$dir_pw_symbolizer/py",
"$dir_pw_thread:protos.python",
@@ -61,6 +63,7 @@ pw_python_package("pw_snapshot") {
"..:snapshot_proto.python",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
pw_python_group("py") {
@@ -69,8 +72,3 @@ pw_python_group("py") {
":pw_snapshot_metadata",
]
}
-
-pw_doc_group("docs") {
- sources = [ "docs.rst" ]
- other_deps = [ ":py" ]
-}
diff --git a/pw_snapshot/py/generate_example_snapshot.py b/pw_snapshot/py/generate_example_snapshot.py
index 11a0b3377..61a8d0043 100644
--- a/pw_snapshot/py/generate_example_snapshot.py
+++ b/pw_snapshot/py/generate_example_snapshot.py
@@ -26,9 +26,9 @@ def _add_threads(snapshot: snapshot_pb2.Snapshot) -> snapshot_pb2.Snapshot:
# Build example idle thread.
thread = thread_pb2.Thread()
thread.name = 'Idle'.encode()
- thread.stack_start_pointer = 0x2001ac00
- thread.stack_end_pointer = 0x2001aa00
- thread.stack_pointer = 0x2001ab0c
+ thread.stack_start_pointer = 0x2001AC00
+ thread.stack_end_pointer = 0x2001AA00
+ thread.stack_pointer = 0x2001AB0C
thread.state = thread_pb2.ThreadState.Enum.RUNNING
snapshot.threads.append(thread)
@@ -36,9 +36,10 @@ def _add_threads(snapshot: snapshot_pb2.Snapshot) -> snapshot_pb2.Snapshot:
thread = thread_pb2.Thread()
thread.name = 'Main Stack (Handler Mode)'.encode()
thread.active = True
- thread.stack_start_pointer = 0x2001b000
- thread.stack_pointer = 0x2001ae20
+ thread.stack_start_pointer = 0x2001B000
+ thread.stack_pointer = 0x2001AE20
thread.state = thread_pb2.ThreadState.Enum.INTERRUPT_HANDLER
+ thread.raw_stack = b'\x00\xCA\xAD\xDE'
snapshot.threads.append(thread)
return snapshot
@@ -48,19 +49,20 @@ def _main(out_file: TextIO):
snapshot = snapshot_pb2.Snapshot()
snapshot.metadata.reason = (
- '■msg♦Assert failed: 1+1 == 42'
- '■file♦../examples/example_rpc.cc').encode('utf-8')
+ '■msg♦Assert failed: 1+1 == 42' '■file♦../examples/example_rpc.cc'
+ ).encode('utf-8')
snapshot.metadata.fatal = True
snapshot.metadata.project_name = 'gShoe'.encode('utf-8')
snapshot.metadata.software_version = 'QUANTUM_CORE-0.1.325-e4a84b1a'
snapshot.metadata.software_build_uuid = (
b'\xAD\x2D\x39\x25\x8C\x1B\xC4\x87'
b'\xF0\x7C\xA7\xE0\x49\x91\xA8\x36'
- b'\xFD\xF7\xD0\xA0')
- snapshot.metadata.device_name = 'GSHOE-QUANTUM_CORE-REV_0.1'.encode(
- 'utf-8')
- snapshot.metadata.snapshot_uuid = (b'\x84\x81\xBB\x12\xA1\x62\x16\x4F'
- b'\x5C\x74\x85\x5F\x6D\x94\xEA\x1A')
+ b'\xFD\xF7\xD0\xA0'
+ )
+ snapshot.metadata.device_name = 'GSHOE-QUANTUM_CORE-REV_0.1'.encode('utf-8')
+ snapshot.metadata.snapshot_uuid = (
+ b'\x84\x81\xBB\x12\xA1\x62\x16\x4F' b'\x5C\x74\x85\x5F\x6D\x94\xEA\x1A'
+ )
# Add some thread-related info.
snapshot = _add_threads(snapshot)
@@ -71,10 +73,12 @@ def _main(out_file: TextIO):
def _parse_args():
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--out-file',
- '-o',
- type=argparse.FileType('w'),
- help='File to output serialized snapshot to.')
+ parser.add_argument(
+ '--out-file',
+ '-o',
+ type=argparse.FileType('w'),
+ help='File to output serialized snapshot to.',
+ )
return parser.parse_args()
diff --git a/pw_snapshot/py/metadata_test.py b/pw_snapshot/py/metadata_test.py
index 31528333f..53ed719ce 100644
--- a/pw_snapshot/py/metadata_test.py
+++ b/pw_snapshot/py/metadata_test.py
@@ -24,19 +24,25 @@ from pw_tokenizer import tokens
class MetadataProcessorTest(unittest.TestCase):
"""Tests that the metadata processor produces expected results."""
+
def setUp(self):
super().setUp()
self.detok = pw_tokenizer.Detokenizer(
- tokens.Database([
- tokens.TokenizedStringEntry(0x3A9BC4C3,
- 'Assert failed: 1+1 == 42'),
- tokens.TokenizedStringEntry(0x01170923, 'gShoe'),
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 0x3A9BC4C3, 'Assert failed: 1+1 == 42'
+ ),
+ tokens.TokenizedStringEntry(0x01170923, 'gShoe'),
+ ]
+ )
+ )
snapshot = snapshot_pb2.Snapshot()
snapshot.metadata.reason = b'\xc3\xc4\x9b\x3a'
snapshot.metadata.project_name = b'$' + base64.b64encode(
- b'\x23\x09\x17\x01')
+ b'\x23\x09\x17\x01'
+ )
snapshot.metadata.device_name = b'hyper-fast-gshoe'
snapshot.metadata.software_version = 'gShoe-debug-1.2.1-6f23412b+'
snapshot.metadata.snapshot_uuid = b'\x00\x00\x00\x01'
@@ -49,11 +55,12 @@ class MetadataProcessorTest(unittest.TestCase):
def test_reason_log_format(self):
self.snapshot.metadata.reason = (
- '■msg♦Assert failed :('
- '■file♦rpc_services/crash.cc').encode('utf-8')
+ '■msg♦Assert failed :(' '■file♦rpc_services/crash.cc'
+ ).encode('utf-8')
meta = MetadataProcessor(self.snapshot.metadata, self.detok)
- self.assertEqual(meta.reason(),
- 'rpc_services/crash.cc: Assert failed :(')
+ self.assertEqual(
+ meta.reason(), 'rpc_services/crash.cc: Assert failed :('
+ )
def test_project_name_tokenized(self):
meta = MetadataProcessor(self.snapshot.metadata, self.detok)
@@ -69,8 +76,9 @@ class MetadataProcessorTest(unittest.TestCase):
def test_fw_version(self):
meta = MetadataProcessor(self.snapshot.metadata, self.detok)
- self.assertEqual(meta.device_fw_version(),
- 'gShoe-debug-1.2.1-6f23412b+')
+ self.assertEqual(
+ meta.device_fw_version(), 'gShoe-debug-1.2.1-6f23412b+'
+ )
def test_snapshot_uuid(self):
meta = MetadataProcessor(self.snapshot.metadata, self.detok)
@@ -82,35 +90,41 @@ class MetadataProcessorTest(unittest.TestCase):
def test_as_str(self):
meta = MetadataProcessor(self.snapshot.metadata, self.detok)
- expected = '\n'.join((
- 'Snapshot capture reason:',
- ' Assert failed: 1+1 == 42',
- '',
- 'Project name: gShoe',
- 'Device: hyper-fast-gshoe',
- 'Device FW version: gShoe-debug-1.2.1-6f23412b+',
- 'Snapshot UUID: 00000001',
- ))
+ expected = '\n'.join(
+ (
+ 'Snapshot capture reason:',
+ ' Assert failed: 1+1 == 42',
+ '',
+ 'Reason token: 0x3a9bc4c3',
+ 'Project name: gShoe',
+ 'Device: hyper-fast-gshoe',
+ 'Device FW version: gShoe-debug-1.2.1-6f23412b+',
+ 'Snapshot UUID: 00000001',
+ )
+ )
self.assertEqual(expected, str(meta))
def test_as_str_fatal(self):
self.snapshot.metadata.fatal = True
meta = MetadataProcessor(self.snapshot.metadata, self.detok)
- expected = '\n'.join((
- ' ▪▄▄▄ ▄▄▄· ▄▄▄▄▄ ▄▄▄· ▄ ·',
- ' █▄▄▄▐█ ▀█ • █▌ ▐█ ▀█ █',
- ' █ ▪ ▄█▀▀█ █. ▄█▀▀█ █',
- ' ▐▌ .▐█ ▪▐▌ ▪▐▌·▐█ ▪▐▌▐▌',
- ' ▀ ▀ ▀ · ▀ ▀ ▀ .▀▀',
- '',
- 'Device crash cause:',
- ' Assert failed: 1+1 == 42',
- '',
- 'Project name: gShoe',
- 'Device: hyper-fast-gshoe',
- 'Device FW version: gShoe-debug-1.2.1-6f23412b+',
- 'Snapshot UUID: 00000001',
- ))
+ expected = '\n'.join(
+ (
+ ' ▪▄▄▄ ▄▄▄· ▄▄▄▄▄ ▄▄▄· ▄ ·',
+ ' █▄▄▄▐█ ▀█ • █▌ ▐█ ▀█ █',
+ ' █ ▪ ▄█▀▀█ █. ▄█▀▀█ █',
+ ' ▐▌ .▐█ ▪▐▌ ▪▐▌·▐█ ▪▐▌▐▌',
+ ' ▀ ▀ ▀ · ▀ ▀ ▀ .▀▀',
+ '',
+ 'Device crash cause:',
+ ' Assert failed: 1+1 == 42',
+ '',
+ 'Reason token: 0x3a9bc4c3',
+ 'Project name: gShoe',
+ 'Device: hyper-fast-gshoe',
+ 'Device FW version: gShoe-debug-1.2.1-6f23412b+',
+ 'Snapshot UUID: 00000001',
+ )
+ )
self.assertEqual(expected, str(meta))
def test_no_reason(self):
@@ -118,37 +132,43 @@ class MetadataProcessorTest(unittest.TestCase):
snapshot.metadata.fatal = True
meta = MetadataProcessor(snapshot.metadata, self.detok)
meta.set_pretty_format_width(40)
- expected = '\n'.join((
- ' ▪▄▄▄ ▄▄▄· ▄▄▄▄▄ ▄▄▄· ▄ ·',
- ' █▄▄▄▐█ ▀█ • █▌ ▐█ ▀█ █',
- ' █ ▪ ▄█▀▀█ █. ▄█▀▀█ █',
- ' ▐▌ .▐█ ▪▐▌ ▪▐▌·▐█ ▪▐▌▐▌',
- ' ▀ ▀ ▀ · ▀ ▀ ▀ .▀▀',
- '',
- 'Device crash cause:',
- ' UNKNOWN (field missing)',
- '',
- ))
+ expected = '\n'.join(
+ (
+ ' ▪▄▄▄ ▄▄▄· ▄▄▄▄▄ ▄▄▄· ▄ ·',
+ ' █▄▄▄▐█ ▀█ • █▌ ▐█ ▀█ █',
+ ' █ ▪ ▄█▀▀█ █. ▄█▀▀█ █',
+ ' ▐▌ .▐█ ▪▐▌ ▪▐▌·▐█ ▪▐▌▐▌',
+ ' ▀ ▀ ▀ · ▀ ▀ ▀ .▀▀',
+ '',
+ 'Device crash cause:',
+ ' UNKNOWN (field missing)',
+ '',
+ )
+ )
self.assertEqual(expected, str(meta))
def test_serialized_snapshot(self):
self.snapshot.tags['type'] = 'obviously a crash'
- expected = '\n'.join((
- 'Snapshot capture reason:',
- ' Assert failed: 1+1 == 42',
- '',
- 'Project name: gShoe',
- 'Device: hyper-fast-gshoe',
- 'Device FW version: gShoe-debug-1.2.1-6f23412b+',
- 'Snapshot UUID: 00000001',
- '',
- 'Tags:',
- ' type: obviously a crash',
- '',
- ))
+ expected = '\n'.join(
+ (
+ 'Snapshot capture reason:',
+ ' Assert failed: 1+1 == 42',
+ '',
+ 'Reason token: 0x3a9bc4c3',
+ 'Project name: gShoe',
+ 'Device: hyper-fast-gshoe',
+ 'Device FW version: gShoe-debug-1.2.1-6f23412b+',
+ 'Snapshot UUID: 00000001',
+ '',
+ 'Tags:',
+ ' type: obviously a crash',
+ '',
+ )
+ )
self.assertEqual(
expected,
- process_snapshot(self.snapshot.SerializeToString(), self.detok))
+ process_snapshot(self.snapshot.SerializeToString(), self.detok),
+ )
if __name__ == '__main__':
diff --git a/pw_snapshot/py/pw_snapshot/processor.py b/pw_snapshot/py/pw_snapshot/processor.py
index d96691028..62d193338 100644
--- a/pw_snapshot/py/pw_snapshot/processor.py
+++ b/pw_snapshot/py/pw_snapshot/processor.py
@@ -26,6 +26,7 @@ from pw_snapshot_metadata import metadata
from pw_snapshot_protos import snapshot_pb2
from pw_symbolizer import LlvmSymbolizer, Symbolizer
from pw_thread import thread_analyzer
+from pw_chrono import timestamp_analyzer
_LOG = logging.getLogger('snapshot_processor')
@@ -54,16 +55,18 @@ SymbolizerMatcher = Callable[[snapshot_pb2.Snapshot], Symbolizer]
def process_snapshot(
- serialized_snapshot: bytes,
- detokenizer: Optional[pw_tokenizer.Detokenizer] = None,
- elf_matcher: Optional[ElfMatcher] = None,
- symbolizer_matcher: Optional[SymbolizerMatcher] = None) -> str:
+ serialized_snapshot: bytes,
+ detokenizer: Optional[pw_tokenizer.Detokenizer] = None,
+ elf_matcher: Optional[ElfMatcher] = None,
+ symbolizer_matcher: Optional[SymbolizerMatcher] = None,
+) -> str:
"""Processes a single snapshot."""
output = [_BRANDING]
- captured_metadata = metadata.process_snapshot(serialized_snapshot,
- detokenizer)
+ captured_metadata = metadata.process_snapshot(
+ serialized_snapshot, detokenizer
+ )
if captured_metadata:
output.append(captured_metadata)
@@ -79,39 +82,50 @@ def process_snapshot(
symbolizer = LlvmSymbolizer()
cortex_m_cpu_state = pw_cpu_exception_cortex_m.process_snapshot(
- serialized_snapshot, symbolizer)
+ serialized_snapshot, symbolizer
+ )
if cortex_m_cpu_state:
output.append(cortex_m_cpu_state)
- thread_info = thread_analyzer.process_snapshot(serialized_snapshot,
- detokenizer, symbolizer)
+ thread_info = thread_analyzer.process_snapshot(
+ serialized_snapshot, detokenizer, symbolizer
+ )
+
if thread_info:
output.append(thread_info)
+ timestamp_info = timestamp_analyzer.process_snapshot(serialized_snapshot)
+
+ if timestamp_info:
+ output.append(timestamp_info)
+
# Check and emit the number of related snapshots embedded in this snapshot.
if snapshot.related_snapshots:
snapshot_count = len(snapshot.related_snapshots)
plural = 's' if snapshot_count > 1 else ''
- output.extend((
- f'This snapshot contains {snapshot_count} related snapshot{plural}',
- '',
- ))
+ output.append(
+ f'This snapshot contains {snapshot_count} related snapshot{plural}'
+ )
+ output.append('')
return '\n'.join(output)
def process_snapshots(
- serialized_snapshot: bytes,
- detokenizer: Optional[pw_tokenizer.Detokenizer] = None,
- elf_matcher: Optional[ElfMatcher] = None,
- user_processing_callback: Optional[Callable[[bytes], str]] = None,
- symbolizer_matcher: Optional[SymbolizerMatcher] = None) -> str:
+ serialized_snapshot: bytes,
+ detokenizer: Optional[pw_tokenizer.Detokenizer] = None,
+ elf_matcher: Optional[ElfMatcher] = None,
+ user_processing_callback: Optional[Callable[[bytes], str]] = None,
+ symbolizer_matcher: Optional[SymbolizerMatcher] = None,
+) -> str:
"""Processes a snapshot that may have multiple embedded snapshots."""
output = []
# Process the top-level snapshot.
output.append(
- process_snapshot(serialized_snapshot, detokenizer, elf_matcher,
- symbolizer_matcher))
+ process_snapshot(
+ serialized_snapshot, detokenizer, elf_matcher, symbolizer_matcher
+ )
+ )
# If the user provided a custom processing callback, call it on each
# snapshot.
@@ -125,61 +139,81 @@ def process_snapshots(
output.append('\n[' + '=' * 78 + ']\n')
output.append(
str(
- process_snapshots(nested_snapshot.SerializeToString(),
- detokenizer, elf_matcher,
- user_processing_callback,
- symbolizer_matcher)))
+ process_snapshots(
+ nested_snapshot.SerializeToString(),
+ detokenizer,
+ elf_matcher,
+ user_processing_callback,
+ symbolizer_matcher,
+ )
+ )
+ )
return '\n'.join(output)
def _snapshot_symbolizer_matcher(
- artifacts_dir: Path,
- snapshot: snapshot_pb2.Snapshot) -> LlvmSymbolizer:
+ artifacts_dir: Path, snapshot: snapshot_pb2.Snapshot
+) -> LlvmSymbolizer:
matching_elf: Optional[Path] = pw_build_info.build_id.find_matching_elf(
- snapshot.metadata.software_build_uuid, artifacts_dir)
+ snapshot.metadata.software_build_uuid, artifacts_dir
+ )
if not matching_elf:
- _LOG.error('Error: No matching ELF found for GNU build ID %s.',
- snapshot.metadata.software_build_uuid.hex())
+ _LOG.error(
+ 'Error: No matching ELF found for GNU build ID %s.',
+ snapshot.metadata.software_build_uuid.hex(),
+ )
return LlvmSymbolizer(matching_elf)
-def _load_and_dump_snapshots(in_file: BinaryIO, out_file: TextIO,
- token_db: Optional[TextIO],
- artifacts_dir: Optional[Path]):
+def _load_and_dump_snapshots(
+ in_file: BinaryIO,
+ out_file: TextIO,
+ token_db: Optional[TextIO],
+ artifacts_dir: Optional[Path],
+):
detokenizer = None
if token_db:
detokenizer = pw_tokenizer.Detokenizer(token_db)
symbolizer_matcher: Optional[SymbolizerMatcher] = None
if artifacts_dir:
- symbolizer_matcher = functools.partial(_snapshot_symbolizer_matcher,
- artifacts_dir)
+ symbolizer_matcher = functools.partial(
+ _snapshot_symbolizer_matcher, artifacts_dir
+ )
out_file.write(
- process_snapshots(serialized_snapshot=in_file.read(),
- detokenizer=detokenizer,
- symbolizer_matcher=symbolizer_matcher))
+ process_snapshots(
+ serialized_snapshot=in_file.read(),
+ detokenizer=detokenizer,
+ symbolizer_matcher=symbolizer_matcher,
+ )
+ )
def _parse_args():
parser = argparse.ArgumentParser(description='Decode Pigweed snapshots')
- parser.add_argument('in_file',
- type=argparse.FileType('rb'),
- help='Binary snapshot file')
+ parser.add_argument(
+ 'in_file', type=argparse.FileType('rb'), help='Binary snapshot file'
+ )
parser.add_argument(
'--out-file',
'-o',
default='-',
type=argparse.FileType('wb'),
- help='File to output decoded snapshots to. Defaults to stdout.')
+ help='File to output decoded snapshots to. Defaults to stdout.',
+ )
parser.add_argument(
'--token-db',
type=argparse.FileType('r'),
- help='Token database or ELF file to use for detokenization.')
+ help='Token database or ELF file to use for detokenization.',
+ )
parser.add_argument(
'--artifacts-dir',
type=Path,
- help=('Directory to recursively search for matching ELF files to use '
- 'for symbolization.'))
+ help=(
+ 'Directory to recursively search for matching ELF files to use '
+ 'for symbolization.'
+ ),
+ )
return parser.parse_args()
diff --git a/pw_snapshot/py/pw_snapshot_metadata/metadata.py b/pw_snapshot/py/pw_snapshot_metadata/metadata.py
index ea3938096..d11a2f403 100644
--- a/pw_snapshot/py/pw_snapshot_metadata/metadata.py
+++ b/pw_snapshot/py/pw_snapshot_metadata/metadata.py
@@ -21,6 +21,14 @@ from pw_snapshot_metadata_proto import snapshot_metadata_pb2
_PRETTY_FORMAT_DEFAULT_WIDTH = 80
+_FATAL = (
+ '▪▄▄▄ ▄▄▄· ▄▄▄▄▄ ▄▄▄· ▄ ·',
+ '█▄▄▄▐█ ▀█ • █▌ ▐█ ▀█ █ ',
+ '█ ▪ ▄█▀▀█ █. ▄█▀▀█ █ ',
+ '▐▌ .▐█ ▪▐▌ ▪▐▌·▐█ ▪▐▌▐▌ ',
+ '▀ ▀ ▀ · ▀ ▀ ▀ .▀▀',
+)
+
def _process_tags(tags: Mapping[str, str]) -> Optional[str]:
"""Outputs snapshot tags as a multi-line string."""
@@ -34,8 +42,9 @@ def _process_tags(tags: Mapping[str, str]) -> Optional[str]:
return '\n'.join(output)
-def process_snapshot(serialized_snapshot: bytes,
- tokenizer_db: Optional[pw_tokenizer.Detokenizer]) -> str:
+def process_snapshot(
+ serialized_snapshot: bytes, tokenizer_db: Optional[pw_tokenizer.Detokenizer]
+) -> str:
"""Processes snapshot metadata and tags, producing a multi-line string."""
snapshot = snapshot_metadata_pb2.SnapshotBasicInfo()
snapshot.ParseFromString(serialized_snapshot)
@@ -43,10 +52,12 @@ def process_snapshot(serialized_snapshot: bytes,
output: List[str] = []
if snapshot.HasField('metadata'):
- output.extend((
- str(MetadataProcessor(snapshot.metadata, tokenizer_db)),
- '',
- ))
+ output.extend(
+ (
+ str(MetadataProcessor(snapshot.metadata, tokenizer_db)),
+ '',
+ )
+ )
if snapshot.tags:
tags = _process_tags(snapshot.tags)
@@ -60,11 +71,21 @@ def process_snapshot(serialized_snapshot: bytes,
class MetadataProcessor:
"""This class simplifies dumping contents of a snapshot Metadata message."""
- def __init__(self, metadata: snapshot_metadata_pb2.Metadata,
- tokenizer_db: Optional[pw_tokenizer.Detokenizer]):
+
+ def __init__(
+ self,
+ metadata: snapshot_metadata_pb2.Metadata,
+ tokenizer_db: Optional[pw_tokenizer.Detokenizer],
+ ):
self._metadata = metadata
- self._tokenizer_db = (tokenizer_db if tokenizer_db is not None else
- pw_tokenizer.Detokenizer(None))
+ self._tokenizer_db = (
+ tokenizer_db
+ if tokenizer_db is not None
+ else pw_tokenizer.Detokenizer(None)
+ )
+ self._reason_token = self._tokenizer_db.detokenize(
+ metadata.reason
+ ).token
self._format_width = _PRETTY_FORMAT_DEFAULT_WIDTH
proto_detokenizer.detokenize_fields(self._tokenizer_db, self._metadata)
@@ -76,10 +97,15 @@ class MetadataProcessor:
return 'UNKNOWN (field missing)'
log = pw_log_tokenized.FormatStringWithMetadata(
- self._metadata.reason.decode())
+ self._metadata.reason.decode()
+ )
return f'{log.file}: {log.message}' if log.file else log.message
+ def reason_token(self) -> Optional[int]:
+ """If the snapshot `reason` is tokenized, the value of the token."""
+ return self._reason_token
+
def project_name(self) -> str:
return self._metadata.project_name.decode()
@@ -103,22 +129,24 @@ class MetadataProcessor:
"""outputs a pw.snapshot.Metadata proto as a multi-line string."""
output: List[str] = []
if self._metadata.fatal:
- output.extend((
- '▪▄▄▄ ▄▄▄· ▄▄▄▄▄ ▄▄▄· ▄ ·'.center(self._format_width).rstrip(),
- '█▄▄▄▐█ ▀█ • █▌ ▐█ ▀█ █ '.center(self._format_width).rstrip(),
- '█ ▪ ▄█▀▀█ █. ▄█▀▀█ █ '.center(self._format_width).rstrip(),
- '▐▌ .▐█ ▪▐▌ ▪▐▌·▐█ ▪▐▌▐▌ '.center(self._format_width).rstrip(),
- '▀ ▀ ▀ · ▀ ▀ ▀ .▀▀'.center(self._format_width).rstrip(),
- '',
- 'Device crash cause:',
- ))
+ output.extend(
+ (
+ *[x.center(self._format_width).rstrip() for x in _FATAL],
+ '',
+ 'Device crash cause:',
+ )
+ )
else:
output.append('Snapshot capture reason:')
- output.extend((
- ' ' + self.reason(),
- '',
- ))
+ output.extend(
+ (
+ ' ' + self.reason(),
+ '',
+ )
+ )
+ if self.reason_token():
+ output.append(f'Reason token: 0x{self.reason_token():x}')
if self._metadata.project_name:
output.append(f'Project name: {self.project_name()}')
diff --git a/pw_snapshot/setup.rst b/pw_snapshot/setup.rst
index a2a769080..85c320854 100644
--- a/pw_snapshot/setup.rst
+++ b/pw_snapshot/setup.rst
@@ -322,5 +322,5 @@ by creating a light wrapper around
def process_my_snapshots(serialized_snapshot: bytes) -> str:
"""Runs the snapshot processor with a custom callback."""
- return pw_snaphsot.processor.process_snapshots(
+ return pw_snapshot.processor.process_snapshots(
serialized_snapshot, user_processing_callback=_process_hw_failures)
diff --git a/pw_snapshot/uuid.cc b/pw_snapshot/uuid.cc
index 420d5e3d2..f1c06366b 100644
--- a/pw_snapshot/uuid.cc
+++ b/pw_snapshot/uuid.cc
@@ -15,12 +15,12 @@
#include "pw_snapshot/uuid.h"
#include <cstddef>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_protobuf/decoder.h"
#include "pw_result/result.h"
#include "pw_snapshot_metadata_proto/snapshot_metadata.pwpb.h"
+#include "pw_span/span.h"
#include "pw_status/try.h"
namespace pw::snapshot {
@@ -34,7 +34,7 @@ Result<ConstByteSpan> ReadUuidFromSnapshot(ConstByteSpan snapshot,
while (decoder.Next().ok()) {
if (decoder.FieldNumber() ==
static_cast<uint32_t>(
- pw::snapshot::SnapshotBasicInfo::Fields::METADATA)) {
+ pw::snapshot::pwpb::SnapshotBasicInfo::Fields::kMetadata)) {
PW_TRY(decoder.ReadBytes(&metadata));
break;
}
@@ -48,7 +48,8 @@ Result<ConstByteSpan> ReadUuidFromSnapshot(ConstByteSpan snapshot,
ConstByteSpan snapshot_uuid;
while (decoder.Next().ok()) {
if (decoder.FieldNumber() ==
- static_cast<uint32_t>(pw::snapshot::Metadata::Fields::SNAPSHOT_UUID)) {
+ static_cast<uint32_t>(
+ pw::snapshot::pwpb::Metadata::Fields::kSnapshotUuid)) {
PW_TRY(decoder.ReadBytes(&snapshot_uuid));
break;
}
diff --git a/pw_snapshot/uuid_test.cc b/pw_snapshot/uuid_test.cc
index 795071ea2..7c2f45367 100644
--- a/pw_snapshot/uuid_test.cc
+++ b/pw_snapshot/uuid_test.cc
@@ -15,22 +15,22 @@
#include "pw_snapshot/uuid.h"
#include <array>
-#include <span>
#include "gtest/gtest.h"
#include "pw_bytes/span.h"
#include "pw_protobuf/encoder.h"
#include "pw_result/result.h"
#include "pw_snapshot_metadata_proto/snapshot_metadata.pwpb.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
namespace pw::snapshot {
namespace {
ConstByteSpan EncodeSnapshotWithUuid(ConstByteSpan uuid, ByteSpan dest) {
- SnapshotBasicInfo::MemoryEncoder snapshot_encoder(dest);
+ pwpb::SnapshotBasicInfo::MemoryEncoder snapshot_encoder(dest);
{
- Metadata::StreamEncoder metadata_encoder =
+ pwpb::Metadata::StreamEncoder metadata_encoder =
snapshot_encoder.GetMetadataEncoder();
EXPECT_EQ(OkStatus(), metadata_encoder.WriteSnapshotUuid(uuid));
}
@@ -43,8 +43,8 @@ TEST(ReadUuid, ReadUuid) {
const std::array<uint8_t, 8> kExpectedUuid = {
0x1F, 0x8F, 0xBF, 0xC4, 0x86, 0x0E, 0xED, 0xD4};
std::array<std::byte, 16> snapshot_buffer;
- ConstByteSpan snapshot = EncodeSnapshotWithUuid(
- std::as_bytes(std::span(kExpectedUuid)), snapshot_buffer);
+ ConstByteSpan snapshot =
+ EncodeSnapshotWithUuid(as_bytes(span(kExpectedUuid)), snapshot_buffer);
std::array<std::byte, kUuidSizeBytes> uuid_dest;
Result<ConstByteSpan> result = ReadUuidFromSnapshot(snapshot, uuid_dest);
@@ -57,9 +57,9 @@ TEST(ReadUuid, NoUuid) {
std::array<std::byte, 16> snapshot_buffer;
// Write some snapshot metadata, but no UUID.
- SnapshotBasicInfo::MemoryEncoder snapshot_encoder(snapshot_buffer);
+ pwpb::SnapshotBasicInfo::MemoryEncoder snapshot_encoder(snapshot_buffer);
{
- Metadata::StreamEncoder metadata_encoder =
+ pwpb::Metadata::StreamEncoder metadata_encoder =
snapshot_encoder.GetMetadataEncoder();
EXPECT_EQ(OkStatus(), metadata_encoder.WriteFatal(true));
}
@@ -90,8 +90,8 @@ TEST(ReadUuid, UndersizedBuffer) {
0xB5,
0xBB};
std::array<std::byte, 32> snapshot_buffer;
- ConstByteSpan snapshot = EncodeSnapshotWithUuid(
- std::as_bytes(std::span(kExpectedUuid)), snapshot_buffer);
+ ConstByteSpan snapshot =
+ EncodeSnapshotWithUuid(as_bytes(span(kExpectedUuid)), snapshot_buffer);
std::array<std::byte, kUuidSizeBytes> uuid_dest;
Result<ConstByteSpan> result = ReadUuidFromSnapshot(snapshot, uuid_dest);
diff --git a/pw_software_update/BUILD.bazel b/pw_software_update/BUILD.bazel
index ef586fa1a..f3e21f96e 100644
--- a/pw_software_update/BUILD.bazel
+++ b/pw_software_update/BUILD.bazel
@@ -12,24 +12,90 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
"pw_cc_test",
)
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
proto_library(
+ name = "tuf_proto",
+ srcs = [
+ "tuf.proto",
+ ],
+ deps = [
+ "@com_google_protobuf//:timestamp_proto",
+ ],
+)
+
+proto_library(
name = "update_bundle_proto",
srcs = [
- "pw_software_update_protos/tuf.proto",
- "pw_software_update_protos/update_bundle.proto",
+ "update_bundle.proto",
+ ],
+ deps = [
+ ":tuf_proto",
+ ],
+)
+
+pw_proto_library(
+ name = "update_bundle_proto_cc",
+ deps = [":update_bundle_proto"],
+)
+
+proto_library(
+ name = "bundled_update_proto",
+ srcs = [
+ "bundled_update.proto",
+ ],
+ deps = [
+ "//pw_protobuf:common_proto",
+ "//pw_tokenizer:tokenizer_proto",
+ "@com_google_protobuf//:any_proto",
+ ],
+)
+
+# TODO(b/241456982): Not expected to build yet.
+py_proto_library(
+ name = "bundled_update_py_pb2",
+ srcs = ["bundled_update.proto"],
+ tags = ["manual"],
+)
+
+# TODO(b/258074401): Depends on the `any` proto, which doesn't build under
+# nanopb.
+# pw_proto_library(
+# name = "bundled_update_proto_cc",
+# # TODO(b/258074760): Adding this tag breaks the pw_proto_library rule.
+# tags = ["manual"],
+# deps = [":bundled_update_proto"],
+# )
+
+pw_cc_library(
+ name = "openable_reader",
+ hdrs = [
+ "public/pw_software_update/openable_reader.h",
+ ],
+ deps = [
+ "//pw_stream",
+ ],
+)
+
+pw_cc_library(
+ name = "blob_store_openable_reader",
+ hdrs = [
+ "public/pw_software_update/blob_store_openable_reader.h",
+ ],
+ deps = [
+ ":openable_reader",
],
- strip_import_prefix = "//pw_software_update",
)
pw_cc_library(
@@ -45,8 +111,14 @@ pw_cc_library(
"public/pw_software_update/update_bundle_accessor.h",
],
includes = ["public"],
+ tags = ["manual"], # TODO(b/236321905): Depends on pw_crypto.
deps = [
+ ":blob_store_openable_reader",
+ ":openable_reader",
+ ":update_bundle_proto_cc.pwpb",
"//pw_blob_store",
+ "//pw_crypto:ecdsa_facade",
+ "//pw_crypto:sha256_facade",
"//pw_kvs",
"//pw_log",
"//pw_protobuf",
@@ -56,30 +128,76 @@ pw_cc_library(
],
)
+# TODO(b/258074401): Depends on bundled_update_proto_cc.nanopb_rpc, which
+# doesn't build yet.
pw_cc_library(
name = "bundled_update_service",
srcs = ["bundled_update_service.cc"],
hdrs = ["public/pw_software_update/bundled_update_service.h"],
includes = ["public"],
+ tags = ["manual"],
deps = [
+ # ":bundled_update_proto_cc.nanopb_rpc",
+ # ":bundled_update_proto_cc.pwpb",
":update_bundle",
- ":update_bundle_proto",
+ # ":update_bundle_proto_cc.nanopb_rpc",
+ # ":update_bundle_proto_cc.pwpb",
"//pw_log",
"//pw_result",
"//pw_status",
"//pw_sync:borrow",
"//pw_sync:lock_annotations",
"//pw_sync:mutex",
- "//pw_sync:string",
"//pw_tokenizer",
"//pw_work_queue",
],
)
+# TODO(b/258074401): Depends on bundled_update_proto_cc.nanopb_rpc, which
+# doesn't build yet.
+pw_cc_library(
+ name = "bundled_update_service_pwpb",
+ srcs = ["bundled_update_service_pwpb.cc"],
+ hdrs = ["public/pw_software_update/bundled_update_service_pwpb.h"],
+ includes = ["public"],
+ tags = ["manual"],
+ deps = [
+ # ":bundled_update_proto_cc.pwpb",
+ # ":bundled_update_proto_cc.pwpb_rpc",
+ ":update_bundle",
+ # ":update_bundle_proto_cc.pwpb",
+ # ":update_bundle_proto_cc.pwpb_rpc",
+ "//pw_log",
+ "//pw_result",
+ "//pw_status",
+ "//pw_string:util",
+ "//pw_sync:borrow",
+ "//pw_sync:lock_annotations",
+ "//pw_sync:mutex",
+ "//pw_tokenizer",
+ "//pw_work_queue",
+ ],
+)
+
+# TODO(b/258222107): pw_python_action doesn't exist yet.
+# pw_python_action(
+# name = "generate_test_bundle",
+# outputs = ["$target_gen_dir/generate_test_bundle/test_bundles.h"],
+# script = "py/pw_software_update/generate_test_bundle.py",
+# deps = [
+# ":bundled_update_py_pb2",
+# "py",
+# ],
+# args = [ "$target_gen_dir/generate_test_bundle/test_bundles.h" ],
+# )
+
pw_cc_test(
name = "update_bundle_test",
srcs = ["update_bundle_test.cc"],
+ tags = ["manual"],
deps = [
+ # This dependency is needed, but doesn't exist yet.
+ # "generate_test_bundle",
":update_bundle",
"//pw_kvs:fake_flash_test_key_value_store",
"//pw_unit_test",
@@ -89,8 +207,19 @@ pw_cc_test(
pw_cc_test(
name = "bundled_update_service_test",
srcs = ["bundled_update_service_test.cc"],
+ tags = ["manual"], # bundled_update_service doesn't work yet.
deps = [
":bundled_update_service",
"//pw_unit_test",
],
)
+
+pw_cc_test(
+ name = "bundled_update_service_pwpb_test",
+ srcs = ["bundled_update_service_pwpb_test.cc"],
+ tags = ["manual"], # bundled_update_service_pwpb doesn't work yet.
+ deps = [
+ ":bundled_update_service_pwpb",
+ "//pw_unit_test",
+ ],
+)
diff --git a/pw_software_update/BUILD.gn b/pw_software_update/BUILD.gn
index f59438be0..73a8caba1 100644
--- a/pw_software_update/BUILD.gn
+++ b/pw_software_update/BUILD.gn
@@ -41,38 +41,27 @@ pw_source_set("config") {
public_deps = [ pw_software_update_CONFIG ]
}
-if (dir_pw_third_party_protobuf != "") {
- pw_proto_library("protos") {
- deps = [
- "$dir_pw_protobuf:common_protos",
- "$dir_pw_third_party/protobuf:wellknown_types",
- "$dir_pw_tokenizer:proto",
- ]
- sources = [
- "bundled_update.proto",
- "tuf.proto",
- "update_bundle.proto",
- ]
- inputs = [ "bundled_update.options" ]
- prefix = "pw_software_update"
- python_package = "py"
- }
-} else {
- # placeholder target to allow py package to build
- pw_proto_library("protos") {
- deps = [
- "$dir_pw_protobuf:common_protos",
- "$dir_pw_tokenizer:proto",
- ]
- sources = [
- "bundled_update.proto",
- "tuf.proto",
- "update_bundle.proto",
- ]
- inputs = [ "bundled_update.options" ]
- prefix = "pw_software_update"
- python_package = "py"
+pw_proto_library("protos") {
+ deps = [
+ "$dir_pw_protobuf:common_protos",
+ "$dir_pw_tokenizer:proto",
+ ]
+
+ if (dir_pw_third_party_protobuf != "") {
+ # nanopb does not automatically generate the well-known types. If we have
+ # a checkout of the protobuf repo, add it here so we can enable the nanopb
+ # targets.
+ deps += [ "$dir_pw_third_party/protobuf:wellknown_types" ]
}
+
+ sources = [
+ "bundled_update.proto",
+ "tuf.proto",
+ "update_bundle.proto",
+ ]
+ inputs = [ "bundled_update.options" ]
+ prefix = "pw_software_update"
+ python_package = "py"
}
pw_doc_group("docs") {
@@ -80,14 +69,27 @@ pw_doc_group("docs") {
}
if (pw_crypto_SHA256_BACKEND != "" && pw_crypto_ECDSA_BACKEND != "") {
+ pw_source_set("openable_reader") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ dir_pw_stream ]
+ public = [ "public/pw_software_update/openable_reader.h" ]
+ }
+
+ pw_source_set("blob_store_openable_reader") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":openable_reader",
+ dir_pw_blob_store,
+ ]
+ public = [ "public/pw_software_update/blob_store_openable_reader.h" ]
+ }
+
pw_source_set("update_bundle") {
public_configs = [ ":public_include_path" ]
public_deps = [
- "$dir_pw_crypto:ecdsa",
- "$dir_pw_crypto:sha256",
+ ":blob_store_openable_reader",
+ ":openable_reader",
"$dir_pw_stream:interval_reader",
- dir_pw_blob_store,
- dir_pw_kvs,
dir_pw_protobuf,
dir_pw_result,
dir_pw_status,
@@ -101,6 +103,8 @@ if (pw_crypto_SHA256_BACKEND != "" && pw_crypto_ECDSA_BACKEND != "") {
deps = [
":config",
":protos.pwpb",
+ "$dir_pw_crypto:ecdsa",
+ "$dir_pw_crypto:sha256",
dir_pw_log,
dir_pw_string,
]
@@ -110,6 +114,10 @@ if (pw_crypto_SHA256_BACKEND != "" && pw_crypto_ECDSA_BACKEND != "") {
]
}
} else {
+ group("openable_reader") {
+ }
+ group("blob_store_openable_reader") {
+ }
group("update_bundle") {
}
}
@@ -142,6 +150,29 @@ if (dir_pw_third_party_nanopb != "" && dir_pw_third_party_protobuf != "") {
}
}
+pw_source_set("bundled_update_service_pwpb") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":protos.pwpb_rpc",
+ ":update_bundle",
+ dir_pw_result,
+ dir_pw_status,
+ dir_pw_work_queue,
+ ]
+ deps = [
+ ":config",
+ ":protos.pwpb",
+ "$dir_pw_sync:borrow",
+ "$dir_pw_sync:lock_annotations",
+ "$dir_pw_sync:mutex",
+ dir_pw_log,
+ dir_pw_string,
+ dir_pw_tokenizer,
+ ]
+ public = [ "public/pw_software_update/bundled_update_service_pwpb.h" ]
+ sources = [ "bundled_update_service_pwpb.cc" ]
+}
+
pw_python_action("generate_test_bundle") {
header_output = "$target_gen_dir/$target_name/test_bundles.h"
script = "py/pw_software_update/generate_test_bundle.py"
@@ -167,17 +198,20 @@ pw_test("update_bundle_test") {
enable_if = all_dependency_met
sources = [ "update_bundle_test.cc" ]
public_deps = [
+ ":blob_store_openable_reader",
":bundled_update_service",
":generate_test_bundle",
":update_bundle",
"$dir_pw_kvs:fake_flash",
"$dir_pw_kvs:fake_flash_test_key_value_store",
+ dir_pw_blob_store,
]
configs = [ ":generated_test_bundle_include" ]
}
pw_test_group("tests") {
tests = [
+ ":bundled_update_service_pwpb_test",
":bundled_update_service_test",
":update_bundle_test",
]
@@ -188,3 +222,10 @@ pw_test("bundled_update_service_test") {
sources = [ "bundled_update_service_test.cc" ]
public_deps = [ ":bundled_update_service" ]
}
+
+pw_test("bundled_update_service_pwpb_test") {
+ enable_if = pw_thread_THREAD_BACKEND != "" &&
+ pw_crypto_SHA256_BACKEND != "" && pw_crypto_ECDSA_BACKEND != ""
+ sources = [ "bundled_update_service_pwpb_test.cc" ]
+ public_deps = [ ":bundled_update_service_pwpb" ]
+}
diff --git a/pw_software_update/bundled_update.proto b/pw_software_update/bundled_update.proto
index 697e3dd41..39ccfd1ee 100644
--- a/pw_software_update/bundled_update.proto
+++ b/pw_software_update/bundled_update.proto
@@ -134,10 +134,11 @@ message StartRequest {
optional string bundle_filename = 1;
}
-// TODO(pwbug/478): Add documentation and details of the API contract.
+// TODO(b/235273688): Add documentation and details of the API contract.
service BundledUpdate {
- // TODO: Offer GetCurrentManifest & GetStagedManifest() methods that leverage
- // pw_transfer to upload the manifest off the device, to enable host logic.
+ // TODO(keir): Offer GetCurrentManifest & GetStagedManifest() methods that
+ // leverage pw_transfer to upload the manifest off the device, to enable host
+ // logic.
// Get current status of software update.
rpc GetStatus(pw.protobuf.Empty) returns (BundledUpdateStatus);
diff --git a/pw_software_update/bundled_update_service.cc b/pw_software_update/bundled_update_service.cc
index cbbc67e4f..62c135e89 100644
--- a/pw_software_update/bundled_update_service.cc
+++ b/pw_software_update/bundled_update_service.cc
@@ -136,6 +136,7 @@ Status BundledUpdateService::SetTransferred(const pw_protobuf_Empty&,
if (state != pw_software_update_BundledUpdateState_Enum_TRANSFERRING &&
state != pw_software_update_BundledUpdateState_Enum_INACTIVE) {
+ std::lock_guard lock(mutex_);
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_UNKNOWN_ERROR,
"SetTransferred() can only be called from TRANSFERRING or "
"INACTIVE state. State: %d",
@@ -150,7 +151,7 @@ Status BundledUpdateService::SetTransferred(const pw_protobuf_Empty&,
return OkStatus();
}
-// TODO: Check for "ABORTING" state and bail if it's set.
+// TODO(keir): Check for "ABORTING" state and bail if it's set.
void BundledUpdateService::DoVerify() {
std::lock_guard guard(mutex_);
const BundledUpdateState state = status_.acquire()->state;
@@ -215,7 +216,7 @@ Status BundledUpdateService::Verify(const pw_protobuf_Empty&,
return OkStatus();
}
- // TODO: Remove the transferring permitted state here ASAP.
+ // TODO(keir): Remove the transferring permitted state here ASAP.
// Ensure we're in the right state.
if ((state != pw_software_update_BundledUpdateState_Enum_TRANSFERRING) &&
(state != pw_software_update_BundledUpdateState_Enum_TRANSFERRED)) {
@@ -226,7 +227,7 @@ Status BundledUpdateService::Verify(const pw_protobuf_Empty&,
return Status::FailedPrecondition();
}
- // TODO: We should probably make this mode idempotent.
+ // TODO(keir): We should probably make this mode idempotent.
// Already doing what was asked? Bail.
if (work_enqueued_) {
PW_LOG_DEBUG("Verification is already active");
@@ -280,7 +281,7 @@ Status BundledUpdateService::Apply(const pw_protobuf_Empty&,
return Status::FailedPrecondition();
}
- // TODO: We should probably make these all idempotent properly.
+ // TODO(keir): We should probably make these all idempotent properly.
if (work_enqueued_) {
PW_LOG_DEBUG("Apply is already active");
return OkStatus();
@@ -349,7 +350,7 @@ void BundledUpdateService::DoApply() {
for (pw::protobuf::Message file_name : target_files) {
std::array<std::byte, MAX_TARGET_NAME_LENGTH> buf = {};
protobuf::String name = file_name.AsString(static_cast<uint32_t>(
- pw::software_update::TargetFile::Fields::FILE_NAME));
+ pw::software_update::TargetFile::Fields::kFileName));
PW_CHECK_OK(name.status());
const Result<ByteSpan> read_result = name.GetBytesReader().Read(buf);
PW_CHECK_OK(read_result.status());
@@ -373,7 +374,7 @@ void BundledUpdateService::DoApply() {
SET_ERROR(pw_software_update_BundledUpdateResult_Enum_APPLY_FAILED,
"Could not open contents of file %s from bundle; "
"aborting update apply phase",
- static_cast<int>(file_reader.status().code()));
+ MakeString<MAX_TARGET_NAME_LENGTH>(file_name_view).c_str());
return;
}
@@ -432,7 +433,8 @@ Status BundledUpdateService::Abort(const pw_protobuf_Empty&,
"Tried to abort when already INACTIVE or FINISHED");
return Status::FailedPrecondition();
}
- // TODO: Switch abort to async; this state change isn't externally visible.
+ // TODO(keir): Switch abort to async; this state change isn't externally
+ // visible.
status_.acquire()->state =
pw_software_update_BundledUpdateState_Enum_ABORTING;
@@ -467,7 +469,7 @@ Status BundledUpdateService::Reset(const pw_protobuf_Empty&,
// Reset the bundle.
if (bundle_open_) {
- // TODO: Revisit whether this is recoverable; maybe eliminate CHECK.
+ // TODO(keir): Revisit whether this is recoverable; maybe eliminate CHECK.
PW_CHECK_OK(bundle_.Close());
bundle_open_ = false;
}
@@ -522,7 +524,7 @@ void BundledUpdateService::Finish(
// Close out any open bundles.
if (bundle_open_) {
- // TODO: Revisit this check; may be able to recover.
+ // TODO(keir): Revisit this check; may be able to recover.
PW_CHECK_OK(bundle_.Close());
bundle_open_ = false;
}
diff --git a/pw_software_update/bundled_update_service_pwpb.cc b/pw_software_update/bundled_update_service_pwpb.cc
new file mode 100644
index 000000000..cd6d370b2
--- /dev/null
+++ b/pw_software_update/bundled_update_service_pwpb.cc
@@ -0,0 +1,521 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#define PW_LOG_MODULE_NAME "PWSU"
+#define PW_LOG_LEVEL PW_LOG_LEVEL_WARN
+
+#include "pw_software_update/bundled_update_service_pwpb.h"
+
+#include <mutex>
+#include <string_view>
+
+#include "pw_log/log.h"
+#include "pw_result/result.h"
+#include "pw_software_update/config.h"
+#include "pw_software_update/manifest_accessor.h"
+#include "pw_software_update/update_bundle.pwpb.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_status/try.h"
+#include "pw_string/string_builder.h"
+#include "pw_string/util.h"
+#include "pw_sync/borrow.h"
+#include "pw_sync/mutex.h"
+#include "pw_tokenizer/tokenize.h"
+
+namespace pw::software_update {
+namespace {
+using BorrowedStatus =
+ sync::BorrowedPointer<BundledUpdateStatus::Message, sync::Mutex>;
+
+// TODO(keir): Convert all the CHECKs in the RPC service to gracefully report
+// errors.
+#define SET_ERROR(res, message, ...) \
+ do { \
+ PW_LOG_ERROR(message, __VA_ARGS__); \
+ if (!IsFinished()) { \
+ Finish(res); \
+ { \
+ BorrowedStatus borrowed_status = status_.acquire(); \
+ size_t note_size = borrowed_status->note.max_size(); \
+ borrowed_status->note.resize(note_size); \
+ PW_TOKENIZE_TO_BUFFER( \
+ &borrowed_status->note, &(note_size), message, __VA_ARGS__); \
+ borrowed_status->note.resize(note_size); \
+ } \
+ } \
+ } while (false)
+} // namespace
+
+Status BundledUpdateService::GetStatus(const pw::protobuf::Empty::Message&,
+ BundledUpdateStatus::Message& response) {
+ response = *status_.acquire();
+ return OkStatus();
+}
+
+Status BundledUpdateService::Start(const StartRequest::Message& request,
+ BundledUpdateStatus::Message& response) {
+ std::lock_guard lock(mutex_);
+ // Check preconditions.
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+ if (state != BundledUpdateState::Enum::kInactive) {
+ SET_ERROR(BundledUpdateResult::Enum::kUnknownError,
+ "Start() can only be called from INACTIVE state. "
+ "Current state: %d. Abort() then Reset() must be called first",
+ static_cast<int>(state));
+ response = *status_.acquire();
+ return Status::FailedPrecondition();
+ }
+
+ {
+ BorrowedStatus borrowed_status = status_.acquire();
+ PW_DCHECK(!borrowed_status->transfer_id.has_value());
+ PW_DCHECK(!borrowed_status->result.has_value());
+ PW_DCHECK(
+ !borrowed_status->current_state_progress_hundreth_percent.has_value());
+ PW_DCHECK(borrowed_status->bundle_filename.empty());
+ PW_DCHECK(borrowed_status->note.empty());
+ }
+
+ // Notify the backend of pending transfer.
+ if (const Status status = backend_.BeforeUpdateStart(); !status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kUnknownError,
+ "Backend error on BeforeUpdateStart()");
+ response = *status_.acquire();
+ return status;
+ }
+
+ // Enable bundle transfer.
+ Result<uint32_t> possible_transfer_id =
+ backend_.EnableBundleTransferHandler(request.bundle_filename);
+ if (!possible_transfer_id.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kTransferFailed,
+ "Couldn't enable bundle transfer");
+ response = *status_.acquire();
+ return possible_transfer_id.status();
+ }
+
+ // Update state.
+ {
+ BorrowedStatus borrowed_status = status_.acquire();
+ borrowed_status->transfer_id = possible_transfer_id.value();
+ if (!request.bundle_filename.empty()) {
+ borrowed_status->bundle_filename = request.bundle_filename;
+ }
+ borrowed_status->state = BundledUpdateState::Enum::kTransferring;
+ response = *borrowed_status;
+ }
+ return OkStatus();
+}
+
+Status BundledUpdateService::SetTransferred(
+ const pw::protobuf::Empty::Message&,
+ BundledUpdateStatus::Message& response) {
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+
+ if (state != BundledUpdateState::Enum::kTransferring &&
+ state != BundledUpdateState::Enum::kInactive) {
+ std::lock_guard lock(mutex_);
+ SET_ERROR(BundledUpdateResult::Enum::kUnknownError,
+ "SetTransferred() can only be called from TRANSFERRING or "
+ "INACTIVE state. State: %d",
+ static_cast<int>(state));
+ response = *status_.acquire();
+ return OkStatus();
+ }
+
+ NotifyTransferSucceeded();
+
+ response = *status_.acquire();
+ return OkStatus();
+}
+
+// TODO(elipsitz): Check for "ABORTING" state and bail if it's set.
+void BundledUpdateService::DoVerify() {
+ std::lock_guard guard(mutex_);
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+
+ if (state == BundledUpdateState::Enum::kVerified) {
+ return; // Already done!
+ }
+
+ // Ensure we're in the right state.
+ if (state != BundledUpdateState::Enum::kTransferred) {
+ SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed,
+ "DoVerify() must be called from TRANSFERRED state. State: %d",
+ static_cast<int>(state));
+ return;
+ }
+
+ status_.acquire()->state = BundledUpdateState::Enum::kVerifying;
+
+ // Notify backend about pending verify.
+ if (const Status status = backend_.BeforeBundleVerify(); !status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed,
+ "Backend::BeforeBundleVerify() failed");
+ return;
+ }
+
+ // Do the actual verify.
+ Status status = bundle_.OpenAndVerify();
+ if (!status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed,
+ "Bundle::OpenAndVerify() failed");
+ return;
+ }
+ bundle_open_ = true;
+
+ // Have the backend verify the user_manifest if present.
+ if (!backend_.VerifyManifest(bundle_.GetManifest()).ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed,
+ "Backend::VerifyUserManifest() failed");
+ return;
+ }
+
+ // Notify backend we're done verifying.
+ status = backend_.AfterBundleVerified();
+ if (!status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed,
+ "Backend::AfterBundleVerified() failed");
+ return;
+ }
+ status_.acquire()->state = BundledUpdateState::Enum::kVerified;
+}
+
+Status BundledUpdateService::Verify(const pw::protobuf::Empty::Message&,
+ BundledUpdateStatus::Message& response) {
+ std::lock_guard lock(mutex_);
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+
+ // Already done? Bail.
+ if (state == BundledUpdateState::Enum::kVerified) {
+ PW_LOG_DEBUG("Skipping verify since already verified");
+ return OkStatus();
+ }
+
+ // TODO(elipsitz): Remove the transferring permitted state here ASAP.
+ // Ensure we're in the right state.
+ if ((state != BundledUpdateState::Enum::kTransferring) &&
+ (state != BundledUpdateState::Enum::kTransferred)) {
+ SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed,
+ "Verify() must be called from TRANSFERRED state. State: %d",
+ static_cast<int>(state));
+ response = *status_.acquire();
+ return Status::FailedPrecondition();
+ }
+
+ // TODO(elipsitz): We should probably make this mode idempotent.
+ // Already doing what was asked? Bail.
+ if (work_enqueued_) {
+ PW_LOG_DEBUG("Verification is already active");
+ return OkStatus();
+ }
+
+ // The backend's ApplyReboot as part of DoApply() shall be configured
+ // such that this RPC can send out the reply before the device reboots.
+ const Status status = work_queue_.PushWork([this] {
+ {
+ std::lock_guard y_lock(this->mutex_);
+ PW_DCHECK(this->work_enqueued_);
+ }
+ this->DoVerify();
+ {
+ std::lock_guard y_lock(this->mutex_);
+ this->work_enqueued_ = false;
+ }
+ });
+ if (!status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kVerifyFailed,
+ "Unable to equeue apply to work queue");
+ response = *status_.acquire();
+ return status;
+ }
+ work_enqueued_ = true;
+
+ response = *status_.acquire();
+ return OkStatus();
+}
+
+Status BundledUpdateService::Apply(const pw::protobuf::Empty::Message&,
+ BundledUpdateStatus::Message& response) {
+ std::lock_guard lock(mutex_);
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+
+ // We do not wait to go into a finished error state if we're already
+ // applying, instead just let them know that yes we are working on it --
+ // hold on.
+ if (state == BundledUpdateState::Enum::kApplying) {
+ PW_LOG_DEBUG("Apply is already active");
+ return OkStatus();
+ }
+
+ if ((state != BundledUpdateState::Enum::kTransferred) &&
+ (state != BundledUpdateState::Enum::kVerified)) {
+ SET_ERROR(BundledUpdateResult::Enum::kApplyFailed,
+ "Apply() must be called from TRANSFERRED or VERIFIED state. "
+ "State: %d",
+ static_cast<int>(state));
+ return Status::FailedPrecondition();
+ }
+
+ // TODO(elipsitz): We should probably make these all idempotent properly.
+ if (work_enqueued_) {
+ PW_LOG_DEBUG("Apply is already active");
+ return OkStatus();
+ }
+
+ // The backend's ApplyReboot as part of DoApply() shall be configured
+ // such that this RPC can send out the reply before the device reboots.
+ const Status status = work_queue_.PushWork([this] {
+ {
+ std::lock_guard y_lock(this->mutex_);
+ PW_DCHECK(this->work_enqueued_);
+ }
+ // Error reporting is handled in DoVerify and DoApply.
+ this->DoVerify();
+ this->DoApply();
+ {
+ std::lock_guard y_lock(this->mutex_);
+ this->work_enqueued_ = false;
+ }
+ });
+ if (!status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kApplyFailed,
+ "Unable to equeue apply to work queue");
+ response = *status_.acquire();
+ return status;
+ }
+ work_enqueued_ = true;
+
+ return OkStatus();
+}
+
+void BundledUpdateService::DoApply() {
+ std::lock_guard guard(mutex_);
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+
+ PW_LOG_DEBUG("Attempting to apply the update");
+ if (state != BundledUpdateState::Enum::kVerified) {
+ SET_ERROR(BundledUpdateResult::Enum::kApplyFailed,
+ "Apply() must be called from VERIFIED state. State: %d",
+ static_cast<int>(state));
+ return;
+ }
+
+ status_.acquire()->state = BundledUpdateState::Enum::kApplying;
+
+ if (const Status status = backend_.BeforeApply(); !status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kApplyFailed,
+ "BeforeApply() returned unsuccessful result: %d",
+ static_cast<int>(status.code()));
+ return;
+ }
+
+ // In order to report apply progress, quickly scan to see how many bytes
+ // will be applied.
+ Result<uint64_t> total_payload_bytes = bundle_.GetTotalPayloadSize();
+ PW_CHECK_OK(total_payload_bytes.status());
+ size_t target_file_bytes_to_apply =
+ static_cast<size_t>(total_payload_bytes.value());
+
+ protobuf::RepeatedMessages target_files =
+ bundle_.GetManifest().GetTargetFiles();
+ PW_CHECK_OK(target_files.status());
+
+ size_t target_file_bytes_applied = 0;
+ for (pw::protobuf::Message file_name : target_files) {
+ std::array<std::byte, MAX_TARGET_NAME_LENGTH> buf = {};
+ protobuf::String name = file_name.AsString(static_cast<uint32_t>(
+ pw::software_update::TargetFile::Fields::kFileName));
+ PW_CHECK_OK(name.status());
+ const Result<ByteSpan> read_result = name.GetBytesReader().Read(buf);
+ PW_CHECK_OK(read_result.status());
+ const ConstByteSpan file_name_span = read_result.value();
+ const std::string_view file_name_view(
+ reinterpret_cast<const char*>(file_name_span.data()),
+ file_name_span.size_bytes());
+ if (file_name_view.compare(kUserManifestTargetFileName) == 0) {
+ continue; // user_manifest is not applied by the backend.
+ }
+ // Try to get an IntervalReader for the current file.
+ stream::IntervalReader file_reader =
+ bundle_.GetTargetPayload(file_name_view);
+ if (file_reader.status().IsNotFound()) {
+ PW_LOG_INFO(
+ "Contents of file %s missing from bundle; ignoring",
+ pw::MakeString<MAX_TARGET_NAME_LENGTH>(file_name_view).c_str());
+ continue;
+ }
+ if (!file_reader.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kApplyFailed,
+ "Could not open contents of file %s from bundle; "
+ "aborting update apply phase",
+ MakeString<MAX_TARGET_NAME_LENGTH>(file_name_view).c_str());
+ return;
+ }
+
+ const size_t bundle_offset = file_reader.start();
+ if (const Status status = backend_.ApplyTargetFile(
+ file_name_view, file_reader, bundle_offset);
+ !status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kApplyFailed,
+ "Failed to apply target file: %d",
+ static_cast<int>(status.code()));
+ return;
+ }
+ target_file_bytes_applied += file_reader.interval_size();
+ const uint32_t progress_hundreth_percent =
+ (static_cast<uint64_t>(target_file_bytes_applied) * 100 * 100) /
+ target_file_bytes_to_apply;
+ PW_LOG_DEBUG("Apply progress: %zu/%zu Bytes (%ld%%)",
+ target_file_bytes_applied,
+ target_file_bytes_to_apply,
+ static_cast<unsigned long>(progress_hundreth_percent / 100));
+ {
+ BorrowedStatus borrowed_status = status_.acquire();
+ borrowed_status->current_state_progress_hundreth_percent =
+ progress_hundreth_percent;
+ }
+ }
+
+ // TODO(davidrogers): Add new APPLY_REBOOTING to distinguish between pre and
+ // post reboot.
+
+ // Finalize the apply.
+ if (const Status status = backend_.ApplyReboot(); !status.ok()) {
+ SET_ERROR(BundledUpdateResult::Enum::kApplyFailed,
+ "Failed to do the apply reboot: %d",
+ static_cast<int>(status.code()));
+ return;
+ }
+
+ // TODO(davidrogers): Move this to MaybeFinishApply() once available.
+ Finish(BundledUpdateResult::Enum::kSuccess);
+}
+
+Status BundledUpdateService::Abort(const pw::protobuf::Empty::Message&,
+ BundledUpdateStatus::Message& response) {
+ std::lock_guard lock(mutex_);
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+
+ if (state == BundledUpdateState::Enum::kApplying) {
+ return Status::FailedPrecondition();
+ }
+
+ if (state == BundledUpdateState::Enum::kInactive ||
+ state == BundledUpdateState::Enum::kFinished) {
+ SET_ERROR(BundledUpdateResult::Enum::kUnknownError,
+ "Tried to abort when already INACTIVE or FINISHED");
+ return Status::FailedPrecondition();
+ }
+ // TODO(elipsitz): Switch abort to async; this state change isn't externally
+ // visible.
+ status_.acquire()->state = BundledUpdateState::Enum::kAborting;
+
+ SET_ERROR(BundledUpdateResult::Enum::kAborted, "Update abort requested");
+ response = *status_.acquire();
+ return OkStatus();
+}
+
+Status BundledUpdateService::Reset(const pw::protobuf::Empty::Message&,
+ BundledUpdateStatus::Message& response) {
+ std::lock_guard lock(mutex_);
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+
+ if (state == BundledUpdateState::Enum::kInactive) {
+ return OkStatus(); // Already done.
+ }
+
+ if (state != BundledUpdateState::Enum::kFinished) {
+ SET_ERROR(
+ BundledUpdateResult::Enum::kUnknownError,
+ "Reset() must be called from FINISHED or INACTIVE state. State: %d",
+ static_cast<int>(state));
+ response = *status_.acquire();
+ return Status::FailedPrecondition();
+ }
+
+ {
+ BorrowedStatus status = status_.acquire();
+ *status = {}; // Force-init all fields to zero.
+ status->state = BundledUpdateState::Enum::kInactive;
+ }
+
+ // Reset the bundle.
+ if (bundle_open_) {
+ // TODO(elipsitz): Revisit whether this is recoverable; maybe eliminate
+ // CHECK.
+ PW_CHECK_OK(bundle_.Close());
+ bundle_open_ = false;
+ }
+
+ response = *status_.acquire();
+ return OkStatus();
+}
+
+void BundledUpdateService::NotifyTransferSucceeded() {
+ std::lock_guard lock(mutex_);
+ const BundledUpdateState::Enum state = status_.acquire()->state;
+
+ if (state != BundledUpdateState::Enum::kTransferring) {
+ // This can happen if the update gets Abort()'d during the transfer and
+ // the transfer completes successfully.
+ PW_LOG_WARN(
+ "Got transfer succeeded notification when not in TRANSFERRING state. "
+ "State: %d",
+ static_cast<int>(state));
+ }
+
+ const bool transfer_ongoing = status_.acquire()->transfer_id.has_value();
+ if (transfer_ongoing) {
+ backend_.DisableBundleTransferHandler();
+ status_.acquire()->transfer_id.reset();
+ } else {
+ PW_LOG_WARN("No ongoing transfer found, forcefully set TRANSFERRED.");
+ }
+
+ status_.acquire()->state = BundledUpdateState::Enum::kTransferred;
+}
+
+void BundledUpdateService::Finish(BundledUpdateResult::Enum result) {
+ if (result == BundledUpdateResult::Enum::kSuccess) {
+ BorrowedStatus borrowed_status = status_.acquire();
+ borrowed_status->current_state_progress_hundreth_percent.reset();
+ } else {
+ // In the case of error, notify backend that we're about to abort the
+ // software update.
+ PW_CHECK_OK(backend_.BeforeUpdateAbort());
+ }
+
+ // Turn down the transfer if one is in progress.
+ const bool transfer_ongoing = status_.acquire()->transfer_id.has_value();
+ if (transfer_ongoing) {
+ backend_.DisableBundleTransferHandler();
+ }
+ status_.acquire()->transfer_id.reset();
+
+ // Close out any open bundles.
+ if (bundle_open_) {
+ // TODO(elipsitz): Revisit this check; may be able to recover.
+ PW_CHECK_OK(bundle_.Close());
+ bundle_open_ = false;
+ }
+ {
+ BorrowedStatus borrowed_status = status_.acquire();
+ borrowed_status->state = BundledUpdateState::Enum::kFinished;
+ borrowed_status->result = result;
+ }
+}
+
+} // namespace pw::software_update
diff --git a/pw_software_update/bundled_update_service_pwpb_test.cc b/pw_software_update/bundled_update_service_pwpb_test.cc
new file mode 100644
index 000000000..1193d1a7c
--- /dev/null
+++ b/pw_software_update/bundled_update_service_pwpb_test.cc
@@ -0,0 +1,19 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_software_update/bundled_update_service_pwpb.h"
+
+#include "gtest/gtest.h"
+
+TEST(BundledUpdateServicePwpb, Compiles) {}
diff --git a/pw_software_update/docs.rst b/pw_software_update/docs.rst
index fb1927856..48131e510 100644
--- a/pw_software_update/docs.rst
+++ b/pw_software_update/docs.rst
@@ -4,8 +4,441 @@
pw_software_update
-------------------
-This modules provides software update functionality.
+This module provides the following building blocks of a high assurance software
+update system:
-.. warning::
- This module is under construction, not ready for use, and the documentation
- is incomplete.
+1. A `TUF <https://theupdateframework.io>`_-based security framework.
+2. A `protocol buffer <https://developers.google.com/protocol-buffers>`_ based
+ software update "bundle" format.
+3. An update bundle decoder and verification stack.
+4. An extensible software update RPC service.
+
+High assurance software update
+==============================
+
+On a high-level, a high-assurance software update system should make users feel
+safe to use and be technologically worthy of user's trust over time. In
+particular it should demonstrate the following security and privacy properties.
+
+1. The update packages are generic, sufficiently qualified, and officially
+ signed with strong insider attack guardrails.
+2. The update packages are delivered over secure channels.
+3. Update checking, changelist, and installation are done with strong user
+ authorization and awareness.
+4. Incoming update packages are strongly authenticated by the client.
+5. Software update requires and builds on top of verified boot.
+
+Life of a software update
+=========================
+
+The following describes a typical software update sequence of events. The focus
+is not to prescribe implementation details but to raise awareness in subtle
+security and privacy considerations.
+
+Stage 0: Product makers create and publish updates
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+A (system) software update is essentially a new version of the on-device
+software stack. Product makers create, qualify and publish new software updates
+to deliver new experiences or bug fixes to users.
+
+While not visible to end users, the product maker team is expected to follow
+widely agreed security and release engineering best practices before signing and
+publishing a new software update. A new software update should be generic for
+all devices, rather than targeting specific devices.
+
+Stage 1: Users check for updates
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+For most consumer products, software updates are "opt-in", which means users
+either manually check for new updates or opt-in for the device itself to check
+(and/or install) updates automatically on the user's behalf. The opt-in may be
+blanket or conditioned on the nature of the updates.
+
+If users have authorized automatic updates, update checking also happens on a
+regular schedule and at every reboot.
+
+.. important::
+ As a critical security recovery mechanism, checking and installing software
+ updates ideally should happen early in boot, where the software stack has
+ been freshly verified by verified boot and minimum mutable input is taken
+ into account in update checking and installation.
+
+ In other words, the longer the system has been running (up), the greater
+ the chance that system has been taken control by an attacker. So it is
+ a good idea to remind users to reboot when the system has been running for
+ "too long".
+
+Stage 2: Users install updates
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Once a new update has been determined to be available for the device, users will
+be prompted to authorize downloading and installing the update. Users can also
+opt-in to automatic downloading and installing.
+
+.. important::
+ If feasible, rechecking, downloading and installing an update should be
+ carried out early in a reboot -- to recover from potential temporary attacker
+ control.
+
+To improve reliability and reduce disruption, modern system updates typically
+employ an A/B update mechanism, where the incoming update is installed into
+a backup boot slot, and only enacted and locked down (anti-rollback) after
+the new slot has passed boot verification and fully booted into a good state.
+
+.. important::
+ While a system update is usually carried out by a user space update client,
+ an incoming update may contain more than just the user space. It could
+ contain firmware for the bootloader, trusted execution environment, DSP,
+ sensor cores etc. which could be important components of a device's TCB (
+ trusted compute base, where critical device security policies are enforced).
+ When updating these components across different domains, it is best to let
+ each component carry out the actual updating, some of which may require
+ stronger user authorization (e.g. a test of physical presence, explicit
+ authorization with an admin passcode etc.)
+
+Lastly, updates should be checked again in case there are newer updates
+available.
+
+
+Command-line Interface
+======================
+
+You can access the software update CLI by running ``pw update`` in the pigweed environment.
+
+
+.. code-block:: bash
+
+ ~$ cd pigweed
+ ~/pigweed$ source ./activate.sh
+ ~/pigweed$ pw update
+
+ usage: pw update [-h] <command>
+
+ Software update related operations.
+
+ positional arguments:
+ generate-key
+ create-root-metadata
+ sign-root-metadata
+ inspect-root-metadata
+ create-empty-bundle
+ add-root-metadata-to-bundle
+ add-file-to-bundle
+ sign-bundle
+ inspect-bundle
+ verify-bundle
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ Learn more at: pigweed.dev/pw_software_update
+
+
+generate-key
+^^^^^^^^^^^^
+
+The ``generate-key`` subcommmand generates an ECDSA SHA-256 public + private keypair.
+
+.. code-block:: bash
+
+ $ pw update generate-key -h
+
+ usage: pw update generate-key [-h] pathname
+
+ Generates an ecdsa-sha2-nistp256 signing key pair (private + public)
+
+ positional arguments:
+ pathname Path to generated key pair
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+
++------------+------------+----------------+
+| positional argument |
++============+============+================+
+|``pathname``|path to the generated keypair|
++------------+------------+----------------+
+
+create-root-metadata
+^^^^^^^^^^^^^^^^^^^^
+
+The ``create-root-metadata`` subcommand creates a root metadata.
+
+.. code-block:: bash
+
+ $ pw update create-root-metadata -h
+
+ usage: pw update create-root-metadata [-h] [--version VERSION] --append-root-key ROOT_KEY
+ --append-targets-key TARGETS_KEY -o/--out OUT
+
+ Creation of root metadata
+
+ optional arguments:
+ -h, --help show this help message and exit
+ --version VERSION Canonical version number for rollback checks;
+ Defaults to 1
+
+ required arguments:
+ --append-root-key ROOT_KEY Path to root key
+ --append-targets-key TARGETS_KEY Path to targets key
+ -o OUT, --out OUT Path to output file
+
+
+
++--------------------------+-------------------------------------------+
+| required arguments |
++==========================+===========================================+
+|``--append-root-key`` | path to desired root key |
++--------------------------+-------------------------------------------+
+|``--append-targets-key`` | path to desired target key |
++--------------------------+-------------------------------------------+
+|``--out`` | output path of newly created root metadata|
++--------------------------+-------------------------------------------+
+
+
++-------------+------------+------------------------------+
+| optional argument |
++=============+============+==============================+
+|``--version``| Rollback version number(default set to 1) |
++-------------+------------+------------------------------+
+
+sign-root-metadata
+^^^^^^^^^^^^^^^^^^
+
+The ``sign-root-metadata`` subcommand signs a given root metadata.
+
+.. code-block:: bash
+
+ usage: pw update sign-root-metadata [-h] --root-metadata ROOT_METADATA --root-key ROOT_KEY
+
+ Signing of root metadata
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ required arguments:
+ --root-metadata ROOT_METADATA Root metadata to be signed
+ --root-key ROOT_KEY Root signing key
+
+
+
++--------------------------+-------------------------------------------+
+| required arguments |
++==========================+===========================================+
+|``--root-metadata`` | Path of root metadata to be signed |
++--------------------------+-------------------------------------------+
+|``--root-key`` | Path to root signing key |
++--------------------------+-------------------------------------------+
+
+inspect-root-metadata
+^^^^^^^^^^^^^^^^^^^^^
+
+The ``inspect-root-metadata`` subcommand prints the contents of a given root metadata.
+
+.. code-block:: bash
+
+ $ pw update inspect-root-metadata -h
+
+ usage: pw update inspect-root-metadata [-h] pathname
+
+ Outputs contents of root metadata
+
+ positional arguments:
+ pathname Path to root metadata
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+
++--------------------------+-------------------------------------------+
+| positional argument |
++==========================+===========================================+
+|``pathname`` | Path to root metadata |
++--------------------------+-------------------------------------------+
+
+
+create-empty-bundle
+^^^^^^^^^^^^^^^^^^^
+
+The ``create-empty-bundle`` subcommand creates an empty update bundle.
+
+.. code-block:: bash
+
+ $ pw update create-empty-bundle -h
+
+ usage: pw update create-empty-bundle [-h] [--target-metadata-version VERSION] pathname
+
+ Creation of an empty bundle
+
+ positional arguments:
+ pathname Path to newly created empty bundle
+
+ optional arguments:
+ -h, --help show this help message and exit
+ --target-metadata-version VERSION Version number for targets metadata;
+ Defaults to 1
+
++--------------------------+-------------------------------------------+
+| positional argument |
++==========================+===========================================+
+|``pathname`` | Path to newly created empty bundle |
++--------------------------+-------------------------------------------+
+
++------------------------------+--------------------------------------+
+| optional arguments |
++==============================+======================================+
+|``--target-metadata-version`` | Version number for targets metadata; |
+| | Defaults to 1 |
++------------------------------+--------------------------------------+
+
+add-root-metadata-to-bundle
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``add-root-metadata-to-bundle`` subcommand adds a root metadata to a bundle.
+
+.. code-block:: bash
+
+ $ pw update add-root-metadata-to-bundle -h
+
+ usage: pw update add-root-metadata-to-bundle [-h] --append-root-metadata ROOT_METADATA
+ --bundle BUNDLE
+
+ Add root metadata to a bundle
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ required arguments:
+ --append-root-metadata ROOT_METADATA Path to root metadata
+ --bundle BUNDLE Path to bundle
+
++--------------------------+-------------------------------------------+
+| required arguments |
++==========================+===========================================+
+|``--append-root-metadata``| Path to root metadata |
++--------------------------+-------------------------------------------+
+|``--bundle`` | Path to bundle |
++--------------------------+-------------------------------------------+
+
+
+add-file-to-bundle
+^^^^^^^^^^^^^^^^^^
+
+The ``add-file-to-bundle`` subcommand adds a target file to an existing bundle.
+
+.. code-block:: bash
+
+ $ pw update add-file-to-bundle -h
+
+ usage: pw update add-file-to-bundle [-h] [--new-name NEW_NAME] --bundle BUNDLE
+ --file FILE_PATH
+
+ Add a file to an existing bundle
+
+ optional arguments:
+ -h, --help show this help message and exit
+ --new-name NEW_NAME Optional new name for target
+
+ required arguments:
+ --bundle BUNDLE Path to an existing bundle
+ --file FILE_PATH Path to a target file
+
++--------------------------+-------------------------------------------+
+| required arguments |
++==========================+===========================================+
+|``--file`` | Path to a target file |
++--------------------------+-------------------------------------------+
+|``--bundle`` | Path to bundle |
++--------------------------+-------------------------------------------+
+
++--------------------------+-------------------------------------------+
+| optional argument |
++==========================+===========================================+
+|``--new-name`` | Optional new name for target |
++--------------------------+-------------------------------------------+
+
+sign-bundle
+^^^^^^^^^^^
+
+The ``sign-bundle`` subcommand signs an existing bundle with a dev key.
+
+.. code-block:: bash
+
+ $ pw update sign-bundle -h
+
+ usage: pw update sign-bundle [-h] --bundle BUNDLE --key KEY
+
+ Sign an existing bundle using a development key
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ required arguments:
+ --bundle BUNDLE Bundle to be signed
+ --key KEY Bundle signing key
+
++--------------------------+-------------------------------------------+
+| required arguments |
++==========================+===========================================+
+|``--key`` | Key to sign bundle |
++--------------------------+-------------------------------------------+
+|``--bundle`` | Path to bundle |
++--------------------------+-------------------------------------------+
+
+inspect-bundle
+^^^^^^^^^^^^^^
+
+The ``inspect-bundle`` subcommand prints the contents of a given bundle.
+
+.. code-block:: bash
+
+ $ pw update inspect-bundle -h
+
+ usage: pw update inspect-bundle [-h] pathname
+
+ Outputs contents of bundle
+
+ positional arguments:
+ pathname Path to bundle
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+
++------------+------------+----------------+
+| positional argument |
++============+============+================+
+|``pathname``|Path to bundle |
++------------+------------+----------------+
+
+verify-bundle
+^^^^^^^^^^^^^
+
+The ``verify-bundle`` subcommand performs verification of an existing bundle.
+
+.. code-block:: bash
+
+ $ pw update verify-bundle -h
+
+ usage: pw update verify-bundle [-h] --bundle BUNDLE
+ --trusted-root-metadata ROOT_METADATA
+
+ Verify a bundle
+
+ optional arguments:
+ -h, --help show this help message and exit
+
+ required arguments:
+ --bundle BUNDLE Bundle to be verified
+ --trusted-root-metadata ROOT_METADATA Trusted root metadata
+
++---------------------------+-------------------------------------------+
+| required arguments |
++===========================+===========================================+
+|``--trusted-root-metadata``| Trusted root metadata(anchor) |
++---------------------------+-------------------------------------------+
+|``--bundle`` | Path of bundle to be verified |
++---------------------------+-------------------------------------------+
+
+Getting started with bundles (coming soon)
+==========================================
diff --git a/pw_software_update/manifest_accessor.cc b/pw_software_update/manifest_accessor.cc
index 14e7ad1d1..d263dd51b 100644
--- a/pw_software_update/manifest_accessor.cc
+++ b/pw_software_update/manifest_accessor.cc
@@ -23,13 +23,13 @@ ManifestAccessor ManifestAccessor::FromBundle(protobuf::Message bundle) {
protobuf::Message targets_metadata =
bundle
.AsStringToMessageMap(static_cast<uint32_t>(
- UpdateBundle::Fields::TARGETS_METADATA))[kTopLevelTargetsName]
+ UpdateBundle::Fields::kTargetsMetadata))[kTopLevelTargetsName]
.AsMessage(static_cast<uint32_t>(
- SignedTargetsMetadata::Fields::SERIALIZED_TARGETS_METADATA));
+ SignedTargetsMetadata::Fields::kSerializedTargetsMetadata));
protobuf::Bytes user_manifest =
bundle.AsStringToBytesMap(static_cast<uint32_t>(
- UpdateBundle::Fields::TARGET_PAYLOADS))[kUserManifestTargetFileName];
+ UpdateBundle::Fields::kTargetPayloads))[kUserManifestTargetFileName];
return ManifestAccessor(targets_metadata, user_manifest);
}
@@ -37,10 +37,10 @@ ManifestAccessor ManifestAccessor::FromBundle(protobuf::Message bundle) {
ManifestAccessor ManifestAccessor::FromManifest(protobuf::Message manifest) {
protobuf::Message targets_metadata =
manifest.AsStringToMessageMap(static_cast<uint32_t>(
- Manifest::Fields::TARGETS_METADATA))[kTopLevelTargetsName];
+ Manifest::Fields::kTargetsMetadata))[kTopLevelTargetsName];
protobuf::Bytes user_manifest =
- manifest.AsBytes(static_cast<uint32_t>(Manifest::Fields::USER_MANIFEST));
+ manifest.AsBytes(static_cast<uint32_t>(Manifest::Fields::kUserManifest));
return ManifestAccessor(targets_metadata, user_manifest);
}
@@ -48,28 +48,27 @@ ManifestAccessor ManifestAccessor::FromManifest(protobuf::Message manifest) {
protobuf::RepeatedMessages ManifestAccessor::GetTargetFiles() {
PW_TRY(status());
return targets_metadata_.AsRepeatedMessages(
- static_cast<uint32_t>(TargetsMetadata::Fields::TARGET_FILES));
+ static_cast<uint32_t>(TargetsMetadata::Fields::kTargetFiles));
}
protobuf::Uint32 ManifestAccessor::GetVersion() {
PW_TRY(status());
return targets_metadata_
.AsMessage(
- static_cast<uint32_t>(TargetsMetadata::Fields::COMMON_METADATA))
- .AsUint32(static_cast<uint32_t>(CommonMetadata::Fields::VERSION));
+ static_cast<uint32_t>(TargetsMetadata::Fields::kCommonMetadata))
+ .AsUint32(static_cast<uint32_t>(CommonMetadata::Fields::kVersion));
}
Status ManifestAccessor::Export(stream::Writer& writer) {
PW_TRY(status());
// Write out the targets metadata map.
- stream::MemoryReader name_reader(
- std::as_bytes(std::span(kTopLevelTargetsName)));
+ stream::MemoryReader name_reader(as_bytes(span(kTopLevelTargetsName)));
stream::IntervalReader metadata_reader =
targets_metadata_.ToBytes().GetBytesReader();
std::byte stream_pipe_buffer[WRITE_MANIFEST_STREAM_PIPE_BUFFER_SIZE];
PW_TRY(protobuf::WriteProtoStringToBytesMapEntry(
- static_cast<uint32_t>(Manifest::Fields::TARGETS_METADATA),
+ static_cast<uint32_t>(Manifest::Fields::kTargetsMetadata),
name_reader,
kTopLevelTargetsName.size(),
metadata_reader,
@@ -82,7 +81,7 @@ Status ManifestAccessor::Export(stream::Writer& writer) {
if (user_manifest_reader.ok()) {
protobuf::StreamEncoder encoder(writer, {});
PW_TRY(encoder.WriteBytesFromStream(
- static_cast<uint32_t>(Manifest::Fields::USER_MANIFEST),
+ static_cast<uint32_t>(Manifest::Fields::kUserManifest),
user_manifest_reader,
user_manifest_reader.interval_size(),
stream_pipe_buffer));
@@ -118,7 +117,7 @@ protobuf::Message ManifestAccessor::GetTargetFile(std::string_view name) {
for (protobuf::Message target_file : GetTargetFiles()) {
protobuf::String target_name = target_file.AsString(
- static_cast<uint32_t>(TargetFile::Fields::FILE_NAME));
+ static_cast<uint32_t>(TargetFile::Fields::kFileName));
Result<bool> compare_result = target_name.Equal(name);
PW_TRY(compare_result.status());
if (compare_result.value()) {
diff --git a/pw_software_update/public/pw_software_update/blob_store_openable_reader.h b/pw_software_update/public/pw_software_update/blob_store_openable_reader.h
new file mode 100644
index 000000000..d6e14cf63
--- /dev/null
+++ b/pw_software_update/public/pw_software_update/blob_store_openable_reader.h
@@ -0,0 +1,38 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_blob_store/blob_store.h"
+#include "pw_software_update/openable_reader.h"
+#include "pw_status/status.h"
+#include "pw_stream/stream.h"
+
+namespace pw::software_update {
+
+class BlobStoreOpenableReader final : public OpenableReader {
+ public:
+ explicit constexpr BlobStoreOpenableReader(blob_store::BlobStore& blob_store)
+ : blob_store_(blob_store), blob_reader_(blob_store_) {}
+
+ Status Open() override { return blob_reader_.Open(); }
+ Status Close() override { return blob_reader_.Close(); }
+ bool IsOpen() override { return blob_reader_.IsOpen(); }
+ stream::SeekableReader& reader() override { return blob_reader_; }
+
+ private:
+ blob_store::BlobStore& blob_store_;
+ blob_store::BlobStore::BlobReader blob_reader_;
+};
+
+} // namespace pw::software_update
diff --git a/pw_software_update/public/pw_software_update/bundled_update_backend.h b/pw_software_update/public/pw_software_update/bundled_update_backend.h
index ae80e065e..c6685d5ac 100644
--- a/pw_software_update/public/pw_software_update/bundled_update_backend.h
+++ b/pw_software_update/public/pw_software_update/bundled_update_backend.h
@@ -25,7 +25,7 @@
namespace pw::software_update {
-// TODO(pwbug/478): update documentation for backend api contract
+// TODO(b/235273688): update documentation for backend api contract
class BundledUpdateBackend {
public:
virtual ~BundledUpdateBackend() = default;
@@ -42,10 +42,10 @@ class BundledUpdateBackend {
// It is safe to assume the target's payload has passed standard
// verification.
return OkStatus();
- };
+ }
// Perform any product-specific tasks needed before starting update sequence.
- virtual Status BeforeUpdateStart() { return OkStatus(); };
+ virtual Status BeforeUpdateStart() { return OkStatus(); }
// Attempts to enable the transfer service transfer handler, returning the
// transfer_id if successful. This is invoked after BeforeUpdateStart();
@@ -59,26 +59,26 @@ class BundledUpdateBackend {
// Perform any product-specific abort tasks before marking the update as
// aborted in bundled updater. This should set any downstream state to a
// default no-update-pending state.
- // TODO: Revisit invariants; should this instead be "Abort()"? This is called
- // for all error paths in the service and needs to reset. Furthermore, should
- // this be async?
- virtual Status BeforeUpdateAbort() { return OkStatus(); };
+ // TODO(keir): Revisit invariants; should this instead be "Abort()"? This is
+ // called for all error paths in the service and needs to reset. Furthermore,
+ // should this be async?
+ virtual Status BeforeUpdateAbort() { return OkStatus(); }
// Perform any product-specific tasks needed before starting verification.
- virtual Status BeforeBundleVerify() { return OkStatus(); };
+ virtual Status BeforeBundleVerify() { return OkStatus(); }
// Perform any product-specific bundle verification tasks (e.g. hw version
// match check), done after TUF bundle verification process.
virtual Status VerifyManifest(
[[maybe_unused]] ManifestAccessor manifest_accessor) {
return OkStatus();
- };
+ }
// Perform product-specific tasks after all bundle verifications are complete.
- virtual Status AfterBundleVerified() { return OkStatus(); };
+ virtual Status AfterBundleVerified() { return OkStatus(); }
// Perform any product-specific tasks before apply sequence started
- virtual Status BeforeApply() { return OkStatus(); };
+ virtual Status BeforeApply() { return OkStatus(); }
// Get status information from update backend. This will not be called when
// BundledUpdater is in a step where it has entire control with no operation
@@ -214,19 +214,16 @@ class BundledUpdateBackend {
// failures.
virtual Result<stream::SeekableReader*> GetRootMetadataReader() {
return Status::Unimplemented();
- };
+ }
// Write a given root metadata to persistent storage in a failsafe manner.
//
// The updating must be atomic/fail-safe. An invalid or corrupted root
// metadata will result in permanent OTA failures.
- //
- // TODO(pwbug/456): Investigate whether we should get a writer i.e.
- // GetRootMetadataWriter() instead of passing a reader.
virtual Status SafelyPersistRootMetadata(
[[maybe_unused]] stream::IntervalReader root_metadata) {
return Status::Unimplemented();
- };
+ }
};
} // namespace pw::software_update
diff --git a/pw_software_update/public/pw_software_update/bundled_update_service.h b/pw_software_update/public/pw_software_update/bundled_update_service.h
index d517d2203..1c9dbd596 100644
--- a/pw_software_update/public/pw_software_update/bundled_update_service.h
+++ b/pw_software_update/public/pw_software_update/bundled_update_service.h
@@ -62,7 +62,7 @@ class BundledUpdateService
pw_software_update_BundledUpdateStatus& response);
// Currently sync, should be async.
- // TODO: Make this async to support aborting verify/apply.
+ // TODO(keir): Make this async to support aborting verify/apply.
Status Abort(const pw_protobuf_Empty& request,
pw_software_update_BundledUpdateStatus& response);
@@ -81,24 +81,22 @@ class BundledUpdateService
// TODO(davidrogers) Add a MaybeFinishApply() method that is called after
// reboot to finish any need apply and verify work.
- // TODO:
- // VerifyProgress - to update % complete.
- // ApplyProgress - to update % complete.
+ // TODO(keir): VerifyProgress - to update % complete.
+ // TODO(keir): ApplyProgress - to update % complete.
private:
// Top-level lock for OTA state coherency. May be held for extended periods.
sync::Mutex mutex_;
- BundledUpdateBackend& backend_ PW_GUARDED_BY(mutex_);
- UpdateBundleAccessor& bundle_ PW_GUARDED_BY(mutex_);
- bool bundle_open_ PW_GUARDED_BY(mutex_);
- work_queue::WorkQueue& work_queue_ PW_GUARDED_BY(mutex_);
- bool work_enqueued_ PW_GUARDED_BY(mutex_);
-
// Nested lock for safe status updates and queries.
sync::Mutex status_mutex_ PW_ACQUIRED_AFTER(mutex_);
pw_software_update_BundledUpdateStatus unsafe_status_
PW_GUARDED_BY(status_mutex_);
sync::Borrowable<pw_software_update_BundledUpdateStatus, sync::Mutex> status_;
+ BundledUpdateBackend& backend_ PW_GUARDED_BY(mutex_);
+ UpdateBundleAccessor& bundle_ PW_GUARDED_BY(mutex_);
+ bool bundle_open_ PW_GUARDED_BY(mutex_);
+ work_queue::WorkQueue& work_queue_ PW_GUARDED_BY(mutex_);
+ bool work_enqueued_ PW_GUARDED_BY(mutex_);
void DoVerify() PW_LOCKS_EXCLUDED(status_mutex_);
void DoApply() PW_LOCKS_EXCLUDED(status_mutex_);
diff --git a/pw_software_update/public/pw_software_update/bundled_update_service_pwpb.h b/pw_software_update/public/pw_software_update/bundled_update_service_pwpb.h
new file mode 100644
index 000000000..27af2ad3a
--- /dev/null
+++ b/pw_software_update/public/pw_software_update/bundled_update_service_pwpb.h
@@ -0,0 +1,112 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include "pw_software_update/bundled_update.pwpb.h"
+#include "pw_software_update/bundled_update.rpc.pwpb.h"
+#include "pw_software_update/bundled_update_backend.h"
+#include "pw_software_update/update_bundle_accessor.h"
+#include "pw_status/status.h"
+#include "pw_sync/borrow.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+#include "pw_work_queue/work_queue.h"
+
+namespace pw::software_update {
+
+// Implementation class for pw.software_update.BundledUpdate.
+// See bundled_update.proto for RPC method documentation.
+class BundledUpdateService
+ : public pw_rpc::pwpb::BundledUpdate::Service<BundledUpdateService> {
+ public:
+ PW_MODIFY_DIAGNOSTICS_PUSH();
+ PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
+ BundledUpdateService(UpdateBundleAccessor& bundle,
+ BundledUpdateBackend& backend,
+ work_queue::WorkQueue& work_queue)
+ : unsafe_status_{.state = BundledUpdateState::Enum::kInactive},
+ status_(unsafe_status_, status_mutex_),
+ backend_(backend),
+ bundle_(bundle),
+ bundle_open_(false),
+ work_queue_(work_queue),
+ work_enqueued_(false) {}
+ PW_MODIFY_DIAGNOSTICS_POP();
+ Status GetStatus(const pw::protobuf::Empty::Message& request,
+ BundledUpdateStatus::Message& response);
+
+ // Sync
+ Status Start(const StartRequest::Message& request,
+ BundledUpdateStatus::Message& response);
+
+ // Sync
+ Status SetTransferred(const pw::protobuf::Empty::Message& request,
+ BundledUpdateStatus::Message& response);
+
+ // Async
+ Status Verify(const pw::protobuf::Empty::Message& request,
+ BundledUpdateStatus::Message& response);
+
+ // Async
+ Status Apply(const pw::protobuf::Empty::Message& request,
+ BundledUpdateStatus::Message& response);
+
+ // Currently sync, should be async.
+ // TODO(elipsitz): Make this async to support aborting verify/apply.
+ Status Abort(const pw::protobuf::Empty::Message& request,
+ BundledUpdateStatus::Message& response);
+
+ // Sync
+ Status Reset(const pw::protobuf::Empty::Message& request,
+ BundledUpdateStatus::Message& response);
+
+ // Notify the service that the bundle transfer has completed. The service has
+ // no way to know when the bundle transfer completes, so users must invoke
+ // this method in their transfer completion handler.
+ //
+ // After this call, the service will be in TRANSFERRED state if and only if
+ // it was in the TRANSFERRING state.
+ void NotifyTransferSucceeded();
+
+ // TODO(davidrogers) Add a MaybeFinishApply() method that is called after
+ // reboot to finish any need apply and verify work.
+
+ // TODO(elipsitz): VerifyProgress - to update % complete.
+ // TODO(elipsitz): ApplyProgress - to update % complete.
+
+ private:
+ // Top-level lock for OTA state coherency. May be held for extended periods.
+ sync::Mutex mutex_;
+ // Nested lock for safe status updates and queries.
+ sync::Mutex status_mutex_ PW_ACQUIRED_AFTER(mutex_);
+ BundledUpdateStatus::Message unsafe_status_ PW_GUARDED_BY(status_mutex_);
+ sync::Borrowable<BundledUpdateStatus::Message, sync::Mutex> status_;
+ BundledUpdateBackend& backend_ PW_GUARDED_BY(mutex_);
+ UpdateBundleAccessor& bundle_ PW_GUARDED_BY(mutex_);
+ bool bundle_open_ PW_GUARDED_BY(mutex_);
+ work_queue::WorkQueue& work_queue_ PW_GUARDED_BY(mutex_);
+ bool work_enqueued_ PW_GUARDED_BY(mutex_);
+
+ void DoVerify() PW_LOCKS_EXCLUDED(status_mutex_);
+ void DoApply() PW_LOCKS_EXCLUDED(status_mutex_);
+ void Finish(BundledUpdateResult::Enum result)
+ PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_) PW_LOCKS_EXCLUDED(status_mutex_);
+ bool IsFinished() PW_EXCLUSIVE_LOCKS_REQUIRED(mutex_)
+ PW_LOCKS_EXCLUDED(status_mutex_) {
+ return status_.acquire()->state == BundledUpdateState::Enum::kFinished;
+ }
+};
+
+} // namespace pw::software_update
diff --git a/pw_software_update/public/pw_software_update/config.h b/pw_software_update/public/pw_software_update/config.h
index 3eaf18c82..2b8f70fb6 100644
--- a/pw_software_update/public/pw_software_update/config.h
+++ b/pw_software_update/public/pw_software_update/config.h
@@ -36,4 +36,20 @@
// incoming bundle in order to improve performance.
#ifndef PW_SOFTWARE_UPDATE_WITH_PERSONALIZATION
#define PW_SOFTWARE_UPDATE_WITH_PERSONALIZATION (true)
-#endif // PW_SOFTWARE_UPDATE_WITH_PERSONALIZATION \ No newline at end of file
+#endif // PW_SOFTWARE_UPDATE_WITH_PERSONALIZATION
+
+// Whether to support root metadata rotation.
+//
+// Root metadata rotation is recommended to mitigate potential signing key
+// vulnerabilities, e.g.:
+// 1. Voluntary refresh of any / all signing keys on a regular schedule -- trust
+// has an expiration date.
+// 2. Revoke compromised keys.
+// 3. Recover from fast-forward attacks.
+//
+// See more rational at: https://theupdateframework.io/security/
+#ifndef PW_SOFTWARE_UPDATE_WITH_ROOT_ROTATION
+// WARNING: (false) case NOT covered by unit tests (nontrivial to add),
+// use at your own risk!
+#define PW_SOFTWARE_UPDATE_WITH_ROOT_ROTATION (true)
+#endif // PW_SOFTWARE_UPDATE_WITH_ROOT_ROTATION
diff --git a/pw_software_update/public/pw_software_update/manifest_accessor.h b/pw_software_update/public/pw_software_update/manifest_accessor.h
index 05a75cc38..40c53766d 100644
--- a/pw_software_update/public/pw_software_update/manifest_accessor.h
+++ b/pw_software_update/public/pw_software_update/manifest_accessor.h
@@ -62,7 +62,7 @@ class ManifestAccessor {
ManifestAccessor(Status status) : targets_metadata_(status) {}
ManifestAccessor(protobuf::Message targets_metadata,
protobuf::Bytes user_manifest)
- : targets_metadata_(targets_metadata), user_manifest_(user_manifest){};
+ : targets_metadata_(targets_metadata), user_manifest_(user_manifest) {}
// Constructs a `ManifestAccessor` from an update bundle.
static ManifestAccessor FromBundle(protobuf::Message bundle);
diff --git a/pw_software_update/public/pw_software_update/openable_reader.h b/pw_software_update/public/pw_software_update/openable_reader.h
new file mode 100644
index 000000000..c1d7d9818
--- /dev/null
+++ b/pw_software_update/public/pw_software_update/openable_reader.h
@@ -0,0 +1,35 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_stream/stream.h"
+
+namespace pw::software_update {
+
+// A SeekableReader that needs Open/Closed state management. This abstraction
+// decouples pw_software_update from pw_blob_store.
+class OpenableReader {
+ public:
+ virtual Status Open() = 0;
+ virtual Status Close() = 0;
+ virtual bool IsOpen() = 0;
+
+ // Returns the underlying SeekableReader. Must only be called after a
+ // successful call to Open, before the matching call to Close.
+ virtual stream::SeekableReader& reader() = 0;
+
+ virtual ~OpenableReader() = default;
+};
+
+} // namespace pw::software_update
diff --git a/pw_software_update/public/pw_software_update/update_bundle_accessor.h b/pw_software_update/public/pw_software_update/update_bundle_accessor.h
index d2b80c081..45dcd6bc7 100644
--- a/pw_software_update/public/pw_software_update/update_bundle_accessor.h
+++ b/pw_software_update/public/pw_software_update/update_bundle_accessor.h
@@ -19,10 +19,13 @@
#include "pw_blob_store/blob_store.h"
#include "pw_protobuf/map_utils.h"
#include "pw_protobuf/message.h"
+#include "pw_software_update/blob_store_openable_reader.h"
#include "pw_software_update/bundled_update_backend.h"
#include "pw_software_update/manifest_accessor.h"
+#include "pw_software_update/openable_reader.h"
namespace pw::software_update {
+
class BundledUpdateBackend;
// Name of the top-level Targets metadata.
@@ -44,16 +47,36 @@ constexpr std::string_view kUserManifestTargetFileName = "user_manifest";
class UpdateBundleAccessor {
public:
// UpdateBundleAccessor
- // blob_store - The staged incoming software update bundle.
+ // update_reader - The staged incoming software update bundle.
// backend - Project-specific BundledUpdateBackend.
- // disable_verification - Disable verification.
+ // self_verification - When set to true, perform a voluntary best effort
+ // verification against available metadata in the incoming bundle itself.
+ // Self verification does NOT use any on-device metadata, thus does not
+ // guard against malicious attacks. Self-verification is primarily meant
+ // to de-risk 0-day verification turn-on.
+ constexpr UpdateBundleAccessor(OpenableReader& update_reader,
+ BundledUpdateBackend& backend,
+ bool self_verification = false)
+ : optional_blob_store_reader_unused_(),
+ update_reader_(update_reader),
+ backend_(backend),
+ self_verification_(self_verification) {}
+
+ // Overloaded constructor to maintain backwards compatibility. This should be
+ // removed once users have migrated.
constexpr UpdateBundleAccessor(blob_store::BlobStore& blob_store,
BundledUpdateBackend& backend,
- bool disable_verification = false)
- : blob_store_(blob_store),
- blob_store_reader_(blob_store_),
+ bool self_verification = false)
+ : optional_blob_store_openeable_reader_(blob_store),
+ update_reader_(optional_blob_store_openeable_reader_),
backend_(backend),
- disable_verification_(disable_verification) {}
+ self_verification_(self_verification) {}
+
+ ~UpdateBundleAccessor() {
+ if (&update_reader_ == &optional_blob_store_openeable_reader_) {
+ optional_blob_store_openeable_reader_.~BlobStoreOpenableReader();
+ }
+ }
// Opens and verifies the software update bundle.
//
@@ -97,7 +120,6 @@ class UpdateBundleAccessor {
//
// Returns:
// FAILED_PRECONDITION - Bundle is not open and verified.
- // TODO(pwbug/456): Add other error codes if necessary.
Status PersistManifest();
// Returns a reader for the (verified) payload bytes of a specified target
@@ -105,7 +127,6 @@ class UpdateBundleAccessor {
//
// Returns:
// A reader instance for the target file.
- // TODO(pwbug/456): Figure out a way to propagate error.
stream::IntervalReader GetTargetPayload(std::string_view target_name);
stream::IntervalReader GetTargetPayload(protobuf::String target_name);
@@ -118,13 +139,20 @@ class UpdateBundleAccessor {
Result<uint64_t> GetTotalPayloadSize();
private:
- blob_store::BlobStore& blob_store_;
- blob_store::BlobStore::BlobReader blob_store_reader_;
+ // Union is a temporary measure to allow for migration from the BlobStore
+ // constructor to the OpenableReader constructor. The BlobStoreOpenableReader
+ // should never be accessed directly. Access it through the update_reader_.
+ union {
+ BlobStoreOpenableReader optional_blob_store_openeable_reader_;
+ char optional_blob_store_reader_unused_;
+ };
+
+ OpenableReader& update_reader_;
BundledUpdateBackend& backend_;
protobuf::Message bundle_;
// The current, cached, trusted `SignedRootMetadata{}`.
protobuf::Message trusted_root_;
- bool disable_verification_;
+ bool self_verification_;
bool bundle_verified_ = false;
// Opens the bundle for read-only access and readies the parser.
diff --git a/pw_software_update/py/BUILD.gn b/pw_software_update/py/BUILD.gn
index 6450989b0..785838b74 100644
--- a/pw_software_update/py/BUILD.gn
+++ b/pw_software_update/py/BUILD.gn
@@ -23,31 +23,43 @@ pw_python_package("py") {
version = "0.0.1"
}
options = {
- install_requires = [ "cryptography" ]
+ install_requires = [
+ "cryptography",
+ "google-cloud-storage",
+ ]
+ tests_require = [
+ "cryptography",
+ "google-cloud-storage",
+ ]
}
}
sources = [
"pw_software_update/__init__.py",
+ "pw_software_update/cli.py",
"pw_software_update/dev_sign.py",
"pw_software_update/generate_test_bundle.py",
"pw_software_update/keys.py",
"pw_software_update/metadata.py",
+ "pw_software_update/remote_sign.py",
"pw_software_update/root_metadata.py",
"pw_software_update/update_bundle.py",
"pw_software_update/verify.py",
]
tests = [
+ "cli_test.py",
"dev_sign_test.py",
"keys_test.py",
"metadata_test.py",
+ "remote_sign_test.py",
"root_metadata_test.py",
"update_bundle_test.py",
"verify_test.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
proto_library = "..:protos"
}
diff --git a/pw_software_update/py/cli_test.py b/pw_software_update/py/cli_test.py
new file mode 100644
index 000000000..9e6a3e937
--- /dev/null
+++ b/pw_software_update/py/cli_test.py
@@ -0,0 +1,104 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Unit tests for pw_software_update/cli.py."""
+
+import unittest
+from pw_software_update import cli, metadata, update_bundle
+from pw_software_update.tuf_pb2 import TargetsMetadata
+
+
+class AddFileToBundleTest(unittest.TestCase):
+ """Test adding a target file to an existing bundle"""
+
+ def test_adding_file_to_bundle(self):
+ """Adds a file to bundle"""
+
+ bundle = update_bundle.gen_empty_update_bundle()
+
+ target_payloads = {
+ 'foo': b'foo contents',
+ 'bar': b'bar contents',
+ }
+
+ for name, contents in target_payloads.items():
+ bundle = cli.add_file_to_bundle(
+ bundle=bundle, file_name=name, file_contents=contents
+ )
+
+ # Checks for existence of target in target payloads
+ self.assertEqual(target_payloads['foo'], bundle.target_payloads['foo'])
+ self.assertEqual(
+ len(bundle.target_payloads['foo']), len(target_payloads['foo'])
+ )
+
+ self.assertEqual(target_payloads['bar'], bundle.target_payloads['bar'])
+ self.assertEqual(
+ len(bundle.target_payloads['bar']), len(target_payloads['bar'])
+ )
+
+ def test_adding_duplicate_file_fails(self):
+ """Test for adding a duplicate target name to bundle"""
+
+ bundle = update_bundle.gen_empty_update_bundle()
+
+ target_payloads = {
+ 'foo': b'foo contents',
+ }
+
+ for name, contents in target_payloads.items():
+ bundle = cli.add_file_to_bundle(
+ bundle=bundle, file_name=name, file_contents=contents
+ )
+
+ # Checks for raised exceptions when adding a duplicate file name
+ # in target payload
+ with self.assertRaises(Exception):
+ bundle = cli.add_file_to_bundle(
+ bundle=bundle, file_name='foo', file_contents=b'does not matter'
+ )
+
+ def test_adding_duplicate_target_file_fails(self):
+ """Test for adding a duplicate target file to bundle"""
+
+ bundle = update_bundle.gen_empty_update_bundle()
+
+ target_payloads = {'foo': b'asrgfdasrgfdasrgfdasrgfd'}
+
+ for name, contents in target_payloads.items():
+ bundle = cli.add_file_to_bundle(
+ bundle=bundle, file_name=name, file_contents=contents
+ )
+
+ # Adding a target file with no matching target_payload file name
+ target_file = metadata.gen_target_file('shoo', b'cvbfbzbz')
+
+ signed_targets_metadata = bundle.targets_metadata['targets']
+ targets_metadata = TargetsMetadata().FromString(
+ signed_targets_metadata.serialized_targets_metadata
+ )
+
+ targets_metadata.target_files.append(target_file)
+ bundle.targets_metadata[
+ 'targets'
+ ].serialized_targets_metadata = targets_metadata.SerializeToString()
+
+ # Checks for raised exception for duplicate target file
+ with self.assertRaises(Exception):
+ bundle = cli.add_file_to_bundle(
+ bundle=bundle, file_name='shoo', file_contents=b'cvbfbzbz'
+ )
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_software_update/py/dev_sign_test.py b/pw_software_update/py/dev_sign_test.py
index 514954254..0b1dc5298 100644
--- a/pw_software_update/py/dev_sign_test.py
+++ b/pw_software_update/py/dev_sign_test.py
@@ -16,46 +16,56 @@
import unittest
from pw_software_update.dev_sign import sign_root_metadata, sign_update_bundle
-from pw_software_update.root_metadata import (gen_root_metadata, RootKeys,
- TargetsKeys)
+from pw_software_update.root_metadata import (
+ gen_root_metadata,
+ RootKeys,
+ TargetsKeys,
+)
from pw_software_update.tuf_pb2 import SignedRootMetadata, SignedTargetsMetadata
from pw_software_update.update_bundle_pb2 import UpdateBundle
class RootMetadataSigningTest(unittest.TestCase):
"""Test Root Metadata signing."""
+
def setUp(self) -> None:
self.root_key = (
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyk3DEQdl346MS5N/'
b'quNEneJa4HxkJBETGzlEEKkCmZOhRANCAAThdY5PejbtM2p6HtgXs/7YSsvPMWZz'
b'9Ui1gAEKrDseHnPzC02MbKjQadRIFZ4hKDcsyz9aM6QKLCNrCOqYjw6t'
- b'\n-----END PRIVATE KEY-----\n')
+ b'\n-----END PRIVATE KEY-----\n'
+ )
self.root_key_public = (
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4XWOT3o27TNqeh7YF7P+2ErLzzFm'
b'c/VItYABCqw7Hh5z8wtNjGyo0GnUSBWeISg3LMs/WjOkCiwjawjqmI8OrQ=='
- b'\n-----END PUBLIC KEY-----\n')
+ b'\n-----END PUBLIC KEY-----\n'
+ )
self.targets_key_public = (
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9UM6qRZJ0gIWwLjo8tjbrrBTlKXg'
b'ukwVjOlnguSSiYMrN4MDqMlNDnaJgLvcCuiNUKHu9Oj1DG1i6ckNdE4VTA=='
- b'\n-----END PUBLIC KEY-----\n')
+ b'\n-----END PUBLIC KEY-----\n'
+ )
root_metadata = gen_root_metadata(
RootKeys([self.root_key_public]),
- TargetsKeys([self.targets_key_public]))
+ TargetsKeys([self.targets_key_public]),
+ )
self.root_metadata = SignedRootMetadata(
- serialized_root_metadata=root_metadata.SerializeToString())
+ serialized_root_metadata=root_metadata.SerializeToString()
+ )
self.another_signing_key = (
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5OIalt8DcZYeEf/4'
b'5/iIX6jqM0I5t4dScAdcmgNF9vKhRANCAAQdMBqcn//pXIwss9nLEVjz+4Mz4oVt'
b'hKTFLqlzwKHZngL/0IyH6FC+4bq5CnR43WADPAyJHo18V0NCO/8QFQ2c'
- b'\n-----END PRIVATE KEY-----\n')
+ b'\n-----END PRIVATE KEY-----\n'
+ )
def test_typical_signing(self):
signed = sign_root_metadata(self.root_metadata, self.root_key)
@@ -66,17 +76,23 @@ class RootMetadataSigningTest(unittest.TestCase):
class BundleSigningTest(unittest.TestCase):
"""Test UpdateBundle signing."""
+
def setUp(self):
self.targets_key = (
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkMEZ0u84HzC51nhh'
b'f2ZykPj6WfAjBxXVWndjVdn6bh6hRANCAAT1QzqpFknSAhbAuOjy2NuusFOUpeC6'
b'TBWM6WeC5JKJgys3gwOoyU0OdomAu9wK6I1Qoe706PUMbWLpyQ10ThVM'
- b'\n-----END PRIVATE KEY-----\n')
+ b'\n-----END PRIVATE KEY-----\n'
+ )
- self.update_bundle = UpdateBundle(targets_metadata=dict(
- targets=SignedTargetsMetadata(
- serialized_targets_metadata=b'blahblah')))
+ self.update_bundle = UpdateBundle(
+ targets_metadata=dict(
+ targets=SignedTargetsMetadata(
+ serialized_targets_metadata=b'blahblah'
+ )
+ )
+ )
def test_typical_signing(self):
signed = sign_update_bundle(self.update_bundle, self.targets_key)
diff --git a/pw_software_update/py/keys_test.py b/pw_software_update/py/keys_test.py
index 214b62d74..3e318d34e 100644
--- a/pw_software_update/py/keys_test.py
+++ b/pw_software_update/py/keys_test.py
@@ -23,63 +23,83 @@ from pw_software_update.tuf_pb2 import Key, KeyType, KeyScheme
class KeyGenTest(unittest.TestCase):
"""Test the generation of keys."""
+
def test_ecdsa_keygen(self):
"""Test ECDSA key generation."""
with tempfile.TemporaryDirectory() as tempdir_name:
temp_root = Path(tempdir_name)
- private_key_filename = (temp_root / 'test_key')
- public_key_filename = (temp_root / 'test_key.pub')
+ private_key_filename = temp_root / 'test_key'
+ public_key_filename = temp_root / 'test_key.pub'
keys.gen_ecdsa_keypair(private_key_filename)
self.assertTrue(private_key_filename.exists())
self.assertTrue(public_key_filename.exists())
public_key = keys.import_ecdsa_public_key(
- public_key_filename.read_bytes())
- self.assertEqual(public_key.key.key_type,
- KeyType.ECDSA_SHA2_NISTP256)
- self.assertEqual(public_key.key.scheme,
- KeyScheme.ECDSA_SHA2_NISTP256_SCHEME)
+ public_key_filename.read_bytes()
+ )
+ self.assertEqual(
+ public_key.key.key_type, KeyType.ECDSA_SHA2_NISTP256
+ )
+ self.assertEqual(
+ public_key.key.scheme, KeyScheme.ECDSA_SHA2_NISTP256_SCHEME
+ )
class KeyIdTest(unittest.TestCase):
- """Test Key ID generations """
+ """Test Key ID generations"""
+
def test_256bit_length(self):
key_id = keys.gen_key_id(
- Key(key_type=KeyType.ECDSA_SHA2_NISTP256,
+ Key(
+ key_type=KeyType.ECDSA_SHA2_NISTP256,
scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
- keyval=b'public_key bytes'))
+ keyval=b'public_key bytes',
+ )
+ )
self.assertEqual(len(key_id), 32)
def test_different_keyval(self):
- key1 = Key(key_type=KeyType.ECDSA_SHA2_NISTP256,
- scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
- keyval=b'key 1 bytes')
- key2 = Key(key_type=KeyType.ECDSA_SHA2_NISTP256,
- scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
- keyval=b'key 2 bytes')
+ key1 = Key(
+ key_type=KeyType.ECDSA_SHA2_NISTP256,
+ scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
+ keyval=b'key 1 bytes',
+ )
+ key2 = Key(
+ key_type=KeyType.ECDSA_SHA2_NISTP256,
+ scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
+ keyval=b'key 2 bytes',
+ )
key1_id, key2_id = keys.gen_key_id(key1), keys.gen_key_id(key2)
self.assertNotEqual(key1_id, key2_id)
def test_different_key_type(self):
- key1 = Key(key_type=KeyType.RSA,
- scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
- keyval=b'key bytes')
- key2 = Key(key_type=KeyType.ECDSA_SHA2_NISTP256,
- scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
- keyval=b'key bytes')
+ key1 = Key(
+ key_type=KeyType.RSA,
+ scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
+ keyval=b'key bytes',
+ )
+ key2 = Key(
+ key_type=KeyType.ECDSA_SHA2_NISTP256,
+ scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
+ keyval=b'key bytes',
+ )
key1_id, key2_id = keys.gen_key_id(key1), keys.gen_key_id(key2)
self.assertNotEqual(key1_id, key2_id)
def test_different_scheme(self):
- key1 = Key(key_type=KeyType.ECDSA_SHA2_NISTP256,
- scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
- keyval=b'key bytes')
- key2 = Key(key_type=KeyType.ECDSA_SHA2_NISTP256,
- scheme=KeyScheme.ED25519_SCHEME,
- keyval=b'key bytes')
+ key1 = Key(
+ key_type=KeyType.ECDSA_SHA2_NISTP256,
+ scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
+ keyval=b'key bytes',
+ )
+ key2 = Key(
+ key_type=KeyType.ECDSA_SHA2_NISTP256,
+ scheme=KeyScheme.ED25519_SCHEME,
+ keyval=b'key bytes',
+ )
key1_id, key2_id = keys.gen_key_id(key1), keys.gen_key_id(key2)
self.assertNotEqual(key1_id, key2_id)
@@ -87,6 +107,7 @@ class KeyIdTest(unittest.TestCase):
class KeyImportTest(unittest.TestCase):
"""Test key importing"""
+
def setUp(self):
# Generated with:
# $> openssl ecparam -name prime256v1 -genkey -noout -out priv.pem
@@ -96,7 +117,8 @@ class KeyImportTest(unittest.TestCase):
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKmK5mJwMV7eimA6MfFQL2q6KbZDr'
b'SnWwoeHvXB/aZBnwF422OLifuOuMjEUEHrNMmoekcua+ulHW41X3AgbvIw==\n'
- b'-----END PUBLIC KEY-----\n')
+ b'-----END PUBLIC KEY-----\n'
+ )
# Generated with:
# $> openssl ecparam -name secp384r1 -genkey -noout -out priv.pem
@@ -107,14 +129,16 @@ class KeyImportTest(unittest.TestCase):
b'MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE6xs+TEjb2/vIzs4AzSm2CSUWpJMCPAts'
b'e+gwvGwFrr2bXKHVLNCxr5/Va6rD0nDmB2NOiJwAXX1Z8CB5wqLLB31emCBFRb5i'
b'1LjZu8Bp3hrWOL7uvXer8uExnSfTKAoT\n'
- b'-----END PUBLIC KEY-----\n')
+ b'-----END PUBLIC KEY-----\n'
+ )
# Replaces "MF" with "MM"
self.tampered_nistp256_pem_bytes = (
b'-----BEGIN PUBLIC KEY-----\n'
b'MMkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKmK5mJwMV7eimA6MfFQL2q6KbZDr'
b'SnWwoeHvXB/aZBnwF422OLifuOuMjEUEHrNMmoekcua+ulHW41X3AgbvIw==\n'
- b'-----END PUBLIC KEY-----\n')
+ b'-----END PUBLIC KEY-----\n'
+ )
self.rsa_2048_pem_bytes = (
b'-----BEGIN PUBLIC KEY-----\n'
@@ -125,7 +149,8 @@ class KeyImportTest(unittest.TestCase):
b'd/Jv8GfZL/ykZstP6Ow1/ByP1ZKvrZvg2iXjC686hZXiMJLqmp0sIqLire82oW+8'
b'XFc1uyr1j20m+NI5Siy0G3RbfPXrVKyXIgAYPW12+a/BXR9SrqYJYcWwuOGbHZCM'
b'pwIDAQAB\n'
- b'-----END PUBLIC KEY-----\n')
+ b'-----END PUBLIC KEY-----\n'
+ )
def test_valid_nistp256_key(self):
keys.import_ecdsa_public_key(self.valid_nistp256_pem_bytes)
@@ -145,6 +170,7 @@ class KeyImportTest(unittest.TestCase):
class SignatureVerificationTest(unittest.TestCase):
"""ECDSA signing and verification test."""
+
def setUp(self):
# Generated with:
# $> openssl ecparam -name prime256v1 -genkey -noout -out priv.pem
@@ -155,12 +181,14 @@ class SignatureVerificationTest(unittest.TestCase):
b'MHcCAQEEIH9u1n4qAT59f7KRRl/ZB0Y/BUfS4blba+LONlF4s3ltoAoGCCqGSM49'
b'AwEHoUQDQgAEgKf3kY9Hi3hxIyqm2EkfqQvJkCijjlJSmEAJ1oAp0Godi5x2af+m'
b'cSNuBjpRcC8iW8x1/gizqyWlfAVrZV0XdA==\n'
- b'-----END EC PRIVATE KEY-----\n')
+ b'-----END EC PRIVATE KEY-----\n'
+ )
self.public_key_pem = (
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgKf3kY9Hi3hxIyqm2EkfqQvJkCij'
b'jlJSmEAJ1oAp0Godi5x2af+mcSNuBjpRcC8iW8x1/gizqyWlfAVrZV0XdA==\n'
- b'-----END PUBLIC KEY-----\n')
+ b'-----END PUBLIC KEY-----\n'
+ )
self.message = b'Hello Pigweed!'
self.tampered_message = b'Hell0 Pigweed!'
@@ -169,15 +197,21 @@ class SignatureVerificationTest(unittest.TestCase):
sig = keys.create_ecdsa_signature(self.message, self.private_key_pem)
self.assertTrue(
keys.verify_ecdsa_signature(
- sig.sig, self.message,
- keys.import_ecdsa_public_key(self.public_key_pem).key))
+ sig.sig,
+ self.message,
+ keys.import_ecdsa_public_key(self.public_key_pem).key,
+ )
+ )
def test_tampered_message(self):
sig = keys.create_ecdsa_signature(self.message, self.private_key_pem)
self.assertFalse(
keys.verify_ecdsa_signature(
- sig.sig, self.tampered_message,
- keys.import_ecdsa_public_key(self.public_key_pem).key))
+ sig.sig,
+ self.tampered_message,
+ keys.import_ecdsa_public_key(self.public_key_pem).key,
+ )
+ )
def test_tampered_signature(self):
sig = keys.create_ecdsa_signature(self.message, self.private_key_pem)
@@ -185,8 +219,11 @@ class SignatureVerificationTest(unittest.TestCase):
tampered_sig[0] ^= 1
self.assertFalse(
keys.verify_ecdsa_signature(
- tampered_sig, self.message,
- keys.import_ecdsa_public_key(self.public_key_pem).key))
+ tampered_sig,
+ self.message,
+ keys.import_ecdsa_public_key(self.public_key_pem).key,
+ )
+ )
if __name__ == '__main__':
diff --git a/pw_software_update/py/metadata_test.py b/pw_software_update/py/metadata_test.py
index fd54b12a2..210a9bf9f 100644
--- a/pw_software_update/py/metadata_test.py
+++ b/pw_software_update/py/metadata_test.py
@@ -21,6 +21,7 @@ from pw_software_update.tuf_pb2 import HashFunction
class GenTargetsMetadataTest(unittest.TestCase):
"""Test the generation of targets metadata."""
+
def test_multiple_targets(self):
"""Checks that multiple targets generates multiple TargetFiles."""
target_payloads = {
@@ -28,22 +29,27 @@ class GenTargetsMetadataTest(unittest.TestCase):
'bar': b'\x12\x34',
}
targets_metadata = metadata.gen_targets_metadata(
- target_payloads, (HashFunction.SHA256, ), version=42)
+ target_payloads, (HashFunction.SHA256,), version=42
+ )
self.assertEqual(2, len(targets_metadata.target_files))
- self.assertEqual(metadata.RoleType.TARGETS.value,
- targets_metadata.common_metadata.role)
+ self.assertEqual(
+ metadata.RoleType.TARGETS.value,
+ targets_metadata.common_metadata.role,
+ )
self.assertEqual(42, targets_metadata.common_metadata.version)
class GenHashesTest(unittest.TestCase):
"""Test the generation of hashes."""
+
def test_sha256(self):
"""Checks that SHA256 hashes are computed and stored properly."""
data = b'\x1e\xe7'
- sha256_hash = metadata.gen_hashes(data, (HashFunction.SHA256, ))[0]
+ sha256_hash = metadata.gen_hashes(data, (HashFunction.SHA256,))[0]
self.assertEqual(
'9f36ce605a3b28110d2a25ec36bdfff86059086cbd53c9efc1428ef01070515d',
- sha256_hash.hash.hex())
+ sha256_hash.hash.hex(),
+ )
if __name__ == '__main__':
diff --git a/pw_software_update/py/pw_software_update/cli.py b/pw_software_update/py/pw_software_update/cli.py
new file mode 100644
index 000000000..a099beff5
--- /dev/null
+++ b/pw_software_update/py/pw_software_update/cli.py
@@ -0,0 +1,554 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""
+Software update related operations.
+
+Learn more at: pigweed.dev/pw_software_update
+
+"""
+
+import argparse
+import os
+import sys
+from pathlib import Path
+
+from pw_software_update import (
+ dev_sign,
+ keys,
+ metadata,
+ root_metadata,
+ update_bundle,
+)
+from pw_software_update.tuf_pb2 import (
+ RootMetadata,
+ SignedRootMetadata,
+ TargetsMetadata,
+)
+from pw_software_update.update_bundle_pb2 import UpdateBundle
+
+
+def inspect_bundle_handler(arg) -> None:
+ """Prints bundle contents."""
+
+ try:
+ bundle = UpdateBundle.FromString(arg.pathname.read_bytes())
+ signed_targets_metadata = bundle.targets_metadata['targets']
+ targets_metadata = TargetsMetadata().FromString(
+ signed_targets_metadata.serialized_targets_metadata
+ )
+ print('Targets Metadata:')
+ print('=================')
+ print(targets_metadata)
+
+ print('\nTarget Files:')
+ print('=============')
+ for i, (name, contents) in enumerate(bundle.target_payloads.items()):
+ print(f'{i+1} of {len(bundle.target_payloads)}:')
+ print(f' filename: {name}')
+ print(f' length: {len(contents)}')
+
+ first_32_bytes = contents[:32]
+ print(f' ascii contents(first 32 bytes): {first_32_bytes!r}')
+ print(f' hex contents(first 32 bytes): {first_32_bytes.hex()}\n')
+
+ signed_root_metadata = bundle.root_metadata
+ deserialized_root_metadata = RootMetadata.FromString(
+ signed_root_metadata.serialized_root_metadata
+ )
+ print('\nRoot Metadata:')
+ print('==============')
+ print(deserialized_root_metadata)
+
+ except IOError as error:
+ print(error)
+
+
+def _new_inspect_bundle_parser(subparsers) -> None:
+ """Parser to handle inspect-bundle subcommand"""
+
+ formatter_class = lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=100, width=200
+ )
+ inspect_bundle_parser = subparsers.add_parser(
+ 'inspect-bundle',
+ description='Outputs contents of bundle',
+ formatter_class=formatter_class,
+ help="",
+ )
+
+ inspect_bundle_parser.set_defaults(func=inspect_bundle_handler)
+ inspect_bundle_parser.add_argument(
+ 'pathname', type=Path, help='Path to bundle'
+ )
+
+
+def sign_bundle_handler(arg) -> None:
+ """Handles signing of a bundle"""
+
+ try:
+ signed_bundle = dev_sign.sign_update_bundle(
+ UpdateBundle.FromString(arg.bundle.read_bytes()),
+ arg.key.read_bytes(),
+ )
+ arg.bundle.write_bytes(signed_bundle.SerializeToString())
+
+ except IOError as error:
+ print(error)
+
+
+def _new_sign_bundle_parser(subparsers) -> None:
+ """Parser for sign-bundle subcommand"""
+
+ formatter_class = lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=100, width=200
+ )
+ sign_bundle_parser = subparsers.add_parser(
+ 'sign-bundle',
+ description='Sign an existing bundle using a development key',
+ formatter_class=formatter_class,
+ help="",
+ )
+
+ sign_bundle_parser.set_defaults(func=sign_bundle_handler)
+ required_arguments = sign_bundle_parser.add_argument_group(
+ 'required arguments'
+ )
+ required_arguments.add_argument(
+ '--bundle',
+ help='Bundle to be signed',
+ metavar='BUNDLE',
+ required=True,
+ type=Path,
+ )
+ required_arguments.add_argument(
+ '--key',
+ help='Bundle signing key',
+ metavar='KEY',
+ required=True,
+ type=Path,
+ )
+
+
+def add_file_to_bundle(
+ bundle: UpdateBundle, file_name: str, file_contents: bytes
+) -> UpdateBundle:
+ """Adds a target file represented by file_name and file_contents to an
+ existing UpdateBundle -- bundle and returns the updated UpdateBundle object.
+ """
+
+ if not file_name in bundle.target_payloads:
+ bundle.target_payloads[file_name] = file_contents
+ else:
+ raise Exception(f'File name {file_name} already exists in bundle')
+
+ signed_targets_metadata = bundle.targets_metadata['targets']
+ targets_metadata = TargetsMetadata().FromString(
+ signed_targets_metadata.serialized_targets_metadata
+ )
+
+ matching_file_names = list(
+ filter(
+ lambda name: name.file_name == file_name,
+ targets_metadata.target_files,
+ )
+ )
+
+ target_file = metadata.gen_target_file(file_name, file_contents)
+
+ if not matching_file_names:
+ targets_metadata.target_files.append(target_file)
+ else:
+ raise Exception(f'File name {file_name} already exists in bundle')
+
+ bundle.targets_metadata[
+ 'targets'
+ ].serialized_targets_metadata = targets_metadata.SerializeToString()
+
+ return bundle
+
+
+def add_file_to_bundle_handler(arg) -> None:
+ """Add a new file to an existing bundle. Updates the targets metadata
+ and errors out if the file already exists.
+ """
+
+ try:
+ if not arg.new_name:
+ file_name = os.path.splitext(os.path.basename(arg.file))[0]
+ else:
+ file_name = arg.new_name
+
+ bundle = UpdateBundle().FromString(arg.bundle.read_bytes())
+ updated_bundle = add_file_to_bundle(
+ bundle=bundle,
+ file_name=file_name,
+ file_contents=arg.file.read_bytes(),
+ )
+
+ arg.bundle.write_bytes(updated_bundle.SerializeToString())
+
+ except IOError as error:
+ print(error)
+
+
+def _new_add_file_to_bundle_parser(subparsers) -> None:
+ """Parser for adding file to bundle subcommand"""
+
+ formatter_class = lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=100, width=200
+ )
+ add_file_to_bundle_parser = subparsers.add_parser(
+ 'add-file-to-bundle',
+ description='Add a file to an existing bundle',
+ formatter_class=formatter_class,
+ help="",
+ )
+ add_file_to_bundle_parser.set_defaults(func=add_file_to_bundle_handler)
+ required_arguments = add_file_to_bundle_parser.add_argument_group(
+ 'required arguments'
+ )
+ required_arguments.add_argument(
+ '--bundle',
+ help='Path to an existing bundle',
+ metavar='BUNDLE',
+ required=True,
+ type=Path,
+ )
+ required_arguments.add_argument(
+ '--file',
+ help='Path to a target file',
+ metavar='FILE_PATH',
+ required=True,
+ type=Path,
+ )
+ required_arguments.add_argument(
+ '--new-name',
+ help='Optional new name for target',
+ metavar='NEW_NAME',
+ required=False,
+ type=str,
+ )
+
+
+def add_root_metadata_to_bundle_handler(arg) -> None:
+ """Handles appending of root metadata to a bundle"""
+
+ try:
+ bundle = UpdateBundle().FromString(arg.bundle.read_bytes())
+ bundle.root_metadata.CopyFrom(
+ SignedRootMetadata().FromString(
+ arg.append_root_metadata.read_bytes()
+ )
+ )
+ arg.bundle.write_bytes(bundle.SerializeToString())
+
+ except IOError as error:
+ print(error)
+
+
+def _new_add_root_metadata_to_bundle_parser(subparsers) -> None:
+ """Parser for subcommand adding root metadata to bundle"""
+
+ formatter_class = lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=100, width=200
+ )
+ add_root_metadata_to_bundle_parser = subparsers.add_parser(
+ 'add-root-metadata-to-bundle',
+ description='Add root metadata to a bundle',
+ formatter_class=formatter_class,
+ help="",
+ )
+ add_root_metadata_to_bundle_parser.set_defaults(
+ func=add_root_metadata_to_bundle_handler
+ )
+ required_arguments = add_root_metadata_to_bundle_parser.add_argument_group(
+ 'required arguments'
+ )
+ required_arguments.add_argument(
+ '--append-root-metadata',
+ help='Path to root metadata',
+ metavar='ROOT_METADATA',
+ required=True,
+ type=Path,
+ )
+ required_arguments.add_argument(
+ '--bundle',
+ help='Path to bundle',
+ metavar='BUNDLE',
+ required=True,
+ type=Path,
+ )
+
+
+def create_empty_bundle_handler(arg) -> None:
+ """Handles the creation of an empty bundle and writes it to disc."""
+
+ try:
+ bundle = update_bundle.gen_empty_update_bundle(
+ arg.target_metadata_version
+ )
+ arg.pathname.write_bytes(bundle.SerializeToString())
+
+ except IOError as error:
+ print(error)
+
+
+def _new_create_empty_bundle_parser(subparsers) -> None:
+ """Parser for creation of an empty bundle."""
+
+ formatter_class = lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=100, width=200
+ )
+ create_empty_bundle_parser = subparsers.add_parser(
+ 'create-empty-bundle',
+ description='Creation of an empty bundle',
+ formatter_class=formatter_class,
+ help="",
+ )
+ create_empty_bundle_parser.set_defaults(func=create_empty_bundle_handler)
+ create_empty_bundle_parser.add_argument(
+ 'pathname', type=Path, help='Path to newly created empty bundle'
+ )
+ create_empty_bundle_parser.add_argument(
+ '--target-metadata-version',
+ help='Version number for targets metadata; Defaults to 1',
+ metavar='VERSION',
+ type=int,
+ default=1,
+ required=False,
+ )
+
+
+def inspect_root_metadata_handler(arg) -> None:
+ """Prints root metadata contents as defined by "RootMetadata" message
+ structure in tuf.proto as well as the number of identified signatures.
+ """
+
+ try:
+ signed_root_metadata = SignedRootMetadata.FromString(
+ arg.pathname.read_bytes()
+ )
+
+ deserialized_root_metadata = RootMetadata.FromString(
+ signed_root_metadata.serialized_root_metadata
+ )
+ print(deserialized_root_metadata)
+
+ print(
+ 'Number of signatures found:', len(signed_root_metadata.signatures)
+ )
+
+ except IOError as error:
+ print(error)
+
+
+def _new_inspect_root_metadata_parser(subparsers) -> None:
+ """Parser to handle inspect-root-metadata subcommand"""
+
+ formatter_class = lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=100, width=200
+ )
+ inspect_root_metadata_parser = subparsers.add_parser(
+ 'inspect-root-metadata',
+ description='Outputs contents of root metadata',
+ formatter_class=formatter_class,
+ help="",
+ )
+
+ inspect_root_metadata_parser.set_defaults(
+ func=inspect_root_metadata_handler
+ )
+ inspect_root_metadata_parser.add_argument(
+ 'pathname', type=Path, help='Path to root metadata'
+ )
+
+
+def sign_root_metadata_handler(arg) -> None:
+ """Handler for signing of root metadata"""
+
+ try:
+ signed_root_metadata = dev_sign.sign_root_metadata(
+ SignedRootMetadata.FromString(arg.root_metadata.read_bytes()),
+ arg.root_key.read_bytes(),
+ )
+ arg.root_metadata.write_bytes(signed_root_metadata.SerializeToString())
+
+ except IOError as error:
+ print(error)
+
+
+def _new_sign_root_metadata_parser(subparsers) -> None:
+ """Parser to handle sign-root-metadata subcommand"""
+
+ formatter_class = lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=100, width=200
+ )
+ sign_root_metadata_parser = subparsers.add_parser(
+ 'sign-root-metadata',
+ description='Signing of root metadata',
+ formatter_class=formatter_class,
+ help="",
+ )
+
+ sign_root_metadata_parser.set_defaults(func=sign_root_metadata_handler)
+ required_arguments = sign_root_metadata_parser.add_argument_group(
+ 'required arguments'
+ )
+ required_arguments.add_argument(
+ '--root-metadata',
+ help='Root metadata to be signed',
+ metavar='ROOT_METADATA',
+ required=True,
+ type=Path,
+ )
+ required_arguments.add_argument(
+ '--root-key',
+ help='Root signing key',
+ metavar='ROOT_KEY',
+ required=True,
+ type=Path,
+ )
+
+
+def create_root_metadata_handler(arg) -> None:
+ """Handler function for creation of root metadata."""
+
+ try:
+ root_metadata.main(
+ arg.out, arg.append_root_key, arg.append_targets_key, arg.version
+ )
+
+ # TODO(eashansingh): Print message that allows user
+ # to visualize root metadata with
+ # `pw update inspect-root-metadata` command
+
+ except IOError as error:
+ print(error)
+
+
+def _new_create_root_metadata_parser(subparsers) -> None:
+ """Parser to handle create-root-metadata subcommand."""
+
+ formatter_class = lambda prog: argparse.HelpFormatter(
+ prog, max_help_position=100, width=200
+ )
+ create_root_metadata_parser = subparsers.add_parser(
+ 'create-root-metadata',
+ description='Creation of root metadata',
+ formatter_class=formatter_class,
+ help='',
+ )
+ create_root_metadata_parser.set_defaults(func=create_root_metadata_handler)
+ create_root_metadata_parser.add_argument(
+ '--version',
+ help='Canonical version number for rollback checks; Defaults to 1',
+ type=int,
+ default=1,
+ required=False,
+ )
+
+ required_arguments = create_root_metadata_parser.add_argument_group(
+ 'required arguments'
+ )
+ required_arguments.add_argument(
+ '--append-root-key',
+ help='Path to root key',
+ metavar='ROOT_KEY',
+ required=True,
+ action='append',
+ type=Path,
+ )
+ required_arguments.add_argument(
+ '--append-targets-key',
+ help='Path to targets key',
+ metavar='TARGETS_KEY',
+ required=True,
+ action='append',
+ type=Path,
+ )
+ required_arguments.add_argument(
+ '-o', '--out', help='Path to output file', required=True, type=Path
+ )
+
+
+def generate_key_handler(arg) -> None:
+ """Handler function for key generation"""
+
+ try:
+ keys.gen_ecdsa_keypair(arg.pathname)
+ print('Private key: ' + str(arg.pathname))
+ print('Public key: ' + str(arg.pathname) + '.pub')
+
+ except IOError as error:
+ print(error)
+
+
+def _new_generate_key_parser(subparsers) -> None:
+ """Parser to handle key generation subcommand."""
+
+ generate_key_parser = subparsers.add_parser(
+ 'generate-key',
+ description=(
+ 'Generates an ecdsa-sha2-nistp256 signing key pair '
+ '(private + public)'
+ ),
+ help='',
+ )
+ generate_key_parser.set_defaults(func=generate_key_handler)
+ generate_key_parser.add_argument(
+ 'pathname', type=Path, help='Path to generated key pair'
+ )
+
+
+def _parse_args() -> argparse.Namespace:
+ parser_root = argparse.ArgumentParser(
+ description='Software update related operations.',
+ epilog='Learn more at: pigweed.dev/pw_software_update',
+ )
+ parser_root.set_defaults(
+ func=lambda *_args, **_kwargs: parser_root.print_help()
+ )
+
+ subparsers = parser_root.add_subparsers()
+
+ # Key generation related parsers
+ _new_generate_key_parser(subparsers)
+
+ # Root metadata related parsers
+ _new_create_root_metadata_parser(subparsers)
+ _new_sign_root_metadata_parser(subparsers)
+ _new_inspect_root_metadata_parser(subparsers)
+
+ # Bundle related parsers
+ _new_create_empty_bundle_parser(subparsers)
+ _new_add_root_metadata_to_bundle_parser(subparsers)
+ _new_add_file_to_bundle_parser(subparsers)
+ _new_sign_bundle_parser(subparsers)
+ _new_inspect_bundle_parser(subparsers)
+
+ return parser_root.parse_args()
+
+
+def _dispatch_command(args) -> None:
+ args.func(args)
+
+
+def main() -> int:
+ """Software update command-line interface(WIP)."""
+ _dispatch_command(_parse_args())
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_software_update/py/pw_software_update/dev_sign.py b/pw_software_update/py/pw_software_update/dev_sign.py
index 95e728e24..899fb2dee 100644
--- a/pw_software_update/py/pw_software_update/dev_sign.py
+++ b/pw_software_update/py/pw_software_update/dev_sign.py
@@ -21,8 +21,9 @@ from pw_software_update.tuf_pb2 import SignedRootMetadata
from pw_software_update.update_bundle_pb2 import UpdateBundle
-def sign_root_metadata(root_metadata: SignedRootMetadata,
- root_key_pem: bytes) -> SignedRootMetadata:
+def sign_root_metadata(
+ root_metadata: SignedRootMetadata, root_key_pem: bytes
+) -> SignedRootMetadata:
"""Signs or re-signs a Root Metadata.
Args:
@@ -31,14 +32,16 @@ def sign_root_metadata(root_metadata: SignedRootMetadata,
"""
signature = keys.create_ecdsa_signature(
- root_metadata.serialized_root_metadata, root_key_pem)
+ root_metadata.serialized_root_metadata, root_key_pem
+ )
root_metadata.signatures.append(signature)
return root_metadata
-def sign_update_bundle(bundle: UpdateBundle,
- targets_key_pem: bytes) -> UpdateBundle:
+def sign_update_bundle(
+ bundle: UpdateBundle, targets_key_pem: bytes
+) -> UpdateBundle:
"""Signs or re-signs an update bundle.
Args:
@@ -48,7 +51,9 @@ def sign_update_bundle(bundle: UpdateBundle,
bundle.targets_metadata['targets'].signatures.append(
keys.create_ecdsa_signature(
bundle.targets_metadata['targets'].serialized_targets_metadata,
- targets_key_pem))
+ targets_key_pem,
+ )
+ )
return bundle
@@ -56,33 +61,38 @@ def parse_args():
"""Parse CLI arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--root-metadata',
- type=Path,
- required=False,
- help='Path to the root metadata to be signed')
+ parser.add_argument(
+ '--root-metadata',
+ type=Path,
+ required=False,
+ help='Path to the root metadata to be signed',
+ )
- parser.add_argument('--bundle',
- type=Path,
- required=False,
- help='Path to the bundle to be signed')
+ parser.add_argument(
+ '--bundle',
+ type=Path,
+ required=False,
+ help='Path to the bundle to be signed',
+ )
parser.add_argument(
'--output',
type=Path,
required=False,
- help=('Path to save the signed root metadata or bundle '
- 'to; Defaults to the input path if unspecified'))
+ help=(
+ 'Path to save the signed root metadata or bundle '
+ 'to; Defaults to the input path if unspecified'
+ ),
+ )
- parser.add_argument('--key',
- type=Path,
- required=True,
- help='Path to the signing key')
+ parser.add_argument(
+ '--key', type=Path, required=True, help='Path to the signing key'
+ )
args = parser.parse_args()
if not (args.root_metadata or args.bundle):
- parser.error(
- 'either "--root-metadata" or "--bundle" must be specified')
+ parser.error('either "--root-metadata" or "--bundle" must be specified')
if args.root_metadata and args.bundle:
parser.error('"--root-metadata" and "--bundle" are mutually exclusive')
@@ -94,14 +104,16 @@ def main(root_metadata: Path, bundle: Path, key: Path, output: Path) -> None:
if root_metadata:
signed_root_metadata = sign_root_metadata(
SignedRootMetadata.FromString(root_metadata.read_bytes()),
- key.read_bytes())
+ key.read_bytes(),
+ )
if not output:
output = root_metadata
output.write_bytes(signed_root_metadata.SerializeToString())
else:
signed_bundle = sign_update_bundle(
- UpdateBundle.FromString(bundle.read_bytes()), key.read_bytes())
+ UpdateBundle.FromString(bundle.read_bytes()), key.read_bytes()
+ )
if not output:
output = bundle
diff --git a/pw_software_update/py/pw_software_update/generate_test_bundle.py b/pw_software_update/py/pw_software_update/generate_test_bundle.py
index 304e0bdc5..14409ae7d 100644
--- a/pw_software_update/py/pw_software_update/generate_test_bundle.py
+++ b/pw_software_update/py/pw_software_update/generate_test_bundle.py
@@ -16,12 +16,16 @@
import argparse
import subprocess
import sys
-from typing import Dict
+from typing import Dict, Optional
from pw_software_update import dev_sign, keys, metadata, root_metadata
from pw_software_update.update_bundle_pb2 import Manifest, UpdateBundle
-from pw_software_update.tuf_pb2 import (RootMetadata, SignedRootMetadata,
- TargetsMetadata, SignedTargetsMetadata)
+from pw_software_update.tuf_pb2 import (
+ RootMetadata,
+ SignedRootMetadata,
+ TargetsMetadata,
+ SignedTargetsMetadata,
+)
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import serialization
@@ -99,27 +103,35 @@ def private_key_public_pem_bytes(key: ec.EllipticCurvePrivateKey) -> bytes:
"""Serializes the public part of a private key in PEM format"""
return key.public_key().public_bytes(
serialization.Encoding.PEM,
- serialization.PublicFormat.SubjectPublicKeyInfo)
+ serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
def private_key_private_pem_bytes(key: ec.EllipticCurvePrivateKey) -> bytes:
"""Serializes the private part of a private key in PEM format"""
- return key.private_bytes(encoding=serialization.Encoding.PEM,
- format=serialization.PrivateFormat.PKCS8,
- encryption_algorithm=serialization.NoEncryption())
+ return key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.PKCS8,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
class Bundle:
"""A helper for test UpdateBundle generation"""
+
def __init__(self):
self._root_dev_key = serialization.load_pem_private_key(
- TEST_DEV_KEY.encode(), None)
+ TEST_DEV_KEY.encode(), None
+ )
self._root_prod_key = serialization.load_pem_private_key(
- TEST_PROD_KEY.encode(), None)
+ TEST_PROD_KEY.encode(), None
+ )
self._targets_dev_key = serialization.load_pem_private_key(
- TEST_TARGETS_DEV_KEY.encode(), None)
+ TEST_TARGETS_DEV_KEY.encode(), None
+ )
self._targets_prod_key = serialization.load_pem_private_key(
- TEST_TARGETS_PROD_KEY.encode(), None)
+ TEST_TARGETS_PROD_KEY.encode(), None
+ )
self._payloads: Dict[str, bytes] = {}
# Adds some update files.
for key, value in TARGET_FILES.items():
@@ -135,13 +147,17 @@ class Bundle:
# so that it can rotate to prod. But it will only use a dev targets
# key.
return root_metadata.gen_root_metadata(
- root_metadata.RootKeys([
- private_key_public_pem_bytes(self._root_dev_key),
- private_key_public_pem_bytes(self._root_prod_key),
- ]),
+ root_metadata.RootKeys(
+ [
+ private_key_public_pem_bytes(self._root_dev_key),
+ private_key_public_pem_bytes(self._root_prod_key),
+ ]
+ ),
root_metadata.TargetsKeys(
- [private_key_public_pem_bytes(self._targets_dev_key)]),
- TEST_ROOT_VERSION)
+ [private_key_public_pem_bytes(self._targets_dev_key)]
+ ),
+ TEST_ROOT_VERSION,
+ )
def generate_prod_root_metadata(self) -> RootMetadata:
"""Generates a root metadata with the prod key"""
@@ -149,44 +165,52 @@ class Bundle:
# prod targets key
return root_metadata.gen_root_metadata(
root_metadata.RootKeys(
- [private_key_public_pem_bytes(self._root_prod_key)]),
+ [private_key_public_pem_bytes(self._root_prod_key)]
+ ),
root_metadata.TargetsKeys(
- [private_key_public_pem_bytes(self._targets_prod_key)]),
- TEST_ROOT_VERSION)
+ [private_key_public_pem_bytes(self._targets_prod_key)]
+ ),
+ TEST_ROOT_VERSION,
+ )
def generate_dev_signed_root_metadata(self) -> SignedRootMetadata:
"""Generates a dev signed root metadata"""
signed_root = SignedRootMetadata()
root_metadata_proto = self.generate_dev_root_metadata()
- signed_root.serialized_root_metadata = \
+ signed_root.serialized_root_metadata = (
root_metadata_proto.SerializeToString()
+ )
return dev_sign.sign_root_metadata(
- signed_root, private_key_private_pem_bytes(self._root_dev_key))
+ signed_root, private_key_private_pem_bytes(self._root_dev_key)
+ )
def generate_prod_signed_root_metadata(
- self,
- root_metadata_proto: RootMetadata = None) -> SignedRootMetadata:
+ self, root_metadata_proto: Optional[RootMetadata] = None
+ ) -> SignedRootMetadata:
"""Generates a root metadata signed by the prod key"""
if not root_metadata_proto:
root_metadata_proto = self.generate_prod_root_metadata()
signed_root = SignedRootMetadata(
- serialized_root_metadata=root_metadata_proto.SerializeToString())
+ serialized_root_metadata=root_metadata_proto.SerializeToString()
+ )
return dev_sign.sign_root_metadata(
- signed_root, private_key_private_pem_bytes(self._root_prod_key))
+ signed_root, private_key_private_pem_bytes(self._root_prod_key)
+ )
def generate_targets_metadata(self) -> TargetsMetadata:
"""Generates the targets metadata"""
- targets = metadata.gen_targets_metadata(self._payloads,
- metadata.DEFAULT_HASHES,
- TEST_TARGETS_VERSION)
+ targets = metadata.gen_targets_metadata(
+ self._payloads, metadata.DEFAULT_HASHES, TEST_TARGETS_VERSION
+ )
return targets
def generate_unsigned_bundle(
- self,
- targets_metadata: TargetsMetadata = None,
- signed_root_metadata: SignedRootMetadata = None) -> UpdateBundle:
+ self,
+ targets_metadata: Optional[TargetsMetadata] = None,
+ signed_root_metadata: Optional[SignedRootMetadata] = None,
+ ) -> UpdateBundle:
"""Generate an unsigned (targets metadata) update bundle"""
bundle = UpdateBundle()
@@ -197,8 +221,10 @@ class Bundle:
bundle.root_metadata.CopyFrom(signed_root_metadata)
bundle.targets_metadata['targets'].CopyFrom(
- SignedTargetsMetadata(serialized_targets_metadata=targets_metadata.
- SerializeToString()))
+ SignedTargetsMetadata(
+ serialized_targets_metadata=targets_metadata.SerializeToString()
+ )
+ )
for name, payload in self._payloads.items():
bundle.target_payloads[name] = payload
@@ -206,33 +232,40 @@ class Bundle:
return bundle
def generate_dev_signed_bundle(
- self,
- targets_metadata_override: TargetsMetadata = None,
- signed_root_metadata: SignedRootMetadata = None) -> UpdateBundle:
+ self,
+ targets_metadata_override: Optional[TargetsMetadata] = None,
+ signed_root_metadata: Optional[SignedRootMetadata] = None,
+ ) -> UpdateBundle:
"""Generate a dev signed update bundle"""
return dev_sign.sign_update_bundle(
- self.generate_unsigned_bundle(targets_metadata_override,
- signed_root_metadata),
- private_key_private_pem_bytes(self._targets_dev_key))
+ self.generate_unsigned_bundle(
+ targets_metadata_override, signed_root_metadata
+ ),
+ private_key_private_pem_bytes(self._targets_dev_key),
+ )
def generate_prod_signed_bundle(
- self,
- targets_metadata_override: TargetsMetadata = None,
- signed_root_metadata: SignedRootMetadata = None) -> UpdateBundle:
+ self,
+ targets_metadata_override: Optional[TargetsMetadata] = None,
+ signed_root_metadata: Optional[SignedRootMetadata] = None,
+ ) -> UpdateBundle:
"""Generate a prod signed update bundle"""
# The targets metadata in a prod signed bundle can only be verified
# by a prod signed root. Because it is signed by the prod targets key.
# The prod signed root however, can be verified by a dev root.
return dev_sign.sign_update_bundle(
- self.generate_unsigned_bundle(targets_metadata_override,
- signed_root_metadata),
- private_key_private_pem_bytes(self._targets_prod_key))
+ self.generate_unsigned_bundle(
+ targets_metadata_override, signed_root_metadata
+ ),
+ private_key_private_pem_bytes(self._targets_prod_key),
+ )
def generate_manifest(self) -> Manifest:
"""Generates the manifest"""
manifest = Manifest()
manifest.targets_metadata['targets'].CopyFrom(
- self.generate_targets_metadata())
+ self.generate_targets_metadata()
+ )
if USER_MANIFEST_FILE_NAME in self._payloads:
manifest.user_manifest = self._payloads[USER_MANIFEST_FILE_NAME]
return manifest
@@ -241,16 +274,17 @@ class Bundle:
def parse_args():
"""Setup argparse."""
parser = argparse.ArgumentParser()
- parser.add_argument("output_header",
- help="output path of the generated C header")
+ parser.add_argument(
+ "output_header", help="output path of the generated C header"
+ )
return parser.parse_args()
+# TODO(b/237580538): Refactor the code so that each test bundle generation
+# is done in a separate function or script.
+# pylint: disable=too-many-locals
def main() -> int:
"""Main"""
- # TODO(pwbug/456): Refactor the code so that each test bundle generation
- # is done in a separate function or script.
- # pylint: disable=too-many-locals
args = parse_args()
test_bundle = Bundle()
@@ -258,66 +292,80 @@ def main() -> int:
dev_signed_root = test_bundle.generate_dev_signed_root_metadata()
dev_signed_bundle = test_bundle.generate_dev_signed_bundle()
dev_signed_bundle_with_root = test_bundle.generate_dev_signed_bundle(
- signed_root_metadata=dev_signed_root)
+ signed_root_metadata=dev_signed_root
+ )
unsigned_bundle_with_root = test_bundle.generate_unsigned_bundle(
- signed_root_metadata=dev_signed_root)
+ signed_root_metadata=dev_signed_root
+ )
manifest_proto = test_bundle.generate_manifest()
- prod_signed_root = \
- test_bundle.generate_prod_signed_root_metadata()
+ prod_signed_root = test_bundle.generate_prod_signed_root_metadata()
prod_signed_bundle = test_bundle.generate_prod_signed_bundle(
- None, prod_signed_root)
+ None, prod_signed_root
+ )
dev_signed_bundle_with_prod_root = test_bundle.generate_dev_signed_bundle(
- signed_root_metadata=prod_signed_root)
+ signed_root_metadata=prod_signed_root
+ )
# Generates a prod root metadata that fails signature verification against
# the dev root (i.e. it has a bad prod signature). This is done by making
# a bad prod signature.
bad_prod_signature = test_bundle.generate_prod_root_metadata()
- signed_bad_prod_signature = \
- test_bundle\
- .generate_prod_signed_root_metadata(
- bad_prod_signature)
+ signed_bad_prod_signature = test_bundle.generate_prod_signed_root_metadata(
+ bad_prod_signature
+ )
# Compromises the signature.
signed_bad_prod_signature.signatures[0].sig = b'1' * 64
signed_bad_prod_signature_bundle = test_bundle.generate_prod_signed_bundle(
- None, signed_bad_prod_signature)
+ None, signed_bad_prod_signature
+ )
# Generates a prod root metadtata that fails to verify itself. Specifically,
# the prod signature cannot be verified by the key in the incoming root
# metadata. This is done by dev signing a prod root metadata.
+ # pylint: disable=line-too-long
signed_mismatched_root_key_and_signature = SignedRootMetadata(
- serialized_root_metadata=test_bundle.generate_prod_root_metadata(
- ).SerializeToString())
- dev_root_key = serialization.load_pem_private_key(TEST_DEV_KEY.encode(),
- None)
+ serialized_root_metadata=test_bundle.generate_prod_root_metadata().SerializeToString()
+ )
+ # pylint: enable=line-too-long
+ dev_root_key = serialization.load_pem_private_key(
+ TEST_DEV_KEY.encode(), None
+ )
signature = keys.create_ecdsa_signature(
signed_mismatched_root_key_and_signature.serialized_root_metadata,
- private_key_private_pem_bytes(dev_root_key)) # type: ignore
+ private_key_private_pem_bytes(dev_root_key), # type: ignore
+ )
signed_mismatched_root_key_and_signature.signatures.append(signature)
- mismatched_root_key_and_signature_bundle = test_bundle\
- .generate_prod_signed_bundle(None,
- signed_mismatched_root_key_and_signature)
+ mismatched_root_key_and_signature_bundle = (
+ test_bundle.generate_prod_signed_bundle(
+ None, signed_mismatched_root_key_and_signature
+ )
+ )
# Generates a prod root metadata with rollback attempt.
root_rollback = test_bundle.generate_prod_root_metadata()
root_rollback.common_metadata.version = TEST_ROOT_VERSION - 1
- signed_root_rollback = test_bundle.\
- generate_prod_signed_root_metadata(root_rollback)
+ signed_root_rollback = test_bundle.generate_prod_signed_root_metadata(
+ root_rollback
+ )
root_rollback_bundle = test_bundle.generate_prod_signed_bundle(
- None, signed_root_rollback)
+ None, signed_root_rollback
+ )
# Generates a bundle with a bad target signature.
bad_targets_siganture = test_bundle.generate_prod_signed_bundle(
- None, prod_signed_root)
+ None, prod_signed_root
+ )
# Compromises the signature.
- bad_targets_siganture.targets_metadata['targets'].signatures[
- 0].sig = b'1' * 64
+ bad_targets_siganture.targets_metadata['targets'].signatures[0].sig = (
+ b'1' * 64
+ )
# Generates a bundle with rollback attempt
targets_rollback = test_bundle.generate_targets_metadata()
targets_rollback.common_metadata.version = TEST_TARGETS_VERSION - 1
targets_rollback_bundle = test_bundle.generate_prod_signed_bundle(
- targets_rollback, prod_signed_root)
+ targets_rollback, prod_signed_root
+ )
# Generate bundles with mismatched hash
mismatched_hash_targets_bundles = []
@@ -340,102 +388,140 @@ def main() -> int:
for idx, payload_file in enumerate(TARGET_FILES.items()):
mismatched_hash_targets = test_bundle.generate_targets_metadata()
mismatched_hash_targets.target_files[idx].hashes[0].hash = b'0' * 32
- mismatched_hash_targets_bundle = test_bundle\
- .generate_prod_signed_bundle(
- mismatched_hash_targets, prod_signed_root)
+ mismatched_hash_targets_bundle = (
+ test_bundle.generate_prod_signed_bundle(
+ mismatched_hash_targets, prod_signed_root
+ )
+ )
mismatched_hash_targets_bundles.append(mismatched_hash_targets_bundle)
mismatched_length_targets = test_bundle.generate_targets_metadata()
mismatched_length_targets.target_files[idx].length = 1
- mismatched_length_targets_bundle = test_bundle\
- .generate_prod_signed_bundle(
- mismatched_length_targets, prod_signed_root)
+ mismatched_length_targets_bundle = (
+ test_bundle.generate_prod_signed_bundle(
+ mismatched_length_targets, prod_signed_root
+ )
+ )
mismatched_length_targets_bundles.append(
- mismatched_length_targets_bundle)
+ mismatched_length_targets_bundle
+ )
missing_hash_targets = test_bundle.generate_targets_metadata()
missing_hash_targets.target_files[idx].hashes.pop()
missing_hash_targets_bundle = test_bundle.generate_prod_signed_bundle(
- missing_hash_targets, prod_signed_root)
+ missing_hash_targets, prod_signed_root
+ )
missing_hash_targets_bundles.append(missing_hash_targets_bundle)
file_name, _ = payload_file
personalized_out_bundle = test_bundle.generate_prod_signed_bundle(
- None, prod_signed_root)
+ None, prod_signed_root
+ )
personalized_out_bundle.target_payloads.pop(file_name)
personalized_out_bundles.append(personalized_out_bundle)
with open(args.output_header, 'w') as header:
header.write(HEADER)
header.write(
- proto_array_declaration(dev_signed_bundle, 'kTestDevBundle'))
+ proto_array_declaration(dev_signed_bundle, 'kTestDevBundle')
+ )
header.write(
- proto_array_declaration(dev_signed_bundle_with_root,
- 'kTestDevBundleWithRoot'))
+ proto_array_declaration(
+ dev_signed_bundle_with_root, 'kTestDevBundleWithRoot'
+ )
+ )
header.write(
- proto_array_declaration(unsigned_bundle_with_root,
- 'kTestUnsignedBundleWithRoot'))
+ proto_array_declaration(
+ unsigned_bundle_with_root, 'kTestUnsignedBundleWithRoot'
+ )
+ )
header.write(
- proto_array_declaration(dev_signed_bundle_with_prod_root,
- 'kTestDevBundleWithProdRoot'))
+ proto_array_declaration(
+ dev_signed_bundle_with_prod_root, 'kTestDevBundleWithProdRoot'
+ )
+ )
header.write(
- proto_array_declaration(manifest_proto, 'kTestBundleManifest'))
- header.write(proto_array_declaration(dev_signed_root,
- 'kDevSignedRoot'))
+ proto_array_declaration(manifest_proto, 'kTestBundleManifest')
+ )
+ header.write(proto_array_declaration(dev_signed_root, 'kDevSignedRoot'))
header.write(
- proto_array_declaration(prod_signed_bundle, 'kTestProdBundle'))
+ proto_array_declaration(prod_signed_bundle, 'kTestProdBundle')
+ )
header.write(
- proto_array_declaration(mismatched_root_key_and_signature_bundle,
- 'kTestMismatchedRootKeyAndSignature'))
+ proto_array_declaration(
+ mismatched_root_key_and_signature_bundle,
+ 'kTestMismatchedRootKeyAndSignature',
+ )
+ )
header.write(
- proto_array_declaration(signed_bad_prod_signature_bundle,
- 'kTestBadProdSignature'))
+ proto_array_declaration(
+ signed_bad_prod_signature_bundle, 'kTestBadProdSignature'
+ )
+ )
header.write(
- proto_array_declaration(bad_targets_siganture,
- 'kTestBadTargetsSignature'))
+ proto_array_declaration(
+ bad_targets_siganture, 'kTestBadTargetsSignature'
+ )
+ )
header.write(
- proto_array_declaration(targets_rollback_bundle,
- 'kTestTargetsRollback'))
+ proto_array_declaration(
+ targets_rollback_bundle, 'kTestTargetsRollback'
+ )
+ )
header.write(
- proto_array_declaration(root_rollback_bundle, 'kTestRootRollback'))
+ proto_array_declaration(root_rollback_bundle, 'kTestRootRollback')
+ )
for idx, mismatched_hash_bundle in enumerate(
- mismatched_hash_targets_bundles):
+ mismatched_hash_targets_bundles
+ ):
header.write(
proto_array_declaration(
mismatched_hash_bundle,
- f'kTestBundleMismatchedTargetHashFile{idx}'))
+ f'kTestBundleMismatchedTargetHashFile{idx}',
+ )
+ )
- for idx, missing_hash_bundle in enumerate(
- missing_hash_targets_bundles):
+ for idx, missing_hash_bundle in enumerate(missing_hash_targets_bundles):
header.write(
proto_array_declaration(
missing_hash_bundle,
- f'kTestBundleMissingTargetHashFile{idx}'))
+ f'kTestBundleMissingTargetHashFile{idx}',
+ )
+ )
for idx, mismatched_length_bundle in enumerate(
- mismatched_length_targets_bundles):
+ mismatched_length_targets_bundles
+ ):
header.write(
proto_array_declaration(
mismatched_length_bundle,
- f'kTestBundleMismatchedTargetLengthFile{idx}'))
+ f'kTestBundleMismatchedTargetLengthFile{idx}',
+ )
+ )
- for idx, personalized_out_bundle in enumerate(
- personalized_out_bundles):
+ for idx, personalized_out_bundle in enumerate(personalized_out_bundles):
header.write(
proto_array_declaration(
personalized_out_bundle,
- f'kTestBundlePersonalizedOutFile{idx}'))
- subprocess.run([
- 'clang-format',
- '-i',
- args.output_header,
- ], check=True)
- # TODO(pwbug/456): Refactor the code so that each test bundle generation
- # is done in a separate function or script.
- # pylint: enable=too-many-locals
+ f'kTestBundlePersonalizedOutFile{idx}',
+ )
+ )
+ subprocess.run(
+ [
+ 'clang-format',
+ '-i',
+ args.output_header,
+ ],
+ check=True,
+ )
return 0
+# TODO(b/237580538): Refactor the code so that each test bundle generation
+# is done in a separate function or script.
+# pylint: enable=too-many-locals
+
+
if __name__ == "__main__":
sys.exit(main())
diff --git a/pw_software_update/py/pw_software_update/keys.py b/pw_software_update/py/pw_software_update/keys.py
index c9a72d360..89f5d0ca6 100644
--- a/pw_software_update/py/pw_software_update/keys.py
+++ b/pw_software_update/py/pw_software_update/keys.py
@@ -29,23 +29,37 @@ from pathlib import Path
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import (
- decode_dss_signature, encode_dss_signature)
+ decode_dss_signature,
+ encode_dss_signature,
+)
from cryptography.hazmat.primitives.serialization import (
- Encoding, NoEncryption, PrivateFormat, PublicFormat, load_pem_private_key,
- load_pem_public_key)
-
-from pw_software_update.tuf_pb2 import (Key, KeyMapping, KeyScheme, KeyType,
- Signature)
+ Encoding,
+ NoEncryption,
+ PrivateFormat,
+ PublicFormat,
+ load_pem_private_key,
+ load_pem_public_key,
+)
+
+from pw_software_update.tuf_pb2 import (
+ Key,
+ KeyMapping,
+ KeyScheme,
+ KeyType,
+ Signature,
+)
def parse_args():
"""Parse CLI arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('-o',
- '--out',
- type=Path,
- required=True,
- help='Output path for the generated key')
+ parser.add_argument(
+ '-o',
+ '--out',
+ type=Path,
+ required=True,
+ help='Output path for the generated key',
+ )
return parser.parse_args()
@@ -61,12 +75,14 @@ def gen_ecdsa_keypair(out: Path) -> None:
private_pem = private_key.private_bytes(
encoding=Encoding.PEM,
format=PrivateFormat.PKCS8,
- encryption_algorithm=NoEncryption())
+ encryption_algorithm=NoEncryption(),
+ )
public_pem = public_key.public_bytes(
- encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo)
+ encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
+ )
out.write_bytes(private_pem)
- public_out = (out.parent / f'{out.name}.pub')
+ public_out = out.parent / f'{out.name}.pub'
public_out.write_bytes(public_pem)
@@ -86,18 +102,24 @@ def import_ecdsa_public_key(pem: bytes) -> KeyMapping:
if not isinstance(ec_key, ec.EllipticCurvePublicKey):
raise TypeError(
f'Not an elliptic curve public key type: {type(ec_key)}.'
- 'Try generate a key with gen_ecdsa_keypair()?')
+ 'Try generate a key with gen_ecdsa_keypair()?'
+ )
# pylint: disable=no-member
if not (ec_key.curve.name == 'secp256r1' and ec_key.key_size == 256):
- raise TypeError(f'Unsupported curve: {ec_key.curve.name}.'
- 'Try generate a key with gen_ecdsa_keypair()?')
+ raise TypeError(
+ f'Unsupported curve: {ec_key.curve.name}.'
+ 'Try generate a key with gen_ecdsa_keypair()?'
+ )
# pylint: enable=no-member
- tuf_key = Key(key_type=KeyType.ECDSA_SHA2_NISTP256,
- scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
- keyval=ec_key.public_bytes(Encoding.X962,
- PublicFormat.UncompressedPoint))
+ tuf_key = Key(
+ key_type=KeyType.ECDSA_SHA2_NISTP256,
+ scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
+ keyval=ec_key.public_bytes(
+ Encoding.X962, PublicFormat.UncompressedPoint
+ ),
+ )
return KeyMapping(key_id=gen_key_id(tuf_key), key=tuf_key)
@@ -105,15 +127,22 @@ def create_ecdsa_signature(data: bytes, key: bytes) -> Signature:
"""Creates an ECDSA-SHA2-NISTP256 signature."""
ec_key = load_pem_private_key(key, password=None)
if not isinstance(ec_key, ec.EllipticCurvePrivateKey):
- raise TypeError(f'Not an elliptic curve private key: {type(ec_key)}.'
- 'Try generate a key with gen_ecdsa_keypair()?')
-
- tuf_key = Key(key_type=KeyType.ECDSA_SHA2_NISTP256,
- scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
- keyval=ec_key.public_key().public_bytes(
- Encoding.X962, PublicFormat.UncompressedPoint))
-
- der_signature = ec_key.sign(data, ec.ECDSA(hashes.SHA256())) # pylint: disable=no-value-for-parameter
+ raise TypeError(
+ f'Not an elliptic curve private key: {type(ec_key)}.'
+ 'Try generate a key with gen_ecdsa_keypair()?'
+ )
+
+ tuf_key = Key(
+ key_type=KeyType.ECDSA_SHA2_NISTP256,
+ scheme=KeyScheme.ECDSA_SHA2_NISTP256_SCHEME,
+ keyval=ec_key.public_key().public_bytes(
+ Encoding.X962, PublicFormat.UncompressedPoint
+ ),
+ )
+
+ der_signature = ec_key.sign(
+ data, ec.ECDSA(hashes.SHA256())
+ ) # pylint: disable=no-value-for-parameter
int_r, int_s = decode_dss_signature(der_signature)
sig_bytes = int_r.to_bytes(32, 'big') + int_s.to_bytes(32, 'big')
@@ -132,10 +161,12 @@ def verify_ecdsa_signature(sig: bytes, data: bytes, key: Key) -> bool:
True if the signature is verified. False otherwise.
"""
ec_key = ec.EllipticCurvePublicKey.from_encoded_point(
- ec.SECP256R1(), key.keyval)
+ ec.SECP256R1(), key.keyval
+ )
try:
- dss_sig = encode_dss_signature(int.from_bytes(sig[:32], 'big'),
- int.from_bytes(sig[-32:], 'big'))
+ dss_sig = encode_dss_signature(
+ int.from_bytes(sig[:32], 'big'), int.from_bytes(sig[-32:], 'big')
+ )
ec_key.verify(dss_sig, data, ec.ECDSA(hashes.SHA256()))
except: # pylint: disable=bare-except
return False
diff --git a/pw_software_update/py/pw_software_update/metadata.py b/pw_software_update/py/pw_software_update/metadata.py
index 5390cfbf3..0816d7cc8 100644
--- a/pw_software_update/py/pw_software_update/metadata.py
+++ b/pw_software_update/py/pw_software_update/metadata.py
@@ -17,31 +17,48 @@ import enum
import hashlib
from typing import Dict, Iterable
-from pw_software_update.tuf_pb2 import (CommonMetadata, Hash, HashFunction,
- TargetFile, TargetsMetadata)
+from pw_software_update.tuf_pb2 import (
+ CommonMetadata,
+ Hash,
+ HashFunction,
+ TargetFile,
+ TargetsMetadata,
+)
HASH_FACTORIES = {
HashFunction.SHA256: hashlib.sha256,
}
-DEFAULT_HASHES = (HashFunction.SHA256, )
+DEFAULT_HASHES = (HashFunction.SHA256,)
DEFAULT_SPEC_VERSION = "0.0.1"
DEFAULT_METADATA_VERSION = 0
class RoleType(enum.Enum):
"""Set of allowed TUF metadata role types."""
+
ROOT = 'root'
TARGETS = 'targets'
def gen_common_metadata(
- role: RoleType,
- spec_version: str = DEFAULT_SPEC_VERSION,
- version: int = DEFAULT_METADATA_VERSION) -> CommonMetadata:
+ role: RoleType,
+ spec_version: str = DEFAULT_SPEC_VERSION,
+ version: int = DEFAULT_METADATA_VERSION,
+) -> CommonMetadata:
"""Generates CommonMetadata."""
- return CommonMetadata(role=role.value,
- spec_version=spec_version,
- version=version)
+ return CommonMetadata(
+ role=role.value, spec_version=spec_version, version=version
+ )
+
+
+def gen_target_file(
+ file_name: str, file_contents: bytes, hash_funcs=DEFAULT_HASHES
+) -> TargetFile:
+ return TargetFile(
+ file_name=file_name,
+ length=len(file_contents),
+ hashes=gen_hashes(file_contents, hash_funcs),
+ )
def gen_targets_metadata(
@@ -52,24 +69,30 @@ def gen_targets_metadata(
"""Generates TargetsMetadata the given target payloads."""
target_files = []
for target_file_name, target_payload in target_payloads.items():
- target_files.append(
- TargetFile(file_name=target_file_name,
- length=len(target_payload),
- hashes=gen_hashes(target_payload, hash_funcs)))
+ new_target_file = gen_target_file(
+ file_name=target_file_name,
+ file_contents=target_payload,
+ hash_funcs=hash_funcs,
+ )
+
+ target_files.append(new_target_file)
common_metadata = gen_common_metadata(RoleType.TARGETS, version=version)
- return TargetsMetadata(common_metadata=common_metadata,
- target_files=target_files)
+ return TargetsMetadata(
+ common_metadata=common_metadata, target_files=target_files
+ )
-def gen_hashes(data: bytes,
- hash_funcs: Iterable['HashFunction.V']) -> Iterable[Hash]:
+def gen_hashes(
+ data: bytes, hash_funcs: Iterable['HashFunction.V']
+) -> Iterable[Hash]:
"""Computes all the specified hashes over the data."""
result = []
for func in hash_funcs:
if func == HashFunction.UNKNOWN_HASH_FUNCTION:
raise ValueError(
- 'UNKNOWN_HASH_FUNCTION cannot be used to generate hashes.')
+ 'UNKNOWN_HASH_FUNCTION cannot be used to generate hashes.'
+ )
digest = HASH_FACTORIES[func](data).digest()
result.append(Hash(function=func, hash=digest))
diff --git a/pw_software_update/py/pw_software_update/remote_sign.py b/pw_software_update/py/pw_software_update/remote_sign.py
new file mode 100644
index 000000000..f0e5ecefd
--- /dev/null
+++ b/pw_software_update/py/pw_software_update/remote_sign.py
@@ -0,0 +1,428 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Upload update bundles to a GCS bucket for signing.
+
+This module does not implement any actual signing logic; that is left up to the
+remote signing service. This module takes care of uploading bundles to GCS for
+signing, waiting the the signed version to appear, and downloading the signed
+bundle from the output bucket. It can be used either as an entry point by
+invoking it as a runnable module and providing all the necessary arguments (run
+the tool with --help for details), or as a library by instantiating
+RemoteSignClient and calling its sign() method.
+
+The expected API for the remote signing service consists of the following:
+
+ - A pair of GCS buckets. One bucket to serve as a queue of update bundles
+ to be signed, and the other bucket to serve as the output area where
+ signed bundles are deposited.
+
+ - Three artifacts should be placed into the input queue bucket:
+ 1. The update bundle to be signed
+ 2. A signing request file whose name ends with "signing_request.json"
+ 3. A public builder key named "<signing_request_name>.public_key.pem"
+
+Builder keys are used to generate intermediate signatures for the signing
+request. Specifically, a private builder key is used to generate an
+intermediate signature for both the update bundle to be signed, and the signing
+request file. These signatures are then added to the GCS blob metadata for
+their respective blobs. The corresponding public builder key is uploaded
+alongside the signing request.
+
+The signing service should be set up to trigger whenever a new signing request
+file is added anywhere inside the input queue bucket. The signing request file
+is a JSON file with the following fields:
+
+ remote_signing_key_name: A string that should correspond to the name of a
+ signing key known to the remote signing service.
+
+ bundle_path: The path (relative to GCS input bucket root) to an update
+ bundle to be signed by the remote signing service.
+
+ bundle_public_key_path: The path (relative to GCS input bucket root) to a
+ public builder key .pem file corresponding to the private builder key
+ that was used to sign the update bundle and signing request file.
+
+ output_bucket: Name of the output GCS bucket into which the remote signing
+ service should place signed artifacts.
+
+ output_path: The path (relative to the GCS output bucket root) at which the
+ signed update bundle should be deposited by the remote signing service.
+
+On the remote side, the signing service is expected to check the public builder
+key against its list of allowed builder keys. Provided the key is found in the
+allow list, the signing service should use it to verify the intermediate
+signatures of both the update bundle to be signed and the signing request file.
+If the key is not found in the allow list, or if the signature on the update
+bundle or signing request file do not match the public builder key, the signing
+service should reject the signing request.
+
+If the builder public key is found in the allow list and the intermediate
+signatures are verified, the signing service should produce a signed version of
+the update bundle, and place it the GCS output bucket at the specified path.
+
+In order to authenticate to GCS, the "Application Default Credentials" will be
+used. This assumes remote_sign.py is running in an environment that provides
+such credentials. See the Google cloud platform documentation for details:
+
+ https://cloud.google.com/docs/authentication/production
+
+For development purposes, it is possible to provide an alternate method of
+authenticating to GCS. The alternate authentication method should be a Python
+module that is importable in the running Python environment. The module should
+define a 'get_credentials()' function that takes no arguments and returns an
+instance of google.auth.credentials.Credentials.
+"""
+import argparse
+import base64
+import importlib
+import json
+from pathlib import Path
+import time
+from typing import Dict, Optional, Union
+
+from cryptography.hazmat import backends
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import ed25519, padding, rsa
+from google.auth.credentials import Credentials # type: ignore
+from google.cloud import storage # type: ignore
+from google.cloud.storage.bucket import Bucket # type: ignore
+
+DEFAULT_TIMEOUT_S = 600
+
+PathOrBytes = Union[Path, bytes]
+
+
+def _parse_args():
+ """Parse CLI aguments."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--project', help='GCP project that owns storage buckets.'
+ )
+ parser.add_argument(
+ '--input-bucket', help='GCS bucket used as a signing queue'
+ )
+ parser.add_argument(
+ '--output-bucket', help='GCS bucket to watch for signed bundles'
+ )
+ parser.add_argument(
+ '--bundle', type=Path, help='Update bundle to upload for signing'
+ )
+ parser.add_argument(
+ '--out', type=Path, help='Path to which to download signed bundle'
+ )
+ parser.add_argument(
+ '--signing-key-name',
+ help='Name of signing key remote signing service should use',
+ )
+ parser.add_argument(
+ '--builder-key',
+ type=Path,
+ help='Path to builder private key for intermediate signatures',
+ )
+ parser.add_argument(
+ '--builder-public-key', type=Path, help='Path to builder public key'
+ )
+ parser.add_argument(
+ '--bundle-blob-name',
+ default=None,
+ help='Path in the input bucket at which to upload bundle',
+ )
+ parser.add_argument(
+ '--request-blob-name',
+ default=None,
+ help='Path in the input bucket at which to put signing request',
+ )
+ parser.add_argument(
+ '--signed-bundle-blob-name',
+ default=None,
+ help='Path in the output bucket for the signed bundle',
+ )
+ parser.add_argument(
+ '--dev-gcs-auth-module-override',
+ default=None,
+ help='Developer use only; custom auth module to use with GCS.',
+ )
+ parser.add_argument(
+ '--timeout',
+ type=int,
+ default=DEFAULT_TIMEOUT_S,
+ help='Seconds to wait for signed bundle to appeaer before giving up.',
+ )
+
+ return parser.parse_args()
+
+
+class BlobExistsError(Exception):
+ """Raised if the blob to be uploaded already exists in the input bucket."""
+
+
+class RemoteSignClient:
+ """GCS client for use in remote signing."""
+
+ def __init__(self, input_bucket: Bucket, output_bucket: Bucket):
+ # "Application Default Credentials" are used implicitly when None is
+ # passed to Client() as credentials. See the cloud docs for details:
+ # https://cloud.google.com/docs/authentication/production
+ self._input_bucket = input_bucket
+ self._output_bucket = output_bucket
+
+ @classmethod
+ def from_names(
+ cls,
+ project_name: str,
+ input_bucket_name: str,
+ output_bucket_name: str,
+ gcs_credentials: Optional[Credentials] = None,
+ ):
+ storage_client = storage.Client(
+ project=project_name, credentials=gcs_credentials
+ )
+ return cls(
+ input_bucket=storage_client.bucket(input_bucket_name),
+ output_bucket=storage_client.bucket(output_bucket_name),
+ )
+
+ def sign(
+ self,
+ bundle: Path,
+ signing_key_name: str,
+ builder_key: Path,
+ builder_public_key: Path,
+ bundle_blob_name: Optional[str] = None,
+ request_blob_name: Optional[str] = None,
+ signed_bundle_blob_name: Optional[str] = None,
+ request_overrides: Optional[Dict] = None,
+ timeout_s: int = DEFAULT_TIMEOUT_S,
+ ) -> bytes:
+ """Upload file to GCS and download signed counterpart when ready.
+
+ Args:
+ bundle: Path object for an UpdateBundle to upload for signing.
+ signing_key_name: Name of remote signing key to use for signing.
+ builder_key: Path to builder private key for intermediate signature.
+ builder_public_key: Path to corresponding builder public key.
+ bundle_blob_name: GCS path at which to upload bundle to sign.
+ request_blob_name: GCS path at which to upload request file.
+ signed_bundle_blob_name: GCS path in output bucket to request.
+ request_overrides: Dict of signing request JSON keys and values to
+ add to the signing requests. If this dict contains any keys whose
+ values are already in the signing request, the existing values
+ will be overwritten by the ones passed in here.
+ timeout_s: Maximum seconds to wait for output before failing.
+ """
+ if bundle_blob_name is None:
+ bundle_blob_name = bundle.name
+
+ if request_blob_name is None:
+ request_blob_name = f't{time.time()}_signing_request.json'
+
+ if not request_blob_name.endswith('signing_request.json'):
+ raise ValueError(
+ f'Signing request blob name {request_blob_name}'
+ ' does not end with "signing_request.json".'
+ )
+
+ request_name = request_blob_name[:-5] # strip the ".json"
+ builder_public_key_blob_name = f'{request_name}.publickey.pem'
+
+ if signed_bundle_blob_name is None:
+ signed_bundle_blob_name = f'{bundle.name}.signed'
+
+ signing_request = {
+ 'remote_signing_key_names': [signing_key_name],
+ 'bundle_path': bundle_blob_name,
+ 'bundle_public_key_path': builder_public_key_blob_name,
+ 'output_bucket': self._output_bucket.name,
+ 'output_path': signed_bundle_blob_name,
+ }
+
+ if request_overrides is not None:
+ signing_request.update(request_overrides)
+
+ builder_public_key_blob = self._input_bucket.blob(
+ builder_public_key_blob_name
+ )
+ bundle_blob = self._input_bucket.blob(bundle_blob_name)
+ request_blob = self._input_bucket.blob(request_blob_name)
+
+ for blob in (builder_public_key_blob, bundle_blob, request_blob):
+ if blob.exists():
+ raise BlobExistsError(
+ f'A blob named "{blob}" already exists in the input bucket.'
+ ' A unique blob name is required for uploading.'
+ )
+
+ builder_public_key_blob.upload_from_filename(str(builder_public_key))
+
+ bundle_blob.metadata = {
+ 'signature': self._get_builder_signature(
+ bundle, builder_key
+ ).decode('ascii')
+ }
+ bundle_blob.upload_from_filename(str(bundle))
+
+ encoded_json = bytes(json.dumps(signing_request), 'utf-8')
+ request_blob.metadata = {
+ 'signature': self._get_builder_signature(
+ encoded_json, builder_key
+ ).decode('ascii')
+ }
+
+ # Despite its name, the upload_from_string() method can take either a
+ # str or bytes object; here we already pre-encoded the string in utf-8.
+ request_blob.upload_from_string(encoded_json)
+
+ return self._wait_for_blob(signed_bundle_blob_name, timeout_s=timeout_s)
+
+ def _wait_for_blob(
+ self,
+ blob_name,
+ interval: int = 1,
+ max_tries: Optional[int] = None,
+ timeout_s: int = DEFAULT_TIMEOUT_S,
+ ) -> storage.Blob:
+ """Wait for a specific blob to appear in the output bucket.
+
+ Args:
+ blob_name: Name of the blob to wait for.
+ interval: Time (seconds) to wait between checks for blob's existence.
+ max_tries: Number of times to check for the blob before failing.
+ timeout_s: Maximum seconds to keep watching before failing.
+ """
+ blob = self._output_bucket.blob(blob_name)
+ end_time = time.time() + timeout_s
+ tries = 0
+ while max_tries is None or tries < max_tries:
+ if time.time() > end_time:
+ raise FileNotFoundError(
+ 'Timed out while waiting for signed blob.'
+ )
+ if blob.exists():
+ return blob
+ tries += 1
+ time.sleep(interval)
+
+ raise FileNotFoundError(
+ 'Too many retries while waiting for signed blob.'
+ )
+
+ @staticmethod
+ def _get_builder_signature(data: PathOrBytes, key: Path) -> bytes:
+ """Generate a base64-encided builder signature for file.
+
+ In order for the remote signing system to have some level of trust in
+ the artifacts it's signing, an intermediate signature is used to verify
+ that the artifacts came from an approved builder.
+ """
+ if isinstance(data, Path):
+ data = data.read_bytes()
+
+ private_key = serialization.load_pem_private_key(
+ key.read_bytes(), None, backends.default_backend()
+ )
+
+ if isinstance(private_key, ed25519.Ed25519PrivateKey):
+ signature = private_key.sign(data)
+
+ elif isinstance(private_key, rsa.RSAPrivateKey):
+ signature = private_key.sign(
+ data, # type: ignore
+ padding=padding.PSS(
+ mgf=padding.MGF1(hashes.SHA256()),
+ salt_length=padding.PSS.MAX_LENGTH,
+ ),
+ algorithm=hashes.SHA256(),
+ )
+
+ else:
+ raise TypeError(
+ f'Key {private_key} has unsupported type'
+ f' ({type(private_key)}). Valid types are'
+ ' Ed25519PrivateKey and RSAPrivateKey.'
+ )
+
+ return base64.b64encode(signature)
+
+
+def _credentials_from_module(module_name: str) -> Credentials:
+ """Return GCS Credential from the named auth module.
+
+ The module name should correspond to a module that's importable in the
+ running Python environment. It must define a get_credentials() function
+ that takes no args and returns a Credentials instance.
+ """
+ auth_module = importlib.import_module(module_name)
+ return auth_module.get_credentials() # type: ignore
+
+
+def main( # pylint: disable=too-many-arguments
+ project: str,
+ input_bucket: str,
+ output_bucket: str,
+ bundle: Path,
+ out: Path,
+ signing_key_name: str,
+ builder_key: Path,
+ builder_public_key: Path,
+ bundle_blob_name: str,
+ request_blob_name: str,
+ signed_bundle_blob_name: str,
+ dev_gcs_auth_module_override: str,
+ timeout: int,
+) -> None:
+ """Send bundle for remote signing and write signed counterpart to disk.
+
+ Args:
+ project: Project name for GCS project containing signing bucket pair.
+ input_bucket: Name of GCS bucket to deposit to-be-signed artifacts in.
+ output_bucket: Name of GCS bucket to watch for signed artifacts.
+ bundle: Update bundle to be signed.
+ out: Path to which to download signed version of bundle.
+ signing_key_name: Name of key the remote signing service should use.
+ bundle_blob_name: Path in input bucket to upload bundle to.
+ request_blob_name: Path in input bucket to upload signing request to.
+ signed_bundle_blob_name: Output bucket path for signed bundle.
+ dev_gcs_auth_module_override: For developer use only; optional module
+ to use to generate GCS client credentials. Must be importable in
+ the running Python environment, and must define a get_credentials()
+ function that takes no args and returns a Credentials instance.
+ timeout: Seconds to wait for signed bundle before giving up.
+ """
+ credentials = None
+ if dev_gcs_auth_module_override is not None:
+ credentials = _credentials_from_module(dev_gcs_auth_module_override)
+
+ remote_sign_client = RemoteSignClient.from_names(
+ project_name=project,
+ input_bucket_name=input_bucket,
+ output_bucket_name=output_bucket,
+ gcs_credentials=credentials,
+ )
+
+ signed_bundle = remote_sign_client.sign(
+ bundle,
+ signing_key_name,
+ builder_key,
+ builder_public_key,
+ bundle_blob_name,
+ request_blob_name,
+ signed_bundle_blob_name,
+ timeout_s=timeout,
+ )
+
+ out.write_bytes(signed_bundle.download_as_bytes())
+
+
+if __name__ == '__main__':
+ main(**vars(_parse_args()))
diff --git a/pw_software_update/py/pw_software_update/root_metadata.py b/pw_software_update/py/pw_software_update/root_metadata.py
index db7d59a2b..5641fc9e5 100644
--- a/pw_software_update/py/pw_software_update/root_metadata.py
+++ b/pw_software_update/py/pw_software_update/root_metadata.py
@@ -18,16 +18,19 @@ from pathlib import Path
from typing import Iterable, List, NewType
from pw_software_update import keys, metadata
-from pw_software_update.tuf_pb2 import (RootMetadata, SignedRootMetadata,
- SignatureRequirement)
+from pw_software_update.tuf_pb2 import (
+ RootMetadata,
+ SignedRootMetadata,
+ SignatureRequirement,
+)
RootKeys = NewType('RootKeys', List[bytes])
TargetsKeys = NewType('TargetsKeys', List[bytes])
-def gen_root_metadata(root_key_pems: RootKeys,
- targets_key_pems: TargetsKeys,
- version: int = 1) -> RootMetadata:
+def gen_root_metadata(
+ root_key_pems: RootKeys, targets_key_pems: TargetsKeys, version: int = 1
+) -> RootMetadata:
"""Generates a RootMetadata.
Args:
@@ -35,63 +38,82 @@ def gen_root_metadata(root_key_pems: RootKeys,
targets_key_pems: list of targets keys in PEM format.
version: Version number for rollback checks.
"""
- common = metadata.gen_common_metadata(metadata.RoleType.ROOT,
- version=version)
+ common = metadata.gen_common_metadata(
+ metadata.RoleType.ROOT, version=version
+ )
root_keys = [keys.import_ecdsa_public_key(pem) for pem in root_key_pems]
targets_keys = [
keys.import_ecdsa_public_key(pem) for pem in targets_key_pems
]
- return RootMetadata(common_metadata=common,
- consistent_snapshot=False,
- keys=root_keys + targets_keys,
- root_signature_requirement=SignatureRequirement(
- key_ids=[k.key_id for k in root_keys],
- threshold=1),
- targets_signature_requirement=SignatureRequirement(
- key_ids=[k.key_id for k in targets_keys],
- threshold=1))
+ return RootMetadata(
+ common_metadata=common,
+ consistent_snapshot=False,
+ keys=root_keys + targets_keys,
+ root_signature_requirement=SignatureRequirement(
+ key_ids=[k.key_id for k in root_keys], threshold=1
+ ),
+ targets_signature_requirement=SignatureRequirement(
+ key_ids=[k.key_id for k in targets_keys], threshold=1
+ ),
+ )
def parse_args():
"""Parse CLI arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('-o',
- '--out',
- type=Path,
- required=True,
- help='Output path for the generated root metadata')
-
- parser.add_argument('--version',
- type=int,
- default=1,
- help='Canonical version number for rollback checks')
-
- parser.add_argument('--root-key',
- type=Path,
- required=True,
- nargs='+',
- help='Public key filename for the "Root" role')
-
- parser.add_argument('--targets-key',
- type=Path,
- required=True,
- nargs='+',
- help='Public key filename for the "Targets" role')
+ parser.add_argument(
+ '-o',
+ '--out',
+ type=Path,
+ required=True,
+ help='Output path for the generated root metadata',
+ )
+
+ parser.add_argument(
+ '--version',
+ type=int,
+ default=1,
+ help='Canonical version number for rollback checks',
+ )
+
+ parser.add_argument(
+ '--root-key',
+ type=Path,
+ required=True,
+ nargs='+',
+ help='Public key filename for the "Root" role',
+ )
+
+ parser.add_argument(
+ '--targets-key',
+ type=Path,
+ required=True,
+ nargs='+',
+ help='Public key filename for the "Targets" role',
+ )
return parser.parse_args()
-def main(out: Path, root_key: Iterable[Path], targets_key: Iterable[Path],
- version: int) -> None:
+def main(
+ out: Path,
+ root_key: Iterable[Path],
+ targets_key: Iterable[Path],
+ version: int,
+) -> None:
"""Generates and writes to disk an unsigned SignedRootMetadata."""
root_metadata = gen_root_metadata(
RootKeys([k.read_bytes() for k in root_key]),
- TargetsKeys([k.read_bytes() for k in targets_key]), version)
+ TargetsKeys([k.read_bytes() for k in targets_key]),
+ version,
+ )
signed = SignedRootMetadata(
- serialized_root_metadata=root_metadata.SerializeToString())
+ serialized_root_metadata=root_metadata.SerializeToString()
+ )
+
out.write_bytes(signed.SerializeToString())
diff --git a/pw_software_update/py/pw_software_update/update_bundle.py b/pw_software_update/py/pw_software_update/update_bundle.py
index 978f9887d..c3c1056bb 100644
--- a/pw_software_update/py/pw_software_update/update_bundle.py
+++ b/pw_software_update/py/pw_software_update/update_bundle.py
@@ -28,9 +28,10 @@ _LOG = logging.getLogger(__package__)
def targets_from_directory(
- root_dir: Path,
- exclude: Iterable[Path] = tuple(),
- remap_paths: Optional[Dict[Path, str]] = None) -> Dict[str, Path]:
+ root_dir: Path,
+ exclude: Iterable[Path] = tuple(),
+ remap_paths: Optional[Dict[Path, str]] = None,
+) -> Dict[str, Path]:
"""Given a directory on dist, generate a dict of target names to files.
Args:
@@ -49,7 +50,8 @@ def targets_from_directory(
"""
if not root_dir.is_dir():
raise ValueError(
- f'Cannot generate TUF targets from {root_dir}; not a directory.')
+ f'Cannot generate TUF targets from {root_dir}; not a directory.'
+ )
targets = {}
for path in root_dir.glob('**/*'):
if path.is_dir():
@@ -70,16 +72,46 @@ def targets_from_directory(
if new_target_file_name not in targets:
raise FileNotFoundError(
f'Unable to remap "{original_path}" to'
- f' "{new_target_file_name}"; file not found in root dir.')
+ f' "{new_target_file_name}"; file not found in root dir.'
+ )
return targets
+def gen_empty_update_bundle(
+ targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION,
+) -> UpdateBundle:
+ """Generates an empty bundle
+
+ Given an optional target metadata version, generates an empty bundle.
+
+ Args:
+ targets_metadata_version: default set to 1
+
+ Returns:
+ UpdateBundle: empty bundle
+ """
+
+ targets_metadata = metadata.gen_targets_metadata(
+ target_payloads={}, version=targets_metadata_version
+ )
+ unsigned_targets_metadata = SignedTargetsMetadata(
+ serialized_targets_metadata=targets_metadata.SerializeToString()
+ )
+
+ return UpdateBundle(
+ root_metadata=None,
+ targets_metadata=dict(targets=unsigned_targets_metadata),
+ target_payloads=None,
+ )
+
+
def gen_unsigned_update_bundle(
- targets: Dict[Path, str],
- persist: Optional[Path] = None,
- targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION,
- root_metadata: SignedRootMetadata = None) -> UpdateBundle:
+ targets: Dict[Path, str],
+ persist: Optional[Path] = None,
+ targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION,
+ root_metadata: Optional[SignedRootMetadata] = None,
+) -> UpdateBundle:
"""Given a set of targets, generates an unsigned UpdateBundle.
Args:
@@ -105,8 +137,10 @@ def gen_unsigned_update_bundle(
"""
if persist:
if persist.exists() and not persist.is_dir():
- raise ValueError(f'TUF repo cannot be persisted to "{persist}";'
- ' file exists and is not a directory.')
+ raise ValueError(
+ f'TUF repo cannot be persisted to "{persist}";'
+ ' file exists and is not a directory.'
+ )
if persist.exists():
shutil.rmtree(persist)
@@ -121,14 +155,17 @@ def gen_unsigned_update_bundle(
shutil.copy(path, target_persist_path)
targets_metadata = metadata.gen_targets_metadata(
- target_payloads, version=targets_metadata_version)
+ target_payloads, version=targets_metadata_version
+ )
unsigned_targets_metadata = SignedTargetsMetadata(
- serialized_targets_metadata=targets_metadata.SerializeToString())
+ serialized_targets_metadata=targets_metadata.SerializeToString()
+ )
return UpdateBundle(
root_metadata=root_metadata,
targets_metadata=dict(targets=unsigned_targets_metadata),
- target_payloads=target_payloads)
+ target_payloads=target_payloads,
+ )
def parse_target_arg(target_arg: str) -> Tuple[Path, str]:
@@ -144,52 +181,70 @@ def parse_target_arg(target_arg: str) -> Tuple[Path, str]:
file_path_str, target_name = target_arg.split('>')
return Path(file_path_str.strip()), target_name.strip()
except ValueError as err:
- raise ValueError('Targets must be strings of the form:\n'
- ' "FILE_PATH > TARGET_NAME"') from err
+ raise ValueError(
+ 'Targets must be strings of the form:\n'
+ ' "FILE_PATH > TARGET_NAME"'
+ ) from err
def parse_args() -> argparse.Namespace:
"""Parse CLI arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('-t',
- '--targets',
- type=str,
- nargs='+',
- required=True,
- help='Strings defining targets to bundle')
- parser.add_argument('-o',
- '--out',
- type=Path,
- required=True,
- help='Output path for serialized UpdateBundle')
- parser.add_argument('--persist',
- type=Path,
- default=None,
- help=('If provided, TUF repo will be persisted to disk'
- ' at this path for debugging'))
- parser.add_argument('--targets-metadata-version',
- type=int,
- default=metadata.DEFAULT_METADATA_VERSION,
- help='Version number for the targets metadata')
- parser.add_argument('--targets-metadata-version-file',
- type=Path,
- default=None,
- help='Read version number string from this file. When '
- 'provided, content of this file supersede '
- '--targets-metadata-version')
- parser.add_argument('--signed-root-metadata',
- type=Path,
- default=None,
- help='Path to the signed Root metadata')
+ parser.add_argument(
+ '-t',
+ '--targets',
+ type=str,
+ nargs='+',
+ required=True,
+ help='Strings defining targets to bundle',
+ )
+ parser.add_argument(
+ '-o',
+ '--out',
+ type=Path,
+ required=True,
+ help='Output path for serialized UpdateBundle',
+ )
+ parser.add_argument(
+ '--persist',
+ type=Path,
+ default=None,
+ help=(
+ 'If provided, TUF repo will be persisted to disk'
+ ' at this path for debugging'
+ ),
+ )
+ parser.add_argument(
+ '--targets-metadata-version',
+ type=int,
+ default=metadata.DEFAULT_METADATA_VERSION,
+ help='Version number for the targets metadata',
+ )
+ parser.add_argument(
+ '--targets-metadata-version-file',
+ type=Path,
+ default=None,
+ help='Read version number string from this file. When '
+ 'provided, content of this file supersede '
+ '--targets-metadata-version',
+ )
+ parser.add_argument(
+ '--signed-root-metadata',
+ type=Path,
+ default=None,
+ help='Path to the signed Root metadata',
+ )
return parser.parse_args()
-def main(targets: Iterable[str],
- out: Path,
- persist: Path = None,
- targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION,
- targets_metadata_version_file: Path = None,
- signed_root_metadata: Path = None) -> None:
+def main(
+ targets: Iterable[str],
+ out: Path,
+ persist: Optional[Path] = None,
+ targets_metadata_version: int = metadata.DEFAULT_METADATA_VERSION,
+ targets_metadata_version_file: Optional[Path] = None,
+ signed_root_metadata: Optional[Path] = None,
+) -> None:
"""Generates an UpdateBundle and serializes it to disk."""
target_dict = {}
for target_arg in targets:
@@ -199,15 +254,16 @@ def main(targets: Iterable[str],
root_metadata = None
if signed_root_metadata:
root_metadata = SignedRootMetadata.FromString(
- signed_root_metadata.read_bytes())
+ signed_root_metadata.read_bytes()
+ )
if targets_metadata_version_file:
with targets_metadata_version_file.open() as version_file:
targets_metadata_version = int(version_file.read().strip())
- bundle = gen_unsigned_update_bundle(target_dict, persist,
- targets_metadata_version,
- root_metadata)
+ bundle = gen_unsigned_update_bundle(
+ target_dict, persist, targets_metadata_version, root_metadata
+ )
out.write_bytes(bundle.SerializeToString())
diff --git a/pw_software_update/py/pw_software_update/verify.py b/pw_software_update/py/pw_software_update/verify.py
index 7eca73e30..2e1e353c4 100644
--- a/pw_software_update/py/pw_software_update/verify.py
+++ b/pw_software_update/py/pw_software_update/verify.py
@@ -21,16 +21,18 @@ import sys
from typing import Iterable
from pw_software_update import keys, metadata
-from pw_software_update.tuf_pb2 import (RootMetadata, SignedRootMetadata,
- SignedTargetsMetadata, TargetsMetadata)
+from pw_software_update.tuf_pb2 import (
+ RootMetadata,
+ SignedRootMetadata,
+ SignedTargetsMetadata,
+ TargetsMetadata,
+)
from pw_software_update.update_bundle_pb2 import UpdateBundle
_LOG = logging.getLogger(__package__)
-def log_progress(message: str,
- indent_offset: int = -5,
- indent_str: str = ' '):
+def log_progress(message: str, indent_offset: int = -5, indent_str: str = ' '):
"""Logs verification progress.
The default indent offset is chosen per actual output of 'python -m verify'.
@@ -64,8 +66,10 @@ def lint_root_metadata(root: RootMetadata) -> Iterable[str]:
elif not entry.key.keyval:
errors.append(f'Key {entry.key_id.hex()} does not have a value')
elif not entry.key_id == keys.gen_key_id(entry.key):
- errors.append(f'Key id "{entry.key_id.hex()}" cannot be derived'
- f'from key content')
+ errors.append(
+ f'Key id "{entry.key_id.hex()}" cannot be derived'
+ f'from key content'
+ )
# Check root signature requirement.
log_progress('Checking root signature requirement')
@@ -76,7 +80,8 @@ def lint_root_metadata(root: RootMetadata) -> Iterable[str]:
if len(root_sig_req.key_ids) < root_sig_req.threshold:
errors.append(
f'Insufficient root keys: '
- f'{len(root_sig_req.key_ids)} < {root_sig_req.threshold}')
+ f'{len(root_sig_req.key_ids)} < {root_sig_req.threshold}'
+ )
for key_id in root_sig_req.key_ids:
if key_id not in [km.key_id for km in root.keys]:
@@ -91,7 +96,8 @@ def lint_root_metadata(root: RootMetadata) -> Iterable[str]:
if len(targets_sig_req.key_ids) < targets_sig_req.threshold:
errors.append(
f'Insufficient Targets keys: '
- f'{len(targets_sig_req.key_ids)} < {targets_sig_req.threshold}')
+ f'{len(targets_sig_req.key_ids)} < {targets_sig_req.threshold}'
+ )
for key_id in targets_sig_req.key_ids:
if key_id not in [km.key_id for km in root.keys]:
@@ -106,8 +112,9 @@ def lint_root_metadata(root: RootMetadata) -> Iterable[str]:
return errors
-def verify_root_metadata_signatures(incoming: SignedRootMetadata,
- trusted: RootMetadata) -> None:
+def verify_root_metadata_signatures(
+ incoming: SignedRootMetadata, trusted: RootMetadata
+) -> None:
"""Verifies the signatures of an incoming root metadata.
Verifies the signatures of an incoming root metadata against signature
@@ -118,8 +125,10 @@ def verify_root_metadata_signatures(incoming: SignedRootMetadata,
"""
sig_requirement = trusted.root_signature_requirement
- log_progress(f'Total={len(incoming.signatures)}, '
- f'threshold={sig_requirement.threshold}')
+ log_progress(
+ f'Total={len(incoming.signatures)}, '
+ f'threshold={sig_requirement.threshold}'
+ )
good_signature_count = 0
for sig in incoming.signatures:
if sig.key_id not in sig_requirement.key_ids:
@@ -134,7 +143,8 @@ def verify_root_metadata_signatures(incoming: SignedRootMetadata,
raise VerificationError(f'Invalid key_id: {sig.key_id.hex()}.')
if not keys.verify_ecdsa_signature(
- sig.sig, incoming.serialized_root_metadata, key):
+ sig.sig, incoming.serialized_root_metadata, key
+ ):
raise VerificationError('Invalid signature, key_id={sig.key_id}.')
good_signature_count += 1
@@ -144,8 +154,9 @@ def verify_root_metadata_signatures(incoming: SignedRootMetadata,
raise VerificationError('Not enough good signatures.')
-def verify_root_metadata(incoming: SignedRootMetadata,
- trusted: RootMetadata) -> bool:
+def verify_root_metadata(
+ incoming: SignedRootMetadata, trusted: RootMetadata
+) -> bool:
"""Verifies an incoming root metadata against a trusted root metadata.
Returns:
@@ -164,7 +175,8 @@ def verify_root_metadata(incoming: SignedRootMetadata,
# before parsing it to guard against chosen-ciphertext attacks.
log_progress('Checking content')
lint_errors = lint_root_metadata(
- RootMetadata.FromString(incoming.serialized_root_metadata))
+ RootMetadata.FromString(incoming.serialized_root_metadata)
+ )
if lint_errors:
log_progress(f'Lint errors: {lint_errors}')
raise VerificationError('Malformed root metadata.')
@@ -173,7 +185,8 @@ def verify_root_metadata(incoming: SignedRootMetadata,
# target root metadata.
log_progress('Checking signatures against current root')
verify_root_metadata_signatures(
- incoming, RootMetadata.FromString(incoming.serialized_root_metadata))
+ incoming, RootMetadata.FromString(incoming.serialized_root_metadata)
+ )
# Check rollback attack.
log_progress('Checking for version rollback')
@@ -182,14 +195,16 @@ def verify_root_metadata(incoming: SignedRootMetadata,
cur_ver = trusted.common_metadata.version
if new_ver < cur_ver:
raise VerificationError(
- f'Root metadata version rollback ({cur_ver}->{new_ver}) detected!')
+ f'Root metadata version rollback ({cur_ver}->{new_ver}) detected!'
+ )
# Any signature requirement change indicates a targets key rotation.
new_sig_req = incoming_meta.targets_signature_requirement
cur_sig_req = trusted.targets_signature_requirement
targets_key_rotated = not (
set(new_sig_req.key_ids) == set(cur_sig_req.key_ids)
- and new_sig_req.threshold == cur_sig_req.threshold)
+ and new_sig_req.threshold == cur_sig_req.threshold
+ )
log_progress(f'Targets key rotation: {targets_key_rotated}')
return targets_key_rotated
@@ -207,7 +222,8 @@ def lint_targets_metadata(meta: TargetsMetadata) -> Iterable[str]:
log_progress("Checking role type")
if meta.common_metadata.role != metadata.RoleType.TARGETS.value:
errors.append(
- f'Role type is not "targets" but "{meta.common_metadata.role}"')
+ f'Role type is not "targets" but "{meta.common_metadata.role}"'
+ )
for file in meta.target_files:
if not file.file_name:
@@ -218,8 +234,9 @@ def lint_targets_metadata(meta: TargetsMetadata) -> Iterable[str]:
return errors
-def verify_targets_metadata(signed: SignedTargetsMetadata,
- root: RootMetadata) -> None:
+def verify_targets_metadata(
+ signed: SignedTargetsMetadata, root: RootMetadata
+) -> None:
"""Verifies a targets metadata is sufficiently signed and well-formed.
Raises:
@@ -227,8 +244,10 @@ def verify_targets_metadata(signed: SignedTargetsMetadata,
malformed.
"""
sig_requirement = root.targets_signature_requirement
- log_progress(f'Checking signatures: total={len(signed.signatures)}, '
- f'threshold={sig_requirement.threshold}')
+ log_progress(
+ f'Checking signatures: total={len(signed.signatures)}, '
+ f'threshold={sig_requirement.threshold}'
+ )
good_signatures_count = 0
for sig in signed.signatures:
# Ignore extraneous signatures.
@@ -245,12 +264,15 @@ def verify_targets_metadata(signed: SignedTargetsMetadata,
break
if not key:
raise VerificationError(
- f'No such key_id in root: {sig.key_id.hex()}.')
+ f'No such key_id in root: {sig.key_id.hex()}.'
+ )
if not keys.verify_ecdsa_signature(
- sig=sig.sig, data=signed.serialized_targets_metadata, key=key):
+ sig=sig.sig, data=signed.serialized_targets_metadata, key=key
+ ):
raise VerificationError(
- f'Invalid signature, key_id={sig.key_id.hex()}.')
+ f'Invalid signature, key_id={sig.key_id.hex()}.'
+ )
good_signatures_count += 1
@@ -258,11 +280,13 @@ def verify_targets_metadata(signed: SignedTargetsMetadata,
if good_signatures_count < sig_requirement.threshold:
raise VerificationError(
f'Not enough good signatures: {good_signatures_count} < '
- f'{sig_requirement.threshold}.')
+ f'{sig_requirement.threshold}.'
+ )
log_progress('Checking content')
lint_errors = lint_targets_metadata(
- TargetsMetadata.FromString(signed.serialized_targets_metadata))
+ TargetsMetadata.FromString(signed.serialized_targets_metadata)
+ )
if lint_errors:
log_progress(f'Lint errors: {lint_errors}')
raise VerificationError('Malformed targets metadata.')
@@ -278,7 +302,8 @@ def verify_bundle(incoming: UpdateBundle, trusted: UpdateBundle) -> None:
if not trusted.HasField('root_metadata'):
raise VerificationError('Trusted bundle missing root metadata')
trusted_root = RootMetadata.FromString(
- trusted.root_metadata.serialized_root_metadata)
+ trusted.root_metadata.serialized_root_metadata
+ )
# Check the contents of the trusted root metadata. This is optional
# in practice as we generally trust what is provisioned in the factory.
@@ -299,11 +324,13 @@ def verify_bundle(incoming: UpdateBundle, trusted: UpdateBundle) -> None:
incoming_root = incoming.root_metadata
if incoming_root:
log_progress('Verifying incoming root metadata')
- targets_key_rotated = verify_root_metadata(incoming=incoming_root,
- trusted=trusted_root)
+ targets_key_rotated = verify_root_metadata(
+ incoming=incoming_root, trusted=trusted_root
+ )
log_progress('Upgrading trust to the incoming root metadata')
trusted_root = RootMetadata.FromString(
- incoming_root.serialized_root_metadata)
+ incoming_root.serialized_root_metadata
+ )
log_progress('Verifying targets metadata')
signed_targets_metadata = incoming.targets_metadata['targets']
@@ -312,7 +339,8 @@ def verify_bundle(incoming: UpdateBundle, trusted: UpdateBundle) -> None:
# Unless the targets signing key has been rotated, check for version
# rollback attack.
targets_metadata = TargetsMetadata.FromString(
- signed_targets_metadata.serialized_targets_metadata)
+ signed_targets_metadata.serialized_targets_metadata
+ )
if not targets_key_rotated:
log_progress('Checking targets metadata for version rollback')
new_ver = targets_metadata.common_metadata.version
@@ -321,8 +349,8 @@ def verify_bundle(incoming: UpdateBundle, trusted: UpdateBundle) -> None:
).common_metadata.version
if new_ver < cur_ver:
raise VerificationError(
- f'Targets metadata rolling back: {cur_ver} '
- f'-> {new_ver}.')
+ f'Targets metadata rolling back: {cur_ver} ' f'-> {new_ver}.'
+ )
# Verify all files listed in the targets metadata exist along with the
# correct sizes and hashes.
@@ -331,33 +359,41 @@ def verify_bundle(incoming: UpdateBundle, trusted: UpdateBundle) -> None:
payload = incoming.target_payloads[file.file_name]
if file.length != len(payload):
- raise VerificationError(f'Wrong file size for {file.file_name}: '
- f'expected: {file.length}, '
- f'got: {len(payload)}.')
+ raise VerificationError(
+ f'Wrong file size for {file.file_name}: '
+ f'expected: {file.length}, '
+ f'got: {len(payload)}.'
+ )
if not file.hashes:
raise VerificationError(f'Missing hashes for: {file.file_name}.')
calculated_hashes = metadata.gen_hashes(
- payload, [h.function for h in file.hashes])
+ payload, [h.function for h in file.hashes]
+ )
if list(calculated_hashes) != list(file.hashes):
- raise VerificationError(
- f'Mismatched hashes for: {file.file_name}.')
+ raise VerificationError(f'Mismatched hashes for: {file.file_name}.')
def parse_args():
"""Parse CLI arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--incoming',
- type=Path,
- required=True,
- help='Path to the TUF bundle to be verified')
-
- parser.add_argument('--trusted',
- type=Path,
- help=('Path to the TUF bundle to be trusted; '
- 'defaults to the value of `--incoming` '
- 'if unspecified.'))
+ parser.add_argument(
+ '--incoming',
+ type=Path,
+ required=True,
+ help='Path to the TUF bundle to be verified',
+ )
+
+ parser.add_argument(
+ '--trusted',
+ type=Path,
+ help=(
+ 'Path to the TUF bundle to be trusted; '
+ 'defaults to the value of `--incoming` '
+ 'if unspecified.'
+ ),
+ )
return parser.parse_args()
@@ -375,7 +411,7 @@ def main(incoming: Path, trusted: Path) -> int:
log_progress(f'Verifying: {incoming}')
incoming_bundle = UpdateBundle.FromString(incoming.read_bytes())
- is_self_verification = (not trusted)
+ is_self_verification = not trusted
if is_self_verification:
trusted_bundle = incoming_bundle
log_progress('(self-verification)')
diff --git a/pw_software_update/py/remote_sign_test.py b/pw_software_update/py/remote_sign_test.py
new file mode 100644
index 000000000..78d6df4fa
--- /dev/null
+++ b/pw_software_update/py/remote_sign_test.py
@@ -0,0 +1,129 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Unit tests for pw_software_update.remote_sign module."""
+
+import os
+from pathlib import Path
+import tempfile
+import textwrap
+import unittest
+from unittest import mock
+
+from google.cloud.storage import Blob # type: ignore
+from google.cloud.storage.bucket import Bucket # type: ignore
+
+from pw_software_update import remote_sign
+
+# inclusive-language: disable
+FAKE_BUILDER_KEY = textwrap.dedent(
+ """\
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEpAIBAAKCAQEA4qEQSHM0QpWEhTvhWMBahS7wbTIihaiRpUQC8+hEkmHhoJQy
+ zaNR3CKdYWnJ1bAjdBT1HTHznbYSBasFAUKPiB16K/akuKSPnwHG9OM6+8Psw7lt
+ GLP2jP65HE4a8n9lGas399xIK4hxZJkV2BXocociXVEVB3nzzNk1AQZdJxik/ToL
+ MYC2EKTu1kdt+OLl56/O1Mq9p8V7u2G1l8fqHtJi4Z34LzUzIoyFf7+bSmZBcHG1
+ F/QdjbHb4temShDzptOM1VfXZchYTDVnbNsmR7TP2B857agog4rhqtVlPvHqFial
+ WEU1WmAQz+oYqtRikUVWHq10SACxo6MFoM7LqQIDAQABAoIBAQCjrdoZyYLUGDLn
+ G1FtDTgTesxQwWXnjNDsQMu1J2rnImSX2pE6rhtAV4u9QG9ys01X2I8Tr/EYVdh8
+ WYE64LzTfR6ww+lCJjBIkjsEwVznWyEUV0bxEYEfYhWF2O9jdxkoyd2ZWXKSZnAn
+ TN1W/LOui+UI6re6d5zatYGvpM4AnlMTmwcO5aPQqTZMBOqJQZgEgyyHH2DZpIRI
+ L7dG/k9Y/ML4T+hSVvi84+NS7GyTajPtaNRoVnlwr9+QVKplIgT8ZSqAF7unBsmF
+ +s/U+TCFKq0pOhamOVz8eVd/uusy0d7a2oomtKoIzcPd74J5KZMub8izmEOwp5TZ
+ 17rsBDuVAoGBAPJMv737tYf5T5ihwEJ84OWxq3qLwWSoNwezOF3iWi4r42+QoqsC
+ F0dLlgTmsafNTwVP1ztoeGvvezXSUfKfMTjjaZDRB226gwW7+eZ0MbrVcEnI9wm5
+ K9MOWut40KsoAHHGs5sLyAtIENwnPAkPQPwPxmEcUxJJZwI/Rq78zR1bAoGBAO9x
+ fAi9M8VdbV3r/l1SnExRlTu2gp9Rv02Zy78HVOWEWdEn6mhG3to5mXsHNIQwLulQ
+ jm/hBSme5g4xbSCL/qCqRkc1rRautq/W50G8h7S3+KFGdnzXdEYnEh4oh5t5PJtA
+ LHWc2WhTe5bsBNwOR1xQ8bEm+V/lf4Fbq+jOcjZLAoGBALzKgDwPfApOf2512c/0
+ bWeLYAlEC5PaXcZqJmlAjPOczsGG+Lg2EN1ET8fR2Gre1ctVwmZPqESxfFcbYS6i
+ S0AAMajcteURhjVZmgWuU3E4DR3wsEurNDJm5QDEShKSQIZmRFtyepQPutNO3sBQ
+ WloMEI5p+3AsMU7W7sQ5xbgxAoGABWwAbwI5xeJTs6jAXcSdHW1Lf8qmMo1bU5qD
+ 7pNv7LKOhhntSOcx7KcZPpvvKH8e0NGuKAJkZ4jdlLyxx+bjoSe556rjfHwATwMC
+ wY5PVFxGGQDLdhA65cvEsUIhr/eS08EkQJWIpsAdMFGv2nvISeLbVjOXugAsXvWA
+ cwkZtPkCgYBQJPRCGBz73lBFB16oYJR6AnC32nf0WbrnMLFwMejUrsTLN6M72axw
+ bWFpW7rWV7ldSgxKJ6ucKWl78uUMUnkM+CGPt5WJiisZXM2X1Q8+V7fmdAtK/AFj
+ /tbEFpftkCyIM1nGZwZ/ziPF4n5hzGGF6w/ZMWZkwFZlqxlejK9IfQ==
+ -----END RSA PRIVATE KEY-----
+"""
+)
+
+FAKE_BUILDER_PUBLIC_KEY = textwrap.dedent(
+ """\
+ -----BEGIN PUBLIC KEY-----
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4qEQSHM0QpWEhTvhWMBa
+ hS7wbTIihaiRpUQC8+hEkmHhoJQyzaNR3CKdYWnJ1bAjdBT1HTHznbYSBasFAUKP
+ iB16K/akuKSPnwHG9OM6+8Psw7ltGLP2jP65HE4a8n9lGas399xIK4hxZJkV2BXo
+ cociXVEVB3nzzNk1AQZdJxik/ToLMYC2EKTu1kdt+OLl56/O1Mq9p8V7u2G1l8fq
+ HtJi4Z34LzUzIoyFf7+bSmZBcHG1F/QdjbHb4temShDzptOM1VfXZchYTDVnbNsm
+ R7TP2B857agog4rhqtVlPvHqFialWEU1WmAQz+oYqtRikUVWHq10SACxo6MFoM7L
+ qQIDAQAB
+ -----END PUBLIC KEY-----
+"""
+)
+
+# inclusive-language: enable
+
+
+# TODO(b/235240430): Improve unit test coverage.
+class PathSigningTest(unittest.TestCase):
+ """Tests the signing of bundles by path."""
+
+ @classmethod
+ def setUpClass(cls):
+ cls.tempdir = tempfile.mkdtemp()
+
+ cls.builder_key = Path(cls.tempdir) / 'fake_builder_key'
+ cls.builder_key.write_text(FAKE_BUILDER_KEY)
+
+ cls.builder_pub_key = Path(cls.tempdir) / 'fake_builder_pub_key.pem'
+ cls.builder_pub_key.write_text(FAKE_BUILDER_PUBLIC_KEY)
+
+ cls.bundle = Path(cls.tempdir) / 'fake_bundle'
+ cls.bundle.write_bytes(b'FAKE BUNDLE CONTENTS\n')
+
+ def test_bundle_blob_uploads(self):
+ """Signing should upload the bundle, pub key, and signing request."""
+ mock_blob = mock.create_autospec(Blob, instance=True)
+ mock_blob.exists = mock.MagicMock(return_value=False)
+ mock_in_bucket = mock.create_autospec(Bucket, instance=True)
+ mock_in_bucket.blob = mock.MagicMock(return_value=mock_blob)
+ mock_out_bucket = mock.create_autospec(Bucket, instance=True)
+ mock_out_bucket.name = 'fake_out_bucket'
+ client = remote_sign.RemoteSignClient(
+ input_bucket=mock_in_bucket, output_bucket=mock_out_bucket
+ )
+
+ client.sign(
+ self.bundle,
+ signing_key_name='fake_key',
+ builder_key=self.builder_key,
+ builder_public_key=self.builder_pub_key,
+ request_blob_name='signing_request.json',
+ timeout_s=0.1,
+ )
+
+ mock_blob.upload_from_filename.assert_has_calls(
+ [
+ mock.call(os.path.join(self.tempdir, 'fake_bundle')),
+ mock.call(
+ os.path.join(self.tempdir, 'fake_builder_pub_key.pem')
+ ),
+ ],
+ any_order=True,
+ )
+ mock_blob.upload_from_string.assert_called_once()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_software_update/py/root_metadata_test.py b/pw_software_update/py/root_metadata_test.py
index a9afeebdd..976987509 100644
--- a/pw_software_update/py/root_metadata_test.py
+++ b/pw_software_update/py/root_metadata_test.py
@@ -16,44 +16,55 @@
import unittest
from pw_software_update import metadata
-from pw_software_update.root_metadata import (RootKeys, TargetsKeys,
- gen_root_metadata)
+from pw_software_update.root_metadata import (
+ RootKeys,
+ TargetsKeys,
+ gen_root_metadata,
+)
class GenRootMetadataTest(unittest.TestCase):
"""Test the generation of root metadata."""
+
def setUp(self):
self.root_key_public = (
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4XWOT3o27TNqeh7YF7P+2ErLzzFm'
b'c/VItYABCqw7Hh5z8wtNjGyo0GnUSBWeISg3LMs/WjOkCiwjawjqmI8OrQ=='
- b'\n-----END PUBLIC KEY-----\n')
+ b'\n-----END PUBLIC KEY-----\n'
+ )
self.targets_key_public = (
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9UM6qRZJ0gIWwLjo8tjbrrBTlKXg'
b'ukwVjOlnguSSiYMrN4MDqMlNDnaJgLvcCuiNUKHu9Oj1DG1i6ckNdE4VTA=='
- b'\n-----END PUBLIC KEY-----\n')
+ b'\n-----END PUBLIC KEY-----\n'
+ )
def test_multiple_keys(self) -> None:
"""Checks that multiple keys generates multiple KeyMappings and
SignatureRequirements."""
- root_metadata = gen_root_metadata(RootKeys([self.root_key_public]),
- TargetsKeys(
- [self.targets_key_public]),
- version=42)
+ root_metadata = gen_root_metadata(
+ RootKeys([self.root_key_public]),
+ TargetsKeys([self.targets_key_public]),
+ version=42,
+ )
self.assertEqual(len(root_metadata.keys), 2)
- self.assertEqual(len(root_metadata.root_signature_requirement.key_ids),
- 1)
+ self.assertEqual(
+ len(root_metadata.root_signature_requirement.key_ids), 1
+ )
self.assertEqual(root_metadata.root_signature_requirement.threshold, 1)
self.assertEqual(
- len(root_metadata.targets_signature_requirement.key_ids), 1)
- self.assertEqual(root_metadata.targets_signature_requirement.threshold,
- 1)
+ len(root_metadata.targets_signature_requirement.key_ids), 1
+ )
+ self.assertEqual(
+ root_metadata.targets_signature_requirement.threshold, 1
+ )
self.assertEqual(root_metadata.common_metadata.version, 42)
- self.assertEqual(root_metadata.common_metadata.role,
- metadata.RoleType.ROOT.value)
+ self.assertEqual(
+ root_metadata.common_metadata.role, metadata.RoleType.ROOT.value
+ )
if __name__ == '__main__':
diff --git a/pw_software_update/py/update_bundle_test.py b/pw_software_update/py/update_bundle_test.py
index a4279f6f9..4fef32ed6 100644
--- a/pw_software_update/py/update_bundle_test.py
+++ b/pw_software_update/py/update_bundle_test.py
@@ -23,6 +23,7 @@ from pw_software_update.tuf_pb2 import SignedRootMetadata, TargetsMetadata
class TargetsFromDirectoryTest(unittest.TestCase):
"""Test turning a directory into TUF targets."""
+
def test_excludes(self):
"""Checks that excludes are excluded."""
with tempfile.TemporaryDirectory() as tempdir_name:
@@ -35,7 +36,8 @@ class TargetsFromDirectoryTest(unittest.TestCase):
path.touch()
targets = update_bundle.targets_from_directory(
- temp_root, exclude=(Path('foo.bin'), Path('baz.bin')))
+ temp_root, exclude=(Path('foo.bin'), Path('baz.bin'))
+ )
self.assertNotIn('foo.bin', targets)
self.assertEqual(bar_path, targets['bar.bin'])
@@ -59,9 +61,8 @@ class TargetsFromDirectoryTest(unittest.TestCase):
path.touch()
targets = update_bundle.targets_from_directory(
- temp_root,
- exclude=(Path('qux.exe'), ),
- remap_paths=remap_paths)
+ temp_root, exclude=(Path('qux.exe'),), remap_paths=remap_paths
+ )
self.assertEqual(foo_path, targets['main'])
self.assertEqual(bar_path, targets['backup'])
@@ -81,11 +82,13 @@ class TargetsFromDirectoryTest(unittest.TestCase):
with self.assertLogs(level='WARNING') as log:
update_bundle.targets_from_directory(
temp_root,
- exclude=(Path('qux.exe'), ),
- remap_paths=remap_paths)
+ exclude=(Path('qux.exe'),),
+ remap_paths=remap_paths,
+ )
- self.assertIn('Some remaps defined, but not "bar.bin"',
- log.output[0])
+ self.assertIn(
+ 'Some remaps defined, but not "bar.bin"', log.output[0]
+ )
def test_remap_of_missing_file(self):
"""Checks that remapping a missing file raises an error."""
@@ -99,12 +102,14 @@ class TargetsFromDirectoryTest(unittest.TestCase):
}
with self.assertRaises(FileNotFoundError):
- update_bundle.targets_from_directory(temp_root,
- remap_paths=remap_paths)
+ update_bundle.targets_from_directory(
+ temp_root, remap_paths=remap_paths
+ )
class GenUnsignedUpdateBundleTest(unittest.TestCase):
"""Test the generation of unsigned update bundles."""
+
def test_bundle_generation(self):
"""Tests basic creation of an UpdateBundle."""
with tempfile.TemporaryDirectory() as tempdir_name:
@@ -134,17 +139,22 @@ class GenUnsignedUpdateBundleTest(unittest.TestCase):
targets,
targets_metadata_version=42,
root_metadata=SignedRootMetadata(
- serialized_root_metadata=serialized_root_metadata_bytes))
+ serialized_root_metadata=serialized_root_metadata_bytes
+ ),
+ )
self.assertEqual(foo_bytes, bundle.target_payloads['foo'])
self.assertEqual(bar_bytes, bundle.target_payloads['bar'])
self.assertEqual(baz_bytes, bundle.target_payloads['baz'])
self.assertEqual(qux_bytes, bundle.target_payloads['qux'])
targets_metadata = TargetsMetadata.FromString(
- bundle.targets_metadata['targets'].serialized_targets_metadata)
+ bundle.targets_metadata['targets'].serialized_targets_metadata
+ )
self.assertEqual(targets_metadata.common_metadata.version, 42)
- self.assertEqual(serialized_root_metadata_bytes,
- bundle.root_metadata.serialized_root_metadata)
+ self.assertEqual(
+ serialized_root_metadata_bytes,
+ bundle.root_metadata.serialized_root_metadata,
+ )
def test_persist_to_disk(self):
"""Tests persisting the TUF repo to disk for debugging"""
@@ -171,22 +181,26 @@ class GenUnsignedUpdateBundleTest(unittest.TestCase):
}
persist_path = temp_root / 'persisted'
- update_bundle.gen_unsigned_update_bundle(targets,
- persist=persist_path)
+ update_bundle.gen_unsigned_update_bundle(
+ targets, persist=persist_path
+ )
self.assertEqual(foo_bytes, (persist_path / 'foo').read_bytes())
self.assertEqual(bar_bytes, (persist_path / 'bar').read_bytes())
self.assertEqual(baz_bytes, (persist_path / 'baz').read_bytes())
- self.assertEqual(qux_bytes,
- (persist_path / 'subdir' / 'qux').read_bytes())
+ self.assertEqual(
+ qux_bytes, (persist_path / 'subdir' / 'qux').read_bytes()
+ )
class ParseTargetArgTest(unittest.TestCase):
"""Test the parsing of target argument strings."""
+
def test_valid_arg(self):
"""Checks that valid remap strings are parsed correctly."""
file_path, target_name = update_bundle.parse_target_arg(
- 'foo.bin > main')
+ 'foo.bin > main'
+ )
self.assertEqual(Path('foo.bin'), file_path)
self.assertEqual('main', target_name)
diff --git a/pw_software_update/py/verify_test.py b/pw_software_update/py/verify_test.py
index a19cee0c5..0295d7d81 100644
--- a/pw_software_update/py/verify_test.py
+++ b/pw_software_update/py/verify_test.py
@@ -16,7 +16,7 @@
from dataclasses import dataclass
from pathlib import Path
import tempfile
-from typing import NamedTuple
+from typing import NamedTuple, Optional
import unittest
from pw_software_update import dev_sign, root_metadata, update_bundle
@@ -25,8 +25,10 @@ from pw_software_update.tuf_pb2 import SignedRootMetadata
from pw_software_update.update_bundle_pb2 import UpdateBundle
-def gen_unsigned_bundle(signed_root_metadata: SignedRootMetadata = None,
- targets_metadata_version: int = 0) -> UpdateBundle:
+def gen_unsigned_bundle(
+ signed_root_metadata: Optional[SignedRootMetadata] = None,
+ targets_metadata_version: int = 0,
+) -> UpdateBundle:
"""Generates an unsigned test bundle."""
with tempfile.TemporaryDirectory() as tempdir_name:
targets_root = Path(tempdir_name)
@@ -52,11 +54,13 @@ def gen_unsigned_bundle(signed_root_metadata: SignedRootMetadata = None,
return update_bundle.gen_unsigned_update_bundle(
targets,
root_metadata=signed_root_metadata,
- targets_metadata_version=targets_metadata_version)
+ targets_metadata_version=targets_metadata_version,
+ )
class TestKey(NamedTuple):
"""A test key pair"""
+
public: bytes
private: bytes
@@ -64,6 +68,7 @@ class TestKey(NamedTuple):
@dataclass
class BundleOptions:
"""Parameters used in test bundle generations."""
+
root_key_version: int = 0
root_metadata_version: int = 0
targets_key_version: int = 0
@@ -74,99 +79,114 @@ def gen_signed_bundle(options: BundleOptions) -> UpdateBundle:
"""Generates a test bundle per given options."""
# Root keys look up table: version->TestKey
root_keys = {
- 0:
- TestKey(
+ 0: TestKey(
private=(
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyk3DEQdl'
b'346MS5N/quNEneJa4HxkJBETGzlEEKkCmZOhRANCAAThdY5PejbtM2p6'
b'HtgXs/7YSsvPMWZz9Ui1gAEKrDseHnPzC02MbKjQadRIFZ4hKDcsyz9a'
b'M6QKLCNrCOqYjw6t'
- b'\n-----END PRIVATE KEY-----\n'),
+ b'\n-----END PRIVATE KEY-----\n'
+ ),
public=(
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4XWOT3o27TNqeh7YF7P+2'
b'ErLzzFmc/VItYABCqw7Hh5z8wtNjGyo0GnUSBWeISg3LMs/WjOkCiwjaw'
b'jqmI8OrQ=='
- b'\n-----END PUBLIC KEY-----\n')),
- 1:
- TestKey(
+ b'\n-----END PUBLIC KEY-----\n'
+ ),
+ ),
+ 1: TestKey(
private=(
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgE3MRbMxo'
b'Gv3I/Ok/0qE8GV/mQuIbZo9kk+AsJnYetQ6hRANCAAQ5UhycwdcfYe34'
b'NpmG32t0klnKlrUbk3LyvYLq5uDWG2MfP3L0ciNFsEnW7vHpqqjKsoru'
b'Qt30G10K7D+reC77'
- b'\n-----END PRIVATE KEY-----\n'),
+ b'\n-----END PRIVATE KEY-----\n'
+ ),
public=(
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOVIcnMHXH2Ht+DaZht9rd'
b'JJZypa1G5Ny8r2C6ubg1htjHz9y9HIjRbBJ1u7x6aqoyrKK7kLd9BtdCu'
b'w/q3gu+w=='
- b'\n-----END PUBLIC KEY-----\n'))
+ b'\n-----END PUBLIC KEY-----\n'
+ ),
+ ),
}
# Targets keys look up table: version->TestKey
targets_keys = {
- 0:
- TestKey(
+ 0: TestKey(
private=(
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgkMEZ0u84'
b'HzC51nhhf2ZykPj6WfAjBxXVWndjVdn6bh6hRANCAAT1QzqpFknSAhbA'
b'uOjy2NuusFOUpeC6TBWM6WeC5JKJgys3gwOoyU0OdomAu9wK6I1Qoe70'
b'6PUMbWLpyQ10ThVM'
- b'\n-----END PRIVATE KEY-----\n'),
+ b'\n-----END PRIVATE KEY-----\n'
+ ),
public=(
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9UM6qRZJ0gIWwLjo8tjbr'
b'rBTlKXgukwVjOlnguSSiYMrN4MDqMlNDnaJgLvcCuiNUKHu9Oj1DG1i6c'
b'kNdE4VTA=='
- b'\n-----END PUBLIC KEY-----\n')),
- 1:
- TestKey(
+ b'\n-----END PUBLIC KEY-----\n'
+ ),
+ ),
+ 1: TestKey(
private=(
b'-----BEGIN PRIVATE KEY-----\n'
b'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+Q+u2KoO'
b'CwpY1HEKDTIjQXmTlxhoo3gVkE7nrtHhMemhRANCAASgc+0AHCfUxoHy'
b'+ZkSslLvMufiDqGPABvfuKzHd0wUWs2Y0eIvQc7tsBP0bcuJsFuxvL6a'
b'8Ek7y3kUmFWVL01v'
- b'\n-----END PRIVATE KEY-----\n'),
+ b'\n-----END PRIVATE KEY-----\n'
+ ),
public=(
b'-----BEGIN PUBLIC KEY-----\n'
b'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoHPtABwn1MaB8vmZErJS7'
b'zLn4g6hjwAb37isx3dMFFrNmNHiL0HO7bAT9G3LibBbsby+mvBJO8t5FJ'
b'hVlS9Nbw=='
- b'\n-----END PUBLIC KEY-----\n'))
+ b'\n-----END PUBLIC KEY-----\n'
+ ),
+ ),
}
unsigned_root = root_metadata.gen_root_metadata(
root_metadata.RootKeys([root_keys[options.root_key_version].public]),
root_metadata.TargetsKeys(
- [targets_keys[options.targets_key_version].public]),
- version=options.root_metadata_version)
+ [targets_keys[options.targets_key_version].public]
+ ),
+ version=options.root_metadata_version,
+ )
serialized_root = unsigned_root.SerializeToString()
signed_root = SignedRootMetadata(serialized_root_metadata=serialized_root)
signed_root = dev_sign.sign_root_metadata(
- signed_root, root_keys[options.root_key_version].private)
+ signed_root, root_keys[options.root_key_version].private
+ )
# Additionaly sign the root metadata with the previous version of root key
# to enable upgrading from the previous root.
if options.root_key_version > 0:
signed_root = dev_sign.sign_root_metadata(
- signed_root, root_keys[options.root_key_version - 1].private)
+ signed_root, root_keys[options.root_key_version - 1].private
+ )
unsigned_bundle = gen_unsigned_bundle(
signed_root_metadata=signed_root,
- targets_metadata_version=options.targets_metadata_version)
+ targets_metadata_version=options.targets_metadata_version,
+ )
signed_bundle = dev_sign.sign_update_bundle(
- unsigned_bundle, targets_keys[options.targets_key_version].private)
+ unsigned_bundle, targets_keys[options.targets_key_version].private
+ )
return signed_bundle
class VerifyBundleTest(unittest.TestCase):
"""Bundle verification test cases."""
+
def test_self_verification(self): # pylint: disable=no-self-use
incoming = gen_signed_bundle(BundleOptions())
verify_bundle(incoming, trusted=incoming)
@@ -184,9 +204,11 @@ class VerifyBundleTest(unittest.TestCase):
def test_root_metadata_anti_rollback_with_key_rotation(self):
trusted = gen_signed_bundle(
- BundleOptions(root_key_version=0, root_metadata_version=1))
+ BundleOptions(root_key_version=0, root_metadata_version=1)
+ )
incoming = gen_signed_bundle(
- BundleOptions(root_key_version=1, root_metadata_version=0))
+ BundleOptions(root_key_version=1, root_metadata_version=0)
+ )
# Anti-rollback enforced regardless of key rotation.
with self.assertRaises(VerificationError):
verify_bundle(incoming, trusted)
@@ -210,10 +232,12 @@ class VerifyBundleTest(unittest.TestCase):
def test_targets_fastforward_recovery(self): # pylint: disable=no-self-use
trusted = gen_signed_bundle(
- BundleOptions(targets_key_version=0, targets_metadata_version=999))
+ BundleOptions(targets_key_version=0, targets_metadata_version=999)
+ )
# Revoke key and bring back the metadata version.
incoming = gen_signed_bundle(
- BundleOptions(targets_key_version=1, targets_metadata_version=0))
+ BundleOptions(targets_key_version=1, targets_metadata_version=0)
+ )
# Anti-rollback is not enforced upon key rotation.
verify_bundle(incoming, trusted)
diff --git a/pw_software_update/tuf.proto b/pw_software_update/tuf.proto
index 74cfe3be4..05baed4c9 100644
--- a/pw_software_update/tuf.proto
+++ b/pw_software_update/tuf.proto
@@ -181,8 +181,8 @@ message TargetsMetadata {
// the file lives relative to the base directory of the repository, e.g.
// "path/to/amber_tools/0".
- // TODO: When it is time to support delegation, add delegation information
- // here.
+ // TODO(davidrogers): When it is time to support delegation, add delegation
+ // information here.
// This is NOT a part of the TUF Specification.
reserved 9 to 31; // Reserved for TUF Specification changes.
diff --git a/pw_software_update/update_bundle_accessor.cc b/pw_software_update/update_bundle_accessor.cc
index 453f932e8..1544cbb93 100644
--- a/pw_software_update/update_bundle_accessor.cc
+++ b/pw_software_update/update_bundle_accessor.cc
@@ -39,7 +39,7 @@ namespace {
Result<bool> VerifyEcdsaSignature(protobuf::Bytes public_key,
ConstByteSpan digest,
protobuf::Bytes signature) {
- // TODO(pwbug/456): Move this logic into an variant of the API in
+ // TODO(b/237580538): Move this logic into an variant of the API in
// pw_crypto:ecdsa that takes readers as inputs.
std::byte public_key_bytes[65];
std::byte signature_bytes[64];
@@ -81,13 +81,13 @@ Status VerifyMetadataSignatures(protobuf::Bytes message,
// Gets the threshold -- at least `threshold` number of signatures must
// pass verification in order to trust this metadata.
protobuf::Uint32 threshold = signature_requirement.AsUint32(
- static_cast<uint32_t>(SignatureRequirement::Fields::THRESHOLD));
+ static_cast<uint32_t>(SignatureRequirement::Fields::kThreshold));
PW_TRY(threshold.status());
// Gets the ids of keys that are allowed for verifying the signatures.
protobuf::RepeatedBytes allowed_key_ids =
signature_requirement.AsRepeatedBytes(
- static_cast<uint32_t>(SignatureRequirement::Fields::KEY_IDS));
+ static_cast<uint32_t>(SignatureRequirement::Fields::kKeyIds));
PW_TRY(allowed_key_ids.status());
// Verifies the signatures. Check that at least `threshold` number of
@@ -97,7 +97,7 @@ Status VerifyMetadataSignatures(protobuf::Bytes message,
for (protobuf::Message signature : signatures) {
total_signatures++;
protobuf::Bytes key_id =
- signature.AsBytes(static_cast<uint32_t>(Signature::Fields::KEY_ID));
+ signature.AsBytes(static_cast<uint32_t>(Signature::Fields::kKeyId));
PW_TRY(key_id.status());
// Reads the key id into a buffer, so that we can check whether it is
@@ -131,7 +131,7 @@ Status VerifyMetadataSignatures(protobuf::Bytes message,
// Retrieves the signature bytes.
protobuf::Bytes sig =
- signature.AsBytes(static_cast<uint32_t>(Signature::Fields::SIG));
+ signature.AsBytes(static_cast<uint32_t>(Signature::Fields::kSig));
PW_TRY(sig.status());
// Extracts the key type, scheme and value information.
@@ -141,7 +141,7 @@ Status VerifyMetadataSignatures(protobuf::Bytes message,
PW_TRY(key_info.status());
protobuf::Bytes key_val =
- key_info.AsBytes(static_cast<uint32_t>(Key::Fields::KEYVAL));
+ key_info.AsBytes(static_cast<uint32_t>(Key::Fields::kKeyval));
PW_TRY(key_val.status());
// The function assume that all keys are ECDSA keys. This is guaranteed
@@ -166,8 +166,8 @@ Status VerifyMetadataSignatures(protobuf::Bytes message,
return Status::NotFound();
}
- PW_LOG_ERROR("Insufficient signatures. Requires at least %u, verified %u",
- threshold.value(),
+ PW_LOG_ERROR("Insufficient signatures. Requires at least %u, verified %zu",
+ static_cast<unsigned>(threshold.value()),
verified_count);
return Status::Unauthenticated();
}
@@ -182,27 +182,27 @@ Result<bool> VerifyRootMetadataSignatures(protobuf::Message trusted_root,
protobuf::Message new_root) {
// Retrieves the trusted root metadata content message.
protobuf::Message trusted = trusted_root.AsMessage(static_cast<uint32_t>(
- SignedRootMetadata::Fields::SERIALIZED_ROOT_METADATA));
+ SignedRootMetadata::Fields::kSerializedRootMetadata));
PW_TRY(trusted.status());
// Retrieves the serialized new root metadata bytes.
protobuf::Bytes serialized = new_root.AsBytes(static_cast<uint32_t>(
- SignedRootMetadata::Fields::SERIALIZED_ROOT_METADATA));
+ SignedRootMetadata::Fields::kSerializedRootMetadata));
PW_TRY(serialized.status());
// Gets the key mapping from the trusted root metadata.
protobuf::StringToMessageMap key_mapping = trusted.AsStringToMessageMap(
- static_cast<uint32_t>(RootMetadata::Fields::KEYS));
+ static_cast<uint32_t>(RootMetadata::Fields::kKeys));
PW_TRY(key_mapping.status());
// Gets the signatures of the new root.
protobuf::RepeatedMessages signatures = new_root.AsRepeatedMessages(
- static_cast<uint32_t>(SignedRootMetadata::Fields::SIGNATURES));
+ static_cast<uint32_t>(SignedRootMetadata::Fields::kSignatures));
PW_TRY(signatures.status());
// Gets the signature requirement from the trusted root metadata.
protobuf::Message signature_requirement = trusted.AsMessage(
- static_cast<uint32_t>(RootMetadata::Fields::ROOT_SIGNATURE_REQUIREMENT));
+ static_cast<uint32_t>(RootMetadata::Fields::kRootSignatureRequirement));
PW_TRY(signature_requirement.status());
// Verifies the signatures.
@@ -228,20 +228,20 @@ Result<uint32_t> GetMetadataVersion(protobuf::Message& metadata,
metadata.AsMessage(common_metatdata_field_number);
PW_TRY(common_metadata.status());
protobuf::Uint32 res = common_metadata.AsUint32(
- static_cast<uint32_t>(software_update::CommonMetadata::Fields::VERSION));
+ static_cast<uint32_t>(software_update::CommonMetadata::Fields::kVersion));
PW_TRY(res.status());
return res.value();
}
// Reads a protobuf::String into a buffer and returns a std::string_view.
Result<std::string_view> ReadProtoString(protobuf::String str,
- std::span<char> buffer) {
+ span<char> buffer) {
stream::IntervalReader reader = str.GetBytesReader();
if (reader.interval_size() > buffer.size()) {
return Status::ResourceExhausted();
}
- Result<ByteSpan> res = reader.Read(std::as_writable_bytes(buffer));
+ Result<ByteSpan> res = reader.Read(as_writable_bytes(buffer));
PW_TRY(res.status());
return std::string_view(buffer.data(), res.value().size());
}
@@ -256,7 +256,7 @@ Status UpdateBundleAccessor::OpenAndVerify() {
if (Status status = DoVerify(); !status.ok()) {
PW_LOG_ERROR("Failed to verified staged bundle");
- Close();
+ Close().IgnoreError();
return status;
}
@@ -269,14 +269,14 @@ Result<uint64_t> UpdateBundleAccessor::GetTotalPayloadSize() {
PW_TRY(manifested_targets.status());
protobuf::StringToBytesMap bundled_payloads = bundle_.AsStringToBytesMap(
- static_cast<uint32_t>(UpdateBundle::Fields::TARGET_PAYLOADS));
+ static_cast<uint32_t>(UpdateBundle::Fields::kTargetPayloads));
PW_TRY(bundled_payloads.status());
- uint64_t total_bytes;
+ uint64_t total_bytes = 0;
std::array<std::byte, MAX_TARGET_NAME_LENGTH> name_buffer = {};
for (protobuf::Message target : manifested_targets) {
protobuf::String target_name =
- target.AsString(static_cast<uint32_t>(TargetFile::Fields::FILE_NAME));
+ target.AsString(static_cast<uint32_t>(TargetFile::Fields::kFileName));
stream::IntervalReader name_reader = target_name.GetBytesReader();
PW_TRY(name_reader.status());
@@ -295,7 +295,7 @@ Result<uint64_t> UpdateBundleAccessor::GetTotalPayloadSize() {
continue;
}
protobuf::Uint64 target_length =
- target.AsUint64(static_cast<uint32_t>(TargetFile::Fields::LENGTH));
+ target.AsUint64(static_cast<uint32_t>(TargetFile::Fields::kLength));
PW_TRY(target_length.status());
total_bytes += target_length.value();
}
@@ -310,7 +310,7 @@ stream::IntervalReader UpdateBundleAccessor::GetTargetPayload(
PW_TRY(manifest_entry.status());
protobuf::StringToBytesMap payloads_map = bundle_.AsStringToBytesMap(
- static_cast<uint32_t>(UpdateBundle::Fields::TARGET_PAYLOADS));
+ static_cast<uint32_t>(UpdateBundle::Fields::kTargetPayloads));
return payloads_map[target_name].GetBytesReader();
}
@@ -346,16 +346,15 @@ Status UpdateBundleAccessor::PersistManifest() {
Status UpdateBundleAccessor::Close() {
bundle_verified_ = false;
- return blob_store_reader_.IsOpen() ? blob_store_reader_.Close() : OkStatus();
+ return update_reader_.IsOpen() ? update_reader_.Close() : OkStatus();
}
Status UpdateBundleAccessor::DoOpen() {
- PW_TRY(blob_store_.Init());
- PW_TRY(blob_store_reader_.Open());
- bundle_ = protobuf::Message(blob_store_reader_,
- blob_store_reader_.ConservativeReadLimit());
+ PW_TRY(update_reader_.Open());
+ bundle_ = protobuf::Message(update_reader_.reader(),
+ update_reader_.reader().ConservativeReadLimit());
if (!bundle_.ok()) {
- blob_store_reader_.Close();
+ update_reader_.Close().IgnoreError();
return bundle_.status();
}
return OkStatus();
@@ -369,30 +368,38 @@ Status UpdateBundleAccessor::DoVerify() {
#else // PW_SOFTWARE_UPDATE_DISABLE_BUNDLE_VERIFICATION
bundle_verified_ = false;
+ if (self_verification_) {
+ // Use root metadata in staged bundle for self-verification. This root
+ // metadata is optional and used opportunistically in the rest of the
+ // verification flow.
+ trusted_root_ = bundle_.AsMessage(
+ static_cast<uint32_t>(UpdateBundle::Fields::kRootMetadata));
+ } else {
+ // A provisioned on-device root metadata is *required* for formal
+ // verification.
+ if (trusted_root_ = GetOnDeviceTrustedRoot(); !trusted_root_.ok()) {
+ PW_LOG_CRITICAL("Missing on-device trusted root");
+ return Status::Unauthenticated();
+ }
+ }
+
// Verify and upgrade the on-device trust to the incoming root metadata if
// one is included.
if (Status status = UpgradeRoot(); !status.ok()) {
- PW_LOG_ERROR("Failed to upgrade to Root in staged bundle");
+ PW_LOG_ERROR("Failed to rotate root metadata");
return status;
}
- // TODO(pwbug/456): Verify the targets metadata against the current trusted
- // root.
if (Status status = VerifyTargetsMetadata(); !status.ok()) {
PW_LOG_ERROR("Failed to verify Targets metadata");
return status;
}
- // TODO(pwbug/456): Investigate whether targets payload verification should
- // be performed here or deferred until a specific target is requested.
if (Status status = VerifyTargetsPayloads(); !status.ok()) {
PW_LOG_ERROR("Failed to verify all manifested payloads");
return status;
}
- // TODO(pwbug/456): Invoke the backend to do downstream verification of the
- // bundle (e.g. compatibility and manifest completeness checks).
-
bundle_verified_ = true;
return OkStatus();
#endif // PW_SOFTWARE_UPDATE_DISABLE_BUNDLE_VERIFICATION
@@ -429,26 +436,17 @@ ManifestAccessor UpdateBundleAccessor::GetOnDeviceManifest() {
}
Status UpdateBundleAccessor::UpgradeRoot() {
+#if PW_SOFTWARE_UPDATE_WITH_ROOT_ROTATION
protobuf::Message new_root = bundle_.AsMessage(
- static_cast<uint32_t>(UpdateBundle::Fields::ROOT_METADATA));
-
- // Try self-verification even if verification is disabled by the caller. This
- // minimizes surprises when the caller do decide to turn on verification.
- bool self_verifying = disable_verification_;
-
- // Choose and cache the root metadata to trust.
- trusted_root_ = self_verifying ? new_root : GetOnDeviceTrustedRoot();
+ static_cast<uint32_t>(UpdateBundle::Fields::kRootMetadata));
if (!new_root.status().ok()) {
// Don't bother upgrading if not found or invalid.
- PW_LOG_WARN("Incoming root metadata not found or invalid");
+ PW_LOG_WARN("Skipping root metadata rotation: not found or invalid");
return OkStatus();
}
- // A valid trust anchor is required onwards from here.
- PW_TRY(trusted_root_.status());
-
- // TODO(pwbug/456): Check whether the bundle contains a root metadata that
+ // TODO(b/237580538): Check whether the bundle contains a root metadata that
// is different from the on-device trusted root.
// Verify the signatures against the trusted root metadata.
@@ -459,7 +457,7 @@ Status UpdateBundleAccessor::UpgradeRoot() {
return Status::Unauthenticated();
}
- // TODO(pwbug/456): Verifiy the content of the new root metadata, including:
+ // TODO(b/237580538): Verifiy the content of the new root metadata, including:
// 1) Check role magic field.
// 2) Check signature requirement. Specifically, check that no key is
// reused across different roles and keys are unique in the same
@@ -475,34 +473,33 @@ Status UpdateBundleAccessor::UpgradeRoot() {
return Status::Unauthenticated();
}
- // TODO(pwbug/456): Check rollback.
// Retrieves the trusted root metadata content message.
protobuf::Message trusted_root_content =
trusted_root_.AsMessage(static_cast<uint32_t>(
- SignedRootMetadata::Fields::SERIALIZED_ROOT_METADATA));
+ SignedRootMetadata::Fields::kSerializedRootMetadata));
PW_TRY(trusted_root_content.status());
Result<uint32_t> trusted_root_version = GetMetadataVersion(
trusted_root_content,
- static_cast<uint32_t>(RootMetadata::Fields::COMMON_METADATA));
+ static_cast<uint32_t>(RootMetadata::Fields::kCommonMetadata));
PW_TRY(trusted_root_version.status());
// Retrieves the serialized new root metadata message.
protobuf::Message new_root_content = new_root.AsMessage(static_cast<uint32_t>(
- SignedRootMetadata::Fields::SERIALIZED_ROOT_METADATA));
+ SignedRootMetadata::Fields::kSerializedRootMetadata));
PW_TRY(new_root_content.status());
Result<uint32_t> new_root_version = GetMetadataVersion(
new_root_content,
- static_cast<uint32_t>(RootMetadata::Fields::COMMON_METADATA));
+ static_cast<uint32_t>(RootMetadata::Fields::kCommonMetadata));
PW_TRY(new_root_version.status());
if (trusted_root_version.value() > new_root_version.value()) {
PW_LOG_ERROR("Root attempts to rollback from %u to %u",
- trusted_root_version.value(),
- new_root_version.value());
+ static_cast<unsigned>(trusted_root_version.value()),
+ static_cast<unsigned>(new_root_version.value()));
return Status::Unauthenticated();
}
- if (!self_verifying) {
+ if (!self_verification_) {
// Persist the root immediately after it is successfully verified. This is
// to make sure the trust anchor is up-to-date in storage as soon as
// we are confident. Although targets metadata and product-specific
@@ -511,20 +508,26 @@ Status UpdateBundleAccessor::UpgradeRoot() {
// compromise keys.
stream::IntervalReader new_root_reader =
new_root.ToBytes().GetBytesReader();
- PW_TRY(backend_.SafelyPersistRootMetadata(new_root_reader));
+ if (Status status = backend_.SafelyPersistRootMetadata(new_root_reader);
+ !status.ok()) {
+ PW_LOG_ERROR("Failed to persist rotated root metadata");
+ return status;
+ }
}
- // TODO(pwbug/456): Implement key change detection to determine whether
+ // TODO(b/237580538): Implement key change detection to determine whether
// rotation has occured or not. Delete the persisted targets metadata version
// if any of the targets keys has been rotated.
return OkStatus();
+#else
+ // Root metadata rotation opted out.
+ return OkStatus();
+#endif // PW_SOFTWARE_UPDATE_WITH_ROOT_ROTATION
}
Status UpdateBundleAccessor::VerifyTargetsMetadata() {
- bool self_verifying = disable_verification_;
-
- if (self_verifying && !trusted_root_.status().ok()) {
+ if (self_verification_ && !trusted_root_.status().ok()) {
PW_LOG_WARN(
"Self-verification won't verify Targets metadata because there is no "
"root");
@@ -543,7 +546,7 @@ Status UpdateBundleAccessor::VerifyTargetsMetadata() {
// }
protobuf::StringToMessageMap signed_targets_metadata_map =
bundle_.AsStringToMessageMap(
- static_cast<uint32_t>(UpdateBundle::Fields::TARGETS_METADATA));
+ static_cast<uint32_t>(UpdateBundle::Fields::kTargetsMetadata));
PW_TRY(signed_targets_metadata_map.status());
// The top-level targets metadata is identified by key name "targets" in the
@@ -561,29 +564,29 @@ Status UpdateBundleAccessor::VerifyTargetsMetadata() {
// }
protobuf::Message top_level_targets_metadata =
signed_top_level_targets_metadata.AsMessage(static_cast<uint32_t>(
- SignedTargetsMetadata::Fields::SERIALIZED_TARGETS_METADATA));
+ SignedTargetsMetadata::Fields::kSerializedTargetsMetadata));
// Get the sigantures from the signed targets metadata.
protobuf::RepeatedMessages signatures =
signed_top_level_targets_metadata.AsRepeatedMessages(
- static_cast<uint32_t>(SignedTargetsMetadata::Fields::SIGNATURES));
+ static_cast<uint32_t>(SignedTargetsMetadata::Fields::kSignatures));
PW_TRY(signatures.status());
// Retrieve the trusted root metadata message.
protobuf::Message trusted_root =
trusted_root_.AsMessage(static_cast<uint32_t>(
- SignedRootMetadata::Fields::SERIALIZED_ROOT_METADATA));
+ SignedRootMetadata::Fields::kSerializedRootMetadata));
PW_TRY(trusted_root.status());
// Get the key_mapping from the trusted root metadata.
protobuf::StringToMessageMap key_mapping = trusted_root.AsStringToMessageMap(
- static_cast<uint32_t>(RootMetadata::Fields::KEYS));
+ static_cast<uint32_t>(RootMetadata::Fields::kKeys));
PW_TRY(key_mapping.status());
- // Get the targest metadtata siganture requirement from the trusted root.
+ // Get the target metadtata signature requirement from the trusted root.
protobuf::Message signature_requirement =
trusted_root.AsMessage(static_cast<uint32_t>(
- RootMetadata::Fields::TARGETS_SIGNATURE_REQUIREMENT));
+ RootMetadata::Fields::kTargetsSignatureRequirement));
PW_TRY(signature_requirement.status());
// Verify the sigantures
@@ -593,7 +596,7 @@ Status UpdateBundleAccessor::VerifyTargetsMetadata() {
signature_requirement,
key_mapping);
- if (self_verifying && sig_res.IsNotFound()) {
+ if (self_verification_ && sig_res.IsNotFound()) {
PW_LOG_WARN("Self-verification ignoring unsigned bundle");
return OkStatus();
}
@@ -603,11 +606,9 @@ Status UpdateBundleAccessor::VerifyTargetsMetadata() {
return Status::Unauthenticated();
}
- // TODO(pwbug/456): Check targets metadtata content.
-
- if (self_verifying) {
+ if (self_verification_) {
// Don't bother because it does not matter.
- PW_LOG_WARN("Self verification does not do Targets metadata anti-rollback");
+ PW_LOG_WARN("Self verification skips Targets metadata anti-rollback");
return OkStatus();
}
@@ -625,12 +626,12 @@ Status UpdateBundleAccessor::VerifyTargetsMetadata() {
Result<uint32_t> new_version = GetMetadataVersion(
top_level_targets_metadata,
static_cast<uint32_t>(
- software_update::TargetsMetadata::Fields::COMMON_METADATA));
+ software_update::TargetsMetadata::Fields::kCommonMetadata));
PW_TRY(new_version.status());
if (current_version.value() > new_version.value()) {
PW_LOG_ERROR("Blocking Targets metadata rollback from %u to %u",
- current_version.value(),
- new_version.value());
+ static_cast<unsigned>(current_version.value()),
+ static_cast<unsigned>(new_version.value()));
return Status::Unauthenticated();
}
@@ -650,7 +651,7 @@ Status UpdateBundleAccessor::VerifyTargetsPayloads() {
for (protobuf::Message target_file : target_files) {
// Extract target file name in the form of a `std::string_view`.
protobuf::String name_proto = target_file.AsString(
- static_cast<uint32_t>(TargetFile::Fields::FILE_NAME));
+ static_cast<uint32_t>(TargetFile::Fields::kFileName));
PW_TRY(name_proto.status());
char name_buf[MAX_TARGET_NAME_LENGTH] = {0};
Result<std::string_view> target_name =
@@ -658,11 +659,11 @@ Status UpdateBundleAccessor::VerifyTargetsPayloads() {
PW_TRY(target_name.status());
// Get target length.
- protobuf::Uint64 target_length =
- target_file.AsUint64(static_cast<uint32_t>(TargetFile::Fields::LENGTH));
+ protobuf::Uint64 target_length = target_file.AsUint64(
+ static_cast<uint32_t>(TargetFile::Fields::kLength));
PW_TRY(target_length.status());
if (target_length.value() > PW_SOFTWARE_UPDATE_MAX_TARGET_PAYLOAD_SIZE) {
- PW_LOG_ERROR("Target payload too big. Maximum is %llu bytes",
+ PW_LOG_ERROR("Target payload too big. Maximum is %u bytes",
PW_SOFTWARE_UPDATE_MAX_TARGET_PAYLOAD_SIZE);
return Status::OutOfRange();
}
@@ -670,15 +671,16 @@ Status UpdateBundleAccessor::VerifyTargetsPayloads() {
// Get target SHA256 hash.
protobuf::Bytes target_sha256 = Status::NotFound();
protobuf::RepeatedMessages hashes = target_file.AsRepeatedMessages(
- static_cast<uint32_t>(TargetFile::Fields::HASHES));
+ static_cast<uint32_t>(TargetFile::Fields::kHashes));
for (protobuf::Message hash : hashes) {
protobuf::Uint32 hash_function =
- hash.AsUint32(static_cast<uint32_t>(Hash::Fields::FUNCTION));
+ hash.AsUint32(static_cast<uint32_t>(Hash::Fields::kFunction));
PW_TRY(hash_function.status());
if (hash_function.value() ==
static_cast<uint32_t>(HashFunction::SHA256)) {
- target_sha256 = hash.AsBytes(static_cast<uint32_t>(Hash::Fields::HASH));
+ target_sha256 =
+ hash.AsBytes(static_cast<uint32_t>(Hash::Fields::kHash));
break;
}
}
@@ -697,12 +699,12 @@ Status UpdateBundleAccessor::VerifyTargetsPayloads() {
}
Status UpdateBundleAccessor::VerifyTargetPayload(
- ManifestAccessor manifest,
+ ManifestAccessor,
std::string_view target_name,
protobuf::Uint64 expected_length,
protobuf::Bytes expected_sha256) {
protobuf::StringToBytesMap payloads_map = bundle_.AsStringToBytesMap(
- static_cast<uint32_t>(UpdateBundle::Fields::TARGET_PAYLOADS));
+ static_cast<uint32_t>(UpdateBundle::Fields::kTargetPayloads));
stream::IntervalReader payload_reader =
payloads_map[target_name].GetBytesReader();
@@ -724,8 +726,8 @@ Status UpdateBundleAccessor::VerifyTargetPayload(
// TODO(alizhang): Add unit tests for all failure conditions.
Status UpdateBundleAccessor::VerifyOutOfBundleTargetPayload(
std::string_view target_name,
- protobuf::Uint64 expected_length,
- protobuf::Bytes expected_sha256) {
+ [[maybe_unused]] protobuf::Uint64 expected_length,
+ [[maybe_unused]] protobuf::Bytes expected_sha256) {
#if PW_SOFTWARE_UPDATE_WITH_PERSONALIZATION
// The target payload is "personalized out". We we can't take a measurement
// without backend help. For now we will check against the device manifest
@@ -747,25 +749,25 @@ Status UpdateBundleAccessor::VerifyOutOfBundleTargetPayload(
}
protobuf::Uint64 cached_length =
- cached.AsUint64(static_cast<uint32_t>(TargetFile::Fields::LENGTH));
+ cached.AsUint64(static_cast<uint32_t>(TargetFile::Fields::kLength));
PW_TRY(cached_length.status());
if (cached_length.value() != expected_length.value()) {
- PW_LOG_ERROR("Personalized-out target has bad length: %llu, expected: %llu",
- cached_length.value(),
- expected_length.value());
+ PW_LOG_ERROR("Personalized-out target has bad length: %u, expected: %u",
+ static_cast<unsigned>(cached_length.value()),
+ static_cast<unsigned>(expected_length.value()));
return Status::Unauthenticated();
}
protobuf::Bytes cached_sha256 = Status::NotFound();
protobuf::RepeatedMessages hashes = cached.AsRepeatedMessages(
- static_cast<uint32_t>(TargetFile::Fields::HASHES));
+ static_cast<uint32_t>(TargetFile::Fields::kHashes));
for (protobuf::Message hash : hashes) {
protobuf::Uint32 hash_function =
- hash.AsUint32(static_cast<uint32_t>(Hash::Fields::FUNCTION));
+ hash.AsUint32(static_cast<uint32_t>(Hash::Fields::kFunction));
PW_TRY(hash_function.status());
if (hash_function.value() == static_cast<uint32_t>(HashFunction::SHA256)) {
- cached_sha256 = hash.AsBytes(static_cast<uint32_t>(Hash::Fields::HASH));
+ cached_sha256 = hash.AsBytes(static_cast<uint32_t>(Hash::Fields::kHash));
break;
}
}
@@ -781,7 +783,7 @@ Status UpdateBundleAccessor::VerifyOutOfBundleTargetPayload(
return OkStatus();
#else
- PW_LOG_ERROR("Target file %s not found in bundle", target_name);
+ PW_LOG_ERROR("Target file %s not found in bundle", target_name.data());
return Status::Unauthenticated();
#endif // PW_SOFTWARE_UPDATE_WITH_PERSONALIZATION
}
@@ -794,9 +796,9 @@ Status UpdateBundleAccessor::VerifyInBundleTargetPayload(
// measurement.
uint64_t actual_length = payload_reader.interval_size();
if (actual_length != expected_length.value()) {
- PW_LOG_ERROR("Wrong payload length. Expected: %llu, actual: %llu",
- expected_length.value(),
- actual_length);
+ PW_LOG_ERROR("Wrong payload length. Expected: %u, actual: %u",
+ static_cast<unsigned>(expected_length.value()),
+ static_cast<unsigned>(actual_length));
return Status::Unauthenticated();
}
diff --git a/pw_software_update/update_bundle_test.cc b/pw_software_update/update_bundle_test.cc
index 927c0e6ed..e35987a38 100644
--- a/pw_software_update/update_bundle_test.cc
+++ b/pw_software_update/update_bundle_test.cc
@@ -15,8 +15,10 @@
#include <array>
#include "gtest/gtest.h"
+#include "pw_blob_store/blob_store.h"
#include "pw_kvs/fake_flash_memory.h"
#include "pw_kvs/test_key_value_store.h"
+#include "pw_software_update/blob_store_openable_reader.h"
#include "pw_software_update/bundled_update_backend.h"
#include "pw_software_update/update_bundle_accessor.h"
#include "pw_stream/memory_stream.h"
@@ -67,9 +69,9 @@ class TestBundledUpdateBackend final : public BundledUpdateBackend {
void SetManifestWriter(stream::Writer* writer) { manifest_writer_ = writer; }
- virtual Result<stream::SeekableReader*> GetRootMetadataReader() override {
+ Result<stream::SeekableReader*> GetRootMetadataReader() override {
return &trusted_root_reader_;
- };
+ }
Status BeforeManifestRead() override {
before_manifest_read_called_ = true;
@@ -77,7 +79,7 @@ class TestBundledUpdateBackend final : public BundledUpdateBackend {
return OkStatus();
}
return Status::NotFound();
- };
+ }
bool BeforeManifestReadCalled() { return before_manifest_read_called_; }
@@ -103,12 +105,12 @@ class TestBundledUpdateBackend final : public BundledUpdateBackend {
return manifest_writer_;
}
- virtual Status SafelyPersistRootMetadata(
+ Status SafelyPersistRootMetadata(
[[maybe_unused]] stream::IntervalReader root_metadata) override {
new_root_persisted_ = true;
trusted_root_reader_ = root_metadata;
return OkStatus();
- };
+ }
bool IsNewRootPersisted() const { return new_root_persisted_; }
@@ -120,7 +122,6 @@ class TestBundledUpdateBackend final : public BundledUpdateBackend {
bool before_manifest_write_called_ = false;
bool after_manifest_write_called_ = false;
bool new_root_persisted_ = false;
- size_t backend_verified_files_ = 0;
// A memory reader for buffer passed by SetTrustedRoot(). This will be used
// to back `trusted_root_reader_`
@@ -136,12 +137,15 @@ class UpdateBundleTest : public testing::Test {
blob_partition_,
nullptr,
kvs::TestKvs(),
- kBufferSize) {}
+ kBufferSize),
+ blob_reader_(bundle_blob_) {}
blob_store::BlobStoreBuffer<kBufferSize>& bundle_blob() {
return bundle_blob_;
}
+ BlobStoreOpenableReader& blob_reader() { return blob_reader_; }
+
TestBundledUpdateBackend& backend() { return backend_; }
void StageTestBundle(ConstByteSpan bundle_data) {
@@ -188,6 +192,7 @@ class UpdateBundleTest : public testing::Test {
kvs::FakeFlashMemoryBuffer<kSectorSize, kSectorCount> blob_flash_;
kvs::FlashPartition blob_partition_;
blob_store::BlobStoreBuffer<kBufferSize> bundle_blob_;
+ BlobStoreOpenableReader blob_reader_;
std::array<std::byte, kMetadataBufferSize> metadata_buffer_;
TestBundledUpdateBackend backend_;
};
@@ -197,7 +202,7 @@ class UpdateBundleTest : public testing::Test {
TEST_F(UpdateBundleTest, GetTargetPayload) {
backend().SetTrustedRoot(kDevSignedRoot);
StageTestBundle(kTestDevBundle);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_OK(update_bundle.OpenAndVerify());
@@ -230,7 +235,7 @@ TEST_F(UpdateBundleTest, GetTargetPayload) {
TEST_F(UpdateBundleTest, PersistManifest) {
backend().SetTrustedRoot(kDevSignedRoot);
StageTestBundle(kTestDevBundle);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_OK(update_bundle.OpenAndVerify());
@@ -251,7 +256,7 @@ TEST_F(UpdateBundleTest, PersistManifest) {
TEST_F(UpdateBundleTest, PersistManifestFailIfNotVerified) {
backend().SetTrustedRoot(kDevSignedRoot);
StageTestBundle(kTestBadProdSignature);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_FAIL(update_bundle.OpenAndVerify());
@@ -268,7 +273,7 @@ TEST_F(UpdateBundleTest, PersistManifestFailIfNotVerified) {
TEST_F(UpdateBundleTest, SelfVerificationWithIncomingRoot) {
StageTestBundle(kTestDevBundleWithRoot);
UpdateBundleAccessor update_bundle(
- bundle_blob(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_OK(update_bundle.OpenAndVerify());
// Self verification must not persist anything.
@@ -288,7 +293,7 @@ TEST_F(UpdateBundleTest, SelfVerificationWithIncomingRoot) {
TEST_F(UpdateBundleTest, SelfVerificationWithoutIncomingRoot) {
StageTestBundle(kTestDevBundle);
UpdateBundleAccessor update_bundle(
- bundle_blob(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_OK(update_bundle.OpenAndVerify());
}
@@ -296,7 +301,7 @@ TEST_F(UpdateBundleTest, SelfVerificationWithoutIncomingRoot) {
TEST_F(UpdateBundleTest, SelfVerificationWithMessedUpRoot) {
StageTestBundle(kTestDevBundleWithProdRoot);
UpdateBundleAccessor update_bundle(
- bundle_blob(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_FAIL(update_bundle.OpenAndVerify());
}
@@ -304,7 +309,7 @@ TEST_F(UpdateBundleTest, SelfVerificationWithMessedUpRoot) {
TEST_F(UpdateBundleTest, SelfVerificationChecksMissingHashes) {
StageTestBundle(kTestBundleMissingTargetHashFile0);
UpdateBundleAccessor update_bundle(
- bundle_blob(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_FAIL(update_bundle.OpenAndVerify());
}
@@ -312,7 +317,7 @@ TEST_F(UpdateBundleTest, SelfVerificationChecksMissingHashes) {
TEST_F(UpdateBundleTest, SelfVerificationChecksBadHashes) {
StageTestBundle(kTestBundleMismatchedTargetHashFile0);
UpdateBundleAccessor update_bundle(
- bundle_blob(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_FAIL(update_bundle.OpenAndVerify());
}
@@ -320,7 +325,7 @@ TEST_F(UpdateBundleTest, SelfVerificationChecksBadHashes) {
TEST_F(UpdateBundleTest, SelfVerificationIgnoresUnsignedBundle) {
StageTestBundle(kTestUnsignedBundleWithRoot);
UpdateBundleAccessor update_bundle(
- bundle_blob(), backend(), /* disable_verification = */ true);
+ blob_reader(), backend(), /* self_verification = */ true);
ASSERT_OK(update_bundle.OpenAndVerify());
}
@@ -329,7 +334,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifySucceedsWithAllVerification) {
backend().SetTrustedRoot(kDevSignedRoot);
backend().SetCurrentManifest(kTestBundleManifest);
StageTestBundle(kTestProdBundle);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_FALSE(backend().IsNewRootPersisted());
ASSERT_FALSE(backend().BeforeManifestReadCalled());
@@ -349,7 +354,7 @@ TEST_F(UpdateBundleTest,
// pw_software_update/py/pw_software_update/generate_test_bundle.py for
// detail of generation.
StageTestBundle(kTestDevBundle);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_FALSE(backend().IsNewRootPersisted());
ASSERT_FALSE(backend().BeforeManifestReadCalled());
@@ -369,7 +374,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnMismatchedRootKeyAndSignature) {
// See pw_software_update/py/pw_software_update/generate_test_bundle.py for
// detail of generation.
StageTestBundle(kTestMismatchedRootKeyAndSignature);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, false);
}
@@ -377,7 +382,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnBadProdSignature) {
backend().SetTrustedRoot(kDevSignedRoot);
backend().SetCurrentManifest(kTestBundleManifest);
StageTestBundle(kTestBadProdSignature);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, false);
}
@@ -385,7 +390,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnBadTargetsSignature) {
backend().SetTrustedRoot(kDevSignedRoot);
backend().SetCurrentManifest(kTestBundleManifest);
StageTestBundle(kTestBadTargetsSignature);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, true);
}
@@ -393,14 +398,14 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnBadTargetsRollBack) {
backend().SetTrustedRoot(kDevSignedRoot);
backend().SetCurrentManifest(kTestBundleManifest);
StageTestBundle(kTestTargetsRollback);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, true);
}
TEST_F(UpdateBundleTest, OpenAndVerifySucceedsWithoutExistingManifest) {
backend().SetTrustedRoot(kDevSignedRoot);
StageTestBundle(kTestProdBundle);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_FALSE(backend().IsNewRootPersisted());
ASSERT_OK(update_bundle.OpenAndVerify());
@@ -411,7 +416,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnRootRollback) {
backend().SetTrustedRoot(kDevSignedRoot);
backend().SetCurrentManifest(kTestBundleManifest);
StageTestBundle(kTestRootRollback);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, false);
}
@@ -422,7 +427,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnMismatchedTargetHashFile0) {
// pw_software_update/py/pw_software_update/generate_test_bundle.py.
// The hash value for file 0 in the targets metadata is made incorrect.
StageTestBundle(kTestBundleMismatchedTargetHashFile0);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, true);
}
@@ -433,7 +438,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnMismatchedTargetHashFile1) {
// pw_software_update/py/pw_software_update/generate_test_bundle.py
// The hash value for file 1 in the targets metadata is made incorrect.
StageTestBundle(kTestBundleMismatchedTargetHashFile1);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, true);
}
@@ -444,7 +449,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnMissingTargetHashFile0) {
// pw_software_update/py/pw_software_update/generate_test_bundle.py.
// The hash value for file 0 is removed.
StageTestBundle(kTestBundleMissingTargetHashFile0);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, true);
}
@@ -455,7 +460,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnMissingTargetHashFile1) {
// pw_software_update/py/pw_software_update/generate_test_bundle.py
// The hash value for file 1 is removed.
StageTestBundle(kTestBundleMissingTargetHashFile1);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, true);
}
@@ -466,7 +471,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnMismatchedTargetLengthFile0) {
// pw_software_update/py/pw_software_update/generate_test_bundle.py.
// The length value for file 0 in the targets metadata is made incorrect (1).
StageTestBundle(kTestBundleMismatchedTargetLengthFile0);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, true);
}
@@ -477,7 +482,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifyFailsOnMismatchedTargetLengthFile1) {
// pw_software_update/py/pw_software_update/generate_test_bundle.py.
// The length value for file 0 in the targets metadata is made incorrect (1).
StageTestBundle(kTestBundleMismatchedTargetLengthFile1);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
CheckOpenAndVerifyFail(update_bundle, true);
}
@@ -489,7 +494,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifySucceedsWithPersonalizedOutFile0) {
// The payload for file 0 is removed from the bundle to emulate being
// personalized out.
StageTestBundle(kTestBundlePersonalizedOutFile0);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_OK(update_bundle.OpenAndVerify());
}
@@ -502,7 +507,7 @@ TEST_F(UpdateBundleTest, OpenAndVerifySucceedsWithPersonalizedOutFile1) {
// The payload for file 1 is removed from the bundle to emulate being
// personalized out.
StageTestBundle(kTestBundlePersonalizedOutFile1);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_OK(update_bundle.OpenAndVerify());
}
@@ -515,7 +520,7 @@ TEST_F(UpdateBundleTest,
// The payload for file 0 is removed from the bundle to emulate being
// personalized out.
StageTestBundle(kTestBundlePersonalizedOutFile0);
- UpdateBundleAccessor update_bundle(bundle_blob(), backend());
+ UpdateBundleAccessor update_bundle(blob_reader(), backend());
ASSERT_FAIL(update_bundle.OpenAndVerify());
}
diff --git a/pw_span/Android.bp b/pw_span/Android.bp
new file mode 100644
index 000000000..10f9db881
--- /dev/null
+++ b/pw_span/Android.bp
@@ -0,0 +1,26 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_span_headers",
+ vendor_available: true,
+ cpp_std: "c++2a",
+ export_include_dirs: ["public"],
+ header_libs: [],
+ host_supported: true,
+}
diff --git a/pw_span/BUILD.bazel b/pw_span/BUILD.bazel
index 94919c004..ec57864ba 100644
--- a/pw_span/BUILD.bazel
+++ b/pw_span/BUILD.bazel
@@ -24,23 +24,32 @@ licenses(["notice"])
pw_cc_library(
name = "pw_span",
- srcs = ["public/pw_span/internal/span.h"],
- hdrs = ["public_overrides/span"],
- includes = [
- "public",
- "public_overrides",
+ srcs = [
+ "public/pw_span/internal/config.h",
+ "public/pw_span/internal/span_impl.h",
],
+ hdrs = ["public/pw_span/span.h"],
+ includes = ["public"],
deps = [
- "//pw_polyfill",
- "//pw_polyfill:standard_library",
+ # TODO(b/243851191): Depending on pw_assert causes a dependency cycle.
],
)
pw_cc_test(
- name = "span_test",
+ name = "pw_span_test",
srcs = ["span_test.cc"],
deps = [
":pw_span",
+ "//pw_polyfill",
"//pw_unit_test",
],
)
+
+pw_cc_test(
+ name = "compatibility_test",
+ srcs = ["compatibility_test.cc"],
+ deps = [
+ ":pw_span",
+ "//pw_polyfill",
+ ],
+)
diff --git a/pw_span/BUILD.gn b/pw_span/BUILD.gn
index e1c3f1171..91e294cc0 100644
--- a/pw_span/BUILD.gn
+++ b/pw_span/BUILD.gn
@@ -14,50 +14,105 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_toolchain/traits.gni")
import("$dir_pw_unit_test/test.gni")
-config("public_config") {
+declare_args() {
+ # Whether or not to enable bounds-checking asserts in pw::span. Enabling this
+ # may significantly increase binary size, and can introduce dependency cycles
+ # if your pw_assert backend's headers depends directly or indirectly on
+ # pw_span. It's recommended to enable this for debug builds if possible.
+ pw_span_ENABLE_ASSERTS = false
+
+ # The build target that overrides the default configuration options for this
+ # module. This should point to a source set that provides defines through a
+ # public config (which may -include a file or add defines directly).
+ #
+ # Most modules depend on pw_build_DEFAULT_MODULE_CONFIG as the default config,
+ # but since this module's config options require interaction with the build
+ # system, this defaults to an internal config to properly support
+ # pw_span_ENABLE_ASSERTS.
+ pw_span_CONFIG = "$dir_pw_span:span_asserts"
+}
+
+config("public_include_path") {
include_dirs = [ "public" ]
visibility = [ ":*" ]
}
-config("overrides_config") {
- include_dirs = [ "public_overrides" ]
+pw_source_set("config") {
+ public = [ "public/pw_span/internal/config.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ pw_span_CONFIG ]
+ remove_public_deps = [ "*" ]
visibility = [ ":*" ]
}
-# This source set provides the <span> header, which is accessed only through
-# pw_polyfill.
-pw_source_set("polyfill") {
- remove_public_deps = [ "*" ]
- public_configs = [ ":overrides_config" ]
- public_deps = [ ":pw_span" ]
- public = [ "public_overrides/span" ]
- visibility = [ "$dir_pw_polyfill:*" ]
+config("public_config") {
+ include_dirs = [ "public" ]
+ visibility = [ ":*" ]
+}
+
+config("span_asserts_config") {
+ defines = [ "PW_SPAN_ENABLE_ASSERTS=${pw_span_ENABLE_ASSERTS}" ]
+ visibility = [ ":span_asserts" ]
+}
+
+pw_source_set("span_asserts") {
+ public_configs = [ ":span_asserts_config" ]
+ visibility = [ ":config" ]
}
-# This source set provides the internal span.h header included by <span>. This
-# source set is only used by pw_polyfill, so its visibility is restricted.
+# Provides "pw_span/span.h" and pw::span.
pw_source_set("pw_span") {
- remove_public_deps = [ "*" ]
public_configs = [ ":public_config" ]
- public_deps = [ "$dir_pw_polyfill" ]
- sources = [ "public/pw_span/internal/span.h" ]
- visibility = [ ":*" ]
+ public = [ "public/pw_span/span.h" ]
+ public_deps = [ ":config" ]
+
+ # Polyfill <cstddef> (std::byte) and <iterator> (std::size(), std::data) if
+ # C++17 is not supported.
+ if (pw_toolchain_CXX_STANDARD < pw_toolchain_STANDARD.CXX17) {
+ public_deps += [
+ "$dir_pw_polyfill:cstddef",
+ "$dir_pw_polyfill:iterator",
+ ]
+ }
+
+ # Only add a dependency on pw_assert if the flag is explicitly enabled.
+ if (pw_span_ENABLE_ASSERTS) {
+ public_deps += [ "$dir_pw_assert:assert" ]
+ }
+
+ sources = [ "public/pw_span/internal/span_impl.h" ]
}
pw_test_group("tests") {
- tests = [ ":test" ]
+ tests = [
+ ":pw_span_test",
+ ":compatibility_test",
+ ]
}
-pw_test("test") {
- deps = [ ":pw_span" ]
+pw_test("pw_span_test") {
+ deps = [
+ ":pw_span",
+ dir_pw_polyfill,
+ ]
remove_configs = [ "$dir_pw_build:extra_strict_warnings" ]
sources = [ "span_test.cc" ]
}
+pw_test("compatibility_test") {
+ deps = [
+ ":pw_span",
+ dir_pw_polyfill,
+ ]
+ sources = [ "compatibility_test.cc" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_span/CMakeLists.txt b/pw_span/CMakeLists.txt
index a53fd0d52..63cffd227 100644
--- a/pw_span/CMakeLists.txt
+++ b/pw_span/CMakeLists.txt
@@ -14,12 +14,29 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_auto_add_simple_module(pw_span
+# Provides pw::span in "pw_span/span.h".
+pw_add_library(pw_span INTERFACE
+ HEADERS
+ public/pw_span/span.h
+ public/pw_span/internal/config.h
+ public/pw_span/internal/span_impl.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
- pw_polyfill
+ pw_assert
pw_polyfill.standard_library
)
-target_include_directories(pw_span INTERFACE public_overrides)
+
+pw_add_test(pw_span.pw_span_test
+ SOURCES
+ span_test.cc
+ PRIVATE_DEPS
+ pw_polyfill
+ pw_span
+ GROUPS
+ modules
+ pw_span
+)
if(Zephyr_FOUND AND CONFIG_PIGWEED_SPAN)
zephyr_link_libraries(pw_span)
diff --git a/pw_span/compatibility_test.cc b/pw_span/compatibility_test.cc
new file mode 100644
index 000000000..3f17ad0ed
--- /dev/null
+++ b/pw_span/compatibility_test.cc
@@ -0,0 +1,77 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_polyfill/standard.h"
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(20)
+
+#include <span>
+
+#include "gtest/gtest.h"
+#include "pw_span/span.h"
+
+namespace {
+
+constexpr int kCArray[5] = {0, 1, 2, 3, 4};
+
+void TakesPwSpan(pw::span<const int>) {}
+void TakesStdSpan(std::span<const int>) {}
+
+TEST(SpanCompatibility, CallFunction) {
+ TakesPwSpan(std::span<const int>(kCArray));
+ TakesStdSpan(pw::span<const int>(kCArray));
+}
+
+TEST(SpanCompatibility, StdToPwConversions) {
+ std::span<const int> std_span(kCArray);
+ pw::span<const int> pw_span(std_span);
+
+ EXPECT_EQ(std_span.data(), pw_span.data());
+ EXPECT_EQ(std_span.size(), pw_span.size());
+
+ pw_span = std_span;
+
+ EXPECT_EQ(std_span.data(), pw_span.data());
+ EXPECT_EQ(std_span.size(), pw_span.size());
+}
+
+TEST(SpanCompatibility, PwToStdConversions) {
+ pw::span<const int> pw_span(kCArray);
+ std::span<const int> std_span(pw_span);
+
+ EXPECT_EQ(std_span.data(), pw_span.data());
+ EXPECT_EQ(std_span.size(), pw_span.size());
+
+ std_span = pw_span;
+
+ EXPECT_EQ(std_span.data(), pw_span.data());
+ EXPECT_EQ(std_span.size(), pw_span.size());
+}
+
+TEST(SpanCompatibility, SameArray) {
+ pw::span<const int> pw_span(kCArray);
+ std::span<const int> std_span(kCArray);
+
+ EXPECT_EQ(std_span.data(), pw_span.data());
+ EXPECT_EQ(std_span.size(), pw_span.size());
+
+ EXPECT_EQ(std_span[0], 0);
+ EXPECT_EQ(pw_span[0], 0);
+ EXPECT_EQ(std_span[4], 4);
+ EXPECT_EQ(pw_span[4], 4);
+}
+
+} // namespace
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(20)
diff --git a/pw_span/docs.rst b/pw_span/docs.rst
index 63d44ba76..d1dcd5bff 100644
--- a/pw_span/docs.rst
+++ b/pw_span/docs.rst
@@ -1,27 +1,22 @@
.. _module-pw_span:
--------
+=======
pw_span
--------
-The ``pw_span`` module provides an implementation of C++20's
-`std::span <https://en.cppreference.com/w/cpp/container/span>`_, which is a
-non-owning view of an array of values. The intent is for this implementation of
-``std::span`` is to exactly match the C++20 standard.
-
-The only header provided by the ``pw_span`` module is ``<span>``. It is included
-as if it were coming from the C++ Standard Library. If the C++ library provides
-``<span>``, the library's version of ``std::span`` is used in place of
-``pw_span``'s.
-
-``pw_span`` requires two include paths -- ``public/`` and ``public_overrides/``.
-The internal implementation header is in ``public/``, and the ``<span>`` header
-that mimics the C++ Standard Library is in ``public_overrides/``.
-
-Using std::span
-===============
-``std::span`` is a convenient abstraction that wraps a pointer and a size.
-``std::span`` is especially useful in APIs. Spans support implicit conversions
-from C arrays, ``std::array``, or any STL-style container, such as
+=======
+The ``pw_span`` module provides :cpp:class:`pw::span`, an implementation of
+C++20's `std::span <https://en.cppreference.com/w/cpp/container/span>`_.
+``std::span`` is a non-owning view of an array of values. The intent is for
+:cpp:class:`pw::span` is to match the C++20 standard as closely as possible.
+
+If C++20's ``std::span`` is available, :cpp:class:`pw::span` is simply an alias
+of it.
+
+--------------
+Using pw::span
+--------------
+:cpp:class:`pw::span` is a convenient abstraction that wraps a pointer and a
+size. :cpp:class:`pw::span` is especially useful in APIs. Spans support implicit
+conversions from C arrays, ``std::array``, or any STL-style container, such as
``std::string_view``.
Functions operating on an array of bytes typically accept pointer and size
@@ -37,40 +32,67 @@ arguments:
ProcessBuffer(data_pointer, data_size);
}
-Pointer and size arguments can be replaced with a ``std::span``:
+Pointer and size arguments can be replaced with a :cpp:class:`pw::span`:
.. code-block:: cpp
#include <span>
- // With std::span, the buffer is passed as a single argument.
- bool ProcessBuffer(std::span<uint8_t> buffer);
+ // With pw::span, the buffer is passed as a single argument.
+ bool ProcessBuffer(pw::span<uint8_t> buffer);
bool DoStuff() {
ProcessBuffer(c_array);
ProcessBuffer(array_object);
- ProcessBuffer(std::span(data_pointer, data_size));
+ ProcessBuffer(pw::span(data_pointer, data_size));
}
.. tip::
- Use ``std::span<std::byte>`` or ``std::span<const std::byte>`` to represent
- spans of binary data. Use ``std::as_bytes`` or ``std::as_writeable_bytes``
- to convert any span to a byte span.
+
+ Use ``pw::span<std::byte>`` or ``pw::span<const std::byte>`` to represent
+ spans of binary data. Use ``pw::as_bytes`` or ``pw::as_writable_bytes`` to
+ convert any span to a byte span.
.. code-block:: cpp
- void ProcessData(std::span<const std::byte> data);
+ void ProcessData(pw::span<const std::byte> data);
void DoStuff() {
std::array<AnyType, 7> data = { ... };
- ProcessData(std::as_bytes(std::span(data)));
+ ProcessData(pw::as_bytes(pw::span(data)));
}
+ ``pw_bytes/span.h`` provides ``ByteSpan`` and ``ConstByteSpan`` aliases for
+ these types.
+
+----------------------------
+Module Configuration Options
+----------------------------
+The following configurations can be adjusted via compile-time configuration of
+this module, see the
+:ref:`module documentation <module-structure-compile-time-configuration>` for
+more details.
+
+.. c:macro:: PW_SPAN_ENABLE_ASSERTS
+
+ PW_SPAN_ENABLE_ASSERTS controls whether pw_span's implementation includes
+ asserts for detecting disallowed span operations at runtime. For C++20 and
+ later, this replaces std::span with the custom implementation in pw_span to
+ ensure bounds-checking asserts have been enabled.
+
+ This defaults to disabled because of the significant increase in code size
+ caused by enabling this feature. It's strongly recommended to enable this
+ in debug and testing builds. This can be done by setting
+ ``pw_span_ENABLE_ASSERTS`` to ``true`` in the GN build.
+
+-------------
Compatibility
-=============
-Works with C++14, but some features require C++17.
+-------------
+Works with C++14, but some features require C++17. In C++20, use ``std::span``
+instead.
+------
Zephyr
-======
+------
To enable ``pw_span`` for Zephyr add ``CONFIG_PIGWEED_SPAN=y`` to the project's
configuration.
diff --git a/pw_span/public/pw_span/internal/config.h b/pw_span/public/pw_span/internal/config.h
new file mode 100644
index 000000000..89e6fe38c
--- /dev/null
+++ b/pw_span/public/pw_span/internal/config.h
@@ -0,0 +1,26 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// PW_SPAN_ENABLE_ASSERTS controls whether pw_span's implementation includes
+// asserts for detecting disallowed span operations at runtime. For C++20 and
+// later, this replaces std::span with the custom implementation in pw_span to
+// ensure bounds-checking asserts have been enabled.
+//
+// This defaults to disabled because of the significant increase in code size
+// caused by enabling this feature. It's strongly recommended to enable this
+// in debug and testing builds.
+#if !defined(PW_SPAN_ENABLE_ASSERTS)
+#define PW_SPAN_ENABLE_ASSERTS 0
+#endif // !defined(PW_SPAN_ENABLE_ASSERTS)
diff --git a/pw_span/public/pw_span/internal/span.h b/pw_span/public/pw_span/internal/span_impl.h
index 4e37611a4..43c3d8cd6 100644
--- a/pw_span/public/pw_span/internal/span.h
+++ b/pw_span/public/pw_span/internal/span_impl.h
@@ -12,8 +12,8 @@
// License for the specific language governing permissions and limitations under
// the License.
-// std::span is a stand-in for C++20's std::span. Do NOT include this header
-// directly; instead, include it as <span>.
+// This span implementation is a stand-in for C++20's std::span. Do NOT include
+// this header directly; instead, include it as "pw_span/span.h".
//
// A span is a non-owning array view class. It refers to an external array by
// storing a pointer and length. Unlike std::array, the size does not have to be
@@ -27,17 +27,13 @@
//
// A few changes were made to the Chromium version of span. These include:
// - Use std::data and std::size instead of base::* versions.
-// - Rename base namespace to std.
+// - Rename base namespace to pw.
// - Rename internal namespace to pw_span_internal.
// - Remove uses of checked_iterators.h and CHECK.
// - Replace make_span functions with C++17 class template deduction guides.
// - Use std::byte instead of uint8_t for compatibility with std::span.
-//
#pragma once
-#ifndef __cpp_lib_span
-#define __cpp_lib_span 202002L
-
#include <algorithm>
#include <array>
#include <cstddef>
@@ -46,13 +42,19 @@
#include <type_traits>
#include <utility>
-#include "pw_polyfill/language_feature_macros.h"
-#include "pw_polyfill/standard_library/namespace.h"
+#include "pw_span/internal/config.h"
+
+#if PW_SPAN_ENABLE_ASSERTS
-// Pigweed: Disable the asserts from Chromium for now.
+#include "pw_assert/assert.h"
+
+#define _PW_SPAN_ASSERT(arg) PW_ASSERT(arg)
+
+#else
#define _PW_SPAN_ASSERT(arg)
+#endif // PW_SPAN_ENABLE_ASSERTS
-_PW_POLYFILL_BEGIN_NAMESPACE_STD
+namespace pw {
// [views.constants]
constexpr size_t dynamic_extent = std::numeric_limits<size_t>::max();
@@ -72,7 +74,7 @@ template <typename T, size_t N>
struct ExtentImpl<std::array<T, N>> : std::integral_constant<size_t, N> {};
template <typename T, size_t N>
-struct ExtentImpl<std::span<T, N>> : std::integral_constant<size_t, N> {};
+struct ExtentImpl<span<T, N>> : std::integral_constant<size_t, N> {};
template <typename T>
using Extent = ExtentImpl<std::remove_cv_t<std::remove_reference_t<T>>>;
@@ -262,6 +264,9 @@ class span : public pw_span_internal::ExtentStorage<Extent> {
_PW_SPAN_ASSERT(Extent == dynamic_extent || Extent == size);
}
+ // Prevent construction from nullptr, which is disallowed by C++20's std::span
+ constexpr span(std::nullptr_t data, size_t size) = delete;
+
// Artificially templatized to break ambiguity for span(ptr, 0).
template <typename = void>
constexpr span(T* begin, T* end) noexcept : span(begin, end - begin) {
@@ -470,7 +475,6 @@ span(const Container&) -> span<pw_span_internal::ValueType<const Container>>;
#endif // __cpp_deduction_guides
-_PW_POLYFILL_END_NAMESPACE_STD
+} // namespace pw
#undef _PW_SPAN_ASSERT
-#endif // __cpp_lib_span
diff --git a/pw_span/public/pw_span/span.h b/pw_span/public/pw_span/span.h
new file mode 100644
index 000000000..3bc9ac559
--- /dev/null
+++ b/pw_span/public/pw_span/span.h
@@ -0,0 +1,46 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// pw::span is an implementation of std::span for C++14 or newer. The
+// implementation is shared with the std::span polyfill class.
+#pragma once
+
+#include "pw_span/internal/config.h"
+
+#if __has_include(<version>)
+#include <version>
+#endif // __has_include(<version>)
+
+// If the C++ library fully supports <span>, pw::span is an alias of std::span,
+// but only if PW_SPAN_ENABLE_ASSERTS is not enabled.
+#if defined(__cpp_lib_span) && __cpp_lib_span >= 202002L && \
+ !PW_SPAN_ENABLE_ASSERTS
+
+#include <span>
+
+namespace pw {
+
+using std::as_bytes;
+using std::as_writable_bytes;
+using std::dynamic_extent;
+using std::span;
+
+} // namespace pw
+
+#else
+
+// If std::span is not available, use Pigweed's span implementation.
+#include "pw_span/internal/span_impl.h" // IWYU pragma: export
+
+#endif // defined(__cpp_lib_span) && __cpp_lib_span >= 202002L
diff --git a/pw_span/public_overrides/span b/pw_span/public_overrides/span
deleted file mode 100644
index 1ac428862..000000000
--- a/pw_span/public_overrides/span
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-#pragma once
-
-#if __has_include_next(<span>)
-#include_next <span>
-#endif // __has_include_next(<span>)
-
-#include "pw_span/internal/span.h"
diff --git a/pw_span/span_test.cc b/pw_span/span_test.cc
index 22e1044b7..de74ad537 100644
--- a/pw_span/span_test.cc
+++ b/pw_span/span_test.cc
@@ -21,15 +21,20 @@
// In order to minimize changes from the original, this file does NOT fully
// adhere to Pigweed's style guide.
+// NOLINTBEGIN(modernize-unary-static-assert)
+
+#include "pw_span/span.h"
+
#include <algorithm>
#include <cstdint>
+#include <cstring>
#include <memory>
-#include <span>
#include <string>
#include <type_traits>
#include <vector>
#include "gtest/gtest.h"
+#include "pw_polyfill/standard.h"
// Pigweed: gMock matchers are not yet supported.
#if 0
@@ -38,8 +43,7 @@ using ::testing::Eq;
using ::testing::Pointwise;
#endif // 0
-namespace std {
-
+namespace pw {
namespace {
// constexpr implementation of std::equal's 4 argument overload.
@@ -58,6 +62,8 @@ constexpr bool constexpr_equal(InputIterator1 first1,
} // namespace
+#ifdef __cpp_deduction_guides
+
TEST(SpanTest, DeductionGuides_MutableArray) {
char array[] = {'a', 'b', 'c', 'd', '\0'};
@@ -102,7 +108,7 @@ TEST(SpanTest, DeductionGuides_ConstStdArray) {
TEST(SpanTest, DeductionGuides_MutableContainerWithConstElements) {
std::string_view string("Hello");
- auto the_span = span(string);
+ auto the_span = span<const char>(string);
static_assert(the_span.extent == dynamic_extent);
EXPECT_STREQ("Hello", the_span.data());
@@ -120,6 +126,8 @@ TEST(SpanTest, DeductionGuides_MutableContainerWithMutableElements) {
EXPECT_STREQ("Hallo", the_span.data());
}
+#endif // __cpp_deduction_guides
+
class MutableStringView {
public:
using element_type = char;
@@ -138,13 +146,15 @@ class MutableStringView {
char& operator[](size_type index) const { return data_[index]; }
pointer data() const { return data_.data(); }
size_type size() const { return data_.size(); }
- iterator begin() const { return data_.begin(); }
- iterator end() const { return data_.end(); }
+ iterator begin() const { return data_.data(); }
+ iterator end() const { return data_.data() + size(); }
private:
span<char> data_;
};
+#ifdef __cpp_deduction_guides
+
TEST(SpanTest, DeductionGuides_ConstContainerWithMutableElements) {
char data[] = "54321";
MutableStringView view(data);
@@ -206,6 +216,8 @@ TEST(SpanTest, DeductionGuides_FromConstReference) {
EXPECT_EQ(string, the_span.data());
}
+#endif // __cpp_deduction_guides
+
TEST(SpanTest, DefaultConstructor) {
span<int> dynamic_span;
EXPECT_EQ(nullptr, dynamic_span.data());
@@ -217,7 +229,7 @@ TEST(SpanTest, DefaultConstructor) {
}
TEST(SpanTest, ConstructFromDataAndSize) {
- constexpr span<int> empty_span(nullptr, 0);
+ constexpr span<int> empty_span(static_cast<int*>(nullptr), 0);
EXPECT_TRUE(empty_span.empty());
EXPECT_EQ(nullptr, empty_span.data());
@@ -239,7 +251,8 @@ TEST(SpanTest, ConstructFromDataAndSize) {
}
TEST(SpanTest, ConstructFromPointerPair) {
- constexpr span<int> empty_span(nullptr, nullptr);
+ constexpr span<int> empty_span(static_cast<int*>(nullptr),
+ static_cast<int*>(nullptr));
EXPECT_TRUE(empty_span.empty());
EXPECT_EQ(nullptr, empty_span.data());
@@ -423,6 +436,8 @@ TEST(SpanTest, ConstructFromStdArray) {
EXPECT_EQ(array[i], static_span[i]);
}
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+
TEST(SpanTest, ConstructFromInitializerList) {
std::initializer_list<int> il = {1, 1, 2, 3, 5, 8};
@@ -466,6 +481,8 @@ TEST(SpanTest, ConstructFromStdString) {
EXPECT_EQ(str[i], static_span[i]);
}
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
TEST(SpanTest, ConstructFromConstContainer) {
const std::vector<int> vector = {1, 1, 2, 3, 5, 8};
@@ -1601,9 +1618,11 @@ TEST(SpanTest, Sort) {
TEST(SpanTest, SpanExtentConversions) {
// Statically checks that various conversions between spans of dynamic and
// static extent are possible or not.
- static_assert(
- !std::is_constructible<span<int, 0>, span<int>>::value,
- "Error: static span should not be constructible from dynamic span");
+
+ // This test fails with the real C++20 std::span, so skip it.
+ // static_assert(
+ // !std::is_constructible<span<int, 0>, span<int>>::value,
+ // "Error: static span should not be constructible from dynamic span");
static_assert(!std::is_constructible<span<int, 2>, span<int, 1>>::value,
"Error: static span should not be constructible from static "
@@ -1629,4 +1648,6 @@ TEST(SpanTest, IteratorConversions) {
"Error: const iterator should not be convertible to iterator");
}
-} // namespace std
+// NOLINTEND(modernize-unary-static-assert)
+
+} // namespace pw
diff --git a/pw_spi/BUILD.bazel b/pw_spi/BUILD.bazel
index 217f7d432..c50f61a0b 100644
--- a/pw_spi/BUILD.bazel
+++ b/pw_spi/BUILD.bazel
@@ -47,6 +47,38 @@ pw_cc_library(
)
pw_cc_library(
+ name = "initiator_mock",
+ testonly = True,
+ srcs = ["initiator_mock.cc"],
+ hdrs = [
+ "public/pw_spi/chip_selector_mock.h",
+ "public/pw_spi/initiator_mock.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":chip_selector",
+ ":initiator",
+ "//pw_assert",
+ "//pw_containers",
+ "//pw_containers:to_array",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "initiator_mock_test",
+ srcs = [
+ "initiator_mock_test.cc",
+ ],
+ deps = [
+ ":initiator_mock",
+ "//pw_bytes",
+ "//pw_containers",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_library(
name = "device",
hdrs = [
"public/pw_spi/device.h",
@@ -90,12 +122,3 @@ pw_cc_library(
"//pw_sync:mutex",
],
)
-
-pw_cc_test(
- name = "linux_spi_test",
- srcs = ["linux_spi_test.cc"],
- deps = [
- ":device",
- ":linux_spi",
- ],
-)
diff --git a/pw_spi/BUILD.gn b/pw_spi/BUILD.gn
index 3c46fc92c..5a683629d 100644
--- a/pw_spi/BUILD.gn
+++ b/pw_spi/BUILD.gn
@@ -61,6 +61,26 @@ pw_source_set("device") {
]
}
+pw_source_set("mock") {
+ public_configs = [ ":public_include_path" ]
+ public = [
+ "public/pw_spi/chip_selector_mock.h",
+ "public/pw_spi/initiator_mock.h",
+ ]
+ sources = [ "initiator_mock.cc" ]
+ public_deps = [
+ ":chip_selector",
+ ":initiator",
+ "$dir_pw_bytes",
+ "$dir_pw_containers:to_array",
+ ]
+ deps = [
+ "$dir_pw_assert",
+ "$dir_pw_containers",
+ "$dir_pw_unit_test",
+ ]
+}
+
# Linux-specific spidev implementation.
pw_source_set("linux_spi") {
public_configs = [ ":public_include_path" ]
@@ -77,7 +97,10 @@ pw_source_set("linux_spi") {
}
pw_test_group("tests") {
- tests = [ ":spi_test" ]
+ tests = [
+ ":spi_test",
+ ":initiator_mock_test",
+ ]
}
pw_test("spi_test") {
@@ -88,13 +111,11 @@ pw_test("spi_test") {
]
}
-# Linux tests currently only work on a target with spidev support and a SPI endpoint
-# mounted at /dev/spidev0.0
-pw_test("linux_spi_test") {
- sources = [ "linux_spi_test.cc" ]
+pw_test("initiator_mock_test") {
+ sources = [ "initiator_mock_test.cc" ]
deps = [
- ":device",
- ":linux_spi",
+ ":mock",
+ "$dir_pw_containers",
]
}
diff --git a/pw_spi/docs.rst b/pw_spi/docs.rst
index 6332e1114..d1ba66823 100644
--- a/pw_spi/docs.rst
+++ b/pw_spi/docs.rst
@@ -87,15 +87,12 @@ Example - Performing a Multi-part Transaction:
std::byte{0x13}, std::byte{0x37}};
// Creation of the RAII `transaction` acquires exclusive access to the bus
- std::optional<pw::spi::Device::Transaction> transaction =
+ pw::spi::Device::Transaction transaction =
device.StartTransaction(pw::spi::ChipSelectBehavior::kPerTransaction);
- if (!transaction.has_value()) {
- return pw::Status::Unknown();
- )
// This device only supports half-duplex transfers
- PW_TRY(transaction->Write(kAccelReportCommand));
- PW_TRY(transaction->Read(raw_sensor_data))
+ PW_TRY(transaction.Write(kAccelReportCommand));
+ PW_TRY(transaction.Read(raw_sensor_data))
return UnpackSensorData(raw_sensor_data);
@@ -144,7 +141,7 @@ method.
.. Note:
- Throughtout ``pw_spi``, the terms "controller" and "peripheral" are used to
+ Throughout ``pw_spi``, the terms "controller" and "peripheral" are used to
describe the two roles SPI devices can implement. These terms correspond
to the "master" and "slave" roles described in legacy documentation
related to the SPI protocol.
@@ -159,7 +156,7 @@ method.
.. cpp:function:: Status Configure(const Config& config)
- Configure the SPI bus to coummunicate using a specific set of properties,
+ Configure the SPI bus to communicate using a specific set of properties,
including the clock polarity, clock phase, bit-order, and bits-per-word.
Returns OkStatus() on success, and implementation-specific values on
@@ -204,7 +201,7 @@ use the SPI HAL to communicate with a peripheral.
SetActive sets the state of the chip-select signal to the value
represented by the `active` parameter. Passing a value of `true` will
- activate the chip-select signal, and `false` will deactive the
+ activate the chip-select signal, and `false` will deactivate the
chip-select signal.
Returns OkStatus() on success, and implementation-specific values on
@@ -227,13 +224,13 @@ use the SPI HAL to communicate with a peripheral.
pw::spi::Device
---------------
This is primary object used by a client to interact with a target SPI device.
-It provides a wrapper for an injected ``pw::spi::Initator`` object, using
+It provides a wrapper for an injected ``pw::spi::Initiator`` object, using
its methods to configure the bus and perform individual SPI transfers. The
injected ``pw::spi::ChipSelector`` object is used internally to activate and
de-actviate the device on-demand from within the data transfer methods.
The ``Read()``/``Write()``/``WriteRead()`` methods provide support for
-performing inidividual transfers: ``Read()`` and ``Write()`` perform
+performing individual transfers: ``Read()`` and ``Write()`` perform
half-duplex operations, where ``WriteRead()`` provides support for
full-duplex transfers.
@@ -255,7 +252,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
Synchronously read data from the SPI peripheral until the provided
`read_buffer` is full.
- This call will configure the bus and activate/deactive chip select
+ This call will configure the bus and activate/deactivate chip select
for the transfer
Note: This call will block in the event that other clients are currently
@@ -267,7 +264,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
.. cpp:function:: Status Write(ConstByteSpan write_buffer)
Synchronously write the contents of `write_buffer` to the SPI peripheral.
- This call will configure the bus and activate/deactive chip select
+ This call will configure the bus and activate/deactivate chip select
for the transfer
Note: This call will block in the event that other clients are currently
@@ -285,7 +282,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
additional input bytes are discarded. In the event the write buffer is
smaller than the read buffer (or zero size), the output is padded with
0-bits for the remainder of the transfer.
- This call will configure the bus and activate/deactive chip select
+ This call will configure the bus and activate/deactivate chip select
for the transfer
Note: This call will block in the event that other clients are currently
@@ -301,7 +298,7 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
underlying SPI bus (Initiator) for the object's duration. The `behavior`
parameter provides a means for a client to select how the chip-select
signal will be applied on Read/Write/WriteRead calls taking place with
- the Transaction object. A value of `kPerWriteRead` will activate/deactive
+ the Transaction object. A value of `kPerWriteRead` will activate/deactivate
chip-select on each operation, while `kPerTransaction` will hold the
chip-select active for the duration of the Transaction object.
@@ -336,3 +333,41 @@ the ``pw::sync::Borrowable`` object, where the ``pw::spi::Initiator`` object is
Returns OkStatus() on success, and implementation-specific values on
failure.
+pw::spi::MockInitiator
+----------------------
+A generic mocked backend for for pw::spi::Initiator. This is specifically
+intended for use when developing drivers for spi devices. This is structured
+around a set of 'transactions' where each transaction contains a write, read and
+a status. A transaction list can then be passed to the MockInitiator, where
+each consecutive call to read/write will iterate to the next transaction in the
+list. An example of this is shown below:
+
+.. code-block:: cpp
+
+ using pw::spi::MakeExpectedTransactionlist;
+ using pw::spi::MockInitiator;
+ using pw::spi::MockWriteTransaction;
+
+ constexpr auto kExpectWrite1 = pw::bytes::Array<1, 2, 3, 4, 5>();
+ constexpr auto kExpectWrite2 = pw::bytes::Array<3, 4, 5>();
+ auto expected_transactions = MakeExpectedTransactionArray(
+ {MockWriteTransaction(pw::OkStatus(), kExpectWrite1),
+ MockWriteTransaction(pw::OkStatus(), kExpectWrite2)});
+ MockInitiator spi_mock(expected_transactions);
+
+ // Begin driver code
+ ConstByteSpan write1 = kExpectWrite1;
+ // write1 is ok as spi_mock expects {1, 2, 3, 4, 5} == {1, 2, 3, 4, 5}
+ Status status = spi_mock.WriteRead(write1, ConstByteSpan());
+
+ // Takes the first two bytes from the expected array to build a mismatching
+ // span to write.
+ ConstByteSpan write2 = pw::span(kExpectWrite2).first(2);
+ // write2 fails as spi_mock expects {3, 4, 5} != {3, 4}
+ status = spi_mock.WriteRead(write2, ConstByteSpan());
+ // End driver code
+
+ // Optionally check if the mocked transaction list has been exhausted.
+ // Alternatively this is also called from MockInitiator::~MockInitiator().
+ EXPECT_EQ(spi_mock.Finalize(), OkStatus());
+
diff --git a/pw_spi/initiator_mock.cc b/pw_spi/initiator_mock.cc
new file mode 100644
index 000000000..d735d3ff5
--- /dev/null
+++ b/pw_spi/initiator_mock.cc
@@ -0,0 +1,49 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_spi/initiator_mock.h"
+
+#include "gtest/gtest.h"
+#include "pw_assert/check.h"
+#include "pw_containers/algorithm.h"
+
+namespace pw::spi {
+
+Status MockInitiator::WriteRead(ConstByteSpan tx_buffer, ByteSpan rx_buffer) {
+ PW_CHECK_INT_LT(expected_transaction_index_, expected_transactions_.size());
+
+ ConstByteSpan expected_tx_buffer =
+ expected_transactions_[expected_transaction_index_].write_buffer();
+ EXPECT_TRUE(pw::containers::Equal(expected_tx_buffer, tx_buffer));
+
+ ConstByteSpan expected_rx_buffer =
+ expected_transactions_[expected_transaction_index_].read_buffer();
+ PW_CHECK_INT_EQ(expected_rx_buffer.size(), rx_buffer.size());
+
+ std::copy(
+ expected_rx_buffer.begin(), expected_rx_buffer.end(), rx_buffer.begin());
+
+ // Do not directly return this value as expected_transaction_index_ should be
+ // incremented.
+ const Status expected_return_value =
+ expected_transactions_[expected_transaction_index_].return_value();
+
+ expected_transaction_index_ += 1;
+
+ return expected_return_value;
+}
+
+MockInitiator::~MockInitiator() { EXPECT_EQ(Finalize(), OkStatus()); }
+
+} // namespace pw::spi
diff --git a/pw_spi/initiator_mock_test.cc b/pw_spi/initiator_mock_test.cc
new file mode 100644
index 000000000..8a1f7a964
--- /dev/null
+++ b/pw_spi/initiator_mock_test.cc
@@ -0,0 +1,92 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_spi/initiator_mock.h"
+
+#include <array>
+
+#include "gtest/gtest.h"
+#include "pw_bytes/array.h"
+#include "pw_bytes/span.h"
+#include "pw_containers/algorithm.h"
+#include "pw_span/span.h"
+
+namespace pw::spi {
+namespace {
+
+TEST(Transaction, Read) {
+ constexpr auto kExpectRead1 = bytes::Array<1, 2, 3, 4, 5>();
+ constexpr auto kExpectRead2 = bytes::Array<3, 4, 5>();
+
+ auto expected_transactions = MakeExpectedTransactionArray(
+ {MockReadTransaction(OkStatus(), kExpectRead1),
+ MockReadTransaction(OkStatus(), kExpectRead2)});
+
+ MockInitiator mocked_spi(expected_transactions);
+
+ std::array<std::byte, kExpectRead1.size()> read1;
+ EXPECT_EQ(mocked_spi.WriteRead(ConstByteSpan(), read1), OkStatus());
+ EXPECT_TRUE(pw::containers::Equal(read1, kExpectRead1));
+
+ std::array<std::byte, kExpectRead2.size()> read2;
+ EXPECT_EQ(mocked_spi.WriteRead(ConstByteSpan(), read2), OkStatus());
+ EXPECT_TRUE(pw::containers::Equal(read2, kExpectRead2));
+
+ EXPECT_EQ(mocked_spi.Finalize(), OkStatus());
+}
+
+TEST(Transaction, Write) {
+ constexpr auto kExpectWrite1 = bytes::Array<1, 2, 3, 4, 5>();
+ constexpr auto kExpectWrite2 = bytes::Array<3, 4, 5>();
+
+ auto expected_transactions = MakeExpectedTransactionArray(
+ {MockWriteTransaction(OkStatus(), kExpectWrite1),
+ MockWriteTransaction(OkStatus(), kExpectWrite2)});
+
+ MockInitiator mocked_spi(expected_transactions);
+
+ EXPECT_EQ(mocked_spi.WriteRead(kExpectWrite1, ByteSpan()), OkStatus());
+
+ EXPECT_EQ(mocked_spi.WriteRead(kExpectWrite2, ByteSpan()), OkStatus());
+
+ EXPECT_EQ(mocked_spi.Finalize(), OkStatus());
+}
+
+TEST(Transaction, WriteRead) {
+ constexpr auto kExpectWrite1 = bytes::Array<1, 2, 3, 4, 5>();
+ constexpr auto kExpectRead1 = bytes::Array<1, 2>();
+
+ constexpr auto kExpectWrite2 = bytes::Array<3, 4, 5>();
+ constexpr auto kExpectRead2 = bytes::Array<3, 4>();
+
+ auto expected_transactions = MakeExpectedTransactionArray({
+ MockTransaction(OkStatus(), kExpectWrite1, kExpectRead1),
+ MockTransaction(OkStatus(), kExpectWrite2, kExpectRead2),
+ });
+
+ MockInitiator mocked_spi(expected_transactions);
+
+ std::array<std::byte, kExpectRead1.size()> read1;
+ EXPECT_EQ(mocked_spi.WriteRead(kExpectWrite1, read1), OkStatus());
+ EXPECT_TRUE(pw::containers::Equal(read1, kExpectRead1));
+
+ std::array<std::byte, kExpectRead2.size()> read2;
+ EXPECT_EQ(mocked_spi.WriteRead(kExpectWrite2, read2), OkStatus());
+ EXPECT_TRUE(pw::containers::Equal(read2, kExpectRead2));
+
+ EXPECT_EQ(mocked_spi.Finalize(), OkStatus());
+}
+
+} // namespace
+} // namespace pw::spi
diff --git a/pw_spi/linux_spi.cc b/pw_spi/linux_spi.cc
index 89a6c071a..cb4847cd8 100644
--- a/pw_spi/linux_spi.cc
+++ b/pw_spi/linux_spi.cc
@@ -20,6 +20,8 @@
#include <sys/ioctl.h>
#include <unistd.h>
+#include <cstring>
+
#include "pw_log/log.h"
#include "pw_spi/chip_selector.h"
#include "pw_spi/device.h"
@@ -34,21 +36,7 @@ LinuxInitiator::~LinuxInitiator() {
}
}
-Status LinuxInitiator::LazyInit() {
- if (fd_ >= 0) {
- return OkStatus();
- }
- fd_ = open(path_, O_RDWR | O_EXCL);
- if (fd_ < 0) {
- PW_LOG_ERROR("Unable to open SPI device %s for read/write", path_);
- return Status::Unavailable();
- }
- return OkStatus();
-}
-
Status LinuxInitiator::Configure(const Config& config) {
- PW_TRY(LazyInit());
-
// Map clock polarity/phase to Linux userspace equivalents
uint32_t mode = 0;
if (config.polarity == ClockPolarity::kActiveLow) {
@@ -90,8 +78,6 @@ Status LinuxInitiator::Configure(const Config& config) {
Status LinuxInitiator::WriteRead(ConstByteSpan write_buffer,
ByteSpan read_buffer) {
- PW_TRY(LazyInit());
-
// Configure a full-duplex transfer using ioctl()
struct spi_ioc_transfer transaction[2];
memset(transaction, 0, sizeof(transaction));
diff --git a/pw_spi/linux_spi_test.cc b/pw_spi/linux_spi_test.cc
deleted file mode 100644
index 23a881b87..000000000
--- a/pw_spi/linux_spi_test.cc
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// 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
-//
-// https://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 "pw_spi/linux_spi.h"
-
-#include <array>
-#include <optional>
-
-#include "gtest/gtest.h"
-#include "pw_spi/chip_selector.h"
-#include "pw_spi/device.h"
-#include "pw_spi/initiator.h"
-#include "pw_status/status.h"
-#include "pw_sync/borrow.h"
-
-namespace pw::spi {
-namespace {
-
-const pw::spi::Config kConfig = {.polarity = ClockPolarity::kActiveHigh,
- .phase = ClockPhase::kFallingEdge,
- .bits_per_word = BitsPerWord(8),
- .bit_order = BitOrder::kMsbFirst};
-
-class LinuxSpi : public ::testing::Test {
- public:
- LinuxSpi()
- : initiator_(LinuxInitiator("/dev/spidev0.0", 1000000)),
- chip_selector_(),
- initiator_lock_(),
- borrowable_initiator_(initiator_, initiator_lock_),
- device_(borrowable_initiator_, kConfig, chip_selector_) {}
-
- Device& device() { return device_; }
-
- private:
- LinuxInitiator initiator_;
- LinuxChipSelector chip_selector_;
- sync::VirtualMutex initiator_lock_;
- sync::Borrowable<Initiator> borrowable_initiator_;
- [[maybe_unused]] Device device_;
-};
-
-TEST_F(LinuxSpi, StartTransaction_Succeeds) {
- // arrange
- std::optional<Device::Transaction> transaction =
- device().StartTransaction(ChipSelectBehavior::kPerWriteRead);
-
- // act
-
- // assert
- EXPECT_TRUE(transaction.has_value());
-}
-
-TEST_F(LinuxSpi, HalfDuplexTransaction_Succeeds) {
- // arrange
- std::optional<Device::Transaction> transaction =
- device().StartTransaction(ChipSelectBehavior::kPerWriteRead);
-
- // act
- ASSERT_TRUE(transaction.has_value());
-
- std::array write_data{std::byte(1), std::byte(2), std::byte(3), std::byte(4)};
- auto write_status = transaction->Write(ConstByteSpan(write_data));
-
- std::array read_data{std::byte(1), std::byte(2), std::byte(3), std::byte(4)};
- auto read_status = transaction->Read(read_data);
-
- // assert
- EXPECT_TRUE(write_status.ok());
- EXPECT_TRUE(read_status.ok());
-}
-
-TEST_F(LinuxSpi, FullDuplexTransaction_Succeeds) {
- // arrange
- std::optional<Device::Transaction> transaction =
- device().StartTransaction(ChipSelectBehavior::kPerWriteRead);
-
- // act
- ASSERT_TRUE(transaction.has_value());
-
- std::array write_data{std::byte(1), std::byte(2), std::byte(3), std::byte(4)};
- std::array read_data{std::byte(0), std::byte(0), std::byte(0), std::byte(0)};
- auto wr_status = transaction->WriteRead(ConstByteSpan(write_data), read_data);
-
- // assert
- EXPECT_TRUE(wr_status.ok());
-}
-
-} // namespace
-} // namespace pw::spi
diff --git a/pw_spi/public/pw_spi/chip_selector.h b/pw_spi/public/pw_spi/chip_selector.h
index 4792623a1..3290f507d 100644
--- a/pw_spi/public/pw_spi/chip_selector.h
+++ b/pw_spi/public/pw_spi/chip_selector.h
@@ -37,7 +37,7 @@ class ChipSelector {
// SetActive sets the state of the chip-select signal to the value represented
// by the `active` parameter. Passing a value of `true` will activate the
- // chip-select signal, and `false` will deactive the chip-select signal.
+ // chip-select signal, and `false` will deactivate the chip-select signal.
// Returns OkStatus() on success, and implementation-specific values on
// failure.
virtual Status SetActive(bool active) = 0;
diff --git a/pw_spi/public/pw_spi/chip_selector_mock.h b/pw_spi/public/pw_spi/chip_selector_mock.h
new file mode 100644
index 000000000..7ab1e980e
--- /dev/null
+++ b/pw_spi/public/pw_spi/chip_selector_mock.h
@@ -0,0 +1,36 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include <cstdint>
+
+#include "pw_spi/chip_selector.h"
+#include "pw_status/status.h"
+
+namespace pw::spi {
+
+class MockChipSelector : public ChipSelector {
+ public:
+ bool is_active() { return active_; }
+
+ private:
+ Status SetActive(bool active) override {
+ active_ = active;
+ return pw::OkStatus();
+ }
+ bool active_ = false;
+};
+
+} // namespace pw::spi
diff --git a/pw_spi/public/pw_spi/device.h b/pw_spi/public/pw_spi/device.h
index db07e9ce9..3f0449653 100644
--- a/pw_spi/public/pw_spi/device.h
+++ b/pw_spi/public/pw_spi/device.h
@@ -14,7 +14,7 @@
#pragma once
-#include <optional>
+#include <utility>
#include "pw_bytes/span.h"
#include "pw_spi/chip_selector.h"
@@ -41,7 +41,7 @@ class Device {
// Synchronously read data from the SPI peripheral until the provided
// `read_buffer` is full.
- // This call will configure the bus and activate/deactive chip select
+ // This call will configure the bus and activate/deactivate chip select
// for the transfer
//
// Note: This call will block in the event that other clients are currently
@@ -51,7 +51,7 @@ class Device {
Status Read(ByteSpan read_buffer) { return WriteRead({}, read_buffer); }
// Synchronously write the contents of `write_buffer` to the SPI peripheral.
- // This call will configure the bus and activate/deactive chip select
+ // This call will configure the bus and activate/deactivate chip select
// for the transfer
//
// Note: This call will block in the event that other clients are currently
@@ -69,7 +69,7 @@ class Device {
// additional input bytes are discarded. In the event the write buffer is
// smaller than the read buffer (or zero size), the output is padded with
// 0-bits for the remainder of the transfer.
- // This call will configure the bus and activate/deactive chip select
+ // This call will configure the bus and activate/deactivate chip select
// for the transfer
//
// Note: This call will block in the event that other clients
@@ -93,7 +93,7 @@ class Device {
(behavior_ == ChipSelectBehavior::kPerTransaction) &&
(!first_write_read_)) {
selector_->Deactivate()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
}
@@ -105,7 +105,7 @@ class Device {
behavior_(other.behavior_),
first_write_read_(other.first_write_read_) {
other.selector_ = nullptr;
- };
+ }
Transaction& operator=(Transaction&& other) {
initiator_ = std::move(other.initiator_);
@@ -192,7 +192,7 @@ class Device {
// underlying SPI bus (Initiator) for the object's duration. The `behavior`
// parameter provides a means for a client to select how the chip-select
// signal will be applied on Read/Write/WriteRead calls taking place with the
- // Transaction object. A value of `kPerWriteRead` will activate/deactive
+ // Transaction object. A value of `kPerWriteRead` will activate/deactivate
// chip-select on each operation, while `kPerTransaction` will hold the
// chip-select active for the duration of the Transaction object.
Transaction StartTransaction(ChipSelectBehavior behavior) {
diff --git a/pw_spi/public/pw_spi/initiator.h b/pw_spi/public/pw_spi/initiator.h
index cc826c234..a750b03be 100644
--- a/pw_spi/public/pw_spi/initiator.h
+++ b/pw_spi/public/pw_spi/initiator.h
@@ -71,7 +71,7 @@ struct Config {
static_assert(sizeof(Config) == sizeof(uint32_t),
"Ensure that the config struct fits in 32-bits");
-// The Inititor class provides an abstract interface used to configure and
+// The Initiator class provides an abstract interface used to configure and
// transmit data using a SPI bus.
class Initiator {
public:
@@ -79,7 +79,7 @@ class Initiator {
// Configure the SPI bus to communicate with peripherals using a given set of
// properties, including the clock polarity, clock phase, bit-order, and
- // bits-per-wrod.
+ // bits-per-word.
// Returns OkStatus() on success, and implementation-specific values on
// failure.
virtual Status Configure(const Config& config) = 0;
diff --git a/pw_spi/public/pw_spi/initiator_mock.h b/pw_spi/public/pw_spi/initiator_mock.h
new file mode 100644
index 000000000..a814261a9
--- /dev/null
+++ b/pw_spi/public/pw_spi/initiator_mock.h
@@ -0,0 +1,123 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <optional>
+
+#include "pw_bytes/span.h"
+#include "pw_containers/to_array.h"
+#include "pw_spi/initiator.h"
+
+namespace pw::spi {
+
+// Represents a complete parameter set for the Initiator::WriteRead().
+class MockTransaction {
+ public:
+ // Same set of parameters as Initiator::WriteRead().
+ constexpr MockTransaction(Status expected_return_value,
+ ConstByteSpan write_buffer,
+ ConstByteSpan read_buffer)
+ : return_value_(expected_return_value),
+ read_buffer_(read_buffer),
+ write_buffer_(write_buffer) {}
+
+ // Gets the buffer that is virtually read.
+ ConstByteSpan read_buffer() const { return read_buffer_; }
+
+ // Gets the buffer that should be written by the driver.
+ ConstByteSpan write_buffer() const { return write_buffer_; }
+
+ // Gets the expected return value.
+ Status return_value() const { return return_value_; }
+
+ private:
+ const Status return_value_;
+ const ConstByteSpan read_buffer_;
+ const ConstByteSpan write_buffer_;
+};
+
+// Read transaction is a helper that constructs a read only transaction.
+constexpr MockTransaction MockReadTransaction(Status expected_return_value,
+ ConstByteSpan read_buffer) {
+ return MockTransaction(expected_return_value, ConstByteSpan(), read_buffer);
+}
+
+// WriteTransaction is a helper that constructs a write only transaction.
+constexpr MockTransaction MockWriteTransaction(Status expected_return_value,
+ ConstByteSpan write_buffer) {
+ return MockTransaction(expected_return_value, write_buffer, ConstByteSpan());
+}
+
+// MockInitiator takes a series of read and/or write transactions and
+// compares them against user/driver input.
+//
+// This mock uses Gtest to ensure that the transactions instantiated meet
+// expectations. This MockedInitiator should be instantiated inside a Gtest test
+// frame.
+class MockInitiator : public pw::spi::Initiator {
+ public:
+ explicit constexpr MockInitiator(span<MockTransaction> transaction_list)
+ : expected_transactions_(transaction_list),
+ expected_transaction_index_(0) {}
+
+ // Should be called at the end of the test to ensure that all expected
+ // transactions have been met.
+ // Returns:
+ // Ok - Success.
+ // OutOfRange - The mocked set of transactions has not been exhausted.
+ Status Finalize() const {
+ if (expected_transaction_index_ != expected_transactions_.size()) {
+ return Status::OutOfRange();
+ }
+ return Status();
+ }
+
+ // Runs Finalize() regardless of whether it was already optionally finalized.
+ ~MockInitiator() override;
+
+ // Implements a mocked backend for the SPI initiator.
+ //
+ // Expects (via Gtest):
+ // tx_buffer == expected_transaction_tx_buffer
+ // tx_buffer.size() == expected_transaction_tx_buffer.size()
+ // rx_buffer.size() == expected_transaction_rx_buffer.size()
+ //
+ // Asserts:
+ // When the number of calls to this method exceed the number of expected
+ // transactions.
+ //
+ // Returns:
+ // Specified transaction return type
+ pw::Status WriteRead(pw::ConstByteSpan, pw::ByteSpan) override;
+
+ pw::Status Configure(const pw::spi::Config& /*config */) override {
+ return pw::OkStatus();
+ }
+
+ private:
+ span<MockTransaction> expected_transactions_;
+ size_t expected_transaction_index_;
+};
+
+// Makes a new SPI transactions list.
+template <size_t kSize>
+constexpr std::array<MockTransaction, kSize> MakeExpectedTransactionArray(
+ const MockTransaction (&transactions)[kSize]) {
+ return containers::to_array(transactions);
+}
+
+} // namespace pw::spi
diff --git a/pw_spi/public/pw_spi/linux_spi.h b/pw_spi/public/pw_spi/linux_spi.h
index 27bfc1b2c..762b3518d 100644
--- a/pw_spi/public/pw_spi/linux_spi.h
+++ b/pw_spi/public/pw_spi/linux_spi.h
@@ -29,10 +29,10 @@ namespace pw::spi {
// Linux userspace implementation of the SPI Initiator
class LinuxInitiator : public Initiator {
public:
- // Configure the Linux Initiator object for use with a bus specified by path,
- // with a maximum bus-speed (in hz).
- constexpr LinuxInitiator(const char* path, uint32_t max_speed_hz)
- : path_(path), max_speed_hz_(max_speed_hz), fd_(-1) {}
+ // Configure the Linux Initiator object for use with a bus file descriptor,
+ // and maximum bus-speed (in hz).
+ constexpr LinuxInitiator(int fd, uint32_t max_speed_hz)
+ : max_speed_hz_(max_speed_hz), fd_(fd) {}
~LinuxInitiator();
// Implements pw::spi::Initiator
@@ -40,9 +40,6 @@ class LinuxInitiator : public Initiator {
Status WriteRead(ConstByteSpan write_buffer, ByteSpan read_buffer) override;
private:
- Status LazyInit();
-
- const char* path_;
uint32_t max_speed_hz_;
int fd_;
};
diff --git a/pw_spi/spi_test.cc b/pw_spi/spi_test.cc
index 21e5178bc..f3b42ae61 100644
--- a/pw_spi/spi_test.cc
+++ b/pw_spi/spi_test.cc
@@ -44,11 +44,11 @@ class SpiTestDevice : public ::testing::Test {
// Stub SPI Initiator/ChipSelect objects, used to exercise public API surface.
class TestInitiator : public Initiator {
public:
- Status Configure(const Config& /*config */) override { return OkStatus(); };
+ Status Configure(const Config& /*config */) override { return OkStatus(); }
Status WriteRead(ConstByteSpan /* write_buffer */,
ByteSpan /* read_buffer */) override {
return OkStatus();
- };
+ }
};
class TestChipSelector : public ChipSelector {
diff --git a/pw_status/Android.bp b/pw_status/Android.bp
new file mode 100644
index 000000000..2d9e31ecd
--- /dev/null
+++ b/pw_status/Android.bp
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_status",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ host_supported: true,
+ srcs: [
+ "status.cc",
+ ],
+}
diff --git a/pw_status/CMakeLists.txt b/pw_status/CMakeLists.txt
index 09c5f94d5..70f2be287 100644
--- a/pw_status/CMakeLists.txt
+++ b/pw_status/CMakeLists.txt
@@ -16,7 +16,7 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_module_config(pw_status_CONFIG)
-pw_add_module_library(pw_status.config
+pw_add_library(pw_status.config INTERFACE
HEADERS
public/pw_status/internal/config.h
PUBLIC_INCLUDES
@@ -25,7 +25,7 @@ pw_add_module_library(pw_status.config
${pw_thread_CONFIG}
)
-pw_add_module_library(pw_status
+pw_add_library(pw_status STATIC
HEADERS
public/pw_status/status.h
public/pw_status/status_with_size.h
@@ -42,7 +42,7 @@ if(Zephyr_FOUND AND CONFIG_PIGWEED_STATUS)
endif()
# Use this for pw_status_CONFIG to require pw::Status objects to be used.
-pw_add_module_library(pw_status.check_if_used
+pw_add_library(pw_status.check_if_used INTERFACE
PUBLIC_DEFINES
PW_STATUS_CFG_CHECK_IF_USED=1
)
@@ -51,7 +51,7 @@ pw_add_test(pw_status.status_test
SOURCES
status_test.cc
status_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_status
GROUPS
modules
@@ -61,7 +61,7 @@ pw_add_test(pw_status.status_test
pw_add_test(pw_status.status_with_size_test
SOURCES
status_with_size_test.cc
- DEPS
+ PRIVATE_DEPS
pw_status
GROUPS
modules
@@ -71,7 +71,7 @@ pw_add_test(pw_status.status_with_size_test
pw_add_test(pw_status.try_test
SOURCES
try_test.cc
- DEPS
+ PRIVATE_DEPS
pw_status
GROUPS
modules
diff --git a/pw_status/py/Android.bp b/pw_status/py/Android.bp
new file mode 100644
index 000000000..074586278
--- /dev/null
+++ b/pw_status/py/Android.bp
@@ -0,0 +1,24 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+python_library_host {
+ name: "pw_status_py_lib",
+ srcs: [
+ "pw_status/*.py",
+ ],
+}
diff --git a/pw_status/py/BUILD.gn b/pw_status/py/BUILD.gn
index 5bd0b69de..c01ee12cd 100644
--- a/pw_status/py/BUILD.gn
+++ b/pw_status/py/BUILD.gn
@@ -24,4 +24,5 @@ pw_python_package("py") {
]
sources = [ "pw_status/__init__.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_status/status.cc b/pw_status/status.cc
index 9adbeab56..41cf311f3 100644
--- a/pw_status/status.cc
+++ b/pw_status/status.cc
@@ -37,6 +37,7 @@ extern "C" const char* pw_StatusString(pw_Status status) {
PW_CASE_RETURN_ENUM_STRING(UNAVAILABLE);
PW_CASE_RETURN_ENUM_STRING(DATA_LOSS);
PW_CASE_RETURN_ENUM_STRING(UNAUTHENTICATED);
+ case PW_STATUS_DO_NOT_USE_RESERVED_FOR_FUTURE_EXPANSION_USE_DEFAULT_IN_SWITCH_INSTEAD_:
default:
return "INVALID STATUS";
}
diff --git a/pw_status/ts/package.json b/pw_status/ts/package.json
deleted file mode 100644
index 37cedac60..000000000
--- a/pw_status/ts/package.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "name": "@pigweed/pw_status",
- "version": "1.0.0",
- "main": "index.js",
- "license": "Apache-2.0",
- "dependencies": {
- }
-}
diff --git a/pw_status/ts/tsconfig.json b/pw_status/ts/tsconfig.json
deleted file mode 100644
index 4ddd637e9..000000000
--- a/pw_status/ts/tsconfig.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "compilerOptions": {
- "allowUnreachableCode": false,
- "allowUnusedLabels": false,
- "declaration": true,
- "forceConsistentCasingInFileNames": true,
- "lib": [
- "es2018",
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "module": "commonjs",
- "noEmitOnError": true,
- "noFallthroughCasesInSwitch": true,
- "noImplicitReturns": true,
- "pretty": true,
- "sourceMap": true,
- "strict": true,
- "target": "es2018",
- "jsx": "react",
- "plugins": [
- {
- "name": "@bazel/tsetse",
- "disabledRules": [
- "must-use-promises"
- ]
- }
- ]
- },
- "exclude": [
- "node_modules"
- ]
-}
diff --git a/pw_status/ts/yarn.lock b/pw_status/ts/yarn.lock
deleted file mode 100644
index fb57ccd13..000000000
--- a/pw_status/ts/yarn.lock
+++ /dev/null
@@ -1,4 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
diff --git a/pw_stm32cube_build/BUILD.gn b/pw_stm32cube_build/BUILD.gn
index 9a6699a8e..1b11f77f5 100644
--- a/pw_stm32cube_build/BUILD.gn
+++ b/pw_stm32cube_build/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_stm32cube_build/py/BUILD.gn b/pw_stm32cube_build/py/BUILD.gn
index 04c2c9526..c69c513f8 100644
--- a/pw_stm32cube_build/py/BUILD.gn
+++ b/pw_stm32cube_build/py/BUILD.gn
@@ -36,4 +36,5 @@ pw_python_package("py") {
"tests/inject_init_test.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/__main__.py b/pw_stm32cube_build/py/pw_stm32cube_build/__main__.py
index fff71baa5..5bfca64de 100644
--- a/pw_stm32cube_build/py/pw_stm32cube_build/__main__.py
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/__main__.py
@@ -18,44 +18,56 @@ import argparse
import pathlib
import sys
-from pw_stm32cube_build import find_files, gen_file_list, icf_to_ld, inject_init
+try:
+ from pw_stm32cube_build import (
+ find_files,
+ gen_file_list,
+ icf_to_ld,
+ inject_init,
+ )
+except ImportError:
+ # Load from this directory if pw_stm32cube_build is not available.
+ import find_files # type: ignore
+ import gen_file_list # type: ignore
+ import icf_to_ld # type: ignore
+ import inject_init # type: ignore
def _parse_args() -> argparse.Namespace:
"""Setup argparse and parse command line args."""
parser = argparse.ArgumentParser()
- subparsers = parser.add_subparsers(dest='command',
- metavar='<command>',
- required=True)
+ subparsers = parser.add_subparsers(
+ dest='command', metavar='<command>', required=True
+ )
gen_file_list_parser = subparsers.add_parser(
- 'gen_file_list', help='generate files.txt for stm32cube directory')
+ 'gen_file_list', help='generate files.txt for stm32cube directory'
+ )
gen_file_list_parser.add_argument('stm32cube_dir', type=pathlib.Path)
find_files_parser = subparsers.add_parser(
- 'find_files', help='find files in stm32cube directory')
+ 'find_files', help='find files in stm32cube directory'
+ )
find_files_parser.add_argument('stm32cube_dir', type=pathlib.Path)
find_files_parser.add_argument('product_str')
- find_files_parser.add_argument('--init',
- default=False,
- action='store_true')
+ find_files_parser.add_argument('--init', default=False, action='store_true')
icf_to_ld_parser = subparsers.add_parser(
- 'icf_to_ld', help='convert stm32cube .icf linker files to .ld')
+ 'icf_to_ld', help='convert stm32cube .icf linker files to .ld'
+ )
icf_to_ld_parser.add_argument('icf_path', type=pathlib.Path)
- icf_to_ld_parser.add_argument('--ld-path',
- nargs=1,
- default=None,
- type=pathlib.Path)
+ icf_to_ld_parser.add_argument(
+ '--ld-path', nargs=1, default=None, type=pathlib.Path
+ )
inject_init_parser = subparsers.add_parser(
- 'inject_init', help='inject `pw_stm32cube_Init()` into startup_*.s')
+ 'inject_init', help='inject `pw_stm32cube_Init()` into startup_*.s'
+ )
inject_init_parser.add_argument('in_startup_path', type=pathlib.Path)
- inject_init_parser.add_argument('--out-startup-path',
- nargs=1,
- default=None,
- type=pathlib.Path)
+ inject_init_parser.add_argument(
+ '--out-startup-path', nargs=1, default=None, type=pathlib.Path
+ )
return parser.parse_args()
@@ -69,12 +81,14 @@ def main():
elif args.command == 'find_files':
find_files.find_files(args.stm32cube_dir, args.product_str, args.init)
elif args.command == 'icf_to_ld':
- icf_to_ld.icf_to_ld(args.icf_path,
- args.ld_path[0] if args.ld_path else None)
+ icf_to_ld.icf_to_ld(
+ args.icf_path, args.ld_path[0] if args.ld_path else None
+ )
elif args.command == 'inject_init':
inject_init.inject_init(
args.in_startup_path,
- args.out_startup_path[0] if args.out_startup_path else None)
+ args.out_startup_path[0] if args.out_startup_path else None,
+ )
sys.exit(0)
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py b/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py
index 6e233ef88..549ca50a7 100644
--- a/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/find_files.py
@@ -48,7 +48,8 @@ def parse_product_str(product_str: str) -> Tuple[str, Set[str], str]:
if len(product_name) < 9:
raise ValueError(
- "Product string too short. Must specify at least the chip model.")
+ "Product string too short. Must specify at least the chip model."
+ )
family = product_name[:7] + 'xx'
name = product_name
@@ -91,8 +92,11 @@ def select_define(defines: Set[str], family_header: str) -> str:
"""
valid_defines = list(
filter(
- lambda x: f'defined({x})' in family_header or f'defined ({x})' in
- family_header, defines))
+ lambda x: f'defined({x})' in family_header
+ or f'defined ({x})' in family_header,
+ defines,
+ )
+ )
if len(valid_defines) != 1:
raise ValueError("Unable to select a valid define")
@@ -112,8 +116,10 @@ def match_filename(product_name: str, filename: str):
False otherwise.
"""
stm32_parts = list(
- filter(lambda x: x.startswith('stm32'),
- re.split(r'\.|_', filename.lower())))
+ filter(
+ lambda x: x.startswith('stm32'), re.split(r'\.|_', filename.lower())
+ )
+ )
if len(stm32_parts) != 1:
return False
@@ -145,34 +151,42 @@ def find_linker_files(
"""
linker_files = list(
filter(
- lambda x:
- (x.endswith('.ld') or x.endswith('.icf')) and '_flash.' in x.lower(
- ), files))
+ lambda x: (x.endswith('.ld') or x.endswith('.icf'))
+ and '_flash.' in x.lower(),
+ files,
+ )
+ )
matching_linker_files = list(
- filter(lambda x: match_filename(product_name,
- pathlib.Path(x).name), linker_files))
+ filter(
+ lambda x: match_filename(product_name, pathlib.Path(x).name),
+ linker_files,
+ )
+ )
matching_ld_files = list(
- filter(lambda x: x.endswith('.ld'), matching_linker_files))
+ filter(lambda x: x.endswith('.ld'), matching_linker_files)
+ )
matching_icf_files = list(
- filter(lambda x: x.endswith('.icf'), matching_linker_files))
+ filter(lambda x: x.endswith('.icf'), matching_linker_files)
+ )
if len(matching_ld_files) > 1 or len(matching_icf_files) > 1:
raise ValueError(
- f'Too many linker file matches for {product_name}.' +
- ' Provide a more specific product string or your own linker script'
+ f'Too many linker file matches for {product_name}. '
+ 'Provide a more specific product string or your own linker script'
)
if not matching_ld_files and not matching_icf_files:
raise ValueError(f'No linker script matching {product_name} found')
- return (stm32cube_path /
- matching_ld_files[0] if matching_ld_files else None,
- stm32cube_path /
- matching_icf_files[0] if matching_icf_files else None)
+ return (
+ stm32cube_path / matching_ld_files[0] if matching_ld_files else None,
+ stm32cube_path / matching_icf_files[0] if matching_icf_files else None,
+ )
-def find_startup_file(product_name: str, files: List[str],
- stm32cube_path: pathlib.Path) -> pathlib.Path:
+def find_startup_file(
+ product_name: str, files: List[str], stm32cube_path: pathlib.Path
+) -> pathlib.Path:
"""Finds startup file for the given product.
Searches for gcc startup files.
@@ -192,8 +206,12 @@ def find_startup_file(product_name: str, files: List[str],
# same filenames, so this looks for a 'gcc' folder in the path.
matching_startup_files = list(
filter(
- lambda f: '/gcc/' in f and f.endswith('.s') and match_filename(
- product_name, f), files))
+ lambda f: '/gcc/' in f
+ and f.endswith('.s')
+ and match_filename(product_name, f),
+ files,
+ )
+ )
if not matching_startup_files:
raise ValueError(f'No matching startup file found for {product_name}')
@@ -201,7 +219,8 @@ def find_startup_file(product_name: str, files: List[str],
return stm32cube_path / matching_startup_files[0]
raise ValueError(
- f'Multiple matching startup files found for {product_name}')
+ f'Multiple matching startup files found for {product_name}'
+ )
_INCLUDE_DIRS = [
@@ -219,8 +238,8 @@ def get_include_dirs(stm32cube_path: pathlib.Path) -> List[pathlib.Path]:
def get_sources_and_headers(
- files: List[str],
- stm32cube_path: pathlib.Path) -> Tuple[List[str], List[str]]:
+ files: List[str], stm32cube_path: pathlib.Path
+) -> Tuple[List[str], List[str]]:
"""Gets list of all sources and headers needed to build the stm32cube hal.
Args:
@@ -234,24 +253,33 @@ def get_sources_and_headers(
`headers` is a list of absolute paths to all needed headers
"""
source_files = filter(
- lambda f: f.startswith('hal_driver/Src') and f.endswith('.c') and
- 'template' not in f, files)
+ lambda f: f.startswith('hal_driver/Src')
+ and f.endswith('.c')
+ and 'template' not in f,
+ files,
+ )
header_files = filter(
- lambda f: (any(f.startswith(dir)
- for dir in _INCLUDE_DIRS)) and f.endswith('.h'), files)
+ lambda f: (any(f.startswith(dir) for dir in _INCLUDE_DIRS))
+ and f.endswith('.h'),
+ files,
+ )
rebase_path = lambda f: str(stm32cube_path / f)
- return list(map(rebase_path,
- source_files)), list(map(rebase_path, header_files))
+ return list(map(rebase_path, source_files)), list(
+ map(rebase_path, header_files)
+ )
def parse_files_txt(stm32cube_path: pathlib.Path) -> List[str]:
"""Reads files.txt into list."""
with open(stm32cube_path / 'files.txt', 'r') as files:
return list(
- filter(lambda x: not x.startswith('#'),
- map(lambda f: f.strip(), files.readlines())))
+ filter(
+ lambda x: not x.startswith('#'),
+ map(lambda f: f.strip(), files.readlines()),
+ )
+ )
def _gn_str_out(name: str, val: Any):
@@ -261,8 +289,9 @@ def _gn_str_out(name: str, val: Any):
def _gn_list_str_out(name: str, val: List[Any]):
"""Outputs list of strings in GN format with correct escaping."""
- list_str = ','.join('"' + str(x).replace('"', r'\"').replace('$', r'\$') +
- '"' for x in val)
+ list_str = ','.join(
+ '"' + str(x).replace('"', r'\"').replace('$', r'\$') + '"' for x in val
+ )
print(f'{name} = [{list_str}]')
@@ -275,11 +304,13 @@ def find_files(stm32cube_path: pathlib.Path, product_str: str, init: bool):
(family, defines, name) = parse_product_str(product_str)
family_header_path = list(
- filter(lambda p: p.endswith(f'/{family}.h'), headers))[0]
+ filter(lambda p: p.endswith(f'/{family}.h'), headers)
+ )[0]
with open(family_header_path, 'rb') as family_header:
- family_header_str = family_header.read().decode('utf-8',
- errors='ignore')
+ family_header_str = family_header.read().decode(
+ 'utf-8', errors='ignore'
+ )
define = select_define(defines, family_header_str)
@@ -291,8 +322,9 @@ def find_files(stm32cube_path: pathlib.Path, product_str: str, init: bool):
if init:
startup_file_path = find_startup_file(name, file_list, stm32cube_path)
- gcc_linker, iar_linker = find_linker_files(name, file_list,
- stm32cube_path)
+ gcc_linker, iar_linker = find_linker_files(
+ name, file_list, stm32cube_path
+ )
_gn_str_out('startup', startup_file_path)
_gn_str_out('gcc_linker', gcc_linker if gcc_linker else '')
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/gen_file_list.py b/pw_stm32cube_build/py/pw_stm32cube_build/gen_file_list.py
index 89d40d47b..8a64fb8a3 100644
--- a/pw_stm32cube_build/py/pw_stm32cube_build/gen_file_list.py
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/gen_file_list.py
@@ -43,9 +43,10 @@ def gen_file_list(stm32cube_dir: pathlib.Path):
file_paths.extend(stm32cube_dir.glob("**/*.ld"))
file_paths.extend(stm32cube_dir.glob("**/*.icf"))
- #TODO: allow arbitrary path for generated file list
+ # TODO(vars): allow arbitrary path for generated file list
with open(stm32cube_dir / "files.txt", "w") as out_file:
out_file.write('# Generated by pw_stm32cube_build/gen_file_list\n')
for file_path in file_paths:
out_file.write(
- file_path.relative_to(stm32cube_dir).as_posix() + '\n')
+ file_path.relative_to(stm32cube_dir).as_posix() + '\n'
+ )
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py b/pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py
index fafede0d4..ef18a642b 100644
--- a/pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/icf_to_ld.py
@@ -51,23 +51,25 @@ def parse_icf(icf_file: str) -> Tuple[Dict, Dict]:
if tokens[1] == 'symbol':
symbols[tokens[2]] = tokens[4].strip(';')
elif tokens[1] == 'region':
- regions[tokens[2].split('_')[0]] = (tokens[5],
- tokens[7].strip('];'))
+ regions[tokens[2].split('_')[0]] = (
+ tokens[5],
+ tokens[7].strip('];'),
+ )
elif tokens[1] == 'block':
blocks[tokens[2]] = {
tokens[4]: tokens[6].strip(','),
- tokens[7]: tokens[9]
+ tokens[7]: tokens[9],
}
parsed_regions = {
- region: (symbols[start] if start in symbols else start,
- symbols[end] if end in symbols else end)
+ region: (
+ symbols[start] if start in symbols else start,
+ symbols[end] if end in symbols else end,
+ )
for region, (start, end) in regions.items()
}
parsed_blocks = {
- name:
- {k: symbols[v] if v in symbols else v
- for k, v in fields.items()}
+ name: {k: symbols[v] if v in symbols else v for k, v in fields.items()}
for name, fields in blocks.items()
}
@@ -143,16 +145,22 @@ MEMORY
SECTIONS
{{
+
+ /* The ARMv8-M architecture requires this is at least aligned to 128 bytes,
+ * and aligned to a power of two that is greater than 4 times the number of
+ * supported exceptions. 512 has been selected as it accommodates most vector
+ * tables.
+ */
.isr_vector :
{{
- . = ALIGN(8);
+ . = ALIGN(512);
KEEP(*(.isr_vector))
- . = ALIGN(8);
+ . = ALIGN(4);
}} >FLASH
.text :
{{
- . = ALIGN(8);
+ . = ALIGN(4);
*(.text)
*(.text*)
*(.glue_7)
@@ -162,73 +170,73 @@ SECTIONS
KEEP (*(.init))
KEEP (*(.fini))
- . = ALIGN(8);
+ . = ALIGN(4);
_etext = .;
}} >FLASH
.rodata :
{{
- . = ALIGN(8);
+ . = ALIGN(4);
*(.rodata)
*(.rodata*)
- . = ALIGN(8);
+ . = ALIGN(4);
}} >FLASH
.ARM.extab : {{
- . = ALIGN(8);
+ . = ALIGN(4);
*(.ARM.extab* .gnu.linkonce.armextab.*)
- . = ALIGN(8);
+ . = ALIGN(4);
}} >FLASH
.ARM : {{
- . = ALIGN(8);
+ . = ALIGN(4);
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
- . = ALIGN(8);
+ . = ALIGN(4);
}} >FLASH
.preinit_array :
{{
- . = ALIGN(8);
+ . = ALIGN(4);
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
- . = ALIGN(8);
+ . = ALIGN(4);
}} >FLASH
.init_array :
{{
- . = ALIGN(8);
+ . = ALIGN(4);
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
- . = ALIGN(8);
+ . = ALIGN(4);
}} >FLASH
.fini_array :
{{
- . = ALIGN(8);
+ . = ALIGN(4);
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT(.fini_array.*)))
KEEP (*(.fini_array*))
PROVIDE_HIDDEN (__fini_array_end = .);
- . = ALIGN(8);
+ . = ALIGN(4);
}} >FLASH
_sidata = LOADADDR(.data);
.data :
{{
- . = ALIGN(8);
+ . = ALIGN(4);
_sdata = .;
*(.data)
*(.data*)
- . = ALIGN(8);
+ . = ALIGN(4);
_edata = .;
}} >RAM AT> FLASH
- . = ALIGN(8);
+ . = ALIGN(4);
.bss :
{{
_sbss = .;
@@ -237,11 +245,16 @@ SECTIONS
*(.bss*)
*(COMMON)
- . = ALIGN(8);
+ . = ALIGN(4);
_ebss = .;
__bss_end__ = _ebss;
}} >RAM
+ /* The ARMv7-M architecture may require 8-byte alignment of the stack pointer
+ * rather than 4 in some contexts and implementations, so this region is
+ * 8-byte aligned (see ARMv7-M Architecture Reference Manual DDI0403E
+ * section B1.5.7).
+ */
._user_heap_stack :
{{
. = ALIGN(8);
diff --git a/pw_stm32cube_build/py/pw_stm32cube_build/inject_init.py b/pw_stm32cube_build/py/pw_stm32cube_build/inject_init.py
index 8a68cb659..26448891a 100644
--- a/pw_stm32cube_build/py/pw_stm32cube_build/inject_init.py
+++ b/pw_stm32cube_build/py/pw_stm32cube_build/inject_init.py
@@ -42,8 +42,11 @@ def add_pre_main_init(startup: str) -> str:
if match is None:
raise ValueError("`bl main` not found in startup script")
- return startup[:match.start(
- )] + '\nbl pw_stm32cube_Init' + startup[match.start():]
+ return (
+ startup[: match.start()]
+ + '\nbl pw_stm32cube_Init'
+ + startup[match.start() :]
+ )
def inject_init(startup_in: pathlib.Path, startup_out: Optional[pathlib.Path]):
@@ -55,8 +58,7 @@ def inject_init(startup_in: pathlib.Path, startup_out: Optional[pathlib.Path]):
If None, output startup script printed to stdout
"""
with open(startup_in, 'rb') as startup_in_file:
- startup_in_str = startup_in_file.read().decode('utf-8',
- errors='ignore')
+ startup_in_str = startup_in_file.read().decode('utf-8', errors='ignore')
startup_out_str = add_pre_main_init(startup_in_str)
diff --git a/pw_stm32cube_build/py/tests/find_files_test.py b/pw_stm32cube_build/py/tests/find_files_test.py
index 882c3bfea..364e2b081 100644
--- a/pw_stm32cube_build/py/tests/find_files_test.py
+++ b/pw_stm32cube_build/py/tests/find_files_test.py
@@ -22,6 +22,7 @@ from pw_stm32cube_build import find_files
class ParseProductStringTest(unittest.TestCase):
"""parse_product_str tests."""
+
def test_start_with_stm32(self):
with self.assertRaises(ValueError):
find_files.parse_product_str('f439zit')
@@ -59,13 +60,13 @@ class ParseProductStringTest(unittest.TestCase):
self.assertEqual(name, 'stm32f439xi')
def test_stm32f439zit6u(self):
- (family, defines,
- name) = find_files.parse_product_str('stm32f439zit6u')
+ (family, defines, name) = find_files.parse_product_str('stm32f439zit6u')
self.assertEqual(family, 'stm32f4xx')
self.assertEqual(
defines,
- {'STM32F439xx', 'STM32F439Zx', 'STM32F439xI', 'STM32F439ZI'})
+ {'STM32F439xx', 'STM32F439Zx', 'STM32F439xI', 'STM32F439ZI'},
+ )
self.assertEqual(name, 'stm32f439zit6u')
def test_stm32l552zet(self):
@@ -74,7 +75,8 @@ class ParseProductStringTest(unittest.TestCase):
self.assertEqual(family, 'stm32l5xx')
self.assertEqual(
defines,
- {'STM32L552xx', 'STM32L552Zx', 'STM32L552xE', 'STM32L552ZE'})
+ {'STM32L552xx', 'STM32L552Zx', 'STM32L552xE', 'STM32L552ZE'},
+ )
self.assertEqual(name, 'stm32l552zet')
def test_stm32l552xc(self):
@@ -94,6 +96,7 @@ class ParseProductStringTest(unittest.TestCase):
class SelectDefineTest(unittest.TestCase):
"""select_define tests."""
+
def test_stm32f412zx_not_found(self):
with self.assertRaises(ValueError):
find_files.select_define({'STM32F412xx', 'STM32F412Zx'}, "")
@@ -101,77 +104,101 @@ class SelectDefineTest(unittest.TestCase):
def test_stm32f412zx_found(self):
define = find_files.select_define(
{'STM32F412xx', 'STM32F412Zx'},
- "asdf\nfdas\n#if defined(STM32F412Zx)\n")
+ "asdf\nfdas\n#if defined(STM32F412Zx)\n",
+ )
self.assertEqual(define, 'STM32F412Zx')
def test_stm32f412zx_multiple_found(self):
with self.assertRaises(ValueError):
- find_files.select_define({
- 'STM32F412xx', 'STM32F412Zx'
- }, "asdf\n#if defined (STM32F412xx)\n#elif defined(STM32F412Zx)\n")
+ find_files.select_define(
+ {'STM32F412xx', 'STM32F412Zx'},
+ "asdf\n#if defined (STM32F412xx)\n#elif defined(STM32F412Zx)\n",
+ )
class MatchFilenameTest(unittest.TestCase):
"""match_filename tests."""
+
def test_stm32f412zx(self):
# Match should fail if product name is not specific enough
self.assertTrue(
- find_files.match_filename('stm32f412zx', 'stm32f412zx_flash.icf'))
+ find_files.match_filename('stm32f412zx', 'stm32f412zx_flash.icf')
+ )
self.assertFalse(
- find_files.match_filename('stm32f412xx', 'stm32f412zx_flash.icf'))
+ find_files.match_filename('stm32f412xx', 'stm32f412zx_flash.icf')
+ )
self.assertTrue(
- find_files.match_filename('stm32f412zx', 'startup_stm32f412zx.s'))
+ find_files.match_filename('stm32f412zx', 'startup_stm32f412zx.s')
+ )
self.assertFalse(
- find_files.match_filename('stm32f412xx', 'startup_stm32f429zx.s'))
+ find_files.match_filename('stm32f412xx', 'startup_stm32f429zx.s')
+ )
def test_stm32f439xx(self):
self.assertTrue(
- find_files.match_filename('stm32f439xx', 'stm32f439xx_flash.icf'))
+ find_files.match_filename('stm32f439xx', 'stm32f439xx_flash.icf')
+ )
self.assertFalse(
- find_files.match_filename('stm32f439xx', 'stm32f429xx_flash.icf'))
+ find_files.match_filename('stm32f439xx', 'stm32f429xx_flash.icf')
+ )
self.assertTrue(
- find_files.match_filename('stm32f439xx', 'startup_stm32f439xx.s'))
+ find_files.match_filename('stm32f439xx', 'startup_stm32f439xx.s')
+ )
self.assertFalse(
- find_files.match_filename('stm32f439xx', 'startup_stm32f429xx.s'))
+ find_files.match_filename('stm32f439xx', 'startup_stm32f429xx.s')
+ )
def test_stm32f439xi(self):
self.assertTrue(
- find_files.match_filename('stm32f439xi', 'stm32f439xx_flash.icf'))
+ find_files.match_filename('stm32f439xi', 'stm32f439xx_flash.icf')
+ )
self.assertFalse(
- find_files.match_filename('stm32f439xi', 'stm32f429xx_flash.icf'))
+ find_files.match_filename('stm32f439xi', 'stm32f429xx_flash.icf')
+ )
self.assertTrue(
- find_files.match_filename('stm32f439xi', 'startup_stm32f439xx.s'))
+ find_files.match_filename('stm32f439xi', 'startup_stm32f439xx.s')
+ )
self.assertFalse(
- find_files.match_filename('stm32f439xi', 'startup_stm32f429xx.s'))
+ find_files.match_filename('stm32f439xi', 'startup_stm32f429xx.s')
+ )
def test_stm32l552zet(self):
self.assertTrue(
- find_files.match_filename('stm32l552zet', 'STM32L552xE_FLASH.ld'))
+ find_files.match_filename('stm32l552zet', 'STM32L552xE_FLASH.ld')
+ )
self.assertTrue(
- find_files.match_filename('stm32l552zet', 'STM32L552xx_FLASH.ld'))
+ find_files.match_filename('stm32l552zet', 'STM32L552xx_FLASH.ld')
+ )
self.assertFalse(
- find_files.match_filename('stm32l552zet', 'STM32L552xC_FLASH.ld'))
+ find_files.match_filename('stm32l552zet', 'STM32L552xC_FLASH.ld')
+ )
self.assertTrue(
- find_files.match_filename('stm32l552zet', 'stm32l552xe_flash.icf'))
+ find_files.match_filename('stm32l552zet', 'stm32l552xe_flash.icf')
+ )
self.assertFalse(
- find_files.match_filename('stm32l552zet', 'stm32l552xc_flash.icf'))
+ find_files.match_filename('stm32l552zet', 'stm32l552xc_flash.icf')
+ )
self.assertTrue(
- find_files.match_filename('stm32l552zet', 'startup_stm32l552xx.s'))
+ find_files.match_filename('stm32l552zet', 'startup_stm32l552xx.s')
+ )
self.assertFalse(
- find_files.match_filename('stm32l552zet', 'startup_stm32l562xx.s'))
+ find_files.match_filename('stm32l552zet', 'startup_stm32l562xx.s')
+ )
class FindLinkerFilesTest(unittest.TestCase):
"""find_linker_files tests."""
+
TEST_PATH = pathlib.Path('/test/path')
def test_stm32f439xx(self):
files = [
'path/to/stm32f439xx_flash.icf',
- 'other/path/to/stm32f439xx_sram.icf'
+ 'other/path/to/stm32f439xx_sram.icf',
]
gcc_linker, iar_linker = find_files.find_linker_files(
- 'stm32f439xx', files, self.TEST_PATH)
+ 'stm32f439xx', files, self.TEST_PATH
+ )
self.assertEqual(gcc_linker, None)
self.assertEqual(iar_linker, self.TEST_PATH / files[0])
@@ -183,7 +210,8 @@ class FindLinkerFilesTest(unittest.TestCase):
'path/to/STM32F439xx_FLASH.ld',
]
gcc_linker, iar_linker = find_files.find_linker_files(
- 'stm32f439xx', files, self.TEST_PATH)
+ 'stm32f439xx', files, self.TEST_PATH
+ )
self.assertEqual(gcc_linker, self.TEST_PATH / files[2])
self.assertEqual(iar_linker, self.TEST_PATH / files[0])
@@ -225,7 +253,8 @@ class FindLinkerFilesTest(unittest.TestCase):
'gcc/linker/STM32L552xE_FLASH.ld',
]
gcc_linker, iar_linker = find_files.find_linker_files(
- 'stm32l552xe', files, self.TEST_PATH)
+ 'stm32l552xe', files, self.TEST_PATH
+ )
self.assertEqual(gcc_linker, self.TEST_PATH / files[-1])
self.assertEqual(iar_linker, self.TEST_PATH / files[2])
@@ -233,6 +262,7 @@ class FindLinkerFilesTest(unittest.TestCase):
class FindStartupFileTest(unittest.TestCase):
"""find_startup_file tests."""
+
TEST_PATH = pathlib.Path('/test/path')
def test_stm32f439xx_none_found(self):
@@ -251,8 +281,9 @@ class FindStartupFileTest(unittest.TestCase):
'path/iar/startup_stm32f439xx.s',
'path/gcc/startup_stm32f439xx.s',
]
- startup_file = find_files.find_startup_file('stm32f439xx', files,
- self.TEST_PATH)
+ startup_file = find_files.find_startup_file(
+ 'stm32f439xx', files, self.TEST_PATH
+ )
self.assertEqual(startup_file, self.TEST_PATH / files[3])
@@ -269,6 +300,7 @@ class FindStartupFileTest(unittest.TestCase):
class GetSourceAndHeadersTest(unittest.TestCase):
"""test_sources_and_headers tests."""
+
def test_sources_and_headers(self):
files = [
'random/header.h',
@@ -285,18 +317,26 @@ class GetSourceAndHeadersTest(unittest.TestCase):
path = pathlib.Path('/test/path/to/stm32cube')
sources, headers = find_files.get_sources_and_headers(files, path)
self.assertSetEqual(
- set([
- str(path / 'hal_driver/Src/stm32f4xx_hal_adc.c'),
- str(path / 'hal_driver/Src/stm32f4xx_hal_eth.c'),
- ]), set(sources))
+ set(
+ [
+ str(path / 'hal_driver/Src/stm32f4xx_hal_adc.c'),
+ str(path / 'hal_driver/Src/stm32f4xx_hal_eth.c'),
+ ]
+ ),
+ set(sources),
+ )
self.assertSetEqual(
- set([
- str(path / 'cmsis_core/Include/core_cm4.h'),
- str(path / 'cmsis_device/Include/stm32f4xx.h'),
- str(path / 'cmsis_device/Include/stm32f439xx.h'),
- str(path / 'hal_driver/Inc/stm32f4xx_hal_eth.h'),
- str(path / 'hal_driver/Inc/stm32f4xx_hal.h'),
- ]), set(headers))
+ set(
+ [
+ str(path / 'cmsis_core/Include/core_cm4.h'),
+ str(path / 'cmsis_device/Include/stm32f4xx.h'),
+ str(path / 'cmsis_device/Include/stm32f439xx.h'),
+ str(path / 'hal_driver/Inc/stm32f4xx_hal_eth.h'),
+ str(path / 'hal_driver/Inc/stm32f4xx_hal.h'),
+ ]
+ ),
+ set(headers),
+ )
if __name__ == '__main__':
diff --git a/pw_stm32cube_build/py/tests/icf_to_ld_test.py b/pw_stm32cube_build/py/tests/icf_to_ld_test.py
index 3d76d9163..892eb5fc7 100644
--- a/pw_stm32cube_build/py/tests/icf_to_ld_test.py
+++ b/pw_stm32cube_build/py/tests/icf_to_ld_test.py
@@ -21,6 +21,7 @@ from pw_stm32cube_build import icf_to_ld
class ParseIcfTest(unittest.TestCase):
"""parse_icf tests."""
+
TEST_ICF_1 = """
/*test comments*/
// some other comments
@@ -106,73 +107,79 @@ place in RAM_region { readwrite,
'ROM': ('0x08000000', '0x081FFFFF'),
'RAM': ('0x20000000', '0x2002FFFF'),
'CCMRAM': ('0x10000000', '0x1000FFFF'),
- }, regions)
+ },
+ regions,
+ )
self.assertEqual(
{
- 'CSTACK': {
- 'alignment': '8',
- 'size': '0x400'
- },
- 'HEAP': {
- 'alignment': '8',
- 'size': '0x200'
- },
- }, blocks)
+ 'CSTACK': {'alignment': '8', 'size': '0x400'},
+ 'HEAP': {'alignment': '8', 'size': '0x200'},
+ },
+ blocks,
+ )
class IcfRegionsToLdRegionsTest(unittest.TestCase):
"""icf_regions_to_ld_regions tests."""
+
def test_icf_region(self):
- ld_regions = icf_to_ld.icf_regions_to_ld_regions({
- 'ROM': ('0x08000000', '0x081FFFFF'),
- 'RAM': ('0x20000000', '0x2002FFFF'),
- 'CCMRAM': ('0x10000000', '0x1000FFFF'),
- })
+ ld_regions = icf_to_ld.icf_regions_to_ld_regions(
+ {
+ 'ROM': ('0x08000000', '0x081FFFFF'),
+ 'RAM': ('0x20000000', '0x2002FFFF'),
+ 'CCMRAM': ('0x10000000', '0x1000FFFF'),
+ }
+ )
self.assertEqual(
{
'FLASH': ('0x08000000', '2048K'),
'RAM': ('0x20000000', '192K'),
'CCMRAM': ('0x10000000', '64K'),
- }, ld_regions)
+ },
+ ld_regions,
+ )
def test_icf_region_off_by_one(self):
- ld_regions = icf_to_ld.icf_regions_to_ld_regions({
- 'ROM': ('0x08000000', '0x080FFFFF'),
- 'RAM': ('0x20000000', '0x20020000'),
- })
+ ld_regions = icf_to_ld.icf_regions_to_ld_regions(
+ {
+ 'ROM': ('0x08000000', '0x080FFFFF'),
+ 'RAM': ('0x20000000', '0x20020000'),
+ }
+ )
self.assertEqual(
{
'FLASH': ('0x08000000', '1024K'),
'RAM': ('0x20000000', '128K'),
- }, ld_regions)
+ },
+ ld_regions,
+ )
class CreateLdTest(unittest.TestCase):
"""create_ld tests."""
+
def test_create_ld(self):
ld_str = icf_to_ld.create_ld(
{
'FLASH': ('0x08000000', '2048K'),
'RAM': ('0x20000000', '192K'),
'CCMRAM': ('0x10000000', '64K'),
- }, {
- 'CSTACK': {
- 'alignment': '8',
- 'size': '0x400'
- },
- 'HEAP': {
- 'alignment': '8',
- 'size': '0x200'
- },
- })
+ },
+ {
+ 'CSTACK': {'alignment': '8', 'size': '0x400'},
+ 'HEAP': {'alignment': '8', 'size': '0x200'},
+ },
+ )
self.assertTrue(
- 'RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K' in ld_str)
+ 'RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K' in ld_str
+ )
self.assertTrue(
- 'FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K' in ld_str)
+ 'FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K' in ld_str
+ )
if __name__ == '__main__':
diff --git a/pw_stm32cube_build/py/tests/inject_init_test.py b/pw_stm32cube_build/py/tests/inject_init_test.py
index b08cf359e..5ce2dd6ac 100644
--- a/pw_stm32cube_build/py/tests/inject_init_test.py
+++ b/pw_stm32cube_build/py/tests/inject_init_test.py
@@ -21,79 +21,92 @@ from pw_stm32cube_build import inject_init
class AddPreMainInitTest(unittest.TestCase):
"""add_pre_main_init tests."""
- def test_spaces(self):
- startup = '\n'.join([
- '/* Call the clock system intitialization function.*/',
- ' bl SystemInit ',
- '/* Call static constructors */',
- ' bl __libc_init_array',
- '/* Call the application\'s entry point.*/',
- ' bl main',
- ' bx lr ',
- '.size Reset_Handler, .-Reset_Handler',
- ])
-
- new_startup = inject_init.add_pre_main_init(startup)
- self.assertEqual(
- new_startup, '\n'.join([
+ def test_spaces(self):
+ startup = '\n'.join(
+ [
'/* Call the clock system intitialization function.*/',
' bl SystemInit ',
'/* Call static constructors */',
' bl __libc_init_array',
'/* Call the application\'s entry point.*/',
- 'bl pw_stm32cube_Init',
' bl main',
' bx lr ',
'.size Reset_Handler, .-Reset_Handler',
- ]))
-
- def test_tabs(self):
- startup = '\n'.join([
- 'LoopFillZerobss:',
- ' ldr r3, = _ebss',
- ' cmp r2, r3',
- ' bcc FillZerobss',
- ''
- '/* Call static constructors */',
- ' bl __libc_init_array',
- '/* Call the application\'s entry point.*/',
- ' bl main',
- '',
- 'LoopForever:',
- ' b LoopForever',
- ])
+ ]
+ )
new_startup = inject_init.add_pre_main_init(startup)
self.assertEqual(
- new_startup, '\n'.join([
+ new_startup,
+ '\n'.join(
+ [
+ '/* Call the clock system intitialization function.*/',
+ ' bl SystemInit ',
+ '/* Call static constructors */',
+ ' bl __libc_init_array',
+ '/* Call the application\'s entry point.*/',
+ 'bl pw_stm32cube_Init',
+ ' bl main',
+ ' bx lr ',
+ '.size Reset_Handler, .-Reset_Handler',
+ ]
+ ),
+ )
+
+ def test_tabs(self):
+ startup = '\n'.join(
+ [
'LoopFillZerobss:',
' ldr r3, = _ebss',
' cmp r2, r3',
' bcc FillZerobss',
- ''
'/* Call static constructors */',
' bl __libc_init_array',
'/* Call the application\'s entry point.*/',
- 'bl pw_stm32cube_Init',
' bl main',
'',
'LoopForever:',
' b LoopForever',
- ]))
+ ]
+ )
+
+ new_startup = inject_init.add_pre_main_init(startup)
+
+ self.assertEqual(
+ new_startup,
+ '\n'.join(
+ [
+ 'LoopFillZerobss:',
+ ' ldr r3, = _ebss',
+ ' cmp r2, r3',
+ ' bcc FillZerobss',
+ '/* Call static constructors */',
+ ' bl __libc_init_array',
+ '/* Call the application\'s entry point.*/',
+ 'bl pw_stm32cube_Init',
+ ' bl main',
+ '',
+ 'LoopForever:',
+ ' b LoopForever',
+ ]
+ ),
+ )
def test_main_not_found(self):
- startup = '\n'.join([
- '/* Call the clock system intitialization function.*/',
- ' bl SystemInit ',
- '/* Call static constructors */',
- ' bl __libc_init_array',
- '/* Call the application\'s entry point.*/',
- ' bl application_entry',
- ' bx lr ',
- '.size Reset_Handler, .-Reset_Handler',
- ])
+ startup = '\n'.join(
+ [
+ '/* Call the clock system intitialization function.*/',
+ ' bl SystemInit ',
+ '/* Call static constructors */',
+ ' bl __libc_init_array',
+ '/* Call the application\'s entry point.*/',
+ ' bl application_entry',
+ ' bx lr ',
+ '.size Reset_Handler, .-Reset_Handler',
+ ]
+ )
with self.assertRaises(ValueError):
inject_init.add_pre_main_init(startup)
diff --git a/pw_stream/Android.bp b/pw_stream/Android.bp
new file mode 100644
index 000000000..b1e16c64b
--- /dev/null
+++ b/pw_stream/Android.bp
@@ -0,0 +1,41 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_stream",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
+ host_supported: true,
+ srcs: [
+ "memory_stream.cc",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_status",
+ ],
+}
diff --git a/pw_stream/BUILD.bazel b/pw_stream/BUILD.bazel
index 0d716a6c3..524e2082a 100644
--- a/pw_stream/BUILD.bazel
+++ b/pw_stream/BUILD.bazel
@@ -38,7 +38,6 @@ pw_cc_library(
"//pw_assert",
"//pw_bytes",
"//pw_polyfill",
- "//pw_polyfill:iterator",
"//pw_result",
"//pw_span",
"//pw_status",
@@ -52,6 +51,7 @@ pw_cc_library(
deps = [
":pw_stream",
"//pw_log",
+ "//pw_string",
"//pw_sys_io",
],
)
@@ -69,7 +69,10 @@ pw_cc_library(
name = "std_file_stream",
srcs = ["std_file_stream.cc"],
hdrs = ["public/pw_stream/std_file_stream.h"],
- deps = [":pw_stream"],
+ deps = [
+ ":pw_stream",
+ "//pw_assert",
+ ],
)
pw_cc_library(
@@ -91,6 +94,31 @@ pw_cc_test(
)
pw_cc_test(
+ name = "null_stream_test",
+ srcs = ["null_stream_test.cc"],
+ deps = [
+ ":pw_stream",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
+ name = "std_file_stream_test",
+ srcs = ["std_file_stream_test.cc"],
+ deps = [
+ ":std_file_stream",
+ "//pw_assert",
+ "//pw_bytes",
+ "//pw_containers",
+ "//pw_random",
+ "//pw_result",
+ "//pw_status",
+ "//pw_string",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "seek_test",
srcs = ["seek_test.cc"],
deps = [
@@ -116,3 +144,12 @@ pw_cc_test(
"//pw_unit_test",
],
)
+
+pw_cc_test(
+ name = "socket_stream_test",
+ srcs = ["socket_stream_test.cc"],
+ deps = [
+ ":socket_stream",
+ "//pw_unit_test",
+ ],
+)
diff --git a/pw_stream/BUILD.gn b/pw_stream/BUILD.gn
index 1f6a9da06..9f4eb2f8f 100644
--- a/pw_stream/BUILD.gn
+++ b/pw_stream/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
@@ -37,6 +38,7 @@ pw_source_set("pw_stream") {
dir_pw_bytes,
dir_pw_polyfill,
dir_pw_result,
+ dir_pw_span,
dir_pw_status,
]
}
@@ -44,7 +46,11 @@ pw_source_set("pw_stream") {
pw_source_set("socket_stream") {
public_configs = [ ":public_include_path" ]
public_deps = [ ":pw_stream" ]
- deps = [ dir_pw_log ]
+ deps = [
+ dir_pw_assert,
+ dir_pw_log,
+ dir_pw_string,
+ ]
sources = [ "socket_stream.cc" ]
public = [ "public/pw_stream/socket_stream.h" ]
}
@@ -61,6 +67,7 @@ pw_source_set("sys_io_stream") {
pw_source_set("std_file_stream") {
public_configs = [ ":public_include_path" ]
public_deps = [ ":pw_stream" ]
+ deps = [ "$dir_pw_assert" ]
public = [ "public/pw_stream/std_file_stream.h" ]
sources = [ "std_file_stream.cc" ]
}
@@ -84,9 +91,20 @@ pw_test_group("tests") {
tests = [
":interval_reader_test",
":memory_stream_test",
+ ":null_stream_test",
":seek_test",
":stream_test",
]
+
+ if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
+ pw_toolchain_SCOPE.is_host_toolchain) {
+ tests += [ ":std_file_stream_test" ]
+
+ # socket_stream_test doesn't compile on Windows.
+ if (host_os != "win") {
+ tests += [ ":socket_stream_test" ]
+ }
+ }
}
pw_test("memory_stream_test") {
@@ -94,6 +112,25 @@ pw_test("memory_stream_test") {
deps = [ ":pw_stream" ]
}
+pw_test("null_stream_test") {
+ sources = [ "null_stream_test.cc" ]
+ deps = [ ":pw_stream" ]
+}
+
+pw_test("std_file_stream_test") {
+ sources = [ "std_file_stream_test.cc" ]
+ deps = [
+ ":std_file_stream",
+ "$dir_pw_assert",
+ "$dir_pw_bytes",
+ "$dir_pw_containers",
+ "$dir_pw_random",
+ "$dir_pw_result",
+ "$dir_pw_status",
+ "$dir_pw_string",
+ ]
+}
+
pw_test("seek_test") {
sources = [ "seek_test.cc" ]
deps = [ ":pw_stream" ]
@@ -108,3 +145,8 @@ pw_test("interval_reader_test") {
sources = [ "interval_reader_test.cc" ]
deps = [ ":interval_reader" ]
}
+
+pw_test("socket_stream_test") {
+ sources = [ "socket_stream_test.cc" ]
+ deps = [ ":socket_stream" ]
+}
diff --git a/pw_stream/CMakeLists.txt b/pw_stream/CMakeLists.txt
index a149210da..dfa45823f 100644
--- a/pw_stream/CMakeLists.txt
+++ b/pw_stream/CMakeLists.txt
@@ -14,7 +14,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_stream
+pw_add_library(pw_stream STATIC
HEADERS
public/pw_stream/memory_stream.h
public/pw_stream/null_stream.h
@@ -28,15 +28,15 @@ pw_add_module_library(pw_stream
pw_assert
pw_bytes
pw_polyfill
- pw_polyfill.span
pw_result
+ pw_span
pw_status
)
if(Zephyr_FOUND AND CONFIG_PIGWEED_STREAM)
zephyr_link_libraries(pw_stream)
endif()
-pw_add_module_library(pw_stream.socket_stream
+pw_add_library(pw_stream.socket_stream STATIC
HEADERS
public/pw_stream/socket_stream.h
PUBLIC_INCLUDES
@@ -47,9 +47,10 @@ pw_add_module_library(pw_stream.socket_stream
socket_stream.cc
PRIVATE_DEPS
pw_log
+ pw_string
)
-pw_add_module_library(pw_stream.sys_io_stream
+pw_add_library(pw_stream.sys_io_stream INTERFACE
HEADERS
public/pw_stream/sys_io_stream.h
PUBLIC_INCLUDES
@@ -59,7 +60,7 @@ pw_add_module_library(pw_stream.sys_io_stream
pw_sys_io
)
-pw_add_module_library(pw_stream.std_file_stream
+pw_add_library(pw_stream.std_file_stream STATIC
HEADERS
public/pw_stream/std_file_stream.h
PUBLIC_INCLUDES
@@ -70,7 +71,7 @@ pw_add_module_library(pw_stream.std_file_stream
std_file_stream.cc
)
-pw_add_module_library(pw_stream.interval_reader
+pw_add_library(pw_stream.interval_reader STATIC
HEADERS
public/pw_stream/interval_reader.h
PUBLIC_INCLUDES
@@ -86,7 +87,17 @@ pw_add_module_library(pw_stream.interval_reader
pw_add_test(pw_stream.memory_stream_test
SOURCES
memory_stream_test.cc
- DEPS
+ PRIVATE_DEPS
+ pw_stream
+ GROUPS
+ modules
+ pw_stream
+)
+
+pw_add_test(pw_stream.null_stream_test
+ SOURCES
+ null_stream_test.cc
+ PRIVATE_DEPS
pw_stream
GROUPS
modules
@@ -96,7 +107,7 @@ pw_add_test(pw_stream.memory_stream_test
pw_add_test(pw_stream.seek_test
SOURCES
seek_test.cc
- DEPS
+ PRIVATE_DEPS
pw_stream
GROUPS
modules
@@ -106,7 +117,7 @@ pw_add_test(pw_stream.seek_test
pw_add_test(pw_stream.stream_test
SOURCES
stream_test.cc
- DEPS
+ PRIVATE_DEPS
pw_stream
GROUPS
modules
@@ -116,7 +127,7 @@ pw_add_test(pw_stream.stream_test
pw_add_test(pw_stream.interval_reader_test
SOURCES
interval_reader_test.cc
- DEPS
+ PRIVATE_DEPS
pw_stream.interval_reader
GROUPS
modules
diff --git a/pw_stream/Kconfig b/pw_stream/Kconfig
index 8b8bb235c..a0d41dd45 100644
--- a/pw_stream/Kconfig
+++ b/pw_stream/Kconfig
@@ -19,3 +19,4 @@ config PIGWEED_STREAM
select PIGWEED_RESULT
select PIGWEED_SPAN
select PIGWEED_STATUS
+ select PIGWEED_SYS_IO
diff --git a/pw_stream/docs.rst b/pw_stream/docs.rst
index 951e9d456..a4aaeb913 100644
--- a/pw_stream/docs.rst
+++ b/pw_stream/docs.rst
@@ -86,7 +86,7 @@ Interface documentation
=======================
Summary documentation for the ``pw_stream`` interfaces is below. See the API
comments in `pw_stream/public/pw_stream/stream.h
-<https://cs.opensource.google/pigweed/pigweed/+/main:pw_stream/public/pw_stream/stream.h>`_
+<https://cs.pigweed.dev/pigweed/+/main:pw_stream/public/pw_stream/stream.h>`_
for full details.
.. cpp:class:: Stream
@@ -209,7 +209,7 @@ Reader interfaces
-----------------
.. cpp:class:: Reader : public Stream
- A Stream that supports writing but not reading. The Write() method is hidden.
+ A Stream that supports reading but not writing. The Write() method is hidden.
Use in APIs when:
* Must read from, but not write to, a stream.
@@ -399,11 +399,20 @@ Implementations
The ``MemoryReader`` class implements the :cpp:class:`Reader` interface by
backing the data source with an **externally-provided** memory buffer.
-.. cpp:class:: NullReaderWriter : public SeekableReaderWriter
+.. cpp:class:: NullStream : public SeekableReaderWriter
- ``NullReaderWriter`` is a no-op stream implementation, similar to
- ``/dev/null``. Writes are always dropped. Reads always return
- ``OUT_OF_RANGE``. Seeks have no effect.
+ ``NullStream`` is a no-op stream implementation, similar to ``/dev/null``.
+ Writes are always dropped. Reads always return ``OUT_OF_RANGE``. Seeks have no
+ effect.
+
+.. cpp:class:: CountingNullStream : public SeekableReaderWriter
+
+ ``CountingNullStream`` is a no-op stream implementation, like
+ :cpp:class:`NullStream`, that counts the number of bytes written.
+
+ .. cpp:function:: size_t bytes_written() const
+
+ Returns the number of bytes provided to previous ``Write()`` calls.
.. cpp:class:: StdFileWriter : public SeekableWriter
@@ -415,6 +424,17 @@ Implementations
``StdFileReader`` wraps an ``std::ifstream`` with the :cpp:class:`Reader`
interface.
+.. cpp:class:: SocketStream : public NonSeekableReaderWriter
+
+ ``SocketStream`` wraps posix-style TCP sockets with the :cpp:class:`Reader`
+ and :cpp:class:`Writer` interfaces. It can be used to connect to a TCP server,
+ or to communicate with a client via the ``ServerSocket`` class.
+
+.. cpp:class:: ServerSocket
+
+ ``ServerSocket`` wraps a posix server socket, and produces a
+ :cpp:class:`SocketStream` for each accepted client connection.
+
------------------
Why use pw_stream?
------------------
diff --git a/pw_stream/interval_reader.cc b/pw_stream/interval_reader.cc
index cbe4d7a3d..3708e9b8c 100644
--- a/pw_stream/interval_reader.cc
+++ b/pw_stream/interval_reader.cc
@@ -80,4 +80,4 @@ Status IntervalReader::DoSeek(ptrdiff_t offset, Whence origin) {
return OkStatus();
}
-}; // namespace pw::stream
+} // namespace pw::stream
diff --git a/pw_stream/interval_reader_test.cc b/pw_stream/interval_reader_test.cc
index 663e45ba8..7b697a7ac 100644
--- a/pw_stream/interval_reader_test.cc
+++ b/pw_stream/interval_reader_test.cc
@@ -23,7 +23,7 @@ namespace {
TEST(IntervalReader, IntervalReaderRead) {
std::uint8_t data[] = {0, 1, 2, 3, 4, 5, 6, 7, 9, 10};
- stream::MemoryReader reader(std::as_bytes(std::span(data)));
+ stream::MemoryReader reader(as_bytes(span(data)));
IntervalReader reader_first_half(reader, 0, 5);
IntervalReader reader_second_half(reader, 5, 10);
@@ -53,7 +53,7 @@ TEST(IntervalReader, IntervalReaderRead) {
TEST(IntervalReader, IntervalReaderSeek) {
std::uint8_t data[] = {0, 1, 2, 3, 4, 5, 6, 7, 9, 10};
- stream::MemoryReader reader(std::as_bytes(std::span(data)));
+ stream::MemoryReader reader(as_bytes(span(data)));
IntervalReader interval_reader(reader, 0, 10);
// Absolute seeking.
diff --git a/pw_stream/memory_stream_test.cc b/pw_stream/memory_stream_test.cc
index 04fa8fb71..1b7c0f967 100644
--- a/pw_stream/memory_stream_test.cc
+++ b/pw_stream/memory_stream_test.cc
@@ -14,6 +14,8 @@
#include "pw_stream/memory_stream.h"
+#include <cstring>
+
#include "gtest/gtest.h"
#include "pw_preprocessor/compiler.h"
@@ -33,8 +35,7 @@ constexpr TestStruct kExpectedStruct = {.day = 18, .month = 5, .year = 2020};
class MemoryWriterTest : public ::testing::Test {
protected:
- MemoryWriterTest() : memory_buffer_ {}
- {}
+ MemoryWriterTest() : memory_buffer_{} {}
std::array<std::byte, kSinkBufferSize> memory_buffer_;
};
@@ -61,7 +62,7 @@ TEST_F(MemoryWriterTest, ValidateContents) {
EXPECT_TRUE(
memory_writer.Write(&kExpectedStruct, sizeof(kExpectedStruct)).ok());
- std::span<const std::byte> written_data = memory_writer.WrittenData();
+ span<const std::byte> written_data = memory_writer.WrittenData();
EXPECT_EQ(written_data.size_bytes(), sizeof(kExpectedStruct));
TestStruct temp;
std::memcpy(&temp, written_data.data(), written_data.size_bytes());
@@ -82,14 +83,13 @@ TEST_F(MemoryWriterTest, MultipleWrites) {
for (size_t i = 0; i < sizeof(buffer); ++i) {
buffer[i] = std::byte(counter++);
}
- EXPECT_EQ(memory_writer.Write(std::span(buffer)), OkStatus());
+ EXPECT_EQ(memory_writer.Write(span(buffer)), OkStatus());
}
EXPECT_GT(memory_writer.ConservativeWriteLimit(), 0u);
EXPECT_LT(memory_writer.ConservativeWriteLimit(), kTempBufferSize);
- EXPECT_EQ(memory_writer.Write(std::span(buffer)),
- Status::ResourceExhausted());
+ EXPECT_EQ(memory_writer.Write(span(buffer)), Status::ResourceExhausted());
EXPECT_EQ(memory_writer.bytes_written(), counter);
counter = 0;
@@ -112,13 +112,12 @@ TEST_F(MemoryWriterTest, FullWriter) {
while (memory_writer.ConservativeWriteLimit() > 0) {
size_t bytes_to_write =
std::min(sizeof(buffer), memory_writer.ConservativeWriteLimit());
- EXPECT_EQ(memory_writer.Write(std::span(buffer, bytes_to_write)),
- OkStatus());
+ EXPECT_EQ(memory_writer.Write(span(buffer, bytes_to_write)), OkStatus());
}
EXPECT_EQ(memory_writer.ConservativeWriteLimit(), 0u);
- EXPECT_EQ(memory_writer.Write(std::span(buffer)), Status::OutOfRange());
+ EXPECT_EQ(memory_writer.Write(span(buffer)), Status::OutOfRange());
EXPECT_EQ(memory_writer.bytes_written(), memory_buffer_.size());
for (const std::byte& value : memory_writer) {
@@ -254,7 +253,7 @@ TEST(MemoryReader, EmptySpanRead) {
std::array<std::byte, kTempBufferSize> source;
// Use a span with nullptr and zero length;
- ByteSpan dest(nullptr, 0);
+ ByteSpan dest;
EXPECT_EQ(dest.size_bytes(), 0u);
MemoryReader memory_reader(source);
@@ -334,7 +333,7 @@ TEST(MemoryReader, MultipleReads) {
TEST(MemoryReader, Seek) {
constexpr std::string_view data = "0123456789";
- MemoryReader reader(std::as_bytes(std::span(data)));
+ MemoryReader reader(as_bytes(span(data)));
char buffer[5] = {}; // Leave a null terminator at the end.
ASSERT_EQ(OkStatus(), reader.Read(buffer, sizeof(buffer) - 1).status());
@@ -350,18 +349,18 @@ TEST(MemoryReader, Seek) {
}
TEST(MemoryReader, Tell_StartsAt0) {
- MemoryReader reader(std::as_bytes(std::span("\3\2\1")));
+ MemoryReader reader(as_bytes(span("\3\2\1")));
EXPECT_EQ(0u, reader.Tell());
}
TEST(MemoryReader, Tell_UpdatesOnSeek) {
- MemoryReader reader(std::as_bytes(std::span("\3\2\1")));
+ MemoryReader reader(as_bytes(span("\3\2\1")));
ASSERT_EQ(OkStatus(), reader.Seek(2, Stream::kCurrent));
EXPECT_EQ(2u, reader.Tell());
}
TEST(MemoryReader, Tell_UpdatesOnRead) {
- MemoryReader reader(std::as_bytes(std::span("\3\2\1")));
+ MemoryReader reader(as_bytes(span("\3\2\1")));
std::byte buffer[4];
ASSERT_EQ(OkStatus(), reader.Read(buffer).status());
EXPECT_EQ(4u, reader.Tell());
diff --git a/pw_stream/null_stream_test.cc b/pw_stream/null_stream_test.cc
new file mode 100644
index 000000000..7b1aee447
--- /dev/null
+++ b/pw_stream/null_stream_test.cc
@@ -0,0 +1,84 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_stream/null_stream.h"
+
+#include <cstddef>
+
+#include "gtest/gtest.h"
+
+namespace pw::stream {
+namespace {
+
+TEST(NullStream, DefaultConservativeWriteLimit) {
+ NullStream stream;
+ EXPECT_EQ(stream.ConservativeWriteLimit(), Stream::kUnlimited);
+}
+
+TEST(NullStream, DefaultConservativeReadLimit) {
+ NullStream stream;
+ EXPECT_EQ(stream.ConservativeReadLimit(), Stream::kUnlimited);
+}
+
+TEST(NullStream, DefaultConservativeReadWriteLimit) {
+ NullStream stream;
+ EXPECT_EQ(stream.ConservativeWriteLimit(), Stream::kUnlimited);
+ EXPECT_EQ(stream.ConservativeReadLimit(), Stream::kUnlimited);
+}
+
+TEST(NullStream, DefaultTell) {
+ NullStream stream;
+ EXPECT_EQ(stream.Tell(), Stream::kUnknownPosition);
+}
+
+TEST(CountingNullStream, DefaultConservativeWriteLimit) {
+ CountingNullStream stream;
+ EXPECT_EQ(stream.ConservativeWriteLimit(), Stream::kUnlimited);
+}
+
+TEST(CountingNullStream, DefaultConservativeReadLimit) {
+ CountingNullStream stream;
+ EXPECT_EQ(stream.ConservativeReadLimit(), Stream::kUnlimited);
+}
+
+TEST(CountingNullStream, DefaultConservativeReadWriteLimit) {
+ CountingNullStream stream;
+ EXPECT_EQ(stream.ConservativeWriteLimit(), Stream::kUnlimited);
+ EXPECT_EQ(stream.ConservativeReadLimit(), Stream::kUnlimited);
+}
+
+TEST(CountingNullStream, DefaultTell) {
+ CountingNullStream stream;
+ EXPECT_EQ(stream.Tell(), Stream::kUnknownPosition);
+}
+
+std::byte data[32];
+
+TEST(CountingNullStream, CountsWrites) {
+ CountingNullStream stream;
+ EXPECT_EQ(OkStatus(), stream.Write(data));
+ EXPECT_EQ(sizeof(data), stream.bytes_written());
+
+ EXPECT_EQ(OkStatus(), stream.Write(span(data).first(1)));
+ EXPECT_EQ(sizeof(data) + 1, stream.bytes_written());
+
+ EXPECT_EQ(OkStatus(), stream.Write(span<std::byte>()));
+ EXPECT_EQ(sizeof(data) + 1, stream.bytes_written());
+
+ EXPECT_EQ(OkStatus(), stream.Write(data));
+ EXPECT_EQ(2 * sizeof(data) + 1, stream.bytes_written());
+}
+
+} // namespace
+} // namespace pw::stream
diff --git a/pw_stream/public/pw_stream/interval_reader.h b/pw_stream/public/pw_stream/interval_reader.h
index 775beac52..868f40727 100644
--- a/pw_stream/public/pw_stream/interval_reader.h
+++ b/pw_stream/public/pw_stream/interval_reader.h
@@ -29,13 +29,8 @@ namespace pw::stream {
// A reader wrapper that reads from a sub-interval of a given seekable
// source reader. The IntervalReader tracks and maintains its own read offset.
// It seeks the source reader to its current read offset before reading. In
-// this way, multiple IntervalReader can share the same source reader without
-// interfereing each other.
-//
-// The reader additionally embedds a `Status` to indicate whether itself
-// is valid. This is a workaround for Reader not being compatibile with
-// Result<>. TODO(pwbug/363): Migrate this to Result<> once we have StatusOr
-// like support.
+// this way, multiple IntervalReaders can share the same source reader without
+// interfering with each other.
class IntervalReader : public SeekableReader {
public:
constexpr IntervalReader() : status_(Status::Unavailable()) {}
@@ -62,7 +57,7 @@ class IntervalReader : public SeekableReader {
return *this;
}
- // Move the read offset to the end of the intetrval;
+ // Move the read offset to the end of the interval;
IntervalReader& Exhaust() {
current_ = end_;
return *this;
@@ -86,7 +81,7 @@ class IntervalReader : public SeekableReader {
private:
StatusWithSize DoRead(ByteSpan destination) final;
Status DoSeek(ptrdiff_t offset, Whence origin) final;
- size_t DoTell() const final { return current_ - start_; }
+ size_t DoTell() final { return current_ - start_; }
size_t ConservativeLimit(LimitType limit) const override {
if (limit == LimitType::kRead) {
return end_ - current_;
diff --git a/pw_stream/public/pw_stream/memory_stream.h b/pw_stream/public/pw_stream/memory_stream.h
index 2b9e60c74..b823ef75a 100644
--- a/pw_stream/public/pw_stream/memory_stream.h
+++ b/pw_stream/public/pw_stream/memory_stream.h
@@ -15,10 +15,10 @@
#include <array>
#include <cstddef>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_result/result.h"
+#include "pw_span/span.h"
#include "pw_stream/seek.h"
#include "pw_stream/stream.h"
@@ -62,8 +62,8 @@ class MemoryWriter : public SeekableWriter {
size_t capacity() const { return dest_.size(); }
- const std::byte* begin() const { return dest_.begin(); }
- const std::byte* end() const { return dest_.begin() + position_; }
+ const std::byte* begin() const { return dest_.data(); }
+ const std::byte* end() const { return dest_.data() + position_; }
private:
size_t ConservativeLimit(LimitType type) const override {
@@ -80,7 +80,7 @@ class MemoryWriter : public SeekableWriter {
return CalculateSeek(offset, origin, dest_.size(), position_);
}
- size_t DoTell() const final { return position_; }
+ size_t DoTell() final { return position_; }
ByteSpan dest_;
size_t position_ = 0;
@@ -113,7 +113,7 @@ class MemoryReader final : public SeekableReader {
return CalculateSeek(offset, origin, source_.size(), position_);
}
- size_t DoTell() const override { return position_; }
+ size_t DoTell() override { return position_; }
// Implementation for reading data from this stream.
//
diff --git a/pw_stream/public/pw_stream/null_stream.h b/pw_stream/public/pw_stream/null_stream.h
index a6272f39c..108c88ebf 100644
--- a/pw_stream/public/pw_stream/null_stream.h
+++ b/pw_stream/public/pw_stream/null_stream.h
@@ -14,10 +14,10 @@
#pragma once
#include <cstddef>
-#include <span>
#include "pw_bytes/span.h"
#include "pw_polyfill/language_feature_macros.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_stream/stream.h"
@@ -41,4 +41,23 @@ class NullStream final : public SeekableReaderWriter {
Status DoSeek(ptrdiff_t, Whence) final { return OkStatus(); }
};
+// Same as NullStream, but tracks the number of bytes written.
+class CountingNullStream final : public SeekableReaderWriter {
+ public:
+ constexpr CountingNullStream() : bytes_written_(0) {}
+
+ size_t bytes_written() const { return bytes_written_; }
+
+ private:
+ Status DoWrite(ConstByteSpan data) final {
+ bytes_written_ += data.size();
+ return OkStatus();
+ }
+
+ StatusWithSize DoRead(ByteSpan) final { return StatusWithSize::OutOfRange(); }
+ Status DoSeek(ptrdiff_t, Whence) final { return OkStatus(); }
+
+ size_t bytes_written_;
+};
+
} // namespace pw::stream
diff --git a/pw_stream/public/pw_stream/socket_stream.h b/pw_stream/public/pw_stream/socket_stream.h
index 058b81053..0092fc808 100644
--- a/pw_stream/public/pw_stream/socket_stream.h
+++ b/pw_stream/public/pw_stream/socket_stream.h
@@ -16,8 +16,9 @@
#include <netinet/in.h>
#include <cstdint>
-#include <span>
+#include "pw_result/result.h"
+#include "pw_span/span.h"
#include "pw_stream/stream.h"
namespace pw::stream {
@@ -26,29 +27,98 @@ class SocketStream : public NonSeekableReaderWriter {
public:
constexpr SocketStream() = default;
- ~SocketStream() { Close(); }
+ // SocketStream objects are moveable but not copyable.
+ SocketStream& operator=(SocketStream&& other) {
+ listen_port_ = other.listen_port_;
+ socket_fd_ = other.socket_fd_;
+ other.socket_fd_ = kInvalidFd;
+ connection_fd_ = other.connection_fd_;
+ other.connection_fd_ = kInvalidFd;
+ sockaddr_client_ = other.sockaddr_client_;
+ return *this;
+ }
+ SocketStream(SocketStream&& other) noexcept
+ : listen_port_(other.listen_port_),
+ socket_fd_(other.socket_fd_),
+ connection_fd_(other.connection_fd_),
+ sockaddr_client_(other.sockaddr_client_) {
+ other.socket_fd_ = kInvalidFd;
+ other.connection_fd_ = kInvalidFd;
+ }
+ SocketStream(const SocketStream&) = delete;
+ SocketStream& operator=(const SocketStream&) = delete;
+
+ ~SocketStream() override { Close(); }
// Listen to the port and return after a client is connected
+ //
+ // DEPRECATED: Use the ServerSocket class instead.
+ // TODO(b/271323032): Remove when this method is no longer used.
Status Serve(uint16_t port);
- // Connect to a local or remote endpoint. Host must be an IPv4 address. If
- // host is nullptr then the locahost address is used instead.
+ // Connect to a local or remote endpoint. Host may be either an IPv4 or IPv6
+ // address. If host is nullptr then the IPv4 localhost address is used
+ // instead.
Status Connect(const char* host, uint16_t port);
// Close the socket stream and release all resources
void Close();
+ // Exposes the file descriptor for the active connection. This is exposed to
+ // allow configuration and introspection of this socket's current
+ // configuration using setsockopt() and getsockopt().
+ //
+ // Returns -1 if there is no active connection.
+ int connection_fd() { return connection_fd_; }
+
private:
+ friend class ServerSocket;
+
static constexpr int kInvalidFd = -1;
- Status DoWrite(std::span<const std::byte> data) override;
+ Status DoWrite(span<const std::byte> data) override;
StatusWithSize DoRead(ByteSpan dest) override;
uint16_t listen_port_ = 0;
int socket_fd_ = kInvalidFd;
- int conn_fd_ = kInvalidFd;
+ int connection_fd_ = kInvalidFd;
struct sockaddr_in sockaddr_client_ = {};
};
+/// `ServerSocket` wraps a POSIX-style server socket, producing a `SocketStream`
+/// for each accepted client connection.
+///
+/// Call `Listen` to create the socket and start listening for connections.
+/// Then call `Accept` any number of times to accept client connections.
+class ServerSocket {
+ public:
+ ServerSocket() = default;
+ ~ServerSocket() { Close(); }
+
+ ServerSocket(const ServerSocket& other) = delete;
+ ServerSocket& operator=(const ServerSocket& other) = delete;
+
+ // Listen for connections on the given port.
+ // If port is 0, a random unused port is chosen and can be retrieved with
+ // port().
+ Status Listen(uint16_t port = 0);
+
+ // Accept a connection. Blocks until after a client is connected.
+ // On success, returns a SocketStream connected to the new client.
+ Result<SocketStream> Accept();
+
+ // Close the server socket, preventing further connections.
+ void Close();
+
+ // Returns the port this socket is listening on.
+ uint16_t port() const { return port_; }
+
+ private:
+ static constexpr int kInvalidFd = -1;
+
+ uint16_t port_ = -1;
+ int socket_fd_ = kInvalidFd;
+};
+
} // namespace pw::stream
diff --git a/pw_stream/public/pw_stream/std_file_stream.h b/pw_stream/public/pw_stream/std_file_stream.h
index c1f9fbc70..fdf7892da 100644
--- a/pw_stream/public/pw_stream/std_file_stream.h
+++ b/pw_stream/public/pw_stream/std_file_stream.h
@@ -29,6 +29,8 @@ class StdFileReader final : public stream::SeekableReader {
private:
StatusWithSize DoRead(ByteSpan dest) override;
Status DoSeek(ptrdiff_t offset, Whence origin) override;
+ size_t DoTell() override;
+ size_t ConservativeLimit(LimitType limit) const override;
std::ifstream stream_;
};
@@ -44,6 +46,7 @@ class StdFileWriter final : public stream::SeekableWriter {
private:
Status DoWrite(ConstByteSpan data) override;
Status DoSeek(ptrdiff_t offset, Whence origin) override;
+ size_t DoTell() override;
std::ofstream stream_;
};
diff --git a/pw_stream/public/pw_stream/stream.h b/pw_stream/public/pw_stream/stream.h
index 31fdb333e..a7eed528e 100644
--- a/pw_stream/public/pw_stream/stream.h
+++ b/pw_stream/public/pw_stream/stream.h
@@ -15,11 +15,12 @@
#include <array>
#include <cstddef>
-#include <span>
+#include <limits>
#include "pw_assert/assert.h"
#include "pw_bytes/span.h"
#include "pw_result/result.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -107,7 +108,7 @@ class Stream {
return result.status();
}
Result<ByteSpan> Read(void* dest, size_t size_bytes) {
- return Read(std::span(static_cast<std::byte*>(dest), size_bytes));
+ return Read(span(static_cast<std::byte*>(dest), size_bytes));
}
// Writes data to this stream. Data is not guaranteed to be fully written out
@@ -144,7 +145,7 @@ class Stream {
return DoWrite(data);
}
Status Write(const void* data, size_t size_bytes) {
- return Write(std::span(static_cast<const std::byte*>(data), size_bytes));
+ return Write(span(static_cast<const std::byte*>(data), size_bytes));
}
Status Write(const std::byte b) { return Write(&b, 1); }
@@ -172,7 +173,7 @@ class Stream {
//
// Streams that support seeking from the beginning always support Tell().
// Other streams may or may not support Tell().
- size_t Tell() const { return DoTell(); }
+ size_t Tell() { return DoTell(); }
// Liklely (not guaranteed) minimum bytes available to read at this time.
// This number is advisory and not guaranteed to read full number of requested
@@ -250,7 +251,7 @@ class Stream {
virtual Status DoSeek(ptrdiff_t offset, Whence origin) = 0;
- virtual size_t DoTell() const { return kUnknownPosition; }
+ virtual size_t DoTell() { return kUnknownPosition; }
virtual size_t ConservativeLimit(LimitType limit_type) const {
if (limit_type == LimitType::kRead) {
diff --git a/pw_stream/public/pw_stream/sys_io_stream.h b/pw_stream/public/pw_stream/sys_io_stream.h
index 443cbc034..b19d8fe13 100644
--- a/pw_stream/public/pw_stream/sys_io_stream.h
+++ b/pw_stream/public/pw_stream/sys_io_stream.h
@@ -16,8 +16,8 @@
#include <array>
#include <cstddef>
#include <limits>
-#include <span>
+#include "pw_span/span.h"
#include "pw_stream/stream.h"
#include "pw_sys_io/sys_io.h"
@@ -25,7 +25,7 @@ namespace pw::stream {
class SysIoWriter : public NonSeekableWriter {
private:
- Status DoWrite(std::span<const std::byte> data) override {
+ Status DoWrite(span<const std::byte> data) override {
return pw::sys_io::WriteBytes(data).status();
}
};
diff --git a/pw_stream/socket_stream.cc b/pw_stream/socket_stream.cc
index 016af92cf..3978e49ab 100644
--- a/pw_stream/socket_stream.cc
+++ b/pw_stream/socket_stream.cc
@@ -15,20 +15,41 @@
#include "pw_stream/socket_stream.h"
#include <arpa/inet.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+#include <sys/types.h>
#include <unistd.h>
+#include <cerrno>
#include <cstring>
+#include "pw_assert/check.h"
#include "pw_log/log.h"
+#include "pw_string/to_string.h"
namespace pw::stream {
namespace {
-constexpr uint32_t kMaxConcurrentUser = 1;
+constexpr uint32_t kServerBacklogLength = 1;
constexpr const char* kLocalhostAddress = "127.0.0.1";
+// Set necessary options on a socket file descriptor.
+void ConfigureSocket([[maybe_unused]] int socket) {
+#if defined(__APPLE__)
+ // Use SO_NOSIGPIPE to avoid getting a SIGPIPE signal when the remote peer
+ // drops the connection. This is supported on macOS only.
+ constexpr int value = 1;
+ if (setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &value, sizeof(int)) < 0) {
+ PW_LOG_WARN("Failed to set SO_NOSIGPIPE: %s", std::strerror(errno));
+ }
+#endif // defined(__APPLE__)
+}
+
} // namespace
+// TODO(b/240982565): Implement SocketStream for Windows.
+
// Listen to the port and return after a client is connected
Status SocketStream::Serve(uint16_t port) {
listen_port_ = port;
@@ -63,38 +84,48 @@ Status SocketStream::Serve(uint16_t port) {
return Status::Unknown();
}
- if (listen(socket_fd_, kMaxConcurrentUser) < 0) {
+ if (listen(socket_fd_, kServerBacklogLength) < 0) {
PW_LOG_ERROR("Failed to listen to socket: %s", std::strerror(errno));
return Status::Unknown();
}
socklen_t len = sizeof(sockaddr_client_);
- conn_fd_ =
+ connection_fd_ =
accept(socket_fd_, reinterpret_cast<sockaddr*>(&sockaddr_client_), &len);
- if (conn_fd_ < 0) {
+ if (connection_fd_ < 0) {
return Status::Unknown();
}
+ ConfigureSocket(connection_fd_);
return OkStatus();
}
Status SocketStream::SocketStream::Connect(const char* host, uint16_t port) {
- conn_fd_ = socket(AF_INET, SOCK_STREAM, 0);
-
- sockaddr_in addr;
- addr.sin_family = AF_INET;
- addr.sin_port = htons(port);
-
if (host == nullptr || std::strcmp(host, "localhost") == 0) {
host = kLocalhostAddress;
}
- if (inet_pton(AF_INET, host, &addr.sin_addr) <= 0) {
+ struct addrinfo hints = {};
+ struct addrinfo* res;
+ char port_buffer[6];
+ PW_CHECK(ToString(port, port_buffer).ok());
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV | AI_PASSIVE;
+ if (getaddrinfo(host, port_buffer, &hints, &res) != 0) {
PW_LOG_ERROR("Failed to configure connection address for socket");
return Status::InvalidArgument();
}
- if (connect(conn_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
+ connection_fd_ = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+ ConfigureSocket(connection_fd_);
+ if (connect(connection_fd_, res->ai_addr, res->ai_addrlen) < 0) {
+ close(connection_fd_);
+ connection_fd_ = kInvalidFd;
+ }
+ freeaddrinfo(res);
+
+ if (connection_fd_ == kInvalidFd) {
PW_LOG_ERROR(
"Failed to connect to %s:%d: %s", host, port, std::strerror(errno));
return Status::Unknown();
@@ -109,27 +140,121 @@ void SocketStream::Close() {
socket_fd_ = kInvalidFd;
}
- if (conn_fd_ != kInvalidFd) {
- close(conn_fd_);
- conn_fd_ = kInvalidFd;
+ if (connection_fd_ != kInvalidFd) {
+ close(connection_fd_);
+ connection_fd_ = kInvalidFd;
}
}
-Status SocketStream::DoWrite(std::span<const std::byte> data) {
- ssize_t bytes_sent = send(conn_fd_, data.data(), data.size_bytes(), 0);
+Status SocketStream::DoWrite(span<const std::byte> data) {
+ int send_flags = 0;
+#if defined(__linux__)
+ // Use MSG_NOSIGNAL to avoid getting a SIGPIPE signal when the remote
+ // peer drops the connection. This is supported on Linux only.
+ send_flags |= MSG_NOSIGNAL;
+#endif // defined(__linux__)
+
+ ssize_t bytes_sent =
+ send(connection_fd_, data.data(), data.size_bytes(), send_flags);
if (bytes_sent < 0 || static_cast<size_t>(bytes_sent) != data.size()) {
+ if (errno == EPIPE) {
+ // An EPIPE indicates that the connection is closed. Return an OutOfRange
+ // error.
+ return Status::OutOfRange();
+ }
+
return Status::Unknown();
}
return OkStatus();
}
StatusWithSize SocketStream::DoRead(ByteSpan dest) {
- ssize_t bytes_rcvd = recv(conn_fd_, dest.data(), dest.size_bytes(), 0);
- if (bytes_rcvd < 0) {
+ ssize_t bytes_rcvd = recv(connection_fd_, dest.data(), dest.size_bytes(), 0);
+ if (bytes_rcvd == 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK) {
+ // Socket timed out when trying to read.
+ // This should only occur if SO_RCVTIMEO was configured to be nonzero, or
+ // if the socket was opened with the O_NONBLOCK flag to prevent any
+ // blocking when performing reads or writes.
+ return StatusWithSize::ResourceExhausted();
+ }
+ // Remote peer has closed the connection.
+ Close();
+ return StatusWithSize::OutOfRange();
+ } else if (bytes_rcvd < 0) {
return StatusWithSize::Unknown();
}
return StatusWithSize(bytes_rcvd);
}
-}; // namespace pw::stream
+// Listen for connections on the given port.
+// If port is 0, a random unused port is chosen and can be retrieved with
+// port().
+Status ServerSocket::Listen(uint16_t port) {
+ socket_fd_ = socket(AF_INET6, SOCK_STREAM, 0);
+ if (socket_fd_ == kInvalidFd) {
+ return Status::Unknown();
+ }
+
+ // Allow binding to an address that may still be in use by a closed socket.
+ constexpr int value = 1;
+ setsockopt(socket_fd_, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(int));
+
+ if (port != 0) {
+ struct sockaddr_in6 addr = {};
+ socklen_t addr_len = sizeof(addr);
+ addr.sin6_family = AF_INET6;
+ addr.sin6_port = htons(port);
+ addr.sin6_addr = in6addr_any;
+ if (bind(socket_fd_, reinterpret_cast<sockaddr*>(&addr), addr_len) < 0) {
+ return Status::Unknown();
+ }
+ }
+
+ if (listen(socket_fd_, kServerBacklogLength) < 0) {
+ return Status::Unknown();
+ }
+
+ // Find out which port the socket is listening on, and fill in port_.
+ struct sockaddr_in6 addr = {};
+ socklen_t addr_len = sizeof(addr);
+ if (getsockname(socket_fd_, reinterpret_cast<sockaddr*>(&addr), &addr_len) <
+ 0 ||
+ addr_len > sizeof(addr)) {
+ close(socket_fd_);
+ return Status::Unknown();
+ }
+
+ port_ = ntohs(addr.sin6_port);
+
+ return OkStatus();
+}
+
+// Accept a connection. Blocks until after a client is connected.
+// On success, returns a SocketStream connected to the new client.
+Result<SocketStream> ServerSocket::Accept() {
+ struct sockaddr_in6 sockaddr_client_ = {};
+ socklen_t len = sizeof(sockaddr_client_);
+
+ int connection_fd =
+ accept(socket_fd_, reinterpret_cast<sockaddr*>(&sockaddr_client_), &len);
+ if (connection_fd == kInvalidFd) {
+ return Status::Unknown();
+ }
+ ConfigureSocket(connection_fd);
+
+ SocketStream client_stream;
+ client_stream.connection_fd_ = connection_fd;
+ return client_stream;
+}
+
+// Close the server socket, preventing further connections.
+void ServerSocket::Close() {
+ if (socket_fd_ != kInvalidFd) {
+ close(socket_fd_);
+ socket_fd_ = kInvalidFd;
+ }
+}
+
+} // namespace pw::stream
diff --git a/pw_stream/socket_stream_test.cc b/pw_stream/socket_stream_test.cc
new file mode 100644
index 000000000..bd8e58eae
--- /dev/null
+++ b/pw_stream/socket_stream_test.cc
@@ -0,0 +1,194 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_stream/socket_stream.h"
+
+#include <thread>
+
+#include "gtest/gtest.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+
+namespace pw::stream {
+namespace {
+
+// Helper function to create a ServerSocket and connect to it via loopback.
+void RunConnectTest(const char* hostname) {
+ ServerSocket server;
+ EXPECT_EQ(server.Listen(), OkStatus());
+
+ Result<SocketStream> server_stream = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() { server_stream = server.Accept(); }};
+
+ SocketStream client;
+ EXPECT_EQ(client.Connect(hostname, server.port()), OkStatus());
+
+ accept_thread.join();
+ EXPECT_EQ(server_stream.status(), OkStatus());
+
+ server_stream.value().Close();
+ server.Close();
+ client.Close();
+}
+
+TEST(SocketStreamTest, ConnectIpv4) { RunConnectTest("127.0.0.1"); }
+
+TEST(SocketStreamTest, ConnectIpv6) { RunConnectTest("::1"); }
+
+TEST(SocketStreamTest, ConnectSpecificPort) {
+ // We want to test the "listen on a specific port" functionality,
+ // but hard-coding a port number in a test is inherently problematic, as
+ // port numbers are a global resource.
+ //
+ // We use the automatic port assignment initially to get a port assignment,
+ // close that server, and then use that port explicitly in a new server.
+ //
+ // There's still the possibility that the port will get swiped, but it
+ // shouldn't happen by chance.
+ ServerSocket initial_server;
+ EXPECT_EQ(initial_server.Listen(), OkStatus());
+ uint16_t port = initial_server.port();
+ initial_server.Close();
+
+ ServerSocket server;
+ EXPECT_EQ(server.Listen(port), OkStatus());
+ EXPECT_EQ(server.port(), port);
+
+ Result<SocketStream> server_stream = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() { server_stream = server.Accept(); }};
+
+ SocketStream client;
+ EXPECT_EQ(client.Connect("localhost", server.port()), OkStatus());
+
+ accept_thread.join();
+ EXPECT_EQ(server_stream.status(), OkStatus());
+
+ server_stream.value().Close();
+ server.Close();
+ client.Close();
+}
+
+// Helper function to test exchanging data on a pair of sockets.
+void ExchangeData(SocketStream& stream1, SocketStream& stream2) {
+ auto kPayload1 = as_bytes(span("some data"));
+ auto kPayload2 = as_bytes(span("other bytes"));
+ std::array<char, 100> read_buffer{};
+
+ // Write data from stream1 and read it from stream2.
+ auto write_status = Status::Unavailable();
+ auto write_thread =
+ std::thread{[&]() { write_status = stream1.Write(kPayload1); }};
+ Result<ByteSpan> read_result =
+ stream2.Read(as_writable_bytes(span(read_buffer)));
+ EXPECT_EQ(read_result.status(), OkStatus());
+ EXPECT_EQ(read_result.value().size(), kPayload1.size());
+ EXPECT_TRUE(
+ std::equal(kPayload1.begin(), kPayload1.end(), read_result->begin()));
+
+ write_thread.join();
+ EXPECT_EQ(write_status, OkStatus());
+
+ // Read data in the client and write it from the server.
+ auto read_thread = std::thread{[&]() {
+ read_result = stream1.Read(as_writable_bytes(span(read_buffer)));
+ }};
+ EXPECT_EQ(stream2.Write(kPayload2), OkStatus());
+
+ read_thread.join();
+ EXPECT_EQ(read_result.status(), OkStatus());
+ EXPECT_EQ(read_result.value().size(), kPayload2.size());
+ EXPECT_TRUE(
+ std::equal(kPayload2.begin(), kPayload2.end(), read_result->begin()));
+
+ // Close stream1 and attempt to read from stream2.
+ stream1.Close();
+ read_result = stream2.Read(as_writable_bytes(span(read_buffer)));
+ EXPECT_EQ(read_result.status(), Status::OutOfRange());
+
+ stream2.Close();
+}
+
+TEST(SocketStreamTest, ReadWrite) {
+ ServerSocket server;
+ EXPECT_EQ(server.Listen(), OkStatus());
+
+ Result<SocketStream> server_stream = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() { server_stream = server.Accept(); }};
+
+ SocketStream client;
+ EXPECT_EQ(client.Connect("localhost", server.port()), OkStatus());
+
+ accept_thread.join();
+ EXPECT_EQ(server_stream.status(), OkStatus());
+
+ ExchangeData(client, server_stream.value());
+ server.Close();
+}
+
+TEST(SocketStreamTest, MultipleClients) {
+ ServerSocket server;
+ EXPECT_EQ(server.Listen(), OkStatus());
+
+ Result<SocketStream> server_stream1 = Status::Unavailable();
+ Result<SocketStream> server_stream2 = Status::Unavailable();
+ Result<SocketStream> server_stream3 = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() {
+ server_stream1 = server.Accept();
+ server_stream2 = server.Accept();
+ server_stream3 = server.Accept();
+ }};
+
+ SocketStream client1;
+ SocketStream client2;
+ SocketStream client3;
+ EXPECT_EQ(client1.Connect("localhost", server.port()), OkStatus());
+ EXPECT_EQ(client2.Connect("localhost", server.port()), OkStatus());
+ EXPECT_EQ(client3.Connect("localhost", server.port()), OkStatus());
+
+ accept_thread.join();
+ EXPECT_EQ(server_stream1.status(), OkStatus());
+ EXPECT_EQ(server_stream2.status(), OkStatus());
+ EXPECT_EQ(server_stream3.status(), OkStatus());
+
+ ExchangeData(client1, server_stream1.value());
+ ExchangeData(client2, server_stream2.value());
+ ExchangeData(client3, server_stream3.value());
+ server.Close();
+}
+
+TEST(SocketStreamTest, ReuseAutomaticServerPort) {
+ uint16_t server_port = 0;
+ SocketStream client_stream;
+ ServerSocket server;
+
+ EXPECT_EQ(server.Listen(0), OkStatus());
+ server_port = server.port();
+ EXPECT_NE(server_port, 0);
+
+ Result<SocketStream> server_stream = Status::Unavailable();
+ auto accept_thread = std::thread{[&]() { server_stream = server.Accept(); }};
+
+ EXPECT_EQ(client_stream.Connect(nullptr, server_port), OkStatus());
+ accept_thread.join();
+ ASSERT_EQ(server_stream.status(), OkStatus());
+
+ server_stream->Close();
+ server.Close();
+
+ ServerSocket server2;
+ EXPECT_EQ(server2.Listen(server_port), OkStatus());
+}
+
+} // namespace
+} // namespace pw::stream
diff --git a/pw_stream/std_file_stream.cc b/pw_stream/std_file_stream.cc
index f934413c2..95427218a 100644
--- a/pw_stream/std_file_stream.cc
+++ b/pw_stream/std_file_stream.cc
@@ -14,6 +14,8 @@
#include "pw_stream/std_file_stream.h"
+#include "pw_assert/check.h"
+
namespace pw::stream {
namespace {
@@ -26,6 +28,7 @@ std::ios::seekdir WhenceToSeekDir(Stream::Whence whence) {
case Stream::Whence::kEnd:
return std::ios::end;
}
+ PW_CRASH("Unknown value for enum Stream::Whence");
}
} // namespace
@@ -45,12 +48,45 @@ StatusWithSize StdFileReader::DoRead(ByteSpan dest) {
}
Status StdFileReader::DoSeek(ptrdiff_t offset, Whence origin) {
+ // Explicitly clear EOF bit if needed.
+ if (stream_.eof()) {
+ stream_.clear();
+ }
if (!stream_.seekg(offset, WhenceToSeekDir(origin))) {
return Status::Unknown();
}
return OkStatus();
}
+size_t StdFileReader::DoTell() {
+ auto pos = static_cast<int>(stream_.tellg());
+ return pos < 0 ? kUnknownPosition : pos;
+}
+
+size_t StdFileReader::ConservativeLimit(LimitType limit) const {
+ if (limit == LimitType::kWrite) {
+ return 0;
+ }
+
+ // Attempt to determine the number of bytes left in the file by seeking
+ // to the end and checking where we end up.
+ if (stream_.eof()) {
+ return 0;
+ }
+ auto stream = const_cast<std::ifstream*>(&this->stream_);
+ auto start = stream->tellg();
+ if (start == -1) {
+ return 0;
+ }
+ stream->seekg(0, std::ios::end);
+ auto end = stream->tellg();
+ if (end == -1) {
+ return 0;
+ }
+ stream->seekg(start, std::ios::beg);
+ return end - start;
+}
+
Status StdFileWriter::DoWrite(ConstByteSpan data) {
if (stream_.eof()) {
return Status::OutOfRange();
@@ -70,4 +106,9 @@ Status StdFileWriter::DoSeek(ptrdiff_t offset, Whence origin) {
return OkStatus();
}
+size_t StdFileWriter::DoTell() {
+ auto pos = static_cast<int>(stream_.tellp());
+ return pos < 0 ? kUnknownPosition : pos;
+}
+
} // namespace pw::stream
diff --git a/pw_stream/std_file_stream_test.cc b/pw_stream/std_file_stream_test.cc
new file mode 100644
index 000000000..476d7c395
--- /dev/null
+++ b/pw_stream/std_file_stream_test.cc
@@ -0,0 +1,182 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_stream/std_file_stream.h"
+
+#include <algorithm>
+#include <array>
+#include <cinttypes>
+#include <cstdio>
+#include <filesystem>
+#include <random>
+#include <string>
+#include <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_assert/assert.h"
+#include "pw_bytes/span.h"
+#include "pw_containers/algorithm.h"
+#include "pw_random/xor_shift.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_string/string_builder.h"
+
+namespace pw::stream {
+namespace {
+
+constexpr std::string_view kSmallTestData(
+ "This is a test string used to verify correctness!");
+
+// Creates a directory with a specified prefix followed by a random 32-bit hex
+// number. Random temporary file handle names can then be requested. When the
+// TempDir is destroyed, the entire directory is deleted.
+//
+// Example created temporary files:
+// /tmp/StdFileStreamTest32B37409/997BDDA2
+// /tmp/StdFileStreamTest32B37409/C181909B
+//
+// WARNING: This class should ONLY be used for these tests!
+//
+// These tests need to open and close files by file name, which is incompatible
+// with std::tmpfile() (which deletes files on close). Even though std::tmpnam()
+// looks like the right tool to use, it's not thread safe and doesn't provide
+// any guarantees that the provided file name is not in use. std::tmpnam() is
+// also marked with a deprecation warning on some systems, warning against using
+// it at all.
+//
+// While on some systems this approach may provide significantly better
+// uniqueness since std::random_device may be backed with thread-safe random
+// sources, the STL does not explicitly require std::random_device to produce
+// non-deterministic random data (instead only recommending it). If
+// std::random_device is pseudo-random, this temporary directory will always
+// end up with the same naming pattern.
+//
+// If the STL required std::random_device to be thread-safe and
+// cryptographically-secure, this class could be made reasonably production
+// ready by increasing use of entropy and making temporary file name selection
+// thread-safe (in case a TempDir is static and shared across multiple threads).
+//
+// Today, this class does not provide much better safety guarantees than
+// std::tmpnam(), but thanks to the required directory prefix and typical
+// implementations of std::random_device, should see less risk of collisions in
+// practice.
+class TempDir {
+ public:
+ TempDir(std::string_view prefix) : rng_(GetSeed()) {
+ temp_dir_ = std::filesystem::temp_directory_path();
+ temp_dir_ /= std::string(prefix) + GetRandomSuffix();
+ PW_ASSERT(std::filesystem::create_directory(temp_dir_));
+ }
+
+ ~TempDir() { PW_ASSERT(std::filesystem::remove_all(temp_dir_)); }
+
+ std::filesystem::path GetTempFileName() {
+ return temp_dir_ / GetRandomSuffix();
+ }
+
+ private:
+ std::string GetRandomSuffix() {
+ pw::StringBuffer<9> random_suffix_str;
+ uint32_t random_suffix_int = 0;
+ rng_.GetInt(random_suffix_int);
+ PW_ASSERT(random_suffix_str.Format("%08" PRIx32, random_suffix_int).ok());
+ return std::string(random_suffix_str.view());
+ }
+
+ // Generate a 64-bit random from system entropy pool. This is used to seed a
+ // pseudo-random number generator for individual file names.
+ static uint64_t GetSeed() {
+ std::random_device sys_rand;
+ uint64_t seed = 0;
+ for (size_t seed_bytes = 0; seed_bytes < sizeof(seed);
+ seed_bytes += sizeof(std::random_device::result_type)) {
+ std::random_device::result_type val = sys_rand();
+ seed = seed << 8 * sizeof(std::random_device::result_type);
+ seed |= val;
+ }
+ return seed;
+ }
+
+ random::XorShiftStarRng64 rng_;
+ std::filesystem::path temp_dir_;
+};
+
+class StdFileStreamTest : public ::testing::Test {
+ protected:
+ StdFileStreamTest() = default;
+
+ void SetUp() override {
+ temp_file_path_ = temp_dir_.GetTempFileName().generic_string();
+ }
+ void TearDown() override {
+ PW_ASSERT(std::filesystem::remove(TempFilename()));
+ }
+
+ const char* TempFilename() { return temp_file_path_.c_str(); }
+
+ private:
+ // Only construct one temporary directory to reduce waste of system entropy.
+ static TempDir temp_dir_;
+
+ std::string temp_file_path_;
+};
+
+TempDir StdFileStreamTest::temp_dir_{"StdFileStreamTest"};
+
+TEST_F(StdFileStreamTest, SeekAtEnd) {
+ // Write some data to the temporary file.
+ const std::string_view kTestData = kSmallTestData;
+ StdFileWriter writer(TempFilename());
+ ASSERT_EQ(writer.Write(as_bytes(span(kTestData))), OkStatus());
+ writer.Close();
+
+ StdFileReader reader(TempFilename());
+ ASSERT_EQ(reader.ConservativeReadLimit(), kTestData.size());
+
+ std::array<char, 3> read_buffer;
+ size_t read_offset = 0;
+ while (read_offset < kTestData.size()) {
+ Result<ConstByteSpan> result =
+ reader.Read(as_writable_bytes(span(read_buffer)));
+ ASSERT_EQ(result.status(), OkStatus());
+ ASSERT_GT(result.value().size(), 0u);
+ ASSERT_LE(result.value().size(), read_buffer.size());
+ ASSERT_LE(result.value().size(), kTestData.size() - read_offset);
+ ConstByteSpan expect_window =
+ as_bytes(span(kTestData)).subspan(read_offset, result.value().size());
+ EXPECT_TRUE(pw::containers::Equal(result.value(), expect_window));
+ read_offset += result.value().size();
+ ASSERT_EQ(reader.ConservativeReadLimit(), kTestData.size() - read_offset);
+ }
+ // After data has been read, do a final read to trigger EOF.
+ Result<ConstByteSpan> result =
+ reader.Read(as_writable_bytes(span(read_buffer)));
+ EXPECT_EQ(result.status(), Status::OutOfRange());
+ ASSERT_EQ(reader.ConservativeReadLimit(), 0u);
+
+ EXPECT_EQ(read_offset, kTestData.size());
+
+ // Seek backwards and read again to ensure seek at EOF works.
+ ASSERT_EQ(reader.Seek(-1 * read_buffer.size(), Stream::Whence::kEnd),
+ OkStatus());
+ ASSERT_EQ(reader.ConservativeReadLimit(), read_buffer.size());
+ result = reader.Read(as_writable_bytes(span(read_buffer)));
+ EXPECT_EQ(result.status(), OkStatus());
+
+ reader.Close();
+}
+
+} // namespace
+} // namespace pw::stream
diff --git a/pw_stream/stream_test.cc b/pw_stream/stream_test.cc
index a33270eb2..d08f9064d 100644
--- a/pw_stream/stream_test.cc
+++ b/pw_stream/stream_test.cc
@@ -17,7 +17,6 @@
#include <limits>
#include "gtest/gtest.h"
-#include "pw_stream/null_stream.h"
namespace pw::stream {
namespace {
@@ -194,26 +193,5 @@ TEST(Stream, SeekableReaderWriter) {
TestStreamImpl<TestSeekableReaderWriter, kReadable, kWritable, kSeekable>();
}
-TEST(NullStream, DefaultConservativeWriteLimit) {
- NullStream stream;
- EXPECT_EQ(stream.ConservativeWriteLimit(), Stream::kUnlimited);
-}
-
-TEST(NullStream, DefaultConservativeReadLimit) {
- NullStream stream;
- EXPECT_EQ(stream.ConservativeReadLimit(), Stream::kUnlimited);
-}
-
-TEST(NullStream, DefaultConservativeReadWriteLimit) {
- NullStream stream;
- EXPECT_EQ(stream.ConservativeWriteLimit(), Stream::kUnlimited);
- EXPECT_EQ(stream.ConservativeReadLimit(), Stream::kUnlimited);
-}
-
-TEST(NullStream, DefaultTell) {
- NullStream stream;
- EXPECT_EQ(stream.Tell(), Stream::kUnknownPosition);
-}
-
} // namespace
} // namespace pw::stream
diff --git a/pw_string/Android.bp b/pw_string/Android.bp
new file mode 100644
index 000000000..2a6a79dbc
--- /dev/null
+++ b/pw_string/Android.bp
@@ -0,0 +1,42 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_string",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_assert_headers",
+ "pw_assert_log_headers",
+ "pw_log_headers",
+ "pw_log_null_headers",
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_result_headers",
+ "pw_span_headers",
+ ],
+ host_supported: true,
+ srcs: [
+ "format.cc",
+ "string_builder.cc",
+ "type_to_string.cc",
+ ],
+ static_libs: [
+ "pw_status",
+ ],
+}
diff --git a/pw_string/BUILD.bazel b/pw_string/BUILD.bazel
index 663bb4f26..420026054 100644
--- a/pw_string/BUILD.bazel
+++ b/pw_string/BUILD.bazel
@@ -23,24 +23,86 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
+ name = "config",
+ hdrs = ["public/pw_string/internal/config.h"],
+ includes = ["public"],
+)
+
+pw_cc_library(
name = "pw_string",
+ deps = [
+ ":builder",
+ ":format",
+ ":to_string",
+ ":util",
+ ],
+)
+
+pw_cc_library(
+ name = "builder",
+ srcs = ["string_builder.cc"],
+ hdrs = ["public/pw_string/string_builder.h"],
+ includes = ["public"],
+ deps = [
+ ":format",
+ ":string",
+ ":to_string",
+ ":util",
+ "//pw_preprocessor",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "format",
+ srcs = ["format.cc"],
+ hdrs = ["public/pw_string/format.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_preprocessor",
+ "//pw_span",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "string",
srcs = [
- "format.cc",
- "string_builder.cc",
- "type_to_string.cc",
+ "public/pw_string/internal/string_common_functions.inc",
+ "public/pw_string/internal/string_impl.h",
+ ],
+ hdrs = ["public/pw_string/string.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_assert:facade",
+ "//pw_polyfill",
],
+)
+
+pw_cc_library(
+ name = "to_string",
+ srcs = ["type_to_string.cc"],
hdrs = [
- "public/pw_string/format.h",
- "public/pw_string/internal/length.h",
- "public/pw_string/string_builder.h",
"public/pw_string/to_string.h",
"public/pw_string/type_to_string.h",
- "public/pw_string/util.h",
],
includes = ["public"],
deps = [
+ ":config",
+ ":format",
+ ":util",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "util",
+ srcs = ["public/pw_string/internal/length.h"],
+ hdrs = ["public/pw_string/util.h"],
+ includes = ["public"],
+ deps = [
+ ":string",
"//pw_assert:facade",
- "//pw_preprocessor",
"//pw_result",
"//pw_span",
"//pw_status",
@@ -51,17 +113,27 @@ pw_cc_test(
name = "format_test",
srcs = ["format_test.cc"],
deps = [
- ":pw_string",
+ ":format",
"//pw_span",
"//pw_unit_test",
],
)
pw_cc_test(
+ name = "string_test",
+ srcs = ["string_test.cc"],
+ deps = [
+ ":string",
+ "//pw_compilation_testing:negative_compilation_testing",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "type_to_string_test",
srcs = ["type_to_string_test.cc"],
deps = [
- ":pw_string",
+ ":to_string",
"//pw_unit_test",
],
)
@@ -70,7 +142,7 @@ pw_cc_test(
name = "string_builder_test",
srcs = ["string_builder_test.cc"],
deps = [
- ":pw_string",
+ ":builder",
"//pw_unit_test",
],
)
@@ -79,7 +151,8 @@ pw_cc_test(
name = "to_string_test",
srcs = ["to_string_test.cc"],
deps = [
- ":pw_string",
+ ":config",
+ ":to_string",
"//pw_status",
"//pw_unit_test",
],
@@ -89,7 +162,7 @@ pw_cc_test(
name = "util_test",
srcs = ["util_test.cc"],
deps = [
- ":pw_string",
+ ":util",
"//pw_unit_test",
],
)
diff --git a/pw_string/BUILD.gn b/pw_string/BUILD.gn
index 0742938c9..bc73a2504 100644
--- a/pw_string/BUILD.gn
+++ b/pw_string/BUILD.gn
@@ -15,39 +15,115 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_bloat/bloat.gni")
+import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_unit_test/test.gni")
+declare_args() {
+ # The build target that overrides the default configuration options for this
+ # module. This should point to a source set that provides defines through a
+ # public config (which may -include a file or add defines directly).
+ pw_string_CONFIG = pw_build_DEFAULT_MODULE_CONFIG
+}
+
config("public_include_path") {
include_dirs = [ "public" ]
}
-pw_source_set("pw_string") {
+config("enable_decimal_float_expansion_config") {
+ defines = [ "PW_STRING_ENABLE_DECIMAL_FLOAT_EXPANSION=1" ]
+}
+
+pw_source_set("enable_decimal_float_expansion") {
+ public_configs = [ ":enable_decimal_float_expansion_config" ]
+}
+
+pw_source_set("config") {
+ public = [ "public/pw_string/internal/config.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ pw_string_CONFIG ]
+}
+
+group("pw_string") {
+ public_deps = [
+ ":builder",
+ ":format",
+ ":to_string",
+ ]
+}
+
+pw_source_set("builder") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_string/string_builder.h" ]
+ sources = [ "string_builder.cc" ]
+ public_deps = [
+ ":format",
+ ":string",
+ ":to_string",
+ ":util",
+ dir_pw_preprocessor,
+ dir_pw_span,
+ dir_pw_status,
+ ]
+}
+
+pw_source_set("format") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_string/format.h" ]
+ sources = [ "format.cc" ]
+ public_deps = [
+ dir_pw_preprocessor,
+ dir_pw_span,
+ dir_pw_status,
+ ]
+}
+
+pw_source_set("string") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_string/string.h" ]
+ sources = [
+ "public/pw_string/internal/string_common_functions.inc",
+ "public/pw_string/internal/string_impl.h",
+ ]
+ public_deps = [
+ dir_pw_assert,
+ dir_pw_polyfill,
+ ]
+}
+
+pw_source_set("to_string") {
public_configs = [ ":public_include_path" ]
public = [
- "public/pw_string/format.h",
- "public/pw_string/internal/length.h",
- "public/pw_string/string_builder.h",
"public/pw_string/to_string.h",
"public/pw_string/type_to_string.h",
- "public/pw_string/util.h",
]
- sources = [
- "format.cc",
- "string_builder.cc",
- "type_to_string.cc",
+ sources = [ "type_to_string.cc" ]
+ public_deps = [
+ ":config",
+ ":format",
+ ":util",
+ dir_pw_span,
+ dir_pw_status,
]
+}
+
+pw_source_set("util") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_string/util.h" ]
+ sources = [ "public/pw_string/internal/length.h" ]
public_deps = [
- "$dir_pw_assert",
- "$dir_pw_preprocessor",
- "$dir_pw_result",
- "$dir_pw_status",
+ ":string",
+ dir_pw_assert,
+ dir_pw_result,
+ dir_pw_span,
+ dir_pw_status,
]
}
pw_test_group("tests") {
tests = [
+ ":string_test",
":format_test",
":string_builder_test",
":to_string_test",
@@ -61,39 +137,53 @@ pw_test_group("tests") {
}
pw_test("format_test") {
- deps = [ ":pw_string" ]
+ deps = [ ":format" ]
sources = [ "format_test.cc" ]
}
+pw_test("string_test") {
+ deps = [ ":string" ]
+ sources = [ "string_test.cc" ]
+ negative_compilation_tests = true
+}
+
pw_test("string_builder_test") {
- deps = [ ":pw_string" ]
+ deps = [ ":builder" ]
sources = [ "string_builder_test.cc" ]
}
pw_test("to_string_test") {
- deps = [ ":pw_string" ]
+ deps = [
+ ":config",
+ ":pw_string",
+ ]
sources = [ "to_string_test.cc" ]
}
pw_test("type_to_string_test") {
- deps = [ ":pw_string" ]
+ deps = [ ":to_string" ]
sources = [ "type_to_string_test.cc" ]
}
pw_test("util_test") {
- deps = [ ":pw_string" ]
+ deps = [ ":util" ]
sources = [ "util_test.cc" ]
}
pw_doc_group("docs") {
- sources = [ "docs.rst" ]
+ sources = [
+ "api.rst",
+ "design.rst",
+ "docs.rst",
+ "guide.rst",
+ ]
report_deps = [
":format_size_report",
":string_builder_size_report",
]
}
-pw_size_report("format_size_report") {
+pw_size_diff("format_size_report") {
title = "Using pw::string::Format instead of snprintf"
binaries = [
@@ -115,7 +205,7 @@ pw_size_report("format_size_report") {
]
}
-pw_size_report("string_builder_size_report") {
+pw_size_diff("string_builder_size_report") {
title = "Using pw::StringBuilder instead of snprintf"
binaries = [
diff --git a/pw_string/CMakeLists.txt b/pw_string/CMakeLists.txt
index 6103ddcd6..07bba6293 100644
--- a/pw_string/CMakeLists.txt
+++ b/pw_string/CMakeLists.txt
@@ -14,35 +14,99 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_string
+pw_add_module_config(pw_string_CONFIG)
+
+pw_add_library(pw_string.config INTERFACE
+ HEADERS
+ public/pw_string/internal/config.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ ${pw_string_CONFIG}
+)
+
+pw_add_library(pw_string INTERFACE
+ PUBLIC_DEPS
+ pw_string.builder
+ pw_string.format
+ pw_string.to_string
+ pw_string.util
+)
+
+pw_add_library(pw_string.builder STATIC
HEADERS
- public/pw_string/format.h
- public/pw_string/internal/length.h
public/pw_string/string_builder.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_string.format
+ pw_string.string
+ pw_string.to_string
+ pw_string.util
+ pw_preprocessor
+ pw_status
+ SOURCES
+ string_builder.cc
+)
+
+pw_add_library(pw_string.format STATIC
+ HEADERS
+ public/pw_string/format.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_preprocessor
+ pw_span
+ pw_status
+ SOURCES
+ format.cc
+)
+
+pw_add_library(pw_string.string INTERFACE
+ HEADERS
+ public/pw_string/string.h
+ public/pw_string/internal/string_impl.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_assert
+ pw_polyfill
+)
+
+pw_add_library(pw_string.to_string STATIC
+ HEADERS
public/pw_string/to_string.h
public/pw_string/type_to_string.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_string.config
+ pw_string.format
+ pw_string.util
+ pw_status
+ SOURCES
+ type_to_string.cc
+)
+
+pw_add_library(pw_string.util STATIC
+ HEADERS
public/pw_string/util.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
pw_assert
- pw_polyfill.span
- pw_preprocessor
pw_result
+ pw_span
pw_status
+ pw_string.string
SOURCES
- format.cc
- string_builder.cc
- type_to_string.cc
+ public/pw_string/internal/length.h
)
-if(Zephyr_FOUND AND CONFIG_PIGWEED_STRING)
- zephyr_link_libraries(pw_string)
-endif()
pw_add_test(pw_string.format_test
SOURCES
format_test.cc
- DEPS
+ PRIVATE_DEPS
pw_string
GROUPS
modules
@@ -52,8 +116,19 @@ pw_add_test(pw_string.format_test
pw_add_test(pw_string.string_builder_test
SOURCES
string_builder_test.cc
- DEPS
+ PRIVATE_DEPS
+ pw_string
+ GROUPS
+ modules
pw_string
+)
+
+pw_add_test(pw_string.string_test
+ SOURCES
+ string_test.cc
+ PRIVATE_DEPS
+ pw_compilation_testing._pigweed_only_negative_compilation
+ pw_string.string
GROUPS
modules
pw_string
@@ -62,8 +137,9 @@ pw_add_test(pw_string.string_builder_test
pw_add_test(pw_string.to_string_test
SOURCES
to_string_test.cc
- DEPS
+ PRIVATE_DEPS
pw_string
+ pw_string.config
GROUPS
modules
pw_string
@@ -72,7 +148,7 @@ pw_add_test(pw_string.to_string_test
pw_add_test(pw_string.type_to_string_test
SOURCES
type_to_string_test.cc
- DEPS
+ PRIVATE_DEPS
pw_string
GROUPS
modules
@@ -82,9 +158,13 @@ pw_add_test(pw_string.type_to_string_test
pw_add_test(pw_string.util_test
SOURCES
util_test.cc
- DEPS
+ PRIVATE_DEPS
pw_string
GROUPS
modules
pw_string
)
+
+if(Zephyr_FOUND AND CONFIG_PIGWEED_STRING)
+ zephyr_link_libraries(pw_string)
+endif()
diff --git a/pw_string/api.rst b/pw_string/api.rst
new file mode 100644
index 000000000..a526023eb
--- /dev/null
+++ b/pw_string/api.rst
@@ -0,0 +1,91 @@
+.. _module-pw_string-api:
+
+=============
+API Reference
+=============
+
+--------
+Overview
+--------
+This module provides two types of strings, and utility functions for working
+with strings.
+
+**pw::StringBuilder**
+
+.. doxygenfile:: pw_string/string_builder.h
+ :sections: briefdescription
+
+**pw::InlineString**
+
+.. doxygenfile:: pw_string/string.h
+ :sections: briefdescription
+
+**String utility functions**
+
+.. doxygenfile:: pw_string/util.h
+ :sections: briefdescription
+
+-----------------
+pw::StringBuilder
+-----------------
+.. doxygenfile:: pw_string/string_builder.h
+ :sections: briefdescription
+.. doxygenclass:: pw::StringBuilder
+ :members:
+
+----------------
+pw::InlineString
+----------------
+.. doxygenfile:: pw_string/string.h
+ :sections: detaileddescription
+
+.. doxygenclass:: pw::InlineBasicString
+ :members:
+
+.. doxygentypedef:: pw::InlineString
+
+------------------------
+String utility functions
+------------------------
+
+pw::string::Assign()
+--------------------
+.. doxygenfunction:: pw::string::Assign(InlineString<> &string, const std::string_view &view)
+
+pw::string::Append()
+--------------------
+.. doxygenfunction:: pw::string::Append(InlineString<>& string, const std::string_view& view)
+
+pw::string::ClampedCString()
+----------------------------
+.. doxygenfunction:: pw::string::ClampedCString(const char* str, size_t max_len)
+.. doxygenfunction:: pw::string::ClampedCString(span<const char> str)
+
+pw::string::Copy()
+------------------
+.. doxygenfunction:: pw::string::Copy(const char* source, char* dest, size_t num)
+.. doxygenfunction:: pw::string::Copy(const char* source, Span&& dest)
+.. doxygenfunction:: pw::string::Copy(const std::string_view& source, Span&& dest)
+
+It also has variants that provide a destination of ``pw::Vector<char>``
+(see :ref:`module-pw_containers` for details) that do not store the null
+terminator in the vector.
+
+.. cpp:function:: StatusWithSize Copy(const std::string_view& source, pw::Vector<char>& dest)
+.. cpp:function:: StatusWithSize Copy(const char* source, pw::Vector<char>& dest)
+
+pw::string::Format()
+--------------------
+.. doxygenfile:: pw_string/format.h
+ :sections: briefdescription
+.. doxygenfunction:: pw::string::Format
+.. doxygenfunction:: pw::string::FormatVaList
+
+pw::string::NullTerminatedLength()
+----------------------------------
+.. doxygenfunction:: pw::string::NullTerminatedLength(const char* str, size_t max_len)
+.. doxygenfunction:: pw::string::NullTerminatedLength(span<const char> str)
+
+pw::string::PrintableCopy()
+---------------------------
+.. doxygenfunction:: pw::string::PrintableCopy(const std::string_view& source, span<char> dest)
diff --git a/pw_string/design.rst b/pw_string/design.rst
new file mode 100644
index 000000000..91202e185
--- /dev/null
+++ b/pw_string/design.rst
@@ -0,0 +1,75 @@
+.. _module-pw_string-design:
+
+================
+pw_string design
+================
+``pw_string`` provides string classes and utility functions designed to
+prioritize safety and static allocation. The APIs are broadly similar to those
+of the string classes in the C++ standard library, so familiarity with those
+classes will provide some context around ``pw_string`` design decisions.
+
+------------
+InlineString
+------------
+:cpp:type:`pw::InlineString` / :cpp:class:`pw::InlineBasicString` are designed
+to match the ``std::string`` / ``std::basic_string<T>`` API as closely as
+possible, but with key differences to improve performance on embedded systems:
+
+- **Fixed capacity:** Operations that add characters to the string beyond its
+ capacity are an error. These trigger a ``PW_ASSERT`` at runtime. When
+ detectable, these situations trigger a ``static_assert`` at compile time.
+- **Minimal overhead:** :cpp:type:`pw::InlineString` operations never
+ allocate. Reading the contents of the string is a direct memory access within
+ the string object, without pointer indirection.
+- **Constexpr support:** :cpp:type:`pw::InlineString` works in ``constexpr``
+ contexts, which is not supported by ``std::string`` until C++20.
+
+We don't aim to provide complete API compatibility with
+``std::string`` / ``std::basic_string<T>``. Some areas of deviation include:
+
+- **Compile-time capacity checks:** :cpp:type:`InlineString` provides overloads
+ specific to character arrays. These perform compile-time capacity checks and
+ are used for class template argument deduction.
+- **Implicit conversions from** ``std::string_view`` **:** Specifying the
+ capacity parameter is cumbersome, so implicit conversions are helpful. Also,
+ implicitly creating a :cpp:type:`InlineString` is less costly than creating a
+ ``std::string``. As with ``std::string``, explicit conversions are required
+ from types that convert to ``std::string_view``.
+- **No dynamic allocation functions:** Functions that allocate memory, like
+ ``reserve()``, ``shrink_to_fit()``, and ``get_allocator()``, are simply not
+ present.
+
+Capacity
+========
+:cpp:type:`InlineBasicString` has a template parameter for the capacity, but the
+capacity does not need to be known by the user to use the string safely. The
+:cpp:type:`InlineBasicString` template inherits from a
+:cpp:type:`InlineBasicString` specialization with capacity of the reserved value
+``pw::InlineString<>::npos``. The actual capacity is stored in a single word
+alongside the size. This allows code to work with strings of any capacity
+through a ``InlineString<>`` or ``InlineBasicString<T>`` reference.
+
+Exceeding the capacity
+----------------------
+Any :cpp:type:`pw::InlineString` operations that exceed the string's capacity
+fail an assertion, resulting in a crash. Helpers are provided in
+``pw_string/util.h`` that return ``pw::Status::ResourceExhausted()`` instead of
+failing an assert when the capacity would be exceeded.
+
+------------------------
+String utility functions
+------------------------
+
+Safe length checking
+====================
+This module provides two safer alternatives to ``std::strlen`` in case the
+string is extremely long and/or potentially not null-terminated.
+
+First, a constexpr alternative to C11's ``strnlen_s`` is offerred through
+:cpp:func:`pw::string::ClampedCString`. This does not return a length by
+design and instead returns a string_view which does not require
+null-termination.
+
+Second, a constexpr specialized form is offered where null termination is
+required through :cpp:func:`pw::string::NullTerminatedLength`. This will only
+return a length if the string is null-terminated.
diff --git a/pw_string/docs.rst b/pw_string/docs.rst
index c44f5801b..e7b46608d 100644
--- a/pw_string/docs.rst
+++ b/pw_string/docs.rst
@@ -1,194 +1,180 @@
.. _module-pw_string:
+.. rst-class:: with-subtitle
+
=========
pw_string
=========
-String manipulation is a very common operation, but the standard C and C++
-string libraries have drawbacks. The C++ functions are easy-to-use and powerful,
-but require too much flash and memory for many embedded projects. The C string
-functions are lighter weight, but can be difficult to use correctly. Mishandling
-of null terminators or buffer sizes can result in serious bugs.
-
-The ``pw_string`` module provides the flexibility, ease-of-use, and safety of
-C++-style string manipulation, but with no dynamic memory allocation and a much
-smaller binary size impact. Using ``pw_string`` in place of the standard C
-functions eliminates issues related to buffer overflow or missing null
-terminators.
-
--------------
-Compatibility
--------------
-C++17
-
------
-Usage
------
-pw::string::Format
-==================
-The ``pw::string::Format`` and ``pw::string::FormatVaList`` functions provide
-safer alternatives to ``std::snprintf`` and ``std::vsnprintf``. The snprintf
-return value is awkward to interpret, and misinterpreting it can lead to serious
-bugs.
-
-Size report: replacing snprintf with pw::string::Format
--------------------------------------------------------
-The ``Format`` functions have a small, fixed code size cost. However, relative
-to equivalent ``std::snprintf`` calls, there is no incremental code size cost to
-using ``Format``.
-
-.. include:: format_size_report
-
-Safe Length Checking
-====================
-This module provides two safer alternatives to ``std::strlen`` in case the
-string is extremely long and/or potentially not null-terminated.
-
-First, a constexpr alternative to C11's ``strnlen_s`` is offerred through
-:cpp:func:`pw::string::ClampedCString`. This does not return a length by
-design and instead returns a string_view which does not require
-null-termination.
-
-Second, a constexpr specialized form is offered where null termination is
-required through :cpp:func:`pw::string::NullTerminatedLength`. This will only
-return a length if the string is null-terminated.
-
-.. cpp:function:: constexpr std::string_view pw::string::ClampedCString(std::span<const char> str)
-.. cpp:function:: constexpr std::string_view pw::string::ClampedCString(const char* str, size_t max_len)
-
- Safe alternative to the string_view constructor to avoid the risk of an
- unbounded implicit or explicit use of strlen.
-
- This is strongly recommended over using something like C11's strnlen_s as
- a string_view does not require null-termination.
-
-.. cpp:function:: constexpr pw::Result<size_t> pw::string::NullTerminatedLength(std::span<const char> str)
-.. cpp:function:: pw::Result<size_t> pw::string::NullTerminatedLength(const char* str, size_t max_len)
-
- Safe alternative to strlen to calculate the null-terminated length of the
- string within the specified span, excluding the null terminator. Like C11's
- strnlen_s, the scan for the null-terminator is bounded.
-
- Returns:
- null-terminated length of the string excluding the null terminator.
- OutOfRange - if the string is not null-terminated.
-
- Precondition: The string shall be at a valid pointer.
-
-pw::string::Copy
-================
-The ``pw::string::Copy`` functions provide a safer alternative to
-``std::strncpy`` as it always null-terminates whenever the destination
-buffer has a non-zero size.
-
-.. cpp:function:: StatusWithSize Copy(const std::string_view& source, std::span<char> dest)
-.. cpp:function:: StatusWithSize Copy(const char* source, std::span<char> dest)
-.. cpp:function:: StatusWithSize Copy(const char* source, char* dest, size_t num)
-
- Copies the source string to the dest, truncating if the full string does not
- fit. Always null terminates if dest.size() or num > 0.
-
- Returns the number of characters written, excluding the null terminator. If
- the string is truncated, the status is ResourceExhausted.
-
- Precondition: The destination and source shall not overlap.
- Precondition: The source shall be a valid pointer.
-
-pw::StringBuilder
-=================
-``pw::StringBuilder`` facilitates building formatted strings in a fixed-size
-buffer. It is designed to give the flexibility of ``std::string`` and
-``std::ostringstream``, but with a small footprint.
-
-.. code-block:: cpp
-
- #include "pw_log/log.h"
- #include "pw_string/string_builder.h"
- pw::Status LogProducedData(std::string_view func_name,
- std::span<const std::byte> data) {
- pw::StringBuffer<42> sb;
-
- // Append a std::string_view to the buffer.
- sb << func_name;
-
- // Append a format string to the buffer.
- sb.Format(" produced %d bytes of data: ", static_cast<int>(data.data()));
-
- // Append bytes as hex to the buffer.
- sb << data;
-
- // Log the final string.
- PW_LOG_DEBUG("%s", sb.c_str());
-
- // Errors encountered while mutating the string builder are tracked.
- return sb.status();
- }
+.. pigweed-module::
+ :name: pw_string
+ :tagline: Efficient, easy, and safe string manipulation
+ :status: stable
+ :languages: C++14, C++17
+ :code-size-impact: 500 to 1500 bytes
+ :get-started: module-pw_string-get-started
+ :design: module-pw_string-design
+ :guides: module-pw_string-guide
+ :api: module-pw_string-api
+
+ - **Efficient**: No memory allocation, no pointer indirection.
+ - **Easy**: Use the string API you already know.
+ - **Safe**: Never worry about buffer overruns or undefined behavior.
+
+ *Pick three!* If you know how to use ``std::string``, just use
+ :cpp:type:`pw::InlineString` in the same way:
+
+ .. code:: cpp
+
+ // Create a string from a C-style char array; storage is pre-allocated!
+ pw::InlineString<16> my_string = "Literally";
+
+ // We have some space left, so let's add to the string.
+ my_string.append('?', 3); // "Literally???"
+
+ // Let's try something evil and extend this past its capacity 😈
+ my_string.append('!', 8);
+ // Foiled by a crash! No mysterious bugs or undefined behavior.
+
+ Need to build up a string? :cpp:type:`pw::StringBuilder` works like
+ ``std::ostringstream``, but with most of the efficiency and memory benefits
+ of :cpp:type:`pw::InlineString`:
+
+ .. code:: cpp
+
+ // Create a pw::StringBuilder with a built-in buffer
+ pw::StringBuffer<32> my_string_builder = "Is it really this easy?";
+
+ // Add to it with idiomatic C++
+ my_string << " YES!";
+
+ // Use it like any other string
+ PW_LOG_DEBUG("%s", my_string_builder.c_str());
+
+ Check out :ref:`module-pw_string-guide` for more code samples.
+
+----------
+Background
+----------
+String manipulation on embedded systems can be surprisingly challenging.
+C strings are light weight but come with many pitfalls for those who don't know
+the standard library deeply. C++ provides string classes that are safe and easy
+to use, but they consume way too much code space and are designed to be used
+with dynamic memory allocation.
+
+Embedded systems need string functionality that is both safe and suitable for
+resource-constrained platforms.
+
+------------
+Our solution
+------------
+``pw_string`` provides safe string handling functionality with an API that
+closely matches that of ``std::string``, but without dynamic memory allocation
+and with a *much* smaller :ref:`binary size impact <module-pw_string-size-reports>`.
+
+---------------
+Who this is for
+---------------
+``pw_string`` is useful any time you need to handle strings in embedded C++.
+
+--------------------
+Is it right for you?
+--------------------
+If your project written in C, ``pw_string`` is not a good fit since we don't
+currently expose a C API.
+
+For larger platforms where code space isn't in short supply and dynamic memory
+allocation isn't a problem, you may find that ``std::string`` meets your needs.
+
+.. tip::
+ ``pw_string`` works just as well on larger embedded platforms and host
+ systems. Using ``pw_string`` even when you might get away with ``std:string``
+ gives you the flexibility to move to smaller platforms later with much less
+ rework.
+
+Here are some size reports that may affect whether ``pw_string`` is right for
+you.
+
+.. _module-pw_string-size-reports:
+
+Size comparison: snprintf versus pw::StringBuilder
+--------------------------------------------------
+:cpp:type:`pw::StringBuilder` is safe, flexible, and results in much smaller
+code size than using ``std::ostringstream``. However, applications sensitive to
+code size should use :cpp:type:`pw::StringBuilder` with care.
+
+The fixed code size cost of :cpp:type:`pw::StringBuilder` is significant, though
+smaller than ``std::snprintf``. Using :cpp:type:`pw::StringBuilder`'s ``<<`` and
+``append`` methods exclusively in place of ``snprintf`` reduces code size, but
+``snprintf`` may be difficult to avoid.
+
+The incremental code size cost of :cpp:type:`pw::StringBuilder` is comparable to
+``snprintf`` if errors are handled. Each argument to
+:cpp:type:`pw::StringBuilder`'s ``<<`` method expands to a function call, but
+one or two :cpp:type:`pw::StringBuilder` appends may have a smaller code size
+impact than a single ``snprintf`` call.
-Supporting custom types with StringBuilder
-------------------------------------------
-As with ``std::ostream``, StringBuilder supports printing custom types by
-overriding the ``<<`` operator. This is is done by defining ``operator<<`` in
-the same namespace as the custom type. For example:
+.. include:: string_builder_size_report
-.. code-block:: cpp
+Size comparison: snprintf versus pw::string::Format
+---------------------------------------------------
+The ``pw::string::Format`` functions have a small, fixed code size
+cost. However, relative to equivalent ``std::snprintf`` calls, there is no
+incremental code size cost to using ``pw::string::Format``.
- namespace my_project {
+.. include:: format_size_report
- struct MyType {
- int foo;
- const char* bar;
- };
+Roadmap
+-------
+* StringBuilder's fixed size cost can be dramatically reduced by limiting
+ support for 64-bit integers.
+* Consider integrating with the tokenizer module.
- pw::StringBuilder& operator<<(pw::StringBuilder& sb, const MyType& value) {
- return sb << "MyType(" << value.foo << ", " << value.bar << ')';
- }
+Compatibility
+-------------
+C++17, C++14 (:cpp:type:`pw::InlineString`)
- } // namespace my_project
+.. _module-pw_string-get-started:
-Internally, ``StringBuilder`` uses the ``ToString`` function to print. The
-``ToString`` template function can be specialized to support custom types with
-``StringBuilder``, though it is recommended to overload ``operator<<`` instead.
-This example shows how to specialize ``pw::ToString``:
+---------------
+Getting started
+---------------
-.. code-block:: cpp
+GN
+--
- #include "pw_string/to_string.h"
+Add ``$dir_pw_string`` to the ``deps`` list in your ``pw_executable()`` build
+target:
- namespace pw {
+.. code::
- template <>
- StatusWithSize ToString<MyStatus>(MyStatus value, std::span<char> buffer) {
- return Copy(MyStatusString(value), buffer);
+ pw_executable("...") {
+ # ...
+ deps = [
+ # ...
+ "$dir_pw_string",
+ # ...
+ ]
}
- } // namespace pw
-
-Size report: replacing snprintf with pw::StringBuilder
-------------------------------------------------------
-StringBuilder is safe, flexible, and results in much smaller code size than
-using ``std::ostringstream``. However, applications sensitive to code size
-should use StringBuilder with care.
-
-The fixed code size cost of StringBuilder is significant, though smaller than
-``std::snprintf``. Using StringBuilder's << and append methods exclusively in
-place of ``snprintf`` reduces code size, but ``snprintf`` may be difficult to
-avoid.
-
-The incremental code size cost of StringBuilder is comparable to ``snprintf`` if
-errors are handled. Each argument to StringBuilder's ``<<`` expands to a
-function call, but one or two StringBuilder appends may have a smaller code size
-impact than a single ``snprintf`` call.
-
-.. include:: string_builder_size_report
-
------------
-Future work
------------
-* StringBuilder's fixed size cost can be dramatically reduced by limiting
- support for 64-bit integers.
-* Consider integrating with the tokenizer module.
+See `//source/BUILD.gn <https://pigweed.googlesource.com/pigweed/sample_project/+/refs/heads/main/source/BUILD.gn>`_
+in the Pigweed Sample Project for an example.
Zephyr
-======
-To enable ``pw_string`` for Zephyr add ``CONFIG_PIGWEED_STRING=y`` to the
-project's configuration.
+------
+Add ``CONFIG_PIGWEED_STRING=y`` to the Zephyr project's configuration.
+
+-------
+Roadmap
+-------
+* The fixed size cost of :cpp:type:`pw::StringBuilder` can be dramatically
+ reduced by limiting support for 64-bit integers.
+* ``pw_string`` may be integrated with :ref:`module-pw_tokenizer`.
+
+.. toctree::
+ :hidden:
+ :maxdepth: 1
+
+ design
+ guide
+ api
diff --git a/pw_string/format.cc b/pw_string/format.cc
index c08bf3c03..57dea10f2 100644
--- a/pw_string/format.cc
+++ b/pw_string/format.cc
@@ -18,7 +18,7 @@
namespace pw::string {
-StatusWithSize Format(std::span<char> buffer, const char* format, ...) {
+StatusWithSize Format(span<char> buffer, const char* format, ...) {
va_list args;
va_start(args, format);
const StatusWithSize result = FormatVaList(buffer, format, args);
@@ -27,7 +27,7 @@ StatusWithSize Format(std::span<char> buffer, const char* format, ...) {
return result;
}
-StatusWithSize FormatVaList(std::span<char> buffer,
+StatusWithSize FormatVaList(span<char> buffer,
const char* format,
va_list args) {
if (buffer.empty()) {
diff --git a/pw_string/format_test.cc b/pw_string/format_test.cc
index 9aef4195b..f2bfbf545 100644
--- a/pw_string/format_test.cc
+++ b/pw_string/format_test.cc
@@ -15,9 +15,9 @@
#include "pw_string/format.h"
#include <cstdarg>
-#include <span>
#include "gtest/gtest.h"
+#include "pw_span/span.h"
namespace pw::string {
namespace {
@@ -41,7 +41,7 @@ TEST(Format, ValidFormatStringAndArguments_Succeeds) {
}
TEST(Format, EmptyBuffer_ReturnsResourceExhausted) {
- auto result = Format(std::span<char>(), "?");
+ auto result = Format(span<char>(), "?");
EXPECT_EQ(Status::ResourceExhausted(), result.status());
EXPECT_EQ(0u, result.size());
@@ -65,9 +65,7 @@ TEST(Format, ArgumentLargerThanBuffer_ReturnsResourceExhausted) {
EXPECT_STREQ("2big", buffer);
}
-StatusWithSize CallFormatWithVaList(std::span<char> buffer,
- const char* fmt,
- ...) {
+StatusWithSize CallFormatWithVaList(span<char> buffer, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
diff --git a/pw_string/guide.rst b/pw_string/guide.rst
new file mode 100644
index 000000000..87ceec03e
--- /dev/null
+++ b/pw_string/guide.rst
@@ -0,0 +1,248 @@
+.. _module-pw_string-guide:
+
+================
+pw_string: Guide
+================
+
+InlineString and StringBuilder?
+===============================
+Use :cpp:type:`pw::InlineString` if you need:
+
+* Compatibility with ``std::string``
+* Storage internal to the object
+* A string object to persist in other data structures
+* Lower code size overhead
+
+Use :cpp:class:`pw::StringBuilder` if you need:
+
+* Compatibility with ``std::ostringstream``, including custom object support
+* Storage external to the object
+* Non-fatal handling of failed append/format operations
+* Tracking of the status of a series of operations
+* A temporary stack object to aid string construction
+* Medium code size overhead
+
+An example of when to prefer :cpp:type:`pw::InlineString` is wrapping a
+length-delimited string (e.g. ``std::string_view``) for APIs that require null
+termination:
+
+.. code-block:: cpp
+
+ #include <string>
+ #include "pw_log/log.h"
+ #include "pw_string/string_builder.h"
+
+ void ProcessName(std::string_view name) {
+ // %s format strings require null terminated strings, so create one on the
+ // stack with size up to kMaxNameLen, copy the string view `name` contents
+ // into it, add a null terminator, and log it.
+ PW_LOG_DEBUG("The name is %s",
+ pw::InlineString<kMaxNameLen>(name).c_str());
+ }
+
+An example of when to prefer :cpp:class:`pw::StringBuilder` is when
+constructing a string for external use.
+
+.. code-block:: cpp
+
+ #include "pw_string/string_builder.h"
+
+ pw::Status FlushSensorValueToUart(int32_t sensor_value) {
+ pw::StringBuffer<42> sb;
+ sb << "Sensor value: ";
+ sb << sensor_value; // Formats as int.
+ FlushCStringToUart(sb.c_str());
+
+ if (!sb.status().ok) {
+ format_error_metric.Increment(); // Track overflows.
+ }
+ return sb.status();
+ }
+
+
+Building strings with pw::StringBuilder
+=======================================
+The following shows basic use of a :cpp:class:`pw::StringBuilder`.
+
+.. code-block:: cpp
+
+ #include "pw_log/log.h"
+ #include "pw_string/string_builder.h"
+
+ pw::Status LogProducedData(std::string_view func_name,
+ span<const std::byte> data) {
+ // pw::StringBuffer allocates a pw::StringBuilder with a built-in buffer.
+ pw::StringBuffer<42> sb;
+
+ // Append a std::string_view to the buffer.
+ sb << func_name;
+
+ // Append a format string to the buffer.
+ sb.Format(" produced %d bytes of data: ", static_cast<int>(data.data()));
+
+ // Append bytes as hex to the buffer.
+ sb << data;
+
+ // Log the final string.
+ PW_LOG_DEBUG("%s", sb.c_str());
+
+ // Errors encountered while mutating the string builder are tracked.
+ return sb.status();
+ }
+
+Building strings with pw::InlineString
+======================================
+:cpp:type:`pw::InlineString` objects must be constructed by specifying a fixed
+capacity for the string.
+
+.. code-block:: c++
+
+ #include "pw_string/string.h"
+
+ // Initialize from a C string.
+ pw::InlineString<32> inline_string = "Literally";
+ inline_string.append('?', 3); // contains "Literally???"
+
+ // Supports copying into known-capacity strings.
+ pw::InlineString<64> other = inline_string;
+
+ // Supports various helpful std::string functions
+ if (inline_string.starts_with("Lit") || inline_string == "not\0literally"sv) {
+ other += inline_string;
+ }
+
+ // Like std::string, InlineString is always null terminated when accessed
+ // through c_str(). InlineString can be used to null-terminate
+ // length-delimited strings for APIs that expect null-terminated strings.
+ std::string_view file(".gif");
+ if (std::fopen(pw::InlineString<kMaxNameLen>(file).c_str(), "r") == nullptr) {
+ return;
+ }
+
+ // pw::InlineString integrates well with std::string_view. It supports
+ // implicit conversions to and from std::string_view.
+ inline_string = std::string_view("not\0literally", 12);
+
+ FunctionThatTakesAStringView(inline_string);
+
+ FunctionThatTakesAnInlineString(std::string_view("1234", 4));
+
+Building strings inside InlineString with a StringBuilder
+=========================================================
+:cpp:class:`pw::StringBuilder` can build a string in a
+:cpp:type:`pw::InlineString`:
+
+.. code-block:: c++
+
+ #include "pw_string/string.h"
+
+ void DoFoo() {
+ InlineString<32> inline_str;
+ StringBuilder sb(inline_str);
+ sb << 123 << "456";
+ // inline_str contains "456"
+ }
+
+Passing InlineStrings as parameters
+===================================
+:cpp:type:`pw::InlineString` objects can be passed to non-templated functions
+via type erasure. This saves code size in most cases, since it avoids template
+expansions triggered by string size differences.
+
+Unknown size strings
+--------------------
+To operate on :cpp:type:`pw::InlineString` objects without knowing their type,
+use the ``pw::InlineString<>`` type, shown in the examples below:
+
+.. code-block:: c++
+
+ // Note that the first argument is a generically-sized InlineString.
+ void RemoveSuffix(pw::InlineString<>& string, std::string_view suffix) {
+ if (string.ends_with(suffix)) {
+ string.resize(string.size() - suffix.size());
+ }
+ }
+
+ void DoStuff() {
+ pw::InlineString<32> str1 = "Good morning!";
+ RemoveSuffix(str1, " morning!");
+
+ pw::InlineString<40> str2 = "Good";
+ RemoveSuffix(str2, " morning!");
+
+ PW_ASSERT(str1 == str2);
+ }
+
+However, generically sized :cpp:type:`pw::InlineString` objects don't work in
+``constexpr`` contexts.
+
+Known size strings
+------------------
+:cpp:type:`pw::InlineString` operations on known-size strings may be used in
+``constexpr`` expressions.
+
+.. code-block:: c++
+
+ static constexpr pw::InlineString<64> kMyString = [] {
+ pw::InlineString<64> string;
+
+ for (int i = 0; i < 10; ++i) {
+ string += "Hello";
+ }
+
+ return string;
+ }();
+
+Compact initialization of InlineStrings
+=======================================
+:cpp:type:`pw::InlineBasicString` supports class template argument deduction
+(CTAD) in C++17 and newer. Since :cpp:type:`pw::InlineString` is an alias, CTAD
+is not supported until C++20.
+
+.. code-block:: c++
+
+ // Deduces a capacity of 5 characters to match the 5-character string literal
+ // (not counting the null terminator).
+ pw::InlineBasicString inline_string = "12345";
+
+ // In C++20, CTAD may be used with the pw::InlineString alias.
+ pw::InlineString my_other_string("123456789");
+
+Supporting custom types with StringBuilder
+==========================================
+As with ``std::ostream``, StringBuilder supports printing custom types by
+overriding the ``<<`` operator. This is is done by defining ``operator<<`` in
+the same namespace as the custom type. For example:
+
+.. code-block:: cpp
+
+ namespace my_project {
+
+ struct MyType {
+ int foo;
+ const char* bar;
+ };
+
+ pw::StringBuilder& operator<<(pw::StringBuilder& sb, const MyType& value) {
+ return sb << "MyType(" << value.foo << ", " << value.bar << ')';
+ }
+
+ } // namespace my_project
+
+Internally, ``StringBuilder`` uses the ``ToString`` function to print. The
+``ToString`` template function can be specialized to support custom types with
+``StringBuilder``, though it is recommended to overload ``operator<<`` instead.
+This example shows how to specialize ``pw::ToString``:
+
+.. code-block:: cpp
+
+ #include "pw_string/to_string.h"
+
+ namespace pw {
+
+ template <>
+ StatusWithSize ToString<MyStatus>(MyStatus value, span<char> buffer) {
+ return Copy(MyStatusString(value), buffer);
+ }
+
+ } // namespace pw
diff --git a/pw_string/public/pw_string/format.h b/pw_string/public/pw_string/format.h
index 9499eb23a..205210a8c 100644
--- a/pw_string/public/pw_string/format.h
+++ b/pw_string/public/pw_string/format.h
@@ -22,30 +22,33 @@
// the null terminator.
#include <cstdarg>
-#include <span>
#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
namespace pw::string {
-// Writes a printf-style formatted string to the provided buffer, similarly to
-// std::snprintf. Returns the number of characters written, excluding the null
-// terminator. The buffer is always null-terminated unless it is empty.
-//
-// The status is
-//
-// OkStatus() if the operation succeeded,
-// Status::ResourceExhausted() if the buffer was too small to fit the output,
-// Status::InvalidArgument() if there was a formatting error.
-//
+/// @brief Writes a printf-style formatted string to the provided buffer,
+/// similarly to `std::snprintf()`.
+///
+/// The `std::snprintf()` return value is awkward to interpret, and
+/// misinterpreting it can lead to serious bugs.
+///
+/// @returns The number of characters written, excluding the null
+/// terminator. The buffer is always null-terminated unless it is empty.
+/// The status is `OkStatus()` if the operation succeeded,
+/// `Status::ResourceExhausted()` if the buffer was too small to fit the output,
+/// or `Status::InvalidArgument()` if there was a formatting error.
PW_PRINTF_FORMAT(2, 3)
-StatusWithSize Format(std::span<char> buffer, const char* format, ...);
+StatusWithSize Format(span<char> buffer, const char* format, ...);
-// Writes a printf-style formatted string with va_list-packed arguments to the
-// provided buffer, similarly to std::vsnprintf. The return value is the same as
-// above.
-StatusWithSize FormatVaList(std::span<char> buffer,
+/// @brief Writes a printf-style formatted string with va_list-packed arguments
+/// to the provided buffer, similarly to `std::vsnprintf()`.
+///
+/// @returns See `pw::string::Format()`.
+PW_PRINTF_FORMAT(2, 0)
+StatusWithSize FormatVaList(span<char> buffer,
const char* format,
va_list args);
diff --git a/pw_string/public/pw_string/internal/config.h b/pw_string/public/pw_string/internal/config.h
new file mode 100644
index 000000000..259e0d2fe
--- /dev/null
+++ b/pw_string/public/pw_string/internal/config.h
@@ -0,0 +1,35 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+// PW_STRING_ENABLE_DECIMAL_FLOAT_EXPANSION controls whether floating point
+// values passed to the ToString function will be expanded after a decimal
+// point, or just rounded to the nearest int. Enabling decimal expansion may
+// significantly increase code size.
+//
+// Note: This currently relies on floating point support for `snprintf`, which
+// might require extra compiler configuration, e.g. `-u_printf_float` for
+// newlib-nano.
+#ifndef PW_STRING_ENABLE_DECIMAL_FLOAT_EXPANSION
+#define PW_STRING_ENABLE_DECIMAL_FLOAT_EXPANSION 0
+#endif
+
+namespace pw::string::internal::config {
+
+constexpr bool kEnableDecimalFloatExpansion =
+ PW_STRING_ENABLE_DECIMAL_FLOAT_EXPANSION;
+
+}
+
+#undef PW_STRING_ENABLE_DECIMAL_FLOAT_EXPANSION
diff --git a/pw_string/public/pw_string/internal/string_common_functions.inc b/pw_string/public/pw_string/internal/string_common_functions.inc
new file mode 100644
index 000000000..c2d8625b6
--- /dev/null
+++ b/pw_string/public/pw_string/internal/string_common_functions.inc
@@ -0,0 +1,324 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// This file contains the definitions of most pw::InlineBasicString functions.
+// The file is included inline in the fixed-capacity pw::InlineBasicString<T,
+// kCapacity> and generic-capacity pw::InlineBasicString<T> specialization
+// classes.
+//
+// These functions cannot simply be defined in the pw::InlineBasicString<T> base
+// class because:
+//
+// 1. Many functions return a *this reference. The functions should to return
+// a reference to the exact type they are called on rather than the
+// generic-capacity base class.
+// 2. Operations on the generic base class cannot be constexpr unless the
+// class is an InlineBasicString<T, 0>. The functions must be defined in
+// the fixed-capacity dervied classes to support constexpr operations.
+//
+// These functions have no logic and simply defer to shared, length-agnostic
+// implementations so they do not increase code size.
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC_GCC(ignored, "-Wtype-limits");
+
+// Assignment functions
+
+constexpr InlineBasicString& assign(size_type count, T ch) {
+ return static_cast<InlineBasicString&>(Fill(data(), ch, count));
+}
+
+// Checks capacity rather than current size.
+template <size_type kOtherCapacity>
+constexpr InlineBasicString& assign(
+ const InlineBasicString<T, kOtherCapacity>& other) {
+ static_assert(
+ kOtherCapacity == string_impl::kGeneric || kOtherCapacity <= kCapacity,
+ _PW_STRING_CAPACITY_TOO_SMALL_FOR_STRING);
+ return static_cast<InlineBasicString&>(
+ Copy(data(), other.data(), other.size()));
+}
+
+constexpr InlineBasicString& assign(const InlineBasicString& other,
+ size_type index,
+ size_type count = npos) {
+ return static_cast<InlineBasicString&>(
+ CopySubstr(data(), other.data(), other.size(), index, count));
+}
+
+constexpr InlineBasicString& assign(const T* string, size_type count) {
+ return static_cast<InlineBasicString&>(Copy(data(), string, count));
+}
+
+template <typename U, typename = string_impl::EnableIfNonArrayCharPointer<T, U>>
+constexpr InlineBasicString& assign(U c_string) {
+ return assign(c_string,
+ string_impl::BoundedStringLength(c_string, max_size()));
+}
+
+// Assignment from character array or string literal. For known-size strings,
+// the capacity is checked against the string/array size at compile time.
+template <size_t kCharArraySize>
+constexpr InlineBasicString& assign(const T (&array)[kCharArraySize]) {
+ static_assert(
+ string_impl::NullTerminatedArrayFitsInString(kCharArraySize, kCapacity),
+ _PW_STRING_CAPACITY_TOO_SMALL_FOR_ARRAY);
+ return static_cast<InlineBasicString&>(
+ Copy(data(), array, string_impl::ArrayStringLength(array, max_size())));
+}
+
+template <typename InputIterator,
+ typename = string_impl::EnableIfInputIterator<InputIterator>>
+constexpr InlineBasicString& assign(InputIterator start, InputIterator finish) {
+ return static_cast<InlineBasicString&>(
+ IteratorCopy(start, finish, data(), data() + max_size()));
+}
+
+constexpr InlineBasicString& assign(std::initializer_list<T> list) {
+ return assign(list.begin(), list.size());
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+
+template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+constexpr InlineBasicString& assign(const StringView& string) {
+ const std::basic_string_view<T> view = string;
+ PW_ASSERT(view.size() < npos);
+ return assign(view.data(), view.size());
+}
+
+template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+constexpr InlineBasicString& assign(const StringView& string,
+ size_type index,
+ size_type count = npos) {
+ const std::basic_string_view<T> view = string;
+ PW_ASSERT(view.size() < npos);
+ return static_cast<InlineBasicString&>(CopySubstr(
+ data(), view.data(), static_cast<size_type>(view.size()), index, count));
+}
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+constexpr InlineBasicString& assign(std::nullptr_t) = delete;
+
+// Element access
+
+constexpr reference at(size_type index) {
+ PW_ASSERT(index < length());
+ return data()[index];
+}
+constexpr const_reference at(size_type index) const {
+ PW_ASSERT(index < length());
+ return data()[index];
+}
+
+constexpr reference operator[](size_type index) { return data()[index]; }
+constexpr const_reference operator[](size_type index) const {
+ return data()[index];
+}
+
+constexpr reference front() { return data()[0]; }
+constexpr const_reference front() const { return data()[0]; }
+
+constexpr reference back() { return data()[size() - 1]; }
+constexpr const_reference back() const { return data()[size() - 1]; }
+
+constexpr const_pointer c_str() const noexcept { return data(); }
+
+constexpr operator std::basic_string_view<T>() const noexcept {
+ return std::basic_string_view<T>(data(), size());
+}
+
+// Iterators
+
+constexpr iterator begin() noexcept { return &data()[0]; }
+constexpr const_iterator begin() const noexcept { return &data()[0]; }
+constexpr const_iterator cbegin() const noexcept { return &data()[0]; }
+
+constexpr iterator end() noexcept { return &data()[size()]; }
+constexpr const_iterator end() const noexcept { return &data()[size()]; }
+constexpr const_iterator cend() const noexcept { return &data()[size()]; }
+
+constexpr reverse_iterator rbegin() noexcept { return reverse_iterator(end()); }
+constexpr const_reverse_iterator rbegin() const noexcept {
+ return const_reverse_iterator(end());
+}
+constexpr const_reverse_iterator crbegin() const noexcept {
+ return const_reverse_iterator(cend());
+}
+
+constexpr reverse_iterator rend() noexcept { return reverse_iterator(begin()); }
+constexpr const_reverse_iterator rend() const noexcept {
+ return const_reverse_iterator(begin());
+}
+constexpr const_reverse_iterator crend() const noexcept {
+ return const_reverse_iterator(cbegin());
+}
+
+// Capacity
+
+[[nodiscard]] constexpr bool empty() const noexcept { return size() == 0u; }
+
+// The number of characters in the string.
+constexpr size_type length() const noexcept { return size(); }
+
+constexpr size_type capacity() const noexcept { return max_size(); }
+
+// Operations
+
+constexpr void clear() { SetSizeAndTerminate(data(), 0); }
+
+// TODO(b/239996007): Implement insert and erase.
+
+constexpr void push_back(value_type ch) {
+ static_assert(kCapacity != 0,
+ "Cannot add a character to pw::InlineString<0>");
+ PushBack(data(), ch);
+}
+
+constexpr void pop_back() {
+ static_assert(kCapacity != 0,
+ "Cannot remove a character from pw::InlineString<0>");
+ PopBack(data());
+}
+
+constexpr InlineBasicString& append(size_type count, T ch) {
+ return static_cast<InlineBasicString&>(FillExtend(data(), ch, count));
+}
+
+template <size_type kOtherCapacity>
+constexpr InlineBasicString& append(
+ const InlineBasicString<T, kOtherCapacity>& string) {
+ static_assert(
+ kOtherCapacity == string_impl::kGeneric || kOtherCapacity <= kCapacity,
+ _PW_STRING_CAPACITY_TOO_SMALL_FOR_STRING);
+ return append(string.data(), string.size());
+}
+
+template <size_type kOtherCapacity>
+constexpr InlineBasicString& append(
+ const InlineBasicString<T, kOtherCapacity>& other,
+ size_type index,
+ size_type count = npos) {
+ return static_cast<InlineBasicString&>(
+ CopyExtendSubstr(data(), other.data(), other.size(), index, count));
+}
+
+constexpr InlineBasicString& append(const T* string, size_type count) {
+ return static_cast<InlineBasicString&>(CopyExtend(data(), string, count));
+}
+
+template <size_t kCharArraySize>
+constexpr InlineBasicString& append(const T (&array)[kCharArraySize]) {
+ static_assert(
+ string_impl::NullTerminatedArrayFitsInString(kCharArraySize, kCapacity),
+ _PW_STRING_CAPACITY_TOO_SMALL_FOR_ARRAY);
+ return append(array, string_impl::ArrayStringLength(array, max_size()));
+}
+
+template <typename U, typename = string_impl::EnableIfNonArrayCharPointer<T, U>>
+constexpr InlineBasicString& append(U c_string) {
+ return append(c_string,
+ string_impl::BoundedStringLength(c_string, max_size()));
+}
+
+template <typename InputIterator,
+ typename = string_impl::EnableIfInputIterator<InputIterator>>
+constexpr InlineBasicString& append(InputIterator first, InputIterator last) {
+ return static_cast<InlineBasicString&>(
+ IteratorExtend(first, last, data() + size(), data() + max_size()));
+}
+
+constexpr InlineBasicString& append(std::initializer_list<T> list) {
+ return append(list.begin(), list.size());
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+
+template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+constexpr InlineBasicString& append(const StringView& string) {
+ const std::basic_string_view<T> view = string;
+ PW_ASSERT(view.size() < npos);
+ return append(view.data(), view.size());
+}
+
+template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+constexpr InlineBasicString& append(const StringView& string,
+ size_type index,
+ size_type count = npos) {
+ const std::basic_string_view<T> view = string;
+ PW_ASSERT(view.size() < npos);
+ return static_cast<InlineBasicString&>(CopyExtendSubstr(
+ data(), view.data(), static_cast<size_type>(view.size()), index, count));
+}
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+template <size_type kOtherCapacity>
+constexpr int compare(
+ const InlineBasicString<T, kOtherCapacity>& other) const noexcept {
+ return string_impl::Compare(data(), size(), other.data(), other.size());
+}
+
+constexpr int compare(const T* other) const {
+ return string_impl::Compare(
+ data(),
+ size(),
+ other,
+ string_impl::BoundedStringLength(other, max_size()));
+}
+
+// TODO(b/239996007): Implement other compare overloads.
+
+// TODO(b/239996007): Implement other std::string functions:
+//
+// - starts_with
+// - ends_with
+// - replace
+// - substr
+// - copy
+
+constexpr void resize(size_type new_size) { resize(new_size, T()); }
+
+constexpr void resize(size_type new_size, T ch) {
+ return Resize(data(), new_size, ch);
+}
+
+// resize_and_overwrite() only takes the callable object since the underlying
+// buffer has a fixed size.
+template <typename Operation>
+constexpr void resize_and_overwrite(Operation operation) {
+ const auto new_size = std::move(operation)(data(), max_size());
+ PW_ASSERT(static_cast<size_t>(new_size) <= max_size());
+ SetSizeAndTerminate(data(), new_size);
+}
+
+// TODO(b/239996007): Implement swap
+
+// Search
+
+// TODO(b/239996007): Implement std::string search functions:
+//
+// - find
+// - rfind
+// - find_first_of
+// - find_first_not_of
+// - find_last_of
+// - find_last_not_of
+
+PW_MODIFY_DIAGNOSTICS_POP();
diff --git a/pw_string/public/pw_string/internal/string_impl.h b/pw_string/public/pw_string/internal/string_impl.h
new file mode 100644
index 000000000..5c2eb0de1
--- /dev/null
+++ b/pw_string/public/pw_string/internal/string_impl.h
@@ -0,0 +1,179 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <string> // for std::char_traits
+#include <type_traits>
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+#include <string_view>
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+#include "pw_assert/assert.h"
+#include "pw_polyfill/language_feature_macros.h"
+
+namespace pw {
+namespace string_impl {
+
+// pw::InlineString<>::size_type is unsigned short so the capacity and current
+// size fit into a single word.
+using size_type = unsigned short;
+
+template <typename CharType, typename T>
+using EnableIfNonArrayCharPointer = std::enable_if_t<
+ std::is_pointer<T>::value && !std::is_array<T>::value &&
+ std::is_same<CharType, std::remove_cv_t<std::remove_pointer_t<T>>>::value>;
+
+template <typename T>
+using EnableIfInputIterator = std::enable_if_t<
+ std::is_convertible<typename std::iterator_traits<T>::iterator_category,
+ std::input_iterator_tag>::value>;
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+template <typename CharType, typename T>
+using EnableIfStringViewLike = std::enable_if_t<
+ std::is_convertible<const T&, std::basic_string_view<CharType>>() &&
+ !std::is_convertible<const T&, const CharType*>()>;
+
+template <typename CharType, typename T>
+using EnableIfStringViewLikeButNotStringView = std::enable_if_t<
+ !std::is_same<T, std::basic_string_view<CharType>>() &&
+ std::is_convertible<const T&, std::basic_string_view<CharType>>() &&
+ !std::is_convertible<const T&, const CharType*>()>;
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+// Reserved capacity that is used to represent a generic-length
+// pw::InlineString.
+PW_INLINE_VARIABLE constexpr size_type kGeneric = size_type(-1);
+
+#if defined(__cpp_lib_constexpr_string) && __cpp_lib_constexpr_string >= 201907L
+
+// Like std::string and std::string_view, pw::InlineString uses std::char_traits
+// for low-level operations.
+using std::char_traits;
+
+#else
+
+// If constexpr std::char_traits is not available, provide a custom traits class
+// with constexpr versions of the necessary functions. Defer to std::char_traits
+// when possible.
+template <typename T>
+class char_traits : private std::char_traits<T> {
+ public:
+ static constexpr void assign(T& dest, const T& source) noexcept {
+ dest = source;
+ }
+
+ static constexpr T* assign(T* dest, size_t count, T value) {
+ for (size_t i = 0; i < count; ++i) {
+ dest[i] = value;
+ }
+ return dest;
+ }
+
+ using std::char_traits<T>::eq;
+
+ static constexpr T* copy(T* dest, const T* source, size_t count) {
+ for (size_type i = 0; i < count; ++i) {
+ char_traits<T>::assign(dest[i], source[i]);
+ }
+ return dest;
+ }
+
+ using std::char_traits<T>::compare;
+};
+
+#endif // __cpp_lib_constexpr_string
+
+// Used in static_asserts to check that a C array fits in an InlineString.
+constexpr bool NullTerminatedArrayFitsInString(
+ size_t null_terminated_array_size, size_type capacity) {
+ return null_terminated_array_size > 0u &&
+ null_terminated_array_size - 1 <= capacity &&
+ null_terminated_array_size - 1 < kGeneric;
+}
+
+// Constexpr utility functions for pw::InlineString. These are NOT intended for
+// general use. These mostly map directly to general purpose standard library
+// utilities that are not constexpr until C++20.
+
+// Calculates the length of a C string up to the capacity. Returns capacity + 1
+// if the string is longer than the capacity. This replaces
+// std::char_traits<T>::length, which is unbounded. The string must contain at
+// least one character.
+template <typename T>
+constexpr size_type BoundedStringLength(const T* string, size_type capacity) {
+ size_type length = 0;
+ for (; length <= capacity; ++length) {
+ if (char_traits<T>::eq(string[length], T())) {
+ break;
+ }
+ }
+ return length; // length is capacity + 1 if T() was not found.
+}
+
+// As with std::string, InlineString treats literals and character arrays as
+// null-terminated strings. ArrayStringLength checks that the array size fits
+// within size_type and asserts if no null terminator was found in the array.
+template <typename T>
+constexpr size_type ArrayStringLength(const T* array,
+ size_type max_string_length,
+ size_type capacity) {
+ const size_type max_length = std::min(max_string_length, capacity);
+ const size_type length = BoundedStringLength(array, max_length);
+ PW_ASSERT(length <= max_string_length); // The array is not null terminated
+ return length;
+}
+
+template <typename T, size_t kCharArraySize>
+constexpr size_type ArrayStringLength(const T (&array)[kCharArraySize],
+ size_type capacity) {
+ static_assert(kCharArraySize > 0u, "C arrays cannot have a length of 0");
+ static_assert(kCharArraySize - 1 < kGeneric,
+ "The size of this literal or character array is too large "
+ "for pw::InlineString<>::size_type");
+ return ArrayStringLength(
+ array, static_cast<size_type>(kCharArraySize - 1), capacity);
+}
+
+// Constexpr version of std::copy that returns the number of copied characters.
+template <typename InputIterator, typename T>
+constexpr size_type IteratorCopyAndTerminate(InputIterator begin,
+ InputIterator end,
+ T* const string_begin,
+ const T* const string_end) {
+ T* current_position = string_begin;
+ for (InputIterator it = begin; it != end; ++it) {
+ PW_ASSERT(current_position != string_end);
+ char_traits<T>::assign(*current_position++, *it);
+ }
+ char_traits<T>::assign(*current_position, T()); // Null terminate
+ return static_cast<size_type>(current_position - string_begin);
+}
+
+// Constexpr lexicographical comparison.
+template <typename T>
+constexpr int Compare(const T* lhs,
+ size_type lhs_size,
+ const T* rhs,
+ size_type rhs_size) noexcept {
+ int result = char_traits<T>::compare(lhs, rhs, std::min(lhs_size, rhs_size));
+ if (result != 0 || lhs_size == rhs_size) {
+ return result;
+ }
+ return lhs_size < rhs_size ? -1 : 1;
+}
+
+} // namespace string_impl
+} // namespace pw
diff --git a/pw_string/public/pw_string/string.h b/pw_string/public/pw_string/string.h
new file mode 100644
index 000000000..a6041f52a
--- /dev/null
+++ b/pw_string/public/pw_string/string.h
@@ -0,0 +1,681 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+/// @file pw_string/string.h
+///
+/// @brief `pw::InlineBasicString` and `pw::InlineString` are safer alternatives
+/// to `std::basic_string` and `std::string`.
+
+#include <cstddef>
+#include <initializer_list>
+#include <iterator>
+
+#include "pw_assert/assert.h"
+#include "pw_polyfill/standard.h"
+#include "pw_preprocessor/compiler.h"
+#include "pw_string/internal/string_impl.h"
+
+// Messages to use in static_assert statements.
+#define _PW_STRING_CAPACITY_TOO_SMALL_FOR_ARRAY \
+ "The pw::InlineString's capacity is too small to hold the assigned string " \
+ "literal or character array. When assigning a literal or array to a " \
+ "pw::InlineString, the pw::InlineString's capacity must be large enough " \
+ "for the entire string, not counting the null terminator."
+
+#define _PW_STRING_CAPACITY_TOO_SMALL_FOR_STRING \
+ "When assigning one pw::InlineString with known capacity to another, the " \
+ "capacity of the destination pw::InlineString must be at least as large as " \
+ "the source string."
+
+namespace pw {
+
+/// @brief `pw::InlineBasicString` is a fixed-capacity version of
+/// `std::basic_string`. In brief:
+///
+/// - It is C++14-compatible and null-terminated.
+/// - It stores the string contents inline and uses no dynamic memory.
+/// - It implements mostly the same API as `std::basic_string`, but the capacity
+/// of the string is fixed at construction and cannot grow. Attempting to
+/// increase the size beyond the capacity triggers an assert.
+///
+/// `pw::InlineBasicString` is efficient and compact. The current size and
+/// capacity are stored in a single word. Accessing its contents is a simple
+/// array access within the object, with no pointer indirection, even when
+/// working from a generic reference `pw::InlineBasicString<T>` where the
+/// capacity is not specified as a template argument. A string object can be
+/// used safely without the need to know its capacity.
+///
+/// See also `pw::InlineString`, which is an alias of
+/// `pw::InlineBasicString<char>` and is equivalent to `std::string`.
+template <typename T, string_impl::size_type kCapacity = string_impl::kGeneric>
+class InlineBasicString final
+ : public InlineBasicString<T, string_impl::kGeneric> {
+ public:
+ using typename InlineBasicString<T, string_impl::kGeneric>::value_type;
+ using typename InlineBasicString<T, string_impl::kGeneric>::size_type;
+ using typename InlineBasicString<T, string_impl::kGeneric>::difference_type;
+ using typename InlineBasicString<T, string_impl::kGeneric>::reference;
+ using typename InlineBasicString<T, string_impl::kGeneric>::const_reference;
+ using typename InlineBasicString<T, string_impl::kGeneric>::pointer;
+ using typename InlineBasicString<T, string_impl::kGeneric>::const_pointer;
+ using typename InlineBasicString<T, string_impl::kGeneric>::iterator;
+ using typename InlineBasicString<T, string_impl::kGeneric>::const_iterator;
+ using typename InlineBasicString<T, string_impl::kGeneric>::reverse_iterator;
+ using
+ typename InlineBasicString<T,
+ string_impl::kGeneric>::const_reverse_iterator;
+
+ using InlineBasicString<T, string_impl::kGeneric>::npos;
+
+ // Constructors
+
+ constexpr InlineBasicString() noexcept
+ : InlineBasicString<T, string_impl::kGeneric>(kCapacity), buffer_() {}
+
+ constexpr InlineBasicString(size_type count, T ch) : InlineBasicString() {
+ Fill(data(), ch, count);
+ }
+
+ template <size_type kOtherCapacity>
+ constexpr InlineBasicString(const InlineBasicString<T, kOtherCapacity>& other,
+ size_type index,
+ size_type count = npos)
+ : InlineBasicString() {
+ CopySubstr(data(), other.data(), other.size(), index, count);
+ }
+
+ constexpr InlineBasicString(const T* string, size_type count)
+ : InlineBasicString() {
+ Copy(data(), string, count);
+ }
+
+ template <typename U,
+ typename = string_impl::EnableIfNonArrayCharPointer<T, U>>
+ constexpr InlineBasicString(U c_string)
+ : InlineBasicString(
+ c_string, string_impl::BoundedStringLength(c_string, kCapacity)) {}
+
+ template <size_t kCharArraySize>
+ constexpr InlineBasicString(const T (&array)[kCharArraySize])
+ : InlineBasicString() {
+ static_assert(
+ string_impl::NullTerminatedArrayFitsInString(kCharArraySize, kCapacity),
+ _PW_STRING_CAPACITY_TOO_SMALL_FOR_ARRAY);
+ Copy(data(), array, string_impl::ArrayStringLength(array, max_size()));
+ }
+
+ template <typename InputIterator,
+ typename = string_impl::EnableIfInputIterator<InputIterator>>
+ constexpr InlineBasicString(InputIterator start, InputIterator finish)
+ : InlineBasicString() {
+ IteratorCopy(start, finish, data(), data() + max_size());
+ }
+
+ // Use the default copy for InlineBasicString with the same capacity.
+ constexpr InlineBasicString(const InlineBasicString&) = default;
+
+ // When copying from an InlineBasicString with a different capacity, check
+ // that the destination capacity is at least as large as the source capacity.
+ template <size_type kOtherCapacity>
+ constexpr InlineBasicString(const InlineBasicString<T, kOtherCapacity>& other)
+ : InlineBasicString(other.data(), other.size()) {
+ static_assert(
+ kOtherCapacity == string_impl::kGeneric || kOtherCapacity <= kCapacity,
+ _PW_STRING_CAPACITY_TOO_SMALL_FOR_STRING);
+ }
+
+ constexpr InlineBasicString(std::initializer_list<T> list)
+ : InlineBasicString(list.begin(), list.size()) {}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+ // Unlike std::string, pw::InlineString<> supports implicit conversions from
+ // std::string_view. However, explicit conversions are still required from
+ // types that convert to std::string_view, as with std::string.
+ //
+ // pw::InlineString<> allows implicit conversions from std::string_view
+ // because it can be cumbersome to specify the capacity parameter. In
+ // particular, this can make using aggregate initialization more difficult.
+ //
+ // This explicit constructor is enabled for an argument that converts to
+ // std::string_view, but is not a std::string_view.
+ template <
+ typename StringViewLike,
+ string_impl::EnableIfStringViewLikeButNotStringView<T, StringViewLike>* =
+ nullptr>
+ explicit constexpr InlineBasicString(const StringViewLike& string)
+ : InlineBasicString(std::basic_string_view<T>(string)) {}
+
+ // This converting constructor is enabled for std::string_view, but not types
+ // that convert to it.
+ template <typename StringView,
+ std::enable_if_t<
+ std::is_same<StringView, std::basic_string_view<T>>::value>* =
+ nullptr>
+ constexpr InlineBasicString(const StringView& view)
+ : InlineBasicString(view.data(), view.size()) {}
+
+ template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+ constexpr InlineBasicString(const StringView& string,
+ size_type index,
+ size_type count)
+ : InlineBasicString() {
+ const std::basic_string_view<T> view = string;
+ CopySubstr(data(), view.data(), view.size(), index, count);
+ }
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+ InlineBasicString(std::nullptr_t) = delete; // Cannot construct from nullptr
+
+ // Assignment operators
+
+ constexpr InlineBasicString& operator=(const InlineBasicString& other) =
+ default;
+
+ // Checks capacity rather than current size.
+ template <size_type kOtherCapacity>
+ constexpr InlineBasicString& operator=(
+ const InlineBasicString<T, kOtherCapacity>& other) {
+ return assign<kOtherCapacity>(other); // NOLINT
+ }
+
+ template <size_t kCharArraySize>
+ constexpr InlineBasicString& operator=(const T (&array)[kCharArraySize]) {
+ return assign<kCharArraySize>(array); // NOLINT
+ }
+
+ // Use SFINAE to avoid ambiguity with the array overload.
+ template <typename U,
+ typename = string_impl::EnableIfNonArrayCharPointer<T, U>>
+ constexpr InlineBasicString& operator=(U c_string) {
+ return assign(c_string); // NOLINT
+ }
+
+ constexpr InlineBasicString& operator=(T ch) {
+ static_assert(kCapacity != 0,
+ "Cannot assign a character to pw::InlineString<0>");
+ return assign(1, ch); // NOLINT
+ }
+
+ constexpr InlineBasicString& operator=(std::initializer_list<T> list) {
+ return assign(list); // NOLINT
+ }
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+ template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+ constexpr InlineBasicString& operator=(const StringView& string) {
+ return assign(string); // NOLINT
+ }
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+ constexpr InlineBasicString& operator=(std::nullptr_t) = delete;
+
+ template <size_type kOtherCapacity>
+ constexpr InlineBasicString& operator+=(
+ const InlineBasicString<T, kOtherCapacity>& string) {
+ return append(string);
+ }
+
+ constexpr InlineBasicString& operator+=(T character) {
+ push_back(character);
+ return *this;
+ }
+
+ template <size_t kCharArraySize>
+ constexpr InlineBasicString& operator+=(const T (&array)[kCharArraySize]) {
+ return append(array);
+ }
+
+ template <typename U,
+ typename = string_impl::EnableIfNonArrayCharPointer<T, U>>
+ constexpr InlineBasicString& operator+=(U c_string) {
+ return append(c_string);
+ }
+
+ constexpr InlineBasicString& operator+=(std::initializer_list<T> list) {
+ return append(list.begin(), list.size());
+ }
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+ template <typename StringView,
+ typename = string_impl::EnableIfStringViewLike<T, StringView>>
+ constexpr InlineBasicString& operator+=(const StringView& string) {
+ return append(string);
+ }
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+ // The data() and size() functions are defined differently for the generic and
+ // known-size specializations. This is to support using pw::InlineBasicString
+ // in constexpr statements. This data() implementation simply returns the
+ // underlying buffer. The generic-capacity data() function casts *this to
+ // InlineBasicString<T, 0>, so is only constexpr when the capacity is actually
+ // 0.
+ constexpr pointer data() { return buffer_; }
+ constexpr const_pointer data() const { return buffer_; }
+
+ // Use the size() function from the base, but define max_size() to return the
+ // kCapacity template parameter instead of reading the stored capacity value.
+ using InlineBasicString<T, string_impl::kGeneric>::size;
+ constexpr size_type max_size() const noexcept { return kCapacity; }
+
+ // Most string functions are defined in separate file so they can be shared
+ // between the known capacity and generic capacity versions of
+ // InlineBasicString.
+#include "pw_string/internal/string_common_functions.inc"
+
+ private:
+ using InlineBasicString<T, string_impl::kGeneric>::PushBack;
+ using InlineBasicString<T, string_impl::kGeneric>::PopBack;
+ using InlineBasicString<T, string_impl::kGeneric>::Copy;
+ using InlineBasicString<T, string_impl::kGeneric>::CopySubstr;
+ using InlineBasicString<T, string_impl::kGeneric>::Fill;
+ using InlineBasicString<T, string_impl::kGeneric>::IteratorCopy;
+ using InlineBasicString<T, string_impl::kGeneric>::CopyExtend;
+ using InlineBasicString<T, string_impl::kGeneric>::CopyExtendSubstr;
+ using InlineBasicString<T, string_impl::kGeneric>::FillExtend;
+ using InlineBasicString<T, string_impl::kGeneric>::IteratorExtend;
+ using InlineBasicString<T, string_impl::kGeneric>::Resize;
+ using InlineBasicString<T, string_impl::kGeneric>::SetSizeAndTerminate;
+
+ // Store kCapacity + 1 bytes to reserve space for a null terminator.
+ // InlineBasicString<T, 0> only stores a null terminator.
+ T buffer_[kCapacity + 1];
+};
+
+// Generic-capacity version of pw::InlineBasicString. Generic-capacity strings
+// cannot be constructed; they can only be used as references to fixed-capacity
+// pw::InlineBasicString objects.
+template <typename T>
+class InlineBasicString<T, string_impl::kGeneric> {
+ public:
+ using value_type = T;
+ using size_type = string_impl::size_type;
+ using difference_type = std::ptrdiff_t;
+ using reference = value_type&;
+ using const_reference = const value_type&;
+ using pointer = value_type*;
+ using const_pointer = const value_type*;
+ using iterator = value_type*;
+ using const_iterator = const value_type*;
+ using reverse_iterator = std::reverse_iterator<iterator>;
+ using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+
+ static constexpr size_type npos = string_impl::kGeneric;
+
+ InlineBasicString() = delete; // Must specify capacity to construct a string.
+
+ // For the generic-capacity pw::InlineBasicString, cast this object to a
+ // fixed-capacity class so the address of the data can be found. Even though
+ // the capacity isn't known at compile time, the location of the data never
+ // changes.
+ constexpr pointer data() noexcept {
+ return static_cast<InlineBasicString<T, 0>*>(this)->data();
+ }
+ constexpr const_pointer data() const noexcept {
+ return static_cast<const InlineBasicString<T, 0>*>(this)->data();
+ }
+
+ constexpr size_type size() const noexcept { return length_; }
+ constexpr size_type max_size() const noexcept { return capacity_; }
+
+ // Most string functions are defined in separate file so they can be shared
+ // between the known capacity and generic capacity versions of
+ // InlineBasicString.
+#include "pw_string/internal/string_common_functions.inc"
+
+ protected:
+ explicit constexpr InlineBasicString(size_type capacity)
+ : capacity_(capacity), length_(0) {}
+
+ // The generic-capacity InlineBasicString<T> is not copyable or movable, but
+ // BasicStrings can copied or assigned through a fixed capacity derived class.
+ InlineBasicString(const InlineBasicString&) = default;
+
+ InlineBasicString& operator=(const InlineBasicString&) = default;
+
+ constexpr void PushBack(T* data, T ch);
+
+ constexpr void PopBack(T* data) {
+ PW_ASSERT(!empty());
+ SetSizeAndTerminate(data, size() - 1);
+ }
+
+ constexpr InlineBasicString& Copy(T* data,
+ const T* source,
+ size_type new_size);
+
+ constexpr InlineBasicString& CopySubstr(T* data,
+ const T* source,
+ size_type source_size,
+ size_type index,
+ size_type count);
+
+ constexpr InlineBasicString& Fill(T* data, T fill_char, size_type new_size);
+
+ template <typename InputIterator>
+ constexpr InlineBasicString& IteratorCopy(InputIterator start,
+ InputIterator finish,
+ T* data_start,
+ const T* data_finish) {
+ set_size(string_impl::IteratorCopyAndTerminate(
+ start, finish, data_start, data_finish));
+ return *this;
+ }
+
+ constexpr InlineBasicString& CopyExtend(T* data,
+ const T* source,
+ size_type count);
+
+ constexpr InlineBasicString& CopyExtendSubstr(T* data,
+ const T* source,
+ size_type source_size,
+ size_type index,
+ size_type count);
+
+ constexpr InlineBasicString& FillExtend(T* data,
+ T fill_char,
+ size_type count);
+
+ template <typename InputIterator>
+ constexpr InlineBasicString& IteratorExtend(InputIterator start,
+ InputIterator finish,
+ T* data_start,
+ const T* data_finish) {
+ length_ += string_impl::IteratorCopyAndTerminate(
+ start, finish, data_start, data_finish);
+ return *this;
+ }
+
+ constexpr void Resize(T* data, size_type new_size, T ch);
+
+ constexpr void set_size(size_type length) { length_ = length; }
+ constexpr void SetSizeAndTerminate(T* data, size_type length) {
+ string_impl::char_traits<T>::assign(data[length], T());
+ set_size(length);
+ }
+
+ private:
+ // Allow StringBuilder to directly set length_ when doing string operations.
+ friend class StringBuilder;
+
+ // Provide this constant for static_assert checks. If the capacity is unknown,
+ // use the maximum value so that compile-time capacity checks pass. If
+ // overflow occurs, the operation triggers a PW_ASSERT at runtime.
+ static constexpr size_type kCapacity = string_impl::kGeneric;
+
+ size_type capacity_;
+ size_type length_;
+};
+
+// Class template argument deduction guides
+
+#ifdef __cpp_deduction_guides
+
+// In C++17, the capacity of the string may be deduced from a string literal or
+// array. For example, the following deduces a character type of char and a
+// capacity of 4 (which does not include the null terminator).
+//
+// InlineBasicString my_string = "1234";
+//
+// In C++20, the template parameters for the pw::InlineString alias may be
+// deduced similarly:
+//
+// InlineString my_string = "abc"; // deduces capacity of 3.
+//
+template <typename T, size_t kCharArraySize>
+InlineBasicString(const T (&)[kCharArraySize])
+ -> InlineBasicString<T, kCharArraySize - 1>;
+
+#endif // __cpp_deduction_guides
+
+// Operators
+
+// TODO(b/239996007): Implement operator+
+
+template <typename T,
+ string_impl::size_type kLhsCapacity,
+ string_impl::size_type kRhsCapacity>
+constexpr bool operator==(
+ const InlineBasicString<T, kLhsCapacity>& lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
+ return lhs.compare(rhs) == 0;
+}
+
+template <typename T,
+ string_impl::size_type kLhsCapacity,
+ string_impl::size_type kRhsCapacity>
+constexpr bool operator!=(
+ const InlineBasicString<T, kLhsCapacity>& lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
+ return lhs.compare(rhs) != 0;
+}
+
+template <typename T,
+ string_impl::size_type kLhsCapacity,
+ string_impl::size_type kRhsCapacity>
+constexpr bool operator<(
+ const InlineBasicString<T, kLhsCapacity>& lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
+ return lhs.compare(rhs) < 0;
+}
+
+template <typename T,
+ string_impl::size_type kLhsCapacity,
+ string_impl::size_type kRhsCapacity>
+constexpr bool operator<=(
+ const InlineBasicString<T, kLhsCapacity>& lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
+ return lhs.compare(rhs) <= 0;
+}
+
+template <typename T,
+ string_impl::size_type kLhsCapacity,
+ string_impl::size_type kRhsCapacity>
+constexpr bool operator>(
+ const InlineBasicString<T, kLhsCapacity>& lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
+ return lhs.compare(rhs) > 0;
+}
+
+template <typename T,
+ string_impl::size_type kLhsCapacity,
+ string_impl::size_type kRhsCapacity>
+constexpr bool operator>=(
+ const InlineBasicString<T, kLhsCapacity>& lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) noexcept {
+ return lhs.compare(rhs) >= 0;
+}
+
+template <typename T, string_impl::size_type kLhsCapacity>
+constexpr bool operator==(const InlineBasicString<T, kLhsCapacity>& lhs,
+ const T* rhs) {
+ return lhs.compare(rhs) == 0;
+}
+
+template <typename T, string_impl::size_type kRhsCapacity>
+constexpr bool operator==(const T* lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) {
+ return rhs.compare(lhs) == 0;
+}
+
+template <typename T, string_impl::size_type kLhsCapacity>
+constexpr bool operator!=(const InlineBasicString<T, kLhsCapacity>& lhs,
+ const T* rhs) {
+ return lhs.compare(rhs) != 0;
+}
+
+template <typename T, string_impl::size_type kRhsCapacity>
+constexpr bool operator!=(const T* lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) {
+ return rhs.compare(lhs) != 0;
+}
+
+template <typename T, string_impl::size_type kLhsCapacity>
+constexpr bool operator<(const InlineBasicString<T, kLhsCapacity>& lhs,
+ const T* rhs) {
+ return lhs.compare(rhs) < 0;
+}
+
+template <typename T, string_impl::size_type kRhsCapacity>
+constexpr bool operator<(const T* lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) {
+ return rhs.compare(lhs) >= 0;
+}
+
+template <typename T, string_impl::size_type kLhsCapacity>
+constexpr bool operator<=(const InlineBasicString<T, kLhsCapacity>& lhs,
+ const T* rhs) {
+ return lhs.compare(rhs) <= 0;
+}
+
+template <typename T, string_impl::size_type kRhsCapacity>
+constexpr bool operator<=(const T* lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) {
+ return rhs.compare(lhs) >= 0;
+}
+
+template <typename T, string_impl::size_type kLhsCapacity>
+constexpr bool operator>(const InlineBasicString<T, kLhsCapacity>& lhs,
+ const T* rhs) {
+ return lhs.compare(rhs) > 0;
+}
+
+template <typename T, string_impl::size_type kRhsCapacity>
+constexpr bool operator>(const T* lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) {
+ return rhs.compare(lhs) <= 0;
+}
+
+template <typename T, string_impl::size_type kLhsCapacity>
+constexpr bool operator>=(const InlineBasicString<T, kLhsCapacity>& lhs,
+ const T* rhs) {
+ return lhs.compare(rhs) >= 0;
+}
+
+template <typename T, string_impl::size_type kRhsCapacity>
+constexpr bool operator>=(const T* lhs,
+ const InlineBasicString<T, kRhsCapacity>& rhs) {
+ return rhs.compare(lhs) <= 0;
+}
+
+// TODO(b/239996007): Implement other comparison operator overloads.
+
+// Aliases
+
+/// @brief `pw::InlineString` is an alias of `pw::InlineBasicString<char>` and
+/// is equivalent to `std::string`.
+template <string_impl::size_type kCapacity = string_impl::kGeneric>
+using InlineString = InlineBasicString<char, kCapacity>;
+
+// Function implementations
+
+template <typename T>
+constexpr void InlineBasicString<T, string_impl::kGeneric>::PushBack(T* data,
+ T ch) {
+ PW_ASSERT(size() < max_size());
+ string_impl::char_traits<T>::assign(data[size()], ch);
+ SetSizeAndTerminate(data, size() + 1);
+}
+
+template <typename T>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::Copy(T* data,
+ const T* source,
+ size_type new_size) {
+ PW_ASSERT(new_size <= max_size());
+ string_impl::char_traits<T>::copy(data, source, new_size);
+ SetSizeAndTerminate(data, new_size);
+ return *this;
+}
+
+template <typename T>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::CopySubstr(T* data,
+ const T* source,
+ size_type source_size,
+ size_type index,
+ size_type count) {
+ PW_ASSERT(index <= source_size);
+ return Copy(data,
+ source + index,
+ std::min(count, static_cast<size_type>(source_size - index)));
+}
+
+template <typename T>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::Fill(T* data,
+ T fill_char,
+ size_type new_size) {
+ PW_ASSERT(new_size <= max_size());
+ string_impl::char_traits<T>::assign(data, new_size, fill_char);
+ SetSizeAndTerminate(data, new_size);
+ return *this;
+}
+
+template <typename T>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::CopyExtend(T* data,
+ const T* source,
+ size_type count) {
+ PW_ASSERT(count <= max_size() - size());
+ string_impl::char_traits<T>::copy(data + size(), source, count);
+ SetSizeAndTerminate(data, size() + count);
+ return *this;
+}
+
+template <typename T>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::CopyExtendSubstr(
+ T* data,
+ const T* source,
+ size_type source_size,
+ size_type index,
+ size_type count) {
+ PW_ASSERT(index <= source_size);
+ return CopyExtend(
+ data,
+ source + index,
+ std::min(count, static_cast<size_type>(source_size - index)));
+ return *this;
+}
+
+template <typename T>
+constexpr InlineBasicString<T, string_impl::kGeneric>&
+InlineBasicString<T, string_impl::kGeneric>::FillExtend(T* data,
+ T fill_char,
+ size_type count) {
+ PW_ASSERT(count <= max_size() - size());
+ string_impl::char_traits<T>::assign(data + size(), count, fill_char);
+ SetSizeAndTerminate(data, size() + count);
+ return *this;
+}
+
+template <typename T>
+constexpr void InlineBasicString<T, string_impl::kGeneric>::Resize(
+ T* data, size_type new_size, T ch) {
+ PW_ASSERT(new_size <= max_size());
+
+ if (new_size > size()) {
+ string_impl::char_traits<T>::assign(data + size(), new_size - size(), ch);
+ }
+
+ SetSizeAndTerminate(data, new_size);
+}
+
+} // namespace pw
+
+#undef _PW_STRING_CAPACITY_TOO_SMALL_FOR_ARRAY
+#undef _PW_STRING_CAPACITY_TOO_SMALL_FOR_STRING
diff --git a/pw_string/public/pw_string/string_builder.h b/pw_string/public/pw_string/string_builder.h
index 220abf1d8..c3c9d1760 100644
--- a/pw_string/public/pw_string/string_builder.h
+++ b/pw_string/public/pw_string/string_builder.h
@@ -12,193 +12,220 @@
// License for the specific language governing permissions and limitations under
// the License.
#pragma once
+/// @file pw_string/string_builder.h
+///
+/// @brief `pw::StringBuilder` facilitates creating formatted strings in a
+/// fixed-sized buffer or in a `pw::InlineString`. It is designed to give the
+/// flexibility of std::ostringstream, but with a small footprint.
#include <algorithm>
#include <cstdarg>
#include <cstddef>
#include <cstring>
-#include <span>
#include <string_view>
#include <type_traits>
#include <utility>
#include "pw_preprocessor/compiler.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
+#include "pw_string/string.h"
#include "pw_string/to_string.h"
namespace pw {
-// StringBuilder facilitates building formatted strings in a fixed-size buffer.
-// StringBuilders are always null terminated (unless they are constructed with
-// an empty buffer) and never overflow. Status is tracked for each operation and
-// an overall status is maintained, which reflects the most recent error.
-//
-// A StringBuilder does not own the buffer it writes to. It can be used to write
-// strings to any buffer. The StringBuffer template class, defined below,
-// allocates a buffer alongside a StringBuilder.
-//
-// StringBuilder supports C++-style << output, similar to std::ostringstream. It
-// also supports std::string-like append functions and printf-style output.
-//
-// Support for custom types is added by overloading operator<< in the same
-// namespace as the custom type. For example:
-//
-// namespace my_project {
-//
-// struct MyType {
-// int foo;
-// const char* bar;
-// };
-//
-// pw::StringBuilder& operator<<(pw::StringBuilder& sb, const MyType& value) {
-// return sb << "MyType(" << value.foo << ", " << value.bar << ')';
-// }
-//
-// } // namespace my_project
-//
-// The ToString template function can be specialized to support custom types
-// with StringBuilder, though overloading operator<< is generally preferred. For
-// example:
-//
-// namespace pw {
-//
-// template <>
-// StatusWithSize ToString<MyStatus>(MyStatus value, std::span<char> buffer) {
-// return Copy(MyStatusString(value), buffer);
-// }
-//
-// } // namespace pw
-//
+/// @class StringBuilder
+///
+/// `pw::StringBuilder` instances are always null-terminated (unless they are
+/// constructed with an empty buffer) and never overflow. Status is tracked for
+/// each operation and an overall status is maintained, which reflects the most
+/// recent error.
+///
+/// `pw::StringBuilder` does not own the buffer it writes to. It can be used
+/// to write strings to any buffer. The `pw::StringBuffer` template class,
+/// defined below, allocates a buffer alongside a `pw::StringBuilder`.
+///
+/// `pw::StringBuilder` supports C++-style `<<` output, similar to
+/// `std::ostringstream`. It also supports append functions like `std::string`
+/// and `printf`-style output.
+///
+/// Support for custom types is added by overloading `operator<<` in the same
+/// namespace as the custom type. For example:
+///
+/// @code
+/// namespace my_project {
+///
+/// struct MyType {
+/// int foo;
+/// const char* bar;
+/// };
+///
+/// pw::StringBuilder& operator<<(pw::StringBuilder& sb, const MyType& value)
+/// {
+/// return sb << "MyType(" << value.foo << ", " << value.bar << ')';
+/// }
+///
+/// } // namespace my_project
+/// @endcode
+///
+/// The `ToString` template function can be specialized to support custom types
+/// with `pw::StringBuilder`, though overloading `operator<<` is generally
+/// preferred. For example:
+///
+/// @code
+/// namespace pw {
+///
+/// template <>
+/// StatusWithSize ToString<MyStatus>(MyStatus value, span<char> buffer) {
+/// return Copy(MyStatusString(value), buffer);
+/// }
+///
+/// } // namespace pw
+/// @endcode
+///
class StringBuilder {
public:
- // Creates an empty StringBuilder.
- constexpr StringBuilder(std::span<char> buffer) : buffer_(buffer), size_(0) {
+ /// Creates an empty `pw::StringBuilder`.
+ explicit constexpr StringBuilder(span<char> buffer)
+ : buffer_(buffer), size_(&inline_size_), inline_size_(0) {
NullTerminate();
}
- StringBuilder(std::span<std::byte> buffer)
+
+ explicit StringBuilder(span<std::byte> buffer)
: StringBuilder(
{reinterpret_cast<char*>(buffer.data()), buffer.size_bytes()}) {}
- // Disallow copy/assign to avoid confusion about where the string is actually
- // stored. StringBuffers may be copied into one another.
+ explicit constexpr StringBuilder(InlineString<>& string)
+ : buffer_(string.data(), string.max_size() + 1),
+ size_(&string.length_),
+ inline_size_(0) {}
+
+ /// Disallow copy/assign to avoid confusion about where the string is actually
+ /// stored. `pw::StringBuffer` instances may be copied into one another.
StringBuilder(const StringBuilder&) = delete;
StringBuilder& operator=(const StringBuilder&) = delete;
- // Returns the contents of the string buffer. Always null-terminated.
+ /// @fn data
+ /// @fn c_str
+ ///
+ /// Returns the contents of the string buffer. Always null-terminated.
const char* data() const { return buffer_.data(); }
const char* c_str() const { return data(); }
- // Returns a std::string_view of the contents of this StringBuilder. The
- // std::string_view is invalidated if the StringBuilder contents change.
+ /// Returns a `std::string_view` of the contents of this `pw::StringBuilder`.
+ /// The `std::string_view` is invalidated if the `pw::StringBuilder` contents
+ /// change.
std::string_view view() const { return std::string_view(data(), size()); }
- // Allow implicit conversions to std::string_view so StringBuilders can be
- // passed into functions that take a std::string_view.
+ /// Allow implicit conversions to `std::string_view` so `pw::StringBuilder`
+ /// instances can be passed into functions that take a `std::string_view`.
operator std::string_view() const { return view(); }
- // Returns a std::span<const std::byte> representation of this StringBuffer.
- std::span<const std::byte> as_bytes() const {
- return std::span(reinterpret_cast<const std::byte*>(buffer_.data()), size_);
+ /// Returns a `span<const std::byte>` representation of this
+ /// `pw::StringBuffer`.
+ span<const std::byte> as_bytes() const {
+ return span(reinterpret_cast<const std::byte*>(buffer_.data()), size());
}
- // Returns the StringBuilder's status, which reflects the most recent error
- // that occurred while updating the string. After an update fails, the status
- // remains non-OK until it is cleared with clear() or clear_status(). Returns:
- //
- // OK if no errors have occurred
- // RESOURCE_EXHAUSTED if output to the StringBuilder was truncated
- // INVALID_ARGUMENT if printf-style formatting failed
- // OUT_OF_RANGE if an operation outside the buffer was attempted
- //
- Status status() const { return status_; }
-
- // Returns status() and size() as a StatusWithSize.
+ /// Returns the status of `pw::StringBuilder`, which reflects the most recent
+ /// error that occurred while updating the string. After an update fails, the
+ /// status remains non-OK until it is cleared with
+ /// `pw::StringBuilder::clear()` or `pw::StringBuilder::clear_status()`.
+ ///
+ /// @returns `OK` if no errors have occurred; `RESOURCE_EXHAUSTED` if output
+ /// to the `StringBuilder` was truncated; `INVALID_ARGUMENT` if `printf`-style
+ /// formatting failed; `OUT_OF_RANGE` if an operation outside the buffer was
+ /// attempted.
+ Status status() const { return static_cast<Status::Code>(status_); }
+
+ /// Returns `status()` and `size()` as a `StatusWithSize`.
StatusWithSize status_with_size() const {
- return StatusWithSize(status_, size_);
+ return StatusWithSize(status(), size());
}
- // The status from the last operation. May be OK while status() is not OK.
- Status last_status() const { return last_status_; }
+ /// The status from the last operation. May be OK while `status()` is not OK.
+ Status last_status() const { return static_cast<Status::Code>(last_status_); }
- // True if status() is OkStatus().
- bool ok() const { return status_.ok(); }
+ /// True if `status()` is `OkStatus()`.
+ bool ok() const { return status().ok(); }
- // True if the string is empty.
+ /// True if the string is empty.
bool empty() const { return size() == 0u; }
- // Returns the current length of the string, excluding the null terminator.
- size_t size() const { return size_; }
+ /// Returns the current length of the string, excluding the null terminator.
+ size_t size() const { return *size_; }
- // Returns the maximum length of the string, excluding the null terminator.
+ /// Returns the maximum length of the string, excluding the null terminator.
size_t max_size() const { return buffer_.empty() ? 0u : buffer_.size() - 1; }
- // Clears the string and resets its error state.
+ /// Clears the string and resets its error state.
void clear();
- // Sets the statuses to OkStatus();
+ /// Sets the statuses to `OkStatus()`;
void clear_status() {
- status_ = OkStatus();
- last_status_ = OkStatus();
+ status_ = static_cast<unsigned char>(OkStatus().code());
+ last_status_ = static_cast<unsigned char>(OkStatus().code());
}
- // Appends a single character. Stets the status to RESOURCE_EXHAUSTED if the
- // character cannot be added because the buffer is full.
+ /// Appends a single character. Sets the status to `RESOURCE_EXHAUSTED` if the
+ /// character cannot be added because the buffer is full.
void push_back(char ch) { append(1, ch); }
- // Removes the last character. Sets the status to OUT_OF_RANGE if the buffer
- // is empty (in which case the unsigned overflow is intentional).
+ /// Removes the last character. Sets the status to `OUT_OF_RANGE` if the
+ /// buffer is empty (in which case the unsigned overflow is intentional).
void pop_back() PW_NO_SANITIZE("unsigned-integer-overflow") {
resize(size() - 1);
}
- // Appends the provided character count times.
+ /// Appends the provided character `count` times.
StringBuilder& append(size_t count, char ch);
- // Appends count characters from str to the end of the StringBuilder. If count
- // exceeds the remaining space in the StringBuffer, max_size() - size()
- // characters are appended and the status is set to RESOURCE_EXHAUSTED.
- //
- // str is not considered null-terminated and may contain null characters.
+ /// Appends `count` characters from `str` to the end of the `StringBuilder`.
+ /// If count exceeds the remaining space in the `StringBuffer`,
+ /// `max_size() - size()` characters are appended and the status is set to
+ /// `RESOURCE_EXHAUSTED`.
+ ///
+ /// `str` is not considered null-terminated and may contain null characters.
StringBuilder& append(const char* str, size_t count);
- // Appends characters from the null-terminated string to the end of the
- // StringBuilder. If the string's length exceeds the remaining space in the
- // buffer, max_size() - size() characters are copied and the status is set to
- // RESOURCE_EXHAUSTED.
- //
- // This function uses string::Length instead of std::strlen to avoid unbounded
- // reads if the string is not null terminated.
+ /// Appends characters from the null-terminated string to the end of the
+ /// `StringBuilder`. If the string's length exceeds the remaining space in the
+ /// buffer, `max_size() - size()` characters are copied and the status is
+ /// set to `RESOURCE_EXHAUSTED`.
+ ///
+ /// This function uses `string::Length` instead of `std::strlen` to avoid
+ /// unbounded reads if the string is not null-terminated.
StringBuilder& append(const char* str);
- // Appends a std::string_view to the end of the StringBuilder.
+ /// Appends a `std::string_view` to the end of the `StringBuilder`.
StringBuilder& append(const std::string_view& str);
- // Appends a substring from the std::string_view to the StringBuilder. Copies
- // up to count characters starting from pos to the end of the StringBuilder.
- // If pos > str.size(), sets the status to OUT_OF_RANGE.
+ /// Appends a substring from the `std::string_view` to the `StringBuilder`.
+ /// Copies up to count characters starting from `pos` to the end of the
+ /// `StringBuilder`. If `pos > str.size()`, sets the status to `OUT_OF_RANGE`.
StringBuilder& append(const std::string_view& str,
size_t pos,
size_t count = std::string_view::npos);
- // Appends to the end of the StringBuilder using the << operator. This enables
- // C++ stream-style formatted to StringBuilders.
+ /// Appends to the end of the `StringBuilder` using the `<<` operator. This
+ /// enables C++ stream-style formatted to `StringBuilder` instances.
template <typename T>
StringBuilder& operator<<(const T& value) {
- // For std::string_view-compatible types, use the append function, which
- // gives smaller code size.
+ /// For types compatible with `std::string_view`, use the `append` function,
+ /// which gives smaller code size.
if constexpr (std::is_convertible_v<T, std::string_view>) {
append(value);
- } else if constexpr (std::is_convertible_v<T, std::span<const std::byte>>) {
+ } else if constexpr (std::is_convertible_v<T, span<const std::byte>>) {
WriteBytes(value);
} else {
- HandleStatusWithSize(ToString(value, buffer_.subspan(size_)));
+ HandleStatusWithSize(ToString(value, buffer_.subspan(size())));
}
return *this;
}
- // Provide a few additional operator<< overloads that reduce code size.
+ /// Provide a few additional `operator<<` overloads that reduce code size.
StringBuilder& operator<<(bool value) {
return append(value ? "true" : "false");
}
@@ -214,36 +241,49 @@ class StringBuilder {
StringBuilder& operator<<(Status status) { return *this << status.str(); }
- // Appends a printf-style string to the end of the StringBuilder. If the
- // formatted string does not fit, the results are truncated and the status is
- // set to RESOURCE_EXHAUSTED.
- //
- // Internally, calls string::Format, which calls std::vsnprintf.
+ /// @fn pw::StringBuilder::Format
+ /// Appends a `printf`-style string to the end of the `StringBuilder`. If the
+ /// formatted string does not fit, the results are truncated and the status is
+ /// set to `RESOURCE_EXHAUSTED`.
+ ///
+ /// @param format The format string
+ /// @param ... Arguments for format specification
+ ///
+ /// @returns `StringBuilder&`
+ ///
+ /// @note Internally, calls `string::Format`, which calls `std::vsnprintf`.
PW_PRINTF_FORMAT(2, 3) StringBuilder& Format(const char* format, ...);
- // Appends a vsnprintf-style string with va_list arguments to the end of the
- // StringBuilder. If the formatted string does not fit, the results are
- // truncated and the status is set to RESOURCE_EXHAUSTED.
- //
- // Internally, calls string::Format, which calls std::vsnprintf.
+ /// Appends a `vsnprintf`-style string with `va_list` arguments to the end of
+ /// the `StringBuilder`. If the formatted string does not fit, the results are
+ /// truncated and the status is set to `RESOURCE_EXHAUSTED`.
+ ///
+ /// @note Internally, calls `string::Format`, which calls `std::vsnprintf`.
+ PW_PRINTF_FORMAT(2, 0)
StringBuilder& FormatVaList(const char* format, va_list args);
- // Sets the StringBuilder's size. This function only truncates; if
- // new_size > size(), it sets status to OUT_OF_RANGE and does nothing.
+ /// Sets the size of the `StringBuilder`. This function only truncates; if
+ /// `new_size > size()`, it sets status to `OUT_OF_RANGE` and does nothing.
void resize(size_t new_size);
protected:
- // Functions to support StringBuffer copies.
- constexpr StringBuilder(std::span<char> buffer, const StringBuilder& other)
+ /// Functions to support `StringBuffer` copies.
+ constexpr StringBuilder(span<char> buffer, const StringBuilder& other)
: buffer_(buffer),
- size_(other.size_),
+ size_(&inline_size_),
+ inline_size_(*other.size_),
status_(other.status_),
last_status_(other.last_status_) {}
void CopySizeAndStatus(const StringBuilder& other);
private:
- void WriteBytes(std::span<const std::byte> data);
+ /// Statuses are stored as an `unsigned char` so they pack into a single word.
+ static constexpr unsigned char StatusCode(Status status) {
+ return static_cast<unsigned char>(status.code());
+ }
+
+ void WriteBytes(span<const std::byte> data);
size_t ResizeAndTerminate(size_t chars_to_append);
@@ -251,21 +291,26 @@ class StringBuilder {
constexpr void NullTerminate() {
if (!buffer_.empty()) {
- buffer_[size_] = '\0';
+ buffer_[size()] = '\0';
}
}
void SetErrorStatus(Status status);
- const std::span<char> buffer_;
+ const span<char> buffer_;
+
+ InlineString<>::size_type* size_;
- size_t size_;
- Status status_;
- Status last_status_;
+ // Place the `inline_size_`, `status_`, and `last_status_` members together
+ // and use `unsigned char` for the status codes so these members can be
+ // packed into a single word.
+ InlineString<>::size_type inline_size_;
+ unsigned char status_ = StatusCode(OkStatus());
+ unsigned char last_status_ = StatusCode(OkStatus());
};
-// StringBuffers declare a buffer along with a StringBuilder. StringBuffer can
-// be used as a statically allocated replacement for std::ostringstream or
+// StringBuffer declares a buffer along with a StringBuilder. StringBuffer
+// can be used as a statically allocated replacement for std::ostringstream or
// std::string. For example:
//
// StringBuffer<32> str;
diff --git a/pw_string/public/pw_string/to_string.h b/pw_string/public/pw_string/to_string.h
index 1979375ec..74923cd63 100644
--- a/pw_string/public/pw_string/to_string.h
+++ b/pw_string/public/pw_string/to_string.h
@@ -38,7 +38,7 @@
//
// template <>
// StatusWithSize ToString<SomeCustomType>(const SomeCustomType& value,
-// std::span<char> buffer) {
+// span<char> buffer) {
// return /* ... implementation ... */;
// }
//
@@ -52,11 +52,14 @@
// StringBuilder may be easier to work with. StringBuilder's operator<< may be
// overloaded for custom types.
-#include <span>
#include <string_view>
#include <type_traits>
+#include "pw_span/span.h"
#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_string/format.h"
+#include "pw_string/internal/config.h"
#include "pw_string/type_to_string.h"
namespace pw {
@@ -64,7 +67,7 @@ namespace pw {
// This function provides string printing numeric types, enums, and anything
// that convertible to a std::string_view, such as std::string.
template <typename T>
-StatusWithSize ToString(const T& value, std::span<char> buffer) {
+StatusWithSize ToString(const T& value, span<char> buffer) {
if constexpr (std::is_same_v<std::remove_cv_t<T>, bool>) {
return string::BoolToString(value, buffer);
} else if constexpr (std::is_same_v<std::remove_cv_t<T>, char>) {
@@ -74,7 +77,13 @@ StatusWithSize ToString(const T& value, std::span<char> buffer) {
} else if constexpr (std::is_enum_v<T>) {
return string::IntToString(std::underlying_type_t<T>(value), buffer);
} else if constexpr (std::is_floating_point_v<T>) {
- return string::FloatAsIntToString(value, buffer);
+ if constexpr (string::internal::config::kEnableDecimalFloatExpansion) {
+ // TODO(hepler): Look into using the float overload of std::to_chars when
+ // it is available.
+ return string::Format(buffer, "%.3f", value);
+ } else {
+ return string::FloatAsIntToString(value, buffer);
+ }
} else if constexpr (std::is_convertible_v<T, std::string_view>) {
return string::CopyStringOrNull(value, buffer);
} else if constexpr (std::is_pointer_v<std::remove_cv_t<T>> ||
@@ -88,15 +97,15 @@ StatusWithSize ToString(const T& value, std::span<char> buffer) {
// ToString overloads for Pigweed types. To override ToString for a custom type,
// specialize the ToString template function.
-inline StatusWithSize ToString(Status status, std::span<char> buffer) {
+inline StatusWithSize ToString(Status status, span<char> buffer) {
return string::Copy(status.str(), buffer);
}
-inline StatusWithSize ToString(pw_Status status, std::span<char> buffer) {
+inline StatusWithSize ToString(pw_Status status, span<char> buffer) {
return ToString(Status(status), buffer);
}
-inline StatusWithSize ToString(std::byte byte, std::span<char> buffer) {
+inline StatusWithSize ToString(std::byte byte, span<char> buffer) {
return string::IntToHexString(static_cast<unsigned>(byte), buffer, 2);
}
diff --git a/pw_string/public/pw_string/type_to_string.h b/pw_string/public/pw_string/type_to_string.h
index 9c32120a4..b461ed007 100644
--- a/pw_string/public/pw_string/type_to_string.h
+++ b/pw_string/public/pw_string/type_to_string.h
@@ -18,10 +18,10 @@
// in "pw_string/to_string.h" should be used instead of these functions.
#include <cstdint>
-#include <span>
#include <string_view>
#include <type_traits>
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
#include "pw_string/util.h"
@@ -34,7 +34,8 @@ uint_fast8_t DecimalDigitCount(uint64_t integer);
// Returns the number of digits in the hexadecimal representation of the
// provided non-negative integer.
constexpr uint_fast8_t HexDigitCount(uint64_t integer) {
- return (64u - __builtin_clzll(integer | 1u) + 3u) / 4u;
+ return static_cast<uint_fast8_t>((64 - __builtin_clzll(integer | 1u) + 3) /
+ 4);
}
// Writes an integer as a null-terminated string in base 10. Returns the number
@@ -56,7 +57,7 @@ constexpr uint_fast8_t HexDigitCount(uint64_t integer) {
// sites pass their arguments directly and casting instructions are shared.
//
template <typename T>
-StatusWithSize IntToString(T value, std::span<char> buffer) {
+StatusWithSize IntToString(T value, span<char> buffer) {
if constexpr (std::is_signed_v<T>) {
return IntToString<int64_t>(value, buffer);
} else {
@@ -65,16 +66,16 @@ StatusWithSize IntToString(T value, std::span<char> buffer) {
}
template <>
-StatusWithSize IntToString(uint64_t value, std::span<char> buffer);
+StatusWithSize IntToString(uint64_t value, span<char> buffer);
template <>
-StatusWithSize IntToString(int64_t value, std::span<char> buffer);
+StatusWithSize IntToString(int64_t value, span<char> buffer);
// Writes an integer as a hexadecimal string. Semantics match IntToString. The
// output is lowercase without a leading 0x. min_width adds leading zeroes such
// that the final string is at least the specified number of characters wide.
StatusWithSize IntToHexString(uint64_t value,
- std::span<char> buffer,
+ span<char> buffer,
uint_fast8_t min_width = 0);
// Rounds a floating point number to an integer and writes it as a
@@ -96,17 +97,17 @@ StatusWithSize IntToHexString(uint64_t value,
// FloatAsIntToString(INFINITY, buffer) -> writes "-inf" to the buffer
// FloatAsIntToString(-NAN, buffer) -> writes "-NaN" to the buffer
//
-StatusWithSize FloatAsIntToString(float value, std::span<char> buffer);
+StatusWithSize FloatAsIntToString(float value, span<char> buffer);
// Writes a bool as "true" or "false". Semantics match CopyEntireString.
-StatusWithSize BoolToString(bool value, std::span<char> buffer);
+StatusWithSize BoolToString(bool value, span<char> buffer);
// String used to represent null pointers.
inline constexpr std::string_view kNullPointerString("(null)");
// Writes the pointer's address or kNullPointerString. Semantics match
// CopyEntireString.
-StatusWithSize PointerToString(const void* pointer, std::span<char> buffer);
+StatusWithSize PointerToString(const void* pointer, span<char> buffer);
// Specialized form of pw::string::Copy which supports nullptr values.
//
@@ -118,11 +119,10 @@ StatusWithSize PointerToString(const void* pointer, std::span<char> buffer);
// Returns the number of characters written, excluding the null terminator. If
// the string is truncated, the status is RESOURCE_EXHAUSTED.
inline StatusWithSize CopyStringOrNull(const std::string_view& value,
- std::span<char> buffer) {
+ span<char> buffer) {
return Copy(value, buffer);
}
-inline StatusWithSize CopyStringOrNull(const char* value,
- std::span<char> buffer) {
+inline StatusWithSize CopyStringOrNull(const char* value, span<char> buffer) {
if (value == nullptr) {
return PointerToString(value, buffer);
}
@@ -138,12 +138,12 @@ inline StatusWithSize CopyStringOrNull(const char* value,
// the full string does not fit, only a null terminator is written and the
// status is RESOURCE_EXHAUSTED.
StatusWithSize CopyEntireStringOrNull(const std::string_view& value,
- std::span<char> buffer);
+ span<char> buffer);
// Same as the string_view form of CopyEntireString, except that if value is a
// nullptr, then "(null)" is used as a fallback.
inline StatusWithSize CopyEntireStringOrNull(const char* value,
- std::span<char> buffer) {
+ span<char> buffer) {
if (value == nullptr) {
return PointerToString(value, buffer);
}
@@ -158,6 +158,6 @@ inline StatusWithSize CopyEntireStringOrNull(const char* value,
// printing for unknown types, if desired. Implementations must follow the
// ToString semantics.
template <typename T>
-StatusWithSize UnknownTypeToString(const T& value, std::span<char> buffer);
+StatusWithSize UnknownTypeToString(const T& value, span<char> buffer);
} // namespace pw::string
diff --git a/pw_string/public/pw_string/util.h b/pw_string/public/pw_string/util.h
index 0b818670b..d95bb798f 100644
--- a/pw_string/public/pw_string/util.h
+++ b/pw_string/public/pw_string/util.h
@@ -12,44 +12,69 @@
// License for the specific language governing permissions and limitations under
// the License.
#pragma once
+/// @file pw_string/util.h
+///
+/// @brief The `pw::string::*` functions provide safer alternatives to
+/// C++ standard library string functions.
+#include <cctype>
#include <cstddef>
-#include <span>
#include <string_view>
#include "pw_assert/assert.h"
+#include "pw_polyfill/language_feature_macros.h"
#include "pw_result/result.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_string/internal/length.h"
+#include "pw_string/string.h"
namespace pw {
namespace string {
+namespace internal {
-// Safe alternative to the string_view constructor to avoid the risk of an
-// unbounded implicit or explicit use of strlen.
-//
-// This is strongly recommended over using something like C11's strnlen_s as
-// a string_view does not require null-termination.
-constexpr std::string_view ClampedCString(std::span<const char> str) {
+PW_CONSTEXPR_CPP20 inline StatusWithSize CopyToSpan(
+ const std::string_view& source, span<char> dest) {
+ if (dest.empty()) {
+ return StatusWithSize::ResourceExhausted();
+ }
+
+ const size_t copied = source.copy(dest.data(), dest.size() - 1);
+ dest[copied] = '\0';
+
+ return StatusWithSize(
+ copied == source.size() ? OkStatus() : Status::ResourceExhausted(),
+ copied);
+}
+
+} // namespace internal
+
+/// @brief Safe alternative to the `string_view` constructor that avoids the
+/// risk of an unbounded implicit or explicit use of `strlen`.
+///
+/// This is strongly recommended over using something like C11's `strnlen_s` as
+/// a `string_view` does not require null-termination.
+constexpr std::string_view ClampedCString(span<const char> str) {
return std::string_view(str.data(),
internal::ClampedLength(str.data(), str.size()));
}
constexpr std::string_view ClampedCString(const char* str, size_t max_len) {
- return ClampedCString(std::span<const char>(str, max_len));
+ return ClampedCString(span<const char>(str, max_len));
}
-// Safe alternative to strlen to calculate the null-terminated length of the
-// string within the specified span, excluding the null terminator. Like C11's
-// strnlen_s, the scan for the null-terminator is bounded.
-//
-// Returns:
-// null-terminated length of the string excluding the null terminator.
-// OutOfRange - if the string is not null-terminated.
-//
-// Precondition: The string shall be at a valid pointer.
-constexpr pw::Result<size_t> NullTerminatedLength(std::span<const char> str) {
+/// @brief `pw::string::NullTerminatedLength` is a safer alternative to
+/// `strlen` for calculating the null-terminated length of the
+/// string within the specified span, excluding the null terminator.
+///
+/// Like `strnlen_s` in C11, the scan for the null-terminator is bounded.
+///
+/// @pre The string shall be at a valid pointer.
+///
+/// @returns the null-terminated length of the string excluding the null
+/// terminator or `OutOfRange` if the string is not null-terminated.
+constexpr Result<size_t> NullTerminatedLength(span<const char> str) {
PW_DASSERT(str.data() != nullptr);
const size_t length = internal::ClampedLength(str.data(), str.size());
@@ -60,43 +85,99 @@ constexpr pw::Result<size_t> NullTerminatedLength(std::span<const char> str) {
return length;
}
-constexpr pw::Result<size_t> NullTerminatedLength(const char* str,
- size_t max_len) {
- return NullTerminatedLength(std::span<const char>(str, max_len));
+constexpr Result<size_t> NullTerminatedLength(const char* str, size_t max_len) {
+ return NullTerminatedLength(span<const char>(str, max_len));
}
-// Copies the source string to the dest, truncating if the full string does not
-// fit. Always null terminates if dest.size() or num > 0.
-//
-// Returns the number of characters written, excluding the null terminator. If
-// the string is truncated, the status is ResourceExhausted.
-//
-// Precondition: The destination and source shall not overlap.
-// Precondition: The source shall be a valid pointer.
+/// @brief `pw::string::Copy` is a safer alternative to `std::strncpy` as it
+/// always null-terminates whenever the destination buffer has a non-zero size.
+///
+/// Copies the `source` string to the `dest`, truncating if the full string does
+/// not fit. Always null terminates if `dest.size()` or `num` is greater than 0.
+///
+/// @pre The destination and source shall not overlap. The source
+/// shall be a valid pointer.
+///
+/// @returns the number of characters written, excluding the null terminator. If
+/// the string is truncated, the status is `RESOURCE_EXHAUSTED`.
+template <typename Span>
PW_CONSTEXPR_CPP20 inline StatusWithSize Copy(const std::string_view& source,
- std::span<char> dest) {
- if (dest.empty()) {
- return StatusWithSize::ResourceExhausted();
- }
-
- const size_t copied = source.copy(dest.data(), dest.size() - 1);
- dest[copied] = '\0';
-
- return StatusWithSize(
- copied == source.size() ? OkStatus() : Status::ResourceExhausted(),
- copied);
+ Span&& dest) {
+ static_assert(
+ !std::is_base_of_v<InlineString<>, std::decay_t<Span>>,
+ "Do not use pw::string::Copy() with pw::InlineString<>. Instead, use "
+ "pw::InlineString<>'s assignment operator or assign() function, or "
+ "pw::string::Append().");
+ return internal::CopyToSpan(source, std::forward<Span>(dest));
}
-PW_CONSTEXPR_CPP20 inline StatusWithSize Copy(const char* source,
- std::span<char> dest) {
+template <typename Span>
+PW_CONSTEXPR_CPP20 inline StatusWithSize Copy(const char* source, Span&& dest) {
PW_DASSERT(source != nullptr);
- return Copy(ClampedCString(source, dest.size()), dest);
+ return Copy(ClampedCString(source, std::size(dest)),
+ std::forward<Span>(dest));
}
PW_CONSTEXPR_CPP20 inline StatusWithSize Copy(const char* source,
char* dest,
size_t num) {
- return Copy(source, std::span<char>(dest, num));
+ return Copy(source, span<char>(dest, num));
+}
+
+/// Assigns a `std::string_view` to a `pw::InlineString`, truncating if it does
+/// not fit. The `assign()` function of `pw::InlineString` asserts if the
+/// string's requested size exceeds its capacity; `pw::string::Assign()`
+/// returns a `Status` instead.
+///
+/// @return `OK` if the entire `std::string_view` was copied to the end of the
+/// `pw::InlineString`. `RESOURCE_EXHAUSTED` if the `std::string_view` was
+/// truncated to fit.
+inline Status Assign(InlineString<>& string, const std::string_view& view) {
+ const size_t chars_copied =
+ std::min(view.size(), static_cast<size_t>(string.capacity()));
+ string.assign(view, 0, static_cast<string_impl::size_type>(chars_copied));
+ return chars_copied == view.size() ? OkStatus() : Status::ResourceExhausted();
+}
+
+inline Status Assign(InlineString<>& string, const char* c_string) {
+ PW_DASSERT(c_string != nullptr);
+ // Clamp to capacity + 1 so strings larger than the capacity yield an error.
+ return Assign(string, ClampedCString(c_string, string.capacity() + 1));
+}
+
+/// Appends a `std::string_view` to a `pw::InlineString`, truncating if it
+/// does not fit. The `append()` function of `pw::InlineString` asserts if the
+/// string's requested size exceeds its capacity; `pw::string::Append()` returns
+/// a `Status` instead.
+///
+/// @return `OK` if the entire `std::string_view` was assigned.
+/// `RESOURCE_EXHAUSTED` if the `std::string_view` was truncated to fit.
+inline Status Append(InlineString<>& string, const std::string_view& view) {
+ const size_t chars_copied = std::min(
+ view.size(), static_cast<size_t>(string.capacity() - string.size()));
+ string.append(view, 0, static_cast<string_impl::size_type>(chars_copied));
+ return chars_copied == view.size() ? OkStatus() : Status::ResourceExhausted();
+}
+
+inline Status Append(InlineString<>& string, const char* c_string) {
+ PW_DASSERT(c_string != nullptr);
+ // Clamp to capacity + 1 so strings larger than the capacity yield an error.
+ return Append(string, ClampedCString(c_string, string.capacity() + 1));
+}
+
+/// @brief Provides a safe, printable copy of a string.
+///
+/// Copies the `source` string to the `dest` string with same behavior as
+/// `pw::string::Copy`, with the difference that any non-printable characters
+/// are changed to `.`.
+PW_CONSTEXPR_CPP20 inline StatusWithSize PrintableCopy(
+ const std::string_view& source, span<char> dest) {
+ StatusWithSize copy_result = Copy(source, dest);
+ for (size_t i = 0; i < copy_result.size(); i++) {
+ dest[i] = std::isprint(dest[i]) ? dest[i] : '.';
+ }
+
+ return copy_result;
}
} // namespace string
diff --git a/pw_string/size_report/format_many_without_error_handling.cc b/pw_string/size_report/format_many_without_error_handling.cc
index 915e935cf..fc460020c 100644
--- a/pw_string/size_report/format_many_without_error_handling.cc
+++ b/pw_string/size_report/format_many_without_error_handling.cc
@@ -28,7 +28,7 @@
#define FORMAT_CASE(...) \
pw::string::Format(buffer, __VA_ARGS__) \
- .IgnoreError() // TODO(pwbug/387): Handle Status properly
+ .IgnoreError() // TODO(b/242598609): Handle Status properly
#else // std::snprintf
@@ -49,7 +49,7 @@ volatile unsigned get_size;
void OutputStringsToBuffer() {
#if USE_FORMAT
- auto buffer = std::span(get_buffer_1, get_size);
+ auto buffer = span(get_buffer_1, get_size);
#else
char* buffer = get_buffer_1;
unsigned buffer_size = get_size;
diff --git a/pw_string/size_report/format_multiple.cc b/pw_string/size_report/format_multiple.cc
index 7f516cbb2..19dd091a1 100644
--- a/pw_string/size_report/format_multiple.cc
+++ b/pw_string/size_report/format_multiple.cc
@@ -31,7 +31,7 @@
#include "pw_string/format.h"
#define FORMAT_FUNCTION(...) \
- pw::string::Format(std::span(buffer, buffer_size - string_size), __VA_ARGS__)
+ pw::string::Format(span(buffer, buffer_size - string_size), __VA_ARGS__)
#define CHECK_RESULT(result) ProcessResult(&string_size, result)
namespace {
diff --git a/pw_string/size_report/format_single.cc b/pw_string/size_report/format_single.cc
index d3ea99ae2..c49d9d2f5 100644
--- a/pw_string/size_report/format_single.cc
+++ b/pw_string/size_report/format_single.cc
@@ -44,10 +44,8 @@ unsigned OutputStringsToBuffer() {
#if USE_FORMAT
// The code for using pw::string::Format is much simpler and safer.
- return Format(std::span(buffer, buffer_size),
- "hello %s %d",
- get_buffer_2,
- get_size)
+ return Format(
+ span(buffer, buffer_size), "hello %s %d", get_buffer_2, get_size)
.size();
#else // std::snprintf
if (buffer_size == 0u) {
diff --git a/pw_string/size_report/string_builder_size_report_incremental.cc b/pw_string/size_report/string_builder_size_report_incremental.cc
index a37fdbea4..87e7da920 100644
--- a/pw_string/size_report/string_builder_size_report_incremental.cc
+++ b/pw_string/size_report/string_builder_size_report_incremental.cc
@@ -87,7 +87,7 @@ int main() {
ProcessResult(buffer, &bytes, size, result);
- pw::StringBuilder sb(std::span(buffer, size));
+ pw::StringBuilder sb(pw::span(buffer, size));
sb << "This is part of the base " << 123 << false << '\n';
sb.clear();
diff --git a/pw_string/string_builder.cc b/pw_string/string_builder.cc
index 9317cc217..c034c4cae 100644
--- a/pw_string/string_builder.cc
+++ b/pw_string/string_builder.cc
@@ -22,20 +22,20 @@
namespace pw {
void StringBuilder::clear() {
- size_ = 0;
+ *size_ = 0;
NullTerminate();
- status_ = OkStatus();
- last_status_ = OkStatus();
+ status_ = StatusCode(OkStatus());
+ last_status_ = StatusCode(OkStatus());
}
StringBuilder& StringBuilder::append(size_t count, char ch) {
- char* const append_destination = buffer_.data() + size_;
+ char* const append_destination = buffer_.data() + size();
std::fill_n(append_destination, ResizeAndTerminate(count), ch);
return *this;
}
StringBuilder& StringBuilder::append(const char* str, size_t count) {
- char* const append_destination = buffer_.data() + size_;
+ char* const append_destination = buffer_.data() + size();
std::copy_n(str, ResizeAndTerminate(count), append_destination);
return *this;
}
@@ -64,22 +64,22 @@ StringBuilder& StringBuilder::append(const std::string_view& str,
size_t StringBuilder::ResizeAndTerminate(size_t chars_to_append) {
const size_t copied = std::min(chars_to_append, max_size() - size());
- size_ += copied;
+ *size_ += copied;
NullTerminate();
if (buffer_.empty() || chars_to_append != copied) {
SetErrorStatus(Status::ResourceExhausted());
} else {
- last_status_ = OkStatus();
+ last_status_ = StatusCode(OkStatus());
}
return copied;
}
void StringBuilder::resize(size_t new_size) {
- if (new_size <= size_) {
- size_ = new_size;
+ if (new_size <= size()) {
+ *size_ = static_cast<InlineString<>::size_type>(new_size);
NullTerminate();
- last_status_ = OkStatus();
+ last_status_ = StatusCode(OkStatus());
} else {
SetErrorStatus(Status::OutOfRange());
}
@@ -96,11 +96,11 @@ StringBuilder& StringBuilder::Format(const char* format, ...) {
StringBuilder& StringBuilder::FormatVaList(const char* format, va_list args) {
HandleStatusWithSize(
- string::FormatVaList(buffer_.subspan(size_), format, args));
+ string::FormatVaList(buffer_.subspan(size()), format, args));
return *this;
}
-void StringBuilder::WriteBytes(std::span<const std::byte> data) {
+void StringBuilder::WriteBytes(span<const std::byte> data) {
if (size() + data.size() * 2 > max_size()) {
SetErrorStatus(Status::ResourceExhausted());
} else {
@@ -111,24 +111,24 @@ void StringBuilder::WriteBytes(std::span<const std::byte> data) {
}
void StringBuilder::CopySizeAndStatus(const StringBuilder& other) {
- size_ = other.size_;
+ *size_ = static_cast<InlineString<>::size_type>(other.size());
status_ = other.status_;
last_status_ = other.last_status_;
}
void StringBuilder::HandleStatusWithSize(StatusWithSize written) {
const Status status = written.status();
- last_status_ = status;
+ last_status_ = StatusCode(status);
if (!status.ok()) {
- status_ = status;
+ status_ = StatusCode(status);
}
- size_ += written.size();
+ *size_ += written.size();
}
void StringBuilder::SetErrorStatus(Status status) {
- last_status_ = status;
- status_ = status;
+ last_status_ = StatusCode(status);
+ status_ = StatusCode(status);
}
} // namespace pw
diff --git a/pw_string/string_builder_test.cc b/pw_string/string_builder_test.cc
index 1e0dc0939..ad05c8a15 100644
--- a/pw_string/string_builder_test.cc
+++ b/pw_string/string_builder_test.cc
@@ -18,10 +18,10 @@
#include <cmath>
#include <cstdint>
#include <cstring>
-#include <span>
#include <string_view>
#include "gtest/gtest.h"
+#include "pw_span/span.h"
#include "pw_string/format.h"
namespace this_pw_test {
@@ -45,7 +45,7 @@ namespace pw {
template <>
StatusWithSize ToString<this_pw_test::CustomType>(
- const this_pw_test::CustomType&, std::span<char> buffer) {
+ const this_pw_test::CustomType&, span<char> buffer) {
return string::Format(buffer, this_pw_test::CustomType::kToString);
}
@@ -57,7 +57,7 @@ namespace {
using this_pw_test::CustomType;
TEST(StringBuilder, EmptyBuffer_SizeAndMaxSizeAreCorrect) {
- StringBuilder sb(std::span<char>{});
+ StringBuilder sb(span<char>{});
EXPECT_TRUE(sb.empty());
EXPECT_EQ(0u, sb.size());
@@ -72,7 +72,7 @@ TEST(StringBuilder, EmptyBuffer_StreamOutput_WritesNothing) {
char buffer[kNoTouch.size()];
std::memcpy(buffer, kNoTouch.data(), sizeof(buffer));
- StringBuilder sb(std::span(buffer, 0));
+ StringBuilder sb(span(buffer, 0));
sb << CustomType() << " is " << 12345;
EXPECT_EQ(Status::ResourceExhausted(), sb.status());
@@ -83,7 +83,7 @@ TEST(StringBuilder, EmptyBuffer_Append_WritesNothing) {
char buffer[kNoTouch.size()];
std::memcpy(buffer, kNoTouch.data(), sizeof(buffer));
- StringBuilder sb(std::span(buffer, 0));
+ StringBuilder sb(span(buffer, 0));
EXPECT_FALSE(sb.append("Hello").ok());
EXPECT_EQ(kNoTouch, std::string_view(buffer, sizeof(buffer)));
@@ -93,7 +93,7 @@ TEST(StringBuilder, EmptyBuffer_Resize_WritesNothing) {
char buffer[kNoTouch.size()];
std::memcpy(buffer, kNoTouch.data(), sizeof(buffer));
- StringBuilder sb(std::span(buffer, 0));
+ StringBuilder sb(span(buffer, 0));
sb.resize(0);
EXPECT_TRUE(sb.ok());
@@ -101,7 +101,7 @@ TEST(StringBuilder, EmptyBuffer_Resize_WritesNothing) {
}
TEST(StringBuilder, EmptyBuffer_AppendEmpty_ResourceExhausted) {
- StringBuilder sb(std::span<char>{});
+ StringBuilder sb(span<char>{});
EXPECT_EQ(OkStatus(), sb.last_status());
EXPECT_EQ(OkStatus(), sb.status());
@@ -204,7 +204,7 @@ TEST(StringBuilder, Append_Chars_Full) {
}
TEST(StringBuilder, Append_Chars_ToEmpty) {
- StringBuilder sb(std::span<char>{});
+ StringBuilder sb(span<char>{});
EXPECT_EQ(Status::ResourceExhausted(), sb.append(1, '?').last_status());
}
@@ -395,7 +395,7 @@ TEST(StringBuilder, StreamOutput_ByteSpan) {
std::byte(0x02),
std::byte(0x41),
std::byte(0xe0)}};
- buffer << std::as_bytes(std::span(data));
+ buffer << as_bytes(span(data));
EXPECT_EQ(buffer.status(), OkStatus());
EXPECT_STREQ("00c80241e0", buffer.data());
}
@@ -403,7 +403,7 @@ TEST(StringBuilder, StreamOutput_ByteSpan) {
TEST(StringBuilder, StreamOutput_ByteSpanOutOfSpace) {
StringBuffer<4> buffer;
std::array<uint8_t, 3> data{{0xc8, 0x02, 0x41}};
- buffer << std::as_bytes(std::span(data));
+ buffer << as_bytes(span(data));
EXPECT_EQ(buffer.status(), Status::ResourceExhausted());
EXPECT_STREQ("", buffer.data());
}
@@ -476,6 +476,31 @@ TEST(StringBuilder, Object) {
EXPECT_EQ(std::strlen(CustomType::kToString), sb.size());
}
+TEST(StringBuilder, UseStringAsBuffer) {
+ InlineString<32> string;
+ StringBuilder sb(string);
+
+ sb << 123 << "456";
+
+ EXPECT_EQ(sb.data(), string.data());
+ EXPECT_EQ(sb.size(), string.size());
+ EXPECT_EQ(6u, string.size());
+
+ EXPECT_STREQ(sb.c_str(), "123456");
+ EXPECT_STREQ(string.c_str(), "123456");
+}
+
+TEST(StringBuilder, OverflowStringAsBuffer) {
+ InlineString<5> string;
+ StringBuilder sb(string);
+
+ sb << 123 << "456";
+
+ EXPECT_EQ(sb.status(), Status::ResourceExhausted());
+ EXPECT_EQ(string.size(), 5u);
+ EXPECT_STREQ(string.c_str(), "12345");
+}
+
TEST(MakeString, Object) {
CustomType custom;
const auto sb = MakeString<64>(custom);
diff --git a/pw_string/string_test.cc b/pw_string/string_test.cc
new file mode 100644
index 000000000..1a20f00cf
--- /dev/null
+++ b/pw_string/string_test.cc
@@ -0,0 +1,1874 @@
+// Copyright 2020 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_string/string.h"
+
+#include <cstddef>
+#include <iterator>
+#include <type_traits>
+
+#include "gtest/gtest.h"
+#include "pw_compilation_testing/negative_compilation.h"
+#include "pw_polyfill/standard.h"
+
+namespace pw {
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+
+using namespace std::string_view_literals;
+
+template <typename T>
+class StringViewLike {
+ public:
+ constexpr StringViewLike(const T* data, size_t size) : value_(data, size) {}
+
+ constexpr operator std::basic_string_view<T>() const { return value_; }
+
+ private:
+ std::basic_string_view<T> value_;
+};
+
+// The StringView overload ignores types that convert to const T* to avoid
+// ambiguity with the existing const T* overload.
+template <typename T>
+class StringViewLikeButConvertsToPointer : public StringViewLike<T> {
+ public:
+ using StringViewLike<T>::StringViewLike;
+
+ constexpr operator std::basic_string_view<T>() const { return value_; }
+ constexpr operator const T*() const { return value_.data(); }
+
+ private:
+ std::basic_string_view<T> value_;
+};
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+template <typename T>
+class EvenNumberIterator {
+ public:
+ using difference_type = std::ptrdiff_t;
+ using value_type = T;
+ using pointer = const T*;
+ using reference = const T&;
+ using iterator_category = std::input_iterator_tag;
+
+ // Rounds down to nearest even.
+ explicit constexpr EvenNumberIterator(value_type value)
+ : value_(static_cast<value_type>(value & ~static_cast<value_type>(1))) {}
+
+ constexpr EvenNumberIterator& operator++() {
+ value_ += 2;
+ return *this;
+ }
+
+ constexpr const T& operator*() const { return value_; }
+
+ constexpr bool operator==(const EvenNumberIterator& rhs) const {
+ return value_ == rhs.value_;
+ }
+
+ constexpr bool operator!=(const EvenNumberIterator& rhs) const {
+ return value_ != rhs.value_;
+ }
+
+ private:
+ value_type value_;
+};
+
+#ifdef __cpp_deduction_guides
+
+TEST(InlineString, DeduceBasicString_Char) {
+ InlineBasicString string_10("1234567890");
+ static_assert(std::is_same_v<decltype(string_10), InlineString<10>>);
+
+ InlineBasicString string_3 = "abc";
+ static_assert(std::is_same_v<decltype(string_3), InlineString<3>>);
+
+ string_10.resize(6);
+ EXPECT_STREQ(
+ string_10.append(string_3).append(InlineBasicString("?")).c_str(),
+ "123456abc?");
+}
+
+TEST(InlineString, DeduceBasicString_Int) {
+ constexpr long kLongArray[4] = {0, 1, 2, 0};
+ InlineBasicString string_3 = kLongArray;
+ static_assert(std::is_same_v<decltype(string_3), InlineBasicString<long, 3>>);
+
+ EXPECT_EQ(string_3, InlineBasicString(kLongArray));
+}
+
+// Test CTAD on the InlineString alias, if supported.
+#if __cpp_deduction_guides >= 201907L
+
+TEST(InlineString, DeduceString) {
+ InlineString string("123456789");
+ static_assert(std::is_same_v<decltype(string), InlineString<10>>);
+
+ EXPECT_STREQ("123456789", string.c_str());
+}
+
+#endif // __cpp_deduction_guides >= 201907L
+#endif // __cpp_deduction_guides
+
+template <typename T, size_t kExpectedSize>
+constexpr bool TestEqual(const T* string, const T (&expected)[kExpectedSize]) {
+ for (size_t i = 0; i < kExpectedSize; ++i) {
+ if (string[i] != expected[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Compares a pw::InlineBasicString to a null-terminated array of characters.
+#define EXPECT_PW_STRING(pw_string, array) \
+ ASSERT_EQ(pw_string.size(), sizeof(array) / sizeof(array[0]) - 1); \
+ ASSERT_EQ(pw_string.c_str()[pw_string.size()], decltype(array[0]){0}); \
+ EXPECT_TRUE(TestEqual(pw_string.c_str(), array))
+
+#define EXPECT_CONSTEXPR_PW_STRING(string, array) \
+ static_assert(string.size() == sizeof(array) / sizeof(array[0]) - 1, \
+ #string ".size() == sizeof(" #array ") - 1 FAILED"); \
+ static_assert(string.c_str()[string.size()] == decltype(array[0]){0}, \
+ #string " must be null terminated"); \
+ static_assert(TestEqual(string.c_str(), array), \
+ #string " == " #array " FAILED")
+
+// This macro performs operations on a string and checks the result.
+//
+// 1. Declare a string variable named fixed_str using the provided
+// initialization statement. fixed_str can be used in tests that
+// specifically want a known-capacity string.
+// 2. Declare a generic-capacity generic_str reference for use in tests that
+// specifically want to use a generic-capacity string.
+// 3. Declare a str reference for use in tests. It is fixed-capacity in
+// constexpr tests and known-capacity in runtime tests.
+// 4. Execute the provided statements.
+// 5. Check that str equals the provided string literal.
+//
+// The test is executed twice:
+// - At compile time (with constexpr and static_assert) on a known-length
+// string (InlineString<kLength>).
+// - At runtime a generic reference (InlineString<>&) at runtime.
+#if __cpp_constexpr >= 201603L // constexpr lambdas are required
+#define TEST_CONSTEXPR_STRING(create_string, statements, expected) \
+ do { \
+ constexpr auto constexpr_str = [] { \
+ [[maybe_unused]] auto fixed_str = create_string; \
+ [[maybe_unused]] auto& str = fixed_str; \
+ [[maybe_unused]] auto& generic_str = Generic(fixed_str); \
+ statements; \
+ return str; \
+ }(); \
+ EXPECT_CONSTEXPR_PW_STRING(constexpr_str, expected); \
+ } while (0)
+#else // Skip constexpr tests in C++14.
+#define TEST_CONSTEXPR_STRING(create_string, statements, expected) \
+ do { \
+ constexpr auto str = create_string; \
+ EXPECT_NE(str.data(), nullptr); \
+ } while (0)
+#endif //__cpp_constexpr >= 201603L
+
+#define TEST_RUNTIME_STRING(create_string, statements, expected) \
+ do { \
+ [[maybe_unused]] auto fixed_str = create_string; \
+ [[maybe_unused]] auto& generic_str = Generic(fixed_str); \
+ [[maybe_unused]] auto& str = generic_str; \
+ statements; \
+ EXPECT_PW_STRING(str, expected); \
+ } while (0)
+
+#define TEST_STRING(create_string, statements, expected) \
+ TEST_CONSTEXPR_STRING(create_string, statements, expected); \
+ TEST_RUNTIME_STRING(create_string, statements, expected)
+
+// Casts any pw::InlineString to a generic (runtime-capacity) reference.
+template <typename T>
+constexpr const InlineBasicString<T>& Generic(const InlineBasicString<T>& str) {
+ return str;
+}
+
+template <typename T>
+constexpr InlineBasicString<T>& Generic(InlineBasicString<T>& str) {
+ return str;
+}
+
+constexpr InlineString<0> kEmptyCapacity0;
+constexpr InlineString<10> kEmptyCapacity10;
+
+constexpr InlineString<10> kSize5Capacity10 = "12345";
+constexpr InlineString<10> kSize10Capacity10("1234567890", 10);
+
+constexpr const char* kPointer0 = "";
+constexpr const char* kPointer10 = "9876543210";
+
+constexpr const char kArrayNull1[1] = {'\0'};
+constexpr const char kArray5[5] = {'1', '2', '3', '4', '\0'};
+
+// Invalid, non-terminated arrays used in negative compilation tests.
+[[maybe_unused]] constexpr const char kArrayNonNull1[1] = {'?'};
+[[maybe_unused]] constexpr const char kArrayNonNull5[5] = {
+ '1', '2', '3', '4', '5'};
+
+constexpr EvenNumberIterator<char> kEvenNumbers0(0);
+constexpr EvenNumberIterator<char> kEvenNumbers2(2);
+constexpr EvenNumberIterator<char> kEvenNumbers8(8);
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+
+constexpr std::string_view kView0;
+constexpr std::string_view kView5 = "12345"sv;
+constexpr std::string_view kView10 = "1234567890"sv;
+
+constexpr StringViewLike<char> kStringViewLike10("0123456789", 10);
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+//
+// Construction and assignment
+//
+
+// Constructor
+
+TEST(InlineString, Construct_Default) {
+ constexpr InlineString<0> kEmpty0;
+ static_assert(kEmpty0.empty(), "Must be empty");
+ static_assert(kEmpty0.c_str()[0] == '\0', "Must be null terminated");
+
+ constexpr InlineString<10> kEmpty10;
+ static_assert(kEmpty10.empty(), "Must be empty");
+ static_assert(kEmpty10.c_str()[0] == '\0', "Must be null terminated");
+}
+
+TEST(InlineString, Construct_Characters) {
+ TEST_STRING(InlineString<0>(0, 'a'), , "");
+ TEST_STRING(InlineString<10>(0, 'a'), , "");
+
+ TEST_STRING(InlineString<1>(1, 'a'), , "a");
+ TEST_STRING(InlineString<10>(1, 'a'), , "a");
+
+ TEST_STRING(InlineString<10>(10, 'a'), , "aaaaaaaaaa");
+
+ TEST_STRING(InlineString<10>(0, '\0'), , "");
+ TEST_STRING(InlineString<10>(1, '\0'), , "\0");
+ TEST_STRING(InlineString<10>(5, '\0'), , "\0\0\0\0\0");
+
+#if PW_NC_TEST(Construct_Char_TooMany_0)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr InlineString<0> too_large(1, 'A');
+#elif PW_NC_TEST(Construct_Char_TooMany_10)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr InlineString<10> too_large(11, 'A');
+#endif // PW_NC_TEST
+}
+// NOLINTNEXTLINE(google-readability-function-size)
+TEST(InlineString, Construct_Substr) {
+ TEST_STRING(InlineString<10>(kEmptyCapacity0, 0), , "");
+ TEST_STRING(InlineString<10>(kEmptyCapacity0, 0, 0), , "");
+ TEST_STRING(InlineString<10>(Generic(kEmptyCapacity0), 0), , "");
+ TEST_STRING(InlineString<10>(Generic(kEmptyCapacity0), 0, 0), , "");
+
+ TEST_STRING(InlineString<0>(kEmptyCapacity10, 0), , "");
+ TEST_STRING(InlineString<0>(kEmptyCapacity10, 0, 0), , "");
+ TEST_RUNTIME_STRING(InlineString<0>(Generic(kEmptyCapacity10), 0), , "");
+ TEST_RUNTIME_STRING(InlineString<0>(Generic(kEmptyCapacity10), 0, 0), , "");
+
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 0), , "12345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(Generic(kSize5Capacity10), 0), , "12345");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 1), , "2345");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 1), , "2345");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 4), , "5");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 4), , "5");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 5), , "");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 5), , "");
+
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 0, 0), , "");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 0, 0), , "");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 0, 1), , "1");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 0, 1), , "1");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 1, 0), , "");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 1, 0), , "");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 1, 1), , "2");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 1, 1), , "2");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 1, 4), , "2345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(Generic(kSize5Capacity10), 1, 4), , "2345");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 1, 5), , "2345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(Generic(kSize5Capacity10), 1, 4), , "2345");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 1, 9000), , "2345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(Generic(kSize5Capacity10), 1, 9000), , "2345");
+
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 4, 9000), , "5");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(Generic(kSize5Capacity10), 4, 9000), , "5");
+
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 5, 0), , "");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 5, 0), , "");
+ TEST_STRING(InlineString<10>(kSize5Capacity10, 5, 1), , "");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10), 5, 1), , "");
+
+#if PW_NC_TEST(Construct_Substr_IndexPastEnd)
+ PW_NC_EXPECT("PW_ASSERT\(index <= source_size\)");
+ [[maybe_unused]] constexpr InlineString<10> bad_string(kSize5Capacity10, 6);
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Construct_PointerLength) {
+ TEST_STRING(InlineString<0>(static_cast<const char*>(nullptr), 0), , "");
+ TEST_STRING(InlineString<10>(static_cast<const char*>(nullptr), 0), , "");
+
+ TEST_STRING(InlineString<0>(kPointer0, 0), , "");
+ TEST_STRING(InlineString<0>(kPointer10, 0), , "");
+ TEST_STRING(InlineString<10>(kPointer10, 0), , "");
+
+ TEST_STRING(InlineString<1>(kPointer10, 1), , "9");
+ TEST_STRING(InlineString<5>(kPointer10, 1), , "9");
+
+ TEST_STRING(InlineString<5>(kPointer10, 4), , "9876");
+ TEST_STRING(InlineString<5>(kPointer10 + 1, 4), , "8765");
+
+ TEST_STRING(InlineString<5>(kPointer10, 5), , "98765");
+ TEST_STRING(InlineString<5>(kPointer10 + 1, 5), , "87654");
+
+#if PW_NC_TEST(Construct_PointerLength_LengthLargerThanCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr InlineString<5> bad_string(kPointer10, 6);
+#elif PW_NC_TEST(Construct_PointerLength_LengthLargerThanInputString)
+ PW_NC_EXPECT_CLANG(
+ "constexpr variable 'bad_string' must be initialized by a constant "
+ "expression");
+ PW_NC_EXPECT_GCC("outside the bounds of array type");
+ [[maybe_unused]] constexpr InlineString<10> bad_string(kPointer10 + 6, 7);
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Construct_Pointer) {
+ TEST_STRING(InlineString<10>(static_cast<const char*>("")), , "");
+ TEST_STRING(InlineString<10>(kPointer10), , "9876543210");
+ TEST_STRING(InlineString<10>(kPointer10 + 5), , "43210");
+ TEST_STRING(InlineString<10>(kPointer10 + 10), , "");
+
+ TEST_STRING(InlineString<10>(static_cast<const char*>("ab\0cde")), , "ab");
+ TEST_STRING(InlineString<2>(static_cast<const char*>("ab\0cde")), , "ab");
+
+#if PW_NC_TEST(Construct_Pointer_LargerThanCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr InlineString<5> bad_string(kPointer10);
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Construct_Array) {
+ TEST_STRING(InlineString<0>(""), , "");
+
+ TEST_STRING(InlineString<1>(""), , "");
+ TEST_STRING(InlineString<10>(""), , "");
+
+ TEST_STRING(InlineString<2>("A"), , "A");
+ TEST_STRING(InlineString<10>("A"), , "A");
+ TEST_STRING(InlineString<10>("123456789"), , "123456789");
+
+ TEST_STRING(InlineString<2>("\0"), , "");
+ TEST_STRING(InlineString<10>(""), , "");
+ TEST_STRING(InlineString<10>("12\000456789"), , "12");
+
+ TEST_STRING(InlineString<1>(kArrayNull1), , "");
+ TEST_STRING(InlineString<5>(kArray5), , "1234");
+ TEST_STRING(InlineString<10>(kArray5), , "1234");
+
+#if PW_NC_TEST(Construct_Array_NullTerminationIsRequiredFillsCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(.*The array is not null terminated");
+ [[maybe_unused]] constexpr InlineString<1> bad_string(kArrayNonNull1);
+#elif PW_NC_TEST(Construct_Array_NullTerminationIsRequiredExtraCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(.*The array is not null terminated");
+ [[maybe_unused]] constexpr InlineString<10> bad_string(kArrayNonNull5);
+#elif PW_NC_TEST(Construct_Array_NonTerminatedArrayDoesNotFit)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr InlineString<3> bad_string(kArrayNonNull5);
+#elif PW_NC_TEST(Construct_Array_SingleCharLiteralRequiresCapacityOfAtLeast1)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr InlineString<0> bad_string("A");
+#elif PW_NC_TEST(Construct_Array_5CharLiteralRequiresCapacityOfAtLeast5)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr InlineString<4> bad_string("ACDEF");
+#elif PW_NC_TEST(Construct_Array_TooManyNulls)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr InlineString<3> bad_string(kArray5);
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Construct_Iterator) {
+#if PW_CXX_STANDARD_IS_SUPPORTED(17) // std::string_view is a C++17 feature
+ TEST_STRING(InlineString<0>(kView0.begin(), kView0.end()), , "");
+ TEST_STRING(InlineString<0>(kView5.end(), kView5.end()), , "");
+ TEST_STRING(InlineString<5>(kView0.begin(), kView0.end()), , "");
+ TEST_STRING(InlineString<5>(kView5.end(), kView5.end()), , "");
+
+ TEST_STRING(InlineString<5>(kView5.begin(), kView5.end()), , "12345");
+ TEST_STRING(InlineString<10>(kView5.begin(), kView5.end()), , "12345");
+ TEST_STRING(InlineString<10>(kView10.begin(), kView10.end()), , "1234567890");
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+ TEST_STRING(InlineString<0>(kEvenNumbers0, kEvenNumbers0), , "");
+ TEST_STRING(InlineString<10>(kEvenNumbers2, kEvenNumbers2), , "");
+
+ TEST_STRING(InlineString<4>(kEvenNumbers0, kEvenNumbers2), , "\0");
+ TEST_STRING(InlineString<4>(kEvenNumbers0, kEvenNumbers8), , "\0\2\4\6");
+ TEST_STRING(InlineString<10>(kEvenNumbers0, kEvenNumbers8), , "\0\2\4\6");
+
+#if PW_NC_TEST(Construct_Iterator_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(current_position != string_end\)");
+ [[maybe_unused]] constexpr InlineString<3> str(kEvenNumbers0, kEvenNumbers8);
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Construct_CopySameCapacity) {
+ static_assert(std::is_trivially_copyable<InlineString<0>>(), "Copy");
+ static_assert(std::is_trivially_copyable<InlineString<10>>(), "Copy");
+ static_assert(std::is_trivially_copyable<InlineBasicString<int, 10>>(),
+ "Copy");
+
+ TEST_STRING(InlineString<0>(kEmptyCapacity0), , "");
+ TEST_STRING(InlineString<10>(kEmptyCapacity10), , "");
+ TEST_STRING(InlineString<10>(kSize5Capacity10), , "12345");
+ TEST_STRING(InlineString<10>(kSize10Capacity10), , "1234567890");
+}
+
+TEST(InlineString, Construct_CopyDifferentCapacity) {
+ TEST_STRING(InlineString<1>(kEmptyCapacity0), , "");
+ TEST_STRING(InlineString<5>(kEmptyCapacity0), , "");
+ TEST_STRING(InlineString<11>(kEmptyCapacity10), , "");
+ TEST_STRING(InlineString<11>(kSize5Capacity10), , "12345");
+ TEST_STRING(InlineString<11>(kSize10Capacity10), , "1234567890");
+ TEST_STRING(InlineString<30>(kSize10Capacity10), , "1234567890");
+
+#if PW_NC_TEST(Construct_CopyDifferentCapacity_DoesNotFit)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ [[maybe_unused]] InlineString<5> bad_string(kEmptyCapacity10);
+#elif PW_NC_TEST(Construct_CopyDifferentCapacity_DoesNotFitConstexpr)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ constexpr [[maybe_unused]] InlineString<5> bad_string(kEmptyCapacity10);
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Construct_CopyGenericCapacity) {
+ TEST_STRING(InlineString<10>(Generic(kEmptyCapacity0)), , "");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kEmptyCapacity10)), , "");
+ TEST_RUNTIME_STRING(InlineString<10>(Generic(kSize5Capacity10)), , "12345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(Generic(kSize10Capacity10)), , "1234567890");
+ TEST_RUNTIME_STRING(
+ InlineString<20>(Generic(kSize10Capacity10)), , "1234567890");
+}
+
+TEST(InlineString, Construct_InitializerList) {
+ TEST_STRING(InlineString<0>({}), , "");
+ TEST_STRING(InlineString<1>({}), , "");
+ TEST_STRING(InlineString<10>({}), , "");
+
+ TEST_STRING(InlineString<1>({'\0'}), , "\0");
+ TEST_STRING(InlineString<1>({'?'}), , "?");
+
+ TEST_STRING(InlineString<5>({'A'}), , "A");
+ TEST_STRING(InlineString<5>({'\0', '\0', '\0'}), , "\0\0\0");
+ TEST_STRING(InlineString<5>({'5', '4', '3', '2', '1'}), , "54321");
+
+#if PW_NC_TEST(Construct_InitializerList_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr InlineString<3> bad_string({'1', '2', '3', '\0'});
+#endif // PW_NC_TEST
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+constexpr InlineString<16> TakesInlineString(const InlineString<16>& str) {
+ return str;
+}
+
+struct HoldsString {
+ InlineString<16> value;
+};
+
+TEST(InlineString, Construct_StringView) {
+ TEST_STRING(InlineString<0>(""sv), , "");
+ TEST_STRING(InlineString<10>(""sv), , "");
+ TEST_STRING(InlineString<10>("01234"sv), , "01234");
+ TEST_STRING(InlineString<10>("0123456789"sv), , "0123456789");
+ TEST_STRING(InlineString<20>("0123456789"sv), , "0123456789");
+
+ TEST_STRING(InlineString<10>(StringViewLike<char>("01234", 5)), , "01234");
+ TEST_STRING(InlineString<10>(kStringViewLike10), , "0123456789");
+
+ // pw::InlineString supports implicit conversion from std::string_view.
+ constexpr InlineString<16> implicit_call = TakesInlineString("1234"sv);
+ EXPECT_CONSTEXPR_PW_STRING(implicit_call, "1234");
+
+ constexpr HoldsString implicit_initialize_1{.value = "1234"sv};
+ EXPECT_CONSTEXPR_PW_STRING(implicit_initialize_1.value, "1234");
+
+ constexpr HoldsString implicit_initialize_2{.value{"1234"sv}};
+ EXPECT_CONSTEXPR_PW_STRING(implicit_initialize_2.value, "1234");
+
+ constexpr HoldsString implicit_initialize_3{"1234"sv};
+ EXPECT_CONSTEXPR_PW_STRING(implicit_initialize_3.value, "1234");
+
+#if PW_NC_TEST(Construct_StringView_DoesNotFit)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ [[maybe_unused]] InlineString<5> bad_string(kEmptyCapacity10);
+#elif PW_NC_TEST(Construct_StringView_DoesNotFitConstexpr)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ constexpr [[maybe_unused]] InlineString<5> bad_string(kEmptyCapacity10);
+#elif PW_NC_TEST(Construct_StringView_NoConversionFromAmbiguousClass)
+ PW_NC_EXPECT_CLANG("no matching constructor");
+ PW_NC_EXPECT_GCC("no matching function for call to");
+ [[maybe_unused]] InlineString<10> fail(
+ StringViewLikeButConvertsToPointer<char>("1", 1));
+#elif PW_NC_TEST(Construct_StringView_NoImplicitConversionFromStringViewLike)
+ PW_NC_EXPECT_CLANG("no matching function for call to 'TakesInlineString'");
+ PW_NC_EXPECT_GCC(
+ "invalid initialization of reference of type .* from expression of type "
+ "'const pw::StringViewLike<char>'");
+ TakesInlineString(kStringViewLike10);
+#elif PW_NC_TEST(Construct_StringView_NoImplicitConvFromStringViewLikeInInit1)
+ PW_NC_EXPECT_GCC("could not convert 'pw::kStringViewLike10'");
+ PW_NC_EXPECT_CLANG("no viable conversion from 'const StringViewLike<char>'");
+ (void)HoldsString{.value = kStringViewLike10};
+#elif PW_NC_TEST(Construct_StringView_NoImplicitConvFromStringViewLikeInInit2)
+ PW_NC_EXPECT_GCC("could not convert 'pw::kStringViewLike10'");
+ PW_NC_EXPECT_CLANG("no viable conversion from 'const StringViewLike<char>'");
+ (void)HoldsString{kStringViewLike10};
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Construct_StringViewSubstr) {
+ TEST_STRING(InlineString<0>(""sv, 0, 0), , "");
+ TEST_STRING(InlineString<5>(""sv, 0, 0), , "");
+
+ TEST_STRING(InlineString<5>("0123456789"sv, 5, 0), , "");
+ TEST_STRING(InlineString<5>("0123456789"sv, 10, 0), , "");
+
+ TEST_STRING(InlineString<5>("0123456789"sv, 0, 5), , "01234");
+ TEST_STRING(InlineString<5>("0123456789"sv, 1, 5), , "12345");
+ TEST_STRING(InlineString<5>("0123456789"sv, 8, 2), , "89");
+ TEST_STRING(InlineString<5>("0123456789"sv, 8, 10), , "89");
+ TEST_STRING(InlineString<5>("0123456789"sv, 10, 100), , "");
+
+ TEST_STRING(InlineString<10>("0123456789"sv, 0, 10), , "0123456789");
+ TEST_STRING(InlineString<10>("0123456789"sv, 0, 100), , "0123456789");
+
+ TEST_STRING(InlineString<10>(kStringViewLike10, 0, 100), , "0123456789");
+
+#if PW_NC_TEST(Construct_StringViewSubstr_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr InlineString<10> bad_string(
+ "0123456789?"sv, 0, 11);
+#elif PW_NC_TEST(Construct_StringViewSubstr_IndexTooFar)
+ PW_NC_EXPECT_CLANG("must be initialized by a constant expression");
+ PW_NC_EXPECT_GCC("call to non-'constexpr' function");
+ [[maybe_unused]] constexpr InlineString<10> bad_string("12345"sv, 6, 0);
+#endif // PW_NC_TEST
+}
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+TEST(InlineString, Construct_Nullptr) {
+#if PW_NC_TEST(Construct_Nullptr)
+ PW_NC_EXPECT("Cannot construct from nullptr");
+ [[maybe_unused]] constexpr InlineString<0> bad_string(nullptr);
+#endif // PW_NC_TEST
+}
+
+// operator=
+
+TEST(InlineString, AssignOperator_Copy) {
+ TEST_STRING(InlineString<0>(), fixed_str = InlineString<0>(), "");
+ TEST_STRING(InlineString<10>("something"),
+ fixed_str = InlineString<9>("el\0se"),
+ "el");
+ TEST_STRING(InlineString<10>("0_o"), fixed_str = InlineString<10>(), "");
+
+#if PW_NC_TEST(AssignOperator_Copy_DoesNotFit)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<5> str;
+ return str = InlineString<6>("2big");
+ }();
+#elif PW_NC_TEST(AssignOperator_Copy_NotSupportedByGeneric)
+ PW_NC_EXPECT("operator=.*protected");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<5> str;
+ return Generic(str) = InlineString<0>();
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, AssignOperator_Array) {
+ TEST_STRING(InlineString<1>({'a'}), fixed_str = "", "");
+ TEST_STRING(InlineString<10>("hey"), fixed_str = "wow", "wow");
+ TEST_STRING(InlineString<10>("hey"), fixed_str = "123456789", "123456789");
+
+#if PW_NC_TEST(AssignOperator_Array_DoesNotFit)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<4> str("abc");
+ return str = "12345";
+ }();
+#elif PW_NC_TEST(AssignOperator_Array_NotSupportedByGeneric)
+ PW_NC_EXPECT("operator=");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<5> str("abc");
+ return Generic(str) = "";
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, AssignOperator_Pointer) {
+ TEST_STRING(InlineString<1>({'a'}), fixed_str = kPointer0, "");
+ TEST_STRING(InlineString<10>("hey"),
+ fixed_str = static_cast<const char*>("wow"),
+ "wow");
+ TEST_STRING(InlineString<10>("hey"), fixed_str = kPointer10, "9876543210");
+
+#if PW_NC_TEST(AssignOperator_Pointer_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<5> str("abc");
+ return str = static_cast<const char*>("123456");
+ }();
+#elif PW_NC_TEST(AssignPointer_Pointer_NotSupportedByGeneric)
+ PW_NC_EXPECT("operator=");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<5> str("abc");
+ return Generic(str) = kPointer0;
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, AssignOperator_Character) {
+ TEST_STRING(InlineString<1>(), fixed_str = '\0', "\0");
+ TEST_STRING(InlineString<1>({'a'}), fixed_str = '\0', "\0");
+ TEST_STRING(InlineString<10>("hey"), fixed_str = '?', "?");
+
+#if PW_NC_TEST(AssignPointer_Character_DoesNotFit)
+ PW_NC_EXPECT("Cannot assign a character to pw::InlineString<0>");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<0> str;
+ return str = 'a';
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, AssignOperator_InitializerList) {
+ TEST_STRING(InlineString<1>(), fixed_str = {'\0'}, "\0");
+ TEST_STRING(InlineString<1>({'a'}), fixed_str = {'\0'}, "\0");
+ TEST_STRING(
+ InlineString<10>("hey"), (fixed_str = {'W', 'h', 'y', '?'}), "Why?");
+
+#if PW_NC_TEST(AssignPointer_InitializerList_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<2> str;
+ return str = {'1', '2', '3'};
+ }();
+#endif // PW_NC_TEST
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+TEST(InlineString, AssignOperator_StringView) {
+ TEST_STRING(InlineString<1>(), fixed_str = "\0"sv, "\0");
+ TEST_STRING(InlineString<1>({'a'}), fixed_str = "\0"sv, "\0");
+ TEST_STRING(InlineString<10>("hey"), fixed_str = "Why?"sv, "Why?");
+
+#if PW_NC_TEST(AssignPointer_StringView_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<2> str;
+ return str = "123"sv;
+ }();
+#endif // PW_NC_TEST
+}
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+// assign
+
+TEST(InlineString, Assign_Characters) {
+ TEST_STRING(InlineString<0>(), str.assign(0, 'a'), "");
+ TEST_STRING(InlineString<10>(), str.assign(0, 'a'), "");
+ TEST_STRING(InlineString<10>("hey"), str.assign(0, 'a'), "");
+
+ TEST_STRING(InlineString<1>(), str.assign(1, 'a'), "a");
+ TEST_STRING(InlineString<10>(), str.assign(10, 'a'), "aaaaaaaaaa");
+
+ TEST_STRING(InlineString<1>({'?'}), str.assign(1, 'a'), "a");
+ TEST_STRING(InlineString<10>("1"), str.assign(1, 'a'), "a");
+ TEST_STRING(InlineString<10>("123456789"), str.assign(1, 'a'), "a");
+ TEST_STRING(InlineString<10>("?"), str.assign(10, 'a'), "aaaaaaaaaa");
+
+ TEST_STRING(InlineString<5>("?"), str.assign(0, '\0'), "");
+ TEST_STRING(InlineString<5>("?"), str.assign(1, '\0'), "\0");
+ TEST_STRING(InlineString<5>("?"), str.assign(5, '\0'), "\0\0\0\0\0");
+ TEST_STRING(InlineString<10>("???"), str.assign(5, '\0'), "\0\0\0\0\0");
+
+#if PW_NC_TEST(Assign_Characters_TooMany)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto value = [] {
+ InlineString<6> str;
+ return str.assign(7, '?');
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Assign_CopySameCapacity) {
+ TEST_STRING(InlineString<0>(), str.assign(kEmptyCapacity0), "");
+ TEST_STRING(InlineString<10>(), str.assign(kEmptyCapacity10), "");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10), "12345");
+ TEST_STRING(InlineString<10>(), str.assign(kSize10Capacity10), "1234567890");
+}
+
+TEST(InlineString, Assign_CopyDifferentCapacity) {
+ TEST_STRING(InlineString<1>(), str.assign(kEmptyCapacity0), "");
+ TEST_STRING(InlineString<5>(), str.assign(kEmptyCapacity0), "");
+ TEST_STRING(InlineString<11>(), str.assign(kEmptyCapacity10), "");
+ TEST_STRING(InlineString<11>(), str.assign(kSize5Capacity10), "12345");
+ TEST_STRING(InlineString<11>(), str.assign(kSize10Capacity10), "1234567890");
+ TEST_STRING(InlineString<30>(), str.assign(kSize10Capacity10), "1234567890");
+
+#if PW_NC_TEST(Assign_CopyDifferentCapacity_DoesNotFit)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ [[maybe_unused]] InlineString<5> bad_string;
+ bad_string.assign(kEmptyCapacity10);
+#elif PW_NC_TEST(Assign_CopyDifferentCapacity_DoesNotFitConstexpr)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ [[maybe_unused]] InlineString<5> bad_string;
+ bad_string.assign(kEmptyCapacity10);
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Assign_CopyGenericCapacity) {
+ TEST_STRING(InlineString<10>(), str.assign(Generic(kEmptyCapacity0)), "");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kEmptyCapacity10)), "");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10)), "12345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize10Capacity10)), "1234567890");
+ TEST_RUNTIME_STRING(
+ InlineString<20>(), str.assign(Generic(kSize10Capacity10)), "1234567890");
+}
+
+TEST(InlineString, Assign_Substr) { // NOLINT(google-readability-function-size)
+ TEST_STRING(InlineString<10>(), str.assign(kEmptyCapacity0, 0), "");
+ TEST_STRING(InlineString<10>(), str.assign(kEmptyCapacity0, 0, 0), "");
+ TEST_STRING(InlineString<10>(), str.assign(Generic(kEmptyCapacity0), 0), "");
+ TEST_STRING(
+ InlineString<10>(), str.assign(Generic(kEmptyCapacity0), 0, 0), "");
+
+ TEST_STRING(InlineString<0>(), str.assign(kEmptyCapacity10, 0), "");
+ TEST_STRING(InlineString<0>(), str.assign(kEmptyCapacity10, 0, 0), "");
+ TEST_RUNTIME_STRING(
+ InlineString<0>(), str.assign(Generic(kEmptyCapacity10), 0), "");
+ TEST_RUNTIME_STRING(
+ InlineString<0>(), str.assign(Generic(kEmptyCapacity10), 0, 0), "");
+
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 0), "12345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 0), "12345");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 1), "2345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 1), "2345");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 4), "5");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 4), "5");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 5), "");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 5), "");
+
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 0, 0), "");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 0, 0), "");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 0, 1), "1");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 0, 1), "1");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 1, 0), "");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 1, 0), "");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 1, 1), "2");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 1, 1), "2");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 1, 4), "2345");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 1, 4), "2345");
+
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 4, 9000), "5");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 4, 9000), "5");
+
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 5, 0), "");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 5, 0), "");
+ TEST_STRING(InlineString<10>(), str.assign(kSize5Capacity10, 5, 1), "");
+ TEST_RUNTIME_STRING(
+ InlineString<10>(), str.assign(Generic(kSize5Capacity10), 5, 1), "");
+
+#if PW_NC_TEST(Assign_Substr_IndexPastEnd)
+ PW_NC_EXPECT("PW_ASSERT\(index <= source_size\)");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<10> str;
+ return str.assign(kSize5Capacity10, 6);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Assign_PointerLength) {
+ TEST_STRING(InlineString<0>(), str.assign(nullptr, 0), "");
+ TEST_STRING(InlineString<10>(), str.assign(nullptr, 0), "");
+
+ TEST_STRING(InlineString<0>(), str.assign(kPointer0, 0), "");
+ TEST_STRING(InlineString<0>(), str.assign(kPointer10, 0), "");
+ TEST_STRING(InlineString<10>("abc"), str.assign(kPointer10, 0), "");
+
+ TEST_STRING(InlineString<1>(), str.assign(kPointer10, 1), "9");
+ TEST_STRING(InlineString<5>("?"), str.assign(kPointer10, 1), "9");
+
+ TEST_STRING(InlineString<5>("?"), str.assign(kPointer10, 4), "9876");
+ TEST_STRING(InlineString<5>("?"), str.assign(kPointer10 + 1, 4), "8765");
+
+ TEST_STRING(InlineString<5>("?"), str.assign(kPointer10, 5), "98765");
+ TEST_STRING(InlineString<5>("?"), str.assign(kPointer10 + 1, 5), "87654");
+
+#if PW_NC_TEST(Assign_PointerLength_LengthLargerThanCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<5> str;
+ return str.assign(kPointer10, 6);
+ }();
+#elif PW_NC_TEST(Assign_PointerLength_LengthLargerThanInputString)
+ PW_NC_EXPECT_CLANG(
+ "constexpr variable 'bad_string' must be initialized by a constant "
+ "expression");
+ PW_NC_EXPECT_GCC("outside the bounds of array type");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<10> str;
+ return str.assign(kPointer10 + 6, 7);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Assign_Pointer) {
+ TEST_STRING(
+ InlineString<10>("\0"), str.assign(static_cast<const char*>("")), "");
+ TEST_STRING(InlineString<10>("abc"), str.assign(kPointer10), "9876543210");
+ TEST_STRING(InlineString<10>("abc"), str.assign(kPointer10 + 5), "43210");
+ TEST_STRING(InlineString<10>("abc"), str.assign(kPointer10 + 10), "");
+
+ TEST_STRING(InlineString<10>(),
+ str.assign(static_cast<const char*>("ab\0cde")),
+ "ab");
+ TEST_STRING(
+ InlineString<2>(), str.assign(static_cast<const char*>("ab\0cde")), "ab");
+
+#if PW_NC_TEST(Assign_Pointer_LargerThanCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<5> str;
+ return str.assign(kPointer10);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Assign_Array) {
+ TEST_STRING(InlineString<0>(), str.assign(""), "");
+ TEST_STRING(InlineString<1>(), str.assign(""), "");
+ TEST_STRING(InlineString<10>("a"), str.assign(""), "");
+
+ TEST_STRING(InlineString<1>(), str.assign("A"), "A");
+ TEST_STRING(InlineString<10>(), str.assign("A"), "A");
+ TEST_STRING(InlineString<10>(), str.assign("123456789"), "123456789");
+
+ TEST_STRING(InlineString<1>(), str.assign("\0"), "");
+ TEST_STRING(InlineString<10>(), str.assign("\0"), "");
+ TEST_STRING(InlineString<10>(), str.assign("12\000456789"), "12");
+
+ TEST_STRING(InlineString<1>(""), str.assign(kArrayNull1), "");
+ TEST_STRING(InlineString<5>(), str.assign(kArray5), "1234");
+ TEST_STRING(InlineString<10>(), str.assign(kArray5), "1234");
+
+ TEST_RUNTIME_STRING(InlineString<1>(), Generic(str).assign("?"), "?");
+ TEST_RUNTIME_STRING(
+ InlineString<5>("abcd"), Generic(str).assign("12345"), "12345");
+
+#if 0 // Triggers PW_ASSERT
+ [[maybe_unused]] InlineString<5> too_small("abcd");
+ Generic(too_small).assign("123456");
+#endif // 0
+
+#if PW_NC_TEST(Assign_Array_NullTerminationIsRequiredFillsCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(.*The array is not null terminated");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<1> bad_string;
+ return bad_string.assign(kArrayNonNull1);
+ }();
+#elif PW_NC_TEST(Assign_Array_NullTerminationIsRequiredExtraCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(.*The array is not null terminated");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<10> bad_string;
+ return bad_string.assign(kArrayNonNull5);
+ }();
+#elif PW_NC_TEST(Assign_Array_NonTerminatedArrayDoesNotFit)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> bad_string;
+ return bad_string.assign(kArrayNonNull5);
+ }();
+#elif PW_NC_TEST(Assign_Array_SingleCharLiteralRequiresCapacityOfAtLeast1)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<0> str;
+ return str.assign("?");
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Assign_Iterator) {
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+ TEST_STRING(InlineString<0>(), str.assign(kView0.begin(), kView0.end()), "");
+ TEST_STRING(InlineString<0>(), str.assign(kView5.end(), kView5.end()), "");
+ TEST_STRING(
+ InlineString<5>("abc"), str.assign(kView0.begin(), kView0.end()), "");
+ TEST_STRING(
+ InlineString<5>("abc"), str.assign(kView5.end(), kView5.end()), "");
+
+ TEST_STRING(
+ InlineString<5>(), str.assign(kView5.begin(), kView5.end()), "12345");
+ TEST_STRING(InlineString<10>("abc"),
+ str.assign(kView5.begin(), kView5.end()),
+ "12345");
+ TEST_STRING(InlineString<10>("abc"),
+ str.assign(kView10.begin(), kView10.end()),
+ "1234567890");
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+ TEST_STRING(InlineString<0>(), str.assign(kEvenNumbers0, kEvenNumbers0), "");
+ TEST_STRING(InlineString<10>(), str.assign(kEvenNumbers2, kEvenNumbers2), "");
+
+ TEST_STRING(
+ InlineString<4>("abc"), str.assign(kEvenNumbers0, kEvenNumbers2), "\0");
+ TEST_STRING(InlineString<4>("abc"),
+ str.assign(kEvenNumbers0, kEvenNumbers8),
+ "\0\2\4\6");
+ TEST_STRING(InlineString<10>("abc"),
+ str.assign(kEvenNumbers0, kEvenNumbers8),
+ "\0\2\4\6");
+
+#if PW_NC_TEST(Assign_Iterator_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(current_position != string_end\)");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<3> str;
+ return str.assign(kEvenNumbers0, kEvenNumbers8);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Assign_InitializerList) {
+ TEST_STRING(InlineString<0>(), str.assign({}), "");
+ TEST_STRING(InlineString<1>(), str.assign({}), "");
+ TEST_STRING(InlineString<10>("abc"), str.assign({}), "");
+
+ TEST_STRING(InlineString<1>(), str.assign({'\0'}), "\0");
+ TEST_STRING(InlineString<1>(), str.assign({'?'}), "?");
+
+ TEST_STRING(InlineString<5>("abc"), str.assign({'A'}), "A");
+ TEST_STRING(InlineString<5>("abc"), str.assign({'\0', '\0', '\0'}), "\0\0\0");
+ TEST_STRING(
+ InlineString<5>("abc"), str.assign({'5', '4', '3', '2', '1'}), "54321");
+
+#if PW_NC_TEST(Assign_InitializerList_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<3> str;
+ return str.assign({'1', '2', '3', '\0'});
+ }();
+#endif // PW_NC_TEST
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+TEST(InlineString, Assign_StringView) {
+ TEST_STRING(InlineString<0>(), str.assign(""sv), "");
+ TEST_STRING(InlineString<10>("abc"), str.assign(""sv), "");
+ TEST_STRING(InlineString<10>("abc"), str.assign("01234"sv), "01234");
+ TEST_STRING(
+ InlineString<10>("abc"), str.assign("0123456789"sv), "0123456789");
+ TEST_STRING(InlineString<20>(), str.assign("0123456789"sv), "0123456789");
+
+ TEST_STRING(InlineString<10>("abc"),
+ str.assign(StringViewLike<char>("01234", 5)),
+ "01234");
+ TEST_STRING(InlineString<10>(), str.assign(kStringViewLike10), "0123456789");
+
+#if PW_NC_TEST(Assign_StringView_DoesNotFit)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ [[maybe_unused]] InlineString<5> bad_string;
+ bad_string.assign(kEmptyCapacity10);
+#elif PW_NC_TEST(Assign_StringView_DoesNotFitConstexpr)
+ PW_NC_EXPECT(
+ "pw::InlineString must be at least as large as the source string");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<5> str;
+ str.assign(kEmptyCapacity10);
+ return str;
+ }();
+#elif PW_NC_TEST(Assign_StringView_NoAssignmentFromAmbiguousClass)
+ PW_NC_EXPECT_CLANG("no matching member function for call to");
+ PW_NC_EXPECT_GCC("no matching function for call to");
+ [[maybe_unused]] InlineString<10> fail;
+ fail.assign(StringViewLikeButConvertsToPointer<char>("1", 1));
+#endif // PW_NC_TEST
+}
+
+// NOLINTNEXTLINE(google-readability-function-size)
+TEST(InlineString, Assign_StringViewSubstr) {
+ TEST_STRING(InlineString<0>(), str.assign(""sv, 0, 0), "");
+ TEST_STRING(InlineString<5>(), str.assign(""sv, 0, 0), "");
+
+ TEST_STRING(InlineString<5>(), str.assign("0123456789"sv, 5, 0), "");
+ TEST_STRING(InlineString<5>(), str.assign("0123456789"sv, 10, 0), "");
+
+ TEST_STRING(InlineString<5>(), str.assign("0123456789"sv, 0, 5), "01234");
+ TEST_STRING(InlineString<5>(), str.assign("0123456789"sv, 1, 5), "12345");
+ TEST_STRING(InlineString<5>(), str.assign("0123456789"sv, 8, 2), "89");
+ TEST_STRING(InlineString<5>(), str.assign("0123456789"sv, 8, 10), "89");
+ TEST_STRING(InlineString<5>(), str.assign("0123456789"sv, 10, 10), "");
+ TEST_STRING(InlineString<5>(), str.assign("0123456789"sv, 10, 100), "");
+
+ TEST_STRING(
+ InlineString<10>(), str.assign("0123456789"sv, 0, 10), "0123456789");
+ TEST_STRING(
+ InlineString<10>(), str.assign("0123456789"sv, 0, 100), "0123456789");
+
+ TEST_STRING(
+ InlineString<10>(), str.assign(kStringViewLike10, 0, 100), "0123456789");
+
+#if PW_NC_TEST(Assign_StringViewSubstr_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<10> str;
+ return str.assign("0123456789?"sv, 0, 11);
+ }();
+#elif PW_NC_TEST(Assign_StringViewSubstr_IndexTooFar)
+ PW_NC_EXPECT_CLANG("must be initialized by a constant expression");
+ PW_NC_EXPECT_GCC("call to non-'constexpr' function");
+ [[maybe_unused]] constexpr auto bad_string = [] {
+ InlineString<10> str;
+ return str.assign("12345"sv, 6, 0);
+ }();
+#endif // PW_NC_TEST
+}
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+//
+// Element access
+//
+
+TEST(InlineString, At) {
+ static_assert(kSize5Capacity10.at(0) == '1', "1");
+ static_assert(kSize5Capacity10.at(1) == '2', "2");
+ static_assert(kSize5Capacity10.at(2) == '3', "3");
+ static_assert(kSize5Capacity10.at(3) == '4', "4");
+ static_assert(kSize5Capacity10.at(4) == '5', "5");
+
+ static_assert(kSize10Capacity10.at(9) == '0', "null");
+
+ EXPECT_EQ(Generic(kSize5Capacity10).at(0), '1');
+ EXPECT_EQ(Generic(kSize5Capacity10).at(1), '2');
+ EXPECT_EQ(Generic(kSize5Capacity10).at(2), '3');
+ EXPECT_EQ(Generic(kSize5Capacity10).at(3), '4');
+ EXPECT_EQ(Generic(kSize5Capacity10).at(4), '5');
+
+#if PW_NC_TEST(At_OutOfBounds)
+ PW_NC_EXPECT("PW_ASSERT\(index < length\(\)\);");
+ [[maybe_unused]] constexpr char out_of_bounds = kSize5Capacity10.at(5);
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, SubscriptOperator) {
+ static_assert(kSize5Capacity10[0] == '1', "1");
+ static_assert(kSize5Capacity10[1] == '2', "2");
+ static_assert(kSize5Capacity10[2] == '3', "3");
+ static_assert(kSize5Capacity10[3] == '4', "4");
+ static_assert(kSize5Capacity10[4] == '5', "5");
+
+ static_assert(kSize10Capacity10[9] == '0', "null");
+
+ EXPECT_EQ(Generic(kSize5Capacity10)[0], '1');
+ EXPECT_EQ(Generic(kSize5Capacity10)[1], '2');
+ EXPECT_EQ(Generic(kSize5Capacity10)[2], '3');
+ EXPECT_EQ(Generic(kSize5Capacity10)[3], '4');
+ EXPECT_EQ(Generic(kSize5Capacity10)[4], '5');
+
+ static_assert(kSize5Capacity10[5] == '\0', "No range checking");
+ static_assert(kSize5Capacity10[6] == '\0', "No range checking");
+}
+
+TEST(InlineString, FrontBack) {
+ static_assert(kSize10Capacity10.front() == '1', "1");
+ static_assert(kSize10Capacity10.back() == '0', "0");
+ EXPECT_EQ(Generic(kSize10Capacity10).front(), '1');
+ EXPECT_EQ(Generic(kSize10Capacity10).back(), '0');
+}
+
+TEST(InlineString, DataCStr) {
+ static_assert(kSize10Capacity10.data() == kSize10Capacity10.c_str(),
+ "data() and c_str()");
+ EXPECT_EQ(Generic(kSize10Capacity10).data(),
+ Generic(kSize10Capacity10).c_str());
+
+ EXPECT_STREQ(kSize10Capacity10.data(), "1234567890");
+ EXPECT_STREQ(kSize10Capacity10.c_str(), "1234567890");
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+TEST(InlineString, ConvertsToStringView) {
+ static_assert(std::string_view(kSize5Capacity10) == "12345"sv);
+ EXPECT_EQ(std::string_view(Generic(kSize5Capacity10)), "12345"sv);
+}
+
+//
+// Iterators
+//
+
+TEST(InlineString, Iterators) {
+ static_assert(kEmptyCapacity10.begin() == kEmptyCapacity10.end());
+ static_assert(kSize5Capacity10.end() - kSize5Capacity10.begin() == 5u);
+ static_assert(kSize5Capacity10.begin() + 5 == kSize5Capacity10.end());
+
+ static_assert(*kSize5Capacity10.begin() == '1');
+ static_assert(*(kSize5Capacity10.begin() + 1) == '2');
+
+ static_assert(kEmptyCapacity10.rbegin() == kEmptyCapacity10.rend());
+ static_assert(kSize5Capacity10.rend() - kSize5Capacity10.rbegin() == 5u);
+ static_assert(kSize5Capacity10.rbegin() + 5 == kSize5Capacity10.rend());
+
+ static_assert(*kSize5Capacity10.rbegin() == '5');
+ static_assert(*(kSize5Capacity10.rbegin() + 1) == '4');
+
+ static_assert(kSize5Capacity10.begin() == kSize5Capacity10.cbegin());
+ static_assert(kSize5Capacity10.end() == kSize5Capacity10.cend());
+ static_assert(kSize5Capacity10.rbegin() == kSize5Capacity10.crbegin());
+ static_assert(kSize5Capacity10.rend() == kSize5Capacity10.crend());
+
+ static_assert([] {
+ char expected = '1';
+ for (char ch : kSize5Capacity10) {
+ if (ch != expected) {
+ return false;
+ }
+ expected += 1;
+ }
+ return true;
+ }());
+}
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+//
+// Capacity
+//
+
+TEST(InlineString, Size) {
+ static_assert(kEmptyCapacity0.empty(), "empty");
+ static_assert(kEmptyCapacity10.empty(), "empty");
+
+ static_assert(kEmptyCapacity10.size() == 0u, "0"); // NOLINT
+ static_assert(kSize5Capacity10.size() == 5u, "5");
+ static_assert(kEmptyCapacity10.length() == 0u, "0");
+ static_assert(kSize5Capacity10.length() == 5u, "5");
+}
+
+TEST(InlineString, MaxSize) {
+ static_assert(InlineString<0>().max_size() == 0u, "0");
+ static_assert(InlineString<1>().max_size() == 1u, "1");
+ static_assert(InlineString<10>().max_size() == 10u, "10");
+ static_assert(InlineString<10>("123").max_size() == 10u, "10");
+ static_assert(Generic(InlineString<10>("123")).max_size() == 10u, "10");
+
+ static_assert(InlineString<0>().capacity() == 0u, "0");
+ static_assert(InlineString<10>().capacity() == 10u, "10");
+}
+
+//
+// Operations
+//
+
+// clear
+
+TEST(InlineString, Clear) {
+ TEST_STRING(InlineString<0>(), str.clear(), "");
+ TEST_STRING(InlineString<8>(), str.clear(), "");
+ TEST_STRING(InlineString<8>("stuff"), str.clear(), "");
+ TEST_RUNTIME_STRING(InlineString<8>("stuff"), generic_str.clear(), "");
+ TEST_STRING(InlineString<8>("!!"), str.clear(); str.assign("?"), "?");
+}
+
+// TODO(b/239996007): Test insert.
+
+// TODO(b/239996007): Test erase.
+
+TEST(InlineString, PushBack) {
+ TEST_STRING(InlineString<1>(), str.push_back('#'), "#");
+ TEST_STRING(InlineString<5>("abc"), str.push_back('d');
+ str.push_back('e'), "abcde");
+
+#if PW_NC_TEST(PushBack_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(size\(\) < max_size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<1> str("?", 1);
+ str.push_back('a');
+ return str;
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, PopBack) {
+ TEST_STRING(InlineString<1>("?", 1), str.pop_back(), "");
+ TEST_STRING(InlineString<1>(), str.push_back('?'); str.pop_back(), "");
+
+ TEST_STRING(InlineString<5>("abc"), str.pop_back(), "ab");
+ TEST_STRING(InlineString<5>("abcde", 5), str.pop_back(), "abcd");
+
+#if PW_NC_TEST(PopBack_Empty)
+ PW_NC_EXPECT("PW_ASSERT\(!empty\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<0> str;
+ str.pop_back();
+ return str;
+ }();
+#endif // PW_NC_TEST
+}
+
+// append
+
+TEST(InlineString, Append_BasicString) {
+ TEST_STRING(InlineString<0>(), str.append(kEmptyCapacity0), "");
+ TEST_STRING(InlineString<10>(), str.append(kEmptyCapacity10), "");
+ TEST_STRING(InlineString<10>(), str.append(kSize5Capacity10), "12345");
+ TEST_STRING(InlineString<10>(), str.append(kSize10Capacity10), "1234567890");
+
+ TEST_STRING(InlineString<1>({'a'}), str.append(kEmptyCapacity0), "a");
+ TEST_STRING(InlineString<11>("a"), str.append(kEmptyCapacity10), "a");
+ TEST_STRING(InlineString<11>("a"), str.append(kSize5Capacity10), "a12345");
+ TEST_STRING(
+ InlineString<11>("a"), str.append(kSize10Capacity10), "a1234567890");
+
+#if PW_NC_TEST(Append_BasicString_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.append(kSize5Capacity10);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Append_Characters) {
+ TEST_STRING(InlineString<1>(), str.append(0, '1'), "");
+ TEST_STRING(InlineString<1>(), str.append(1, '1'), "1");
+ TEST_STRING(InlineString<10>(), str.append(2, '1'), "11");
+ TEST_STRING(InlineString<10>(), str.append(10, '1'), "1111111111");
+
+ TEST_STRING(InlineString<4>("Hi"), str.append(0, '!'), "Hi");
+ TEST_STRING(InlineString<4>("Hi"), str.append(1, '!'), "Hi!");
+ TEST_STRING(InlineString<6>("Hi"), str.append(2, '!'), "Hi!!");
+ TEST_STRING(InlineString<6>("Hi"), str.append(4, '!'), "Hi!!!!");
+
+#if PW_NC_TEST(Append_Characters_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.append(2, '?');
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Append_PointerSize) {
+ TEST_STRING(InlineString<0>(), str.append("", 0), "");
+ TEST_STRING(InlineString<10>(), str.append("", 0), "");
+ TEST_STRING(InlineString<1>(), str.append("?", 1), "?");
+ TEST_STRING(InlineString<10>("abc"), str.append("", 0), "abc");
+ TEST_STRING(InlineString<10>(), str.append("1234567", 1), "1");
+ TEST_STRING(InlineString<10>("abc"), str.append("1234567", 3), "abc123");
+
+#if PW_NC_TEST(Append_PointerSize_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.append("23", 2);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Append_Array) {
+ TEST_STRING(InlineString<1>(), fixed_str.append(""), "");
+ TEST_STRING(InlineString<2>(), fixed_str.append("a"), "a");
+ TEST_STRING(InlineString<6>(), fixed_str.append("12345"), "12345");
+
+ TEST_STRING(InlineString<1>({'a'}), fixed_str.append(""), "a");
+ TEST_STRING(InlineString<2>("a"), fixed_str.append("a"), "aa");
+ TEST_STRING(InlineString<6>("a"), fixed_str.append("12345"), "a12345");
+
+#if PW_NC_TEST(Append_Array_DoesNotFit)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<2> str;
+ return str.append("123");
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Append_Pointer) {
+ TEST_STRING(InlineString<0>(), str.append(kPointer0), "");
+ TEST_STRING(InlineString<10>(), str.append(kPointer10), "9876543210");
+ TEST_STRING(InlineString<10>("abc"), str.append(kPointer10 + 5), "abc43210");
+ TEST_STRING(InlineString<13>("abc"), str.append(kPointer10), "abc9876543210");
+
+#if PW_NC_TEST(Append_Pointer_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.append(kPointer10 + 8);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Append_Iterator) {
+ TEST_STRING(InlineString<0>(), str.append(kEvenNumbers0, kEvenNumbers0), "");
+ TEST_STRING(InlineString<10>(), str.append(kEvenNumbers0, kEvenNumbers0), "");
+ TEST_STRING(InlineString<10>(), str.append(kEvenNumbers0, kEvenNumbers0), "");
+ TEST_STRING(
+ InlineString<10>(), str.append(kEvenNumbers0, kEvenNumbers8), "\0\2\4\6");
+ TEST_STRING(InlineString<10>("a"),
+ str.append(kEvenNumbers0, kEvenNumbers8),
+ "a\0\2\4\6");
+
+#if PW_NC_TEST(Append_Iterator_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(current_position != string_end\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str;
+ return str.append(kEvenNumbers0, kEvenNumbers8);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Append_InitializerList) {
+ TEST_STRING(InlineString<0>(), str.append({}), "");
+ TEST_STRING(InlineString<10>(), str.append({1, 2, 3}), "\1\2\3");
+ TEST_STRING(InlineString<10>("abc"), str.append({1, 2, 3}), "abc\1\2\3");
+ TEST_STRING(InlineString<5>("abc"), str.append({'4', '5'}), "abc45");
+
+#if PW_NC_TEST(Append_InitializerList_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str.append({3});
+ }();
+#endif // PW_NC_TEST
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+TEST(InlineString, Append_StringView) {
+ TEST_STRING(InlineString<0>(), str.append(""sv), "");
+ TEST_STRING(InlineString<10>("a"), str.append(""sv), "a");
+ TEST_STRING(InlineString<10>("abc"), str.append("123"sv), "abc123");
+ TEST_STRING(InlineString<5>("abc"), str.append("45"sv), "abc45");
+
+#if PW_NC_TEST(Append_StringView_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str.append("3"sv);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, Append_StringViewSubstr) {
+ TEST_STRING(InlineString<0>(), str.append(""sv, 0), "");
+ TEST_STRING(InlineString<0>(), str.append(""sv, 0, 0), "");
+ TEST_RUNTIME_STRING(InlineString<4>("a"), str.append("123"sv, 0), "a123");
+ TEST_RUNTIME_STRING(InlineString<4>("a"), str.append("123"sv, 1, 0), "a");
+ TEST_RUNTIME_STRING(InlineString<4>("a"), str.append("123"sv, 1, 1), "a2");
+ TEST_RUNTIME_STRING(InlineString<4>("a"), str.append("123"sv, 1, 99), "a23");
+ TEST_RUNTIME_STRING(InlineString<4>("a"), str.append("123"sv, 3, 99), "a");
+
+#if PW_NC_TEST(Append_StringViewSubstr_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str.append("34"sv, 1);
+ }();
+#endif // PW_NC_TEST
+}
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+// operator+=
+
+TEST(InlineString, AppendOperator_BasicString) {
+ TEST_STRING(InlineString<1>(), str.append(0, '1'), "");
+ TEST_STRING(InlineString<1>(), str.append(1, '1'), "1");
+ TEST_STRING(InlineString<10>(), str.append(2, '1'), "11");
+ TEST_STRING(InlineString<10>(), str.append(10, '1'), "1111111111");
+
+ TEST_STRING(InlineString<4>("Hi"), str.append(0, '!'), "Hi");
+ TEST_STRING(InlineString<4>("Hi"), str.append(1, '!'), "Hi!");
+ TEST_STRING(InlineString<6>("Hi"), str.append(2, '!'), "Hi!!");
+ TEST_STRING(InlineString<6>("Hi"), str.append(4, '!'), "Hi!!!!");
+
+#if PW_NC_TEST(AppendOperator_BasicString_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.append(kSize5Capacity10);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, AppendOperator_Character) {
+ TEST_STRING(InlineString<1>(), fixed_str += '1', "1");
+ TEST_STRING(InlineString<10>(), fixed_str += '\0', "\0");
+
+ TEST_STRING(InlineString<3>("Hi"), fixed_str += '!', "Hi!");
+ TEST_STRING(InlineString<10>("Hi"), fixed_str += '!', "Hi!");
+
+#if PW_NC_TEST(AppendOperator_Characters_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(size\(\) < max_size\(\)\);");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str += '?';
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, AppendOperator_Array) {
+ TEST_STRING(InlineString<1>(), fixed_str += "", "");
+ TEST_STRING(InlineString<2>(), fixed_str += "a", "a");
+ TEST_STRING(InlineString<6>(), fixed_str += "12345", "12345");
+
+ TEST_STRING(InlineString<1>({'a'}), fixed_str += "", "a");
+ TEST_STRING(InlineString<2>("a"), fixed_str += "a", "aa");
+ TEST_STRING(InlineString<6>("a"), fixed_str += "12345", "a12345");
+
+#if PW_NC_TEST(AppendOperator_Array_DoesNotFit)
+ PW_NC_EXPECT(
+ "InlineString's capacity is too small to hold the assigned string");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str;
+ return str += "1234";
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, AppendOperator_Pointer) {
+ TEST_STRING(InlineString<0>(), fixed_str += kPointer0, "");
+ TEST_STRING(InlineString<10>(), fixed_str += kPointer10, "9876543210");
+ TEST_STRING(InlineString<10>("abc"), fixed_str += kPointer10 + 5, "abc43210");
+ TEST_STRING(
+ InlineString<13>("abc"), fixed_str += kPointer10, "abc9876543210");
+
+#if PW_NC_TEST(AppendOperator_Pointer_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1});
+ return str.append(kPointer10 + 8);
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, AppendOperator_InitializerList) {
+ TEST_STRING(InlineString<0>(), fixed_str += {}, "");
+ TEST_STRING(InlineString<10>(), (fixed_str += {1, 2, 3}), "\1\2\3");
+ TEST_STRING(InlineString<10>("abc"), (fixed_str += {1, 2, 3}), "abc\1\2\3");
+ TEST_STRING(InlineString<5>("abc"), (fixed_str += {'4', '5'}), "abc45");
+
+#if PW_NC_TEST(AppendOperator_InitializerList_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str.append({3});
+ }();
+#endif // PW_NC_TEST
+}
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+TEST(InlineString, AppendOperator_StringView) {
+ TEST_STRING(InlineString<0>(), fixed_str += ""sv, "");
+ TEST_STRING(InlineString<10>("a"), fixed_str += ""sv, "a");
+ TEST_STRING(InlineString<10>("abc"), fixed_str += "123"sv, "abc123");
+ TEST_STRING(InlineString<5>("abc"), fixed_str += "45"sv, "abc45");
+
+#if PW_NC_TEST(AppendOperator_StringView_DoesNotFit)
+ PW_NC_EXPECT("PW_ASSERT\(count <= max_size\(\) - size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<3> str({0, 1, 2});
+ return str.append("3"sv);
+ }();
+#endif // PW_NC_TEST
+}
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+TEST(InlineString, Compare) {
+ EXPECT_EQ(InlineString<10>("abb").compare(InlineString<5>("abb")), 0);
+ EXPECT_LT(InlineString<10>("abb").compare(InlineString<5>("bbb")), 0);
+ EXPECT_LT(InlineString<10>("bb").compare(InlineString<5>("bbb")), 0);
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+ static_assert(InlineString<10>("bbb").compare(InlineString<5>("bbb")) == 0,
+ "equal");
+ static_assert(InlineString<10>("abb").compare(InlineString<5>("bbb")) < 0,
+ "less");
+ static_assert(InlineString<10>("bbb").compare(InlineString<5>("abb")) > 0,
+ "greater");
+
+ static_assert(InlineString<10>("bb").compare(InlineString<5>("bbb")) < 0,
+ "less");
+ static_assert(InlineString<10>("bbb").compare(InlineString<5>("bb")) > 0,
+ "greater");
+
+ static_assert(InlineString<10>("bb").compare(InlineString<5>("abb")) > 0,
+ "less");
+ static_assert(InlineString<10>("abb").compare(InlineString<5>("bb")) < 0,
+ "greater");
+
+ static_assert(InlineString<5>("").compare(InlineString<5>("")) == 0, "equal");
+ static_assert(InlineString<5>("").compare(InlineString<5>("abc")) < 0,
+ "less");
+ static_assert(InlineString<5>("abc").compare(InlineString<5>("")) > 0,
+ "greater");
+
+ constexpr InlineBasicString<unsigned long long, 3> kUllString1(
+ {0, std::numeric_limits<unsigned long long>::max(), 0});
+ constexpr InlineBasicString<unsigned long long, 3> kUllString2(
+ {std::numeric_limits<unsigned long long>::max(), 0});
+
+ static_assert(kUllString1.compare(kUllString1) == 0, "equal");
+ static_assert(kUllString1.compare(kUllString2) < 0, "less");
+ static_assert(kUllString2.compare(kUllString1) > 0, "greater");
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+}
+
+// TODO(b/239996007): Test other pw::InlineString functions:
+//
+// - starts_with
+// - ends_with
+// - contains
+// - replace
+// - substr
+// - copy
+
+TEST(InlineString, Resize) {
+ TEST_STRING(InlineString<10>(), str.resize(0), "");
+ TEST_STRING(InlineString<10>(), str.resize(5), "\0\0\0\0\0");
+ TEST_STRING(InlineString<10>(), str.resize(10), "\0\0\0\0\0\0\0\0\0\0");
+ TEST_STRING(InlineString<10>(), str.resize(0, 'a'), "");
+ TEST_STRING(InlineString<10>(), str.resize(5, 'a'), "aaaaa");
+ TEST_STRING(InlineString<10>(), str.resize(10, 'a'), "aaaaaaaaaa");
+
+ TEST_STRING(InlineString<10>("ABCDE"), str.resize(0), "");
+ TEST_STRING(InlineString<10>("ABCDE"), str.resize(4), "ABCD");
+ TEST_STRING(InlineString<10>("ABCDE"), str.resize(5), "ABCDE");
+ TEST_STRING(InlineString<10>("ABCDE"), str.resize(10), "ABCDE\0\0\0\0\0");
+ TEST_STRING(InlineString<10>("ABCDE"), str.resize(0, 'a'), "");
+ TEST_STRING(InlineString<10>("ABCDE"), str.resize(3, 'a'), "ABC");
+ TEST_STRING(InlineString<10>("ABCDE"), str.resize(5, 'a'), "ABCDE");
+ TEST_STRING(InlineString<10>("ABCDE"), str.resize(10, 'a'), "ABCDEaaaaa");
+
+#if PW_NC_TEST(Resize_LargerThanCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(new_size <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<4> str("123");
+ str.resize(5);
+ return str;
+ }();
+#endif // PW_NC_TEST
+}
+
+TEST(InlineString, ResizeAndOverwrite) {
+ TEST_STRING(InlineString<2>(),
+ str.resize_and_overwrite([](char* out, size_t) {
+ out[0] = '\0';
+ out[1] = '?';
+ return 2;
+ }),
+ "\0?");
+ TEST_STRING(InlineString<10>("ABCDE"),
+ str.resize_and_overwrite([](char* out, size_t size) {
+ out[1] = '?';
+ for (size_t i = 5; i < size; ++i) {
+ out[i] = static_cast<char>('0' + i);
+ }
+ return size - 1; // chop off the last character
+ }),
+ "A?CDE5678");
+
+#if PW_NC_TEST(ResizeAndOverwrite_LargerThanCapacity)
+ PW_NC_EXPECT("PW_ASSERT\(static_cast<size_t>\(new_size\) <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<4> str("123");
+ str.resize_and_overwrite([](char*, size_t) { return 5; });
+ return str;
+ }();
+#elif PW_NC_TEST(ResizeAndOverwrite_NegativeSize)
+ PW_NC_EXPECT("PW_ASSERT\(static_cast<size_t>\(new_size\) <= max_size\(\)\)");
+ [[maybe_unused]] constexpr auto fail = [] {
+ InlineString<4> str("123");
+ str.resize_and_overwrite([](char*, size_t) { return -1; });
+ return str;
+ }();
+#endif // PW_NC_TEST
+}
+
+// TODO(b/239996007): Test other pw::InlineString functions:
+// - swap
+
+//
+// Search
+//
+
+// TODO(b/239996007): Test search functions.
+
+// TODO(b/239996007): Test operator+.
+
+TEST(InlineString, ComparisonOperators_InlineString) {
+ EXPECT_EQ(InlineString<10>("a"), InlineString<10>("a"));
+ EXPECT_NE(InlineString<10>("a"), InlineString<10>("b"));
+ EXPECT_LT(InlineString<10>("a"), InlineString<10>("b"));
+ EXPECT_LE(InlineString<10>("a"), InlineString<10>("b"));
+ EXPECT_GT(InlineString<10>("b"), InlineString<10>("a"));
+ EXPECT_GE(InlineString<10>("b"), InlineString<10>("a"));
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+ static_assert(InlineString<10>() == InlineString<10>(), "equal"); // NOLINT
+ static_assert(InlineString<10>("abc") == InlineString<5>("abc"), "equal");
+ static_assert(InlineString<1>({'a'}) == InlineString<10>("a"), "equal");
+ static_assert(!(InlineString<10>("?") == InlineString<10>()), // NOLINT
+ "equal");
+
+ static_assert(InlineString<10>() != InlineString<10>("a"), // NOLINT
+ "not equal");
+ static_assert(InlineString<10>("") != InlineString<5>("abc"), "not equal");
+ static_assert(InlineString<1>({'\0'}) != InlineString<10>(""), "not equal");
+ static_assert(!(InlineString<1>({'\0'}) != InlineString<10>("\0"sv)),
+ "not equal");
+
+ static_assert(InlineString<10>() < InlineString<10>("a"), "less");
+ static_assert(InlineString<10>("ab") < InlineString<5>("abc"), "less");
+ static_assert(InlineString<1>({'\0'}) < InlineString<10>("\1\0"), "less");
+ static_assert(!(InlineString<1>({'\2'}) < InlineString<10>("\1\0")), "less");
+
+ static_assert(InlineString<10>() <= InlineString<10>("a"), "less equal");
+ static_assert(InlineString<10>("a") <= InlineString<10>("a"), "less equal");
+ static_assert(InlineString<10>("ab") <= InlineString<5>("abc"), "less equal");
+ static_assert(InlineString<10>("abc") <= InlineString<5>("abc"),
+ "less equal");
+ static_assert(InlineString<1>({'\0'}) <= InlineString<10>("\1\0"),
+ "less equal");
+ static_assert(InlineString<2>({'\1', '\0'}) <= InlineString<10>("\1\0"sv),
+ "less equal");
+ static_assert(!(InlineString<2>({'\2', '\0'}) <= InlineString<10>("\1\0"sv)),
+ "less equal");
+
+ static_assert(InlineString<10>("?") > InlineString<10>(""), "greater");
+ static_assert(InlineString<10>("abc") > InlineString<5>("ab"), "greater");
+ static_assert(InlineString<2>({'\1', '\0'}) > InlineString<10>("\1"),
+ "greater");
+ static_assert(!(InlineString<2>({'\1', '\0'}) > InlineString<10>("\2")),
+ "greater");
+
+ static_assert(InlineString<10>("?") >= InlineString<10>(""), "greater equal");
+ static_assert(InlineString<10>("?") >= InlineString<10>("?"),
+ "greater equal");
+ static_assert(InlineString<10>("abc") >= InlineString<5>("ab"),
+ "greater equal");
+ static_assert(InlineString<10>("abc") >= InlineString<5>("abc"),
+ "greater equal");
+ static_assert(InlineString<2>({'\1', '\0'}) >= InlineString<10>("\1"),
+ "greater equal");
+ static_assert(InlineString<2>({'\1', '\0'}) >= InlineString<10>("\1\0"),
+ "greater equal");
+ static_assert(!(InlineString<3>("\0\0") >= InlineString<10>("\1\0")),
+ "greater equal");
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+}
+
+TEST(InlineString, ComparisonOperators_NullTerminatedString) {
+ EXPECT_EQ(InlineString<10>("a"), "a");
+ EXPECT_EQ("a", InlineString<10>("a"));
+
+ EXPECT_NE(InlineString<10>("a"), "b");
+ EXPECT_NE("a", InlineString<10>("b"));
+
+ EXPECT_LT(InlineString<10>("a"), "b");
+ EXPECT_LT("a", InlineString<10>("b"));
+
+ EXPECT_LE(InlineString<10>("a"), "b");
+ EXPECT_LE("a", InlineString<10>("b"));
+ EXPECT_LE(InlineString<10>("a"), "a");
+ EXPECT_LE("a", InlineString<10>("a"));
+
+ EXPECT_GT(InlineString<10>("b"), "a");
+ EXPECT_GT("b", InlineString<10>("a"));
+
+ EXPECT_GE(InlineString<10>("b"), "a");
+ EXPECT_GE("b", InlineString<10>("a"));
+ EXPECT_GE(InlineString<10>("a"), "a");
+ EXPECT_GE("a", InlineString<10>("a"));
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+ static_assert(InlineString<10>() == "", "equal"); // NOLINT
+ static_assert("" == InlineString<10>(), "equal"); // NOLINT
+ static_assert(InlineString<10>("abc") == "abc", "equal");
+ static_assert("abc" == InlineString<5>("abc"), "equal");
+
+ static_assert("" != InlineString<10>("a"), "not equal"); // NOLINT
+ static_assert(InlineString<10>("a") != "", "not equal"); // NOLINT
+ static_assert(InlineString<10>("") != "abc", "not equal"); // NOLINT
+ static_assert("" != InlineString<5>("abc"), "not equal"); // NOLINT
+
+ static_assert(InlineString<10>() < "a", "less");
+ static_assert("" < InlineString<10>("a"), "less");
+ static_assert(InlineString<10>("ab") < "abc", "less");
+ static_assert("ab" < InlineString<5>("abc"), "less");
+
+ static_assert(InlineString<10>() <= "a", "less equal");
+ static_assert("" <= InlineString<10>("a"), "less equal");
+ static_assert(InlineString<10>("a") <= "a", "less equal");
+ static_assert("a" <= InlineString<10>("a"), "less equal");
+
+ static_assert(InlineString<10>("ab") <= "abc", "less equal");
+ static_assert("ab" <= InlineString<5>("abc"), "less equal");
+ static_assert(InlineString<10>("abc") <= "abc", "less equal");
+ static_assert("abc" <= InlineString<5>("abc"), "less equal");
+
+ static_assert(InlineString<10>("?") > "", "greater");
+ static_assert("?" > InlineString<10>(""), "greater");
+ static_assert(InlineString<10>("abc") > "ab", "greater");
+ static_assert("abc" > InlineString<5>("ab"), "greater");
+
+ static_assert(InlineString<10>("?") >= "", "greater equal");
+ static_assert("?" >= InlineString<10>(""), "greater equal");
+ static_assert(InlineString<10>("abc") >= "ab", "greater equal");
+ static_assert("abc" >= InlineString<5>("ab"), "greater equal");
+ static_assert(InlineString<10>("abc") >= "abc", "greater equal");
+ static_assert("abc" >= InlineString<5>("abc"), "greater equal");
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+}
+
+// Test instantiating an InlineBasicString with different character types.
+
+#if __cpp_constexpr >= 201603L // constexpr lambdas are required
+#define PW_STRING_WRAP_TEST_EXPANSION(expr) \
+ do { \
+ expr; \
+ } while (0)
+#else
+#define PW_STRING_WRAP_TEST_EXPANSION(expr)
+#endif // __cpp_constexpr >= 201603L
+
+#define TEST_FOR_TYPES(test_macro, ...) \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(char, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(unsigned char, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(signed char, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(short, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(int, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(unsigned, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(long, __VA_ARGS__)); \
+ PW_STRING_WRAP_TEST_EXPANSION(test_macro(long long, __VA_ARGS__))
+
+TEST(BasicStrings, Empty) {
+#define BASIC_STRINGS_EMPTY(type, capacity) \
+ constexpr InlineBasicString<type, capacity> string; \
+ static_assert(string.empty(), "empty"); \
+ static_assert(string.size() == 0u, "size 0"); /* NOLINT */ \
+ static_assert(string.c_str()[0] == static_cast<type>(0), "null"); \
+ static_assert(std::basic_string_view<type>(string).empty())
+
+ TEST_FOR_TYPES(BASIC_STRINGS_EMPTY, 0);
+ TEST_FOR_TYPES(BASIC_STRINGS_EMPTY, 1);
+ TEST_FOR_TYPES(BASIC_STRINGS_EMPTY, 50);
+
+#undef BASIC_STRINGS_EMPTY
+}
+
+TEST(BasicStrings, InitializerList) {
+#define BASIC_STRINGS_INITIALIZER_LIST(type, capacity) \
+ constexpr InlineBasicString<type, capacity> string({0, 1, 2, 3, 4}); \
+ static_assert(string.size() == 5u, "size 5"); \
+ static_assert(string[0] == static_cast<type>(0), "0"); \
+ static_assert(string[1] == static_cast<type>(1), "1"); \
+ static_assert(string[2] == static_cast<type>(2), "2"); \
+ static_assert(string[3] == static_cast<type>(3), "3"); \
+ static_assert(string[4] == static_cast<type>(4), "4"); \
+ static_assert(string.c_str()[0] == static_cast<type>(0), "null"); \
+ static_assert(std::basic_string_view<type>(string).size() == 5)
+
+ TEST_FOR_TYPES(BASIC_STRINGS_INITIALIZER_LIST, 5);
+ TEST_FOR_TYPES(BASIC_STRINGS_INITIALIZER_LIST, 10);
+ TEST_FOR_TYPES(BASIC_STRINGS_INITIALIZER_LIST, 50);
+
+#undef BASIC_STRINGS_INITIALIZER_LIST
+}
+
+TEST(BasicStrings, VariousOperations) {
+#define BASIC_STRINGS_VARIOUS_OPERATIONS(type, capacity) \
+ static constexpr type kOne[2] = {1, 0}; \
+ constexpr auto string = [] { \
+ InlineBasicString<type, capacity> str({0}); \
+ str.append(kOne); \
+ str.append({2, 10, 99}); \
+ str.resize(3); \
+ str.push_back(static_cast<int>(3)); \
+ str.append(InlineBasicString<type, 2>({4})); \
+ return str; \
+ }(); \
+ static_assert(string.size() == 5); \
+ static_assert(string[0] == static_cast<type>(0), "0"); \
+ static_assert(string[1] == static_cast<type>(1), "1"); \
+ static_assert(string[2] == static_cast<type>(2), "2"); \
+ static_assert(string[3] == static_cast<type>(3), "3"); \
+ static_assert(string[4] == static_cast<type>(4), "4"); \
+ static_assert(string.c_str()[0] == static_cast<type>(0), "null")
+
+ TEST_FOR_TYPES(BASIC_STRINGS_VARIOUS_OPERATIONS, 5);
+ TEST_FOR_TYPES(BASIC_STRINGS_VARIOUS_OPERATIONS, 10);
+ TEST_FOR_TYPES(BASIC_STRINGS_VARIOUS_OPERATIONS, 50);
+
+#undef BASIC_STRINGS_VARIOUS_OPERATIONS
+}
+
+} // namespace pw
diff --git a/pw_string/to_string_test.cc b/pw_string/to_string_test.cc
index 347010acd..39a09fbdd 100644
--- a/pw_string/to_string_test.cc
+++ b/pw_string/to_string_test.cc
@@ -22,6 +22,7 @@
#include "gtest/gtest.h"
#include "pw_status/status.h"
+#include "pw_string/internal/config.h"
#include "pw_string/type_to_string.h"
namespace pw {
@@ -32,14 +33,14 @@ struct CustomType {
static constexpr const char* kToString = "This is a CustomType";
- CustomType() = default;
+ CustomType() : a(0), b(0) {}
// Non-copyable to verify that ToString doesn't copy it.
CustomType(const CustomType&) = delete;
CustomType& operator=(const CustomType&) = delete;
};
-StatusWithSize ToString(const CustomType&, std::span<char> buffer) {
+StatusWithSize ToString(const CustomType&, span<char> buffer) {
int result =
std::snprintf(buffer.data(), buffer.size(), CustomType::kToString);
if (result < 0) {
@@ -106,25 +107,36 @@ TEST(ToString, ScopedEnum) {
}
TEST(ToString, Integer_EmptyBuffer_WritesNothing) {
- auto result = ToString(-1234, std::span(buffer, 0));
+ auto result = ToString(-1234, span(buffer, 0));
EXPECT_EQ(0u, result.size());
EXPECT_EQ(Status::ResourceExhausted(), result.status());
}
TEST(ToString, Integer_BufferTooSmall_WritesNullTerminator) {
- auto result = ToString(-1234, std::span(buffer, 5));
+ auto result = ToString(-1234, span(buffer, 5));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer);
}
TEST(ToString, Float) {
- EXPECT_EQ(1u, ToString(0.0f, buffer).size());
- EXPECT_STREQ("0", buffer);
- EXPECT_EQ(3u, ToString(INFINITY, buffer).size());
- EXPECT_STREQ("inf", buffer);
- EXPECT_EQ(4u, ToString(-NAN, buffer).size());
- EXPECT_STREQ("-NaN", buffer);
+ if (string::internal::config::kEnableDecimalFloatExpansion) {
+ EXPECT_EQ(5u, ToString(0.0f, buffer).size());
+ EXPECT_STREQ("0.000", buffer);
+ EXPECT_EQ(6u, ToString(33.444, buffer).size());
+ EXPECT_STREQ("33.444", buffer);
+ EXPECT_EQ(3u, ToString(INFINITY, buffer).size());
+ EXPECT_STREQ("inf", buffer);
+ EXPECT_EQ(3u, ToString(NAN, buffer).size());
+ EXPECT_STREQ("nan", buffer);
+ } else {
+ EXPECT_EQ(1u, ToString(0.0f, buffer).size());
+ EXPECT_STREQ("0", buffer);
+ EXPECT_EQ(3u, ToString(INFINITY, buffer).size());
+ EXPECT_STREQ("inf", buffer);
+ EXPECT_EQ(4u, ToString(-NAN, buffer).size());
+ EXPECT_STREQ("-NaN", buffer);
+ }
}
TEST(ToString, Pointer_NonNull_WritesValue) {
@@ -241,7 +253,7 @@ TEST(ToString, StringView) {
TEST(ToString, StringView_TooSmall_Truncates) {
std::string_view view = "kale!";
- EXPECT_EQ(3u, ToString(view, std::span(buffer, 4)).size());
+ EXPECT_EQ(3u, ToString(view, span(buffer, 4)).size());
EXPECT_STREQ("kal", buffer);
}
@@ -250,9 +262,8 @@ TEST(ToString, StringView_EmptyBuffer_WritesNothing) {
char test_buffer[sizeof(kOriginal)];
std::memcpy(test_buffer, kOriginal, sizeof(kOriginal));
- EXPECT_EQ(
- 0u,
- ToString(std::string_view("Hello!"), std::span(test_buffer, 0)).size());
+ EXPECT_EQ(0u,
+ ToString(std::string_view("Hello!"), span(test_buffer, 0)).size());
ASSERT_EQ(0, std::memcmp(kOriginal, test_buffer, sizeof(kOriginal)));
}
diff --git a/pw_string/type_to_string.cc b/pw_string/type_to_string.cc
index 8b70dc3c1..37311c4da 100644
--- a/pw_string/type_to_string.cc
+++ b/pw_string/type_to_string.cc
@@ -47,7 +47,7 @@ constexpr std::array<uint64_t, 20> kPowersOf10{
10000000000000000000ull, // 10^19
};
-StatusWithSize HandleExhaustedBuffer(std::span<char> buffer) {
+StatusWithSize HandleExhaustedBuffer(span<char> buffer) {
if (!buffer.empty()) {
buffer[0] = '\0';
}
@@ -60,7 +60,8 @@ uint_fast8_t DecimalDigitCount(uint64_t integer) {
// This fancy piece of code takes the log base 2, then approximates the
// change-of-base formula by multiplying by 1233 / 4096.
// TODO(hepler): Replace __builtin_clzll with std::countl_zeros in C++20.
- const uint_fast8_t log_10 = (64 - __builtin_clzll(integer | 1)) * 1233 >> 12;
+ const uint_fast8_t log_10 = static_cast<uint_fast8_t>(
+ (64 - __builtin_clzll(integer | 1)) * 1233 >> 12);
// Adjust the estimated log base 10 by comparing against the power of 10.
return log_10 + (integer < kPowersOf10[log_10] ? 0u : 1u);
@@ -72,7 +73,7 @@ uint_fast8_t DecimalDigitCount(uint64_t integer) {
// DecimalDigitCount and its table). I didn't measure performance, but I don't
// think std::to_chars will be faster, so I kept this implementation for now.
template <>
-StatusWithSize IntToString(uint64_t value, std::span<char> buffer) {
+StatusWithSize IntToString(uint64_t value, span<char> buffer) {
constexpr uint32_t base = 10;
constexpr uint32_t max_uint32_base_power = 1'000'000'000;
constexpr uint_fast8_t max_uint32_base_power_exponent = 9;
@@ -93,7 +94,7 @@ StatusWithSize IntToString(uint64_t value, std::span<char> buffer) {
// 64-bit division is slow on 32-bit platforms, so print large numbers in
// 32-bit chunks to minimize the number of 64-bit divisions.
if (value <= std::numeric_limits<uint32_t>::max()) {
- lower_digits = value;
+ lower_digits = static_cast<uint32_t>(value);
digit_count = remaining;
} else {
lower_digits = value % max_uint32_base_power;
@@ -111,7 +112,7 @@ StatusWithSize IntToString(uint64_t value, std::span<char> buffer) {
}
StatusWithSize IntToHexString(uint64_t value,
- std::span<char> buffer,
+ span<char> buffer,
uint_fast8_t min_width) {
const uint_fast8_t digits = std::max(HexDigitCount(value), min_width);
@@ -129,7 +130,7 @@ StatusWithSize IntToHexString(uint64_t value,
}
template <>
-StatusWithSize IntToString(int64_t value, std::span<char> buffer) {
+StatusWithSize IntToString(int64_t value, span<char> buffer) {
if (value >= 0) {
return IntToString<uint64_t>(value, buffer);
}
@@ -146,9 +147,7 @@ StatusWithSize IntToString(int64_t value, std::span<char> buffer) {
return HandleExhaustedBuffer(buffer);
}
-// TODO(hepler): Look into using the float overload of std::to_chars when it is
-// available.
-StatusWithSize FloatAsIntToString(float value, std::span<char> buffer) {
+StatusWithSize FloatAsIntToString(float value, span<char> buffer) {
// If it's finite and fits in an int64_t, print it as a rounded integer.
if (std::isfinite(value) &&
std::abs(value) <
@@ -170,11 +169,11 @@ StatusWithSize FloatAsIntToString(float value, std::span<char> buffer) {
return HandleExhaustedBuffer(buffer);
}
-StatusWithSize BoolToString(bool value, std::span<char> buffer) {
+StatusWithSize BoolToString(bool value, span<char> buffer) {
return CopyEntireStringOrNull(value ? "true" : "false", buffer);
}
-StatusWithSize PointerToString(const void* pointer, std::span<char> buffer) {
+StatusWithSize PointerToString(const void* pointer, span<char> buffer) {
if (pointer == nullptr) {
return CopyEntireStringOrNull(kNullPointerString, buffer);
}
@@ -182,7 +181,7 @@ StatusWithSize PointerToString(const void* pointer, std::span<char> buffer) {
}
StatusWithSize CopyEntireStringOrNull(const std::string_view& value,
- std::span<char> buffer) {
+ span<char> buffer) {
if (value.size() >= buffer.size()) {
return HandleExhaustedBuffer(buffer);
}
diff --git a/pw_string/type_to_string_test.cc b/pw_string/type_to_string_test.cc
index ffadc27db..f9fe670a3 100644
--- a/pw_string/type_to_string_test.cc
+++ b/pw_string/type_to_string_test.cc
@@ -107,47 +107,47 @@ class TestWithBuffer : public ::testing::Test {
class IntToStringTest : public TestWithBuffer {};
TEST_F(IntToStringTest, Unsigned_EmptyBuffer_WritesNothing) {
- auto result = IntToString(9u, std::span(buffer_, 0));
+ auto result = IntToString(9u, span(buffer_, 0));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ(kStartingString, buffer_);
}
TEST_F(IntToStringTest, Unsigned_TooSmall_1Char_OnlyNullTerminates) {
- auto result = IntToString(9u, std::span(buffer_, 1));
+ auto result = IntToString(9u, span(buffer_, 1));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(IntToStringTest, Unsigned_TooSmall_2Chars_OnlyNullTerminates) {
- auto result = IntToString(10u, std::span(buffer_, 2));
+ auto result = IntToString(10u, span(buffer_, 2));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(IntToStringTest, Unsigned_TooSmall_3Chars_OnlyNullTerminates) {
- auto result = IntToString(123u, std::span(buffer_, 3));
+ auto result = IntToString(123u, span(buffer_, 3));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(IntToStringTest, Unsigned_1Char_FitsExactly) {
- auto result = IntToString(0u, std::span(buffer_, 2));
+ auto result = IntToString(0u, span(buffer_, 2));
EXPECT_EQ(1u, result.size());
EXPECT_TRUE(result.ok());
EXPECT_STREQ("0", buffer_);
- result = IntToString(9u, std::span(buffer_, 2));
+ result = IntToString(9u, span(buffer_, 2));
EXPECT_EQ(1u, result.size());
EXPECT_TRUE(result.ok());
EXPECT_STREQ("9", buffer_);
}
TEST_F(IntToStringTest, Unsigned_2Chars_FitsExactly) {
- auto result = IntToString(10u, std::span(buffer_, 3));
+ auto result = IntToString(10u, span(buffer_, 3));
EXPECT_EQ(2u, result.size());
EXPECT_STREQ("10", buffer_);
}
@@ -155,44 +155,44 @@ TEST_F(IntToStringTest, Unsigned_2Chars_FitsExactly) {
TEST_F(IntToStringTest, Unsigned_MaxFitsExactly) {
EXPECT_EQ(20u,
IntToString(std::numeric_limits<uint64_t>::max(),
- std::span(buffer_, sizeof(kUint64Max)))
+ span(buffer_, sizeof(kUint64Max)))
.size());
EXPECT_STREQ(kUint64Max, buffer_);
}
TEST_F(IntToStringTest, SignedPositive_EmptyBuffer_WritesNothing) {
- auto result = IntToString(9, std::span(buffer_, 0));
+ auto result = IntToString(9, span(buffer_, 0));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ(kStartingString, buffer_);
}
TEST_F(IntToStringTest, SignedPositive_TooSmall_NullTerminates) {
- auto result = IntToString(9, std::span(buffer_, 1));
+ auto result = IntToString(9, span(buffer_, 1));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(IntToStringTest, SignedPositive_TooSmall_DoesNotWritePastEnd) {
- EXPECT_EQ(0u, IntToString(9, std::span(buffer_, 1)).size());
+ EXPECT_EQ(0u, IntToString(9, span(buffer_, 1)).size());
EXPECT_EQ(0, std::memcmp("\0@#$%^&*()!@#$%^&*()", buffer_, sizeof(buffer_)));
}
TEST_F(IntToStringTest, SignedPositive_1Char_FitsExactly) {
- auto result = IntToString(0, std::span(buffer_, 2));
+ auto result = IntToString(0, span(buffer_, 2));
EXPECT_EQ(1u, result.size());
EXPECT_TRUE(result.ok());
EXPECT_STREQ("0", buffer_);
- result = IntToString(9, std::span(buffer_, 2));
+ result = IntToString(9, span(buffer_, 2));
EXPECT_EQ(1u, result.size());
EXPECT_TRUE(result.ok());
EXPECT_STREQ("9", buffer_);
}
TEST_F(IntToStringTest, SignedPositive_2Chars_FitsExactly) {
- auto result = IntToString(10, std::span(buffer_, 4));
+ auto result = IntToString(10, span(buffer_, 4));
EXPECT_EQ(2u, result.size());
EXPECT_TRUE(result.ok());
EXPECT_STREQ("10", buffer_);
@@ -200,20 +200,20 @@ TEST_F(IntToStringTest, SignedPositive_2Chars_FitsExactly) {
TEST_F(IntToStringTest, SignedPositive_MaxFitsExactly) {
auto result = IntToString(std::numeric_limits<int64_t>::max(),
- std::span(buffer_, sizeof(kInt64Min)));
+ span(buffer_, sizeof(kInt64Min)));
EXPECT_EQ(19u, result.size());
EXPECT_STREQ(kInt64Max, buffer_);
}
TEST_F(IntToStringTest, SignedNegative_EmptyBuffer_WritesNothing) {
- auto result = IntToString(-9, std::span(buffer_, 0));
+ auto result = IntToString(-9, span(buffer_, 0));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ(kStartingString, buffer_);
}
TEST_F(IntToStringTest, SignedNegative_TooSmall_NullTerminates) {
- auto result = IntToString(-9, std::span(buffer_, 1));
+ auto result = IntToString(-9, span(buffer_, 1));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
@@ -221,25 +221,25 @@ TEST_F(IntToStringTest, SignedNegative_TooSmall_NullTerminates) {
TEST_F(IntToStringTest, SignedNegative_TooSmall_DoesNotWritePastEnd) {
// Note that two \0 are written due to the unsigned IntToString call.
- EXPECT_EQ(0u, IntToString(-9, std::span(buffer_, 2)).size());
+ EXPECT_EQ(0u, IntToString(-9, span(buffer_, 2)).size());
EXPECT_EQ(0, std::memcmp("\0\0#$%^&*()!@#$%^&*()", buffer_, sizeof(buffer_)));
}
TEST_F(IntToStringTest, SignedNegative_FitsExactly) {
- auto result = IntToString(-9, std::span(buffer_, 3));
+ auto result = IntToString(-9, span(buffer_, 3));
EXPECT_EQ(2u, result.size());
EXPECT_STREQ("-9", buffer_);
- result = IntToString(-99, std::span(buffer_, 4));
+ result = IntToString(-99, span(buffer_, 4));
EXPECT_EQ(3u, result.size());
EXPECT_STREQ("-99", buffer_);
- result = IntToString(-123, std::span(buffer_, 5));
+ result = IntToString(-123, span(buffer_, 5));
EXPECT_EQ(4u, result.size());
EXPECT_STREQ("-123", buffer_);
}
TEST_F(IntToStringTest, SignedNegative_MinFitsExactly) {
auto result = IntToString(std::numeric_limits<int64_t>::min(),
- std::span(buffer_, sizeof(kInt64Min)));
+ span(buffer_, sizeof(kInt64Min)));
EXPECT_EQ(20u, result.size());
EXPECT_STREQ(kInt64Min, buffer_);
}
@@ -310,14 +310,14 @@ TEST_F(IntToHexStringTest, Uint64Max) {
}
TEST_F(IntToHexStringTest, EmptyBuffer_WritesNothing) {
- auto result = IntToHexString(0xbeef, std::span(buffer_, 0));
+ auto result = IntToHexString(0xbeef, span(buffer_, 0));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ(kStartingString, buffer_);
}
TEST_F(IntToHexStringTest, TooSmall_Truncates) {
- auto result = IntToHexString(0xbeef, std::span(buffer_, 3));
+ auto result = IntToHexString(0xbeef, span(buffer_, 3));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
@@ -387,21 +387,21 @@ TEST_F(FloatAsIntToStringTest, SmallerThanInteger) {
}
TEST_F(FloatAsIntToStringTest, TooSmall_Numeric_NullTerminates) {
- auto result = FloatAsIntToString(-3.14e20f, std::span(buffer_, 1));
+ auto result = FloatAsIntToString(-3.14e20f, span(buffer_, 1));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(FloatAsIntToStringTest, TooSmall_Infinity_NullTerminates) {
- auto result = FloatAsIntToString(-INFINITY, std::span(buffer_, 3));
+ auto result = FloatAsIntToString(-INFINITY, span(buffer_, 3));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(FloatAsIntToStringTest, TooSmall_NaN_NullTerminates) {
- auto result = FloatAsIntToString(NAN, std::span(buffer_, 2));
+ auto result = FloatAsIntToString(NAN, span(buffer_, 2));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
@@ -423,28 +423,28 @@ TEST_F(CopyStringOrNullTest, EmptyStringView_WritesNullTerminator) {
}
TEST_F(CopyStringOrNullTest, EmptyBuffer_WritesNothing) {
- auto result = CopyStringOrNull("Hello", std::span(buffer_, 0));
+ auto result = CopyStringOrNull("Hello", span(buffer_, 0));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ(kStartingString, buffer_);
}
TEST_F(CopyStringOrNullTest, TooSmall_Truncates) {
- auto result = CopyStringOrNull("Hi!", std::span(buffer_, 3));
+ auto result = CopyStringOrNull("Hi!", span(buffer_, 3));
EXPECT_EQ(2u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("Hi", buffer_);
}
TEST_F(CopyStringOrNullTest, ExactFit) {
- auto result = CopyStringOrNull("Hi!", std::span(buffer_, 4));
+ auto result = CopyStringOrNull("Hi!", span(buffer_, 4));
EXPECT_EQ(3u, result.size());
EXPECT_TRUE(result.ok());
EXPECT_STREQ("Hi!", buffer_);
}
TEST_F(CopyStringOrNullTest, NullTerminatorsInString) {
- ASSERT_EQ(4u, CopyStringOrNull("\0!\0\0"sv, std::span(buffer_, 5)).size());
+ ASSERT_EQ(4u, CopyStringOrNull("\0!\0\0"sv, span(buffer_, 5)).size());
EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
}
@@ -462,36 +462,35 @@ TEST_F(CopyEntireStringOrNullTest, EmptyStringView_WritesNullTerminator) {
}
TEST_F(CopyEntireStringOrNullTest, EmptyBuffer_WritesNothing) {
- auto result = CopyEntireStringOrNull("Hello", std::span(buffer_, 0));
+ auto result = CopyEntireStringOrNull("Hello", span(buffer_, 0));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ(kStartingString, buffer_);
}
TEST_F(CopyEntireStringOrNullTest, TooSmall_WritesNothing) {
- auto result = CopyEntireStringOrNull("Hi!", std::span(buffer_, 3));
+ auto result = CopyEntireStringOrNull("Hi!", span(buffer_, 3));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(CopyEntireStringOrNullTest, ExactFit) {
- auto result = CopyEntireStringOrNull("Hi!", std::span(buffer_, 4));
+ auto result = CopyEntireStringOrNull("Hi!", span(buffer_, 4));
EXPECT_EQ(3u, result.size());
EXPECT_TRUE(result.ok());
EXPECT_STREQ("Hi!", buffer_);
}
TEST_F(CopyEntireStringOrNullTest, NullTerminatorsInString) {
- ASSERT_EQ(4u,
- CopyEntireStringOrNull("\0!\0\0"sv, std::span(buffer_, 5)).size());
+ ASSERT_EQ(4u, CopyEntireStringOrNull("\0!\0\0"sv, span(buffer_, 5)).size());
EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
}
class PointerToStringTest : public TestWithBuffer {};
TEST_F(PointerToStringTest, Nullptr_WritesNull) {
- EXPECT_EQ(6u, PointerToString(nullptr, std::span(buffer_, 7)).size());
+ EXPECT_EQ(6u, PointerToString(nullptr, span(buffer_, 7)).size());
EXPECT_STREQ("(null)", buffer_);
}
@@ -504,32 +503,32 @@ TEST_F(PointerToStringTest, WritesAddress) {
class BoolToStringTest : public TestWithBuffer {};
TEST_F(BoolToStringTest, ExactFit) {
- EXPECT_EQ(4u, BoolToString(true, std::span(buffer_, 5)).size());
+ EXPECT_EQ(4u, BoolToString(true, span(buffer_, 5)).size());
EXPECT_STREQ("true", buffer_);
- EXPECT_EQ(5u, BoolToString(false, std::span(buffer_, 6)).size());
+ EXPECT_EQ(5u, BoolToString(false, span(buffer_, 6)).size());
EXPECT_STREQ("false", buffer_);
}
TEST_F(BoolToStringTest, True_TooSmall_WritesNullTerminator) {
- auto result = BoolToString(true, std::span(buffer_, 4));
+ auto result = BoolToString(true, span(buffer_, 4));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(BoolToStringTest, False_TooSmall_WritesNullTerminator) {
- auto result = BoolToString(false, std::span(buffer_, 5));
+ auto result = BoolToString(false, span(buffer_, 5));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("", buffer_);
}
TEST_F(BoolToStringTest, EmptyBuffer_WritesNothing) {
- EXPECT_EQ(0u, BoolToString(true, std::span(buffer_, 0)).size());
+ EXPECT_EQ(0u, BoolToString(true, span(buffer_, 0)).size());
EXPECT_STREQ(kStartingString, buffer_);
- EXPECT_EQ(0u, BoolToString(false, std::span(buffer_, 0)).size());
+ EXPECT_EQ(0u, BoolToString(false, span(buffer_, 0)).size());
EXPECT_STREQ(kStartingString, buffer_);
}
diff --git a/pw_string/util_test.cc b/pw_string/util_test.cc
index b22749cfe..9c1a7d7e9 100644
--- a/pw_string/util_test.cc
+++ b/pw_string/util_test.cc
@@ -14,6 +14,8 @@
#include "pw_string/util.h"
+#include <cstring>
+
#include "gtest/gtest.h"
namespace pw::string {
@@ -116,30 +118,168 @@ TEST_F(CopyTest, EmptyStringView_WritesNullTerminator) {
}
TEST_F(CopyTest, EmptyBuffer_WritesNothing) {
- auto result = Copy("Hello", std::span(buffer_, 0));
+ auto result = Copy("Hello", span(buffer_, 0));
EXPECT_EQ(0u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ(kStartingString, buffer_);
}
TEST_F(CopyTest, TooSmall_Truncates) {
- auto result = Copy("Hi!", std::span(buffer_, 3));
+ auto result = Copy("Hi!", span(buffer_, 3));
EXPECT_EQ(2u, result.size());
EXPECT_FALSE(result.ok());
EXPECT_STREQ("Hi", buffer_);
}
TEST_F(CopyTest, ExactFit) {
- auto result = Copy("Hi!", std::span(buffer_, 4));
+ auto result = Copy("Hi!", span(buffer_, 4));
EXPECT_EQ(3u, result.size());
EXPECT_TRUE(result.ok());
EXPECT_STREQ("Hi!", buffer_);
}
TEST_F(CopyTest, NullTerminatorsInString) {
- ASSERT_EQ(4u, Copy("\0!\0\0"sv, std::span(buffer_, 5)).size());
+ ASSERT_EQ(4u, Copy("\0!\0\0"sv, span(buffer_, 5)).size());
EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
}
+class InlineStringUtilTest : public ::testing::Test {
+ protected:
+ InlineString<5> string_;
+};
+
+TEST_F(InlineStringUtilTest, Assign_EmptyStringView_WritesNullTerminator) {
+ EXPECT_EQ(OkStatus(), Assign(string_, ""));
+ EXPECT_EQ(string_, "");
+}
+
+TEST_F(InlineStringUtilTest, Assign_EmptyBuffer_WritesNothing) {
+ InlineString<0> zero_capacity;
+ EXPECT_EQ(Status::ResourceExhausted(), Assign(zero_capacity, "Hello"));
+ EXPECT_TRUE(zero_capacity.empty());
+ EXPECT_EQ(zero_capacity.c_str()[0], '\0');
+}
+
+TEST_F(InlineStringUtilTest, Assign_TooSmall_Truncates) {
+ EXPECT_EQ(Status::ResourceExhausted(), Assign(string_, "12345HELLO?"));
+ EXPECT_EQ("12345", string_);
+}
+
+TEST_F(InlineStringUtilTest, Assign_ExactFit) {
+ EXPECT_EQ(OkStatus(), Assign(string_, "12345"));
+ EXPECT_EQ("12345", string_);
+}
+
+TEST_F(InlineStringUtilTest, Assign_NullTerminatorsInString) {
+ EXPECT_EQ(OkStatus(), Assign(string_, "\0!\0\0\0"sv));
+ EXPECT_EQ("\0!\0\0\0"sv, string_);
+}
+
+TEST_F(InlineStringUtilTest, Assign_ExistingContent_Replaces) {
+ string_ = "12345";
+ EXPECT_EQ(OkStatus(), Assign(string_, ""));
+ EXPECT_EQ("", string_);
+}
+
+TEST_F(InlineStringUtilTest, Assign_ExistingContent_ExactFit) {
+ string_.append("yo");
+ EXPECT_EQ(OkStatus(), Assign(string_, "12345"));
+ EXPECT_EQ("12345", string_);
+}
+
+TEST_F(InlineStringUtilTest, Assign_ExistingContent_Truncates) {
+ string_.append("yo");
+ EXPECT_EQ(Status::ResourceExhausted(), Assign(string_, "1234567"));
+ EXPECT_EQ("12345", string_);
+}
+
+TEST_F(InlineStringUtilTest, Append_EmptyStringView_WritesNullTerminator) {
+ EXPECT_EQ(OkStatus(), Append(string_, ""));
+ EXPECT_EQ(string_, "");
+}
+
+TEST_F(InlineStringUtilTest, Append_EmptyBuffer_WritesNothing) {
+ InlineString<0> zero_capacity;
+ EXPECT_EQ(Status::ResourceExhausted(), Append(zero_capacity, "Hello"));
+ EXPECT_TRUE(zero_capacity.empty());
+ EXPECT_EQ(zero_capacity.c_str()[0], '\0');
+}
+
+TEST_F(InlineStringUtilTest, Append_TooSmall_Truncates) {
+ EXPECT_EQ(Status::ResourceExhausted(), Append(string_, "12345HELLO?"));
+ EXPECT_EQ("12345", string_);
+}
+
+TEST_F(InlineStringUtilTest, Append_ExactFit) {
+ EXPECT_EQ(OkStatus(), Append(string_, "12345"));
+ EXPECT_EQ("12345", string_);
+}
+
+TEST_F(InlineStringUtilTest, Append_NullTerminatorsInString) {
+ EXPECT_EQ(OkStatus(), Append(string_, "\0!\0\0\0"sv));
+ EXPECT_EQ("\0!\0\0\0"sv, string_);
+}
+
+TEST_F(InlineStringUtilTest, Append_ExistingContent_AppendNothing) {
+ string_ = "12345";
+ EXPECT_EQ(OkStatus(), Append(string_, ""));
+ EXPECT_EQ("12345", string_);
+}
+
+TEST_F(InlineStringUtilTest, Append_ExistingContent_ExactFit) {
+ string_.append("yo");
+ EXPECT_EQ(OkStatus(), Append(string_, "123"));
+ EXPECT_EQ("yo123", string_);
+}
+
+TEST_F(InlineStringUtilTest, Append_ExistingContent_Truncates) {
+ string_.append("yo");
+ EXPECT_EQ(Status::ResourceExhausted(), Append(string_, "12345"));
+ EXPECT_EQ("yo123", string_);
+}
+
+class PrintableCopyTest : public TestWithBuffer {};
+
+TEST_F(PrintableCopyTest, EmptyBuffer_WritesNothing) {
+ auto result = PrintableCopy("Hello", span(buffer_, 0));
+ EXPECT_EQ(0u, result.size());
+ EXPECT_FALSE(result.ok());
+ EXPECT_STREQ(kStartingString, buffer_);
+}
+
+TEST_F(PrintableCopyTest, TooSmall_Truncates) {
+ auto result = PrintableCopy("Hi!", span(buffer_, 3));
+ EXPECT_EQ(2u, result.size());
+ EXPECT_FALSE(result.ok());
+ EXPECT_STREQ("Hi", buffer_);
+}
+
+TEST_F(PrintableCopyTest, ExactFit) {
+ auto result = PrintableCopy("Hi!", span(buffer_, 4));
+ EXPECT_EQ(3u, result.size());
+ EXPECT_TRUE(result.ok());
+ EXPECT_STREQ("Hi!", buffer_);
+}
+
+TEST_F(PrintableCopyTest, StartingString) {
+ memset(buffer_, '\0', sizeof(buffer_));
+ auto result = PrintableCopy(kStartingString, span(buffer_));
+ EXPECT_EQ(sizeof(kStartingString) - 1, result.size());
+ EXPECT_TRUE(result.ok());
+ EXPECT_STREQ(kStartingString, buffer_);
+}
+
+TEST_F(PrintableCopyTest, NullTerminatorsInString) {
+ ASSERT_EQ(4u, PrintableCopy("\0!\0\0"sv, span(buffer_, 5)).size());
+ EXPECT_STREQ(".!..", buffer_);
+}
+
+TEST_F(PrintableCopyTest, ControlCharsInString) {
+ ASSERT_EQ(
+ 14u,
+ PrintableCopy("\n!\t\n\x10\x7F\xFF\vabcd\b\r"sv, span(buffer_)).size());
+ EXPECT_STREQ(".!......abcd..", buffer_);
+}
+
} // namespace
} // namespace pw::string
diff --git a/pw_symbolizer/BUILD.bazel b/pw_symbolizer/BUILD.bazel
index f8b38d462..7d19ffe73 100644
--- a/pw_symbolizer/BUILD.bazel
+++ b/pw_symbolizer/BUILD.bazel
@@ -15,11 +15,3 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
-
-# This is only used for the python tests.
-filegroup(
- name = "symbolizer_test",
- srcs = [
- "py/symbolizer_test.cc",
- ],
-)
diff --git a/pw_symbolizer/BUILD.gn b/pw_symbolizer/BUILD.gn
index 9a6699a8e..1b11f77f5 100644
--- a/pw_symbolizer/BUILD.gn
+++ b/pw_symbolizer/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_symbolizer/py/BUILD.bazel b/pw_symbolizer/py/BUILD.bazel
new file mode 100644
index 000000000..fdbdfe975
--- /dev/null
+++ b/pw_symbolizer/py/BUILD.bazel
@@ -0,0 +1,45 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "pw_symbolizer",
+ srcs = [
+ "pw_symbolizer/__init__.py",
+ "pw_symbolizer/llvm_symbolizer.py",
+ "pw_symbolizer/symbolizer.py",
+ ],
+ imports = ["."],
+)
+
+py_test(
+ name = "symbolizer_test",
+ size = "small",
+ srcs = ["symbolizer_test.py"],
+ deps = [":pw_symbolizer"],
+)
+
+# This test attempts to run subprocesses directly in the source tree, which is
+# incompatible with sandboxing.
+# TODO(b/241307309): Update this test to work with bazel.
+filegroup(
+ name = "llvm_symbolizer_test",
+ # size = "small",
+ srcs = [
+ "llvm_symbolizer_test.py",
+ "symbolizer_test.cc",
+ ],
+ # deps = [":pw_symbolizer"],
+)
diff --git a/pw_symbolizer/py/BUILD.gn b/pw_symbolizer/py/BUILD.gn
index 46dea1c10..90a85c7b5 100644
--- a/pw_symbolizer/py/BUILD.gn
+++ b/pw_symbolizer/py/BUILD.gn
@@ -42,4 +42,5 @@ pw_python_package("py") {
}
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_symbolizer/py/llvm_symbolizer_test.py b/pw_symbolizer/py/llvm_symbolizer_test.py
index 9eb43b38a..d3a2f64fb 100644
--- a/pw_symbolizer/py/llvm_symbolizer_test.py
+++ b/pw_symbolizer/py/llvm_symbolizer_test.py
@@ -28,6 +28,7 @@ _COMPILER = 'clang++'
class TestSymbolizer(unittest.TestCase):
"""Unit tests for binary symbolization."""
+
def _test_symbolization_results(self, expected_symbols, symbolizer):
for expected_symbol in expected_symbols:
result = symbolizer.symbolize(expected_symbol['Address'])
@@ -53,19 +54,24 @@ class TestSymbolizer(unittest.TestCase):
'-gfull',
f'-ffile-prefix-map={_MODULE_PY_DIR}=',
'-std=c++17',
+ '-fno-pic',
+ '-fno-pie',
+ '-nopie',
'-o',
exe_file,
]
- process = subprocess.run(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- cwd=_MODULE_PY_DIR)
+ process = subprocess.run(
+ cmd,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ cwd=_MODULE_PY_DIR,
+ )
self.assertEqual(process.returncode, 0)
- process = subprocess.run([exe_file],
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT)
+ process = subprocess.run(
+ [exe_file], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
self.assertEqual(process.returncode, 0)
expected_symbols = [
@@ -78,8 +84,9 @@ class TestSymbolizer(unittest.TestCase):
# Test backwards compatibility with older versions of
# llvm-symbolizer.
- symbolizer = pw_symbolizer.LlvmSymbolizer(exe_file,
- force_legacy=True)
+ symbolizer = pw_symbolizer.LlvmSymbolizer(
+ exe_file, force_legacy=True
+ )
self._test_symbolization_results(expected_symbols, symbolizer)
diff --git a/pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py b/pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py
index 33621723a..b36267827 100644
--- a/pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py
+++ b/pw_symbolizer/py/pw_symbolizer/llvm_symbolizer.py
@@ -24,6 +24,7 @@ from pw_symbolizer import symbolizer
class LlvmSymbolizer(symbolizer.Symbolizer):
"""A symbolizer that wraps llvm-symbolizer."""
+
def __init__(self, binary: Optional[Path] = None, force_legacy=False):
# Lets destructor return cleanly if the binary is not found.
self._symbolizer = None
@@ -31,7 +32,8 @@ class LlvmSymbolizer(symbolizer.Symbolizer):
raise FileNotFoundError(
'llvm-symbolizer not installed. Run bootstrap, or download '
'LLVM (https://github.com/llvm/llvm-project/releases/) and add '
- 'the tools to your system PATH')
+ 'the tools to your system PATH'
+ )
# Prefer JSON output as it's easier to decode.
if force_legacy:
@@ -53,22 +55,25 @@ class LlvmSymbolizer(symbolizer.Symbolizer):
'--exe',
str(binary),
]
- self._symbolizer = subprocess.Popen(cmd,
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE)
+ self._symbolizer = subprocess.Popen(
+ cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE
+ )
self._lock: threading.Lock = threading.Lock()
def __del__(self):
if self._symbolizer:
self._symbolizer.terminate()
+ self._symbolizer.wait()
@staticmethod
def _is_json_compatibile() -> bool:
"""Checks llvm-symbolizer to ensure compatibility"""
- result = subprocess.run(('llvm-symbolizer', '--help'),
- stdout=subprocess.PIPE,
- stdin=subprocess.PIPE)
+ result = subprocess.run(
+ ('llvm-symbolizer', '--help'),
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ )
for line in result.stdout.decode().splitlines():
if '--output-style' in line and 'JSON' in line:
return True
@@ -86,10 +91,12 @@ class LlvmSymbolizer(symbolizer.Symbolizer):
# Get the first symbol.
symbol = results["Symbol"][0]
- return symbolizer.Symbol(address=address,
- name=symbol['FunctionName'],
- file=symbol['FileName'],
- line=symbol['Line'])
+ return symbolizer.Symbol(
+ address=address,
+ name=symbol['FunctionName'],
+ file=symbol['FileName'],
+ line=symbol['Line'],
+ )
@staticmethod
def _llvm_output_line_splitter(file_and_line: str) -> Tuple[str, int]:
@@ -126,7 +133,8 @@ class LlvmSymbolizer(symbolizer.Symbolizer):
return symbolizer.Symbol(address)
file, line_number = LlvmSymbolizer._llvm_output_line_splitter(
- file_and_line)
+ file_and_line
+ )
return symbolizer.Symbol(address, symbol, file, line_number)
diff --git a/pw_symbolizer/py/pw_symbolizer/symbolizer.py b/pw_symbolizer/py/pw_symbolizer/symbolizer.py
index 56ce07bff..2193b7296 100644
--- a/pw_symbolizer/py/pw_symbolizer/symbolizer.py
+++ b/pw_symbolizer/py/pw_symbolizer/symbolizer.py
@@ -14,13 +14,14 @@
"""Utilities for address symbolization."""
import abc
-from typing import Iterable, List
+from typing import Iterable, List, Optional
from dataclasses import dataclass
@dataclass(frozen=True)
class Symbol:
"""Symbols produced by a symbolizer."""
+
address: int
name: str = ''
file: str = ''
@@ -51,13 +52,14 @@ class Symbol:
class Symbolizer(abc.ABC):
"""An interface for symbolizing addresses."""
+
@abc.abstractmethod
def symbolize(self, address: int) -> Symbol:
"""Symbolizes an address using a loaded binary or symbol database."""
- def dump_stack_trace(self,
- addresses,
- most_recent_first: bool = True) -> str:
+ def dump_stack_trace(
+ self, addresses, most_recent_first: bool = True
+ ) -> str:
"""Symbolizes and dumps a list of addresses as a stack trace.
most_recent_first controls the hint provided at the top of the stack
@@ -87,7 +89,8 @@ class Symbolizer(abc.ABC):
class FakeSymbolizer(Symbolizer):
"""A fake symbolizer that only knows a fixed set of symbols."""
- def __init__(self, known_symbols: Iterable[Symbol] = None):
+
+ def __init__(self, known_symbols: Optional[Iterable[Symbol]] = None):
if known_symbols is not None:
self._db = {sym.address: sym for sym in known_symbols}
else:
diff --git a/pw_symbolizer/py/symbolizer_test.py b/pw_symbolizer/py/symbolizer_test.py
index 4d2584651..029c68495 100644
--- a/pw_symbolizer/py/symbolizer_test.py
+++ b/pw_symbolizer/py/symbolizer_test.py
@@ -19,39 +19,45 @@ import pw_symbolizer
class TestSymbolFormatting(unittest.TestCase):
"""Tests Symbol objects to validate formatted output."""
+
def test_blank_symbol(self):
- sym = pw_symbolizer.Symbol(address=0x00000000,
- name='',
- file='',
- line=0)
+ sym = pw_symbolizer.Symbol(address=0x00000000, name='', file='', line=0)
self.assertEqual('??:?', sym.file_and_line())
self.assertEqual('0x00000000 (??:?)', str(sym))
def test_default_symbol(self):
- sym = pw_symbolizer.Symbol(address=0x0000a400)
+ sym = pw_symbolizer.Symbol(address=0x0000A400)
self.assertEqual('??:?', sym.file_and_line())
self.assertEqual('0x0000A400 (??:?)', str(sym))
def test_to_str(self):
- sym = pw_symbolizer.Symbol(address=0x12345678,
- name='idle_thread_context',
- file='device/system/threads.cc',
- line=59)
+ sym = pw_symbolizer.Symbol(
+ address=0x12345678,
+ name='idle_thread_context',
+ file='device/system/threads.cc',
+ line=59,
+ )
self.assertEqual('device/system/threads.cc:59', sym.file_and_line())
- self.assertEqual('idle_thread_context (device/system/threads.cc:59)',
- str(sym))
+ self.assertEqual(
+ 'idle_thread_context (device/system/threads.cc:59)', str(sym)
+ )
def test_truncated_filename(self):
- sym = pw_symbolizer.Symbol(address=0x12345678,
- name='idle_thread_context',
- file='device/system/threads.cc',
- line=59)
- self.assertEqual('idle_thread_context ([...]stem/threads.cc:59)',
- sym.to_string(max_filename_len=15))
+ sym = pw_symbolizer.Symbol(
+ address=0x12345678,
+ name='idle_thread_context',
+ file='device/system/threads.cc',
+ line=59,
+ )
+ self.assertEqual(
+ 'idle_thread_context ([...]stem/threads.cc:59)',
+ sym.to_string(max_filename_len=15),
+ )
class TestFakeSymbolizer(unittest.TestCase):
"""Tests the FakeSymbolizer class."""
+
def test_empty_db(self):
symbolizer = pw_symbolizer.FakeSymbolizer()
symbol = symbolizer.symbolize(0x404)
@@ -61,10 +67,15 @@ class TestFakeSymbolizer(unittest.TestCase):
def test_db_with_entries(self):
known_symbols = (
- pw_symbolizer.Symbol(0x404, 'do_a_flip(int n)', 'source/tricks.cc',
- 1403),
- pw_symbolizer.Symbol(0xffffffff, 'a_variable_here_would_be_funny',
- 'source/globals.cc', 21),
+ pw_symbolizer.Symbol(
+ 0x404, 'do_a_flip(int n)', 'source/tricks.cc', 1403
+ ),
+ pw_symbolizer.Symbol(
+ 0xFFFFFFFF,
+ 'a_variable_here_would_be_funny',
+ 'source/globals.cc',
+ 21,
+ ),
)
symbolizer = pw_symbolizer.FakeSymbolizer(known_symbols)
@@ -74,8 +85,8 @@ class TestFakeSymbolizer(unittest.TestCase):
self.assertEqual(symbol.file, 'source/tricks.cc')
self.assertEqual(symbol.line, 1403)
- symbol = symbolizer.symbolize(0xffffffff)
- self.assertEqual(symbol.address, 0xffffffff)
+ symbol = symbolizer.symbolize(0xFFFFFFFF)
+ self.assertEqual(symbol.address, 0xFFFFFFFF)
self.assertEqual(symbol.name, 'a_variable_here_would_be_funny')
self.assertEqual(symbol.file, 'source/globals.cc')
self.assertEqual(symbol.line, 21)
diff --git a/pw_sync/Android.bp b/pw_sync/Android.bp
new file mode 100644
index 000000000..b4bde2119
--- /dev/null
+++ b/pw_sync/Android.bp
@@ -0,0 +1,24 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_sync_headers",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ host_supported: true,
+}
diff --git a/pw_sync/BUILD.bazel b/pw_sync/BUILD.bazel
index 0b6da709f..5d48dac9c 100644
--- a/pw_sync/BUILD.bazel
+++ b/pw_sync/BUILD.bazel
@@ -54,7 +54,6 @@ pw_cc_library(
name = "binary_semaphore_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:binary_semaphore"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:binary_semaphore"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:binary_semaphore"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:binary_semaphore"],
@@ -89,7 +88,6 @@ pw_cc_library(
name = "counting_semaphore_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:counting_semaphore"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:counting_semaphore"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:counting_semaphore"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:counting_semaphore"],
@@ -122,6 +120,20 @@ pw_cc_library(
)
pw_cc_library(
+ name = "inline_borrowable",
+ hdrs = [
+ "public/pw_sync/inline_borrowable.h",
+ "public/pw_sync/internal/borrowable_storage.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":borrow",
+ ":mutex",
+ ":virtual_basic_lockable",
+ ],
+)
+
+pw_cc_library(
name = "virtual_basic_lockable",
hdrs = [
"public/pw_sync/virtual_basic_lockable.h",
@@ -161,7 +173,6 @@ pw_cc_library(
name = "mutex_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:mutex"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:mutex"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:mutex"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:mutex"],
@@ -201,7 +212,6 @@ pw_cc_library(
name = "timed_mutex_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:timed_mutex"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:timed_mutex"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:timed_mutex"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:timed_mutex"],
@@ -209,6 +219,35 @@ pw_cc_library(
}),
)
+pw_cc_library(
+ name = "recursive_mutex_facade",
+ hdrs = ["public/pw_sync/recursive_mutex.h"],
+ includes = ["public"],
+ deps = [
+ ":lock_annotations",
+ "//pw_preprocessor",
+ ],
+)
+
+pw_cc_library(
+ name = "recursive_mutex",
+ srcs = ["recursive_mutex.cc"],
+ visibility = ["//pw_sync_baremetal:__pkg__"],
+ deps = [
+ ":recursive_mutex_facade",
+ "@pigweed_config//:pw_sync_recursive_mutex_backend",
+ ],
+)
+
+pw_cc_library(
+ name = "recursive_mutex_backend_multiplexer",
+ visibility = ["@pigweed_config//:__pkg__"],
+ deps = select({
+ "@platforms//os:none": ["//pw_sync_baremetal:recursive_mutex"],
+ "//conditions:default": ["//pw_sync_stl:recursive_mutex"],
+ }),
+)
+
pw_cc_facade(
name = "interrupt_spin_lock_facade",
hdrs = [
@@ -237,7 +276,6 @@ pw_cc_library(
name = "interrupt_spin_lock_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "@platforms//os:none": ["//pw_sync_baremetal:interrupt_spin_lock"],
"//pw_build/constraints/rtos:embos": ["//pw_sync_embos:interrupt_spin_lock"],
"//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:interrupt_spin_lock"],
"//pw_build/constraints/rtos:threadx": ["//pw_sync_threadx:interrupt_spin_lock"],
@@ -265,7 +303,8 @@ pw_cc_library(
name = "thread_notification_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
- "//conditions:default": ["//pw_sync:binary_semaphore_thread_notification_backend"],
+ "//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:thread_notification"],
+ "//conditions:default": [":binary_semaphore_thread_notification_backend"],
}),
)
@@ -276,7 +315,7 @@ pw_cc_facade(
],
includes = ["public"],
deps = [
- ":thread_notification_facade",
+ ":thread_notification",
"//pw_chrono:system_clock",
],
)
@@ -294,6 +333,7 @@ pw_cc_library(
name = "timed_thread_notification_backend_multiplexer",
visibility = ["@pigweed_config//:__pkg__"],
deps = select({
+ "//pw_build/constraints/rtos:freertos": ["//pw_sync_freertos:timed_thread_notification"],
"//conditions:default": ["//pw_sync:binary_semaphore_timed_thread_notification_backend"],
}),
)
@@ -359,6 +399,42 @@ pw_cc_library(
includes = ["public"],
)
+pw_cc_facade(
+ name = "condition_variable_facade",
+ hdrs = [
+ "public/pw_sync/condition_variable.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_chrono:system_clock",
+ "//pw_sync:mutex",
+ ],
+)
+
+# TODO(b/228998350): This needs to be instantiated for each platform that
+# provides an implementation of $dir_pw_thread:test_threads and
+# $dir_pw_sync:condition_variable.
+# pw_cc_library(
+# name = "condition_variable_test",
+# srcs = ["condition_variable_test.cc"],
+# deps = [
+# ":condition_variable_facade",
+# "//pw_containers:vector",
+# "//pw_sync:mutex",
+# "//pw_sync:timed_thread_notification",
+# "//pw_thread:sleep",
+# "//pw_thread:test_threads_header",
+# "//pw_thread:thread",
+# "//pw_unit_test",
+# ],
+# )
+#
+# Filegroup to mark `condition_variable_test.cc` as used for the linter:
+filegroup(
+ name = "condition_variable_test_filegroup",
+ srcs = ["condition_variable_test.cc"],
+)
+
pw_cc_test(
name = "borrow_test",
srcs = [
@@ -373,6 +449,20 @@ pw_cc_test(
)
pw_cc_test(
+ name = "inline_borrowable_test",
+ srcs = [
+ "inline_borrowable_test.cc",
+ ],
+ deps = [
+ ":inline_borrowable",
+ ":interrupt_spin_lock",
+ ":lock_annotations",
+ ":mutex",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "binary_semaphore_facade_test",
srcs = [
"binary_semaphore_facade_test.cc",
@@ -426,6 +516,19 @@ pw_cc_test(
)
pw_cc_test(
+ name = "recursive_mutex_facade_test",
+ srcs = [
+ "recursive_mutex_facade_test.cc",
+ "recursive_mutex_facade_test_c.c",
+ ],
+ deps = [
+ ":recursive_mutex",
+ "//pw_preprocessor",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "interrupt_spin_lock_facade_test",
srcs = [
"interrupt_spin_lock_facade_test.cc",
@@ -442,7 +545,6 @@ pw_cc_test(
name = "thread_notification_facade_test",
srcs = [
"thread_notification_facade_test.cc",
- "thread_notification_facade_test_c.c",
],
deps = [
":thread_notification",
@@ -454,7 +556,6 @@ pw_cc_test(
name = "timed_thread_notification_facade_test",
srcs = [
"timed_thread_notification_facade_test.cc",
- "timed_thread_notification_facade_test_c.c",
],
deps = [
":timed_thread_notification",
diff --git a/pw_sync/BUILD.gn b/pw_sync/BUILD.gn
index 81b700329..2d7a86742 100644
--- a/pw_sync/BUILD.gn
+++ b/pw_sync/BUILD.gn
@@ -69,6 +69,19 @@ pw_source_set("borrow") {
]
}
+pw_source_set("inline_borrowable") {
+ public = [
+ "public/pw_sync/inline_borrowable.h",
+ "public/pw_sync/internal/borrowable_storage.h",
+ ]
+ public_deps = [
+ ":borrow",
+ ":mutex",
+ ":virtual_basic_lockable",
+ ]
+ public_configs = [ ":public_include_path" ]
+}
+
pw_source_set("virtual_basic_lockable") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_sync/virtual_basic_lockable.h" ]
@@ -103,6 +116,18 @@ pw_facade("timed_mutex") {
sources = [ "timed_mutex.cc" ]
}
+pw_facade("recursive_mutex") {
+ backend = pw_sync_RECURSIVE_MUTEX_BACKEND
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_sync/recursive_mutex.h" ]
+ public_deps = [
+ ":lock_annotations",
+ "$dir_pw_preprocessor",
+ ]
+ sources = [ "recursive_mutex.cc" ]
+ visibility = [ ":*" ]
+}
+
pw_facade("interrupt_spin_lock") {
backend = pw_sync_INTERRUPT_SPIN_LOCK_BACKEND
public_configs = [ ":public_include_path" ]
@@ -173,6 +198,16 @@ pw_source_set("yield_core") {
public_configs = [ ":public_include_path" ]
}
+pw_facade("condition_variable") {
+ backend = pw_sync_CONDITION_VARIABLE_BACKEND
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_sync/condition_variable.h" ]
+ public_deps = [
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_sync:mutex",
+ ]
+}
+
pw_test_group("tests") {
tests = [
":borrow_test",
@@ -180,9 +215,11 @@ pw_test_group("tests") {
":counting_semaphore_facade_test",
":mutex_facade_test",
":timed_mutex_facade_test",
+ ":recursive_mutex_facade_test",
":interrupt_spin_lock_facade_test",
":thread_notification_facade_test",
":timed_thread_notification_facade_test",
+ ":inline_borrowable_test",
]
}
@@ -195,6 +232,16 @@ pw_test("borrow_test") {
]
}
+pw_test("inline_borrowable_test") {
+ sources = [ "inline_borrowable_test.cc" ]
+ deps = [
+ ":inline_borrowable",
+ ":interrupt_spin_lock",
+ ":lock_annotations",
+ ":mutex",
+ ]
+}
+
pw_test("binary_semaphore_facade_test") {
enable_if = pw_sync_BINARY_SEMAPHORE_BACKEND != ""
sources = [
@@ -247,6 +294,19 @@ pw_test("timed_mutex_facade_test") {
]
}
+pw_test("recursive_mutex_facade_test") {
+ enable_if = pw_sync_RECURSIVE_MUTEX_BACKEND != ""
+ sources = [
+ "recursive_mutex_facade_test.cc",
+ "recursive_mutex_facade_test_c.c",
+ ]
+ deps = [
+ ":recursive_mutex",
+ "$dir_pw_preprocessor",
+ pw_sync_RECURSIVE_MUTEX_BACKEND,
+ ]
+}
+
pw_test("interrupt_spin_lock_facade_test") {
enable_if = pw_sync_INTERRUPT_SPIN_LOCK_BACKEND != ""
sources = [
@@ -278,6 +338,23 @@ pw_test("timed_thread_notification_facade_test") {
]
}
+# This needs to be instantiated per platform that provides
+# an implementation of $dir_pw_thread:test_threads and
+# $dir_pw_sync:condition_variable.
+pw_source_set("condition_variable_test") {
+ sources = [ "condition_variable_test.cc" ]
+ deps = [
+ ":condition_variable",
+ "$dir_pw_containers:vector",
+ "$dir_pw_sync:mutex",
+ "$dir_pw_sync:timed_thread_notification",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread:thread",
+ "$dir_pw_unit_test",
+ ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_sync/CMakeLists.txt b/pw_sync/CMakeLists.txt
index 312d6c252..064adb338 100644
--- a/pw_sync/CMakeLists.txt
+++ b/pw_sync/CMakeLists.txt
@@ -13,8 +13,11 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_sync/backend.cmake)
-pw_add_facade(pw_sync.binary_semaphore
+pw_add_facade(pw_sync.binary_semaphore STATIC
+ BACKEND
+ pw_sync.binary_semaphore_BACKEND
HEADERS
public/pw_sync/binary_semaphore.h
PUBLIC_INCLUDES
@@ -26,7 +29,9 @@ pw_add_facade(pw_sync.binary_semaphore
binary_semaphore.cc
)
-pw_add_facade(pw_sync.counting_semaphore
+pw_add_facade(pw_sync.counting_semaphore STATIC
+ BACKEND
+ pw_sync.counting_semaphore_BACKEND
HEADERS
public/pw_sync/counting_semaphore.h
PUBLIC_INCLUDES
@@ -38,7 +43,7 @@ pw_add_facade(pw_sync.counting_semaphore
counting_semaphore.cc
)
-pw_add_module_library(pw_sync.lock_annotations
+pw_add_library(pw_sync.lock_annotations INTERFACE
HEADERS
public/pw_sync/lock_annotations.h
PUBLIC_INCLUDES
@@ -47,7 +52,7 @@ pw_add_module_library(pw_sync.lock_annotations
pw_preprocessor
)
-pw_add_module_library(pw_sync.borrow
+pw_add_library(pw_sync.borrow INTERFACE
HEADERS
public/pw_sync/borrow.h
PUBLIC_INCLUDES
@@ -58,7 +63,19 @@ pw_add_module_library(pw_sync.borrow
pw_sync.virtual_basic_lockable
)
-pw_add_module_library(pw_sync.virtual_basic_lockable
+pw_add_library(pw_sync.inline_borrowable INTERFACE
+ HEADERS
+ public/pw_sync/inline_borrowable.h
+ public/pw_sync/internal/borrowable_storage.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_sync.borrow
+ pw_sync.mutex
+ pw_sync.virtual_basic_lockable
+)
+
+pw_add_library(pw_sync.virtual_basic_lockable INTERFACE
HEADERS
public/pw_sync/virtual_basic_lockable.h
PUBLIC_INCLUDES
@@ -68,7 +85,9 @@ pw_add_module_library(pw_sync.virtual_basic_lockable
pw_sync.lock_annotations
)
-pw_add_facade(pw_sync.mutex
+pw_add_facade(pw_sync.mutex STATIC
+ BACKEND
+ pw_sync.mutex_BACKEND
HEADERS
public/pw_sync/mutex.h
PUBLIC_INCLUDES
@@ -81,7 +100,9 @@ pw_add_facade(pw_sync.mutex
mutex.cc
)
-pw_add_facade(pw_sync.timed_mutex
+pw_add_facade(pw_sync.timed_mutex STATIC
+ BACKEND
+ pw_sync.timed_mutex_BACKEND
HEADERS
public/pw_sync/timed_mutex.h
PUBLIC_INCLUDES
@@ -95,7 +116,23 @@ pw_add_facade(pw_sync.timed_mutex
timed_mutex.cc
)
-pw_add_facade(pw_sync.interrupt_spin_lock
+pw_add_facade(pw_sync.recursive_mutex STATIC
+ BACKEND
+ pw_sync.recursive_mutex_BACKEND
+ HEADERS
+ public/pw_sync/recursive_mutex.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_sync.lock_annotations
+ pw_preprocessor
+ SOURCES
+ recursive_mutex.cc
+)
+
+pw_add_facade(pw_sync.interrupt_spin_lock STATIC
+ BACKEND
+ pw_sync.interrupt_spin_lock_BACKEND
HEADERS
public/pw_sync/interrupt_spin_lock.h
PUBLIC_INCLUDES
@@ -108,14 +145,18 @@ pw_add_facade(pw_sync.interrupt_spin_lock
interrupt_spin_lock.cc
)
-pw_add_facade(pw_sync.thread_notification
+pw_add_facade(pw_sync.thread_notification INTERFACE
+ BACKEND
+ pw_sync.thread_notification_BACKEND
HEADERS
public/pw_sync/thread_notification.h
PUBLIC_INCLUDES
public
)
-pw_add_facade(pw_sync.timed_thread_notification
+pw_add_facade(pw_sync.timed_thread_notification INTERFACE
+ BACKEND
+ pw_sync.timed_thread_notification_BACKEND
HEADERS
public/pw_sync/timed_thread_notification.h
PUBLIC_INCLUDES
@@ -127,7 +168,7 @@ pw_add_facade(pw_sync.timed_thread_notification
# This target provides the backend for pw::sync::ThreadNotification based on
# pw::sync::BinarySemaphore.
-pw_add_module_library(pw_sync.binary_semaphore_thread_notification_backend
+pw_add_library(pw_sync.binary_semaphore_thread_notification_backend INTERFACE
HEADERS
public/pw_sync/backends/binary_semaphore_thread_notification_inline.h
public/pw_sync/backends/binary_semaphore_thread_notification_native.h
@@ -142,7 +183,7 @@ pw_add_module_library(pw_sync.binary_semaphore_thread_notification_backend
# This target provides the backend for pw::sync::TimedThreadNotification based
# on pw::sync::BinarySemaphore.
-pw_add_module_library(pw_sync.binary_semaphore_timed_thread_notification_backend
+pw_add_library(pw_sync.binary_semaphore_timed_thread_notification_backend INTERFACE
HEADERS
public/pw_sync/backends/binary_semaphore_timed_thread_notification_inline.h
public_overrides/pw_sync_backend/timed_thread_notification_inline.h
@@ -154,17 +195,29 @@ pw_add_module_library(pw_sync.binary_semaphore_timed_thread_notification_backend
pw_sync.binary_semaphore_thread_notification_backend
)
-pw_add_module_library(pw_sync.yield_core
+pw_add_library(pw_sync.yield_core INTERFACE
HEADERS
public/pw_sync/yield_core.h
PUBLIC_INCLUDES
public
)
+pw_add_facade(pw_sync.condition_variable INTERFACE
+ BACKEND
+ pw_sync.condition_variable_BACKEND
+ HEADERS
+ public/pw_sync/condition_variable.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_sync.mutex
+)
+
pw_add_test(pw_sync.borrow_test
SOURCES
borrow_test.cc
- DEPS
+ PRIVATE_DEPS
pw_assert
pw_sync.borrow
pw_sync.virtual_basic_lockable
@@ -173,13 +226,25 @@ pw_add_test(pw_sync.borrow_test
pw_sync
)
-if(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL
- "pw_sync.binary_semaphore.NO_BACKEND_SET")
+pw_add_test(pw_sync.inline_borrowable_test
+ SOURCES
+ inline_borrowable_test.cc
+ PRIVATE_DEPS
+ pw_sync.inline_borrowable
+ pw_sync.interrupt_spin_lock
+ pw_sync.lock_annotations
+ pw_sync.mutex
+ GROUPS
+ modules
+ pw_sync
+)
+
+if(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL "")
pw_add_test(pw_sync.binary_semaphore_facade_test
SOURCES
binary_semaphore_facade_test.cc
binary_semaphore_facade_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_preprocessor
pw_sync.binary_semaphore
GROUPS
@@ -188,13 +253,12 @@ if(NOT "${pw_sync.binary_semaphore_BACKEND}" STREQUAL
)
endif()
-if(NOT "${pw_sync.counting_semaphore_BACKEND}" STREQUAL
- "pw_sync.counting_semaphore.NO_BACKEND_SET")
+if(NOT "${pw_sync.counting_semaphore_BACKEND}" STREQUAL "")
pw_add_test(pw_sync.counting_semaphore_facade_test
SOURCES
counting_semaphore_facade_test.cc
counting_semaphore_facade_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_preprocessor
pw_sync.counting_semaphore
GROUPS
@@ -203,12 +267,12 @@ if(NOT "${pw_sync.counting_semaphore_BACKEND}" STREQUAL
)
endif()
-if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "pw_sync.mutex.NO_BACKEND_SET")
+if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "")
pw_add_test(pw_sync.mutex_facade_test
SOURCES
mutex_facade_test.cc
mutex_facade_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_preprocessor
pw_sync.mutex
GROUPS
@@ -217,13 +281,12 @@ if(NOT "${pw_sync.mutex_BACKEND}" STREQUAL "pw_sync.mutex.NO_BACKEND_SET")
)
endif()
-if(NOT "${pw_sync.timed_mutex_BACKEND}" STREQUAL
- "pw_sync.timed_mutex.NO_BACKEND_SET")
+if(NOT "${pw_sync.timed_mutex_BACKEND}" STREQUAL "")
pw_add_test(pw_sync.timed_mutex_facade_test
SOURCES
timed_mutex_facade_test.cc
timed_mutex_facade_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_preprocessor
pw_sync.timed_mutex
GROUPS
@@ -232,13 +295,12 @@ if(NOT "${pw_sync.timed_mutex_BACKEND}" STREQUAL
)
endif()
-if(NOT "${pw_sync.interrupt_spin_lock_BACKEND}" STREQUAL
- "pw_sync.interrupt_spin_lock.NO_BACKEND_SET")
+if(NOT "${pw_sync.interrupt_spin_lock_BACKEND}" STREQUAL "")
pw_add_test(pw_sync.interrupt_spin_lock_facade_test
SOURCES
interrupt_spin_lock_facade_test.cc
interrupt_spin_lock_facade_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_preprocessor
pw_sync.interrupt_spin_lock
GROUPS
@@ -247,12 +309,11 @@ if(NOT "${pw_sync.interrupt_spin_lock_BACKEND}" STREQUAL
)
endif()
-if(NOT "${pw_sync.thread_notification_BACKEND}" STREQUAL
- "pw_sync.thread_notification.NO_BACKEND_SET")
+if(NOT "${pw_sync.thread_notification_BACKEND}" STREQUAL "")
pw_add_test(pw_sync.thread_notification_facade_test
SOURCES
thread_notification_facade_test.cc
- DEPS
+ PRIVATE_DEPS
pw_sync.thread_notification
GROUPS
modules
@@ -260,15 +321,28 @@ if(NOT "${pw_sync.thread_notification_BACKEND}" STREQUAL
)
endif()
-if(NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL
- "pw_sync.timed_thread_notification.NO_BACKEND_SET")
+if(NOT "${pw_sync.timed_thread_notification_BACKEND}" STREQUAL "")
pw_add_test(pw_sync.timed_thread_notification_facade_test
SOURCES
timed_thread_notification_facade_test.cc
- DEPS
+ PRIVATE_DEPS
pw_sync.timed_thread_notification
GROUPS
modules
pw_sync
)
endif()
+
+pw_add_library(pw_sync.condition_variable_test STATIC
+ PUBLIC_DEPS
+ pw_sync.condition_variable
+ pw_containers.vector
+ pw_sync.mutex
+ pw_sync.timed_thread_notification
+ pw_thread.sleep
+ pw_thread.test_threads
+ pw_thread.thread
+ pw_unit_test
+ SOURCES
+ condition_variable_test.cc
+)
diff --git a/pw_sync/backend.cmake b/pw_sync/backend.cmake
new file mode 100644
index 000000000..4fab61b21
--- /dev/null
+++ b/pw_sync/backend.cmake
@@ -0,0 +1,43 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_sync module's binary semaphore.
+pw_add_backend_variable(pw_sync.binary_semaphore_BACKEND)
+
+# Backend for the pw_sync module's condition variable.
+pw_add_backend_variable(pw_sync.condition_variable_BACKEND)
+
+# Backend for the pw_sync module's counting semaphore.
+pw_add_backend_variable(pw_sync.counting_semaphore_BACKEND)
+
+# Backend for the pw_sync module's mutex.
+pw_add_backend_variable(pw_sync.mutex_BACKEND)
+
+# Backend for the pw_sync module's timed mutex.
+pw_add_backend_variable(pw_sync.timed_mutex_BACKEND)
+
+# Backend for the pw_sync module's recursive mutex.
+pw_add_backend_variable(pw_sync.recursive_mutex_BACKEND)
+
+# Backend for the pw_sync module's interrupt spin lock.
+pw_add_backend_variable(pw_sync.interrupt_spin_lock_BACKEND)
+
+# Backend for the pw_sync module's thread notification.
+pw_add_backend_variable(pw_sync.thread_notification_BACKEND)
+
+# Backend for the pw_sync module's timed thread notification.
+pw_add_backend_variable(pw_sync.timed_thread_notification_BACKEND)
diff --git a/pw_sync/backend.gni b/pw_sync/backend.gni
index 9712a4526..2327640a9 100644
--- a/pw_sync/backend.gni
+++ b/pw_sync/backend.gni
@@ -16,6 +16,9 @@ declare_args() {
# Backend for the pw_sync module's binary semaphore.
pw_sync_BINARY_SEMAPHORE_BACKEND = ""
+ # Backend for the pw_sync module's condition variable.
+ pw_sync_CONDITION_VARIABLE_BACKEND = ""
+
# Backend for the pw_sync module's counting semaphore.
pw_sync_COUNTING_SEMAPHORE_BACKEND = ""
@@ -25,6 +28,9 @@ declare_args() {
# Backend for the pw_sync module's timed mutex.
pw_sync_TIMED_MUTEX_BACKEND = ""
+ # Backend for the pw_sync module's recursive mutex.
+ pw_sync_RECURSIVE_MUTEX_BACKEND = ""
+
# Backend for the pw_sync module's interrupt spin lock.
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND = ""
diff --git a/pw_sync/binary_semaphore_facade_test.cc b/pw_sync/binary_semaphore_facade_test.cc
index 7f9ce30cb..c52d792d9 100644
--- a/pw_sync/binary_semaphore_facade_test.cc
+++ b/pw_sync/binary_semaphore_facade_test.cc
@@ -53,7 +53,7 @@ TEST(BinarySemaphore, EmptyInitialState) {
EXPECT_FALSE(semaphore.try_acquire());
}
-// TODO(pwbug/291): Add real concurrency tests once we have pw::thread.
+// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
TEST(BinarySemaphore, Release) {
BinarySemaphore semaphore;
diff --git a/pw_sync/condition_variable_test.cc b/pw_sync/condition_variable_test.cc
new file mode 100644
index 000000000..c0ddc4df0
--- /dev/null
+++ b/pw_sync/condition_variable_test.cc
@@ -0,0 +1,356 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_sync/condition_variable.h"
+
+#include <chrono>
+#include <functional>
+
+#include "gtest/gtest.h"
+#include "pw_containers/vector.h"
+#include "pw_sync/mutex.h"
+#include "pw_sync/timed_thread_notification.h"
+#include "pw_thread/sleep.h"
+#include "pw_thread/test_threads.h"
+#include "pw_thread/thread.h"
+
+namespace pw::sync {
+namespace {
+
+using namespace std::chrono_literals;
+
+// A timeout for tests where successful behaviour involves waiting.
+constexpr auto kRequiredTimeout = 100ms;
+
+// Maximum extra wait time allowed for test that ensure something waits for
+// `kRequiredTimeout`.
+const auto kAllowedSlack = kRequiredTimeout * 1.5;
+
+// A timeout that should only be hit if something goes wrong.
+constexpr auto kFailureTimeout = 5s;
+
+using StateLock = std::unique_lock<Mutex>;
+
+struct ThreadInfo {
+ explicit ThreadInfo(int id) : thread_id(id) {}
+
+ // waiting_notifier is signalled in predicates to indicate that the predicate
+ // has been evaluated. This guarantees (via insider information) that the
+ // thread will acquire the internal ThreadNotification.
+ TimedThreadNotification waiting_notifier;
+
+ // Signals when the worker thread is done.
+ TimedThreadNotification done_notifier;
+
+ // The result of the predicate the worker thread uses with wait*(). Set from
+ // the main test thread and read by the worker thread.
+ bool predicate_result = false;
+
+ // Stores the result of ConditionVariable::wait_for() or ::wait_until() for
+ // use in test asserts.
+ bool wait_result = false;
+
+ // For use in recording the order in which threads block on a condition.
+ const int thread_id;
+
+ // Returns a function which will return the current value of
+ //`predicate_result` and release `waiting_notifier`.
+ std::function<bool()> Predicate() {
+ return [this]() {
+ bool result = this->predicate_result;
+ this->waiting_notifier.release();
+ return result;
+ };
+ }
+};
+
+// A `ThreadCore` implementation that delegates to an `std::function`.
+class LambdaThreadCore : public pw::thread::ThreadCore {
+ public:
+ explicit LambdaThreadCore(std::function<void()> work)
+ : work_(std::move(work)) {}
+
+ private:
+ void Run() override { work_(); }
+
+ std::function<void()> work_;
+};
+
+class LambdaThread {
+ public:
+ // Starts a new thread which runs `work`, joining the thread on destruction.
+ explicit LambdaThread(
+ std::function<void()> work,
+ pw::thread::Options options = pw::thread::test::TestOptionsThread0())
+ : thread_core_(std::move(work)), thread_(options, thread_core_) {}
+ ~LambdaThread() { thread_.join(); }
+ LambdaThread(const LambdaThread&) = delete;
+ LambdaThread(LambdaThread&&) = delete;
+ LambdaThread& operator=(const LambdaThread&) = delete;
+ LambdaThread&& operator=(LambdaThread&&) = delete;
+
+ private:
+ LambdaThreadCore thread_core_;
+ pw::thread::Thread thread_;
+};
+
+TEST(Wait, PredicateTrueNoWait) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ ThreadInfo thread_info(0);
+
+ LambdaThread thread([&mutex, &condvar, &info = thread_info] {
+ StateLock l{mutex};
+ condvar.wait(l, [] { return true; });
+
+ info.done_notifier.release();
+ });
+ EXPECT_TRUE(thread_info.done_notifier.try_acquire_for(kFailureTimeout));
+}
+
+TEST(NotifyOne, BlocksUntilSignaled) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ ThreadInfo thread_info(0);
+
+ LambdaThread thread([&mutex, &condvar, &info = thread_info] {
+ StateLock l{mutex};
+ condvar.wait(l, info.Predicate());
+ info.done_notifier.release();
+ });
+ ASSERT_TRUE(thread_info.waiting_notifier.try_acquire_for(kFailureTimeout));
+ {
+ StateLock l{mutex};
+ thread_info.predicate_result = true;
+ }
+ condvar.notify_one();
+ ASSERT_TRUE(thread_info.done_notifier.try_acquire_for(kFailureTimeout));
+}
+
+TEST(NotifyOne, UnblocksOne) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ std::array<ThreadInfo, 2> thread_info = {ThreadInfo(0), ThreadInfo(1)};
+ pw::Vector<int, 2> wait_order;
+
+ LambdaThread thread_1(
+ [&mutex, &condvar, &info = thread_info[0], &wait_order] {
+ StateLock l{mutex};
+ auto predicate = [&info, &wait_order] {
+ wait_order.push_back(info.thread_id);
+ auto result = info.predicate_result;
+ info.waiting_notifier.release();
+ return result;
+ };
+ condvar.wait(l, predicate);
+ info.done_notifier.release();
+ },
+ pw::thread::test::TestOptionsThread0());
+ LambdaThread thread_2(
+ [&mutex, &condvar, &info = thread_info[1], &wait_order] {
+ StateLock l{mutex};
+ auto predicate = [&info, &wait_order] {
+ wait_order.push_back(info.thread_id);
+ auto result = info.predicate_result;
+ info.waiting_notifier.release();
+ return result;
+ };
+ condvar.wait(l, predicate);
+ info.done_notifier.release();
+ },
+ pw::thread::test::TestOptionsThread1());
+
+ ASSERT_TRUE(thread_info[0].waiting_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_TRUE(thread_info[1].waiting_notifier.try_acquire_for(kFailureTimeout));
+
+ {
+ StateLock l{mutex};
+ thread_info[1].predicate_result = true;
+ thread_info[0].predicate_result = true;
+ }
+ condvar.notify_one();
+ ASSERT_TRUE(thread_info[wait_order[0]].done_notifier.try_acquire_for(
+ kFailureTimeout));
+ ASSERT_FALSE(thread_info[wait_order[0]].done_notifier.try_acquire());
+ condvar.notify_one();
+ ASSERT_TRUE(thread_info[wait_order[1]].done_notifier.try_acquire_for(
+ kFailureTimeout));
+}
+
+TEST(NotifyAll, UnblocksMultiple) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ std::array<ThreadInfo, 2> thread_info = {ThreadInfo(0), ThreadInfo(1)};
+
+ LambdaThread thread_1(
+ [&mutex, &condvar, &info = thread_info[0]] {
+ StateLock l{mutex};
+ condvar.wait(l, info.Predicate());
+ info.done_notifier.release();
+ },
+ pw::thread::test::TestOptionsThread0());
+ LambdaThread thread_2(
+ [&mutex, &condvar, &info = thread_info[1]] {
+ StateLock l{mutex};
+ condvar.wait(l, info.Predicate());
+ info.done_notifier.release();
+ },
+ pw::thread::test::TestOptionsThread1());
+
+ ASSERT_TRUE(thread_info[0].waiting_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_TRUE(thread_info[1].waiting_notifier.try_acquire_for(kFailureTimeout));
+ {
+ StateLock l{mutex};
+ thread_info[0].predicate_result = true;
+ thread_info[1].predicate_result = true;
+ }
+ condvar.notify_all();
+ ASSERT_TRUE(thread_info[0].done_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_TRUE(thread_info[1].done_notifier.try_acquire_for(kFailureTimeout));
+}
+
+TEST(WaitFor, ReturnsTrueIfSignalled) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ ThreadInfo thread_info(0);
+
+ LambdaThread thread([&mutex, &condvar, &info = thread_info] {
+ StateLock l{mutex};
+ info.wait_result = condvar.wait_for(l, kFailureTimeout, info.Predicate());
+ info.done_notifier.release();
+ });
+
+ ASSERT_TRUE(thread_info.waiting_notifier.try_acquire_for(kFailureTimeout));
+ {
+ StateLock l{mutex};
+ thread_info.predicate_result = true;
+ }
+ condvar.notify_one();
+ ASSERT_TRUE(thread_info.done_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_TRUE(thread_info.wait_result);
+}
+
+TEST(WaitFor, ReturnsFalseIfTimesOut) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ ThreadInfo thread_info(0);
+
+ LambdaThread thread([&mutex, &condvar, &info = thread_info] {
+ StateLock l{mutex};
+ info.wait_result = condvar.wait_for(l, 0ms, info.Predicate());
+ info.done_notifier.release();
+ });
+
+ ASSERT_TRUE(thread_info.waiting_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_TRUE(thread_info.done_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_FALSE(thread_info.wait_result);
+}
+
+// NOTE: This test waits even in successful circumstances.
+TEST(WaitFor, TimeoutApproximatelyCorrect) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ ThreadInfo thread_info(0);
+ pw::chrono::SystemClock::duration wait_duration{};
+
+ LambdaThread thread([&mutex, &condvar, &info = thread_info, &wait_duration] {
+ StateLock l{mutex};
+ auto start = pw::chrono::SystemClock::now();
+ info.wait_result = condvar.wait_for(l, kRequiredTimeout, info.Predicate());
+ wait_duration = pw::chrono::SystemClock::now() - start;
+ info.done_notifier.release();
+ });
+
+ ASSERT_TRUE(thread_info.waiting_notifier.try_acquire_for(kFailureTimeout));
+ // Wake up thread multiple times. Make sure the timeout is observed.
+ for (int i = 0; i < 5; ++i) {
+ condvar.notify_one();
+ pw::this_thread::sleep_for(kRequiredTimeout / 6);
+ }
+ ASSERT_TRUE(thread_info.done_notifier.try_acquire_for(kFailureTimeout));
+ EXPECT_FALSE(thread_info.wait_result);
+ EXPECT_GE(wait_duration, kRequiredTimeout);
+ EXPECT_LT(wait_duration, (kRequiredTimeout + kAllowedSlack));
+}
+
+TEST(WaitUntil, ReturnsTrueIfSignalled) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ ThreadInfo thread_info(0);
+
+ LambdaThread thread([&mutex, &condvar, &info = thread_info] {
+ StateLock l{mutex};
+ info.wait_result = condvar.wait_until(
+ l, pw::chrono::SystemClock::now() + kRequiredTimeout, info.Predicate());
+ info.done_notifier.release();
+ });
+
+ ASSERT_TRUE(thread_info.waiting_notifier.try_acquire_for(kFailureTimeout));
+ {
+ StateLock l{mutex};
+ thread_info.predicate_result = true;
+ }
+ condvar.notify_one();
+ ASSERT_TRUE(thread_info.done_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_TRUE(thread_info.wait_result);
+}
+
+// NOTE: This test waits even in successful circumstances.
+TEST(WaitUntil, ReturnsFalseIfTimesOut) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ ThreadInfo thread_info(0);
+
+ LambdaThread thread([&mutex, &condvar, &info = thread_info] {
+ StateLock l{mutex};
+ info.wait_result = condvar.wait_until(
+ l, pw::chrono::SystemClock::now() + kRequiredTimeout, info.Predicate());
+ info.done_notifier.release();
+ });
+
+ ASSERT_TRUE(thread_info.waiting_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_TRUE(thread_info.done_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_FALSE(thread_info.wait_result);
+}
+
+// NOTE: This test waits even in successful circumstances.
+TEST(WaitUntil, TimeoutApproximatelyCorrect) {
+ Mutex mutex;
+ ConditionVariable condvar;
+ ThreadInfo thread_info(0);
+ pw::chrono::SystemClock::duration wait_duration{};
+
+ LambdaThread thread([&mutex, &condvar, &info = thread_info, &wait_duration] {
+ StateLock l{mutex};
+ auto start = pw::chrono::SystemClock::now();
+ info.wait_result = condvar.wait_until(
+ l, pw::chrono::SystemClock::now() + kRequiredTimeout, info.Predicate());
+ wait_duration = pw::chrono::SystemClock::now() - start;
+ info.done_notifier.release();
+ });
+
+ ASSERT_TRUE(thread_info.waiting_notifier.try_acquire_for(kFailureTimeout));
+ // Wake up thread multiple times. Make sure the timeout is observed.
+ for (int i = 0; i < 5; ++i) {
+ condvar.notify_one();
+ pw::this_thread::sleep_for(kRequiredTimeout / 6);
+ }
+ ASSERT_TRUE(thread_info.done_notifier.try_acquire_for(kFailureTimeout));
+ ASSERT_FALSE(thread_info.wait_result);
+ ASSERT_GE(wait_duration, kRequiredTimeout);
+ ASSERT_LE(wait_duration, kRequiredTimeout + kAllowedSlack);
+}
+
+} // namespace
+} // namespace pw::sync
diff --git a/pw_sync/counting_semaphore_facade_test.cc b/pw_sync/counting_semaphore_facade_test.cc
index 2fcc9830e..c7224b292 100644
--- a/pw_sync/counting_semaphore_facade_test.cc
+++ b/pw_sync/counting_semaphore_facade_test.cc
@@ -59,7 +59,7 @@ TEST(CountingSemaphore, EmptyInitialState) {
EXPECT_FALSE(semaphore.try_acquire());
}
-// TODO(pwbug/291): Add real concurrency tests once we have pw::thread.
+// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
TEST(CountingSemaphore, SingleRelease) {
CountingSemaphore semaphore;
diff --git a/pw_sync/docs.rst b/pw_sync/docs.rst
index 201cdf0b8..e30c01f51 100644
--- a/pw_sync/docs.rst
+++ b/pw_sync/docs.rst
@@ -8,18 +8,21 @@ and/or interrupts through signaling primitives and critical section lock
primitives.
.. Warning::
- This module is still under construction, the API is not yet stable.
+
+ This module is still under construction, the API is not yet stable.
.. Note::
- The objects in this module do not have an Init() style public API which is
- common in many RTOS C APIs. Instead, they rely on being able to invoke the
- native initialization APIs for synchronization primitives during C++
- construction.
- In order to support global statically constructed synchronization without
- constexpr constructors, the user and/or backend **MUST** ensure that any
- initialization required in your environment is done prior to the creation
- and/or initialization of the native synchronization primitives
- (e.g. kernel initialization).
+
+ The objects in this module do not have an Init() style public API which is
+ common in many RTOS C APIs. Instead, they rely on being able to invoke the
+ native initialization APIs for synchronization primitives during C++
+ construction.
+
+ In order to support global statically constructed synchronization without
+ constexpr constructors, the user and/or backend **MUST** ensure that any
+ initialization required in your environment is done prior to the creation
+ and/or initialization of the native synchronization primitives
+ (e.g. kernel initialization).
--------------------------------
Critical Section Lock Primitives
@@ -30,8 +33,9 @@ The critical section lock primitives provided by this module comply with
relevant
`TimedLockable <https://en.cppreference.com/w/cpp/named_req/TimedLockable>`_ C++
named requirements. This means that they are compatible with existing helpers in
-the STL's ``<mutex>`` thread support library. For example `std::lock_guard <https://en.cppreference.com/w/cpp/thread/lock_guard>`_
-and `std::unique_lock <https://en.cppreference.com/w/cpp/thread/unique_lock>`_ can be directly used.
+the STL's ``<mutex>`` thread support library. For example `std::lock_guard
+<https://en.cppreference.com/w/cpp/thread/lock_guard>`_ and `std::unique_lock
+<https://en.cppreference.com/w/cpp/thread/unique_lock>`_ can be directly used.
Mutex
=====
@@ -47,369 +51,302 @@ meaning it is a
and `Lockable <https://en.cppreference.com/w/cpp/named_req/Lockable>`_.
.. list-table::
-
- * - *Supported on*
- - *Backend module*
- * - FreeRTOS
- - :ref:`module-pw_sync_freertos`
- * - ThreadX
- - :ref:`module-pw_sync_threadx`
- * - embOS
- - :ref:`module-pw_sync_embos`
- * - STL
- - :ref:`module-pw_sync_stl`
- * - Baremetal
- - Planned
- * - Zephyr
- - Planned
- * - CMSIS-RTOS API v2 & RTX5
- - Planned
+ :header-rows: 1
+
+ * - Supported on
+ - Backend module
+ * - FreeRTOS
+ - :ref:`module-pw_sync_freertos`
+ * - ThreadX
+ - :ref:`module-pw_sync_threadx`
+ * - embOS
+ - :ref:`module-pw_sync_embos`
+ * - STL
+ - :ref:`module-pw_sync_stl`
+ * - Baremetal
+ - Planned
+ * - Zephyr
+ - Planned
+ * - CMSIS-RTOS API v2 & RTX5
+ - Planned
C++
---
-.. cpp:class:: pw::sync::Mutex
-
- .. cpp:function:: void lock()
-
- Locks the mutex, blocking indefinitely. Failures are fatal.
-
- **Precondition:** The lock isn't already held by this thread. Recursive
- locking is undefined behavior.
-
- .. cpp:function:: bool try_lock()
-
- Tries to lock the mutex in a non-blocking manner.
- Returns true if the mutex was successfully acquired.
-
- **Precondition:** The lock isn't already held by this thread. Recursive
- locking is undefined behavior.
+.. doxygenclass:: pw::sync::Mutex
+ :members:
- .. cpp:function:: void unlock()
+.. cpp:namespace-push:: pw::sync::Mutex
- Unlocks the mutex. Failures are fatal.
-
- **Precondition:** The mutex is held by this thread.
-
-
- .. list-table::
+.. list-table::
+ :header-rows: 1
+ :widths: 70 10 10 10
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:class:`pw::sync::Mutex::Mutex`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::Mutex::~Mutex`
+ - ✔
+ -
+ -
+ * - :cpp:func:`lock`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_lock`
+ - ✔
+ -
+ -
+ * - :cpp:func:`unlock`
+ - ✔
+ -
+ -
+
+.. cpp:namespace-pop::
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``Mutex::Mutex``
- - ✔
- -
- -
- * - ``Mutex::~Mutex``
- - ✔
- -
- -
- * - ``void Mutex::lock``
- - ✔
- -
- -
- * - ``bool Mutex::try_lock``
- - ✔
- -
- -
- * - ``void Mutex::unlock``
- - ✔
- -
- -
Examples in C++
^^^^^^^^^^^^^^^
.. code-block:: cpp
- #include "pw_sync/mutex.h"
+ #include "pw_sync/mutex.h"
- pw::sync::Mutex mutex;
+ pw::sync::Mutex mutex;
- void ThreadSafeCriticalSection() {
- mutex.lock();
- NotThreadSafeCriticalSection();
- mutex.unlock();
- }
+ void ThreadSafeCriticalSection() {
+ mutex.lock();
+ NotThreadSafeCriticalSection();
+ mutex.unlock();
+ }
Alternatively you can use C++'s RAII helpers to ensure you always unlock.
.. code-block:: cpp
- #include <mutex>
-
- #include "pw_sync/mutex.h"
+ #include <mutex>
- pw::sync::Mutex mutex;
+ #include "pw_sync/mutex.h"
- void ThreadSafeCriticalSection() {
- std::lock_guard lock(mutex);
- NotThreadSafeCriticalSection();
- }
+ pw::sync::Mutex mutex;
+ void ThreadSafeCriticalSection() {
+ std::lock_guard lock(mutex);
+ NotThreadSafeCriticalSection();
+ }
C
-
The Mutex must be created in C++, however it can be passed into C using the
``pw_sync_Mutex`` opaque struct alias.
-.. cpp:function:: void pw_sync_Mutex_Lock(pw_sync_Mutex* mutex)
-
- Invokes the ``Mutex::lock`` member function on the given ``mutex``.
-
-.. cpp:function:: bool pw_sync_Mutex_TryLock(pw_sync_Mutex* mutex)
-
- Invokes the ``Mutex::try_lock`` member function on the given ``mutex``.
-
-.. cpp:function:: void pw_sync_Mutex_Unlock(pw_sync_Mutex* mutex)
-
- Invokes the ``Mutex::unlock`` member function on the given ``mutex``.
+.. doxygenfunction:: pw_sync_Mutex_Lock
+.. doxygenfunction:: pw_sync_Mutex_TryLock
+.. doxygenfunction:: pw_sync_Mutex_Unlock
.. list-table::
-
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``void pw_sync_Mutex_Lock``
- - ✔
- -
- -
- * - ``bool pw_sync_Mutex_TryLock``
- - ✔
- -
- -
- * - ``void pw_sync_Mutex_Unlock``
- - ✔
- -
- -
+ :header-rows: 1
+ :widths: 70 10 10 10
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - ``void pw_sync_Mutex_Lock``
+ - ✔
+ -
+ -
+ * - ``bool pw_sync_Mutex_TryLock``
+ - ✔
+ -
+ -
+ * - ``void pw_sync_Mutex_Unlock``
+ - ✔
+ -
+ -
Example in C
^^^^^^^^^^^^
.. code-block:: cpp
- #include "pw_sync/mutex.h"
+ #include "pw_sync/mutex.h"
- pw::sync::Mutex mutex;
+ pw::sync::Mutex mutex;
- extern pw_sync_Mutex mutex; // This can only be created in C++.
+ extern pw_sync_Mutex mutex; // This can only be created in C++.
- void ThreadSafeCriticalSection(void) {
- pw_sync_Mutex_Lock(&mutex);
- NotThreadSafeCriticalSection();
- pw_sync_Mutex_Unlock(&mutex);
- }
+ void ThreadSafeCriticalSection(void) {
+ pw_sync_Mutex_Lock(&mutex);
+ NotThreadSafeCriticalSection();
+ pw_sync_Mutex_Unlock(&mutex);
+ }
TimedMutex
==========
-The TimedMutex is an extension of the Mutex which offers timeout and deadline
-based semantics.
+.. cpp:namespace-push:: pw::sync
-The TimedMutex's API is C++11 STL
+The :cpp:class:`TimedMutex` is an extension of the Mutex which offers timeout
+and deadline based semantics.
+
+The :cpp:class:`TimedMutex`'s API is C++11 STL
`std::timed_mutex <https://en.cppreference.com/w/cpp/thread/timed_mutex>`_ like,
meaning it is a
`BasicLockable <https://en.cppreference.com/w/cpp/named_req/BasicLockable>`_,
`Lockable <https://en.cppreference.com/w/cpp/named_req/Lockable>`_, and
`TimedLockable <https://en.cppreference.com/w/cpp/named_req/TimedLockable>`_.
-Note that the ``TimedMutex`` is a derived ``Mutex`` class, meaning that
-a ``TimedMutex`` can be used by someone who needs the basic ``Mutex``. This is
-in contrast to the C++ STL's
+Note that the :cpp:class:`TimedMutex` is a derived :cpp:class:`Mutex` class,
+meaning that a :cpp:class:`TimedMutex` can be used by someone who needs the
+basic :cpp:class:`Mutex`. This is in contrast to the C++ STL's
`std::timed_mutex <https://en.cppreference.com/w/cpp/thread/timed_mutex>`_.
+.. cpp:namespace-pop::
.. list-table::
-
- * - *Supported on*
- - *Backend module*
- * - FreeRTOS
- - :ref:`module-pw_sync_freertos`
- * - ThreadX
- - :ref:`module-pw_sync_threadx`
- * - embOS
- - :ref:`module-pw_sync_embos`
- * - STL
- - :ref:`module-pw_sync_stl`
- * - Zephyr
- - Planned
- * - CMSIS-RTOS API v2 & RTX5
- - Planned
+ :header-rows: 1
+
+ * - Supported on
+ - Backend module
+ * - FreeRTOS
+ - :ref:`module-pw_sync_freertos`
+ * - ThreadX
+ - :ref:`module-pw_sync_threadx`
+ * - embOS
+ - :ref:`module-pw_sync_embos`
+ * - STL
+ - :ref:`module-pw_sync_stl`
+ * - Zephyr
+ - Planned
+ * - CMSIS-RTOS API v2 & RTX5
+ - Planned
C++
---
-.. cpp:class:: pw::sync::TimedMutex
-
- .. cpp:function:: void lock()
-
- Locks the mutex, blocking indefinitely. Failures are fatal.
-
- **Precondition:** The lock isn't already held by this thread. Recursive
- locking is undefined behavior.
-
- .. cpp:function:: bool try_lock()
-
- Tries to lock the mutex in a non-blocking manner.
- Returns true if the mutex was successfully acquired.
-
- **Precondition:** The lock isn't already held by this thread. Recursive
- locking is undefined behavior.
-
-
- .. cpp:function:: bool try_lock_for(const chrono::SystemClock::duration& timeout)
+.. doxygenclass:: pw::sync::TimedMutex
+ :members:
- Tries to lock the mutex. Blocks until specified the timeout has elapsed or
- the lock is acquired, whichever comes first.
- Returns true if the mutex was successfully acquired.
+.. cpp:namespace-push:: pw::sync::TimedMutex
- **Precondition:** The lock isn't already held by this thread. Recursive
- locking is undefined behavior.
-
- .. cpp:function:: bool try_lock_until(const chrono::SystemClock::time_point& deadline)
-
- Tries to lock the mutex. Blocks until specified deadline has been reached
- or the lock is acquired, whichever comes first.
- Returns true if the mutex was successfully acquired.
-
- **Precondition:** The lock isn't already held by this thread. Recursive
- locking is undefined behavior.
-
- .. cpp:function:: void unlock()
-
- Unlocks the mutex. Failures are fatal.
-
- **Precondition:** The mutex is held by this thread.
-
-
- .. list-table::
+.. list-table::
+ :header-rows: 1
+ :widths: 70 10 10 10
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:class:`pw::sync::TimedMutex::TimedMutex`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::TimedMutex::~TimedMutex`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::Mutex::lock`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::Mutex::try_lock`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_lock_for`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_lock_until`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::Mutex::unlock`
+ - ✔
+ -
+ -
+
+.. cpp:namespace-pop::
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``TimedMutex::TimedMutex``
- - ✔
- -
- -
- * - ``TimedMutex::~TimedMutex``
- - ✔
- -
- -
- * - ``void TimedMutex::lock``
- - ✔
- -
- -
- * - ``bool TimedMutex::try_lock``
- - ✔
- -
- -
- * - ``bool TimedMutex::try_lock_for``
- - ✔
- -
- -
- * - ``bool TimedMutex::try_lock_until``
- - ✔
- -
- -
- * - ``void TimedMutex::unlock``
- - ✔
- -
- -
Examples in C++
^^^^^^^^^^^^^^^
.. code-block:: cpp
- #include "pw_chrono/system_clock.h"
- #include "pw_sync/timed_mutex.h"
-
- pw::sync::TimedMutex mutex;
+ #include "pw_chrono/system_clock.h"
+ #include "pw_sync/timed_mutex.h"
- bool ThreadSafeCriticalSectionWithTimeout(
- const SystemClock::duration timeout) {
- if (!mutex.try_lock_for(timeout)) {
- return false;
- }
- NotThreadSafeCriticalSection();
- mutex.unlock();
- return true;
- }
+ pw::sync::TimedMutex mutex;
+ bool ThreadSafeCriticalSectionWithTimeout(
+ const SystemClock::duration timeout) {
+ if (!mutex.try_lock_for(timeout)) {
+ return false;
+ }
+ NotThreadSafeCriticalSection();
+ mutex.unlock();
+ return true;
+ }
Alternatively you can use C++'s RAII helpers to ensure you always unlock.
.. code-block:: cpp
- #include <mutex>
+ #include <mutex>
- #include "pw_chrono/system_clock.h"
- #include "pw_sync/timed_mutex.h"
-
- pw::sync::TimedMutex mutex;
-
- bool ThreadSafeCriticalSectionWithTimeout(
- const SystemClock::duration timeout) {
- std::unique_lock lock(mutex, std::defer_lock);
- if (!lock.try_lock_for(timeout)) {
- return false;
- }
- NotThreadSafeCriticalSection();
- return true;
- }
+ #include "pw_chrono/system_clock.h"
+ #include "pw_sync/timed_mutex.h"
+ pw::sync::TimedMutex mutex;
+ bool ThreadSafeCriticalSectionWithTimeout(
+ const SystemClock::duration timeout) {
+ std::unique_lock lock(mutex, std::defer_lock);
+ if (!lock.try_lock_for(timeout)) {
+ return false;
+ }
+ NotThreadSafeCriticalSection();
+ return true;
+ }
C
-
The TimedMutex must be created in C++, however it can be passed into C using the
``pw_sync_TimedMutex`` opaque struct alias.
-.. cpp:function:: void pw_sync_TimedMutex_Lock(pw_sync_TimedMutex* mutex)
-
- Invokes the ``TimedMutex::lock`` member function on the given ``mutex``.
-
-.. cpp:function:: bool pw_sync_TimedMutex_TryLock(pw_sync_TimedMutex* mutex)
-
- Invokes the ``TimedMutex::try_lock`` member function on the given ``mutex``.
-
-.. cpp:function:: bool pw_sync_TimedMutex_TryLockFor(pw_sync_TimedMutex* mutex, pw_chrono_SystemClock_Duration timeout)
-
- Invokes the ``TimedMutex::try_lock_for`` member function on the given ``mutex``.
-
-.. cpp:function:: bool pw_sync_TimedMutex_TryLockUntil(pw_sync_TimedMutex* mutex, pw_chrono_SystemClock_TimePoint deadline)
-
- Invokes the ``TimedMutex::try_lock_until`` member function on the given ``mutex``.
-
-.. cpp:function:: void pw_sync_TimedMutex_Unlock(pw_sync_TimedMutex* mutex)
-
- Invokes the ``TimedMutex::unlock`` member function on the given ``mutex``.
+.. doxygenfile:: timed_mutex.h
+ :sections: func
.. list-table::
-
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``void pw_sync_TimedMutex_Lock``
- - ✔
- -
- -
- * - ``bool pw_sync_TimedMutex_TryLock``
- - ✔
- -
- -
- * - ``bool pw_sync_TimedMutex_TryLockFor``
- - ✔
- -
- -
- * - ``bool pw_sync_TimedMutex_TryLockUntil``
- - ✔
- -
- -
- * - ``void pw_sync_TimedMutex_Unlock``
- - ✔
- -
- -
+ :header-rows: 1
+ :widths: 70 10 10 10
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:func:`pw_sync_TimedMutex_Lock`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw_sync_TimedMutex_TryLock`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw_sync_TimedMutex_TryLockFor`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw_sync_TimedMutex_TryLockUntil`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw_sync_TimedMutex_Unlock`
+ - ✔
+ -
+ -
Example in C
^^^^^^^^^^^^
@@ -432,6 +369,11 @@ Example in C
return true;
}
+RecursiveMutex
+==============
+``pw_sync`` provides ``pw::sync::RecursiveMutex``, a recursive mutex
+implementation. At this time, this facade can only be used internally by
+Pigweed.
InterruptSpinLock
=================
@@ -456,103 +398,92 @@ and
`Lockable <https://en.cppreference.com/w/cpp/named_req/Lockable>`_.
.. list-table::
-
- * - *Supported on*
- - *Backend module*
- * - FreeRTOS
- - :ref:`module-pw_sync_freertos`
- * - ThreadX
- - :ref:`module-pw_sync_threadx`
- * - embOS
- - :ref:`module-pw_sync_embos`
- * - STL
- - :ref:`module-pw_sync_stl`
- * - Baremetal
- - Planned, not ready for use
- * - Zephyr
- - Planned
- * - CMSIS-RTOS API v2 & RTX5
- - Planned
+ :header-rows: 1
+
+ * - Supported on
+ - Backend module
+ * - FreeRTOS
+ - :ref:`module-pw_sync_freertos`
+ * - ThreadX
+ - :ref:`module-pw_sync_threadx`
+ * - embOS
+ - :ref:`module-pw_sync_embos`
+ * - STL
+ - :ref:`module-pw_sync_stl`
+ * - Baremetal
+ - Planned, not ready for use
+ * - Zephyr
+ - Planned
+ * - CMSIS-RTOS API v2 & RTX5
+ - Planned
C++
---
-.. cpp:class:: pw::sync::InterruptSpinLock
-
- .. cpp:function:: void lock()
-
- Locks the spinlock, blocking indefinitely. Failures are fatal.
-
- **Precondition:** Recursive locking is undefined behavior.
-
- .. cpp:function:: bool try_lock()
-
- Tries to lock the spinlock in a non-blocking manner.
- Returns true if the spinlock was successfully acquired.
-
- **Precondition:** Recursive locking is undefined behavior.
+.. doxygenclass:: pw::sync::InterruptSpinLock
+ :members:
- .. cpp:function:: void unlock()
+.. cpp:namespace-push:: pw::sync::InterruptSpinLock
- Unlocks the mutex. Failures are fatal.
-
- **Precondition:** The spinlock is held by the caller.
-
- .. list-table::
-
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``InterruptSpinLock::InterruptSpinLock``
- - ✔
- - ✔
- -
- * - ``InterruptSpinLock::~InterruptSpinLock``
- - ✔
- - ✔
- -
- * - ``void InterruptSpinLock::lock``
- - ✔
- - ✔
- -
- * - ``bool InterruptSpinLock::try_lock``
- - ✔
- - ✔
- -
- * - ``void InterruptSpinLock::unlock``
- - ✔
- - ✔
- -
+.. list-table::
+ :widths: 70 10 10 10
+ :header-rows: 1
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:class:`pw::sync::InterruptSpinLock::InterruptSpinLock`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`pw::sync::InterruptSpinLock::~InterruptSpinLock`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`lock`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`try_lock`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`unlock`
+ - ✔
+ - ✔
+ -
+
+.. cpp:namespace-pop::
Examples in C++
^^^^^^^^^^^^^^^
.. code-block:: cpp
- #include "pw_sync/interrupt_spin_lock.h"
+ #include "pw_sync/interrupt_spin_lock.h"
- pw::sync::InterruptSpinLock interrupt_spin_lock;
+ pw::sync::InterruptSpinLock interrupt_spin_lock;
- void InterruptSafeCriticalSection() {
- interrupt_spin_lock.lock();
- NotThreadSafeCriticalSection();
- interrupt_spin_lock.unlock();
- }
+ void InterruptSafeCriticalSection() {
+ interrupt_spin_lock.lock();
+ NotThreadSafeCriticalSection();
+ interrupt_spin_lock.unlock();
+ }
Alternatively you can use C++'s RAII helpers to ensure you always unlock.
.. code-block:: cpp
- #include <mutex>
+ #include <mutex>
- #include "pw_sync/interrupt_spin_lock.h"
+ #include "pw_sync/interrupt_spin_lock.h"
- pw::sync::InterruptSpinLock interrupt_spin_lock;
+ pw::sync::InterruptSpinLock interrupt_spin_lock;
- void InterruptSafeCriticalSection() {
- std::lock_guard lock(interrupt_spin_lock);
- NotThreadSafeCriticalSection();
- }
+ void InterruptSafeCriticalSection() {
+ std::lock_guard lock(interrupt_spin_lock);
+ NotThreadSafeCriticalSection();
+ }
C
@@ -560,53 +491,47 @@ C
The InterruptSpinLock must be created in C++, however it can be passed into C using the
``pw_sync_InterruptSpinLock`` opaque struct alias.
-.. cpp:function:: void pw_sync_InterruptSpinLock_Lock(pw_sync_InterruptSpinLock* interrupt_spin_lock)
-
- Invokes the ``InterruptSpinLock::lock`` member function on the given ``interrupt_spin_lock``.
-
-.. cpp:function:: bool pw_sync_InterruptSpinLock_TryLock(pw_sync_InterruptSpinLock* interrupt_spin_lock)
-
- Invokes the ``InterruptSpinLock::try_lock`` member function on the given ``interrupt_spin_lock``.
-
-.. cpp:function:: void pw_sync_InterruptSpinLock_Unlock(pw_sync_InterruptSpinLock* interrupt_spin_lock)
-
- Invokes the ``InterruptSpinLock::unlock`` member function on the given ``interrupt_spin_lock``.
+.. doxygenfunction:: pw_sync_InterruptSpinLock_Lock
+.. doxygenfunction:: pw_sync_InterruptSpinLock_TryLock
+.. doxygenfunction:: pw_sync_InterruptSpinLock_Unlock
.. list-table::
-
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``void pw_sync_InterruptSpinLock_Lock``
- - ✔
- - ✔
- -
- * - ``bool pw_sync_InterruptSpinLock_TryLock``
- - ✔
- - ✔
- -
- * - ``void pw_sync_InterruptSpinLock_Unlock``
- - ✔
- - ✔
- -
+ :widths: 70 10 10 10
+ :header-rows: 1
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:func:`pw_sync_InterruptSpinLock_Lock`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`pw_sync_InterruptSpinLock_TryLock`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`pw_sync_InterruptSpinLock_Unlock`
+ - ✔
+ - ✔
+ -
Example in C
^^^^^^^^^^^^
.. code-block:: cpp
- #include "pw_chrono/system_clock.h"
- #include "pw_sync/interrupt_spin_lock.h"
+ #include "pw_chrono/system_clock.h"
+ #include "pw_sync/interrupt_spin_lock.h"
- pw::sync::InterruptSpinLock interrupt_spin_lock;
+ pw::sync::InterruptSpinLock interrupt_spin_lock;
- extern pw_sync_InterruptSpinLock interrupt_spin_lock; // This can only be created in C++.
+ extern pw_sync_InterruptSpinLock interrupt_spin_lock; // This can only be created in C++.
- void InterruptSafeCriticalSection(void) {
- pw_sync_InterruptSpinLock_Lock(&interrupt_spin_lock);
- NotThreadSafeCriticalSection();
- pw_sync_InterruptSpinLock_Unlock(&interrupt_spin_lock);
- }
+ void InterruptSafeCriticalSection(void) {
+ pw_sync_InterruptSpinLock_Lock(&interrupt_spin_lock);
+ NotThreadSafeCriticalSection();
+ pw_sync_InterruptSpinLock_Unlock(&interrupt_spin_lock);
+ }
Thread Safety Lock Annotations
==============================
@@ -656,140 +581,24 @@ you want to refer to is not in scope, you may use a member pointer
Annotating Lock Usage
^^^^^^^^^^^^^^^^^^^^^
-.. cpp:function:: PW_GUARDED_BY(x)
-
- Documents if a shared field or global variable needs to be protected by a
- lock. ``PW_GUARDED_BY()`` allows the user to specify a particular lock that
- should be held when accessing the annotated variable.
-
- Although this annotation (and ``PW_PT_GUARDED_BY``, below) cannot be applied
- to local variables, a local variable and its associated lock can often be
- combined into a small class or struct, thereby allowing the annotation.
-
- Example:
-
- .. code-block:: cpp
-
- class Foo {
- Mutex mu_;
- int p1_ PW_GUARDED_BY(mu_);
- ...
- };
-
-.. cpp:function:: PW_PT_GUARDED_BY(x)
-
- Documents if the memory location pointed to by a pointer should be guarded
- by a lock when dereferencing the pointer.
-
- Example:
-
- .. code-block:: cpp
-
- class Foo {
- Mutex mu_;
- int *p1_ PW_PT_GUARDED_BY(mu_);
- ...
- };
-
- Note that a pointer variable to a shared memory location could itself be a
- shared variable.
-
- Example:
-
- .. code-block:: cpp
-
- // `q_`, guarded by `mu1_`, points to a shared memory location that is
- // guarded by `mu2_`:
- int *q_ PW_GUARDED_BY(mu1_) PW_PT_GUARDED_BY(mu2_);
-
-.. cpp:function:: PW_ACQUIRED_AFTER(...)
-.. cpp:function:: PW_ACQUIRED_BEFORE(...)
-
- Documents the acquisition order between locks that can be held
- simultaneously by a thread. For any two locks that need to be annotated
- to establish an acquisition order, only one of them needs the annotation.
- (i.e. You don't have to annotate both locks with both ``PW_ACQUIRED_AFTER``
- and ``PW_ACQUIRED_BEFORE``.)
-
- As with ``PW_GUARDED_BY``, this is only applicable to locks that are shared
- fields or global variables.
-
- Example:
-
- .. code-block:: cpp
-
- Mutex m1_;
- Mutex m2_ PW_ACQUIRED_AFTER(m1_);
-
-.. cpp:function:: PW_EXCLUSIVE_LOCKS_REQUIRED(...)
-.. cpp:function:: PW_SHARED_LOCKS_REQUIRED(...)
-
- Documents a function that expects a lock to be held prior to entry.
- The lock is expected to be held both on entry to, and exit from, the
- function.
-
- An exclusive lock allows read-write access to the guarded data member(s), and
- only one thread can acquire a lock exclusively at any one time. A shared lock
- allows read-only access, and any number of threads can acquire a shared lock
- concurrently.
-
- Generally, non-const methods should be annotated with
- ``PW_EXCLUSIVE_LOCKS_REQUIRED``, while const methods should be annotated with
- ``PW_SHARED_LOCKS_REQUIRED``.
-
- Example:
-
- .. code-block:: cpp
-
- Mutex mu1, mu2;
- int a PW_GUARDED_BY(mu1);
- int b PW_GUARDED_BY(mu2);
-
- void foo() PW_EXCLUSIVE_LOCKS_REQUIRED(mu1, mu2) { ... }
- void bar() const PW_SHARED_LOCKS_REQUIRED(mu1, mu2) { ... }
-
-.. cpp:function:: PW_LOCKS_EXCLUDED(...)
-
- Documents the locks acquired in the body of the function. These locks
- cannot be held when calling this function (as Pigweed's default locks are
- non-reentrant).
-
- Example:
-
- .. code-block:: cpp
-
- Mutex mu;
- int a PW_GUARDED_BY(mu);
-
- void foo() PW_LOCKS_EXCLUDED(mu) {
- mu.lock();
- ...
- mu.unlock();
- }
-
-.. cpp:function:: PW_LOCK_RETURNED(...)
-
- Documents a function that returns a lock without acquiring it. For example,
- a public getter method that returns a pointer to a private lock should
- be annotated with ``PW_LOCK_RETURNED``.
-
- Example:
-
- .. code-block:: cpp
-
- class Foo {
- public:
- Mutex* mu() PW_LOCK_RETURNED(mu) { return &mu; }
-
- private:
- Mutex mu;
- };
-
-.. cpp:function:: PW_NO_LOCK_SAFETY_ANALYSIS()
-
- Turns off thread safety checking within the body of a particular function.
- This annotation is used to mark functions that are known to be correct, but
- the locking behavior is more complicated than the analyzer can handle.
+.. doxygendefine:: PW_GUARDED_BY
+.. doxygendefine:: PW_PT_GUARDED_BY
+.. doxygendefine:: PW_ACQUIRED_AFTER
+.. doxygendefine:: PW_ACQUIRED_BEFORE
+.. doxygendefine:: PW_EXCLUSIVE_LOCKS_REQUIRED
+.. doxygendefine:: PW_SHARED_LOCKS_REQUIRED
+.. doxygendefine:: PW_LOCKS_EXCLUDED
+.. doxygendefine:: PW_LOCK_RETURNED
+.. doxygendefine:: PW_LOCKABLE
+.. doxygendefine:: PW_SCOPED_LOCKABLE
+.. doxygendefine:: PW_EXCLUSIVE_LOCK_FUNCTION
+.. doxygendefine:: PW_SHARED_LOCK_FUNCTION
+.. doxygendefine:: PW_UNLOCK_FUNCTION
+.. doxygendefine:: PW_EXCLUSIVE_TRYLOCK_FUNCTION
+.. doxygendefine:: PW_SHARED_TRYLOCK_FUNCTION
+.. doxygendefine:: PW_ASSERT_EXCLUSIVE_LOCK
+.. doxygendefine:: PW_ASSERT_SHARED_LOCK
+.. doxygendefine:: PW_NO_LOCK_SAFETY_ANALYSIS
Annotating Lock Objects
^^^^^^^^^^^^^^^^^^^^^^^
@@ -802,151 +611,104 @@ the macro documentation after for more details:
.. code-block:: cpp
- class PW_LOCKABLE("Lock") Lock {
- public:
- void Lock() PW_EXCLUSIVE_LOCK_FUNCTION();
-
- void ReaderLock() PW_SHARED_LOCK_FUNCTION();
-
- void Unlock() PW_UNLOCK_FUNCTION();
-
- void ReaderUnlock() PW_SHARED_TRYLOCK_FUNCTION();
-
- bool TryLock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);
-
- bool ReaderTryLock() PW_SHARED_TRYLOCK_FUNCTION(true);
-
- void AssertHeld() PW_ASSERT_EXCLUSIVE_LOCK();
-
- void AssertReaderHeld() PW_ASSERT_SHARED_LOCK();
- };
-
-
- // Tag types for selecting a constructor.
- struct adopt_lock_t {} inline constexpr adopt_lock = {};
- struct defer_lock_t {} inline constexpr defer_lock = {};
- struct shared_lock_t {} inline constexpr shared_lock = {};
-
- class PW_SCOPED_LOCKABLE ScopedLocker {
- // Acquire lock, implicitly acquire *this and associate it with lock.
- ScopedLocker(Lock *lock) PW_EXCLUSIVE_LOCK_FUNCTION(lock)
- : lock_(lock), locked(true) {
- lock->Lock();
- }
-
- // Assume lock is held, implicitly acquire *this and associate it with lock.
- ScopedLocker(Lock *lock, adopt_lock_t) PW_EXCLUSIVE_LOCKS_REQUIRED(lock)
- : lock_(lock), locked(true) {}
-
- // Acquire lock in shared mode, implicitly acquire *this and associate it
- // with lock.
- ScopedLocker(Lock *lock, shared_lock_t) PW_SHARED_LOCK_FUNCTION(lock)
- : lock_(lock), locked(true) {
- lock->ReaderLock();
- }
-
- // Assume lock is held in shared mode, implicitly acquire *this and associate
- // it with lock.
- ScopedLocker(Lock *lock, adopt_lock_t, shared_lock_t)
- PW_SHARED_LOCKS_REQUIRED(lock) : lock_(lock), locked(true) {}
-
- // Assume lock is not held, implicitly acquire *this and associate it with
- // lock.
- ScopedLocker(Lock *lock, defer_lock_t) PW_LOCKS_EXCLUDED(lock)
- : lock_(lock), locked(false) {}
-
- // Release *this and all associated locks, if they are still held.
- // There is no warning if the scope was already unlocked before.
- ~ScopedLocker() PW_UNLOCK_FUNCTION() {
- if (locked)
- lock_->GenericUnlock();
- }
-
- // Acquire all associated locks exclusively.
- void Lock() PW_EXCLUSIVE_LOCK_FUNCTION() {
- lock_->Lock();
- locked = true;
- }
-
- // Try to acquire all associated locks exclusively.
- bool TryLock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true) {
- return locked = lock_->TryLock();
- }
-
- // Acquire all associated locks in shared mode.
- void ReaderLock() PW_SHARED_LOCK_FUNCTION() {
- lock_->ReaderLock();
- locked = true;
- }
-
- // Try to acquire all associated locks in shared mode.
- bool ReaderTryLock() PW_SHARED_TRYLOCK_FUNCTION(true) {
- return locked = lock_->ReaderTryLock();
- }
-
- // Release all associated locks. Warn on double unlock.
- void Unlock() PW_UNLOCK_FUNCTION() {
- lock_->Unlock();
- locked = false;
- }
-
- // Release all associated locks. Warn on double unlock.
- void ReaderUnlock() PW_UNLOCK_FUNCTION() {
- lock_->ReaderUnlock();
- locked = false;
- }
-
- private:
- Lock* lock_;
- bool locked_;
- };
-
-.. cpp:function:: PW_LOCKABLE(name)
-
- Documents if a class/type is a lockable type (such as the ``pw::sync::Mutex``
- class). The name is used in the warning messages. This can also be useful on
- classes which have locking like semantics but aren't actually locks.
-
-.. cpp:function:: PW_SCOPED_LOCKABLE()
-
- Documents if a class does RAII locking. The name is used in the warning
- messages.
-
- The constructor should use ``LOCK_FUNCTION()`` to specify the lock that is
- acquired, and the destructor should use ``UNLOCK_FUNCTION()`` with no
- arguments; the analysis will assume that the destructor unlocks whatever the
- constructor locked.
-
-.. cpp:function:: PW_EXCLUSIVE_LOCK_FUNCTION()
-
- Documents functions that acquire a lock in the body of a function, and do
- not release it.
-
-.. cpp:function:: PW_SHARED_LOCK_FUNCTION()
-
- Documents functions that acquire a shared (reader) lock in the body of a
- function, and do not release it.
-
-.. cpp:function:: PW_UNLOCK_FUNCTION()
-
- Documents functions that expect a lock to be held on entry to the function,
- and release it in the body of the function.
-
-.. cpp:function:: PW_EXCLUSIVE_TRYLOCK_FUNCTION(try_success)
-.. cpp:function:: PW_SHARED_TRYLOCK_FUNCTION(try_success)
-
- Documents functions that try to acquire a lock, and return success or failure
- (or a non-boolean value that can be interpreted as a boolean).
- The first argument should be ``true`` for functions that return ``true`` on
- success, or ``false`` for functions that return `false` on success. The second
- argument specifies the lock that is locked on success. If unspecified, this
- lock is assumed to be ``this``.
-
-.. cpp:function:: PW_ASSERT_EXCLUSIVE_LOCK()
-.. cpp:function:: PW_ASSERT_SHARED_LOCK()
-
- Documents functions that dynamically check to see if a lock is held, and fail
- if it is not held.
+ class PW_LOCKABLE("Lock") Lock {
+ public:
+ void Lock() PW_EXCLUSIVE_LOCK_FUNCTION();
+
+ void ReaderLock() PW_SHARED_LOCK_FUNCTION();
+
+ void Unlock() PW_UNLOCK_FUNCTION();
+
+ void ReaderUnlock() PW_SHARED_TRYLOCK_FUNCTION();
+
+ bool TryLock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);
+
+ bool ReaderTryLock() PW_SHARED_TRYLOCK_FUNCTION(true);
+
+ void AssertHeld() PW_ASSERT_EXCLUSIVE_LOCK();
+
+ void AssertReaderHeld() PW_ASSERT_SHARED_LOCK();
+ };
+
+
+ // Tag types for selecting a constructor.
+ struct adopt_lock_t {} inline constexpr adopt_lock = {};
+ struct defer_lock_t {} inline constexpr defer_lock = {};
+ struct shared_lock_t {} inline constexpr shared_lock = {};
+
+ class PW_SCOPED_LOCKABLE ScopedLocker {
+ // Acquire lock, implicitly acquire *this and associate it with lock.
+ ScopedLocker(Lock *lock) PW_EXCLUSIVE_LOCK_FUNCTION(lock)
+ : lock_(lock), locked(true) {
+ lock->Lock();
+ }
+
+ // Assume lock is held, implicitly acquire *this and associate it with lock.
+ ScopedLocker(Lock *lock, adopt_lock_t) PW_EXCLUSIVE_LOCKS_REQUIRED(lock)
+ : lock_(lock), locked(true) {}
+
+ // Acquire lock in shared mode, implicitly acquire *this and associate it
+ // with lock.
+ ScopedLocker(Lock *lock, shared_lock_t) PW_SHARED_LOCK_FUNCTION(lock)
+ : lock_(lock), locked(true) {
+ lock->ReaderLock();
+ }
+
+ // Assume lock is held in shared mode, implicitly acquire *this and associate
+ // it with lock.
+ ScopedLocker(Lock *lock, adopt_lock_t, shared_lock_t)
+ PW_SHARED_LOCKS_REQUIRED(lock) : lock_(lock), locked(true) {}
+
+ // Assume lock is not held, implicitly acquire *this and associate it with
+ // lock.
+ ScopedLocker(Lock *lock, defer_lock_t) PW_LOCKS_EXCLUDED(lock)
+ : lock_(lock), locked(false) {}
+
+ // Release *this and all associated locks, if they are still held.
+ // There is no warning if the scope was already unlocked before.
+ ~ScopedLocker() PW_UNLOCK_FUNCTION() {
+ if (locked)
+ lock_->GenericUnlock();
+ }
+
+ // Acquire all associated locks exclusively.
+ void Lock() PW_EXCLUSIVE_LOCK_FUNCTION() {
+ lock_->Lock();
+ locked = true;
+ }
+
+ // Try to acquire all associated locks exclusively.
+ bool TryLock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true) {
+ return locked = lock_->TryLock();
+ }
+
+ // Acquire all associated locks in shared mode.
+ void ReaderLock() PW_SHARED_LOCK_FUNCTION() {
+ lock_->ReaderLock();
+ locked = true;
+ }
+
+ // Try to acquire all associated locks in shared mode.
+ bool ReaderTryLock() PW_SHARED_TRYLOCK_FUNCTION(true) {
+ return locked = lock_->ReaderTryLock();
+ }
+
+ // Release all associated locks. Warn on double unlock.
+ void Unlock() PW_UNLOCK_FUNCTION() {
+ lock_->Unlock();
+ locked = false;
+ }
+
+ // Release all associated locks. Warn on double unlock.
+ void ReaderUnlock() PW_UNLOCK_FUNCTION() {
+ lock_->ReaderUnlock();
+ locked = false;
+ }
+
+ private:
+ Lock* lock_;
+ bool locked_;
+ };
-----------------------------
Critical Section Lock Helpers
@@ -976,9 +738,9 @@ The ``VirtualBasicLock`` interface meets the
named requirement. Our critical section lock primitives offer optional virtual
versions, including:
-* ``pw::sync::VirtualMutex``
-* ``pw::sync::VirtualTimedMutex``
-* ``pw::sync::VirtualInterruptSpinLock``
+* :cpp:func:`pw::sync::VirtualMutex`
+* :cpp:func:`pw::sync::VirtualTimedMutex`
+* :cpp:func:`pw::sync::VirtualInterruptSpinLock`
Borrowable
==========
@@ -1007,27 +769,27 @@ internally to the API. For example:
.. code-block:: cpp
- class BankAccount {
- public:
- void Deposit(int amount) {
- std::lock_guard lock(mutex_);
- balance_ += amount;
- }
+ class BankAccount {
+ public:
+ void Deposit(int amount) {
+ std::lock_guard lock(mutex_);
+ balance_ += amount;
+ }
- void Withdraw(int amount) {
- std::lock_guard lock(mutex_);
- balance_ -= amount;
- }
+ void Withdraw(int amount) {
+ std::lock_guard lock(mutex_);
+ balance_ -= amount;
+ }
- void Balance() const {
- std::lock_guard lock(mutex_);
- return balance_;
- }
+ void Balance() const {
+ std::lock_guard lock(mutex_);
+ return balance_;
+ }
- private:
- int balance_ PW_GUARDED_BY(mutex_);
- pw::sync::Mutex mutex_;
- };
+ private:
+ int balance_ PW_GUARDED_BY(mutex_);
+ pw::sync::Mutex mutex_;
+ };
Internal locking guarantees that any concurrent calls to its public member
functions don't corrupt an instance of that class. This is typically ensured by
@@ -1054,26 +816,26 @@ instance, e.g.:
.. code-block:: cpp
- class BankAccount {
- public:
- void Deposit(int amount) {
- balance_ += amount;
- }
+ class BankAccount {
+ public:
+ void Deposit(int amount) {
+ balance_ += amount;
+ }
- void Withdraw(int amount) {
- balance_ -= amount;
- }
+ void Withdraw(int amount) {
+ balance_ -= amount;
+ }
- void Balance() const {
- return balance_;
- }
+ void Balance() const {
+ return balance_;
+ }
- private:
- int balance_;
- };
+ private:
+ int balance_;
+ };
- pw::sync::Mutex bobs_account_mutex;
- BankAccount bobs_account PW_GUARDED_BY(bobs_account_mutex);
+ pw::sync::Mutex bobs_account_mutex;
+ BankAccount bobs_account PW_GUARDED_BY(bobs_account_mutex);
The lock is acquired before the bank account is used for a transaction. In
addition, we do not have to modify every public function and its trivial to
@@ -1091,10 +853,10 @@ protected instance and its lock which provides RAII-style access.
.. code-block:: cpp
- pw::sync::Mutex bobs_account_mutex;
- BankAccount bobs_account PW_GUARDED_BY(bobs_account_mutex);
- pw::sync::Borrowable<BankAccount, pw::sync::Mutex> bobs_acount(
- bobs_account, bobs_account_mutex);
+ pw::sync::Mutex bobs_account_mutex;
+ BankAccount bobs_account PW_GUARDED_BY(bobs_account_mutex);
+ pw::sync::Borrowable<BankAccount, pw::sync::Mutex> bobs_acount(
+ bobs_account, bobs_account_mutex);
This construct is useful when sharing objects or data which are transactional in
nature where making individual operations threadsafe is insufficient. See the
@@ -1111,89 +873,151 @@ you do exactly this if you provide access to the I2c bus through a
C++
---
-.. cpp:class:: template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable> pw::sync::BorrowedPointer
+.. doxygenclass:: pw::sync::BorrowedPointer
+ :members:
- The BorrowedPointer is an RAII handle which wraps a pointer to a borrowed
- object along with a held lock which is guarding the object. When destroyed,
- the lock is released.
+.. doxygenclass:: pw::sync::Borrowable
+ :members:
- This object is moveable, but not copyable.
+Example in C++
+^^^^^^^^^^^^^^
- .. cpp:function:: GuardedType* operator->()
+.. code-block:: cpp
- Provides access to the borrowed object's members.
+ #include <chrono>
- .. cpp:function:: GuardedType& operator*()
+ #include "pw_bytes/span.h"
+ #include "pw_i2c/initiator.h"
+ #include "pw_status/try.h"
+ #include "pw_status/result.h"
+ #include "pw_sync/borrow.h"
+ #include "pw_sync/mutex.h"
- Provides access to the borrowed object directly.
+ class ExampleI2c : public pw::i2c::Initiator;
- **Warning:** The member of pointer member access operator, operator->(), is
- recommended over this API as this is prone to leaking references. However,
- this is sometimes necessary.
+ pw::sync::VirtualMutex i2c_mutex;
+ ExampleI2c i2c;
+ pw::sync::Borrowable<ExampleI2c> borrowable_i2c(i2c, i2c_mutex);
- **Warning:** Be careful not to leak references to the borrowed object.
+ pw::Result<ConstByteSpan> ReadI2cData(ByteSpan buffer) {
+ // Block indefinitely waiting to borrow the i2c bus.
+ pw::sync::BorrowedPointer<ExampleI2c> borrowed_i2c =
+ borrowable_i2c.acquire();
-.. cpp:class:: template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable> pw::sync::Borrowable
+ // Execute a sequence of transactions to get the needed data.
+ PW_TRY(borrowed_i2c->WriteFor(kFirstWrite, std::chrono::milliseconds(50)));
+ PW_TRY(borrowed_i2c->WriteReadFor(kSecondWrite, buffer,
+ std::chrono::milliseconds(10)));
- .. cpp:function:: BorrowedPointer<GuardedType, Lock> acquire()
+ // Borrowed i2c pointer is returned when the scope exits.
+ return buffer;
+ }
- Blocks indefinitely until the object can be borrowed. Failures are fatal.
+InlineBorrowable
+=================
+``InlineBorrowable`` is a helper to simplify the common use case where an object
+is wrapped in a ``Borrowable`` for its entire lifetime. The InlineBorrowable
+owns the guarded object and the lock object.
- .. cpp:function:: std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire()
+InlineBorrowable has a separate parameter for the concrete lock type
+that is instantiated and a (possibly virtual) lock interface type that is
+referenced by users of the guarded object. The default lock is
+``pw::sync::VirtualMutex`` and the default lock interface is
+``pw::sync::VirtualBasicLockable``.
- Tries to borrow the object in a non-blocking manner. Returns a
- BorrowedPointer on success, otherwise std::nullopt (nothing).
+An InlineBorrowable is a Borrowable with the same guarded object and lock
+interface types, and it can be passed directly to APIs that expect a Borrowable
+reference.
- .. cpp:function:: template <class Rep, class Period> std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_for(std::chrono::duration<Rep, Period> timeout)
+Why use InlineBorrowable?
+-------------------------
+It is a safer and simpler way to guard an object for its entire lifetime. The
+unguarded object is never exposed and doesn't need to be stored in a separate
+variable or data member. The guarded object and its lock are guaranteed to have
+the same lifetime, and the lock cannot be re-used for any other purpose.
- Tries to borrow the object. Blocks until the specified timeout has elapsed
- or the object has been borrowed, whichever comes first. Returns a
- BorrowedPointer on success, otherwise std::nullopt (nothing).
+Constructing objects in-place
+-----------------------------
+The guarded object and its lock are constructed in-place by the
+InlineBorrowable, and any constructor parameters required by the object or
+its lock must be passed through the InlineBorrowable constructor. There are
+several ways to do this:
- .. cpp:function:: template <class Rep, class Period> std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_until(std::chrono::duration<Rep, Period> deadline)
+* Pass the parameters for the guarded object inline to the constructor. This is
+ the recommended way to construct the object when the lock does not require any
+ constructor parameters. Use the ``std::in_place`` marker to invoke the inline
+ constructor.
- Tries to borrow the object. Blocks until the specified deadline has been
- reached or the object has been borrowed, whichever comes first. Returns a
- BorrowedPointer on success, otherwise std::nullopt (nothing).
+ .. code-block:: cpp
+
+ InlineBorrowable<Foo> foo(std::in_place, foo_arg1, foo_arg2);
+ InlineBorrowable<std::array<int, 2>> foo_array(std::in_place, 1, 2);
+
+* Pass the parameters inside tuples:
+
+ .. code-block:: cpp
+
+ InlineBorrowable<Foo> foo(std::forward_as_tuple(foo_arg1, foo_arg2));
+
+ InlineBorrowable<Foo, MyLock> foo_lock(
+ std::forward_as_tuple(foo_arg1, foo_arg2),
+ std::forward_as_tuple(lock_arg1, lock_arg2));
+
+ .. note:: This approach only supports list initialization starting with C++20.
+
+* Use callables to construct the guarded object and lock object:
+
+ .. code-block:: cpp
+
+ InlineBorrowable<Foo> foo([&]{ return Foo{foo_arg1, foo_arg2}; });
+
+ InlineBorrowable<Foo, MyLock> foo_lock(
+ [&]{ return Foo{foo_arg1, foo_arg2}; }
+ [&]{ return MyLock{lock_arg1, lock_arg2}; }
+
+ .. note:: It is possible to construct and return objects that are not copyable
+ or movable, thanks to mandatory copy ellision (return value optimization).
+
+C++
+---
+.. doxygenclass:: pw::sync::InlineBorrowable
+ :members:
Example in C++
^^^^^^^^^^^^^^
-
.. code-block:: cpp
- #include <chrono>
+ #include <utility>
- #include "pw_bytes/span.h"
- #include "pw_i2c/initiator.h"
- #include "pw_status/try.h"
- #include "pw_status/result.h"
- #include "pw_sync/borrow.h"
- #include "pw_sync/mutex.h"
+ #include "pw_bytes/span.h"
+ #include "pw_i2c/initiator.h"
+ #include "pw_status/result.h"
+ #include "pw_sync/inline_borrowable.h"
- class ExampleI2c : public pw::i2c::Initiator;
+ struct I2cOptions;
- pw::sync::VirtualMutex i2c_mutex;
- ExampleI2c i2c;
- pw::sync::Borrowable<ExampleI2c> borrowable_i2c(i2c, i2c_mutex);
+ class ExampleI2c : public pw::i2c::Initiator {
+ public:
+ ExampleI2c(int bus_id, I2cOptions options);
+ // ...
+ };
- pw::Result<ConstByteSpan> ReadI2cData(ByteSpan buffer) {
- // Block indefinitely waiting to borrow the i2c bus.
- pw::sync::BorrowedPointer<ExampleI2c> borrowed_i2c =
- borrowable_i2c.acquire();
+ int kBusId;
+ I2cOptions opts;
- // Execute a sequence of transactions to get the needed data.
- PW_TRY(borrowed_i2c->WriteFor(kFirstWrite, std::chrono::milliseconds(50)));
- PW_TRY(borrowed_i2c->WriteReadFor(kSecondWrite, buffer,
- std::chrono::milliseconds(10)));
+ pw::sync::InlineBorrowable<ExampleI2c> i2c(std::in_place, kBusId, opts);
- // Borrowed i2c pointer is returned when the scope exits.
- return buffer;
- }
+ pw::Result<ConstByteSpan> ReadI2cData(
+ pw::sync::Borrowable<pw::i2c::Initiator>& initiator,
+ ByteSpan buffer);
+
+ pw::Result<ConstByteSpan> ReadData(ByteSpan buffer) {
+ return ReadI2cData(i2c, buffer);
+ }
--------------------
Signaling Primitives
--------------------
-
Native signaling primitives tend to vary more compared to critial section locks
across different platforms. For example, although common signaling primtives
like semaphores are in most if not all RTOSes and even POSIX, it was not in the
@@ -1209,27 +1033,30 @@ efficiently as possible for the platform that it is used on.
This simpler but highly portable class of signaling primitives is intended to
ensure that a portability efficiency tradeoff does not have to be made up front.
Today this is class of simpler signaling primitives is limited to the
-``pw::sync::ThreadNotification`` and ``pw::sync::TimedThreadNotification``.
+:cpp:class:`pw::sync::ThreadNotification` and
+:cpp:class:`pw::sync::TimedThreadNotification`.
ThreadNotification
==================
-The ThreadNotification is a synchronization primitive that can be used to
+.. cpp:namespace-push:: pw::sync
+
+The :cpp:class:`ThreadNotification` is a synchronization primitive that can be used to
permit a SINGLE thread to block and consume a latching, saturating
notification from multiple notifiers.
.. Note::
- Although only a single thread can block on a ThreadNotification at a time,
- many instances may be used by a single thread just like binary semaphores.
- This is in contrast to some native RTOS APIs, such as direct task
- notifications, which re-use the same state within a thread's context.
+ Although only a single thread can block on a :cpp:class:`ThreadNotification`
+ at a time, many instances may be used by a single thread just like binary
+ semaphores. This is in contrast to some native RTOS APIs, such as direct
+ task notifications, which re-use the same state within a thread's context.
.. Warning::
- This is a single consumer/waiter, multiple producer/notifier API!
- The acquire APIs must only be invoked by a single consuming thread. As a
- result, having multiple threads receiving notifications via the acquire API
- is unsupported.
+ This is a single consumer/waiter, multiple producer/notifier API!
+ The acquire APIs must only be invoked by a single consuming thread. As a
+ result, having multiple threads receiving notifications via the acquire API
+ is unsupported.
-This is effectively a subset of the ``pw::sync::BinarySemaphore`` API, except
+This is effectively a subset of the :cpp:class:`BinarySemaphore` API, except
that only a single thread can be notified and block at a time.
The single consumer aspect of the API permits the use of a smaller and/or
@@ -1238,415 +1065,324 @@ backed by the most efficient native primitive for a target, regardless of
whether that is a semaphore, event flag group, condition variable, or something
else.
+The :cpp:class:`ThreadNotification` is initialized to being empty (latch is not
+set).
+
+.. cpp:namespace-pop::
+
Generic BinarySemaphore-based Backend
-------------------------------------
-This module provides a generic backend for ``pw::sync::ThreadNotification`` via
+This module provides a generic backend for
+:cpp:class:`pw::sync::ThreadNotification` via
``pw_sync:binary_semaphore_thread_notification`` which uses a
-``pw::sync::BinarySemaphore`` as the backing primitive. See
+:cpp:class:`pw::sync::BinarySemaphore` as the backing primitive. See
:ref:`BinarySemaphore <module-pw_sync-binary-semaphore>` for backend
availability.
Optimized Backend
-----------------
.. list-table::
-
- * - *Supported on*
- - *Optimized backend module*
- * - FreeRTOS
- - ``pw_sync_freertos:thread_notification``
- * - ThreadX
- - Not possible, use ``pw_sync:binary_semaphore_thread_notification``
- * - embOS
- - Not needed, use ``pw_sync:binary_semaphore_thread_notification``
- * - STL
- - Not planned, use ``pw_sync:binary_semaphore_thread_notification``
- * - Baremetal
- - Planned
- * - Zephyr
- - Planned
- * - CMSIS-RTOS API v2 & RTX5
- - Planned
+ :header-rows: 1
+
+ * - Supported on
+ - Optimized backend module
+ * - FreeRTOS
+ - ``pw_sync_freertos:thread_notification``
+ * - ThreadX
+ - Not possible, use ``pw_sync:binary_semaphore_thread_notification``
+ * - embOS
+ - Not needed, use ``pw_sync:binary_semaphore_thread_notification``
+ * - STL
+ - Not planned, use ``pw_sync:binary_semaphore_thread_notification``
+ * - Baremetal
+ - Planned
+ * - Zephyr
+ - Planned
+ * - CMSIS-RTOS API v2 & RTX5
+ - Planned
C++
---
-.. cpp:class:: pw::sync::ThreadNotification
-
- .. cpp:function:: void acquire()
-
- Blocks indefinitely until the thread is notified, i.e. until the
- notification latch can be cleared because it was set.
-
- Clears the notification latch.
+.. doxygenclass:: pw::sync::ThreadNotification
+ :members:
- **IMPORTANT:** This should only be used by a single consumer thread.
+.. cpp:namespace-push:: pw::sync::ThreadNotification
- .. cpp:function:: bool try_acquire()
-
- Returns whether the thread has been notified, i.e. whether the notificion
- latch was set and resets the latch regardless.
-
- Clears the notification latch.
-
- Returns true if the thread was notified, meaning the the internal latch was
- reset successfully.
-
- **IMPORTANT:** This should only be used by a single consumer thread.
-
- .. cpp:function:: void release()
-
- Notifies the thread in a saturating manner, setting the notification latch.
-
- Raising the notification multiple time without it being acquired by the
- consuming thread is equivalent to raising the notification once to the
- thread. The notification is latched in case the thread was not waiting at
- the time.
-
- This is IRQ and thread safe.
-
- .. list-table::
+.. list-table::
+ :widths: 70 10 10 10
+ :header-rows: 1
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:class:`pw::sync::ThreadNotification::ThreadNotification`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::ThreadNotification::~ThreadNotification`
+ - ✔
+ -
+ -
+ * - :cpp:func:`acquire`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_acquire`
+ - ✔
+ -
+ -
+ * - :cpp:func:`release`
+ - ✔
+ - ✔
+ -
+
+.. cpp:namespace-pop::
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``ThreadNotification::ThreadNotification``
- - ✔
- -
- -
- * - ``ThreadNotification::~ThreadNotification``
- - ✔
- -
- -
- * - ``void ThreadNotification::acquire``
- - ✔
- -
- -
- * - ``bool ThreadNotification::try_acquire``
- - ✔
- -
- -
- * - ``void ThreadNotification::release``
- - ✔
- - ✔
- -
Examples in C++
^^^^^^^^^^^^^^^
.. code-block:: cpp
- #include "pw_sync/thread_notification.h"
- #include "pw_thread/thread_core.h"
+ #include "pw_sync/thread_notification.h"
+ #include "pw_thread/thread_core.h"
- class FooHandler() : public pw::thread::ThreadCore {
- // Public API invoked by other threads and/or interrupts.
- void NewFooAvailable() {
- new_foo_notification_.release();
- }
+ class FooHandler() : public pw::thread::ThreadCore {
+ // Public API invoked by other threads and/or interrupts.
+ void NewFooAvailable() {
+ new_foo_notification_.release();
+ }
- private:
- pw::sync::ThreadNotification new_foo_notification_;
+ private:
+ pw::sync::ThreadNotification new_foo_notification_;
- // Thread function.
- void Run() override {
- while (true) {
- new_foo_notification_.acquire();
- HandleFoo();
- }
- }
+ // Thread function.
+ void Run() override {
+ while (true) {
+ new_foo_notification_.acquire();
+ HandleFoo();
+ }
+ }
- void HandleFoo();
- }
+ void HandleFoo();
+ }
TimedThreadNotification
=======================
-The TimedThreadNotification is an extension of the ThreadNotification which
-offers timeout and deadline based semantics.
+The :cpp:class:`TimedThreadNotification` is an extension of the
+:cpp:class:`ThreadNotification` which offers timeout and deadline based
+semantics.
+
+The :cpp:class:`TimedThreadNotification` is initialized to being empty (latch is
+not set).
.. Warning::
- This is a single consumer/waiter, multiple producer/notifier API!
- The acquire APIs must only be invoked by a single consuming thread. As a
- result, having multiple threads receiving notifications via the acquire API
- is unsupported.
+ This is a single consumer/waiter, multiple producer/notifier API! The
+ acquire APIs must only be invoked by a single consuming thread. As a result,
+ having multiple threads receiving notifications via the acquire API is
+ unsupported.
Generic BinarySemaphore-based Backend
-------------------------------------
-This module provides a generic backend for ``pw::sync::TimedThreadNotification``
-via ``pw_sync:binary_semaphore_timed_thread_notification`` which uses a
-``pw::sync::BinarySemaphore`` as the backing primitive. See
+This module provides a generic backend for
+:cpp:class:`pw::sync::TimedThreadNotification` via
+``pw_sync:binary_semaphore_timed_thread_notification`` which uses a
+:cpp:class:`pw::sync::BinarySemaphore` as the backing primitive. See
:ref:`BinarySemaphore <module-pw_sync-binary-semaphore>` for backend
availability.
Optimized Backend
-----------------
.. list-table::
-
- * - *Supported on*
- - *Backend module*
- * - FreeRTOS
- - ``pw_sync_freertos:timed_thread_notification``
- * - ThreadX
- - Not possible, use ``pw_sync:binary_semaphore_timed_thread_notification``
- * - embOS
- - Not needed, use ``pw_sync:binary_semaphore_timed_thread_notification``
- * - STL
- - Not planned, use ``pw_sync:binary_semaphore_timed_thread_notification``
- * - Zephyr
- - Planned
- * - CMSIS-RTOS API v2 & RTX5
- - Planned
+ :header-rows: 1
+
+ * - Supported on
+ - Backend module
+ * - FreeRTOS
+ - ``pw_sync_freertos:timed_thread_notification``
+ * - ThreadX
+ - Not possible, use ``pw_sync:binary_semaphore_timed_thread_notification``
+ * - embOS
+ - Not needed, use ``pw_sync:binary_semaphore_timed_thread_notification``
+ * - STL
+ - Not planned, use ``pw_sync:binary_semaphore_timed_thread_notification``
+ * - Zephyr
+ - Planned
+ * - CMSIS-RTOS API v2 & RTX5
+ - Planned
C++
---
-.. cpp:class:: pw::sync::TimedThreadNotification
-
- .. cpp:function:: void acquire()
-
- Blocks indefinitely until the thread is notified, i.e. until the
- notification latch can be cleared because it was set.
-
- Clears the notification latch.
-
- **IMPORTANT:** This should only be used by a single consumer thread.
-
- .. cpp:function:: bool try_acquire()
-
- Returns whether the thread has been notified, i.e. whether the notificion
- latch was set and resets the latch regardless.
-
- Clears the notification latch.
-
- Returns true if the thread was notified, meaning the the internal latch was
- reset successfully.
-
- **IMPORTANT:** This should only be used by a single consumer thread.
-
- .. cpp:function:: void release()
-
- Notifies the thread in a saturating manner, setting the notification latch.
-
- Raising the notification multiple time without it being acquired by the
- consuming thread is equivalent to raising the notification once to the
- thread. The notification is latched in case the thread was not waiting at
- the time.
+.. doxygenclass:: pw::sync::TimedThreadNotification
+ :members:
- This is IRQ and thread safe.
+.. cpp:namespace-push:: pw::sync::TimedThreadNotification
- .. cpp:function:: bool try_acquire_for(chrono::SystemClock::duration timeout)
-
- Blocks until the specified timeout duration has elapsed or the thread
- has been notified (i.e. notification latch can be cleared because it was
- set), whichever comes first.
-
- Clears the notification latch.
-
- Returns true if the thread was notified, meaning the the internal latch was
- reset successfully.
-
- **IMPORTANT:** This should only be used by a single consumer thread.
-
- .. cpp:function:: bool try_acquire_until(chrono::SystemClock::time_point deadline)
-
- Blocks until the specified deadline time has been reached the thread has
- been notified (i.e. notification latch can be cleared because it was set),
- whichever comes first.
-
- Clears the notification latch.
-
- Returns true if the thread was notified, meaning the the internal latch was
- reset successfully.
-
- **IMPORTANT:** This should only be used by a single consumer thread.
-
- .. list-table::
-
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``TimedThreadNotification::TimedThreadNotification``
- - ✔
- -
- -
- * - ``TimedThreadNotification::~TimedThreadNotification``
- - ✔
- -
- -
- * - ``void TimedThreadNotification::acquire``
- - ✔
- -
- -
- * - ``bool TimedThreadNotification::try_acquire``
- - ✔
- -
- -
- * - ``bool TimedThreadNotification::try_acquire_for``
- - ✔
- -
- -
- * - ``bool TimedThreadNotification::try_acquire_until``
- - ✔
- -
- -
- * - ``void TimedThreadNotification::release``
- - ✔
- - ✔
- -
+.. list-table::
+ :widths: 70 10 10 10
+ :header-rows: 1
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:class:`pw::sync::TimedThreadNotification::TimedThreadNotification`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::TimedThreadNotification::~TimedThreadNotification`
+ - ✔
+ -
+ -
+ * - :cpp:func:`acquire`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_acquire`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_acquire_for`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_acquire_until`
+ - ✔
+ -
+ -
+ * - :cpp:func:`release`
+ - ✔
+ - ✔
+ -
+
+.. cpp:namespace-pop::
Examples in C++
^^^^^^^^^^^^^^^
.. code-block:: cpp
- #include "pw_sync/timed_thread_notification.h"
- #include "pw_thread/thread_core.h"
+ #include "pw_sync/timed_thread_notification.h"
+ #include "pw_thread/thread_core.h"
- class FooHandler() : public pw::thread::ThreadCore {
- // Public API invoked by other threads and/or interrupts.
- void NewFooAvailable() {
- new_foo_notification_.release();
- }
-
- private:
- pw::sync::TimedThreadNotification new_foo_notification_;
-
- // Thread function.
- void Run() override {
- while (true) {
- if (new_foo_notification_.try_acquire_for(kNotificationTimeout)) {
- HandleFoo();
- }
- DoOtherStuff();
- }
+ class FooHandler() : public pw::thread::ThreadCore {
+ // Public API invoked by other threads and/or interrupts.
+ void NewFooAvailable() {
+ new_foo_notification_.release();
}
- void HandleFoo();
- void DoOtherStuff();
- }
+ private:
+ pw::sync::TimedThreadNotification new_foo_notification_;
+
+ // Thread function.
+ void Run() override {
+ while (true) {
+ if (new_foo_notification_.try_acquire_for(kNotificationTimeout)) {
+ HandleFoo();
+ }
+ DoOtherStuff();
+ }
+ }
+
+ void HandleFoo();
+ void DoOtherStuff();
+ }
CountingSemaphore
=================
-The CountingSemaphore is a synchronization primitive that can be used for
-counting events and/or resource management where receiver(s) can block on
-acquire until notifier(s) signal by invoking release.
+.. cpp:namespace-push:: pw::sync
+
+The :cpp:class:`CountingSemaphore` is a synchronization primitive that can be
+used for counting events and/or resource management where receiver(s) can block
+on acquire until notifier(s) signal by invoking release.
-Note that unlike Mutexes, priority inheritance is not used by semaphores meaning
-semaphores are subject to unbounded priority inversions. Due to this, Pigweed
-does not recommend semaphores for mutual exclusion.
+Note that unlike :cpp:class:`Mutex`, priority inheritance is not used by
+semaphores meaning semaphores are subject to unbounded priority inversions. Due
+to this, Pigweed does not recommend semaphores for mutual exclusion.
-The CountingSemaphore is initialized to being empty or having no tokens.
+The :cpp:class:`CountingSemaphore` is initialized to being empty or having no
+tokens.
The entire API is thread safe, but only a subset is interrupt safe.
.. Note::
- If there is only a single consuming thread, we recommend using a
- ThreadNotification instead which can be much more efficient on some RTOSes
- such as FreeRTOS.
+ If there is only a single consuming thread, we recommend using a
+ :cpp:class:`ThreadNotification` instead which can be much more efficient on
+ some RTOSes such as FreeRTOS.
+
+.. cpp:namespace-pop::
.. Warning::
- Releasing multiple tokens is often not natively supported, meaning you may
- end up invoking the native kernel API many times, i.e. once per token you
- are releasing!
+ Releasing multiple tokens is often not natively supported, meaning you may
+ end up invoking the native kernel API many times, i.e. once per token you
+ are releasing!
.. list-table::
-
- * - *Supported on*
- - *Backend module*
- * - FreeRTOS
- - :ref:`module-pw_sync_freertos`
- * - ThreadX
- - :ref:`module-pw_sync_threadx`
- * - embOS
- - :ref:`module-pw_sync_embos`
- * - STL
- - :ref:`module-pw_sync_stl`
- * - Zephyr
- - Planned
- * - CMSIS-RTOS API v2 & RTX5
- - Planned
+ :header-rows: 1
+
+ * - Supported on
+ - Backend module
+ * - FreeRTOS
+ - :ref:`module-pw_sync_freertos`
+ * - ThreadX
+ - :ref:`module-pw_sync_threadx`
+ * - embOS
+ - :ref:`module-pw_sync_embos`
+ * - STL
+ - :ref:`module-pw_sync_stl`
+ * - Zephyr
+ - Planned
+ * - CMSIS-RTOS API v2 & RTX5
+ - Planned
C++
---
-.. cpp:class:: pw::sync::CountingSemaphore
-
- .. cpp:function:: void acquire()
-
- Decrements the internal counter by 1 or blocks indefinitely until it can.
- This is thread safe, but not IRQ safe.
-
- .. cpp:function:: bool try_acquire() noexcept
-
- Tries to decrement by the internal counter by 1 without blocking.
- Returns true if the internal counter was decremented successfully.
- This is thread and IRQ safe.
-
- .. cpp:function:: bool try_acquire_for(chrono::SystemClock::duration timeout)
-
- Tries to decrement the internal counter by 1. Blocks until the specified
- timeout has elapsed or the counter was decremented by 1, whichever comes
- first.
- Returns true if the internal counter was decremented successfully.
- This is thread safe, but not IRQ safe.
-
- .. cpp:function:: bool try_acquire_until(chrono::SystemClock::time_point deadline)
-
- Tries to decrement the internal counter by 1. Blocks until the specified
- deadline has been reached or the counter was decremented by 1, whichever
- comes first.
- Returns true if the internal counter was decremented successfully.
- This is thread safe, but not IRQ safe.
-
- .. cpp:function:: void release(ptrdiff_t update = 1)
-
- Atomically increments the internal counter by the value of update.
- Any thread(s) waiting for the counter to be greater than 0, i.e.
- blocked in acquire, will subsequently be unblocked.
- This is thread and IRQ safe.
-
- **Precondition:** update >= 0
-
- **Precondition:** update <= max() - counter
-
- .. cpp:function:: static constexpr ptrdiff_t max() noexcept
-
- Returns the internal counter's maximum possible value.
-
- .. list-table::
-
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``CountingSemaphore::CountingSemaphore``
- - ✔
- -
- -
- * - ``CountingSemaphore::~CountingSemaphore``
- - ✔
- -
- -
- * - ``void CountingSemaphore::acquire``
- - ✔
- -
- -
- * - ``bool CountingSemaphore::try_acquire``
- - ✔
- - ✔
- -
- * - ``bool CountingSemaphore::try_acquire_for``
- - ✔
- -
- -
- * - ``bool CountingSemaphore::try_acquire_until``
- - ✔
- -
- -
- * - ``void CountingSemaphore::release``
- - ✔
- - ✔
- -
- * - ``void CountingSemaphore::max``
- - ✔
- - ✔
- - ✔
+.. doxygenclass:: pw::sync::CountingSemaphore
+ :members:
+
+.. cpp:namespace-push:: pw::sync::CountingSemaphore
+
+.. list-table::
+ :widths: 70 10 10 10
+ :header-rows: 1
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:class:`pw::sync::CountingSemaphore::CountingSemaphore`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::CountingSemaphore::~CountingSemaphore`
+ - ✔
+ -
+ -
+ * - :cpp:func:`acquire`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_acquire`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`try_acquire_for`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_acquire_until`
+ - ✔
+ -
+ -
+ * - :cpp:func:`release`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`max`
+ - ✔
+ - ✔
+ - ✔
+
+.. cpp:namespace-pop::
Examples in C++
^^^^^^^^^^^^^^^
@@ -1656,201 +1392,164 @@ you detect whether you ever fall behind.
.. code-block:: cpp
- #include "pw_sync/counting_semaphore.h"
- #include "pw_thread/thread_core.h"
+ #include "pw_sync/counting_semaphore.h"
+ #include "pw_thread/thread_core.h"
- class PeriodicWorker() : public pw::thread::ThreadCore {
- // Public API invoked by a higher frequency timer interrupt.
- void TimeToExecute() {
- periodic_run_semaphore_.release();
- }
-
- private:
- pw::sync::CountingSemaphore periodic_run_semaphore_;
-
- // Thread function.
- void Run() override {
- while (true) {
- size_t behind_by_n_cycles = 0;
- periodic_run_semaphore_.acquire(); // Wait to run until it's time.
- while (periodic_run_semaphore_.try_acquire()) {
- ++behind_by_n_cycles;
- }
- if (behind_by_n_cycles > 0) {
- PW_LOG_WARNING("Not keeping up, behind by %d cycles",
- behind_by_n_cycles);
- }
- DoPeriodicWork();
- }
+ class PeriodicWorker() : public pw::thread::ThreadCore {
+ // Public API invoked by a higher frequency timer interrupt.
+ void TimeToExecute() {
+ periodic_run_semaphore_.release();
}
- void DoPeriodicWork();
- }
-
-
+ private:
+ pw::sync::CountingSemaphore periodic_run_semaphore_;
+
+ // Thread function.
+ void Run() override {
+ while (true) {
+ size_t behind_by_n_cycles = 0;
+ periodic_run_semaphore_.acquire(); // Wait to run until it's time.
+ while (periodic_run_semaphore_.try_acquire()) {
+ ++behind_by_n_cycles;
+ }
+ if (behind_by_n_cycles > 0) {
+ PW_LOG_WARNING("Not keeping up, behind by %d cycles",
+ behind_by_n_cycles);
+ }
+ DoPeriodicWork();
+ }
+ }
+
+ void DoPeriodicWork();
+ }
.. _module-pw_sync-binary-semaphore:
BinarySemaphore
===============
-BinarySemaphore is a specialization of CountingSemaphore with an arbitrary token
-limit of 1. Note that that ``max()`` is >= 1, meaning it may be released up to
-``max()`` times but only acquired once for those N releases.
+.. cpp:namespace-push:: pw::sync
+
+:cpp:class:`BinarySemaphore` is a specialization of CountingSemaphore with an
+arbitrary token limit of 1. Note that that ``max()`` is >= 1, meaning it may be
+released up to ``max()`` times but only acquired once for those N releases.
+
+Implementations of :cpp:class:`BinarySemaphore` are typically more
+efficient than the default implementation of :cpp:class:`CountingSemaphore`.
-Implementations of BinarySemaphore are typically more efficient than the
-default implementation of CountingSemaphore.
+The :cpp:class:`BinarySemaphore` is initialized to being empty or having no
+tokens.
-The BinarySemaphore is initialized to being empty or having no tokens.
+.. cpp:namespace-pop::
The entire API is thread safe, but only a subset is interrupt safe.
.. Note::
- If there is only a single consuming thread, we recommend using a
- ThreadNotification instead which can be much more efficient on some RTOSes
- such as FreeRTOS.
-
+ If there is only a single consuming thread, we recommend using a
+ ThreadNotification instead which can be much more efficient on some RTOSes
+ such as FreeRTOS.
.. list-table::
-
- * - *Supported on*
- - *Backend module*
- * - FreeRTOS
- - :ref:`module-pw_sync_freertos`
- * - ThreadX
- - :ref:`module-pw_sync_threadx`
- * - embOS
- - :ref:`module-pw_sync_embos`
- * - STL
- - :ref:`module-pw_sync_stl`
- * - Zephyr
- - Planned
- * - CMSIS-RTOS API v2 & RTX5
- - Planned
+ :header-rows: 1
+
+ * - Supported on
+ - Backend module
+ * - FreeRTOS
+ - :ref:`module-pw_sync_freertos`
+ * - ThreadX
+ - :ref:`module-pw_sync_threadx`
+ * - embOS
+ - :ref:`module-pw_sync_embos`
+ * - STL
+ - :ref:`module-pw_sync_stl`
+ * - Zephyr
+ - Planned
+ * - CMSIS-RTOS API v2 & RTX5
+ - Planned
C++
---
-.. cpp:class:: pw::sync::BinarySemaphore
-
- .. cpp:function:: void acquire()
-
- Decrements the internal counter to 0 or blocks indefinitely until it can.
- This is thread safe, but not IRQ safe.
-
- .. cpp:function:: bool try_acquire() noexcept
-
- Tries to decrement by the internal counter to 0 without blocking.
- Returns true if the internal counter was decremented successfully.
- This is thread and IRQ safe.
-
- .. cpp:function:: bool try_acquire_for(chrono::SystemClock::duration timeout)
-
- Tries to decrement the internal counter to 0. Blocks until the specified
- timeout has elapsed or the counter was decremented to 0, whichever comes
- first.
- Returns true if the internal counter was decremented successfully.
- This is thread safe, but not IRQ safe.
-
- .. cpp:function:: bool try_acquire_until(chrono::SystemClock::time_point deadline)
-
- Tries to decrement the internal counter to 0. Blocks until the specified
- deadline has been reached or the counter was decremented to 0, whichever
- comes first.
- Returns true if the internal counter was decremented successfully.
- This is thread safe, but not IRQ safe.
-
- .. cpp:function:: void release()
-
- Atomically increments the internal counter by 1.
- Any thread(s) waiting for the counter to be greater than 0, i.e.
- blocked in acquire, will subsequently be unblocked.
- This is thread and IRQ safe.
-
- There exists an overflow risk if one releases more than max() times
- between acquires because many RTOS implementations internally
- increment the counter past one where it is only cleared when acquired.
-
- **Precondition:** 1 <= max() - counter
-
- .. cpp:function:: static constexpr ptrdiff_t max() noexcept
-
- Returns the internal counter's maximum possible value.
-
- .. list-table::
-
- * - *Safe to use in context*
- - *Thread*
- - *Interrupt*
- - *NMI*
- * - ``BinarySemaphore::BinarySemaphore``
- - ✔
- -
- -
- * - ``BinarySemaphore::~BinarySemaphore``
- - ✔
- -
- -
- * - ``void BinarySemaphore::acquire``
- - ✔
- -
- -
- * - ``bool BinarySemaphore::try_acquire``
- - ✔
- - ✔
- -
- * - ``bool BinarySemaphore::try_acquire_for``
- - ✔
- -
- -
- * - ``bool BinarySemaphore::try_acquire_until``
- - ✔
- -
- -
- * - ``void BinarySemaphore::release``
- - ✔
- - ✔
- -
- * - ``void BinarySemaphore::max``
- - ✔
- - ✔
- - ✔
+.. doxygenclass:: pw::sync::BinarySemaphore
+ :members:
+
+.. cpp:namespace-push:: pw::sync::BinarySemaphore
+
+.. list-table::
+ :widths: 70 10 10 10
+ :header-rows: 1
+
+ * - Safe to use in context
+ - Thread
+ - Interrupt
+ - NMI
+ * - :cpp:class:`pw::sync::BinarySemaphore::BinarySemaphore`
+ - ✔
+ -
+ -
+ * - :cpp:func:`pw::sync::BinarySemaphore::~BinarySemaphore`
+ - ✔
+ -
+ -
+ * - :cpp:func:`acquire`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_acquire`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`try_acquire_for`
+ - ✔
+ -
+ -
+ * - :cpp:func:`try_acquire_until`
+ - ✔
+ -
+ -
+ * - :cpp:func:`release`
+ - ✔
+ - ✔
+ -
+ * - :cpp:func:`max`
+ - ✔
+ - ✔
+ - ✔
+
+.. cpp:namespace-pop::
Examples in C++
^^^^^^^^^^^^^^^
.. code-block:: cpp
- #include "pw_sync/binary_semaphore.h"
- #include "pw_thread/thread_core.h"
+ #include "pw_sync/binary_semaphore.h"
+ #include "pw_thread/thread_core.h"
- class FooHandler() : public pw::thread::ThreadCore {
- // Public API invoked by other threads and/or interrupts.
- void NewFooAvailable() {
- new_foo_semaphore_.release();
- }
-
- private:
- pw::sync::BinarySemaphore new_foo_semaphore_;
-
- // Thread function.
- void Run() override {
- while (true) {
- if (new_foo_semaphore_.try_acquire_for(kNotificationTimeout)) {
- HandleFoo();
- }
- DoOtherStuff();
- }
+ class FooHandler() : public pw::thread::ThreadCore {
+ // Public API invoked by other threads and/or interrupts.
+ void NewFooAvailable() {
+ new_foo_semaphore_.release();
}
- void HandleFoo();
- void DoOtherStuff();
- }
+ private:
+ pw::sync::BinarySemaphore new_foo_semaphore_;
+
+ // Thread function.
+ void Run() override {
+ while (true) {
+ if (new_foo_semaphore_.try_acquire_for(kNotificationTimeout)) {
+ HandleFoo();
+ }
+ DoOtherStuff();
+ }
+ }
+
+ void HandleFoo();
+ void DoOtherStuff();
+ }
Conditional Variables
=====================
-We've decided for now to skip on conditional variables. These are constructs,
-which are typically not natively available on RTOSes. CVs would have to be
-backed by a multiple hidden semaphore(s) in addition to the explicit public
-mutex. In other words a CV typically ends up as a a composition of
-synchronization primitives on RTOSes. That being said, one could implement them
-using our semaphore and mutex layers and we may consider providing this in the
-future. However for most of our resource constrained customers they will mostly
-likely be using semaphores more often than CVs.
+:cpp:class:`pw::sync::ConditionVariable` provides a condition variable
+implementation that provides semantics and an API very similar to
+`std::condition_variable
+<https://en.cppreference.com/w/cpp/thread/condition_variable>`_ in the C++
+Standard Library.
diff --git a/pw_sync/inline_borrowable_test.cc b/pw_sync/inline_borrowable_test.cc
new file mode 100644
index 000000000..4e55ac771
--- /dev/null
+++ b/pw_sync/inline_borrowable_test.cc
@@ -0,0 +1,224 @@
+// Copyright 2021 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_sync/inline_borrowable.h"
+
+#include <array>
+#include <chrono>
+#include <tuple>
+
+#include "gtest/gtest.h"
+#include "pw_sync/interrupt_spin_lock.h"
+#include "pw_sync/lock_annotations.h"
+#include "pw_sync/mutex.h"
+
+namespace pw::sync {
+namespace {
+
+using namespace std::chrono_literals;
+
+// A trivial type that is copyable and movable.
+struct TrivialType {
+ bool yes() const { return true; }
+};
+
+// A custom type that is neither copyable nor movable.
+class CustomType {
+ public:
+ explicit constexpr CustomType(int z) : x_(z), y_(-z) {}
+ constexpr CustomType(int x, int y) : x_(x), y_(y) {}
+
+ CustomType(const CustomType&) = delete;
+ CustomType& operator=(const CustomType&) = delete;
+ CustomType(CustomType&&) = delete;
+ CustomType&& operator=(CustomType&&) = delete;
+
+ std::pair<int, int> data() const { return std::make_pair(x_, y_); }
+
+ private:
+ int x_, y_;
+};
+
+// A custom lockable interface.
+class PW_LOCKABLE("VirtualCustomLocakble") VirtualCustomLockable {
+ public:
+ virtual ~VirtualCustomLockable() {}
+
+ virtual void lock() PW_EXCLUSIVE_LOCK_FUNCTION() = 0;
+ virtual void unlock() PW_UNLOCK_FUNCTION() = 0;
+};
+
+// A custom mutex type that requires a constructor parameter.
+class PW_LOCKABLE("VirtualCustomMutex") VirtualCustomMutex
+ : public VirtualCustomLockable {
+ public:
+ explicit VirtualCustomMutex(int id) : mutex_{}, id_{id} {}
+
+ void lock() override PW_EXCLUSIVE_LOCK_FUNCTION() { mutex_.lock(); }
+ void unlock() override PW_UNLOCK_FUNCTION() { mutex_.unlock(); }
+
+ int id() const { return id_; }
+
+ private:
+ pw::sync::Mutex mutex_;
+ int id_;
+};
+
+TEST(InlineBorrowableTest, TestTrivialType) {
+ InlineBorrowable<TrivialType> trivial;
+ EXPECT_TRUE(trivial.acquire()->yes());
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeInPlace1Arg) {
+ InlineBorrowable<CustomType> custom(std::in_place, 1);
+ EXPECT_EQ(custom.acquire()->data(), std::make_pair(1, -1));
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeInPlace1ArgLValue) {
+ int x = 1;
+ InlineBorrowable<CustomType> custom(std::in_place, x);
+ EXPECT_EQ(custom.acquire()->data(), std::make_pair(x, -x));
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeInPlace2Arg) {
+ InlineBorrowable<CustomType> custom(std::in_place, 1, 2);
+ EXPECT_EQ(custom.acquire()->data(), std::make_pair(1, 2));
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeFromTuple) {
+ InlineBorrowable<CustomType> custom(std::make_tuple(1, 2));
+ EXPECT_EQ(custom.acquire()->data(), std::make_pair(1, 2));
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeFromFactory) {
+ InlineBorrowable<CustomType> custom([] { return CustomType(1, 2); });
+ EXPECT_EQ(custom.acquire()->data(), std::make_pair(1, 2));
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeFromMutableFactory) {
+ int i = 0;
+ auto factory = [&i]() mutable {
+ i++;
+ return CustomType(1, 2);
+ };
+ InlineBorrowable<CustomType> custom(factory);
+ EXPECT_EQ(custom.acquire()->data(), std::make_pair(1, 2));
+}
+
+TEST(InlineBorrowableTest, TestTrivialTypeWithInterruptSpinLock) {
+ InlineBorrowable<TrivialType, VirtualInterruptSpinLock>
+ trivial_interrupt_safe;
+ EXPECT_TRUE(trivial_interrupt_safe.acquire()->yes());
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeWithInterruptSpinLock) {
+ InlineBorrowable<CustomType, VirtualInterruptSpinLock> custom_interrupt_safe(
+ std::in_place, 1, 2);
+ EXPECT_EQ(custom_interrupt_safe.acquire()->data(), std::make_pair(1, 2));
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeWithCustomMutexFromTuple) {
+ InlineBorrowable<CustomType, VirtualCustomMutex, VirtualCustomLockable>
+ custom_mutex(std::make_tuple(1, 2), std::make_tuple(42));
+ EXPECT_EQ(custom_mutex.acquire()->data(), std::make_pair(1, 2));
+}
+
+TEST(InlineBorrowableTest, TestCustomTypeWithCustomMutexFromFactory) {
+ InlineBorrowable<CustomType, VirtualCustomMutex, VirtualCustomLockable>
+ custom_mutex([] { return CustomType(1, 2); },
+ [] { return VirtualCustomMutex(42); });
+ EXPECT_EQ(custom_mutex.acquire()->data(), std::make_pair(1, 2));
+}
+
+TEST(InlineBorrowableTest, TestArrayAggregateInitializationInPlace) {
+ using ArrayAggregate = std::array<int, 2>;
+ InlineBorrowable<ArrayAggregate> aggregate{std::in_place, 1, 2};
+ EXPECT_EQ((*aggregate.acquire())[0], 1);
+ EXPECT_EQ((*aggregate.acquire())[1], 2);
+}
+
+struct StructAggregate {
+ int a;
+ int b;
+};
+
+TEST(InlineBorrowableTest, TestStructAggregateInitializationInPlace) {
+ InlineBorrowable<StructAggregate> aggregate{std::in_place, 1, 2};
+ EXPECT_EQ(aggregate.acquire()->a, 1);
+ EXPECT_EQ(aggregate.acquire()->b, 2);
+}
+
+TEST(InlineBorrowableTest, TestStructAggregateInitializationFromFactory) {
+ InlineBorrowable<StructAggregate> aggregate(
+ []() -> StructAggregate { return {.a = 1, .b = 2}; });
+ EXPECT_EQ(aggregate.acquire()->a, 1);
+ EXPECT_EQ(aggregate.acquire()->b, 2);
+}
+
+TEST(InlineBorrowableTest,
+ TestStructAggregateInitializationFromMutableFactory) {
+ int i = 0;
+ auto factory = [&i]() mutable -> StructAggregate {
+ i++;
+ return {.a = 1, .b = 2};
+ };
+ InlineBorrowable<StructAggregate> aggregate(factory);
+ EXPECT_EQ(aggregate.acquire()->a, 1);
+ EXPECT_EQ(aggregate.acquire()->b, 2);
+}
+
+struct ReferenceTypes {
+ ReferenceTypes(const int& a, int& b, BorrowedPointer<int>&& c)
+ : in(a), out(b), borrowed(std::move(c)) {}
+ const int& in;
+ int& out;
+ BorrowedPointer<int> borrowed; // move-only type
+};
+
+class InlineBorrowableReferenceTypesTest : public ::testing::Test {
+ protected:
+ int input_ = 1;
+ int output_ = 2;
+ InlineBorrowable<int> borrowable_{std::in_place, 3};
+
+ void Validate(BorrowedPointer<ReferenceTypes>&& references) {
+ EXPECT_EQ(references->in, 1);
+ EXPECT_EQ(references->out, 2);
+ EXPECT_EQ(*references->borrowed, 3);
+
+ references->out = -2;
+ EXPECT_EQ(output_, -2);
+ }
+};
+
+TEST_F(InlineBorrowableReferenceTypesTest, TestInPlace) {
+ InlineBorrowable<ReferenceTypes> references(
+ std::in_place, input_, output_, borrowable_.acquire());
+ Validate(references.acquire());
+}
+
+TEST_F(InlineBorrowableReferenceTypesTest, TestFromTuple) {
+ InlineBorrowable<ReferenceTypes> references(
+ std::forward_as_tuple(input_, output_, borrowable_.acquire()));
+ Validate(references.acquire());
+}
+
+TEST_F(InlineBorrowableReferenceTypesTest, TestFromFactory) {
+ InlineBorrowable<ReferenceTypes> references(
+ [&] { return ReferenceTypes(input_, output_, borrowable_.acquire()); });
+ Validate(references.acquire());
+}
+
+} // namespace
+} // namespace pw::sync
diff --git a/pw_sync/interrupt_spin_lock_facade_test.cc b/pw_sync/interrupt_spin_lock_facade_test.cc
index fa3a72321..86a751c03 100644
--- a/pw_sync/interrupt_spin_lock_facade_test.cc
+++ b/pw_sync/interrupt_spin_lock_facade_test.cc
@@ -37,13 +37,13 @@ TEST(InterruptSpinLock, LockUnlock) {
interrupt_spin_lock.unlock();
}
-// TODO(pwbug/291): Add real concurrency tests once we have pw::thread on SMP
+// TODO(b/235284163): Add real concurrency tests once we have pw::thread on SMP
// systems given that uniprocessor systems cannot fail to acquire an ISL.
InterruptSpinLock static_interrupt_spin_lock;
TEST(InterruptSpinLock, LockUnlockStatic) {
static_interrupt_spin_lock.lock();
- // TODO(pwbug/291): Ensure other cores fail to lock when its locked.
+ // TODO(b/235284163): Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(static_interrupt_spin_lock.try_lock());
static_interrupt_spin_lock.unlock();
}
@@ -53,7 +53,7 @@ TEST(InterruptSpinLock, TryLockUnlock) {
const bool locked = interrupt_spin_lock.try_lock();
EXPECT_TRUE(locked);
if (locked) {
- // TODO(pwbug/291): Ensure other cores fail to lock when its locked.
+ // TODO(b/235284163): Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(interrupt_spin_lock.try_lock());
interrupt_spin_lock.unlock();
}
@@ -62,7 +62,7 @@ TEST(InterruptSpinLock, TryLockUnlock) {
TEST(VirtualInterruptSpinLock, LockUnlock) {
pw::sync::VirtualInterruptSpinLock interrupt_spin_lock;
interrupt_spin_lock.lock();
- // TODO(pwbug/291): Ensure other cores fail to lock when its locked.
+ // TODO(b/235284163): Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(interrupt_spin_lock.try_lock());
interrupt_spin_lock.unlock();
}
@@ -70,7 +70,7 @@ TEST(VirtualInterruptSpinLock, LockUnlock) {
VirtualInterruptSpinLock static_virtual_interrupt_spin_lock;
TEST(VirtualInterruptSpinLock, LockUnlockStatic) {
static_virtual_interrupt_spin_lock.lock();
- // TODO(pwbug/291): Ensure other cores fail to lock when its locked.
+ // TODO(b/235284163): Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(static_virtual_interrupt_spin_lock.try_lock());
static_virtual_interrupt_spin_lock.unlock();
}
@@ -84,7 +84,7 @@ TEST(InterruptSpinLock, LockUnlockInC) {
TEST(InterruptSpinLock, TryLockUnlockInC) {
pw::sync::InterruptSpinLock interrupt_spin_lock;
ASSERT_TRUE(pw_sync_InterruptSpinLock_CallTryLock(&interrupt_spin_lock));
- // TODO(pwbug/291): Ensure other cores fail to lock when its locked.
+ // TODO(b/235284163): Ensure other cores fail to lock when its locked.
// EXPECT_FALSE(pw_sync_InterruptSpinLock_CallTryLock(&interrupt_spin_lock));
pw_sync_InterruptSpinLock_CallUnlock(&interrupt_spin_lock);
}
diff --git a/pw_sync/mutex_facade_test.cc b/pw_sync/mutex_facade_test.cc
index 771b0032a..4c7cdd03a 100644
--- a/pw_sync/mutex_facade_test.cc
+++ b/pw_sync/mutex_facade_test.cc
@@ -29,12 +29,12 @@ void pw_sync_Mutex_CallUnlock(pw_sync_Mutex* mutex);
} // extern "C"
-// TODO(pwbug/291): Add real concurrency tests once we have pw::thread.
+// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
TEST(Mutex, LockUnlock) {
pw::sync::Mutex mutex;
mutex.lock();
- // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // TODO(b/235284163): Ensure it fails to lock when already held.
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
@@ -42,7 +42,7 @@ TEST(Mutex, LockUnlock) {
Mutex static_mutex;
TEST(Mutex, LockUnlockStatic) {
static_mutex.lock();
- // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // TODO(b/235284163): Ensure it fails to lock when already held.
// EXPECT_FALSE(static_mutex.try_lock());
static_mutex.unlock();
}
@@ -52,7 +52,7 @@ TEST(Mutex, TryLockUnlock) {
const bool locked = mutex.try_lock();
EXPECT_TRUE(locked);
if (locked) {
- // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // TODO(b/235284163): Ensure it fails to lock when already held.
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
@@ -61,7 +61,7 @@ TEST(Mutex, TryLockUnlock) {
TEST(VirtualMutex, LockUnlock) {
pw::sync::VirtualMutex mutex;
mutex.lock();
- // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // TODO(b/235284163): Ensure it fails to lock when already held.
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
@@ -69,7 +69,7 @@ TEST(VirtualMutex, LockUnlock) {
VirtualMutex static_virtual_mutex;
TEST(VirtualMutex, LockUnlockStatic) {
static_virtual_mutex.lock();
- // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // TODO(b/235284163): Ensure it fails to lock when already held.
// EXPECT_FALSE(static_virtual_mutex.try_lock());
static_virtual_mutex.unlock();
}
@@ -83,7 +83,7 @@ TEST(Mutex, LockUnlockInC) {
TEST(Mutex, TryLockUnlockInC) {
pw::sync::Mutex mutex;
ASSERT_TRUE(pw_sync_Mutex_CallTryLock(&mutex));
- // TODO(pwbug/291): Ensure it fails to lock when already held.
+ // TODO(b/235284163): Ensure it fails to lock when already held.
// EXPECT_FALSE(pw_sync_Mutex_CallTryLock(&mutex));
pw_sync_Mutex_CallUnlock(&mutex);
}
diff --git a/pw_sync/public/pw_sync/binary_semaphore.h b/pw_sync/public/pw_sync/binary_semaphore.h
index a0516051e..c3e025dea 100644
--- a/pw_sync/public/pw_sync/binary_semaphore.h
+++ b/pw_sync/public/pw_sync/binary_semaphore.h
@@ -25,17 +25,19 @@
namespace pw::sync {
-// BinarySemaphore is a specialization of CountingSemaphore with an arbitrary
-// token limit of 1. Note that that max() is >= 1, meaning it may be
-// released up to max() times but only acquired once for those N releases.
-// Implementations of BinarySemaphore are typically more efficient than the
-// default implementation of CountingSemaphore. The entire API is thread safe
-// but only a subset is IRQ safe.
-//
-// WARNING: In order to support global statically constructed BinarySemaphores,
-// the user and/or backend MUST ensure that any initialization required in your
-// environment is done prior to the creation and/or initialization of the native
-// synchronization primitives (e.g. kernel initialization).
+/// `BinarySemaphore` is a specialization of `CountingSemaphore` with an
+/// arbitrary token limit of 1. Note that that max() is >= 1, meaning it may be
+/// released up to `max()` times but only acquired once for those `N` releases.
+/// Implementations of `BinarySemaphore` are typically more efficient than the
+/// default implementation of `CountingSemaphore`. The entire API is thread safe
+/// but only a subset is IRQ safe.
+///
+/// WARNING: In order to support global statically constructed BinarySemaphores,
+/// the user and/or backend MUST ensure that any initialization required in your
+/// environment is done prior to the creation and/or initialization of the
+/// native synchronization primitives (e.g. kernel initialization).
+///
+/// The `BinarySemaphore` is initialized to being empty or having no tokens.
class BinarySemaphore {
public:
using native_handle_type = backend::NativeBinarySemaphoreHandle;
@@ -47,41 +49,50 @@ class BinarySemaphore {
BinarySemaphore& operator=(const BinarySemaphore&) = delete;
BinarySemaphore& operator=(BinarySemaphore&&) = delete;
- // Atomically increments the internal counter by 1.
- // Any thread(s) waiting for the counter to be greater than 0, i.e.
- // blocked in acquire, will subsequently be unblocked.
- // This is thread and IRQ safe.
- //
- // There exists an overflow risk if one releases more than max() times
- // between acquires because many RTOS implementations internally
- // increment the counter past one where it is only cleared when acquired.
- //
- // Precondition: 1 <= max() - counter
+ /// Atomically increments the internal counter by 1.
+ /// Any thread(s) waiting for the counter to be greater than 0, i.e.
+ /// blocked in acquire, will subsequently be unblocked.
+ /// This is thread and IRQ safe.
+ ///
+ /// There exists an overflow risk if one releases more than max() times
+ /// between acquires because many RTOS implementations internally
+ /// increment the counter past one where it is only cleared when acquired.
+ ///
+ /// @b PRECONDITION: `1 <= max() - counter`
void release();
- // Decrements the internal counter to 0 or blocks indefinitely until it can.
- // This is thread safe, but not IRQ safe.
+ /// Decrements the internal counter to 0 or blocks indefinitely until it can.
+ ///
+ /// This is thread safe, but not IRQ safe.
void acquire();
- // Tries to decrement by the internal counter to 0 without blocking.
- // Returns true if the internal counter was reset successfully.
- // This is thread and IRQ safe.
+ /// Tries to decrement by the internal counter to 0 without blocking.
+ ///
+ /// @retval true if the internal counter was reset successfully.
+ ///
+ /// This is thread and IRQ safe.
bool try_acquire() noexcept;
- // Tries to decrement the internal counter to 0. Blocks until the specified
- // timeout has elapsed or the counter was decremented to 0, whichever comes
- // first.
- // Returns true if the internal counter was decremented successfully.
- // This is thread safe, but not IRQ safe.
+ /// Tries to decrement the internal counter to 0. Blocks until the specified
+ /// timeout has elapsed or the counter was decremented to 0, whichever comes
+ /// first.
+ ///
+ /// @retval true if the internal counter was decremented successfully.
+ ///
+ /// This is thread safe, but not IRQ safe.
bool try_acquire_for(chrono::SystemClock::duration timeout);
- // Tries to decrement the internal counter to 0. Blocks until the specified
- // deadline has been reached or the counter was decremented to 0, whichever
- // comes first.
- // Returns true if the internal counter was decremented successfully.
- // This is thread safe, but not IRQ safe.
+ /// Tries to decrement the internal counter to 0. Blocks until the specified
+ /// deadline has been reached or the counter was decremented to 0, whichever
+ /// comes first.
+ ///
+ /// @retval true if the internal counter was decremented successfully.
+ ///
+ /// This is thread safe, but not IRQ safe.
bool try_acquire_until(chrono::SystemClock::time_point deadline);
+ /// @retval backend::kBinarySemaphoreMaxValue the internal counter's maximum
+ /// possible value.
static constexpr ptrdiff_t max() noexcept {
return backend::kBinarySemaphoreMaxValue;
}
@@ -89,7 +100,7 @@ class BinarySemaphore {
native_handle_type native_handle();
private:
- // This may be a wrapper around a native type with additional members.
+ /// This may be a wrapper around a native type with additional members.
backend::NativeBinarySemaphore native_type_;
};
diff --git a/pw_sync/public/pw_sync/borrow.h b/pw_sync/public/pw_sync/borrow.h
index da5d0d9ee..204b888a9 100644
--- a/pw_sync/public/pw_sync/borrow.h
+++ b/pw_sync/public/pw_sync/borrow.h
@@ -23,23 +23,23 @@
namespace pw::sync {
-// The BorrowedPointer is an RAII handle which wraps a pointer to a borrowed
-// object along with a held lock which is guarding the object. When destroyed,
-// the lock is released.
+/// The `BorrowedPointer` is an RAII handle which wraps a pointer to a borrowed
+/// object along with a held lock which is guarding the object. When destroyed,
+/// the lock is released.
template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class BorrowedPointer {
public:
- // Release the lock on destruction.
+ /// Release the lock on destruction.
~BorrowedPointer() {
if (lock_ != nullptr) {
lock_->unlock();
}
}
- // This object is moveable, but not copyable.
- //
- // Postcondition: The other BorrowedPointer is no longer valid and will assert
- // if the GuardedType is accessed.
+ /// This object is moveable, but not copyable.
+ ///
+ /// @b Postcondition: The other BorrowedPointer is no longer valid and will
+ /// assert if the GuardedType is accessed.
BorrowedPointer(BorrowedPointer&& other)
: lock_(other.lock_), object_(other.object_) {
other.lock_ = nullptr;
@@ -55,26 +55,42 @@ class BorrowedPointer {
BorrowedPointer(const BorrowedPointer&) = delete;
BorrowedPointer& operator=(const BorrowedPointer&) = delete;
- // Provides access to the borrowed object's members.
+ /// Provides access to the borrowed object's members.
GuardedType* operator->() {
PW_ASSERT(object_ != nullptr); // Ensure this isn't a stale moved instance.
return object_;
}
- // Provides access to the borrowed object directly.
- //
- // NOTE: The member of pointer member access operator, operator->(), is
- // recommended over this API as this is prone to leaking references. However,
- // this is sometimes necessary.
- //
- // WARNING: Be careful not to leak references to the borrowed object!
+ /// Const overload
+ const GuardedType* operator->() const {
+ PW_ASSERT(object_ != nullptr); // Ensure this isn't a stale moved instance.
+ return object_;
+ }
+
+ /// Provides access to the borrowed object directly.
+ ///
+ /// @rst
+ /// .. note::
+ /// The member of pointer member access operator, ``operator->()``, is
+ /// recommended over this API as this is prone to leaking references.
+ /// However, this is sometimes necessary.
+ ///
+ /// .. warning:
+ /// Be careful not to leak references to the borrowed object!
+ /// @endrst
GuardedType& operator*() {
PW_ASSERT(object_ != nullptr); // Ensure this isn't a stale moved instance.
return *object_;
}
+ /// Const overload
+ const GuardedType& operator*() const {
+ PW_ASSERT(object_ != nullptr); // Ensure this isn't a stale moved instance.
+ return *object_;
+ }
+
private:
- // Allow BorrowedPointer creation inside of Borrowable's acquire methods.
+ /// Allow BorrowedPointer creation inside of Borrowable's acquire methods.
template <typename G, typename L>
friend class Borrowable;
@@ -85,14 +101,14 @@ class BorrowedPointer {
GuardedType* object_;
};
-// The Borrowable is a helper construct that enables callers to borrow an object
-// which is guarded by a lock.
-//
-// Users who need access to the guarded object can ask to acquire a
-// BorrowedPointer which permits access while the lock is held.
-//
-// This class is compatible with locks which comply with BasicLockable,
-// Lockable, and TimedLockable C++ named requirements.
+/// The `Borrowable` is a helper construct that enables callers to borrow an
+/// object which is guarded by a lock.
+///
+/// Users who need access to the guarded object can ask to acquire a
+/// `BorrowedPointer` which permits access while the lock is held.
+///
+/// This class is compatible with locks which comply with `BasicLockable`,
+/// `Lockable`, and `TimedLockable` C++ named requirements.
template <typename GuardedType, typename Lock = pw::sync::VirtualBasicLockable>
class Borrowable {
public:
@@ -104,14 +120,14 @@ class Borrowable {
Borrowable(Borrowable&& other) = default;
Borrowable& operator=(Borrowable&& other) = default;
- // Blocks indefinitely until the object can be borrowed. Failures are fatal.
+ /// Blocks indefinitely until the object can be borrowed. Failures are fatal.
BorrowedPointer<GuardedType, Lock> acquire() PW_NO_LOCK_SAFETY_ANALYSIS {
lock_->lock();
return BorrowedPointer<GuardedType, Lock>(*lock_, *object_);
}
- // Tries to borrow the object in a non-blocking manner. Returns a
- // BorrowedPointer on success, otherwise std::nullopt (nothing).
+ /// Tries to borrow the object in a non-blocking manner. Returns a
+ /// BorrowedPointer on success, otherwise `std::nullopt` (nothing).
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire() {
if (!lock_->try_lock()) {
return std::nullopt;
@@ -119,9 +135,9 @@ class Borrowable {
return BorrowedPointer<GuardedType, Lock>(*lock_, *object_);
}
- // Tries to borrow the object. Blocks until the specified timeout has elapsed
- // or the object has been borrowed, whichever comes first. Returns a
- // BorrowedPointer on success, otherwise std::nullopt (nothing).
+ /// Tries to borrow the object. Blocks until the specified timeout has elapsed
+ /// or the object has been borrowed, whichever comes first. Returns a
+ /// `BorrowedPointer` on success, otherwise `std::nullopt` (nothing).
template <class Rep, class Period>
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_for(
std::chrono::duration<Rep, Period> timeout) {
@@ -131,9 +147,9 @@ class Borrowable {
return BorrowedPointer<GuardedType, Lock>(*lock_, *object_);
}
- // Tries to borrow the object. Blocks until the specified deadline has passed
- // or the object has been borrowed, whichever comes first. Returns a
- // BorrowedPointer on success, otherwise std::nullopt (nothing).
+ /// Tries to borrow the object. Blocks until the specified deadline has passed
+ /// or the object has been borrowed, whichever comes first. Returns a
+ /// `BorrowedPointer` on success, otherwise `std::nullopt` (nothing).
template <class Clock, class Duration>
std::optional<BorrowedPointer<GuardedType, Lock>> try_acquire_until(
std::chrono::time_point<Clock, Duration> deadline) {
diff --git a/pw_sync/public/pw_sync/condition_variable.h b/pw_sync/public/pw_sync/condition_variable.h
new file mode 100644
index 000000000..7aa16ae11
--- /dev/null
+++ b/pw_sync/public/pw_sync/condition_variable.h
@@ -0,0 +1,91 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <mutex>
+
+#include "pw_chrono/system_clock.h"
+#include "pw_sync/mutex.h"
+#include "pw_sync_backend/condition_variable_native.h"
+
+namespace pw::sync {
+
+// ConditionVariable represents a condition variable using an API very similar
+// to std::condition_variable. Implementations of this class should share the
+// same semantics as std::condition_variable.
+class ConditionVariable {
+ public:
+ using native_handle_type = backend::NativeConditionVariableHandle;
+
+ ConditionVariable() = default;
+
+ ConditionVariable(const ConditionVariable&) = delete;
+
+ ~ConditionVariable() = default;
+
+ ConditionVariable& operator=(const ConditionVariable&) = delete;
+
+ // Wake up one thread waiting on a condition.
+ //
+ // The thread will re-evaluate the condition via its predicate. Threads where
+ // the predicate evaluates false will go back to waiting. The new order of
+ // waiting threads is undefined.
+ void notify_one();
+
+ // Wake up all threads waiting on the condition variable.
+ //
+ // Woken threads will re-evaluate the condition via their predicate. Threads
+ // where the predicate evaluates false will go back to waiting. The new order
+ // of waiting threads is undefined.
+ void notify_all();
+
+ // Block the current thread until predicate() == true.
+ //
+ // Precondition: the provided lock must be locked.
+ template <typename Predicate>
+ void wait(std::unique_lock<Mutex>& lock, Predicate predicate);
+
+ // Block the current thread for a duration up to the given timeout or
+ // until predicate() == true whichever comes first.
+ //
+ // Returns: true if predicate() == true.
+ // false if timeout expired.
+ //
+ // Precondition: the provided lock must be locked.
+ template <typename Predicate>
+ bool wait_for(std::unique_lock<Mutex>& lock,
+ pw::chrono::SystemClock::duration timeout,
+ Predicate predicate);
+
+ // Block the current thread until given point in time or until predicate() ==
+ // true whichever comes first.
+ //
+ // Returns: true if predicate() == true.
+ // false if the deadline was reached.
+ //
+ // Precondition: the provided lock must be locked.
+ template <typename Predicate>
+ bool wait_until(std::unique_lock<Mutex>& lock,
+ pw::chrono::SystemClock::time_point deadline,
+ Predicate predicate);
+
+ native_handle_type native_handle();
+
+ private:
+ backend::NativeConditionVariable native_type_;
+};
+
+} // namespace pw::sync
+
+#include "pw_sync_backend/condition_variable_inline.h"
diff --git a/pw_sync/public/pw_sync/counting_semaphore.h b/pw_sync/public/pw_sync/counting_semaphore.h
index 875ef975f..b0ab87671 100644
--- a/pw_sync/public/pw_sync/counting_semaphore.h
+++ b/pw_sync/public/pw_sync/counting_semaphore.h
@@ -25,18 +25,23 @@
namespace pw::sync {
-// The CountingSemaphore is a synchronization primitive that can be used for
-// counting events and/or resource management where receiver(s) can block on
-// acquire until notifier(s) signal by invoking release.
-// Note that unlike Mutexes, priority inheritance is not used by semaphores
-// meaning semaphores are subject to unbounded priority inversions.
-// Pigweed does not recommend semaphores for mutual exclusion. The entire API is
-// thread safe but only a subset is IRQ safe.
-//
-// WARNING: In order to support global statically constructed CountingSemaphores
-// the user and/or backend MUST ensure that any initialization required in your
-// environment is done prior to the creation and/or initialization of the native
-// synchronization primitives (e.g. kernel initialization).
+/// The `CountingSemaphore` is a synchronization primitive that can be used for
+/// counting events and/or resource management where receiver(s) can block on
+/// acquire until notifier(s) signal by invoking release.
+/// Note that unlike Mutexes, priority inheritance is not used by semaphores
+/// meaning semaphores are subject to unbounded priority inversions.
+/// Pigweed does not recommend semaphores for mutual exclusion. The entire API
+/// is thread safe but only a subset is IRQ safe.
+///
+/// @rst
+/// .. WARNING::
+/// In order to support global statically constructed ``CountingSemaphores``
+/// the user and/or backend MUST ensure that any initialization required in
+/// your environment is done prior to the creation and/or initialization of
+/// the native synchronization primitives (e.g. kernel initialization).
+/// @endrst
+///
+/// The `CountingSemaphore` is initialized to being empty or having no tokens.
class CountingSemaphore {
public:
using native_handle_type = backend::NativeCountingSemaphoreHandle;
@@ -48,38 +53,45 @@ class CountingSemaphore {
CountingSemaphore& operator=(const CountingSemaphore&) = delete;
CountingSemaphore& operator=(CountingSemaphore&&) = delete;
- // Atomically increments the internal counter by the value of update.
- // Any thread(s) waiting for the counter to be greater than 0, i.e. blocked
- // in acquire, will subsequently be unblocked.
- // This is IRQ safe.
- //
- // Precondition: update >= 0
- // Precondition: update <= max() - counter
+ /// Atomically increments the internal counter by the value of update.
+ /// Any thread(s) waiting for the counter to be greater than 0, i.e. blocked
+ /// in acquire, will subsequently be unblocked.
+ /// This is IRQ safe.
+ ///
+ /// @b Precondition: update >= 0
+ ///
+ /// @b Precondition: update <= max() - counter
void release(ptrdiff_t update = 1);
- // Decrements the internal counter by 1 or blocks indefinitely until it can.
- // This is thread safe, but not IRQ safe.
+ /// Decrements the internal counter by 1 or blocks indefinitely until it can.
+ ///
+ /// This is thread safe, but not IRQ safe.
void acquire();
- // Tries to decrement by the internal counter by 1 without blocking.
- // Returns true if the internal counter was decremented successfully.
- // This is IRQ safe.
+ /// Tries to decrement by the internal counter by 1 without blocking.
+ /// Returns true if the internal counter was decremented successfully.
+ ///
+ /// This is IRQ safe.
bool try_acquire() noexcept;
- // Tries to decrement the internal counter by 1. Blocks until the specified
- // timeout has elapsed or the counter was decremented by 1, whichever comes
- // first.
- // Returns true if the internal counter was decremented successfully.
- // This is thread safe, but not IRQ safe.
+ /// Tries to decrement the internal counter by 1. Blocks until the specified
+ /// timeout has elapsed or the counter was decremented by 1, whichever comes
+ /// first.
+ ///
+ /// Returns true if the internal counter was decremented successfully.
+ /// This is thread safe, but not IRQ safe.
bool try_acquire_for(chrono::SystemClock::duration timeout);
- // Tries to decrement the internal counter by 1. Blocks until the specified
- // deadline has been reached or the counter was decremented by 1, whichever
- // comes first.
- // Returns true if the internal counter was decremented successfully.
- // This is thread safe, but not IRQ safe.
+ /// Tries to decrement the internal counter by 1. Blocks until the specified
+ /// deadline has been reached or the counter was decremented by 1, whichever
+ /// comes first.
+ ///
+ /// Returns true if the internal counter was decremented successfully.
+ ///
+ /// This is thread safe, but not IRQ safe.
bool try_acquire_until(chrono::SystemClock::time_point deadline);
+ /// Returns the internal counter's maximum possible value.
static constexpr ptrdiff_t max() noexcept {
return backend::kCountingSemaphoreMaxValue;
}
@@ -87,7 +99,7 @@ class CountingSemaphore {
native_handle_type native_handle();
private:
- // This may be a wrapper around a native type with additional members.
+ /// This may be a wrapper around a native type with additional members.
backend::NativeCountingSemaphore native_type_;
};
diff --git a/pw_sync/public/pw_sync/inline_borrowable.h b/pw_sync/public/pw_sync/inline_borrowable.h
new file mode 100644
index 000000000..0a423f40a
--- /dev/null
+++ b/pw_sync/public/pw_sync/inline_borrowable.h
@@ -0,0 +1,151 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <tuple>
+#include <utility>
+
+#include "pw_sync/borrow.h"
+#include "pw_sync/internal/borrowable_storage.h"
+#include "pw_sync/mutex.h"
+#include "pw_sync/virtual_basic_lockable.h"
+
+namespace pw::sync {
+
+/// `InlineBorrowable` holds an object of `GuardedType` and a Lock that guards
+/// access to the object. It should be used when an object should be guarded for
+/// its entire lifecycle by a single lock.
+///
+/// This object should be shared with other componetns as a reference of type
+/// `Borrowable<GuardedType, LockInterface>`.
+///
+template <typename GuardedType,
+ typename Lock = pw::sync::VirtualMutex,
+ typename LockInterface = pw::sync::VirtualBasicLockable>
+class InlineBorrowable : private internal::BorrowableStorage<GuardedType, Lock>,
+ public Borrowable<GuardedType, LockInterface> {
+ using Storage = internal::BorrowableStorage<GuardedType, Lock>;
+ using Base = Borrowable<GuardedType, LockInterface>;
+
+ public:
+ /// Construct the guarded object and lock using their default constructors.
+ constexpr InlineBorrowable()
+ : Storage(std::in_place), Base(Storage::object_, Storage::lock_) {}
+
+ /// Construct the guarded object by providing its constructor arguments
+ /// inline. The lock is constructed using its default constructor.
+ ///
+ /// This constructor supports list initialization for arrays, structs, and
+ /// other objects such as `std::array`.
+ ///
+ /// Example:
+ ///
+ /// @code
+ /// InlineBorrowable<Foo> foo(std::in_place, foo_arg1, foo_arg2);
+ ///
+ /// InlineBorrowable<std::array<int, 2>> foo_array(std::in_place, 1, 2);
+ /// @endcode
+ ///
+ template <typename... Args>
+ constexpr explicit InlineBorrowable(std::in_place_t, Args&&... args)
+ : Storage(std::in_place, std::forward<Args>(args)...),
+ Base(Storage::object_, Storage::lock_) {}
+
+ /// Construct the guarded object and lock by providing their construction
+ /// parameters using separate tuples. The 2nd tuple can be ommitted to
+ /// construct the lock using its default constructor.
+ ///
+ /// Example:
+ ///
+ /// @code
+ /// InlineBorrowable<Foo> foo(std::forward_as_tuple(foo_arg1, foo_arg2));
+ ///
+ /// InlineBorrowable<Foo, MyLock> foo_lock(
+ /// std::forward_as_tuple(foo_arg1, foo_arg2),
+ /// std::forward_as_tuple(lock_arg1, lock_arg2));
+ /// @endcode
+ ///
+ /// @note This constructor only supports list initialization with C++20 or
+ /// later, because it requires https://wg21.link/p0960.
+ ///
+ template <typename... ObjectArgs, typename... LockArgs>
+ constexpr explicit InlineBorrowable(
+ std::tuple<ObjectArgs...>&& object_args,
+ std::tuple<LockArgs...>&& lock_args = std::make_tuple())
+ : Storage(std::forward<std::tuple<ObjectArgs...>>(object_args),
+ std::forward<std::tuple<LockArgs...>>(lock_args)),
+ Base(Storage::object_, Storage::lock_) {}
+
+ /// Construct the guarded object and lock by providing factory functions. The
+ /// 2nd callable can be ommitted to construct the lock using its default
+ /// constructor.
+ ///
+ /// Example:
+ ///
+ /// @code
+ /// InlineBorrowable<Foo> foo([&]{ return Foo{foo_arg1, foo_arg2}; });
+ ///
+ /// InlineBorrowable<Foo, MyLock> foo_lock(
+ /// [&]{ return Foo{foo_arg1, foo_arg2}; }
+ /// [&]{ return MyLock{lock_arg1, lock_arg2}; }
+ /// @endcode
+ ///
+ template <typename ObjectConstructor,
+ typename LockConstructor = Lock(),
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<GuardedType&&, ObjectConstructor>>,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<Lock&&, LockConstructor>>>
+ constexpr explicit InlineBorrowable(
+ const ObjectConstructor& object_ctor,
+ const LockConstructor& lock_ctor = internal::DefaultConstruct<Lock>)
+ : Storage(object_ctor, lock_ctor),
+ Base(Storage::object_, Storage::lock_) {}
+
+ template <typename ObjectConstructor,
+ typename LockConstructor = Lock(),
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<GuardedType&&, ObjectConstructor>>,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<Lock&&, LockConstructor>>>
+ constexpr explicit InlineBorrowable(
+ ObjectConstructor& object_ctor,
+ const LockConstructor& lock_ctor = internal::DefaultConstruct<Lock>)
+ : Storage(object_ctor, lock_ctor),
+ Base(Storage::object_, Storage::lock_) {}
+
+ template <typename ObjectConstructor,
+ typename LockConstructor = Lock(),
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<GuardedType&&, ObjectConstructor>>,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<Lock&&, LockConstructor>>>
+ constexpr explicit InlineBorrowable(const ObjectConstructor& object_ctor,
+ LockConstructor& lock_ctor)
+ : Storage(object_ctor, lock_ctor),
+ Base(Storage::object_, Storage::lock_) {}
+
+ template <typename ObjectConstructor,
+ typename LockConstructor = Lock(),
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<GuardedType&&, ObjectConstructor>>,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<Lock&&, LockConstructor>>>
+ constexpr explicit InlineBorrowable(ObjectConstructor& object_ctor,
+ LockConstructor& lock_ctor)
+ : Storage(object_ctor, lock_ctor),
+ Base(Storage::object_, Storage::lock_) {}
+};
+
+} // namespace pw::sync
diff --git a/pw_sync/public/pw_sync/internal/borrowable_storage.h b/pw_sync/public/pw_sync/internal/borrowable_storage.h
new file mode 100644
index 000000000..0705ed560
--- /dev/null
+++ b/pw_sync/public/pw_sync/internal/borrowable_storage.h
@@ -0,0 +1,89 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <tuple>
+
+namespace pw::sync::internal {
+
+// BorrowableStorage stores an object and associated lock. Both objects are
+// constructed in-place.
+template <typename ObjectType, typename Lock>
+class BorrowableStorage {
+ protected:
+ // Construct the object in-place using a list of arguments.
+ template <typename... Args>
+ constexpr explicit BorrowableStorage(std::in_place_t, Args&&... args)
+ : object_{std::forward<Args>(args)...}, lock_{} {}
+
+ // Construct the object and lock in-place using the provided parameters.
+ template <typename... ObjectArgs, typename... LockArgs>
+ constexpr BorrowableStorage(std::tuple<ObjectArgs...>&& object_args,
+ std::tuple<LockArgs...>&& lock_args)
+ : object_{std::make_from_tuple<ObjectType>(
+ std::forward<std::tuple<ObjectArgs...>>(object_args))},
+ lock_{std::make_from_tuple<Lock>(
+ std::forward<std::tuple<LockArgs...>>(lock_args))} {}
+
+ // Construct the object and lock in-place using the provided factories.
+ template <typename ObjectConstructor,
+ typename LockConstructor,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<ObjectType&&, ObjectConstructor>>,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<Lock&&, LockConstructor>>>
+ constexpr BorrowableStorage(const ObjectConstructor& object_ctor,
+ const LockConstructor& lock_ctor)
+ : object_{object_ctor()}, lock_{lock_ctor()} {}
+
+ template <typename ObjectConstructor,
+ typename LockConstructor,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<ObjectType&&, ObjectConstructor>>,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<Lock&&, LockConstructor>>>
+ constexpr BorrowableStorage(ObjectConstructor& object_ctor,
+ const LockConstructor& lock_ctor)
+ : object_{object_ctor()}, lock_{lock_ctor()} {}
+
+ template <typename ObjectConstructor,
+ typename LockConstructor,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<ObjectType&&, ObjectConstructor>>,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<Lock&&, LockConstructor>>>
+ constexpr BorrowableStorage(const ObjectConstructor& object_ctor,
+ LockConstructor& lock_ctor)
+ : object_{object_ctor()}, lock_{lock_ctor()} {}
+
+ template <typename ObjectConstructor,
+ typename LockConstructor,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<ObjectType&&, ObjectConstructor>>,
+ typename = std::enable_if_t<
+ std::is_invocable_r_v<Lock&&, LockConstructor>>>
+ constexpr BorrowableStorage(ObjectConstructor& object_ctor,
+ LockConstructor& lock_ctor)
+ : object_{object_ctor()}, lock_{lock_ctor()} {}
+
+ ObjectType object_;
+ Lock lock_;
+};
+
+template <typename T>
+T DefaultConstruct() {
+ return T();
+}
+
+} // namespace pw::sync::internal
diff --git a/pw_sync/public/pw_sync/interrupt_spin_lock.h b/pw_sync/public/pw_sync/interrupt_spin_lock.h
index d9b75f7c9..7726c92a8 100644
--- a/pw_sync/public/pw_sync/interrupt_spin_lock.h
+++ b/pw_sync/public/pw_sync/interrupt_spin_lock.h
@@ -25,26 +25,26 @@
namespace pw::sync {
-// The InterruptSpinLock is a synchronization primitive that can be used to
-// protect shared data from being simultaneously accessed by multiple threads
-// and/or interrupts as a targeted global lock, with the exception of
-// Non-Maskable Interrupts (NMIs).
-// It offers exclusive, non-recursive ownership semantics where IRQs up to a
-// backend defined level of "NMIs" will be masked to solve priority-inversion.
-//
-// NOTE: This InterruptSpinLock relies on built-in local interrupt masking to
-// make it interrupt safe without requiring the caller to separately mask and
-// unmask interrupts when using this primitive.
-//
-// Unlike global interrupt locks, this also works safely and efficiently on SMP
-// systems. On systems which are not SMP, spinning is not required and it's
-// possible that only interrupt masking occurs but some state may still be used
-// to detect recursion.
-//
-// This entire API is IRQ safe, but NOT NMI safe.
-//
-// Precondition: Code that holds a specific InterruptSpinLock must not try to
-// re-acquire it. However, it is okay to nest distinct spinlocks.
+/// The `InterruptSpinLock` is a synchronization primitive that can be used to
+/// protect shared data from being simultaneously accessed by multiple threads
+/// and/or interrupts as a targeted global lock, with the exception of
+/// Non-Maskable Interrupts (NMIs).
+/// It offers exclusive, non-recursive ownership semantics where IRQs up to a
+/// backend defined level of "NMIs" will be masked to solve priority-inversion.
+///
+/// @note This `InterruptSpinLock` relies on built-in local interrupt masking to
+/// make it interrupt safe without requiring the caller to separately mask
+/// and unmask interrupts when using this primitive.
+///
+/// Unlike global interrupt locks, this also works safely and efficiently on SMP
+/// systems. On systems which are not SMP, spinning is not required and it's
+/// possible that only interrupt masking occurs but some state may still be used
+/// to detect recursion.
+///
+/// This entire API is IRQ safe, but NOT NMI safe.
+///
+/// @b Precondition: Code that holds a specific `InterruptSpinLock` must not try
+/// to re-acquire it. However, it is okay to nest distinct spinlocks.
class PW_LOCKABLE("pw::sync::InterruptSpinLock") InterruptSpinLock {
public:
using native_handle_type = backend::NativeInterruptSpinLockHandle;
@@ -56,27 +56,27 @@ class PW_LOCKABLE("pw::sync::InterruptSpinLock") InterruptSpinLock {
InterruptSpinLock& operator=(const InterruptSpinLock&) = delete;
InterruptSpinLock& operator=(InterruptSpinLock&&) = delete;
- // Locks the spinlock, blocking indefinitely. Failures are fatal.
- //
- // Precondition: Recursive locking is undefined behavior.
+ /// Locks the spinlock, blocking indefinitely. Failures are fatal.
+ ///
+ /// @b Precondition: Recursive locking is undefined behavior.
void lock() PW_EXCLUSIVE_LOCK_FUNCTION();
- // Tries to lock the spinlock in a non-blocking manner.
- // Returns true if the spinlock was successfully acquired.
- //
- // Precondition: Recursive locking is undefined behavior.
+ /// Tries to lock the spinlock in a non-blocking manner.
+ /// Returns true if the spinlock was successfully acquired.
+ ///
+ /// @b Precondition: Recursive locking is undefined behavior.
bool try_lock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);
- // Unlocks the spinlock. Failures are fatal.
- //
- // PRECONDITION:
- // The spinlock is held by the caller.
+ /// Unlocks the spinlock. Failures are fatal.
+ ///
+ /// @b Precondition:
+ /// The spinlock is held by the caller.
void unlock() PW_UNLOCK_FUNCTION();
native_handle_type native_handle();
private:
- // This may be a wrapper around a native type with additional members.
+ /// This may be a wrapper around a native type with additional members.
backend::NativeInterruptSpinLock native_type_;
};
@@ -122,10 +122,18 @@ typedef struct pw_sync_InterruptSpinLock pw_sync_InterruptSpinLock;
PW_EXTERN_C_START
+/// Invokes the `InterruptSpinLock::lock` member function on the given
+/// `interrupt_spin_lock`.
void pw_sync_InterruptSpinLock_Lock(pw_sync_InterruptSpinLock* spin_lock)
PW_NO_LOCK_SAFETY_ANALYSIS;
+
+/// Invokes the `InterruptSpinLock::try_lock` member function on the given
+/// `interrupt_spin_lock`.
bool pw_sync_InterruptSpinLock_TryLock(pw_sync_InterruptSpinLock* spin_lock)
PW_NO_LOCK_SAFETY_ANALYSIS;
+
+/// Invokes the `InterruptSpinLock::unlock` member function on the given
+/// `interrupt_spin_lock`.
void pw_sync_InterruptSpinLock_Unlock(pw_sync_InterruptSpinLock* spin_lock)
PW_NO_LOCK_SAFETY_ANALYSIS;
diff --git a/pw_sync/public/pw_sync/lock_annotations.h b/pw_sync/public/pw_sync/lock_annotations.h
index 6d60b5502..756a61c0f 100644
--- a/pw_sync/public/pw_sync/lock_annotations.h
+++ b/pw_sync/public/pw_sync/lock_annotations.h
@@ -34,70 +34,81 @@
#include "pw_preprocessor/compiler.h"
-// PW_GUARDED_BY()
-//
-// Documents if a shared field or global variable needs to be protected by a
-// lock. PW_GUARDED_BY() allows the user to specify a particular lock that
-// should be held when accessing the annotated variable.
-//
-// Although this annotation (and PW_PT_GUARDED_BY, below) cannot be applied to
-// local variables, a local variable and its associated lock can often be
-// combined into a small class or struct, thereby allowing the annotation.
-//
-// Example:
-//
-// class Foo {
-// Mutex mu_;
-// int p1_ PW_GUARDED_BY(mu_);
-// ...
-// };
+/// @def PW_GUARDED_BY
+///
+/// Documents if a shared field or global variable needs to be protected by a
+/// lock. `PW_GUARDED_BY()` allows the user to specify a particular lock that
+/// should be held when accessing the annotated variable.
+///
+/// Although this annotation (and `PW_PT_GUARDED_BY()`, below) cannot be applied
+/// to local variables, a local variable and its associated lock can often be
+/// combined into a small class or struct, thereby allowing the annotation.
+///
+/// Example:
+///
+/// @code
+/// class Foo {
+/// Mutex mu_;
+/// int p1_ PW_GUARDED_BY(mu_);
+/// ...
+/// };
+/// @endcode
+///
#if PW_HAVE_ATTRIBUTE(guarded_by)
#define PW_GUARDED_BY(x) __attribute__((guarded_by(x)))
#else
#define PW_GUARDED_BY(x)
#endif
-// PW_PT_GUARDED_BY()
-//
-// Documents if the memory location pointed to by a pointer should be guarded
-// by a lock when dereferencing the pointer.
-//
-// Example:
-// class Foo {
-// Mutex mu_;
-// int *p1_ PW_PT_GUARDED_BY(mu_);
-// ...
-// };
-//
-// Note that a pointer variable to a shared memory location could itself be a
-// shared variable.
-//
-// Example:
-//
-// // `q_`, guarded by `mu1_`, points to a shared memory location that is
-// // guarded by `mu2_`:
-// int *q_ PW_GUARDED_BY(mu1_) PW_PT_GUARDED_BY(mu2_);
+/// @def PW_PT_GUARDED_BY
+///
+/// Documents if the memory location pointed to by a pointer should be guarded
+/// by a lock when dereferencing the pointer.
+///
+/// Example:
+///
+/// @code
+/// class Foo {
+/// Mutex mu_;
+/// int *p1_ PW_PT_GUARDED_BY(mu_);
+/// ...
+/// };
+/// @endcode
+///
+/// @note A pointer variable to a shared memory location could itself be a
+/// shared variable.
+///
+/// Example:
+///
+/// @code
+/// // `q_`, guarded by `mu1_`, points to a shared memory location that is
+/// // guarded by `mu2_`:
+/// int *q_ PW_GUARDED_BY(mu1_) PW_PT_GUARDED_BY(mu2_);
+/// @endcode
#if PW_HAVE_ATTRIBUTE(pt_guarded_by)
#define PW_PT_GUARDED_BY(x) __attribute__((pt_guarded_by(x)))
#else
#define PW_PT_GUARDED_BY(x)
#endif
-// PW_ACQUIRED_AFTER() / PW_ACQUIRED_BEFORE()
-//
-// Documents the acquisition order between locks that can be held
-// simultaneously by a thread. For any two locks that need to be annotated
-// to establish an acquisition order, only one of them needs the annotation.
-// (i.e. You don't have to annotate both locks with both PW_ACQUIRED_AFTER
-// and PW_ACQUIRED_BEFORE.)
-//
-// As with PW_GUARDED_BY, this is only applicable to locks that are shared
-// fields or global variables.
-//
-// Example:
-//
-// Mutex m1_;
-// Mutex m2_ PW_ACQUIRED_AFTER(m1_);
+/// @def PW_ACQUIRED_AFTER
+/// @def PW_ACQUIRED_BEFORE
+///
+/// Documents the acquisition order between locks that can be held
+/// simultaneously by a thread. For any two locks that need to be annotated
+/// to establish an acquisition order, only one of them needs the annotation.
+/// (i.e. You don't have to annotate both locks with both `PW_ACQUIRED_AFTER()`
+/// and `PW_ACQUIRED_BEFORE()`.)
+///
+/// As with `PW_GUARDED_BY()`, this is only applicable to locks that are shared
+/// fields or global variables.
+///
+/// Example:
+///
+/// @code
+/// Mutex m1_;
+/// Mutex m2_ PW_ACQUIRED_AFTER(m1_);
+/// @endcode
#if PW_HAVE_ATTRIBUTE(acquired_after)
#define PW_ACQUIRED_AFTER(...) __attribute__((acquired_after(__VA_ARGS__)))
#else
@@ -110,29 +121,32 @@
#define PW_ACQUIRED_BEFORE(...)
#endif
-// PW_EXCLUSIVE_LOCKS_REQUIRED() / PW_SHARED_LOCKS_REQUIRED()
-//
-// Documents a function that expects a lock to be held prior to entry.
-// The lock is expected to be held both on entry to, and exit from, the
-// function.
-//
-// An exclusive lock allows read-write access to the guarded data member(s), and
-// only one thread can acquire a lock exclusively at any one time. A shared lock
-// allows read-only access, and any number of threads can acquire a shared lock
-// concurrently.
-//
-// Generally, non-const methods should be annotated with
-// PW_EXCLUSIVE_LOCKS_REQUIRED, while const methods should be annotated with
-// PW_SHARED_LOCKS_REQUIRED.
-//
-// Example:
-//
-// Mutex mu1, mu2;
-// int a PW_GUARDED_BY(mu1);
-// int b PW_GUARDED_BY(mu2);
-//
-// void foo() PW_EXCLUSIVE_LOCKS_REQUIRED(mu1, mu2) { ... }
-// void bar() const PW_SHARED_LOCKS_REQUIRED(mu1, mu2) { ... }
+/// @def PW_EXCLUSIVE_LOCKS_REQUIRED
+/// @def PW_SHARED_LOCKS_REQUIRED
+///
+/// Documents a function that expects a lock to be held prior to entry.
+/// The lock is expected to be held both on entry to, and exit from, the
+/// function.
+///
+/// An exclusive lock allows read-write access to the guarded data member(s),
+/// and only one thread can acquire a lock exclusively at any one time. A shared
+/// lock allows read-only access, and any number of threads can acquire a shared
+/// lock concurrently.
+///
+/// Generally, non-const methods should be annotated with
+/// `PW_EXCLUSIVE_LOCKS_REQUIRED()`, while const methods should be annotated
+/// with `PW_SHARED_LOCKS_REQUIRED()`.
+///
+/// Example:
+///
+/// @code
+/// Mutex mu1, mu2;
+/// int a PW_GUARDED_BY(mu1);
+/// int b PW_GUARDED_BY(mu2);
+///
+/// void foo() PW_EXCLUSIVE_LOCKS_REQUIRED(mu1, mu2) { ... }
+/// void bar() const PW_SHARED_LOCKS_REQUIRED(mu1, mu2) { ... }
+/// @endcode
#if PW_HAVE_ATTRIBUTE(exclusive_locks_required)
#define PW_EXCLUSIVE_LOCKS_REQUIRED(...) \
__attribute__((exclusive_locks_required(__VA_ARGS__)))
@@ -147,32 +161,59 @@
#define PW_SHARED_LOCKS_REQUIRED(...)
#endif
-// PW_LOCKS_EXCLUDED()
-//
-// Documents the locks acquired in the body of the function. These locks
-// cannot be held when calling this function (as Pigweed's default locks are
-// non-reentrant).
+/// @def PW_LOCKS_EXCLUDED
+///
+/// Documents that the caller must not hold the given lock. This annotation is
+/// often used to prevent deadlocks. Pigweed's mutex implementation is not
+/// re-entrant, so a deadlock will occur if the function acquires the mutex a
+/// second time.
+///
+/// Example:
+///
+/// @code
+/// Mutex mu;
+/// int a PW_GUARDED_BY(mu);
+///
+/// void foo() PW_LOCKS_EXCLUDED(mu) {
+/// mu.lock();
+/// ...
+/// mu.unlock();
+/// }
+/// @endcode
#if PW_HAVE_ATTRIBUTE(locks_excluded)
#define PW_LOCKS_EXCLUDED(...) __attribute__((locks_excluded(__VA_ARGS__)))
#else
#define PW_LOCKS_EXCLUDED(...)
#endif
-// PW_LOCK_RETURNED()
-//
-// Documents a function that returns a lock without acquiring it. For example,
-// a public getter method that returns a pointer to a private lock should
-// be annotated with PW_LOCK_RETURNED.
+/// @def PW_LOCK_RETURNED
+///
+/// Documents a function that returns a lock without acquiring it. For example,
+/// a public getter method that returns a pointer to a private lock should
+/// be annotated with `PW_LOCK_RETURNED()`.
+///
+/// Example:
+///
+/// @code
+/// class Foo {
+/// public:
+/// Mutex* mu() PW_LOCK_RETURNED(mu) { return &mu; }
+///
+/// private:
+/// Mutex mu;
+/// };
+/// @endcode
#if PW_HAVE_ATTRIBUTE(lock_returned)
#define PW_LOCK_RETURNED(x) __attribute__((lock_returned(x)))
#else
#define PW_LOCK_RETURNED(x)
#endif
-// PW_LOCKABLE(name)
-//
-// Documents if a class/type is a lockable type (such as the `pw::sync::Mutex`
-// class). The name is used in the warning messages.
+/// @def PW_LOCKABLE
+///
+/// Documents if a class/type is a lockable type (such as the `pw::sync::Mutex`
+/// class). The name is used in the warning messages. This can also be useful on
+/// classes which have locking like semantics but aren't actually locks.
#if PW_HAVE_ATTRIBUTE(capability)
#define PW_LOCKABLE(name) __attribute__((capability(name)))
#elif PW_HAVE_ATTRIBUTE(lockable)
@@ -181,25 +222,25 @@
#define PW_LOCKABLE(name)
#endif
-// PW_SCOPED_LOCKABLE
-//
-// Documents if a class does RAII locking. The name is used in the warning
-// messages.
-//
-// The constructor should use `LOCK_FUNCTION()` to specify the lock that is
-// acquired, and the destructor should use `UNLOCK_FUNCTION()` with no
-// arguments; the analysis will assume that the destructor unlocks whatever the
-// constructor locked.
+/// @def PW_SCOPED_LOCKABLE
+///
+/// Documents if a class does RAII locking. The name is used in the warning
+/// messages.
+///
+/// The constructor should use `LOCK_FUNCTION()` to specify the lock that is
+/// acquired, and the destructor should use `UNLOCK_FUNCTION()` with no
+/// arguments; the analysis will assume that the destructor unlocks whatever the
+/// constructor locked.
#if PW_HAVE_ATTRIBUTE(scoped_lockable)
#define PW_SCOPED_LOCKABLE __attribute__((scoped_lockable))
#else
#define PW_SCOPED_LOCKABLE
#endif
-// PW_EXCLUSIVE_LOCK_FUNCTION()
-//
-// Documents functions that acquire a lock in the body of a function, and do
-// not release it.
+/// @def PW_EXCLUSIVE_LOCK_FUNCTION
+///
+/// Documents functions that acquire a lock in the body of a function, and do
+/// not release it.
#if PW_HAVE_ATTRIBUTE(exclusive_lock_function)
#define PW_EXCLUSIVE_LOCK_FUNCTION(...) \
__attribute__((exclusive_lock_function(__VA_ARGS__)))
@@ -207,10 +248,10 @@
#define PW_EXCLUSIVE_LOCK_FUNCTION(...)
#endif
-// PW_SHARED_LOCK_FUNCTION()
-//
-// Documents functions that acquire a shared (reader) lock in the body of a
-// function, and do not release it.
+/// @def PW_SHARED_LOCK_FUNCTION
+///
+/// Documents functions that acquire a shared (reader) lock in the body of a
+/// function, and do not release it.
#if PW_HAVE_ATTRIBUTE(shared_lock_function)
#define PW_SHARED_LOCK_FUNCTION(...) \
__attribute__((shared_lock_function(__VA_ARGS__)))
@@ -218,24 +259,25 @@
#define PW_SHARED_LOCK_FUNCTION(...)
#endif
-// PW_UNLOCK_FUNCTION()
-//
-// Documents functions that expect a lock to be held on entry to the function,
-// and release it in the body of the function.
+/// @def PW_UNLOCK_FUNCTION
+///
+/// Documents functions that expect a lock to be held on entry to the function,
+/// and release it in the body of the function.
#if PW_HAVE_ATTRIBUTE(unlock_function)
#define PW_UNLOCK_FUNCTION(...) __attribute__((unlock_function(__VA_ARGS__)))
#else
#define PW_UNLOCK_FUNCTION(...)
#endif
-// PW_EXCLUSIVE_TRYLOCK_FUNCTION() / PW_SHARED_TRYLOCK_FUNCTION()
-//
-// Documents functions that try to acquire a lock, and return success or failure
-// (or a non-boolean value that can be interpreted as a boolean).
-// The first argument should be `true` for functions that return `true` on
-// success, or `false` for functions that return `false` on success. The second
-// argument specifies the lock that is locked on success. If unspecified, this
-// lock is assumed to be `this`.
+/// @def PW_EXCLUSIVE_TRYLOCK_FUNCTION
+/// @def PW_SHARED_TRYLOCK_FUNCTION
+///
+/// Documents functions that try to acquire a lock, and return success or
+/// failure (or a non-boolean value that can be interpreted as a boolean). The
+/// first argument should be `true` for functions that return `true` on success,
+/// or `false` for functions that return `false` on success. The second argument
+/// specifies the lock that is locked on success. If unspecified, this lock is
+/// assumed to be `this`.
#if PW_HAVE_ATTRIBUTE(exclusive_trylock_function)
#define PW_EXCLUSIVE_TRYLOCK_FUNCTION(...) \
__attribute__((exclusive_trylock_function(__VA_ARGS__)))
@@ -250,10 +292,11 @@
#define PW_SHARED_TRYLOCK_FUNCTION(...)
#endif
-// PW_ASSERT_EXCLUSIVE_LOCK() / PW_ASSERT_SHARED_LOCK()
-//
-// Documents functions that dynamically check to see if a lock is held, and fail
-// if it is not held.
+/// @def PW_ASSERT_EXCLUSIVE_LOCK
+/// @def PW_ASSERT_SHARED_LOCK
+///
+/// Documents functions that dynamically check to see if a lock is held, and
+/// fail if it is not held.
#if PW_HAVE_ATTRIBUTE(assert_exclusive_lock)
#define PW_ASSERT_EXCLUSIVE_LOCK(...) \
__attribute__((assert_exclusive_lock(__VA_ARGS__)))
@@ -268,11 +311,11 @@
#define PW_ASSERT_SHARED_LOCK(...)
#endif
-// PW_NO_LOCK_SAFETY_ANALYSIS
-//
-// Turns off thread safety checking within the body of a particular function.
-// This annotation is used to mark functions that are known to be correct, but
-// the locking behavior is more complicated than the analyzer can handle.
+/// @def PW_NO_LOCK_SAFETY_ANALYSIS
+///
+/// Turns off thread safety checking within the body of a particular function.
+/// This annotation is used to mark functions that are known to be correct, but
+/// the locking behavior is more complicated than the analyzer can handle.
#if PW_HAVE_ATTRIBUTE(no_thread_safety_analysis)
#define PW_NO_LOCK_SAFETY_ANALYSIS __attribute__((no_thread_safety_analysis))
#else
diff --git a/pw_sync/public/pw_sync/mutex.h b/pw_sync/public/pw_sync/mutex.h
index eefbed699..5b4246812 100644
--- a/pw_sync/public/pw_sync/mutex.h
+++ b/pw_sync/public/pw_sync/mutex.h
@@ -25,16 +25,20 @@
namespace pw::sync {
-// The Mutex is a synchronization primitive that can be used to protect
-// shared data from being simultaneously accessed by multiple threads.
-// It offers exclusive, non-recursive ownership semantics where priority
-// inheritance is used to solve the classic priority-inversion problem.
-// This is thread safe, but NOT IRQ safe.
-//
-// WARNING: In order to support global statically constructed Mutexes, the user
-// and/or backend MUST ensure that any initialization required in your
-// environment is done prior to the creation and/or initialization of the native
-// synchronization primitives (e.g. kernel initialization).
+/// The `Mutex` is a synchronization primitive that can be used to protect
+/// shared data from being simultaneously accessed by multiple threads. It
+/// offers exclusive, non-recursive ownership semantics where priority
+/// inheritance is used to solve the classic priority-inversion problem. This
+/// is thread safe, but NOT IRQ safe.
+///
+/// @rst
+/// .. warning::
+///
+/// In order to support global statically constructed Mutexes, the user
+/// and/or backend MUST ensure that any initialization required in your
+/// environment is done prior to the creation and/or initialization of the
+/// native synchronization primitives (e.g. kernel initialization).
+/// @endrst
class PW_LOCKABLE("pw::sync::Mutex") Mutex {
public:
using native_handle_type = backend::NativeMutexHandle;
@@ -46,38 +50,38 @@ class PW_LOCKABLE("pw::sync::Mutex") Mutex {
Mutex& operator=(const Mutex&) = delete;
Mutex& operator=(Mutex&&) = delete;
- // Locks the mutex, blocking indefinitely. Failures are fatal.
- //
- // PRECONDITION:
- // The lock isn't already held by this thread. Recursive locking is
- // undefined behavior.
+ /// Locks the mutex, blocking indefinitely. Failures are fatal.
+ ///
+ /// @b PRECONDITION:
+ /// The lock isn't already held by this thread. Recursive locking is
+ /// undefined behavior.
void lock() PW_EXCLUSIVE_LOCK_FUNCTION();
- // Attempts to lock the mutex in a non-blocking manner.
- // Returns true if the mutex was successfully acquired.
- //
- // PRECONDITION:
- // The lock isn't already held by this thread. Recursive locking is
- // undefined behavior.
+ /// Attempts to lock the mutex in a non-blocking manner.
+ /// Returns true if the mutex was successfully acquired.
+ ///
+ /// @b PRECONDITION:
+ /// The lock isn't already held by this thread. Recursive locking is
+ /// undefined behavior.
bool try_lock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);
- // Unlocks the mutex. Failures are fatal.
- //
- // PRECONDITION:
- // The mutex is held by this thread.
+ /// Unlocks the mutex. Failures are fatal.
+ ///
+ /// @b PRECONDITION:
+ /// The mutex is held by this thread.
void unlock() PW_UNLOCK_FUNCTION();
native_handle_type native_handle();
protected:
- // Expose the NativeMutex directly to derived classes (TimedMutex) in
- // case implementations use different types for backend::NativeMutex and
- // native_handle().
+ /// Expose the NativeMutex directly to derived classes (TimedMutex) in case
+ /// implementations use different types for backend::NativeMutex and
+ /// native_handle().
backend::NativeMutex& native_type() { return native_type_; }
const backend::NativeMutex& native_type() const { return native_type_; }
private:
- // This may be a wrapper around a native type with additional members.
+ /// This may be a wrapper around a native type with additional members.
backend::NativeMutex native_type_;
};
@@ -123,8 +127,13 @@ typedef struct pw_sync_Mutex pw_sync_Mutex;
PW_EXTERN_C_START
+/// Invokes the `Mutex::lock` member function on the given `mutex`.
void pw_sync_Mutex_Lock(pw_sync_Mutex* mutex) PW_NO_LOCK_SAFETY_ANALYSIS;
+
+/// Invokes the `Mutex::try_lock` member function on the given `mutex`.
bool pw_sync_Mutex_TryLock(pw_sync_Mutex* mutex) PW_NO_LOCK_SAFETY_ANALYSIS;
+
+/// Invokes the `Mutex::unlock` member function on the given `mutex`.
void pw_sync_Mutex_Unlock(pw_sync_Mutex* mutex) PW_NO_LOCK_SAFETY_ANALYSIS;
PW_EXTERN_C_END
diff --git a/pw_sync/public/pw_sync/recursive_mutex.h b/pw_sync/public/pw_sync/recursive_mutex.h
new file mode 100644
index 000000000..1c09a1929
--- /dev/null
+++ b/pw_sync/public/pw_sync/recursive_mutex.h
@@ -0,0 +1,95 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <stdbool.h>
+
+#include "pw_preprocessor/util.h"
+#include "pw_sync/lock_annotations.h"
+
+#ifdef __cplusplus
+
+#include "pw_sync_backend/recursive_mutex_native.h"
+
+namespace pw::sync {
+
+// WARNING: Recursive mutexes are problematic and their use is discouraged. DO
+// NOT use this class without consulting with the Pigweed team first.
+//
+// RecursiveMutex is the same as Mutex, except that the thread holding the mutex
+// may safely attempt to lock the mutex again.
+// This is thread safe, but NOT IRQ safe.
+//
+// Note that RecursiveMutex's lock safety annotations the same as Mutex's, so
+// lock safety analysis will not allow for recursive locking. Many cases where
+// recursive locking is needed cannot be checked with lock safety analysis
+// anyway (e.g. callbacks). For cases where lock safety analysis complains
+// about safe usage, disable it locally.
+//
+// WARNING: In order to support global statically constructed RecusiveMutexes,
+// the user and/or backend MUST ensure that any initialization required in your
+// environment is done prior to the creation and/or initialization of the native
+// synchronization primitives (e.g. kernel initialization).
+class PW_LOCKABLE("pw::sync::RecursiveMutex") RecursiveMutex {
+ public:
+ using native_handle_type = backend::NativeRecursiveMutexHandle;
+
+ RecursiveMutex();
+ ~RecursiveMutex();
+
+ RecursiveMutex(const RecursiveMutex&) = delete;
+ RecursiveMutex(RecursiveMutex&&) = delete;
+
+ RecursiveMutex& operator=(const RecursiveMutex&) = delete;
+ RecursiveMutex& operator=(RecursiveMutex&&) = delete;
+
+ // Locks the mutex, blocking indefinitely. Failures are fatal.
+ void lock() PW_EXCLUSIVE_LOCK_FUNCTION();
+
+ // Attempts to lock the mutex in a non-blocking manner.
+ // Returns true if the mutex was successfully acquired.
+ bool try_lock() PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);
+
+ // Unlocks the mutex. Failures are fatal.
+ void unlock() PW_UNLOCK_FUNCTION();
+
+ native_handle_type native_handle();
+
+ private:
+ // This may be a wrapper around a native type with additional members.
+ backend::NativeRecursiveMutex native_type_;
+};
+
+} // namespace pw::sync
+
+#include "pw_sync_backend/recursive_mutex_inline.h"
+
+using pw_sync_RecursiveMutex = pw::sync::RecursiveMutex;
+
+#else // !defined(__cplusplus)
+
+typedef struct pw_sync_RecursiveMutex pw_sync_RecursiveMutex;
+
+#endif // __cplusplus
+
+PW_EXTERN_C_START
+
+void pw_sync_RecursiveMutex_Lock(pw_sync_RecursiveMutex* mutex)
+ PW_NO_LOCK_SAFETY_ANALYSIS;
+bool pw_sync_RecursiveMutex_TryLock(pw_sync_RecursiveMutex* mutex)
+ PW_NO_LOCK_SAFETY_ANALYSIS;
+void pw_sync_RecursiveMutex_Unlock(pw_sync_RecursiveMutex* mutex)
+ PW_NO_LOCK_SAFETY_ANALYSIS;
+
+PW_EXTERN_C_END
diff --git a/pw_sync/public/pw_sync/thread_notification.h b/pw_sync/public/pw_sync/thread_notification.h
index ba8cf58b9..85e6e1b6b 100644
--- a/pw_sync/public/pw_sync/thread_notification.h
+++ b/pw_sync/public/pw_sync/thread_notification.h
@@ -17,20 +17,22 @@
namespace pw::sync {
-// The ThreadNotification is a synchronization primitive that can be used to
-// permit a SINGLE thread to block and consume a latching, saturating
-// notification from multiple notifiers.
-//
-// IMPORTANT: This is a single consumer/waiter, multiple producer/notifier API!
-// The acquire APIs must only be invoked by a single consuming thread. As a
-// result, having multiple threads receiving notifications via the acquire API
-// is unsupported.
-//
-// This is effectively a subset of a binary semaphore API, except that only a
-// single thread can be notified and block at a time.
-//
-// The single consumer aspect of the API permits the use of a smaller and/or
-// faster native APIs such as direct thread signaling.
+/// The `ThreadNotification` is a synchronization primitive that can be used to
+/// permit a SINGLE thread to block and consume a latching, saturating
+/// notification from multiple notifiers.
+///
+/// IMPORTANT: This is a single consumer/waiter, multiple producer/notifier API!
+/// The acquire APIs must only be invoked by a single consuming thread. As a
+/// result, having multiple threads receiving notifications via the acquire API
+/// is unsupported.
+///
+/// This is effectively a subset of a binary semaphore API, except that only a
+/// single thread can be notified and block at a time.
+///
+/// The single consumer aspect of the API permits the use of a smaller and/or
+/// faster native APIs such as direct thread signaling.
+///
+/// The `ThreadNotification` is initialized to being empty (latch is not set).
class ThreadNotification {
public:
using native_handle_type = backend::NativeThreadNotificationHandle;
@@ -42,39 +44,40 @@ class ThreadNotification {
ThreadNotification& operator=(const ThreadNotification&) = delete;
ThreadNotification& operator=(ThreadNotification&&) = delete;
- // Blocks indefinitely until the thread is notified, i.e. until the
- // notification latch can be cleared because it was set.
- //
- // Clears the notification latch.
- //
- // IMPORTANT: This should only be used by a single consumer thread.
+ /// Blocks indefinitely until the thread is notified, i.e. until the
+ /// notification latch can be cleared because it was set.
+ ///
+ /// Clears the notification latch.
+ ///
+ /// @b IMPORTANT: This should only be used by a single consumer thread.
void acquire();
- // Returns whether the thread has been notified, i.e. whether the notificion
- // latch was set and resets the latch regardless.
- //
- // Clears the notification latch.
- //
- // Returns true if the thread was notified, meaning the the internal latch was
- // reset successfully.
- //
- // IMPORTANT: This should only be used by a single consumer thread.
+ /// Returns whether the thread has been notified, i.e. whether the notificion
+ /// latch was set and resets the latch regardless.
+ ///
+ /// Clears the notification latch.
+ ///
+ /// Returns true if the thread was notified, meaning the the internal latch
+ /// was reset successfully.
+ ///
+ /// @b IMPORTANT: This should only be used by a single consumer thread.
bool try_acquire();
- // Notifies the thread in a saturating manner, setting the notification latch.
- //
- // Raising the notification multiple time without it being acquired by the
- // consuming thread is equivalent to raising the notification once to the
- // thread. The notification is latched in case the thread was not waiting at
- // the time.
- //
- // This is IRQ and thread safe.
+ /// Notifies the thread in a saturating manner, setting the notification
+ /// latch.
+ ///
+ /// Raising the notification multiple time without it being acquired by the
+ /// consuming thread is equivalent to raising the notification once to the
+ /// thread. The notification is latched in case the thread was not waiting at
+ /// the time.
+ ///
+ /// This is IRQ and thread safe.
void release();
native_handle_type native_handle();
private:
- // This may be a wrapper around a native type with additional members.
+ /// This may be a wrapper around a native type with additional members.
backend::NativeThreadNotification native_type_;
};
diff --git a/pw_sync/public/pw_sync/timed_mutex.h b/pw_sync/public/pw_sync/timed_mutex.h
index eb0328479..abb48c1ee 100644
--- a/pw_sync/public/pw_sync/timed_mutex.h
+++ b/pw_sync/public/pw_sync/timed_mutex.h
@@ -26,17 +26,20 @@
namespace pw::sync {
-// The TimedMutex is a synchronization primitive that can be used to protect
-// shared data from being simultaneously accessed by multiple threads with
-// timeouts and deadlines, extending the Mutex.
-// It offers exclusive, non-recursive ownership semantics where priority
-// inheritance is used to solve the classic priority-inversion problem.
-// This is thread safe, but NOT IRQ safe.
-//
-// WARNING: In order to support global statically constructed TimedMutexes, the
-// user and/or backend MUST ensure that any initialization required in your
-// environment is done prior to the creation and/or initialization of the native
-// synchronization primitives (e.g. kernel initialization).
+/// The `TimedMutex` is a synchronization primitive that can be used to protect
+/// shared data from being simultaneously accessed by multiple threads with
+/// timeouts and deadlines, extending the `Mutex`. It offers exclusive,
+/// non-recursive ownership semantics where priority inheritance is used to
+/// solve the classic priority-inversion problem. This is thread safe, but NOT
+/// IRQ safe.
+///
+/// @rst
+/// .. warning::
+/// In order to support global statically constructed TimedMutexes, the user
+/// and/or backend MUST ensure that any initialization required in your
+/// environment is done prior to the creation and/or initialization of the
+/// native synchronization primitives (e.g. kernel initialization).
+/// @endrst
class TimedMutex : public Mutex {
public:
TimedMutex() = default;
@@ -46,23 +49,23 @@ class TimedMutex : public Mutex {
TimedMutex& operator=(const TimedMutex&) = delete;
TimedMutex& operator=(TimedMutex&&) = delete;
- // Tries to lock the mutex. Blocks until specified the timeout has elapsed or
- // the lock is acquired, whichever comes first.
- // Returns true if the mutex was successfully acquired.
- //
- // PRECONDITION:
- // The lock isn't already held by this thread. Recursive locking is
- // undefined behavior.
+ /// Tries to lock the mutex. Blocks until specified the timeout has elapsed or
+ /// the lock is acquired, whichever comes first.
+ /// Returns true if the mutex was successfully acquired.
+ ///
+ /// @b PRECONDITION:
+ /// The lock isn't already held by this thread. Recursive locking is
+ /// undefined behavior.
bool try_lock_for(chrono::SystemClock::duration timeout)
PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);
- // Tries to lock the mutex. Blocks until specified deadline has been reached
- // or the lock is acquired, whichever comes first.
- // Returns true if the mutex was successfully acquired.
- //
- // PRECONDITION:
- // The lock isn't already held by this thread. Recursive locking is
- // undefined behavior.
+ /// Tries to lock the mutex. Blocks until specified deadline has been reached
+ /// or the lock is acquired, whichever comes first.
+ /// Returns true if the mutex was successfully acquired.
+ ///
+ /// @b PRECONDITION:
+ /// The lock isn't already held by this thread. Recursive locking is
+ /// undefined behavior.
bool try_lock_until(chrono::SystemClock::time_point deadline)
PW_EXCLUSIVE_TRYLOCK_FUNCTION(true);
};
@@ -109,16 +112,26 @@ typedef struct pw_sync_TimedMutex pw_sync_TimedMutex;
PW_EXTERN_C_START
+/// Invokes the `TimedMutex::lock` member function on the given `mutex`.
void pw_sync_TimedMutex_Lock(pw_sync_TimedMutex* mutex)
PW_NO_LOCK_SAFETY_ANALYSIS;
+
+/// Invokes the `TimedMutex::try_lock` member function on the given `mutex`.
bool pw_sync_TimedMutex_TryLock(pw_sync_TimedMutex* mutex)
PW_NO_LOCK_SAFETY_ANALYSIS;
+
+/// Invokes the `TimedMutex::try_lock_for` member function on the given `mutex`.
bool pw_sync_TimedMutex_TryLockFor(pw_sync_TimedMutex* mutex,
pw_chrono_SystemClock_Duration timeout)
PW_NO_LOCK_SAFETY_ANALYSIS;
+
+/// Invokes the `TimedMutex::try_lock_until` member function on the given
+/// `mutex`.
bool pw_sync_TimedMutex_TryLockUntil(pw_sync_TimedMutex* mutex,
pw_chrono_SystemClock_TimePoint deadline)
PW_NO_LOCK_SAFETY_ANALYSIS;
+
+/// Invokes the `TimedMutex::unlock` member function on the given `mutex`.
void pw_sync_TimedMutex_Unlock(pw_sync_TimedMutex* mutex)
PW_NO_LOCK_SAFETY_ANALYSIS;
diff --git a/pw_sync/public/pw_sync/timed_thread_notification.h b/pw_sync/public/pw_sync/timed_thread_notification.h
index 3e17db194..7b98b39d8 100644
--- a/pw_sync/public/pw_sync/timed_thread_notification.h
+++ b/pw_sync/public/pw_sync/timed_thread_notification.h
@@ -18,20 +18,23 @@
namespace pw::sync {
-// The TimedThreadNotification is a synchronization primitive that can be used
-// to permit a SINGLE thread to block and consume a latching, saturating
-// notification from multiple notifiers.
-//
-// IMPORTANT: This is a single consumer/waiter, multiple producer/notifier API!
-// The acquire APIs must only be invoked by a single consuming thread. As a
-// result, having multiple threads receiving notifications via the acquire API
-// is unsupported.
-//
-// This is effectively a subset of a binary semaphore API, except that only a
-// single thread can be notified and block at a time.
-//
-// The single consumer aspect of the API permits the use of a smaller and/or
-// faster native APIs such as direct thread signaling.
+/// The `TimedThreadNotification` is a synchronization primitive that can be
+/// used to permit a SINGLE thread to block and consume a latching, saturating
+/// notification from multiple notifiers.
+///
+/// @b IMPORTANT: This is a single consumer/waiter, multiple producer/notifier
+/// API! The acquire APIs must only be invoked by a single consuming thread. As
+/// a result, having multiple threads receiving notifications via the acquire
+/// API is unsupported.
+///
+/// This is effectively a subset of a binary semaphore API, except that only a
+/// single thread can be notified and block at a time.
+///
+/// The single consumer aspect of the API permits the use of a smaller and/or
+/// faster native APIs such as direct thread signaling.
+///
+/// The `TimedThreadNotification` is initialized to being empty (latch is not
+/// set).
class TimedThreadNotification : public ThreadNotification {
public:
TimedThreadNotification() = default;
@@ -41,28 +44,28 @@ class TimedThreadNotification : public ThreadNotification {
TimedThreadNotification& operator=(const TimedThreadNotification&) = delete;
TimedThreadNotification& operator=(TimedThreadNotification&&) = delete;
- // Blocks until the specified timeout duration has elapsed or the thread
- // has been notified (i.e. notification latch can be cleared because it was
- // set), whichever comes first.
- //
- // Clears the notification latch.
- //
- // Returns true if the thread was notified, meaning the the internal latch was
- // reset successfully.
- //
- // IMPORTANT: This should only be used by a single consumer thread.
+ /// Blocks until the specified timeout duration has elapsed or the thread
+ /// has been notified (i.e. notification latch can be cleared because it was
+ /// set), whichever comes first.
+ ///
+ /// Clears the notification latch.
+ ///
+ /// Returns true if the thread was notified, meaning the the internal latch
+ /// was reset successfully.
+ ///
+ /// @b IMPORTANT: This should only be used by a single consumer thread.
bool try_acquire_for(chrono::SystemClock::duration timeout);
- // Blocks until the specified deadline time has been reached the thread has
- // been notified (i.e. notification latch can be cleared because it was set),
- // whichever comes first.
- //
- // Clears the notification latch.
- //
- // Returns true if the thread was notified, meaning the the internal latch was
- // reset successfully.
- //
- // IMPORTANT: This should only be used by a single consumer thread.
+ /// Blocks until the specified deadline time has been reached the thread has
+ /// been notified (i.e. notification latch can be cleared because it was set),
+ /// whichever comes first.
+ ///
+ /// Clears the notification latch.
+ ///
+ /// Returns true if the thread was notified, meaning the the internal latch
+ /// was reset successfully.
+ ///
+ /// @b IMPORTANT: This should only be used by a single consumer thread.
bool try_acquire_until(chrono::SystemClock::time_point deadline);
};
diff --git a/pw_sync/public/pw_sync/virtual_basic_lockable.h b/pw_sync/public/pw_sync/virtual_basic_lockable.h
index 4593e0767..501036535 100644
--- a/pw_sync/public/pw_sync/virtual_basic_lockable.h
+++ b/pw_sync/public/pw_sync/virtual_basic_lockable.h
@@ -18,21 +18,21 @@
namespace pw::sync {
-// The VirtualBasicLockable is a virtual lock abstraction for locks which meet
-// the C++ named BasicLockable requirements of lock() and unlock().
-//
-// This virtual indirection is useful in case you need configurable lock
-// selection in a portable module where the final type is not defined upstream
-// and ergo module configuration cannot be used or in case the lock type is not
-// fixed at compile time, for example to support run time and crash time use of
-// an object without incurring the code size hit for templating the object.
+/// The `VirtualBasicLockable` is a virtual lock abstraction for locks which
+/// meet the C++ named BasicLockable requirements of lock() and unlock().
+///
+/// This virtual indirection is useful in case you need configurable lock
+/// selection in a portable module where the final type is not defined upstream
+/// and ergo module configuration cannot be used or in case the lock type is not
+/// fixed at compile time, for example to support run time and crash time use of
+/// an object without incurring the code size hit for templating the object.
class PW_LOCKABLE("pw::sync::VirtualBasicLockable") VirtualBasicLockable {
public:
void lock() PW_EXCLUSIVE_LOCK_FUNCTION() {
DoLockOperation(Operation::kLock);
- };
+ }
- void unlock() PW_UNLOCK_FUNCTION() { DoLockOperation(Operation::kUnlock); };
+ void unlock() PW_UNLOCK_FUNCTION() { DoLockOperation(Operation::kUnlock); }
protected:
~VirtualBasicLockable() = default;
@@ -43,13 +43,13 @@ class PW_LOCKABLE("pw::sync::VirtualBasicLockable") VirtualBasicLockable {
};
private:
- // Uses a single virtual method with an enum to minimize the vtable cost per
- // implementation of VirtualBasicLockable.
+ /// Uses a single virtual method with an enum to minimize the vtable cost per
+ /// implementation of `VirtualBasicLockable`.
virtual void DoLockOperation(Operation operation) = 0;
};
-// The NoOpLock is a type of VirtualBasicLockable that does nothing, i.e. lock
-// operations are no-ops.
+/// The `NoOpLock` is a type of `VirtualBasicLockable` that does nothing, i.e.
+/// lock operations are no-ops.
class PW_LOCKABLE("pw::sync::NoOpLock") NoOpLock final
: public VirtualBasicLockable {
public:
@@ -59,8 +59,8 @@ class PW_LOCKABLE("pw::sync::NoOpLock") NoOpLock final
NoOpLock& operator=(const NoOpLock&) = delete;
NoOpLock& operator=(NoOpLock&&) = delete;
- // Gives access to a global NoOpLock instance. It is not necessary to have
- // multiple NoOpLock instances since they have no state and do nothing.
+ /// Gives access to a global NoOpLock instance. It is not necessary to have
+ /// multiple NoOpLock instances since they have no state and do nothing.
static NoOpLock& Instance() {
PW_CONSTINIT static NoOpLock lock;
return lock;
diff --git a/pw_sync/recursive_mutex.cc b/pw_sync/recursive_mutex.cc
new file mode 100644
index 000000000..89849704e
--- /dev/null
+++ b/pw_sync/recursive_mutex.cc
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_sync/recursive_mutex.h"
+
+extern "C" void pw_sync_RecursiveMutex_Lock(pw_sync_RecursiveMutex* mutex) {
+ mutex->lock();
+}
+
+extern "C" bool pw_sync_RecursiveMutex_TryLock(pw_sync_RecursiveMutex* mutex) {
+ return mutex->try_lock();
+}
+
+extern "C" void pw_sync_RecursiveMutex_Unlock(pw_sync_RecursiveMutex* mutex) {
+ mutex->unlock();
+}
diff --git a/pw_sync/recursive_mutex_facade_test.cc b/pw_sync/recursive_mutex_facade_test.cc
new file mode 100644
index 000000000..6dd2bfbc8
--- /dev/null
+++ b/pw_sync/recursive_mutex_facade_test.cc
@@ -0,0 +1,96 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <chrono>
+
+#include "gtest/gtest.h"
+#include "pw_sync/recursive_mutex.h"
+
+namespace pw::sync {
+namespace {
+
+extern "C" {
+
+// Functions defined in recusive_mutex_facade_test_c.c that call the API from C.
+void pw_sync_RecursiveMutex_CallLock(pw_sync_RecursiveMutex* mutex);
+bool pw_sync_RecursiveMutex_CallTryLock(pw_sync_RecursiveMutex* mutex);
+void pw_sync_RecursiveMutex_CallUnlock(pw_sync_RecursiveMutex* mutex);
+
+} // extern "C"
+
+// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
+
+TEST(RecursiveMutex, LockUnlock) PW_NO_LOCK_SAFETY_ANALYSIS {
+ pw::sync::RecursiveMutex mutex;
+ for (int i = 0; i < 10; ++i) {
+ mutex.lock();
+ }
+
+ for (int i = 0; i < 10; ++i) {
+ mutex.unlock();
+ }
+}
+
+RecursiveMutex static_mutex;
+TEST(RecursiveMutex, LockUnlockStatic) PW_NO_LOCK_SAFETY_ANALYSIS {
+ static_mutex.lock();
+ for (int i = 0; i < 10; ++i) {
+ EXPECT_TRUE(static_mutex.try_lock());
+ }
+ for (int i = 0; i < 10; ++i) {
+ static_mutex.unlock(); // undo the try_lock() calls
+ }
+ static_mutex.unlock(); // undo the inital lock() call
+}
+
+TEST(RecursiveMutex, TryLockUnlock) PW_NO_LOCK_SAFETY_ANALYSIS {
+ pw::sync::RecursiveMutex mutex;
+ ASSERT_TRUE(mutex.try_lock());
+
+ const bool locked_again = mutex.try_lock();
+ EXPECT_TRUE(locked_again);
+ if (locked_again) {
+ mutex.unlock();
+ }
+
+ mutex.unlock();
+}
+
+TEST(RecursiveMutex, LockUnlockInC) {
+ pw::sync::RecursiveMutex mutex;
+
+ pw_sync_RecursiveMutex_CallLock(&mutex);
+ pw_sync_RecursiveMutex_CallLock(&mutex);
+ pw_sync_RecursiveMutex_CallLock(&mutex);
+
+ pw_sync_RecursiveMutex_CallUnlock(&mutex);
+ pw_sync_RecursiveMutex_CallUnlock(&mutex);
+ pw_sync_RecursiveMutex_CallUnlock(&mutex);
+}
+
+TEST(RecursiveMutex, TryLockUnlockInC) {
+ pw::sync::RecursiveMutex mutex;
+ ASSERT_TRUE(pw_sync_RecursiveMutex_CallTryLock(&mutex));
+
+ const bool locked_again = pw_sync_RecursiveMutex_CallTryLock(&mutex);
+ EXPECT_TRUE(locked_again);
+ if (locked_again) {
+ pw_sync_RecursiveMutex_CallUnlock(&mutex);
+ }
+
+ pw_sync_RecursiveMutex_CallUnlock(&mutex);
+}
+
+} // namespace
+} // namespace pw::sync
diff --git a/pw_sync/recursive_mutex_facade_test_c.c b/pw_sync/recursive_mutex_facade_test_c.c
new file mode 100644
index 000000000..a56e62a5d
--- /dev/null
+++ b/pw_sync/recursive_mutex_facade_test_c.c
@@ -0,0 +1,32 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// These tests call the pw_sync module mutex API from C. The return values are
+// checked in the main C++ tests.
+
+#include <stdbool.h>
+
+#include "pw_sync/recursive_mutex.h"
+
+void pw_sync_RecursiveMutex_CallLock(pw_sync_RecursiveMutex* mutex) {
+ pw_sync_RecursiveMutex_Lock(mutex);
+}
+
+bool pw_sync_RecursiveMutex_CallTryLock(pw_sync_RecursiveMutex* mutex) {
+ return pw_sync_RecursiveMutex_TryLock(mutex);
+}
+
+void pw_sync_RecursiveMutex_CallUnlock(pw_sync_RecursiveMutex* mutex) {
+ pw_sync_RecursiveMutex_Unlock(mutex);
+}
diff --git a/pw_sync/thread_notification_facade_test.cc b/pw_sync/thread_notification_facade_test.cc
index c8f6eaa24..fa10dfa54 100644
--- a/pw_sync/thread_notification_facade_test.cc
+++ b/pw_sync/thread_notification_facade_test.cc
@@ -25,7 +25,7 @@ TEST(ThreadNotification, EmptyInitialState) {
EXPECT_FALSE(notification.try_acquire());
}
-// TODO(pwbug/291): Add real concurrency tests.
+// TODO(b/235284163): Add real concurrency tests.
TEST(ThreadNotification, Release) {
ThreadNotification notification;
diff --git a/pw_sync/timed_mutex_facade_test.cc b/pw_sync/timed_mutex_facade_test.cc
index 75ba2b625..c63233757 100644
--- a/pw_sync/timed_mutex_facade_test.cc
+++ b/pw_sync/timed_mutex_facade_test.cc
@@ -45,13 +45,14 @@ constexpr SystemClock::duration kRoundedArbitraryDuration =
constexpr pw_chrono_SystemClock_Duration kRoundedArbitraryDurationInC =
PW_SYSTEM_CLOCK_MS(42);
-// TODO(pwbug/291): Add real concurrency tests once we have pw::thread.
+// TODO(b/235284163): Add real concurrency tests once we have pw::thread.
TEST(TimedMutex, LockUnlock) {
pw::sync::TimedMutex mutex;
mutex.lock();
mutex.unlock();
- // TODO(pwbug/291): Ensure it fails to lock when already held by someone else.
+ // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // else.
// EXPECT_FALSE(mutex.try_lock());
}
@@ -59,7 +60,8 @@ TimedMutex static_mutex;
TEST(TimedMutex, LockUnlockStatic) {
static_mutex.lock();
static_mutex.unlock();
- // TODO(pwbug/291): Ensure it fails to lock when already held by someone else.
+ // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // else.
// EXPECT_FALSE(static_mutex.try_lock());
}
@@ -71,7 +73,7 @@ TEST(TimedMutex, TryLockUnlock) {
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
- // TODO(pwbug/291): Ensure it fails to lock when already held by someone
+ // TODO(b/235284163): Ensure it fails to lock when already held by someone
// else.
}
@@ -86,11 +88,11 @@ TEST(TimedMutex, TryLockUnlockFor) {
EXPECT_LT(time_elapsed, kRoundedArbitraryDuration);
mutex.unlock();
}
- // TODO(pwbug/291): Ensure it blocks and fails to lock when already held by
+ // TODO(b/235284163): Ensure it blocks and fails to lock when already held by
// someone else.
- // TODO(pwbug/291): Ensure it does not block and fails to lock when already
+ // TODO(b/235284163): Ensure it does not block and fails to lock when already
// held by someone else and a zero length duration is used.
- // TODO(pwbug/291): Ensure it does not block and fails to lock when already
+ // TODO(b/235284163): Ensure it does not block and fails to lock when already
// held by someone else and a negative duration is used.
}
@@ -105,18 +107,19 @@ TEST(TimedMutex, TryLockUnlockUntil) {
EXPECT_LT(SystemClock::now(), deadline);
mutex.unlock();
}
- // TODO(pwbug/291): Ensure it blocks and fails to lock when already held by
+ // TODO(b/235284163): Ensure it blocks and fails to lock when already held by
// someone else.
- // TODO(pwbug/291): Ensure it does not block and fails to lock when already
+ // TODO(b/235284163): Ensure it does not block and fails to lock when already
// held by someone else and now is used.
- // TODO(pwbug/291): Ensure it does not block and fails to lock when already
+ // TODO(b/235284163): Ensure it does not block and fails to lock when already
// held by someone else and a timestamp in the past is used.
}
TEST(VirtualTimedMutex, LockUnlock) {
pw::sync::VirtualTimedMutex mutex;
mutex.lock();
- // TODO(pwbug/291): Ensure it fails to lock when already held by someone else.
+ // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // else.
// EXPECT_FALSE(mutex.try_lock());
mutex.unlock();
}
@@ -124,7 +127,8 @@ TEST(VirtualTimedMutex, LockUnlock) {
VirtualTimedMutex static_virtual_mutex;
TEST(VirtualTimedMutex, LockUnlockStatic) {
static_virtual_mutex.lock();
- // TODO(pwbug/291): Ensure it fails to lock when already held by someone else.
+ // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // else.
// EXPECT_FALSE(static_virtual_mutex.try_lock());
static_virtual_mutex.unlock();
}
@@ -138,7 +142,8 @@ TEST(TimedMutex, LockUnlockInC) {
TEST(TimedMutex, TryLockUnlockInC) {
pw::sync::TimedMutex mutex;
ASSERT_TRUE(pw_sync_TimedMutex_CallTryLock(&mutex));
- // TODO(pwbug/291): Ensure it fails to lock when already held by someone else.
+ // TODO(b/235284163): Ensure it fails to lock when already held by someone
+ // else.
// EXPECT_FALSE(pw_sync_TimedMutex_CallTryLock(&mutex));
pw_sync_TimedMutex_CallUnlock(&mutex);
}
@@ -153,11 +158,11 @@ TEST(TimedMutex, TryLockUnlockForInC) {
pw_chrono_SystemClock_TimeElapsed(before, pw_chrono_SystemClock_Now());
EXPECT_LT(time_elapsed.ticks, kRoundedArbitraryDurationInC.ticks);
pw_sync_TimedMutex_CallUnlock(&mutex);
- // TODO(pwbug/291): Ensure it blocks and fails to lock when already held by
+ // TODO(b/235284163): Ensure it blocks and fails to lock when already held by
// someone else.
- // TODO(pwbug/291): Ensure it does not block and fails to lock when already
+ // TODO(b/235284163): Ensure it does not block and fails to lock when already
// held by someone else and a zero length duration is used.
- // TODO(pwbug/291): Ensure it does not block and fails to lock when already
+ // TODO(b/235284163): Ensure it does not block and fails to lock when already
// held by someone else and a negative duration is used.
}
@@ -171,11 +176,11 @@ TEST(TimedMutex, TryLockUnlockUntilInC) {
EXPECT_LT(pw_chrono_SystemClock_Now().duration_since_epoch.ticks,
deadline.duration_since_epoch.ticks);
pw_sync_TimedMutex_CallUnlock(&mutex);
- // TODO(pwbug/291): Ensure it blocks and fails to lock when already held by
+ // TODO(b/235284163): Ensure it blocks and fails to lock when already held by
// someone else.
- // TODO(pwbug/291): Ensure it does not block and fails to lock when already
+ // TODO(b/235284163): Ensure it does not block and fails to lock when already
// held by someone else and now is used.
- // TODO(pwbug/291): Ensure it does not block and fails to lock when already
+ // TODO(b/235284163): Ensure it does not block and fails to lock when already
// held by someone else and a timestamp in the past is used.
}
diff --git a/pw_sync/timed_thread_notification_facade_test.cc b/pw_sync/timed_thread_notification_facade_test.cc
index 84bb415e3..ee1da8983 100644
--- a/pw_sync/timed_thread_notification_facade_test.cc
+++ b/pw_sync/timed_thread_notification_facade_test.cc
@@ -35,7 +35,7 @@ TEST(TimedThreadNotification, EmptyInitialState) {
EXPECT_FALSE(notification.try_acquire());
}
-// TODO(pwbug/291): Add real concurrency tests.
+// TODO(b/235284163): Add real concurrency tests.
TEST(TimedThreadNotification, Release) {
TimedThreadNotification notification;
diff --git a/pw_sync_baremetal/Android.bp b/pw_sync_baremetal/Android.bp
new file mode 100644
index 000000000..691bf7bcb
--- /dev/null
+++ b/pw_sync_baremetal/Android.bp
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_sync_baremetal_headers",
+ vendor_available: true,
+ export_include_dirs: [
+ "public",
+ "public_overrides",
+ ],
+ host_supported: true,
+}
diff --git a/pw_sync_baremetal/BUILD.bazel b/pw_sync_baremetal/BUILD.bazel
index e23c3c7a5..d401d99d7 100644
--- a/pw_sync_baremetal/BUILD.bazel
+++ b/pw_sync_baremetal/BUILD.bazel
@@ -71,3 +71,23 @@ pw_cc_library(
"//pw_sync:mutex_facade",
],
)
+
+pw_cc_library(
+ name = "recursive_mutex",
+ hdrs = [
+ "public/pw_sync_baremetal/recursive_mutex_inline.h",
+ "public/pw_sync_baremetal/recursive_mutex_native.h",
+ "public_overrides/pw_sync_backend/recursive_mutex_inline.h",
+ "public_overrides/pw_sync_backend/recursive_mutex_native.h",
+ ],
+ includes = [
+ "public",
+ "public_overrides",
+ ],
+ target_compatible_with = ["@platforms//os:none"],
+ visibility = ["//pw_sync:__pkg__"],
+ deps = [
+ "//pw_assert",
+ "//pw_sync:recursive_mutex_facade",
+ ],
+)
diff --git a/pw_sync_baremetal/BUILD.gn b/pw_sync_baremetal/BUILD.gn
index e862556d8..a857c7af6 100644
--- a/pw_sync_baremetal/BUILD.gn
+++ b/pw_sync_baremetal/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
include_dirs = [ "public" ]
@@ -74,6 +75,31 @@ pw_source_set("mutex") {
]
}
+# This target provides the backend for pw::sync::RecursiveMutex.
+# The provided implementation makes a single attempt to acquire the lock and
+# asserts if it is unavailable. This implementation is not yet set up to support
+# hardware multi-threading (SMP, SMT, etc).
+pw_source_set("recursive_mutex") {
+ public_configs = [
+ ":public_include_path",
+ ":backend_config",
+ ]
+ public = [
+ "public/pw_sync_baremetal/recursive_mutex_inline.h",
+ "public/pw_sync_baremetal/recursive_mutex_native.h",
+ "public_overrides/pw_sync_backend/recursive_mutex_inline.h",
+ "public_overrides/pw_sync_backend/recursive_mutex_native.h",
+ ]
+ public_deps = [
+ "$dir_pw_assert",
+ "$dir_pw_sync:recursive_mutex.facade",
+ ]
+ visibility = [ "$dir_pw_sync:*" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sync_baremetal/CMakeLists.txt b/pw_sync_baremetal/CMakeLists.txt
new file mode 100644
index 000000000..574a35fa2
--- /dev/null
+++ b/pw_sync_baremetal/CMakeLists.txt
@@ -0,0 +1,29 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_sync_baremetal.recursive_mutex INTERFACE
+ HEADERS
+ public/pw_sync_baremetal/recursive_mutex_inline.h
+ public/pw_sync_baremetal/recursive_mutex_native.h
+ public_overrides/pw_sync_backend/recursive_mutex_inline.h
+ public_overrides/pw_sync_backend/recursive_mutex_native.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_assert
+ pw_sync.recursive_mutex.facade
+)
diff --git a/pw_sync_baremetal/docs.rst b/pw_sync_baremetal/docs.rst
index 827679140..421af51a9 100644
--- a/pw_sync_baremetal/docs.rst
+++ b/pw_sync_baremetal/docs.rst
@@ -7,22 +7,29 @@ This is a set of backends for pw_sync that works on baremetal targets. It is not
ready for use, and is under construction.
.. note::
- All constructs in this baremetal backend do not support hardware multi-threading
- (SMP, SMT, etc).
+ All constructs in this baremetal backend do not support hardware
+ multi-threading (SMP, SMT, etc).
.. warning::
- It does not perform interrupt masking or disable global interrupts. This is not
- safe to use yet!
+ It does not perform interrupt masking or disable global interrupts. This is
+ not safe to use yet!
--------------------------------------
-pw_sync_baremetal's InterruptSpinLock
--------------------------------------
-The interrupt spin-lock implementation makes a single attempt to acquire the lock
-and asserts if it is unavailable. It does not perform interrupt masking or disable global
-interrupts.
+-----------------
+InterruptSpinLock
+-----------------
+The interrupt spin-lock implementation makes a single attempt to acquire the
+lock and asserts if it is unavailable. It does not perform interrupt masking or
+disable global interrupts.
--------------------------
-pw_sync_baremetal's Mutex
--------------------------
-The mutex implementation makes a single attempt to acquire the lock and asserts if
-it is unavailable.
+-----
+Mutex
+-----
+The mutex implementation makes a single attempt to acquire the lock and asserts
+if it is unavailable.
+
+--------------
+RecursiveMutex
+--------------
+The recursive mutex implementation counts the number of lock and unlock calls
+and asserts if the mutex is unlocked too many times or destroyed while locked.
+Note that recursive mutexes are not available for general use in Pigweed.
diff --git a/pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h b/pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h
index 505cef396..70b286bc2 100644
--- a/pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h
+++ b/pw_sync_baremetal/public/pw_sync_baremetal/interrupt_spin_lock_inline.h
@@ -24,7 +24,7 @@ constexpr InterruptSpinLock::InterruptSpinLock() : native_type_() {}
inline void InterruptSpinLock::lock() { PW_ASSERT(try_lock()); }
inline bool InterruptSpinLock::try_lock() {
- // TODO(pwbug/303): Use the pw_interrupt API here to disable interrupts.
+ // TODO(b/235352722): Use the pw_interrupt API here to disable interrupts.
return !native_type_.test_and_set(std::memory_order_acquire);
}
diff --git a/pw_sync_baremetal/public/pw_sync_baremetal/mutex_inline.h b/pw_sync_baremetal/public/pw_sync_baremetal/mutex_inline.h
index 70a10c21e..a04306557 100644
--- a/pw_sync_baremetal/public/pw_sync_baremetal/mutex_inline.h
+++ b/pw_sync_baremetal/public/pw_sync_baremetal/mutex_inline.h
@@ -20,7 +20,7 @@ namespace pw::sync {
inline Mutex::Mutex() : native_type_() {}
-inline Mutex::~Mutex() {}
+inline Mutex::~Mutex() = default;
inline void Mutex::lock() { PW_ASSERT(try_lock()); }
diff --git a/pw_sync_baremetal/public/pw_sync_baremetal/recursive_mutex_inline.h b/pw_sync_baremetal/public/pw_sync_baremetal/recursive_mutex_inline.h
new file mode 100644
index 000000000..4a40e9b8f
--- /dev/null
+++ b/pw_sync_baremetal/public/pw_sync_baremetal/recursive_mutex_inline.h
@@ -0,0 +1,46 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <limits>
+
+#include "pw_assert/assert.h"
+#include "pw_sync/recursive_mutex.h"
+
+namespace pw::sync {
+
+inline RecursiveMutex::RecursiveMutex() : native_type_(0) {}
+
+inline RecursiveMutex::~RecursiveMutex() = default;
+
+inline void RecursiveMutex::lock() {
+ try_lock(); // Locking always succeeds
+}
+
+inline bool RecursiveMutex::try_lock() {
+ int lock_count = native_type_.fetch_add(1, std::memory_order_acquire);
+ PW_DASSERT(lock_count != std::numeric_limits<int>::max()); // Detect overflow
+ return true; // No threads, so you can always acquire a recursive mutex.
+}
+
+inline void RecursiveMutex::unlock() {
+ int lock_count = native_type_.fetch_sub(1, std::memory_order_release);
+ PW_ASSERT(lock_count > 0); // Unlocked mutex that wasn't held
+}
+
+inline RecursiveMutex::native_handle_type RecursiveMutex::native_handle() {
+ return native_type_;
+}
+
+} // namespace pw::sync
diff --git a/pw_sync_baremetal/public/pw_sync_baremetal/recursive_mutex_native.h b/pw_sync_baremetal/public/pw_sync_baremetal/recursive_mutex_native.h
new file mode 100644
index 000000000..8fb87048a
--- /dev/null
+++ b/pw_sync_baremetal/public/pw_sync_baremetal/recursive_mutex_native.h
@@ -0,0 +1,23 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <atomic>
+
+namespace pw::sync::backend {
+
+using NativeRecursiveMutex = std::atomic<int>;
+using NativeRecursiveMutexHandle = std::atomic<int>&;
+
+} // namespace pw::sync::backend
diff --git a/pw_sync_baremetal/public_overrides/pw_sync_backend/recursive_mutex_inline.h b/pw_sync_baremetal/public_overrides/pw_sync_backend/recursive_mutex_inline.h
new file mode 100644
index 000000000..db2784709
--- /dev/null
+++ b/pw_sync_baremetal/public_overrides/pw_sync_backend/recursive_mutex_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_sync_baremetal/recursive_mutex_inline.h"
diff --git a/pw_sync_baremetal/public_overrides/pw_sync_backend/recursive_mutex_native.h b/pw_sync_baremetal/public_overrides/pw_sync_backend/recursive_mutex_native.h
new file mode 100644
index 000000000..96afb844d
--- /dev/null
+++ b/pw_sync_baremetal/public_overrides/pw_sync_backend/recursive_mutex_native.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_sync_baremetal/recursive_mutex_native.h"
diff --git a/pw_sync_embos/BUILD.bazel b/pw_sync_embos/BUILD.bazel
index 23ad1ff35..81f2e83e2 100644
--- a/pw_sync_embos/BUILD.bazel
+++ b/pw_sync_embos/BUILD.bazel
@@ -37,7 +37,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:embos",
],
deps = [
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
"//pw_chrono:system_clock",
"//pw_chrono_embos:system_clock_headers",
@@ -75,7 +75,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:embos",
],
deps = [
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
"//pw_chrono:system_clock",
"//pw_chrono_embos:system_clock_headers",
@@ -113,7 +113,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:embos",
],
deps = [
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
"//pw_sync:mutex_facade",
],
@@ -144,7 +144,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:embos",
],
deps = [
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
"//pw_chrono:system_clock",
"//pw_sync:timed_mutex_facade",
@@ -182,7 +182,7 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:embos",
],
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
)
diff --git a/pw_sync_embos/BUILD.gn b/pw_sync_embos/BUILD.gn
index c3997484b..7f4376612 100644
--- a/pw_sync_embos/BUILD.gn
+++ b/pw_sync_embos/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_build/error.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
include_dirs = [ "public" ]
@@ -158,3 +159,6 @@ pw_source_set("interrupt_spin_lock") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sync_freertos/BUILD.bazel b/pw_sync_freertos/BUILD.bazel
index aca4016da..1be8b9faa 100644
--- a/pw_sync_freertos/BUILD.bazel
+++ b/pw_sync_freertos/BUILD.bazel
@@ -37,11 +37,10 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties currently
- # do not have Bazel support.
"//pw_assert",
"//pw_chrono:system_clock",
"//pw_chrono_freertos:system_clock_headers",
+ "@freertos",
],
)
@@ -77,12 +76,11 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties currently
- # do not have Bazel support.
"//pw_assert",
- "//pw_sync:counting_semaphore_facade",
"//pw_chrono:system_clock",
"//pw_chrono_freertos:system_clock_headers",
+ "//pw_sync:counting_semaphore_facade",
+ "@freertos",
],
)
@@ -91,9 +89,17 @@ pw_cc_library(
srcs = [
"counting_semaphore.cc",
],
- target_compatible_with = [
- "//pw_build/constraints/rtos:freertos",
- ],
+ target_compatible_with = select({
+ # Not compatible with this FreeRTOS config, because it does not enable
+ # FreeRTOS counting semaphores. We mark it explicitly incompatible to
+ # that this library is skipped when you
+ # `bazel build //pw_sync_freertos/...` for a platform using that
+ # config.
+ "//targets/stm32f429i_disc1_stm32cube:freertos_config_cv": ["@platforms//:incompatible"],
+ "//conditions:default": [
+ "//pw_build/constraints/rtos:freertos",
+ ],
+ }),
deps = [
":counting_semaphore_headers",
"//pw_assert",
@@ -118,9 +124,9 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties currently
- # do not have Bazel support.
"//pw_assert",
+ "//pw_interrupt:context",
+ "@freertos",
],
)
@@ -152,9 +158,12 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
+ "//pw_assert",
"//pw_interrupt:context",
+ "//pw_polyfill",
+ "//pw_sync:interrupt_spin_lock",
+ "//pw_sync:lock_annotations",
+ "@freertos",
],
)
@@ -188,10 +197,9 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
"//pw_chrono:system_clock",
"//pw_sync:timed_thread_notification_facade",
+ "@freertos",
],
)
@@ -226,10 +234,9 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
"//pw_chrono:system_clock",
"//pw_sync:timed_mutex_facade",
+ "@freertos",
],
)
@@ -242,6 +249,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:freertos",
],
deps = [
+ ":mutex_headers",
":timed_mutex_headers",
"//pw_assert",
"//pw_chrono_freertos:system_clock_headers",
@@ -265,8 +273,9 @@ pw_cc_library(
target_compatible_with = [
"//pw_build/constraints/rtos:freertos",
],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
+ deps = [
+ "@freertos",
+ ],
)
pw_cc_library(
@@ -284,3 +293,99 @@ pw_cc_library(
"//pw_sync:interrupt_spin_lock_facade",
],
)
+
+# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+#
+# You can instantiate this with your own implementation of
+# "//pw_thread:test_threads_header", see
+# ":thread_notification_test_with_static_threads" below as an example.
+# pw_cc_library(
+# name = "thread_notification_test",
+# srcs = [
+# "thread_notification_test.cc",
+# ],
+# target_compatible_with = [
+# "//pw_build/constraints/rtos:freertos",
+# ],
+# # TODO(b/234876414): This should depend on FreeRTOS but our third parties
+# # currently do not have Bazel support.
+# deps = [
+# "//pw_chrono:system_clock",
+# "//pw_sync:thread_notification",
+# "//pw_thread:sleep",
+# "//pw_thread:test_threads_header",
+# "//pw_thread:thread",
+# "//pw_unit_test",
+# ],
+# )
+# This is only used for the python tests.
+filegroup(
+ name = "thread_notification_test",
+ srcs = [
+ "thread_notification_test.cc",
+ ],
+)
+
+# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+# with:
+# enable_if = pw_sync_THREAD_NOTIFICATION_BACKEND ==
+# "$dir_pw_sync_freertos:thread_notification" &&
+# pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
+# pw_thread_THREAD_BACKEND != "" && pw_thread_SLEEP_BACKEND != ""
+#
+# pw_cc_test(
+# name = "thread_notification_test_with_static_threads",
+# target_compatible_with = [
+# "//pw_build/constraints/rtos:freertos",
+# ],
+# deps = [
+# ":thread_notification_test",
+# "//pw_thread_freertos:static_test_threads",
+# ],
+# )
+
+# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+#
+# You can instantiate this with your own implementation of
+# "//pw_thread:test_threads_header", see
+# ":timed_thread_notification_test_with_static_threads" below as an example.
+#pw_cc_library(
+# name = "timed_thread_notification_test",
+# srcs = [
+# "timed_thread_notification_test.cc",
+# ],
+# # TODO(b/234876414): This should depend on FreeRTOS but our third parties
+# # currently do not have Bazel support.
+# deps = [
+# "//pw_chrono:system_clock",
+# "//pw_sync:timed_thread_notification",
+# "//pw_thread:sleep",
+# "//pw_thread:test_threads_header",
+# "//pw_thread:thread",
+# "//pw_unit_test",
+# ],
+#)
+filegroup(
+ name = "timed_thread_notification_test",
+ srcs = [
+ "timed_thread_notification_test.cc",
+ ],
+)
+
+# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+# with:
+# enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND ==
+# "$dir_pw_sync_freertos:timed_thread_notification" &&
+# pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
+# pw_thread_THREAD_BACKEND != "" && pw_thread_SLEEP_BACKEND != ""
+#
+# pw_cc_test(
+# name = "timed_thread_notification_test_with_static_threads",
+# target_compatible_with = [
+# "//pw_build/constraints/rtos:freertos",
+# ],
+# deps = [
+# ":timed_thread_notification_test",
+# "//pw_thread_freertos:static_test_threads",
+# ],
+# )
diff --git a/pw_sync_freertos/BUILD.gn b/pw_sync_freertos/BUILD.gn
index c560d907d..89bc0a11a 100644
--- a/pw_sync_freertos/BUILD.gn
+++ b/pw_sync_freertos/BUILD.gn
@@ -19,6 +19,9 @@ import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# The build target that overrides the default configuration options for this
@@ -170,15 +173,16 @@ pw_source_set("thread_notification") {
"public_overrides/thread_notification/pw_sync_backend/thread_notification_native.h",
]
public_deps = [
+ "$dir_pw_assert",
"$dir_pw_interrupt:context",
+ "$dir_pw_polyfill",
+ "$dir_pw_sync:interrupt_spin_lock",
+ "$dir_pw_sync:lock_annotations",
"$dir_pw_sync:thread_notification.facade",
"$dir_pw_third_party/freertos",
]
sources = [ "thread_notification.cc" ]
- deps = [
- ":config",
- dir_pw_assert,
- ]
+ deps = [ ":config" ]
}
config("public_overrides_timed_thread_notification_include_path") {
@@ -199,6 +203,7 @@ pw_source_set("timed_thread_notification") {
]
public_deps = [
"$dir_pw_chrono:system_clock",
+ "$dir_pw_sync:interrupt_spin_lock",
"$dir_pw_sync:timed_thread_notification.facade",
]
sources = [ "timed_thread_notification.cc" ]
@@ -233,6 +238,65 @@ pw_source_set("interrupt_spin_lock") {
]
}
+pw_test_group("tests") {
+ tests = [
+ ":thread_notification_test_with_static_threads",
+ ":timed_thread_notification_test_with_static_threads",
+ ]
+}
+
+# You can instantiate this with your own provided "$dir_pw_thread:test_threads",
+# see ":thread_notification_test_with_static_threads" below as an example.
+pw_source_set("thread_notification_test") {
+ sources = [ "thread_notification_test.cc" ]
+ deps = [
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_sync:thread_notification",
+ "$dir_pw_third_party/freertos",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread:thread",
+ "$dir_pw_unit_test",
+ ]
+}
+
+pw_test("thread_notification_test_with_static_threads") {
+ enable_if = pw_sync_THREAD_NOTIFICATION_BACKEND ==
+ "$dir_pw_sync_freertos:thread_notification" &&
+ pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
+ pw_thread_THREAD_BACKEND != "" && pw_thread_SLEEP_BACKEND != ""
+ deps = [
+ ":thread_notification_test",
+ "$dir_pw_thread_freertos:static_test_threads",
+ ]
+}
+
+# You can instantiate this with your own provided "$dir_pw_thread:test_threads",
+# see ":timed_thread_notification_test_with_static_threads" below as an example.
+pw_source_set("timed_thread_notification_test") {
+ sources = [ "timed_thread_notification_test.cc" ]
+ deps = [
+ "$dir_pw_chrono:system_clock",
+ "$dir_pw_sync:timed_thread_notification",
+ "$dir_pw_third_party/freertos",
+ "$dir_pw_thread:sleep",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread:thread",
+ "$dir_pw_unit_test",
+ ]
+}
+
+pw_test("timed_thread_notification_test_with_static_threads") {
+ enable_if = pw_sync_TIMED_THREAD_NOTIFICATION_BACKEND ==
+ "$dir_pw_sync_freertos:timed_thread_notification" &&
+ pw_chrono_SYSTEM_CLOCK_BACKEND != "" &&
+ pw_thread_THREAD_BACKEND != "" && pw_thread_SLEEP_BACKEND != ""
+ deps = [
+ ":timed_thread_notification_test",
+ "$dir_pw_thread_freertos:static_test_threads",
+ ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
diff --git a/pw_sync_freertos/CMakeLists.txt b/pw_sync_freertos/CMakeLists.txt
index 6139c3966..19a30f4d1 100644
--- a/pw_sync_freertos/CMakeLists.txt
+++ b/pw_sync_freertos/CMakeLists.txt
@@ -16,7 +16,7 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_module_config(pw_sync_freertos_CONFIG)
-pw_add_module_library(pw_sync_freertos.config
+pw_add_library(pw_sync_freertos.config INTERFACE
HEADERS
public/pw_sync_freertos/config.h
PUBLIC_INCLUDES
@@ -29,9 +29,7 @@ pw_add_module_library(pw_sync_freertos.config
# TODO(ewout): Add system_clock backend compatibility check like in GN.
# This target provides the backend for pw::sync::BinarySemaphore.
-pw_add_module_library(pw_sync_freertos.binary_semaphore
- IMPLEMENTS_FACADES
- pw_sync.binary_semaphore
+pw_add_library(pw_sync_freertos.binary_semaphore STATIC
HEADERS
public/pw_sync_freertos/binary_semaphore_inline.h
public/pw_sync_freertos/binary_semaphore_native.h
@@ -45,15 +43,14 @@ pw_add_module_library(pw_sync_freertos.binary_semaphore
pw_chrono.system_clock
pw_chrono_freertos.system_clock
pw_interrupt.context
+ pw_sync.binary_semaphore.facade
pw_third_party.freertos
SOURCES
binary_semaphore.cc
)
# This target provides the backend for pw::sync::CountingSemaphore.
-pw_add_module_library(pw_sync_freertos.counting_semaphore
- IMPLEMENTS_FACADES
- pw_sync.counting_semaphore
+pw_add_library(pw_sync_freertos.counting_semaphore STATIC
HEADERS
public/pw_sync_freertos/counting_semaphore_inline.h
public/pw_sync_freertos/counting_semaphore_native.h
@@ -67,15 +64,14 @@ pw_add_module_library(pw_sync_freertos.counting_semaphore
pw_chrono.system_clock
pw_chrono_freertos.system_clock
pw_interrupt.context
+ pw_sync.counting_semaphore.facade
pw_third_party.freertos
SOURCES
counting_semaphore.cc
)
# This target provides the backend for pw::sync::Mutex.
-pw_add_module_library(pw_sync_freertos.mutex
- IMPLEMENTS_FACADES
- pw_sync.mutex
+pw_add_library(pw_sync_freertos.mutex INTERFACE
HEADERS
public/pw_sync_freertos/mutex_inline.h
public/pw_sync_freertos/mutex_native.h
@@ -87,13 +83,12 @@ pw_add_module_library(pw_sync_freertos.mutex
PUBLIC_DEPS
pw_assert
pw_interrupt.context
+ pw_sync.mutex.facade
pw_third_party.freertos
)
# This target provides the backend for pw::sync::TimedMutex.
-pw_add_module_library(pw_sync_freertos.timed_mutex
- IMPLEMENTS_FACADES
- pw_sync.timed_mutex
+pw_add_library(pw_sync_freertos.timed_mutex STATIC
HEADERS
public/pw_sync_freertos/timed_mutex_inline.h
public_overrides/pw_sync_backend/timed_mutex_inline.h
@@ -102,6 +97,7 @@ pw_add_module_library(pw_sync_freertos.timed_mutex
public_overrides
PUBLIC_DEPS
pw_chrono.system_clock
+ pw_sync.timed_mutex.facade
SOURCES
timed_mutex.cc
PRIVATE_DEPS
@@ -112,9 +108,7 @@ pw_add_module_library(pw_sync_freertos.timed_mutex
)
# This target provides the backend for pw::sync::ThreadNotification.
-pw_add_module_library(pw_sync_freertos.thread_notification
- IMPLEMENTS_FACADES
- pw_sync.thread_notification
+pw_add_library(pw_sync_freertos.thread_notification STATIC
HEADERS
public/pw_sync_freertos/thread_notification_inline.h
public/pw_sync_freertos/thread_notification_native.h
@@ -124,19 +118,21 @@ pw_add_module_library(pw_sync_freertos.thread_notification
public
public_overrides/thread_notification
PUBLIC_DEPS
+ pw_assert
pw_interrupt.context
+ pw_polyfill
+ pw_sync.interrupt_spin_lock
+ pw_sync.lock_annotations
+ pw_sync.thread_notification.facade
pw_third_party.freertos
SOURCES
thread_notification.cc
PRIVATE_DEPS
- pw_assert
pw_sync_freertos.config
)
# This target provides the backend for pw::sync::TimedThreadNotification.
-pw_add_module_library(pw_sync_freertos.timed_thread_notification
- IMPLEMENTS_FACADES
- pw_sync.timed_thread_notification
+pw_add_library(pw_sync_freertos.timed_thread_notification STATIC
HEADERS
public/pw_sync_freertos/timed_thread_notification_inline.h
public_overrides/timed_thread_notification/pw_sync_backend/timed_thread_notification_inline.h
@@ -145,6 +141,8 @@ pw_add_module_library(pw_sync_freertos.timed_thread_notification
public_overrides/timed_thread_notification
PUBLIC_DEPS
pw_chrono.system_clock
+ pw_sync.interrupt_spin_lock
+ pw_sync.timed_thread_notification.facade
SOURCES
timed_thread_notification.cc
PRIVATE_DEPS
@@ -156,9 +154,7 @@ pw_add_module_library(pw_sync_freertos.timed_thread_notification
)
# This target provides the backend for pw::sync::InterruptSpinLock.
-pw_add_module_library(pw_sync_freertos.interrupt_spin_lock
- IMPLEMENTS_FACADES
- pw_sync.interrupt_spin_lock
+pw_add_library(pw_sync_freertos.interrupt_spin_lock STATIC
HEADERS
public/pw_sync_freertos/interrupt_spin_lock_inline.h
public/pw_sync_freertos/interrupt_spin_lock_native.h
@@ -168,6 +164,7 @@ pw_add_module_library(pw_sync_freertos.interrupt_spin_lock
public
public_overrides
PUBLIC_DEPS
+ pw_sync.interrupt_spin_lock.facade
pw_third_party.freertos
SOURCES
interrupt_spin_lock.cc
diff --git a/pw_sync_freertos/docs.rst b/pw_sync_freertos/docs.rst
index a4fd5917e..77f7e3bf3 100644
--- a/pw_sync_freertos/docs.rst
+++ b/pw_sync_freertos/docs.rst
@@ -27,8 +27,119 @@ The FreeRTOS backend for InterruptSpinLock is backed by ``UBaseType_t`` and a
detect accidental recursive locking.
This object uses ``taskENTER_CRITICAL_FROM_ISR`` and
-``taskEXIT_CRITICAL_FROM_ISR`` from interrupt contexts and
-``taskENTER_CRITICAL`` and ``taskEXIT_CRITICAL`` from other contexts.
+``taskEXIT_CRITICAL_FROM_ISR`` from interrupt contexts, and
+``taskENTER_CRITICAL`` and ``taskEXIT_CRITICAL`` in all other contexts.
+``vTaskSuspendAll`` and ``xTaskResumeAll`` are additionally used within
+lock/unlock respectively when called from task context in the scheduler-enabled
+state.
+
+.. Note::
+ Scheduler State API support is required in your FreeRTOS Configuration, i.e.
+ ``INCLUDE_xTaskGetSchedulerState == 1``.
+
+.. warning::
+ ``taskENTER_CRITICAL_FROM_ISR`` only disables interrupts with priority at or
+ below ``configMAX_SYSCALL_INTERRUPT_PRIORITY``. Therefore, it is unsafe to
+ use InterruptSpinLock from higher-priority interrupts, even if they are not
+ non-maskable interrupts. This is consistent with the rest of the FreeRTOS
+ APIs, see the `FreeRTOS kernel interrupt priority documentation
+ <https://www.freertos.org/a00110.html#kernel_priority>`_ for more details.
+
+Design Notes
+------------
+FreeRTOS does not supply an interrupt spin-lock API, so this backend provides
+a suitable implementation using a compbination of both critical section and
+schduler APIs provided by FreeRTOS.
+
+This design is influenced by the following factors:
+
+- FreeRTOS support for both synchronous and asynchronous yield behavior in
+ different ports.
+- Critical sections behave differently depending on whether or not yield is
+ synchronous or asynchronous.
+- Users must be allowed to call functions that result in a call to yield
+ while an InterruptSpinLock is held.
+- The signaling mechanisms in FreeRTOS all internally call yield to preempt
+ the currently-running task in the event that a higher-priority task is
+ unblocked during execution.
+
+Synchronous and Asynchronous Yield
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+In FreeRTOS, any kernel API call that results in a higher-priority task being
+made “ready” triggers a call to ``taskYIELD()``.
+
+In some ports, this results in an immediate context switch directly from
+within the API - this is known as synchronous yielding behavior.
+
+In other cases, this results in a software-triggered interrupt
+being pended - and depending on the state of interrupts being masked, this
+results in thread-scheduling being deferred until interrupts are unmasked.
+This is known as asynchronous yielding behavior.
+
+As part of a yield, it is left to the port-specific code to call
+the FreeRTOS ``vTaskSwitchContext()`` function to swap current/ready tasks.
+This function will select the next task to run, and swap it for the
+currently executing task.
+
+Yield Within a Critical Section
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+A FreeRTOS critical section provides an interrupt-disabled context that ensures
+that a thread of execution cannot be interrupted by incoming ISRs.
+
+If a port implements asynchronous yield, any calls to ``taskYIELD()`` that
+occur during execution of a critical section will not be handled until the
+interrupts are re-enabled at the end of the critical section. As a result,
+any higher priority tasks that are unblocked will not preempt the current task
+from within the critical section. In these ports, a critical section alone is
+sufficient to prevent any interruption to code flow - be it from preempting
+tasks or ISRs.
+
+If a port implements synchronous yield, then a context switch to a
+higher-priority ready task can occur within a critical section as a result
+of a kernel API unblocking a higher-prirority task. When this occurs, the
+higher-priority task will be swapped in immediately, and its interrupt-enabled
+status applied to the CPU core. This typically causes interrupts to be
+re-enabled as a result of the context switch, which is an unintended
+side-effect for tasks that presume to have exclusive access to the CPU,
+leading to logic errors and broken assumptions.
+
+In short, any code that uses a FreeRTOS interrupt-disabled critical section
+alone to provide an interrupt-safe context is subject to port-specific behavior
+if it calls kernel APIs that can unblock tasks. A critical section alone is
+insufficient to implement InterruptSpinLock correctly.
+
+Yielding with Scheduling Suspended
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+If a task is unblocked while the scheduler is suspended, the task is moved
+to a "pending ready-list", and a flag is set to ensure that tasks are
+scheduled as necessary once the scheduler is resumed. Once scheduling
+resumes, any tasks that were unblocked while the scheduler was suspended
+are processed immediately, and rescheduling/preemption resumes at that time.
+
+In the event that a call to ``taskYIELD()`` occurs directly while the
+scheduler is suspended, the result is that ``vTaskSwitchContext()`` switches
+back to the currently running task. This is a guard-rail that short-circuits
+any attempts to bypass the scheduler-suspended state manually.
+
+Critical Section with Suspended Scheduling
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+It is important to note that a critical section may be entered while the
+scheduler is also disabled. In such a state, the system observes FreeRTOS'
+contract that threads are not re-scheduled while the scheduler is supsended,
+with the benefit that ISRs may not break the atomicity of code executing
+while the lock is held.
+
+This state is also compatible with either synchronous or asynchronous
+yield behavior:
+
+- In the synchronous cases, the result of a call to yield is that
+ ``vTaskSwitchContext`` is invoked immediately, with the current task being
+ restored.
+- In the Asynchronous case, the result of a call to yield is that the context
+ switch interrupt is deferred until the end of the critical section.
+
+This is sufficient to satisfy the requirements implement an InterruptSpinLock
+for any FreeRTOS target.
--------------------
Signaling Primitives
@@ -124,6 +235,12 @@ notifications and clears the notification state before returning. This exact
mechanism is used by FreeRTOS internally for their Stream and Message Buffer
implementations.
+One other thing to note is that FreeRTOS has undocumented side effects between
+``vTaskSuspend`` and ``xTaskNotifyWait``. If a thread is suspended via
+``vTaskSuspend`` while blocked on ``xTaskNotifyWait``, the wait is aborted
+regardless of the timeout (even if the request was indefinite) and the thread
+is resumed whenever ``vTaskResume`` is invoked.
+
BinarySemaphore
===============
The FreeRTOS backend for the BinarySemaphore uses ``StaticSemaphore_t`` as the
diff --git a/pw_sync_freertos/interrupt_spin_lock.cc b/pw_sync_freertos/interrupt_spin_lock.cc
index a82b602f7..96c822b9d 100644
--- a/pw_sync_freertos/interrupt_spin_lock.cc
+++ b/pw_sync_freertos/interrupt_spin_lock.cc
@@ -20,10 +20,26 @@
namespace pw::sync {
+#if (INCLUDE_xTaskGetSchedulerState != 1) && (configUSE_TIMERS != 1)
+#error "xTaskGetSchedulerState is required for pw::sync::InterruptSpinLock"
+#endif
+
void InterruptSpinLock::lock() {
if (interrupt::InInterruptContext()) {
native_type_.saved_interrupt_mask = taskENTER_CRITICAL_FROM_ISR();
} else { // Task context
+ // Suspending the scheduler ensures that kernel API calls that occur
+ // within the critical section will not preempt the current task
+ // (if called from a thread context). Otherwise, kernel APIs called
+ // from within the critical section may preempt the running task if
+ // the port implements portYIELD synchronously.
+ // Note: calls to vTaskSuspendAll(), like taskENTER_CRITICAL() can
+ // be nested.
+ // Note: vTaskSuspendAll()/xTaskResumeAll() are not safe to call before the
+ // scheduler has been started.
+ if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {
+ vTaskSuspendAll();
+ }
taskENTER_CRITICAL();
}
// We can't deadlock here so crash instead.
@@ -38,6 +54,9 @@ void InterruptSpinLock::unlock() {
taskEXIT_CRITICAL_FROM_ISR(native_type_.saved_interrupt_mask);
} else { // Task context
taskEXIT_CRITICAL();
+ if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {
+ xTaskResumeAll();
+ }
}
}
diff --git a/pw_sync_freertos/public/pw_sync_freertos/thread_notification_inline.h b/pw_sync_freertos/public/pw_sync_freertos/thread_notification_inline.h
index cf45f21e0..632a3d2d3 100644
--- a/pw_sync_freertos/public/pw_sync_freertos/thread_notification_inline.h
+++ b/pw_sync_freertos/public/pw_sync_freertos/thread_notification_inline.h
@@ -13,6 +13,8 @@
// the License.
#pragma once
+#include <mutex>
+
#include "FreeRTOS.h"
#include "pw_assert/assert.h"
#include "pw_interrupt/context.h"
@@ -38,10 +40,9 @@ inline ThreadNotification::~ThreadNotification() = default;
inline bool ThreadNotification::try_acquire() {
// Enforce the pw::sync::ThreadNotification IRQ contract.
PW_DASSERT(!interrupt::InInterruptContext());
- taskENTER_CRITICAL();
+ std::lock_guard lock(native_type_.shared_spin_lock);
const bool notified = native_type_.notified;
native_type_.notified = false;
- taskEXIT_CRITICAL();
return notified;
}
diff --git a/pw_sync_freertos/public/pw_sync_freertos/thread_notification_native.h b/pw_sync_freertos/public/pw_sync_freertos/thread_notification_native.h
index 9cc0a6a52..fe6b8baa4 100644
--- a/pw_sync_freertos/public/pw_sync_freertos/thread_notification_native.h
+++ b/pw_sync_freertos/public/pw_sync_freertos/thread_notification_native.h
@@ -14,13 +14,21 @@
#pragma once
#include "FreeRTOS.h"
+#include "pw_polyfill/language_feature_macros.h"
+#include "pw_sync/interrupt_spin_lock.h"
+#include "pw_sync/lock_annotations.h"
#include "task.h"
namespace pw::sync::backend {
struct NativeThreadNotification {
- TaskHandle_t blocked_thread;
- bool notified;
+ TaskHandle_t blocked_thread PW_GUARDED_BY(shared_spin_lock);
+ bool notified PW_GUARDED_BY(shared_spin_lock);
+ // We use a global ISL for all thread notifications because these backends
+ // only support uniprocessor targets and ergo we reduce the memory cost for
+ // all ISL instances without any risk of spin contention between different
+ // instances.
+ PW_CONSTINIT inline static InterruptSpinLock shared_spin_lock = {};
};
using NativeThreadNotificationHandle = NativeThreadNotification&;
diff --git a/pw_sync_freertos/public/pw_sync_freertos/timed_thread_notification_inline.h b/pw_sync_freertos/public/pw_sync_freertos/timed_thread_notification_inline.h
index 671fe4d41..dd0823e6a 100644
--- a/pw_sync_freertos/public/pw_sync_freertos/timed_thread_notification_inline.h
+++ b/pw_sync_freertos/public/pw_sync_freertos/timed_thread_notification_inline.h
@@ -18,11 +18,12 @@
namespace pw::sync {
-inline bool TimedThreadNotification::try_acquire_until(
- chrono::SystemClock::time_point deadline) {
- // Note that if this deadline is in the future, it will get rounded up by
- // one whole tick due to how try_lock_for is implemented.
- return try_acquire_for(deadline - chrono::SystemClock::now());
+inline bool TimedThreadNotification::try_acquire_for(
+ chrono::SystemClock::duration timeout) {
+ // Because xTaskNotifyWait may spuriously return pdFALSE due to vTaskSuspend &
+ // vTaskResume, a deadline is used instead of a timeout just like FreeRTOS
+ // stream buffers.
+ return try_acquire_until(chrono::SystemClock::TimePointAfterAtLeast(timeout));
}
} // namespace pw::sync
diff --git a/pw_sync_freertos/thread_notification.cc b/pw_sync_freertos/thread_notification.cc
index 9fc5f08ff..c539864a4 100644
--- a/pw_sync_freertos/thread_notification.cc
+++ b/pw_sync_freertos/thread_notification.cc
@@ -14,6 +14,8 @@
#include "pw_sync/thread_notification.h"
+#include <mutex>
+
#include "FreeRTOS.h"
#include "pw_assert/check.h"
#include "pw_interrupt/context.h"
@@ -52,36 +54,33 @@ void ThreadNotification::acquire() {
// state in the TCB.
PW_DCHECK(xTaskNotifyStateClear(nullptr) == pdFALSE);
- taskENTER_CRITICAL();
- if (native_type_.notified) {
- native_type_.notified = false;
- taskEXIT_CRITICAL();
- return;
+ {
+ std::lock_guard lock(native_type_.shared_spin_lock);
+ if (native_type_.notified) {
+ native_type_.notified = false;
+ return;
+ }
+ // Not notified yet, set the task handle for a one-time notification.
+ native_type_.blocked_thread = xTaskGetCurrentTaskHandle();
}
- // Not notified yet, set the task handle for a one-time notification.
- native_type_.blocked_thread = xTaskGetCurrentTaskHandle();
- taskEXIT_CRITICAL();
-
-#if INCLUDE_vTaskSuspend == 1 // This means portMAX_DELAY is indefinite.
- const BaseType_t result = WaitForNotification(portMAX_DELAY);
- PW_DCHECK_UINT_EQ(result, pdTRUE);
-#else // INCLUDE_vTaskSuspend != 1
+
+ // Even if INCLUDE_vTaskSuspend == 1 and ergo portMAX_DELAY means indefinite,
+ // vTaskSuspend() can abort xTaskNotifyWait() causing it to spuriously wake up
+ // after vTaskResume() returning pdFALSE as we were not actually notified.
while (WaitForNotification(portMAX_DELAY) == pdFALSE) {
}
-#endif // INCLUDE_vTaskSuspend
- taskENTER_CRITICAL();
+ std::lock_guard lock(native_type_.shared_spin_lock);
// The task handle was cleared by the notifier.
// Note that this may hide another notification, however this is considered
// a form of notification saturation just like as if this happened before
// acquire() was invoked.
native_type_.notified = false;
- taskEXIT_CRITICAL();
}
void ThreadNotification::release() {
if (!interrupt::InInterruptContext()) { // Task context
- taskENTER_CRITICAL();
+ std::lock_guard lock(native_type_.shared_spin_lock);
if (native_type_.blocked_thread != nullptr) {
#ifdef configTASK_NOTIFICATION_ARRAY_ENTRIES
xTaskNotifyIndexed(native_type_.blocked_thread,
@@ -91,16 +90,14 @@ void ThreadNotification::release() {
#else // !configTASK_NOTIFICATION_ARRAY_ENTRIES
xTaskNotify(native_type_.blocked_thread, 0u, eNoAction);
#endif // configTASK_NOTIFICATION_ARRAY_ENTRIES
-
native_type_.blocked_thread = nullptr;
}
native_type_.notified = true;
- taskEXIT_CRITICAL();
return;
}
// Interrupt context
- const UBaseType_t saved_interrupt_mask = taskENTER_CRITICAL_FROM_ISR();
+ std::lock_guard lock(native_type_.shared_spin_lock);
if (native_type_.blocked_thread != nullptr) {
BaseType_t woke_higher_task = pdFALSE;
@@ -120,7 +117,6 @@ void ThreadNotification::release() {
portYIELD_FROM_ISR(woke_higher_task);
}
native_type_.notified = true;
- taskEXIT_CRITICAL_FROM_ISR(saved_interrupt_mask);
}
} // namespace pw::sync
diff --git a/pw_sync_freertos/thread_notification_test.cc b/pw_sync_freertos/thread_notification_test.cc
new file mode 100644
index 000000000..0e6322fe1
--- /dev/null
+++ b/pw_sync_freertos/thread_notification_test.cc
@@ -0,0 +1,123 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_sync/thread_notification.h"
+
+#include <chrono>
+
+#include "FreeRTOS.h"
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_thread/sleep.h"
+#include "pw_thread/test_threads.h"
+#include "pw_thread/thread.h"
+#include "pw_thread/thread_core.h"
+#include "task.h"
+
+namespace pw::sync::freertos {
+namespace {
+
+using pw::chrono::SystemClock;
+using pw::thread::Thread;
+
+} // namespace
+
+// These tests are targeted specifically to verify interactions between suspend
+// and being blocked on direct task notifications and how they impact usage of
+// the FreeRTOS optimized ThreadNotification backend.
+#if INCLUDE_vTaskSuspend == 1
+
+class NotificationAcquirer : public thread::ThreadCore {
+ public:
+ void WaitUntilRunning() { started_notification_.acquire(); }
+ void Release() { unblock_notification_.release(); }
+ void WaitUntilFinished() { finished_notification_.acquire(); }
+ std::optional<SystemClock::time_point> notified_time() const {
+ return notified_time_;
+ }
+ TaskHandle_t task_handle() const { return task_handle_; }
+
+ private:
+ void Run() final {
+ task_handle_ = xTaskGetCurrentTaskHandle();
+ started_notification_.release();
+ unblock_notification_.acquire();
+ notified_time_ = SystemClock::now();
+ finished_notification_.release();
+ }
+
+ TaskHandle_t task_handle_;
+ ThreadNotification started_notification_;
+ ThreadNotification unblock_notification_;
+ ThreadNotification finished_notification_;
+ std::optional<SystemClock::time_point> notified_time_;
+};
+
+TEST(ThreadNotification, AcquireWithoutSuspend) {
+ NotificationAcquirer notification_acquirer;
+ Thread thread =
+ Thread(thread::test::TestOptionsThread0(), notification_acquirer);
+
+ notification_acquirer.WaitUntilRunning();
+ // At this point the thread is blocked and waiting on the notification.
+ const SystemClock::time_point release_time = SystemClock::now();
+ notification_acquirer.Release();
+ notification_acquirer.WaitUntilFinished();
+ ASSERT_TRUE(notification_acquirer.notified_time().has_value());
+ EXPECT_GE(notification_acquirer.notified_time().value(), release_time);
+
+ // Clean up the test thread context.
+#if PW_THREAD_JOINING_ENABLED
+ thread.join();
+#else
+ thread.detach();
+ thread::test::WaitUntilDetachedThreadsCleanedUp();
+#endif // PW_THREAD_JOINING_ENABLED
+}
+
+TEST(ThreadNotification, AcquireWithSuspend) {
+ NotificationAcquirer notification_acquirer;
+ Thread thread =
+ Thread(thread::test::TestOptionsThread0(), notification_acquirer);
+
+ notification_acquirer.WaitUntilRunning();
+
+ // Suspend and resume the task before notifying it, which should cause the
+ // internal xTaskNotifyWait to stop blocking and return pdFALSE upon resume.
+ vTaskSuspend(notification_acquirer.task_handle());
+ vTaskResume(notification_acquirer.task_handle());
+
+ // Sleep for at least one tick to ensure the time moved forward to let us
+ // observe the unblock time is in fact after resumed it.
+ this_thread::sleep_for(SystemClock::duration(1));
+
+ // At this point the thread is blocked and waiting on the notification.
+ const SystemClock::time_point release_time = SystemClock::now();
+ notification_acquirer.Release();
+ notification_acquirer.WaitUntilFinished();
+ ASSERT_TRUE(notification_acquirer.notified_time().has_value());
+ EXPECT_GE(notification_acquirer.notified_time().value(), release_time);
+
+ // Clean up the test thread context.
+#if PW_THREAD_JOINING_ENABLED
+ thread.join();
+#else
+ thread.detach();
+ thread::test::WaitUntilDetachedThreadsCleanedUp();
+#endif // PW_THREAD_JOINING_ENABLED
+}
+
+#endif // INCLUDE_vTaskSuspend == 1
+
+} // namespace pw::sync::freertos
diff --git a/pw_sync_freertos/timed_thread_notification.cc b/pw_sync_freertos/timed_thread_notification.cc
index e6d82bb22..07625dec6 100644
--- a/pw_sync_freertos/timed_thread_notification.cc
+++ b/pw_sync_freertos/timed_thread_notification.cc
@@ -14,6 +14,8 @@
#include "pw_sync/timed_thread_notification.h"
+#include <algorithm>
+
#include "FreeRTOS.h"
#include "pw_assert/check.h"
#include "pw_chrono/system_clock.h"
@@ -45,7 +47,8 @@ BaseType_t WaitForNotification(TickType_t xTicksToWait) {
} // namespace
-bool TimedThreadNotification::try_acquire_for(SystemClock::duration timeout) {
+bool TimedThreadNotification::try_acquire_until(
+ const SystemClock::time_point deadline) {
// Enforce the pw::sync::TImedThreadNotification IRQ contract.
PW_DCHECK(!interrupt::InInterruptContext());
@@ -56,69 +59,48 @@ bool TimedThreadNotification::try_acquire_for(SystemClock::duration timeout) {
// state in the TCB.
PW_DCHECK(xTaskNotifyStateClear(nullptr) == pdFALSE);
- taskENTER_CRITICAL();
{
+ std::lock_guard lock(native_handle().shared_spin_lock);
const bool notified = native_handle().notified;
- // Don't block for negative or zero length durations.
- if (notified || (timeout <= SystemClock::duration::zero())) {
+ // Don't block if we've already reached the specified deadline time.
+ if (notified || (SystemClock::now() >= deadline)) {
native_handle().notified = false;
- taskEXIT_CRITICAL();
return notified;
}
// Not notified yet, set the task handle for a one-time notification.
native_handle().blocked_thread = xTaskGetCurrentTaskHandle();
}
- taskEXIT_CRITICAL();
- const bool notified = [&]() {
- // In case the timeout is too long for us to express through the native
- // FreeRTOS API, we repeatedly wait with shorter durations. Note that on a
- // tick based kernel we cannot tell how far along we are on the current
- // tick, ergo we add one whole tick to the final duration. However, this
- // also means that the loop must ensure that timeout + 1 is less than the
- // max timeout.
- constexpr SystemClock::duration kMaxTimeoutMinusOne =
- pw::chrono::freertos::kMaxTimeout - SystemClock::duration(1);
- // In case the timeout is too long for us to express through the native
- // FreeRTOS API, we repeatedly wait with shorter durations.
- while (timeout > kMaxTimeoutMinusOne) {
- if (WaitForNotification(
- static_cast<TickType_t>(kMaxTimeoutMinusOne.count())) == pdTRUE) {
- return true;
- }
- timeout -= kMaxTimeoutMinusOne;
+ // xTaskNotifyWait may spuriously return pdFALSE due to vTaskSuspend &
+ // vTaskResume. Ergo, loop until we have been notified or the specified
+ // deadline time has been reached (whichever comes first).
+ for (SystemClock::time_point now = SystemClock::now(); now < deadline;
+ now = SystemClock::now()) {
+ // Note that this must be greater than zero, due to the condition above.
+ const SystemClock::duration timeout =
+ std::min(deadline - now, pw::chrono::freertos::kMaxTimeout);
+ if (WaitForNotification(static_cast<TickType_t>(timeout.count())) ==
+ pdTRUE) {
+ break; // We were notified!
}
+ }
- // On a tick based kernel we cannot tell how far along we are on the current
- // tick, ergo we add one whole tick to the final duration.
- return WaitForNotification(static_cast<TickType_t>(timeout.count() + 1)) ==
- pdTRUE;
- }();
-
- taskENTER_CRITICAL();
- if (notified) {
- // Note that this may hide another notification, however this is considered
- // a form of notification saturation just like as if this happened before
- // acquire() was invoked.
- native_handle().notified = false;
- // The task handle and notification state were cleared by the notifier.
- } else {
- // Note that we do NOT want to clear the notified value so the next call
- // can detect the notification which came after we timed out but before this
- // critical section.
- //
- // However, we do need to clear the task handle if we weren't notified and
- // the notification state in case we were notified to ensure we can block
- // in the future.
- native_handle().blocked_thread = nullptr;
+ std::lock_guard lock(native_handle().shared_spin_lock);
+ // We need to clear the thread notification state in case we were
+ // notified after timing out but before entering this critical section.
#ifdef configTASK_NOTIFICATION_ARRAY_ENTRIES
- xTaskNotifyStateClearIndexed(
- nullptr, pw::sync::freertos::config::kThreadNotificationIndex);
+ xTaskNotifyStateClearIndexed(
+ nullptr, pw::sync::freertos::config::kThreadNotificationIndex);
#else // !configTASK_NOTIFICATION_ARRAY_ENTRIES
- xTaskNotifyStateClear(nullptr);
+ xTaskNotifyStateClear(nullptr);
#endif // configTASK_NOTIFICATION_ARRAY_ENTRIES
- }
- taskEXIT_CRITICAL();
+ // Instead of determining whether we were notified above while blocking in
+ // the loop above, we instead read it in this subsequent critical section in
+ // order to also include notifications which arrived after we timed out but
+ // before we entered this critical section.
+ const bool notified = native_handle().notified;
+ native_handle().notified = false;
+ native_handle().blocked_thread = nullptr;
return notified;
}
diff --git a/pw_sync_freertos/timed_thread_notification_test.cc b/pw_sync_freertos/timed_thread_notification_test.cc
new file mode 100644
index 000000000..0ee87c88c
--- /dev/null
+++ b/pw_sync_freertos/timed_thread_notification_test.cc
@@ -0,0 +1,125 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_sync/timed_thread_notification.h"
+
+#include <chrono>
+
+#include "FreeRTOS.h"
+#include "gtest/gtest.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_thread/sleep.h"
+#include "pw_thread/test_threads.h"
+#include "pw_thread/thread.h"
+#include "pw_thread/thread_core.h"
+#include "task.h"
+
+namespace pw::sync::freertos {
+namespace {
+
+using pw::chrono::SystemClock;
+using pw::thread::Thread;
+
+} // namespace
+
+// These tests are targeted specifically to verify interactions between suspend
+// and being blocked on direct task notifications and how they impact usage of
+// the FreeRTOS optimized TimedThreadNotification backend.
+#if INCLUDE_vTaskSuspend == 1
+
+class NotificationAcquirer : public thread::ThreadCore {
+ public:
+ void WaitUntilRunning() { started_notification_.acquire(); }
+ void Release() { unblock_notification_.release(); }
+ void WaitUntilFinished() { finished_notification_.acquire(); }
+ std::optional<SystemClock::time_point> notified_time() const {
+ return notified_time_;
+ }
+ TaskHandle_t task_handle() const { return task_handle_; }
+
+ private:
+ void Run() final {
+ task_handle_ = xTaskGetCurrentTaskHandle();
+ started_notification_.release();
+ if (unblock_notification_.try_acquire_until(
+ SystemClock::TimePointAfterAtLeast(std::chrono::hours(42)))) {
+ notified_time_ = SystemClock::now();
+ }
+ finished_notification_.release();
+ }
+
+ TaskHandle_t task_handle_;
+ TimedThreadNotification started_notification_;
+ TimedThreadNotification unblock_notification_;
+ ThreadNotification finished_notification_;
+ std::optional<SystemClock::time_point> notified_time_;
+};
+
+TEST(TimedThreadNotification, AcquireWithoutSuspend) {
+ NotificationAcquirer notification_acquirer;
+ Thread thread =
+ Thread(thread::test::TestOptionsThread0(), notification_acquirer);
+
+ notification_acquirer.WaitUntilRunning();
+ // At this point the thread is blocked and waiting on the notification.
+ const SystemClock::time_point release_time = SystemClock::now();
+ notification_acquirer.Release();
+ notification_acquirer.WaitUntilFinished();
+ ASSERT_TRUE(notification_acquirer.notified_time().has_value());
+ EXPECT_GE(notification_acquirer.notified_time().value(), release_time);
+
+ // Clean up the test thread context.
+#if PW_THREAD_JOINING_ENABLED
+ thread.join();
+#else
+ thread.detach();
+ thread::test::WaitUntilDetachedThreadsCleanedUp();
+#endif // PW_THREAD_JOINING_ENABLED
+}
+
+TEST(TimedThreadNotification, AcquireWithSuspend) {
+ NotificationAcquirer notification_acquirer;
+ Thread thread =
+ Thread(thread::test::TestOptionsThread0(), notification_acquirer);
+
+ notification_acquirer.WaitUntilRunning();
+
+ // Suspend and resume the task before notifying it, which should cause the
+ // internal xTaskNotifyWait to stop blocking and return pdFALSE upon resume.
+ vTaskSuspend(notification_acquirer.task_handle());
+ vTaskResume(notification_acquirer.task_handle());
+
+ // Sleep for at least one tick to ensure the time moved forward to let us
+ // observe the unblock time is in fact after resumed it.
+ this_thread::sleep_for(SystemClock::duration(1));
+
+ // At this point the thread is blocked and waiting on the notification.
+ const SystemClock::time_point release_time = SystemClock::now();
+ notification_acquirer.Release();
+ notification_acquirer.WaitUntilFinished();
+ ASSERT_TRUE(notification_acquirer.notified_time().has_value());
+ EXPECT_GE(notification_acquirer.notified_time().value(), release_time);
+
+ // Clean up the test thread context.
+#if PW_THREAD_JOINING_ENABLED
+ thread.join();
+#else
+ thread.detach();
+ thread::test::WaitUntilDetachedThreadsCleanedUp();
+#endif // PW_THREAD_JOINING_ENABLED
+}
+
+#endif // INCLUDE_vTaskSuspend == 1
+
+} // namespace pw::sync::freertos
diff --git a/pw_sync_stl/BUILD.bazel b/pw_sync_stl/BUILD.bazel
index e3cb3a01c..d0e62b86c 100644
--- a/pw_sync_stl/BUILD.bazel
+++ b/pw_sync_stl/BUILD.bazel
@@ -142,6 +142,31 @@ pw_cc_library(
)
pw_cc_library(
+ name = "recursive_mutex_headers",
+ hdrs = [
+ "public/pw_sync_stl/recursive_mutex_inline.h",
+ "public/pw_sync_stl/recursive_mutex_native.h",
+ "public_overrides/pw_sync_backend/recursive_mutex_inline.h",
+ "public_overrides/pw_sync_backend/recursive_mutex_native.h",
+ ],
+ includes = [
+ "public",
+ "public_overrides",
+ ],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = ["//pw_assert"],
+)
+
+pw_cc_library(
+ name = "recursive_mutex",
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":recursive_mutex_headers",
+ "//pw_sync:recursive_mutex_facade",
+ ],
+)
+
+pw_cc_library(
name = "interrupt_spin_lock_headers",
hdrs = [
"public/pw_sync_stl/interrupt_spin_lock_inline.h",
@@ -168,3 +193,36 @@ pw_cc_library(
"//pw_sync:yield_core",
],
)
+
+pw_cc_library(
+ name = "condition_variable_headers",
+ hdrs = [
+ "public/pw_sync_stl/condition_variable_inline.h",
+ "public/pw_sync_stl/condition_variable_native.h",
+ "public_overrides/pw_sync_backend/condition_variable_inline.h",
+ "public_overrides/pw_sync_backend/condition_variable_native.h",
+ ],
+ includes = [
+ "public",
+ "public_overrides",
+ ],
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+)
+
+pw_cc_library(
+ name = "condition_variable",
+ target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
+ deps = [
+ ":condition_variable_headers",
+ "//pw_sync:condition_variable_facade",
+ ],
+)
+
+# TODO(b/228998350): Figure out how to conditionally enable this test like GN
+# pw_cc_test(
+# name = "condition_variable_test",
+# deps = [
+# "//pw_sync:condition_variable_test",
+# "//pw_thread_stl:test_threads",
+# ]
+# )
diff --git a/pw_sync_stl/BUILD.gn b/pw_sync_stl/BUILD.gn
index 42aea99ec..e5b2d5d9a 100644
--- a/pw_sync_stl/BUILD.gn
+++ b/pw_sync_stl/BUILD.gn
@@ -18,6 +18,9 @@ import("$dir_pw_build/error.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_sync/backend.gni")
+import("$dir_pw_thread/backend.gni")
+import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
include_dirs = [ "public" ]
@@ -113,6 +116,24 @@ pw_source_set("timed_mutex_backend") {
deps = [ ":check_system_clock_backend" ]
}
+# This target provides the backend for pw::sync::RecursiveMutex.
+pw_source_set("recursive_mutex_backend") {
+ public_configs = [
+ ":public_include_path",
+ ":backend_config",
+ ]
+ public = [
+ "public/pw_sync_stl/recursive_mutex_inline.h",
+ "public/pw_sync_stl/recursive_mutex_native.h",
+ "public_overrides/pw_sync_backend/recursive_mutex_inline.h",
+ "public_overrides/pw_sync_backend/recursive_mutex_native.h",
+ ]
+ public_deps = [
+ "$dir_pw_sync:recursive_mutex.facade",
+ dir_pw_assert,
+ ]
+}
+
# This target provides the backend for pw::sync::InterruptSpinLock.
pw_source_set("interrupt_spin_lock") {
public_configs = [
@@ -131,6 +152,36 @@ pw_source_set("interrupt_spin_lock") {
]
}
+# This target provides the backend for pw::sync::ConditionVariable.
+pw_source_set("condition_variable_backend") {
+ allow_circular_includes_from = [ "$dir_pw_sync:condition_variable.facade" ]
+ public_configs = [
+ ":public_include_path",
+ ":backend_config",
+ ]
+ public = [
+ "public/pw_sync_stl/condition_variable_inline.h",
+ "public/pw_sync_stl/condition_variable_native.h",
+ "public_overrides/pw_sync_backend/condition_variable_inline.h",
+ "public_overrides/pw_sync_backend/condition_variable_native.h",
+ ]
+ public_deps = [ "$dir_pw_sync:condition_variable.facade" ]
+}
+
+pw_test("condition_variable_test") {
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread" &&
+ pw_sync_CONDITION_VARIABLE_BACKEND ==
+ "$dir_pw_sync_stl:condition_variable"
+ deps = [
+ "$dir_pw_sync:condition_variable_test",
+ "$dir_pw_thread_stl:test_threads",
+ ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+ tests = [ ":condition_variable_test" ]
+}
diff --git a/pw_sync_stl/CMakeLists.txt b/pw_sync_stl/CMakeLists.txt
index e2d443341..1cbbeeb9d 100644
--- a/pw_sync_stl/CMakeLists.txt
+++ b/pw_sync_stl/CMakeLists.txt
@@ -15,9 +15,7 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
# This target provides the backend for pw::sync::BinarySemaphore.
-pw_add_module_library(pw_sync_stl.binary_semaphore_backend
- IMPLEMENTS_FACADES
- pw_sync.binary_semaphore
+pw_add_library(pw_sync_stl.binary_semaphore_backend STATIC
HEADERS
public/pw_sync_stl/binary_semaphore_inline.h
public/pw_sync_stl/binary_semaphore_native.h
@@ -26,6 +24,8 @@ pw_add_module_library(pw_sync_stl.binary_semaphore_backend
PUBLIC_INCLUDES
public
public_overrides
+ PUBLIC_DEPS
+ pw_sync.binary_semaphore.facade
SOURCES
binary_semaphore.cc
PRIVATE_DEPS
@@ -33,10 +33,21 @@ pw_add_module_library(pw_sync_stl.binary_semaphore_backend
pw_chrono.system_clock
)
+pw_add_library(pw_sync_stl.condition_variable_backend INTERFACE
+ HEADERS
+ public/pw_sync_stl/condition_variable_inline.h
+ public/pw_sync_stl/condition_variable_native.h
+ public_overrides/pw_sync_backend/condition_variable_inline.h
+ public_overrides/pw_sync_backend/condition_variable_native.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_sync.condition_variable.facade
+)
+
# This target provides the backend for pw::sync::CountingSemaphore.
-pw_add_module_library(pw_sync_stl.counting_semaphore_backend
- IMPLEMENTS_FACADES
- pw_sync.counting_semaphore
+pw_add_library(pw_sync_stl.counting_semaphore_backend STATIC
HEADERS
public/pw_sync_stl/counting_semaphore_inline.h
public/pw_sync_stl/counting_semaphore_native.h
@@ -45,6 +56,8 @@ pw_add_module_library(pw_sync_stl.counting_semaphore_backend
PUBLIC_INCLUDES
public
public_overrides
+ PUBLIC_DEPS
+ pw_sync.counting_semaphore.facade
SOURCES
counting_semaphore.cc
PRIVATE_DEPS
@@ -53,9 +66,7 @@ pw_add_module_library(pw_sync_stl.counting_semaphore_backend
)
# This target provides the backend for pw::sync::Mutex.
-pw_add_module_library(pw_sync_stl.mutex_backend
- IMPLEMENTS_FACADES
- pw_sync.mutex
+pw_add_library(pw_sync_stl.mutex_backend STATIC
HEADERS
public/pw_sync_stl/mutex_inline.h
public/pw_sync_stl/mutex_native.h
@@ -64,6 +75,8 @@ pw_add_module_library(pw_sync_stl.mutex_backend
PUBLIC_INCLUDES
public
public_overrides
+ PUBLIC_DEPS
+ pw_sync.mutex.facade
SOURCES
mutex.cc
PRIVATE_DEPS
@@ -71,9 +84,7 @@ pw_add_module_library(pw_sync_stl.mutex_backend
)
# This target provides the backend for pw::sync::TimedMutex.
-pw_add_module_library(pw_sync_stl.timed_mutex_backend
- IMPLEMENTS_FACADES
- pw_sync.timed_mutex
+pw_add_library(pw_sync_stl.timed_mutex_backend INTERFACE
HEADERS
public/pw_sync_stl/timed_mutex_inline.h
public_overrides/pw_sync_backend/timed_mutex_inline.h
@@ -83,11 +94,10 @@ pw_add_module_library(pw_sync_stl.timed_mutex_backend
PUBLIC_DEPS
pw_sync.mutex
pw_chrono.system_clock
+ pw_sync.timed_mutex.facade
)
-pw_add_module_library(pw_sync_stl.interrupt_spin_lock
- IMPLEMENTS_FACADES
- pw_sync.interrupt_spin_lock
+pw_add_library(pw_sync_stl.interrupt_spin_lock INTERFACE
HEADERS
public/pw_sync_stl/interrupt_spin_lock_inline.h
public/pw_sync_stl/interrupt_spin_lock_native.h
@@ -97,5 +107,6 @@ pw_add_module_library(pw_sync_stl.interrupt_spin_lock
public
public_overrides
PUBLIC_DEPS
+ pw_sync.interrupt_spin_lock.facade
pw_sync.yield_core
)
diff --git a/pw_sync_stl/public/pw_sync_stl/condition_variable_inline.h b/pw_sync_stl/public/pw_sync_stl/condition_variable_inline.h
new file mode 100644
index 000000000..2c84902d6
--- /dev/null
+++ b/pw_sync_stl/public/pw_sync_stl/condition_variable_inline.h
@@ -0,0 +1,50 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <mutex>
+
+#include "pw_sync/condition_variable.h"
+
+namespace pw::sync {
+
+inline void ConditionVariable::notify_one() { native_type_.notify_one(); }
+
+inline void ConditionVariable::notify_all() { native_type_.notify_all(); }
+
+template <typename Predicate>
+void ConditionVariable::wait(std::unique_lock<Mutex>& lock,
+ Predicate predicate) {
+ native_type_.wait(lock, std::move(predicate));
+}
+
+template <typename Predicate>
+bool ConditionVariable::wait_for(std::unique_lock<Mutex>& lock,
+ pw::chrono::SystemClock::duration timeout,
+ Predicate predicate) {
+ return native_type_.wait_for(lock, timeout, std::move(predicate));
+}
+
+template <typename Predicate>
+bool ConditionVariable::wait_until(std::unique_lock<Mutex>& lock,
+ pw::chrono::SystemClock::time_point deadline,
+ Predicate predicate) {
+ return native_type_.wait_until(lock, deadline, std::move(predicate));
+}
+
+inline auto ConditionVariable::native_handle() -> native_handle_type {
+ return native_type_;
+}
+
+} // namespace pw::sync
diff --git a/pw_sync_stl/public/pw_sync_stl/condition_variable_native.h b/pw_sync_stl/public/pw_sync_stl/condition_variable_native.h
new file mode 100644
index 000000000..e3032a593
--- /dev/null
+++ b/pw_sync_stl/public/pw_sync_stl/condition_variable_native.h
@@ -0,0 +1,25 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <condition_variable>
+
+#include "pw_sync/condition_variable.h"
+
+namespace pw::sync::backend {
+
+using NativeConditionVariable = std::condition_variable_any;
+using NativeConditionVariableHandle = std::condition_variable_any&;
+
+} // namespace pw::sync::backend
diff --git a/pw_sync_stl/public/pw_sync_stl/recursive_mutex_inline.h b/pw_sync_stl/public/pw_sync_stl/recursive_mutex_inline.h
new file mode 100644
index 000000000..e2fa00149
--- /dev/null
+++ b/pw_sync_stl/public/pw_sync_stl/recursive_mutex_inline.h
@@ -0,0 +1,52 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <thread>
+
+#include "pw_assert/assert.h"
+
+namespace pw::sync {
+
+inline RecursiveMutex::RecursiveMutex() = default;
+
+inline RecursiveMutex::~RecursiveMutex() {
+ PW_ASSERT(native_type_.lock_count == 0u);
+}
+
+inline void RecursiveMutex::lock() {
+ native_handle().lock();
+ native_type_.lock_count += 1;
+}
+
+inline bool RecursiveMutex::try_lock() {
+ if (native_handle().try_lock()) {
+ native_type_.lock_count += 1;
+ return true;
+ }
+ return false;
+}
+
+inline void RecursiveMutex::unlock() {
+ PW_ASSERT(native_type_.lock_count > 0u);
+ native_type_.lock_count -= 1;
+ native_handle().unlock();
+}
+
+// Return a std::recursive_mutex instead of the customized class.
+inline RecursiveMutex::native_handle_type RecursiveMutex::native_handle() {
+ return native_type_.mutex;
+}
+
+} // namespace pw::sync
diff --git a/pw_sync_stl/public/pw_sync_stl/recursive_mutex_native.h b/pw_sync_stl/public/pw_sync_stl/recursive_mutex_native.h
new file mode 100644
index 000000000..07a8802fd
--- /dev/null
+++ b/pw_sync_stl/public/pw_sync_stl/recursive_mutex_native.h
@@ -0,0 +1,31 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <mutex>
+
+namespace pw::sync::backend {
+
+// The NativeRecursiveMutex class adds a flag that tracks how many times the
+// std::recursive_mutex has been locked. The C++ standard states that misusing a
+// mutex is undefined behavior, so library implementations may simply ignore
+// misuse. This ensures misuse hits a PW_ASSERT.
+struct NativeRecursiveMutex {
+ std::recursive_mutex mutex;
+ unsigned lock_count = 0;
+};
+
+using NativeRecursiveMutexHandle = std::recursive_mutex&;
+
+} // namespace pw::sync::backend
diff --git a/pw_sync_stl/public_overrides/pw_sync_backend/condition_variable_inline.h b/pw_sync_stl/public_overrides/pw_sync_backend/condition_variable_inline.h
new file mode 100644
index 000000000..c6579c629
--- /dev/null
+++ b/pw_sync_stl/public_overrides/pw_sync_backend/condition_variable_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_sync_stl/condition_variable_inline.h"
diff --git a/pw_sync_stl/public_overrides/pw_sync_backend/condition_variable_native.h b/pw_sync_stl/public_overrides/pw_sync_backend/condition_variable_native.h
new file mode 100644
index 000000000..bb31927bd
--- /dev/null
+++ b/pw_sync_stl/public_overrides/pw_sync_backend/condition_variable_native.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_sync_stl/condition_variable_native.h"
diff --git a/pw_sync_stl/public_overrides/pw_sync_backend/recursive_mutex_inline.h b/pw_sync_stl/public_overrides/pw_sync_backend/recursive_mutex_inline.h
new file mode 100644
index 000000000..8c264efe1
--- /dev/null
+++ b/pw_sync_stl/public_overrides/pw_sync_backend/recursive_mutex_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_sync_stl/recursive_mutex_inline.h"
diff --git a/pw_sync_stl/public_overrides/pw_sync_backend/recursive_mutex_native.h b/pw_sync_stl/public_overrides/pw_sync_backend/recursive_mutex_native.h
new file mode 100644
index 000000000..b30c6464c
--- /dev/null
+++ b/pw_sync_stl/public_overrides/pw_sync_backend/recursive_mutex_native.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_sync_stl/recursive_mutex_native.h"
diff --git a/pw_sync_threadx/BUILD.bazel b/pw_sync_threadx/BUILD.bazel
index cb78f9e6f..2b767d8d5 100644
--- a/pw_sync_threadx/BUILD.bazel
+++ b/pw_sync_threadx/BUILD.bazel
@@ -37,7 +37,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:threadx",
],
deps = [
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
+ # TODO(b/234876414): This should depend on ThreadX but our third parties
# currently do not have Bazel support.
"//pw_chrono:system_clock",
],
@@ -75,7 +75,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:threadx",
],
deps = [
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
+ # TODO(b/234876414): This should depend on ThreadX but our third parties
# currently do not have Bazel support.
# do not have Bazel support.
"//pw_chrono:system_clock",
@@ -114,7 +114,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:threadx",
],
deps = [
- # TODO(pwbug/317): This should depend on ThreadX but our third parties currently
+ # TODO(b/234876414): This should depend on ThreadX but our third parties currently
# do not have Bazel support.
"//pw_sync:mutex_facade",
],
@@ -145,7 +145,7 @@ pw_cc_library(
"//pw_build/constraints/rtos:threadx",
],
deps = [
- # TODO(pwbug/317): This should depend on ThreadX but our third parties currently
+ # TODO(b/234876414): This should depend on ThreadX but our third parties currently
# do not have Bazel support.
"//pw_chrono:system_clock",
"//pw_sync:timed_mutex_facade",
@@ -180,7 +180,7 @@ pw_cc_library(
"public",
"public_overrides",
],
- # TODO(pwbug/317): This should depend on ThreadX but our third parties currently
+ # TODO(b/234876414): This should depend on ThreadX but our third parties currently
# do not have Bazel support.
target_compatible_with = [
"//pw_build/constraints/rtos:threadx",
diff --git a/pw_sync_threadx/BUILD.gn b/pw_sync_threadx/BUILD.gn
index 524c47864..6932c847c 100644
--- a/pw_sync_threadx/BUILD.gn
+++ b/pw_sync_threadx/BUILD.gn
@@ -19,6 +19,7 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_chrono/backend.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_sync/backend.gni")
+import("$dir_pw_unit_test/test.gni")
config("public_include_path") {
include_dirs = [ "public" ]
@@ -162,3 +163,6 @@ pw_source_set("interrupt_spin_lock") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sync_zephyr/BUILD.gn b/pw_sync_zephyr/BUILD.gn
index 9a6699a8e..1b11f77f5 100644
--- a/pw_sync_zephyr/BUILD.gn
+++ b/pw_sync_zephyr/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sync_zephyr/CMakeLists.txt b/pw_sync_zephyr/CMakeLists.txt
index b035dc52b..ce23060ca 100644
--- a/pw_sync_zephyr/CMakeLists.txt
+++ b/pw_sync_zephyr/CMakeLists.txt
@@ -15,23 +15,38 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
if(CONFIG_PIGWEED_SYNC_MUTEX)
- pw_add_module_library(pw_sync_zephyr.mutex_backend
- IMPLEMENTS_FACADES
- pw_sync.mutex
+ pw_add_library(pw_sync_zephyr.mutex_backend INTERFACE
+ HEADERS
+ public/pw_sync_zephyr/mutex_inline.h
+ public/pw_sync_zephyr/mutex_native.h
+ public_overrides/pw_sync_backend/mutex_inline.h
+ public_overrides/pw_sync_backend/mutex_native.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_sync.mutex.facade
)
- pw_set_backend(pw_sync.mutex pw_sync_zephyr.mutex_backend)
zephyr_link_libraries(pw_sync_zephyr.mutex_backend)
zephyr_link_interface(pw_sync_zephyr.mutex_backend)
endif()
if(CONFIG_PIGWEED_SYNC_BINARY_SEMAPHORE)
- pw_add_module_library(pw_sync_zephyr.binary_semaphore_backend
- IMPLEMENTS_FACADES
- pw_sync.binary_semaphore
+ pw_add_library(pw_sync_zephyr.binary_semaphore_backend STATIC
+ HEADERS
+ public/pw_sync_zephyr/binary_semaphore_native.h
+ public/pw_sync_zephyr/binary_semaphore_inline.h
+ public_overrides/pw_sync_backend/binary_semaphore_native.h
+ public_overrides/pw_sync_backend/binary_semaphore_inline.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_sync.binary_semaphore.facade
SOURCES
binary_semaphore.cc
)
- pw_set_backend(pw_sync.binary_semaphore pw_sync_zephyr.binary_semaphore_backend)
zephyr_link_libraries(pw_sync_zephyr.binary_semaphore_backend)
zephyr_link_interface(pw_sync_zephyr.binary_semaphore_backend)
endif()
diff --git a/pw_sync_zephyr/Kconfig b/pw_sync_zephyr/Kconfig
index 2ca2e7dec..15a9ec5ae 100644
--- a/pw_sync_zephyr/Kconfig
+++ b/pw_sync_zephyr/Kconfig
@@ -12,20 +12,17 @@
# License for the specific language governing permissions and limitations under
# the License.
-menuconfig PIGWEED_SYNC
- bool "Enable Pigweed's sync module (pw_sync)"
-
-if PIGWEED_SYNC
+config PIGWEED_SYNC
+ bool
+ select PIGWEED_CHRONO_SYSTEM_CLOCK
+ select PIGWEED_PREPROCESSOR
+ select PIGWEED_INTERRUPT_CONTEXT
config PIGWEED_SYNC_MUTEX
bool "Enable Pigweed mutex library (pw_sync.mutex)"
- select PIGWEED_CHRONO_SYSTEM_CLOCK
+ select PIGWEED_SYNC
select PIGWEED_POLYFILL
- select PIGWEED_PREPROCESSOR
config PIGWEED_SYNC_BINARY_SEMAPHORE
bool "Enable Pigweed binary semaphore library (pw_sync.binary_semaphore)"
- select PIGWEED_CHRONO_SYSTEM_CLOCK
- select PIGWEED_PREPROCESSOR
-
-endif # PIGWEED_SYNC
+ select PIGWEED_SYNC
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/mutex_inline.h b/pw_sync_zephyr/public/pw_sync_zephyr/mutex_inline.h
index cbc9772f9..c11806495 100644
--- a/pw_sync_zephyr/public/pw_sync_zephyr/mutex_inline.h
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/mutex_inline.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
#include "pw_assert/assert.h"
#include "pw_interrupt/context.h"
diff --git a/pw_sync_zephyr/public/pw_sync_zephyr/mutex_native.h b/pw_sync_zephyr/public/pw_sync_zephyr/mutex_native.h
index e1099e94c..bfb604ebf 100644
--- a/pw_sync_zephyr/public/pw_sync_zephyr/mutex_native.h
+++ b/pw_sync_zephyr/public/pw_sync_zephyr/mutex_native.h
@@ -13,7 +13,7 @@
// the License.
#pragma once
-#include <kernel.h>
+#include <zephyr/kernel.h>
namespace pw::sync::backend {
diff --git a/pw_sys_io/BUILD.bazel b/pw_sys_io/BUILD.bazel
index cf1ecc3f5..fdba093be 100644
--- a/pw_sys_io/BUILD.bazel
+++ b/pw_sys_io/BUILD.bazel
@@ -27,7 +27,7 @@ pw_cc_facade(
hdrs = ["public/pw_sys_io/sys_io.h"],
includes = ["public"],
deps = [
- "//pw_span",
+ "//pw_bytes",
"//pw_status",
],
)
@@ -44,11 +44,8 @@ pw_cc_library(
pw_cc_library(
name = "pw_sys_io",
- hdrs = ["public/pw_sys_io/sys_io.h"],
deps = [
":facade",
- "//pw_span",
- "//pw_status",
"@pigweed_config//:pw_sys_io_backend",
],
)
diff --git a/pw_sys_io/BUILD.gn b/pw_sys_io/BUILD.gn
index a15e99907..0fabc26ef 100644
--- a/pw_sys_io/BUILD.gn
+++ b/pw_sys_io/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
import("backend.gni")
config("public_include_path") {
@@ -26,7 +27,10 @@ config("public_include_path") {
pw_facade("pw_sys_io") {
backend = pw_sys_io_BACKEND
public_configs = [ ":public_include_path" ]
- public_deps = [ dir_pw_status ]
+ public_deps = [
+ dir_pw_bytes,
+ dir_pw_status,
+ ]
public = [ "public/pw_sys_io/sys_io.h" ]
}
@@ -38,3 +42,6 @@ pw_source_set("default_putget_bytes") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io/CMakeLists.txt b/pw_sys_io/CMakeLists.txt
index ee69592ec..9b9003fb8 100644
--- a/pw_sys_io/CMakeLists.txt
+++ b/pw_sys_io/CMakeLists.txt
@@ -13,11 +13,23 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_sys_io/backend.cmake)
-pw_add_facade(pw_sys_io
+pw_add_facade(pw_sys_io INTERFACE
+ BACKEND
+ pw_sys_io_BACKEND
+ HEADERS
+ public/pw_sys_io/sys_io.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_bytes
+ pw_status
+)
+
+pw_add_library(pw_sys_io.default_putget_bytes STATIC
SOURCES
sys_io.cc
PUBLIC_DEPS
- pw_span
- pw_status
+ pw_sys_io.facade
)
diff --git a/pw_sys_io/backend.cmake b/pw_sys_io/backend.cmake
new file mode 100644
index 000000000..eb05a5603
--- /dev/null
+++ b/pw_sys_io/backend.cmake
@@ -0,0 +1,19 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_sys_io facade.
+pw_add_backend_variable(pw_sys_io_BACKEND)
diff --git a/pw_sys_io/public/pw_sys_io/sys_io.h b/pw_sys_io/public/pw_sys_io/sys_io.h
index f93e64aad..2e9f663ff 100644
--- a/pw_sys_io/public/pw_sys_io/sys_io.h
+++ b/pw_sys_io/public/pw_sys_io/sys_io.h
@@ -37,9 +37,9 @@
#include <cstddef>
#include <cstring>
-#include <span>
#include <string_view>
+#include "pw_bytes/span.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
@@ -84,7 +84,7 @@ Status WriteByte(std::byte b);
// are returned as part of the StatusWithSize.
StatusWithSize WriteLine(const std::string_view& s);
-// Fill a byte std::span from the sys io backend using ReadByte().
+// Fill a byte span from the sys io backend using ReadByte().
// Implemented by: Facade
//
// This function is implemented by this facade and simply uses ReadByte() to
@@ -96,9 +96,9 @@ StatusWithSize WriteLine(const std::string_view& s);
// Return status is OkStatus() if the destination span was successfully
// filled. In all cases, the number of bytes successuflly read to the
// destination span are returned as part of the StatusWithSize.
-StatusWithSize ReadBytes(std::span<std::byte> dest);
+StatusWithSize ReadBytes(ByteSpan dest);
-// Write std::span of bytes out the sys io backend using WriteByte().
+// Write span of bytes out the sys io backend using WriteByte().
// Implemented by: Facade
//
// This function is implemented by this facade and simply writes the source
@@ -110,6 +110,6 @@ StatusWithSize ReadBytes(std::span<std::byte> dest);
// Return status is OkStatus() if all the bytes from the source span were
// successfully written. In all cases, the number of bytes successfully written
// are returned as part of the StatusWithSize.
-StatusWithSize WriteBytes(std::span<const std::byte> src);
+StatusWithSize WriteBytes(ConstByteSpan src);
} // namespace pw::sys_io
diff --git a/pw_sys_io/sys_io.cc b/pw_sys_io/sys_io.cc
index 8e013c3b2..a9f42a275 100644
--- a/pw_sys_io/sys_io.cc
+++ b/pw_sys_io/sys_io.cc
@@ -16,7 +16,7 @@
namespace pw::sys_io {
-StatusWithSize ReadBytes(std::span<std::byte> dest) {
+StatusWithSize ReadBytes(ByteSpan dest) {
for (size_t i = 0; i < dest.size_bytes(); ++i) {
Status result = ReadByte(&dest[i]);
if (!result.ok()) {
@@ -26,7 +26,7 @@ StatusWithSize ReadBytes(std::span<std::byte> dest) {
return StatusWithSize(dest.size_bytes());
}
-StatusWithSize WriteBytes(std::span<const std::byte> src) {
+StatusWithSize WriteBytes(ConstByteSpan src) {
for (size_t i = 0; i < src.size_bytes(); ++i) {
Status result = WriteByte(src[i]);
if (!result.ok()) {
diff --git a/pw_sys_io_arduino/BUILD.bazel b/pw_sys_io_arduino/BUILD.bazel
index c7da849b1..dafbb9b1b 100644
--- a/pw_sys_io_arduino/BUILD.bazel
+++ b/pw_sys_io_arduino/BUILD.bazel
@@ -25,8 +25,18 @@ pw_cc_library(
name = "pw_sys_io_arduino",
srcs = ["sys_io_arduino.cc"],
hdrs = ["public/pw_sys_io_arduino/init.h"],
+ # TODO(b/259149817): Get this to build
+ tags = ["manual"],
deps = [
"//pw_preprocessor",
"//pw_sys_io",
],
)
+
+# Used in targets/arduino
+pw_cc_library(
+ name = "pw_sys_io_arduino_header",
+ hdrs = ["public/pw_sys_io_arduino/init.h"],
+ includes = ["public"],
+ visibility = ["//visibility:public"],
+)
diff --git a/pw_sys_io_arduino/BUILD.gn b/pw_sys_io_arduino/BUILD.gn
index eb4b51c9d..e29de267a 100644
--- a/pw_sys_io_arduino/BUILD.gn
+++ b/pw_sys_io_arduino/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_arduino_build/arduino.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("default_config") {
include_dirs = [ "public" ]
@@ -40,3 +41,6 @@ if (pw_arduino_build_CORE_PATH != "") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_arduino/sys_io_arduino.cc b/pw_sys_io_arduino/sys_io_arduino.cc
index 3a7780078..f704e762a 100644
--- a/pw_sys_io_arduino/sys_io_arduino.cc
+++ b/pw_sys_io_arduino/sys_io_arduino.cc
@@ -20,7 +20,18 @@
#include "pw_preprocessor/compiler.h"
#include "pw_sys_io/sys_io.h"
-extern "C" void pw_sys_io_arduino_Init() { Serial.begin(115200); }
+extern "C" void pw_sys_io_arduino_Init() {
+ // On Linux serial output may still not work if the serial monitor is not
+ // connected to ttyACM0 quickly enough after reset. This check forces the
+ // device to wait for a serial connection for up to 3 seconds.
+ //
+ // If you get no serial output, try to connect minicom on the port and then
+ // reboot the chip (reset button). If using Python miniterm, start it right
+ // after pushing the reset switch.
+ while (!Serial && millis() < 3000) {
+ }
+ Serial.begin(115200);
+}
namespace pw::sys_io {
@@ -54,14 +65,14 @@ Status WriteByte(std::byte b) {
// Writes a string using pw::sys_io, and add newline characters at the end.
StatusWithSize WriteLine(const std::string_view& s) {
size_t chars_written = 0;
- StatusWithSize result = WriteBytes(std::as_bytes(std::span(s)));
+ StatusWithSize result = WriteBytes(as_bytes(span(s)));
if (!result.ok()) {
return result;
}
chars_written += result.size();
// Write trailing newline.
- result = WriteBytes(std::as_bytes(std::span("\r\n", 2)));
+ result = WriteBytes(as_bytes(span("\r\n", 2)));
chars_written += result.size();
return StatusWithSize(result.status(), chars_written);
diff --git a/pw_sys_io_baremetal_lm3s6965evb/BUILD.gn b/pw_sys_io_baremetal_lm3s6965evb/BUILD.gn
index 1fa0ecf73..3e14bb4c2 100644
--- a/pw_sys_io_baremetal_lm3s6965evb/BUILD.gn
+++ b/pw_sys_io_baremetal_lm3s6965evb/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("default_config") {
include_dirs = [ "public" ]
@@ -34,3 +35,10 @@ pw_source_set("pw_sys_io_baremetal_lm3s6965evb") {
]
sources = [ "sys_io_baremetal.cc" ]
}
+
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_sys_io_baremetal_lm3s6965evb/docs.rst b/pw_sys_io_baremetal_lm3s6965evb/docs.rst
new file mode 100644
index 000000000..a86537373
--- /dev/null
+++ b/pw_sys_io_baremetal_lm3s6965evb/docs.rst
@@ -0,0 +1,9 @@
+.. _module-pw_sys_io_baremetal_lm3s6965evb:
+
+===============================
+pw_sys_io_baremetal_lm3s6965evb
+===============================
+
+.. warning::
+
+ This documentation is under construction.
diff --git a/pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc b/pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc
index 0964d0c9c..c6d4da8bf 100644
--- a/pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc
+++ b/pw_sys_io_baremetal_lm3s6965evb/sys_io_baremetal.cc
@@ -121,14 +121,14 @@ Status WriteByte(std::byte b) {
// Writes a string using pw::sys_io, and add newline characters at the end.
StatusWithSize WriteLine(const std::string_view& s) {
size_t chars_written = 0;
- StatusWithSize result = WriteBytes(std::as_bytes(std::span(s)));
+ StatusWithSize result = WriteBytes(as_bytes(span(s)));
if (!result.ok()) {
return result;
}
chars_written += result.size();
// Write trailing newline.
- result = WriteBytes(std::as_bytes(std::span("\r\n", 2)));
+ result = WriteBytes(as_bytes(span("\r\n", 2)));
chars_written += result.size();
return StatusWithSize(result.status(), chars_written);
diff --git a/pw_sys_io_baremetal_stm32f429/BUILD.bazel b/pw_sys_io_baremetal_stm32f429/BUILD.bazel
index d39253141..248d987de 100644
--- a/pw_sys_io_baremetal_stm32f429/BUILD.bazel
+++ b/pw_sys_io_baremetal_stm32f429/BUILD.bazel
@@ -25,13 +25,14 @@ pw_cc_library(
name = "pw_sys_io_baremetal_stm32f429",
srcs = ["sys_io_baremetal.cc"],
hdrs = ["public/pw_sys_io_baremetal_stm32f429/init.h"],
+ includes = ["public"],
target_compatible_with = [
"//pw_build/constraints/chipset:stm32f429",
"@platforms//os:none",
],
deps = [
- "//pw_boot_cortex_m:armv7m",
"//pw_preprocessor",
- "//pw_sys_io",
+ "//pw_sys_io:default_putget_bytes",
+ "//pw_sys_io:facade",
],
)
diff --git a/pw_sys_io_baremetal_stm32f429/BUILD.gn b/pw_sys_io_baremetal_stm32f429/BUILD.gn
index c9eb3b276..43850895a 100644
--- a/pw_sys_io_baremetal_stm32f429/BUILD.gn
+++ b/pw_sys_io_baremetal_stm32f429/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
config("default_config") {
include_dirs = [ "public" ]
@@ -38,3 +39,6 @@ pw_source_set("pw_sys_io_baremetal_stm32f429") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_baremetal_stm32f429/public/pw_sys_io_baremetal_stm32f429/init.h b/pw_sys_io_baremetal_stm32f429/public/pw_sys_io_baremetal_stm32f429/init.h
index cdff801ae..8b3b99047 100644
--- a/pw_sys_io_baremetal_stm32f429/public/pw_sys_io_baremetal_stm32f429/init.h
+++ b/pw_sys_io_baremetal_stm32f429/public/pw_sys_io_baremetal_stm32f429/init.h
@@ -17,7 +17,7 @@
PW_EXTERN_C_START
-// The actual implement of PreMainInit() in sys_io_BACKEND.
+// The actual implementation of PreMainInit() in sys_io_BACKEND.
void pw_sys_io_stm32f429_Init();
PW_EXTERN_C_END
diff --git a/pw_sys_io_baremetal_stm32f429/sys_io_baremetal.cc b/pw_sys_io_baremetal_stm32f429/sys_io_baremetal.cc
index 60c4727d0..7cd8fc594 100644
--- a/pw_sys_io_baremetal_stm32f429/sys_io_baremetal.cc
+++ b/pw_sys_io_baremetal_stm32f429/sys_io_baremetal.cc
@@ -90,9 +90,11 @@ constexpr uint32_t kTxRegisterEmpty = 0x1u << 7;
// to reasonable values and we don't need to change them.
constexpr uint32_t kReceiveEnable = 0x1 << 2;
constexpr uint32_t kTransmitEnable = 0x1 << 3;
-constexpr uint32_t kReadDataReady = 0x1u << 5;
constexpr uint32_t kEnableUsart = 0x1 << 13;
+// USART configuration flags for status register.
+constexpr uint32_t kReadDataReady = 0x1u << 5;
+
// Layout of memory mapped registers for USART blocks.
PW_PACKED(struct) UsartBlock {
uint32_t status;
@@ -132,24 +134,30 @@ volatile UsartBlock& usart1 =
extern "C" void pw_sys_io_stm32f429_Init() {
// Enable 'A' GIPO clocks.
- platform_rcc.ahb1_config |= kGpioAEnable;
+ platform_rcc.ahb1_config = platform_rcc.ahb1_config | kGpioAEnable;
// Enable Uart TX pin.
// Output type defaults to push-pull (rather than open/drain).
- gpio_a.modes |= kGpioPortModeAlternate << kGpio9PortModePos;
- gpio_a.out_speed |= kGpioSpeedVeryHigh << kGpio9PortSpeedPos;
- gpio_a.pull_up_down |= kPullTypePullUp << kGpio9PullTypePos;
- gpio_a.alt_high |= kGpioAlternateFunctionUsart1 << kGpio9AltModeHighPos;
+ gpio_a.modes = gpio_a.modes | (kGpioPortModeAlternate << kGpio9PortModePos);
+ gpio_a.out_speed =
+ gpio_a.out_speed | (kGpioSpeedVeryHigh << kGpio9PortSpeedPos);
+ gpio_a.pull_up_down =
+ gpio_a.pull_up_down | (kPullTypePullUp << kGpio9PullTypePos);
+ gpio_a.alt_high =
+ gpio_a.alt_high | (kGpioAlternateFunctionUsart1 << kGpio9AltModeHighPos);
// Enable Uart RX pin.
// Output type defaults to push-pull (rather than open/drain).
- gpio_a.modes |= kGpioPortModeAlternate << kGpio10PortModePos;
- gpio_a.out_speed |= kGpioSpeedVeryHigh << kGpio10PortSpeedPos;
- gpio_a.pull_up_down |= kPullTypePullUp << kGpio10PullTypePos;
- gpio_a.alt_high |= kGpioAlternateFunctionUsart1 << kGpio10AltModeHighPos;
+ gpio_a.modes = gpio_a.modes | (kGpioPortModeAlternate << kGpio10PortModePos);
+ gpio_a.out_speed =
+ gpio_a.out_speed | (kGpioSpeedVeryHigh << kGpio10PortSpeedPos);
+ gpio_a.pull_up_down =
+ gpio_a.pull_up_down | (kPullTypePullUp << kGpio10PullTypePos);
+ gpio_a.alt_high =
+ gpio_a.alt_high | (kGpioAlternateFunctionUsart1 << kGpio10AltModeHighPos);
// Initialize USART1. Initialized to 8N1 at the specified baud rate.
- platform_rcc.apb2_config |= kUsart1Enable;
+ platform_rcc.apb2_config = platform_rcc.apb2_config | (kUsart1Enable);
// Warning: Normally the baud rate register calculation is based off
// peripheral 2 clock. For this code, the peripheral clock defaults to
@@ -199,14 +207,14 @@ Status WriteByte(std::byte b) {
// Writes a string using pw::sys_io, and add newline characters at the end.
StatusWithSize WriteLine(const std::string_view& s) {
size_t chars_written = 0;
- StatusWithSize result = WriteBytes(std::as_bytes(std::span(s)));
+ StatusWithSize result = WriteBytes(as_bytes(span(s)));
if (!result.ok()) {
return result;
}
chars_written += result.size();
// Write trailing newline.
- result = WriteBytes(std::as_bytes(std::span("\r\n", 2)));
+ result = WriteBytes(as_bytes(span("\r\n", 2)));
chars_written += result.size();
return StatusWithSize(result.status(), chars_written);
diff --git a/pw_sys_io_emcraft_sf2/BUILD.gn b/pw_sys_io_emcraft_sf2/BUILD.gn
index 236722f70..a9b29b0a8 100644
--- a/pw_sys_io_emcraft_sf2/BUILD.gn
+++ b/pw_sys_io_emcraft_sf2/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_third_party/smartfusion_mss/mss.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# The build target that overrides the default configuration options for this
@@ -58,3 +59,6 @@ pw_source_set("pw_sys_io_emcraft_sf2") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_emcraft_sf2/OWNERS b/pw_sys_io_emcraft_sf2/OWNERS
new file mode 100644
index 000000000..8c230af99
--- /dev/null
+++ b/pw_sys_io_emcraft_sf2/OWNERS
@@ -0,0 +1 @@
+skeys@google.com
diff --git a/pw_sys_io_emcraft_sf2/sys_io_emcraft_sf2.cc b/pw_sys_io_emcraft_sf2/sys_io_emcraft_sf2.cc
index d94a621b5..0dd51b632 100644
--- a/pw_sys_io_emcraft_sf2/sys_io_emcraft_sf2.cc
+++ b/pw_sys_io_emcraft_sf2/sys_io_emcraft_sf2.cc
@@ -88,14 +88,14 @@ Status WriteByte(std::byte b) {
// Writes a string using pw::sys_io, and add newline characters at the end.
StatusWithSize WriteLine(const std::string_view& s) {
size_t chars_written = 0;
- StatusWithSize result = WriteBytes(std::as_bytes(std::span(s)));
+ StatusWithSize result = WriteBytes(as_bytes(span(s)));
if (!result.ok()) {
return result;
}
chars_written += result.size();
// Write trailing newline.
- result = WriteBytes(std::as_bytes(std::span("\r\n", 2)));
+ result = WriteBytes(as_bytes(span("\r\n", 2)));
chars_written += result.size();
return StatusWithSize(OkStatus(), chars_written);
diff --git a/pw_sys_io_mcuxpresso/BUILD.bazel b/pw_sys_io_mcuxpresso/BUILD.bazel
index a91e0952d..0e1a323bc 100644
--- a/pw_sys_io_mcuxpresso/BUILD.bazel
+++ b/pw_sys_io_mcuxpresso/BUILD.bazel
@@ -25,6 +25,8 @@ pw_cc_library(
name = "pw_sys_io_mcuxpresso",
srcs = ["sys_io.cc"],
hdrs = ["public/pw_sys_io_mcuxpresso/init.h"],
+ # TODO(b/259150983): Get this to build, requires SDK
+ tags = ["manual"],
deps = [
"//pw_preprocessor",
"//pw_sys_io",
diff --git a/pw_sys_io_mcuxpresso/BUILD.gn b/pw_sys_io_mcuxpresso/BUILD.gn
index 8c0671754..7e8ccff82 100644
--- a/pw_sys_io_mcuxpresso/BUILD.gn
+++ b/pw_sys_io_mcuxpresso/BUILD.gn
@@ -17,6 +17,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_third_party/mcuxpresso/mcuxpresso.gni")
+import("$dir_pw_unit_test/test.gni")
config("default_config") {
include_dirs = [ "public" ]
@@ -42,3 +43,6 @@ if (pw_third_party_mcuxpresso_SDK != "") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_mcuxpresso/sys_io.cc b/pw_sys_io_mcuxpresso/sys_io.cc
index 62b2c240d..c6486da69 100644
--- a/pw_sys_io_mcuxpresso/sys_io.cc
+++ b/pw_sys_io_mcuxpresso/sys_io.cc
@@ -64,14 +64,14 @@ Status WriteByte(std::byte b) {
// Writes a string using pw::sys_io, and add newline characters at the end.
StatusWithSize WriteLine(const std::string_view& s) {
size_t chars_written = 0;
- StatusWithSize result = WriteBytes(std::as_bytes(std::span(s)));
+ StatusWithSize result = WriteBytes(as_bytes(span(s)));
if (!result.ok()) {
return result;
}
chars_written += result.size();
// Write trailing newline.
- result = WriteBytes(std::as_bytes(std::span("\r\n", 2)));
+ result = WriteBytes(as_bytes(span("\r\n", 2)));
chars_written += result.size();
return StatusWithSize(result.status(), chars_written);
diff --git a/pw_sys_io_pico/BUILD.bazel b/pw_sys_io_pico/BUILD.bazel
new file mode 100644
index 000000000..91d4242aa
--- /dev/null
+++ b/pw_sys_io_pico/BUILD.bazel
@@ -0,0 +1,35 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "pw_sys_io_pico",
+ srcs = [
+ "sys_io.cc",
+ ],
+ # TODO(b/261603269): Get this to build.
+ tags = ["manual"],
+ deps = [
+ "//pw_status",
+ "//pw_sys_io",
+ ],
+)
diff --git a/pw_sys_io_pico/BUILD.gn b/pw_sys_io_pico/BUILD.gn
new file mode 100644
index 000000000..943392af3
--- /dev/null
+++ b/pw_sys_io_pico/BUILD.gn
@@ -0,0 +1,38 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pigweed/build_overrides/pi_pico.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_source_set("pw_sys_io_pico") {
+ sources = [ "sys_io.cc" ]
+ deps = [
+ "$PICO_ROOT/src/common/pico_base",
+ "$PICO_ROOT/src/common/pico_stdlib",
+ "$dir_pw_status",
+ "$dir_pw_sys_io:default_putget_bytes",
+ "$dir_pw_sys_io:facade",
+ ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_pico/OWNERS b/pw_sys_io_pico/OWNERS
new file mode 100644
index 000000000..307b1deb5
--- /dev/null
+++ b/pw_sys_io_pico/OWNERS
@@ -0,0 +1 @@
+amontanez@google.com
diff --git a/pw_sys_io_pico/docs.rst b/pw_sys_io_pico/docs.rst
new file mode 100644
index 000000000..3bcdfc2fc
--- /dev/null
+++ b/pw_sys_io_pico/docs.rst
@@ -0,0 +1,8 @@
+.. _module-pw_sys_io_pico:
+
+--------------
+pw_sys_io_pico
+--------------
+
+``pw_sys_io_pico`` implements the ``pw_sys_io`` facade using the Pico's STDIO
+library.
diff --git a/pw_sys_io_pico/sys_io.cc b/pw_sys_io_pico/sys_io.cc
new file mode 100644
index 000000000..da0ecf652
--- /dev/null
+++ b/pw_sys_io_pico/sys_io.cc
@@ -0,0 +1,89 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_sys_io/sys_io.h"
+
+#include <cinttypes>
+
+#include "pico/stdlib.h"
+#include "pw_status/status.h"
+
+namespace {
+
+void LazyInitSysIo() {
+ static bool initialized = false;
+ if (!initialized) {
+ stdio_init_all();
+ initialized = true;
+ }
+}
+
+// Spin until host connects.
+void WaitForConnect() {
+ while (!stdio_usb_connected()) {
+ sleep_ms(50);
+ }
+}
+
+} // namespace
+
+// This whole implementation is very inefficient because it only reads / writes
+// 1 byte at a time. It also does lazy initialization checks with every byte.
+namespace pw::sys_io {
+
+Status ReadByte(std::byte* dest) {
+ LazyInitSysIo();
+ WaitForConnect();
+ int c = PICO_ERROR_TIMEOUT;
+ while (c == PICO_ERROR_TIMEOUT) {
+ c = getchar_timeout_us(0);
+ }
+ *dest = static_cast<std::byte>(c);
+ return OkStatus();
+}
+
+Status TryReadByte(std::byte* dest) {
+ LazyInitSysIo();
+ int c = getchar_timeout_us(0);
+ if (c == PICO_ERROR_TIMEOUT) {
+ return Status::DeadlineExceeded();
+ }
+ *dest = static_cast<std::byte>(c);
+ return OkStatus();
+}
+
+Status WriteByte(std::byte b) {
+ // The return value of this is just the character sent.
+ LazyInitSysIo();
+ putchar_raw(static_cast<int>(b));
+ return OkStatus();
+}
+
+// Writes a string using pw::sys_io, and add newline characters at the end.
+StatusWithSize WriteLine(const std::string_view& s) {
+ size_t chars_written = 0;
+ StatusWithSize result = WriteBytes(as_bytes(span(s)));
+ if (!result.ok()) {
+ return result;
+ }
+ chars_written += result.size();
+
+ // Write trailing newline.
+ result = WriteBytes(as_bytes(span("\r\n", 2)));
+ chars_written += result.size();
+
+ return StatusWithSize(OkStatus(), chars_written);
+}
+
+} // namespace pw::sys_io
diff --git a/pw_sys_io_stdio/BUILD.gn b/pw_sys_io_stdio/BUILD.gn
index 2a7c57845..8de56e32d 100644
--- a/pw_sys_io_stdio/BUILD.gn
+++ b/pw_sys_io_stdio/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_source_set("pw_sys_io_stdio") {
deps = [
@@ -28,3 +29,6 @@ pw_source_set("pw_sys_io_stdio") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_stdio/CMakeLists.txt b/pw_sys_io_stdio/CMakeLists.txt
index 1493cf762..100d393c5 100644
--- a/pw_sys_io_stdio/CMakeLists.txt
+++ b/pw_sys_io_stdio/CMakeLists.txt
@@ -14,7 +14,10 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_auto_add_simple_module(pw_sys_io_stdio
- IMPLEMENTS_FACADE
- pw_sys_io
+pw_add_library(pw_sys_io_stdio STATIC
+ SOURCES
+ sys_io.cc
+ PRIVATE_DEPS
+ pw_sys_io.default_putget_bytes
+ pw_sys_io.facade
)
diff --git a/pw_sys_io_stdio/sys_io.cc b/pw_sys_io_stdio/sys_io.cc
index 333b18f8f..cedab3c83 100644
--- a/pw_sys_io_stdio/sys_io.cc
+++ b/pw_sys_io_stdio/sys_io.cc
@@ -45,7 +45,7 @@ Status WriteByte(std::byte b) {
StatusWithSize WriteLine(const std::string_view& s) {
size_t chars_written = 0;
- StatusWithSize size_result = WriteBytes(std::as_bytes(std::span(s)));
+ StatusWithSize size_result = WriteBytes(as_bytes(span(s)));
if (!size_result.ok()) {
return size_result;
}
diff --git a/pw_sys_io_stm32cube/BUILD.bazel b/pw_sys_io_stm32cube/BUILD.bazel
index 1146a5ef5..39c863ddd 100644
--- a/pw_sys_io_stm32cube/BUILD.bazel
+++ b/pw_sys_io_stm32cube/BUILD.bazel
@@ -28,10 +28,12 @@ pw_cc_library(
"sys_io.cc",
],
hdrs = ["public/pw_sys_io_stm32cube/init.h"],
+ # TODO(b/259151566): Get this to build.
+ tags = ["manual"],
deps = [
"//pw_preprocessor",
"//pw_status",
- "//pw_sys_io",
+ "//pw_sys_io:facade",
"//third_party/stm32cube",
],
)
diff --git a/pw_sys_io_stm32cube/BUILD.gn b/pw_sys_io_stm32cube/BUILD.gn
index edf540a50..a24d83bd3 100644
--- a/pw_sys_io_stm32cube/BUILD.gn
+++ b/pw_sys_io_stm32cube/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_third_party/stm32cube/stm32cube.gni")
+import("$dir_pw_unit_test/test.gni")
declare_args() {
# The build target that overrides the default configuration options for this
@@ -57,3 +58,6 @@ pw_source_set("pw_sys_io_stm32cube") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_stm32cube/docs.rst b/pw_sys_io_stm32cube/docs.rst
index cd0fdbeb8..e4155d6a0 100644
--- a/pw_sys_io_stm32cube/docs.rst
+++ b/pw_sys_io_stm32cube/docs.rst
@@ -38,7 +38,15 @@ more details.
The port that the USART peripheral TX/RX pins are on. (e.g. to use A9/A10
pins for TX and RX, respectively, set this to A)
- This defaults to 64 Bytes.
+.. c:macro:: PW_SYS_IO_STM32CUBE_GPIO_TX_PORT
+
+ The port for the USART peripheral TX pin, if different from
+ PW_SYS_IO_STM32CUBE_GPIO_PORT.
+
+.. c:macro:: PW_SYS_IO_STM32CUBE_GPIO_RX_PORT
+
+ The port for the USART peripheral RX pin, if different from
+ PW_SYS_IO_STM32CUBE_GPIO_PORT.
.. c:macro:: PW_SYS_IO_STM32CUBE_GPIO_TX_PIN
@@ -50,6 +58,16 @@ more details.
The pin index to use for USART reception within the port set by
``PW_SYS_IO_STM32CUBE_GPIO_PORT``.
+.. c:macro:: PW_SYS_IO_STM32CUBE_GPIO_AF
+
+ The alternate function index to use for USART reception within the port set by
+ ``PW_SYS_IO_STM32CUBE_GPIO_PORT``.
+
+.. c:macro:: PW_SYS_IO_STM32CUBE_USART_PREFIX
+
+ The peripheral name prefix (either UART or USART) for the peripheral selected
+ by ``PW_SYS_IO_STM32CUBE_USART_NUM``. Defaults to USART.
+
Module usage
============
After building an executable that utilizes this backend, flash the
diff --git a/pw_sys_io_stm32cube/pw_sys_io_stm32cube_private/config.h b/pw_sys_io_stm32cube/pw_sys_io_stm32cube_private/config.h
index a48d4812a..9e21221b1 100644
--- a/pw_sys_io_stm32cube/pw_sys_io_stm32cube_private/config.h
+++ b/pw_sys_io_stm32cube/pw_sys_io_stm32cube_private/config.h
@@ -27,6 +27,15 @@
#define PW_SYS_IO_STM32CUBE_GPIO_PORT A
#endif // PW_SYS_IO_STM32CUBE_GPIO_PORT
+// The ports the USART peripheral TX and RX pins are on (if different ports).
+#ifndef PW_SYS_IO_STM32CUBE_GPIO_TX_PORT
+#define PW_SYS_IO_STM32CUBE_GPIO_TX_PORT PW_SYS_IO_STM32CUBE_GPIO_PORT
+#endif // PW_SYS_IO_STM32CUBE_GPIO_TX_PORT
+
+#ifndef PW_SYS_IO_STM32CUBE_GPIO_RX_PORT
+#define PW_SYS_IO_STM32CUBE_GPIO_RX_PORT PW_SYS_IO_STM32CUBE_GPIO_PORT
+#endif // PW_SYS_IO_STM32CUBE_GPIO_RX_PORT
+
// The pin index to use for USART transmission within the port set by
// PW_SYS_IO_STM32CUBE_GPIO_PORT.
#ifndef PW_SYS_IO_STM32CUBE_GPIO_TX_PIN
@@ -38,3 +47,13 @@
#ifndef PW_SYS_IO_STM32CUBE_GPIO_RX_PIN
#define PW_SYS_IO_STM32CUBE_GPIO_RX_PIN 10
#endif // PW_SYS_IO_STM32CUBE_GPIO_RX_PIN
+
+// The Alternate Function to use for configuring USART pins.
+#ifndef PW_SYS_IO_STM32CUBE_GPIO_AF
+#define PW_SYS_IO_STM32CUBE_GPIO_AF 7
+#endif // PW_SYS_IO_STM32CUBE_GPIO_AF
+
+// The type of this peripheral. "USART" or "UART".
+#ifndef PW_SYS_IO_STM32CUBE_USART_PREFIX
+#define PW_SYS_IO_STM32CUBE_USART_PREFIX USART
+#endif // PW_SYS_IO_STM32CUBE_GPIO_AF
diff --git a/pw_sys_io_stm32cube/sys_io.cc b/pw_sys_io_stm32cube/sys_io.cc
index e9e6d7e73..bd87ffb2d 100644
--- a/pw_sys_io_stm32cube/sys_io.cc
+++ b/pw_sys_io_stm32cube/sys_io.cc
@@ -24,28 +24,40 @@
// These macros remap config options to the various STM32Cube HAL macro names.
// USART_INSTANCE defined to USARTn, where n is the USART peripheral index.
-#define USART_INSTANCE PW_CONCAT(USART, PW_SYS_IO_STM32CUBE_USART_NUM)
-
-// USART_GPIO_ALTERNATE_FUNC defined to GPIO_AF7_USARTn, where n is the USART
-// peripheral index.
-#define USART_GPIO_ALTERNATE_FUNC \
- PW_CONCAT(GPIO_AF7_USART, PW_SYS_IO_STM32CUBE_USART_NUM)
+#define USART_INSTANCE \
+ PW_CONCAT(PW_SYS_IO_STM32CUBE_USART_PREFIX, PW_SYS_IO_STM32CUBE_USART_NUM)
+
+// USART_GPIO_ALTERNATE_FUNC defined to GPIO_AFm_USARTn, where m is the
+// alternate function index and n is the USART peripheral index.
+#define USART_GPIO_ALTERNATE_FUNC \
+ PW_CONCAT(GPIO_AF, \
+ PW_SYS_IO_STM32CUBE_GPIO_AF, \
+ _, \
+ PW_SYS_IO_STM32CUBE_USART_PREFIX, \
+ PW_SYS_IO_STM32CUBE_USART_NUM)
// USART_GPIO_PORT defined to GPIOx, where x is the GPIO port letter that the
// TX/RX pins are on.
-#define USART_GPIO_PORT PW_CONCAT(GPIO, PW_SYS_IO_STM32CUBE_GPIO_PORT)
+#define USART_GPIO_TX_PORT PW_CONCAT(GPIO, PW_SYS_IO_STM32CUBE_GPIO_TX_PORT)
+#define USART_GPIO_RX_PORT PW_CONCAT(GPIO, PW_SYS_IO_STM32CUBE_GPIO_RX_PORT)
#define USART_GPIO_TX_PIN PW_CONCAT(GPIO_PIN_, PW_SYS_IO_STM32CUBE_GPIO_TX_PIN)
#define USART_GPIO_RX_PIN PW_CONCAT(GPIO_PIN_, PW_SYS_IO_STM32CUBE_GPIO_RX_PIN)
// USART_GPIO_PORT_ENABLE defined to __HAL_RCC_GPIOx_CLK_ENABLE, where x is the
// GPIO port letter that the TX/RX pins are on.
-#define USART_GPIO_PORT_ENABLE \
- PW_CONCAT(__HAL_RCC_GPIO, PW_SYS_IO_STM32CUBE_GPIO_PORT, _CLK_ENABLE)
+#define USART_GPIO_TX_PORT_ENABLE \
+ PW_CONCAT(__HAL_RCC_GPIO, PW_SYS_IO_STM32CUBE_GPIO_TX_PORT, _CLK_ENABLE)
+
+#define USART_GPIO_RX_PORT_ENABLE \
+ PW_CONCAT(__HAL_RCC_GPIO, PW_SYS_IO_STM32CUBE_GPIO_RX_PORT, _CLK_ENABLE)
// USART_ENABLE defined to __HAL_RCC_USARTn_CLK_ENABLE, where n is the USART
// peripheral index.
-#define USART_ENABLE \
- PW_CONCAT(__HAL_RCC_USART, PW_SYS_IO_STM32CUBE_USART_NUM, _CLK_ENABLE)
+#define USART_ENABLE \
+ PW_CONCAT(__HAL_RCC_, \
+ PW_SYS_IO_STM32CUBE_USART_PREFIX, \
+ PW_SYS_IO_STM32CUBE_USART_NUM, \
+ _CLK_ENABLE)
static UART_HandleTypeDef uart;
@@ -53,14 +65,22 @@ extern "C" void pw_sys_io_Init() {
GPIO_InitTypeDef GPIO_InitStruct = {};
USART_ENABLE();
- USART_GPIO_PORT_ENABLE();
+ USART_GPIO_TX_PORT_ENABLE();
+ USART_GPIO_RX_PORT_ENABLE();
+
+ GPIO_InitStruct.Pin = USART_GPIO_TX_PIN;
+ GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
+ GPIO_InitStruct.Pull = GPIO_NOPULL;
+ GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
+ GPIO_InitStruct.Alternate = USART_GPIO_ALTERNATE_FUNC;
+ HAL_GPIO_Init(USART_GPIO_TX_PORT, &GPIO_InitStruct);
- GPIO_InitStruct.Pin = USART_GPIO_TX_PIN | USART_GPIO_RX_PIN;
+ GPIO_InitStruct.Pin = USART_GPIO_RX_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = USART_GPIO_ALTERNATE_FUNC;
- HAL_GPIO_Init(USART_GPIO_PORT, &GPIO_InitStruct);
+ HAL_GPIO_Init(USART_GPIO_RX_PORT, &GPIO_InitStruct);
uart.Instance = USART_INSTANCE;
uart.Init.BaudRate = 115200;
@@ -98,14 +118,14 @@ Status WriteByte(std::byte b) {
// Writes a string using pw::sys_io, and add newline characters at the end.
StatusWithSize WriteLine(const std::string_view& s) {
size_t chars_written = 0;
- StatusWithSize result = WriteBytes(std::as_bytes(std::span(s)));
+ StatusWithSize result = WriteBytes(as_bytes(span(s)));
if (!result.ok()) {
return result;
}
chars_written += result.size();
// Write trailing newline.
- result = WriteBytes(std::as_bytes(std::span("\r\n", 2)));
+ result = WriteBytes(as_bytes(span("\r\n", 2)));
chars_written += result.size();
return StatusWithSize(OkStatus(), chars_written);
diff --git a/pw_sys_io_zephyr/BUILD.gn b/pw_sys_io_zephyr/BUILD.gn
index 9a6699a8e..1b11f77f5 100644
--- a/pw_sys_io_zephyr/BUILD.gn
+++ b/pw_sys_io_zephyr/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_sys_io_zephyr/CMakeLists.txt b/pw_sys_io_zephyr/CMakeLists.txt
index 4d1955f67..3a103168d 100644
--- a/pw_sys_io_zephyr/CMakeLists.txt
+++ b/pw_sys_io_zephyr/CMakeLists.txt
@@ -18,11 +18,12 @@ if(NOT CONFIG_PIGWEED_SYS_IO)
return()
endif()
-pw_auto_add_simple_module(pw_sys_io_zephyr
- IMPLEMENTS_FACADE
- pw_sys_io
- PUBLIC_DEPS
+pw_add_library(pw_sys_io_zephyr STATIC
+ SOURCES
+ sys_io.cc
+ PRIVATE_DEPS
+ pw_sys_io.default_putget_bytes
+ pw_sys_io.facade
zephyr_interface
)
-pw_set_backend(pw_sys_io pw_sys_io_zephyr)
zephyr_link_libraries(pw_sys_io_zephyr)
diff --git a/pw_sys_io_zephyr/sys_io.cc b/pw_sys_io_zephyr/sys_io.cc
index fd9255e63..6891341ac 100644
--- a/pw_sys_io_zephyr/sys_io.cc
+++ b/pw_sys_io_zephyr/sys_io.cc
@@ -14,10 +14,10 @@
#include "pw_sys_io/sys_io.h"
-#include <console/console.h>
-#include <init.h>
-#include <usb/usb_device.h>
-#include <zephyr.h>
+#include <zephyr/console/console.h>
+#include <zephyr/init.h>
+#include <zephyr/kernel.h>
+#include <zephyr/usb/usb_device.h>
static int sys_io_init(const struct device* dev) {
int err;
@@ -72,7 +72,7 @@ Status WriteByte(std::byte b) {
StatusWithSize WriteLine(const std::string_view& s) {
size_t chars_written = 0;
- StatusWithSize size_result = WriteBytes(std::as_bytes(std::span(s)));
+ StatusWithSize size_result = WriteBytes(as_bytes(span(s)));
if (!size_result.ok()) {
return size_result;
}
diff --git a/pw_system/BUILD.bazel b/pw_system/BUILD.bazel
index 0a16c5722..d9fb2e97c 100644
--- a/pw_system/BUILD.bazel
+++ b/pw_system/BUILD.bazel
@@ -22,10 +22,6 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
-# WARNING: Many of the dependencies in this file are missing and need to be
-# added/updated. This is provided as a starting point, but currently does not
-# work.
-
pw_cc_library(
name = "config",
hdrs = [
@@ -66,7 +62,9 @@ pw_cc_library(
"//pw_log:facade",
"//pw_log:proto_utils",
"//pw_log_string:handler_facade",
- "//pw_log_tokenized:metadata",
+ "//pw_log_tokenized:handler_facade",
+ "//pw_log_tokenized:headers",
+ "//pw_metric:global",
"//pw_multisink",
"//pw_result",
"//pw_string",
@@ -77,14 +75,22 @@ pw_cc_library(
)
pw_cc_library(
- name = "rpc_server",
+ name = "rpc_server_headers",
hdrs = [
"public/pw_system/rpc_server.h",
],
includes = ["public"],
deps = [
":config",
+ ],
+)
+
+pw_cc_library(
+ name = "rpc_server",
+ deps = [
+ ":config",
":hdlc_rpc_server",
+ ":rpc_server_headers",
],
)
@@ -96,7 +102,7 @@ pw_cc_library(
includes = ["public"],
deps = [
":io",
- ":rpc_server",
+ ":rpc_server_headers",
":target_io",
"//pw_assert",
"//pw_hdlc:pw_rpc",
@@ -107,6 +113,21 @@ pw_cc_library(
)
pw_cc_library(
+ name = "thread_snapshot_service",
+ srcs = [
+ "thread_snapshot_service.cc",
+ ],
+ hdrs = [
+ "public/pw_system/thread_snapshot_service.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_rpc",
+ "//pw_thread:thread_snapshot_service",
+ ],
+)
+
+pw_cc_library(
name = "io",
hdrs = [
"public/pw_system/io.h",
@@ -128,7 +149,12 @@ pw_cc_library(
deps = [
":log",
":rpc_server",
- "//pw_rpc/nanopb:echo_service",
+ ":target_hooks",
+ ":thread_snapshot_service",
+ ":work_queue",
+ "//pw_metric:global",
+ "//pw_metric:metric_service_pwpb",
+ "//pw_rpc/pwpb:echo_service",
"//pw_thread:thread",
],
)
@@ -143,6 +169,7 @@ pw_cc_library(
],
includes = ["public"],
deps = [
+ ":config",
"//pw_work_queue",
],
)
@@ -176,6 +203,17 @@ pw_cc_library(
)
pw_cc_library(
+ name = "target_hooks_headers",
+ hdrs = [
+ "public/pw_system/target_hooks.h",
+ ],
+ includes = ["public"],
+ deps = [
+ "//pw_thread:thread",
+ ],
+)
+
+pw_cc_library(
name = "target_hooks",
hdrs = [
"public/pw_system/target_hooks.h",
@@ -183,16 +221,28 @@ pw_cc_library(
includes = ["public"],
deps = [
"//pw_thread:thread",
+ "@pigweed_config//:pw_system_target_hooks_backend",
],
)
+# This isn't the best solution, but it's close enough for now. Target hooks are
+# not generically related to an OS, and should be inject-able by downstream
+# projects. For now, assume the pre-baked OS-specific hooks are good enough.
+pw_cc_library(
+ name = "target_hooks_multiplexer",
+ visibility = ["@pigweed_config//:__pkg__"],
+ deps = select({
+ "//pw_build/constraints/rtos:freertos": [":freertos_target_hooks"],
+ "//conditions:default": [":stl_target_hooks"],
+ }),
+)
+
pw_cc_library(
name = "stl_target_hooks",
srcs = [
"stl_target_hooks.cc",
],
deps = [
- "//pw_thread:sleep",
"//pw_thread:thread",
"//pw_thread_stl:thread",
],
@@ -203,10 +253,11 @@ pw_cc_library(
srcs = [
"freertos_target_hooks.cc",
],
-
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
+ ":target_hooks_headers",
"//pw_thread:thread",
"//pw_thread_freertos:thread",
],
@@ -221,5 +272,9 @@ pw_cc_binary(
":target_hooks",
"//pw_stream",
"//pw_stream:sys_io_stream",
- ],
+ "//pw_unit_test:rpc_service",
+ ] + select({
+ "//pw_build/constraints/rtos:freertos": [],
+ "//conditions:default": ["//targets/host_device_simulator:boot"],
+ }),
)
diff --git a/pw_system/BUILD.gn b/pw_system/BUILD.gn
index fd5cb5ad8..a0e84d52d 100644
--- a/pw_system/BUILD.gn
+++ b/pw_system/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pigweed/third_party/freertos/freertos.gni")
import("$dir_pigweed/third_party/nanopb/nanopb.gni")
+import("$dir_pigweed/third_party/pico_sdk/pi_pico.gni")
import("$dir_pigweed/third_party/smartfusion_mss/mss.gni")
import("$dir_pigweed/third_party/stm32cube/stm32cube.gni")
import("$dir_pw_build/error.gni")
@@ -23,6 +24,7 @@ import("$dir_pw_build/facade.gni")
import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
import("backend.gni")
declare_args() {
@@ -44,17 +46,6 @@ pw_source_set("config") {
friend = [ "./*" ]
}
-group("pw_system") {
- public_deps = [
- ":init",
- ":io",
- ":log",
- ":rpc_server",
- ":work_queue",
- ]
- deps = [ ":target_hooks" ]
-}
-
pw_source_set("log") {
public_configs = [ ":public_include_path" ]
sources = [
@@ -90,14 +81,13 @@ pw_source_set("log_backend.impl") {
"$dir_pw_log:proto_utils",
"$dir_pw_log:pw_log.facade",
"$dir_pw_log_string:handler.facade",
- "$dir_pw_log_tokenized:metadata",
+ "$dir_pw_metric:global",
"$dir_pw_multisink",
"$dir_pw_result",
"$dir_pw_string",
"$dir_pw_sync:interrupt_spin_lock",
"$dir_pw_sync:lock_annotations",
"$dir_pw_tokenizer",
- "$dir_pw_tokenizer:global_handler_with_payload.facade",
]
}
@@ -107,6 +97,7 @@ pw_facade("rpc_server") {
public_configs = [ ":public_include_path" ]
public_deps = [
":config",
+ "$dir_pw_rpc:server",
"$dir_pw_thread:thread_core",
]
}
@@ -126,8 +117,11 @@ pw_source_set("init") {
":log",
":rpc_server",
":target_hooks.facade",
+ ":thread_snapshot_service",
":work_queue",
- "$dir_pw_rpc/nanopb:echo_service",
+ "$dir_pw_metric:global",
+ "$dir_pw_metric:metric_service_pwpb",
+ "$dir_pw_rpc/pwpb:echo_service",
"$dir_pw_thread:thread",
]
}
@@ -174,6 +168,14 @@ pw_source_set("socket_target_io") {
]
}
+pw_source_set("thread_snapshot_service") {
+ public = [ "public/pw_system/thread_snapshot_service.h" ]
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ "$dir_pw_rpc:server" ]
+ sources = [ "thread_snapshot_service.cc" ]
+ deps = [ "$dir_pw_thread:thread_snapshot_service" ]
+}
+
pw_facade("target_hooks") {
backend = pw_system_TARGET_HOOKS_BACKEND
public = [ "public/pw_system/target_hooks.h" ]
@@ -189,9 +191,6 @@ if (pw_system_TARGET_HOOKS_BACKEND == "") {
get_label_info(":stl_target_hooks", "label_no_toolchain")) {
pw_source_set("stl_target_hooks") {
deps = [
- ":init",
- "$dir_pw_log",
- "$dir_pw_thread:sleep",
"$dir_pw_thread:thread",
"$dir_pw_thread_stl:thread",
]
@@ -211,37 +210,49 @@ if (pw_system_TARGET_HOOKS_BACKEND == "") {
}
}
+group("pw_system") {
+ public_deps = [
+ ":init",
+ ":io",
+ ":log",
+ ":rpc_server",
+ ":work_queue",
+ ]
+ deps = [ ":target_hooks" ]
+}
+
pw_executable("system_example") {
sources = [ "example_user_app_init.cc" ]
deps = [
":pw_system",
"$dir_pw_log",
"$dir_pw_thread:sleep",
+ "$dir_pw_unit_test:rpc_service",
+
+ # Adds a test that the test server can run.
+ "$dir_pw_status:status_test.lib",
+ "$dir_pw_string:string_builder_test.lib",
]
}
-if (dir_pw_third_party_nanopb != "") {
- group("system_examples") {
- deps = [ ":system_example($dir_pigweed/targets/host_device_simulator:host_device_simulator.speed_optimized)" ]
- if (dir_pw_third_party_stm32cube_f4 != "" &&
- dir_pw_third_party_freertos != "") {
- deps += [ ":system_example($dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube.size_optimized)" ]
- }
- if (dir_pw_third_party_smartfusion_mss != "" &&
- dir_pw_third_party_freertos != "") {
- deps += [
- ":system_example($dir_pigweed/targets/emcraft_sf2_som:emcraft_sf2_som.size_optimized)",
- ":system_example($dir_pigweed/targets/emcraft_sf2_som:emcraft_sf2_som.speed_optimized)",
- ":system_example($dir_pigweed/targets/emcraft_sf2_som:emcraft_sf2_som_debug.debug)",
- ]
- }
+group("system_examples") {
+ deps = [ ":system_example($dir_pigweed/targets/host_device_simulator:host_device_simulator.speed_optimized)" ]
+ if (dir_pw_third_party_stm32cube_f4 != "" &&
+ dir_pw_third_party_freertos != "") {
+ deps += [ ":system_example($dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f429i_disc1_stm32cube.size_optimized)" ]
}
-} else {
- pw_error("system_examples") {
- message_lines = [
- "Building the pw_system examples requires Nanopb.",
- "Nanopb can be installed by running the command below and then following the prompted setup steps:",
- " pw package install nanopb",
+ if (dir_pw_third_party_smartfusion_mss != "" &&
+ dir_pw_third_party_freertos != "") {
+ deps += [
+ ":system_example($dir_pigweed/targets/emcraft_sf2_som:emcraft_sf2_som.size_optimized)",
+ ":system_example($dir_pigweed/targets/emcraft_sf2_som:emcraft_sf2_som.speed_optimized)",
+ ":system_example($dir_pigweed/targets/emcraft_sf2_som:emcraft_sf2_som_debug.debug)",
+ ]
+ }
+ if (PICO_SRC_DIR != "" && dir_pw_third_party_freertos != "") {
+ deps += [
+ ":system_example($dir_pigweed/targets/rp2040_pw_system:rp2040_pw_system.debug)",
+ ":system_example($dir_pigweed/targets/rp2040_pw_system:rp2040_pw_system.size_optimized)",
]
}
}
@@ -249,3 +260,6 @@ if (dir_pw_third_party_nanopb != "") {
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_system/CMakeLists.txt b/pw_system/CMakeLists.txt
index e63e71f29..f926726ef 100644
--- a/pw_system/CMakeLists.txt
+++ b/pw_system/CMakeLists.txt
@@ -13,25 +13,30 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_system/backend.cmake)
# WARNING: Many of the dependencies in this file are missing and need to be
# added/updated. This is provided as a starting point, but currently does not
# work.
-pw_add_module_library(pw_system.config
+pw_add_library(pw_system.config INTERFACE
HEADERS
public/pw_system/config.h
+ PUBLIC_INCLUDES
+ public
)
-pw_add_module_library(pw_system.log
+pw_add_library(pw_system.log STATIC
PUBLIC_DEPS
- pw_log_rpc.log_service
- pw_log_rpc.rpc_log_drain_thread
+ # TODO(b/246101669): Add CMake support.
+ # pw_log_rpc.log_service
+ # pw_log_rpc.rpc_log_drain_thread
pw_multisink
PRIVATE_DEPS
pw_system.config
pw_system.rpc_server
- pw_log_rpc.rpc_log_drain
+ # TODO(b/246101669): Add CMake support.
+ # pw_log_rpc.rpc_log_drain
pw_sync.lock_annotations
pw_sync.mutex
HEADERS
@@ -40,7 +45,7 @@ pw_add_module_library(pw_system.log
log.cc
)
-pw_add_module_library(pw_system.log_backend
+pw_add_library(pw_system.log_backend STATIC
PRIVATE_DEPS
pw_system.config
pw_system.log
@@ -49,26 +54,30 @@ pw_add_module_library(pw_system.log_backend
pw_log.facade
pw_log.proto_utils
pw_log_string.handler.facade
+ pw_log_tokenized.handler
pw_log_tokenized.metadata
pw_multisink
pw_result
pw_sync.interrupt_spin_lock
pw_sync.lock_annotations
pw_tokenizer
- pw_tokenizer.global_handler_with_payload.facade
SOURCES
log_backend.cc
)
-pw_add_facade(pw_system.rpc_server
+pw_add_facade(pw_system.rpc_server INTERFACE
+ BACKEND
+ pw_system.rpc_server_BACKEND
+ HEADERS
+ public/pw_system/rpc_server.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_system.config
pw_thread.thread_core
- HEADERS
- public/pw_system/rpc_server.h
)
-pw_add_module_library(pw_system.hdlc_rpc_server
+pw_add_library(pw_system.hdlc_rpc_server STATIC
PRIVATE_DEPS
pw_assert
pw_hdlc.pw_rpc
@@ -83,35 +92,41 @@ pw_add_module_library(pw_system.hdlc_rpc_server
hdlc_rpc_server.cc
)
-pw_add_module_library(pw_system.io
+pw_add_library(pw_system.io INTERFACE
HEADERS
public/pw_system/io.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
pw_stream
)
-pw_add_module_library(pw_system.init
+pw_add_library(pw_system.init STATIC
+ HEADERS
+ public/pw_system/init.h
+ PUBLIC_INCLUDES
+ public
+ SOURCES
+ init.cc
PRIVATE_DEPS
pw_system.log
pw_system.rpc_server
- pw_rpc.nanopb.echo_service
+ pw_rpc.pwpb.echo_service
pw_thread.thread
- SOURCES
- init.cc
- HEADERS
- public/pw_system/init.h
)
-pw_add_module_library(pw_system.work_queue
- PRIVATE_DEPS
- pw_work_queue
- SOURCES
- work_queue.cc
+pw_add_library(pw_system.work_queue STATIC
HEADERS
public/pw_system/work_queue.h
+ PUBLIC_INCLUDES
+ public
+ SOURCES
+ work_queue.cc
+ PRIVATE_DEPS
+ pw_work_queue
)
-pw_add_module_library(pw_system.target_io
+pw_add_library(pw_system.target_io STATIC
PRIVATE_DEPS
pw_system.io
pw_stream
@@ -120,14 +135,16 @@ pw_add_module_library(pw_system.target_io
target_io.cc
)
-pw_add_module_library(pw_system.target_hooks
- PUBLIC_DEPS
- pw_thread
+pw_add_library(pw_system.target_hooks INTERFACE
HEADERS
public/pw_system/target_hooks.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_thread.thread
)
-pw_add_module_library(pw_system.stl_target_hooks
+pw_add_library(pw_system.stl_target_hooks STATIC
PRIVATE_DEPS
pw_thread.sleep
pw_thread.thread
@@ -137,17 +154,17 @@ pw_add_module_library(pw_system.stl_target_hooks
stl_target_hooks.cc
)
-pw_add_module_library(pw_system.freertos_target_hooks
+pw_add_library(pw_system.freertos_target_hooks STATIC
SOURCES
freertos_target_hooks.cc
PRIVATE_DEPS
pw_thread.thread
pw_thread_freertos.thread
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
+ # TODO(b/234876414): This should depend on FreeRTOS but our third parties
# currently do not have CMake support.
)
-pw_add_module_library(pw_system.system_example
+pw_add_library(pw_system.system_example STATIC
PRIVATE_DEPS
pw_system.init
pw_system.io
diff --git a/pw_system/backend.cmake b/pw_system/backend.cmake
new file mode 100644
index 000000000..940de5f26
--- /dev/null
+++ b/pw_system/backend.cmake
@@ -0,0 +1,19 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# The pw_system backend that provides the system RPC server.
+pw_add_backend_variable(pw_system.rpc_server_BACKEND)
diff --git a/pw_system/docs.rst b/pw_system/docs.rst
index b9572b9b9..33fa91823 100644
--- a/pw_system/docs.rst
+++ b/pw_system/docs.rst
@@ -85,6 +85,9 @@ being foundational infrastructure.
cpu = PW_SYSTEM_CPU.CORTEX_M4F
scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
+ # Optionally, override pw_system's defaults to build with clang.
+ system_toolchain = pw_toolchain_arm_clang
+
# The pre_init source set provides things like the interrupt vector table,
# pre-main init, and provision of FreeRTOS hooks.
link_deps = [ "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:pre_init" ]
@@ -122,7 +125,7 @@ being foundational infrastructure.
link_deps = [ "$dir_pigweed/targets/emcraft_sf2_som:pre_init" ]
build_args = {
pw_log_BACKEND = dir_pw_log_basic #dir_pw_log_tokenized
- pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND = "//pw_system:log"
+ pw_log_tokenized_HANDLER_BACKEND = "//pw_system:log"
pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/emcraft_sf2_som:sf2_freertos_config"
pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm3"
pw_sys_io_BACKEND = dir_pw_sys_io_emcraft_sf2
@@ -134,7 +137,7 @@ being foundational infrastructure.
"PW_BOOT_FLASH_BEGIN=0x00000200",
"PW_BOOT_FLASH_SIZE=200K",
- # TODO(pwbug/219): Currently "pw_tokenizer/detokenize_test" requires at
+ # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
# least 6K bytes in heap when using pw_malloc_freelist. The heap size
# required for tests should be investigated.
"PW_BOOT_HEAP_SIZE=7K",
@@ -146,3 +149,9 @@ being foundational infrastructure.
]
}
}
+
+
+Metrics
+=======
+The log backend is tracking metrics to illustrate how to use pw_metric and
+retrieve them using `Device.get_and_log_metrics()`.
diff --git a/pw_system/example_user_app_init.cc b/pw_system/example_user_app_init.cc
index 93c841592..025dc7b8f 100644
--- a/pw_system/example_user_app_init.cc
+++ b/pw_system/example_user_app_init.cc
@@ -11,13 +11,22 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+#define PW_LOG_MODULE_NAME "user_init"
#include "pw_log/log.h"
+#include "pw_system/rpc_server.h"
#include "pw_thread/sleep.h"
+#include "pw_unit_test/unit_test_service.h"
+
namespace pw::system {
+pw::unit_test::UnitTestService unit_test_service;
+
// This will run once after pw::system::Init() completes. This callback must
// return or it will block the work queue.
-void UserAppInit() { PW_LOG_INFO("Pigweed is fun!"); }
+void UserAppInit() {
+ PW_LOG_INFO("Pigweed is fun!");
+ GetRpcServer().RegisterService(unit_test_service);
+}
} // namespace pw::system
diff --git a/pw_system/freertos_backends.gni b/pw_system/freertos_backends.gni
index 0426c82b2..8829bc2bb 100644
--- a/pw_system/freertos_backends.gni
+++ b/pw_system/freertos_backends.gni
@@ -29,6 +29,12 @@ PW_SYSTEM_FREERTOS_BACKENDS = {
pw_thread_ID_BACKEND = "$dir_pw_thread_freertos:id"
pw_thread_SLEEP_BACKEND = "$dir_pw_thread_freertos:sleep"
pw_thread_THREAD_BACKEND = "$dir_pw_thread_freertos:thread"
+ pw_thread_THREAD_ITERATION_BACKEND =
+ "$dir_pw_thread_freertos:thread_iteration"
pw_thread_YIELD_BACKEND = "$dir_pw_thread_freertos:yield"
pw_system_TARGET_HOOKS_BACKEND = "$dir_pw_system:freertos_target_hooks"
+
+ # Enable pw_third_party_freertos_DISABLE_TASKS_STATICS so thread iteration
+ # works out-of-the-box.
+ pw_third_party_freertos_DISABLE_TASKS_STATICS = true
}
diff --git a/pw_system/freertos_target_hooks.cc b/pw_system/freertos_target_hooks.cc
index 04c70055f..d533308a6 100644
--- a/pw_system/freertos_target_hooks.cc
+++ b/pw_system/freertos_target_hooks.cc
@@ -13,7 +13,6 @@
// the License.
#include "FreeRTOS.h"
-#include "pw_system/init.h"
#include "pw_thread/detached_thread.h"
#include "pw_thread/thread.h"
#include "pw_thread_freertos/context.h"
@@ -34,8 +33,8 @@ enum class ThreadPriority : UBaseType_t {
static_assert(static_cast<UBaseType_t>(ThreadPriority::kNumPriorities) <=
configMAX_PRIORITIES);
-static constexpr size_t kLogThreadStackWorkds = 1024;
-static thread::freertos::StaticContextWithStack<kLogThreadStackWorkds>
+static constexpr size_t kLogThreadStackWords = 1024;
+static thread::freertos::StaticContextWithStack<kLogThreadStackWords>
log_thread_context;
const thread::Options& LogThreadOptions() {
static constexpr auto options =
@@ -46,8 +45,8 @@ const thread::Options& LogThreadOptions() {
return options;
}
-static constexpr size_t kRpcThreadStackWorkds = 512;
-static thread::freertos::StaticContextWithStack<kRpcThreadStackWorkds>
+static constexpr size_t kRpcThreadStackWords = 512;
+static thread::freertos::StaticContextWithStack<kRpcThreadStackWords>
rpc_thread_context;
const thread::Options& RpcThreadOptions() {
static constexpr auto options =
@@ -58,8 +57,8 @@ const thread::Options& RpcThreadOptions() {
return options;
}
-static constexpr size_t kWorkQueueThreadStackWorkds = 512;
-static thread::freertos::StaticContextWithStack<kWorkQueueThreadStackWorkds>
+static constexpr size_t kWorkQueueThreadStackWords = 512;
+static thread::freertos::StaticContextWithStack<kWorkQueueThreadStackWords>
work_queue_thread_context;
const thread::Options& WorkQueueThreadOptions() {
static constexpr auto options =
diff --git a/pw_system/hdlc_rpc_server.cc b/pw_system/hdlc_rpc_server.cc
index fe4582651..5bbcd83ab 100644
--- a/pw_system/hdlc_rpc_server.cc
+++ b/pw_system/hdlc_rpc_server.cc
@@ -18,6 +18,7 @@
#include <cstdio>
#include "pw_assert/check.h"
+#include "pw_hdlc/encoded_size.h"
#include "pw_hdlc/rpc_channel.h"
#include "pw_hdlc/rpc_packets.h"
#include "pw_log/log.h"
@@ -31,15 +32,19 @@ namespace {
constexpr size_t kMaxTransmissionUnit = PW_SYSTEM_MAX_TRANSMISSION_UNIT;
-hdlc::RpcChannelOutput hdlc_channel_output(GetWriter(),
- PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS,
- "HDLC channel");
+static_assert(kMaxTransmissionUnit ==
+ hdlc::MaxEncodedFrameSize(rpc::cfg::kEncodingBufferSizeBytes));
+
+hdlc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
+ GetWriter(), PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS, "HDLC channel");
rpc::Channel channels[] = {
rpc::Channel::Create<kDefaultRpcChannelId>(&hdlc_channel_output)};
rpc::Server server(channels);
+constexpr size_t kDecoderBufferSize =
+ hdlc::Decoder::RequiredBufferSizeForFrameSize(kMaxTransmissionUnit);
// Declare a buffer for decoding incoming HDLC frames.
-std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+std::array<std::byte, kDecoderBufferSize> input_buffer;
hdlc::Decoder decoder(input_buffer);
std::array<std::byte, 1> data;
@@ -65,7 +70,7 @@ class RpcDispatchThread final : public thread::ThreadCore {
if (auto result = decoder.Process(byte); result.ok()) {
hdlc::Frame& frame = result.value();
if (frame.address() == PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS) {
- server.ProcessPacket(frame.data(), hdlc_channel_output);
+ server.ProcessPacket(frame.data());
}
}
}
diff --git a/pw_system/init.cc b/pw_system/init.cc
index 215778611..13c4e1bf6 100644
--- a/pw_system/init.cc
+++ b/pw_system/init.cc
@@ -11,19 +11,30 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
+#define PW_LOG_MODULE_NAME "pw_system"
#include "pw_system/init.h"
#include "pw_log/log.h"
-#include "pw_rpc/echo_service_nanopb.h"
+#include "pw_metric/global.h"
+#include "pw_metric/metric_service_pwpb.h"
+#include "pw_rpc/echo_service_pwpb.h"
+#include "pw_system/config.h"
#include "pw_system/rpc_server.h"
#include "pw_system/target_hooks.h"
#include "pw_system/work_queue.h"
#include "pw_system_private/log.h"
#include "pw_thread/detached_thread.h"
+#if PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
+#include "pw_system/thread_snapshot_service.h"
+#endif // PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
+
namespace pw::system {
namespace {
+metric::MetricService metric_service(metric::global_metrics,
+ metric::global_groups);
+
rpc::EchoService echo_service;
void InitImpl() {
@@ -40,6 +51,10 @@ void InitImpl() {
PW_LOG_INFO("Registering RPC services");
GetRpcServer().RegisterService(echo_service);
GetRpcServer().RegisterService(GetLogService());
+ GetRpcServer().RegisterService(metric_service);
+#if PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
+ RegisterThreadSnapshotService(GetRpcServer());
+#endif // PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
PW_LOG_INFO("Starting threads");
// Start threads.
diff --git a/pw_system/log.cc b/pw_system/log.cc
index 7cfbd828f..652f3e2b2 100644
--- a/pw_system/log.cc
+++ b/pw_system/log.cc
@@ -36,7 +36,12 @@ std::array<std::byte, PW_SYSTEM_LOG_BUFFER_SIZE> log_buffer;
// To save RAM, share the mutex and buffer between drains, since drains are
// flushed sequentially.
sync::Mutex drains_mutex;
+
// Buffer to decode and remove entries from log buffer, to send to a drain.
+//
+// TODO(amontanez): pw_log_rpc should provide a helper for this since there's
+// proto encoding overhead unaccounted for here.
+static_assert(rpc::MaxSafePayloadSize() >= PW_SYSTEM_MAX_LOG_ENTRY_SIZE);
std::array<std::byte, PW_SYSTEM_MAX_LOG_ENTRY_SIZE> log_decode_buffer
PW_GUARDED_BY(drains_mutex);
@@ -49,10 +54,7 @@ std::array<RpcLogDrain, 1> drains{{
log_rpc::RpcLogDrainMap drain_map(drains);
-// TODO(amontanez): Is there a helper to subtract RPC overhead?
-constexpr size_t kMaxPackedLogMessagesSize =
- PW_SYSTEM_MAX_TRANSMISSION_UNIT - 32;
-
+constexpr size_t kMaxPackedLogMessagesSize = rpc::MaxSafePayloadSize();
std::array<std::byte, kMaxPackedLogMessagesSize> log_packing_buffer;
} // namespace
diff --git a/pw_system/log_backend.cc b/pw_system/log_backend.cc
index e08ba2ecf..30940633b 100644
--- a/pw_system/log_backend.cc
+++ b/pw_system/log_backend.cc
@@ -20,7 +20,9 @@
#include "pw_chrono/system_clock.h"
#include "pw_log/proto_utils.h"
#include "pw_log_string/handler.h"
+#include "pw_log_tokenized/handler.h"
#include "pw_log_tokenized/metadata.h"
+#include "pw_metric/global.h"
#include "pw_multisink/multisink.h"
#include "pw_result/result.h"
#include "pw_string/string_builder.h"
@@ -28,11 +30,15 @@
#include "pw_sync/lock_annotations.h"
#include "pw_system/config.h"
#include "pw_system_private/log.h"
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
namespace pw::system {
namespace {
+// Sample metric usage.
+PW_METRIC_GROUP_GLOBAL(log_metric_group, "log");
+PW_METRIC(log_metric_group, total_created, "total_created", 0u);
+PW_METRIC(log_metric_group, total_dropped, "total_dropped", 0u);
+
// Buffer used to encode each log entry before saving into log buffer.
sync::InterruptSpinLock log_encode_lock;
std::array<std::byte, PW_SYSTEM_MAX_LOG_ENTRY_SIZE> log_encode_buffer
@@ -56,8 +62,9 @@ int64_t GetTimestamp() {
// Implementation for tokenized log handling. This will be optimized out for
// devices that only use string logging.
-extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload payload, const uint8_t message[], size_t size_bytes) {
+extern "C" void pw_log_tokenized_HandleLog(uint32_t payload,
+ const uint8_t message[],
+ size_t size_bytes) {
log_tokenized::Metadata metadata = payload;
const int64_t timestamp = GetTimestamp();
@@ -66,9 +73,11 @@ extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
metadata, message, size_bytes, timestamp, log_encode_buffer);
if (!encoded_log_result.ok()) {
GetMultiSink().HandleDropped();
+ total_dropped.Increment();
return;
}
GetMultiSink().HandleEntry(encoded_log_result.value());
+ total_created.Increment();
}
// Implementation for string log handling. This will be optimized out for
@@ -98,9 +107,11 @@ extern "C" void pw_log_string_HandleMessageVaList(int level,
log_encode_buffer);
if (!encoded_log_result.ok()) {
GetMultiSink().HandleDropped();
+ total_dropped.Increment();
return;
}
GetMultiSink().HandleEntry(encoded_log_result.value());
+ total_created.Increment();
}
} // namespace pw::system
diff --git a/pw_system/public/pw_system/config.h b/pw_system/public/pw_system/config.h
index e7021eb03..1dc3c3cbd 100644
--- a/pw_system/public/pw_system/config.h
+++ b/pw_system/public/pw_system/config.h
@@ -24,16 +24,17 @@
// PW_SYSTEM_MAX_LOG_ENTRY_SIZE limits the proto-encoded log entry size. This
// value might depend on a target interface's MTU.
//
-// Defaults to 512B.
+// Defaults to 256B.
#ifndef PW_SYSTEM_MAX_LOG_ENTRY_SIZE
-#define PW_SYSTEM_MAX_LOG_ENTRY_SIZE 512
+#define PW_SYSTEM_MAX_LOG_ENTRY_SIZE 256
#endif // PW_SYSTEM_MAX_LOG_ENTRY_SIZE
// PW_SYSTEM_MAX_TRANSMISSION_UNIT target's MTU.
//
-// Defaults to 512B.
+// Defaults to 1055 bytes, which is enough to fit 512-byte payloads when using
+// HDLC framing.
#ifndef PW_SYSTEM_MAX_TRANSMISSION_UNIT
-#define PW_SYSTEM_MAX_TRANSMISSION_UNIT 512
+#define PW_SYSTEM_MAX_TRANSMISSION_UNIT 1055
#endif // PW_SYSTEM_MAX_TRANSMISSION_UNIT
// PW_SYSTEM_DEFAULT_CHANNEL_ID RPC channel ID to host.
@@ -50,6 +51,14 @@
#define PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS 82
#endif // PW_SYSTEM_DEFAULT_RPC_HDLC_ADDRESS
+// PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE specifies if the thread snapshot
+// RPC service is enabled.
+//
+// Defaults to 1.
+#ifndef PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
+#define PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE 1
+#endif // PW_SYSTEM_ENABLE_THREAD_SNAPSHOT_SERVICE
+
// PW_SYSTEM_WORK_QUEUE_MAX_ENTRIES specifies the maximum number of work queue
// entries that may be staged at once.
//
diff --git a/pw_system/public/pw_system/rpc_server.h b/pw_system/public/pw_system/rpc_server.h
index 3918ded3c..bf05fccdb 100644
--- a/pw_system/public/pw_system/rpc_server.h
+++ b/pw_system/public/pw_system/rpc_server.h
@@ -16,6 +16,7 @@
#include <cstdint>
+#include "pw_rpc/server.h"
#include "pw_system/config.h"
#include "pw_thread/thread_core.h"
diff --git a/pw_system/public/pw_system/thread_snapshot_service.h b/pw_system/public/pw_system/thread_snapshot_service.h
new file mode 100644
index 000000000..0b1d715df
--- /dev/null
+++ b/pw_system/public/pw_system/thread_snapshot_service.h
@@ -0,0 +1,22 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_rpc/server.h"
+
+namespace pw::system {
+
+void RegisterThreadSnapshotService(rpc::Server& rpc_server);
+
+} // namespace pw::system
diff --git a/pw_system/py/BUILD.bazel b/pw_system/py/BUILD.bazel
new file mode 100644
index 000000000..0feba3139
--- /dev/null
+++ b/pw_system/py/BUILD.bazel
@@ -0,0 +1,60 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_python//python:defs.bzl", "py_binary", "py_library")
+
+package(default_visibility = ["//visibility:public"])
+
+# TODO(b/241456982): The following deps are required to build :pw_system_lib
+# deps = [
+# "//pw_thread/py:pw_thread",
+# "//pw_log:log_proto_py_pb2",
+# "//pw_metric:metric_proto_py_pb2",
+# "//pw_thread:thread_proto_py_pb2",
+# "//pw_thread:thread_snapshot_service_py_pb2",
+# "//pw_tokenizer:tokenizer_proto_py_pb2",
+# "//pw_unit_test:unit_test_py_pb2",
+# "//pw_unit_test/py:pw_unit_test_lib",
+# ],
+py_library(
+ name = "pw_system_lib",
+ srcs = [
+ "pw_system/__init__.py",
+ "pw_system/console.py",
+ "pw_system/device.py",
+ ],
+ imports = ["."],
+ tags = ["manual"],
+ deps = [
+ "//pw_cli/py:pw_cli",
+ "//pw_console/py:pw_console",
+ "//pw_hdlc/py:pw_hdlc",
+ "//pw_metric/py:pw_metric",
+ "//pw_rpc/py:pw_rpc",
+ "//pw_status/py:pw_status",
+ "//pw_tokenizer/py:pw_tokenizer",
+ ],
+)
+
+py_binary(
+ name = "pw_system_console",
+ srcs = [
+ "pw_system/console.py",
+ ],
+ main = "pw_system/console.py",
+ tags = ["manual"],
+ deps = [
+ ":pw_system_lib",
+ ],
+)
diff --git a/pw_system/py/BUILD.gn b/pw_system/py/BUILD.gn
index ec9e08d59..f8af592f9 100644
--- a/pw_system/py/BUILD.gn
+++ b/pw_system/py/BUILD.gn
@@ -31,11 +31,19 @@ pw_python_package("py") {
"$dir_pw_cli/py",
"$dir_pw_console/py",
"$dir_pw_hdlc/py",
+ "$dir_pw_log:protos.python",
+ "$dir_pw_metric:metric_service_proto.python",
+ "$dir_pw_metric/py",
"$dir_pw_protobuf_compiler/py",
"$dir_pw_rpc/py",
+ "$dir_pw_thread:protos.python",
+ "$dir_pw_thread/py",
"$dir_pw_tokenizer/py",
+ "$dir_pw_unit_test:unit_test_proto.python",
+ "$dir_pw_unit_test/py",
]
inputs = []
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_system/py/pw_system/console.py b/pw_system/py/pw_system/console.py
index 7843837a4..49950884a 100644
--- a/pw_system/py/pw_system/console.py
+++ b/pw_system/py/pw_system/console.py
@@ -16,7 +16,7 @@
To start the console, provide a serial port as the --device argument and paths
or globs for .proto files that define the RPC services to support:
- python -m pw_hdlc.rpc_console --device /dev/ttyUSB0 sample.proto
+ python -m pw_system.console --device /dev/ttyUSB0 --proto-globs pw_rpc/echo.proto
This starts an IPython console for communicating with the connected device. A
few variables are predefined in the interactive console. These include:
@@ -29,7 +29,7 @@ few variables are predefined in the interactive console. These include:
An example echo RPC command:
rpcs.pw.rpc.EchoService.Echo(msg="hello!")
-"""
+""" # pylint: disable=line-too-long
import argparse
import datetime
@@ -42,6 +42,7 @@ from types import ModuleType
from typing import (
Any,
Collection,
+ Dict,
Iterable,
Iterator,
List,
@@ -50,21 +51,29 @@ from typing import (
)
import socket
-import serial # type: ignore
+import serial
+import IPython # type: ignore
-import pw_cli.log
-import pw_console.python_logging
-from pw_console import PwConsoleEmbed
-from pw_console.pyserial_wrapper import SerialWithLogging
+from pw_cli import log as pw_cli_log
+from pw_console.embed import PwConsoleEmbed
+from pw_console.log_store import LogStore
from pw_console.plugins.bandwidth_toolbar import BandwidthToolbar
-
-from pw_log.proto import log_pb2
+from pw_console.pyserial_wrapper import SerialWithLogging
+from pw_console.python_logging import create_temp_log_file, JsonLogFormatter
from pw_rpc.console_tools.console import flattened_rpc_completions
from pw_system.device import Device
from pw_tokenizer.detokenize import AutoUpdatingDetokenizer
+# Default proto imports:
+from pw_log.proto import log_pb2
+from pw_metric_proto import metric_service_pb2
+from pw_thread_protos import thread_snapshot_service_pb2
+from pw_unit_test_proto import unit_test_pb2
+
_LOG = logging.getLogger('tools')
_DEVICE_LOG = logging.getLogger('rpc_device')
+_SERIAL_DEBUG = logging.getLogger('pw_console.serial_debug_logger')
+_ROOT_LOG = logging.getLogger()
PW_RPC_MAX_PACKET_SIZE = 256
SOCKET_SERVER = 'localhost'
@@ -74,47 +83,130 @@ MKFIFO_MODE = 0o666
def _parse_args():
"""Parses and returns the command line arguments."""
- parser = argparse.ArgumentParser(description=__doc__)
+ parser = argparse.ArgumentParser(
+ prog="python -m pw_system.console", description=__doc__
+ )
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-d', '--device', help='the serial port to use')
- parser.add_argument('-b',
- '--baudrate',
- type=int,
- default=115200,
- help='the baud rate to use')
+ parser.add_argument(
+ '-b',
+ '--baudrate',
+ type=int,
+ default=115200,
+ help='the baud rate to use',
+ )
parser.add_argument(
'--serial-debug',
action='store_true',
- help=('Enable debug log tracing of all data passed through'
- 'pyserial read and write.'))
+ help=(
+ 'Enable debug log tracing of all data passed through'
+ 'pyserial read and write.'
+ ),
+ )
parser.add_argument(
'-o',
'--output',
type=argparse.FileType('wb'),
default=sys.stdout.buffer,
- help=('The file to which to write device output (HDLC channel 1); '
- 'provide - or omit for stdout.'))
- parser.add_argument('--logfile', help='Console debug log file.')
- group.add_argument('-s',
- '--socket-addr',
- type=str,
- help='use socket to connect to server, type default for\
- localhost:33000, or manually input the server address:port')
- parser.add_argument("--token-databases",
- metavar='elf_or_token_database',
- nargs="+",
- type=Path,
- help="Path to tokenizer database csv file(s).")
- parser.add_argument('--config-file',
- type=Path,
- help='Path to a pw_console yaml config file.')
- parser.add_argument('--proto-globs',
- nargs='+',
- help='glob pattern for .proto files')
- parser.add_argument('-v',
- '--verbose',
- action='store_true',
- help='Enables debug logging when set')
+ help=(
+ 'The file to which to write device output (HDLC channel 1); '
+ 'provide - or omit for stdout.'
+ ),
+ )
+
+ # Log file options
+ parser.add_argument(
+ '--logfile',
+ default='pw_console-logs.txt',
+ help=(
+ 'Default log file. This will contain host side '
+ 'log messages only unles the '
+ '--merge-device-and-host-logs argument is used.'
+ ),
+ )
+
+ parser.add_argument(
+ '--merge-device-and-host-logs',
+ action='store_true',
+ help=(
+ 'Include device logs in the default --logfile.'
+ 'These are normally shown in a separate device '
+ 'only log file.'
+ ),
+ )
+
+ parser.add_argument(
+ '--host-logfile',
+ help=(
+ 'Additional host only log file. Normally all logs in the '
+ 'default logfile are host only.'
+ ),
+ )
+
+ parser.add_argument(
+ '--device-logfile',
+ default='pw_console-device-logs.txt',
+ help='Device only log file.',
+ )
+
+ parser.add_argument(
+ '--json-logfile', help='Device only JSON formatted log file.'
+ )
+
+ group.add_argument(
+ '-s',
+ '--socket-addr',
+ type=str,
+ help='use socket to connect to server, type default for\
+ localhost:33000, or manually input the server address:port',
+ )
+ parser.add_argument(
+ "--token-databases",
+ metavar='elf_or_token_database',
+ nargs="+",
+ type=Path,
+ help="Path to tokenizer database csv file(s).",
+ )
+ parser.add_argument(
+ '--config-file',
+ type=Path,
+ help='Path to a pw_console yaml config file.',
+ )
+ parser.add_argument(
+ '--proto-globs',
+ nargs='+',
+ default=[],
+ help='glob pattern for .proto files.',
+ )
+ parser.add_argument(
+ '-v',
+ '--verbose',
+ action='store_true',
+ help='Enables debug logging when set.',
+ )
+ parser.add_argument(
+ '--ipython',
+ action='store_true',
+ dest='use_ipython',
+ help='Use IPython instead of pw_console.',
+ )
+
+ # TODO(b/248257406) Use argparse.BooleanOptionalAction when Python 3.8 is
+ # no longer supported.
+ parser.add_argument(
+ '--rpc-logging',
+ action='store_true',
+ default=True,
+ help='Use pw_rpc based logging.',
+ )
+
+ parser.add_argument(
+ '--no-rpc-logging',
+ action='store_false',
+ dest='rpc_logging',
+ help="Don't use pw_rpc based logging.",
+ )
+
return parser.parse_args()
@@ -124,10 +216,20 @@ def _expand_globs(globs: Iterable[str]) -> Iterator[Path]:
yield Path(file)
-def _start_ipython_terminal(device: Device,
- serial_debug: bool = False,
- config_file_path: Optional[Path] = None) -> None:
- """Starts an interactive IPython terminal with preset variables."""
+def _start_python_terminal( # pylint: disable=too-many-arguments
+ device: Device,
+ device_log_store: LogStore,
+ root_log_store: LogStore,
+ serial_debug_log_store: LogStore,
+ log_file: str,
+ host_logfile: str,
+ device_logfile: str,
+ json_logfile: str,
+ serial_debug: bool = False,
+ config_file_path: Optional[Path] = None,
+ use_ipython: bool = False,
+) -> None:
+ """Starts an interactive Python terminal with preset variables."""
local_variables = dict(
client=device.client,
device=device,
@@ -138,7 +240,8 @@ def _start_ipython_terminal(device: Device,
LOG=logging.getLogger(),
)
- welcome_message = cleandoc("""
+ welcome_message = cleandoc(
+ """
Welcome to the Pigweed Console!
Help: Press F1 or click the [Help] menu
@@ -149,19 +252,37 @@ def _start_ipython_terminal(device: Device,
device.rpcs.pw.rpc.EchoService.Echo(msg='hello!')
LOG.warning('Message appears in Host Logs window.')
DEVICE_LOG.warning('Message appears in Device Logs window.')
- """)
+ """
+ )
+
+ welcome_message += '\n\nLogs are being saved to:\n ' + log_file
+ if host_logfile:
+ welcome_message += '\nHost logs are being saved to:\n ' + host_logfile
+ if device_logfile:
+ welcome_message += (
+ '\nDevice logs are being saved to:\n ' + device_logfile
+ )
+ if json_logfile:
+ welcome_message += (
+ '\nJSON device logs are being saved to:\n ' + json_logfile
+ )
+
+ if use_ipython:
+ print(welcome_message)
+ IPython.terminal.embed.InteractiveShellEmbed().mainloop(
+ local_ns=local_variables, module=argparse.Namespace()
+ )
+ return
client_info = device.info()
completions = flattened_rpc_completions([client_info])
- log_windows = {
- 'Device Logs': [_DEVICE_LOG],
- 'Host Logs': [logging.getLogger()],
+ log_windows: Dict[str, Union[List[logging.Logger], LogStore]] = {
+ 'Device Logs': device_log_store,
+ 'Host Logs': root_log_store,
}
if serial_debug:
- log_windows['Serial Debug'] = [
- logging.getLogger('pw_console.serial_debug_logger')
- ]
+ log_windows['Serial Debug'] = serial_debug_log_store
interactive_console = PwConsoleEmbed(
global_vars=local_variables,
@@ -171,16 +292,17 @@ def _start_ipython_terminal(device: Device,
help_text=__doc__,
config_file_path=config_file_path,
)
- interactive_console.hide_windows('Host Logs')
interactive_console.add_sentence_completer(completions)
if serial_debug:
interactive_console.add_bottom_toolbar(BandwidthToolbar())
# Setup Python logger propagation
- interactive_console.setup_python_logging()
-
- # Don't send device logs to the root logger.
- _DEVICE_LOG.propagate = False
+ interactive_console.setup_python_logging(
+ # Send any unhandled log messages to the external file.
+ last_resort_filename=log_file,
+ # Don't change propagation for these loggers.
+ loggers_with_no_propagation=[_DEVICE_LOG],
+ )
interactive_console.embed()
@@ -206,54 +328,126 @@ class SocketClientImpl:
return self.socket.recv(num_bytes)
-def console(device: str,
- baudrate: int,
- proto_globs: Collection[str],
- token_databases: Collection[Path],
- socket_addr: str,
- logfile: str,
- output: Any,
- serial_debug: bool = False,
- config_file: Optional[Path] = None,
- verbose: bool = False) -> int:
+# pylint: disable=too-many-arguments,too-many-locals
+def console(
+ device: str,
+ baudrate: int,
+ proto_globs: Collection[str],
+ token_databases: Collection[Path],
+ socket_addr: str,
+ logfile: str,
+ host_logfile: str,
+ device_logfile: str,
+ json_logfile: str,
+ output: Any,
+ serial_debug: bool = False,
+ config_file: Optional[Path] = None,
+ verbose: bool = False,
+ compiled_protos: Optional[List[ModuleType]] = None,
+ merge_device_and_host_logs: bool = False,
+ rpc_logging: bool = True,
+ use_ipython: bool = False,
+) -> int:
"""Starts an interactive RPC console for HDLC."""
# argparse.FileType doesn't correctly handle '-' for binary files.
if output is sys.stdout:
output = sys.stdout.buffer
+ # Don't send device logs to the root logger.
+ _DEVICE_LOG.propagate = False
+ # Create pw_console LogStore handlers. These are the data source for log
+ # messages to be displayed in the UI.
+ device_log_store = LogStore()
+ root_log_store = LogStore()
+ serial_debug_log_store = LogStore()
+ # Attach the LogStores as handlers for each log window we want to show.
+ # This should be done before device initialization to capture early
+ # messages.
+ _DEVICE_LOG.addHandler(device_log_store)
+ _ROOT_LOG.addHandler(root_log_store)
+ _SERIAL_DEBUG.addHandler(serial_debug_log_store)
+
if not logfile:
# Create a temp logfile to prevent logs from appearing over stdout. This
# would corrupt the prompt toolkit UI.
- logfile = pw_console.python_logging.create_temp_log_file()
+ logfile = create_temp_log_file()
log_level = logging.DEBUG if verbose else logging.INFO
- pw_cli.log.install(log_level, True, False, logfile)
- _DEVICE_LOG.setLevel(log_level)
+
+ pw_cli_log.install(
+ level=log_level, use_color=False, hide_timestamp=False, log_file=logfile
+ )
+
+ if device_logfile:
+ pw_cli_log.install(
+ level=log_level,
+ use_color=False,
+ hide_timestamp=False,
+ log_file=device_logfile,
+ logger=_DEVICE_LOG,
+ )
+ if host_logfile:
+ pw_cli_log.install(
+ level=log_level,
+ use_color=False,
+ hide_timestamp=False,
+ log_file=host_logfile,
+ logger=_ROOT_LOG,
+ )
+
+ if merge_device_and_host_logs:
+ # Add device logs to the default logfile.
+ pw_cli_log.install(
+ level=log_level,
+ use_color=False,
+ hide_timestamp=False,
+ log_file=logfile,
+ logger=_DEVICE_LOG,
+ )
+
_LOG.setLevel(log_level)
+ _DEVICE_LOG.setLevel(log_level)
+ _ROOT_LOG.setLevel(log_level)
+ _SERIAL_DEBUG.setLevel(logging.DEBUG)
+
+ if json_logfile:
+ json_filehandler = logging.FileHandler(json_logfile, encoding='utf-8')
+ json_filehandler.setLevel(log_level)
+ json_filehandler.setFormatter(JsonLogFormatter())
+ _DEVICE_LOG.addHandler(json_filehandler)
detokenizer = None
if token_databases:
detokenizer = AutoUpdatingDetokenizer(*token_databases)
detokenizer.show_errors = True
- if not proto_globs:
- proto_globs = ['**/*.proto']
-
protos: List[Union[ModuleType, Path]] = list(_expand_globs(proto_globs))
+ if compiled_protos is None:
+ compiled_protos = []
+
# Append compiled log.proto library to avoid include errors when manually
# provided, and shadowing errors due to ordering when the default global
# search path is used.
- protos.append(log_pb2)
+ if rpc_logging:
+ compiled_protos.append(log_pb2)
+ compiled_protos.append(unit_test_pb2)
+ protos.extend(compiled_protos)
+ protos.append(metric_service_pb2)
+ protos.append(thread_snapshot_service_pb2)
if not protos:
- _LOG.critical('No .proto files were found with %s',
- ', '.join(proto_globs))
+ _LOG.critical(
+ 'No .proto files were found with %s', ', '.join(proto_globs)
+ )
_LOG.critical('At least one .proto file is required')
return 1
- _LOG.debug('Found %d .proto files found with %s', len(protos),
- ', '.join(proto_globs))
+ _LOG.debug(
+ 'Found %d .proto files found with %s',
+ len(protos),
+ ', '.join(proto_globs),
+ )
serial_impl = serial.Serial
if serial_debug:
@@ -264,7 +458,11 @@ def console(device: str,
serial_device = serial_impl(
device,
baudrate,
- timeout=0, # Non-blocking mode
+ # Timeout in seconds. This should be a very small value. Setting to
+ # zero makes pyserial read() non-blocking which will cause the host
+ # machine to busy loop and 100% CPU usage.
+ # https://pythonhosted.org/pyserial/pyserial_api.html#serial.Serial
+ timeout=0.1,
)
read = lambda: serial_device.read(8192)
write = serial_device.write
@@ -284,15 +482,30 @@ def console(device: str,
_LOG.exception('Failed to initialize socket at %s', socket_addr)
return 1
- device_client = Device(1,
- read,
- write,
- protos,
- detokenizer,
- timestamp_decoder=timestamp_decoder,
- rpc_timeout_s=5)
+ device_client = Device(
+ 1,
+ read,
+ write,
+ protos,
+ detokenizer,
+ timestamp_decoder=timestamp_decoder,
+ rpc_timeout_s=5,
+ use_rpc_logging=rpc_logging,
+ )
- _start_ipython_terminal(device_client, serial_debug, config_file)
+ _start_python_terminal(
+ device=device_client,
+ device_log_store=device_log_store,
+ root_log_store=root_log_store,
+ serial_debug_log_store=serial_debug_log_store,
+ log_file=logfile,
+ host_logfile=host_logfile,
+ device_logfile=device_logfile,
+ json_logfile=json_logfile,
+ serial_debug=serial_debug,
+ config_file_path=config_file,
+ use_ipython=use_ipython,
+ )
return 0
@@ -300,5 +513,9 @@ def main() -> int:
return console(**vars(_parse_args()))
+def main_with_compiled_protos(compiled_protos):
+ return console(**vars(_parse_args()), compiled_protos=compiled_protos)
+
+
if __name__ == '__main__':
sys.exit(main())
diff --git a/pw_system/py/pw_system/device.py b/pw_system/py/pw_system/device.py
index 1288f68b8..3ec387f99 100644
--- a/pw_system/py/pw_system/device.py
+++ b/pw_system/py/pw_system/device.py
@@ -20,13 +20,16 @@ from types import ModuleType
from typing import Any, Callable, List, Union, Optional
from pw_hdlc.rpc import HdlcRpcClient, default_channels
-import pw_log_tokenized
-
+from pw_log_tokenized import FormatStringWithMetadata
from pw_log.proto import log_pb2
+from pw_metric import metric_parser
from pw_rpc import callback_client, console_tools
from pw_status import Status
-from pw_tokenizer.detokenize import Detokenizer
+from pw_thread.thread_analyzer import ThreadSnapshotAnalyzer
+from pw_thread_protos import thread_pb2
+from pw_tokenizer import detokenize
from pw_tokenizer.proto import decode_optionally_tokenized
+from pw_unit_test.rpc import run_tests as pw_unit_test_run_tests
# Internal log for troubleshooting this tool (the console).
_LOG = logging.getLogger('tools')
@@ -39,17 +42,22 @@ class Device:
The target must have and RPC support, RPC logging.
Note: use this class as a base for specialized device representations.
"""
- def __init__(self,
- channel_id: int,
- read,
- write,
- proto_library: List[Union[ModuleType, Path]],
- detokenizer: Optional[Detokenizer],
- timestamp_decoder: Optional[Callable[[int], str]],
- rpc_timeout_s=5):
+
+ def __init__(
+ self,
+ channel_id: int,
+ read,
+ write,
+ proto_library: List[Union[ModuleType, Path]],
+ detokenizer: Optional[detokenize.Detokenizer],
+ timestamp_decoder: Optional[Callable[[int], str]],
+ rpc_timeout_s: float = 5,
+ use_rpc_logging: bool = True,
+ ):
self.channel_id = channel_id
self.protos = proto_library
self.detokenizer = detokenizer
+ self.rpc_timeout_s = rpc_timeout_s
self.logger = DEFAULT_DEVICE_LOGGER
self.logger.setLevel(logging.DEBUG) # Allow all device logs through.
@@ -57,28 +65,47 @@ class Device:
self._expected_log_sequence_id = 0
callback_client_impl = callback_client.Impl(
- default_unary_timeout_s=rpc_timeout_s,
+ default_unary_timeout_s=self.rpc_timeout_s,
default_stream_timeout_s=None,
)
+
+ def detokenize_and_log_output(data: bytes, _detokenizer=None):
+ log_messages = data.decode(
+ encoding='utf-8', errors='surrogateescape'
+ )
+
+ if self.detokenizer:
+ log_messages = decode_optionally_tokenized(
+ self.detokenizer, data
+ )
+
+ for line in log_messages.splitlines():
+ self.logger.info(line)
+
self.client = HdlcRpcClient(
read,
self.protos,
default_channels(write),
- lambda data: self.logger.info("%s", str(data)),
- client_impl=callback_client_impl)
+ detokenize_and_log_output,
+ client_impl=callback_client_impl,
+ )
- # Start listening to logs as soon as possible.
- self.listen_to_log_stream()
+ if use_rpc_logging:
+ # Start listening to logs as soon as possible.
+ self.listen_to_log_stream()
def info(self) -> console_tools.ClientInfo:
- return console_tools.ClientInfo('device', self.rpcs,
- self.client.client)
+ return console_tools.ClientInfo('device', self.rpcs, self.client.client)
@property
def rpcs(self) -> Any:
"""Returns an object for accessing services on the specified channel."""
return next(iter(self.client.client.channels())).rpcs
+ def run_tests(self, timeout_s: Optional[float] = 5) -> bool:
+ """Runs the unit tests on this device."""
+ return pw_unit_test_run_tests(self.rpcs, timeout_s=timeout_s)
+
def listen_to_log_stream(self):
"""Opens a log RPC for the device's unrequested log stream.
@@ -86,11 +113,14 @@ class Device:
with a response or error packet.
"""
self.rpcs.pw.log.Logs.Listen.open(
- on_next=lambda _, log_entries_proto: self.
- _log_entries_proto_parser(log_entries_proto),
+ on_next=lambda _, log_entries_proto: self._log_entries_proto_parser(
+ log_entries_proto
+ ),
on_completed=lambda _, status: _LOG.info(
- 'Log stream completed with status: %s', status),
- on_error=lambda _, error: self._handle_log_stream_error(error))
+ 'Log stream completed with status: %s', status
+ ),
+ on_error=lambda _, error: self._handle_log_stream_error(error),
+ )
def _handle_log_stream_error(self, error: Status):
"""Resets the log stream RPC on error to avoid losing logs."""
@@ -103,16 +133,21 @@ class Device:
def _handle_log_drop_count(self, drop_count: int, reason: str):
log_text = 'log' if drop_count == 1 else 'logs'
message = f'Dropped {drop_count} {log_text} due to {reason}'
- self._emit_device_log(logging.WARNING, '', '', '', message)
+ self._emit_device_log(logging.WARNING, '', '', message)
def _check_for_dropped_logs(self, log_entries_proto: log_pb2.LogEntries):
# Count log messages received that don't use the dropped field.
- messages_received = sum(1 if not log_proto.dropped else 0
- for log_proto in log_entries_proto.entries)
- dropped_log_count = (log_entries_proto.first_entry_sequence_id -
- self._expected_log_sequence_id)
+ messages_received = sum(
+ 1 if not log_proto.dropped else 0
+ for log_proto in log_entries_proto.entries
+ )
+ dropped_log_count = (
+ log_entries_proto.first_entry_sequence_id
+ - self._expected_log_sequence_id
+ )
self._expected_log_sequence_id = (
- log_entries_proto.first_entry_sequence_id + messages_received)
+ log_entries_proto.first_entry_sequence_id + messages_received
+ )
if dropped_log_count > 0:
self._handle_log_drop_count(dropped_log_count, 'loss at transport')
elif dropped_log_count < 0:
@@ -126,38 +161,54 @@ class Device:
level = (log_proto.line_level & 0x7) * 10
if self.detokenizer:
message = str(
- decode_optionally_tokenized(self.detokenizer,
- log_proto.message))
+ decode_optionally_tokenized(
+ self.detokenizer, log_proto.message
+ )
+ )
else:
- message = log_proto.message.decode("utf-8")
- log = pw_log_tokenized.FormatStringWithMetadata(message)
+ message = log_proto.message.decode('utf-8')
+ log = FormatStringWithMetadata(message)
# Handle dropped count.
if log_proto.dropped:
- drop_reason = log_proto.message.decode("utf-8").lower(
- ) if log_proto.message else 'enqueue failure on device'
+ drop_reason = (
+ log_proto.message.decode('utf-8').lower()
+ if log_proto.message
+ else 'enqueue failure on device'
+ )
self._handle_log_drop_count(log_proto.dropped, drop_reason)
continue
- self._emit_device_log(level, '', decoded_timestamp, log.module,
- log.message, **dict(log.fields))
-
- def _emit_device_log(self, level: int, source_name: str, timestamp: str,
- module_name: str, message: str, **metadata_fields):
+ self._emit_device_log(
+ level,
+ decoded_timestamp,
+ log.module,
+ log.message,
+ **dict(log.fields),
+ )
+
+ def _emit_device_log(
+ self,
+ level: int,
+ timestamp: str,
+ module_name: str,
+ message: str,
+ **metadata_fields,
+ ):
# Fields used for console table view
fields = metadata_fields
- fields['source_name'] = source_name
fields['timestamp'] = timestamp
fields['msg'] = message
fields['module'] = module_name
# Format used for file or stdout logging.
- self.logger.log(level,
- '[%s] %s %s%s',
- source_name,
- timestamp,
- f'{module_name} '.lstrip(),
- message,
- extra=dict(extra_metadata_fields=fields))
+ self.logger.log(
+ level,
+ '%s %s%s',
+ timestamp,
+ f'{module_name} '.lstrip(),
+ message,
+ extra=dict(extra_metadata_fields=fields),
+ )
def decode_timestamp(self, timestamp: int) -> str:
"""Decodes timestamp to a human-readable value.
@@ -168,3 +219,32 @@ class Device:
if self.timestamp_decoder:
return self.timestamp_decoder(timestamp)
return str(datetime.timedelta(seconds=timestamp / 1e9))[:-3]
+
+ def get_and_log_metrics(self) -> dict:
+ """Retrieves the parsed metrics and logs them to the console."""
+ metrics = metric_parser.parse_metrics(
+ self.rpcs, self.detokenizer, self.rpc_timeout_s
+ )
+
+ def print_metrics(metrics, path):
+ """Traverses dictionaries, until a non-dict value is reached."""
+ for path_name, metric in metrics.items():
+ if isinstance(metric, dict):
+ print_metrics(metric, path + '/' + path_name)
+ else:
+ _LOG.info('%s/%s: %s', path, path_name, str(metric))
+
+ print_metrics(metrics, '')
+ return metrics
+
+ def snapshot_peak_stack_usage(self, thread_name: Optional[str] = None):
+ _, rsp = self.rpcs.pw.thread.ThreadSnapshotService.GetPeakStackUsage(
+ name=thread_name
+ )
+
+ thread_info = thread_pb2.SnapshotThreadInfo()
+ for thread_info_block in rsp:
+ for thread in thread_info_block.threads:
+ thread_info.threads.append(thread)
+ for line in str(ThreadSnapshotAnalyzer(thread_info)).splitlines():
+ _LOG.info('%s', line)
diff --git a/pw_system/py/setup.cfg b/pw_system/py/setup.cfg
index b71b3b159..fea637c72 100644
--- a/pw_system/py/setup.cfg
+++ b/pw_system/py/setup.cfg
@@ -22,12 +22,8 @@ description = Pigweed System
packages = find:
zip_safe = False
install_requires =
- pw_cli
- pw_console
- pw_hdlc
- pw_protobuf_compiler
- pw_rpc
- pw_tokenizer
+ pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
[options.entry_points]
console_scripts = pw-system-console = pw_system.console:main
diff --git a/pw_system/socket_target_io.cc b/pw_system/socket_target_io.cc
index 55f105408..441de02b4 100644
--- a/pw_system/socket_target_io.cc
+++ b/pw_system/socket_target_io.cc
@@ -29,11 +29,15 @@ constexpr uint16_t kPort = PW_SYSTEM_SOCKET_IO_PORT;
stream::SocketStream& GetStream() {
static bool running = false;
static std::mutex socket_open_lock;
+ static stream::ServerSocket server_socket;
static stream::SocketStream socket_stream;
std::lock_guard guard(socket_open_lock);
if (!running) {
printf("Awaiting connection on port %d\n", static_cast<int>(kPort));
- PW_CHECK_OK(socket_stream.Serve(kPort));
+ PW_CHECK_OK(server_socket.Listen(kPort));
+ auto accept_result = server_socket.Accept();
+ PW_CHECK_OK(accept_result.status());
+ socket_stream = *std::move(accept_result);
printf("Client connected\n");
running = true;
}
diff --git a/pw_system/stl_backends.gni b/pw_system/stl_backends.gni
index 7956f6a65..06d901ebd 100644
--- a/pw_system/stl_backends.gni
+++ b/pw_system/stl_backends.gni
@@ -17,10 +17,12 @@ import("//build_overrides/pigweed.gni")
PW_SYSTEM_STL_BACKENDS = {
pw_chrono_SYSTEM_CLOCK_BACKEND = "$dir_pw_chrono_stl:system_clock"
pw_chrono_SYSTEM_TIMER_BACKEND = "$dir_pw_chrono_stl:system_timer"
- pw_sync_INTERRUPT_SPIN_LOCK_BACKEND = "$dir_pw_sync_stl:interrupt_spin_lock"
pw_sync_BINARY_SEMAPHORE_BACKEND = "$dir_pw_sync_stl:binary_semaphore_backend"
+ pw_sync_CONDITION_VARIABLE_BACKEND =
+ "$dir_pw_sync_stl:condition_variable_backend"
pw_sync_COUNTING_SEMAPHORE_BACKEND =
"$dir_pw_sync_stl:counting_semaphore_backend"
+ pw_sync_INTERRUPT_SPIN_LOCK_BACKEND = "$dir_pw_sync_stl:interrupt_spin_lock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_stl:mutex_backend"
pw_sync_TIMED_MUTEX_BACKEND = "$dir_pw_sync_stl:timed_mutex_backend"
pw_sync_THREAD_NOTIFICATION_BACKEND =
@@ -31,6 +33,7 @@ PW_SYSTEM_STL_BACKENDS = {
pw_thread_ID_BACKEND = "$dir_pw_thread_stl:id"
pw_thread_SLEEP_BACKEND = "$dir_pw_thread_stl:sleep"
pw_thread_THREAD_BACKEND = "$dir_pw_thread_stl:thread"
+ pw_thread_THREAD_ITERATION_BACKEND = "$dir_pw_thread_stl:thread_iteration"
pw_thread_YIELD_BACKEND = "$dir_pw_thread_stl:yield"
pw_system_TARGET_HOOKS_BACKEND = "$dir_pw_system:stl_target_hooks"
}
diff --git a/pw_system/stl_target_hooks.cc b/pw_system/stl_target_hooks.cc
index f9d7fef78..048153d4c 100644
--- a/pw_system/stl_target_hooks.cc
+++ b/pw_system/stl_target_hooks.cc
@@ -12,11 +12,6 @@
// License for the specific language governing permissions and limitations under
// the License.
-#define PW_LOG_MODULE_NAME "SYS"
-
-#include "pw_log/log.h"
-#include "pw_system/init.h"
-#include "pw_thread/sleep.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
@@ -38,16 +33,3 @@ const thread::Options& WorkQueueThreadOptions() {
}
} // namespace pw::system
-
-extern "C" int main() {
- pw::system::Init();
- // Sleep loop rather than return on this thread so the process isn't closed.
- while (true) {
- pw::this_thread::sleep_for(std::chrono::seconds(10));
- // It's hard to tell that simulator is alive and working since nothing is
- // logging after initial "boot," so for now log a line occasionally so
- // users can see that the simulator is alive and well.
- PW_LOG_INFO("Simulated device is still alive");
- // TODO(amontanez): This thread should probably have a way to exit.
- }
-}
diff --git a/pw_system/system_target.gni b/pw_system/system_target.gni
index 8fe9d431d..6000f1c8c 100644
--- a/pw_system/system_target.gni
+++ b/pw_system/system_target.gni
@@ -37,6 +37,7 @@ import("stl_backends.gni")
# This scope is essentially an enum for pw_system_target's `cpu` selection.
PW_SYSTEM_CPU = {
+ CORTEX_M0PLUS = "cortex-m0plus"
CORTEX_M4F = "cortex-m4f"
CORTEX_M3 = "cortex-m3"
CORTEX_M7F = "cortex-m7f"
@@ -64,6 +65,7 @@ PW_SYSTEM_SCHEDULER = {
# scheduler: (required) The scheduler implementation and API to use for this
# target.
# Supported choices: PW_SYSTEM_SCHEDULER.FREERTOS, PW_SYSTEM_SCHEDULER.NATIVE
+# system_toolchain: Override the default toolchain selection.
# use_pw_malloc: Whether or not to replace the default malloc implementation
# with pw_malloc. Defaults enabled for supported targets.
# link_deps: Additional link-time dependencies required for all executables.
@@ -106,8 +108,6 @@ template("pw_system_target") {
# is added.
pw_log_BACKEND = dir_pw_log_basic
- pw_rpc_CONFIG = "$dir_pw_rpc:use_global_mutex"
-
# TODO(amontanez): This should be set to a "$dir_pw_unit_test:rpc_main"
# when RPC is working.
pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
@@ -126,19 +126,25 @@ template("pw_system_target") {
pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
}
+ if (defined(invoker.system_toolchain)) {
+ _system_toolchain = invoker.system_toolchain
+ } else {
+ _system_toolchain = pw_toolchain_arm_gcc
+ }
+
_final_binary_extension = ".elf"
_toolchains = [
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_debug
+ toolchain_base = _system_toolchain.cortex_m7f_debug
level_name = _OPTIMIZATION_LEVELS.DEBUG
},
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_size_optimized
+ toolchain_base = _system_toolchain.cortex_m7f_size_optimized
level_name = _OPTIMIZATION_LEVELS.SIZE_OPTIMIZED
},
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m7f_speed_optimized
+ toolchain_base = _system_toolchain.cortex_m7f_speed_optimized
level_name = _OPTIMIZATION_LEVELS.SPEED_OPTIMIZED
},
]
@@ -151,19 +157,25 @@ template("pw_system_target") {
pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
}
+ if (defined(invoker.system_toolchain)) {
+ _system_toolchain = invoker.system_toolchain
+ } else {
+ _system_toolchain = pw_toolchain_arm_gcc
+ }
+
_final_binary_extension = ".elf"
_toolchains = [
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m4f_debug
+ toolchain_base = _system_toolchain.cortex_m4f_debug
level_name = _OPTIMIZATION_LEVELS.DEBUG
},
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m4f_size_optimized
+ toolchain_base = _system_toolchain.cortex_m4f_size_optimized
level_name = _OPTIMIZATION_LEVELS.SIZE_OPTIMIZED
},
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m4f_speed_optimized
+ toolchain_base = _system_toolchain.cortex_m4f_speed_optimized
level_name = _OPTIMIZATION_LEVELS.SPEED_OPTIMIZED
},
]
@@ -175,19 +187,56 @@ template("pw_system_target") {
pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
}
+ if (defined(invoker.system_toolchain)) {
+ _system_toolchain = invoker.system_toolchain
+ } else {
+ _system_toolchain = pw_toolchain_arm_gcc
+ }
+
_final_binary_extension = ".elf"
_toolchains = [
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m3_debug
+ toolchain_base = _system_toolchain.cortex_m3_debug
+ level_name = _OPTIMIZATION_LEVELS.DEBUG
+ },
+ {
+ toolchain_base = _system_toolchain.cortex_m3_size_optimized
+ level_name = _OPTIMIZATION_LEVELS.SIZE_OPTIMIZED
+ },
+ {
+ toolchain_base = _system_toolchain.cortex_m3_speed_optimized
+ level_name = _OPTIMIZATION_LEVELS.SPEED_OPTIMIZED
+ },
+ ]
+ } else if (invoker.cpu == PW_SYSTEM_CPU.CORTEX_M0PLUS) {
+ _current_cpu = "arm"
+ _arch_build_args = {
+ pw_bloat_BLOATY_CONFIG = "$dir_pw_boot_cortex_m/bloaty_config.bloaty"
+ pw_boot_BACKEND = "$dir_pw_boot_cortex_m:armv7m"
+ pw_interrupt_CONTEXT_BACKEND = "$dir_pw_interrupt_cortex_m:context_armv7m"
+ }
+
+ if (defined(invoker.system_toolchain)) {
+ _system_toolchain = invoker.system_toolchain
+ } else {
+ _system_toolchain = pw_toolchain_arm_gcc
+ }
+
+ # This creates a double .elf.elf extension for the rp2040 target.
+ # _final_binary_extension = ".elf"
+
+ _toolchains = [
+ {
+ toolchain_base = _system_toolchain.cortex_m0plus_debug
level_name = _OPTIMIZATION_LEVELS.DEBUG
},
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m3_size_optimized
+ toolchain_base = _system_toolchain.cortex_m0plus_size_optimized
level_name = _OPTIMIZATION_LEVELS.SIZE_OPTIMIZED
},
{
- toolchain_base = pw_toolchain_arm_gcc.cortex_m3_speed_optimized
+ toolchain_base = _system_toolchain.cortex_m0plus_speed_optimized
level_name = _OPTIMIZATION_LEVELS.SPEED_OPTIMIZED
},
]
@@ -201,37 +250,28 @@ template("pw_system_target") {
}
_link_deps += [ "$dir_pw_log_string:handler.impl" ]
- if (host_os != "win") {
- _toolchains = [
- {
- toolchain_base = pw_toolchain_host_clang.debug
- level_name = _OPTIMIZATION_LEVELS.DEBUG
- },
- {
- toolchain_base = pw_toolchain_host_clang.size_optimized
- level_name = _OPTIMIZATION_LEVELS.SIZE_OPTIMIZED
- },
- {
- toolchain_base = pw_toolchain_host_clang.speed_optimized
- level_name = _OPTIMIZATION_LEVELS.SPEED_OPTIMIZED
- },
- ]
+ if (defined(invoker.system_toolchain)) {
+ _system_toolchain = invoker.system_toolchain
+ } else if (host_os == "win") {
+ _system_toolchain = pw_toolchain_host_gcc
} else {
- _toolchains = [
- {
- toolchain_base = pw_toolchain_host_gcc.debug
- level_name = _OPTIMIZATION_LEVELS.DEBUG
- },
- {
- toolchain_base = pw_toolchain_host_gcc.size_optimized
- level_name = _OPTIMIZATION_LEVELS.SIZE_OPTIMIZED
- },
- {
- toolchain_base = pw_toolchain_host_gcc.speed_optimized
- level_name = _OPTIMIZATION_LEVELS.SPEED_OPTIMIZED
- },
- ]
+ _system_toolchain = pw_toolchain_host_clang
}
+
+ _toolchains = [
+ {
+ toolchain_base = _system_toolchain.debug
+ level_name = _OPTIMIZATION_LEVELS.DEBUG
+ },
+ {
+ toolchain_base = _system_toolchain.size_optimized
+ level_name = _OPTIMIZATION_LEVELS.SIZE_OPTIMIZED
+ },
+ {
+ toolchain_base = _system_toolchain.speed_optimized
+ level_name = _OPTIMIZATION_LEVELS.SPEED_OPTIMIZED
+ },
+ ]
}
assert(defined(_arch_build_args),
"Unknown cpu choice for $target_name: `${invoker.cpu}`")
diff --git a/pw_system/thread_snapshot_service.cc b/pw_system/thread_snapshot_service.cc
new file mode 100644
index 000000000..fcc4c2a1c
--- /dev/null
+++ b/pw_system/thread_snapshot_service.cc
@@ -0,0 +1,30 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_system/thread_snapshot_service.h"
+
+#include "pw_thread/thread_snapshot_service.h"
+
+namespace pw::system {
+namespace {
+
+thread::proto::ThreadSnapshotServiceBuffer<> system_thread_snapshot_service;
+
+} // namespace
+
+void RegisterThreadSnapshotService(rpc::Server& rpc_server) {
+ rpc_server.RegisterService(system_thread_snapshot_service);
+}
+
+} // namespace pw::system
diff --git a/pw_system/work_queue.cc b/pw_system/work_queue.cc
index 1c7a05478..4c680e285 100644
--- a/pw_system/work_queue.cc
+++ b/pw_system/work_queue.cc
@@ -19,7 +19,7 @@
namespace pw::system {
-// TODO(pwbug/590): Consider switching this to a "NoDestroy" wrapped type to
+// TODO(b/234876895): Consider switching this to a "NoDestroy" wrapped type to
// allow the static destructor to be optimized out.
work_queue::WorkQueue& GetWorkQueue() {
static constexpr size_t kMaxWorkQueueEntries =
diff --git a/pw_target_runner/BUILD.gn b/pw_target_runner/BUILD.gn
index 9db101629..6bc6ea27a 100644
--- a/pw_target_runner/BUILD.gn
+++ b/pw_target_runner/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
@@ -29,3 +30,6 @@ pw_proto_library("target_runner_proto") {
pw_proto_library("exec_server_config_proto") {
sources = [ "pw_target_runner_server_protos/exec_server_config.proto" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_target_runner/go/src/pigweed.dev/pw_target_runner_client/main.go b/pw_target_runner/go/src/pigweed.dev/pw_target_runner_client/main.go
index 90eb4a8d5..0bbdc15ff 100644
--- a/pw_target_runner/go/src/pigweed.dev/pw_target_runner_client/main.go
+++ b/pw_target_runner/go/src/pigweed.dev/pw_target_runner_client/main.go
@@ -4,7 +4,7 @@
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
-// https://www.apache.org/licenses/LICENSE-2.0
+// https://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
diff --git a/pw_target_runner/go/src/pigweed.dev/pw_target_runner_server/main.go b/pw_target_runner/go/src/pigweed.dev/pw_target_runner_server/main.go
index 8c18912c7..97b542b5f 100644
--- a/pw_target_runner/go/src/pigweed.dev/pw_target_runner_server/main.go
+++ b/pw_target_runner/go/src/pigweed.dev/pw_target_runner_server/main.go
@@ -4,7 +4,7 @@
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
-// https://www.apache.org/licenses/LICENSE-2.0
+// https://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
diff --git a/pw_thread/BUILD.bazel b/pw_thread/BUILD.bazel
index 6b2daa66b..c54848a8f 100644
--- a/pw_thread/BUILD.bazel
+++ b/pw_thread/BUILD.bazel
@@ -18,6 +18,8 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -50,6 +52,51 @@ pw_cc_library(
}),
)
+pw_cc_library(
+ name = "config",
+ hdrs = ["public/pw_thread/config.h"],
+ includes = ["public"],
+)
+
+pw_cc_library(
+ name = "thread_info",
+ hdrs = ["public/pw_thread/thread_info.h"],
+ includes = ["public"],
+ deps = ["//pw_span"],
+)
+
+pw_cc_facade(
+ name = "thread_iteration_facade",
+ hdrs = [
+ "public/pw_thread/thread_iteration.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":thread_info",
+ "//pw_function",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "thread_iteration",
+ deps = [
+ ":thread_iteration_facade",
+ "@pigweed_config//:pw_thread_iteration_backend",
+ ],
+)
+
+pw_cc_library(
+ name = "iteration_backend_multiplexer",
+ visibility = ["@pigweed_config//:__pkg__"],
+ deps = select({
+ "//pw_build/constraints/rtos:embos": ["//pw_thread_embos:thread_iteration"],
+ "//pw_build/constraints/rtos:freertos": ["//pw_thread_freertos:thread_iteration"],
+ "//pw_build/constraints/rtos:threadx": ["//pw_thread_threadx:thread_iteration"],
+ "//conditions:default": ["//pw_thread_stl:thread_iteration"],
+ }),
+)
+
pw_cc_facade(
name = "sleep_facade",
hdrs = [
@@ -94,6 +141,7 @@ pw_cc_facade(
includes = ["public"],
deps = [
":id_facade",
+ ":thread_core",
],
)
@@ -129,6 +177,34 @@ pw_cc_library(
"public/pw_thread/thread_core.h",
],
includes = ["public"],
+ deps = [
+ "//pw_log",
+ "//pw_status",
+ ],
+)
+
+pw_cc_library(
+ name = "thread_snapshot_service",
+ srcs = [
+ "pw_thread_private/thread_snapshot_service.h",
+ "thread_snapshot_service.cc",
+ ],
+ hdrs = ["public/pw_thread/thread_snapshot_service.h"],
+ includes = ["public"],
+ deps = [
+ "//pw_protobuf",
+ "//pw_rpc/raw:server_api",
+ "//pw_span",
+ "//pw_status",
+ ":config",
+ ":thread_cc.pwpb",
+ ":thread_info",
+ ":thread_iteration",
+ ":thread_snapshot_service_cc.pwpb",
+ ":thread_snapshot_service_cc.raw_rpc",
+ # TODO(amontanez): This should depend on FreeRTOS but our third parties
+ # currently do not have Bazel support.
+ ],
)
pw_cc_facade(
@@ -174,13 +250,13 @@ pw_cc_library(
"public/pw_thread/snapshot.h",
],
deps = [
- ":util",
+ ":thread",
+ ":thread_cc.pwpb",
"//pw_bytes",
"//pw_function",
"//pw_log",
"//pw_protobuf",
"//pw_status",
- "//pw_thread:protos",
],
)
@@ -238,6 +314,35 @@ pw_cc_test(
)
pw_cc_test(
+ name = "thread_info_test",
+ srcs = [
+ "thread_info_test.cc",
+ ],
+ deps = [
+ ":thread_info",
+ "//pw_span",
+ ],
+)
+
+pw_cc_test(
+ name = "thread_snapshot_service_test",
+ srcs = [
+ "pw_thread_private/thread_snapshot_service.h",
+ "thread_snapshot_service_test.cc",
+ ],
+ deps = [
+ ":thread_cc.pwpb",
+ ":thread_info",
+ ":thread_iteration",
+ ":thread_snapshot_service",
+ ":thread_snapshot_service_cc.pwpb",
+ "//pw_protobuf",
+ "//pw_span",
+ "//pw_sync:thread_notification",
+ ],
+)
+
+pw_cc_test(
name = "yield_facade_test",
srcs = [
"yield_facade_test.cc",
@@ -249,3 +354,45 @@ pw_cc_test(
"//pw_unit_test",
],
)
+
+proto_library(
+ name = "thread_proto",
+ srcs = ["pw_thread_protos/thread.proto"],
+ strip_import_prefix = "/pw_thread",
+ deps = [
+ "//pw_tokenizer:tokenizer_proto",
+ ],
+)
+
+# TODO(b/241456982): Not expected to build yet.
+py_proto_library(
+ name = "thread_proto_py_pb2",
+ tags = ["manual"],
+ deps = [":thread_proto"],
+)
+
+proto_library(
+ name = "thread_snapshot_service_proto",
+ srcs = ["pw_thread_protos/thread_snapshot_service.proto"],
+ strip_import_prefix = "/pw_thread",
+ deps = [
+ ":thread_proto",
+ ],
+)
+
+pw_proto_library(
+ name = "thread_snapshot_service_cc",
+ deps = [":thread_snapshot_service_proto"],
+)
+
+# TODO(b/241456982): Not expected to build yet.
+py_proto_library(
+ name = "thread_snapshot_service_py_pb2",
+ tags = ["manual"],
+ deps = [":thread_snapshot_service_proto"],
+)
+
+pw_proto_library(
+ name = "thread_cc",
+ deps = [":thread_proto"],
+)
diff --git a/pw_thread/BUILD.gn b/pw_thread/BUILD.gn
index 69a9c864d..011e465fd 100644
--- a/pw_thread/BUILD.gn
+++ b/pw_thread/BUILD.gn
@@ -77,6 +77,17 @@ pw_source_set("thread_core") {
public = [ "public/pw_thread/thread_core.h" ]
}
+pw_facade("thread_iteration") {
+ backend = pw_thread_THREAD_ITERATION_BACKEND
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_thread/thread_iteration.h" ]
+ public_deps = [
+ ":thread_info",
+ "$dir_pw_function",
+ "$dir_pw_status",
+ ]
+}
+
pw_facade("yield") {
backend = pw_thread_YIELD_BACKEND
public_configs = [ ":public_include_path" ]
@@ -102,11 +113,45 @@ pw_source_set("snapshot") {
]
}
+pw_source_set("thread_info") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ dir_pw_span ]
+ public = [ "public/pw_thread/thread_info.h" ]
+ deps = [ ":config" ]
+}
+
+pw_source_set("thread_snapshot_service") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_thread/thread_snapshot_service.h" ]
+ public_deps = [
+ ":protos.pwpb",
+ ":protos.raw_rpc",
+ ":thread_info",
+ "$dir_pw_rpc/raw:server_api",
+ "$dir_pw_status:pw_status",
+ ]
+ sources = [
+ "pw_thread_private/thread_snapshot_service.h",
+ "thread_snapshot_service.cc",
+ ]
+ deps = [
+ ":config",
+ ":thread_iteration",
+ "$dir_pw_log",
+ "$dir_pw_protobuf",
+ "$dir_pw_rpc/raw:server_api",
+ "$dir_pw_span",
+ "$dir_pw_status:pw_status",
+ ]
+}
+
pw_test_group("tests") {
tests = [
":id_facade_test",
":sleep_facade_test",
+ ":thread_info_test",
":yield_facade_test",
+ ":thread_snapshot_service_test",
]
}
@@ -116,6 +161,23 @@ pw_test("id_facade_test") {
deps = [ ":id" ]
}
+pw_test("thread_snapshot_service_test") {
+ enable_if = pw_thread_THREAD_ITERATION_BACKEND != ""
+ sources = [
+ "pw_thread_private/thread_snapshot_service.h",
+ "thread_snapshot_service_test.cc",
+ ]
+ deps = [
+ ":protos.pwpb",
+ ":thread_iteration",
+ "$dir_pw_protobuf",
+ "$dir_pw_span",
+ "$dir_pw_sync:thread_notification",
+ "$dir_pw_thread:thread_info",
+ "$dir_pw_thread:thread_snapshot_service",
+ ]
+}
+
pw_test("sleep_facade_test") {
enable_if = pw_thread_SLEEP_BACKEND != "" && pw_thread_ID_BACKEND != ""
sources = [
@@ -151,6 +213,14 @@ pw_source_set("thread_facade_test") {
]
}
+pw_test("thread_info_test") {
+ sources = [ "thread_info_test.cc" ]
+ deps = [
+ ":thread_info",
+ dir_pw_span,
+ ]
+}
+
pw_test("yield_facade_test") {
enable_if = pw_thread_YIELD_BACKEND != "" && pw_thread_ID_BACKEND != ""
sources = [
@@ -164,7 +234,10 @@ pw_test("yield_facade_test") {
}
pw_proto_library("protos") {
- sources = [ "pw_thread_protos/thread.proto" ]
+ sources = [
+ "pw_thread_protos/thread.proto",
+ "pw_thread_protos/thread_snapshot_service.proto",
+ ]
deps = [ "$dir_pw_tokenizer:proto" ]
}
diff --git a/pw_thread/CMakeLists.txt b/pw_thread/CMakeLists.txt
index 0ad8e33eb..dc16136bd 100644
--- a/pw_thread/CMakeLists.txt
+++ b/pw_thread/CMakeLists.txt
@@ -13,10 +13,11 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_thread/backend.cmake)
pw_add_module_config(pw_thread_CONFIG)
-pw_add_module_library(pw_thread.config
+pw_add_library(pw_thread.config INTERFACE
HEADERS
public/pw_thread/config.h
PUBLIC_INCLUDES
@@ -25,14 +26,18 @@ pw_add_module_library(pw_thread.config
${pw_thread_CONFIG}
)
-pw_add_facade(pw_thread.id
+pw_add_facade(pw_thread.id INTERFACE
+ BACKEND
+ pw_thread.id_BACKEND
HEADERS
public/pw_thread/id.h
PUBLIC_INCLUDES
public
)
-pw_add_facade(pw_thread.sleep
+pw_add_facade(pw_thread.sleep STATIC
+ BACKEND
+ pw_thread.sleep_BACKEND
HEADERS
public/pw_thread/sleep.h
PUBLIC_INCLUDES
@@ -44,7 +49,9 @@ pw_add_facade(pw_thread.sleep
sleep.cc
)
-pw_add_facade(pw_thread.thread
+pw_add_facade(pw_thread.thread STATIC
+ BACKEND
+ pw_thread.thread_BACKEND
HEADERS
public/pw_thread/detached_thread.h
public/pw_thread/thread.h
@@ -57,14 +64,16 @@ pw_add_facade(pw_thread.thread
thread.cc
)
-pw_add_module_library(pw_thread.thread_core
+pw_add_library(pw_thread.thread_core INTERFACE
HEADERS
public/pw_thread/thread_core.h
PUBLIC_INCLUDES
public
)
-pw_add_facade(pw_thread.yield
+pw_add_facade(pw_thread.yield STATIC
+ BACKEND
+ pw_thread.yield_BACKEND
HEADERS
public/pw_thread/yield.h
PUBLIC_INCLUDES
@@ -75,7 +84,7 @@ pw_add_facade(pw_thread.yield
yield.cc
)
-pw_add_module_library(pw_thread.snapshot
+pw_add_library(pw_thread.snapshot STATIC
HEADERS
public/pw_thread/snapshot.h
PUBLIC_INCLUDES
@@ -100,11 +109,11 @@ pw_proto_library(pw_thread.protos
pw_tokenizer.proto
)
-if(NOT "${pw_thread.id_BACKEND}" STREQUAL "pw_thread.id.NO_BACKEND_SET")
+if(NOT "${pw_thread.id_BACKEND}" STREQUAL "")
pw_add_test(pw_thread.id_facade_test
SOURCES
id_facade_test.cc
- DEPS
+ PRIVATE_DEPS
pw_thread.id
GROUPS
modules
@@ -112,13 +121,13 @@ if(NOT "${pw_thread.id_BACKEND}" STREQUAL "pw_thread.id.NO_BACKEND_SET")
)
endif()
-if((NOT "${pw_thread.id_BACKEND}" STREQUAL "pw_thread.id.NO_BACKEND_SET") AND
- (NOT "${pw_thread.sleep_BACKEND}" STREQUAL "pw_thread.sleep.NO_BACKEND_SET"))
+if((NOT "${pw_thread.id_BACKEND}" STREQUAL "") AND
+ (NOT "${pw_thread.sleep_BACKEND}" STREQUAL ""))
pw_add_test(pw_thread.sleep_facade_test
SOURCES
sleep_facade_test.cc
sleep_facade_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_chrono.system_clock
pw_thread.id
pw_thread.sleep
@@ -128,7 +137,7 @@ if((NOT "${pw_thread.id_BACKEND}" STREQUAL "pw_thread.id.NO_BACKEND_SET") AND
)
endif()
-pw_add_module_library(pw_thread.test_threads
+pw_add_library(pw_thread.test_threads INTERFACE
HEADERS
public/pw_thread/test_threads.h
PUBLIC_INCLUDES
@@ -141,7 +150,7 @@ pw_add_module_library(pw_thread.test_threads
# test_threads you can create a pw_add_test target which depends on this
# target and a target which provides the implementation of
# test_threads. See pw_thread_stl.thread_backend_test as an example.
-pw_add_module_library(pw_thread.thread_facade_test
+pw_add_library(pw_thread.thread_facade_test STATIC
SOURCES
thread_facade_test.cc
PRIVATE_DEPS
@@ -153,13 +162,13 @@ pw_add_module_library(pw_thread.thread_facade_test
pw_unit_test
)
-if((NOT "${pw_thread.id_BACKEND}" STREQUAL "pw_thread.id.NO_BACKEND_SET") AND
- (NOT "${pw_thread.yield_BACKEND}" STREQUAL "pw_thread.yield.NO_BACKEND_SET"))
+if((NOT "${pw_thread.id_BACKEND}" STREQUAL "") AND
+ (NOT "${pw_thread.yield_BACKEND}" STREQUAL ""))
pw_add_test(pw_thread.yield_facade_test
SOURCES
yield_facade_test.cc
yield_facade_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_thread.id
pw_thread.yield
GROUPS
diff --git a/pw_thread/backend.cmake b/pw_thread/backend.cmake
new file mode 100644
index 000000000..8928f052a
--- /dev/null
+++ b/pw_thread/backend.cmake
@@ -0,0 +1,28 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Backend for the pw_thread module's pw::thread::Id.
+pw_add_backend_variable(pw_thread.id_BACKEND)
+
+# Backend for the pw_thread module's pw::thread::sleep_{for,until}.
+pw_add_backend_variable(pw_thread.sleep_BACKEND)
+
+# Backend for the pw_thread module's pw::thread::Thread to create threads.
+pw_add_backend_variable(pw_thread.thread_BACKEND)
+
+# Backend for the pw_thread module's pw::thread::yield.
+pw_add_backend_variable(pw_thread.yield_BACKEND)
diff --git a/pw_thread/backend.gni b/pw_thread/backend.gni
index 6ecd37871..c104808b7 100644
--- a/pw_thread/backend.gni
+++ b/pw_thread/backend.gni
@@ -29,4 +29,7 @@ declare_args() {
# backend for pw_chrono_SYSTEM_CLOCK_BACKEND is chosen.
# Set to true to disable the asserts.
pw_thread_OVERRIDE_SYSTEM_CLOCK_BACKEND_CHECK = false
+
+ # Backend for the pw_thread module's pw::thread::thread_iteration.
+ pw_thread_THREAD_ITERATION_BACKEND = ""
}
diff --git a/pw_thread/docs.rst b/pw_thread/docs.rst
index ebc77b541..f68c8136b 100644
--- a/pw_thread/docs.rst
+++ b/pw_thread/docs.rst
@@ -145,6 +145,9 @@ Example
const pw::thread::Id my_id = pw::this_thread::get_id();
}
+
+.. _module-pw_thread-thread-creation:
+
---------------
Thread Creation
---------------
@@ -225,13 +228,26 @@ but may contain things like the thread name, priority, scheduling policy,
core/processor affinity, and/or an optional reference to a pre-allocated
Context (the collection of memory allocations needed for a thread to run).
-Options shall NOT permit starting as detached, this must be done explicitly
-through the Thread API.
+Options shall NOT have an attribute to start threads as detached vs joinable.
+All ``pw::thread::Thread`` instances must be explicitly ``join()``'d or
+``detach()``'d through the run-time Thread API.
+
+Note that if backends set ``PW_THREAD_JOINING_ENABLED`` to false, backends
+may use native OS specific APIs to create native detached threads because the
+``join()`` API would be compiled out. However, users must still explicitly
+invoke ``detach()``.
Options must not contain any memory needed for a thread to run (TCB,
stack, etc.). The Options may be deleted or re-used immediately after
starting a thread.
+Options subclass must contain non-default explicit constructor (parametrized or
+not), e.g. ``constexpr Options() {}``. It is not enough to have them as
+``= default`` ones, because C++17 considers subclasses like ``stl::Options`` as
+aggregate classes if they have a default constructor and requires base class
+constructor to be public (which is not the case for the ``thread::Options``) for
+``Options{}`` syntax.
+
Please see the thread creation backend documentation for how their Options work.
Portable Thread Creation
@@ -260,7 +276,7 @@ with the following contents:
// Contents of my_app/threads.h
#pragma once
- #include "pw_thread/options.h"
+ #include "pw_thread/thread.h"
namespace my_app {
@@ -436,6 +452,96 @@ function without arguments. For example:
implements the ThreadCore MUST meet or exceed the lifetime of its thread of
execution!
+----------------
+Thread Iteration
+----------------
+C++
+===
+.. cpp:function:: Status ForEachThread(const ThreadCallback& cb)
+
+ Calls the provided callback for each thread that has not been joined/deleted.
+
+ This function provides a generalized subset of information that a TCB might
+ contain to make it easier to introspect system state. Depending on the RTOS
+ and its configuration, some of these fields may not be populated, so it is
+ important to check that they have values before attempting to access them.
+
+ **Warning:** The function may disable the scheduler to perform
+ a runtime capture of thread information.
+
+-----------------------
+Thread Snapshot Service
+-----------------------
+``pw_thread`` offers an optional RPC service library
+(``:thread_snapshot_service``) that enables thread info capture of
+running threads on a device at runtime via RPC. The service will guide
+optimization of stack usage by providing an overview of thread information,
+including thread name, stack bounds, and peak stack usage.
+
+``ThreadSnapshotService`` currently supports peak stack usage capture for
+all running threads (``ThreadSnapshotService::GetPeakStackUsage()``) as well as
+for a specific thread, filtering by name
+(``ThreadSnapshotService::GetPeakStackUsage(name=b"/* thread name */")``).
+Thread information capture relies on the thread iteration facade which will
+**momentarily halt your RTOS**, collect information about running threads, and
+return this information through the service.
+
+RPC service setup
+=================
+To expose a ``ThreadSnapshotService`` in your application, do the following:
+
+1. Create an instance of ``pw::thread::proto::ThreadSnapshotServiceBuffer``.
+ This template takes the number of expected threads, and uses it to properly
+ size buffers required for a ``ThreadSnapshotService``. If no thread count
+ argument is provided, this defaults to ``PW_THREAD_MAXIMUM_THREADS``.
+2. Register the service with your RPC server.
+
+For example:
+
+.. code::
+
+ #include "pw_rpc/server.h"
+ #include "pw_thread/thread_snapshot_service.h"
+
+ // Note: You must customize the RPC server setup; see pw_rpc.
+ pw::rpc::Channel channels[] = {
+ pw::rpc::Channel::Create<1>(&uart_output),
+ };
+ Server server(channels);
+
+ // Thread snapshot service builder instance.
+ pw::thread::proto::ThreadSnapshotServiceBuffer</*num threads*/>
+ thread_snapshot_service;
+
+ void RegisterServices() {
+ server.RegisterService(thread_snapshot_service);
+ // Register other services here.
+ }
+
+ void main() {
+ // ... system initialization ...
+
+ RegisterServices();
+
+ // ... start your application ...
+ }
+
+.. c:macro:: PW_THREAD_MAXIMUM_THREADS
+
+ The max number of threads to use by default for thread snapshot service.
+
+.. cpp:function:: constexpr size_t RequiredServiceBufferSize(const size_t num_threads)
+
+ Function provided through the service to calculate buffer sizing. If no
+ argument ``num_threads`` is specified, the function will take ``num_threads``
+ to be ``PW_THREAD_MAXIMUM_THREADS``.
+
+.. attention::
+ Some platforms may only support limited subsets of this service
+ depending on RTOS configuration. **Ensure that your RTOS is configured
+ properly before using this service.** Please see the thread iteration
+ documentation for your backend for more detail on RTOS support.
+
-----------------------
pw_snapshot integration
-----------------------
diff --git a/pw_thread/public/pw_thread/config.h b/pw_thread/public/pw_thread/config.h
index c2c0adce6..7b83a7eca 100644
--- a/pw_thread/public/pw_thread/config.h
+++ b/pw_thread/public/pw_thread/config.h
@@ -17,3 +17,13 @@
#ifndef PW_THREAD_CONFIG_LOG_LEVEL
#define PW_THREAD_CONFIG_LOG_LEVEL PW_LOG_LEVEL_DEBUG
#endif // PW_THREAD_CONFIG_LOG_LEVEL
+
+// The max number of threads to use by default for thread snapshot service.
+#ifndef PW_THREAD_MAXIMUM_THREADS
+#define PW_THREAD_MAXIMUM_THREADS 10
+#endif // PW_THREAD_MAXIMUM_THREADS
+
+// The max number of threads to bundle by default for thread snapshot service.
+#ifndef PW_THREAD_NUM_BUNDLED_THREADS
+#define PW_THREAD_NUM_BUNDLED_THREADS 3
+#endif // PW_THREAD_MAXIMUM_THREADS \ No newline at end of file
diff --git a/pw_thread/public/pw_thread/snapshot.h b/pw_thread/public/pw_thread/snapshot.h
index dfa96deda..c2502ae2b 100644
--- a/pw_thread/public/pw_thread/snapshot.h
+++ b/pw_thread/public/pw_thread/snapshot.h
@@ -13,6 +13,7 @@
// the License.
#pragma once
+#include <optional>
#include <string_view>
#include "pw_bytes/span.h"
@@ -27,7 +28,7 @@ namespace pw::thread {
// field. This should encode either raw_backtrace or raw_stack to the provided
// Thread stream encoder.
using ProcessThreadStackCallback =
- Function<Status(Thread::StreamEncoder&, ConstByteSpan)>;
+ Function<Status(proto::Thread::StreamEncoder&, ConstByteSpan)>;
struct StackContext {
std::string_view thread_name;
@@ -47,7 +48,7 @@ struct StackContext {
// stack_end_pointer
// stack_pointer
Status SnapshotStack(const StackContext& stack,
- Thread::StreamEncoder& encoder,
+ proto::Thread::StreamEncoder& encoder,
const ProcessThreadStackCallback& thread_stack_callback);
} // namespace pw::thread
diff --git a/pw_thread/public/pw_thread/thread.h b/pw_thread/public/pw_thread/thread.h
index aa9271cf1..792afaa1d 100644
--- a/pw_thread/public/pw_thread/thread.h
+++ b/pw_thread/public/pw_thread/thread.h
@@ -33,15 +33,23 @@ namespace pw::thread {
// core/processor affinity, and/or an optional reference to a pre-allocated
// Context (the collection of memory allocations needed for a thread to run).
//
-// Options shall NOT permit starting as detached, this must be done explicitly
-// through the Thread API.
+// Options shall NOT have an attribute to start threads as detached vs joinable.
+// All `pw::thread::Thread` instances must be explicitly `join()`'d or
+// `detach()`'d through the run-time Thread API.
+//
+// Note that if backends set `PW_THREAD_JOINING_ENABLED` to false, backends may
+// use native OS specific APIs to create native detached threads because the
+// `join()` API would be compiled out. However, users must still explicitly
+// invoke `detach()`.
//
// Options must not contain any memory needed for a thread to run (TCB,
// stack, etc.). The Options may be deleted or re-used immediately after
// starting a thread.
class Options {
protected:
- constexpr Options() = default;
+ // We can't use `= default` here, because it allows to create an Options
+ // instance in C++17 with `pw::thread::Options{}` syntax.
+ constexpr Options() {}
};
// The class Thread can represent a single thread of execution. Threads allow
diff --git a/pw_thread/public/pw_thread/thread_info.h b/pw_thread/public/pw_thread/thread_info.h
new file mode 100644
index 000000000..e18ebe524
--- /dev/null
+++ b/pw_thread/public/pw_thread/thread_info.h
@@ -0,0 +1,117 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <bitset>
+#include <optional>
+
+#include "pw_span/span.h"
+
+namespace pw::thread {
+
+// The class ThreadInfo provides a summary of specific thread information and is
+// used by thread iteration to dump thread info generically.
+//
+// Captures the following fields:
+// stack_start_pointer
+// stack_end_pointer
+// stack_est_peak_pointer
+// thread_name
+class ThreadInfo {
+ public:
+ ThreadInfo() = default;
+
+ constexpr std::optional<uintptr_t> stack_low_addr() const {
+ return get_stack_info_ptr(kStackLowAddress);
+ }
+
+ void set_stack_low_addr(uintptr_t val) {
+ set_stack_info_ptr(kStackLowAddress, val);
+ }
+
+ void clear_stack_low_addr() { clear_stack_info_ptr(kStackLowAddress); }
+
+ constexpr std::optional<uintptr_t> stack_high_addr() const {
+ return get_stack_info_ptr(kStackHighAddress);
+ }
+
+ void set_stack_high_addr(uintptr_t val) {
+ set_stack_info_ptr(kStackHighAddress, val);
+ }
+
+ void clear_stack_high_addr() { clear_stack_info_ptr(kStackHighAddress); }
+
+ constexpr std::optional<uintptr_t> stack_pointer() const {
+ return get_stack_info_ptr(kStackPointer);
+ }
+
+ void set_stack_pointer(uintptr_t val) {
+ set_stack_info_ptr(kStackPointer, val);
+ }
+
+ void clear_stack_pointer() { clear_stack_info_ptr(kStackPointer); }
+
+ constexpr std::optional<uintptr_t> stack_peak_addr() const {
+ return get_stack_info_ptr(kStackPeakAddress);
+ }
+
+ void set_stack_peak_addr(uintptr_t val) {
+ set_stack_info_ptr(kStackPeakAddress, val);
+ }
+
+ void clear_stack_peak_addr() { clear_stack_info_ptr(kStackPeakAddress); }
+
+ constexpr std::optional<span<const std::byte>> thread_name() const {
+ return has_value_[kThreadName] ? std::make_optional(thread_name_)
+ : std::nullopt;
+ }
+
+ void set_thread_name(span<const std::byte> val) {
+ thread_name_ = val;
+ has_value_.set(kThreadName, true);
+ }
+
+ void clear_thread_name() { clear_stack_info_ptr(kThreadName); }
+
+ private:
+ enum ThreadInfoIndex {
+ kStackLowAddress,
+ kStackHighAddress,
+ kStackPointer,
+ kStackPeakAddress,
+ kThreadName,
+ kMaxNumMembersDoNotUse,
+ };
+
+ constexpr std::optional<uintptr_t> get_stack_info_ptr(
+ ThreadInfoIndex index) const {
+ return has_value_[index] ? std::make_optional(stack_info_ptrs_[index])
+ : std::nullopt;
+ }
+
+ void set_stack_info_ptr(ThreadInfoIndex index, uintptr_t val) {
+ stack_info_ptrs_[index] = val;
+ has_value_.set(index, true);
+ }
+
+ void clear_stack_info_ptr(ThreadInfoIndex index) {
+ has_value_.set(index, false);
+ }
+
+ std::bitset<ThreadInfoIndex::kMaxNumMembersDoNotUse> has_value_;
+ uintptr_t stack_info_ptrs_[ThreadInfoIndex::kMaxNumMembersDoNotUse];
+ span<const std::byte> thread_name_;
+};
+
+} // namespace pw::thread
diff --git a/pw_thread/public/pw_thread/thread_iteration.h b/pw_thread/public/pw_thread/thread_iteration.h
new file mode 100644
index 000000000..d1babbae5
--- /dev/null
+++ b/pw_thread/public/pw_thread/thread_iteration.h
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_function/function.h"
+#include "pw_status/status.h"
+#include "pw_thread/thread_info.h"
+
+namespace pw::thread {
+
+// A callback that is executed for each thread when using ForEachThread(). The
+// callback should return a struct reference for each thread iteration.
+//
+// This ThreadCallback should be treated as if the scheduler may be disabled,
+// meaning:
+// - Processing inside of the callback should be kept to a minimum.
+// - Callback should never attempt to block.
+using ThreadCallback = pw::Function<bool(const ThreadInfo&)>;
+
+// Iterates through all threads that haven't been deleted, calling the provided
+// callback on each thread.
+//
+// Warning: This may disable the scheduler.
+Status ForEachThread(const ThreadCallback& cb);
+
+} // namespace pw::thread
diff --git a/pw_thread/public/pw_thread/thread_snapshot_service.h b/pw_thread/public/pw_thread/thread_snapshot_service.h
new file mode 100644
index 000000000..06445eb0d
--- /dev/null
+++ b/pw_thread/public/pw_thread/thread_snapshot_service.h
@@ -0,0 +1,85 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_thread/config.h"
+#include "pw_thread/thread_info.h"
+#include "pw_thread_protos/thread.pwpb.h"
+#include "pw_thread_protos/thread_snapshot_service.pwpb.h"
+#include "pw_thread_protos/thread_snapshot_service.raw_rpc.pb.h"
+
+namespace pw::thread::proto {
+
+Status ProtoEncodeThreadInfo(pwpb::SnapshotThreadInfo::StreamEncoder& encoder,
+ const ThreadInfo& thread_info);
+
+// Calculates encoded buffer size based on code gen constants.
+constexpr size_t RequiredServiceBufferSize(
+ size_t num_threads = PW_THREAD_MAXIMUM_THREADS) {
+ constexpr size_t kSizeOfResponse =
+ pwpb::SnapshotThreadInfo::kMaxEncodedSizeBytes +
+ pwpb::Thread::kMaxEncodedSizeBytes;
+ return kSizeOfResponse * num_threads;
+}
+
+// The ThreadSnapshotService will return peak stack usage across running
+// threads when requested by GetPeak().
+//
+// Parameter encode_buffer: buffer where thread information is encoded. Size
+// depends on RequiredBufferSize().
+//
+// Parameter thread_proto_indices: array keeping track of thread boundaries in
+// the encode buffer. The service uses these indices to send response data out
+// in bundles.
+//
+// Parameter num_bundled_threads: constant describing number of threads per
+// bundle in response.
+class ThreadSnapshotService
+ : public pw_rpc::raw::ThreadSnapshotService::Service<
+ ThreadSnapshotService> {
+ public:
+ constexpr ThreadSnapshotService(
+ span<std::byte> encode_buffer,
+ Vector<size_t>& thread_proto_indices,
+ size_t num_bundled_threads = PW_THREAD_NUM_BUNDLED_THREADS)
+ : encode_buffer_(encode_buffer),
+ thread_proto_indices_(thread_proto_indices),
+ num_bundled_threads_(num_bundled_threads) {}
+ void GetPeakStackUsage(ConstByteSpan request, rpc::RawServerWriter& response);
+
+ private:
+ span<std::byte> encode_buffer_;
+ Vector<size_t>& thread_proto_indices_;
+ size_t num_bundled_threads_;
+};
+
+// A ThreadSnapshotService that allocates required buffers based on the
+// number of running threads on a device.
+template <size_t kNumThreads = PW_THREAD_MAXIMUM_THREADS>
+class ThreadSnapshotServiceBuffer : public ThreadSnapshotService {
+ public:
+ ThreadSnapshotServiceBuffer()
+ : ThreadSnapshotService(encode_buffer_, thread_proto_indices_) {}
+
+ private:
+ std::array<std::byte, RequiredServiceBufferSize(kNumThreads)> encode_buffer_;
+ // + 1 is needed to account for extra index that comes with the first
+ // submessage start or the last submessage end.
+ Vector<size_t, kNumThreads + 1> thread_proto_indices_;
+};
+
+} // namespace pw::thread::proto
diff --git a/pw_thread/pw_thread_private/thread_snapshot_service.h b/pw_thread/pw_thread_private/thread_snapshot_service.h
new file mode 100644
index 000000000..330c7e67a
--- /dev/null
+++ b/pw_thread/pw_thread_private/thread_snapshot_service.h
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_log/log.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+
+namespace pw::thread::proto {
+
+void ErrorLog(Status status);
+
+Status DecodeThreadName(ConstByteSpan serialized_path,
+ ConstByteSpan& thread_name);
+
+} // namespace pw::thread::proto
diff --git a/pw_thread/pw_thread_protos/thread.proto b/pw_thread/pw_thread_protos/thread.proto
index bcfbb09b7..d49f6031b 100644
--- a/pw_thread/pw_thread_protos/thread.proto
+++ b/pw_thread/pw_thread_protos/thread.proto
@@ -13,7 +13,7 @@
// the License.
syntax = "proto3";
-package pw.thread;
+package pw.thread.proto;
import "pw_tokenizer/proto/options.proto";
@@ -106,5 +106,5 @@ message Thread {
// This message overlays the pw.snapshot.Snapshot proto. It's valid to encode
// this message to the same sink that a Snapshot proto is being written to.
message SnapshotThreadInfo {
- repeated pw.thread.Thread threads = 18;
+ repeated pw.thread.proto.Thread threads = 18;
}
diff --git a/pw_thread/pw_thread_protos/thread_snapshot_service.proto b/pw_thread/pw_thread_protos/thread_snapshot_service.proto
new file mode 100644
index 000000000..ef108b250
--- /dev/null
+++ b/pw_thread/pw_thread_protos/thread_snapshot_service.proto
@@ -0,0 +1,32 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+syntax = "proto3";
+
+package pw.thread.proto;
+
+import "pw_thread_protos/thread.proto";
+
+option java_package = "pw.thread.proto";
+option java_outer_classname = "Thread";
+
+message ThreadRequest {
+ // Thread information matched to the given name is returned. The intent
+ // is to support specific snapshots of a single thread.
+ optional bytes name = 1;
+}
+
+service ThreadSnapshotService {
+ // Returns peak stack usage across running threads in the device.
+ rpc GetPeakStackUsage(ThreadRequest) returns (stream SnapshotThreadInfo) {}
+}
diff --git a/pw_thread/py/BUILD.bazel b/pw_thread/py/BUILD.bazel
new file mode 100644
index 000000000..873ea5ad0
--- /dev/null
+++ b/pw_thread/py/BUILD.bazel
@@ -0,0 +1,47 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+package(default_visibility = ["//visibility:public"])
+
+# TODO(b/241456982): Not expected to build. We need a dependency on a
+# py_proto_library built from thread_proto, but that in turn depends on
+# creating a py_proto_library for tokenizer_proto.
+py_library(
+ name = "pw_thread",
+ srcs = [
+ "pw_thread/__init__.py",
+ "pw_thread/thread_analyzer.py",
+ ],
+ tags = ["manual"],
+ deps = [
+ "//pw_symbolizer/py:pw_symbolizer",
+ "//pw_thread:thread_proto_py_pb2",
+ "//pw_tokenizer/py:pw_tokenizer",
+ ],
+)
+
+# TODO(b/241456982): Not expected to build. We need a dependency on a
+# py_proto_library built from thread_proto, but that in turn depends on
+# creating a py_proto_library for tokenizer_proto.
+py_test(
+ name = "thread_analyzer_test",
+ srcs = [
+ "thread_analyzer_test.py",
+ ],
+ tags = ["manual"],
+ deps = [
+ ":pw_thread",
+ "//pw_thread:thread_proto_py_pb2",
+ ],
+)
diff --git a/pw_thread/py/BUILD.gn b/pw_thread/py/BUILD.gn
index f2d4bb446..0cd5dc117 100644
--- a/pw_thread/py/BUILD.gn
+++ b/pw_thread/py/BUILD.gn
@@ -36,4 +36,5 @@ pw_python_package("py") {
"..:protos.python",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_thread/py/pw_thread/thread_analyzer.py b/pw_thread/py/pw_thread/thread_analyzer.py
index 37a349570..a0e1a41bb 100644
--- a/pw_thread/py/pw_thread/thread_analyzer.py
+++ b/pw_thread/py/pw_thread/thread_analyzer.py
@@ -13,6 +13,7 @@
# the License.
"""Library to analyze and dump Thread protos and Thread snapshots into text."""
+import binascii
from typing import Optional, List, Mapping
import pw_tokenizer
from pw_symbolizer import LlvmSymbolizer, Symbolizer
@@ -30,9 +31,11 @@ THREAD_STATE_TO_STRING: Mapping[int, str] = {
}
-def process_snapshot(serialized_snapshot: bytes,
- tokenizer_db: Optional[pw_tokenizer.Detokenizer] = None,
- symbolizer: Optional[Symbolizer] = None) -> str:
+def process_snapshot(
+ serialized_snapshot: bytes,
+ tokenizer_db: Optional[pw_tokenizer.Detokenizer] = None,
+ symbolizer: Optional[Symbolizer] = None,
+) -> str:
"""Processes snapshot threads, producing a multi-line string."""
captured_threads = thread_pb2.SnapshotThreadInfo()
captured_threads.ParseFromString(serialized_snapshot)
@@ -40,7 +43,8 @@ def process_snapshot(serialized_snapshot: bytes,
symbolizer = LlvmSymbolizer()
return str(
- ThreadSnapshotAnalyzer(captured_threads, tokenizer_db, symbolizer))
+ ThreadSnapshotAnalyzer(captured_threads, tokenizer_db, symbolizer)
+ )
class ThreadInfo:
@@ -54,7 +58,7 @@ class ThreadInfo:
def _cpu_used_str(self) -> str:
if not self._thread.HasField('cpu_usage_hundredths'):
return 'unknown'
- cpu_last_percent = (self._thread.cpu_usage_hundredths / 100)
+ cpu_last_percent = self._thread.cpu_usage_hundredths / 100
return f'{cpu_last_percent:.2f}%'
def _stack_size_limit_limit_str(self) -> str:
@@ -80,42 +84,54 @@ class ThreadInfo:
high_used_str = f'{self.stack_pointer_est_peak()} bytes'
if not self.has_stack_size_limit():
return high_used_str
- high_water_mark_percent = (100 * self.stack_pointer_est_peak() /
- self.stack_size_limit())
+ high_water_mark_percent = (
+ 100 * self.stack_pointer_est_peak() / self.stack_size_limit()
+ )
high_used_str += f', {high_water_mark_percent:.2f}%'
return high_used_str
def _stack_used_range_str(self) -> str:
- start_str = (f'0x{self._thread.stack_start_pointer:08x}'
- if self._thread.HasField('stack_start_pointer') else
- ThreadInfo._UNKNOWN_VALUE_STR)
- end_str = (f'0x{self._thread.stack_pointer:08x}'
- if self._thread.HasField('stack_pointer') else
- ThreadInfo._UNKNOWN_VALUE_STR)
+ start_str = (
+ f'0x{self._thread.stack_start_pointer:08x}'
+ if self._thread.HasField('stack_start_pointer')
+ else ThreadInfo._UNKNOWN_VALUE_STR
+ )
+ end_str = (
+ f'0x{self._thread.stack_pointer:08x}'
+ if self._thread.HasField('stack_pointer')
+ else ThreadInfo._UNKNOWN_VALUE_STR
+ )
# TODO(amontanez): Would be nice to represent stack growth direction.
return f'{start_str} - {end_str} ({self._stack_used_str()})'
def _stack_limit_range_str(self) -> str:
- start_str = (f'0x{self._thread.stack_start_pointer:08x}'
- if self._thread.HasField('stack_start_pointer') else
- ThreadInfo._UNKNOWN_VALUE_STR)
- end_str = (f'0x{self._thread.stack_end_pointer:08x}'
- if self._thread.HasField('stack_end_pointer') else
- ThreadInfo._UNKNOWN_VALUE_STR)
+ start_str = (
+ f'0x{self._thread.stack_start_pointer:08x}'
+ if self._thread.HasField('stack_start_pointer')
+ else ThreadInfo._UNKNOWN_VALUE_STR
+ )
+ end_str = (
+ f'0x{self._thread.stack_end_pointer:08x}'
+ if self._thread.HasField('stack_end_pointer')
+ else ThreadInfo._UNKNOWN_VALUE_STR
+ )
# TODO(amontanez): Would be nice to represent stack growth direction.
return f'{start_str} - {end_str} ({self._stack_size_limit_limit_str()})'
def _stack_pointer_str(self) -> str:
- return (f'0x{self._thread.stack_end_pointer:08x}'
- if self._thread.HasField('stack_pointer') else
- ThreadInfo._UNKNOWN_VALUE_STR)
+ return (
+ f'0x{self._thread.stack_end_pointer:08x}'
+ if self._thread.HasField('stack_pointer')
+ else ThreadInfo._UNKNOWN_VALUE_STR
+ )
def has_stack_size_limit(self) -> bool:
"""Returns true if there's enough info to calculate stack size."""
- return (self._thread.HasField('stack_start_pointer')
- and self._thread.HasField('stack_end_pointer'))
+ return self._thread.HasField(
+ 'stack_start_pointer'
+ ) and self._thread.HasField('stack_end_pointer')
def stack_size_limit(self) -> int:
"""Returns the stack size limit in bytes.
@@ -124,13 +140,15 @@ class ThreadInfo:
has_stack_size_limit() must be true.
"""
assert self.has_stack_size_limit(), 'Missing stack size information'
- return abs(self._thread.stack_start_pointer -
- self._thread.stack_end_pointer)
+ return abs(
+ self._thread.stack_start_pointer - self._thread.stack_end_pointer
+ )
def has_stack_used(self) -> bool:
"""Returns true if there's enough info to calculate stack usage."""
- return (self._thread.HasField('stack_start_pointer')
- and self._thread.HasField('stack_pointer'))
+ return self._thread.HasField(
+ 'stack_start_pointer'
+ ) and self._thread.HasField('stack_pointer')
def stack_used(self) -> int:
"""Returns the stack usage in bytes.
@@ -139,15 +157,17 @@ class ThreadInfo:
has_stack_used() must be true.
"""
assert self.has_stack_used(), 'Missing stack usage information'
- return abs(self._thread.stack_start_pointer -
- self._thread.stack_pointer)
+ return abs(
+ self._thread.stack_start_pointer - self._thread.stack_pointer
+ )
def has_stack_pointer_est_peak(self) -> bool:
"""Returns true if there's enough info to calculate estimate
used stack.
"""
- return (self._thread.HasField('stack_start_pointer')
- and self._thread.HasField('stack_pointer_est_peak'))
+ return self._thread.HasField(
+ 'stack_start_pointer'
+ ) and self._thread.HasField('stack_pointer_est_peak')
def stack_pointer_est_peak(self) -> int:
"""Returns the max estimated used stack usage in bytes.
@@ -156,8 +176,10 @@ class ThreadInfo:
has_stack_estimated_used_bytes() must be true.
"""
assert self.has_stack_pointer_est_peak(), 'Missing stack est. peak'
- return abs(self._thread.stack_start_pointer -
- self._thread.stack_pointer_est_peak)
+ return abs(
+ self._thread.stack_start_pointer
+ - self._thread.stack_pointer_est_peak
+ )
def __str__(self) -> str:
output = [
@@ -172,13 +194,19 @@ class ThreadInfo:
class ThreadSnapshotAnalyzer:
"""This class simplifies dumping contents of a snapshot Metadata message."""
- def __init__(self,
- threads: thread_pb2.SnapshotThreadInfo,
- tokenizer_db: Optional[pw_tokenizer.Detokenizer] = None,
- symbolizer: Optional[Symbolizer] = None):
+
+ def __init__(
+ self,
+ threads: thread_pb2.SnapshotThreadInfo,
+ tokenizer_db: Optional[pw_tokenizer.Detokenizer] = None,
+ symbolizer: Optional[Symbolizer] = None,
+ ):
self._threads = threads.threads
- self._tokenizer_db = (tokenizer_db if tokenizer_db is not None else
- pw_tokenizer.Detokenizer(None))
+ self._tokenizer_db = (
+ tokenizer_db
+ if tokenizer_db is not None
+ else pw_tokenizer.Detokenizer(None)
+ )
if symbolizer is not None:
self._symbolizer = symbolizer
else:
@@ -218,10 +246,13 @@ class ThreadSnapshotAnalyzer:
output.append(thread_state_overview)
else:
thread_state_overview += ', '
- underline = (' ' * len(thread_state_overview) +
- '~' * len(requesting_thread.name.decode()))
- thread_state_overview += (f'{requesting_thread.name.decode()}'
- ' active at the time of capture.')
+ underline = ' ' * len(thread_state_overview) + '~' * len(
+ requesting_thread.name.decode()
+ )
+ thread_state_overview += (
+ f'{requesting_thread.name.decode()}'
+ ' active at the time of capture.'
+ )
output.append(thread_state_overview)
output.append(underline)
@@ -237,16 +268,26 @@ class ThreadSnapshotAnalyzer:
thread_name = thread.name.decode()
if not thread_name:
thread_name = '[unnamed thread]'
- thread_headline = ('Thread '
- f'({THREAD_STATE_TO_STRING[thread.state]}): '
- f'{thread_name}')
+ thread_headline = (
+ 'Thread '
+ f'({THREAD_STATE_TO_STRING[thread.state]}): '
+ f'{thread_name}'
+ )
if self.active_thread() == thread:
thread_headline += ' <-- [ACTIVE]'
output.append(thread_headline)
output.append(str(ThreadInfo(thread)))
if thread.raw_backtrace:
output.append(
- self._symbolizer.dump_stack_trace(thread.raw_backtrace))
+ self._symbolizer.dump_stack_trace(thread.raw_backtrace)
+ )
+ if thread.raw_stack:
+ output.append('Raw Stack')
+ output.append(
+ binascii.hexlify(thread.raw_stack, b'\n', 32).decode(
+ 'utf-8'
+ )
+ )
# Blank line between threads for nicer formatting.
output.append('')
diff --git a/pw_thread/py/thread_analyzer_test.py b/pw_thread/py/thread_analyzer_test.py
index 0a6844f07..4f380f300 100644
--- a/pw_thread/py/thread_analyzer_test.py
+++ b/pw_thread/py/thread_analyzer_test.py
@@ -21,13 +21,18 @@ from pw_thread_protos import thread_pb2
class ThreadInfoTest(unittest.TestCase):
"""Tests that the ThreadInfo class produces expected results."""
+
def test_empty_thread(self):
thread_info = ThreadInfo(thread_pb2.Thread())
expected = '\n'.join(
- ('Est CPU usage: unknown', 'Stack info',
- ' Current usage: 0x???????? - 0x???????? (size unknown)',
- ' Est peak usage: size unknown',
- ' Stack limits: 0x???????? - 0x???????? (size unknown)'))
+ (
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x???????? - 0x???????? (size unknown)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x???????? - 0x???????? (size unknown)',
+ )
+ )
self.assertFalse(thread_info.has_stack_size_limit())
self.assertFalse(thread_info.has_stack_used())
self.assertEqual(expected, str(thread_info))
@@ -38,10 +43,14 @@ class ThreadInfoTest(unittest.TestCase):
thread_info = ThreadInfo(thread)
expected = '\n'.join(
- ('Est CPU usage: 12.34%', 'Stack info',
- ' Current usage: 0x???????? - 0x???????? (size unknown)',
- ' Est peak usage: size unknown',
- ' Stack limits: 0x???????? - 0x???????? (size unknown)'))
+ (
+ 'Est CPU usage: 12.34%',
+ 'Stack info',
+ ' Current usage: 0x???????? - 0x???????? (size unknown)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x???????? - 0x???????? (size unknown)',
+ )
+ )
self.assertFalse(thread_info.has_stack_size_limit())
self.assertFalse(thread_info.has_stack_used())
self.assertEqual(expected, str(thread_info))
@@ -52,10 +61,14 @@ class ThreadInfoTest(unittest.TestCase):
thread_info = ThreadInfo(thread)
expected = '\n'.join(
- ('Est CPU usage: unknown', 'Stack info',
- ' Current usage: 0x???????? - 0x5ac6a86c (size unknown)',
- ' Est peak usage: size unknown',
- ' Stack limits: 0x???????? - 0x???????? (size unknown)'))
+ (
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x???????? - 0x5ac6a86c (size unknown)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x???????? - 0x???????? (size unknown)',
+ )
+ )
self.assertFalse(thread_info.has_stack_size_limit())
self.assertFalse(thread_info.has_stack_used())
self.assertEqual(expected, str(thread_info))
@@ -67,10 +80,14 @@ class ThreadInfoTest(unittest.TestCase):
thread_info = ThreadInfo(thread)
expected = '\n'.join(
- ('Est CPU usage: unknown', 'Stack info',
- ' Current usage: 0x5ac6b86c - 0x5ac6a86c (4096 bytes)',
- ' Est peak usage: size unknown',
- ' Stack limits: 0x5ac6b86c - 0x???????? (size unknown)'))
+ (
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x5ac6b86c - 0x5ac6a86c (4096 bytes)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x5ac6b86c - 0x???????? (size unknown)',
+ )
+ )
self.assertFalse(thread_info.has_stack_size_limit())
self.assertTrue(thread_info.has_stack_used())
self.assertEqual(expected, str(thread_info))
@@ -82,11 +99,17 @@ class ThreadInfoTest(unittest.TestCase):
thread.stack_pointer = 0x5AC6A86C
thread_info = ThreadInfo(thread)
+ # pylint: disable=line-too-long
expected = '\n'.join(
- ('Est CPU usage: unknown', 'Stack info',
- ' Current usage: 0x5ac6b86c - 0x5ac6a86c (4096 bytes, 50.00%)',
- ' Est peak usage: size unknown',
- ' Stack limits: 0x5ac6b86c - 0x5ac6986c (8192 bytes)'))
+ (
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x5ac6b86c - 0x5ac6a86c (4096 bytes, 50.00%)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x5ac6b86c - 0x5ac6986c (8192 bytes)',
+ )
+ )
+ # pylint: enable=line-too-long
self.assertTrue(thread_info.has_stack_size_limit())
self.assertTrue(thread_info.has_stack_used())
self.assertEqual(expected, str(thread_info))
@@ -94,6 +117,7 @@ class ThreadInfoTest(unittest.TestCase):
class ThreadSnapshotAnalyzerTest(unittest.TestCase):
"""Tests that the ThreadSnapshotAnalyzer class produces expected results."""
+
def test_no_threads(self):
analyzer = ThreadSnapshotAnalyzer(thread_pb2.SnapshotThreadInfo())
self.assertEqual('', str(analyzer))
@@ -101,18 +125,20 @@ class ThreadSnapshotAnalyzerTest(unittest.TestCase):
def test_one_empty_thread(self):
snapshot = thread_pb2.SnapshotThreadInfo()
snapshot.threads.append(thread_pb2.Thread())
- expected = '\n'.join((
- 'Thread State',
- ' 1 thread running.',
- '',
- 'Thread (UNKNOWN): [unnamed thread]',
- 'Est CPU usage: unknown',
- 'Stack info',
- ' Current usage: 0x???????? - 0x???????? (size unknown)',
- ' Est peak usage: size unknown',
- ' Stack limits: 0x???????? - 0x???????? (size unknown)',
- '',
- ))
+ expected = '\n'.join(
+ (
+ 'Thread State',
+ ' 1 thread running.',
+ '',
+ 'Thread (UNKNOWN): [unnamed thread]',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x???????? - 0x???????? (size unknown)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x???????? - 0x???????? (size unknown)',
+ '',
+ )
+ )
analyzer = ThreadSnapshotAnalyzer(snapshot)
self.assertEqual(analyzer.active_thread(), None)
self.assertEqual(str(ThreadSnapshotAnalyzer(snapshot)), expected)
@@ -124,38 +150,42 @@ class ThreadSnapshotAnalyzerTest(unittest.TestCase):
temp_thread = thread_pb2.Thread()
temp_thread.name = 'Idle'.encode()
temp_thread.state = thread_pb2.ThreadState.Enum.READY
- temp_thread.stack_start_pointer = 0x2001ac00
- temp_thread.stack_end_pointer = 0x2001aa00
- temp_thread.stack_pointer = 0x2001ab0c
- temp_thread.stack_pointer_est_peak = 0x2001aa00
+ temp_thread.stack_start_pointer = 0x2001AC00
+ temp_thread.stack_end_pointer = 0x2001AA00
+ temp_thread.stack_pointer = 0x2001AB0C
+ temp_thread.stack_pointer_est_peak = 0x2001AA00
snapshot.threads.append(temp_thread)
temp_thread = thread_pb2.Thread()
temp_thread.name = 'Alice'.encode()
- temp_thread.stack_start_pointer = 0x2001b000
- temp_thread.stack_pointer = 0x2001ae20
+ temp_thread.stack_start_pointer = 0x2001B000
+ temp_thread.stack_pointer = 0x2001AE20
temp_thread.state = thread_pb2.ThreadState.Enum.BLOCKED
snapshot.threads.append(temp_thread)
- expected = '\n'.join((
- 'Thread State',
- ' 2 threads running.',
- '',
- 'Thread (READY): Idle',
- 'Est CPU usage: unknown',
- 'Stack info',
- ' Current usage: 0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
- ' Est peak usage: 512 bytes, 100.00%',
- ' Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)',
- '',
- 'Thread (BLOCKED): Alice',
- 'Est CPU usage: unknown',
- 'Stack info',
- ' Current usage: 0x2001b000 - 0x2001ae20 (480 bytes)',
- ' Est peak usage: size unknown',
- ' Stack limits: 0x2001b000 - 0x???????? (size unknown)',
- '',
- ))
+ # pylint: disable=line-too-long
+ expected = '\n'.join(
+ (
+ 'Thread State',
+ ' 2 threads running.',
+ '',
+ 'Thread (READY): Idle',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
+ ' Est peak usage: 512 bytes, 100.00%',
+ ' Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)',
+ '',
+ 'Thread (BLOCKED): Alice',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x2001b000 - 0x2001ae20 (480 bytes)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x2001b000 - 0x???????? (size unknown)',
+ '',
+ )
+ )
+ # pylint: enable=line-too-long
analyzer = ThreadSnapshotAnalyzer(snapshot)
self.assertEqual(analyzer.active_thread(), None)
self.assertEqual(str(ThreadSnapshotAnalyzer(snapshot)), expected)
@@ -167,40 +197,44 @@ class ThreadSnapshotAnalyzerTest(unittest.TestCase):
temp_thread = thread_pb2.Thread()
temp_thread.name = 'Idle'.encode()
temp_thread.state = thread_pb2.ThreadState.Enum.READY
- temp_thread.stack_start_pointer = 0x2001ac00
- temp_thread.stack_end_pointer = 0x2001aa00
- temp_thread.stack_pointer = 0x2001ab0c
- temp_thread.stack_pointer_est_peak = 0x2001aa00
+ temp_thread.stack_start_pointer = 0x2001AC00
+ temp_thread.stack_end_pointer = 0x2001AA00
+ temp_thread.stack_pointer = 0x2001AB0C
+ temp_thread.stack_pointer_est_peak = 0x2001AA00
snapshot.threads.append(temp_thread)
temp_thread = thread_pb2.Thread()
temp_thread.name = 'Main/Handler'.encode()
- temp_thread.stack_start_pointer = 0x2001b000
- temp_thread.stack_pointer = 0x2001ae20
+ temp_thread.stack_start_pointer = 0x2001B000
+ temp_thread.stack_pointer = 0x2001AE20
temp_thread.state = thread_pb2.ThreadState.Enum.INTERRUPT_HANDLER
snapshot.threads.append(temp_thread)
- expected = '\n'.join((
- 'Thread State',
- ' 2 threads running, Main/Handler active at the time of capture.',
- ' ~~~~~~~~~~~~',
- '',
- # Ensure the active thread is moved to the top of the list.
- 'Thread (INTERRUPT_HANDLER): Main/Handler <-- [ACTIVE]',
- 'Est CPU usage: unknown',
- 'Stack info',
- ' Current usage: 0x2001b000 - 0x2001ae20 (480 bytes)',
- ' Est peak usage: size unknown',
- ' Stack limits: 0x2001b000 - 0x???????? (size unknown)',
- '',
- 'Thread (READY): Idle',
- 'Est CPU usage: unknown',
- 'Stack info',
- ' Current usage: 0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
- ' Est peak usage: 512 bytes, 100.00%',
- ' Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)',
- '',
- ))
+ # pylint: disable=line-too-long
+ expected = '\n'.join(
+ (
+ 'Thread State',
+ ' 2 threads running, Main/Handler active at the time of capture.',
+ ' ~~~~~~~~~~~~',
+ '',
+ # Ensure the active thread is moved to the top of the list.
+ 'Thread (INTERRUPT_HANDLER): Main/Handler <-- [ACTIVE]',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x2001b000 - 0x2001ae20 (480 bytes)',
+ ' Est peak usage: size unknown',
+ ' Stack limits: 0x2001b000 - 0x???????? (size unknown)',
+ '',
+ 'Thread (READY): Idle',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
+ ' Est peak usage: 512 bytes, 100.00%',
+ ' Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)',
+ '',
+ )
+ )
+ # pylint: enable=line-too-long
analyzer = ThreadSnapshotAnalyzer(snapshot)
self.assertEqual(analyzer.active_thread(), temp_thread)
self.assertEqual(str(ThreadSnapshotAnalyzer(snapshot)), expected)
@@ -212,42 +246,46 @@ class ThreadSnapshotAnalyzerTest(unittest.TestCase):
temp_thread = thread_pb2.Thread()
temp_thread.name = 'Idle'.encode()
temp_thread.state = thread_pb2.ThreadState.Enum.READY
- temp_thread.stack_start_pointer = 0x2001ac00
- temp_thread.stack_end_pointer = 0x2001aa00
- temp_thread.stack_pointer = 0x2001ab0c
- temp_thread.stack_pointer_est_peak = 0x2001ac00 + 0x100
+ temp_thread.stack_start_pointer = 0x2001AC00
+ temp_thread.stack_end_pointer = 0x2001AA00
+ temp_thread.stack_pointer = 0x2001AB0C
+ temp_thread.stack_pointer_est_peak = 0x2001AC00 + 0x100
snapshot.threads.append(temp_thread)
temp_thread = thread_pb2.Thread()
temp_thread.name = 'Main/Handler'.encode()
temp_thread.active = True
- temp_thread.stack_start_pointer = 0x2001b000
- temp_thread.stack_pointer = 0x2001ae20
- temp_thread.stack_pointer_est_peak = 0x2001b000 + 0x200
+ temp_thread.stack_start_pointer = 0x2001B000
+ temp_thread.stack_pointer = 0x2001AE20
+ temp_thread.stack_pointer_est_peak = 0x2001B000 + 0x200
temp_thread.state = thread_pb2.ThreadState.Enum.INTERRUPT_HANDLER
snapshot.threads.append(temp_thread)
- expected = '\n'.join((
- 'Thread State',
- ' 2 threads running, Main/Handler active at the time of capture.',
- ' ~~~~~~~~~~~~',
- '',
- # Ensure the active thread is moved to the top of the list.
- 'Thread (INTERRUPT_HANDLER): Main/Handler <-- [ACTIVE]',
- 'Est CPU usage: unknown',
- 'Stack info',
- ' Current usage: 0x2001b000 - 0x2001ae20 (480 bytes)',
- ' Est peak usage: 512 bytes',
- ' Stack limits: 0x2001b000 - 0x???????? (size unknown)',
- '',
- 'Thread (READY): Idle',
- 'Est CPU usage: unknown',
- 'Stack info',
- ' Current usage: 0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
- ' Est peak usage: 256 bytes, 50.00%',
- ' Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)',
- '',
- ))
+ # pylint: disable=line-too-long
+ expected = '\n'.join(
+ (
+ 'Thread State',
+ ' 2 threads running, Main/Handler active at the time of capture.',
+ ' ~~~~~~~~~~~~',
+ '',
+ # Ensure the active thread is moved to the top of the list.
+ 'Thread (INTERRUPT_HANDLER): Main/Handler <-- [ACTIVE]',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x2001b000 - 0x2001ae20 (480 bytes)',
+ ' Est peak usage: 512 bytes',
+ ' Stack limits: 0x2001b000 - 0x???????? (size unknown)',
+ '',
+ 'Thread (READY): Idle',
+ 'Est CPU usage: unknown',
+ 'Stack info',
+ ' Current usage: 0x2001ac00 - 0x2001ab0c (244 bytes, 47.66%)',
+ ' Est peak usage: 256 bytes, 50.00%',
+ ' Stack limits: 0x2001ac00 - 0x2001aa00 (512 bytes)',
+ '',
+ )
+ )
+ # pylint: enable=line-too-long
analyzer = ThreadSnapshotAnalyzer(snapshot)
# Ensure the active thread is found.
diff --git a/pw_thread/snapshot.cc b/pw_thread/snapshot.cc
index 9a68338f2..5b15239ec 100644
--- a/pw_thread/snapshot.cc
+++ b/pw_thread/snapshot.cc
@@ -16,6 +16,7 @@
#include "pw_thread/snapshot.h"
+#include <cinttypes>
#include <string_view>
#include "pw_bytes/span.h"
@@ -29,13 +30,15 @@
namespace pw::thread {
Status SnapshotStack(const StackContext& stack,
- Thread::StreamEncoder& encoder,
+ proto::Thread::StreamEncoder& encoder,
const ProcessThreadStackCallback& thread_stack_callback) {
- // TODO(pwbug/422): Add support for ascending stacks.
- encoder.WriteStackStartPointer(stack.stack_high_addr);
- encoder.WriteStackEndPointer(stack.stack_low_addr);
- encoder.WriteStackPointer(stack.stack_pointer);
- PW_LOG_DEBUG("Active stack: 0x%08x-0x%08x (%ld bytes)",
+ // TODO(b/234890430): Add support for ascending stacks.
+ encoder.WriteStackStartPointer(stack.stack_high_addr).IgnoreError();
+ encoder.WriteStackEndPointer(stack.stack_low_addr).IgnoreError();
+ encoder.WriteStackPointer(stack.stack_pointer).IgnoreError();
+ // The PRIxPTR is an appropriate format specifier for hex uintptr_t values
+ // https://stackoverflow.com/a/5796039/1224002
+ PW_LOG_DEBUG("Active stack: 0x%08" PRIxPTR "-0x%08" PRIxPTR " (%ld bytes)",
stack.stack_high_addr,
stack.stack_pointer,
static_cast<long>(stack.stack_high_addr) -
@@ -43,14 +46,15 @@ Status SnapshotStack(const StackContext& stack,
if (stack.stack_pointer_est_peak.has_value()) {
const uintptr_t stack_pointer_est_peak =
stack.stack_pointer_est_peak.value();
- encoder.WriteStackPointerEstPeak(stack_pointer_est_peak);
- PW_LOG_DEBUG("Est peak stack: 0x%08x-0x%08x (%ld bytes)",
+ encoder.WriteStackPointerEstPeak(stack_pointer_est_peak).IgnoreError();
+ PW_LOG_DEBUG("Est peak stack: 0x%08" PRIxPTR "-0x%08" PRIxPTR
+ " (%ld bytes)",
stack.stack_high_addr,
stack_pointer_est_peak,
static_cast<long>(stack.stack_high_addr) -
static_cast<long>(stack_pointer_est_peak));
}
- PW_LOG_DEBUG("Stack Limits: 0x%08x-0x%08x (%ld bytes)",
+ PW_LOG_DEBUG("Stack Limits: 0x%08" PRIxPTR "-0x%08" PRIxPTR " (%ld bytes)",
stack.stack_low_addr,
stack.stack_high_addr,
static_cast<long>(stack.stack_high_addr) -
diff --git a/pw_thread/thread_info_test.cc b/pw_thread/thread_info_test.cc
new file mode 100644
index 000000000..b54b4de8e
--- /dev/null
+++ b/pw_thread/thread_info_test.cc
@@ -0,0 +1,99 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_thread/thread_info.h"
+
+#include <optional>
+
+#include "gtest/gtest.h"
+#include "pw_span/span.h"
+
+namespace pw::thread {
+namespace {
+
+TEST(ThreadInfo, ThreadName) {
+ ThreadInfo thread_info;
+ // Getter.
+ EXPECT_EQ(thread_info.thread_name(), std::nullopt);
+ char buffer[] = "hello, world";
+ span<char, 13> name_string(buffer);
+ span<const std::byte> name =
+ span(reinterpret_cast<const std::byte*>(name_string.data()), 13);
+ // Setter.
+ thread_info.set_thread_name(name);
+ EXPECT_EQ(thread_info.thread_name().value().data(), name.data());
+ // Clear.
+ thread_info.clear_thread_name();
+ EXPECT_EQ(thread_info.thread_name(), std::nullopt);
+}
+
+TEST(ThreadInfo, StackLowAddr) {
+ ThreadInfo thread_info;
+ // Getter.
+ EXPECT_EQ(thread_info.stack_low_addr(), std::nullopt);
+ const unsigned int* null_addr = nullptr;
+
+ const unsigned int example_addr = 12345678u;
+ const unsigned int* addr = &example_addr;
+ // Setter.
+ thread_info.set_stack_low_addr(reinterpret_cast<uintptr_t>(null_addr));
+ EXPECT_EQ(thread_info.stack_low_addr(),
+ reinterpret_cast<uintptr_t>(null_addr));
+ thread_info.set_stack_low_addr(reinterpret_cast<uintptr_t>(addr));
+ EXPECT_EQ(thread_info.stack_low_addr(), reinterpret_cast<uintptr_t>(addr));
+ // Clear.
+ thread_info.clear_stack_low_addr();
+ EXPECT_EQ(thread_info.stack_low_addr(), std::nullopt);
+}
+
+TEST(ThreadInfo, StackHighAddr) {
+ ThreadInfo thread_info;
+ // Getter.
+ EXPECT_EQ(thread_info.stack_high_addr(), std::nullopt);
+ const unsigned int* null_addr = nullptr;
+
+ const unsigned int example_addr = 12345678u;
+ const unsigned int* addr = &example_addr;
+ // Setter.
+ thread_info.set_stack_high_addr(reinterpret_cast<uintptr_t>(null_addr));
+ EXPECT_EQ(thread_info.stack_high_addr(),
+ reinterpret_cast<uintptr_t>(null_addr));
+ thread_info.set_stack_high_addr(reinterpret_cast<uintptr_t>(addr));
+ EXPECT_EQ(thread_info.stack_high_addr(), reinterpret_cast<uintptr_t>(addr));
+ // Clear.
+ thread_info.clear_stack_high_addr();
+ EXPECT_EQ(thread_info.stack_high_addr(), std::nullopt);
+}
+
+TEST(ThreadInfo, PeakAddr) {
+ ThreadInfo thread_info;
+ // Getter.
+ EXPECT_EQ(thread_info.stack_peak_addr(), std::nullopt);
+ const unsigned int* null_addr = nullptr;
+
+ const unsigned int example_addr = 12345678u;
+ const unsigned int* addr = &example_addr;
+ // Setter.
+ thread_info.set_stack_peak_addr(reinterpret_cast<uintptr_t>(null_addr));
+ EXPECT_EQ(thread_info.stack_peak_addr(),
+ reinterpret_cast<uintptr_t>(null_addr));
+ thread_info.set_stack_peak_addr(reinterpret_cast<uintptr_t>(addr));
+ EXPECT_EQ(thread_info.stack_peak_addr(), reinterpret_cast<uintptr_t>(addr));
+ // Clear.
+ thread_info.clear_stack_peak_addr();
+ EXPECT_EQ(thread_info.stack_peak_addr(), std::nullopt);
+}
+
+} // namespace
+} // namespace pw::thread
diff --git a/pw_thread/thread_snapshot_service.cc b/pw_thread/thread_snapshot_service.cc
new file mode 100644
index 000000000..8fc20011a
--- /dev/null
+++ b/pw_thread/thread_snapshot_service.cc
@@ -0,0 +1,197 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_thread/thread_snapshot_service.h"
+
+#include "pw_containers/vector.h"
+#include "pw_log/log.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+#include "pw_thread/thread_info.h"
+#include "pw_thread/thread_iteration.h"
+#include "pw_thread_private/thread_snapshot_service.h"
+#include "pw_thread_protos/thread.pwpb.h"
+#include "pw_thread_protos/thread_snapshot_service.pwpb.h"
+
+namespace pw::thread::proto {
+
+Status ProtoEncodeThreadInfo(pwpb::SnapshotThreadInfo::StreamEncoder& encoder,
+ const ThreadInfo& thread_info) {
+ // Grab the next available Thread slot to write to in the response.
+ pwpb::Thread::StreamEncoder proto_encoder = encoder.GetThreadsEncoder();
+ if (thread_info.thread_name().has_value()) {
+ PW_TRY(proto_encoder.WriteName(thread_info.thread_name().value()));
+ } else {
+ // Name is necessary to identify thread.
+ return Status::FailedPrecondition();
+ }
+ if (thread_info.stack_low_addr().has_value()) {
+ PW_TRY(proto_encoder.WriteStackEndPointer(
+ thread_info.stack_low_addr().value()));
+ }
+ if (thread_info.stack_high_addr().has_value()) {
+ PW_TRY(proto_encoder.WriteStackStartPointer(
+ thread_info.stack_high_addr().value()));
+ } else {
+ // Need stack start pointer to contextualize estimated peak.
+ return Status::FailedPrecondition();
+ }
+ if (thread_info.stack_pointer().has_value()) {
+ PW_TRY(
+ proto_encoder.WriteStackPointer(thread_info.stack_pointer().value()));
+ }
+
+ if (thread_info.stack_peak_addr().has_value()) {
+ PW_TRY(proto_encoder.WriteStackPointerEstPeak(
+ thread_info.stack_peak_addr().value()));
+ } else {
+ // Peak stack usage reporting is not supported.
+ return Status::Unimplemented();
+ }
+
+ return proto_encoder.status();
+}
+
+void ErrorLog(Status status) {
+ if (status == Status::Unimplemented()) {
+ PW_LOG_ERROR(
+ "Peak stack usage reporting not supported by your current OS or "
+ "configuration.");
+ } else if (status == Status::FailedPrecondition()) {
+ PW_LOG_ERROR("Thread missing information needed by service.");
+ } else if (status == Status::ResourceExhausted()) {
+ PW_LOG_ERROR("Buffer capacity limit exceeded.");
+ } else if (status != OkStatus()) {
+ PW_LOG_ERROR(
+ "Failure with error code %d, RPC service was unable to capture thread "
+ "information",
+ status.code());
+ }
+}
+
+Status DecodeThreadName(ConstByteSpan serialized_path,
+ ConstByteSpan& thread_name) {
+ protobuf::Decoder decoder(serialized_path);
+ Status status;
+ while (decoder.Next().ok()) {
+ switch (decoder.FieldNumber()) {
+ case static_cast<uint32_t>(pwpb::Thread::Fields::kName): {
+ status.Update(decoder.ReadBytes(&thread_name));
+ }
+ }
+ }
+ return status;
+}
+
+void ThreadSnapshotService::GetPeakStackUsage(
+ ConstByteSpan request, rpc::RawServerWriter& response_writer) {
+ // For now, ignore the request and just stream all the thread information
+ // back.
+ struct IterationInfo {
+ pwpb::SnapshotThreadInfo::MemoryEncoder encoder;
+ Status status;
+ ConstByteSpan name;
+
+ // For sending out data by chunks.
+ Vector<size_t>& thread_proto_indices;
+ };
+
+ ConstByteSpan name_request;
+ if (!request.empty()) {
+ if (const auto status = DecodeThreadName(request, name_request);
+ !status.ok()) {
+ PW_LOG_ERROR("Service unable to decode thread name with error code %d",
+ status.code());
+ }
+ }
+
+ IterationInfo iteration_info{
+ pwpb::SnapshotThreadInfo::MemoryEncoder(encode_buffer_),
+ OkStatus(),
+ name_request,
+ thread_proto_indices_};
+
+ iteration_info.thread_proto_indices.clear();
+ iteration_info.thread_proto_indices.push_back(iteration_info.encoder.size());
+
+ auto cb = [&iteration_info](const ThreadInfo& thread_info) {
+ if (!iteration_info.name.empty() && thread_info.thread_name().has_value()) {
+ if (std::equal(thread_info.thread_name().value().begin(),
+ thread_info.thread_name().value().end(),
+ iteration_info.name.begin())) {
+ iteration_info.status.Update(
+ ProtoEncodeThreadInfo(iteration_info.encoder, thread_info));
+ iteration_info.thread_proto_indices.push_back(
+ iteration_info.encoder.size());
+ return false;
+ }
+ } else {
+ iteration_info.status.Update(
+ ProtoEncodeThreadInfo(iteration_info.encoder, thread_info));
+ iteration_info.thread_proto_indices.push_back(
+ iteration_info.encoder.size());
+ }
+ return iteration_info.status.ok();
+ };
+ if (const auto status = ForEachThread(cb); !status.ok()) {
+ PW_LOG_ERROR("Failed to capture thread information, error %d",
+ status.code());
+ }
+
+ // This logging action is external to thread iteration because it is
+ // unsafe to log within ForEachThread() when the scheduler is disabled.
+ ErrorLog(iteration_info.status);
+
+ Status status;
+ if (iteration_info.encoder.size() && iteration_info.status.ok()) {
+ // Must subtract 1 because the last boundary index of thread_proto_indices
+ // is the end of the last submessage, and NOT the start of another.
+ size_t last_start_index = iteration_info.thread_proto_indices.size() - 1;
+ for (size_t i = 0; i < last_start_index; i += num_bundled_threads_) {
+ const size_t num_threads =
+ std::min(num_bundled_threads_, last_start_index - i);
+
+ // Sending out a bundle of threads at a time.
+ const size_t bundle_size =
+ iteration_info.thread_proto_indices[i + num_threads] -
+ iteration_info.thread_proto_indices[i];
+
+ ConstByteSpan thread =
+ ConstByteSpan(iteration_info.encoder.data() +
+ iteration_info.thread_proto_indices[i],
+ bundle_size);
+
+ if (bundle_size) {
+ status.Update(response_writer.Write(thread));
+ }
+ if (!status.ok()) {
+ PW_LOG_ERROR(
+ "Failed to send response with error code %d, packet may be too "
+ "large to send",
+ status.code());
+ }
+ }
+ }
+
+ if (response_writer.Finish(status) != OkStatus()) {
+ PW_LOG_ERROR(
+ "Failed to close stream for GetPeakStackUsage() with error code %d",
+ status.code());
+ }
+}
+
+} // namespace pw::thread::proto
diff --git a/pw_thread/thread_snapshot_service_test.cc b/pw_thread/thread_snapshot_service_test.cc
new file mode 100644
index 000000000..9b37d9c79
--- /dev/null
+++ b/pw_thread/thread_snapshot_service_test.cc
@@ -0,0 +1,206 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_thread/thread_snapshot_service.h"
+
+#include "gtest/gtest.h"
+#include "pw_protobuf/decoder.h"
+#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
+#include "pw_thread/thread_info.h"
+#include "pw_thread/thread_iteration.h"
+#include "pw_thread_private/thread_snapshot_service.h"
+#include "pw_thread_protos/thread.pwpb.h"
+#include "pw_thread_protos/thread_snapshot_service.pwpb.h"
+
+namespace pw::thread::proto {
+namespace {
+
+// Iterates through each proto encoded thread in the buffer.
+bool EncodedThreadExists(ConstByteSpan serialized_thread_buffer,
+ ConstByteSpan thread_name) {
+ protobuf::Decoder decoder(serialized_thread_buffer);
+ while (decoder.Next().ok()) {
+ switch (decoder.FieldNumber()) {
+ case static_cast<uint32_t>(proto::SnapshotThreadInfo::Fields::kThreads): {
+ ConstByteSpan thread_buffer;
+ EXPECT_EQ(OkStatus(), decoder.ReadBytes(&thread_buffer));
+ ConstByteSpan encoded_name;
+ EXPECT_EQ(OkStatus(), DecodeThreadName(thread_buffer, encoded_name));
+ if (encoded_name.size() == thread_name.size()) {
+ if (std::equal(thread_name.begin(),
+ thread_name.end(),
+ encoded_name.begin())) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
+
+ThreadInfo CreateThreadInfoObject(std::optional<ConstByteSpan> name,
+ std::optional<uintptr_t> low_addr,
+ std::optional<uintptr_t> high_addr,
+ std::optional<uintptr_t> peak_addr) {
+ ThreadInfo thread_info;
+
+ if (name.has_value()) {
+ thread_info.set_thread_name(name.value());
+ }
+ if (low_addr.has_value()) {
+ thread_info.set_stack_low_addr(low_addr.value());
+ }
+ if (high_addr.has_value()) {
+ thread_info.set_stack_high_addr(high_addr.value());
+ }
+ if (peak_addr.has_value()) {
+ thread_info.set_stack_peak_addr(peak_addr.value());
+ }
+
+ return thread_info;
+}
+
+// Test creates a custom thread info object and proto encodes. Checks that the
+// custom object is encoded properly.
+TEST(ThreadSnapshotService, DecodeSingleThreadInfoObject) {
+ std::array<std::byte, RequiredServiceBufferSize(1)> encode_buffer;
+
+ proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+
+ ConstByteSpan name = bytes::String("MyThread\0");
+ ThreadInfo thread_info = CreateThreadInfoObject(
+ std::make_optional(name), /* thread name */
+ std::make_optional(
+ static_cast<uintptr_t>(12345678u)) /* stack low address */,
+ std::make_optional(static_cast<uintptr_t>(0u)) /* stack high address */,
+ std::make_optional(
+ static_cast<uintptr_t>(987654321u)) /* stack peak address */);
+
+ EXPECT_EQ(OkStatus(), ProtoEncodeThreadInfo(encoder, thread_info));
+
+ ConstByteSpan response_span(encoder);
+ EXPECT_TRUE(
+ EncodedThreadExists(response_span, thread_info.thread_name().value()));
+}
+
+TEST(ThreadSnapshotService, DecodeMultipleThreadInfoObjects) {
+ std::array<std::byte, RequiredServiceBufferSize(3)> encode_buffer;
+
+ proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+
+ ConstByteSpan name = bytes::String("MyThread1\0");
+ ThreadInfo thread_info_1 =
+ CreateThreadInfoObject(std::make_optional(name),
+ std::make_optional(static_cast<uintptr_t>(123u)),
+ std::make_optional(static_cast<uintptr_t>(1023u)),
+ std::make_optional(static_cast<uintptr_t>(321u)));
+
+ name = bytes::String("MyThread2\0");
+ ThreadInfo thread_info_2 = CreateThreadInfoObject(
+ std::make_optional(name),
+ std::make_optional(static_cast<uintptr_t>(1000u)),
+ std::make_optional(static_cast<uintptr_t>(999999u)),
+ std::make_optional(static_cast<uintptr_t>(0u)));
+
+ name = bytes::String("MyThread3\0");
+ ThreadInfo thread_info_3 =
+ CreateThreadInfoObject(std::make_optional(name),
+ std::make_optional(static_cast<uintptr_t>(123u)),
+ std::make_optional(static_cast<uintptr_t>(1023u)),
+ std::make_optional(static_cast<uintptr_t>(321u)));
+
+ // Encode out of order.
+ EXPECT_EQ(OkStatus(), ProtoEncodeThreadInfo(encoder, thread_info_3));
+ EXPECT_EQ(OkStatus(), ProtoEncodeThreadInfo(encoder, thread_info_1));
+ EXPECT_EQ(OkStatus(), ProtoEncodeThreadInfo(encoder, thread_info_2));
+
+ ConstByteSpan response_span(encoder);
+ EXPECT_TRUE(
+ EncodedThreadExists(response_span, thread_info_1.thread_name().value()));
+ EXPECT_TRUE(
+ EncodedThreadExists(response_span, thread_info_2.thread_name().value()));
+ EXPECT_TRUE(
+ EncodedThreadExists(response_span, thread_info_3.thread_name().value()));
+}
+
+TEST(ThreadSnapshotService, DefaultBufferSize) {
+ static std::array<std::byte, RequiredServiceBufferSize()> encode_buffer;
+
+ proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+
+ ConstByteSpan name = bytes::String("MyThread\0");
+ std::optional<uintptr_t> example_addr =
+ std::make_optional(std::numeric_limits<uintptr_t>::max());
+
+ ThreadInfo thread_info = CreateThreadInfoObject(
+ std::make_optional(name), example_addr, example_addr, example_addr);
+
+ for (int i = 0; i < PW_THREAD_MAXIMUM_THREADS; i++) {
+ EXPECT_EQ(OkStatus(), ProtoEncodeThreadInfo(encoder, thread_info));
+ }
+
+ ConstByteSpan response_span(encoder);
+ EXPECT_TRUE(
+ EncodedThreadExists(response_span, thread_info.thread_name().value()));
+}
+
+TEST(ThreadSnapshotService, FailedPrecondition) {
+ static std::array<std::byte, RequiredServiceBufferSize(1)> encode_buffer;
+
+ proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+
+ ThreadInfo thread_info_no_name = CreateThreadInfoObject(
+ std::nullopt,
+ std::make_optional(static_cast<uintptr_t>(1111111111u)),
+ std::make_optional(static_cast<uintptr_t>(2222222222u)),
+ std::make_optional(static_cast<uintptr_t>(3333333333u)));
+ Status status = ProtoEncodeThreadInfo(encoder, thread_info_no_name);
+ EXPECT_EQ(status, Status::FailedPrecondition());
+ // Expected log: "Thread missing information needed by service."
+ ErrorLog(status);
+
+ // Same error log as above.
+ ConstByteSpan name = bytes::String("MyThread\0");
+ ThreadInfo thread_info_no_high_addr = CreateThreadInfoObject(
+ std::make_optional(name),
+ std::make_optional(static_cast<uintptr_t>(1111111111u)),
+ std::nullopt,
+ std::make_optional(static_cast<uintptr_t>(3333333333u)));
+ EXPECT_EQ(ProtoEncodeThreadInfo(encoder, thread_info_no_high_addr),
+ Status::FailedPrecondition());
+}
+
+TEST(ThreadSnapshotService, Unimplemented) {
+ static std::array<std::byte, RequiredServiceBufferSize(1)> encode_buffer;
+
+ proto::SnapshotThreadInfo::MemoryEncoder encoder(encode_buffer);
+
+ ConstByteSpan name = bytes::String("MyThread\0");
+ ThreadInfo thread_info_no_peak_addr =
+ CreateThreadInfoObject(std::make_optional(name),
+ std::make_optional(static_cast<uintptr_t>(0u)),
+ std::make_optional(static_cast<uintptr_t>(0u)),
+ std::nullopt);
+
+ Status status = ProtoEncodeThreadInfo(encoder, thread_info_no_peak_addr);
+ EXPECT_EQ(status, Status::Unimplemented());
+ // Expected log: "Peak stack usage reporting not supported by your current OS
+ // or configuration."
+ ErrorLog(status);
+}
+
+} // namespace
+} // namespace pw::thread::proto
diff --git a/pw_thread_embos/BUILD.bazel b/pw_thread_embos/BUILD.bazel
index da4635aba..e4329bd5a 100644
--- a/pw_thread_embos/BUILD.bazel
+++ b/pw_thread_embos/BUILD.bazel
@@ -24,14 +24,17 @@ licenses(["notice"])
pw_cc_library(
name = "id_headers",
hdrs = [
+ "id_public_overrides/pw_thread_backend/id_inline.h",
+ "id_public_overrides/pw_thread_backend/id_native.h",
"public/pw_thread_embos/id_inline.h",
"public/pw_thread_embos/id_native.h",
- "public_overrides/pw_thread_backend/id_inline.h",
- "public_overrides/pw_thread_backend/id_native.h",
],
includes = [
+ "id_public_overrides",
"public",
- "public_overrides",
+ ],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:embos",
],
)
@@ -41,7 +44,7 @@ pw_cc_library(
":id_headers",
"//pw_thread:id_facade",
],
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
)
@@ -49,11 +52,11 @@ pw_cc_library(
name = "sleep_headers",
hdrs = [
"public/pw_thread_embos/sleep_inline.h",
- "public_overrides/pw_thread_backend/sleep_inline.h",
+ "sleep_public_overrides/pw_thread_backend/sleep_inline.h",
],
includes = [
"public",
- "public_overrides",
+ "sleep_public_overrides",
],
deps = [
"//pw_chrono:system_clock",
@@ -72,7 +75,7 @@ pw_cc_library(
"//pw_chrono_embos:system_clock_headers",
"//pw_thread:sleep_facade",
],
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
)
@@ -85,20 +88,20 @@ pw_cc_library(
"public/pw_thread_embos/options.h",
"public/pw_thread_embos/thread_inline.h",
"public/pw_thread_embos/thread_native.h",
- "public_overrides/pw_thread_backend/thread_inline.h",
- "public_overrides/pw_thread_backend/thread_native.h",
+ "thread_public_overrides/pw_thread_backend/thread_inline.h",
+ "thread_public_overrides/pw_thread_backend/thread_native.h",
],
includes = [
"public",
- "public_overrides",
+ "thread_public_overrides",
],
deps = [
":id",
"//pw_assert",
"//pw_string",
- "//pw_thread:thread_headers",
+ "//pw_thread:thread_facade",
],
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
)
@@ -112,7 +115,7 @@ pw_cc_library(
":thread_headers",
"//pw_assert",
],
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
)
@@ -121,6 +124,9 @@ pw_cc_library(
srcs = [
"test_threads.cc",
],
+ includes = ["public"],
+ # TODO(b/260637734): This target doesn't build
+ tags = ["manual"],
deps = [
"//pw_chrono:system_clock",
"//pw_thread:sleep",
@@ -133,13 +139,13 @@ pw_cc_library(
name = "yield_headers",
hdrs = [
"public/pw_thread_embos/yield_inline.h",
- "public_overrides/pw_thread_backend/yield_inline.h",
+ "yield_public_overrides/pw_thread_backend/yield_inline.h",
],
includes = [
"public",
- "public_overrides",
+ "yield_public_overrides",
],
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
)
@@ -159,11 +165,14 @@ pw_cc_library(
hdrs = [
"public/pw_thread_embos/util.h",
],
+ includes = ["public"],
+ # TODO(b/260637734): This target doesn't build
+ tags = ["manual"],
deps = [
"//pw_function",
"//pw_status",
],
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
)
@@ -175,6 +184,9 @@ pw_cc_library(
hdrs = [
"public/pw_thread_embos/snapshot.h",
],
+ includes = ["public"],
+ # TODO(b/260637734): This target doesn't build
+ tags = ["manual"],
deps = [
":util",
"//pw_bytes",
@@ -182,9 +194,8 @@ pw_cc_library(
"//pw_log",
"//pw_protobuf",
"//pw_status",
- "//pw_thread:protos",
"//pw_thread:snapshot",
],
- # TODO(pwbug/317): This should depend on embOS but our third parties
+ # TODO(b/234876414): This should depend on embOS but our third parties
# currently do not have Bazel support.
)
diff --git a/pw_thread_embos/BUILD.gn b/pw_thread_embos/BUILD.gn
index 1b4875012..023fdcda2 100644
--- a/pw_thread_embos/BUILD.gn
+++ b/pw_thread_embos/BUILD.gn
@@ -34,8 +34,8 @@ config("public_include_path") {
visibility = [ ":*" ]
}
-config("backend_config") {
- include_dirs = [ "public_overrides" ]
+config("id_public_overrides") {
+ include_dirs = [ "id_public_overrides" ]
visibility = [ ":*" ]
}
@@ -52,7 +52,7 @@ pw_source_set("config") {
pw_source_set("id") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":id_public_overrides",
]
public_deps = [
"$dir_pw_assert",
@@ -60,10 +60,10 @@ pw_source_set("id") {
"$dir_pw_third_party/embos",
]
public = [
+ "id_public_overrides/pw_thread_backend/id_inline.h",
+ "id_public_overrides/pw_thread_backend/id_native.h",
"public/pw_thread_embos/id_inline.h",
"public/pw_thread_embos/id_native.h",
- "public_overrides/pw_thread_backend/id_inline.h",
- "public_overrides/pw_thread_backend/id_native.h",
]
deps = [ "$dir_pw_thread:id.facade" ]
}
@@ -78,15 +78,20 @@ pw_build_assert("check_system_clock_backend") {
}
if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_SLEEP_BACKEND != "") {
+ config("sleep_public_overrides") {
+ include_dirs = [ "sleep_public_overrides" ]
+ visibility = [ ":*" ]
+ }
+
# This target provides the backend for pw::thread::sleep_{for,until}.
pw_source_set("sleep") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":sleep_public_overrides",
]
public = [
"public/pw_thread_embos/sleep_inline.h",
- "public_overrides/pw_thread_backend/sleep_inline.h",
+ "sleep_public_overrides/pw_thread_backend/sleep_inline.h",
]
public_deps = [ "$dir_pw_chrono:system_clock" ]
sources = [ "sleep.cc" ]
@@ -101,12 +106,17 @@ if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_SLEEP_BACKEND != "") {
}
}
+config("thread_public_overrides") {
+ include_dirs = [ "thread_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::thread::Thread and the headers needed
# for thread creation.
pw_source_set("thread") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":thread_public_overrides",
]
public_deps = [
":config",
@@ -115,28 +125,34 @@ pw_source_set("thread") {
"$dir_pw_third_party/embos",
"$dir_pw_thread:id",
"$dir_pw_thread:thread.facade",
+ dir_pw_span,
]
public = [
"public/pw_thread_embos/context.h",
"public/pw_thread_embos/options.h",
"public/pw_thread_embos/thread_inline.h",
"public/pw_thread_embos/thread_native.h",
- "public_overrides/pw_thread_backend/thread_inline.h",
- "public_overrides/pw_thread_backend/thread_native.h",
+ "thread_public_overrides/pw_thread_backend/thread_inline.h",
+ "thread_public_overrides/pw_thread_backend/thread_native.h",
]
allow_circular_includes_from = [ "$dir_pw_thread:thread.facade" ]
sources = [ "thread.cc" ]
}
+config("yield_public_overrides") {
+ include_dirs = [ "yield_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::thread::yield.
pw_source_set("yield") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":yield_public_overrides",
]
public = [
"public/pw_thread_embos/yield_inline.h",
- "public_overrides/pw_thread_backend/yield_inline.h",
+ "yield_public_overrides/pw_thread_backend/yield_inline.h",
]
public_deps = [
"$dir_pw_assert",
diff --git a/pw_thread_embos/docs.rst b/pw_thread_embos/docs.rst
index 3bdbc4b07..b1fb32ba9 100644
--- a/pw_thread_embos/docs.rst
+++ b/pw_thread_embos/docs.rst
@@ -130,7 +130,7 @@ embOS Thread Options
Set the pre-allocated context (all memory needed to run a thread). Note that
this is required for this thread creation backend! The ``Context`` can
- either be constructed with an externally provided ``std::span<OS_UINT>``
+ either be constructed with an externally provided ``pw::span<OS_UINT>``
stack or the templated form of ``ContextWithStack<kStackSizeWords>`` can
be used.
@@ -199,7 +199,7 @@ captured. For ARM Cortex-M CPUs, you can do something like this:
void* stack_ptr = 0;
asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
pw::thread::ProcessThreadStackCallback cb =
- [](pw::thread::Thread::StreamEncoder& encoder,
+ [](pw::thread::proto::Thread::StreamEncoder& encoder,
pw::ConstByteSpan stack) -> pw::Status {
return encoder.WriteRawStack(stack);
};
@@ -207,10 +207,11 @@ captured. For ARM Cortex-M CPUs, you can do something like this:
snapshot_encoder, cb);
``SnapshotThreads()`` wraps the singular thread capture to instead captures
-all created threads to a ``pw::thread::SnapshotThreadInfo`` message. This proto
-message overlays a snapshot, so it is safe to static cast a
+all created threads to a ``pw::thread::proto::SnapshotThreadInfo`` message.
+This proto message overlays a snapshot, so it is safe to static cast a
``pw::snapshot::Snapshot::StreamEncoder`` to a
-``pw::thread::SnapshotThreadInfo::StreamEncoder`` when calling this function.
+``pw::thread::proto::SnapshotThreadInfo::StreamEncoder`` when calling this
+function.
Thread Name Capture
-------------------
diff --git a/pw_thread_embos/public_overrides/pw_thread_backend/id_inline.h b/pw_thread_embos/id_public_overrides/pw_thread_backend/id_inline.h
index 3ce10e07c..3ce10e07c 100644
--- a/pw_thread_embos/public_overrides/pw_thread_backend/id_inline.h
+++ b/pw_thread_embos/id_public_overrides/pw_thread_backend/id_inline.h
diff --git a/pw_thread_embos/public_overrides/pw_thread_backend/id_native.h b/pw_thread_embos/id_public_overrides/pw_thread_backend/id_native.h
index 9dd53ece0..9dd53ece0 100644
--- a/pw_thread_embos/public_overrides/pw_thread_backend/id_native.h
+++ b/pw_thread_embos/id_public_overrides/pw_thread_backend/id_native.h
diff --git a/pw_thread_embos/public/pw_thread_embos/context.h b/pw_thread_embos/public/pw_thread_embos/context.h
index 0b3ed5d0e..932f10740 100644
--- a/pw_thread_embos/public/pw_thread_embos/context.h
+++ b/pw_thread_embos/public/pw_thread_embos/context.h
@@ -14,9 +14,9 @@
#pragma once
#include <cstring>
-#include <span>
#include "RTOS.h"
+#include "pw_span/span.h"
#include "pw_string/util.h"
#include "pw_thread_embos/config.h"
@@ -45,7 +45,7 @@ namespace pw::thread::embos {
// }
class Context {
public:
- explicit Context(std::span<OS_UINT> stack_span)
+ explicit Context(span<OS_UINT> stack_span)
: tcb_{}, stack_span_(stack_span) {}
Context(const Context&) = delete;
Context& operator=(const Context&) = delete;
@@ -56,7 +56,7 @@ class Context {
private:
friend Thread;
- std::span<OS_UINT> stack() { return stack_span_; }
+ span<OS_UINT> stack() { return stack_span_; }
bool in_use() const { return in_use_; }
void set_in_use(bool in_use = true) { in_use_ = in_use; }
@@ -84,7 +84,7 @@ class Context {
static void TerminateThread(Context& context);
OS_TASK tcb_;
- std::span<OS_UINT> stack_span_;
+ span<OS_UINT> stack_span_;
ThreadRoutine user_thread_entry_function_ = nullptr;
void* user_thread_entry_arg_ = nullptr;
diff --git a/pw_thread_embos/public/pw_thread_embos/options.h b/pw_thread_embos/public/pw_thread_embos/options.h
index d024fdec3..5dac5e675 100644
--- a/pw_thread_embos/public/pw_thread_embos/options.h
+++ b/pw_thread_embos/public/pw_thread_embos/options.h
@@ -42,9 +42,9 @@ namespace pw::thread::embos {
//
class Options : public thread::Options {
public:
- constexpr Options() = default;
+ constexpr Options() {}
constexpr Options(const Options&) = default;
- constexpr Options(Options&& other) = default;
+ constexpr Options(Options&&) = default;
// Sets the name for the embOS task, this is optional.
// Note that this will be deep copied into the context and may be truncated
diff --git a/pw_thread_embos/public/pw_thread_embos/snapshot.h b/pw_thread_embos/public/pw_thread_embos/snapshot.h
index 75b8adf40..8fabec447 100644
--- a/pw_thread_embos/public/pw_thread_embos/snapshot.h
+++ b/pw_thread_embos/public/pw_thread_embos/snapshot.h
@@ -31,7 +31,7 @@ namespace pw::thread::embos {
// void* stack_ptr = 0;
// asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
// pw::thread::ProcessThreadStackCallback cb =
-// [](pw::thread::Thread::StreamEncoder& encoder,
+// [](pw::thread::proto::Thread::StreamEncoder& encoder,
// pw::ConstByteSpan stack) -> pw::Status {
// return encoder.WriteRawStack(stack);
// };
@@ -40,7 +40,7 @@ namespace pw::thread::embos {
// Warning: This is only safe to use when interrupts and the scheduler are
// disabled!
Status SnapshotThreads(void* running_thread_stack_pointer,
- SnapshotThreadInfo::StreamEncoder& encoder,
+ proto::SnapshotThreadInfo::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback);
// Captures only the provided thread handle as a pw::thread::Thread proto
@@ -68,7 +68,7 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
// disabled!
Status SnapshotThread(const OS_TASK& thread,
void* running_thread_stack_pointer,
- Thread::StreamEncoder& encoder,
+ proto::Thread::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback);
} // namespace pw::thread::embos
diff --git a/pw_thread_embos/public_overrides/pw_thread_backend/sleep_inline.h b/pw_thread_embos/sleep_public_overrides/pw_thread_backend/sleep_inline.h
index 12db456d5..12db456d5 100644
--- a/pw_thread_embos/public_overrides/pw_thread_backend/sleep_inline.h
+++ b/pw_thread_embos/sleep_public_overrides/pw_thread_backend/sleep_inline.h
diff --git a/pw_thread_embos/snapshot.cc b/pw_thread_embos/snapshot.cc
index 1a9e17d67..966d56d53 100644
--- a/pw_thread_embos/snapshot.cc
+++ b/pw_thread_embos/snapshot.cc
@@ -37,10 +37,11 @@ inline bool ThreadIsRunning(const OS_TASK& thread) {
return OS_GetpCurrentTask() == &thread;
}
-void CaptureThreadState(const OS_TASK& thread, Thread::StreamEncoder& encoder) {
+void CaptureThreadState(const OS_TASK& thread,
+ proto::Thread::StreamEncoder& encoder) {
if (ThreadIsRunning(thread)) {
PW_LOG_DEBUG("Thread state: RUNNING");
- encoder.WriteState(ThreadState::Enum::RUNNING);
+ encoder.WriteState(proto::ThreadState::Enum::RUNNING);
return;
}
@@ -61,24 +62,24 @@ void CaptureThreadState(const OS_TASK& thread, Thread::StreamEncoder& encoder) {
if ((thread.Stat & 0x3) != 0) {
PW_LOG_DEBUG("Thread state: SUSPENDED");
- encoder.WriteState(ThreadState::Enum::SUSPENDED);
+ encoder.WriteState(proto::ThreadState::Enum::SUSPENDED);
} else if ((thread.Stat & 0xf8) == 0) {
PW_LOG_DEBUG("Thread state: READY");
- encoder.WriteState(ThreadState::Enum::READY);
+ encoder.WriteState(proto::ThreadState::Enum::READY);
} else {
PW_LOG_DEBUG("Thread state: BLOCKED");
- encoder.WriteState(ThreadState::Enum::BLOCKED);
+ encoder.WriteState(proto::ThreadState::Enum::BLOCKED);
}
}
} // namespace
Status SnapshotThreads(void* running_thread_stack_pointer,
- SnapshotThreadInfo::StreamEncoder& encoder,
+ proto::SnapshotThreadInfo::StreamEncoder& encoder,
ProcessThreadStackCallback& stack_dumper) {
struct {
void* running_thread_stack_pointer;
- SnapshotThreadInfo::StreamEncoder* encoder;
+ proto::SnapshotThreadInfo::StreamEncoder* encoder;
ProcessThreadStackCallback* stack_dumper;
Status thread_capture_status;
} ctx;
@@ -87,7 +88,8 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
ctx.stack_dumper = &stack_dumper;
ThreadCallback thread_capture_cb([&ctx](const OS_TASK& thread) -> bool {
- Thread::StreamEncoder thread_encoder = ctx.encoder->GetThreadsEncoder();
+ proto::Thread::StreamEncoder thread_encoder =
+ ctx.encoder->GetThreadsEncoder();
ctx.thread_capture_status.Update(
SnapshotThread(thread,
ctx.running_thread_stack_pointer,
@@ -108,11 +110,11 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
Status SnapshotThread(const OS_TASK& thread,
void* running_thread_stack_pointer,
- Thread::StreamEncoder& encoder,
+ proto::Thread::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback) {
#if OS_TRACKNAME
PW_LOG_DEBUG("Capturing thread info for %s", thread.Name);
- encoder.WriteName(std::as_bytes(std::span(std::string_view(thread.Name))));
+ encoder.WriteName(as_bytes(span(std::string_view(thread.Name))));
#else
PW_LOG_DEBUG("Capturing thread info for thread at 0x%08x", &thread);
#endif // OS_TRACKNAME
diff --git a/pw_thread_embos/public_overrides/pw_thread_backend/thread_inline.h b/pw_thread_embos/thread_public_overrides/pw_thread_backend/thread_inline.h
index fd5df5c92..fd5df5c92 100644
--- a/pw_thread_embos/public_overrides/pw_thread_backend/thread_inline.h
+++ b/pw_thread_embos/thread_public_overrides/pw_thread_backend/thread_inline.h
diff --git a/pw_thread_embos/public_overrides/pw_thread_backend/thread_native.h b/pw_thread_embos/thread_public_overrides/pw_thread_backend/thread_native.h
index f8dd12291..f8dd12291 100644
--- a/pw_thread_embos/public_overrides/pw_thread_backend/thread_native.h
+++ b/pw_thread_embos/thread_public_overrides/pw_thread_backend/thread_native.h
diff --git a/pw_thread_embos/public_overrides/pw_thread_backend/yield_inline.h b/pw_thread_embos/yield_public_overrides/pw_thread_backend/yield_inline.h
index 6b0779607..6b0779607 100644
--- a/pw_thread_embos/public_overrides/pw_thread_backend/yield_inline.h
+++ b/pw_thread_embos/yield_public_overrides/pw_thread_backend/yield_inline.h
diff --git a/pw_thread_freertos/BUILD.bazel b/pw_thread_freertos/BUILD.bazel
index a03e530bc..c401c2a85 100644
--- a/pw_thread_freertos/BUILD.bazel
+++ b/pw_thread_freertos/BUILD.bazel
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
load(
"//pw_build:pigweed.bzl",
"pw_cc_facade",
@@ -26,36 +27,41 @@ licenses(["notice"])
pw_cc_library(
name = "id_headers",
hdrs = [
+ "id_public_overrides/pw_thread_backend/id_inline.h",
+ "id_public_overrides/pw_thread_backend/id_native.h",
"public/pw_thread_freertos/id_inline.h",
"public/pw_thread_freertos/id_native.h",
- "public_overrides/pw_thread_backend/id_inline.h",
- "public_overrides/pw_thread_backend/id_native.h",
],
includes = [
+ "id_public_overrides",
"public",
- "public_overrides",
+ ],
+ deps = [
+ "//pw_interrupt:context",
+ "@freertos",
],
)
pw_cc_library(
name = "id",
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
":id_headers",
"//pw_thread:id_facade",
],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
name = "sleep_headers",
hdrs = [
"public/pw_thread_freertos/sleep_inline.h",
- "public_overrides/pw_thread_backend/sleep_inline.h",
+ "sleep_public_overrides/pw_thread_backend/sleep_inline.h",
],
includes = [
"public",
- "public_overrides",
+ "sleep_public_overrides",
],
deps = [
"//pw_chrono:system_clock",
@@ -67,15 +73,17 @@ pw_cc_library(
srcs = [
"sleep.cc",
],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
":sleep_headers",
"//pw_assert",
"//pw_chrono:system_clock",
"//pw_chrono_freertos:system_clock_headers",
+ "//pw_thread:id",
"//pw_thread:sleep_facade",
],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
# This target provides the FreeRTOS specific headers needs for thread creation.
@@ -87,22 +95,23 @@ pw_cc_library(
"public/pw_thread_freertos/options.h",
"public/pw_thread_freertos/thread_inline.h",
"public/pw_thread_freertos/thread_native.h",
- "public_overrides/pw_thread_backend/thread_inline.h",
- "public_overrides/pw_thread_backend/thread_native.h",
+ "thread_public_overrides/pw_thread_backend/thread_inline.h",
+ "thread_public_overrides/pw_thread_backend/thread_native.h",
],
includes = [
"public",
- "public_overrides",
+ "thread_public_overrides",
+ ],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
],
deps = [
":id",
"//pw_assert",
"//pw_string",
"//pw_sync:binary_semaphore",
- "//pw_thread:thread_headers",
+ "//pw_thread:thread_facade",
],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -110,13 +119,14 @@ pw_cc_library(
srcs = [
"thread.cc",
],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
":id",
":thread_headers",
"//pw_assert",
],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -124,6 +134,9 @@ pw_cc_library(
srcs = [
"dynamic_test_threads.cc",
],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
"//pw_chrono:system_clock",
"//pw_thread:sleep",
@@ -134,6 +147,8 @@ pw_cc_library(
pw_cc_test(
name = "dynamic_thread_backend_test",
+ # TODO(b/271465588): Get this test to build.
+ tags = ["manual"],
deps = [
":dynamic_test_threads",
"//pw_thread:thread_facade_test",
@@ -145,6 +160,9 @@ pw_cc_library(
srcs = [
"static_test_threads.cc",
],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
"//pw_chrono:system_clock",
"//pw_thread:sleep",
@@ -155,6 +173,8 @@ pw_cc_library(
pw_cc_test(
name = "static_thread_backend_test",
+ # TODO(b/271465588): Get this test to build.
+ tags = ["manual"],
deps = [
":static_test_threads",
"//pw_thread:thread_facade_test",
@@ -165,14 +185,18 @@ pw_cc_library(
name = "yield_headers",
hdrs = [
"public/pw_thread_freertos/yield_inline.h",
- "public_overrides/pw_thread_backend/yield_inline.h",
+ "yield_public_overrides/pw_thread_backend/yield_inline.h",
],
includes = [
"public",
- "public_overrides",
+ "yield_public_overrides",
+ ],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
+ deps = [
+ "@freertos",
],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -184,6 +208,50 @@ pw_cc_library(
)
pw_cc_library(
+ name = "thread_iteration",
+ srcs = [
+ "pw_thread_freertos_private/thread_iteration.h",
+ "thread_iteration.cc",
+ ],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
+ deps = [
+ ":freertos_tasktcb",
+ "//pw_function",
+ "//pw_span",
+ "//pw_status",
+ "//pw_thread:thread_info",
+ "//pw_thread:thread_iteration_facade",
+ "//pw_thread_freertos:util",
+ ],
+)
+
+pw_cc_test(
+ name = "thread_iteration_test",
+ srcs = [
+ "pw_thread_freertos_private/thread_iteration.h",
+ "thread_iteration_test.cc",
+ ],
+ # TODO(b/271465588): Get this test to build.
+ tags = ["manual"],
+ deps = [
+ ":freertos_tasktcb",
+ ":static_test_threads",
+ ":thread_iteration",
+ "//pw_bytes",
+ "//pw_span",
+ "//pw_string:builder",
+ "//pw_string:util",
+ "//pw_sync:thread_notification",
+ "//pw_thread:test_threads_header",
+ "//pw_thread:thread",
+ "//pw_thread:thread_info",
+ "//pw_thread:thread_iteration",
+ ],
+)
+
+pw_cc_library(
name = "util",
srcs = [
"util.cc",
@@ -191,12 +259,16 @@ pw_cc_library(
hdrs = [
"public/pw_thread_freertos/util.h",
],
+ includes = ["public"],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
"//pw_function",
+ "//pw_log",
"//pw_status",
+ "@freertos",
],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -207,6 +279,14 @@ pw_cc_library(
hdrs = [
"public/pw_thread_freertos/snapshot.h",
],
+ # TODO(b/269204725): Put this in the toolchain configuration instead. I
+ # would like to say `copts = ["-Wno-c++20-designator"]`, but arm-gcc tells
+ # me that's an "unrecognized command line option"; I think it may be a
+ # clang-only flag.
+ copts = ["-Wno-pedantic"],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
":freertos_tasktcb",
":util",
@@ -214,11 +294,9 @@ pw_cc_library(
"//pw_log",
"//pw_protobuf",
"//pw_status",
- "//pw_thread:protos",
"//pw_thread:snapshot",
+ "//pw_thread:thread_cc.pwpb",
],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
)
pw_cc_facade(
@@ -227,13 +305,31 @@ pw_cc_facade(
"public/pw_thread_freertos/freertos_tsktcb.h",
],
includes = ["public"],
- # TODO(pwbug/317): This should depend on FreeRTOS but our third parties
- # currently do not have Bazel support.
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
)
pw_cc_library(
name = "freertos_tasktcb",
+ hdrs = [
+ ":generate_freertos_tsktcb",
+ ],
+ includes = ["thread_public_overrides"],
deps = [
":freertos_tasktcb_facade",
],
)
+
+run_binary(
+ name = "generate_freertos_tsktcb",
+ srcs = [
+ "@freertos//:tasks.c",
+ ],
+ outs = [":thread_public_overrides/pw_thread_freertos_backend/freertos_tsktcb.h"],
+ args = [
+ "--freertos-tasks-c=$(location @freertos//:tasks.c)",
+ "--output=$(location :thread_public_overrides/pw_thread_freertos_backend/freertos_tsktcb.h)",
+ ],
+ tool = "//pw_thread_freertos/py:generate_freertos_tsktcb",
+)
diff --git a/pw_thread_freertos/BUILD.gn b/pw_thread_freertos/BUILD.gn
index c1027cc66..532a79dba 100644
--- a/pw_thread_freertos/BUILD.gn
+++ b/pw_thread_freertos/BUILD.gn
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pigweed/third_party/freertos/freertos.gni")
import("$dir_pw_build/error.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_build/module_config.gni")
@@ -36,11 +37,6 @@ config("public_include_path") {
visibility = [ ":*" ]
}
-config("backend_config") {
- include_dirs = [ "public_overrides" ]
- visibility = [ ":*" ]
-}
-
pw_source_set("config") {
public = [ "public/pw_thread_freertos/config.h" ]
public_configs = [ ":public_include_path" ]
@@ -50,11 +46,16 @@ pw_source_set("config") {
]
}
+config("id_public_overrides") {
+ include_dirs = [ "id_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::thread::Id & pw::this_thread::get_id.
pw_source_set("id") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":id_public_overrides",
]
public_deps = [
"$dir_pw_assert",
@@ -62,10 +63,10 @@ pw_source_set("id") {
"$dir_pw_third_party/freertos",
]
public = [
+ "id_public_overrides/pw_thread_backend/id_inline.h",
+ "id_public_overrides/pw_thread_backend/id_native.h",
"public/pw_thread_freertos/id_inline.h",
"public/pw_thread_freertos/id_native.h",
- "public_overrides/pw_thread_backend/id_inline.h",
- "public_overrides/pw_thread_backend/id_native.h",
]
deps = [ "$dir_pw_thread:id.facade" ]
}
@@ -81,15 +82,20 @@ pw_build_assert("check_system_clock_backend") {
visibility = [ ":*" ]
}
+config("sleep_public_overrides") {
+ include_dirs = [ "sleep_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::this_thread::sleep_{for,until}.
pw_source_set("sleep") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":sleep_public_overrides",
]
public = [
"public/pw_thread_freertos/sleep_inline.h",
- "public_overrides/pw_thread_backend/sleep_inline.h",
+ "sleep_public_overrides/pw_thread_backend/sleep_inline.h",
]
public_deps = [ "$dir_pw_chrono:system_clock" ]
sources = [ "sleep.cc" ]
@@ -103,12 +109,17 @@ pw_source_set("sleep") {
]
}
+config("thread_public_overrides") {
+ include_dirs = [ "thread_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::thread::Thread and the headers needed
# for thread creation.
pw_source_set("thread") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":thread_public_overrides",
]
public_deps = [
":config",
@@ -117,28 +128,55 @@ pw_source_set("thread") {
"$dir_pw_third_party/freertos",
"$dir_pw_thread:id",
"$dir_pw_thread:thread.facade",
+ dir_pw_span,
]
public = [
"public/pw_thread_freertos/context.h",
"public/pw_thread_freertos/options.h",
"public/pw_thread_freertos/thread_inline.h",
"public/pw_thread_freertos/thread_native.h",
- "public_overrides/pw_thread_backend/thread_inline.h",
- "public_overrides/pw_thread_backend/thread_native.h",
+ "thread_public_overrides/pw_thread_backend/thread_inline.h",
+ "thread_public_overrides/pw_thread_backend/thread_native.h",
]
allow_circular_includes_from = [ "$dir_pw_thread:thread.facade" ]
sources = [ "thread.cc" ]
}
+# This target provides the backend for pw::thread::thread_iteration.
+if (pw_thread_freertos_FREERTOS_TSKTCB_BACKEND != "") {
+ pw_source_set("thread_iteration") {
+ public_configs = [ ":thread_public_overrides" ]
+ deps = [
+ ":freertos_tsktcb",
+ "$dir_pw_third_party/freertos",
+ "$dir_pw_thread:thread_info",
+ "$dir_pw_thread:thread_iteration.facade",
+ "$dir_pw_thread_freertos:util",
+ dir_pw_function,
+ dir_pw_span,
+ dir_pw_status,
+ ]
+ sources = [
+ "pw_thread_freertos_private/thread_iteration.h",
+ "thread_iteration.cc",
+ ]
+ }
+}
+
+config("yield_public_overrides") {
+ include_dirs = [ "yield_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::this_thread::yield.
pw_source_set("yield") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":yield_public_overrides",
]
public = [
"public/pw_thread_freertos/yield_inline.h",
- "public_overrides/pw_thread_backend/yield_inline.h",
+ "yield_public_overrides/pw_thread_backend/yield_inline.h",
]
public_deps = [
"$dir_pw_assert",
@@ -153,6 +191,7 @@ pw_source_set("util") {
public_deps = [
"$dir_pw_third_party/freertos",
dir_pw_function,
+ dir_pw_span,
dir_pw_status,
]
public = [ "public/pw_thread_freertos/util.h" ]
@@ -160,6 +199,38 @@ pw_source_set("util") {
sources = [ "util.cc" ]
}
+# Action to generate `freertos_tsktcb.h` for
+# pw_thread_freertos_FREERTOS_TSKTCB_BACKEND.
+if (dir_pw_third_party_freertos != "") {
+ pw_python_action("generate_freertos_tsktcb") {
+ _out_path = "${target_gen_dir}/public_overrides/pw_thread_freertos_backend/freertos_tsktcb.h"
+ script = "py/pw_thread_freertos/generate_freertos_tsktcb.py"
+ args = [
+ "--freertos-src-dir",
+ rebase_path(dir_pw_third_party_freertos, root_build_dir),
+ "-o",
+ rebase_path(_out_path, root_build_dir),
+ ]
+ outputs = [ _out_path ]
+ visibility = [ ":auto_freertos_tsktcb" ]
+ }
+
+ config("auto_freertos_include_path") {
+ include_dirs = [ "${target_gen_dir}/public_overrides" ]
+ visibility = [ ":auto_freertos_tsktcb" ]
+ }
+
+ # Source set that provides backend for automatically generated
+ # `freertos_tsktcb.h` header.
+ pw_source_set("auto_freertos_tsktcb") {
+ public_configs = [ ":auto_freertos_include_path" ]
+ public = [ "${target_gen_dir}/public_overrides/pw_thread_freertos_backend/freertos_tsktcb.h" ]
+ public_deps = [ "$dir_pw_third_party/freertos" ]
+ deps = [ ":generate_freertos_tsktcb" ]
+ visibility = [ ":freertos_tsktcb" ]
+ }
+}
+
pw_facade("freertos_tsktcb") {
backend = pw_thread_freertos_FREERTOS_TSKTCB_BACKEND
public_configs = [ ":public_include_path" ]
@@ -193,6 +264,34 @@ pw_test_group("tests") {
":dynamic_thread_backend_test",
":static_thread_backend_test",
]
+ if (pw_thread_freertos_FREERTOS_TSKTCB_BACKEND != "") {
+ tests += [ ":thread_iteration_test" ]
+ }
+}
+
+if (pw_thread_freertos_FREERTOS_TSKTCB_BACKEND != "") {
+ pw_test("thread_iteration_test") {
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_freertos:thread"
+ sources = [
+ "pw_thread_freertos_private/thread_iteration.h",
+ "thread_iteration_test.cc",
+ ]
+ deps = [
+ ":freertos_tsktcb",
+ ":static_test_threads",
+ ":thread_iteration",
+ "$dir_pw_bytes",
+ "$dir_pw_span",
+ "$dir_pw_string:builder",
+ "$dir_pw_string:util",
+ "$dir_pw_sync:thread_notification",
+ "$dir_pw_third_party/freertos",
+ "$dir_pw_thread:test_threads",
+ "$dir_pw_thread:thread",
+ "$dir_pw_thread:thread_info",
+ "$dir_pw_thread:thread_iteration",
+ ]
+ }
}
pw_source_set("dynamic_test_threads") {
diff --git a/pw_thread_freertos/CMakeLists.txt b/pw_thread_freertos/CMakeLists.txt
index 1e524f646..6439e12a9 100644
--- a/pw_thread_freertos/CMakeLists.txt
+++ b/pw_thread_freertos/CMakeLists.txt
@@ -13,10 +13,11 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_thread_freertos/backend.cmake)
pw_add_module_config(pw_thread_freertos_CONFIG)
-pw_add_module_library(pw_thread_freertos.config
+pw_add_library(pw_thread_freertos.config INTERFACE
HEADERS
public/pw_thread_freertos/config.h
PUBLIC_INCLUDES
@@ -27,35 +28,33 @@ pw_add_module_library(pw_thread_freertos.config
)
# This target provides the backend for pw::thread::Id & pw::this_thread::get_id.
-pw_add_module_library(pw_thread_freertos.id
- IMPLEMENTS_FACADES
- pw_thread.id
+pw_add_library(pw_thread_freertos.id INTERFACE
HEADERS
public/pw_thread_freertos/id_inline.h
public/pw_thread_freertos/id_native.h
- public_overrides/pw_thread_backend/id_inline.h
- public_overrides/pw_thread_backend/id_native.h
+ id_public_overrides/pw_thread_backend/id_inline.h
+ id_public_overrides/pw_thread_backend/id_native.h
PUBLIC_INCLUDES
public
- public_overrides
+ id_public_overrides
PUBLIC_DEPS
pw_assert
pw_interrupt.context
pw_third_party.freertos
+ pw_thread.id.facade
)
# This target provides the backend for pw::this_thread::sleep_{for,until}.
-pw_add_module_library(pw_thread_freertos.sleep
- IMPLEMENTS_FACADES
- pw_thread.sleep
+pw_add_library(pw_thread_freertos.sleep STATIC
HEADERS
public/pw_thread_freertos/sleep_inline.h
- public_overrides/pw_thread_backend/sleep_inline.h
+ sleep_public_overrides/pw_thread_backend/sleep_inline.h
PUBLIC_INCLUDES
public
- public_overrides
+ sleep_public_overrides
PUBLIC_DEPS
pw_chrono.system_clock
+ pw_thread.sleep.facade
SOURCES
sleep.cc
PRIVATE_DEPS
@@ -67,47 +66,45 @@ pw_add_module_library(pw_thread_freertos.sleep
# This target provides the backend for pw::thread::Thread and the headers needed
# for thread creation.
-pw_add_module_library(pw_thread_freertos.thread
- IMPLEMENTS_FACADES
- pw_thread.thread
+pw_add_library(pw_thread_freertos.thread STATIC
HEADERS
public/pw_thread_freertos/context.h
public/pw_thread_freertos/options.h
public/pw_thread_freertos/thread_inline.h
public/pw_thread_freertos/thread_native.h
- public_overrides/pw_thread_backend/thread_inline.h
- public_overrides/pw_thread_backend/thread_native.h
+ thread_public_overrides/pw_thread_backend/thread_inline.h
+ thread_public_overrides/pw_thread_backend/thread_native.h
PUBLIC_INCLUDES
public
- public_overrides
+ thread_public_overrides
PUBLIC_DEPS
pw_assert
- pw_polyfill.span
+ pw_span
pw_string
pw_third_party.freertos
pw_thread.id
+ pw_thread.thread.facade
pw_thread_freertos.config
SOURCES
thread.cc
)
# This target provides the backend for pw::this_thread::yield.
-pw_add_module_library(pw_thread_freertos.yield
- IMPLEMENTS_FACADES
- pw_thread.yield
+pw_add_library(pw_thread_freertos.yield INTERFACE
HEADERS
public/pw_thread_freertos/yield_inline.h
- public_overrides/pw_thread_backend/yield_inline.h
+ yield_public_overrides/pw_thread_backend/yield_inline.h
PUBLIC_INCLUDES
public
- public_overrides
+ yield_public_overrides
PUBLIC_DEPS
pw_assert
pw_third_party.freertos
pw_thread.id
+ pw_thread.yield.facade
)
-pw_add_module_library(pw_thread_freertos.util
+pw_add_library(pw_thread_freertos.util STATIC
HEADERS
public/pw_thread_freertos/util.h
PUBLIC_INCLUDES
@@ -115,7 +112,7 @@ pw_add_module_library(pw_thread_freertos.util
PUBLIC_DEPS
pw_third_party.freertos
pw_function
- pw_polyfill.span
+ pw_span
pw_status
SOURCES
util.cc
@@ -123,14 +120,14 @@ pw_add_module_library(pw_thread_freertos.util
pw_log
)
-pw_add_module_library(pw_thread_freertos.snapshot
+pw_add_library(pw_thread_freertos.snapshot STATIC
HEADERS
public/pw_thread_freertos/snapshot.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
pw_function
- pw_polyfill.span
+ pw_span
pw_protobuf
pw_status
pw_third_party.freertos
@@ -145,7 +142,9 @@ pw_add_module_library(pw_thread_freertos.snapshot
pw_log
)
-pw_add_facade(pw_thread_freertos.freertos_tsktcb
+pw_add_facade(pw_thread_freertos.freertos_tsktcb INTERFACE
+ BACKEND
+ pw_thread_freertos.freertos_tsktcb_BACKEND
HEADERS
public/pw_thread_freertos/freertos_tsktcb.h
PUBLIC_INCLUDES
diff --git a/pw_thread_freertos/backend.cmake b/pw_thread_freertos/backend.cmake
new file mode 100644
index 000000000..bc2b42fbf
--- /dev/null
+++ b/pw_thread_freertos/backend.cmake
@@ -0,0 +1,26 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Unfortunately FreeRTOS entirely hides the contents of the TCB inside of
+# tasks.c but it's necessary for snapshot processing in order to access the
+# stack limits. Set this to a pw_source_set which provides the tskTCB struct
+# definition for snapshot to work with FreeRTOS. By default, this is
+# auto-generated from FreeRTOS sources and shouldn't need to be manually
+# modified.
+#
+# See the pw_thread_freertos docs for more details.
+pw_add_backend_variable(pw_thread_freertos.freertos_tsktcb_BACKEND)
diff --git a/pw_thread_freertos/backend.gni b/pw_thread_freertos/backend.gni
index f88b7753b..409623f39 100644
--- a/pw_thread_freertos/backend.gni
+++ b/pw_thread_freertos/backend.gni
@@ -14,12 +14,21 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pigweed/third_party/freertos/freertos.gni")
+
declare_args() {
# Unfortunately FreeRTOS entirely hides the contents of the TCB inside of
# tasks.c but it's necessary for snapshot processing in order to access the
# stack limits. Set this to a pw_source_set which provides the tskTCB struct
- # definition for snapshot to work with FreeRTOS.
+ # definition for snapshot to work with FreeRTOS. By default, this is
+ # auto-generated from FreeRTOS sources and shouldn't need to be manually
+ # modified.
#
# See the pw_thread_freertos docs for more details.
- pw_thread_freertos_FREERTOS_TSKTCB_BACKEND = ""
+ if (dir_pw_third_party_freertos != "") {
+ pw_thread_freertos_FREERTOS_TSKTCB_BACKEND =
+ "$dir_pw_thread_freertos:auto_freertos_tsktcb"
+ } else {
+ pw_thread_freertos_FREERTOS_TSKTCB_BACKEND = ""
+ }
}
diff --git a/pw_thread_freertos/docs.rst b/pw_thread_freertos/docs.rst
index 4e500303b..c0cd11d0b 100644
--- a/pw_thread_freertos/docs.rst
+++ b/pw_thread_freertos/docs.rst
@@ -16,6 +16,10 @@ Optional dynamic allocation for threads is supported using ``xTaskCreate()``.
Optional joining support is enabled via an ``StaticEventGroup_t`` in each
thread's context.
+.. Note::
+ Scheduler State API support is required in your FreeRTOS Configuration, i.e.
+ ``INCLUDE_xTaskGetSchedulerState == 1``.
+
This backend always permits users to start threads where static contexts are
passed in as an option. As a quick example, a detached thread can be created as
follows:
@@ -154,18 +158,26 @@ FreeRTOS Thread Options
Set the pre-allocated context (all memory needed to run a thread). The
``StaticContext`` can either be constructed with an externally provided
- ``std::span<StackType_t>`` stack or the templated form of
+ ``pw::span<StackType_t>`` stack or the templated form of
``StaticContextWithStack<kStackSizeWords>`` can be used.
-----------------------------
Thread Identification Backend
-----------------------------
-A backend for ``pw::thread::Id`` and ``pw::thread::get_id()`` is offerred using
+A backend for ``pw::thread::Id`` and ``pw::thread::get_id()`` is offered using
``xTaskGetCurrentTaskHandle()``. It uses ``DASSERT`` to ensure that it is not
invoked from interrupt context and if possible that the scheduler has started
via ``xTaskGetSchedulerState()``.
+------------------------
+Thread Iteration Backend
+------------------------
+``pw_thread_freertos_TSKTCB_BACKEND`` to be configured
+properly and ``pw_third_party_freertos_DISABLE_TASKS_STATICS`` to be enabled.
+To allow for peak stack usage measurement, the FreeRTOS config
+``INCLUDE_uxTaskGetStackHighWaterMark`` should also be enabled.
+
--------------------
Thread Sleep Backend
--------------------
@@ -222,12 +234,15 @@ Unfortunately FreeRTOS entirely hides the contents of the TCB inside of
``Source/tasks.c``, but it's necessary for snapshot processing in order to
access the stack limits from interrupt contexts. For this reason, FreeRTOS
snapshot integration relies on the ``pw_thread_freertos:freertos_tsktcb`` facade
-to provide the ``tskTCB`` definition.
+to provide the ``tskTCB`` definition. By default, a header will automatically be
+generated from FreeRTOS's ``tasks.c`` file to work around this limitation.
-The selected backend is expected to provide the ``struct tskTCB`` definition
-through ``pw_thread_freertos_backend/freertos_tsktcb.h``. The facade asserts
-that this definition matches the size of FreeRTOS's ``StaticTask_T`` which is
-the public opaque TCB type.
+In the event that the automatic header generation is incompatible with your
+version of FreeRTOS, ``pw_thread_freertos_FREERTOS_TSKTCB_BACKEND`` must be
+configured to point to a source set that provides the ``struct tskTCB``
+definition through ``pw_thread_freertos_backend/freertos_tsktcb.h``. The facade
+asserts that this definition matches the size of FreeRTOS's ``StaticTask_T``
+which is the public opaque TCB type.
``SnapshotThreads()``
=====================
@@ -243,7 +258,7 @@ captured. For ARM Cortex-M CPUs, you can do something like this:
void* stack_ptr = 0;
asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
pw::thread::ProcessThreadStackCallback cb =
- [](pw::thread::Thread::StreamEncoder& encoder,
+ [](pw::thread::proto::Thread::StreamEncoder& encoder,
pw::ConstByteSpan stack) -> pw::Status {
return encoder.WriteRawStack(stack);
};
@@ -251,11 +266,12 @@ captured. For ARM Cortex-M CPUs, you can do something like this:
snapshot_encoder, cb);
``SnapshotThreads()`` wraps the singular thread capture to instead captures
-all created threads to a ``pw::thread::SnapshotThreadInfo`` message which also
-captures the thread state for you. This proto
+all created threads to a ``pw::thread::proto::SnapshotThreadInfo`` message
+which also captures the thread state for you. This proto
message overlays a snapshot, so it is safe to static cast a
``pw::snapshot::Snapshot::StreamEncoder`` to a
-``pw::thread::SnapshotThreadInfo::StreamEncoder`` when calling this function.
+``pw::thread::proto::SnapshotThreadInfo::StreamEncoder`` when calling this
+function.
.. Note:: ``SnapshotThreads()`` is only safe to use this while the scheduler and
interrupts are disabled as it relies on ``ForEachThread()``.
@@ -263,7 +279,7 @@ message overlays a snapshot, so it is safe to static cast a
Thread Stack Capture
--------------------
Snapshot attempts to capture as much of the thread stack state as possible,
-however it can be limited by on the FreeRTOS configuration.
+however it can be limited by the FreeRTOS configuration.
The ``stack_start_ptr`` can only be provided if the ``portSTACK_GROWTH`` is < 0,
i.e. the stack grows down, when ``configRECORD_STACK_HIGH_ADDRESS`` is enabled.
diff --git a/pw_thread_freertos/public_overrides/pw_thread_backend/id_inline.h b/pw_thread_freertos/id_public_overrides/pw_thread_backend/id_inline.h
index dcaf69997..dcaf69997 100644
--- a/pw_thread_freertos/public_overrides/pw_thread_backend/id_inline.h
+++ b/pw_thread_freertos/id_public_overrides/pw_thread_backend/id_inline.h
diff --git a/pw_thread_freertos/public_overrides/pw_thread_backend/id_native.h b/pw_thread_freertos/id_public_overrides/pw_thread_backend/id_native.h
index 245d29b88..245d29b88 100644
--- a/pw_thread_freertos/public_overrides/pw_thread_backend/id_native.h
+++ b/pw_thread_freertos/id_public_overrides/pw_thread_backend/id_native.h
diff --git a/pw_thread_freertos/public/pw_thread_freertos/context.h b/pw_thread_freertos/public/pw_thread_freertos/context.h
index 026bfdb72..bc4d231e2 100644
--- a/pw_thread_freertos/public/pw_thread_freertos/context.h
+++ b/pw_thread_freertos/public/pw_thread_freertos/context.h
@@ -14,9 +14,9 @@
#pragma once
#include <cstdint>
-#include <span>
#include "FreeRTOS.h"
+#include "pw_span/span.h"
#include "pw_thread_freertos/config.h"
#include "task.h"
#if PW_THREAD_JOINING_ENABLED
@@ -108,17 +108,17 @@ class Context {
// }
class StaticContext : public Context {
public:
- explicit StaticContext(std::span<StackType_t> stack_span)
+ explicit StaticContext(span<StackType_t> stack_span)
: tcb_{}, stack_span_(stack_span) {}
private:
friend Thread;
StaticTask_t& tcb() { return tcb_; }
- std::span<StackType_t> stack() { return stack_span_; }
+ span<StackType_t> stack() { return stack_span_; }
StaticTask_t tcb_;
- std::span<StackType_t> stack_span_;
+ span<StackType_t> stack_span_;
};
// Static thread context allocation including the stack along with the Context.
diff --git a/pw_thread_freertos/public/pw_thread_freertos/options.h b/pw_thread_freertos/public/pw_thread_freertos/options.h
index b87e2c244..107234c99 100644
--- a/pw_thread_freertos/public/pw_thread_freertos/options.h
+++ b/pw_thread_freertos/public/pw_thread_freertos/options.h
@@ -42,9 +42,9 @@ namespace pw::thread::freertos {
//
class Options : public thread::Options {
public:
- constexpr Options() = default;
+ constexpr Options() {}
constexpr Options(const Options&) = default;
- constexpr Options(Options&& other) = default;
+ constexpr Options(Options&&) = default;
// Sets the name for the FreeRTOS task, note that this will be truncated
// based on configMAX_TASK_NAME_LEN.
@@ -86,13 +86,19 @@ class Options : public thread::Options {
return *this;
}
+ // Returns name of FreeRTOS task.
+ //
+ // Note that this thread name may not match the actual thread name. See the
+ // FreeRTOS documentation on how names must be <= configMAX_TASK_NAME_LEN in
+ // order to avoid truncation.
+ const char* name() const { return name_; }
+
private:
friend thread::Thread;
// FreeRTOS requires a valid name when asserts are enabled,
// configMAX_TASK_NAME_LEN may be as small as one character.
static constexpr char kDefaultName[] = "pw::Thread";
- const char* name() const { return name_; }
UBaseType_t priority() const { return priority_; }
#if PW_THREAD_FREERTOS_CONFIG_DYNAMIC_ALLOCATION_ENABLED
size_t stack_size_words() const { return stack_size_words_; }
diff --git a/pw_thread_freertos/public/pw_thread_freertos/sleep_inline.h b/pw_thread_freertos/public/pw_thread_freertos/sleep_inline.h
index 44fe603ad..a6bd9a591 100644
--- a/pw_thread_freertos/public/pw_thread_freertos/sleep_inline.h
+++ b/pw_thread_freertos/public/pw_thread_freertos/sleep_inline.h
@@ -14,6 +14,7 @@
#pragma once
#include "pw_chrono/system_clock.h"
+#include "pw_thread/sleep.h"
namespace pw::this_thread {
diff --git a/pw_thread_freertos/public/pw_thread_freertos/snapshot.h b/pw_thread_freertos/public/pw_thread_freertos/snapshot.h
index ea5735f54..d05b928de 100644
--- a/pw_thread_freertos/public/pw_thread_freertos/snapshot.h
+++ b/pw_thread_freertos/public/pw_thread_freertos/snapshot.h
@@ -13,10 +13,9 @@
// the License.
#pragma once
-#include <span>
-
#include "FreeRTOS.h"
#include "pw_protobuf/encoder.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_thread/snapshot.h"
#include "pw_thread_protos/thread.pwpb.h"
@@ -38,7 +37,7 @@ namespace pw::thread::freertos {
// void* stack_ptr = 0;
// asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
// pw::thread::ProcessThreadStackCallback cb =
-// [](pw::thread::Thread::StreamEncoder& encoder,
+// [](pw::thread::proto::Thread::StreamEncoder& encoder,
// pw::ConstByteSpan stack) -> pw::Status {
// return encoder.WriteRawStack(stack);
// };
@@ -48,7 +47,7 @@ namespace pw::thread::freertos {
// Warning: This is only safe to use when the scheduler and interrupts are
// disabled.
Status SnapshotThreads(void* running_thread_stack_pointer,
- SnapshotThreadInfo::StreamEncoder& encoder,
+ proto::SnapshotThreadInfo::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback);
// Captures only the provided thread handle as a pw::thread::Thread proto
@@ -76,7 +75,7 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
Status SnapshotThread(TaskHandle_t thread,
eTaskState thread_state,
void* running_thread_stack_pointer,
- Thread::StreamEncoder& encoder,
+ proto::Thread::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback);
} // namespace pw::thread::freertos
diff --git a/pw_thread_freertos/public/pw_thread_freertos/thread_inline.h b/pw_thread_freertos/public/pw_thread_freertos/thread_inline.h
index e69212215..67b19aaa4 100644
--- a/pw_thread_freertos/public/pw_thread_freertos/thread_inline.h
+++ b/pw_thread_freertos/public/pw_thread_freertos/thread_inline.h
@@ -18,6 +18,7 @@
#include "FreeRTOS.h"
#include "pw_assert/assert.h"
#include "pw_thread/id.h"
+#include "pw_thread/thread.h"
#include "pw_thread_freertos/config.h"
#include "pw_thread_freertos/options.h"
#include "task.h"
diff --git a/pw_thread_freertos/public/pw_thread_freertos/util.h b/pw_thread_freertos/public/pw_thread_freertos/util.h
index 55b39f2fd..dee4021f4 100644
--- a/pw_thread_freertos/public/pw_thread_freertos/util.h
+++ b/pw_thread_freertos/public/pw_thread_freertos/util.h
@@ -13,10 +13,9 @@
// the License.
#pragma once
-#include <span>
-
#include "FreeRTOS.h"
#include "pw_function/function.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "task.h"
diff --git a/pw_thread_freertos/public/pw_thread_freertos/yield_inline.h b/pw_thread_freertos/public/pw_thread_freertos/yield_inline.h
index c2afaff6b..cd0ddc273 100644
--- a/pw_thread_freertos/public/pw_thread_freertos/yield_inline.h
+++ b/pw_thread_freertos/public/pw_thread_freertos/yield_inline.h
@@ -18,6 +18,7 @@
#include "FreeRTOS.h"
#include "pw_assert/assert.h"
#include "pw_thread/id.h"
+#include "pw_thread/yield.h"
#include "task.h"
namespace pw::this_thread {
diff --git a/pw_thread_freertos/pw_thread_freertos_private/thread_iteration.h b/pw_thread_freertos/pw_thread_freertos_private/thread_iteration.h
new file mode 100644
index 000000000..f82b8f702
--- /dev/null
+++ b/pw_thread_freertos/pw_thread_freertos_private/thread_iteration.h
@@ -0,0 +1,25 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include "FreeRTOS.h"
+#include "pw_thread/thread_iteration.h"
+
+namespace pw::thread::freertos {
+
+bool StackInfoCollector(TaskHandle_t current_thread,
+ const pw::thread::ThreadCallback& cb);
+
+} // namespace pw::thread::freertos
diff --git a/pw_thread_freertos/py/BUILD.bazel b/pw_thread_freertos/py/BUILD.bazel
new file mode 100644
index 000000000..a6b3f453d
--- /dev/null
+++ b/pw_thread_freertos/py/BUILD.bazel
@@ -0,0 +1,21 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Python utilities for code generation.
+
+py_binary(
+ name = "generate_freertos_tsktcb",
+ srcs = ["pw_thread_freertos/generate_freertos_tsktcb.py"],
+ visibility = ["//pw_thread_freertos:__subpackages__"],
+)
diff --git a/pw_thread_freertos/py/BUILD.gn b/pw_thread_freertos/py/BUILD.gn
new file mode 100644
index 000000000..33621663b
--- /dev/null
+++ b/pw_thread_freertos/py/BUILD.gn
@@ -0,0 +1,32 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+
+pw_python_package("py") {
+ setup = [
+ "pyproject.toml",
+ "setup.cfg",
+ "setup.py",
+ ]
+ sources = [
+ "pw_thread_freertos/__init__.py",
+ "pw_thread_freertos/generate_freertos_tsktcb.py",
+ ]
+
+ pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+}
diff --git a/pw_thread_freertos/py/pw_thread_freertos/__init__.py b/pw_thread_freertos/py/pw_thread_freertos/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_thread_freertos/py/pw_thread_freertos/__init__.py
diff --git a/pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py b/pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py
new file mode 100644
index 000000000..c37a376c9
--- /dev/null
+++ b/pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Generates freertos_tsktcb.h from FreeRTOS source.
+
+Extracts the tskTCB struct from FreeRTOS sources and writes it as a header to
+the specified output path.
+"""
+import argparse
+import re
+import sys
+from typing import Optional, TextIO
+from pathlib import Path
+
+_GENERATED_HEADER = """\
+// This header is generated by generate_freertos_tsktcb.py, DO NOT EDIT!
+#pragma once
+
+#include "FreeRTOS.h"
+#include "task.h"
+
+"""
+
+
+def _parse_args() -> argparse.Namespace:
+ """Parses arguments for this script, splitting out the command to run."""
+
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--freertos-src-dir',
+ type=Path,
+ help=(
+ 'Path to the FreeRTOS source directory. Required unless'
+ ' --freertos-tasks-c is provided.'
+ ),
+ )
+ parser.add_argument(
+ '--freertos-tasks-c',
+ type=Path,
+ help=(
+ 'Path to the tasks.c file in the FreeRTOS source directory. '
+ 'Required unless --freertos-src-dir is provided.'
+ ),
+ )
+ parser.add_argument(
+ '--output',
+ '-o',
+ type=argparse.FileType('w'),
+ help=('Path to write generated tskTCB.h file to'),
+ )
+ return parser.parse_args()
+
+
+def _extract_struct(tasks_src: str):
+ tsk_tcb_struct = re.search(
+ r'(typedef struct tskTaskControlBlock.*tskTCB;\n)',
+ tasks_src,
+ flags=re.DOTALL,
+ )
+ if tsk_tcb_struct:
+ return tsk_tcb_struct.group(1)
+ raise ValueError('Could not find tskTCB struct in tasks.c')
+
+
+def _main(
+ freertos_src_dir: Optional[Path],
+ freertos_tasks_c: Optional[Path],
+ output: TextIO,
+):
+ if freertos_tasks_c is None or not freertos_tasks_c.is_file():
+ assert freertos_src_dir is not None
+ freertos_tasks_c = freertos_src_dir / 'tasks.c'
+ with open(freertos_tasks_c, 'r') as tasks_c:
+ output.write(_GENERATED_HEADER)
+ output.write(_extract_struct(tasks_c.read()))
+
+
+if __name__ == '__main__':
+ sys.exit(_main(**vars(_parse_args())))
diff --git a/pw_thread_freertos/py/py.typed b/pw_thread_freertos/py/py.typed
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/pw_thread_freertos/py/py.typed
diff --git a/pw_thread_freertos/py/pyproject.toml b/pw_thread_freertos/py/pyproject.toml
new file mode 100644
index 000000000..ac821710a
--- /dev/null
+++ b/pw_thread_freertos/py/pyproject.toml
@@ -0,0 +1,16 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+[build-system]
+requires = ['setuptools', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/pw_thread_freertos/py/setup.cfg b/pw_thread_freertos/py/setup.cfg
new file mode 100644
index 000000000..41d22f9ce
--- /dev/null
+++ b/pw_thread_freertos/py/setup.cfg
@@ -0,0 +1,27 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+[metadata]
+name = pw_thread_freertos
+version = 0.0.1
+author = Pigweed Authors
+author_email = pigweed-developers@googlegroups.com
+description = Pigweed utilities for FreeRTOS thread integration
+
+[options]
+packages = find:
+zip_safe = False
+
+[options.package_data]
+pw_thread_freertos =
+ py.typed
diff --git a/pw_thread_freertos/py/setup.py b/pw_thread_freertos/py/setup.py
new file mode 100644
index 000000000..3ed783f64
--- /dev/null
+++ b/pw_thread_freertos/py/setup.py
@@ -0,0 +1,18 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_thread_freertos"""
+
+import setuptools # type: ignore
+
+setuptools.setup() # Package definition in setup.cfg
diff --git a/pw_thread_freertos/public_overrides/pw_thread_backend/sleep_inline.h b/pw_thread_freertos/sleep_public_overrides/pw_thread_backend/sleep_inline.h
index 4ccbe1de2..4ccbe1de2 100644
--- a/pw_thread_freertos/public_overrides/pw_thread_backend/sleep_inline.h
+++ b/pw_thread_freertos/sleep_public_overrides/pw_thread_backend/sleep_inline.h
diff --git a/pw_thread_freertos/snapshot.cc b/pw_thread_freertos/snapshot.cc
index 0d7b8a844..730623598 100644
--- a/pw_thread_freertos/snapshot.cc
+++ b/pw_thread_freertos/snapshot.cc
@@ -16,13 +16,13 @@
#include "pw_thread_freertos/snapshot.h"
-#include <span>
#include <string_view>
#include "FreeRTOS.h"
#include "pw_function/function.h"
#include "pw_log/log.h"
#include "pw_protobuf/encoder.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_thread/snapshot.h"
#include "pw_thread_freertos/config.h"
@@ -44,37 +44,37 @@ extern "C" uint16_t prvTaskCheckFreeStackSpace(const uint8_t* pucStackByte);
// (INCLUDE_uxTaskGetStackHighWaterMark == 1))
void CaptureThreadState(eTaskState thread_state,
- Thread::StreamEncoder& encoder) {
+ proto::Thread::StreamEncoder& encoder) {
switch (thread_state) {
case eRunning:
PW_LOG_DEBUG("Thread state: RUNNING");
- encoder.WriteState(ThreadState::Enum::RUNNING);
+ encoder.WriteState(proto::ThreadState::Enum::RUNNING).IgnoreError();
return;
case eReady:
PW_LOG_DEBUG("Thread state: READY");
- encoder.WriteState(ThreadState::Enum::READY);
+ encoder.WriteState(proto::ThreadState::Enum::READY).IgnoreError();
return;
case eBlocked:
PW_LOG_DEBUG("Thread state: BLOCKED");
- encoder.WriteState(ThreadState::Enum::BLOCKED);
+ encoder.WriteState(proto::ThreadState::Enum::BLOCKED).IgnoreError();
return;
case eSuspended:
PW_LOG_DEBUG("Thread state: SUSPENDED");
- encoder.WriteState(ThreadState::Enum::SUSPENDED);
+ encoder.WriteState(proto::ThreadState::Enum::SUSPENDED).IgnoreError();
return;
case eDeleted:
PW_LOG_DEBUG("Thread state: INACTIVE");
- encoder.WriteState(ThreadState::Enum::INACTIVE);
+ encoder.WriteState(proto::ThreadState::Enum::INACTIVE).IgnoreError();
return;
case eInvalid:
default:
PW_LOG_DEBUG("Thread state: UNKNOWN");
- encoder.WriteState(ThreadState::Enum::UNKNOWN);
+ encoder.WriteState(proto::ThreadState::Enum::UNKNOWN).IgnoreError();
return;
}
}
@@ -82,11 +82,11 @@ void CaptureThreadState(eTaskState thread_state,
} // namespace
Status SnapshotThreads(void* running_thread_stack_pointer,
- SnapshotThreadInfo::StreamEncoder& encoder,
+ proto::SnapshotThreadInfo::StreamEncoder& encoder,
ProcessThreadStackCallback& stack_dumper) {
struct {
void* running_thread_stack_pointer;
- SnapshotThreadInfo::StreamEncoder* encoder;
+ proto::SnapshotThreadInfo::StreamEncoder* encoder;
ProcessThreadStackCallback* stack_dumper;
Status thread_capture_status;
} ctx;
@@ -97,7 +97,8 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
ThreadCallback thread_capture_cb(
[&ctx](TaskHandle_t thread, eTaskState thread_state) -> bool {
- Thread::StreamEncoder thread_encoder = ctx.encoder->GetThreadsEncoder();
+ proto::Thread::StreamEncoder thread_encoder =
+ ctx.encoder->GetThreadsEncoder();
ctx.thread_capture_status.Update(
SnapshotThread(thread,
thread_state,
@@ -114,19 +115,20 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
return ctx.thread_capture_status;
}
-Status SnapshotThread(TaskHandle_t thread,
- eTaskState thread_state,
- void* running_thread_stack_pointer,
- Thread::StreamEncoder& encoder,
- ProcessThreadStackCallback& thread_stack_callback) {
+Status SnapshotThread(
+ TaskHandle_t thread,
+ eTaskState thread_state,
+ void* running_thread_stack_pointer,
+ proto::Thread::StreamEncoder& encoder,
+ [[maybe_unused]] ProcessThreadStackCallback& thread_stack_callback) {
const tskTCB& tcb = *reinterpret_cast<tskTCB*>(thread);
PW_LOG_DEBUG("Capturing thread info for %s", tcb.pcTaskName);
- encoder.WriteName(std::as_bytes(std::span(std::string_view(tcb.pcTaskName))));
+ PW_TRY(encoder.WriteName(as_bytes(span(std::string_view(tcb.pcTaskName)))));
CaptureThreadState(thread_state, encoder);
- // TODO(pwbug/422): Update this once we add support for ascending stacks.
+ // TODO(b/234890430): Update this once we add support for ascending stacks.
static_assert(portSTACK_GROWTH < 0, "Ascending stacks are not yet supported");
// If the thread is active, the stack pointer in the TCB is stale.
diff --git a/pw_thread_freertos/thread.cc b/pw_thread_freertos/thread.cc
index 865b2a311..a7e36814d 100644
--- a/pw_thread_freertos/thread.cc
+++ b/pw_thread_freertos/thread.cc
@@ -26,6 +26,11 @@ using pw::thread::freertos::Context;
namespace pw::thread {
namespace {
+
+#if (INCLUDE_xTaskGetSchedulerState != 1) && (configUSE_TIMERS != 1)
+#error "xTaskGetSchedulerState is required for pw::thread::Thread"
+#endif
+
#if PW_THREAD_JOINING_ENABLED
constexpr EventBits_t kThreadDoneBit = 1 << 0;
#endif // PW_THREAD_JOINING_ENABLED
@@ -188,27 +193,23 @@ Thread::Thread(const thread::Options& facade_options,
void Thread::detach() {
PW_CHECK(joinable());
-#if (INCLUDE_vTaskSuspend == 1) && (INCLUDE_xTaskGetSchedulerState == 1)
- // No need to suspend extra tasks.
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
+#if (INCLUDE_vTaskSuspend == 1)
+ // No need to suspend extra tasks.
vTaskSuspend(native_type_->task_handle());
- }
#else
- // Safe to suspend all tasks while scheduler is not running.
- vTaskSuspendAll();
+ vTaskSuspendAll();
#endif // INCLUDE_vTaskSuspend == 1
+ }
native_type_->set_detached();
const bool thread_done = native_type_->thread_done();
-#if (INCLUDE_vTaskSuspend == 1) && (INCLUDE_xTaskGetSchedulerState == 1)
- // No need to suspend extra tasks, but only safe to call once scheduler is
- // running.
if (xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) {
+#if (INCLUDE_vTaskSuspend == 1)
vTaskResume(native_type_->task_handle());
- }
#else
- // Safe to resume all tasks while scheduler is not running.
- xTaskResumeAll();
+ xTaskResumeAll();
#endif // INCLUDE_vTaskSuspend == 1
+ }
if (thread_done) {
// The task finished (hit end of Context::ThreadEntryPoint) before we
diff --git a/pw_thread_freertos/thread_iteration.cc b/pw_thread_freertos/thread_iteration.cc
new file mode 100644
index 000000000..0d0691d38
--- /dev/null
+++ b/pw_thread_freertos/thread_iteration.cc
@@ -0,0 +1,81 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_thread/thread_iteration.h"
+
+#include <cstddef>
+#include <string_view>
+
+#include "FreeRTOS.h"
+#include "pw_span/span.h"
+#include "pw_status/status.h"
+#include "pw_thread/thread_info.h"
+#include "pw_thread_freertos/freertos_tsktcb.h"
+#include "pw_thread_freertos/util.h"
+#include "pw_thread_freertos_private/thread_iteration.h"
+
+namespace pw::thread {
+namespace freertos {
+
+bool StackInfoCollector(TaskHandle_t current_thread,
+ const pw::thread::ThreadCallback& cb) {
+ const tskTCB& tcb = *reinterpret_cast<tskTCB*>(current_thread);
+ ThreadInfo thread_info;
+
+ span<const std::byte> current_name =
+ as_bytes(span(std::string_view(tcb.pcTaskName)));
+ thread_info.set_thread_name(current_name);
+
+ thread_info.set_stack_pointer(reinterpret_cast<uintptr_t>(tcb.pxTopOfStack));
+
+ // Current thread stack bounds.
+ thread_info.set_stack_low_addr(reinterpret_cast<uintptr_t>(tcb.pxStack));
+#if configRECORD_STACK_HIGH_ADDRESS
+ thread_info.set_stack_high_addr(
+ reinterpret_cast<uintptr_t>(tcb.pxEndOfStack));
+#if INCLUDE_uxTaskGetStackHighWaterMark
+// Walk through the stack from start to end to measure the current peak
+// using high-water marked stack data.
+#if (portSTACK_GROWTH > 0)
+ thread_info.set_stack_peak_addr(
+ thread_info.stack_high_addr().value() -
+ (sizeof(StackType_t) * uxTaskGetStackHighWaterMark(current_thread)));
+#else
+ thread_info.set_stack_peak_addr(
+ thread_info.stack_low_addr().value() +
+ (sizeof(StackType_t) * uxTaskGetStackHighWaterMark(current_thread)));
+#endif // portSTACK_GROWTH > 0
+#endif // INCLUDE_uxTaskGetStackHighWaterMark
+#endif // configRECORD_STACK_HIGH_ADDRESS
+
+ return cb(thread_info);
+}
+
+} // namespace freertos
+
+// This will disable the scheduler.
+Status ForEachThread(const pw::thread::ThreadCallback& cb) {
+ pw::thread::freertos::ThreadCallback adapter_cb =
+ [&cb](TaskHandle_t current_thread, eTaskState) -> bool {
+ return freertos::StackInfoCollector(current_thread, cb);
+ };
+ // Suspend scheduler.
+ vTaskSuspendAll();
+ Status status = pw::thread::freertos::ForEachThread(adapter_cb);
+ // Resume scheduler.
+ xTaskResumeAll();
+ return status;
+}
+
+} // namespace pw::thread
diff --git a/pw_thread_freertos/thread_iteration_test.cc b/pw_thread_freertos/thread_iteration_test.cc
new file mode 100644
index 000000000..05f4e3625
--- /dev/null
+++ b/pw_thread_freertos/thread_iteration_test.cc
@@ -0,0 +1,160 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_thread/thread_iteration.h"
+
+#include <cstddef>
+#include <string_view>
+
+#include "FreeRTOS.h"
+#include "gtest/gtest.h"
+#include "pw_bytes/span.h"
+#include "pw_span/span.h"
+#include "pw_string/string_builder.h"
+#include "pw_string/util.h"
+#include "pw_sync/thread_notification.h"
+#include "pw_thread/test_threads.h"
+#include "pw_thread/thread.h"
+#include "pw_thread/thread_info.h"
+#include "pw_thread_freertos/freertos_tsktcb.h"
+#include "pw_thread_freertos_private/thread_iteration.h"
+
+namespace pw::thread::freertos {
+namespace {
+
+sync::ThreadNotification lock_start;
+sync::ThreadNotification lock_end;
+
+void ForkedThreadEntry(void*) {
+ // Release start lock to allow test thread to continue execution.
+ lock_start.release();
+ while (true) {
+ // Return only when end lock released by test thread.
+ if (lock_end.try_acquire()) {
+ return;
+ }
+ }
+}
+
+// Tests thread iteration API by:
+// - Forking a test thread.
+// - Using iteration API to iterate over all running threads.
+// - Compares name of forked thread and current thread.
+// - Confirms thread exists and is iterated over.
+TEST(ThreadIteration, ForkOneThread) {
+ const auto& options = *static_cast<const pw::thread::freertos::Options*>(
+ &thread::test::TestOptionsThread0());
+ thread::Thread t(options, ForkedThreadEntry);
+
+ // Blocked until thread t releases start lock.
+ lock_start.acquire();
+
+ struct {
+ bool thread_exists;
+ span<const std::byte> name;
+ } temp_struct;
+
+ temp_struct.thread_exists = false;
+ // Max permissible length of task name including null byte.
+ static constexpr size_t buffer_size = configMAX_TASK_NAME_LEN;
+
+ std::string_view string(string::ClampedCString(options.name(), buffer_size));
+ temp_struct.name = as_bytes(span(string));
+
+ // Callback that confirms forked thread is checked by the iterator.
+ auto cb = [&temp_struct](const ThreadInfo& thread_info) {
+ // Compare sizes accounting for null byte.
+ if (thread_info.thread_name().has_value()) {
+ for (size_t i = 0; i < thread_info.thread_name().value().size(); i++) {
+ // Compare character by character of span.
+ if ((unsigned char)thread_info.thread_name().value().data()[i] !=
+ (unsigned char)temp_struct.name.data()[i]) {
+ return true;
+ }
+ }
+ temp_struct.thread_exists = true;
+ }
+ // Signal to stop iteration.
+ return false;
+ };
+
+ thread::ForEachThread(cb);
+
+ // Signal to forked thread that execution is complete.
+ lock_end.release();
+
+ // Clean up the test thread context.
+#if PW_THREAD_JOINING_ENABLED
+ t.join();
+#else
+ t.detach();
+ thread::test::WaitUntilDetachedThreadsCleanedUp();
+#endif // PW_THREAD_JOINING_ENABLED
+
+ EXPECT_TRUE(temp_struct.thread_exists);
+}
+
+#if INCLUDE_uxTaskGetStackHighWaterMark
+#if configRECORD_STACK_HIGH_ADDRESS
+
+TEST(ThreadIteration, StackInfoCollector_PeakStackUsage) {
+ // This is the value FreeRTOS expects, but it's worth noting that there's no
+ // easy way to get this value directly from FreeRTOS.
+ constexpr uint8_t tskSTACK_FILL_BYTE = 0xa5U;
+ std::array<StackType_t, 128> stack;
+ ByteSpan stack_bytes(as_writable_bytes(span(stack)));
+ std::memset(stack_bytes.data(), tskSTACK_FILL_BYTE, stack_bytes.size_bytes());
+
+ tskTCB fake_tcb;
+ StringBuilder sb(fake_tcb.pcTaskName);
+ sb.append("FakeTCB");
+ fake_tcb.pxStack = stack.data();
+ fake_tcb.pxEndOfStack = stack.data() + stack.size();
+
+ // Clobber bytes as if they were used.
+ constexpr size_t kBytesRemaining = 96;
+#if portSTACK_GROWTH > 0
+ std::memset(stack_bytes.data(),
+ tskSTACK_FILL_BYTE ^ 0x2b,
+ stack_bytes.size() - kBytesRemaining);
+#else
+ std::memset(&stack_bytes[kBytesRemaining],
+ tskSTACK_FILL_BYTE ^ 0x2b,
+ stack_bytes.size() - kBytesRemaining);
+#endif // portSTACK_GROWTH > 0
+
+ ThreadCallback cb = [kBytesRemaining](const ThreadInfo& info) -> bool {
+ EXPECT_TRUE(info.stack_high_addr().has_value());
+ EXPECT_TRUE(info.stack_low_addr().has_value());
+ EXPECT_TRUE(info.stack_peak_addr().has_value());
+
+#if portSTACK_GROWTH > 0
+ EXPECT_EQ(info.stack_high_addr().value() - info.stack_peak_addr().value(),
+ kBytesRemaining);
+#else
+ EXPECT_EQ(info.stack_peak_addr().value() - info.stack_low_addr().value(),
+ kBytesRemaining);
+#endif // portSTACK_GROWTH > 0
+ return true;
+ };
+
+ EXPECT_TRUE(
+ StackInfoCollector(reinterpret_cast<TaskHandle_t>(&fake_tcb), cb));
+}
+
+#endif // INCLUDE_uxTaskGetStackHighWaterMark
+#endif // configRECORD_STACK_HIGH_ADDRESS
+
+} // namespace
+} // namespace pw::thread::freertos
diff --git a/pw_thread_freertos/public_overrides/pw_thread_backend/thread_inline.h b/pw_thread_freertos/thread_public_overrides/pw_thread_backend/thread_inline.h
index 7305e9162..7305e9162 100644
--- a/pw_thread_freertos/public_overrides/pw_thread_backend/thread_inline.h
+++ b/pw_thread_freertos/thread_public_overrides/pw_thread_backend/thread_inline.h
diff --git a/pw_thread_freertos/public_overrides/pw_thread_backend/thread_native.h b/pw_thread_freertos/thread_public_overrides/pw_thread_backend/thread_native.h
index 478f79120..478f79120 100644
--- a/pw_thread_freertos/public_overrides/pw_thread_backend/thread_native.h
+++ b/pw_thread_freertos/thread_public_overrides/pw_thread_backend/thread_native.h
diff --git a/pw_thread_freertos/public_overrides/pw_thread_backend/yield_inline.h b/pw_thread_freertos/yield_public_overrides/pw_thread_backend/yield_inline.h
index 4ad04ab5c..4ad04ab5c 100644
--- a/pw_thread_freertos/public_overrides/pw_thread_backend/yield_inline.h
+++ b/pw_thread_freertos/yield_public_overrides/pw_thread_backend/yield_inline.h
diff --git a/pw_thread_stl/BUILD.bazel b/pw_thread_stl/BUILD.bazel
index 91837a0cf..3923c7c66 100644
--- a/pw_thread_stl/BUILD.bazel
+++ b/pw_thread_stl/BUILD.bazel
@@ -29,14 +29,14 @@ licenses(["notice"])
pw_cc_library(
name = "id_headers",
hdrs = [
+ "id_public_overrides/pw_thread_backend/id_inline.h",
+ "id_public_overrides/pw_thread_backend/id_native.h",
"public/pw_thread_stl/id_inline.h",
"public/pw_thread_stl/id_native.h",
- "public_overrides/pw_thread_backend/id_inline.h",
- "public_overrides/pw_thread_backend/id_native.h",
],
includes = [
+ "id_public_overrides",
"public",
- "public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
)
@@ -54,11 +54,11 @@ pw_cc_library(
name = "sleep_headers",
hdrs = [
"public/pw_thread_stl/sleep_inline.h",
- "public_overrides/pw_thread_backend/sleep_inline.h",
+ "sleep_public_overrides/pw_thread_backend/sleep_inline.h",
],
includes = [
"public",
- "public_overrides",
+ "sleep_public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
@@ -82,12 +82,12 @@ pw_cc_library(
"public/pw_thread_stl/options.h",
"public/pw_thread_stl/thread_inline.h",
"public/pw_thread_stl/thread_native.h",
- "public_overrides/pw_thread_backend/thread_inline.h",
- "public_overrides/pw_thread_backend/thread_native.h",
+ "thread_public_overrides/pw_thread_backend/thread_inline.h",
+ "thread_public_overrides/pw_thread_backend/thread_native.h",
],
includes = [
"public",
- "public_overrides",
+ "thread_public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
)
@@ -101,6 +101,18 @@ pw_cc_library(
],
)
+# This target provides a stub backend for pw::this_thread::thread_iteration.
+# Iterating over child threads isn't supported by STL, so this only exists
+# for portability reasons.
+pw_cc_library(
+ name = "thread_iteration",
+ srcs = ["thread_iteration.cc"],
+ deps = [
+ "//pw_status",
+ "//pw_thread:thread_iteration_facade",
+ ],
+)
+
pw_cc_library(
name = "test_threads",
srcs = [
@@ -126,11 +138,11 @@ pw_cc_library(
name = "yield_headers",
hdrs = [
"public/pw_thread_stl/yield_inline.h",
- "public_overrides/pw_thread_backend/yield_inline.h",
+ "yield_public_overrides/pw_thread_backend/yield_inline.h",
],
includes = [
"public",
- "public_overrides",
+ "yield_public_overrides",
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
)
diff --git a/pw_thread_stl/BUILD.gn b/pw_thread_stl/BUILD.gn
index 0e4e6b663..da4f2991a 100644
--- a/pw_thread_stl/BUILD.gn
+++ b/pw_thread_stl/BUILD.gn
@@ -26,8 +26,8 @@ config("public_include_path") {
visibility = [ ":*" ]
}
-config("backend_config") {
- include_dirs = [ "public_overrides" ]
+config("id_public_overrides") {
+ include_dirs = [ "id_public_overrides" ]
visibility = [ ":*" ]
}
@@ -35,30 +35,35 @@ config("backend_config") {
pw_source_set("id") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":id_public_overrides",
]
public = [
+ "id_public_overrides/pw_thread_backend/id_inline.h",
+ "id_public_overrides/pw_thread_backend/id_native.h",
"public/pw_thread_stl/id_inline.h",
"public/pw_thread_stl/id_native.h",
- "public_overrides/pw_thread_backend/id_inline.h",
- "public_overrides/pw_thread_backend/id_native.h",
]
deps = [ "$dir_pw_thread:id.facade" ]
}
+config("thread_public_overrides") {
+ include_dirs = [ "thread_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::thread::Thread with joining
# joining capability.
pw_source_set("thread") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":thread_public_overrides",
]
public = [
"public/pw_thread_stl/options.h",
"public/pw_thread_stl/thread_inline.h",
"public/pw_thread_stl/thread_native.h",
- "public_overrides/pw_thread_backend/thread_inline.h",
- "public_overrides/pw_thread_backend/thread_native.h",
+ "thread_public_overrides/pw_thread_backend/thread_inline.h",
+ "thread_public_overrides/pw_thread_backend/thread_native.h",
]
allow_circular_includes_from = [ "$dir_pw_thread:thread.facade" ]
deps = [ "$dir_pw_thread:thread.facade" ]
@@ -75,15 +80,20 @@ pw_build_assert("check_system_clock_backend") {
"\"$dir_pw_chrono_stl:system_clock\")"
}
+config("sleep_public_overrides") {
+ include_dirs = [ "sleep_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::this_thread::sleep_{for,until}.
pw_source_set("sleep") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":sleep_public_overrides",
]
public = [
"public/pw_thread_stl/sleep_inline.h",
- "public_overrides/pw_thread_backend/sleep_inline.h",
+ "sleep_public_overrides/pw_thread_backend/sleep_inline.h",
]
deps = [
":check_system_clock_backend",
@@ -92,19 +102,35 @@ pw_source_set("sleep") {
]
}
+config("yield_public_overrides") {
+ include_dirs = [ "yield_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::this_thread::yield.
pw_source_set("yield") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":yield_public_overrides",
]
public = [
"public/pw_thread_stl/yield_inline.h",
- "public_overrides/pw_thread_backend/yield_inline.h",
+ "yield_public_overrides/pw_thread_backend/yield_inline.h",
]
deps = [ "$dir_pw_thread:yield.facade" ]
}
+# This target provides a stub backend for pw::this_thread::thread_iteration.
+# Iterating over child threads isn't supported by STL, so this only exists
+# for portability reasons.
+pw_source_set("thread_iteration") {
+ deps = [
+ "$dir_pw_thread:thread_iteration.facade",
+ dir_pw_status,
+ ]
+ sources = [ "thread_iteration.cc" ]
+}
+
pw_test_group("tests") {
tests = [ ":thread_backend_test" ]
}
diff --git a/pw_thread_stl/CMakeLists.txt b/pw_thread_stl/CMakeLists.txt
index 86f916eb3..7f290e5b3 100644
--- a/pw_thread_stl/CMakeLists.txt
+++ b/pw_thread_stl/CMakeLists.txt
@@ -15,63 +15,62 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
# This target provides the backend for pw::thread::Id & pw::this_thread::get_id.
-pw_add_module_library(pw_thread_stl.id
- IMPLEMENTS_FACADES
- pw_thread.id
+pw_add_library(pw_thread_stl.id INTERFACE
HEADERS
public/pw_thread_stl/id_inline.h
public/pw_thread_stl/id_native.h
- public_overrides/pw_thread_backend/id_inline.h
- public_overrides/pw_thread_backend/id_native.h
+ id_public_overrides/pw_thread_backend/id_inline.h
+ id_public_overrides/pw_thread_backend/id_native.h
PUBLIC_INCLUDES
public
- public_overrides
+ id_public_overrides
+ PUBLIC_DEPS
+ pw_thread.id.facade
)
# This target provides the backend for pw::thread::Thread with joining
# joining capability.
-pw_add_module_library(pw_thread_stl.thread
- IMPLEMENTS_FACADES
- pw_thread.thread
+pw_add_library(pw_thread_stl.thread INTERFACE
HEADERS
public/pw_thread_stl/options.h
public/pw_thread_stl/thread_inline.h
public/pw_thread_stl/thread_native.h
- public_overrides/pw_thread_backend/thread_inline.h
- public_overrides/pw_thread_backend/thread_native.h
+ thread_public_overrides/pw_thread_backend/thread_inline.h
+ thread_public_overrides/pw_thread_backend/thread_native.h
PUBLIC_INCLUDES
public
- public_overrides
+ thread_public_overrides
+ PUBLIC_DEPS
+ pw_thread.thread.facade
)
# This target provides the backend for pw::this_thread::sleep_{for,until}.
-pw_add_module_library(pw_thread_stl.sleep
- IMPLEMENTS_FACADES
- pw_thread.sleep
+pw_add_library(pw_thread_stl.sleep INTERFACE
HEADERS
public/pw_thread_stl/sleep_inline.h
- public_overrides/pw_thread_backend/sleep_inline.h
+ sleep_public_overrides/pw_thread_backend/sleep_inline.h
PUBLIC_INCLUDES
public
- public_overrides
+ sleep_public_overrides
PUBLIC_DEPS
pw_chrono.system_clock
+ pw_thread.sleep.facade
)
# This target provides the backend for pw::this_thread::yield.
-pw_add_module_library(pw_thread_stl.yield
- IMPLEMENTS_FACADES
- pw_thread.yield
+pw_add_library(pw_thread_stl.yield INTERFACE
HEADERS
public/pw_thread_stl/yield_inline.h
- public_overrides/pw_thread_backend/yield_inline.h
+ yield_public_overrides/pw_thread_backend/yield_inline.h
PUBLIC_INCLUDES
public
- public_overrides
+ yield_public_overrides
+ PUBLIC_DEPS
+ pw_thread.yield.facade
)
-pw_add_module_library(pw_thread_stl.test_threads
+pw_add_library(pw_thread_stl.test_threads STATIC
PUBLIC_DEPS
pw_thread.test_threads
SOURCES
@@ -81,9 +80,9 @@ pw_add_module_library(pw_thread_stl.test_threads
)
if(("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread") AND
- (NOT "${pw_thread.sleep_BACKEND}" STREQUAL "pw_thread.sleep.NO_BACKEND_SET"))
+ (NOT "${pw_thread.sleep_BACKEND}" STREQUAL ""))
pw_add_test(pw_thread_stl.thread_backend_test
- DEPS
+ PRIVATE_DEPS
pw_thread_stl.test_threads
pw_thread.thread_facade_test
GROUPS
diff --git a/pw_thread_stl/public_overrides/pw_thread_backend/id_inline.h b/pw_thread_stl/id_public_overrides/pw_thread_backend/id_inline.h
index 56f5e5d4f..56f5e5d4f 100644
--- a/pw_thread_stl/public_overrides/pw_thread_backend/id_inline.h
+++ b/pw_thread_stl/id_public_overrides/pw_thread_backend/id_inline.h
diff --git a/pw_thread_stl/public_overrides/pw_thread_backend/id_native.h b/pw_thread_stl/id_public_overrides/pw_thread_backend/id_native.h
index 82aa09685..82aa09685 100644
--- a/pw_thread_stl/public_overrides/pw_thread_backend/id_native.h
+++ b/pw_thread_stl/id_public_overrides/pw_thread_backend/id_native.h
diff --git a/pw_thread_stl/public/pw_thread_stl/options.h b/pw_thread_stl/public/pw_thread_stl/options.h
index 26f5bf578..fc5fefbd2 100644
--- a/pw_thread_stl/public/pw_thread_stl/options.h
+++ b/pw_thread_stl/public/pw_thread_stl/options.h
@@ -21,6 +21,9 @@ namespace pw::thread::stl {
// Instead, users are expected to start the thread and after dynamically adjust
// the thread's attributes using std::thread::native_handle based on the native
// threading APIs.
-class Options : public thread::Options {};
+class Options : public thread::Options {
+ public:
+ constexpr Options() {}
+};
} // namespace pw::thread::stl
diff --git a/pw_thread_stl/public/pw_thread_stl/sleep_inline.h b/pw_thread_stl/public/pw_thread_stl/sleep_inline.h
index d10787127..f98da458e 100644
--- a/pw_thread_stl/public/pw_thread_stl/sleep_inline.h
+++ b/pw_thread_stl/public/pw_thread_stl/sleep_inline.h
@@ -13,6 +13,7 @@
// the License.
#pragma once
+#include <algorithm>
#include <thread>
#include "pw_chrono/system_clock.h"
diff --git a/pw_thread_stl/public/pw_thread_stl/thread_inline.h b/pw_thread_stl/public/pw_thread_stl/thread_inline.h
index 018e80755..1b4a5679a 100644
--- a/pw_thread_stl/public/pw_thread_stl/thread_inline.h
+++ b/pw_thread_stl/public/pw_thread_stl/thread_inline.h
@@ -15,6 +15,8 @@
#include <thread>
+#include "pw_thread/thread.h"
+
namespace pw::thread {
inline Thread::Thread() : native_type_() {}
diff --git a/pw_thread_stl/public/pw_thread_stl/yield_inline.h b/pw_thread_stl/public/pw_thread_stl/yield_inline.h
index ec018533d..8fee713de 100644
--- a/pw_thread_stl/public/pw_thread_stl/yield_inline.h
+++ b/pw_thread_stl/public/pw_thread_stl/yield_inline.h
@@ -15,6 +15,8 @@
#include <thread>
+#include "pw_thread/yield.h"
+
namespace pw::this_thread {
inline void yield() noexcept { std::this_thread::yield(); }
diff --git a/pw_thread_stl/public_overrides/pw_thread_backend/sleep_inline.h b/pw_thread_stl/sleep_public_overrides/pw_thread_backend/sleep_inline.h
index 8d0132a85..8d0132a85 100644
--- a/pw_thread_stl/public_overrides/pw_thread_backend/sleep_inline.h
+++ b/pw_thread_stl/sleep_public_overrides/pw_thread_backend/sleep_inline.h
diff --git a/pw_thread_stl/thread_iteration.cc b/pw_thread_stl/thread_iteration.cc
new file mode 100644
index 000000000..034a88f61
--- /dev/null
+++ b/pw_thread_stl/thread_iteration.cc
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_thread/thread_iteration.h"
+
+#include "pw_status/status.h"
+
+namespace pw::thread {
+
+// Stub backend implementation for STL. Unable to provide real implementation
+// for thread iteration on STL targets.
+Status ForEachThread([[maybe_unused]] const pw::thread::ThreadCallback& cb) {
+ return Status::Unimplemented();
+}
+
+} // namespace pw::thread
diff --git a/pw_thread_stl/public_overrides/pw_thread_backend/thread_inline.h b/pw_thread_stl/thread_public_overrides/pw_thread_backend/thread_inline.h
index 2a52ee866..2a52ee866 100644
--- a/pw_thread_stl/public_overrides/pw_thread_backend/thread_inline.h
+++ b/pw_thread_stl/thread_public_overrides/pw_thread_backend/thread_inline.h
diff --git a/pw_thread_stl/public_overrides/pw_thread_backend/thread_native.h b/pw_thread_stl/thread_public_overrides/pw_thread_backend/thread_native.h
index 272a59577..272a59577 100644
--- a/pw_thread_stl/public_overrides/pw_thread_backend/thread_native.h
+++ b/pw_thread_stl/thread_public_overrides/pw_thread_backend/thread_native.h
diff --git a/pw_thread_stl/public_overrides/pw_thread_backend/yield_inline.h b/pw_thread_stl/yield_public_overrides/pw_thread_backend/yield_inline.h
index 61528f66b..61528f66b 100644
--- a/pw_thread_stl/public_overrides/pw_thread_backend/yield_inline.h
+++ b/pw_thread_stl/yield_public_overrides/pw_thread_backend/yield_inline.h
diff --git a/pw_thread_threadx/BUILD.bazel b/pw_thread_threadx/BUILD.bazel
index fc3718ddc..78f7f36b7 100644
--- a/pw_thread_threadx/BUILD.bazel
+++ b/pw_thread_threadx/BUILD.bazel
@@ -25,14 +25,14 @@ licenses(["notice"])
pw_cc_library(
name = "id_headers",
hdrs = [
+ "id_public_overrides/pw_thread_backend/id_inline.h",
+ "id_public_overrides/pw_thread_backend/id_native.h",
"public/pw_thread_threadx/id_inline.h",
"public/pw_thread_threadx/id_native.h",
- "public_overrides/pw_thread_backend/id_inline.h",
- "public_overrides/pw_thread_backend/id_native.h",
],
includes = [
+ "id_public_overrides",
"public",
- "public_overrides",
],
)
@@ -42,7 +42,7 @@ pw_cc_library(
":id_headers",
"//pw_thread:id_facade",
],
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
+ # TODO(b/234876414): This should depend on ThreadX but our third parties
# currently do not have Bazel support.
)
@@ -55,20 +55,22 @@ pw_cc_library(
"public/pw_thread_threadx/options.h",
"public/pw_thread_threadx/thread_inline.h",
"public/pw_thread_threadx/thread_native.h",
- "public_overrides/pw_thread_backend/thread_inline.h",
- "public_overrides/pw_thread_backend/thread_native.h",
+ "thread_public_overrides/pw_thread_backend/thread_inline.h",
+ "thread_public_overrides/pw_thread_backend/thread_native.h",
],
includes = [
"public",
- "public_overrides",
+ "thread_public_overrides",
],
+ # TODO(b/257321712): This target doesn't build.
+ tags = ["manual"],
deps = [
":id",
"//pw_assert",
"//pw_string",
- "//pw_thread:thread_headers",
+ "//pw_thread:thread_facade",
],
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
+ # TODO(b/234876414): This should depend on ThreadX but our third parties
# currently do not have Bazel support.
)
@@ -77,13 +79,13 @@ pw_cc_library(
srcs = [
"thread.cc",
],
+ # TODO(b/257321712): This target doesn't build.
+ tags = ["manual"],
deps = [
":id",
":thread_headers",
"//pw_assert",
],
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -91,6 +93,8 @@ pw_cc_library(
srcs = [
"test_threads.cc",
],
+ # TODO(b/257321712): This target doesn't build.
+ tags = ["manual"],
deps = [
"//pw_chrono:system_clock",
"//pw_thread:sleep",
@@ -101,6 +105,8 @@ pw_cc_library(
pw_cc_test(
name = "thread_backend_test",
+ # TODO(b/257321712): This target doesn't build.
+ tags = ["manual"],
deps = [
":test_threads",
"//pw_thread:thread_facade_test",
@@ -111,11 +117,11 @@ pw_cc_library(
name = "sleep_headers",
hdrs = [
"public/pw_thread_threadx/sleep_inline.h",
- "public_overrides/pw_thread_backend/sleep_inline.h",
+ "sleep_public_overrides/pw_thread_backend/sleep_inline.h",
],
includes = [
"public",
- "public_overrides",
+ "sleep_public_overrides",
],
deps = [
"//pw_chrono:system_clock",
@@ -127,6 +133,8 @@ pw_cc_library(
srcs = [
"sleep.cc",
],
+ # TODO(b/257321712): This target doesn't build.
+ tags = ["manual"],
deps = [
":sleep_headers",
"//pw_assert",
@@ -134,22 +142,20 @@ pw_cc_library(
"//pw_chrono_threadx:system_clock_headers",
"//pw_thread:sleep_facade",
],
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
name = "yield_headers",
hdrs = [
"public/pw_thread_threadx/yield_inline.h",
- "public_overrides/pw_thread_backend/yield_inline.h",
+ "yield_public_overrides/pw_thread_backend/yield_inline.h",
],
includes = [
"public",
- "public_overrides",
+ "yield_public_overrides",
],
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
+ # TODO(b/257321712): This target doesn't build.
+ tags = ["manual"],
)
pw_cc_library(
@@ -168,12 +174,13 @@ pw_cc_library(
hdrs = [
"public/pw_thread_threadx/util.h",
],
+ includes = ["public"],
+ # TODO(b/257321712): This target doesn't build.
+ tags = ["manual"],
deps = [
"//pw_function",
"//pw_status",
],
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
)
pw_cc_library(
@@ -184,6 +191,8 @@ pw_cc_library(
hdrs = [
"public/pw_thread_threadx/snapshot.h",
],
+ # TODO(b/257321712): This target doesn't build.
+ tags = ["manual"],
deps = [
":util",
"//pw_bytes",
@@ -191,8 +200,6 @@ pw_cc_library(
"//pw_log",
"//pw_protobuf",
"//pw_status",
- "//pw_thread:protos",
+ "//pw_thread:thread_cc.pwpb",
],
- # TODO(pwbug/317): This should depend on ThreadX but our third parties
- # currently do not have Bazel support.
)
diff --git a/pw_thread_threadx/BUILD.gn b/pw_thread_threadx/BUILD.gn
index 291714790..fb314380c 100644
--- a/pw_thread_threadx/BUILD.gn
+++ b/pw_thread_threadx/BUILD.gn
@@ -48,11 +48,16 @@ pw_source_set("config") {
]
}
+config("id_public_overrides") {
+ include_dirs = [ "id_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::thread::Id.
pw_source_set("id") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":id_public_overrides",
]
public_deps = [
"$dir_pw_assert",
@@ -60,10 +65,10 @@ pw_source_set("id") {
"$dir_pw_third_party/threadx",
]
public = [
+ "id_public_overrides/pw_thread_backend/id_inline.h",
+ "id_public_overrides/pw_thread_backend/id_native.h",
"public/pw_thread_threadx/id_inline.h",
"public/pw_thread_threadx/id_native.h",
- "public_overrides/pw_thread_backend/id_inline.h",
- "public_overrides/pw_thread_backend/id_native.h",
]
deps = [ "$dir_pw_thread:id.facade" ]
}
@@ -77,15 +82,20 @@ if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_SLEEP_BACKEND != "") {
"works with the ThreadX pw::chrono::SystemClock backend."
}
+ config("sleep_public_overrides") {
+ include_dirs = [ "sleep_public_overrides" ]
+ visibility = [ ":*" ]
+ }
+
# This target provides the backend for pw::this_thread::sleep_{for,until}.
pw_source_set("sleep") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":sleep_public_overrides",
]
public = [
"public/pw_thread_threadx/sleep_inline.h",
- "public_overrides/pw_thread_backend/sleep_inline.h",
+ "sleep_public_overrides/pw_thread_backend/sleep_inline.h",
]
public_deps = [ "$dir_pw_chrono:system_clock" ]
sources = [ "sleep.cc" ]
@@ -99,12 +109,17 @@ if (pw_chrono_SYSTEM_CLOCK_BACKEND != "" && pw_thread_SLEEP_BACKEND != "") {
}
}
+config("thread_public_overrides") {
+ include_dirs = [ "thread_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::thread::Thread and the headers needed
# for thread creation.
pw_source_set("thread") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":thread_public_overrides",
]
public_deps = [
":config",
@@ -113,28 +128,34 @@ pw_source_set("thread") {
"$dir_pw_third_party/threadx",
"$dir_pw_thread:id",
"$dir_pw_thread:thread.facade",
+ dir_pw_span,
]
public = [
"public/pw_thread_threadx/context.h",
"public/pw_thread_threadx/options.h",
"public/pw_thread_threadx/thread_inline.h",
"public/pw_thread_threadx/thread_native.h",
- "public_overrides/pw_thread_backend/thread_inline.h",
- "public_overrides/pw_thread_backend/thread_native.h",
+ "thread_public_overrides/pw_thread_backend/thread_inline.h",
+ "thread_public_overrides/pw_thread_backend/thread_native.h",
]
allow_circular_includes_from = [ "$dir_pw_thread:thread.facade" ]
sources = [ "thread.cc" ]
}
+config("yield_public_overrides") {
+ include_dirs = [ "yield_public_overrides" ]
+ visibility = [ ":*" ]
+}
+
# This target provides the backend for pw::this_thread::yield.
pw_source_set("yield") {
public_configs = [
":public_include_path",
- ":backend_config",
+ ":yield_public_overrides",
]
public = [
"public/pw_thread_threadx/yield_inline.h",
- "public_overrides/pw_thread_backend/yield_inline.h",
+ "yield_public_overrides/pw_thread_backend/yield_inline.h",
]
public_deps = [
"$dir_pw_assert",
diff --git a/pw_thread_threadx/docs.rst b/pw_thread_threadx/docs.rst
index 4b73a409d..026ed2bff 100644
--- a/pw_thread_threadx/docs.rst
+++ b/pw_thread_threadx/docs.rst
@@ -175,7 +175,7 @@ ThreadX Thread Options
Set the pre-allocated context (all memory needed to run a thread). Note
that this is required for this thread creation backend! The Context can
- either be constructed with an externally provided ``std::span<ULONG>``
+ either be constructed with an externally provided ``pw::span<ULONG>``
stack or the templated form of ``ContextWihtStack<kStackSizeWords`` can be
used.
@@ -240,7 +240,7 @@ captured. For ARM Cortex-M CPUs, you can do something like this:
void* stack_ptr = 0;
asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
pw::thread::ProcessThreadStackCallback cb =
- [](pw::thread::Thread::StreamEncoder& encoder,
+ [](pw::thread::proto::Thread::StreamEncoder& encoder,
pw::ConstByteSpan stack) -> pw::Status {
return encoder.WriteRawStack(stack);
};
@@ -248,7 +248,8 @@ captured. For ARM Cortex-M CPUs, you can do something like this:
snapshot_encoder, cb);
``SnapshotThreads()`` wraps the singular thread capture to instead captures
-all created threads to a ``pw::thread::SnapshotThreadInfo`` message. This proto
-message overlays a snapshot, so it is safe to static cast a
+all created threads to a ``pw::thread::proto::SnapshotThreadInfo`` message.
+This proto message overlays a snapshot, so it is safe to static cast a
``pw::snapshot::Snapshot::StreamEncoder`` to a
-``pw::thread::SnapshotThreadInfo::StreamEncoder`` when calling this function.
+``pw::thread::proto::SnapshotThreadInfo::StreamEncoder`` when calling this
+function.
diff --git a/pw_thread_threadx/public_overrides/pw_thread_backend/id_inline.h b/pw_thread_threadx/id_public_overrides/pw_thread_backend/id_inline.h
index 046e84243..046e84243 100644
--- a/pw_thread_threadx/public_overrides/pw_thread_backend/id_inline.h
+++ b/pw_thread_threadx/id_public_overrides/pw_thread_backend/id_inline.h
diff --git a/pw_thread_threadx/public_overrides/pw_thread_backend/id_native.h b/pw_thread_threadx/id_public_overrides/pw_thread_backend/id_native.h
index bf144ddb6..bf144ddb6 100644
--- a/pw_thread_threadx/public_overrides/pw_thread_backend/id_native.h
+++ b/pw_thread_threadx/id_public_overrides/pw_thread_backend/id_native.h
diff --git a/pw_thread_threadx/public/pw_thread_threadx/context.h b/pw_thread_threadx/public/pw_thread_threadx/context.h
index 8d5c87cc7..903caea28 100644
--- a/pw_thread_threadx/public/pw_thread_threadx/context.h
+++ b/pw_thread_threadx/public/pw_thread_threadx/context.h
@@ -15,8 +15,8 @@
#include <cstdint>
#include <cstring>
-#include <span>
+#include "pw_span/span.h"
#include "pw_string/util.h"
#include "pw_thread_threadx/config.h"
#include "tx_api.h"
@@ -47,8 +47,7 @@ namespace pw::thread::threadx {
// }
class Context {
public:
- explicit Context(std::span<ULONG> stack_span)
- : tcb_{}, stack_span_(stack_span) {}
+ explicit Context(span<ULONG> stack_span) : tcb_{}, stack_span_(stack_span) {}
Context(const Context&) = delete;
Context& operator=(const Context&) = delete;
@@ -58,7 +57,7 @@ class Context {
private:
friend Thread;
- std::span<ULONG> stack() { return stack_span_; }
+ span<ULONG> stack() { return stack_span_; }
bool in_use() const { return in_use_; }
void set_in_use(bool in_use = true) { in_use_ = in_use; }
@@ -86,7 +85,7 @@ class Context {
static void DeleteThread(Context& context);
TX_THREAD tcb_;
- std::span<ULONG> stack_span_;
+ span<ULONG> stack_span_;
ThreadRoutine user_thread_entry_function_ = nullptr;
void* user_thread_entry_arg_ = nullptr;
diff --git a/pw_thread_threadx/public/pw_thread_threadx/id_inline.h b/pw_thread_threadx/public/pw_thread_threadx/id_inline.h
index 1ae08b812..369383935 100644
--- a/pw_thread_threadx/public/pw_thread_threadx/id_inline.h
+++ b/pw_thread_threadx/public/pw_thread_threadx/id_inline.h
@@ -21,7 +21,7 @@
namespace pw::this_thread {
-inline thread::Id get_id() {
+inline thread::Id get_id() noexcept {
// When this value is 0, a thread is executing or the system is idle.
// Other values indicate that interrupt or initialization processing is
// active.
diff --git a/pw_thread_threadx/public/pw_thread_threadx/options.h b/pw_thread_threadx/public/pw_thread_threadx/options.h
index 079b58734..d73f3a935 100644
--- a/pw_thread_threadx/public/pw_thread_threadx/options.h
+++ b/pw_thread_threadx/public/pw_thread_threadx/options.h
@@ -13,6 +13,8 @@
// the License.
#pragma once
+#include <optional>
+
#include "pw_assert/assert.h"
#include "pw_thread/thread.h"
#include "pw_thread_threadx/config.h"
@@ -46,9 +48,9 @@ namespace pw::thread::threadx {
//
class Options : public thread::Options {
public:
- constexpr Options() = default;
+ constexpr Options() {}
constexpr Options(const Options&) = default;
- constexpr Options(Options&& other) = default;
+ constexpr Options(Options&&) = default;
// Sets the name for the ThreadX thread, note that this will be deep copied
// into the context and may be truncated based on
@@ -112,7 +114,7 @@ class Options : public thread::Options {
// Set the pre-allocated context (all memory needed to run a thread). Note
// that this is required for this thread creation backend! The Context can
- // either be constructed with an externally provided std::span<ULONG> stack
+ // either be constructed with an externally provided span<ULONG> stack
// or the templated form of ContextWihtStack<kStackSizeWords> can be used.
constexpr Options& set_context(Context& context) {
context_ = &context;
diff --git a/pw_thread_threadx/public/pw_thread_threadx/snapshot.h b/pw_thread_threadx/public/pw_thread_threadx/snapshot.h
index 1845b53d9..1b82bc705 100644
--- a/pw_thread_threadx/public/pw_thread_threadx/snapshot.h
+++ b/pw_thread_threadx/public/pw_thread_threadx/snapshot.h
@@ -31,7 +31,7 @@ namespace pw::thread::threadx {
// void* stack_ptr = 0;
// asm volatile("mrs %0, psp\n" : "=r"(stack_ptr));
// pw::thread::ProcessThreadStackCallback cb =
-// [](pw::thread::Thread::StreamEncoder& encoder,
+// [](pw::thread::proto::Thread::StreamEncoder& encoder,
// pw::ConstByteSpan stack) -> pw::Status {
// return encoder.WriteRawStack(stack);
// };
@@ -42,7 +42,7 @@ namespace pw::thread::threadx {
// disabled!
// Warning: SMP ports are not yet supported.
Status SnapshotThreads(void* running_thread_stack_pointer,
- SnapshotThreadInfo::StreamEncoder& encoder,
+ proto::SnapshotThreadInfo::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback);
// Captures only the provided thread handle as a pw::thread::Thread proto
@@ -68,7 +68,7 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
// Warning: SMP ports are not yet supported.
Status SnapshotThread(const TX_THREAD& thread,
void* running_thread_stack_pointer,
- Thread::StreamEncoder& encoder,
+ proto::Thread::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback);
} // namespace pw::thread::threadx
diff --git a/pw_thread_threadx/public_overrides/pw_thread_backend/sleep_inline.h b/pw_thread_threadx/sleep_public_overrides/pw_thread_backend/sleep_inline.h
index 7f1cea942..7f1cea942 100644
--- a/pw_thread_threadx/public_overrides/pw_thread_backend/sleep_inline.h
+++ b/pw_thread_threadx/sleep_public_overrides/pw_thread_backend/sleep_inline.h
diff --git a/pw_thread_threadx/snapshot.cc b/pw_thread_threadx/snapshot.cc
index f38e16d47..8dbb969f7 100644
--- a/pw_thread_threadx/snapshot.cc
+++ b/pw_thread_threadx/snapshot.cc
@@ -41,27 +41,27 @@ inline bool ThreadIsRunning(const TX_THREAD& thread) {
}
void CaptureThreadState(const TX_THREAD& thread,
- Thread::StreamEncoder& encoder) {
+ proto::Thread::StreamEncoder& encoder) {
if (ThreadIsRunning(thread)) {
PW_LOG_DEBUG("Thread state: RUNNING");
- encoder.WriteState(ThreadState::Enum::RUNNING);
+ encoder.WriteState(proto::ThreadState::Enum::RUNNING);
return;
}
switch (thread.tx_thread_state) {
case TX_READY:
PW_LOG_DEBUG("Thread state: READY");
- encoder.WriteState(ThreadState::Enum::READY);
+ encoder.WriteState(proto::ThreadState::Enum::READY);
break;
case TX_COMPLETED:
case TX_TERMINATED:
PW_LOG_DEBUG("Thread state: INACTIVE");
- encoder.WriteState(ThreadState::Enum::INACTIVE);
+ encoder.WriteState(proto::ThreadState::Enum::INACTIVE);
break;
case TX_SUSPENDED:
case TX_SLEEP:
PW_LOG_DEBUG("Thread state: SUSPENDED");
- encoder.WriteState(ThreadState::Enum::SUSPENDED);
+ encoder.WriteState(proto::ThreadState::Enum::SUSPENDED);
break;
case TX_QUEUE_SUSP:
case TX_SEMAPHORE_SUSP:
@@ -73,22 +73,22 @@ void CaptureThreadState(const TX_THREAD& thread,
case TX_TCP_IP:
case TX_MUTEX_SUSP:
PW_LOG_DEBUG("Thread state: BLOCKED");
- encoder.WriteState(ThreadState::Enum::BLOCKED);
+ encoder.WriteState(proto::ThreadState::Enum::BLOCKED);
break;
default:
PW_LOG_DEBUG("Thread state: UNKNOWN");
- encoder.WriteState(ThreadState::Enum::UNKNOWN);
+ encoder.WriteState(proto::ThreadState::Enum::UNKNOWN);
}
}
} // namespace
Status SnapshotThreads(void* running_thread_stack_pointer,
- SnapshotThreadInfo::StreamEncoder& encoder,
+ proto::SnapshotThreadInfo::StreamEncoder& encoder,
ProcessThreadStackCallback& stack_dumper) {
struct {
void* running_thread_stack_pointer;
- SnapshotThreadInfo::StreamEncoder* encoder;
+ proto::SnapshotThreadInfo::StreamEncoder* encoder;
ProcessThreadStackCallback* stack_dumper;
Status thread_capture_status;
} ctx;
@@ -97,7 +97,8 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
ctx.stack_dumper = &stack_dumper;
ThreadCallback thread_capture_cb([&ctx](const TX_THREAD& thread) -> bool {
- Thread::StreamEncoder thread_encoder = ctx.encoder->GetThreadsEncoder();
+ proto::Thread::StreamEncoder thread_encoder =
+ ctx.encoder->GetThreadsEncoder();
ctx.thread_capture_status.Update(
SnapshotThread(thread,
ctx.running_thread_stack_pointer,
@@ -117,11 +118,10 @@ Status SnapshotThreads(void* running_thread_stack_pointer,
Status SnapshotThread(const TX_THREAD& thread,
void* running_thread_stack_pointer,
- Thread::StreamEncoder& encoder,
+ proto::Thread::StreamEncoder& encoder,
ProcessThreadStackCallback& thread_stack_callback) {
PW_LOG_DEBUG("Capturing thread info for %s", thread.tx_thread_name);
- encoder.WriteName(
- std::as_bytes(std::span(std::string_view(thread.tx_thread_name))));
+ encoder.WriteName(as_bytes(span(std::string_view(thread.tx_thread_name))));
CaptureThreadState(thread, encoder);
diff --git a/pw_thread_threadx/public_overrides/pw_thread_backend/thread_inline.h b/pw_thread_threadx/thread_public_overrides/pw_thread_backend/thread_inline.h
index de91c73d7..de91c73d7 100644
--- a/pw_thread_threadx/public_overrides/pw_thread_backend/thread_inline.h
+++ b/pw_thread_threadx/thread_public_overrides/pw_thread_backend/thread_inline.h
diff --git a/pw_thread_threadx/public_overrides/pw_thread_backend/thread_native.h b/pw_thread_threadx/thread_public_overrides/pw_thread_backend/thread_native.h
index dfa8d2174..dfa8d2174 100644
--- a/pw_thread_threadx/public_overrides/pw_thread_backend/thread_native.h
+++ b/pw_thread_threadx/thread_public_overrides/pw_thread_backend/thread_native.h
diff --git a/pw_thread_threadx/public_overrides/pw_thread_backend/yield_inline.h b/pw_thread_threadx/yield_public_overrides/pw_thread_backend/yield_inline.h
index c97d25968..c97d25968 100644
--- a/pw_thread_threadx/public_overrides/pw_thread_backend/yield_inline.h
+++ b/pw_thread_threadx/yield_public_overrides/pw_thread_backend/yield_inline.h
diff --git a/pw_thread_zephyr/BUILD.gn b/pw_thread_zephyr/BUILD.gn
new file mode 100644
index 000000000..8cbd4532f
--- /dev/null
+++ b/pw_thread_zephyr/BUILD.gn
@@ -0,0 +1,25 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+pw_test_group("tests") {
+}
diff --git a/pw_thread_zephyr/CMakeLists.txt b/pw_thread_zephyr/CMakeLists.txt
new file mode 100644
index 000000000..41e357392
--- /dev/null
+++ b/pw_thread_zephyr/CMakeLists.txt
@@ -0,0 +1,30 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+pw_add_library(pw_thread_zephyr.sleep STATIC
+ HEADERS
+ public/pw_thread_zephyr/sleep_inline.h
+ sleep_public_overrides/pw_thread_backend/sleep_inline.h
+ PUBLIC_INCLUDES
+ public
+ sleep_public_overrides
+ PUBLIC_DEPS
+ pw_chrono.system_clock
+ pw_thread.sleep.facade
+ SOURCES
+ sleep.cc
+ PRIVATE_DEPS
+ pw_chrono_zephyr.system_clock
+ pw_assert.check
+)
diff --git a/pw_thread_zephyr/Kconfig b/pw_thread_zephyr/Kconfig
new file mode 100644
index 000000000..65b9c3ab8
--- /dev/null
+++ b/pw_thread_zephyr/Kconfig
@@ -0,0 +1,18 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+config PIGWEED_THREAD_SLEEP
+ bool "Enabled the Zephyr pw_thread.sleep backend"
+ select PIGWEED_CHRONO_SYSTEM_CLOCK
+ select PIGWEED_ASSERT
diff --git a/pw_thread_zephyr/docs.rst b/pw_thread_zephyr/docs.rst
new file mode 100644
index 000000000..9608eb149
--- /dev/null
+++ b/pw_thread_zephyr/docs.rst
@@ -0,0 +1,8 @@
+.. _module-pw_thread_zephyr:
+
+----------------
+pw_thread_zephyr
+----------------
+This is a set of backends for pw_thread based on the Zephyr RTOS. Currently,
+only the pw_thread.sleep facade is implemented which is enabled via
+``CONFIG_PIGWEED_THREAD_SLEEP=y``.
diff --git a/pw_thread_zephyr/public/pw_thread_zephyr/sleep_inline.h b/pw_thread_zephyr/public/pw_thread_zephyr/sleep_inline.h
new file mode 100644
index 000000000..f7f9abd80
--- /dev/null
+++ b/pw_thread_zephyr/public/pw_thread_zephyr/sleep_inline.h
@@ -0,0 +1,28 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <zephyr/kernel.h>
+#include <zephyr/sys/util.h>
+
+#include "pw_chrono/system_clock.h"
+#include "pw_thread/sleep.h"
+
+namespace pw::this_thread {
+
+inline void sleep_for(chrono::SystemClock::duration sleep_duration) {
+ sleep_until(chrono::SystemClock::TimePointAfterAtLeast(sleep_duration));
+}
+
+} // namespace pw::this_thread
diff --git a/pw_thread_zephyr/sleep.cc b/pw_thread_zephyr/sleep.cc
new file mode 100644
index 000000000..7a3c78d19
--- /dev/null
+++ b/pw_thread_zephyr/sleep.cc
@@ -0,0 +1,51 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_thread/sleep.h"
+
+#include <algorithm>
+#include <limits>
+
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_chrono_zephyr/system_clock_constants.h"
+
+using pw::chrono::SystemClock;
+
+namespace pw::this_thread {
+
+void sleep_until(SystemClock::time_point wakeup_time) {
+ SystemClock::time_point now = chrono::SystemClock::now();
+
+ // Check if the expiration deadline has already passed, yield.
+ if (wakeup_time <= now) {
+ k_yield();
+ return;
+ }
+
+ // The maximum amount of time we should sleep for in a single command.
+ constexpr chrono::SystemClock::duration kMaxTimeoutMinusOne =
+ pw::chrono::zephyr::kMaxTimeout - SystemClock::duration(1);
+
+ while (now < wakeup_time) {
+ // Sleep either the full remaining duration or the maximum timout
+ k_sleep(Z_TIMEOUT_TICKS(
+ std::min((wakeup_time - now).count(), kMaxTimeoutMinusOne.count())));
+
+ // Check how much time has passed, the scheduler can wake us up early.
+ now = SystemClock::now();
+ }
+}
+
+} // namespace pw::this_thread
diff --git a/pw_thread_zephyr/sleep_public_overrides/pw_thread_backend/sleep_inline.h b/pw_thread_zephyr/sleep_public_overrides/pw_thread_backend/sleep_inline.h
new file mode 100644
index 000000000..3dcf01136
--- /dev/null
+++ b/pw_thread_zephyr/sleep_public_overrides/pw_thread_backend/sleep_inline.h
@@ -0,0 +1,16 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_thread_zephyr/sleep_inline.h"
diff --git a/pw_tls_client/BUILD.bazel b/pw_tls_client/BUILD.bazel
index bae944d12..d9d388db9 100644
--- a/pw_tls_client/BUILD.bazel
+++ b/pw_tls_client/BUILD.bazel
@@ -65,6 +65,7 @@ pw_cc_library(
srcs = ["fake_entropy.cc"],
deps = [
":entropy_facade",
+ "//pw_log",
],
)
@@ -77,6 +78,8 @@ pw_cc_library(
srcs = [
"build_time.cc",
],
+ # TODO(b/257527057): Get this to build.
+ tags = ["manual"],
)
pw_cc_library(
@@ -93,10 +96,21 @@ pw_cc_library(
srcs = ["test_server.cc"],
hdrs = ["public/pw_tls_client/test/test_server.h"],
includes = ["public"],
+ # TODO(b/257527057): Get this to build.
+ tags = ["manual"],
+ deps = [
+ "//pw_bytes",
+ "//pw_log",
+ "//pw_preprocessor",
+ "//pw_result",
+ "//pw_stream",
+ ],
)
pw_cc_test(
name = "test_server_test",
srcs = ["test_server_test.cc"],
+ # TODO(b/257527057): Get this to build.
+ tags = ["manual"],
deps = [":test_server"],
)
diff --git a/pw_tls_client/BUILD.gn b/pw_tls_client/BUILD.gn
index 8a89cf3a9..c944fda57 100644
--- a/pw_tls_client/BUILD.gn
+++ b/pw_tls_client/BUILD.gn
@@ -85,39 +85,33 @@ pw_facade("time") {
# The build time is obtained with a python script and put in a generated header
# file. The header file is included in build_time.cc
-pw_python_action("generate_buid_time_header") {
+pw_python_action("generate_build_time_header") {
header_output = "$target_gen_dir/$target_name/build_time.h"
script = "generate_build_time_header.py"
- outputs = [
- header_output,
-
- # A output file that is never generated so that this action is always
- # re-run. This is to make sure that the build time in the header is always
- # up-to-date.
- "$target_gen_dir/non_exists",
- ]
+ outputs = [ header_output ]
args = [ rebase_path(header_output) ]
}
# The target provides a backend to :time that returns build time.
pw_source_set("build_time") {
- time_injection_outputs = get_target_outputs(":generate_buid_time_header")
+ time_injection_outputs = get_target_outputs(":generate_build_time_header")
include_dirs = [ get_path_info(time_injection_outputs[0], "dir") ]
sources = [ "build_time.cc" ]
deps = [
- ":generate_buid_time_header",
+ ":generate_build_time_header",
":time.facade",
]
}
-# TODO(pwbug/396): Add a python target to generate source file from the
+# TODO(b/235290724): Add a python target to generate source file from the
# specified CRLSet file in `pw_tls_client_CRLSET_FILE`
pw_source_set("crlset") {
public_configs = [ ":public_includes" ]
public = [ "public/pw_tls_client/crlset.h" ]
+ public_deps = [ dir_pw_bytes ]
- # TODO(pwbug/396): Add sources generated from a CRLSet file to build.
+ # TODO(b/235290724): Add sources generated from a CRLSet file to build.
}
pw_source_set("test_server") {
diff --git a/pw_tls_client/docs.rst b/pw_tls_client/docs.rst
index 380018c34..78c9cf0f2 100644
--- a/pw_tls_client/docs.rst
+++ b/pw_tls_client/docs.rst
@@ -63,8 +63,8 @@ to the TLS library in use. However, common TLS libraires, such as BoringSSL
and MbedTLS, support the use of C APIs ``time()`` and ``getimtofday()`` for
obtaining date time. To accomodate the use of these libraries, a facade target
``pw_tls_client:time`` is added that wraps these APIs. For GN builds,
-specify the backend target with variable ``pw_tls_client_C_TIME_BACKEND``.
-``pw_tls_client_C_TIME_BACKEND`` defaults to the ``pw_tls_client::build_time``
+specify the backend target with variable ``pw_tls_client_TIME_BACKEND``.
+``pw_tls_client_TIME_BACKEND`` defaults to the ``pw_tls_client::build_time``
backend that returns build time.
If downstream project chooses to use other TLS libraires that handle time source
@@ -135,7 +135,7 @@ connection to www.google.com:
// pw::stream::SocketStream doesn't accept host domain name as input. Thus we
// introduce this helper function for getting the IP address
- pw::Status GetIPAddrFromHostName(std::string_view host, std::span<char> ip) {
+ pw::Status GetIPAddrFromHostName(std::string_view host, pw::span<char> ip) {
char null_terminated_host_name[256] = {0};
auto host_copy_status = pw::string::Copy(host, null_terminated_host_name);
if (!host_copy_status.ok()) {
@@ -194,7 +194,7 @@ connection to www.google.com:
return 1;
}
- auto write_status = tls_conn.value()->Write(std::as_bytes(std::span{kHTTPRequest}));
+ auto write_status = tls_conn.value()->Write(pw::as_bytes(pw::span{kHTTPRequest}));
if (!write_status.ok()) {
// Inspect/handle error with write_status.code() and
// tls_conn.value()->GetLastTLSStatus().
diff --git a/pw_tls_client/generate_build_time_header.py b/pw_tls_client/generate_build_time_header.py
index 824ac6e32..aa747b4b4 100644
--- a/pw_tls_client/generate_build_time_header.py
+++ b/pw_tls_client/generate_build_time_header.py
@@ -54,14 +54,19 @@ def main() -> int:
# Add a comment in the generated header to show readable build time
string_date = datetime.fromtimestamp(time_stamp).strftime(
- "%m/%d/%Y %H:%M:%S")
+ "%m/%d/%Y %H:%M:%S"
+ )
header.write(f'// {string_date}\n')
# Write to the header.
- header.write(''.join([
- 'constexpr uint64_t kBuildTimeMicrosecondsUTC = ',
- f'{int(time_stamp * 1e6)};\n'
- ]))
+ header.write(
+ ''.join(
+ [
+ 'constexpr uint64_t kBuildTimeMicrosecondsUTC = ',
+ f'{int(time_stamp * 1e6)};\n',
+ ]
+ )
+ )
return 0
diff --git a/pw_tls_client/public/pw_tls_client/entropy.h b/pw_tls_client/public/pw_tls_client/entropy.h
index 3001b7ef5..01b42e3a9 100644
--- a/pw_tls_client/public/pw_tls_client/entropy.h
+++ b/pw_tls_client/public/pw_tls_client/entropy.h
@@ -24,7 +24,7 @@ namespace pw::tls_client {
Status GetRandomBytes(ByteSpan dest);
// An overloaded variant for accomodating C API interfaces, i.e. mbed TLS.
-inline Status GetRandomBytes(std::span<unsigned char> dest) {
- return GetRandomBytes(std::as_writable_bytes(dest));
+inline Status GetRandomBytes(span<unsigned char> dest) {
+ return GetRandomBytes(as_writable_bytes(dest));
}
} // namespace pw::tls_client
diff --git a/pw_tls_client/public/pw_tls_client/session.h b/pw_tls_client/public/pw_tls_client/session.h
index 384045392..d8b851fe5 100644
--- a/pw_tls_client/public/pw_tls_client/session.h
+++ b/pw_tls_client/public/pw_tls_client/session.h
@@ -41,7 +41,7 @@ class Session : public stream::NonSeekableReaderWriter {
// Close() will be called if the Session is open. Since Close() returns a
// pw::Status which cannot be forwarded from a destructor for callers to
// check. Backend shall assert check that it is OkStatus().
- virtual ~Session();
+ ~Session() override;
// Starts a TLS connection. The backend performs TLS handshaking and
// certificate verification/revocation/expiration check.
diff --git a/pw_tls_client/public/pw_tls_client/test/test_server.h b/pw_tls_client/public/pw_tls_client/test/test_server.h
index 372463198..df0fdbc8a 100644
--- a/pw_tls_client/public/pw_tls_client/test/test_server.h
+++ b/pw_tls_client/public/pw_tls_client/test/test_server.h
@@ -14,9 +14,16 @@
#pragma once
+#include "pw_preprocessor/compiler.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wcast-qual");
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wignored-qualifiers");
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wpedantic");
#include <openssl/bio.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
+PW_MODIFY_DIAGNOSTICS_POP();
#include "pw_bytes/span.h"
#include "pw_result/result.h"
@@ -65,7 +72,7 @@ class InMemoryTestServer : public stream::NonSeekableReaderWriter {
// CA chains (all DER format)
Status Initialize(ConstByteSpan key,
ConstByteSpan cert,
- std::span<const ConstByteSpan> chains);
+ span<const ConstByteSpan> chains);
// Is handshake completed.
bool SessionEstablished() { return is_handshake_done_; }
@@ -99,7 +106,7 @@ class InMemoryTestServer : public stream::NonSeekableReaderWriter {
// Methods for loading private key, certificate, and intermediate CA chain.
Status LoadPrivateKey(ConstByteSpan key);
Status LoadCertificate(ConstByteSpan cert);
- Status LoadCAChain(std::span<const ConstByteSpan> chains);
+ Status LoadCAChain(span<const ConstByteSpan> chains);
// Methods for providing BIO interfaces.
static int BioRead(BIO* bio, char* out, int output_length);
diff --git a/pw_tls_client/py/BUILD.gn b/pw_tls_client/py/BUILD.gn
index 6b59a08ee..fc7bbb8b1 100644
--- a/pw_tls_client/py/BUILD.gn
+++ b/pw_tls_client/py/BUILD.gn
@@ -26,4 +26,5 @@ pw_python_package("py") {
"pw_tls_client/generate_test_data.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_tls_client/py/pw_tls_client/generate_test_data.py b/pw_tls_client/py/pw_tls_client/generate_test_data.py
index 291278bd1..00dda960a 100644
--- a/pw_tls_client/py/pw_tls_client/generate_test_data.py
+++ b/pw_tls_client/py/pw_tls_client/generate_test_data.py
@@ -52,17 +52,24 @@ CERTS_AND_KEYS_HEADER = """// Copyright 2021 The Pigweed Authors
class Subject:
"""A subject wraps a name, private key and extensions for issuers
to issue its certificate"""
- def __init__(self, name: str, extensions: List[Tuple[x509.ExtensionType,
- bool]]):
- self._subject_name = x509.Name([
- x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
- x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"California"),
- x509.NameAttribute(NameOID.LOCALITY_NAME, u"Mountain View"),
- x509.NameAttribute(NameOID.ORGANIZATION_NAME, name),
- x509.NameAttribute(NameOID.COMMON_NAME, u"Google-Pigweed"),
- ])
- self._private_key = rsa.generate_private_key(public_exponent=65537,
- key_size=2048)
+
+ def __init__(
+ self, name: str, extensions: List[Tuple[x509.ExtensionType, bool]]
+ ):
+ self._subject_name = x509.Name(
+ [
+ x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
+ x509.NameAttribute(
+ NameOID.STATE_OR_PROVINCE_NAME, u"California"
+ ),
+ x509.NameAttribute(NameOID.LOCALITY_NAME, u"Mountain View"),
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, name),
+ x509.NameAttribute(NameOID.COMMON_NAME, u"Google-Pigweed"),
+ ]
+ )
+ self._private_key = rsa.generate_private_key(
+ public_exponent=65537, key_size=2048
+ )
self._extensions = extensions
def subject_name(self) -> x509.Name:
@@ -84,23 +91,30 @@ class Subject:
class CA(Subject):
"""A CA/Sub-ca that issues certificates"""
+
def __init__(self, *args, **kwargs):
- ext = [(x509.BasicConstraints(True, None), True),
- (x509.KeyUsage(
- digital_signature=False,
- content_commitment=False,
- key_encipherment=False,
- data_encipherment=False,
- key_agreement=False,
- crl_sign=False,
- encipher_only=False,
- decipher_only=False,
- key_cert_sign=True,
- ), True)]
+ ext = [
+ (x509.BasicConstraints(True, None), True),
+ (
+ x509.KeyUsage(
+ digital_signature=False,
+ content_commitment=False,
+ key_encipherment=False,
+ data_encipherment=False,
+ key_agreement=False,
+ crl_sign=False,
+ encipher_only=False,
+ decipher_only=False,
+ key_cert_sign=True,
+ ),
+ True,
+ ),
+ ]
super().__init__(*args, extensions=ext, **kwargs)
- def sign(self, subject: Subject, not_before: datetime,
- not_after: datetime) -> x509.Certificate:
+ def sign(
+ self, subject: Subject, not_before: datetime, not_after: datetime
+ ) -> x509.Certificate:
"""Issues a certificate for another CA/Sub-ca/Server"""
builder = x509.CertificateBuilder()
@@ -115,7 +129,8 @@ class CA(Subject):
# Validity period.
builder = builder.not_valid_before(not_before).not_valid_after(
- not_after)
+ not_after
+ )
# Uses a random serial number
builder = builder.serial_number(x509.random_serial_number())
@@ -127,30 +142,37 @@ class CA(Subject):
# Sign and returns the certificate.
return builder.sign(self._private_key, hashes.SHA256())
- def self_sign(self, not_before: datetime,
- not_after: datetime) -> x509.Certificate:
+ def self_sign(
+ self, not_before: datetime, not_after: datetime
+ ) -> x509.Certificate:
"""Issues a self sign certificate"""
return self.sign(self, not_before, not_after)
class Server(Subject):
"""The end-entity server"""
+
def __init__(self, *args, **kwargs):
ext = [
(x509.BasicConstraints(False, None), True),
- (x509.KeyUsage(
- digital_signature=True,
- content_commitment=False,
- key_encipherment=False,
- data_encipherment=False,
- key_agreement=False,
- crl_sign=False,
- encipher_only=False,
- decipher_only=False,
- key_cert_sign=False,
- ), True),
- (x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
- True),
+ (
+ x509.KeyUsage(
+ digital_signature=True,
+ content_commitment=False,
+ key_encipherment=False,
+ data_encipherment=False,
+ key_agreement=False,
+ crl_sign=False,
+ encipher_only=False,
+ decipher_only=False,
+ key_cert_sign=False,
+ ),
+ True,
+ ),
+ (
+ x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
+ True,
+ ),
]
super().__init__(*args, extensions=ext, **kwargs)
@@ -170,18 +192,21 @@ def c_escaped_string(data: bytes):
def byte_array_declaration(data: bytes, name: str) -> str:
"""Generates a ConstByteSpan declaration for a byte array"""
type_name = '[[maybe_unused]] const pw::ConstByteSpan'
- array_body = f'std::as_bytes(std::span{c_escaped_string(data)})'
+ array_body = f'pw::as_bytes(pw::span{c_escaped_string(data)})'
return f'{type_name} {name} = {array_body};'
class Codegen:
"""Base helper class for code generation"""
- def generate_code(self) -> str:
+
+ def generate_code(self) -> str: # pylint: disable=no-self-use
"""Generates C++ code for this object"""
+ return ''
class PrivateKeyGen(Codegen):
"""Codegen class for a private key"""
+
def __init__(self, key: rsa.RSAPrivateKey, name: str):
self._key = key
self._name = name
@@ -192,11 +217,15 @@ class PrivateKeyGen(Codegen):
self._key.private_bytes(
serialization.Encoding.DER,
serialization.PrivateFormat.TraditionalOpenSSL,
- serialization.NoEncryption()), self._name)
+ serialization.NoEncryption(),
+ ),
+ self._name,
+ )
class CertificateGen(Codegen):
"""Codegen class for a single certificate"""
+
def __init__(self, cert: x509.Certificate, name: str):
self._cert = cert
self._name = name
@@ -204,7 +233,8 @@ class CertificateGen(Codegen):
def generate_code(self) -> str:
"""Code generation"""
return byte_array_declaration(
- self._cert.public_bytes(serialization.Encoding.DER), self._name)
+ self._cert.public_bytes(serialization.Encoding.DER), self._name
+ )
def generate_test_data() -> str:
@@ -220,22 +250,26 @@ def generate_test_data() -> str:
# Generate a root-A CA certificates
root_a = CA("root-A")
subjects.append(
- CertificateGen(root_a.self_sign(not_before, not_after), "kRootACert"))
+ CertificateGen(root_a.self_sign(not_before, not_after), "kRootACert")
+ )
# Generate a sub CA certificate signed by root-A.
sub = CA("sub")
subjects.append(
- CertificateGen(root_a.sign(sub, not_before, not_after), "kSubCACert"))
+ CertificateGen(root_a.sign(sub, not_before, not_after), "kSubCACert")
+ )
# Generate a valid server certificate signed by sub
server = Server("server")
subjects.append(
- CertificateGen(sub.sign(server, not_before, not_after), "kServerCert"))
+ CertificateGen(sub.sign(server, not_before, not_after), "kServerCert")
+ )
subjects.append(PrivateKeyGen(server.private_key(), "kServerKey"))
root_b = CA("root-B")
subjects.append(
- CertificateGen(root_b.self_sign(not_before, not_after), "kRootBCert"))
+ CertificateGen(root_b.self_sign(not_before, not_after), "kRootBCert")
+ )
code = 'namespace {\n\n'
for subject in subjects:
@@ -246,11 +280,14 @@ def generate_test_data() -> str:
def clang_format(file):
- subprocess.run([
- "clang-format",
- "-i",
- file,
- ], check=True)
+ subprocess.run(
+ [
+ "clang-format",
+ "-i",
+ file,
+ ],
+ check=True,
+ )
def parse_args():
@@ -258,7 +295,8 @@ def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"certs_and_keys_header",
- help="output header file for test certificates and keys")
+ help="output header file for test certificates and keys",
+ )
return parser.parse_args()
diff --git a/pw_tls_client/py/setup.cfg b/pw_tls_client/py/setup.cfg
index 6a52caeb4..3fceb695c 100644
--- a/pw_tls_client/py/setup.cfg
+++ b/pw_tls_client/py/setup.cfg
@@ -21,7 +21,8 @@ description = pw_tls_client python package
[options]
packages = find:
zip_safe = False
-install_requires = cryptography
+install_requires =
+ cryptography
[options.package_data]
pw_tls_client = py.typed
diff --git a/pw_tls_client/test_server.cc b/pw_tls_client/test_server.cc
index ecbf48909..b0fd8fbc6 100644
--- a/pw_tls_client/test_server.cc
+++ b/pw_tls_client/test_server.cc
@@ -14,10 +14,6 @@
#include "pw_tls_client/test/test_server.h"
-#include <openssl/bio.h>
-#include <openssl/pem.h>
-#include <openssl/ssl.h>
-
#include <cstring>
#include "pw_log/log.h"
@@ -84,8 +80,8 @@ InMemoryTestServer::InMemoryTestServer(ByteSpan input_buffer,
int InMemoryTestServer::BioRead(BIO* bio, char* out, int output_length) {
auto server = static_cast<InMemoryTestServer*>(bio->ptr);
- auto read = server->input_buffer_.Read(std::as_writable_bytes(
- std::span{out, static_cast<size_t>(output_length)}));
+ auto read = server->input_buffer_.Read(
+ as_writable_bytes(span{out, static_cast<size_t>(output_length)}));
if (!read.ok()) {
server->last_bio_status_ = read.status();
return -1;
@@ -102,7 +98,7 @@ int InMemoryTestServer::BioWrite(BIO* bio,
int input_length) {
auto server = static_cast<InMemoryTestServer*>(bio->ptr);
if (auto status = server->output_buffer_.Write(
- std::as_bytes(std::span{input, static_cast<size_t>(input_length)}));
+ as_bytes(span{input, static_cast<size_t>(input_length)}));
!status.ok()) {
server->last_bio_status_ = status;
return -1;
@@ -113,7 +109,7 @@ int InMemoryTestServer::BioWrite(BIO* bio,
Status InMemoryTestServer::Initialize(ConstByteSpan key,
ConstByteSpan cert,
- std::span<const ConstByteSpan> chains) {
+ span<const ConstByteSpan> chains) {
input_buffer_.clear();
output_buffer_.clear();
is_handshake_done_ = false;
@@ -201,7 +197,7 @@ Status InMemoryTestServer::LoadCertificate(ConstByteSpan cert) {
return OkStatus();
}
-Status InMemoryTestServer::LoadCAChain(std::span<const ConstByteSpan> chains) {
+Status InMemoryTestServer::LoadCAChain(span<const ConstByteSpan> chains) {
for (auto cert : chains) {
auto res = ParseDerCertificate(cert);
if (!res.ok()) {
diff --git a/pw_tls_client/test_server_test.cc b/pw_tls_client/test_server_test.cc
index 19b1efedf..3e76aef85 100644
--- a/pw_tls_client/test_server_test.cc
+++ b/pw_tls_client/test_server_test.cc
@@ -14,10 +14,10 @@
#include "pw_tls_client/test/test_server.h"
-#include <span>
#include <string>
#include "gtest/gtest.h"
+#include "pw_span/span.h"
// The following header contains a set of test certificates and keys.
// It is generated by
diff --git a/pw_tls_client_boringssl/BUILD.bazel b/pw_tls_client_boringssl/BUILD.bazel
index 0e94f7d8f..38e25d338 100644
--- a/pw_tls_client_boringssl/BUILD.bazel
+++ b/pw_tls_client_boringssl/BUILD.bazel
@@ -43,6 +43,8 @@ pw_cc_test(
srcs = [
"tls_client_boringssl_test.cc",
],
+ #TODO(b/257529537): Get this to build.
+ tags = ["manual"],
deps = [
":pw_tls_client_boringssl",
],
diff --git a/pw_tls_client_boringssl/tls_client_boringssl.cc b/pw_tls_client_boringssl/tls_client_boringssl.cc
index 8d8ce801b..aeb4c75ae 100644
--- a/pw_tls_client_boringssl/tls_client_boringssl.cc
+++ b/pw_tls_client_boringssl/tls_client_boringssl.cc
@@ -19,7 +19,7 @@ namespace pw::tls_client {
namespace backend {
SessionImplementation::SessionImplementation(SessionOptions) {
- // TODO(pwbug/421): To implement
+ // TODO(b/235291139): To implement
}
SessionImplementation::~SessionImplementation() = default;
@@ -27,33 +27,33 @@ SessionImplementation::~SessionImplementation() = default;
} // namespace backend
Session::Session(const SessionOptions& options) : session_impl_(options) {
- // TODO(pwbug/421): To implement
+ // TODO(b/235291139): To implement
}
Session::~Session() = default;
Result<Session*> Session::Create(const SessionOptions&) {
- // TODO(pwbug/421): To implement
+ // TODO(b/235291139): To implement
return PW_STATUS_UNIMPLEMENTED;
}
Status Session::Open() {
- // TODO(pwbug/421): To implement
+ // TODO(b/235291139): To implement
return PW_STATUS_UNIMPLEMENTED;
}
Status Session::Close() {
- // TODO(pwbug/421): To implement
+ // TODO(b/235291139): To implement
return PW_STATUS_UNIMPLEMENTED;
}
StatusWithSize Session::DoRead(ByteSpan) {
- // TODO(pwbug/421): To implement
+ // TODO(b/235291139): To implement
return StatusWithSize(PW_STATUS_UNIMPLEMENTED, 0);
}
Status Session::DoWrite(ConstByteSpan) {
- // TODO(pwbug/421): To implement
+ // TODO(b/235291139): To implement
return PW_STATUS_UNIMPLEMENTED;
}
diff --git a/pw_tls_client_mbedtls/BUILD.bazel b/pw_tls_client_mbedtls/BUILD.bazel
index e3761157b..5833994d7 100644
--- a/pw_tls_client_mbedtls/BUILD.bazel
+++ b/pw_tls_client_mbedtls/BUILD.bazel
@@ -22,8 +22,6 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
-# TODO(pwbug/398): The recipe is under construction.
-
pw_cc_library(
name = "pw_tls_client_mbedtls",
srcs = ["tls_client_mbedtls.cc"],
@@ -35,7 +33,11 @@ pw_cc_library(
"public",
"public_overrides",
],
+ # TODO(b/258068735): Get this target to build. Requires adding mbedtls
+ # build targets.
+ tags = ["manual"],
deps = [
+ "//pw_log",
"//pw_tls_client:pw_tls_client_facade",
],
)
@@ -45,6 +47,9 @@ pw_cc_test(
srcs = [
"tls_client_mbedtls_test.cc",
],
+ # TODO(b/258068735): Get this target to build. Requires adding mbedtls
+ # build targets.
+ tags = ["manual"],
deps = [
":pw_tls_client_mbedtls",
],
diff --git a/pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h b/pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h
index e3a2fd44b..e7cb83ef7 100644
--- a/pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h
+++ b/pw_tls_client_mbedtls/public/pw_tls_client_mbedtls/backend_types.h
@@ -14,11 +14,17 @@
#pragma once
+#include "pw_preprocessor/compiler.h"
+
+PW_MODIFY_DIAGNOSTICS_PUSH();
+PW_MODIFY_DIAGNOSTIC(ignored, "-Wswitch-enum");
#include "mbedtls/certs.h"
#include "mbedtls/ctr_drbg.h"
#include "mbedtls/entropy.h"
#include "mbedtls/error.h"
#include "mbedtls/ssl.h"
+PW_MODIFY_DIAGNOSTICS_POP();
+
#include "pw_status/status.h"
#include "pw_tls_client/options.h"
diff --git a/pw_tls_client_mbedtls/tls_client_mbedtls.cc b/pw_tls_client_mbedtls/tls_client_mbedtls.cc
index dc01c2266..c28990138 100644
--- a/pw_tls_client_mbedtls/tls_client_mbedtls.cc
+++ b/pw_tls_client_mbedtls/tls_client_mbedtls.cc
@@ -12,7 +12,6 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "mbedtls/ssl.h"
#include "pw_assert/check.h"
#include "pw_log/log.h"
#include "pw_tls_client/entropy.h"
@@ -145,7 +144,7 @@ Status SessionImplementation::Setup() {
// The API does not fail.
mbedtls_ssl_conf_authmode(&ssl_config_, MBEDTLS_SSL_VERIFY_REQUIRED);
- // TODO(pwbug/398): Add logic for loading trust anchors.
+ // TODO(b/235289501): Add logic for loading trust anchors.
// Load configuration to SSL.
ret = mbedtls_ssl_setup(&ssl_ctx_, &ssl_config_);
@@ -187,7 +186,7 @@ Result<Session*> Session::Create(const SessionOptions& options) {
auto setup_status = sess->session_impl_.Setup();
if (!setup_status.ok()) {
PW_LOG_DEBUG("Failed to setup");
- // TODO(pwbug/398): `tls_status_` may be set, but the session object will
+ // TODO(b/235289501): `tls_status_` may be set, but the session object will
// be released. Map `tls_stauts_` to string and print out here so that
// the information can be catched.
delete sess;
@@ -198,22 +197,22 @@ Result<Session*> Session::Create(const SessionOptions& options) {
}
Status Session::Open() {
- // TODO(pwbug/398): To implement
+ // TODO(b/235289501): To implement
return Status::Unimplemented();
}
Status Session::Close() {
- // TODO(pwbug/398): To implement
+ // TODO(b/235289501): To implement
return Status::Unimplemented();
}
StatusWithSize Session::DoRead(ByteSpan) {
- // TODO(pwbug/398): To implement
+ // TODO(b/235289501): To implement
return StatusWithSize(Status::Unimplemented(), 0);
}
Status Session::DoWrite(ConstByteSpan) {
- // TODO(pwbug/398): To implement
+ // TODO(b/235289501): To implement
return Status::Unimplemented();
}
diff --git a/pw_tokenizer/Android.bp b/pw_tokenizer/Android.bp
new file mode 100644
index 000000000..8d41e7d25
--- /dev/null
+++ b/pw_tokenizer/Android.bp
@@ -0,0 +1,37 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_detokenizer",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ srcs: [
+ "detokenize.cc",
+ "decode.cc",
+ ],
+ header_libs: [
+ "pw_polyfill_headers",
+ "pw_preprocessor_headers",
+ "pw_span_headers",
+ ],
+ static_libs: [
+ "pw_bytes",
+ "pw_varint"
+ ],
+}
diff --git a/pw_tokenizer/BUILD.bazel b/pw_tokenizer/BUILD.bazel
index cafab2c09..26ce5dcbb 100644
--- a/pw_tokenizer/BUILD.bazel
+++ b/pw_tokenizer/BUILD.bazel
@@ -1,4 +1,4 @@
-# Copyright 2020 The Pigweed Authors
+# Copyright 2023 The Pigweed Authors
#
# 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
@@ -12,7 +12,6 @@
# License for the specific language governing permissions and limitations under
# the License.
-load("@rules_cc//cc:defs.bzl", "cc_binary")
load(
"//pw_build:pigweed.bzl",
"pw_cc_binary",
@@ -20,6 +19,7 @@ load(
"pw_cc_test",
)
load("//pw_fuzzer:fuzzer.bzl", "pw_cc_fuzz_test")
+load("//pw_build/bazel_internal:py_proto_library.bzl", "py_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -48,6 +48,7 @@ pw_cc_library(
],
includes = ["public"],
deps = [
+ "//pw_bytes:bit",
"//pw_containers:to_array",
"//pw_polyfill",
"//pw_preprocessor",
@@ -61,24 +62,12 @@ pw_cc_library(
visibility = ["@pigweed_config//:__pkg__"],
)
-pw_cc_library(
- name = "global_handler",
- srcs = ["tokenize_to_global_handler.cc"],
- hdrs = ["public/pw_tokenizer/tokenize_to_global_handler.h"],
- deps = [
- ":pw_tokenizer",
- "@pigweed_config//:pw_tokenizer_global_handler_backend",
- ],
-)
-
+# This header is only provided for backwards compatibility and is not used in
+# the Bazel build.
pw_cc_library(
name = "global_handler_with_payload",
- srcs = ["tokenize_to_global_handler_with_payload.cc"],
hdrs = ["public/pw_tokenizer/tokenize_to_global_handler_with_payload.h"],
- deps = [
- ":pw_tokenizer",
- "@pigweed_config//:pw_tokenizer_global_handler_with_payload_backend",
- ],
+ visibility = ["//pw_log_tokenized:__pkg__"],
)
pw_cc_library(
@@ -95,6 +84,7 @@ pw_cc_library(
"//pw_base64",
"//pw_preprocessor",
"//pw_span",
+ "//pw_string:string",
],
)
@@ -112,6 +102,7 @@ pw_cc_library(
],
includes = ["public"],
deps = [
+ "//pw_bytes",
"//pw_span",
"//pw_varint",
],
@@ -123,12 +114,19 @@ proto_library(
"options.proto",
],
import_prefix = "pw_tokenizer/proto",
- strip_import_prefix = "//pw_tokenizer",
+ strip_import_prefix = "/pw_tokenizer",
deps = [
"@com_google_protobuf//:descriptor_proto",
],
)
+# TODO(b/241456982): Not expected to build yet.
+py_proto_library(
+ name = "tokenizer_proto_py_pb2",
+ tags = ["manual"],
+ deps = [":tokenizer_proto"],
+)
+
# Executable for generating test data for the C++ and Python detokenizers. This
# target should only be built for the host.
pw_cc_binary(
@@ -153,20 +151,6 @@ pw_cc_binary(
],
)
-# Executable for generating a test ELF file for elf_reader_test.py. A host
-# version of this binary is checked in for use in elf_reader_test.py.
-cc_binary(
- name = "elf_reader_test_binary",
- srcs = [
- "py/elf_reader_test_binary.c",
- ],
- linkopts = ["-Wl,--unresolved-symbols=ignore-all"], # main is not defined
- deps = [
- ":pw_tokenizer",
- "//pw_varint",
- ],
-)
-
pw_cc_test(
name = "argument_types_test",
srcs = [
@@ -229,15 +213,11 @@ pw_cc_fuzz_test(
)
pw_cc_test(
- name = "global_handlers_test",
- srcs = [
- "global_handlers_test.cc",
- "global_handlers_test_c.c",
- "pw_tokenizer_private/tokenize_test.h",
- ],
+ name = "encode_args_test",
+ srcs = ["encode_args_test.cc"],
deps = [
- ":global_handler",
- ":global_handler_with_payload",
+ ":pw_tokenizer",
+ "//pw_unit_test",
],
)
@@ -260,8 +240,6 @@ pw_cc_test(
"simple_tokenize_test.cc",
],
deps = [
- ":global_handler",
- ":global_handler_with_payload",
":pw_tokenizer",
"//pw_unit_test",
],
diff --git a/pw_tokenizer/BUILD.gn b/pw_tokenizer/BUILD.gn
index 269419edf..fc276f456 100644
--- a/pw_tokenizer/BUILD.gn
+++ b/pw_tokenizer/BUILD.gn
@@ -15,7 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_arduino_build/arduino.gni")
-import("$dir_pw_build/facade.gni")
+import("$dir_pw_bloat/bloat.gni")
import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
@@ -37,7 +37,11 @@ config("public_include_path") {
}
config("linker_script") {
- inputs = [ "pw_tokenizer_linker_sections.ld" ]
+ inputs = [
+ "pw_tokenizer_linker_sections.ld",
+ "pw_tokenizer_linker_rules.ld",
+ ]
+ lib_dirs = [ "." ]
# Automatically add the tokenizer linker sections when cross-compiling or
# building for Linux. macOS and Windows executables are not supported.
@@ -56,7 +60,6 @@ config("linker_script") {
rebase_path("add_tokenizer_sections_to_default_script.ld",
root_build_dir),
]
- lib_dirs = [ "." ]
inputs += [ "add_tokenizer_sections_to_default_script.ld" ]
}
@@ -75,7 +78,9 @@ pw_source_set("pw_tokenizer") {
public_deps = [
":config",
"$dir_pw_containers:to_array",
+ dir_pw_polyfill,
dir_pw_preprocessor,
+ dir_pw_span,
]
deps = [ dir_pw_varint ]
public = [
@@ -99,57 +104,34 @@ pw_source_set("pw_tokenizer") {
friend = [ ":*" ]
}
-# As a temporary workaround, if no backend is set, use an empty test backend so
-# that the test can define the handler function.
-# TODO(hepler): Switch this to a facade test when available.
-if (pw_tokenizer_GLOBAL_HANDLER_BACKEND == "" &&
- pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND == "") {
- # This is an empty library to use as the backend for global_handler and
- # global_handler_with_payload tests.
- pw_source_set("test_backend") {
- visibility = [ ":*" ]
- }
-
- pw_tokenizer_GLOBAL_HANDLER_BACKEND = ":test_backend"
- pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND = ":test_backend"
-
- enable_global_handler_test = true
-} else {
- enable_global_handler_test = false
-}
-
-pw_facade("global_handler") {
- backend = pw_tokenizer_GLOBAL_HANDLER_BACKEND
-
- public_configs = [ ":public_include_path" ]
- public = [ "public/pw_tokenizer/tokenize_to_global_handler.h" ]
- sources = [ "tokenize_to_global_handler.cc" ]
- public_deps = [ ":pw_tokenizer" ]
-}
-
-pw_facade("global_handler_with_payload") {
- backend = pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND
-
- public_configs = [ ":public_include_path" ]
- public = [ "public/pw_tokenizer/tokenize_to_global_handler_with_payload.h" ]
- sources = [ "tokenize_to_global_handler_with_payload.cc" ]
- public_deps = [ ":pw_tokenizer" ]
-}
-
pw_source_set("base64") {
public_configs = [ ":public_include_path" ]
public = [ "public/pw_tokenizer/base64.h" ]
sources = [ "base64.cc" ]
public_deps = [
":pw_tokenizer",
+ "$dir_pw_string:string",
dir_pw_base64,
dir_pw_preprocessor,
]
}
+# TODO(hepler): Remove this backwards compatibility header after projects have
+# migrated.
+pw_source_set("global_handler_with_payload") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_tokenizer/tokenize_to_global_handler_with_payload.h" ]
+ public_deps = [ dir_pw_preprocessor ]
+}
+
pw_source_set("decoder") {
public_configs = [ ":public_include_path" ]
- deps = [ dir_pw_varint ]
+ public_deps = [ dir_pw_span ]
+ deps = [
+ "$dir_pw_bytes:bit",
+ dir_pw_bytes,
+ dir_pw_varint,
+ ]
public = [
"public/pw_tokenizer/detokenize.h",
"public/pw_tokenizer/token_database.h",
@@ -190,19 +172,25 @@ pw_test_group("tests") {
":argument_types_test",
":base64_test",
":decode_test",
- ":detokenize_fuzzer",
+ ":detokenize_fuzzer_test",
":detokenize_test",
- ":global_handlers_test",
+ ":encode_args_test",
":hash_test",
- ":simple_tokenize_test_cpp14",
- ":simple_tokenize_test_cpp17",
- ":token_database_fuzzer",
+ ":simple_tokenize_test",
+ ":token_database_fuzzer_test",
":token_database_test",
":tokenize_test",
]
group_deps = [ "$dir_pw_preprocessor:tests" ]
}
+group("fuzzers") {
+ deps = [
+ ":detokenize_fuzzer",
+ ":token_database_fuzzer",
+ ]
+}
+
pw_test("argument_types_test") {
sources = [
"argument_types_test.cc",
@@ -218,7 +206,10 @@ pw_test("argument_types_test") {
pw_test("base64_test") {
sources = [ "base64_test.cc" ]
- deps = [ ":base64" ]
+ deps = [
+ ":base64",
+ dir_pw_span,
+ ]
}
pw_test("decode_test") {
@@ -246,19 +237,9 @@ pw_test("detokenize_test") {
enable_if = pw_build_EXECUTABLE_TARGET_TYPE != "arduino_executable"
}
-pw_test("global_handlers_test") {
- sources = [
- "global_handlers_test.cc",
- "global_handlers_test_c.c",
- "pw_tokenizer_private/tokenize_test.h",
- ]
- deps = [
- ":global_handler",
- ":global_handler_with_payload",
- ]
-
- # TODO(hepler): Switch this to a facade test when available.
- enable_if = enable_global_handler_test
+pw_test("encode_args_test") {
+ sources = [ "encode_args_test.cc" ]
+ deps = [ ":pw_tokenizer" ]
}
pw_test("hash_test") {
@@ -269,47 +250,9 @@ pw_test("hash_test") {
deps = [ ":pw_tokenizer" ]
}
-# Fully test C++14 compatibility by compiling all sources as C++14.
-_simple_tokenize_test_sources = [
- "$dir_pw_containers/public/pw_containers/to_array.h",
- "$dir_pw_varint/public/pw_varint/varint.h",
- "$dir_pw_varint/varint.cc",
- "encode_args.cc",
- "public/pw_tokenizer/config.h",
- "public/pw_tokenizer/encode_args.h",
- "public/pw_tokenizer/hash.h",
- "public/pw_tokenizer/internal/argument_types.h",
- "public/pw_tokenizer/internal/argument_types_macro_4_byte.h",
- "public/pw_tokenizer/internal/argument_types_macro_8_byte.h",
- "public/pw_tokenizer/internal/pw_tokenizer_65599_fixed_length_128_hash_macro.h",
- "public/pw_tokenizer/internal/pw_tokenizer_65599_fixed_length_80_hash_macro.h",
- "public/pw_tokenizer/internal/pw_tokenizer_65599_fixed_length_96_hash_macro.h",
- "public/pw_tokenizer/internal/tokenize_string.h",
- "public/pw_tokenizer/tokenize.h",
- "public/pw_tokenizer/tokenize_to_global_handler.h",
- "public/pw_tokenizer/tokenize_to_global_handler_with_payload.h",
- "simple_tokenize_test.cc",
- "tokenize.cc",
- "tokenize_to_global_handler.cc",
- "tokenize_to_global_handler_with_payload.cc",
-]
-_simple_tokenize_test_configs = [
- ":public_include_path",
- "$dir_pw_varint:default_config",
- "$dir_pw_containers:public_include_path",
-]
-
-pw_test("simple_tokenize_test_cpp14") {
- remove_configs = [ "$dir_pw_build:cpp17" ]
- configs = [ "$dir_pw_build:cpp14" ] + _simple_tokenize_test_configs
- sources = _simple_tokenize_test_sources
- deps = [ dir_pw_preprocessor ]
-}
-
-pw_test("simple_tokenize_test_cpp17") {
- configs = _simple_tokenize_test_configs
- sources = _simple_tokenize_test_sources
- deps = [ dir_pw_preprocessor ]
+pw_test("simple_tokenize_test") {
+ sources = [ "simple_tokenize_test.cc" ]
+ deps = [ ":pw_tokenizer" ]
}
pw_test("token_database_test") {
@@ -335,7 +278,9 @@ pw_fuzzer("token_database_fuzzer") {
":decoder",
"$dir_pw_fuzzer",
"$dir_pw_preprocessor",
+ dir_pw_span,
]
+ enable_test_if = false
}
pw_fuzzer("detokenize_fuzzer") {
@@ -379,6 +324,7 @@ pw_shared_library("detokenizer_jni") {
":decoder",
"$dir_pw_preprocessor",
]
+ deps = [ dir_pw_span ]
}
pw_doc_group("docs") {
@@ -387,4 +333,22 @@ pw_doc_group("docs") {
"proto.rst",
]
inputs = [ "py/pw_tokenizer/encode.py" ]
+ report_deps = [ ":tokenizer_size_report" ]
+}
+
+# Pigweed tokenizer size report.
+pw_size_diff("tokenizer_size_report") {
+ title = "Pigweed tokenizer size report"
+ binaries = [
+ {
+ target = "size_report:tokenize_string"
+ base = "size_report:tokenize_string_base"
+ label = "tokenize a string"
+ },
+ {
+ target = "size_report:tokenize_string_expr"
+ base = "size_report:tokenize_string_expr_base"
+ label = "tokenize a string expression"
+ },
+ ]
}
diff --git a/pw_tokenizer/CMakeLists.txt b/pw_tokenizer/CMakeLists.txt
index 1ceecbc2a..cd4a419b8 100644
--- a/pw_tokenizer/CMakeLists.txt
+++ b/pw_tokenizer/CMakeLists.txt
@@ -17,7 +17,7 @@ include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
pw_add_module_config(pw_tokenizer_CONFIG)
-pw_add_module_library(pw_tokenizer.config
+pw_add_library(pw_tokenizer.config INTERFACE
HEADERS
public/pw_tokenizer/config.h
PUBLIC_INCLUDES
@@ -26,7 +26,7 @@ pw_add_module_library(pw_tokenizer.config
${pw_tokenizer_CONFIG}
)
-pw_add_module_library(pw_tokenizer
+pw_add_library(pw_tokenizer STATIC
HEADERS
public/pw_tokenizer/encode_args.h
public/pw_tokenizer/hash.h
@@ -35,7 +35,7 @@ pw_add_module_library(pw_tokenizer
public
PUBLIC_DEPS
pw_containers
- pw_polyfill.span
+ pw_span
pw_preprocessor
pw_tokenizer.config
SOURCES
@@ -54,10 +54,13 @@ pw_add_module_library(pw_tokenizer
pw_varint
)
-if("${CMAKE_SYSTEM_NAME}" STREQUAL "")
+if(Zephyr_FOUND)
+ zephyr_linker_sources(SECTIONS "${CMAKE_CURRENT_SOURCE_DIR}/pw_tokenizer_linker_rules.ld")
+elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "")
target_link_options(pw_tokenizer
PUBLIC
"-T${CMAKE_CURRENT_SOURCE_DIR}/pw_tokenizer_linker_sections.ld"
+ "-L${CMAKE_CURRENT_SOURCE_DIR}"
)
elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
target_link_options(pw_tokenizer
@@ -67,29 +70,29 @@ elseif("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")
)
endif()
-pw_add_module_library(pw_tokenizer.base64
+pw_add_library(pw_tokenizer.base64 STATIC
HEADERS
public/pw_tokenizer/base64.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
pw_base64
- pw_polyfill.cstddef
- pw_polyfill.span
+ pw_span
+ pw_string.string
pw_tokenizer
pw_tokenizer.config
SOURCES
base64.cc
)
-pw_add_module_library(pw_tokenizer.decoder
+pw_add_library(pw_tokenizer.decoder STATIC
HEADERS
public/pw_tokenizer/detokenize.h
public/pw_tokenizer/token_database.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
- pw_polyfill.span
+ pw_span
pw_tokenizer
SOURCES
decode.cc
@@ -97,37 +100,11 @@ pw_add_module_library(pw_tokenizer.decoder
public/pw_tokenizer/internal/decode.h
token_database.cc
PRIVATE_DEPS
+ pw_bytes
+ pw_bytes.bit
pw_varint
)
-pw_add_facade(pw_tokenizer.global_handler
- DEFAULT_BACKEND
- pw_build.empty # Default to an empty backend so the tests can run.
- HEADERS
- public/pw_tokenizer/tokenize_to_global_handler.h
- PUBLIC_INCLUDES
- public
- PUBLIC_DEPS
- pw_preprocessor
- pw_tokenizer
- SOURCES
- tokenize_to_global_handler.cc
-)
-
-pw_add_facade(pw_tokenizer.global_handler_with_payload
- DEFAULT_BACKEND
- pw_build.empty # Default to an empty backend so the tests can run.
- HEADERS
- public/pw_tokenizer/tokenize_to_global_handler_with_payload.h
- PUBLIC_INCLUDES
- public
- PUBLIC_DEPS
- pw_preprocessor
- pw_tokenizer
- SOURCES
- tokenize_to_global_handler_with_payload.cc
-)
-
pw_proto_library(pw_tokenizer.proto
SOURCES
options.proto
@@ -157,7 +134,7 @@ pw_add_test(pw_tokenizer.argument_types_test
SOURCES
argument_types_test_c.c
argument_types_test.cc
- DEPS
+ PRIVATE_DEPS
pw_tokenizer
GROUPS
modules
@@ -167,7 +144,7 @@ pw_add_test(pw_tokenizer.argument_types_test
pw_add_test(pw_tokenizer.base64_test
SOURCES
base64_test.cc
- DEPS
+ PRIVATE_DEPS
pw_tokenizer.base64
GROUPS
modules
@@ -177,7 +154,7 @@ pw_add_test(pw_tokenizer.base64_test
pw_add_test(pw_tokenizer.decode_test
SOURCES
decode_test.cc
- DEPS
+ PRIVATE_DEPS
pw_varint
pw_tokenizer.decoder
GROUPS
@@ -188,20 +165,18 @@ pw_add_test(pw_tokenizer.decode_test
pw_add_test(pw_tokenizer.detokenize_test
SOURCES
detokenize_test.cc
- DEPS
+ PRIVATE_DEPS
pw_tokenizer.decoder
GROUPS
modules
pw_tokenizer
)
-pw_add_test(pw_tokenizer.global_handlers_test
+pw_add_test(pw_tokenizer.encode_args_test
SOURCES
- global_handlers_test_c.c
- global_handlers_test.cc
- DEPS
- pw_tokenizer.global_handler
- pw_tokenizer.global_handler_with_payload
+ encode_args_test.cc
+ PRIVATE_DEPS
+ pw_tokenizer
GROUPS
modules
pw_tokenizer
@@ -210,7 +185,7 @@ pw_add_test(pw_tokenizer.global_handlers_test
pw_add_test(pw_tokenizer.hash_test
SOURCES
hash_test.cc
- DEPS
+ PRIVATE_DEPS
pw_tokenizer
GROUPS
modules
@@ -220,7 +195,7 @@ pw_add_test(pw_tokenizer.hash_test
pw_add_test(pw_tokenizer.token_database_test
SOURCES
token_database_test.cc
- DEPS
+ PRIVATE_DEPS
pw_tokenizer.decoder
GROUPS
modules
@@ -231,10 +206,16 @@ pw_add_test(pw_tokenizer.tokenize_test
SOURCES
tokenize_test_c.c
tokenize_test.cc
- DEPS
+ PRIVATE_DEPS
pw_varint
pw_tokenizer
GROUPS
modules
pw_tokenizer
)
+
+if(Zephyr_FOUND)
+ zephyr_link_libraries_ifdef(CONFIG_PIGWEED_TOKENIZER pw_tokenizer)
+ zephyr_link_libraries_ifdef(CONFIG_PIGWEED_TOKENIZER_BASE64 pw_tokenizer.base64)
+ zephyr_link_libraries_ifdef(CONFIG_PIGWEED_DETOKENIZER pw_tokenizer.decoder)
+endif()
diff --git a/pw_tokenizer/Kconfig b/pw_tokenizer/Kconfig
new file mode 100644
index 000000000..99d128068
--- /dev/null
+++ b/pw_tokenizer/Kconfig
@@ -0,0 +1,33 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+config PIGWEED_TOKENIZER
+ bool "Enable the pw_tokenizer library"
+ select PIGWEED_CONTAINERS
+ select PIGWEED_PREPROCESSOR
+ select PIGWEED_SPAN
+ select PIGWEED_VARINT
+
+config PIGWEED_TOKENIZER_BASE64
+ bool "Enable the pw_tokenizer.base64 library"
+ select PIGWEED_TOKENIZER
+ select PIGWEED_BASE64
+ select PIGWEED_SPAN
+
+config PIGWEED_DETOKENIZER
+ bool "Enable the pw_tokenizer.decoder library"
+ select PIGWEED_TOKENIZER
+ select PIGWEED_SPAN
+ select PIGWEED_BYTES
+ select PIGWEED_VARINT \ No newline at end of file
diff --git a/pw_tokenizer/argument_types_test.cc b/pw_tokenizer/argument_types_test.cc
index 696886e37..10c309529 100644
--- a/pw_tokenizer/argument_types_test.cc
+++ b/pw_tokenizer/argument_types_test.cc
@@ -18,6 +18,7 @@
#include "gtest/gtest.h"
#include "pw_preprocessor/concat.h"
+#include "pw_tokenizer/tokenize.h"
#include "pw_tokenizer_private/argument_types_test.h"
namespace pw::tokenizer {
diff --git a/pw_tokenizer/argument_types_test_c.c b/pw_tokenizer/argument_types_test_c.c
index 05257d72e..aa2b52ffb 100644
--- a/pw_tokenizer/argument_types_test_c.c
+++ b/pw_tokenizer/argument_types_test_c.c
@@ -18,13 +18,17 @@
#include <assert.h>
#include <stddef.h>
+#include "pw_tokenizer/tokenize.h"
#include "pw_tokenizer_private/argument_types_test.h"
#ifdef __cplusplus
#error "This is a test of C code and must be compiled as C, not C++."
#endif // __cplusplus
-struct FakeType {}; // stand-in type for pointer argument type test
+// Stand-in type for pointer argument type test
+struct FakeType {
+ char unused;
+};
// Check each relevant type mapping using static_asserts.
#define CHECK_TYPE(c_type, enum_type) \
@@ -76,7 +80,8 @@ static char char_array[16];
pw_tokenizer_ArgTypes pw_TestTokenizer##name(void) { \
(void)char_array; \
return PW_TOKENIZER_ARG_TYPES(__VA_ARGS__); \
- }
+ } \
+ static_assert(1, "Macros must be terminated with a semicolon")
DEFINE_TEST_FUNCTION(NoArgs);
@@ -86,7 +91,7 @@ DEFINE_TEST_FUNCTION(Uint16, ((int16_t)100));
DEFINE_TEST_FUNCTION(Int32, ((int32_t)1));
DEFINE_TEST_FUNCTION(Int64, ((int64_t)0));
DEFINE_TEST_FUNCTION(Uint64, ((uint64_t)1));
-DEFINE_TEST_FUNCTION(Float, 1e10f)
+DEFINE_TEST_FUNCTION(Float, 1e10f);
DEFINE_TEST_FUNCTION(Double, -2.5e-50);
DEFINE_TEST_FUNCTION(String, "const char*");
DEFINE_TEST_FUNCTION(MutableString, ((char*)NULL));
diff --git a/pw_tokenizer/backend.gni b/pw_tokenizer/backend.gni
index dd00ac93b..ed0addf54 100644
--- a/pw_tokenizer/backend.gni
+++ b/pw_tokenizer/backend.gni
@@ -13,8 +13,9 @@
# the License.
declare_args() {
- # Backends for the pw_tokenizer:global_handler and
- # pw_tokenizer:global_handler_with_payload facades.
- pw_tokenizer_GLOBAL_HANDLER_BACKEND = ""
+ # This variable is deprecated. It is only used for pw_log_tokenized, and is
+ # provided for backwards compatibility. Set pw_log_tokenized_HANDLER_BACKEND
+ # instead.
+ # TODO(hepler): Remove this variable after migrating projects.
pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND = ""
}
diff --git a/pw_tokenizer/base64.cc b/pw_tokenizer/base64.cc
index 2b871617d..658ee5930 100644
--- a/pw_tokenizer/base64.cc
+++ b/pw_tokenizer/base64.cc
@@ -33,13 +33,19 @@ extern "C" size_t pw_tokenizer_PrefixedBase64Encode(
}
output[0] = kBase64Prefix;
- base64::Encode(std::span(static_cast<const std::byte*>(binary_message),
- binary_size_bytes),
- &output[1]);
+ base64::Encode(
+ span(static_cast<const std::byte*>(binary_message), binary_size_bytes),
+ &output[1]);
output[encoded_size - 1] = '\0';
return encoded_size - sizeof('\0'); // exclude the null terminator
}
+void PrefixedBase64Encode(span<const std::byte> binary_message,
+ InlineString<>& output) {
+ output.push_back(kBase64Prefix);
+ base64::Encode(binary_message, output);
+}
+
extern "C" size_t pw_tokenizer_PrefixedBase64Decode(const void* base64_message,
size_t base64_size_bytes,
void* output_buffer,
@@ -52,7 +58,7 @@ extern "C" size_t pw_tokenizer_PrefixedBase64Decode(const void* base64_message,
return base64::Decode(
std::string_view(&base64[1], base64_size_bytes - 1),
- std::span(static_cast<std::byte*>(output_buffer), output_buffer_size));
+ span(static_cast<std::byte*>(output_buffer), output_buffer_size));
}
} // namespace pw::tokenizer
diff --git a/pw_tokenizer/base64_test.cc b/pw_tokenizer/base64_test.cc
index d751b8ee1..fee5a673d 100644
--- a/pw_tokenizer/base64_test.cc
+++ b/pw_tokenizer/base64_test.cc
@@ -15,10 +15,10 @@
#include "pw_tokenizer/base64.h"
#include <cstring>
-#include <span>
#include <string_view>
#include "gtest/gtest.h"
+#include "pw_span/span.h"
namespace pw::tokenizer {
namespace {
@@ -41,10 +41,9 @@ class PrefixedBase64 : public ::testing::Test {
const struct TestData {
template <size_t kSize>
TestData(const char (&binary_data)[kSize], const char* base64_data)
- : binary{std::as_bytes(std::span(binary_data, kSize - 1))},
- base64(base64_data) {}
+ : binary{as_bytes(span(binary_data, kSize - 1))}, base64(base64_data) {}
- std::span<const byte> binary;
+ span<const byte> binary;
std::string_view base64;
} kTestData[] = {
{"", "$"},
@@ -72,33 +71,48 @@ TEST_F(PrefixedBase64, Encode) {
}
TEST_F(PrefixedBase64, Encode_EmptyInput_WritesPrefix) {
- EXPECT_EQ(1u, PrefixedBase64Encode(std::span<byte>(), base64_));
+ EXPECT_EQ(1u, PrefixedBase64Encode(span<byte>(), base64_));
EXPECT_EQ('$', base64_[0]);
EXPECT_EQ('\0', base64_[1]);
}
TEST_F(PrefixedBase64, Encode_EmptyOutput_WritesNothing) {
- EXPECT_EQ(0u,
- PrefixedBase64Encode(kTestData[5].binary, std::span(base64_, 0)));
+ EXPECT_EQ(0u, PrefixedBase64Encode(kTestData[5].binary, span(base64_, 0)));
EXPECT_EQ(kUnset, base64_[0]);
}
TEST_F(PrefixedBase64, Encode_SingleByteOutput_OnlyNullTerminates) {
- EXPECT_EQ(0u,
- PrefixedBase64Encode(kTestData[5].binary, std::span(base64_, 1)));
+ EXPECT_EQ(0u, PrefixedBase64Encode(kTestData[5].binary, span(base64_, 1)));
EXPECT_EQ('\0', base64_[0]);
EXPECT_EQ(kUnset, base64_[1]);
}
TEST_F(PrefixedBase64, Encode_NoRoomForNullAfterMessage_OnlyNullTerminates) {
- EXPECT_EQ(
- 0u,
- PrefixedBase64Encode(kTestData[5].binary,
- std::span(base64_, kTestData[5].base64.size())));
+ EXPECT_EQ(0u,
+ PrefixedBase64Encode(kTestData[5].binary,
+ span(base64_, kTestData[5].base64.size())));
EXPECT_EQ('\0', base64_[0]);
EXPECT_EQ(kUnset, base64_[1]);
}
+TEST_F(PrefixedBase64, Encode_InlineString) {
+ for (auto& [binary, base64] : kTestData) {
+ EXPECT_EQ(base64, PrefixedBase64Encode(binary));
+ }
+}
+
+TEST_F(PrefixedBase64, Encode_InlineString_Append) {
+ for (auto& [binary, base64] : kTestData) {
+ pw::InlineString<32> string("Other stuff!");
+ PrefixedBase64Encode(binary, string);
+
+ pw::InlineString<32> expected("Other stuff!");
+ expected.append(base64);
+
+ EXPECT_EQ(expected, string);
+ }
+}
+
TEST_F(PrefixedBase64, Base64EncodedBufferSize_Empty_RoomForPrefixAndNull) {
EXPECT_EQ(2u, Base64EncodedBufferSize(0));
}
@@ -130,27 +144,26 @@ TEST_F(PrefixedBase64, Decode_OnlyPrefix_WritesNothing) {
}
TEST_F(PrefixedBase64, Decode_EmptyOutput_WritesNothing) {
- EXPECT_EQ(0u,
- PrefixedBase64Decode(kTestData[5].base64, std::span(binary_, 0)));
+ EXPECT_EQ(0u, PrefixedBase64Decode(kTestData[5].base64, span(binary_, 0)));
EXPECT_EQ(byte{kUnset}, binary_[0]);
}
TEST_F(PrefixedBase64, Decode_OutputTooSmall_WritesNothing) {
auto& item = kTestData[5];
- EXPECT_EQ(0u,
- PrefixedBase64Decode(item.base64,
- std::span(binary_, item.binary.size() - 1)));
+ EXPECT_EQ(
+ 0u,
+ PrefixedBase64Decode(item.base64, span(binary_, item.binary.size() - 1)));
EXPECT_EQ(byte{kUnset}, binary_[0]);
}
-TEST(PrefixedBase64, DecodeInPlace) {
+TEST(PrefixedBase64DecodeInPlace, DecodeInPlace) {
byte buffer[32];
for (auto& [binary, base64] : kTestData) {
std::memcpy(buffer, base64.data(), base64.size());
EXPECT_EQ(binary.size(),
- PrefixedBase64DecodeInPlace(std::span(buffer, base64.size())));
+ PrefixedBase64DecodeInPlace(span(buffer, base64.size())));
ASSERT_EQ(0, std::memcmp(binary.data(), buffer, binary.size()));
}
}
diff --git a/pw_tokenizer/database.gni b/pw_tokenizer/database.gni
index 07d441f41..a0cb024e2 100644
--- a/pw_tokenizer/database.gni
+++ b/pw_tokenizer/database.gni
@@ -94,14 +94,19 @@ template("pw_tokenizer_database") {
pw_python_action(target_name) {
script = "$dir_pw_tokenizer/py/pw_tokenizer/database.py"
- # Restrict parallelism for updating this database file to one thread. This
- # makes it safe to update it from multiple toolchains.
- pool = "$dir_pw_tokenizer/pool:database($default_toolchain)"
-
inputs = _input_databases
if (_create == "") {
+ # Restrict parallelism for updating this database file to one thread. This
+ # makes it safe to update it from multiple toolchains.
+ pool = "$dir_pw_tokenizer/pool:database($default_toolchain)"
args = [ "add" ]
+ if (defined(invoker.commit)) {
+ args += [
+ "--discard-temporary",
+ invoker.commit,
+ ]
+ }
inputs += [ _database ]
stamp = true
} else {
diff --git a/pw_tokenizer/decode.cc b/pw_tokenizer/decode.cc
index ea18c110e..76b524d40 100644
--- a/pw_tokenizer/decode.cc
+++ b/pw_tokenizer/decode.cc
@@ -172,7 +172,7 @@ StringSegment::ArgSize StringSegment::VarargSize(std::array<char, 2> length,
}
DecodedArg StringSegment::DecodeString(
- const std::span<const uint8_t>& arguments) const {
+ const span<const uint8_t>& arguments) const {
if (arguments.empty()) {
return DecodedArg(ArgStatus::kMissing, text_);
}
@@ -184,14 +184,15 @@ DecodedArg StringSegment::DecodeString(
if (arguments.size() - 1 < size) {
status.Update(ArgStatus::kDecodeError);
+ span<const uint8_t> arg_val = arguments.subspan(1);
return DecodedArg(
status,
text_,
arguments.size(),
- {reinterpret_cast<const char*>(&arguments[1]), arguments.size() - 1});
+ {reinterpret_cast<const char*>(arg_val.data()), arg_val.size()});
}
- std::string value(reinterpret_cast<const char*>(&arguments[1]), size);
+ std::string value(reinterpret_cast<const char*>(arguments.data() + 1), size);
if (status.HasError(ArgStatus::kTruncated)) {
value.append("[...]");
@@ -201,13 +202,13 @@ DecodedArg StringSegment::DecodeString(
}
DecodedArg StringSegment::DecodeInteger(
- const std::span<const uint8_t>& arguments) const {
+ const span<const uint8_t>& arguments) const {
if (arguments.empty()) {
return DecodedArg(ArgStatus::kMissing, text_);
}
int64_t value;
- const size_t bytes = varint::Decode(std::as_bytes(arguments), &value);
+ const size_t bytes = varint::Decode(as_bytes(arguments), &value);
if (bytes == 0u) {
return DecodedArg(ArgStatus::kDecodeError,
@@ -229,7 +230,7 @@ DecodedArg StringSegment::DecodeInteger(
}
DecodedArg StringSegment::DecodeFloatingPoint(
- const std::span<const uint8_t>& arguments) const {
+ const span<const uint8_t>& arguments) const {
static_assert(sizeof(float) == 4u);
if (arguments.size() < sizeof(float)) {
return DecodedArg(ArgStatus::kMissing, text_);
@@ -240,8 +241,7 @@ DecodedArg StringSegment::DecodeFloatingPoint(
return DecodedArg::FromValue(text_.c_str(), value, sizeof(value));
}
-DecodedArg StringSegment::Decode(
- const std::span<const uint8_t>& arguments) const {
+DecodedArg StringSegment::Decode(const span<const uint8_t>& arguments) const {
switch (type_) {
case kLiteral:
return DecodedArg(text_);
@@ -266,6 +266,11 @@ DecodedArg StringSegment::Skip() const {
return DecodedArg(text_);
case kPercent:
return DecodedArg("%");
+ case kString:
+ case kSignedInt:
+ case kUnsigned32:
+ case kUnsigned64:
+ case kFloatingPoint:
default:
return DecodedArg(ArgStatus::kSkipped, text_);
}
@@ -331,8 +336,7 @@ FormatString::FormatString(const char* format) {
}
}
-DecodedFormatString FormatString::Format(
- std::span<const uint8_t> arguments) const {
+DecodedFormatString FormatString::Format(span<const uint8_t> arguments) const {
std::vector<DecodedArg> results;
bool skip = false;
diff --git a/pw_tokenizer/detokenize.cc b/pw_tokenizer/detokenize.cc
index ad7bb78dd..5e3262f86 100644
--- a/pw_tokenizer/detokenize.cc
+++ b/pw_tokenizer/detokenize.cc
@@ -15,7 +15,10 @@
#include "pw_tokenizer/detokenize.h"
#include <algorithm>
+#include <cstring>
+#include "pw_bytes/bit.h"
+#include "pw_bytes/endian.h"
#include "pw_tokenizer/internal/decode.h"
namespace pw::tokenizer {
@@ -69,8 +72,8 @@ bool IsBetterResult(const DecodingResult& lhs, const DecodingResult& rhs) {
DetokenizedString::DetokenizedString(
uint32_t token,
- const std::span<const TokenizedStringEntry>& entries,
- const std::span<const uint8_t>& arguments)
+ const span<const TokenizedStringEntry>& entries,
+ const span<const uint8_t>& arguments)
: token_(token), has_token_(true) {
std::vector<DecodingResult> results;
@@ -104,22 +107,23 @@ Detokenizer::Detokenizer(const TokenDatabase& database) {
}
DetokenizedString Detokenizer::Detokenize(
- const std::span<const uint8_t>& encoded) const {
+ const span<const uint8_t>& encoded) const {
// The token is missing from the encoded data; there is nothing to do.
- if (encoded.size() < sizeof(uint32_t)) {
+ if (encoded.empty()) {
return DetokenizedString();
}
- const uint32_t token =
- encoded[3] << 24 | encoded[2] << 16 | encoded[1] << 8 | encoded[0];
+ uint32_t token = bytes::ReadInOrder<uint32_t>(
+ endian::little, encoded.data(), encoded.size());
const auto result = database_.find(token);
- return DetokenizedString(token,
- result == database_.end()
- ? std::span<TokenizedStringEntry>()
- : std::span(result->second),
- encoded.subspan(sizeof(token)));
+ return DetokenizedString(
+ token,
+ result == database_.end() ? span<TokenizedStringEntry>()
+ : span(result->second),
+ encoded.size() < sizeof(token) ? span<const uint8_t>()
+ : encoded.subspan(sizeof(token)));
}
} // namespace pw::tokenizer
diff --git a/pw_tokenizer/detokenize_fuzzer.cc b/pw_tokenizer/detokenize_fuzzer.cc
index 2b5604cc9..d7d36c15e 100644
--- a/pw_tokenizer/detokenize_fuzzer.cc
+++ b/pw_tokenizer/detokenize_fuzzer.cc
@@ -76,7 +76,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
std::vector<uint8_t> buffer =
provider.ConsumeBytes<uint8_t>(consumed_size);
auto detokenized_string =
- detokenizer.Detokenize(std::span(&buffer[0], buffer.size()));
+ detokenizer.Detokenize(span(&buffer[0], buffer.size()));
static_cast<void>(detokenized_string);
break;
}
diff --git a/pw_tokenizer/detokenize_test.cc b/pw_tokenizer/detokenize_test.cc
index 688cf7a5c..07dc1853a 100644
--- a/pw_tokenizer/detokenize_test.cc
+++ b/pw_tokenizer/detokenize_test.cc
@@ -57,10 +57,13 @@ TEST_F(Detokenize, NoFormatting) {
TEST_F(Detokenize, BestString_MissingToken_IsEmpty) {
EXPECT_FALSE(detok_.Detokenize("").ok());
EXPECT_TRUE(detok_.Detokenize("", 0u).BestString().empty());
- EXPECT_TRUE(detok_.Detokenize("\1", 1u).BestString().empty());
- EXPECT_TRUE(detok_.Detokenize("\1\0"sv).BestString().empty());
- EXPECT_TRUE(detok_.Detokenize("\1\0\0"sv).BestString().empty());
- EXPECT_TRUE(detok_.Detokenize("\0\0\0"sv).BestString().empty());
+}
+
+TEST_F(Detokenize, BestString_ShorterToken_ZeroExtended) {
+ EXPECT_EQ(detok_.Detokenize("\x42", 1u).token(), 0x42u);
+ EXPECT_EQ(detok_.Detokenize("\1\0"sv).token(), 0x1u);
+ EXPECT_EQ(detok_.Detokenize("\1\0\3"sv).token(), 0x030001u);
+ EXPECT_EQ(detok_.Detokenize("\0\0\0"sv).token(), 0x0u);
}
TEST_F(Detokenize, BestString_UnknownToken_IsEmpty) {
@@ -75,18 +78,20 @@ TEST_F(Detokenize, BestStringWithErrors_MissingToken_ErrorMessage) {
EXPECT_FALSE(detok_.Detokenize("").ok());
EXPECT_EQ(detok_.Detokenize("", 0u).BestStringWithErrors(),
ERR("missing token"));
- EXPECT_EQ(detok_.Detokenize("\1", 1u).BestStringWithErrors(),
- ERR("missing token"));
- EXPECT_EQ(detok_.Detokenize("\1\0"sv).BestStringWithErrors(),
- ERR("missing token"));
- EXPECT_EQ(detok_.Detokenize("\1\0\0"sv).BestStringWithErrors(),
- ERR("missing token"));
- EXPECT_EQ(detok_.Detokenize("\0\0\0"sv).BestStringWithErrors(),
- ERR("missing token"));
+}
+
+TEST_F(Detokenize, BestStringWithErrors_ShorterTokenMatchesStrings) {
+ EXPECT_EQ(detok_.Detokenize("\1", 1u).BestStringWithErrors(), "One");
+ EXPECT_EQ(detok_.Detokenize("\1\0"sv).BestStringWithErrors(), "One");
+ EXPECT_EQ(detok_.Detokenize("\1\0\0"sv).BestStringWithErrors(), "One");
}
TEST_F(Detokenize, BestStringWithErrors_UnknownToken_ErrorMessage) {
- EXPECT_FALSE(detok_.Detokenize("\0\0\0\0"sv).ok());
+ ASSERT_FALSE(detok_.Detokenize("\0\0\0\0"sv).ok());
+ EXPECT_EQ(detok_.Detokenize("\0"sv).BestStringWithErrors(),
+ ERR("unknown token 00000000"));
+ EXPECT_EQ(detok_.Detokenize("\0\0\0"sv).BestStringWithErrors(),
+ ERR("unknown token 00000000"));
EXPECT_EQ(detok_.Detokenize("\0\0\0\0"sv).BestStringWithErrors(),
ERR("unknown token 00000000"));
EXPECT_EQ(detok_.Detokenize("\2\0\0\0"sv).BestStringWithErrors(),
diff --git a/pw_tokenizer/docs.rst b/pw_tokenizer/docs.rst
index dc908ea4b..944f4cb32 100644
--- a/pw_tokenizer/docs.rst
+++ b/pw_tokenizer/docs.rst
@@ -1,8 +1,15 @@
.. _module-pw_tokenizer:
-------------
+============
pw_tokenizer
-------------
+============
+:bdg-primary:`host`
+:bdg-primary:`device`
+:bdg-secondary:`Python`
+:bdg-secondary:`C++`
+:bdg-secondary:`TypeScript`
+:bdg-success:`stable`
+
Logging is critical, but developers are often forced to choose between
additional logging or saving crucial flash space. The ``pw_tokenizer`` module
helps address this by replacing printf-style strings with binary tokens during
@@ -21,41 +28,41 @@ without printf-style arguments.
**Why tokenize strings?**
- * Dramatically reduce binary size by removing string literals from binaries.
- * Reduce I/O traffic, RAM, and flash usage by sending and storing compact
- tokens instead of strings. We've seen over 50% reduction in encoded log
- contents.
- * Reduce CPU usage by replacing snprintf calls with simple tokenization code.
- * Remove potentially sensitive log, assert, and other strings from binaries.
+* Dramatically reduce binary size by removing string literals from binaries.
+* Reduce I/O traffic, RAM, and flash usage by sending and storing compact tokens
+ instead of strings. We've seen over 50% reduction in encoded log contents.
+* Reduce CPU usage by replacing snprintf calls with simple tokenization code.
+* Remove potentially sensitive log, assert, and other strings from binaries.
+--------------
Basic overview
-==============
+--------------
There are two sides to ``pw_tokenizer``, which we call tokenization and
detokenization.
- * **Tokenization** converts string literals in the source code to
- binary tokens at compile time. If the string has printf-style arguments,
- these are encoded to compact binary form at runtime.
- * **Detokenization** converts tokenized strings back to the original
- human-readable strings.
+* **Tokenization** converts string literals in the source code to binary tokens
+ at compile time. If the string has printf-style arguments, these are encoded
+ to compact binary form at runtime.
+* **Detokenization** converts tokenized strings back to the original
+ human-readable strings.
Here's an overview of what happens when ``pw_tokenizer`` is used:
- 1. During compilation, the ``pw_tokenizer`` module hashes string literals to
- generate stable 32-bit tokens.
- 2. The tokenization macro removes these strings by declaring them in an ELF
- section that is excluded from the final binary.
- 3. After compilation, strings are extracted from the ELF to build a database
- of tokenized strings for use by the detokenizer. The ELF file may also be
- used directly.
- 4. During operation, the device encodes the string token and its arguments, if
- any.
- 5. The encoded tokenized strings are sent off-device or stored.
- 6. Off-device, the detokenizer tools use the token database to decode the
- strings to human-readable form.
+1. During compilation, the ``pw_tokenizer`` module hashes string literals to
+ generate stable 32-bit tokens.
+2. The tokenization macro removes these strings by declaring them in an ELF
+ section that is excluded from the final binary.
+3. After compilation, strings are extracted from the ELF to build a database of
+ tokenized strings for use by the detokenizer. The ELF file may also be used
+ directly.
+4. During operation, the device encodes the string token and its arguments, if
+ any.
+5. The encoded tokenized strings are sent off-device or stored.
+6. Off-device, the detokenizer tools use the token database to decode the
+ strings to human-readable form.
Example: tokenized logging
---------------------------
+==========================
This example demonstrates using ``pw_tokenizer`` for logging. In this example,
tokenized logging saves ~90% in binary size (41 → 4 bytes) and 70% in encoded
size (49 → 15 bytes).
@@ -106,281 +113,256 @@ size (49 → 15 bytes).
| When viewed | ``"Battery state: CHARGING; battery voltage: 3989 mV"`` | |
+------------------+-----------------------------------------------------------+---------+
+---------------
Getting started
-===============
+---------------
Integrating ``pw_tokenizer`` requires a few steps beyond building the code. This
section describes one way ``pw_tokenizer`` might be integrated with a project.
These steps can be adapted as needed.
- 1. Add ``pw_tokenizer`` to your build. Build files for GN, CMake, and Bazel
- are provided. For Make or other build systems, add the files specified in
- the BUILD.gn's ``pw_tokenizer`` target to the build.
- 2. Use the tokenization macros in your code. See `Tokenization`_.
- 3. Add the contents of ``pw_tokenizer_linker_sections.ld`` to your project's
- linker script. In GN and CMake, this step is done automatically.
- 4. Compile your code to produce an ELF file.
- 5. Run ``database.py create`` on the ELF file to generate a CSV token
- database. See `Managing token databases`_.
- 6. Commit the token database to your repository. See notes in `Database
- management`_.
- 7. Integrate a ``database.py add`` command to your build to automatically
- update the committed token database. In GN, use the
- ``pw_tokenizer_database`` template to do this. See `Update a database`_.
- 8. Integrate ``detokenize.py`` or the C++ detokenization library with your
- tools to decode tokenized logs. See `Detokenization`_.
+1. Add ``pw_tokenizer`` to your build. Build files for GN, CMake, and Bazel are
+ provided. For Make or other build systems, add the files specified in the
+ BUILD.gn's ``pw_tokenizer`` target to the build.
+2. Use the tokenization macros in your code. See `Tokenization`_.
+3. Add the contents of ``pw_tokenizer_linker_sections.ld`` to your project's
+ linker script. In GN and CMake, this step is done automatically.
+4. Compile your code to produce an ELF file.
+5. Run ``database.py create`` on the ELF file to generate a CSV token
+ database. See `Managing token databases`_.
+6. Commit the token database to your repository. See notes in `Database
+ management`_.
+7. Integrate a ``database.py add`` command to your build to automatically update
+ the committed token database. In GN, use the ``pw_tokenizer_database``
+ template to do this. See `Update a database`_.
+8. Integrate ``detokenize.py`` or the C++ detokenization library with your tools
+ to decode tokenized logs. See `Detokenization`_.
+
+Using with Zephyr
+=================
+When building ``pw_tokenizer`` with Zephyr, 3 Kconfigs can be used currently:
+
+* ``CONFIG_PIGWEED_TOKENIZER`` will automatically link ``pw_tokenizer`` as well
+ as any dependencies.
+* ``CONFIG_PIGWEED_TOKENIZER_BASE64`` will automatically link
+ ``pw_tokenizer.base64`` as well as any dependencies.
+* ``CONFIG_PIGWEED_DETOKENIZER`` will automatically link
+ ``pw_tokenizer.decoder`` as well as any dependencies.
+
+Once enabled, the tokenizer headers can be included like any Zephyr headers:
+
+.. code-block:: cpp
+
+ #include <pw_tokenizer/tokenize.h>
+
+.. note::
+ Zephyr handles the additional linker sections via
+ ``pw_tokenizer_linker_rules.ld`` which is added to the end of the linker file
+ via a call to ``zephyr_linker_sources(SECTIONS ...)``.
+------------
Tokenization
-============
+------------
Tokenization converts a string literal to a token. If it's a printf-style
string, its arguments are encoded along with it. The results of tokenization can
be sent off device or stored in place of a full string.
+.. doxygentypedef:: pw_tokenizer_Token
+
Tokenization macros
--------------------
+===================
Adding tokenization to a project is simple. To tokenize a string, include
``pw_tokenizer/tokenize.h`` and invoke one of the ``PW_TOKENIZE_`` macros.
Tokenize a string literal
-^^^^^^^^^^^^^^^^^^^^^^^^^
-The ``PW_TOKENIZE_STRING`` macro converts a string literal to a ``uint32_t``
-token.
-
-.. code-block:: cpp
-
- constexpr uint32_t token = PW_TOKENIZE_STRING("Any string literal!");
+-------------------------
+``pw_tokenizer`` provides macros for tokenizing string literals with no
+arguments.
-.. admonition:: When to use this macro
+.. doxygendefine:: PW_TOKENIZE_STRING
+.. doxygendefine:: PW_TOKENIZE_STRING_DOMAIN
+.. doxygendefine:: PW_TOKENIZE_STRING_MASK
- Use ``PW_TOKENIZE_STRING`` to tokenize string literals that do not have
- %-style arguments.
+The tokenization macros above cannot be used inside other expressions.
-Tokenize to a handler function
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-``PW_TOKENIZE_TO_GLOBAL_HANDLER`` is the most efficient tokenization function,
-since it takes the fewest arguments. It encodes a tokenized string to a
-buffer on the stack. The size of the buffer is set with
-``PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES``.
+.. admonition:: **Yes**: Assign :c:macro:`PW_TOKENIZE_STRING` to a ``constexpr`` variable.
+ :class: checkmark
-This macro is provided by the ``pw_tokenizer:global_handler`` facade. The
-backend for this facade must define the ``pw_tokenizer_HandleEncodedMessage``
-C-linkage function.
+ .. code:: cpp
-.. code-block:: cpp
+ constexpr uint32_t kGlobalToken = PW_TOKENIZE_STRING("Wowee Zowee!");
- PW_TOKENIZE_TO_GLOBAL_HANDLER(format_string_literal, arguments...);
+ void Function() {
+ constexpr uint32_t local_token = PW_TOKENIZE_STRING("Wowee Zowee?");
+ }
- void pw_tokenizer_HandleEncodedMessage(const uint8_t encoded_message[],
- size_t size_bytes);
+.. admonition:: **No**: Use :c:macro:`PW_TOKENIZE_STRING` in another expression.
+ :class: error
-``PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD`` is similar, but passes a
-``uintptr_t`` argument to the global handler function. Values like a log level
-can be packed into the ``uintptr_t``.
+ .. code:: cpp
-This macro is provided by the ``pw_tokenizer:global_handler_with_payload``
-facade. The backend for this facade must define the
-``pw_tokenizer_HandleEncodedMessageWithPayload`` C-linkage function.
+ void BadExample() {
+ ProcessToken(PW_TOKENIZE_STRING("This won't compile!"));
+ }
-.. code-block:: cpp
+ Use :c:macro:`PW_TOKENIZE_STRING_EXPR` instead.
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(payload,
- format_string_literal,
- arguments...);
+An alternate set of macros are provided for use inside expressions. These make
+use of lambda functions, so while they can be used inside expressions, they
+require C++ and cannot be assigned to constexpr variables or be used with
+special function variables like ``__func__``.
- void pw_tokenizer_HandleEncodedMessageWithPayload(
- uintptr_t payload, const uint8_t encoded_message[], size_t size_bytes);
+.. doxygendefine:: PW_TOKENIZE_STRING_EXPR
+.. doxygendefine:: PW_TOKENIZE_STRING_DOMAIN_EXPR
+.. doxygendefine:: PW_TOKENIZE_STRING_MASK_EXPR
.. admonition:: When to use these macros
- Use anytime a global handler is sufficient, particularly for widely expanded
- macros, like a logging macro. ``PW_TOKENIZE_TO_GLOBAL_HANDLER`` or
- ``PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD`` are the most efficient macros
- for tokenizing printf-style strings.
+ Use :c:macro:`PW_TOKENIZE_STRING` and related macros to tokenize string
+ literals that do not need %-style arguments encoded.
-Tokenize to a callback
-^^^^^^^^^^^^^^^^^^^^^^
-``PW_TOKENIZE_TO_CALLBACK`` tokenizes to a buffer on the stack and calls a
-``void(const uint8_t* buffer, size_t buffer_size)`` callback that is provided at
-the call site. The size of the buffer is set with
-``PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES``.
+.. admonition:: **Yes**: Use :c:macro:`PW_TOKENIZE_STRING_EXPR` within other expressions.
+ :class: checkmark
-.. code-block:: cpp
+ .. code:: cpp
- PW_TOKENIZE_TO_CALLBACK(HandlerFunction, "Format string: %x", arguments...);
+ void GoodExample() {
+ ProcessToken(PW_TOKENIZE_STRING_EXPR("This will compile!"));
+ }
-.. admonition:: When to use this macro
+.. admonition:: **No**: Assign :c:macro:`PW_TOKENIZE_STRING_EXPR` to a ``constexpr`` variable.
+ :class: error
- Use ``PW_TOKENIZE_TO_CALLBACK`` if the global handler version is already in
- use for another purpose or more flexibility is needed.
+ .. code:: cpp
-Tokenize to a buffer
-^^^^^^^^^^^^^^^^^^^^
-The most flexible tokenization macro is ``PW_TOKENIZE_TO_BUFFER``, which encodes
-to a caller-provided buffer.
+ constexpr uint32_t wont_work = PW_TOKENIZE_STRING_EXPR("This won't compile!"));
-.. code-block:: cpp
+ Instead, use :c:macro:`PW_TOKENIZE_STRING` to assign to a ``constexpr`` variable.
- uint8_t buffer[BUFFER_SIZE];
- size_t size_bytes = sizeof(buffer);
- PW_TOKENIZE_TO_BUFFER(buffer, &size_bytes, format_string_literal, arguments...);
+.. admonition:: **No**: Tokenize ``__func__`` in :c:macro:`PW_TOKENIZE_STRING_EXPR`.
+ :class: error
-While ``PW_TOKENIZE_TO_BUFFER`` is maximally flexible, it takes more arguments
-than the other macros, so its per-use code size overhead is larger.
+ .. code:: cpp
-.. admonition:: When to use this macro
+ void BadExample() {
+ // This compiles, but __func__ will not be the outer function's name, and
+ // there may be compiler warnings.
+ constexpr uint32_t wont_work = PW_TOKENIZE_STRING_EXPR(__func__);
+ }
- Use ``PW_TOKENIZE_TO_BUFFER`` to encode to a custom-sized buffer or if the
- other macros are insufficient. Avoid using ``PW_TOKENIZE_TO_BUFFER`` in
- widely expanded macros, such as a logging macro, because it will result in
- larger code size than its alternatives.
+ Instead, use :c:macro:`PW_TOKENIZE_STRING` to tokenize ``__func__`` or similar macros.
.. _module-pw_tokenizer-custom-macro:
-Tokenize with a custom macro
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Projects may need more flexbility than the standard ``pw_tokenizer`` macros
-provide. To support this, projects may define custom tokenization macros. This
-requires the use of two low-level ``pw_tokenizer`` macros:
-
-.. c:macro:: PW_TOKENIZE_FORMAT_STRING(domain, mask, format, ...)
+Tokenize a message with arguments in a custom macro
+---------------------------------------------------
+Projects can leverage the tokenization machinery in whichever way best suits
+their needs. The most efficient way to use ``pw_tokenizer`` is to pass tokenized
+data to a global handler function. A project's custom tokenization macro can
+handle tokenized data in a function of their choosing.
- Tokenizes a format string and sets the ``_pw_tokenizer_token`` variable to the
- token. Must be used in its own scope, since the same variable is used in every
- invocation.
+``pw_tokenizer`` provides two low-level macros for projects to use
+to create custom tokenization macros.
- The tokenized string uses the specified :ref:`tokenization domain
- <module-pw_tokenizer-domains>`. Use ``PW_TOKENIZER_DEFAULT_DOMAIN`` for the
- default. The token also may be masked; use ``UINT32_MAX`` to keep all bits.
+.. doxygendefine:: PW_TOKENIZE_FORMAT_STRING
+.. doxygendefine:: PW_TOKENIZER_ARG_TYPES
-.. c:macro:: PW_TOKENIZER_ARG_TYPES(...)
+The outputs of these macros are typically passed to an encoding function. That
+function encodes the token, argument types, and argument data to a buffer using
+helpers provided by ``pw_tokenizer/encode_args.h``.
- Converts a series of arguments to a compact format that replaces the format
- string literal.
+.. doxygenfunction:: pw::tokenizer::EncodeArgs
+.. doxygenclass:: pw::tokenizer::EncodedMessage
+ :members:
+.. doxygenfunction:: pw_tokenizer_EncodeArgs
-Use these two macros within the custom tokenization macro to call a function
-that does the encoding. The following example implements a custom tokenization
-macro for use with :ref:`module-pw_log_tokenized`.
+Example
+^^^^^^^
+The following example implements a custom tokenization macro similar to
+:ref:`module-pw_log_tokenized`.
.. code-block:: cpp
- #include "pw_tokenizer/tokenize.h"
+ #include "pw_tokenizer/tokenize.h"
- #ifndef __cplusplus
- extern "C" {
- #endif
+ #ifndef __cplusplus
+ extern "C" {
+ #endif
- void EncodeTokenizedMessage(pw_tokenizer_Payload metadata,
- pw_tokenizer_Token token,
- pw_tokenizer_ArgTypes types,
- ...);
+ void EncodeTokenizedMessage(uint32_t metadata,
+ pw_tokenizer_Token token,
+ pw_tokenizer_ArgTypes types,
+ ...);
- #ifndef __cplusplus
- } // extern "C"
- #endif
+ #ifndef __cplusplus
+ } // extern "C"
+ #endif
- #define PW_LOG_TOKENIZED_ENCODE_MESSAGE(metadata, format, ...) \
- do { \
- PW_TOKENIZE_FORMAT_STRING( \
- PW_TOKENIZER_DEFAULT_DOMAIN, UINT32_MAX, format, __VA_ARGS__); \
- EncodeTokenizedMessage(payload, \
- _pw_tokenizer_token, \
- PW_TOKENIZER_ARG_TYPES(__VA_ARGS__) \
- PW_COMMA_ARGS(__VA_ARGS__)); \
- } while (0)
+ #define PW_LOG_TOKENIZED_ENCODE_MESSAGE(metadata, format, ...) \
+ do { \
+ PW_TOKENIZE_FORMAT_STRING( \
+ PW_TOKENIZER_DEFAULT_DOMAIN, UINT32_MAX, format, __VA_ARGS__); \
+ EncodeTokenizedMessage(payload, \
+ _pw_tokenizer_token, \
+ PW_TOKENIZER_ARG_TYPES(__VA_ARGS__) \
+ PW_COMMA_ARGS(__VA_ARGS__)); \
+ } while (0)
In this example, the ``EncodeTokenizedMessage`` function would handle encoding
and processing the message. Encoding is done by the
-``pw::tokenizer::EncodedMessage`` class or ``pw::tokenizer::EncodeArgs``
-function from ``pw_tokenizer/encode_args.h``. The encoded message can then be
-transmitted or stored as needed.
+:cpp:class:`pw::tokenizer::EncodedMessage` class or
+:cpp:func:`pw::tokenizer::EncodeArgs` function from
+``pw_tokenizer/encode_args.h``. The encoded message can then be transmitted or
+stored as needed.
.. code-block:: cpp
- #include "pw_log_tokenized/log_tokenized.h"
- #include "pw_tokenizer/encode_args.h"
-
- void HandleTokenizedMessage(pw::log_tokenized::Metadata metadata,
- std::span<std::byte> message);
+ #include "pw_log_tokenized/log_tokenized.h"
+ #include "pw_tokenizer/encode_args.h"
- extern "C" void EncodeTokenizedMessage(const pw_tokenizer_Payload metadata,
- const pw_tokenizer_Token token,
- const pw_tokenizer_ArgTypes types,
- ...) {
- va_list args;
- va_start(args, types);
- pw::tokenizer::EncodedMessage encoded_message(token, types, args);
- va_end(args);
+ void HandleTokenizedMessage(pw::log_tokenized::Metadata metadata,
+ pw::span<std::byte> message);
- HandleTokenizedMessage(metadata, encoded_message);
- }
+ extern "C" void EncodeTokenizedMessage(const uint32_t metadata,
+ const pw_tokenizer_Token token,
+ const pw_tokenizer_ArgTypes types,
+ ...) {
+ va_list args;
+ va_start(args, types);
+ pw::tokenizer::EncodedMessage<> encoded_message(token, types, args);
+ va_end(args);
-.. admonition:: When to use a custom macro
+ HandleTokenizedMessage(metadata, encoded_message);
+ }
- Use existing tokenization macros whenever possible. A custom macro may be
- needed to support use cases like the following:
+.. admonition:: Why use a custom macro
- * Variations of ``PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD`` that take
- different arguments.
- * Supporting global handler macros that use different handler functions.
-
-Binary logging with pw_tokenizer
---------------------------------
-String tokenization is perfect for logging. Consider the following log macro,
-which gathers the file, line number, and log message. It calls the ``RecordLog``
-function, which formats the log string, collects a timestamp, and transmits the
-result.
+ - Optimal code size. Invoking a free function with the tokenized data results
+ in the smallest possible call site.
+ - Pass additional arguments, such as metadata, with the tokenized message.
+ - Integrate ``pw_tokenizer`` with other systems.
-.. code-block:: cpp
-
- #define LOG_INFO(format, ...) \
- RecordLog(LogLevel_INFO, __FILE_NAME__, __LINE__, format, ##__VA_ARGS__)
-
- void RecordLog(LogLevel level, const char* file, int line, const char* format,
- ...) {
- if (level < current_log_level) {
- return;
- }
+Tokenize a message with arguments to a buffer
+---------------------------------------------
+.. doxygendefine:: PW_TOKENIZE_TO_BUFFER
+.. doxygendefine:: PW_TOKENIZE_TO_BUFFER_DOMAIN
+.. doxygendefine:: PW_TOKENIZE_TO_BUFFER_MASK
- int bytes = snprintf(buffer, sizeof(buffer), "%s:%d ", file, line);
+.. admonition:: Why use this macro
- va_list args;
- va_start(args, format);
- bytes += vsnprintf(&buffer[bytes], sizeof(buffer) - bytes, format, args);
- va_end(args);
+ - Encode a tokenized message for consumption within a function.
+ - Encode a tokenized message into an existing buffer.
- TransmitLog(TimeSinceBootMillis(), buffer, size);
- }
+ Avoid using ``PW_TOKENIZE_TO_BUFFER`` in widely expanded macros, such as a
+ logging macro, because it will result in larger code size than passing the
+ tokenized data to a function.
-It is trivial to convert this to a binary log using the tokenizer. The
-``RecordLog`` call is replaced with a
-``PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD`` invocation. The
-``pw_tokenizer_HandleEncodedMessageWithPayload`` implementation collects the
-timestamp and transmits the message with ``TransmitLog``.
-
-.. code-block:: cpp
-
- #define LOG_INFO(format, ...) \
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD( \
- (pw_tokenizer_Payload)LogLevel_INFO, \
- __FILE_NAME__ ":%d " format, \
- __LINE__, \
- __VA_ARGS__); \
-
- extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- uintptr_t level, const uint8_t encoded_message[], size_t size_bytes) {
- if (static_cast<LogLevel>(level) >= current_log_level) {
- TransmitLog(TimeSinceBootMillis(), encoded_message, size_bytes);
- }
- }
-
-Note that the ``__FILE_NAME__`` string is directly included in the log format
-string. Since the string is tokenized, this has no effect on binary size. A
-``%d`` for the line number is added to the format string, so that changing the
-line of the log message does not generate a new token. There is no overhead for
-additional tokens, but it may not be desirable to fill a token database with
-duplicate log lines.
+Binary logging with pw_tokenizer
+================================
+String tokenization can be used to convert plain text logs to a compact,
+efficient binary format. See :ref:`module-pw_log_tokenized`.
Tokenizing function names
--------------------------
+=========================
The string literal tokenization functions support tokenizing string literals or
constexpr character arrays (``constexpr const char[]``). In GCC and Clang, the
special ``__func__`` variable and ``__PRETTY_FUNCTION__`` extension are declared
@@ -390,13 +372,9 @@ tokenized while compiling C++ with GCC or Clang.
.. code-block:: cpp
- // Tokenize the special function name variables.
- constexpr uint32_t function = PW_TOKENIZE_STRING(__func__);
- constexpr uint32_t pretty_function = PW_TOKENIZE_STRING(__PRETTY_FUNCTION__);
-
- // Tokenize the function name variables to a handler function.
- PW_TOKENIZE_TO_GLOBAL_HANDLER(__func__)
- PW_TOKENIZE_TO_GLOBAL_HANDLER(__PRETTY_FUNCTION__)
+ // Tokenize the special function name variables.
+ constexpr uint32_t function = PW_TOKENIZE_STRING(__func__);
+ constexpr uint32_t pretty_function = PW_TOKENIZE_STRING(__PRETTY_FUNCTION__);
Note that ``__func__`` and ``__PRETTY_FUNCTION__`` are not string literals.
They are defined as static character arrays, so they cannot be implicitly
@@ -404,7 +382,7 @@ concatentated with string literals. For example, ``printf(__func__ ": %d",
123);`` will not compile.
Tokenization in Python
-----------------------
+======================
The Python ``pw_tokenizer.encode`` module has limited support for encoding
tokenized messages with the ``encode_token_and_args`` function.
@@ -421,14 +399,14 @@ tokenized strings in a binary cannot be embedded as parsable pw_tokenizer
entries.
.. note::
- In C, the hash length of a string has a fixed limit controlled by
- ``PW_TOKENIZER_CFG_C_HASH_LENGTH``. To match tokens produced by C (as opposed
- to C++) code, ``pw_tokenizer_65599_hash()`` should be called with a matching
- hash length limit. When creating an offline database, it's a good idea to
- generate tokens for both, and merge the databases.
+ In C, the hash length of a string has a fixed limit controlled by
+ ``PW_TOKENIZER_CFG_C_HASH_LENGTH``. To match tokens produced by C (as opposed
+ to C++) code, ``pw_tokenizer_65599_hash()`` should be called with a matching
+ hash length limit. When creating an offline database, it's a good idea to
+ generate tokens for both, and merge the databases.
Encoding
---------
+========
The token is a 32-bit hash calculated during compilation. The string is encoded
little-endian with the token followed by arguments, if any. For example, the
31-byte string ``You can go about your business.`` hashes to 0xdac9a244.
@@ -436,24 +414,49 @@ This is encoded as 4 bytes: ``44 a2 c9 da``.
Arguments are encoded as follows:
- * **Integers** (1--10 bytes) --
- `ZagZag and varint encoded <https://developers.google.com/protocol-buffers/docs/encoding#signed-integers>`_,
- similarly to Protocol Buffers. Smaller values take fewer bytes.
- * **Floating point numbers** (4 bytes) -- Single precision floating point.
- * **Strings** (1--128 bytes) -- Length byte followed by the string contents.
- The top bit of the length whether the string was truncated or
- not. The remaining 7 bits encode the string length, with a maximum of 127
- bytes.
+* **Integers** (1--10 bytes) --
+ `ZagZag and varint encoded <https://developers.google.com/protocol-buffers/docs/encoding#signed-integers>`_,
+ similarly to Protocol Buffers. Smaller values take fewer bytes.
+* **Floating point numbers** (4 bytes) -- Single precision floating point.
+* **Strings** (1--128 bytes) -- Length byte followed by the string contents.
+ The top bit of the length whether the string was truncated or not. The
+ remaining 7 bits encode the string length, with a maximum of 127 bytes.
-.. TODO: insert diagram here!
+.. TODO(hepler): insert diagram here!
.. tip::
- ``%s`` arguments can quickly fill a tokenization buffer. Keep ``%s`` arguments
- short or avoid encoding them as strings (e.g. encode an enum as an integer
- instead of a string). See also `Tokenized strings as %s arguments`_.
+ ``%s`` arguments can quickly fill a tokenization buffer. Keep ``%s``
+ arguments short or avoid encoding them as strings (e.g. encode an enum as an
+ integer instead of a string). See also `Tokenized strings as %s arguments`_.
+
+Buffer sizing helper
+--------------------
+.. doxygenfunction:: pw::tokenizer::MinEncodingBufferSizeBytes
+
+Encoding command line utility
+-----------------------------
+The ``pw_tokenizer.encode`` command line tool can be used to encode tokenized
+strings.
+
+.. code-block:: bash
+
+ python -m pw_tokenizer.encode [-h] FORMAT_STRING [ARG ...]
+
+Example:
+
+.. code-block:: text
+
+ $ python -m pw_tokenizer.encode "There's... %d many of %s!" 2 them
+ Raw input: "There's... %d many of %s!" % (2, 'them')
+ Formatted input: There's... 2 many of them!
+ Token: 0xb6ef8b2d
+ Encoded: b'-\x8b\xef\xb6\x04\x04them' (2d 8b ef b6 04 04 74 68 65 6d) [10 bytes]
+ Prefixed Base64: $LYvvtgQEdGhlbQ==
+
+See ``--help`` for full usage details.
Token generation: fixed length hashing at compile time
-------------------------------------------------------
+======================================================
String tokens are generated using a modified version of the x65599 hash used by
the SDBM project. All hashing is done at compile time.
@@ -472,7 +475,7 @@ calculated values will differ between C and C++ for strings longer than
.. _module-pw_tokenizer-domains:
Tokenization domains
---------------------
+====================
``pw_tokenizer`` supports having multiple tokenization domains. Domains are a
string label associated with each tokenized string. This allows projects to keep
tokens from different sources separate. Potential use cases include the
@@ -487,11 +490,11 @@ default domain is sufficient, so no additional configuration is required.
.. code-block:: cpp
- // Tokenizes this string to the default ("") domain.
- PW_TOKENIZE_STRING("Hello, world!");
+ // Tokenizes this string to the default ("") domain.
+ PW_TOKENIZE_STRING("Hello, world!");
- // Tokenizes this string to the "my_custom_domain" domain.
- PW_TOKENIZE_STRING_DOMAIN("my_custom_domain", "Hello, world!");
+ // Tokenizes this string to the "my_custom_domain" domain.
+ PW_TOKENIZE_STRING_DOMAIN("my_custom_domain", "Hello, world!");
The database and detokenization command line tools default to reading from the
default domain. The domain may be specified for ELF files by appending
@@ -500,13 +503,15 @@ example, the following reads strings in ``some_domain`` from ``my_image.elf``.
.. code-block:: sh
- ./database.py create --database my_db.csv path/to/my_image.elf#some_domain
+ ./database.py create --database my_db.csv path/to/my_image.elf#some_domain
See `Managing token databases`_ for information about the ``database.py``
command line tool.
+.. _module-pw_tokenizer-masks:
+
Smaller tokens with masking
----------------------------
+===========================
``pw_tokenizer`` uses 32-bit tokens. On 32-bit or 64-bit architectures, using
fewer than 32 bits does not improve runtime or code size efficiency. However,
when tokens are packed into data structures or stored in arrays, the size of the
@@ -524,15 +529,20 @@ existing value.
.. code-block:: cpp
- constexpr uint32_t token = PW_TOKENIZE_STRING_MASK("domain", 0xFFFF, "Pigweed!");
- uint32_t packed_word = (other_bits << 16) | token;
+ constexpr uint32_t token = PW_TOKENIZE_STRING_MASK("domain", 0xFFFF, "Pigweed!");
+ uint32_t packed_word = (other_bits << 16) | token;
Tokens are hashes, so tokens of any size have a collision risk. The fewer bits
used for tokens, the more likely two strings are to hash to the same token. See
`token collisions`_.
+Masked tokens without arguments may be encoded in fewer bytes. For example, the
+16-bit token ``0x1234`` may be encoded as two little-endian bytes (``34 12``)
+rather than four (``34 12 00 00``). The detokenizer tools zero-pad data smaller
+than four bytes. Tokens with arguments must always be encoded as four bytes.
+
Token collisions
-----------------
+================
Tokens are calculated with a hash function. It is possible for different
strings to hash to the same token. When this happens, multiple strings will have
the same token in the database, and it may not be possible to unambiguously
@@ -541,34 +551,34 @@ decode a token.
The detokenization tools attempt to resolve collisions automatically. Collisions
are resolved based on two things:
- - whether the tokenized data matches the strings arguments' (if any), and
- - if / when the string was marked as having been removed from the database.
+- whether the tokenized data matches the strings arguments' (if any), and
+- if / when the string was marked as having been removed from the database.
Working with collisions
-^^^^^^^^^^^^^^^^^^^^^^^
+-----------------------
Collisions may occur occasionally. Run the command
``python -m pw_tokenizer.database report <database>`` to see information about a
token database, including any collisions.
If there are collisions, take the following steps to resolve them.
- - Change one of the colliding strings slightly to give it a new token.
- - In C (not C++), artificial collisions may occur if strings longer than
- ``PW_TOKENIZER_CFG_C_HASH_LENGTH`` are hashed. If this is happening,
- consider setting ``PW_TOKENIZER_CFG_C_HASH_LENGTH`` to a larger value.
- See ``pw_tokenizer/public/pw_tokenizer/config.h``.
- - Run the ``mark_removed`` command with the latest version of the build
- artifacts to mark missing strings as removed. This deprioritizes them in
- collision resolution.
+- Change one of the colliding strings slightly to give it a new token.
+- In C (not C++), artificial collisions may occur if strings longer than
+ ``PW_TOKENIZER_CFG_C_HASH_LENGTH`` are hashed. If this is happening, consider
+ setting ``PW_TOKENIZER_CFG_C_HASH_LENGTH`` to a larger value. See
+ ``pw_tokenizer/public/pw_tokenizer/config.h``.
+- Run the ``mark_removed`` command with the latest version of the build
+ artifacts to mark missing strings as removed. This deprioritizes them in
+ collision resolution.
- .. code-block:: sh
+ .. code-block:: sh
- python -m pw_tokenizer.database mark_removed --database <database> <ELF files>
+ python -m pw_tokenizer.database mark_removed --database <database> <ELF files>
- The ``purge`` command may be used to delete these tokens from the database.
+ The ``purge`` command may be used to delete these tokens from the database.
Probability of collisions
-^^^^^^^^^^^^^^^^^^^^^^^^^
+-------------------------
Hashes of any size have a collision risk. The probability of one at least
one collision occurring for a given number of strings is unintuitively high
(this is known as the `birthday problem
@@ -600,15 +610,22 @@ masking`_). 16 bits might be acceptable when tokenizing a small set of strings,
such as module names, but won't be suitable for large sets of strings, like log
messages.
+---------------
Token databases
-===============
+---------------
Token databases store a mapping of tokens to the strings they represent. An ELF
file can be used as a token database, but it only contains the strings for its
exact build. A token database file aggregates tokens from multiple ELF files, so
that a single database can decode tokenized strings from any known ELF.
Token databases contain the token, removal date (if any), and string for each
-tokenized string. Two token database formats are supported: CSV and binary.
+tokenized string.
+
+Token database formats
+======================
+Three token database formats are supported: CSV, binary, and directory. Tokens
+may also be read from ELF files or ``.a`` archives, but cannot be written to
+these formats.
CSV database format
-------------------
@@ -621,12 +638,12 @@ This example database contains six strings, three of which have removal dates.
.. code-block::
- 141c35d5, ,"The answer: ""%s"""
- 2e668cd6,2019-12-25,"Jello, world!"
- 7b940e2a, ,"Hello %s! %hd %e"
- 851beeb6, ,"%u %d"
- 881436a0,2020-01-01,"The answer is: %s"
- e13b0f94,2020-04-01,"%llu"
+ 141c35d5, ,"The answer: ""%s"""
+ 2e668cd6,2019-12-25,"Jello, world!"
+ 7b940e2a, ,"Hello %s! %hd %e"
+ 851beeb6, ,"%u %d"
+ 881436a0,2020-01-01,"The answer is: %s"
+ e13b0f94,2020-04-01,"%llu"
Binary database format
----------------------
@@ -643,28 +660,55 @@ compared with the CSV database's 211 B.
.. code-block:: text
- [header]
- 0x00: 454b4f54 0000534e TOKENS..
- 0x08: 00000006 00000000 ........
+ [header]
+ 0x00: 454b4f54 0000534e TOKENS..
+ 0x08: 00000006 00000000 ........
+
+ [entries]
+ 0x10: 141c35d5 ffffffff .5......
+ 0x18: 2e668cd6 07e30c19 ..f.....
+ 0x20: 7b940e2a ffffffff *..{....
+ 0x28: 851beeb6 ffffffff ........
+ 0x30: 881436a0 07e40101 .6......
+ 0x38: e13b0f94 07e40401 ..;.....
+
+ [string table]
+ 0x40: 54 68 65 20 61 6e 73 77 65 72 3a 20 22 25 73 22 The answer: "%s"
+ 0x50: 00 4a 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 00 48 .Jello, world!.H
+ 0x60: 65 6c 6c 6f 20 25 73 21 20 25 68 64 20 25 65 00 ello %s! %hd %e.
+ 0x70: 25 75 20 25 64 00 54 68 65 20 61 6e 73 77 65 72 %u %d.The answer
+ 0x80: 20 69 73 3a 20 25 73 00 25 6c 6c 75 00 is: %s.%llu.
+
+Directory database format
+-------------------------
+pw_tokenizer can consume directories of CSV databases. A directory database
+will be searched recursively for files with a `.pw_tokenizer.csv` suffix, all
+of which will be used for subsequent detokenization lookups.
+
+An example directory database might look something like this:
+
+.. code-block:: text
- [entries]
- 0x10: 141c35d5 ffffffff .5......
- 0x18: 2e668cd6 07e30c19 ..f.....
- 0x20: 7b940e2a ffffffff *..{....
- 0x28: 851beeb6 ffffffff ........
- 0x30: 881436a0 07e40101 .6......
- 0x38: e13b0f94 07e40401 ..;.....
+ token_database
+ ├── chuck_e_cheese.pw_tokenizer.csv
+ ├── fungi_ble.pw_tokenizer.csv
+ └── some_more
+ └── arcade.pw_tokenizer.csv
- [string table]
- 0x40: 54 68 65 20 61 6e 73 77 65 72 3a 20 22 25 73 22 The answer: "%s"
- 0x50: 00 4a 65 6c 6c 6f 2c 20 77 6f 72 6c 64 21 00 48 .Jello, world!.H
- 0x60: 65 6c 6c 6f 20 25 73 21 20 25 68 64 20 25 65 00 ello %s! %hd %e.
- 0x70: 25 75 20 25 64 00 54 68 65 20 61 6e 73 77 65 72 %u %d.The answer
- 0x80: 20 69 73 3a 20 25 73 00 25 6c 6c 75 00 is: %s.%llu.
+This format is optimized for storage in a Git repository alongside source code.
+The token database commands randomly generate unique file names for the CSVs in
+the database to prevent merge conflicts. Running ``mark_removed`` or ``purge``
+commands in the database CLI consolidates the files to a single CSV.
+The database command line tool supports a ``--discard-temporary
+<upstream_commit>`` option for ``add``. In this mode, the tool attempts to
+discard temporary tokens. It identifies the latest CSV not present in the
+provided ``<upstream_commit>``, and tokens present that CSV that are not in the
+newly added tokens are discarded. This helps keep temporary tokens (e.g from
+debug logs) out of the database.
JSON support
-------------
+============
While pw_tokenizer doesn't specify a JSON database format, a token database can
be created from a JSON formatted array of strings. This is useful for side-band
token database generation for strings that are not embedded as parsable tokens
@@ -672,7 +716,7 @@ in compiled binaries. See :ref:`module-pw_tokenizer-database-creation` for
instructions on generating a token database from a JSON file.
Managing token databases
-------------------------
+========================
Token databases are managed with the ``database.py`` script. This script can be
used to extract tokens from compilation artifacts and manage database files.
Invoke ``database.py`` with ``-h`` for full usage information.
@@ -684,14 +728,14 @@ file to experiment with the ``database.py`` commands.
.. _module-pw_tokenizer-database-creation:
Create a database
-^^^^^^^^^^^^^^^^^
+-----------------
The ``create`` command makes a new token database from ELF files (.elf, .o, .so,
etc.), archives (.a), existing token databases (CSV or binary), or a JSON file
containing an array of strings.
.. code-block:: sh
- ./database.py create --database DATABASE_NAME ELF_OR_DATABASE_FILE...
+ ./database.py create --database DATABASE_NAME ELF_OR_DATABASE_FILE...
Two database output formats are supported: CSV and binary. Provide
``--type binary`` to ``create`` to generate a binary database instead of the
@@ -700,20 +744,24 @@ human review. Binary databases are more compact and simpler to parse. The C++
detokenizer library only supports binary databases currently.
Update a database
-^^^^^^^^^^^^^^^^^
+-----------------
As new tokenized strings are added, update the database with the ``add``
command.
.. code-block:: sh
- ./database.py add --database DATABASE_NAME ELF_OR_DATABASE_FILE...
+ ./database.py add --database DATABASE_NAME ELF_OR_DATABASE_FILE...
+
+This command adds new tokens from ELF files or other databases to the database.
+Adding tokens already present in the database updates the date removed, if any,
+to the latest.
A CSV token database can be checked into a source repository and updated as code
changes are made. The build system can invoke ``database.py`` to update the
database after each build.
GN integration
-^^^^^^^^^^^^^^
+--------------
Token databases may be updated or created as part of a GN build. The
``pw_tokenizer_database`` template provided by
``$dir_pw_tokenizer/database.gni`` automatically updates an in-source tokenized
@@ -727,39 +775,40 @@ the ``database`` variable.
.. code-block::
- import("//build_overrides/pigweed.gni")
+ import("//build_overrides/pigweed.gni")
- import("$dir_pw_tokenizer/database.gni")
+ import("$dir_pw_tokenizer/database.gni")
- pw_tokenizer_database("my_database") {
- database = "database_in_the_source_tree.csv"
- targets = [ "//firmware/image:foo(//targets/my_board:some_toolchain)" ]
- input_databases = [ "other_database.csv" ]
- }
+ pw_tokenizer_database("my_database") {
+ database = "database_in_the_source_tree.csv"
+ targets = [ "//firmware/image:foo(//targets/my_board:some_toolchain)" ]
+ input_databases = [ "other_database.csv" ]
+ }
Instead of specifying GN targets, paths or globs to output files may be provided
with the ``paths`` option.
.. code-block::
- pw_tokenizer_database("my_database") {
- database = "database_in_the_source_tree.csv"
- deps = [ ":apps" ]
- optional_paths = [ "$root_build_dir/**/*.elf" ]
- }
+ pw_tokenizer_database("my_database") {
+ database = "database_in_the_source_tree.csv"
+ deps = [ ":apps" ]
+ optional_paths = [ "$root_build_dir/**/*.elf" ]
+ }
.. note::
- The ``paths`` and ``optional_targets`` arguments do not add anything to
- ``deps``, so there is no guarantee that the referenced artifacts will exist
- when the database is updated. Provide ``targets`` or ``deps`` or build other
- GN targets first if this is a concern.
+ The ``paths`` and ``optional_targets`` arguments do not add anything to
+ ``deps``, so there is no guarantee that the referenced artifacts will exist
+ when the database is updated. Provide ``targets`` or ``deps`` or build other
+ GN targets first if this is a concern.
+--------------
Detokenization
-==============
+--------------
Detokenization is the process of expanding a token to the string it represents
-and decoding its arguments. This module provides Python and C++ detokenization
-libraries.
+and decoding its arguments. This module provides Python, C++ and TypeScript
+detokenization libraries.
**Example: decoding tokenized logs**
@@ -768,57 +817,59 @@ the following log file, which has four tokenized logs and one plain text log:
.. code-block:: text
- 20200229 14:38:58 INF $HL2VHA==
- 20200229 14:39:00 DBG $5IhTKg==
- 20200229 14:39:20 DBG Crunching numbers to calculate probability of success
- 20200229 14:39:21 INF $EgFj8lVVAUI=
- 20200229 14:39:23 ERR $DFRDNwlOT1RfUkVBRFk=
+ 20200229 14:38:58 INF $HL2VHA==
+ 20200229 14:39:00 DBG $5IhTKg==
+ 20200229 14:39:20 DBG Crunching numbers to calculate probability of success
+ 20200229 14:39:21 INF $EgFj8lVVAUI=
+ 20200229 14:39:23 ERR $DFRDNwlOT1RfUkVBRFk=
The project's log strings are stored in a database like the following:
.. code-block::
- 1c95bd1c, ,"Initiating retrieval process for recovery object"
- 2a5388e4, ,"Determining optimal approach and coordinating vectors"
- 3743540c, ,"Recovery object retrieval failed with status %s"
- f2630112, ,"Calculated acceptable probability of success (%.2f%%)"
+ 1c95bd1c, ,"Initiating retrieval process for recovery object"
+ 2a5388e4, ,"Determining optimal approach and coordinating vectors"
+ 3743540c, ,"Recovery object retrieval failed with status %s"
+ f2630112, ,"Calculated acceptable probability of success (%.2f%%)"
Using the detokenizing tools with the database, the logs can be decoded:
.. code-block:: text
- 20200229 14:38:58 INF Initiating retrieval process for recovery object
- 20200229 14:39:00 DBG Determining optimal algorithm and coordinating approach vectors
- 20200229 14:39:20 DBG Crunching numbers to calculate probability of success
- 20200229 14:39:21 INF Calculated acceptable probability of success (32.33%)
- 20200229 14:39:23 ERR Recovery object retrieval failed with status NOT_READY
+ 20200229 14:38:58 INF Initiating retrieval process for recovery object
+ 20200229 14:39:00 DBG Determining optimal algorithm and coordinating approach vectors
+ 20200229 14:39:20 DBG Crunching numbers to calculate probability of success
+ 20200229 14:39:21 INF Calculated acceptable probability of success (32.33%)
+ 20200229 14:39:23 ERR Recovery object retrieval failed with status NOT_READY
.. note::
- This example uses the `Base64 format`_, which occupies about 4/3 (133%) as
- much space as the default binary format when encoded. For projects that wish
- to interleave tokenized with plain text, using Base64 is a worthwhile
- tradeoff.
+ This example uses the `Base64 format`_, which occupies about 4/3 (133%) as
+ much space as the default binary format when encoded. For projects that wish
+ to interleave tokenized with plain text, using Base64 is a worthwhile
+ tradeoff.
Python
-------
+======
To detokenize in Python, import ``Detokenizer`` from the ``pw_tokenizer``
package, and instantiate it with paths to token databases or ELF files.
.. code-block:: python
- import pw_tokenizer
+ import pw_tokenizer
- detokenizer = pw_tokenizer.Detokenizer('path/to/database.csv', 'other/path.elf')
+ detokenizer = pw_tokenizer.Detokenizer('path/to/database.csv', 'other/path.elf')
- def process_log_message(log_message):
- result = detokenizer.detokenize(log_message.payload)
- self._log(str(result))
+ def process_log_message(log_message):
+ result = detokenizer.detokenize(log_message.payload)
+ self._log(str(result))
The ``pw_tokenizer`` package also provides the ``AutoUpdatingDetokenizer``
class, which can be used in place of the standard ``Detokenizer``. This class
monitors database files for changes and automatically reloads them when they
-change. This is helpful for long-running tools that use detokenization.
+change. This is helpful for long-running tools that use detokenization. The
+class also supports token domains for the given database files in the
+``<path>#<domain>`` format.
For messages that are optionally tokenized and may be encoded as binary,
Base64, or plaintext UTF-8, use
@@ -827,8 +878,238 @@ determine the correct method to detokenize and always provide a printable
string. For more information on this feature, see
:ref:`module-pw_tokenizer-proto`.
+C99 ``printf`` Compatibility Notes
+----------------------------------
+This implementation is designed to align with the
+`C99 specification, section 7.19.6
+<https://www.dii.uchile.cl/~daespino/files/Iso_C_1999_definition.pdf>`_.
+Notably, this specification is slightly different than what is implemented
+in most compilers due to each compiler choosing to interpret undefined
+behavior in slightly different ways. Treat the following description as the
+source of truth.
+
+This implementation supports:
+
+- Overall Format: ``%[flags][width][.precision][length][specifier]``
+- Flags (Zero or More)
+ - ``-``: Left-justify within the given field width; Right justification is
+ the default (see Width modifier).
+ - ``+``: Forces to preceed the result with a plus or minus sign (``+`` or
+ ``-``) even for positive numbers. By default, only negative numbers are
+ preceded with a ``-`` sign.
+ - (space): If no sign is going to be written, a blank space is inserted
+ before the value.
+ - ``#``: Specifies an alternative print syntax should be used.
+ - Used with ``o``, ``x`` or ``X`` specifiers the value is preceeded with
+ ``0``, ``0x`` or ``0X``, respectively, for values different than zero.
+ - Used with ``a``, ``A``, ``e``, ``E``, ``f``, ``F``, ``g``, or ``G`` it
+ forces the written output to contain a decimal point even if no more
+ digits follow. By default, if no digits follow, no decimal point is
+ written.
+ - ``0``: Left-pads the number with zeroes (``0``) instead of spaces when
+ padding is specified (see width sub-specifier).
+- Width (Optional)
+ - ``(number)``: Minimum number of characters to be printed. If the value to
+ be printed is shorter than this number, the result is padded with blank
+ spaces or ``0`` if the ``0`` flag is present. The value is not truncated
+ even if the result is larger. If the value is negative and the ``0`` flag
+ is present, the ``0``\s are padded after the ``-`` symbol.
+ - ``*``: The width is not specified in the format string, but as an
+ additional integer value argument preceding the argument that has to be
+ formatted.
+- Precision (Optional)
+ - ``.(number)``
+ - For ``d``, ``i``, ``o``, ``u``, ``x``, ``X``, specifies the minimum
+ number of digits to be written. If the value to be written is shorter
+ than this number, the result is padded with leading zeros. The value is
+ not truncated even if the result is longer.
+
+ - A precision of ``0`` means that no character is written for the value
+ ``0``.
+
+ - For ``a``, ``A``, ``e``, ``E``, ``f``, and ``F``, specifies the number
+ of digits to be printed after the decimal point. By default, this is
+ ``6``.
+
+ - For ``g`` and ``G``, specifies the maximum number of significant digits
+ to be printed.
+
+ - For ``s``, specifies the maximum number of characters to be printed. By
+ default all characters are printed until the ending null character is
+ encountered.
+
+ - If the period is specified without an explicit value for precision,
+ ``0`` is assumed.
+ - ``.*``: The precision is not specified in the format string, but as an
+ additional integer value argument preceding the argument that has to be
+ formatted.
+- Length (Optional)
+ - ``hh``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``signed char`` or ``unsigned char``.
+ However, this is largely ignored in the implementation due to it not being
+ necessary for Python or argument decoding (since the argument is always
+ encoded at least as a 32-bit integer).
+ - ``h``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``signed short int`` or
+ ``unsigned short int``. However, this is largely ignored in the
+ implementation due to it not being necessary for Python or argument
+ decoding (since the argument is always encoded at least as a 32-bit
+ integer).
+ - ``l``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``signed long int`` or
+ ``unsigned long int``. Also is usable with ``c`` and ``s`` to specify that
+ the arguments will be encoded with ``wchar_t`` values (which isn't
+ different from normal ``char`` values). However, this is largely ignored in
+ the implementation due to it not being necessary for Python or argument
+ decoding (since the argument is always encoded at least as a 32-bit
+ integer).
+ - ``ll``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``signed long long int`` or
+ ``unsigned long long int``. This is required to properly decode the
+ argument as a 64-bit integer.
+ - ``L``: Usable with ``a``, ``A``, ``e``, ``E``, ``f``, ``F``, ``g``, or
+ ``G`` conversion specifiers applies to a long double argument. However,
+ this is ignored in the implementation due to floating point value encoded
+ that is unaffected by bit width.
+ - ``j``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``intmax_t`` or ``uintmax_t``.
+ - ``z``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``size_t``. This will force the argument
+ to be decoded as an unsigned integer.
+ - ``t``: Usable with ``d``, ``i``, ``o``, ``u``, ``x``, or ``X`` specifiers
+ to convey the argument will be a ``ptrdiff_t``.
+ - If a length modifier is provided for an incorrect specifier, it is ignored.
+- Specifier (Required)
+ - ``d`` / ``i``: Used for signed decimal integers.
+
+ - ``u``: Used for unsigned decimal integers.
+
+ - ``o``: Used for unsigned decimal integers and specifies formatting should
+ be as an octal number.
+
+ - ``x``: Used for unsigned decimal integers and specifies formatting should
+ be as a hexadecimal number using all lowercase letters.
+
+ - ``X``: Used for unsigned decimal integers and specifies formatting should
+ be as a hexadecimal number using all uppercase letters.
+
+ - ``f``: Used for floating-point values and specifies to use lowercase,
+ decimal floating point formatting.
+
+ - Default precision is ``6`` decimal places unless explicitly specified.
+
+ - ``F``: Used for floating-point values and specifies to use uppercase,
+ decimal floating point formatting.
+
+ - Default precision is ``6`` decimal places unless explicitly specified.
+
+ - ``e``: Used for floating-point values and specifies to use lowercase,
+ exponential (scientific) formatting.
+
+ - Default precision is ``6`` decimal places unless explicitly specified.
+
+ - ``E``: Used for floating-point values and specifies to use uppercase,
+ exponential (scientific) formatting.
+
+ - Default precision is ``6`` decimal places unless explicitly specified.
+
+ - ``g``: Used for floating-point values and specified to use ``f`` or ``e``
+ formatting depending on which would be the shortest representation.
+
+ - Precision specifies the number of significant digits, not just digits
+ after the decimal place.
+
+ - If the precision is specified as ``0``, it is interpreted to mean ``1``.
+
+ - ``e`` formatting is used if the the exponent would be less than ``-4`` or
+ is greater than or equal to the precision.
+
+ - Trailing zeros are removed unless the ``#`` flag is set.
+
+ - A decimal point only appears if it is followed by a digit.
+
+ - ``NaN`` or infinities always follow ``f`` formatting.
+
+ - ``G``: Used for floating-point values and specified to use ``f`` or ``e``
+ formatting depending on which would be the shortest representation.
+
+ - Precision specifies the number of significant digits, not just digits
+ after the decimal place.
+
+ - If the precision is specified as ``0``, it is interpreted to mean ``1``.
+
+ - ``E`` formatting is used if the the exponent would be less than ``-4`` or
+ is greater than or equal to the precision.
+
+ - Trailing zeros are removed unless the ``#`` flag is set.
+
+ - A decimal point only appears if it is followed by a digit.
+
+ - ``NaN`` or infinities always follow ``F`` formatting.
+
+ - ``c``: Used for formatting a ``char`` value.
+
+ - ``s``: Used for formatting a string of ``char`` values.
+
+ - If width is specified, the null terminator character is included as a
+ character for width count.
+
+ - If precision is specified, no more ``char``\s than that value will be
+ written from the string (padding is used to fill additional width).
+
+ - ``p``: Used for formatting a pointer address.
+
+ - ``%``: Prints a single ``%``. Only valid as ``%%`` (supports no flags,
+ width, precision, or length modifiers).
+
+Underspecified details:
+
+- If both ``+`` and (space) flags appear, the (space) is ignored.
+- The ``+`` and (space) flags will error if used with ``c`` or ``s``.
+- The ``#`` flag will error if used with ``d``, ``i``, ``u``, ``c``, ``s``, or
+ ``p``.
+- The ``0`` flag will error if used with ``c``, ``s``, or ``p``.
+- Both ``+`` and (space) can work with the unsigned integer specifiers ``u``,
+ ``o``, ``x``, and ``X``.
+- If a length modifier is provided for an incorrect specifier, it is ignored.
+- The ``z`` length modifier will decode arugments as signed as long as ``d`` or
+ ``i`` is used.
+- ``p`` is implementation defined.
+
+ - For this implementation, it will print with a ``0x`` prefix and then the
+ pointer value was printed using ``%08X``.
+
+ - ``p`` supports the ``+``, ``-``, and (space) flags, but not the ``#`` or
+ ``0`` flags.
+
+ - None of the length modifiers are usable with ``p``.
+
+ - This implementation will try to adhere to user-specified width (assuming the
+ width provided is larger than the guaranteed minimum of ``10``).
+
+ - Specifying precision for ``p`` is considered an error.
+- Only ``%%`` is allowed with no other modifiers. Things like ``%+%`` will fail
+ to decode. Some C stdlib implementations support any modifiers being
+ present between ``%``, but ignore any for the output.
+- If a width is specified with the ``0`` flag for a negative value, the padded
+ ``0``\s will appear after the ``-`` symbol.
+- A precision of ``0`` for ``d``, ``i``, ``u``, ``o``, ``x``, or ``X`` means
+ that no character is written for the value ``0``.
+- Precision cannot be specified for ``c``.
+- Using ``*`` or fixed precision with the ``s`` specifier still requires the
+ string argument to be null-terminated. This is due to argument encoding
+ happening on the C/C++-side while the precision value is not read or
+ otherwise used until decoding happens in this Python code.
+
+Non-conformant details:
+
+- ``n`` specifier: We do not support the ``n`` specifier since it is impossible
+ for us to retroactively tell the original program how many characters have
+ been printed since this decoding happens a great deal of time after the
+ device sent it, usually on a separate processing device entirely.
+
C++
----
+===
The C++ detokenization libraries can be used in C++ or any language that can
call into C++ with a C-linkage wrapper, such as Java or Rust. A reference
Java Native Interface (JNI) implementation is provided.
@@ -840,11 +1121,11 @@ file or include it in the source code. Pass the database array to
.. code-block:: cpp
- Detokenizer detokenizer(TokenDatabase::Create(token_database_array));
+ Detokenizer detokenizer(TokenDatabase::Create(token_database_array));
- std::string ProcessLog(span<uint8_t> log_data) {
- return detokenizer.Detokenize(log_data).BestString();
- }
+ std::string ProcessLog(span<uint8_t> log_data) {
+ return detokenizer.Detokenize(log_data).BestString();
+ }
The ``TokenDatabase`` class verifies that its data is valid before using it. If
it is invalid, the ``TokenDatabase::Create`` returns an empty database for which
@@ -853,35 +1134,60 @@ this check can be done at compile time.
.. code-block:: cpp
- // This line fails to compile with a static_assert if the database is invalid.
- constexpr TokenDatabase kDefaultDatabase = TokenDatabase::Create<kData>();
+ // This line fails to compile with a static_assert if the database is invalid.
+ constexpr TokenDatabase kDefaultDatabase = TokenDatabase::Create<kData>();
- Detokenizer OpenDatabase(std::string_view path) {
- std::vector<uint8_t> data = ReadWholeFile(path);
+ Detokenizer OpenDatabase(std::string_view path) {
+ std::vector<uint8_t> data = ReadWholeFile(path);
- TokenDatabase database = TokenDatabase::Create(data);
+ TokenDatabase database = TokenDatabase::Create(data);
- // This checks if the file contained a valid database. It is safe to use a
- // TokenDatabase that failed to load (it will be empty), but it may be
- // desirable to provide a default database or otherwise handle the error.
- if (database.ok()) {
- return Detokenizer(database);
- }
- return Detokenizer(kDefaultDatabase);
- }
+ // This checks if the file contained a valid database. It is safe to use a
+ // TokenDatabase that failed to load (it will be empty), but it may be
+ // desirable to provide a default database or otherwise handle the error.
+ if (database.ok()) {
+ return Detokenizer(database);
+ }
+ return Detokenizer(kDefaultDatabase);
+ }
+
+
+TypeScript
+==========
+To detokenize in TypeScript, import ``Detokenizer`` from the ``pigweedjs``
+package, and instantiate it with a CSV token database.
+
+.. code-block:: typescript
+
+ import { pw_tokenizer, pw_hdlc } from 'pigweedjs';
+ const { Detokenizer } = pw_tokenizer;
+ const { Frame } = pw_hdlc;
+
+ const detokenizer = new Detokenizer(String(tokenCsv));
+
+ function processLog(frame: Frame){
+ const result = detokenizer.detokenize(frame);
+ console.log(result);
+ }
+
+For messages that are encoded in Base64, use ``Detokenizer::detokenizeBase64``.
+`detokenizeBase64` will also attempt to detokenize nested Base64 tokens. There
+is also `detokenizeUint8Array` that works just like `detokenize` but expects
+`Uint8Array` instead of a `Frame` argument.
Protocol buffers
-----------------
+================
``pw_tokenizer`` provides utilities for handling tokenized fields in protobufs.
See :ref:`module-pw_tokenizer-proto` for details.
.. toctree::
- :hidden:
+ :hidden:
- proto.rst
+ proto.rst
+-------------
Base64 format
-=============
+-------------
The tokenizer encodes messages to a compact binary representation. Applications
may desire a textual representation of tokenized strings. This makes it easy to
use tokenized messages alongside plain text messages, but comes at a small
@@ -895,67 +1201,55 @@ string's token is 0x4b016e66.
.. code-block:: text
- Source code: PW_TOKENIZE_TO_GLOBAL_HANDLER("This is an example: %d!", -1);
+ Source code: PW_LOG("This is an example: %d!", -1);
- Plain text: This is an example: -1! [23 bytes]
+ Plain text: This is an example: -1! [23 bytes]
- Binary: 66 6e 01 4b 01 [ 5 bytes]
+ Binary: 66 6e 01 4b 01 [ 5 bytes]
- Base64: $Zm4BSwE= [ 9 bytes]
+ Base64: $Zm4BSwE= [ 9 bytes]
Encoding
---------
+========
To encode with the Base64 format, add a call to
``pw::tokenizer::PrefixedBase64Encode`` or ``pw_tokenizer_PrefixedBase64Encode``
in the tokenizer handler function. For example,
.. code-block:: cpp
- void pw_tokenizer_HandleEncodedMessage(const uint8_t encoded_message[],
- size_t size_bytes) {
- char base64_buffer[64];
- size_t base64_size = pw::tokenizer::PrefixedBase64Encode(
- pw::span(encoded_message, size_bytes), base64_buffer);
+ void TokenizedMessageHandler(const uint8_t encoded_message[],
+ size_t size_bytes) {
+ pw::InlineBasicString base64 = pw::tokenizer::PrefixedBase64Encode(
+ pw::span(encoded_message, size_bytes));
- TransmitLogMessage(base64_buffer, base64_size);
- }
+ TransmitLogMessage(base64.data(), base64.size());
+ }
Decoding
---------
+========
The Python ``Detokenizer`` class supprts decoding and detokenizing prefixed
Base64 messages with ``detokenize_base64`` and related methods.
.. tip::
- The Python detokenization tools support recursive detokenization for prefixed
- Base64 text. Tokenized strings found in detokenized text are detokenized, so
- prefixed Base64 messages can be passed as ``%s`` arguments.
+ The Python detokenization tools support recursive detokenization for prefixed
+ Base64 text. Tokenized strings found in detokenized text are detokenized, so
+ prefixed Base64 messages can be passed as ``%s`` arguments.
- For example, the tokenized string for "Wow!" is ``$RhYjmQ==``. This could be
- passed as an argument to the printf-style string ``Nested message: %s``, which
- encodes to ``$pEVTYQkkUmhZam1RPT0=``. The detokenizer would decode the message
- as follows:
+ For example, the tokenized string for "Wow!" is ``$RhYjmQ==``. This could be
+ passed as an argument to the printf-style string ``Nested message: %s``, which
+ encodes to ``$pEVTYQkkUmhZam1RPT0=``. The detokenizer would decode the message
+ as follows:
- ::
+ ::
- "$pEVTYQkkUmhZam1RPT0=" → "Nested message: $RhYjmQ==" → "Nested message: Wow!"
+ "$pEVTYQkkUmhZam1RPT0=" → "Nested message: $RhYjmQ==" → "Nested message: Wow!"
Base64 decoding is supported in C++ or C with the
``pw::tokenizer::PrefixedBase64Decode`` or ``pw_tokenizer_PrefixedBase64Decode``
functions.
-.. code-block:: cpp
-
- void pw_tokenizer_HandleEncodedMessage(const uint8_t encoded_message[],
- size_t size_bytes) {
- char base64_buffer[64];
- size_t base64_size = pw::tokenizer::PrefixedBase64Encode(
- pw::span(encoded_message, size_bytes), base64_buffer);
-
- TransmitLogMessage(base64_buffer, base64_size);
- }
-
Investigating undecoded messages
---------------------------------
+================================
Tokenized messages cannot be decoded if the token is not recognized. The Python
package includes the ``parse_message`` tool, which parses tokenized Base64
messages without looking up the token in a database. This tool attempts to guess
@@ -971,29 +1265,29 @@ The tool is executed by passing Base64 tokenized messages, with or without the
see full usage information.
Example
-^^^^^^^
+-------
.. code-block::
- $ python -m pw_tokenizer.parse_message '$329JMwA=' koSl524TRkFJTEVEX1BSRUNPTkRJVElPTgJPSw== --specs %s %d
+ $ python -m pw_tokenizer.parse_message '$329JMwA=' koSl524TRkFJTEVEX1BSRUNPTkRJVElPTgJPSw== --specs %s %d
- INF Decoding arguments for '$329JMwA='
- INF Binary: b'\xdfoI3\x00' [df 6f 49 33 00] (5 bytes)
- INF Token: 0x33496fdf
- INF Args: b'\x00' [00] (1 bytes)
- INF Decoding with up to 8 %s or %d arguments
- INF Attempt 1: [%s]
- INF Attempt 2: [%d] 0
+ INF Decoding arguments for '$329JMwA='
+ INF Binary: b'\xdfoI3\x00' [df 6f 49 33 00] (5 bytes)
+ INF Token: 0x33496fdf
+ INF Args: b'\x00' [00] (1 bytes)
+ INF Decoding with up to 8 %s or %d arguments
+ INF Attempt 1: [%s]
+ INF Attempt 2: [%d] 0
- INF Decoding arguments for '$koSl524TRkFJTEVEX1BSRUNPTkRJVElPTgJPSw=='
- INF Binary: b'\x92\x84\xa5\xe7n\x13FAILED_PRECONDITION\x02OK' [92 84 a5 e7 6e 13 46 41 49 4c 45 44 5f 50 52 45 43 4f 4e 44 49 54 49 4f 4e 02 4f 4b] (28 bytes)
- INF Token: 0xe7a58492
- INF Args: b'n\x13FAILED_PRECONDITION\x02OK' [6e 13 46 41 49 4c 45 44 5f 50 52 45 43 4f 4e 44 49 54 49 4f 4e 02 4f 4b] (24 bytes)
- INF Decoding with up to 8 %s or %d arguments
- INF Attempt 1: [%d %s %d %d %d] 55 FAILED_PRECONDITION 1 -40 -38
- INF Attempt 2: [%d %s %s] 55 FAILED_PRECONDITION OK
+ INF Decoding arguments for '$koSl524TRkFJTEVEX1BSRUNPTkRJVElPTgJPSw=='
+ INF Binary: b'\x92\x84\xa5\xe7n\x13FAILED_PRECONDITION\x02OK' [92 84 a5 e7 6e 13 46 41 49 4c 45 44 5f 50 52 45 43 4f 4e 44 49 54 49 4f 4e 02 4f 4b] (28 bytes)
+ INF Token: 0xe7a58492
+ INF Args: b'n\x13FAILED_PRECONDITION\x02OK' [6e 13 46 41 49 4c 45 44 5f 50 52 45 43 4f 4e 44 49 54 49 4f 4e 02 4f 4b] (24 bytes)
+ INF Decoding with up to 8 %s or %d arguments
+ INF Attempt 1: [%d %s %d %d %d] 55 FAILED_PRECONDITION 1 -40 -38
+ INF Attempt 2: [%d %s %s] 55 FAILED_PRECONDITION OK
Command line utilities
-^^^^^^^^^^^^^^^^^^^^^^
+----------------------
``pw_tokenizer`` provides two standalone command line utilities for detokenizing
Base64-encoded tokenized strings.
@@ -1007,54 +1301,55 @@ as runnable modules. For example:
.. code-block::
- # Detokenize Base64-encoded strings in a file
- python -m pw_tokenizer.detokenize -i input_file.txt
+ # Detokenize Base64-encoded strings in a file
+ python -m pw_tokenizer.detokenize -i input_file.txt
- # Detokenize Base64-encoded strings in output from a serial device
- python -m pw_tokenizer.serial_detokenizer --device /dev/ttyACM0
+ # Detokenize Base64-encoded strings in output from a serial device
+ python -m pw_tokenizer.serial_detokenizer --device /dev/ttyACM0
See the ``--help`` options for these tools for full usage information.
+--------------------
Deployment war story
-====================
+--------------------
The tokenizer module was developed to bring tokenized logging to an
in-development product. The product already had an established text-based
logging system. Deploying tokenization was straightforward and had substantial
benefits.
Results
--------
- * Log contents shrunk by over 50%, even with Base64 encoding.
+=======
+* Log contents shrunk by over 50%, even with Base64 encoding.
- * Significant size savings for encoded logs, even using the less-efficient
- Base64 encoding required for compatibility with the existing log system.
- * Freed valuable communication bandwidth.
- * Allowed storing many more logs in crash dumps.
+ * Significant size savings for encoded logs, even using the less-efficient
+ Base64 encoding required for compatibility with the existing log system.
+ * Freed valuable communication bandwidth.
+ * Allowed storing many more logs in crash dumps.
- * Substantial flash savings.
+* Substantial flash savings.
- * Reduced the size firmware images by up to 18%.
+ * Reduced the size firmware images by up to 18%.
- * Simpler logging code.
+* Simpler logging code.
- * Removed CPU-heavy ``snprintf`` calls.
- * Removed complex code for forwarding log arguments to a low-priority task.
+ * Removed CPU-heavy ``snprintf`` calls.
+ * Removed complex code for forwarding log arguments to a low-priority task.
This section describes the tokenizer deployment process and highlights key
insights.
Firmware deployment
--------------------
- * In the project's logging macro, calls to the underlying logging function
- were replaced with a ``PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD``
- invocation.
- * The log level was passed as the payload argument to facilitate runtime log
- level control.
- * For this project, it was necessary to encode the log messages as text. In
- ``pw_tokenizer_HandleEncodedMessageWithPayload``, the log messages were
- encoded in the $-prefixed `Base64 format`_, then dispatched as normal log
- messages.
- * Asserts were tokenized using ``PW_TOKENIZE_TO_CALLBACK``.
+===================
+* In the project's logging macro, calls to the underlying logging function were
+ replaced with a tokenized log macro invocation.
+* The log level was passed as the payload argument to facilitate runtime log
+ level control.
+* For this project, it was necessary to encode the log messages as text. In
+ the handler function the log messages were encoded in the $-prefixed `Base64
+ format`_, then dispatched as normal log messages.
+* Asserts were tokenized a callback-based API that has been removed (a
+ :ref:`custom macro <module-pw_tokenizer-custom-macro>` is a better
+ alternative).
.. attention::
Do not encode line numbers in tokenized strings. This results in a huge
@@ -1064,78 +1359,114 @@ Firmware deployment
by adding ``"%d"`` to the format string and passing ``__LINE__``.
Database management
--------------------
- * The token database was stored as a CSV file in the project's Git repo.
- * The token database was automatically updated as part of the build, and
- developers were expected to check in the database changes alongside their
- code changes.
- * A presubmit check verified that all strings added by a change were added to
- the token database.
- * The token database included logs and asserts for all firmware images in the
- project.
- * No strings were purged from the token database.
+===================
+* The token database was stored as a CSV file in the project's Git repo.
+* The token database was automatically updated as part of the build, and
+ developers were expected to check in the database changes alongside their code
+ changes.
+* A presubmit check verified that all strings added by a change were added to
+ the token database.
+* The token database included logs and asserts for all firmware images in the
+ project.
+* No strings were purged from the token database.
.. tip::
- Merge conflicts may be a frequent occurrence with an in-source database. If
- the database is in-source, make sure there is a simple script to resolve any
- merge conflicts. The script could either keep both sets of lines or discard
- local changes and regenerate the database.
+ Merge conflicts may be a frequent occurrence with an in-source CSV database.
+ Use the `Directory database format`_ instead.
Decoding tooling deployment
----------------------------
- * The Python detokenizer in ``pw_tokenizer`` was deployed to two places:
+===========================
+* The Python detokenizer in ``pw_tokenizer`` was deployed to two places:
- * Product-specific Python command line tools, using
- ``pw_tokenizer.Detokenizer``.
- * Standalone script for decoding prefixed Base64 tokens in files or
- live output (e.g. from ``adb``), using ``detokenize.py``'s command line
- interface.
+ * Product-specific Python command line tools, using
+ ``pw_tokenizer.Detokenizer``.
+ * Standalone script for decoding prefixed Base64 tokens in files or
+ live output (e.g. from ``adb``), using ``detokenize.py``'s command line
+ interface.
- * The C++ detokenizer library was deployed to two Android apps with a Java
- Native Interface (JNI) layer.
+* The C++ detokenizer library was deployed to two Android apps with a Java
+ Native Interface (JNI) layer.
- * The binary token database was included as a raw resource in the APK.
- * In one app, the built-in token database could be overridden by copying a
- file to the phone.
+ * The binary token database was included as a raw resource in the APK.
+ * In one app, the built-in token database could be overridden by copying a
+ file to the phone.
.. tip::
- Make the tokenized logging tools simple to use for your project.
-
- * Provide simple wrapper shell scripts that fill in arguments for the
- project. For example, point ``detokenize.py`` to the project's token
- databases.
- * Use ``pw_tokenizer.AutoUpdatingDetokenizer`` to decode in
- continuously-running tools, so that users don't have to restart the tool
- when the token database updates.
- * Integrate detokenization everywhere it is needed. Integrating the tools
- takes just a few lines of code, and token databases can be embedded in
- APKs or binaries.
+ Make the tokenized logging tools simple to use for your project.
+
+ * Provide simple wrapper shell scripts that fill in arguments for the
+ project. For example, point ``detokenize.py`` to the project's token
+ databases.
+ * Use ``pw_tokenizer.AutoUpdatingDetokenizer`` to decode in
+ continuously-running tools, so that users don't have to restart the tool
+ when the token database updates.
+ * Integrate detokenization everywhere it is needed. Integrating the tools
+ takes just a few lines of code, and token databases can be embedded in APKs
+ or binaries.
+---------------------------
Limitations and future work
-===========================
+---------------------------
GCC bug: tokenization in template functions
--------------------------------------------
-GCC incorrectly ignores the section attribute for template
-`functions <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70435>`_ and
-`variables <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88061>`_. Due to this
-bug, tokenized strings in template functions may be emitted into ``.rodata``
-instead of the special tokenized string section. This causes two problems:
+===========================================
+GCC incorrectly ignores the section attribute for template `functions
+<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=70435>`_ and `variables
+<https://gcc.gnu.org/bugzilla/show_bug.cgi?id=88061>`_. For example, the
+following won't work when compiling with GCC and tokenized logging:
+
+.. code-block:: cpp
+
+ template <...>
+ void DoThings() {
+ int value = GetValue();
+ // This log won't work with tokenized logs due to the templated context.
+ PW_LOG_INFO("Got value: %d", value);
+ ...
+ }
+
+The bug causes tokenized strings in template functions to be emitted into
+``.rodata`` instead of the special tokenized string section. This causes two
+problems:
+
+1. Tokenized strings will not be discovered by the token database tools.
+2. Tokenized strings may not be removed from the final binary.
+
+There are two workarounds.
+
+#. **Use Clang.** Clang puts the string data in the requested section, as
+ expected. No extra steps are required.
+
+#. **Move tokenization calls to a non-templated context.** Creating a separate
+ non-templated function and invoking it from the template resolves the issue.
+ This enables tokenizing in most cases encountered in practice with
+ templates.
- 1. Tokenized strings will not be discovered by the token database tools.
- 2. Tokenized strings may not be removed from the final binary.
+ .. code-block:: cpp
-clang does **not** have this issue! Use clang to avoid this.
+ // In .h file:
+ void LogThings(value);
-It is possible to work around this bug in GCC. One approach would be to tag
-format strings so that the database tools can find them in ``.rodata``. Then, to
-remove the strings, compile two binaries: one metadata binary with all tokenized
-strings and a second, final binary that removes the strings. The strings could
-be removed by providing the appropriate linker flags or by removing the ``used``
-attribute from the tokenized string character array declaration.
+ template <...>
+ void DoThings() {
+ int value = GetValue();
+ // This log will work: calls non-templated helper.
+ LogThings(value);
+ ...
+ }
+
+ // In .cc file:
+ void LogThings(int value) {
+ // Tokenized logging works as expected in this non-templated context.
+ PW_LOG_INFO("Got value %d", value);
+ }
+
+There is a third option, which isn't implemented yet, which is to compile the
+binary twice: once to extract the tokens, and once for the production binary
+(without tokens). If this is interesting to you please get in touch.
64-bit tokenization
--------------------
+===================
The Python and C++ detokenizing libraries currently assume that strings were
tokenized on a system with 32-bit ``long``, ``size_t``, ``intptr_t``, and
``ptrdiff_t``. Decoding may not work correctly for these types if a 64-bit
@@ -1148,7 +1479,7 @@ simple. This could be done by adding an option to switch the 32-bit types to
by checking the ELF file, if necessary.
Tokenization in headers
------------------------
+=======================
Tokenizing code in header files (inline functions or templates) may trigger
warnings such as ``-Wlto-type-mismatch`` under certain conditions. That
is because tokenization requires declaring a character array for each tokenized
@@ -1159,7 +1490,7 @@ possible, code that tokenizes strings with macros that can change value should
be moved to source files rather than headers.
Tokenized strings as ``%s`` arguments
--------------------------------------
+=====================================
Encoding ``%s`` string arguments is inefficient, since ``%s`` strings are
encoded 1:1, with no tokenization. It would be better to send a tokenized string
literal as an integer instead of a string argument, but this is not yet
@@ -1171,11 +1502,11 @@ argument to the string represented by the integer.
.. code-block:: cpp
- #define PW_TOKEN_ARG PRIx32 "<PW_TOKEN]"
+ #define PW_TOKEN_ARG PRIx32 "<PW_TOKEN]"
- constexpr uint32_t answer_token = PW_TOKENIZE_STRING("Uh, who is there");
+ constexpr uint32_t answer_token = PW_TOKENIZE_STRING("Uh, who is there");
- PW_TOKENIZE_TO_GLOBAL_HANDLER("Knock knock: %" PW_TOKEN_ARG "?", answer_token);
+ PW_TOKENIZE_STRING("Knock knock: %" PW_TOKEN_ARG "?", answer_token);
Strings with arguments could be encoded to a buffer, but since printf strings
are null-terminated, a binary encoding would not work. These strings can be
@@ -1185,37 +1516,16 @@ Another possibility: encode strings with arguments to a ``uint64_t`` and send
them as an integer. This would be efficient and simple, but only support a small
number of arguments.
-Legacy tokenized string ELF format
-==================================
-The original version of ``pw_tokenizer`` stored tokenized stored as plain C
-strings in the ELF file instead of structured tokenized string entries. Strings
-in different domains were stored in different linker sections. The Python script
-that parsed the ELF file would re-calculate the tokens.
-
-In the current version of ``pw_tokenizer``, tokenized strings are stored in a
-structured entry containing a token, domain, and length-delimited string. This
-has several advantages over the legacy format:
-
-* The Python script does not have to recalculate the token, so any hash
- algorithm may be used in the firmware.
-* In C++, the tokenization hash no longer has a length limitation.
-* Strings with null terminators in them are properly handled.
-* Only one linker section is required in the linker script, instead of a
- separate section for each domain.
-
-To migrate to the new format, all that is required is update the linker sections
-to match those in ``pw_tokenizer_linker_sections.ld``. Replace all
-``pw_tokenized.<DOMAIN>`` sections with one ``pw_tokenizer.entries`` section.
-The Python tooling continues to support the legacy tokenized string ELF format.
-
+-------------
Compatibility
-=============
- * C11
- * C++14
- * Python 3
+-------------
+* C11
+* C++14
+* Python 3
+------------
Dependencies
-============
- * ``pw_varint`` module
- * ``pw_preprocessor`` module
- * ``pw_span`` module
+------------
+* ``pw_varint`` module
+* ``pw_preprocessor`` module
+* ``pw_span`` module
diff --git a/pw_tokenizer/encode_args.cc b/pw_tokenizer/encode_args.cc
index ccd5c61f8..444afb73a 100644
--- a/pw_tokenizer/encode_args.cc
+++ b/pw_tokenizer/encode_args.cc
@@ -32,15 +32,15 @@ enum class ArgType : uint8_t {
kString = PW_TOKENIZER_ARG_TYPE_STRING,
};
-size_t EncodeInt(int value, const std::span<std::byte>& output) {
- return varint::Encode(value, std::as_writable_bytes(output));
+size_t EncodeInt(int value, const span<std::byte>& output) {
+ return varint::Encode(value, as_writable_bytes(output));
}
-size_t EncodeInt64(int64_t value, const std::span<std::byte>& output) {
- return varint::Encode(value, std::as_writable_bytes(output));
+size_t EncodeInt64(int64_t value, const span<std::byte>& output) {
+ return varint::Encode(value, as_writable_bytes(output));
}
-size_t EncodeFloat(float value, const std::span<std::byte>& output) {
+size_t EncodeFloat(float value, const span<std::byte>& output) {
if (output.size() < sizeof(value)) {
return 0;
}
@@ -48,7 +48,7 @@ size_t EncodeFloat(float value, const std::span<std::byte>& output) {
return sizeof(value);
}
-size_t EncodeString(const char* string, const std::span<std::byte>& output) {
+size_t EncodeString(const char* string, const span<std::byte>& output) {
// The top bit of the status byte indicates if the string was truncated.
static constexpr size_t kMaxStringLength = 0x7Fu;
@@ -86,7 +86,7 @@ size_t EncodeString(const char* string, const std::span<std::byte>& output) {
size_t EncodeArgs(pw_tokenizer_ArgTypes types,
va_list args,
- std::span<std::byte> output) {
+ span<std::byte> output) {
size_t arg_count = types & PW_TOKENIZER_TYPE_COUNT_MASK;
types >>= PW_TOKENIZER_TYPE_COUNT_SIZE_BITS;
@@ -126,5 +126,15 @@ size_t EncodeArgs(pw_tokenizer_ArgTypes types,
return encoded_bytes;
}
+extern "C" size_t pw_tokenizer_EncodeArgs(pw_tokenizer_ArgTypes types,
+ va_list args,
+ void* output_buffer,
+ size_t output_buffer_size) {
+ return EncodeArgs(types,
+ args,
+ span<std::byte>(static_cast<std::byte*>(output_buffer),
+ output_buffer_size));
+}
+
} // namespace tokenizer
} // namespace pw
diff --git a/pw_tokenizer/encode_args_test.cc b/pw_tokenizer/encode_args_test.cc
new file mode 100644
index 000000000..131f27b9a
--- /dev/null
+++ b/pw_tokenizer/encode_args_test.cc
@@ -0,0 +1,42 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_tokenizer/encode_args.h"
+
+#include "gtest/gtest.h"
+
+namespace pw {
+namespace tokenizer {
+
+static_assert(MinEncodingBufferSizeBytes<>() == 4);
+static_assert(MinEncodingBufferSizeBytes<bool>() == 4 + 2);
+static_assert(MinEncodingBufferSizeBytes<char>() == 4 + 2);
+static_assert(MinEncodingBufferSizeBytes<short>() == 4 + 3);
+static_assert(MinEncodingBufferSizeBytes<int>() == 4 + 5);
+static_assert(MinEncodingBufferSizeBytes<long long>() == 4 + 10);
+static_assert(MinEncodingBufferSizeBytes<float>() == 4 + 4);
+static_assert(MinEncodingBufferSizeBytes<double>() == 4 + 4);
+static_assert(MinEncodingBufferSizeBytes<const char*>() == 4 + 1);
+static_assert(MinEncodingBufferSizeBytes<void*>() == 4 + 5 ||
+ MinEncodingBufferSizeBytes<void*>() == 4 + 10);
+
+static_assert(MinEncodingBufferSizeBytes<int, double>() == 4 + 5 + 4);
+static_assert(MinEncodingBufferSizeBytes<int, int, const char*>() ==
+ 4 + 5 + 5 + 1);
+static_assert(
+ MinEncodingBufferSizeBytes<const char*, long long, int, short>() ==
+ 4 + 1 + 10 + 5 + 3);
+
+} // namespace tokenizer
+} // namespace pw
diff --git a/pw_tokenizer/generate_decoding_test_data.cc b/pw_tokenizer/generate_decoding_test_data.cc
index 83fda7f28..34e6dd232 100644
--- a/pw_tokenizer/generate_decoding_test_data.cc
+++ b/pw_tokenizer/generate_decoding_test_data.cc
@@ -25,8 +25,8 @@
#include <cstdint>
#include <cstdio>
#include <random>
-#include <span>
+#include "pw_span/span.h"
#include "pw_tokenizer/internal/decode.h"
#include "pw_tokenizer/tokenize.h"
#include "pw_varint/varint.h"
@@ -95,7 +95,7 @@ def TestCase(*args): # pylint: disable=invalid-name
return tuple(args)
-# yapf: disable
+
TEST_DATA = (
)";
@@ -168,7 +168,7 @@ class TestDataFile {
// Writes a decoding test case to the file.
void TestCase(TestDataFile* file,
- std::span<const uint8_t> buffer,
+ pw::span<const uint8_t> buffer,
const char* format,
const char* formatted) {
file->printf(R"(TestCase("%s", "%s", %s)",
@@ -189,7 +189,7 @@ void TestCase(TestDataFile* file,
const char (&buffer)[kSize],
const char* formatted) {
TestCase(file,
- std::span(reinterpret_cast<const uint8_t*>(buffer), kSize - 1),
+ pw::span(reinterpret_cast<const uint8_t*>(buffer), kSize - 1),
format,
formatted);
}
@@ -204,7 +204,7 @@ void TestCase(TestDataFile* file,
std::array<char, 128> formatted = {}; \
std::snprintf(formatted.data(), formatted.size(), format, ##__VA_ARGS__); \
TestCase(file, \
- std::span(buffer).first(size).subspan(4), /* skip the token */ \
+ pw::span(buffer).first(size).subspan(4), /* skip the token */ \
format, \
formatted.data()); \
} while (0)
@@ -382,8 +382,7 @@ void OutputVarintTest(TestDataFile* file, T i) {
std::array<uint8_t, 10> buffer;
// All integers are encoded as signed for tokenization.
- size_t size =
- pw::varint::Encode(i, std::as_writable_bytes(std::span(buffer)));
+ size_t size = pw::varint::Encode(i, pw::as_writable_bytes(pw::span(buffer)));
for (size_t i = 0; i < size; ++i) {
file->printf("\\x%02x", buffer[i]);
diff --git a/pw_tokenizer/global_handlers_test.cc b/pw_tokenizer/global_handlers_test.cc
deleted file mode 100644
index bef914e65..000000000
--- a/pw_tokenizer/global_handlers_test.cc
+++ /dev/null
@@ -1,299 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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 <cinttypes>
-#include <cstdint>
-#include <cstring>
-
-#include "gtest/gtest.h"
-#include "pw_tokenizer/tokenize_to_global_handler.h"
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
-#include "pw_tokenizer_private/tokenize_test.h"
-
-namespace pw::tokenizer {
-namespace {
-
-// Constructs an array with the hashed string followed by the provided bytes.
-template <uint8_t... data, size_t kSize>
-constexpr auto ExpectedData(
- const char (&format)[kSize],
- uint32_t token_mask = std::numeric_limits<uint32_t>::max()) {
- const uint32_t value = Hash(format) & token_mask;
- return std::array<uint8_t, sizeof(uint32_t) + sizeof...(data)>{
- static_cast<uint8_t>(value & 0xff),
- static_cast<uint8_t>(value >> 8 & 0xff),
- static_cast<uint8_t>(value >> 16 & 0xff),
- static_cast<uint8_t>(value >> 24 & 0xff),
- data...};
-}
-
-// Test fixture for both global handler functions. Both need a global message
-// buffer. To keep the message buffers separate, template this on the derived
-// class type.
-template <typename Impl>
-class GlobalMessage : public ::testing::Test {
- public:
- static void SetMessage(const uint8_t* message, size_t size) {
- ASSERT_LE(size, sizeof(message_));
- std::memcpy(message_, message, size);
- message_size_bytes_ = size;
- }
-
- protected:
- GlobalMessage() {
- std::memset(message_, 0, sizeof(message_));
- message_size_bytes_ = 0;
- }
-
- static uint8_t message_[256];
- static size_t message_size_bytes_;
-};
-
-template <typename Impl>
-uint8_t GlobalMessage<Impl>::message_[256] = {};
-template <typename Impl>
-size_t GlobalMessage<Impl>::message_size_bytes_ = 0;
-
-class TokenizeToGlobalHandler : public GlobalMessage<TokenizeToGlobalHandler> {
-};
-
-TEST_F(TokenizeToGlobalHandler, Variety) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER("%x%lld%1.2f%s", 0, 0ll, -0.0, "");
- const auto expected =
- ExpectedData<0, 0, 0x00, 0x00, 0x00, 0x80, 0>("%x%lld%1.2f%s");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToGlobalHandler, Strings) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER("The answer is: %s", "5432!");
- constexpr std::array<uint8_t, 10> expected =
- ExpectedData<5, '5', '4', '3', '2', '!'>("The answer is: %s");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToGlobalHandler, Domain_Strings) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER_DOMAIN(
- "TEST_DOMAIN", "The answer is: %s", "5432!");
- constexpr std::array<uint8_t, 10> expected =
- ExpectedData<5, '5', '4', '3', '2', '!'>("The answer is: %s");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToGlobalHandler, Mask) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER_MASK(
- "TEST_DOMAIN", 0x00FFF000, "The answer is: %s", "5432!");
- constexpr std::array<uint8_t, 10> expected =
- ExpectedData<5, '5', '4', '3', '2', '!'>("The answer is: %s", 0x00FFF000);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToGlobalHandler, C_SequentialZigZag) {
- pw_tokenizer_ToGlobalHandlerTest_SequentialZigZag();
-
- constexpr std::array<uint8_t, 18> expected =
- ExpectedData<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13>(
- TEST_FORMAT_SEQUENTIAL_ZIG_ZAG);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-extern "C" void pw_tokenizer_HandleEncodedMessage(
- const uint8_t* encoded_message, size_t size_bytes) {
- TokenizeToGlobalHandler::SetMessage(encoded_message, size_bytes);
-}
-
-class TokenizeToGlobalHandlerWithPayload
- : public GlobalMessage<TokenizeToGlobalHandlerWithPayload> {
- public:
- static void SetPayload(pw_tokenizer_Payload payload) {
- payload_ = static_cast<intptr_t>(payload);
- }
-
- protected:
- TokenizeToGlobalHandlerWithPayload() { payload_ = {}; }
-
- static intptr_t payload_;
-};
-
-intptr_t TokenizeToGlobalHandlerWithPayload::payload_;
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, Variety) {
- ASSERT_NE(payload_, 123);
-
- const auto expected =
- ExpectedData<0, 0, 0x00, 0x00, 0x00, 0x80, 0>("%x%lld%1.2f%s");
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
- static_cast<pw_tokenizer_Payload>(123),
- "%x%lld%1.2f%s",
- 0,
- 0ll,
- -0.0,
- "");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
- EXPECT_EQ(payload_, 123);
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
- static_cast<pw_tokenizer_Payload>(-543),
- "%x%lld%1.2f%s",
- 0,
- 0ll,
- -0.0,
- "");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
- EXPECT_EQ(payload_, -543);
-}
-
-constexpr std::array<uint8_t, 10> kExpected =
- ExpectedData<5, '5', '4', '3', '2', '!'>("The answer is: %s");
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, Strings_ZeroPayload) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD({}, "The answer is: %s", "5432!");
-
- ASSERT_EQ(kExpected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(kExpected.data(), message_, kExpected.size()), 0);
- EXPECT_EQ(payload_, 0);
-}
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, Strings_NonZeroPayload) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
- static_cast<pw_tokenizer_Payload>(5432), "The answer is: %s", "5432!");
-
- ASSERT_EQ(kExpected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(kExpected.data(), message_, kExpected.size()), 0);
- EXPECT_EQ(payload_, 5432);
-}
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, Domain_Strings) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD_DOMAIN(
- "TEST_DOMAIN",
- static_cast<pw_tokenizer_Payload>(5432),
- "The answer is: %s",
- "5432!");
- ASSERT_EQ(kExpected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(kExpected.data(), message_, kExpected.size()), 0);
- EXPECT_EQ(payload_, 5432);
-}
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, Mask) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD_MASK(
- "TEST_DOMAIN",
- 0x12345678,
- static_cast<pw_tokenizer_Payload>(5432),
- "The answer is: %s",
- "5432!");
- constexpr std::array<uint8_t, 10> expected =
- ExpectedData<5, '5', '4', '3', '2', '!'>("The answer is: %s", 0x12345678);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
- EXPECT_EQ(payload_, 5432);
-}
-
-struct Foo {
- unsigned char a;
- bool b;
-};
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, PointerToStack) {
- Foo foo{254u, true};
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
- reinterpret_cast<pw_tokenizer_Payload>(&foo), "Boring!");
-
- constexpr auto expected = ExpectedData("Boring!");
- static_assert(expected.size() == 4);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-
- Foo* payload_foo = reinterpret_cast<Foo*>(payload_);
- ASSERT_EQ(&foo, payload_foo);
- EXPECT_EQ(payload_foo->a, 254u);
- EXPECT_TRUE(payload_foo->b);
-}
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, C_SequentialZigZag) {
- pw_tokenizer_ToGlobalHandlerWithPayloadTest_SequentialZigZag();
-
- constexpr std::array<uint8_t, 18> expected =
- ExpectedData<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13>(
- TEST_FORMAT_SEQUENTIAL_ZIG_ZAG);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
- EXPECT_EQ(payload_, 600613);
-}
-
-extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload payload,
- const uint8_t* encoded_message,
- size_t size_bytes) {
- TokenizeToGlobalHandlerWithPayload::SetMessage(encoded_message, size_bytes);
- TokenizeToGlobalHandlerWithPayload::SetPayload(payload);
-}
-
-// Hijack an internal macro to capture the tokenizer domain.
-#undef _PW_TOKENIZER_RECORD_ORIGINAL_STRING
-#define _PW_TOKENIZER_RECORD_ORIGINAL_STRING(token, domain, string) \
- tokenizer_domain = domain; \
- string_literal = string
-
-TEST_F(TokenizeToGlobalHandler, Domain_Default) {
- const char* tokenizer_domain = nullptr;
- const char* string_literal = nullptr;
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER("404");
-
- EXPECT_STREQ(tokenizer_domain, PW_TOKENIZER_DEFAULT_DOMAIN);
- EXPECT_STREQ(string_literal, "404");
-}
-
-TEST_F(TokenizeToGlobalHandler, Domain_Specified) {
- const char* tokenizer_domain = nullptr;
- const char* string_literal = nullptr;
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER_DOMAIN("www.google.com", "404");
-
- EXPECT_STREQ(tokenizer_domain, "www.google.com");
- EXPECT_STREQ(string_literal, "404");
-}
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, Domain_Default) {
- const char* tokenizer_domain = nullptr;
- const char* string_literal = nullptr;
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
- static_cast<pw_tokenizer_Payload>(123), "Wow%s", "???");
-
- EXPECT_STREQ(tokenizer_domain, PW_TOKENIZER_DEFAULT_DOMAIN);
- EXPECT_STREQ(string_literal, "Wow%s");
-}
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, Domain_Specified) {
- const char* tokenizer_domain = nullptr;
- const char* string_literal = nullptr;
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD_DOMAIN(
- "THEDOMAIN", static_cast<pw_tokenizer_Payload>(123), "1234567890");
-
- EXPECT_STREQ(tokenizer_domain, "THEDOMAIN");
- EXPECT_STREQ(string_literal, "1234567890");
-}
-
-} // namespace
-} // namespace pw::tokenizer
diff --git a/pw_tokenizer/global_handlers_test_c.c b/pw_tokenizer/global_handlers_test_c.c
deleted file mode 100644
index 04d555171..000000000
--- a/pw_tokenizer/global_handlers_test_c.c
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-// This function tests the C implementation of tokenization API. These functions
-// are called from the main C++ test file.
-
-#include "pw_tokenizer/tokenize_to_global_handler.h"
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
-#include "pw_tokenizer_private/tokenize_test.h"
-
-#ifdef __cplusplus
-#error "This is a test of C code and must be compiled as C, not C++."
-#endif // __cplusplus
-
-// This test invokes the tokenization API with a variety of types. To simplify
-// validating the encoded data, numbers that are sequential when zig-zag encoded
-// are used as arguments.
-void pw_tokenizer_ToGlobalHandlerTest_SequentialZigZag(void) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER(TEST_FORMAT_SEQUENTIAL_ZIG_ZAG,
- 0u,
- -1,
- 1u,
- (unsigned)-2,
- (unsigned short)2u,
- (signed char)-3,
- 3,
- -4l,
- 4ul,
- -5ll,
- 5ull,
- (signed char)-6,
- (char)6,
- (signed char)-7);
-}
-
-void pw_tokenizer_ToGlobalHandlerWithPayloadTest_SequentialZigZag(void) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD((pw_tokenizer_Payload)600613,
- TEST_FORMAT_SEQUENTIAL_ZIG_ZAG,
- 0u,
- -1,
- 1u,
- (unsigned)-2,
- (unsigned short)2u,
- (signed char)-3,
- 3,
- -4l,
- 4ul,
- -5ll,
- 5ull,
- (signed char)-6,
- (char)6,
- (signed char)-7);
-}
diff --git a/pw_tokenizer/hash.cc b/pw_tokenizer/hash.cc
index 8cb3f26fc..d8ec6a59c 100644
--- a/pw_tokenizer/hash.cc
+++ b/pw_tokenizer/hash.cc
@@ -14,8 +14,9 @@
#include "pw_tokenizer/hash.h"
-namespace pw {
-namespace tokenizer {
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+namespace pw::tokenizer {
extern "C" uint32_t pw_tokenizer_65599FixedLengthHash(const char* string,
size_t string_length,
@@ -24,5 +25,6 @@ extern "C" uint32_t pw_tokenizer_65599FixedLengthHash(const char* string,
std::string_view(string, string_length), hash_length);
}
-} // namespace tokenizer
-} // namespace pw
+} // namespace pw::tokenizer
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
diff --git a/pw_tokenizer/java/dev/pigweed/tokenizer/detokenizer.cc b/pw_tokenizer/java/dev/pigweed/tokenizer/detokenizer.cc
index 06a5003c1..5f0740092 100644
--- a/pw_tokenizer/java/dev/pigweed/tokenizer/detokenizer.cc
+++ b/pw_tokenizer/java/dev/pigweed/tokenizer/detokenizer.cc
@@ -19,9 +19,9 @@
#include <jni.h>
#include <cstring>
-#include <span>
#include "pw_preprocessor/concat.h"
+#include "pw_span/span.h"
#include "pw_tokenizer/detokenize.h"
#include "pw_tokenizer/token_database.h"
@@ -57,7 +57,7 @@ JNIEXPORT jlong DETOKENIZER_METHOD(newNativeDetokenizer)(JNIEnv* env,
jbyte* const data = env->GetByteArrayElements(array, nullptr);
const jsize size = env->GetArrayLength(array);
- TokenDatabase tokens = TokenDatabase::Create(std::span(data, size));
+ TokenDatabase tokens = TokenDatabase::Create(span(data, size));
const jlong handle =
PointerToHandle(new Detokenizer(tokens.ok() ? tokens : TokenDatabase()));
diff --git a/pw_tokenizer/options.proto b/pw_tokenizer/options.proto
index bc0b87a6c..288a2dac6 100644
--- a/pw_tokenizer/options.proto
+++ b/pw_tokenizer/options.proto
@@ -29,7 +29,7 @@ enum Tokenization {
extend google.protobuf.FieldOptions {
// The field number was randomly selected from the reserved, internal use
// field numbers (50000-99999).
- // TODO(pwbug/393): Register with the Protobuf Global Extension Registry:
+ // TODO(b/234886465): Register with the Protobuf Global Extension Registry:
// https://github.com/protocolbuffers/protobuf/blob/HEAD/docs/options.md
Tokenization format = 78576;
}
diff --git a/pw_tokenizer/public/pw_tokenizer/base64.h b/pw_tokenizer/public/pw_tokenizer/base64.h
index 735196a2a..36acaf20d 100644
--- a/pw_tokenizer/public/pw_tokenizer/base64.h
+++ b/pw_tokenizer/public/pw_tokenizer/base64.h
@@ -30,6 +30,7 @@
#include <stddef.h>
#include "pw_preprocessor/util.h"
+#include "pw_tokenizer/config.h"
// This character is used to mark the start of a Base64-encoded tokenized
// message. For consistency, it is recommended to always use $ if possible.
@@ -64,10 +65,10 @@ PW_EXTERN_C_END
#ifdef __cplusplus
-#include <span>
#include <string_view>
#include "pw_base64/base64.h"
+#include "pw_span/span.h"
#include "pw_tokenizer/config.h"
#include "pw_tokenizer/tokenize.h"
@@ -75,12 +76,19 @@ namespace pw::tokenizer {
inline constexpr char kBase64Prefix = PW_TOKENIZER_BASE64_PREFIX;
+#undef PW_TOKENIZER_BASE64_PREFIX // In C++, use the variable, not the macro.
+
// Returns the size of a tokenized message (token + arguments) when encoded as
-// prefixed Base64. This can be used to size a buffer for encoding. Includes
-// room for the prefix character ($), encoded message, and a null terminator.
+// prefixed Base64. Includes room for the prefix character ($) and encoded
+// message. This value is the capacity needed to encode to a pw::InlineString.
+constexpr size_t Base64EncodedStringSize(size_t message_size) {
+ return sizeof(kBase64Prefix) + base64::EncodedSize(message_size);
+}
+
+// Same as Base64EncodedStringSize(), but for sizing char buffers. Includes room
+// for the prefix character ($), encoded message, and a null terminator.
constexpr size_t Base64EncodedBufferSize(size_t message_size) {
- return sizeof(kBase64Prefix) + base64::EncodedSize(message_size) +
- sizeof('\0');
+ return Base64EncodedStringSize(message_size) + sizeof('\0');
}
// The minimum buffer size that can hold a tokenized message that is
@@ -92,25 +100,56 @@ inline constexpr size_t kDefaultBase64EncodedBufferSize =
// Returns the encoded string length (excluding the null terminator). Returns 0
// if the buffer is too small. Always null terminates if the output buffer is
// not empty.
-inline size_t PrefixedBase64Encode(std::span<const std::byte> binary_message,
- std::span<char> output_buffer) {
+inline size_t PrefixedBase64Encode(span<const std::byte> binary_message,
+ span<char> output_buffer) {
return pw_tokenizer_PrefixedBase64Encode(binary_message.data(),
binary_message.size(),
output_buffer.data(),
output_buffer.size());
}
-// Also accept a std::span<const uint8_t> for the binary message.
-inline size_t PrefixedBase64Encode(std::span<const uint8_t> binary_message,
- std::span<char> output_buffer) {
- return PrefixedBase64Encode(std::as_bytes(binary_message), output_buffer);
+// Also accept a span<const uint8_t> for the binary message.
+inline size_t PrefixedBase64Encode(span<const uint8_t> binary_message,
+ span<char> output_buffer) {
+ return PrefixedBase64Encode(as_bytes(binary_message), output_buffer);
+}
+
+// Encodes a binary tokenized message as prefixed Base64 to a pw::InlineString,
+// appending to any existing contents. Asserts if the message does not fit in
+// the string.
+void PrefixedBase64Encode(span<const std::byte> binary_message,
+ InlineString<>& output);
+
+inline void PrefixedBase64Encode(span<const uint8_t> binary_message,
+ InlineString<>& output) {
+ return PrefixedBase64Encode(as_bytes(binary_message), output);
+}
+
+// Encodes a binary tokenized message as prefixed Base64 to a pw::InlineString.
+// The pw::InlineString is sized to fit messages up to
+// kMaxBinaryMessageSizeBytes long. Asserts if the message is larger.
+template <size_t kMaxBinaryMessageSizeBytes =
+ PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES>
+auto PrefixedBase64Encode(span<const std::byte> binary_message) {
+ static_assert(kMaxBinaryMessageSizeBytes >= 1, "Messages cannot be empty");
+ InlineString<Base64EncodedStringSize(kMaxBinaryMessageSizeBytes)> string(
+ 1, kBase64Prefix);
+ base64::Encode(binary_message, string);
+ return string;
+}
+
+template <size_t kMaxBinaryMessageSizeBytes =
+ PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES>
+auto PrefixedBase64Encode(span<const uint8_t> binary_message) {
+ return PrefixedBase64Encode<kMaxBinaryMessageSizeBytes>(
+ as_bytes(binary_message));
}
// Decodes a prefixed Base64 tokenized message to binary. Returns the size of
// the decoded binary data. The resulting data is ready to be passed to
// pw::tokenizer::Detokenizer::Detokenize.
inline size_t PrefixedBase64Decode(std::string_view base64_message,
- std::span<std::byte> output_buffer) {
+ span<std::byte> output_buffer) {
return pw_tokenizer_PrefixedBase64Decode(base64_message.data(),
base64_message.size(),
output_buffer.data(),
@@ -119,11 +158,20 @@ inline size_t PrefixedBase64Decode(std::string_view base64_message,
// Decodes a prefixed Base64 tokenized message to binary in place. Returns the
// size of the decoded binary data.
-inline size_t PrefixedBase64DecodeInPlace(std::span<std::byte> buffer) {
+inline size_t PrefixedBase64DecodeInPlace(span<std::byte> buffer) {
return pw_tokenizer_PrefixedBase64Decode(
buffer.data(), buffer.size(), buffer.data(), buffer.size());
}
+// Decodes a prefixed Base64 tokenized message to binary in place. Resizes the
+// string to fit the decoded binary data.
+template <typename CharT>
+inline void PrefixedBase64DecodeInPlace(InlineBasicString<CharT>& string) {
+ static_assert(sizeof(CharT) == sizeof(char));
+ string.resize(pw_tokenizer_PrefixedBase64Decode(
+ string.data(), string.size(), string.data(), string.size()));
+}
+
} // namespace pw::tokenizer
#endif // __cplusplus
diff --git a/pw_tokenizer/public/pw_tokenizer/config.h b/pw_tokenizer/public/pw_tokenizer/config.h
index 3614112b2..d48ce53e3 100644
--- a/pw_tokenizer/public/pw_tokenizer/config.h
+++ b/pw_tokenizer/public/pw_tokenizer/config.h
@@ -52,11 +52,11 @@ static_assert(PW_TOKENIZER_CFG_ARG_TYPES_SIZE_BYTES == 4 ||
#define PW_TOKENIZER_CFG_C_HASH_LENGTH 128
#endif // PW_TOKENIZER_CFG_C_HASH_LENGTH
-// The size of the stack-allocated argument encoding buffer to use. This only
-// affects tokenization macros that stack-allocate the encoding buffer
-// (PW_TOKENIZE_TO_CALLBACK and PW_TOKENIZE_TO_GLOBAL_HANDLER). A buffer of this
-// size is allocated and used for the 4-byte token and for encoding all
-// arguments. It must be at least large enough for the token (4 bytes).
+// The size of the stack-allocated argument encoding buffer to use by default.
+// This only affects tokenization macros that use the
+// pw::tokenizer::EncodedMessage class. A buffer of this size is allocated and
+// used for the 4-byte token and for encoding all arguments. It must be at least
+// large enough for the token (4 bytes).
//
// This buffer does not need to be large to accommodate a good number of
// tokenized string arguments. Integer arguments are usually encoded smaller
diff --git a/pw_tokenizer/public/pw_tokenizer/detokenize.h b/pw_tokenizer/public/pw_tokenizer/detokenize.h
index 7c658cfda..9a3e58847 100644
--- a/pw_tokenizer/public/pw_tokenizer/detokenize.h
+++ b/pw_tokenizer/public/pw_tokenizer/detokenize.h
@@ -26,12 +26,12 @@
#include <cstddef>
#include <cstdint>
-#include <span>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
+#include "pw_span/span.h"
#include "pw_tokenizer/internal/decode.h"
#include "pw_tokenizer/token_database.h"
@@ -44,8 +44,8 @@ using TokenizedStringEntry = std::pair<FormatString, uint32_t /*date removed*/>;
class DetokenizedString {
public:
DetokenizedString(uint32_t token,
- const std::span<const TokenizedStringEntry>& entries,
- const std::span<const uint8_t>& arguments);
+ const span<const TokenizedStringEntry>& entries,
+ const span<const uint8_t>& arguments);
DetokenizedString() : has_token_(false) {}
@@ -55,6 +55,8 @@ class DetokenizedString {
// Returns the strings that matched the token, with the best matches first.
const std::vector<DecodedFormatString>& matches() const { return matches_; }
+ const uint32_t& token() const { return token_; }
+
// Returns the detokenized string or an empty string if there were no matches.
// If there are multiple possible results, the DetokenizedString returns the
// first match.
@@ -80,15 +82,14 @@ class Detokenizer {
// Decodes and detokenizes the encoded message. Returns a DetokenizedString
// that stores all possible detokenized string results.
- DetokenizedString Detokenize(const std::span<const uint8_t>& encoded) const;
+ DetokenizedString Detokenize(const span<const uint8_t>& encoded) const;
DetokenizedString Detokenize(const std::string_view& encoded) const {
return Detokenize(encoded.data(), encoded.size());
}
DetokenizedString Detokenize(const void* encoded, size_t size_bytes) const {
- return Detokenize(
- std::span(static_cast<const uint8_t*>(encoded), size_bytes));
+ return Detokenize(span(static_cast<const uint8_t*>(encoded), size_bytes));
}
private:
diff --git a/pw_tokenizer/public/pw_tokenizer/encode_args.h b/pw_tokenizer/public/pw_tokenizer/encode_args.h
index 19f4e8825..07e6e3192 100644
--- a/pw_tokenizer/public/pw_tokenizer/encode_args.h
+++ b/pw_tokenizer/public/pw_tokenizer/encode_args.h
@@ -13,51 +13,98 @@
// the License.
#pragma once
-#include <cstdarg>
-#include <cstddef>
+#include <stdarg.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "pw_polyfill/standard.h"
+#include "pw_preprocessor/util.h"
+#include "pw_tokenizer/internal/argument_types.h"
+
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
+
#include <cstring>
-#include <span>
+#include "pw_polyfill/standard.h"
+#include "pw_span/span.h"
#include "pw_tokenizer/config.h"
-#include "pw_tokenizer/internal/argument_types.h"
#include "pw_tokenizer/tokenize.h"
-namespace pw {
-namespace tokenizer {
+namespace pw::tokenizer {
+namespace internal {
-// Encodes a tokenized string's arguments to a buffer. The
-// pw_tokenizer_ArgTypes parameter specifies the argument types, in place of a
-// format string.
-//
-// Most tokenization implementations may use the EncodedMessage class below.
+// Returns the maximum encoded size of an argument of the specified type.
+template <typename T>
+constexpr size_t ArgEncodedSizeBytes() {
+ constexpr pw_tokenizer_ArgTypes kType = VarargsType<T>();
+ if constexpr (kType == PW_TOKENIZER_ARG_TYPE_DOUBLE) {
+ return sizeof(float);
+ } else if constexpr (kType == PW_TOKENIZER_ARG_TYPE_STRING) {
+ return 1; // Size of the length byte only
+ } else if constexpr (kType == PW_TOKENIZER_ARG_TYPE_INT64) {
+ return 10; // Max size of a varint-encoded 64-bit integer
+ } else if constexpr (kType == PW_TOKENIZER_ARG_TYPE_INT) {
+ return sizeof(T) + 1; // Max size of zig-zag varint integer <= 32-bits
+ } else {
+ static_assert(sizeof(T) != sizeof(T), "Unsupported argument type");
+ }
+}
+
+} // namespace internal
+
+/// Calculates the minimum buffer size to allocate that is guaranteed to support
+/// encoding the specified arguments.
+///
+/// The contents of strings are NOT included in this total. The string's
+/// length/status byte is guaranteed to fit, but the string contents may be
+/// truncated. Encoding is considered to succeed as long as the string's
+/// length/status byte is written, even if the actual string is truncated.
+///
+/// Examples:
+///
+/// - Message with no arguments:
+/// `MinEncodingBufferSizeBytes() == 4`
+/// - Message with an int argument
+/// `MinEncodingBufferSizeBytes<int>() == 9 (4 + 5)`
+template <typename... ArgTypes>
+constexpr size_t MinEncodingBufferSizeBytes() {
+ return (sizeof(pw_tokenizer_Token) + ... +
+ internal::ArgEncodedSizeBytes<ArgTypes>());
+}
+
+/// Encodes a tokenized string's arguments to a buffer. The
+/// @cpp_type{pw_tokenizer_ArgTypes} parameter specifies the argument types, in
+/// place of a format string.
+///
+/// Most tokenization implementations should use the @cpp_class{EncodedMessage}
+/// class.
size_t EncodeArgs(pw_tokenizer_ArgTypes types,
va_list args,
- std::span<std::byte> output);
+ span<std::byte> output);
-// Encodes a tokenized message to a fixed size buffer. The size of the buffer is
-// determined by the PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES config macro.
-//
-// This class is used to encode tokenized messages passed in from the
-// tokenization macros. The macros provided by pw_tokenizer use this class, and
-// projects that elect to define their own versions of the tokenization macros
-// should use it when possible.
-//
-// To use the pw::Tokenizer::EncodedMessage, construct it with the token,
-// argument types, and va_list from the variadic arguments:
-//
-// void SendLogMessage(std::span<std::byte> log_data);
-//
-// extern "C" void TokenizeToSendLogMessage(pw_tokenizer_Token token,
-// pw_tokenizer_ArgTypes types,
-// ...) {
-// va_list args;
-// va_start(args, types);
-// EncodedMessage encoded_message(token, types, args);
-// va_end(args);
-//
-// SendLogMessage(encoded_message); // EncodedMessage converts to std::span
-// }
-//
+/// Encodes a tokenized message to a fixed size buffer. By default, the buffer
+/// size is set by the @c_macro{PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES}
+/// config macro. This class is used to encode tokenized messages passed in from
+/// tokenization macros.
+///
+/// To use `pw::tokenizer::EncodedMessage`, construct it with the token,
+/// argument types, and `va_list` from the variadic arguments:
+///
+/// @code{.cpp}
+/// void SendLogMessage(span<std::byte> log_data);
+///
+/// extern "C" void TokenizeToSendLogMessage(pw_tokenizer_Token token,
+/// pw_tokenizer_ArgTypes types,
+/// ...) {
+/// va_list args;
+/// va_start(args, types);
+/// EncodedMessage encoded_message(token, types, args);
+/// va_end(args);
+///
+/// SendLogMessage(encoded_message); // EncodedMessage converts to span
+/// }
+/// @endcode
+template <size_t kMaxSizeBytes = PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES>
class EncodedMessage {
public:
// Encodes a tokenized message to an internal buffer.
@@ -65,30 +112,41 @@ class EncodedMessage {
pw_tokenizer_ArgTypes types,
va_list args) {
std::memcpy(data_, &token, sizeof(token));
- args_size_ = EncodeArgs(
- types, args, std::span<std::byte>(data_).subspan(sizeof(token)));
+ size_ =
+ sizeof(token) +
+ EncodeArgs(types, args, span<std::byte>(data_).subspan(sizeof(token)));
}
- // The binary-encoded tokenized message.
+ /// The binary-encoded tokenized message.
const std::byte* data() const { return data_; }
- // Returns the data() as a pointer to uint8_t instead of std::byte.
+ /// Returns `data()` as a pointer to `uint8_t` instead of `std::byte`.
const uint8_t* data_as_uint8() const {
return reinterpret_cast<const uint8_t*>(data());
}
- // The size of the encoded tokenized message in bytes.
- size_t size() const { return sizeof(pw_tokenizer_Token) + args_size_; }
+ /// The size of the encoded tokenized message in bytes.
+ size_t size() const { return size_; }
private:
- std::byte data_[PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES];
- size_t args_size_;
+ static_assert(kMaxSizeBytes >= sizeof(pw_tokenizer_Token),
+ "The encoding buffer must be at least large enough for a token "
+ "(4 bytes)");
+
+ std::byte data_[kMaxSizeBytes];
+ size_t size_;
};
-static_assert(PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES >=
- sizeof(pw_tokenizer_Token),
- "PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES must be at least "
- "large enough for a token (4 bytes)");
+} // namespace pw::tokenizer
+
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
+
+PW_EXTERN_C_START
-} // namespace tokenizer
-} // namespace pw
+/// C function that encodes arguments to a tokenized buffer. Use the
+/// @cpp_func{pw::tokenizer::EncodeArgs} function from C++.
+size_t pw_tokenizer_EncodeArgs(pw_tokenizer_ArgTypes types,
+ va_list args,
+ void* output_buffer,
+ size_t output_buffer_size);
+PW_EXTERN_C_END
diff --git a/pw_tokenizer/public/pw_tokenizer/hash.h b/pw_tokenizer/public/pw_tokenizer/hash.h
index f97d32f6b..35fb28457 100644
--- a/pw_tokenizer/public/pw_tokenizer/hash.h
+++ b/pw_tokenizer/public/pw_tokenizer/hash.h
@@ -16,12 +16,16 @@
#include <stddef.h>
#include <stdint.h>
-#ifdef __cplusplus
+#include "pw_polyfill/standard.h"
+#include "pw_preprocessor/util.h"
+
+// The hash implementation uses std::string_view. Use the C implementation when
+// compiling with older C++ standards.
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
#include <string_view>
#include "pw_preprocessor/compiler.h"
-#include "pw_preprocessor/util.h"
#include "pw_tokenizer/config.h"
namespace pw::tokenizer {
@@ -104,7 +108,7 @@ constexpr uint32_t PwTokenizer65599FixedLengthHash(
} // namespace pw::tokenizer
-#endif // __cplusplus
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
// C version of the fixed-length hash. Can be used to calculate hashes
// equivalent to the hashing macros at runtime in C.
diff --git a/pw_tokenizer/public/pw_tokenizer/internal/argument_types.h b/pw_tokenizer/public/pw_tokenizer/internal/argument_types.h
index 0fdf96bc5..64c9a4cb1 100644
--- a/pw_tokenizer/public/pw_tokenizer/internal/argument_types.h
+++ b/pw_tokenizer/public/pw_tokenizer/internal/argument_types.h
@@ -174,14 +174,4 @@ constexpr pw_tokenizer_ArgTypes VarargsType() {
#endif // __cplusplus
-// Encodes the types of the provided arguments as a pw_tokenizer_ArgTypes
-// value. Depending on the size of pw_tokenizer_ArgTypes, the bottom 4 or 6
-// bits store the number of arguments and the remaining bits store the types,
-// two bits per type.
-//
-// The arguments are not evaluated; only their types are used to
-// select the set their corresponding PW_TOKENIZER_ARG_TYPEs.
-#define PW_TOKENIZER_ARG_TYPES(...) \
- PW_DELEGATE_BY_ARG_COUNT(_PW_TOKENIZER_TYPES_, __VA_ARGS__)
-
#define _PW_TOKENIZER_TYPES_0() ((pw_tokenizer_ArgTypes)0)
diff --git a/pw_tokenizer/public/pw_tokenizer/internal/decode.h b/pw_tokenizer/public/pw_tokenizer/internal/decode.h
index 913838f00..8cdf69081 100644
--- a/pw_tokenizer/public/pw_tokenizer/internal/decode.h
+++ b/pw_tokenizer/public/pw_tokenizer/internal/decode.h
@@ -20,12 +20,13 @@
#include <cstddef>
#include <cstdint>
#include <cstdio>
-#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
+#include "pw_span/span.h"
+
// Decoding errors are marked with prefix and suffix so that they stand out from
// the rest of the decoded strings. These macros are used to build decoding
// error strings.
@@ -131,7 +132,7 @@ class StringSegment {
// Returns the DecodedArg with this StringSegment decoded according to the
// provided arguments.
- DecodedArg Decode(const std::span<const uint8_t>& arguments) const;
+ DecodedArg Decode(const span<const uint8_t>& arguments) const;
// Skips decoding this StringSegment. Literals and %% are expanded as normal.
DecodedArg Skip() const;
@@ -169,12 +170,11 @@ class StringSegment {
StringSegment(const std::string_view& text, Type type, ArgSize local_size)
: text_(text), type_(type), local_size_(local_size) {}
- DecodedArg DecodeString(const std::span<const uint8_t>& arguments) const;
+ DecodedArg DecodeString(const span<const uint8_t>& arguments) const;
- DecodedArg DecodeInteger(const std::span<const uint8_t>& arguments) const;
+ DecodedArg DecodeInteger(const span<const uint8_t>& arguments) const;
- DecodedArg DecodeFloatingPoint(
- const std::span<const uint8_t>& arguments) const;
+ DecodedArg DecodeFloatingPoint(const span<const uint8_t>& arguments) const;
std::string text_;
Type type_;
@@ -229,11 +229,11 @@ class FormatString {
// Formats this format string according to the provided encoded arguments and
// returns a string.
- DecodedFormatString Format(std::span<const uint8_t> arguments) const;
+ DecodedFormatString Format(span<const uint8_t> arguments) const;
DecodedFormatString Format(const std::string_view& arguments) const {
- return Format(std::span(reinterpret_cast<const uint8_t*>(arguments.data()),
- arguments.size()));
+ return Format(span(reinterpret_cast<const uint8_t*>(arguments.data()),
+ arguments.size()));
}
private:
diff --git a/pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h b/pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h
index 80df724a7..6191ddf99 100644
--- a/pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h
+++ b/pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h
@@ -27,7 +27,7 @@
#include <stdint.h>
-#define _PW_TOKENIZER_ENTRY_MAGIC UINT32_C(0xBAA98DEE)
+#define _PW_TOKENIZER_ENTRY_MAGIC 0xBAA98DEE
#ifdef __cplusplus
diff --git a/pw_tokenizer/public/pw_tokenizer/tokenize.h b/pw_tokenizer/public/pw_tokenizer/tokenize.h
index 8ad2b3a0d..6b8e62c57 100644
--- a/pw_tokenizer/public/pw_tokenizer/tokenize.h
+++ b/pw_tokenizer/public/pw_tokenizer/tokenize.h
@@ -33,42 +33,64 @@
#include "pw_tokenizer/internal/argument_types.h"
#include "pw_tokenizer/internal/tokenize_string.h"
-// The type of the token used in place of a format string. Also available as
-// pw::tokenizer::Token.
+/// The type of the 32-bit token used in place of a string. Also available as
+/// `pw::tokenizer::Token`.
typedef uint32_t pw_tokenizer_Token;
-// Strings may optionally be tokenized to a domain. Strings in different domains
-// can be processed separately by the token database tools. Each domain in use
-// must have a corresponding section declared in the linker script. See
-// pw_tokenizer_linker_sections.ld for more details.
+// Strings may optionally be tokenized to a domain. Strings in different
+// domains can be processed separately by the token database tools. Each domain
+// in use must have a corresponding section declared in the linker script. See
+// `pw_tokenizer_linker_sections.ld` for more details.
//
// The default domain is an empty string.
#define PW_TOKENIZER_DEFAULT_DOMAIN ""
-// Tokenizes a string and converts it to a pw_tokenizer_Token. In C++, the
-// string may be a literal or a constexpr char array. In C, the argument must be
-// a string literal. In either case, the string must be null terminated, but may
-// contain any characters (including '\0').
-//
-// This expression can be assigned to a local or global variable, but cannot be
-// used in another expression. For example:
-//
-// constexpr uint32_t global = PW_TOKENIZE_STRING("Wow!"); // This works.
-//
-// void SomeFunction() {
-// constexpr uint32_t token = PW_TOKENIZE_STRING("Cool!"); // This works.
-//
-// DoSomethingElse(PW_TOKENIZE_STRING("Lame!")); // This does NOT work.
-// }
-//
+/// Converts a string literal to a `pw_tokenizer_Token` (`uint32_t`) token in a
+/// standalone statement. C and C++ compatible. In C++, the string may be a
+/// literal or a constexpr char array, including function variables like
+/// `__func__`. In C, the argument must be a string literal. In either case, the
+/// string must be null terminated, but may contain any characters (including
+/// '\0').
+///
+/// @code
+///
+/// constexpr uint32_t token = PW_TOKENIZE_STRING("Any string literal!");
+///
+/// @endcode
#define PW_TOKENIZE_STRING(string_literal) \
PW_TOKENIZE_STRING_DOMAIN(PW_TOKENIZER_DEFAULT_DOMAIN, string_literal)
-// Same as PW_TOKENIZE_STRING, but tokenizes to the specified domain.
+/// Converts a string literal to a ``uint32_t`` token within an expression.
+/// Requires C++.
+///
+/// @code
+///
+/// DoSomething(PW_TOKENIZE_STRING_EXPR("Succeed"));
+///
+/// @endcode
+#define PW_TOKENIZE_STRING_EXPR(string_literal) \
+ [&] { \
+ constexpr uint32_t lambda_ret_token = PW_TOKENIZE_STRING(string_literal); \
+ return lambda_ret_token; \
+ }()
+
+/// Tokenizes a string literal in a standalone statement using the specified
+/// @rstref{domain <module-pw_tokenizer-domains>}. C and C++ compatible.
#define PW_TOKENIZE_STRING_DOMAIN(domain, string_literal) \
PW_TOKENIZE_STRING_MASK(domain, UINT32_MAX, string_literal)
-// Same as PW_TOKENIZE_STRING_DOMAIN, but applies a mask to the token.
+/// Tokenizes a string literal using the specified @rstref{domain
+/// <module-pw_tokenizer-domains>} within an expression. Requires C++.
+#define PW_TOKENIZE_STRING_DOMAIN_EXPR(domain, string_literal) \
+ [&] { \
+ constexpr uint32_t lambda_ret_token = \
+ PW_TOKENIZE_STRING_DOMAIN(domain, string_literal); \
+ return lambda_ret_token; \
+ }()
+
+/// Tokenizes a string literal in a standalone statement using the specified
+/// @rstref{domain <module-pw_tokenizer-domains>} and @rstref{bit mask
+/// <module-pw_tokenizer-masks>}. C and C++ compatible.
#define PW_TOKENIZE_STRING_MASK(domain, mask, string_literal) \
/* assign to a variable */ _PW_TOKENIZER_MASK_TOKEN(mask, string_literal); \
\
@@ -78,29 +100,49 @@ typedef uint32_t pw_tokenizer_Token;
_PW_TOKENIZER_RECORD_ORIGINAL_STRING( \
_PW_TOKENIZER_MASK_TOKEN(mask, string_literal), domain, string_literal)
+/// Tokenizes a string literal using the specified @rstref{domain
+/// <module-pw_tokenizer-domains>} and @rstref{bit mask
+/// <module-pw_tokenizer-masks>} within an expression. Requires C++.
+#define PW_TOKENIZE_STRING_MASK_EXPR(domain, mask, string_literal) \
+ [&] { \
+ constexpr uint32_t lambda_ret_token = \
+ PW_TOKENIZE_STRING_MASK(domain, mask, string_literal); \
+ return lambda_ret_token; \
+ }()
+
#define _PW_TOKENIZER_MASK_TOKEN(mask, string_literal) \
((pw_tokenizer_Token)(mask)&PW_TOKENIZER_STRING_TOKEN(string_literal))
-// Encodes a tokenized string and arguments to the provided buffer. The size of
-// the buffer is passed via a pointer to a size_t. After encoding is complete,
-// the size_t is set to the number of bytes written to the buffer.
-//
-// The macro's arguments are equivalent to the following function signature:
-//
-// TokenizeToBuffer(void* buffer,
-// size_t* buffer_size_pointer,
-// const char* format,
-// ...); /* printf-style arguments */
-//
-// For example, the following encodes a tokenized string with a temperature to a
-// buffer. The buffer is passed to a function to send the message over a UART.
-//
-// uint8_t buffer[32];
-// size_t size_bytes = sizeof(buffer);
-// PW_TOKENIZE_TO_BUFFER(
-// buffer, &size_bytes, "Temperature (C): %0.2f", temperature_c);
-// MyProject_EnqueueMessageForUart(buffer, size);
-//
+/// Encodes a tokenized string and arguments to the provided buffer. The size of
+/// the buffer is passed via a pointer to a `size_t`. After encoding is
+/// complete, the `size_t` is set to the number of bytes written to the buffer.
+///
+/// The macro's arguments are equivalent to the following function signature:
+///
+/// @code
+///
+/// TokenizeToBuffer(void* buffer,
+/// size_t* buffer_size_pointer,
+/// const char* format,
+/// ...); // printf-style arguments
+/// @endcode
+///
+/// For example, the following encodes a tokenized string with a temperature to
+/// a buffer. The buffer is passed to a function to send the message over a
+/// UART.
+///
+/// @code
+///
+/// uint8_t buffer[32];
+/// size_t size_bytes = sizeof(buffer);
+/// PW_TOKENIZE_TO_BUFFER(
+/// buffer, &size_bytes, "Temperature (C): %0.2f", temperature_c);
+/// MyProject_EnqueueMessageForUart(buffer, size);
+///
+/// @endcode
+///
+/// While `PW_TOKENIZE_TO_BUFFER` is very flexible, it must be passed a buffer,
+/// which increases its code size footprint at the call site.
#define PW_TOKENIZE_TO_BUFFER(buffer, buffer_size_pointer, format, ...) \
PW_TOKENIZE_TO_BUFFER_DOMAIN(PW_TOKENIZER_DEFAULT_DOMAIN, \
buffer, \
@@ -108,13 +150,15 @@ typedef uint32_t pw_tokenizer_Token;
format, \
__VA_ARGS__)
-// Same as PW_TOKENIZE_TO_BUFFER, but tokenizes to the specified domain.
+/// Same as @c_macro{PW_TOKENIZE_TO_BUFFER}, but tokenizes to the specified
+/// @rstref{domain <module-pw_tokenizer-domains>}.
#define PW_TOKENIZE_TO_BUFFER_DOMAIN( \
domain, buffer, buffer_size_pointer, format, ...) \
PW_TOKENIZE_TO_BUFFER_MASK( \
domain, UINT32_MAX, buffer, buffer_size_pointer, format, __VA_ARGS__)
-// Same as PW_TOKENIZE_TO_BUFFER_DOMAIN, but applies a mask to the token.
+/// Same as @c_macro{PW_TOKENIZE_TO_BUFFER_DOMAIN}, but applies a
+/// @rstref{bit mask <module-pw_tokenizer-masks>} to the token.
#define PW_TOKENIZE_TO_BUFFER_MASK( \
domain, mask, buffer, buffer_size_pointer, format, ...) \
do { \
@@ -126,52 +170,14 @@ typedef uint32_t pw_tokenizer_Token;
PW_COMMA_ARGS(__VA_ARGS__)); \
} while (0)
-// Encodes a tokenized string and arguments to a buffer on the stack. The
-// provided callback is called with the encoded data. The size of the
-// stack-allocated argument encoding buffer is set with the
-// PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES option.
-//
-// The macro's arguments are equivalent to the following function signature:
-//
-// TokenizeToCallback(void (*callback)(const uint8_t* data, size_t size),
-// const char* format,
-// ...); /* printf-style arguments */
-//
-// For example, the following encodes a tokenized string with a sensor name and
-// floating point data. The encoded message is passed directly to the
-// MyProject_EnqueueMessageForUart function, which the caller provides as a
-// callback.
-//
-// void MyProject_EnqueueMessageForUart(const uint8_t* buffer,
-// size_t size_bytes) {
-// uart_queue_write(uart_instance, buffer, size_bytes);
-// }
-//
-// void LogSensorValue(const char* sensor_name, float value) {
-// PW_TOKENIZE_TO_CALLBACK(MyProject_EnqueueMessageForUart,
-// "%s: %f",
-// sensor_name,
-// value);
-// }
-//
-#define PW_TOKENIZE_TO_CALLBACK(callback, format, ...) \
- PW_TOKENIZE_TO_CALLBACK_DOMAIN( \
- PW_TOKENIZER_DEFAULT_DOMAIN, callback, format, __VA_ARGS__)
-
-// Same as PW_TOKENIZE_TO_CALLBACK, but tokenizes to the specified domain.
-#define PW_TOKENIZE_TO_CALLBACK_DOMAIN(domain, callback, format, ...) \
- PW_TOKENIZE_TO_CALLBACK_MASK( \
- domain, UINT32_MAX, callback, format, __VA_ARGS__)
-
-// Same as PW_TOKENIZE_TO_CALLBACK_DOMAIN, but applies a mask to the token.
-#define PW_TOKENIZE_TO_CALLBACK_MASK(domain, mask, callback, format, ...) \
- do { \
- PW_TOKENIZE_FORMAT_STRING(domain, mask, format, __VA_ARGS__); \
- _pw_tokenizer_ToCallback(callback, \
- _pw_tokenizer_token, \
- PW_TOKENIZER_ARG_TYPES(__VA_ARGS__) \
- PW_COMMA_ARGS(__VA_ARGS__)); \
- } while (0)
+/// Converts a series of arguments to a compact format that replaces the format
+/// string literal. Evaluates to a `pw_tokenizer_ArgTypes` value.
+///
+/// Depending on the size of `pw_tokenizer_ArgTypes`, the bottom 4 or 6 bits
+/// store the number of arguments and the remaining bits store the types, two
+/// bits per type. The arguments are not evaluated; only their types are used.
+#define PW_TOKENIZER_ARG_TYPES(...) \
+ PW_DELEGATE_BY_ARG_COUNT(_PW_TOKENIZER_TYPES_, __VA_ARGS__)
PW_EXTERN_C_START
@@ -183,12 +189,6 @@ void _pw_tokenizer_ToBuffer(void* buffer,
pw_tokenizer_ArgTypes types,
...);
-void _pw_tokenizer_ToCallback(void (*callback)(const uint8_t* encoded_message,
- size_t size_bytes),
- pw_tokenizer_Token token,
- pw_tokenizer_ArgTypes types,
- ...);
-
// This empty function allows the compiler to check the format string.
static inline void pw_tokenizer_CheckFormatString(const char* format, ...)
PW_PRINTF_FORMAT(1, 2);
@@ -199,12 +199,17 @@ static inline void pw_tokenizer_CheckFormatString(const char* format, ...) {
PW_EXTERN_C_END
-// These macros implement string tokenization. They should not be used directly;
-// use one of the PW_TOKENIZE_* macros above instead.
-
-// This macro takes a printf-style format string and corresponding arguments. It
-// checks that the arguments are correct, stores the format string in a special
-// section, and calculates the string's token at compile time. This
+/// Tokenizes a format string with optional arguments and sets the
+/// `_pw_tokenizer_token` variable to the token. Must be used in its own scope,
+/// since the same variable is used in every invocation.
+///
+/// The tokenized string uses the specified @rstref{tokenization domain
+/// <module-pw_tokenizer-domains>}. Use `PW_TOKENIZER_DEFAULT_DOMAIN` for the
+/// default. The token also may be masked; use `UINT32_MAX` to keep all bits.
+///
+/// This macro checks that the printf-style format string matches the arguments,
+/// stores the format string in a special section, and calculates the string's
+/// token at compile time.
// clang-format off
#define PW_TOKENIZE_FORMAT_STRING(domain, mask, format, ...) \
if (0) { /* Do not execute to prevent double evaluation of the arguments. */ \
diff --git a/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler.h b/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler.h
deleted file mode 100644
index ca7e0b946..000000000
--- a/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler.h
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-#pragma once
-
-#include <stddef.h>
-#include <stdint.h>
-
-#include "pw_preprocessor/util.h"
-#include "pw_tokenizer/tokenize.h"
-
-// Encodes a tokenized string and arguments to a buffer on the stack. The buffer
-// is passed to the user-defined pw_tokenizer_HandleEncodedMessage function. The
-// size of the stack-allocated argument encoding buffer is set with the
-// PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES option.
-//
-// The macro's arguments are equivalent to the following function signature:
-//
-// TokenizeToGlobalHandler(const char* format,
-// ...); /* printf-style arguments */
-//
-// For example, the following encodes a tokenized string with a value returned
-// from a function call. The encoded message is passed to the caller-defined
-// pw_tokenizer_HandleEncodedMessage function.
-//
-// void OutputLastReadSize() {
-// PW_TOKENIZE_TO_GLOBAL_HANDLER("Read %u bytes", ReadSizeBytes());
-// }
-//
-// void pw_tokenizer_HandleEncodedMessage(const uint8_t encoded_message[],
-// size_t size_bytes) {
-// MyProject_EnqueueMessageForUart(buffer, size_bytes);
-// }
-//
-#define PW_TOKENIZE_TO_GLOBAL_HANDLER(format, ...) \
- PW_TOKENIZE_TO_GLOBAL_HANDLER_DOMAIN( \
- PW_TOKENIZER_DEFAULT_DOMAIN, format, __VA_ARGS__)
-
-// Same as PW_TOKENIZE_TO_GLOBAL_HANDLER, but tokenizes to the specified domain.
-#define PW_TOKENIZE_TO_GLOBAL_HANDLER_DOMAIN(domain, format, ...) \
- PW_TOKENIZE_TO_GLOBAL_HANDLER_MASK(domain, UINT32_MAX, format, __VA_ARGS__)
-
-// Same as PW_TOKENIZE_TO_GLOBAL_HANDLER_DOMAIN, but applies a mask to the
-// token.
-#define PW_TOKENIZE_TO_GLOBAL_HANDLER_MASK(domain, mask, format, ...) \
- do { \
- PW_TOKENIZE_FORMAT_STRING(domain, mask, format, __VA_ARGS__); \
- _pw_tokenizer_ToGlobalHandler(_pw_tokenizer_token, \
- PW_TOKENIZER_ARG_TYPES(__VA_ARGS__) \
- PW_COMMA_ARGS(__VA_ARGS__)); \
- } while (0)
-
-PW_EXTERN_C_START
-
-// This function must be defined by the pw_tokenizer:global_handler backend.
-// This function is called with the encoded message by
-// _pw_tokenizer_ToGlobalHandler.
-void pw_tokenizer_HandleEncodedMessage(const uint8_t encoded_message[],
- size_t size_bytes);
-
-// This function encodes the tokenized strings. Do not call it directly;
-// instead, use the PW_TOKENIZE_TO_GLOBAL_HANDLER macro.
-void _pw_tokenizer_ToGlobalHandler(pw_tokenizer_Token token,
- pw_tokenizer_ArgTypes types,
- ...);
-
-PW_EXTERN_C_END
diff --git a/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h b/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h
index 1faace319..175f71e5c 100644
--- a/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h
+++ b/pw_tokenizer/public/pw_tokenizer/tokenize_to_global_handler_with_payload.h
@@ -17,69 +17,18 @@
#include <stdint.h>
#include "pw_preprocessor/util.h"
-#include "pw_tokenizer/tokenize.h"
-
-// Like PW_TOKENIZE_TO_GLOBAL_HANDLER, encodes a tokenized string and arguments
-// to a buffer on the stack. The macro adds a payload argument, which is passed
-// through to the global handler function
-// pw_tokenizer_HandleEncodedMessageWithPayload, which must be defined by the
-// user of pw_tokenizer. The payload is a uintptr_t.
-//
-// For example, the following tokenizes a log string and passes the log level as
-// the payload.
-/*
- #define LOG_ERROR(...) \
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(kLogLevelError, __VA_ARGS__)
-
- void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload log_level,
- const uint8_t encoded_message[],
- size_t size_bytes) {
- if (log_level >= kLogLevelWarning) {
- MyProject_EnqueueMessageForUart(buffer, size_bytes);
- }
- }
- */
-#define PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(payload, format, ...) \
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD_DOMAIN( \
- PW_TOKENIZER_DEFAULT_DOMAIN, payload, format, __VA_ARGS__)
-
-// Same as PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD, but tokenizes to the
-// specified domain.
-#define PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD_DOMAIN( \
- domain, payload, format, ...) \
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD_MASK( \
- domain, UINT32_MAX, payload, format, __VA_ARGS__)
-
-// Same as PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD_DOMAIN, but applies a mask
-// to the token.
-#define PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD_MASK( \
- domain, mask, payload, format, ...) \
- do { \
- PW_TOKENIZE_FORMAT_STRING(domain, mask, format, __VA_ARGS__); \
- _pw_tokenizer_ToGlobalHandlerWithPayload( \
- payload, \
- _pw_tokenizer_token, \
- PW_TOKENIZER_ARG_TYPES(__VA_ARGS__) PW_COMMA_ARGS(__VA_ARGS__)); \
- } while (0)
PW_EXTERN_C_START
-typedef uintptr_t pw_tokenizer_Payload;
+// This typedef is deprecated. Use uint32_t instead.
+typedef uint32_t pw_tokenizer_Payload;
-// This function must be defined pw_tokenizer:global_handler_with_payload
-// backend. This function is called with the encoded message by
-// pw_tokenizer_ToGlobalHandler and a caller-provided payload argument.
+// This function is deprecated. For use with pw_log_tokenized, call
+// pw_log_tokenized_HandleLog. For other uses, implement a pw_tokenizer macro
+// that calls a custom handler.
void pw_tokenizer_HandleEncodedMessageWithPayload(
pw_tokenizer_Payload payload,
const uint8_t encoded_message[],
size_t size_bytes);
-// This function encodes the tokenized strings. Do not call it directly;
-// instead, use the PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD macro.
-void _pw_tokenizer_ToGlobalHandlerWithPayload(pw_tokenizer_Payload payload,
- pw_tokenizer_Token token,
- pw_tokenizer_ArgTypes types,
- ...);
-
PW_EXTERN_C_END
diff --git a/pw_tokenizer/pw_tokenizer_linker_rules.ld b/pw_tokenizer/pw_tokenizer_linker_rules.ld
new file mode 100644
index 000000000..d1f0aa796
--- /dev/null
+++ b/pw_tokenizer/pw_tokenizer_linker_rules.ld
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2020 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/*
+ * This file is separate from pw_tokenizer_linker_sections.ld because Zephyr
+ * already defines the top level SECTIONS label and requires new linker
+ * scripts to only add the individual sections.
+ */
+
+/*
+ * This section stores metadata that may be used during tokenized string
+ * decoding. This metadata describes properties that may affect how the
+ * tokenized string is encoded or decoded -- the maximum length of the hash
+ * function and the sizes of certain integer types.
+ *
+ * Metadata is declared as key-value pairs. See the metadata variable in
+ * tokenize.cc for further details.
+ */
+.pw_tokenizer.info 0x0 (INFO) :
+{
+ KEEP(*(.pw_tokenizer.info))
+}
+
+/*
+ * Tokenized string entries are stored in this section. Each entry contains
+ * the original string literal and the calculated token that represents it. In
+ * the compiled code, the token and a compact argument list encoded in a
+ * uint32_t are used in place of the format string. The compiled code
+ * contains no references to the tokenized string entries in this section.
+ *
+ * The tokenized string entry format is specified by the
+ * pw::tokenizer::internal::Entry class in
+ * pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h.
+ *
+ * The section contents are declared with KEEP so that they are not removed
+ * from the ELF. These are never emitted in the final binary or loaded into
+ * memory.
+ */
+.pw_tokenizer.entries 0x0 (INFO) :
+{
+ KEEP(*(.pw_tokenizer.entries.*))
+}
diff --git a/pw_tokenizer/pw_tokenizer_linker_sections.ld b/pw_tokenizer/pw_tokenizer_linker_sections.ld
index ae17f47cd..a48a804cc 100644
--- a/pw_tokenizer/pw_tokenizer_linker_sections.ld
+++ b/pw_tokenizer/pw_tokenizer_linker_sections.ld
@@ -34,37 +34,5 @@
SECTIONS
{
- /*
- * This section stores metadata that may be used during tokenized string
- * decoding. This metadata describes properties that may affect how the
- * tokenized string is encoded or decoded -- the maximum length of the hash
- * function and the sizes of certain integer types.
- *
- * Metadata is declared as key-value pairs. See the metadata variable in
- * tokenize.cc for further details.
- */
- .pw_tokenizer.info 0x0 (INFO) :
- {
- KEEP(*(.pw_tokenizer.info))
- }
-
- /*
- * Tokenized string entries are stored in this section. Each entry contains
- * the original string literal and the calculated token that represents it. In
- * the compiled code, the token and a compact argument list encoded in a
- * uint32_t are used in place of the format string. The compiled code
- * contains no references to the tokenized string entries in this section.
- *
- * The tokenized string entry format is specified by the
- * pw::tokenizer::internal::Entry class in
- * pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h.
- *
- * The section contents are declared with KEEP so that they are not removed
- * from the ELF. These are never emitted in the final binary or loaded into
- * memory.
- */
- .pw_tokenizer.entries 0x0 (INFO) :
- {
- KEEP(*(.pw_tokenizer.entries.*))
- }
+ INCLUDE pw_tokenizer_linker_rules.ld
}
diff --git a/pw_tokenizer/pw_tokenizer_private/tokenize_test.h b/pw_tokenizer/pw_tokenizer_private/tokenize_test.h
index 3c4b4633f..e65ceb610 100644
--- a/pw_tokenizer/pw_tokenizer_private/tokenize_test.h
+++ b/pw_tokenizer/pw_tokenizer_private/tokenize_test.h
@@ -40,8 +40,4 @@ void pw_tokenizer_ToCallbackTest_SequentialZigZag(
void pw_tokenizer_ToBufferTest_Requires8(void* buffer, size_t* buffer_size);
-void pw_tokenizer_ToGlobalHandlerTest_SequentialZigZag(void);
-
-void pw_tokenizer_ToGlobalHandlerWithPayloadTest_SequentialZigZag(void);
-
PW_EXTERN_C_END
diff --git a/pw_tokenizer/py/BUILD.bazel b/pw_tokenizer/py/BUILD.bazel
new file mode 100644
index 000000000..540495a66
--- /dev/null
+++ b/pw_tokenizer/py/BUILD.bazel
@@ -0,0 +1,156 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_python//python:defs.bzl", "py_binary")
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "pw_tokenizer",
+ srcs = [
+ "pw_tokenizer/__init__.py",
+ "pw_tokenizer/database.py",
+ "pw_tokenizer/decode.py",
+ "pw_tokenizer/detokenize.py",
+ "pw_tokenizer/elf_reader.py",
+ "pw_tokenizer/encode.py",
+ "pw_tokenizer/parse_message.py",
+ "pw_tokenizer/proto/__init__.py",
+ "pw_tokenizer/serial_detokenizer.py",
+ "pw_tokenizer/tokens.py",
+ ],
+ deps = [
+ "//pw_cli/py:pw_cli",
+ ],
+)
+
+py_binary(
+ name = "detokenize",
+ srcs = [
+ "pw_tokenizer/__main__.py",
+ ],
+ main = "pw_tokenizer/__main__.py",
+ deps = [":pw_tokenizer"],
+)
+
+# This test attempts to directly access files in the source tree, which is
+# incompatible with sandboxing.
+# TODO(b/241307309): Fix this test.
+filegroup(
+ name = "database_test",
+ srcs = ["database_test.py"],
+ # deps = [":pw_tokenizer"],
+)
+
+py_test(
+ name = "decode_test",
+ srcs = [
+ "decode_test.py",
+ "tokenized_string_decoding_test_data.py",
+ "varint_test_data.py",
+ ],
+ deps = [":pw_tokenizer"],
+)
+
+# This test can't be built in bazel because it depends on detokenize_proto_test_pb2.
+filegroup(
+ name = "detokenize_proto_test",
+ srcs = [
+ "detokenize_proto_test.py",
+ ],
+ # deps = [
+ # ":pw_tokenizer",
+ # ":detokenize_proto_test_pb2",
+ # ],
+)
+
+proto_library(
+ name = "detokenize_proto_test_proto",
+ srcs = ["detokenize_proto_test.proto"],
+ deps = [
+ "//pw_tokenizer:tokenizer_proto",
+ ],
+)
+
+# TODO(b/241456982): This target can't be built due to limitations of
+# py_proto_library.
+# py_proto_library(
+# name = "detokenize_proto_test_pb2",
+# srcs = ["detokenize_proto_test.proto"],
+# deps = [
+# "//pw_tokenizer:tokenizer_pb2",
+# ],
+#)
+
+py_test(
+ name = "detokenize_test",
+ srcs = ["detokenize_test.py"],
+ data = [
+ "example_binary_with_tokenized_strings.elf",
+ ],
+ deps = [
+ ":pw_tokenizer",
+ "@rules_python//python/runfiles",
+ ],
+)
+
+py_test(
+ name = "elf_reader_test",
+ srcs = ["elf_reader_test.py"],
+ data = [
+ "elf_reader_test_binary.elf",
+ ],
+ deps = [
+ ":pw_tokenizer",
+ "@rules_python//python/runfiles",
+ ],
+)
+
+# Executable for generating a test ELF file for elf_reader_test.py. A host
+# version of this binary is checked in for use in elf_reader_test.py.
+# Commented out because it fails to compile with bazel, with the error
+# ld.lld: error: symbol 'main' has no type. Instead use a filegroup to
+# keep pw_presubmit happy.
+# cc_binary(
+# name = "elf_reader_test_binary",
+# srcs = [
+# "py/elf_reader_test_binary.c",
+# ],
+# linkopts = ["-Wl,--unresolved-symbols=ignore-all"], # main is not defined
+# deps = [
+# ":pw_tokenizer",
+# "//pw_varint",
+# ],
+# )
+filegroup(
+ name = "elf_reader_test_binary",
+ srcs = [
+ "elf_reader_test_binary.c",
+ ],
+)
+
+py_test(
+ name = "encode_test",
+ srcs = [
+ "encode_test.py",
+ "varint_test_data.py",
+ ],
+ deps = [":pw_tokenizer"],
+)
+
+py_test(
+ name = "tokens_test",
+ srcs = ["tokens_test.py"],
+ deps = [":pw_tokenizer"],
+)
diff --git a/pw_tokenizer/py/BUILD.gn b/pw_tokenizer/py/BUILD.gn
index 2ac30448c..18f19bb49 100644
--- a/pw_tokenizer/py/BUILD.gn
+++ b/pw_tokenizer/py/BUILD.gn
@@ -60,10 +60,15 @@ pw_python_package("py") {
inputs = [
"elf_reader_test_binary.elf",
"example_binary_with_tokenized_strings.elf",
- "example_legacy_binary_with_tokenized_strings.elf",
]
proto_library = "..:proto"
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+
+ python_deps = [
+ "$dir_pw_cli/py",
+ "$dir_pw_protobuf_compiler:test_protos.python",
+ ]
}
# This setup.py may be used to install pw_tokenizer without GN. It does not
diff --git a/pw_tokenizer/py/database_test.py b/pw_tokenizer/py/database_test.py
index 74cf9951d..dfa942cd2 100755
--- a/pw_tokenizer/py/database_test.py
+++ b/pw_tokenizer/py/database_test.py
@@ -16,8 +16,11 @@
import json
import io
+import os
from pathlib import Path
import shutil
+import stat
+import subprocess
import sys
import tempfile
import unittest
@@ -31,10 +34,9 @@ from pw_tokenizer import database
#
# arm-none-eabi-objcopy -S --only-section ".pw_tokenize*" <ELF> <OUTPUT>
#
-TOKENIZED_ENTRIES_ELF = Path(
- __file__).parent / 'example_binary_with_tokenized_strings.elf'
-LEGACY_PLAIN_STRING_ELF = Path(
- __file__).parent / 'example_legacy_binary_with_tokenized_strings.elf'
+TOKENIZED_ENTRIES_ELF = (
+ Path(__file__).parent / 'example_binary_with_tokenized_strings.elf'
+)
CSV_DEFAULT_DOMAIN = '''\
00000000, ,""
@@ -124,15 +126,15 @@ EXPECTED_REPORT = {
'present_size_bytes': 289,
'total_entries': 22,
'total_size_bytes': 289,
- 'collisions': {}
+ 'collisions': {},
},
'TEST_DOMAIN': {
'present_entries': 5,
'present_size_bytes': 57,
'total_entries': 5,
'total_size_bytes': 57,
- 'collisions': {}
- }
+ 'collisions': {},
+ },
}
}
@@ -158,37 +160,54 @@ def _mock_output() -> io.TextIOWrapper:
return io.TextIOWrapper(output, write_through=True)
+def _remove_readonly( # pylint: disable=unused-argument
+ func, path, excinfo
+) -> None:
+ """Changes file permission and recalls the calling function."""
+ print('Path attempted to be deleted:', path)
+ if not os.access(path, os.W_OK):
+ # Change file permissions.
+ os.chmod(path, stat.S_IWUSR)
+ # Call the calling function again.
+ func(path)
+
+
class DatabaseCommandLineTest(unittest.TestCase):
"""Tests the database.py command line interface."""
- def setUp(self):
+
+ def setUp(self) -> None:
self._dir = Path(tempfile.mkdtemp('_pw_tokenizer_test'))
self._csv = self._dir / 'db.csv'
self._elf = TOKENIZED_ENTRIES_ELF
self._csv_test_domain = CSV_TEST_DOMAIN
- def tearDown(self):
+ def tearDown(self) -> None:
shutil.rmtree(self._dir)
- def test_create_csv(self):
+ def test_create_csv(self) -> None:
run_cli('create', '--database', self._csv, self._elf)
- self.assertEqual(CSV_DEFAULT_DOMAIN.splitlines(),
- self._csv.read_text().splitlines())
+ self.assertEqual(
+ CSV_DEFAULT_DOMAIN.splitlines(), self._csv.read_text().splitlines()
+ )
- def test_create_csv_test_domain(self):
+ def test_create_csv_test_domain(self) -> None:
run_cli('create', '--database', self._csv, f'{self._elf}#TEST_DOMAIN')
- self.assertEqual(self._csv_test_domain.splitlines(),
- self._csv.read_text().splitlines())
+ self.assertEqual(
+ self._csv_test_domain.splitlines(),
+ self._csv.read_text().splitlines(),
+ )
- def test_create_csv_all_domains(self):
+ def test_create_csv_all_domains(self) -> None:
run_cli('create', '--database', self._csv, f'{self._elf}#.*')
- self.assertEqual(CSV_ALL_DOMAINS.splitlines(),
- self._csv.read_text().splitlines())
+ self.assertEqual(
+ CSV_ALL_DOMAINS.splitlines(), self._csv.read_text().splitlines()
+ )
- def test_create_force(self):
+ def test_create_force(self) -> None:
self._csv.write_text(CSV_ALL_DOMAINS)
with self.assertRaises(FileExistsError):
@@ -196,17 +215,18 @@ class DatabaseCommandLineTest(unittest.TestCase):
run_cli('create', '--force', '--database', self._csv, self._elf)
- def test_create_binary(self):
+ def test_create_binary(self) -> None:
binary = self._dir / 'db.bin'
run_cli('create', '--type', 'binary', '--database', binary, self._elf)
# Write the binary database as CSV to verify its contents.
run_cli('create', '--database', self._csv, binary)
- self.assertEqual(CSV_DEFAULT_DOMAIN.splitlines(),
- self._csv.read_text().splitlines())
+ self.assertEqual(
+ CSV_DEFAULT_DOMAIN.splitlines(), self._csv.read_text().splitlines()
+ )
- def test_add_does_not_recalculate_tokens(self):
+ def test_add_does_not_recalculate_tokens(self) -> None:
db_with_custom_token = '01234567, ,"hello"'
to_add = self._dir / 'add_this.csv'
@@ -214,97 +234,371 @@ class DatabaseCommandLineTest(unittest.TestCase):
self._csv.touch()
run_cli('add', '--database', self._csv, to_add)
- self.assertEqual(db_with_custom_token.splitlines(),
- self._csv.read_text().splitlines())
+ self.assertEqual(
+ db_with_custom_token.splitlines(),
+ self._csv.read_text().splitlines(),
+ )
- def test_mark_removed(self):
+ def test_mark_removed(self) -> None:
self._csv.write_text(CSV_ALL_DOMAINS)
- run_cli('mark_removed', '--database', self._csv, '--date',
- '1998-09-04', self._elf)
+ run_cli(
+ 'mark_removed',
+ '--database',
+ self._csv,
+ '--date',
+ '1998-09-04',
+ self._elf,
+ )
# Add the removal date to the four tokens not in the default domain
new_csv = CSV_ALL_DOMAINS
- new_csv = new_csv.replace('17fa86d3, ,"hello"',
- '17fa86d3,1998-09-04,"hello"')
- new_csv = new_csv.replace('18c5017c, ,"yes"',
- '18c5017c,1998-09-04,"yes"')
- new_csv = new_csv.replace('59b2701c, ,"The answer was: %s"',
- '59b2701c,1998-09-04,"The answer was: %s"')
- new_csv = new_csv.replace('d18ada0f, ,"something"',
- 'd18ada0f,1998-09-04,"something"')
+ new_csv = new_csv.replace(
+ '17fa86d3, ,"hello"', '17fa86d3,1998-09-04,"hello"'
+ )
+ new_csv = new_csv.replace(
+ '18c5017c, ,"yes"', '18c5017c,1998-09-04,"yes"'
+ )
+ new_csv = new_csv.replace(
+ '59b2701c, ,"The answer was: %s"',
+ '59b2701c,1998-09-04,"The answer was: %s"',
+ )
+ new_csv = new_csv.replace(
+ 'd18ada0f, ,"something"', 'd18ada0f,1998-09-04,"something"'
+ )
self.assertNotEqual(CSV_ALL_DOMAINS, new_csv)
- self.assertEqual(new_csv.splitlines(),
- self._csv.read_text().splitlines())
+ self.assertEqual(
+ new_csv.splitlines(), self._csv.read_text().splitlines()
+ )
- def test_purge(self):
+ def test_purge(self) -> None:
self._csv.write_text(CSV_ALL_DOMAINS)
# Mark everything not in TEST_DOMAIN as removed.
- run_cli('mark_removed', '--database', self._csv,
- f'{self._elf}#TEST_DOMAIN')
+ run_cli(
+ 'mark_removed', '--database', self._csv, f'{self._elf}#TEST_DOMAIN'
+ )
# Delete all entries except those in TEST_DOMAIN.
run_cli('purge', '--database', self._csv)
- self.assertEqual(self._csv_test_domain.splitlines(),
- self._csv.read_text().splitlines())
+ self.assertEqual(
+ self._csv_test_domain.splitlines(),
+ self._csv.read_text().splitlines(),
+ )
@mock.patch('sys.stdout', new_callable=_mock_output)
- def test_report(self, mock_stdout):
+ def test_report(self, mock_stdout) -> None:
run_cli('report', self._elf)
- self.assertEqual(json.loads(mock_stdout.buffer.getvalue()),
- EXPECTED_REPORT)
+ self.assertEqual(
+ json.loads(mock_stdout.buffer.getvalue()), EXPECTED_REPORT
+ )
- def test_replace(self):
+ def test_replace(self) -> None:
sub = 'replace/ment'
- run_cli('create', '--database', self._csv, self._elf, '--replace',
- r'(?i)\b[jh]ello\b/' + sub)
+ run_cli(
+ 'create',
+ '--database',
+ self._csv,
+ self._elf,
+ '--replace',
+ r'(?i)\b[jh]ello\b/' + sub,
+ )
self.assertEqual(
CSV_DEFAULT_DOMAIN.replace('Jello', sub).replace('Hello', sub),
- self._csv.read_text())
+ self._csv.read_text(),
+ )
- def test_json_strings(self):
+ def test_json_strings(self) -> None:
strings_file = self._dir / "strings.json"
with open(strings_file, 'w') as file:
file.write(JSON_SOURCE_STRINGS)
run_cli('create', '--force', '--database', self._csv, strings_file)
- self.assertEqual(CSV_STRINGS.splitlines(),
- self._csv.read_text().splitlines())
+ self.assertEqual(
+ CSV_STRINGS.splitlines(), self._csv.read_text().splitlines()
+ )
-class LegacyDatabaseCommandLineTest(DatabaseCommandLineTest):
- """Test an ELF with the legacy plain string storage format."""
- def setUp(self):
- super().setUp()
- self._elf = LEGACY_PLAIN_STRING_ELF
+class TestDirectoryDatabaseCommandLine(unittest.TestCase):
+ """Tests the directory database command line interface."""
- # The legacy approach for storing tokenized strings in an ELF always
- # adds an entry for "", even if the empty string was never tokenized.
- self._csv_test_domain = '00000000, ,""\n' + CSV_TEST_DOMAIN
+ def setUp(self) -> None:
+ self._dir = Path(tempfile.mkdtemp('_pw_tokenizer_test'))
+ self._db_dir = self._dir / '_dir_database_test'
+ self._db_dir.mkdir(exist_ok=True)
+ self._db_csv = self._db_dir / '8123913.pw_tokenizer.csv'
+ self._elf = TOKENIZED_ENTRIES_ELF
+ self._csv_test_domain = CSV_TEST_DOMAIN
- @mock.patch('sys.stdout', new_callable=_mock_output)
- def test_report(self, mock_stdout):
- run_cli('report', self._elf)
+ def _git(self, *command: str) -> None:
+ """Runs git in self._dir with forced user name and email values.
+
+ Prevents accidentally running git in the wrong directory and avoids
+ errors if the name and email are not configured.
+ """
+ subprocess.run(
+ [
+ 'git',
+ '-c',
+ 'user.name=pw_tokenizer tests',
+ '-c',
+ 'user.email=noreply@google.com',
+ *command,
+ ],
+ cwd=self._dir,
+ check=True,
+ )
+
+ def tearDown(self) -> None:
+ shutil.rmtree(self._dir, onerror=_remove_readonly)
+
+ def test_add_csv_to_dir(self) -> None:
+ """Tests a CSV can be created within the database."""
+ run_cli('add', '--database', self._db_dir, f'{self._elf}#TEST_DOMAIN')
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ self._db_csv = directory.pop()
- report = EXPECTED_REPORT[str(TOKENIZED_ENTRIES_ELF)].copy()
+ self.assertEqual(
+ self._csv_test_domain.splitlines(),
+ self._db_csv.read_text().splitlines(),
+ )
+
+ def test_add_all_domains_to_dir(self) -> None:
+ """Tests a CSV with all domains can be added to the database."""
+ run_cli('add', '--database', self._db_dir, f'{self._elf}#.*')
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ self._db_csv = directory.pop()
+
+ self.assertEqual(
+ CSV_ALL_DOMAINS.splitlines(), self._db_csv.read_text().splitlines()
+ )
- # Count the implicitly added "" entry in TEST_DOMAIN.
- report['TEST_DOMAIN']['present_entries'] += 1
- report['TEST_DOMAIN']['present_size_bytes'] += 1
- report['TEST_DOMAIN']['total_entries'] += 1
- report['TEST_DOMAIN']['total_size_bytes'] += 1
+ def test_not_adding_existing_tokens(self) -> None:
+ """Tests duplicate tokens are not added to the database."""
+ run_cli('add', '--database', self._db_dir, f'{self._elf}#TEST_DOMAIN')
+ run_cli('add', '--database', self._db_dir, f'{self._elf}#TEST_DOMAIN')
+ directory = list(self._db_dir.iterdir())
- # Rename "" to the legacy name "default"
- report['default'] = report['']
- del report['']
+ self.assertEqual(1, len(directory))
- self.assertEqual({str(LEGACY_PLAIN_STRING_ELF): report},
- json.loads(mock_stdout.buffer.getvalue()))
+ self._db_csv = directory.pop()
+
+ self.assertEqual(
+ self._csv_test_domain.splitlines(),
+ self._db_csv.read_text().splitlines(),
+ )
+
+ def test_adding_tokens_without_git_repo(self):
+ """Tests creating new files with new entries when no repo exists."""
+ # Add CSV_TEST_DOMAIN to a new CSV in the directory database.
+ run_cli('add', '--database', self._db_dir, f'{self._elf}#TEST_DOMAIN')
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ first_csv_in_db = directory.pop()
+
+ self.assertEqual(
+ self._csv_test_domain.splitlines(),
+ first_csv_in_db.read_text().splitlines(),
+ )
+ # Add CSV_ALL_DOMAINS to a new CSV in the directory database.
+ run_cli('add', '--database', self._db_dir, f'{self._elf}#.*')
+ directory = list(self._db_dir.iterdir())
+ # Assert two different CSVs were created to store new tokens.
+ self.assertEqual(2, len(directory))
+ # Retrieve the other CSV in the directory.
+ second_csv_in_db = (
+ directory[0] if directory[0] != first_csv_in_db else directory[1]
+ )
+
+ self.assertNotEqual(first_csv_in_db, second_csv_in_db)
+ self.assertEqual(
+ self._csv_test_domain.splitlines(),
+ first_csv_in_db.read_text().splitlines(),
+ )
+
+ # Retrieve entries that exclusively exist in CSV_ALL_DOMAINS
+ # as CSV_ALL_DOMAINS contains all entries in TEST_DOMAIN.
+ entries_exclusively_in_all_domain = set(
+ CSV_ALL_DOMAINS.splitlines()
+ ) - set(self._csv_test_domain.splitlines())
+ # Ensure only new tokens not in CSV_TEST_DOMAIN were added to
+ # the second CSV added to the directory database.
+ self.assertEqual(
+ entries_exclusively_in_all_domain,
+ set(second_csv_in_db.read_text().splitlines()),
+ )
+
+ def test_untracked_files_in_dir(self):
+ """Tests untracked CSVs are reused by the database."""
+ self._git('init')
+ # Add CSV_TEST_DOMAIN to a new CSV in the directory database.
+ run_cli(
+ 'add',
+ '--database',
+ self._db_dir,
+ '--discard-temporary',
+ 'HEAD',
+ f'{self._elf}#TEST_DOMAIN',
+ )
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ first_path_in_db = directory.pop()
+
+ self.assertEqual(
+ self._csv_test_domain.splitlines(),
+ first_path_in_db.read_text().splitlines(),
+ )
+ # Retrieve the untracked CSV in the Git repository and discard
+ # tokens that do not exist in CSV_DEFAULT_DOMAIN.
+ run_cli(
+ 'add',
+ '--database',
+ self._db_dir,
+ '--discard-temporary',
+ 'HEAD',
+ self._elf,
+ )
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ reused_path_in_db = directory.pop()
+ # Ensure the first path created is the same being reused. Also,
+ # the CSV content is the same as CSV_DEFAULT_DOMAIN.
+ self.assertEqual(first_path_in_db, reused_path_in_db)
+ self.assertEqual(
+ CSV_DEFAULT_DOMAIN.splitlines(),
+ reused_path_in_db.read_text().splitlines(),
+ )
+
+ def test_adding_multiple_elf_files(self) -> None:
+ """Tests adding multiple elf files to a file in the database."""
+ # Add CSV_TEST_DOMAIN to a new CSV in the directory database.
+ run_cli(
+ 'add',
+ '--database',
+ self._db_dir,
+ f'{self._elf}#TEST_DOMAIN',
+ self._elf,
+ )
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+ # Combines CSV_DEFAULT_DOMAIN and TEST_DOMAIN into a unique set
+ # of token entries.
+ entries_from_default_and_test_domain = set(
+ CSV_DEFAULT_DOMAIN.splitlines()
+ ).union(set(self._csv_test_domain.splitlines()))
+ # Multiple ELF files were added at once to a single CSV.
+ self.assertEqual(
+ entries_from_default_and_test_domain,
+ set(directory.pop().read_text().splitlines()),
+ )
+
+ def test_discarding_old_entries(self) -> None:
+ """Tests discarding old entries for new entries when re-adding."""
+ self._git('init')
+ # Add CSV_ALL_DOMAINS to a new CSV in the directory database.
+ run_cli(
+ 'add',
+ '--database',
+ self._db_dir,
+ '--discard-temporary',
+ 'HEAD',
+ f'{self._elf}#.*',
+ )
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ untracked_path_in_db = directory.pop()
+
+ self.assertEqual(
+ CSV_ALL_DOMAINS.splitlines(),
+ untracked_path_in_db.read_text().splitlines(),
+ )
+ # Add CSV_DEFAULT_DOMAIN and CSV_TEST_DOMAIN to a CSV in the
+ # directory database, while replacing entries in CSV_ALL_DOMAINS
+ # that no longer exist.
+ run_cli(
+ 'add',
+ '--database',
+ self._db_dir,
+ '--discard-temporary',
+ 'HEAD',
+ f'{self._elf}#TEST_DOMAIN',
+ self._elf,
+ )
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ reused_path_in_db = directory.pop()
+ # Combines CSV_DEFAULT_DOMAIN and TEST_DOMAIN.
+ entries_from_default_and_test_domain = set(
+ CSV_DEFAULT_DOMAIN.splitlines()
+ ).union(set(self._csv_test_domain.splitlines()))
+
+ self.assertEqual(untracked_path_in_db, reused_path_in_db)
+ self.assertEqual(
+ entries_from_default_and_test_domain,
+ set(reused_path_in_db.read_text().splitlines()),
+ )
+
+ def test_retrieving_csv_from_commit(self) -> None:
+ """Tests retrieving a CSV from a commit and removing temp tokens."""
+ self._git('init')
+ self._git('commit', '--allow-empty', '-m', 'First Commit')
+ # Add CSV_ALL_DOMAINS to a new CSV in the directory database.
+ run_cli('add', '--database', self._db_dir, f'{self._elf}#.*')
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ tracked_path_in_db = directory.pop()
+
+ self.assertEqual(
+ CSV_ALL_DOMAINS.splitlines(),
+ tracked_path_in_db.read_text().splitlines(),
+ )
+ # Commit the CSV to avoid retrieving the CSV with the checks
+ # for untracked changes.
+ self._git('add', '--all')
+ self._git('commit', '-m', 'Adding a CSV to a new commit.')
+ # Retrieve the CSV in HEAD~ and discard tokens that exist in
+ # CSV_ALL_DOMAINS and not exist in CSV_TEST_DOMAIN.
+ run_cli(
+ 'add',
+ '--database',
+ self._db_dir,
+ '--discard-temporary',
+ 'HEAD~2',
+ f'{self._elf}#TEST_DOMAIN',
+ )
+ directory = list(self._db_dir.iterdir())
+
+ self.assertEqual(1, len(directory))
+
+ reused_path_in_db = directory.pop()
+
+ self.assertEqual(
+ self._csv_test_domain.splitlines(),
+ reused_path_in_db.read_text().splitlines(),
+ )
if __name__ == '__main__':
diff --git a/pw_tokenizer/py/decode_test.py b/pw_tokenizer/py/decode_test.py
index be08eb824..fe20fd80c 100755
--- a/pw_tokenizer/py/decode_test.py
+++ b/pw_tokenizer/py/decode_test.py
@@ -15,11 +15,13 @@
"""Tests the tokenized string decode module."""
from datetime import datetime
+import math
import unittest
import tokenized_string_decoding_test_data as tokenized_string
import varint_test_data
from pw_tokenizer import decode
+from pw_tokenizer import encode
def error(msg, value=None) -> str:
@@ -31,6 +33,7 @@ def error(msg, value=None) -> str:
class TestDecodeTokenized(unittest.TestCase):
"""Tests decoding tokenized strings with various arguments."""
+
def test_decode_generated_data(self) -> None:
self.assertGreater(len(tokenized_string.TEST_DATA), 100)
@@ -39,22 +42,33 @@ class TestDecodeTokenized(unittest.TestCase):
def test_unicode_decode_errors(self) -> None:
"""Tests unicode errors, which do not occur in the C++ decoding code."""
- self.assertEqual(decode.decode('Why, %c', b'\x01', True),
- 'Why, ' + error('%c ERROR', -1))
+ self.assertEqual(
+ decode.decode('Why, %c', b'\x01', True),
+ 'Why, ' + error('%c ERROR', -1),
+ )
self.assertEqual(
decode.decode('%sXY%+ldxy%u', b'\x83N\x80!\x01\x02', True),
- '{}XY{}xy{}'.format(error('%s ERROR', "'N\\x80!'"),
- error('%+ld SKIPPED', -1),
- error('%u SKIPPED', 1)))
+ '{}XY{}xy{}'.format(
+ error('%s ERROR', "'N\\x80!'"),
+ error('%+ld SKIPPED', -1),
+ error('%u SKIPPED', 1),
+ ),
+ )
self.assertEqual(
decode.decode('%s%lld%9u', b'\x82$\x80\x80', True),
- '{0}{1}{2}'.format(error("%s ERROR ('$\\x80')"),
- error('%lld SKIPPED'), error('%9u SKIPPED')))
+ '{0}{1}{2}'.format(
+ error("%s ERROR ('$\\x80')"),
+ error('%lld SKIPPED'),
+ error('%9u SKIPPED'),
+ ),
+ )
- self.assertEqual(decode.decode('%c', b'\xff\xff\xff\xff\x0f', True),
- error('%c ERROR', -2147483648))
+ self.assertEqual(
+ decode.decode('%c', b'\xff\xff\xff\xff\x0f', True),
+ error('%c ERROR', -2147483648),
+ )
def test_ignore_errors(self) -> None:
self.assertEqual(decode.decode('Why, %c', b'\x01'), 'Why, %c')
@@ -63,14 +77,654 @@ class TestDecodeTokenized(unittest.TestCase):
def test_pointer(self) -> None:
"""Tests pointer args, which are not natively supported in Python."""
- self.assertEqual(decode.decode('Hello: %p', b'\x00', True),
- 'Hello: 0x00000000')
- self.assertEqual(decode.decode('%p%d%d', b'\x02\x80', True),
- '0x00000001<[%d ERROR]><[%d SKIPPED]>')
+ self.assertEqual(
+ decode.decode('Hello: %p', b'\x00', True), 'Hello: 0x00000000'
+ )
+ self.assertEqual(
+ decode.decode('%p%d%d', b'\x02\x80', True),
+ '0x00000001<[%d ERROR]><[%d SKIPPED]>',
+ )
+
+ def test_nothing_printed_fails(self) -> None:
+ result = decode.FormatString('%n').format(b'')
+ self.assertFalse(result.ok())
+
+
+class TestPercentLiteralDecoding(unittest.TestCase):
+ """Tests decoding the %-literal in various invalid situations."""
+
+ def test_percent(self) -> None:
+ result = decode.FormatString('%%').format(b'')
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '%')
+ self.assertEqual(result.remaining, b'')
+
+ def test_percent_with_leading_plus_fails(self) -> None:
+ result = decode.FormatString('%+%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ def test_percent_with_leading_negative(self) -> None:
+ result = decode.FormatString('%-%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ def test_percent_with_leading_space(self) -> None:
+ result = decode.FormatString('% %').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ def test_percent_with_leading_hashtag(self) -> None:
+ result = decode.FormatString('%#%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+ def test_percent_with_leading_zero(self) -> None:
+ result = decode.FormatString('%0%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ def test_percent_with_length(self) -> None:
+ """Test that all length prefixes fail to decode with %."""
+
+ result = decode.FormatString('%hh%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%h%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%l%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ll%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+ result = decode.FormatString('%L%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%j%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%z%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%t%').format(b'')
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, b'')
+
+ def test_percent_with_width(self):
+ result = decode.FormatString('%9%').format(b'')
+ self.assertFalse(result.ok())
+
+ def test_percent_with_multidigit_width(self):
+ result = decode.FormatString('%10%').format(b'')
+ self.assertFalse(result.ok())
+
+ def test_percent_with_star_width(self):
+ result = decode.FormatString('%*%').format(b'')
+ self.assertFalse(result.ok())
+
+ def test_percent_with_precision(self):
+ result = decode.FormatString('%.5%').format(b'')
+ self.assertFalse(result.ok())
+
+ def test_percent_with_multidigit_precision(self):
+ result = decode.FormatString('%.10%').format(b'')
+ self.assertFalse(result.ok())
+
+ def test_percent_with_star_precision(self):
+ result = decode.FormatString('%.*%').format(b'')
+ self.assertFalse(result.ok())
+
+
+# pylint: disable=too-many-public-methods
class TestIntegerDecoding(unittest.TestCase):
"""Tests decoding variable-length integers."""
+
+ def test_signed_integer_d(self) -> None:
+ result = decode.FormatString('%d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_d_with_minus(self) -> None:
+ result = decode.FormatString('%-5d').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10 ')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_d_with_plus(self) -> None:
+ result = decode.FormatString('%+d').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_d_with_blank_space(self) -> None:
+ result = decode.FormatString('% d').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_d_with_plus_and_blank_space_ignores_blank_space(
+ self,
+ ) -> None:
+ result = decode.FormatString('%+ d').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('% +d').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_d_with_hashtag(self) -> None:
+ result = decode.FormatString('%#d').format(encode.encode_args(10))
+ self.assertFalse(result.ok())
+
+ def test_signed_integer_d_with_zero(self) -> None:
+ result = decode.FormatString('%05d').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '00010')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_d_with_length(self) -> None:
+ """Tests that length modifiers do not affect signed integer decoding."""
+ result = decode.FormatString('%hhd').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hd').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ld').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lld').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jd').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zd').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%td').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_d_with_width(self) -> None:
+ result = decode.FormatString('%5d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' -10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_d_with_width_and_0_flag(self) -> None:
+ result = decode.FormatString('%05d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-0010')
+
+ def test_signed_integer_d_with_multidigit_width(self) -> None:
+ result = decode.FormatString('%10d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' -10')
+
+ def test_signed_integer_d_with_star_width(self) -> None:
+ result = decode.FormatString('%*d').format(encode.encode_args(10, -10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' -10')
+
+ def test_signed_integer_d_with_missing_width_or_value(self) -> None:
+ result = decode.FormatString('%*d').format(encode.encode_args(-10))
+ self.assertFalse(result.ok())
+
+ def test_signed_integer_d_with_precision(self) -> None:
+ result = decode.FormatString('%.5d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-00010')
+
+ def test_signed_integer_d_with_multidigit_precision(self) -> None:
+ result = decode.FormatString('%.10d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-0000000010')
+
+ def test_signed_integer_d_with_star_precision(self) -> None:
+ result = decode.FormatString('%.*d').format(encode.encode_args(10, -10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-0000000010')
+
+ def test_signed_integer_d_with_zero_precision(self) -> None:
+ result = decode.FormatString('%.0d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+
+ def test_signed_integer_d_with_empty_precision(self) -> None:
+ result = decode.FormatString('%.d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+
+ def test_zero_with_zero_precision(self) -> None:
+ result = decode.FormatString('%.0d').format(encode.encode_args(0))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '')
+
+ def test_zero_with_empty_precision(self) -> None:
+ result = decode.FormatString('%.d').format(encode.encode_args(0))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '')
+
+ def test_signed_integer_d_with_width_and_precision(self) -> None:
+ result = decode.FormatString('%10.5d').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' -00010')
+
+ def test_signed_integer_d_with_star_width_and_precision(self) -> None:
+ result = decode.FormatString('%*.*d').format(
+ encode.encode_args(15, 10, -10)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' -0000000010')
+
+ def test_signed_integer_d_with_missing_precision_or_value(self) -> None:
+ result = decode.FormatString('%.*d').format(encode.encode_args(-10))
+ self.assertFalse(result.ok())
+
+ def test_64_bit_specifier_workaround(self) -> None:
+ result = decode.FormatString('%.u%.*lu%0*lu').format(
+ encode.encode_args(0, 0, 0, 0, 0)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%.u%.*lu%0*lu').format(
+ encode.encode_args(0, 0, 1, 9, 0)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '1000000000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%.u%.*lu%0*lu').format(
+ encode.encode_args(1, 9, 0, 9, 0)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '1000000000000000000')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_signed_integer_i(self) -> None:
+ result = decode.FormatString('%i').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_i_with_minus(self) -> None:
+ result = decode.FormatString('%-5i').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10 ')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_i_with_plus(self) -> None:
+ result = decode.FormatString('%+i').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_i_with_blank_space(self) -> None:
+ result = decode.FormatString('% i').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_i_with_plus_and_blank_space_ignores_blank_space(
+ self,
+ ) -> None:
+ result = decode.FormatString('%+ i').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('% +i').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_i_with_hashtag(self) -> None:
+ result = decode.FormatString('%#i').format(encode.encode_args(10))
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(10))
+
+ def test_signed_integer_i_with_zero(self) -> None:
+ result = decode.FormatString('%05i').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '00010')
+ self.assertEqual(result.remaining, b'')
+
+ def test_signed_integer_i_with_length(self) -> None:
+ """Tests that length modifiers do not affect signed integer decoding."""
+ result = decode.FormatString('%hhi').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hi').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%li').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lli').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ji').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zi').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ti').format(encode.encode_args(-10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '-10')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_unsigned_integer(self) -> None:
+ result = decode.FormatString('%u').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ def test_unsigned_integer_with_hashtag(self) -> None:
+ result = decode.FormatString('%#u').format(encode.encode_args(10))
+ self.assertFalse(result.ok())
+
+ def test_unsigned_integer_with_length(self) -> None:
+ """Tests that length modifiers pass unsigned integer decoding."""
+ result = decode.FormatString('%hhu').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hu').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lu').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llu').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ju').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zu').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%tu').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%Lu').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '10')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_octal_integer(self) -> None:
+ result = decode.FormatString('%o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_hashtag(self) -> None:
+ result = decode.FormatString('%#o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '012')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_hashtag_and_width(self) -> None:
+ result = decode.FormatString('%#10o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 012')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_hashtag_and_zero_and_width(self) -> None:
+ result = decode.FormatString('%#010o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0000000012')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_minus_and_hashtag(self) -> None:
+ result = decode.FormatString('%#-5o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '012 ')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_plus_and_hashtag(self) -> None:
+ result = decode.FormatString('%+#o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+012')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_space_and_hashtag(self) -> None:
+ result = decode.FormatString('% #o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 012')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_zero_and_hashtag(self) -> None:
+ result = decode.FormatString('%#05o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '00012')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_plus_and_space_and_hashtag_ignores_space(
+ self,
+ ) -> None:
+ result = decode.FormatString('%+ #o').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+012')
+ self.assertEqual(result.remaining, b'')
+
+ def test_octal_integer_with_length(self) -> None:
+ """Tests that length modifiers do not affect octal integer decoding."""
+ result = decode.FormatString('%hho').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ho').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lo').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llo').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jo').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zo').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%to').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%Lo').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '12')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_lowercase_hex_integer(self) -> None:
+ result = decode.FormatString('%x').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_hex_integer_with_hashtag(self) -> None:
+ result = decode.FormatString('%#x').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0xa')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_hex_integer_with_length(self) -> None:
+ """Tests that length modifiers do not affect lowercase hex decoding."""
+ result = decode.FormatString('%hhx').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hx').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lx').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llx').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jx').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zx').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%tx').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%Lx').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'a')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_uppercase_hex_integer(self) -> None:
+ result = decode.FormatString('%X').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ def test_uppercase_hex_integer_with_hashtag(self) -> None:
+ result = decode.FormatString('%#X').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0XA')
+ self.assertEqual(result.remaining, b'')
+
+ def test_uppercase_hex_integer_with_length(self) -> None:
+ """Tests that length modifiers do not affect uppercase hex decoding."""
+ result = decode.FormatString('%hhX').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hX').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lX').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llX').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jX').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zX').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%tX').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%LX').format(encode.encode_args(10))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'A')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
def test_decode_generated_data(self) -> None:
test_data = varint_test_data.TEST_DATA
self.assertGreater(len(test_data), 100)
@@ -78,17 +732,1028 @@ class TestIntegerDecoding(unittest.TestCase):
for signed_spec, signed, unsigned_spec, unsigned, encoded in test_data:
self.assertEqual(
int(signed),
- decode.FormatSpec.from_string(signed_spec).decode(
- bytearray(encoded)).value)
+ decode.FormatSpec.from_string(signed_spec)
+ .decode(bytearray(encoded))
+ .value,
+ )
self.assertEqual(
int(unsigned),
- decode.FormatSpec.from_string(unsigned_spec).decode(
- bytearray(encoded)).value)
+ decode.FormatSpec.from_string(unsigned_spec)
+ .decode(bytearray(encoded))
+ .value,
+ )
+
+
+# pylint: disable=too-many-public-methods
+class TestFloatDecoding(unittest.TestCase):
+ """Tests decoding floating-point values using f or F."""
+
+ def test_lowercase_float(self) -> None:
+ result = decode.FormatString('%f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_minus(self) -> None:
+ result = decode.FormatString('%-10f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000 ')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_plus(self) -> None:
+ result = decode.FormatString('%+f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_blank_space(self) -> None:
+ result = decode.FormatString('% f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_plus_and_blank_space_ignores_blank_space(
+ self,
+ ) -> None:
+ result = decode.FormatString('%+ f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('% +f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_hashtag(self) -> None:
+ result = decode.FormatString('%.0f').format(encode.encode_args(2.0))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%#.0f').format(encode.encode_args(2.0))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_zero(self) -> None:
+ result = decode.FormatString('%010f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '002.200000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_length(self) -> None:
+ """Tests that length modifiers do not affect f decoding."""
+ result = decode.FormatString('%hhf').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hf').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lf').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llf').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jf').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zf').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%tf').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%Lf').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_width(self) -> None:
+ result = decode.FormatString('%9f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 2.200000')
+
+ def test_lowercase_float_with_multidigit_width(self) -> None:
+ result = decode.FormatString('%10f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 2.200000')
+
+ def test_lowercase_float_with_star_width(self) -> None:
+ result = decode.FormatString('%*f').format(encode.encode_args(10, 2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 2.200000')
+
+ def test_lowercase_float_non_number(self) -> None:
+ result = decode.FormatString('%f').format(encode.encode_args(math.inf))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'inf')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_with_precision(self) -> None:
+ result = decode.FormatString('%.4f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2000')
+
+ def test_lowercase_float_with_multidigit_precision(self) -> None:
+ result = decode.FormatString('%.10f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2000000477')
+
+ def test_lowercase_float_with_star_preision(self) -> None:
+ result = decode.FormatString('%.*f').format(encode.encode_args(10, 2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2000000477')
+
+ def test_lowercase_float_with_zero_precision(self) -> None:
+ result = decode.FormatString('%.0f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2')
+
+ def test_lowercase_float_with_empty_precision(self) -> None:
+ result = decode.FormatString('%.f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2')
+
+ def test_lowercase_float_with_width_and_precision(self) -> None:
+ result = decode.FormatString('%10.0f').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 2')
+
+ def test_lowercase_float_with_star_width_and_star_precision(self) -> None:
+ result = decode.FormatString('%*.*f').format(
+ encode.encode_args(20, 10, 2.2)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 2.2000000477')
+
+ def test_lowercase_float_non_number_with_minus(self) -> None:
+ result = decode.FormatString('%-5f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'inf ')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_non_number_with_plus(self) -> None:
+ result = decode.FormatString('%+f').format(encode.encode_args(math.inf))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+inf')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_non_number_with_blank_space(self) -> None:
+ result = decode.FormatString('% f').format(encode.encode_args(math.inf))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' inf')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_non_number_with_plus_and_blank_ignores_blank(
+ self,
+ ) -> None:
+ result = decode.FormatString('%+ f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+inf')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('% +f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+inf')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_non_number_with_hashtag(self) -> None:
+ result = decode.FormatString('%#f').format(encode.encode_args(math.inf))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'inf')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_non_number_zero(self) -> None:
+ result = decode.FormatString('%05f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' inf')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_float_non_number_with_width(self) -> None:
+ result = decode.FormatString('%9f').format(encode.encode_args(math.inf))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' inf')
+
+ def test_lowercase_float_non_number_with_multidigit_width(self) -> None:
+ result = decode.FormatString('%10f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' inf')
+
+ def test_lowercase_float_non_number_with_star_width(self) -> None:
+ result = decode.FormatString('%*f').format(
+ encode.encode_args(10, math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' inf')
+
+ def test_lowercase_float_non_number_with_precision(self) -> None:
+ result = decode.FormatString('%.4f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'inf')
+
+ def test_lowercase_float_non_number_with_multidigit_precision(self) -> None:
+ result = decode.FormatString('%.10f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'inf')
+
+ def test_lowercase_float_non_number_with_star_preision(self) -> None:
+ result = decode.FormatString('%.*f').format(
+ encode.encode_args(10, math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'inf')
+
+ def test_lowercase_float_non_number_with_zero_precision(self) -> None:
+ result = decode.FormatString('%.0f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'inf')
+
+ def test_lowercase_float_non_number_with_empty_precision(self) -> None:
+ result = decode.FormatString('%.f').format(encode.encode_args(math.inf))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'inf')
+
+ def test_lowercase_float_non_number_with_width_and_precision(self) -> None:
+ result = decode.FormatString('%10.0f').format(
+ encode.encode_args(math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' inf')
+
+ def test_lowercase_float_non_number_with_star_width_and_star_precision(
+ self,
+ ) -> None:
+ result = decode.FormatString('%*.*f').format(
+ encode.encode_args(10, 0, math.inf)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' inf')
+
+ def test_zero_with_zero_precision(self) -> None:
+ result = decode.FormatString('%.0f').format(encode.encode_args(0.0))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_uppercase_float(self) -> None:
+ result = decode.FormatString('%F').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_uppercase_float_with_length(self) -> None:
+ """Tests that length modifiers do not affect F decoding."""
+ result = decode.FormatString('%hhF').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hF').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lF').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llF').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jF').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zF').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+
+ result = decode.FormatString('%tF').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%LF').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_uppercase_float_non_number(self) -> None:
+ result = decode.FormatString('%F').format(encode.encode_args(math.inf))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'INF')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_exponential(self) -> None:
+ result = decode.FormatString('%e').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_exponential_with_length(self) -> None:
+ """Tests that length modifiers do not affect e decoding."""
+ result = decode.FormatString('%hhe').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ # inclusive-language: disable
+ result = decode.FormatString('%he').format(encode.encode_args(2.2))
+ # inclusive-language: enable
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%le').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lle').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%je').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ze').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%te').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%Le').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000e+00')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_uppercase_exponential(self) -> None:
+ result = decode.FormatString('%E').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ def test_uppercase_exponential_with_length(self) -> None:
+ """Tests that length modifiers do not affect E decoding."""
+ result = decode.FormatString('%hhE').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ # inclusive-language: disable
+ result = decode.FormatString('%hE').format(encode.encode_args(2.2))
+ # inclusive-language: enable
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lE').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llE').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jE').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zE').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%tE').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%LE').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.200000E+00')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_lowercase_shortest_take_normal(self) -> None:
+ result = decode.FormatString('%g').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_shortest_take_exponential(self) -> None:
+ result = decode.FormatString('%g').format(encode.encode_args(1048580.0))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '1.04858e+06')
+ self.assertEqual(result.remaining, b'')
+
+ def test_lowercase_shortest_with_length(self) -> None:
+ """Tests that length modifiers do not affect g decoding."""
+ result = decode.FormatString('%hhg').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hg').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lg').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llg').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jg').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zg').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%tg').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%Lg').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_uppercase_shortest_take_normal(self) -> None:
+ result = decode.FormatString('%G').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ def test_uppercase_shortest_take_exponential(self) -> None:
+ result = decode.FormatString('%G').format(encode.encode_args(1048580.0))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '1.04858E+06')
+ self.assertEqual(result.remaining, b'')
+
+ def test_uppercase_shortest_with_length(self) -> None:
+ """Tests that length modifiers do not affect G decoding."""
+ result = decode.FormatString('%hhG').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hG').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lG').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llG').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jG').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zG').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%tG').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%LG').format(encode.encode_args(2.2))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '2.2')
+ self.assertEqual(result.remaining, b'')
+
+
+class TestCharDecoding(unittest.TestCase):
+ """Tests decoding character values."""
+
+ def test_char(self) -> None:
+ result = decode.FormatString('%c').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ def test_char_with_minus(self) -> None:
+ result = decode.FormatString('%-5c').format(
+ encode.encode_args(ord('c'))
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c ')
+ self.assertEqual(result.remaining, b'')
+
+ def test_char_with_plus(self) -> None:
+ result = decode.FormatString('%+c').format(encode.encode_args(ord('c')))
+ self.assertFalse(result.ok())
+
+ def test_char_with_blank_space(self) -> None:
+ result = decode.FormatString('% c').format(encode.encode_args(ord('c')))
+ self.assertFalse(result.ok())
+
+ def test_char_with_hashtag(self) -> None:
+ result = decode.FormatString('%#c').format(encode.encode_args(ord('c')))
+ self.assertFalse(result.ok())
+
+ def test_char_with_zero(self) -> None:
+ result = decode.FormatString('%0c').format(encode.encode_args(ord('c')))
+ self.assertFalse(result.ok())
+
+ def test_char_with_length(self) -> None:
+ """Tests that length modifiers do not affectchar decoding."""
+ result = decode.FormatString('%hhc').format(
+ encode.encode_args(ord('c'))
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hc').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lc').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%llc').format(
+ encode.encode_args(ord('c'))
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%jc').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zc').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%tc').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%Lc').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ def test_char_with_width(self) -> None:
+ result = decode.FormatString('%5c').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' c')
+
+ def test_char_with_multidigit_width(self) -> None:
+ result = decode.FormatString('%10c').format(
+ encode.encode_args(ord('c'))
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' c')
+
+ def test_char_with_star_width(self) -> None:
+ result = decode.FormatString('%*c').format(
+ encode.encode_args(10, ord('c'))
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' c')
+
+ def test_char_with_precision(self) -> None:
+ result = decode.FormatString('%.4c').format(
+ encode.encode_args(ord('c'))
+ )
+ self.assertFalse(result.ok())
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_long_char(self) -> None:
+ result = decode.FormatString('%lc').format(encode.encode_args(ord('c')))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'c')
+ self.assertEqual(result.remaining, b'')
+
+ def test_long_char_with_hashtag(self) -> None:
+ result = decode.FormatString('%#lc').format(
+ encode.encode_args(ord('c'))
+ )
+ self.assertFalse(result.ok())
+
+ def test_long_char_with_zero(self) -> None:
+ result = decode.FormatString('%0lc').format(
+ encode.encode_args(ord('c'))
+ )
+ self.assertFalse(result.ok())
+
+
+class TestStringDecoding(unittest.TestCase):
+ """Tests decoding string values."""
+
+ def test_string(self) -> None:
+ result = decode.FormatString('%s').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ def test_string_with_minus(self) -> None:
+ result = decode.FormatString('%-6s').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello ')
+ self.assertEqual(result.remaining, b'')
+
+ def test_string_with_plus(self) -> None:
+ result = decode.FormatString('%+s').format(encode.encode_args('hello'))
+ self.assertFalse(result.ok())
+
+ def test_string_with_blank_space(self) -> None:
+ result = decode.FormatString('% s').format(encode.encode_args('hello'))
+ self.assertFalse(result.ok())
+
+ def test_string_with_hashtag(self) -> None:
+ result = decode.FormatString('%#s').format(encode.encode_args('hello'))
+ self.assertFalse(result.ok())
+
+ def test_string_with_zero(self) -> None:
+ result = decode.FormatString('%0s').format(encode.encode_args('hello'))
+ self.assertFalse(result.ok())
+
+ def test_string_with_length(self) -> None:
+ """Tests that length modifiers do not affect string values (s)."""
+ result = decode.FormatString('%hhs').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%hs').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ls').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%lls').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%js').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%zs').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%ts').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%Ls').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ def test_string_with_width(self) -> None:
+ result = decode.FormatString('%6s').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' hello')
+
+ def test_string_with_width_does_not_pad_a_string_with_same_length(
+ self,
+ ) -> None:
+ result = decode.FormatString('%5s').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+
+ def test_string_with_multidigit_width(self) -> None:
+ result = decode.FormatString('%10s').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' hello')
+
+ def test_string_with_star_width(self) -> None:
+ result = decode.FormatString('%*s').format(
+ encode.encode_args(10, 'hello')
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' hello')
+
+ def test_string_with_precision(self) -> None:
+ result = decode.FormatString('%.3s').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hel')
+
+ def test_string_with_multidigit_precision(self) -> None:
+ result = decode.FormatString('%.10s').format(
+ encode.encode_args('hello')
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+
+ def test_string_with_star_precision(self) -> None:
+ result = decode.FormatString('%.*s').format(
+ encode.encode_args(3, 'hello')
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hel')
+
+ def test_string_with_width_and_precision(self) -> None:
+ result = decode.FormatString('%10.3s').format(
+ encode.encode_args('hello')
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' hel')
+
+ def test_string_with_star_with_and_star_precision(self) -> None:
+ result = decode.FormatString('%*.*s').format(
+ encode.encode_args(10, 3, 'hello')
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' hel')
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_long_string(self) -> None:
+ result = decode.FormatString('%ls').format(encode.encode_args('hello'))
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, 'hello')
+ self.assertEqual(result.remaining, b'')
+
+ def test_long_string_with_hashtag(self) -> None:
+ result = decode.FormatString('%#ls').format(encode.encode_args('hello'))
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args('hello'))
+
+ def test_long_string_with_zero(self) -> None:
+ result = decode.FormatString('%0ls').format(encode.encode_args('hello'))
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args('hello'))
+
+
+class TestPointerDecoding(unittest.TestCase):
+ """Tests decoding pointer values."""
+
+ def test_pointer(self) -> None:
+ result = decode.FormatString('%p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0xDEADBEEF')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_with_minus(self) -> None:
+ result = decode.FormatString('%-12p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0xDEADBEEF ')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_with_plus(self) -> None:
+ result = decode.FormatString('%+p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '+0xDEADBEEF')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_with_blank_space(self) -> None:
+ result = decode.FormatString('% p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 0xDEADBEEF')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_with_hashtag(self) -> None:
+ result = decode.FormatString('%#p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ def test_pointer_with_zero(self) -> None:
+ result = decode.FormatString('%0p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ def test_pointer_with_length(self) -> None:
+ """Tests that length modifiers do not affect decoding pointers (p)."""
+ result = decode.FormatString('%hhp').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ result = decode.FormatString('%hp').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ result = decode.FormatString('%lp').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ result = decode.FormatString('%llp').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ result = decode.FormatString('%jp').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ result = decode.FormatString('%zp').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ result = decode.FormatString('%tp').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ result = decode.FormatString('%Lp').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ def test_pointer_with_width(self) -> None:
+ result = decode.FormatString('%9p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0xDEADBEEF')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_with_multidigit_width(self) -> None:
+ result = decode.FormatString('%11p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 0xDEADBEEF')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_with_star_width(self) -> None:
+ result = decode.FormatString('%*p').format(
+ encode.encode_args(10, 0xDEADBEEF)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0xDEADBEEF')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%*p').format(
+ encode.encode_args(15, 0xDEADBEEF)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 0xDEADBEEF')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_with_precision(self) -> None:
+ result = decode.FormatString('%.10p').format(
+ encode.encode_args(0xDEADBEEF)
+ )
+ self.assertFalse(result.ok())
+ self.assertEqual(result.remaining, encode.encode_args(0xDEADBEEF))
+
+ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
+
+ def test_pointer_0_padding(self) -> None:
+ result = decode.FormatString('%p').format(
+ encode.encode_args(0x00000000)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0x00000000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_0_with_width(self) -> None:
+ result = decode.FormatString('%9p').format(
+ encode.encode_args(0x00000000)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0x00000000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_0_with_multidigit_width(self) -> None:
+ result = decode.FormatString('%11p').format(
+ encode.encode_args(0x00000000)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 0x00000000')
+ self.assertEqual(result.remaining, b'')
+
+ def test_pointer_0_with_star_width(self) -> None:
+ result = decode.FormatString('%*p').format(
+ encode.encode_args(10, 0x00000000)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, '0x00000000')
+ self.assertEqual(result.remaining, b'')
+
+ result = decode.FormatString('%*p').format(
+ encode.encode_args(15, 0x00000000)
+ )
+ self.assertTrue(result.ok())
+ self.assertEqual(result.value, ' 0x00000000')
+ self.assertEqual(result.remaining, b'')
class TestFormattedString(unittest.TestCase):
"""Tests scoring how successfully a formatted string decoded."""
+
def test_no_args(self) -> None:
result = decode.FormatString('string').format(b'')
@@ -96,7 +1761,7 @@ class TestFormattedString(unittest.TestCase):
self.assertEqual(result.score(), (True, True, 0, 0, datetime.max))
def test_one_arg(self) -> None:
- result = decode.FormatString('%d').format(b'\0')
+ result = decode.FormatString('%d').format(encode.encode_args(0))
self.assertTrue(result.ok())
self.assertEqual(result.score(), (True, True, 0, 1, datetime.max))
@@ -107,23 +1772,35 @@ class TestFormattedString(unittest.TestCase):
self.assertFalse(result.ok())
self.assertEqual(result.score(), (False, True, -2, 3, datetime.max))
self.assertGreater(result.score(), result.score(datetime.now()))
- self.assertGreater(result.score(datetime.now()),
- result.score(datetime.min))
+ self.assertGreater(
+ result.score(datetime.now()), result.score(datetime.min)
+ )
def test_compare_score(self) -> None:
- all_args_ok = decode.FormatString('%d%d%d').format(b'\0\0\0')
- missing_one_arg = decode.FormatString('%d%d%d').format(b'\0\0')
- missing_two_args = decode.FormatString('%d%d%d').format(b'\0')
- all_args_extra_data = decode.FormatString('%d%d%d').format(b'\0\0\0\1')
+ all_args_ok = decode.FormatString('%d%d%d').format(
+ encode.encode_args(0, 0, 0)
+ )
+ missing_one_arg = decode.FormatString('%d%d%d').format(
+ encode.encode_args(0, 0)
+ )
+ missing_two_args = decode.FormatString('%d%d%d').format(
+ encode.encode_args(0)
+ )
+ all_args_extra_data = decode.FormatString('%d%d%d').format(
+ encode.encode_args(0, 0, 0, 1)
+ )
missing_one_arg_extra_data = decode.FormatString('%d%d%d').format(
- b'\0' + b'\x80' * 100)
+ b'\0' + b'\x80' * 100
+ )
self.assertGreater(all_args_ok.score(), missing_one_arg.score())
self.assertGreater(missing_one_arg.score(), missing_two_args.score())
- self.assertGreater(missing_two_args.score(),
- all_args_extra_data.score())
- self.assertGreater(all_args_extra_data.score(),
- missing_one_arg_extra_data.score())
+ self.assertGreater(
+ missing_two_args.score(), all_args_extra_data.score()
+ )
+ self.assertGreater(
+ all_args_extra_data.score(), missing_one_arg_extra_data.score()
+ )
if __name__ == '__main__':
diff --git a/pw_tokenizer/py/detokenize_proto_test.py b/pw_tokenizer/py/detokenize_proto_test.py
index 1421dbf49..a6136f2fc 100644
--- a/pw_tokenizer/py/detokenize_proto_test.py
+++ b/pw_tokenizer/py/detokenize_proto_test.py
@@ -22,16 +22,19 @@ from pw_tokenizer_tests.detokenize_proto_test_pb2 import TheMessage
from pw_tokenizer import detokenize, encode, tokens
from pw_tokenizer.proto import detokenize_fields, decode_optionally_tokenized
-_DATABASE = tokens.Database([
- tokens.TokenizedStringEntry(0xAABBCCDD, "Luke, we're gonna have %s"),
- tokens.TokenizedStringEntry(0x12345678, "This string has a $oeQAAA=="),
- tokens.TokenizedStringEntry(0x0000e4a1, "recursive token"),
-])
+_DATABASE = tokens.Database(
+ [
+ tokens.TokenizedStringEntry(0xAABBCCDD, "Luke, we're gonna have %s"),
+ tokens.TokenizedStringEntry(0x12345678, "This string has a $oeQAAA=="),
+ tokens.TokenizedStringEntry(0x0000E4A1, "recursive token"),
+ ]
+)
_DETOKENIZER = detokenize.Detokenizer(_DATABASE)
class TestDetokenizeProtoFields(unittest.TestCase):
"""Tests detokenizing optionally tokenized proto fields."""
+
def test_plain_text(self) -> None:
proto = TheMessage(message=b'boring conversation anyway!')
detokenize_fields(_DETOKENIZER, proto)
@@ -68,63 +71,77 @@ class TestDetokenizeProtoFields(unittest.TestCase):
base64_msg = encode.prefixed_base64(b'\xDD\xCC\xBB\xAA\x09pancakes!')
proto = TheMessage(message=f'Good morning, {base64_msg}'.encode())
detokenize_fields(_DETOKENIZER, proto)
- self.assertEqual(proto.message,
- b"Good morning, Luke, we're gonna have pancakes!")
+ self.assertEqual(
+ proto.message, b"Good morning, Luke, we're gonna have pancakes!"
+ )
def test_unknown_token_not_utf8(self) -> None:
proto = TheMessage(message=b'\xFE\xED\xF0\x0D')
detokenize_fields(_DETOKENIZER, proto)
- self.assertEqual(proto.message.decode(),
- encode.prefixed_base64(b'\xFE\xED\xF0\x0D'))
+ self.assertEqual(
+ proto.message.decode(), encode.prefixed_base64(b'\xFE\xED\xF0\x0D')
+ )
def test_only_control_characters(self) -> None:
proto = TheMessage(message=b'\1\2\3\4')
detokenize_fields(_DETOKENIZER, proto)
- self.assertEqual(proto.message.decode(),
- encode.prefixed_base64(b'\1\2\3\4'))
+ self.assertEqual(
+ proto.message.decode(), encode.prefixed_base64(b'\1\2\3\4')
+ )
class TestDecodeOptionallyTokenized(unittest.TestCase):
"""Tests optional detokenization directly."""
+
def setUp(self):
self.detok = detokenize.Detokenizer(
- tokens.Database([
- tokens.TokenizedStringEntry(0, 'cheese'),
- tokens.TokenizedStringEntry(1, 'on pizza'),
- tokens.TokenizedStringEntry(2, 'is quite good'),
- tokens.TokenizedStringEntry(3, 'they say'),
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(0, 'cheese'),
+ tokens.TokenizedStringEntry(1, 'on pizza'),
+ tokens.TokenizedStringEntry(2, 'is quite good'),
+ tokens.TokenizedStringEntry(3, 'they say'),
+ ]
+ )
+ )
def test_found_binary_token(self):
self.assertEqual(
'on pizza',
- decode_optionally_tokenized(self.detok, b'\x01\x00\x00\x00'))
+ decode_optionally_tokenized(self.detok, b'\x01\x00\x00\x00'),
+ )
def test_missing_binary_token(self):
self.assertEqual(
'$' + base64.b64encode(b'\xD5\x8A\xF9\x2A\x8A').decode(),
- decode_optionally_tokenized(self.detok, b'\xD5\x8A\xF9\x2A\x8A'))
+ decode_optionally_tokenized(self.detok, b'\xD5\x8A\xF9\x2A\x8A'),
+ )
def test_found_b64_token(self):
b64_bytes = b'$' + base64.b64encode(b'\x03\x00\x00\x00')
- self.assertEqual('they say',
- decode_optionally_tokenized(self.detok, b64_bytes))
+ self.assertEqual(
+ 'they say', decode_optionally_tokenized(self.detok, b64_bytes)
+ )
def test_missing_b64_token(self):
b64_bytes = b'$' + base64.b64encode(b'\xD5\x8A\xF9\x2A\x8A')
- self.assertEqual(b64_bytes.decode(),
- decode_optionally_tokenized(self.detok, b64_bytes))
+ self.assertEqual(
+ b64_bytes.decode(),
+ decode_optionally_tokenized(self.detok, b64_bytes),
+ )
def test_found_alternate_prefix(self):
b64_bytes = b'~' + base64.b64encode(b'\x00\x00\x00\x00')
self.assertEqual(
- 'cheese', decode_optionally_tokenized(self.detok, b64_bytes, b'~'))
+ 'cheese', decode_optionally_tokenized(self.detok, b64_bytes, b'~')
+ )
def test_missing_alternate_prefix(self):
b64_bytes = b'~' + base64.b64encode(b'\x02\x00\x00\x00')
self.assertEqual(
b64_bytes.decode(),
- decode_optionally_tokenized(self.detok, b64_bytes, b'^'))
+ decode_optionally_tokenized(self.detok, b64_bytes, b'^'),
+ )
if __name__ == '__main__':
diff --git a/pw_tokenizer/py/detokenize_test.py b/pw_tokenizer/py/detokenize_test.py
index 4b8830534..df710c7e9 100755
--- a/pw_tokenizer/py/detokenize_test.py
+++ b/pw_tokenizer/py/detokenize_test.py
@@ -49,7 +49,7 @@ def path_to_byte_string(path):
except StopIteration:
break
- line += repr(data[i:i + 1])[2:-1].replace("'", r'\'')
+ line += repr(data[i : i + 1])[2:-1].replace("'", r'\'')
if not line:
return ''.join(output)
@@ -81,7 +81,8 @@ EMPTY_ELF = (
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01'
b'\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xd4\x00\x00'
b'\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00'
- b'\x00\x00\x00')
+ b'\x00\x00\x00'
+)
# This is an ELF file with only the pw_tokenizer sections. It was created
# from a tokenize_test binary built for the STM32F429i Discovery board. The
@@ -89,10 +90,13 @@ EMPTY_ELF = (
#
# arm-none-eabi-objcopy -S --only-section ".pw_tokenizer*" <ELF> <OUTPUT>
#
-ELF_WITH_TOKENIZER_SECTIONS = Path(__file__).parent.joinpath(
- 'example_binary_with_tokenized_strings.elf').read_bytes()
+ELF_WITH_TOKENIZER_SECTIONS_PATH = Path(__file__).parent.joinpath(
+ 'example_binary_with_tokenized_strings.elf'
+)
+ELF_WITH_TOKENIZER_SECTIONS = ELF_WITH_TOKENIZER_SECTIONS_PATH.read_bytes()
TOKENS_IN_ELF = 22
+TOKENS_IN_ELF_WITH_TOKENIZER_SECTIONS = 26
# 0x2e668cd6 is 'Jello, world!' (which is also used in database_test.py).
JELLO_WORLD_TOKEN = b'\xd6\x8c\x66\x2e'
@@ -100,23 +104,31 @@ JELLO_WORLD_TOKEN = b'\xd6\x8c\x66\x2e'
class DetokenizeTest(unittest.TestCase):
"""Tests the detokenize.Detokenizer."""
+
def test_simple(self):
detok = detokenize.Detokenizer(
- tokens.Database([
- tokens.TokenizedStringEntry(0xcdab,
- '%02d %s %c%%',
- date_removed=dt.datetime.now())
- ]))
- self.assertEqual(str(detok.detokenize(b'\xab\xcd\0\0\x02\x03Two\x66')),
- '01 Two 3%')
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 0xCDAB, '%02d %s %c%%', date_removed=dt.datetime.now()
+ )
+ ]
+ )
+ )
+ self.assertEqual(
+ str(detok.detokenize(b'\xab\xcd\0\0\x02\x03Two\x66')), '01 Two 3%'
+ )
def test_detokenize_extra_data_is_unsuccessful(self):
detok = detokenize.Detokenizer(
- tokens.Database([
- tokens.TokenizedStringEntry(1,
- 'no args',
- date_removed=dt.datetime(1, 1, 1))
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 1, 'no args', date_removed=dt.datetime(1, 1, 1)
+ )
+ ]
+ )
+ )
result = detok.detokenize(b'\x01\0\0\0\x04args')
self.assertEqual(len(result.failures), 1)
@@ -127,13 +139,26 @@ class DetokenizeTest(unittest.TestCase):
self.assertEqual('no args', string)
self.assertEqual('no args', str(result))
+ def test_detokenize_zero_extend_short_token_with_no_args(self):
+ detok = detokenize.Detokenizer(
+ tokens.Database(
+ [tokens.TokenizedStringEntry(0xCDAB, 'This token is 16 bits')]
+ )
+ )
+ self.assertEqual(
+ str(detok.detokenize(b'\xab\xcd')), 'This token is 16 bits'
+ )
+
def test_detokenize_missing_data_is_unsuccessful(self):
detok = detokenize.Detokenizer(
- tokens.Database([
- tokens.TokenizedStringEntry(2,
- '%s',
- date_removed=dt.datetime(1, 1, 1))
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 2, '%s', date_removed=dt.datetime(1, 1, 1)
+ )
+ ]
+ )
+ )
result = detok.detokenize(b'\x02\0\0\0')
string, args, remaining = result.failures[0]
@@ -144,12 +169,16 @@ class DetokenizeTest(unittest.TestCase):
self.assertEqual('%s', str(result))
def test_detokenize_missing_data_with_errors_is_unsuccessful(self):
- detok = detokenize.Detokenizer(tokens.Database([
- tokens.TokenizedStringEntry(2,
- '%s',
- date_removed=dt.datetime(1, 1, 1))
- ]),
- show_errors=True)
+ detok = detokenize.Detokenizer(
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 2, '%s', date_removed=dt.datetime(1, 1, 1)
+ )
+ ]
+ ),
+ show_errors=True,
+ )
result = detok.detokenize(b'\x02\0\0\0')
string, args, remaining = result.failures[0]
@@ -161,12 +190,14 @@ class DetokenizeTest(unittest.TestCase):
def test_unparsed_data(self):
detok = detokenize.Detokenizer(
- tokens.Database([
- tokens.TokenizedStringEntry(1,
- 'no args',
- date_removed=dt.datetime(
- 100, 1, 1)),
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 1, 'no args', date_removed=dt.datetime(100, 1, 1)
+ ),
+ ]
+ )
+ )
result = detok.detokenize(b'\x01\0\0\0o_o')
self.assertFalse(result.ok())
self.assertEqual('no args', str(result))
@@ -176,20 +207,24 @@ class DetokenizeTest(unittest.TestCase):
def test_empty_db(self):
detok = detokenize.Detokenizer(io.BytesIO(EMPTY_ELF))
self.assertFalse(detok.detokenize(b'\x12\x34\0\0').ok())
- self.assertIn('unknown token',
- detok.detokenize(b'1234').error_message())
+ self.assertIn(
+ 'unknown token', detok.detokenize(b'1234').error_message()
+ )
self.assertIn('unknown token', repr(detok.detokenize(b'1234')))
- self.assertEqual('$' + base64.b64encode(b'1234').decode(),
- str(detok.detokenize(b'1234')))
+ self.assertEqual(
+ '$' + base64.b64encode(b'1234').decode(),
+ str(detok.detokenize(b'1234')),
+ )
self.assertIsNone(detok.detokenize(b'').token)
def test_empty_db_show_errors(self):
detok = detokenize.Detokenizer(io.BytesIO(EMPTY_ELF), show_errors=True)
self.assertFalse(detok.detokenize(b'\x12\x34\0\0').ok())
- self.assertIn('unknown token',
- detok.detokenize(b'1234').error_message())
+ self.assertIn(
+ 'unknown token', detok.detokenize(b'1234').error_message()
+ )
self.assertIn('unknown token', repr(detok.detokenize(b'1234')))
self.assertIn('unknown token', str(detok.detokenize(b'1234')))
@@ -199,40 +234,54 @@ class DetokenizeTest(unittest.TestCase):
detok = detokenize.Detokenizer(io.BytesIO(EMPTY_ELF), show_errors=True)
self.assertIn('missing token', detok.detokenize(b'').error_message())
self.assertIn('missing token', str(detok.detokenize(b'')))
- self.assertIn('missing token', repr(detok.detokenize(b'123')))
-
- self.assertIn('missing token', detok.detokenize(b'1').error_message())
- self.assertIn('missing token', str(detok.detokenize(b'1')))
- self.assertIn('missing token', repr(detok.detokenize(b'1')))
-
- self.assertIn('missing token',
- detok.detokenize(b'123').error_message())
- self.assertIn('missing token', str(detok.detokenize(b'123')))
- self.assertIn('missing token', repr(detok.detokenize(b'123')))
def test_missing_token(self):
detok = detokenize.Detokenizer(io.BytesIO(EMPTY_ELF))
self.assertIn('missing token', detok.detokenize(b'').error_message())
self.assertEqual('$', str(detok.detokenize(b'')))
- self.assertIn('missing token', repr(detok.detokenize(b'123')))
- self.assertIn('missing token', detok.detokenize(b'1').error_message())
- self.assertEqual('$' + base64.b64encode(b'1').decode(),
- str(detok.detokenize(b'1')))
- self.assertIn('missing token', repr(detok.detokenize(b'1')))
+ def test_unknown_shorter_token_show_error(self):
+ detok = detokenize.Detokenizer(io.BytesIO(EMPTY_ELF), show_errors=True)
+
+ self.assertIn('unknown token', detok.detokenize(b'1').error_message())
+ self.assertIn('unknown token', str(detok.detokenize(b'1')))
+ self.assertIn('unknown token', repr(detok.detokenize(b'1')))
+
+ self.assertIn('unknown token', detok.detokenize(b'123').error_message())
+ self.assertIn('unknown token', str(detok.detokenize(b'123')))
+ self.assertIn('unknown token', repr(detok.detokenize(b'123')))
+
+ def test_unknown_shorter_token(self):
+ detok = detokenize.Detokenizer(io.BytesIO(EMPTY_ELF))
- self.assertIn('missing token',
- detok.detokenize(b'123').error_message())
- self.assertEqual('$' + base64.b64encode(b'123').decode(),
- str(detok.detokenize(b'123')))
- self.assertIn('missing token', repr(detok.detokenize(b'123')))
+ self.assertEqual(
+ 'unknown token 00000001', detok.detokenize(b'\1').error_message()
+ )
+ self.assertEqual(
+ '$' + base64.b64encode(b'\1\0\0\0').decode(),
+ str(detok.detokenize(b'\1')),
+ )
+ self.assertIn('unknown token 00000001', repr(detok.detokenize(b'\1')))
+
+ self.assertEqual(
+ 'unknown token 00030201',
+ detok.detokenize(b'\1\2\3').error_message(),
+ )
+ self.assertEqual(
+ '$' + base64.b64encode(b'\1\2\3\0').decode(),
+ str(detok.detokenize(b'\1\2\3')),
+ )
+ self.assertIn(
+ 'unknown token 00030201', repr(detok.detokenize(b'\1\2\3'))
+ )
def test_decode_from_elf_data(self):
detok = detokenize.Detokenizer(io.BytesIO(ELF_WITH_TOKENIZER_SECTIONS))
self.assertTrue(detok.detokenize(JELLO_WORLD_TOKEN).ok())
- self.assertEqual(str(detok.detokenize(JELLO_WORLD_TOKEN)),
- 'Jello, world!')
+ self.assertEqual(
+ str(detok.detokenize(JELLO_WORLD_TOKEN)), 'Jello, world!'
+ )
undecoded_args = detok.detokenize(JELLO_WORLD_TOKEN + b'some junk')
self.assertFalse(undecoded_args.ok())
@@ -257,13 +306,15 @@ class DetokenizeTest(unittest.TestCase):
self.assertEqual(
expected_tokens,
- frozenset(detok.database.token_to_entries.keys()))
+ frozenset(detok.database.token_to_entries.keys()),
+ )
# Open ELF by path
detok = detokenize.Detokenizer(elf.name)
self.assertEqual(
expected_tokens,
- frozenset(detok.database.token_to_entries.keys()))
+ frozenset(detok.database.token_to_entries.keys()),
+ )
# Open ELF by elf_reader.Elf
with open(elf.name, 'rb') as fd:
@@ -271,7 +322,8 @@ class DetokenizeTest(unittest.TestCase):
self.assertEqual(
expected_tokens,
- frozenset(detok.database.token_to_entries.keys()))
+ frozenset(detok.database.token_to_entries.keys()),
+ )
finally:
os.unlink(elf.name)
@@ -291,7 +343,8 @@ class DetokenizeTest(unittest.TestCase):
detok = detokenize.Detokenizer(csv_file.name)
self.assertEqual(
expected_tokens,
- frozenset(detok.database.token_to_entries.keys()))
+ frozenset(detok.database.token_to_entries.keys()),
+ )
# Open CSV by file object
with open(csv_file.name) as fd:
@@ -299,7 +352,8 @@ class DetokenizeTest(unittest.TestCase):
self.assertEqual(
expected_tokens,
- frozenset(detok.database.token_to_entries.keys()))
+ frozenset(detok.database.token_to_entries.keys()),
+ )
finally:
os.unlink(csv_file.name)
@@ -308,33 +362,42 @@ class DetokenizeTest(unittest.TestCase):
expected_tokens = frozenset(detok.database.token_to_entries.keys())
detok = detokenize.Detokenizer(detok.database)
- self.assertEqual(expected_tokens,
- frozenset(detok.database.token_to_entries.keys()))
+ self.assertEqual(
+ expected_tokens, frozenset(detok.database.token_to_entries.keys())
+ )
class DetokenizeWithCollisions(unittest.TestCase):
"""Tests collision resolution."""
+
def setUp(self):
super().setUp()
- token = 0xbaad
+ token = 0xBAAD
# Database with several conflicting tokens.
- self.detok = detokenize.Detokenizer(tokens.Database([
- tokens.TokenizedStringEntry(
- token, 'REMOVED', date_removed=dt.datetime(9, 1, 1)),
- tokens.TokenizedStringEntry(token, 'newer'),
- tokens.TokenizedStringEntry(
- token, 'A: %d', date_removed=dt.datetime(30, 5, 9)),
- tokens.TokenizedStringEntry(
- token, 'B: %c', date_removed=dt.datetime(30, 5, 10)),
- tokens.TokenizedStringEntry(token, 'C: %s'),
- tokens.TokenizedStringEntry(token, '%d%u'),
- tokens.TokenizedStringEntry(token, '%s%u %d'),
- tokens.TokenizedStringEntry(1, '%s'),
- tokens.TokenizedStringEntry(1, '%d'),
- tokens.TokenizedStringEntry(2, 'Three %s %s %s'),
- tokens.TokenizedStringEntry(2, 'Five %d %d %d %d %s'),
- ])) # yapf: disable
+ self.detok = detokenize.Detokenizer(
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ token, 'REMOVED', date_removed=dt.datetime(9, 1, 1)
+ ),
+ tokens.TokenizedStringEntry(token, 'newer'),
+ tokens.TokenizedStringEntry(
+ token, 'A: %d', date_removed=dt.datetime(30, 5, 9)
+ ),
+ tokens.TokenizedStringEntry(
+ token, 'B: %c', date_removed=dt.datetime(30, 5, 10)
+ ),
+ tokens.TokenizedStringEntry(token, 'C: %s'),
+ tokens.TokenizedStringEntry(token, '%d%u'),
+ tokens.TokenizedStringEntry(token, '%s%u %d'),
+ tokens.TokenizedStringEntry(1, '%s'),
+ tokens.TokenizedStringEntry(1, '%d'),
+ tokens.TokenizedStringEntry(2, 'Three %s %s %s'),
+ tokens.TokenizedStringEntry(2, 'Five %d %d %d %d %s'),
+ ]
+ )
+ )
def test_collision_no_args_favors_most_recently_present(self):
no_args = self.detok.detokenize(b'\xad\xba\0\0')
@@ -391,11 +454,13 @@ class DetokenizeWithCollisions(unittest.TestCase):
@mock.patch('os.path.getmtime')
class AutoUpdatingDetokenizerTest(unittest.TestCase):
"""Tests the AutoUpdatingDetokenizer class."""
+
def test_update(self, mock_getmtime):
"""Tests the update command."""
db = database.load_token_database(
- io.BytesIO(ELF_WITH_TOKENIZER_SECTIONS))
+ io.BytesIO(ELF_WITH_TOKENIZER_SECTIONS)
+ )
self.assertEqual(len(db), TOKENS_IN_ELF)
the_time = [100]
@@ -413,8 +478,9 @@ class AutoUpdatingDetokenizerTest(unittest.TestCase):
try:
file.close()
- detok = detokenize.AutoUpdatingDetokenizer(file.name,
- min_poll_period_s=0)
+ detok = detokenize.AutoUpdatingDetokenizer(
+ file.name, min_poll_period_s=0
+ )
self.assertFalse(detok.detokenize(JELLO_WORLD_TOKEN).ok())
with open(file.name, 'wb') as fd:
@@ -434,11 +500,15 @@ class AutoUpdatingDetokenizerTest(unittest.TestCase):
try:
tokens.write_csv(
database.load_token_database(
- io.BytesIO(ELF_WITH_TOKENIZER_SECTIONS)), file)
+ io.BytesIO(ELF_WITH_TOKENIZER_SECTIONS)
+ ),
+ file,
+ )
file.close()
- detok = detokenize.AutoUpdatingDetokenizer(file,
- min_poll_period_s=0)
+ detok = detokenize.AutoUpdatingDetokenizer(
+ file.name, min_poll_period_s=0
+ )
self.assertTrue(detok.detokenize(JELLO_WORLD_TOKEN).ok())
# Empty the database, but keep the mock modified time the same.
@@ -454,6 +524,38 @@ class AutoUpdatingDetokenizerTest(unittest.TestCase):
finally:
os.unlink(file.name)
+ def test_token_domain_in_str(self, _) -> None:
+ """Tests a str containing a domain"""
+ detok = detokenize.AutoUpdatingDetokenizer(
+ f'{ELF_WITH_TOKENIZER_SECTIONS_PATH}#.*', min_poll_period_s=0
+ )
+ self.assertEqual(
+ len(detok.database), TOKENS_IN_ELF_WITH_TOKENIZER_SECTIONS
+ )
+
+ def test_token_domain_in_path(self, _) -> None:
+ """Tests a Path() containing a domain"""
+ detok = detokenize.AutoUpdatingDetokenizer(
+ Path(f'{ELF_WITH_TOKENIZER_SECTIONS_PATH}#.*'), min_poll_period_s=0
+ )
+ self.assertEqual(
+ len(detok.database), TOKENS_IN_ELF_WITH_TOKENIZER_SECTIONS
+ )
+
+ def test_token_no_domain_in_str(self, _) -> None:
+ """Tests a str without a domain"""
+ detok = detokenize.AutoUpdatingDetokenizer(
+ str(ELF_WITH_TOKENIZER_SECTIONS_PATH), min_poll_period_s=0
+ )
+ self.assertEqual(len(detok.database), TOKENS_IN_ELF)
+
+ def test_token_no_domain_in_path(self, _) -> None:
+ """Tests a Path() without a domain"""
+ detok = detokenize.AutoUpdatingDetokenizer(
+ ELF_WITH_TOKENIZER_SECTIONS_PATH, min_poll_period_s=0
+ )
+ self.assertEqual(len(detok.database), TOKENS_IN_ELF)
+
def _next_char(message: bytes) -> bytes:
return bytes(b + 1 for b in message)
@@ -467,24 +569,32 @@ class PrefixedMessageDecoderTest(unittest.TestCase):
def test_transform_single_message(self):
self.assertEqual(
b'%bcde',
- b''.join(self.decode.transform(io.BytesIO(b'$abcd'), _next_char)))
+ b''.join(self.decode.transform(io.BytesIO(b'$abcd'), _next_char)),
+ )
def test_transform_message_amidst_other_only_affects_message(self):
self.assertEqual(
- b'%%WHAT?%bcd%WHY? is this %ok %', b''.join(
+ b'%%WHAT?%bcd%WHY? is this %ok %',
+ b''.join(
self.decode.transform(
- io.BytesIO(b'$$WHAT?$abc$WHY? is this $ok $'),
- _next_char)))
+ io.BytesIO(b'$$WHAT?$abc$WHY? is this $ok $'), _next_char
+ )
+ ),
+ )
def test_transform_empty_message(self):
self.assertEqual(
b'%1%',
- b''.join(self.decode.transform(io.BytesIO(b'$1$'), _next_char)))
+ b''.join(self.decode.transform(io.BytesIO(b'$1$'), _next_char)),
+ )
def test_transform_sequential_messages(self):
self.assertEqual(
- b'%bcd%efghh', b''.join(
- self.decode.transform(io.BytesIO(b'$abc$defgh'), _next_char)))
+ b'%bcd%efghh',
+ b''.join(
+ self.decode.transform(io.BytesIO(b'$abc$defgh'), _next_char)
+ ),
+ )
class DetokenizeBase64(unittest.TestCase):
@@ -494,11 +604,13 @@ class DetokenizeBase64(unittest.TestCase):
RECURSION_STRING = f'The secret message is "{JELLO.decode()}"'
RECURSION = b'$' + base64.b64encode(
- struct.pack('I', tokens.default_hash(RECURSION_STRING)))
+ struct.pack('I', tokens.c_hash(RECURSION_STRING))
+ )
RECURSION_STRING_2 = f"'{RECURSION.decode()}', said the spy."
RECURSION_2 = b'$' + base64.b64encode(
- struct.pack('I', tokens.default_hash(RECURSION_STRING_2)))
+ struct.pack('I', tokens.c_hash(RECURSION_STRING_2))
+ )
TEST_CASES = (
(b'', b''),
@@ -516,17 +628,21 @@ class DetokenizeBase64(unittest.TestCase):
(b'$3141', b'$3141'),
(JELLO + b'$3141', b'Jello, world!$3141'),
(RECURSION, b'The secret message is "Jello, world!"'),
- (RECURSION_2,
- b'\'The secret message is "Jello, world!"\', said the spy.'),
+ (
+ RECURSION_2,
+ b'\'The secret message is "Jello, world!"\', said the spy.',
+ ),
)
def setUp(self):
super().setUp()
db = database.load_token_database(
- io.BytesIO(ELF_WITH_TOKENIZER_SECTIONS))
+ io.BytesIO(ELF_WITH_TOKENIZER_SECTIONS)
+ )
db.add(
- tokens.TokenizedStringEntry(tokens.default_hash(s), s)
- for s in [self.RECURSION_STRING, self.RECURSION_STRING_2])
+ tokens.TokenizedStringEntry(tokens.c_hash(s), s)
+ for s in [self.RECURSION_STRING, self.RECURSION_STRING_2]
+ )
self.detok = detokenize.Detokenizer(db)
def test_detokenize_base64_live(self):
@@ -545,48 +661,57 @@ class DetokenizeBase64(unittest.TestCase):
def test_detokenize_base64(self):
for data, expected in self.TEST_CASES:
- self.assertEqual(expected,
- self.detok.detokenize_base64(data, b'$'))
+ self.assertEqual(expected, self.detok.detokenize_base64(data, b'$'))
def test_detokenize_base64_str(self):
for data, expected in self.TEST_CASES:
- self.assertEqual(expected.decode(),
- self.detok.detokenize_base64(data.decode()))
+ self.assertEqual(
+ expected.decode(), self.detok.detokenize_base64(data.decode())
+ )
class DetokenizeBase64InfiniteRecursion(unittest.TestCase):
"""Tests that infinite Bas64 token recursion resolves."""
+
def setUp(self):
super().setUp()
self.detok = detokenize.Detokenizer(
- tokens.Database([
- tokens.TokenizedStringEntry(0, '$AAAAAA=='), # token for 0
- tokens.TokenizedStringEntry(1, '$AgAAAA=='), # token for 2
- tokens.TokenizedStringEntry(2, '$AwAAAA=='), # token for 3
- tokens.TokenizedStringEntry(3, '$AgAAAA=='), # token for 2
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(0, '$AAAAAA=='), # token for 0
+ tokens.TokenizedStringEntry(1, '$AgAAAA=='), # token for 2
+ tokens.TokenizedStringEntry(2, '$AwAAAA=='), # token for 3
+ tokens.TokenizedStringEntry(3, '$AgAAAA=='), # token for 2
+ ]
+ )
+ )
def test_detokenize_self_recursion(self):
for depth in range(5):
self.assertEqual(
- self.detok.detokenize_base64(b'This one is deep: $AAAAAA==',
- recursion=depth),
- b'This one is deep: $AAAAAA==')
+ self.detok.detokenize_base64(
+ b'This one is deep: $AAAAAA==', recursion=depth
+ ),
+ b'This one is deep: $AAAAAA==',
+ )
def test_detokenize_self_recursion_default(self):
self.assertEqual(
self.detok.detokenize_base64(b'This one is deep: $AAAAAA=='),
- b'This one is deep: $AAAAAA==')
+ b'This one is deep: $AAAAAA==',
+ )
def test_detokenize_cyclic_recursion_even(self):
self.assertEqual(
self.detok.detokenize_base64(b'I said "$AQAAAA=="', recursion=2),
- b'I said "$AgAAAA=="')
+ b'I said "$AgAAAA=="',
+ )
def test_detokenize_cyclic_recursion_odd(self):
self.assertEqual(
self.detok.detokenize_base64(b'I said "$AQAAAA=="', recursion=3),
- b'I said "$AwAAAA=="')
+ b'I said "$AwAAAA=="',
+ )
if __name__ == '__main__':
diff --git a/pw_tokenizer/py/elf_reader_test.py b/pw_tokenizer/py/elf_reader_test.py
index 2473182eb..923112562 100755
--- a/pw_tokenizer/py/elf_reader_test.py
+++ b/pw_tokenizer/py/elf_reader_test.py
@@ -25,7 +25,7 @@ from pw_tokenizer import elf_reader
#
# readelf -WS elf_reader_test_binary.elf
#
-TEST_READELF_OUTPUT = ("""
+TEST_READELF_OUTPUT = """
There are 33 section headers, starting at offset 0x1758:
Section Headers:
@@ -68,54 +68,61 @@ Key to Flags:
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
-""")
+"""
-TEST_ELF_PATH = os.path.join(os.path.dirname(__file__),
- 'elf_reader_test_binary.elf')
+TEST_ELF_PATH = os.path.join(
+ os.path.dirname(__file__), 'elf_reader_test_binary.elf'
+)
class ElfReaderTest(unittest.TestCase):
"""Tests the elf_reader.Elf class."""
- def setUp(self):
+
+ def setUp(self) -> None:
super().setUp()
self._elf_file = open(TEST_ELF_PATH, 'rb')
self._elf = elf_reader.Elf(self._elf_file)
- def tearDown(self):
+ def tearDown(self) -> None:
super().tearDown()
self._elf_file.close()
- def _section(self, name):
- return next(self._elf.sections_with_name(name))
+ def _section(self, name) -> elf_reader.Elf.Section:
+ return next(iter(self._elf.sections_with_name(name)))
- def test_readelf_comparison_using_the_readelf_binary(self):
+ def test_readelf_comparison_using_the_readelf_binary(self) -> None:
"""Compares elf_reader to readelf's output."""
- parse_readelf_output = re.compile(r'\s+'
- r'\[\s*(?P<number>\d+)\]\s+'
- r'(?P<name>\.\S*)?\s+'
- r'(?P<type>\S+)\s+'
- r'(?P<addr>[0-9a-fA-F]+)\s+'
- r'(?P<offset>[0-9a-fA-F]+)\s+'
- r'(?P<size>[0-9a-fA-F]+)\s+')
+ parse_readelf_output = re.compile(
+ r'\s+'
+ r'\[\s*(?P<number>\d+)\]\s+'
+ r'(?P<name>\.\S*)?\s+'
+ r'(?P<type>\S+)\s+'
+ r'(?P<addr>[0-9a-fA-F]+)\s+'
+ r'(?P<offset>[0-9a-fA-F]+)\s+'
+ r'(?P<size>[0-9a-fA-F]+)\s+'
+ )
readelf_sections = []
for number, name, _, addr, offset, size in parse_readelf_output.findall(
- TEST_READELF_OUTPUT):
- readelf_sections.append((
- int(number),
- name or '',
- int(addr, 16),
- int(offset, 16),
- int(size, 16),
- ))
+ TEST_READELF_OUTPUT
+ ):
+ readelf_sections.append(
+ (
+ int(number),
+ name or '',
+ int(addr, 16),
+ int(offset, 16),
+ int(size, 16),
+ )
+ )
self.assertEqual(len(readelf_sections), 33)
self.assertEqual(len(readelf_sections), len(self._elf.sections))
- for (index,
- section), readelf_section in zip(enumerate(self._elf.sections),
- readelf_sections):
+ for (index, section), readelf_section in zip(
+ enumerate(self._elf.sections), readelf_sections
+ ):
readelf_index, name, address, offset, size = readelf_section
self.assertEqual(index, readelf_index)
@@ -124,64 +131,78 @@ class ElfReaderTest(unittest.TestCase):
self.assertEqual(section.offset, offset)
self.assertEqual(section.size, size)
- def test_dump_single_section(self):
- self.assertEqual(self._elf.dump_section_contents(r'\.test_section_1'),
- b'You cannot pass\0')
- self.assertEqual(self._elf.dump_section_contents(r'\.test_section_2'),
- b'\xef\xbe\xed\xfe')
-
- def test_dump_multiple_sections(self):
- if (self._section('.test_section_1').address <
- self._section('.test_section_2').address):
+ def test_dump_single_section(self) -> None:
+ self.assertEqual(
+ self._elf.dump_section_contents(r'\.test_section_1'),
+ b'You cannot pass\0',
+ )
+ self.assertEqual(
+ self._elf.dump_section_contents(r'\.test_section_2'),
+ b'\xef\xbe\xed\xfe',
+ )
+
+ def test_dump_multiple_sections(self) -> None:
+ if (
+ self._section('.test_section_1').address
+ < self._section('.test_section_2').address
+ ):
contents = b'You cannot pass\0\xef\xbe\xed\xfe'
else:
contents = b'\xef\xbe\xed\xfeYou cannot pass\0'
- self.assertIn(self._elf.dump_section_contents(r'.test_section_\d'),
- contents)
+ self.assertIn(
+ self._elf.dump_section_contents(r'.test_section_\d'), contents
+ )
- def test_read_values(self):
+ def test_read_values(self) -> None:
address = self._section('.test_section_1').address
self.assertEqual(self._elf.read_value(address), b'You cannot pass')
int32_address = self._section('.test_section_2').address
- self.assertEqual(self._elf.read_value(int32_address, 4),
- b'\xef\xbe\xed\xfe')
+ self.assertEqual(
+ self._elf.read_value(int32_address, 4), b'\xef\xbe\xed\xfe'
+ )
- def test_read_string(self):
+ def test_read_string(self) -> None:
bytes_io = io.BytesIO(
- b'This is a null-terminated string\0No terminator!')
- self.assertEqual(elf_reader.read_c_string(bytes_io),
- b'This is a null-terminated string')
+ b'This is a null-terminated string\0No terminator!'
+ )
+ self.assertEqual(
+ elf_reader.read_c_string(bytes_io),
+ b'This is a null-terminated string',
+ )
self.assertEqual(elf_reader.read_c_string(bytes_io), b'No terminator!')
self.assertEqual(elf_reader.read_c_string(bytes_io), b'')
- def test_compatible_file_for_elf(self):
+ def test_compatible_file_for_elf(self) -> None:
self.assertTrue(elf_reader.compatible_file(self._elf_file))
self.assertTrue(elf_reader.compatible_file(io.BytesIO(b'\x7fELF')))
- def test_compatible_file_for_elf_start_at_offset(self):
+ def test_compatible_file_for_elf_start_at_offset(self) -> None:
self._elf_file.seek(13) # Seek ahead to get out of sync
self.assertTrue(elf_reader.compatible_file(self._elf_file))
self.assertEqual(13, self._elf_file.tell())
- def test_compatible_file_for_invalid_elf(self):
+ def test_compatible_file_for_invalid_elf(self) -> None:
self.assertFalse(elf_reader.compatible_file(io.BytesIO(b'\x7fELVESF')))
def _archive_file(data: bytes) -> bytes:
- return ('FILE ID 90123456'
- 'MODIFIED 012'
- 'OWNER '
- 'GROUP '
- 'MODE 678'
- f'{len(data):10}' # File size -- the only part that's needed.
- '`\n'.encode() + data)
+ return (
+ 'FILE ID 90123456'
+ 'MODIFIED 012'
+ 'OWNER '
+ 'GROUP '
+ 'MODE 678'
+ f'{len(data):10}' # File size -- the only part that's needed.
+ '`\n'.encode() + data
+ )
class ArchiveTest(unittest.TestCase):
"""Tests reading from archive files."""
- def setUp(self):
+
+ def setUp(self) -> None:
super().setUp()
with open(TEST_ELF_PATH, 'rb') as fd:
@@ -190,49 +211,58 @@ class ArchiveTest(unittest.TestCase):
self._archive_entries = b'blah', b'hello', self._elf_data
self._archive_data = elf_reader.ARCHIVE_MAGIC + b''.join(
- _archive_file(f) for f in self._archive_entries)
+ _archive_file(f) for f in self._archive_entries
+ )
self._archive = io.BytesIO(self._archive_data)
- def test_compatible_file_for_archive(self):
+ def test_compatible_file_for_archive(self) -> None:
self.assertTrue(elf_reader.compatible_file(io.BytesIO(b'!<arch>\n')))
self.assertTrue(elf_reader.compatible_file(self._archive))
- def test_compatible_file_for_invalid_archive(self):
+ def test_compatible_file_for_invalid_archive(self) -> None:
self.assertFalse(elf_reader.compatible_file(io.BytesIO(b'!<arch>')))
- def test_iterate_over_files(self):
- for expected, size in zip(self._archive_entries,
- elf_reader.files_in_archive(self._archive)):
+ def test_iterate_over_files(self) -> None:
+ for expected, size in zip(
+ self._archive_entries, elf_reader.files_in_archive(self._archive)
+ ):
self.assertEqual(expected, self._archive.read(size))
- def test_iterate_over_empty_archive(self):
+ def test_iterate_over_empty_archive(self) -> None:
with self.assertRaises(StopIteration):
next(iter(elf_reader.files_in_archive(io.BytesIO(b'!<arch>\n'))))
- def test_iterate_over_invalid_archive(self):
+ def test_iterate_over_invalid_archive(self) -> None:
with self.assertRaises(elf_reader.FileDecodeError):
for _ in elf_reader.files_in_archive(
- io.BytesIO(b'!<arch>blah blahblah')):
+ io.BytesIO(b'!<arch>blah blahblah')
+ ):
pass
- def test_extra_newline_after_entry_is_ignored(self):
- archive = io.BytesIO(elf_reader.ARCHIVE_MAGIC +
- _archive_file(self._elf_data) + b'\n' +
- _archive_file(self._elf_data))
+ def test_extra_newline_after_entry_is_ignored(self) -> None:
+ archive = io.BytesIO(
+ elf_reader.ARCHIVE_MAGIC
+ + _archive_file(self._elf_data)
+ + b'\n'
+ + _archive_file(self._elf_data)
+ )
for size in elf_reader.files_in_archive(archive):
self.assertEqual(self._elf_data, archive.read(size))
- def test_two_extra_newlines_parsing_fails(self):
- archive = io.BytesIO(elf_reader.ARCHIVE_MAGIC +
- _archive_file(self._elf_data) + b'\n\n' +
- _archive_file(self._elf_data))
+ def test_two_extra_newlines_parsing_fails(self) -> None:
+ archive = io.BytesIO(
+ elf_reader.ARCHIVE_MAGIC
+ + _archive_file(self._elf_data)
+ + b'\n\n'
+ + _archive_file(self._elf_data)
+ )
with self.assertRaises(elf_reader.FileDecodeError):
for size in elf_reader.files_in_archive(archive):
self.assertEqual(self._elf_data, archive.read(size))
- def test_iterate_over_archive_with_invalid_size(self):
+ def test_iterate_over_archive_with_invalid_size(self) -> None:
data = elf_reader.ARCHIVE_MAGIC + _archive_file(b'$' * 3210)
file = io.BytesIO(data)
@@ -243,24 +273,44 @@ class ArchiveTest(unittest.TestCase):
# Replace the size with a hex number, which is not valid.
with self.assertRaises(elf_reader.FileDecodeError):
for _ in elf_reader.files_in_archive(
- io.BytesIO(data.replace(b'3210', b'0x99'))):
+ io.BytesIO(data.replace(b'3210', b'0x99'))
+ ):
pass
- def test_elf_reader_dump_single_section(self):
+ def test_elf_reader_dump_single_section(self) -> None:
elf = elf_reader.Elf(self._archive)
- self.assertEqual(elf.dump_section_contents(r'\.test_section_1'),
- b'You cannot pass\0')
- self.assertEqual(elf.dump_section_contents(r'\.test_section_2'),
- b'\xef\xbe\xed\xfe')
-
- def test_elf_reader_read_values(self):
+ self.assertEqual(
+ elf.dump_section_contents(r'\.test_section_1'), b'You cannot pass\0'
+ )
+ self.assertEqual(
+ elf.dump_section_contents(r'\.test_section_2'), b'\xef\xbe\xed\xfe'
+ )
+
+ def test_elf_reader_read_values(self) -> None:
elf = elf_reader.Elf(self._archive)
- address = next(elf.sections_with_name('.test_section_1')).address
+ address = next(iter(elf.sections_with_name('.test_section_1'))).address
self.assertEqual(elf.read_value(address), b'You cannot pass')
- int32_address = next(elf.sections_with_name('.test_section_2')).address
+ int32_address = next(
+ iter(elf.sections_with_name('.test_section_2'))
+ ).address
self.assertEqual(elf.read_value(int32_address, 4), b'\xef\xbe\xed\xfe')
+ def test_elf_reader_duplicate_sections_are_concatenated(self) -> None:
+ archive_data = elf_reader.ARCHIVE_MAGIC + b''.join(
+ _archive_file(f) for f in [self._elf_data, self._elf_data]
+ )
+ elf = elf_reader.Elf(io.BytesIO(archive_data))
+
+ self.assertEqual(
+ elf.dump_section_contents(r'\.test_section_1'),
+ b'You cannot pass\0You cannot pass\0',
+ )
+ self.assertEqual(
+ elf.dump_section_contents(r'\.test_section_2'),
+ b'\xef\xbe\xed\xfe' * 2,
+ )
+
if __name__ == '__main__':
unittest.main()
diff --git a/pw_tokenizer/py/encode_test.py b/pw_tokenizer/py/encode_test.py
index 0dfc22a61..fe504dc55 100755
--- a/pw_tokenizer/py/encode_test.py
+++ b/pw_tokenizer/py/encode_test.py
@@ -23,50 +23,68 @@ from pw_tokenizer.encode import encode_token_and_args
class TestEncodeTokenized(unittest.TestCase):
"""Tests encoding tokenized strings with various arguments."""
+
def test_no_args(self):
- self.assertEqual(b'\xab\xcd\x12\x34',
- encode_token_and_args(0x3412cdab))
+ self.assertEqual(b'\xab\xcd\x12\x34', encode_token_and_args(0x3412CDAB))
self.assertEqual(b'\x00\x00\x00\x00', encode_token_and_args(0))
def test_int(self):
- self.assertEqual(b'\xff\xff\xff\xff\0',
- encode_token_and_args(0xffffffff, 0))
- self.assertEqual(b'\xff\xff\xff\xff\1',
- encode_token_and_args(0xffffffff, -1))
- self.assertEqual(b'\xff\xff\xff\xff\2',
- encode_token_and_args(0xffffffff, 1))
+ self.assertEqual(
+ b'\xff\xff\xff\xff\0', encode_token_and_args(0xFFFFFFFF, 0)
+ )
+ self.assertEqual(
+ b'\xff\xff\xff\xff\1', encode_token_and_args(0xFFFFFFFF, -1)
+ )
+ self.assertEqual(
+ b'\xff\xff\xff\xff\2', encode_token_and_args(0xFFFFFFFF, 1)
+ )
def test_float(self):
- self.assertEqual(b'\xff\xff\xff\xff\0\0\0\0',
- encode_token_and_args(0xffffffff, 0.0))
- self.assertEqual(b'\xff\xff\xff\xff\0\0\0\x80',
- encode_token_and_args(0xffffffff, -0.0))
+ self.assertEqual(
+ b'\xff\xff\xff\xff\0\0\0\0', encode_token_and_args(0xFFFFFFFF, 0.0)
+ )
+ self.assertEqual(
+ b'\xff\xff\xff\xff\0\0\0\x80',
+ encode_token_and_args(0xFFFFFFFF, -0.0),
+ )
def test_string(self):
- self.assertEqual(b'\xff\xff\xff\xff\5hello',
- encode_token_and_args(0xffffffff, 'hello'))
- self.assertEqual(b'\xff\xff\xff\xff\x7f' + b'!' * 127,
- encode_token_and_args(0xffffffff, '!' * 127))
+ self.assertEqual(
+ b'\xff\xff\xff\xff\5hello',
+ encode_token_and_args(0xFFFFFFFF, 'hello'),
+ )
+ self.assertEqual(
+ b'\xff\xff\xff\xff\x7f' + b'!' * 127,
+ encode_token_and_args(0xFFFFFFFF, '!' * 127),
+ )
def test_string_too_long(self):
- self.assertEqual(b'\xff\xff\xff\xff\xff' + b'!' * 127,
- encode_token_and_args(0xffffffff, '!' * 128))
+ self.assertEqual(
+ b'\xff\xff\xff\xff\xff' + b'!' * 127,
+ encode_token_and_args(0xFFFFFFFF, '!' * 128),
+ )
def test_bytes(self):
- self.assertEqual(b'\xff\xff\xff\xff\4\0yo\0',
- encode_token_and_args(0xffffffff, '\0yo\0'))
+ self.assertEqual(
+ b'\xff\xff\xff\xff\4\0yo\0',
+ encode_token_and_args(0xFFFFFFFF, '\0yo\0'),
+ )
def test_bytes_too_long(self):
- self.assertEqual(b'\xff\xff\xff\xff\xff' + b'?' * 127,
- encode_token_and_args(0xffffffff, b'?' * 200))
+ self.assertEqual(
+ b'\xff\xff\xff\xff\xff' + b'?' * 127,
+ encode_token_and_args(0xFFFFFFFF, b'?' * 200),
+ )
def test_multiple_args(self):
- self.assertEqual(b'\xdd\xcc\xbb\xaa\0',
- encode_token_and_args(0xaabbccdd, 0))
+ self.assertEqual(
+ b'\xdd\xcc\xbb\xaa\0', encode_token_and_args(0xAABBCCDD, 0)
+ )
class TestIntegerEncoding(unittest.TestCase):
"""Test encoding variable-length integers."""
+
def test_encode_generated_data(self):
test_data = varint_test_data.TEST_DATA
self.assertGreater(len(test_data), 100)
@@ -78,10 +96,10 @@ class TestIntegerEncoding(unittest.TestCase):
continue
# Encode the value as an arg, but skip the 4 bytes for the token.
+ self.assertEqual(encode_token_and_args(0, int(signed))[4:], encoded)
self.assertEqual(
- encode_token_and_args(0, int(signed))[4:], encoded)
- self.assertEqual(
- encode_token_and_args(0, int(unsigned))[4:], encoded)
+ encode_token_and_args(0, int(unsigned))[4:], encoded
+ )
if __name__ == '__main__':
diff --git a/pw_tokenizer/py/example_legacy_binary_with_tokenized_strings.elf b/pw_tokenizer/py/example_legacy_binary_with_tokenized_strings.elf
deleted file mode 100755
index 0fe2e6084..000000000
--- a/pw_tokenizer/py/example_legacy_binary_with_tokenized_strings.elf
+++ /dev/null
Binary files differ
diff --git a/pw_tokenizer/py/generate_argument_types_macro.py b/pw_tokenizer/py/generate_argument_types_macro.py
index 306fc4df0..a1ef9d4f7 100755
--- a/pw_tokenizer/py/generate_argument_types_macro.py
+++ b/pw_tokenizer/py/generate_argument_types_macro.py
@@ -64,9 +64,11 @@ def generate_argument_types_macro(size_bytes):
raise ValueError('Invalid size_bytes (must be 4 or 8)')
output = [
- FILE_HEADER.format(script=os.path.basename(__file__),
- year=datetime.date.today().year,
- size=size_bytes)
+ FILE_HEADER.format(
+ script=os.path.basename(__file__),
+ year=datetime.date.today().year,
+ size=size_bytes,
+ )
]
for i in range(1, max_args + 1):
@@ -76,8 +78,10 @@ def generate_argument_types_macro(size_bytes):
for x in range(i, 0, -1)
]
types.append(f'{i}')
- output.append(f'#define _PW_TOKENIZER_TYPES_{i}({", ".join(args)}) '
- f'({" | ".join(types)})\n\n')
+ output.append(
+ f'#define _PW_TOKENIZER_TYPES_{i}({", ".join(args)}) '
+ f'({" | ".join(types)})\n\n'
+ )
output.append('// clang-format on\n')
@@ -86,8 +90,14 @@ def generate_argument_types_macro(size_bytes):
def _main():
base = os.path.abspath(
- os.path.join(os.path.dirname(__file__), '..', 'public', 'pw_tokenizer',
- 'internal'))
+ os.path.join(
+ os.path.dirname(__file__),
+ '..',
+ 'public',
+ 'pw_tokenizer',
+ 'internal',
+ )
+ )
with open(os.path.join(base, 'argument_types_macro_4_byte.h'), 'w') as fd:
fd.write(generate_argument_types_macro(4))
diff --git a/pw_tokenizer/py/generate_hash_macro.py b/pw_tokenizer/py/generate_hash_macro.py
index 7120b593a..71abe4ac9 100755
--- a/pw_tokenizer/py/generate_hash_macro.py
+++ b/pw_tokenizer/py/generate_hash_macro.py
@@ -84,23 +84,29 @@ def generate_pw_tokenizer_65599_fixed_length_hash_macro(hash_length):
Returns:
the macro header file as a string
- """
+ """
- first_hash_term = ('(uint32_t)(sizeof(str "") - 1 + '
- '/* The argument must be a string literal. */ \\\n')
+ first_hash_term = (
+ '(uint32_t)(sizeof(str "") - 1 + '
+ '/* The argument must be a string literal. */ \\\n'
+ )
# Use this to add the aligned backslash at the end of the macro lines.
line_format = '{{:<{}}}\\\n'.format(len(first_hash_term))
lines = [
- FILE_HEADER.format(script=os.path.basename(__file__),
- hash_length=hash_length,
- year=datetime.date.today().year)
+ FILE_HEADER.format(
+ script=os.path.basename(__file__),
+ hash_length=hash_length,
+ year=datetime.date.today().year,
+ )
]
lines.append(
- line_format.format('#define {}_{}_HASH(str)'.format(
- HASH_NAME.upper(), hash_length)))
+ line_format.format(
+ '#define {}_{}_HASH(str)'.format(HASH_NAME.upper(), hash_length)
+ )
+ )
lines.append(' ' + first_hash_term) # add indendation and the macro line
indent = ' ' * len(' (uint32_t)(')
@@ -108,19 +114,23 @@ def generate_pw_tokenizer_65599_fixed_length_hash_macro(hash_length):
# The string will have at least a null terminator
lines.append(
- line_format.format('{}0x{:0>8x}u * (uint8_t)str[0] +'.format(
- indent, HASH_CONSTANT)))
+ line_format.format(
+ '{}0x{:0>8x}u * (uint8_t)str[0] +'.format(indent, HASH_CONSTANT)
+ )
+ )
# Format string to use for the remaining terms.
term_format = (
'{indent}{coefficient} * '
- '(uint8_t)({index} < sizeof(str) ? str[{index}] : 0) +').format(
- indent=indent,
- coefficient=coefficient_format,
- index='{{index:>{}}}'.format(len(str(hash_length - 1))))
+ '(uint8_t)({index} < sizeof(str) ? str[{index}] : 0) +'
+ ).format(
+ indent=indent,
+ coefficient=coefficient_format,
+ index='{{index:>{}}}'.format(len(str(hash_length - 1))),
+ )
for i in range(1, hash_length):
- coefficient = HASH_CONSTANT**(i + 1) % 2**32
+ coefficient = HASH_CONSTANT ** (i + 1) % 2**32
term = term_format.format(index=i, coefficient=coefficient)
lines.append(line_format.format(term))
@@ -134,21 +144,29 @@ def generate_pw_tokenizer_65599_fixed_length_hash_macro(hash_length):
def _main():
base = os.path.abspath(
- os.path.join(os.path.dirname(__file__), '..', 'public', 'pw_tokenizer',
- 'internal'))
+ os.path.join(
+ os.path.dirname(__file__),
+ '..',
+ 'public',
+ 'pw_tokenizer',
+ 'internal',
+ )
+ )
# Generate macros for hashes of the specified lengths.
for hash_length in HASH_LENGTHS:
path = os.path.join(
- base, '{}_{}_hash_macro.h'.format(HASH_NAME, hash_length))
+ base, '{}_{}_hash_macro.h'.format(HASH_NAME, hash_length)
+ )
with open(path, 'w') as header_file:
header_file.write(
- generate_pw_tokenizer_65599_fixed_length_hash_macro(
- hash_length))
+ generate_pw_tokenizer_65599_fixed_length_hash_macro(hash_length)
+ )
- print('Generated {}-character hash macro at {}'.format(
- hash_length, path))
+ print(
+ 'Generated {}-character hash macro at {}'.format(hash_length, path)
+ )
if __name__ == '__main__':
diff --git a/pw_tokenizer/py/generate_hash_test_data.py b/pw_tokenizer/py/generate_hash_test_data.py
index 373c82ed6..b875f188f 100755
--- a/pw_tokenizer/py/generate_hash_test_data.py
+++ b/pw_tokenizer/py/generate_hash_test_data.py
@@ -84,7 +84,9 @@ def _include_paths(lengths):
sorted(
'#include "pw_tokenizer/internal/'
'pw_tokenizer_65599_fixed_length_{}_hash_macro.h"'.format(length)
- for length in lengths))
+ for length in lengths
+ )
+ )
def _test_case_at_length(data, hash_length):
@@ -98,17 +100,19 @@ def _test_case_at_length(data, hash_length):
else:
escaped_str = ''.join(r'\x{:02x}'.format(b) for b in data)
- return _TEST_CASE.format(str=escaped_str,
- string_length=len(data),
- hash_length=hash_length,
- hash=tokens.pw_tokenizer_65599_hash(
- data, hash_length),
- macro=HASH_MACRO.format(hash_length))
+ return _TEST_CASE.format(
+ str=escaped_str,
+ string_length=len(data),
+ hash_length=hash_length,
+ hash=tokens.c_hash(data, hash_length),
+ macro=HASH_MACRO.format(hash_length),
+ )
def test_case(data):
return ''.join(
- _test_case_at_length(data, length) for length in (80, 96, 128))
+ _test_case_at_length(data, length) for length in (80, 96, 128)
+ )
def generate_test_cases():
@@ -125,7 +129,8 @@ def generate_test_cases():
random.seed(600613)
random_string = lambda size: bytes(
- random.randrange(256) for _ in range(size))
+ random.randrange(256) for _ in range(size)
+ )
for i in range(1, 16):
yield test_case(random_string(i))
@@ -139,14 +144,22 @@ def generate_test_cases():
if __name__ == '__main__':
path = os.path.realpath(
- os.path.join(os.path.dirname(__file__), '..', 'pw_tokenizer_private',
- 'generated_hash_test_cases.h'))
+ os.path.join(
+ os.path.dirname(__file__),
+ '..',
+ 'pw_tokenizer_private',
+ 'generated_hash_test_cases.h',
+ )
+ )
with open(path, 'w') as output:
output.write(
- FILE_HEADER.format(year=datetime.date.today().year,
- script=os.path.basename(__file__),
- includes=_include_paths(HASH_LENGTHS)))
+ FILE_HEADER.format(
+ year=datetime.date.today().year,
+ script=os.path.basename(__file__),
+ includes=_include_paths(HASH_LENGTHS),
+ )
+ )
for case in generate_test_cases():
output.write(case)
diff --git a/pw_tokenizer/py/pw_tokenizer/database.py b/pw_tokenizer/py/pw_tokenizer/database.py
index 37351fa46..26a32a7fa 100755
--- a/pw_tokenizer/py/pw_tokenizer/database.py
+++ b/pw_tokenizer/py/pw_tokenizer/database.py
@@ -21,6 +21,7 @@ maintaining token databases.
import argparse
from datetime import datetime
import glob
+import itertools
import json
import logging
import os
@@ -28,16 +29,27 @@ from pathlib import Path
import re
import struct
import sys
-from typing import (Any, Callable, Dict, Iterable, Iterator, List, Pattern,
- Set, TextIO, Tuple, Union)
+from typing import (
+ Any,
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ Optional,
+ Pattern,
+ Set,
+ TextIO,
+ Tuple,
+ Union,
+)
try:
from pw_tokenizer import elf_reader, tokens
except ImportError:
# Append this path to the module search path to allow running this module
# without installing the pw_tokenizer package.
- sys.path.append(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__))))
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from pw_tokenizer import elf_reader, tokens
_LOG = logging.getLogger('pw_tokenizer')
@@ -52,11 +64,7 @@ def _elf_reader(elf) -> elf_reader.Elf:
# pw_tokenizer/public/pw_tokenizer/internal/tokenize_string.h.
_TOKENIZED_ENTRY_MAGIC = 0xBAA98DEE
_ENTRY = struct.Struct('<4I')
-_TOKENIZED_ENTRY_SECTIONS = re.compile(
- r'^\.pw_tokenizer.entries(?:\.[_\d]+)?$')
-
-_LEGACY_STRING_SECTIONS = re.compile(
- r'^\.pw_tokenized\.(?P<domain>[^.]+)(?:\.\d+)?$')
+_TOKENIZED_ENTRY_SECTIONS = re.compile(r'^\.pw_tokenizer.entries(?:\.[_\d]+)?$')
_ERROR_HANDLER = 'surrogateescape' # How to deal with UTF-8 decoding errors
@@ -66,8 +74,8 @@ class Error(Exception):
def _read_tokenized_entries(
- data: bytes,
- domain: Pattern[str]) -> Iterator[tokens.TokenizedStringEntry]:
+ data: bytes, domain: Pattern[str]
+) -> Iterator[tokens.TokenizedStringEntry]:
index = 0
while index + _ENTRY.size <= len(data):
@@ -76,7 +84,8 @@ def _read_tokenized_entries(
if magic != _TOKENIZED_ENTRY_MAGIC:
raise Error(
f'Expected magic number 0x{_TOKENIZED_ENTRY_MAGIC:08x}, '
- f'found 0x{magic:08x}')
+ f'found 0x{magic:08x}'
+ )
start = index + _ENTRY.size
index = start + domain_len + string_len
@@ -84,8 +93,8 @@ def _read_tokenized_entries(
# Create the entries, trimming null terminators.
entry = tokens.TokenizedStringEntry(
token,
- data[start + domain_len:index - 1].decode(errors=_ERROR_HANDLER),
- data[start:start + domain_len - 1].decode(errors=_ERROR_HANDLER),
+ data[start + domain_len : index - 1].decode(errors=_ERROR_HANDLER),
+ data[start : start + domain_len - 1].decode(errors=_ERROR_HANDLER),
)
if data[start + domain_len - 1] != 0:
@@ -100,21 +109,6 @@ def _read_tokenized_entries(
yield entry
-def _read_tokenized_strings(sections: Dict[str, bytes],
- domain: Pattern[str]) -> Iterator[tokens.Database]:
- # Legacy ELF files used "default" as the default domain instead of "". Remap
- # the default if necessary.
- if domain.pattern == tokens.DEFAULT_DOMAIN:
- domain = re.compile('default')
-
- for section, data in sections.items():
- match = _LEGACY_STRING_SECTIONS.match(section)
- if match and domain.match(match.group('domain')):
- yield tokens.Database.from_strings(
- (s.decode(errors=_ERROR_HANDLER) for s in data.split(b'\0')),
- match.group('domain'))
-
-
def _database_from_elf(elf, domain: Pattern[str]) -> tokens.Database:
"""Reads the tokenized strings from an elf_reader.Elf or ELF file object."""
_LOG.debug('Reading tokenized strings in domain "%s" from %s', domain, elf)
@@ -126,12 +120,6 @@ def _database_from_elf(elf, domain: Pattern[str]) -> tokens.Database:
if section_data is not None:
return tokens.Database(_read_tokenized_entries(section_data, domain))
- # Read legacy null-terminated string entries.
- sections = reader.dump_sections(_LEGACY_STRING_SECTIONS)
- if sections:
- return tokens.Database.merged(
- *_read_tokenized_strings(sections, domain))
-
return tokens.Database([])
@@ -142,12 +130,8 @@ def tokenization_domains(elf) -> Iterator[str]:
if section_data is not None:
yield from frozenset(
e.domain
- for e in _read_tokenized_entries(section_data, re.compile('.*')))
- else: # Check for the legacy domain sections
- for section in reader.sections:
- match = _LEGACY_STRING_SECTIONS.match(section.name)
- if match:
- yield match.group('domain')
+ for e in _read_tokenized_entries(section_data, re.compile('.*'))
+ )
def read_tokenizer_metadata(elf) -> Dict[str, int]:
@@ -160,24 +144,25 @@ def read_tokenizer_metadata(elf) -> Dict[str, int]:
try:
metadata[key.rstrip(b'\0').decode()] = value
except UnicodeDecodeError as err:
- _LOG.error('Failed to decode metadata key %r: %s',
- key.rstrip(b'\0'), err)
+ _LOG.error(
+ 'Failed to decode metadata key %r: %s',
+ key.rstrip(b'\0'),
+ err,
+ )
return metadata
def _database_from_strings(strings: List[str]) -> tokens.Database:
"""Generates a C and C++ compatible database from untokenized strings."""
- # Generate a C compatible database from the fixed length hash.
- c_db = tokens.Database.from_strings(
- strings,
- tokenize=lambda string: tokens.pw_tokenizer_65599_hash(
- string, tokens.DEFAULT_C_HASH_LENGTH))
+ # Generate a C-compatible database from the fixed length hash.
+ c_db = tokens.Database.from_strings(strings, tokenize=tokens.c_hash)
# Generate a C++ compatible database by allowing the hash to follow the
# string length.
cpp_db = tokens.Database.from_strings(
- strings, tokenize=tokens.pw_tokenizer_65599_hash)
+ strings, tokenize=tokens.pw_tokenizer_65599_hash
+ )
# Use a union of the C and C++ compatible databases.
return tokens.Database.merged(c_db, cpp_db)
@@ -187,7 +172,9 @@ def _database_from_json(fd) -> tokens.Database:
return _database_from_strings(json.load(fd))
-def _load_token_database(db, domain: Pattern[str]) -> tokens.Database:
+def _load_token_database( # pylint: disable=too-many-return-statements
+ db, domain: Pattern[str]
+) -> tokens.Database:
"""Loads a Database from supported database types.
Supports Database objects, JSONs, ELFs, CSVs, and binary databases.
@@ -204,8 +191,10 @@ def _load_token_database(db, domain: Pattern[str]) -> tokens.Database:
# If it's a str, it might be a path. Check if it's an ELF, CSV, or JSON.
if isinstance(db, (str, Path)):
if not os.path.exists(db):
- raise FileNotFoundError(
- f'"{db}" is not a path to a token database')
+ raise FileNotFoundError(f'"{db}" is not a path to a token database')
+
+ if Path(db).is_dir():
+ return tokens.DatabaseFile.load(Path(db))
# Read the path as an ELF file.
with open(db, 'rb') as fd:
@@ -214,11 +203,11 @@ def _load_token_database(db, domain: Pattern[str]) -> tokens.Database:
# Generate a database from JSON.
if str(db).endswith('.json'):
- with open(db, 'r') as json_fd:
+ with open(db, 'r', encoding='utf-8') as json_fd:
return _database_from_json(json_fd)
# Read the path as a packed binary or CSV file.
- return tokens.DatabaseFile(db)
+ return tokens.DatabaseFile.load(Path(db))
# Assume that it's a file object and check if it's an ELF.
if elf_reader.compatible_file(db):
@@ -230,23 +219,23 @@ def _load_token_database(db, domain: Pattern[str]) -> tokens.Database:
if db.name.endswith('.json'):
return _database_from_json(db)
- return tokens.DatabaseFile(db.name)
+ return tokens.DatabaseFile.load(Path(db.name))
# Read CSV directly from the file object.
return tokens.Database(tokens.parse_csv(db))
def load_token_database(
- *databases,
- domain: Union[str,
- Pattern[str]] = tokens.DEFAULT_DOMAIN) -> tokens.Database:
+ *databases, domain: Union[str, Pattern[str]] = tokens.DEFAULT_DOMAIN
+) -> tokens.Database:
"""Loads a Database from supported database types.
Supports Database objects, JSONs, ELFs, CSVs, and binary databases.
"""
domain = re.compile(domain)
- return tokens.Database.merged(*(_load_token_database(db, domain)
- for db in databases))
+ return tokens.Database.merged(
+ *(_load_token_database(db, domain) for db in databases)
+ )
def database_summary(db: tokens.Database) -> Dict[str, Any]:
@@ -275,77 +264,128 @@ def generate_reports(paths: Iterable[Path]) -> _DatabaseReport:
reports: _DatabaseReport = {}
for path in paths:
- with path.open('rb') as file:
- if elf_reader.compatible_file(file):
- domains = list(tokenization_domains(file))
- else:
- domains = ['']
+ domains = ['']
+ if path.is_file():
+ with path.open('rb') as file:
+ if elf_reader.compatible_file(file):
+ domains = list(tokenization_domains(file))
domain_reports = {}
for domain in domains:
domain_reports[domain] = database_summary(
- load_token_database(path, domain=domain))
+ load_token_database(path, domain=domain)
+ )
reports[str(path)] = domain_reports
return reports
-def _handle_create(databases, database, force, output_type, include, exclude,
- replace):
+def _handle_create(
+ databases,
+ database: Path,
+ force: bool,
+ output_type: str,
+ include: list,
+ exclude: list,
+ replace: list,
+) -> None:
"""Creates a token database file from one or more ELF files."""
+ if not force and database.exists():
+ raise FileExistsError(
+ f'The file {database} already exists! Use --force to overwrite.'
+ )
+
+ if output_type == 'directory':
+ if str(database) == '-':
+ raise ValueError(
+ 'Cannot specify "-" (stdout) for directory databases'
+ )
+
+ database.mkdir(exist_ok=True)
+ database = database / f'database{tokens.DIR_DB_SUFFIX}'
+ output_type = 'csv'
- if database == '-':
+ if str(database) == '-':
# Must write bytes to stdout; use sys.stdout.buffer.
fd = sys.stdout.buffer
- elif not force and os.path.exists(database):
- raise FileExistsError(
- f'The file {database} already exists! Use --force to overwrite.')
else:
- fd = open(database, 'wb')
+ fd = database.open('wb')
- database = tokens.Database.merged(*databases)
- database.filter(include, exclude, replace)
+ db = tokens.Database.merged(*databases)
+ db.filter(include, exclude, replace)
with fd:
if output_type == 'csv':
- tokens.write_csv(database, fd)
+ tokens.write_csv(db, fd)
elif output_type == 'binary':
- tokens.write_binary(database, fd)
+ tokens.write_binary(db, fd)
else:
raise ValueError(f'Unknown database type "{output_type}"')
- _LOG.info('Wrote database with %d entries to %s as %s', len(database),
- fd.name, output_type)
+ _LOG.info(
+ 'Wrote database with %d entries to %s as %s',
+ len(db),
+ fd.name,
+ output_type,
+ )
-def _handle_add(token_database, databases):
+def _handle_add(
+ token_database: tokens.DatabaseFile,
+ databases: List[tokens.Database],
+ commit: Optional[str],
+) -> None:
initial = len(token_database)
+ if commit:
+ entries = itertools.chain.from_iterable(
+ db.entries() for db in databases
+ )
+ token_database.add_and_discard_temporary(entries, commit)
+ else:
+ for source in databases:
+ token_database.add(source.entries())
- for source in databases:
- token_database.add(source.entries())
+ token_database.write_to_file()
- token_database.write_to_file()
+ number_of_changes = len(token_database) - initial
- _LOG.info('Added %d entries to %s',
- len(token_database) - initial, token_database.path)
+ if number_of_changes:
+ _LOG.info(
+ 'Added %d entries to %s', number_of_changes, token_database.path
+ )
-def _handle_mark_removed(token_database, databases, date):
+def _handle_mark_removed(
+ token_database: tokens.DatabaseFile,
+ databases: List[tokens.Database],
+ date: Optional[datetime],
+):
marked_removed = token_database.mark_removed(
- (entry for entry in tokens.Database.merged(*databases).entries()
- if not entry.date_removed), date)
+ (
+ entry
+ for entry in tokens.Database.merged(*databases).entries()
+ if not entry.date_removed
+ ),
+ date,
+ )
- token_database.write_to_file()
+ token_database.write_to_file(rewrite=True)
- _LOG.info('Marked %d of %d entries as removed in %s', len(marked_removed),
- len(token_database), token_database.path)
+ _LOG.info(
+ 'Marked %d of %d entries as removed in %s',
+ len(marked_removed),
+ len(token_database),
+ token_database.path,
+ )
-def _handle_purge(token_database, before):
+def _handle_purge(
+ token_database: tokens.DatabaseFile, before: Optional[datetime]
+):
purged = token_database.purge(before)
- token_database.write_to_file()
+ token_database.write_to_file(rewrite=True)
_LOG.info('Removed %d entries from %s', len(purged), token_database.path)
@@ -371,23 +411,28 @@ def expand_paths_or_globs(*paths_or_globs: str) -> Iterable[Path]:
for path in paths:
# Resolve globs to JSON, CSV, or compatible binary files.
if elf_reader.compatible_file(path) or path.endswith(
- ('.csv', '.json')):
+ ('.csv', '.json')
+ ):
yield Path(path)
class ExpandGlobs(argparse.Action):
"""Argparse action that expands and appends paths."""
+
def __call__(self, parser, namespace, values, unused_option_string=None):
setattr(namespace, self.dest, list(expand_paths_or_globs(*values)))
-def _read_elf_with_domain(elf: str,
- domain: Pattern[str]) -> Iterable[tokens.Database]:
+def _read_elf_with_domain(
+ elf: str, domain: Pattern[str]
+) -> Iterable[tokens.Database]:
for path in expand_paths_or_globs(elf):
with path.open('rb') as file:
if not elf_reader.compatible_file(file):
- raise ValueError(f'{elf} is not an ELF file, '
- f'but the "{domain}" domain was specified')
+ raise ValueError(
+ f'{elf} is not an ELF file, '
+ f'but the "{domain}" domain was specified'
+ )
yield _database_from_elf(file, domain)
@@ -398,6 +443,7 @@ class LoadTokenDatabases(argparse.Action):
ELF files may have #domain appended to them to specify a tokenization domain
other than the default.
"""
+
def __call__(self, parser, namespace, values, option_string=None):
databases: List[tokens.Database] = []
paths: Set[Path] = set()
@@ -417,13 +463,16 @@ class LoadTokenDatabases(argparse.Action):
parser.error(
f'argument elf_or_token_database: {path} is not a supported '
'token database file. Only ELF files or token databases (CSV '
- f'or binary format) are supported. {err}. ')
+ f'or binary format) are supported. {err}. '
+ )
except FileNotFoundError as err:
parser.error(f'argument elf_or_token_database: {err}')
except: # pylint: disable=bare-except
_LOG.exception('Failed to load token database %s', path)
- parser.error('argument elf_or_token_database: '
- f'Error occurred while loading token database {path}')
+ parser.error(
+ 'argument elf_or_token_database: '
+ f'Error occurred while loading token database {path}'
+ )
setattr(namespace, self.dest, databases)
@@ -439,112 +488,139 @@ def token_databases_parser(nargs: str = '+') -> argparse.ArgumentParser:
metavar='elf_or_token_database',
nargs=nargs,
action=LoadTokenDatabases,
- help=('ELF or token database files from which to read strings and '
- 'tokens. For ELF files, the tokenization domain to read from '
- 'may specified after the path as #domain_name (e.g. '
- 'foo.elf#TEST_DOMAIN). Unless specified, only the default '
- 'domain ("") is read from ELF files; .* reads all domains. '
- 'Globs are expanded to compatible database files.'))
+ help=(
+ 'ELF or token database files from which to read strings and '
+ 'tokens. For ELF files, the tokenization domain to read from '
+ 'may specified after the path as #domain_name (e.g. '
+ 'foo.elf#TEST_DOMAIN). Unless specified, only the default '
+ 'domain ("") is read from ELF files; .* reads all domains. '
+ 'Globs are expanded to compatible database files.'
+ ),
+ )
return parser
def _parse_args():
"""Parse and return command line arguments."""
+
def year_month_day(value) -> datetime:
if value == 'today':
return datetime.now()
- return datetime.strptime(value, tokens.DATE_FORMAT)
+ return datetime.fromisoformat(value)
year_month_day.__name__ = 'year-month-day (YYYY-MM-DD)'
# Shared command line options.
option_db = argparse.ArgumentParser(add_help=False)
- option_db.add_argument('-d',
- '--database',
- dest='token_database',
- type=tokens.DatabaseFile,
- required=True,
- help='The database file to update.')
+ option_db.add_argument(
+ '-d',
+ '--database',
+ dest='token_database',
+ type=lambda arg: tokens.DatabaseFile.load(Path(arg)),
+ required=True,
+ help='The database file to update.',
+ )
option_tokens = token_databases_parser('*')
# Top-level argument parser.
parser = argparse.ArgumentParser(
description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
parser.set_defaults(handler=lambda **_: parser.print_help())
subparsers = parser.add_subparsers(
- help='Tokenized string database management actions:')
+ help='Tokenized string database management actions:'
+ )
# The 'create' command creates a database file.
subparser = subparsers.add_parser(
'create',
parents=[option_tokens],
- help=
- 'Creates a database with tokenized strings from one or more sources.')
+ help=(
+ 'Creates a database with tokenized strings from one or more '
+ 'sources.'
+ ),
+ )
subparser.set_defaults(handler=_handle_create)
subparser.add_argument(
'-d',
'--database',
required=True,
- help='Path to the database file to create; use - for stdout.')
+ type=Path,
+ help='Path to the database file to create; use - for stdout.',
+ )
subparser.add_argument(
'-t',
'--type',
dest='output_type',
- choices=('csv', 'binary'),
+ choices=('csv', 'binary', 'directory'),
default='csv',
- help='Which type of database to create. (default: csv)')
- subparser.add_argument('-f',
- '--force',
- action='store_true',
- help='Overwrite the database if it exists.')
+ help='Which type of database to create. (default: csv)',
+ )
+ subparser.add_argument(
+ '-f',
+ '--force',
+ action='store_true',
+ help='Overwrite the database if it exists.',
+ )
subparser.add_argument(
'-i',
'--include',
type=re.compile,
default=[],
action='append',
- help=('If provided, at least one of these regular expressions must '
- 'match for a string to be included in the database.'))
+ help=(
+ 'If provided, at least one of these regular expressions must '
+ 'match for a string to be included in the database.'
+ ),
+ )
subparser.add_argument(
'-e',
'--exclude',
type=re.compile,
default=[],
action='append',
- help=('If provided, none of these regular expressions may match for a '
- 'string to be included in the database.'))
+ help=(
+ 'If provided, none of these regular expressions may match for a '
+ 'string to be included in the database.'
+ ),
+ )
unescaped_slash = re.compile(r'(?<!\\)/')
def replacement(value: str) -> Tuple[Pattern, 'str']:
try:
find, sub = unescaped_slash.split(value, 1)
- except ValueError as err:
+ except ValueError as _err:
raise argparse.ArgumentTypeError(
- 'replacements must be specified as "search_regex/replacement"')
+ 'replacements must be specified as "search_regex/replacement"'
+ )
try:
return re.compile(find.replace(r'\/', '/')), sub
except re.error as err:
raise argparse.ArgumentTypeError(
- f'"{value}" is not a valid regular expression: {err}')
+ f'"{value}" is not a valid regular expression: {err}'
+ )
subparser.add_argument(
'--replace',
type=replacement,
default=[],
action='append',
- help=('If provided, replaces text that matches a regular expression. '
- 'This can be used to replace sensitive terms in a token '
- 'database that will be distributed publicly. The expression and '
- 'replacement are specified as "search_regex/replacement". '
- 'Plain slash characters in the regex must be escaped with a '
- r'backslash (\/). The replacement text may include '
- 'backreferences for captured groups in the regex.'))
+ help=(
+ 'If provided, replaces text that matches a regular expression. '
+ 'This can be used to replace sensitive terms in a token '
+ 'database that will be distributed publicly. The expression and '
+ 'replacement are specified as "search_regex/replacement". '
+ 'Plain slash characters in the regex must be escaped with a '
+ r'backslash (\/). The replacement text may include '
+ 'backreferences for captured groups in the regex.'
+ ),
+ )
# The 'add' command adds strings to a database from a set of ELFs.
subparser = subparsers.add_parser(
@@ -553,8 +629,20 @@ def _parse_args():
help=(
'Adds new strings to a database with tokenized strings from a set '
'of ELF files or other token databases. Missing entries are NOT '
- 'marked as removed.'))
+ 'marked as removed.'
+ ),
+ )
subparser.set_defaults(handler=_handle_add)
+ subparser.add_argument(
+ '--discard-temporary',
+ dest='commit',
+ help=(
+ 'Deletes temporary tokens in memory and on disk when a CSV exists '
+ 'within a commit. Afterwards, new strings are added to the '
+ 'database from a set of ELF files or other token databases. '
+ 'Missing entries are NOT marked as removed.'
+ ),
+ )
# The 'mark_removed' command marks removed entries to match a set of ELFs.
subparser = subparsers.add_parser(
@@ -563,43 +651,57 @@ def _parse_args():
help=(
'Updates a database with tokenized strings from a set of strings. '
'Strings not present in the set remain in the database but are '
- 'marked as removed. New strings are NOT added.'))
+ 'marked as removed. New strings are NOT added.'
+ ),
+ )
subparser.set_defaults(handler=_handle_mark_removed)
subparser.add_argument(
'--date',
type=year_month_day,
- help=('The removal date to use for all strings. '
- 'May be YYYY-MM-DD or "today". (default: today)'))
+ help=(
+ 'The removal date to use for all strings. '
+ 'May be YYYY-MM-DD or "today". (default: today)'
+ ),
+ )
# The 'purge' command removes old entries.
subparser = subparsers.add_parser(
'purge',
parents=[option_db],
- help='Purges removed strings from a database.')
+ help='Purges removed strings from a database.',
+ )
subparser.set_defaults(handler=_handle_purge)
subparser.add_argument(
'-b',
'--before',
type=year_month_day,
- help=('Delete all entries removed on or before this date. '
- 'May be YYYY-MM-DD or "today".'))
+ help=(
+ 'Delete all entries removed on or before this date. '
+ 'May be YYYY-MM-DD or "today".'
+ ),
+ )
# The 'report' command prints a report about a database.
- subparser = subparsers.add_parser('report',
- help='Prints a report about a database.')
+ subparser = subparsers.add_parser(
+ 'report', help='Prints a report about a database.'
+ )
subparser.set_defaults(handler=_handle_report)
subparser.add_argument(
'token_database_or_elf',
nargs='+',
action=ExpandGlobs,
- help='The ELF files or token databases about which to generate reports.'
+ help=(
+ 'The ELF files or token databases about which to generate '
+ 'reports.'
+ ),
)
subparser.add_argument(
'-o',
'--output',
type=argparse.FileType('w'),
default=sys.stdout,
- help='The file to which to write the output; use - for stdout.')
+ help='The file to which to write the output; use - for stdout.',
+ )
args = parser.parse_args()
@@ -616,7 +718,9 @@ def _init_logging(level: int) -> None:
log_to_stderr.setFormatter(
logging.Formatter(
fmt='%(asctime)s.%(msecs)03d-%(levelname)s: %(message)s',
- datefmt='%H:%M:%S'))
+ datefmt='%H:%M:%S',
+ )
+ )
_LOG.addHandler(log_to_stderr)
diff --git a/pw_tokenizer/py/pw_tokenizer/decode.py b/pw_tokenizer/py/pw_tokenizer/decode.py
index f6ca50364..4796fb05e 100644
--- a/pw_tokenizer/py/pw_tokenizer/decode.py
+++ b/pw_tokenizer/py/pw_tokenizer/decode.py
@@ -21,9 +21,18 @@ in the resulting string with an error message.
"""
from datetime import datetime
+import math
import re
import struct
-from typing import Iterable, List, NamedTuple, Match, Sequence, Tuple
+from typing import (
+ Iterable,
+ List,
+ NamedTuple,
+ Match,
+ Optional,
+ Sequence,
+ Tuple,
+)
def zigzag_decode(value: int) -> int:
@@ -34,21 +43,206 @@ def zigzag_decode(value: int) -> int:
class FormatSpec:
- """Represents a format specifier parsed from a printf-style string."""
+ """Represents a format specifier parsed from a printf-style string.
+
+ This implementation is designed to align with the C99 specification,
+ section 7.19.6
+ (https://www.dii.uchile.cl/~daespino/files/Iso_C_1999_definition.pdf).
+ Notably, this specification is slightly different than what is implemented
+ in most compilers due to each compiler choosing to interpret undefined
+ behavior in slightly different ways. Treat the following description as the
+ source of truth.
+
+ This implementation supports:
+ - Overall Format: `%[flags][width][.precision][length][specifier]`
+ - Flags (Zero or More)
+ - `-`: Left-justify within the given field width; Right justification is
+ the default (see Width modifier).
+ - `+`: Forces to preceed the result with a plus or minus sign (`+` or `-`)
+ even for positive numbers. By default, only negative numbers are
+ preceded with a `-` sign.
+ - ` ` (space): If no sign is going to be written, a blank space is
+ inserted before the value.
+ - `#`: Specifies an alternative print syntax should be used.
+ - Used with `o`, `x` or `X` specifiers the value is preceeded with `0`,
+ `0x`, or `0X`, respectively, for values different than zero.
+ - Used with `a`, `A`, `e`, `E`, `f`, `F`, `g`, or `G` it forces the
+ written output to contain a decimal point even if no more digits
+ follow. By default, if no digits follow, no decimal point is written.
+ - `0`: Left-pads the number with zeroes (`0`) instead of spaces when
+ padding is specified (see width sub-specifier).
+ - Width (Optional)
+ - ``(number)``: Minimum number of characters to be printed. If the value
+ to be printed is shorter than this number, the result is
+ padded with blank spaces or `0` if the `0` flag is
+ present. The value is not truncated even if the result is
+ larger. If the value is negative and the `0` flag is
+ present, the `0`s are padded after the `-` symbol.
+ - `*`: The width is not specified in the format string, but as an
+ additional integer value argument preceding the argument that has
+ to be formatted.
+ - Precision (Optional)
+ - `.(number)`
+ - For `d`, `i`, `o`, `u`, `x`, `X`, specifies the minimum number of
+ digits to be written. If the value to be written is shorter than this
+ number, the result is padded with leading zeros. The value is not
+ truncated even if the result is longer.
+ - A precision of `0` means that no character is written for the value
+ `0`.
+ - For `a`, `A`, `e`, `E`, `f`, and `F`, specifies the number of digits
+ to be printed after the decimal point. By default, this is `6`.
+ - For `g` and `G`, specifies the maximum number of significant digits to
+ be printed.
+ - For `s`, specifies the maximum number of characters to be printed. By
+ default all characters are printed until the ending null character is
+ encountered.
+ - If the period is specified without an explicit value for precision,
+ `0` is assumed.
+ - `.*`: The precision is not specified in the format string, but as an
+ additional integer value argument preceding the argument that has
+ to be formatted.
+ - Length (Optional)
+ - `hh`: Usable with `d`, `i`, `o`, `u`, `x`, or `X` specifiers to convey
+ the argument will be a `signed char` or `unsigned char`. However,
+ this is largely ignored in the implementation due to it not being
+ necessary for Python or argument decoding (since the argument is
+ always encoded at least as a 32-bit integer).
+ - `h`: Usable with `d`, `i`, `o`, `u`, `x`, or `X` specifiers to convey
+ the argument will be a `signed short int` or `unsigned short int`.
+ However, this is largely ignored in the implementation due to it
+ not being necessary for Python or argument decoding (since the
+ argument is always encoded at least as a 32-bit integer).
+ - `l`: Usable with `d`, `i`, `o`, `u`, `x`, or `X` specifiers to convey
+ the argument will be a `signed long int` or `unsigned long int`.
+ Also is usable with `c` and `s` to specify that the arguments will
+ be encoded with `wchar_t` values (which isn't different from normal
+ `char` values). However, this is largely ignored in the
+ implementation due to it not being necessary for Python or argument
+ decoding (since the argument is always encoded at least as a 32-bit
+ integer).
+ - `ll`: Usable with `d`, `i`, `o`, `u`, `x`, or `X` specifiers to convey
+ the argument will be a `signed long long int` or
+ `unsigned long long int`. This is required to properly decode the
+ argument as a 64-bit integer.
+ - `L`: Usable with `a`, `A`, `e`, `E`, `f`, `F`, `g`, or `G` conversion
+ specifiers applies to a long double argument. However, this is
+ ignored in the implementation due to floating point value encoded
+ that is unaffected by bit width.
+ - `j`: Usable with `d`, `i`, `o`, `u`, `x`, or `X` specifiers to convey
+ the argument will be a `intmax_t` or `uintmax_t`.
+ - `z`: Usable with `d`, `i`, `o`, `u`, `x`, or `X` specifiers to convey
+ the argument will be a `size_t`. This will force the argument to be
+ decoded as an unsigned integer.
+ - `t`: Usable with `d`, `i`, `o`, `u`, `x`, or `X` specifiers to convey
+ the argument will be a `ptrdiff_t`.
+ - If a length modifier is provided for an incorrect specifier, it is
+ ignored.
+ - Specifier (Required)
+ - `d` / `i`: Used for signed decimal integers.
+ - `u`: Used for unsigned decimal integers.
+ - `o`: Used for unsigned decimal integers and specifies formatting should
+ be as an octal number.
+ - `x`: Used for unsigned decimal integers and specifies formatting should
+ be as a hexadecimal number using all lowercase letters.
+ - `X`: Used for unsigned decimal integers and specifies formatting should
+ be as a hexadecimal number using all uppercase letters.
+ - `f`: Used for floating-point values and specifies to use lowercase,
+ decimal floating point formatting.
+ - Default precision is `6` decimal places unless explicitly specified.
+ - `F`: Used for floating-point values and specifies to use uppercase,
+ decimal floating point formatting.
+ - Default precision is `6` decimal places unless explicitly specified.
+ - `e`: Used for floating-point values and specifies to use lowercase,
+ exponential (scientific) formatting.
+ - Default precision is `6` decimal places unless explicitly specified.
+ - `E`: Used for floating-point values and specifies to use uppercase,
+ exponential (scientific) formatting.
+ - Default precision is `6` decimal places unless explicitly specified.
+ - `g`: Used for floating-point values and specified to use `f` or `e`
+ formatting depending on which would be the shortest representation.
+ - Precision specifies the number of significant digits, not just digits
+ after the decimal place.
+ - If the precision is specified as `0`, it is interpreted to mean `1`.
+ - `e` formatting is used if the the exponent would be less than `-4` or
+ is greater than or equal to the precision.
+ - Trailing zeros are removed unless the `#` flag is set.
+ - A decimal point only appears if it is followed by a digit.
+ - `NaN` or infinities always follow `f` formatting.
+ - `G`: Used for floating-point values and specified to use `f` or `e`
+ formatting depending on which would be the shortest representation.
+ - Precision specifies the number of significant digits, not just digits
+ after the decimal place.
+ - If the precision is specified as `0`, it is interpreted to mean `1`.
+ - `E` formatting is used if the the exponent would be less than `-4` or
+ is greater than or equal to the precision.
+ - Trailing zeros are removed unless the `#` flag is set.
+ - A decimal point only appears if it is followed by a digit.
+ - `NaN` or infinities always follow `F` formatting.
+ - `c`: Used for formatting a `char` value.
+ - `s`: Used for formatting a string of `char` values.
+ - If width is specified, the null terminator character is included as a
+ character for width count.
+ - If precision is specified, no more `char`s than that value will be
+ written from the string (padding is used to fill additional width).
+ - `p`: Used for formatting a pointer address.
+ - `%`: Prints a single `%`. Only valid as `%%` (supports no flags, width,
+ precision, or length modifiers).
+
+ Underspecified details:
+ - If both `+` and ` ` flags appear, the ` ` is ignored.
+ - The `+` and ` ` flags will error if used with `c` or `s`.
+ - The `#` flag will error if used with `d`, `i`, `u`, `c`, `s`, or `p`.
+ - The `0` flag will error if used with `c`, `s`, or `p`.
+ - Both `+` and ` ` can work with the unsigned integer specifiers `u`, `o`,
+ `x`, and `X`.
+ - If a length modifier is provided for an incorrect specifier, it is
+ ignored.
+ - The `z` length modifier will decode arugments as signed as long as `d` or
+ `i` is used.
+ - `p` is implementation defined. For this implementation, it will print
+ with a `0x` prefix and then the pointer value was printed using `%08X`.
+ `p` supports the `+`, `-`, and ` ` flags, but not the `#` or `0` flags.
+ None of the length modifiers are usable with `p`. This implementation will
+ try to adhere to user-specified width (assuming the width provided is
+ larger than the guaranteed minimum of 10). Specifying precision for `p` is
+ considered an error.
+ - Only `%%` is allowed with no other modifiers. Things like `%+%` will fail
+ to decode. Some C stdlib implementations support any modifiers being
+ present between `%`, but ignore any for the output.
+ - If a width is specified with the `0` flag for a negative value, the padded
+ `0`s will appear after the `-` symbol.
+ - A precision of `0` for `d`, `i`, `u`, `o`, `x`, or `X` means that no
+ character is written for the value `0`.
+ - Precision cannot be specified for `c`.
+ - Using `*` or fixed precision with the `s` specifier still requires the
+ string argument to be null-terminated. This is due to argument encoding
+ happening on the C/C++-side while the precision value is not read or
+ otherwise used until decoding happens in this Python code.
+
+ Non-conformant details:
+ - `n` specifier: We do not support the `n` specifier since it is impossible
+ for us to retroactively tell the original program how many
+ characters have been printed since this decoding happens a
+ great deal of time after the device sent it, usually on a
+ separate processing device entirely.
+ """
# Regular expression for finding format specifiers.
- FORMAT_SPEC = re.compile(r'%(?:(?P<flags>[+\- #0]*\d*(?:\.\d+)?)'
- r'(?P<length>hh|h|ll|l|j|z|t|L)?'
- r'(?P<type>[csdioxXufFeEaAgGnp])|%)')
+ FORMAT_SPEC = re.compile(
+ r'%(?P<flags>[+\- #0]+)?'
+ r'(?P<width>\d+|\*)?'
+ r'(?P<precision>\.(?:\d*|\*))?'
+ r'(?P<length>hh|h|ll|l|j|z|t|L)?'
+ r'(?P<type>[csdioxXufFeEaAgGnp%])'
+ )
# Conversions to make format strings Python compatible.
- _UNSUPPORTED_LENGTH = frozenset(['hh', 'll', 'j', 'z', 't'])
- _REMAP_TYPE = {'a': 'f', 'A': 'F'}
+ _REMAP_TYPE = {'a': 'f', 'A': 'F', 'p': 'X'}
# Conversion specifiers by type; n is not supported.
- _SIGNED_INT = 'di'
- _UNSIGNED_INT = frozenset('oxXup')
- _FLOATING_POINT = frozenset('fFeEaAgG')
+ SIGNED_INT = frozenset('di')
+ UNSIGNED_INT = frozenset('oxXup')
+ FLOATING_POINT = frozenset('fFeEaAgG')
_PACKED_FLOAT = struct.Struct('<f')
@@ -60,7 +254,9 @@ class FormatSpec:
if not match:
raise ValueError(
'{!r} is not a valid single format specifier'.format(
- format_specifier))
+ format_specifier
+ )
+ )
return cls(match)
@@ -70,48 +266,204 @@ class FormatSpec:
self.specifier: str = self.match.group()
self.flags: str = self.match.group('flags') or ''
+ self.width: str = self.match.group('width') or ''
+ self.precision: str = self.match.group('precision') or ''
self.length: str = self.match.group('length') or ''
-
- # If there is no type, the format spec is %%.
- self.type: str = self.match.group('type') or '%'
-
- # %p prints as 0xFEEDBEEF; other specs may need length/type switched
+ self.type: str = self.match.group('type')
+
+ self.error = None
+ if self.type == 'n':
+ self.error = 'Unsupported conversion specifier n.'
+ elif self.type == '%':
+ if self.flags or self.width or self.precision or self.length:
+ self.error = (
+ '%% does not support any flags, width, precision,'
+ 'or length modifiers.'
+ )
+ elif self.type in 'csdiup' and '#' in self.flags:
+ self.error = (
+ '# is only supported with o, x, X, f, F, e, E, a, A, '
+ 'g, and G specifiers.'
+ )
+ elif self.type in 'csp' and '0' in self.flags:
+ self.error = (
+ '0 is only supported with d, i, o, u, x, X, a, A, e, '
+ 'E, f, F, g, and G specifiers.'
+ )
+ elif self.type in 'cs' and ('+' in self.flags or ' ' in self.flags):
+ self.error = (
+ '+ and space are only available for d, i, o, u, x, X,'
+ 'a, A, e, E, f, F, g, and G specifiers.'
+ )
+ elif self.type == 'c':
+ if self.precision != '':
+ self.error = 'Precision is not supported for specifier c.'
+ elif self.type == 'p':
+ if self.length != '':
+ self.error = 'p does not support any length modifiers.'
+ elif self.precision != '':
+ self.error = 'p does not support precision modifiers.'
+
+ # If we are going to add additional characters to the output, we add to
+ # width_bias to ensure user-provided widths are reduced by that amount.
+ self._width_bias = 0
+ # Some of our machinery requires that we maintain a minimum precision
+ # width to ensure a certain amount of digits gets printed. This
+ # increases the user-provided precision in these cases if it was not
+ # enough.
+ self._minimum_precision = 0
+ # Python's handling of %#o is non-standard and prepends a 0o
+ # instead of single 0.
+ if self.type == 'o' and '#' in self.flags:
+ self._width_bias = 1
+ # Python does not support %p natively.
if self.type == 'p':
- self.compatible = '0x%08X'
- else:
- self.compatible = ''.join([
- '%', self.flags,
- '' if self.length in self._UNSUPPORTED_LENGTH else '',
- self._REMAP_TYPE.get(self.type, self.type)
- ])
+ self._width_bias = 2
+ self._minimum_precision = 8
+
+ # If we have a concrete width, we reduce it by any width bias.
+ # Otherwise, we either have no width or width is *, where the decoding
+ # logic will handle the width bias.
+ parsed_width = int(self.width.replace('*', '') or '0')
+ if parsed_width > self._width_bias:
+ self.width = f'{parsed_width - self._width_bias}'
+
+ # Python %-operator does not support `.` without a
+ # trailing number. `.` is defined to be equivalent to `.0`.
+ if self.precision == '.':
+ self.precision = '.0'
+
+ # If we have a concrete precision that is not *, we check that it is at
+ # least minimum precision. If it is *, other parts of decoding will
+ # ensure the minimum is upheld.
+ if (
+ self.precision != '.*'
+ and int(self.precision.replace('.', '') or '0')
+ < self._minimum_precision
+ ):
+ self.precision = f'.{self._minimum_precision}'
+
+ # The Python %-format machinery never requires the length
+ # modifier to work correctly, and it doesn't support all of the
+ # C99 length format specifiers anyway. We remove it from the
+ # python-compaitble format string.
+ self.compatible = ''.join(
+ [
+ '%',
+ self.flags,
+ self.width,
+ self.precision,
+ self._REMAP_TYPE.get(self.type, self.type),
+ ]
+ )
def decode(self, encoded_arg: bytes) -> 'DecodedArg':
"""Decodes the provided data according to this format specifier."""
- if self.type == '%': # literal %
- return DecodedArg(self, (),
- b'') # Use () as the value for % formatting.
-
- if self.type == 's': # string
- return self._decode_string(encoded_arg)
-
- if self.type == 'c': # character
- return self._decode_char(encoded_arg)
-
- if self.type in self._SIGNED_INT:
- return self._decode_signed_integer(encoded_arg)
-
- if self.type in self._UNSIGNED_INT:
- return self._decode_unsigned_integer(encoded_arg)
+ if self.error is not None:
+ return DecodedArg(
+ self, None, b'', DecodedArg.DECODE_ERROR, self.error
+ )
+
+ width = None
+ if self.width == '*':
+ width = FormatSpec.from_string('%d').decode(encoded_arg)
+ encoded_arg = encoded_arg[len(width.raw_data) :]
+
+ precision = None
+ if self.precision == '.*':
+ precision = FormatSpec.from_string('%d').decode(encoded_arg)
+ encoded_arg = encoded_arg[len(precision.raw_data) :]
+
+ if self.type == '%':
+ return DecodedArg(
+ self, (), b''
+ ) # Use () as the value for % formatting.
- if self.type in self._FLOATING_POINT:
- return self._decode_float(encoded_arg)
-
- # Unsupported specifier (e.g. %n)
- return DecodedArg(
- self, None, b'', DecodedArg.DECODE_ERROR,
- 'Unsupported conversion specifier "{}"'.format(self.type))
-
- def _decode_signed_integer(self, encoded: bytes) -> 'DecodedArg':
+ if self.type == 's':
+ return self._merge_decoded_args(
+ width, precision, self._decode_string(encoded_arg)
+ )
+
+ if self.type == 'c':
+ return self._merge_decoded_args(
+ width, precision, self._decode_char(encoded_arg)
+ )
+
+ if self.type in self.SIGNED_INT:
+ return self._merge_decoded_args(
+ width, precision, self._decode_signed_integer(encoded_arg)
+ )
+
+ if self.type in self.UNSIGNED_INT:
+ return self._merge_decoded_args(
+ width, precision, self._decode_unsigned_integer(encoded_arg)
+ )
+
+ if self.type in self.FLOATING_POINT:
+ return self._merge_decoded_args(
+ width, precision, self._decode_float(encoded_arg)
+ )
+
+ # Should be unreachable.
+ assert False, f'Unhandled format specifier: {self.type}'
+
+ def text_float_safe_compatible(self) -> str:
+ return ''.join(
+ [
+ '%',
+ self.flags.replace('0', ' '),
+ self.width,
+ self.precision,
+ self._REMAP_TYPE.get(self.type, self.type),
+ ]
+ )
+
+ def _merge_decoded_args(
+ self,
+ width: Optional['DecodedArg'],
+ precision: Optional['DecodedArg'],
+ main: 'DecodedArg',
+ ) -> 'DecodedArg':
+ def merge_optional_str(*args: Optional[str]) -> Optional[str]:
+ return ' '.join(a for a in args if a) or None
+
+ if width is not None and precision is not None:
+ return DecodedArg(
+ main.specifier,
+ (
+ width.value - self._width_bias,
+ max(precision.value, self._minimum_precision),
+ main.value,
+ ),
+ width.raw_data + precision.raw_data + main.raw_data,
+ width.status | precision.status | main.status,
+ merge_optional_str(width.error, precision.error, main.error),
+ )
+
+ if width is not None:
+ return DecodedArg(
+ main.specifier,
+ (width.value - self._width_bias, main.value),
+ width.raw_data + main.raw_data,
+ width.status | main.status,
+ merge_optional_str(width.error, main.error),
+ )
+
+ if precision is not None:
+ return DecodedArg(
+ main.specifier,
+ (max(precision.value, self._minimum_precision), main.value),
+ precision.raw_data + main.raw_data,
+ precision.status | main.status,
+ merge_optional_str(precision.error, main.error),
+ )
+
+ return main
+
+ def _decode_signed_integer(
+ self,
+ encoded: bytes,
+ ) -> 'DecodedArg':
"""Decodes a signed variable-length integer."""
if not encoded:
return DecodedArg.missing(self)
@@ -122,21 +474,31 @@ class FormatSpec:
for byte in encoded:
count += 1
- result |= (byte & 0x7f) << shift
+ result |= (byte & 0x7F) << shift
if not byte & 0x80:
- return DecodedArg(self, zigzag_decode(result), encoded[:count])
+ return DecodedArg(
+ self,
+ zigzag_decode(result),
+ encoded[:count],
+ DecodedArg.OK,
+ )
shift += 7
if shift >= 64:
break
- return DecodedArg(self, None, encoded[:count], DecodedArg.DECODE_ERROR,
- 'Unterminated variable-length integer')
+ return DecodedArg(
+ self,
+ None,
+ encoded[:count],
+ DecodedArg.DECODE_ERROR,
+ 'Unterminated variable-length integer',
+ )
def _decode_unsigned_integer(self, encoded: bytes) -> 'DecodedArg':
+ """Decodes an unsigned variable-length integer."""
arg = self._decode_signed_integer(encoded)
-
# Since ZigZag encoding is used, unsigned integers must be masked off to
# their original bit length.
if arg.value is not None:
@@ -148,9 +510,9 @@ class FormatSpec:
if len(encoded) < 4:
return DecodedArg.missing(self)
- return DecodedArg(self,
- self._PACKED_FLOAT.unpack_from(encoded)[0],
- encoded[:4])
+ return DecodedArg(
+ self, self._PACKED_FLOAT.unpack_from(encoded)[0], encoded[:4]
+ )
def _decode_string(self, encoded: bytes) -> 'DecodedArg':
"""Reads a unicode string from the encoded data."""
@@ -162,9 +524,9 @@ class FormatSpec:
if size_and_status & 0x80:
status |= DecodedArg.TRUNCATED
- size_and_status &= 0x7f
+ size_and_status &= 0x7F
- raw_data = encoded[0:size_and_status + 1]
+ raw_data = encoded[0 : size_and_status + 1]
data = raw_data[1:]
if len(data) < size_and_status:
@@ -173,9 +535,13 @@ class FormatSpec:
try:
decoded = data.decode()
except UnicodeDecodeError as err:
- return DecodedArg(self,
- repr(bytes(data)).lstrip('b'), raw_data,
- status | DecodedArg.DECODE_ERROR, err)
+ return DecodedArg(
+ self,
+ repr(bytes(data)).lstrip('b'),
+ raw_data,
+ status | DecodedArg.DECODE_ERROR,
+ err,
+ )
return DecodedArg(self, decoded, raw_data, status)
@@ -219,16 +585,19 @@ class DecodedArg:
def missing(cls, specifier: FormatSpec):
return cls(specifier, None, b'', cls.MISSING)
- def __init__(self,
- specifier: FormatSpec,
- value,
- raw_data: bytes,
- status: int = OK,
- error=None):
+ def __init__(
+ self,
+ specifier: FormatSpec,
+ value,
+ raw_data: bytes,
+ status: int = OK,
+ error=None,
+ ):
self.specifier = specifier # FormatSpec (e.g. to represent "%0.2f")
self.value = value # the decoded value, or None if decoding failed
self.raw_data = bytes(
- raw_data) # the exact bytes used to decode this arg
+ raw_data
+ ) # the exact bytes used to decode this arg
self._status = status
self.error = error
@@ -251,10 +620,43 @@ class DecodedArg:
return self.specifier.compatible % (self.value + '[...]')
if self.ok():
+ # Check if we are effectively .0{diuoxX} with a 0 value (this
+ # includes .* with (0, 0)). C standard says a value of 0 with 0
+ # precision produces an empty string.
+ is_integer_specifier_type = self.specifier.type in 'diuoxX'
+ is_simple_0_precision_with_0_value = self.value == 0 and (
+ self.specifier.precision == '.0'
+ or self.specifier.precision == '.'
+ )
+ is_star_0_precision_with_0_value = (
+ self.value == (0, 0) and self.specifier.precision == '.*'
+ )
+ if is_integer_specifier_type and (
+ is_simple_0_precision_with_0_value
+ or is_star_0_precision_with_0_value
+ ):
+ return ''
+
try:
+ # Python has a nonstandard alternative octal form.
+ if self.specifier.type == 'o' and '#' in self.specifier.flags:
+ return self._format_alternative_octal()
+
+ # Python doesn't pad zeros correctly for inf/nan.
+ if self.specifier.type in FormatSpec.FLOATING_POINT and (
+ self.value == math.inf
+ or self.value == -math.inf
+ or self.value == math.nan
+ ):
+ return self._format_text_float()
+
+ # Python doesn't have a native pointer formatter.
+ if self.specifier.type == 'p':
+ return self._format_pointer()
+
return self.specifier.compatible % self.value
except (OverflowError, TypeError, ValueError) as err:
- self.status |= self.DECODE_ERROR
+ self._status |= self.DECODE_ERROR
self.error = err
if self.status & self.SKIPPED:
@@ -264,14 +666,60 @@ class DecodedArg:
elif self.status & self.DECODE_ERROR:
message = '{} ERROR'.format(self.specifier)
else:
- raise AssertionError('Unhandled DecodedArg status {:x}!'.format(
- self.status))
+ raise AssertionError(
+ 'Unhandled DecodedArg status {:x}!'.format(self.status)
+ )
if self.value is None or not str(self.value):
return '<[{}]>'.format(message)
return '<[{} ({})]>'.format(message, self.value)
+ def _format_alternative_octal(self) -> str:
+ """Formats an alternative octal specifier.
+
+ This potentially throws OverflowError, TypeError, or ValueError.
+ """
+ compatible_specifier = self.specifier.compatible.replace('#', '')
+ result = compatible_specifier % self.value
+
+ # Find index of the first non-space, non-plus, and non-zero
+ # character. If we cannot find anything, we will simply
+ # prepend a 0 to the formatted string.
+ counter = 0
+ for i, value in enumerate(result):
+ if value not in ' +0':
+ counter = i
+ break
+ return result[:counter] + '0' + result[counter:]
+
+ def _format_text_float(self) -> str:
+ """Formats a float specifier with txt value (e.g. NAN, INF).
+
+ This potentially throws OverflowError, TypeError, or ValueError.
+ """
+ return self.specifier.text_float_safe_compatible() % self.value
+
+ def _format_pointer(self) -> str:
+ """Formats a pointer specifier.
+
+ This potentially throws OverflowError, TypeError, or ValueError.
+ """
+ result = self.specifier.compatible % self.value
+
+ # Find index of the first non-space, non-plus, and non-zero
+ # character (unless we hit the first of the 8 required hex
+ # digits).
+ counter = 0
+ for i, value in enumerate(result[:-7]):
+ if value not in ' +0' or i == len(result) - 8:
+ counter = i
+ break
+
+ # Insert the pointer 0x prefix in after the leading `+`,
+ # space, or `0`
+ return result[:counter] + '0x' + result[counter:]
+
def __str__(self) -> str:
return self.format()
@@ -293,7 +741,7 @@ class FormattedString(NamedTuple):
"""Arg data decoded successfully and all expected args were found."""
return all(arg.ok() for arg in self.args) and not self.remaining
- def score(self, date_removed: datetime = None) -> tuple:
+ def score(self, date_removed: Optional[datetime] = None) -> tuple:
"""Returns a key for sorting by how successful a decode was.
Decoded strings are sorted by whether they
@@ -314,11 +762,13 @@ class FormattedString(NamedTuple):
not self.remaining, # decoded all data
-sum(not arg.ok() for arg in self.args), # fewest errors
len(self.args), # decoded the most arguments
- date_removed or datetime.max) # most recently present
+ date_removed or datetime.max,
+ ) # most recently present
class FormatString:
"""Represents a printf-style format string."""
+
def __init__(self, format_string: str):
"""Parses format specifiers in the format string."""
self.format_string = format_string
@@ -335,13 +785,13 @@ class FormatString:
spec_spans = [spec.match.span() for spec in self.specifiers]
# Start with the part of the format string up to the first specifier.
- string_pieces = [self.format_string[:spec_spans[0][0]]]
+ string_pieces = [self.format_string[: spec_spans[0][0]]]
- for ((_, end1), (start2, _)) in zip(spec_spans[:-1], spec_spans[1:]):
+ for (_, end1), (start2, _) in zip(spec_spans[:-1], spec_spans[1:]):
string_pieces.append(self.format_string[end1:start2])
# Append the format string segment after the last format specifier.
- string_pieces.append(self.format_string[spec_spans[-1][1]:])
+ string_pieces.append(self.format_string[spec_spans[-1][1] :])
# Make a list with spots for the replacements between the string pieces.
segments: List = [None] * (len(string_pieces) + len(self.specifiers))
@@ -379,9 +829,9 @@ class FormatString:
return tuple(decoded_args), encoded[index:]
- def format(self,
- encoded_args: bytes,
- show_errors: bool = False) -> FormattedString:
+ def format(
+ self, encoded_args: bytes, show_errors: bool = False
+ ) -> FormattedString:
"""Decodes arguments and formats the string with them.
Args:
@@ -398,16 +848,17 @@ class FormatString:
if show_errors:
self._segments[1::2] = (arg.format() for arg in args)
else:
- self._segments[1::2] = (arg.format()
- if arg.ok() else arg.specifier.specifier
- for arg in args)
+ self._segments[1::2] = (
+ arg.format() if arg.ok() else arg.specifier.specifier
+ for arg in args
+ )
return FormattedString(''.join(self._segments), args, remaining)
-def decode(format_string: str,
- encoded_arguments: bytes,
- show_errors: bool = False) -> str:
+def decode(
+ format_string: str, encoded_arguments: bytes, show_errors: bool = False
+) -> str:
"""Decodes arguments and formats them with the provided format string.
Args:
@@ -420,5 +871,6 @@ def decode(format_string: str,
Returns:
the printf-style formatted string
"""
- return FormatString(format_string).format(encoded_arguments,
- show_errors).value
+ return (
+ FormatString(format_string).format(encoded_arguments, show_errors).value
+ )
diff --git a/pw_tokenizer/py/pw_tokenizer/detokenize.py b/pw_tokenizer/py/pw_tokenizer/detokenize.py
index 8f94fa04e..3aa7a3a8b 100755
--- a/pw_tokenizer/py/pw_tokenizer/detokenize.py
+++ b/pw_tokenizer/py/pw_tokenizer/detokenize.py
@@ -43,17 +43,28 @@ import string
import struct
import sys
import time
-from typing import (AnyStr, BinaryIO, Callable, Dict, List, Iterable, IO,
- Iterator, Match, NamedTuple, Optional, Pattern, Tuple,
- Union)
+from typing import (
+ AnyStr,
+ BinaryIO,
+ Callable,
+ Dict,
+ List,
+ Iterable,
+ Iterator,
+ Match,
+ NamedTuple,
+ Optional,
+ Pattern,
+ Tuple,
+ Union,
+)
try:
from pw_tokenizer import database, decode, encode, tokens
except ImportError:
# Append this path to the module search path to allow running this module
# without installing the pw_tokenizer package.
- sys.path.append(os.path.dirname(os.path.dirname(
- os.path.abspath(__file__))))
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from pw_tokenizer import database, decode, encode, tokens
_LOG = logging.getLogger('pw_tokenizer')
@@ -62,14 +73,19 @@ ENCODED_TOKEN = struct.Struct('<I')
BASE64_PREFIX = encode.BASE64_PREFIX.encode()
DEFAULT_RECURSION = 9
+_RawIO = Union[io.RawIOBase, BinaryIO]
+
class DetokenizedString:
"""A detokenized string, with all results if there are collisions."""
- def __init__(self,
- token: Optional[int],
- format_string_entries: Iterable[tuple],
- encoded_message: bytes,
- show_errors: bool = False):
+
+ def __init__(
+ self,
+ token: Optional[int],
+ format_string_entries: Iterable[tuple],
+ encoded_message: bytes,
+ show_errors: bool = False,
+ ):
self.token = token
self.encoded_message = encoded_message
self._show_errors = show_errors
@@ -80,8 +96,9 @@ class DetokenizedString:
decode_attempts: List[Tuple[Tuple, decode.FormattedString]] = []
for entry, fmt in format_string_entries:
- result = fmt.format(encoded_message[ENCODED_TOKEN.size:],
- show_errors)
+ result = fmt.format(
+ encoded_message[ENCODED_TOKEN.size :], show_errors
+ )
decode_attempts.append((result.score(entry.date_removed), result))
# Sort the attempts by the score so the most likely results are first.
@@ -132,8 +149,9 @@ class DetokenizedString:
return result[0]
if self._show_errors:
- return '<[ERROR: {}|{!r}]>'.format(self.error_message(),
- self.encoded_message)
+ return '<[ERROR: {}|{!r}]>'.format(
+ self.error_message(), self.encoded_message
+ )
# Display the string as prefixed Base64 if it cannot be decoded.
return encode.prefixed_base64(self.encoded_message)
@@ -142,8 +160,9 @@ class DetokenizedString:
if self.ok():
message = repr(str(self))
else:
- message = 'ERROR: {}|{!r}'.format(self.error_message(),
- self.encoded_message)
+ message = 'ERROR: {}|{!r}'.format(
+ self.error_message(), self.encoded_message
+ )
return '{}({})'.format(type(self).__name__, message)
@@ -155,6 +174,7 @@ class _TokenizedFormatString(NamedTuple):
class Detokenizer:
"""Main detokenization class; detokenizes strings and caches results."""
+
def __init__(self, *token_database_or_elf, show_errors: bool = False):
"""Decodes and detokenizes binary messages.
@@ -189,18 +209,29 @@ class Detokenizer:
def detokenize(self, encoded_message: bytes) -> DetokenizedString:
"""Decodes and detokenizes a message as a DetokenizedString."""
- if len(encoded_message) < ENCODED_TOKEN.size:
- return DetokenizedString(None, (), encoded_message,
- self.show_errors)
-
- token, = ENCODED_TOKEN.unpack_from(encoded_message)
- return DetokenizedString(token, self.lookup(token), encoded_message,
- self.show_errors)
-
- def detokenize_base64(self,
- data: AnyStr,
- prefix: Union[str, bytes] = BASE64_PREFIX,
- recursion: int = DEFAULT_RECURSION) -> AnyStr:
+ if not encoded_message:
+ return DetokenizedString(
+ None, (), encoded_message, self.show_errors
+ )
+
+ # Pad messages smaller than ENCODED_TOKEN.size with zeroes to support
+ # tokens smaller than a uint32. Messages with arguments must always use
+ # a full 32-bit token.
+ missing_token_bytes = ENCODED_TOKEN.size - len(encoded_message)
+ if missing_token_bytes > 0:
+ encoded_message += b'\0' * missing_token_bytes
+
+ (token,) = ENCODED_TOKEN.unpack_from(encoded_message)
+ return DetokenizedString(
+ token, self.lookup(token), encoded_message, self.show_errors
+ )
+
+ def detokenize_base64(
+ self,
+ data: AnyStr,
+ prefix: Union[str, bytes] = BASE64_PREFIX,
+ recursion: int = DEFAULT_RECURSION,
+ ) -> AnyStr:
"""Decodes and replaces prefixed Base64 messages in the provided data.
Args:
@@ -216,24 +247,30 @@ class Detokenizer:
result = output.getvalue()
return result.decode() if isinstance(data, str) else result
- def detokenize_base64_to_file(self,
- data: Union[str, bytes],
- output: BinaryIO,
- prefix: Union[str, bytes] = BASE64_PREFIX,
- recursion: int = DEFAULT_RECURSION) -> None:
+ def detokenize_base64_to_file(
+ self,
+ data: Union[str, bytes],
+ output: BinaryIO,
+ prefix: Union[str, bytes] = BASE64_PREFIX,
+ recursion: int = DEFAULT_RECURSION,
+ ) -> None:
"""Decodes prefixed Base64 messages in data; decodes to output file."""
data = data.encode() if isinstance(data, str) else data
prefix = prefix.encode() if isinstance(prefix, str) else prefix
output.write(
_base64_message_regex(prefix).sub(
- self._detokenize_prefixed_base64(prefix, recursion), data))
-
- def detokenize_base64_live(self,
- input_file: BinaryIO,
- output: BinaryIO,
- prefix: Union[str, bytes] = BASE64_PREFIX,
- recursion: int = DEFAULT_RECURSION) -> None:
+ self._detokenize_prefixed_base64(prefix, recursion), data
+ )
+ )
+
+ def detokenize_base64_live(
+ self,
+ input_file: _RawIO,
+ output: BinaryIO,
+ prefix: Union[str, bytes] = BASE64_PREFIX,
+ recursion: int = DEFAULT_RECURSION,
+ ) -> None:
"""Reads chars one-at-a-time, decoding messages; SLOW for big files."""
prefix_bytes = prefix.encode() if isinstance(prefix, str) else prefix
@@ -241,13 +278,12 @@ class Detokenizer:
def transform(data: bytes) -> bytes:
return base64_message.sub(
- self._detokenize_prefixed_base64(prefix_bytes, recursion),
- data)
+ self._detokenize_prefixed_base64(prefix_bytes, recursion), data
+ )
for message in PrefixedMessageDecoder(
- prefix,
- string.ascii_letters + string.digits + '+/-_=').transform(
- input_file, transform):
+ prefix, string.ascii_letters + string.digits + '+/-_='
+ ).transform(input_file, transform):
output.write(message)
# Flush each line to prevent delays when piping between processes.
@@ -255,22 +291,25 @@ class Detokenizer:
output.flush()
def _detokenize_prefixed_base64(
- self, prefix: bytes,
- recursion: int) -> Callable[[Match[bytes]], bytes]:
+ self, prefix: bytes, recursion: int
+ ) -> Callable[[Match[bytes]], bytes]:
"""Returns a function that decodes prefixed Base64."""
+
def decode_and_detokenize(match: Match[bytes]) -> bytes:
"""Decodes prefixed base64 with this detokenizer."""
original = match.group(0)
try:
detokenized_string = self.detokenize(
- base64.b64decode(original[1:], validate=True))
+ base64.b64decode(original[1:], validate=True)
+ )
if detokenized_string.matches():
result = str(detokenized_string).encode()
if recursion > 0 and original != result:
result = self.detokenize_base64(
- result, prefix, recursion - 1)
+ result, prefix, recursion - 1
+ )
return result
except binascii.Error:
@@ -281,15 +320,38 @@ class Detokenizer:
return decode_and_detokenize
-_PathOrFile = Union[IO, str, Path]
+_PathOrStr = Union[Path, str]
+
+
+# TODO(b/265334753): Reuse this function in database.py:LoadTokenDatabases
+def _parse_domain(path: _PathOrStr) -> Tuple[Path, Optional[Pattern[str]]]:
+ """Extracts an optional domain regex pattern suffix from a path"""
+
+ if isinstance(path, Path):
+ path = str(path)
+
+ delimiters = path.count('#')
+
+ if delimiters == 0:
+ return Path(path), None
+
+ if delimiters == 1:
+ path, domain = path.split('#')
+ return Path(path), re.compile(domain)
+
+ raise ValueError(
+ f'Too many # delimiters. Expected 0 or 1, found {delimiters}'
+ )
class AutoUpdatingDetokenizer(Detokenizer):
"""Loads and updates a detokenizer from database paths."""
+
class _DatabasePath:
"""Tracks the modified time of a path or file object."""
- def __init__(self, path: _PathOrFile) -> None:
- self.path = path if isinstance(path, (str, Path)) else path.name
+
+ def __init__(self, path: _PathOrStr) -> None:
+ self.path, self.domain = _parse_domain(path)
self._modified_time: Optional[float] = self._last_modified_time()
def updated(self) -> bool:
@@ -309,13 +371,17 @@ class AutoUpdatingDetokenizer(Detokenizer):
def load(self) -> tokens.Database:
try:
+ if self.domain is not None:
+ return database.load_token_database(
+ self.path, domain=self.domain
+ )
return database.load_token_database(self.path)
except FileNotFoundError:
return database.load_token_database()
- def __init__(self,
- *paths_or_files: _PathOrFile,
- min_poll_period_s: float = 1.0) -> None:
+ def __init__(
+ self, *paths_or_files: _PathOrStr, min_poll_period_s: float = 1.0
+ ) -> None:
self.paths = tuple(self._DatabasePath(path) for path in paths_or_files)
self.min_poll_period_s = min_poll_period_s
self._last_checked_time: float = time.time()
@@ -336,6 +402,7 @@ class AutoUpdatingDetokenizer(Detokenizer):
class PrefixedMessageDecoder:
"""Parses messages that start with a prefix character from a byte stream."""
+
def __init__(self, prefix: Union[str, bytes], chars: Union[str, bytes]):
"""Parses prefixed messages.
@@ -349,26 +416,28 @@ class PrefixedMessageDecoder:
chars = chars.encode()
# Store the valid message bytes as a set of binary strings.
- self._message_bytes = frozenset(chars[i:i + 1]
- for i in range(len(chars)))
+ self._message_bytes = frozenset(
+ chars[i : i + 1] for i in range(len(chars))
+ )
if len(self._prefix) != 1 or self._prefix in self._message_bytes:
raise ValueError(
'Invalid prefix {!r}: the prefix must be a single '
'character that is not a valid message character.'.format(
- prefix))
+ prefix
+ )
+ )
self.data = bytearray()
- def _read_next(self, fd: BinaryIO) -> Tuple[bytes, int]:
+ def _read_next(self, fd: _RawIO) -> Tuple[bytes, int]:
"""Returns the next character and its index."""
- char = fd.read(1)
+ char = fd.read(1) or b''
index = len(self.data)
self.data += char
return char, index
- def read_messages(self,
- binary_fd: BinaryIO) -> Iterator[Tuple[bool, bytes]]:
+ def read_messages(self, binary_fd: _RawIO) -> Iterator[Tuple[bool, bytes]]:
"""Parses prefixed messages; yields (is_message, contents) chunks."""
message_start = None
@@ -394,8 +463,9 @@ class PrefixedMessageDecoder:
else:
yield False, char
- def transform(self, binary_fd: BinaryIO,
- transform: Callable[[bytes], bytes]) -> Iterator[bytes]:
+ def transform(
+ self, binary_fd: _RawIO, transform: Callable[[bytes], bytes]
+ ) -> Iterator[bytes]:
"""Yields the file with a transformation applied to the messages."""
for is_message, chunk in self.read_messages(binary_fd):
yield transform(chunk) if is_message else chunk
@@ -405,27 +475,34 @@ def _base64_message_regex(prefix: bytes) -> Pattern[bytes]:
"""Returns a regular expression for prefixed base64 tokenized strings."""
return re.compile(
# Base64 tokenized strings start with the prefix character ($)
- re.escape(prefix) + (
+ re.escape(prefix)
+ + (
# Tokenized strings contain 0 or more blocks of four Base64 chars.
br'(?:[A-Za-z0-9+/\-_]{4})*'
# The last block of 4 chars may have one or two padding chars (=).
- br'(?:[A-Za-z0-9+/\-_]{3}=|[A-Za-z0-9+/\-_]{2}==)?'))
+ br'(?:[A-Za-z0-9+/\-_]{3}=|[A-Za-z0-9+/\-_]{2}==)?'
+ )
+ )
# TODO(hepler): Remove this unnecessary function.
-def detokenize_base64(detokenizer: Detokenizer,
- data: bytes,
- prefix: Union[str, bytes] = BASE64_PREFIX,
- recursion: int = DEFAULT_RECURSION) -> bytes:
+def detokenize_base64(
+ detokenizer: Detokenizer,
+ data: bytes,
+ prefix: Union[str, bytes] = BASE64_PREFIX,
+ recursion: int = DEFAULT_RECURSION,
+) -> bytes:
"""Alias for detokenizer.detokenize_base64 for backwards compatibility."""
return detokenizer.detokenize_base64(data, prefix, recursion)
-def _follow_and_detokenize_file(detokenizer: Detokenizer,
- file: BinaryIO,
- output: BinaryIO,
- prefix: Union[str, bytes],
- poll_period_s: float = 0.01) -> None:
+def _follow_and_detokenize_file(
+ detokenizer: Detokenizer,
+ file: BinaryIO,
+ output: BinaryIO,
+ prefix: Union[str, bytes],
+ poll_period_s: float = 0.01,
+) -> None:
"""Polls a file to detokenize it and any appended data."""
try:
@@ -440,8 +517,14 @@ def _follow_and_detokenize_file(detokenizer: Detokenizer,
pass
-def _handle_base64(databases, input_file: BinaryIO, output: BinaryIO,
- prefix: str, show_errors: bool, follow: bool) -> None:
+def _handle_base64(
+ databases,
+ input_file: BinaryIO,
+ output: BinaryIO,
+ prefix: str,
+ show_errors: bool,
+ follow: bool,
+) -> None:
"""Handles the base64 command line option."""
# argparse.FileType doesn't correctly handle - for binary files.
if input_file is sys.stdin:
@@ -450,15 +533,15 @@ def _handle_base64(databases, input_file: BinaryIO, output: BinaryIO,
if output is sys.stdout:
output = sys.stdout.buffer
- detokenizer = Detokenizer(tokens.Database.merged(*databases),
- show_errors=show_errors)
+ detokenizer = Detokenizer(
+ tokens.Database.merged(*databases), show_errors=show_errors
+ )
if follow:
_follow_and_detokenize_file(detokenizer, input_file, output, prefix)
elif input_file.seekable():
# Process seekable files all at once, which is MUCH faster.
- detokenizer.detokenize_base64_to_file(input_file.read(), output,
- prefix)
+ detokenizer.detokenize_base64_to_file(input_file.read(), output, prefix)
else:
# For non-seekable inputs (e.g. pipes), read one character at a time.
detokenizer.detokenize_base64_live(input_file, output, prefix)
@@ -469,7 +552,8 @@ def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
parser.set_defaults(handler=lambda **_: parser.print_help())
subparsers = parser.add_subparsers(help='Encoding of the input.')
@@ -479,7 +563,8 @@ def _parse_args() -> argparse.Namespace:
'base64',
description=base64_help,
parents=[database.token_databases_parser()],
- help=base64_help)
+ help=base64_help,
+ )
subparser.set_defaults(handler=_handle_base64)
subparser.add_argument(
'-i',
@@ -487,31 +572,45 @@ def _parse_args() -> argparse.Namespace:
dest='input_file',
type=argparse.FileType('rb'),
default=sys.stdin.buffer,
- help='The file from which to read; provide - or omit for stdin.')
+ help='The file from which to read; provide - or omit for stdin.',
+ )
subparser.add_argument(
'-f',
'--follow',
action='store_true',
- help=('Detokenize data appended to input_file as it grows; similar to '
- 'tail -f.'))
- subparser.add_argument('-o',
- '--output',
- type=argparse.FileType('wb'),
- default=sys.stdout.buffer,
- help=('The file to which to write the output; '
- 'provide - or omit for stdout.'))
+ help=(
+ 'Detokenize data appended to input_file as it grows; similar to '
+ 'tail -f.'
+ ),
+ )
+ subparser.add_argument(
+ '-o',
+ '--output',
+ type=argparse.FileType('wb'),
+ default=sys.stdout.buffer,
+ help=(
+ 'The file to which to write the output; '
+ 'provide - or omit for stdout.'
+ ),
+ )
subparser.add_argument(
'-p',
'--prefix',
default=BASE64_PREFIX,
- help=('The one-character prefix that signals the start of a '
- 'Base64-encoded message. (default: $)'))
+ help=(
+ 'The one-character prefix that signals the start of a '
+ 'Base64-encoded message. (default: $)'
+ ),
+ )
subparser.add_argument(
'-s',
'--show_errors',
action='store_true',
- help=('Show error messages instead of conversion specifiers when '
- 'arguments cannot be decoded.'))
+ help=(
+ 'Show error messages instead of conversion specifiers when '
+ 'arguments cannot be decoded.'
+ ),
+ )
return parser.parse_args()
diff --git a/pw_tokenizer/py/pw_tokenizer/elf_reader.py b/pw_tokenizer/py/pw_tokenizer/elf_reader.py
index a917c5b39..0c737e28b 100755
--- a/pw_tokenizer/py/pw_tokenizer/elf_reader.py
+++ b/pw_tokenizer/py/pw_tokenizer/elf_reader.py
@@ -24,12 +24,21 @@ archive are read as one unit.
"""
import argparse
+import collections
from pathlib import Path
import re
import struct
import sys
-from typing import BinaryIO, Dict, Iterable, NamedTuple, Optional
-from typing import Pattern, Tuple, Union
+from typing import (
+ BinaryIO,
+ Iterable,
+ Mapping,
+ NamedTuple,
+ Optional,
+ Pattern,
+ Tuple,
+ Union,
+)
ARCHIVE_MAGIC = b'!<arch>\n'
ELF_MAGIC = b'\x7fELF'
@@ -40,7 +49,8 @@ def _check_next_bytes(fd: BinaryIO, expected: bytes, what: str) -> None:
if expected != actual:
raise FileDecodeError(
f'Invalid {what}: expected {expected!r}, found {actual!r} in file '
- f'{getattr(fd, "name", "(unknown")}')
+ f'{getattr(fd, "name", "(unknown")}'
+ )
def files_in_archive(fd: BinaryIO) -> Iterable[int]:
@@ -74,7 +84,8 @@ def files_in_archive(fd: BinaryIO) -> Iterable[int]:
size = int(size_str, 10)
except ValueError as exc:
raise FileDecodeError(
- 'Archive file sizes must be decimal integers') from exc
+ 'Archive file sizes must be decimal integers'
+ ) from exc
_check_next_bytes(fd, b'`\n', 'archive file header ending')
offset = fd.tell() # Store offset in case the caller reads the file.
@@ -175,6 +186,7 @@ class FileDecodeError(Exception):
class FieldReader:
"""Reads ELF fields defined with a Field tuple from an ELF file."""
+
def __init__(self, elf: BinaryIO):
self._elf = elf
self.file_offset = self._elf.tell()
@@ -195,7 +207,7 @@ class FieldReader:
else:
raise FileDecodeError('Unknown size {!r}'.format(size_field))
- def _determine_integer_format(self) -> Dict[int, struct.Struct]:
+ def _determine_integer_format(self) -> Mapping[int, struct.Struct]:
"""Returns a dict of structs used for converting bytes to integers."""
endianness_byte = self._elf.read(1) # e_ident[EI_DATA] (endianness)
if endianness_byte == b'\x01':
@@ -204,7 +216,8 @@ class FieldReader:
endianness = '>'
else:
raise FileDecodeError(
- 'Unknown endianness {!r}'.format(endianness_byte))
+ 'Unknown endianness {!r}'.format(endianness_byte)
+ )
return {
1: struct.Struct(endianness + 'B'),
@@ -225,8 +238,10 @@ class FieldReader:
class Elf:
"""Represents an ELF file and the sections in it."""
+
class Section(NamedTuple):
"""Info about a section in an ELF file."""
+
name: str
address: int
offset: int
@@ -250,26 +265,32 @@ class Elf:
reader = FieldReader(self._elf)
base = reader.read(FILE_HEADER.section_header_offset)
section_header_size = reader.offset(
- SECTION_HEADER.section_header_end)
+ SECTION_HEADER.section_header_end
+ )
# Find the section with the section names in it.
names_section_header_base = (
- base + section_header_size *
- reader.read(FILE_HEADER.section_names_index))
- names_table_base = reader.read(SECTION_HEADER.section_offset,
- names_section_header_base)
+ base
+ + section_header_size
+ * reader.read(FILE_HEADER.section_names_index)
+ )
+ names_table_base = reader.read(
+ SECTION_HEADER.section_offset, names_section_header_base
+ )
base = reader.read(FILE_HEADER.section_header_offset)
for _ in range(reader.read(FILE_HEADER.section_count)):
- name_offset = reader.read(SECTION_HEADER.section_name_offset,
- base)
+ name_offset = reader.read(
+ SECTION_HEADER.section_name_offset, base
+ )
yield self.Section(
reader.read_string(names_table_base + name_offset),
reader.read(SECTION_HEADER.section_address, base),
reader.read(SECTION_HEADER.section_offset, base),
reader.read(SECTION_HEADER.section_size, base),
- reader.file_offset)
+ reader.file_offset,
+ )
base += section_header_size
@@ -287,49 +308,66 @@ class Elf:
if section.name == name:
yield section
- def read_value(self,
- address: int,
- size: Optional[int] = None) -> Union[None, bytes, int]:
+ def read_value(
+ self, address: int, size: Optional[int] = None
+ ) -> Union[None, bytes, int]:
"""Reads specified bytes or null-terminated string at address."""
section = self.section_by_address(address)
if not section:
return None
assert section.address <= address
- self._elf.seek(section.file_offset + section.offset + address -
- section.address)
+ self._elf.seek(
+ section.file_offset + section.offset + address - section.address
+ )
if size is None:
return read_c_string(self._elf)
return self._elf.read(size)
- def dump_sections(self, name: Union[str,
- Pattern[str]]) -> Dict[str, bytes]:
- """Dumps a binary string containing the sections matching the regex."""
+ def dump_sections(
+ self, name: Union[str, Pattern[str]]
+ ) -> Mapping[str, bytes]:
+ """Returns a mapping of section names to section contents.
+
+ If processing an archive with multiple object files, the contents of
+ sections with duplicate names are concatenated in the order they appear
+ in the archive.
+ """
name_regex = re.compile(name)
- sections: Dict[str, bytes] = {}
+ sections: Mapping[str, bytearray] = collections.defaultdict(bytearray)
for section in self.sections:
if name_regex.match(section.name):
self._elf.seek(section.file_offset + section.offset)
- sections[section.name] = self._elf.read(section.size)
+ sections[section.name].extend(self._elf.read(section.size))
return sections
def dump_section_contents(
- self, name: Union[str, Pattern[str]]) -> Optional[bytes]:
+ self, name: Union[str, Pattern[str]]
+ ) -> Optional[bytes]:
+ """Dumps a binary string containing the sections matching the regex.
+
+ If processing an archive with multiple object files, the contents of
+ sections with duplicate names are concatenated in the order they appear
+ in the archive.
+ """
sections = self.dump_sections(name)
return b''.join(sections.values()) if sections else None
def summary(self) -> str:
return '\n'.join(
- '[{0:2}] {1.address:08x} {1.offset:08x} {1.size:08x} {1.name}'.
- format(i, section) for i, section in enumerate(self.sections))
+ '[{0:2}] {1.address:08x} {1.offset:08x} {1.size:08x} '
+ '{1.name}'.format(i, section)
+ for i, section in enumerate(self.sections)
+ )
def __str__(self) -> str:
- return 'Elf({}\n)'.format(''.join('\n {},'.format(s)
- for s in self.sections))
+ return 'Elf({}\n)'.format(
+ ''.join('\n {},'.format(s) for s in self.sections)
+ )
def _read_addresses(elf, size: int, output, address: Iterable[int]) -> None:
@@ -358,23 +396,27 @@ def _parse_args() -> argparse.Namespace:
def hex_int(arg):
return int(arg, 16)
- parser.add_argument('-e',
- '--elf',
- type=argparse.FileType('rb'),
- help='the ELF file to examine',
- required=True)
+ parser.add_argument(
+ '-e',
+ '--elf',
+ type=argparse.FileType('rb'),
+ help='the ELF file to examine',
+ required=True,
+ )
parser.add_argument(
'-d',
'--delimiter',
default=ord('\n'),
type=int,
- help=r'delimiter to write after each value; \n by default')
+ help=r'delimiter to write after each value; \n by default',
+ )
parser.set_defaults(handler=lambda **_: parser.print_help())
subparsers = parser.add_subparsers(
- help='select whether to work with addresses or whole sections')
+ help='select whether to work with addresses or whole sections'
+ )
section_parser = subparsers.add_parser('section')
section_parser.set_defaults(handler=_dump_sections)
@@ -383,18 +425,19 @@ def _parse_args() -> argparse.Namespace:
metavar='section_regex',
nargs='*',
type=re.compile, # type: ignore
- help='section name regular expression')
+ help='section name regular expression',
+ )
address_parser = subparsers.add_parser('address')
address_parser.set_defaults(handler=_read_addresses)
address_parser.add_argument(
'--size',
type=int,
- help='the size to read; reads until a null terminator by default')
- address_parser.add_argument('address',
- nargs='+',
- type=hex_int,
- help='hexadecimal addresses to read')
+ help='the size to read; reads until a null terminator by default',
+ )
+ address_parser.add_argument(
+ 'address', nargs='+', type=hex_int, help='hexadecimal addresses to read'
+ )
return parser.parse_args()
diff --git a/pw_tokenizer/py/pw_tokenizer/encode.py b/pw_tokenizer/py/pw_tokenizer/encode.py
index 97c62bf70..5b8583256 100644
--- a/pw_tokenizer/py/pw_tokenizer/encode.py
+++ b/pw_tokenizer/py/pw_tokenizer/encode.py
@@ -13,9 +13,13 @@
# the License.
"""Provides functionality for encoding tokenized messages."""
+import argparse
import base64
import struct
-from typing import Union
+import sys
+from typing import Sequence, Union
+
+from pw_tokenizer import tokens
_INT32_MAX = 2**31 - 1
_UINT32_MAX = 2**32 - 1
@@ -32,13 +36,13 @@ def _little_endian_base128_encode(integer: int) -> bytearray:
while True:
# Grab 7 bits; the eighth bit is set to 1 to indicate more data coming.
- data.append((integer & 0x7f) | 0x80)
+ data.append((integer & 0x7F) | 0x80)
integer >>= 7
if not integer:
break
- data[-1] &= 0x7f # clear the top bit of the last byte
+ data[-1] &= 0x7F # clear the top bit of the last byte
return data
@@ -51,25 +55,14 @@ def _encode_int32(arg: int) -> bytearray:
def _encode_string(arg: bytes) -> bytes:
- size_byte = len(arg) if len(arg) < 128 else 0xff
+ size_byte = len(arg) if len(arg) < 128 else 0xFF
return struct.pack('B', size_byte) + arg[:127]
-def encode_token_and_args(token: int, *args: Union[int, float, bytes,
- str]) -> bytes:
- """Encodes a tokenized message given its token and arguments.
-
- This function assumes that the token represents a format string with
- conversion specifiers that correspond with the provided argument types.
- Currently, only 32-bit integers are supported.
- """
-
- if token < 0 or token > _UINT32_MAX:
- raise ValueError(
- f'The token ({token}) must be an unsigned 32-bit integer')
-
- data = bytearray(struct.pack('<I', token))
+def encode_args(*args: Union[int, float, bytes, str]) -> bytes:
+ """Encodes a list of arguments to their on-wire representation."""
+ data = bytearray(b'')
for arg in args:
if isinstance(arg, int):
if arg.bit_length() > 32:
@@ -85,11 +78,86 @@ def encode_token_and_args(token: int, *args: Union[int, float, bytes,
data += _encode_string(arg)
else:
raise ValueError(
- f'{arg} has type {type(arg)}, which is not supported')
-
+ f'{arg} has type {type(arg)}, which is not supported'
+ )
return bytes(data)
+def encode_token_and_args(
+ token: int, *args: Union[int, float, bytes, str]
+) -> bytes:
+ """Encodes a tokenized message given its token and arguments.
+
+ This function assumes that the token represents a format string with
+ conversion specifiers that correspond with the provided argument types.
+ Currently, only 32-bit integers are supported.
+ """
+
+ if token < 0 or token > _UINT32_MAX:
+ raise ValueError(
+ f'The token ({token}) must be an unsigned 32-bit integer'
+ )
+
+ return struct.pack('<I', token) + encode_args(*args)
+
+
def prefixed_base64(data: bytes, prefix: str = '$') -> str:
"""Encodes a tokenized message as prefixed Base64."""
return prefix + base64.b64encode(data).decode()
+
+
+def _parse_user_input(string: str):
+ """Evaluates a string as Python code or returns it as a literal string."""
+ try:
+ value = eval(string, dict(__builtins__={})) # pylint: disable=eval-used
+ except (NameError, SyntaxError):
+ return string
+
+ return value if isinstance(value, (int, float)) else string
+
+
+def _main(format_string_list: Sequence[str], raw_args: Sequence[str]) -> int:
+ (format_string,) = format_string_list
+ token = tokens.pw_tokenizer_65599_hash(format_string)
+ args = tuple(_parse_user_input(a) for a in raw_args)
+
+ data = encode_token_and_args(token, *args)
+ token = int.from_bytes(data[:4], 'little')
+ binary = ' '.join(f'{b:02x}' for b in data)
+
+ print(f' Raw input: {format_string!r} % {args!r}')
+ print(f'Formatted input: {format_string % args}')
+ print(f' Token: 0x{token:08x}')
+ print(f' Encoded: {data!r} ({binary}) [{len(data)} bytes]')
+ print(f'Prefixed Base64: {prefixed_base64(data)}')
+
+ return 0
+
+
+def _parse_args() -> dict:
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ 'format_string_list',
+ metavar='FORMAT_STRING',
+ nargs=1,
+ help='Format string with optional %%-style arguments.',
+ )
+ parser.add_argument(
+ 'raw_args',
+ metavar='ARG',
+ nargs='*',
+ help=(
+ 'Arguments for the format string, if any. Arguments are parsed '
+ 'as Python expressions, with no builtins (e.g. 9 is the number '
+ '9 and \'"9"\' is the string "9"). Arguments that are not valid '
+ 'Python are treated as string literals.'
+ ),
+ )
+ return vars(parser.parse_args())
+
+
+if __name__ == '__main__':
+ sys.exit(_main(**_parse_args()))
diff --git a/pw_tokenizer/py/pw_tokenizer/parse_message.py b/pw_tokenizer/py/pw_tokenizer/parse_message.py
index f8655e1f3..7f33e1ffa 100644
--- a/pw_tokenizer/py/pw_tokenizer/parse_message.py
+++ b/pw_tokenizer/py/pw_tokenizer/parse_message.py
@@ -40,11 +40,12 @@ PREFIX = '$'
def attempt_to_decode(
- arg_data: bytes,
- format_specs: Collection[str] = DEFAULT_FORMAT_SPECS,
- max_args: int = DEFAULT_MAX_ARGS,
- yield_failures: bool = False) -> Iterator[FormattedString]:
- """Attemps to decode arguments using the provided format specifiers."""
+ arg_data: bytes,
+ format_specs: Collection[str] = DEFAULT_FORMAT_SPECS,
+ max_args: int = DEFAULT_MAX_ARGS,
+ yield_failures: bool = False,
+) -> Iterator[FormattedString]:
+ """Attempts to decode arguments using the provided format specifiers."""
format_strings = [(0, '')] # (argument count, format string)
# Each argument requires at least 1 byte.
@@ -59,7 +60,8 @@ def attempt_to_decode(
if arg_count < max_args:
format_strings.extend(
- (arg_count + 1, string + spec) for spec in format_specs)
+ (arg_count + 1, string + spec) for spec in format_specs
+ )
@dataclass(frozen=True)
@@ -79,13 +81,16 @@ class TokenizedMessage:
def parse(cls, message: str, prefix: str = '$') -> 'TokenizedMessage':
if not message.startswith(prefix):
raise ValueError(
- f'{message} does not start wtih {prefix!r} as expected')
+ f'{message} does not start with {prefix!r} as expected'
+ )
binary = base64.b64decode(message[1:])
if len(binary) < 4:
- raise ValueError(f'{message} is only {len(binary)} bytes; '
- 'tokenized messages must be at least 4 bytes')
+ raise ValueError(
+ f'{message} is only {len(binary)} bytes; '
+ 'tokenized messages must be at least 4 bytes'
+ )
return cls(message, binary)
@@ -105,8 +110,12 @@ def _text_list(items: Sequence, conjunction: str = 'or') -> str:
return f'{", ".join(str(i) for i in items[:-1])} {conjunction} {items[-1]}'
-def main(messages: Iterable[str], max_args: int, specs: Sequence[str],
- show_failures: bool) -> int:
+def main(
+ messages: Iterable[str],
+ max_args: int,
+ specs: Sequence[str],
+ show_failures: bool,
+) -> int:
"""Parses the arguments for a series of tokenized messages."""
exit_code = 0
@@ -125,30 +134,47 @@ def main(messages: Iterable[str], max_args: int, specs: Sequence[str],
exit_code = 2
continue
- _LOG.info('Binary: %r [%s] (%d bytes)', parsed.binary,
- parsed.binary.hex(' ', 1), len(parsed.binary))
+ _LOG.info(
+ 'Binary: %r [%s] (%d bytes)',
+ parsed.binary,
+ parsed.binary.hex(' ', 1),
+ len(parsed.binary),
+ )
_LOG.info('Token: 0x%08x', parsed.token)
- _LOG.info('Args: %r [%s] (%d bytes)', parsed.binary_args,
- parsed.binary_args.hex(' ', 1), len(parsed.binary_args))
- _LOG.info('Decoding with up to %d %s arguments', max_args,
- _text_list(specs))
-
- results = sorted(attempt_to_decode(parsed.binary_args, specs, max_args,
- show_failures),
- key=FormattedString.score,
- reverse=True)
+ _LOG.info(
+ 'Args: %r [%s] (%d bytes)',
+ parsed.binary_args,
+ parsed.binary_args.hex(' ', 1),
+ len(parsed.binary_args),
+ )
+ _LOG.info(
+ 'Decoding with up to %d %s arguments', max_args, _text_list(specs)
+ )
+
+ results = sorted(
+ attempt_to_decode(
+ parsed.binary_args, specs, max_args, show_failures
+ ),
+ key=FormattedString.score,
+ reverse=True,
+ )
if not any(result.ok() for result in results):
_LOG.warning(
' No combinations of up to %d %s arguments decoded '
- 'successfully', max_args, _text_list(specs))
+ 'successfully',
+ max_args,
+ _text_list(specs),
+ )
exit_code = 1
for i, result in enumerate(results, 1):
_LOG.info( # pylint: disable=logging-fstring-interpolation
- f' Attempt %{len(str(len(results)))}d: [%s] %s', i,
+ f' Attempt %{len(str(len(results)))}d: [%s] %s',
+ i,
' '.join(str(a.specifier) for a in result.args),
- ' '.join(str(a) for a in result.args))
+ ' '.join(str(a) for a in result.args),
+ )
print()
return exit_code
@@ -157,23 +183,33 @@ def main(messages: Iterable[str], max_args: int, specs: Sequence[str],
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=__doc__,
- formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- parser.add_argument('--max-args',
- default=DEFAULT_MAX_ARGS,
- type=int,
- help='Maximum number of printf-style arguments')
- parser.add_argument('--specs',
- nargs='*',
- default=DEFAULT_FORMAT_SPECS,
- help='Which printf-style format specifiers to check')
- parser.add_argument('--show-failures',
- action='store_true',
- help='Show argument combintations that fail to decode')
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
+ )
+ parser.add_argument(
+ '--max-args',
+ default=DEFAULT_MAX_ARGS,
+ type=int,
+ help='Maximum number of printf-style arguments',
+ )
+ parser.add_argument(
+ '--specs',
+ nargs='*',
+ default=DEFAULT_FORMAT_SPECS,
+ help='Which printf-style format specifiers to check',
+ )
+ parser.add_argument(
+ '--show-failures',
+ action='store_true',
+ help='Show argument combintations that fail to decode',
+ )
parser.add_argument(
'messages',
nargs='*',
- help=
- 'Base64-encoded tokenized messages to decode; omit to read from stdin')
+ help=(
+ 'Base64-encoded tokenized messages to decode; omit to read from '
+ 'stdin'
+ ),
+ )
return parser.parse_args()
diff --git a/pw_tokenizer/py/pw_tokenizer/proto/__init__.py b/pw_tokenizer/py/pw_tokenizer/proto/__init__.py
index 2dd7237e9..da11d5e5e 100644
--- a/pw_tokenizer/py/pw_tokenizer/proto/__init__.py
+++ b/pw_tokenizer/py/pw_tokenizer/proto/__init__.py
@@ -25,14 +25,19 @@ from pw_tokenizer import detokenize, encode
def _tokenized_fields(proto: Message) -> Iterator[FieldDescriptor]:
for field in proto.DESCRIPTOR.fields:
extensions = field.GetOptions().Extensions
- if options_pb2.format in extensions and extensions[
- options_pb2.format] == options_pb2.TOKENIZATION_OPTIONAL:
+ if (
+ options_pb2.format in extensions
+ and extensions[options_pb2.format]
+ == options_pb2.TOKENIZATION_OPTIONAL
+ ):
yield field
-def decode_optionally_tokenized(detokenizer: detokenize.Detokenizer,
- data: bytes,
- prefix: str = encode.BASE64_PREFIX) -> str:
+def decode_optionally_tokenized(
+ detokenizer: detokenize.Detokenizer,
+ data: bytes,
+ prefix: str = encode.BASE64_PREFIX,
+) -> str:
"""Decodes data that may be plain text or binary / Base64 tokenized text."""
# Try detokenizing as binary.
result = detokenizer.detokenize(data)
@@ -62,9 +67,11 @@ def decode_optionally_tokenized(detokenizer: detokenize.Detokenizer,
return encode.prefixed_base64(data, prefix)
-def detokenize_fields(detokenizer: detokenize.Detokenizer,
- proto: Message,
- prefix: str = encode.BASE64_PREFIX) -> None:
+def detokenize_fields(
+ detokenizer: detokenize.Detokenizer,
+ proto: Message,
+ prefix: str = encode.BASE64_PREFIX,
+) -> None:
"""Detokenizes fields annotated as tokenized in the given proto.
The fields are replaced with their detokenized version in the proto.
@@ -72,7 +79,7 @@ def detokenize_fields(detokenizer: detokenize.Detokenizer,
bytes. Call .decode() to convert the detokenized string from bytes to str.
"""
for field in _tokenized_fields(proto):
- decoded = decode_optionally_tokenized(detokenizer,
- getattr(proto, field.name),
- prefix)
+ decoded = decode_optionally_tokenized(
+ detokenizer, getattr(proto, field.name), prefix
+ )
setattr(proto, field.name, decoded.encode())
diff --git a/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py b/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
index 234ca64de..ab673a55e 100644
--- a/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
+++ b/pw_tokenizer/py/pw_tokenizer/serial_detokenizer.py
@@ -21,7 +21,7 @@ import argparse
import sys
from typing import BinaryIO, Iterable
-import serial # type: ignore
+import serial
from pw_tokenizer import database, detokenize, tokens
@@ -31,46 +31,67 @@ def _parse_args():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
- parents=[database.token_databases_parser()])
- parser.add_argument('-d',
- '--device',
- required=True,
- help='The serial device from which to read')
- parser.add_argument('-b',
- '--baudrate',
- type=int,
- default=115200,
- help='The baud rate for the serial device')
- parser.add_argument('-o',
- '--output',
- type=argparse.FileType('wb'),
- default=sys.stdout.buffer,
- help=('The file to which to write the output; '
- 'provide - or omit for stdout.'))
+ parents=[database.token_databases_parser()],
+ )
+ parser.add_argument(
+ '-d',
+ '--device',
+ required=True,
+ help='The serial device from which to read',
+ )
+ parser.add_argument(
+ '-b',
+ '--baudrate',
+ type=int,
+ default=115200,
+ help='The baud rate for the serial device',
+ )
+ parser.add_argument(
+ '-o',
+ '--output',
+ type=argparse.FileType('wb'),
+ default=sys.stdout.buffer,
+ help=(
+ 'The file to which to write the output; '
+ 'provide - or omit for stdout.'
+ ),
+ )
parser.add_argument(
'-p',
'--prefix',
default=detokenize.BASE64_PREFIX,
- help=('The one-character prefix that signals the start of a '
- 'Base64-encoded message. (default: $)'))
+ help=(
+ 'The one-character prefix that signals the start of a '
+ 'Base64-encoded message. (default: $)'
+ ),
+ )
parser.add_argument(
'-s',
'--show_errors',
action='store_true',
- help=('Show error messages instead of conversion specifiers when '
- 'arguments cannot be decoded.'))
+ help=(
+ 'Show error messages instead of conversion specifiers when '
+ 'arguments cannot be decoded.'
+ ),
+ )
return parser.parse_args()
-def _detokenize_serial(databases: Iterable, device: serial.Serial,
- baudrate: int, show_errors: bool, output: BinaryIO,
- prefix: str) -> None:
+def _detokenize_serial(
+ databases: Iterable,
+ device: str,
+ baudrate: int,
+ show_errors: bool,
+ output: BinaryIO,
+ prefix: str,
+) -> None:
if output is sys.stdout:
output = sys.stdout.buffer
- detokenizer = detokenize.Detokenizer(tokens.Database.merged(*databases),
- show_errors=show_errors)
+ detokenizer = detokenize.Detokenizer(
+ tokens.Database.merged(*databases), show_errors=show_errors
+ )
serial_device = serial.Serial(port=device, baudrate=baudrate)
try:
diff --git a/pw_tokenizer/py/pw_tokenizer/tokens.py b/pw_tokenizer/py/pw_tokenizer/tokens.py
index f01c00a71..b7ebac87c 100644
--- a/pw_tokenizer/py/pw_tokenizer/tokens.py
+++ b/pw_tokenizer/py/pw_tokenizer/tokens.py
@@ -13,6 +13,7 @@
# the License.
"""Builds and manages databases of tokenized strings."""
+from abc import abstractmethod
import collections
import csv
from dataclasses import dataclass
@@ -22,15 +23,29 @@ import logging
from pathlib import Path
import re
import struct
-from typing import (BinaryIO, Callable, Dict, Iterable, Iterator, List,
- NamedTuple, Optional, Pattern, Tuple, Union, ValuesView)
+import subprocess
+from typing import (
+ BinaryIO,
+ Callable,
+ Dict,
+ Iterable,
+ Iterator,
+ List,
+ NamedTuple,
+ Optional,
+ Pattern,
+ TextIO,
+ Tuple,
+ Union,
+ ValuesView,
+)
+from uuid import uuid4
-DATE_FORMAT = '%Y-%m-%d'
DEFAULT_DOMAIN = ''
-# The default hash length to use. This value only applies when hashing strings
-# from a legacy-style ELF with plain strings. New tokenized string entries
-# include the token alongside the string.
+# The default hash length to use for C-style hashes. This value only applies
+# when manually hashing strings to recreate token calculations in C. The C++
+# hash function does not have a maximum length.
#
# This MUST match the default value of PW_TOKENIZER_CFG_C_HASH_LENGTH in
# pw_tokenizer/public/pw_tokenizer/config.h.
@@ -45,12 +60,14 @@ def _value(char: Union[int, str]) -> int:
return char if isinstance(char, int) else ord(char)
-def pw_tokenizer_65599_hash(string: Union[str, bytes],
- hash_length: int = None) -> int:
- """Hashes the provided string.
+def pw_tokenizer_65599_hash(
+ string: Union[str, bytes], *, hash_length: Optional[int] = None
+) -> int:
+ """Hashes the string with the hash function used to generate tokens in C++.
- This hash function is only used when adding tokens from legacy-style
- tokenized strings in an ELF, which do not include the token.
+ This hash function is used calculate tokens from strings in Python. It is
+ not used when extracting tokens from an ELF, since the token is stored in
+ the ELF as part of tokenization.
"""
hash_value = len(string)
coefficient = TOKENIZER_HASH_CONSTANT
@@ -62,12 +79,16 @@ def pw_tokenizer_65599_hash(string: Union[str, bytes],
return hash_value
-def default_hash(string: Union[str, bytes]) -> int:
- return pw_tokenizer_65599_hash(string, DEFAULT_C_HASH_LENGTH)
+def c_hash(
+ string: Union[str, bytes], hash_length: int = DEFAULT_C_HASH_LENGTH
+) -> int:
+ """Hashes the string with the hash function used in C."""
+ return pw_tokenizer_65599_hash(string, hash_length=hash_length)
class _EntryKey(NamedTuple):
"""Uniquely refers to an entry."""
+
token: int
string: str
@@ -75,6 +96,7 @@ class _EntryKey(NamedTuple):
@dataclass(eq=True, order=False)
class TokenizedStringEntry:
"""A tokenized string with its metadata."""
+
token: int
string: str
domain: str = DEFAULT_DOMAIN
@@ -84,8 +106,7 @@ class TokenizedStringEntry:
"""The key determines uniqueness for a tokenized string."""
return _EntryKey(self.token, self.string)
- def update_date_removed(self,
- new_date_removed: Optional[datetime]) -> None:
+ def update_date_removed(self, new_date_removed: Optional[datetime]) -> None:
"""Sets self.date_removed if the other date is newer."""
# No removal date (None) is treated as the newest date.
if self.date_removed is None:
@@ -102,8 +123,9 @@ class TokenizedStringEntry:
# Sort removal dates in reverse, so the most recently removed (or still
# present) entry appears first.
if self.date_removed != other.date_removed:
- return (other.date_removed or datetime.max) < (self.date_removed
- or datetime.max)
+ return (other.date_removed or datetime.max) < (
+ self.date_removed or datetime.max
+ )
return self.string < other.string
@@ -113,26 +135,31 @@ class TokenizedStringEntry:
class Database:
"""Database of tokenized strings stored as TokenizedStringEntry objects."""
+
def __init__(self, entries: Iterable[TokenizedStringEntry] = ()):
"""Creates a token database."""
# The database dict stores each unique (token, string) entry.
- self._database: Dict[_EntryKey, TokenizedStringEntry] = {
- entry.key(): entry
- for entry in entries
- }
+ self._database: Dict[_EntryKey, TokenizedStringEntry] = {}
# This is a cache for fast token lookup that is built as needed.
self._cache: Optional[Dict[int, List[TokenizedStringEntry]]] = None
+ self.add(entries)
+
@classmethod
def from_strings(
- cls,
- strings: Iterable[str],
- domain: str = DEFAULT_DOMAIN,
- tokenize: Callable[[str], int] = default_hash) -> 'Database':
+ cls,
+ strings: Iterable[str],
+ domain: str = DEFAULT_DOMAIN,
+ tokenize: Callable[[str], int] = pw_tokenizer_65599_hash,
+ ) -> 'Database':
"""Creates a Database from an iterable of strings."""
- return cls((TokenizedStringEntry(tokenize(string), string, domain)
- for string in strings))
+ return cls(
+ (
+ TokenizedStringEntry(tokenize(string), string, domain)
+ for string in strings
+ )
+ )
@classmethod
def merged(cls, *databases: 'Database') -> 'Database':
@@ -162,9 +189,9 @@ class Database:
yield token, entries
def mark_removed(
- self,
- all_entries: Iterable[TokenizedStringEntry],
- removal_date: Optional[datetime] = None
+ self,
+ all_entries: Iterable[TokenizedStringEntry],
+ removal_date: Optional[datetime] = None,
) -> List[TokenizedStringEntry]:
"""Marks entries missing from all_entries as having been removed.
@@ -191,9 +218,9 @@ class Database:
removed = []
for entry in self._database.values():
- if (entry.key() not in all_keys
- and (entry.date_removed is None
- or removal_date < entry.date_removed)):
+ if entry.key() not in all_keys and (
+ entry.date_removed is None or removal_date < entry.date_removed
+ ):
# Add a removal date, or update it to the oldest date.
entry.date_removed = removal_date
removed.append(entry)
@@ -201,7 +228,10 @@ class Database:
return removed
def add(self, entries: Iterable[TokenizedStringEntry]) -> None:
- """Adds new entries and updates date_removed for existing entries."""
+ """Adds new entries and updates date_removed for existing entries.
+
+ If the added tokens have removal dates, the newest date is used.
+ """
self._cache = None
for new_entry in entries:
@@ -209,14 +239,23 @@ class Database:
try:
entry = self._database[new_entry.key()]
entry.domain = new_entry.domain
- entry.date_removed = None
+
+ # Keep the latest removal date between the two entries.
+ if new_entry.date_removed is None:
+ entry.date_removed = None
+ elif (
+ entry.date_removed
+ and entry.date_removed < new_entry.date_removed
+ ):
+ entry.date_removed = new_entry.date_removed
except KeyError:
+ # Make a copy to avoid unintentially updating the database.
self._database[new_entry.key()] = TokenizedStringEntry(
- new_entry.token, new_entry.string, new_entry.domain)
+ **vars(new_entry)
+ )
def purge(
- self,
- date_removed_cutoff: Optional[datetime] = None
+ self, date_removed_cutoff: Optional[datetime] = None
) -> List[TokenizedStringEntry]:
"""Removes and returns entries removed on/before date_removed_cutoff."""
self._cache = None
@@ -225,7 +264,8 @@ class Database:
date_removed_cutoff = datetime.max
to_delete = [
- entry for _, entry in self._database.items()
+ entry
+ for _, entry in self._database.items()
if entry.date_removed and entry.date_removed <= date_removed_cutoff
]
@@ -251,7 +291,7 @@ class Database:
self,
include: Iterable[Union[str, Pattern[str]]] = (),
exclude: Iterable[Union[str, Pattern[str]]] = (),
- replace: Iterable[Tuple[Union[str, Pattern[str]], str]] = ()
+ replace: Iterable[Tuple[Union[str, Pattern[str]], str]] = (),
) -> None:
"""Filters the database using regular expressions (strings or compiled).
@@ -267,13 +307,18 @@ class Database:
if include:
include_re = [re.compile(pattern) for pattern in include]
to_delete.extend(
- key for key, val in self._database.items()
- if not any(rgx.search(val.string) for rgx in include_re))
+ key
+ for key, val in self._database.items()
+ if not any(rgx.search(val.string) for rgx in include_re)
+ )
if exclude:
exclude_re = [re.compile(pattern) for pattern in exclude]
- to_delete.extend(key for key, val in self._database.items() if any(
- rgx.search(val.string) for rgx in exclude_re))
+ to_delete.extend(
+ key
+ for key, val in self._database.items()
+ if any(rgx.search(val.string) for rgx in exclude_re)
+ )
for key in to_delete:
del self._database[key]
@@ -284,10 +329,22 @@ class Database:
for value in self._database.values():
value.string = search.sub(replacement, value.string)
+ def difference(self, other: 'Database') -> 'Database':
+ """Returns a new Database with entries in this DB not in the other."""
+ # pylint: disable=protected-access
+ return Database(
+ e for k, e in self._database.items() if k not in other._database
+ )
+ # pylint: enable=protected-access
+
def __len__(self) -> int:
"""Returns the number of entries in the database."""
return len(self.entries())
+ def __bool__(self) -> bool:
+ """True if the database is non-empty."""
+ return bool(self._database)
+
def __str__(self) -> str:
"""Outputs the database as CSV."""
csv_output = io.BytesIO()
@@ -295,32 +352,47 @@ class Database:
return csv_output.getvalue().decode()
-def parse_csv(fd) -> Iterable[TokenizedStringEntry]:
+def parse_csv(fd: TextIO) -> Iterable[TokenizedStringEntry]:
"""Parses TokenizedStringEntries from a CSV token database file."""
+ entries = []
for line in csv.reader(fd):
try:
token_str, date_str, string_literal = line
token = int(token_str, 16)
- date = (datetime.strptime(date_str, DATE_FORMAT)
- if date_str.strip() else None)
-
- yield TokenizedStringEntry(token, string_literal, DEFAULT_DOMAIN,
- date)
+ date = (
+ datetime.fromisoformat(date_str) if date_str.strip() else None
+ )
+
+ entries.append(
+ TokenizedStringEntry(
+ token, string_literal, DEFAULT_DOMAIN, date
+ )
+ )
except (ValueError, UnicodeDecodeError) as err:
- _LOG.error('Failed to parse tokenized string entry %s: %s', line,
- err)
+ _LOG.error(
+ 'Failed to parse tokenized string entry %s: %s', line, err
+ )
+ return entries
def write_csv(database: Database, fd: BinaryIO) -> None:
"""Writes the database as CSV to the provided binary file."""
for entry in sorted(database.entries()):
- # Align the CSV output to 10-character columns for improved readability.
- # Use \n instead of RFC 4180's \r\n.
- fd.write('{:08x},{:10},"{}"\n'.format(
+ _write_csv_line(fd, entry)
+
+
+def _write_csv_line(fd: BinaryIO, entry: TokenizedStringEntry):
+ """Write a line in CSV format to the provided binary file."""
+ # Align the CSV output to 10-character columns for improved readability.
+ # Use \n instead of RFC 4180's \r\n.
+ fd.write(
+ '{:08x},{:10},"{}"\n'.format(
entry.token,
- entry.date_removed.strftime(DATE_FORMAT) if entry.date_removed else
- '', entry.string.replace('"', '""')).encode()) # escape " as ""
+ entry.date_removed.date().isoformat() if entry.date_removed else '',
+ entry.string.replace('"', '""'),
+ ).encode()
+ ) # escape " as ""
class _BinaryFileFormat(NamedTuple):
@@ -361,7 +433,8 @@ def _check_that_file_is_csv_database(path: Path) -> None:
if len(data) != 8:
raise DatabaseFormatError(
f'Attempted to read {path} as a CSV token database, but the '
- f'file is too short ({len(data)} B)')
+ f'file is too short ({len(data)} B)'
+ )
# Make sure the first 8 chars are a valid hexadecimal number.
_ = int(data.decode(), 16)
@@ -374,18 +447,21 @@ def _check_that_file_is_csv_database(path: Path) -> None:
def parse_binary(fd: BinaryIO) -> Iterable[TokenizedStringEntry]:
"""Parses TokenizedStringEntries from a binary token database file."""
magic, entry_count = BINARY_FORMAT.header.unpack(
- fd.read(BINARY_FORMAT.header.size))
+ fd.read(BINARY_FORMAT.header.size)
+ )
if magic != BINARY_FORMAT.magic:
raise DatabaseFormatError(
f'Binary token database magic number mismatch (found {magic!r}, '
- f'expected {BINARY_FORMAT.magic!r}) while reading from {fd}')
+ f'expected {BINARY_FORMAT.magic!r}) while reading from {fd}'
+ )
entries = []
for _ in range(entry_count):
token, day, month, year = BINARY_FORMAT.entry.unpack(
- fd.read(BINARY_FORMAT.entry.size))
+ fd.read(BINARY_FORMAT.entry.size)
+ )
try:
date_removed: Optional[datetime] = datetime(year, month, day)
@@ -399,8 +475,10 @@ def parse_binary(fd: BinaryIO) -> Iterable[TokenizedStringEntry]:
def read_string(start):
end = string_table.find(b'\0', start)
- return string_table[start:string_table.find(b'\0', start)].decode(
- ), end + 1
+ return (
+ string_table[start : string_table.find(b'\0', start)].decode(),
+ end + 1,
+ )
offset = 0
for token, removed in entries:
@@ -425,16 +503,18 @@ def write_binary(database: Database, fd: BinaryIO) -> None:
# If there is no removal date, use the special value 0xffffffff for
# the day/month/year. That ensures that still-present tokens appear
# as the newest tokens when sorted by removal date.
- removed_day = 0xff
- removed_month = 0xff
- removed_year = 0xffff
+ removed_day = 0xFF
+ removed_month = 0xFF
+ removed_year = 0xFFFF
string_table += entry.string.encode()
string_table.append(0)
fd.write(
- BINARY_FORMAT.entry.pack(entry.token, removed_day, removed_month,
- removed_year))
+ BINARY_FORMAT.entry.pack(
+ entry.token, removed_day, removed_month, removed_year
+ )
+ )
fd.write(string_table)
@@ -445,23 +525,221 @@ class DatabaseFile(Database):
This class adds the write_to_file() method that writes to file from which it
was created in the correct format (CSV or binary).
"""
- def __init__(self, path: Union[Path, str]):
- self.path = Path(path)
+
+ def __init__(
+ self, path: Path, entries: Iterable[TokenizedStringEntry]
+ ) -> None:
+ super().__init__(entries)
+ self.path = path
+
+ @staticmethod
+ def load(path: Path) -> 'DatabaseFile':
+ """Creates a DatabaseFile that coincides to the file type."""
+ if path.is_dir():
+ return _DirectoryDatabase(path)
# Read the path as a packed binary file.
- with self.path.open('rb') as fd:
+ with path.open('rb') as fd:
if file_is_binary_database(fd):
- super().__init__(parse_binary(fd))
- self._export = write_binary
- return
+ return _BinaryDatabase(path, fd)
# Read the path as a CSV file.
- _check_that_file_is_csv_database(self.path)
- with self.path.open('r', newline='', encoding='utf-8') as file:
- super().__init__(parse_csv(file))
- self._export = write_csv
-
- def write_to_file(self, path: Optional[Union[Path, str]] = None) -> None:
- """Exports in the original format to the original or provided path."""
- with open(self.path if path is None else path, 'wb') as fd:
- self._export(self, fd)
+ _check_that_file_is_csv_database(path)
+ return _CSVDatabase(path)
+
+ @abstractmethod
+ def write_to_file(self, *, rewrite: bool = False) -> None:
+ """Exports in the original format to the original path."""
+
+ @abstractmethod
+ def add_and_discard_temporary(
+ self, entries: Iterable[TokenizedStringEntry], commit: str
+ ) -> None:
+ """Discards and adds entries to export in the original format.
+
+ Adds entries after removing temporary entries from the Database
+ to exclusively write re-occurring entries into memory and disk.
+ """
+
+
+class _BinaryDatabase(DatabaseFile):
+ def __init__(self, path: Path, fd: BinaryIO) -> None:
+ super().__init__(path, parse_binary(fd))
+
+ def write_to_file(self, *, rewrite: bool = False) -> None:
+ """Exports in the binary format to the original path."""
+ del rewrite # Binary databases are always rewritten
+ with self.path.open('wb') as fd:
+ write_binary(self, fd)
+
+ def add_and_discard_temporary(
+ self, entries: Iterable[TokenizedStringEntry], commit: str
+ ) -> None:
+ # TODO(b/241471465): Implement adding new tokens and removing
+ # temporary entries for binary databases.
+ raise NotImplementedError(
+ '--discard-temporary is currently only '
+ 'supported for directory databases'
+ )
+
+
+class _CSVDatabase(DatabaseFile):
+ def __init__(self, path: Path) -> None:
+ with path.open('r', newline='', encoding='utf-8') as csv_fd:
+ super().__init__(path, parse_csv(csv_fd))
+
+ def write_to_file(self, *, rewrite: bool = False) -> None:
+ """Exports in the CSV format to the original path."""
+ del rewrite # CSV databases are always rewritten
+ with self.path.open('wb') as fd:
+ write_csv(self, fd)
+
+ def add_and_discard_temporary(
+ self, entries: Iterable[TokenizedStringEntry], commit: str
+ ) -> None:
+ # TODO(b/241471465): Implement adding new tokens and removing
+ # temporary entries for CSV databases.
+ raise NotImplementedError(
+ '--discard-temporary is currently only '
+ 'supported for directory databases'
+ )
+
+
+# The suffix used for CSV files in a directory database.
+DIR_DB_SUFFIX = '.pw_tokenizer.csv'
+_DIR_DB_GLOB = '*' + DIR_DB_SUFFIX
+
+
+def _parse_directory(directory: Path) -> Iterable[TokenizedStringEntry]:
+ """Parses TokenizedStringEntries tokenizer CSV files in the directory."""
+ for path in directory.glob(_DIR_DB_GLOB):
+ yield from _CSVDatabase(path).entries()
+
+
+def _most_recently_modified_file(paths: Iterable[Path]) -> Path:
+ return max(paths, key=lambda path: path.stat().st_mtime)
+
+
+class _DirectoryDatabase(DatabaseFile):
+ def __init__(self, directory: Path) -> None:
+ super().__init__(directory, _parse_directory(directory))
+
+ def write_to_file(self, *, rewrite: bool = False) -> None:
+ """Creates a new CSV file in the directory with any new tokens."""
+ if rewrite:
+ # Write the entire database to a new CSV file
+ new_file = self._create_filename()
+ with new_file.open('wb') as fd:
+ write_csv(self, fd)
+
+ # Delete all CSV files except for the new CSV with everything.
+ for csv_file in self.path.glob(_DIR_DB_GLOB):
+ if csv_file != new_file:
+ csv_file.unlink()
+ else:
+ # Reread the tokens from disk and write only the new entries to CSV.
+ current_tokens = Database(_parse_directory(self.path))
+ new_entries = self.difference(current_tokens)
+ if new_entries:
+ with self._create_filename().open('wb') as fd:
+ write_csv(new_entries, fd)
+
+ def _git_paths(self, commands: List) -> List[Path]:
+ """Returns a list of files from a Git command, filtered to matc."""
+ try:
+ output = subprocess.run(
+ ['git', *commands, _DIR_DB_GLOB],
+ capture_output=True,
+ check=True,
+ cwd=self.path,
+ text=True,
+ ).stdout.strip()
+ return [self.path / repo_path for repo_path in output.splitlines()]
+ except subprocess.CalledProcessError:
+ return []
+
+ def _find_latest_csv(self, commit: str) -> Path:
+ """Finds or creates a CSV to which to write new entries.
+
+ - Check for untracked CSVs. Use the most recently modified file, if any.
+ - Check for CSVs added in HEAD, if HEAD is not an ancestor of commit.
+ Use the most recently modified file, if any.
+ - If no untracked or committed files were found, create a new file.
+ """
+
+ # Prioritize untracked files in the directory database.
+ untracked_changes = self._git_paths(
+ ['ls-files', '--others', '--exclude-standard']
+ )
+ if untracked_changes:
+ return _most_recently_modified_file(untracked_changes)
+
+ # Check if HEAD is an ancestor of the base commit. This checks whether
+ # the top commit has been merged or not. If it has been merged, create a
+ # new CSV to use. Otherwise, check if a CSV was added in the commit.
+ head_is_not_merged = (
+ subprocess.run(
+ ['git', 'merge-base', '--is-ancestor', 'HEAD', commit],
+ cwd=self.path,
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL,
+ ).returncode
+ != 0
+ )
+
+ if head_is_not_merged:
+ # Find CSVs added in the top commit.
+ csvs_from_top_commit = self._git_paths(
+ [
+ 'diff',
+ '--name-only',
+ '--diff-filter=A',
+ '--relative',
+ 'HEAD~',
+ ]
+ )
+
+ if csvs_from_top_commit:
+ return _most_recently_modified_file(csvs_from_top_commit)
+
+ return self._create_filename()
+
+ def _create_filename(self) -> Path:
+ """Generates a unique filename not in the directory."""
+ # Tracked and untracked files do not exist in the repo.
+ while (file := self.path / f'{uuid4().hex}{DIR_DB_SUFFIX}').exists():
+ pass
+ return file
+
+ def add_and_discard_temporary(
+ self, entries: Iterable[TokenizedStringEntry], commit: str
+ ) -> None:
+ """Adds new entries and discards temporary entries on disk.
+
+ - Find the latest CSV in the directory database or create a new one.
+ - Delete entries in the latest CSV that are not in the entries passed to
+ this function.
+ - Add the new entries to this database.
+ - Overwrite the latest CSV with only the newly added entries.
+ """
+ # Find entries not currently in the database.
+ added = Database(entries)
+ new_entries = added.difference(self)
+
+ csv_path = self._find_latest_csv(commit)
+ if csv_path.exists():
+ # Loading the CSV as a DatabaseFile.
+ csv_db = DatabaseFile.load(csv_path)
+
+ # Delete entries added in the CSV, but not added in this function.
+ for key in (e.key() for e in csv_db.difference(added).entries()):
+ del self._database[key]
+ del csv_db._database[key] # pylint: disable=protected-access
+
+ csv_db.add(new_entries.entries())
+ csv_db.write_to_file()
+ elif new_entries: # If the CSV does not exist, write all new tokens.
+ with csv_path.open('wb') as fd:
+ write_csv(new_entries, fd)
+
+ self.add(new_entries.entries())
diff --git a/pw_tokenizer/py/setup.cfg b/pw_tokenizer/py/setup.cfg
index 99e075d61..f7a20b7a7 100644
--- a/pw_tokenizer/py/setup.cfg
+++ b/pw_tokenizer/py/setup.cfg
@@ -21,6 +21,9 @@ description = Tools for working with tokenized strings
[options]
packages = pw_tokenizer
zip_safe = False
+install_requires =
+ pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
[options.package_data]
pw_tokenizer = py.typed
diff --git a/pw_tokenizer/py/tokenized_string_decoding_test_data.py b/pw_tokenizer/py/tokenized_string_decoding_test_data.py
index 56561bf2a..e0c85c357 100644
--- a/pw_tokenizer/py/tokenized_string_decoding_test_data.py
+++ b/pw_tokenizer/py/tokenized_string_decoding_test_data.py
@@ -25,7 +25,7 @@ def TestCase(*args): # pylint: disable=invalid-name
return tuple(args)
-# yapf: disable
+# fmt: off
TEST_DATA = (
# Simple strings
@@ -413,3 +413,4 @@ TestCase("%s: %lld 0x%16u%08X %d", "98: 428374782176628108 0x 1712762010661
TestCase("%s: %lld 0x%16u%08X %d", "99: 7023621621475593673 0x 1965201350BB6A8C7 58", b'\x02\x39\x39\x92\xb7\xf2\x8c\xdd\xa9\xf5\xf8\xc2\x01\x8e\xa3\xb5\xbb\x01\x8e\xa3\xb5\xbb\x01\x74'),
)
+# fmt: on
diff --git a/pw_tokenizer/py/tokens_test.py b/pw_tokenizer/py/tokens_test.py
index c20576276..ee3431b07 100755
--- a/pw_tokenizer/py/tokens_test.py
+++ b/pw_tokenizer/py/tokens_test.py
@@ -14,16 +14,17 @@
# the License.
"""Tests for the tokens module."""
-import datetime
+from datetime import datetime
import io
import logging
from pathlib import Path
+import shutil
import tempfile
from typing import Iterator
import unittest
from pw_tokenizer import tokens
-from pw_tokenizer.tokens import default_hash, _LOG
+from pw_tokenizer.tokens import c_hash, DIR_DB_SUFFIX, _LOG
CSV_DATABASE = '''\
00000000,2019-06-10,""
@@ -78,17 +79,80 @@ BINARY_DATABASE = (
b'%x%lld%1.2f%s\x00'
b'Jello?\x00'
b'%llu\x00'
- b'Won\'t fit : %s%d\x00')
+ b'Won\'t fit : %s%d\x00'
+)
INVALID_CSV = """\
1,,"Whoa there!"
2,this is totally invalid,"Whoa there!"
3,,"This one's OK"
,,"Also broken"
-5,1845-2-2,"I'm %s fine"
+5,1845-02-02,"I'm %s fine"
6,"Missing fields"
"""
+CSV_DATABASE_2 = '''\
+00000000, ,""
+141c35d5, ,"The answer: ""%s"""
+29aef586, ,"1234"
+2b78825f, ,"[:-)"
+2e668cd6, ,"Jello, world!"
+31631781, ,"%d"
+61fd1e26, ,"%ld"
+68ab92da, ,"%s there are %x (%.2f) of them%c"
+7b940e2a, ,"Hello %s! %hd %e"
+7da55d52, ,">:-[]"
+7f35a9a5, ,"TestName"
+851beeb6, ,"%u %d"
+881436a0, ,"The answer is: %s"
+88808930, ,"%u%d%02x%X%hu%hhd%d%ld%lu%lld%llu%c%c%c"
+92723f44, ,"???"
+a09d6698, ,"won-won-won-wonderful"
+aa9ffa66, ,"void pw::tokenizer::{anonymous}::TestName()"
+ad002c97, ,"%llx"
+b3653e13, ,"Jello!"
+cc6d3131, ,"Jello?"
+e13b0f94, ,"%llu"
+e65aefef, ,"Won't fit : %s%d"
+'''
+
+CSV_DATABASE_3 = """\
+17fa86d3, ,"hello"
+18c5017c, ,"yes"
+59b2701c, ,"The answer was: %s"
+881436a0, ,"The answer is: %s"
+d18ada0f, ,"something"
+"""
+
+CSV_DATABASE_4 = '''\
+00000000, ,""
+141c35d5, ,"The answer: ""%s"""
+17fa86d3, ,"hello"
+18c5017c, ,"yes"
+29aef586, ,"1234"
+2b78825f, ,"[:-)"
+2e668cd6, ,"Jello, world!"
+31631781, ,"%d"
+59b2701c, ,"The answer was: %s"
+61fd1e26, ,"%ld"
+68ab92da, ,"%s there are %x (%.2f) of them%c"
+7b940e2a, ,"Hello %s! %hd %e"
+7da55d52, ,">:-[]"
+7f35a9a5, ,"TestName"
+851beeb6, ,"%u %d"
+881436a0, ,"The answer is: %s"
+88808930, ,"%u%d%02x%X%hu%hhd%d%ld%lu%lld%llu%c%c%c"
+92723f44, ,"???"
+a09d6698, ,"won-won-won-wonderful"
+aa9ffa66, ,"void pw::tokenizer::{anonymous}::TestName()"
+ad002c97, ,"%llx"
+b3653e13, ,"Jello!"
+cc6d3131, ,"Jello?"
+d18ada0f, ,"something"
+e13b0f94, ,"%llu"
+e65aefef, ,"Won't fit : %s%d"
+'''
+
def read_db_from_csv(csv_str: str) -> tokens.Database:
with io.StringIO(csv_str) as csv_db:
@@ -97,31 +161,38 @@ def read_db_from_csv(csv_str: str) -> tokens.Database:
def _entries(*strings: str) -> Iterator[tokens.TokenizedStringEntry]:
for string in strings:
- yield tokens.TokenizedStringEntry(default_hash(string), string)
+ yield tokens.TokenizedStringEntry(c_hash(string), string)
class TokenDatabaseTest(unittest.TestCase):
"""Tests the token database class."""
- def test_csv(self):
+
+ def test_csv(self) -> None:
db = read_db_from_csv(CSV_DATABASE)
self.assertEqual(str(db), CSV_DATABASE)
db = read_db_from_csv('')
self.assertEqual(str(db), '')
- def test_csv_formatting(self):
+ def test_csv_formatting(self) -> None:
db = read_db_from_csv('')
self.assertEqual(str(db), '')
- db = read_db_from_csv('abc123,2048-4-1,Fake string\n')
+ db = read_db_from_csv('abc123,2048-04-01,Fake string\n')
self.assertEqual(str(db), '00abc123,2048-04-01,"Fake string"\n')
- db = read_db_from_csv('1,1990-01-01,"Quotes"""\n'
- '0,1990-02-01,"Commas,"",,"\n')
- self.assertEqual(str(db), ('00000000,1990-02-01,"Commas,"",,"\n'
- '00000001,1990-01-01,"Quotes"""\n'))
-
- def test_bad_csv(self):
+ db = read_db_from_csv(
+ '1,1990-01-01,"Quotes"""\n' '0,1990-02-01,"Commas,"",,"\n'
+ )
+ self.assertEqual(
+ str(db),
+ (
+ '00000000,1990-02-01,"Commas,"",,"\n'
+ '00000001,1990-01-01,"Quotes"""\n'
+ ),
+ )
+
+ def test_bad_csv(self) -> None:
with self.assertLogs(_LOG, logging.ERROR) as logs:
db = read_db_from_csv(INVALID_CSV)
@@ -135,31 +206,31 @@ class TokenDatabaseTest(unittest.TestCase):
self.assertEqual(db.token_to_entries[5][0].string, "I'm %s fine")
self.assertFalse(db.token_to_entries[6])
- def test_lookup(self):
+ def test_lookup(self) -> None:
db = read_db_from_csv(CSV_DATABASE)
self.assertEqual(db.token_to_entries[0x9999], [])
- matches = db.token_to_entries[0x2e668cd6]
+ matches = db.token_to_entries[0x2E668CD6]
self.assertEqual(len(matches), 1)
jello = matches[0]
- self.assertEqual(jello.token, 0x2e668cd6)
+ self.assertEqual(jello.token, 0x2E668CD6)
self.assertEqual(jello.string, 'Jello, world!')
- self.assertEqual(jello.date_removed, datetime.datetime(2019, 6, 11))
+ self.assertEqual(jello.date_removed, datetime(2019, 6, 11))
- matches = db.token_to_entries[0xe13b0f94]
+ matches = db.token_to_entries[0xE13B0F94]
self.assertEqual(len(matches), 1)
llu = matches[0]
- self.assertEqual(llu.token, 0xe13b0f94)
+ self.assertEqual(llu.token, 0xE13B0F94)
self.assertEqual(llu.string, '%llu')
self.assertIsNone(llu.date_removed)
- answer, = db.token_to_entries[0x141c35d5]
+ (answer,) = db.token_to_entries[0x141C35D5]
self.assertEqual(answer.string, 'The answer: "%s"')
- def test_collisions(self):
- hash_1 = tokens.pw_tokenizer_65599_hash('o000', 96)
- hash_2 = tokens.pw_tokenizer_65599_hash('0Q1Q', 96)
+ def test_collisions(self) -> None:
+ hash_1 = tokens.c_hash('o000', 96)
+ hash_2 = tokens.c_hash('0Q1Q', 96)
self.assertEqual(hash_1, hash_2)
db = tokens.Database.from_strings(['o000', '0Q1Q'])
@@ -167,151 +238,188 @@ class TokenDatabaseTest(unittest.TestCase):
self.assertEqual(len(db.token_to_entries[hash_1]), 2)
self.assertCountEqual(
[entry.string for entry in db.token_to_entries[hash_1]],
- ['o000', '0Q1Q'])
+ ['o000', '0Q1Q'],
+ )
- def test_purge(self):
+ def test_purge(self) -> None:
db = read_db_from_csv(CSV_DATABASE)
original_length = len(db.token_to_entries)
self.assertEqual(db.token_to_entries[0][0].string, '')
self.assertEqual(db.token_to_entries[0x31631781][0].string, '%d')
- self.assertEqual(db.token_to_entries[0x2e668cd6][0].string,
- 'Jello, world!')
- self.assertEqual(db.token_to_entries[0xb3653e13][0].string, 'Jello!')
- self.assertEqual(db.token_to_entries[0xcc6d3131][0].string, 'Jello?')
- self.assertEqual(db.token_to_entries[0xe65aefef][0].string,
- "Won't fit : %s%d")
-
- db.purge(datetime.datetime(2019, 6, 11))
+ self.assertEqual(
+ db.token_to_entries[0x2E668CD6][0].string, 'Jello, world!'
+ )
+ self.assertEqual(db.token_to_entries[0xB3653E13][0].string, 'Jello!')
+ self.assertEqual(db.token_to_entries[0xCC6D3131][0].string, 'Jello?')
+ self.assertEqual(
+ db.token_to_entries[0xE65AEFEF][0].string, "Won't fit : %s%d"
+ )
+
+ db.purge(datetime(2019, 6, 11))
self.assertLess(len(db.token_to_entries), original_length)
self.assertFalse(db.token_to_entries[0])
self.assertEqual(db.token_to_entries[0x31631781][0].string, '%d')
- self.assertFalse(db.token_to_entries[0x2e668cd6])
- self.assertEqual(db.token_to_entries[0xb3653e13][0].string, 'Jello!')
- self.assertEqual(db.token_to_entries[0xcc6d3131][0].string, 'Jello?')
- self.assertFalse(db.token_to_entries[0xe65aefef])
+ self.assertFalse(db.token_to_entries[0x2E668CD6])
+ self.assertEqual(db.token_to_entries[0xB3653E13][0].string, 'Jello!')
+ self.assertEqual(db.token_to_entries[0xCC6D3131][0].string, 'Jello?')
+ self.assertFalse(db.token_to_entries[0xE65AEFEF])
- def test_merge(self):
+ def test_merge(self) -> None:
"""Tests the tokens.Database merge method."""
db = tokens.Database()
# Test basic merging into an empty database.
db.merge(
- tokens.Database([
- tokens.TokenizedStringEntry(
- 1, 'one', date_removed=datetime.datetime.min),
- tokens.TokenizedStringEntry(
- 2, 'two', date_removed=datetime.datetime.min),
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 1, 'one', date_removed=datetime.min
+ ),
+ tokens.TokenizedStringEntry(
+ 2, 'two', date_removed=datetime.min
+ ),
+ ]
+ )
+ )
self.assertEqual({str(e) for e in db.entries()}, {'one', 'two'})
- self.assertEqual(db.token_to_entries[1][0].date_removed,
- datetime.datetime.min)
- self.assertEqual(db.token_to_entries[2][0].date_removed,
- datetime.datetime.min)
+ self.assertEqual(db.token_to_entries[1][0].date_removed, datetime.min)
+ self.assertEqual(db.token_to_entries[2][0].date_removed, datetime.min)
# Test merging in an entry with a removal date.
db.merge(
- tokens.Database([
- tokens.TokenizedStringEntry(3, 'three'),
- tokens.TokenizedStringEntry(
- 4, 'four', date_removed=datetime.datetime.min),
- ]))
- self.assertEqual({str(e)
- for e in db.entries()},
- {'one', 'two', 'three', 'four'})
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(3, 'three'),
+ tokens.TokenizedStringEntry(
+ 4, 'four', date_removed=datetime.min
+ ),
+ ]
+ )
+ )
+ self.assertEqual(
+ {str(e) for e in db.entries()}, {'one', 'two', 'three', 'four'}
+ )
self.assertIsNone(db.token_to_entries[3][0].date_removed)
- self.assertEqual(db.token_to_entries[4][0].date_removed,
- datetime.datetime.min)
+ self.assertEqual(db.token_to_entries[4][0].date_removed, datetime.min)
# Test merging in one entry.
- db.merge(tokens.Database([
- tokens.TokenizedStringEntry(5, 'five'),
- ]))
- self.assertEqual({str(e)
- for e in db.entries()},
- {'one', 'two', 'three', 'four', 'five'})
- self.assertEqual(db.token_to_entries[4][0].date_removed,
- datetime.datetime.min)
+ db.merge(
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(5, 'five'),
+ ]
+ )
+ )
+ self.assertEqual(
+ {str(e) for e in db.entries()},
+ {'one', 'two', 'three', 'four', 'five'},
+ )
+ self.assertEqual(db.token_to_entries[4][0].date_removed, datetime.min)
self.assertIsNone(db.token_to_entries[5][0].date_removed)
# Merge in repeated entries different removal dates.
db.merge(
- tokens.Database([
- tokens.TokenizedStringEntry(
- 4, 'four', date_removed=datetime.datetime.max),
- tokens.TokenizedStringEntry(
- 5, 'five', date_removed=datetime.datetime.max),
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 4, 'four', date_removed=datetime.max
+ ),
+ tokens.TokenizedStringEntry(
+ 5, 'five', date_removed=datetime.max
+ ),
+ ]
+ )
+ )
self.assertEqual(len(db.entries()), 5)
- self.assertEqual({str(e)
- for e in db.entries()},
- {'one', 'two', 'three', 'four', 'five'})
- self.assertEqual(db.token_to_entries[4][0].date_removed,
- datetime.datetime.max)
+ self.assertEqual(
+ {str(e) for e in db.entries()},
+ {'one', 'two', 'three', 'four', 'five'},
+ )
+ self.assertEqual(db.token_to_entries[4][0].date_removed, datetime.max)
self.assertIsNone(db.token_to_entries[5][0].date_removed)
# Merge in the same repeated entries now without removal dates.
db.merge(
- tokens.Database([
- tokens.TokenizedStringEntry(4, 'four'),
- tokens.TokenizedStringEntry(5, 'five')
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(4, 'four'),
+ tokens.TokenizedStringEntry(5, 'five'),
+ ]
+ )
+ )
self.assertEqual(len(db.entries()), 5)
- self.assertEqual({str(e)
- for e in db.entries()},
- {'one', 'two', 'three', 'four', 'five'})
+ self.assertEqual(
+ {str(e) for e in db.entries()},
+ {'one', 'two', 'three', 'four', 'five'},
+ )
self.assertIsNone(db.token_to_entries[4][0].date_removed)
self.assertIsNone(db.token_to_entries[5][0].date_removed)
# Merge in an empty databsse.
db.merge(tokens.Database([]))
- self.assertEqual({str(e)
- for e in db.entries()},
- {'one', 'two', 'three', 'four', 'five'})
+ self.assertEqual(
+ {str(e) for e in db.entries()},
+ {'one', 'two', 'three', 'four', 'five'},
+ )
- def test_merge_multiple_datbases_in_one_call(self):
+ def test_merge_multiple_datbases_in_one_call(self) -> None:
"""Tests the merge and merged methods with multiple databases."""
db = tokens.Database.merged(
- tokens.Database([
- tokens.TokenizedStringEntry(1,
- 'one',
- date_removed=datetime.datetime.max)
- ]),
- tokens.Database([
- tokens.TokenizedStringEntry(2,
- 'two',
- date_removed=datetime.datetime.min)
- ]),
- tokens.Database([
- tokens.TokenizedStringEntry(1,
- 'one',
- date_removed=datetime.datetime.min)
- ]))
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 1, 'one', date_removed=datetime.max
+ )
+ ]
+ ),
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 2, 'two', date_removed=datetime.min
+ )
+ ]
+ ),
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 1, 'one', date_removed=datetime.min
+ )
+ ]
+ ),
+ )
self.assertEqual({str(e) for e in db.entries()}, {'one', 'two'})
db.merge(
- tokens.Database([
- tokens.TokenizedStringEntry(4,
- 'four',
- date_removed=datetime.datetime.max)
- ]),
- tokens.Database([
- tokens.TokenizedStringEntry(2,
- 'two',
- date_removed=datetime.datetime.max)
- ]),
- tokens.Database([
- tokens.TokenizedStringEntry(3,
- 'three',
- date_removed=datetime.datetime.min)
- ]))
- self.assertEqual({str(e)
- for e in db.entries()},
- {'one', 'two', 'three', 'four'})
-
- def test_entry_counts(self):
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 4, 'four', date_removed=datetime.max
+ )
+ ]
+ ),
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 2, 'two', date_removed=datetime.max
+ )
+ ]
+ ),
+ tokens.Database(
+ [
+ tokens.TokenizedStringEntry(
+ 3, 'three', date_removed=datetime.min
+ )
+ ]
+ ),
+ )
+ self.assertEqual(
+ {str(e) for e in db.entries()}, {'one', 'two', 'three', 'four'}
+ )
+
+ def test_entry_counts(self) -> None:
self.assertEqual(len(CSV_DATABASE.splitlines()), 16)
db = read_db_from_csv(CSV_DATABASE)
@@ -324,42 +432,49 @@ class TokenDatabaseTest(unittest.TestCase):
self.assertEqual(len(db.entries()), 18)
self.assertEqual(len(db.token_to_entries), 17)
- def test_mark_removed(self):
+ def test_mark_removed(self) -> None:
"""Tests that date_removed field is set by mark_removed."""
db = tokens.Database.from_strings(
- ['MILK', 'apples', 'oranges', 'CHEESE', 'pears'])
+ ['MILK', 'apples', 'oranges', 'CHEESE', 'pears']
+ )
self.assertTrue(
- all(entry.date_removed is None for entry in db.entries()))
- date_1 = datetime.datetime(1, 2, 3)
+ all(entry.date_removed is None for entry in db.entries())
+ )
+ date_1 = datetime(1, 2, 3)
db.mark_removed(_entries('apples', 'oranges', 'pears'), date_1)
self.assertEqual(
- db.token_to_entries[default_hash('MILK')][0].date_removed, date_1)
+ db.token_to_entries[c_hash('MILK')][0].date_removed, date_1
+ )
self.assertEqual(
- db.token_to_entries[default_hash('CHEESE')][0].date_removed,
- date_1)
+ db.token_to_entries[c_hash('CHEESE')][0].date_removed, date_1
+ )
- now = datetime.datetime.now()
+ now = datetime.now()
db.mark_removed(_entries('MILK', 'CHEESE', 'pears'))
# New strings are not added or re-added in mark_removed().
- self.assertGreaterEqual(
- db.token_to_entries[default_hash('MILK')][0].date_removed, date_1)
- self.assertGreaterEqual(
- db.token_to_entries[default_hash('CHEESE')][0].date_removed,
- date_1)
+ milk_date = db.token_to_entries[c_hash('MILK')][0].date_removed
+ assert milk_date is not None
+ self.assertGreaterEqual(milk_date, date_1)
+
+ cheese_date = db.token_to_entries[c_hash('CHEESE')][0].date_removed
+ assert cheese_date is not None
+ self.assertGreaterEqual(cheese_date, date_1)
# These strings were removed.
- self.assertGreaterEqual(
- db.token_to_entries[default_hash('apples')][0].date_removed, now)
- self.assertGreaterEqual(
- db.token_to_entries[default_hash('oranges')][0].date_removed, now)
- self.assertIsNone(
- db.token_to_entries[default_hash('pears')][0].date_removed)
-
- def test_add(self):
+ apples_date = db.token_to_entries[c_hash('apples')][0].date_removed
+ assert apples_date is not None
+ self.assertGreaterEqual(apples_date, now)
+
+ oranges_date = db.token_to_entries[c_hash('oranges')][0].date_removed
+ assert oranges_date is not None
+ self.assertGreaterEqual(oranges_date, now)
+ self.assertIsNone(db.token_to_entries[c_hash('pears')][0].date_removed)
+
+ def test_add(self) -> None:
db = tokens.Database()
db.add(_entries('MILK', 'apples'))
self.assertEqual({e.string for e in db.entries()}, {'MILK', 'apples'})
@@ -371,13 +486,62 @@ class TokenDatabaseTest(unittest.TestCase):
self.assertEqual(len(db.entries()), 6)
db.add(_entries('MILK'))
- self.assertEqual({e.string
- for e in db.entries()}, {
- 'MILK', 'apples', 'oranges', 'CHEESE', 'pears',
- 'only this one is new'
- })
+ self.assertEqual(
+ {e.string for e in db.entries()},
+ {
+ 'MILK',
+ 'apples',
+ 'oranges',
+ 'CHEESE',
+ 'pears',
+ 'only this one is new',
+ },
+ )
+
+ def test_add_duplicate_entries_keeps_none_as_removal_date(self) -> None:
+ db = tokens.Database()
+ db.add(
+ [
+ tokens.TokenizedStringEntry(1, 'Spam', '', datetime.now()),
+ tokens.TokenizedStringEntry(1, 'Spam', ''),
+ tokens.TokenizedStringEntry(1, 'Spam', '', datetime.min),
+ ]
+ )
+ self.assertEqual(len(db), 1)
+ self.assertIsNone(db.token_to_entries[1][0].date_removed)
+
+ def test_add_duplicate_entries_keeps_newest_removal_date(self) -> None:
+ db = tokens.Database()
+ db.add(
+ [
+ tokens.TokenizedStringEntry(1, 'Spam', '', datetime.now()),
+ tokens.TokenizedStringEntry(1, 'Spam', '', datetime.max),
+ tokens.TokenizedStringEntry(1, 'Spam', '', datetime.now()),
+ tokens.TokenizedStringEntry(1, 'Spam', '', datetime.min),
+ ]
+ )
+ self.assertEqual(len(db), 1)
+ self.assertEqual(db.token_to_entries[1][0].date_removed, datetime.max)
+
+ def test_difference(self) -> None:
+ first = tokens.Database(
+ [
+ tokens.TokenizedStringEntry(1, 'one'),
+ tokens.TokenizedStringEntry(2, 'two'),
+ tokens.TokenizedStringEntry(3, 'three'),
+ ]
+ )
+ second = tokens.Database(
+ [
+ tokens.TokenizedStringEntry(1, 'one'),
+ tokens.TokenizedStringEntry(3, 'three'),
+ tokens.TokenizedStringEntry(4, 'four'),
+ ]
+ )
+ difference = first.difference(second)
+ self.assertEqual({e.string for e in difference.entries()}, {'two'})
- def test_binary_format_write(self):
+ def test_binary_format_write(self) -> None:
db = read_db_from_csv(CSV_DATABASE)
with io.BytesIO() as fd:
@@ -386,7 +550,7 @@ class TokenDatabaseTest(unittest.TestCase):
self.assertEqual(BINARY_DATABASE, binary_db)
- def test_binary_format_parse(self):
+ def test_binary_format_parse(self) -> None:
with io.BytesIO(BINARY_DATABASE) as binary_db:
db = tokens.Database(tokens.parse_binary(binary_db))
@@ -395,100 +559,208 @@ class TokenDatabaseTest(unittest.TestCase):
class TestDatabaseFile(unittest.TestCase):
"""Tests the DatabaseFile class."""
- def setUp(self):
+
+ def setUp(self) -> None:
file = tempfile.NamedTemporaryFile(delete=False)
file.close()
self._path = Path(file.name)
- def tearDown(self):
+ def tearDown(self) -> None:
self._path.unlink()
- def test_update_csv_file(self):
+ def test_update_csv_file(self) -> None:
self._path.write_text(CSV_DATABASE)
- db = tokens.DatabaseFile(self._path)
+ db = tokens.DatabaseFile.load(self._path)
self.assertEqual(str(db), CSV_DATABASE)
- db.add([tokens.TokenizedStringEntry(0xffffffff, 'New entry!')])
+ db.add([tokens.TokenizedStringEntry(0xFFFFFFFF, 'New entry!')])
db.write_to_file()
- self.assertEqual(self._path.read_text(),
- CSV_DATABASE + 'ffffffff, ,"New entry!"\n')
+ self.assertEqual(
+ self._path.read_text(),
+ CSV_DATABASE + 'ffffffff, ,"New entry!"\n',
+ )
- def test_csv_file_too_short_raises_exception(self):
+ def test_csv_file_too_short_raises_exception(self) -> None:
self._path.write_text('1234')
with self.assertRaises(tokens.DatabaseFormatError):
- tokens.DatabaseFile(self._path)
+ tokens.DatabaseFile.load(self._path)
- def test_csv_invalid_format_raises_exception(self):
+ def test_csv_invalid_format_raises_exception(self) -> None:
self._path.write_text('MK34567890')
with self.assertRaises(tokens.DatabaseFormatError):
- tokens.DatabaseFile(self._path)
+ tokens.DatabaseFile.load(self._path)
- def test_csv_not_utf8(self):
+ def test_csv_not_utf8(self) -> None:
self._path.write_bytes(b'\x80' * 20)
with self.assertRaises(tokens.DatabaseFormatError):
- tokens.DatabaseFile(self._path)
+ tokens.DatabaseFile.load(self._path)
class TestFilter(unittest.TestCase):
"""Tests the filtering functionality."""
- def setUp(self):
- self.db = tokens.Database([
- tokens.TokenizedStringEntry(1, 'Luke'),
- tokens.TokenizedStringEntry(2, 'Leia'),
- tokens.TokenizedStringEntry(2, 'Darth Vader'),
- tokens.TokenizedStringEntry(2, 'Emperor Palpatine'),
- tokens.TokenizedStringEntry(3, 'Han'),
- tokens.TokenizedStringEntry(4, 'Chewbacca'),
- tokens.TokenizedStringEntry(5, 'Darth Maul'),
- tokens.TokenizedStringEntry(6, 'Han Solo'),
- ])
-
- def test_filter_include_single_regex(self):
+
+ def setUp(self) -> None:
+ self.db = tokens.Database(
+ [
+ tokens.TokenizedStringEntry(1, 'Luke'),
+ tokens.TokenizedStringEntry(2, 'Leia'),
+ tokens.TokenizedStringEntry(2, 'Darth Vader'),
+ tokens.TokenizedStringEntry(2, 'Emperor Palpatine'),
+ tokens.TokenizedStringEntry(3, 'Han'),
+ tokens.TokenizedStringEntry(4, 'Chewbacca'),
+ tokens.TokenizedStringEntry(5, 'Darth Maul'),
+ tokens.TokenizedStringEntry(6, 'Han Solo'),
+ ]
+ )
+
+ def test_filter_include_single_regex(self) -> None:
self.db.filter(include=[' ']) # anything with a space
self.assertEqual(
set(e.string for e in self.db.entries()),
- {'Darth Vader', 'Emperor Palpatine', 'Darth Maul', 'Han Solo'})
+ {'Darth Vader', 'Emperor Palpatine', 'Darth Maul', 'Han Solo'},
+ )
- def test_filter_include_multiple_regexes(self):
+ def test_filter_include_multiple_regexes(self) -> None:
self.db.filter(include=['Darth', 'cc', '^Han$'])
- self.assertEqual(set(e.string for e in self.db.entries()),
- {'Darth Vader', 'Darth Maul', 'Han', 'Chewbacca'})
+ self.assertEqual(
+ set(e.string for e in self.db.entries()),
+ {'Darth Vader', 'Darth Maul', 'Han', 'Chewbacca'},
+ )
- def test_filter_include_no_matches(self):
+ def test_filter_include_no_matches(self) -> None:
self.db.filter(include=['Gandalf'])
self.assertFalse(self.db.entries())
- def test_filter_exclude_single_regex(self):
+ def test_filter_exclude_single_regex(self) -> None:
self.db.filter(exclude=['^[^L]'])
- self.assertEqual(set(e.string for e in self.db.entries()),
- {'Luke', 'Leia'})
+ self.assertEqual(
+ set(e.string for e in self.db.entries()), {'Luke', 'Leia'}
+ )
- def test_filter_exclude_multiple_regexes(self):
+ def test_filter_exclude_multiple_regexes(self) -> None:
self.db.filter(exclude=[' ', 'Han', 'Chewbacca'])
- self.assertEqual(set(e.string for e in self.db.entries()),
- {'Luke', 'Leia'})
+ self.assertEqual(
+ set(e.string for e in self.db.entries()), {'Luke', 'Leia'}
+ )
- def test_filter_exclude_no_matches(self):
+ def test_filter_exclude_no_matches(self) -> None:
self.db.filter(exclude=['.*'])
self.assertFalse(self.db.entries())
- def test_filter_include_and_exclude(self):
+ def test_filter_include_and_exclude(self) -> None:
self.db.filter(include=[' '], exclude=['Darth', 'Emperor'])
- self.assertEqual(set(e.string for e in self.db.entries()),
- {'Han Solo'})
+ self.assertEqual(set(e.string for e in self.db.entries()), {'Han Solo'})
- def test_filter_neither_include_nor_exclude(self):
+ def test_filter_neither_include_nor_exclude(self) -> None:
self.db.filter()
self.assertEqual(
- set(e.string for e in self.db.entries()), {
- 'Luke', 'Leia', 'Darth Vader', 'Emperor Palpatine', 'Han',
- 'Chewbacca', 'Darth Maul', 'Han Solo'
- })
+ set(e.string for e in self.db.entries()),
+ {
+ 'Luke',
+ 'Leia',
+ 'Darth Vader',
+ 'Emperor Palpatine',
+ 'Han',
+ 'Chewbacca',
+ 'Darth Maul',
+ 'Han Solo',
+ },
+ )
+
+
+class TestDirectoryDatabase(unittest.TestCase):
+ """Test DirectoryDatabase class is properly loaded."""
+
+ def setUp(self) -> None:
+ self._dir = Path(tempfile.mkdtemp('_pw_tokenizer_test'))
+ self._db_dir = self._dir / '_dir_database_test'
+ self._db_dir.mkdir(exist_ok=True)
+ self._db_csv = self._db_dir / f'first{DIR_DB_SUFFIX}'
+
+ def tearDown(self) -> None:
+ shutil.rmtree(self._dir)
+
+ def test_loading_empty_directory(self) -> None:
+ self.assertFalse(tokens.DatabaseFile.load(self._db_dir).entries())
+
+ def test_loading_a_single_file(self) -> None:
+ self._db_csv.write_text(CSV_DATABASE)
+ csv = tokens.DatabaseFile.load(self._db_csv)
+ directory_db = tokens.DatabaseFile.load(self._db_dir)
+ self.assertEqual(1, len(list(self._db_dir.iterdir())))
+ self.assertEqual(str(csv), str(directory_db))
+
+ def test_loading_multiples_files(self) -> None:
+ self._db_csv.write_text(CSV_DATABASE_3)
+ first_csv = tokens.DatabaseFile.load(self._db_csv)
+
+ path_to_second_csv = self._db_dir / f'second{DIR_DB_SUFFIX}'
+ path_to_second_csv.write_text(CSV_DATABASE_2)
+ second_csv = tokens.DatabaseFile.load(path_to_second_csv)
+
+ path_to_third_csv = self._db_dir / f'third{DIR_DB_SUFFIX}'
+ path_to_third_csv.write_text(CSV_DATABASE_4)
+ third_csv = tokens.DatabaseFile.load(path_to_third_csv)
+
+ all_databases_merged = tokens.Database.merged(
+ first_csv, second_csv, third_csv
+ )
+ directory_db = tokens.DatabaseFile.load(self._db_dir)
+ self.assertEqual(3, len(list(self._db_dir.iterdir())))
+ self.assertEqual(str(all_databases_merged), str(directory_db))
+
+ def test_loading_multiples_files_with_removal_dates(self) -> None:
+ self._db_csv.write_text(CSV_DATABASE)
+ first_csv = tokens.DatabaseFile.load(self._db_csv)
+
+ path_to_second_csv = self._db_dir / f'second{DIR_DB_SUFFIX}'
+ path_to_second_csv.write_text(CSV_DATABASE_2)
+ second_csv = tokens.DatabaseFile.load(path_to_second_csv)
+
+ path_to_third_csv = self._db_dir / f'third{DIR_DB_SUFFIX}'
+ path_to_third_csv.write_text(CSV_DATABASE_3)
+ third_csv = tokens.DatabaseFile.load(path_to_third_csv)
+
+ all_databases_merged = tokens.Database.merged(
+ first_csv, second_csv, third_csv
+ )
+ directory_db = tokens.DatabaseFile.load(self._db_dir)
+ self.assertEqual(3, len(list(self._db_dir.iterdir())))
+ self.assertEqual(str(all_databases_merged), str(directory_db))
+
+ def test_rewrite(self) -> None:
+ self._db_dir.joinpath('junk_file').write_text('should be ignored')
+
+ self._db_csv.write_text(CSV_DATABASE_3)
+ first_csv = tokens.DatabaseFile.load(self._db_csv)
+
+ path_to_second_csv = self._db_dir / f'second{DIR_DB_SUFFIX}'
+ path_to_second_csv.write_text(CSV_DATABASE_2)
+ second_csv = tokens.DatabaseFile.load(path_to_second_csv)
+
+ path_to_third_csv = self._db_dir / f'third{DIR_DB_SUFFIX}'
+ path_to_third_csv.write_text(CSV_DATABASE_4)
+ third_csv = tokens.DatabaseFile.load(path_to_third_csv)
+
+ all_databases_merged = tokens.Database.merged(
+ first_csv, second_csv, third_csv
+ )
+
+ directory_db = tokens.DatabaseFile.load(self._db_dir)
+ directory_db.write_to_file(rewrite=True)
+
+ self.assertEqual(1, len(list(self._db_dir.glob(f'*{DIR_DB_SUFFIX}'))))
+ self.assertEqual(
+ self._db_dir.joinpath('junk_file').read_text(), 'should be ignored'
+ )
+
+ directory_db = tokens.DatabaseFile.load(self._db_dir)
+ self.assertEqual(str(all_databases_merged), str(directory_db))
if __name__ == '__main__':
diff --git a/pw_tokenizer/py/varint_test_data.py b/pw_tokenizer/py/varint_test_data.py
index 26525da2e..1bd95923c 100644
--- a/pw_tokenizer/py/varint_test_data.py
+++ b/pw_tokenizer/py/varint_test_data.py
@@ -25,7 +25,7 @@ def TestCase(*args): # pylint: disable=invalid-name
return tuple(args)
-# yapf: disable
+# fmt: off
TEST_DATA = (
# Important numbers
@@ -1008,3 +1008,4 @@ TestCase("%d", "126", "%u", "126", b'\xfc\x01'),
TestCase("%d", "127", "%u", "127", b'\xfe\x01'),
)
+# fmt: on
diff --git a/pw_tokenizer/simple_tokenize_test.cc b/pw_tokenizer/simple_tokenize_test.cc
index e4bd95623..4e9bb6522 100644
--- a/pw_tokenizer/simple_tokenize_test.cc
+++ b/pw_tokenizer/simple_tokenize_test.cc
@@ -14,11 +14,10 @@
#include <algorithm>
#include <array>
+#include <cstring>
#include "gtest/gtest.h"
#include "pw_tokenizer/tokenize.h"
-#include "pw_tokenizer/tokenize_to_global_handler.h"
-#include "pw_tokenizer/tokenize_to_global_handler_with_payload.h"
namespace pw {
namespace tokenizer {
@@ -67,136 +66,12 @@ TEST(TokenizeStringLiteral, GlobalVariable_MatchesHash) {
class TokenizeToBuffer : public ::testing::Test {
public:
- TokenizeToBuffer() : buffer_ {}
- {}
+ TokenizeToBuffer() : buffer_{} {}
protected:
uint8_t buffer_[64];
};
-// Test fixture for callback and global handler. Both of these need a global
-// message buffer. To keep the message buffers separate, template this on the
-// derived class type.
-template <typename Impl>
-class GlobalMessage : public ::testing::Test {
- public:
- static void SetMessage(const uint8_t* message, size_t size) {
- ASSERT_LE(size, sizeof(message_));
- std::memcpy(message_, message, size);
- message_size_bytes_ = size;
- }
-
- protected:
- GlobalMessage() {
- std::memset(message_, 0, sizeof(message_));
- message_size_bytes_ = 0;
- }
-
- static uint8_t message_[256];
- static size_t message_size_bytes_;
-};
-
-template <typename Impl>
-uint8_t GlobalMessage<Impl>::message_[256] = {};
-template <typename Impl>
-size_t GlobalMessage<Impl>::message_size_bytes_ = 0;
-
-class TokenizeToCallback : public GlobalMessage<TokenizeToCallback> {};
-
-template <uint8_t... kData, size_t kSize>
-std::array<uint8_t, sizeof(uint32_t) + sizeof...(kData)> ExpectedData(
- const char (&format)[kSize]) {
- const uint32_t value = TestHash(format);
- return std::array<uint8_t, sizeof(uint32_t) + sizeof...(kData)>{
- static_cast<uint8_t>(value & 0xff),
- static_cast<uint8_t>(value >> 8 & 0xff),
- static_cast<uint8_t>(value >> 16 & 0xff),
- static_cast<uint8_t>(value >> 24 & 0xff),
- kData...};
-}
-
-TEST_F(TokenizeToCallback, Variety) {
- PW_TOKENIZE_TO_CALLBACK(
- SetMessage, "%s there are %x (%.2f) of them%c", "Now", 2u, 2.0f, '.');
- const auto expected = // clang-format off
- ExpectedData<3, 'N', 'o', 'w', // string "Now"
- 0x04, // unsigned 2 (zig-zag encoded)
- 0x00, 0x00, 0x00, 0x40, // float 2.0
- 0x5C // char '.' (0x2E, zig-zag encoded)
- >("%s there are %x (%.2f) of them%c");
- // clang-format on
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-class TokenizeToGlobalHandler : public GlobalMessage<TokenizeToGlobalHandler> {
-};
-
-TEST_F(TokenizeToGlobalHandler, Variety) {
- PW_TOKENIZE_TO_GLOBAL_HANDLER("%x%lld%1.2f%s", 0, 0ll, -0.0, "");
- const auto expected =
- ExpectedData<0, 0, 0x00, 0x00, 0x00, 0x80, 0>("%x%lld%1.2f%s");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-extern "C" void pw_tokenizer_HandleEncodedMessage(
- const uint8_t* encoded_message, size_t size_bytes) {
- TokenizeToGlobalHandler::SetMessage(encoded_message, size_bytes);
-}
-
-class TokenizeToGlobalHandlerWithPayload
- : public GlobalMessage<TokenizeToGlobalHandlerWithPayload> {
- public:
- static void SetPayload(pw_tokenizer_Payload payload) {
- payload_ = static_cast<intptr_t>(payload);
- }
-
- protected:
- TokenizeToGlobalHandlerWithPayload() { payload_ = {}; }
-
- static intptr_t payload_;
-};
-
-intptr_t TokenizeToGlobalHandlerWithPayload::payload_;
-
-TEST_F(TokenizeToGlobalHandlerWithPayload, Variety) {
- ASSERT_NE(payload_, 123);
-
- const auto expected =
- ExpectedData<0, 0, 0x00, 0x00, 0x00, 0x80, 0>("%x%lld%1.2f%s");
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
- static_cast<pw_tokenizer_Payload>(123),
- "%x%lld%1.2f%s",
- 0,
- 0ll,
- -0.0,
- "");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
- EXPECT_EQ(payload_, 123);
-
- PW_TOKENIZE_TO_GLOBAL_HANDLER_WITH_PAYLOAD(
- static_cast<pw_tokenizer_Payload>(-543),
- "%x%lld%1.2f%s",
- 0,
- 0ll,
- -0.0,
- "");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
- EXPECT_EQ(payload_, -543);
-}
-
-extern "C" void pw_tokenizer_HandleEncodedMessageWithPayload(
- pw_tokenizer_Payload payload,
- const uint8_t* encoded_message,
- size_t size_bytes) {
- TokenizeToGlobalHandlerWithPayload::SetMessage(encoded_message, size_bytes);
- TokenizeToGlobalHandlerWithPayload::SetPayload(payload);
-}
-
} // namespace
} // namespace tokenizer
} // namespace pw
diff --git a/pw_tokenizer/size_report/BUILD.bazel b/pw_tokenizer/size_report/BUILD.bazel
new file mode 100644
index 000000000..3d2ae23e0
--- /dev/null
+++ b/pw_tokenizer/size_report/BUILD.bazel
@@ -0,0 +1,67 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_binary",
+ "pw_cc_library",
+)
+
+# Baseline size report library.
+pw_cc_library(
+ name = "base_lib",
+ hdrs = ["base.h"],
+)
+
+# Baseline tokenize string size report binary.
+pw_cc_binary(
+ name = "tokenize_string_base",
+ srcs = ["tokenize_string.cc"],
+ defines = ["BASELINE"],
+ deps = [
+ ":base_lib",
+ "//pw_tokenizer",
+ ],
+)
+
+# Tokenize string size report binary.
+pw_cc_binary(
+ name = "tokenize_string",
+ srcs = ["tokenize_string.cc"],
+ deps = [
+ ":base_lib",
+ "//pw_tokenizer",
+ ],
+)
+
+# Baseline tokenize string expression size report binary.
+pw_cc_binary(
+ name = "tokenize_string_expr_base",
+ srcs = ["tokenize_string_expr.cc"],
+ defines = ["BASELINE"],
+ deps = [
+ ":base_lib",
+ "//pw_tokenizer",
+ ],
+)
+
+# Tokenize string expression size report binary.
+pw_cc_binary(
+ name = "tokenize_string_expr",
+ srcs = ["tokenize_string_expr.cc"],
+ deps = [
+ ":base_lib",
+ "//pw_tokenizer",
+ ],
+)
diff --git a/pw_tokenizer/size_report/BUILD.gn b/pw_tokenizer/size_report/BUILD.gn
new file mode 100644
index 000000000..81489b5b0
--- /dev/null
+++ b/pw_tokenizer/size_report/BUILD.gn
@@ -0,0 +1,61 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+# Baseline size report library.
+pw_source_set("base_lib") {
+ public = [ "base.h" ]
+ public_deps = [ dir_pw_tokenizer ]
+}
+
+# Baseline tokenize string size report executable.
+pw_executable("tokenize_string_base") {
+ sources = [ "tokenize_string.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+ defines = [ "BASELINE" ]
+}
+
+# Tokenize string size report executable.
+pw_executable("tokenize_string") {
+ sources = [ "tokenize_string.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+}
+
+# Baseline tokenize string expression size report executable.
+pw_executable("tokenize_string_expr_base") {
+ sources = [ "tokenize_string_expr.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+ defines = [ "BASELINE" ]
+}
+
+# Tokenize string expression size report executable.
+pw_executable("tokenize_string_expr") {
+ sources = [ "tokenize_string_expr.cc" ]
+ deps = [
+ ":base_lib",
+ "..",
+ ]
+}
diff --git a/pw_tokenizer/size_report/base.h b/pw_tokenizer/size_report/base.h
new file mode 100644
index 000000000..bd7599f36
--- /dev/null
+++ b/pw_tokenizer/size_report/base.h
@@ -0,0 +1,57 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "pw_tokenizer/tokenize.h"
+
+//
+// Baseline data container structure.
+//
+// This structure is a container for data used to establish a baseline for size
+// report diffs.
+//
+// The baseline contains one global instance of this structure. Other binaries
+// should contain one global instance of a subclass of this structure, adding
+// any data that should be a part of the size report diff.
+//
+// This structure may be used to load sections of code and data in order to
+// establish a basline for size report diffs. This structure causes sections to
+// be loaded by referencing symbols within those sections.
+//
+// Simple references to symbols can be optimized out if they don't have any
+// external effects. The LoadData method uses functions and other symbols to
+// generate and return a run-time value. In this way, the symbol references
+// cannot be optimized out.
+//
+struct BaseContainer {
+ // Causes code and data sections to be loaded. Returns a generated value
+ // based on symbols in those sections to force them to be loaded. The caller
+ // should ensure that the return value has some external effect (e.g.,
+ // returning the value from the "main" function).
+ virtual long LoadData() {
+ // Prevent this object and token from being optimized out.
+ return reinterpret_cast<long>(this) ^ base_token;
+ }
+
+ // Destructor.
+ virtual ~BaseContainer() {}
+
+ // Reference the tokenizer.
+ uint32_t base_token = PW_TOKENIZE_STRING("base_token");
+
+ // Explicit padding. If this structure is empty, the baseline will include
+ // padding that won't appear in subclasses, so the padding is explicitly
+ // added here so it appears in both the baseline and all subclasses.
+ char padding[4];
+};
diff --git a/pw_tokenizer/size_report/tokenize_string.cc b/pw_tokenizer/size_report/tokenize_string.cc
new file mode 100644
index 000000000..c397a0660
--- /dev/null
+++ b/pw_tokenizer/size_report/tokenize_string.cc
@@ -0,0 +1,31 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "base.h"
+
+// Data to be included in size report.
+static struct TokenizeStringContainer : BaseContainer {
+#ifdef BASELINE
+ uint32_t token = 1234;
+#else // BASELINE
+ uint32_t token = PW_TOKENIZE_STRING("token");
+#endif // BASELINE
+
+ virtual long LoadData() override { return BaseContainer::LoadData() ^ token; }
+} size_report_data;
+
+int main() {
+ // Load size report data.
+ return size_report_data.LoadData();
+}
diff --git a/pw_tokenizer/size_report/tokenize_string_expr.cc b/pw_tokenizer/size_report/tokenize_string_expr.cc
new file mode 100644
index 000000000..0e94dd9c0
--- /dev/null
+++ b/pw_tokenizer/size_report/tokenize_string_expr.cc
@@ -0,0 +1,31 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "base.h"
+
+// Data to be included in size report.
+static struct TokenizeStringExprContainer : BaseContainer {
+#ifdef BASELINE
+ uint32_t token = 1234;
+#else // BASELINE
+ uint32_t token = PW_TOKENIZE_STRING_EXPR("token");
+#endif // BASELINE
+
+ virtual long LoadData() override { return BaseContainer::LoadData() ^ token; }
+} size_report_data;
+
+int main() {
+ // Load size report data.
+ return size_report_data.LoadData();
+}
diff --git a/pw_tokenizer/token_database_fuzzer.cc b/pw_tokenizer/token_database_fuzzer.cc
index 9391f8702..f73340a66 100644
--- a/pw_tokenizer/token_database_fuzzer.cc
+++ b/pw_tokenizer/token_database_fuzzer.cc
@@ -18,11 +18,11 @@
// operations on this database.
#include <cstring>
-#include <span>
#include "pw_fuzzer/asan_interface.h"
#include "pw_fuzzer/fuzzed_data_provider.h"
#include "pw_preprocessor/util.h"
+#include "pw_span/span.h"
#include "pw_tokenizer/token_database.h"
namespace pw::tokenizer {
@@ -113,12 +113,12 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
ASAN_POISON_MEMORY_REGION(poisoned, poisoned_length);
- // We create a database from a std::span of the buffer since the string
+ // We create a database from a span of the buffer since the string
// entries might not be null terminated, and the creation of a database
// from a raw buffer has an explicit null terminated string requirement
// specified in the API.
- std::span<uint8_t> data_span(buffer, data_size);
- auto token_database = TokenDatabase::Create<std::span<uint8_t>>(data_span);
+ span<uint8_t> data_span(buffer, data_size);
+ auto token_database = TokenDatabase::Create<span<uint8_t>>(data_span);
[[maybe_unused]] volatile auto match = token_database.Find(random_token);
IterateOverDatabase(&token_database);
diff --git a/pw_tokenizer/tokenize.cc b/pw_tokenizer/tokenize.cc
index 4d28010c0..dbe3fd696 100644
--- a/pw_tokenizer/tokenize.cc
+++ b/pw_tokenizer/tokenize.cc
@@ -20,6 +20,7 @@
#include <cstring>
+#include "pw_span/span.h"
#include "pw_tokenizer/encode_args.h"
namespace pw {
@@ -77,28 +78,23 @@ extern "C" void _pw_tokenizer_ToBuffer(void* buffer,
va_list args;
va_start(args, types);
+#if PW_CXX_STANDARD_IS_SUPPORTED(17)
const size_t encoded_bytes = EncodeArgs(
types,
args,
- std::span<std::byte>(static_cast<std::byte*>(buffer) + sizeof(token),
- *buffer_size_bytes - sizeof(token)));
+ span<std::byte>(static_cast<std::byte*>(buffer) + sizeof(token),
+ *buffer_size_bytes - sizeof(token)));
+#else
+ const size_t encoded_bytes =
+ pw_tokenizer_EncodeArgs(types,
+ args,
+ static_cast<std::byte*>(buffer) + sizeof(token),
+ *buffer_size_bytes - sizeof(token));
+#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
va_end(args);
*buffer_size_bytes = sizeof(token) + encoded_bytes;
}
-extern "C" void _pw_tokenizer_ToCallback(
- void (*callback)(const uint8_t* encoded_message, size_t size_bytes),
- Token token,
- pw_tokenizer_ArgTypes types,
- ...) {
- va_list args;
- va_start(args, types);
- EncodedMessage encoded(token, types, args);
- va_end(args);
-
- callback(encoded.data_as_uint8(), encoded.size());
-}
-
} // namespace tokenizer
} // namespace pw
diff --git a/pw_tokenizer/tokenize_test.cc b/pw_tokenizer/tokenize_test.cc
index f302e14f0..8efbba446 100644
--- a/pw_tokenizer/tokenize_test.cc
+++ b/pw_tokenizer/tokenize_test.cc
@@ -52,6 +52,20 @@ TEST(TokenizeString, String_MatchesHash) {
EXPECT_EQ(Hash("[:-)"), token);
}
+TEST(TokenizeString, String_MatchesHashExpr) {
+ EXPECT_EQ(Hash("[:-)"), PW_TOKENIZE_STRING_EXPR("[:-)"));
+}
+
+TEST(TokenizeString, ExpressionWithStringVariable) {
+ constexpr char kTestString[] = "test";
+ EXPECT_EQ(Hash(kTestString), PW_TOKENIZE_STRING_EXPR(kTestString));
+ EXPECT_EQ(Hash(kTestString),
+ PW_TOKENIZE_STRING_DOMAIN_EXPR("TEST_DOMAIN", kTestString));
+ EXPECT_EQ(
+ Hash(kTestString) & 0xAAAAAAAA,
+ PW_TOKENIZE_STRING_MASK_EXPR("TEST_DOMAIN", 0xAAAAAAAA, kTestString));
+}
+
constexpr uint32_t kGlobalToken = PW_TOKENIZE_STRING(">:-[]");
TEST(TokenizeString, GlobalVariable_MatchesHash) {
@@ -68,6 +82,28 @@ TEST(TokenizeString, ClassMember_MatchesHash) {
EXPECT_EQ(Hash("???"), TokenizedWithinClass().kThisToken);
}
+TEST(TokenizeString, WithinNonCapturingLambda) {
+ uint32_t non_capturing_lambda = [] {
+ return PW_TOKENIZE_STRING("Lambda!");
+ }();
+
+ EXPECT_EQ(Hash("Lambda!"), non_capturing_lambda);
+}
+
+TEST(TokenizeString, WithinCapturingLambda) {
+ bool executed_lambda = false;
+ uint32_t capturing_lambda = [&executed_lambda] {
+ if (executed_lambda) {
+ return PW_TOKENIZE_STRING("Never should be returned!");
+ }
+ executed_lambda = true;
+ return PW_TOKENIZE_STRING("Capturing lambda!");
+ }();
+
+ ASSERT_TRUE(executed_lambda);
+ EXPECT_EQ(Hash("Capturing lambda!"), capturing_lambda);
+}
+
TEST(TokenizeString, Mask) {
[[maybe_unused]] constexpr uint32_t token = PW_TOKENIZE_STRING("(O_o)");
[[maybe_unused]] constexpr uint32_t masked_1 =
@@ -84,6 +120,22 @@ TEST(TokenizeString, Mask) {
static_assert((token & 0xFFFF0000) == masked_3);
}
+TEST(TokenizeString, MaskExpr) {
+ uint32_t token = PW_TOKENIZE_STRING("(O_o)");
+ uint32_t masked_1 =
+ PW_TOKENIZE_STRING_MASK_EXPR("domain", 0xAAAAAAAA, "(O_o)");
+ uint32_t masked_2 =
+ PW_TOKENIZE_STRING_MASK_EXPR("domain", 0x55555555, "(O_o)");
+ uint32_t masked_3 =
+ PW_TOKENIZE_STRING_MASK_EXPR("domain", 0xFFFF0000, "(O_o)");
+
+ EXPECT_TRUE(token != masked_1 && token != masked_2 && token != masked_3);
+ EXPECT_TRUE(masked_1 != masked_2 && masked_2 != masked_3);
+ EXPECT_TRUE((token & 0xAAAAAAAA) == masked_1);
+ EXPECT_TRUE((token & 0x55555555) == masked_2);
+ EXPECT_TRUE((token & 0xFFFF0000) == masked_3);
+}
+
// Use a function with a shorter name to test tokenizing __func__ and
// __PRETTY_FUNCTION__.
//
@@ -146,10 +198,28 @@ TEST(TokenizeString, MultipleTokenizationsInOneMacroExpansion) {
THREE_FOR_ONE("hello", "yes", "something");
}
+// Verify that we can tokenize multiple strings from one source line.
+#define THREE_FOR_ONE_EXPR(first, second, third) \
+ [[maybe_unused]] uint32_t token_1 = \
+ PW_TOKENIZE_STRING_DOMAIN_EXPR("TEST_DOMAIN", first); \
+ [[maybe_unused]] uint32_t token_2 = \
+ PW_TOKENIZE_STRING_DOMAIN_EXPR("TEST_DOMAIN", second); \
+ [[maybe_unused]] uint32_t token_3 = \
+ PW_TOKENIZE_STRING_DOMAIN_EXPR("TEST_DOMAIN", third);
+
+TEST(TokenizeString, MultipleTokenizationsInOneMacroExpansionExpr) {
+ // This verifies that we can safely tokenize multiple times in a single macro
+ // expansion. This can be useful when for example a name and description are
+ // both tokenized after being passed into a macro.
+ //
+ // This test only verifies that this compiles correctly; it does not test
+ // that the tokenizations make it to the final token database.
+ THREE_FOR_ONE_EXPR("hello", "yes", "something");
+}
+
class TokenizeToBuffer : public ::testing::Test {
public:
- TokenizeToBuffer() : buffer_ {}
- {}
+ TokenizeToBuffer() : buffer_{} {}
protected:
uint8_t buffer_[64];
@@ -470,91 +540,6 @@ TEST_F(TokenizeToBuffer, AsArgumentToAnotherMacro) {
EXPECT_EQ(std::memcmp(expected.data(), buffer_, expected.size()), 0);
}
-class TokenizeToCallback : public ::testing::Test {
- public:
- static void SetMessage(const uint8_t* message, size_t size) {
- ASSERT_LE(size, sizeof(message_));
- std::memcpy(message_, message, size);
- message_size_bytes_ = size;
- }
-
- protected:
- TokenizeToCallback() {
- std::memset(message_, 0, sizeof(message_));
- message_size_bytes_ = 0;
- }
-
- static uint8_t message_[256];
- static size_t message_size_bytes_;
-};
-
-uint8_t TokenizeToCallback::message_[256] = {};
-size_t TokenizeToCallback::message_size_bytes_ = 0;
-
-TEST_F(TokenizeToCallback, Variety) {
- PW_TOKENIZE_TO_CALLBACK(
- SetMessage, "%s there are %x (%.2f) of them%c", "Now", 2u, 2.0f, '.');
- const auto expected = // clang-format off
- ExpectedData<3, 'N', 'o', 'w', // string "Now"
- 0x04, // unsigned 2 (zig-zag encoded)
- 0x00, 0x00, 0x00, 0x40, // float 2.0
- 0x5C // char '.' (0x2E, zig-zag encoded)
- >("%s there are %x (%.2f) of them%c");
- // clang-format on
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToCallback, Strings) {
- PW_TOKENIZE_TO_CALLBACK(SetMessage, "The answer is: %s", "5432!");
- constexpr std::array<uint8_t, 10> expected =
- ExpectedData<5, '5', '4', '3', '2', '!'>("The answer is: %s");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToCallback, Domain_Strings) {
- PW_TOKENIZE_TO_CALLBACK_DOMAIN(
- "TEST_DOMAIN", SetMessage, "The answer is: %s", "5432!");
- constexpr std::array<uint8_t, 10> expected =
- ExpectedData<5, '5', '4', '3', '2', '!'>("The answer is: %s");
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToCallback, Mask) {
- PW_TOKENIZE_TO_CALLBACK_MASK(
- "TEST_DOMAIN", 0x00000FFF, SetMessage, "The answer is: %s", "5432!");
- constexpr std::array<uint8_t, 10> expected =
- ExpectedData<5, '5', '4', '3', '2', '!'>("The answer is: %s", 0x00000FFF);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToCallback, CharArray) {
- PW_TOKENIZE_TO_CALLBACK(SetMessage, __func__);
- constexpr auto expected = ExpectedData(__func__);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToCallback, C_SequentialZigZag) {
- pw_tokenizer_ToCallbackTest_SequentialZigZag(SetMessage);
-
- constexpr std::array<uint8_t, 18> expected =
- ExpectedData<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13>(
- TEST_FORMAT_SEQUENTIAL_ZIG_ZAG);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
-TEST_F(TokenizeToCallback, AsArgumentToAnotherMacro) {
- MACRO_THAT_CALLS_ANOTHER_MACRO(PW_TOKENIZE_TO_CALLBACK(SetMessage, __func__));
- constexpr auto expected = ExpectedData(__func__);
- ASSERT_EQ(expected.size(), message_size_bytes_);
- EXPECT_EQ(std::memcmp(expected.data(), message_, expected.size()), 0);
-}
-
#undef MACRO_THAT_CALLS_ANOTHER_MACRO
#undef ANOTHER_MACRO
@@ -589,26 +574,5 @@ TEST_F(TokenizeToBuffer, Domain_Specified) {
EXPECT_STREQ(string_literal, "The answer is: %s");
}
-TEST_F(TokenizeToCallback, Domain_Default) {
- const char* tokenizer_domain = nullptr;
- const char* string_literal = nullptr;
-
- PW_TOKENIZE_TO_CALLBACK(SetMessage, "The answer is: %s", "5432!");
-
- EXPECT_STREQ(tokenizer_domain, PW_TOKENIZER_DEFAULT_DOMAIN);
- EXPECT_STREQ(string_literal, "The answer is: %s");
-}
-
-TEST_F(TokenizeToCallback, Domain_Specified) {
- const char* tokenizer_domain = nullptr;
- const char* string_literal = nullptr;
-
- PW_TOKENIZE_TO_CALLBACK_DOMAIN(
- "ThisIsTheDomain", SetMessage, "The answer is: %s", "5432!");
-
- EXPECT_STREQ(tokenizer_domain, "ThisIsTheDomain");
- EXPECT_STREQ(string_literal, "The answer is: %s");
-}
-
} // namespace
} // namespace pw::tokenizer
diff --git a/pw_tokenizer/tokenize_test_c.c b/pw_tokenizer/tokenize_test_c.c
index bf8877a68..485bb5072 100644
--- a/pw_tokenizer/tokenize_test_c.c
+++ b/pw_tokenizer/tokenize_test_c.c
@@ -53,26 +53,6 @@ void pw_tokenizer_ToBufferTest_SequentialZigZag(void* buffer,
(signed char)-7);
}
-void pw_tokenizer_ToCallbackTest_SequentialZigZag(
- void (*callback)(const uint8_t* buffer, size_t size)) {
- PW_TOKENIZE_TO_CALLBACK(callback,
- TEST_FORMAT_SEQUENTIAL_ZIG_ZAG,
- 0u,
- -1,
- 1u,
- (unsigned)-2,
- (unsigned short)2u,
- (signed char)-3,
- 3,
- -4l,
- 4ul,
- -5ll,
- 5ull,
- (signed char)-6,
- (char)6,
- (signed char)-7);
-}
-
void pw_tokenizer_ToBufferTest_Requires8(void* buffer, size_t* buffer_size) {
PW_TOKENIZE_TO_BUFFER(buffer, buffer_size, TEST_FORMAT_REQUIRES_8, "hi", -7);
}
diff --git a/pw_tokenizer/tokenize_to_global_handler.cc b/pw_tokenizer/tokenize_to_global_handler.cc
deleted file mode 100644
index 5ac275529..000000000
--- a/pw_tokenizer/tokenize_to_global_handler.cc
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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 "pw_tokenizer/tokenize_to_global_handler.h"
-
-#include "pw_tokenizer/encode_args.h"
-
-namespace pw {
-namespace tokenizer {
-
-extern "C" void _pw_tokenizer_ToGlobalHandler(pw_tokenizer_Token token,
- pw_tokenizer_ArgTypes types,
- ...) {
- va_list args;
- va_start(args, types);
- EncodedMessage encoded(token, types, args);
- va_end(args);
-
- pw_tokenizer_HandleEncodedMessage(encoded.data_as_uint8(), encoded.size());
-}
-
-} // namespace tokenizer
-} // namespace pw
diff --git a/pw_tokenizer/ts/detokenizer.ts b/pw_tokenizer/ts/detokenizer.ts
new file mode 100644
index 000000000..fe6ea910a
--- /dev/null
+++ b/pw_tokenizer/ts/detokenizer.ts
@@ -0,0 +1,139 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/** Decodes and detokenizes strings from binary or Base64 input. */
+import {Buffer} from 'buffer';
+import {Frame} from 'pigweedjs/pw_hdlc';
+import {TokenDatabase} from './token_database';
+import {PrintfDecoder} from './printf_decoder';
+
+const MAX_RECURSIONS = 9;
+const BASE64CHARS = '[A-Za-z0-9+/-_]';
+const PATTERN = new RegExp(
+ // Base64 tokenized strings start with the prefix character ($)
+ '\\$' +
+ // Tokenized strings contain 0 or more blocks of four Base64 chars.
+ `(?:${BASE64CHARS}{4})*` +
+ // The last block of 4 chars may have one or two padding chars (=).
+ `(?:${BASE64CHARS}{3}=|${BASE64CHARS}{2}==)?`,
+ 'g'
+);
+
+interface TokenAndArgs {
+ token: number;
+ args: Uint8Array;
+}
+
+export class Detokenizer {
+ private database: TokenDatabase;
+
+ constructor(csvDatabase: string) {
+ this.database = new TokenDatabase(csvDatabase);
+ }
+
+ /**
+ * Detokenize frame data into actual string messages using the provided
+ * token database.
+ *
+ * If the frame doesn't match any token from database, the frame will be
+ * returned as string as-is.
+ */
+ detokenize(tokenizedFrame: Frame): string {
+ return this.detokenizeUint8Array(tokenizedFrame.data);
+ }
+
+ /**
+ * Detokenize uint8 into actual string messages using the provided
+ * token database.
+ *
+ * If the data doesn't match any token from database, the data will be
+ * returned as string as-is.
+ */
+ detokenizeUint8Array(data: Uint8Array): string {
+ const {token, args} = this.decodeUint8Array(data);
+ // Parse arguments if this is printf-style text.
+ const format = this.database.get(token);
+ if (format) {
+ return new PrintfDecoder().decode(String(format), args);
+ }
+
+ return new TextDecoder().decode(data);
+ }
+
+ /**
+ * Detokenize Base64-encoded frame data into actual string messages using the
+ * provided token database.
+ *
+ * If the frame doesn't match any token from database, the frame will be
+ * returned as string as-is.
+ */
+ detokenizeBase64(
+ tokenizedFrame: Frame,
+ maxRecursion: number = MAX_RECURSIONS
+ ): string {
+ const base64String = new TextDecoder().decode(tokenizedFrame.data);
+ return this.detokenizeBase64String(base64String, maxRecursion);
+ }
+
+ private detokenizeBase64String(
+ base64String: string,
+ recursions: number
+ ): string {
+ return base64String.replace(PATTERN, base64Substring => {
+ const {token, args} = this.decodeBase64TokenFrame(base64Substring);
+ const format = this.database.get(token);
+ // Parse arguments if this is printf-style text.
+ if (format) {
+ const decodedOriginal = new PrintfDecoder().decode(
+ String(format),
+ args
+ );
+ // Detokenize nested Base64 tokens and their arguments.
+ if (recursions > 0) {
+ return this.detokenizeBase64String(decodedOriginal, recursions - 1);
+ }
+ return decodedOriginal;
+ }
+ return base64Substring;
+ });
+ }
+
+ private decodeUint8Array(data: Uint8Array): TokenAndArgs {
+ const token = new DataView(
+ data.buffer,
+ data.byteOffset,
+ 4
+ ).getUint32(0, true);
+ const args = new Uint8Array(data.buffer.slice(data.byteOffset + 4));
+
+ return {token, args};
+ }
+
+ private decodeBase64TokenFrame(base64Data: string): TokenAndArgs {
+ // Remove the prefix '$' and convert from Base64.
+ const prefixRemoved = base64Data.slice(1);
+ const noBase64 = Buffer.from(prefixRemoved, 'base64').toString('binary');
+ // Convert back to bytes and return token and arguments.
+ const bytes = noBase64.split('').map(ch => ch.charCodeAt(0));
+ const uIntArray = new Uint8Array(bytes);
+ const token = new DataView(
+ uIntArray.buffer,
+ uIntArray.byteOffset,
+ 4
+ ).getUint32(0, true);
+ const args = new Uint8Array(bytes.slice(4));
+
+ return {token, args};
+ }
+}
diff --git a/pw_tokenizer/ts/detokenizer_test.ts b/pw_tokenizer/ts/detokenizer_test.ts
new file mode 100644
index 000000000..1be4de0ae
--- /dev/null
+++ b/pw_tokenizer/ts/detokenizer_test.ts
@@ -0,0 +1,80 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/* eslint-env browser */
+
+import {Frame, Encoder, Decoder} from 'pigweedjs/pw_hdlc';
+import {Detokenizer} from './detokenizer';
+
+const CSV = `
+64636261, ,"regular token"
+86fc33f3, ,"base64 token"
+0d6bd33c, ,"Regular Token: %s and Nested Token: %s"
+97185e6f, ,"(token: %s, string: %s, int: %d, float: %f)"
+451d86ed, ,"Cat"
+`;
+
+function generateFrame(text: string): Frame {
+ const uintArray = new TextEncoder().encode(text);
+ const encodedFrame = new Encoder().uiFrame(1, uintArray);
+ const decodedFrames = Array.from(new Decoder().process(encodedFrame));
+ return decodedFrames[0];
+}
+
+describe('Detokenizer', () => {
+ let detokenizer: Detokenizer;
+
+ beforeEach(() => {
+ detokenizer = new Detokenizer(CSV);
+ });
+
+ it('parses a base64 correct frame properly', () => {
+ const frame = generateFrame('$8zP8hg==');
+ expect(detokenizer.detokenizeBase64(frame)).toEqual('base64 token');
+ });
+ it('parses a correct frame properly', () => {
+ const frame = generateFrame('abcde');
+ expect(detokenizer.detokenize(frame)).toEqual('regular token');
+ });
+ it('failure to detokenize returns original string', () => {
+ expect(detokenizer.detokenize(generateFrame('aabbcc'))).toEqual('aabbcc');
+ expect(detokenizer.detokenizeBase64(generateFrame('$8zP7hg=='))).toEqual(
+ '$8zP7hg=='
+ );
+ });
+ it('recursive detokenize all nested base64 tokens', () => {
+ expect(
+ detokenizer.detokenizeBase64(
+ generateFrame(
+ '$PNNrDQkkN1lZZFJRPT0lJGIxNFlsd2trTjFsWlpGSlJQVDBGUTJGdFpXeFlwSENkUHc9PQ=='
+ )
+ )
+ ).toEqual(
+ 'Regular Token: Cat and Nested Token: (token: Cat, string: Camel, int: 44, float: 1.2300000190734863)'
+ );
+ });
+
+ it('recursion detokenize with limits on max recursion', () => {
+ expect(
+ detokenizer.detokenizeBase64(
+ generateFrame(
+ '$PNNrDQkkN1lZZFJRPT0lJGIxNFlsd2trTjFsWlpGSlJQVDBGUTJGdFpXeFlwSENkUHc9PQ=='
+ ),
+ 1
+ )
+ ).toEqual(
+ 'Regular Token: Cat and Nested Token: (token: $7YYdRQ==, string: Camel, int: 44, float: 1.2300000190734863)'
+ );
+ });
+});
diff --git a/pw_polyfill/public_overrides/cstddef b/pw_tokenizer/ts/index.ts
index cbdb67bdd..1989b0592 100644
--- a/pw_polyfill/public_overrides/cstddef
+++ b/pw_tokenizer/ts/index.ts
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -11,8 +11,6 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-#pragma once
-#include_next <cstddef>
-
-#include "pw_polyfill/standard_library/cstddef.h"
+export {Detokenizer} from './detokenizer';
+export {PrintfDecoder} from './printf_decoder';
diff --git a/pw_tokenizer/ts/int_testdata.ts b/pw_tokenizer/ts/int_testdata.ts
new file mode 100644
index 000000000..36b5a21be
--- /dev/null
+++ b/pw_tokenizer/ts/int_testdata.ts
@@ -0,0 +1,52 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+const IntDB = [
+ ["%d", "-128", "%u", "4294967168", '\xff\x01'],
+ ["%d", "-10", "%u", "4294967286", '\x13'],
+ ["%d", "-9", "%u", "4294967287", '\x11'],
+ ["%d", "-8", "%u", "4294967288", '\x0f'],
+ ["%d", "-7", "%u", "4294967289", '\x0d'],
+ ["%d", "-6", "%u", "4294967290", '\x0b'],
+ ["%d", "-5", "%u", "4294967291", '\x09'],
+ ["%d", "-4", "%u", "4294967292", '\x07'],
+ ["%d", "-3", "%u", "4294967293", '\x05'],
+ ["%d", "-2", "%u", "4294967294", '\x03'],
+ ["%d", "-1", "%u", "4294967295", '\x01'],
+ ["%d", "0", "%u", "0", '\x00'],
+ ["%d", "1", "%u", "1", '\x02'],
+ ["%d", "2", "%u", "2", '\x04'],
+ ["%d", "3", "%u", "3", '\x06'],
+ ["%d", "4", "%u", "4", '\x08'],
+ ["%d", "5", "%u", "5", '\x0a'],
+ ["%d", "6", "%u", "6", '\x0c'],
+ ["%d", "7", "%u", "7", '\x0e'],
+ ["%d", "8", "%u", "8", '\x10'],
+ ["%d", "9", "%u", "9", '\x12'],
+ ["%d", "10", "%u", "10", '\x14'],
+ ["%d", "127", "%u", "127", '\xfe\x01'],
+ ["%d", "-32768", "%u", "4294934528", '\xff\xff\x03'],
+ ["%d", "652344632", "%u", "652344632", '\xf0\xf4\x8f\xee\x04'],
+ ["%d", "18567", "%u", "18567", '\x8e\xa2\x02'],
+ ["%d", "-14", "%u", "4294967282", '\x1b'],
+ ["%d", "-2147483648", "%u", "2147483648", '\xff\xff\xff\xff\x0f'],
+ ["%ld", "-14", "%lu", "4294967282", '\x1b'],
+ ["%d", "2075650855", "%u", "2075650855", '\xce\xac\xbf\xbb\x0f'],
+ ["%lld", "5922204476835468009", "%llu", "5922204476835468009", '\xd2\xcb\x8c\x90\x86\xe6\xf2\xaf\xa4\x01'],
+ ["%lld", "-9223372036854775808", "%llu", "9223372036854775808", '\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01'],
+ ["%lld", "3273441488341945355", "%llu", "3273441488341945355", '\x96\xb0\xae\x9a\x96\xec\xcc\xed\x5a'],
+ ["%lld", "-9223372036854775807", "%llu", "9223372036854775809", '\xfd\xff\xff\xff\xff\xff\xff\xff\xff\x01'],
+]
+
+export default IntDB;
diff --git a/pw_tokenizer/ts/printf_decoder.ts b/pw_tokenizer/ts/printf_decoder.ts
new file mode 100644
index 000000000..7b0203031
--- /dev/null
+++ b/pw_tokenizer/ts/printf_decoder.ts
@@ -0,0 +1,170 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/** Decodes arguments and formats them with the provided format string. */
+import Long from "long";
+
+const SPECIFIER_REGEX = /%(\.([0-9]+))?(hh|h|ll|l|j|z|t|L)?([%csdioxXufFeEaAgGnp])/g;
+// Conversion specifiers by type; n is not supported.
+const SIGNED_INT = 'di'.split('');
+const UNSIGNED_INT = 'oxXup'.split('');
+const FLOATING_POINT = 'fFeEaAgG'.split('');
+
+enum DecodedStatusFlags {
+ // Status flags for a decoded argument. These values should match the
+ // DecodingStatus enum in pw_tokenizer/internal/decode.h.
+ OK = 0, // decoding was successful
+ MISSING = 1, // the argument was not present in the data
+ TRUNCATED = 2, // the argument was truncated during encoding
+ DECODE_ERROR = 4, // an error occurred while decoding the argument
+ SKIPPED = 8, // argument was skipped due to a previous error
+}
+
+interface DecodedArg {
+ size: number;
+ value: string | number | Long | null;
+}
+
+// ZigZag decode function from protobuf's wire_format module.
+function zigzagDecode(value: Long, unsigned: boolean = false): Long {
+ // 64 bit math is:
+ // signmask = (zigzag & 1) ? -1 : 0;
+ // twosComplement = (zigzag >> 1) ^ signmask;
+ //
+ // To work with 32 bit, we can operate on both but "carry" the lowest bit
+ // from the high word by shifting it up 31 bits to be the most significant bit
+ // of the low word.
+ var bitsLow = value.low, bitsHigh = value.high;
+ var signFlipMask = -(bitsLow & 1);
+ bitsLow = ((bitsLow >>> 1) | (bitsHigh << 31)) ^ signFlipMask;
+ bitsHigh = (bitsHigh >>> 1) ^ signFlipMask;
+ return new Long(bitsLow, bitsHigh, unsigned);
+};
+
+export class PrintfDecoder {
+ // Reads a unicode string from the encoded data.
+ private decodeString(args: Uint8Array): DecodedArg {
+ if (args.length === 0) return {size: 0, value: null};
+ let sizeAndStatus = args[0];
+ let status = DecodedStatusFlags.OK;
+
+ if (sizeAndStatus & 0x80) {
+ status |= DecodedStatusFlags.TRUNCATED;
+ sizeAndStatus &= 0x7f;
+ }
+
+ const rawData = args.slice(0, sizeAndStatus + 1);
+ const data = rawData.slice(1);
+ if (data.length < sizeAndStatus) {
+ status |= DecodedStatusFlags.DECODE_ERROR;
+ }
+
+ const decoded = new TextDecoder().decode(data);
+ return {size: rawData.length, value: decoded};
+ }
+
+ private decodeSignedInt(args: Uint8Array): DecodedArg {
+ return this._decodeInt(args);
+ }
+
+ private _decodeInt(args: Uint8Array, unsigned: boolean = false): DecodedArg {
+ if (args.length === 0) return {size: 0, value: null};
+ let count = 0;
+ let result = new Long(0);
+ let shift = 0;
+ for (count = 0; count < args.length; count++) {
+ const byte = args[count];
+ result = result.or((Long.fromInt(byte, unsigned).and(0x7f)).shiftLeft(shift));
+ if (!(byte & 0x80)) {
+ return {value: zigzagDecode(result, unsigned), size: count + 1};
+ }
+ shift += 7;
+ if (shift >= 64) break;
+ }
+
+ return {size: 0, value: null};
+ }
+
+ private decodeUnsignedInt(args: Uint8Array, lengthSpecifier: string): DecodedArg {
+ const arg = this._decodeInt(args, true);
+ const bits = ['ll', 'j'].indexOf(lengthSpecifier) !== -1 ? 64 : 32;
+
+ // Since ZigZag encoding is used, unsigned integers must be masked off to
+ // their original bit length.
+ if (arg.value !== null) {
+ let num = arg.value as Long;
+ if (bits === 32) {
+ num = num.and((Long.fromInt(1).shiftLeft(bits)).add(-1));
+ }
+ else {
+ num = num.and(-1);
+ }
+ arg.value = num.toString();
+ }
+ return arg;
+ }
+
+ private decodeChar(args: Uint8Array): DecodedArg {
+ const arg = this.decodeSignedInt(args);
+
+ if (arg.value !== null) {
+ const num = arg.value as Long;
+ arg.value = String.fromCharCode(num.toInt());
+ }
+ return arg;
+ }
+
+ private decodeFloat(args: Uint8Array, precision: string): DecodedArg {
+ if (args.length < 4) return {size: 0, value: ''};
+ const floatValue = new DataView(args.buffer, args.byteOffset, 4).getFloat32(
+ 0,
+ true
+ );
+ if (precision) return {size: 4, value: floatValue.toFixed(parseInt(precision))}
+ return {size: 4, value: floatValue};
+ }
+
+ private format(specifierType: string, args: Uint8Array, precision: string, lengthSpecifier: string): DecodedArg {
+ if (specifierType == '%') return {size: 0, value: '%'}; // literal %
+ if (specifierType === 's') {
+ return this.decodeString(args);
+ }
+ if (specifierType === 'c') {
+ return this.decodeChar(args);
+ }
+ if (SIGNED_INT.indexOf(specifierType) !== -1) {
+ return this.decodeSignedInt(args);
+ }
+ if (UNSIGNED_INT.indexOf(specifierType) !== -1) {
+ return this.decodeUnsignedInt(args, lengthSpecifier);
+ }
+ if (FLOATING_POINT.indexOf(specifierType) !== -1) {
+ return this.decodeFloat(args, precision);
+ }
+
+ // Unsupported specifier, return as-is
+ return {size: 0, value: '%' + specifierType};
+ }
+
+ decode(formatString: string, args: Uint8Array): string {
+ return formatString.replace(
+ SPECIFIER_REGEX,
+ (_specifier, _precisionFull, precision, lengthSpecifier, specifierType) => {
+ const decodedArg = this.format(specifierType, args, precision, lengthSpecifier);
+ args = args.slice(decodedArg.size);
+ if (decodedArg === null) return '';
+ return String(decodedArg.value);
+ });
+ }
+}
diff --git a/pw_tokenizer/ts/printf_decoder_test.ts b/pw_tokenizer/ts/printf_decoder_test.ts
new file mode 100644
index 000000000..db28e488a
--- /dev/null
+++ b/pw_tokenizer/ts/printf_decoder_test.ts
@@ -0,0 +1,122 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/* eslint-env browser */
+import {PrintfDecoder} from './printf_decoder';
+import IntDB from './int_testdata';
+
+function argFromString(arg: string): Uint8Array {
+ const data = new TextEncoder().encode(arg);
+ return new Uint8Array([data.length, ...data]);
+}
+
+function argFromStringBinary(arg: string): Uint8Array {
+ return new Uint8Array(arg.split('').map(ch => ch.charCodeAt(0)));
+}
+
+function argsConcat(...args: Uint8Array[]): Uint8Array {
+ let data: number[] = [];
+ for (const index in args) {
+ const argData = args[index];
+ data = data.concat([...argData]);
+ }
+ return new Uint8Array(data);
+}
+
+describe('PrintfDecoder', () => {
+ let printfDecoder: PrintfDecoder;
+
+ beforeEach(() => {
+ printfDecoder = new PrintfDecoder();
+ });
+
+ it('formats string correctly', () => {
+ expect(printfDecoder.decode('Hello %s', argFromString('Computer'))).toEqual(
+ 'Hello Computer'
+ );
+ expect(
+ printfDecoder.decode(
+ 'Hello %s and %s',
+ argsConcat(argFromString('Mac'), argFromString('PC'))
+ )
+ ).toEqual('Hello Mac and PC');
+ });
+
+ it('formats string + number correctly', () => {
+ expect(
+ printfDecoder.decode(
+ 'Hello %s and %u',
+ argsConcat(argFromString('Computer'), argFromStringBinary('\xff\xff\x03'))
+ )).toEqual(
+ 'Hello Computer and 4294934528');
+ });
+
+ it('formats integers correctly', () => {
+ for (let index = 0; index < IntDB.length; index++) {
+ const testcase = IntDB[index];
+ // Test signed
+ expect(
+ printfDecoder
+ .decode(testcase[0], argFromStringBinary(testcase[4])))
+ .toEqual(testcase[1]);
+
+ // Test unsigned
+ expect(
+ printfDecoder
+ .decode(testcase[2], argFromStringBinary(testcase[4])))
+ .toEqual(testcase[3]);
+ }
+ });
+
+ it('formats string correctly', () => {
+ expect(
+ printfDecoder.decode(
+ 'Hello %s and %s',
+ argsConcat(argFromString('Mac'), argFromString('PC'))
+ )
+ ).toEqual('Hello Mac and PC');
+ });
+
+ it('formats varint correctly', () => {
+ const arg = argFromStringBinary('\xff\xff\x03');
+ expect(printfDecoder.decode('Number %d', arg)).toEqual('Number -32768');
+ expect(
+ printfDecoder.decode('Numbers %u and %d', argsConcat(arg, arg))
+ ).toEqual('Numbers 4294934528 and -32768');
+ expect(printfDecoder.decode('Growth is %u%', arg)).toEqual(
+ 'Growth is 4294934528%'
+ );
+ });
+
+ it('formats char correctly', () => {
+ expect(
+ printfDecoder.decode('Battery: 100%c', argFromStringBinary('\x4a'))
+ ).toEqual('Battery: 100%');
+ expect(
+ printfDecoder.decode('Price: %c97.99', argFromStringBinary('\x48'))
+ ).toEqual('Price: $97.99');
+ });
+
+ it('formats floats correctly', () => {
+ expect(
+ printfDecoder.decode('Value: %f', argFromStringBinary('\xdb\x0f\x49\x40'))
+ ).toEqual('Value: 3.1415927410125732');
+ expect(
+ printfDecoder.decode(
+ 'Value: %.5f',
+ argFromStringBinary('\xdb\x0f\x49\x40')
+ )
+ ).toEqual('Value: 3.14159');
+ });
+});
diff --git a/pw_tokenizer/ts/token_database.ts b/pw_tokenizer/ts/token_database.ts
new file mode 100644
index 000000000..bc5bcf56f
--- /dev/null
+++ b/pw_tokenizer/ts/token_database.ts
@@ -0,0 +1,57 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/** Parses CSV Database for easier lookups */
+
+export class TokenDatabase {
+ private tokens: Map<number, string> = new Map();
+
+ constructor(readonly csv: string) {
+ this.parseTokensToTokensMap(csv.split(/\r?\n/));
+ }
+
+ has(token: number): boolean {
+ return this.tokens.has(token);
+ }
+
+ get(token: number): string | undefined {
+ return this.tokens.get(token);
+ }
+
+ private parseTokensToTokensMap(csv: string[]) {
+ for (const [lineNumber, line] of Object.entries(
+ csv.map(line => line.split(/,/))
+ )) {
+ if (!line[0] || !line[2]) {
+ continue;
+ }
+ if (!/^[a-fA-F0-9]+$/.test(line[0])) {
+ // Malformed number
+ console.error(
+ new Error(
+ `TokenDatabase number ${line[0]} at line ` +
+ `${lineNumber} is not a valid hex number`
+ )
+ );
+ continue;
+ }
+ const tokenNumber = parseInt(line[0], 16);
+ // To extract actual string value of a token number, we:
+ // - Slice token number and whitespace that are in [0] and [1] of line.
+ // - Join the rest as a string and trim the trailing quotes.
+ const data = line.slice(2).join(',').slice(1, -1);
+ this.tokens.set(tokenNumber, data);
+ }
+ }
+}
diff --git a/pw_tool/BUILD.gn b/pw_tool/BUILD.gn
index 522ae3a83..04df09bc5 100644
--- a/pw_tool/BUILD.gn
+++ b/pw_tool/BUILD.gn
@@ -14,12 +14,22 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_executable("pw_tool") {
output_name = "pw_tool"
deps = [
"$dir_pw_log",
"$dir_pw_polyfill",
+ dir_pw_span,
]
sources = [ "main.cc" ]
}
+
+pw_test_group("tests") {
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/pw_tool/docs.rst b/pw_tool/docs.rst
new file mode 100644
index 000000000..0bc0b3429
--- /dev/null
+++ b/pw_tool/docs.rst
@@ -0,0 +1,9 @@
+.. _module-pw_tool:
+
+=======
+pw_tool
+=======
+
+.. warning::
+
+ This documentation is under construction.
diff --git a/pw_tool/main.cc b/pw_tool/main.cc
index b716276c6..181f85989 100644
--- a/pw_tool/main.cc
+++ b/pw_tool/main.cc
@@ -16,13 +16,13 @@
#include <cctype>
#include <functional>
#include <iostream>
-#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "pw_log/log.h"
+#include "pw_span/span.h"
namespace {
@@ -106,11 +106,11 @@ struct CommandContext {
// Commands are given mutable CommandContext and a span tokens in the line of
// the command.
using Command =
- std::function<bool(CommandContext*, std::span<std::string_view>)>;
+ std::function<bool(CommandContext*, pw::span<std::string_view>)>;
// Echoes all arguments provided to cout.
bool CommandEcho(CommandContext* /*context*/,
- std::span<std::string_view> tokens) {
+ pw::span<std::string_view> tokens) {
bool first = true;
for (const auto& token : tokens.subspan(1)) {
if (!first) {
@@ -127,7 +127,7 @@ bool CommandEcho(CommandContext* /*context*/,
// Quit the CLI.
bool CommandQuit(CommandContext* context,
- std::span<std::string_view> /*tokens*/) {
+ pw::span<std::string_view> /*tokens*/) {
context->quit = true;
return true;
}
diff --git a/pw_toolchain/Android.bp b/pw_toolchain/Android.bp
new file mode 100644
index 000000000..4ba2e5888
--- /dev/null
+++ b/pw_toolchain/Android.bp
@@ -0,0 +1,27 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "pw_toolchain",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: [
+ "public",
+ ],
+ host_supported: true,
+} \ No newline at end of file
diff --git a/pw_toolchain/BUILD.bazel b/pw_toolchain/BUILD.bazel
index 7bb181081..5f160c1f7 100644
--- a/pw_toolchain/BUILD.bazel
+++ b/pw_toolchain/BUILD.bazel
@@ -1,4 +1,4 @@
-# Copyright 2019 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -12,6 +12,54 @@
# License for the specific language governing permissions and limitations under
# the License.
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+ "pw_cc_test",
+)
+load(":rust_toolchain.bzl", "pw_rust_toolchain")
+
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+
+pw_cc_library(
+ name = "no_destructor",
+ hdrs = ["public/pw_toolchain/no_destructor.h"],
+ includes = ["public"],
+)
+
+pw_cc_test(
+ name = "no_destructor_test",
+ srcs = ["no_destructor_test.cc"],
+ deps = [
+ ":no_destructor",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_library(
+ name = "wrap_abort",
+ srcs = ["wrap_abort.cc"],
+ linkopts = ["-Wl,--wrap=abort"],
+ deps = ["//pw_assert"],
+)
+
+# Define rust toolchains that are compatable with @bazel_embedded.
+pw_rust_toolchain(
+ name = "thumbv7m_rust_linux_x86_64",
+ exec_cpu = "x86_64",
+ exec_os = "linux",
+ exec_triple = "x86_64-unknown-linux-gnu",
+ rust_target_triple = "thumbv7m-none-eabi",
+ target_cpu = "armv7-m",
+)
+
+pw_rust_toolchain(
+ name = "thumbv6m_rust_linux_x86_64",
+ exec_cpu = "x86_64",
+ exec_os = "linux",
+ exec_triple = "x86_64-unknown-linux-gnu",
+ rust_target_triple = "thumbv6m-none-eabi",
+ target_cpu = "armv6-m",
+)
diff --git a/pw_toolchain/BUILD.gn b/pw_toolchain/BUILD.gn
index 0ac53a821..a62608714 100644
--- a/pw_toolchain/BUILD.gn
+++ b/pw_toolchain/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
import("arm_gcc/toolchains.gni")
import("generate_toolchain.gni")
import("host_clang/toolchains.gni")
@@ -40,6 +41,38 @@ generate_toolchains("host_clang_suite") {
toolchains = pw_toolchain_host_clang_list
}
+config("public_include_path") {
+ include_dirs = [ "public" ]
+}
+
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_source_set("no_destructor") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_toolchain/no_destructor.h" ]
+}
+
+config("wrap_abort_config") {
+ ldflags = [ "-Wl,--wrap=abort" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("wrap_abort") {
+ all_dependent_configs = [ ":wrap_abort_config" ]
+ sources = [ "wrap_abort.cc" ]
+ deps = [ dir_pw_assert ]
+}
+
+pw_test_group("tests") {
+ tests = [ ":no_destructor_test" ]
+}
+
+pw_test("no_destructor_test") {
+ sources = [ "no_destructor_test.cc" ]
+ deps = [
+ ":no_destructor",
+ dir_pw_assert,
+ ]
+}
diff --git a/pw_toolchain/CMakeLists.txt b/pw_toolchain/CMakeLists.txt
new file mode 100644
index 000000000..6570d8687
--- /dev/null
+++ b/pw_toolchain/CMakeLists.txt
@@ -0,0 +1,43 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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("$ENV{PW_ROOT}/pw_build/pigweed.cmake")
+
+add_subdirectory(arm_gcc EXCLUDE_FROM_ALL)
+
+pw_add_library(pw_toolchain.no_destructor INTERFACE
+ HEADERS
+ public/pw_toolchain/no_destructor.h
+ PUBLIC_INCLUDES
+ public
+)
+
+pw_add_library(pw_toolchain.wrap_abort STATIC
+ SOURCES
+ wrap_abort.cc
+ PUBLIC_DEPS
+ pw_assert
+ PUBLIC_LINK_OPTIONS
+ "-Wl,--wrap=abort"
+)
+
+pw_add_test(pw_toolchain.no_destructor_test
+ SOURCES
+ no_destructor_test.cc
+ PRIVATE_DEPS
+ pw_toolchain.no_destructor
+ GROUPS
+ modules
+ pw_toolchain
+)
diff --git a/pw_toolchain/arm_clang/BUILD.gn b/pw_toolchain/arm_clang/BUILD.gn
index 414b8569b..966014284 100644
--- a/pw_toolchain/arm_clang/BUILD.gn
+++ b/pw_toolchain/arm_clang/BUILD.gn
@@ -23,15 +23,29 @@ cortex_m_common_flags = [
cortex_m_software_fpu_flags = [ "-mfloat-abi=soft" ]
-cortex_m_hardware_fpu_flags = [
+cortex_m_hardware_fpu_flags_common = [
"-mfloat-abi=hard",
- "-mfpu=fpv4-sp-d16",
# Used by some pigweed tests/targets to correctly handle hardware FPU
# behavior.
"-DPW_ARMV7M_ENABLE_FPU=1",
]
+cortex_m_hardware_fpu_flags =
+ cortex_m_hardware_fpu_flags_common + [ "-mfpu=fpv4-sp-d16" ]
+
+cortex_m_hardware_fpu_v5_flags =
+ cortex_m_hardware_fpu_flags_common + [ "-mfpu=fpv5-d16" ]
+
+cortex_m_hardware_fpu_v5_sp_flags =
+ cortex_m_hardware_fpu_flags_common + [ "-mfpu=fpv5-sp-d16" ]
+
+# Default config added to all the ARM cortex M targets to link `nosys` library.
+config("nosys") {
+ # TODO(prabhukr): libs = ["nosys"] did not work as expected (pwrev/133110).
+ ldflags = [ "-lnosys" ]
+}
+
config("enable_float_printf") {
ldflags = [ "-Wl,-u_printf_float" ]
}
@@ -52,6 +66,14 @@ pw_clang_arm_config("cortex_m3") {
ldflags = cflags
}
+pw_clang_arm_config("cortex_m4") {
+ cflags = [ "-mcpu=cortex-m4" ]
+ cflags += cortex_m_common_flags
+ cflags += cortex_m_software_fpu_flags
+ asmflags = cflags
+ ldflags = cflags
+}
+
pw_clang_arm_config("cortex_m4f") {
cflags = [ "-mcpu=cortex-m4" ]
cflags += cortex_m_common_flags
@@ -59,3 +81,35 @@ pw_clang_arm_config("cortex_m4f") {
asmflags = cflags
ldflags = cflags
}
+
+pw_clang_arm_config("cortex_m7") {
+ cflags = [ "-mcpu=cortex-m7" ]
+ cflags += cortex_m_common_flags
+ cflags += cortex_m_software_fpu_flags
+ asmflags = cflags
+ ldflags = cflags
+}
+
+pw_clang_arm_config("cortex_m7f") {
+ cflags = [ "-mcpu=cortex-m7" ]
+ cflags += cortex_m_common_flags
+ cflags += cortex_m_hardware_fpu_v5_flags
+ asmflags = cflags
+ ldflags = cflags
+}
+
+pw_clang_arm_config("cortex_m33") {
+ cflags = [ "-mcpu=cortex-m33" ]
+ cflags += cortex_m_common_flags
+ cflags += cortex_m_software_fpu_flags
+ asmflags = cflags
+ ldflags = cflags
+}
+
+pw_clang_arm_config("cortex_m33f") {
+ cflags = [ "-mcpu=cortex-m33" ]
+ cflags += cortex_m_common_flags
+ cflags += cortex_m_hardware_fpu_v5_sp_flags
+ asmflags = cflags
+ ldflags = cflags
+}
diff --git a/pw_toolchain/arm_clang/toolchains.gni b/pw_toolchain/arm_clang/toolchains.gni
index 2171e6736..402082159 100644
--- a/pw_toolchain/arm_clang/toolchains.gni
+++ b/pw_toolchain/arm_clang/toolchains.gni
@@ -23,17 +23,51 @@ _arm_clang_toolchain = {
link_whole_archive = true
# Enable static analysis for arm clang based toolchains.
- static_analysis = true
+ static_analysis = {
+ enabled = true
+ }
}
# Configs specific to different architectures.
-_cortex_m0plus = [ "$dir_pw_toolchain/arm_clang:cortex_m0plus" ]
+_cortex_m0plus = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m0plus",
+]
+
+_cortex_m3 = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m3",
+]
+
+_cortex_m4 = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m4",
+]
+
+_cortex_m4f = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m4f",
+]
-_cortex_m3 = [ "$dir_pw_toolchain/arm_clang:cortex_m3" ]
+_cortex_m7 = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m7",
+]
+
+_cortex_m7f = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m7f",
+]
-_cortex_m4 = [ "$dir_pw_toolchain/arm_clang:cortex_m4" ]
+_cortex_m33 = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m33",
+]
-_cortex_m4f = [ "$dir_pw_toolchain/arm_clang:cortex_m4f" ]
+_cortex_m33f = [
+ "$dir_pw_toolchain/arm_clang:nosys",
+ "$dir_pw_toolchain/arm_clang:cortex_m33f",
+]
# Describes ARM clang toolchains for specific targets.
pw_toolchain_arm_clang = {
@@ -55,7 +89,7 @@ pw_toolchain_arm_clang = {
name = "arm_clang_cortex_m0plus_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m0plus + [ "$dir_pw_build:optimize_size" ]
+ default_configs = _cortex_m0plus + [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m3_debug = {
@@ -76,7 +110,7 @@ pw_toolchain_arm_clang = {
name = "arm_clang_cortex_m3_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m3 + [ "$dir_pw_build:optimize_size" ]
+ default_configs = _cortex_m3 + [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m4_debug = {
@@ -97,7 +131,7 @@ pw_toolchain_arm_clang = {
name = "arm_clang_cortex_m4_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m4 + [ "$dir_pw_build:optimize_size" ]
+ default_configs = _cortex_m4 + [ "$dir_pw_build:optimize_size_clang" ]
}
}
cortex_m4f_debug = {
@@ -118,7 +152,91 @@ pw_toolchain_arm_clang = {
name = "arm_clang_cortex_m4f_size_optimized"
forward_variables_from(_arm_clang_toolchain, "*")
defaults = {
- default_configs = _cortex_m4f + [ "$dir_pw_build:optimize_size" ]
+ default_configs = _cortex_m4f + [ "$dir_pw_build:optimize_size_clang" ]
+ }
+ }
+ cortex_m7_debug = {
+ name = "arm_clang_cortex_m7_debug"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m7 + [ "$dir_pw_build:optimize_debugging" ]
+ }
+ }
+ cortex_m7_speed_optimized = {
+ name = "arm_clang_cortex_m7_speed_optimized"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m7 + [ "$dir_pw_build:optimize_speed" ]
+ }
+ }
+ cortex_m7_size_optimized = {
+ name = "arm_clang_cortex_m7_size_optimized"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m7 + [ "$dir_pw_build:optimize_size_clang" ]
+ }
+ }
+ cortex_m7f_debug = {
+ name = "arm_clang_cortex_m7f_debug"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m7f + [ "$dir_pw_build:optimize_debugging" ]
+ }
+ }
+ cortex_m7f_speed_optimized = {
+ name = "arm_clang_cortex_m7f_speed_optimized"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m7f + [ "$dir_pw_build:optimize_speed" ]
+ }
+ }
+ cortex_m7f_size_optimized = {
+ name = "arm_clang_cortex_m7f_size_optimized"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m7f + [ "$dir_pw_build:optimize_size_clang" ]
+ }
+ }
+ cortex_m33_debug = {
+ name = "arm_clang_cortex_m33_debug"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m33 + [ "$dir_pw_build:optimize_debugging" ]
+ }
+ }
+ cortex_m33_speed_optimized = {
+ name = "arm_clang_cortex_m33_speed_optimized"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m33 + [ "$dir_pw_build:optimize_speed" ]
+ }
+ }
+ cortex_m33_size_optimized = {
+ name = "arm_clang_cortex_m33_size_optimized"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m33 + [ "$dir_pw_build:optimize_size_clang" ]
+ }
+ }
+ cortex_m33f_debug = {
+ name = "arm_clang_cortex_m33f_debug"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m33f + [ "$dir_pw_build:optimize_debugging" ]
+ }
+ }
+ cortex_m33f_speed_optimized = {
+ name = "arm_clang_cortex_m33f_speed_optimized"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m33f + [ "$dir_pw_build:optimize_speed" ]
+ }
+ }
+ cortex_m33f_size_optimized = {
+ name = "arm_clang_cortex_m33f_size_optimized"
+ forward_variables_from(_arm_clang_toolchain, "*")
+ defaults = {
+ default_configs = _cortex_m33f + [ "$dir_pw_build:optimize_size_clang" ]
}
}
}
@@ -139,4 +257,16 @@ pw_toolchain_arm_clang_list = [
pw_toolchain_arm_clang.cortex_m4f_debug,
pw_toolchain_arm_clang.cortex_m4f_speed_optimized,
pw_toolchain_arm_clang.cortex_m4f_size_optimized,
+ pw_toolchain_arm_clang.cortex_m7_debug,
+ pw_toolchain_arm_clang.cortex_m7_speed_optimized,
+ pw_toolchain_arm_clang.cortex_m7_size_optimized,
+ pw_toolchain_arm_clang.cortex_m7f_debug,
+ pw_toolchain_arm_clang.cortex_m7f_speed_optimized,
+ pw_toolchain_arm_clang.cortex_m7f_size_optimized,
+ pw_toolchain_arm_clang.cortex_m33_debug,
+ pw_toolchain_arm_clang.cortex_m33_speed_optimized,
+ pw_toolchain_arm_clang.cortex_m33_size_optimized,
+ pw_toolchain_arm_clang.cortex_m33f_debug,
+ pw_toolchain_arm_clang.cortex_m33f_speed_optimized,
+ pw_toolchain_arm_clang.cortex_m33f_size_optimized,
]
diff --git a/pw_toolchain/arm_gcc/BUILD.bazel b/pw_toolchain/arm_gcc/BUILD.bazel
new file mode 100644
index 000000000..5e3475134
--- /dev/null
+++ b/pw_toolchain/arm_gcc/BUILD.bazel
@@ -0,0 +1,42 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "newlib_os_interface_stubs",
+ srcs = ["newlib_os_interface_stubs.cc"],
+ linkopts = [
+ "-Wl,--wrap=__sread",
+ "-Wl,--wrap=__swrite",
+ "-Wl,--wrap=__sseek",
+ "-Wl,--wrap=__sclose",
+ ],
+ visibility = ["//visibility:private"],
+ deps = ["//pw_assert"],
+)
+
+pw_cc_library(
+ name = "arm_none_eabi_gcc_support",
+ visibility = ["//visibility:public"],
+ deps = [
+ ":newlib_os_interface_stubs",
+ "//pw_toolchain:wrap_abort",
+ ],
+)
diff --git a/pw_toolchain/arm_gcc/BUILD.gn b/pw_toolchain/arm_gcc/BUILD.gn
index a5f802323..436c4de83 100644
--- a/pw_toolchain/arm_gcc/BUILD.gn
+++ b/pw_toolchain/arm_gcc/BUILD.gn
@@ -13,6 +13,9 @@
# the License.
import("//build_overrides/pigweed.gni")
+import("//build_overrides/pigweed_environment.gni")
+
+import("$dir_pw_build/target_types.gni")
# Disable obnoxious ABI warning.
#
@@ -37,6 +40,7 @@ config("cortex_common") {
"-mthumb",
]
cflags = asmflags + [
+ "--sysroot=" + rebase_path(pw_env_setup_CIPD_ARM, root_build_dir),
"-specs=nano.specs",
"-specs=nosys.specs",
]
@@ -116,3 +120,29 @@ config("cortex_hardware_fpu_v5_sp") {
defines = [ "PW_ARMV7M_ENABLE_FPU=1" ]
ldflags = cflags
}
+
+config("wrap_newlib_stdio_functions") {
+ ldflags = [
+ "-Wl,--wrap=__sread",
+ "-Wl,--wrap=__swrite",
+ "-Wl,--wrap=__sseek",
+ "-Wl,--wrap=__sclose",
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("newlib_os_interface_stubs") {
+ all_dependent_configs = [ ":wrap_newlib_stdio_functions" ]
+ sources = [ "newlib_os_interface_stubs.cc" ]
+ deps = [ dir_pw_assert ]
+ visibility = [ ":*" ]
+}
+
+# Basic libraries any arm-none-eabi-gcc target should use. This library should
+# be included in pw_build_LINK_DEPS.
+group("arm_none_eabi_gcc_support") {
+ deps = [
+ ":newlib_os_interface_stubs",
+ "$dir_pw_toolchain:wrap_abort",
+ ]
+}
diff --git a/pw_toolchain/arm_gcc/CMakeLists.txt b/pw_toolchain/arm_gcc/CMakeLists.txt
new file mode 100644
index 000000000..af5612894
--- /dev/null
+++ b/pw_toolchain/arm_gcc/CMakeLists.txt
@@ -0,0 +1,33 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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("$ENV{PW_ROOT}/pw_build/pigweed.cmake")
+
+pw_add_library_generic(pw_toolchain.arm_gcc.newlib_os_interface_stubs OBJECT
+ SOURCES
+ newlib_os_interface_stubs.cc
+ PUBLIC_DEPS
+ pw_assert
+ PUBLIC_LINK_OPTIONS
+ "-Wl,--wrap=__sread"
+ "-Wl,--wrap=__swrite"
+ "-Wl,--wrap=__sseek"
+ "-Wl,--wrap=__sclose"
+)
+
+pw_add_library_generic(pw_toolchain.arm_gcc.arm_none_eabi_gcc_support INTERFACE
+ PUBLIC_DEPS
+ pw_toolchain.arm_gcc.newlib_os_interface_stubs
+ pw_toolchain.wrap_abort
+)
diff --git a/pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc b/pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc
new file mode 100644
index 000000000..03184e417
--- /dev/null
+++ b/pw_toolchain/arm_gcc/newlib_os_interface_stubs.cc
@@ -0,0 +1,103 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <sys/stat.h> // POSIX header provided by Newlib; needed for stat.
+#include <sys/times.h> // POSIX header provided by Newlib; needed for times.
+
+#include <cstdio> // fpos_t
+
+#include "pw_assert/check.h"
+
+namespace pw::toolchain {
+namespace {
+
+[[noreturn]] void AbortIfUnsupportedNewlibFunctionIsCalled() {
+ PW_CRASH(
+ "Attempted to invoke an unsupported Newlib function! The stdout and "
+ "stderr FILE objects are not supported.");
+}
+
+} // namespace
+
+// Wrap the stdio read, write, seek, and close Newlib functions defined in
+// libc/stdio/stdio.c. These should never be called, so abort if they are.
+//
+// These functions are unconditionally linked, even if they're never called,
+// because they are assigned as members of the stdout/stderr FILE struct. The
+// Newlib implementations invoke some of the unsupported OS interface functions.
+#define PW_WRAP_NEWLIB_FILE_FUNCTION(function, ...) \
+ extern "C" int __wrap_##function(__VA_ARGS__) { \
+ AbortIfUnsupportedNewlibFunctionIsCalled(); \
+ }
+
+PW_WRAP_NEWLIB_FILE_FUNCTION(__sread, void*, char*, int)
+PW_WRAP_NEWLIB_FILE_FUNCTION(__swrite, void*, char*, int)
+PW_WRAP_NEWLIB_FILE_FUNCTION(__sseek, void*, fpos_t, int)
+PW_WRAP_NEWLIB_FILE_FUNCTION(__sclose, void*)
+
+#undef PW_WRAP_NEWLIB_FILE_FUNCTION
+
+// Newlib defines a set of OS interface functions. Most of these should never be
+// called, since they're used by libc functions not supported in Pigweed(e.g.
+// fopen or printf). If they're linked into a binary, that indicates that an
+// unsupported function was called.
+//
+// Newlib provides default, nop implementations of these functions. Starting
+// with arm-none-eabi-gcc 11.3, a warning is issued when any of these defaults
+// are used.
+//
+// Provide implementations for most of the Newlib OS interface functions, which
+// are documented at https://sourceware.org/newlib/libc.html#Stubs. The default
+// implementation calls the following function, which is never defined,
+// resulting in a linker error.
+[[noreturn]] void AttempedToInvokeUnsupportedNewlibOsInterfaceFunction();
+
+#define PW_DISABLE_NEWLIB_FUNCTION(function, ...) \
+ extern "C" int _##function(__VA_ARGS__) { \
+ AttempedToInvokeUnsupportedNewlibOsInterfaceFunction(); \
+ }
+
+PW_DISABLE_NEWLIB_FUNCTION(_exit, void)
+PW_DISABLE_NEWLIB_FUNCTION(close, int)
+PW_DISABLE_NEWLIB_FUNCTION(execve, char*, char**, char**)
+PW_DISABLE_NEWLIB_FUNCTION(fork, void)
+
+// Provide the minimal fstat implementation recommended by the Newlib
+// documentation since fstat is called indirectly by snprintf.
+extern "C" int _fstat(int, struct stat* st) {
+ st->st_mode = S_IFCHR;
+ return 0;
+}
+
+PW_DISABLE_NEWLIB_FUNCTION(getpid, void)
+
+// Provide the minimal isatty implementation recommended by the Newlib
+// documentation since isatty is called indirectly by snprintf.
+extern "C" int _isatty(int) { return 1; }
+
+PW_DISABLE_NEWLIB_FUNCTION(kill, int, int)
+PW_DISABLE_NEWLIB_FUNCTION(link, char*, char*)
+PW_DISABLE_NEWLIB_FUNCTION(lseek, int, int, int)
+PW_DISABLE_NEWLIB_FUNCTION(open, const char*, int, int)
+PW_DISABLE_NEWLIB_FUNCTION(read, int, char*, int)
+PW_DISABLE_NEWLIB_FUNCTION(sbrk, int)
+PW_DISABLE_NEWLIB_FUNCTION(stat, char*, struct stat*)
+PW_DISABLE_NEWLIB_FUNCTION(times, struct tms*)
+PW_DISABLE_NEWLIB_FUNCTION(unlink, char*)
+PW_DISABLE_NEWLIB_FUNCTION(wait, int*)
+PW_DISABLE_NEWLIB_FUNCTION(write, int, char*, int)
+
+#undef PW_DISABLE_NEWLIB_FUNCTION
+
+} // namespace pw::toolchain
diff --git a/pw_toolchain/arm_gcc/toolchains.gni b/pw_toolchain/arm_gcc/toolchains.gni
index 659507942..c7a138564 100644
--- a/pw_toolchain/arm_gcc/toolchains.gni
+++ b/pw_toolchain/arm_gcc/toolchains.gni
@@ -13,13 +13,35 @@
# the License.
import("//build_overrides/pigweed.gni")
+import("//build_overrides/pigweed_environment.gni")
+import("$dir_pw_toolchain/rbe.gni")
# Specifies the tools used by ARM GCC toolchains.
arm_gcc_toolchain_tools = {
- _tool_name_root = "arm-none-eabi-"
- ar = _tool_name_root + "ar"
- cc = _tool_name_root + "gcc"
- cxx = _tool_name_root + "g++"
+ _rbe_debug_flag = ""
+ _local_tool_name_root = "arm-none-eabi-"
+ if (pw_toolchain_USE_RBE) {
+ if (pw_toolchain_RBE_DEBUG) {
+ _rbe_debug_flag = " -v "
+ }
+ _exec_root = rebase_path("//")
+ _rewrapper_binary = "rewrapper"
+ _pw_rbe_config = pw_rbe_arm_gcc_config
+ _inputs = rebase_path(pw_env_setup_CIPD_ARM, _exec_root) + "/"
+ _rbe_tool_name_root =
+ _rewrapper_binary +
+ " --labels=type=compile,lang=cpp,compiler=clang --cfg=" +
+ _pw_rbe_config + " --exec_root=" + _exec_root + " --inputs=" + _inputs +
+ " -- " + rebase_path(pw_env_setup_CIPD_ARM, root_build_dir) +
+ "/bin/arm-none-eabi-"
+ cc = _rbe_tool_name_root + "gcc" + _rbe_debug_flag
+ cxx = _rbe_tool_name_root + "g++" + _rbe_debug_flag
+ } else {
+ cc = _local_tool_name_root + "gcc"
+ cxx = _local_tool_name_root + "g++"
+ }
+ ar = _local_tool_name_root + "ar"
+ ld = _local_tool_name_root + "g++"
link_whole_archive = true
}
diff --git a/pw_toolchain/c_optimization.gni b/pw_toolchain/c_optimization.gni
new file mode 100644
index 000000000..2fb0fdd80
--- /dev/null
+++ b/pw_toolchain/c_optimization.gni
@@ -0,0 +1,20 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# The optimization levels supported by Pigweed toolchains.
+pw_toolchain_SUPPORTED_C_OPTIMIZATION_LEVELS = [
+ "debug", # -Og
+ "size_optimized", # -Os in GCC or -Oz in clang
+ "speed_optimized", # -O2
+]
diff --git a/pw_toolchain/clang_tools.gni b/pw_toolchain/clang_tools.gni
index 6ecd3b7b8..56796f4e4 100644
--- a/pw_toolchain/clang_tools.gni
+++ b/pw_toolchain/clang_tools.gni
@@ -11,27 +11,55 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
+import("//build_overrides/pigweed.gni")
+import("//build_overrides/pigweed_environment.gni")
+import("$dir_pw_toolchain/rbe.gni")
declare_args() {
# This flag allows you to specify the root directory of the clang, clang++,
# and llvm-ar binaries to use when compiling with a clang-based toolchain.
# This is useful for debugging toolchain-related issues by building with an
# externally-provided toolchain.
- pw_toolchain_CLANG_PREFIX = ""
+ pw_toolchain_CLANG_PREFIX =
+ rebase_path(pw_env_setup_CIPD_PIGWEED + "/bin/", root_build_dir)
+
+ # This flag allows you to specify the root directory of the rustc binary.
+ pw_toolchain_RUST_PREFIX =
+ rebase_path(pw_env_setup_CIPD_PIGWEED + "/rust/bin/", root_build_dir)
}
pw_toolchain_clang_tools = {
+ ar = "llvm-ar"
+ cc = "clang"
+ cxx = "clang++"
+ ld = cxx
+ rustc = "rustc"
+
if (pw_toolchain_CLANG_PREFIX != "") {
- ar = pw_toolchain_CLANG_PREFIX + "/"
- cc = pw_toolchain_CLANG_PREFIX + "/"
- cxx = pw_toolchain_CLANG_PREFIX + "/"
- } else {
- ar = ""
- cc = ""
- cxx = ""
+ ar = pw_toolchain_CLANG_PREFIX + ar
+ cc = pw_toolchain_CLANG_PREFIX + cc
+ cxx = pw_toolchain_CLANG_PREFIX + cxx
+ ld = pw_toolchain_CLANG_PREFIX + ld
+ }
+
+ if (pw_toolchain_RUST_PREFIX != "") {
+ rustc = pw_toolchain_RUST_PREFIX + rustc
}
- ar += "llvm-ar"
- cc += "clang"
- cxx += "clang++"
+ if (pw_toolchain_USE_RBE) {
+ _rbe_debug_flag = ""
+ if (pw_toolchain_RBE_DEBUG) {
+ _rbe_debug_flag = " -v"
+ }
+ _exec_root = rebase_path("//")
+ _rewrapper_binary = "rewrapper"
+ _pw_rbe_config = pw_rbe_clang_config
+ _rbe_toolchain_prefix =
+ _rewrapper_binary +
+ " --labels=type=compile,lang=cpp,compiler=clang --cfg=" +
+ _pw_rbe_config + " --exec_root=" + _exec_root + " -- "
+
+ cc = _rbe_toolchain_prefix + cc + _rbe_debug_flag
+ cxx = _rbe_toolchain_prefix + cxx + _rbe_debug_flag
+ }
}
diff --git a/pw_toolchain/docs.rst b/pw_toolchain/docs.rst
index 81f60318c..46958c132 100644
--- a/pw_toolchain/docs.rst
+++ b/pw_toolchain/docs.rst
@@ -1,67 +1,24 @@
.. _module-pw_toolchain:
-------------
+============
pw_toolchain
-------------
+============
GN toolchains function both as a set of tools for compilation and as a workspace
for evaluating build files. The same compilations and actions can be executed by
different toolchains. Each toolchain maintains its own set of build args, and
build steps from all toolchains can be executed in parallel.
+----------
Toolchains
-==========
-``pw_toolchain`` provides GN toolchains that may be used to build Pigweed. The
-following toolchains are defined:
-
- - pw_toolchain_arm_clang.cortex_m0plus_debug
- - pw_toolchain_arm_clang.cortex_m0plus_speed_optimized
- - pw_toolchain_arm_clang.cortex_m0plus_size_optimized
- - pw_toolchain_arm_clang.cortex_m3_debug
- - pw_toolchain_arm_clang.cortex_m3_speed_optimized
- - pw_toolchain_arm_clang.cortex_m3_size_optimized
- - pw_toolchain_arm_clang.cortex_m4_debug
- - pw_toolchain_arm_clang.cortex_m4_speed_optimized
- - pw_toolchain_arm_clang.cortex_m4_size_optimized
- - pw_toolchain_arm_clang.cortex_m4f_debug
- - pw_toolchain_arm_clang.cortex_m4f_speed_optimized
- - pw_toolchain_arm_clang.cortex_m4f_size_optimized
- - pw_toolchain_arm_gcc.cortex_m0plus_debug
- - pw_toolchain_arm_gcc.cortex_m0plus_speed_optimized
- - pw_toolchain_arm_gcc.cortex_m0plus_size_optimized
- - pw_toolchain_arm_gcc.cortex_m3_debug
- - pw_toolchain_arm_gcc.cortex_m3_speed_optimized
- - pw_toolchain_arm_gcc.cortex_m3_size_optimized
- - pw_toolchain_arm_gcc.cortex_m4_debug
- - pw_toolchain_arm_gcc.cortex_m4_speed_optimized
- - pw_toolchain_arm_gcc.cortex_m4_size_optimized
- - pw_toolchain_arm_gcc.cortex_m4f_debug
- - pw_toolchain_arm_gcc.cortex_m4f_speed_optimized
- - pw_toolchain_arm_gcc.cortex_m4f_size_optimized
- - pw_toolchain_arm_gcc.cortex_m7_debug
- - pw_toolchain_arm_gcc.cortex_m7_speed_optimized
- - pw_toolchain_arm_gcc.cortex_m7_size_optimized
- - pw_toolchain_arm_gcc.cortex_m7f_debug
- - pw_toolchain_arm_gcc.cortex_m7f_speed_optimized
- - pw_toolchain_arm_gcc.cortex_m7f_size_optimized
- - pw_toolchain_arm_gcc.cortex_m33_debug
- - pw_toolchain_arm_gcc.cortex_m33_speed_optimized
- - pw_toolchain_arm_gcc.cortex_m33_size_optimized
- - pw_toolchain_arm_gcc.cortex_m33f_debug
- - pw_toolchain_arm_gcc.cortex_m33f_speed_optimized
- - pw_toolchain_arm_gcc.cortex_m33f_size_optimized
- - pw_toolchain_host_clang.debug
- - pw_toolchain_host_clang.speed_optimized
- - pw_toolchain_host_clang.size_optimized
- - pw_toolchain_host_clang.fuzz
- - pw_toolchain_host_gcc.debug
- - pw_toolchain_host_gcc.speed_optimized
- - pw_toolchain_host_gcc.size_optimized
-
- .. note::
- The documentation for this module is currently incomplete.
+----------
+``pw_toolchain`` module provides GN toolchains that may be used to build
+Pigweed. Various GCC and Clang toolchains for multiple platforms are provided.
+Toolchains names typically include the compiler (``clang`` or ``gcc`` and
+optimization level (``debug``, ``size_optimized``, ``speed_optimized``).
+--------------------
Non-C/C++ toolchains
-====================
+--------------------
``pw_toolchain/non_c_toolchain.gni`` provides the ``pw_non_c_toolchain``
template. This template creates toolchains that cannot compile C/C++ source
code. These toolchains may only be used to execute GN actions or declare groups
@@ -78,72 +35,232 @@ installation and Pylint in toolchains created with ``pw_non_c_toolchain``. This
allows all toolchains to cleanly share the same protobuf and Python declarations
without any duplicated work.
+-------------------------------
Testing other compiler versions
-===============================
+-------------------------------
The clang-based toolchain provided by Pigweed can be substituted with another
version by modifying the ``pw_toolchain_CLANG_PREFIX`` GN build argument to
point to the directory that contains the desired clang, clang++, and llvm-ar
binaries. This should only be used for debugging purposes. Pigweed does not
officially support any compilers other than those provided by Pigweed.
+------------------------------
Running static analysis checks
-==============================
+------------------------------
``clang-tidy`` can be run as a compiler replacement, to analyze all sources
built for a target. ``pw_toolchain/static_analysis_toolchain.gni`` provides
the ``pw_static_analysis_toolchain`` template. This template creates toolchains
that execute ``clang-tidy`` for C/C++ sources, and mock implementations of
the ``link``, ``alink`` and ``solink`` tools.
-Additionally, ``generate_toolchain`` implements a boolean flag
-``static_analysis`` (default ``false``) which generates the derived
-toolchain ``${target_name}.static_analysis`` using
+In addition to the standard toolchain requirements (`cc`, `cxx`, etc..), the
+``pw_static_analysis_toolchain`` template requires a scope ``static_analysis``
+to be defined on the invoker.
+
+.. code-block::
+
+ static_analysis = {
+ # Configure whether static_analysis should be enabled for invoker toolchain.
+ # This is must be set true if using pw_static_analysis_toolchain.
+ enabled = true
+ # Optionally override clang-tidy binary to use by setting to proper path.
+ clang_tidy_path = ""
+ # Optionally specify additional command(s) to run as part of cc tool.
+ cc_post = ""
+ # Optionally specify additional command(s) to run as part of cxx tool.
+ cxx_post = ""
+ }
+
+The ``generate_toolchain`` supports the above mentioned ``static_analysis``
+scope, which if specified must at the very least define the bool ``enabled``
+within the scope. If the ``static_analysis`` scope is provided and
+``static_analysis.enabled = true``, the derived toolchain
+``${target_name}.static_analysis`` will be generated using
``pw_generate_static_analysis_toolchain`` and the toolchain options.
+An example on the utility of the ``static_analysis`` scope args is shown in the
+snippet below where we enable clang-tidy caching and add ``//.clang-tidy`` as a
+dependency to the generated ``.d`` files for the
+``pw_static_analysis_toolchain``.
+
+.. code-block::
+
+ static_analysis = {
+ clang_tidy_path = "//third_party/ctcache/clang-tidy"
+ _clang_tidy_cfg_path = rebase_path("//.clang-tidy", root_build_dir)
+ cc_post = "echo '-: $_clang_tidy_cfg_path' >> {{output}}.d"
+ cxx_post = "echo '-: $_clang_tidy_cfg_path' >> {{output}}.d"
+ }
+
Excluding files from checks
----------------------------
+===========================
The build argument ``pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES`` is used
-used to exclude source files from the analysis. The list must contain regular
+to exclude source files from the analysis. The list must contain regular
expressions matching individual files, rather than directories. For example,
provide ``"the_path/.*"`` to exclude all files in all directories under
``the_path``.
The build argument ``pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS`` is used
used to exclude header files from the analysis. This argument must be a list of
-POSIX-style path suffixes for include paths, or regular expressions. For
-example, passing ``the_path/include`` excludes all header files that are
-accessed from include paths ending in ``the_path/include``, while passing
-``.*/third_party/.*`` excludes all third-party header files.
+POSIX-style path suffixes for include paths, or regular expressions matching
+include paths. For example, passing ``the_path/include`` excludes all header
+files that are accessed from include paths ending in ``the_path/include``,
+while passing ``.*/third_party/.*`` excludes all third-party header files.
+
+Note that ``pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS`` operates on
+include paths, not header file paths. For example, say your compile commands
+include ``-Idrivers``, and this results in a file at ``drivers/public/i2c.h``
+being included. You can skip this header by adding ``drivers`` or ``drivers.*``
+to ``pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS``, but *not* by adding
+``drivers/.*``: this last regex matches the header file path, but not the
+include path.
Provided toolchains
--------------------
+===================
``pw_toolchain`` provides static analysis GN toolchains that may be used to
test host targets:
- - pw_toolchain_host_clang.debug.static_analysis
- - pw_toolchain_host_clang.speed_optimized.static_analysis
- - pw_toolchain_host_clang.size_optimized.static_analysis
- - pw_toolchain_host_clang.fuzz.static_analysis
- (if pw_toolchain_OSS_FUZZ_ENABLED is false)
- - pw_toolchain_arm_clang.debug.static_analysis
- - pw_toolchain_arm_clang.speed_optimized.static_analysis
- - pw_toolchain_arm_clang.size_optimized.static_analysis
+- pw_toolchain_host_clang.debug.static_analysis
+- pw_toolchain_host_clang.speed_optimized.static_analysis
+- pw_toolchain_host_clang.size_optimized.static_analysis
+- pw_toolchain_host_clang.fuzz.static_analysis
+ (if pw_toolchain_OSS_FUZZ_ENABLED is false)
+- pw_toolchain_arm_clang.debug.static_analysis
+- pw_toolchain_arm_clang.speed_optimized.static_analysis
+- pw_toolchain_arm_clang.size_optimized.static_analysis
- For example, to run ``clang-tidy`` on all source dependencies of the
- ``default`` target:
+For example, to run ``clang-tidy`` on all source dependencies of the
+``default`` target:
.. code-block::
- generate_toolchain("my_toolchain") {
- ..
- static_analysis = true
- }
+ generate_toolchain("my_toolchain") {
+ ..
+ static_analysis = {
+ enabled = true
+ }
+ }
+
+ group("static_analysis") {
+ deps = [ ":default(my_toolchain.static_analysis)" ]
+ }
+
+.. warning::
+
+ The status of the static analysis checks might change when
+ any relevant .clang-tidy file is updated. You should
+ clean the output directory before invoking
+ ``clang-tidy``.
+
+-------------
+Target traits
+-------------
+Pigweed targets expose a set of constants that describe properties of the target
+or the toolchain compiling code for it. These are referred to as target traits.
+
+In GN, these traits are exposed as GN args and are prefixed with
+``pw_toolchain_`` (e.g. ``pw_toolchain_CXX_STANDARD``). They are defined in
+``pw_toolchain/traits.gni``.
+
+Traits must never be set by the user (e.g. with ``gn args``). Traits are always
+set by the target.
+
+.. warning::
+
+ This feature is under development and is likely to change significantly.
+ See `b/234883746 <http://issuetracker.google.com/issues/234883746>`_.
+
+List of traits
+==============
+- ``CXX_STANDARD``. The C++ standard used by the toolchain. The value must be an
+ integer value matching one of the standard values for the ``__cplusplus``
+ macro. For example, ``201703`` corresponds to C++17. See
+ https://en.cppreference.com/w/cpp/preprocessor/replace#Predefined_macros for
+ further details.
+
+---------------
+C/C++ libraries
+---------------
+``pw_toolchain`` provides some toolchain-related C/C++ libraries.
+
+``std:abort`` wrapper
+=====================
+The `std::abort <https://en.cppreference.com/w/cpp/utility/program/abort>`_
+function is used to terminate a program abnormally. This function may be called
+by standard library functions, so is often linked into binaries, even if users
+never intentionally call it.
+
+For embedded builds, the ``abort`` implementation likely does not work as
+intended. For example, it may pull in undesired dependencies (e.g.
+``std::raise``) and end in an infinite loop.
+
+``pw_toolchain`` provides the ``pw_toolchain:wrap_abort`` library that replaces
+``abort`` in builds where the default behavior is undesirable. It uses the
+``-Wl,--wrap=abort`` linker option to redirect to ``abort`` calls to
+``PW_CRASH`` instead.
+
+arm-none-eabi-gcc support
+=========================
+Targets building with the GNU Arm Embedded Toolchain (``arm-none-eabi-gcc``)
+should depend on the ``pw_toolchain/arm_gcc:arm_none_eabi_gcc_support`` library
+into their builds. In GN, that target should be included in
+``pw_build_LINK_DEPS``.
+
+Newlib OS interface
+-------------------
+`Newlib <https://sourceware.org/newlib/>`_, the C Standard Library
+implementation provided with ``arm-none-eabi-gcc``, defines a set of `OS
+interface functions <https://sourceware.org/newlib/libc.html#Stubs>`_ that
+should be implemented. A default is provided if these functions are not
+implemented, but using the default results in a compiler warning.
+
+Most of the OS interface functions should never be called in embedded builds.
+The ``pw_toolchain/arg_gcc:newlib_os_interface_stubs`` library, which is
+provided through ``pw_toolchain/arm_gcc:arm_none_eabi_gcc_support``, implements
+these functions and forces a linker error if they are used. It also wraps some
+functions related to use of ``stdout`` and ``stderr`` that abort if they are
+called.
- group("static_analysis") {
- deps = [ ":default(my_toolchain.static_analysis)" ]
- }
+pw_toolchain/no_destructor.h
+============================
+.. cpp:class:: template <typename T> pw::NoDestructor
+
+ Helper type to create a function-local static variable of type ``T`` when
+ ``T`` has a non-trivial destructor. Storing a ``T`` in a
+ ``pw::NoDestructor<T>`` will prevent ``~T()`` from running, even when the
+ variable goes out of scope.
+
+ This class is useful when a variable has static storage duration but its type
+ has a non-trivial destructor. Destructor ordering is not defined and can
+ cause issues in multithreaded environments. Additionally, removing destructor
+ calls can save code size.
+
+ Except in generic code, do not use ``pw::NoDestructor<T>`` with trivially
+ destructible types. Use the type directly instead. If the variable can be
+ constexpr, make it constexpr.
+
+ ``pw::NoDestructor<T>`` provides a similar API to std::optional. Use ``*`` or
+ ``->`` to access the wrapped type.
+
+ ``pw::NoDestructor<T>`` is based on Chromium's ``base::NoDestructor<T>`` in
+ `src/base/no_destructor.h <https://chromium.googlesource.com/chromium/src/base/+/5ea6e31f927aa335bfceb799a2007c7f9007e680/no_destructor.h>`_.
+
+ In Clang, ``pw::NoDestructor`` can be replaced with the `[[clang::no_destroy]]
+ <https://clang.llvm.org/docs/AttributeReference.html#no-destroy>`_.
+ attribute.
+
+Example usage
+-------------
+.. code-block:: cpp
+
+ pw::sync::Mutex& GetMutex() {
+ // Use NoDestructor to avoid running the mutex destructor when exit-time
+ // destructors run.
+ static const pw::NoDestructor<pw::sync::Mutex> global_mutex;
+ return *global_mutex;
+ }
.. warning::
- The status of the static analysis checks might change when
- any relevant .clang-tidy file is updated. You should
- clean the output directory before invoking
- ``clang-tidy``.
+
+ Misuse of :cpp:class:`pw::NoDestructor` can cause resource leaks and other
+ problems. Only skip destructors when you know it is safe to do so.
diff --git a/pw_toolchain/generate_toolchain.gni b/pw_toolchain/generate_toolchain.gni
index db057c24d..ff12f0b70 100644
--- a/pw_toolchain/generate_toolchain.gni
+++ b/pw_toolchain/generate_toolchain.gni
@@ -35,6 +35,7 @@ declare_args() {
# ar: (required) String indicating the archive tool to use.
# cc: (required) String indicating the C compiler to use.
# cxx: (required) String indicating the C++ compiler to use.
+# ld: (optional) String indicating the linking binary to use.
# is_host_toolchain: (optional) Boolean indicating if the outputs are meant
# for the $host_os.
# final_binary_extension: (optional) The extension to apply to final linked
@@ -51,6 +52,11 @@ declare_args() {
# default_toolchain.
# defaults: (required) A scope setting GN build arg values to apply to GN
# targets in this toolchain. These take precedence over args.gni settings.
+# static_analysis: (optional) A scope defining args to apply to the
+# static_analysis toolchain. If the scope is not defined, static analysis
+# will be disabled. If provided, static_analysis will be enabled iff
+# required enabled field in scope is declared true. See
+# static_analysis_toolchain.gni for more information on scope members.
#
# The defaults scope should contain values for builtin GN arguments:
# current_cpu: The CPU of the toolchain.
@@ -58,7 +64,7 @@ declare_args() {
# current_os: The OS of the toolchain. Defaults to "".
# Well known values include "win", "mac", "linux", "android", and "ios".
#
-# TODO(pwbug/333): This should be renamed to pw_generate_toolchain.
+# TODO(b/234891809): This should be renamed to pw_generate_toolchain.
template("generate_toolchain") {
assert(defined(invoker.defaults), "toolchain is missing 'defaults'")
@@ -242,10 +248,17 @@ template("generate_toolchain") {
_link_outfile =
"{{output_dir}}/{{target_output_name}}{{output_extension}}"
_link_mapfile = "{{output_dir}}/{{target_output_name}}.map"
- _link_flags = [
- invoker.cxx,
- "{{ldflags}}",
- ]
+ if (defined(invoker.ld)) {
+ _link_flags = [
+ invoker.ld,
+ "{{ldflags}}",
+ ]
+ } else {
+ _link_flags = [
+ invoker.cxx,
+ "{{ldflags}}",
+ ]
+ }
if (toolchain_os == "mac" || toolchain_os == "ios") {
_link_flags += [
@@ -362,11 +375,57 @@ template("generate_toolchain") {
}
forward_variables_from(invoker_toolchain_args, "*")
}
- }
- _generate_static_analysis_toolchain =
- defined(invoker.static_analysis) && invoker.static_analysis
+ _generate_rust_tools = defined(invoker.rustc)
+ if (_generate_rust_tools) {
+ _rustc_command = string_join(
+ " ",
+ [
+ # TODO(b/234872510): Ensure this works with Windows.
+ "RUST_BACKTRACE=1",
+ "{{rustenv}}",
+ invoker.rustc,
+ "{{source}}",
+ "--crate-name {{crate_name}}",
+ "--crate-type {{crate_type}}",
+ "{{externs}}",
+ "{{rustdeps}}",
+ "{{rustflags}}",
+ "-D warnings",
+ "--color always",
+ "--emit=dep-info={{output}}.d,link",
+ "-o {{output_dir}}/{{target_output_name}}{{output_extension}}",
+ ])
+
+ _output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
+
+ tool("rust_bin") {
+ description = "rustc {{output}}"
+ default_output_dir = "{{target_out_dir}}/bin"
+ depfile = "{{output}}.d"
+ command = _rustc_command
+ outputs = [ _output ]
+ }
+ tool("rust_rlib") {
+ description = "rustc {{output}}"
+ default_output_dir = "{{target_out_dir}}/lib"
+ depfile = "{{output}}.d"
+ output_prefix = "lib"
+ default_output_extension = ".rlib"
+ command = _rustc_command
+ outputs = [ _output ]
+ }
+ }
+ }
+
+ _generate_static_analysis_toolchain = false
+ if (defined(invoker.static_analysis)) {
+ _static_analysis_args = invoker.static_analysis
+ assert(defined(_static_analysis_args.enabled),
+ "static_analysis.enabled missing from scope.")
+ _generate_static_analysis_toolchain = _static_analysis_args.enabled
+ }
if (_generate_static_analysis_toolchain) {
pw_static_analysis_toolchain(target_name + ".static_analysis") {
forward_variables_from(invoker, "*")
diff --git a/pw_toolchain/host_clang/BUILD.gn b/pw_toolchain/host_clang/BUILD.gn
index bed41fbc8..eeebe1231 100644
--- a/pw_toolchain/host_clang/BUILD.gn
+++ b/pw_toolchain/host_clang/BUILD.gn
@@ -106,6 +106,15 @@ config("xcode_sysroot") {
}
}
+config("linux_sysroot") {
+ if (current_os == "linux") {
+ cflags = [ "--sysroot=" +
+ rebase_path(pw_env_setup_CIPD_PIGWEED, root_build_dir) +
+ "/clang_sysroot/" ]
+ ldflags = cflags
+ }
+}
+
# The CIPD provided Clang/LLVM toolchain must link against the matched
# libc++ which is also from CIPD. However, by default, Clang on Mac (but
# not on Linux) will fall back to the system libc++, which is
@@ -114,7 +123,7 @@ config("xcode_sysroot") {
# Pull the appropriate paths from our Pigweed env setup.
config("no_system_libcpp") {
if (current_os == "mac") {
- install_dir = dir_cipd_pigweed
+ install_dir = pw_env_setup_CIPD_PIGWEED
assert(install_dir != "",
"You forgot to activate the Pigweed environment; " +
"did you source pw_env_setup/setup.sh?")
@@ -123,7 +132,7 @@ config("no_system_libcpp") {
"-nostdlib++",
# Use the libc++ from CIPD.
- dir_cipd_pigweed + "/lib/libc++.a",
+ rebase_path(pw_env_setup_CIPD_PIGWEED + "/lib/libc++.a", root_build_dir),
]
}
}
diff --git a/pw_toolchain/host_clang/toolchain.cmake b/pw_toolchain/host_clang/toolchain.cmake
index ae34701b8..d84db47ba 100644
--- a/pw_toolchain/host_clang/toolchain.cmake
+++ b/pw_toolchain/host_clang/toolchain.cmake
@@ -16,15 +16,22 @@
# Regardless of whether it's set or not the following include will ensure it is.
include(${CMAKE_CURRENT_LIST_DIR}/../../pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_assert/backend.cmake)
+include($ENV{PW_ROOT}/pw_chrono/backend.cmake)
+include($ENV{PW_ROOT}/pw_log/backend.cmake)
+include($ENV{PW_ROOT}/pw_perf_test/backend.cmake)
+include($ENV{PW_ROOT}/pw_rpc/system_server/backend.cmake)
+include($ENV{PW_ROOT}/pw_sync/backend.cmake)
+include($ENV{PW_ROOT}/pw_sys_io/backend.cmake)
+include($ENV{PW_ROOT}/pw_thread/backend.cmake)
+include($ENV{PW_ROOT}/pw_trace/backend.cmake)
+
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
-# TODO(pwbug/606): set up this facade in CMake
-# Use logging-based test output on host.
-# pw_set_backend(pw_unit_test.main pw_unit_test.logging_main)
-
# Configure backend for assert facade.
-pw_set_backend(pw_assert pw_assert_basic)
+pw_set_backend(pw_assert.check pw_assert.print_and_abort_check_backend)
+pw_set_backend(pw_assert.assert pw_assert.print_and_abort_assert_backend)
# Configure backend for logging facade.
pw_set_backend(pw_log pw_log_basic)
@@ -52,24 +59,34 @@ pw_set_backend(pw_rpc.system_server targets.host.system_rpc_server)
pw_set_backend(pw_chrono.system_clock pw_chrono_stl.system_clock)
pw_set_backend(pw_chrono.system_timer pw_chrono_stl.system_timer)
+# Configure backend for pw_perf_test's facade
+pw_set_backend(pw_perf_test.TIMER_INTERFACE_BACKEND pw_perf_test.chrono_timer)
+
# Configure backends for pw_thread's facades.
pw_set_backend(pw_thread.id pw_thread_stl.id)
pw_set_backend(pw_thread.yield pw_thread_stl.yield)
pw_set_backend(pw_thread.sleep pw_thread_stl.sleep)
pw_set_backend(pw_thread.thread pw_thread_stl.thread)
-# TODO: Migrate this to match GN's tokenized trace setup.
+# TODO(ewout): Migrate this to match GN's tokenized trace setup.
pw_set_backend(pw_trace pw_trace.null)
-# The CIPD provided Clang/LLVM toolchain must link against the matched
-# libc++ which is also from CIPD. However, by default, Clang on Mac (but
-# not on Linux) will fall back to the system libc++, which is
-# incompatible due to an ABI change.
-#
-# Pull the appropriate paths from our Pigweed env setup.
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
+ # The CIPD provided Clang/LLVM toolchain must link against the matched
+ # libc++ which is also from CIPD. However, by default, Clang on Mac (but
+ # not on Linux) will fall back to the system libc++, which is
+ # incompatible due to an ABI change.
+ #
+ # Pull the appropriate paths from our Pigweed env setup.
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -nostdlib++ $ENV{PW_PIGWEED_CIPD_INSTALL_DIR}/lib/libc++.a")
+elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ # Use the CIPD provided sysroot to make host builds more hermetic.
+ set(CMAKE_SYSROOT "$ENV{PW_PIGWEED_CIPD_INSTALL_DIR}/clang_sysroot")
endif()
-set(pw_build_WARNINGS pw_build.strict_warnings pw_build.extra_strict_warnings
- CACHE STRING "" FORCE)
+set(pw_build_WARNINGS
+ pw_build.strict_warnings
+ pw_build.extra_strict_warnings
+ pw_build.pedantic_warnings
+ CACHE STRING "" FORCE
+)
diff --git a/pw_toolchain/host_clang/toolchains.gni b/pw_toolchain/host_clang/toolchains.gni
index d9f9da76a..3e847c4af 100644
--- a/pw_toolchain/host_clang/toolchains.gni
+++ b/pw_toolchain/host_clang/toolchains.gni
@@ -18,9 +18,20 @@ import("$dir_pw_toolchain/clang_tools.gni")
declare_args() {
# Sets the sanitizer to pass to clang. Valid values are "address", "memory",
- # "thread", "undefined".
+ # "thread", "undefined", "undefined_heuristic", and "coverage".
pw_toolchain_SANITIZERS = []
+ # Indicates if this toolchain supports generating coverage reports from
+ # pw_test targets.
+ #
+ # For example, the static analysis toolchains that run `clang-tidy` instead
+ # of the test binary itself cannot generate coverage reports.
+ pw_toolchain_COVERAGE_ENABLED = false
+
+ # Indicates if this toolchain supports building fuzzers. This is typically
+ # set by individual toolchains and not by GN args.
+ pw_toolchain_FUZZING_ENABLED = false
+
# Indicates if this build is a part of OSS-Fuzz, which needs to be able to
# provide its own compiler and flags. This violates the build hermeticisim and
# should only be used for OSS-Fuzz.
@@ -30,6 +41,9 @@ declare_args() {
# Specifies the tools used by host Clang toolchains.
_host_clang_toolchain = {
if (pw_toolchain_OSS_FUZZ_ENABLED) {
+ # OSS-Fuzz sets compiler and linker paths. See
+ # google.github.io/oss-fuzz/getting-started/new-project-guide/#Requirements.
+
# Just use the "llvm-ar" on the system path.
ar = "llvm-ar"
cc = getenv("CC")
@@ -40,14 +54,16 @@ _host_clang_toolchain = {
is_host_toolchain = true
- # Enable static analysis for host clang based toolchains,
- # even with OSS-Fuzz enabled.
- static_analysis = true
+ static_analysis = {
+ # Enable static analysis for host clang based toolchains,
+ # even with OSS-Fuzz enabled.
+ enabled = true
+ }
}
# Common default scope shared by all host Clang toolchains.
_defaults = {
- # TODO(pwbug/461) amend toolchain declaration process to
+ # TODO(b/234888755) amend toolchain declaration process to
# remove this hack.
default_configs = []
default_configs = [
@@ -55,6 +71,12 @@ _defaults = {
"$dir_pw_toolchain/host_clang:no_system_libcpp",
"$dir_pw_toolchain/host_clang:xcode_sysroot",
]
+
+ # OSS-Fuzz uses -stdlib=libc++, which isn't included in the CIPD-provided
+ # Linux sysroot (it instead provides libstdc++).
+ if (!pw_toolchain_OSS_FUZZ_ENABLED) {
+ default_configs += [ "$dir_pw_toolchain/host_clang:linux_sysroot" ]
+ }
}
pw_toolchain_host_clang = {
@@ -68,6 +90,9 @@ pw_toolchain_host_clang = {
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_$sanitizer" ]
}
+
+ # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
+ pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -81,6 +106,9 @@ pw_toolchain_host_clang = {
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_$sanitizer" ]
}
+
+ # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
+ pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -89,11 +117,14 @@ pw_toolchain_host_clang = {
forward_variables_from(_host_clang_toolchain, "*")
defaults = {
forward_variables_from(_defaults, "*")
- default_configs += [ "$dir_pw_build:optimize_size" ]
+ default_configs += [ "$dir_pw_build:optimize_size_clang" ]
foreach(sanitizer, pw_toolchain_SANITIZERS) {
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_$sanitizer" ]
}
+
+ # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
+ pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -103,6 +134,12 @@ pw_toolchain_host_clang = {
defaults = {
forward_variables_from(_defaults, "*")
+ pw_toolchain_FUZZING_ENABLED = true
+ default_configs += [ "$dir_pw_fuzzer:instrumentation" ]
+
+ # Always disable coverage generation.
+ pw_toolchain_COVERAGE_ENABLED = false
+
# Fuzz faster.
default_configs += [ "$dir_pw_build:optimize_speed" ]
@@ -114,10 +151,6 @@ pw_toolchain_host_clang = {
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_$sanitizer" ]
}
-
- if (pw_toolchain_OSS_FUZZ_ENABLED) {
- default_configs += [ "$dir_pw_fuzzer:oss_fuzz_extra" ]
- }
}
}
@@ -130,6 +163,9 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_address" ]
+
+ # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
+ pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -142,6 +178,9 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_undefined" ]
+
+ # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
+ pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -155,6 +194,9 @@ pw_toolchain_host_clang = {
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs +=
[ "$dir_pw_toolchain/host_clang:sanitize_undefined_heuristic" ]
+
+ # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
+ pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -167,6 +209,9 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_memory" ]
+
+ # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
+ pw_toolchain_COVERAGE_ENABLED = true
}
}
@@ -179,6 +224,29 @@ pw_toolchain_host_clang = {
# Use debug mode to get proper debug information.
default_configs += [ "$dir_pw_build:optimize_debugging" ]
default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_thread" ]
+
+ # Allow coverage generation if pw_toolchain_SANITIZERS = ["coverage"].
+ pw_toolchain_COVERAGE_ENABLED = true
+ }
+ }
+
+ coverage = {
+ name = "host_clang_coverage"
+ forward_variables_from(_host_clang_toolchain, "*")
+ defaults = {
+ forward_variables_from(_defaults, "*")
+
+ # Use debug mode to get proper debug information.
+ default_configs += [ "$dir_pw_build:optimize_debugging" ]
+ default_configs += [ "$dir_pw_toolchain/host_clang:sanitize_coverage" ]
+
+ # Enable PW toolchain arguments for coverage. This will only apply to
+ # binaries built using this toolchain.
+ #
+ # "coverage" works with "address", "memory", "thread", "undefined", and
+ # "undefined_heuristic" sanitizers.
+ pw_toolchain_SANITIZERS += [ "coverage" ]
+ pw_toolchain_COVERAGE_ENABLED = true
}
}
}
@@ -194,4 +262,5 @@ pw_toolchain_host_clang_list = [
pw_toolchain_host_clang.ubsan_heuristic,
pw_toolchain_host_clang.msan,
pw_toolchain_host_clang.tsan,
+ pw_toolchain_host_clang.coverage,
]
diff --git a/pw_toolchain/host_gcc/toolchain.cmake b/pw_toolchain/host_gcc/toolchain.cmake
index 4529f692f..ed011e21c 100644
--- a/pw_toolchain/host_gcc/toolchain.cmake
+++ b/pw_toolchain/host_gcc/toolchain.cmake
@@ -16,15 +16,22 @@
# Regardless of whether it's set or not the following include will ensure it is.
include(${CMAKE_CURRENT_LIST_DIR}/../../pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_assert/backend.cmake)
+include($ENV{PW_ROOT}/pw_chrono/backend.cmake)
+include($ENV{PW_ROOT}/pw_log/backend.cmake)
+include($ENV{PW_ROOT}/pw_perf_test/backend.cmake)
+include($ENV{PW_ROOT}/pw_rpc/system_server/backend.cmake)
+include($ENV{PW_ROOT}/pw_sync/backend.cmake)
+include($ENV{PW_ROOT}/pw_sys_io/backend.cmake)
+include($ENV{PW_ROOT}/pw_thread/backend.cmake)
+include($ENV{PW_ROOT}/pw_trace/backend.cmake)
+
set(CMAKE_C_COMPILER gcc)
set(CMAKE_CXX_COMPILER g++)
-# TODO(pwbug/606): set up this facade in CMake
-# Use logging-based test output on host.
-# pw_set_backend(pw_unit_test.main pw_unit_test.logging_main)
-
# Configure backend for assert facade.
-pw_set_backend(pw_assert pw_assert_basic)
+pw_set_backend(pw_assert.check pw_assert.print_and_abort_check_backend)
+pw_set_backend(pw_assert.assert pw_assert.print_and_abort_assert_backend)
# Configure backend for logging facade.
pw_set_backend(pw_log pw_log_basic)
@@ -52,13 +59,16 @@ pw_set_backend(pw_rpc.system_server targets.host.system_rpc_server)
pw_set_backend(pw_chrono.system_clock pw_chrono_stl.system_clock)
pw_set_backend(pw_chrono.system_timer pw_chrono_stl.system_timer)
+# Configure backend for pw_perf_test's facade.
+pw_perf_test(pw_perf_test.timer pw_perf_test.chrono_timer)
+
# Configure backends for pw_thread's facades.
pw_set_backend(pw_thread.id pw_thread_stl.id)
pw_set_backend(pw_thread.yield pw_thread_stl.yield)
pw_set_backend(pw_thread.sleep pw_thread_stl.sleep)
pw_set_backend(pw_thread.thread pw_thread_stl.thread)
-# TODO: Migrate this to match GN's tokenized trace setup.
+# TODO(ewout): Migrate this to match GN's tokenized trace setup.
pw_set_backend(pw_trace pw_trace.null)
set(pw_build_WARNINGS pw_build.strict_warnings pw_build.extra_strict_warnings
diff --git a/pw_toolchain/no_destructor_test.cc b/pw_toolchain/no_destructor_test.cc
new file mode 100644
index 000000000..936cedd31
--- /dev/null
+++ b/pw_toolchain/no_destructor_test.cc
@@ -0,0 +1,87 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_toolchain/no_destructor.h"
+
+#include "gtest/gtest.h"
+#include "pw_assert/check.h"
+
+namespace pw {
+namespace {
+
+class HasADestructor {
+ public:
+ HasADestructor(bool& destructor_called_flag)
+ : destructor_called_(destructor_called_flag) {
+ destructor_called_ = false;
+ }
+
+ ~HasADestructor() { destructor_called_ = true; }
+
+ private:
+ bool& destructor_called_;
+};
+
+class CrashInDestructor {
+ public:
+ const CrashInDestructor* MyAddress() const { return this; }
+
+ int some_value = 0;
+
+ private:
+ ~CrashInDestructor() { PW_CRASH("This destructor should never execute!"); }
+};
+
+TEST(NoDestructor, ShouldNotCallDestructor) {
+ bool destructor_called = false;
+
+ { HasADestructor should_be_destroyed(destructor_called); }
+
+ EXPECT_TRUE(destructor_called);
+
+ { NoDestructor<HasADestructor> should_not_be_destroyed(destructor_called); }
+
+ EXPECT_FALSE(destructor_called);
+}
+
+TEST(NoDestructor, MemberAccess) {
+ NoDestructor<CrashInDestructor> no_destructor;
+
+ no_destructor->some_value = 123;
+ EXPECT_EQ(123, (*no_destructor).some_value);
+ EXPECT_EQ(no_destructor.operator->(), no_destructor->MyAddress());
+}
+
+TEST(NoDestructor, TrivialType) {
+ NoDestructor<int> no_destructor;
+
+ EXPECT_EQ(*no_destructor, 0);
+ *no_destructor = 123;
+ EXPECT_EQ(*no_destructor, 123);
+}
+
+TEST(NoDestructor, FunctionStatic) {
+ static NoDestructor<CrashInDestructor> function_static_no_destructor;
+}
+
+NoDestructor<CrashInDestructor> global_no_destructor;
+
+static_assert(!std::is_trivially_destructible<CrashInDestructor>::value,
+ "Type should not be trivially destructible");
+static_assert(
+ std::is_trivially_destructible<NoDestructor<CrashInDestructor>>::value,
+ "Wrapper should be trivially destructible");
+
+} // namespace
+} // namespace pw
diff --git a/pw_toolchain/public/pw_toolchain/no_destructor.h b/pw_toolchain/public/pw_toolchain/no_destructor.h
new file mode 100644
index 000000000..6986a08a5
--- /dev/null
+++ b/pw_toolchain/public/pw_toolchain/no_destructor.h
@@ -0,0 +1,122 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <new>
+#include <type_traits>
+#include <utility>
+
+namespace pw {
+
+// Helper type to create a function-local static variable of type T when T has a
+// non-trivial destructor. Storing a T in a pw::NoDestructor<T> will prevent
+// ~T() from running, even when the variable goes out of scope.
+//
+// This class is useful when a variable has static storage duration but its type
+// has a non-trivial destructor. Destructor ordering is not defined and can
+// cause issues in multithreaded environments. Additionally, removing destructor
+// calls can save code size.
+//
+// Except in generic code, do not use pw::NoDestructor<T> with trivially
+// destructible types. Use the type directly instead. If the variable can be
+// constexpr, make it constexpr.
+//
+// pw::NoDestructor<T> provides a similar API to std::optional. Use * or -> to
+// access the wrapped type.
+//
+// Example usage:
+//
+// pw::sync::Mutex& GetMutex() {
+// // Use NoDestructor to avoid running the mutex destructor when exit-time
+// // destructors run.
+// static const pw::NoDestructor<pw::sync::Mutex> global_mutex;
+// return *global_mutex;
+// }
+//
+// WARNING: Misuse of NoDestructor can cause memory leaks and other problems.
+// Only skip destructors when you know it is safe to do so.
+//
+// In Clang, pw::NoDestructor can be replaced with the [[clang::no_destroy]]
+// attribute.
+template <typename T>
+class NoDestructor {
+ public:
+ using value_type = T;
+
+ // Initializes a T in place.
+ //
+ // This overload is disabled when it might collide with copy/move.
+ template <typename... Args,
+ typename std::enable_if<!std::is_same<void(std::decay_t<Args>&...),
+ void(NoDestructor&)>::value,
+ int>::type = 0>
+ explicit constexpr NoDestructor(Args&&... args)
+ : storage_(std::forward<Args>(args)...) {}
+
+ // Move or copy from the contained type. This allows for construction from an
+ // initializer list, e.g. for std::vector.
+ explicit constexpr NoDestructor(const T& x) : storage_(x) {}
+ explicit constexpr NoDestructor(T&& x) : storage_(std::move(x)) {}
+
+ NoDestructor(const NoDestructor&) = delete;
+ NoDestructor& operator=(const NoDestructor&) = delete;
+
+ ~NoDestructor() = default;
+
+ const T& operator*() const { return *storage_.get(); }
+ T& operator*() { return *storage_.get(); }
+
+ const T* operator->() const { return storage_.get(); }
+ T* operator->() { return storage_.get(); }
+
+ private:
+ class DirectStorage {
+ public:
+ template <typename... Args>
+ explicit constexpr DirectStorage(Args&&... args)
+ : value_(std::forward<Args>(args)...) {}
+
+ const T* get() const { return &value_; }
+ T* get() { return &value_; }
+
+ private:
+ T value_;
+ };
+
+ class PlacementStorage {
+ public:
+ template <typename... Args>
+ explicit PlacementStorage(Args&&... args) {
+ new (&memory_) T(std::forward<Args>(args)...);
+ }
+
+ const T* get() const {
+ return std::launder(reinterpret_cast<const T*>(&memory_));
+ }
+ T* get() { return std::launder(reinterpret_cast<T*>(&memory_)); }
+
+ private:
+ alignas(T) char memory_[sizeof(T)];
+ };
+
+ // If the type is already trivially destructible, use it directly. Trivially
+ // destructible types do not need NoDestructor, but NoDestructor supports them
+ // to work better with generic code.
+ std::conditional_t<std::is_trivially_destructible<T>::value,
+ DirectStorage,
+ PlacementStorage>
+ storage_;
+};
+
+} // namespace pw
diff --git a/pw_toolchain/py/BUILD.gn b/pw_toolchain/py/BUILD.gn
index ca04dd6bb..cc8a554d3 100644
--- a/pw_toolchain/py/BUILD.gn
+++ b/pw_toolchain/py/BUILD.gn
@@ -31,4 +31,5 @@ pw_python_package("py") {
]
tests = [ "clang_tidy_test.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_toolchain/py/clang_tidy_test.py b/pw_toolchain/py/clang_tidy_test.py
index c6ac615d3..5ec5f304b 100644
--- a/pw_toolchain/py/clang_tidy_test.py
+++ b/pw_toolchain/py/clang_tidy_test.py
@@ -23,16 +23,27 @@ from pw_toolchain import clang_tidy
class ClangTidyTest(unittest.TestCase):
"""Unit tests for the clang-tidy wrapper."""
+
@mock.patch('subprocess.run', autospec=True)
def test_source_exclude_filters(self, mock_run):
# Build the path using joinpath to use OS-appropriate separators on both
# Windows and Linux.
- source_file = pathlib.Path('..').joinpath('third_party').joinpath(
- 'somefile.cc')
+ source_file = (
+ pathlib.Path('..').joinpath('third_party').joinpath('somefile.cc')
+ )
source_root = pathlib.Path('..')
source_exclude = ['third_party.*']
- got = clang_tidy.main(False, 'clang-tidy', source_file, source_root,
- None, source_exclude, list(), list())
+ extra_args = ['END_OF_INVOKER']
+ got = clang_tidy.main(
+ False,
+ 'clang-tidy',
+ source_file,
+ source_root,
+ None,
+ source_exclude,
+ list(),
+ extra_args,
+ )
# Return code is zero.
self.assertEqual(got, 0)
@@ -42,12 +53,22 @@ class ClangTidyTest(unittest.TestCase):
@mock.patch('subprocess.run', autospec=True)
def test_source_exclude_does_not_filter(self, mock_run):
mock_run.return_value.returncode = 0
- source_file = pathlib.Path('..').joinpath('third_party').joinpath(
- 'somefile.cc')
+ source_file = (
+ pathlib.Path('..').joinpath('third_party').joinpath('somefile.cc')
+ )
source_root = pathlib.Path('..')
source_exclude = ['someotherdir.*']
- got = clang_tidy.main(False, 'clang-tidy', source_file, source_root,
- None, source_exclude, list(), list())
+ extra_args = ['END_OF_INVOKER']
+ got = clang_tidy.main(
+ False,
+ 'clang-tidy',
+ source_file,
+ source_root,
+ None,
+ source_exclude,
+ list(),
+ extra_args,
+ )
# Return code is zero.
self.assertEqual(got, 0)
diff --git a/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py b/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
index 5c0cf61c0..5a02f3896 100644
--- a/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
+++ b/pw_toolchain/py/pw_toolchain/clang_arm_toolchain.py
@@ -49,14 +49,21 @@ def _parse_args() -> argparse.Namespace:
parser.add_argument(
'--gn-scope',
action='store_true',
- help=("Formats the output like a GN scope so it can be ingested by "
- "exec_script()"))
- parser.add_argument('--cflags',
- action='store_true',
- help=('Include necessary C flags in the output'))
- parser.add_argument('--ldflags',
- action='store_true',
- help=('Include necessary linker flags in the output'))
+ help=(
+ "Formats the output like a GN scope so it can be ingested by "
+ "exec_script()"
+ ),
+ )
+ parser.add_argument(
+ '--cflags',
+ action='store_true',
+ help=('Include necessary C flags in the output'),
+ )
+ parser.add_argument(
+ '--ldflags',
+ action='store_true',
+ help=('Include necessary linker flags in the output'),
+ )
parser.add_argument(
'clang_flags',
nargs=argparse.REMAINDER,
@@ -83,23 +90,28 @@ def _compiler_info_command(print_command: str, cflags: List[str]) -> str:
def get_gcc_lib_dir(cflags: List[str]) -> Path:
- return Path(_compiler_info_command('-print-libgcc-file-name',
- cflags)).parent
+ return Path(
+ _compiler_info_command('-print-libgcc-file-name', cflags)
+ ).parent
def get_compiler_info(cflags: List[str]) -> Dict[str, str]:
compiler_info: Dict[str, str] = {}
compiler_info['gcc_libs_dir'] = os.path.relpath(
- str(get_gcc_lib_dir(cflags)), ".")
+ str(get_gcc_lib_dir(cflags)), "."
+ )
compiler_info['sysroot'] = os.path.relpath(
- _compiler_info_command('-print-sysroot', cflags), ".")
+ _compiler_info_command('-print-sysroot', cflags), "."
+ )
compiler_info['version'] = _compiler_info_command('-dumpversion', cflags)
compiler_info['multi_dir'] = _compiler_info_command(
- '-print-multi-directory', cflags)
+ '-print-multi-directory', cflags
+ )
return compiler_info
def get_cflags(compiler_info: Dict[str, str]):
+ """TODO(amontanez): Add docstring."""
# TODO(amontanez): Make newlib-nano optional.
cflags = [
# TODO(amontanez): For some reason, -stdlib++-isystem and
@@ -108,24 +120,35 @@ def get_cflags(compiler_info: Dict[str, str]):
'-Qunused-arguments',
# Disable all default libraries.
"-nodefaultlibs",
- '--target=arm-none-eabi'
+ '--target=arm-none-eabi',
]
# Add sysroot info.
- cflags.extend((
- '--sysroot=' + compiler_info['sysroot'],
- '-isystem' +
- str(Path(compiler_info['sysroot']) / 'include' / 'newlib-nano'),
- # This must be included after Clang's builtin headers.
- '-isystem-after' + str(Path(compiler_info['sysroot']) / 'include'),
- '-stdlib++-isystem' + str(
- Path(compiler_info['sysroot']) / 'include' / 'c++' /
- compiler_info['version']),
- '-isystem' + str(
- Path(compiler_info['sysroot']) / 'include' / 'c++' /
- compiler_info['version'] / _ARM_COMPILER_PREFIX /
- compiler_info['multi_dir']),
- ))
+ cflags.extend(
+ (
+ '--sysroot=' + compiler_info['sysroot'],
+ '-isystem'
+ + str(Path(compiler_info['sysroot']) / 'include' / 'newlib-nano'),
+ # This must be included after Clang's builtin headers.
+ '-isystem-after' + str(Path(compiler_info['sysroot']) / 'include'),
+ '-stdlib++-isystem'
+ + str(
+ Path(compiler_info['sysroot'])
+ / 'include'
+ / 'c++'
+ / compiler_info['version']
+ ),
+ '-isystem'
+ + str(
+ Path(compiler_info['sysroot'])
+ / 'include'
+ / 'c++'
+ / compiler_info['version']
+ / _ARM_COMPILER_PREFIX
+ / compiler_info['multi_dir']
+ ),
+ )
+ )
return cflags
@@ -136,19 +159,22 @@ def get_crt_objs(compiler_info: Dict[str, str]) -> Tuple[str, ...]:
str(Path(compiler_info['gcc_libs_dir']) / 'crti.o'),
str(Path(compiler_info['gcc_libs_dir']) / 'crtn.o'),
str(
- Path(compiler_info['sysroot']) / 'lib' /
- compiler_info['multi_dir'] / 'crt0.o'),
+ Path(compiler_info['sysroot'])
+ / 'lib'
+ / compiler_info['multi_dir']
+ / 'crt0.o'
+ ),
)
def get_ldflags(compiler_info: Dict[str, str]) -> List[str]:
ldflags: List[str] = [
- '-lnosys',
# Add library search paths.
'-L' + compiler_info['gcc_libs_dir'],
- '-L' + str(
- Path(compiler_info['sysroot']) / 'lib' /
- compiler_info['multi_dir']),
+ '-L'
+ + str(
+ Path(compiler_info['sysroot']) / 'lib' / compiler_info['multi_dir']
+ ),
# Add libraries to link.
'-lc_nano',
'-lm',
diff --git a/pw_toolchain/py/pw_toolchain/clang_tidy.py b/pw_toolchain/py/pw_toolchain/clang_tidy.py
index fd7c625f0..868a68ac4 100644
--- a/pw_toolchain/py/pw_toolchain/clang_tidy.py
+++ b/pw_toolchain/py/pw_toolchain/clang_tidy.py
@@ -19,7 +19,7 @@ clang-tidy:
- add option `--source-exclude` to exclude matching sources from the
clang-tidy analysis.
- inputs the full compile command, with the cc binary name
- - TODO: infer platform options from the full compile command
+ - TODO(henrichataing): infer platform options from the full compile command
"""
import argparse
@@ -38,84 +38,111 @@ def _parse_args() -> argparse.Namespace:
"""Parses arguments for this script, splitting out the command to run."""
parser = argparse.ArgumentParser()
- parser.add_argument('-v',
- '--verbose',
- action='store_true',
- help='Run clang_tidy with extra debug output.')
+ parser.add_argument(
+ '-v',
+ '--verbose',
+ action='store_true',
+ help='Run clang_tidy with extra debug output.',
+ )
- parser.add_argument('--clang-tidy',
- default='clang-tidy',
- help='Path to clang-tidy executable.')
+ parser.add_argument(
+ '--clang-tidy',
+ default='clang-tidy',
+ help='Path to clang-tidy executable.',
+ )
parser.add_argument(
'--source-file',
required=True,
type=Path,
- help='Path to the source file to analyze with clang-tidy.')
+ help='Path to the source file to analyze with clang-tidy.',
+ )
parser.add_argument(
'--source-root',
required=True,
type=Path,
- help=('Path to the root source directory.'
- ' The relative path from the root directory is matched'
- ' against source filter rather than the absolute path.'))
+ help=(
+ 'Path to the root source directory.'
+ ' The relative path from the root directory is matched'
+ ' against source filter rather than the absolute path.'
+ ),
+ )
parser.add_argument(
'--export-fixes',
required=False,
type=Path,
- help=('YAML file to store suggested fixes in. The '
- 'stored fixes can be applied to the input source '
- 'code with clang-apply-replacements.'))
-
- parser.add_argument('--source-exclude',
- default=[],
- action='append',
- type=str,
- help=('Regular expressions matching the paths of'
- ' source files to be excluded from the'
- ' analysis.'))
+ help=(
+ 'YAML file to store suggested fixes in. The '
+ 'stored fixes can be applied to the input source '
+ 'code with clang-apply-replacements.'
+ ),
+ )
+
+ parser.add_argument(
+ '--source-exclude',
+ default=[],
+ action='append',
+ type=str,
+ help=(
+ 'Regular expressions matching the paths of'
+ ' source files to be excluded from the'
+ ' analysis.'
+ ),
+ )
parser.add_argument(
'--skip-include-path',
default=[],
- nargs='*',
- help=('Exclude include paths ending in these paths from clang-tidy. '
- 'These paths are switched from -I to -isystem so clang-tidy '
- 'ignores them.'))
+ action='append',
+ type=str,
+ help=(
+ 'Exclude include paths ending in these paths from clang-tidy. '
+ 'These paths are switched from -I to -isystem so clang-tidy '
+ 'ignores them.'
+ ),
+ )
# Add a silent placeholder arg for everything that was left over.
- parser.add_argument('extra_args',
- nargs=argparse.REMAINDER,
- help=argparse.SUPPRESS)
+ parser.add_argument(
+ 'extra_args', nargs=argparse.REMAINDER, help=argparse.SUPPRESS
+ )
parsed_args = parser.parse_args()
if parsed_args.extra_args[0] != '--':
parser.error('arguments not correctly split')
-
parsed_args.extra_args = parsed_args.extra_args[1:]
return parsed_args
-def _filter_include_paths(args: Iterable[str],
- skip_include_paths: Iterable[str]) -> Iterable[str]:
+def _filter_include_paths(
+ args: Iterable[str], skip_include_paths: Iterable[str]
+) -> Iterable[str]:
filters = [f.rstrip('/') for f in skip_include_paths]
for arg in args:
if arg.startswith('-I'):
path = Path(arg[2:]).as_posix()
- if any(
- path.endswith(f) or re.match(f, str(path))
- for f in filters):
+ if any(path.endswith(f) or re.match(f, str(path)) for f in filters):
yield '-isystem' + arg[2:]
continue
+ if arg.startswith('--sysroot'):
+ path = Path(arg[9:]).as_posix()
+ if any(path.endswith(f) or re.match(f, str(path)) for f in filters):
+ yield '-isysroot' + arg[9:]
+ continue
yield arg
-def run_clang_tidy(clang_tidy: str, verbose: bool, source_file: Path,
- export_fixes: Optional[Path], skip_include_path: List[str],
- extra_args: List[str]) -> int:
+def run_clang_tidy(
+ clang_tidy: str,
+ verbose: bool,
+ source_file: Path,
+ export_fixes: Optional[Path],
+ skip_include_path: List[str],
+ extra_args: List[str],
+) -> int:
"""Executes clang_tidy via subprocess. Returns true if no failures."""
command: List[Union[str, Path]] = [clang_tidy, source_file, '--use-color']
@@ -125,18 +152,23 @@ def run_clang_tidy(clang_tidy: str, verbose: bool, source_file: Path,
if export_fixes is not None:
command.extend(['--export-fixes', export_fixes])
- # Append extra compilation flags. extra_args[0] is skipped as it contains
- # the compiler binary name.
+ # Append extra compilation flags. Extra args up to
+ # END_OF_INVOKER are skipped.
command.append('--')
- command.extend(_filter_include_paths(extra_args[1:], skip_include_path))
+ end_of_invoker = extra_args.index('END_OF_INVOKER')
+ command.extend(
+ _filter_include_paths(
+ extra_args[end_of_invoker + 1 :], skip_include_path
+ )
+ )
process = subprocess.run(
command,
stdout=subprocess.PIPE,
# clang-tidy prints regular information on
# stderr, even with the option --quiet.
- stderr=subprocess.PIPE)
-
+ stderr=subprocess.PIPE,
+ )
if process.returncode != 0:
_LOG.warning('%s', ' '.join(shlex.quote(str(arg)) for arg in command))
@@ -172,10 +204,17 @@ def main(
return 0
source_file_path = source_file.resolve()
- export_fixes_path = (export_fixes.resolve()
- if export_fixes is not None else None)
- return run_clang_tidy(clang_tidy, verbose, source_file_path,
- export_fixes_path, skip_include_path, extra_args)
+ export_fixes_path = (
+ export_fixes.resolve() if export_fixes is not None else None
+ )
+ return run_clang_tidy(
+ clang_tidy,
+ verbose,
+ source_file_path,
+ export_fixes_path,
+ skip_include_path,
+ extra_args,
+ )
if __name__ == '__main__':
diff --git a/pw_toolchain/rbe.gni b/pw_toolchain/rbe.gni
new file mode 100644
index 000000000..a749c36de
--- /dev/null
+++ b/pw_toolchain/rbe.gni
@@ -0,0 +1,31 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+declare_args() {
+ # Sets the appropriate RBE variables based on environment variables.
+ if (getenv("PW_USE_RBE") == "true") {
+ pw_toolchain_USE_RBE = true
+ } else {
+ pw_toolchain_USE_RBE = false
+ }
+
+ if (getenv("PW_RBE_DEBUG") == "true") {
+ pw_toolchain_RBE_DEBUG = true
+ } else {
+ pw_toolchain_RBE_DEBUG = false
+ }
+
+ pw_rbe_clang_config = getenv("PW_RBE_CLANG_CONFIG")
+ pw_rbe_arm_gcc_config = getenv("PW_RBE_ARM_GCC_CONFIG")
+}
diff --git a/pw_toolchain/rust_toolchain.bzl b/pw_toolchain/rust_toolchain.bzl
new file mode 100644
index 000000000..32b5ce48b
--- /dev/null
+++ b/pw_toolchain/rust_toolchain.bzl
@@ -0,0 +1,48 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Utilities for declaring Rust toolchains that are compatible with @bazel_embedded"""
+
+load("@rules_rust//rust:toolchain.bzl", "rust_toolchain")
+
+def pw_rust_toolchain(name, exec_cpu, exec_os, target_cpu, rust_target_triple, exec_triple):
+ proxy_toolchain = "@rust_{}_{}__{}__stable_tools".format(exec_os, exec_cpu, rust_target_triple)
+
+ rust_toolchain(
+ name = "{}_impl".format(name),
+ binary_ext = "",
+ dylib_ext = ".so",
+ exec_triple = exec_triple,
+ os = "none",
+ rust_doc = "{}//:rustdoc".format(proxy_toolchain),
+ rust_std = "{}//:rust_std-{}".format(proxy_toolchain, rust_target_triple),
+ rustc = "{}//:rustc".format(proxy_toolchain),
+ rustc_lib = "{}//:rustc_lib".format(proxy_toolchain),
+ staticlib_ext = ".a",
+ stdlib_linkflags = [],
+ target_triple = rust_target_triple,
+ )
+
+ native.toolchain(
+ name = name,
+ exec_compatible_with = [
+ "@platforms//cpu:{}".format(exec_cpu),
+ "@platforms//os:{}".format(exec_os),
+ ],
+ target_compatible_with = [
+ "@platforms//cpu:{}".format(target_cpu),
+ "@bazel_embedded//constraints/fpu:none",
+ ],
+ toolchain = ":{}_impl".format(name),
+ toolchain_type = "@rules_rust//rust:toolchain",
+ )
diff --git a/pw_toolchain/static_analysis_toolchain.gni b/pw_toolchain/static_analysis_toolchain.gni
index 732f2094d..9aa0fa46b 100644
--- a/pw_toolchain/static_analysis_toolchain.gni
+++ b/pw_toolchain/static_analysis_toolchain.gni
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_compilation_testing/negative_compilation_test.gni")
import("$dir_pw_toolchain/universal_tools.gni")
declare_args() {
@@ -52,8 +53,28 @@ declare_args() {
# Args:
# cc: (required) String indicating the C compiler to use.
# cxx: (required) String indicating the C++ compiler to use.
+# static_analysis: (required) A scope defining args to apply to the
+# static_analysis toolchain.
+# static_analysis.enabled: (required) Bool used to indicate whether
+# static_analysis should be enabled for the toolchain where scope is
+# applied to. Note that static_analysis.enabled must be set in order to
+# use this toolchain.
+# static_analysis.clang_tidy_path: (optional) String indicating clang-tidy bin
+# to use.
+# static_analysis.cc_post: (optional) String defining additional commands to
+# append to cc tool's command list (i.e command(s) to run after cc command
+# chain).
+# static_analysis.cxx_post: (optional) String defining additional commands to
+# append to cxx tool's command list (i.e command(s) to run after cxx
+# command chain).
template("pw_static_analysis_toolchain") {
invoker_toolchain_args = invoker.defaults
+ assert(defined(invoker.static_analysis), "static_analysis scope missing.")
+ _static_analysis_args = invoker.static_analysis
+ assert(defined(_static_analysis_args.enabled),
+ "static_analysis.enabled is missing")
+ assert(_static_analysis_args.enabled,
+ "static_analysis.enabled must be true to use this toolchain.")
# Clang tidy is invoked by a wrapper script which implements the missing
# option --source-filter.
@@ -66,10 +87,17 @@ template("pw_static_analysis_toolchain") {
foreach(pattern, pw_toolchain_STATIC_ANALYSIS_SKIP_SOURCES_RES) {
_source_exclude = _source_exclude + " --source-exclude '${pattern}'"
}
-
- _skip_include_path =
- "--skip-include-path " +
- string_join(" ", pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS)
+ _skip_include_path = ""
+ foreach(pattern, pw_toolchain_STATIC_ANALYSIS_SKIP_INCLUDE_PATHS) {
+ _skip_include_path =
+ _skip_include_path + " --skip-include-path '${pattern}'"
+ }
+ _clang_tidy_path = ""
+ if (defined(_static_analysis_args.clang_tidy_path)) {
+ _clang_tidy_path =
+ "--clang-tidy " +
+ rebase_path(_static_analysis_args.clang_tidy_path, root_build_dir)
+ }
toolchain(target_name) {
# Uncomment this line to see which toolchains generate other toolchains.
@@ -90,17 +118,25 @@ template("pw_static_analysis_toolchain") {
assert(defined(invoker.cc), "toolchain is missing 'cc'")
tool("cc") {
+ _post_command_hook = ""
+ if (defined(_static_analysis_args.cc_post) &&
+ _static_analysis_args.cc_post != "") {
+ _post_command_hook += " && " + _static_analysis_args.cc_post
+ }
+
depfile = "{{output}}.d"
command = string_join(" ",
[
_clang_tidy_py,
_source_exclude,
_skip_include_path,
+ _clang_tidy_path,
"--source-file {{source}}",
"--source-root '${_source_root}'",
"--export-fixes {{output}}.yaml",
"--",
invoker.cc,
+ "END_OF_INVOKER",
"-MMD -MF $depfile", # Write out dependencies.
"{{cflags}}",
"{{cflags_c}}", # Must come after {{cflags}}.
@@ -108,7 +144,7 @@ template("pw_static_analysis_toolchain") {
"{{include_dirs}}",
"-c {{source}}",
"-o {{output}}",
- ]) + " && touch {{output}}"
+ ]) + " && touch {{output}}" + _post_command_hook
depsformat = "gcc"
description = "clang-tidy {{source}}"
outputs =
@@ -117,17 +153,25 @@ template("pw_static_analysis_toolchain") {
assert(defined(invoker.cxx), "toolchain is missing 'cxx'")
tool("cxx") {
+ _post_command_hook = ""
+ if (defined(_static_analysis_args.cxx_post) &&
+ _static_analysis_args.cxx_post != "") {
+ _post_command_hook += " && " + _static_analysis_args.cxx_post
+ }
+
depfile = "{{output}}.d"
command = string_join(" ",
[
_clang_tidy_py,
_source_exclude,
_skip_include_path,
+ _clang_tidy_path,
"--source-file {{source}}",
"--source-root '${_source_root}'",
"--export-fixes {{output}}.yaml",
"--",
invoker.cxx,
+ "END_OF_INVOKER",
"-MMD -MF $depfile", # Write out dependencies.
"{{cflags}}",
"{{cflags_cc}}", # Must come after {{cflags}}.
@@ -135,7 +179,7 @@ template("pw_static_analysis_toolchain") {
"{{include_dirs}}",
"-c {{source}}",
"-o {{output}}",
- ]) + " && touch {{output}}"
+ ]) + " && touch {{output}}" + _post_command_hook
depsformat = "gcc"
description = "clang-tidy {{source}}"
outputs =
@@ -208,6 +252,14 @@ template("pw_static_analysis_toolchain") {
forward_variables_from(_copy, "*")
}
+ tool("rust_bin") {
+ _output = "{{output_dir}}/{{target_output_name}}{{output_extension}}"
+ command = pw_universal_stamp.command
+ description = "rustc {{output}}"
+ outputs = [ _output ]
+ default_output_dir = "{{target_out_dir}}/bin"
+ }
+
# Build arguments to be overridden when compiling cross-toolchain:
#
# pw_toolchain_defaults: A scope setting defaults to apply to GN targets
@@ -225,6 +277,13 @@ template("pw_static_analysis_toolchain") {
name = target_name
}
forward_variables_from(invoker_toolchain_args, "*")
+
+ # Disable compilation testing for static analysis toolchains.
+ pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = false
+
+ # Always disable coverage generation since we will not actually run the
+ # instrumented binaries to produce a profraw file.
+ pw_toolchain_COVERAGE_ENABLED = false
}
}
}
diff --git a/pw_toolchain/traits.gni b/pw_toolchain/traits.gni
new file mode 100644
index 000000000..fee99dd23
--- /dev/null
+++ b/pw_toolchain/traits.gni
@@ -0,0 +1,57 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Constants that represent the different C++ standards. These are set to the
+# value of __cplusplus, which allows for unambiguous numeric comparison of C++
+# standards.
+pw_toolchain_STANDARD = {
+ CXX98 = 199711
+ CXX11 = 201103
+ CXX14 = 201402
+ CXX17 = 201703
+ CXX20 = 202002
+}
+
+if (defined(is_fuchsia_tree) && is_fuchsia_tree) {
+ # In the Fuchsia build, this must align with how code is actually being
+ # compiled in that build, which is controlled by Fuchsia's build argument.
+ import("//build/config/fuchsia_cxx_version.gni")
+
+ # This is not a GN build argument at all when Pigweed is integrated into the
+ # Fuchsia build, just a reflection of the Fuchsia build's configuration.
+ # **NOTE:** This line will blow up if the value isn't one of the supported
+ # ones listed above.
+ pw_toolchain_CXX_STANDARD = pw_toolchain_STANDARD["CXX$fuchsia_cxx_version"]
+} else {
+ declare_args() {
+ # Specifies the C++ standard this toolchain is compiling for. The value
+ # must be an integer that matches the value of the __cplusplus macro when
+ # compiling with that standard. Use the pw_toolchain_CXX_STANDARD_##
+ # constants to set this value.
+ pw_toolchain_CXX_STANDARD = pw_toolchain_STANDARD.CXX17
+ }
+}
+
+assert(
+ pw_toolchain_CXX_STANDARD == pw_toolchain_STANDARD.CXX98 ||
+ pw_toolchain_CXX_STANDARD == pw_toolchain_STANDARD.CXX11 ||
+ pw_toolchain_CXX_STANDARD == pw_toolchain_STANDARD.CXX14 ||
+ pw_toolchain_CXX_STANDARD == pw_toolchain_STANDARD.CXX17 ||
+ pw_toolchain_CXX_STANDARD == pw_toolchain_STANDARD.CXX20,
+ "pw_toolchain_CXX_STANDARD ($pw_toolchain_CXX_STANDARD) is an " +
+ "unsupported value. The toolchain must set it to one of the " +
+ "pw_toolchain_STANDARD constants (e.g. pw_toolchain_STANDARD.CXX17).")
+
+assert(pw_toolchain_CXX_STANDARD >= pw_toolchain_STANDARD.CXX14,
+ "Pigweed requires at least C++14.")
diff --git a/pw_toolchain/wrap_abort.cc b/pw_toolchain/wrap_abort.cc
new file mode 100644
index 000000000..b3d254c7c
--- /dev/null
+++ b/pw_toolchain/wrap_abort.cc
@@ -0,0 +1,26 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// This file provides an implementation of std::abort that can be used with ld's
+// --wrap option. This should be used for embedded tooclahisnt that don't have a
+// proper implementation of std::abort.
+
+#include "pw_assert/check.h"
+
+namespace pw::toolchain {
+
+// Redirect std::abort to PW_CRASH.
+extern "C" void __wrap_abort(void) { PW_CRASH("std::abort"); }
+
+} // namespace pw::toolchain
diff --git a/pw_trace/BUILD.bazel b/pw_trace/BUILD.bazel
index deeb15c5b..8f90e4ed3 100644
--- a/pw_trace/BUILD.bazel
+++ b/pw_trace/BUILD.bazel
@@ -15,6 +15,7 @@
load(
"//pw_build:pigweed.bzl",
"pw_cc_binary",
+ "pw_cc_facade",
"pw_cc_library",
"pw_cc_test",
)
@@ -23,9 +24,7 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
-# TODO(pwbug/101): Need to add support for facades/backends to Bazel.
-
-pw_cc_library(
+pw_cc_facade(
name = "facade",
hdrs = [
"public/pw_trace/internal/trace_internal.h",
@@ -41,11 +40,18 @@ pw_cc_library(
name = "pw_trace",
deps = [
":facade",
+ "@pigweed_config//:pw_trace_backend",
],
)
pw_cc_library(
- name = "null_headers",
+ name = "backend_multiplexer",
+ visibility = ["@pigweed_config//:__pkg__"],
+ deps = ["//pw_trace:null"],
+)
+
+pw_cc_library(
+ name = "null",
hdrs = [
"public/pw_trace/internal/null.h",
"public_overrides/pw_trace_backend/trace_backend.h",
@@ -56,22 +62,10 @@ pw_cc_library(
],
deps = [
"//pw_preprocessor",
- ],
-)
-
-pw_cc_library(
- name = "null",
- deps = [
"//pw_trace:facade",
- "//pw_trace:null_headers",
],
)
-pw_cc_library(
- name = "backend",
- deps = [],
-)
-
pw_cc_test(
name = "trace_backend_compile_test",
srcs = [
@@ -79,8 +73,6 @@ pw_cc_test(
"trace_backend_compile_test_c.c",
],
deps = [
- ":backend",
- ":facade",
":pw_trace",
"//pw_preprocessor",
"//pw_unit_test",
@@ -99,14 +91,30 @@ pw_cc_test(
"pw_trace_test/public_overrides",
],
deps = [
- ":backend",
- ":facade",
":pw_trace",
"//pw_preprocessor",
"//pw_unit_test",
],
)
+pw_cc_test(
+ name = "trace_zero_facade_test",
+ srcs = [
+ "pw_trace_zero/public_overrides/pw_trace_backend/trace_backend.h",
+ "trace_backend_compile_test.cc",
+ "trace_backend_compile_test_c.c",
+ ],
+ includes = [
+ "pw_trace_zero",
+ "pw_trace_zero/public_overrides",
+ ],
+ deps = [
+ ":facade",
+ "//pw_preprocessor",
+ "//pw_unit_test",
+ ],
+)
+
pw_cc_library(
name = "trace_null_test",
srcs = [
@@ -114,8 +122,6 @@ pw_cc_library(
"trace_null_test_c.c",
],
deps = [
- ":facade",
- ":null",
":pw_trace",
"//pw_preprocessor",
"//pw_unit_test",
@@ -127,12 +133,19 @@ pw_cc_library(
srcs = ["example/sample_app.cc"],
hdrs = ["example/public/pw_trace/example/sample_app.h"],
includes = ["example/public"],
- deps = ["//pw_trace"],
+ # TODO(b/258071921): Fix puzzling compiler errors
+ tags = ["manual"],
+ deps = [
+ "//pw_ring_buffer",
+ "//pw_trace",
+ ],
)
pw_cc_binary(
name = "trace_example_basic",
srcs = ["example/basic.cc"],
+ # TODO(b/258071921): Fix puzzling compiler errors
+ tags = ["manual"],
deps = [
":pw_trace_sample_app",
"//pw_log",
diff --git a/pw_trace/BUILD.gn b/pw_trace/BUILD.gn
index a62d1af46..e66a66cf7 100644
--- a/pw_trace/BUILD.gn
+++ b/pw_trace/BUILD.gn
@@ -16,6 +16,7 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/facade.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/facade_test.gni")
import("$dir_pw_unit_test/test.gni")
import("backend.gni")
@@ -53,6 +54,7 @@ pw_test_group("tests") {
tests = [
":trace_facade_test",
":trace_null_test",
+ ":trace_zero_backend_test",
]
if (pw_trace_BACKEND != "") {
tests += [ ":trace_backend_compile_test" ]
@@ -63,8 +65,6 @@ pw_test("trace_facade_test") {
configs = [ ":default_config" ]
sources = [ "trace_facade_test.cc" ]
public = [
- "public/pw_trace/internal/trace_internal.h",
- "public/pw_trace/trace.h",
"pw_trace_test/fake_backend.h",
"pw_trace_test/public_overrides/pw_trace_backend/trace_backend.h",
]
@@ -72,6 +72,7 @@ pw_test("trace_facade_test") {
"pw_trace_test",
"pw_trace_test/public_overrides",
]
+ deps = [ ":pw_trace" ]
}
pw_test("trace_backend_compile_test") {
@@ -88,6 +89,32 @@ pw_test("trace_backend_compile_test") {
]
}
+config("zero_config") {
+ include_dirs = [ "pw_trace_zero/public_overrides" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("zero") {
+ public_configs = [
+ ":default_config",
+ ":zero_config",
+ ]
+ public = [ "pw_trace_zero/public_overrides/pw_trace_backend/trace_backend.h" ]
+}
+
+pw_facade_test("trace_zero_backend_test") {
+ build_args = {
+ pw_trace_BACKEND = ":zero"
+ }
+
+ deps = [ ":pw_trace" ]
+
+ sources = [
+ "trace_backend_compile_test.cc",
+ "trace_backend_compile_test_c.c",
+ ]
+}
+
pw_test("trace_null_test") {
sources = [
"trace_null_test.cc",
diff --git a/pw_trace/CMakeLists.txt b/pw_trace/CMakeLists.txt
index c7df7377a..7d68e7259 100644
--- a/pw_trace/CMakeLists.txt
+++ b/pw_trace/CMakeLists.txt
@@ -13,8 +13,11 @@
# the License.
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+include($ENV{PW_ROOT}/pw_trace/backend.cmake)
-pw_add_facade(pw_trace
+pw_add_facade(pw_trace INTERFACE
+ BACKEND
+ pw_trace_BACKEND
HEADERS
public/pw_trace/internal/trace_internal.h
public/pw_trace/trace.h
@@ -24,9 +27,7 @@ pw_add_facade(pw_trace
pw_preprocessor
)
-pw_add_module_library(pw_trace.null
- IMPLEMENTS_FACADES
- pw_trace
+pw_add_library(pw_trace.null INTERFACE
HEADERS
public/pw_trace/internal/null.h
public_overrides/pw_trace_backend/trace_backend.h
@@ -41,19 +42,19 @@ pw_add_test(pw_trace.pw_trace_null_test
SOURCES
trace_null_test.cc
trace_null_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_trace.null
GROUPS
modules
pw_trace
)
-if(NOT "${pw_trace_BACKEND}" STREQUAL "pw_trace.NO_BACKEND_SET")
+if(NOT "${pw_trace_BACKEND}" STREQUAL "")
pw_add_test(pw_trace.trace_backend_compile_test
SOURCES
trace_backend_compile_test.cc
trace_backend_compile_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_trace
GROUPS
modules
diff --git a/pw_trace/backend.cmake b/pw_trace/backend.cmake
new file mode 100644
index 000000000..01af1046e
--- /dev/null
+++ b/pw_trace/backend.cmake
@@ -0,0 +1,19 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+# Default backend for the pw_trace module is pw_trace:null.
+pw_add_backend_variable(pw_trace_BACKEND DEFAULT_BACKEND pw_trace.null)
diff --git a/pw_trace/docs.rst b/pw_trace/docs.rst
index 51171ee3a..cfc46586f 100644
--- a/pw_trace/docs.rst
+++ b/pw_trace/docs.rst
@@ -199,6 +199,24 @@ Currently the included python tool supports a few different options for
can be used to either provide a single value type, or provide multiple
different values with a variety of types. Options for format string types can
be found here: https://docs.python.org/3/library/struct.html#format-characters
+ . The data is always assumed to be packed with little-endian ordering if not
+ indicated otherwise::
+
+ // Example
+ data_format_string = "@pw_py_struct_fmt:ll"
+ data = 0x1400000014000000
+ args = {data_0: 20, data_1:20}
+- *@pw_py_map_fmt:* - Interprets the string after the ":" as a dictionary
+ relating the data field name to the python struct format string. Once
+ collected, the format strings are concatenated and used to unpack the data
+ elements as above. The data is always assumed to be packed with little-endian
+ ordering if not indicated otherwise. To specify a different ordering,
+ construct the format string as ``@pw_py_map_fmt:[@=<>!]{k:v,...}``::
+
+ // Example
+ data_format_string = "@pw_py_map_fmt:{Field: l, Field2: l }"
+ data = 0x1400000014000000
+ args = {Field: 20, Field2:20}
.. tip::
@@ -220,6 +238,42 @@ label. It still can optionally be provided a *group_id*.
.. cpp:function:: PW_TRACE_FUNCTION()
.. cpp:function:: PW_TRACE_FUNCTION(group_label)
+Compile time enabling/disabling
+-------------------------------
+Traces in a file can be enabled/disabled at compile time by defining through
+the ``PW_TRACE_ENABLE`` macro. A value of 0 causes traces to be disabled.
+A non-zero value will enable traces. While tracing defaults to enabled,
+it is best practice to define ``PW_TRACE_ENABLE`` explicitly in files that
+use tracing as the default may change in the future.
+
+A good pattern is to have a module level configuration parameter for enabling
+tracing and define ``PW_TRACE_ENABLE`` in terms of that at the top of each
+of the module's files:
+
+
+.. code-block:: cpp
+
+ // Enable tracing based on pw_example module config parameter.
+ #define PW_TRACE_ENABLE PW_EXAMPLE_TRACE_ENABLE
+
+
+Additionally specific trace points (or sets of points) can be enabled/disabled
+using the following pattern:
+
+.. code-block:: cpp
+
+ // Assuming tracing is disabled at the top of the file.
+
+ // Enable specific trace.
+ #undef PW_TRACE_ENABLE
+ #define PW_TRACE_ENABLE 1
+ PW_TRACE_INSTANT("important trace");
+
+ // Set traces back to disabled. PW_TRACE_ENABLE can not be left
+ // undefined.
+ #undef PW_TRACE_ENABLE
+ #define PW_TRACE_ENABLE 0
+
-----------
Backend API
-----------
diff --git a/pw_trace/example/sample_app.cc b/pw_trace/example/sample_app.cc
index aad177136..016f7787d 100644
--- a/pw_trace/example/sample_app.cc
+++ b/pw_trace/example/sample_app.cc
@@ -76,10 +76,10 @@ class ProcessingTask : public SimpleRunnable {
};
ProcessingTask() {
// Buffer is used for the job queue.
- std::span<std::byte> buf_span = std::span<std::byte>(
+ pw::span<std::byte> buf_span = pw::span<std::byte>(
reinterpret_cast<std::byte*>(jobs_buffer_), sizeof(jobs_buffer_));
jobs_.SetBuffer(buf_span)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
const char* Name() const override { return "Processing Task"; }
bool ShouldRun() override { return jobs_.EntryCount() > 0; }
@@ -92,8 +92,9 @@ class ProcessingTask : public SimpleRunnable {
// Get the next job from the queue.
jobs_.PeekFront(job_bytes.bytes, &bytes_read)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- jobs_.PopFront().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
+ jobs_.PopFront()
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
Job& job = job_bytes.job;
// Process the job
@@ -138,7 +139,7 @@ class ProcessingTask : public SimpleRunnable {
void AddJobInternal(uint32_t job_id, uint8_t value) {
JobBytes job{.job = {.job_id = job_id, .value = value}};
jobs_.PushBack(job.bytes)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
} processing_task;
diff --git a/pw_trace/public/pw_trace/internal/trace_internal.h b/pw_trace/public/pw_trace/internal/trace_internal.h
index 18e51a729..982765f99 100644
--- a/pw_trace/public/pw_trace/internal/trace_internal.h
+++ b/pw_trace/public/pw_trace/internal/trace_internal.h
@@ -49,9 +49,12 @@
// Default: behaviour for unimplemented trace event types
#ifndef _PW_TRACE_DISABLED
-#define _PW_TRACE_DISABLED(...) \
- do { \
- } while (0)
+static inline void _pw_trace_disabled(int x, ...) { (void)x; }
+// `_PW_TRACE_DISABLED` must be called with at least one arg.
+#define _PW_TRACE_DISABLED(...) \
+ do { \
+ _pw_trace_disabled(0, ##__VA_ARGS__); \
+ } while (false)
#endif // _PW_TRACE_DISABLED
// Default: label used for PW_TRACE_FUNCTION trace events
@@ -59,18 +62,53 @@
#define PW_TRACE_FUNCTION_LABEL __PRETTY_FUNCTION__
#endif
+// Control to enable/disable tracing. If 0, no traces are emitted.
+//
+// Defaults to enabled.
+#ifndef PW_TRACE_ENABLE
+#define PW_TRACE_ENABLE 1
+#endif // PW_TRACE_ENABLE
+
+#define _PW_TRACE_IF_ENABLED(event_type, flags, label, group_label, trace_id) \
+ do { \
+ if ((PW_TRACE_ENABLE) != 0) { \
+ PW_TRACE(event_type, flags, label, group_label, trace_id); \
+ } \
+ } while (0)
+
+#define _PW_TRACE_DATA_IF_ENABLED(event_type, \
+ flags, \
+ label, \
+ group_label, \
+ trace_id, \
+ data_format_string, \
+ data, \
+ size) \
+ do { \
+ if ((PW_TRACE_ENABLE) != 0) { \
+ PW_TRACE_DATA(event_type, \
+ flags, \
+ label, \
+ group_label, \
+ trace_id, \
+ data_format_string, \
+ data, \
+ size); \
+ } \
+ } while (0)
+
// This block handles:
// - PW_TRACE_INSTANT(label)
// - PW_TRACE_INSTANT_FLAG(flag, label)
// Which creates a trace event with the type: PW_TRACE_TYPE_INSTANT
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_INSTANT
-#define _PW_TRACE_INSTANT_ARGS2(flag, label) \
- PW_TRACE(PW_TRACE_TYPE_INSTANT, \
- flag, \
- label, \
- PW_TRACE_GROUP_LABEL_DEFAULT, \
- PW_TRACE_TRACE_ID_DEFAULT)
+#define _PW_TRACE_INSTANT_ARGS2(flag, label) \
+ _PW_TRACE_IF_ENABLED(PW_TRACE_TYPE_INSTANT, \
+ flag, \
+ label, \
+ PW_TRACE_GROUP_LABEL_DEFAULT, \
+ PW_TRACE_TRACE_ID_DEFAULT)
#else // PW_TRACE_TYPE_INSTANT
#define _PW_TRACE_INSTANT_ARGS2(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_INSTANT
@@ -82,11 +120,11 @@
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_INSTANT_GROUP
#define _PW_TRACE_INSTANT_ARGS3(flag, label, group) \
- PW_TRACE(PW_TRACE_TYPE_INSTANT_GROUP, \
- flag, \
- label, \
- group, \
- PW_TRACE_TRACE_ID_DEFAULT)
+ _PW_TRACE_IF_ENABLED(PW_TRACE_TYPE_INSTANT_GROUP, \
+ flag, \
+ label, \
+ group, \
+ PW_TRACE_TRACE_ID_DEFAULT)
#else // PW_TRACE_TYPE_INSTANT_GROUP
#define _PW_TRACE_INSTANT_ARGS3(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_INSTANT_GROUP
@@ -98,7 +136,8 @@
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_ASYNC_INSTANT
#define _PW_TRACE_INSTANT_ARGS4(flag, label, group, trace_id) \
- PW_TRACE(PW_TRACE_TYPE_ASYNC_INSTANT, flag, label, group, trace_id)
+ _PW_TRACE_IF_ENABLED( \
+ PW_TRACE_TYPE_ASYNC_INSTANT, flag, label, group, trace_id)
#else // PW_TRACE_TYPE_ASYNC_INSTANT
#define _PW_TRACE_INSTANT_ARGS4(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_ASYNC_INSTANT
@@ -109,12 +148,12 @@
// Which creates a trace event with the type: PW_TRACE_TYPE_DURATION_START
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_DURATION_START
-#define _PW_TRACE_START_ARGS2(flag, label) \
- PW_TRACE(PW_TRACE_TYPE_DURATION_START, \
- flag, \
- label, \
- PW_TRACE_GROUP_LABEL_DEFAULT, \
- PW_TRACE_TRACE_ID_DEFAULT)
+#define _PW_TRACE_START_ARGS2(flag, label) \
+ _PW_TRACE_IF_ENABLED(PW_TRACE_TYPE_DURATION_START, \
+ flag, \
+ label, \
+ PW_TRACE_GROUP_LABEL_DEFAULT, \
+ PW_TRACE_TRACE_ID_DEFAULT)
#else // PW_TRACE_TYPE_DURATION_START
#define _PW_TRACE_START_ARGS2(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_DURATION_START
@@ -126,12 +165,12 @@
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_DURATION_GROUP_START // Disabled if backend doesn't define
// this
-#define _PW_TRACE_START_ARGS3(flag, label, group) \
- PW_TRACE(PW_TRACE_TYPE_DURATION_GROUP_START, \
- flag, \
- label, \
- group, \
- PW_TRACE_TRACE_ID_DEFAULT)
+#define _PW_TRACE_START_ARGS3(flag, label, group) \
+ _PW_TRACE_IF_ENABLED(PW_TRACE_TYPE_DURATION_GROUP_START, \
+ flag, \
+ label, \
+ group, \
+ PW_TRACE_TRACE_ID_DEFAULT)
#else // PW_TRACE_TYPE_DURATION_GROUP_START
#define _PW_TRACE_START_ARGS3(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_DURATION_GROUP_START
@@ -143,7 +182,7 @@
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_ASYNC_START
#define _PW_TRACE_START_ARGS4(flag, label, group, trace_id) \
- PW_TRACE(PW_TRACE_TYPE_ASYNC_START, flag, label, group, trace_id)
+ _PW_TRACE_IF_ENABLED(PW_TRACE_TYPE_ASYNC_START, flag, label, group, trace_id)
#else // PW_TRACE_TYPE_ASYNC_START
#define _PW_TRACE_START_ARGS4(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_ASYNC_START
@@ -154,12 +193,12 @@
// Which creates a trace event with the type: PW_TRACE_TYPE_DURATION_END
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_DURATION_END
-#define _PW_TRACE_END_ARGS2(flag, label) \
- PW_TRACE(PW_TRACE_TYPE_DURATION_END, \
- flag, \
- label, \
- PW_TRACE_GROUP_LABEL_DEFAULT, \
- PW_TRACE_TRACE_ID_DEFAULT)
+#define _PW_TRACE_END_ARGS2(flag, label) \
+ _PW_TRACE_IF_ENABLED(PW_TRACE_TYPE_DURATION_END, \
+ flag, \
+ label, \
+ PW_TRACE_GROUP_LABEL_DEFAULT, \
+ PW_TRACE_TRACE_ID_DEFAULT)
#else // PW_TRACE_TYPE_DURATION_END
#define _PW_TRACE_END_ARGS2(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_DURATION_END
@@ -170,12 +209,12 @@
// Which creates a trace event with the type: PW_TRACE_TYPE_DURATION_GROUP_END
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_DURATION_GROUP_END
-#define _PW_TRACE_END_ARGS3(flag, label, group) \
- PW_TRACE(PW_TRACE_TYPE_DURATION_GROUP_END, \
- flag, \
- label, \
- group, \
- PW_TRACE_TRACE_ID_DEFAULT)
+#define _PW_TRACE_END_ARGS3(flag, label, group) \
+ _PW_TRACE_IF_ENABLED(PW_TRACE_TYPE_DURATION_GROUP_END, \
+ flag, \
+ label, \
+ group, \
+ PW_TRACE_TRACE_ID_DEFAULT)
#else // PW_TRACE_TYPE_DURATION_GROUP_END
#define _PW_TRACE_END_ARGS3(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_DURATION_GROUP_END
@@ -187,7 +226,7 @@
// NOTE: If this type is not defined by the backend this trace is removed.
#ifdef PW_TRACE_TYPE_ASYNC_END
#define _PW_TRACE_END_ARGS4(flag, label, group, trace_id) \
- PW_TRACE(PW_TRACE_TYPE_ASYNC_END, flag, label, group, trace_id)
+ _PW_TRACE_IF_ENABLED(PW_TRACE_TYPE_ASYNC_END, flag, label, group, trace_id)
#else // PW_TRACE_TYPE_ASYNC_END
#define _PW_TRACE_END_ARGS4(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // PW_TRACE_TYPE_ASYNC_END
@@ -195,20 +234,26 @@
// The pigweed scope objects gets defined inline with the trace event. The
// constructor handles the start trace event, and the destructor does the end.
#ifndef _PW_TRACE_SCOPE_OBJECT
-#define _PW_TRACE_SCOPE_OBJECT( \
- object_name, flag, event_type_start, event_type_end, label, group) \
- class object_name { \
- public: \
- object_name(uint32_t trace_id = PW_TRACE_TRACE_ID_DEFAULT) \
- : trace_id_(trace_id) { \
- PW_TRACE(event_type_start, flag, label, group, trace_id_); \
- } \
- ~object_name() { \
- PW_TRACE(event_type_end, flag, label, group, trace_id_); \
- } \
- \
- private: \
- const uint32_t trace_id_; \
+#define _PW_TRACE_SCOPE_OBJECT( \
+ object_name, flag, event_type_start, event_type_end, label, group) \
+ class object_name { \
+ public: \
+ object_name(const object_name&) = delete; \
+ object_name(object_name&&) = delete; \
+ object_name& operator=(const object_name&) = delete; \
+ object_name& operator=(object_name&&) = delete; \
+ \
+ object_name(uint32_t PW_CONCAT(object_name, \
+ _trace_id) = PW_TRACE_TRACE_ID_DEFAULT) \
+ : trace_id_(PW_CONCAT(object_name, _trace_id)) { \
+ _PW_TRACE_IF_ENABLED(event_type_start, flag, label, group, trace_id_); \
+ } \
+ ~object_name() { \
+ _PW_TRACE_IF_ENABLED(event_type_end, flag, label, group, trace_id_); \
+ } \
+ \
+ private: \
+ const uint32_t trace_id_; \
}
#endif // _PW_TRACE_SCOPE_OBJECT
@@ -234,6 +279,11 @@
_PW_TRACE_SCOPE_ARGS2(PW_TRACE_FLAGS, PW_TRACE_FUNCTION_LABEL)
#define _PW_TRACE_FUNCTION_FLAGS_ARGS1(flag) \
_PW_TRACE_SCOPE_ARGS2(flag, PW_TRACE_FUNCTION_LABEL)
+#else // PW_TRACE_TYPE_DURATION_GROUP_END
+#define _PW_TRACE_SCOPE_ARGS2(...) _PW_TRACE_DISABLED(__VA_ARGS__)
+#define _PW_TRACE_FUNCTION_ARGS0() // No need to/can't call _PW_TRACE_DISABLED
+ // with zero args.
+#define _PW_TRACE_FUNCTION_FLAGS_ARGS1(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_TYPE_DURATION_START) &&
// defined(PW_TRACE_TYPE_DURATION_END)
@@ -260,6 +310,10 @@
_PW_TRACE_SCOPE_ARGS3(PW_TRACE_FLAGS, PW_TRACE_FUNCTION_LABEL, group)
#define _PW_TRACE_FUNCTION_FLAGS_ARGS2(flag, group) \
_PW_TRACE_SCOPE_ARGS3(flag, PW_TRACE_FUNCTION_LABEL, group)
+#else // PW_TRACE_TYPE_DURATION_GROUP_END
+#define _PW_TRACE_SCOPE_ARGS3(...) _PW_TRACE_DISABLED(__VA_ARGS__)
+#define _PW_TRACE_FUNCTION_ARGS1(...) _PW_TRACE_DISABLED(__VA_ARGS__)
+#define _PW_TRACE_FUNCTION_FLAGS_ARGS2(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_TYPE_DURATION_GROUP_START) &&
// defined(PW_TRACE_TYPE_DURATION_GROUP_END)
@@ -286,6 +340,10 @@
PW_TRACE_FLAGS, PW_TRACE_FUNCTION_LABEL, group, trace_id)
#define _PW_TRACE_FUNCTION_FLAGS_ARGS3(flag, group, trace_id) \
_PW_TRACE_SCOPE_ARGS4(flag, PW_TRACE_FUNCTION_LABEL, group, trace_id)
+#else // PW_TRACE_TYPE_DURATION_GROUP_END
+#define _PW_TRACE_SCOPE_ARGS4(...) _PW_TRACE_DISABLED(__VA_ARGS__)
+#define _PW_TRACE_FUNCTION_ARGS2(...) _PW_TRACE_DISABLED(__VA_ARGS__)
+#define _PW_TRACE_FUNCTION_FLAGS_ARGS3(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_TYPE_ASYNC_START) &&
// defined(PW_TRACE_TYPE_ASYNC_END)
@@ -303,16 +361,16 @@
// NOTE: If this type or PW_TRACE_DATA is not defined by the backend this trace
// is removed.
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_INSTANT)
-#define _PW_TRACE_INSTANT_DATA_ARGS5( \
- flag, label, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_INSTANT, \
- flag, \
- label, \
- PW_TRACE_GROUP_LABEL_DEFAULT, \
- PW_TRACE_TRACE_ID_DEFAULT, \
- data_format_string, \
- data, \
- size)
+#define _PW_TRACE_INSTANT_DATA_ARGS5( \
+ flag, label, data_format_string, data, size) \
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_INSTANT, \
+ flag, \
+ label, \
+ PW_TRACE_GROUP_LABEL_DEFAULT, \
+ PW_TRACE_TRACE_ID_DEFAULT, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_INSTANT)
#define _PW_TRACE_INSTANT_DATA_ARGS5(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_INSTANT)
@@ -333,16 +391,16 @@
// NOTE: If this type or PW_TRACE_DATA is not defined by the backend this trace
// is removed.
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_INSTANT_GROUP)
-#define _PW_TRACE_INSTANT_DATA_ARGS6( \
- flag, label, group, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_INSTANT_GROUP, \
- flag, \
- label, \
- group, \
- PW_TRACE_TRACE_ID_DEFAULT, \
- data_format_string, \
- data, \
- size)
+#define _PW_TRACE_INSTANT_DATA_ARGS6( \
+ flag, label, group, data_format_string, data, size) \
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_INSTANT_GROUP, \
+ flag, \
+ label, \
+ group, \
+ PW_TRACE_TRACE_ID_DEFAULT, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_INSTANT_GROUP)
#define _PW_TRACE_INSTANT_DATA_ARGS6(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_INSTANT_GROUP)
@@ -367,14 +425,14 @@
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_INSTANT)
#define _PW_TRACE_INSTANT_DATA_ARGS7( \
flag, label, group, trace_id, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_ASYNC_INSTANT, \
- flag, \
- label, \
- group, \
- trace_id, \
- data_format_string, \
- data, \
- size)
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_ASYNC_INSTANT, \
+ flag, \
+ label, \
+ group, \
+ trace_id, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_INSTANT)
#define _PW_TRACE_INSTANT_DATA_ARGS7(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_INSTANT)
@@ -393,16 +451,16 @@
// NOTE: If this type or PW_TRACE_DATA is not defined by the backend this trace
// is removed.
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_START)
-#define _PW_TRACE_START_DATA_ARGS5( \
- flag, label, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_DURATION_START, \
- flag, \
- label, \
- PW_TRACE_GROUP_LABEL_DEFAULT, \
- PW_TRACE_TRACE_ID_DEFAULT, \
- data_format_string, \
- data, \
- size)
+#define _PW_TRACE_START_DATA_ARGS5( \
+ flag, label, data_format_string, data, size) \
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_DURATION_START, \
+ flag, \
+ label, \
+ PW_TRACE_GROUP_LABEL_DEFAULT, \
+ PW_TRACE_TRACE_ID_DEFAULT, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_START)
#define _PW_TRACE_START_DATA_ARGS5(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_START)
@@ -423,16 +481,16 @@
// NOTE: If this type or PW_TRACE_DATA is not defined by the backend this trace
// is removed.
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_GROUP_START)
-#define _PW_TRACE_START_DATA_ARGS6( \
- flag, label, group, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_DURATION_GROUP_START, \
- flag, \
- label, \
- group, \
- PW_TRACE_TRACE_ID_DEFAULT, \
- data_format_string, \
- data, \
- size)
+#define _PW_TRACE_START_DATA_ARGS6( \
+ flag, label, group, data_format_string, data, size) \
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_DURATION_GROUP_START, \
+ flag, \
+ label, \
+ group, \
+ PW_TRACE_TRACE_ID_DEFAULT, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_START)
#define _PW_TRACE_START_DATA_ARGS6(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_START)
@@ -457,14 +515,14 @@
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_START)
#define _PW_TRACE_START_DATA_ARGS7( \
flag, label, group, trace_id, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_ASYNC_START, \
- flag, \
- label, \
- group, \
- trace_id, \
- data_format_string, \
- data, \
- size)
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_ASYNC_START, \
+ flag, \
+ label, \
+ group, \
+ trace_id, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_START)
#define _PW_TRACE_START_DATA_ARGS7(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_START)
@@ -484,14 +542,14 @@
// is removed.
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_END)
#define _PW_TRACE_END_DATA_ARGS5(flag, label, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_DURATION_END, \
- flag, \
- label, \
- PW_TRACE_GROUP_LABEL_DEFAULT, \
- PW_TRACE_TRACE_ID_DEFAULT, \
- data_format_string, \
- data, \
- size)
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_DURATION_END, \
+ flag, \
+ label, \
+ PW_TRACE_GROUP_LABEL_DEFAULT, \
+ PW_TRACE_TRACE_ID_DEFAULT, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_START)
#define _PW_TRACE_END_DATA_ARGS5(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_START)
@@ -512,16 +570,16 @@
// NOTE: If this type or PW_TRACE_DATA is not defined by the backend this trace
// is removed.
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_GROUP_END)
-#define _PW_TRACE_END_DATA_ARGS6( \
- flag, label, group, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_DURATION_GROUP_END, \
- flag, \
- label, \
- group, \
- PW_TRACE_TRACE_ID_DEFAULT, \
- data_format_string, \
- data, \
- size)
+#define _PW_TRACE_END_DATA_ARGS6( \
+ flag, label, group, data_format_string, data, size) \
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_DURATION_GROUP_END, \
+ flag, \
+ label, \
+ group, \
+ PW_TRACE_TRACE_ID_DEFAULT, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_GROUP_END)
#define _PW_TRACE_END_DATA_ARGS6(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_DURATION_GROUP_END)
@@ -546,14 +604,14 @@
#if defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_END)
#define _PW_TRACE_END_DATA_ARGS7( \
flag, label, group, trace_id, data_format_string, data, size) \
- PW_TRACE_DATA(PW_TRACE_TYPE_ASYNC_END, \
- flag, \
- label, \
- group, \
- trace_id, \
- data_format_string, \
- data, \
- size)
+ _PW_TRACE_DATA_IF_ENABLED(PW_TRACE_TYPE_ASYNC_END, \
+ flag, \
+ label, \
+ group, \
+ trace_id, \
+ data_format_string, \
+ data, \
+ size)
#else // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_END)
#define _PW_TRACE_END_DATA_ARGS7(...) _PW_TRACE_DISABLED(__VA_ARGS__)
#endif // defined(PW_TRACE_DATA) && defined(PW_TRACE_TYPE_ASYNC_END)
diff --git a/pw_trace/pw_trace_zero/public_overrides/pw_trace_backend/trace_backend.h b/pw_trace/pw_trace_zero/public_overrides/pw_trace_backend/trace_backend.h
new file mode 100644
index 000000000..a79d49b99
--- /dev/null
+++ b/pw_trace/pw_trace_zero/public_overrides/pw_trace_backend/trace_backend.h
@@ -0,0 +1,20 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+//==============================================================================
+//
+#pragma once
+
+// No backend is included so that all backend macros are left undefined. This
+// is primarily used to test that `trace.h` compiles without warnings when
+// using a backend that does not implement certain macros.
diff --git a/pw_trace/py/BUILD.gn b/pw_trace/py/BUILD.gn
index a445dd94f..dd50814ce 100644
--- a/pw_trace/py/BUILD.gn
+++ b/pw_trace/py/BUILD.gn
@@ -28,4 +28,5 @@ pw_python_package("py") {
]
tests = [ "trace_test.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_trace/py/pw_trace/trace.py b/pw_trace/py/pw_trace/trace.py
index 03b00c25f..571d66f0c 100755
--- a/pw_trace/py/pw_trace/trace.py
+++ b/pw_trace/py/pw_trace/trace.py
@@ -28,6 +28,7 @@ import struct
from typing import Iterable, NamedTuple
_LOG = logging.getLogger('pw_trace')
+_ORDERING_CHARS = ("@", "=", "<", ">", "!")
class TraceType(Enum):
@@ -52,7 +53,7 @@ class TraceEvent(NamedTuple):
event_type: TraceType
module: str
label: str
- timestamp_us: int
+ timestamp_us: float
group: str = ""
trace_id: int = 0
flags: int = 0
@@ -69,19 +70,83 @@ def event_has_trace_id(event_type):
}
+def decode_struct_fmt_args(event):
+ """Decodes the trace's event data for struct-formatted data"""
+ args = {}
+ # we assume all data is packed, little-endian ordering if not specified
+ struct_fmt = event.data_fmt[len("@pw_py_struct_fmt:") :]
+ if not struct_fmt.startswith(_ORDERING_CHARS):
+ struct_fmt = "<" + struct_fmt
+ try:
+ # needed in case the buffer is larger than expected
+ assert struct.calcsize(struct_fmt) == len(event.data)
+ items = struct.unpack_from(struct_fmt, event.data)
+ for i, item in enumerate(items):
+ args["data_" + str(i)] = item
+ except (AssertionError, struct.error):
+ args["error"] = (
+ f"Mismatched struct/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize(struct_fmt)} "
+ f"data {event.data.hex()} "
+ f"data len {len(event.data)}"
+ )
+ return args
+
+
+def decode_map_fmt_args(event):
+ """Decodes the trace's event data for map-formatted data"""
+ args = {}
+ fmt = event.data_fmt[len("@pw_py_map_fmt:") :]
+
+ # we assume all data is packed, little-endian ordering if not specified
+ if not fmt.startswith(_ORDERING_CHARS):
+ fmt = '<' + fmt
+
+ try:
+ (fmt_bytes, fmt_list) = fmt.split("{")
+ fmt_list = fmt_list.strip("}").split(",")
+
+ names = []
+ for pair in fmt_list:
+ (name, fmt_char) = (s.strip() for s in pair.split(":"))
+ names.append(name)
+ fmt_bytes += fmt_char
+ except ValueError:
+ args["error"] = f"Invalid map format {event.data_fmt}"
+ else:
+ try:
+ # needed in case the buffer is larger than expected
+ assert struct.calcsize(fmt_bytes) == len(event.data)
+ items = struct.unpack_from(fmt_bytes, event.data)
+ for i, item in enumerate(items):
+ args[names[i]] = item
+ except (AssertionError, struct.error):
+ args["error"] = (
+ f"Mismatched map/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize(fmt_bytes)} "
+ f"data {event.data.hex()} "
+ f"data len {len(event.data)}"
+ )
+ return args
+
+
def generate_trace_json(events: Iterable[TraceEvent]):
"""Generates a list of JSON lines from provided trace events."""
json_lines = []
for event in events:
- if event.module is None or event.timestamp_us is None or \
- event.event_type is None or event.label is None:
+ if (
+ event.module is None
+ or event.timestamp_us is None
+ or event.event_type is None
+ or event.label is None
+ ):
_LOG.error("Invalid sample")
continue
line = {
"pid": event.module,
"name": (event.label),
- "ts": event.timestamp_us
+ "ts": event.timestamp_us,
}
if event.event_type == TraceType.DURATION_START:
line["ph"] = "B"
@@ -139,12 +204,9 @@ def generate_trace_json(events: Iterable[TraceEvent]):
line["name"]: int.from_bytes(event.data, "little")
}
elif event.data_fmt.startswith("@pw_py_struct_fmt:"):
- items = struct.unpack_from(
- event.data_fmt[len("@pw_py_struct_fmt:"):], event.data)
- args = {}
- for i, item in enumerate(items):
- args["data_" + str(i)] = item
- line["args"] = args
+ line["args"] = decode_struct_fmt_args(event)
+ elif event.data_fmt.startswith("@pw_py_map_fmt:"):
+ line["args"] = decode_map_fmt_args(event)
else:
line["args"] = {"data": event.data.hex()}
diff --git a/pw_trace/py/trace_test.py b/pw_trace/py/trace_test.py
index 6cd2bcc50..60249623d 100755
--- a/pw_trace/py/trace_test.py
+++ b/pw_trace/py/trace_test.py
@@ -25,46 +25,72 @@ test_events = [
trace.TraceEvent(trace.TraceType.INSTANTANEOUS_GROUP, "m2", "L2", 2, "G2"),
trace.TraceEvent(trace.TraceType.ASYNC_STEP, "m3", "L3", 3, "G3", 103),
trace.TraceEvent(trace.TraceType.DURATION_START, "m4", "L4", 4),
- trace.TraceEvent(trace.TraceType.DURATION_GROUP_START, "m5", "L5", 5,
- "G5"),
+ trace.TraceEvent(trace.TraceType.DURATION_GROUP_START, "m5", "L5", 5, "G5"),
trace.TraceEvent(trace.TraceType.ASYNC_START, "m6", "L6", 6, "G6", 106),
trace.TraceEvent(trace.TraceType.DURATION_END, "m7", "L7", 7),
trace.TraceEvent(trace.TraceType.DURATION_GROUP_END, "m8", "L8", 8, "G8"),
- trace.TraceEvent(trace.TraceType.ASYNC_END, "m9", "L9", 9, "G9", 109)
+ trace.TraceEvent(trace.TraceType.ASYNC_END, "m9", "L9", 9, "G9", 109),
]
test_json = [
{"ph": "I", "pid": "m1", "name": "L1", "ts": 1, "s": "p"},
{"ph": "I", "pid": "m2", "tid": "G2", "name": "L2", "ts": 2, "s": "t"},
- {"ph": "n", "pid": "m3", "tid": "G3", "name": "L3", "ts": 3, \
- "scope": "G3", "cat": "m3", "id": 103, "args": {"id": 103}},
+ {
+ "ph": "n",
+ "pid": "m3",
+ "tid": "G3",
+ "name": "L3",
+ "ts": 3,
+ "scope": "G3",
+ "cat": "m3",
+ "id": 103,
+ "args": {"id": 103},
+ },
{"ph": "B", "pid": "m4", "tid": "L4", "name": "L4", "ts": 4},
{"ph": "B", "pid": "m5", "tid": "G5", "name": "L5", "ts": 5},
- {"ph": "b", "pid": "m6", "tid": "G6", "name": "L6", "ts": 6, \
- "scope": "G6", "cat": "m6", "id": 106, "args": {"id": 106}},
+ {
+ "ph": "b",
+ "pid": "m6",
+ "tid": "G6",
+ "name": "L6",
+ "ts": 6,
+ "scope": "G6",
+ "cat": "m6",
+ "id": 106,
+ "args": {"id": 106},
+ },
{"ph": "E", "pid": "m7", "tid": "L7", "name": "L7", "ts": 7},
{"ph": "E", "pid": "m8", "tid": "G8", "name": "L8", "ts": 8},
- {"ph": "e", "pid": "m9", "tid": "G9", "name": "L9", "ts": 9, \
- "scope": "G9", "cat": "m9", "id": 109, "args": {"id": 109}},
+ {
+ "ph": "e",
+ "pid": "m9",
+ "tid": "G9",
+ "name": "L9",
+ "ts": 9,
+ "scope": "G9",
+ "cat": "m9",
+ "id": 109,
+ "args": {"id": 109},
+ },
]
class TestTraceGenerateJson(unittest.TestCase):
"""Tests generate json with various events."""
+
def test_generate_single_json_event(self):
- event = trace.TraceEvent(event_type=trace.TraceType.INSTANTANEOUS,
- module="module",
- label="label",
- timestamp_us=10)
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="label",
+ timestamp_us=10,
+ )
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
- self.assertEqual(json.loads(json_lines[0]), {
- "ph": "I",
- "pid": "module",
- "name": "label",
- "ts": 10,
- "s": "p"
- })
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {"ph": "I", "pid": "module", "name": "label", "ts": 10, "s": "p"},
+ )
def test_generate_multiple_json_events(self):
json_lines = trace.generate_trace_json(test_events)
@@ -80,16 +106,14 @@ class TestTraceGenerateJson(unittest.TestCase):
timestamp_us=10,
has_data=True,
data_fmt="@pw_arg_label",
- data=bytes("arg", "utf-8"))
+ data=bytes("arg", "utf-8"),
+ )
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
- self.assertEqual(json.loads(json_lines[0]), {
- "ph": "I",
- "pid": "module",
- "name": "arg",
- "ts": 10,
- "s": "p"
- })
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {"ph": "I", "pid": "module", "name": "arg", "ts": 10, "s": "p"},
+ )
def test_generate_json_data_arg_group(self):
event = trace.TraceEvent(
@@ -99,85 +123,277 @@ class TestTraceGenerateJson(unittest.TestCase):
timestamp_us=10,
has_data=True,
data_fmt="@pw_arg_group",
- data=bytes("arg", "utf-8"))
+ data=bytes("arg", "utf-8"),
+ )
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
self.assertEqual(
- json.loads(json_lines[0]), {
+ json.loads(json_lines[0]),
+ {
"ph": "I",
"pid": "module",
"name": "label",
"tid": "arg",
"ts": 10,
- "s": "t"
- })
+ "s": "t",
+ },
+ )
def test_generate_json_data_counter(self):
- event = trace.TraceEvent(event_type=trace.TraceType.INSTANTANEOUS,
- module="module",
- label="counter",
- timestamp_us=10,
- has_data=True,
- data_fmt="@pw_arg_counter",
- data=(5).to_bytes(4, byteorder="little"))
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="counter",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_arg_counter",
+ data=(5).to_bytes(4, byteorder="little"),
+ )
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
self.assertEqual(
- json.loads(json_lines[0]), {
+ json.loads(json_lines[0]),
+ {
"ph": "C",
"pid": "module",
"name": "counter",
"ts": 10,
"s": "p",
- "args": {
- "counter": 5
- }
- })
+ "args": {"counter": 5},
+ },
+ )
def test_generate_json_data_struct_fmt_single(self):
- event = trace.TraceEvent(event_type=trace.TraceType.INSTANTANEOUS,
- module="module",
- label="counter",
- timestamp_us=10,
- has_data=True,
- data_fmt="@pw_py_struct_fmt:H",
- data=(5).to_bytes(2, byteorder="little"))
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="counter",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_struct_fmt:H",
+ data=(5).to_bytes(2, byteorder="little"),
+ )
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
self.assertEqual(
- json.loads(json_lines[0]), {
+ json.loads(json_lines[0]),
+ {
"ph": "I",
"pid": "module",
"name": "counter",
"ts": 10,
"s": "p",
- "args": {
- "data_0": 5
- }
- })
+ "args": {"data_0": 5},
+ },
+ )
def test_generate_json_data_struct_fmt_multi(self):
- event = trace.TraceEvent(event_type=trace.TraceType.INSTANTANEOUS,
- module="module",
- label="counter",
- timestamp_us=10,
- has_data=True,
- data_fmt="@pw_py_struct_fmt:Hl",
- data=struct.pack("Hl", 5, 2))
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="counter",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_struct_fmt:Hl",
+ data=struct.pack("<Hl", 5, 2),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "counter",
+ "ts": 10,
+ "s": "p",
+ "args": {"data_0": 5, "data_1": 2},
+ },
+ )
+
+ def test_generate_error_json_data_struct_invalid_small_buffer(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="counter",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_struct_fmt:Hl",
+ data=struct.pack("<H", 5),
+ )
json_lines = trace.generate_trace_json([event])
self.assertEqual(1, len(json_lines))
self.assertEqual(
- json.loads(json_lines[0]), {
+ json.loads(json_lines[0]),
+ {
"ph": "I",
"pid": "module",
"name": "counter",
"ts": 10,
"s": "p",
"args": {
- "data_0": 5,
- "data_1": 2
- }
- })
+ "error": f"Mismatched struct/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize('<Hl')} data "
+ f"{event.data.hex()} data len {len(event.data)}"
+ },
+ },
+ )
+
+ def test_generate_error_json_data_struct_invalid_large_buffer(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="counter",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_struct_fmt:Hl",
+ data=struct.pack("<Hll", 5, 2, 5),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "counter",
+ "ts": 10,
+ "s": "p",
+ "args": {
+ "error": f"Mismatched struct/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize('<Hl')} data "
+ f"{event.data.hex()} data len {len(event.data)}"
+ },
+ },
+ )
+
+ def test_generate_json_data_map_fmt_single(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="label",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_map_fmt:{Field:l}",
+ data=struct.pack("<l", 20),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "label",
+ "ts": 10,
+ "s": "p",
+ "args": {"Field": 20},
+ },
+ )
+
+ def test_generate_json_data_map_fmt_multi(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="label",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_map_fmt:{Field: l, Field2: l }",
+ data=struct.pack("<ll", 20, 40),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "label",
+ "ts": 10,
+ "s": "p",
+ "args": {"Field": 20, "Field2": 40},
+ },
+ )
+
+ def test_generate_error_json_data_map_bad_fmt(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="label",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_map_fmt:{Field;l,Field2;l}",
+ data=struct.pack("<ll", 20, 40),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "label",
+ "ts": 10,
+ "s": "p",
+ "args": {"error": f"Invalid map format {event.data_fmt}"},
+ },
+ )
+
+ def test_generate_error_json_data_map_invalid_small_buffer(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="label",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_map_fmt:{Field:l,Field2:l}",
+ data=struct.pack("<l", 20),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "label",
+ "ts": 10,
+ "s": "p",
+ "args": {
+ "error": f"Mismatched map/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize('<ll')} data "
+ f"{event.data.hex()} data len {len(event.data)}"
+ },
+ },
+ )
+
+ def test_generate_error_json_data_map_invalid_large_buffer(self):
+ event = trace.TraceEvent(
+ event_type=trace.TraceType.INSTANTANEOUS,
+ module="module",
+ label="label",
+ timestamp_us=10,
+ has_data=True,
+ data_fmt="@pw_py_map_fmt:{Field:H,Field2:H}",
+ data=struct.pack("<ll", 20, 40),
+ )
+ json_lines = trace.generate_trace_json([event])
+ self.assertEqual(1, len(json_lines))
+ self.assertEqual(
+ json.loads(json_lines[0]),
+ {
+ "ph": "I",
+ "pid": "module",
+ "name": "label",
+ "ts": 10,
+ "s": "p",
+ "args": {
+ "error": f"Mismatched map/data format {event.data_fmt} "
+ f"expected data len {struct.calcsize('<HH')} data "
+ f"{event.data.hex()} data len {len(event.data)}"
+ },
+ },
+ )
if __name__ == '__main__':
diff --git a/pw_trace/trace_backend_compile_test.cc b/pw_trace/trace_backend_compile_test.cc
index 83e7df6dc..c96703f23 100644
--- a/pw_trace/trace_backend_compile_test.cc
+++ b/pw_trace/trace_backend_compile_test.cc
@@ -22,76 +22,166 @@ namespace {
void TraceFunction() { PW_TRACE_FUNCTION(); }
void TraceFunctionGroup() { PW_TRACE_FUNCTION("FunctionGroup"); }
+void TraceFunctionGroupId() {
+ const uint32_t trace_id = 1;
+ PW_TRACE_FUNCTION("FunctionGroup", trace_id);
+}
+void TraceFunctionFlag() { PW_TRACE_FUNCTION_FLAG(PW_TRACE_FLAGS); }
+void TraceFunctionFlagGroup() {
+ PW_TRACE_FUNCTION_FLAG(PW_TRACE_FLAGS, "FunctionGroup");
+}
+void TraceFunctionFlagGroupId() {
+ const uint32_t trace_id = 1;
+ PW_TRACE_FUNCTION_FLAG(PW_TRACE_FLAGS, "FunctionGroup", trace_id);
+}
const char kSomeData[] = "SOME DATA";
} // namespace
-TEST(BasicTrace, Instant) { PW_TRACE_INSTANT("Test"); }
+TEST(BasicTrace, Instant) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_INSTANT("Test");
+ PW_TRACE_INSTANT("Test", "Group");
+ PW_TRACE_INSTANT("Test", "Group", trace_id);
+}
+
+TEST(BasicTrace, InstantFlag) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_INSTANT_FLAG(PW_TRACE_FLAGS, "Test");
+ PW_TRACE_INSTANT_FLAG(PW_TRACE_FLAGS, "Test", "Group");
+ PW_TRACE_INSTANT_FLAG(PW_TRACE_FLAGS, "Test", "Group", trace_id);
+}
+
+TEST(BasicTrace, InstantData) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_INSTANT_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_INSTANT_DATA("Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_INSTANT_DATA(
+ "Test", "Group", trace_id, "s", kSomeData, sizeof(kSomeData));
+}
-TEST(BasicTrace, InstantGroup) { PW_TRACE_INSTANT("Test", "group"); }
+TEST(BasicTrace, InstantDataFlag) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_INSTANT_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_INSTANT_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_INSTANT_DATA_FLAG(PW_TRACE_FLAGS,
+ "Test",
+ "Group",
+ trace_id,
+ "s",
+ kSomeData,
+ sizeof(kSomeData));
+}
-TEST(BasicTrace, Duration) {
+TEST(BasicTrace, Start) {
+ const uint32_t trace_id = 1;
PW_TRACE_START("Test");
- PW_TRACE_END("Test");
+ PW_TRACE_START("Test", "Group");
+ PW_TRACE_START("Test", "Group", trace_id);
}
-TEST(BasicTrace, DurationGroup) {
- PW_TRACE_START("Parent", "group");
- PW_TRACE_START("Child", "group");
- PW_TRACE_END("child", "group");
- PW_TRACE_START("Other Child", "group");
- PW_TRACE_END("Other Child", "group");
- PW_TRACE_END("Parent", "group");
+TEST(BasicTrace, StartFlag) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_START_FLAG(PW_TRACE_FLAGS, "Test");
+ PW_TRACE_START_FLAG(PW_TRACE_FLAGS, "Test", "Group");
+ PW_TRACE_START_FLAG(PW_TRACE_FLAGS, "Test", "Group", trace_id);
}
-TEST(BasicTrace, Async) {
- uint32_t trace_id = 1;
- PW_TRACE_START("label for start", "group", trace_id);
- PW_TRACE_INSTANT("label for step", "group", trace_id);
- PW_TRACE_END("label for end", "group", trace_id);
+TEST(BasicTrace, StartData) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_START_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA("Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA(
+ "Test", "Group", trace_id, "s", kSomeData, sizeof(kSomeData));
}
-TEST(BasicTrace, Scope) { PW_TRACE_SCOPE("scoped trace"); }
+TEST(BasicTrace, StartDataFlag) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_START_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA_FLAG(PW_TRACE_FLAGS,
+ "Test",
+ "Group",
+ trace_id,
+ "s",
+ kSomeData,
+ sizeof(kSomeData));
+}
-TEST(BasicTrace, ScopeGroup) {
- PW_TRACE_SCOPE("scoped group trace", "group");
- { PW_TRACE_SCOPE("sub scoped group trace", "group"); }
+TEST(BasicTrace, End) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_END("Test");
+ PW_TRACE_END("Test", "Group");
+ PW_TRACE_END("Test", "Group", trace_id);
}
-TEST(BasicTrace, Function) { TraceFunction(); }
+TEST(BasicTrace, EndFlag) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_END_FLAG(PW_TRACE_FLAGS, "Test");
+ PW_TRACE_END_FLAG(PW_TRACE_FLAGS, "Test", "Group");
+ PW_TRACE_END_FLAG(PW_TRACE_FLAGS, "Test", "Group", trace_id);
+}
-TEST(BasicTrace, FunctionGroup) { TraceFunctionGroup(); }
+TEST(BasicTrace, EndData) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_END_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_END_DATA("Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_END_DATA(
+ "Test", "Group", trace_id, "s", kSomeData, sizeof(kSomeData));
+}
-TEST(BasicTrace, InstantData) {
- PW_TRACE_INSTANT_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+TEST(BasicTrace, EndDataFlag) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_END_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_END_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_END_DATA_FLAG(PW_TRACE_FLAGS,
+ "Test",
+ "Group",
+ trace_id,
+ "s",
+ kSomeData,
+ sizeof(kSomeData));
}
-TEST(BasicTrace, InstantGroupData) {
- PW_TRACE_INSTANT_DATA("Test", "Group", "s", kSomeData, sizeof(kSomeData));
+TEST(BasicTrace, Scope) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_SCOPE("scoped trace");
+ PW_TRACE_SCOPE("scoped trace", "Group");
+ PW_TRACE_SCOPE("scoped trace", "Group", trace_id);
+ {
+ PW_TRACE_SCOPE("sub scoped trace");
+ PW_TRACE_SCOPE("sub scoped trace", "Group");
+ PW_TRACE_SCOPE("sub scoped trace", "Group", trace_id);
+ }
}
-TEST(BasicTrace, DurationData) {
- PW_TRACE_START_DATA("Test", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_END_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+TEST(BasicTrace, ScopeFlag) {
+ const uint32_t trace_id = 1;
+ PW_TRACE_SCOPE_FLAG(PW_TRACE_FLAGS, "scoped trace");
+ PW_TRACE_SCOPE_FLAG(PW_TRACE_FLAGS, "scoped trace", "Group");
+ PW_TRACE_SCOPE_FLAG(PW_TRACE_FLAGS, "scoped trace", "Group", trace_id);
+ {
+ PW_TRACE_SCOPE_FLAG(PW_TRACE_FLAGS, "sub scoped trace");
+ PW_TRACE_SCOPE_FLAG(PW_TRACE_FLAGS, "sub scoped trace", "Group");
+ PW_TRACE_SCOPE_FLAG(PW_TRACE_FLAGS, "sub scoped trace", "Group", trace_id);
+ }
}
-TEST(BasicTrace, DurationGroupData) {
- PW_TRACE_START_DATA("Parent", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_START_DATA("Child", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_END_DATA("child", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_START_DATA(
- "Other Child", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_END_DATA("Other Child", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_END_DATA("Parent", "group", "s", kSomeData, sizeof(kSomeData));
+TEST(BasicTrace, Function) {
+ TraceFunction();
+ TraceFunctionGroup();
+ TraceFunctionGroupId();
}
-TEST(BasicTrace, AsyncData) {
- uint32_t trace_id = 1;
- PW_TRACE_START_DATA(
- "label for start", "group", trace_id, "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_INSTANT_DATA(
- "label for step", "group", trace_id, "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_END_DATA(
- "label for end", "group", trace_id, "s", kSomeData, sizeof(kSomeData));
+TEST(BasicTrace, FunctionFlag) {
+ TraceFunctionFlag();
+ TraceFunctionFlagGroup();
+ TraceFunctionFlagGroupId();
}
diff --git a/pw_trace/trace_backend_compile_test_c.c b/pw_trace/trace_backend_compile_test_c.c
index 4d99c071f..ad5facb9a 100644
--- a/pw_trace/trace_backend_compile_test_c.c
+++ b/pw_trace/trace_backend_compile_test_c.c
@@ -23,40 +23,80 @@
#endif // __cplusplus
void BasicTraceTestPlainC(void) {
+ const uint32_t trace_id = 1;
+ const char kSomeData[] = "SOME DATA";
+
PW_TRACE_INSTANT("Test");
+ PW_TRACE_INSTANT("Test", "Group");
+ PW_TRACE_INSTANT("Test", "Group", trace_id);
- PW_TRACE_START("Test");
- PW_TRACE_END("Test");
+ PW_TRACE_INSTANT_FLAG(PW_TRACE_FLAGS, "Test");
+ PW_TRACE_INSTANT_FLAG(PW_TRACE_FLAGS, "Test", "Group");
+ PW_TRACE_INSTANT_FLAG(PW_TRACE_FLAGS, "Test", "Group", trace_id);
- PW_TRACE_START("Parent", "group");
- PW_TRACE_START("Child", "group");
- PW_TRACE_END("Child", "group");
- PW_TRACE_INSTANT("Test", "group");
- PW_TRACE_START("Other Child", "group");
- PW_TRACE_END("Other Child", "group");
- PW_TRACE_END("Parent", "group");
+ PW_TRACE_INSTANT_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_INSTANT_DATA("Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_INSTANT_DATA(
+ "Test", "Group", trace_id, "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_START("label for start", "group", 1);
- PW_TRACE_INSTANT("label for step", "group", 1);
- PW_TRACE_END("label for end", "group", 1);
+ PW_TRACE_INSTANT_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_INSTANT_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_INSTANT_DATA_FLAG(PW_TRACE_FLAGS,
+ "Test",
+ "Group",
+ trace_id,
+ "s",
+ kSomeData,
+ sizeof(kSomeData));
+ PW_TRACE_START("Test");
+ PW_TRACE_START("Test", "Group");
+ PW_TRACE_START("Test", "Group", trace_id);
- const char kSomeData[] = "SOME DATA";
- PW_TRACE_INSTANT_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_FLAG(PW_TRACE_FLAGS, "Test");
+ PW_TRACE_START_FLAG(PW_TRACE_FLAGS, "Test", "Group");
+ PW_TRACE_START_FLAG(PW_TRACE_FLAGS, "Test", "Group", trace_id);
- PW_TRACE_START_DATA("Parent", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_START_DATA("Child", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_END_DATA("child", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_INSTANT_DATA("Test", "group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA("Test", "Group", "s", kSomeData, sizeof(kSomeData));
PW_TRACE_START_DATA(
- "Other Child", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_END_DATA("Other Child", "group", "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_END_DATA("Parent", "group", "s", kSomeData, sizeof(kSomeData));
+ "Test", "Group", trace_id, "s", kSomeData, sizeof(kSomeData));
- uint32_t trace_id = 1;
- PW_TRACE_START_DATA(
- "label for start", "group", trace_id, "s", kSomeData, sizeof(kSomeData));
- PW_TRACE_INSTANT_DATA(
- "label for step", "group", trace_id, "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_START_DATA_FLAG(PW_TRACE_FLAGS,
+ "Test",
+ "Group",
+ trace_id,
+ "s",
+ kSomeData,
+ sizeof(kSomeData));
+
+ PW_TRACE_END("Test");
+ PW_TRACE_END("Test", "Group");
+ PW_TRACE_END("Test", "Group", trace_id);
+
+ PW_TRACE_END_FLAG(PW_TRACE_FLAGS, "Test");
+ PW_TRACE_END_FLAG(PW_TRACE_FLAGS, "Test", "Group");
+ PW_TRACE_END_FLAG(PW_TRACE_FLAGS, "Test", "Group", trace_id);
+
+ PW_TRACE_END_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_END_DATA("Test", "Group", "s", kSomeData, sizeof(kSomeData));
PW_TRACE_END_DATA(
- "label for end", "group", trace_id, "s", kSomeData, sizeof(kSomeData));
+ "Test", "Group", trace_id, "s", kSomeData, sizeof(kSomeData));
+
+ PW_TRACE_END_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_END_DATA_FLAG(
+ PW_TRACE_FLAGS, "Test", "Group", "s", kSomeData, sizeof(kSomeData));
+ PW_TRACE_END_DATA_FLAG(PW_TRACE_FLAGS,
+ "Test",
+ "Group",
+ trace_id,
+ "s",
+ kSomeData,
+ sizeof(kSomeData));
}
diff --git a/pw_trace/trace_facade_test.cc b/pw_trace/trace_facade_test.cc
index 949728424..5e8060168 100644
--- a/pw_trace/trace_facade_test.cc
+++ b/pw_trace/trace_facade_test.cc
@@ -399,3 +399,53 @@ TEST(BasicTrace, MacroFlag) {
PW_TRACE_GROUP_LABEL_DEFAULT,
PW_TRACE_TRACE_ID_DEFAULT));
}
+
+TEST(DisableTrace, Instant) {
+ PW_TRACE_INSTANT("Test");
+ EXPECT_EQ(LastEvent::Instance().Get(),
+ Event(Instantaneous,
+ PW_TRACE_FLAGS_DEFAULT,
+ "Test",
+ PW_TRACE_GROUP_LABEL_DEFAULT,
+ PW_TRACE_TRACE_ID_DEFAULT));
+#undef PW_TRACE_ENABLE
+#define PW_TRACE_ENABLE 0
+ PW_TRACE_INSTANT("TestDisabled");
+
+ EXPECT_EQ(LastEvent::Instance().Get(),
+ Event(Instantaneous,
+ PW_TRACE_FLAGS_DEFAULT,
+ "Test",
+ PW_TRACE_GROUP_LABEL_DEFAULT,
+ PW_TRACE_TRACE_ID_DEFAULT));
+#undef PW_TRACE_ENABLE
+#define PW_TRACE_ENABLE 1
+}
+
+TEST(DisableTrace, InstantData) {
+ PW_TRACE_INSTANT_DATA("Test", "s", kSomeData, sizeof(kSomeData));
+ EXPECT_EQ(LastEvent::Instance().Get(),
+ Event(Instantaneous,
+ PW_TRACE_FLAGS_DEFAULT,
+ "Test",
+ PW_TRACE_GROUP_LABEL_DEFAULT,
+ PW_TRACE_TRACE_ID_DEFAULT,
+ "s",
+ kSomeData,
+ sizeof(kSomeData)));
+
+#undef PW_TRACE_ENABLE
+#define PW_TRACE_ENABLE 0
+ PW_TRACE_INSTANT_DATA("TestDisabled", "s", kSomeData, sizeof(kSomeData));
+ EXPECT_EQ(LastEvent::Instance().Get(),
+ Event(Instantaneous,
+ PW_TRACE_FLAGS_DEFAULT,
+ "Test",
+ PW_TRACE_GROUP_LABEL_DEFAULT,
+ PW_TRACE_TRACE_ID_DEFAULT,
+ "s",
+ kSomeData,
+ sizeof(kSomeData)));
+#undef PW_TRACE_ENABLE
+#define PW_TRACE_ENABLE 1
+}
diff --git a/pw_trace_tokenized/BUILD.bazel b/pw_trace_tokenized/BUILD.bazel
index 34e65ae1b..26f1302a8 100644
--- a/pw_trace_tokenized/BUILD.bazel
+++ b/pw_trace_tokenized/BUILD.bazel
@@ -18,13 +18,12 @@ load(
"pw_cc_library",
"pw_cc_test",
)
+load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
-# TODO(pwbug/101): Need to add support for facades/backends to Bazel.
-
pw_cc_library(
name = "headers",
hdrs = [
@@ -45,6 +44,12 @@ pw_cc_library(
)
pw_cc_library(
+ name = "linux_config_overrides",
+ hdrs = ["linux_config_overrides.h"],
+ tags = ["manual"],
+)
+
+pw_cc_library(
name = "pw_trace_tokenized",
srcs = [
"trace.cc",
@@ -71,10 +76,13 @@ pw_cc_library(
includes = [
"public",
],
+ # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ tags = ["manual"],
deps = [
+ ":buffer",
+ ":protos_cc.nanopb_rpc",
"//pw_log",
"//pw_trace",
- "//pw_trace_tokenized_buffer",
],
)
@@ -87,17 +95,19 @@ pw_cc_library(
"public",
],
deps = [
+ ":pw_trace_tokenized",
"//pw_ring_buffer",
"//pw_status",
- "//pw_trace_tokenized",
],
)
pw_cc_library(
- name = "pw_trace_tokenized_buffer",
+ name = "buffer",
srcs = [
"trace_buffer.cc",
],
+ # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ tags = ["manual"],
deps = [
":trace_buffer_headers",
"//pw_ring_buffer",
@@ -106,13 +116,15 @@ pw_cc_library(
)
pw_cc_library(
- name = "pw_trace_tokenized_buffer_log",
+ name = "buffer_log",
srcs = [
"trace_buffer_log.cc",
],
hdrs = [
"public/pw_trace_tokenized/trace_buffer_log.h",
],
+ # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ tags = ["manual"],
deps = [
":trace_buffer_headers",
"//pw_base64",
@@ -127,6 +139,14 @@ proto_library(
"pw_trace_protos/trace.proto",
"pw_trace_protos/trace_rpc.proto",
],
+ # TODO(tpudlik): We should provide trace_rpc.options to nanopb here, but the
+ # current proto codegen implementation provides no mechanism for doing so.
+ # inputs = [ "pw_trace_protos/trace_rpc.options" ]
+)
+
+pw_proto_library(
+ name = "protos_cc",
+ deps = [":protos"],
)
pw_cc_library(
@@ -135,7 +155,7 @@ pw_cc_library(
"fake_trace_time.cc",
],
deps = [
- "//pw_trace",
+ ":pw_trace_tokenized",
],
)
@@ -148,38 +168,40 @@ pw_cc_test(
"pw_trace_test",
"pw_trace_test/public_overrides",
],
+ # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ tags = ["manual"],
deps = [
- ":backend",
- ":facade",
- ":pw_trace",
+ ":pw_trace_tokenized",
"//pw_preprocessor",
"//pw_unit_test",
],
)
pw_cc_test(
- name = "trace_tokenized_buffer_test",
+ name = "buffer_test",
srcs = [
"trace_buffer_test.cc",
],
+ # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ tags = ["manual"],
deps = [
- ":backend",
- ":facade",
- ":pw_trace",
+ ":buffer",
+ ":pw_trace_tokenized",
"//pw_preprocessor",
"//pw_unit_test",
],
)
pw_cc_test(
- name = "trace_tokenized_buffer_log_test",
+ name = "buffer_log_test",
srcs = [
"trace_buffer_log_test.cc",
],
+ # TODO(b/260641850): Get pw_trace_tokenized building in Bazel.
+ tags = ["manual"],
deps = [
- ":backend",
- ":facade",
- ":pw_trace_log",
+ ":buffer_log",
+ ":pw_trace_tokenized",
"//pw_preprocessor",
"//pw_unit_test",
],
@@ -189,7 +211,7 @@ pw_cc_library(
name = "pw_trace_host_trace_time",
srcs = ["host_trace_time.cc"],
includes = ["example/public"],
- deps = ["//pw_trace"],
+ deps = [":pw_trace_tokenized"],
)
pw_cc_library(
@@ -202,46 +224,64 @@ pw_cc_library(
pw_cc_binary(
name = "trace_tokenized_example_basic",
srcs = ["example/basic.cc"],
+ # TODO(b/258071921): Fix puzzling compiler errors
+ tags = ["manual"],
deps = [
":pw_trace_example_to_file",
- "//dir_pw_trace",
- "//dir_pw_trace:pw_trace_sample_app",
"//pw_log",
+ "//pw_trace",
+ "//pw_trace:pw_trace_sample_app",
],
)
pw_cc_binary(
name = "trace_tokenized_example_trigger",
srcs = ["example/trigger.cc"],
+ # TODO(b/258071921): Fix puzzling compiler errors
+ tags = ["manual"],
deps = [
":pw_trace_example_to_file",
- "//dir_pw_trace",
- "//dir_pw_trace:pw_trace_sample_app",
"//pw_log",
+ "//pw_trace",
+ "//pw_trace:pw_trace_sample_app",
],
)
pw_cc_binary(
name = "trace_tokenized_example_filter",
srcs = ["example/filter.cc"],
+ # TODO(b/258071921): Fix puzzling compiler errors
+ tags = ["manual"],
deps = [
":pw_trace_example_to_file",
- "//dir_pw_trace",
- "//dir_pw_trace:pw_trace_sample_app",
"//pw_log",
+ "//pw_trace",
+ "//pw_trace:pw_trace_sample_app",
],
)
pw_cc_library(
name = "trace_tokenized_example_rpc",
srcs = ["example/rpc.cc"],
+ # TODO(b/258071921): Fix puzzling compiler errors
+ tags = ["manual"],
deps = [
- ":pw_trace_rpc_service",
- "//dir_pw_rpc:server",
- "//dir_pw_rpc:system_server",
- "//dir_pw_trace",
- "//dir_pw_trace:pw_trace_sample_app",
"//pw_hdlc",
"//pw_log",
+ "//pw_rpc",
+ "//pw_rpc/system_server",
+ "//pw_trace",
+ "//pw_trace:pw_trace_sample_app",
+ ],
+)
+
+pw_cc_library(
+ name = "trace_tokenized_example_linux_group_by_tid",
+ srcs = ["example/linux_group_by_tid.cc"],
+ tags = ["manual"],
+ deps = [
+ ":pw_trace_example_to_file",
+ "//pw_log",
+ "//pw_trace",
],
)
diff --git a/pw_trace_tokenized/BUILD.gn b/pw_trace_tokenized/BUILD.gn
index e248c8e93..7af4ddfd9 100644
--- a/pw_trace_tokenized/BUILD.gn
+++ b/pw_trace_tokenized/BUILD.gn
@@ -36,6 +36,19 @@ pw_source_set("config") {
public = [ "public/pw_trace_tokenized/config.h" ]
}
+config("linux_include_config_overrides") {
+ cflags = [
+ "-include",
+ rebase_path("linux_config_overrides.h", root_build_dir),
+ ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("linux_config_overrides") {
+ public_configs = [ ":linux_include_config_overrides" ]
+ sources = [ "linux_config_overrides.h" ]
+}
+
pw_test_group("tests") {
tests = [
":trace_tokenized_test",
@@ -54,8 +67,9 @@ pw_source_set("pw_trace_tokenized") {
":core",
"$dir_pw_tokenizer",
]
+ deps = []
if (pw_trace_tokenizer_time != "") {
- deps = [ "$pw_trace_tokenizer_time" ]
+ deps += [ "$pw_trace_tokenizer_time" ]
}
public = [ "public_overrides/pw_trace_backend/trace_backend.h" ]
@@ -106,6 +120,7 @@ pw_source_set("tokenized_trace_buffer") {
"$dir_pw_ring_buffer",
"$dir_pw_tokenizer",
"$dir_pw_varint",
+ dir_pw_span,
]
sources = [ "trace_buffer.cc" ]
public_configs = [
@@ -161,8 +176,10 @@ pw_source_set("core") {
":public_include_path",
]
public_deps = [
+ "$dir_pw_log",
"$dir_pw_status",
"$dir_pw_tokenizer",
+ dir_pw_span,
]
deps = [
":config",
@@ -242,3 +259,17 @@ if (dir_pw_third_party_nanopb == "") {
]
}
}
+
+if (current_os != "linux") {
+ group("trace_tokenized_example_linux_group_by_tid") {
+ }
+} else {
+ pw_executable("trace_tokenized_example_linux_group_by_tid") {
+ sources = [ "example/linux_group_by_tid.cc" ]
+ deps = [
+ ":trace_example_to_file",
+ "$dir_pw_log",
+ "$dir_pw_trace",
+ ]
+ }
+}
diff --git a/pw_trace_tokenized/CMakeLists.txt b/pw_trace_tokenized/CMakeLists.txt
index 3aa0786ac..e8780f6a4 100644
--- a/pw_trace_tokenized/CMakeLists.txt
+++ b/pw_trace_tokenized/CMakeLists.txt
@@ -15,22 +15,42 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
-pw_add_module_library(pw_trace_tokenized
- IMPLEMENTS_FACADES
- pw_trace
+pw_add_module_config(pw_trace_tokenized_CONFIG)
+
+pw_add_library(pw_trace_tokenized.config INTERFACE
+ HEADERS
+ public/pw_trace_tokenized/config.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ ${pw_trace_CONFIG}
+)
+
+pw_add_library(pw_trace_tokenized STATIC
+ HEADERS
+ public/pw_trace_tokenized/internal/trace_tokenized_internal.h
+ public/pw_trace_tokenized/trace_callback.h
+ public/pw_trace_tokenized/trace_tokenized.h
+ public_overrides/pw_trace_backend/trace_backend.h
+ PUBLIC_INCLUDES
+ public
+ public_overrides
+ PUBLIC_DEPS
+ pw_span
+ pw_status
+ pw_tokenizer
+ pw_trace_tokenized.config
SOURCES
trace.cc
PRIVATE_DEPS
pw_assert
pw_log
pw_ring_buffer
+ pw_trace.facade
pw_varint
- PUBLIC_DEPS
- pw_status
- pw_tokenizer
)
-pw_add_module_library(pw_trace_tokenized.trace_buffer
+pw_add_library(pw_trace_tokenized.trace_buffer STATIC
SOURCES
trace_buffer.cc
PRIVATE_DEPS
@@ -42,6 +62,7 @@ pw_add_module_library(pw_trace_tokenized.trace_buffer
pw_status
pw_tokenizer
pw_trace_tokenized
+ pw_trace_tokenized.config
pw_varint
)
@@ -52,7 +73,7 @@ pw_proto_library(pw_trace_tokenized.protos
pw_trace_protos/trace_rpc.options
)
-pw_add_module_library(pw_trace_tokenized.rpc_service
+pw_add_library(pw_trace_tokenized.rpc_service STATIC
SOURCES
trace_rpc_service_nanopb.cc
PRIVATE_DEPS
diff --git a/pw_trace_tokenized/OWNERS b/pw_trace_tokenized/OWNERS
index 8819b2c20..8c66826c9 100644
--- a/pw_trace_tokenized/OWNERS
+++ b/pw_trace_tokenized/OWNERS
@@ -1 +1,3 @@
keir@google.com
+rgoliver@google.com
+ykyyip@google.com
diff --git a/pw_trace_tokenized/docs.rst b/pw_trace_tokenized/docs.rst
index ca537e5e2..f58dc62c1 100644
--- a/pw_trace_tokenized/docs.rst
+++ b/pw_trace_tokenized/docs.rst
@@ -70,27 +70,35 @@ The tokenized trace module adds both event callbacks and data sinks which
provide hooks into tracing.
The *event callbacks* are called when trace events occur, with the trace event
-data. Using the return flags, these callbacks can be used to adjust the trace
-behaviour at runtime in response to specific events. If requested (using
-``called_on_every_event``) the callback will be called on every trace event
-regardless if tracing is currently enabled or not. Using this, the application
-can trigger tracing on or off when specific traces or patterns of traces are
-observed, or can selectively filter traces to preserve the trace buffer.
+data, before the event is encoded or sent to the sinks. The callbacks may
+modify the run-time fields of the trace event, i.e. ``trace_id``,
+``data_buffer`` and ``data_size``. Using the return flags, these callbacks can
+be used to adjust the trace behaviour at runtime in response to specific events.
-The event callback is a single function which is provided the details of the
-trace as arguments, and returns ``pw_trace_TraceEventReturnFlags``, which can be
-used to change how the trace is handled.
+If requested (using ``called_on_every_event``) the callback will be called on
+every trace event regardless if tracing is currently enabled or not. Using this,
+the application can trigger tracing on or off when specific traces or patterns
+of traces are observed, or can selectively filter traces to preserve the trace
+buffer.
+
+The event callback is called in the context of the traced task. It must be
+ISR-safe to support tracing within ISRs. It must be lightweight to prevent
+performance issues in the trace tasks.
+
+The return flags ``pw_trace_TraceEventReturnFlags`` support the following
+behaviors:
+
+* ``PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT`` can be set true to skip this
+ sample.
+* ``PW_TRACE_EVENT_RETURN_FLAGS_DISABLE_AFTER_PROCESSING`` can be set true to
+ disable tracing after this sample.
.. cpp:function:: pw_trace_TraceEventReturnFlags pw_trace_EventCallback( \
- void* user_data, \
- uint32_t trace_ref, \
- pw_trace_EventType event_type, \
- const char* module, \
- uint32_t trace_id, \
- uint8_t flags)
+ void* user_data, \
+ pw_trace_tokenized_TraceEvent* event)
.. cpp:function:: pw_Status pw_trace_RegisterEventCallback( \
pw_trace_EventCallback callback, \
- bool called_on_every_event, \
+ pw_trace_EventCallbackFlags flags, \
void* user_data, \
pw_trace_EventCallbackHandle* handle)
.. cpp:function:: pw_Status pw_trace_UnregisterEventCallback( \
diff --git a/pw_trace_tokenized/example/filter.cc b/pw_trace_tokenized/example/filter.cc
index 6d68fb9e1..8461f560a 100644
--- a/pw_trace_tokenized/example/filter.cc
+++ b/pw_trace_tokenized/example/filter.cc
@@ -35,15 +35,11 @@
#include "pw_trace_tokenized/trace_tokenized.h"
pw_trace_TraceEventReturnFlags TraceEventCallback(
- void* /* user_data */,
- uint32_t /* trace_ref */,
- pw_trace_EventType /* event_type */,
- const char* module,
- uint32_t trace_id,
- uint8_t /* flags */) {
+ void* /* user_data */, pw_trace_tokenized_TraceEvent* event) {
// Filter out all traces from processing task, which aren't traceId 3
static constexpr uint32_t kFilterId = 3;
- return (strcmp("Processing", module) == 0 && trace_id != kFilterId)
+ return (strcmp("Processing", event->module) == 0 &&
+ event->trace_id != kFilterId)
? PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT
: 0;
}
@@ -57,7 +53,7 @@ int main(int argc, char** argv) { // Take filename as arg
// Register filter callback
pw::trace::Callbacks::Instance()
.RegisterEventCallback(TraceEventCallback)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
PW_TRACE_SET_ENABLED(true); // Start with tracing enabled
diff --git a/pw_trace_tokenized/example/linux_group_by_tid.cc b/pw_trace_tokenized/example/linux_group_by_tid.cc
new file mode 100644
index 000000000..6c8a204a3
--- /dev/null
+++ b/pw_trace_tokenized/example/linux_group_by_tid.cc
@@ -0,0 +1,100 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 <pthread.h>
+#include <sys/syscall.h>
+#include <unistd.h>
+
+#include <chrono>
+#include <cstdio>
+#include <thread>
+
+#include "pw_log/log.h"
+#include "pw_trace/trace.h"
+#include "pw_trace_tokenized/example/trace_to_file.h"
+
+// Example for annotating trace events with thread id.
+// The platform annotates instants and duration events with the thread id if the
+// caller does not explicitly provide a group. The thread id is written in
+// the trace_id field.
+//
+// This example requires linux_config_overrides.h to define
+// PW_TRACE_HAS_TRACE_ID. Set pw_trace_CONFIG to
+// "$dir_pw_trace_tokenized:linux_config_overrides" in target_toolchains.gni to
+// enable the override before building this example.
+//
+// TODO(ykyyip): update trace_tokenized.py to handle the trace_id.
+
+pw_trace_TraceEventReturnFlags TraceEventCallback(
+ void* /*user_data*/, pw_trace_tokenized_TraceEvent* event) {
+ // Instant and duration events with no group means group by pid/tid.
+ if ((event->event_type == PW_TRACE_EVENT_TYPE_INSTANT) ||
+ (event->event_type == PW_TRACE_EVENT_TYPE_DURATION_START) ||
+ (event->event_type == PW_TRACE_EVENT_TYPE_DURATION_END)) {
+ event->trace_id = syscall(__NR_gettid);
+ }
+ return PW_TRACE_EVENT_RETURN_FLAGS_NONE;
+}
+
+void ExampleTask(void* /*arg*/) {
+ int times_to_run = 10;
+ while (times_to_run--) {
+ PW_TRACE_START("Processing");
+ // Fake processing time.
+ std::this_thread::sleep_for(std::chrono::milliseconds(42));
+ PW_TRACE_END("Processing");
+ // Sleep for a random amount before running again.
+ int sleep_time = 1 + std::rand() % 20;
+ std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
+ }
+}
+
+void RunThreadedTraceSampleApp() {
+ std::srand(std::time(nullptr));
+
+ // Start threads to show parallel processing.
+ int num_threads = 5;
+ while (num_threads--) {
+ PW_TRACE_INSTANT("CreateThread");
+ std::thread thread(ExampleTask, nullptr);
+ thread.detach();
+ }
+}
+
+int main(int argc, char** argv) {
+ if (argc != 2) {
+ PW_LOG_ERROR("Expected output file name as argument.\n");
+ return -1;
+ }
+
+ // Enable tracing.
+ PW_TRACE_SET_ENABLED(true);
+
+ // Dump trace data to the file passed in.
+ pw::trace::TraceToFile trace_to_file{argv[1]};
+
+ // Register platform callback
+ pw::trace::RegisterCallbackWhenCreated{TraceEventCallback};
+
+ PW_LOG_INFO("Running threaded trace example...\n");
+ RunThreadedTraceSampleApp();
+
+ // Sleep forever
+ while (true) {
+ std::this_thread::sleep_for(std::chrono::seconds(60));
+ }
+ return 0;
+}
diff --git a/pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h b/pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h
index 7527143d8..c392a3853 100644
--- a/pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h
+++ b/pw_trace_tokenized/example/public/pw_trace_tokenized/example/trace_to_file.h
@@ -20,7 +20,6 @@
#include <fstream>
-#include "pw_trace/example/sample_app.h"
#include "pw_trace_tokenized/trace_callback.h"
namespace pw {
@@ -35,14 +34,14 @@ class TraceToFile {
TraceSinkEndBlock,
&out_,
&sink_handle_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
out_.open(file_name, std::ios::out | std::ios::binary);
}
~TraceToFile() {
Callbacks::Instance()
.UnregisterSink(sink_handle_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
out_.close();
}
diff --git a/pw_trace_tokenized/example/trigger.cc b/pw_trace_tokenized/example/trigger.cc
index 1995aff42..cc7c3936f 100644
--- a/pw_trace_tokenized/example/trigger.cc
+++ b/pw_trace_tokenized/example/trigger.cc
@@ -53,17 +53,14 @@ constexpr uint32_t kTriggerEndTraceRef = PW_TRACE_REF(PW_TRACE_TYPE_ASYNC_END,
} // namespace
pw_trace_TraceEventReturnFlags TraceEventCallback(
- void* /* user_data */,
- uint32_t trace_ref,
- pw_trace_EventType /* event_type */,
- const char* /* module */,
- uint32_t trace_id,
- uint8_t /* flags */) {
- if (trace_ref == kTriggerStartTraceRef && trace_id == kTriggerId) {
+ void* /* user_data */, pw_trace_tokenized_TraceEvent* event) {
+ if (event->trace_token == kTriggerStartTraceRef &&
+ event->trace_id == kTriggerId) {
PW_LOG_INFO("Trace capture started!");
PW_TRACE_SET_ENABLED(true);
}
- if (trace_ref == kTriggerEndTraceRef && trace_id == kTriggerId) {
+ if (event->trace_token == kTriggerEndTraceRef &&
+ event->trace_id == kTriggerId) {
PW_LOG_INFO("Trace capture ended!");
return PW_TRACE_EVENT_RETURN_FLAGS_DISABLE_AFTER_PROCESSING;
}
@@ -80,7 +77,7 @@ int main(int argc, char** argv) { // Take filename as arg
pw::trace::Callbacks::Instance()
.RegisterEventCallback(TraceEventCallback,
pw::trace::CallbacksImpl::kCallOnEveryEvent)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
// Ensure tracing is off at start, the trigger will turn it on.
PW_TRACE_SET_ENABLED(false);
diff --git a/pw_trace_tokenized/linux_config_overrides.h b/pw_trace_tokenized/linux_config_overrides.h
new file mode 100644
index 000000000..d4215d055a
--- /dev/null
+++ b/pw_trace_tokenized/linux_config_overrides.h
@@ -0,0 +1,30 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+//==============================================================================
+//
+
+#pragma once
+
+// pw_trace config overrides when the host os is linux
+
+// When pw_trace_tokenized_ANNOTATION == linux_tid_trace_annotation,
+// the platform sets trace_id to the Linux thread id when the caller does
+// not provide a group.
+#define PW_TRACE_HAS_TRACE_ID(TRACE_TYPE) \
+ ((TRACE_TYPE) == PW_TRACE_TYPE_INSTANT || \
+ (TRACE_TYPE) == PW_TRACE_TYPE_DURATION_START || \
+ (TRACE_TYPE) == PW_TRACE_TYPE_DURATION_END || \
+ (TRACE_TYPE) == PW_TRACE_TYPE_ASYNC_INSTANT || \
+ (TRACE_TYPE) == PW_TRACE_TYPE_ASYNC_START || \
+ (TRACE_TYPE) == PW_TRACE_TYPE_ASYNC_END)
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/internal/trace_tokenized_internal.h b/pw_trace_tokenized/public/pw_trace_tokenized/internal/trace_tokenized_internal.h
index 8427dc674..e2954dd43 100644
--- a/pw_trace_tokenized/public/pw_trace_tokenized/internal/trace_tokenized_internal.h
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/internal/trace_tokenized_internal.h
@@ -17,9 +17,11 @@
#pragma once
#include <stdbool.h>
+#include <stddef.h>
#include <stdint.h>
#include "pw_preprocessor/arguments.h"
+#include "pw_preprocessor/util.h"
// Because __FUNCTION__ is not a string literal to the preprocessor it can't be
// tokenized. So this backend redefines the implementation to instead use the
@@ -47,6 +49,7 @@
#define PW_TRACE_TYPE_ASYNC_END PW_TRACE_EVENT_TYPE_ASYNC_END
PW_EXTERN_C_START
+
typedef enum {
PW_TRACE_EVENT_TYPE_INVALID = 0,
PW_TRACE_EVENT_TYPE_INSTANT = 1,
@@ -60,6 +63,16 @@ typedef enum {
PW_TRACE_EVENT_TYPE_DURATION_GROUP_END = 9,
} pw_trace_EventType;
+typedef struct {
+ uint32_t trace_token;
+ pw_trace_EventType event_type;
+ const char* module;
+ uint8_t flags;
+ uint32_t trace_id;
+ size_t data_size;
+ const void* data_buffer;
+} pw_trace_tokenized_TraceEvent;
+
// This should not be called directly, instead use the PW_TRACE_* macros.
void pw_trace_TraceEvent(uint32_t trace_token,
pw_trace_EventType event_type,
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h b/pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h
index 6d9f23def..48ac84811 100644
--- a/pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/trace_callback.h
@@ -22,8 +22,7 @@
#include <stdint.h>
#include <string.h>
-#include <span>
-
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_trace_tokenized/config.h"
#include "pw_trace_tokenized/trace_tokenized.h"
@@ -65,12 +64,7 @@ typedef uint32_t pw_trace_TraceEventReturnFlags;
typedef size_t pw_trace_EventCallbackHandle;
typedef pw_trace_TraceEventReturnFlags (*pw_trace_EventCallback)(
- void* user_data,
- uint32_t trace_ref,
- pw_trace_EventType event_type,
- const char* module,
- uint32_t trace_id,
- uint8_t flags);
+ void* user_data, pw_trace_tokenized_TraceEvent* event);
pw_Status pw_trace_RegisterEventCallback(
pw_trace_EventCallback callback,
@@ -143,6 +137,7 @@ class CallbacksImpl {
};
using EventCallback = pw_trace_EventCallback;
using EventCallbackHandle = pw_trace_EventCallbackHandle;
+ using TraceEvent = pw_trace_tokenized_TraceEvent;
struct EventCallbacks {
void* user_data;
EventCallback callback;
@@ -157,8 +152,7 @@ class CallbacksImpl {
pw::Status UnregisterSink(SinkHandle handle);
pw::Status UnregisterAllSinks();
SinkCallbacks* GetSink(SinkHandle handle);
- void CallSinks(std::span<const std::byte> header,
- std::span<const std::byte> data);
+ void CallSinks(span<const std::byte> header, span<const std::byte> data);
pw::Status RegisterEventCallback(
EventCallback callback,
@@ -169,12 +163,7 @@ class CallbacksImpl {
pw::Status UnregisterAllEventCallbacks();
EventCallbacks* GetEventCallback(EventCallbackHandle handle);
pw_trace_TraceEventReturnFlags CallEventCallbacks(
- CallOnEveryEvent called_on_every_event,
- uint32_t trace_ref,
- EventType event_type,
- const char* module,
- uint32_t trace_id,
- uint8_t flags);
+ CallOnEveryEvent called_on_every_event, TraceEvent* event);
size_t GetCalledOnEveryEventCount() const {
return called_on_every_event_count_;
}
@@ -196,7 +185,7 @@ class CallbacksImpl {
// Example: pw::trace::Callbacks::Instance().UnregisterAllSinks();
class Callbacks {
public:
- static CallbacksImpl& Instance() { return instance_; };
+ static CallbacksImpl& Instance() { return instance_; }
private:
static CallbacksImpl instance_;
@@ -214,7 +203,7 @@ class RegisterCallbackWhenCreated {
void* user_data = nullptr) {
Callbacks::Instance()
.RegisterEventCallback(event_callback, called_on_every_event, user_data)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
RegisterCallbackWhenCreated(CallbacksImpl::SinkStartBlock sink_start,
CallbacksImpl::SinkAddBytes sink_add_bytes,
@@ -222,7 +211,7 @@ class RegisterCallbackWhenCreated {
void* user_data = nullptr) {
Callbacks::Instance()
.RegisterSink(sink_start, sink_add_bytes, sink_end, user_data)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
};
diff --git a/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h b/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
index ec5b0ea1e..65e52b5ff 100644
--- a/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
+++ b/pw_trace_tokenized/public/pw_trace_tokenized/trace_tokenized.h
@@ -149,7 +149,7 @@ class TokenizedTraceImpl {
// Example: pw::trace::TokenizedTrace::Instance().Enable(true);
class TokenizedTrace {
public:
- static TokenizedTraceImpl& Instance() { return instance_; };
+ static TokenizedTraceImpl& Instance() { return instance_; }
private:
static TokenizedTraceImpl instance_;
diff --git a/pw_trace_tokenized/py/BUILD.gn b/pw_trace_tokenized/py/BUILD.gn
index 4efae83ed..49ba7e0c5 100644
--- a/pw_trace_tokenized/py/BUILD.gn
+++ b/pw_trace_tokenized/py/BUILD.gn
@@ -29,8 +29,10 @@ pw_python_package("py") {
]
python_deps = [
"$dir_pw_hdlc/py",
+ "$dir_pw_log:protos.python",
"$dir_pw_tokenizer/py",
"$dir_pw_trace/py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py b/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
index 1c3aa9b9d..041c6bcdf 100755
--- a/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
+++ b/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py
@@ -26,16 +26,17 @@ python pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py -s localhost:33000
# pylint: enable=line-too-long
import argparse
-import logging
import glob
+import logging
from pathlib import Path
+import socket
import sys
from typing import Collection, Iterable, Iterator
-import serial # type: ignore
+
+import serial
from pw_tokenizer import database
from pw_trace import trace
from pw_hdlc.rpc import HdlcRpcClient, default_channels
-from pw_hdlc.rpc_console import SocketClientImpl
from pw_trace_tokenized import trace_tokenized
_LOG = logging.getLogger('pw_trace_tokenizer')
@@ -46,15 +47,40 @@ SOCKET_PORT = 33000
MKFIFO_MODE = 0o666
+class SocketClientImpl:
+ def __init__(self, config: str):
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ socket_server = ''
+ socket_port = 0
+
+ if config == 'default':
+ socket_server = SOCKET_SERVER
+ socket_port = SOCKET_PORT
+ else:
+ socket_server, socket_port_str = config.split(':')
+ socket_port = int(socket_port_str)
+ self.socket.connect((socket_server, socket_port))
+
+ def write(self, data: bytes):
+ self.socket.sendall(data)
+
+ def read(self, num_bytes: int = PW_RPC_MAX_PACKET_SIZE):
+ return self.socket.recv(num_bytes)
+
+
def _expand_globs(globs: Iterable[str]) -> Iterator[Path]:
for pattern in globs:
for file in glob.glob(pattern, recursive=True):
yield Path(file)
-def get_hdlc_rpc_client(device: str, baudrate: int,
- proto_globs: Collection[str], socket_addr: str,
- **kwargs):
+def get_hdlc_rpc_client(
+ device: str,
+ baudrate: int,
+ proto_globs: Collection[str],
+ socket_addr: str,
+ **kwargs,
+):
"""Get the HdlcRpcClient based on arguments."""
del kwargs # ignore
if not proto_globs:
@@ -63,13 +89,17 @@ def get_hdlc_rpc_client(device: str, baudrate: int,
protos = list(_expand_globs(proto_globs))
if not protos:
- _LOG.critical('No .proto files were found with %s',
- ', '.join(proto_globs))
+ _LOG.critical(
+ 'No .proto files were found with %s', ', '.join(proto_globs)
+ )
_LOG.critical('At least one .proto file is required')
return 1
- _LOG.debug('Found %d .proto files found with %s', len(protos),
- ', '.join(proto_globs))
+ _LOG.debug(
+ 'Found %d .proto files found with %s',
+ len(protos),
+ ', '.join(proto_globs),
+ )
# TODO(rgoliver): When pw has a generalized transport for RPC this should
# use it so it isn't specific to HDLC
@@ -106,48 +136,66 @@ def _parse_args():
parser = argparse.ArgumentParser(
description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-d', '--device', help='the serial port to use')
- parser.add_argument('-b',
- '--baudrate',
- type=int,
- default=115200,
- help='the baud rate to use')
- group.add_argument('-s',
- '--socket-addr',
- type=str,
- help='use socket to connect to server, type default for\
- localhost:33000, or manually input the server address:port')
- parser.add_argument('-o',
- '--trace_output',
- dest='trace_output_file',
- help=('The json file to which to write the output.'))
+ parser.add_argument(
+ '-b',
+ '--baudrate',
+ type=int,
+ default=115200,
+ help='the baud rate to use',
+ )
+ group.add_argument(
+ '-s',
+ '--socket-addr',
+ type=str,
+ help='use socket to connect to server, type default for\
+ localhost:33000, or manually input the server address:port',
+ )
+ parser.add_argument(
+ '-o',
+ '--trace_output',
+ dest='trace_output_file',
+ help=('The json file to which to write the output.'),
+ )
parser.add_argument(
'-t',
'--trace_token_database',
- help='Databases (ELF, binary, or CSV) to use to lookup trace tokens.')
- parser.add_argument('proto_globs',
- nargs='+',
- help='glob pattern for .proto files')
+ help='Databases (ELF, binary, or CSV) to use to lookup trace tokens.',
+ )
+ parser.add_argument(
+ 'proto_globs', nargs='+', help='glob pattern for .proto files'
+ )
parser.add_argument(
'-f',
'--ticks_per_second',
type=int,
dest='ticks_per_second',
default=1000,
- help=('The clock rate of the trace events (Default 1000).'))
+ help=('The clock rate of the trace events (Default 1000).'),
+ )
+ parser.add_argument(
+ '--time_offset',
+ type=int,
+ dest='time_offset',
+ default=0,
+ help=('Time offset (us) of the trace events (Default 0).'),
+ )
return parser.parse_args()
def _main(args):
- token_database = \
- database.load_token_database(args.trace_token_database, domain="trace")
+ token_database = database.load_token_database(
+ args.trace_token_database, domain="trace"
+ )
_LOG.info(database.database_summary(token_database))
client = get_hdlc_rpc_client(**vars(args))
data = get_trace_data_from_device(client)
- events = trace_tokenized.get_trace_events([token_database], data,
- args.ticks_per_second)
+ events = trace_tokenized.get_trace_events(
+ [token_database], data, args.ticks_per_second, args.time_offset
+ )
json_lines = trace.generate_trace_json(events)
trace_tokenized.save_trace_file(json_lines, args.trace_output_file)
diff --git a/pw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py b/pw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py
index 25108a5be..aa515ee87 100755
--- a/pw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py
+++ b/pw_trace_tokenized/py/pw_trace_tokenized/trace_tokenized.py
@@ -40,7 +40,7 @@ def varint_decode(encoded):
shift = 0
for byte in encoded:
count += 1
- result |= (byte & 0x7f) << shift
+ result |= (byte & 0x7F) << shift
if not byte & 0x80:
return result, count
@@ -94,18 +94,20 @@ def has_data(token_string):
def create_trace_event(token_string, timestamp_us, trace_id, data):
token_values = token_string.split("|")
- return trace.TraceEvent(event_type=get_trace_type(
- token_values[TokenIdx.EVENT_TYPE]),
- module=token_values[TokenIdx.MODULE],
- label=token_values[TokenIdx.LABEL],
- timestamp_us=timestamp_us,
- group=token_values[TokenIdx.GROUP],
- trace_id=trace_id,
- flags=token_values[TokenIdx.FLAG],
- has_data=has_data(token_string),
- data_fmt=(token_values[TokenIdx.DATA_FMT]
- if has_data(token_string) else ""),
- data=data if has_data(token_string) else b'')
+ return trace.TraceEvent(
+ event_type=get_trace_type(token_values[TokenIdx.EVENT_TYPE]),
+ module=token_values[TokenIdx.MODULE],
+ label=token_values[TokenIdx.LABEL],
+ timestamp_us=timestamp_us,
+ group=token_values[TokenIdx.GROUP],
+ trace_id=trace_id,
+ flags=token_values[TokenIdx.FLAG],
+ has_data=has_data(token_string),
+ data_fmt=(
+ token_values[TokenIdx.DATA_FMT] if has_data(token_string) else ""
+ ),
+ data=data if has_data(token_string) else b'',
+ )
def parse_trace_event(buffer, db, last_time, ticks_per_second):
@@ -113,7 +115,7 @@ def parse_trace_event(buffer, db, last_time, ticks_per_second):
us_per_tick = 1000000 / ticks_per_second
idx = 0
# Read token
- token = struct.unpack('I', buffer[idx:idx + 4])[0]
+ token = struct.unpack('I', buffer[idx : idx + 4])[0]
idx += 4
# Decode token
@@ -143,11 +145,13 @@ def parse_trace_event(buffer, db, last_time, ticks_per_second):
return create_trace_event(token_string, timestamp_us, trace_id, data)
-def get_trace_events(databases, raw_trace_data, ticks_per_second):
+def get_trace_events(
+ databases, raw_trace_data, ticks_per_second, time_offset: int
+):
"""Handles the decoding traces."""
db = tokens.Database.merged(*databases)
- last_timestamp = 0
+ last_timestamp = time_offset
events = []
idx = 0
@@ -158,8 +162,12 @@ def get_trace_events(databases, raw_trace_data, ticks_per_second):
_LOG.error("incomplete file")
break
- event = parse_trace_event(raw_trace_data[idx + 1:idx + 1 + size], db,
- last_timestamp, ticks_per_second)
+ event = parse_trace_event(
+ raw_trace_data[idx + 1 : idx + 1 + size],
+ db,
+ last_timestamp,
+ ticks_per_second,
+ )
if event:
last_timestamp = event.timestamp_us
events.append(event)
@@ -183,10 +191,14 @@ def save_trace_file(trace_lines, file_name):
output_file.write("{}]")
-def get_trace_events_from_file(databases, input_file_name, ticks_per_second):
+def get_trace_events_from_file(
+ databases, input_file_name, ticks_per_second, time_offset: int
+):
"""Get trace events from a file."""
raw_trace_data = get_trace_data_from_file(input_file_name)
- return get_trace_events(databases, raw_trace_data, ticks_per_second)
+ return get_trace_events(
+ databases, raw_trace_data, ticks_per_second, time_offset
+ )
def _parse_args():
@@ -194,35 +206,49 @@ def _parse_args():
parser = argparse.ArgumentParser(
description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
parser.add_argument(
'databases',
nargs='+',
action=database.LoadTokenDatabases,
- help='Databases (ELF, binary, or CSV) to use to lookup tokens.')
+ help='Databases (ELF, binary, or CSV) to use to lookup tokens.',
+ )
parser.add_argument(
'-i',
'--input',
dest='input_file',
- help='The binary trace input file, generated using trace_to_file.h.')
- parser.add_argument('-o',
- '--output',
- dest='output_file',
- help=('The json file to which to write the output.'))
+ help='The binary trace input file, generated using trace_to_file.h.',
+ )
+ parser.add_argument(
+ '-o',
+ '--output',
+ dest='output_file',
+ help=('The json file to which to write the output.'),
+ )
parser.add_argument(
'-t',
'--ticks_per_second',
type=int,
dest='ticks_per_second',
default=1000,
- help=('The clock rate of the trace events (Default 1000).'))
+ help=('The clock rate of the trace events (Default 1000).'),
+ )
+ parser.add_argument(
+ '--time_offset',
+ type=int,
+ dest='time_offset',
+ default=0,
+ help=('Time offset (us) of the trace events (Default 0).'),
+ )
return parser.parse_args()
def _main(args):
- events = get_trace_events_from_file(args.databases, args.input_file,
- args.ticks_per_second)
+ events = get_trace_events_from_file(
+ args.databases, args.input_file, args.ticks_per_second, args.time_offset
+ )
json_lines = trace.generate_trace_json(events)
save_trace_file(json_lines, args.output_file)
diff --git a/pw_trace_tokenized/py/setup.cfg b/pw_trace_tokenized/py/setup.cfg
index 5a5cfb234..98038d397 100644
--- a/pw_trace_tokenized/py/setup.cfg
+++ b/pw_trace_tokenized/py/setup.cfg
@@ -22,9 +22,8 @@ description = pw_trace backend to tokenize trace events
packages = find:
zip_safe = False
install_requires =
- pw_hdlc
- pw_tokenizer
- pw_trace
+ pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
[options.package_data]
pw_trace_tokenized = py.typed
diff --git a/pw_trace_tokenized/trace.cc b/pw_trace_tokenized/trace.cc
index ccc9ae56b..5547c6298 100644
--- a/pw_trace_tokenized/trace.cc
+++ b/pw_trace_tokenized/trace.cc
@@ -27,6 +27,8 @@ namespace trace {
TokenizedTraceImpl TokenizedTrace::instance_;
CallbacksImpl Callbacks::instance_;
+using TraceEvent = pw_trace_tokenized_TraceEvent;
+
void TokenizedTraceImpl::HandleTraceEvent(uint32_t trace_token,
EventType event_type,
const char* module,
@@ -40,16 +42,44 @@ void TokenizedTraceImpl::HandleTraceEvent(uint32_t trace_token,
return;
}
+ TraceEvent event = {
+ .trace_token = trace_token,
+ .event_type = event_type,
+ .module = module,
+ .flags = flags,
+ .trace_id = trace_id,
+ .data_size = data_size,
+ .data_buffer = data_buffer,
+ };
+
+ // Call any event callback which is registered to receive every event.
+ pw_trace_TraceEventReturnFlags ret_flags = 0;
+ ret_flags |= Callbacks::Instance().CallEventCallbacks(
+ CallbacksImpl::kCallOnEveryEvent, &event);
+ // Return if disabled.
+ if ((PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT & ret_flags) || !enabled_) {
+ return;
+ }
+
+ // Call any event callback not already called.
+ ret_flags |= Callbacks::Instance().CallEventCallbacks(
+ CallbacksImpl::kCallOnlyWhenEnabled, &event);
+ // Return if disabled (from a callback) or if a callback has indicated the
+ // sample should be skipped.
+ if ((PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT & ret_flags) || !enabled_) {
+ return;
+ }
+
// Create trace event
PW_TRACE_QUEUE_LOCK();
if (!event_queue_
- .TryPushBack(trace_token,
- event_type,
- module,
- trace_id,
- flags,
- data_buffer,
- data_size)
+ .TryPushBack(event.trace_token,
+ event.event_type,
+ event.module,
+ event.trace_id,
+ event.flags,
+ event.data_buffer,
+ event.data_size)
.ok()) {
// Queue full dropping sample
// TODO(rgoliver): Allow other strategies, for example: drop oldest, try
@@ -66,6 +96,11 @@ void TokenizedTraceImpl::HandleTraceEvent(uint32_t trace_token,
}
PW_TRACE_UNLOCK();
}
+
+ // Disable after processing if an event callback had set the flag.
+ if (PW_TRACE_EVENT_RETURN_FLAGS_DISABLE_AFTER_PROCESSING & ret_flags) {
+ enabled_ = false;
+ }
}
void TokenizedTraceImpl::HandleNextItemInQueue(
@@ -73,41 +108,11 @@ void TokenizedTraceImpl::HandleNextItemInQueue(
// Get next item in queue
uint32_t trace_token = event_block->trace_token;
EventType event_type = event_block->event_type;
- const char* module = event_block->module;
uint32_t trace_id = event_block->trace_id;
- uint8_t flags = event_block->flags;
const std::byte* data_buffer =
const_cast<const std::byte*>(event_block->data_buffer);
size_t data_size = event_block->data_size;
- // Call any event callback which is registered to receive every event.
- pw_trace_TraceEventReturnFlags ret_flags = 0;
- ret_flags |=
- Callbacks::Instance().CallEventCallbacks(CallbacksImpl::kCallOnEveryEvent,
- trace_token,
- event_type,
- module,
- trace_id,
- flags);
- // Return if disabled.
- if ((PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT & ret_flags) || !enabled_) {
- return;
- }
-
- // Call any event callback not already called.
- ret_flags |= Callbacks::Instance().CallEventCallbacks(
- CallbacksImpl::kCallOnlyWhenEnabled,
- trace_token,
- event_type,
- module,
- trace_id,
- flags);
- // Return if disabled (from a callback) or if a callback has indicated the
- // sample should be skipped.
- if ((PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT & ret_flags) || !enabled_) {
- return;
- }
-
// Create header to store trace info
static constexpr size_t kMaxHeaderSize =
sizeof(trace_token) + pw::varint::kMaxVarint64SizeBytes + // time
@@ -124,53 +129,38 @@ void TokenizedTraceImpl::HandleNextItemInQueue(
: PW_TRACE_GET_TIME_DELTA(last_trace_time_, trace_time);
header_size += pw::varint::Encode(
delta,
- std::span<std::byte>(&header[header_size], kMaxHeaderSize - header_size));
+ span<std::byte>(&header[header_size], kMaxHeaderSize - header_size));
last_trace_time_ = trace_time;
// Calculate packet id if needed.
if (PW_TRACE_HAS_TRACE_ID(event_type)) {
- header_size +=
- pw::varint::Encode(trace_id,
- std::span<std::byte>(&header[header_size],
- kMaxHeaderSize - header_size));
+ header_size += pw::varint::Encode(
+ trace_id,
+ span<std::byte>(&header[header_size], kMaxHeaderSize - header_size));
}
// Send encoded output to any registered trace sinks.
Callbacks::Instance().CallSinks(
- std::span<const std::byte>(header, header_size),
- std::span<const std::byte>(
- reinterpret_cast<const std::byte*>(data_buffer), data_size));
- // Disable after processing if an event callback had set the flag.
- if (PW_TRACE_EVENT_RETURN_FLAGS_DISABLE_AFTER_PROCESSING & ret_flags) {
- enabled_ = false;
- }
+ span<const std::byte>(header, header_size),
+ span<const std::byte>(reinterpret_cast<const std::byte*>(data_buffer),
+ data_size));
}
pw_trace_TraceEventReturnFlags CallbacksImpl::CallEventCallbacks(
- CallOnEveryEvent called_on_every_event,
- uint32_t trace_ref,
- EventType event_type,
- const char* module,
- uint32_t trace_id,
- uint8_t flags) {
+ CallOnEveryEvent called_on_every_event, TraceEvent* event) {
pw_trace_TraceEventReturnFlags ret_flags = 0;
for (size_t i = 0; i < PW_TRACE_CONFIG_MAX_EVENT_CALLBACKS; i++) {
if (event_callbacks_[i].callback &&
event_callbacks_[i].called_on_every_event == called_on_every_event) {
ret_flags |= Callbacks::Instance().GetEventCallback(i)->callback(
- event_callbacks_[i].user_data,
- trace_ref,
- event_type,
- module,
- trace_id,
- flags);
+ event_callbacks_[i].user_data, event);
}
}
return ret_flags;
}
-void CallbacksImpl::CallSinks(std::span<const std::byte> header,
- std::span<const std::byte> data) {
+void CallbacksImpl::CallSinks(span<const std::byte> header,
+ span<const std::byte> data) {
for (size_t sink_idx = 0; sink_idx < PW_TRACE_CONFIG_MAX_SINKS; sink_idx++) {
void* user_data = sink_callbacks_[sink_idx].user_data;
if (sink_callbacks_[sink_idx].start_block) {
@@ -230,7 +220,7 @@ pw::Status CallbacksImpl::UnregisterSink(SinkHandle handle) {
pw::Status CallbacksImpl::UnregisterAllSinks() {
for (size_t sink_idx = 0; sink_idx < PW_TRACE_CONFIG_MAX_SINKS; sink_idx++) {
UnregisterSink(sink_idx)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
return PW_STATUS_OK;
}
@@ -283,7 +273,7 @@ pw::Status CallbacksImpl::UnregisterEventCallback(EventCallbackHandle handle) {
pw::Status CallbacksImpl::UnregisterAllEventCallbacks() {
for (size_t i = 0; i < PW_TRACE_CONFIG_MAX_EVENT_CALLBACKS; i++) {
UnregisterEventCallback(i)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
return PW_STATUS_OK;
}
diff --git a/pw_trace_tokenized/trace_buffer.cc b/pw_trace_tokenized/trace_buffer.cc
index 31314ec7a..b1de8e717 100644
--- a/pw_trace_tokenized/trace_buffer.cc
+++ b/pw_trace_tokenized/trace_buffer.cc
@@ -15,9 +15,8 @@
//
#include "pw_trace_tokenized/trace_buffer.h"
-#include <span>
-
#include "pw_ring_buffer/prefixed_entry_ring_buffer.h"
+#include "pw_span/span.h"
#include "pw_trace_tokenized/trace_callback.h"
namespace pw {
@@ -28,11 +27,11 @@ class TraceBuffer {
public:
TraceBuffer() {
ring_buffer_.SetBuffer(raw_buffer_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
Callbacks::Instance()
.RegisterSink(
TraceSinkStartBlock, TraceSinkAddBytes, TraceSinkEndBlock, this)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
static void TraceSinkStartBlock(void* user_data, size_t size) {
@@ -63,18 +62,18 @@ class TraceBuffer {
return; // Block is too large, skipping.
}
buffer->ring_buffer_
- .PushBack(std::span<const std::byte>(&buffer->current_block_[0],
- buffer->block_size_))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .PushBack(span<const std::byte>(&buffer->current_block_[0],
+ buffer->block_size_))
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
pw::ring_buffer::PrefixedEntryRingBuffer& RingBuffer() {
return ring_buffer_;
- };
+ }
ConstByteSpan DeringAndViewRawBuffer() {
ring_buffer_.Dering()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return ByteSpan(raw_buffer_, ring_buffer_.TotalUsedBytes());
}
diff --git a/pw_trace_tokenized/trace_buffer_log.cc b/pw_trace_tokenized/trace_buffer_log.cc
index 11019bab1..8a58cc0dc 100644
--- a/pw_trace_tokenized/trace_buffer_log.cc
+++ b/pw_trace_tokenized/trace_buffer_log.cc
@@ -15,10 +15,9 @@
//
#include "pw_trace_tokenized/trace_buffer_log.h"
-#include <span>
-
#include "pw_base64/base64.h"
#include "pw_log/log.h"
+#include "pw_span/span.h"
#include "pw_string/string_builder.h"
#include "pw_trace_tokenized/trace_buffer.h"
@@ -53,17 +52,16 @@ pw::Status DumpTraceBufferToLog() {
pw::trace::GetBuffer();
size_t bytes_read = 0;
PW_LOG_INFO("[TRACE] begin");
- while (trace_buffer->PeekFront(std::span(entry_buffer).subspan(1),
- &bytes_read) != pw::Status::OutOfRange()) {
+ while (trace_buffer->PeekFront(span(entry_buffer).subspan(1), &bytes_read) !=
+ pw::Status::OutOfRange()) {
trace_buffer->PopFront()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
entry_buffer[0] = static_cast<std::byte>(bytes_read);
// The entry buffer is formatted as (size, entry) with an extra byte as
// a header to the entry. The calcuation of bytes_read + 1 represents
// the extra size header.
- size_t to_write =
- pw::base64::Encode(std::span(entry_buffer, bytes_read + 1),
- std::span(entry_base64_buffer));
+ size_t to_write = pw::base64::Encode(span(entry_buffer, bytes_read + 1),
+ span(entry_base64_buffer));
size_t space_left = line_builder.max_size() - line_builder.size();
size_t written = 0;
while (to_write - written >= space_left) {
diff --git a/pw_trace_tokenized/trace_buffer_log_test.cc b/pw_trace_tokenized/trace_buffer_log_test.cc
index 20c9366be..4f89fcf71 100644
--- a/pw_trace_tokenized/trace_buffer_log_test.cc
+++ b/pw_trace_tokenized/trace_buffer_log_test.cc
@@ -19,20 +19,22 @@
#include "gtest/gtest.h"
#include "pw_trace/trace.h"
+namespace pw::trace {
+namespace {
+
TEST(TokenizedTrace, DumpSmallBuffer) {
- // TODO(pwbug/266): This test only verifies that the dump function does not
+ // TODO(b/235283406): This test only verifies that the dump function does not
// crash, and requires manual inspection to confirm that the log output is
// correct. When there is support to mock and verify the calls to pw_log,
// these tests should be improved to validate the output.
PW_TRACE_SET_ENABLED(true);
PW_TRACE_INSTANT("test1");
PW_TRACE_INSTANT("test2");
- pw::trace::DumpTraceBufferToLog()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), pw::trace::DumpTraceBufferToLog());
}
TEST(TokenizedTrace, DumpLargeBuffer) {
- // TODO(pwbug/266): This test only verifies that the dump function does not
+ // TODO(b/235283406): This test only verifies that the dump function does not
// crash, and requires manual inspection to confirm that the log output is
// correct. When there is support to mock and verify the calls to pw_log,
// these tests should be improved to validate the output.
@@ -40,6 +42,8 @@ TEST(TokenizedTrace, DumpLargeBuffer) {
for (int i = 0; i < 100; i++) {
PW_TRACE_INSTANT("test");
}
- pw::trace::DumpTraceBufferToLog()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(OkStatus(), pw::trace::DumpTraceBufferToLog());
}
+
+} // namespace
+} // namespace pw::trace
diff --git a/pw_trace_tokenized/trace_buffer_test.cc b/pw_trace_tokenized/trace_buffer_test.cc
index c2f4b764f..ca25c68a6 100644
--- a/pw_trace_tokenized/trace_buffer_test.cc
+++ b/pw_trace_tokenized/trace_buffer_test.cc
@@ -83,7 +83,7 @@ TEST(TokenizedTrace, Data) {
std::byte value[expected_max_bytes_used];
size_t bytes_read = 0;
- EXPECT_EQ(buf->PeekFront(std::span<std::byte>(value), &bytes_read),
+ EXPECT_EQ(buf->PeekFront(pw::span<std::byte>(value), &bytes_read),
pw::OkStatus());
// read size is minus 1, since doesn't include varint size
@@ -122,7 +122,7 @@ TEST(TokenizedTrace, Overflow) {
while (buf->EntryCount() > 0) {
std::byte value[PW_TRACE_BUFFER_MAX_BLOCK_SIZE_BYTES];
size_t bytes_read = 0;
- EXPECT_EQ(buf->PeekFront(std::span<std::byte>(value), &bytes_read),
+ EXPECT_EQ(buf->PeekFront(pw::span<std::byte>(value), &bytes_read),
pw::OkStatus());
EXPECT_EQ(buf->PopFront(), pw::OkStatus());
size_t entry_count;
diff --git a/pw_trace_tokenized/trace_rpc_service_nanopb.cc b/pw_trace_tokenized/trace_rpc_service_nanopb.cc
index 55b3f43f2..38e8d8205 100644
--- a/pw_trace_tokenized/trace_rpc_service_nanopb.cc
+++ b/pw_trace_tokenized/trace_rpc_service_nanopb.cc
@@ -42,11 +42,10 @@ void TraceService::GetTraceData(
pw::ring_buffer::PrefixedEntryRingBuffer* trace_buffer =
pw::trace::GetBuffer();
- while (trace_buffer->PeekFront(
- std::as_writable_bytes(std::span(buffer.data.bytes)), &size) !=
- pw::Status::OutOfRange()) {
+ while (trace_buffer->PeekFront(as_writable_bytes(span(buffer.data.bytes)),
+ &size) != pw::Status::OutOfRange()) {
trace_buffer->PopFront()
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
buffer.data.size = size;
pw::Status status = writer.Write(buffer);
if (!status.ok()) {
@@ -55,6 +54,6 @@ void TraceService::GetTraceData(
break;
}
}
- writer.Finish().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ writer.Finish().IgnoreError(); // TODO(b/242598609): Handle Status properly
}
} // namespace pw::trace
diff --git a/pw_trace_tokenized/trace_test.cc b/pw_trace_tokenized/trace_test.cc
index d2dd31618..72457ab96 100644
--- a/pw_trace_tokenized/trace_test.cc
+++ b/pw_trace_tokenized/trace_test.cc
@@ -56,27 +56,25 @@ class TraceTestInterface {
TraceTestInterface() {
PW_TRACE_SET_ENABLED(true);
- pw::trace::Callbacks::Instance()
- .RegisterSink(TraceSinkStartBlock,
- TraceSinkAddBytes,
- TraceSinkEndBlock,
- this,
- &sink_handle_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- pw::trace::Callbacks::Instance()
- .RegisterEventCallback(TraceEventCallback,
- pw::trace::CallbacksImpl::kCallOnlyWhenEnabled,
- this,
- &event_callback_handle_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(pw::OkStatus(),
+ pw::trace::Callbacks::Instance().RegisterSink(TraceSinkStartBlock,
+ TraceSinkAddBytes,
+ TraceSinkEndBlock,
+ this,
+ &sink_handle_));
+ EXPECT_EQ(pw::OkStatus(),
+ pw::trace::Callbacks::Instance().RegisterEventCallback(
+ TraceEventCallback,
+ pw::trace::CallbacksImpl::kCallOnlyWhenEnabled,
+ this,
+ &event_callback_handle_));
}
~TraceTestInterface() {
- pw::trace::Callbacks::Instance()
- .UnregisterSink(sink_handle_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- pw::trace::Callbacks::Instance()
- .UnregisterEventCallback(event_callback_handle_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(pw::OkStatus(),
+ pw::trace::Callbacks::Instance().UnregisterSink(sink_handle_));
+ EXPECT_EQ(pw::OkStatus(),
+ pw::trace::Callbacks::Instance().UnregisterEventCallback(
+ event_callback_handle_));
}
// ActionOnEvent will perform a specific action within the callback when an
// event matches one of the characteristics of event_match_.
@@ -90,21 +88,16 @@ class TraceTestInterface {
// buffer_ in the TraceSink callback, that way it only gets added to the
// buffer if tracing is enabled and the sample was not surpressed.
static pw_trace_TraceEventReturnFlags TraceEventCallback(
- void* user_data,
- uint32_t trace_ref,
- pw_trace_EventType event_type,
- const char* module,
- uint32_t trace_id,
- uint8_t /* flags */) {
+ void* user_data, pw_trace_tokenized_TraceEvent* event) {
TraceTestInterface* test_interface =
reinterpret_cast<TraceTestInterface*>(user_data);
pw_trace_TraceEventReturnFlags ret = 0;
if (test_interface->action_ != ActionOnEvent::None &&
- (test_interface->event_match_.trace_ref == trace_ref ||
- test_interface->event_match_.event_type == event_type ||
- test_interface->event_match_.module == module ||
- (trace_id != PW_TRACE_TRACE_ID_DEFAULT &&
- test_interface->event_match_.trace_id == trace_id))) {
+ (test_interface->event_match_.trace_ref == event->trace_token ||
+ test_interface->event_match_.event_type == event->event_type ||
+ test_interface->event_match_.module == event->module ||
+ (event->trace_id != PW_TRACE_TRACE_ID_DEFAULT &&
+ test_interface->event_match_.trace_id == event->trace_id))) {
if (test_interface->action_ == ActionOnEvent::Skip) {
ret |= PW_TRACE_EVENT_RETURN_FLAGS_SKIP_EVENT;
} else if (test_interface->action_ == ActionOnEvent::Enable) {
@@ -116,8 +109,8 @@ class TraceTestInterface {
}
}
- test_interface->current_trace_event_ =
- TraceInfo{trace_ref, event_type, module, trace_id};
+ test_interface->current_trace_event_ = TraceInfo{
+ event->trace_token, event->event_type, event->module, event->trace_id};
return ret;
}
@@ -574,8 +567,7 @@ TEST(TokenizedTrace, QueueSimple) {
constexpr size_t kQueueSize = 5;
pw::trace::internal::TraceQueue<kQueueSize> queue;
constexpr size_t kTestNum = 1;
- queue.TryPushBack(QUEUE_TESTS_ARGS(kTestNum))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ ASSERT_EQ(pw::OkStatus(), queue.TryPushBack(QUEUE_TESTS_ARGS(kTestNum)));
EXPECT_FALSE(queue.IsEmpty());
EXPECT_FALSE(queue.IsFull());
EXPECT_TRUE(QUEUE_CHECK_RESULT(kQueueSize, queue.PeekFront(), kTestNum));
diff --git a/pw_transfer/BUILD.bazel b/pw_transfer/BUILD.bazel
index 3bae8c0b5..577d658c8 100644
--- a/pw_transfer/BUILD.bazel
+++ b/pw_transfer/BUILD.bazel
@@ -16,7 +16,6 @@ load("//pw_build:pigweed.bzl", "pw_cc_binary", "pw_cc_library", "pw_cc_test")
load("//pw_protobuf_compiler:proto.bzl", "pw_proto_library")
load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
-load("@rules_proto_grpc//js:defs.bzl", "js_proto_library")
package(default_visibility = ["//visibility:public"])
@@ -41,6 +40,7 @@ pw_cc_library(
"public/pw_transfer/internal/client_context.h",
"public/pw_transfer/internal/context.h",
"public/pw_transfer/internal/event.h",
+ "public/pw_transfer/internal/protocol.h",
"public/pw_transfer/internal/server_context.h",
"rate_estimate.cc",
"server_context.cc",
@@ -54,7 +54,8 @@ pw_cc_library(
includes = ["public"],
deps = [
":config",
- ":transfer_pwpb",
+ ":transfer_pwpb.pwpb",
+ ":transfer_pwpb.raw_rpc",
"//pw_bytes",
"//pw_chrono:system_clock",
"//pw_containers:intrusive_list",
@@ -116,6 +117,28 @@ pw_cc_library(
)
pw_cc_library(
+ name = "atomic_file_transfer_handler",
+ srcs = ["atomic_file_transfer_handler.cc"],
+ hdrs = [
+ "public/pw_transfer/atomic_file_transfer_handler.h",
+ ],
+ includes = ["public"],
+ deps = [
+ ":atomic_file_transfer_handler_internal",
+ ":core",
+ "//pw_log",
+ "//pw_stream:std_file_stream",
+ ],
+)
+
+pw_cc_library(
+ name = "atomic_file_transfer_handler_internal",
+ srcs = [
+ "pw_transfer_private/filename_generator.h",
+ ],
+)
+
+pw_cc_library(
name = "test_helpers",
srcs = [
"pw_transfer_private/chunk_testing.h",
@@ -126,10 +149,13 @@ pw_cc_library(
],
)
-# TODO(pwbug/507): Add the client integration test to the build.
-filegroup(
- name = "integration_test",
- srcs = ["integration_test.cc"],
+pw_cc_test(
+ name = "chunk_test",
+ srcs = ["chunk_test.cc"],
+ deps = [
+ ":core",
+ "//pw_unit_test",
+ ],
)
pw_cc_test(
@@ -142,12 +168,25 @@ pw_cc_test(
)
pw_cc_test(
+ name = "atomic_file_transfer_handler_test",
+ srcs = ["atomic_file_transfer_handler_test.cc"],
+ deps = [
+ ":atomic_file_transfer_handler",
+ ":atomic_file_transfer_handler_internal",
+ ":pw_transfer",
+ "//pw_random",
+ "//pw_string",
+ "//pw_unit_test",
+ ],
+)
+
+pw_cc_test(
name = "transfer_thread_test",
srcs = ["transfer_thread_test.cc"],
deps = [
":pw_transfer",
":test_helpers",
- "//pw_rpc:thread_testing",
+ "//pw_rpc:test_helpers",
"//pw_rpc/raw:client_testing",
"//pw_rpc/raw:test_method_context",
"//pw_thread:thread",
@@ -161,7 +200,9 @@ pw_cc_test(
deps = [
":pw_transfer",
":test_helpers",
- "//pw_rpc:thread_testing",
+ "//pw_assert",
+ "//pw_containers",
+ "//pw_rpc:test_helpers",
"//pw_rpc/raw:test_method_context",
"//pw_thread:thread",
"//pw_unit_test",
@@ -174,7 +215,7 @@ pw_cc_test(
deps = [
":client",
":test_helpers",
- "//pw_rpc:thread_testing",
+ "//pw_rpc:test_helpers",
"//pw_rpc/raw:client_testing",
"//pw_thread:sleep",
"//pw_thread:thread",
@@ -186,8 +227,9 @@ pw_cc_binary(
name = "test_rpc_server",
srcs = ["test_rpc_server.cc"],
deps = [
+ ":atomic_file_transfer_handler",
":pw_transfer",
- ":test_server_pwpb",
+ ":test_server_pwpb.raw_rpc",
"//pw_log",
"//pw_rpc/system_server",
"//pw_stream:std_file_stream",
@@ -213,9 +255,9 @@ py_proto_library(
srcs = ["transfer.proto"],
)
-js_proto_library(
- name = "transfer_proto_tspb",
- protos = [":transfer_proto"],
+java_lite_proto_library(
+ name = "transfer_proto_java_lite",
+ deps = [":transfer_proto"],
)
proto_library(
@@ -224,9 +266,9 @@ proto_library(
"test_server.proto",
],
import_prefix = "pw_transfer_test",
- strip_import_prefix = "//pw_transfer",
+ strip_import_prefix = "/pw_transfer",
deps = [
- "//pw_protobuf:common_protos",
+ "//pw_protobuf:common_proto",
],
)
diff --git a/pw_transfer/BUILD.gn b/pw_transfer/BUILD.gn
index cf47f33fc..9b1eb7fff 100644
--- a/pw_transfer/BUILD.gn
+++ b/pw_transfer/BUILD.gn
@@ -78,6 +78,7 @@ pw_source_set("core") {
public_configs = [ ":public_include_path" ]
public_deps = [
":config",
+ ":proto.pwpb",
"$dir_pw_chrono:system_clock",
"$dir_pw_preprocessor",
"$dir_pw_rpc:client",
@@ -89,11 +90,11 @@ pw_source_set("core") {
dir_pw_assert,
dir_pw_bytes,
dir_pw_result,
+ dir_pw_span,
dir_pw_status,
dir_pw_stream,
]
deps = [
- ":proto.pwpb",
dir_pw_log,
dir_pw_protobuf,
dir_pw_varint,
@@ -111,6 +112,7 @@ pw_source_set("core") {
"public/pw_transfer/internal/client_context.h",
"public/pw_transfer/internal/context.h",
"public/pw_transfer/internal/event.h",
+ "public/pw_transfer/internal/protocol.h",
"public/pw_transfer/internal/server_context.h",
"rate_estimate.cc",
"server_context.cc",
@@ -120,6 +122,26 @@ pw_source_set("core") {
visibility = [ ":*" ]
}
+pw_source_set("atomic_file_transfer_handler") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_transfer/atomic_file_transfer_handler.h" ]
+ sources = [ "atomic_file_transfer_handler.cc" ]
+ public_deps = [
+ ":core",
+ "$dir_pw_stream:std_file_stream",
+ ]
+ deps = [
+ ":atomic_file_transfer_handler_internal",
+ dir_pw_log,
+ ]
+}
+
+pw_source_set("atomic_file_transfer_handler_internal") {
+ sources = [ "pw_transfer_private/filename_generator.h" ]
+ friend = [ ":*" ]
+ visibility = [ ":*" ]
+}
+
pw_source_set("test_helpers") {
public_deps = [
":core",
@@ -141,47 +163,69 @@ pw_test_group("tests") {
# pw_transfer requires threading.
if (pw_thread_THREAD_BACKEND != "") {
- tests = [
+ tests += [
+ ":chunk_test",
":client_test",
":transfer_thread_test",
]
- # TODO(pwbug/441): Fix transfer tests on Windows and non-host builds.
+ # TODO(b/235345886): Fix transfer tests on Windows and non-host builds.
if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
pw_toolchain_SCOPE.is_host_toolchain && host_os != "win") {
tests += [
":handler_test",
+ ":atomic_file_transfer_handler_test",
":transfer_test",
]
}
}
}
+pw_test("chunk_test") {
+ sources = [ "chunk_test.cc" ]
+ deps = [ ":core" ]
+}
+
pw_test("handler_test") {
sources = [ "handler_test.cc" ]
deps = [ ":pw_transfer" ]
}
+pw_test("atomic_file_transfer_handler_test") {
+ sources = [ "atomic_file_transfer_handler_test.cc" ]
+ deps = [
+ ":atomic_file_transfer_handler",
+ ":atomic_file_transfer_handler_internal",
+ ":pw_transfer",
+ "$dir_pw_random",
+ "$dir_pw_string",
+ ]
+}
+
pw_test("transfer_test") {
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
sources = [ "transfer_test.cc" ]
deps = [
":proto.pwpb",
":pw_transfer",
":test_helpers",
- "$dir_pw_rpc:thread_testing",
+ "$dir_pw_assert",
+ "$dir_pw_containers",
+ "$dir_pw_rpc:test_helpers",
"$dir_pw_rpc/raw:test_method_context",
"$dir_pw_thread:thread",
]
}
pw_test("transfer_thread_test") {
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
sources = [ "transfer_thread_test.cc" ]
deps = [
":core",
":proto.raw_rpc",
":pw_transfer",
":test_helpers",
- "$dir_pw_rpc:thread_testing",
+ "$dir_pw_rpc:test_helpers",
"$dir_pw_rpc/raw:client_testing",
"$dir_pw_rpc/raw:test_method_context",
"$dir_pw_thread:thread",
@@ -189,11 +233,12 @@ pw_test("transfer_thread_test") {
}
pw_test("client_test") {
+ enable_if = pw_thread_THREAD_BACKEND == "$dir_pw_thread_stl:thread"
sources = [ "client_test.cc" ]
deps = [
":client",
":test_helpers",
- "$dir_pw_rpc:thread_testing",
+ "$dir_pw_rpc:test_helpers",
"$dir_pw_rpc/raw:client_testing",
"$dir_pw_thread:sleep",
"$dir_pw_thread:thread",
@@ -218,6 +263,7 @@ pw_proto_library("test_server_proto") {
pw_executable("test_rpc_server") {
sources = [ "test_rpc_server.cc" ]
deps = [
+ ":atomic_file_transfer_handler",
":pw_transfer",
":test_server_proto.raw_rpc",
"$dir_pw_rpc/system_server",
@@ -228,35 +274,91 @@ pw_executable("test_rpc_server") {
]
}
-pw_executable("integration_test") {
- sources = [ "integration_test.cc" ]
+pw_executable("integration_test_server") {
+ sources = [ "integration_test/server.cc" ]
+ deps = [
+ ":pw_transfer",
+ "$dir_pw_assert",
+ "$dir_pw_rpc/system_server",
+ "$dir_pw_rpc/system_server:socket",
+ "$dir_pw_stream",
+ "$dir_pw_stream:std_file_stream",
+ "$dir_pw_thread:thread",
+ "$dir_pw_thread_stl:thread",
+ dir_pw_log,
+ ]
+}
+
+pw_executable("integration_test_client") {
+ sources = [ "integration_test/client.cc" ]
deps = [
":client",
- ":test_server_proto.raw_rpc",
"$dir_pw_rpc:integration_testing",
+ "$dir_pw_stream:std_file_stream",
"$dir_pw_sync:binary_semaphore",
"$dir_pw_thread:thread",
dir_pw_assert,
dir_pw_log,
- dir_pw_unit_test,
+ dir_pw_status,
]
}
-pw_python_action("cpp_client_integration_test") {
- script = "$dir_pw_rpc/py/pw_rpc/testing.py"
- args = [
- "--server",
- "<TARGET_FILE(:test_rpc_server)>",
- "--client",
- "<TARGET_FILE(:integration_test)>",
- "--",
- "$pw_transfer_CPP_CPP_TRANSFER_TEST_PORT",
- "(pw_rpc:CREATE_TEMP_DIR)",
- ]
- deps = [
- ":integration_test",
- ":test_rpc_server",
- ]
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("integration_test_python_client") {
+ sources = [ "integration_test/python_client.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("integration_test_proxy") {
+ sources = [ "integration_test/proxy.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("integration_test_proxy_test") {
+ sources = [ "integration_test/proxy_test.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("integration_test_fixture") {
+ sources = [ "integration_test/test_fixture.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("cross_language_small_test") {
+ sources = [ "integration_test/cross_language_small_test.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("cross_language_medium_read_test") {
+ sources = [ "integration_test/cross_language_medium_read_test.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("cross_language_medium_write_test") {
+ sources = [ "integration_test/cross_language_medium_write_test.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("cross_language_large_read_test") {
+ sources = [ "integration_test/cross_language_large_read_test.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("cross_language_large_write_test") {
+ sources = [ "integration_test/cross_language_large_write_test.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("multi_transfer_test") {
+ sources = [ "integration_test/multi_transfer_test.py" ]
+}
+
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("expected_errors_test") {
+ sources = [ "integration_test/expected_errors_test.py" ]
+}
- stamp = true
+# TODO(b/228516801): Make this actually work; this is just a placeholder.
+pw_python_script("legacy_binaries_test") {
+ sources = [ "integration_test/legacy_binaries_test.py" ]
}
diff --git a/pw_transfer/CMakeLists.txt b/pw_transfer/CMakeLists.txt
index 7f03af22d..11c220dce 100644
--- a/pw_transfer/CMakeLists.txt
+++ b/pw_transfer/CMakeLists.txt
@@ -17,14 +17,16 @@ include($ENV{PW_ROOT}/pw_protobuf_compiler/proto.cmake)
pw_add_module_config(pw_transfer_CONFIG)
-pw_add_module_library(pw_transfer.config
+pw_add_library(pw_transfer.config INTERFACE
PUBLIC_DEPS
${pw_transfer_CONFIG}
HEADERS
public/pw_transfer/internal/config.h
+ PUBLIC_INCLUDES
+ public
)
-pw_add_module_library(pw_transfer
+pw_add_library(pw_transfer INTERFACE
PUBLIC_DEPS
pw_assert
pw_result
@@ -35,11 +37,9 @@ pw_add_module_library(pw_transfer
PRIVATE_DEPS
pw_log
pw_transfer.proto.pwpb
- TEST_DEPS
- pw_rpc.test_utils
)
-pw_add_module_library(pw_transfer.client
+pw_add_library(pw_transfer.client INTERFACE
PUBLIC_DEPS
pw_assert
pw_function
@@ -50,11 +50,9 @@ pw_add_module_library(pw_transfer.client
PRIVATE_DEPS
pw_log
pw_transfer.proto.pwpb
- TEST_DEPS
- pw_rpc.test_utils
)
-pw_add_module_library(pw_transfer.core
+pw_add_library(pw_transfer.core INTERFACE
PUBLIC_DEPS
pw_bytes
pw_chrono.system_clock
diff --git a/pw_transfer/atomic_file_transfer_handler.cc b/pw_transfer/atomic_file_transfer_handler.cc
new file mode 100644
index 000000000..7166c3571
--- /dev/null
+++ b/pw_transfer/atomic_file_transfer_handler.cc
@@ -0,0 +1,130 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_transfer/atomic_file_transfer_handler.h"
+
+#include <filesystem>
+#include <system_error>
+
+#include "pw_log/log.h"
+#include "pw_status/status.h"
+#include "pw_stream/std_file_stream.h"
+#include "pw_transfer_private/filename_generator.h"
+
+namespace pw::transfer {
+
+namespace {
+// Linux Error code for Cross-Device Link Error.
+constexpr auto CROSS_DEVICE_LINK_ERROR = 18;
+
+pw::Status EnsureDirectoryExists(std::string_view filepath) {
+ const auto target_directory = std::filesystem::path{filepath}.parent_path();
+ return std::filesystem::exists(target_directory) ||
+ std::filesystem::create_directories(target_directory)
+ ? pw::OkStatus()
+ : pw::Status::Internal();
+}
+
+// Copy file and remove on succes.
+// If the copy fails, the file `input_target` is not removed.
+pw::Status CopyFile(const std::string_view input_target,
+ const std::string_view output_target) {
+ auto err = std::error_code{};
+ std::filesystem::copy(input_target,
+ output_target,
+ std::filesystem::copy_options::overwrite_existing,
+ err);
+ if (err) {
+ PW_LOG_ERROR("Error with status code: %d (%s) during copy of file %s",
+ err.value(),
+ err.message().c_str(),
+ input_target.data());
+ return pw::Status::Internal();
+ }
+ PW_LOG_INFO("Successfully copied the file.");
+ if (!std::filesystem::remove(input_target)) {
+ PW_LOG_WARN("Failed to remove tmp file %s", input_target.data());
+ }
+ return pw::OkStatus();
+}
+
+// Uses the same approach as unix `mv` command. First try to rename. If we get
+// a cross-device link error, copies then deletes input_target.
+pw::Status RenameFile(const std::string_view input_target,
+ const std::string_view output_target) {
+ auto err = std::error_code{};
+ std::filesystem::rename(input_target, output_target, err);
+ if (err && err.value() == CROSS_DEVICE_LINK_ERROR) {
+ PW_LOG_INFO("%s[%d] during rename of file %s. Trying Copy/Remove.",
+ err.message().c_str(),
+ err.value(),
+ input_target.data());
+ return CopyFile(input_target, output_target);
+ }
+ return err ? pw::Status::Internal() : pw::OkStatus();
+}
+
+} // namespace
+
+Status AtomicFileTransferHandler::PrepareRead() {
+ auto file_path = path_.c_str();
+ PW_LOG_DEBUG("Preparing read for file %s", file_path);
+ if (!std::filesystem::exists(file_path)) {
+ PW_LOG_ERROR("File does not exist, path: %s", file_path);
+ return Status::NotFound();
+ }
+ set_reader(stream_.emplace<stream::StdFileReader>(file_path));
+ return OkStatus();
+}
+
+void AtomicFileTransferHandler::FinalizeRead(Status) {
+ std::get<stream::StdFileReader>(stream_).Close();
+}
+
+Status AtomicFileTransferHandler::PrepareWrite() {
+ const std::string tmp_file = GetTempFilePath(path_);
+ PW_LOG_DEBUG("Preparing write for file %s", tmp_file.c_str());
+ set_writer(stream_.emplace<stream::StdFileWriter>(tmp_file.c_str()));
+ return OkStatus();
+}
+
+Status AtomicFileTransferHandler::FinalizeWrite(Status status) {
+ std::get<stream::StdFileWriter>(stream_).Close();
+ auto tmp_file = GetTempFilePath(path_);
+ if (!status.ok() || !std::filesystem::exists(tmp_file) ||
+ std::filesystem::is_empty(tmp_file)) {
+ PW_LOG_ERROR("Transfer unsuccesful, attempt to remove temp file %s",
+ tmp_file.c_str());
+ // Remove temp file if transfer fails.
+ return std::filesystem::remove(tmp_file) ? status : Status::Aborted();
+ }
+
+ const auto directory_exists_status = EnsureDirectoryExists(path_);
+ if (!directory_exists_status.ok()) {
+ std::filesystem::remove(tmp_file);
+ return directory_exists_status;
+ }
+
+ PW_LOG_DEBUG(
+ "Copying file from: %s, to: %s", tmp_file.c_str(), path_.c_str());
+ const auto rename_status = RenameFile(tmp_file, path_);
+ if (!rename_status.ok()) {
+ std::filesystem::remove(tmp_file);
+ return rename_status;
+ }
+
+ PW_LOG_INFO("File transfer was successful.");
+ return OkStatus();
+}
+
+} // namespace pw::transfer
diff --git a/pw_transfer/atomic_file_transfer_handler_test.cc b/pw_transfer/atomic_file_transfer_handler_test.cc
new file mode 100644
index 000000000..77a2a90be
--- /dev/null
+++ b/pw_transfer/atomic_file_transfer_handler_test.cc
@@ -0,0 +1,215 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_transfer/atomic_file_transfer_handler.h"
+
+#include <cinttypes>
+#include <filesystem>
+#include <fstream>
+#include <random>
+#include <string>
+#include <string_view>
+
+#include "gtest/gtest.h"
+#include "pw_random/xor_shift.h"
+#include "pw_result/result.h"
+#include "pw_status/status.h"
+#include "pw_string/string_builder.h"
+#include "pw_transfer/transfer.h"
+#include "pw_transfer_private/filename_generator.h"
+
+namespace pw::transfer {
+
+namespace {
+
+// Copied from go/pw-src/+/main:pw_stream/std_file_stream_test.cc;l=75
+class TempDir {
+ public:
+ TempDir(std::string_view prefix) : rng_(GetSeed()) {
+ temp_dir_ = std::filesystem::temp_directory_path();
+ temp_dir_ /= std::string(prefix) + GetRandomSuffix();
+ PW_ASSERT(std::filesystem::create_directory(temp_dir_));
+ }
+
+ ~TempDir() { PW_ASSERT(std::filesystem::remove_all(temp_dir_)); }
+
+ std::filesystem::path GetTempFileName() {
+ return temp_dir_ / GetRandomSuffix();
+ }
+
+ private:
+ std::string GetRandomSuffix() {
+ StringBuffer<9> random_suffix_str;
+ uint32_t random_suffix_int = 0;
+ rng_.GetInt(random_suffix_int);
+ PW_ASSERT(random_suffix_str.Format("%08" PRIx32, random_suffix_int).ok());
+ return std::string(random_suffix_str.view());
+ }
+
+ // Generate a 64-bit random from system entropy pool. This is used to seed a
+ // pseudo-random number generator for individual file names.
+ static uint64_t GetSeed() {
+ std::random_device sys_rand;
+ uint64_t seed = 0;
+ for (size_t seed_bytes = 0; seed_bytes < sizeof(seed);
+ seed_bytes += sizeof(std::random_device::result_type)) {
+ std::random_device::result_type val = sys_rand();
+ seed = seed << 8 * sizeof(std::random_device::result_type);
+ seed |= val;
+ }
+ return seed;
+ }
+
+ random::XorShiftStarRng64 rng_;
+ std::filesystem::path temp_dir_;
+};
+
+class AtomicFileTransferHandlerTest : public ::testing::Test {
+ public:
+ TempDir temp_dir_{"atomic_file_transfer_handler_test"};
+ std::string test_data_location_pass_ = temp_dir_.GetTempFileName();
+ std::string transfer_temp_file_ = GetTempFilePath(test_data_location_pass_);
+
+ protected:
+ static constexpr auto test_data_location_fail = "not/a/directory/no_data.txt";
+ static constexpr auto temp_file_content = "Temp File Success.";
+ static constexpr auto test_data_content = "Test File Success.";
+
+ bool WriteContentFile(std::string_view path, std::string_view value) {
+ std::ofstream file(path);
+ if (!file.is_open()) {
+ return false;
+ }
+ file << value;
+ return true;
+ }
+
+ Result<std::string> ReadFile(std::string_view path) {
+ std::ifstream file(path);
+ if (!file.is_open()) {
+ return Status::NotFound();
+ }
+ std::string return_value;
+ std::getline(file, return_value);
+ return return_value;
+ }
+
+ void ClearContent(std::string_view path) {
+ std::ofstream ofs(path, std::ofstream::out | std::ofstream::trunc);
+ }
+
+ void check_finalize(Status status) {
+ EXPECT_EQ(status, OkStatus());
+ // Temp file does not exist after finalize.
+ EXPECT_TRUE(!std::filesystem::exists(transfer_temp_file_));
+ // Test path does exist, file has been created.
+ EXPECT_TRUE(std::filesystem::exists(test_data_location_pass_));
+ // File content is the same as expected.
+ const auto file_content = ReadFile(test_data_location_pass_);
+ ASSERT_TRUE(file_content.ok());
+
+ EXPECT_EQ(file_content.value(), temp_file_content);
+ }
+
+ void SetUp() override {
+ // Write content file and check correct.
+ ASSERT_TRUE(WriteContentFile(test_data_location_pass_, test_data_content));
+ const auto file_content_data = ReadFile(test_data_location_pass_);
+ ASSERT_TRUE(file_content_data.ok());
+ ASSERT_EQ(file_content_data.value(), test_data_content);
+
+ // Write temp file and check content is correct
+ ASSERT_TRUE(WriteContentFile(transfer_temp_file_, temp_file_content));
+ const auto file_content_tmp = ReadFile(transfer_temp_file_);
+ ASSERT_TRUE(file_content_tmp.ok());
+ ASSERT_EQ(file_content_tmp.value(), temp_file_content);
+ }
+
+ void TearDown() override {
+ // Ensure temp file is deleted.
+ ASSERT_TRUE(!std::filesystem::exists(transfer_temp_file_) ||
+ std::filesystem::remove(transfer_temp_file_));
+ // Ensure test file is deleted.
+ ASSERT_TRUE(!std::filesystem::exists(test_data_location_pass_) ||
+ std::filesystem::remove(test_data_location_pass_));
+ }
+};
+
+TEST_F(AtomicFileTransferHandlerTest, PrepareReadPass) {
+ AtomicFileTransferHandler test_handler{/*resource_id = */ 0,
+ test_data_location_pass_};
+ EXPECT_EQ(test_handler.PrepareRead(), OkStatus());
+}
+
+TEST_F(AtomicFileTransferHandlerTest, PrepareReadFail) {
+ AtomicFileTransferHandler test_handler{/*resource_id = */ 0,
+ test_data_location_fail};
+ EXPECT_EQ(test_handler.PrepareRead(), Status::NotFound());
+}
+
+TEST_F(AtomicFileTransferHandlerTest, PrepareWritePass) {
+ AtomicFileTransferHandler test_handler{/*resource_id = */ 0,
+ test_data_location_pass_};
+ // Open a file for write returns OkStatus.
+ EXPECT_EQ(test_handler.PrepareWrite(), OkStatus());
+}
+
+TEST_F(AtomicFileTransferHandlerTest, PrepareWriteFail) {
+ AtomicFileTransferHandler test_handler{/*resource_id = */ 0,
+ test_data_location_fail};
+ // Open a file with non existing path pass.
+ // No access to underlying stream
+ // so rely on the write during transfer to catch the error.
+ EXPECT_EQ(test_handler.PrepareWrite(), OkStatus());
+}
+
+TEST_F(AtomicFileTransferHandlerTest, FinalizeWriteRenameExisting) {
+ ASSERT_TRUE(std::filesystem::exists(transfer_temp_file_));
+ ASSERT_TRUE(std::filesystem::exists(test_data_location_pass_));
+ AtomicFileTransferHandler test_handler{/*resource_id = */
+ 0,
+ test_data_location_pass_};
+ // Prepare Write to open the stream. should be closed during Finalize.
+ ASSERT_EQ(test_handler.PrepareWrite(), OkStatus());
+ WriteContentFile(transfer_temp_file_, temp_file_content);
+ auto status = test_handler.FinalizeWrite(OkStatus());
+ check_finalize(status);
+}
+
+TEST_F(AtomicFileTransferHandlerTest, FinalizeWriteNoExistingFile) {
+ AtomicFileTransferHandler test_handler{/*resource_id = */
+ 0,
+ test_data_location_pass_};
+ // Remove file test file and test creation.
+ ASSERT_TRUE(std::filesystem::remove(test_data_location_pass_));
+ ASSERT_EQ(test_handler.PrepareWrite(), OkStatus());
+ WriteContentFile(transfer_temp_file_, temp_file_content);
+ auto status = test_handler.FinalizeWrite(OkStatus());
+ check_finalize(status);
+}
+
+TEST_F(AtomicFileTransferHandlerTest, FinalizeWriteExpectErr) {
+ AtomicFileTransferHandler test_handler{/*resource_id = */
+ 0,
+ test_data_location_pass_};
+ ASSERT_EQ(test_handler.PrepareWrite(), OkStatus());
+ // Simulate write fails, file is empty, No write here.
+ ClearContent(transfer_temp_file_);
+ ASSERT_TRUE(std::filesystem::is_empty(transfer_temp_file_));
+ ASSERT_TRUE(std::filesystem::exists(test_data_location_pass_));
+ EXPECT_EQ(test_handler.FinalizeWrite(Status::DataLoss()), Status::DataLoss());
+}
+
+} // namespace
+
+} // namespace pw::transfer
diff --git a/pw_transfer/chunk.cc b/pw_transfer/chunk.cc
index 7066e251e..cf5133ed3 100644
--- a/pw_transfer/chunk.cc
+++ b/pw_transfer/chunk.cc
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -14,143 +14,337 @@
#include "pw_transfer/internal/chunk.h"
+#include "pw_assert/check.h"
#include "pw_protobuf/decoder.h"
+#include "pw_protobuf/serialized_size.h"
#include "pw_status/try.h"
-#include "pw_transfer/transfer.pwpb.h"
namespace pw::transfer::internal {
-namespace ProtoChunk = transfer::Chunk;
+namespace ProtoChunk = transfer::pwpb::Chunk;
-Result<uint32_t> ExtractTransferId(ConstByteSpan message) {
+Result<Chunk::Identifier> Chunk::ExtractIdentifier(ConstByteSpan message) {
protobuf::Decoder decoder(message);
+ uint32_t session_id = 0;
+ uint32_t resource_id = 0;
+
while (decoder.Next().ok()) {
ProtoChunk::Fields field =
static_cast<ProtoChunk::Fields>(decoder.FieldNumber());
- switch (field) {
- case ProtoChunk::Fields::TRANSFER_ID: {
- uint32_t transfer_id;
- PW_TRY(decoder.ReadUint32(&transfer_id));
- return transfer_id;
+ if (field == ProtoChunk::Fields::kTransferId) {
+ // Interpret a legacy transfer_id field as a session ID if an explicit
+ // session_id field has not already been seen.
+ if (session_id == 0) {
+ PW_TRY(decoder.ReadUint32(&session_id));
}
-
- default:
- continue;
+ } else if (field == ProtoChunk::Fields::kSessionId) {
+ // A session_id field always takes precedence over transfer_id.
+ PW_TRY(decoder.ReadUint32(&session_id));
+ } else if (field == ProtoChunk::Fields::kResourceId) {
+ PW_TRY(decoder.ReadUint32(&resource_id));
}
}
+ // Always prioritize a resource_id if one is set. Resource IDs should only be
+ // set in cases where the transfer session ID has not yet been negotiated.
+ if (resource_id != 0) {
+ return Identifier::Resource(resource_id);
+ }
+
+ if (session_id != 0) {
+ return Identifier::Session(session_id);
+ }
+
return Status::DataLoss();
}
-Status DecodeChunk(ConstByteSpan message, Chunk& chunk) {
+Result<Chunk> Chunk::Parse(ConstByteSpan message) {
protobuf::Decoder decoder(message);
Status status;
uint32_t value;
- chunk = {};
+ Chunk chunk;
+
+ // Determine the protocol version of the chunk depending on field presence in
+ // the serialized message.
+ chunk.protocol_version_ = ProtocolVersion::kUnknown;
+
+ // Some older versions of the protocol set the deprecated pending_bytes field
+ // in their chunks. The newer transfer handling code does not process this
+ // field, instead working only in terms of window_end_offset. If pending_bytes
+ // is encountered in the serialized message, save its value, then calculate
+ // window_end_offset from it once parsing is complete.
+ uint32_t pending_bytes = 0;
while ((status = decoder.Next()).ok()) {
ProtoChunk::Fields field =
static_cast<ProtoChunk::Fields>(decoder.FieldNumber());
switch (field) {
- case ProtoChunk::Fields::TRANSFER_ID:
- PW_TRY(decoder.ReadUint32(&chunk.transfer_id));
+ case ProtoChunk::Fields::kTransferId:
+ // transfer_id is a legacy field. session_id will always take precedence
+ // over it, so it should only be read if session_id has not yet been
+ // encountered.
+ if (chunk.session_id_ == 0) {
+ PW_TRY(decoder.ReadUint32(&chunk.session_id_));
+ }
break;
- case ProtoChunk::Fields::PENDING_BYTES:
- PW_TRY(decoder.ReadUint32(&value));
- chunk.pending_bytes = value;
+ case ProtoChunk::Fields::kSessionId:
+ // The existence of a session_id field indicates that a newer protocol
+ // is running. Update the deduced protocol unless it was explicitly
+ // specified.
+ if (chunk.protocol_version_ == ProtocolVersion::kUnknown) {
+ chunk.protocol_version_ = ProtocolVersion::kVersionTwo;
+ }
+
+ PW_TRY(decoder.ReadUint32(&chunk.session_id_));
+ break;
+
+ case ProtoChunk::Fields::kPendingBytes:
+ PW_TRY(decoder.ReadUint32(&pending_bytes));
break;
- case ProtoChunk::Fields::MAX_CHUNK_SIZE_BYTES:
+ case ProtoChunk::Fields::kMaxChunkSizeBytes:
PW_TRY(decoder.ReadUint32(&value));
- chunk.max_chunk_size_bytes = value;
+ chunk.set_max_chunk_size_bytes(value);
break;
- case ProtoChunk::Fields::MIN_DELAY_MICROSECONDS:
+ case ProtoChunk::Fields::kMinDelayMicroseconds:
PW_TRY(decoder.ReadUint32(&value));
- chunk.min_delay_microseconds = value;
+ chunk.set_min_delay_microseconds(value);
break;
- case ProtoChunk::Fields::OFFSET:
- PW_TRY(decoder.ReadUint32(&chunk.offset));
+ case ProtoChunk::Fields::kOffset:
+ PW_TRY(decoder.ReadUint32(&chunk.offset_));
break;
- case ProtoChunk::Fields::DATA:
- PW_TRY(decoder.ReadBytes(&chunk.data));
+ case ProtoChunk::Fields::kData:
+ PW_TRY(decoder.ReadBytes(&chunk.payload_));
break;
- case ProtoChunk::Fields::REMAINING_BYTES: {
- uint64_t remaining;
- PW_TRY(decoder.ReadUint64(&remaining));
- chunk.remaining_bytes = remaining;
+ case ProtoChunk::Fields::kRemainingBytes: {
+ uint64_t remaining_bytes;
+ PW_TRY(decoder.ReadUint64(&remaining_bytes));
+ chunk.set_remaining_bytes(remaining_bytes);
break;
}
- case ProtoChunk::Fields::STATUS:
+ case ProtoChunk::Fields::kStatus:
PW_TRY(decoder.ReadUint32(&value));
- chunk.status = static_cast<Status::Code>(value);
+ chunk.set_status(static_cast<Status::Code>(value));
break;
- case ProtoChunk::Fields::WINDOW_END_OFFSET:
- PW_TRY(decoder.ReadUint32(&chunk.window_end_offset));
+ case ProtoChunk::Fields::kWindowEndOffset:
+ PW_TRY(decoder.ReadUint32(&chunk.window_end_offset_));
break;
- case ProtoChunk::Fields::TYPE: {
+ case ProtoChunk::Fields::kType: {
uint32_t type;
PW_TRY(decoder.ReadUint32(&type));
- chunk.type = static_cast<Chunk::Type>(type);
+ chunk.type_ = static_cast<Chunk::Type>(type);
break;
}
+
+ case ProtoChunk::Fields::kResourceId:
+ PW_TRY(decoder.ReadUint32(&value));
+ chunk.set_resource_id(value);
+ break;
+
+ case ProtoChunk::Fields::kProtocolVersion:
+ // The protocol_version field is added as part of the initial handshake
+ // starting from version 2. If provided, it should override any deduced
+ // protocol version.
+ PW_TRY(decoder.ReadUint32(&value));
+ if (!ValidProtocolVersion(value)) {
+ return Status::DataLoss();
+ }
+ chunk.protocol_version_ = static_cast<ProtocolVersion>(value);
+ break;
+
+ // Silently ignore any unrecognized fields.
}
}
- return status.IsOutOfRange() ? OkStatus() : status;
+ if (chunk.protocol_version_ == ProtocolVersion::kUnknown) {
+ // If no fields in the chunk specified its protocol version, assume it is a
+ // legacy chunk.
+ chunk.protocol_version_ = ProtocolVersion::kLegacy;
+ }
+
+ if (pending_bytes != 0) {
+ // Compute window_end_offset if it isn't explicitly provided (in older
+ // protocol versions).
+ chunk.set_window_end_offset(chunk.offset() + pending_bytes);
+ }
+
+ if (status.ok() || status.IsOutOfRange()) {
+ return chunk;
+ }
+
+ return status;
}
-Result<ConstByteSpan> EncodeChunk(const Chunk& chunk, ByteSpan buffer) {
- ProtoChunk::MemoryEncoder encoder(buffer);
+Result<ConstByteSpan> Chunk::Encode(ByteSpan buffer) const {
+ PW_CHECK(protocol_version_ != ProtocolVersion::kUnknown,
+ "Cannot encode a transfer chunk with an unknown protocol version");
- encoder.WriteTransferId(chunk.transfer_id).IgnoreError();
+ ProtoChunk::MemoryEncoder encoder(buffer);
- if (chunk.window_end_offset != 0) {
- encoder.WriteWindowEndOffset(chunk.window_end_offset).IgnoreError();
+ // Write the payload first to avoid clobbering it if it shares the same buffer
+ // as the encode buffer.
+ if (has_payload()) {
+ encoder.WriteData(payload_).IgnoreError();
}
- if (chunk.pending_bytes.has_value()) {
- encoder.WritePendingBytes(chunk.pending_bytes.value()).IgnoreError();
+ if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
+ if (session_id_ != 0) {
+ encoder.WriteSessionId(session_id_).IgnoreError();
+ }
+
+ if (resource_id_.has_value()) {
+ encoder.WriteResourceId(resource_id_.value()).IgnoreError();
+ }
}
- if (chunk.max_chunk_size_bytes.has_value()) {
- encoder.WriteMaxChunkSizeBytes(chunk.max_chunk_size_bytes.value())
+
+ // During the initial handshake, the chunk's configured protocol version is
+ // explicitly serialized to the wire.
+ if (IsInitialHandshakeChunk()) {
+ encoder.WriteProtocolVersion(static_cast<uint32_t>(protocol_version_))
.IgnoreError();
}
- if (chunk.min_delay_microseconds.has_value()) {
- encoder.WriteMinDelayMicroseconds(chunk.min_delay_microseconds.value())
+
+ if (type_.has_value()) {
+ encoder.WriteType(static_cast<ProtoChunk::Type>(type_.value()))
.IgnoreError();
}
- if (chunk.offset != 0) {
- encoder.WriteOffset(chunk.offset).IgnoreError();
+
+ if (window_end_offset_ != 0) {
+ encoder.WriteWindowEndOffset(window_end_offset_).IgnoreError();
}
- if (!chunk.data.empty()) {
- encoder.WriteData(chunk.data).IgnoreError();
+
+ // Encode additional fields from the legacy protocol.
+ if (ShouldEncodeLegacyFields()) {
+ // The legacy protocol uses the transfer_id field instead of session_id or
+ // resource_id.
+ if (resource_id_.has_value()) {
+ encoder.WriteTransferId(resource_id_.value()).IgnoreError();
+ } else {
+ encoder.WriteTransferId(session_id_).IgnoreError();
+ }
+
+ // In the legacy protocol, the pending_bytes field must be set alongside
+ // window_end_offset, as some transfer implementations require it.
+ if (window_end_offset_ != 0) {
+ encoder.WritePendingBytes(window_end_offset_ - offset_).IgnoreError();
+ }
}
- if (chunk.remaining_bytes.has_value()) {
- encoder.WriteRemainingBytes(chunk.remaining_bytes.value()).IgnoreError();
+
+ if (max_chunk_size_bytes_.has_value()) {
+ encoder.WriteMaxChunkSizeBytes(max_chunk_size_bytes_.value()).IgnoreError();
}
- if (chunk.status.has_value()) {
- encoder.WriteStatus(chunk.status.value().code()).IgnoreError();
+ if (min_delay_microseconds_.has_value()) {
+ encoder.WriteMinDelayMicroseconds(min_delay_microseconds_.value())
+ .IgnoreError();
}
- if (chunk.type.has_value()) {
- encoder.WriteType(static_cast<ProtoChunk::Type>(chunk.type.value()))
- .IgnoreError();
+ if (offset_ != 0) {
+ encoder.WriteOffset(offset_).IgnoreError();
+ }
+
+ if (remaining_bytes_.has_value()) {
+ encoder.WriteRemainingBytes(remaining_bytes_.value()).IgnoreError();
+ }
+
+ if (status_.has_value()) {
+ encoder.WriteStatus(status_.value().code()).IgnoreError();
}
PW_TRY(encoder.status());
return ConstByteSpan(encoder);
}
+size_t Chunk::EncodedSize() const {
+ size_t size = 0;
+
+ if (session_id_ != 0) {
+ if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kSessionId,
+ session_id_);
+ }
+
+ if (ShouldEncodeLegacyFields()) {
+ if (resource_id_.has_value()) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kTransferId,
+ resource_id_.value());
+ } else {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kTransferId,
+ session_id_);
+ }
+ }
+ }
+
+ if (IsInitialHandshakeChunk()) {
+ size +=
+ protobuf::SizeOfVarintField(ProtoChunk::Fields::kProtocolVersion,
+ static_cast<uint32_t>(protocol_version_));
+ }
+
+ if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
+ if (resource_id_.has_value()) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kResourceId,
+ resource_id_.value());
+ }
+ }
+
+ if (offset_ != 0) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kOffset, offset_);
+ }
+
+ if (window_end_offset_ != 0) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kWindowEndOffset,
+ window_end_offset_);
+
+ if (ShouldEncodeLegacyFields()) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kPendingBytes,
+ window_end_offset_ - offset_);
+ }
+ }
+
+ if (type_.has_value()) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kType,
+ static_cast<uint32_t>(type_.value()));
+ }
+
+ if (has_payload()) {
+ size += protobuf::SizeOfDelimitedField(ProtoChunk::Fields::kData,
+ payload_.size());
+ }
+
+ if (max_chunk_size_bytes_.has_value()) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kMaxChunkSizeBytes,
+ max_chunk_size_bytes_.value());
+ }
+
+ if (min_delay_microseconds_.has_value()) {
+ size +=
+ protobuf::SizeOfVarintField(ProtoChunk::Fields::kMinDelayMicroseconds,
+ min_delay_microseconds_.value());
+ }
+
+ if (remaining_bytes_.has_value()) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kRemainingBytes,
+ remaining_bytes_.value());
+ }
+
+ if (status_.has_value()) {
+ size += protobuf::SizeOfVarintField(ProtoChunk::Fields::kStatus,
+ status_.value().code());
+ }
+
+ return size;
+}
+
} // namespace pw::transfer::internal
diff --git a/pw_transfer/chunk_test.cc b/pw_transfer/chunk_test.cc
new file mode 100644
index 000000000..ecc2d687a
--- /dev/null
+++ b/pw_transfer/chunk_test.cc
@@ -0,0 +1,66 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_transfer/internal/chunk.h"
+
+#include "gtest/gtest.h"
+#include "pw_bytes/array.h"
+
+namespace pw::transfer::internal {
+namespace {
+
+TEST(Chunk, EncodedSizeMatchesEncode) {
+ // Use a START chunk to encode a bunch of legacy fields as well.
+ Chunk chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart);
+ chunk.set_session_id(42).set_resource_id(7).set_window_end_offset(128);
+
+ std::array<std::byte, 64> buffer;
+ EXPECT_LT(chunk.EncodedSize(), buffer.size());
+
+ auto result = chunk.Encode(buffer);
+ ASSERT_EQ(result.status(), OkStatus());
+ EXPECT_EQ(chunk.EncodedSize(), result->size_bytes());
+}
+
+TEST(Chunk, EncodedSizeGreaterThanBuffer) {
+ Chunk chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersRetransmit);
+ chunk.set_session_id(42).set_resource_id(7).set_window_end_offset(128);
+
+ std::array<std::byte, 8> buffer;
+ EXPECT_GT(chunk.EncodedSize(), buffer.size());
+
+ auto result = chunk.Encode(buffer);
+ ASSERT_EQ(result.status(), Status::ResourceExhausted());
+}
+
+TEST(Chunk, EncodedSizeMatchesBuffer) {
+ // 16 bytes for metadata.
+ Chunk chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart);
+ chunk.set_session_id(42).set_resource_id(7).set_window_end_offset(128);
+ EXPECT_EQ(chunk.EncodedSize(), 16u);
+
+ // 2 bytes for payload key & size, leaving 46 for data.
+ constexpr auto kData = bytes::Initialized<46>(0x11);
+ chunk.set_payload(kData);
+
+ std::array<std::byte, 64> buffer;
+ EXPECT_EQ(chunk.EncodedSize(), buffer.size());
+
+ auto result = chunk.Encode(buffer);
+ ASSERT_EQ(result.status(), OkStatus());
+ EXPECT_EQ(chunk.EncodedSize(), result->size_bytes());
+}
+
+} // namespace
+} // namespace pw::transfer::internal
diff --git a/pw_transfer/client.cc b/pw_transfer/client.cc
index 73822c246..95abb81a3 100644
--- a/pw_transfer/client.cc
+++ b/pw_transfer/client.cc
@@ -20,11 +20,13 @@
namespace pw::transfer {
-Status Client::Read(uint32_t transfer_id,
+Status Client::Read(uint32_t resource_id,
stream::Writer& output,
CompletionFunc&& on_completion,
- chrono::SystemClock::duration timeout) {
- if (on_completion == nullptr) {
+ chrono::SystemClock::duration timeout,
+ ProtocolVersion protocol_version) {
+ if (on_completion == nullptr ||
+ protocol_version == ProtocolVersion::kUnknown) {
return Status::InvalidArgument();
}
@@ -41,21 +43,24 @@ Status Client::Read(uint32_t transfer_id,
}
transfer_thread_.StartClientTransfer(internal::TransferType::kReceive,
- transfer_id,
- transfer_id,
+ protocol_version,
+ resource_id,
&output,
max_parameters_,
std::move(on_completion),
timeout,
- cfg::kDefaultMaxRetries);
+ max_retries_,
+ max_lifetime_retries_);
return OkStatus();
}
-Status Client::Write(uint32_t transfer_id,
+Status Client::Write(uint32_t resource_id,
stream::Reader& input,
CompletionFunc&& on_completion,
- chrono::SystemClock::duration timeout) {
- if (on_completion == nullptr) {
+ chrono::SystemClock::duration timeout,
+ ProtocolVersion protocol_version) {
+ if (on_completion == nullptr ||
+ protocol_version == ProtocolVersion::kUnknown) {
return Status::InvalidArgument();
}
@@ -72,17 +77,23 @@ Status Client::Write(uint32_t transfer_id,
}
transfer_thread_.StartClientTransfer(internal::TransferType::kTransmit,
- transfer_id,
- transfer_id,
+ protocol_version,
+ resource_id,
&input,
max_parameters_,
std::move(on_completion),
timeout,
- cfg::kDefaultMaxRetries);
+ max_retries_,
+ max_lifetime_retries_);
return OkStatus();
}
+void Client::CancelTransfer(uint32_t resource_id) {
+ transfer_thread_.EndClientTransfer(
+ resource_id, Status::Cancelled(), /*send_status_chunk=*/true);
+}
+
void Client::OnRpcError(Status status, internal::TransferType type) {
bool is_write_error = type == internal::TransferType::kTransmit;
diff --git a/pw_transfer/client_test.cc b/pw_transfer/client_test.cc
index cbf82d7ba..1b16d9142 100644
--- a/pw_transfer/client_test.cc
+++ b/pw_transfer/client_test.cc
@@ -20,7 +20,7 @@
#include "pw_assert/check.h"
#include "pw_bytes/array.h"
#include "pw_rpc/raw/client_testing.h"
-#include "pw_rpc/thread_testing.h"
+#include "pw_rpc/test_helpers.h"
#include "pw_thread/sleep.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
@@ -34,9 +34,6 @@ using pw_rpc::raw::Transfer;
using namespace std::chrono_literals;
-PW_MODIFY_DIAGNOSTICS_PUSH();
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
-
thread::Options& TransferThreadOptions() {
static thread::stl::Options options;
return options;
@@ -52,7 +49,7 @@ class ReadTransfer : public ::testing::Test {
max_bytes_to_receive),
system_thread_(TransferThreadOptions(), transfer_thread_) {}
- ~ReadTransfer() {
+ ~ReadTransfer() override {
transfer_thread_.Terminate();
system_thread_.join();
}
@@ -89,20 +86,25 @@ TEST_F(ReadTransfer, SingleChunk) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 3u);
- EXPECT_EQ(c0.offset, 0u);
- EXPECT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 3u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 64u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
- context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
- {.transfer_id = 3u, .offset = 0, .data = kData32, .remaining_bytes = 0}));
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(3)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 2u);
- Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 3u);
- ASSERT_TRUE(c1.status.has_value());
- EXPECT_EQ(c1.status.value(), OkStatus());
+ Chunk c1 = DecodeChunk(payloads.back());
+ EXPECT_EQ(c1.session_id(), 3u);
+ ASSERT_TRUE(c1.status().has_value());
+ EXPECT_EQ(c1.status().value(), OkStatus());
EXPECT_EQ(transfer_status, OkStatus());
EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
@@ -127,30 +129,35 @@ TEST_F(ReadTransfer, MultiChunk) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 4u);
- EXPECT_EQ(c0.offset, 0u);
- EXPECT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 4u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 64u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
constexpr ConstByteSpan data(kData32);
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 4u, .offset = 0, .data = data.first(16)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(4)
+ .set_offset(0)
+ .set_payload(data.first(16))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 1u);
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 4u,
- .offset = 16,
- .data = data.subspan(16),
- .remaining_bytes = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(4)
+ .set_offset(16)
+ .set_payload(data.subspan(16))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 2u);
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 4u);
- ASSERT_TRUE(c1.status.has_value());
- EXPECT_EQ(c1.status.value(), OkStatus());
+ EXPECT_EQ(c1.session_id(), 4u);
+ ASSERT_TRUE(c1.status().has_value());
+ EXPECT_EQ(c1.status().value(), OkStatus());
EXPECT_EQ(transfer_status, OkStatus());
EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
@@ -167,8 +174,12 @@ TEST_F(ReadTransfer, MultipleTransfers) {
}));
transfer_thread_.WaitUntilEventIsProcessed();
- context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
- {.transfer_id = 3u, .offset = 0, .data = kData32, .remaining_bytes = 0}));
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(3)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(transfer_status, OkStatus());
@@ -180,8 +191,12 @@ TEST_F(ReadTransfer, MultipleTransfers) {
}));
transfer_thread_.WaitUntilEventIsProcessed();
- context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
- {.transfer_id = 3u, .offset = 0, .data = kData32, .remaining_bytes = 0}));
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(3)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_EQ(transfer_status, OkStatus());
@@ -203,9 +218,10 @@ TEST_F(ReadTransferMaxBytes32, SetsPendingBytesFromConstructorArg) {
ASSERT_EQ(payloads.size(), 1u);
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 5u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.pending_bytes.value(), 32u);
+ EXPECT_EQ(c0.session_id(), 5u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 32u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
}
TEST_F(ReadTransferMaxBytes32, SetsPendingBytesFromWriterLimit) {
@@ -219,9 +235,10 @@ TEST_F(ReadTransferMaxBytes32, SetsPendingBytesFromWriterLimit) {
ASSERT_EQ(payloads.size(), 1u);
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 5u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.pending_bytes.value(), 16u);
+ EXPECT_EQ(c0.session_id(), 5u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 16u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
}
TEST_F(ReadTransferMaxBytes32, MultiParameters) {
@@ -241,13 +258,16 @@ TEST_F(ReadTransferMaxBytes32, MultiParameters) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 6u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.pending_bytes.value(), 32u);
+ EXPECT_EQ(c0.session_id(), 6u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.window_end_offset(), 32u);
constexpr ConstByteSpan data(kData64);
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 6u, .offset = 0, .data = data.first(32)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(6)
+ .set_offset(0)
+ .set_payload(data.first(32))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 2u);
@@ -255,23 +275,24 @@ TEST_F(ReadTransferMaxBytes32, MultiParameters) {
// Second parameters chunk.
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 6u);
- EXPECT_EQ(c1.offset, 32u);
- ASSERT_EQ(c1.pending_bytes.value(), 32u);
+ EXPECT_EQ(c1.session_id(), 6u);
+ EXPECT_EQ(c1.offset(), 32u);
+ ASSERT_EQ(c1.window_end_offset(), 64u);
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 6u,
- .offset = 32,
- .data = data.subspan(32),
- .remaining_bytes = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(6)
+ .set_offset(32)
+ .set_payload(data.subspan(32))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 3u);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 6u);
- ASSERT_TRUE(c2.status.has_value());
- EXPECT_EQ(c2.status.value(), OkStatus());
+ EXPECT_EQ(c2.session_id(), 6u);
+ ASSERT_TRUE(c2.status().has_value());
+ EXPECT_EQ(c2.status().value(), OkStatus());
EXPECT_EQ(transfer_status, OkStatus());
EXPECT_EQ(std::memcmp(writer.data(), data.data(), writer.bytes_written()), 0);
@@ -294,13 +315,16 @@ TEST_F(ReadTransfer, UnexpectedOffset) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 7u);
- EXPECT_EQ(c0.offset, 0u);
- EXPECT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 7u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 64u);
constexpr ConstByteSpan data(kData32);
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 7u, .offset = 0, .data = data.first(16)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(data.first(16))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 1u);
@@ -308,34 +332,36 @@ TEST_F(ReadTransfer, UnexpectedOffset) {
// Send a chunk with an incorrect offset. The client should resend parameters.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 7u,
- .offset = 8, // wrong!
- .data = data.subspan(16),
- .remaining_bytes = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(8) // wrong!
+ .set_payload(data.subspan(16))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 2u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 7u);
- EXPECT_EQ(c1.offset, 16u);
- EXPECT_EQ(c1.pending_bytes.value(), 48u);
+ EXPECT_EQ(c1.session_id(), 7u);
+ EXPECT_EQ(c1.offset(), 16u);
+ EXPECT_EQ(c1.window_end_offset(), 64u);
// Send the correct chunk, completing the transfer.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 7u,
- .offset = 16,
- .data = data.subspan(16),
- .remaining_bytes = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(16)
+ .set_payload(data.subspan(16))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 3u);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 7u);
- ASSERT_TRUE(c2.status.has_value());
- EXPECT_EQ(c2.status.value(), OkStatus());
+ EXPECT_EQ(c2.session_id(), 7u);
+ ASSERT_TRUE(c2.status().has_value());
+ EXPECT_EQ(c2.status().value(), OkStatus());
EXPECT_EQ(transfer_status, OkStatus());
EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
@@ -359,31 +385,40 @@ TEST_F(ReadTransferMaxBytes32, TooMuchData) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 8u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.pending_bytes.value(), 32u);
+ EXPECT_EQ(c0.session_id(), 8u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.window_end_offset(), 32u);
constexpr ConstByteSpan data(kData64);
// pending_bytes == 32
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 8u, .offset = 0, .data = data.first(16)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(8)
+ .set_offset(0)
+ .set_payload(data.first(16))));
// pending_bytes == 16
- context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
- {.transfer_id = 8u, .offset = 16, .data = data.subspan(16, 8)}));
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(8)
+ .set_offset(16)
+ .set_payload(data.subspan(16, 8))));
// pending_bytes == 8, send 16 instead.
- context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
- {.transfer_id = 8u, .offset = 24, .data = data.subspan(24, 16)}));
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(8)
+ .set_offset(24)
+ .set_payload(data.subspan(24, 16))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 4u);
Chunk c1 = DecodeChunk(payloads[3]);
- EXPECT_EQ(c1.transfer_id, 8u);
- ASSERT_TRUE(c1.status.has_value());
- EXPECT_EQ(c1.status.value(), Status::Internal());
+ EXPECT_EQ(c1.session_id(), 8u);
+ ASSERT_TRUE(c1.status().has_value());
+ EXPECT_EQ(c1.status().value(), Status::Internal());
EXPECT_EQ(transfer_status, Status::Internal());
}
@@ -405,14 +440,14 @@ TEST_F(ReadTransfer, ServerError) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 9u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 9u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.window_end_offset(), 64u);
// Server sends an error. Client should not respond and terminate the
// transfer.
- context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 9u, .status = Status::NotFound()}));
+ context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
+ Chunk::Final(ProtocolVersion::kLegacy, 9, Status::NotFound())));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 1u);
@@ -436,22 +471,26 @@ TEST_F(ReadTransfer, OnlySendsParametersOnceAfterDrop) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 10u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 10u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.window_end_offset(), 64u);
- constexpr ConstByteSpan data(kData64);
+ constexpr ConstByteSpan data(kData32);
// Send the first 8 bytes of the transfer.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 10u, .offset = 0, .data = data.first(8)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(10)
+ .set_offset(0)
+ .set_payload(data.first(8))));
// Skip offset 8, send the rest starting from 16.
for (uint32_t offset = 16; offset < data.size(); offset += 8) {
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 10u,
- .offset = offset,
- .data = data.subspan(offset, 8)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(10)
+ .set_offset(offset)
+ .set_payload(data.subspan(offset, 8))));
}
transfer_thread_.WaitUntilEventIsProcessed();
@@ -460,24 +499,25 @@ TEST_F(ReadTransfer, OnlySendsParametersOnceAfterDrop) {
ASSERT_EQ(payloads.size(), 2u);
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 10u);
- EXPECT_EQ(c1.offset, 8u);
- ASSERT_EQ(c1.pending_bytes.value(), 56u);
+ EXPECT_EQ(c1.session_id(), 10u);
+ EXPECT_EQ(c1.offset(), 8u);
+ ASSERT_EQ(c1.window_end_offset(), 64u);
// Send the remaining data to complete the transfer.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 10u,
- .offset = 8,
- .data = data.subspan(8, 56),
- .remaining_bytes = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(10)
+ .set_offset(8)
+ .set_payload(data.subspan(8))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 3u);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 10u);
- ASSERT_TRUE(c2.status.has_value());
- EXPECT_EQ(c2.status.value(), OkStatus());
+ EXPECT_EQ(c2.session_id(), 10u);
+ ASSERT_TRUE(c2.status().has_value());
+ EXPECT_EQ(c2.status().value(), OkStatus());
EXPECT_EQ(transfer_status, OkStatus());
}
@@ -499,22 +539,26 @@ TEST_F(ReadTransfer, ResendsParametersIfSentRepeatedChunkDuringRecovery) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 11u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 11u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.window_end_offset(), 64u);
- constexpr ConstByteSpan data(kData64);
+ constexpr ConstByteSpan data(kData32);
// Send the first 8 bytes of the transfer.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 11u, .offset = 0, .data = data.first(8)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(11)
+ .set_offset(0)
+ .set_payload(data.first(8))));
// Skip offset 8, send the rest starting from 16.
for (uint32_t offset = 16; offset < data.size(); offset += 8) {
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 11u,
- .offset = offset,
- .data = data.subspan(offset, 8)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(11)
+ .set_offset(offset)
+ .set_payload(data.subspan(offset, 8))));
}
transfer_thread_.WaitUntilEventIsProcessed();
@@ -522,8 +566,10 @@ TEST_F(ReadTransfer, ResendsParametersIfSentRepeatedChunkDuringRecovery) {
// dropped packet.
ASSERT_EQ(payloads.size(), 2u);
- const Chunk last_chunk = {
- .transfer_id = 11u, .offset = 56, .data = data.subspan(56)};
+ const Chunk last_chunk = Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(11)
+ .set_offset(24)
+ .set_payload(data.subspan(24));
// Re-send the final chunk of the block.
context_.server().SendServerStream<Transfer::Read>(EncodeChunk(last_chunk));
@@ -532,9 +578,9 @@ TEST_F(ReadTransfer, ResendsParametersIfSentRepeatedChunkDuringRecovery) {
// The original drop parameters should be re-sent.
ASSERT_EQ(payloads.size(), 3u);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 11u);
- EXPECT_EQ(c2.offset, 8u);
- ASSERT_EQ(c2.pending_bytes.value(), 56u);
+ EXPECT_EQ(c2.session_id(), 11u);
+ EXPECT_EQ(c2.offset(), 8u);
+ ASSERT_EQ(c2.window_end_offset(), 64u);
// Do it again.
context_.server().SendServerStream<Transfer::Read>(EncodeChunk(last_chunk));
@@ -542,24 +588,25 @@ TEST_F(ReadTransfer, ResendsParametersIfSentRepeatedChunkDuringRecovery) {
ASSERT_EQ(payloads.size(), 4u);
Chunk c3 = DecodeChunk(payloads[3]);
- EXPECT_EQ(c3.transfer_id, 11u);
- EXPECT_EQ(c3.offset, 8u);
- ASSERT_EQ(c3.pending_bytes.value(), 56u);
+ EXPECT_EQ(c3.session_id(), 11u);
+ EXPECT_EQ(c3.offset(), 8u);
+ ASSERT_EQ(c3.window_end_offset(), 64u);
// Finish the transfer normally.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 11u,
- .offset = 8,
- .data = data.subspan(8, 56),
- .remaining_bytes = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(11)
+ .set_offset(8)
+ .set_payload(data.subspan(8))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 5u);
Chunk c4 = DecodeChunk(payloads[4]);
- EXPECT_EQ(c4.transfer_id, 11u);
- ASSERT_TRUE(c4.status.has_value());
- EXPECT_EQ(c4.status.value(), OkStatus());
+ EXPECT_EQ(c4.session_id(), 11u);
+ ASSERT_TRUE(c4.status().has_value());
+ EXPECT_EQ(c4.status().value(), OkStatus());
EXPECT_EQ(transfer_status, OkStatus());
}
@@ -587,37 +634,40 @@ TEST_F(ReadTransfer, Timeout_ResendsCurrentParameters) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 12u);
- EXPECT_EQ(c0.offset, 0u);
- EXPECT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 12u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 64u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Wait for the timeout to expire without doing anything. The client should
- // resend its parameters chunk.
+ // resend its initial parameters chunk.
transfer_thread_.SimulateClientTimeout(12);
ASSERT_EQ(payloads.size(), 2u);
Chunk c = DecodeChunk(payloads.back());
- EXPECT_EQ(c.transfer_id, 12u);
- EXPECT_EQ(c.offset, 0u);
- EXPECT_EQ(c.pending_bytes.value(), 64u);
+ EXPECT_EQ(c.session_id(), 12u);
+ EXPECT_EQ(c.offset(), 0u);
+ EXPECT_EQ(c.window_end_offset(), 64u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Transfer has not yet completed.
EXPECT_EQ(transfer_status, Status::Unknown());
// Finish the transfer following the timeout.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 12u,
- .offset = 0,
- .data = kData32,
- .remaining_bytes = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(12)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 3u);
Chunk c4 = DecodeChunk(payloads.back());
- EXPECT_EQ(c4.transfer_id, 12u);
- ASSERT_TRUE(c4.status.has_value());
- EXPECT_EQ(c4.status.value(), OkStatus());
+ EXPECT_EQ(c4.session_id(), 12u);
+ ASSERT_TRUE(c4.status().has_value());
+ EXPECT_EQ(c4.status().value(), OkStatus());
EXPECT_EQ(transfer_status, OkStatus());
}
@@ -641,15 +691,19 @@ TEST_F(ReadTransfer, Timeout_ResendsUpdatedParameters) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 13u);
- EXPECT_EQ(c0.offset, 0u);
- EXPECT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 13u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 64u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
constexpr ConstByteSpan data(kData32);
// Send some data, but not everything.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 13u, .offset = 0, .data = data.first(16)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(13)
+ .set_offset(0)
+ .set_payload(data.first(16))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 1u);
@@ -660,27 +714,29 @@ TEST_F(ReadTransfer, Timeout_ResendsUpdatedParameters) {
ASSERT_EQ(payloads.size(), 2u);
Chunk c = DecodeChunk(payloads.back());
- EXPECT_EQ(c.transfer_id, 13u);
- EXPECT_EQ(c.offset, 16u);
- EXPECT_EQ(c.pending_bytes.value(), 48u);
+ EXPECT_EQ(c.session_id(), 13u);
+ EXPECT_EQ(c.offset(), 16u);
+ EXPECT_EQ(c.window_end_offset(), 64u);
+ EXPECT_EQ(c.type(), Chunk::Type::kParametersRetransmit);
// Transfer has not yet completed.
EXPECT_EQ(transfer_status, Status::Unknown());
// Send the rest of the data, finishing the transfer.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 13u,
- .offset = 16,
- .data = data.subspan(16),
- .remaining_bytes = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(13)
+ .set_offset(16)
+ .set_payload(data.subspan(16))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 3u);
Chunk c4 = DecodeChunk(payloads.back());
- EXPECT_EQ(c4.transfer_id, 13u);
- ASSERT_TRUE(c4.status.has_value());
- EXPECT_EQ(c4.status.value(), OkStatus());
+ EXPECT_EQ(c4.session_id(), 13u);
+ ASSERT_TRUE(c4.status().has_value());
+ EXPECT_EQ(c4.status().value(), OkStatus());
EXPECT_EQ(transfer_status, OkStatus());
}
@@ -704,9 +760,10 @@ TEST_F(ReadTransfer, Timeout_EndsTransferAfterMaxRetries) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 14u);
- EXPECT_EQ(c0.offset, 0u);
- EXPECT_EQ(c0.pending_bytes.value(), 64u);
+ EXPECT_EQ(c0.session_id(), 14u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 64u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
for (unsigned retry = 1; retry <= kTestRetries; ++retry) {
// Wait for the timeout to expire without doing anything. The client should
@@ -715,30 +772,26 @@ TEST_F(ReadTransfer, Timeout_EndsTransferAfterMaxRetries) {
ASSERT_EQ(payloads.size(), retry + 1);
Chunk c = DecodeChunk(payloads.back());
- EXPECT_EQ(c.transfer_id, 14u);
- EXPECT_EQ(c.offset, 0u);
- EXPECT_EQ(c.pending_bytes.value(), 64u);
+ EXPECT_EQ(c.session_id(), 14u);
+ EXPECT_EQ(c.offset(), 0u);
+ EXPECT_EQ(c.window_end_offset(), 64u);
// Transfer has not yet completed.
EXPECT_EQ(transfer_status, Status::Unknown());
}
// Sleep one more time after the final retry. The client should cancel the
- // transfer at this point and send a DEADLINE_EXCEEDED chunk.
+ // transfer at this point. As no packets were received from the server, no
+ // final status chunk should be sent.
transfer_thread_.SimulateClientTimeout(14);
- ASSERT_EQ(payloads.size(), 5u);
-
- Chunk c4 = DecodeChunk(payloads.back());
- EXPECT_EQ(c4.transfer_id, 14u);
- ASSERT_TRUE(c4.status.has_value());
- EXPECT_EQ(c4.status.value(), Status::DeadlineExceeded());
+ ASSERT_EQ(payloads.size(), 4u);
EXPECT_EQ(transfer_status, Status::DeadlineExceeded());
// After finishing the transfer, nothing else should be sent. Verify this by
// waiting for a bit.
this_thread::sleep_for(kTestTimeout * 4);
- ASSERT_EQ(payloads.size(), 5u);
+ ASSERT_EQ(payloads.size(), 4u);
}
TEST_F(ReadTransfer, Timeout_ReceivingDataResetsRetryCount) {
@@ -762,9 +815,9 @@ TEST_F(ReadTransfer, Timeout_ReceivingDataResetsRetryCount) {
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 14u);
- EXPECT_EQ(c0.offset, 0u);
- EXPECT_EQ(c0.window_end_offset, 64u);
+ EXPECT_EQ(c0.session_id(), 14u);
+ EXPECT_EQ(c0.offset(), 0u);
+ EXPECT_EQ(c0.window_end_offset(), 64u);
// Simulate one less timeout than the maximum amount of retries.
for (unsigned retry = 1; retry <= kTestRetries - 1; ++retry) {
@@ -772,9 +825,9 @@ TEST_F(ReadTransfer, Timeout_ReceivingDataResetsRetryCount) {
ASSERT_EQ(payloads.size(), retry + 1);
Chunk c = DecodeChunk(payloads.back());
- EXPECT_EQ(c.transfer_id, 14u);
- EXPECT_EQ(c.offset, 0u);
- EXPECT_EQ(c.window_end_offset, 64u);
+ EXPECT_EQ(c.session_id(), 14u);
+ EXPECT_EQ(c.offset(), 0u);
+ EXPECT_EQ(c.window_end_offset(), 64u);
// Transfer has not yet completed.
EXPECT_EQ(transfer_status, Status::Unknown());
@@ -782,7 +835,10 @@ TEST_F(ReadTransfer, Timeout_ReceivingDataResetsRetryCount) {
// Send some data.
context_.server().SendServerStream<Transfer::Read>(
- EncodeChunk({.transfer_id = 14u, .offset = 0, .data = data.first(16)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(14)
+ .set_offset(0)
+ .set_payload(data.first(16))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 3u);
@@ -793,19 +849,23 @@ TEST_F(ReadTransfer, Timeout_ReceivingDataResetsRetryCount) {
ASSERT_EQ(payloads.size(), 4u);
Chunk c = DecodeChunk(payloads.back());
- EXPECT_FALSE(c.status.has_value());
- EXPECT_EQ(c.transfer_id, 14u);
- EXPECT_EQ(c.offset, 16u);
- EXPECT_EQ(c.window_end_offset, 64u);
+ EXPECT_FALSE(c.status().has_value());
+ EXPECT_EQ(c.session_id(), 14u);
+ EXPECT_EQ(c.offset(), 16u);
+ EXPECT_EQ(c.window_end_offset(), 64u);
transfer_thread_.SimulateClientTimeout(14);
ASSERT_EQ(payloads.size(), 5u);
c = DecodeChunk(payloads.back());
- EXPECT_FALSE(c.status.has_value());
- EXPECT_EQ(c.transfer_id, 14u);
- EXPECT_EQ(c.offset, 16u);
- EXPECT_EQ(c.window_end_offset, 64u);
+ EXPECT_FALSE(c.status().has_value());
+ EXPECT_EQ(c.session_id(), 14u);
+ EXPECT_EQ(c.offset(), 16u);
+ EXPECT_EQ(c.window_end_offset(), 64u);
+
+ // Ensure we don't leave a dangling reference to transfer_status.
+ client_.CancelTransfer(14);
+ transfer_thread_.WaitUntilEventIsProcessed();
}
TEST_F(ReadTransfer, InitialPacketFails_OnCompletedCalledWithDataLoss) {
@@ -836,7 +896,7 @@ class WriteTransfer : public ::testing::Test {
client_(context_.client(), context_.channel().id(), transfer_thread_),
system_thread_(TransferThreadOptions(), transfer_thread_) {}
- ~WriteTransfer() {
+ ~WriteTransfer() override {
transfer_thread_.Terminate();
system_thread_.join();
}
@@ -862,42 +922,47 @@ TEST_F(WriteTransfer, SingleChunk) {
}));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 3u);
+ EXPECT_EQ(c0.session_id(), 3u);
+ EXPECT_EQ(c0.resource_id(), 3u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Send transfer parameters. Client should send a data chunk and the final
// chunk.
rpc::test::WaitForPackets(context_.output(), 2, [this] {
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 3,
- .pending_bytes = 64,
- .max_chunk_size_bytes = 32,
- .offset = 0}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_offset(0)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(32)));
});
ASSERT_EQ(payloads.size(), 3u);
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.offset, 0u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData32.data(), c1.data.size()), 0);
+ EXPECT_EQ(c1.session_id(), 3u);
+ EXPECT_EQ(c1.offset(), 0u);
+ EXPECT_TRUE(c1.has_payload());
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData32.data(), c1.payload().size()), 0);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 3u);
- ASSERT_TRUE(c2.remaining_bytes.has_value());
- EXPECT_EQ(c2.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c2.session_id(), 3u);
+ ASSERT_TRUE(c2.remaining_bytes().has_value());
+ EXPECT_EQ(c2.remaining_bytes().value(), 0u);
EXPECT_EQ(transfer_status, Status::Unknown());
// Send the final status chunk to complete the transfer.
context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_EQ(payloads.size(), 3u);
@@ -914,50 +979,57 @@ TEST_F(WriteTransfer, MultiChunk) {
}));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 4u);
+ EXPECT_EQ(c0.session_id(), 4u);
+ EXPECT_EQ(c0.resource_id(), 4u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Send transfer parameters with a chunk size smaller than the data.
// Client should send two data chunks and the final chunk.
rpc::test::WaitForPackets(context_.output(), 3, [this] {
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 4,
- .pending_bytes = 64,
- .max_chunk_size_bytes = 16,
- .offset = 0}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(4)
+ .set_offset(0)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(16)));
});
ASSERT_EQ(payloads.size(), 4u);
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 4u);
- EXPECT_EQ(c1.offset, 0u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData32.data(), c1.data.size()), 0);
+ EXPECT_EQ(c1.session_id(), 4u);
+ EXPECT_EQ(c1.offset(), 0u);
+ EXPECT_TRUE(c1.has_payload());
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData32.data(), c1.payload().size()), 0);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 4u);
- EXPECT_EQ(c2.offset, 16u);
- EXPECT_EQ(
- std::memcmp(c2.data.data(), kData32.data() + c2.offset, c2.data.size()),
- 0);
+ EXPECT_EQ(c2.session_id(), 4u);
+ EXPECT_EQ(c2.offset(), 16u);
+ EXPECT_TRUE(c2.has_payload());
+ EXPECT_EQ(std::memcmp(c2.payload().data(),
+ kData32.data() + c2.offset(),
+ c2.payload().size()),
+ 0);
Chunk c3 = DecodeChunk(payloads[3]);
- EXPECT_EQ(c3.transfer_id, 4u);
- ASSERT_TRUE(c3.remaining_bytes.has_value());
- EXPECT_EQ(c3.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c3.session_id(), 4u);
+ ASSERT_TRUE(c3.remaining_bytes().has_value());
+ EXPECT_EQ(c3.remaining_bytes().value(), 0u);
EXPECT_EQ(transfer_status, Status::Unknown());
// Send the final status chunk to complete the transfer.
context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 4, .status = OkStatus()}));
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 4, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_EQ(payloads.size(), 4u);
@@ -974,44 +1046,49 @@ TEST_F(WriteTransfer, OutOfOrder_SeekSupported) {
}));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 5u);
+ EXPECT_EQ(c0.session_id(), 5u);
+ EXPECT_EQ(c0.resource_id(), 5u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Send transfer parameters with a nonzero offset, requesting a seek.
// Client should send a data chunk and the final chunk.
rpc::test::WaitForPackets(context_.output(), 2, [this] {
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 5,
- .pending_bytes = 64,
- .max_chunk_size_bytes = 32,
- .offset = 16}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(5)
+ .set_offset(16)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(32)));
});
ASSERT_EQ(payloads.size(), 3u);
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 5u);
- EXPECT_EQ(c1.offset, 16u);
- EXPECT_EQ(
- std::memcmp(c1.data.data(), kData32.data() + c1.offset, c1.data.size()),
- 0);
+ EXPECT_EQ(c1.session_id(), 5u);
+ EXPECT_EQ(c1.offset(), 16u);
+ EXPECT_TRUE(c1.has_payload());
+ EXPECT_EQ(std::memcmp(c1.payload().data(),
+ kData32.data() + c1.offset(),
+ c1.payload().size()),
+ 0);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 5u);
- ASSERT_TRUE(c2.remaining_bytes.has_value());
- EXPECT_EQ(c2.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c2.session_id(), 5u);
+ ASSERT_TRUE(c2.remaining_bytes().has_value());
+ EXPECT_EQ(c2.remaining_bytes().value(), 0u);
EXPECT_EQ(transfer_status, Status::Unknown());
// Send the final status chunk to complete the transfer.
context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 5, .status = OkStatus()}));
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 5, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_EQ(payloads.size(), 3u);
@@ -1049,30 +1126,34 @@ TEST_F(WriteTransfer, OutOfOrder_SeekNotSupported) {
}));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 6u);
+ EXPECT_EQ(c0.session_id(), 6u);
+ EXPECT_EQ(c0.resource_id(), 6u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Send transfer parameters with a nonzero offset, requesting a seek.
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 6,
- .pending_bytes = 64,
- .max_chunk_size_bytes = 32,
- .offset = 16}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(6)
+ .set_offset(16)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(32)));
transfer_thread_.WaitUntilEventIsProcessed();
// Client should send a status chunk and end the transfer.
ASSERT_EQ(payloads.size(), 2u);
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 6u);
- ASSERT_TRUE(c1.status.has_value());
- EXPECT_EQ(c1.status.value(), Status::Unimplemented());
+ EXPECT_EQ(c1.session_id(), 6u);
+ EXPECT_EQ(c1.type(), Chunk::Type::kCompletion);
+ ASSERT_TRUE(c1.status().has_value());
+ EXPECT_EQ(c1.status().value(), Status::Unimplemented());
EXPECT_EQ(transfer_status, Status::Unimplemented());
}
@@ -1087,18 +1168,20 @@ TEST_F(WriteTransfer, ServerError) {
}));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 7u);
+ EXPECT_EQ(c0.session_id(), 7u);
+ EXPECT_EQ(c0.resource_id(), 7u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Send an error from the server.
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 7, .status = Status::NotFound()}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk::Final(ProtocolVersion::kLegacy, 7, Status::NotFound())));
transfer_thread_.WaitUntilEventIsProcessed();
// Client should not respond and terminate the transfer.
@@ -1106,41 +1189,6 @@ TEST_F(WriteTransfer, ServerError) {
EXPECT_EQ(transfer_status, Status::NotFound());
}
-TEST_F(WriteTransfer, MalformedParametersChunk) {
- stream::MemoryReader reader(kData32);
- Status transfer_status = Status::Unknown();
-
- ASSERT_EQ(OkStatus(),
- client_.Write(8, reader, [&transfer_status](Status status) {
- transfer_status = status;
- }));
- transfer_thread_.WaitUntilEventIsProcessed();
-
- // The client begins by just sending the transfer ID.
- rpc::PayloadsView payloads =
- context_.output().payloads<Transfer::Write>(context_.channel().id());
- ASSERT_EQ(payloads.size(), 1u);
- EXPECT_EQ(transfer_status, Status::Unknown());
-
- Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 8u);
-
- // Send an invalid transfer parameters chunk without pending_bytes.
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 8, .max_chunk_size_bytes = 32}));
- transfer_thread_.WaitUntilEventIsProcessed();
-
- // Client should send a status chunk and end the transfer.
- ASSERT_EQ(payloads.size(), 2u);
-
- Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 8u);
- ASSERT_TRUE(c1.status.has_value());
- EXPECT_EQ(c1.status.value(), Status::InvalidArgument());
-
- EXPECT_EQ(transfer_status, Status::InvalidArgument());
-}
-
TEST_F(WriteTransfer, AbortIfZeroBytesAreRequested) {
stream::MemoryReader reader(kData32);
Status transfer_status = Status::Unknown();
@@ -1151,27 +1199,33 @@ TEST_F(WriteTransfer, AbortIfZeroBytesAreRequested) {
}));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads[0]);
- EXPECT_EQ(c0.transfer_id, 9u);
+ EXPECT_EQ(c0.session_id(), 9u);
+ EXPECT_EQ(c0.resource_id(), 9u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
- // Send an invalid transfer parameters chunk with 0 pending_bytes.
+ // Send an invalid transfer parameters chunk with 0 pending bytes.
context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
- {.transfer_id = 9, .pending_bytes = 0, .max_chunk_size_bytes = 32}));
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(9)
+ .set_offset(0)
+ .set_window_end_offset(0)
+ .set_max_chunk_size_bytes(32)));
transfer_thread_.WaitUntilEventIsProcessed();
// Client should send a status chunk and end the transfer.
ASSERT_EQ(payloads.size(), 2u);
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 9u);
- ASSERT_TRUE(c1.status.has_value());
- EXPECT_EQ(c1.status.value(), Status::ResourceExhausted());
+ EXPECT_EQ(c1.session_id(), 9u);
+ ASSERT_TRUE(c1.status().has_value());
+ EXPECT_EQ(c1.status().value(), Status::ResourceExhausted());
EXPECT_EQ(transfer_status, Status::ResourceExhausted());
}
@@ -1188,14 +1242,16 @@ TEST_F(WriteTransfer, Timeout_RetriesWithInitialChunk) {
kTestTimeout));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 10u);
+ EXPECT_EQ(c0.session_id(), 10u);
+ EXPECT_EQ(c0.resource_id(), 10u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Wait for the timeout to expire without doing anything. The client should
// resend the initial transmit chunk.
@@ -1203,10 +1259,16 @@ TEST_F(WriteTransfer, Timeout_RetriesWithInitialChunk) {
ASSERT_EQ(payloads.size(), 2u);
Chunk c = DecodeChunk(payloads.back());
- EXPECT_EQ(c.transfer_id, 10u);
+ EXPECT_EQ(c.session_id(), 10u);
+ EXPECT_EQ(c.resource_id(), 10u);
+ EXPECT_EQ(c.type(), Chunk::Type::kStart);
// Transfer has not yet completed.
EXPECT_EQ(transfer_status, Status::Unknown());
+
+ // Ensure we don't leave a dangling reference to transfer_status.
+ client_.CancelTransfer(10);
+ transfer_thread_.WaitUntilEventIsProcessed();
}
TEST_F(WriteTransfer, Timeout_RetriesWithMostRecentChunk) {
@@ -1221,40 +1283,45 @@ TEST_F(WriteTransfer, Timeout_RetriesWithMostRecentChunk) {
kTestTimeout));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 11u);
+ EXPECT_EQ(c0.session_id(), 11u);
+ EXPECT_EQ(c0.resource_id(), 11u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Send the first parameters chunk.
rpc::test::WaitForPackets(context_.output(), 2, [this] {
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 11,
- .pending_bytes = 16,
- .max_chunk_size_bytes = 8,
- .offset = 0}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(11)
+ .set_offset(0)
+ .set_window_end_offset(16)
+ .set_max_chunk_size_bytes(8)));
});
ASSERT_EQ(payloads.size(), 3u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 11u);
- EXPECT_EQ(c1.offset, 0u);
- EXPECT_EQ(c1.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData32.data(), c1.data.size()), 0);
+ EXPECT_EQ(c1.session_id(), 11u);
+ EXPECT_EQ(c1.offset(), 0u);
+ EXPECT_EQ(c1.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData32.data(), c1.payload().size()), 0);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 11u);
- EXPECT_EQ(c2.offset, 8u);
- EXPECT_EQ(c2.data.size(), 8u);
- EXPECT_EQ(
- std::memcmp(c2.data.data(), kData32.data() + c2.offset, c1.data.size()),
- 0);
+ EXPECT_EQ(c2.session_id(), 11u);
+ EXPECT_EQ(c2.offset(), 8u);
+ EXPECT_EQ(c2.payload().size(), 8u);
+ EXPECT_EQ(std::memcmp(c2.payload().data(),
+ kData32.data() + c2.offset(),
+ c2.payload().size()),
+ 0);
// Wait for the timeout to expire without doing anything. The client should
// resend the most recently sent chunk.
@@ -1262,13 +1329,19 @@ TEST_F(WriteTransfer, Timeout_RetriesWithMostRecentChunk) {
ASSERT_EQ(payloads.size(), 4u);
Chunk c3 = DecodeChunk(payloads[3]);
- EXPECT_EQ(c3.transfer_id, c2.transfer_id);
- EXPECT_EQ(c3.offset, c2.offset);
- EXPECT_EQ(c3.data.size(), c2.data.size());
- EXPECT_EQ(std::memcmp(c3.data.data(), c2.data.data(), c3.data.size()), 0);
+ EXPECT_EQ(c3.session_id(), c2.session_id());
+ EXPECT_EQ(c3.offset(), c2.offset());
+ EXPECT_EQ(c3.payload().size(), c2.payload().size());
+ EXPECT_EQ(std::memcmp(
+ c3.payload().data(), c2.payload().data(), c3.payload().size()),
+ 0);
// Transfer has not yet completed.
EXPECT_EQ(transfer_status, Status::Unknown());
+
+ // Ensure we don't leave a dangling reference to transfer_status.
+ client_.CancelTransfer(11);
+ transfer_thread_.WaitUntilEventIsProcessed();
}
TEST_F(WriteTransfer, Timeout_RetriesWithSingleChunkTransfer) {
@@ -1283,38 +1356,42 @@ TEST_F(WriteTransfer, Timeout_RetriesWithSingleChunkTransfer) {
kTestTimeout));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 12u);
+ EXPECT_EQ(c0.session_id(), 12u);
+ EXPECT_EQ(c0.resource_id(), 12u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Send the first parameters chunk, requesting all the data. The client should
// respond with one data chunk and a remaining_bytes = 0 chunk.
rpc::test::WaitForPackets(context_.output(), 2, [this] {
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 12,
- .pending_bytes = 64,
- .max_chunk_size_bytes = 64,
- .offset = 0}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(12)
+ .set_offset(0)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(64)));
});
ASSERT_EQ(payloads.size(), 3u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 12u);
- EXPECT_EQ(c1.offset, 0u);
- EXPECT_EQ(c1.data.size(), 32u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData32.data(), c1.data.size()), 0);
+ EXPECT_EQ(c1.session_id(), 12u);
+ EXPECT_EQ(c1.offset(), 0u);
+ EXPECT_EQ(c1.payload().size(), 32u);
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData32.data(), c1.payload().size()), 0);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 12u);
- ASSERT_TRUE(c2.remaining_bytes.has_value());
- EXPECT_EQ(c2.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c2.session_id(), 12u);
+ ASSERT_TRUE(c2.remaining_bytes().has_value());
+ EXPECT_EQ(c2.remaining_bytes().value(), 0u);
// Wait for the timeout to expire without doing anything. The client should
// resend the data chunk.
@@ -1322,28 +1399,30 @@ TEST_F(WriteTransfer, Timeout_RetriesWithSingleChunkTransfer) {
ASSERT_EQ(payloads.size(), 4u);
Chunk c3 = DecodeChunk(payloads[3]);
- EXPECT_EQ(c3.transfer_id, c1.transfer_id);
- EXPECT_EQ(c3.offset, c1.offset);
- EXPECT_EQ(c3.data.size(), c1.data.size());
- EXPECT_EQ(std::memcmp(c3.data.data(), c1.data.data(), c3.data.size()), 0);
+ EXPECT_EQ(c3.session_id(), c1.session_id());
+ EXPECT_EQ(c3.offset(), c1.offset());
+ EXPECT_EQ(c3.payload().size(), c1.payload().size());
+ EXPECT_EQ(std::memcmp(
+ c3.payload().data(), c1.payload().data(), c3.payload().size()),
+ 0);
// The remaining_bytes = 0 chunk should be resent on the next parameters.
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 12,
- .pending_bytes = 64,
- .max_chunk_size_bytes = 64,
- .offset = 32}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(12)
+ .set_offset(32)
+ .set_window_end_offset(64)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(payloads.size(), 5u);
Chunk c4 = DecodeChunk(payloads[4]);
- EXPECT_EQ(c4.transfer_id, 12u);
- ASSERT_TRUE(c4.remaining_bytes.has_value());
- EXPECT_EQ(c4.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c4.session_id(), 12u);
+ ASSERT_TRUE(c4.remaining_bytes().has_value());
+ EXPECT_EQ(c4.remaining_bytes().value(), 0u);
context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 12, .status = OkStatus()}));
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 12, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_EQ(transfer_status, OkStatus());
@@ -1361,14 +1440,16 @@ TEST_F(WriteTransfer, Timeout_EndsTransferAfterMaxRetries) {
kTestTimeout));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 13u);
+ EXPECT_EQ(c0.session_id(), 13u);
+ EXPECT_EQ(c0.resource_id(), 13u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
for (unsigned retry = 1; retry <= kTestRetries; ++retry) {
// Wait for the timeout to expire without doing anything. The client should
@@ -1377,28 +1458,30 @@ TEST_F(WriteTransfer, Timeout_EndsTransferAfterMaxRetries) {
ASSERT_EQ(payloads.size(), retry + 1);
Chunk c = DecodeChunk(payloads.back());
- EXPECT_EQ(c.transfer_id, 13u);
+ EXPECT_EQ(c.session_id(), 13u);
+ EXPECT_EQ(c.resource_id(), 13u);
+ EXPECT_EQ(c.type(), Chunk::Type::kStart);
// Transfer has not yet completed.
EXPECT_EQ(transfer_status, Status::Unknown());
}
// Sleep one more time after the final retry. The client should cancel the
- // transfer at this point and send a DEADLINE_EXCEEDED chunk.
+ // transfer at this point. As no packets were received from the server, no
+ // final status chunk should be sent.
transfer_thread_.SimulateClientTimeout(13);
- ASSERT_EQ(payloads.size(), 5u);
-
- Chunk c4 = DecodeChunk(payloads.back());
- EXPECT_EQ(c4.transfer_id, 13u);
- ASSERT_TRUE(c4.status.has_value());
- EXPECT_EQ(c4.status.value(), Status::DeadlineExceeded());
+ ASSERT_EQ(payloads.size(), 4u);
EXPECT_EQ(transfer_status, Status::DeadlineExceeded());
// After finishing the transfer, nothing else should be sent. Verify this by
// waiting for a bit.
this_thread::sleep_for(kTestTimeout * 4);
- ASSERT_EQ(payloads.size(), 5u);
+ ASSERT_EQ(payloads.size(), 4u);
+
+ // Ensure we don't leave a dangling reference to transfer_status.
+ client_.CancelTransfer(13);
+ transfer_thread_.WaitUntilEventIsProcessed();
}
TEST_F(WriteTransfer, Timeout_NonSeekableReaderEndsTransfer) {
@@ -1413,40 +1496,47 @@ TEST_F(WriteTransfer, Timeout_NonSeekableReaderEndsTransfer) {
kTestTimeout));
transfer_thread_.WaitUntilEventIsProcessed();
- // The client begins by just sending the transfer ID.
+ // The client begins by sending the ID of the resource to transfer.
rpc::PayloadsView payloads =
context_.output().payloads<Transfer::Write>(context_.channel().id());
ASSERT_EQ(payloads.size(), 1u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c0 = DecodeChunk(payloads.back());
- EXPECT_EQ(c0.transfer_id, 14u);
+ EXPECT_EQ(c0.session_id(), 14u);
+ EXPECT_EQ(c0.resource_id(), 14u);
+ EXPECT_EQ(c0.type(), Chunk::Type::kStart);
// Send the first parameters chunk.
rpc::test::WaitForPackets(context_.output(), 2, [this] {
- context_.server().SendServerStream<Transfer::Write>(
- EncodeChunk({.transfer_id = 14,
- .pending_bytes = 16,
- .max_chunk_size_bytes = 8,
- .offset = 0}));
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(14)
+ .set_offset(0)
+ .set_window_end_offset(16)
+ .set_max_chunk_size_bytes(8)));
});
ASSERT_EQ(payloads.size(), 3u);
EXPECT_EQ(transfer_status, Status::Unknown());
Chunk c1 = DecodeChunk(payloads[1]);
- EXPECT_EQ(c1.transfer_id, 14u);
- EXPECT_EQ(c1.offset, 0u);
- EXPECT_EQ(c1.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData32.data(), c1.data.size()), 0);
+ EXPECT_EQ(c1.session_id(), 14u);
+ EXPECT_EQ(c1.offset(), 0u);
+ EXPECT_TRUE(c1.has_payload());
+ EXPECT_EQ(c1.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData32.data(), c1.payload().size()), 0);
Chunk c2 = DecodeChunk(payloads[2]);
- EXPECT_EQ(c2.transfer_id, 14u);
- EXPECT_EQ(c2.offset, 8u);
- EXPECT_EQ(c2.data.size(), 8u);
- EXPECT_EQ(
- std::memcmp(c2.data.data(), kData32.data() + c2.offset, c1.data.size()),
- 0);
+ EXPECT_EQ(c2.session_id(), 14u);
+ EXPECT_EQ(c2.offset(), 8u);
+ EXPECT_TRUE(c2.has_payload());
+ EXPECT_EQ(c2.payload().size(), 8u);
+ EXPECT_EQ(std::memcmp(c2.payload().data(),
+ kData32.data() + c2.offset(),
+ c2.payload().size()),
+ 0);
// Wait for the timeout to expire without doing anything. The client should
// fail to seek back and end the transfer.
@@ -1454,14 +1544,1039 @@ TEST_F(WriteTransfer, Timeout_NonSeekableReaderEndsTransfer) {
ASSERT_EQ(payloads.size(), 4u);
Chunk c3 = DecodeChunk(payloads[3]);
- EXPECT_EQ(c3.transfer_id, 14u);
- ASSERT_TRUE(c3.status.has_value());
- EXPECT_EQ(c3.status.value(), Status::DeadlineExceeded());
+ EXPECT_EQ(c3.protocol_version(), ProtocolVersion::kLegacy);
+ EXPECT_EQ(c3.session_id(), 14u);
+ ASSERT_TRUE(c3.status().has_value());
+ EXPECT_EQ(c3.status().value(), Status::DeadlineExceeded());
EXPECT_EQ(transfer_status, Status::DeadlineExceeded());
+
+ // Ensure we don't leave a dangling reference to transfer_status.
+ client_.CancelTransfer(14);
+ transfer_thread_.WaitUntilEventIsProcessed();
}
-PW_MODIFY_DIAGNOSTICS_POP();
+TEST_F(WriteTransfer, ManualCancel) {
+ stream::MemoryReader reader(kData32);
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Write(
+ 15,
+ reader,
+ [&transfer_status](Status status) { transfer_status = status; },
+ kTestTimeout));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // The client begins by sending the ID of the resource to transfer.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Write>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 15u);
+ EXPECT_EQ(chunk.resource_id(), 15u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+
+ // Get a response from the server, then cancel the transfer.
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(15)
+ .set_offset(0)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(32)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+ ASSERT_EQ(payloads.size(), 2u);
+
+ client_.CancelTransfer(15);
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Client should send a cancellation chunk to the server.
+ ASSERT_EQ(payloads.size(), 3u);
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 15u);
+ ASSERT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.status().value(), Status::Cancelled());
+
+ EXPECT_EQ(transfer_status, Status::Cancelled());
+}
+
+TEST_F(WriteTransfer, ManualCancel_NoContact) {
+ stream::MemoryReader reader(kData32);
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Write(
+ 15,
+ reader,
+ [&transfer_status](Status status) { transfer_status = status; },
+ kTestTimeout));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // The client begins by sending the ID of the resource to transfer.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Write>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 15u);
+ EXPECT_EQ(chunk.resource_id(), 15u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+
+ // Cancel transfer without a server response. No final chunk should be sent.
+ client_.CancelTransfer(15);
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 1u);
+
+ EXPECT_EQ(transfer_status, Status::Cancelled());
+}
+
+TEST_F(ReadTransfer, Version2_SingleChunk) {
+ stream::MemoryWriterBuffer<64> writer;
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Read(
+ 3,
+ writer,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Initial chunk of the transfer is sent. This chunk should contain all the
+ // fields from both legacy and version 2 protocols for backwards
+ // compatibility.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Read>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads[0]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // The server responds with a START_ACK, continuing the version 2 handshake
+ // and assigning a session_id to the transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
+ .set_session_id(29)
+ .set_resource_id(3)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 2u);
+
+ // Client should accept the session_id with a START_ACK_CONFIRMATION,
+ // additionally containing the initial parameters for the read transfer.
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // Send all the transfer data. Client should accept it and complete the
+ // transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(29)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 3u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ EXPECT_EQ(transfer_status, OkStatus());
+ EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
+ 0);
+
+ context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
+ .set_session_id(29)));
+}
+
+TEST_F(ReadTransfer, Version2_ServerRunsLegacy) {
+ stream::MemoryWriterBuffer<64> writer;
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Read(
+ 3,
+ writer,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Initial chunk of the transfer is sent. This chunk should contain all the
+ // fields from both legacy and version 2 protocols for backwards
+ // compatibility.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Read>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads[0]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // Instead of a START_ACK to continue the handshake, the server responds with
+ // an immediate data chunk, indicating that it is running the legacy protocol
+ // version. Client should revert to legacy, using the resource_id of 3 as the
+ // session_id, and complete the transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(3)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 2u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kLegacy);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ EXPECT_EQ(transfer_status, OkStatus());
+ EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
+ 0);
+}
+
+TEST_F(ReadTransfer, Version2_TimeoutDuringHandshake) {
+ stream::MemoryWriterBuffer<64> writer;
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Read(
+ 3,
+ writer,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Initial chunk of the transfer is sent. This chunk should contain all the
+ // fields from both legacy and version 2 protocols for backwards
+ // compatibility.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Read>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // Wait for the timeout to expire without doing anything. The client should
+ // resend the initial chunk.
+ transfer_thread_.SimulateClientTimeout(3);
+ ASSERT_EQ(payloads.size(), 2u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // This time, the server responds, continuing the handshake and transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
+ .set_session_id(31)
+ .set_resource_id(3)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 3u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(31)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 4u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ EXPECT_EQ(transfer_status, OkStatus());
+ EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
+ 0);
+
+ context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
+ .set_session_id(31)));
+}
+
+TEST_F(ReadTransfer, Version2_TimeoutAfterHandshake) {
+ stream::MemoryWriterBuffer<64> writer;
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Read(
+ 3,
+ writer,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Initial chunk of the transfer is sent. This chunk should contain all the
+ // fields from both legacy and version 2 protocols for backwards
+ // compatibility.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Read>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // The server responds with a START_ACK, continuing the version 2 handshake
+ // and assigning a session_id to the transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
+ .set_session_id(33)
+ .set_resource_id(3)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 2u);
+
+ // Client should accept the session_id with a START_ACK_CONFIRMATION,
+ // additionally containing the initial parameters for the read transfer.
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // Wait for the timeout to expire without doing anything. The client should
+ // resend the confirmation chunk.
+ transfer_thread_.SimulateClientTimeout(33);
+ ASSERT_EQ(payloads.size(), 3u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // The server responds and the transfer should continue normally.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(33)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 4u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ EXPECT_EQ(transfer_status, OkStatus());
+ EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
+ 0);
+
+ context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
+ .set_session_id(33)));
+}
+
+TEST_F(ReadTransfer, Version2_ServerErrorDuringHandshake) {
+ stream::MemoryWriterBuffer<64> writer;
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Read(
+ 3,
+ writer,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Initial chunk of the transfer is sent. This chunk should contain all the
+ // fields from both legacy and version 2 protocols for backwards
+ // compatibility.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Read>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // The server responds to the start request with an error.
+ context_.server().SendServerStream<Transfer::Read>(EncodeChunk(Chunk::Final(
+ ProtocolVersion::kVersionTwo, 3, Status::Unauthenticated())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unauthenticated());
+}
+
+TEST_F(ReadTransfer, Version2_TimeoutWaitingForCompletionAckRetries) {
+ stream::MemoryWriterBuffer<64> writer;
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Read(
+ 3,
+ writer,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Initial chunk of the transfer is sent. This chunk should contain all the
+ // fields from both legacy and version 2 protocols for backwards
+ // compatibility.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Read>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads[0]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // The server responds with a START_ACK, continuing the version 2 handshake
+ // and assigning a session_id to the transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
+ .set_session_id(29)
+ .set_resource_id(3)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 2u);
+
+ // Client should accept the session_id with a START_ACK_CONFIRMATION,
+ // additionally containing the initial parameters for the read transfer.
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // Send all the transfer data. Client should accept it and complete the
+ // transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(29)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 3u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ EXPECT_EQ(transfer_status, OkStatus());
+ EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
+ 0);
+
+ // Time out instead of sending a completion ACK. THe transfer should resend
+ // its completion chunk.
+ transfer_thread_.SimulateClientTimeout(29);
+ ASSERT_EQ(payloads.size(), 4u);
+
+ // Reset transfer_status to check whether the handler is called again.
+ transfer_status = Status::Unknown();
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ // Transfer handler should not be called a second time in response to the
+ // re-sent completion chunk.
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ // Send a completion ACK to end the transfer.
+ context_.server().SendServerStream<Transfer::Read>(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
+ .set_session_id(29)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // No further chunks should be sent following the ACK.
+ transfer_thread_.SimulateClientTimeout(29);
+ ASSERT_EQ(payloads.size(), 4u);
+}
+
+TEST_F(ReadTransfer,
+ Version2_TimeoutWaitingForCompletionAckEndsTransferAfterRetries) {
+ stream::MemoryWriterBuffer<64> writer;
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Read(
+ 3,
+ writer,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Initial chunk of the transfer is sent. This chunk should contain all the
+ // fields from both legacy and version 2 protocols for backwards
+ // compatibility.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Read>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads[0]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // The server responds with a START_ACK, continuing the version 2 handshake
+ // and assigning a session_id to the transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
+ .set_session_id(29)
+ .set_resource_id(3)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 2u);
+
+ // Client should accept the session_id with a START_ACK_CONFIRMATION,
+ // additionally containing the initial parameters for the read transfer.
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 64u);
+ EXPECT_EQ(chunk.max_chunk_size_bytes(), 37u);
+
+ // Send all the transfer data. Client should accept it and complete the
+ // transfer.
+ context_.server().SendServerStream<Transfer::Read>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(29)
+ .set_offset(0)
+ .set_payload(kData32)
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 3u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ EXPECT_EQ(transfer_status, OkStatus());
+ EXPECT_EQ(std::memcmp(writer.data(), kData32.data(), writer.bytes_written()),
+ 0);
+
+ // Time out instead of sending a completion ACK. THe transfer should resend
+ // its completion chunk at first, then terminate after the maximum number of
+ // retries.
+ transfer_thread_.SimulateClientTimeout(29);
+ ASSERT_EQ(payloads.size(), 4u); // Retry 1.
+
+ transfer_thread_.SimulateClientTimeout(29);
+ ASSERT_EQ(payloads.size(), 5u); // Retry 2.
+
+ transfer_thread_.SimulateClientTimeout(29);
+ ASSERT_EQ(payloads.size(), 6u); // Retry 3.
+
+ transfer_thread_.SimulateClientTimeout(29);
+ ASSERT_EQ(payloads.size(), 6u); // No more retries; transfer has ended.
+}
+
+TEST_F(WriteTransfer, Version2_SingleChunk) {
+ stream::MemoryReader reader(kData32);
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Write(
+ 3,
+ reader,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // The client begins by sending the ID of the resource to transfer.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Write>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // The server responds with a START_ACK, continuing the version 2 handshake
+ // and assigning a session_id to the transfer.
+ context_.server().SendServerStream<Transfer::Write>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
+ .set_session_id(29)
+ .set_resource_id(3)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 2u);
+
+ // Client should accept the session_id with a START_ACK_CONFIRMATION.
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+
+ // The server can then begin the data transfer by sending its transfer
+ // parameters. Client should respond with a data chunk and the final chunk.
+ rpc::test::WaitForPackets(context_.output(), 2, [this] {
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersRetransmit)
+ .set_session_id(29)
+ .set_offset(0)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(32)));
+ });
+
+ ASSERT_EQ(payloads.size(), 4u);
+
+ chunk = DecodeChunk(payloads[2]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 29u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_TRUE(chunk.has_payload());
+ EXPECT_EQ(std::memcmp(
+ chunk.payload().data(), kData32.data(), chunk.payload().size()),
+ 0);
+
+ chunk = DecodeChunk(payloads[3]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 29u);
+ ASSERT_TRUE(chunk.remaining_bytes().has_value());
+ EXPECT_EQ(chunk.remaining_bytes().value(), 0u);
+
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ // Send the final status chunk to complete the transfer.
+ context_.server().SendServerStream<Transfer::Write>(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 29, OkStatus())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Client should acknowledge the completion of the transfer.
+ EXPECT_EQ(payloads.size(), 5u);
+
+ chunk = DecodeChunk(payloads[4]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletionAck);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 29u);
+
+ EXPECT_EQ(transfer_status, OkStatus());
+}
+
+TEST_F(WriteTransfer, Version2_ServerRunsLegacy) {
+ stream::MemoryReader reader(kData32);
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Write(
+ 3,
+ reader,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // The client begins by sending the ID of the resource to transfer.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Write>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // Instead of continuing the handshake with a START_ACK, the server
+ // immediately sends parameters, indicating that it only supports the legacy
+ // protocol. Client should switch over to legacy and continue the transfer.
+ rpc::test::WaitForPackets(context_.output(), 2, [this] {
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_offset(0)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(32)));
+ });
+
+ ASSERT_EQ(payloads.size(), 3u);
+
+ chunk = DecodeChunk(payloads[1]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kLegacy);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_TRUE(chunk.has_payload());
+ EXPECT_EQ(std::memcmp(
+ chunk.payload().data(), kData32.data(), chunk.payload().size()),
+ 0);
+
+ chunk = DecodeChunk(payloads[2]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kLegacy);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ ASSERT_TRUE(chunk.remaining_bytes().has_value());
+ EXPECT_EQ(chunk.remaining_bytes().value(), 0u);
+
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ // Send the final status chunk to complete the transfer.
+ context_.server().SendServerStream<Transfer::Write>(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_EQ(payloads.size(), 3u);
+ EXPECT_EQ(transfer_status, OkStatus());
+}
+
+TEST_F(WriteTransfer, Version2_RetryDuringHandshake) {
+ stream::MemoryReader reader(kData32);
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Write(
+ 3,
+ reader,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // The client begins by sending the ID of the resource to transfer.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Write>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // Time out waiting for a server response. The client should resend the
+ // initial packet.
+ transfer_thread_.SimulateClientTimeout(3);
+ ASSERT_EQ(payloads.size(), 2u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // This time, respond with the correct continuation packet. The transfer
+ // should resume and complete normally.
+ context_.server().SendServerStream<Transfer::Write>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
+ .set_session_id(31)
+ .set_resource_id(3)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 3u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+
+ rpc::test::WaitForPackets(context_.output(), 2, [this] {
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersRetransmit)
+ .set_session_id(31)
+ .set_offset(0)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(32)));
+ });
+
+ ASSERT_EQ(payloads.size(), 5u);
+
+ chunk = DecodeChunk(payloads[3]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 31u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_TRUE(chunk.has_payload());
+ EXPECT_EQ(std::memcmp(
+ chunk.payload().data(), kData32.data(), chunk.payload().size()),
+ 0);
+
+ chunk = DecodeChunk(payloads[4]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 31u);
+ ASSERT_TRUE(chunk.remaining_bytes().has_value());
+ EXPECT_EQ(chunk.remaining_bytes().value(), 0u);
+
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ context_.server().SendServerStream<Transfer::Write>(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 31, OkStatus())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Client should acknowledge the completion of the transfer.
+ EXPECT_EQ(payloads.size(), 6u);
+
+ chunk = DecodeChunk(payloads[5]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletionAck);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 31u);
+
+ EXPECT_EQ(transfer_status, OkStatus());
+}
+
+TEST_F(WriteTransfer, Version2_RetryAfterHandshake) {
+ stream::MemoryReader reader(kData32);
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Write(
+ 3,
+ reader,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // The client begins by sending the ID of the resource to transfer.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Write>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // The server responds with a START_ACK, continuing the version 2 handshake
+ // and assigning a session_id to the transfer.
+ context_.server().SendServerStream<Transfer::Write>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAck)
+ .set_session_id(33)
+ .set_resource_id(3)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(payloads.size(), 2u);
+
+ // Client should accept the session_id with a START_ACK_CONFIRMATION.
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+
+ // Time out waiting for a server response. The client should resend the
+ // initial packet.
+ transfer_thread_.SimulateClientTimeout(33);
+ ASSERT_EQ(payloads.size(), 3u);
+
+ chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAckConfirmation);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_FALSE(chunk.resource_id().has_value());
+
+ // This time, respond with the first transfer parameters chunk. The transfer
+ // should resume and complete normally.
+ rpc::test::WaitForPackets(context_.output(), 2, [this] {
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersRetransmit)
+ .set_session_id(33)
+ .set_offset(0)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(32)));
+ });
+
+ ASSERT_EQ(payloads.size(), 5u);
+
+ chunk = DecodeChunk(payloads[3]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 33u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_TRUE(chunk.has_payload());
+ EXPECT_EQ(std::memcmp(
+ chunk.payload().data(), kData32.data(), chunk.payload().size()),
+ 0);
+
+ chunk = DecodeChunk(payloads[4]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 33u);
+ ASSERT_TRUE(chunk.remaining_bytes().has_value());
+ EXPECT_EQ(chunk.remaining_bytes().value(), 0u);
+
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ context_.server().SendServerStream<Transfer::Write>(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 33, OkStatus())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Client should acknowledge the completion of the transfer.
+ EXPECT_EQ(payloads.size(), 6u);
+
+ chunk = DecodeChunk(payloads[5]);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletionAck);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 33u);
+
+ EXPECT_EQ(transfer_status, OkStatus());
+}
+
+TEST_F(WriteTransfer, Version2_ServerErrorDuringHandshake) {
+ stream::MemoryReader reader(kData32);
+ Status transfer_status = Status::Unknown();
+
+ ASSERT_EQ(OkStatus(),
+ client_.Write(
+ 3,
+ reader,
+ [&transfer_status](Status status) { transfer_status = status; },
+ cfg::kDefaultChunkTimeout,
+ ProtocolVersion::kVersionTwo));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // The client begins by sending the ID of the resource to transfer.
+ rpc::PayloadsView payloads =
+ context_.output().payloads<Transfer::Write>(context_.channel().id());
+ ASSERT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::Unknown());
+
+ Chunk chunk = DecodeChunk(payloads.back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStart);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // The server responds to the start request with an error.
+ context_.server().SendServerStream<Transfer::Write>(EncodeChunk(
+ Chunk::Final(ProtocolVersion::kVersionTwo, 3, Status::NotFound())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_EQ(payloads.size(), 1u);
+ EXPECT_EQ(transfer_status, Status::NotFound());
+}
} // namespace
} // namespace pw::transfer::test
diff --git a/pw_transfer/context.cc b/pw_transfer/context.cc
index 04027036e..22686398c 100644
--- a/pw_transfer/context.cc
+++ b/pw_transfer/context.cc
@@ -17,7 +17,6 @@
#include "pw_transfer/internal/context.h"
#include <chrono>
-#include <mutex>
#include "pw_assert/check.h"
#include "pw_chrono/system_clock.h"
@@ -27,9 +26,6 @@
#include "pw_transfer/transfer_thread.h"
#include "pw_varint/varint.h"
-PW_MODIFY_DIAGNOSTICS_PUSH();
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
-
namespace pw::transfer::internal {
void Context::HandleEvent(const Event& event) {
@@ -37,7 +33,7 @@ void Context::HandleEvent(const Event& event) {
case EventType::kNewClientTransfer:
case EventType::kNewServerTransfer: {
if (active()) {
- Finish(Status::Aborted());
+ Abort(Status::Aborted());
}
Initialize(event.new_transfer);
@@ -45,7 +41,13 @@ void Context::HandleEvent(const Event& event) {
if (event.type == EventType::kNewClientTransfer) {
InitiateTransferAsClient();
} else {
- StartTransferAsServer(event.new_transfer);
+ if (StartTransferAsServer(event.new_transfer)) {
+ // TODO(frolv): This should probably be restructured.
+ HandleChunkEvent({.context_identifier = event.new_transfer.session_id,
+ .match_resource_id = false, // Unused.
+ .data = event.new_transfer.raw_chunk_data,
+ .size = event.new_transfer.raw_chunk_size});
+ }
}
return;
}
@@ -61,8 +63,18 @@ void Context::HandleEvent(const Event& event) {
HandleTimeout();
return;
+ case EventType::kClientEndTransfer:
+ case EventType::kServerEndTransfer:
+ if (active()) {
+ if (event.end_transfer.send_status_chunk) {
+ TerminateTransfer(event.end_transfer.status);
+ } else {
+ Abort(event.end_transfer.status);
+ }
+ }
+ return;
+
case EventType::kSendStatusChunk:
- case EventType::kSetTransferStream:
case EventType::kAddTransferHandler:
case EventType::kRemoveTransferHandler:
case EventType::kTerminate:
@@ -77,33 +89,64 @@ void Context::InitiateTransferAsClient() {
SetTimeout(chunk_timeout_);
+ PW_LOG_INFO("Starting transfer for resource %u",
+ static_cast<unsigned>(resource_id_));
+
+ // Receive transfers should prepare their initial parameters to be send in the
+ // initial chunk.
if (type() == TransferType::kReceive) {
- // A receiver begins a new transfer with a parameters chunk telling the
- // transmitter what to send.
- UpdateAndSendTransferParameters(TransmitAction::kBegin);
- } else {
- SendInitialTransmitChunk();
+ UpdateTransferParameters();
+ }
+
+ if (desired_protocol_version_ == ProtocolVersion::kLegacy) {
+ // Legacy transfers go straight into the data transfer phase without a
+ // handshake.
+ if (type() == TransferType::kReceive) {
+ SendTransferParameters(TransmitAction::kBegin);
+ } else {
+ SendInitialTransmitChunk();
+ }
+
+ LogTransferConfiguration();
+ return;
}
- // Don't send an error packet. If the transfer failed to start, then there's
- // nothing to tell the server about.
+ // In newer protocol versions, begin the initial transfer handshake.
+ Chunk start_chunk(desired_protocol_version_, Chunk::Type::kStart);
+ start_chunk.set_resource_id(resource_id_);
+
+ if (type() == TransferType::kReceive) {
+ // Parameters should still be set on the initial chunk for backwards
+ // compatibility if the server only supports the legacy protocol.
+ SetTransferParameters(start_chunk);
+ }
+
+ EncodeAndSendChunk(start_chunk);
}
-void Context::StartTransferAsServer(const NewTransferEvent& new_transfer) {
- PW_LOG_INFO("Starting transfer %u with handler %u",
- static_cast<unsigned>(new_transfer.transfer_id),
- static_cast<unsigned>(new_transfer.handler_id));
+bool Context::StartTransferAsServer(const NewTransferEvent& new_transfer) {
+ PW_LOG_INFO("Starting %s transfer %u for resource %u",
+ new_transfer.type == TransferType::kTransmit ? "read" : "write",
+ static_cast<unsigned>(new_transfer.session_id),
+ static_cast<unsigned>(new_transfer.resource_id));
+ LogTransferConfiguration();
+
+ flags_ |= kFlagsContactMade;
- if (const Status status = new_transfer.handler->Prepare(new_transfer.type);
+ if (Status status = new_transfer.handler->Prepare(new_transfer.type);
!status.ok()) {
PW_LOG_WARN("Transfer handler %u prepare failed with status %u",
static_cast<unsigned>(new_transfer.handler->id()),
status.code());
- Finish(status.IsPermissionDenied() ? status : Status::DataLoss());
- // Do not send the final status packet here! On the server, a start event
- // will immediately be followed by the server chunk event. Sending the final
- // chunk will be handled then.
- return;
+
+ // As this failure occurs at the start of a transfer, no protocol version is
+ // yet negotiated and one must be set to send a response. It is okay to use
+ // the desired version here, as that comes from the client.
+ configured_protocol_version_ = desired_protocol_version_;
+
+ status = status.IsPermissionDenied() ? status : Status::DataLoss();
+ TerminateTransfer(status, /*with_resource_id=*/true);
+ return false;
}
// Initialize doesn't set the handler since it's specific to server transfers.
@@ -112,101 +155,130 @@ void Context::StartTransferAsServer(const NewTransferEvent& new_transfer) {
// Server transfers use the stream provided by the handler rather than the
// stream included in the NewTransferEvent.
stream_ = &new_transfer.handler->stream();
+
+ return true;
}
void Context::SendInitialTransmitChunk() {
- // A transmitter begins a transfer by just sending its ID.
- internal::Chunk chunk = {};
- chunk.transfer_id = transfer_id_;
- chunk.type = Chunk::Type::kTransferStart;
+ // A transmitter begins a transfer by sending the ID of the resource to which
+ // it wishes to write.
+ Chunk chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart);
+ chunk.set_session_id(session_id_);
EncodeAndSendChunk(chunk);
}
+void Context::UpdateTransferParameters() {
+ size_t pending_bytes =
+ std::min(max_parameters_->pending_bytes(),
+ static_cast<uint32_t>(writer().ConservativeWriteLimit()));
+
+ window_size_ = pending_bytes;
+ window_end_offset_ = offset_ + pending_bytes;
+
+ max_chunk_size_bytes_ = MaxWriteChunkSize(
+ max_parameters_->max_chunk_size_bytes(), rpc_writer_->channel_id());
+}
+
+void Context::SetTransferParameters(Chunk& parameters) {
+ parameters.set_window_end_offset(window_end_offset_)
+ .set_max_chunk_size_bytes(max_chunk_size_bytes_)
+ .set_min_delay_microseconds(kDefaultChunkDelayMicroseconds)
+ .set_offset(offset_);
+}
+
+void Context::UpdateAndSendTransferParameters(TransmitAction action) {
+ UpdateTransferParameters();
+
+ PW_LOG_INFO("Transfer rate: %u B/s",
+ static_cast<unsigned>(transfer_rate_.GetRateBytesPerSecond()));
+
+ return SendTransferParameters(action);
+}
+
void Context::SendTransferParameters(TransmitAction action) {
- internal::Chunk parameters = {
- .transfer_id = transfer_id_,
- .window_end_offset = window_end_offset_,
- .pending_bytes = pending_bytes_,
- .max_chunk_size_bytes = max_chunk_size_bytes_,
- .min_delay_microseconds = kDefaultChunkDelayMicroseconds,
- .offset = offset_,
- };
+ Chunk::Type type = Chunk::Type::kParametersRetransmit;
switch (action) {
case TransmitAction::kBegin:
- parameters.type = internal::Chunk::Type::kTransferStart;
+ type = Chunk::Type::kStart;
break;
case TransmitAction::kRetransmit:
- parameters.type = internal::Chunk::Type::kParametersRetransmit;
+ type = Chunk::Type::kParametersRetransmit;
break;
case TransmitAction::kExtend:
- parameters.type = internal::Chunk::Type::kParametersContinue;
+ type = Chunk::Type::kParametersContinue;
break;
}
+ Chunk parameters(configured_protocol_version_, type);
+ parameters.set_session_id(session_id_);
+ SetTransferParameters(parameters);
+
PW_LOG_DEBUG(
"Transfer %u sending transfer parameters: "
- "offset=%u, window_end_offset=%u, pending_bytes=%u, chunk_size=%u",
- static_cast<unsigned>(transfer_id_),
+ "offset=%u, window_end_offset=%u, max_chunk_size=%u",
+ static_cast<unsigned>(session_id_),
static_cast<unsigned>(offset_),
static_cast<unsigned>(window_end_offset_),
- static_cast<unsigned>(pending_bytes_),
static_cast<unsigned>(max_chunk_size_bytes_));
EncodeAndSendChunk(parameters);
}
void Context::EncodeAndSendChunk(const Chunk& chunk) {
- Result<ConstByteSpan> data =
- internal::EncodeChunk(chunk, thread_->encode_buffer());
+ last_chunk_sent_ = chunk.type();
+
+ Result<ConstByteSpan> data = chunk.Encode(thread_->encode_buffer());
if (!data.ok()) {
PW_LOG_ERROR("Failed to encode chunk for transfer %u: %d",
- static_cast<unsigned>(chunk.transfer_id),
+ static_cast<unsigned>(chunk.session_id()),
data.status().code());
if (active()) {
- Finish(Status::Internal());
+ TerminateTransfer(Status::Internal());
}
return;
}
if (const Status status = rpc_writer_->Write(*data); !status.ok()) {
PW_LOG_ERROR("Failed to write chunk for transfer %u: %d",
- static_cast<unsigned>(chunk.transfer_id),
+ static_cast<unsigned>(chunk.session_id()),
status.code());
if (active()) {
- Finish(Status::Internal());
+ TerminateTransfer(Status::Internal());
}
return;
}
}
-void Context::UpdateAndSendTransferParameters(TransmitAction action) {
- size_t pending_bytes =
- std::min(max_parameters_->pending_bytes(),
- static_cast<uint32_t>(writer().ConservativeWriteLimit()));
-
- window_size_ = pending_bytes;
- window_end_offset_ = offset_ + pending_bytes;
- pending_bytes_ = pending_bytes;
-
- max_chunk_size_bytes_ = MaxWriteChunkSize(
- max_parameters_->max_chunk_size_bytes(), rpc_writer_->channel_id());
-
- PW_LOG_INFO("Transfer rate: %u B/s",
- static_cast<unsigned>(transfer_rate_.GetRateBytesPerSecond()));
-
- return SendTransferParameters(action);
-}
-
void Context::Initialize(const NewTransferEvent& new_transfer) {
PW_DCHECK(!active());
- transfer_id_ = new_transfer.transfer_id;
+ PW_DCHECK_INT_NE(new_transfer.protocol_version,
+ ProtocolVersion::kUnknown,
+ "Cannot start a transfer with an unknown protocol");
+
+ session_id_ = new_transfer.session_id;
+ resource_id_ = new_transfer.resource_id;
+ desired_protocol_version_ = new_transfer.protocol_version;
+ configured_protocol_version_ = ProtocolVersion::kUnknown;
+
flags_ = static_cast<uint8_t>(new_transfer.type);
transfer_state_ = TransferState::kWaiting;
retries_ = 0;
max_retries_ = new_transfer.max_retries;
+ lifetime_retries_ = 0;
+ max_lifetime_retries_ = new_transfer.max_lifetime_retries;
+
+ if (desired_protocol_version_ == ProtocolVersion::kLegacy) {
+ // In a legacy transfer, there is no protocol negotiation stage.
+ // Automatically configure the context to run the legacy protocol and
+ // proceed to waiting for a chunk.
+ configured_protocol_version_ = ProtocolVersion::kLegacy;
+ transfer_state_ = TransferState::kWaiting;
+ } else {
+ transfer_state_ = TransferState::kInitiating;
+ }
rpc_writer_ = new_transfer.rpc_writer;
stream_ = new_transfer.stream;
@@ -214,12 +286,12 @@ void Context::Initialize(const NewTransferEvent& new_transfer) {
offset_ = 0;
window_size_ = 0;
window_end_offset_ = 0;
- pending_bytes_ = 0;
max_chunk_size_bytes_ = new_transfer.max_parameters->max_chunk_size_bytes();
max_parameters_ = new_transfer.max_parameters;
thread_ = new_transfer.transfer_thread;
+ last_chunk_sent_ = Chunk::Type::kStart;
last_chunk_offset_ = 0;
chunk_timeout_ = new_transfer.timeout;
interchunk_delay_ = chrono::SystemClock::for_at_least(
@@ -230,23 +302,25 @@ void Context::Initialize(const NewTransferEvent& new_transfer) {
}
void Context::HandleChunkEvent(const ChunkEvent& event) {
- PW_DCHECK(event.transfer_id == transfer_id_);
-
- Chunk chunk;
- if (!DecodeChunk(ConstByteSpan(event.data, event.size), chunk).ok()) {
+ Result<Chunk> maybe_chunk =
+ Chunk::Parse(ConstByteSpan(event.data, event.size));
+ if (!maybe_chunk.ok()) {
return;
}
+ Chunk chunk = *maybe_chunk;
+
// Received some data. Reset the retry counter.
retries_ = 0;
+ flags_ |= kFlagsContactMade;
- if (chunk.status.has_value()) {
+ if (chunk.IsTerminatingChunk()) {
if (active()) {
- Finish(chunk.status.value());
+ HandleTermination(chunk.status().value());
} else {
PW_LOG_DEBUG("Got final status %d for completed transfer %d",
- static_cast<int>(chunk.status.value().code()),
- static_cast<int>(transfer_id_));
+ static_cast<int>(chunk.status().value().code()),
+ static_cast<int>(session_id_));
}
return;
}
@@ -258,6 +332,114 @@ void Context::HandleChunkEvent(const ChunkEvent& event) {
}
}
+void Context::PerformInitialHandshake(const Chunk& chunk) {
+ switch (chunk.type()) {
+ // Initial packet sent from a client to a server.
+ case Chunk::Type::kStart: {
+ UpdateLocalProtocolConfigurationFromPeer(chunk);
+
+ // This cast is safe as we know we're running in a transfer server.
+ uint32_t resource_id = static_cast<ServerContext&>(*this).handler()->id();
+
+ Chunk start_ack(configured_protocol_version_, Chunk::Type::kStartAck);
+ start_ack.set_session_id(session_id_).set_resource_id(resource_id);
+
+ EncodeAndSendChunk(start_ack);
+ break;
+ }
+
+ // Response packet sent from a server to a client. Contains the assigned
+ // session_id of the transfer.
+ case Chunk::Type::kStartAck: {
+ UpdateLocalProtocolConfigurationFromPeer(chunk);
+
+ // Accept the assigned session_id and tell the server that the transfer
+ // can begin.
+ session_id_ = chunk.session_id();
+ PW_LOG_DEBUG("Transfer for resource %u was assigned session ID %u",
+ static_cast<unsigned>(resource_id_),
+ static_cast<unsigned>(session_id_));
+
+ Chunk start_ack_confirmation(configured_protocol_version_,
+ Chunk::Type::kStartAckConfirmation);
+ start_ack_confirmation.set_session_id(session_id_);
+
+ if (type() == TransferType::kReceive) {
+ // In a receive transfer, tag the initial transfer parameters onto the
+ // confirmation chunk so that the server can immediately begin sending
+ // data.
+ UpdateTransferParameters();
+ SetTransferParameters(start_ack_confirmation);
+ }
+
+ set_transfer_state(TransferState::kWaiting);
+ EncodeAndSendChunk(start_ack_confirmation);
+ break;
+ }
+
+ // Confirmation sent by a client to a server of the configured transfer
+ // version and session ID. Completes the handshake and begins the actual
+ // data transfer.
+ case Chunk::Type::kStartAckConfirmation: {
+ set_transfer_state(TransferState::kWaiting);
+
+ if (type() == TransferType::kTransmit) {
+ HandleTransmitChunk(chunk);
+ } else {
+ HandleReceiveChunk(chunk);
+ }
+ break;
+ }
+
+ // If a non-handshake chunk is received during an INITIATING state, the
+ // transfer peer is running a legacy protocol version, which does not
+ // perform a handshake. End the handshake, revert to the legacy protocol,
+ // and process the chunk appropriately.
+ case Chunk::Type::kData:
+ case Chunk::Type::kParametersRetransmit:
+ case Chunk::Type::kParametersContinue:
+ // Update the local context's session ID in case it was expecting one to
+ // be assigned by the server.
+ session_id_ = chunk.session_id();
+
+ configured_protocol_version_ = ProtocolVersion::kLegacy;
+ set_transfer_state(TransferState::kWaiting);
+
+ PW_LOG_DEBUG(
+ "Transfer %u tried to start on protocol version %d, but peer only "
+ "supports legacy",
+ id_for_log(),
+ static_cast<int>(desired_protocol_version_));
+
+ if (type() == TransferType::kTransmit) {
+ HandleTransmitChunk(chunk);
+ } else {
+ HandleReceiveChunk(chunk);
+ }
+ break;
+
+ case Chunk::Type::kCompletion:
+ case Chunk::Type::kCompletionAck:
+ PW_CRASH(
+ "Transfer completion packets should be processed by "
+ "HandleChunkEvent()");
+ break;
+ }
+}
+
+void Context::UpdateLocalProtocolConfigurationFromPeer(const Chunk& chunk) {
+ PW_LOG_DEBUG("Negotiating protocol version: ours=%d, theirs=%d",
+ static_cast<int>(desired_protocol_version_),
+ static_cast<int>(chunk.protocol_version()));
+
+ configured_protocol_version_ =
+ std::min(desired_protocol_version_, chunk.protocol_version());
+
+ PW_LOG_INFO("Transfer %u: using protocol version %d",
+ id_for_log(),
+ static_cast<int>(configured_protocol_version_));
+}
+
void Context::HandleTransmitChunk(const Chunk& chunk) {
switch (transfer_state_) {
case TransferState::kInactive:
@@ -278,38 +460,44 @@ void Context::HandleTransmitChunk(const Chunk& chunk) {
SendFinalStatusChunk();
return;
+ case TransferState::kInitiating:
+ PerformInitialHandshake(chunk);
+ return;
+
case TransferState::kWaiting:
case TransferState::kTransmitting:
- HandleTransferParametersUpdate(chunk);
- if (transfer_state_ == TransferState::kCompleted) {
- SendFinalStatusChunk();
+ if (chunk.protocol_version() == configured_protocol_version_) {
+ HandleTransferParametersUpdate(chunk);
+ } else {
+ PW_LOG_ERROR(
+ "Transmit transfer %u was configured to use protocol version %d "
+ "but received a chunk with version %d",
+ id_for_log(),
+ static_cast<int>(configured_protocol_version_),
+ static_cast<int>(chunk.protocol_version()));
+ TerminateTransfer(Status::Internal());
}
return;
+
+ case TransferState::kTerminating:
+ HandleTerminatingChunk(chunk);
+ return;
}
}
void Context::HandleTransferParametersUpdate(const Chunk& chunk) {
- if (!chunk.pending_bytes.has_value()) {
- // Malformed chunk.
- Finish(Status::InvalidArgument());
- return;
- }
-
- bool retransmit = true;
- if (chunk.type.has_value()) {
- retransmit = chunk.type == Chunk::Type::kParametersRetransmit ||
- chunk.type == Chunk::Type::kTransferStart;
- }
+ bool retransmit = chunk.RequestsTransmissionFromOffset();
if (retransmit) {
// If the offsets don't match, attempt to seek on the reader. Not all
// readers support seeking; abort with UNIMPLEMENTED if this handler
// doesn't.
- if (offset_ != chunk.offset) {
- if (Status seek_status = reader().Seek(chunk.offset); !seek_status.ok()) {
+ if (offset_ != chunk.offset()) {
+ if (Status seek_status = reader().Seek(chunk.offset());
+ !seek_status.ok()) {
PW_LOG_WARN("Transfer %u seek to %u failed with status %u",
- static_cast<unsigned>(transfer_id_),
- static_cast<unsigned>(chunk.offset),
+ static_cast<unsigned>(session_id_),
+ static_cast<unsigned>(chunk.offset()),
seek_status.code());
// Remap status codes to return one of the following:
@@ -324,36 +512,31 @@ void Context::HandleTransferParametersUpdate(const Chunk& chunk) {
seek_status = Status::DataLoss();
}
- Finish(seek_status);
+ TerminateTransfer(seek_status);
return;
}
}
- // Retransmit is the default behavior for older versions of the transfer
- // protocol. The window_end_offset field is not guaranteed to be set in
- // these versions, so it must be calculated.
- offset_ = chunk.offset;
- window_end_offset_ = offset_ + chunk.pending_bytes.value();
- pending_bytes_ = chunk.pending_bytes.value();
- } else {
- window_end_offset_ = chunk.window_end_offset;
+ offset_ = chunk.offset();
}
- if (chunk.max_chunk_size_bytes.has_value()) {
- max_chunk_size_bytes_ = std::min(chunk.max_chunk_size_bytes.value(),
+ window_end_offset_ = chunk.window_end_offset();
+
+ if (chunk.max_chunk_size_bytes().has_value()) {
+ max_chunk_size_bytes_ = std::min(chunk.max_chunk_size_bytes().value(),
max_parameters_->max_chunk_size_bytes());
}
- if (chunk.min_delay_microseconds.has_value()) {
+ if (chunk.min_delay_microseconds().has_value()) {
interchunk_delay_ = chrono::SystemClock::for_at_least(
- std::chrono::microseconds(chunk.min_delay_microseconds.value()));
+ std::chrono::microseconds(chunk.min_delay_microseconds().value()));
}
PW_LOG_DEBUG(
"Transfer %u received parameters type=%s offset=%u window_end_offset=%u",
- static_cast<unsigned>(transfer_id_),
+ static_cast<unsigned>(session_id_),
retransmit ? "RETRANSMIT" : "CONTINUE",
- static_cast<unsigned>(chunk.offset),
+ static_cast<unsigned>(chunk.offset()),
static_cast<unsigned>(window_end_offset_));
// Parsed all of the parameters; start sending the window.
@@ -363,23 +546,16 @@ void Context::HandleTransferParametersUpdate(const Chunk& chunk) {
}
void Context::TransmitNextChunk(bool retransmit_requested) {
- ByteSpan buffer = thread_->encode_buffer();
-
- // Begin by doing a partial encode of all the metadata fields, leaving the
- // buffer with usable space for the chunk data at the end.
- transfer::Chunk::MemoryEncoder encoder{buffer};
- encoder.WriteTransferId(transfer_id_).IgnoreError();
- encoder.WriteOffset(offset_).IgnoreError();
-
- // TODO(frolv): Type field presence is currently meaningful, so this type must
- // be serialized. Once all users of transfer always set chunk types, the field
- // can be made non-optional and this write can be removed as TRANSFER_DATA has
- // the default proto value of 0.
- encoder.WriteType(transfer::Chunk::Type::TRANSFER_DATA).IgnoreError();
+ Chunk chunk(configured_protocol_version_, Chunk::Type::kData);
+ chunk.set_session_id(session_id_);
+ chunk.set_offset(offset_);
// Reserve space for the data proto field overhead and use the remainder of
// the buffer for the chunk data.
- size_t reserved_size = encoder.size() + 1 /* data key */ + 5 /* data size */;
+ size_t reserved_size =
+ chunk.EncodedSize() + 1 /* data key */ + 5 /* data size */;
+
+ ByteSpan buffer = thread_->encode_buffer();
ByteSpan data_buffer = buffer.subspan(reserved_size);
size_t max_bytes_to_send =
@@ -392,12 +568,11 @@ void Context::TransmitNextChunk(bool retransmit_requested) {
Result<ByteSpan> data = reader().Read(data_buffer);
if (data.status().IsOutOfRange()) {
// No more data to read.
- encoder.WriteRemainingBytes(0).IgnoreError();
+ chunk.set_remaining_bytes(0);
window_end_offset_ = offset_;
- pending_bytes_ = 0;
PW_LOG_DEBUG("Transfer %u sending final chunk with remaining_bytes=0",
- static_cast<unsigned>(transfer_id_));
+ static_cast<unsigned>(session_id_));
} else if (data.ok()) {
if (offset_ == window_end_offset_) {
if (retransmit_requested) {
@@ -405,7 +580,7 @@ void Context::TransmitNextChunk(bool retransmit_requested) {
"Transfer %u: received an empty retransmit request, but there is "
"still data to send; aborting with RESOURCE_EXHAUSTED",
id_for_log());
- Finish(Status::ResourceExhausted());
+ TerminateTransfer(Status::ResourceExhausted());
} else {
PW_LOG_DEBUG(
"Transfer %u: ignoring continuation packet for transfer window "
@@ -417,37 +592,38 @@ void Context::TransmitNextChunk(bool retransmit_requested) {
}
PW_LOG_DEBUG("Transfer %u sending chunk offset=%u size=%u",
- static_cast<unsigned>(transfer_id_),
+ static_cast<unsigned>(session_id_),
static_cast<unsigned>(offset_),
static_cast<unsigned>(data.value().size()));
- encoder.WriteData(data.value()).IgnoreError();
+ chunk.set_payload(data.value());
last_chunk_offset_ = offset_;
offset_ += data.value().size();
- pending_bytes_ -= data.value().size();
} else {
PW_LOG_ERROR("Transfer %u Read() failed with status %u",
- static_cast<unsigned>(transfer_id_),
+ static_cast<unsigned>(session_id_),
data.status().code());
- Finish(Status::DataLoss());
+ TerminateTransfer(Status::DataLoss());
return;
}
- if (!encoder.status().ok()) {
+ Result<ConstByteSpan> encoded_chunk = chunk.Encode(buffer);
+ if (!encoded_chunk.ok()) {
PW_LOG_ERROR("Transfer %u failed to encode transmit chunk",
- static_cast<unsigned>(transfer_id_));
- Finish(Status::Internal());
+ static_cast<unsigned>(session_id_));
+ TerminateTransfer(Status::Internal());
return;
}
- if (const Status status = rpc_writer_->Write(encoder); !status.ok()) {
+ if (const Status status = rpc_writer_->Write(*encoded_chunk); !status.ok()) {
PW_LOG_ERROR("Transfer %u failed to send transmit chunk, status %u",
- static_cast<unsigned>(transfer_id_),
+ static_cast<unsigned>(session_id_),
status.code());
- Finish(Status::DataLoss());
+ TerminateTransfer(Status::DataLoss());
return;
}
+ last_chunk_sent_ = chunk.type();
flags_ |= kFlagsDataSent;
if (offset_ == window_end_offset_) {
@@ -463,12 +639,28 @@ void Context::TransmitNextChunk(bool retransmit_requested) {
}
void Context::HandleReceiveChunk(const Chunk& chunk) {
+ if (transfer_state_ == TransferState::kInitiating) {
+ PerformInitialHandshake(chunk);
+ return;
+ }
+
+ if (chunk.protocol_version() != configured_protocol_version_) {
+ PW_LOG_ERROR(
+ "Receive transfer %u was configured to use protocol version %d "
+ "but received a chunk with version %d",
+ id_for_log(),
+ static_cast<int>(configured_protocol_version_),
+ static_cast<int>(chunk.protocol_version()));
+ TerminateTransfer(Status::Internal());
+ return;
+ }
+
switch (transfer_state_) {
case TransferState::kInactive:
- PW_CRASH("Never should handle chunk while inactive");
-
case TransferState::kTransmitting:
- PW_CRASH("Receive transfer somehow entered TRANSMITTING state");
+ case TransferState::kInitiating:
+ PW_CRASH("HandleReceiveChunk() called in bad transfer state %d",
+ static_cast<int>(transfer_state_));
case TransferState::kCompleted:
// If the transfer has already completed and another chunk is received,
@@ -482,32 +674,31 @@ void Context::HandleReceiveChunk(const Chunk& chunk) {
return;
case TransferState::kRecovery:
- if (chunk.offset != offset_) {
- if (last_chunk_offset_ == chunk.offset) {
+ if (chunk.offset() != offset_) {
+ if (last_chunk_offset_ == chunk.offset()) {
PW_LOG_DEBUG(
"Transfer %u received repeated offset %u; retry detected, "
"resending transfer parameters",
- static_cast<unsigned>(transfer_id_),
- static_cast<unsigned>(chunk.offset));
+ static_cast<unsigned>(session_id_),
+ static_cast<unsigned>(chunk.offset()));
UpdateAndSendTransferParameters(TransmitAction::kRetransmit);
- if (transfer_state_ == TransferState::kCompleted) {
- SendFinalStatusChunk();
+ if (DataTransferComplete()) {
return;
}
PW_LOG_DEBUG("Transfer %u waiting for offset %u, ignoring %u",
- static_cast<unsigned>(transfer_id_),
+ static_cast<unsigned>(session_id_),
static_cast<unsigned>(offset_),
- static_cast<unsigned>(chunk.offset));
+ static_cast<unsigned>(chunk.offset()));
}
- last_chunk_offset_ = chunk.offset;
+ last_chunk_offset_ = chunk.offset();
SetTimeout(chunk_timeout_);
return;
}
PW_LOG_DEBUG("Transfer %u received expected offset %u, resuming transfer",
- static_cast<unsigned>(transfer_id_),
+ static_cast<unsigned>(session_id_),
static_cast<unsigned>(offset_));
set_transfer_state(TransferState::kWaiting);
@@ -515,35 +706,22 @@ void Context::HandleReceiveChunk(const Chunk& chunk) {
[[fallthrough]];
case TransferState::kWaiting:
HandleReceivedData(chunk);
- if (transfer_state_ == TransferState::kCompleted) {
- SendFinalStatusChunk();
- }
+ return;
+
+ case TransferState::kTerminating:
+ HandleTerminatingChunk(chunk);
return;
}
}
void Context::HandleReceivedData(const Chunk& chunk) {
- if (chunk.data.size() > pending_bytes_) {
- // End the transfer, as this indicates a bug with the client implementation
- // where it doesn't respect pending_bytes. Trying to recover from here
- // could potentially result in an infinite transfer loop.
- PW_LOG_ERROR(
- "Transfer %u received more data than what was requested (%u received "
- "for %u pending); terminating transfer.",
- id_for_log(),
- static_cast<unsigned>(chunk.data.size()),
- static_cast<unsigned>(pending_bytes_));
- Finish(Status::Internal());
- return;
- }
-
- if (chunk.offset != offset_) {
+ if (chunk.offset() != offset_) {
// Bad offset; reset pending_bytes to send another parameters chunk.
PW_LOG_DEBUG(
"Transfer %u expected offset %u, received %u; entering recovery state",
- static_cast<unsigned>(transfer_id_),
+ static_cast<unsigned>(session_id_),
static_cast<unsigned>(offset_),
- static_cast<unsigned>(chunk.offset));
+ static_cast<unsigned>(chunk.offset()));
set_transfer_state(TransferState::kRecovery);
SetTimeout(chunk_timeout_);
@@ -552,48 +730,61 @@ void Context::HandleReceivedData(const Chunk& chunk) {
return;
}
+ if (chunk.offset() + chunk.payload().size() > window_end_offset_) {
+ // End the transfer, as this indicates a bug with the client implementation
+ // where it doesn't respect pending_bytes. Trying to recover from here
+ // could potentially result in an infinite transfer loop.
+ PW_LOG_ERROR(
+ "Transfer %u received more data than what was requested (%u received "
+ "for %u pending); terminating transfer.",
+ id_for_log(),
+ static_cast<unsigned>(chunk.payload().size()),
+ static_cast<unsigned>(window_end_offset_ - offset_));
+ TerminateTransfer(Status::Internal());
+ return;
+ }
+
// Update the last offset seen so that retries can be detected.
- last_chunk_offset_ = chunk.offset;
+ last_chunk_offset_ = chunk.offset();
// Write staged data from the buffer to the stream.
- if (!chunk.data.empty()) {
- if (Status status = writer().Write(chunk.data); !status.ok()) {
+ if (chunk.has_payload()) {
+ if (Status status = writer().Write(chunk.payload()); !status.ok()) {
PW_LOG_ERROR(
"Transfer %u write of %u B chunk failed with status %u; aborting "
"with DATA_LOSS",
- static_cast<unsigned>(transfer_id_),
- static_cast<unsigned>(chunk.data.size()),
+ static_cast<unsigned>(session_id_),
+ static_cast<unsigned>(chunk.payload().size()),
status.code());
- Finish(Status::DataLoss());
+ TerminateTransfer(Status::DataLoss());
return;
}
- transfer_rate_.Update(chunk.data.size());
+ transfer_rate_.Update(chunk.payload().size());
}
// When the client sets remaining_bytes to 0, it indicates completion of the
// transfer. Acknowledge the completion through a status chunk and clean up.
if (chunk.IsFinalTransmitChunk()) {
- Finish(OkStatus());
+ TerminateTransfer(OkStatus());
return;
}
// Update the transfer state.
- offset_ += chunk.data.size();
- pending_bytes_ -= chunk.data.size();
+ offset_ += chunk.payload().size();
- if (chunk.window_end_offset != 0) {
- if (chunk.window_end_offset < offset_) {
+ if (chunk.window_end_offset() != 0) {
+ if (chunk.window_end_offset() < offset_) {
PW_LOG_ERROR(
"Transfer %u got invalid end offset of %u (current offset %u)",
id_for_log(),
- static_cast<unsigned>(chunk.window_end_offset),
+ static_cast<unsigned>(chunk.window_end_offset()),
static_cast<unsigned>(offset_));
- Finish(Status::Internal());
+ TerminateTransfer(Status::Internal());
return;
}
- if (chunk.window_end_offset > window_end_offset_) {
+ if (chunk.window_end_offset() > window_end_offset_) {
// A transmitter should never send a larger end offset than what the
// receiver has advertised. If this occurs, there is a bug in the
// transmitter implementation. Terminate the transfer.
@@ -601,19 +792,18 @@ void Context::HandleReceivedData(const Chunk& chunk) {
"Transfer %u transmitter sent invalid end offset of %u, "
"greater than receiver offset %u",
id_for_log(),
- static_cast<unsigned>(chunk.window_end_offset),
+ static_cast<unsigned>(chunk.window_end_offset()),
static_cast<unsigned>(window_end_offset_));
- Finish(Status::Internal());
+ TerminateTransfer(Status::Internal());
return;
}
- window_end_offset_ = chunk.window_end_offset;
- pending_bytes_ = chunk.window_end_offset - offset_;
+ window_end_offset_ = chunk.window_end_offset();
}
SetTimeout(chunk_timeout_);
- if (pending_bytes_ == 0u) {
+ if (offset_ == window_end_offset_) {
// Received all pending data. Advance the transfer parameters.
UpdateAndSendTransferParameters(TransmitAction::kRetransmit);
return;
@@ -631,32 +821,99 @@ void Context::HandleReceivedData(const Chunk& chunk) {
}
}
-void Context::SendFinalStatusChunk() {
- PW_DCHECK(transfer_state_ == TransferState::kCompleted);
+void Context::HandleTerminatingChunk(const Chunk& chunk) {
+ switch (chunk.type()) {
+ case Chunk::Type::kCompletion:
+ PW_CRASH("Completion chunks should be processed by HandleChunkEvent()");
+
+ case Chunk::Type::kCompletionAck:
+ PW_LOG_INFO(
+ "Transfer %u completed with status %u", id_for_log(), status_.code());
+ set_transfer_state(TransferState::kInactive);
+ break;
+
+ case Chunk::Type::kData:
+ case Chunk::Type::kStart:
+ case Chunk::Type::kParametersRetransmit:
+ case Chunk::Type::kParametersContinue:
+ case Chunk::Type::kStartAck:
+ case Chunk::Type::kStartAckConfirmation:
+ // If a non-completion chunk is received in a TERMINATING state, re-send
+ // the transfer's completion chunk to the peer.
+ EncodeAndSendChunk(
+ Chunk::Final(configured_protocol_version_, session_id_, status_));
+ break;
+ }
+}
+
+void Context::TerminateTransfer(Status status, bool with_resource_id) {
+ if (transfer_state_ == TransferState::kTerminating ||
+ transfer_state_ == TransferState::kCompleted) {
+ // Transfer has already been terminated; no need to do it again.
+ return;
+ }
+
+ Finish(status);
+
+ PW_LOG_INFO("Transfer %u terminating with status %u",
+ static_cast<unsigned>(session_id_),
+ status.code());
+
+ if (ShouldSkipCompletionHandshake()) {
+ set_transfer_state(TransferState::kCompleted);
+ } else {
+ set_transfer_state(TransferState::kTerminating);
+ SetTimeout(chunk_timeout_);
+ }
+
+ // Don't send a final chunk if the other end of the transfer has not yet
+ // made contact, as there is no one to notify.
+ if ((flags_ & kFlagsContactMade) == kFlagsContactMade) {
+ SendFinalStatusChunk(with_resource_id);
+ }
+}
+
+void Context::HandleTermination(Status status) {
+ Finish(status);
+
+ PW_LOG_INFO("Transfer %u completed with status %u",
+ static_cast<unsigned>(session_id_),
+ status.code());
+
+ if (ShouldSkipCompletionHandshake()) {
+ set_transfer_state(TransferState::kCompleted);
+ } else {
+ EncodeAndSendChunk(
+ Chunk(configured_protocol_version_, Chunk::Type::kCompletionAck)
+ .set_session_id(session_id_));
+
+ set_transfer_state(TransferState::kInactive);
+ }
+}
- internal::Chunk chunk = {};
- chunk.transfer_id = transfer_id_;
- chunk.status = status_.code();
- chunk.type = Chunk::Type::kTransferCompletion;
+void Context::SendFinalStatusChunk(bool with_resource_id) {
+ PW_DCHECK(transfer_state_ == TransferState::kCompleted ||
+ transfer_state_ == TransferState::kTerminating);
PW_LOG_DEBUG("Sending final chunk for transfer %u with status %u",
- static_cast<unsigned>(transfer_id_),
+ static_cast<unsigned>(session_id_),
status_.code());
+
+ Chunk chunk =
+ Chunk::Final(configured_protocol_version_, session_id_, status_);
+ if (with_resource_id) {
+ chunk.set_resource_id(resource_id_);
+ }
EncodeAndSendChunk(chunk);
}
void Context::Finish(Status status) {
PW_DCHECK(active());
- PW_LOG_INFO("Transfer %u completed with status %u",
- static_cast<unsigned>(transfer_id_),
- status.code());
-
status.Update(FinalCleanup(status));
+ status_ = status;
- set_transfer_state(TransferState::kCompleted);
SetTimeout(kFinalChunkAckTimeout);
- status_ = status;
}
void Context::SetTimeout(chrono::SystemClock::duration timeout) {
@@ -680,12 +937,14 @@ void Context::HandleTimeout() {
TransmitNextChunk(/*retransmit_requested=*/false);
break;
+ case TransferState::kInitiating:
case TransferState::kWaiting:
case TransferState::kRecovery:
- // A timeout occurring in a WAITING or RECOVERY state indicates that no
+ case TransferState::kTerminating:
+ // A timeout occurring in a transfer or handshake state indicates that no
// chunk has been received from the other side. The transfer should retry
// its previous operation.
- SetTimeout(chunk_timeout_); // Finish() clears the timeout if retry fails
+ SetTimeout(chunk_timeout_); // Retry() clears the timeout if it fails
Retry();
break;
@@ -693,29 +952,47 @@ void Context::HandleTimeout() {
PW_LOG_ERROR("Timeout occurred in INACTIVE state");
return;
}
-
- if (transfer_state_ == TransferState::kCompleted) {
- SendFinalStatusChunk();
- }
}
void Context::Retry() {
- if (retries_ == max_retries_) {
- PW_LOG_ERROR("Transfer %u failed to receive a chunk after %u retries.",
- static_cast<unsigned>(transfer_id_),
- static_cast<unsigned>(retries_));
+ if (retries_ == max_retries_ || lifetime_retries_ == max_lifetime_retries_) {
+ PW_LOG_ERROR(
+ "Transfer %u failed to receive a chunk after %u retries (lifetime %u).",
+ id_for_log(),
+ static_cast<unsigned>(retries_),
+ static_cast<unsigned>(lifetime_retries_));
PW_LOG_ERROR("Canceling transfer.");
- Finish(Status::DeadlineExceeded());
+
+ if (transfer_state_ == TransferState::kTerminating) {
+ // Timeouts occurring in a TERMINATING state indicate that the completion
+ // chunk was never ACKed. Simply clean up the transfer context.
+ set_transfer_state(TransferState::kInactive);
+ } else {
+ TerminateTransfer(Status::DeadlineExceeded());
+ }
return;
}
++retries_;
+ ++lifetime_retries_;
+
+ if (transfer_state_ == TransferState::kInitiating ||
+ last_chunk_sent_ == Chunk::Type::kStartAckConfirmation) {
+ RetryHandshake();
+ return;
+ }
+
+ if (transfer_state_ == TransferState::kTerminating) {
+ EncodeAndSendChunk(
+ Chunk::Final(configured_protocol_version_, session_id_, status_));
+ return;
+ }
if (type() == TransferType::kReceive) {
// Resend the most recent transfer parameters.
PW_LOG_DEBUG(
"Receive transfer %u timed out waiting for chunk; resending parameters",
- static_cast<unsigned>(transfer_id_));
+ static_cast<unsigned>(session_id_));
SendTransferParameters(TransmitAction::kRetransmit);
return;
@@ -726,7 +1003,7 @@ void Context::Retry() {
if ((flags_ & kFlagsDataSent) != kFlagsDataSent) {
PW_LOG_DEBUG(
"Transmit transfer %u timed out waiting for initial parameters",
- static_cast<unsigned>(transfer_id_));
+ static_cast<unsigned>(session_id_));
SendInitialTransmitChunk();
return;
}
@@ -734,21 +1011,56 @@ void Context::Retry() {
// Otherwise, resend the most recent chunk. If the reader doesn't support
// seeking, this isn't possible, so just terminate the transfer immediately.
if (!reader().Seek(last_chunk_offset_).ok()) {
- PW_LOG_ERROR("Transmit transfer %d timed out waiting for new parameters.",
- static_cast<unsigned>(transfer_id_));
+ PW_LOG_ERROR("Transmit transfer %u timed out waiting for new parameters.",
+ id_for_log());
PW_LOG_ERROR("Retrying requires a seekable reader. Alas, ours is not.");
- Finish(Status::DeadlineExceeded());
+ TerminateTransfer(Status::DeadlineExceeded());
return;
}
// Rewind the transfer position and resend the chunk.
- size_t last_size_sent = offset_ - last_chunk_offset_;
offset_ = last_chunk_offset_;
- pending_bytes_ += last_size_sent;
TransmitNextChunk(/*retransmit_requested=*/false);
}
+void Context::RetryHandshake() {
+ Chunk retry_chunk(configured_protocol_version_, last_chunk_sent_);
+
+ switch (last_chunk_sent_) {
+ case Chunk::Type::kStart:
+ // No protocol version is yet configured at the time of sending the start
+ // chunk, so we use the client's desired version instead.
+ retry_chunk.set_protocol_version(desired_protocol_version_)
+ .set_resource_id(resource_id_);
+ if (type() == TransferType::kReceive) {
+ SetTransferParameters(retry_chunk);
+ }
+ break;
+
+ case Chunk::Type::kStartAck:
+ retry_chunk.set_session_id(session_id_)
+ .set_resource_id(static_cast<ServerContext&>(*this).handler()->id());
+ break;
+
+ case Chunk::Type::kStartAckConfirmation:
+ retry_chunk.set_session_id(session_id_);
+ if (type() == TransferType::kReceive) {
+ SetTransferParameters(retry_chunk);
+ }
+ break;
+
+ case Chunk::Type::kData:
+ case Chunk::Type::kParametersRetransmit:
+ case Chunk::Type::kParametersContinue:
+ case Chunk::Type::kCompletion:
+ case Chunk::Type::kCompletionAck:
+ PW_CRASH("Should not RetryHandshake() when not in handshake phase");
+ }
+
+ EncodeAndSendChunk(retry_chunk);
+}
+
uint32_t Context::MaxWriteChunkSize(uint32_t max_chunk_size_bytes,
uint32_t channel_id) const {
// Start with the user-provided maximum chunk size, which should be the usable
@@ -778,14 +1090,14 @@ uint32_t Context::MaxWriteChunkSize(uint32_t max_chunk_size_bytes,
// Subtract the transfer service overhead for a client write chunk
// (pw_transfer/transfer.proto).
//
- // transfer_id: 1 byte key, varint value (calculate)
- // offset: 1 byte key, varint value (calculate)
- // data: 1 byte key, varint length (remaining space)
+ // session_id: 1 byte key, varint value (calculate)
+ // offset: 1 byte key, varint value (calculate)
+ // data: 1 byte key, varint length (remaining space)
//
- // TOTAL: 3 + encoded transfer_id + encoded offset + encoded data length
+ // TOTAL: 3 + encoded session_id + encoded offset + encoded data length
//
max_size -= 3;
- max_size -= varint::EncodedSize(transfer_id_);
+ max_size -= varint::EncodedSize(session_id_);
max_size -= varint::EncodedSize(window_end_offset_);
max_size -= varint::EncodedSize(max_size);
@@ -801,6 +1113,23 @@ uint32_t Context::MaxWriteChunkSize(uint32_t max_chunk_size_bytes,
return max_size;
}
-} // namespace pw::transfer::internal
+void Context::LogTransferConfiguration() {
+ PW_LOG_DEBUG(
+ "Local transfer timing configuration: "
+ "chunk_timeout=%ums, max_retries=%u, interchunk_delay=%uus",
+ static_cast<unsigned>(
+ std::chrono::ceil<std::chrono::milliseconds>(chunk_timeout_).count()),
+ static_cast<unsigned>(max_retries_),
+ static_cast<unsigned>(
+ std::chrono::ceil<std::chrono::microseconds>(interchunk_delay_)
+ .count()));
-PW_MODIFY_DIAGNOSTICS_POP();
+ PW_LOG_DEBUG(
+ "Local transfer windowing configuration: "
+ "pending_bytes=%u, extend_window_divisor=%u, max_chunk_size_bytes=%u",
+ static_cast<unsigned>(max_parameters_->pending_bytes()),
+ static_cast<unsigned>(max_parameters_->extend_window_divisor()),
+ static_cast<unsigned>(max_parameters_->max_chunk_size_bytes()));
+}
+
+} // namespace pw::transfer::internal
diff --git a/pw_transfer/docs.rst b/pw_transfer/docs.rst
index b0a068b58..01e1b30f6 100644
--- a/pw_transfer/docs.rst
+++ b/pw_transfer/docs.rst
@@ -3,6 +3,8 @@
===========
pw_transfer
===========
+``pw_transfer`` is a reliable data transfer protocol which runs on top of
+Pigweed RPC.
.. attention::
@@ -14,33 +16,112 @@ Usage
C++
===
-The transfer service is defined and registered with an RPC server like any other
-RPC service.
-To know how to read data from or write data to device, a ``TransferHandler``
-interface is defined (``pw_transfer/public/pw_transfer/handler.h``). Transfer
-handlers wrap a stream reader and/or writer with initialization and completion
-code. Custom transfer handler implementations should derive from
-``ReadOnlyHandler``, ``WriteOnlyHandler``, or ``ReadWriteHandler`` as
-appropriate and override Prepare and Finalize methods if necessary.
+Transfer thread
+---------------
+To run transfers as either a client or server (or both), a dedicated thread is
+required. The transfer thread is used to process all transfer-related events
+safely. The same transfer thread can be shared by a transfer client and service
+running on the same system.
-A transfer handler should be implemented and instantiated for each unique data
-transfer to or from a device. These handlers are then registered with the
-transfer service using their transfer IDs.
+.. note::
-**Example**
+ All user-defined transfer callbacks (i.e. the virtual interface of a
+ ``Handler`` or completion function in a transfer client) will be
+ invoked from the transfer thread's context.
+
+In order to operate, a transfer thread requires two buffers:
+
+- The first is a *chunk buffer*. This is used to stage transfer packets received
+ by the RPC system to be processed by the transfer thread. It must be large
+ enough to store the largest possible chunk the system supports.
+
+- The second is an *encode buffer*. This is used by the transfer thread to
+ encode outgoing RPC packets. It is necessarily larger than the chunk buffer.
+ Typically, this is sized to the system's maximum transmission unit at the
+ transport layer.
+
+A transfer thread is created by instantiating a ``pw::transfer::Thread``. This
+class derives from ``pw::thread::ThreadCore``, allowing it to directly be used
+when creating a system thread. Refer to :ref:`module-pw_thread-thread-creation`
+for additional information.
+
+**Example thread configuration**
.. code-block:: cpp
- #include "pw_transfer/transfer.h"
+ #include "pw_transfer/transfer_thread.h"
- namespace {
+ namespace {
+
+ // The maximum number of concurrent transfers the thread should support as
+ // either a client or a server. These can be set to 0 (if only using one or
+ // the other).
+ constexpr size_t kMaxConcurrentClientTransfers = 5;
+ constexpr size_t kMaxConcurrentServerTransfers = 3;
+
+ // The maximum payload size that can be transmitted by the system's
+ // transport stack. This would typically be defined within some transport
+ // header.
+ constexpr size_t kMaxTransmissionUnit = 512;
+
+ // The maximum amount of data that should be sent within a single transfer
+ // packet. By necessity, this should be less than the max transmission unit.
+ //
+ // pw_transfer requires some additional per-packet overhead, so the actual
+ // amount of data it sends may be lower than this.
+ constexpr size_t kMaxTransferChunkSizeBytes = 480;
+
+ // Buffers for storing and encoding chunks (see documentation above).
+ std::array<std::byte, kMaxTransferChunkSizeBytes> chunk_buffer;
+ std::array<std::byte, kMaxTransmissionUnit> encode_buffer;
+
+ pw::transfer::Thread<kMaxConcurrentClientTransfers,
+ kMaxConcurrentServerTransfers>
+ transfer_thread(chunk_buffer, encode_buffer);
+
+ } // namespace
+
+ // pw::transfer::TransferThread is the generic, non-templated version of the
+ // Thread class. A Thread can implicitly convert to a TransferThread.
+ pw::transfer::TransferThread& GetSystemTransferThread() {
+ return transfer_thread;
+ }
+
+
+Transfer server
+---------------
+``pw_transfer`` provides an RPC service for running transfers through an RPC
+server.
+
+To know how to read data from or write data to device, a ``Handler`` interface
+is defined (``pw_transfer/public/pw_transfer/handler.h``). Transfer handlers
+represent a transferable resource, wrapping a stream reader and/or writer with
+initialization and completion code. Custom transfer handler implementations
+should derive from ``ReadOnlyHandler``, ``WriteOnlyHandler``, or
+``ReadWriteHandler`` as appropriate and override Prepare and Finalize methods
+if necessary.
+
+A transfer handler should be implemented and instantiated for each unique
+resource that can be transferred to or from a device. Each instantiated handler
+must have a globally-unique integer ID used to identify the resource.
+
+Handlers are registered with the transfer service. This may be done during
+system initialization (for static resources), or dynamically at runtime to
+support ephemeral transfer resources.
+
+**Example transfer handler implementation**
- // Simple transfer handler which reads data from an in-memory buffer.
+.. code-block:: cpp
+
+ #include "pw_stream/memory_stream.h"
+ #include "pw_transfer/transfer.h"
+
+ // A simple transfer handler which reads data from an in-memory buffer.
class SimpleBufferReadHandler : public pw::transfer::ReadOnlyHandler {
public:
- SimpleReadTransfer(uint32_t transfer_id, pw::ConstByteSpan data)
- : ReadOnlyHandler(transfer_id), reader_(data) {
+ SimpleReadTransfer(uint32_t resource_id, pw::ConstByteSpan data)
+ : ReadOnlyHandler(resource_id), reader_(data) {
set_reader(reader_);
}
@@ -48,35 +129,119 @@ transfer service using their transfer IDs.
pw::stream::MemoryReader reader_;
};
- // The maximum amount of data that can be sent in a single chunk, excluding
- // transport layer overhead.
- constexpr size_t kMaxChunkSizeBytes = 256;
+The transfer service is instantiated with a reference to the system's transfer
+thread and registered with the system's RPC server.
+
+**Example transfer service initialization**
+
+.. code-block:: cpp
+
+ #include "pw_transfer/transfer.h"
- // In a write transfer, the maximum number of bytes to receive at one time,
+ namespace {
+
+ // In a write transfer, the maximum number of bytes to receive at one time
// (potentially across multiple chunks), unless specified otherwise by the
// transfer handler's stream::Writer.
constexpr size_t kDefaultMaxBytesToReceive = 1024;
- // Instantiate a static transfer service.
- // The service requires a work queue, and a buffer to store data from a chunk.
- // The helper class TransferServiceBuffer comes with a builtin buffer.
- pw::transfer::TransferServiceBuffer<kMaxChunkSizeBytes> transfer_service(
- GetSystemWorkQueue(), kDefaultMaxBytesToReceive);
+ pw::transfer::TransferService transfer_service(
+ GetSystemTransferThread(), kDefaultMaxBytesToReceive);
- // Instantiate a handler for the data to be transferred.
- constexpr uint32_t kBufferTransferId = 1;
- char buffer_to_transfer[256] = { /* ... */ };
- SimpleBufferReadHandler buffer_handler(kBufferTransferId, buffer_to_transfer);
+ // Instantiate a handler for the data to be transferred. The resource ID will
+ // be used by the transfer client and server to identify the handler.
+ constexpr uint32_t kMagicBufferResourceId = 1;
+ char magic_buffer_to_transfer[256] = { /* ... */ };
+ SimpleBufferReadHandler magic_buffer_handler(
+ kMagicBufferResourceId, magic_buffer_to_transfer);
} // namespace
- void InitTransfer() {
+ void InitTransferService() {
// Register the handler with the transfer service, then the transfer service
// with an RPC server.
- transfer_service.RegisterHandler(buffer_handler);
+ transfer_service.RegisterHandler(magic_buffer_handler);
GetSystemRpcServer().RegisterService(transfer_service);
}
+Transfer client
+---------------
+``pw_transfer`` provides a transfer client capable of running transfers through
+an RPC client.
+
+.. note::
+
+ Currently, a transfer client is only capable of running transfers on a single
+ RPC channel. This may be expanded in the future.
+
+The transfer client provides the following two APIs for starting data transfers:
+
+.. cpp:function:: pw::Status pw::transfer::Client::Read(uint32_t resource_id, pw::stream::Writer& output, CompletionFunc&& on_completion, pw::chrono::SystemClock::duration timeout = cfg::kDefaultChunkTimeout, pw::transfer::ProtocolVersion version = kDefaultProtocolVersion)
+
+ Reads data from a transfer server to the specified ``pw::stream::Writer``.
+ Invokes the provided callback function with the overall status of the
+ transfer.
+
+ Due to the asynchronous nature of transfer operations, this function will only
+ return a non-OK status if it is called with bad arguments. Otherwise, it will
+ return OK and errors will be reported through the completion callback.
+
+.. cpp:function:: pw::Status pw::transfer::Client::Write(uint32_t resource_id, pw::stream::Reader& input, CompletionFunc&& on_completion, pw::chrono::SystemClock::duration timeout = cfg::kDefaultChunkTimeout, pw::transfer::ProtocolVersion version = kDefaultProtocolVersion)
+
+ Writes data from a source ``pw::stream::Reader`` to a transfer server.
+ Invokes the provided callback function with the overall status of the
+ transfer.
+
+ Due to the asynchronous nature of transfer operations, this function will only
+ return a non-OK status if it is called with bad arguments. Otherwise, it will
+ return OK and errors will be reported through the completion callback.
+
+**Example client setup**
+
+.. code-block:: cpp
+
+ #include "pw_transfer/client.h"
+
+ namespace {
+
+ // RPC channel on which transfers should be run.
+ constexpr uint32_t kChannelId = 42;
+
+ pw::transfer::Client transfer_client(
+ GetSystemRpcClient(), kChannelId, GetSystemTransferThread());
+
+ } // namespace
+
+ Status ReadMagicBufferSync(pw::ByteSpan sink) {
+ pw::stream::Writer writer(sink);
+
+ struct {
+ pw::sync::ThreadNotification notification;
+ pw::Status status;
+ } transfer_state;
+
+ transfer_client.Read(
+ kMagicBufferResourceId,
+ writer,
+ [&transfer_state](pw::Status status) {
+ transfer_state.status = status;
+ transfer_state.notification.release();
+ });
+
+ // Block until the transfer completes.
+ transfer_state.notification.acquire();
+ return transfer_state.status;
+ }
+
+Atomic File Transfer Handler
+----------------------------
+Transfers are handled using the generic `Handler` interface. A specialized
+`Handler`, `AtomicFileTransferHandler` is available to handle file transfers
+with atomic semantics. It guarantees that the target file of the transfer is
+always in a correct state. A temporary file is written to prior to updating the
+target file. If any transfer failure occurs, the transfer is aborted and the
+target file is either not created or not updated.
+
Module Configuration Options
----------------------------
The following configurations can be adjusted via compile-time configuration of
@@ -89,6 +254,15 @@ more details.
The default maximum number of times a transfer should retry sending a chunk
when no response is received. This can later be configured per-transfer.
+.. c:macro:: PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES
+
+ The default maximum number of times a transfer should retry sending any chunk
+ over the course of its entire lifetime.
+
+ This number should be high, particularly if long-running transfers are
+ expected. Its purpose is to prevent transfers from getting stuck in an
+ infinite loop.
+
.. c:macro:: PW_TRANSFER_DEFAULT_TIMEOUT_MS
The default amount of time, in milliseconds, to wait for a chunk to arrive
@@ -107,7 +281,7 @@ more details.
Python
======
.. automodule:: pw_transfer
- :members: ProgressStats, Manager, Error
+ :members: ProgressStats, ProtocolVersion, Manager, Error
**Example**
@@ -123,7 +297,7 @@ Python
transfer_manager = pw_transfer.Manager(transfer_service)
try:
- # Read transfer_id 3 from the server.
+ # Read the transfer resource with ID 3 from the server.
data = transfer_manager.read(3)
except pw_transfer.Error as err:
print('Failed to read:', err.status)
@@ -137,42 +311,144 @@ Python
Typescript
==========
-
Provides a simple interface for transferring bulk data over pw_rpc.
**Example**
.. code-block:: typescript
- import {Manager} from '@pigweed/pw_transfer'
+ import { pw_transfer } from 'pigweedjs';
+ const { Manager } from pw_transfer;
+
+ const client = new CustomRpcClient();
+ service = client.channel()!.service('pw.transfer.Transfer')!;
+
+ const manager = new Manager(service, DEFAULT_TIMEOUT_S);
+
+ manager.read(3, (stats: ProgressStats) => {
+ console.log(`Progress Update: ${stats}`);
+ }).then((data: Uint8Array) => {
+ console.log(`Completed read: ${data}`);
+ }).catch(error => {
+ console.log(`Failed to read: ${error.status}`);
+ });
+
+ manager.write(2, textEncoder.encode('hello world'))
+ .catch(error => {
+ console.log(`Failed to read: ${error.status}`);
+ });
+
+Java
+====
+pw_transfer provides a Java client. The transfer client returns a
+`ListenableFuture <https://guava.dev/releases/21.0/api/docs/com/google/common/util/concurrent/ListenableFuture>`_
+to represent the results of a read or write transfer.
+
+.. code-block:: java
- const client = new CustomRpcClient();
- service = client.channel()!.service('pw.transfer.Transfer')!;
+ import dev.pigweed.pw_transfer.TransferClient;
- const manager = new Manager(service, DEFAULT_TIMEOUT_S);
+ public class TheClass {
+ public void DoTransfer(MethodClient transferReadMethodClient,
+ MethodClient transferWriteMethodClient) {
+ // Create a new transfer client.
+ TransferClient client = new TransferClient(
+ transferReadMethodClient,
+ transferWriteMethodClient,
+ TransferTimeoutSettings.builder()
+ .setTimeoutMillis(TRANSFER_TIMEOUT_MS)
+ .setMaxRetries(MAX_RETRIES)
+ .build());
- manager.read(3, (stats: ProgressStats) => {
- console.log(`Progress Update: ${stats}`);
- }).then((data: Uint8Array) => {
- console.log(`Completed read: ${data}`);
- }).catch(error => {
- console.log(`Failed to read: ${error.status}`);
- });
+ // Start a read transfer.
+ ListenableFuture<byte[]> readTransfer = client.read(123);
- manager.write(2, textEncoder.encode('hello world'))
- .catch(error => {
- console.log(`Failed to read: ${error.status}`);
- });
+ // Start a write transfer.
+ ListenableFuture<Void> writeTransfer = client.write(123, dataToWrite);
+
+ // Get the data from the read transfer.
+ byte[] readData = readTransfer.get();
+
+ // Wait for the write transfer to complete.
+ writeTransfer.get();
+ }
+ }
--------
Protocol
--------
-Protocol buffer definition
-==========================
-.. literalinclude:: transfer.proto
- :language: protobuf
- :lines: 14-
+Chunks
+======
+Transfers run as a series of *chunks* exchanged over an RPC stream. Chunks can
+contain transferable data, metadata, and control parameters. Each chunk has an
+associated type, which determines what information it holds and the semantics of
+its fields.
+
+The chunk is a protobuf message, whose definition can be found
+:ref:`here <module-pw_transfer-proto-definition>`.
+
+Resources and sessions
+======================
+Transfers are run for a specific *resource* --- a stream of data which can be
+read from or written to. Resources have a system-specific integral identifier
+defined by the implementers of the server-side transfer node.
+
+The series of chunks exchanged in an individual transfer operation for a
+resource constitute a transfer *session*. The session runs from its opening
+chunk until either a terminating chunk is received or the transfer times out.
+Sessions are assigned unique IDs by the transfer server in response to an
+initiating chunk from the client.
+
+Reliability
+===========
+``pw_transfer`` attempts to be a reliable data transfer protocol.
+
+As Pigweed RPC is considered an unreliable communications system,
+``pw_transfer`` implements its own mechanisms for reliability. These include
+timeouts, data retransmissions, and handshakes.
+
+.. note::
+
+ A transfer can only be reliable if its underlying data stream is seekable.
+ A non-seekable stream could prematurely terminate a transfer following a
+ packet drop.
+
+Opening handshake
+=================
+Transfers begin with a three-way handshake, whose purpose is to identify the
+resource being transferred, assign a session ID, and synchronize the protocol
+version to use.
+
+A read or write transfer for a resource is initiated by a transfer client. The
+client sends the ID of the resource to the server in a ``START`` chunk,
+indicating that it wishes to begin a new transfer. This chunk additionally
+encodes the protocol version which the client is configured to use.
+
+Upon receiving a ``START`` chunk, the transfer server checks whether the
+requested resource is available. If so, it prepares the resource for the
+operation, which typically involves opening a data stream, alongside any
+additional user-specified setup. The server generates a session ID, then
+responds to the client with a ``START_ACK`` chunk containing the resource,
+session, and configured protocol version for the transfer.
+
+Transfer completion
+===================
+Either side of a transfer can terminate the operation at any time by sending a
+``COMPLETION`` chunk containing the final status of the transfer. When a
+``COMPLETION`` chunk is sent, the terminator of the transfer performs local
+cleanup, then waits for its peer to acknowledge the completion.
+
+Upon receving a ``COMPLETION`` chunk, the transfer peer cancels any pending
+operations, runs its set of cleanups, and responds with a ``COMPLETION_ACK``,
+fully ending the session from the peer's side.
+
+The terminator's session remains active waiting for a ``COMPLETION_ACK``. If not
+received after a timeout, it re-sends its ``COMPLETION`` chunk. The session ends
+either following receipt of the acknowledgement or if a maximum number of
+retries is hit.
+
+.. _module-pw_transfer-proto-definition:
Server to client transfer (read)
================================
@@ -182,15 +458,17 @@ Client to server transfer (write)
=================================
.. image:: write.svg
+Protocol buffer definition
+==========================
+.. literalinclude:: transfer.proto
+ :language: protobuf
+ :lines: 14-
+
Errors
======
Protocol errors
---------------
-At any point, either the client or server may terminate the transfer with a
-status code. The transfer chunk with the status is the final chunk of the
-transfer.
-
The following table describes the meaning of each status code when sent by the
sender or the receiver (see `Transfer roles`_).
@@ -242,23 +520,6 @@ sender or the receiver (see `Transfer roles`_).
.. cpp:namespace-pop::
-Client errors
--------------
-``pw_transfer`` clients may immediately return certain errors if they cannot
-start a transfer.
-
-.. list-table::
-
- * - **Status**
- - **Reason**
- * - ``ALREADY_EXISTS``
- - A transfer with the requested ID is already pending on this client.
- * - ``DATA_LOSS``
- - Sending the initial transfer chunk failed.
- * - ``RESOURCE_EXHAUSTED``
- - The client has insufficient resources to start an additional transfer at
- this time.
-
Transfer roles
==============
@@ -319,3 +580,98 @@ Receiver flow
signal_completion[Signal completion]-->done
done([Transfer complete])
+
+Legacy protocol
+===============
+``pw_transfer`` was initially released into production prior to several of the
+reliability improvements of its modern protocol. As a result of this, transfer
+implementations support a "legacy" protocol mode, in which transfers run without
+utilizing these features.
+
+The primary differences between the legacy and modern protocols are listed
+below.
+
+- There is no distinction between a transfer resource and session --- a single
+ ``transfer_id`` field represents both. Only one transfer for a given resource
+ can run at a time, and it is not possible to determine where one transfer for
+ a resource ends and the next begins.
+- The legacy protocol has no opening handshake phase. The client initiates with
+ a transfer ID and starting transfer parameters (during a read), and the data
+ transfer phase begins immediately.
+- The legacy protocol has no terminating handshake phase. When either end
+ completes a transfer by sending a status chunk, it does not wait for the peer
+ to acknowledge. Resources used by the transfer are immediately freed, and
+ there is no guarantee that the peer is notified of completion.
+
+Transfer clients request the latest transfer protocol version by default, but
+may be configured to request the legacy protocol. Transfer server and client
+implementations detect if their transfer peer is running the legacy protocol and
+automatically switch to it if required, even if they requested a newer protocol
+version. It is **strongly** unadvised to use the legacy protocol in new code.
+
+-----------------
+Integration tests
+-----------------
+The ``pw_transfer`` module has a set of integration tests that verify the
+correctness of implementations in different languages.
+`Test source code <https://cs.pigweed.dev/pigweed/+/main:pw_transfer/integration_test/>`_.
+
+To run the tests on your machine, run
+
+.. code:: bash
+
+ $ bazel test --features=c++17 \
+ pw_transfer/integration_test:cross_language_small_test \
+ pw_transfer/integration_test:cross_language_medium_test
+
+.. note:: There is a large test that tests transfers that are megabytes in size.
+ These are not run automatically, but can be run manually via the
+ pw_transfer/integration_test:cross_language_large_test test. These are VERY
+ slow, but exist for manual validation of real-world use cases.
+
+The integration tests permit injection of client/server/proxy binaries to use
+when running the tests. This allows manual testing of older versions of
+pw_transfer against newer versions.
+
+.. code:: bash
+
+ # Test a newer version of pw_transfer against an old C++ client that was
+ # backed up to another directory.
+ $ bazel run pw_transfer/integration_test:cross_language_medium_test -- \
+ --cpp-client-binary ../old_pw_transfer_version/cpp_client
+
+Backwards compatibility tests
+=============================
+``pw_transfer`` includes a `suite of backwards-compatibility tests
+<https://cs.pigweed.dev/pigweed/+/main:pw_transfer/integration_test/legacy_binaries_test.py>`_
+that are intended to continuously validate a degree of backwards-compatibility
+with older pw_transfer servers and clients. This is done by retrieving older
+binaries hosted in CIPD and running tests between the older client/server
+binaries and the latest binaries.
+
+The CIPD package contents can be created with this command:
+
+.. code::bash
+
+ $ bazel build --features=c++17 pw_transfer/integration_test:server \
+ pw_transfer/integration_test:cpp_client
+ $ mkdir pw_transfer_test_binaries
+ $ cp bazel-bin/pw_transfer/integration_test/server \
+ pw_transfer_test_binaries
+ $ cp bazel-bin/pw_transfer/integration_test/cpp_client \
+ pw_transfer_test_binaries
+
+To update the CIPD package itself, follow the `internal documentation for
+updating a CIPD package <go/pigweed-cipd#installing-packages-into-cipd>`_.
+
+CI/CQ integration
+=================
+`Current status of the test in CI <https://ci.chromium.org/p/pigweed/builders/ci/pigweed-integration-transfer>`_.
+
+By default, these tests are not run in CQ (on presubmit) because they are too
+slow. However, you can request that the tests be run in presubmit on your
+change by adding to following line to the commit message footer:
+
+.. code::
+
+ Cq-Include-Trybots: luci.pigweed.try:pigweed-integration-transfer
diff --git a/pw_transfer/integration_test.cc b/pw_transfer/integration_test.cc
deleted file mode 100644
index 3f6f829e4..000000000
--- a/pw_transfer/integration_test.cc
+++ /dev/null
@@ -1,258 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// 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
-//
-// https://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 <algorithm>
-#include <chrono>
-#include <filesystem>
-#include <fstream>
-#include <string>
-#include <thread>
-
-#include "gtest/gtest.h"
-#include "pw_assert/check.h"
-#include "pw_bytes/array.h"
-#include "pw_log/log.h"
-#include "pw_rpc/integration_testing.h"
-#include "pw_status/status.h"
-#include "pw_sync/binary_semaphore.h"
-#include "pw_thread_stl/options.h"
-#include "pw_transfer/client.h"
-#include "pw_transfer_test/test_server.raw_rpc.pb.h"
-
-namespace pw::transfer {
-namespace {
-
-using namespace std::chrono_literals;
-
-constexpr int kIterations = 5;
-
-constexpr auto kData512 = bytes::Initialized<512>([](size_t i) { return i; });
-constexpr auto kData8192 = bytes::Initialized<8192>([](size_t i) { return i; });
-constexpr auto kDataHdlcEscape = bytes::Initialized<8192>(0x7e);
-
-std::filesystem::path directory;
-
-// Reads the file that represents the transfer with the specific ID.
-std::string GetContent(uint32_t transfer_id) {
- std::ifstream stream(directory / std::to_string(transfer_id),
- std::ios::binary | std::ios::ate);
- std::string contents(stream.tellg(), '\0');
-
- stream.seekg(0, std::ios::beg);
- PW_CHECK(stream.read(contents.data(), contents.size()));
-
- return contents;
-}
-
-// Drops the null terminator from a string literal.
-template <size_t kLengthWithNull>
-ConstByteSpan AsByteSpan(const char (&data)[kLengthWithNull]) {
- constexpr size_t kLength = kLengthWithNull - 1;
- PW_CHECK_INT_EQ('\0', data[kLength], "Expecting null for last character");
- return std::as_bytes(std::span(data, kLength));
-}
-
-constexpr ConstByteSpan AsByteSpan(ConstByteSpan data) { return data; }
-
-thread::Options& TransferThreadOptions() {
- static thread::stl::Options options;
- return options;
-}
-
-// Test fixture for pw_transfer tests. Clears the transfer files before and
-// after each test.
-class TransferIntegration : public ::testing::Test {
- protected:
- TransferIntegration()
- : transfer_thread_(chunk_buffer_, encode_buffer_),
- system_thread_(TransferThreadOptions(), transfer_thread_),
- client_(rpc::integration_test::client(),
- rpc::integration_test::kChannelId,
- transfer_thread_,
- 256),
- test_server_client_(rpc::integration_test::client(),
- rpc::integration_test::kChannelId) {
- ClearFiles();
- }
-
- ~TransferIntegration() {
- ClearFiles();
- transfer_thread_.Terminate();
- system_thread_.join();
- }
-
- // Sets the content of a transfer ID and returns a MemoryReader for that data.
- template <typename T>
- void SetContent(uint32_t transfer_id, const T& content) {
- const ConstByteSpan data = AsByteSpan(content);
- std::ofstream stream(directory / std::to_string(transfer_id),
- std::ios::binary);
- PW_CHECK(
- stream.write(reinterpret_cast<const char*>(data.data()), data.size()));
-
- sync::BinarySemaphore reload_complete;
- rpc::RawUnaryReceiver call = test_server_client_.ReloadTransferFiles(
- {}, [&reload_complete](ConstByteSpan, Status) {
- reload_complete.release();
- });
- PW_CHECK(reload_complete.try_acquire_for(3s));
- }
-
- auto OnCompletion() {
- return [this](Status status) {
- last_status_ = status;
- completed_.release();
- };
- }
-
- // Checks that a read transfer succeeded and that the data matches the
- // expected data.
- void ExpectReadData(ConstByteSpan expected) {
- ASSERT_EQ(WaitForCompletion(), OkStatus());
- ASSERT_EQ(expected.size(), read_buffer_.size());
-
- EXPECT_TRUE(std::equal(read_buffer_.begin(),
- read_buffer_.end(),
- std::as_bytes(std::span(expected)).begin()));
- }
-
- // Checks that a write transfer succeeded and that the written contents match.
- void ExpectWriteData(uint32_t transfer_id, ConstByteSpan expected) {
- ASSERT_EQ(WaitForCompletion(), OkStatus());
-
- const std::string written = GetContent(transfer_id);
- ASSERT_EQ(expected.size(), written.size());
-
- ConstByteSpan bytes = std::as_bytes(std::span(written));
- EXPECT_TRUE(std::equal(bytes.begin(), bytes.end(), expected.begin()));
- }
-
- // Waits for the transfer to complete and returns the status.
- Status WaitForCompletion() {
- PW_CHECK(completed_.try_acquire_for(3s));
- return last_status_;
- }
-
- // Exact match the size of kData8192 to test filling the receiving buffer.
- stream::MemoryWriterBuffer<kData8192.size()> read_buffer_;
-
- Client& client() { return client_; }
-
- private:
- static void ClearFiles() {
- for (const auto& entry : std::filesystem::directory_iterator(directory)) {
- if (!entry.is_regular_file()) {
- continue;
- }
-
- if (const std::string name = entry.path().filename().string();
- std::all_of(name.begin(), name.end(), [](char c) {
- return std::isdigit(c);
- })) {
- PW_LOG_DEBUG("Clearing transfer file %s", name.c_str());
- std::filesystem::remove(entry.path());
- }
- }
- }
-
- std::byte chunk_buffer_[512];
- std::byte encode_buffer_[512];
- transfer::Thread<2, 2> transfer_thread_;
- thread::Thread system_thread_;
-
- Client client_;
-
- pw_rpc::raw::TestServer::Client test_server_client_;
- Status last_status_ = Status::Unknown();
- sync::BinarySemaphore completed_;
-};
-
-TEST_F(TransferIntegration, Read_UnknownId) {
- SetContent(123, "hello");
-
- ASSERT_EQ(OkStatus(), client().Read(456, read_buffer_, OnCompletion()));
-
- EXPECT_EQ(Status::NotFound(), WaitForCompletion());
-}
-
-#define PW_TRANSFER_TEST_READ(name, content) \
- TEST_F(TransferIntegration, Read_##name) { \
- for (int i = 0; i < kIterations; ++i) { \
- const ConstByteSpan data = AsByteSpan(content); \
- SetContent(__LINE__, data); \
- ASSERT_EQ(OkStatus(), \
- client().Read(__LINE__, read_buffer_, OnCompletion())); \
- ExpectReadData(data); \
- read_buffer_.clear(); \
- } \
- } \
- static_assert(true, "Semicolons are required")
-
-PW_TRANSFER_TEST_READ(Empty, "");
-PW_TRANSFER_TEST_READ(SingleByte_1, "\0");
-PW_TRANSFER_TEST_READ(SingleByte_2, "?");
-PW_TRANSFER_TEST_READ(SmallData, "hunter2");
-PW_TRANSFER_TEST_READ(LargeData, kData512);
-PW_TRANSFER_TEST_READ(VeryLargeData, kData8192);
-
-TEST_F(TransferIntegration, Write_UnknownId) {
- constexpr std::byte kData[] = {std::byte{0}, std::byte{1}, std::byte{2}};
- stream::MemoryReader reader(kData);
-
- ASSERT_EQ(OkStatus(), client().Write(99, reader, OnCompletion()));
- EXPECT_EQ(Status::NotFound(), WaitForCompletion());
-
- SetContent(99, "something");
- ASSERT_EQ(OkStatus(), client().Write(100, reader, OnCompletion()));
- EXPECT_EQ(Status::NotFound(), WaitForCompletion());
-}
-
-#define PW_TRANSFER_TEST_WRITE(name, content) \
- TEST_F(TransferIntegration, Write_##name) { \
- for (int i = 0; i < kIterations; ++i) { \
- SetContent(__LINE__, "This is junk data that should be overwritten!"); \
- const ConstByteSpan data = AsByteSpan(content); \
- stream::MemoryReader reader(data); \
- ASSERT_EQ(OkStatus(), client().Write(__LINE__, reader, OnCompletion())); \
- ExpectWriteData(__LINE__, data); \
- } \
- } \
- static_assert(true, "Semicolons are required")
-
-PW_TRANSFER_TEST_WRITE(Empty, "");
-PW_TRANSFER_TEST_WRITE(SingleByte_1, "\0");
-PW_TRANSFER_TEST_WRITE(SingleByte_2, "?");
-PW_TRANSFER_TEST_WRITE(SmallData, "hunter2");
-PW_TRANSFER_TEST_WRITE(LargeData, kData512);
-PW_TRANSFER_TEST_WRITE(HdlcEscape, kDataHdlcEscape);
-PW_TRANSFER_TEST_WRITE(VeryLargeData, kData8192);
-
-} // namespace
-} // namespace pw::transfer
-
-int main(int argc, char* argv[]) {
- if (!pw::rpc::integration_test::InitializeClient(argc, argv, "PORT DIRECTORY")
- .ok()) {
- return 1;
- }
-
- if (argc != 3) {
- PW_LOG_INFO("Usage: %s PORT DIRECTORY", argv[0]);
- return 1;
- }
-
- pw::transfer::directory = argv[2];
-
- return RUN_ALL_TESTS();
-}
diff --git a/pw_transfer/integration_test/BUILD.bazel b/pw_transfer/integration_test/BUILD.bazel
new file mode 100644
index 000000000..5cb92ee7a
--- /dev/null
+++ b/pw_transfer/integration_test/BUILD.bazel
@@ -0,0 +1,263 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("//pw_build:pigweed.bzl", "pw_cc_binary")
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
+load("@rules_proto//proto:defs.bzl", "proto_library")
+
+pw_cc_binary(
+ name = "server",
+ srcs = ["server.cc"],
+ deps = [
+ ":config_cc_proto",
+ "//pw_assert",
+ "//pw_chrono:system_clock",
+ "//pw_log",
+ "//pw_rpc/system_server",
+ "//pw_stream",
+ "//pw_stream:std_file_stream",
+ "//pw_thread:thread",
+ "//pw_thread_stl:thread_headers",
+ "//pw_transfer",
+ "@com_google_protobuf//:protobuf",
+ ],
+)
+
+py_binary(
+ name = "proxy",
+ srcs = ["proxy.py"],
+ deps = [
+ ":config_pb2",
+ "//pw_hdlc/py:pw_hdlc",
+ "//pw_transfer:transfer_proto_pb2",
+ "//pw_transfer/py:pw_transfer",
+ "@com_google_protobuf//:protobuf_python",
+ ],
+)
+
+py_test(
+ name = "proxy_test",
+ srcs = [
+ "proxy.py",
+ "proxy_test.py",
+ ],
+ main = "proxy_test.py",
+ deps = [
+ ":config_pb2",
+ "//pw_rpc:internal_packet_proto_pb2",
+ "//pw_transfer:transfer_proto_pb2",
+ "//pw_transfer/py:pw_transfer",
+ ],
+)
+
+proto_library(
+ name = "config_proto",
+ srcs = ["config.proto"],
+)
+
+cc_proto_library(
+ name = "config_cc_proto",
+ deps = [":config_proto"],
+)
+
+py_proto_library(
+ name = "config_pb2",
+ srcs = ["config.proto"],
+)
+
+java_proto_library(
+ name = "config_java_proto",
+ deps = [":config_proto"],
+)
+
+pw_cc_binary(
+ name = "cpp_client",
+ srcs = ["client.cc"],
+ deps = [
+ ":config_cc_proto",
+ "//pw_log",
+ "//pw_rpc:integration_testing",
+ "//pw_status",
+ "//pw_stream:std_file_stream",
+ "//pw_sync:binary_semaphore",
+ "//pw_thread:thread",
+ "//pw_transfer",
+ "//pw_transfer:client",
+ "@com_google_protobuf//:protobuf",
+ ],
+)
+
+py_library(
+ name = "integration_test_fixture",
+ srcs = [
+ "test_fixture.py",
+ ],
+ data = [
+ ":cpp_client",
+ ":java_client",
+ ":proxy",
+ ":python_client",
+ ":server",
+ ],
+ deps = [
+ ":config_pb2",
+ "//pw_protobuf:status_proto_pb2",
+ "@com_google_protobuf//:protobuf_python",
+ "@rules_python//python/runfiles",
+ ],
+)
+
+# Uses ports 3310 and 3311.
+py_test(
+ name = "cross_language_large_write_test",
+ # Actually 1 hour, see
+ # https://docs.bazel.build/versions/main/test-encyclopedia.html#role-of-the-test-runner
+ timeout = "eternal",
+ srcs = [
+ "cross_language_large_write_test.py",
+ ],
+ # This test is not run in CQ because it's too slow.
+ tags = ["manual"],
+ deps = [
+ ":integration_test_fixture",
+ ],
+)
+
+# Uses ports 3306 and 3307.
+py_test(
+ name = "cross_language_large_read_test",
+ # Actually 1 hour, see
+ # https://docs.bazel.build/versions/main/test-encyclopedia.html#role-of-the-test-runner
+ timeout = "eternal",
+ srcs = [
+ "cross_language_large_read_test.py",
+ ],
+ # This test is not run in CQ because it's too slow.
+ tags = ["manual"],
+ deps = [
+ ":integration_test_fixture",
+ ],
+)
+
+# Uses ports 3304 and 3305.
+py_test(
+ name = "cross_language_medium_read_test",
+ timeout = "long",
+ srcs = [
+ "cross_language_medium_read_test.py",
+ ],
+ deps = [
+ ":config_pb2",
+ ":integration_test_fixture",
+ "@com_google_protobuf//:protobuf_python",
+ ],
+)
+
+# Uses ports 3316 and 3317.
+py_test(
+ name = "cross_language_medium_write_test",
+ timeout = "long",
+ srcs = [
+ "cross_language_medium_write_test.py",
+ ],
+ deps = [
+ ":config_pb2",
+ ":integration_test_fixture",
+ "@com_google_protobuf//:protobuf_python",
+ ],
+)
+
+# Uses ports 3302 and 3303.
+py_test(
+ name = "cross_language_small_test",
+ timeout = "moderate",
+ srcs = [
+ "cross_language_small_test.py",
+ ],
+ deps = [
+ ":config_pb2",
+ ":integration_test_fixture",
+ ],
+)
+
+# Uses ports 3308 and 3309.
+py_test(
+ name = "multi_transfer_test",
+ timeout = "moderate",
+ srcs = [
+ "multi_transfer_test.py",
+ ],
+ deps = [
+ ":config_pb2",
+ ":integration_test_fixture",
+ ],
+)
+
+# Uses ports 3312 and 3313.
+py_test(
+ name = "expected_errors_test",
+ timeout = "moderate",
+ srcs = ["expected_errors_test.py"],
+ deps = [
+ ":config_pb2",
+ ":integration_test_fixture",
+ "//pw_protobuf:status_proto_pb2",
+ "@com_google_protobuf//:protobuf_python",
+ ],
+)
+
+# Uses ports 3314 and 3315.
+py_test(
+ name = "legacy_binaries_test",
+ timeout = "moderate",
+ srcs = ["legacy_binaries_test.py"],
+ data = [
+ "@pw_transfer_test_binaries//:all",
+ ],
+ deps = [
+ ":config_pb2",
+ ":integration_test_fixture",
+ "//pw_protobuf:status_proto_pb2",
+ "@rules_python//python/runfiles",
+ ],
+)
+
+java_binary(
+ name = "java_client",
+ srcs = ["JavaClient.java"],
+ main_class = "JavaClient",
+ deps = [
+ ":config_java_proto",
+ "//pw_hdlc/java/main/dev/pigweed/pw_hdlc",
+ "//pw_log/java/main/dev/pigweed/pw_log",
+ "//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
+ "//pw_transfer/java/main/dev/pigweed/pw_transfer:client",
+ "@com_google_protobuf//:protobuf_java",
+ "@maven//:com_google_flogger_flogger_system_backend",
+ "@maven//:com_google_guava_guava",
+ ],
+)
+
+py_binary(
+ name = "python_client",
+ srcs = ["python_client.py"],
+ deps = [
+ ":config_pb2",
+ "//pw_hdlc/py:pw_hdlc",
+ "//pw_rpc/py:pw_rpc",
+ "//pw_transfer:transfer_proto_pb2",
+ "//pw_transfer/py:pw_transfer",
+ "@com_google_protobuf//:protobuf_python",
+ ],
+)
diff --git a/pw_transfer/integration_test/JavaClient.java b/pw_transfer/integration_test/JavaClient.java
new file mode 100644
index 000000000..834faadcf
--- /dev/null
+++ b/pw_transfer/integration_test/JavaClient.java
@@ -0,0 +1,291 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.TextFormat;
+import dev.pigweed.pw_hdlc.Decoder;
+import dev.pigweed.pw_hdlc.Encoder;
+import dev.pigweed.pw_hdlc.Frame;
+import dev.pigweed.pw_log.Logger;
+import dev.pigweed.pw_rpc.Channel;
+import dev.pigweed.pw_rpc.ChannelOutputException;
+import dev.pigweed.pw_rpc.Client;
+import dev.pigweed.pw_rpc.Status;
+import dev.pigweed.pw_transfer.ProtocolVersion;
+import dev.pigweed.pw_transfer.TransferClient;
+import dev.pigweed.pw_transfer.TransferError;
+import dev.pigweed.pw_transfer.TransferService;
+import dev.pigweed.pw_transfer.TransferTimeoutSettings;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.ExecutionException;
+import pw.transfer.ConfigProtos;
+import pw.transfer.ConfigProtos.TransferAction;
+
+public class JavaClient {
+ private static final String SERVICE = "pw.transfer.Transfer";
+ private static final Logger logger = Logger.forClass(Client.class);
+
+ private static final int CHANNEL_ID = 1;
+ private static final long RPC_HDLC_ADDRESS = 'R';
+ private static final String HOSTNAME = "localhost";
+
+ // This is the maximum size of the socket send buffers. Ideally, this is set
+ // to the lowest allowed value to minimize buffering between the proxy and
+ // clients so rate limiting causes the client to block and wait for the
+ // integration test proxy to drain rather than allowing OS buffers to backlog
+ // large quantities of data.
+ //
+ // Note that the OS may chose to not strictly follow this requested buffer
+ // size. Still, setting this value to be as small as possible does reduce
+ // bufer sizes significantly enough to better reflect typical inter-device
+ // communication.
+ //
+ // For this to be effective, servers should also configure their sockets to a
+ // smaller receive buffer size.
+ private static final int MAX_SOCKET_SEND_BUFFER_SIZE = 1;
+
+ private final HdlcRpcChannelOutput channelOutput;
+ private final Client rpcClient;
+ private final HdlcParseThread parseThread;
+
+ public JavaClient(OutputStream writer, InputStream reader) {
+ this.channelOutput = new HdlcRpcChannelOutput(writer, RPC_HDLC_ADDRESS);
+
+ this.rpcClient = Client.create(ImmutableList.of(new Channel(CHANNEL_ID, this.channelOutput)),
+ ImmutableList.of(TransferService.get()));
+
+ this.parseThread = new HdlcParseThread(reader, this.rpcClient);
+ }
+
+ void startClient() {
+ parseThread.start();
+ }
+
+ Client getRpcClient() {
+ return this.rpcClient;
+ }
+
+ private class HdlcRpcChannelOutput implements Channel.Output {
+ private final OutputStream writer;
+ private final long address;
+
+ public HdlcRpcChannelOutput(OutputStream writer, long address) {
+ this.writer = writer;
+ this.address = address;
+ }
+
+ public void send(byte[] packet) throws ChannelOutputException {
+ try {
+ Encoder.writeUiFrame(this.address, ByteBuffer.wrap(packet), this.writer);
+ } catch (IOException e) {
+ throw new ChannelOutputException("Failed to write HDLC UI Frame", e);
+ }
+ }
+ }
+
+ private class HdlcParseThread extends Thread {
+ private final InputStream reader;
+ private final RpcOnComplete frame_handler;
+ private final Decoder decoder;
+
+ private class RpcOnComplete implements Decoder.OnCompleteFrame {
+ private final Client rpc_client;
+
+ public RpcOnComplete(Client rpc_client) {
+ this.rpc_client = rpc_client;
+ }
+
+ public void onCompleteFrame(Frame frame) {
+ if (frame.getAddress() == RPC_HDLC_ADDRESS) {
+ this.rpc_client.processPacket(frame.getPayload());
+ }
+ }
+ }
+
+ public HdlcParseThread(InputStream reader, Client rpc_client) {
+ this.reader = reader;
+ this.frame_handler = new RpcOnComplete(rpc_client);
+ this.decoder = new Decoder(this.frame_handler);
+ }
+
+ public void run() {
+ while (true) {
+ int val = 0;
+ try {
+ val = this.reader.read();
+ } catch (IOException e) {
+ logger.atSevere().log("HDLC parse thread read failed");
+ System.exit(1);
+ }
+ this.decoder.process((byte) val);
+ }
+ }
+ }
+
+ public static ConfigProtos.ClientConfig ParseConfigFrom(InputStream reader) throws IOException {
+ byte[] buffer = new byte[reader.available()];
+ reader.read(buffer);
+ ConfigProtos.ClientConfig.Builder config_builder = ConfigProtos.ClientConfig.newBuilder();
+ TextFormat.merge(new String(buffer, StandardCharsets.UTF_8), config_builder);
+ if (config_builder.getChunkTimeoutMs() == 0) {
+ throw new AssertionError("chunk_timeout_ms may not be 0");
+ }
+ if (config_builder.getInitialChunkTimeoutMs() == 0) {
+ throw new AssertionError("initial_chunk_timeout_ms may not be 0");
+ }
+ if (config_builder.getMaxRetries() == 0) {
+ throw new AssertionError("max_retries may not be 0");
+ }
+ if (config_builder.getMaxLifetimeRetries() == 0) {
+ throw new AssertionError("max_lifetime_retries may not be 0");
+ }
+ return config_builder.build();
+ }
+
+ public static void ReadFromServer(
+ int resourceId, Path fileName, TransferClient client, Status expected_status) {
+ byte[] data;
+ try {
+ data = client.read(resourceId).get();
+ } catch (ExecutionException e) {
+ if (((TransferError) e.getCause()).status() != expected_status) {
+ throw new AssertionError("Unexpected transfer read failure", e);
+ }
+ // Expected failure occurred, skip trying to write the data knowing that
+ // it is missing.
+ return;
+ } catch (InterruptedException e) {
+ throw new AssertionError("Read from server failed", e);
+ }
+
+ try {
+ Files.write(fileName, data);
+ } catch (IOException e) {
+ logger.atSevere().log("Failed to write to output file `%s`", fileName);
+ throw new AssertionError("Failed to write output file from server", e);
+ }
+ }
+
+ public static void WriteToServer(
+ int resourceId, Path fileName, TransferClient client, Status expected_status) {
+ if (Files.notExists(fileName)) {
+ logger.atSevere().log("Input file `%s` does not exist", fileName);
+ }
+
+ byte[] data;
+ try {
+ data = Files.readAllBytes(fileName);
+ } catch (IOException e) {
+ logger.atSevere().log("Failed to read input file `%s`", fileName);
+ throw new AssertionError("Failed to read input file on write to server", e);
+ }
+
+ try {
+ client.write(resourceId, data).get();
+ } catch (ExecutionException e) {
+ if (((TransferError) e.getCause()).status() != expected_status) {
+ throw new AssertionError("Unexpected transfer write failure", e);
+ }
+ } catch (InterruptedException e) {
+ throw new AssertionError("Write to server failed", e);
+ }
+ }
+
+ public static void main(String[] args) {
+ if (args.length != 1) {
+ logger.atSevere().log("Usage: PORT");
+ System.exit(1);
+ }
+
+ // The port is provided directly as a commandline argument.
+ int port = Integer.parseInt(args[0]);
+
+ ConfigProtos.ClientConfig config;
+ try {
+ config = ParseConfigFrom(System.in);
+ } catch (IOException e) {
+ throw new AssertionError("Failed to parse config file from stdin", e);
+ }
+
+ Socket socket;
+ try {
+ socket = new Socket(HOSTNAME, port);
+ } catch (IOException e) {
+ logger.atSevere().log("Failed to connect to %s:%d", HOSTNAME, port);
+ throw new AssertionError("Failed to connect to server/proxy port", e);
+ }
+ try {
+ socket.setSendBufferSize(MAX_SOCKET_SEND_BUFFER_SIZE);
+ } catch (SocketException e) {
+ logger.atSevere().log("Invalid socket buffer size %d", MAX_SOCKET_SEND_BUFFER_SIZE);
+ throw new AssertionError("Invalid socket buffer size", e);
+ }
+ InputStream reader;
+ OutputStream writer;
+
+ try {
+ writer = socket.getOutputStream();
+ reader = socket.getInputStream();
+ } catch (IOException e) {
+ throw new AssertionError("Failed to open socket streams", e);
+ }
+
+ JavaClient hdlc_rpc_client = new JavaClient(writer, reader);
+
+ hdlc_rpc_client.startClient();
+
+ TransferClient client = new TransferClient(
+ hdlc_rpc_client.getRpcClient().method(CHANNEL_ID, TransferService.get().name() + "/Read"),
+ hdlc_rpc_client.getRpcClient().method(CHANNEL_ID, TransferService.get().name() + "/Write"),
+ TransferTimeoutSettings.builder()
+ .setTimeoutMillis(config.getChunkTimeoutMs())
+ .setInitialTimeoutMillis(config.getInitialChunkTimeoutMs())
+ .setMaxRetries(config.getMaxRetries())
+ .setMaxLifetimeRetries(config.getMaxLifetimeRetries())
+ .build());
+
+ for (ConfigProtos.TransferAction action : config.getTransferActionsList()) {
+ int resourceId = action.getResourceId();
+ Path fileName = Paths.get(action.getFilePath());
+
+ if (action.getProtocolVersion() != TransferAction.ProtocolVersion.UNKNOWN_VERSION) {
+ client.setProtocolVersion(ProtocolVersion.values()[action.getProtocolVersionValue()]);
+ } else {
+ client.setProtocolVersion(ProtocolVersion.latest());
+ }
+
+ if (action.getTransferType() == ConfigProtos.TransferAction.TransferType.WRITE_TO_SERVER) {
+ WriteToServer(resourceId, fileName, client, Status.fromCode(action.getExpectedStatus()));
+ } else if (action.getTransferType()
+ == ConfigProtos.TransferAction.TransferType.READ_FROM_SERVER) {
+ ReadFromServer(resourceId, fileName, client, Status.fromCode(action.getExpectedStatus()));
+ } else {
+ throw new AssertionError("Unknown transfer action type");
+ }
+ }
+
+ logger.atInfo().log("Transfer completed successfully");
+
+ System.exit(0);
+ }
+}
diff --git a/pw_transfer/integration_test/client.cc b/pw_transfer/integration_test/client.cc
new file mode 100644
index 000000000..49bb3f267
--- /dev/null
+++ b/pw_transfer/integration_test/client.cc
@@ -0,0 +1,221 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// Client binary for the cross-language integration test.
+//
+// Usage:
+// bazel-bin/pw_transfer/integration_test_client 3300 <<< "resource_id: 12
+// file: '/tmp/myfile.txt'"
+//
+// WORK IN PROGRESS, SEE b/228516801
+#include "pw_transfer/client.h"
+
+#include <sys/socket.h>
+
+#include <cstddef>
+#include <cstdio>
+
+#include "google/protobuf/text_format.h"
+#include "pw_assert/check.h"
+#include "pw_log/log.h"
+#include "pw_rpc/channel.h"
+#include "pw_rpc/integration_testing.h"
+#include "pw_status/status.h"
+#include "pw_status/try.h"
+#include "pw_stream/std_file_stream.h"
+#include "pw_sync/binary_semaphore.h"
+#include "pw_thread/thread.h"
+#include "pw_thread_stl/options.h"
+#include "pw_transfer/integration_test/config.pb.h"
+#include "pw_transfer/transfer_thread.h"
+
+namespace pw::transfer::integration_test {
+namespace {
+
+// This is the maximum size of the socket send buffers. Ideally, this is set
+// to the lowest allowed value to minimize buffering between the proxy and
+// clients so rate limiting causes the client to block and wait for the
+// integration test proxy to drain rather than allowing OS buffers to backlog
+// large quantities of data.
+//
+// Note that the OS may chose to not strictly follow this requested buffer size.
+// Still, setting this value to be as small as possible does reduce bufer sizes
+// significantly enough to better reflect typical inter-device communication.
+//
+// For this to be effective, servers should also configure their sockets to a
+// smaller receive buffer size.
+constexpr int kMaxSocketSendBufferSize = 1;
+
+// This client configures a socket read timeout to allow the RPC dispatch thread
+// to exit gracefully.
+constexpr timeval kSocketReadTimeout = {.tv_sec = 1, .tv_usec = 0};
+
+thread::Options& TransferThreadOptions() {
+ static thread::stl::Options options;
+ return options;
+}
+
+// Transfer status, valid only after semaphore is acquired.
+//
+// We need to bundle the status and semaphore together because a pw_function
+// callback can at most capture the reference to one variable (and we need to
+// both set the status and release the semaphore).
+struct TransferResult {
+ Status status = Status::Unknown();
+ sync::BinarySemaphore completed;
+};
+
+// Create a pw_transfer client and perform the transfer actions.
+pw::Status PerformTransferActions(const pw::transfer::ClientConfig& config) {
+ constexpr size_t kMaxPayloadSize = rpc::MaxSafePayloadSize();
+ std::byte chunk_buffer[kMaxPayloadSize];
+ std::byte encode_buffer[kMaxPayloadSize];
+ transfer::Thread<2, 2> transfer_thread(chunk_buffer, encode_buffer);
+ thread::Thread system_thread(TransferThreadOptions(), transfer_thread);
+
+ pw::transfer::Client client(rpc::integration_test::client(),
+ rpc::integration_test::kChannelId,
+ transfer_thread);
+
+ client.set_max_retries(config.max_retries());
+ client.set_max_lifetime_retries(config.max_lifetime_retries());
+
+ Status status = pw::OkStatus();
+ for (const pw::transfer::TransferAction& action : config.transfer_actions()) {
+ TransferResult result;
+ // If no protocol version is specified, default to the latest version.
+ pw::transfer::ProtocolVersion protocol_version =
+ action.protocol_version() ==
+ pw::transfer::TransferAction::ProtocolVersion::
+ TransferAction_ProtocolVersion_UNKNOWN_VERSION
+ ? pw::transfer::ProtocolVersion::kLatest
+ : static_cast<pw::transfer::ProtocolVersion>(
+ action.protocol_version());
+ if (action.transfer_type() ==
+ pw::transfer::TransferAction::TransferType::
+ TransferAction_TransferType_WRITE_TO_SERVER) {
+ pw::stream::StdFileReader input(action.file_path().c_str());
+ client.Write(
+ action.resource_id(),
+ input,
+ [&result](Status status) {
+ result.status = status;
+ result.completed.release();
+ },
+ pw::transfer::cfg::kDefaultChunkTimeout,
+ protocol_version);
+ // Wait for the transfer to complete. We need to do this here so that the
+ // StdFileReader doesn't go out of scope.
+ result.completed.acquire();
+
+ } else if (action.transfer_type() ==
+ pw::transfer::TransferAction::TransferType::
+ TransferAction_TransferType_READ_FROM_SERVER) {
+ pw::stream::StdFileWriter output(action.file_path().c_str());
+ client.Read(
+ action.resource_id(),
+ output,
+ [&result](Status status) {
+ result.status = status;
+ result.completed.release();
+ },
+ pw::transfer::cfg::kDefaultChunkTimeout,
+ protocol_version);
+ // Wait for the transfer to complete.
+ result.completed.acquire();
+ } else {
+ PW_LOG_ERROR("Unrecognized transfer action type %d",
+ action.transfer_type());
+ status = pw::Status::InvalidArgument();
+ break;
+ }
+
+ if (result.status.code() != action.expected_status()) {
+ PW_LOG_ERROR("Failed to perform action:\n%s",
+ action.DebugString().c_str());
+ status = result.status;
+ break;
+ }
+ }
+
+ transfer_thread.Terminate();
+
+ system_thread.join();
+
+ // The RPC thread must join before destroying transfer objects as the transfer
+ // service may still reference the transfer thread or transfer client objects.
+ pw::rpc::integration_test::TerminateClient();
+ return status;
+}
+
+} // namespace
+} // namespace pw::transfer::integration_test
+
+int main(int argc, char* argv[]) {
+ if (argc < 2) {
+ PW_LOG_INFO("Usage: %s PORT <<< config textproto", argv[0]);
+ return 1;
+ }
+
+ const int port = std::atoi(argv[1]);
+
+ std::string config_string;
+ std::string line;
+ while (std::getline(std::cin, line)) {
+ config_string = config_string + line + '\n';
+ }
+ pw::transfer::ClientConfig config;
+
+ bool ok =
+ google::protobuf::TextFormat::ParseFromString(config_string, &config);
+ if (!ok) {
+ PW_LOG_INFO("Failed to parse config: %s", config_string.c_str());
+ PW_LOG_INFO("Usage: %s PORT <<< config textproto", argv[0]);
+ return 1;
+ } else {
+ PW_LOG_INFO("Client loaded config:\n%s", config.DebugString().c_str());
+ }
+
+ if (!pw::rpc::integration_test::InitializeClient(port).ok()) {
+ return 1;
+ }
+
+ int retval = setsockopt(
+ pw::rpc::integration_test::GetClientSocketFd(),
+ SOL_SOCKET,
+ SO_SNDBUF,
+ &pw::transfer::integration_test::kMaxSocketSendBufferSize,
+ sizeof(pw::transfer::integration_test::kMaxSocketSendBufferSize));
+ PW_CHECK_INT_EQ(retval,
+ 0,
+ "Failed to configure socket send buffer size with errno=%d",
+ errno);
+
+ retval =
+ setsockopt(pw::rpc::integration_test::GetClientSocketFd(),
+ SOL_SOCKET,
+ SO_RCVTIMEO,
+ &pw::transfer::integration_test::kSocketReadTimeout,
+ sizeof(pw::transfer::integration_test::kSocketReadTimeout));
+ PW_CHECK_INT_EQ(retval,
+ 0,
+ "Failed to configure socket receive timeout with errno=%d",
+ errno);
+
+ if (!pw::transfer::integration_test::PerformTransferActions(config).ok()) {
+ PW_LOG_INFO("Failed to transfer!");
+ return 1;
+ }
+ return 0;
+}
diff --git a/pw_transfer/integration_test/config.proto b/pw_transfer/integration_test/config.proto
new file mode 100644
index 000000000..d120fc576
--- /dev/null
+++ b/pw_transfer/integration_test/config.proto
@@ -0,0 +1,225 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+syntax = "proto3";
+
+package pw.transfer;
+
+option java_package = "pw.transfer";
+option java_outer_classname = "ConfigProtos";
+
+message TransferAction {
+ enum ProtocolVersion {
+ option allow_alias = true;
+ UNKNOWN_VERSION = 0;
+ V1 = 1;
+ V2 = 2;
+ LATEST = 2;
+ }
+
+ // As transfers are always client initiated, TransferType is from the client's
+ // perspective.
+ enum TransferType {
+ UNKNOWN = 0;
+ WRITE_TO_SERVER = 1;
+ READ_FROM_SERVER = 2;
+ }
+
+ // Transfer resource ID to use for this transfer.
+ uint32 resource_id = 1;
+
+ // Path to the file that data should be read from or written to (depending on
+ // transfer_type). When reading from the server, this is the path the file
+ // is written to. When writing to the server, this is the path to the file
+ // that should be read from.
+ string file_path = 2;
+
+ // Whether to write to the server, or read from it.
+ TransferType transfer_type = 3;
+
+ // Expected final status of transfer operation.
+ //
+ // TODO(b/241456982): This should be a pw.protobuf.StatusCode, but importing
+ // other Pigweed protos doesn't work in Bazel.
+ uint32 expected_status = 4;
+
+ // Protocol version to initiate the transfer with.
+ ProtocolVersion protocol_version = 5;
+}
+
+// Configuration for the integration test client.
+message ClientConfig {
+ // The sequence of transfer actions to perform during the lifetime of this
+ // client configuration.
+ repeated TransferAction transfer_actions = 1;
+
+ // The maximum number of times this client will attempt to send a packet
+ // before the transfer aborts due to a lack of response.
+ uint32 max_retries = 2;
+
+ // The maximum time this client will wait for a response to the first
+ // packet sent to the pw_transfer server before attempting to retry. Extending
+ // this can help work around cases where transfer initialization takes a long
+ // time.
+ //
+ // Note: This parameter is only supported on Java transfer clients.
+ // TODO(tpudlik): google.protobuf.Duration?
+ uint32 initial_chunk_timeout_ms = 3;
+
+ // The maximum amount of time this client will wait for a response to a sent
+ // packet before attempting to re-send the packet.
+ //
+ // TODO(tpudlik): google.protobuf.Duration?
+ uint32 chunk_timeout_ms = 4;
+
+ // Cumulative maximum number of times to retry over the course of the transfer
+ // before giving up.
+ uint32 max_lifetime_retries = 5;
+}
+
+// Stacks of paths to use when doing transfers. Each new initiated transfer
+// on this resource gets the next file path in the stack. Once the stack is
+// exhausted, new transfers on this resource fail as though this resource is
+// unregistered.
+message ServerResourceLocations {
+ // When a client reads from this server, this stack is used to determine which
+ // file path to read data from.
+ repeated string source_paths = 3;
+
+ // When a client writes to this server, this stack is used to determine which
+ // file path to write data to.
+ repeated string destination_paths = 4;
+
+ // If source_paths is exhausted or empty, this source path can be reused
+ // as a fallback indefinitely.
+ string default_source_path = 5;
+
+ // If destination_paths is exhausted or empty, this destination path can be
+ // reused as a fallback indefinitely.
+ string default_destination_path = 6;
+}
+
+// Configuration for the integration test server.
+message ServerConfig {
+ // A mapping of transfer resource ID to files to read from or write to.
+ map<uint32, ServerResourceLocations> resources = 1;
+
+ // Size of the chunk buffer used by this server's transfer thread, in bytes.
+ uint32 chunk_size_bytes = 2;
+
+ // Window size, in bytes.
+ uint32 pending_bytes = 3;
+
+ // TODO(tpudlik): google.protobuf.Duration?
+ uint32 chunk_timeout_seconds = 4;
+ uint32 transfer_service_retries = 5;
+ uint32 extend_window_divisor = 6;
+}
+
+// Configuration for the HdlcPacketizer proxy filter.
+message HdlcPacketizerConfig {}
+
+// Configuration for the DataDropper proxy filter.
+message DataDropperConfig {
+ // Rate at which to drop data
+ float rate = 1;
+
+ // Seed to use for the rand number generator used for determining
+ // when data is dropped.
+ int64 seed = 2;
+}
+
+// Configuration for the KeepDropQueue proxy filter.
+message KeepDropQueueConfig {
+ // A KeepDropQueue filter will alternate between keeping packets and dropping
+ // chunks of data based on a keep/drop queue provided during its creation. The
+ // queue is looped over unless a negative element is found. A negative number
+ // is effectively the same as a value of infinity.
+ //
+ // This filter is typically most pratical when used with a packetizer so data
+ // can be dropped as distinct packets.
+ //
+ // Examples:
+ //
+ // keep_drop_queue = [3, 2]:
+ // Keeps 3 packets,
+ // Drops 2 packets,
+ // Keeps 3 packets,
+ // Drops 2 packets,
+ // ... [loops indefinitely]
+ //
+ // keep_drop_queue = [5, 99, 1, -1]:
+ // Keeps 5 packets,
+ // Drops 99 packets,
+ // Keeps 1 packet,
+ // Drops all further packets.
+ repeated int32 keep_drop_queue = 1;
+}
+
+// Configuration for the RateLimiter proxy filter.
+message RateLimiterConfig {
+ // Rate limit, in bytes/sec.
+ float rate = 1;
+}
+
+// Configuration for the DataTransposer proxy filter.
+message DataTransposerConfig {
+ // Rate at which to transpose data. Probability of transposition
+ // between 0.0 and 1.0.
+ float rate = 1;
+
+ // Maximum time a chunk of data will be held for Transposition. After this
+ // time has elapsed, the packet is sent in order.
+ float timeout = 2;
+
+ // Seed to use for the rand number generator used for determining
+ // when data is transposed.
+ int64 seed = 3;
+}
+
+// Configuration for the ServerFailure proxy filter.
+message ServerFailureConfig {
+ // A list of numbers of packets to send before dropping all subsequent
+ // packets until a TRANSFER_START packet is seen. This process is
+ // repeated for each element in packets_before_failure. After that list
+ // is exhausted, ServerFailure will send all packets.
+ repeated uint32 packets_before_failure = 1;
+}
+
+// Configuration for the WindowPacketDropper proxy filter.
+message WindowPacketDropperConfig {
+ // The nth packet of every window to drop.
+ uint32 window_packet_to_drop = 1;
+}
+
+// Configuration for a single stage in the proxy filter stack.
+message FilterConfig {
+ oneof filter {
+ HdlcPacketizerConfig hdlc_packetizer = 1;
+ DataDropperConfig data_dropper = 2;
+ RateLimiterConfig rate_limiter = 3;
+ DataTransposerConfig data_transposer = 4;
+ ServerFailureConfig server_failure = 5;
+ KeepDropQueueConfig keep_drop_queue = 6;
+ WindowPacketDropperConfig window_packet_dropper = 7;
+ }
+}
+
+message ProxyConfig {
+ // Filters are listed in order of execution. I.e. the first filter listed
+ // will get the received data first then pass it on the the second listed
+ // filter. That process repeats until the last filter send the data to the
+ // other side of the proxy.
+ repeated FilterConfig client_filter_stack = 1;
+ repeated FilterConfig server_filter_stack = 2;
+}
diff --git a/pw_transfer/integration_test/cross_language_large_read_test.py b/pw_transfer/integration_test/cross_language_large_read_test.py
new file mode 100644
index 000000000..31d41e82e
--- /dev/null
+++ b/pw_transfer/integration_test/cross_language_large_read_test.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Cross-language pw_transfer read tests that take tens of minutes to run.
+
+Usage:
+
+ bazel run pw_transfer/integration_test:cross_language_large_read_test
+
+Command-line arguments must be provided after a double-dash:
+
+ bazel run pw_transfer/integration_test:cross_language_large_read_test -- \
+ --server-port 3304
+
+Which tests to run can be specified as command-line arguments:
+
+ bazel run pw_transfer/integration_test:cross_language_large_read_test -- \
+ LargeReadTransferIntegrationTest.test_1mb_read_dropped_data_1_java
+
+"""
+
+from parameterized import parameterized
+import random
+
+from google.protobuf import text_format
+
+import test_fixture
+from test_fixture import (
+ TransferConfig,
+ TransferIntegrationTest,
+ TransferIntegrationTestHarness,
+)
+from pigweed.pw_transfer.integration_test import config_pb2
+
+_ALL_LANGUAGES = ("cpp", "java", "python")
+
+
+class LargeReadTransferIntegrationTest(TransferIntegrationTest):
+ # Each set of transfer tests uses a different client/server port pair to
+ # allow tests to be run in parallel.
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ server_port=3306, client_port=3307
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES)
+ def test_1mb_read_dropped_data(self, client_type):
+ server_config = config_pb2.ServerConfig(
+ chunk_size_bytes=216,
+ pending_bytes=32 * 1024,
+ chunk_timeout_seconds=5,
+ transfer_service_retries=4,
+ extend_window_divisor=32,
+ )
+ client_config = config_pb2.ClientConfig(
+ max_retries=5,
+ initial_chunk_timeout_ms=10000,
+ chunk_timeout_ms=4000,
+ )
+ proxy_config = text_format.Parse(
+ """
+ client_filter_stack: [
+ { rate_limiter: {rate: 50000} },
+ { hdlc_packetizer: {} },
+ { data_dropper: {rate: 0.01, seed: 1649963713563718435} }
+ ]
+
+ server_filter_stack: [
+ { rate_limiter: {rate: 50000} },
+ { hdlc_packetizer: {} },
+ { data_dropper: {rate: 0.01, seed: 1649963713563718436} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ )
+
+ payload = random.Random(1649963713563718437).randbytes(1 * 1024 * 1024)
+ resource_id = 12
+ config = TransferConfig(server_config, client_config, proxy_config)
+ self.do_single_read(client_type, config, resource_id, payload)
+
+ @parameterized.expand(_ALL_LANGUAGES)
+ def test_1mb_read_reordered_data(self, client_type):
+ server_config = config_pb2.ServerConfig(
+ chunk_size_bytes=216,
+ pending_bytes=32 * 1024,
+ chunk_timeout_seconds=5,
+ transfer_service_retries=4,
+ extend_window_divisor=32,
+ )
+ client_config = config_pb2.ClientConfig(
+ max_retries=5,
+ initial_chunk_timeout_ms=10000,
+ chunk_timeout_ms=4000,
+ )
+ proxy_config = text_format.Parse(
+ """
+ client_filter_stack: [
+ { rate_limiter: {rate: 50000} },
+ { hdlc_packetizer: {} },
+ { data_transposer: {rate: 0.005, timeout: 0.5, seed: 1649963713563718435} }
+ ]
+
+ server_filter_stack: [
+ { rate_limiter: {rate: 50000} },
+ { hdlc_packetizer: {} },
+ { data_transposer: {rate: 0.005, timeout: 0.5, seed: 1649963713563718435} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ )
+
+ payload = random.Random(1649963713563718437).randbytes(1 * 1024 * 1024)
+ resource_id = 12
+ config = TransferConfig(server_config, client_config, proxy_config)
+ self.do_single_read(client_type, config, resource_id, payload)
+
+
+if __name__ == '__main__':
+ test_fixture.run_tests_for(LargeReadTransferIntegrationTest)
diff --git a/pw_transfer/integration_test/cross_language_large_write_test.py b/pw_transfer/integration_test/cross_language_large_write_test.py
new file mode 100644
index 000000000..0f195e2da
--- /dev/null
+++ b/pw_transfer/integration_test/cross_language_large_write_test.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Cross-language pw_transfer write tests that take tens of minutes to run.
+
+Usage:
+
+ bazel run pw_transfer/integration_test:cross_language_large_write_test
+
+Command-line arguments must be provided after a double-dash:
+
+ bazel run pw_transfer/integration_test:cross_language_large_write_test -- \
+ --server-port 3304
+
+Which tests to run can be specified as command-line arguments:
+
+ bazel run pw_transfer/integration_test:cross_language_large_write_test -- \
+ LargeWriteTransferIntegrationTest.test_1mb_write_dropped_data_1_java
+
+"""
+
+from parameterized import parameterized
+import random
+
+from google.protobuf import text_format
+
+import test_fixture
+from test_fixture import (
+ TransferConfig,
+ TransferIntegrationTest,
+ TransferIntegrationTestHarness,
+)
+from pigweed.pw_transfer.integration_test import config_pb2
+
+_ALL_LANGUAGES = ("cpp", "java", "python")
+
+
+class LargeWriteTransferIntegrationTest(TransferIntegrationTest):
+ # Each set of transfer tests uses a different client/server port pair to
+ # allow tests to be run in parallel.
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ server_port=3310, client_port=3311
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES)
+ def test_1mb_write_dropped_data(self, client_type):
+ server_config = config_pb2.ServerConfig(
+ chunk_size_bytes=216,
+ pending_bytes=32 * 1024,
+ chunk_timeout_seconds=5,
+ transfer_service_retries=4,
+ extend_window_divisor=32,
+ )
+ client_config = config_pb2.ClientConfig(
+ max_retries=5,
+ initial_chunk_timeout_ms=10000,
+ chunk_timeout_ms=4000,
+ )
+ proxy_config = text_format.Parse(
+ """
+ client_filter_stack: [
+ { rate_limiter: {rate: 50000} },
+ { hdlc_packetizer: {} },
+ { data_dropper: {rate: 0.01, seed: 1649963713563718435} }
+ ]
+
+ server_filter_stack: [
+ { rate_limiter: {rate: 50000} },
+ { hdlc_packetizer: {} },
+ { data_dropper: {rate: 0.01, seed: 1649963713563718436} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ )
+
+ payload = random.Random(1649963713563718437).randbytes(1 * 1024 * 1024)
+ resource_id = 12
+ config = TransferConfig(server_config, client_config, proxy_config)
+ self.do_single_write(client_type, config, resource_id, payload)
+
+ @parameterized.expand(_ALL_LANGUAGES)
+ def test_1mb_write_reordered_data(self, client_type):
+ server_config = config_pb2.ServerConfig(
+ chunk_size_bytes=216,
+ pending_bytes=32 * 1024,
+ chunk_timeout_seconds=5,
+ transfer_service_retries=4,
+ extend_window_divisor=32,
+ )
+ client_config = config_pb2.ClientConfig(
+ max_retries=5,
+ initial_chunk_timeout_ms=10000,
+ chunk_timeout_ms=4000,
+ )
+ proxy_config = text_format.Parse(
+ """
+ client_filter_stack: [
+ { rate_limiter: {rate: 50000} },
+ { hdlc_packetizer: {} },
+ { data_transposer: {rate: 0.005, timeout: 0.5, seed: 1649963713563718435} }
+ ]
+
+ server_filter_stack: [
+ { rate_limiter: {rate: 50000} },
+ { hdlc_packetizer: {} },
+ { data_transposer: {rate: 0.005, timeout: 0.5, seed: 1649963713563718435} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ )
+
+ payload = random.Random(1649963713563718437).randbytes(1 * 1024 * 1024)
+ resource_id = 12
+ config = TransferConfig(server_config, client_config, proxy_config)
+ self.do_single_write(client_type, config, resource_id, payload)
+
+
+if __name__ == '__main__':
+ test_fixture.run_tests_for(LargeWriteTransferIntegrationTest)
diff --git a/pw_transfer/integration_test/cross_language_medium_read_test.py b/pw_transfer/integration_test/cross_language_medium_read_test.py
new file mode 100644
index 000000000..8899d8f78
--- /dev/null
+++ b/pw_transfer/integration_test/cross_language_medium_read_test.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Cross-language pw_transfer tests that take several seconds each.
+
+Usage:
+
+ bazel run pw_transfer/integration_test:cross_language_medium_read_test
+
+Command-line arguments must be provided after a double-dash:
+
+ bazel run pw_transfer/integration_test:cross_language_medium_read_test -- \
+ --server-port 3304
+
+Which tests to run can be specified as command-line arguments:
+
+ bazel run pw_transfer/integration_test:cross_language_medium_read_test -- \
+ MediumTransferReadIntegrationTest.test_medium_client_read_1_java
+
+"""
+
+import itertools
+from parameterized import parameterized
+import random
+
+from google.protobuf import text_format
+
+from pigweed.pw_transfer.integration_test import config_pb2
+import test_fixture
+from test_fixture import TransferIntegrationTestHarness, TransferConfig
+
+_ALL_LANGUAGES = ("cpp", "java", "python")
+_ALL_VERSIONS = (
+ config_pb2.TransferAction.ProtocolVersion.V1,
+ config_pb2.TransferAction.ProtocolVersion.V2,
+)
+_ALL_LANGUAGES_AND_VERSIONS = tuple(
+ itertools.product(_ALL_LANGUAGES, _ALL_VERSIONS)
+)
+
+
+class MediumTransferReadIntegrationTest(test_fixture.TransferIntegrationTest):
+ # Each set of transfer tests uses a different client/server port pair to
+ # allow tests to be run in parallel.
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ server_port=3304, client_port=3305
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_medium_client_read(self, client_type, protocol_version):
+ payload = random.Random(67336391945).randbytes(512)
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_large_hdlc_escape_client_read(self, client_type, protocol_version):
+ # Use bytes that will be escaped by HDLC to ensure transfer over a
+ # HDLC channel doesn't cause frame corruption due to insufficient
+ # buffer space. ~10KB is relatively arbitrary, but is to ensure that
+ # more than a small handful of packets are sent between the server
+ # and client.
+ payload = b"~" * 98731
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_pattern_drop_client_read(self, client_type, protocol_version):
+ """Drops packets with an alternating pattern."""
+ payload = random.Random(67336391945).randbytes(1234)
+ config = TransferConfig(
+ self.default_server_config(),
+ self.default_client_config(),
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} },
+ { keep_drop_queue: {keep_drop_queue: [5, 1]} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} },
+ { keep_drop_queue: {keep_drop_queue: [5, 1]} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ # Resource ID is arbitrary, but deliberately set to be >1 byte.
+ resource_id = 1337
+
+ # This test causes flakes during the opening handshake of a transfer, so
+ # allow the resource_id of this transfer to be reused multiple times.
+ self.do_single_read(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ protocol_version,
+ permanent_resource_id=True,
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_parameter_drop_client_read(self, client_type, protocol_version):
+ """Drops the first few transfer initialization packets."""
+ payload = random.Random(67336391945).randbytes(1234)
+ config = TransferConfig(
+ self.default_server_config(),
+ self.default_client_config(),
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} },
+ { keep_drop_queue: {keep_drop_queue: [2, 1, -1]} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} },
+ { keep_drop_queue: {keep_drop_queue: [1, 2, -1]} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ # Resource ID is arbitrary, but deliberately set to be >2 bytes.
+ resource_id = 597419
+
+ # This test deliberately causes flakes during the opening handshake of
+ # a transfer, so allow the resource_id of this transfer to be reused
+ # multiple times.
+ self.do_single_read(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ protocol_version,
+ permanent_resource_id=True,
+ )
+
+
+if __name__ == '__main__':
+ test_fixture.run_tests_for(MediumTransferReadIntegrationTest)
diff --git a/pw_transfer/integration_test/cross_language_medium_write_test.py b/pw_transfer/integration_test/cross_language_medium_write_test.py
new file mode 100644
index 000000000..5f8c314dc
--- /dev/null
+++ b/pw_transfer/integration_test/cross_language_medium_write_test.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Cross-language pw_transfer tests that take several seconds each.
+
+Usage:
+
+ bazel run pw_transfer/integration_test:cross_language_medium_write_test
+
+Command-line arguments must be provided after a double-dash:
+
+ bazel run pw_transfer/integration_test:cross_language_medium_write_test -- \
+ --server-port 3304
+
+Which tests to run can be specified as command-line arguments:
+
+ bazel run pw_transfer/integration_test:cross_language_medium_write_test -- \
+ MediumTransferWriteIntegrationTest.test_medium_client_write_1_java
+
+"""
+
+import itertools
+from parameterized import parameterized
+import random
+
+from google.protobuf import text_format
+
+from pigweed.pw_transfer.integration_test import config_pb2
+import test_fixture
+from test_fixture import TransferIntegrationTestHarness, TransferConfig
+
+_ALL_LANGUAGES = ("cpp", "java", "python")
+_ALL_VERSIONS = (
+ config_pb2.TransferAction.ProtocolVersion.V1,
+ config_pb2.TransferAction.ProtocolVersion.V2,
+)
+_ALL_LANGUAGES_AND_VERSIONS = tuple(
+ itertools.product(_ALL_LANGUAGES, _ALL_VERSIONS)
+)
+
+
+class MediumTransferWriteIntegrationTest(test_fixture.TransferIntegrationTest):
+ # Each set of transfer tests uses a different client/server port pair to
+ # allow tests to be run in parallel.
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ server_port=3316, client_port=3317
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_medium_client_write(self, client_type, protocol_version):
+ payload = random.Random(67336391945).randbytes(512)
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_write(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_large_hdlc_escape_client_write(
+ self, client_type, protocol_version
+ ):
+ # Use bytes that will be escaped by HDLC to ensure transfer over a
+ # HDLC channel doesn't cause frame corruption due to insufficient
+ # buffer space. ~10KB is relatively arbitrary, but is to ensure that
+ # more than a small handful of packets are sent between the server
+ # and client.
+ payload = b"~" * 98731
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_write(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_pattern_drop_client_write(self, client_type, protocol_version):
+ """Drops packets with an alternating pattern."""
+ payload = random.Random(67336391945).randbytes(1234)
+ config = TransferConfig(
+ self.default_server_config(),
+ self.default_client_config(),
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} },
+ { keep_drop_queue: {keep_drop_queue: [5, 1]} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} },
+ { keep_drop_queue: {keep_drop_queue: [5, 1]} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ # Resource ID is arbitrary, but deliberately set to be >1 byte.
+ resource_id = 1337
+
+ # This test deliberately causes flakes during the opening handshake of
+ # a transfer, so allow the resource_id of this transfer to be reused
+ # multiple times.
+ self.do_single_write(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ protocol_version,
+ permanent_resource_id=True,
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_parameter_drop_client_write(self, client_type, protocol_version):
+ """Drops the first few transfer initialization packets."""
+ payload = random.Random(67336391945).randbytes(1234)
+ config = TransferConfig(
+ self.default_server_config(),
+ self.default_client_config(),
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} },
+ { keep_drop_queue: {keep_drop_queue: [2, 1, -1]} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} },
+ { keep_drop_queue: {keep_drop_queue: [1, 2, -1]} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ # Resource ID is arbitrary, but deliberately set to be >2 bytes.
+ resource_id = 597419
+ self.do_single_write(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+
+if __name__ == '__main__':
+ test_fixture.run_tests_for(MediumTransferWriteIntegrationTest)
diff --git a/pw_transfer/integration_test/cross_language_small_test.py b/pw_transfer/integration_test/cross_language_small_test.py
new file mode 100644
index 000000000..cfda8f0b4
--- /dev/null
+++ b/pw_transfer/integration_test/cross_language_small_test.py
@@ -0,0 +1,140 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Cross-language pw_transfer tests that are as small/fast as possible.
+
+Usage:
+
+ bazel run pw_transfer/integration_test:cross_language_small_test
+
+Command-line arguments must be provided after a double-dash:
+
+ bazel run pw_transfer/integration_test:cross_language_small_test -- \
+ --server-port 3304
+
+Which tests to run can be specified as command-line arguments:
+
+ bazel run pw_transfer/integration_test:cross_language_small_test -- \
+ SmallTransferIntegrationTest.test_single_byte_client_write_1_java
+
+"""
+
+import itertools
+from parameterized import parameterized
+
+from pigweed.pw_transfer.integration_test import config_pb2
+import test_fixture
+from test_fixture import TransferIntegrationTestHarness
+
+_ALL_LANGUAGES = ("cpp", "java", "python")
+_ALL_VERSIONS = (
+ config_pb2.TransferAction.ProtocolVersion.V1,
+ config_pb2.TransferAction.ProtocolVersion.V2,
+)
+_ALL_LANGUAGES_AND_VERSIONS = tuple(
+ itertools.product(_ALL_LANGUAGES, _ALL_VERSIONS)
+)
+
+
+class SmallTransferIntegrationTest(test_fixture.TransferIntegrationTest):
+ # Each set of transfer tests uses a different client/server port pair to
+ # allow tests to be run in parallel.
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ server_port=3302, client_port=3303
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_empty_client_write(self, client_type, protocol_version):
+ payload = b""
+ config = self.default_config()
+ resource_id = 5
+
+ # Packet drops can cause the resource ID for this to be opened/closed
+ # multiple times due to the zero-size transfer. Use a
+ # permanent_resource_id so the retry process can succeed and the harness
+ # won't flake.
+ self.do_single_write(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ protocol_version,
+ permanent_resource_id=True,
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_null_byte_client_write(self, client_type, protocol_version):
+ payload = b"\0"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_write(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_single_byte_client_write(self, client_type, protocol_version):
+ payload = b"?"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_write(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_small_client_write(self, client_type, protocol_version):
+ payload = b"some data"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_write(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_empty_client_read(self, client_type, protocol_version):
+ payload = b""
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_null_byte_client_read(self, client_type, protocol_version):
+ payload = b"\0"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_single_byte_client_read(self, client_type, protocol_version):
+ payload = b"?"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+ @parameterized.expand(_ALL_LANGUAGES_AND_VERSIONS)
+ def test_small_client_read(self, client_type, protocol_version):
+ payload = b"some data"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ client_type, config, resource_id, payload, protocol_version
+ )
+
+
+if __name__ == '__main__':
+ test_fixture.run_tests_for(SmallTransferIntegrationTest)
diff --git a/pw_transfer/integration_test/expected_errors_test.py b/pw_transfer/integration_test/expected_errors_test.py
new file mode 100644
index 000000000..64d90a390
--- /dev/null
+++ b/pw_transfer/integration_test/expected_errors_test.py
@@ -0,0 +1,336 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Cross-language pw_transfer tests that verify failure modes.
+
+Note: these tests DO NOT work with older integration test binaries that only
+support the legacy transfer protocol.
+
+Usage:
+
+ bazel run pw_transfer/integration_test:expected_errors_test
+
+Command-line arguments must be provided after a double-dash:
+
+ bazel run pw_transfer/integration_test:expected_errors_test -- \
+ --server-port 3304
+
+Which tests to run can be specified as command-line arguments:
+
+ bazel run pw_transfer/integration_test:expected_errors_test -- \
+ ErrorTransferIntegrationTest.test_write_to_unknown_id
+
+"""
+
+import asyncio
+from parameterized import parameterized
+import random
+import tempfile
+
+from google.protobuf import text_format
+
+from pigweed.pw_transfer.integration_test import config_pb2
+from pigweed.pw_protobuf.pw_protobuf_protos import status_pb2
+import test_fixture
+from test_fixture import TransferIntegrationTestHarness, TransferConfig
+
+
+class ErrorTransferIntegrationTest(test_fixture.TransferIntegrationTest):
+ # Each set of transfer tests uses a different client/server port pair to
+ # allow tests to be run in parallel.
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ server_port=3312, client_port=3313
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_write_to_unknown_id(self, client_type):
+ payload = b"Rabbits are the best pets"
+ config = self.default_config()
+ resource_id = 5
+
+ with tempfile.NamedTemporaryFile() as f_payload, tempfile.NamedTemporaryFile() as f_server_output:
+ # Add the resource at a different resource ID.
+ config.server.resources[resource_id + 1].destination_paths.append(
+ f_server_output.name
+ )
+ config.client.transfer_actions.append(
+ config_pb2.TransferAction(
+ resource_id=resource_id,
+ file_path=f_payload.name,
+ transfer_type=config_pb2.TransferAction.TransferType.WRITE_TO_SERVER,
+ expected_status=status_pb2.StatusCode.NOT_FOUND,
+ )
+ )
+
+ f_payload.write(payload)
+ f_payload.flush() # Ensure contents are there to read!
+ exit_codes = asyncio.run(
+ self.harness.perform_transfers(
+ config.server, client_type, config.client, config.proxy
+ )
+ )
+
+ self.assertEqual(exit_codes.client, 0)
+ self.assertEqual(exit_codes.server, 0)
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_client_write_timeout(self, client_type):
+ payload = random.Random(67336391945).randbytes(4321)
+ config = TransferConfig(
+ self.default_server_config(),
+ self.default_client_config(),
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} },
+ { server_failure: {packets_before_failure: [5]} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ resource_id = 987654321
+
+ # This test deliberately tries to time out the transfer, so because of
+ # the retry process the resource ID might be re-initialized multiple
+ # times.
+ self.do_single_write(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ permanent_resource_id=True,
+ expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED,
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_server_write_timeout(self, client_type):
+ payload = random.Random(67336391945).randbytes(4321)
+ config = TransferConfig(
+ self.default_server_config(),
+ self.default_client_config(),
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} },
+ { server_failure: {packets_before_failure: [5]} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ resource_id = 987654321
+
+ # This test deliberately tries to time out the transfer, so because of
+ # the retry process the resource ID might be re-initialized multiple
+ # times.
+ self.do_single_write(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ permanent_resource_id=True,
+ expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED,
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_read_from_unknown_id(self, client_type):
+ payload = b"Rabbits are the best pets"
+ config = self.default_config()
+ resource_id = 5
+
+ with tempfile.NamedTemporaryFile() as f_payload, tempfile.NamedTemporaryFile() as f_client_output:
+ # Add the resource at a different resource ID.
+ config.server.resources[resource_id + 1].source_paths.append(
+ f_payload.name
+ )
+ config.client.transfer_actions.append(
+ config_pb2.TransferAction(
+ resource_id=resource_id,
+ file_path=f_client_output.name,
+ transfer_type=config_pb2.TransferAction.TransferType.READ_FROM_SERVER,
+ expected_status=status_pb2.StatusCode.NOT_FOUND,
+ )
+ )
+
+ f_payload.write(payload)
+ f_payload.flush() # Ensure contents are there to read!
+ exit_codes = asyncio.run(
+ self.harness.perform_transfers(
+ config.server, client_type, config.client, config.proxy
+ )
+ )
+
+ self.assertEqual(exit_codes.client, 0)
+ self.assertEqual(exit_codes.server, 0)
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_client_read_timeout(self, client_type):
+ payload = random.Random(67336391945).randbytes(4321)
+ config = TransferConfig(
+ self.default_server_config(),
+ self.default_client_config(),
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} },
+ { server_failure: {packets_before_failure: [5]} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ resource_id = 987654321
+
+ # This test deliberately tries to time out the transfer, so because of
+ # the retry process the resource ID might be re-initialized multiple
+ # times.
+ self.do_single_read(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ permanent_resource_id=True,
+ expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED,
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_server_read_timeout(self, client_type):
+ payload = random.Random(67336391945).randbytes(4321)
+ config = TransferConfig(
+ self.default_server_config(),
+ self.default_client_config(),
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} },
+ { server_failure: {packets_before_failure: [5]} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ resource_id = 987654321
+
+ # This test deliberately tries to time out the transfer, so because of
+ # the retry process the resource ID might be re-initialized multiple
+ # times.
+ self.do_single_read(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ permanent_resource_id=True,
+ expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED,
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_data_drop_client_lifetime_timeout(self, client_type):
+ """Drops the first data chunk of a transfer but allows the rest."""
+ payload = random.Random(67336391945).randbytes(1234)
+
+ # This test is expected to hit the lifetime retry count, so make it
+ # reasonable.
+ client_config = self.default_client_config()
+ client_config.max_lifetime_retries = 20
+ client_config.chunk_timeout_ms = 1000
+
+ config = TransferConfig(
+ self.default_server_config(),
+ client_config,
+ text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} },
+ { window_packet_dropper: { window_packet_to_drop: 0 } }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} },
+ { window_packet_dropper: { window_packet_to_drop: 0 } }
+ ]""",
+ config_pb2.ProxyConfig(),
+ ),
+ )
+ # Resource ID is arbitrary, but deliberately set to be >1 byte.
+ resource_id = 7332
+
+ # This test deliberately tries to time out the transfer, so because of
+ # the retry process the resource ID might be re-initialized multiple
+ # times.
+ self.do_single_read(
+ client_type,
+ config,
+ resource_id,
+ payload,
+ permanent_resource_id=True,
+ expected_status=status_pb2.StatusCode.DEADLINE_EXCEEDED,
+ )
+
+
+if __name__ == '__main__':
+ test_fixture.run_tests_for(ErrorTransferIntegrationTest)
diff --git a/pw_transfer/integration_test/legacy_binaries_test.py b/pw_transfer/integration_test/legacy_binaries_test.py
new file mode 100644
index 000000000..7c6b8aa99
--- /dev/null
+++ b/pw_transfer/integration_test/legacy_binaries_test.py
@@ -0,0 +1,279 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""A variety of transfer tests that validate backwards compatibility.
+
+Usage:
+
+ bazel run pw_transfer/integration_test:legacy_binaries_test
+
+Command-line arguments must be provided after a double-dash:
+
+ bazel run pw_transfer/integration_test:legacy_binaries_test -- \
+ --server-port 3304
+
+Which tests to run can be specified as command-line arguments:
+
+ bazel run pw_transfer/integration_test:legacy_binaries_test -- \
+ LegacyClientTransferIntegrationTests.test_small_client_write_0_cpp
+
+"""
+
+import itertools
+from parameterized import parameterized
+import random
+
+from pigweed.pw_transfer.integration_test import config_pb2
+import test_fixture
+from test_fixture import TransferIntegrationTestHarness
+from rules_python.python.runfiles import runfiles
+
+# Each set of transfer tests uses a different client/server port pair to
+# allow tests to be run in parallel.
+_SERVER_PORT = 3314
+_CLIENT_PORT = 3315
+
+
+# NOTE: These backwards compatibility tests DO NOT include tests that verify
+# expected error cases (e.g. timeouts, unknown resource ID) because legacy
+# integration test clients did not support the ability to check those.
+# Additionally, there are deliberately NOT back-to-back read/write transfers
+# because of known issues with transfer cleanup in the legacy transfer protocol.
+class LegacyTransferIntegrationTest(test_fixture.TransferIntegrationTest):
+ """This base class defines the tests to run, but isn't run directly."""
+
+ # Explicitly use UNKNOWN_VERSION (the default value of
+ # TransferAction.protocol_version), as that will cause protocol_version to
+ # be omitted from the generated text proto, which is critical for the legacy
+ # client that doesn't support transfer version specifications (and will
+ # cause a proto parse error).
+ PROTOCOL_VERSION = config_pb2.TransferAction.ProtocolVersion.UNKNOWN_VERSION
+ LEGACY_SERVER = False
+ LEGACY_CLIENT = False
+
+ def default_config(self) -> test_fixture.TransferConfig:
+ # The legacy binaries aren't aware of the max_lifetime_retries field,
+ # which was added more recently. Clear it so it isn't encoded into the
+ # serialized message.
+ config = super().default_config()
+ config.client.max_lifetime_retries = 0
+ return config
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_single_byte_client_write(self, client_type):
+ if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
+ self.skipTest("No legacy binary in use, skipping")
+
+ if not self.LEGACY_SERVER and (
+ client_type == "java" or client_type == "python"
+ ):
+ self.skipTest("Java and Python legacy clients not yet set up")
+
+ payload = b"?"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_write(
+ "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_small_client_write(self, client_type):
+ if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
+ self.skipTest("No legacy binary in use, skipping")
+
+ if not self.LEGACY_SERVER and (
+ client_type == "java" or client_type == "python"
+ ):
+ self.skipTest("Java and Python legacy clients not yet set up")
+
+ payload = b"some data"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_write(
+ "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_medium_hdlc_escape_client_write(self, client_type):
+ if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
+ self.skipTest("No legacy binary in use, skipping")
+
+ if not self.LEGACY_SERVER and (
+ client_type == "java" or client_type == "python"
+ ):
+ self.skipTest("Java and Python legacy clients not yet set up")
+
+ payload = b"~" * 8731
+ config = self.default_config()
+ resource_id = 12345678
+ self.do_single_write(
+ "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_medium_random_data_client_write(self, client_type):
+ if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
+ self.skipTest("No legacy binary in use, skipping")
+
+ if not self.LEGACY_SERVER and (
+ client_type == "java" or client_type == "python"
+ ):
+ self.skipTest("Java and Python legacy clients not yet set up")
+
+ rng = random.Random(1533659510898)
+ payload = rng.randbytes(13713)
+ config = self.default_config()
+ resource_id = 12345678
+ self.do_single_write(
+ "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_single_byte_client_read(self, client_type):
+ if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
+ self.skipTest("No legacy binary in use, skipping")
+
+ if not self.LEGACY_SERVER and (
+ client_type == "java" or client_type == "python"
+ ):
+ self.skipTest("Java and Python legacy clients not yet set up")
+
+ payload = b"?"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_small_client_read(self, client_type):
+ if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
+ self.skipTest("No legacy binary in use, skipping")
+
+ if not self.LEGACY_SERVER and (
+ client_type == "java" or client_type == "python"
+ ):
+ self.skipTest("Java and Python legacy clients not yet set up")
+
+ payload = b"some data"
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_medium_hdlc_escape_client_read(self, client_type):
+ if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
+ self.skipTest("No legacy binary in use, skipping")
+
+ if self.LEGACY_SERVER:
+ self.skipTest("Legacy server has HDLC buffer sizing issues")
+
+ payload = b"~" * 8731
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_medium_random_data_client_read(self, client_type):
+ if not (self.LEGACY_SERVER or self.LEGACY_CLIENT):
+ self.skipTest("No legacy binary in use, skipping")
+
+ if self.LEGACY_SERVER:
+ self.skipTest("Legacy server has HDLC buffer sizing issues")
+
+ rng = random.Random(1533659510898)
+ payload = rng.randbytes(13713)
+ config = self.default_config()
+ resource_id = 5
+ self.do_single_read(
+ "cpp", config, resource_id, payload, self.PROTOCOL_VERSION
+ )
+
+
+class LegacyClientTransferIntegrationTests(LegacyTransferIntegrationTest):
+ r = runfiles.Create()
+ client_binary = r.Rlocation("pw_transfer_test_binaries/cpp_client_528098d5")
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ cpp_client_binary=client_binary,
+ server_port=_SERVER_PORT,
+ client_port=_CLIENT_PORT,
+ )
+ LEGACY_CLIENT = True
+
+
+class LegacyServerTransferIntegrationTests(LegacyTransferIntegrationTest):
+ r = runfiles.Create()
+ server_binary = r.Rlocation("pw_transfer_test_binaries/server_528098d5")
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ server_binary=server_binary,
+ server_port=_SERVER_PORT,
+ client_port=_CLIENT_PORT,
+ )
+ LEGACY_SERVER = True
+
+
+if __name__ == '__main__':
+ test_fixture.run_tests_for(LegacyClientTransferIntegrationTests)
+ test_fixture.run_tests_for(LegacyServerTransferIntegrationTests)
diff --git a/pw_transfer/integration_test/multi_transfer_test.py b/pw_transfer/integration_test/multi_transfer_test.py
new file mode 100644
index 000000000..88ec079c1
--- /dev/null
+++ b/pw_transfer/integration_test/multi_transfer_test.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Cross-language pw_transfer tests that do several transfers per session.
+
+Usage:
+
+ bazel run pw_transfer/integration_test:multi_transfer_test
+
+Command-line arguments must be provided after a double-dash:
+
+ bazel run pw_transfer/integration_test:multi_transfer_test -- \
+ --server-port 3304
+
+Which tests to run can be specified as command-line arguments:
+
+ bazel run pw_transfer/integration_test:multi_transfer_test -- \
+ MultiTransferIntegrationTest.test_write_to_same_id_1_java
+
+"""
+
+from parameterized import parameterized
+import random
+from typing import List
+
+import test_fixture
+from test_fixture import TransferIntegrationTestHarness, BasicTransfer
+from pigweed.pw_transfer.integration_test import config_pb2
+
+
+class MultiTransferIntegrationTest(test_fixture.TransferIntegrationTest):
+ # Each set of transfer tests uses a different client/server port pair to
+ # allow tests to be run in parallel.
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config(
+ server_port=3308, client_port=3309
+ )
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_write_to_same_id(self, client_type):
+ rng = random.Random(1533659510898)
+ config = self.default_config()
+ resource_id = 5
+ transfers: List[BasicTransfer] = []
+ for i in range(1, 6):
+ transfers.append(
+ BasicTransfer(
+ id=resource_id,
+ type=config_pb2.TransferAction.TransferType.WRITE_TO_SERVER,
+ data=rng.randbytes(rng.randrange(213, 1111)),
+ )
+ )
+
+ self.do_basic_transfer_sequence(client_type, config, transfers)
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_read_from_same_id(self, client_type):
+ rng = random.Random(1533659510898)
+ config = self.default_config()
+ resource_id = 5
+ transfers: List[BasicTransfer] = []
+ for i in range(1, 6):
+ transfers.append(
+ BasicTransfer(
+ id=resource_id,
+ type=config_pb2.TransferAction.TransferType.READ_FROM_SERVER,
+ data=rng.randbytes(rng.randrange(213, 1111)),
+ )
+ )
+
+ self.do_basic_transfer_sequence(client_type, config, transfers)
+
+ @parameterized.expand(
+ [
+ ("cpp"),
+ ("java"),
+ ("python"),
+ ]
+ )
+ def test_read_write_with_same_id(self, client_type):
+ rng = random.Random(1533659510898)
+ config = self.default_config()
+ resource_id = 53333333
+ transfers: List[BasicTransfer] = []
+ for i in range(1, 6):
+ transfer_type = (
+ config_pb2.TransferAction.TransferType.READ_FROM_SERVER
+ if i % 2 == 0
+ else config_pb2.TransferAction.TransferType.WRITE_TO_SERVER
+ )
+ transfers.append(
+ BasicTransfer(
+ id=resource_id,
+ type=config_pb2.TransferAction.TransferType.READ_FROM_SERVER,
+ data=rng.randbytes(rng.randrange(213, 1111)),
+ )
+ )
+
+ self.do_basic_transfer_sequence(client_type, config, transfers)
+
+
+if __name__ == '__main__':
+ test_fixture.run_tests_for(MultiTransferIntegrationTest)
diff --git a/pw_transfer/integration_test/proxy.py b/pw_transfer/integration_test/proxy.py
new file mode 100644
index 000000000..d9df6555c
--- /dev/null
+++ b/pw_transfer/integration_test/proxy.py
@@ -0,0 +1,637 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Proxy for transfer integration testing.
+
+This module contains a proxy for transfer intergation testing. It is capable
+of introducing various link failures into the connection between the client and
+server.
+"""
+
+import abc
+import argparse
+import asyncio
+from enum import Enum
+import logging
+import random
+import socket
+import sys
+import time
+from typing import Any, Awaitable, Callable, Iterable, List, Optional
+
+from google.protobuf import text_format
+
+from pigweed.pw_rpc.internal import packet_pb2
+from pigweed.pw_transfer import transfer_pb2
+from pigweed.pw_transfer.integration_test import config_pb2
+from pw_hdlc import decode
+from pw_transfer.chunk import Chunk
+
+_LOG = logging.getLogger('pw_transfer_intergration_test_proxy')
+
+# This is the maximum size of the socket receive buffers. Ideally, this is set
+# to the lowest allowed value to minimize buffering between the proxy and
+# clients so rate limiting causes the client to block and wait for the
+# integration test proxy to drain rather than allowing OS buffers to backlog
+# large quantities of data.
+#
+# Note that the OS may chose to not strictly follow this requested buffer size.
+# Still, setting this value to be relatively small does reduce bufer sizes
+# significantly enough to better reflect typical inter-device communication.
+#
+# For this to be effective, clients should also configure their sockets to a
+# smaller send buffer size.
+_RECEIVE_BUFFER_SIZE = 2048
+
+
+class Event(Enum):
+ TRANSFER_START = 1
+ PARAMETERS_RETRANSMIT = 2
+ PARAMETERS_CONTINUE = 3
+ START_ACK_CONFIRMATION = 4
+
+
+class Filter(abc.ABC):
+ """An abstract interface for manipulating a stream of data.
+
+ ``Filter``s are used to implement various transforms to simulate real
+ world link properties. Some examples include: data corruption,
+ packet loss, packet reordering, rate limiting, latency modeling.
+
+ A ``Filter`` implementation should implement the ``process`` method
+ and call ``self.send_data()`` when it has data to send.
+ """
+
+ def __init__(self, send_data: Callable[[bytes], Awaitable[None]]):
+ self.send_data = send_data
+ pass
+
+ @abc.abstractmethod
+ async def process(self, data: bytes) -> None:
+ """Processes incoming data.
+
+ Implementations of this method may send arbitrary data, or none, using
+ the ``self.send_data()`` handler.
+ """
+
+ async def __call__(self, data: bytes) -> None:
+ await self.process(data)
+
+
+class HdlcPacketizer(Filter):
+ """A filter which aggregates data into complete HDLC packets.
+
+ Since the proxy transport (SOCK_STREAM) has no framing and we want some
+ filters to operates on whole frames, this filter can be used so that
+ downstream filters see whole frames.
+ """
+
+ def __init__(self, send_data: Callable[[bytes], Awaitable[None]]):
+ super().__init__(send_data)
+ self.decoder = decode.FrameDecoder()
+
+ async def process(self, data: bytes) -> None:
+ for frame in self.decoder.process(data):
+ await self.send_data(frame.raw_encoded)
+
+
+class DataDropper(Filter):
+ """A filter which drops some data.
+
+ DataDropper will drop data passed through ``process()`` at the
+ specified ``rate``.
+ """
+
+ def __init__(
+ self,
+ send_data: Callable[[bytes], Awaitable[None]],
+ name: str,
+ rate: float,
+ seed: Optional[int] = None,
+ ):
+ super().__init__(send_data)
+ self._rate = rate
+ self._name = name
+ if seed == None:
+ seed = time.time_ns()
+ self._rng = random.Random(seed)
+ _LOG.info(f'{name} DataDropper initialized with seed {seed}')
+
+ async def process(self, data: bytes) -> None:
+ if self._rng.uniform(0.0, 1.0) < self._rate:
+ _LOG.info(f'{self._name} dropped {len(data)} bytes of data')
+ else:
+ await self.send_data(data)
+
+
+class KeepDropQueue(Filter):
+ """A filter which alternates between sending packets and dropping packets.
+
+ A KeepDropQueue filter will alternate between keeping packets and dropping
+ chunks of data based on a keep/drop queue provided during its creation. The
+ queue is looped over unless a negative element is found. A negative number
+ is effectively the same as a value of infinity.
+
+ This filter is typically most pratical when used with a packetizer so data
+ can be dropped as distinct packets.
+
+ Examples:
+
+ keep_drop_queue = [3, 2]:
+ Keeps 3 packets,
+ Drops 2 packets,
+ Keeps 3 packets,
+ Drops 2 packets,
+ ... [loops indefinitely]
+
+ keep_drop_queue = [5, 99, 1, -1]:
+ Keeps 5 packets,
+ Drops 99 packets,
+ Keeps 1 packet,
+ Drops all further packets.
+ """
+
+ def __init__(
+ self,
+ send_data: Callable[[bytes], Awaitable[None]],
+ name: str,
+ keep_drop_queue: Iterable[int],
+ ):
+ super().__init__(send_data)
+ self._keep_drop_queue = list(keep_drop_queue)
+ self._loop_idx = 0
+ self._current_count = self._keep_drop_queue[0]
+ self._keep = True
+ self._name = name
+
+ async def process(self, data: bytes) -> None:
+ # Move forward through the queue if neeeded.
+ while self._current_count == 0:
+ self._loop_idx += 1
+ self._current_count = self._keep_drop_queue[
+ self._loop_idx % len(self._keep_drop_queue)
+ ]
+ self._keep = not self._keep
+
+ if self._current_count > 0:
+ self._current_count -= 1
+
+ if self._keep:
+ await self.send_data(data)
+ _LOG.info(f'{self._name} forwarded {len(data)} bytes of data')
+ else:
+ _LOG.info(f'{self._name} dropped {len(data)} bytes of data')
+
+
+class RateLimiter(Filter):
+ """A filter which limits transmission rate.
+
+ This filter delays transmission of data by len(data)/rate.
+ """
+
+ def __init__(
+ self, send_data: Callable[[bytes], Awaitable[None]], rate: float
+ ):
+ super().__init__(send_data)
+ self._rate = rate
+
+ async def process(self, data: bytes) -> None:
+ delay = len(data) / self._rate
+ await asyncio.sleep(delay)
+ await self.send_data(data)
+
+
+class DataTransposer(Filter):
+ """A filter which occasionally transposes two chunks of data.
+
+ This filter transposes data at the specified rate. It does this by
+ holding a chunk to transpose until another chunk arrives. The filter
+ will not hold a chunk longer than ``timeout`` seconds.
+ """
+
+ def __init__(
+ self,
+ send_data: Callable[[bytes], Awaitable[None]],
+ name: str,
+ rate: float,
+ timeout: float,
+ seed: int,
+ ):
+ super().__init__(send_data)
+ self._name = name
+ self._rate = rate
+ self._timeout = timeout
+ self._data_queue = asyncio.Queue()
+ self._rng = random.Random(seed)
+ self._transpose_task = asyncio.create_task(self._transpose_handler())
+
+ _LOG.info(f'{name} DataTranspose initialized with seed {seed}')
+
+ def __del__(self):
+ _LOG.info(f'{self._name} cleaning up transpose task.')
+ self._transpose_task.cancel()
+
+ async def _transpose_handler(self):
+ """Async task that handles the packet transposition and timeouts"""
+ held_data: Optional[bytes] = None
+ while True:
+ # Only use timeout if we have data held for transposition
+ timeout = None if held_data is None else self._timeout
+ try:
+ data = await asyncio.wait_for(
+ self._data_queue.get(), timeout=timeout
+ )
+
+ if held_data is not None:
+ # If we have held data, send it out of order.
+ await self.send_data(data)
+ await self.send_data(held_data)
+ held_data = None
+ else:
+ # Otherwise decide if we should transpose the current data.
+ if self._rng.uniform(0.0, 1.0) < self._rate:
+ _LOG.info(
+ f'{self._name} transposing {len(data)} bytes of data'
+ )
+ held_data = data
+ else:
+ await self.send_data(data)
+
+ except asyncio.TimeoutError:
+ _LOG.info(f'{self._name} sending data in order due to timeout')
+ await self.send_data(held_data)
+ held_data = None
+
+ async def process(self, data: bytes) -> None:
+ # Queue data for processing by the transpose task.
+ await self._data_queue.put(data)
+
+
+class ServerFailure(Filter):
+ """A filter to simulate the server stopping sending packets.
+
+ ServerFailure takes a list of numbers of packets to send before
+ dropping all subsequent packets until a TRANSFER_START packet
+ is seen. This process is repeated for each element in
+ packets_before_failure. After that list is exhausted, ServerFailure
+ will send all packets.
+
+ This filter should be instantiated in the same filter stack as an
+ HdlcPacketizer so that EventFilter can decode complete packets.
+ """
+
+ def __init__(
+ self,
+ send_data: Callable[[bytes], Awaitable[None]],
+ name: str,
+ packets_before_failure_list: List[int],
+ ):
+ super().__init__(send_data)
+ self._name = name
+ self._relay_packets = True
+ self._packets_before_failure_list = packets_before_failure_list
+ self.advance_packets_before_failure()
+
+ def advance_packets_before_failure(self):
+ if len(self._packets_before_failure_list) > 0:
+ self._packets_before_failure = (
+ self._packets_before_failure_list.pop(0)
+ )
+ else:
+ self._packets_before_failure = None
+
+ async def process(self, data: bytes) -> None:
+ if self._packets_before_failure is None:
+ await self.send_data(data)
+ elif self._packets_before_failure > 0:
+ self._packets_before_failure -= 1
+ await self.send_data(data)
+
+ def handle_event(self, event: Event) -> None:
+ if event is Event.TRANSFER_START:
+ self.advance_packets_before_failure()
+
+
+class WindowPacketDropper(Filter):
+ """A filter to allow the same packet in each window to be dropped
+
+ WindowPacketDropper with drop the nth packet in each window as
+ specified by window_packet_to_drop. This process will happend
+ indefinitely for each window.
+
+ This filter should be instantiated in the same filter stack as an
+ HdlcPacketizer so that EventFilter can decode complete packets.
+ """
+
+ def __init__(
+ self,
+ send_data: Callable[[bytes], Awaitable[None]],
+ name: str,
+ window_packet_to_drop: int,
+ ):
+ super().__init__(send_data)
+ self._name = name
+ self._relay_packets = True
+ self._window_packet_to_drop = window_packet_to_drop
+ self._window_packet = 0
+
+ async def process(self, data: bytes) -> None:
+ try:
+ is_data_chunk = (
+ _extract_transfer_chunk(data).type is Chunk.Type.DATA
+ )
+ except Exception:
+ # Invalid / non-chunk data (e.g. text logs); ignore.
+ is_data_chunk = False
+
+ # Only count transfer data chunks as part of a window.
+ if is_data_chunk:
+ if self._window_packet != self._window_packet_to_drop:
+ await self.send_data(data)
+
+ self._window_packet += 1
+ else:
+ await self.send_data(data)
+
+ def handle_event(self, event: Event) -> None:
+ if event in (
+ Event.PARAMETERS_RETRANSMIT,
+ Event.PARAMETERS_CONTINUE,
+ Event.START_ACK_CONFIRMATION,
+ ):
+ self._window_packet = 0
+
+
+class EventFilter(Filter):
+ """A filter that inspects packets and send events to other filters.
+
+ This filter should be instantiated in the same filter stack as an
+ HdlcPacketizer so that it can decode complete packets.
+ """
+
+ def __init__(
+ self,
+ send_data: Callable[[bytes], Awaitable[None]],
+ name: str,
+ event_queue: asyncio.Queue,
+ ):
+ super().__init__(send_data)
+ self._queue = event_queue
+
+ async def process(self, data: bytes) -> None:
+ try:
+ chunk = _extract_transfer_chunk(data)
+ if chunk.type is Chunk.Type.START:
+ await self._queue.put(Event.TRANSFER_START)
+ if chunk.type is Chunk.Type.START_ACK_CONFIRMATION:
+ await self._queue.put(Event.START_ACK_CONFIRMATION)
+ elif chunk.type is Chunk.Type.PARAMETERS_RETRANSMIT:
+ await self._queue.put(Event.PARAMETERS_RETRANSMIT)
+ elif chunk.type is Chunk.Type.PARAMETERS_CONTINUE:
+ await self._queue.put(Event.PARAMETERS_CONTINUE)
+ except:
+ # Silently ignore invalid packets
+ pass
+
+ await self.send_data(data)
+
+
+def _extract_transfer_chunk(data: bytes) -> Chunk:
+ """Gets a transfer Chunk from an HDLC frame containing an RPC packet.
+
+ Raises an exception if a valid chunk does not exist.
+ """
+
+ decoder = decode.FrameDecoder()
+ for frame in decoder.process(data):
+ packet = packet_pb2.RpcPacket()
+ packet.ParseFromString(frame.data)
+ raw_chunk = transfer_pb2.Chunk()
+ raw_chunk.ParseFromString(packet.payload)
+ return Chunk.from_message(raw_chunk)
+
+ raise ValueError("Invalid transfer frame")
+
+
+async def _handle_simplex_events(
+ event_queue: asyncio.Queue, handlers: List[Callable[[Event], None]]
+):
+ while True:
+ event = await event_queue.get()
+ for handler in handlers:
+ handler(event)
+
+
+async def _handle_simplex_connection(
+ name: str,
+ filter_stack_config: List[config_pb2.FilterConfig],
+ reader: asyncio.StreamReader,
+ writer: asyncio.StreamWriter,
+ inbound_event_queue: asyncio.Queue,
+ outbound_event_queue: asyncio.Queue,
+) -> None:
+ """Handle a single direction of a bidirectional connection between
+ server and client."""
+
+ async def send(data: bytes):
+ writer.write(data)
+ await writer.drain()
+
+ filter_stack = EventFilter(send, name, outbound_event_queue)
+
+ event_handlers: List[Callable[[Event], None]] = []
+
+ # Build the filter stack from the bottom up
+ for config in reversed(filter_stack_config):
+ filter_name = config.WhichOneof("filter")
+ if filter_name == "hdlc_packetizer":
+ filter_stack = HdlcPacketizer(filter_stack)
+ elif filter_name == "data_dropper":
+ data_dropper = config.data_dropper
+ filter_stack = DataDropper(
+ filter_stack, name, data_dropper.rate, data_dropper.seed
+ )
+ elif filter_name == "rate_limiter":
+ filter_stack = RateLimiter(filter_stack, config.rate_limiter.rate)
+ elif filter_name == "data_transposer":
+ transposer = config.data_transposer
+ filter_stack = DataTransposer(
+ filter_stack,
+ name,
+ transposer.rate,
+ transposer.timeout,
+ transposer.seed,
+ )
+ elif filter_name == "server_failure":
+ server_failure = config.server_failure
+ filter_stack = ServerFailure(
+ filter_stack, name, server_failure.packets_before_failure
+ )
+ event_handlers.append(filter_stack.handle_event)
+ elif filter_name == "keep_drop_queue":
+ keep_drop_queue = config.keep_drop_queue
+ filter_stack = KeepDropQueue(
+ filter_stack, name, keep_drop_queue.keep_drop_queue
+ )
+ elif filter_name == "window_packet_dropper":
+ window_packet_dropper = config.window_packet_dropper
+ filter_stack = WindowPacketDropper(
+ filter_stack, name, window_packet_dropper.window_packet_to_drop
+ )
+ event_handlers.append(filter_stack.handle_event)
+ else:
+ sys.exit(f'Unknown filter {filter_name}')
+
+ event_task = asyncio.create_task(
+ _handle_simplex_events(inbound_event_queue, event_handlers)
+ )
+
+ while True:
+ # Arbitrarily chosen "page sized" read.
+ data = await reader.read(4096)
+
+ # An empty data indicates that the connection is closed.
+ if not data:
+ _LOG.info(f'{name} connection closed.')
+ return
+
+ await filter_stack.process(data)
+
+
+async def _handle_connection(
+ server_port: int,
+ config: config_pb2.ProxyConfig,
+ client_reader: asyncio.StreamReader,
+ client_writer: asyncio.StreamWriter,
+) -> None:
+ """Handle a connection between server and client."""
+
+ client_addr = client_writer.get_extra_info('peername')
+ _LOG.info(f'New client connection from {client_addr}')
+
+ # Open a new connection to the server for each client connection.
+ #
+ # TODO(konkers): catch exception and close client writer
+ server_reader, server_writer = await asyncio.open_connection(
+ 'localhost', server_port
+ )
+ _LOG.info(f'New connection opened to server')
+
+ # Queues for the simplex connections to pass events to each other.
+ server_event_queue = asyncio.Queue()
+ client_event_queue = asyncio.Queue()
+
+ # Instantiate two simplex handler one for each direction of the connection.
+ _, pending = await asyncio.wait(
+ [
+ asyncio.create_task(
+ _handle_simplex_connection(
+ "client",
+ config.client_filter_stack,
+ client_reader,
+ server_writer,
+ server_event_queue,
+ client_event_queue,
+ )
+ ),
+ asyncio.create_task(
+ _handle_simplex_connection(
+ "server",
+ config.server_filter_stack,
+ server_reader,
+ client_writer,
+ client_event_queue,
+ server_event_queue,
+ )
+ ),
+ ],
+ return_when=asyncio.FIRST_COMPLETED,
+ )
+
+ # When one side terminates the connection, also terminate the other side
+ for task in pending:
+ task.cancel()
+
+ for stream in [client_writer, server_writer]:
+ stream.close()
+
+
+def _parse_args() -> argparse.Namespace:
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ required=True,
+ help='Port of the integration test server. The proxy will forward connections to this port',
+ )
+ parser.add_argument(
+ '--client-port',
+ type=int,
+ required=True,
+ help='Port on which to listen for connections from integration test client.',
+ )
+
+ return parser.parse_args()
+
+
+def _init_logging(level: int) -> None:
+ _LOG.setLevel(logging.DEBUG)
+ log_to_stderr = logging.StreamHandler()
+ log_to_stderr.setLevel(level)
+ log_to_stderr.setFormatter(
+ logging.Formatter(
+ fmt='%(asctime)s.%(msecs)03d-%(levelname)s: %(message)s',
+ datefmt='%H:%M:%S',
+ )
+ )
+
+ _LOG.addHandler(log_to_stderr)
+
+
+async def _main(server_port: int, client_port: int) -> None:
+ _init_logging(logging.DEBUG)
+
+ # Load config from stdin using synchronous IO
+ text_config = sys.stdin.buffer.read()
+
+ config = text_format.Parse(text_config, config_pb2.ProxyConfig())
+
+ # Instantiate the TCP server.
+ server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ server_socket.setsockopt(
+ socket.SOL_SOCKET, socket.SO_RCVBUF, _RECEIVE_BUFFER_SIZE
+ )
+ server_socket.bind(('localhost', client_port))
+ server = await asyncio.start_server(
+ lambda reader, writer: _handle_connection(
+ server_port, config, reader, writer
+ ),
+ limit=_RECEIVE_BUFFER_SIZE,
+ sock=server_socket,
+ )
+
+ addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets)
+ _LOG.info(f'Listening for client connection on {addrs}')
+
+ # Run the TCP server.
+ async with server:
+ await server.serve_forever()
+
+
+if __name__ == '__main__':
+ asyncio.run(_main(**vars(_parse_args())))
diff --git a/pw_transfer/integration_test/proxy_test.py b/pw_transfer/integration_test/proxy_test.py
new file mode 100644
index 000000000..5a0599c96
--- /dev/null
+++ b/pw_transfer/integration_test/proxy_test.py
@@ -0,0 +1,261 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Unit test for proxy.py"""
+
+import abc
+import asyncio
+from struct import pack
+import time
+from typing import List
+import unittest
+
+from pigweed.pw_rpc.internal import packet_pb2
+from pigweed.pw_transfer import transfer_pb2
+from pw_hdlc import encode
+from pw_transfer import ProtocolVersion
+from pw_transfer.chunk import Chunk
+
+import proxy
+
+
+class MockRng(abc.ABC):
+ def __init__(self, results: List[float]):
+ self._results = results
+
+ def uniform(self, from_val: float, to_val: float) -> float:
+ val_range = to_val - from_val
+ val = self._results.pop()
+ val *= val_range
+ val += from_val
+ return val
+
+
+class ProxyTest(unittest.IsolatedAsyncioTestCase):
+ async def test_transposer_simple(self):
+ sent_packets: List[bytes] = []
+
+ # Async helper so DataTransposer can await on it.
+ async def append(list: List[bytes], data: bytes):
+ list.append(data)
+
+ transposer = proxy.DataTransposer(
+ lambda data: append(sent_packets, data),
+ name="test",
+ rate=0.5,
+ timeout=100,
+ seed=1234567890,
+ )
+ transposer._rng = MockRng([0.6, 0.4])
+ await transposer.process(b'aaaaaaaaaa')
+ await transposer.process(b'bbbbbbbbbb')
+
+ # Give the transposer task time to process the data.
+ await asyncio.sleep(0.05)
+
+ self.assertEqual(sent_packets, [b'bbbbbbbbbb', b'aaaaaaaaaa'])
+
+ async def test_transposer_timeout(self):
+ sent_packets: List[bytes] = []
+
+ # Async helper so DataTransposer can await on it.
+ async def append(list: List[bytes], data: bytes):
+ list.append(data)
+
+ transposer = proxy.DataTransposer(
+ lambda data: append(sent_packets, data),
+ name="test",
+ rate=0.5,
+ timeout=0.100,
+ seed=1234567890,
+ )
+ transposer._rng = MockRng([0.4, 0.6])
+ await transposer.process(b'aaaaaaaaaa')
+
+ # Even though this should be transposed, there is no following data so
+ # the transposer should timout and send this in-order.
+ await transposer.process(b'bbbbbbbbbb')
+
+ # Give the transposer time to timeout.
+ await asyncio.sleep(0.5)
+
+ self.assertEqual(sent_packets, [b'aaaaaaaaaa', b'bbbbbbbbbb'])
+
+ async def test_server_failure(self):
+ sent_packets: List[bytes] = []
+
+ # Async helper so DataTransposer can await on it.
+ async def append(list: List[bytes], data: bytes):
+ list.append(data)
+
+ packets_before_failure = [1, 2, 3]
+ server_failure = proxy.ServerFailure(
+ lambda data: append(sent_packets, data),
+ name="test",
+ packets_before_failure_list=packets_before_failure.copy(),
+ )
+
+ # After passing the list to ServerFailure, add a test for no
+ # packets dropped
+ packets_before_failure.append(5)
+
+ packets = [
+ b'1',
+ b'2',
+ b'3',
+ b'4',
+ b'5',
+ ]
+
+ for num_packets in packets_before_failure:
+ sent_packets.clear()
+ for packet in packets:
+ await server_failure.process(packet)
+ self.assertEqual(len(sent_packets), num_packets)
+ server_failure.handle_event(proxy.Event.TRANSFER_START)
+
+ async def test_keep_drop_queue_loop(self):
+ sent_packets: List[bytes] = []
+
+ # Async helper so DataTransposer can await on it.
+ async def append(list: List[bytes], data: bytes):
+ list.append(data)
+
+ keep_drop_queue = proxy.KeepDropQueue(
+ lambda data: append(sent_packets, data),
+ name="test",
+ keep_drop_queue=[2, 1, 3],
+ )
+
+ expected_sequence = [
+ b'1',
+ b'2',
+ b'4',
+ b'5',
+ b'6',
+ b'9',
+ ]
+ input_packets = [
+ b'1',
+ b'2',
+ b'3',
+ b'4',
+ b'5',
+ b'6',
+ b'7',
+ b'8',
+ b'9',
+ ]
+
+ for packet in input_packets:
+ await keep_drop_queue.process(packet)
+ self.assertEqual(sent_packets, expected_sequence)
+
+ async def test_keep_drop_queue(self):
+ sent_packets: List[bytes] = []
+
+ # Async helper so DataTransposer can await on it.
+ async def append(list: List[bytes], data: bytes):
+ list.append(data)
+
+ keep_drop_queue = proxy.KeepDropQueue(
+ lambda data: append(sent_packets, data),
+ name="test",
+ keep_drop_queue=[2, 1, 1, -1],
+ )
+
+ expected_sequence = [
+ b'1',
+ b'2',
+ b'4',
+ ]
+ input_packets = [
+ b'1',
+ b'2',
+ b'3',
+ b'4',
+ b'5',
+ b'6',
+ b'7',
+ b'8',
+ b'9',
+ ]
+
+ for packet in input_packets:
+ await keep_drop_queue.process(packet)
+ self.assertEqual(sent_packets, expected_sequence)
+
+ async def test_window_packet_dropper(self):
+ sent_packets: List[bytes] = []
+
+ # Async helper so DataTransposer can await on it.
+ async def append(list: List[bytes], data: bytes):
+ list.append(data)
+
+ window_packet_dropper = proxy.WindowPacketDropper(
+ lambda data: append(sent_packets, data),
+ name="test",
+ window_packet_to_drop=0,
+ )
+
+ packets = [
+ _encode_rpc_frame(
+ Chunk(ProtocolVersion.VERSION_TWO, Chunk.Type.DATA, data=b'1')
+ ),
+ _encode_rpc_frame(
+ Chunk(ProtocolVersion.VERSION_TWO, Chunk.Type.DATA, data=b'2')
+ ),
+ _encode_rpc_frame(
+ Chunk(ProtocolVersion.VERSION_TWO, Chunk.Type.DATA, data=b'3')
+ ),
+ _encode_rpc_frame(
+ Chunk(ProtocolVersion.VERSION_TWO, Chunk.Type.DATA, data=b'4')
+ ),
+ _encode_rpc_frame(
+ Chunk(ProtocolVersion.VERSION_TWO, Chunk.Type.DATA, data=b'5')
+ ),
+ ]
+
+ expected_packets = packets[1:]
+
+ # Test each even twice to assure the filter does not have issues
+ # on new window bondaries.
+ events = [
+ proxy.Event.PARAMETERS_RETRANSMIT,
+ proxy.Event.PARAMETERS_CONTINUE,
+ proxy.Event.PARAMETERS_RETRANSMIT,
+ proxy.Event.PARAMETERS_CONTINUE,
+ ]
+
+ for event in events:
+ sent_packets.clear()
+ for packet in packets:
+ await window_packet_dropper.process(packet)
+ self.assertEqual(sent_packets, expected_packets)
+ window_packet_dropper.handle_event(event)
+
+
+def _encode_rpc_frame(chunk: Chunk) -> bytes:
+ packet = packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.SERVER_STREAM,
+ channel_id=101,
+ service_id=1001,
+ method_id=100001,
+ payload=chunk.to_message().SerializeToString(),
+ ).SerializeToString()
+ return encode.ui_frame(73, packet)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/pw_transfer/integration_test/python_client.py b/pw_transfer/integration_test/python_client.py
new file mode 100644
index 000000000..4c27189b0
--- /dev/null
+++ b/pw_transfer/integration_test/python_client.py
@@ -0,0 +1,156 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Python client for pw_transfer integration test."""
+
+import logging
+import socket
+import sys
+
+from google.protobuf import text_format
+from pw_hdlc.rpc import HdlcRpcClient, default_channels
+from pw_status import Status
+import pw_transfer
+from pigweed.pw_transfer import transfer_pb2
+from pigweed.pw_transfer.integration_test import config_pb2
+
+_LOG = logging.getLogger('pw_transfer_integration_test_python_client')
+_LOG.level = logging.DEBUG
+_LOG.addHandler(logging.StreamHandler(sys.stdout))
+
+HOSTNAME: str = "localhost"
+
+
+def _main() -> int:
+ if len(sys.argv) != 2:
+ _LOG.critical("Usage: PORT")
+ return 1
+
+ # The port is passed via the command line.
+ try:
+ port = int(sys.argv[1])
+ except:
+ _LOG.critical("Invalid port specified.")
+ return 1
+
+ # Load the config from stdin.
+ try:
+ text_config = sys.stdin.buffer.read()
+ config = text_format.Parse(text_config, config_pb2.ClientConfig())
+ except Exception as e:
+ _LOG.critical("Failed to parse config file from stdin: %s", e)
+ return 1
+
+ # Open a connection to the server.
+ try:
+ rpc_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ rpc_socket.connect((HOSTNAME, port))
+ except:
+ _LOG.critical("Failed to connect to server at %s:%d", HOSTNAME, port)
+ return 1
+
+ # Initialize an RPC client over the socket and set up the pw_transfer manager.
+ rpc_client = HdlcRpcClient(
+ lambda: rpc_socket.recv(4096),
+ [transfer_pb2],
+ default_channels(lambda data: rpc_socket.sendall(data)),
+ lambda data: _LOG.info("%s", str(data)),
+ )
+ transfer_service = rpc_client.rpcs().pw.transfer.Transfer
+ transfer_manager = pw_transfer.Manager(
+ transfer_service,
+ default_response_timeout_s=config.chunk_timeout_ms / 1000,
+ initial_response_timeout_s=config.initial_chunk_timeout_ms / 1000,
+ max_retries=config.max_retries,
+ max_lifetime_retries=config.max_lifetime_retries,
+ default_protocol_version=pw_transfer.ProtocolVersion.LATEST,
+ )
+
+ transfer_logger = logging.getLogger('pw_transfer')
+ transfer_logger.setLevel(logging.DEBUG)
+ transfer_logger.addHandler(logging.StreamHandler(sys.stdout))
+
+ # Perform the requested transfer actions.
+ for action in config.transfer_actions:
+ protocol_version = pw_transfer.ProtocolVersion(
+ int(action.protocol_version)
+ )
+
+ # Default to the latest protocol version if none is specified.
+ if protocol_version == pw_transfer.ProtocolVersion.UNKNOWN:
+ protocol_version = pw_transfer.ProtocolVersion.LATEST
+
+ if (
+ action.transfer_type
+ == config_pb2.TransferAction.TransferType.WRITE_TO_SERVER
+ ):
+ try:
+ with open(action.file_path, 'rb') as f:
+ data = f.read()
+ except:
+ _LOG.critical(
+ "Failed to read input file '%s'", action.file_path
+ )
+ return 1
+
+ try:
+ transfer_manager.write(
+ action.resource_id, data, protocol_version=protocol_version
+ )
+ except pw_transfer.client.Error as e:
+ if e.status != Status(action.expected_status):
+ _LOG.exception(
+ "Unexpected error encountered during write transfer"
+ )
+ return 1
+ except:
+ _LOG.exception("Transfer (write to server) failed")
+ return 1
+ elif (
+ action.transfer_type
+ == config_pb2.TransferAction.TransferType.READ_FROM_SERVER
+ ):
+ try:
+ data = transfer_manager.read(
+ action.resource_id, protocol_version=protocol_version
+ )
+ except pw_transfer.client.Error as e:
+ if e.status != Status(action.expected_status):
+ _LOG.exception(
+ "Unexpected error encountered during read transfer"
+ )
+ return 1
+ continue
+ except:
+ _LOG.exception("Transfer (read from server) failed")
+ return 1
+
+ try:
+ with open(action.file_path, 'wb') as f:
+ f.write(data)
+ except:
+ _LOG.critical(
+ "Failed to write output file '%s'", action.file_path
+ )
+ return 1
+ else:
+ _LOG.critical("Unknown transfer type: %d", action.transfer_type)
+ return 1
+
+ _LOG.info("All transfers completed successfully")
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(_main())
diff --git a/pw_transfer/integration_test/server.cc b/pw_transfer/integration_test/server.cc
new file mode 100644
index 000000000..98a29f045
--- /dev/null
+++ b/pw_transfer/integration_test/server.cc
@@ -0,0 +1,237 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+// Simple RPC server with the transfer service registered. Reads HDLC frames
+// with RPC packets through a socket. This server has a single resource ID that
+// is available, and data must be written to the server before data can be read
+// from the resource ID.
+//
+// Usage:
+//
+// integration_test_server 3300 <<< "resource_id: 12 file: '/tmp/gotbytes'"
+
+#include <sys/socket.h>
+
+#include <chrono>
+#include <cstddef>
+#include <cstdlib>
+#include <deque>
+#include <map>
+#include <memory>
+#include <string>
+#include <thread>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "google/protobuf/text_format.h"
+#include "pw_assert/check.h"
+#include "pw_chrono/system_clock.h"
+#include "pw_log/log.h"
+#include "pw_rpc_system_server/rpc_server.h"
+#include "pw_rpc_system_server/socket.h"
+#include "pw_stream/std_file_stream.h"
+#include "pw_thread/thread.h"
+#include "pw_thread_stl/options.h"
+#include "pw_transfer/integration_test/config.pb.h"
+#include "pw_transfer/transfer.h"
+
+namespace pw::transfer {
+namespace {
+
+using stream::MemoryReader;
+using stream::MemoryWriter;
+
+// This is the maximum size of the socket send buffers. Ideally, this is set
+// to the lowest allowed value to minimize buffering between the proxy and
+// clients so rate limiting causes the client to block and wait for the
+// integration test proxy to drain rather than allowing OS buffers to backlog
+// large quantities of data.
+//
+// Note that the OS may chose to not strictly follow this requested buffer size.
+// Still, setting this value to be as small as possible does reduce bufer sizes
+// significantly enough to better reflect typical inter-device communication.
+//
+// For this to be effective, servers should also configure their sockets to a
+// smaller receive buffer size.
+constexpr int kMaxSocketSendBufferSize = 1;
+
+class FileTransferHandler final : public ReadWriteHandler {
+ public:
+ FileTransferHandler(uint32_t resource_id,
+ std::deque<std::string>&& sources,
+ std::deque<std::string>&& destinations,
+ std::string default_source_path,
+ std::string default_destination_path)
+ : ReadWriteHandler(resource_id),
+ sources_(sources),
+ destinations_(destinations),
+ default_source_path_(default_source_path),
+ default_destination_path_(default_destination_path) {}
+
+ ~FileTransferHandler() = default;
+
+ Status PrepareRead() final {
+ if (sources_.empty() && default_source_path_.length() == 0) {
+ PW_LOG_ERROR("Source paths exhausted");
+ return Status::ResourceExhausted();
+ }
+
+ std::string path;
+ if (!sources_.empty()) {
+ path = sources_.front();
+ sources_.pop_front();
+ } else {
+ path = default_source_path_;
+ }
+
+ PW_LOG_DEBUG("Preparing read for file %s", path.c_str());
+ set_reader(stream_.emplace<stream::StdFileReader>(path.c_str()));
+ return OkStatus();
+ }
+
+ void FinalizeRead(Status) final {
+ std::get<stream::StdFileReader>(stream_).Close();
+ }
+
+ Status PrepareWrite() final {
+ if (destinations_.empty() && default_destination_path_.length() == 0) {
+ PW_LOG_ERROR("Destination paths exhausted");
+ return Status::ResourceExhausted();
+ }
+
+ std::string path;
+ if (!destinations_.empty()) {
+ path = destinations_.front();
+ destinations_.pop_front();
+ } else {
+ path = default_destination_path_;
+ }
+
+ PW_LOG_DEBUG("Preparing write for file %s", path.c_str());
+ set_writer(stream_.emplace<stream::StdFileWriter>(path.c_str()));
+ return OkStatus();
+ }
+
+ Status FinalizeWrite(Status) final {
+ std::get<stream::StdFileWriter>(stream_).Close();
+ return OkStatus();
+ }
+
+ private:
+ std::deque<std::string> sources_;
+ std::deque<std::string> destinations_;
+ std::string default_source_path_;
+ std::string default_destination_path_;
+ std::variant<std::monostate, stream::StdFileReader, stream::StdFileWriter>
+ stream_;
+};
+
+void RunServer(int socket_port, ServerConfig config) {
+ std::vector<std::byte> chunk_buffer(config.chunk_size_bytes());
+ std::vector<std::byte> encode_buffer(config.chunk_size_bytes());
+ transfer::Thread<4, 4> transfer_thread(chunk_buffer, encode_buffer);
+ TransferService transfer_service(
+ transfer_thread,
+ config.pending_bytes(),
+ std::chrono::seconds(config.chunk_timeout_seconds()),
+ config.transfer_service_retries(),
+ config.extend_window_divisor());
+
+ rpc::system_server::set_socket_port(socket_port);
+
+ rpc::system_server::Init();
+ rpc::system_server::Server().RegisterService(transfer_service);
+
+ // Start transfer thread.
+ thread::Thread transfer_thread_handle =
+ thread::Thread(thread::stl::Options(), transfer_thread);
+
+ int retval = setsockopt(rpc::system_server::GetServerSocketFd(),
+ SOL_SOCKET,
+ SO_SNDBUF,
+ &kMaxSocketSendBufferSize,
+ sizeof(kMaxSocketSendBufferSize));
+ PW_CHECK_INT_EQ(retval,
+ 0,
+ "Failed to configure socket send buffer size with errno=%d",
+ errno);
+
+ std::vector<std::unique_ptr<FileTransferHandler>> handlers;
+ for (const auto& resource : config.resources()) {
+ uint32_t id = resource.first;
+
+ std::deque<std::string> source_paths(resource.second.source_paths().begin(),
+ resource.second.source_paths().end());
+ std::deque<std::string> destination_paths(
+ resource.second.destination_paths().begin(),
+ resource.second.destination_paths().end());
+
+ auto handler = std::make_unique<FileTransferHandler>(
+ id,
+ std::move(source_paths),
+ std::move(destination_paths),
+ resource.second.default_source_path(),
+ resource.second.default_destination_path());
+
+ transfer_service.RegisterHandler(*handler);
+ handlers.push_back(std::move(handler));
+ }
+
+ PW_LOG_INFO("Starting pw_rpc server");
+ PW_CHECK_OK(rpc::system_server::Start());
+
+ // Unregister transfer handler before cleaning up the thread since doing so
+ // requires the transfer thread to be running.
+ for (auto& handler : handlers) {
+ transfer_service.UnregisterHandler(*handler);
+ }
+
+ // End transfer thread.
+ transfer_thread.Terminate();
+ transfer_thread_handle.join();
+}
+
+} // namespace
+} // namespace pw::transfer
+
+int main(int argc, char* argv[]) {
+ if (argc != 2) {
+ PW_LOG_INFO("Usage: %s PORT <<< config textproto", argv[0]);
+ return 1;
+ }
+
+ int port = std::atoi(argv[1]);
+ PW_CHECK_UINT_GT(port, 0, "Invalid port!");
+
+ std::string config_string;
+ std::string line;
+ while (std::getline(std::cin, line)) {
+ config_string = config_string + line + '\n';
+ }
+ pw::transfer::ServerConfig config;
+
+ bool ok =
+ google::protobuf::TextFormat::ParseFromString(config_string, &config);
+ if (!ok) {
+ PW_LOG_INFO("Failed to parse config: %s", config_string.c_str());
+ PW_LOG_INFO("Usage: %s PORT <<< config textproto", argv[0]);
+ return 1;
+ } else {
+ PW_LOG_INFO("Server loaded config:\n%s", config.DebugString().c_str());
+ }
+
+ pw::transfer::RunServer(port, config);
+ return 0;
+}
diff --git a/pw_transfer/integration_test/test_fixture.py b/pw_transfer/integration_test/test_fixture.py
new file mode 100644
index 000000000..a7b297c77
--- /dev/null
+++ b/pw_transfer/integration_test/test_fixture.py
@@ -0,0 +1,625 @@
+#!/usr/bin/env python3
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Test fixture for pw_transfer integration tests."""
+
+import argparse
+import asyncio
+from dataclasses import dataclass
+import logging
+import pathlib
+from pathlib import Path
+import sys
+import tempfile
+from typing import BinaryIO, Iterable, List, NamedTuple, Optional
+import unittest
+
+from google.protobuf import text_format
+
+from pigweed.pw_protobuf.pw_protobuf_protos import status_pb2
+from pigweed.pw_transfer.integration_test import config_pb2
+from rules_python.python.runfiles import runfiles
+
+_LOG = logging.getLogger('pw_transfer_intergration_test_proxy')
+_LOG.level = logging.DEBUG
+_LOG.addHandler(logging.StreamHandler(sys.stdout))
+
+
+class LogMonitor:
+ """Monitors lines read from the reader, and logs them."""
+
+ class Error(Exception):
+ """Raised if wait_for_line reaches EOF before expected line."""
+
+ pass
+
+ def __init__(self, prefix: str, reader: asyncio.StreamReader):
+ """Initializer.
+
+ Args:
+ prefix: Prepended to read lines before they are logged.
+ reader: StreamReader to read lines from.
+ """
+ self._prefix = prefix
+ self._reader = reader
+
+ # Queue of messages waiting to be monitored.
+ self._queue = asyncio.Queue()
+ # Relog any messages read from the reader, and enqueue them for
+ # monitoring.
+ self._relog_and_enqueue_task = asyncio.create_task(
+ self._relog_and_enqueue()
+ )
+
+ async def wait_for_line(self, msg: str):
+ """Wait for a line containing msg to be read from the reader."""
+ while True:
+ line = await self._queue.get()
+ if not line:
+ raise LogMonitor.Error(
+ f"Reached EOF before getting line matching {msg}"
+ )
+ if msg in line.decode():
+ return
+
+ async def wait_for_eof(self):
+ """Wait for the reader to reach EOF, relogging any lines read."""
+ # Drain the queue, since we're not monitoring it any more.
+ drain_queue = asyncio.create_task(self._drain_queue())
+ await asyncio.gather(drain_queue, self._relog_and_enqueue_task)
+
+ async def _relog_and_enqueue(self):
+ """Reads lines from the reader, logs them, and puts them in queue."""
+ while True:
+ line = await self._reader.readline()
+ await self._queue.put(line)
+ if line:
+ _LOG.info(f"{self._prefix} {line.decode().rstrip()}")
+ else:
+ # EOF. Note, we still put the EOF in the queue, so that the
+ # queue reader can process it appropriately.
+ return
+
+ async def _drain_queue(self):
+ while True:
+ line = await self._queue.get()
+ if not line:
+ # EOF.
+ return
+
+
+class MonitoredSubprocess:
+ """A subprocess with monitored asynchronous communication."""
+
+ @staticmethod
+ async def create(cmd: List[str], prefix: str, stdinput: bytes):
+ """Starts the subprocess and writes stdinput to stdin.
+
+ This method returns once stdinput has been written to stdin. The
+ MonitoredSubprocess continues to log the process's stderr and stdout
+ (with the prefix) until it terminates.
+
+ Args:
+ cmd: Command line to execute.
+ prefix: Prepended to process logs.
+ stdinput: Written to stdin on process startup.
+ """
+ self = MonitoredSubprocess()
+ self._process = await asyncio.create_subprocess_exec(
+ *cmd,
+ stdin=asyncio.subprocess.PIPE,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+
+ self._stderr_monitor = LogMonitor(
+ f"{prefix} ERR:", self._process.stderr
+ )
+ self._stdout_monitor = LogMonitor(
+ f"{prefix} OUT:", self._process.stdout
+ )
+
+ self._process.stdin.write(stdinput)
+ await self._process.stdin.drain()
+ self._process.stdin.close()
+ await self._process.stdin.wait_closed()
+ return self
+
+ async def wait_for_line(self, stream: str, msg: str, timeout: float):
+ """Wait for a line containing msg to be read on the stream."""
+ if stream == "stdout":
+ monitor = self._stdout_monitor
+ elif stream == "stderr":
+ monitor = self._stderr_monitor
+ else:
+ raise ValueError(
+ "Stream must be 'stdout' or 'stderr', got {stream}"
+ )
+
+ await asyncio.wait_for(monitor.wait_for_line(msg), timeout)
+
+ def returncode(self):
+ return self._process.returncode
+
+ def terminate(self):
+ """Terminate the process."""
+ self._process.terminate()
+
+ async def wait_for_termination(self, timeout: float):
+ """Wait for the process to terminate."""
+ await asyncio.wait_for(
+ asyncio.gather(
+ self._process.wait(),
+ self._stdout_monitor.wait_for_eof(),
+ self._stderr_monitor.wait_for_eof(),
+ ),
+ timeout,
+ )
+
+ async def terminate_and_wait(self, timeout: float):
+ """Terminate the process and wait for it to exit."""
+ if self.returncode() is not None:
+ # Process already terminated
+ return
+ self.terminate()
+ await self.wait_for_termination(timeout)
+
+
+class TransferConfig(NamedTuple):
+ """A simple tuple to collect configs for test binaries."""
+
+ server: config_pb2.ServerConfig
+ client: config_pb2.ClientConfig
+ proxy: config_pb2.ProxyConfig
+
+
+class TransferIntegrationTestHarness:
+ """A class to manage transfer integration tests"""
+
+ # Prefix for log messages coming from the harness (as opposed to the server,
+ # client, or proxy processes). Padded so that the length is the same as
+ # "SERVER OUT:".
+ _PREFIX = "HARNESS: "
+
+ @dataclass
+ class Config:
+ server_port: int = 3300
+ client_port: int = 3301
+ java_client_binary: Optional[Path] = None
+ cpp_client_binary: Optional[Path] = None
+ python_client_binary: Optional[Path] = None
+ proxy_binary: Optional[Path] = None
+ server_binary: Optional[Path] = None
+
+ class TransferExitCodes(NamedTuple):
+ client: int
+ server: int
+
+ def __init__(self, harness_config: Config) -> None:
+ # TODO(tpudlik): This is Bazel-only. Support gn, too.
+ r = runfiles.Create()
+
+ # Set defaults.
+ self._JAVA_CLIENT_BINARY = r.Rlocation(
+ "pigweed/pw_transfer/integration_test/java_client"
+ )
+ self._CPP_CLIENT_BINARY = r.Rlocation(
+ "pigweed/pw_transfer/integration_test/cpp_client"
+ )
+ self._PYTHON_CLIENT_BINARY = r.Rlocation(
+ "pigweed/pw_transfer/integration_test/python_client"
+ )
+ self._PROXY_BINARY = r.Rlocation(
+ "pigweed/pw_transfer/integration_test/proxy"
+ )
+ self._SERVER_BINARY = r.Rlocation(
+ "pigweed/pw_transfer/integration_test/server"
+ )
+
+ # Server/client ports are non-optional, so use those.
+ self._CLIENT_PORT = harness_config.client_port
+ self._SERVER_PORT = harness_config.server_port
+
+ # If the harness configuration specifies overrides, use those.
+ if harness_config.java_client_binary is not None:
+ self._JAVA_CLIENT_BINARY = harness_config.java_client_binary
+ if harness_config.cpp_client_binary is not None:
+ self._CPP_CLIENT_BINARY = harness_config.cpp_client_binary
+ if harness_config.python_client_binary is not None:
+ self._PYTHON_CLIENT_BINARY = harness_config.python_client_binary
+ if harness_config.proxy_binary is not None:
+ self._PROXY_BINARY = harness_config.proxy_binary
+ if harness_config.server_binary is not None:
+ self._SERVER_BINARY = harness_config.server_binary
+
+ self._CLIENT_BINARY = {
+ "cpp": self._CPP_CLIENT_BINARY,
+ "java": self._JAVA_CLIENT_BINARY,
+ "python": self._PYTHON_CLIENT_BINARY,
+ }
+ pass
+
+ async def _start_client(
+ self, client_type: str, config: config_pb2.ClientConfig
+ ):
+ _LOG.info(f"{self._PREFIX} Starting client with config\n{config}")
+ self._client = await MonitoredSubprocess.create(
+ [self._CLIENT_BINARY[client_type], str(self._CLIENT_PORT)],
+ "CLIENT",
+ str(config).encode('ascii'),
+ )
+
+ async def _start_server(self, config: config_pb2.ServerConfig):
+ _LOG.info(f"{self._PREFIX} Starting server with config\n{config}")
+ self._server = await MonitoredSubprocess.create(
+ [self._SERVER_BINARY, str(self._SERVER_PORT)],
+ "SERVER",
+ str(config).encode('ascii'),
+ )
+
+ async def _start_proxy(self, config: config_pb2.ProxyConfig):
+ _LOG.info(f"{self._PREFIX} Starting proxy with config\n{config}")
+ self._proxy = await MonitoredSubprocess.create(
+ [
+ self._PROXY_BINARY,
+ "--server-port",
+ str(self._SERVER_PORT),
+ "--client-port",
+ str(self._CLIENT_PORT),
+ ],
+ # Extra space in "PROXY " so that it lines up with "SERVER".
+ "PROXY ",
+ str(config).encode('ascii'),
+ )
+
+ async def perform_transfers(
+ self,
+ server_config: config_pb2.ServerConfig,
+ client_type: str,
+ client_config: config_pb2.ClientConfig,
+ proxy_config: config_pb2.ProxyConfig,
+ ) -> TransferExitCodes:
+ """Performs a pw_transfer write.
+
+ Args:
+ server_config: Server configuration.
+ client_type: Either "cpp", "java", or "python".
+ client_config: Client configuration.
+ proxy_config: Proxy configuration.
+
+ Returns:
+ Exit code of the client and server as a tuple.
+ """
+ # Timeout for components (server, proxy) to come up or shut down after
+ # write is finished or a signal is sent. Approximately arbitrary. Should
+ # not be too long so that we catch bugs in the server that prevent it
+ # from shutting down.
+ TIMEOUT = 5 # seconds
+
+ try:
+ await self._start_proxy(proxy_config)
+ await self._proxy.wait_for_line(
+ "stderr", "Listening for client connection", TIMEOUT
+ )
+
+ await self._start_server(server_config)
+ await self._server.wait_for_line(
+ "stderr", "Starting pw_rpc server on port", TIMEOUT
+ )
+
+ await self._start_client(client_type, client_config)
+ # No timeout: the client will only exit once the transfer
+ # completes, and this can take a long time for large payloads.
+ await self._client.wait_for_termination(None)
+
+ # Wait for the server to exit.
+ await self._server.wait_for_termination(TIMEOUT)
+
+ finally:
+ # Stop the server, if still running. (Only expected if the
+ # wait_for above timed out.)
+ if self._server:
+ await self._server.terminate_and_wait(TIMEOUT)
+ # Stop the proxy. Unlike the server, we expect it to still be
+ # running at this stage.
+ if self._proxy:
+ await self._proxy.terminate_and_wait(TIMEOUT)
+
+ return self.TransferExitCodes(
+ self._client.returncode(), self._server.returncode()
+ )
+
+
+class BasicTransfer(NamedTuple):
+ id: int
+ type: config_pb2.TransferAction.TransferType.ValueType
+ data: bytes
+
+
+class TransferIntegrationTest(unittest.TestCase):
+ """A base class for transfer integration tests.
+
+ This significantly reduces the boiler plate required for building
+ integration test cases for pw_transfer. This class does not include any
+ tests itself, but instead bundles together much of the boiler plate required
+ for making an integration test for pw_transfer using this test fixture.
+ """
+
+ HARNESS_CONFIG = TransferIntegrationTestHarness.Config()
+
+ @classmethod
+ def setUpClass(cls):
+ cls.harness = TransferIntegrationTestHarness(cls.HARNESS_CONFIG)
+
+ @staticmethod
+ def default_server_config() -> config_pb2.ServerConfig:
+ return config_pb2.ServerConfig(
+ chunk_size_bytes=216,
+ pending_bytes=32 * 1024,
+ chunk_timeout_seconds=5,
+ transfer_service_retries=4,
+ extend_window_divisor=32,
+ )
+
+ @staticmethod
+ def default_client_config() -> config_pb2.ClientConfig:
+ return config_pb2.ClientConfig(
+ max_retries=5,
+ max_lifetime_retries=1500,
+ initial_chunk_timeout_ms=4000,
+ chunk_timeout_ms=4000,
+ )
+
+ @staticmethod
+ def default_proxy_config() -> config_pb2.ProxyConfig:
+ return text_format.Parse(
+ """
+ client_filter_stack: [
+ { hdlc_packetizer: {} },
+ { data_dropper: {rate: 0.01, seed: 1649963713563718435} }
+ ]
+
+ server_filter_stack: [
+ { hdlc_packetizer: {} },
+ { data_dropper: {rate: 0.01, seed: 1649963713563718436} }
+ ]""",
+ config_pb2.ProxyConfig(),
+ )
+
+ @staticmethod
+ def default_config() -> TransferConfig:
+ """Returns a new transfer config with default options."""
+ return TransferConfig(
+ TransferIntegrationTest.default_server_config(),
+ TransferIntegrationTest.default_client_config(),
+ TransferIntegrationTest.default_proxy_config(),
+ )
+
+ def do_single_write(
+ self,
+ client_type: str,
+ config: TransferConfig,
+ resource_id: int,
+ data: bytes,
+ protocol_version=config_pb2.TransferAction.ProtocolVersion.LATEST,
+ permanent_resource_id=False,
+ expected_status=status_pb2.StatusCode.OK,
+ ) -> None:
+ """Performs a single client-to-server write of the provided data."""
+ with tempfile.NamedTemporaryFile() as f_payload, tempfile.NamedTemporaryFile() as f_server_output:
+ if permanent_resource_id:
+ config.server.resources[
+ resource_id
+ ].default_destination_path = f_server_output.name
+ else:
+ config.server.resources[resource_id].destination_paths.append(
+ f_server_output.name
+ )
+ config.client.transfer_actions.append(
+ config_pb2.TransferAction(
+ resource_id=resource_id,
+ file_path=f_payload.name,
+ transfer_type=config_pb2.TransferAction.TransferType.WRITE_TO_SERVER,
+ protocol_version=protocol_version,
+ expected_status=int(expected_status),
+ )
+ )
+
+ f_payload.write(data)
+ f_payload.flush() # Ensure contents are there to read!
+ exit_codes = asyncio.run(
+ self.harness.perform_transfers(
+ config.server, client_type, config.client, config.proxy
+ )
+ )
+
+ self.assertEqual(exit_codes.client, 0)
+ self.assertEqual(exit_codes.server, 0)
+ if expected_status == status_pb2.StatusCode.OK:
+ self.assertEqual(f_server_output.read(), data)
+
+ def do_single_read(
+ self,
+ client_type: str,
+ config: TransferConfig,
+ resource_id: int,
+ data: bytes,
+ protocol_version=config_pb2.TransferAction.ProtocolVersion.LATEST,
+ permanent_resource_id=False,
+ expected_status=status_pb2.StatusCode.OK,
+ ) -> None:
+ """Performs a single server-to-client read of the provided data."""
+ with tempfile.NamedTemporaryFile() as f_payload, tempfile.NamedTemporaryFile() as f_client_output:
+ if permanent_resource_id:
+ config.server.resources[
+ resource_id
+ ].default_source_path = f_payload.name
+ else:
+ config.server.resources[resource_id].source_paths.append(
+ f_payload.name
+ )
+ config.client.transfer_actions.append(
+ config_pb2.TransferAction(
+ resource_id=resource_id,
+ file_path=f_client_output.name,
+ transfer_type=config_pb2.TransferAction.TransferType.READ_FROM_SERVER,
+ protocol_version=protocol_version,
+ expected_status=int(expected_status),
+ )
+ )
+
+ f_payload.write(data)
+ f_payload.flush() # Ensure contents are there to read!
+ exit_codes = asyncio.run(
+ self.harness.perform_transfers(
+ config.server, client_type, config.client, config.proxy
+ )
+ )
+ self.assertEqual(exit_codes.client, 0)
+ self.assertEqual(exit_codes.server, 0)
+ if expected_status == status_pb2.StatusCode.OK:
+ self.assertEqual(f_client_output.read(), data)
+
+ def do_basic_transfer_sequence(
+ self,
+ client_type: str,
+ config: TransferConfig,
+ transfers: Iterable[BasicTransfer],
+ ) -> None:
+ """Performs multiple reads/writes in a single client/server session."""
+
+ class ReadbackSet(NamedTuple):
+ server_file: BinaryIO
+ client_file: BinaryIO
+ expected_data: bytes
+
+ transfer_results: List[ReadbackSet] = []
+ for transfer in transfers:
+ server_file = tempfile.NamedTemporaryFile()
+ client_file = tempfile.NamedTemporaryFile()
+
+ if (
+ transfer.type
+ == config_pb2.TransferAction.TransferType.READ_FROM_SERVER
+ ):
+ server_file.write(transfer.data)
+ server_file.flush()
+ config.server.resources[transfer.id].source_paths.append(
+ server_file.name
+ )
+ elif (
+ transfer.type
+ == config_pb2.TransferAction.TransferType.WRITE_TO_SERVER
+ ):
+ client_file.write(transfer.data)
+ client_file.flush()
+ config.server.resources[transfer.id].destination_paths.append(
+ server_file.name
+ )
+ else:
+ raise ValueError('Unknown TransferType')
+
+ config.client.transfer_actions.append(
+ config_pb2.TransferAction(
+ resource_id=transfer.id,
+ file_path=client_file.name,
+ transfer_type=transfer.type,
+ )
+ )
+
+ transfer_results.append(
+ ReadbackSet(server_file, client_file, transfer.data)
+ )
+
+ exit_codes = asyncio.run(
+ self.harness.perform_transfers(
+ config.server, client_type, config.client, config.proxy
+ )
+ )
+
+ for i, result in enumerate(transfer_results):
+ with self.subTest(i=i):
+ # Need to seek to the beginning of the file to read written
+ # data.
+ result.client_file.seek(0, 0)
+ result.server_file.seek(0, 0)
+ self.assertEqual(
+ result.client_file.read(), result.expected_data
+ )
+ self.assertEqual(
+ result.server_file.read(), result.expected_data
+ )
+
+ # Check exit codes at the end as they provide less useful info.
+ self.assertEqual(exit_codes.client, 0)
+ self.assertEqual(exit_codes.server, 0)
+
+
+def run_tests_for(test_class_name):
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ help='Port of the integration test server. The proxy will forward connections to this port',
+ )
+ parser.add_argument(
+ '--client-port',
+ type=int,
+ help='Port on which to listen for connections from integration test client.',
+ )
+ parser.add_argument(
+ '--java-client-binary',
+ type=pathlib.Path,
+ default=None,
+ help='Path to the Java transfer client to use in tests',
+ )
+ parser.add_argument(
+ '--cpp-client-binary',
+ type=pathlib.Path,
+ default=None,
+ help='Path to the C++ transfer client to use in tests',
+ )
+ parser.add_argument(
+ '--python-client-binary',
+ type=pathlib.Path,
+ default=None,
+ help='Path to the Python transfer client to use in tests',
+ )
+ parser.add_argument(
+ '--server-binary',
+ type=pathlib.Path,
+ default=None,
+ help='Path to the transfer server to use in tests',
+ )
+ parser.add_argument(
+ '--proxy-binary',
+ type=pathlib.Path,
+ default=None,
+ help=(
+ 'Path to the proxy binary to use in tests to allow interception '
+ 'of client/server data'
+ ),
+ )
+
+ (args, passthrough_args) = parser.parse_known_args()
+
+ # Inherrit the default configuration from the class being tested, and only
+ # override provided arguments.
+ for arg in vars(args):
+ val = getattr(args, arg)
+ if val:
+ setattr(test_class_name.HARNESS_CONFIG, arg, val)
+
+ unittest_args = [sys.argv[0]] + passthrough_args
+ unittest.main(argv=unittest_args)
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel b/pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel
new file mode 100644
index 000000000..03a9a007e
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/BUILD.bazel
@@ -0,0 +1,44 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Client for the pw_transfer RPC service, which is used to send and receive data
+# over pw_rpc.
+
+java_library(
+ name = "client",
+ srcs = [
+ "ProtocolVersion.java",
+ "ReadTransfer.java",
+ "Transfer.java",
+ "TransferClient.java",
+ "TransferError.java",
+ "TransferEventHandler.java",
+ "TransferParameters.java",
+ "TransferProgress.java",
+ "TransferService.java",
+ "TransferTimeoutSettings.java",
+ "VersionedChunk.java",
+ "WriteTransfer.java",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pw_log/java/main/dev/pigweed/pw_log",
+ "//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
+ "//pw_transfer:transfer_proto_java_lite",
+ "//third_party/google_auto:value",
+ "@com_google_protobuf//java/lite",
+ "@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_guava_guava",
+ ],
+)
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/ProtocolVersion.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/ProtocolVersion.java
new file mode 100644
index 000000000..003e1ff2a
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/ProtocolVersion.java
@@ -0,0 +1,34 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+public enum ProtocolVersion {
+ // Protocol version not known or not set.
+ UNKNOWN,
+
+ // The original transfer protocol, prior to transfer start/end handshakes.
+ LEGACY,
+
+ // Second version of the transfer protocol. Guarantees type fields on all
+ // chunks, deprecates pending_bytes in favor of window_end_offset, splits
+ // transfer resource IDs from ephemeral session IDs, and adds a handshake
+ // to the start and end of all transfer sessions.
+ VERSION_TWO;
+
+ /** Returns to the most up-to-date version of the transfer protocol. */
+ public static ProtocolVersion latest() {
+ return VERSION_TWO;
+ }
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java
new file mode 100644
index 000000000..15e3cd321
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/ReadTransfer.java
@@ -0,0 +1,206 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import static dev.pigweed.pw_transfer.TransferProgress.UNKNOWN_TRANSFER_SIZE;
+import static java.lang.Math.max;
+
+import dev.pigweed.pw_log.Logger;
+import dev.pigweed.pw_rpc.Status;
+import dev.pigweed.pw_transfer.TransferEventHandler.TransferInterface;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+
+class ReadTransfer extends Transfer<byte[]> {
+ private static final Logger logger = Logger.forClass(ReadTransfer.class);
+
+ // The fractional position within a window at which a receive transfer should
+ // extend its window size to minimize the amount of time the transmitter
+ // spends blocked.
+ //
+ // For example, a divisor of 2 will extend the window when half of the
+ // requested data has been received, a divisor of three will extend at a third
+ // of the window, and so on.
+ private static final int EXTEND_WINDOW_DIVISOR = 2;
+
+ // To minimize copies, store the ByteBuffers directly from the chunk protos in a list.
+ private final List<ByteBuffer> dataChunks = new ArrayList<>();
+ private int totalDataSize = 0;
+
+ private final TransferParameters parameters;
+
+ private long remainingTransferSize = UNKNOWN_TRANSFER_SIZE;
+
+ private int offset = 0;
+ private int windowEndOffset = 0;
+
+ private int lastReceivedOffset = 0;
+
+ ReadTransfer(int resourceId,
+ ProtocolVersion desiredProtocolVersion,
+ TransferInterface transferManager,
+ TransferTimeoutSettings timeoutSettings,
+ TransferParameters transferParameters,
+ Consumer<TransferProgress> progressCallback,
+ BooleanSupplier shouldAbortCallback) {
+ super(resourceId,
+ desiredProtocolVersion,
+ transferManager,
+ timeoutSettings,
+ progressCallback,
+ shouldAbortCallback);
+ this.parameters = transferParameters;
+ this.windowEndOffset = parameters.maxPendingBytes();
+ }
+
+ @Override
+ State getWaitingForDataState() {
+ return new ReceivingData();
+ }
+
+ @Override
+ void prepareInitialChunk(VersionedChunk.Builder chunk) {
+ setTransferParameters(chunk);
+ }
+
+ @Override
+ VersionedChunk getChunkForRetry() {
+ VersionedChunk chunk = getLastChunkSent();
+ // If the last chunk sent was transfer parameters, send an updated RETRANSMIT chunk.
+ if (chunk.type() == Chunk.Type.PARAMETERS_CONTINUE
+ || chunk.type() == Chunk.Type.PARAMETERS_RETRANSMIT) {
+ return prepareTransferParameters(/*extend=*/false);
+ }
+ return chunk;
+ }
+
+ private class ReceivingData extends ActiveState {
+ @Override
+ public void handleDataChunk(VersionedChunk chunk) throws TransferAbortedException {
+ // Track the last seen offset so the DropRecovery state can detect retried packets.
+ lastReceivedOffset = chunk.offset();
+
+ if (chunk.offset() != offset) {
+ logger.atFine().log("%s expected offset %d, received %d; resending transfer parameters",
+ ReadTransfer.this,
+ offset,
+ chunk.offset());
+
+ // For now, only in-order transfers are supported. If data is received out of order,
+ // discard this data and retransmit from the last received offset.
+ sendChunk(prepareTransferParameters(/*extend=*/false));
+ changeState(new DropRecovery());
+ setNextChunkTimeout();
+ return;
+ }
+
+ // Add the underlying array(s) to a list to avoid making copies of the data.
+ dataChunks.addAll(chunk.data().asReadOnlyByteBufferList());
+ totalDataSize += chunk.data().size();
+
+ offset += chunk.data().size();
+
+ if (chunk.remainingBytes().isPresent()) {
+ if (chunk.remainingBytes().getAsLong() == 0) {
+ setStateTerminatingAndSendFinalChunk(Status.OK);
+ return;
+ }
+
+ remainingTransferSize = chunk.remainingBytes().getAsLong();
+ } else if (remainingTransferSize != UNKNOWN_TRANSFER_SIZE) {
+ // If remaining size was not specified, update based on the most recent estimate, if any.
+ remainingTransferSize = max(remainingTransferSize - chunk.data().size(), 0);
+ }
+
+ if (remainingTransferSize == UNKNOWN_TRANSFER_SIZE || remainingTransferSize == 0) {
+ updateProgress(offset, offset, UNKNOWN_TRANSFER_SIZE);
+ } else {
+ updateProgress(offset, offset, offset + remainingTransferSize);
+ }
+
+ int remainingWindowSize = windowEndOffset - offset;
+ boolean extendWindow =
+ remainingWindowSize <= parameters.maxPendingBytes() / EXTEND_WINDOW_DIVISOR;
+
+ if (remainingWindowSize == 0) {
+ logger.atFinest().log(
+ "%s received all pending bytes; sending transfer parameters update", ReadTransfer.this);
+ sendChunk(prepareTransferParameters(/*extend=*/false));
+ } else if (extendWindow) {
+ sendChunk(prepareTransferParameters(/*extend=*/true));
+ }
+ setNextChunkTimeout();
+ }
+ }
+
+ /** State for recovering from dropped packets. */
+ private class DropRecovery extends ActiveState {
+ @Override
+ public void handleDataChunk(VersionedChunk chunk) throws TransferAbortedException {
+ if (chunk.offset() == offset) {
+ logger.atFine().log(
+ "%s received expected offset %d, resuming transfer", ReadTransfer.this, offset);
+ changeState(new ReceivingData()).handleDataChunk(chunk);
+ return;
+ }
+
+ // To avoid a flood of identical parameters packets, only send one if a retry is detected.
+ if (chunk.offset() == lastReceivedOffset) {
+ logger.atFiner().log(
+ "%s received repeated offset %d: retry detected, resending transfer parameters",
+ ReadTransfer.this,
+ lastReceivedOffset);
+ sendChunk(prepareTransferParameters(/*extend=*/false));
+ } else {
+ lastReceivedOffset = chunk.offset();
+ logger.atFiner().log("%s expecting offset %d, ignoring received offset %d",
+ ReadTransfer.this,
+ offset,
+ chunk.offset());
+ }
+ setNextChunkTimeout();
+ }
+ }
+
+ @Override
+ void setFutureResult() {
+ updateProgress(totalDataSize, totalDataSize, totalDataSize);
+
+ ByteBuffer result = ByteBuffer.allocate(totalDataSize);
+ dataChunks.forEach(result::put);
+ getFuture().set(result.array());
+ }
+
+ private VersionedChunk prepareTransferParameters(boolean extend) {
+ windowEndOffset = offset + parameters.maxPendingBytes();
+
+ Chunk.Type type = extend ? Chunk.Type.PARAMETERS_CONTINUE : Chunk.Type.PARAMETERS_RETRANSMIT;
+ return setTransferParameters(newChunk(type)).build();
+ }
+
+ private VersionedChunk.Builder setTransferParameters(VersionedChunk.Builder chunk) {
+ chunk.setWindowEndOffset(offset + parameters.maxPendingBytes())
+ .setMaxChunkSizeBytes(parameters.maxChunkSizeBytes())
+ .setOffset(offset)
+ .setWindowEndOffset(windowEndOffset);
+ if (parameters.chunkDelayMicroseconds() > 0) {
+ chunk.setMinDelayMicroseconds(parameters.chunkDelayMicroseconds());
+ }
+ return chunk;
+ }
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java
new file mode 100644
index 000000000..fef83e59e
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/Transfer.java
@@ -0,0 +1,533 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
+import static dev.pigweed.pw_transfer.TransferProgress.UNKNOWN_TRANSFER_SIZE;
+
+import com.google.common.util.concurrent.SettableFuture;
+import dev.pigweed.pw_log.Logger;
+import dev.pigweed.pw_rpc.Status;
+import dev.pigweed.pw_transfer.TransferEventHandler.TransferInterface;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Locale;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+
+/** Base class for tracking the state of a read or write transfer. */
+abstract class Transfer<T> {
+ private static final Logger logger = Logger.forClass(Transfer.class);
+
+ // Largest nanosecond instant. Used to block indefinitely when no transfers are pending.
+ static final Instant NO_TIMEOUT = Instant.ofEpochSecond(0, Long.MAX_VALUE);
+
+ // Whether to output some particularly noisy logs.
+ static final boolean VERBOSE_LOGGING = false;
+
+ private final int resourceId;
+ private final ProtocolVersion desiredProtocolVersion;
+ private final TransferEventHandler.TransferInterface eventHandler;
+ private final SettableFuture<T> future;
+ private final TransferTimeoutSettings timeoutSettings;
+ private final Consumer<TransferProgress> progressCallback;
+ private final BooleanSupplier shouldAbortCallback;
+ private final Instant startTime;
+
+ private int sessionId = VersionedChunk.UNASSIGNED_SESSION_ID;
+ private ProtocolVersion configuredProtocolVersion = ProtocolVersion.UNKNOWN;
+ private Instant deadline = NO_TIMEOUT;
+ private State state;
+ private VersionedChunk lastChunkSent;
+
+ // The number of times this transfer has retried due to an RPC disconnection. Limit this to
+ // maxRetries to prevent repeated crashes if reading to / writing from a particular transfer is
+ // causing crashes.
+ private int disconnectionRetries = 0;
+ private int lifetimeRetries = 0;
+
+ /**
+ * Creates a new read or write transfer.
+ * @param resourceId The resource ID of the transfer
+ * @param desiredProtocolVersion protocol version to request
+ * @param eventHandler Interface to use to send a chunk.
+ * @param timeoutSettings Timeout and retry settings for this transfer.
+ * @param progressCallback Called each time a packet is sent.
+ * @param shouldAbortCallback BooleanSupplier that returns true if a transfer should be aborted.
+ */
+ Transfer(int resourceId,
+ ProtocolVersion desiredProtocolVersion,
+ TransferInterface eventHandler,
+ TransferTimeoutSettings timeoutSettings,
+ Consumer<TransferProgress> progressCallback,
+ BooleanSupplier shouldAbortCallback) {
+ this.resourceId = resourceId;
+ this.desiredProtocolVersion = desiredProtocolVersion;
+ this.eventHandler = eventHandler;
+
+ this.future = SettableFuture.create();
+ this.timeoutSettings = timeoutSettings;
+ this.progressCallback = progressCallback;
+ this.shouldAbortCallback = shouldAbortCallback;
+
+ // If the future is cancelled, tell the TransferEventHandler to cancel the transfer.
+ future.addListener(() -> {
+ if (future.isCancelled()) {
+ eventHandler.cancelTransfer(this);
+ }
+ }, directExecutor());
+
+ if (desiredProtocolVersion == ProtocolVersion.LEGACY) {
+ // Legacy transfers skip protocol negotiation stage and use the resource ID as the session ID.
+ configuredProtocolVersion = ProtocolVersion.LEGACY;
+ assignSessionId(resourceId);
+ state = getWaitingForDataState();
+ } else {
+ state = new Initiating();
+ }
+
+ startTime = Instant.now();
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ENGLISH,
+ "%s(%d:%d)[%s]",
+ this.getClass().getSimpleName(),
+ resourceId,
+ sessionId,
+ state.getClass().getSimpleName());
+ }
+
+ public final int getResourceId() {
+ return resourceId;
+ }
+
+ public final int getSessionId() {
+ return sessionId;
+ }
+
+ private void assignSessionId(int newSessionId) {
+ sessionId = newSessionId;
+ eventHandler.assignSessionId(this);
+ }
+
+ /** Terminates the transfer without sending any packets. */
+ public final void terminate(TransferError error) {
+ changeState(new Completed(error));
+ }
+
+ final Instant getDeadline() {
+ return deadline;
+ }
+
+ final void setNextChunkTimeout() {
+ deadline = Instant.now().plusMillis(timeoutSettings.timeoutMillis());
+ }
+
+ private void setInitialTimeout() {
+ deadline = Instant.now().plusMillis(timeoutSettings.initialTimeoutMillis());
+ }
+
+ final void setTimeoutMicros(int timeoutMicros) {
+ deadline = Instant.now().plusNanos((long) timeoutMicros * 1000);
+ }
+
+ final SettableFuture<T> getFuture() {
+ return future;
+ }
+
+ final void start() {
+ logger.atInfo().log(
+ "%s starting with parameters: default timeout %d ms, initial timeout %d ms, %d max retires",
+ this,
+ timeoutSettings.timeoutMillis(),
+ timeoutSettings.initialTimeoutMillis(),
+ timeoutSettings.maxRetries());
+ VersionedChunk.Builder chunk =
+ VersionedChunk.createInitialChunk(desiredProtocolVersion, resourceId);
+ prepareInitialChunk(chunk);
+ try {
+ sendChunk(chunk.build());
+ } catch (TransferAbortedException e) {
+ return; // Sending failed, transfer is cancelled
+ }
+ setInitialTimeout();
+ }
+
+ /** Processes an incoming chunk from the server. */
+ final void handleChunk(VersionedChunk chunk) {
+ // Since a packet has been received, don't allow retries on disconnection; abort instead.
+ disconnectionRetries = Integer.MAX_VALUE;
+
+ try {
+ if (chunk.type() == Chunk.Type.COMPLETION) {
+ state.handleFinalChunk(chunk.status().orElseGet(() -> {
+ logger.atWarning().log("Received terminating chunk with no status set; using INTERNAL");
+ return Status.INTERNAL.code();
+ }));
+ } else {
+ state.handleDataChunk(chunk);
+ }
+ } catch (TransferAbortedException e) {
+ // Transfer was aborted; nothing else to do.
+ }
+ }
+
+ final void handleTimeoutIfDeadlineExceeded() {
+ if (Instant.now().isAfter(deadline)) {
+ try {
+ state.handleTimeout();
+ } catch (TransferAbortedException e) {
+ // Transfer was aborted; nothing else to do.
+ }
+ }
+ }
+
+ final void handleTermination() {
+ state.handleTermination();
+ }
+
+ final void handleCancellation() {
+ state.handleCancellation();
+ }
+
+ /** Restarts a transfer after an RPC disconnection. */
+ final void handleDisconnection() {
+ // disconnectionRetries is set to Int.MAX_VALUE when a packet is received to prevent retries
+ // after the initial packet.
+ if (disconnectionRetries++ < timeoutSettings.maxRetries()) {
+ logger.atFine().log("Restarting the pw_transfer RPC for %s (attempt %d/%d)",
+ this,
+ disconnectionRetries,
+ timeoutSettings.maxRetries());
+ try {
+ sendChunk(getChunkForRetry());
+ } catch (TransferAbortedException e) {
+ return; // Transfer is aborted; nothing else to do.
+ }
+ setInitialTimeout();
+ } else {
+ changeState(new Completed(new TransferError("Transfer " + sessionId + " restarted "
+ + timeoutSettings.maxRetries() + " times, aborting",
+ Status.INTERNAL)));
+ }
+ }
+
+ /** Returns the State to enter immediately after sending the first packet. */
+ abstract State getWaitingForDataState();
+
+ abstract void prepareInitialChunk(VersionedChunk.Builder chunk);
+
+ /**
+ * Returns the chunk to send for a retry. Returns the initial chunk if no chunks have been sent.
+ */
+ abstract VersionedChunk getChunkForRetry();
+
+ /** Sets the result for the future after a successful transfer. */
+ abstract void setFutureResult();
+
+ final VersionedChunk.Builder newChunk(Chunk.Type type) {
+ return VersionedChunk.builder()
+ .setVersion(configuredProtocolVersion != ProtocolVersion.UNKNOWN ? configuredProtocolVersion
+ : desiredProtocolVersion)
+ .setType(type)
+ .setSessionId(sessionId);
+ }
+
+ final VersionedChunk getLastChunkSent() {
+ return lastChunkSent;
+ }
+
+ final State changeState(State newState) {
+ if (newState != state) {
+ logger.atFinest().log("%s state %s -> %s",
+ this,
+ state.getClass().getSimpleName(),
+ newState.getClass().getSimpleName());
+ }
+ state = newState;
+ return state;
+ }
+
+ /** Exception thrown when the transfer is aborted. */
+ static class TransferAbortedException extends Exception {}
+
+ /**
+ * Sends a chunk.
+ *
+ * If sending fails, the transfer cannot proceed. sendChunk() sets the state to completed and
+ * throws a TransferAbortedException.
+ */
+ final void sendChunk(VersionedChunk chunk) throws TransferAbortedException {
+ lastChunkSent = chunk;
+ if (shouldAbortCallback.getAsBoolean()) {
+ logger.atWarning().log("Abort signal received.");
+ changeState(new Completed(new TransferError(this, Status.ABORTED)));
+ throw new TransferAbortedException();
+ }
+
+ try {
+ if (VERBOSE_LOGGING) {
+ logger.atFinest().log("%s sending %s", this, chunk);
+ }
+ eventHandler.sendChunk(chunk.toMessage());
+ } catch (TransferError transferError) {
+ changeState(new Completed(transferError));
+ throw new TransferAbortedException();
+ }
+ }
+
+ /** Sends a status chunk to the server and finishes the transfer. */
+ final void setStateTerminatingAndSendFinalChunk(Status status) throws TransferAbortedException {
+ logger.atFine().log("%s sending final chunk with status %s", this, status);
+ sendChunk(newChunk(Chunk.Type.COMPLETION).setStatus(status).build());
+ if (configuredProtocolVersion == ProtocolVersion.VERSION_TWO) {
+ changeState(new Terminating(status));
+ } else {
+ changeState(new Completed(status));
+ }
+ }
+
+ /** Invokes the transfer progress callback and logs the progress. */
+ final void updateProgress(long bytesSent, long bytesConfirmedReceived, long totalSizeBytes) {
+ TransferProgress progress =
+ TransferProgress.create(bytesSent, bytesConfirmedReceived, totalSizeBytes);
+ progressCallback.accept(progress);
+
+ long durationNanos = Duration.between(startTime, Instant.now()).toNanos();
+ long totalRate = durationNanos == 0 ? 0 : (bytesSent * 1_000_000_000 / durationNanos);
+
+ logger.atFiner().log("%s progress: "
+ + "%5.1f%% (%d B sent, %d B confirmed received of %s B total) at %d B/s",
+ this,
+ progress.percentReceived(),
+ bytesSent,
+ bytesConfirmedReceived,
+ totalSizeBytes == UNKNOWN_TRANSFER_SIZE ? "unknown" : totalSizeBytes,
+ totalRate);
+ }
+
+ interface State {
+ /**
+ * Called to handle a non-final chunk for this transfer.
+ */
+ void handleDataChunk(VersionedChunk chunk) throws TransferAbortedException;
+
+ /**
+ * Called to handle the final chunk for this transfer.
+ */
+ void handleFinalChunk(int statusCode) throws TransferAbortedException;
+
+ /**
+ * Called when this transfer's deadline expires.
+ */
+ void handleTimeout() throws TransferAbortedException;
+
+ /**
+ * Called if the transfer is cancelled by the user.
+ */
+ void handleCancellation();
+
+ /**
+ * Called when the transfer thread is shutting down.
+ */
+ void handleTermination();
+ }
+
+ /** Represents an active state in the transfer state machine. */
+ abstract class ActiveState implements State {
+ @Override
+ public final void handleFinalChunk(int statusCode) throws TransferAbortedException {
+ Status status = Status.fromCode(statusCode);
+ if (status == null) {
+ logger.atWarning().log("Received invalid status value %d, using INTERNAL", statusCode);
+ status = Status.INTERNAL;
+ }
+
+ // If this is not version 2, immediately clean up. If it is, send the COMPLETION_ACK first and
+ // clean up if that succeeded.
+ if (configuredProtocolVersion == ProtocolVersion.VERSION_TWO) {
+ sendChunk(newChunk(Chunk.Type.COMPLETION_ACK).build());
+ }
+ changeState(new Completed(status));
+ }
+
+ /** Enters the recovery state and returns to this state if recovery succeeds. */
+ @Override
+ public void handleTimeout() throws TransferAbortedException {
+ changeState(new TimeoutRecovery(this)).handleTimeout();
+ }
+
+ @Override
+ public final void handleCancellation() {
+ try {
+ setStateTerminatingAndSendFinalChunk(Status.CANCELLED);
+ } catch (TransferAbortedException e) {
+ // Transfer was aborted; nothing to do.
+ }
+ }
+
+ @Override
+ public final void handleTermination() {
+ try {
+ setStateTerminatingAndSendFinalChunk(Status.ABORTED);
+ } catch (TransferAbortedException e) {
+ // Transfer was aborted; nothing to do.
+ }
+ }
+ }
+
+ private class Initiating extends ActiveState {
+ @Override
+ public void handleDataChunk(VersionedChunk chunk) throws TransferAbortedException {
+ assignSessionId(chunk.sessionId());
+
+ if (chunk.version() == ProtocolVersion.UNKNOWN) {
+ logger.atWarning().log(
+ "%s aborting due to unsupported protocol version: %s", Transfer.this, chunk);
+ setStateTerminatingAndSendFinalChunk(Status.INVALID_ARGUMENT);
+ return;
+ }
+
+ changeState(getWaitingForDataState());
+
+ if (chunk.type() != Chunk.Type.START_ACK) {
+ logger.atFine().log(
+ "%s got non-handshake chunk; reverting to legacy protocol", Transfer.this);
+ configuredProtocolVersion = ProtocolVersion.LEGACY;
+ state.handleDataChunk(chunk);
+ return;
+ }
+
+ if (chunk.version().compareTo(desiredProtocolVersion) <= 0) {
+ configuredProtocolVersion = chunk.version();
+ } else {
+ configuredProtocolVersion = desiredProtocolVersion;
+ }
+
+ logger.atFine().log("%s negotiated protocol %s (ours=%s, theirs=%s)",
+ Transfer.this,
+ configuredProtocolVersion,
+ desiredProtocolVersion,
+ chunk.version());
+
+ VersionedChunk.Builder startAckConfirmation = newChunk(Chunk.Type.START_ACK_CONFIRMATION);
+ prepareInitialChunk(startAckConfirmation);
+ sendChunk(startAckConfirmation.build());
+ }
+ }
+
+ /** Recovering from an expired timeout. */
+ class TimeoutRecovery extends ActiveState {
+ private final State nextState;
+ private int retries;
+
+ TimeoutRecovery(State nextState) {
+ this.nextState = nextState;
+ }
+
+ @Override
+ public void handleDataChunk(VersionedChunk chunk) throws TransferAbortedException {
+ changeState(nextState).handleDataChunk(chunk);
+ }
+
+ @Override
+ public void handleTimeout() throws TransferAbortedException {
+ // If the transfer timed out, skip to the completed state. Don't send any more packets.
+ if (retries >= timeoutSettings.maxRetries()) {
+ logger.atFine().log("%s exhausted its %d retries", Transfer.this, retries);
+ changeState(new Completed(Status.DEADLINE_EXCEEDED));
+ return;
+ }
+
+ if (lifetimeRetries >= timeoutSettings.maxLifetimeRetries()) {
+ logger.atFine().log("%s exhausted its %d lifetime retries", Transfer.this, retries);
+ changeState(new Completed(Status.DEADLINE_EXCEEDED));
+ return;
+ }
+
+ logger.atFiner().log("%s received no chunks for %d ms; retrying %d/%d",
+ Transfer.this,
+ timeoutSettings.timeoutMillis(),
+ retries,
+ timeoutSettings.maxRetries());
+ sendChunk(getChunkForRetry());
+ retries += 1;
+ lifetimeRetries += 1;
+ setNextChunkTimeout();
+ }
+ }
+
+ /** Transfer completed. Do nothing if the transfer is terminated or cancelled. */
+ class Terminating extends ActiveState {
+ private final Status status;
+
+ Terminating(Status status) {
+ this.status = status;
+ }
+
+ @Override
+ public void handleDataChunk(VersionedChunk chunk) {
+ if (chunk.type() == Chunk.Type.COMPLETION_ACK) {
+ changeState(new Completed(status));
+ }
+ }
+ }
+
+ class Completed implements State {
+ /** Performs final cleanup of a completed transfer. No packets are sent to the server. */
+ Completed(Status status) {
+ cleanUp();
+ logger.atInfo().log("%s completed with status %s", Transfer.this, status);
+ if (status.ok()) {
+ setFutureResult();
+ } else {
+ future.setException(new TransferError(Transfer.this, status));
+ }
+ }
+
+ /** Finishes the transfer due to an exception. No packets are sent to the server. */
+ Completed(TransferError exception) {
+ cleanUp();
+ logger.atWarning().withCause(exception).log("%s terminated with exception", Transfer.this);
+ future.setException(exception);
+ }
+
+ private void cleanUp() {
+ deadline = NO_TIMEOUT;
+ eventHandler.unregisterTransfer(Transfer.this);
+ }
+
+ @Override
+ public void handleDataChunk(VersionedChunk chunk) {
+ logger.atFiner().log("%s [Completed state]: Received unexpected data chunk", Transfer.this);
+ }
+
+ @Override
+ public void handleFinalChunk(int statusCode) {
+ logger.atFiner().log("%s [Completed state]: Received unexpected data chunk", Transfer.this);
+ }
+
+ @Override
+ public void handleTimeout() {}
+
+ @Override
+ public void handleTermination() {}
+
+ @Override
+ public void handleCancellation() {}
+ }
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java
new file mode 100644
index 000000000..248e50ea9
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferClient.java
@@ -0,0 +1,159 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import dev.pigweed.pw_rpc.MethodClient;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+
+/**
+ * Manages ongoing pw_transfer data transfers.
+ *
+ * <p>Use TransferClient to send data to and receive data from a pw_transfer service running on a
+ * pw_rpc server.
+ */
+public class TransferClient {
+ public static final TransferParameters DEFAULT_READ_TRANSFER_PARAMETERS =
+ TransferParameters.create(8192, 1024, 0);
+
+ private final TransferTimeoutSettings settings;
+ private final BooleanSupplier shouldAbortCallback;
+
+ private final TransferEventHandler transferEventHandler;
+ private final Thread transferEventHandlerThread;
+
+ private ProtocolVersion desiredProtocolVersion = ProtocolVersion.latest();
+
+ /**
+ * Creates a new transfer client for sending and receiving data with pw_transfer.
+ *
+ * @param readMethod Method client for the pw.transfer.Transfer.Read method.
+ * @param writeMethod Method client for the pw.transfer.Transfer.Write method.
+ * @param settings Settings for timeouts and retries.
+ */
+ public TransferClient(
+ MethodClient readMethod, MethodClient writeMethod, TransferTimeoutSettings settings) {
+ this(readMethod, writeMethod, settings, () -> false, TransferEventHandler::run);
+ }
+
+ /**
+ * Creates a new transfer client with a callback that can be used to terminate transfers.
+ *
+ * @param shouldAbortCallback BooleanSupplier that returns true if a transfer should be aborted.
+ */
+ public TransferClient(MethodClient readMethod,
+ MethodClient writeMethod,
+ int transferTimeoutMillis,
+ int initialTransferTimeoutMillis,
+ int maxRetries,
+ BooleanSupplier shouldAbortCallback) {
+ this(readMethod,
+ writeMethod,
+ TransferTimeoutSettings.builder()
+ .setTimeoutMillis(transferTimeoutMillis)
+ .setInitialTimeoutMillis(initialTransferTimeoutMillis)
+ .setMaxRetries(maxRetries)
+ .build(),
+ shouldAbortCallback,
+ TransferEventHandler::run);
+ }
+
+ /** Constructor exposed to package for test use only. */
+ TransferClient(MethodClient readMethod,
+ MethodClient writeMethod,
+ TransferTimeoutSettings settings,
+ BooleanSupplier shouldAbortCallback,
+ Consumer<TransferEventHandler> runFunction) {
+ this.settings = settings;
+ this.shouldAbortCallback = shouldAbortCallback;
+
+ transferEventHandler = new TransferEventHandler(readMethod, writeMethod);
+ transferEventHandlerThread = new Thread(() -> runFunction.accept(transferEventHandler));
+ transferEventHandlerThread.start();
+ }
+
+ /** Writes the provided data to the given transfer resource. */
+ public ListenableFuture<Void> write(int resourceId, byte[] data) {
+ return write(resourceId, data, transferProgress -> {});
+ }
+
+ /**
+ * Writes data to the specified transfer resource, calling the progress
+ * callback as data is sent.
+ *
+ * @param resourceId The ID of the resource to which to write
+ * @param data the data to write
+ * @param progressCallback called each time a packet is sent
+ */
+ public ListenableFuture<Void> write(
+ int resourceId, byte[] data, Consumer<TransferProgress> progressCallback) {
+ return transferEventHandler.startWriteTransferAsClient(
+ resourceId, desiredProtocolVersion, settings, data, progressCallback, shouldAbortCallback);
+ }
+
+ /** Reads the data from the given transfer resource ID. */
+ public ListenableFuture<byte[]> read(int resourceId) {
+ return read(resourceId, DEFAULT_READ_TRANSFER_PARAMETERS, progressCallback -> {});
+ }
+
+ /** Reads the data for a transfer resource, calling the progress callback as data is received. */
+ public ListenableFuture<byte[]> read(
+ int resourceId, Consumer<TransferProgress> progressCallback) {
+ return read(resourceId, DEFAULT_READ_TRANSFER_PARAMETERS, progressCallback);
+ }
+
+ /** Reads the data for a transfer resource, using the specified transfer parameters. */
+ public ListenableFuture<byte[]> read(int resourceId, TransferParameters parameters) {
+ return read(resourceId, parameters, (progressCallback) -> {});
+ }
+
+ /**
+ * Reads the data for a transfer resource, using the specified parameters and progress callback.
+ */
+ public ListenableFuture<byte[]> read(
+ int resourceId, TransferParameters parameters, Consumer<TransferProgress> progressCallback) {
+ return transferEventHandler.startReadTransferAsClient(resourceId,
+ desiredProtocolVersion,
+ settings,
+ parameters,
+ progressCallback,
+ shouldAbortCallback);
+ }
+
+ /**
+ * Sets the protocol version to request for future transfers
+ *
+ * Does not affect ongoing transfers. Version cannot be set to UNKNOWN!
+ *
+ * @throws IllegalArgumentException if the protocol version is UNKNOWN
+ */
+ public void setProtocolVersion(ProtocolVersion version) {
+ if (version == ProtocolVersion.UNKNOWN) {
+ throw new IllegalArgumentException("Cannot set protocol version to UNKNOWN!");
+ }
+ desiredProtocolVersion = version;
+ }
+
+ /** Stops the background thread and waits until it terminates. */
+ public void close() throws InterruptedException {
+ transferEventHandler.stop();
+ transferEventHandlerThread.join();
+ }
+
+ void waitUntilEventsAreProcessedForTest() {
+ transferEventHandler.waitUntilEventsAreProcessedForTest();
+ }
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferError.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferError.java
new file mode 100644
index 000000000..c059c05c7
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferError.java
@@ -0,0 +1,46 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import dev.pigweed.pw_rpc.Status;
+import java.util.Locale;
+
+/** Represents errors that terminate a transfer. */
+public class TransferError extends Exception {
+ private final Status error;
+
+ TransferError(String message, Throwable cause) {
+ super(message, cause);
+ error = Status.UNKNOWN;
+ }
+
+ TransferError(Transfer<?> transfer, Status error) {
+ this(String.format(Locale.ENGLISH, "%s failed with status %s", transfer, error.name()), error);
+ }
+
+ TransferError(String msg, Status error) {
+ super(msg);
+ this.error = error;
+ }
+
+ /**
+ * Returns the pw_transfer error code that terminated this transfer.
+ *
+ * <p>UNKNOWN indicates that the transfer was terminated by an exception.
+ */
+ public Status status() {
+ return error;
+ }
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java
new file mode 100644
index 000000000..f4e8189f7
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferEventHandler.java
@@ -0,0 +1,321 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import dev.pigweed.pw_log.Logger;
+import dev.pigweed.pw_rpc.Call;
+import dev.pigweed.pw_rpc.ChannelOutputException;
+import dev.pigweed.pw_rpc.MethodClient;
+import dev.pigweed.pw_rpc.Status;
+import dev.pigweed.pw_rpc.StreamObserver;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.TemporalUnit;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import javax.annotation.Nullable;
+
+/** Manages the active transfers and dispatches events to them. */
+class TransferEventHandler {
+ private static final Logger logger = Logger.forClass(TransferEventHandler.class);
+
+ // Instant and BlockingQueue use different time unit types.
+ private static final TemporalUnit TIME_UNIT = ChronoUnit.MICROS;
+ private static final TimeUnit POLL_TIME_UNIT = TimeUnit.MICROSECONDS;
+
+ private final MethodClient readMethod;
+ private final MethodClient writeMethod;
+
+ private final BlockingQueue<Event> events = new LinkedBlockingQueue<>();
+
+ // Map resource ID to transfer, and session ID to resource ID.
+ private final Map<Integer, Transfer<?>> resourceIdToTransfer = new HashMap<>();
+ private final Map<Integer, Integer> sessionToResourceId = new HashMap<>();
+
+ @Nullable private Call.ClientStreaming<Chunk> readStream = null;
+ @Nullable private Call.ClientStreaming<Chunk> writeStream = null;
+ private boolean processEvents = true;
+
+ TransferEventHandler(MethodClient readMethod, MethodClient writeMethod) {
+ this.readMethod = readMethod;
+ this.writeMethod = writeMethod;
+ }
+
+ ListenableFuture<Void> startWriteTransferAsClient(int resourceId,
+ ProtocolVersion desiredProtocolVersion,
+ TransferTimeoutSettings settings,
+ byte[] data,
+ Consumer<TransferProgress> progressCallback,
+ BooleanSupplier shouldAbortCallback) {
+ WriteTransfer transfer =
+ new WriteTransfer(resourceId, desiredProtocolVersion, new TransferInterface() {
+ @Override
+ Call.ClientStreaming<Chunk> getStream() throws ChannelOutputException {
+ if (writeStream == null) {
+ writeStream = writeMethod.invokeBidirectionalStreaming(new ChunkHandler() {
+ @Override
+ void resetStream() {
+ writeStream = null;
+ }
+ });
+ }
+ return writeStream;
+ }
+ }, settings, data, progressCallback, shouldAbortCallback);
+ startTransferAsClient(transfer);
+ return transfer.getFuture();
+ }
+
+ ListenableFuture<byte[]> startReadTransferAsClient(int resourceId,
+ ProtocolVersion desiredProtocolVersion,
+ TransferTimeoutSettings settings,
+ TransferParameters parameters,
+ Consumer<TransferProgress> progressCallback,
+ BooleanSupplier shouldAbortCallback) {
+ ReadTransfer transfer =
+ new ReadTransfer(resourceId, desiredProtocolVersion, new TransferInterface() {
+ @Override
+ Call.ClientStreaming<Chunk> getStream() throws ChannelOutputException {
+ if (readStream == null) {
+ readStream = readMethod.invokeBidirectionalStreaming(new ChunkHandler() {
+ @Override
+ void resetStream() {
+ readStream = null;
+ }
+ });
+ }
+ return readStream;
+ }
+ }, settings, parameters, progressCallback, shouldAbortCallback);
+ startTransferAsClient(transfer);
+ return transfer.getFuture();
+ }
+
+ private void startTransferAsClient(Transfer<?> transfer) {
+ enqueueEvent(() -> {
+ if (resourceIdToTransfer.containsKey(transfer.getResourceId())) {
+ transfer.terminate(new TransferError("A transfer for resource ID "
+ + transfer.getResourceId()
+ + " is already in progress! Only one read/write transfer per resource is supported at a time",
+ Status.ALREADY_EXISTS));
+ return;
+ }
+ resourceIdToTransfer.put(transfer.getResourceId(), transfer);
+ transfer.start();
+ });
+ }
+
+ /** Handles events until stop() is called. */
+ void run() {
+ while (processEvents) {
+ handleNextEvent();
+ handleTimeouts();
+ }
+ }
+
+ /**
+ * Test version of run() that processes all enqueued events before checking for timeouts.
+ *
+ * Tests that need to time out should process all enqueued events first to prevent flaky failures.
+ * If handling one of several queued packets takes longer than the timeout (which must be short
+ * for a unit test), then the test may fail spuriously.
+ *
+ * This run function is not used outside of tests because processing all incoming packets before
+ * checking for timeouts could delay the transfer client's outgoing write packets if there are
+ * lots of inbound packets. This could delay transfers and cause unnecessary timeouts.
+ */
+ void runForTestsThatMustTimeOut() {
+ while (processEvents) {
+ while (!events.isEmpty()) {
+ handleNextEvent();
+ }
+ handleTimeouts();
+ }
+ }
+ /** Stops the transfer event handler from processing events. */
+ void stop() {
+ enqueueEvent(() -> {
+ logger.atFine().log("Terminating TransferEventHandler");
+ resourceIdToTransfer.values().forEach(Transfer::handleTermination);
+ processEvents = false;
+ });
+ }
+
+ /** Blocks until all events currently in the queue are processed; for test use only. */
+ void waitUntilEventsAreProcessedForTest() {
+ Semaphore semaphore = new Semaphore(0);
+ enqueueEvent(semaphore::release);
+ try {
+ semaphore.acquire();
+ } catch (InterruptedException e) {
+ throw new AssertionError("Unexpectedly interrupted", e);
+ }
+ }
+
+ private void enqueueEvent(Event event) {
+ while (true) {
+ try {
+ events.put(event);
+ return;
+ } catch (InterruptedException e) {
+ // Ignore and keep trying.
+ }
+ }
+ }
+
+ private void handleNextEvent() {
+ final long sleepFor = TIME_UNIT.between(Instant.now(), getNextTimeout());
+ try {
+ Event event = events.poll(sleepFor, POLL_TIME_UNIT);
+ if (event != null) {
+ event.handle();
+ }
+ } catch (InterruptedException e) {
+ // If interrupted, continue around the loop.
+ }
+ }
+
+ private void handleTimeouts() {
+ for (Transfer<?> transfer : resourceIdToTransfer.values()) {
+ transfer.handleTimeoutIfDeadlineExceeded();
+ }
+ }
+
+ private Instant getNextTimeout() {
+ Optional<Transfer<?>> transfer =
+ resourceIdToTransfer.values().stream().min(Comparator.comparing(Transfer::getDeadline));
+ return transfer.isPresent() ? transfer.get().getDeadline() : Transfer.NO_TIMEOUT;
+ }
+
+ /** This interface gives a Transfer access to the TransferEventHandler. */
+ abstract class TransferInterface {
+ private TransferInterface() {}
+
+ /**
+ * Sends the provided transfer chunk.
+ *
+ * Must be called on the transfer thread.
+ */
+ void sendChunk(Chunk chunk) throws TransferError {
+ try {
+ getStream().write(chunk);
+ } catch (ChannelOutputException e) {
+ throw new TransferError("Failed to send chunk for write transfer", e);
+ }
+ }
+
+ /**
+ * Associates the transfer's session ID with its resource ID.
+ *
+ * Must be called on the transfer thread.
+ */
+ void assignSessionId(Transfer<?> transfer) {
+ sessionToResourceId.put(transfer.getSessionId(), transfer.getResourceId());
+ }
+
+ /**
+ * Removes this transfer from the list of active transfers.
+ *
+ * Must be called on the transfer thread.
+ */
+ void unregisterTransfer(Transfer<?> transfer) {
+ resourceIdToTransfer.remove(transfer.getResourceId());
+ sessionToResourceId.remove(transfer.getSessionId());
+ }
+
+ /**
+ * Initiates the cancellation process for the provided transfer.
+ *
+ * May be called from any thread.
+ */
+ void cancelTransfer(Transfer<?> transfer) {
+ enqueueEvent(transfer::handleCancellation);
+ }
+
+ /** Gets either the read or write stream. */
+ abstract Call.ClientStreaming<Chunk> getStream() throws ChannelOutputException;
+ }
+
+ /** Handles responses on the pw_transfer RPCs. */
+ private abstract class ChunkHandler implements StreamObserver<Chunk> {
+ @Override
+ public final void onNext(Chunk chunkProto) {
+ VersionedChunk chunk = VersionedChunk.fromMessage(chunkProto);
+
+ enqueueEvent(() -> {
+ Transfer<?> transfer = null;
+ if (chunk.resourceId().isPresent()) {
+ transfer = resourceIdToTransfer.get(chunk.resourceId().getAsInt());
+ } else {
+ Integer resourceId = sessionToResourceId.get(chunk.sessionId());
+ if (resourceId != null) {
+ transfer = resourceIdToTransfer.get(resourceId);
+ }
+ }
+
+ if (transfer != null) {
+ logger.atFinest().log("%s received chunk: %s", transfer, chunk);
+ transfer.handleChunk(chunk);
+ } else {
+ logger.atInfo().log("Ignoring unrecognized transfer chunk: %s", chunk);
+ }
+ });
+ }
+
+ @Override
+ public final void onCompleted(Status status) {
+ onError(Status.INTERNAL); // This RPC should never complete: treat as an internal error.
+ }
+
+ @Override
+ public final void onError(Status status) {
+ enqueueEvent(() -> {
+ resetStream();
+
+ // The transfers remove themselves from the Map during cleanup, iterate over a copied list.
+ List<Transfer<?>> activeTransfers = new ArrayList<>(resourceIdToTransfer.values());
+
+ // FAILED_PRECONDITION indicates that the stream packet was not recognized as the stream is
+ // not open. This could occur if the server resets. Notify pending transfers that this has
+ // occurred so they can restart.
+ if (status.equals(Status.FAILED_PRECONDITION)) {
+ activeTransfers.forEach(Transfer::handleDisconnection);
+ } else {
+ TransferError error = new TransferError(
+ "Transfer stream RPC closed unexpectedly with status " + status, Status.INTERNAL);
+ activeTransfers.forEach(t -> t.terminate(error));
+ }
+ });
+ }
+
+ abstract void resetStream();
+ }
+
+ // Represents an event that occurs during a transfer
+ private interface Event {
+ void handle();
+ }
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferParameters.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferParameters.java
new file mode 100644
index 000000000..b0a5f6843
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferParameters.java
@@ -0,0 +1,41 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import com.google.auto.value.AutoValue;
+
+/**
+ * Transfer parameters set by the receiver.
+ *
+ * <p>In a client, these are only used for read transfers. These values can be adjusted to optimize
+ * for the service/client limitations and the transport between them.
+ */
+@AutoValue
+public abstract class TransferParameters {
+ public static TransferParameters create(
+ int maxPendingBytes, int maxChunkSizeBytes, int chunkDelayMicroseconds) {
+ return new AutoValue_TransferParameters(
+ maxPendingBytes, maxChunkSizeBytes, chunkDelayMicroseconds);
+ }
+
+ /** Max number of bytes to request at once. Should be a multiple of maxChunkSizeBytes. */
+ public abstract int maxPendingBytes();
+
+ /** Max number of bytes to send in a single chunk. Should be a factor of maxPendingBytes. */
+ public abstract int maxChunkSizeBytes();
+
+ /** How long to require the sender to wait between sending chunks. */
+ public abstract int chunkDelayMicroseconds();
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferProgress.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferProgress.java
new file mode 100644
index 000000000..f3f4873fb
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferProgress.java
@@ -0,0 +1,65 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import com.google.auto.value.AutoValue;
+
+/** Values used to report the progress of an ongoing read or write transfer. */
+@AutoValue
+public abstract class TransferProgress {
+ public static final long UNKNOWN_TRANSFER_SIZE = -1;
+
+ static TransferProgress create(long bytesSent, long bytesConfirmedReceived, long totalSizeBytes) {
+ return new AutoValue_TransferProgress(bytesSent, bytesConfirmedReceived, totalSizeBytes);
+ }
+
+ /**
+ * Number of bytes the sender has sent.
+ *
+ * <p>For the receiver, this value is the same as bytesConfirmedReceived().
+ *
+ * <p>For the sender, this is the current send offset, even if the receiver has not confirmed
+ * receipt by updating the transfer parameters. This value may decrease if the receiver requests
+ * that data is resent due to loss.
+ */
+ public abstract long bytesSent();
+
+ /**
+ * Bytes the receiver has confirmed received. This value is monotonically non-decreasing.
+ *
+ * <p>For the receiver, this value is the latest offset.
+ *
+ * <p>For the sender, this value is the latest offset the receiver sent in a transfer parameters
+ * update.
+ */
+ public abstract long bytesConfirmedReceived();
+
+ /**
+ * Total bytes expected to be transferred. Set to UNKNOWN_TRANSFER_SIZE if the total is not known.
+ *
+ * <p>For the receiver, this value is based on the most recent remaining bytes value.
+ *
+ * <p>For the sender, this value is the number of bytes it expects to send (if known).
+ */
+ public abstract long totalSizeBytes();
+
+ /** Returns bytesConfirmedReceived() of totalSizeBytes() as a percent or NaN if unknown. */
+ public final float percentReceived() {
+ if (totalSizeBytes() == UNKNOWN_TRANSFER_SIZE) {
+ return Float.NaN;
+ }
+ return (float) bytesConfirmedReceived() / totalSizeBytes() * 100;
+ }
+}
diff --git a/pw_assert_log/public/pw_assert_log/assert_lite_log.h b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferService.java
index 08ddcbc8c..f2c21b496 100644
--- a/pw_assert_log/public/pw_assert_log/assert_lite_log.h
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferService.java
@@ -11,16 +11,22 @@
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
-#pragma once
-#include "pw_log/levels.h"
-#include "pw_log/log.h"
-#include "pw_log/options.h"
-#include "pw_preprocessor/compiler.h"
+package dev.pigweed.pw_transfer;
-#define PW_ASSERT_HANDLE_FAILURE(condition_string) \
- do { \
- PW_LOG( \
- PW_LOG_LEVEL_FATAL, PW_LOG_FLAGS, "Assert failed: " condition_string); \
- PW_UNREACHABLE; \
- } while (0)
+import static dev.pigweed.pw_rpc.Service.bidirectionalStreamingMethod;
+
+import dev.pigweed.pw_rpc.Service;
+
+/** Provides a service definition for the pw_transfer service. */
+public class TransferService {
+ private static final Service SERVICE = new Service("pw.transfer.Transfer",
+ bidirectionalStreamingMethod("Read", Chunk.class, Chunk.class),
+ bidirectionalStreamingMethod("Write", Chunk.class, Chunk.class));
+
+ public static Service get() {
+ return SERVICE;
+ }
+
+ private TransferService() {}
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferTimeoutSettings.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferTimeoutSettings.java
new file mode 100644
index 000000000..2ad776140
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/TransferTimeoutSettings.java
@@ -0,0 +1,70 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.base.Preconditions;
+
+/**
+ * Transfer client settings for timeouts and retries.
+ */
+@AutoValue
+public abstract class TransferTimeoutSettings {
+ /** Amount of time to wait for a packet before resending the last packet. */
+ public abstract int timeoutMillis();
+
+ /** Amount of time to wait for the first packet before retrying the transfer. */
+ public abstract int initialTimeoutMillis();
+
+ /** Maximum number of times to retry sending a packet. */
+ public abstract int maxRetries();
+
+ /** Maximum number of retries to allow before aborting the transfer. */
+ public abstract int maxLifetimeRetries();
+
+ /** Creates a builder with defaults applied to all fields. */
+ public static TransferTimeoutSettings.Builder builder() {
+ return new AutoValue_TransferTimeoutSettings.Builder()
+ .setTimeoutMillis(3000)
+ .setInitialTimeoutMillis(6000)
+ .setMaxRetries(5)
+ .setMaxLifetimeRetries(5000);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setTimeoutMillis(int timeoutMillis);
+
+ public abstract Builder setInitialTimeoutMillis(int initialTimeoutMillis);
+
+ public abstract Builder setMaxRetries(int maxRetries);
+
+ public abstract Builder setMaxLifetimeRetries(int maxLifetimeRetries);
+
+ public final TransferTimeoutSettings build() {
+ TransferTimeoutSettings settings = autoBuild();
+ Preconditions.checkState(
+ settings.timeoutMillis() >= 0, "Negative timeouts are not permitted");
+ Preconditions.checkState(settings.initialTimeoutMillis() >= settings.timeoutMillis(),
+ "The initial timeout must be at least as long as the regular timeout");
+ Preconditions.checkState(settings.maxRetries() >= 0, "Retries must be positive");
+ Preconditions.checkState(settings.maxLifetimeRetries() >= settings.maxRetries(),
+ "Lifetime max retries cannot be smaller than max retries");
+ return settings;
+ }
+
+ abstract TransferTimeoutSettings autoBuild();
+ }
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java
new file mode 100644
index 000000000..9fdfb4d1e
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/VersionedChunk.java
@@ -0,0 +1,208 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import com.google.auto.value.AutoValue;
+import com.google.protobuf.ByteString;
+import dev.pigweed.pw_rpc.Status;
+import java.util.OptionalInt;
+import java.util.OptionalLong;
+
+/**
+ * Abstraction of the Chunk proto that supports different protocol versions.
+ */
+@AutoValue
+abstract class VersionedChunk {
+ public static final int UNASSIGNED_SESSION_ID = 0;
+
+ public abstract ProtocolVersion version();
+
+ public abstract Chunk.Type type();
+
+ public abstract int sessionId();
+
+ public abstract OptionalInt resourceId();
+
+ public abstract int offset();
+
+ public abstract int windowEndOffset();
+
+ public abstract ByteString data();
+
+ public abstract OptionalLong remainingBytes();
+
+ public abstract OptionalInt maxChunkSizeBytes();
+
+ public abstract OptionalInt minDelayMicroseconds();
+
+ public abstract OptionalInt status();
+
+ public static Builder builder() {
+ return new AutoValue_VersionedChunk.Builder()
+ .setSessionId(UNASSIGNED_SESSION_ID)
+ .setOffset(0)
+ .setWindowEndOffset(0)
+ .setData(ByteString.EMPTY);
+ }
+
+ @AutoValue.Builder
+ public abstract static class Builder {
+ public abstract Builder setVersion(ProtocolVersion version);
+
+ public abstract Builder setType(Chunk.Type type);
+
+ public abstract Builder setSessionId(int sessionId);
+
+ public abstract Builder setResourceId(int resourceId);
+
+ public abstract Builder setOffset(int offset);
+
+ public abstract Builder setWindowEndOffset(int windowEndOffset);
+
+ public abstract Builder setData(ByteString data);
+
+ public abstract Builder setRemainingBytes(long remainingBytes);
+
+ public abstract Builder setMaxChunkSizeBytes(int maxChunkSizeBytes);
+
+ public abstract Builder setMinDelayMicroseconds(int minDelayMicroseconds);
+
+ public final Builder setStatus(Status status) {
+ return setStatus(status.code());
+ }
+
+ abstract Builder setStatus(int statusCode);
+
+ public abstract VersionedChunk build();
+ }
+
+ public static VersionedChunk fromMessage(Chunk chunk) {
+ Builder builder = builder();
+
+ ProtocolVersion version;
+ if (chunk.hasProtocolVersion()) {
+ if (chunk.getProtocolVersion() < ProtocolVersion.values().length) {
+ version = ProtocolVersion.values()[chunk.getProtocolVersion()];
+ } else {
+ version = ProtocolVersion.UNKNOWN;
+ }
+ } else if (chunk.hasSessionId()) {
+ version = ProtocolVersion.VERSION_TWO;
+ } else {
+ version = ProtocolVersion.LEGACY;
+ }
+ builder.setVersion(version);
+
+ if (chunk.hasType()) {
+ builder.setType(chunk.getType());
+ } else if (chunk.getOffset() == 0 && chunk.getData().isEmpty() && !chunk.hasStatus()) {
+ builder.setType(Chunk.Type.START);
+ } else if (!chunk.getData().isEmpty()) {
+ builder.setType(Chunk.Type.DATA);
+ } else if (chunk.hasStatus()) {
+ builder.setType(Chunk.Type.COMPLETION);
+ } else {
+ builder.setType(Chunk.Type.PARAMETERS_RETRANSMIT);
+ }
+
+ // For legacy chunks, use the transfer ID as both the resource and session IDs.
+ if (version == ProtocolVersion.LEGACY) {
+ builder.setSessionId(chunk.getTransferId());
+ builder.setResourceId(chunk.getTransferId());
+ if (chunk.hasStatus()) {
+ builder.setType(Chunk.Type.COMPLETION);
+ }
+ } else {
+ builder.setSessionId(chunk.getSessionId());
+ }
+
+ builder.setOffset((int) chunk.getOffset()).setData(chunk.getData());
+
+ if (chunk.hasResourceId()) {
+ builder.setResourceId(chunk.getResourceId());
+ }
+ if (chunk.hasPendingBytes()) {
+ builder.setWindowEndOffset((int) chunk.getOffset() + chunk.getPendingBytes());
+ } else {
+ builder.setWindowEndOffset(chunk.getWindowEndOffset());
+ }
+ if (chunk.hasRemainingBytes()) {
+ builder.setRemainingBytes(chunk.getRemainingBytes());
+ }
+ if (chunk.hasMaxChunkSizeBytes()) {
+ builder.setMaxChunkSizeBytes(chunk.getMaxChunkSizeBytes());
+ }
+ if (chunk.hasMinDelayMicroseconds()) {
+ builder.setMinDelayMicroseconds(chunk.getMinDelayMicroseconds());
+ }
+ if (chunk.hasStatus()) {
+ builder.setStatus(chunk.getStatus());
+ }
+ return builder.build();
+ }
+
+ public static VersionedChunk.Builder createInitialChunk(
+ ProtocolVersion desiredVersion, int resourceId) {
+ return builder().setVersion(desiredVersion).setType(Chunk.Type.START).setResourceId(resourceId);
+ }
+
+ public Chunk toMessage() {
+ Chunk.Builder chunk = Chunk.newBuilder()
+ .setType(type())
+ .setOffset(offset())
+ .setWindowEndOffset(windowEndOffset())
+ .setData(data());
+
+ resourceId().ifPresent(chunk::setResourceId);
+ remainingBytes().ifPresent(chunk::setRemainingBytes);
+ maxChunkSizeBytes().ifPresent(chunk::setMaxChunkSizeBytes);
+ minDelayMicroseconds().ifPresent(chunk::setMinDelayMicroseconds);
+ status().ifPresent(chunk::setStatus);
+
+ // session_id did not exist in the legacy protocol, so don't send it.
+ if (version() != ProtocolVersion.LEGACY && sessionId() != UNASSIGNED_SESSION_ID) {
+ chunk.setSessionId(sessionId());
+ }
+
+ if (shouldEncodeLegacyFields()) {
+ chunk.setTransferId(resourceId().orElse(sessionId()));
+
+ if (chunk.getWindowEndOffset() != 0) {
+ chunk.setPendingBytes(chunk.getWindowEndOffset() - offset());
+ }
+ }
+
+ if (isInitialHandshakeChunk()) {
+ chunk.setProtocolVersion(version().ordinal());
+ }
+
+ return chunk.build();
+ }
+
+ private boolean isInitialHandshakeChunk() {
+ return version() == ProtocolVersion.VERSION_TWO
+ && (type() == Chunk.Type.START || type() == Chunk.Type.START_ACK
+ || type() == Chunk.Type.START_ACK_CONFIRMATION);
+ }
+
+ public final boolean requestsTransmissionFromOffset() {
+ return type() == Chunk.Type.PARAMETERS_RETRANSMIT || type() == Chunk.Type.START_ACK_CONFIRMATION
+ || type() == Chunk.Type.START;
+ }
+
+ private boolean shouldEncodeLegacyFields() {
+ return version() == ProtocolVersion.LEGACY || type() == Chunk.Type.START;
+ }
+}
diff --git a/pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java b/pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java
new file mode 100644
index 000000000..daf5213f7
--- /dev/null
+++ b/pw_transfer/java/main/dev/pigweed/pw_transfer/WriteTransfer.java
@@ -0,0 +1,189 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import static java.lang.Math.min;
+
+import com.google.protobuf.ByteString;
+import dev.pigweed.pw_log.Logger;
+import dev.pigweed.pw_rpc.Status;
+import dev.pigweed.pw_transfer.TransferEventHandler.TransferInterface;
+import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+
+class WriteTransfer extends Transfer<Void> {
+ private static final Logger logger = Logger.forClass(WriteTransfer.class);
+
+ // Short chunk delays often turn into much longer delays. Ignore delays <10ms to avoid impacting
+ // performance.
+ private static final int MIN_CHUNK_DELAY_TO_SLEEP_MICROS = 10000;
+
+ private int maxChunkSizeBytes = 0;
+ private int minChunkDelayMicros = 0;
+ private int sentOffset;
+ private long totalDroppedBytes;
+
+ private final byte[] data;
+
+ protected WriteTransfer(int resourceId,
+ ProtocolVersion desiredProtocolVersion,
+ TransferInterface transferManager,
+ TransferTimeoutSettings timeoutSettings,
+ byte[] data,
+ Consumer<TransferProgress> progressCallback,
+ BooleanSupplier shouldAbortCallback) {
+ super(resourceId,
+ desiredProtocolVersion,
+ transferManager,
+ timeoutSettings,
+ progressCallback,
+ shouldAbortCallback);
+ this.data = data;
+ }
+
+ @Override
+ void prepareInitialChunk(VersionedChunk.Builder chunk) {
+ chunk.setRemainingBytes(data.length);
+ }
+
+ @Override
+ State getWaitingForDataState() {
+ return new WaitingForTransferParameters();
+ }
+
+ private class WaitingForTransferParameters extends ActiveState {
+ @Override
+ public void handleDataChunk(VersionedChunk chunk) throws TransferAbortedException {
+ updateTransferParameters(chunk);
+ }
+ }
+
+ /** Transmitting a transfer window. */
+ private class Transmitting extends ActiveState {
+ private final int windowStartOffset;
+ private final int windowEndOffset;
+
+ Transmitting(int windowStartOffset, int windowEndOffset) {
+ this.windowStartOffset = windowStartOffset;
+ this.windowEndOffset = windowEndOffset;
+ }
+
+ @Override
+ public void handleDataChunk(VersionedChunk chunk) throws TransferAbortedException {
+ updateTransferParameters(chunk);
+ }
+
+ @Override
+ public void handleTimeout() throws TransferAbortedException {
+ ByteString chunkData = ByteString.copyFrom(
+ data, sentOffset, min(windowEndOffset - sentOffset, maxChunkSizeBytes));
+
+ if (VERBOSE_LOGGING) {
+ logger.atFinest().log("%s sending bytes %d-%d (%d B chunk, max size %d B)",
+ WriteTransfer.this,
+ sentOffset,
+ sentOffset + chunkData.size() - 1,
+ chunkData.size(),
+ maxChunkSizeBytes);
+ }
+
+ sendChunk(buildDataChunk(chunkData));
+
+ sentOffset += chunkData.size();
+ updateProgress(sentOffset, windowStartOffset, data.length);
+
+ if (sentOffset < windowEndOffset) {
+ setTimeoutMicros(minChunkDelayMicros);
+ return; // Keep transmitting packets
+ }
+ setNextChunkTimeout();
+ changeState(new WaitingForTransferParameters());
+ }
+ }
+
+ @Override
+ VersionedChunk getChunkForRetry() {
+ // The service should resend transfer parameters if there was a timeout. In case the service
+ // doesn't support timeouts and to avoid unnecessary waits, resend the last chunk. If there
+ // were drops, this will trigger a transfer parameters update.
+ return getLastChunkSent();
+ }
+
+ @Override
+ void setFutureResult() {
+ updateProgress(data.length, data.length, data.length);
+ getFuture().set(null);
+ }
+
+ private void updateTransferParameters(VersionedChunk chunk) throws TransferAbortedException {
+ logger.atFiner().log("%s received new chunk %s", this, chunk);
+
+ if (chunk.offset() > data.length) {
+ setStateTerminatingAndSendFinalChunk(Status.OUT_OF_RANGE);
+ return;
+ }
+
+ int windowEndOffset = min(chunk.windowEndOffset(), data.length);
+ if (chunk.requestsTransmissionFromOffset()) {
+ long droppedBytes = sentOffset - chunk.offset();
+ if (droppedBytes > 0) {
+ totalDroppedBytes += droppedBytes;
+ logger.atFine().log("%s retransmitting %d B (%d retransmitted of %d sent)",
+ this,
+ droppedBytes,
+ totalDroppedBytes,
+ sentOffset);
+ }
+ sentOffset = chunk.offset();
+ } else if (windowEndOffset <= sentOffset) {
+ logger.atFiner().log("%s ignoring old rolling window packet", this);
+ setNextChunkTimeout();
+ return; // Received an old rolling window packet, ignore it.
+ }
+
+ // Update transfer parameters if they're set.
+ chunk.maxChunkSizeBytes().ifPresent(size -> maxChunkSizeBytes = size);
+ chunk.minDelayMicroseconds().ifPresent(delay -> {
+ if (delay > MIN_CHUNK_DELAY_TO_SLEEP_MICROS) {
+ minChunkDelayMicros = delay;
+ }
+ });
+
+ if (maxChunkSizeBytes == 0) {
+ if (windowEndOffset == sentOffset) {
+ logger.atWarning().log("%s server requested 0 bytes; aborting", this);
+ setStateTerminatingAndSendFinalChunk(Status.INVALID_ARGUMENT);
+ return;
+ }
+ // Default to sending the entire window if the max chunk size is not specified (or is 0).
+ maxChunkSizeBytes = windowEndOffset - sentOffset;
+ }
+
+ // Enter the transmitting state and immediately send the first packet
+ changeState(new Transmitting(chunk.offset(), windowEndOffset)).handleTimeout();
+ }
+
+ private VersionedChunk buildDataChunk(ByteString chunkData) {
+ VersionedChunk.Builder chunk =
+ newChunk(Chunk.Type.DATA).setOffset(sentOffset).setData(chunkData);
+
+ // If this is the last data chunk, setRemainingBytes to 0.
+ if (sentOffset + chunkData.size() == data.length) {
+ logger.atFiner().log("%s sending final chunk with %d B", this, chunkData.size());
+ chunk.setRemainingBytes(0);
+ }
+ return chunk.build();
+ }
+}
diff --git a/pw_transfer/java/test/dev/pigweed/pw_transfer/BUILD.bazel b/pw_transfer/java/test/dev/pigweed/pw_transfer/BUILD.bazel
new file mode 100644
index 000000000..af824bb39
--- /dev/null
+++ b/pw_transfer/java/test/dev/pigweed/pw_transfer/BUILD.bazel
@@ -0,0 +1,45 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Tests for the generic pw_rpc client.
+
+licenses(["notice"])
+
+java_test(
+ name = "TransferClientTest",
+ size = "small",
+ srcs = ["TransferClientTest.java"],
+ test_class = "dev.pigweed.pw_transfer.TransferClientTest",
+ visibility = ["//visibility:public"],
+ deps = [
+ "//pw_log/java/main/dev/pigweed/pw_log",
+ "//pw_rpc/java/main/dev/pigweed/pw_rpc:client",
+ "//pw_rpc/java/test/dev/pigweed/pw_rpc:test_client",
+ "//pw_transfer:transfer_proto_java_lite",
+ "//pw_transfer/java/main/dev/pigweed/pw_transfer:client",
+ "@com_google_protobuf//java/lite",
+ "@maven//:com_google_code_findbugs_jsr305",
+ "@maven//:com_google_flogger_flogger_system_backend",
+ "@maven//:com_google_guava_guava",
+ "@maven//:com_google_truth_truth",
+ "@maven//:org_mockito_mockito_core",
+ ],
+)
+
+test_suite(
+ name = "pw_transfer",
+ tests = [
+ ":TransferClientTest",
+ ],
+)
diff --git a/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java b/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java
new file mode 100644
index 000000000..3a61b362a
--- /dev/null
+++ b/pw_transfer/java/test/dev/pigweed/pw_transfer/TransferClientTest.java
@@ -0,0 +1,2511 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package dev.pigweed.pw_transfer;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.protobuf.ByteString;
+import dev.pigweed.pw_rpc.ChannelOutputException;
+import dev.pigweed.pw_rpc.Status;
+import dev.pigweed.pw_rpc.TestClient;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public final class TransferClientTest {
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ private static final int CHANNEL_ID = 1;
+ private static final String SERVICE = "pw.transfer.Transfer";
+
+ private static final ByteString TEST_DATA_SHORT = ByteString.copyFromUtf8("O_o");
+ private static final ByteString TEST_DATA_100B = range(0, 100);
+
+ private static final TransferParameters TRANSFER_PARAMETERS =
+ TransferParameters.create(50, 30, 0);
+ private static final int MAX_RETRIES = 2;
+
+ private boolean shouldAbortFlag = false;
+ private TestClient rpcClient;
+ private TransferClient transferClient;
+
+ @Mock private Consumer<TransferProgress> progressCallback;
+ @Captor private ArgumentCaptor<TransferProgress> progress;
+
+ @Before
+ public void setup() {
+ rpcClient = new TestClient(ImmutableList.of(TransferService.get()));
+ }
+
+ @After
+ public void tearDown() {
+ try {
+ transferClient.close();
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Test
+ public void legacy_read_singleChunk_successful() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(1);
+ assertThat(future.isDone()).isFalse();
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 1)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0));
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
+ }
+
+ @Test
+ public void legacy_read_failedPreconditionError_retriesInitialPacket() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.LEGACY));
+
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.LEGACY));
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 1)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0));
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
+ }
+
+ @Test
+ public void legacy_read_failedPreconditionError_abortsAfterInitialPacket() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ TransferParameters params = TransferParameters.create(50, 50, 0);
+ ListenableFuture<byte[]> future = transferClient.read(1, params);
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.LEGACY, params));
+
+ receiveReadChunks(legacyDataChunk(1, TEST_DATA_100B, 0, 50));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 1)
+ .setOffset(50)
+ .setPendingBytes(50)
+ .setWindowEndOffset(100)
+ .setMaxChunkSizeBytes(50)
+ .build());
+
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void legacy_read_failedPreconditionErrorMaxRetriesTimes_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+
+ for (int i = 0; i < MAX_RETRIES; ++i) {
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+ }
+
+ Chunk initialChunk = initialReadChunk(1, ProtocolVersion.LEGACY);
+ assertThat(lastChunks())
+ .containsExactlyElementsIn(Collections.nCopies(1 + MAX_RETRIES, initialChunk));
+
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+
+ assertThat(lastChunks()).isEmpty();
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void legacy_read_singleChunk_ignoresUnknownIdOrWriteChunks() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(1);
+ assertThat(future.isDone()).isFalse();
+
+ receiveReadChunks(legacyFinalChunk(2, Status.OK),
+ newLegacyChunk(Chunk.Type.DATA, 0)
+ .setOffset(0)
+ .setData(TEST_DATA_100B)
+ .setRemainingBytes(0),
+ newLegacyChunk(Chunk.Type.DATA, 3)
+ .setOffset(0)
+ .setData(TEST_DATA_100B)
+ .setRemainingBytes(0));
+ receiveWriteChunks(legacyFinalChunk(1, Status.OK),
+ newLegacyChunk(Chunk.Type.DATA, 1)
+ .setOffset(0)
+ .setData(TEST_DATA_100B)
+ .setRemainingBytes(0),
+ newLegacyChunk(Chunk.Type.DATA, 2)
+ .setOffset(0)
+ .setData(TEST_DATA_100B)
+ .setRemainingBytes(0));
+
+ assertThat(future.isDone()).isFalse();
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 1)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0));
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
+ }
+
+ @Test
+ public void legacy_read_empty() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(2);
+ lastChunks(); // Discard initial chunk (tested elsewhere)
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 2).setRemainingBytes(0));
+
+ assertThat(lastChunks()).containsExactly(legacyFinalChunk(2, Status.OK));
+
+ assertThat(future.get()).isEqualTo(new byte[] {});
+ }
+
+ @Test
+ public void legacy_read_sendsTransferParametersFirst() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ TransferParameters params = TransferParameters.create(3, 2, 1);
+ ListenableFuture<byte[]> future = transferClient.read(99, params);
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(99, ProtocolVersion.LEGACY, params));
+ assertThat(future.cancel(true)).isTrue();
+ }
+
+ @Test
+ public void legacy_read_severalChunks() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(123, ProtocolVersion.LEGACY));
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(0)
+ .setData(range(0, 20))
+ .setRemainingBytes(70),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(20).setData(range(20, 40)));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(40)
+ .setPendingBytes(50)
+ .setMaxChunkSizeBytes(30)
+ .setWindowEndOffset(90)
+ .build());
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(40).setData(range(40, 70)));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(70)
+ .setPendingBytes(50)
+ .setMaxChunkSizeBytes(30)
+ .setWindowEndOffset(120)
+ .build());
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(70)
+ .setData(range(70, 100))
+ .setRemainingBytes(0));
+
+ assertThat(lastChunks()).containsExactly(legacyFinalChunk(123, Status.OK));
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_100B.toByteArray());
+ }
+
+ @Test
+ public void legacy_read_progressCallbackIsCalled() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future =
+ transferClient.read(123, TRANSFER_PARAMETERS, progressCallback);
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)),
+ newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(50)
+ .setData(range(50, 60))
+ .setRemainingBytes(5),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(60).setData(range(60, 70)),
+ newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(70)
+ .setData(range(70, 80))
+ .setRemainingBytes(20),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(90).setData(range(90, 100)),
+ newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(80)
+ .setData(range(80, 100))
+ .setRemainingBytes(0));
+
+ verify(progressCallback, times(6)).accept(progress.capture());
+ assertThat(progress.getAllValues())
+ .containsExactly(TransferProgress.create(30, 30, TransferProgress.UNKNOWN_TRANSFER_SIZE),
+ TransferProgress.create(50, 50, TransferProgress.UNKNOWN_TRANSFER_SIZE),
+ TransferProgress.create(60, 60, 65),
+ TransferProgress.create(70, 70, TransferProgress.UNKNOWN_TRANSFER_SIZE),
+ TransferProgress.create(80, 80, 100),
+ TransferProgress.create(100, 100, 100));
+ assertThat(future.isDone()).isTrue();
+ }
+
+ @Test
+ public void legacy_read_rewindWhenPacketsSkipped() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(123, ProtocolVersion.LEGACY));
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(30, 50)));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setPendingBytes(50)
+ .setWindowEndOffset(50)
+ .setMaxChunkSizeBytes(30)
+ .setOffset(0)
+ .build());
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(30)
+ .setPendingBytes(50)
+ .setWindowEndOffset(80)
+ .setMaxChunkSizeBytes(30)
+ .build());
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(80)
+ .setData(range(80, 100))
+ .setRemainingBytes(0));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(50)
+ .setPendingBytes(50)
+ .setWindowEndOffset(100)
+ .setMaxChunkSizeBytes(30)
+ .build());
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 80)),
+ newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(80)
+ .setData(range(80, 100))
+ .setRemainingBytes(0));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(80)
+ .setPendingBytes(50)
+ .setWindowEndOffset(130)
+ .setMaxChunkSizeBytes(30)
+ .build(),
+ legacyFinalChunk(123, Status.OK));
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_100B.toByteArray());
+ }
+
+ @Test
+ public void legacy_read_multipleWithSameId_sequentially_successful() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ for (int i = 0; i < 3; ++i) {
+ ListenableFuture<byte[]> future = transferClient.read(1);
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 1)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0));
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
+ }
+ }
+
+ @Test
+ public void legacy_read_multipleWithSameId_atSameTime_failsWithAlreadyExistsError() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> first = transferClient.read(123);
+ ListenableFuture<byte[]> second = transferClient.read(123);
+
+ assertThat(first.isDone()).isFalse();
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, second::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ALREADY_EXISTS);
+ }
+
+ @Test
+ public void legacy_read_sendErrorOnFirstPacket_fails() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ChannelOutputException exception = new ChannelOutputException("blah");
+ rpcClient.setChannelOutputException(exception);
+ ListenableFuture<byte[]> future = transferClient.read(123);
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(thrown).hasCauseThat().hasCauseThat().isSameInstanceAs(exception);
+ }
+
+ @Test
+ public void legacy_read_sendErrorOnLaterPacket_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 20)));
+
+ ChannelOutputException exception = new ChannelOutputException("blah");
+ rpcClient.setChannelOutputException(exception);
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(20).setData(range(20, 50)));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(thrown).hasCauseThat().hasCauseThat().isSameInstanceAs(exception);
+ }
+
+ @Test
+ public void legacy_read_cancelFuture_abortsTransfer() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+
+ assertThat(future.cancel(true)).isTrue();
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)));
+ assertThat(lastChunks()).contains(legacyFinalChunk(123, Status.CANCELLED));
+ }
+
+ @Test
+ public void legacy_read_transferProtocolError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(123);
+
+ receiveReadChunks(legacyFinalChunk(123, Status.ALREADY_EXISTS));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ALREADY_EXISTS);
+ }
+
+ @Test
+ public void legacy_read_rpcError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(2);
+
+ receiveReadServerError(Status.NOT_FOUND);
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void legacy_read_timeout() {
+ createTransferClientThatMayTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+
+ // Call future.get() without sending any server-side packets.
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
+
+ // read should have retried sending the transfer parameters 2 times, for a total of 3
+ assertThat(lastChunks())
+ .containsExactly(initialReadChunk(123, ProtocolVersion.LEGACY),
+ initialReadChunk(123, ProtocolVersion.LEGACY),
+ initialReadChunk(123, ProtocolVersion.LEGACY));
+ }
+
+ @Test
+ public void legacy_write_singleChunk() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ assertThat(future.isDone()).isFalse();
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 2)
+ .setOffset(0)
+ .setPendingBytes(1024)
+ .setMaxChunkSizeBytes(128),
+ legacyFinalChunk(2, Status.OK));
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void legacy_write_platformTransferDisabled_aborted() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ assertThat(future.isDone()).isFalse();
+
+ shouldAbortFlag = true;
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 2)
+ .setOffset(0)
+ .setPendingBytes(1024)
+ .setMaxChunkSizeBytes(128),
+ legacyFinalChunk(2, Status.OK));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ABORTED);
+ }
+
+ @Test
+ public void legacy_write_failedPreconditionError_retriesInitialPacket() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()));
+
+ receiveWriteServerError(Status.FAILED_PRECONDITION);
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()));
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 2)
+ .setOffset(0)
+ .setPendingBytes(1024)
+ .setMaxChunkSizeBytes(128),
+ legacyFinalChunk(2, Status.OK));
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void legacy_write_failedPreconditionError_abortsAfterInitialPacket() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_100B.toByteArray());
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 2)
+ .setOffset(0)
+ .setPendingBytes(50)
+ .setMaxChunkSizeBytes(50));
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.LEGACY, TEST_DATA_100B.size()),
+ legacyDataChunk(2, TEST_DATA_100B, 0, 50));
+
+ receiveWriteServerError(Status.FAILED_PRECONDITION);
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void legacy_write_failedPreconditionErrorMaxRetriesTimes_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ for (int i = 0; i < MAX_RETRIES; ++i) {
+ receiveWriteServerError(Status.FAILED_PRECONDITION);
+ }
+
+ Chunk initialChunk = initialWriteChunk(2, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size());
+ assertThat(lastChunks())
+ .containsExactlyElementsIn(Collections.nCopies(1 + MAX_RETRIES, initialChunk));
+
+ receiveWriteServerError(Status.FAILED_PRECONDITION);
+
+ assertThat(lastChunks()).isEmpty();
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void legacy_write_empty() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(2, new byte[] {});
+ assertThat(future.isDone()).isFalse();
+
+ receiveWriteChunks(legacyFinalChunk(2, Status.OK));
+
+ assertThat(lastChunks()).containsExactly(initialWriteChunk(2, ProtocolVersion.LEGACY, 0));
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void legacy_write_severalChunks() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()));
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setPendingBytes(50)
+ .setMaxChunkSizeBytes(30)
+ .setMinDelayMicroseconds(1));
+
+ assertThat(lastChunks())
+ .containsExactly(
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)).build(),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)).build());
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(50)
+ .setPendingBytes(40)
+ .setMaxChunkSizeBytes(25));
+
+ assertThat(lastChunks())
+ .containsExactly(
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 75)).build(),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(75).setData(range(75, 90)).build());
+
+ receiveWriteChunks(
+ newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(90).setPendingBytes(50));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(90)
+ .setData(range(90, 100))
+ .setRemainingBytes(0)
+ .build());
+
+ assertThat(future.isDone()).isFalse();
+
+ receiveWriteChunks(legacyFinalChunk(123, Status.OK));
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void legacy_write_parametersContinue() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()));
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setPendingBytes(50)
+ .setWindowEndOffset(50)
+ .setMaxChunkSizeBytes(30)
+ .setMinDelayMicroseconds(1));
+
+ assertThat(lastChunks())
+ .containsExactly(
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)).build(),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)).build());
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(30)
+ .setPendingBytes(50)
+ .setWindowEndOffset(80));
+
+ // Transfer doesn't roll back to offset 30 but instead continues sending up to 80.
+ assertThat(lastChunks())
+ .containsExactly(
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 80)).build());
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(80)
+ .setPendingBytes(50)
+ .setWindowEndOffset(130));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(80)
+ .setData(range(80, 100))
+ .setRemainingBytes(0)
+ .build());
+
+ assertThat(future.isDone()).isFalse();
+
+ receiveWriteChunks(legacyFinalChunk(123, Status.OK));
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void legacy_write_continuePacketWithWindowEndBeforeOffsetIsIgnored() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()));
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setPendingBytes(90)
+ .setWindowEndOffset(90)
+ .setMaxChunkSizeBytes(90)
+ .setMinDelayMicroseconds(1));
+
+ assertThat(lastChunks())
+ .containsExactly(
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 90)).build());
+
+ receiveWriteChunks(
+ // This stale packet with a window end before the offset should be ignored.
+ newLegacyChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(25)
+ .setPendingBytes(25)
+ .setWindowEndOffset(50),
+ // Start from an arbitrary offset before the current, but extend the window to the end.
+ newLegacyChunk(Chunk.Type.PARAMETERS_CONTINUE, 123).setOffset(80).setWindowEndOffset(100));
+
+ assertThat(lastChunks())
+ .containsExactly(newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(90)
+ .setData(range(90, 100))
+ .setRemainingBytes(0)
+ .build());
+
+ receiveWriteChunks(legacyFinalChunk(123, Status.OK));
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void legacy_write_progressCallbackIsCalled() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future =
+ transferClient.write(123, TEST_DATA_100B.toByteArray(), progressCallback);
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setPendingBytes(90)
+ .setMaxChunkSizeBytes(30),
+ newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(50).setPendingBytes(50),
+ legacyFinalChunk(123, Status.OK));
+
+ verify(progressCallback, times(6)).accept(progress.capture());
+ assertThat(progress.getAllValues())
+ .containsExactly(TransferProgress.create(30, 0, 100),
+ TransferProgress.create(60, 0, 100),
+ TransferProgress.create(90, 0, 100),
+ TransferProgress.create(80, 50, 100),
+ TransferProgress.create(100, 50, 100),
+ TransferProgress.create(100, 100, 100));
+ assertThat(future.isDone()).isTrue();
+ }
+
+ @Test
+ public void legacy_write_asksForFinalOffset_sendsFinalPacket() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(100)
+ .setPendingBytes(40)
+ .setMaxChunkSizeBytes(25));
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()),
+ newLegacyChunk(Chunk.Type.DATA, 123).setOffset(100).setRemainingBytes(0).build());
+ assertThat(future.isDone()).isFalse();
+ }
+
+ @Test
+ public void legacy_write_multipleWithSameId_sequentially_successful() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ for (int i = 0; i < 3; ++i) {
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ receiveWriteChunks(
+ newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(0).setPendingBytes(50),
+ legacyFinalChunk(123, Status.OK));
+
+ future.get();
+ }
+ }
+
+ @Test
+ public void legacy_write_multipleWithSameId_atSameTime_failsWithAlreadyExistsError() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> first = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+ ListenableFuture<Void> second = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ assertThat(first.isDone()).isFalse();
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, second::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ALREADY_EXISTS);
+ }
+
+ @Test
+ public void legacy_write_sendErrorOnFirstPacket_failsImmediately() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ChannelOutputException exception = new ChannelOutputException("blah");
+ rpcClient.setChannelOutputException(exception);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(thrown).hasCauseThat().hasCauseThat().isSameInstanceAs(exception);
+ }
+
+ @Test
+ public void legacy_write_serviceRequestsNoData_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(0));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
+ }
+
+ @Test
+ public void legacy_write_invalidOffset_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(101)
+ .setPendingBytes(40)
+ .setMaxChunkSizeBytes(25));
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()),
+ legacyFinalChunk(123, Status.OUT_OF_RANGE));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.OUT_OF_RANGE);
+ }
+
+ @Test
+ public void legacy_write_sendErrorOnLaterPacket_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ ChannelOutputException exception = new ChannelOutputException("blah");
+ rpcClient.setChannelOutputException(exception);
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setPendingBytes(50)
+ .setMaxChunkSizeBytes(30));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(thrown).hasCauseThat().hasCauseThat().isSameInstanceAs(exception);
+ }
+
+ @Test
+ public void legacy_write_cancelFuture_abortsTransfer() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ assertThat(future.cancel(true)).isTrue();
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setPendingBytes(50)
+ .setMaxChunkSizeBytes(50));
+ assertThat(lastChunks()).contains(legacyFinalChunk(123, Status.CANCELLED));
+ }
+
+ @Test
+ public void legacy_write_transferProtocolError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ receiveWriteChunks(legacyFinalChunk(123, Status.NOT_FOUND));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.NOT_FOUND);
+ }
+
+ @Test
+ public void legacy_write_rpcError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ receiveWriteServerError(Status.NOT_FOUND);
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void legacy_write_timeoutAfterInitialChunk() {
+ createTransferClientThatMayTimeOut(ProtocolVersion.LEGACY);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ // Call future.get() without sending any server-side packets.
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
+
+ // Client should have resent the last chunk (the initial chunk in this case) for each timeout.
+ assertThat(lastChunks())
+ .containsExactly(
+ initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()), // initial
+ initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()), // retry 1
+ initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size())); // retry 2
+ }
+
+ @Test
+ public void legacy_write_timeoutAfterSingleChunk() {
+ createTransferClientThatMayTimeOut(ProtocolVersion.LEGACY);
+
+ // Wait for two outgoing packets (Write RPC request and first chunk), then send the parameters.
+ enqueueWriteChunks(2,
+ newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setPendingBytes(90)
+ .setMaxChunkSizeBytes(30));
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
+
+ Chunk data = newLegacyChunk(Chunk.Type.DATA, 123)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0)
+ .build();
+ assertThat(lastChunks())
+ .containsExactly(
+ initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_SHORT.size()), // initial
+ data, // data chunk
+ data, // retry 1
+ data); // retry 2
+ }
+
+ @Test
+ public void legacy_write_multipleTimeoutsAndRecoveries() throws Exception {
+ createTransferClientThatMayTimeOut(ProtocolVersion.LEGACY);
+
+ // Wait for two outgoing packets (Write RPC request and first chunk), then send the parameters.
+ enqueueWriteChunks(2,
+ newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(40)
+ .setMaxChunkSizeBytes(20));
+
+ // After the second retry, send more transfer parameters
+ enqueueWriteChunks(4,
+ newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(40)
+ .setWindowEndOffset(120)
+ .setMaxChunkSizeBytes(40));
+
+ // After the first retry, send more transfer parameters
+ enqueueWriteChunks(3,
+ newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(80)
+ .setWindowEndOffset(160)
+ .setMaxChunkSizeBytes(10));
+
+ // After the second retry, confirm completed
+ enqueueWriteChunks(
+ 4, newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setStatus(Status.OK.code()));
+
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+
+ assertThat(lastChunks())
+ .containsExactly(
+ // initial
+ initialWriteChunk(123, ProtocolVersion.LEGACY, TEST_DATA_100B.size()),
+ // after 2, receive parameters: 40 from 0 by 20
+ legacyDataChunk(123, TEST_DATA_100B, 0, 20), // data 0-20
+ legacyDataChunk(123, TEST_DATA_100B, 20, 40), // data 20-40
+ legacyDataChunk(123, TEST_DATA_100B, 20, 40), // retry 1
+ legacyDataChunk(123, TEST_DATA_100B, 20, 40), // retry 2
+ // after 4, receive parameters: 80 from 40 by 40
+ legacyDataChunk(123, TEST_DATA_100B, 40, 80), // data 40-80
+ legacyDataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100
+ legacyDataChunk(123, TEST_DATA_100B, 80, 100), // retry 1
+ // after 3, receive parameters: 80 from 80 by 10
+ legacyDataChunk(123, TEST_DATA_100B, 80, 90), // data 80-90
+ legacyDataChunk(123, TEST_DATA_100B, 90, 100), // data 90-100
+ legacyDataChunk(123, TEST_DATA_100B, 90, 100), // retry 1
+ legacyDataChunk(123, TEST_DATA_100B, 90, 100)); // retry 2
+ // after 4, receive final OK
+ }
+
+ @Test
+ public void read_singleChunk_successful() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(3, TRANSFER_PARAMETERS);
+ assertThat(future.isDone()).isFalse();
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(3, ProtocolVersion.VERSION_TWO));
+
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, 321)
+ .setResourceId(3)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ assertThat(lastChunks()).containsExactly(readStartAckConfirmation(321, TRANSFER_PARAMETERS));
+
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, 321).setOffset(0).setData(TEST_DATA_SHORT).setRemainingBytes(0));
+
+ assertThat(lastChunks())
+ .containsExactly(Chunk.newBuilder()
+ .setType(Chunk.Type.COMPLETION)
+ .setSessionId(321)
+ .setStatus(Status.OK.ordinal())
+ .build());
+
+ assertThat(future.isDone()).isFalse();
+
+ receiveReadChunks(newChunk(Chunk.Type.COMPLETION_ACK, 321));
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
+ }
+
+ @Test
+ public void read_requestV2ReceiveLegacy() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+ assertThat(future.isDone()).isFalse();
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO));
+
+ receiveReadChunks(newLegacyChunk(Chunk.Type.DATA, 1)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0));
+
+ // No handshake packets since the server responded as legacy.
+ assertThat(lastChunks()).containsExactly(legacyFinalChunk(1, Status.OK));
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
+ }
+
+ @Test
+ public void read_failedPreconditionError_retriesInitialPacket() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO));
+ for (int i = 0; i < MAX_RETRIES; ++i) {
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+
+ assertThat(lastChunks()).containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO));
+ }
+
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, 54321)
+ .setResourceId(1)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ assertThat(lastChunks()).containsExactly(readStartAckConfirmation(54321, TRANSFER_PARAMETERS));
+ }
+
+ @Test
+ public void read_failedPreconditionError_abortsAfterInitial() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ TransferParameters params = TransferParameters.create(50, 50, 0);
+ ListenableFuture<byte[]> future = transferClient.read(1, params);
+
+ assertThat(lastChunks())
+ .containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO, params));
+
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, 555)
+ .setResourceId(1)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void read_failedPreconditionError_abortsAfterHandshake() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ TransferParameters params = TransferParameters.create(50, 50, 0);
+ ListenableFuture<byte[]> future = transferClient.read(1, params);
+
+ assertThat(lastChunks())
+ .containsExactly(initialReadChunk(1, ProtocolVersion.VERSION_TWO, params));
+
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, 555)
+ .setResourceId(1)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ assertThat(lastChunks()).containsExactly(readStartAckConfirmation(555, params));
+
+ receiveReadChunks(dataChunk(555, TEST_DATA_100B, 0, 50));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 555)
+ .setOffset(50)
+ .setWindowEndOffset(100)
+ .setMaxChunkSizeBytes(50)
+ .build());
+
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void read_failedPreconditionErrorMaxRetriesTimes_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+
+ for (int i = 0; i < MAX_RETRIES; ++i) {
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+ }
+
+ Chunk initialChunk = initialReadChunk(1, ProtocolVersion.VERSION_TWO);
+ assertThat(lastChunks())
+ .containsExactlyElementsIn(Collections.nCopies(1 + MAX_RETRIES, initialChunk));
+
+ receiveReadServerError(Status.FAILED_PRECONDITION);
+
+ assertThat(lastChunks()).isEmpty();
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void read_singleChunk_ignoresUnknownIdOrWriteChunks() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(1);
+ assertThat(future.isDone()).isFalse();
+
+ performReadStartHandshake(1, 99);
+
+ receiveReadChunks(finalChunk(2, Status.OK),
+ newChunk(Chunk.Type.DATA, 1).setOffset(0).setData(TEST_DATA_100B).setRemainingBytes(0),
+ newChunk(Chunk.Type.DATA, 3).setOffset(0).setData(TEST_DATA_100B).setRemainingBytes(0));
+ receiveWriteChunks(finalChunk(99, Status.INVALID_ARGUMENT),
+ newChunk(Chunk.Type.DATA, 99).setOffset(0).setData(TEST_DATA_100B).setRemainingBytes(0),
+ newChunk(Chunk.Type.DATA, 2).setOffset(0).setData(TEST_DATA_100B).setRemainingBytes(0));
+
+ assertThat(future.isDone()).isFalse();
+
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, 99).setOffset(0).setData(TEST_DATA_SHORT).setRemainingBytes(0));
+
+ performReadCompletionHandshake(99, Status.OK);
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
+ }
+
+ @Test
+ public void read_empty() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(2);
+ performReadStartHandshake(2, 5678);
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 5678).setRemainingBytes(0));
+
+ performReadCompletionHandshake(5678, Status.OK);
+
+ assertThat(future.get()).isEqualTo(new byte[] {});
+ }
+
+ @Test
+ public void read_sendsTransferParametersFirst() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ TransferParameters params = TransferParameters.create(3, 2, 1);
+ ListenableFuture<byte[]> future = transferClient.read(99, params);
+
+ assertThat(lastChunks())
+ .containsExactly(initialReadChunk(99, ProtocolVersion.VERSION_TWO, params));
+ assertThat(future.cancel(true)).isTrue();
+ }
+
+ @Test
+ public void read_severalChunks() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(7, TRANSFER_PARAMETERS);
+
+ performReadStartHandshake(7, 123, TRANSFER_PARAMETERS);
+
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 20)).setRemainingBytes(70),
+ newChunk(Chunk.Type.DATA, 123).setOffset(20).setData(range(20, 40)));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(40)
+ .setMaxChunkSizeBytes(30)
+ .setWindowEndOffset(90)
+ .build());
+
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(40).setData(range(40, 70)));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(70)
+ .setMaxChunkSizeBytes(30)
+ .setWindowEndOffset(120)
+ .build());
+
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, 123).setOffset(70).setData(range(70, 100)).setRemainingBytes(0));
+
+ performReadCompletionHandshake(123, Status.OK);
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_100B.toByteArray());
+ }
+
+ @Test
+ public void read_onlySendsOneUpdateAfterDrops() throws Exception {
+ createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
+ TransferParameters params = TransferParameters.create(50, 10, 0);
+
+ // Handshake
+ enqueueReadChunks(2, // Wait for read RPC open & START packet
+ newChunk(Chunk.Type.START_ACK, 99)
+ .setResourceId(7)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+ enqueueReadChunks(1, // Ignore the first START_ACK_CONFIRMATION
+ newChunk(Chunk.Type.START_ACK, 99)
+ .setResourceId(7)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ // Window 1: server waits for START_ACK_CONFIRMATION, drops 2nd packet
+ enqueueReadChunks(1,
+ newChunk(Chunk.Type.DATA, 99).setOffset(0).setData(range(0, 10)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(20).setData(range(20, 30)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(30).setData(range(30, 40)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(40).setData(range(40, 50)));
+
+ // Window 2: server waits for retransmit, drops 1st packet
+ enqueueReadChunks(1,
+ newChunk(Chunk.Type.DATA, 99).setOffset(20).setData(range(20, 30)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(30).setData(range(30, 40)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(40).setData(range(40, 50)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(50).setData(range(50, 60)));
+
+ // Window 3: server waits for retransmit, drops last packet
+ enqueueReadChunks(1,
+ newChunk(Chunk.Type.DATA, 99).setOffset(10).setData(range(10, 20)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(20).setData(range(20, 30)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(30).setData(range(30, 40)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(40).setData(range(40, 50)));
+
+ // Window 4: server waits for continue and retransmit, normal window.
+ enqueueReadChunks(2,
+ newChunk(Chunk.Type.DATA, 99).setOffset(50).setData(range(50, 60)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(60).setData(range(60, 70)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(70).setData(range(70, 80)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(80).setData(range(80, 90)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(90).setData(range(90, 100)));
+ enqueueReadChunks(2, // Ignore continue and retransmit chunks, retry last packet in window
+ newChunk(Chunk.Type.DATA, 99).setOffset(90).setData(range(90, 100)),
+ newChunk(Chunk.Type.DATA, 99).setOffset(90).setData(range(90, 100)));
+
+ // Window 5: Final packet
+ enqueueReadChunks(2, // Receive two retries, then send final packet
+ newChunk(Chunk.Type.DATA, 99).setOffset(100).setData(range(100, 110)).setRemainingBytes(0));
+ enqueueReadChunks(1, // Ignore first COMPLETION packet
+ newChunk(Chunk.Type.DATA, 99).setOffset(100).setData(range(100, 110)).setRemainingBytes(0));
+ enqueueReadChunks(1, newChunk(Chunk.Type.COMPLETION_ACK, 99));
+
+ ListenableFuture<byte[]> future = transferClient.read(7, params);
+ // assertThat(future.get()).isEqualTo(range(0, 110).toByteArray());
+ while (!future.isDone()) {
+ }
+
+ assertThat(lastChunks())
+ .containsExactly(
+ // Handshake
+ initialReadChunk(7, ProtocolVersion.VERSION_TWO, params),
+ readStartAckConfirmation(99, params),
+ readStartAckConfirmation(99, params),
+ // Window 1: send one transfer parameters update after the drop
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99)
+ .setOffset(10)
+ .setWindowEndOffset(60)
+ .setMaxChunkSizeBytes(10)
+ .build(),
+ // Window 2: send one transfer parameters update after the drop
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99)
+ .setOffset(10)
+ .setWindowEndOffset(60)
+ .setMaxChunkSizeBytes(10)
+ .build(),
+ // Window 3: send one transfer parameters update after the drop, then continue packet
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, 99) // Not seen by server
+ .setOffset(40)
+ .setWindowEndOffset(90)
+ .setMaxChunkSizeBytes(10)
+ .build(),
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99) // Sent after timeout
+ .setOffset(50)
+ .setWindowEndOffset(100)
+ .setMaxChunkSizeBytes(10)
+ .build(),
+ // Window 4: send one transfer parameters update after the drop, then continue packet
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, 99) // Ignored by server
+ .setOffset(80)
+ .setWindowEndOffset(130)
+ .setMaxChunkSizeBytes(10)
+ .build(),
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99) // Sent after last packet
+ .setOffset(100)
+ .setWindowEndOffset(150)
+ .setMaxChunkSizeBytes(10)
+ .build(),
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99) // Sent due to repeated packet
+ .setOffset(100)
+ .setWindowEndOffset(150)
+ .setMaxChunkSizeBytes(10)
+ .build(),
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 99) // Sent due to repeated packet
+ .setOffset(100)
+ .setWindowEndOffset(150)
+ .setMaxChunkSizeBytes(10)
+ .build(),
+ // Window 5: final packet and closing handshake
+ newChunk(Chunk.Type.COMPLETION, 99).setStatus(Status.OK.ordinal()).build(),
+ newChunk(Chunk.Type.COMPLETION, 99).setStatus(Status.OK.ordinal()).build());
+ }
+
+ @Test
+ public void read_progressCallbackIsCalled() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future =
+ transferClient.read(123, TRANSFER_PARAMETERS, progressCallback);
+
+ performReadStartHandshake(123, 123, TRANSFER_PARAMETERS);
+
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)),
+ newChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)),
+ newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 60)).setRemainingBytes(5),
+ newChunk(Chunk.Type.DATA, 123).setOffset(60).setData(range(60, 70)),
+ newChunk(Chunk.Type.DATA, 123).setOffset(70).setData(range(70, 80)).setRemainingBytes(20),
+ newChunk(Chunk.Type.DATA, 123).setOffset(90).setData(range(90, 100)),
+ newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)));
+ lastChunks(); // Discard chunks; no need to inspect for this test
+
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, 123).setOffset(80).setData(range(80, 100)).setRemainingBytes(0));
+ performReadCompletionHandshake(123, Status.OK);
+
+ verify(progressCallback, times(6)).accept(progress.capture());
+ assertThat(progress.getAllValues())
+ .containsExactly(TransferProgress.create(30, 30, TransferProgress.UNKNOWN_TRANSFER_SIZE),
+ TransferProgress.create(50, 50, TransferProgress.UNKNOWN_TRANSFER_SIZE),
+ TransferProgress.create(60, 60, 65),
+ TransferProgress.create(70, 70, TransferProgress.UNKNOWN_TRANSFER_SIZE),
+ TransferProgress.create(80, 80, 100),
+ TransferProgress.create(100, 100, 100));
+
+ assertThat(future.isDone()).isTrue();
+ }
+
+ @Test
+ public void read_rewindWhenPacketsSkipped() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+
+ performReadStartHandshake(123, 123, TRANSFER_PARAMETERS);
+
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(30, 50)));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setWindowEndOffset(50)
+ .setMaxChunkSizeBytes(30)
+ .setOffset(0)
+ .build());
+
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)),
+ newChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(30)
+ .setWindowEndOffset(80)
+ .setMaxChunkSizeBytes(30)
+ .build());
+
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, 123).setOffset(80).setData(range(80, 100)).setRemainingBytes(0));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(50)
+ .setWindowEndOffset(100)
+ .setMaxChunkSizeBytes(30)
+ .build());
+
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 80)));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123)
+ .setOffset(80)
+ .setWindowEndOffset(130)
+ .setMaxChunkSizeBytes(30)
+ .build());
+
+ receiveReadChunks(
+ newChunk(Chunk.Type.DATA, 123).setOffset(80).setData(range(80, 100)).setRemainingBytes(0));
+
+ performReadCompletionHandshake(123, Status.OK);
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_100B.toByteArray());
+ }
+
+ @Test
+ public void read_multipleWithSameId_sequentially_successful() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ for (int i = 0; i < 3; ++i) {
+ ListenableFuture<byte[]> future = transferClient.read(1);
+
+ performReadStartHandshake(1, 100 + i);
+
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 100 + i)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0));
+
+ performReadCompletionHandshake(100 + i, Status.OK);
+
+ assertThat(future.get()).isEqualTo(TEST_DATA_SHORT.toByteArray());
+ }
+ }
+
+ @Test
+ public void read_multipleWithSameId_atSameTime_failsWithAlreadyExistsError() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> first = transferClient.read(123);
+ ListenableFuture<byte[]> second = transferClient.read(123);
+
+ assertThat(first.isDone()).isFalse();
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, second::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ALREADY_EXISTS);
+ }
+
+ @Test
+ public void read_sendErrorOnFirstPacket_fails() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ChannelOutputException exception = new ChannelOutputException("blah");
+ rpcClient.setChannelOutputException(exception);
+ ListenableFuture<byte[]> future = transferClient.read(123);
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(thrown).hasCauseThat().hasCauseThat().isSameInstanceAs(exception);
+ }
+
+ @Test
+ public void read_sendErrorOnLaterPacket_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(1024, TRANSFER_PARAMETERS);
+
+ performReadStartHandshake(1024, 123, TRANSFER_PARAMETERS);
+
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 20)));
+
+ ChannelOutputException exception = new ChannelOutputException("blah");
+ rpcClient.setChannelOutputException(exception);
+
+ receiveReadChunks(newChunk(Chunk.Type.DATA, 123).setOffset(20).setData(range(20, 50)));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(thrown).hasCauseThat().hasCauseThat().isSameInstanceAs(exception);
+ }
+
+ @Test
+ public void read_cancelFuture_abortsTransfer() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(1, TRANSFER_PARAMETERS);
+
+ performReadStartHandshake(1, 123, TRANSFER_PARAMETERS);
+
+ assertThat(future.cancel(true)).isTrue();
+
+ assertThat(lastChunks()).contains(finalChunk(123, Status.CANCELLED));
+ }
+
+ @Test
+ public void read_immediateTransferProtocolError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(123);
+
+ // Resource ID will be set since session ID hasn't been assigned yet.
+ receiveReadChunks(newChunk(Chunk.Type.COMPLETION, VersionedChunk.UNASSIGNED_SESSION_ID)
+ .setResourceId(123)
+ .setStatus(Status.ALREADY_EXISTS.ordinal()));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ALREADY_EXISTS);
+ }
+
+ @Test
+ public void read_laterTransferProtocolError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(123);
+
+ performReadStartHandshake(123, 514);
+
+ receiveReadChunks(finalChunk(514, Status.ALREADY_EXISTS));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ALREADY_EXISTS);
+ }
+
+ @Test
+ public void read_rpcError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(2);
+
+ receiveReadServerError(Status.NOT_FOUND);
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void read_serverRespondsWithUnknownVersion_invalidArgument() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(2, TRANSFER_PARAMETERS);
+
+ assertThat(lastChunks())
+ .containsExactly(initialReadChunk(2, ProtocolVersion.VERSION_TWO, TRANSFER_PARAMETERS));
+
+ receiveReadChunks(
+ newChunk(Chunk.Type.START_ACK, 99).setResourceId(2).setProtocolVersion(600613));
+
+ assertThat(lastChunks()).containsExactly(finalChunk(99, Status.INVALID_ARGUMENT));
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
+ }
+
+ @Test
+ public void read_timeout() {
+ createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<byte[]> future = transferClient.read(123, TRANSFER_PARAMETERS);
+
+ // Call future.get() without sending any server-side packets.
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
+
+ // read should have retried sending the transfer parameters 2 times, for a total of 3
+ assertThat(lastChunks())
+ .containsExactly(initialReadChunk(123, ProtocolVersion.VERSION_TWO),
+ initialReadChunk(123, ProtocolVersion.VERSION_TWO),
+ initialReadChunk(123, ProtocolVersion.VERSION_TWO));
+ }
+
+ @Test
+ public void write_singleChunk() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ // Do the start handshake (equivalent to performWriteStartHandshake()).
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()));
+
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 123)
+ .setResourceId(2)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
+ .setRemainingBytes(TEST_DATA_SHORT.size())
+ .build());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(1024)
+ .setMaxChunkSizeBytes(128));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.DATA, 123)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0)
+ .build());
+
+ receiveWriteChunks(finalChunk(123, Status.OK));
+
+ assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void write_requestV2ReceiveLegacy() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()));
+
+ receiveWriteChunks(newLegacyChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 2)
+ .setOffset(0)
+ .setWindowEndOffset(1024)
+ .setMaxChunkSizeBytes(128),
+ legacyFinalChunk(2, Status.OK));
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void write_platformTransferDisabled_aborted() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+ assertThat(future.isDone()).isFalse();
+
+ shouldAbortFlag = true;
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 3).setResourceId(2));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ABORTED);
+ }
+
+ @Test
+ public void write_failedPreconditionError_retriesInitialPacket() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()));
+ for (int i = 0; i < MAX_RETRIES; ++i) {
+ receiveWriteServerError(Status.FAILED_PRECONDITION);
+
+ assertThat(lastChunks())
+ .containsExactly(
+ initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()));
+ }
+
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 54321)
+ .setResourceId(2)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, 54321)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
+ .setRemainingBytes(TEST_DATA_SHORT.size())
+ .build());
+ }
+
+ @Test
+ public void write_failedPreconditionError_abortsAfterInitialPacket() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_100B.toByteArray());
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()));
+
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 4)
+ .setResourceId(2)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ receiveWriteServerError(Status.FAILED_PRECONDITION);
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void write_failedPreconditionErrorMaxRetriesTimes_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ for (int i = 0; i < MAX_RETRIES; ++i) {
+ receiveWriteServerError(Status.FAILED_PRECONDITION);
+ }
+
+ Chunk initialChunk = initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size());
+ assertThat(lastChunks())
+ .containsExactlyElementsIn(Collections.nCopies(1 + MAX_RETRIES, initialChunk));
+
+ receiveWriteServerError(Status.FAILED_PRECONDITION);
+
+ assertThat(lastChunks()).isEmpty();
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void write_empty() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, new byte[] {});
+
+ performWriteStartHandshake(2, 123, 0);
+
+ receiveWriteChunks(finalChunk(123, Status.OK));
+
+ assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void write_severalChunks() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(500, TEST_DATA_100B.toByteArray());
+
+ performWriteStartHandshake(500, 123, TEST_DATA_100B.size());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(50)
+ .setMaxChunkSizeBytes(30)
+ .setMinDelayMicroseconds(1));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)).build(),
+ newChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)).build());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(50)
+ .setWindowEndOffset(90)
+ .setMaxChunkSizeBytes(25));
+
+ assertThat(lastChunks())
+ .containsExactly(
+ newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 75)).build(),
+ newChunk(Chunk.Type.DATA, 123).setOffset(75).setData(range(75, 90)).build());
+
+ receiveWriteChunks(
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(90).setWindowEndOffset(140));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.DATA, 123)
+ .setOffset(90)
+ .setData(range(90, 100))
+ .setRemainingBytes(0)
+ .build());
+
+ assertThat(future.isDone()).isFalse();
+
+ receiveWriteChunks(finalChunk(123, Status.OK));
+
+ assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void write_parametersContinue() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(321, TEST_DATA_100B.toByteArray());
+
+ performWriteStartHandshake(321, 123, TEST_DATA_100B.size());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(50)
+ .setMaxChunkSizeBytes(30)
+ .setMinDelayMicroseconds(1));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.DATA, 123).setOffset(0).setData(range(0, 30)).build(),
+ newChunk(Chunk.Type.DATA, 123).setOffset(30).setData(range(30, 50)).build());
+
+ receiveWriteChunks(
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123).setOffset(30).setWindowEndOffset(80));
+
+ // Transfer doesn't roll back to offset 30 but instead continues sending up to 80.
+ assertThat(lastChunks())
+ .containsExactly(
+ newChunk(Chunk.Type.DATA, 123).setOffset(50).setData(range(50, 80)).build());
+
+ receiveWriteChunks(
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123).setOffset(80).setWindowEndOffset(130));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.DATA, 123)
+ .setOffset(80)
+ .setData(range(80, 100))
+ .setRemainingBytes(0)
+ .build());
+
+ assertThat(future.isDone()).isFalse();
+
+ receiveWriteChunks(finalChunk(123, Status.OK));
+
+ assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void write_continuePacketWithWindowEndBeforeOffsetIsIgnored() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ performWriteStartHandshake(123, 555, TEST_DATA_100B.size());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 555)
+ .setOffset(0)
+ .setWindowEndOffset(90)
+ .setMaxChunkSizeBytes(90)
+ .setMinDelayMicroseconds(1));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.DATA, 555).setOffset(0).setData(range(0, 90)).build());
+
+ receiveWriteChunks(
+ // This stale packet with a window end before the offset should be ignored.
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, 555).setOffset(25).setWindowEndOffset(50),
+ // Start from an arbitrary offset before the current, but extend the window to the end.
+ newChunk(Chunk.Type.PARAMETERS_CONTINUE, 555).setOffset(80).setWindowEndOffset(100));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.DATA, 555)
+ .setOffset(90)
+ .setData(range(90, 100))
+ .setRemainingBytes(0)
+ .build());
+
+ receiveWriteChunks(finalChunk(555, Status.OK));
+ assertThat(lastChunks()).containsExactly(newChunk(Chunk.Type.COMPLETION_ACK, 555).build());
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+ }
+
+ @Test
+ public void write_progressCallbackIsCalled() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future =
+ transferClient.write(123, TEST_DATA_100B.toByteArray(), progressCallback);
+
+ performWriteStartHandshake(123, 123, TEST_DATA_100B.size());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(90)
+ .setMaxChunkSizeBytes(30),
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(50).setWindowEndOffset(100),
+ finalChunk(123, Status.OK));
+
+ verify(progressCallback, times(6)).accept(progress.capture());
+ assertThat(progress.getAllValues())
+ .containsExactly(TransferProgress.create(30, 0, 100),
+ TransferProgress.create(60, 0, 100),
+ TransferProgress.create(90, 0, 100),
+ TransferProgress.create(80, 50, 100),
+ TransferProgress.create(100, 50, 100),
+ TransferProgress.create(100, 100, 100));
+ assertThat(future.isDone()).isTrue();
+ }
+
+ @Test
+ public void write_asksForFinalOffset_sendsFinalPacket() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_100B.toByteArray());
+
+ performWriteStartHandshake(123, 456, TEST_DATA_100B.size());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 456)
+ .setOffset(100)
+ .setWindowEndOffset(140)
+ .setMaxChunkSizeBytes(25));
+
+ assertThat(lastChunks())
+ .containsExactly(
+ newChunk(Chunk.Type.DATA, 456).setOffset(100).setRemainingBytes(0).build());
+ }
+
+ @Test
+ public void write_multipleWithSameId_sequentially_successful() throws Exception {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ for (int i = 0; i < 3; ++i) {
+ ListenableFuture<Void> future = transferClient.write(6, TEST_DATA_SHORT.toByteArray());
+
+ performWriteStartHandshake(6, 123, TEST_DATA_SHORT.size());
+
+ receiveWriteChunks(
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(0).setWindowEndOffset(50),
+ finalChunk(123, Status.OK));
+
+ assertThat(lastChunks())
+ .containsExactly(
+ newChunk(Chunk.Type.DATA, 123).setData(TEST_DATA_SHORT).setRemainingBytes(0).build(),
+ newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+
+ future.get();
+ }
+ }
+
+ @Test
+ public void write_multipleWithSameId_atSameTime_failsWithAlreadyExistsError() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> first = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+ ListenableFuture<Void> second = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ assertThat(first.isDone()).isFalse();
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, second::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.ALREADY_EXISTS);
+ }
+
+ @Test
+ public void write_sendErrorOnFirstPacket_failsImmediately() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ChannelOutputException exception = new ChannelOutputException("blah");
+ rpcClient.setChannelOutputException(exception);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(thrown).hasCauseThat().hasCauseThat().isSameInstanceAs(exception);
+ }
+
+ @Test
+ public void write_serviceRequestsNoData_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(7, TEST_DATA_SHORT.toByteArray());
+
+ performWriteStartHandshake(7, 123, TEST_DATA_SHORT.size());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123).setOffset(0));
+
+ assertThat(lastChunks()).containsExactly(finalChunk(123, Status.INVALID_ARGUMENT));
+ receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, 123));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
+ }
+
+ @Test
+ public void write_invalidOffset_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(7, TEST_DATA_100B.toByteArray());
+
+ performWriteStartHandshake(7, 123, TEST_DATA_100B.size());
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(101)
+ .setWindowEndOffset(141)
+ .setMaxChunkSizeBytes(25));
+
+ assertThat(lastChunks()).containsExactly(finalChunk(123, Status.OUT_OF_RANGE));
+ receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, 123));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.OUT_OF_RANGE);
+ }
+
+ @Test
+ public void write_sendErrorOnLaterPacket_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(7, TEST_DATA_SHORT.toByteArray());
+
+ performWriteStartHandshake(7, 123, TEST_DATA_SHORT.size());
+
+ ChannelOutputException exception = new ChannelOutputException("blah");
+ rpcClient.setChannelOutputException(exception);
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(50)
+ .setMaxChunkSizeBytes(30));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+ assertThat(thrown).hasCauseThat().hasCauseThat().isSameInstanceAs(exception);
+ }
+
+ @Test
+ public void write_cancelFuture_abortsTransfer() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(7, TEST_DATA_100B.toByteArray());
+
+ performWriteStartHandshake(7, 123, TEST_DATA_100B.size());
+
+ assertThat(future.cancel(true)).isTrue();
+ assertThat(future.isCancelled()).isTrue();
+
+ receiveWriteChunks(newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(50)
+ .setMaxChunkSizeBytes(50));
+
+ assertThat(lastChunks()).contains(finalChunk(123, Status.CANCELLED));
+ receiveWriteChunks(newChunk(Chunk.Type.COMPLETION_ACK, 123));
+ }
+
+ @Test
+ public void write_immediateTransferProtocolError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ receiveWriteChunks(newChunk(Chunk.Type.COMPLETION, VersionedChunk.UNASSIGNED_SESSION_ID)
+ .setResourceId(123)
+ .setStatus(Status.NOT_FOUND.ordinal()));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.NOT_FOUND);
+ }
+
+ @Test
+ public void write_laterTransferProtocolError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ performWriteStartHandshake(123, 123, TEST_DATA_SHORT.size());
+
+ receiveWriteChunks(finalChunk(123, Status.NOT_FOUND));
+
+ ExecutionException thrown = assertThrows(ExecutionException.class, future::get);
+ assertThat(thrown).hasCauseThat().isInstanceOf(TransferError.class);
+
+ assertThat(((TransferError) thrown.getCause()).status()).isEqualTo(Status.NOT_FOUND);
+ }
+
+ @Test
+ public void write_rpcError_aborts() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ receiveWriteServerError(Status.NOT_FOUND);
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INTERNAL);
+ }
+
+ @Test
+ public void write_unknownVersion_invalidArgument() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_SHORT.toByteArray());
+
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, 3).setResourceId(2).setProtocolVersion(9));
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()),
+ finalChunk(3, Status.INVALID_ARGUMENT));
+ }
+
+ @Test
+ public void write_serverRespondsWithUnknownVersion_invalidArgument() {
+ createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(2, TEST_DATA_100B.toByteArray());
+
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(2, ProtocolVersion.VERSION_TWO, 100));
+
+ receiveWriteChunks(
+ newChunk(Chunk.Type.START_ACK, 99).setResourceId(2).setProtocolVersion(600613));
+
+ assertThat(lastChunks()).containsExactly(finalChunk(99, Status.INVALID_ARGUMENT));
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.INVALID_ARGUMENT);
+ }
+
+ @Test
+ public void write_timeoutAfterInitialChunk() {
+ createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
+ ListenableFuture<Void> future = transferClient.write(123, TEST_DATA_SHORT.toByteArray());
+
+ // Call future.get() without sending any server-side packets.
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
+
+ // Client should have resent the last chunk (the initial chunk in this case) for each timeout.
+ assertThat(lastChunks())
+ .containsExactly(
+ initialWriteChunk(123, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()), // initial
+ initialWriteChunk(123, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()), // retry 1
+ initialWriteChunk(123, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size())); // retry 2
+ }
+
+ @Test
+ public void write_timeoutAfterSingleChunk() {
+ createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
+
+ // Wait for two outgoing packets (Write RPC request and first chunk), then do the handshake.
+ enqueueWriteChunks(2,
+ newChunk(Chunk.Type.START_ACK, 123).setResourceId(9),
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(90)
+ .setMaxChunkSizeBytes(30));
+ ListenableFuture<Void> future = transferClient.write(9, TEST_DATA_SHORT.toByteArray());
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
+
+ Chunk data = newChunk(Chunk.Type.DATA, 123)
+ .setOffset(0)
+ .setData(TEST_DATA_SHORT)
+ .setRemainingBytes(0)
+ .build();
+ assertThat(lastChunks())
+ .containsExactly(
+ initialWriteChunk(9, ProtocolVersion.VERSION_TWO, TEST_DATA_SHORT.size()), // initial
+ newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
+ .setRemainingBytes(TEST_DATA_SHORT.size())
+ .build(),
+ data, // data chunk
+ data, // retry 1
+ data); // retry 2
+ }
+
+ @Test
+ public void write_timeoutAndRecoverDuringHandshakes() throws Exception {
+ createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
+ assertThat(MAX_RETRIES).isEqualTo(2); // This test assumes 2 retries
+
+ // Wait for four outgoing packets (Write RPC request and START chunk + retry), then handshake.
+ enqueueWriteChunks(3,
+ newChunk(Chunk.Type.START_ACK, 123)
+ .setResourceId(5)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ // Wait for start ack confirmation + 2 retries, then request three packets.
+ enqueueWriteChunks(3,
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(60)
+ .setMaxChunkSizeBytes(20));
+
+ // After two packets, request the remainder of the packets.
+ enqueueWriteChunks(
+ 2, newChunk(Chunk.Type.PARAMETERS_CONTINUE, 123).setOffset(20).setWindowEndOffset(200));
+
+ // Wait for last 3 data packets, then 2 final packet retries.
+ enqueueWriteChunks(5,
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(80)
+ .setWindowEndOffset(200)
+ .setMaxChunkSizeBytes(20),
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(80)
+ .setWindowEndOffset(200)
+ .setMaxChunkSizeBytes(20));
+
+ // After the retry, confirm completed multiple times; additional packets should be dropped
+ enqueueWriteChunks(1,
+ newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()),
+ newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()),
+ newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()),
+ newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()));
+
+ ListenableFuture<Void> future = transferClient.write(5, TEST_DATA_100B.toByteArray());
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+
+ final Chunk startAckConfirmation =
+ newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
+ .setRemainingBytes(TEST_DATA_100B.size())
+ .build();
+
+ assertThat(lastChunks())
+ .containsExactly(
+ // initial handshake with retries
+ initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
+ initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
+ startAckConfirmation,
+ startAckConfirmation,
+ startAckConfirmation,
+ // send all data
+ dataChunk(123, TEST_DATA_100B, 0, 20), // data 0-20
+ dataChunk(123, TEST_DATA_100B, 20, 40), // data 20-40
+ dataChunk(123, TEST_DATA_100B, 40, 60), // data 40-60
+ dataChunk(123, TEST_DATA_100B, 60, 80), // data 60-80
+ dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ // retry last packet two times
+ dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ // respond to two PARAMETERS_RETRANSMIT packets
+ dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100 (final)
+ // respond to OK packet
+ newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ }
+
+ @Test
+ public void write_multipleTimeoutsAndRecoveries() throws Exception {
+ createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO);
+ assertThat(MAX_RETRIES).isEqualTo(2); // This test assumes 2 retries
+
+ // Wait for two outgoing packets (Write RPC request and START chunk), then do the handshake.
+ enqueueWriteChunks(2,
+ newChunk(Chunk.Type.START_ACK, 123)
+ .setResourceId(5)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ // Request two packets.
+ enqueueWriteChunks(1,
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(40)
+ .setMaxChunkSizeBytes(20));
+
+ // After the second retry, send more transfer parameters
+ enqueueWriteChunks(4,
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(40)
+ .setWindowEndOffset(120)
+ .setMaxChunkSizeBytes(40));
+
+ // After the first retry, send more transfer parameters
+ enqueueWriteChunks(3,
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(80)
+ .setWindowEndOffset(160)
+ .setMaxChunkSizeBytes(10));
+
+ // After the second retry, confirm completed
+ enqueueWriteChunks(4, newChunk(Chunk.Type.COMPLETION, 123).setStatus(Status.OK.code()));
+ enqueueWriteChunks(1, newChunk(Chunk.Type.COMPLETION_ACK, 123));
+
+ ListenableFuture<Void> future = transferClient.write(5, TEST_DATA_100B.toByteArray());
+
+ assertThat(future.get()).isNull(); // Ensure that no exceptions are thrown.
+
+ assertThat(lastChunks())
+ .containsExactly(
+ // initial handshake
+ initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
+ newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
+ .setRemainingBytes(TEST_DATA_100B.size())
+ .build(),
+ // after 2, receive parameters: 40 from 0 by 20
+ dataChunk(123, TEST_DATA_100B, 0, 20), // data 0-20
+ dataChunk(123, TEST_DATA_100B, 20, 40), // data 20-40
+ dataChunk(123, TEST_DATA_100B, 20, 40), // retry 1
+ dataChunk(123, TEST_DATA_100B, 20, 40), // retry 2
+ // after 4, receive parameters: 80 from 40 by 40
+ dataChunk(123, TEST_DATA_100B, 40, 80), // data 40-80
+ dataChunk(123, TEST_DATA_100B, 80, 100), // data 80-100
+ dataChunk(123, TEST_DATA_100B, 80, 100), // retry 1
+ // after 3, receive parameters: 80 from 80 by 10
+ dataChunk(123, TEST_DATA_100B, 80, 90), // data 80-90
+ dataChunk(123, TEST_DATA_100B, 90, 100), // data 90-100
+ dataChunk(123, TEST_DATA_100B, 90, 100), // retry 1
+ dataChunk(123, TEST_DATA_100B, 90, 100), // retry 2
+ // after 4, receive final OK
+ newChunk(Chunk.Type.COMPLETION_ACK, 123).build());
+ }
+ @Test
+ public void write_maxLifetimeRetries() throws Exception {
+ createTransferClientThatMayTimeOut(ProtocolVersion.VERSION_TWO, 5);
+ assertThat(MAX_RETRIES).isEqualTo(2); // This test assumes 2 retries
+
+ // Wait for four outgoing packets (Write RPC request and START chunk + 2 retries)
+ enqueueWriteChunks(4, // 2 retries
+ newChunk(Chunk.Type.START_ACK, 123)
+ .setResourceId(5)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ // Wait for start ack confirmation + 2 retries, then request three packets.
+ enqueueWriteChunks(3, // 2 retries
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123)
+ .setOffset(0)
+ .setWindowEndOffset(60)
+ .setMaxChunkSizeBytes(20));
+
+ // After 3 data packets, wait for two more retries, which should put this over the retry limit.
+ enqueueWriteChunks(5, // 2 retries
+ newChunk(Chunk.Type.PARAMETERS_RETRANSMIT, 123) // This packet should be ignored
+ .setOffset(80)
+ .setWindowEndOffset(200)
+ .setMaxChunkSizeBytes(20));
+
+ ListenableFuture<Void> future = transferClient.write(5, TEST_DATA_100B.toByteArray());
+
+ ExecutionException exception = assertThrows(ExecutionException.class, future::get);
+ assertThat(((TransferError) exception.getCause()).status()).isEqualTo(Status.DEADLINE_EXCEEDED);
+
+ final Chunk startAckConfirmation =
+ newChunk(Chunk.Type.START_ACK_CONFIRMATION, 123)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
+ .setRemainingBytes(TEST_DATA_100B.size())
+ .build();
+
+ assertThat(lastChunks())
+ .containsExactly(
+ // initial chunk and 2 retries
+ initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
+ initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
+ initialWriteChunk(5, ProtocolVersion.VERSION_TWO, TEST_DATA_100B.size()),
+ // START_ACK_CONFIRMATION and 2 retries
+ startAckConfirmation,
+ startAckConfirmation,
+ startAckConfirmation,
+ // send all data
+ dataChunk(123, TEST_DATA_100B, 0, 20), // data 0-20
+ dataChunk(123, TEST_DATA_100B, 20, 40), // data 20-40
+ dataChunk(123, TEST_DATA_100B, 40, 60), // data 40-60
+ // last packet retry, then hit the lifetime retry limit and abort
+ dataChunk(123, TEST_DATA_100B, 40, 60)); // data 40-60
+ }
+
+ private static ByteString range(int startInclusive, int endExclusive) {
+ assertThat(startInclusive).isLessThan((int) Byte.MAX_VALUE);
+ assertThat(endExclusive).isLessThan((int) Byte.MAX_VALUE);
+
+ byte[] bytes = new byte[endExclusive - startInclusive];
+ for (byte i = 0; i < bytes.length; ++i) {
+ bytes[i] = (byte) (i + startInclusive);
+ }
+ return ByteString.copyFrom(bytes);
+ }
+
+ private static Chunk.Builder newLegacyChunk(Chunk.Type type, int transferId) {
+ return Chunk.newBuilder().setType(type).setTransferId(transferId);
+ }
+
+ private static Chunk.Builder newChunk(Chunk.Type type, int sessionId) {
+ return Chunk.newBuilder().setType(type).setSessionId(sessionId);
+ }
+
+ private static Chunk initialReadChunk(int resourceId, ProtocolVersion version) {
+ return initialReadChunk(resourceId, version, TRANSFER_PARAMETERS);
+ }
+
+ private static Chunk initialReadChunk(
+ int resourceId, ProtocolVersion version, TransferParameters params) {
+ Chunk.Builder chunk = newLegacyChunk(Chunk.Type.START, resourceId)
+ .setResourceId(resourceId)
+ .setPendingBytes(params.maxPendingBytes())
+ .setWindowEndOffset(params.maxPendingBytes())
+ .setMaxChunkSizeBytes(params.maxChunkSizeBytes())
+ .setOffset(0);
+ if (version != ProtocolVersion.LEGACY) {
+ chunk.setProtocolVersion(version.ordinal());
+ }
+ if (params.chunkDelayMicroseconds() > 0) {
+ chunk.setMinDelayMicroseconds(params.chunkDelayMicroseconds());
+ }
+ return chunk.build();
+ }
+
+ private static Chunk readStartAckConfirmation(int sessionId, TransferParameters params) {
+ Chunk.Builder chunk = newChunk(Chunk.Type.START_ACK_CONFIRMATION, sessionId)
+ .setWindowEndOffset(params.maxPendingBytes())
+ .setMaxChunkSizeBytes(params.maxChunkSizeBytes())
+ .setOffset(0)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal());
+ if (params.chunkDelayMicroseconds() > 0) {
+ chunk.setMinDelayMicroseconds(params.chunkDelayMicroseconds());
+ }
+ return chunk.build();
+ }
+
+ private static Chunk initialWriteChunk(int resourceId, ProtocolVersion version, int size) {
+ Chunk.Builder chunk = newLegacyChunk(Chunk.Type.START, resourceId)
+ .setResourceId(resourceId)
+ .setRemainingBytes(size);
+ if (version != ProtocolVersion.LEGACY) {
+ chunk.setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal());
+ }
+ return chunk.build();
+ }
+
+ private static Chunk legacyFinalChunk(int sessionId, Status status) {
+ return newLegacyChunk(Chunk.Type.COMPLETION, sessionId).setStatus(status.code()).build();
+ }
+
+ private static Chunk finalChunk(int sessionId, Status status) {
+ return newChunk(Chunk.Type.COMPLETION, sessionId).setStatus(status.code()).build();
+ }
+
+ private static Chunk legacyDataChunk(int sessionId, ByteString data, int start, int end) {
+ if (start < 0 || end > data.size()) {
+ throw new IndexOutOfBoundsException("Invalid start or end");
+ }
+
+ Chunk.Builder chunk = newLegacyChunk(Chunk.Type.DATA, sessionId)
+ .setOffset(start)
+ .setData(data.substring(start, end));
+ if (end == data.size()) {
+ chunk.setRemainingBytes(0);
+ }
+ return chunk.build();
+ }
+
+ private static Chunk dataChunk(int sessionId, ByteString data, int start, int end) {
+ if (start < 0 || end > data.size()) {
+ throw new IndexOutOfBoundsException("Invalid start or end");
+ }
+
+ Chunk.Builder chunk =
+ newChunk(Chunk.Type.DATA, sessionId).setOffset(start).setData(data.substring(start, end));
+ if (end == data.size()) {
+ chunk.setRemainingBytes(0);
+ }
+ return chunk.build();
+ }
+
+ /** Runs an action */
+ private void syncWithTransferThread(Runnable action) {
+ transferClient.waitUntilEventsAreProcessedForTest();
+ action.run();
+ transferClient.waitUntilEventsAreProcessedForTest();
+ }
+
+ private void receiveReadServerError(Status status) {
+ syncWithTransferThread(() -> rpcClient.receiveServerError(SERVICE, "Read", status));
+ }
+
+ private void receiveWriteServerError(Status status) {
+ syncWithTransferThread(() -> rpcClient.receiveServerError(SERVICE, "Write", status));
+ }
+
+ private void receiveReadChunks(ChunkOrBuilder... chunks) {
+ for (ChunkOrBuilder chunk : chunks) {
+ syncWithTransferThread(() -> rpcClient.receiveServerStream(SERVICE, "Read", chunk));
+ }
+ }
+
+ private void receiveWriteChunks(ChunkOrBuilder... chunks) {
+ for (ChunkOrBuilder chunk : chunks) {
+ syncWithTransferThread(() -> rpcClient.receiveServerStream(SERVICE, "Write", chunk));
+ }
+ }
+
+ private void performReadStartHandshake(int resourceId, int sessionId) {
+ performReadStartHandshake(
+ resourceId, sessionId, TransferClient.DEFAULT_READ_TRANSFER_PARAMETERS);
+ }
+
+ private void performReadStartHandshake(int resourceId, int sessionId, TransferParameters params) {
+ assertThat(lastChunks())
+ .containsExactly(initialReadChunk(resourceId, ProtocolVersion.VERSION_TWO, params));
+
+ receiveReadChunks(newChunk(Chunk.Type.START_ACK, sessionId)
+ .setResourceId(resourceId)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ assertThat(lastChunks()).containsExactly(readStartAckConfirmation(sessionId, params));
+ }
+
+ private void performReadCompletionHandshake(int sessionId, Status status) {
+ assertThat(lastChunks())
+ .containsExactly(Chunk.newBuilder()
+ .setType(Chunk.Type.COMPLETION)
+ .setSessionId(sessionId)
+ .setStatus(status.ordinal())
+ .build());
+
+ receiveReadChunks(newChunk(Chunk.Type.COMPLETION_ACK, sessionId));
+ }
+
+ private void performWriteStartHandshake(int resourceId, int sessionId, int dataSize) {
+ assertThat(lastChunks())
+ .containsExactly(initialWriteChunk(resourceId, ProtocolVersion.VERSION_TWO, dataSize));
+
+ receiveWriteChunks(newChunk(Chunk.Type.START_ACK, sessionId)
+ .setResourceId(resourceId)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal()));
+
+ assertThat(lastChunks())
+ .containsExactly(newChunk(Chunk.Type.START_ACK_CONFIRMATION, sessionId)
+ .setProtocolVersion(ProtocolVersion.VERSION_TWO.ordinal())
+ .setRemainingBytes(dataSize)
+ .build());
+ }
+
+ /** Receive these read chunks after a chunk is sent. */
+ private void enqueueReadChunks(int afterPackets, Chunk.Builder... chunks) {
+ syncWithTransferThread(
+ () -> rpcClient.enqueueServerStream(SERVICE, "Read", afterPackets, chunks));
+ }
+
+ /** Receive these write chunks after a chunk is sent. */
+ private void enqueueWriteChunks(int afterPackets, Chunk.Builder... chunks) {
+ syncWithTransferThread(
+ () -> rpcClient.enqueueServerStream(SERVICE, "Write", afterPackets, chunks));
+ }
+
+ private List<Chunk> lastChunks() {
+ transferClient.waitUntilEventsAreProcessedForTest();
+ return rpcClient.lastClientStreams(Chunk.class);
+ }
+
+ private void createTransferClientThatMayTimeOut(ProtocolVersion version) {
+ createTransferClientThatMayTimeOut(version, Integer.MAX_VALUE);
+ }
+
+ private void createTransferClientThatMayTimeOut(ProtocolVersion version, int maxLifetimeRetries) {
+ createTransferClient(
+ version, 1, 1, maxLifetimeRetries, TransferEventHandler::runForTestsThatMustTimeOut);
+ }
+
+ private void createTransferClientForTransferThatWillNotTimeOut(ProtocolVersion version) {
+ createTransferClient(version, 60000, 60000, Integer.MAX_VALUE, TransferEventHandler::run);
+ }
+
+ private void createTransferClient(ProtocolVersion version,
+ int transferTimeoutMillis,
+ int initialTransferTimeoutMillis,
+ int maxLifetimeRetries,
+ Consumer<TransferEventHandler> eventHandlerFunction) {
+ if (transferClient != null) {
+ throw new AssertionError("createTransferClient must only be called once!");
+ }
+ transferClient = new TransferClient(rpcClient.client().method(CHANNEL_ID, SERVICE + "/Read"),
+ rpcClient.client().method(CHANNEL_ID, SERVICE + "/Write"),
+ TransferTimeoutSettings.builder()
+ .setTimeoutMillis(transferTimeoutMillis)
+ .setInitialTimeoutMillis(initialTransferTimeoutMillis)
+ .setMaxRetries(MAX_RETRIES)
+ .setMaxLifetimeRetries(maxLifetimeRetries)
+ .build(),
+ ()
+ -> this.shouldAbortFlag,
+ eventHandlerFunction);
+ transferClient.setProtocolVersion(version);
+ }
+}
diff --git a/pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h b/pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h
new file mode 100644
index 000000000..b108c5f52
--- /dev/null
+++ b/pw_transfer/public/pw_transfer/atomic_file_transfer_handler.h
@@ -0,0 +1,58 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <filesystem>
+#include <string>
+#include <string_view>
+#include <variant>
+
+#include "pw_status/status.h"
+#include "pw_stream/std_file_stream.h"
+#include "pw_transfer/handler.h"
+
+namespace pw::transfer {
+
+// The AtomicFileTransferHandler is intended to be used as a transfer
+// handler for files. It ensures that the target file of the transfer is always
+// in a correct state. In particular, the transfer is first done to a temporary
+// file and once complete, the original targeted file is updated.
+class AtomicFileTransferHandler : public ReadWriteHandler {
+ public:
+ AtomicFileTransferHandler(uint32_t resource_id, std::string_view file_path)
+ : ReadWriteHandler(resource_id), path_(file_path) {}
+
+ AtomicFileTransferHandler(const AtomicFileTransferHandler& rhs) = delete;
+ AtomicFileTransferHandler& operator=(const AtomicFileTransferHandler&) =
+ delete;
+ ~AtomicFileTransferHandler() override = default;
+
+ // Function called prior to initializing a read transfer.
+ Status PrepareRead() override;
+ // Function called after a read transfer is done.
+ // Status indicates whether transfer was done successfully.
+ void FinalizeRead(Status) override;
+ // Function called prior to initializing a write transfer.
+ Status PrepareWrite() override;
+ // Function called after a write transfer is done.
+ // Status indicates whether transfer was done successfully.
+ Status FinalizeWrite(Status) override;
+
+ private:
+ std::string path_;
+ std::variant<std::monostate, stream::StdFileReader, stream::StdFileWriter>
+ stream_{};
+};
+
+} // namespace pw::transfer
diff --git a/pw_transfer/public/pw_transfer/client.h b/pw_transfer/public/pw_transfer/client.h
index ae3622392..d7faa08ed 100644
--- a/pw_transfer/public/pw_transfer/client.h
+++ b/pw_transfer/public/pw_transfer/client.h
@@ -64,28 +64,37 @@ class Client {
: transfer_thread.max_chunk_size(),
transfer_thread.max_chunk_size(),
extend_window_divisor),
+ max_retries_(cfg::kDefaultMaxRetries),
+ max_lifetime_retries_(cfg::kDefaultMaxLifetimeRetries),
has_read_stream_(false),
has_write_stream_(false) {}
- // Begins a new read transfer for the given transfer ID. The data read from
+ // Begins a new read transfer for the given resource ID. The data read from
// the server is written to the provided writer. Returns OK if the transfer is
// successfully started. When the transfer finishes (successfully or not), the
// completion callback is invoked with the overall status.
- Status Read(
- uint32_t transfer_id,
- stream::Writer& output,
- CompletionFunc&& on_completion,
- chrono::SystemClock::duration timeout = cfg::kDefaultChunkTimeout);
+ Status Read(uint32_t resource_id,
+ stream::Writer& output,
+ CompletionFunc&& on_completion,
+ chrono::SystemClock::duration timeout = cfg::kDefaultChunkTimeout,
+ ProtocolVersion version = kDefaultProtocolVersion);
- // Begins a new write transfer for the given transfer ID. Data from the
+ // Begins a new write transfer for the given resource ID. Data from the
// provided reader is sent to the server. When the transfer finishes
// (successfully or not), the completion callback is invoked with the overall
// status.
Status Write(
- uint32_t transfer_id,
+ uint32_t resource_id,
stream::Reader& input,
CompletionFunc&& on_completion,
- chrono::SystemClock::duration timeout = cfg::kDefaultChunkTimeout);
+ chrono::SystemClock::duration timeout = cfg::kDefaultChunkTimeout,
+ ProtocolVersion version = kDefaultProtocolVersion);
+
+ // Terminates an ongoing transfer for the specified resource ID.
+ //
+ // TODO(frolv): This should not take a resource_id, but a handle to an active
+ // transfer session.
+ void CancelTransfer(uint32_t resource_id);
Status set_extend_window_divisor(uint32_t extend_window_divisor) {
if (extend_window_divisor <= 1) {
@@ -96,14 +105,36 @@ class Client {
return OkStatus();
}
+ constexpr Status set_max_retries(uint32_t max_retries) {
+ if (max_retries < 1 || max_retries > max_lifetime_retries_) {
+ return Status::InvalidArgument();
+ }
+ max_retries_ = max_retries;
+ return OkStatus();
+ }
+
+ constexpr Status set_max_lifetime_retries(uint32_t max_lifetime_retries) {
+ if (max_lifetime_retries < max_retries_) {
+ return Status::InvalidArgument();
+ }
+ max_lifetime_retries_ = max_lifetime_retries;
+ return OkStatus();
+ }
+
private:
+ static constexpr ProtocolVersion kDefaultProtocolVersion =
+ ProtocolVersion::kLatest;
+
using Transfer = pw_rpc::raw::Transfer;
void OnRpcError(Status status, internal::TransferType type);
Transfer::Client client_;
TransferThread& transfer_thread_;
+
internal::TransferParameters max_parameters_;
+ uint32_t max_retries_;
+ uint32_t max_lifetime_retries_;
bool has_read_stream_;
bool has_write_stream_;
diff --git a/pw_transfer/public/pw_transfer/handler.h b/pw_transfer/public/pw_transfer/handler.h
index 9680fb5e4..55df84db1 100644
--- a/pw_transfer/public/pw_transfer/handler.h
+++ b/pw_transfer/public/pw_transfer/handler.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -22,9 +22,13 @@
namespace pw::transfer {
namespace internal {
-// The internal::Handler class is the base class for the transfer handler
-// classes. Transfer handlers connect a transfer ID to the functions that do the
-// actual reads and/or writes.
+class Context;
+
+} // namespace internal
+
+// The Handler class is the base class for the transfer handler classes.
+// Transfer handlers connect a transfer resource ID to a data stream, wrapped
+// with initialization and cleanup procedures.
//
// Handlers use a stream::Reader or stream::Writer to do the reads and writes.
// They also provide optional Prepare and Finalize functions.
@@ -32,7 +36,7 @@ class Handler : public IntrusiveList<Handler>::Item {
public:
virtual ~Handler() = default;
- constexpr uint32_t id() const { return transfer_id_; }
+ constexpr uint32_t id() const { return resource_id_; }
// Called at the beginning of a read transfer. The stream::Reader must be
// ready to read after a successful PrepareRead() call. Returning a non-OK
@@ -60,17 +64,17 @@ class Handler : public IntrusiveList<Handler>::Item {
virtual Status FinalizeWrite(Status) { return OkStatus(); }
protected:
- constexpr Handler(uint32_t transfer_id, stream::Reader* reader)
- : transfer_id_(transfer_id), reader_(reader) {}
+ constexpr Handler(uint32_t resource_id, stream::Reader* reader)
+ : resource_id_(resource_id), reader_(reader) {}
- constexpr Handler(uint32_t transfer_id, stream::Writer* writer)
- : transfer_id_(transfer_id), writer_(writer) {}
+ constexpr Handler(uint32_t resource_id, stream::Writer* writer)
+ : resource_id_(resource_id), writer_(writer) {}
void set_reader(stream::Reader& reader) { reader_ = &reader; }
void set_writer(stream::Writer& writer) { writer_ = &writer; }
private:
- friend class Context;
+ friend class internal::Context;
// Prepares for either a read or write transfer.
Status Prepare(internal::TransferType type) {
@@ -84,7 +88,7 @@ class Handler : public IntrusiveList<Handler>::Item {
return *reader_;
}
- uint32_t transfer_id_;
+ uint32_t resource_id_;
// Use a union to support constexpr construction.
union {
@@ -93,60 +97,57 @@ class Handler : public IntrusiveList<Handler>::Item {
};
};
-} // namespace internal
-
-class ReadOnlyHandler : public internal::Handler {
+class ReadOnlyHandler : public Handler {
public:
- constexpr ReadOnlyHandler(uint32_t transfer_id)
- : internal::Handler(transfer_id, static_cast<stream::Reader*>(nullptr)) {}
+ constexpr ReadOnlyHandler(uint32_t resource_id)
+ : Handler(resource_id, static_cast<stream::Reader*>(nullptr)) {}
- constexpr ReadOnlyHandler(uint32_t transfer_id, stream::Reader& reader)
- : internal::Handler(transfer_id, &reader) {}
+ constexpr ReadOnlyHandler(uint32_t resource_id, stream::Reader& reader)
+ : Handler(resource_id, &reader) {}
- virtual ~ReadOnlyHandler() = default;
+ ~ReadOnlyHandler() override = default;
Status PrepareRead() override { return OkStatus(); }
// Writes are not supported.
Status PrepareWrite() final { return Status::PermissionDenied(); }
- using internal::Handler::set_reader;
+ using Handler::set_reader;
private:
- using internal::Handler::set_writer;
+ using Handler::set_writer;
};
-class WriteOnlyHandler : public internal::Handler {
+class WriteOnlyHandler : public Handler {
public:
- constexpr WriteOnlyHandler(uint32_t transfer_id)
- : internal::Handler(transfer_id, static_cast<stream::Writer*>(nullptr)) {}
+ constexpr WriteOnlyHandler(uint32_t resource_id)
+ : Handler(resource_id, static_cast<stream::Writer*>(nullptr)) {}
- constexpr WriteOnlyHandler(uint32_t transfer_id, stream::Writer& writer)
- : internal::Handler(transfer_id, &writer) {}
+ constexpr WriteOnlyHandler(uint32_t resource_id, stream::Writer& writer)
+ : Handler(resource_id, &writer) {}
- virtual ~WriteOnlyHandler() = default;
+ ~WriteOnlyHandler() override = default;
// Reads are not supported.
Status PrepareRead() final { return Status::PermissionDenied(); }
Status PrepareWrite() override { return OkStatus(); }
- using internal::Handler::set_writer;
+ using Handler::set_writer;
private:
- using internal::Handler::set_reader;
+ using Handler::set_reader;
};
-class ReadWriteHandler : public internal::Handler {
+class ReadWriteHandler : public Handler {
public:
- constexpr ReadWriteHandler(uint32_t transfer_id)
- : internal::Handler(transfer_id, static_cast<stream::Reader*>(nullptr)) {}
- constexpr ReadWriteHandler(uint32_t transfer_id,
+ constexpr ReadWriteHandler(uint32_t resource_id)
+ : Handler(resource_id, static_cast<stream::Reader*>(nullptr)) {}
+ constexpr ReadWriteHandler(uint32_t resource_id,
stream::ReaderWriter& reader_writer)
- : internal::Handler(transfer_id,
- &static_cast<stream::Reader&>(reader_writer)) {}
+ : Handler(resource_id, &static_cast<stream::Reader&>(reader_writer)) {}
- virtual ~ReadWriteHandler() = default;
+ ~ReadWriteHandler() override = default;
// Both reads and writes are supported.
Status PrepareRead() override { return OkStatus(); }
diff --git a/pw_transfer/public/pw_transfer/internal/chunk.h b/pw_transfer/public/pw_transfer/internal/chunk.h
index b20acf44f..ffb556d6e 100644
--- a/pw_transfer/public/pw_transfer/internal/chunk.h
+++ b/pw_transfer/public/pw_transfer/internal/chunk.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -17,50 +17,262 @@
#include "pw_bytes/span.h"
#include "pw_result/result.h"
+#include "pw_transfer/internal/protocol.h"
+#include "pw_transfer/transfer.pwpb.h"
namespace pw::transfer::internal {
-struct Chunk {
- // TODO(frolv): This is copied from the proto enum. Ideally, this entire class
- // would be generated by pw_protobuf.
- enum Type {
- kTransferData = 0,
- kTransferStart = 1,
- kParametersRetransmit = 2,
- kParametersContinue = 3,
- kTransferCompletion = 4,
- kTransferCompletionAck = 5, // Currently unused.
+class Chunk {
+ public:
+ using Type = transfer::pwpb::Chunk::Type;
+
+ class Identifier {
+ public:
+ constexpr bool is_session() const { return type_ == kSession; }
+ constexpr bool is_resource() const { return !is_session(); }
+
+ constexpr uint32_t value() const { return value_; }
+
+ private:
+ friend class Chunk;
+
+ static constexpr Identifier Session(uint32_t value) {
+ return Identifier(kSession, value);
+ }
+ static constexpr Identifier Resource(uint32_t value) {
+ return Identifier(kResource, value);
+ }
+
+ enum IdType {
+ kSession,
+ kResource,
+ };
+
+ constexpr Identifier(IdType type, uint32_t value)
+ : type_(type), value_(value) {}
+
+ IdType type_;
+ uint32_t value_;
};
- // The initial chunk always has an offset of 0 and no data or status.
- //
- // TODO(frolv): Going forward, all users of transfer should set a type for
- // all chunks. This initial chunk assumption should be removed.
+ // Partially decodes a transfer chunk to find its transfer context identifier.
+ // Depending on the protocol version and type of chunk, this may be one of
+ // several proto fields.
+ static Result<Identifier> ExtractIdentifier(ConstByteSpan message);
+
+ // Constructs a new chunk with the given transfer protocol version. All fields
+ // are initialized to their zero values.
+ constexpr Chunk(ProtocolVersion version, Type type)
+ : Chunk(version, std::optional<Type>(type)) {}
+
+ // Parses a chunk from a serialized protobuf message.
+ static Result<Chunk> Parse(ConstByteSpan message);
+
+ // Creates a terminating status chunk within a transfer.
+ static Chunk Final(ProtocolVersion version,
+ uint32_t session_id,
+ Status status) {
+ return Chunk(version, Type::kCompletion)
+ .set_session_id(session_id)
+ .set_status(status);
+ }
+
+ // Encodes the chunk to the specified buffer, returning a span of the
+ // serialized data on success.
+ Result<ConstByteSpan> Encode(ByteSpan buffer) const;
+
+ // Returns the size of the serialized chunk based on the fields currently set
+ // within the chunk object.
+ size_t EncodedSize() const;
+
+ constexpr Chunk& set_session_id(uint32_t session_id) {
+ session_id_ = session_id;
+ return *this;
+ }
+
+ constexpr Chunk& set_resource_id(uint32_t resource_id) {
+ resource_id_ = resource_id;
+ return *this;
+ }
+
+ constexpr Chunk& set_protocol_version(ProtocolVersion version) {
+ protocol_version_ = version;
+ return *this;
+ }
+
+ constexpr Chunk& set_window_end_offset(uint32_t window_end_offset) {
+ window_end_offset_ = window_end_offset;
+ return *this;
+ }
+
+ constexpr Chunk& set_max_chunk_size_bytes(uint32_t max_chunk_size_bytes) {
+ max_chunk_size_bytes_ = max_chunk_size_bytes;
+ return *this;
+ }
+
+ constexpr Chunk& set_min_delay_microseconds(uint32_t min_delay_microseconds) {
+ min_delay_microseconds_ = min_delay_microseconds;
+ return *this;
+ }
+
+ constexpr Chunk& set_offset(uint32_t offset) {
+ offset_ = offset;
+ return *this;
+ }
+
+ constexpr Chunk& set_payload(ConstByteSpan payload) {
+ payload_ = payload;
+ return *this;
+ }
+
+ constexpr Chunk& set_remaining_bytes(uint64_t remaining_bytes) {
+ remaining_bytes_ = remaining_bytes;
+ return *this;
+ }
+
+ // TODO(frolv): For some reason, the compiler complains if this setter is
+ // marked constexpr. Leaving it off for now, but this should be investigated
+ // and fixed.
+ Chunk& set_status(Status status) {
+ status_ = status;
+ return *this;
+ }
+
+ constexpr uint32_t session_id() const { return session_id_; }
+
+ constexpr std::optional<uint32_t> resource_id() const {
+ if (is_legacy()) {
+ // In the legacy protocol, resource_id and session_id are the same (i.e.
+ // transfer_id).
+ return session_id_;
+ }
+
+ return resource_id_;
+ }
+
+ constexpr uint32_t window_end_offset() const { return window_end_offset_; }
+ constexpr uint32_t offset() const { return offset_; }
+ constexpr std::optional<Status> status() const { return status_; }
+
+ constexpr bool has_payload() const { return !payload_.empty(); }
+ constexpr ConstByteSpan payload() const { return payload_; }
+
+ constexpr std::optional<uint32_t> max_chunk_size_bytes() const {
+ return max_chunk_size_bytes_;
+ }
+ constexpr std::optional<uint32_t> min_delay_microseconds() const {
+ return min_delay_microseconds_;
+ }
+ constexpr std::optional<uint64_t> remaining_bytes() const {
+ return remaining_bytes_;
+ }
+
+ constexpr ProtocolVersion protocol_version() const {
+ return protocol_version_;
+ }
+
+ constexpr bool is_legacy() const {
+ return protocol_version_ == ProtocolVersion::kLegacy;
+ }
+
+ constexpr Type type() const {
+ // Legacy protocol chunks may not have a type, but newer versions always
+ // will. Try to deduce the type of a legacy chunk without one set.
+ if (!is_legacy() || type_.has_value()) {
+ return type_.value();
+ }
+
+ // The type-less legacy transfer protocol doesn't support handshakes or
+ // continuation parameters. Therefore, there are only three possible chunk
+ // types: start, data, and retransmit.
+ if (IsInitialChunk()) {
+ return Type::kStart;
+ }
+
+ if (has_payload()) {
+ return Type::kData;
+ }
+
+ return Type::kParametersRetransmit;
+ }
+
+ // Returns true if this parameters chunk is requesting that the transmitter
+ // transmit from its set offset instead of simply ACKing.
+ constexpr bool RequestsTransmissionFromOffset() const {
+ if (is_legacy() && !type_.has_value()) {
+ return true;
+ }
+
+ return type_.value() == Type::kParametersRetransmit ||
+ type_.value() == Type::kStartAckConfirmation ||
+ type_.value() == Type::kStart;
+ }
+
constexpr bool IsInitialChunk() const {
- return type == Type::kTransferStart ||
- (offset == 0 && data.empty() && !status.has_value());
+ if (protocol_version_ >= ProtocolVersion::kVersionTwo) {
+ return type_ == Type::kStart;
+ }
+
+ // In legacy versions of the transfer protocol, the chunk type is not always
+ // set. Infer that a chunk is initial if it has an offset of 0 and no data
+ // or status.
+ return type_ == Type::kStart ||
+ (offset_ == 0 && !has_payload() && !status_.has_value());
+ }
+
+ constexpr bool IsTerminatingChunk() const {
+ return type_ == Type::kCompletion || (is_legacy() && status_.has_value());
}
// The final chunk from the transmitter sets remaining_bytes to 0 in both Read
// and Write transfers.
- constexpr bool IsFinalTransmitChunk() const { return remaining_bytes == 0u; }
-
- uint32_t transfer_id;
- uint32_t window_end_offset;
- std::optional<uint32_t> pending_bytes;
- std::optional<uint32_t> max_chunk_size_bytes;
- std::optional<uint32_t> min_delay_microseconds;
- uint32_t offset;
- ConstByteSpan data;
- std::optional<uint64_t> remaining_bytes;
- std::optional<Status> status;
- std::optional<Type> type;
-};
+ constexpr bool IsFinalTransmitChunk() const { return remaining_bytes_ == 0u; }
+
+ // Returns true if this chunk is part of an initial transfer handshake.
+ constexpr bool IsInitialHandshakeChunk() const {
+ return type_ == Type::kStart || type_ == Type::kStartAck ||
+ type_ == Type::kStartAckConfirmation;
+ }
+
+ private:
+ constexpr Chunk(ProtocolVersion version, std::optional<Type> type)
+ : session_id_(0),
+ resource_id_(std::nullopt),
+ window_end_offset_(0),
+ max_chunk_size_bytes_(std::nullopt),
+ min_delay_microseconds_(std::nullopt),
+ offset_(0),
+ payload_({}),
+ remaining_bytes_(std::nullopt),
+ status_(std::nullopt),
+ type_(type),
+ protocol_version_(version) {}
-// Partially decodes a transfer chunk to find its transfer ID field.
-Result<uint32_t> ExtractTransferId(ConstByteSpan message);
+ constexpr Chunk() : Chunk(ProtocolVersion::kUnknown, std::nullopt) {}
-Status DecodeChunk(ConstByteSpan message, Chunk& chunk);
-Result<ConstByteSpan> EncodeChunk(const Chunk& chunk, ByteSpan buffer);
+ // Returns true if this chunk should write legacy protocol fields to the
+ // serialized message.
+ //
+ // The first chunk of a transfer (type TRANSFER_START) is a special case: as
+ // we do not yet know what version of the protocol the other end is speaking,
+ // every legacy field must be encoded alongside newer ones to ensure that the
+ // chunk is processable. Following a response, the common protocol version
+ // will be determined and fields omitted as necessary.
+ constexpr bool ShouldEncodeLegacyFields() const {
+ return is_legacy() || type_ == Type::kStart;
+ }
+
+ uint32_t session_id_;
+ std::optional<uint32_t> resource_id_;
+ uint32_t window_end_offset_;
+ std::optional<uint32_t> max_chunk_size_bytes_;
+ std::optional<uint32_t> min_delay_microseconds_;
+ uint32_t offset_;
+ ConstByteSpan payload_;
+ std::optional<uint64_t> remaining_bytes_;
+ std::optional<Status> status_;
+ std::optional<Type> type_;
+ ProtocolVersion protocol_version_;
+};
} // namespace pw::transfer::internal
diff --git a/pw_transfer/public/pw_transfer/internal/client_context.h b/pw_transfer/public/pw_transfer/internal/client_context.h
index a84af3152..9912aa4ac 100644
--- a/pw_transfer/public/pw_transfer/internal/client_context.h
+++ b/pw_transfer/public/pw_transfer/internal/client_context.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -27,6 +27,13 @@ class ClientContext final : public Context {
on_completion_ = std::move(on_completion);
}
+ // In client-side transfer contexts, a session ID may not yet have been
+ // assigned by the server, in which case resource_id is used as the context
+ // identifier.
+ constexpr uint32_t id() const {
+ return session_id() == kUnassignedSessionId ? resource_id() : session_id();
+ }
+
private:
Status FinalCleanup(Status status) override;
diff --git a/pw_transfer/public/pw_transfer/internal/config.h b/pw_transfer/public/pw_transfer/internal/config.h
index f144ac2a7..9410d990b 100644
--- a/pw_transfer/public/pw_transfer/internal/config.h
+++ b/pw_transfer/public/pw_transfer/internal/config.h
@@ -15,6 +15,7 @@
// Configuration macros for the transfer module.
#pragma once
+#include <chrono>
#include <cinttypes>
#include <limits>
@@ -30,6 +31,21 @@ static_assert(PW_TRANSFER_DEFAULT_MAX_RETRIES > 0 &&
PW_TRANSFER_DEFAULT_MAX_RETRIES <=
std::numeric_limits<uint8_t>::max());
+// The default maximum number of times a transfer should retry sending a chunk
+// over the course of its entire lifetime.
+// This number should be high, particularly if long-running transfers are
+// expected. Its purpose is to prevent transfers from getting stuck in an
+// infinite loop.
+#ifndef PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES
+#define PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES \
+ (static_cast<uint32_t>(PW_TRANSFER_DEFAULT_MAX_RETRIES) * 1000u)
+#endif // PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES
+
+static_assert(PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES >
+ PW_TRANSFER_DEFAULT_MAX_RETRIES &&
+ PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES <=
+ std::numeric_limits<uint32_t>::max());
+
// The default amount of time, in milliseconds, to wait for a chunk to arrive
// before retrying. This can later be configured per-transfer.
#ifndef PW_TRANSFER_DEFAULT_TIMEOUT_MS
@@ -54,8 +70,12 @@ static_assert(PW_TRANSFER_DEFAULT_EXTEND_WINDOW_DIVISOR > 1);
namespace pw::transfer::cfg {
inline constexpr uint8_t kDefaultMaxRetries = PW_TRANSFER_DEFAULT_MAX_RETRIES;
+inline constexpr uint16_t kDefaultMaxLifetimeRetries =
+ PW_TRANSFER_DEFAULT_MAX_LIFETIME_RETRIES;
+
inline constexpr chrono::SystemClock::duration kDefaultChunkTimeout =
std::chrono::milliseconds(PW_TRANSFER_DEFAULT_TIMEOUT_MS);
+
inline constexpr uint32_t kDefaultExtendWindowDivisor =
PW_TRANSFER_DEFAULT_EXTEND_WINDOW_DIVISOR;
diff --git a/pw_transfer/public/pw_transfer/internal/context.h b/pw_transfer/public/pw_transfer/internal/context.h
index f825419dd..d72f81384 100644
--- a/pw_transfer/public/pw_transfer/internal/context.h
+++ b/pw_transfer/public/pw_transfer/internal/context.h
@@ -25,6 +25,7 @@
#include "pw_stream/stream.h"
#include "pw_transfer/internal/chunk.h"
#include "pw_transfer/internal/event.h"
+#include "pw_transfer/internal/protocol.h"
#include "pw_transfer/rate_estimate.h"
namespace pw::transfer::internal {
@@ -69,12 +70,15 @@ class TransferParameters {
// Information about a single transfer.
class Context {
public:
+ static constexpr uint32_t kUnassignedSessionId = 0;
+
Context(const Context&) = delete;
Context(Context&&) = delete;
Context& operator=(const Context&) = delete;
Context& operator=(Context&&) = delete;
- constexpr uint32_t transfer_id() const { return transfer_id_; }
+ constexpr uint32_t session_id() const { return session_id_; }
+ constexpr uint32_t resource_id() const { return resource_id_; }
// True if the context has been used for a transfer (it has an ID).
bool initialized() const {
@@ -82,7 +86,7 @@ class Context {
}
// True if the transfer is active.
- bool active() const { return transfer_state_ >= TransferState::kWaiting; }
+ bool active() const { return transfer_state_ >= TransferState::kInitiating; }
std::optional<chrono::SystemClock::time_point> timeout() const {
return active() && next_timeout_ != kNoTimeout
@@ -104,20 +108,25 @@ class Context {
~Context() = default;
constexpr Context()
- : transfer_id_(0),
+ : session_id_(kUnassignedSessionId),
+ resource_id_(0),
+ desired_protocol_version_(ProtocolVersion::kUnknown),
+ configured_protocol_version_(ProtocolVersion::kUnknown),
flags_(0),
transfer_state_(TransferState::kInactive),
retries_(0),
max_retries_(0),
+ lifetime_retries_(0),
+ max_lifetime_retries_(0),
stream_(nullptr),
rpc_writer_(nullptr),
offset_(0),
window_size_(0),
window_end_offset_(0),
- pending_bytes_(0),
max_chunk_size_bytes_(std::numeric_limits<uint32_t>::max()),
max_parameters_(nullptr),
thread_(nullptr),
+ last_chunk_sent_(Chunk::Type::kData),
last_chunk_offset_(0),
chunk_timeout_(chrono::SystemClock::duration::zero()),
interchunk_delay_(chrono::SystemClock::for_at_least(
@@ -130,23 +139,41 @@ class Context {
private:
enum class TransferState : uint8_t {
- // This ServerContext has never been used for a transfer. It is available
- // for use for a transfer.
+ // The context is available for use for a new transfer.
kInactive,
- // A transfer completed and the final status chunk was sent. The Context
- // is
- // available for use for a new transfer. A receive transfer uses this
- // state
- // to allow a transmitter to retry its last chunk if the final status
- // chunk
+
+ // A transfer completed and the final status chunk was sent. The Context is
+ // available for use for a new transfer. A receive transfer uses this state
+ // to allow a transmitter to retry its last chunk if the final status chunk
// was dropped.
+ //
+ // Only used by the legacy protocol. Starting from version 2, transfer
+ // completions are acknowledged, for which the TERMINATING state is used.
kCompleted,
+
+ // Transfer is starting. The server and client are performing an initial
+ // handshake and negotiating protocol and feature flags.
+ kInitiating,
+
// Waiting for the other end to send a chunk.
kWaiting,
+
// Transmitting a window of data to a receiver.
kTransmitting,
+
// Recovering after one or more chunks was dropped in an active transfer.
kRecovery,
+
+ // Transfer has completed locally and is waiting for the peer to acknowledge
+ // its final status. Only entered by the terminating side of the transfer.
+ //
+ // The context remains in a TERMINATING state until it receives an
+ // acknowledgement from the peer or times out. Either way, the context
+ // transitions to INACTIVE afterwards, fully cleaning it up for reuse.
+ //
+ // Used instead of COMPLETED starting from version 2. Unlike COMPLETED,
+ // contexts in a TERMINATING state cannot be used to start new transfers.
+ kTerminating,
};
enum class TransmitAction {
@@ -160,10 +187,10 @@ class Context {
void set_transfer_state(TransferState state) { transfer_state_ = state; }
- // The transfer ID as unsigned instead of uint32_t so it can be used with %u.
+ // The session ID as unsigned instead of uint32_t so it can be used with %u.
unsigned id_for_log() const {
- static_assert(sizeof(unsigned) >= sizeof(transfer_id_));
- return static_cast<unsigned>(transfer_id_);
+ static_assert(sizeof(unsigned) >= sizeof(session_id_));
+ return static_cast<unsigned>(session_id_);
}
stream::Reader& reader() {
@@ -176,6 +203,19 @@ class Context {
return static_cast<stream::Writer&>(*stream_);
}
+ bool DataTransferComplete() const {
+ return transfer_state_ == TransferState::kTerminating ||
+ transfer_state_ == TransferState::kCompleted;
+ }
+
+ bool ShouldSkipCompletionHandshake() const {
+ // Completion handshakes are not part of the legacy protocol. Additionally,
+ // transfers which have not yet fully established should not handshake and
+ // simply time out.
+ return configured_protocol_version_ <= ProtocolVersion::kLegacy ||
+ transfer_state_ == TransferState::kInitiating;
+ }
+
// Calculates the maximum size of actual data that can be sent within a
// single client write transfer chunk, accounting for the overhead of the
// transfer protocol and RPC system.
@@ -206,7 +246,7 @@ class Context {
// Starts a new transfer on the server after receiving a request from a
// client.
- void StartTransferAsServer(const NewTransferEvent& new_transfer);
+ bool StartTransferAsServer(const NewTransferEvent& new_transfer);
// Does final cleanup specific to the server or client. Returns whether the
// cleanup succeeded. An error in cleanup indicates that the transfer
@@ -216,6 +256,11 @@ class Context {
// Processes a chunk in either a transfer or receive transfer.
void HandleChunkEvent(const ChunkEvent& event);
+ // Runs the initial three-way handshake when starting a new transfer.
+ void PerformInitialHandshake(const Chunk& chunk);
+
+ void UpdateLocalProtocolConfigurationFromPeer(const Chunk& chunk);
+
// Processes a chunk in a transmit transfer.
void HandleTransmitChunk(const Chunk& chunk);
@@ -234,18 +279,40 @@ class Context {
// Sends the first chunk in a transmit transfer.
void SendInitialTransmitChunk();
+ // Updates the current receive transfer parameters based on the context's
+ // configuration.
+ void UpdateTransferParameters();
+
+ // Populates the transfer parameters fields on a chunk object.
+ void SetTransferParameters(Chunk& parameters);
+
// In a receive transfer, sends a parameters chunk telling the transmitter
// how much data they can send.
void SendTransferParameters(TransmitAction action);
- // Updates the current receive transfer parameters from the provided object,
- // then sends them.
+ // Updates the current receive transfer parameters, then sends them.
void UpdateAndSendTransferParameters(TransmitAction action);
+ // Processes a chunk in a terminating state.
+ void HandleTerminatingChunk(const Chunk& chunk);
+
+ // Ends the transfer with the specified status, sending a completion chunk to
+ // the peer.
+ void TerminateTransfer(Status status, bool with_resource_id = false);
+
+ // Ends a transfer following notification of completion from the peer.
+ void HandleTermination(Status status);
+
+ // Forcefully ends a transfer locally without contacting the peer.
+ void Abort(Status status) {
+ Finish(status);
+ set_transfer_state(TransferState::kCompleted);
+ }
+
// Sends a final status chunk of a completed transfer without updating the
- // the transfer. Sends status_, which MUST have been set by a previous
- // Finish() call.
- void SendFinalStatusChunk();
+ // transfer. Sends status_, which MUST have been set by a previous Finish()
+ // call.
+ void SendFinalStatusChunk(bool with_resource_id = false);
// Marks the transfer as completed and calls FinalCleanup(). Sets status_ to
// the final status for this transfer. The transfer MUST be active when this
@@ -265,9 +332,13 @@ class Context {
// Resends the last packet or aborts the transfer if the maximum retries has
// been exceeded.
void Retry();
+ void RetryHandshake();
+
+ void LogTransferConfiguration();
static constexpr uint8_t kFlagsType = 1 << 0;
static constexpr uint8_t kFlagsDataSent = 1 << 1;
+ static constexpr uint8_t kFlagsContactMade = 1 << 2;
static constexpr uint32_t kDefaultChunkDelayMicroseconds = 2000;
@@ -281,11 +352,22 @@ class Context {
static constexpr chrono::SystemClock::time_point kNoTimeout =
chrono::SystemClock::time_point(chrono::SystemClock::duration(0));
- uint32_t transfer_id_;
+ uint32_t session_id_;
+ uint32_t resource_id_;
+
+ // The version of the transfer protocol that this node wants to run.
+ ProtocolVersion desired_protocol_version_;
+
+ // The version of the transfer protocol that the context is actually running,
+ // following negotiation with the transfer peer.
+ ProtocolVersion configured_protocol_version_;
+
uint8_t flags_;
TransferState transfer_state_;
uint8_t retries_;
uint8_t max_retries_;
+ uint32_t lifetime_retries_;
+ uint32_t max_lifetime_retries_;
// The stream from which to read or to which to write data.
stream::Stream* stream_;
@@ -294,13 +376,13 @@ class Context {
uint32_t offset_;
uint32_t window_size_;
uint32_t window_end_offset_;
- // TODO(pwbug/584): Remove pending_bytes in favor of window_end_offset.
- uint32_t pending_bytes_;
uint32_t max_chunk_size_bytes_;
const TransferParameters* max_parameters_;
TransferThread* thread_;
+ Chunk::Type last_chunk_sent_;
+
union {
Status status_; // Used when state is kCompleted.
uint32_t last_chunk_offset_; // Used in states kWaiting and kRecovery.
diff --git a/pw_transfer/public/pw_transfer/internal/event.h b/pw_transfer/public/pw_transfer/internal/event.h
index b4cc6ba02..79a7853d1 100644
--- a/pw_transfer/public/pw_transfer/internal/event.h
+++ b/pw_transfer/public/pw_transfer/internal/event.h
@@ -16,8 +16,13 @@
#include "pw_chrono/system_clock.h"
#include "pw_rpc/writer.h"
#include "pw_stream/stream.h"
+#include "pw_transfer/internal/protocol.h"
-namespace pw::transfer::internal {
+namespace pw::transfer {
+
+class Handler;
+
+namespace internal {
enum class TransferType : bool { kTransmit, kReceive };
@@ -41,13 +46,15 @@ enum class EventType {
kClientTimeout,
kServerTimeout,
+ // Terminates an ongoing transfer with a specified status, optionally sending
+ // a status chunk to the other end of the transfer.
+ kClientEndTransfer,
+ kServerEndTransfer,
+
// Sends a status chunk to terminate a transfer. This does not call into the
// transfer context's completion handler; it is for out-of-band termination.
kSendStatusChunk,
- // Updates one of the transfer thread's RPC streams.
- kSetTransferStream,
-
// Manages the list of transfer handlers for a transfer service.
kAddTransferHandler,
kRemoveTransferHandler,
@@ -57,34 +64,53 @@ enum class EventType {
};
// Forward declarations required for events.
-class Handler;
class TransferParameters;
class TransferThread;
struct NewTransferEvent {
TransferType type;
- uint32_t transfer_id;
- uint32_t handler_id;
+ ProtocolVersion protocol_version;
+ uint32_t session_id;
+ uint32_t resource_id;
rpc::Writer* rpc_writer;
const TransferParameters* max_parameters;
chrono::SystemClock::duration timeout;
uint32_t max_retries;
+ uint32_t max_lifetime_retries;
TransferThread* transfer_thread;
union {
stream::Stream* stream; // In client-side transfers.
Handler* handler; // In server-side transfers.
};
+
+ const std::byte* raw_chunk_data;
+ size_t raw_chunk_size;
};
+// A chunk received by a transfer client / server.
struct ChunkEvent {
- uint32_t transfer_id;
+ // Identifier for the transfer to which the chunk belongs.
+ uint32_t context_identifier;
+
+ // If true, only match the identifier against context resource IDs.
+ bool match_resource_id;
+
+ // The raw data of the chunk.
const std::byte* data;
size_t size;
};
+struct EndTransferEvent {
+ uint32_t session_id;
+ Status::Code status;
+ bool send_status_chunk;
+};
+
struct SendStatusChunkEvent {
- uint32_t transfer_id;
+ uint32_t session_id;
+ bool set_resource_id;
+ ProtocolVersion protocol_version;
Status::Code status;
TransferStream stream;
};
@@ -95,11 +121,12 @@ struct Event {
union {
NewTransferEvent new_transfer;
ChunkEvent chunk;
+ EndTransferEvent end_transfer;
SendStatusChunkEvent send_status_chunk;
- TransferStream set_transfer_stream;
Handler* add_transfer_handler;
Handler* remove_transfer_handler;
};
};
-} // namespace pw::transfer::internal
+} // namespace internal
+} // namespace pw::transfer
diff --git a/pw_transfer/public/pw_transfer/internal/protocol.h b/pw_transfer/public/pw_transfer/internal/protocol.h
new file mode 100644
index 000000000..80c9bb7c0
--- /dev/null
+++ b/pw_transfer/public/pw_transfer/internal/protocol.h
@@ -0,0 +1,46 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdint>
+
+namespace pw::transfer {
+
+enum class ProtocolVersion {
+ // Protocol version not known or not set.
+ kUnknown,
+
+ // The original transfer protocol, prior to transfer start/end handshakes.
+ kLegacy,
+
+ // Second version of the transfer protocol. Guarantees type fields on all
+ // chunks, deprecates pending_bytes in favor of window_end_offset, splits
+ // transfer resource IDs from ephemeral session IDs, and adds a handshake
+ // to the start and end of all transfer sessions.
+ kVersionTwo,
+
+ // Alias to the most up-to-date version of the transfer protocol.
+ kLatest = kVersionTwo,
+};
+
+constexpr bool ValidProtocolVersion(ProtocolVersion version) {
+ return version > ProtocolVersion::kUnknown &&
+ version <= ProtocolVersion::kLatest;
+}
+
+constexpr bool ValidProtocolVersion(uint32_t version) {
+ return ValidProtocolVersion(static_cast<ProtocolVersion>(version));
+}
+
+} // namespace pw::transfer
diff --git a/pw_transfer/public/pw_transfer/internal/server_context.h b/pw_transfer/public/pw_transfer/internal/server_context.h
index 038feece6..265b75aa9 100644
--- a/pw_transfer/public/pw_transfer/internal/server_context.h
+++ b/pw_transfer/public/pw_transfer/internal/server_context.h
@@ -32,6 +32,12 @@ class ServerContext final : public Context {
// ClientContexts don't track it.
void set_handler(Handler& handler) { handler_ = &handler; }
+ // Returns the pointer to the current handler.
+ const Handler* handler() { return handler_; }
+
+ // In server-side transfer contexts, a session ID always exists.
+ constexpr uint32_t id() const { return session_id(); }
+
private:
// Ends the transfer with the given status, calling the handler's Finalize
// method. No chunks are sent.
diff --git a/pw_transfer/public/pw_transfer/transfer.h b/pw_transfer/public/pw_transfer/transfer.h
index f6e71c314..42a9d7bca 100644
--- a/pw_transfer/public/pw_transfer/transfer.h
+++ b/pw_transfer/public/pw_transfer/transfer.h
@@ -27,7 +27,7 @@
namespace pw::transfer {
namespace internal {
-struct Chunk;
+class Chunk;
} // namespace internal
@@ -57,13 +57,16 @@ class TransferService : public pw_rpc::raw::Transfer::Service<TransferService> {
uint32_t max_pending_bytes,
chrono::SystemClock::duration chunk_timeout = cfg::kDefaultChunkTimeout,
uint8_t max_retries = cfg::kDefaultMaxRetries,
- uint32_t extend_window_divisor = cfg::kDefaultExtendWindowDivisor)
+ uint32_t extend_window_divisor = cfg::kDefaultExtendWindowDivisor,
+ uint32_t max_lifetime_retries = cfg::kDefaultMaxLifetimeRetries)
: max_parameters_(max_pending_bytes,
transfer_thread.max_chunk_size(),
extend_window_divisor),
thread_(transfer_thread),
chunk_timeout_(chunk_timeout),
- max_retries_(max_retries) {}
+ max_retries_(max_retries),
+ max_lifetime_retries_(max_lifetime_retries),
+ next_session_id_(1) {}
TransferService(const TransferService&) = delete;
TransferService(TransferService&&) = delete;
@@ -85,10 +88,14 @@ class TransferService : public pw_rpc::raw::Transfer::Service<TransferService> {
thread_.SetServerWriteStream(reader_writer);
}
- void RegisterHandler(internal::Handler& handler) {
+ void RegisterHandler(Handler& handler) {
thread_.AddTransferHandler(handler);
}
+ void UnregisterHandler(Handler& handler) {
+ thread_.RemoveTransferHandler(handler);
+ }
+
void set_max_pending_bytes(uint32_t max_pending_bytes) {
max_parameters_.set_pending_bytes(max_pending_bytes);
}
@@ -99,10 +106,6 @@ class TransferService : public pw_rpc::raw::Transfer::Service<TransferService> {
max_parameters_.set_max_chunk_size_bytes(max_chunk_size_bytes);
}
- void UnregisterHandler(internal::Handler& handler) {
- thread_.RemoveTransferHandler(handler);
- }
-
void set_chunk_timeout(chrono::SystemClock::duration chunk_timeout) {
chunk_timeout_ = chunk_timeout;
}
@@ -121,11 +124,17 @@ class TransferService : public pw_rpc::raw::Transfer::Service<TransferService> {
private:
void HandleChunk(ConstByteSpan message, internal::TransferType type);
+ // TODO(frolv): This could be more sophisticated and less predictable.
+ uint32_t GenerateNewSessionId() { return next_session_id_++; }
+
internal::TransferParameters max_parameters_;
TransferThread& thread_;
chrono::SystemClock::duration chunk_timeout_;
uint8_t max_retries_;
+ uint32_t max_lifetime_retries_;
+
+ uint32_t next_session_id_;
};
} // namespace pw::transfer
diff --git a/pw_transfer/public/pw_transfer/transfer_thread.h b/pw_transfer/public/pw_transfer/transfer_thread.h
index 9e21bba5c..5904c9506 100644
--- a/pw_transfer/public/pw_transfer/transfer_thread.h
+++ b/pw_transfer/public/pw_transfer/transfer_thread.h
@@ -14,7 +14,6 @@
#pragma once
#include <cstdint>
-#include <span>
#include "pw_assert/assert.h"
#include "pw_chrono/system_clock.h"
@@ -22,6 +21,7 @@
#include "pw_preprocessor/compiler.h"
#include "pw_rpc/raw/client_reader_writer.h"
#include "pw_rpc/raw/server_reader_writer.h"
+#include "pw_span/span.h"
#include "pw_sync/binary_semaphore.h"
#include "pw_sync/timed_thread_notification.h"
#include "pw_thread/thread_core.h"
@@ -36,8 +36,8 @@ namespace internal {
class TransferThread : public thread::ThreadCore {
public:
- TransferThread(std::span<ClientContext> client_transfers,
- std::span<ServerContext> server_transfers,
+ TransferThread(span<ClientContext> client_transfers,
+ span<ServerContext> server_transfers,
ByteSpan chunk_buffer,
ByteSpan encode_buffer)
: client_transfers_(client_transfers),
@@ -46,37 +46,50 @@ class TransferThread : public thread::ThreadCore {
encode_buffer_(encode_buffer) {}
void StartClientTransfer(TransferType type,
- uint32_t transfer_id,
- uint32_t handler_id,
+ ProtocolVersion version,
+ uint32_t resource_id,
stream::Stream* stream,
const TransferParameters& max_parameters,
Function<void(Status)>&& on_completion,
chrono::SystemClock::duration timeout,
- uint8_t max_retries) {
+ uint8_t max_retries,
+ uint32_t max_lifetime_retries) {
+ uint32_t session_id = version == ProtocolVersion::kLegacy
+ ? resource_id
+ : Context::kUnassignedSessionId;
StartTransfer(type,
- transfer_id,
- handler_id,
+ version,
+ session_id,
+ resource_id,
+ /*raw_chunk=*/{},
stream,
max_parameters,
std::move(on_completion),
timeout,
- max_retries);
+ max_retries,
+ max_lifetime_retries);
}
void StartServerTransfer(TransferType type,
- uint32_t transfer_id,
- uint32_t handler_id,
+ ProtocolVersion version,
+ uint32_t session_id,
+ uint32_t resource_id,
+ ConstByteSpan raw_chunk,
const TransferParameters& max_parameters,
chrono::SystemClock::duration timeout,
- uint8_t max_retries) {
+ uint8_t max_retries,
+ uint32_t max_lifetime_retries) {
StartTransfer(type,
- transfer_id,
- handler_id,
+ version,
+ session_id,
+ resource_id,
+ raw_chunk,
/*stream=*/nullptr,
max_parameters,
/*on_completion=*/nullptr,
timeout,
- max_retries);
+ max_retries,
+ max_lifetime_retries);
}
void ProcessClientChunk(ConstByteSpan chunk) {
@@ -87,20 +100,37 @@ class TransferThread : public thread::ThreadCore {
ProcessChunk(EventType::kServerChunk, chunk);
}
+ void EndClientTransfer(uint32_t session_id,
+ Status status,
+ bool send_status_chunk = false) {
+ EndTransfer(
+ EventType::kClientEndTransfer, session_id, status, send_status_chunk);
+ }
+
+ void EndServerTransfer(uint32_t session_id,
+ Status status,
+ bool send_status_chunk = false) {
+ EndTransfer(
+ EventType::kServerEndTransfer, session_id, status, send_status_chunk);
+ }
+
+ // Move the read/write streams on this thread instead of the transfer thread.
+ // RPC call objects are synchronized by pw_rpc, so this move will be atomic
+ // with respect to the transfer thread.
void SetClientReadStream(rpc::RawClientReaderWriter& read_stream) {
- SetClientStream(TransferStream::kClientRead, read_stream);
+ client_read_stream_ = std::move(read_stream);
}
void SetClientWriteStream(rpc::RawClientReaderWriter& write_stream) {
- SetClientStream(TransferStream::kClientWrite, write_stream);
+ client_write_stream_ = std::move(write_stream);
}
void SetServerReadStream(rpc::RawServerReaderWriter& read_stream) {
- SetServerStream(TransferStream::kServerRead, read_stream);
+ server_read_stream_ = std::move(read_stream);
}
void SetServerWriteStream(rpc::RawServerReaderWriter& write_stream) {
- SetServerStream(TransferStream::kServerWrite, write_stream);
+ server_write_stream_ = std::move(write_stream);
}
void AddTransferHandler(Handler& handler) {
@@ -109,6 +139,9 @@ class TransferThread : public thread::ThreadCore {
void RemoveTransferHandler(Handler& handler) {
TransferHandlerEvent(EventType::kRemoveTransferHandler, handler);
+ // Ensure this function blocks until the transfer handler is fully cleaned
+ // up.
+ WaitUntilEventIsProcessed();
}
size_t max_chunk_size() const { return chunk_buffer_.size(); }
@@ -124,13 +157,13 @@ class TransferThread : public thread::ThreadCore {
}
// For testing only: simulates a timeout event for a client transfer.
- void SimulateClientTimeout(uint32_t transfer_id) {
- SimulateTimeout(EventType::kClientTimeout, transfer_id);
+ void SimulateClientTimeout(uint32_t session_id) {
+ SimulateTimeout(EventType::kClientTimeout, session_id);
}
// For testing only: simulates a timeout event for a server transfer.
- void SimulateServerTimeout(uint32_t transfer_id) {
- SimulateTimeout(EventType::kServerTimeout, transfer_id);
+ void SimulateServerTimeout(uint32_t session_id) {
+ SimulateTimeout(EventType::kServerTimeout, session_id);
}
private:
@@ -140,28 +173,39 @@ class TransferThread : public thread::ThreadCore {
static constexpr chrono::SystemClock::duration kMaxTimeout =
std::chrono::seconds(2);
- // Finds an active server or client transfer.
+ // Finds an active server or client transfer, matching against its legacy ID.
+ template <typename T>
+ static Context* FindActiveTransferByLegacyId(const span<T>& transfers,
+ uint32_t session_id) {
+ auto transfer =
+ std::find_if(transfers.begin(), transfers.end(), [session_id](auto& c) {
+ return c.initialized() && c.id() == session_id;
+ });
+ return transfer != transfers.end() ? &*transfer : nullptr;
+ }
+
+ // Finds an active server or client transfer, matching against resource ID.
template <typename T>
- static Context* FindActiveTransfer(const std::span<T>& transfers,
- uint32_t transfer_id) {
+ static Context* FindActiveTransferByResourceId(const span<T>& transfers,
+ uint32_t resource_id) {
auto transfer = std::find_if(
- transfers.begin(), transfers.end(), [transfer_id](auto& c) {
- return c.initialized() && c.transfer_id() == transfer_id;
+ transfers.begin(), transfers.end(), [resource_id](auto& c) {
+ return c.initialized() && c.resource_id() == resource_id;
});
return transfer != transfers.end() ? &*transfer : nullptr;
}
- void SimulateTimeout(EventType type, uint32_t transfer_id);
+ void SimulateTimeout(EventType type, uint32_t session_id);
// Finds an new server or client transfer.
template <typename T>
- static Context* FindNewTransfer(const std::span<T>& transfers,
- uint32_t transfer_id) {
+ static Context* FindNewTransfer(const span<T>& transfers,
+ uint32_t session_id) {
Context* new_transfer = nullptr;
for (Context& context : transfers) {
if (context.active()) {
- if (context.transfer_id() == transfer_id) {
+ if (context.session_id() == session_id) {
// Restart an already active transfer.
return &context;
}
@@ -201,18 +245,23 @@ class TransferThread : public thread::ThreadCore {
chrono::SystemClock::time_point GetNextTransferTimeout() const;
void StartTransfer(TransferType type,
- uint32_t transfer_id,
- uint32_t handler_id,
+ ProtocolVersion version,
+ uint32_t session_id,
+ uint32_t resource_id,
+ ConstByteSpan raw_chunk,
stream::Stream* stream,
const TransferParameters& max_parameters,
Function<void(Status)>&& on_completion,
chrono::SystemClock::duration timeout,
- uint8_t max_retries);
+ uint8_t max_retries,
+ uint32_t max_lifetime_retries);
void ProcessChunk(EventType type, ConstByteSpan chunk);
- void SetClientStream(TransferStream type, rpc::RawClientReaderWriter& stream);
- void SetServerStream(TransferStream type, rpc::RawServerReaderWriter& stream);
+ void EndTransfer(EventType type,
+ uint32_t session_id,
+ Status status,
+ bool send_status_chunk);
void TransferHandlerEvent(EventType type, Handler& handler);
@@ -226,16 +275,14 @@ class TransferThread : public thread::ThreadCore {
Event next_event_;
Function<void(Status)> staged_on_completion_;
- rpc::RawClientReaderWriter staged_client_stream_;
- rpc::RawServerReaderWriter staged_server_stream_;
rpc::RawClientReaderWriter client_read_stream_;
rpc::RawClientReaderWriter client_write_stream_;
rpc::RawServerReaderWriter server_read_stream_;
rpc::RawServerReaderWriter server_write_stream_;
- std::span<ClientContext> client_transfers_;
- std::span<ServerContext> server_transfers_;
+ span<ClientContext> client_transfers_;
+ span<ServerContext> server_transfers_;
// All registered transfer handlers.
IntrusiveList<Handler> handlers_;
diff --git a/pw_transfer/pw_transfer_private/chunk_testing.h b/pw_transfer/pw_transfer_private/chunk_testing.h
index b94d0d4ac..7347f6b8c 100644
--- a/pw_transfer/pw_transfer_private/chunk_testing.h
+++ b/pw_transfer/pw_transfer_private/chunk_testing.h
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -21,16 +21,16 @@ namespace pw::transfer::test {
Vector<std::byte, 64> EncodeChunk(const internal::Chunk& chunk) {
Vector<std::byte, 64> buffer(64);
- auto result = internal::EncodeChunk(chunk, buffer);
+ auto result = chunk.Encode(buffer);
EXPECT_EQ(result.status(), OkStatus());
buffer.resize(result.value().size());
return buffer;
}
internal::Chunk DecodeChunk(ConstByteSpan buffer) {
- internal::Chunk chunk = {};
- EXPECT_EQ(internal::DecodeChunk(buffer, chunk), OkStatus());
- return chunk;
+ auto result = internal::Chunk::Parse(buffer);
+ EXPECT_EQ(result.status(), OkStatus());
+ return *result;
}
} // namespace pw::transfer::test
diff --git a/pw_transfer/pw_transfer_private/filename_generator.h b/pw_transfer/pw_transfer_private/filename_generator.h
new file mode 100644
index 000000000..5dee568a8
--- /dev/null
+++ b/pw_transfer/pw_transfer_private/filename_generator.h
@@ -0,0 +1,32 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include <filesystem>
+#include <string>
+#include <string_view>
+
+namespace pw::transfer {
+
+// Create tmp file under the same folder as the file under file_path with file
+// name = actual filename +
+// ".tmp".
+inline std::string GetTempFilePath(std::string_view file_path) {
+ const auto path_file_system = std::filesystem::path{file_path};
+ return path_file_system.parent_path() /
+ (path_file_system.filename().string() + ".tmp");
+}
+
+} // namespace pw::transfer
diff --git a/pw_protobuf_compiler/ts/codegen/BUILD.bazel b/pw_transfer/py/BUILD.bazel
index f1959cfcf..4c0531d0f 100644
--- a/pw_protobuf_compiler/ts/codegen/BUILD.bazel
+++ b/pw_transfer/py/BUILD.bazel
@@ -12,29 +12,36 @@
# License for the specific language governing permissions and limitations under
# the License.
-load("@npm//@bazel/typescript:index.bzl", "ts_library")
-load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary")
-
package(default_visibility = ["//visibility:public"])
-ts_library(
- name = "template_replacement_lib",
+licenses(["notice"])
+
+py_library(
+ name = "pw_transfer",
srcs = [
- "template_replacement.ts",
+ "pw_transfer/__init__.py",
+ "pw_transfer/chunk.py",
+ "pw_transfer/client.py",
+ "pw_transfer/transfer.py",
],
+ imports = ["."],
deps = [
- "@//pw_rpc/ts:packet_proto_tspb",
- "@npm//@types/argparse",
- "@npm//@types/google-protobuf",
- "@npm//@types/node",
- "@npm//argparse",
+ "//pw_rpc/py:pw_rpc",
+ "//pw_status/py:pw_status",
+ "//pw_transfer:transfer_proto_pb2",
],
)
-nodejs_binary(
- name = "template_replacement_bin",
- data = [
- ":template_replacement_lib",
+py_test(
+ name = "transfer_test",
+ size = "small",
+ srcs = [
+ "tests/transfer_test.py",
+ ],
+ deps = [
+ ":pw_transfer",
+ "//pw_rpc/py:pw_rpc",
+ "//pw_status/py:pw_status",
+ "//pw_transfer:transfer_proto_pb2",
],
- entry_point = "template_replacement.ts",
)
diff --git a/pw_transfer/py/BUILD.gn b/pw_transfer/py/BUILD.gn
index f21ec38fa..7609f79c8 100644
--- a/pw_transfer/py/BUILD.gn
+++ b/pw_transfer/py/BUILD.gn
@@ -22,11 +22,12 @@ pw_python_package("py") {
generate_setup = {
metadata = {
name = "pw_transfer"
- version = "0.0.1"
+ version = "0.1.0"
}
}
sources = [
"pw_transfer/__init__.py",
+ "pw_transfer/chunk.py",
"pw_transfer/client.py",
"pw_transfer/transfer.py",
]
@@ -37,27 +38,6 @@ pw_python_package("py") {
]
python_test_deps = [ "$dir_pw_build/py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
proto_library = "..:proto"
}
-
-pw_python_script("python_cpp_transfer_test") {
- sources = [ "tests/python_cpp_transfer_test.py" ]
- python_deps = [
- ":py",
- "$dir_pw_hdlc/py",
- "$dir_pw_rpc/py",
- "$dir_pw_status/py",
- "..:test_server_proto.python",
- ]
- pylintrc = "$dir_pigweed/.pylintrc"
-
- action = {
- args = [
- "--port=$pw_transfer_PYTHON_CPP_TRANSFER_TEST_PORT",
- "--test-server-command",
- "<TARGET_FILE(..:test_rpc_server)>",
- ]
- deps = [ "..:test_rpc_server" ]
- stamp = true
- }
-}
diff --git a/pw_transfer/py/pw_transfer/__init__.py b/pw_transfer/py/pw_transfer/__init__.py
index 1f54b3a01..55a0b61d6 100644
--- a/pw_transfer/py/pw_transfer/__init__.py
+++ b/pw_transfer/py/pw_transfer/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -13,5 +13,9 @@
# the License.
"""Provides a simple interface for transferring bulk data over pw_rpc."""
-from pw_transfer.transfer import ProgressCallback, ProgressStats
+from pw_transfer.transfer import (
+ ProgressCallback,
+ ProgressStats,
+ ProtocolVersion,
+)
from pw_transfer.client import Error, Manager
diff --git a/pw_transfer/py/pw_transfer/chunk.py b/pw_transfer/py/pw_transfer/chunk.py
new file mode 100644
index 000000000..0bb0c485a
--- /dev/null
+++ b/pw_transfer/py/pw_transfer/chunk.py
@@ -0,0 +1,236 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Protocol version-aware chunk message wrapper."""
+
+import enum
+from typing import Any, Optional
+
+from pw_status import Status
+
+try:
+ from pw_transfer import transfer_pb2
+except ImportError:
+ # For the bazel build, which puts generated protos in a different location.
+ from pigweed.pw_transfer import transfer_pb2 # type: ignore
+
+
+class ProtocolVersion(enum.Enum):
+ """Supported versions of pw_transfer's RPC data transfer protocol."""
+
+ # Protocol version not known or not set.
+ UNKNOWN = 0
+
+ # The original transfer protocol, prior to transfer start/end handshakes.
+ LEGACY = 1
+
+ # Second version of the transfer protocol. Guarantees type fields on all
+ # chunks, deprecates pending_bytes in favor of window_end_offset, splits
+ # transfer resource IDs from ephemeral session IDs, and adds a handshake
+ # to the start and end of all transfer sessions.
+ VERSION_TWO = 2
+
+ # Alias to the most up-to-date version of the transfer protocol.
+ LATEST = VERSION_TWO
+
+
+_ChunkType = transfer_pb2.Chunk.Type
+
+
+class Chunk:
+ """A chunk exchanged in a pw_transfer stream.
+
+ Wraps the generated protobuf Chunk class with protocol-aware field encoding
+ and decoding.
+ """
+
+ Type = transfer_pb2.Chunk.Type
+
+ # TODO(frolv): Figure out how to make the chunk type annotation work.
+ # pylint: disable=too-many-arguments
+ def __init__(
+ self,
+ protocol_version: ProtocolVersion,
+ chunk_type: Any,
+ session_id: int = 0,
+ resource_id: Optional[int] = None,
+ offset: int = 0,
+ window_end_offset: int = 0,
+ data: bytes = b'',
+ remaining_bytes: Optional[int] = None,
+ max_chunk_size_bytes: Optional[int] = None,
+ min_delay_microseconds: Optional[int] = None,
+ status: Optional[Status] = None,
+ ):
+ self.protocol_version = protocol_version
+ self.type = chunk_type
+ self.session_id = session_id
+ self.resource_id = resource_id
+ self.offset = offset
+ self.window_end_offset = window_end_offset
+ self.data = data
+ self.remaining_bytes = remaining_bytes
+ self.max_chunk_size_bytes = max_chunk_size_bytes
+ self.min_delay_microseconds = min_delay_microseconds
+ self.status = status
+
+ @classmethod
+ def from_message(cls, message: transfer_pb2.Chunk) -> 'Chunk':
+ """Parses a Chunk from a protobuf message."""
+
+ # Some very old versions of transfer don't always encode chunk types,
+ # so they must be deduced.
+ #
+ # The type-less legacy transfer protocol doesn't support handshakes or
+ # continuation parameters. Therefore, there are only three possible
+ # types: start, data, and retransmit.
+ if message.HasField('type'):
+ chunk_type = message.type
+ elif (
+ message.offset == 0
+ and not message.data
+ and not message.HasField('status')
+ ):
+ chunk_type = Chunk.Type.START
+ elif message.data:
+ chunk_type = Chunk.Type.DATA
+ else:
+ chunk_type = Chunk.Type.PARAMETERS_RETRANSMIT
+
+ chunk = cls(
+ ProtocolVersion.UNKNOWN,
+ chunk_type,
+ offset=message.offset,
+ window_end_offset=message.window_end_offset,
+ data=message.data,
+ )
+
+ if message.HasField('session_id'):
+ chunk.protocol_version = ProtocolVersion.VERSION_TWO
+ chunk.session_id = message.session_id
+ else:
+ chunk.protocol_version = ProtocolVersion.LEGACY
+ chunk.session_id = message.transfer_id
+
+ if message.HasField('resource_id'):
+ chunk.resource_id = message.resource_id
+
+ if message.HasField('protocol_version'):
+ # An explicitly specified protocol version overrides any inferred
+ # one.
+ chunk.protocol_version = ProtocolVersion(message.protocol_version)
+
+ if message.HasField('pending_bytes'):
+ chunk.window_end_offset = message.offset + message.pending_bytes
+
+ if message.HasField('remaining_bytes'):
+ chunk.remaining_bytes = message.remaining_bytes
+
+ if message.HasField('max_chunk_size_bytes'):
+ chunk.max_chunk_size_bytes = message.max_chunk_size_bytes
+
+ if message.HasField('min_delay_microseconds'):
+ chunk.min_delay_microseconds = message.min_delay_microseconds
+
+ if message.HasField('status'):
+ chunk.status = Status(message.status)
+
+ if chunk.protocol_version is ProtocolVersion.UNKNOWN:
+ # If no fields in the chunk specified its protocol version,
+ # assume it is a legacy chunk.
+ chunk.protocol_version = ProtocolVersion.LEGACY
+
+ return chunk
+
+ def to_message(self) -> transfer_pb2.Chunk:
+ """Converts the chunk to a protobuf message."""
+ message = transfer_pb2.Chunk(
+ offset=self.offset,
+ window_end_offset=self.window_end_offset,
+ type=self.type,
+ )
+
+ if self.resource_id is not None:
+ message.resource_id = self.resource_id
+
+ if self.protocol_version is ProtocolVersion.VERSION_TWO:
+ if self.session_id != 0:
+ message.session_id = self.session_id
+
+ if self._should_encode_legacy_fields():
+ if self.resource_id is not None:
+ message.transfer_id = self.resource_id
+ else:
+ assert self.session_id != 0
+ message.transfer_id = self.session_id
+
+ # In the legacy protocol, the pending_bytes field must be set
+ # alongside window_end_offset, as some transfer implementations
+ # require it.
+ if self.window_end_offset != 0:
+ message.pending_bytes = self.window_end_offset - self.offset
+
+ if self.data:
+ message.data = self.data
+
+ if self.remaining_bytes is not None:
+ message.remaining_bytes = self.remaining_bytes
+
+ if self.max_chunk_size_bytes is not None:
+ message.max_chunk_size_bytes = self.max_chunk_size_bytes
+
+ if self.min_delay_microseconds is not None:
+ message.min_delay_microseconds = self.min_delay_microseconds
+
+ if self.status is not None:
+ message.status = self.status.value
+
+ if self._is_initial_handshake_chunk():
+ # During the initial handshake, the desired protocol version is
+ # explictly encoded.
+ message.protocol_version = self.protocol_version.value
+
+ return message
+
+ def id(self) -> int:
+ """Returns the transfer context identifier for a chunk.
+
+ Depending on the protocol version and type of chunk, this may correspond
+ to one of several proto fields.
+ """
+ if self.resource_id is not None:
+ # Always prioritize a resource_id over a session_id.
+ return self.resource_id
+
+ return self.session_id
+
+ def requests_transmission_from_offset(self) -> bool:
+ """Returns True if this chunk is requesting a retransmission."""
+ return (
+ self.type is Chunk.Type.PARAMETERS_RETRANSMIT
+ or self.type is Chunk.Type.START
+ or self.type is Chunk.Type.START_ACK_CONFIRMATION
+ )
+
+ def _is_initial_handshake_chunk(self) -> bool:
+ return self.protocol_version is ProtocolVersion.VERSION_TWO and (
+ self.type is Chunk.Type.START
+ or self.type is Chunk.Type.START_ACK
+ or self.type is Chunk.Type.START_ACK_CONFIRMATION
+ )
+
+ def _should_encode_legacy_fields(self) -> bool:
+ return (
+ self.protocol_version is ProtocolVersion.LEGACY
+ or self.type is Chunk.Type.START
+ )
diff --git a/pw_transfer/py/pw_transfer/client.py b/pw_transfer/py/pw_transfer/client.py
index b97f2cef1..85bfc4575 100644
--- a/pw_transfer/py/pw_transfer/client.py
+++ b/pw_transfer/py/pw_transfer/client.py
@@ -1,4 +1,4 @@
-# Copyright 2021 The Pigweed Authors
+# Copyright 2022 The Pigweed Authors
#
# 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
@@ -21,9 +21,20 @@ from typing import Any, Dict, Optional, Union
from pw_rpc.callback_client import BidirectionalStreamingCall
from pw_status import Status
-from pw_transfer.transfer import (ProgressCallback, ReadTransfer, Transfer,
- WriteTransfer)
-from pw_transfer.transfer_pb2 import Chunk
+from pw_transfer.transfer import (
+ ProgressCallback,
+ ProtocolVersion,
+ ReadTransfer,
+ Transfer,
+ WriteTransfer,
+)
+from pw_transfer.chunk import Chunk
+
+try:
+ from pw_transfer import transfer_pb2
+except ImportError:
+ # For the bazel build, which puts generated protos in a different location.
+ from pigweed.pw_transfer import transfer_pb2 # type: ignore
_LOG = logging.getLogger(__package__)
@@ -40,12 +51,17 @@ class Manager: # pylint: disable=too-many-instance-attributes
When created, a Manager starts a separate thread in which transfer
communications and events are handled.
"""
- def __init__(self,
- rpc_transfer_service,
- *,
- default_response_timeout_s: float = 2.0,
- initial_response_timeout_s: float = 4.0,
- max_retries: int = 3):
+
+ def __init__(
+ self,
+ rpc_transfer_service,
+ *,
+ default_response_timeout_s: float = 2.0,
+ initial_response_timeout_s: float = 4.0,
+ max_retries: int = 3,
+ max_lifetime_retries: int = 1500,
+ default_protocol_version=ProtocolVersion.LATEST,
+ ):
"""Initializes a Manager on top of a TransferService.
Args:
@@ -53,14 +69,18 @@ class Manager: # pylint: disable=too-many-instance-attributes
default_response_timeout_s: max time to wait between receiving packets
initial_response_timeout_s: timeout for the first packet; may be
longer to account for transfer handler initialization
- max_retires: number of times to retry after a timeout
+ max_retires: number of times to retry a single package after a timeout
+ max_lifetime_retires: Cumulative maximum number of times to retry over
+ the course of the transfer before giving up.
"""
self._service: Any = rpc_transfer_service
self._default_response_timeout_s = default_response_timeout_s
self._initial_response_timeout_s = initial_response_timeout_s
self.max_retries = max_retries
+ self.max_lifetime_retries = max_lifetime_retries
+ self._default_protocol_version = default_protocol_version
- # Ongoing transfers in the service by ID.
+ # Ongoing transfers in the service by resource ID.
self._read_transfers: _TransferDict = {}
self._write_transfers: _TransferDict = {}
@@ -70,6 +90,8 @@ class Manager: # pylint: disable=too-many-instance-attributes
self._write_stream: Optional[BidirectionalStreamingCall] = None
self._loop = asyncio.new_event_loop()
+ # Set the event loop for the current thread.
+ asyncio.set_event_loop(self._loop)
# Queues are used for communication between the Manager context and the
# dedicated asyncio transfer thread.
@@ -78,8 +100,9 @@ class Manager: # pylint: disable=too-many-instance-attributes
self._write_chunk_queue: asyncio.Queue = asyncio.Queue()
self._quit_event = asyncio.Event()
- self._thread = threading.Thread(target=self._start_event_loop_thread,
- daemon=True)
+ self._thread = threading.Thread(
+ target=self._start_event_loop_thread, daemon=True
+ )
self._thread.start()
@@ -90,42 +113,63 @@ class Manager: # pylint: disable=too-many-instance-attributes
self._loop.call_soon_threadsafe(self._quit_event.set)
self._thread.join()
- def read(self,
- transfer_id: int,
- progress_callback: ProgressCallback = None) -> bytes:
+ def read(
+ self,
+ resource_id: int,
+ progress_callback: Optional[ProgressCallback] = None,
+ protocol_version: Optional[ProtocolVersion] = None,
+ ) -> bytes:
"""Receives ("downloads") data from the server.
+ Args:
+ resource_id: ID of the resource from which to read.
+ progress_callback: Optional callback periodically invoked throughout
+ the transfer with the transfer state. Can be used to provide user-
+ facing status updates such as progress bars.
+
Raises:
Error: the transfer failed to complete
"""
- if transfer_id in self._read_transfers:
- raise ValueError(f'Read transfer {transfer_id} already exists')
-
- transfer = ReadTransfer(transfer_id,
- self._send_read_chunk,
- self._end_read_transfer,
- self._default_response_timeout_s,
- self._initial_response_timeout_s,
- self.max_retries,
- progress_callback=progress_callback)
+ if resource_id in self._read_transfers:
+ raise ValueError(
+ f'Read transfer for resource {resource_id} already exists'
+ )
+
+ if protocol_version is None:
+ protocol_version = self._default_protocol_version
+
+ transfer = ReadTransfer(
+ resource_id,
+ self._send_read_chunk,
+ self._end_read_transfer,
+ self._default_response_timeout_s,
+ self._initial_response_timeout_s,
+ self.max_retries,
+ self.max_lifetime_retries,
+ protocol_version,
+ progress_callback=progress_callback,
+ )
self._start_read_transfer(transfer)
transfer.done.wait()
if not transfer.status.ok():
- raise Error(transfer.id, transfer.status)
+ raise Error(transfer.resource_id, transfer.status)
return transfer.data
- def write(self,
- transfer_id: int,
- data: Union[bytes, str],
- progress_callback: ProgressCallback = None) -> None:
+ def write(
+ self,
+ resource_id: int,
+ data: Union[bytes, str],
+ progress_callback: Optional[ProgressCallback] = None,
+ protocol_version: Optional[ProtocolVersion] = None,
+ ) -> None:
"""Transmits ("uploads") data to the server.
Args:
- transfer_id: ID of the write transfer
+ resource_id: ID of the resource to which to write.
data: Data to send to the server.
progress_callback: Optional callback periodically invoked throughout
the transfer with the transfer state. Can be used to provide user-
@@ -138,31 +182,40 @@ class Manager: # pylint: disable=too-many-instance-attributes
if isinstance(data, str):
data = data.encode()
- if transfer_id in self._write_transfers:
- raise ValueError(f'Write transfer {transfer_id} already exists')
-
- transfer = WriteTransfer(transfer_id,
- data,
- self._send_write_chunk,
- self._end_write_transfer,
- self._default_response_timeout_s,
- self._initial_response_timeout_s,
- self.max_retries,
- progress_callback=progress_callback)
+ if resource_id in self._write_transfers:
+ raise ValueError(
+ f'Write transfer for resource {resource_id} already exists'
+ )
+
+ if protocol_version is None:
+ protocol_version = self._default_protocol_version
+
+ transfer = WriteTransfer(
+ resource_id,
+ data,
+ self._send_write_chunk,
+ self._end_write_transfer,
+ self._default_response_timeout_s,
+ self._initial_response_timeout_s,
+ self.max_retries,
+ self.max_lifetime_retries,
+ protocol_version,
+ progress_callback=progress_callback,
+ )
self._start_write_transfer(transfer)
transfer.done.wait()
if not transfer.status.ok():
- raise Error(transfer.id, transfer.status)
+ raise Error(transfer.resource_id, transfer.status)
def _send_read_chunk(self, chunk: Chunk) -> None:
assert self._read_stream is not None
- self._read_stream.send(chunk)
+ self._read_stream.send(chunk.to_message())
def _send_write_chunk(self, chunk: Chunk) -> None:
assert self._write_stream is not None
- self._write_stream.send(chunk)
+ self._write_stream.send(chunk.to_message())
def _start_event_loop_thread(self):
"""Entry point for event loop thread that starts an asyncio context."""
@@ -189,7 +242,8 @@ class Manager: # pylint: disable=too-many-instance-attributes
# Perform a select(2)-like wait for one of several events to occur.
done, _ = await asyncio.wait(
(exit_thread, new_transfer, read_chunk, write_chunk),
- return_when=asyncio.FIRST_COMPLETED)
+ return_when=asyncio.FIRST_COMPLETED,
+ )
if exit_thread in done:
break
@@ -197,26 +251,35 @@ class Manager: # pylint: disable=too-many-instance-attributes
if new_transfer in done:
await new_transfer.result().begin()
new_transfer = self._loop.create_task(
- self._new_transfer_queue.get())
+ self._new_transfer_queue.get()
+ )
if read_chunk in done:
self._loop.create_task(
- self._handle_chunk(self._read_transfers,
- read_chunk.result()))
+ self._handle_chunk(
+ self._read_transfers, read_chunk.result()
+ )
+ )
read_chunk = self._loop.create_task(
- self._read_chunk_queue.get())
+ self._read_chunk_queue.get()
+ )
if write_chunk in done:
self._loop.create_task(
- self._handle_chunk(self._write_transfers,
- write_chunk.result()))
+ self._handle_chunk(
+ self._write_transfers, write_chunk.result()
+ )
+ )
write_chunk = self._loop.create_task(
- self._write_chunk_queue.get())
+ self._write_chunk_queue.get()
+ )
self._loop.stop()
@staticmethod
- async def _handle_chunk(transfers: _TransferDict, chunk: Chunk) -> None:
+ async def _handle_chunk(
+ transfers: _TransferDict, message: transfer_pb2.Chunk
+ ) -> None:
"""Processes an incoming chunk from a stream.
The chunk is dispatched to an active transfer based on its ID. If the
@@ -224,12 +287,23 @@ class Manager: # pylint: disable=too-many-instance-attributes
is invoked.
"""
+ chunk = Chunk.from_message(message)
+
+ # Find a transfer for the chunk in the list of active transfers.
try:
- transfer = transfers[chunk.transfer_id]
- except KeyError:
+ if chunk.resource_id is not None:
+ # Prioritize a resource_id if one is set.
+ transfer = transfers[chunk.resource_id]
+ else:
+ # Otherwise, match against either resource or session ID.
+ transfer = next(
+ t for t in transfers.values() if t.id == chunk.id()
+ )
+ except (KeyError, StopIteration):
_LOG.error(
'TransferManager received chunk for unknown transfer %d',
- chunk.transfer_id)
+ chunk.id(),
+ )
# TODO(frolv): What should be done here, if anything?
return
@@ -238,8 +312,10 @@ class Manager: # pylint: disable=too-many-instance-attributes
def _open_read_stream(self) -> None:
self._read_stream = self._service.Read.invoke(
lambda _, chunk: self._loop.call_soon_threadsafe(
- self._read_chunk_queue.put_nowait, chunk),
- on_error=lambda _, status: self._on_read_error(status))
+ self._read_chunk_queue.put_nowait, chunk
+ ),
+ on_error=lambda _, status: self._on_read_error(status),
+ )
def _on_read_error(self, status: Status) -> None:
"""Callback for an RPC error in the read stream."""
@@ -265,8 +341,10 @@ class Manager: # pylint: disable=too-many-instance-attributes
def _open_write_stream(self) -> None:
self._write_stream = self._service.Write.invoke(
lambda _, chunk: self._loop.call_soon_threadsafe(
- self._write_chunk_queue.put_nowait, chunk),
- on_error=lambda _, status: self._on_write_error(status))
+ self._write_chunk_queue.put_nowait, chunk
+ ),
+ on_error=lambda _, status: self._on_write_error(status),
+ )
def _on_write_error(self, status: Status) -> None:
"""Callback for an RPC error in the write stream."""
@@ -292,22 +370,26 @@ class Manager: # pylint: disable=too-many-instance-attributes
def _start_read_transfer(self, transfer: Transfer) -> None:
"""Begins a new read transfer, opening the stream if it isn't."""
- self._read_transfers[transfer.id] = transfer
+ self._read_transfers[transfer.resource_id] = transfer
if not self._read_stream:
self._open_read_stream()
_LOG.debug('Starting new read transfer %d', transfer.id)
- self._loop.call_soon_threadsafe(self._new_transfer_queue.put_nowait,
- transfer)
+ self._loop.call_soon_threadsafe(
+ self._new_transfer_queue.put_nowait, transfer
+ )
def _end_read_transfer(self, transfer: Transfer) -> None:
"""Completes a read transfer."""
- del self._read_transfers[transfer.id]
+ del self._read_transfers[transfer.resource_id]
if not transfer.status.ok():
- _LOG.error('Read transfer %d terminated with status %s',
- transfer.id, transfer.status)
+ _LOG.error(
+ 'Read transfer %d terminated with status %s',
+ transfer.id,
+ transfer.status,
+ )
# TODO(frolv): This doesn't seem to work. Investigate why.
# If no more transfers are using the read stream, close it.
@@ -318,22 +400,26 @@ class Manager: # pylint: disable=too-many-instance-attributes
def _start_write_transfer(self, transfer: Transfer) -> None:
"""Begins a new write transfer, opening the stream if it isn't."""
- self._write_transfers[transfer.id] = transfer
+ self._write_transfers[transfer.resource_id] = transfer
if not self._write_stream:
self._open_write_stream()
_LOG.debug('Starting new write transfer %d', transfer.id)
- self._loop.call_soon_threadsafe(self._new_transfer_queue.put_nowait,
- transfer)
+ self._loop.call_soon_threadsafe(
+ self._new_transfer_queue.put_nowait, transfer
+ )
def _end_write_transfer(self, transfer: Transfer) -> None:
"""Completes a write transfer."""
- del self._write_transfers[transfer.id]
+ del self._write_transfers[transfer.resource_id]
if not transfer.status.ok():
- _LOG.error('Write transfer %d terminated with status %s',
- transfer.id, transfer.status)
+ _LOG.error(
+ 'Write transfer %d terminated with status %s',
+ transfer.id,
+ transfer.status,
+ )
# TODO(frolv): This doesn't seem to work. Investigate why.
# If no more transfers are using the write stream, close it.
@@ -345,9 +431,10 @@ class Manager: # pylint: disable=too-many-instance-attributes
class Error(Exception):
"""Exception raised when a transfer fails.
- Stores the ID of the failed transfer and the error that occurred.
+ Stores the ID of the failed transfer resource and the error that occurred.
"""
- def __init__(self, transfer_id: int, status: Status):
- super().__init__(f'Transfer {transfer_id} failed with status {status}')
- self.transfer_id = transfer_id
+
+ def __init__(self, resource_id: int, status: Status):
+ super().__init__(f'Transfer {resource_id} failed with status {status}')
+ self.resource_id = resource_id
self.status = status
diff --git a/pw_transfer/py/pw_transfer/transfer.py b/pw_transfer/py/pw_transfer/transfer.py
index 06574f4e7..11dc55636 100644
--- a/pw_transfer/py/pw_transfer/transfer.py
+++ b/pw_transfer/py/pw_transfer/transfer.py
@@ -16,13 +16,14 @@
import abc
import asyncio
from dataclasses import dataclass
+import enum
import logging
import math
import threading
from typing import Any, Callable, Optional
from pw_status import Status
-from pw_transfer.transfer_pb2 import Chunk
+from pw_transfer.chunk import Chunk, ProtocolVersion
_LOG = logging.getLogger(__package__)
@@ -40,10 +41,13 @@ class ProgressStats:
return self.bytes_confirmed_received / self.total_size_bytes * 100
def __str__(self) -> str:
- total = str(
- self.total_size_bytes) if self.total_size_bytes else 'unknown'
- return (f'{self.percent_received():5.1f}% ({self.bytes_sent} B sent, '
- f'{self.bytes_confirmed_received} B received of {total} B)')
+ total = (
+ str(self.total_size_bytes) if self.total_size_bytes else 'unknown'
+ )
+ return (
+ f'{self.percent_received():5.1f}% ({self.bytes_sent} B sent, '
+ f'{self.bytes_confirmed_received} B received of {total} B)'
+ )
ProgressCallback = Callable[[ProgressStats], Any]
@@ -51,12 +55,13 @@ ProgressCallback = Callable[[ProgressStats], Any]
class _Timer:
"""A timer which invokes a callback after a certain timeout."""
+
def __init__(self, timeout_s: float, callback: Callable[[], Any]):
self.timeout_s = timeout_s
self._callback = callback
self._task: Optional[asyncio.Task[Any]] = None
- def start(self, timeout_s: float = None) -> None:
+ def start(self, timeout_s: Optional[float] = None) -> None:
"""Starts a new timer.
If a timer is already running, it is stopped and a new timer started.
@@ -86,23 +91,76 @@ class Transfer(abc.ABC):
of transfer, receiving messages from the server and sending the appropriate
messages in response.
"""
- def __init__(self,
- transfer_id: int,
- send_chunk: Callable[[Chunk], None],
- end_transfer: Callable[['Transfer'], None],
- response_timeout_s: float,
- initial_response_timeout_s: float,
- max_retries: int,
- progress_callback: ProgressCallback = None):
- self.id = transfer_id
+
+ # pylint: disable=too-many-instance-attributes
+
+ class _State(enum.Enum):
+ # Transfer is starting. The server and client are performing an initial
+ # handshake and negotiating protocol and feature flags.
+ INITIATING = 0
+
+ # Waiting for the other end to send a chunk.
+ WAITING = 1
+
+ # Transmitting a window of data to a receiver.
+ TRANSMITTING = 2
+
+ # Recovering after one or more chunks was dropped in an active transfer.
+ RECOVERY = 3
+
+ # Transfer has completed locally and is waiting for the peer to
+ # acknowledge its final status. Only entered by the terminating side of
+ # the transfer.
+ #
+ # The context remains in a TERMINATING state until it receives an
+ # acknowledgement from the peer or times out.
+ TERMINATING = 4
+
+ # A transfer has fully completed.
+ COMPLETE = 5
+
+ _UNASSIGNED_SESSION_ID = 0
+
+ def __init__( # pylint: disable=too-many-arguments
+ self,
+ resource_id: int,
+ send_chunk: Callable[[Chunk], None],
+ end_transfer: Callable[['Transfer'], None],
+ response_timeout_s: float,
+ initial_response_timeout_s: float,
+ max_retries: int,
+ max_lifetime_retries: int,
+ protocol_version: ProtocolVersion,
+ progress_callback: Optional[ProgressCallback] = None,
+ ):
self.status = Status.OK
self.done = threading.Event()
- self._send_chunk = send_chunk
+ self._session_id = self._UNASSIGNED_SESSION_ID
+ self._resource_id = resource_id
+
+ self._send_chunk_fn = send_chunk
self._end_transfer = end_transfer
+ self._desired_protocol_version = protocol_version
+ self._configured_protocol_version = ProtocolVersion.UNKNOWN
+
+ if self._desired_protocol_version is ProtocolVersion.LEGACY:
+ # In a legacy transfer, there is no protocol negotiation stage.
+ # Automatically configure the context to run the legacy protocol and
+ # proceed to waiting for a chunk.
+ self._configured_protocol_version = ProtocolVersion.LEGACY
+ self._state = Transfer._State.WAITING
+ self._session_id = self._resource_id
+ else:
+ self._state = Transfer._State.INITIATING
+
+ self._last_chunk: Optional[Chunk] = None
+
self._retries = 0
self._max_retries = max_retries
+ self._lifetime_retries = 0
+ self._max_lifetime_retries = max_lifetime_retries
self._response_timer = _Timer(response_timeout_s, self._on_timeout)
self._initial_response_timeout_s = initial_response_timeout_s
@@ -110,17 +168,57 @@ class Transfer(abc.ABC):
async def begin(self) -> None:
"""Sends the initial chunk of the transfer."""
- self._send_chunk(self._initial_chunk())
+
+ if (
+ self._desired_protocol_version is ProtocolVersion.UNKNOWN
+ or self._desired_protocol_version.value
+ > ProtocolVersion.LATEST.value
+ ):
+ _LOG.error(
+ 'Cannot start a transfer with unsupported protocol version %d',
+ self._desired_protocol_version.value,
+ )
+ self.finish(Status.INVALID_ARGUMENT)
+ return
+
+ initial_chunk = Chunk(
+ self._desired_protocol_version,
+ Chunk.Type.START,
+ resource_id=self._resource_id,
+ )
+
+ # Regardless of the desired protocol version, set any additional fields
+ # on the opening chunk, in case the server only runs legacy.
+ self._set_initial_chunk_fields(initial_chunk)
+
+ self._send_chunk(initial_chunk)
self._response_timer.start(self._initial_response_timeout_s)
@property
+ def id(self) -> int:
+ """Returns the identifier for the active transfer."""
+ if self._session_id != self._UNASSIGNED_SESSION_ID:
+ return self._session_id
+ return self._resource_id
+
+ @property
+ def resource_id(self) -> int:
+ """Returns the identifier of the resource being transferred."""
+ return self._resource_id
+
+ @property
@abc.abstractmethod
def data(self) -> bytes:
"""Returns the data read or written in this transfer."""
@abc.abstractmethod
- def _initial_chunk(self) -> Chunk:
- """Returns the initial chunk to notify the sever of the transfer."""
+ def _set_initial_chunk_fields(self, chunk: Chunk) -> None:
+ """Sets fields for the initial non-handshake chunk of the transfer."""
+
+ def _send_chunk(self, chunk: Chunk) -> None:
+ """Sends a chunk to the server, keeping track of the last chunk sent."""
+ self._send_chunk_fn(chunk)
+ self._last_chunk = chunk
async def handle_chunk(self, chunk: Chunk) -> None:
"""Processes an incoming chunk from the server.
@@ -131,41 +229,144 @@ class Transfer(abc.ABC):
self._response_timer.stop()
self._retries = 0 # Received data from service, so reset the retries.
- _LOG.debug('Received chunk\n%s', str(chunk).rstrip())
+ _LOG.debug('Received chunk\n%s', str(chunk.to_message()).rstrip())
# Status chunks are only used to terminate a transfer. They do not
# contain any data that requires processing.
- if chunk.HasField('status'):
+ if chunk.status is not None:
+ if self._configured_protocol_version is ProtocolVersion.VERSION_TWO:
+ self._send_chunk(
+ Chunk(
+ self._configured_protocol_version,
+ Chunk.Type.COMPLETION_ACK,
+ session_id=self._session_id,
+ )
+ )
+
self.finish(Status(chunk.status))
return
- await self._handle_data_chunk(chunk)
+ if self._state is Transfer._State.INITIATING:
+ await self._perform_initial_handshake(chunk)
+ elif self._state is Transfer._State.TERMINATING:
+ if chunk.type is Chunk.Type.COMPLETION_ACK:
+ self.finish(self.status)
+ else:
+ # Expecting a completion ACK but didn't receive one. Go through
+ # the retry process.
+ self._on_timeout()
+ else:
+ await self._handle_data_chunk(chunk)
# Start the timeout for the server to send a chunk in response.
self._response_timer.start()
+ async def _perform_initial_handshake(self, chunk: Chunk) -> None:
+ """Progresses the initial handshake phase of a v2+ transfer."""
+ assert self._state is Transfer._State.INITIATING
+
+ # If a non-handshake chunk is received during an INITIATING state, the
+ # transfer server is running a legacy protocol version, which does not
+ # perform a handshake. End the handshake, revert to the legacy protocol,
+ # and process the chunk appropriately.
+ if chunk.type is not Chunk.Type.START_ACK:
+ _LOG.debug(
+ 'Transfer %d got non-handshake chunk, reverting to legacy',
+ self.id,
+ )
+
+ self._configured_protocol_version = ProtocolVersion.LEGACY
+ self._state = Transfer._State.WAITING
+
+ # Update the transfer's session ID in case it was expecting one to
+ # be assigned by the server.
+ self._session_id = chunk.session_id
+
+ await self._handle_data_chunk(chunk)
+ return
+
+ self._session_id = chunk.session_id
+
+ self._configured_protocol_version = ProtocolVersion(
+ min(
+ self._desired_protocol_version.value,
+ chunk.protocol_version.value,
+ )
+ )
+ _LOG.debug(
+ 'Transfer %d negotiating protocol version: ours=%d, theirs=%d',
+ self.id,
+ self._desired_protocol_version.value,
+ chunk.protocol_version.value,
+ )
+
+ # Send a confirmation chunk to the server accepting the assigned session
+ # ID and protocol version. Tag any initial transfer parameters onto the
+ # chunk to begin the data transfer.
+ start_ack_confirmation = Chunk(
+ self._configured_protocol_version,
+ Chunk.Type.START_ACK_CONFIRMATION,
+ session_id=self._session_id,
+ )
+ self._set_initial_chunk_fields(start_ack_confirmation)
+
+ self._state = Transfer._State.WAITING
+ self._send_chunk(start_ack_confirmation)
+
@abc.abstractmethod
async def _handle_data_chunk(self, chunk: Chunk) -> None:
"""Handles a chunk that contains or requests data."""
@abc.abstractmethod
- def _retry_after_timeout(self) -> None:
- """Retries after a timeout occurs."""
+ def _retry_after_data_timeout(self) -> None:
+ """Retries after a timeout occurs during the data transfer phase.
+
+ Only invoked when in the data transfer phase (i.e. state is in
+ {WAITING, TRANSMITTING, RECOVERY}). Timeouts occurring during an
+ opening or closing handshake are handled by the base Transfer.
+ """
def _on_timeout(self) -> None:
"""Handles a timeout while waiting for a chunk."""
- if self.done.is_set():
+ if self._state is Transfer._State.COMPLETE:
return
self._retries += 1
- if self._retries > self._max_retries:
- self.finish(Status.DEADLINE_EXCEEDED)
+ self._lifetime_retries += 1
+
+ if (
+ self._retries > self._max_retries
+ or self._lifetime_retries > self._max_lifetime_retries
+ ):
+ if self._state is Transfer._State.TERMINATING:
+ # If the server never responded to the sent completion chunk,
+ # simply end the transfer locally with its original status.
+ self.finish(self.status)
+ else:
+ self.finish(Status.DEADLINE_EXCEEDED)
return
- _LOG.debug('Received no responses for %.3fs; retrying %d/%d',
- self._response_timer.timeout_s, self._retries,
- self._max_retries)
- self._retry_after_timeout()
+ _LOG.debug(
+ 'Received no responses for %.3fs; retrying %d/%d',
+ self._response_timer.timeout_s,
+ self._retries,
+ self._max_retries,
+ )
+
+ retry_handshake_chunk = self._state in (
+ Transfer._State.INITIATING,
+ Transfer._State.TERMINATING,
+ ) or (
+ self._last_chunk is not None
+ and self._last_chunk.type is Chunk.Type.START_ACK_CONFIRMATION
+ )
+
+ if retry_handshake_chunk:
+ assert self._last_chunk is not None
+ self._send_chunk(self._last_chunk)
+ else:
+ self._retry_after_data_timeout()
+
self._response_timer.start()
def finish(self, status: Status, skip_callback: bool = False) -> None:
@@ -181,49 +382,74 @@ class Transfer(abc.ABC):
self._end_transfer(self)
# Set done last so that the transfer has been fully cleaned up.
+ self._state = Transfer._State.COMPLETE
self.done.set()
- def _update_progress(self, bytes_sent: int, bytes_confirmed_received: int,
- total_size_bytes: Optional[int]) -> None:
+ def _update_progress(
+ self,
+ bytes_sent: int,
+ bytes_confirmed_received: int,
+ total_size_bytes: Optional[int],
+ ) -> None:
"""Invokes the provided progress callback, if any, with the progress."""
- stats = ProgressStats(bytes_sent, bytes_confirmed_received,
- total_size_bytes)
+ stats = ProgressStats(
+ bytes_sent, bytes_confirmed_received, total_size_bytes
+ )
_LOG.debug('Transfer %d progress: %s', self.id, stats)
if self._progress_callback:
self._progress_callback(stats)
- def _send_error(self, error: Status) -> None:
- """Sends an error chunk to the server and finishes the transfer."""
+ def _send_final_chunk(self, status: Status) -> None:
+ """Sends a status chunk to the server and finishes the transfer."""
self._send_chunk(
- Chunk(transfer_id=self.id,
- status=error.value,
- type=Chunk.Type.TRANSFER_COMPLETION))
- self.finish(error)
+ Chunk(
+ self._configured_protocol_version,
+ Chunk.Type.COMPLETION,
+ session_id=self.id,
+ status=status,
+ )
+ )
+
+ if self._configured_protocol_version is ProtocolVersion.VERSION_TWO:
+ # Wait for a completion ACK from the server.
+ self.status = status
+ self._state = Transfer._State.TERMINATING
+ self._response_timer.start()
+ else:
+ self.finish(status)
class WriteTransfer(Transfer):
"""A client -> server write transfer."""
- def __init__(
+
+ def __init__( # pylint: disable=too-many-arguments
self,
- transfer_id: int,
+ resource_id: int,
data: bytes,
send_chunk: Callable[[Chunk], None],
end_transfer: Callable[[Transfer], None],
response_timeout_s: float,
initial_response_timeout_s: float,
max_retries: int,
- progress_callback: ProgressCallback = None,
+ max_lifetime_retries: int,
+ protocol_version: ProtocolVersion,
+ progress_callback: Optional[ProgressCallback] = None,
):
- super().__init__(transfer_id, send_chunk, end_transfer,
- response_timeout_s, initial_response_timeout_s,
- max_retries, progress_callback)
+ super().__init__(
+ resource_id,
+ send_chunk,
+ end_transfer,
+ response_timeout_s,
+ initial_response_timeout_s,
+ max_retries,
+ max_lifetime_retries,
+ protocol_version,
+ progress_callback,
+ )
self._data = data
- # Guard this class with a lock since a transfer parameters update might
- # arrive while responding to a prior update.
- self._lock = asyncio.Lock()
self._offset = 0
self._window_end_offset = 0
self._max_chunk_size = 0
@@ -232,14 +458,15 @@ class WriteTransfer(Transfer):
# The window ID increments for each parameters update.
self._window_id = 0
- self._last_chunk = self._initial_chunk()
+ self._bytes_confirmed_received = 0
@property
def data(self) -> bytes:
return self._data
- def _initial_chunk(self) -> Chunk:
- return Chunk(transfer_id=self.id, type=Chunk.Type.TRANSFER_START)
+ def _set_initial_chunk_fields(self, chunk: Chunk) -> None:
+ # Nothing to tag onto the initial chunk in a write transfer.
+ pass
async def _handle_data_chunk(self, chunk: Chunk) -> None:
"""Processes an incoming chunk from the server.
@@ -249,107 +476,124 @@ class WriteTransfer(Transfer):
send data accordingly.
"""
- async with self._lock:
- self._window_id += 1
- window_id = self._window_id
+ if self._state is Transfer._State.TRANSMITTING:
+ self._state = Transfer._State.WAITING
- if not self._handle_parameters_update(chunk):
- return
+ assert self._state is Transfer._State.WAITING
+
+ if not self._handle_parameters_update(chunk):
+ return
+
+ self._bytes_confirmed_received = chunk.offset
+ self._state = Transfer._State.TRANSMITTING
- bytes_acknowledged = chunk.offset
+ self._window_id += 1
+ asyncio.create_task(self._transmit_next_chunk(self._window_id))
- while True:
- if self._chunk_delay_us:
- await asyncio.sleep(self._chunk_delay_us / 1e6)
+ async def _transmit_next_chunk(
+ self, window_id: int, timeout_us: Optional[int] = None
+ ) -> None:
+ """Transmits a single data chunk to the server.
- async with self._lock:
- if self.done.is_set():
- return
+ If the chunk completes the active window, returns to a WAITING state.
+ Otherwise, schedules another transmission for the next chunk.
+ """
+ if timeout_us is not None:
+ await asyncio.sleep(timeout_us / 1e6)
- if window_id != self._window_id:
- _LOG.debug('Transfer %d: Skipping stale window', self.id)
- return
+ if self._state is not Transfer._State.TRANSMITTING:
+ return
- write_chunk = self._next_chunk()
- self._offset += len(write_chunk.data)
- sent_requested_bytes = self._offset == self._window_end_offset
+ if window_id != self._window_id:
+ _LOG.debug('Transfer %d: Skipping stale window', self.id)
+ return
- self._send_chunk(write_chunk)
+ chunk = self._next_chunk()
+ self._offset += len(chunk.data)
- self._update_progress(self._offset, bytes_acknowledged,
- len(self.data))
+ sent_requested_bytes = self._offset == self._window_end_offset
- if sent_requested_bytes:
- break
+ self._send_chunk(chunk)
+ self._update_progress(
+ self._offset, self._bytes_confirmed_received, len(self.data)
+ )
- self._last_chunk = write_chunk
+ if sent_requested_bytes:
+ self._state = Transfer._State.WAITING
+ else:
+ asyncio.create_task(
+ self._transmit_next_chunk(
+ window_id, timeout_us=self._chunk_delay_us
+ )
+ )
def _handle_parameters_update(self, chunk: Chunk) -> bool:
"""Updates transfer state based on a transfer parameters update."""
- retransmit = True
- if chunk.HasField('type'):
- retransmit = (chunk.type == Chunk.Type.PARAMETERS_RETRANSMIT
- or chunk.type == Chunk.Type.TRANSFER_START)
-
if chunk.offset > len(self.data):
# Bad offset; terminate the transfer.
_LOG.error(
'Transfer %d: server requested invalid offset %d (size %d)',
- self.id, chunk.offset, len(self.data))
+ self.id,
+ chunk.offset,
+ len(self.data),
+ )
- self._send_error(Status.OUT_OF_RANGE)
+ self._send_final_chunk(Status.OUT_OF_RANGE)
return False
- if chunk.pending_bytes == 0:
+ if chunk.offset == chunk.window_end_offset:
_LOG.error(
'Transfer %d: service requested 0 bytes (invalid); aborting',
- self.id)
- self._send_error(Status.INTERNAL)
+ self.id,
+ )
+ self._send_final_chunk(Status.INTERNAL)
return False
- if retransmit:
+ # Extend the window to the new end offset specified by the server.
+ self._window_end_offset = min(chunk.window_end_offset, len(self.data))
+
+ if chunk.requests_transmission_from_offset():
# Check whether the client has sent a previous data offset, which
# indicates that some chunks were lost in transmission.
if chunk.offset < self._offset:
- _LOG.debug('Write transfer %d rolling back: offset %d from %d',
- self.id, chunk.offset, self._offset)
+ _LOG.debug(
+ 'Write transfer %d rolling back: offset %d from %d',
+ self.id,
+ chunk.offset,
+ self._offset,
+ )
self._offset = chunk.offset
- # Retransmit is the default behavior for older versions of the
- # transfer protocol. The window_end_offset field is not guaranteed
- # to be set in these version, so it must be calculated.
- max_bytes_to_send = min(chunk.pending_bytes,
- len(self.data) - self._offset)
- self._window_end_offset = self._offset + max_bytes_to_send
- else:
- assert chunk.type == Chunk.Type.PARAMETERS_CONTINUE
-
- # Extend the window to the new end offset specified by the server.
- self._window_end_offset = min(chunk.window_end_offset,
- len(self.data))
-
- if chunk.HasField('max_chunk_size_bytes'):
+ if chunk.max_chunk_size_bytes is not None:
self._max_chunk_size = chunk.max_chunk_size_bytes
- if chunk.HasField('min_delay_microseconds'):
+ if chunk.min_delay_microseconds is not None:
self._chunk_delay_us = chunk.min_delay_microseconds
return True
- def _retry_after_timeout(self) -> None:
- self._send_chunk(self._last_chunk)
+ def _retry_after_data_timeout(self) -> None:
+ if (
+ self._state is Transfer._State.WAITING
+ and self._last_chunk is not None
+ ):
+ self._send_chunk(self._last_chunk)
def _next_chunk(self) -> Chunk:
"""Returns the next Chunk message to send in the data transfer."""
- chunk = Chunk(transfer_id=self.id,
- offset=self._offset,
- type=Chunk.Type.TRANSFER_DATA)
- max_bytes_in_chunk = min(self._max_chunk_size,
- self._window_end_offset - self._offset)
-
- chunk.data = self.data[self._offset:self._offset + max_bytes_in_chunk]
+ chunk = Chunk(
+ self._configured_protocol_version,
+ Chunk.Type.DATA,
+ session_id=self.id,
+ offset=self._offset,
+ )
+
+ max_bytes_in_chunk = min(
+ self._max_chunk_size, self._window_end_offset - self._offset
+ )
+ chunk.data = self.data[self._offset : self._offset + max_bytes_in_chunk]
# Mark the final chunk of the transfer.
if len(self.data) - self._offset <= max_bytes_in_chunk:
@@ -376,20 +620,31 @@ class ReadTransfer(Transfer):
EXTEND_WINDOW_DIVISOR = 2
def __init__( # pylint: disable=too-many-arguments
- self,
- transfer_id: int,
- send_chunk: Callable[[Chunk], None],
- end_transfer: Callable[[Transfer], None],
- response_timeout_s: float,
- initial_response_timeout_s: float,
- max_retries: int,
- max_bytes_to_receive: int = 8192,
- max_chunk_size: int = 1024,
- chunk_delay_us: int = None,
- progress_callback: ProgressCallback = None):
- super().__init__(transfer_id, send_chunk, end_transfer,
- response_timeout_s, initial_response_timeout_s,
- max_retries, progress_callback)
+ self,
+ resource_id: int,
+ send_chunk: Callable[[Chunk], None],
+ end_transfer: Callable[[Transfer], None],
+ response_timeout_s: float,
+ initial_response_timeout_s: float,
+ max_retries: int,
+ max_lifetime_retries: int,
+ protocol_version: ProtocolVersion,
+ max_bytes_to_receive: int = 8192,
+ max_chunk_size: int = 1024,
+ chunk_delay_us: Optional[int] = None,
+ progress_callback: Optional[ProgressCallback] = None,
+ ):
+ super().__init__(
+ resource_id,
+ send_chunk,
+ end_transfer,
+ response_timeout_s,
+ initial_response_timeout_s,
+ max_retries,
+ max_lifetime_retries,
+ protocol_version,
+ progress_callback,
+ )
self._max_bytes_to_receive = max_bytes_to_receive
self._max_chunk_size = max_chunk_size
self._chunk_delay_us = chunk_delay_us
@@ -397,16 +652,16 @@ class ReadTransfer(Transfer):
self._remaining_transfer_size: Optional[int] = None
self._data = bytearray()
self._offset = 0
- self._pending_bytes = max_bytes_to_receive
self._window_end_offset = max_bytes_to_receive
+ self._last_chunk_offset: Optional[int] = None
@property
def data(self) -> bytes:
"""Returns an immutable copy of the data that has been read."""
return bytes(self._data)
- def _initial_chunk(self) -> Chunk:
- return self._transfer_parameters(Chunk.Type.TRANSFER_START)
+ def _set_initial_chunk_fields(self, chunk: Chunk) -> None:
+ self._set_transfer_parameters(chunk)
async def _handle_data_chunk(self, chunk: Chunk) -> None:
"""Processes an incoming chunk from the server.
@@ -415,26 +670,67 @@ class ReadTransfer(Transfer):
Once all pending data is received, the transfer parameters are updated.
"""
+ if self._state is Transfer._State.RECOVERY:
+ if chunk.offset != self._offset:
+ if self._last_chunk_offset == chunk.offset:
+ _LOG.debug(
+ 'Transfer %d received repeated offset %d: '
+ 'retry detected, resending transfer parameters',
+ self.id,
+ chunk.offset,
+ )
+ self._send_chunk(
+ self._transfer_parameters(
+ Chunk.Type.PARAMETERS_RETRANSMIT
+ )
+ )
+ else:
+ _LOG.debug(
+ 'Transfer %d waiting for offset %d, ignoring %d',
+ self.id,
+ self._offset,
+ chunk.offset,
+ )
+ self._last_chunk_offset = chunk.offset
+ return
+
+ _LOG.info(
+ 'Transfer %d received expected offset %d, resuming transfer',
+ self.id,
+ chunk.offset,
+ )
+ self._state = Transfer._State.WAITING
+
+ assert self._state is Transfer._State.WAITING
+
if chunk.offset != self._offset:
# Initially, the transfer service only supports in-order transfers.
# If data is received out of order, request that the server
# retransmit from the previous offset.
+ _LOG.debug(
+ 'Transfer %d expected offset %d, received %d: '
+ 'entering recovery state',
+ self.id,
+ self._offset,
+ chunk.offset,
+ )
+ self._state = Transfer._State.RECOVERY
+
self._send_chunk(
- self._transfer_parameters(Chunk.Type.PARAMETERS_RETRANSMIT))
+ self._transfer_parameters(Chunk.Type.PARAMETERS_RETRANSMIT)
+ )
return
self._data += chunk.data
- self._pending_bytes -= len(chunk.data)
self._offset += len(chunk.data)
- if chunk.HasField('remaining_bytes'):
+ # Update the last offset seen so that retries can be detected.
+ self._last_chunk_offset = chunk.offset
+
+ if chunk.remaining_bytes is not None:
if chunk.remaining_bytes == 0:
# No more data to read. Acknowledge receipt and finish.
- self._send_chunk(
- Chunk(transfer_id=self.id,
- status=Status.OK.value,
- type=Chunk.Type.TRANSFER_COMPLETION))
- self.finish(Status.OK)
+ self._send_final_chunk(Status.OK)
return
# The server may indicate if the amount of remaining data is known.
@@ -447,61 +743,80 @@ class ReadTransfer(Transfer):
if self._remaining_transfer_size <= 0:
self._remaining_transfer_size = None
- total_size = None if self._remaining_transfer_size is None else (
- self._remaining_transfer_size + self._offset)
+ total_size = (
+ None
+ if self._remaining_transfer_size is None
+ else (self._remaining_transfer_size + self._offset)
+ )
self._update_progress(self._offset, self._offset, total_size)
if chunk.window_end_offset != 0:
if chunk.window_end_offset < self._offset:
_LOG.error(
'Transfer %d: transmitter sent invalid earlier end offset '
- '%d (receiver offset %d)', self.id,
- chunk.window_end_offset, self._offset)
- self._send_error(Status.INTERNAL)
+ '%d (receiver offset %d)',
+ self.id,
+ chunk.window_end_offset,
+ self._offset,
+ )
+ self._send_final_chunk(Status.INTERNAL)
return
if chunk.window_end_offset > self._window_end_offset:
_LOG.error(
'Transfer %d: transmitter sent invalid later end offset '
- '%d (receiver end offset %d)', self.id,
- chunk.window_end_offset, self._window_end_offset)
- self._send_error(Status.INTERNAL)
+ '%d (receiver end offset %d)',
+ self.id,
+ chunk.window_end_offset,
+ self._window_end_offset,
+ )
+ self._send_final_chunk(Status.INTERNAL)
return
self._window_end_offset = chunk.window_end_offset
- self._pending_bytes -= chunk.window_end_offset - self._offset
remaining_window_size = self._window_end_offset - self._offset
- extend_window = (remaining_window_size <= self._max_bytes_to_receive /
- ReadTransfer.EXTEND_WINDOW_DIVISOR)
+ extend_window = (
+ remaining_window_size
+ <= self._max_bytes_to_receive / ReadTransfer.EXTEND_WINDOW_DIVISOR
+ )
- if self._pending_bytes == 0:
+ if self._offset == self._window_end_offset:
# All pending data was received. Send out a new parameters chunk for
# the next block.
self._send_chunk(
- self._transfer_parameters(Chunk.Type.PARAMETERS_RETRANSMIT))
+ self._transfer_parameters(Chunk.Type.PARAMETERS_RETRANSMIT)
+ )
elif extend_window:
self._send_chunk(
- self._transfer_parameters(Chunk.Type.PARAMETERS_CONTINUE))
-
- def _retry_after_timeout(self) -> None:
- self._send_chunk(
- self._transfer_parameters(Chunk.Type.PARAMETERS_RETRANSMIT))
-
- def _transfer_parameters(self, chunk_type: Any) -> Chunk:
- """Sends an updated transfer parameters chunk to the server."""
+ self._transfer_parameters(Chunk.Type.PARAMETERS_CONTINUE)
+ )
+
+ def _retry_after_data_timeout(self) -> None:
+ if (
+ self._state is Transfer._State.WAITING
+ or self._state is Transfer._State.RECOVERY
+ ):
+ self._send_chunk(
+ self._transfer_parameters(Chunk.Type.PARAMETERS_RETRANSMIT)
+ )
- self._pending_bytes = self._max_bytes_to_receive
+ def _set_transfer_parameters(self, chunk: Chunk) -> None:
self._window_end_offset = self._offset + self._max_bytes_to_receive
- chunk = Chunk(transfer_id=self.id,
- pending_bytes=self._pending_bytes,
- window_end_offset=self._window_end_offset,
- max_chunk_size_bytes=self._max_chunk_size,
- offset=self._offset,
- type=chunk_type)
+ chunk.offset = self._offset
+ chunk.window_end_offset = self._window_end_offset
+ chunk.max_chunk_size_bytes = self._max_chunk_size
if self._chunk_delay_us:
chunk.min_delay_microseconds = self._chunk_delay_us
+ def _transfer_parameters(self, chunk_type: Any) -> Chunk:
+ """Returns an updated transfer parameters chunk."""
+
+ chunk = Chunk(
+ self._configured_protocol_version, chunk_type, session_id=self.id
+ )
+ self._set_transfer_parameters(chunk)
+
return chunk
diff --git a/pw_transfer/py/tests/python_cpp_transfer_test.py b/pw_transfer/py/tests/python_cpp_transfer_test.py
deleted file mode 100755
index 62e0d14eb..000000000
--- a/pw_transfer/py/tests/python_cpp_transfer_test.py
+++ /dev/null
@@ -1,212 +0,0 @@
-#!/usr/bin/env python3
-# Copyright 2021 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-"""Tests transfers between the Python client and C++ service."""
-
-from pathlib import Path
-import random
-import tempfile
-from typing import List, Tuple, Union
-import unittest
-
-from pw_hdlc import rpc
-from pw_rpc import testing
-from pw_status import Status
-import pw_transfer
-from pw_transfer import transfer_pb2
-from pw_transfer_test import test_server_pb2
-
-ITERATIONS = 5
-TIMEOUT_S = 0.05
-
-_DATA_4096B = b'SPAM' * (4096 // len('SPAM'))
-
-
-class TransferServiceIntegrationTest(unittest.TestCase):
- """Tests transfers between the Python client and C++ service."""
- test_server_command: Tuple[str, ...] = ()
- port: int
-
- def setUp(self) -> None:
- self._tempdir = tempfile.TemporaryDirectory(
- prefix=f'pw_transfer_{self.id().rsplit(".", 1)[-1]}_')
- self.directory = Path(self._tempdir.name)
-
- command = (*self.test_server_command, str(self.directory))
- self._outgoing_filter = rpc.PacketFilter('outgoing RPC')
- self._incoming_filter = rpc.PacketFilter('incoming RPC')
- self._context = rpc.HdlcRpcLocalServerAndClient(
- command,
- self.port, [transfer_pb2, test_server_pb2],
- outgoing_processor=self._outgoing_filter,
- incoming_processor=self._incoming_filter)
-
- service = self._context.client.channel(1).rpcs.pw.transfer.Transfer
- self.manager = pw_transfer.Manager(
- service, default_response_timeout_s=TIMEOUT_S)
-
- self._test_server = self._context.client.channel(
- 1).rpcs.pw.transfer.TestServer
-
- def tearDown(self) -> None:
- try:
- self._tempdir.cleanup()
- finally:
- if hasattr(self, '_context'):
- self._context.close()
-
- def transfer_file_path(self, transfer_id: int) -> Path:
- return self.directory / str(transfer_id)
-
- def set_content(self, transfer_id: int, data: Union[bytes, str]) -> None:
- self.transfer_file_path(transfer_id).write_bytes(
- data.encode() if isinstance(data, str) else data)
- self._test_server.ReloadTransferFiles()
-
- def get_content(self, transfer_id: int) -> bytes:
- return self.transfer_file_path(transfer_id).read_bytes()
-
- def test_read_unknown_id(self) -> None:
- with self.assertRaises(pw_transfer.Error) as ctx:
- self.manager.read(99)
- self.assertEqual(ctx.exception.status, Status.NOT_FOUND)
-
- def test_read_empty(self) -> None:
- for _ in range(ITERATIONS):
- self.set_content(24, '')
- self.assertEqual(self.manager.read(24), b'')
-
- def test_read_single_byte(self) -> None:
- for _ in range(ITERATIONS):
- self.set_content(25, '0')
- self.assertEqual(self.manager.read(25), b'0')
-
- def test_read_small_amount_of_data(self) -> None:
- for _ in range(ITERATIONS):
- self.set_content(26, 'hunter2')
- self.assertEqual(self.manager.read(26), b'hunter2')
-
- def test_read_large_amount_of_data(self) -> None:
- for _ in range(ITERATIONS):
- size = 2**13 # TODO(hepler): Increase to 2**14 when it passes.
- self.set_content(27, '~' * size)
- self.assertEqual(self.manager.read(27), b'~' * size)
-
- def test_write_unknown_id(self) -> None:
- with self.assertRaises(pw_transfer.Error) as ctx:
- self.manager.write(99, '')
- self.assertEqual(ctx.exception.status, Status.NOT_FOUND)
-
- def test_write_empty(self) -> None:
- for _ in range(ITERATIONS):
- self.set_content(28, 'junk')
- self.manager.write(28, b'')
- self.assertEqual(self.get_content(28), b'')
-
- def test_write_single_byte(self) -> None:
- for _ in range(ITERATIONS):
- self.set_content(29, 'junk')
- self.manager.write(29, b'$')
- self.assertEqual(self.get_content(29), b'$')
-
- def test_write_small_amount_of_data(self) -> None:
- for _ in range(ITERATIONS):
- self.set_content(30, 'junk')
- self.manager.write(30, b'file transfer')
- self.assertEqual(self.get_content(30), b'file transfer')
-
- def test_write_large_amount_of_data(self) -> None:
- for _ in range(ITERATIONS):
- self.set_content(31, 'junk')
- self.manager.write(31, b'*' * 512)
- self.assertEqual(self.get_content(31), b'*' * 512)
-
- def test_write_very_large_amount_of_data(self) -> None:
- for _ in range(ITERATIONS):
- self.set_content(32, 'junk')
-
- # Larger than the transfer service's configured pending_bytes.
- self.manager.write(32, _DATA_4096B)
- self.assertEqual(self.get_content(32), _DATA_4096B)
-
- def test_write_string(self) -> None:
- for _ in range(ITERATIONS):
- # Write a string instead of bytes.
- self.set_content(33, 'junk')
- self.manager.write(33, 'hello world')
- self.assertEqual(self.get_content(33), b'hello world')
-
- def test_write_drop_data_chunks_and_transfer_parameters(self) -> None:
- self.set_content(34, 'junk')
-
- # Allow the initial packet and first chunk, then drop the second chunk.
- self._outgoing_filter.keep(2)
- self._outgoing_filter.drop(1)
-
- # Allow the initial transfer parameters updates, then drop the next two.
- self._incoming_filter.keep(1)
- self._incoming_filter.drop(2)
-
- with self.assertLogs('pw_transfer', 'DEBUG') as logs:
- self.manager.write(34, _DATA_4096B)
-
- self.assertEqual(self.get_content(34), _DATA_4096B)
-
- # Verify that the client retried twice.
- messages = [r.getMessage() for r in logs.records]
- retry = f'Received no responses for {TIMEOUT_S:.3f}s; retrying {{}}/3'
- self.assertIn(retry.format(1), messages)
- self.assertIn(retry.format(2), messages)
-
- def test_write_regularly_drop_packets(self) -> None:
- self.set_content(35, 'junk')
-
- self._outgoing_filter.drop_every(5) # drop one per window
- self._incoming_filter.drop_every(3)
-
- self.manager.write(35, _DATA_4096B)
-
- self.assertEqual(self.get_content(35), _DATA_4096B)
-
- def test_write_randomly_drop_packets(self) -> None:
- # Allow lots of retries since there are lots of drops.
- self.manager.max_retries = 9
-
- for seed in [1, 5678, 600613]:
- self.set_content(seed, 'junk')
-
- rand = random.Random(seed)
- self._incoming_filter.randomly_drop(3, rand)
- self._outgoing_filter.randomly_drop(3, rand)
-
- data = bytes(
- rand.randrange(256) for _ in range(rand.randrange(16384)))
- self.manager.write(seed, data)
- self.assertEqual(self.get_content(seed), data)
-
- self._incoming_filter.reset()
- self._outgoing_filter.reset()
-
-
-def _main(test_server_command: List[str], port: int,
- unittest_args: List[str]) -> None:
- TransferServiceIntegrationTest.test_server_command = tuple(
- test_server_command)
- TransferServiceIntegrationTest.port = port
-
- unittest.main(argv=unittest_args)
-
-
-if __name__ == '__main__':
- _main(**vars(testing.parse_test_server_args()))
diff --git a/pw_transfer/py/tests/transfer_test.py b/pw_transfer/py/tests/transfer_test.py
index e73c09ddf..f06bbf508 100644
--- a/pw_transfer/py/tests/transfer_test.py
+++ b/pw_transfer/py/tests/transfer_test.py
@@ -16,6 +16,7 @@
import enum
import math
+import os
import unittest
from typing import Iterable, List
@@ -24,7 +25,13 @@ from pw_rpc import callback_client, client, ids, packets
from pw_rpc.internal import packet_pb2
import pw_transfer
-from pw_transfer.transfer_pb2 import Chunk
+from pw_transfer import ProtocolVersion
+
+try:
+ from pw_transfer import transfer_pb2
+except ImportError:
+ # For the bazel build, which puts generated protos in a different location.
+ from pigweed.pw_transfer import transfer_pb2 # type: ignore
_TRANSFER_SERVICE_ID = ids.calculate('pw.transfer.Transfer')
@@ -37,20 +44,27 @@ class _Method(enum.Enum):
WRITE = ids.calculate('Write')
+# pylint: disable=missing-function-docstring, missing-class-docstring
+
+
class TransferManagerTest(unittest.TestCase):
+ # pylint: disable=too-many-public-methods
"""Tests for the transfer manager."""
+
def setUp(self) -> None:
self._client = client.Client.from_modules(
- callback_client.Impl(), [client.Channel(1, self._handle_request)],
- (pw_transfer.transfer_pb2, ))
+ callback_client.Impl(),
+ [client.Channel(1, self._handle_request)],
+ (transfer_pb2,),
+ )
self._service = self._client.channel(1).rpcs.pw.transfer.Transfer
- self._sent_chunks: List[Chunk] = []
+ self._sent_chunks: List[transfer_pb2.Chunk] = []
self._packets_to_send: List[List[bytes]] = []
def _enqueue_server_responses(
- self, method: _Method,
- responses: Iterable[Iterable[Chunk]]) -> None:
+ self, method: _Method, responses: Iterable[Iterable[transfer_pb2.Chunk]]
+ ) -> None:
for group in responses:
serialized_group = []
for response in group:
@@ -61,25 +75,30 @@ class TransferManagerTest(unittest.TestCase):
service_id=_TRANSFER_SERVICE_ID,
method_id=method.value,
status=Status.OK.value,
- payload=response.SerializeToString()).
- SerializeToString())
+ payload=response.SerializeToString(),
+ ).SerializeToString()
+ )
self._packets_to_send.append(serialized_group)
def _enqueue_server_error(self, method: _Method, error: Status) -> None:
- self._packets_to_send.append([
- packet_pb2.RpcPacket(type=packet_pb2.PacketType.SERVER_ERROR,
- channel_id=1,
- service_id=_TRANSFER_SERVICE_ID,
- method_id=method.value,
- status=error.value).SerializeToString()
- ])
+ self._packets_to_send.append(
+ [
+ packet_pb2.RpcPacket(
+ type=packet_pb2.PacketType.SERVER_ERROR,
+ channel_id=1,
+ service_id=_TRANSFER_SERVICE_ID,
+ method_id=method.value,
+ status=error.value,
+ ).SerializeToString()
+ ]
+ )
def _handle_request(self, data: bytes) -> None:
packet = packets.decode(data)
if packet.type is not packet_pb2.PacketType.CLIENT_STREAM:
return
- chunk = Chunk()
+ chunk = transfer_pb2.Chunk()
chunk.MergeFromString(packet.payload)
self._sent_chunks.append(chunk)
@@ -96,12 +115,18 @@ class TransferManagerTest(unittest.TestCase):
def test_read_transfer_basic(self):
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.READ,
- ((Chunk(transfer_id=3, offset=0, data=b'abc',
- remaining_bytes=0), ), ),
+ (
+ (
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=0, data=b'abc', remaining_bytes=0
+ ),
+ ),
+ ),
)
data = manager.read(3)
@@ -112,14 +137,21 @@ class TransferManagerTest(unittest.TestCase):
def test_read_transfer_multichunk(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.READ,
- ((
- Chunk(transfer_id=3, offset=0, data=b'abc', remaining_bytes=3),
- Chunk(transfer_id=3, offset=3, data=b'def', remaining_bytes=0),
- ), ),
+ (
+ (
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=0, data=b'abc', remaining_bytes=3
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=3, data=b'def', remaining_bytes=0
+ ),
+ ),
+ ),
)
data = manager.read(3)
@@ -130,14 +162,21 @@ class TransferManagerTest(unittest.TestCase):
def test_read_transfer_progress_callback(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.READ,
- ((
- Chunk(transfer_id=3, offset=0, data=b'abc', remaining_bytes=3),
- Chunk(transfer_id=3, offset=3, data=b'def', remaining_bytes=0),
- ), ),
+ (
+ (
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=0, data=b'abc', remaining_bytes=3
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=3, data=b'def', remaining_bytes=0
+ ),
+ ),
+ ),
)
progress: List[pw_transfer.ProgressStats] = []
@@ -147,42 +186,42 @@ class TransferManagerTest(unittest.TestCase):
self.assertEqual(len(self._sent_chunks), 2)
self.assertTrue(self._sent_chunks[-1].HasField('status'))
self.assertEqual(self._sent_chunks[-1].status, 0)
- self.assertEqual(progress, [
- pw_transfer.ProgressStats(3, 3, 6),
- pw_transfer.ProgressStats(6, 6, 6),
- ])
+ self.assertEqual(
+ progress,
+ [
+ pw_transfer.ProgressStats(3, 3, 6),
+ pw_transfer.ProgressStats(6, 6, 6),
+ ],
+ )
def test_read_transfer_retry_bad_offset(self) -> None:
"""Server responds with an unexpected offset in a read transfer."""
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.READ,
(
(
- Chunk(transfer_id=3,
- offset=0,
- data=b'123',
- remaining_bytes=6),
-
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=0, data=b'123', remaining_bytes=6
+ ),
# Incorrect offset; expecting 3.
- Chunk(transfer_id=3,
- offset=1,
- data=b'456',
- remaining_bytes=3),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=1, data=b'456', remaining_bytes=3
+ ),
),
(
- Chunk(transfer_id=3,
- offset=3,
- data=b'456',
- remaining_bytes=3),
- Chunk(transfer_id=3,
- offset=6,
- data=b'789',
- remaining_bytes=0),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=3, data=b'456', remaining_bytes=3
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=6, data=b'789', remaining_bytes=0
+ ),
),
- ))
+ ),
+ )
data = manager.read(3)
self.assertEqual(data, b'123456789')
@@ -192,18 +231,94 @@ class TransferManagerTest(unittest.TestCase):
self.assertTrue(self._sent_chunks[-1].HasField('status'))
self.assertEqual(self._sent_chunks[-1].status, 0)
+ def test_read_transfer_recovery_sends_parameters_on_retry(self) -> None:
+ """Server sends the same chunk twice (retry) in a read transfer."""
+ manager = pw_transfer.Manager(
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
+
+ self._enqueue_server_responses(
+ _Method.READ,
+ (
+ (
+ # Bad offset, enter recovery state. Only one parameters
+ # chunk should be sent.
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=1, data=b'234', remaining_bytes=5
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=4, data=b'567', remaining_bytes=2
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=7, data=b'8', remaining_bytes=1
+ ),
+ ),
+ (
+ # Only one parameters chunk should be sent after the server
+ # retries the same offset twice.
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=1, data=b'234', remaining_bytes=5
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=4, data=b'567', remaining_bytes=2
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=7, data=b'8', remaining_bytes=1
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=7, data=b'8', remaining_bytes=1
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ transfer_id=3,
+ offset=0,
+ data=b'123456789',
+ remaining_bytes=0,
+ ),
+ ),
+ ),
+ )
+
+ data = manager.read(3)
+ self.assertEqual(data, b'123456789')
+
+ self.assertEqual(len(self._sent_chunks), 4)
+ self.assertEqual(
+ self._sent_chunks[0].type, transfer_pb2.Chunk.Type.START
+ )
+ self.assertEqual(self._sent_chunks[0].offset, 0)
+ self.assertEqual(
+ self._sent_chunks[1].type,
+ transfer_pb2.Chunk.Type.PARAMETERS_RETRANSMIT,
+ )
+ self.assertEqual(self._sent_chunks[1].offset, 0)
+ self.assertEqual(
+ self._sent_chunks[2].type,
+ transfer_pb2.Chunk.Type.PARAMETERS_RETRANSMIT,
+ )
+ self.assertEqual(self._sent_chunks[2].offset, 0)
+ self.assertEqual(
+ self._sent_chunks[3].type, transfer_pb2.Chunk.Type.COMPLETION
+ )
+
def test_read_transfer_retry_timeout(self) -> None:
"""Server doesn't respond to read transfer parameters."""
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.READ,
(
(), # Send nothing in response to the initial parameters.
- (Chunk(transfer_id=3, offset=0, data=b'xyz',
- remaining_bytes=0), ),
- ))
+ (
+ transfer_pb2.Chunk(
+ transfer_id=3, offset=0, data=b'xyz', remaining_bytes=0
+ ),
+ ),
+ ),
+ )
data = manager.read(3)
self.assertEqual(data, b'xyz')
@@ -213,15 +328,49 @@ class TransferManagerTest(unittest.TestCase):
self.assertTrue(self._sent_chunks[-1].HasField('status'))
self.assertEqual(self._sent_chunks[-1].status, 0)
+ def test_read_transfer_lifetime_retries(self) -> None:
+ """Server doesn't respond several times during the transfer."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ max_retries=2**32 - 1,
+ max_lifetime_retries=4,
+ )
+
+ self._enqueue_server_responses(
+ _Method.READ,
+ (
+ (), # Retry 1
+ (), # Retry 2
+ (
+ transfer_pb2.Chunk( # Expected chunk.
+ transfer_id=43, offset=0, data=b'xyz'
+ ),
+ ),
+ # Don't send anything else. The maximum lifetime retry count
+ # should be hit.
+ ),
+ )
+
+ with self.assertRaises(pw_transfer.Error) as context:
+ manager.read(43)
+
+ self.assertEqual(len(self._sent_chunks), 5)
+
+ exception = context.exception
+ self.assertEqual(exception.resource_id, 43)
+ self.assertEqual(exception.status, Status.DEADLINE_EXCEEDED)
+
def test_read_transfer_timeout(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
with self.assertRaises(pw_transfer.Error) as context:
manager.read(27)
exception = context.exception
- self.assertEqual(exception.transfer_id, 27)
+ self.assertEqual(exception.resource_id, 27)
self.assertEqual(exception.status, Status.DEADLINE_EXCEEDED)
# The client should have sent four transfer parameters requests: one
@@ -230,23 +379,31 @@ class TransferManagerTest(unittest.TestCase):
def test_read_transfer_error(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.READ,
- ((Chunk(transfer_id=31, status=Status.NOT_FOUND.value), ), ),
+ (
+ (
+ transfer_pb2.Chunk(
+ transfer_id=31, status=Status.NOT_FOUND.value
+ ),
+ ),
+ ),
)
with self.assertRaises(pw_transfer.Error) as context:
manager.read(31)
exception = context.exception
- self.assertEqual(exception.transfer_id, 31)
+ self.assertEqual(exception.resource_id, 31)
self.assertEqual(exception.status, Status.NOT_FOUND)
def test_read_transfer_server_error(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_error(_Method.READ, Status.NOT_FOUND)
@@ -254,21 +411,26 @@ class TransferManagerTest(unittest.TestCase):
manager.read(31)
exception = context.exception
- self.assertEqual(exception.transfer_id, 31)
+ self.assertEqual(exception.resource_id, 31)
self.assertEqual(exception.status, Status.INTERNAL)
def test_write_transfer_basic(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.WRITE,
(
- (Chunk(transfer_id=4,
- offset=0,
- pending_bytes=32,
- max_chunk_size_bytes=8), ),
- (Chunk(transfer_id=4, status=Status.OK.value), ),
+ (
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=0,
+ pending_bytes=32,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value),),
),
)
@@ -278,17 +440,22 @@ class TransferManagerTest(unittest.TestCase):
def test_write_transfer_max_chunk_size(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.WRITE,
(
- (Chunk(transfer_id=4,
- offset=0,
- pending_bytes=32,
- max_chunk_size_bytes=8), ),
+ (
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=0,
+ pending_bytes=32,
+ max_chunk_size_bytes=8,
+ ),
+ ),
(),
- (Chunk(transfer_id=4, status=Status.OK.value), ),
+ (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value),),
),
)
@@ -300,20 +467,29 @@ class TransferManagerTest(unittest.TestCase):
def test_write_transfer_multiple_parameters(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.WRITE,
(
- (Chunk(transfer_id=4,
- offset=0,
- pending_bytes=8,
- max_chunk_size_bytes=8), ),
- (Chunk(transfer_id=4,
- offset=8,
- pending_bytes=8,
- max_chunk_size_bytes=8), ),
- (Chunk(transfer_id=4, status=Status.OK.value), ),
+ (
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=0,
+ pending_bytes=8,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=8,
+ pending_bytes=8,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value),),
),
)
@@ -325,20 +501,29 @@ class TransferManagerTest(unittest.TestCase):
def test_write_transfer_progress_callback(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.WRITE,
(
- (Chunk(transfer_id=4,
- offset=0,
- pending_bytes=8,
- max_chunk_size_bytes=8), ),
- (Chunk(transfer_id=4,
- offset=8,
- pending_bytes=8,
- max_chunk_size_bytes=8), ),
- (Chunk(transfer_id=4, status=Status.OK.value), ),
+ (
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=0,
+ pending_bytes=8,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=8,
+ pending_bytes=8,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value),),
),
)
@@ -349,41 +534,57 @@ class TransferManagerTest(unittest.TestCase):
self.assertEqual(self._received_data(), b'data to write')
self.assertEqual(self._sent_chunks[1].data, b'data to ')
self.assertEqual(self._sent_chunks[2].data, b'write')
- self.assertEqual(progress, [
- pw_transfer.ProgressStats(8, 0, 13),
- pw_transfer.ProgressStats(13, 8, 13),
- pw_transfer.ProgressStats(13, 13, 13)
- ])
+ self.assertEqual(
+ progress,
+ [
+ pw_transfer.ProgressStats(8, 0, 13),
+ pw_transfer.ProgressStats(13, 8, 13),
+ pw_transfer.ProgressStats(13, 13, 13),
+ ],
+ )
def test_write_transfer_rewind(self) -> None:
"""Write transfer in which the server re-requests an earlier offset."""
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.WRITE,
(
- (Chunk(transfer_id=4,
- offset=0,
- pending_bytes=8,
- max_chunk_size_bytes=8), ),
- (Chunk(transfer_id=4,
- offset=8,
- pending_bytes=8,
- max_chunk_size_bytes=8), ),
- (
- Chunk(
+ (
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=0,
+ pending_bytes=8,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=8,
+ pending_bytes=8,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
transfer_id=4,
offset=4, # rewind
pending_bytes=8,
- max_chunk_size_bytes=8), ),
+ max_chunk_size_bytes=8,
+ ),
+ ),
(
- Chunk(
+ transfer_pb2.Chunk(
transfer_id=4,
offset=12,
pending_bytes=16, # update max size
- max_chunk_size_bytes=16), ),
- (Chunk(transfer_id=4, status=Status.OK.value), ),
+ max_chunk_size_bytes=16,
+ ),
+ ),
+ (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value),),
),
)
@@ -396,22 +597,29 @@ class TransferManagerTest(unittest.TestCase):
def test_write_transfer_bad_offset(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.WRITE,
(
- (Chunk(transfer_id=4,
- offset=0,
- pending_bytes=8,
- max_chunk_size_bytes=8), ),
(
- Chunk(
+ transfer_pb2.Chunk(
+ transfer_id=4,
+ offset=0,
+ pending_bytes=8,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
transfer_id=4,
offset=100, # larger offset than data
pending_bytes=8,
- max_chunk_size_bytes=8), ),
- (Chunk(transfer_id=4, status=Status.OK.value), ),
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (transfer_pb2.Chunk(transfer_id=4, status=Status.OK.value),),
),
)
@@ -419,28 +627,36 @@ class TransferManagerTest(unittest.TestCase):
manager.write(4, b'small data')
exception = context.exception
- self.assertEqual(exception.transfer_id, 4)
+ self.assertEqual(exception.resource_id, 4)
self.assertEqual(exception.status, Status.OUT_OF_RANGE)
def test_write_transfer_error(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.WRITE,
- ((Chunk(transfer_id=21, status=Status.UNAVAILABLE.value), ), ),
+ (
+ (
+ transfer_pb2.Chunk(
+ transfer_id=21, status=Status.UNAVAILABLE.value
+ ),
+ ),
+ ),
)
with self.assertRaises(pw_transfer.Error) as context:
manager.write(21, b'no write')
exception = context.exception
- self.assertEqual(exception.transfer_id, 21)
+ self.assertEqual(exception.resource_id, 21)
self.assertEqual(exception.status, Status.UNAVAILABLE)
def test_write_transfer_server_error(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_error(_Method.WRITE, Status.NOT_FOUND)
@@ -448,13 +664,16 @@ class TransferManagerTest(unittest.TestCase):
manager.write(21, b'server error')
exception = context.exception
- self.assertEqual(exception.transfer_id, 21)
+ self.assertEqual(exception.resource_id, 21)
self.assertEqual(exception.status, Status.INTERNAL)
def test_write_transfer_timeout_after_initial_chunk(self) -> None:
- manager = pw_transfer.Manager(self._service,
- default_response_timeout_s=0.001,
- max_retries=2)
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=0.001,
+ max_retries=2,
+ default_protocol_version=ProtocolVersion.LEGACY,
+ )
with self.assertRaises(pw_transfer.Error) as context:
manager.write(22, b'no server response!')
@@ -462,16 +681,26 @@ class TransferManagerTest(unittest.TestCase):
self.assertEqual(
self._sent_chunks,
[
- Chunk(transfer_id=22,
- type=Chunk.Type.TRANSFER_START), # initial chunk
- Chunk(transfer_id=22,
- type=Chunk.Type.TRANSFER_START), # retry 1
- Chunk(transfer_id=22,
- type=Chunk.Type.TRANSFER_START), # retry 2
- ])
+ transfer_pb2.Chunk(
+ transfer_id=22,
+ resource_id=22,
+ type=transfer_pb2.Chunk.Type.START,
+ ), # initial chunk
+ transfer_pb2.Chunk(
+ transfer_id=22,
+ resource_id=22,
+ type=transfer_pb2.Chunk.Type.START,
+ ), # retry 1
+ transfer_pb2.Chunk(
+ transfer_id=22,
+ resource_id=22,
+ type=transfer_pb2.Chunk.Type.START,
+ ), # retry 2
+ ],
+ )
exception = context.exception
- self.assertEqual(exception.transfer_id, 22)
+ self.assertEqual(exception.resource_id, 22)
self.assertEqual(exception.status, Status.DEADLINE_EXCEEDED)
def test_write_transfer_timeout_after_intermediate_chunk(self) -> None:
@@ -479,71 +708,695 @@ class TransferManagerTest(unittest.TestCase):
manager = pw_transfer.Manager(
self._service,
default_response_timeout_s=DEFAULT_TIMEOUT_S,
- max_retries=2)
+ max_retries=2,
+ default_protocol_version=ProtocolVersion.LEGACY,
+ )
self._enqueue_server_responses(
_Method.WRITE,
- [[Chunk(transfer_id=22, pending_bytes=10, max_chunk_size_bytes=5)]
- ])
+ [
+ [
+ transfer_pb2.Chunk(
+ transfer_id=22, pending_bytes=10, max_chunk_size_bytes=5
+ )
+ ]
+ ],
+ )
with self.assertRaises(pw_transfer.Error) as context:
manager.write(22, b'0123456789')
- last_data_chunk = Chunk(transfer_id=22,
- data=b'56789',
- offset=5,
- remaining_bytes=0,
- type=Chunk.Type.TRANSFER_DATA)
+ last_data_chunk = transfer_pb2.Chunk(
+ transfer_id=22,
+ data=b'56789',
+ offset=5,
+ remaining_bytes=0,
+ type=transfer_pb2.Chunk.Type.DATA,
+ )
self.assertEqual(
self._sent_chunks,
[
- Chunk(transfer_id=22, type=Chunk.Type.TRANSFER_START),
- Chunk(transfer_id=22,
- data=b'01234',
- type=Chunk.Type.TRANSFER_DATA),
+ transfer_pb2.Chunk(
+ transfer_id=22,
+ resource_id=22,
+ type=transfer_pb2.Chunk.Type.START,
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=22,
+ data=b'01234',
+ type=transfer_pb2.Chunk.Type.DATA,
+ ),
last_data_chunk, # last chunk
last_data_chunk, # retry 1
last_data_chunk, # retry 2
- ])
+ ],
+ )
exception = context.exception
- self.assertEqual(exception.transfer_id, 22)
+ self.assertEqual(exception.resource_id, 22)
self.assertEqual(exception.status, Status.DEADLINE_EXCEEDED)
def test_write_zero_pending_bytes_is_internal_error(self) -> None:
manager = pw_transfer.Manager(
- self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S)
+ self._service, default_response_timeout_s=DEFAULT_TIMEOUT_S
+ )
self._enqueue_server_responses(
_Method.WRITE,
- ((Chunk(transfer_id=23, pending_bytes=0), ), ),
+ ((transfer_pb2.Chunk(transfer_id=23, pending_bytes=0),),),
)
with self.assertRaises(pw_transfer.Error) as context:
manager.write(23, b'no write')
exception = context.exception
- self.assertEqual(exception.transfer_id, 23)
+ self.assertEqual(exception.resource_id, 23)
self.assertEqual(exception.status, Status.INTERNAL)
+ def test_v2_read_transfer_basic(self) -> None:
+ """Tests a simple protocol version 2 read transfer."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ self._enqueue_server_responses(
+ _Method.READ,
+ (
+ (
+ transfer_pb2.Chunk(
+ resource_id=39,
+ session_id=280,
+ type=transfer_pb2.Chunk.Type.START_ACK,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ session_id=280,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=0,
+ data=b'version two',
+ remaining_bytes=0,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ session_id=280,
+ type=transfer_pb2.Chunk.Type.COMPLETION_ACK,
+ ),
+ ),
+ ),
+ )
+
+ data = manager.read(39)
+
+ self.assertEqual(
+ self._sent_chunks,
+ [
+ transfer_pb2.Chunk(
+ transfer_id=39,
+ resource_id=39,
+ pending_bytes=8192,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=280,
+ type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ # pending_bytes should no longer exist as server and client
+ # have agreed on v2.
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=280,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ ],
+ )
+
+ self.assertEqual(data, b'version two')
+
+ def test_v2_read_transfer_legacy_fallback(self) -> None:
+ """Tests a v2 read transfer when the server only supports legacy."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ # Respond to the START chunk with a legacy data transfer chunk instead
+ # of a START_ACK.
+ self._enqueue_server_responses(
+ _Method.READ,
+ (
+ (
+ transfer_pb2.Chunk(
+ transfer_id=40,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=0,
+ data=b'sorry, legacy only',
+ remaining_bytes=0,
+ ),
+ ),
+ ),
+ )
+
+ data = manager.read(40)
+
+ self.assertEqual(
+ self._sent_chunks,
+ [
+ transfer_pb2.Chunk(
+ transfer_id=40,
+ resource_id=40,
+ pending_bytes=8192,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=40,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ ],
+ )
+
+ self.assertEqual(data, b'sorry, legacy only')
+
+ def test_v2_write_transfer_basic(self) -> None:
+ """Tests a simple protocol version 2 write transfer."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ self._enqueue_server_responses(
+ _Method.WRITE,
+ (
+ (
+ transfer_pb2.Chunk(
+ resource_id=72,
+ session_id=880,
+ type=transfer_pb2.Chunk.Type.START_ACK,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ session_id=880,
+ type=transfer_pb2.Chunk.Type.PARAMETERS_RETRANSMIT,
+ offset=0,
+ window_end_offset=32,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (), # In response to the first data chunk.
+ (
+ transfer_pb2.Chunk(
+ session_id=880,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ ),
+ ),
+ )
+
+ manager.write(72, b'write version 2')
+
+ self.assertEqual(
+ self._sent_chunks,
+ [
+ transfer_pb2.Chunk(
+ transfer_id=72,
+ resource_id=72,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=880,
+ type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=880,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=0,
+ data=b'write ve',
+ ),
+ transfer_pb2.Chunk(
+ session_id=880,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=8,
+ data=b'rsion 2',
+ remaining_bytes=0,
+ ),
+ transfer_pb2.Chunk(
+ session_id=880, type=transfer_pb2.Chunk.Type.COMPLETION_ACK
+ ),
+ ],
+ )
+
+ self.assertEqual(self._received_data(), b'write version 2')
+
+ def test_v2_write_transfer_legacy_fallback(self) -> None:
+ """Tests a v2 write transfer when the server only supports legacy."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ self._enqueue_server_responses(
+ _Method.WRITE,
+ (
+ # Send a parameters chunk immediately per the legacy protocol.
+ (
+ transfer_pb2.Chunk(
+ transfer_id=76,
+ type=transfer_pb2.Chunk.Type.PARAMETERS_RETRANSMIT,
+ offset=0,
+ pending_bytes=32,
+ window_end_offset=32,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (), # In response to the first data chunk.
+ (
+ transfer_pb2.Chunk(
+ transfer_id=76,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ ),
+ ),
+ )
+
+ manager.write(76, b'write v... NOPE')
+
+ self.assertEqual(
+ self._sent_chunks,
+ [
+ transfer_pb2.Chunk(
+ transfer_id=76,
+ resource_id=76,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=76,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=0,
+ data=b'write v.',
+ ),
+ transfer_pb2.Chunk(
+ transfer_id=76,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=8,
+ data=b'.. NOPE',
+ remaining_bytes=0,
+ ),
+ ],
+ )
+
+ self.assertEqual(self._received_data(), b'write v... NOPE')
+
+ def test_v2_server_error(self) -> None:
+ """Tests a timeout occurring during the opening handshake."""
+
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ self._enqueue_server_responses(
+ _Method.READ,
+ (
+ (
+ transfer_pb2.Chunk(
+ resource_id=43,
+ session_id=680,
+ type=transfer_pb2.Chunk.Type.START_ACK,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ session_id=680,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.DATA_LOSS.value,
+ ),
+ ),
+ ),
+ )
+
+ with self.assertRaises(pw_transfer.Error) as context:
+ manager.read(43)
+
+ self.assertEqual(
+ self._sent_chunks,
+ [
+ transfer_pb2.Chunk(
+ transfer_id=43,
+ resource_id=43,
+ pending_bytes=8192,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=680,
+ type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ # Client sends a COMPLETION_ACK in response to the server.
+ transfer_pb2.Chunk(
+ session_id=680, type=transfer_pb2.Chunk.Type.COMPLETION_ACK
+ ),
+ ],
+ )
+
+ exception = context.exception
+ self.assertEqual(exception.resource_id, 43)
+ self.assertEqual(exception.status, Status.DATA_LOSS)
+
+ def test_v2_timeout_during_opening_handshake(self) -> None:
+ """Tests a timeout occurring during the opening handshake."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ # Don't enqueue any server responses.
+
+ with self.assertRaises(pw_transfer.Error) as context:
+ manager.read(41)
+
+ start_chunk = transfer_pb2.Chunk(
+ transfer_id=41,
+ resource_id=41,
+ pending_bytes=8192,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ )
+
+ # The opening chunk should be sent initially, then retried three times.
+ self.assertEqual(self._sent_chunks, [start_chunk] * 4)
+
+ exception = context.exception
+ self.assertEqual(exception.resource_id, 41)
+ self.assertEqual(exception.status, Status.DEADLINE_EXCEEDED)
+
+ def test_v2_timeout_recovery_during_opening_handshake(self) -> None:
+ """Tests a timeout during the opening handshake which recovers."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ self._enqueue_server_responses(
+ _Method.WRITE,
+ (
+ (
+ transfer_pb2.Chunk(
+ resource_id=73,
+ session_id=101,
+ type=transfer_pb2.Chunk.Type.START_ACK,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ ),
+ (), # Don't respond to the START_ACK_CONFIRMATION.
+ (), # Don't respond to the first START_ACK_CONFIRMATION retry.
+ (
+ transfer_pb2.Chunk(
+ session_id=101,
+ type=transfer_pb2.Chunk.Type.PARAMETERS_RETRANSMIT,
+ offset=0,
+ window_end_offset=32,
+ max_chunk_size_bytes=8,
+ ),
+ ),
+ (), # In response to the first data chunk.
+ (
+ transfer_pb2.Chunk(
+ session_id=101,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ ),
+ ),
+ )
+
+ manager.write(73, b'write timeout 2')
+
+ start_ack_confirmation = transfer_pb2.Chunk(
+ session_id=101,
+ type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ )
+
+ self.assertEqual(
+ self._sent_chunks,
+ [
+ transfer_pb2.Chunk(
+ transfer_id=73,
+ resource_id=73,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ start_ack_confirmation, # Initial transmission
+ start_ack_confirmation, # Retry 1
+ start_ack_confirmation, # Retry 2
+ transfer_pb2.Chunk(
+ session_id=101,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=0,
+ data=b'write ti',
+ ),
+ transfer_pb2.Chunk(
+ session_id=101,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=8,
+ data=b'meout 2',
+ remaining_bytes=0,
+ ),
+ transfer_pb2.Chunk(
+ session_id=101, type=transfer_pb2.Chunk.Type.COMPLETION_ACK
+ ),
+ ],
+ )
+
+ self.assertEqual(self._received_data(), b'write timeout 2')
+
+ def test_v2_closing_handshake_bad_chunk(self) -> None:
+ """Tests an unexpected chunk response during the closing handshake."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ self._enqueue_server_responses(
+ _Method.READ,
+ (
+ (
+ transfer_pb2.Chunk(
+ resource_id=47,
+ session_id=580,
+ type=transfer_pb2.Chunk.Type.START_ACK,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ session_id=580,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=0,
+ data=b'version two',
+ remaining_bytes=0,
+ ),
+ ),
+ # In response to the COMPLETION, re-send the last chunk instead
+ # of a COMPLETION_ACK.
+ (
+ transfer_pb2.Chunk(
+ session_id=580,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=0,
+ data=b'version two',
+ remaining_bytes=0,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ session_id=580,
+ type=transfer_pb2.Chunk.Type.COMPLETION_ACK,
+ ),
+ ),
+ ),
+ )
+
+ data = manager.read(47)
+
+ self.assertEqual(
+ self._sent_chunks,
+ [
+ transfer_pb2.Chunk(
+ transfer_id=47,
+ resource_id=47,
+ pending_bytes=8192,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=580,
+ type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=580,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ # Completion should be re-sent following the repeated chunk.
+ transfer_pb2.Chunk(
+ session_id=580,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ ],
+ )
+
+ self.assertEqual(data, b'version two')
+
+ def test_v2_timeout_during_closing_handshake(self) -> None:
+ """Tests a timeout occurring during the closing handshake."""
+ manager = pw_transfer.Manager(
+ self._service,
+ default_response_timeout_s=DEFAULT_TIMEOUT_S,
+ default_protocol_version=ProtocolVersion.VERSION_TWO,
+ )
+
+ self._enqueue_server_responses(
+ _Method.READ,
+ (
+ (
+ transfer_pb2.Chunk(
+ resource_id=47,
+ session_id=980,
+ type=transfer_pb2.Chunk.Type.START_ACK,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ ),
+ (
+ transfer_pb2.Chunk(
+ session_id=980,
+ type=transfer_pb2.Chunk.Type.DATA,
+ offset=0,
+ data=b'dropped completion',
+ remaining_bytes=0,
+ ),
+ ),
+ # Never send the expected COMPLETION_ACK chunk.
+ ),
+ )
+
+ data = manager.read(47)
+
+ self.assertEqual(
+ self._sent_chunks,
+ [
+ transfer_pb2.Chunk(
+ transfer_id=47,
+ resource_id=47,
+ pending_bytes=8192,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ type=transfer_pb2.Chunk.Type.START,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=980,
+ type=transfer_pb2.Chunk.Type.START_ACK_CONFIRMATION,
+ max_chunk_size_bytes=1024,
+ window_end_offset=8192,
+ protocol_version=ProtocolVersion.VERSION_TWO.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=980,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ # The completion should be retried per the usual retry flow.
+ transfer_pb2.Chunk(
+ session_id=980,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=980,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ transfer_pb2.Chunk(
+ session_id=980,
+ type=transfer_pb2.Chunk.Type.COMPLETION,
+ status=Status.OK.value,
+ ),
+ ],
+ )
+
+ # Despite timing out following several retries, the transfer should
+ # still conclude successfully, as failing to receive a COMPLETION_ACK
+ # is not fatal.
+ self.assertEqual(data, b'dropped completion')
+
class ProgressStatsTest(unittest.TestCase):
def test_received_percent_known_total(self) -> None:
self.assertEqual(
- pw_transfer.ProgressStats(75, 0, 100).percent_received(), 0.0)
+ pw_transfer.ProgressStats(75, 0, 100).percent_received(), 0.0
+ )
self.assertEqual(
- pw_transfer.ProgressStats(75, 50, 100).percent_received(), 50.0)
+ pw_transfer.ProgressStats(75, 50, 100).percent_received(), 50.0
+ )
self.assertEqual(
- pw_transfer.ProgressStats(100, 100, 100).percent_received(), 100.0)
+ pw_transfer.ProgressStats(100, 100, 100).percent_received(), 100.0
+ )
def test_received_percent_unknown_total(self) -> None:
self.assertTrue(
math.isnan(
- pw_transfer.ProgressStats(75, 50, None).percent_received()))
+ pw_transfer.ProgressStats(75, 50, None).percent_received()
+ )
+ )
self.assertTrue(
math.isnan(
- pw_transfer.ProgressStats(100, 100, None).percent_received()))
+ pw_transfer.ProgressStats(100, 100, None).percent_received()
+ )
+ )
def test_str_known_total(self) -> None:
stats = str(pw_transfer.ProgressStats(75, 50, 100))
@@ -559,4 +1412,11 @@ class ProgressStatsTest(unittest.TestCase):
if __name__ == '__main__':
- unittest.main()
+ # TODO(b/265975025): Only run this test in upstream Pigweed until the
+ # occasional hangs are fixed.
+ if os.environ.get('PW_ROOT') and os.environ.get(
+ 'PW_ROOT'
+ ) == os.environ.get('PW_PROJECT_ROOT'):
+ unittest.main()
+ else:
+ print('Skipping transfer_test.py due to possible hangs (b/265975025).')
diff --git a/pw_transfer/read.svg b/pw_transfer/read.svg
index f2a97cadd..49dcb1b66 100644
--- a/pw_transfer/read.svg
+++ b/pw_transfer/read.svg
@@ -5,31 +5,31 @@
client -> server [
label = "set transfer parameters",
- leftnote = "transfer_id\noffset\nwindow_end_offset\ntype=PARAMETERS_RETRANSMIT\nmax_chunk_size\nchunk_delay"
+ leftnote = "session_id\noffset\nwindow_end_offset\ntype=PARAMETERS_RETRANSMIT\nmax_chunk_size\nchunk_delay"
];
client <-\- server [
noactivate,
label = "requested bytes\n(zero or more chunks)",
- rightnote = "transfer_id\noffset\ndata\n(remaining_bytes)"
+ rightnote = "session_id\noffset\ndata\n(remaining_bytes)"
];
client -\-> server [
noactivate,
label = "update transfer parameters\n(as needed)",
- leftnote = "transfer_id\noffset\nwindow_end_offset\ntype=PARAMETERS_CONTINUE\n(max_chunk_size)\n(chunk_delay)"
+ leftnote = "session_id\noffset\nwindow_end_offset\ntype=PARAMETERS_CONTINUE\n(max_chunk_size)\n(chunk_delay)"
];
client <- server [
noactivate,
label = "final chunk",
- rightnote = "transfer_id\noffset\ndata\nremaining_bytes=0"
+ rightnote = "session_id\noffset\ndata\nremaining_bytes=0"
];
client -> server [
noactivate,
label = "acknowledge completion",
- leftnote = "transfer_id\nstatus=OK"
+ leftnote = "session_id\nstatus=OK"
];
}
@@ -64,7 +64,7 @@
<polygon fill="rgb(240,248,255)" points="24,120 198,120 206,128 206,200 24,200 24,120" stroke="rgb(0,0,0)"></polygon>
<path d="M 198 120 L 198 128" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 198 128 L 206 128" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="65" y="133">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="65" y="133">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="50" y="146">offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="103" x="83" y="159">window_end_offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="158" x="111" y="172">type=PARAMETERS_RETRANSMIT</text>
@@ -75,7 +75,7 @@
<polygon fill="rgb(240,248,255)" points="430,240 549,240 557,248 557,294 430,294 430,240" stroke="rgb(0,0,0)"></polygon>
<path d="M 549 240 L 549 248" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 549 248 L 557 248" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="471" y="253">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="471" y="253">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="456" y="266">offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="25" x="450" y="279">data</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="103" x="489" y="292">(remaining_bytes)</text>
@@ -84,7 +84,7 @@
<polygon fill="rgb(240,248,255)" points="36,334 198,334 206,342 206,414 36,414 36,334" stroke="rgb(0,0,0)"></polygon>
<path d="M 198 334 L 198 342" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 198 342 L 206 342" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="77" y="347">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="77" y="347">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="62" y="360">offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="103" x="95" y="373">window_end_offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="117" y="386">type=PARAMETERS_CONTINUE</text>
@@ -95,7 +95,7 @@
<polygon fill="rgb(240,248,255)" points="430,454 549,454 557,462 557,508 430,508 430,454" stroke="rgb(0,0,0)"></polygon>
<path d="M 549 454 L 549 462" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 549 462 L 557 462" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="471" y="467">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="471" y="467">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="456" y="480">offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="25" x="450" y="493">data</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="103" x="489" y="506">remaining_bytes=0</text>
@@ -104,7 +104,7 @@
<polygon fill="rgb(240,248,255)" points="115,549 198,549 206,557 206,577 115,577 115,549" stroke="rgb(0,0,0)"></polygon>
<path d="M 198 549 L 198 557" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 198 557 L 206 557" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="156" y="562">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="156" y="562">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="150" y="575">status=OK</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="304" y="158">set transfer parameters</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="91" x="360" y="252">requested bytes</text>
diff --git a/pw_transfer/test_rpc_server.cc b/pw_transfer/test_rpc_server.cc
index e0f8e6788..fa911b732 100644
--- a/pw_transfer/test_rpc_server.cc
+++ b/pw_transfer/test_rpc_server.cc
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -14,7 +14,7 @@
// Simple RPC server with the transfer service registered. Reads HDLC frames
// with RPC packets through a socket. The transfer service reads and writes to
-// files within a given directory. The name of a file is its transfer ID.
+// files within a given directory. The name of a file is its resource ID.
#include <cstddef>
#include <filesystem>
@@ -30,57 +30,26 @@
#include "pw_stream/std_file_stream.h"
#include "pw_thread/detached_thread.h"
#include "pw_thread_stl/options.h"
+#include "pw_transfer/atomic_file_transfer_handler.h"
#include "pw_transfer/transfer.h"
#include "pw_transfer_test/test_server.raw_rpc.pb.h"
namespace pw::transfer {
namespace {
-
-class FileTransferHandler final : public ReadWriteHandler {
- public:
- FileTransferHandler(TransferService& service,
- uint32_t transfer_id,
- const char* path)
- : ReadWriteHandler(transfer_id), service_(service), path_(path) {
- service_.RegisterHandler(*this);
- }
-
- ~FileTransferHandler() { service_.UnregisterHandler(*this); }
-
- Status PrepareRead() final {
- PW_LOG_DEBUG("Preparing read for file %s", path_.c_str());
- set_reader(stream_.emplace<stream::StdFileReader>(path_.c_str()));
- return OkStatus();
- }
-
- void FinalizeRead(Status) final {
- std::get<stream::StdFileReader>(stream_).Close();
- }
-
- Status PrepareWrite() final {
- PW_LOG_DEBUG("Preparing write for file %s", path_.c_str());
- set_writer(stream_.emplace<stream::StdFileWriter>(path_.c_str()));
- return OkStatus();
- }
-
- Status FinalizeWrite(Status) final {
- std::get<stream::StdFileWriter>(stream_).Close();
- return OkStatus();
- }
-
- private:
- TransferService& service_;
- std::string path_;
- std::variant<std::monostate, stream::StdFileReader, stream::StdFileWriter>
- stream_;
-};
-
class TestServerService
: public pw_rpc::raw::TestServer::Service<TestServerService> {
public:
TestServerService(TransferService& transfer_service)
: transfer_service_(transfer_service) {}
+ ~TestServerService() { UnregisterHandlers(); }
+
+ void UnregisterHandlers() {
+ for (auto handler : file_transfer_handlers_) {
+ transfer_service_.UnregisterHandler(*handler);
+ }
+ }
+
void set_directory(const char* directory) { directory_ = directory; }
void ReloadTransferFiles(ConstByteSpan, rpc::RawUnaryResponder&) {
@@ -89,6 +58,7 @@ class TestServerService
void LoadFileHandlers() {
PW_LOG_INFO("Reloading file handlers from %s", directory_.c_str());
+ UnregisterHandlers();
file_transfer_handlers_.clear();
for (const auto& entry : std::filesystem::directory_iterator(directory_)) {
@@ -96,12 +66,13 @@ class TestServerService
continue;
}
- int transfer_id = std::atoi(entry.path().filename().c_str());
- if (transfer_id > 0) {
- PW_LOG_DEBUG("Found transfer file %d", transfer_id);
- file_transfer_handlers_.emplace_back(
- std::make_shared<FileTransferHandler>(
- transfer_service_, transfer_id, entry.path().c_str()));
+ int resource_id = std::atoi(entry.path().filename().c_str());
+ if (resource_id > 0) {
+ PW_LOG_DEBUG("Found transfer file %d", resource_id);
+ auto handler = std::make_shared<AtomicFileTransferHandler>(
+ resource_id, entry.path().c_str());
+ transfer_service_.RegisterHandler(*handler);
+ file_transfer_handlers_.emplace_back(handler);
}
}
}
@@ -109,7 +80,8 @@ class TestServerService
private:
TransferService& transfer_service_;
std::string directory_;
- std::vector<std::shared_ptr<FileTransferHandler>> file_transfer_handlers_;
+ std::vector<std::shared_ptr<AtomicFileTransferHandler>>
+ file_transfer_handlers_;
};
constexpr size_t kChunkSizeBytes = 256;
diff --git a/pw_transfer/transfer.cc b/pw_transfer/transfer.cc
index 142ecb364..722ed8217 100644
--- a/pw_transfer/transfer.cc
+++ b/pw_transfer/transfer.cc
@@ -23,24 +23,30 @@ namespace pw::transfer {
void TransferService::HandleChunk(ConstByteSpan message,
internal::TransferType type) {
- internal::Chunk chunk;
- if (Status status = internal::DecodeChunk(message, chunk); !status.ok()) {
- PW_LOG_ERROR("Failed to decode transfer chunk: %d", status.code());
+ Result<internal::Chunk> chunk = internal::Chunk::Parse(message);
+ if (!chunk.ok()) {
+ PW_LOG_ERROR("Failed to decode transfer chunk: %d", chunk.status().code());
return;
}
- if (chunk.IsInitialChunk()) {
- // TODO(frolv): Right now, transfer ID and handler ID are the same thing.
- // The transfer ID should be made into a unique session ID instead.
+ if (chunk->IsInitialChunk()) {
+ uint32_t session_id =
+ chunk->is_legacy() ? chunk->session_id() : GenerateNewSessionId();
+ uint32_t resource_id =
+ chunk->is_legacy() ? chunk->session_id() : chunk->resource_id().value();
+
thread_.StartServerTransfer(type,
- chunk.transfer_id,
- chunk.transfer_id,
+ chunk->protocol_version(),
+ session_id,
+ resource_id,
+ message,
max_parameters_,
chunk_timeout_,
- max_retries_);
+ max_retries_,
+ max_lifetime_retries_);
+ } else {
+ thread_.ProcessServerChunk(message);
}
-
- thread_.ProcessServerChunk(message);
}
} // namespace pw::transfer
diff --git a/pw_transfer/transfer.proto b/pw_transfer/transfer.proto
index 6f598a071..839eb2cd2 100644
--- a/pw_transfer/transfer.proto
+++ b/pw_transfer/transfer.proto
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -16,6 +16,9 @@ syntax = "proto3";
package pw.transfer;
+option java_multiple_files = true;
+option java_package = "dev.pigweed.pw_transfer";
+
// The transfer RPC service is used to send data between the client and server.
service Transfer {
// Transfer data from the server to the client; a "download" from the client's
@@ -38,6 +41,8 @@ message Chunk {
// stable depending on the implementation. Sent in every request to identify
// the transfer target.
//
+ // LEGACY FIELD ONLY. Split into resource_id and session_id in transfer v2.
+ //
// Read → ID of transfer
// Read ← ID of transfer
// Write → ID of transfer
@@ -112,10 +117,10 @@ message Chunk {
// OK: Transfer completed successfully.
// DATA_LOSS: Transfer data could not be read/written (e.g. corruption).
// INVALID_ARGUMENT: Received malformed chunk.
- // NOT_FOUND: The requested transfer ID is not registered (read/write).
+ // NOT_FOUND: The requested resource ID is not registered (read/write).
// OUT_OF_RANGE: The requested offset is larger than the data (read/write).
// RESOURCE_EXHAUSTED: Concurrent transfer limit reached.
- // UNIMPLEMENTED: Transfer ID does not support requested operation (e.g.
+ // UNIMPLEMENTED: Resource ID does not support requested operation (e.g.
// trying to write to a read-only transfer).
//
// Read → Transfer complete.
@@ -138,10 +143,10 @@ message Chunk {
enum Type {
// Chunk containing transfer data.
- TRANSFER_DATA = 0;
+ DATA = 0;
// First chunk of a transfer (only sent by the client).
- TRANSFER_START = 1;
+ START = 1;
// Transfer parameters indicating that the transmitter should retransmit
// from the specified offset.
@@ -153,11 +158,21 @@ message Chunk {
PARAMETERS_CONTINUE = 3;
// Sender of the chunk is terminating the transfer.
- TRANSFER_COMPLETION = 4;
+ COMPLETION = 4;
// Acknowledge the completion of a transfer. Currently unused.
// TODO(konkers): Implement this behavior.
- TRANSFER_COMPLETION_ACK = 5;
+ COMPLETION_ACK = 5;
+
+ // Acknowledges a transfer start request, assigning a session ID for the
+ // transfer and optionally negotiating the protocol version. Sent from
+ // server to client.
+ START_ACK = 6;
+
+ // Confirmation of a START_ACK's assigned session ID and negotiated
+ // parameters, sent by the client to the server. Initiates the data transfer
+ // proper.
+ START_ACK_CONFIRMATION = 7;
};
// The type of this chunk. This field should only be processed when present.
@@ -169,4 +184,34 @@ message Chunk {
// Write → Chunk type (data).
// Write ← Chunk type (start/parameters).
optional Type type = 10;
+
+ // Unique identifier for the source or destination of transfer data. May be
+ // stable or ephemeral depending on the implementation. Only sent during the
+ // initial handshake phase of a version 2 or higher transfer.
+ //
+ // Read → ID of transferable resource
+ // Read ← ID of transferable resource
+ // Write → ID of transferable resource
+ // Write ← ID of transferable resource
+ optional uint32 resource_id = 11;
+
+ // Unique identifier for a specific transfer session. Assigned by a transfer
+ // service during the initial handshake phase, and persists for the remainder
+ // of that transfer operation.
+ //
+ // Read → ID of transfer session
+ // Read ← ID of transfer session
+ // Write → ID of transfer session
+ // Write ← ID of transfer session
+ optional uint32 session_id = 12;
+
+ // The protocol version to use for this transfer. Only sent during the initial
+ // handshake phase of a version 2 or higher transfer to negotiate a common
+ // protocol version between the client and server.
+ //
+ // Read → Desired (START) or configured (START_ACK_CONFIRMATION) version.
+ // Read ← Configured protocol version (START_ACK).
+ // Write → Desired (START) or configured (START_ACK_CONFIRMATION) version.
+ // Write ← Configured protocol version (START_ACK).
+ optional uint32 protocol_version = 13;
}
diff --git a/pw_transfer/transfer_test.cc b/pw_transfer/transfer_test.cc
index 76cfb0113..8b5312107 100644
--- a/pw_transfer/transfer_test.cc
+++ b/pw_transfer/transfer_test.cc
@@ -15,9 +15,11 @@
#include "pw_transfer/transfer.h"
#include "gtest/gtest.h"
+#include "pw_assert/check.h"
#include "pw_bytes/array.h"
+#include "pw_containers/algorithm.h"
#include "pw_rpc/raw/test_method_context.h"
-#include "pw_rpc/thread_testing.h"
+#include "pw_rpc/test_helpers.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
#include "pw_transfer/transfer.pwpb.h"
@@ -28,9 +30,6 @@ namespace {
using namespace std::chrono_literals;
-PW_MODIFY_DIAGNOSTICS_PUSH();
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
-
// TODO(frolv): Have a generic way to obtain a thread for testing on any system.
thread::Options& TransferThreadOptions() {
static thread::stl::Options options;
@@ -41,7 +40,7 @@ using internal::Chunk;
class TestMemoryReader : public stream::SeekableReader {
public:
- constexpr TestMemoryReader(std::span<const std::byte> data)
+ constexpr TestMemoryReader(span<const std::byte> data)
: memory_reader_(data) {}
Status DoSeek(ptrdiff_t offset, Whence origin) override {
@@ -70,8 +69,8 @@ class TestMemoryReader : public stream::SeekableReader {
class SimpleReadTransfer final : public ReadOnlyHandler {
public:
- SimpleReadTransfer(uint32_t transfer_id, ConstByteSpan data)
- : ReadOnlyHandler(transfer_id),
+ SimpleReadTransfer(uint32_t session_id, ConstByteSpan data)
+ : ReadOnlyHandler(session_id),
prepare_read_called(false),
finalize_read_called(false),
finalize_read_status(Status::Unknown()),
@@ -112,20 +111,20 @@ class ReadTransfer : public ::testing::Test {
protected:
ReadTransfer(size_t max_chunk_size_bytes = 64)
: handler_(3, kData),
- transfer_thread_(std::span(data_buffer_).first(max_chunk_size_bytes),
+ transfer_thread_(span(data_buffer_).first(max_chunk_size_bytes),
encode_buffer_),
ctx_(transfer_thread_, 64),
system_thread_(TransferThreadOptions(), transfer_thread_) {
ctx_.service().RegisterHandler(handler_);
- ASSERT_FALSE(handler_.prepare_read_called);
- ASSERT_FALSE(handler_.finalize_read_called);
+ PW_CHECK(!handler_.prepare_read_called);
+ PW_CHECK(!handler_.finalize_read_called);
ctx_.call(); // Open the read stream
transfer_thread_.WaitUntilEventIsProcessed();
}
- ~ReadTransfer() {
+ ~ReadTransfer() override {
transfer_thread_.Terminate();
system_thread_.join();
}
@@ -140,47 +139,11 @@ class ReadTransfer : public ::testing::Test {
TEST_F(ReadTransfer, SingleChunk) {
rpc::test::WaitForPackets(ctx_.output(), 2, [this] {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .window_end_offset = 64,
- .pending_bytes = 64,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
-
- transfer_thread_.WaitUntilEventIsProcessed();
- });
-
- EXPECT_TRUE(handler_.prepare_read_called);
- EXPECT_FALSE(handler_.finalize_read_called);
-
- ASSERT_EQ(ctx_.total_responses(), 2u);
- Chunk c0 = DecodeChunk(ctx_.responses()[0]);
- Chunk c1 = DecodeChunk(ctx_.responses()[1]);
-
- // First chunk should have all the read data.
- EXPECT_EQ(c0.transfer_id, 3u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.data.size(), kData.size());
- EXPECT_EQ(std::memcmp(c0.data.data(), kData.data(), c0.data.size()), 0);
-
- // Second chunk should be empty and set remaining_bytes = 0.
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.data.size(), 0u);
- ASSERT_TRUE(c1.remaining_bytes.has_value());
- EXPECT_EQ(c1.remaining_bytes.value(), 0u);
-
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
- transfer_thread_.WaitUntilEventIsProcessed();
-
- EXPECT_TRUE(handler_.finalize_read_called);
- EXPECT_EQ(handler_.finalize_read_status, OkStatus());
-}
-
-TEST_F(ReadTransfer, PendingBytes_SingleChunk) {
- rpc::test::WaitForPackets(ctx_.output(), 2, [this] {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .pending_bytes = 64,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(64)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
});
@@ -193,18 +156,20 @@ TEST_F(ReadTransfer, PendingBytes_SingleChunk) {
Chunk c1 = DecodeChunk(ctx_.responses()[1]);
// First chunk should have all the read data.
- EXPECT_EQ(c0.transfer_id, 3u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.data.size(), kData.size());
- EXPECT_EQ(std::memcmp(c0.data.data(), kData.data(), c0.data.size()), 0);
+ EXPECT_EQ(c0.session_id(), 3u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.payload().size(), kData.size());
+ EXPECT_EQ(std::memcmp(c0.payload().data(), kData.data(), c0.payload().size()),
+ 0);
// Second chunk should be empty and set remaining_bytes = 0.
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.data.size(), 0u);
- ASSERT_TRUE(c1.remaining_bytes.has_value());
- EXPECT_EQ(c1.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c1.session_id(), 3u);
+ EXPECT_FALSE(c1.has_payload());
+ ASSERT_TRUE(c1.remaining_bytes().has_value());
+ EXPECT_EQ(c1.remaining_bytes().value(), 0u);
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -212,11 +177,11 @@ TEST_F(ReadTransfer, PendingBytes_SingleChunk) {
}
TEST_F(ReadTransfer, MultiChunk) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .window_end_offset = 16,
- .pending_bytes = 16,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -224,46 +189,48 @@ TEST_F(ReadTransfer, MultiChunk) {
EXPECT_FALSE(handler_.finalize_read_called);
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk c0 = DecodeChunk(ctx_.responses()[0]);
+ Chunk c0 = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(c0.transfer_id, 3u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.data.size(), 16u);
- EXPECT_EQ(std::memcmp(c0.data.data(), kData.data(), c0.data.size()), 0);
+ EXPECT_EQ(c0.session_id(), 3u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.payload().size(), 16u);
+ EXPECT_EQ(std::memcmp(c0.payload().data(), kData.data(), c0.payload().size()),
+ 0);
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3,
- .window_end_offset = 32,
- .pending_bytes = 16,
- .offset = 16,
- .type = Chunk::Type::kParametersContinue}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersContinue)
+ .set_session_id(3)
+ .set_window_end_offset(32)
+ .set_offset(16)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
- Chunk c1 = DecodeChunk(ctx_.responses()[1]);
+ Chunk c1 = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.offset, 16u);
- ASSERT_EQ(c1.data.size(), 16u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData.data() + 16, c1.data.size()), 0);
+ EXPECT_EQ(c1.session_id(), 3u);
+ EXPECT_EQ(c1.offset(), 16u);
+ ASSERT_EQ(c1.payload().size(), 16u);
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData.data() + 16, c1.payload().size()),
+ 0);
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3,
- .window_end_offset = 48,
- .pending_bytes = 16,
- .offset = 32,
- .type = Chunk::Type::kParametersContinue}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersContinue)
+ .set_session_id(3)
+ .set_window_end_offset(48)
+ .set_offset(32)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
Chunk c2 = DecodeChunk(ctx_.responses()[2]);
- EXPECT_EQ(c2.transfer_id, 3u);
- EXPECT_EQ(c2.data.size(), 0u);
- ASSERT_TRUE(c2.remaining_bytes.has_value());
- EXPECT_EQ(c2.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c2.session_id(), 3u);
+ EXPECT_FALSE(c2.has_payload());
+ ASSERT_TRUE(c2.remaining_bytes().has_value());
+ EXPECT_EQ(c2.remaining_bytes().value(), 0u);
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -271,20 +238,19 @@ TEST_F(ReadTransfer, MultiChunk) {
}
TEST_F(ReadTransfer, MultiChunk_RepeatedContinuePackets) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .window_end_offset = 16,
- .pending_bytes = 16,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
- const auto continue_chunk =
- EncodeChunk({.transfer_id = 3,
- .window_end_offset = 24,
- .pending_bytes = 8,
- .offset = 16,
- .type = Chunk::Type::kParametersContinue});
+ const auto continue_chunk = EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersContinue)
+ .set_session_id(3)
+ .set_window_end_offset(24)
+ .set_offset(16));
ctx_.SendClientStream(continue_chunk);
transfer_thread_.WaitUntilEventIsProcessed();
@@ -298,112 +264,82 @@ TEST_F(ReadTransfer, MultiChunk_RepeatedContinuePackets) {
ASSERT_EQ(ctx_.total_responses(), 2u); // Only sent one packet
Chunk c1 = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.offset, 16u);
- ASSERT_EQ(c1.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData.data() + 16, c1.data.size()), 0);
-}
-
-TEST_F(ReadTransfer, PendingBytes_MultiChunk) {
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 16, .offset = 0}));
-
- transfer_thread_.WaitUntilEventIsProcessed();
-
- EXPECT_TRUE(handler_.prepare_read_called);
- EXPECT_FALSE(handler_.finalize_read_called);
-
- ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk c0 = DecodeChunk(ctx_.responses()[0]);
-
- EXPECT_EQ(c0.transfer_id, 3u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.data.size(), 16u);
- EXPECT_EQ(std::memcmp(c0.data.data(), kData.data(), c0.data.size()), 0);
-
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 16, .offset = 16}));
- transfer_thread_.WaitUntilEventIsProcessed();
-
- ASSERT_EQ(ctx_.total_responses(), 2u);
- Chunk c1 = DecodeChunk(ctx_.responses()[1]);
-
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.offset, 16u);
- ASSERT_EQ(c1.data.size(), 16u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData.data() + 16, c1.data.size()), 0);
-
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 16, .offset = 32}));
- transfer_thread_.WaitUntilEventIsProcessed();
-
- ASSERT_EQ(ctx_.total_responses(), 3u);
- Chunk c2 = DecodeChunk(ctx_.responses()[2]);
-
- EXPECT_EQ(c2.transfer_id, 3u);
- EXPECT_EQ(c2.data.size(), 0u);
- ASSERT_TRUE(c2.remaining_bytes.has_value());
- EXPECT_EQ(c2.remaining_bytes.value(), 0u);
-
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
- transfer_thread_.WaitUntilEventIsProcessed();
-
- EXPECT_TRUE(handler_.finalize_read_called);
- EXPECT_EQ(handler_.finalize_read_status, OkStatus());
+ EXPECT_EQ(c1.session_id(), 3u);
+ EXPECT_EQ(c1.offset(), 16u);
+ ASSERT_EQ(c1.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData.data() + 16, c1.payload().size()),
+ 0);
}
TEST_F(ReadTransfer, OutOfOrder_SeekingSupported) {
rpc::test::WaitForPackets(ctx_.output(), 4, [this] {
ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 16, .offset = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_TRUE(std::equal(
- &kData[0], &kData[16], chunk.data.begin(), chunk.data.end()));
+ EXPECT_TRUE(pw::containers::Equal(span(kData).first(16), chunk.payload()));
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 8, .offset = 2}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_window_end_offset(10)
+ .set_offset(2)));
transfer_thread_.WaitUntilEventIsProcessed();
chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_TRUE(std::equal(
- &kData[2], &kData[10], chunk.data.begin(), chunk.data.end()));
-
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 64, .offset = 17}));
+ EXPECT_TRUE(
+ pw::containers::Equal(span(kData).subspan(2, 8), chunk.payload()));
+
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_window_end_offset(64)
+ .set_offset(17)));
});
ASSERT_EQ(ctx_.total_responses(), 4u);
Chunk chunk = DecodeChunk(ctx_.responses()[2]);
- EXPECT_TRUE(std::equal(
- &kData[17], kData.end(), chunk.data.begin(), chunk.data.end()));
+ EXPECT_TRUE(
+ pw::containers::Equal(span(&kData[17], kData.end()), chunk.payload()));
}
TEST_F(ReadTransfer, OutOfOrder_SeekingNotSupported_EndsWithUnimplemented) {
handler_.set_seek_status(Status::Unimplemented());
ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 16, .offset = 0}));
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 8, .offset = 2}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_offset(0)));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_window_end_offset(10)
+ .set_offset(2)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.status, Status::Unimplemented());
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::Unimplemented());
}
TEST_F(ReadTransfer, MaxChunkSize_Client) {
rpc::test::WaitForPackets(ctx_.output(), 5, [this] {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .pending_bytes = 64,
- .max_chunk_size_bytes = 8,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(8)
+ .set_offset(0)));
});
EXPECT_TRUE(handler_.prepare_read_called);
@@ -416,32 +352,40 @@ TEST_F(ReadTransfer, MaxChunkSize_Client) {
Chunk c3 = DecodeChunk(ctx_.responses()[3]);
Chunk c4 = DecodeChunk(ctx_.responses()[4]);
- EXPECT_EQ(c0.transfer_id, 3u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c0.data.data(), kData.data(), c0.data.size()), 0);
-
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.offset, 8u);
- ASSERT_EQ(c1.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData.data() + 8, c1.data.size()), 0);
-
- EXPECT_EQ(c2.transfer_id, 3u);
- EXPECT_EQ(c2.offset, 16u);
- ASSERT_EQ(c2.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c2.data.data(), kData.data() + 16, c2.data.size()), 0);
-
- EXPECT_EQ(c3.transfer_id, 3u);
- EXPECT_EQ(c3.offset, 24u);
- ASSERT_EQ(c3.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c3.data.data(), kData.data() + 24, c3.data.size()), 0);
-
- EXPECT_EQ(c4.transfer_id, 3u);
- EXPECT_EQ(c4.data.size(), 0u);
- ASSERT_TRUE(c4.remaining_bytes.has_value());
- EXPECT_EQ(c4.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c0.session_id(), 3u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.payload().size(), 8u);
+ EXPECT_EQ(std::memcmp(c0.payload().data(), kData.data(), c0.payload().size()),
+ 0);
+
+ EXPECT_EQ(c1.session_id(), 3u);
+ EXPECT_EQ(c1.offset(), 8u);
+ ASSERT_EQ(c1.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData.data() + 8, c1.payload().size()),
+ 0);
+
+ EXPECT_EQ(c2.session_id(), 3u);
+ EXPECT_EQ(c2.offset(), 16u);
+ ASSERT_EQ(c2.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c2.payload().data(), kData.data() + 16, c2.payload().size()),
+ 0);
+
+ EXPECT_EQ(c3.session_id(), 3u);
+ EXPECT_EQ(c3.offset(), 24u);
+ ASSERT_EQ(c3.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c3.payload().data(), kData.data() + 24, c3.payload().size()),
+ 0);
+
+ EXPECT_EQ(c4.session_id(), 3u);
+ EXPECT_EQ(c4.payload().size(), 0u);
+ ASSERT_TRUE(c4.remaining_bytes().has_value());
+ EXPECT_EQ(c4.remaining_bytes().value(), 0u);
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -449,12 +393,13 @@ TEST_F(ReadTransfer, MaxChunkSize_Client) {
}
TEST_F(ReadTransfer, HandlerIsClearedAfterTransfer) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .window_end_offset = 64,
- .pending_bytes = 64,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(64)
+ .set_offset(0)));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
@@ -467,11 +412,11 @@ TEST_F(ReadTransfer, HandlerIsClearedAfterTransfer) {
handler_.prepare_read_called = false;
handler_.finalize_read_called = false;
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .window_end_offset = 64,
- .pending_bytes = 64,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(64)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
// Prepare failed, so the handler should not have been stored in the context,
@@ -487,12 +432,14 @@ class ReadTransferMaxChunkSize8 : public ReadTransfer {
TEST_F(ReadTransferMaxChunkSize8, MaxChunkSize_Server) {
// Client asks for max 16-byte chunks, but service places a limit of 8 bytes.
+ // TODO(frolv): Fix
rpc::test::WaitForPackets(ctx_.output(), 5, [this] {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .pending_bytes = 64,
- .max_chunk_size_bytes = 16,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(64)
+ // .set_max_chunk_size_bytes(16)
+ .set_offset(0)));
});
EXPECT_TRUE(handler_.prepare_read_called);
@@ -505,32 +452,40 @@ TEST_F(ReadTransferMaxChunkSize8, MaxChunkSize_Server) {
Chunk c3 = DecodeChunk(ctx_.responses()[3]);
Chunk c4 = DecodeChunk(ctx_.responses()[4]);
- EXPECT_EQ(c0.transfer_id, 3u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c0.data.data(), kData.data(), c0.data.size()), 0);
+ EXPECT_EQ(c0.session_id(), 3u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.payload().size(), 8u);
+ EXPECT_EQ(std::memcmp(c0.payload().data(), kData.data(), c0.payload().size()),
+ 0);
+
+ EXPECT_EQ(c1.session_id(), 3u);
+ EXPECT_EQ(c1.offset(), 8u);
+ ASSERT_EQ(c1.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c1.payload().data(), kData.data() + 8, c1.payload().size()),
+ 0);
+
+ EXPECT_EQ(c2.session_id(), 3u);
+ EXPECT_EQ(c2.offset(), 16u);
+ ASSERT_EQ(c2.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c2.payload().data(), kData.data() + 16, c2.payload().size()),
+ 0);
+
+ EXPECT_EQ(c3.session_id(), 3u);
+ EXPECT_EQ(c3.offset(), 24u);
+ ASSERT_EQ(c3.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(c3.payload().data(), kData.data() + 24, c3.payload().size()),
+ 0);
+
+ EXPECT_EQ(c4.session_id(), 3u);
+ EXPECT_EQ(c4.payload().size(), 0u);
+ ASSERT_TRUE(c4.remaining_bytes().has_value());
+ EXPECT_EQ(c4.remaining_bytes().value(), 0u);
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.offset, 8u);
- ASSERT_EQ(c1.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c1.data.data(), kData.data() + 8, c1.data.size()), 0);
-
- EXPECT_EQ(c2.transfer_id, 3u);
- EXPECT_EQ(c2.offset, 16u);
- ASSERT_EQ(c2.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c2.data.data(), kData.data() + 16, c2.data.size()), 0);
-
- EXPECT_EQ(c3.transfer_id, 3u);
- EXPECT_EQ(c3.offset, 24u);
- ASSERT_EQ(c3.data.size(), 8u);
- EXPECT_EQ(std::memcmp(c3.data.data(), kData.data() + 24, c3.data.size()), 0);
-
- EXPECT_EQ(c4.transfer_id, 3u);
- EXPECT_EQ(c4.data.size(), 0u);
- ASSERT_TRUE(c4.remaining_bytes.has_value());
- EXPECT_EQ(c4.remaining_bytes.value(), 0u);
-
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -538,10 +493,11 @@ TEST_F(ReadTransferMaxChunkSize8, MaxChunkSize_Server) {
}
TEST_F(ReadTransfer, ClientError) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .pending_bytes = 16,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -550,8 +506,8 @@ TEST_F(ReadTransfer, ClientError) {
ASSERT_EQ(ctx_.total_responses(), 1u);
// Send client error.
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .status = Status::OutOfRange()}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk::Final(ProtocolVersion::kLegacy, 3, Status::OutOfRange())));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
@@ -559,53 +515,47 @@ TEST_F(ReadTransfer, ClientError) {
EXPECT_EQ(handler_.finalize_read_status, Status::OutOfRange());
}
-TEST_F(ReadTransfer, MalformedParametersChunk) {
- // pending_bytes is required in a parameters chunk.
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3}));
- transfer_thread_.WaitUntilEventIsProcessed();
-
- EXPECT_TRUE(handler_.prepare_read_called);
- EXPECT_TRUE(handler_.finalize_read_called);
- EXPECT_EQ(handler_.finalize_read_status, Status::InvalidArgument());
-
- ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 3u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::InvalidArgument());
-}
-
TEST_F(ReadTransfer, UnregisteredHandler) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 11,
- .pending_bytes = 32,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(11)
+ .set_window_end_offset(32)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 11u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::NotFound());
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 11u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::NotFound());
}
TEST_F(ReadTransfer, IgnoresNonPendingTransfers) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .offset = 3}));
ctx_.SendClientStream(EncodeChunk(
- {.transfer_id = 3, .offset = 0, .data = std::span(kData).first(10)}));
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_window_end_offset(32)
+ .set_offset(3)));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(3)
+ .set_payload(span(kData).first(10))
+ .set_offset(3)));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
- // Only start transfer for initial packet.
+ // Only start transfer for an initial packet.
EXPECT_FALSE(handler_.prepare_read_called);
EXPECT_FALSE(handler_.finalize_read_called);
}
TEST_F(ReadTransfer, AbortAndRestartIfInitialPacketIsReceived) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .pending_bytes = 16,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
@@ -615,10 +565,10 @@ TEST_F(ReadTransfer, AbortAndRestartIfInitialPacketIsReceived) {
handler_.prepare_read_called = false; // Reset so can check if called again.
ctx_.SendClientStream( // Resend starting chunk
- EncodeChunk({.transfer_id = 3,
- .pending_bytes = 16,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
@@ -628,9 +578,13 @@ TEST_F(ReadTransfer, AbortAndRestartIfInitialPacketIsReceived) {
EXPECT_EQ(handler_.finalize_read_status, Status::Aborted());
handler_.finalize_read_called = false; // Reset so can check later
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_window_end_offset(32)
+ .set_offset(16)));
ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 16, .offset = 16}));
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
@@ -639,9 +593,11 @@ TEST_F(ReadTransfer, AbortAndRestartIfInitialPacketIsReceived) {
}
TEST_F(ReadTransfer, ZeroPendingBytesWithRemainingData_Aborts) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .pending_bytes = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(0)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
@@ -649,23 +605,26 @@ TEST_F(ReadTransfer, ZeroPendingBytesWithRemainingData_Aborts) {
EXPECT_EQ(handler_.finalize_read_status, Status::ResourceExhausted());
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.status, Status::ResourceExhausted());
+ EXPECT_EQ(chunk.status(), Status::ResourceExhausted());
}
TEST_F(ReadTransfer, ZeroPendingBytesNoRemainingData_Completes) {
// Make the next read appear to be the end of the stream.
handler_.set_read_status(Status::OutOfRange());
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .pending_bytes = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(0)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 3u);
- EXPECT_EQ(chunk.remaining_bytes, 0u);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.remaining_bytes(), 0u);
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
@@ -675,10 +634,11 @@ TEST_F(ReadTransfer, ZeroPendingBytesNoRemainingData_Completes) {
TEST_F(ReadTransfer, SendsErrorIfChunkIsReceivedInCompletedState) {
rpc::test::WaitForPackets(ctx_.output(), 2, [this] {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3,
- .pending_bytes = 64,
- .offset = 0,
- .type = Chunk::Type::kTransferStart}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(3)
+ .set_window_end_offset(64)
+ .set_offset(0)));
});
EXPECT_TRUE(handler_.prepare_read_called);
@@ -689,18 +649,20 @@ TEST_F(ReadTransfer, SendsErrorIfChunkIsReceivedInCompletedState) {
Chunk c1 = DecodeChunk(ctx_.responses()[1]);
// First chunk should have all the read data.
- EXPECT_EQ(c0.transfer_id, 3u);
- EXPECT_EQ(c0.offset, 0u);
- ASSERT_EQ(c0.data.size(), kData.size());
- EXPECT_EQ(std::memcmp(c0.data.data(), kData.data(), c0.data.size()), 0);
+ EXPECT_EQ(c0.session_id(), 3u);
+ EXPECT_EQ(c0.offset(), 0u);
+ ASSERT_EQ(c0.payload().size(), kData.size());
+ EXPECT_EQ(std::memcmp(c0.payload().data(), kData.data(), c0.payload().size()),
+ 0);
// Second chunk should be empty and set remaining_bytes = 0.
- EXPECT_EQ(c1.transfer_id, 3u);
- EXPECT_EQ(c1.data.size(), 0u);
- ASSERT_TRUE(c1.remaining_bytes.has_value());
- EXPECT_EQ(c1.remaining_bytes.value(), 0u);
+ EXPECT_EQ(c1.session_id(), 3u);
+ EXPECT_FALSE(c1.has_payload());
+ ASSERT_TRUE(c1.remaining_bytes().has_value());
+ EXPECT_EQ(c1.remaining_bytes().value(), 0u);
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 3, .status = OkStatus()}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kLegacy, 3, OkStatus())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_read_called);
@@ -710,15 +672,18 @@ TEST_F(ReadTransfer, SendsErrorIfChunkIsReceivedInCompletedState) {
// non-initial chunk as a continuation of the transfer.
handler_.finalize_read_called = false;
- ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 3, .pending_bytes = 48, .offset = 16}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_window_end_offset(64)
+ .set_offset(16)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
Chunk c2 = DecodeChunk(ctx_.responses()[2]);
- ASSERT_TRUE(c2.status.has_value());
- EXPECT_EQ(c2.status.value(), Status::FailedPrecondition());
+ ASSERT_TRUE(c2.status().has_value());
+ EXPECT_EQ(c2.status().value(), Status::FailedPrecondition());
// FinalizeRead should not be called again.
EXPECT_FALSE(handler_.finalize_read_called);
@@ -726,8 +691,8 @@ TEST_F(ReadTransfer, SendsErrorIfChunkIsReceivedInCompletedState) {
class SimpleWriteTransfer final : public WriteOnlyHandler {
public:
- SimpleWriteTransfer(uint32_t transfer_id, ByteSpan data)
- : WriteOnlyHandler(transfer_id),
+ SimpleWriteTransfer(uint32_t session_id, ByteSpan data)
+ : WriteOnlyHandler(session_id),
prepare_write_called(false),
finalize_write_called(false),
finalize_write_status(Status::Unknown()),
@@ -772,14 +737,14 @@ class WriteTransfer : public ::testing::Test {
std::chrono::minutes(1)) {
ctx_.service().RegisterHandler(handler_);
- ASSERT_FALSE(handler_.prepare_write_called);
- ASSERT_FALSE(handler_.finalize_write_called);
+ PW_CHECK(!handler_.prepare_write_called);
+ PW_CHECK(!handler_.finalize_write_called);
ctx_.call(); // Open the write stream
transfer_thread_.WaitUntilEventIsProcessed();
}
- ~WriteTransfer() {
+ ~WriteTransfer() override {
transfer_thread_.Terminate();
system_thread_.join();
}
@@ -795,31 +760,33 @@ class WriteTransfer : public ::testing::Test {
};
TEST_F(WriteTransfer, SingleChunk) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 32u);
- ASSERT_TRUE(chunk.max_chunk_size_bytes.has_value());
- EXPECT_EQ(chunk.max_chunk_size_bytes.value(), 37u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+ ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
+ EXPECT_EQ(chunk.max_chunk_size_bytes().value(), 37u);
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 0,
- .data = std::span(kData),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
- chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), OkStatus());
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
EXPECT_TRUE(handler_.finalize_write_called);
EXPECT_EQ(handler_.finalize_write_status, OkStatus());
@@ -830,43 +797,48 @@ TEST_F(WriteTransfer, FinalizeFails) {
// Return an error when FinalizeWrite is called.
handler_.set_finalize_write_return(Status::FailedPrecondition());
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 0,
- .data = std::span(kData),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
Chunk chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::DataLoss());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::DataLoss());
EXPECT_TRUE(handler_.finalize_write_called);
EXPECT_EQ(handler_.finalize_write_status, OkStatus());
}
TEST_F(WriteTransfer, SendingFinalPacketFails) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
ctx_.output().set_send_status(Status::Unknown());
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 0,
- .data = std::span(kData),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
// Should only have sent the transfer parameters.
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 32u);
- ASSERT_TRUE(chunk.max_chunk_size_bytes.has_value());
- EXPECT_EQ(chunk.max_chunk_size_bytes.value(), 37u);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+ ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
+ EXPECT_EQ(chunk.max_chunk_size_bytes().value(), 37u);
// When FinalizeWrite() was called, the transfer was considered successful.
EXPECT_TRUE(handler_.finalize_write_called);
@@ -874,7 +846,8 @@ TEST_F(WriteTransfer, SendingFinalPacketFails) {
}
TEST_F(WriteTransfer, MultiChunk) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
@@ -882,27 +855,31 @@ TEST_F(WriteTransfer, MultiChunk) {
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 32u);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = std::span(kData).first(8)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(span(kData).first(8))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 8,
- .data = std::span(kData).subspan(8),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(8)
+ .set_payload(span(kData).subspan(8))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
- chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), OkStatus());
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
EXPECT_TRUE(handler_.finalize_write_called);
EXPECT_EQ(handler_.finalize_write_status, OkStatus());
@@ -916,7 +893,9 @@ TEST_F(WriteTransfer, WriteFailsOnRetry) {
// Wait for 3 packets: initial params, retry attempt, final error
rpc::test::WaitForPackets(ctx_.output(), 3, [this] {
// Send only one client packet so the service times out.
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(7)));
transfer_thread_.SimulateServerTimeout(7); // Time out to trigger retry
});
@@ -924,39 +903,44 @@ TEST_F(WriteTransfer, WriteFailsOnRetry) {
// Check that the last packet is an INTERNAL error from the RPC write failure.
ASSERT_EQ(ctx_.total_responses(), 2u);
Chunk chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::Internal());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::Internal());
}
TEST_F(WriteTransfer, TimeoutInRecoveryState) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 0u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 32u);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
- constexpr std::span data(kData);
+ constexpr span data(kData);
- ctx_.SendClientStream<64>(
- EncodeChunk({.transfer_id = 7, .offset = 0, .data = data.first(8)}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(data.first(8))));
// Skip offset 8 to enter a recovery state.
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 12, .data = data.subspan(12, 4)}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(12)
+ .set_payload(data.subspan(12, 4))));
transfer_thread_.WaitUntilEventIsProcessed();
// Recovery parameters should be sent for offset 8.
ASSERT_EQ(ctx_.total_responses(), 2u);
chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 8u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 24u);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 8u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
// Timeout while in the recovery state.
transfer_thread_.SimulateServerTimeout(7);
@@ -965,68 +949,76 @@ TEST_F(WriteTransfer, TimeoutInRecoveryState) {
// Same recovery parameters should be re-sent.
ASSERT_EQ(ctx_.total_responses(), 3u);
chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 8u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 24u);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 8u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
}
TEST_F(WriteTransfer, ExtendWindow) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.window_end_offset, 32u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 32u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
// Window starts at 32 bytes and should extend when half of that is sent.
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = std::span(kData).first(4)}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(span(kData).first(4))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 4, .data = std::span(kData).subspan(4, 4)}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(4)
+ .set_payload(span(kData).subspan(4, 4))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 8, .data = std::span(kData).subspan(8, 4)}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(8)
+ .set_payload(span(kData).subspan(8, 4))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- ctx_.SendClientStream<64>(
- EncodeChunk({.transfer_id = 7,
- .offset = 12,
- .data = std::span(kData).subspan(12, 4)}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(12)
+ .set_payload(span(kData).subspan(12, 4))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
// Extend parameters chunk.
- chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.window_end_offset, 32u);
- EXPECT_EQ(chunk.type, Chunk::Type::kParametersContinue);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 16u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kParametersContinue);
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 16,
- .data = std::span(kData).subspan(16),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(16)
+ .set_payload(span(kData).subspan(16))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
chunk = DecodeChunk(ctx_.responses()[2]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), OkStatus());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
EXPECT_TRUE(handler_.finalize_write_called);
EXPECT_EQ(handler_.finalize_write_status, OkStatus());
@@ -1039,7 +1031,8 @@ class WriteTransferMaxBytes16 : public WriteTransfer {
};
TEST_F(WriteTransfer, TransmitterReducesWindow) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
@@ -1047,28 +1040,31 @@ TEST_F(WriteTransfer, TransmitterReducesWindow) {
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.window_end_offset, 32u);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
// Send only 12 bytes and set that as the new end offset.
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .window_end_offset = 12,
- .offset = 0,
- .data = std::span(kData).first(12)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_window_end_offset(12)
+ .set_payload(span(kData).first(12))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
// Receiver should respond immediately with a retransmit chunk as the end of
// the window has been reached.
chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 12u);
- EXPECT_EQ(chunk.window_end_offset, 32u);
- EXPECT_EQ(chunk.type, Chunk::Type::kParametersRetransmit);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 12u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
}
TEST_F(WriteTransfer, TransmitterExtendsWindow_TerminatesWithInvalid) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
@@ -1076,164 +1072,178 @@ TEST_F(WriteTransfer, TransmitterExtendsWindow_TerminatesWithInvalid) {
ASSERT_EQ(ctx_.total_responses(), 1u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.window_end_offset, 32u);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
- // Send only 12 bytes and set that as the new end offset.
ctx_.SendClientStream<64>(
- EncodeChunk({.transfer_id = 7,
- // Larger window end offset than the receiver's.
- .window_end_offset = 48,
- .offset = 0,
- .data = std::span(kData).first(16)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ // Larger window end offset than the receiver's.
+ .set_window_end_offset(48)
+ .set_payload(span(kData).first(16))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::Internal());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::Internal());
}
TEST_F(WriteTransferMaxBytes16, MultipleParameters) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 16u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 16u);
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = std::span(kData).first(8)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(span(kData).first(8))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
- chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 8u);
- EXPECT_EQ(chunk.window_end_offset, 24u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 16u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 8u);
+ EXPECT_EQ(chunk.window_end_offset(), 24u);
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 8, .data = std::span(kData).subspan(8, 8)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(8)
+ .set_payload(span(kData).subspan(8, 8))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
- chunk = DecodeChunk(ctx_.responses()[2]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 16u);
- EXPECT_EQ(chunk.window_end_offset, 32u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 16u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 16u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
ctx_.SendClientStream<64>(
- EncodeChunk({.transfer_id = 7,
- .offset = 16,
- .data = std::span(kData).subspan(16, 8)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(16)
+ .set_payload(span(kData).subspan(16, 8))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 4u);
- chunk = DecodeChunk(ctx_.responses()[3]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 24u);
- EXPECT_EQ(chunk.window_end_offset, 32u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 8u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 24u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 24,
- .data = std::span(kData).subspan(24),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(24)
+ .set_payload(span(kData).subspan(24))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 5u);
- chunk = DecodeChunk(ctx_.responses()[4]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), OkStatus());
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
EXPECT_TRUE(handler_.finalize_write_called);
EXPECT_EQ(handler_.finalize_write_status, OkStatus());
EXPECT_EQ(std::memcmp(buffer.data(), kData.data(), kData.size()), 0);
}
-TEST_F(WriteTransferMaxBytes16, SetsDefaultPendingBytes) {
+TEST_F(WriteTransferMaxBytes16, SetsDefaultWindowEndOffset) {
// Default max bytes is smaller than buffer.
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.pending_bytes.value(), 16u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 16u);
}
-TEST_F(WriteTransfer, SetsWriterPendingBytes) {
+TEST_F(WriteTransfer, SetsWriterWindowEndOffset) {
// Buffer is smaller than constructor's default max bytes.
std::array<std::byte, 8> small_buffer = {};
SimpleWriteTransfer handler_(987, small_buffer);
ctx_.service().RegisterHandler(handler_);
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 987}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(987)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 987u);
- EXPECT_EQ(chunk.pending_bytes.value(), 8u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 987u);
+ EXPECT_EQ(chunk.window_end_offset(), 8u);
+
+ ctx_.service().UnregisterHandler(handler_);
}
TEST_F(WriteTransfer, UnexpectedOffset) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 0u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 32u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = std::span(kData).first(8)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(span(kData).first(8))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 4, // incorrect
- .data = std::span(kData).subspan(16),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(4) // incorrect
+ .set_payload(span(kData).subspan(8))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
- chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 8u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 24u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 8u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 8, // correct
- .data = std::span(kData).subspan(8),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(8) // correct
+ .set_payload(span(kData).subspan(8))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
- chunk = DecodeChunk(ctx_.responses()[2]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), OkStatus());
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
EXPECT_TRUE(handler_.finalize_write_called);
EXPECT_EQ(handler_.finalize_write_status, OkStatus());
@@ -1241,56 +1251,61 @@ TEST_F(WriteTransfer, UnexpectedOffset) {
}
TEST_F(WriteTransferMaxBytes16, TooMuchData) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 16u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 16u);
- // pending_bytes = 16 but send 24
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = std::span(kData).first(24)}));
+ // window_end_offset = 16, but send 24 bytes of data.
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(span(kData).first(24))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
- chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::Internal());
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::Internal());
}
TEST_F(WriteTransfer, UnregisteredHandler) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 999}));
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(999)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 999u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::NotFound());
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 999u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::NotFound());
}
TEST_F(WriteTransfer, ClientError) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 32u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
- ctx_.SendClientStream<64>(
- EncodeChunk({.transfer_id = 7, .status = Status::DataLoss()}));
+ ctx_.SendClientStream<64>(EncodeChunk(
+ Chunk::Final(ProtocolVersion::kLegacy, 7, Status::DataLoss())));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_EQ(ctx_.total_responses(), 1u);
@@ -1300,33 +1315,42 @@ TEST_F(WriteTransfer, ClientError) {
}
TEST_F(WriteTransfer, OnlySendParametersUpdateOnceAfterDrop) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- constexpr std::span data(kData);
+ constexpr span data(kData);
ctx_.SendClientStream<64>(
- EncodeChunk({.transfer_id = 7, .offset = 0, .data = data.first(1)}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(data.first(1))));
// Drop offset 1, then send the rest of the data.
for (uint32_t i = 2; i < kData.size(); ++i) {
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = i, .data = data.subspan(i, 1)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(i)
+ .set_payload(data.subspan(i, 1))));
}
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 1u);
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 1u);
- // Send the remaining data and the final status.
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 1,
- .data = data.subspan(1, 31),
- .status = OkStatus()}));
+ // Send the remaining data.
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(1)
+ .set_payload(data.subspan(1, 31))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_write_called);
@@ -1334,25 +1358,32 @@ TEST_F(WriteTransfer, OnlySendParametersUpdateOnceAfterDrop) {
}
TEST_F(WriteTransfer, ResendParametersIfSentRepeatedChunkDuringRecovery) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- constexpr std::span data(kData);
+ constexpr span data(kData);
// Skip offset 0, then send the rest of the data.
for (uint32_t i = 1; i < kData.size(); ++i) {
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = i, .data = data.subspan(i, 1)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(i)
+ .set_payload(data.subspan(i, 1))));
}
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u); // Resent transfer parameters once.
- const auto last_chunk = EncodeChunk(
- {.transfer_id = 7, .offset = kData.size() - 1, .data = data.last(1)});
+ const auto last_chunk =
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(kData.size() - 1)
+ .set_payload(data.last(1)));
ctx_.SendClientStream<64>(last_chunk);
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1365,13 +1396,17 @@ TEST_F(WriteTransfer, ResendParametersIfSentRepeatedChunkDuringRecovery) {
ASSERT_EQ(ctx_.total_responses(), 4u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 0u);
- EXPECT_TRUE(chunk.pending_bytes.has_value());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
// Resumes normal operation when correct offset is sent.
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = kData, .status = OkStatus()}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.finalize_write_called);
@@ -1379,39 +1414,50 @@ TEST_F(WriteTransfer, ResendParametersIfSentRepeatedChunkDuringRecovery) {
}
TEST_F(WriteTransfer, ResendsStatusIfClientRetriesAfterStatusChunk) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 0,
- .data = std::span(kData),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
Chunk chunk = DecodeChunk(ctx_.responses().back());
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), OkStatus());
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 0,
- .data = std::span(kData),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
chunk = DecodeChunk(ctx_.responses().back());
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), OkStatus());
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
}
TEST_F(WriteTransfer, IgnoresNonPendingTransfers) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7, .offset = 3}));
- ctx_.SendClientStream(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = std::span(kData).first(10)}));
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7, .status = OkStatus()}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(3)));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(span(kData).first(10))
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
@@ -1421,13 +1467,17 @@ TEST_F(WriteTransfer, IgnoresNonPendingTransfers) {
}
TEST_F(WriteTransfer, AbortAndRestartIfInitialPacketIsReceived) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = std::span(kData).first(8)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(span(kData).first(8))));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
@@ -1437,7 +1487,8 @@ TEST_F(WriteTransfer, AbortAndRestartIfInitialPacketIsReceived) {
handler_.prepare_write_called = false; // Reset to check it's called again.
// Simulate client disappearing then restarting the transfer.
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
@@ -1448,10 +1499,12 @@ TEST_F(WriteTransfer, AbortAndRestartIfInitialPacketIsReceived) {
ASSERT_EQ(ctx_.total_responses(), 2u);
- ctx_.SendClientStream<64>(EncodeChunk({.transfer_id = 7,
- .offset = 0,
- .data = std::span(kData),
- .remaining_bytes = 0}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 3u);
@@ -1463,8 +1516,8 @@ TEST_F(WriteTransfer, AbortAndRestartIfInitialPacketIsReceived) {
class SometimesUnavailableReadHandler final : public ReadOnlyHandler {
public:
- SometimesUnavailableReadHandler(uint32_t transfer_id, ConstByteSpan data)
- : ReadOnlyHandler(transfer_id), reader_(data), call_count_(0) {}
+ SometimesUnavailableReadHandler(uint32_t session_id, ConstByteSpan data)
+ : ReadOnlyHandler(session_id), reader_(data), call_count_(0) {}
Status PrepareRead() final {
if ((call_count_++ % 2) == 0) {
@@ -1485,63 +1538,968 @@ TEST_F(ReadTransfer, PrepareError) {
ctx_.service().RegisterHandler(unavailable_handler);
ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 88, .pending_bytes = 128, .offset = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(88)
+ .set_window_end_offset(128)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 88u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::DataLoss());
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 88u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::DataLoss());
// Try starting the transfer again. It should work this time.
// TODO(frolv): This won't work until completion ACKs are supported.
if (false) {
ctx_.SendClientStream(
- EncodeChunk({.transfer_id = 88, .pending_bytes = 128, .offset = 0}));
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(88)
+ .set_window_end_offset(128)
+ .set_offset(0)));
transfer_thread_.WaitUntilEventIsProcessed();
ASSERT_EQ(ctx_.total_responses(), 2u);
- chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 88u);
- ASSERT_EQ(chunk.data.size(), kData.size());
- EXPECT_EQ(std::memcmp(chunk.data.data(), kData.data(), chunk.data.size()),
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 88u);
+ ASSERT_EQ(chunk.payload().size(), kData.size());
+ EXPECT_EQ(std::memcmp(
+ chunk.payload().data(), kData.data(), chunk.payload().size()),
0);
}
}
TEST_F(WriteTransferMaxBytes16, Service_SetMaxPendingBytes) {
- ctx_.SendClientStream(EncodeChunk({.transfer_id = 7}));
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart).set_session_id(7)));
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler_.prepare_write_called);
EXPECT_FALSE(handler_.finalize_write_called);
- // First parameters chunk has default pending bytes of 16.
+ // First parameters chunk has the default window end offset of 16.
ASSERT_EQ(ctx_.total_responses(), 1u);
- Chunk chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 16u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.window_end_offset(), 16u);
// Update the pending bytes value.
ctx_.service().set_max_pending_bytes(12);
- ctx_.SendClientStream<64>(EncodeChunk(
- {.transfer_id = 7, .offset = 0, .data = std::span(kData).first(8)}));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(7)
+ .set_offset(0)
+ .set_payload(span(kData).first(8))));
transfer_thread_.WaitUntilEventIsProcessed();
// Second parameters chunk should use the new max pending bytes.
ASSERT_EQ(ctx_.total_responses(), 2u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.offset(), 8u);
+ EXPECT_EQ(chunk.window_end_offset(), 8u + 12u);
+}
+
+TEST_F(ReadTransfer, Version2_SimpleTransfer) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(3)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_read_called);
+ EXPECT_FALSE(handler_.finalize_read_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // Complete the handshake by confirming the server's ACK and sending the first
+ // read transfer parameters.
+ rpc::test::WaitForPackets(ctx_.output(), 2, [this] {
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)
+ .set_window_end_offset(64)
+ .set_offset(0)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+ });
+
+ // Server should respond by starting the data transfer, sending its sole data
+ // chunk and a remaining_bytes 0 chunk.
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+
+ Chunk c1 = DecodeChunk(ctx_.responses()[1]);
+ EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c1.type(), Chunk::Type::kData);
+ EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.offset(), 0u);
+ ASSERT_TRUE(c1.has_payload());
+ ASSERT_EQ(c1.payload().size(), kData.size());
+ EXPECT_EQ(std::memcmp(c1.payload().data(), kData.data(), c1.payload().size()),
+ 0);
+
+ Chunk c2 = DecodeChunk(ctx_.responses()[2]);
+ EXPECT_EQ(c2.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c2.type(), Chunk::Type::kData);
+ EXPECT_EQ(c2.session_id(), 1u);
+ EXPECT_FALSE(c2.has_payload());
+ EXPECT_EQ(c2.remaining_bytes(), 0u);
+
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.finalize_read_called);
+ EXPECT_EQ(handler_.finalize_read_status, OkStatus());
+}
+
+TEST_F(ReadTransfer, Version2_MultiChunk) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(3)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_read_called);
+ EXPECT_FALSE(handler_.finalize_read_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // Complete the handshake by confirming the server's ACK and sending the first
+ // read transfer parameters.
+ rpc::test::WaitForPackets(ctx_.output(), 3, [this] {
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)
+ .set_window_end_offset(64)
+ .set_max_chunk_size_bytes(16)
+ .set_offset(0)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+ });
+
+ ASSERT_EQ(ctx_.total_responses(), 4u);
+
+ Chunk c1 = DecodeChunk(ctx_.responses()[1]);
+ EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c1.type(), Chunk::Type::kData);
+ EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.offset(), 0u);
+ ASSERT_TRUE(c1.has_payload());
+ ASSERT_EQ(c1.payload().size(), 16u);
+ EXPECT_EQ(std::memcmp(c1.payload().data(), kData.data(), c1.payload().size()),
+ 0);
+
+ Chunk c2 = DecodeChunk(ctx_.responses()[2]);
+ EXPECT_EQ(c2.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c2.type(), Chunk::Type::kData);
+ EXPECT_EQ(c2.session_id(), 1u);
+ EXPECT_EQ(c2.offset(), 16u);
+ ASSERT_TRUE(c2.has_payload());
+ ASSERT_EQ(c2.payload().size(), 16u);
+ EXPECT_EQ(
+ std::memcmp(
+ c2.payload().data(), kData.data() + c2.offset(), c2.payload().size()),
+ 0);
+
+ Chunk c3 = DecodeChunk(ctx_.responses()[3]);
+ EXPECT_EQ(c3.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c3.type(), Chunk::Type::kData);
+ EXPECT_EQ(c3.session_id(), 1u);
+ EXPECT_FALSE(c3.has_payload());
+ EXPECT_EQ(c3.remaining_bytes(), 0u);
+
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.finalize_read_called);
+ EXPECT_EQ(handler_.finalize_read_status, OkStatus());
+}
+
+TEST_F(ReadTransfer, Version2_MultiParameters) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(3)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_read_called);
+ EXPECT_FALSE(handler_.finalize_read_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // Complete the handshake by confirming the server's ACK and sending the first
+ // read transfer parameters.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)
+ .set_window_end_offset(16)
+ .set_offset(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+
+ Chunk c1 = DecodeChunk(ctx_.responses()[1]);
+ EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c1.type(), Chunk::Type::kData);
+ EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.offset(), 0u);
+ ASSERT_TRUE(c1.has_payload());
+ ASSERT_EQ(c1.payload().size(), 16u);
+ EXPECT_EQ(std::memcmp(c1.payload().data(), kData.data(), c1.payload().size()),
+ 0);
+
+ rpc::test::WaitForPackets(ctx_.output(), 2, [this] {
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kParametersContinue)
+ .set_session_id(1)
+ .set_window_end_offset(64)
+ .set_offset(16)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+ });
+
+ ASSERT_EQ(ctx_.total_responses(), 4u);
+
+ Chunk c2 = DecodeChunk(ctx_.responses()[2]);
+ EXPECT_EQ(c2.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c2.type(), Chunk::Type::kData);
+ EXPECT_EQ(c2.session_id(), 1u);
+ EXPECT_EQ(c2.offset(), 16u);
+ ASSERT_TRUE(c2.has_payload());
+ ASSERT_EQ(c2.payload().size(), 16u);
+ EXPECT_EQ(
+ std::memcmp(
+ c2.payload().data(), kData.data() + c2.offset(), c2.payload().size()),
+ 0);
+
+ Chunk c3 = DecodeChunk(ctx_.responses()[3]);
+ EXPECT_EQ(c3.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c3.type(), Chunk::Type::kData);
+ EXPECT_EQ(c3.session_id(), 1u);
+ EXPECT_FALSE(c3.has_payload());
+ EXPECT_EQ(c3.remaining_bytes(), 0u);
+
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk::Final(ProtocolVersion::kVersionTwo, 1, OkStatus())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.finalize_read_called);
+ EXPECT_EQ(handler_.finalize_read_status, OkStatus());
+}
+
+TEST_F(ReadTransfer, Version2_ClientTerminatesDuringHandshake) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(3)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_read_called);
+ EXPECT_FALSE(handler_.finalize_read_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // Send a terminating chunk instead of the third part of the handshake.
+ ctx_.SendClientStream(EncodeChunk(Chunk::Final(
+ ProtocolVersion::kVersionTwo, 1, Status::ResourceExhausted())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.finalize_read_called);
+ EXPECT_EQ(handler_.finalize_read_status, Status::ResourceExhausted());
+}
+
+TEST_F(ReadTransfer, Version2_ClientSendsWrongProtocolVersion) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(3)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_read_called);
+ EXPECT_FALSE(handler_.finalize_read_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // Complete the handshake by confirming the server's ACK and sending the first
+ // read transfer parameters.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)
+ .set_window_end_offset(16)
+ .set_offset(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+
+ Chunk c1 = DecodeChunk(ctx_.responses()[1]);
+ EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c1.type(), Chunk::Type::kData);
+ EXPECT_EQ(c1.session_id(), 1u);
+ EXPECT_EQ(c1.offset(), 0u);
+ ASSERT_TRUE(c1.has_payload());
+ ASSERT_EQ(c1.payload().size(), 16u);
+ EXPECT_EQ(std::memcmp(c1.payload().data(), kData.data(), c1.payload().size()),
+ 0);
+
+ // Send a parameters update, but with the incorrect protocol version. The
+ // server should terminate the transfer.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersContinue)
+ .set_session_id(1)
+ .set_window_end_offset(64)
+ .set_offset(16)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::Internal());
+
+ EXPECT_TRUE(handler_.finalize_read_called);
+ EXPECT_EQ(handler_.finalize_read_status, Status::Internal());
+}
+
+TEST_F(ReadTransfer, Version2_BadParametersInHandshake) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(3)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_read_called);
+ EXPECT_FALSE(handler_.finalize_read_called);
+
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 3u);
+
+ // Complete the handshake, but send an invalid parameters chunk. The server
+ // should terminate the transfer.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)
+ .set_window_end_offset(0)
+ .set_offset(0)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+
+ Chunk c1 = DecodeChunk(ctx_.responses()[1]);
+ EXPECT_EQ(c1.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(c1.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(c1.session_id(), 1u);
+ ASSERT_TRUE(c1.status().has_value());
+ EXPECT_EQ(c1.status().value(), Status::ResourceExhausted());
+}
+
+TEST_F(ReadTransfer, Version2_InvalidResourceId) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(99)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.status().value(), Status::NotFound());
+}
+
+TEST_F(ReadTransfer, Version2_PrepareError) {
+ SometimesUnavailableReadHandler unavailable_handler(99, kData);
+ ctx_.service().RegisterHandler(unavailable_handler);
+
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(99)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.resource_id(), 99u);
+ EXPECT_EQ(chunk.status().value(), Status::DataLoss());
+}
+
+TEST_F(WriteTransfer, Version2_SimpleTransfer) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(7)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_write_called);
+ EXPECT_FALSE(handler_.finalize_write_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 7u);
+
+ // Complete the handshake by confirming the server's ACK.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Server should respond by sending its initial transfer parameters.
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+
+ chunk = DecodeChunk(ctx_.responses()[1]);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+ ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
+ EXPECT_EQ(chunk.max_chunk_size_bytes().value(), 37u);
+
+ // Send all of our data.
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(1)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ // Send the completion acknowledgement.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
+ .set_session_id(1)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+
+ EXPECT_TRUE(handler_.finalize_write_called);
+ EXPECT_EQ(handler_.finalize_write_status, OkStatus());
+ EXPECT_EQ(std::memcmp(buffer.data(), kData.data(), kData.size()), 0);
+}
+
+TEST_F(WriteTransfer, Version2_Multichunk) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(7)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_write_called);
+ EXPECT_FALSE(handler_.finalize_write_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 7u);
+
+ // Complete the handshake by confirming the server's ACK.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Server should respond by sending its initial transfer parameters.
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+
+ chunk = DecodeChunk(ctx_.responses()[1]);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+ ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
+ EXPECT_EQ(chunk.max_chunk_size_bytes().value(), 37u);
+
+ // Send all of our data across two chunks.
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(1)
+ .set_offset(0)
+ .set_payload(span(kData).first(8))));
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(1)
+ .set_offset(8)
+ .set_payload(span(kData).subspan(8))
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ // Send the completion acknowledgement.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
+ .set_session_id(1)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+
+ EXPECT_TRUE(handler_.finalize_write_called);
+ EXPECT_EQ(handler_.finalize_write_status, OkStatus());
+ EXPECT_EQ(std::memcmp(buffer.data(), kData.data(), kData.size()), 0);
+}
+
+TEST_F(WriteTransfer, Version2_ContinueParameters) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(7)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_write_called);
+ EXPECT_FALSE(handler_.finalize_write_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 7u);
+
+ // Complete the handshake by confirming the server's ACK.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Server should respond by sending its initial transfer parameters.
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+
+ chunk = DecodeChunk(ctx_.responses()[1]);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+ ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
+ EXPECT_EQ(chunk.max_chunk_size_bytes().value(), 37u);
+
+ // Send all of our data across several chunks.
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(1)
+ .set_offset(0)
+ .set_payload(span(kData).first(8))));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(1)
+ .set_offset(8)
+ .set_payload(span(kData).subspan(8, 8))));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kParametersContinue);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.offset(), 16u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(1)
+ .set_offset(16)
+ .set_payload(span(kData).subspan(16, 8))));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+ ASSERT_EQ(ctx_.total_responses(), 4u);
+
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kParametersContinue);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.offset(), 24u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kData)
+ .set_session_id(1)
+ .set_offset(24)
+ .set_payload(span(kData).subspan(24))
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 5u);
+
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), OkStatus());
+
+ // Send the completion acknowledgement.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
+ .set_session_id(1)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 5u);
+
+ EXPECT_TRUE(handler_.finalize_write_called);
+ EXPECT_EQ(handler_.finalize_write_status, OkStatus());
+ EXPECT_EQ(std::memcmp(buffer.data(), kData.data(), kData.size()), 0);
+}
+
+TEST_F(WriteTransfer, Version2_ClientTerminatesDuringHandshake) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(7)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_write_called);
+ EXPECT_FALSE(handler_.finalize_write_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 7u);
+
+ // Send an error chunk instead of completing the handshake.
+ ctx_.SendClientStream(EncodeChunk(Chunk::Final(
+ ProtocolVersion::kVersionTwo, 1, Status::FailedPrecondition())));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.finalize_write_called);
+ EXPECT_EQ(handler_.finalize_write_status, Status::FailedPrecondition());
+}
+
+TEST_F(WriteTransfer, Version2_ClientSendsWrongProtocolVersion) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(7)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_write_called);
+ EXPECT_FALSE(handler_.finalize_write_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 7u);
+
+ // Complete the handshake by confirming the server's ACK.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Server should respond by sending its initial transfer parameters.
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+
chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 7u);
- EXPECT_EQ(chunk.offset, 8u);
- EXPECT_EQ(chunk.window_end_offset, 20u);
- ASSERT_TRUE(chunk.pending_bytes.has_value());
- EXPECT_EQ(chunk.pending_bytes.value(), 12u);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kParametersRetransmit);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.window_end_offset(), 32u);
+ ASSERT_TRUE(chunk.max_chunk_size_bytes().has_value());
+ EXPECT_EQ(chunk.max_chunk_size_bytes().value(), 37u);
+
+ // The transfer was configured to use protocol version 2. Send a legacy chunk
+ // instead.
+ ctx_.SendClientStream<64>(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kData)
+ .set_session_id(1)
+ .set_offset(0)
+ .set_payload(kData)
+ .set_remaining_bytes(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // Server should terminate the transfer.
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+
+ chunk = DecodeChunk(ctx_.responses()[2]);
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.status().value(), Status::Internal());
+
+ // Send the completion acknowledgement.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kCompletionAck)
+ .set_session_id(1)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+}
+
+TEST_F(WriteTransfer, Version2_InvalidResourceId) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(99)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.status().value(), Status::NotFound());
+}
+
+class ReadTransferLowMaxRetries : public ::testing::Test {
+ protected:
+ static constexpr uint32_t kMaxRetries = 3;
+ static constexpr uint32_t kMaxLifetimeRetries = 4;
+
+ ReadTransferLowMaxRetries()
+ : handler_(9, kData),
+ transfer_thread_(data_buffer_, encode_buffer_),
+ ctx_(transfer_thread_,
+ 64,
+ // Use a long timeout to avoid accidentally triggering timeouts.
+ std::chrono::minutes(1),
+ kMaxRetries,
+ cfg::kDefaultExtendWindowDivisor,
+ kMaxLifetimeRetries),
+ system_thread_(TransferThreadOptions(), transfer_thread_) {
+ ctx_.service().RegisterHandler(handler_);
+
+ PW_CHECK(!handler_.prepare_read_called);
+ PW_CHECK(!handler_.finalize_read_called);
+
+ ctx_.call(); // Open the read stream
+ transfer_thread_.WaitUntilEventIsProcessed();
+ }
+
+ ~ReadTransferLowMaxRetries() override {
+ transfer_thread_.Terminate();
+ system_thread_.join();
+ }
+
+ SimpleReadTransfer handler_;
+ Thread<1, 1> transfer_thread_;
+ PW_RAW_TEST_METHOD_CONTEXT(TransferService, Read, 10) ctx_;
+ thread::Thread system_thread_;
+ std::array<std::byte, 64> data_buffer_;
+ std::array<std::byte, 64> encode_buffer_;
+};
+
+TEST_F(ReadTransferLowMaxRetries, FailsAfterLifetimeRetryCount) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kLegacy, Chunk::Type::kStart)
+ .set_session_id(9)
+ .set_window_end_offset(16)
+ .set_offset(0)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_read_called);
+ EXPECT_FALSE(handler_.finalize_read_called);
+
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+
+ EXPECT_EQ(chunk.session_id(), 9u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ ASSERT_EQ(chunk.payload().size(), 16u);
+ EXPECT_EQ(
+ std::memcmp(chunk.payload().data(), kData.data(), chunk.payload().size()),
+ 0);
+
+ // Time out twice. Server should retry both times.
+ transfer_thread_.SimulateServerTimeout(9);
+ transfer_thread_.SimulateServerTimeout(9);
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 9u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ ASSERT_EQ(chunk.payload().size(), 16u);
+ EXPECT_EQ(
+ std::memcmp(chunk.payload().data(), kData.data(), chunk.payload().size()),
+ 0);
+
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersContinue)
+ .set_session_id(9)
+ .set_window_end_offset(32)
+ .set_offset(16)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 4u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 9u);
+ EXPECT_EQ(chunk.offset(), 16u);
+ ASSERT_EQ(chunk.payload().size(), 16u);
+ EXPECT_EQ(
+ std::memcmp(
+ chunk.payload().data(), kData.data() + 16, chunk.payload().size()),
+ 0);
+
+ // Time out three more times. The transfer should terminate.
+ transfer_thread_.SimulateServerTimeout(9);
+ ASSERT_EQ(ctx_.total_responses(), 5u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 9u);
+ EXPECT_EQ(chunk.offset(), 16u);
+ ASSERT_EQ(chunk.payload().size(), 16u);
+ EXPECT_EQ(
+ std::memcmp(
+ chunk.payload().data(), kData.data() + 16, chunk.payload().size()),
+ 0);
+
+ transfer_thread_.SimulateServerTimeout(9);
+ ASSERT_EQ(ctx_.total_responses(), 6u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.session_id(), 9u);
+ EXPECT_EQ(chunk.offset(), 16u);
+ ASSERT_EQ(chunk.payload().size(), 16u);
+ EXPECT_EQ(
+ std::memcmp(
+ chunk.payload().data(), kData.data() + 16, chunk.payload().size()),
+ 0);
+
+ transfer_thread_.SimulateServerTimeout(9);
+ ASSERT_EQ(ctx_.total_responses(), 7u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.status(), Status::DeadlineExceeded());
}
-PW_MODIFY_DIAGNOSTICS_POP();
+TEST_F(ReadTransferLowMaxRetries, Version2_FailsAfterLifetimeRetryCount) {
+ ctx_.SendClientStream(
+ EncodeChunk(Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStart)
+ .set_resource_id(9)));
+
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_TRUE(handler_.prepare_read_called);
+ EXPECT_FALSE(handler_.finalize_read_called);
+
+ // First, the server responds with a START_ACK, assigning a session ID and
+ // confirming the protocol version.
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+ Chunk chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.protocol_version(), ProtocolVersion::kVersionTwo);
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+ EXPECT_EQ(chunk.session_id(), 1u);
+ EXPECT_EQ(chunk.resource_id(), 9u);
+
+ // Time out twice. Server should retry both times.
+ transfer_thread_.SimulateServerTimeout(1);
+ transfer_thread_.SimulateServerTimeout(1);
+ ASSERT_EQ(ctx_.total_responses(), 3u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kStartAck);
+
+ // Complete the handshake, allowing the transfer to continue.
+ ctx_.SendClientStream(EncodeChunk(
+ Chunk(ProtocolVersion::kVersionTwo, Chunk::Type::kStartAckConfirmation)
+ .set_session_id(1)
+ .set_window_end_offset(16)
+ .set_offset(0)));
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ ASSERT_EQ(ctx_.total_responses(), 4u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+
+ // Time out three more times. The transfer should terminate.
+ transfer_thread_.SimulateServerTimeout(1);
+ ASSERT_EQ(ctx_.total_responses(), 5u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+
+ transfer_thread_.SimulateServerTimeout(1);
+ ASSERT_EQ(ctx_.total_responses(), 6u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kData);
+
+ transfer_thread_.SimulateServerTimeout(1);
+ ASSERT_EQ(ctx_.total_responses(), 7u);
+ chunk = DecodeChunk(ctx_.responses().back());
+ EXPECT_EQ(chunk.type(), Chunk::Type::kCompletion);
+ EXPECT_EQ(chunk.status(), Status::DeadlineExceeded());
+}
} // namespace
} // namespace pw::transfer::test
diff --git a/pw_transfer/transfer_thread.cc b/pw_transfer/transfer_thread.cc
index aac5cad35..c0d03efa1 100644
--- a/pw_transfer/transfer_thread.cc
+++ b/pw_transfer/transfer_thread.cc
@@ -31,12 +31,12 @@ void TransferThread::Terminate() {
event_notification_.release();
}
-void TransferThread::SimulateTimeout(EventType type, uint32_t transfer_id) {
+void TransferThread::SimulateTimeout(EventType type, uint32_t session_id) {
next_event_ownership_.acquire();
next_event_.type = type;
next_event_.chunk = {};
- next_event_.chunk.transfer_id = transfer_id;
+ next_event_.chunk.context_identifier = session_id;
event_notification_.release();
@@ -49,15 +49,18 @@ void TransferThread::Run() {
while (true) {
if (event_notification_.try_acquire_until(GetNextTransferTimeout())) {
- if (next_event_.type == EventType::kTerminate) {
- return;
- }
-
HandleEvent(next_event_);
+ // Sample event type before we release ownership of next_event_.
+ bool is_terminating = next_event_.type == EventType::kTerminate;
+
// Finished processing the event. Allow the next_event struct to be
// overwritten.
next_event_ownership_.release();
+
+ if (is_terminating) {
+ return;
+ }
}
// Regardless of whether an event was received or not, check for any
@@ -96,13 +99,16 @@ chrono::SystemClock::time_point TransferThread::GetNextTransferTimeout() const {
}
void TransferThread::StartTransfer(TransferType type,
- uint32_t transfer_id,
- uint32_t handler_id,
+ ProtocolVersion version,
+ uint32_t session_id,
+ uint32_t resource_id,
+ ConstByteSpan raw_chunk,
stream::Stream* stream,
const TransferParameters& max_parameters,
Function<void(Status)>&& on_completion,
chrono::SystemClock::duration timeout,
- uint8_t max_retries) {
+ uint8_t max_retries,
+ uint32_t max_lifetime_retries) {
// Block until the last event has been processed.
next_event_ownership_.acquire();
@@ -110,14 +116,23 @@ void TransferThread::StartTransfer(TransferType type,
next_event_.type = is_client_transfer ? EventType::kNewClientTransfer
: EventType::kNewServerTransfer;
+
+ if (!raw_chunk.empty()) {
+ std::memcpy(chunk_buffer_.data(), raw_chunk.data(), raw_chunk.size());
+ }
+
next_event_.new_transfer = {
.type = type,
- .transfer_id = transfer_id,
- .handler_id = handler_id,
+ .protocol_version = version,
+ .session_id = session_id,
+ .resource_id = resource_id,
.max_parameters = &max_parameters,
.timeout = timeout,
.max_retries = max_retries,
+ .max_lifetime_retries = max_lifetime_retries,
.transfer_thread = this,
+ .raw_chunk_data = chunk_buffer_.data(),
+ .raw_chunk_size = raw_chunk.size(),
};
staged_on_completion_ = std::move(on_completion);
@@ -133,7 +148,7 @@ void TransferThread::StartTransfer(TransferType type,
} else {
auto handler = std::find_if(handlers_.begin(),
handlers_.end(),
- [&](auto& h) { return h.id() == handler_id; });
+ [&](auto& h) { return h.id() == resource_id; });
if (handler != handlers_.end()) {
next_event_.new_transfer.handler = &*handler;
next_event_.new_transfer.rpc_writer = &static_cast<rpc::Writer&>(
@@ -143,7 +158,12 @@ void TransferThread::StartTransfer(TransferType type,
// No handler exists for the transfer: return a NOT_FOUND.
next_event_.type = EventType::kSendStatusChunk;
next_event_.send_status_chunk = {
- .transfer_id = transfer_id,
+ // Identify the status chunk using the requested resource ID rather
+ // than the session ID. In legacy, the two are the same, whereas in
+ // v2+ the client has not yet been assigned a session.
+ .session_id = resource_id,
+ .set_resource_id = version == ProtocolVersion::kVersionTwo,
+ .protocol_version = version,
.status = Status::NotFound().code(),
.stream = type == TransferType::kTransmit
? TransferStream::kServerRead
@@ -162,9 +182,9 @@ void TransferThread::ProcessChunk(EventType type, ConstByteSpan chunk) {
PW_CHECK(chunk.size() <= chunk_buffer_.size(),
"Transfer received a larger chunk than it can handle.");
- Result<uint32_t> transfer_id = ExtractTransferId(chunk);
- if (!transfer_id.ok()) {
- PW_LOG_ERROR("Received a malformed chunk without a transfer ID");
+ Result<Chunk::Identifier> identifier = Chunk::ExtractIdentifier(chunk);
+ if (!identifier.ok()) {
+ PW_LOG_ERROR("Received a malformed chunk without a context identifier");
return;
}
@@ -175,7 +195,8 @@ void TransferThread::ProcessChunk(EventType type, ConstByteSpan chunk) {
next_event_.type = type;
next_event_.chunk = {
- .transfer_id = *transfer_id,
+ .context_identifier = identifier->value(),
+ .match_resource_id = identifier->is_resource(),
.data = chunk_buffer_.data(),
.size = chunk.size(),
};
@@ -183,32 +204,24 @@ void TransferThread::ProcessChunk(EventType type, ConstByteSpan chunk) {
event_notification_.release();
}
-void TransferThread::SetClientStream(TransferStream type,
- rpc::RawClientReaderWriter& stream) {
+void TransferThread::EndTransfer(EventType type,
+ uint32_t session_id,
+ Status status,
+ bool send_status_chunk) {
// Block until the last event has been processed.
next_event_ownership_.acquire();
- next_event_.type = EventType::kSetTransferStream;
- next_event_.set_transfer_stream = type;
- staged_client_stream_ = std::move(stream);
-
- event_notification_.release();
-}
-
-void TransferThread::SetServerStream(TransferStream type,
- rpc::RawServerReaderWriter& stream) {
- // Block until the last event has been processed.
- next_event_ownership_.acquire();
-
- next_event_.type = EventType::kSetTransferStream;
- next_event_.set_transfer_stream = type;
- staged_server_stream_ = std::move(stream);
+ next_event_.type = type;
+ next_event_.end_transfer = {
+ .session_id = session_id,
+ .status = status.code(),
+ .send_status_chunk = send_status_chunk,
+ };
event_notification_.release();
}
-void TransferThread::TransferHandlerEvent(EventType type,
- internal::Handler& handler) {
+void TransferThread::TransferHandlerEvent(EventType type, Handler& handler) {
// Block until the last event has been processed.
next_event_ownership_.acquire();
@@ -224,69 +237,151 @@ void TransferThread::TransferHandlerEvent(EventType type,
void TransferThread::HandleEvent(const internal::Event& event) {
switch (event.type) {
- case EventType::kSendStatusChunk:
- SendStatusChunk(event.send_status_chunk);
- break;
-
- case EventType::kSetTransferStream:
- switch (event.set_transfer_stream) {
- case TransferStream::kClientRead:
- client_read_stream_ = std::move(staged_client_stream_);
- break;
-
- case TransferStream::kClientWrite:
- client_write_stream_ = std::move(staged_client_stream_);
- break;
-
- case TransferStream::kServerRead:
- server_read_stream_ = std::move(staged_server_stream_);
- break;
+ case EventType::kTerminate:
+ // Terminate server contexts.
+ for (ServerContext& server_context : server_transfers_) {
+ server_context.HandleEvent(Event{
+ .type = EventType::kServerEndTransfer,
+ .end_transfer =
+ EndTransferEvent{
+ .session_id = server_context.session_id(),
+ .status = Status::Aborted().code(),
+ .send_status_chunk = false,
+ },
+ });
+ }
- case TransferStream::kServerWrite:
- server_write_stream_ = std::move(staged_server_stream_);
- break;
+ // Terminate client contexts.
+ for (ClientContext& client_context : client_transfers_) {
+ client_context.HandleEvent(Event{
+ .type = EventType::kClientEndTransfer,
+ .end_transfer =
+ EndTransferEvent{
+ .session_id = client_context.session_id(),
+ .status = Status::Aborted().code(),
+ .send_status_chunk = false,
+ },
+ });
}
+
+ // Cancel/Finish streams.
+ client_read_stream_.Cancel().IgnoreError();
+ client_write_stream_.Cancel().IgnoreError();
+ server_read_stream_.Finish(Status::Aborted()).IgnoreError();
+ server_write_stream_.Finish(Status::Aborted()).IgnoreError();
return;
+ case EventType::kSendStatusChunk:
+ SendStatusChunk(event.send_status_chunk);
+ break;
+
case EventType::kAddTransferHandler:
handlers_.push_front(*event.add_transfer_handler);
return;
case EventType::kRemoveTransferHandler:
+ for (ServerContext& server_context : server_transfers_) {
+ if (server_context.handler() == event.remove_transfer_handler) {
+ server_context.HandleEvent(Event{
+ .type = EventType::kServerEndTransfer,
+ .end_transfer =
+ EndTransferEvent{
+ .session_id = server_context.session_id(),
+ .status = Status::Aborted().code(),
+ .send_status_chunk = false,
+ },
+ });
+ }
+ }
handlers_.remove(*event.remove_transfer_handler);
return;
+ case EventType::kNewClientTransfer:
+ case EventType::kNewServerTransfer:
+ case EventType::kClientChunk:
+ case EventType::kServerChunk:
+ case EventType::kClientTimeout:
+ case EventType::kServerTimeout:
+ case EventType::kClientEndTransfer:
+ case EventType::kServerEndTransfer:
default:
// Other events are handled by individual transfer contexts.
break;
}
- if (Context* ctx = FindContextForEvent(event); ctx != nullptr) {
+ Context* ctx = FindContextForEvent(event);
+ if (ctx == nullptr) {
+ // No context was found. For new transfer events, report a
+ // RESOURCE_EXHAUSTED error with starting the transfer.
if (event.type == EventType::kNewClientTransfer) {
- // TODO(frolv): This is terrible.
- static_cast<ClientContext*>(ctx)->set_on_completion(
- std::move(staged_on_completion_));
+ // On the client, invoke the completion callback directly.
+ staged_on_completion_(Status::ResourceExhausted());
+ } else if (event.type == EventType::kNewServerTransfer) {
+ // On the server, send a status chunk back to the client.
+ SendStatusChunk(
+ {.session_id = event.new_transfer.resource_id,
+ .set_resource_id = event.new_transfer.protocol_version ==
+ ProtocolVersion::kVersionTwo,
+ .protocol_version = event.new_transfer.protocol_version,
+ .status = Status::ResourceExhausted().code(),
+ .stream = event.new_transfer.type == TransferType::kTransmit
+ ? TransferStream::kServerRead
+ : TransferStream::kServerWrite});
}
+ return;
+ }
- ctx->HandleEvent(event);
+ if (event.type == EventType::kNewClientTransfer) {
+ // TODO(frolv): This is terrible.
+ static_cast<ClientContext*>(ctx)->set_on_completion(
+ std::move(staged_on_completion_));
}
+
+ ctx->HandleEvent(event);
}
Context* TransferThread::FindContextForEvent(
const internal::Event& event) const {
switch (event.type) {
case EventType::kNewClientTransfer:
- return FindNewTransfer(client_transfers_, event.new_transfer.transfer_id);
+ return FindNewTransfer(client_transfers_, event.new_transfer.session_id);
case EventType::kNewServerTransfer:
- return FindNewTransfer(server_transfers_, event.new_transfer.transfer_id);
+ return FindNewTransfer(server_transfers_, event.new_transfer.session_id);
+
case EventType::kClientChunk:
- return FindActiveTransfer(client_transfers_, event.chunk.transfer_id);
+ if (event.chunk.match_resource_id) {
+ return FindActiveTransferByResourceId(client_transfers_,
+ event.chunk.context_identifier);
+ }
+ return FindActiveTransferByLegacyId(client_transfers_,
+ event.chunk.context_identifier);
+
case EventType::kServerChunk:
- return FindActiveTransfer(server_transfers_, event.chunk.transfer_id);
+ if (event.chunk.match_resource_id) {
+ return FindActiveTransferByResourceId(server_transfers_,
+ event.chunk.context_identifier);
+ }
+ return FindActiveTransferByLegacyId(server_transfers_,
+ event.chunk.context_identifier);
+
case EventType::kClientTimeout: // Manually triggered client timeout
- return FindActiveTransfer(client_transfers_, event.chunk.transfer_id);
+ return FindActiveTransferByLegacyId(client_transfers_,
+ event.chunk.context_identifier);
case EventType::kServerTimeout: // Manually triggered server timeout
- return FindActiveTransfer(server_transfers_, event.chunk.transfer_id);
+ return FindActiveTransferByLegacyId(server_transfers_,
+ event.chunk.context_identifier);
+
+ case EventType::kClientEndTransfer:
+ return FindActiveTransferByLegacyId(client_transfers_,
+ event.end_transfer.session_id);
+ case EventType::kServerEndTransfer:
+ return FindActiveTransferByLegacyId(server_transfers_,
+ event.end_transfer.session_id);
+
+ case EventType::kSendStatusChunk:
+ case EventType::kAddTransferHandler:
+ case EventType::kRemoveTransferHandler:
+ case EventType::kTerminate:
default:
return nullptr;
}
@@ -296,21 +391,23 @@ void TransferThread::SendStatusChunk(
const internal::SendStatusChunkEvent& event) {
rpc::Writer& destination = stream_for(event.stream);
- internal::Chunk chunk = {};
- chunk.transfer_id = event.transfer_id;
- chunk.status = event.status;
+ Chunk chunk =
+ Chunk::Final(event.protocol_version, event.session_id, event.status);
- Result<ConstByteSpan> result = internal::EncodeChunk(chunk, chunk_buffer_);
+ if (event.set_resource_id) {
+ chunk.set_resource_id(event.session_id);
+ }
+ Result<ConstByteSpan> result = chunk.Encode(chunk_buffer_);
if (!result.ok()) {
PW_LOG_ERROR("Failed to encode final chunk for transfer %u",
- static_cast<unsigned>(event.transfer_id));
+ static_cast<unsigned>(event.session_id));
return;
}
if (!destination.Write(result.value()).ok()) {
PW_LOG_ERROR("Failed to send final chunk for transfer %u",
- static_cast<unsigned>(event.transfer_id));
+ static_cast<unsigned>(event.session_id));
return;
}
}
diff --git a/pw_transfer/transfer_thread_test.cc b/pw_transfer/transfer_thread_test.cc
index 7890041ba..d83f8298d 100644
--- a/pw_transfer/transfer_thread_test.cc
+++ b/pw_transfer/transfer_thread_test.cc
@@ -19,7 +19,7 @@
#include "pw_bytes/array.h"
#include "pw_rpc/raw/client_testing.h"
#include "pw_rpc/raw/test_method_context.h"
-#include "pw_rpc/thread_testing.h"
+#include "pw_rpc/test_helpers.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
#include "pw_transfer/handler.h"
@@ -32,9 +32,6 @@ namespace {
using internal::Chunk;
-PW_MODIFY_DIAGNOSTICS_PUSH();
-PW_MODIFY_DIAGNOSTIC(ignored, "-Wmissing-field-initializers");
-
// TODO(frolv): Have a generic way to obtain a thread for testing on any system.
thread::Options& TransferThreadOptions() {
static thread::stl::Options options;
@@ -51,7 +48,7 @@ class TransferThreadTest : public ::testing::Test {
transfer_thread_(chunk_buffer_, encode_buffer_),
system_thread_(TransferThreadOptions(), transfer_thread_) {}
- ~TransferThreadTest() {
+ ~TransferThreadTest() override {
transfer_thread_.Terminate();
system_thread_.join();
}
@@ -72,8 +69,8 @@ class TransferThreadTest : public ::testing::Test {
class SimpleReadTransfer final : public ReadOnlyHandler {
public:
- SimpleReadTransfer(uint32_t transfer_id, ConstByteSpan data)
- : ReadOnlyHandler(transfer_id),
+ SimpleReadTransfer(uint32_t session_id, ConstByteSpan data)
+ : ReadOnlyHandler(session_id),
prepare_read_called(false),
finalize_read_called(false),
finalize_read_status(Status::Unknown()),
@@ -109,15 +106,20 @@ TEST_F(TransferThreadTest, AddTransferHandler) {
transfer_thread_.AddTransferHandler(handler);
transfer_thread_.StartServerTransfer(internal::TransferType::kTransmit,
+ ProtocolVersion::kLegacy,
3,
3,
+ {},
max_parameters_,
std::chrono::seconds(2),
- 0);
+ 3,
+ 10);
transfer_thread_.WaitUntilEventIsProcessed();
EXPECT_TRUE(handler.prepare_read_called);
+
+ transfer_thread_.RemoveTransferHandler(handler);
}
TEST_F(TransferThreadTest, RemoveTransferHandler) {
@@ -129,11 +131,14 @@ TEST_F(TransferThreadTest, RemoveTransferHandler) {
transfer_thread_.RemoveTransferHandler(handler);
transfer_thread_.StartServerTransfer(internal::TransferType::kTransmit,
+ ProtocolVersion::kLegacy,
3,
3,
+ {},
max_parameters_,
std::chrono::seconds(2),
- 0);
+ 3,
+ 10);
transfer_thread_.WaitUntilEventIsProcessed();
@@ -141,9 +146,11 @@ TEST_F(TransferThreadTest, RemoveTransferHandler) {
ASSERT_EQ(ctx_.total_responses(), 1u);
auto chunk = DecodeChunk(ctx_.response());
- EXPECT_EQ(chunk.transfer_id, 3u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::NotFound());
+ EXPECT_EQ(chunk.session_id(), 3u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::NotFound());
+
+ transfer_thread_.RemoveTransferHandler(handler);
}
TEST_F(TransferThreadTest, ProcessChunk_SendsWindow) {
@@ -153,66 +160,188 @@ TEST_F(TransferThreadTest, ProcessChunk_SendsWindow) {
SimpleReadTransfer handler(3, kData);
transfer_thread_.AddTransferHandler(handler);
- transfer_thread_.StartServerTransfer(internal::TransferType::kTransmit,
- 3,
- 3,
- max_parameters_,
- std::chrono::seconds(2),
- 0);
-
rpc::test::WaitForPackets(ctx_.output(), 2, [this] {
- // Malformed transfer parameters chunk without a pending_bytes field.
- transfer_thread_.ProcessServerChunk(
- EncodeChunk({.transfer_id = 3,
- .window_end_offset = 16,
- .pending_bytes = 16,
- .max_chunk_size_bytes = 8,
- .offset = 0,
- .type = Chunk::Type::kParametersRetransmit}));
+ transfer_thread_.StartServerTransfer(
+ internal::TransferType::kTransmit,
+ ProtocolVersion::kLegacy,
+ 3,
+ 3,
+ EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_max_chunk_size_bytes(8)
+ .set_offset(0)),
+ max_parameters_,
+ std::chrono::seconds(2),
+ 3,
+ 10);
});
ASSERT_EQ(ctx_.total_responses(), 2u);
auto chunk = DecodeChunk(ctx_.responses()[0]);
- EXPECT_EQ(chunk.transfer_id, 3u);
- EXPECT_EQ(chunk.offset, 0u);
- EXPECT_EQ(chunk.data.size(), 8u);
- EXPECT_EQ(std::memcmp(chunk.data.data(), kData.data(), chunk.data.size()), 0);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 0u);
+ EXPECT_EQ(chunk.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(chunk.payload().data(), kData.data(), chunk.payload().size()),
+ 0);
chunk = DecodeChunk(ctx_.responses()[1]);
- EXPECT_EQ(chunk.transfer_id, 3u);
- EXPECT_EQ(chunk.offset, 8u);
- EXPECT_EQ(chunk.data.size(), 8u);
- EXPECT_EQ(std::memcmp(chunk.data.data(), kData.data() + 8, chunk.data.size()),
- 0);
+ EXPECT_EQ(chunk.session_id(), 3u);
+ EXPECT_EQ(chunk.offset(), 8u);
+ EXPECT_EQ(chunk.payload().size(), 8u);
+ EXPECT_EQ(
+ std::memcmp(
+ chunk.payload().data(), kData.data() + 8, chunk.payload().size()),
+ 0);
+
+ transfer_thread_.RemoveTransferHandler(handler);
}
-TEST_F(TransferThreadTest, ProcessChunk_Malformed) {
+TEST_F(TransferThreadTest, StartTransferExhausted_Server) {
+ auto reader_writer = ctx_.reader_writer();
+ transfer_thread_.SetServerReadStream(reader_writer);
+
+ SimpleReadTransfer handler3(3, kData);
+ SimpleReadTransfer handler4(4, kData);
+ transfer_thread_.AddTransferHandler(handler3);
+ transfer_thread_.AddTransferHandler(handler4);
+
+ transfer_thread_.StartServerTransfer(
+ internal::TransferType::kTransmit,
+ ProtocolVersion::kLegacy,
+ 3,
+ 3,
+ EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(3)
+ .set_window_end_offset(16)
+ .set_max_chunk_size_bytes(8)
+ .set_offset(0)),
+ max_parameters_,
+ std::chrono::seconds(2),
+ 3,
+ 10);
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ // First transfer starts correctly.
+ EXPECT_TRUE(handler3.prepare_read_called);
+ EXPECT_FALSE(handler4.prepare_read_called);
+ ASSERT_EQ(ctx_.total_responses(), 1u);
+
+ // Try to start a simultaneous transfer to resource 4, for which the thread
+ // does not have an available context.
+ transfer_thread_.StartServerTransfer(
+ internal::TransferType::kTransmit,
+ ProtocolVersion::kLegacy,
+ 4,
+ 4,
+ EncodeChunk(
+ Chunk(ProtocolVersion::kLegacy, Chunk::Type::kParametersRetransmit)
+ .set_session_id(4)
+ .set_window_end_offset(16)
+ .set_max_chunk_size_bytes(8)
+ .set_offset(0)),
+ max_parameters_,
+ std::chrono::seconds(2),
+ 3,
+ 10);
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_FALSE(handler4.prepare_read_called);
+
+ ASSERT_EQ(ctx_.total_responses(), 2u);
+ auto chunk = DecodeChunk(ctx_.response());
+ EXPECT_EQ(chunk.session_id(), 4u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::ResourceExhausted());
+
+ transfer_thread_.RemoveTransferHandler(handler3);
+ transfer_thread_.RemoveTransferHandler(handler4);
+}
+
+TEST_F(TransferThreadTest, StartTransferExhausted_Client) {
+ rpc::RawClientReaderWriter read_stream = pw_rpc::raw::Transfer::Read(
+ rpc_client_context_.client(), rpc_client_context_.channel().id());
+ transfer_thread_.SetClientReadStream(read_stream);
+
+ Status status3 = Status::Unknown();
+ Status status4 = Status::Unknown();
+
+ stream::MemoryWriterBuffer<16> buffer3;
+ stream::MemoryWriterBuffer<16> buffer4;
+
+ transfer_thread_.StartClientTransfer(
+ internal::TransferType::kReceive,
+ ProtocolVersion::kLegacy,
+ 3,
+ &buffer3,
+ max_parameters_,
+ [&status3](Status status) { status3 = status; },
+ std::chrono::seconds(2),
+ 3,
+ 10);
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_EQ(status3, Status::Unknown());
+ EXPECT_EQ(status4, Status::Unknown());
+
+ // Try to start a simultaneous transfer to resource 4, for which the thread
+ // does not have an available context.
+ transfer_thread_.StartClientTransfer(
+ internal::TransferType::kReceive,
+ ProtocolVersion::kLegacy,
+ 4,
+ &buffer4,
+ max_parameters_,
+ [&status4](Status status) { status4 = status; },
+ std::chrono::seconds(2),
+ 3,
+ 10);
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_EQ(status3, Status::Unknown());
+ EXPECT_EQ(status4, Status::ResourceExhausted());
+
+ transfer_thread_.EndClientTransfer(3, Status::Cancelled());
+ transfer_thread_.EndClientTransfer(4, Status::Cancelled());
+}
+
+TEST_F(TransferThreadTest, VersionTwo_NoHandler) {
auto reader_writer = ctx_.reader_writer();
transfer_thread_.SetServerReadStream(reader_writer);
SimpleReadTransfer handler(3, kData);
transfer_thread_.AddTransferHandler(handler);
+ transfer_thread_.RemoveTransferHandler(handler);
- rpc::test::WaitForPackets(ctx_.output(), 1, [this] {
- transfer_thread_.StartServerTransfer(internal::TransferType::kTransmit,
- 3,
- 3,
- max_parameters_,
- std::chrono::seconds(2),
- 0);
+ transfer_thread_.StartServerTransfer(internal::TransferType::kTransmit,
+ ProtocolVersion::kVersionTwo,
+ /*session_id=*/421,
+ /*resource_id=*/7,
+ {},
+ max_parameters_,
+ std::chrono::seconds(2),
+ 3,
+ 10);
- // Malformed transfer parameters chunk without a pending_bytes field.
- transfer_thread_.ProcessServerChunk(EncodeChunk({.transfer_id = 3}));
- });
+ transfer_thread_.WaitUntilEventIsProcessed();
+
+ EXPECT_FALSE(handler.prepare_read_called);
ASSERT_EQ(ctx_.total_responses(), 1u);
+ Result<Chunk::Identifier> id = Chunk::ExtractIdentifier(ctx_.response());
+ ASSERT_TRUE(id.ok());
+ EXPECT_EQ(id->value(), 7u);
auto chunk = DecodeChunk(ctx_.response());
- EXPECT_EQ(chunk.transfer_id, 3u);
- ASSERT_TRUE(chunk.status.has_value());
- EXPECT_EQ(chunk.status.value(), Status::InvalidArgument());
-}
+ EXPECT_EQ(chunk.session_id(), 7u);
+ EXPECT_EQ(chunk.resource_id(), 7u);
+ ASSERT_TRUE(chunk.status().has_value());
+ EXPECT_EQ(chunk.status().value(), Status::NotFound());
-PW_MODIFY_DIAGNOSTICS_POP();
+ transfer_thread_.RemoveTransferHandler(handler);
+}
} // namespace
} // namespace pw::transfer::test
diff --git a/pw_transfer/ts/BUILD.bazel b/pw_transfer/ts/BUILD.bazel
deleted file mode 100644
index ca639ae64..000000000
--- a/pw_transfer/ts/BUILD.bazel
+++ /dev/null
@@ -1,74 +0,0 @@
-# Copyright 2021 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@npm//@bazel/typescript:index.bzl", "ts_library", "ts_project")
-load("@npm//@bazel/jasmine:index.bzl", "jasmine_node_test")
-load("//pw_protobuf_compiler/ts:ts_proto_collection.bzl", "ts_proto_collection")
-
-package(default_visibility = ["//visibility:public"])
-
-ts_proto_collection(
- name = "transfer_proto_collection",
- js_proto_library = "@//pw_transfer:transfer_proto_tspb",
- proto_library = "@//pw_transfer:transfer_proto",
-)
-
-ts_project(
- name = "lib",
- srcs = [
- "client.ts",
- "transfer.ts",
- ],
- declaration = True,
- source_map = True,
- deps = [
- "//pw_rpc/ts:pw_rpc",
- "//pw_status/ts:pw_status",
- "//pw_transfer:transfer_proto_tspb",
- "@npm//:node_modules", # can't use fine-grained deps
- ],
-)
-
-js_library(
- name = "pw_transfer",
- package_name = "@pigweed/pw_transfer",
- srcs = ["package.json"],
- deps = [
- ":lib",
- ],
-)
-
-ts_library(
- name = "test_lib",
- srcs = [
- "transfer_test.ts",
- ],
- deps = [
- ":lib",
- ":transfer_proto_collection",
- "//pw_rpc/ts:lib",
- "//pw_rpc/ts:packet_proto_tspb",
- "//pw_status/ts:pw_status",
- "//pw_transfer:transfer_proto_tspb",
- "@npm//@types/jasmine",
- ],
-)
-
-jasmine_node_test(
- name = "test",
- srcs = [
- ":test_lib",
- ],
-)
diff --git a/pw_transfer/ts/client.ts b/pw_transfer/ts/client.ts
index db1f80723..c39e37775 100644
--- a/pw_transfer/ts/client.ts
+++ b/pw_transfer/ts/client.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -18,9 +18,9 @@ import {
BidirectionalStreamingCall,
BidirectionalStreamingMethodStub,
ServiceClient,
-} from '@pigweed/pw_rpc';
-import {Status} from '@pigweed/pw_status';
-import {Chunk} from 'transfer_proto_tspb/transfer_proto_tspb_pb/pw_transfer/transfer_pb';
+} from 'pigweedjs/pw_rpc';
+import {Status} from 'pigweedjs/pw_status';
+import {Chunk} from 'pigweedjs/protos/pw_transfer/transfer_pb';
import {
ReadTransfer,
@@ -82,14 +82,16 @@ export class Manager {
* @throws Throws an error when the transfer fails to complete.
*/
async read(
- transferId: number,
+ resourceId: number,
progressCallback?: ProgressCallback
): Promise<Uint8Array> {
- if (transferId in this.readTransfers) {
- throw new Error(`Read transfer ${transferId} already exists`);
+ if (resourceId in this.readTransfers) {
+ throw new Error(
+ `Read transfer for resource ${resourceId} already exists`
+ );
}
const transfer = new ReadTransfer(
- transferId,
+ resourceId,
this.sendReadChunkCallback,
this.defaultResponseTimeoutS,
this.maxRetries,
@@ -121,16 +123,16 @@ export class Manager {
/**
Transmits (uploads) data to the server.
*
- * @param{number} transferId: ID of the write transfer
+ * @param{number} resourceId: ID of the resource to which to write.
* @param{Uint8Array} data: Data to send to the server.
*/
async write(
- transferId: number,
+ resourceId: number,
data: Uint8Array,
progressCallback?: ProgressCallback
): Promise<void> {
const transfer = new WriteTransfer(
- transferId,
+ resourceId,
data,
this.sendWriteChunkCallback,
this.defaultResponseTimeoutS,
diff --git a/pw_transfer/ts/package.json b/pw_transfer/ts/package.json
deleted file mode 100644
index 81ddd2148..000000000
--- a/pw_transfer/ts/package.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "name": "@pigweed/pw_transfer",
- "version": "1.0.0",
- "main": "index.js",
- "license": "Apache-2.0",
- "dependencies": {
- "@bazel/jasmine": "^4.1.0",
- "@pigweed/pw_rpc": "link:../../pw_status/pw_rpc",
- "@pigweed/pw_status": "link:../../pw_status/ts",
- "@types/jasmine": "^3.9.0",
- "jasmine": "^3.9.0",
- "jasmine-core": "^3.9.0"
- }
-}
diff --git a/pw_transfer/ts/transfer.ts b/pw_transfer/ts/transfer.ts
index 708f5cfcb..c098a8232 100644
--- a/pw_transfer/ts/transfer.ts
+++ b/pw_transfer/ts/transfer.ts
@@ -16,9 +16,9 @@ import {
BidirectionalStreamingCall,
BidirectionalStreamingMethodStub,
ServiceClient,
-} from '@pigweed/pw_rpc';
-import {Status} from '@pigweed/pw_status';
-import {Chunk} from 'transfer_proto_tspb/transfer_proto_tspb_pb/pw_transfer/transfer_pb';
+} from 'pigweedjs/pw_rpc';
+import {Status} from 'pigweedjs/pw_status';
+import {Chunk} from 'pigweedjs/protos/pw_transfer/transfer_pb';
export class ProgressStats {
constructor(
@@ -138,7 +138,7 @@ export abstract class Transfer {
const chunk = new Chunk();
chunk.setStatus(error);
chunk.setTransferId(this.id);
- chunk.setType(Chunk.Type.TRANSFER_COMPLETION);
+ chunk.setType(Chunk.Type.COMPLETION);
this.sendChunk(chunk);
this.finish(error);
}
@@ -253,7 +253,7 @@ export class ReadTransfer extends Transfer {
}
protected get initialChunk(): Chunk {
- return this.transferParameters(Chunk.Type.TRANSFER_START);
+ return this.transferParameters(Chunk.Type.START);
}
/** Builds an updated transfer parameters chunk to send the server. */
@@ -305,7 +305,7 @@ export class ReadTransfer extends Transfer {
const endChunk = new Chunk();
endChunk.setTransferId(this.id);
endChunk.setStatus(Status.OK);
- endChunk.setType(Chunk.Type.TRANSFER_COMPLETION);
+ endChunk.setType(Chunk.Type.COMPLETION);
this.sendChunk(endChunk);
this.finish(Status.OK);
return;
@@ -402,9 +402,12 @@ export class WriteTransfer extends Transfer {
}
protected get initialChunk(): Chunk {
+ // TODO(frolv): The session ID should not be set here but assigned by the
+ // server during an initial handshake.
const chunk = new Chunk();
chunk.setTransferId(this.id);
- chunk.setType(Chunk.Type.TRANSFER_START);
+ chunk.setResourceId(this.id);
+ chunk.setType(Chunk.Type.START);
return chunk;
}
@@ -515,7 +518,7 @@ export class WriteTransfer extends Transfer {
const chunk = new Chunk();
chunk.setTransferId(this.id);
chunk.setOffset(this.offset);
- chunk.setType(Chunk.Type.TRANSFER_DATA);
+ chunk.setType(Chunk.Type.DATA);
const maxBytesInChunk = Math.min(
this.maxChunkSize,
diff --git a/pw_transfer/ts/transfer_test.ts b/pw_transfer/ts/transfer_test.ts
index 1707a82f7..148ad44ac 100644
--- a/pw_transfer/ts/transfer_test.ts
+++ b/pw_transfer/ts/transfer_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2021 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,8 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
-import 'jasmine';
+/* eslint-env browser */
import {
Channel,
@@ -21,21 +20,21 @@ import {
decode,
MethodStub,
ServiceClient,
-} from '@pigweed/pw_rpc';
-import {Status} from '@pigweed/pw_status';
+} from 'pigweedjs/pw_rpc';
+import {Status} from 'pigweedjs/pw_status';
import {
PacketType,
RpcPacket,
-} from 'packet_proto_tspb/packet_proto_tspb_pb/pw_rpc/internal/packet_pb';
-import {ProtoCollection} from 'transfer_proto_collection/generated/ts_proto_collection';
-import {Chunk} from 'transfer_proto_tspb/transfer_proto_tspb_pb/pw_transfer/transfer_pb';
+} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
+import {ProtoCollection} from 'pigweedjs/protos/collection';
+import {Chunk} from 'pigweedjs/protos/pw_transfer/transfer_pb';
import {Manager} from './client';
import {ProgressStats} from './transfer';
const DEFAULT_TIMEOUT_S = 0.3;
-describe('Encoder', () => {
+describe('Transfer client', () => {
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
let client: Client;
@@ -112,13 +111,13 @@ describe('Encoder', () => {
}
function buildChunk(
- transferId: number,
+ sessionId: number,
offset: number,
data: string,
remainingBytes: number
): Chunk {
const chunk = new Chunk();
- chunk.setTransferId(transferId);
+ chunk.setTransferId(sessionId);
chunk.setOffset(offset);
chunk.setData(textEncoder.encode(data));
chunk.setRemainingBytes(remainingBytes);
@@ -133,8 +132,8 @@ describe('Encoder', () => {
const data = await manager.read(3);
expect(textDecoder.decode(data)).toEqual('abc');
- expect(sentChunks).toHaveSize(2);
- expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue();
+ expect(sentChunks).toHaveLength(2);
+ expect(sentChunks[sentChunks.length - 1].hasStatus()).toBe(true);
expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK);
});
@@ -147,8 +146,8 @@ describe('Encoder', () => {
const data = await manager.read(3);
expect(data).toEqual(textEncoder.encode('abcdef'));
- expect(sentChunks).toHaveSize(2);
- expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue();
+ expect(sentChunks).toHaveLength(2);
+ expect(sentChunks[sentChunks.length - 1].hasStatus()).toBe(true);
expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK);
});
@@ -165,8 +164,8 @@ describe('Encoder', () => {
progress.push(stats);
});
expect(textDecoder.decode(data)).toEqual('abcdef');
- expect(sentChunks).toHaveSize(2);
- expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue();
+ expect(sentChunks).toHaveLength(2);
+ expect(sentChunks[sentChunks.length - 1].hasStatus()).toBe(true);
expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK);
expect(progress).toEqual([
@@ -190,8 +189,8 @@ describe('Encoder', () => {
const data = await manager.read(3);
expect(data).toEqual(textEncoder.encode('123456789'));
- expect(sentChunks).toHaveSize(3);
- expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue();
+ expect(sentChunks).toHaveLength(3);
+ expect(sentChunks[sentChunks.length - 1].hasStatus()).toBe(true);
expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK);
});
@@ -205,8 +204,8 @@ describe('Encoder', () => {
expect(textDecoder.decode(data)).toEqual('xyz');
// Two transfer parameter requests should have been sent.
- expect(sentChunks).toHaveSize(3);
- expect(sentChunks[sentChunks.length - 1].hasStatus()).toBeTrue();
+ expect(sentChunks).toHaveLength(3);
+ expect(sentChunks[sentChunks.length - 1].hasStatus()).toBe(true);
expect(sentChunks[sentChunks.length - 1].getStatus()).toEqual(Status.OK);
});
@@ -221,7 +220,7 @@ describe('Encoder', () => {
.catch(error => {
expect(error.id).toEqual(27);
expect(Status[error.status]).toEqual(Status[Status.DEADLINE_EXCEEDED]);
- expect(sentChunks).toHaveSize(4);
+ expect(sentChunks).toHaveLength(4);
});
});
@@ -278,7 +277,7 @@ describe('Encoder', () => {
]);
await manager.write(4, textEncoder.encode('hello'));
- expect(sentChunks).toHaveSize(2);
+ expect(sentChunks).toHaveLength(2);
expect(receivedData()).toEqual(textEncoder.encode('hello'));
});
@@ -301,7 +300,7 @@ describe('Encoder', () => {
]);
await manager.write(4, textEncoder.encode('hello world'));
- expect(sentChunks).toHaveSize(3);
+ expect(sentChunks).toHaveLength(3);
expect(receivedData()).toEqual(textEncoder.encode('hello world'));
expect(sentChunks[1].getData()).toEqual(textEncoder.encode('hello wo'));
expect(sentChunks[2].getData()).toEqual(textEncoder.encode('rld'));
@@ -333,7 +332,7 @@ describe('Encoder', () => {
]);
await manager.write(4, textEncoder.encode('data to write'));
- expect(sentChunks).toHaveSize(3);
+ expect(sentChunks).toHaveLength(3);
expect(receivedData()).toEqual(textEncoder.encode('data to write'));
expect(sentChunks[1].getData()).toEqual(textEncoder.encode('data to '));
expect(sentChunks[2].getData()).toEqual(textEncoder.encode('write'));
@@ -444,7 +443,7 @@ describe('Encoder', () => {
progress.push(stats);
}
);
- expect(sentChunks).toHaveSize(3);
+ expect(sentChunks).toHaveLength(3);
expect(receivedData()).toEqual(textEncoder.encode('data to write'));
expect(sentChunks[1].getData()).toEqual(textEncoder.encode('data to '));
expect(sentChunks[2].getData()).toEqual(textEncoder.encode('write'));
@@ -497,7 +496,7 @@ describe('Encoder', () => {
]);
await manager.write(4, textEncoder.encode('pigweed data transfer'));
- expect(sentChunks).toHaveSize(5);
+ expect(sentChunks).toHaveLength(5);
expect(sentChunks[1].getData()).toEqual(textEncoder.encode('pigweed '));
expect(sentChunks[2].getData()).toEqual(textEncoder.encode('data tra'));
expect(sentChunks[3].getData()).toEqual(textEncoder.encode('eed data'));
@@ -589,7 +588,7 @@ describe('Encoder', () => {
fail('unexpected succesful write');
})
.catch(error => {
- expect(sentChunks).toHaveSize(3); // Initial chunk + two retries.
+ expect(sentChunks).toHaveLength(3); // Initial chunk + two retries.
expect(error.id).toEqual(22);
expect(Status[error.status]).toEqual(Status[Status.DEADLINE_EXCEEDED]);
});
@@ -613,17 +612,18 @@ describe('Encoder', () => {
.catch(error => {
const expectedChunk1 = new Chunk();
expectedChunk1.setTransferId(22);
- expectedChunk1.setType(Chunk.Type.TRANSFER_START);
+ expectedChunk1.setResourceId(22);
+ expectedChunk1.setType(Chunk.Type.START);
const expectedChunk2 = new Chunk();
expectedChunk2.setTransferId(22);
expectedChunk2.setData(textEncoder.encode('01234'));
- expectedChunk2.setType(Chunk.Type.TRANSFER_DATA);
+ expectedChunk2.setType(Chunk.Type.DATA);
const lastChunk = new Chunk();
lastChunk.setTransferId(22);
lastChunk.setData(textEncoder.encode('56789'));
lastChunk.setOffset(5);
lastChunk.setRemainingBytes(0);
- lastChunk.setType(Chunk.Type.TRANSFER_DATA);
+ lastChunk.setType(Chunk.Type.DATA);
const expectedChunks = [
expectedChunk1,
diff --git a/pw_transfer/ts/tsconfig.json b/pw_transfer/ts/tsconfig.json
deleted file mode 100644
index 0fab6a40f..000000000
--- a/pw_transfer/ts/tsconfig.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "compilerOptions": {
- "allowUnreachableCode": false,
- "allowUnusedLabels": false,
- "declaration": true,
- "forceConsistentCasingInFileNames": true,
- "lib": [
- "es2018",
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "module": "commonjs",
- "noEmitOnError": true,
- "noFallthroughCasesInSwitch": true,
- "noImplicitReturns": true,
- "pretty": true,
- "sourceMap": true,
- "strict": true,
- "target": "es2018",
- "jsx": "react",
- "plugins": [
- {
- "name": "@bazel/tsetse",
- "disabledRules": [
- "must-use-promises"
- ]
- }
- ]
- },
- "exclude": [
- "node_modules"
- ]
-}
-
diff --git a/pw_transfer/write.svg b/pw_transfer/write.svg
index fba4e985a..2749379bc 100644
--- a/pw_transfer/write.svg
+++ b/pw_transfer/write.svg
@@ -5,37 +5,37 @@
client -> server [
label = "start",
- leftnote = "transfer_id\ntype=TRANSFER_START"
+ leftnote = "session_id\ntype=TRANSFER_START"
];
client <- server [
noactivate,
label = "set transfer parameters",
- rightnote = "transfer_id\noffset\nwindow_end_offset\ntype=PARAMETERS_RETRANSMIT\nmax_chunk_size\nchunk_delay"
+ rightnote = "session_id\noffset\nwindow_end_offset\ntype=PARAMETERS_RETRANSMIT\nmax_chunk_size\nchunk_delay"
];
client -\-> server [
noactivate,
label = "requested bytes\n(zero or more chunks)",
- leftnote = "transfer_id\noffset\ndata\n(remaining_bytes)"
+ leftnote = "session_id\noffset\ndata\n(remaining_bytes)"
];
client <-\- server [
noactivate,
label = "update transfer parameters\n(as needed)",
- rightnote = "transfer_id\noffset\nwindow_end_offset\ntype=PARAMETERS_CONTINUE\n(max_chunk_size)\n(chunk_delay)"
+ rightnote = "session_id\noffset\nwindow_end_offset\ntype=PARAMETERS_CONTINUE\n(max_chunk_size)\n(chunk_delay)"
];
client -> server [
noactivate,
label = "final chunk",
- leftnote = "transfer_id\noffset\ndata\nremaining_bytes=0"
+ leftnote = "session_id\noffset\ndata\nremaining_bytes=0"
];
client <- server [
noactivate,
label = "acknowledge completion",
- rightnote = "transfer_id\nstatus=OK"
+ rightnote = "session_id\nstatus=OK"
];
}
@@ -71,14 +71,14 @@
<polygon fill="rgb(240,248,255)" points="24,120 155,120 163,128 163,148 24,148 24,120" stroke="rgb(0,0,0)"></polygon>
<path d="M 155 120 L 155 128" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 155 128 L 163 128" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="65" y="133">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="65" y="133">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="115" x="89" y="146">type=TRANSFER_START</text>
<path d="M 187 228 L 363 228" fill="none" stroke="rgb(0,0,0)"></path>
<polygon fill="rgb(0,0,0)" points="195,224 187,228 195,232" stroke="rgb(0,0,0)"></polygon>
<polygon fill="rgb(240,248,255)" points="387,188 561,188 569,196 569,268 387,268 387,188" stroke="rgb(0,0,0)"></polygon>
<path d="M 561 188 L 561 196" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 561 196 L 569 196" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="428" y="201">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="428" y="201">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="413" y="214">offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="103" x="446" y="227">window_end_offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="158" x="474" y="240">type=PARAMETERS_RETRANSMIT</text>
@@ -89,7 +89,7 @@
<polygon fill="rgb(240,248,255)" points="36,308 155,308 163,316 163,362 36,362 36,308" stroke="rgb(0,0,0)"></polygon>
<path d="M 155 308 L 155 316" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 155 316 L 163 316" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="77" y="321">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="77" y="321">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="62" y="334">offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="25" x="56" y="347">data</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="103" x="95" y="360">(remaining_bytes)</text>
@@ -98,7 +98,7 @@
<polygon fill="rgb(240,248,255)" points="387,402 549,402 557,410 557,482 387,482 387,402" stroke="rgb(0,0,0)"></polygon>
<path d="M 549 402 L 549 410" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 549 410 L 557 410" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="428" y="415">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="428" y="415">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="413" y="428">offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="103" x="446" y="441">window_end_offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="146" x="468" y="454">type=PARAMETERS_CONTINUE</text>
@@ -109,7 +109,7 @@
<polygon fill="rgb(240,248,255)" points="36,522 155,522 163,530 163,576 36,576 36,522" stroke="rgb(0,0,0)"></polygon>
<path d="M 155 522 L 155 530" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 155 530 L 163 530" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="77" y="535">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="77" y="535">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="37" x="62" y="548">offset</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="25" x="56" y="561">data</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="103" x="95" y="574">remaining_bytes=0</text>
@@ -118,7 +118,7 @@
<polygon fill="rgb(240,248,255)" points="387,617 470,617 478,625 478,645 387,645 387,617" stroke="rgb(0,0,0)"></polygon>
<path d="M 470 617 L 470 625" fill="none" stroke="rgb(0,0,0)"></path>
<path d="M 470 625 L 478 625" fill="none" stroke="rgb(0,0,0)"></path>
- <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="428" y="630">transfer_id</text>
+ <text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="67" x="428" y="630">session_id</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="55" x="422" y="643">status=OK</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="31" x="206" y="132">start</text>
<text fill="rgb(0,0,0)" font-family="sans-serif" font-size="11" font-style="normal" font-weight="normal" text-anchor="middle" textLength="140" x="293" y="226">set transfer parameters</text>
diff --git a/pw_unit_test/BUILD.bazel b/pw_unit_test/BUILD.bazel
index 7ef41c747..64749537c 100644
--- a/pw_unit_test/BUILD.bazel
+++ b/pw_unit_test/BUILD.bazel
@@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations under
# the License.
+load("@com_google_protobuf//:protobuf.bzl", "py_proto_library")
load(
"//pw_build:pigweed.bzl",
"pw_cc_binary",
@@ -35,20 +36,24 @@ pw_cc_library(
pw_cc_library(
name = "pw_unit_test",
+ deps = ["@pigweed_config//:pw_unit_test_googletest_backend"],
+)
+
+pw_cc_library(
+ name = "light",
srcs = [
"framework.cc",
+ "public/pw_unit_test/internal/framework.h",
],
- hdrs = [
- "public/pw_unit_test/event_handler.h",
- "public/pw_unit_test/framework.h",
- "public_overrides/gtest/gtest.h",
- ],
+ hdrs = ["public_overrides/gtest/gtest.h"],
includes = [
"public",
"public_overrides",
],
deps = [
":config",
+ ":event_handler",
+ "//pw_assert",
"//pw_polyfill",
"//pw_preprocessor",
"//pw_span",
@@ -57,6 +62,22 @@ pw_cc_library(
)
pw_cc_library(
+ name = "event_handler",
+ hdrs = ["public/pw_unit_test/event_handler.h"],
+ includes = ["public"],
+)
+
+pw_cc_library(
+ name = "googletest_style_event_handler",
+ srcs = ["googletest_style_event_handler.cc"],
+ hdrs = ["public/pw_unit_test/googletest_style_event_handler.h"],
+ deps = [
+ ":event_handler",
+ "//pw_preprocessor",
+ ],
+)
+
+pw_cc_library(
name = "simple_printing_event_handler",
srcs = ["simple_printing_event_handler.cc"],
hdrs = [
@@ -66,8 +87,39 @@ pw_cc_library(
"public",
],
deps = [
+ ":googletest_style_event_handler",
"//pw_preprocessor",
- "//pw_unit_test",
+ ],
+)
+
+pw_cc_library(
+ name = "simple_printing_main",
+ srcs = [
+ "simple_printing_main.cc",
+ ],
+ deps = [
+ ":pw_unit_test",
+ ":simple_printing_event_handler",
+ "//pw_span",
+ "//pw_sys_io",
+ ],
+)
+
+pw_cc_library(
+ name = "printf_event_handler",
+ hdrs = ["public/pw_unit_test/printf_event_handler.h"],
+ deps = [
+ ":googletest_style_event_handler",
+ "//pw_preprocessor",
+ ],
+)
+
+pw_cc_library(
+ name = "printf_main",
+ srcs = ["printf_main.cc"],
+ deps = [
+ ":printf_event_handler",
+ ":pw_unit_test",
],
)
@@ -83,8 +135,8 @@ pw_cc_library(
"public",
],
deps = [
+ ":googletest_style_event_handler",
"//pw_log",
- "//pw_unit_test",
],
)
@@ -99,23 +151,15 @@ pw_cc_binary(
],
)
-pw_cc_library(
- name = "main",
- srcs = [
- "simple_printing_main.cc",
- ],
- deps = [
- ":pw_unit_test",
- ":simple_printing_event_handler",
- "//pw_span",
- "//pw_sys_io",
- ],
-)
-
proto_library(
name = "unit_test_proto",
srcs = ["pw_unit_test_proto/unit_test.proto"],
- strip_import_prefix = "//pw_unit_test",
+ strip_import_prefix = "/pw_unit_test",
+)
+
+py_proto_library(
+ name = "unit_test_py_pb2",
+ srcs = ["pw_unit_test_proto/unit_test.proto"],
)
pw_proto_library(
@@ -126,13 +170,14 @@ pw_proto_library(
pw_cc_library(
name = "rpc_service",
srcs = [
- "rpc_event_handler.cc",
+ "rpc_light_event_handler.cc",
"unit_test_service.cc",
],
hdrs = [
- "public/pw_unit_test/internal/rpc_event_handler.h",
"public/pw_unit_test/unit_test_service.h",
+ "rpc_light_public/pw_unit_test/internal/rpc_event_handler.h",
],
+ includes = ["rpc_light_public"],
deps = [
":pw_unit_test",
":unit_test_cc.pwpb",
@@ -156,11 +201,41 @@ pw_cc_library(
],
)
+pw_cc_library(
+ name = "static_library_support",
+ srcs = ["static_library_support.cc"],
+ hdrs = ["public/pw_unit_test/static_library_support.h"],
+ includes = ["public"],
+ deps = [":light"], # This library only works with the light backend
+)
+
+pw_cc_library(
+ name = "tests_in_archive",
+ srcs = [
+ "static_library_archived_tests.cc",
+ "static_library_missing_archived_tests.cc",
+ ],
+ linkstatic = True,
+ visibility = ["//visibility:private"],
+ deps = [":pw_unit_test"],
+)
+
+pw_cc_test(
+ name = "static_library_support_test",
+ srcs = ["static_library_support_test.cc"],
+ deps = [
+ ":static_library_support",
+ ":tests_in_archive",
+ "//pw_assert",
+ ],
+)
+
pw_cc_test(
name = "framework_test",
srcs = ["framework_test.cc"],
deps = [
":pw_unit_test",
+ "//pw_assert",
],
)
@@ -175,3 +250,15 @@ filegroup(
# "//pw_rpc/system_server",
# ],
)
+
+# GTest is not yet supported in the Bazel build. This filegroup silences
+# warnings about these files not being included in the Bazel build.
+filegroup(
+ name = "gtest_support",
+ srcs = [
+ "googletest_handler_adapter.cc",
+ "public/pw_unit_test/googletest_handler_adapter.h",
+ "rpc_gtest_event_handler.cc",
+ "rpc_gtest_public/pw_unit_test/internal/rpc_event_handler.h",
+ ],
+)
diff --git a/pw_unit_test/BUILD.gn b/pw_unit_test/BUILD.gn
index f4c113f98..9ad2f5b8d 100644
--- a/pw_unit_test/BUILD.gn
+++ b/pw_unit_test/BUILD.gn
@@ -18,6 +18,7 @@ import("$dir_pw_build/module_config.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
+import("$dir_pw_toolchain/traits.gni")
import("$dir_pw_unit_test/test.gni")
declare_args() {
@@ -36,16 +37,17 @@ pool("unit_test_pool") {
depth = pw_unit_test_POOL_DEPTH
}
-config("default_config") {
- include_dirs = [
- "public",
- "public_overrides",
- ]
+config("public_include_path") {
+ include_dirs = [ "public" ]
+}
+
+config("public_overrides_include_path") {
+ include_dirs = [ "public_overrides" ]
}
pw_source_set("config") {
public = [ "public/pw_unit_test/config.h" ]
- public_configs = [ ":default_config" ]
+ public_configs = [ ":public_include_path" ]
public_deps = [
dir_pw_polyfill,
pw_unit_test_CONFIG,
@@ -53,27 +55,69 @@ pw_source_set("config") {
visibility = [ ":*" ]
}
-# pw_unit_test core library.
+# pw_unit_test facade. This provides a GoogleTest-compatible test framework.
pw_source_set("pw_unit_test") {
- public_configs = [ ":default_config" ]
+ public_deps = [ pw_unit_test_GOOGLETEST_BACKEND ]
+}
+
+# Lightweight unit test backend that implements a subset of GoogleTest.
+pw_source_set("light") {
+ public_configs = [
+ ":public_include_path",
+ ":public_overrides_include_path",
+ ]
public_deps = [
":config",
+ ":event_handler",
dir_pw_polyfill,
dir_pw_preprocessor,
- dir_pw_string,
+ dir_pw_span,
+ ]
+
+ # If C++17 is supported, depend on StringBuilder.
+ if (pw_toolchain_CXX_STANDARD >= pw_toolchain_STANDARD.CXX17) {
+ public_deps += [ "$dir_pw_string:builder" ]
+ }
+
+ deps = [ dir_pw_assert ]
+
+ public = [ "public_overrides/gtest/gtest.h" ]
+ sources = [
+ "framework.cc",
+ "public/pw_unit_test/internal/framework.h",
+ ]
+}
+
+pw_source_set("event_handler") {
+ public_configs = [ ":public_include_path" ]
+ public = [ "public/pw_unit_test/event_handler.h" ]
+}
+
+# Unit test event handler that provides GoogleTest-style output.
+pw_source_set("googletest_style_event_handler") {
+ public_deps = [
+ ":event_handler",
+ dir_pw_preprocessor,
]
- public = [
- "public/pw_unit_test/event_handler.h",
- "public/pw_unit_test/framework.h",
- "public_overrides/gtest/gtest.h",
+ public = [ "public/pw_unit_test/googletest_style_event_handler.h" ]
+ sources = [ "googletest_style_event_handler.cc" ]
+}
+
+pw_source_set("googletest_handler_adapter") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [
+ ":logging_event_handler",
+ "$dir_pw_third_party/googletest",
+ dir_pw_preprocessor,
]
- sources = [ "framework.cc" ]
+ public = [ "public/pw_unit_test/googletest_handler_adapter.h" ]
+ sources = [ "googletest_handler_adapter.cc" ]
}
# Library providing an event handler which outputs human-readable text.
pw_source_set("simple_printing_event_handler") {
public_deps = [
- ":pw_unit_test",
+ ":googletest_style_event_handler",
"$dir_pw_preprocessor",
]
public = [ "public/pw_unit_test/simple_printing_event_handler.h" ]
@@ -84,51 +128,80 @@ pw_source_set("simple_printing_event_handler") {
# framework. Unit test files can link against this library to build runnable
# unit test executables.
pw_source_set("simple_printing_main") {
- public_deps = [ ":pw_unit_test" ]
deps = [
+ ":pw_unit_test",
":simple_printing_event_handler",
"$dir_pw_sys_io",
+ dir_pw_span,
]
sources = [ "simple_printing_main.cc" ]
}
+pw_source_set("printf_event_handler") {
+ public_deps = [
+ ":googletest_style_event_handler",
+ dir_pw_preprocessor,
+ ]
+ public = [ "public/pw_unit_test/printf_event_handler.h" ]
+}
+
+pw_source_set("printf_main") {
+ deps = [
+ ":printf_event_handler",
+ ":pw_unit_test",
+ ]
+ sources = [ "printf_main.cc" ]
+}
+
# Library providing an event handler which logs using pw_log.
pw_source_set("logging_event_handler") {
public_deps = [
- ":pw_unit_test",
- "$dir_pw_log",
- "$dir_pw_preprocessor",
+ ":googletest_style_event_handler",
+ dir_pw_log,
]
public = [ "public/pw_unit_test/logging_event_handler.h" ]
sources = [ "logging_event_handler.cc" ]
}
pw_source_set("logging_main") {
- public_deps = [ ":pw_unit_test" ]
deps = [
":logging_event_handler",
- "$dir_pw_sys_io",
+ ":pw_unit_test",
]
sources = [ "logging_main.cc" ]
}
+config("rpc_service_backend_light") {
+ include_dirs = [ "rpc_light_public" ]
+}
+
+config("rpc_service_backend_gtest") {
+ include_dirs = [ "rpc_gtest_public" ]
+}
+
pw_source_set("rpc_service") {
- public_configs = [ ":default_config" ]
+ public_configs = [ ":public_include_path" ]
public_deps = [
+ ":event_handler",
":pw_unit_test",
":unit_test_proto.pwpb",
":unit_test_proto.raw_rpc",
"$dir_pw_containers:vector",
]
deps = [ dir_pw_log ]
- public = [
- "public/pw_unit_test/internal/rpc_event_handler.h",
- "public/pw_unit_test/unit_test_service.h",
- ]
- sources = [
- "rpc_event_handler.cc",
- "unit_test_service.cc",
- ]
+ public = [ "public/pw_unit_test/unit_test_service.h" ]
+ sources = [ "unit_test_service.cc" ]
+ defines = []
+
+ if (pw_unit_test_GOOGLETEST_BACKEND == "$dir_pw_unit_test:light") {
+ public_configs += [ ":rpc_service_backend_light" ]
+ sources += [ "rpc_light_event_handler.cc" ]
+ public += [ "rpc_light_public/pw_unit_test/internal/rpc_event_handler.h" ]
+ } else {
+ public_configs += [ ":rpc_service_backend_gtest" ]
+ sources += [ "rpc_gtest_event_handler.cc" ]
+ public += [ "rpc_gtest_public/pw_unit_test/internal/rpc_event_handler.h" ]
+ }
}
pw_source_set("rpc_main") {
@@ -141,6 +214,13 @@ pw_source_set("rpc_main") {
sources = [ "rpc_main.cc" ]
}
+pw_source_set("static_library_support") {
+ public_configs = [ ":public_include_path" ]
+ public_deps = [ ":light" ] # This library only works with the light backend
+ public = [ "public/pw_unit_test/static_library_support.h" ]
+ sources = [ "static_library_support.cc" ]
+}
+
pw_executable("test_rpc_server") {
sources = [ "test_rpc_server.cc" ]
deps = [
@@ -160,10 +240,55 @@ pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+pw_test("metadata_only_test") {
+ extra_metadata = {
+ extra_key = "extra_value"
+ }
+}
+
+# pw_test_group produces the metadata file for its tests.
+pw_test_group("metadata_only_group") {
+ tests = [ ":metadata_only_test" ]
+}
+
+pw_python_script("test_group_metadata_test") {
+ sources = [ "py/test_group_metadata_test.py" ]
+ action = {
+ args = [
+ "--stamp-path",
+ "<TARGET_FILE(:metadata_only_group)>",
+ ]
+ deps = [ ":metadata_only_group" ]
+ stamp = true
+ }
+}
+
pw_test("framework_test") {
sources = [ "framework_test.cc" ]
+ deps = [ dir_pw_assert ]
+}
+
+pw_static_library("tests_in_archive") {
+ sources = [
+ "static_library_archived_tests.cc",
+ "static_library_missing_archived_tests.cc",
+ ]
+ deps = [ ":pw_unit_test" ]
+ visibility = [ ":*" ]
+}
+
+pw_test("static_library_support_test") {
+ sources = [ "static_library_support_test.cc" ]
+ deps = [
+ ":static_library_support",
+ ":tests_in_archive",
+ dir_pw_assert,
+ ]
}
pw_test_group("tests") {
- tests = [ ":framework_test" ]
+ tests = [
+ ":framework_test",
+ ":static_library_support_test",
+ ]
}
diff --git a/pw_unit_test/CMakeLists.txt b/pw_unit_test/CMakeLists.txt
index 5adaebead..f66f9dfdc 100644
--- a/pw_unit_test/CMakeLists.txt
+++ b/pw_unit_test/CMakeLists.txt
@@ -16,15 +16,23 @@ include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
pw_add_module_config(pw_unit_test_CONFIG)
-pw_add_module_library(pw_unit_test.config
+pw_add_library(pw_unit_test.config INTERFACE
+ HEADERS
+ public/pw_unit_test/config.h
+ PUBLIC_INCLUDES
+ public
PUBLIC_DEPS
${pw_unit_test_CONFIG}
pw_polyfill
- HEADERS
- public/pw_unit_test/config.h
)
-pw_add_module_library(pw_unit_test
+add_library(pw_unit_test INTERFACE)
+target_link_libraries(pw_unit_test
+ INTERFACE
+ "${pw_unit_test_GOOGLETEST_BACKEND}"
+)
+
+pw_add_library(pw_unit_test.light STATIC
SOURCES
framework.cc
PUBLIC_DEPS
@@ -32,19 +40,68 @@ pw_add_module_library(pw_unit_test
pw_preprocessor
pw_string
pw_unit_test.config
+ PUBLIC_INCLUDES
+ public_overrides # pw_unit_test overrides the gtest/gtest.h header.
+)
+
+pw_add_library(pw_unit_test.static_library_support STATIC
+ HEADERS
+ public/pw_unit_test/static_library_support.h
+ PUBLIC_INCLUDES
+ public
+ SOURCES
+ static_library_support.cc
+ PUBLIC_DEPS
+ pw_unit_test.light
+)
+
+pw_add_library(pw_unit_test.event_handler INTERFACE
+ HEADERS
+ public/pw_unit_test/event_handler.h
+ PUBLIC_INCLUDES
+ public
)
-# pw_unit_test overrides the gtest/gtest.h header.
-target_include_directories(pw_unit_test PUBLIC public_overrides)
+pw_add_library(pw_unit_test.googletest_style_event_handler STATIC
+ HEADERS
+ public/pw_unit_test/googletest_style_event_handler.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_unit_test.event_handler
+ pw_preprocessor
+ SOURCES
+ googletest_style_event_handler.cc
+)
-pw_add_module_library(pw_unit_test.main
+pw_add_library(pw_unit_test.simple_printing_main STATIC
SOURCES
simple_printing_main.cc
simple_printing_event_handler.cc
- PUBLIC_DEPS
- pw_unit_test
PRIVATE_DEPS
+ pw_unit_test
+ pw_unit_test.googletest_style_event_handler
pw_preprocessor
pw_string
pw_sys_io
)
+
+pw_add_library(pw_unit_test.logging_event_handler STATIC
+ HEADERS
+ public/pw_unit_test/logging_event_handler.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_log
+ pw_unit_test.googletest_style_event_handler
+ SOURCES
+ logging_event_handler.cc
+)
+
+pw_add_library(pw_unit_test.logging_main STATIC
+ SOURCES
+ logging_main.cc
+ PRIVATE_DEPS
+ pw_unit_test.logging_event_handler
+ pw_unit_test
+)
diff --git a/pw_unit_test/README.md b/pw_unit_test/README.md
index 9ce966e10..4b93552dc 100644
--- a/pw_unit_test/README.md
+++ b/pw_unit_test/README.md
@@ -1,6 +1,6 @@
# pw\_unit\_test: Lightweight C++ unit testing framework
The pw\_unit\_test module contains the code for *Pigweed Test*, a
-[Googletest](https://github.com/google/googletest/blob/HEAD/docs/primer.md)-compatible
+[GoogleTest](https://github.com/google/googletest/blob/HEAD/docs/primer.md)-compatible
unit testing framework that runs on anything from bare-metal microcontrollers
to large desktop operating systems.
diff --git a/pw_unit_test/docs.rst b/pw_unit_test/docs.rst
index f96c56abc..12d1e9649 100644
--- a/pw_unit_test/docs.rst
+++ b/pw_unit_test/docs.rst
@@ -3,35 +3,39 @@
============
pw_unit_test
============
-``pw_unit_test`` unit testing library with a `Google Test`_-compatible API,
-built on top of embedded-friendly primitives.
+``pw_unit_test`` provides a `GoogleTest`_-compatible unit testing API for
+Pigweed. The default implementation is the embedded-friendly
+``pw_unit_test:light`` backend. Upstream GoogleTest may be used as well (see
+`Using upstream GoogleTest`_).
-.. _Google Test: https://github.com/google/googletest/blob/HEAD/docs/primer.md
-
-``pw_unit_test`` is a portable library which can run on almost any system from
-bare metal to a full-fledged desktop OS. It does this by offloading the
-responsibility of test reporting and output to the underlying system,
-communicating its results through a common interface. Unit tests can be written
-once and run under many different environments, empowering developers to write
-robust, high quality code.
-
-``pw_unit_test`` is still under development and lacks many features expected in
-a complete testing framework; nevertheless, it is already used heavily within
-Pigweed.
+.. _GoogleTest: https://github.com/google/googletest
.. note::
This documentation is currently incomplete.
-------------------
-Writing unit tests
-------------------
-``pw_unit_test``'s interface is largely compatible with `Google Test`_. Refer to
-the Google Test documentation for examples of to define unit test cases.
+-------------------------------------------
+pw_unit_test:light: GoogleTest for Embedded
+-------------------------------------------
+``pw_unit_test:light`` implements a subset of `GoogleTest`_ with lightweight,
+embedded-friendly primitives. It is also highly portable and can run on almost
+any system from bare metal to a full-fledged desktop OS. It does this by
+offloading the responsibility of test reporting and output to the underlying
+system, communicating its results through a common interface. Unit tests can be
+written once and run under many different environments, empowering developers to
+write robust, high quality code.
+
+``pw_unit_test:light`` usage is the same as GoogleTest;
+refer to the `GoogleTest documentation <https://google.github.io/googletest/>`_
+for examples of how to define unit test cases.
+
+``pw_unit_test:light`` is still under development and lacks many features
+expected in a complete testing framework; nevertheless, it is already used
+heavily within Pigweed.
.. note::
- Many of Google Test's more advanced features are not yet implemented. Missing
+ Many of GoogleTest's more advanced features are not yet implemented. Missing
features include:
* Any GoogleMock features (e.g. :c:macro:`EXPECT_THAT`)
@@ -43,19 +47,15 @@ the Google Test documentation for examples of to define unit test cases.
To request a feature addition, please
`let us know <mailto:pigweed@googlegroups.com>`_.
- See `Using upstream Googletest and Googlemock` below for information
- about using upstream Googletest instead.
-
-------------------------
-Using the test framework
-------------------------
+ See `Using upstream GoogleTest`_ below for information
+ about using upstream GoogleTest instead.
The EventHandler interface
==========================
The ``EventHandler`` class in ``public/pw_unit_test/event_handler.h`` defines
-the interface through which ``pw_unit_test`` communicates the results of its
-test runs. A platform using ``pw_unit_test`` must register an event handler with
-the unit testing framework to receive test output.
+the interface through which ``pw_unit_test:light`` communicates the results of
+its test runs. A platform using the ``pw_unit_test:light`` backend must register
+an event handler with the unit testing framework to receive test output.
As the framework runs tests, it calls the event handler's callback functions to
notify the system of various test events. The system can then choose to perform
@@ -65,61 +65,54 @@ the developer.
Predefined event handlers
-------------------------
Pigweed provides some standard event handlers upstream to simplify the process
-of getting started using ``pw_unit_test``.
-
-* ``SimplePrintingEventHandler``: An event handler that writes Google Test-style
- output to a specified sink.
-
- .. code::
-
- [==========] Running all tests.
- [ RUN ] Status.Default
- [ OK ] Status.Default
- [ RUN ] Status.ConstructWithStatusCode
- [ OK ] Status.ConstructWithStatusCode
- [ RUN ] Status.AssignFromStatusCode
- [ OK ] Status.AssignFromStatusCode
- [ RUN ] Status.CompareToStatusCode
- [ OK ] Status.CompareToStatusCode
- [ RUN ] Status.Ok_OkIsTrue
- [ OK ] Status.Ok_OkIsTrue
- [ RUN ] Status.NotOk_OkIsFalse
- [ OK ] Status.NotOk_OkIsFalse
- [ RUN ] Status.KnownString
- [ OK ] Status.KnownString
- [ RUN ] Status.UnknownString
- [ OK ] Status.UnknownString
- [==========] Done running all tests.
- [ PASSED ] 8 test(s).
-
-
-* ``LoggingEventHandler``: An event handler which uses the ``pw_log`` module to
- output test results, to integrate with the system's existing logging setup.
+of getting started using ``pw_unit_test:light``. All event handlers provide for
+GoogleTest-style output using the shared
+:cpp:class:`pw::unit_test::GoogleTestStyleEventHandler` base.
-.. _running-tests:
+.. code-block::
-Running tests
-=============
-To run unit tests, link the tests into a single binary with the unit testing
-framework, register an event handler, and call the ``RUN_ALL_TESTS`` macro.
+ [==========] Running all tests.
+ [ RUN ] Status.Default
+ [ OK ] Status.Default
+ [ RUN ] Status.ConstructWithStatusCode
+ [ OK ] Status.ConstructWithStatusCode
+ [ RUN ] Status.AssignFromStatusCode
+ [ OK ] Status.AssignFromStatusCode
+ [ RUN ] Status.CompareToStatusCode
+ [ OK ] Status.CompareToStatusCode
+ [ RUN ] Status.Ok_OkIsTrue
+ [ OK ] Status.Ok_OkIsTrue
+ [ RUN ] Status.NotOk_OkIsFalse
+ [ OK ] Status.NotOk_OkIsFalse
+ [ RUN ] Status.KnownString
+ [ OK ] Status.KnownString
+ [ RUN ] Status.UnknownString
+ [ OK ] Status.UnknownString
+ [==========] Done running all tests.
+ [ PASSED ] 8 test(s).
+
+.. cpp:namespace-push:: pw::unit_test
-.. code:: cpp
+.. cpp:class:: GoogleTestStyleEventHandler
- #include "pw_unit_test/framework.h"
- #include "pw_unit_test/simple_printing_event_handler.h"
+ Provides GoogleTest-style output for ``pw_unit_test:light`` events. Must be
+ extended to define how to output the results.
- void WriteString(const std::string_view& string, bool newline) {
- printf("%s", string.data());
- if (newline) {
- printf("\n");
- }
- }
+.. cpp:class:: SimplePrintingEventHandler : public GoogleTestStyleEventHandler
- int main() {
- pw::unit_test::SimplePrintingEventHandler handler(WriteString);
- pw::unit_test::RegisterEventHandler(&handler);
- return RUN_ALL_TESTS();
- }
+ An event handler that writes GoogleTest-style output to a specified sink.
+
+.. cpp:class:: LoggingEventHandler : public GoogleTestStyleEventHandler
+
+ An event handler which uses the ``pw_log`` module to output test results, to
+ integrate with the system's existing logging setup.
+
+.. cpp:class:: PrintfEventHandler : public GoogleTestStyleEventHandler
+
+ A C++14-compatible event handler that uses ``std::printf`` to output test
+ results.
+
+.. cpp:namespace-pop::
Test filtering
==============
@@ -131,37 +124,96 @@ Currently, only a test suite filter is supported. This is set by calling
``pw::unit_test::SetTestSuitesToRun`` with a list of suite names.
.. note::
- Test filtering is only supported in C++17.
+
+ Test filtering is only supported in C++17.
+
+Tests in static libraries
+=========================
+The linker usually ignores tests linked through a static library (``.a`` file).
+This is because test registration relies on the test instance's static
+constructor adding itself to a global list of tests. When linking against a
+static library, static constructors in an object file will be ignored unless at
+least one entity in that object file is linked.
+
+Pigweed's ``pw_unit_test`` implementation provides the
+:c:macro:`PW_UNIT_TEST_LINK_FILE_CONTAINING_TEST` macro to support running tests
+in a static library.
+
+.. c:macro:: PW_UNIT_TEST_LINK_FILE_CONTAINING_TEST(test_suite_name, test_name)
+
+ Ensures tests in a static library are linked and executed. Provide the test
+ suite name and test name for one test in the file linked into a static
+ library. Any test in the file may be used, but it is recommended to use the
+ first for consistency. The test must be in a static library that is a
+ dependency of this target. Referring to a test that does not exist causes a
+ linker error.
+
+.. _running-tests:
+
+-------------
+Running tests
+-------------
+To run unit tests, link the tests into a single binary with the unit testing
+framework, configure the backend if needed, and call the ``RUN_ALL_TESTS``
+macro.
+
+The following example shows how to write a main function that runs
+``pw_unit_test`` with the ``light`` backend.
+
+.. code-block:: cpp
+
+ #include "gtest/gtest.h"
+
+ // pw_unit_test:light requires an event handler to be configured.
+ #include "pw_unit_test/simple_printing_event_handler.h"
+
+ void WriteString(const std::string_view& string, bool newline) {
+ printf("%s", string.data());
+ if (newline) {
+ printf("\n");
+ }
+ }
+
+ int main() {
+ // Since we are using pw_unit_test:light, set up an event handler.
+ pw::unit_test::SimplePrintingEventHandler handler(WriteString);
+ pw::unit_test::RegisterEventHandler(&handler);
+ return RUN_ALL_TESTS();
+ }
Build system integration
========================
-``pw_unit_test`` integrates directly into Pigweed's GN build system. To define
-simple unit tests, set the ``pw_unit_test_MAIN`` build variable to a target
-which configures the test framework as described in the :ref:`running-tests`
-section, and use the ``pw_test`` template to register your test code.
+``pw_unit_test`` integrates directly into Pigweed's GN and CMake build systems.
-.. code::
+The ``pw_unit_test`` module provides a few optional libraries to simplify setup:
- import("$dir_pw_unit_test/test.gni")
+- ``simple_printing_event_handler``: When running tests, output test results
+ as plain text over ``pw_sys_io``.
+- ``simple_printing_main``: Implements a ``main()`` function that simply runs
+ tests using the ``simple_printing_event_handler``.
+- ``logging_event_handler``: When running tests, log test results as
+ plain text using pw_log (ensure your target has set a ``pw_log`` backend).
+- ``logging_main``: Implements a ``main()`` function that simply runs tests
+ using the ``logging_event_handler``.
- pw_test("foo_test") {
- sources = [ "foo_test.cc" ]
- }
-The ``pw_unit_test`` module provides a few optional libraries to simplify setup:
+GN
+--
+To define simple unit tests, set the ``pw_unit_test_MAIN`` build variable to a
+target which configures the test framework as described in the
+:ref:`running-tests` section, and use the ``pw_test`` template to register your
+test code.
- - ``simple_printing_event_handler``: When running tests, output test results
- as plain text over ``pw_sys_io``.
- - ``simple_printing_main``: Implements a ``main()`` function that simply runs
- tests using the ``simple_printing_event_handler``.
- - ``logging_event_handler``: When running tests, log test results as
- plain text using pw_log (ensure your target has set a ``pw_log`` backend).
- - ``logging_main``: Implements a ``main()`` function that simply runs tests
- using the ``logging_event_handler``.
+.. code-block::
+ import("$dir_pw_unit_test/test.gni")
+
+ pw_test("foo_test") {
+ sources = [ "foo_test.cc" ]
+ }
pw_test template
-----------------
+````````````````
``pw_test`` defines a single unit test suite. It creates several sub-targets.
* ``<target_name>``: The test suite within a single binary. The test code is
@@ -179,20 +231,22 @@ pw_test template
* ``test_main``: Target label to add to the tests's dependencies to provide the
``main()`` function. Defaults to ``pw_unit_test_MAIN``. Set to ``""`` if
``main()`` is implemented in the test's ``sources``.
+* ``test_automatic_runner_args``: Array of args to pass to automatic test
+ runner. Defaults to ``pw_unit_test_AUTOMATIC_RUNNER_ARGS``.
**Example**
.. code::
- import("$dir_pw_unit_test/test.gni")
+ import("$dir_pw_unit_test/test.gni")
- pw_test("large_test") {
- sources = [ "large_test.cc" ]
- enable_if = device_has_1m_flash
- }
+ pw_test("large_test") {
+ sources = [ "large_test.cc" ]
+ enable_if = device_has_1m_flash
+ }
pw_test_group template
-----------------------
+``````````````````````
``pw_test_group`` defines a collection of tests or other test groups. It creates
several sub-targets:
@@ -218,30 +272,33 @@ several sub-targets:
.. code::
- import("$dir_pw_unit_test/test.gni")
+ import("$dir_pw_unit_test/test.gni")
- pw_test_group("tests") {
- tests = [
- ":bar_test",
- ":foo_test",
- ]
- }
+ pw_test_group("tests") {
+ tests = [
+ ":bar_test",
+ ":foo_test",
+ ]
+ }
- pw_test("foo_test") {
- # ...
- }
+ pw_test("foo_test") {
+ # ...
+ }
- pw_test("bar_test") {
- # ...
- }
+ pw_test("bar_test") {
+ # ...
+ }
pw_facade_test template
------------------------
+```````````````````````
Pigweed facade test templates allow individual unit tests to build under the
current device target configuration while overriding specific build arguments.
This allows these tests to replace a facade's backend for the purpose of testing
the facade layer.
+Facade tests are disabled by default. To build and run facade tests, set the GN
+arg :option:`pw_unit_test_FACADE_TESTS_ENABLED` to ``true``.
+
.. warning::
Facade tests are costly because each facade test will trigger a re-build of
every dependency of the test. While this sounds excessive, it's the only
@@ -253,79 +310,249 @@ the facade layer.
facade being tested.
Build arguments
----------------
+```````````````
+.. option:: pw_unit_test_GOOGLETEST_BACKEND <source_set>
+
+ The GoogleTest implementation to use for Pigweed unit tests. This library
+ provides "gtest/gtest.h" and related headers. Defaults to pw_unit_test:light,
+ which implements a subset of GoogleTest.
+
+ Type: string (GN path to a source set)
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_MAIN <source_set>
+
+ Implementation of a main function for ``pw_test`` unit test binaries.
+
+ Type: string (GN path to a source set)
+ Usage: toolchain-controlled only
.. option:: pw_unit_test_AUTOMATIC_RUNNER <executable>
- Path to a test runner to automatically run unit tests after they are built.
+ Path to a test runner to automatically run unit tests after they are built.
- If set, a ``pw_test`` target's ``<target_name>.run`` action will invoke the
- test runner specified by this argument, passing the path to the unit test to
- run. If this is unset, the ``pw_test`` target's ``<target_name>.run`` step
- will do nothing.
+ If set, a ``pw_test`` target's ``<target_name>.run`` action will invoke the
+ test runner specified by this argument, passing the path to the unit test to
+ run. If this is unset, the ``pw_test`` target's ``<target_name>.run`` step
+ will do nothing.
- Targets that don't support parallelized execution of tests (e.g. a on-device
- test runner that must flash a device and run the test in serial) should
- set pw_unit_test_POOL_DEPTH to 1.
+ Targets that don't support parallelized execution of tests (e.g. a on-device
+ test runner that must flash a device and run the test in serial) should set
+ pw_unit_test_POOL_DEPTH to 1.
- Type: string (name of an executable on the PATH, or path to an executable)
- Usage: toolchain-controlled only
+ Type: string (name of an executable on the PATH, or path to an executable)
+ Usage: toolchain-controlled only
.. option:: pw_unit_test_AUTOMATIC_RUNNER_ARGS <args>
- An optional list of strings to pass as args to the test runner specified
- by pw_unit_test_AUTOMATIC_RUNNER.
+ An optional list of strings to pass as args to the test runner specified by
+ pw_unit_test_AUTOMATIC_RUNNER.
- Type: list of strings (args to pass to pw_unit_test_AUTOMATIC_RUNNER)
- Usage: toolchain-controlled only
+ Type: list of strings (args to pass to pw_unit_test_AUTOMATIC_RUNNER)
+ Usage: toolchain-controlled only
.. option:: pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT <timeout_seconds>
- An optional timeout to apply when running the automatic runner. Timeout is
- in seconds. Defaults to empty which means no timeout.
+ An optional timeout to apply when running the automatic runner. Timeout is
+ in seconds. Defaults to empty which means no timeout.
- Type: string (number of seconds to wait before killing test runner)
- Usage: toolchain-controlled only
+ Type: string (number of seconds to wait before killing test runner)
+ Usage: toolchain-controlled only
-.. option:: pw_unit_test_PUBLIC_DEPS <dependencies>
+.. option:: pw_unit_test_POOL_DEPTH <pool_depth>
- Additional dependencies required by all unit test targets. (For example, if
- using a different test library like Googletest.)
+ The maximum number of unit tests that may be run concurrently for the
+ current toolchain. Setting this to 0 disables usage of a pool, allowing
+ unlimited parallelization.
- Type: list of strings (list of dependencies as GN paths)
- Usage: toolchain-controlled only
+ Note: A single target with two toolchain configurations (e.g. release/debug)
+ will use two separate test runner pools by default. Set
+ pw_unit_test_POOL_TOOLCHAIN to the same toolchain for both targets to
+ merge the pools and force serialization.
-.. option:: pw_unit_test_MAIN <source_set>
+ Type: integer
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_POOL_TOOLCHAIN <toolchain>
- Implementation of a main function for ``pw_test`` unit test binaries.
+ The toolchain to use when referring to the pw_unit_test runner pool. When
+ this is disabled, the current toolchain is used. This means that every
+ toolchain will use its own pool definition. If two toolchains should share
+ the same pool, this argument should be by one of the toolchains to the GN
+ path of the other toolchain.
- Type: string (GN path to a source set)
- Usage: toolchain-controlled only
+ Type: string (GN path to a toolchain)
+ Usage: toolchain-controlled only
-.. option:: pw_unit_test_POOL_DEPTH <pool_depth>
+.. option:: pw_unit_test_EXECUTABLE_TARGET_TYPE <template name>
- The maximum number of unit tests that may be run concurrently for the
- current toolchain. Setting this to 0 disables usage of a pool, allowing
- unlimited parallelization.
+ The name of the GN target type used to build pw_unit_test executables.
- Note: A single target with two toolchain configurations (e.g. release/debug)
- will use two separate test runner pools by default. Set
- pw_unit_test_POOL_TOOLCHAIN to the same toolchain for both targets to
- merge the pools and force serialization.
+ Type: string (name of a GN template)
+ Usage: toolchain-controlled only
- Type: integer
- Usage: toolchain-controlled only
+.. option:: pw_unit_test_EXECUTABLE_TARGET_TYPE_FILE <gni file path>
-.. option:: pw_unit_test_POOL_TOOLCHAIN <toolchain>
+ The path to the .gni file that defines pw_unit_test_EXECUTABLE_TARGET_TYPE.
+
+ If pw_unit_test_EXECUTABLE_TARGET_TYPE is not the default of
+ `pw_executable`, this .gni file is imported to provide the template
+ definition.
+
+ Type: string (path to a .gni file)
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_FACADE_TESTS_ENABLED <boolean>
- The toolchain to use when referring to the pw_unit_test runner pool. When
- this is disabled, the current toolchain is used. This means that every
- toolchain will use its own pool definition. If two toolchains should share
- the same pool, this argument should be by one of the toolchains to the GN
- path of the other toolchain.
+ Controls whether to build and run facade tests. Facade tests add considerably
+ to build time, so they are disabled by default.
+
+CMake
+-----
+pw_add_test function
+````````````````````
+``pw_add_test`` declares a single unit test suite. It creates several
+sub-targets.
+
+* ``{NAME}`` depends on ``${NAME}.run`` if ``pw_unit_test_AUTOMATIC_RUNNER`` is
+ set, else it depends on ``${NAME}.bin``
+* ``{NAME}.lib`` contains the provided test sources as a library target, which
+ can then be linked into a test executable.
+* ``{NAME}.bin`` is a standalone executable which contains only the test sources
+ specified in the pw_unit_test_template.
+* ``{NAME}.run`` which runs the unit test executable after building it if
+ ``pw_unit_test_AUTOMATIC_RUNNER`` is set, else it fails to build.
+
+**Required Arguments**
+
+* ``NAME``: name to use for the produced test targets specified above
+
+**Optional Arguments**
+
+* ``SOURCES`` - source files for this library
+* ``HEADERS``- header files for this library
+* ``PRIVATE_DEPS``- private pw_target_link_targets arguments
+* ``PRIVATE_INCLUDES``- public target_include_directories argument
+* ``PRIVATE_DEFINES``- private target_compile_definitions arguments
+* ``PRIVATE_COMPILE_OPTIONS``- private target_compile_options arguments
+* ``PRIVATE_LINK_OPTIONS``- private target_link_options arguments
+
+**Example**
+
+.. code::
+
+ include($ENV{PW_ROOT}/pw_unit_test/test.cmake)
+
+ pw_add_test(my_module.foo_test
+ SOURCES
+ foo_test.cc
+ PRIVATE_DEPS
+ my_module.foo
+ )
+
+pw_add_test_group function
+``````````````````````````
+``pw_add_test_group`` defines a collection of tests or other test groups. It
+creates several sub-targets:
+
+* ``{NAME}`` depends on ``${NAME}.run`` if ``pw_unit_test_AUTOMATIC_RUNNER`` is
+ set, else it depends on ``${NAME}.bin``.
+* ``{NAME}.bundle`` depends on ``${NAME}.bundle.run`` if
+ ``pw_unit_test_AUTOMATIC_RUNNER`` is set, else it depends on
+ ``${NAME}.bundle.bin``.
+* ``{NAME}.lib`` depends on ``${NAME}.bundle.lib``.
+* ``{NAME}.bin`` depends on the provided ``TESTS``'s ``<test_dep>.bin`` targets.
+* ``{NAME}.run`` depends on the provided ``TESTS``'s ``<test_dep>.run`` targets
+ if ``pw_unit_test_AUTOMATIC_RUNNER`` is set, else it fails to build.
+* ``{NAME}.bundle.lib`` contains the provided tests bundled as a library target,
+ which can then be linked into a test executable.
+* ``{NAME}.bundle.bin`` standalone executable which contains the bundled tests.
+* ``{NAME}.bundle.run`` runs the ``{NAME}.bundle.bin`` test bundle executable
+ after building it if ``pw_unit_test_AUTOMATIC_RUNNER`` is set, else it fails
+ to build.
+
+**Required Arguments**
+
+* ``NAME`` - The name of the executable target to be created.
+* ``TESTS`` - ``pw_add_test`` targets and ``pw_add_test_group`` bundles to be
+ included in this test bundle
+
+**Example**
+
+.. code::
+
+ include($ENV{PW_ROOT}/pw_unit_test/test.cmake)
+
+ pw_add_test_group(tests
+ TESTS
+ bar_test
+ foo_test
+ )
+
+ pw_add_test(foo_test
+ # ...
+ )
+
+ pw_add_test(bar_test
+ # ...
+ )
+
+Build arguments
+```````````````
+.. option:: pw_unit_test_GOOGLETEST_BACKEND <target>
+
+ The GoogleTest implementation to use for Pigweed unit tests. This library
+ provides "gtest/gtest.h" and related headers. Defaults to pw_unit_test.light,
+ which implements a subset of GoogleTest.
+
+ Type: string (CMake target name)
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_AUTOMATIC_RUNNER <executable>
+
+ Path to a test runner to automatically run unit tests after they are built.
+
+ If set, a ``pw_test`` target's ``${NAME}`` and ``${NAME}.run`` targets will
+ invoke the test runner specified by this argument, passing the path to the
+ unit test to run. If this is unset, the ``pw_test`` target's ``${NAME}`` will
+ only build the unit test(s) and ``${NAME}.run`` will fail to build.
+
+ Type: string (name of an executable on the PATH, or path to an executable)
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_AUTOMATIC_RUNNER_ARGS <args>
+
+ An optional list of strings to pass as args to the test runner specified
+ by pw_unit_test_AUTOMATIC_RUNNER.
+
+ Type: list of strings (args to pass to pw_unit_test_AUTOMATIC_RUNNER)
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT_SECONDS <timeout_seconds>
+
+ An optional timeout to apply when running the automatic runner. Timeout is
+ in seconds. Defaults to empty which means no timeout.
+
+ Type: string (number of seconds to wait before killing test runner)
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_ADD_EXECUTABLE_FUNCTION <function name>
+
+ The name of the CMake function used to build pw_unit_test executables. The
+ provided function must take a ``NAME`` and a ``TEST_LIB`` argument which are
+ the expected name of the executable target and the target which provides the
+ unit test(s).
+
+ Type: string (name of a CMake function)
+ Usage: toolchain-controlled only
+
+.. option:: pw_unit_test_ADD_EXECUTABLE_FUNCTION_FILE <cmake file path>
+
+ The path to the .cmake file that defines pw_unit_test_ADD_EXECUTABLE_FUNCTION.
+
+ Type: string (path to a .cmake file)
+ Usage: toolchain-controlled only
- Type: string (GN path to a toolchain)
- Usage: toolchain-controlled only
RPC service
===========
@@ -334,25 +561,29 @@ streams the results back to the client. The service is defined in
``pw_unit_test_proto/unit_test.proto``, and implemented by the GN target
``$dir_pw_unit_test:rpc_service``.
+The RPC service is primarily intended for use with the default
+``pw_unit_test:light`` backend. It has some support for the GoogleTest backend,
+however some features (such as test suite filtering) are missing.
+
To set up RPC-based unit tests in your application, instantiate a
``pw::unit_test::UnitTestService`` and register it with your RPC server.
.. code:: c++
- #include "pw_rpc/server.h"
- #include "pw_unit_test/unit_test_service.h"
+ #include "pw_rpc/server.h"
+ #include "pw_unit_test/unit_test_service.h"
- // Server setup; refer to pw_rpc docs for more information.
- pw::rpc::Channel channels[] = {
- pw::rpc::Channel::Create<1>(&my_output),
- };
- pw::rpc::Server server(channels);
+ // Server setup; refer to pw_rpc docs for more information.
+ pw::rpc::Channel channels[] = {
+ pw::rpc::Channel::Create<1>(&my_output),
+ };
+ pw::rpc::Server server(channels);
- pw::unit_test::UnitTestService unit_test_service;
+ pw::unit_test::UnitTestService unit_test_service;
- void RegisterServices() {
- server.RegisterService(unit_test_services);
- }
+ void RegisterServices() {
+ server.RegisterService(unit_test_services);
+ }
All tests flashed to an attached device can be run via python by calling
``pw_unit_test.rpc.run_tests()`` with a RPC client services object that has
@@ -361,19 +592,19 @@ logging.
.. code:: python
- from pw_hdlc.rpc import HdlcRpcClient
- from pw_unit_test.rpc import run_tests
+ from pw_hdlc.rpc import HdlcRpcClient
+ from pw_unit_test.rpc import run_tests
- PROTO = Path(os.environ['PW_ROOT'],
- 'pw_unit_test/pw_unit_test_proto/unit_test.proto')
+ PROTO = Path(os.environ['PW_ROOT'],
+ 'pw_unit_test/pw_unit_test_proto/unit_test.proto')
- client = HdlcRpcClient(serial.Serial(device, baud), PROTO)
- run_tests(client.rpcs())
+ client = HdlcRpcClient(serial.Serial(device, baud), PROTO)
+ run_tests(client.rpcs())
pw_unit_test.rpc
----------------
.. automodule:: pw_unit_test.rpc
- :members: EventHandler, run_tests
+ :members: EventHandler, run_tests
Module Configuration Options
============================
@@ -382,28 +613,44 @@ this module.
.. c:macro:: PW_UNIT_TEST_CONFIG_EVENT_BUFFER_SIZE
- The size of the event buffer that the UnitTestService contains.
- This buffer is used to encode events. By default this is set to
- 128 bytes.
+ The size of the event buffer that the UnitTestService contains.
+ This buffer is used to encode events. By default this is set to
+ 128 bytes.
.. c:macro:: PW_UNIT_TEST_CONFIG_MEMORY_POOL_SIZE
- The size of the memory pool to use for test fixture instances. By default this
- is set to 16K.
+ The size of the memory pool to use for test fixture instances. By default this
+ is set to 16K.
-Using upstream Googletest and Googlemock
-========================================
+Using upstream GoogleTest
+=========================
+Upstream `GoogleTest`_ may be used as the backend for ``pw_unit_test``. A clone
+of the GoogleTest repository is required. See the
+:ref:`third_party/googletest documentation <module-pw_third_party_googletest>`
+for details.
-If you want to use the full upstream Googletest/Googlemock, you must do the
-following:
+When using upstream `GoogleTest`_ as the backend, the
+:cpp:class:`pw::unit_test::GoogleTestHandlerAdapter` can be used in conjunction
+with the above mentioned `EventHandler Interface <#the-eventhandler-interface>`_
+and `Predefined event handlers`_. An example of how you can use the adapter in
+conjunction with an ``EventHandler`` is shown below.
-* Set the GN var ``dir_pw_third_party_googletest`` to the location of the
- googletest source. You can use ``pw package install googletest`` to fetch the
- source if desired.
-* Set the GN var ``pw_unit_test_MAIN = "//third_party/googletest:gmock_main"``
-* Set the GN var ``pw_unit_test_PUBLIC_DEPS = [ "//third_party/googletest" ]``
+ .. code-block:: c++
-.. note::
+ testing::InitGoogleTest();
+ auto* unit_test = testing::UnitTest::GetInstance();
+
+ pw::unit_test::LoggingEventHandler logger;
+ pw::unit_test::GoogleTestHandlerAdapter listener_adapter(logger);
+ unit_test->listeners().Append(&listener_adapter);
+
+ const auto status = RUN_ALL_TESTS();
+
+.. cpp:namespace-push:: pw::unit_test
+
+.. cpp:class:: GoogleTestHandlerAdapter
+
+ A GoogleTest Event Listener that fires GoogleTest emitted events to an
+ appropriate ``EventHandler``.
- Not all unit tests build properly with upstream Googletest yet. This is a
- work in progress.
+.. cpp::namespace-pop::
diff --git a/pw_unit_test/facade_test.gni b/pw_unit_test/facade_test.gni
index 04f7bfde3..917bd23ca 100644
--- a/pw_unit_test/facade_test.gni
+++ b/pw_unit_test/facade_test.gni
@@ -19,6 +19,10 @@ import("$dir_pw_toolchain/subtoolchain.gni")
import("$dir_pw_unit_test/test.gni")
declare_args() {
+ # Controls whether to build and run facade tests. Facade tests add
+ # considerably to build time, so they are disabled by default.
+ pw_unit_test_FACADE_TESTS_ENABLED = false
+
# Pigweed uses this internally to manage toolchain generation for facade
# tests. This should NEVER be set manually, or depended on as stable API.
pw_unit_test_FACADE_TEST_NAME = ""
@@ -55,7 +59,7 @@ template("pw_facade_test") {
# Only try to generate a facade test for toolchains created by
# generate_toolchain. Checking if pw_toolchain_SCOPE has the "name" member
# is a reliable way to do this since it's only ever set by generate_toolchain.
- if (defined(pw_toolchain_SCOPE.name)) {
+ if (pw_unit_test_FACADE_TESTS_ENABLED && defined(pw_toolchain_SCOPE.name)) {
if (defined(invoker.toolchain_suffix)) {
_subtoolchain_suffix = invoker.toolchain_suffix
} else {
diff --git a/pw_unit_test/framework.cc b/pw_unit_test/framework.cc
index 46d913b60..945e2f4fb 100644
--- a/pw_unit_test/framework.cc
+++ b/pw_unit_test/framework.cc
@@ -12,11 +12,13 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pw_unit_test/framework.h"
+#include "pw_unit_test/internal/framework.h"
#include <algorithm>
#include <cstring>
+#include "pw_assert/check.h"
+
namespace pw {
namespace unit_test {
@@ -48,6 +50,7 @@ void Framework::RegisterTest(TestInfo* new_test) const {
}
int Framework::RunAllTests() {
+ exit_status_ = 0;
run_tests_summary_.passed_tests = 0;
run_tests_summary_.failed_tests = 0;
run_tests_summary_.skipped_tests = 0;
@@ -116,6 +119,12 @@ void Framework::CurrentTestExpectSimple(const char* expression,
const char* evaluated_expression,
int line,
bool success) {
+ PW_CHECK_NOTNULL(
+ current_test_,
+ "EXPECT/ASSERT was called when no test was running! EXPECT/ASSERT cannot "
+ "be used from static constructors/destructors or before or after "
+ "RUN_ALL_TESTS().");
+
if (!success) {
current_result_ = TestResult::kFailure;
exit_status_ = 1;
diff --git a/pw_unit_test/framework_test.cc b/pw_unit_test/framework_test.cc
index d287859a5..939c818e0 100644
--- a/pw_unit_test/framework_test.cc
+++ b/pw_unit_test/framework_test.cc
@@ -12,10 +12,11 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pw_unit_test/framework.h"
-
#include <cstring>
+#include "gtest/gtest.h"
+#include "pw_assert/check.h"
+
namespace pw {
namespace {
@@ -180,9 +181,9 @@ TEST_F(PigweedTestFixture, YupTheNumberIs35) {
class Expectations : public ::testing::Test {
protected:
- Expectations() : cool_number_(3) { ASSERT_EQ(cool_number_, 3); }
+ Expectations() : cool_number_(3) { PW_CHECK_INT_EQ(cool_number_, 3); }
- ~Expectations() { ASSERT_EQ(cool_number_, 14159); }
+ ~Expectations() override { PW_CHECK_INT_EQ(cool_number_, 14159); }
int cool_number_;
};
@@ -193,7 +194,7 @@ class SetUpAndTearDown : public ::testing::Test {
protected:
SetUpAndTearDown() : value_(0) { EXPECT_EQ(value_, 0); }
- ~SetUpAndTearDown() { EXPECT_EQ(value_, 1); }
+ ~SetUpAndTearDown() override { EXPECT_EQ(value_, 1); }
void SetUp() override { value_ = 1337; }
diff --git a/pw_unit_test/googletest_handler_adapter.cc b/pw_unit_test/googletest_handler_adapter.cc
new file mode 100644
index 000000000..9d29d1858
--- /dev/null
+++ b/pw_unit_test/googletest_handler_adapter.cc
@@ -0,0 +1,79 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_unit_test/googletest_handler_adapter.h"
+
+#include <cstdlib>
+
+namespace pw::unit_test {
+
+void GoogleTestHandlerAdapter::OnTestProgramStart(
+ const testing::UnitTest& unit_test) {
+ handler_.TestProgramStart(
+ {unit_test.test_to_run_count(), unit_test.test_suite_to_run_count(), {}});
+}
+
+void GoogleTestHandlerAdapter::OnEnvironmentsSetUpEnd(
+ const ::testing::UnitTest&) {
+ handler_.EnvironmentsSetUpEnd();
+}
+
+void GoogleTestHandlerAdapter::OnTestSuiteStart(
+ const ::testing::TestSuite& ts) {
+ handler_.TestSuiteStart({ts.name(), ts.test_to_run_count()});
+}
+
+void GoogleTestHandlerAdapter::OnTestStart(const ::testing::TestInfo& ti) {
+ handler_.TestCaseStart({ti.test_suite_name(), ti.name(), ti.file()});
+}
+
+void GoogleTestHandlerAdapter::OnTestPartResult(
+ const ::testing::TestPartResult& tpr) {
+ handler_.TestCaseExpect(
+ {.suite_name = "", .test_name = "", .file_name = tpr.file_name()},
+ {.expression = "",
+ .evaluated_expression = tpr.summary(),
+ .line_number = tpr.line_number(),
+ .success = tpr.passed() || tpr.skipped()});
+}
+
+void GoogleTestHandlerAdapter::OnTestEnd(const ::testing::TestInfo& ti) {
+ auto result = ti.result()->Passed()
+ ? TestResult::kSuccess
+ : (ti.result()->Failed() ? TestResult::kFailure
+ : TestResult::kSkipped);
+
+ handler_.TestCaseEnd({ti.test_suite_name(), ti.name(), ti.file()}, result);
+}
+
+void GoogleTestHandlerAdapter::OnTestSuiteEnd(const ::testing::TestSuite& ts) {
+ handler_.TestSuiteEnd({ts.name(), ts.test_to_run_count()});
+}
+
+void GoogleTestHandlerAdapter::OnEnvironmentsTearDownEnd(
+ const ::testing::UnitTest&) {
+ handler_.EnvironmentsTearDownEnd();
+}
+
+void GoogleTestHandlerAdapter::OnTestProgramEnd(
+ const ::testing::UnitTest& unit_test) {
+ handler_.TestProgramEnd({unit_test.test_to_run_count(),
+ unit_test.test_suite_to_run_count(),
+ {unit_test.successful_test_count(),
+ unit_test.failed_test_count(),
+ unit_test.skipped_test_count(),
+ unit_test.disabled_test_count()}});
+}
+
+} // namespace pw::unit_test
diff --git a/pw_unit_test/googletest_style_event_handler.cc b/pw_unit_test/googletest_style_event_handler.cc
new file mode 100644
index 000000000..4a330d1d8
--- /dev/null
+++ b/pw_unit_test/googletest_style_event_handler.cc
@@ -0,0 +1,141 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_unit_test/googletest_style_event_handler.h"
+
+#include <cstdarg>
+
+namespace pw {
+namespace unit_test {
+
+void GoogleTestStyleEventHandler::TestProgramStart(
+ const ProgramSummary& program_summary) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_TEST_PROGRAM_START,
+ program_summary.tests_to_run,
+ program_summary.test_suites,
+ program_summary.test_suites > 1 ? "s" : "");
+}
+
+void GoogleTestStyleEventHandler::EnvironmentsSetUpEnd() {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_ENVIRONMENTS_SETUP_END);
+}
+
+void GoogleTestStyleEventHandler::TestSuiteStart(const TestSuite& test_suite) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_TEST_SUITE_START,
+ test_suite.test_to_run_count,
+ test_suite.name);
+}
+
+void GoogleTestStyleEventHandler::TestSuiteEnd(const TestSuite& test_suite) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_TEST_SUITE_END,
+ test_suite.test_to_run_count,
+ test_suite.name);
+}
+
+void GoogleTestStyleEventHandler::EnvironmentsTearDownEnd() {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_ENVIRONMENTS_TEAR_DOWN_END);
+}
+
+void GoogleTestStyleEventHandler::TestProgramEnd(
+ const ProgramSummary& program_summary) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_TEST_PROGRAM_END,
+ program_summary.tests_to_run -
+ program_summary.tests_summary.skipped_tests -
+ program_summary.tests_summary.disabled_tests,
+ program_summary.tests_to_run,
+ program_summary.test_suites,
+ program_summary.test_suites > 1 ? "s" : "");
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_PASSED_SUMMARY,
+ program_summary.tests_summary.passed_tests);
+ if (program_summary.tests_summary.skipped_tests ||
+ program_summary.tests_summary.disabled_tests) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_DISABLED_SUMMARY,
+ program_summary.tests_summary.skipped_tests +
+ program_summary.tests_summary.disabled_tests);
+ }
+ if (program_summary.tests_summary.failed_tests) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_FAILED_SUMMARY,
+ program_summary.tests_summary.failed_tests);
+ }
+}
+
+void GoogleTestStyleEventHandler::RunAllTestsStart() {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_RUN_ALL_TESTS_START);
+}
+
+void GoogleTestStyleEventHandler::RunAllTestsEnd(
+ const RunTestsSummary& run_tests_summary) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_RUN_ALL_TESTS_END);
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_PASSED_SUMMARY,
+ run_tests_summary.passed_tests);
+ if (run_tests_summary.skipped_tests) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_DISABLED_SUMMARY,
+ run_tests_summary.skipped_tests);
+ }
+ if (run_tests_summary.failed_tests) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_FAILED_SUMMARY,
+ run_tests_summary.failed_tests);
+ }
+}
+
+void GoogleTestStyleEventHandler::TestCaseStart(const TestCase& test_case) {
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_CASE_START,
+ test_case.suite_name,
+ test_case.test_name);
+}
+
+void GoogleTestStyleEventHandler::TestCaseEnd(const TestCase& test_case,
+ TestResult result) {
+ // Use a switch with no default to detect changes in the test result enum.
+ switch (result) {
+ case TestResult::kSuccess:
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_CASE_OK,
+ test_case.suite_name,
+ test_case.test_name);
+ break;
+ case TestResult::kFailure:
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_CASE_FAILED,
+ test_case.suite_name,
+ test_case.test_name);
+ break;
+ case TestResult::kSkipped:
+ WriteLine(PW_UNIT_TEST_GOOGLETEST_CASE_DISABLED,
+ test_case.suite_name,
+ test_case.test_name);
+ break;
+ }
+}
+
+void GoogleTestStyleEventHandler::TestCaseExpect(
+ const TestCase& test_case, const TestExpectation& expectation) {
+ if (!verbose_ && expectation.success) {
+ return;
+ }
+
+ const char* result = expectation.success ? "Success" : "Failure";
+ WriteLine("%s:%d: %s", test_case.file_name, expectation.line_number, result);
+ WriteLine(" Expected: %s", expectation.expression);
+
+ Write(" Actual: ");
+ WriteLine("%s", expectation.evaluated_expression);
+}
+
+void GoogleTestStyleEventHandler::TestCaseDisabled(const TestCase& test) {
+ if (verbose_) {
+ WriteLine("Skipping disabled test %s.%s", test.suite_name, test.test_name);
+ }
+}
+
+} // namespace unit_test
+} // namespace pw
diff --git a/pw_unit_test/logging_event_handler.cc b/pw_unit_test/logging_event_handler.cc
index 7e80c321c..db2a9e430 100644
--- a/pw_unit_test/logging_event_handler.cc
+++ b/pw_unit_test/logging_event_handler.cc
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -14,33 +14,87 @@
#include "pw_unit_test/logging_event_handler.h"
-//#include <cstdarg>
-//#include <cstdio>
-//#include <string_view>
#include <cstdint>
#include "pw_log/log.h"
+#include "pw_unit_test/googletest_style_event_handler.h"
namespace pw::unit_test {
+void LoggingEventHandler::TestProgramStart(
+ const ProgramSummary& program_summary) {
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_TEST_PROGRAM_START,
+ program_summary.tests_to_run,
+ program_summary.test_suites,
+ program_summary.test_suites != 1 ? "s" : "");
+}
+
+void LoggingEventHandler::EnvironmentsSetUpEnd() {
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_ENVIRONMENTS_SETUP_END);
+}
+
+void LoggingEventHandler::TestSuiteStart(const TestSuite& test_suite) {
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_TEST_SUITE_START,
+ test_suite.test_to_run_count,
+ test_suite.name);
+}
+
+void LoggingEventHandler::TestSuiteEnd(const TestSuite& test_suite) {
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_TEST_SUITE_END,
+ test_suite.test_to_run_count,
+ test_suite.name);
+}
+
+void LoggingEventHandler::EnvironmentsTearDownEnd() {
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_ENVIRONMENTS_TEAR_DOWN_END);
+}
+
+void LoggingEventHandler::TestProgramEnd(
+ const ProgramSummary& program_summary) {
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_TEST_PROGRAM_END,
+ program_summary.tests_to_run -
+ program_summary.tests_summary.skipped_tests -
+ program_summary.tests_summary.disabled_tests,
+ program_summary.tests_to_run,
+ program_summary.test_suites,
+ program_summary.test_suites != 1 ? "s" : "");
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_PASSED_SUMMARY,
+ program_summary.tests_summary.passed_tests);
+ if (program_summary.tests_summary.skipped_tests ||
+ program_summary.tests_summary.disabled_tests) {
+ PW_LOG_WARN(PW_UNIT_TEST_GOOGLETEST_DISABLED_SUMMARY,
+ program_summary.tests_summary.skipped_tests +
+ program_summary.tests_summary.disabled_tests);
+ }
+ if (program_summary.tests_summary.failed_tests) {
+ PW_LOG_ERROR(PW_UNIT_TEST_GOOGLETEST_FAILED_SUMMARY,
+ program_summary.tests_summary.failed_tests);
+ }
+}
+
void LoggingEventHandler::RunAllTestsStart() {
- PW_LOG_INFO("[==========] Running all tests.");
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_RUN_ALL_TESTS_START);
}
void LoggingEventHandler::RunAllTestsEnd(
const RunTestsSummary& run_tests_summary) {
- PW_LOG_INFO("[==========] Done running all tests.");
- PW_LOG_INFO("[ PASSED ] %d test(s).", run_tests_summary.passed_tests);
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_RUN_ALL_TESTS_END);
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_PASSED_SUMMARY,
+ run_tests_summary.passed_tests);
if (run_tests_summary.skipped_tests) {
- PW_LOG_WARN("[ SKIPPED ] %d test(s).", run_tests_summary.skipped_tests);
+ PW_LOG_WARN(PW_UNIT_TEST_GOOGLETEST_DISABLED_SUMMARY,
+ run_tests_summary.skipped_tests);
}
if (run_tests_summary.failed_tests) {
- PW_LOG_ERROR("[ FAILED ] %d test(s).", run_tests_summary.failed_tests);
+ PW_LOG_ERROR(PW_UNIT_TEST_GOOGLETEST_FAILED_SUMMARY,
+ run_tests_summary.failed_tests);
}
}
void LoggingEventHandler::TestCaseStart(const TestCase& test_case) {
- PW_LOG_INFO("[ RUN ] %s.%s", test_case.suite_name, test_case.test_name);
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_CASE_START,
+ test_case.suite_name,
+ test_case.test_name);
}
void LoggingEventHandler::TestCaseEnd(const TestCase& test_case,
@@ -48,16 +102,19 @@ void LoggingEventHandler::TestCaseEnd(const TestCase& test_case,
// Use a switch with no default to detect changes in the test result enum.
switch (result) {
case TestResult::kSuccess:
- PW_LOG_INFO(
- "[ OK ] %s.%s", test_case.suite_name, test_case.test_name);
+ PW_LOG_INFO(PW_UNIT_TEST_GOOGLETEST_CASE_OK,
+ test_case.suite_name,
+ test_case.test_name);
break;
case TestResult::kFailure:
- PW_LOG_ERROR(
- "[ FAILED ] %s.%s", test_case.suite_name, test_case.test_name);
+ PW_LOG_ERROR(PW_UNIT_TEST_GOOGLETEST_CASE_FAILED,
+ test_case.suite_name,
+ test_case.test_name);
break;
case TestResult::kSkipped:
- PW_LOG_WARN(
- "[ SKIPPED ] %s.%s", test_case.suite_name, test_case.test_name);
+ PW_LOG_WARN(PW_UNIT_TEST_GOOGLETEST_CASE_DISABLED,
+ test_case.suite_name,
+ test_case.test_name);
break;
}
}
@@ -71,13 +128,19 @@ void LoggingEventHandler::TestCaseExpect(const TestCase& test_case,
const char* result = expectation.success ? "Success" : "Failure";
uint32_t level = expectation.success ? PW_LOG_LEVEL_INFO : PW_LOG_LEVEL_ERROR;
PW_LOG(level,
+ PW_LOG_MODULE_NAME,
PW_LOG_FLAGS,
"%s:%d: %s",
test_case.file_name,
expectation.line_number,
result);
- PW_LOG(level, PW_LOG_FLAGS, " Expected: %s", expectation.expression);
PW_LOG(level,
+ PW_LOG_MODULE_NAME,
+ PW_LOG_FLAGS,
+ " Expected: %s",
+ expectation.expression);
+ PW_LOG(level,
+ PW_LOG_MODULE_NAME,
PW_LOG_FLAGS,
" Actual: %s",
expectation.evaluated_expression);
diff --git a/pw_unit_test/logging_main.cc b/pw_unit_test/logging_main.cc
index cb44cf59e..b780e2ed3 100644
--- a/pw_unit_test/logging_main.cc
+++ b/pw_unit_test/logging_main.cc
@@ -12,7 +12,7 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pw_unit_test/framework.h"
+#include "gtest/gtest.h"
#include "pw_unit_test/logging_event_handler.h"
int main() {
diff --git a/pw_unit_test/printf_main.cc b/pw_unit_test/printf_main.cc
new file mode 100644
index 000000000..07054fa6f
--- /dev/null
+++ b/pw_unit_test/printf_main.cc
@@ -0,0 +1,22 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+#include "pw_unit_test/printf_event_handler.h"
+
+int main() {
+ pw::unit_test::PrintfEventHandler handler;
+ pw::unit_test::RegisterEventHandler(&handler);
+ return RUN_ALL_TESTS();
+}
diff --git a/pw_unit_test/public/pw_unit_test/event_handler.h b/pw_unit_test/public/pw_unit_test/event_handler.h
index ea029ac3d..d59004bee 100644
--- a/pw_unit_test/public/pw_unit_test/event_handler.h
+++ b/pw_unit_test/public/pw_unit_test/event_handler.h
@@ -94,6 +94,25 @@ struct RunTestsSummary {
int disabled_tests;
};
+struct ProgramSummary {
+ // The total number of tests to run in the program.
+ int tests_to_run;
+
+ // The number of test suites included in the program.
+ int test_suites;
+
+ // Test summary for the program once complete.
+ RunTestsSummary tests_summary;
+};
+
+struct TestSuite {
+ // Name of the test suite.
+ const char* name;
+
+ // Total number of tests in suite to run.
+ int test_to_run_count;
+};
+
// An event handler is responsible for collecting and processing the results of
// a unit test run. Its interface is called by the unit test framework as tests
// are executed and various test events occur.
@@ -101,6 +120,24 @@ class EventHandler {
public:
virtual ~EventHandler() = default;
+ // Called before any test activity starts.
+ virtual void TestProgramStart(const ProgramSummary& program_summary) = 0;
+
+ // Called after environment set-up for each iteration of tests ends.
+ virtual void EnvironmentsSetUpEnd() = 0;
+
+ // Called before the test suite starts.
+ virtual void TestSuiteStart(const TestSuite& test_suite) = 0;
+
+ // Called after the test suite ends.
+ virtual void TestSuiteEnd(const TestSuite& test_suite) = 0;
+
+ // Called after environment tear-down for each iteration of tests ends.
+ virtual void EnvironmentsTearDownEnd() = 0;
+
+ // Called after all test activities have ended.
+ virtual void TestProgramEnd(const ProgramSummary& program_summary) = 0;
+
// Called before all tests are run.
virtual void RunAllTestsStart() = 0;
@@ -123,9 +160,5 @@ class EventHandler {
const TestExpectation& expectation) = 0;
};
-// Sets the event handler for a test run. Must be called before RUN_ALL_TESTS()
-// to receive test output.
-void RegisterEventHandler(EventHandler* event_handler);
-
} // namespace unit_test
} // namespace pw
diff --git a/pw_unit_test/public/pw_unit_test/googletest_handler_adapter.h b/pw_unit_test/public/pw_unit_test/googletest_handler_adapter.h
new file mode 100644
index 000000000..c93a04633
--- /dev/null
+++ b/pw_unit_test/public/pw_unit_test/googletest_handler_adapter.h
@@ -0,0 +1,41 @@
+// Copyright 2023 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include "gtest/gtest.h"
+#include "pw_unit_test/event_handler.h"
+
+namespace pw {
+namespace unit_test {
+
+class GoogleTestHandlerAdapter : public testing::EmptyTestEventListener {
+ public:
+ GoogleTestHandlerAdapter(EventHandler& handler) : handler_(handler) {}
+ void OnTestProgramStart(const testing::UnitTest&) override;
+ void OnEnvironmentsSetUpEnd(const testing::UnitTest&) override;
+ void OnTestSuiteStart(const testing::TestSuite&) override;
+ void OnTestStart(const testing::TestInfo&) override;
+ void OnTestPartResult(const testing::TestPartResult&) override;
+ void OnTestEnd(const testing::TestInfo&) override;
+ void OnTestSuiteEnd(const testing::TestSuite&) override;
+ void OnEnvironmentsTearDownEnd(const testing::UnitTest& unit_test) override;
+ void OnTestProgramEnd(const testing::UnitTest&) override;
+
+ private:
+ EventHandler& handler_;
+};
+
+} // namespace unit_test
+} // namespace pw
diff --git a/pw_unit_test/public/pw_unit_test/googletest_style_event_handler.h b/pw_unit_test/public/pw_unit_test/googletest_style_event_handler.h
new file mode 100644
index 000000000..128e178ed
--- /dev/null
+++ b/pw_unit_test/public/pw_unit_test/googletest_style_event_handler.h
@@ -0,0 +1,90 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#pragma once
+
+#include "pw_preprocessor/compiler.h"
+#include "pw_unit_test/event_handler.h"
+
+// Define the test messages and string formats as literal strings so they
+// work with different log databases.
+#define PW_UNIT_TEST_GOOGLETEST_RUN_ALL_TESTS_START \
+ "[==========] Running all tests."
+#define PW_UNIT_TEST_GOOGLETEST_RUN_ALL_TESTS_END \
+ "[==========] Done running all tests."
+
+#define PW_UNIT_TEST_GOOGLETEST_TEST_PROGRAM_START \
+ "[==========] Running %d tests from %d test suite%s."
+
+#define PW_UNIT_TEST_GOOGLETEST_TEST_PROGRAM_END \
+ "[==========] %d / %d tests from %d test suite%s ran."
+
+#define PW_UNIT_TEST_GOOGLETEST_ENVIRONMENTS_SETUP_END \
+ "[----------] Global test environments setup."
+
+#define PW_UNIT_TEST_GOOGLETEST_ENVIRONMENTS_TEAR_DOWN_END \
+ "[----------] Global test environments tear-down."
+
+#define PW_UNIT_TEST_GOOGLETEST_TEST_SUITE_START \
+ "[----------] %d tests from %s."
+
+#define PW_UNIT_TEST_GOOGLETEST_TEST_SUITE_END "[----------] %d tests from %s."
+
+#define PW_UNIT_TEST_GOOGLETEST_PASSED_SUMMARY "[ PASSED ] %d test(s)."
+#define PW_UNIT_TEST_GOOGLETEST_DISABLED_SUMMARY "[ DISABLED ] %d test(s)."
+#define PW_UNIT_TEST_GOOGLETEST_FAILED_SUMMARY "[ FAILED ] %d test(s)."
+
+#define PW_UNIT_TEST_GOOGLETEST_CASE_START "[ RUN ] %s.%s"
+#define PW_UNIT_TEST_GOOGLETEST_CASE_OK "[ OK ] %s.%s"
+#define PW_UNIT_TEST_GOOGLETEST_CASE_FAILED "[ FAILED ] %s.%s"
+#define PW_UNIT_TEST_GOOGLETEST_CASE_DISABLED "[ DISABLED ] %s.%s"
+
+namespace pw {
+namespace unit_test {
+
+// Renders the test results in Google Test style.
+class GoogleTestStyleEventHandler : public EventHandler {
+ public:
+ void TestProgramStart(const ProgramSummary& program_summary) override;
+ void EnvironmentsSetUpEnd() override;
+ void TestSuiteStart(const TestSuite& test_suite) override;
+ void TestSuiteEnd(const TestSuite& test_suite) override;
+ void EnvironmentsTearDownEnd() override;
+ void TestProgramEnd(const ProgramSummary& program_summary) override;
+
+ void RunAllTestsStart() override;
+ void RunAllTestsEnd(const RunTestsSummary& run_tests_summary) override;
+ void TestCaseStart(const TestCase& test_case) override;
+ void TestCaseEnd(const TestCase& test_case, TestResult result) override;
+ void TestCaseExpect(const TestCase& test_case,
+ const TestExpectation& expectation) override;
+ void TestCaseDisabled(const TestCase& test_case) override;
+
+ protected:
+ constexpr GoogleTestStyleEventHandler(bool verbose) : verbose_(verbose) {}
+
+ bool verbose() const { return verbose_; }
+
+ // Writes the content without a trailing newline.
+ virtual void Write(const char* content) = 0;
+
+ // Writes the formatted content and appends a newline character.
+ virtual void WriteLine(const char* format, ...) PW_PRINTF_FORMAT(2, 3) = 0;
+
+ private:
+ bool verbose_;
+};
+
+} // namespace unit_test
+} // namespace pw
diff --git a/pw_unit_test/public/pw_unit_test/framework.h b/pw_unit_test/public/pw_unit_test/internal/framework.h
index ab3594898..73af3928e 100644
--- a/pw_unit_test/public/pw_unit_test/framework.h
+++ b/pw_unit_test/public/pw_unit_test/internal/framework.h
@@ -13,18 +13,17 @@
// the License.
// The Pigweed unit test framework requires C++17 to use its full functionality.
-// In C++11, only the TEST, TEST_F, EXPECT_TRUE, EXPECT_FALSE, ASSERT_TRUE,
-// ASSERT_FALSE, FAIL, and ADD_FAILURE macros may be used.
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <new>
-#include <span>
#include "pw_polyfill/standard.h"
+#include "pw_preprocessor/compiler.h"
#include "pw_preprocessor/util.h"
+#include "pw_span/span.h"
#include "pw_unit_test/config.h"
#include "pw_unit_test/event_handler.h"
@@ -34,16 +33,18 @@
#include "pw_string/string_builder.h"
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
-#define PW_TEST(test_suite_name, test_name) \
+#define GTEST_TEST(test_suite_name, test_name) \
+ _PW_TEST_SUITE_NAMES_MUST_BE_UNIQUE(void /* TEST */, test_suite_name); \
_PW_TEST(test_suite_name, test_name, ::pw::unit_test::internal::Test)
// TEST() is a pretty generic macro name which could conflict with other code.
-// If GTEST_DONT_DEFINE_TEST is set, don't alias PW_TEST to TEST.
+// If GTEST_DONT_DEFINE_TEST is set, don't alias GTEST_TEST to TEST.
#if !(defined(GTEST_DONT_DEFINE_TEST) && GTEST_DONT_DEFINE_TEST)
-#define TEST PW_TEST
+#define TEST(test_suite_name, test_name) GTEST_TEST(test_suite_name, test_name)
#endif // !GTEST_DONT_DEFINE_TEST
-#define TEST_F(test_fixture, test_name) \
+#define TEST_F(test_fixture, test_name) \
+ _PW_TEST_SUITE_NAMES_MUST_BE_UNIQUE(int /* TEST_F */, test_fixture); \
_PW_TEST(test_fixture, test_name, test_fixture)
#define EXPECT_TRUE(expr) static_cast<void>(_PW_TEST_BOOL(expr, true))
@@ -101,7 +102,7 @@
// pw_unit_test framework entry point. Runs every registered test case and
// dispatches the results through the event handler. Returns a status of zero
// if all tests passed, or nonzero if there were any failures.
-// This is compatible with Googletest.
+// This is compatible with GoogleTest.
//
// In order to receive test output, an event handler must be registered before
// this is called:
@@ -123,7 +124,7 @@
static_cast<void>(statement); \
static_cast<void>(regex); \
} \
- static_assert(true, "Macros must be termianted with a semicolon")
+ static_assert(true, "Macros must be terminated with a semicolon")
#define ASSERT_DEATH_IF_SUPPORTED(statement, regex) \
EXPECT_DEATH_IF_SUPPORTED(statement, regex)
@@ -144,7 +145,7 @@ namespace string {
//
// template <>
// StatusWithSize ToString<MyType>(const MyType& value,
-// std::span<char> buffer) {
+// span<char> buffer) {
// return string::Format("<MyType|%d>", value.id);
// }
//
@@ -152,7 +153,7 @@ namespace string {
//
// See the documentation in pw_string/string_builder.h for more information.
template <typename T>
-StatusWithSize UnknownTypeToString(const T& value, std::span<char> buffer) {
+StatusWithSize UnknownTypeToString(const T& value, span<char> buffer) {
StringBuilder sb(buffer);
sb << '<' << sizeof(value) << "-byte object at 0x" << &value << '>';
return sb.status_with_size();
@@ -163,6 +164,11 @@ StatusWithSize UnknownTypeToString(const T& value, std::span<char> buffer) {
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
namespace unit_test {
+
+// Sets the event handler for a test run. Must be called before RUN_ALL_TESTS()
+// to receive test output.
+void RegisterEventHandler(EventHandler* event_handler);
+
namespace internal {
class Test;
@@ -211,7 +217,7 @@ class Framework {
// Only run test suites whose names are included in the provided list during
// the next test run. This is C++17 only; older versions of C++ will run all
// non-disabled tests.
- void SetTestSuitesToRun(std::span<std::string_view> test_suites) {
+ void SetTestSuitesToRun(span<std::string_view> test_suites) {
test_suites_to_run_ = test_suites;
}
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
@@ -231,12 +237,11 @@ class Framework {
// this method instantiated for its test class.
template <typename TestInstance>
static void CreateAndRunTest(const TestInfo& test_info) {
- // TODO(frolv): Update the assert message with the name of the config option
- // for memory pool size once it is configurable.
static_assert(
sizeof(TestInstance) <= sizeof(memory_pool_),
"The test memory pool is too small for this test. Either increase "
- "kTestMemoryPoolSizeBytes or decrease the size of your test fixture.");
+ "PW_UNIT_TEST_CONFIG_MEMORY_POOL_SIZE or decrease the size of your "
+ "test fixture.");
Framework& framework = Get();
framework.StartTest(test_info);
@@ -337,16 +342,16 @@ class Framework {
// Overall result of the ongoing test run, which covers multiple tests.
RunTestsSummary run_tests_summary_;
- // Program exit status returned by RunAllTests for Googletest compatibility.
+ // Program exit status returned by RunAllTests for GoogleTest compatibility.
int exit_status_;
// Handler to which to dispatch test events.
EventHandler* event_handler_;
#if PW_CXX_STANDARD_IS_SUPPORTED(17)
- std::span<std::string_view> test_suites_to_run_;
+ span<std::string_view> test_suites_to_run_;
#else
- std::span<const char*> test_suites_to_run_; // Always empty in C++14.
+ span<const char*> test_suites_to_run_; // Always empty in C++14.
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
std::aligned_storage_t<config::kMemoryPoolSize, alignof(std::max_align_t)>
@@ -463,7 +468,7 @@ constexpr bool HasNoUnderscores(const char* suite) {
} // namespace internal
#if PW_CXX_STANDARD_IS_SUPPORTED(17)
-inline void SetTestSuitesToRun(std::span<std::string_view> test_suites) {
+inline void SetTestSuitesToRun(span<std::string_view> test_suites) {
internal::Framework::Get().SetTestSuitesToRun(test_suites);
}
#endif // PW_CXX_STANDARD_IS_SUPPORTED(17)
@@ -484,27 +489,32 @@ inline void SetTestSuitesToRun(std::span<std::string_view> test_suites) {
test_suite_name##_##test_name##_Test, \
parent_class)
-#define _PW_TEST_CLASS(suite, name, class_name, parent_class) \
- class class_name final : public parent_class { \
- private: \
- void PigweedTestBody() override; \
- \
- static ::pw::unit_test::internal::TestInfo test_info_; \
- }; \
- \
- ::pw::unit_test::internal::TestInfo class_name::test_info_( \
- #suite, \
- #name, \
- __FILE__, \
- ::pw::unit_test::internal::Framework::CreateAndRunTest<class_name>); \
- \
+#define _PW_TEST_CLASS(suite, name, class_name, parent_class) \
+ class class_name final : public parent_class { \
+ private: \
+ void PigweedTestBody() override; \
+ }; \
+ \
+ extern "C" { \
+ \
+ /* Declare the TestInfo as non-const since const variables do not work */ \
+ /* with the PW_UNIT_TEST_LINK_FILE_CONTAINING_TEST macro. */ \
+ /* NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) */ \
+ ::pw::unit_test::internal::TestInfo _pw_unit_test_Info_##suite##_##name( \
+ #suite, \
+ #name, \
+ __FILE__, \
+ ::pw::unit_test::internal::Framework::CreateAndRunTest<class_name>); \
+ \
+ } /* extern "C" */ \
+ \
void class_name::PigweedTestBody()
-#define _PW_TEST_ASSERT(expectation) \
- do { \
- if (!(expectation)) { \
- return; \
- } \
+#define _PW_TEST_ASSERT(expectation) \
+ do { \
+ if (!(expectation)) { \
+ return static_cast<void>(0); /* Prevent using ASSERT in constructors. */ \
+ } \
} while (0)
#define _PW_TEST_BOOL(expr, value) \
@@ -538,7 +548,27 @@ inline void SetTestSuitesToRun(std::span<std::string_view> test_suites) {
#lhs " " #op " " #rhs, \
__LINE__)
-// Alias Test as ::testing::Test for Googletest compatibility.
+// Checks that test suite names between TEST and TEST_F declarations are unique.
+// This works by declaring a function named for the test suite. The function
+// takes no arguments but is declared with different return types in the TEST
+// and TEST_F macros. If a TEST and TEST_F use the same test suite name, the
+// function declarations conflict, resulting in a compilation error.
+//
+// This catches most conflicts, but a runtime check is ultimately needed since
+// tests may be declared in different translation units.
+#if !defined(__clang__) && !defined(__GNUC___) && __GNUC__ <= 8
+// For some reason GCC8 is unable to ignore -Wredundant-decls here.
+#define _PW_TEST_SUITE_NAMES_MUST_BE_UNIQUE(return_type, test_suite)
+#else // All other compilers.
+#define _PW_TEST_SUITE_NAMES_MUST_BE_UNIQUE(return_type, test_suite) \
+ PW_MODIFY_DIAGNOSTICS_PUSH(); \
+ PW_MODIFY_DIAGNOSTIC(ignored, "-Wredundant-decls"); \
+ extern "C" return_type /* use extern "C" to escape namespacing */ \
+ PwUnitTestSuiteNamesMustBeUniqueBetweenTESTandTEST_F_##test_suite(void); \
+ PW_MODIFY_DIAGNOSTICS_POP()
+#endif // GCC8 or older.
+
+// Alias Test as ::testing::Test for GoogleTest compatibility.
namespace testing {
using Test = ::pw::unit_test::internal::Test;
diff --git a/pw_unit_test/public/pw_unit_test/logging_event_handler.h b/pw_unit_test/public/pw_unit_test/logging_event_handler.h
index b5a2bdcc4..dee76e42f 100644
--- a/pw_unit_test/public/pw_unit_test/logging_event_handler.h
+++ b/pw_unit_test/public/pw_unit_test/logging_event_handler.h
@@ -23,6 +23,12 @@ class LoggingEventHandler : public EventHandler {
public:
// If verbose is set, expectations values are always displayed.
LoggingEventHandler(bool verbose = false) : verbose_(verbose) {}
+ void TestProgramStart(const ProgramSummary& program_summary) override;
+ void EnvironmentsSetUpEnd() override;
+ void TestSuiteStart(const TestSuite& test_suite) override;
+ void TestSuiteEnd(const TestSuite& test_suite) override;
+ void EnvironmentsTearDownEnd() override;
+ void TestProgramEnd(const ProgramSummary& program_summary) override;
void RunAllTestsStart() override;
void RunAllTestsEnd(const RunTestsSummary& run_tests_summary) override;
diff --git a/pw_unit_test/public/pw_unit_test/printf_event_handler.h b/pw_unit_test/public/pw_unit_test/printf_event_handler.h
new file mode 100644
index 000000000..41dc6b28d
--- /dev/null
+++ b/pw_unit_test/public/pw_unit_test/printf_event_handler.h
@@ -0,0 +1,44 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <cstdio>
+
+#include "pw_unit_test/googletest_style_event_handler.h"
+
+namespace pw {
+namespace unit_test {
+
+// An event handler implementation that displays GoogleTest-style output using
+// std::printf.
+class PrintfEventHandler final : public GoogleTestStyleEventHandler {
+ public:
+ constexpr PrintfEventHandler(bool verbose = false)
+ : GoogleTestStyleEventHandler(verbose) {}
+
+ private:
+ void Write(const char* content) override { std::printf("%s", content); }
+
+ void WriteLine(const char* format, ...) override {
+ va_list args;
+
+ va_start(args, format);
+ std::vprintf(format, args);
+ std::printf("\n");
+ va_end(args);
+ }
+};
+
+} // namespace unit_test
+} // namespace pw
diff --git a/pw_unit_test/public/pw_unit_test/simple_printing_event_handler.h b/pw_unit_test/public/pw_unit_test/simple_printing_event_handler.h
index e32991530..5db2bea20 100644
--- a/pw_unit_test/public/pw_unit_test/simple_printing_event_handler.h
+++ b/pw_unit_test/public/pw_unit_test/simple_printing_event_handler.h
@@ -19,6 +19,7 @@
#include "pw_preprocessor/compiler.h"
#include "pw_unit_test/event_handler.h"
+#include "pw_unit_test/googletest_style_event_handler.h"
namespace pw::unit_test {
@@ -32,7 +33,7 @@ namespace pw::unit_test {
// at ../path/to/my/file_test.cc:4831
// <<< Test MyTestSuite.TestCase1 failed
//
-class SimplePrintingEventHandler : public EventHandler {
+class SimplePrintingEventHandler : public GoogleTestStyleEventHandler {
public:
// Function for writing output as a string.
using WriteFunction = void (*)(const std::string_view& string,
@@ -41,22 +42,18 @@ class SimplePrintingEventHandler : public EventHandler {
// Instantiates an event handler with a function to which to output results.
// If verbose is set, information for successful tests is written as well as
// failures.
- SimplePrintingEventHandler(WriteFunction write_function, bool verbose = false)
- : write_(write_function), verbose_(verbose) {}
-
- void RunAllTestsStart() override;
- void RunAllTestsEnd(const RunTestsSummary& run_tests_summary) override;
- void TestCaseStart(const TestCase& test_case) override;
- void TestCaseEnd(const TestCase& test_case, TestResult result) override;
- void TestCaseExpect(const TestCase& test_case,
- const TestExpectation& expectation) override;
- void TestCaseDisabled(const TestCase& test_case) override;
+ constexpr SimplePrintingEventHandler(WriteFunction write_function,
+ bool verbose = false)
+ : GoogleTestStyleEventHandler(verbose),
+ write_(write_function),
+ buffer_{} {}
private:
- void WriteLine(const char* format, ...) PW_PRINTF_FORMAT(2, 3);
+ void WriteLine(const char* format, ...) override PW_PRINTF_FORMAT(2, 3);
+
+ void Write(const char* content) override { write_(content, false); }
WriteFunction write_;
- bool verbose_;
char buffer_[512];
};
diff --git a/pw_unit_test/public/pw_unit_test/static_library_support.h b/pw_unit_test/public/pw_unit_test/static_library_support.h
new file mode 100644
index 000000000..0fe5c9602
--- /dev/null
+++ b/pw_unit_test/public/pw_unit_test/static_library_support.h
@@ -0,0 +1,57 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "gtest/gtest.h"
+
+// Ensures tests in a static library are linked and executed. Provide the test
+// suite name and test name for one test in the file linked into a static
+// library. Any test in the file may be used, but it is recommended to use the
+// first for consistency. The test must be in a static library that is a
+// dependency of this target. Referring to a test that does not exist causes a
+// linker error.
+//
+// The linker usually ignores tests linked through a static library. This is
+// because test registration relies on the test instance's static constructor
+// adding itself to a global list of tests. When linking against a static
+// library, static constructors in an object file will be ignored unless at
+// least one entity in that object file is linked.
+//
+// This macro works by passing the internal TestInfo instance to a constructor
+// defined in a source file. This guarantees that the TestInfo instance is
+// referenced, so the linker will link it and the other tests in that file.
+#define PW_UNIT_TEST_LINK_FILE_CONTAINING_TEST(suite, name) \
+ _PW_UNIT_TEST_LINK_TESTS(_pw_unit_test_Info_##suite##_##name)
+
+#define _PW_UNIT_TEST_LINK_TESTS(info) \
+ extern "C" { \
+ \
+ extern ::pw::unit_test::internal::TestInfo info; \
+ \
+ [[maybe_unused]] const ::pw::unit_test::internal::ReferToTestInfo \
+ _pw_unit_test_reference_to_ensure_link_##info(info); \
+ \
+ } /* extern "C" */ \
+ \
+ static_assert(true, "Macros must end with a semicolon")
+
+namespace pw::unit_test::internal {
+
+// Refers to the TestInfo to ensure it is linked in.
+class ReferToTestInfo {
+ public:
+ explicit ReferToTestInfo(const TestInfo& info);
+};
+
+} // namespace pw::unit_test::internal
diff --git a/pw_unit_test/public/pw_unit_test/unit_test_service.h b/pw_unit_test/public/pw_unit_test/unit_test_service.h
index a7269eecc..ca32ef5d8 100644
--- a/pw_unit_test/public/pw_unit_test/unit_test_service.h
+++ b/pw_unit_test/public/pw_unit_test/unit_test_service.h
@@ -15,6 +15,7 @@
#include "pw_log/log.h"
#include "pw_unit_test/config.h"
+#include "pw_unit_test/event_handler.h"
#include "pw_unit_test/internal/rpc_event_handler.h"
#include "pw_unit_test_proto/unit_test.pwpb.h"
#include "pw_unit_test_proto/unit_test.raw_rpc.pb.h"
@@ -36,11 +37,11 @@ class UnitTestService final
// migrated to it.
template <typename WriteFunction>
void WriteEvent(WriteFunction event_writer) {
- Event::MemoryEncoder event(encoding_buffer_);
+ pwpb::Event::MemoryEncoder event(encoding_buffer_);
event_writer(event);
if (event.status().ok()) {
writer_.Write(event)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
}
}
diff --git a/pw_unit_test/public_overrides/gtest/gtest.h b/pw_unit_test/public_overrides/gtest/gtest.h
index d24631b8d..c3af7177c 100644
--- a/pw_unit_test/public_overrides/gtest/gtest.h
+++ b/pw_unit_test/public_overrides/gtest/gtest.h
@@ -17,4 +17,4 @@
// using pw_unit_test as a backend.
#pragma once
-#include "pw_unit_test/framework.h"
+#include "pw_unit_test/internal/framework.h"
diff --git a/pw_unit_test/py/BUILD.bazel b/pw_unit_test/py/BUILD.bazel
new file mode 100644
index 000000000..8915b04e4
--- /dev/null
+++ b/pw_unit_test/py/BUILD.bazel
@@ -0,0 +1,31 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load("@rules_python//python:defs.bzl", "py_library")
+
+package(default_visibility = ["//visibility:public"])
+
+py_library(
+ name = "pw_unit_test_lib",
+ srcs = [
+ "pw_unit_test/__init__.py",
+ "pw_unit_test/rpc.py",
+ "pw_unit_test/test_runner.py",
+ ],
+ deps = [
+ "//pw_cli/py:pw_cli",
+ "//pw_rpc/py:pw_rpc",
+ "//pw_unit_test:unit_test_py_pb2",
+ ],
+)
diff --git a/pw_unit_test/py/BUILD.gn b/pw_unit_test/py/BUILD.gn
index 57bf6a229..d501fafbf 100644
--- a/pw_unit_test/py/BUILD.gn
+++ b/pw_unit_test/py/BUILD.gn
@@ -34,6 +34,7 @@ pw_python_package("py") {
"..:unit_test_proto.python",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
pw_python_script("rpc_service_test") {
@@ -45,6 +46,7 @@ pw_python_script("rpc_service_test") {
"$dir_pw_status/py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
action = {
args = [
diff --git a/pw_unit_test/py/pw_unit_test/__init__.py b/pw_unit_test/py/pw_unit_test/__init__.py
index b1a78141e..803b2c7f9 100644
--- a/pw_unit_test/py/pw_unit_test/__init__.py
+++ b/pw_unit_test/py/pw_unit_test/__init__.py
@@ -13,4 +13,10 @@
# the License.
"""Utilities for running unit tests over Pigweed RPC."""
-from pw_unit_test.rpc import run_tests, EventHandler, TestCase
+from pw_unit_test.rpc import (
+ run_tests,
+ EventHandler,
+ TestCase,
+ TestExpectation,
+ TestCaseResult,
+)
diff --git a/pw_unit_test/py/pw_unit_test/rpc.py b/pw_unit_test/py/pw_unit_test/rpc.py
index 5a9e9f90c..ac1c56a2f 100644
--- a/pw_unit_test/py/pw_unit_test/rpc.py
+++ b/pw_unit_test/py/pw_unit_test/rpc.py
@@ -13,12 +13,13 @@
# the License.
"""Utilities for running unit tests over Pigweed RPC."""
+import enum
import abc
from dataclasses import dataclass
import logging
from typing import Iterable
-import pw_rpc.client
+from pw_rpc.client import Services
from pw_rpc.callback_client import OptionalTimeout, UseDefault
from pw_unit_test_proto import unit_test_pb2
@@ -39,8 +40,11 @@ class TestCase:
def _test_case(raw_test_case: unit_test_pb2.TestCaseDescriptor) -> TestCase:
- return TestCase(raw_test_case.suite_name, raw_test_case.test_name,
- raw_test_case.file_name)
+ return TestCase(
+ raw_test_case.suite_name,
+ raw_test_case.test_name,
+ raw_test_case.file_name,
+ )
@dataclass(frozen=True)
@@ -57,58 +61,71 @@ class TestExpectation:
return f'TestExpectation({str(self)})'
+class TestCaseResult(enum.IntEnum):
+ SUCCESS = unit_test_pb2.TestCaseResult.SUCCESS
+ FAILURE = unit_test_pb2.TestCaseResult.FAILURE
+ SKIPPED = unit_test_pb2.TestCaseResult.SKIPPED
+
+
class EventHandler(abc.ABC):
@abc.abstractmethod
- def run_all_tests_start(self):
+ def run_all_tests_start(self) -> None:
"""Called before all tests are run."""
@abc.abstractmethod
- def run_all_tests_end(self, passed_tests: int, failed_tests: int):
+ def run_all_tests_end(self, passed_tests: int, failed_tests: int) -> None:
"""Called after the test run is complete."""
@abc.abstractmethod
- def test_case_start(self, test_case: TestCase):
+ def test_case_start(self, test_case: TestCase) -> None:
"""Called when a new test case is started."""
@abc.abstractmethod
- def test_case_end(self, test_case: TestCase, result: int):
+ def test_case_end(
+ self, test_case: TestCase, result: TestCaseResult
+ ) -> None:
"""Called when a test case completes with its overall result."""
@abc.abstractmethod
- def test_case_disabled(self, test_case: TestCase):
+ def test_case_disabled(self, test_case: TestCase) -> None:
"""Called when a disabled test case is encountered."""
@abc.abstractmethod
- def test_case_expect(self, test_case: TestCase,
- expectation: TestExpectation):
+ def test_case_expect(
+ self, test_case: TestCase, expectation: TestExpectation
+ ) -> None:
"""Called after each expect/assert statement within a test case."""
class LoggingEventHandler(EventHandler):
"""Event handler that logs test events using Google Test format."""
- def run_all_tests_start(self):
+
+ def run_all_tests_start(self) -> None:
_LOG.info('[==========] Running all tests.')
- def run_all_tests_end(self, passed_tests: int, failed_tests: int):
+ def run_all_tests_end(self, passed_tests: int, failed_tests: int) -> None:
_LOG.info('[==========] Done running all tests.')
_LOG.info('[ PASSED ] %d test(s).', passed_tests)
if failed_tests:
_LOG.info('[ FAILED ] %d test(s).', failed_tests)
- def test_case_start(self, test_case: TestCase):
+ def test_case_start(self, test_case: TestCase) -> None:
_LOG.info('[ RUN ] %s', test_case)
- def test_case_end(self, test_case: TestCase, result: int):
- if result == unit_test_pb2.TestCaseResult.SUCCESS:
+ def test_case_end(
+ self, test_case: TestCase, result: TestCaseResult
+ ) -> None:
+ if result == TestCaseResult.SUCCESS:
_LOG.info('[ OK ] %s', test_case)
else:
_LOG.info('[ FAILED ] %s', test_case)
- def test_case_disabled(self, test_case: TestCase):
+ def test_case_disabled(self, test_case: TestCase) -> None:
_LOG.info('Skipping disabled test %s', test_case)
- def test_case_expect(self, test_case: TestCase,
- expectation: TestExpectation):
+ def test_case_expect(
+ self, test_case: TestCase, expectation: TestExpectation
+ ) -> None:
result = 'Success' if expectation.success else 'Failure'
log = _LOG.info if expectation.success else _LOG.error
log('%s:%d: %s', test_case.file_name, expectation.line_number, result)
@@ -116,12 +133,13 @@ class LoggingEventHandler(EventHandler):
log(' Actual: %s', expectation.evaluated_expression)
-def run_tests(rpcs: pw_rpc.client.Services,
- report_passed_expectations: bool = False,
- test_suites: Iterable[str] = (),
- event_handlers: Iterable[EventHandler] = (
- LoggingEventHandler(), ),
- timeout_s: OptionalTimeout = UseDefault.VALUE) -> bool:
+def run_tests(
+ rpcs: Services,
+ report_passed_expectations: bool = False,
+ test_suites: Iterable[str] = (),
+ event_handlers: Iterable[EventHandler] = (LoggingEventHandler(),),
+ timeout_s: OptionalTimeout = UseDefault.VALUE,
+) -> bool:
"""Runs unit tests on a device over Pigweed RPC.
Calls each of the provided event handlers as test events occur, and returns
@@ -130,7 +148,8 @@ def run_tests(rpcs: pw_rpc.client.Services,
unit_test_service = rpcs.pw.unit_test.UnitTest # type: ignore[attr-defined]
request = unit_test_service.Run.request(
report_passed_expectations=report_passed_expectations,
- test_suite=test_suites)
+ test_suite=test_suites,
+ )
call = unit_test_service.Run.invoke(request, timeout_s=timeout_s)
test_responses = iter(call)
@@ -140,14 +159,17 @@ def run_tests(rpcs: pw_rpc.client.Services,
except StopIteration:
_LOG.error(
'The "test_run_start" message was dropped! UnitTest.Run '
- 'concluded with %s.', call.status)
+ 'concluded with %s.',
+ call.status,
+ )
raise
if not first_response.HasField('test_run_start'):
raise ValueError(
'Expected a "test_run_start" response from pw.unit_test.Run, '
'but received a different message type. A response may have been '
- 'dropped.')
+ 'dropped.'
+ )
for event_handler in event_handlers:
event_handler.run_all_tests_start()
@@ -163,18 +185,20 @@ def run_tests(rpcs: pw_rpc.client.Services,
if response.HasField('test_run_start'):
event_handler.run_all_tests_start()
elif response.HasField('test_run_end'):
- event_handler.run_all_tests_end(response.test_run_end.passed,
- response.test_run_end.failed)
+ event_handler.run_all_tests_end(
+ response.test_run_end.passed, response.test_run_end.failed
+ )
if response.test_run_end.failed == 0:
all_tests_passed = True
elif response.HasField('test_case_start'):
event_handler.test_case_start(current_test_case)
elif response.HasField('test_case_end'):
- event_handler.test_case_end(current_test_case,
- response.test_case_end)
+ result = TestCaseResult(response.test_case_end)
+ event_handler.test_case_end(current_test_case, result)
elif response.HasField('test_case_disabled'):
event_handler.test_case_disabled(
- _test_case(response.test_case_disabled))
+ _test_case(response.test_case_disabled)
+ )
elif response.HasField('test_case_expectation'):
raw_expectation = response.test_case_expectation
expectation = TestExpectation(
diff --git a/pw_unit_test/py/pw_unit_test/test_runner.py b/pw_unit_test/py/pw_unit_test/test_runner.py
index 420c9a863..a7b06ac5e 100644
--- a/pw_unit_test/py/pw_unit_test/test_runner.py
+++ b/pw_unit_test/py/pw_unit_test/test_runner.py
@@ -15,57 +15,79 @@
import argparse
import asyncio
+import base64
+import datetime
import enum
import json
import logging
import os
+import re
import subprocess
import sys
+import time
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple
+import requests
+
import pw_cli.log
import pw_cli.process
# Global logger for the script.
_LOG: logging.Logger = logging.getLogger(__name__)
+_ANSI_SEQUENCE_REGEX = re.compile(rb'\x1b[^m]*m')
+
+
+def _strip_ansi(bytes_with_sequences: bytes) -> bytes:
+ """Strip out ANSI escape sequences."""
+ return _ANSI_SEQUENCE_REGEX.sub(b'', bytes_with_sequences)
+
def register_arguments(parser: argparse.ArgumentParser) -> None:
"""Registers command-line arguments."""
- parser.add_argument('--root',
- type=str,
- default='out',
- help='Path to the root build directory')
- parser.add_argument('-r',
- '--runner',
- type=str,
- required=True,
- help='Executable which runs a test on the target')
- parser.add_argument('-m',
- '--timeout',
- type=float,
- help='Timeout for test runner in seconds')
- parser.add_argument('runner_args',
- nargs="*",
- help='Arguments to forward to the test runner')
+ parser.add_argument(
+ '--root',
+ type=str,
+ default='out',
+ help='Path to the root build directory',
+ )
+ parser.add_argument(
+ '-r',
+ '--runner',
+ type=str,
+ required=True,
+ help='Executable which runs a test on the target',
+ )
+ parser.add_argument(
+ '-m', '--timeout', type=float, help='Timeout for test runner in seconds'
+ )
+ parser.add_argument(
+ '--coverage-profraw',
+ type=str,
+ help='The name of the coverage profraw file to produce with the'
+ ' coverage information from this test. Only provide this if the test'
+ ' should be run for coverage and is properly instrumented.',
+ )
+ parser.add_argument(
+ 'runner_args', nargs="*", help='Arguments to forward to the test runner'
+ )
# The runner script can either run binaries directly or groups.
group = parser.add_mutually_exclusive_group()
- group.add_argument('-g',
- '--group',
- action='append',
- help='Test groups to run')
- group.add_argument('-t',
- '--test',
- action='append',
- help='Test binaries to run')
+ group.add_argument(
+ '-g', '--group', action='append', help='Test groups to run'
+ )
+ group.add_argument(
+ '-t', '--test', action='append', help='Test binaries to run'
+ )
class TestResult(enum.Enum):
"""Result of a single unit test run."""
+
UNKNOWN = 0
SUCCESS = 1
FAILURE = 2
@@ -73,10 +95,13 @@ class TestResult(enum.Enum):
class Test:
"""A unit test executable."""
- def __init__(self, name: str, file_path: str):
+
+ def __init__(self, name: str, file_path: str) -> None:
self.name: str = name
self.file_path: str = file_path
self.status: TestResult = TestResult.UNKNOWN
+ self.start_time: datetime.datetime
+ self.duration_s: float
def __repr__(self) -> str:
return f'Test({self.name})'
@@ -92,6 +117,7 @@ class Test:
class TestGroup:
"""Graph node representing a group of unit tests."""
+
def __init__(self, name: str, tests: Iterable[Test]):
self._name: str = name
self._deps: Iterable['TestGroup'] = []
@@ -113,7 +139,9 @@ class TestGroup:
for dep in self._deps:
tests.update(
dep._all_test_dependencies( # pylint: disable=protected-access
- processed_groups))
+ processed_groups
+ )
+ )
tests.update(self._tests)
processed_groups.add(self._name)
@@ -126,15 +154,29 @@ class TestGroup:
class TestRunner:
"""Runs unit tests by calling out to a runner script."""
- def __init__(self,
- executable: str,
- args: Sequence[str],
- tests: Iterable[Test],
- timeout: Optional[float] = None):
+
+ def __init__(
+ self,
+ executable: str,
+ args: Sequence[str],
+ tests: Iterable[Test],
+ coverage_profraw: Optional[str] = None,
+ timeout: Optional[float] = None,
+ ) -> None:
self._executable: str = executable
self._args: Sequence[str] = args
self._tests: List[Test] = list(tests)
+ self._coverage_profraw = coverage_profraw
self._timeout = timeout
+ self._result_sink: Optional[Dict[str, str]] = None
+
+ # Access go/result-sink, if available.
+ ctx_path = Path(os.environ.get("LUCI_CONTEXT", ''))
+ if not ctx_path.is_file():
+ return
+
+ ctx = json.loads(ctx_path.read_text(encoding='utf-8'))
+ self._result_sink = ctx.get('result_sink', None)
async def run_tests(self) -> None:
"""Runs all registered unit tests through the runner script."""
@@ -143,42 +185,130 @@ class TestRunner:
total = str(len(self._tests))
test_counter = f'Test {idx:{len(total)}}/{total}'
- _LOG.info('%s: [ RUN] %s', test_counter, test.name)
+ _LOG.debug('%s: [ RUN] %s', test_counter, test.name)
# Convert POSIX to native directory seperators as GN produces '/'
# but the Windows test runner needs '\\'.
command = [
str(Path(self._executable)),
- str(Path(test.file_path)), *self._args
+ *self._args,
+ str(Path(test.file_path)),
]
if self._executable.endswith('.py'):
command.insert(0, sys.executable)
+ test.start_time = datetime.datetime.now(datetime.timezone.utc)
+ start_time = time.monotonic()
try:
- process = await pw_cli.process.run_async(*command,
- timeout=self._timeout)
- if process.returncode == 0:
- test.status = TestResult.SUCCESS
- test_result = 'PASS'
- else:
- test.status = TestResult.FAILURE
- test_result = 'FAIL'
-
- _LOG.log(pw_cli.log.LOGLEVEL_STDOUT, '[%s]\n%s',
- pw_cli.color.colors().bold_white(process.pid),
- process.output.decode(errors='ignore').rstrip())
-
- _LOG.info('%s: [%s] %s', test_counter, test_result,
- test.name)
+ env = {}
+ if self._coverage_profraw is not None:
+ env['LLVM_PROFILE_FILE'] = str(Path(self._coverage_profraw))
+ process = await pw_cli.process.run_async(
+ *command, env=env, timeout=self._timeout
+ )
except subprocess.CalledProcessError as err:
_LOG.error(err)
return
+ test.duration_s = time.monotonic() - start_time
+
+ if process.returncode == 0:
+ test.status = TestResult.SUCCESS
+ test_result = 'PASS'
+ else:
+ test.status = TestResult.FAILURE
+ test_result = 'FAIL'
+
+ _LOG.log(
+ pw_cli.log.LOGLEVEL_STDOUT,
+ '[Pid: %s]\n%s',
+ pw_cli.color.colors().bold_white(process.pid),
+ process.output.decode(errors='ignore').rstrip(),
+ )
+
+ _LOG.info(
+ '%s: [%s] %s in %.3f s',
+ test_counter,
+ test_result,
+ test.name,
+ test.duration_s,
+ )
+
+ try:
+ self._maybe_upload_to_resultdb(test, process)
+ except requests.exceptions.HTTPError as err:
+ _LOG.error(err)
+ return
def all_passed(self) -> bool:
"""Returns true if all unit tests passed."""
return all(test.status is TestResult.SUCCESS for test in self._tests)
+ def _maybe_upload_to_resultdb(
+ self, test: Test, process: pw_cli.process.CompletedProcess
+ ):
+ """Uploads test result to ResultDB, if available."""
+ if self._result_sink is None:
+ # ResultDB integration not enabled.
+ return
+
+ test_result = {
+ # The test.name is not suitable as an identifier because it's just
+ # the basename of the test (channel_test). We want the full path,
+ # including the toolchain used.
+ "testId": test.file_path,
+ # ResultDB also supports CRASH and ABORT, but there's currently no
+ # way to distinguish these in pw_unit_test.
+ "status": "PASS" if test.status is TestResult.SUCCESS else "FAIL",
+ # The "expected" field is required. It could be used to report
+ # expected failures, but we don't currently support these in
+ # pw_unit_test.
+ "expected": test.status is TestResult.SUCCESS,
+ # Ensure to format the duration with '%.9fs' to avoid scientific
+ # notation. If a value is too large or small and formatted with
+ # str() or '%s', python formats the value in scientific notation,
+ # like '1.1e-10', which is an invalid input for
+ # google.protobuf.duration.
+ "duration": "%.9fs" % test.duration_s,
+ "start_time": test.start_time.isoformat(),
+ "testMetadata": {
+ # Use the file path as the test name in the Milo UI. (If this is
+ # left unspecified, the UI will attempt to build a "good enough"
+ # name by truncating the testId. That produces less readable
+ # results.)
+ "name": test.file_path,
+ },
+ "summaryHtml": (
+ '<p><text-artifact '
+ 'artifact-id="artifact-content-in-request"></p>'
+ ),
+ "artifacts": {
+ "artifact-content-in-request": {
+ # Need to decode the bytes back to ASCII or they will not be
+ # encodable by json.dumps.
+ #
+ # TODO(b/248349219): Instead of stripping the ANSI color
+ # codes, convert them to HTML.
+ "contents": base64.b64encode(
+ _strip_ansi(process.output)
+ ).decode('ascii'),
+ },
+ },
+ }
+
+ requests.post(
+ url='http://%s/prpc/luci.resultsink.v1.Sink/ReportTestResults'
+ % self._result_sink['address'],
+ headers={
+ 'Content-Type': 'application/json',
+ 'Accept': 'application/json',
+ 'Authorization': 'ResultSink %s'
+ % self._result_sink['auth_token'],
+ },
+ data=json.dumps({'testResults': [test_result]}),
+ timeout=5.0,
+ ).raise_for_status()
+
# Filename extension for unit test metadata files.
METADATA_EXTENSION = '.testinfo.json'
@@ -224,7 +354,8 @@ def find_binary(target: str) -> str:
return potential_filename
raise FileNotFoundError(
- f'Could not find output binary for build target {target}')
+ f'Could not find output binary for build target {target}'
+ )
def parse_metadata(metadata: List[str], root: str) -> Dict[str, TestGroup]:
@@ -239,6 +370,7 @@ def parse_metadata(metadata: List[str], root: str) -> Dict[str, TestGroup]:
populated with the paths to their unit tests and references to their
dependencies.
"""
+
def canonicalize(path: str) -> str:
"""Removes a trailing slash from a GN target's directory.
@@ -248,7 +380,7 @@ def parse_metadata(metadata: List[str], root: str) -> Dict[str, TestGroup]:
index = path.find(':')
if index == -1 or path[index - 1] != '/':
return path
- return path[:index - 1] + path[index:]
+ return path[: index - 1] + path[index:]
group_deps: List[Tuple[str, List[str]]] = []
all_tests: Dict[str, Test] = {}
@@ -270,11 +402,13 @@ def parse_metadata(metadata: List[str], root: str) -> Dict[str, TestGroup]:
elif entry['type'] == 'test':
test_directory = os.path.join(root, entry['test_directory'])
test_binary = find_binary(
- f'{test_directory}:{entry["test_name"]}')
+ f'{test_directory}:{entry["test_name"]}'
+ )
if test_binary not in all_tests:
- all_tests[test_binary] = Test(entry['test_name'],
- test_binary)
+ all_tests[test_binary] = Test(
+ entry['test_name'], test_binary
+ )
tests.append(all_tests[test_binary])
@@ -291,8 +425,9 @@ def parse_metadata(metadata: List[str], root: str) -> Dict[str, TestGroup]:
return test_groups
-def tests_from_groups(group_names: Optional[Sequence[str]],
- root: str) -> List[Test]:
+def tests_from_groups(
+ group_names: Optional[Sequence[str]], root: str
+) -> List[Test]:
"""Returns unit tests belonging to test groups and their dependencies.
If args.names is nonempty, only searches groups specified there.
@@ -330,8 +465,9 @@ def tests_from_paths(paths: Sequence[str]) -> List[Test]:
async def find_and_run_tests(
root: str,
runner: str,
- timeout: Optional[float],
runner_args: Sequence[str] = (),
+ coverage_profraw: Optional[str] = None,
+ timeout: Optional[float] = None,
group: Optional[Sequence[str]] = None,
test: Optional[Sequence[str]] = None,
) -> int:
@@ -342,7 +478,9 @@ async def find_and_run_tests(
else:
tests = tests_from_groups(group, root)
- test_runner = TestRunner(runner, runner_args, tests, timeout)
+ test_runner = TestRunner(
+ runner, runner_args, tests, coverage_profraw, timeout
+ )
await test_runner.run_tests()
return 0 if test_runner.all_passed() else 1
@@ -353,10 +491,12 @@ def main() -> int:
parser = argparse.ArgumentParser(description=main.__doc__)
register_arguments(parser)
- parser.add_argument('-v',
- '--verbose',
- action='store_true',
- help='Output additional logs as the script runs')
+ parser.add_argument(
+ '-v',
+ '--verbose',
+ action='store_true',
+ help='Output additional logs as the script runs',
+ )
args_as_dict = dict(vars(parser.parse_args()))
del args_as_dict['verbose']
diff --git a/pw_unit_test/py/rpc_service_test.py b/pw_unit_test/py/rpc_service_test.py
index 66c4f4c75..b19f3d785 100755
--- a/pw_unit_test/py/rpc_service_test.py
+++ b/pw_unit_test/py/rpc_service_test.py
@@ -35,7 +35,8 @@ FAILING = tuple(TestCase('Failing', case, _FILE) for case in _CASES[:-1])
EXECUTED_TESTS = PASSING + FAILING
DISABLED_SUITE = tuple(
- TestCase('DISABLED_Disabled', case, _FILE) for case in _CASES)
+ TestCase('DISABLED_Disabled', case, _FILE) for case in _CASES
+)
ALL_DISABLED_TESTS = (
TestCase('Passing', 'DISABLED_Disabled', _FILE),
@@ -46,12 +47,14 @@ ALL_DISABLED_TESTS = (
class RpcIntegrationTest(unittest.TestCase):
"""Calls RPCs on an RPC server through a socket."""
+
test_server_command: Tuple[str, ...] = ()
port: int
def setUp(self) -> None:
self._context = rpc.HdlcRpcLocalServerAndClient(
- self.test_server_command, self.port, [unit_test_pb2])
+ self.test_server_command, self.port, [unit_test_pb2]
+ )
self.rpcs = self._context.client.channel(1).rpcs
self.handler = mock.NonCallableMagicMock(spec=EventHandler)
@@ -59,8 +62,7 @@ class RpcIntegrationTest(unittest.TestCase):
self._context.close()
def test_run_tests_default_handler(self) -> None:
- with self.assertLogs(logging.getLogger('pw_unit_test'),
- 'INFO') as logs:
+ with self.assertLogs(logging.getLogger('pw_unit_test'), 'INFO') as logs:
self.assertFalse(run_tests(self.rpcs))
for test in EXECUTED_TESTS:
@@ -70,15 +72,19 @@ class RpcIntegrationTest(unittest.TestCase):
self.assertFalse(run_tests(self.rpcs, event_handlers=[self.handler]))
self.handler.test_case_start.assert_has_calls(
- [mock.call(case) for case in EXECUTED_TESTS], any_order=True)
+ [mock.call(case) for case in EXECUTED_TESTS], any_order=True
+ )
def test_run_tests_calls_test_case_end(self) -> None:
self.assertFalse(run_tests(self.rpcs, event_handlers=[self.handler]))
calls = [
mock.call(
- case, unit_test_pb2.SUCCESS
- if case.suite_name == 'Passing' else unit_test_pb2.FAILURE)
+ case,
+ unit_test_pb2.SUCCESS
+ if case.suite_name == 'Passing'
+ else unit_test_pb2.FAILURE,
+ )
for case in EXECUTED_TESTS
]
self.handler.test_case_end.assert_has_calls(calls, any_order=True)
@@ -87,38 +93,50 @@ class RpcIntegrationTest(unittest.TestCase):
self.assertFalse(run_tests(self.rpcs, event_handlers=[self.handler]))
self.handler.test_case_disabled.assert_has_calls(
- [mock.call(case) for case in ALL_DISABLED_TESTS], any_order=True)
+ [mock.call(case) for case in ALL_DISABLED_TESTS], any_order=True
+ )
def test_passing_tests_only(self) -> None:
self.assertTrue(
- run_tests(self.rpcs,
- test_suites=['Passing'],
- event_handlers=[self.handler]))
+ run_tests(
+ self.rpcs,
+ test_suites=['Passing'],
+ event_handlers=[self.handler],
+ )
+ )
calls = [mock.call(case, unit_test_pb2.SUCCESS) for case in PASSING]
self.handler.test_case_end.assert_has_calls(calls, any_order=True)
def test_disabled_tests_only(self) -> None:
self.assertTrue(
- run_tests(self.rpcs,
- test_suites=['DISABLED_Disabled'],
- event_handlers=[self.handler]))
+ run_tests(
+ self.rpcs,
+ test_suites=['DISABLED_Disabled'],
+ event_handlers=[self.handler],
+ )
+ )
self.handler.test_case_start.assert_not_called()
self.handler.test_case_end.assert_not_called()
self.handler.test_case_disabled.assert_has_calls(
- [mock.call(case) for case in DISABLED_SUITE], any_order=True)
+ [mock.call(case) for case in DISABLED_SUITE], any_order=True
+ )
def test_failing_tests(self) -> None:
self.assertFalse(
- run_tests(self.rpcs,
- test_suites=['Failing'],
- event_handlers=[self.handler]))
+ run_tests(
+ self.rpcs,
+ test_suites=['Failing'],
+ event_handlers=[self.handler],
+ )
+ )
calls = [mock.call(case, unit_test_pb2.FAILURE) for case in FAILING]
self.handler.test_case_end.assert_has_calls(calls, any_order=True)
-def _main(test_server_command: List[str], port: int,
- unittest_args: List[str]) -> None:
+def _main(
+ test_server_command: List[str], port: int, unittest_args: List[str]
+) -> None:
RpcIntegrationTest.test_server_command = tuple(test_server_command)
RpcIntegrationTest.port = port
unittest.main(argv=unittest_args)
diff --git a/pw_unit_test/py/setup.cfg b/pw_unit_test/py/setup.cfg
index c73ce6922..d0f7b5d9d 100644
--- a/pw_unit_test/py/setup.cfg
+++ b/pw_unit_test/py/setup.cfg
@@ -21,7 +21,9 @@ description = Unit tests for Pigweed projects
[options]
packages = find:
zip_safe = False
-install_requires = pw_cli; pw_rpc; pw_unit_test_proto
+install_requires =
+ requests
+ types-requests
[options.package_data]
pw_unit_test = py.typed
diff --git a/pw_unit_test/py/test_group_metadata_test.py b/pw_unit_test/py/test_group_metadata_test.py
new file mode 100644
index 000000000..d54e86fbc
--- /dev/null
+++ b/pw_unit_test/py/test_group_metadata_test.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests that pw_test_group outputs expected metadata."""
+
+import argparse
+import json
+import pathlib
+import sys
+import unittest
+
+TEST_TARGET_NAME = 'metadata_only_test'
+EXTRA_METADATA_ENTRY = {'extra_key': 'extra_value'}
+
+
+class TestGroupMetadataTest(unittest.TestCase):
+ """Tests that pw_test_group outputs expected metadata."""
+
+ metadata_path: pathlib.Path
+
+ def test_metadata_exists(self) -> None:
+ """Asserts that the metadata file has been created."""
+ self.assertTrue(self.metadata_path.exists())
+
+ def test_metadata_has_test_entry(self) -> None:
+ """Asserts that the expected test entry is present."""
+ meta_text = self.metadata_path.read_text()
+ try:
+ meta = json.loads(meta_text)
+ except json.decoder.JSONDecodeError as jde:
+ raise ValueError(
+ f'Failed to decode file {self.metadata_path} as JSON: {meta_text}'
+ ) from jde
+ self.assertIsInstance(meta, list)
+ found = False
+ for test_entry in meta:
+ self.assertIn('type', test_entry)
+ if test_entry['type'] != 'test':
+ continue
+ self.assertIn('test_name', test_entry)
+ self.assertIn('test_directory', test_entry)
+ if test_entry['test_name'] == TEST_TARGET_NAME:
+ found = True
+ self.assertIn('extra_metadata', test_entry)
+ self.assertEqual(
+ test_entry['extra_metadata'], EXTRA_METADATA_ENTRY
+ )
+ self.assertTrue(found)
+
+
+def main() -> None:
+ """Tests that pw_test_group outputs expected metadata."""
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--stamp-path',
+ type=pathlib.Path,
+ required=True,
+ help='Path to the stamp file output of pw_test_group',
+ )
+ parser.add_argument(
+ 'unittest_args',
+ nargs=argparse.REMAINDER,
+ help='Arguments after "--" are passed to unittest.',
+ )
+ args = parser.parse_args()
+ # Use the stamp file location to find the location of the metadata file.
+ # Unfortunately, there's no reliable toolchain-agnostic way to grab the path
+ # to the metadata file itself within GN.
+ TestGroupMetadataTest.metadata_path = (
+ args.stamp_path.parent / 'metadata_only_group.testinfo.json'
+ )
+ unittest_args = sys.argv[:1] + args.unittest_args[1:]
+ unittest.main(argv=unittest_args)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/pw_unit_test/rpc_gtest_event_handler.cc b/pw_unit_test/rpc_gtest_event_handler.cc
new file mode 100644
index 000000000..9428c4c92
--- /dev/null
+++ b/pw_unit_test/rpc_gtest_event_handler.cc
@@ -0,0 +1,106 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <tuple>
+
+#include "gtest/gtest.h"
+#include "pw_unit_test/internal/rpc_event_handler.h"
+#include "pw_unit_test/unit_test_service.h"
+
+namespace pw::unit_test::internal {
+
+RpcEventHandler::RpcEventHandler(UnitTestService& service) : service_(service) {
+ // Initialize GoogleTest and disable the default result printer.
+ testing::InitGoogleTest();
+ auto unit_test = testing::UnitTest::GetInstance();
+ auto default_listener = unit_test->listeners().default_result_printer();
+ unit_test->listeners().Release(default_listener);
+ delete default_listener;
+}
+
+void RpcEventHandler::ExecuteTests(span<std::string_view> suites_to_run) {
+ if (!suites_to_run.empty()) {
+ PW_LOG_WARN(
+ "GoogleTest backend does not support test suite filtering. Running all "
+ "suites.");
+ }
+ if (service_.verbose_) {
+ PW_LOG_WARN(
+ "GoogleTest backend does not support reporting passed expectations.");
+ }
+
+ auto unit_test = testing::UnitTest::GetInstance();
+ unit_test->listeners().Append(this);
+
+ std::ignore = RUN_ALL_TESTS();
+
+ unit_test->listeners().Release(this);
+}
+
+void RpcEventHandler::OnTestProgramStart(const testing::UnitTest&) {
+ service_.WriteTestRunStart();
+}
+
+void RpcEventHandler::OnTestProgramEnd(const testing::UnitTest& unit_test) {
+ RunTestsSummary run_tests_summary{
+ .passed_tests = unit_test.successful_test_count(),
+ .failed_tests = unit_test.failed_test_count(),
+ .skipped_tests = unit_test.skipped_test_count(),
+ .disabled_tests = unit_test.disabled_test_count(),
+ };
+ service_.WriteTestRunEnd(run_tests_summary);
+}
+
+void RpcEventHandler::OnTestStart(const testing::TestInfo& test_info) {
+ TestCase test_case{
+ .suite_name = test_info.test_suite_name(),
+ .test_name = test_info.name(),
+ .file_name = test_info.file(),
+ };
+ service_.WriteTestCaseStart(test_case);
+}
+
+void RpcEventHandler::OnTestEnd(const testing::TestInfo& test_info) {
+ TestResult result;
+ if (test_info.result()->Passed()) {
+ result = TestResult::kSuccess;
+ } else if (test_info.result()->Skipped()) {
+ result = TestResult::kSkipped;
+ } else {
+ result = TestResult::kFailure;
+ }
+
+ service_.WriteTestCaseEnd(result);
+}
+
+void RpcEventHandler::OnTestPartResult(const testing::TestPartResult& result) {
+ TestExpectation expectation{
+ .expression = "",
+ .evaluated_expression = result.summary(),
+ .line_number = result.line_number(),
+ .success = result.passed(),
+ };
+ service_.WriteTestCaseExpectation(expectation);
+}
+
+void RpcEventHandler::OnTestDisabled(const testing::TestInfo& test_info) {
+ TestCase test_case{
+ .suite_name = test_info.test_suite_name(),
+ .test_name = test_info.name(),
+ .file_name = test_info.file(),
+ };
+ service_.WriteTestCaseDisabled(test_case);
+}
+
+} // namespace pw::unit_test::internal
diff --git a/pw_unit_test/rpc_gtest_public/pw_unit_test/internal/rpc_event_handler.h b/pw_unit_test/rpc_gtest_public/pw_unit_test/internal/rpc_event_handler.h
new file mode 100644
index 000000000..1a9a7547a
--- /dev/null
+++ b/pw_unit_test/rpc_gtest_public/pw_unit_test/internal/rpc_event_handler.h
@@ -0,0 +1,44 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include "gtest/gtest.h"
+#include "pw_span/span.h"
+
+namespace pw::unit_test {
+
+class UnitTestService;
+
+namespace internal {
+
+// GoogleTest event handler that streams test events through an RPC service.
+class RpcEventHandler : public testing::EmptyTestEventListener {
+ public:
+ RpcEventHandler(UnitTestService& service);
+ void ExecuteTests(span<std::string_view> suites_to_run);
+
+ void OnTestProgramStart(const testing::UnitTest& unit_test) override;
+ void OnTestProgramEnd(const testing::UnitTest& unit_test) override;
+ void OnTestStart(const testing::TestInfo& test_info) override;
+ void OnTestEnd(const testing::TestInfo& test_info) override;
+ void OnTestPartResult(
+ const testing::TestPartResult& test_part_result) override;
+ void OnTestDisabled(const testing::TestInfo& test_info) override;
+
+ private:
+ UnitTestService& service_;
+};
+
+} // namespace internal
+} // namespace pw::unit_test
diff --git a/pw_unit_test/rpc_event_handler.cc b/pw_unit_test/rpc_light_event_handler.cc
index 291dec39a..29028ece1 100644
--- a/pw_unit_test/rpc_event_handler.cc
+++ b/pw_unit_test/rpc_light_event_handler.cc
@@ -13,11 +13,26 @@
// the License.
#include "pw_unit_test/internal/rpc_event_handler.h"
-
#include "pw_unit_test/unit_test_service.h"
namespace pw::unit_test::internal {
+RpcEventHandler::RpcEventHandler(UnitTestService& service)
+ : service_(service) {}
+
+void RpcEventHandler::ExecuteTests(span<std::string_view> suites_to_run) {
+ RegisterEventHandler(this);
+ SetTestSuitesToRun(suites_to_run);
+
+ PW_LOG_DEBUG("%u test suite filters applied",
+ static_cast<unsigned>(suites_to_run.size()));
+
+ RUN_ALL_TESTS();
+
+ RegisterEventHandler(nullptr);
+ SetTestSuitesToRun({});
+}
+
void RpcEventHandler::RunAllTestsStart() { service_.WriteTestRunStart(); }
void RpcEventHandler::RunAllTestsEnd(const RunTestsSummary& run_tests_summary) {
diff --git a/pw_unit_test/public/pw_unit_test/internal/rpc_event_handler.h b/pw_unit_test/rpc_light_public/pw_unit_test/internal/rpc_event_handler.h
index ad27fc0bb..fe067a6e9 100644
--- a/pw_unit_test/public/pw_unit_test/internal/rpc_event_handler.h
+++ b/pw_unit_test/rpc_light_public/pw_unit_test/internal/rpc_event_handler.h
@@ -13,7 +13,8 @@
// the License.
#pragma once
-#include "pw_unit_test/event_handler.h"
+#include "gtest/gtest.h"
+#include "pw_span/span.h"
namespace pw::unit_test {
@@ -24,7 +25,15 @@ namespace internal {
// Unit test event handler that streams test events through an RPC service.
class RpcEventHandler : public EventHandler {
public:
- RpcEventHandler(UnitTestService& service) : service_(service) {}
+ RpcEventHandler(UnitTestService& service);
+ void ExecuteTests(span<std::string_view> suites_to_run);
+
+ void TestProgramStart(const ProgramSummary&) override {}
+ void EnvironmentsSetUpEnd() override {}
+ void TestSuiteStart(const TestSuite&) override {}
+ void TestSuiteEnd(const TestSuite&) override {}
+ void EnvironmentsTearDownEnd() override {}
+ void TestProgramEnd(const ProgramSummary&) override {}
void RunAllTestsStart() override;
void RunAllTestsEnd(const RunTestsSummary& run_tests_summary) override;
diff --git a/pw_unit_test/simple_printing_event_handler.cc b/pw_unit_test/simple_printing_event_handler.cc
index 5a84d578e..298fc4252 100644
--- a/pw_unit_test/simple_printing_event_handler.cc
+++ b/pw_unit_test/simple_printing_event_handler.cc
@@ -16,63 +16,9 @@
#include <cstdarg>
#include <cstdio>
-#include <string_view>
namespace pw::unit_test {
-void SimplePrintingEventHandler::RunAllTestsStart() {
- WriteLine("[==========] Running all tests.");
-}
-
-void SimplePrintingEventHandler::RunAllTestsEnd(
- const RunTestsSummary& run_tests_summary) {
- WriteLine("[==========] Done running all tests.");
- WriteLine("[ PASSED ] %d test(s).", run_tests_summary.passed_tests);
- if (run_tests_summary.skipped_tests) {
- WriteLine("[ SKIPPED ] %d test(s).", run_tests_summary.skipped_tests);
- }
- if (run_tests_summary.failed_tests) {
- WriteLine("[ FAILED ] %d test(s).", run_tests_summary.failed_tests);
- }
-}
-
-void SimplePrintingEventHandler::TestCaseStart(const TestCase& test_case) {
- WriteLine("[ RUN ] %s.%s", test_case.suite_name, test_case.test_name);
-}
-
-void SimplePrintingEventHandler::TestCaseEnd(const TestCase& test_case,
- TestResult result) {
- // Use a switch with no default to detect changes in the test result enum.
- switch (result) {
- case TestResult::kSuccess:
- WriteLine(
- "[ OK ] %s.%s", test_case.suite_name, test_case.test_name);
- break;
- case TestResult::kFailure:
- WriteLine(
- "[ FAILED ] %s.%s", test_case.suite_name, test_case.test_name);
- break;
- case TestResult::kSkipped:
- WriteLine(
- "[ SKIPPED ] %s.%s", test_case.suite_name, test_case.test_name);
- break;
- }
-}
-
-void SimplePrintingEventHandler::TestCaseExpect(
- const TestCase& test_case, const TestExpectation& expectation) {
- if (!verbose_ && expectation.success) {
- return;
- }
-
- const char* result = expectation.success ? "Success" : "Failure";
- WriteLine("%s:%d: %s", test_case.file_name, expectation.line_number, result);
- WriteLine(" Expected: %s", expectation.expression);
-
- write_(" Actual: ", false);
- write_(expectation.evaluated_expression, true);
-}
-
void SimplePrintingEventHandler::WriteLine(const char* format, ...) {
va_list args;
@@ -83,10 +29,4 @@ void SimplePrintingEventHandler::WriteLine(const char* format, ...) {
write_(buffer_, true);
}
-void SimplePrintingEventHandler::TestCaseDisabled(const TestCase& test) {
- if (verbose_) {
- WriteLine("Skipping disabled test %s.%s", test.suite_name, test.test_name);
- }
-}
-
} // namespace pw::unit_test
diff --git a/pw_unit_test/simple_printing_main.cc b/pw_unit_test/simple_printing_main.cc
index 5aeab4578..d53ac29ac 100644
--- a/pw_unit_test/simple_printing_main.cc
+++ b/pw_unit_test/simple_printing_main.cc
@@ -12,20 +12,20 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include <span>
#include <string_view>
+#include "gtest/gtest.h"
+#include "pw_span/span.h"
#include "pw_sys_io/sys_io.h"
-#include "pw_unit_test/framework.h"
#include "pw_unit_test/simple_printing_event_handler.h"
int main() {
pw::unit_test::SimplePrintingEventHandler handler(
[](const std::string_view& s, bool append_newline) {
if (append_newline) {
- pw::sys_io::WriteLine(s);
+ pw::sys_io::WriteLine(s).IgnoreError();
} else {
- pw::sys_io::WriteBytes(std::as_bytes(std::span(s)));
+ pw::sys_io::WriteBytes(pw::as_bytes(pw::span(s))).IgnoreError();
}
});
diff --git a/pw_unit_test/static_library_archived_tests.cc b/pw_unit_test/static_library_archived_tests.cc
new file mode 100644
index 000000000..b1dfcb663
--- /dev/null
+++ b/pw_unit_test/static_library_archived_tests.cc
@@ -0,0 +1,29 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+
+namespace pw::unit_test {
+
+extern int test_1_executions;
+extern int test_2_executions;
+
+namespace {
+
+TEST(StaticLibraryArchivedTest, Test1) { test_1_executions += 1; }
+
+TEST(StaticLibraryArchivedTest, Test2) { test_2_executions += 1; }
+
+} // namespace
+} // namespace pw::unit_test
diff --git a/pw_unit_test/static_library_missing_archived_tests.cc b/pw_unit_test/static_library_missing_archived_tests.cc
new file mode 100644
index 000000000..a8c896b96
--- /dev/null
+++ b/pw_unit_test/static_library_missing_archived_tests.cc
@@ -0,0 +1,35 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "gtest/gtest.h"
+
+namespace pw::unit_test {
+
+extern int test_3_executions_not_expected;
+extern int test_4_executions_not_expected;
+
+namespace {
+
+TEST(StaticLibraryArchivedTest, ShouldNotRunTest3) {
+ test_3_executions_not_expected += 1;
+}
+
+TEST(StaticLibraryArchivedTest, ShouldNotRunTest4) {
+ test_4_executions_not_expected += 1;
+}
+
+TEST(StaticLibraryArchivedTest, Fails) { FAIL(); }
+
+} // namespace
+} // namespace pw::unit_test
diff --git a/pw_unit_test/static_library_support.cc b/pw_unit_test/static_library_support.cc
new file mode 100644
index 000000000..2b7f5196e
--- /dev/null
+++ b/pw_unit_test/static_library_support.cc
@@ -0,0 +1,23 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_unit_test/static_library_support.h"
+
+namespace pw::unit_test::internal {
+
+ReferToTestInfo::ReferToTestInfo(const TestInfo& info) {
+ [[maybe_unused]] const TestInfo* volatile force_reference = &info;
+}
+
+} // namespace pw::unit_test::internal
diff --git a/pw_unit_test/static_library_support_test.cc b/pw_unit_test/static_library_support_test.cc
new file mode 100644
index 000000000..166e77c2d
--- /dev/null
+++ b/pw_unit_test/static_library_support_test.cc
@@ -0,0 +1,57 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_unit_test/static_library_support.h"
+
+#include "pw_assert/check.h"
+
+namespace pw::unit_test {
+
+// Refer to one test in static_library_archived_tests.cc. Do not refer to any
+// tests in static_library_missing_archived_tests.cc, though; those tests are
+// expected to be lost because they are not referenced.
+PW_UNIT_TEST_LINK_FILE_CONTAINING_TEST(StaticLibraryArchivedTest, Test1);
+
+// Count the number of times each test executes.
+int test_1_executions = 0;
+int test_2_executions = 0;
+
+int test_3_executions_not_expected = 0;
+int test_4_executions_not_expected = 0;
+
+class CheckThatTestsRanWhenDestructed {
+ public:
+ ~CheckThatTestsRanWhenDestructed() {
+ PW_CHECK_INT_EQ(test_1_executions, 1);
+ PW_CHECK_INT_EQ(test_2_executions, 1);
+
+ PW_CHECK_INT_EQ(test_3_executions_not_expected, 0);
+ PW_CHECK_INT_EQ(test_4_executions_not_expected, 0);
+ }
+} check_that_tests_ran;
+
+// Test that linking fails if these macros refer to tests that do not exist.
+// These cannot be a negative compilation tests because they compile
+// successfully, but fail to link.
+#if 0
+
+PW_UNIT_TEST_LINK_FILE_CONTAINING_TEST(NotARealSuite, NotARealTest);
+
+#elif 0
+
+PW_UNIT_TEST_LINK_FILE_CONTAINING_TEST(StaticLibraryArchivedTest, NotARealTest);
+
+#endif // negative linking tests
+
+} // namespace pw::unit_test
diff --git a/pw_unit_test/test.cmake b/pw_unit_test/test.cmake
new file mode 100644
index 000000000..08e268eb3
--- /dev/null
+++ b/pw_unit_test/test.cmake
@@ -0,0 +1,345 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+set(pw_unit_test_GOOGLETEST_BACKEND pw_unit_test.light CACHE STRING
+ "CMake target which implements GoogleTest, by default pw_unit_test.light \
+ is used. You could, for example, point this at pw_third_party.googletest \
+ if using upstream GoogleTest directly on your host for GoogleMock.")
+
+# TODO(ewout): Remove the default.
+set(pw_unit_test_ADD_EXECUTABLE_FUNCTION "pw_add_test_executable" CACHE STRING
+ "The name of the CMake function used to instantiate pw_unit_test \
+ executables")
+
+# TODO(ewout): Remove the default.
+set(pw_unit_test_ADD_EXECUTABLE_FUNCTION_FILE
+ "$ENV{PW_ROOT}/targets/host/pw_add_test_executable.cmake" CACHE STRING
+ "The path to the .cmake file that defines \
+ pw_unit_test_ADD_EXECUTABLE_FUNCTION.")
+
+# TODO(ewout): Remove the default to match GN and support Windows.
+set(pw_unit_test_AUTOMATIC_RUNNER "$ENV{PW_ROOT}/targets/host/run_test" CACHE
+ STRING
+ "Path to a test runner to automatically run unit tests after they are \
+ built. \
+ If set, a pw_add_test's {NAME}.run action will invoke the test runner \
+ specified by this variable, passing the path to the unit test to run. If \
+ set to an empty string, the {NAME}.run step will fail to build.")
+
+set(pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT_SECONDS "" CACHE STRING
+ "Optional timeout to apply when running tests via the automatic runner. \
+ Timeout is in seconds. Defaults to empty which means no timeout.")
+
+set(pw_unit_test_AUTOMATIC_RUNNER_ARGS "" CACHE STRING
+ "Optional arguments to forward to the automatic runner")
+
+# pw_add_test: Declares a single unit test suite with Pigweed naming rules and
+# compiler warning options.
+#
+# {NAME} depends on ${NAME}.run if pw_unit_test_AUTOMATIC_RUNNER is set, else
+# it depends on ${NAME}.bin
+# {NAME}.lib contains the provided test sources as a library target, which can
+# then be linked into a test executable.
+# {NAME}.bin is a standalone executable which contains only the test sources
+# specified in the pw_unit_test_template.
+# {NAME}.run which runs the unit test executable after building it if
+# pw_unit_test_AUTOMATIC_RUNNER is set, else it fails to build.
+#
+# Required Arguments:
+#
+# NAME: name to use for the produced test targets specified above
+#
+# Optional Arguments:
+#
+# SOURCES - source files for this library
+# HEADERS - header files for this library
+# PRIVATE_DEPS - private pw_target_link_targets arguments
+# PRIVATE_INCLUDES - public target_include_directories argument
+# PRIVATE_DEFINES - private target_compile_definitions arguments
+# PRIVATE_COMPILE_OPTIONS - private target_compile_options arguments
+# PRIVATE_LINK_OPTIONS - private target_link_options arguments
+#
+# TODO(ewout, hepler): Deprecate the following legacy arguments
+# GROUPS - groups to which to add this test.
+#
+function(pw_add_test NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ MULTI_VALUE_ARGS
+ SOURCES HEADERS PRIVATE_DEPS PRIVATE_INCLUDES
+ PRIVATE_DEFINES PRIVATE_COMPILE_OPTIONS
+ PRIVATE_LINK_OPTIONS GROUPS
+ )
+
+ _pw_check_name_is_relative_to_root("${NAME}" "$ENV{PW_ROOT}"
+ REMAP_PREFIXES
+ third_party pw_third_party
+ )
+
+ pw_add_test_generic(${NAME}
+ SOURCES
+ ${arg_SOURCES}
+ HEADERS
+ ${arg_HEADERS}
+ PRIVATE_DEPS
+ # TODO(b/232141950): Apply compilation options that affect ABI
+ # globally in the CMake build instead of injecting them into libraries.
+ pw_build
+ ${arg_PRIVATE_DEPS}
+ PRIVATE_INCLUDES
+ ${arg_PRIVATE_INCLUDES}
+ PRIVATE_DEFINES
+ ${arg_PRIVATE_DEFINES}
+ PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE
+ pw_build.warnings
+ PRIVATE_COMPILE_OPTIONS
+ ${arg_PRIVATE_COMPILE_OPTIONS}
+ PRIVATE_LINK_OPTIONS
+ ${arg_PRIVATE_LINK_OPTIONS}
+ GROUPS
+ ${arg_GROUPS}
+ )
+endfunction()
+
+# pw_add_test_generic: Declares a single unit test suite.
+#
+# {NAME} depends on ${NAME}.run if pw_unit_test_AUTOMATIC_RUNNER is set, else
+# it depends on ${NAME}.bin
+# {NAME}.lib contains the provided test sources as a library target, which can
+# then be linked into a test executable.
+# {NAME}.bin is a standalone executable which contains only the test sources
+# specified in the pw_unit_test_template.
+# {NAME}.run which runs the unit test executable after building it if
+# pw_unit_test_AUTOMATIC_RUNNER is set, else it fails to build.
+#
+# Required Arguments:
+#
+# NAME: name to use for the produced test targets specified above
+#
+# Optional Arguments:
+#
+# SOURCES - source files for this library
+# HEADERS - header files for this library
+# PRIVATE_DEPS - private pw_target_link_targets arguments
+# PRIVATE_INCLUDES - public target_include_directories argument
+# PRIVATE_DEFINES - private target_compile_definitions arguments
+# PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE - private target_compile_options BEFORE
+# arguments from the specified deps's INTERFACE_COMPILE_OPTIONS. Note that
+# these deps are not pulled in as target_link_libraries. This should not be
+# exposed by the non-generic API.
+# PRIVATE_COMPILE_OPTIONS - private target_compile_options arguments
+# PRIVATE_LINK_OPTIONS - private target_link_options arguments
+#
+# TODO(ewout, hepler): Deprecate the following legacy arguments
+# GROUPS - groups to which to add this test.
+#
+function(pw_add_test_generic NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGS
+ 1
+ MULTI_VALUE_ARGS
+ SOURCES HEADERS PRIVATE_DEPS PRIVATE_INCLUDES
+ PRIVATE_DEFINES
+ PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE PRIVATE_COMPILE_OPTIONS
+ PRIVATE_LINK_OPTIONS GROUPS
+ )
+
+ # Add the library target under "${NAME}.lib".
+ # OBJECT libraries require at least one source file.
+ if("${arg_SOURCES}" STREQUAL "")
+ set(lib_type "INTERFACE")
+ else()
+ set(lib_type "OBJECT")
+ endif()
+ pw_add_library_generic("${NAME}.lib" ${lib_type}
+ SOURCES
+ ${arg_SOURCES}
+ HEADERS
+ ${arg_HEADERS}
+ PRIVATE_DEPS
+ pw_unit_test
+ ${arg_PRIVATE_DEPS}
+ PRIVATE_INCLUDES
+ ${arg_PRIVATE_INCLUDES}
+ PRIVATE_DEFINES
+ ${arg_PRIVATE_DEFINES}
+ PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE
+ ${arg_PRIVATE_COMPILE_OPTIONS_DEPS_BEFORE}
+ PRIVATE_COMPILE_OPTIONS
+ ${arg_PRIVATE_COMPILE_OPTIONS}
+ PRIVATE_LINK_OPTIONS
+ ${arg_PRIVATE_LINK_OPTIONS}
+ )
+
+ # Add the executable target under "${NAME}.bin".
+ if(("${pw_unit_test_ADD_EXECUTABLE_FUNCTION}" STREQUAL "") OR
+ ("${pw_unit_test_ADD_EXECUTABLE_FUNCTION_FILE}" STREQUAL ""))
+ pw_add_error_target("${NAME}.bin"
+ MESSAGE
+ "Attempted to build the ${NAME}.bin without enabling the unit "
+ "test executable function via pw_unit_test_ADD_EXECUTABLE_FUNCTION "
+ "and pw_unit_test_ADD_EXECUTABLE_FUNCTION_FILE. "
+ "See https://pigweed.dev/pw_unit_test for more details."
+ )
+ else()
+ include("${pw_unit_test_ADD_EXECUTABLE_FUNCTION_FILE}")
+ cmake_language(CALL "${pw_unit_test_ADD_EXECUTABLE_FUNCTION}"
+ "${NAME}.bin" "${NAME}.lib")
+ endif()
+
+ # Add the ${NAME} target and optionally the run target under ${NAME}.run.
+ add_custom_target("${NAME}")
+ if("${pw_unit_test_AUTOMATIC_RUNNER}" STREQUAL "")
+ # Test runner is not provided, only build the executable.
+ add_dependencies("${NAME}" "${NAME}.bin")
+
+ pw_add_error_target("${NAME}.run"
+ MESSAGE
+ "Attempted to build ${NAME}.run which is not available because "
+ "pw_unit_test_AUTOMATIC_RUNNER has not been configured. "
+ "See https://pigweed.dev/pw_unit_test."
+ )
+ else() # pw_unit_test_AUTOMATIC_RUNNER is provided.
+ # Define a target for running the test. The target creates a stamp file to
+ # indicate successful test completion. This allows running tests in parallel
+ # with Ninja's full dependency resolution.
+ if(NOT "${pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT_SECONDS}" STREQUAL "")
+ set(optional_timeout_arg
+ "--timeout ${pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT_SECONDS}")
+ endif()
+ if(NOT "${pw_unit_test_AUTOMATIC_RUNNER_ARGS}" STREQUAL "")
+ set(optional_runner_args "-- ${pw_unit_test_AUTOMATIC_RUNNER_ARGS}")
+ endif()
+ add_custom_command(
+ COMMAND
+ python3 -m pw_unit_test.test_runner
+ --runner "${pw_unit_test_AUTOMATIC_RUNNER}"
+ --test "$<TARGET_FILE:${NAME}.bin>"
+ ${optional_timeout_arg}
+ ${optional_runner_args}
+ COMMAND
+ "${CMAKE_COMMAND}" -E touch "${NAME}.stamp"
+ DEPENDS
+ "${NAME}.bin"
+ OUTPUT
+ "${NAME}.stamp"
+ )
+ add_custom_target("${NAME}.run" DEPENDS "${NAME}.stamp")
+ add_dependencies("${NAME}" "${NAME}.run")
+ endif()
+
+ if(arg_GROUPS)
+ pw_add_test_to_groups("${NAME}" ${arg_GROUPS})
+ endif()
+endfunction(pw_add_test_generic)
+
+# pw_add_test_group: Defines a collection of tests or other test groups.
+#
+# Creates the following targets:
+#
+# {NAME} depends on ${NAME}.run if pw_unit_test_AUTOMATIC_RUNNER is set, else
+# it depends on ${NAME}.bin
+# {NAME}.bundle depends on ${NAME}.bundle.run if pw_unit_test_AUTOMATIC_RUNNER
+# is set, else it depends on ${NAME}.bundle.bin
+# {NAME}.lib depends on ${NAME}.bundle.lib.
+# {NAME}.bin depends on the provided TESTS's <test_dep>.bin targets.
+# {NAME}.run depends on the provided TESTS's <test_dep>.run targets if
+# pw_unit_test_AUTOMATIC_RUNNER is set, else it fails to build.
+# {NAME}.bundle.lib contains the provided tests bundled as a library target,
+# which can then be linked into a test executable.
+# {NAME}.bundle.bin standalone executable which contains the bundled tests.
+# {NAME}.bundle.run runs the {NAME}.bundle.bin test bundle executable after
+# building it if pw_unit_test_AUTOMATIC_RUNNER is set, else
+# it fails to build.
+#
+# Required Arguments:
+#
+# NAME - The name of the executable target to be created.
+# TESTS - pw_add_test targets and pw_add_test_group bundles to be included in
+# this test bundle
+#
+function(pw_add_test_group NAME)
+ pw_parse_arguments(
+ NUM_POSITIONAL_ARGMENTS
+ 1
+ MULTI_VALUE_ARGS
+ TESTS
+ REQUIRED_ARGS
+ TESTS
+ )
+
+ set(test_lib_targets "")
+ set(test_bin_targets "")
+ set(test_run_targets "")
+ foreach(test IN LISTS arg_TESTS)
+ list(APPEND test_lib_targets "${test}.lib")
+ list(APPEND test_bin_targets "${test}.bin")
+ list(APPEND test_run_targets "${test}.run")
+ endforeach()
+
+ # This produces ${NAME}.bundle, ${NAME}.bundle.lib, ${NAME}.bundle.bin, and
+ # ${NAME}.bundle.run.
+ pw_add_test("${NAME}.bundle"
+ PRIVATE_DEPS
+ ${test_lib_targets}
+ )
+
+ # Produce ${NAME}.lib.
+ pw_add_library_generic("${NAME}.lib" INTERFACE
+ PUBLIC_DEPS
+ ${NAME}.bundle.lib
+ )
+
+ # Produce ${NAME}.bin.
+ add_custom_target("${NAME}.bin")
+ add_dependencies("${NAME}.bin" ${test_bin_targets})
+
+ # Produce ${NAME} and ${NAME}.run.
+ add_custom_target("${NAME}")
+ if("${pw_unit_test_AUTOMATIC_RUNNER}" STREQUAL "")
+ # Test runner is not provided, only build the executable.
+ add_dependencies("${NAME}" "${NAME}.bin")
+
+ pw_add_error_target("${NAME}.run"
+ MESSAGE
+ "Attempted to build ${NAME}.run which is not available because "
+ "pw_unit_test_AUTOMATIC_RUNNER has not been configured. "
+ "See https://pigweed.dev/pw_unit_test."
+ )
+ else() # pw_unit_test_AUTOMATIC_RUNNER is provided, build and run the test.
+ add_custom_target("${NAME}.run")
+ add_dependencies("${NAME}.run" ${test_run_targets})
+
+ add_dependencies("${NAME}" "${NAME}.run")
+ endif()
+endfunction(pw_add_test_group)
+
+# Adds a test target to the specified test groups. Test groups can be built with
+# the pw_tests_GROUP_NAME target or executed with the pw_run_tests_GROUP_NAME
+# target.
+function(pw_add_test_to_groups TEST_NAME)
+ foreach(group IN LISTS ARGN)
+ if(NOT TARGET "pw_tests.${group}")
+ add_custom_target("pw_tests.${group}")
+ add_custom_target("pw_run_tests.${group}")
+ endif()
+
+ add_dependencies("pw_tests.${group}" "${TEST_NAME}.bin")
+ add_dependencies("pw_run_tests.${group}" "${TEST_NAME}.run")
+ endforeach()
+endfunction(pw_add_test_to_groups)
diff --git a/pw_unit_test/test.gni b/pw_unit_test/test.gni
index 016919414..86399f7e0 100644
--- a/pw_unit_test/test.gni
+++ b/pw_unit_test/test.gni
@@ -16,8 +16,25 @@ import("//build_overrides/pigweed.gni")
import("$dir_pw_build/python_action.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_compilation_testing/negative_compilation_test.gni")
+import("$dir_pw_toolchain/host_clang/toolchains.gni")
declare_args() {
+ # The GoogleTest implementation to use for Pigweed unit tests. This library
+ # provides "gtest/gtest.h" and related headers. Defaults to
+ # pw_unit_test:light, which implements a subset of GoogleTest.
+ #
+ # Type: string (GN path to a source set)
+ # Usage: toolchain-controlled only
+ pw_unit_test_GOOGLETEST_BACKEND = "$dir_pw_unit_test:light"
+
+ # Implementation of a main function for ``pw_test`` unit test binaries. Must
+ # be set to an appropriate target for the pw_unit_test backend.
+ #
+ # Type: string (GN path to a source set)
+ # Usage: toolchain-controlled only
+ pw_unit_test_MAIN = "$dir_pw_unit_test:simple_printing_main"
+
# Path to a test runner to automatically run unit tests after they are built.
#
# If set, a ``pw_test`` target's ``<target_name>.run`` action will invoke the
@@ -43,19 +60,6 @@ declare_args() {
# Timeout is in seconds. Defaults to empty which means no timeout.
pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT = ""
- # Additional dependencies required by all unit test targets. (For example, if
- # using a different test library like Googletest.)
- #
- # Type: list of strings (list of dependencies as GN paths)
- # Usage: toolchain-controlled only
- pw_unit_test_PUBLIC_DEPS = []
-
- # Implementation of a main function for ``pw_test`` unit test binaries.
- #
- # Type: string (GN path to a source set)
- # Usage: toolchain-controlled only
- pw_unit_test_MAIN = "$dir_pw_unit_test:simple_printing_main"
-
# The maximum number of unit tests that may be run concurrently for the
# current toolchain. Setting this to 0 disables usage of a pool, allowing
# unlimited parallelization.
@@ -78,17 +82,38 @@ declare_args() {
# Type: string (GN path to a toolchain)
# Usage: toolchain-controlled only
pw_unit_test_POOL_TOOLCHAIN = ""
+
+ # The name of the GN target type used to build pw_unit_test executables.
+ #
+ # Type: string (name of a GN template)
+ # Usage: toolchain-controlled only
+ pw_unit_test_EXECUTABLE_TARGET_TYPE = "pw_executable"
+
+ # The path to the .gni file that defines pw_unit_test_EXECUTABLE_TARGET_TYPE.
+ #
+ # If pw_unit_test_EXECUTABLE_TARGET_TYPE is not the default of
+ # `pw_executable`, this .gni file is imported to provide the template
+ # definition.
+ #
+ # Type: string (path to a .gni file)
+ # Usage: toolchain-controlled only
+ pw_unit_test_EXECUTABLE_TARGET_TYPE_FILE = ""
+}
+
+if (pw_unit_test_EXECUTABLE_TARGET_TYPE != "pw_executable" &&
+ pw_unit_test_EXECUTABLE_TARGET_TYPE_FILE != "") {
+ import(pw_unit_test_EXECUTABLE_TARGET_TYPE_FILE)
}
# Defines a target if enable_if is true. Otherwise, it defines that target as
# <target_name>.DISABLED and creates an empty <target_name> group. This can be
# used to conditionally create targets without having to conditionally add them
# to groups. This results in simpler BUILD.gn files.
-template("_pw_disableable_target") {
+template("pw_internal_disableable_target") {
assert(defined(invoker.enable_if),
- "`enable_if` is required for _pw_disableable_target")
+ "`enable_if` is required for pw_internal_disableable_target")
assert(defined(invoker.target_type),
- "`target_type` is required for _pw_disableable_target")
+ "`target_type` is required for pw_internal_disableable_target")
if (invoker.enable_if) {
_actual_target_name = target_name
@@ -116,8 +141,10 @@ template("_pw_disableable_target") {
forward_variables_from(invoker,
"*",
[
+ "negative_compilation_tests",
"enable_if",
"target_type",
+ "test_automatic_runner_args",
])
# Remove "" from dependencies. This allows disabling targets if a variable
@@ -133,24 +160,28 @@ template("_pw_disableable_target") {
}
}
-# Creates a library and an executable target for a unit test.
+# Creates a library and an executable target for a unit test with pw_unit_test.
#
# <target_name>.lib contains the provided test sources as a library, which can
# then be linked into a test executable.
# <target_name> is a standalone executable which contains only the test sources
# specified in the pw_unit_test_template.
#
-# If the pw_unit_test_AUTOMATIC_RUNNER variable is set, this template also creates a
-# "${test_name}.run" target which runs the unit test executable after building
-# it.
+# If the pw_unit_test_AUTOMATIC_RUNNER variable is set, this template also
+# creates a "${test_name}.run" target which runs the unit test executable after
+# building it.
#
# Args:
-# - enable_if: (optional) Conditionally enables or disables this test. The test
-# target and *.run target do nothing when the test is disabled. The
+# - enable_if: (optional) Conditionally enables or disables this test. The
+# test target and *.run target do nothing when the test is disabled. The
# disabled test can still be built and run with the
# <target_name>.DISABLED and <target_name>.DISABLED.run targets.
# Defaults to true (enable_if).
+# - extra_metadata: (optional) Extra metadata to include in test group
+# metadata output. This can be used to pass information about this test
+# to later build tasks.
# - All of the regular "executable" target args are accepted.
+#
template("pw_test") {
# This is required in order to reference the pw_test template's target name
# within the test_metadata of the metadata group below. The group() definition
@@ -159,6 +190,12 @@ template("pw_test") {
_test_target_name = target_name
_test_is_enabled = !defined(invoker.enable_if) || invoker.enable_if
+ _is_coverage_run =
+ pw_toolchain_COVERAGE_ENABLED &&
+ filter_include(pw_toolchain_SANITIZERS, [ "coverage" ]) == [ "coverage" ]
+ if (_is_coverage_run) {
+ _profraw_path = "$target_out_dir/test/$_test_target_name.profraw"
+ }
# Always set the output_dir as pigweed is not compatible with shared
# bin directories for tests.
@@ -167,27 +204,105 @@ template("pw_test") {
_test_output_dir = invoker.output_dir
}
+ _extra_metadata = {
+ }
+ if (defined(invoker.extra_metadata)) {
+ _extra_metadata = invoker.extra_metadata
+ }
+
_test_main = pw_unit_test_MAIN
if (defined(invoker.test_main)) {
_test_main = invoker.test_main
}
# The unit test code as a source_set.
- _pw_disableable_target("$target_name.lib") {
+ pw_internal_disableable_target("$target_name.lib") {
target_type = "pw_source_set"
enable_if = _test_is_enabled
- forward_variables_from(invoker, "*", [ "metadata" ])
- if (!defined(public_deps)) {
- public_deps = []
+ # It is possible that the executable target type has been overriden by
+ # pw_unit_test_EXECUTABLE_TARGET_TYPE, which may allow for additional
+ # variables to be specified on the executable template. As such, we cannot
+ # forward all variables ("*") from the invoker to source_set library, as
+ # those additional variables would not be used and GN gen would error.
+ _source_set_relevant_variables = [
+ # GN source_set variables
+ # https://gn.googlesource.com/gn/+/main/docs/reference.md#target-declarations-source_set_declare-a-source-set-target-variables
+ "asmflags",
+ "cflags",
+ "cflags_c",
+ "cflags_cc",
+ "cflags_objc",
+ "cflags_objcc",
+ "defines",
+ "include_dirs",
+ "inputs",
+ "ldflags",
+ "lib_dirs",
+ "libs",
+ "precompiled_header",
+ "precompiled_source",
+ "rustenv",
+ "rustflags",
+ "swiftflags",
+ "testonly",
+ "assert_no_deps",
+ "data_deps",
+ "deps",
+ "public_deps",
+ "runtime_deps",
+ "write_runtime_deps",
+ "all_dependent_configs",
+ "public_configs",
+ "check_includes",
+ "configs",
+ "data",
+ "friend",
+ "inputs",
+ "metadata",
+ "output_extension",
+ "output_name",
+ "public",
+ "sources",
+ "testonly",
+ "visibility",
+
+ # pw_source_set variables
+ # https://pigweed.dev/pw_build/?highlight=pw_executable#target-types
+ "remove_configs",
+ "remove_public_deps",
+ ]
+ forward_variables_from(invoker, _source_set_relevant_variables)
+
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ dir_pw_unit_test ]
+
+ if (defined(invoker.negative_compilation_tests) &&
+ invoker.negative_compilation_tests) {
+ deps += [
+ ":$_test_target_name.nc_test",
+ "$dir_pw_compilation_testing:internal_pigweed_use_only",
+ ]
}
- public_deps += pw_unit_test_PUBLIC_DEPS + [ dir_pw_unit_test ]
}
- _pw_disableable_target(_test_target_name) {
- target_type = "pw_executable"
+ pw_internal_disableable_target(_test_target_name) {
+ target_type = pw_unit_test_EXECUTABLE_TARGET_TYPE
enable_if = _test_is_enabled
+ # Include configs, deps, etc. from the pw_test in the executable as well as
+ # the library to ensure that linker flags propagate to the executable.
+ forward_variables_from(invoker,
+ "*",
+ [
+ "extra_metadata",
+ "metadata",
+ "sources",
+ "public",
+ ])
+
# Metadata for this test when used as part of a pw_test_group target.
metadata = {
tests = [
@@ -195,11 +310,27 @@ template("pw_test") {
type = "test"
test_name = _test_target_name
test_directory = rebase_path(_test_output_dir, root_build_dir)
+ extra_metadata = _extra_metadata
},
]
+
+ # N.B.: This is placed here instead of in $_test_target_name._run because
+ # pw_test_group only forwards the metadata from _test_target_name and not
+ # _test_target_name._run or _test_target_name.run.
+ if (_is_coverage_run) {
+ profraws = [
+ {
+ type = "profraw"
+ path = rebase_path(_profraw_path, root_build_dir)
+ },
+ ]
+ }
}
- deps = [ ":$_test_target_name.lib" ]
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ ":$_test_target_name.lib" ]
if (_test_main != "") {
deps += [ _test_main ]
}
@@ -207,6 +338,20 @@ template("pw_test") {
output_dir = _test_output_dir
}
+ if (defined(invoker.negative_compilation_tests) &&
+ invoker.negative_compilation_tests) {
+ pw_cc_negative_compilation_test("$target_name.nc_test") {
+ forward_variables_from(invoker, "*")
+
+ # Add a dependency on pw_unit_test since it is implied for pw_unit_test
+ # targets.
+ if (!defined(deps)) {
+ deps = []
+ }
+ deps += [ dir_pw_unit_test ]
+ }
+ }
+
if (pw_unit_test_AUTOMATIC_RUNNER != "") {
# When the automatic runner is set, create an action which runs the unit
# test executable using the test runner script.
@@ -216,13 +361,19 @@ template("pw_test") {
# Create a run target for the .DISABLED version of the test.
_test_to_run = _test_target_name + ".DISABLED"
- # Create a placeholder _run target for the regular version of the test.
+ # Create a placeholder .run target for the regular version of the test.
group(_test_target_name + ".run") {
deps = [ ":$_test_target_name" ]
}
}
- pw_python_action(_test_to_run + ".run") {
+ _test_automatic_runner_args = pw_unit_test_AUTOMATIC_RUNNER_ARGS
+ if (defined(invoker.test_automatic_runner_args)) {
+ _test_automatic_runner_args = []
+ _test_automatic_runner_args += invoker.test_automatic_runner_args
+ }
+
+ pw_python_action(_test_to_run + "._run") {
# Optionally limit max test runner concurrency.
if (pw_unit_test_POOL_DEPTH != 0) {
_pool_toolchain = current_toolchain
@@ -235,7 +386,10 @@ template("pw_test") {
deps = [ ":$_test_target_name" ]
inputs = [ pw_unit_test_AUTOMATIC_RUNNER ]
module = "pw_unit_test.test_runner"
- python_deps = [ "$dir_pw_unit_test/py" ]
+ python_deps = [
+ "$dir_pw_cli/py",
+ "$dir_pw_unit_test/py",
+ ]
args = [
"--runner",
rebase_path(pw_unit_test_AUTOMATIC_RUNNER, root_build_dir),
@@ -248,19 +402,30 @@ template("pw_test") {
pw_unit_test_AUTOMATIC_RUNNER_TIMEOUT,
]
}
- if (pw_unit_test_AUTOMATIC_RUNNER_ARGS != []) {
- args += [ "--" ] + pw_unit_test_AUTOMATIC_RUNNER_ARGS
+ if (_is_coverage_run) {
+ args += [
+ "--coverage-profraw",
+ rebase_path(_profraw_path, root_build_dir),
+ ]
+ }
+
+ if (_test_automatic_runner_args != []) {
+ args += [ "--" ] + _test_automatic_runner_args
+ }
+
+ outputs = []
+ if (_is_coverage_run) {
+ outputs += [ _profraw_path ]
}
stamp = true
}
- # TODO(frolv): Alias for the deprecated _run target. Remove when projects
- # are migrated.
- group(_test_to_run + "_run") {
- public_deps = [ ":$_test_to_run.run" ]
+ group(_test_to_run + ".run") {
+ public_deps = [ ":$_test_to_run._run" ]
}
} else {
group(_test_target_name + ".run") {
+ public_deps = [ ":$_test_target_name" ]
}
}
}
@@ -283,6 +448,11 @@ template("pw_test_group") {
_deps = []
}
+ # Allow empty pw_test_groups with no tests or group_deps.
+ if (!defined(invoker.tests) && !defined(invoker.group_deps)) {
+ not_needed("*")
+ }
+
_group_is_enabled = !defined(invoker.enable_if) || invoker.enable_if
if (_group_is_enabled) {
@@ -358,11 +528,6 @@ template("pw_test_group") {
deps += [ "$_dep_target.run($_dep_toolchain)" ]
}
}
-
- # TODO(frolv): Remove this deprecated alias.
- group(_group_target + "_run") {
- deps = [ ":$_group_target.run" ]
- }
}
} else { # _group_is_enabled
# Create empty groups for the tests to avoid pulling in any dependencies.
@@ -374,10 +539,6 @@ template("pw_test_group") {
if (pw_unit_test_AUTOMATIC_RUNNER != "") {
group(_group_target + ".run") {
}
-
- # TODO(frolv): Remove this deprecated alias.
- group(_group_target + "_run") {
- }
}
not_needed("*")
diff --git a/pw_unit_test/unit_test_service.cc b/pw_unit_test/unit_test_service.cc
index 596575586..b3b9530ba 100644
--- a/pw_unit_test/unit_test_service.cc
+++ b/pw_unit_test/unit_test_service.cc
@@ -14,10 +14,10 @@
#include "pw_unit_test/unit_test_service.h"
+#include "gtest/gtest.h"
#include "pw_containers/vector.h"
#include "pw_log/log.h"
#include "pw_protobuf/decoder.h"
-#include "pw_unit_test/framework.h"
namespace pw::unit_test {
@@ -35,12 +35,12 @@ void UnitTestService::Run(ConstByteSpan request, RawServerWriter& writer) {
Status status;
while ((status = decoder.Next()).ok()) {
switch (static_cast<TestRunRequest::Fields>(decoder.FieldNumber())) {
- case TestRunRequest::Fields::REPORT_PASSED_EXPECTATIONS:
+ case TestRunRequest::Fields::kReportPassedExpectations:
decoder.ReadBool(&verbose_)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
break;
- case TestRunRequest::Fields::TEST_SUITE: {
+ case TestRunRequest::Fields::kTestSuite: {
std::string_view suite_name;
if (!decoder.ReadString(&suite_name).ok()) {
break;
@@ -52,7 +52,7 @@ void UnitTestService::Run(ConstByteSpan request, RawServerWriter& writer) {
PW_LOG_ERROR("Maximum of %u test suite filters supported",
static_cast<unsigned>(suites_to_run.max_size()));
writer_.Finish(Status::InvalidArgument())
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return;
}
@@ -63,25 +63,15 @@ void UnitTestService::Run(ConstByteSpan request, RawServerWriter& writer) {
if (status != Status::OutOfRange()) {
writer_.Finish(status)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
return;
}
PW_LOG_INFO("Starting unit test run");
-
- RegisterEventHandler(&handler_);
- SetTestSuitesToRun(suites_to_run);
- PW_LOG_DEBUG("%u test suite filters applied",
- static_cast<unsigned>(suites_to_run.size()));
-
- RUN_ALL_TESTS();
-
- RegisterEventHandler(nullptr);
- SetTestSuitesToRun({});
-
+ handler_.ExecuteTests(suites_to_run);
PW_LOG_INFO("Unit test run complete");
- writer_.Finish().IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ writer_.Finish().IgnoreError(); // TODO(b/242598609): Handle Status properly
}
void UnitTestService::WriteTestRunStart() {
@@ -94,13 +84,13 @@ void UnitTestService::WriteTestRunEnd(const RunTestsSummary& summary) {
WriteEvent([&](Event::StreamEncoder& event) {
TestRunEnd::StreamEncoder test_run_end = event.GetTestRunEndEncoder();
test_run_end.WritePassed(summary.passed_tests)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
test_run_end.WriteFailed(summary.failed_tests)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
test_run_end.WriteSkipped(summary.skipped_tests)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
test_run_end.WriteDisabled(summary.disabled_tests)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
});
}
@@ -109,18 +99,18 @@ void UnitTestService::WriteTestCaseStart(const TestCase& test_case) {
TestCaseDescriptor::StreamEncoder descriptor =
event.GetTestCaseStartEncoder();
descriptor.WriteSuiteName(test_case.suite_name)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
descriptor.WriteTestName(test_case.test_name)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
descriptor.WriteFileName(test_case.file_name)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
});
}
void UnitTestService::WriteTestCaseEnd(TestResult result) {
WriteEvent([&](Event::StreamEncoder& event) {
event.WriteTestCaseEnd(static_cast<TestCaseResult>(result))
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
});
}
@@ -129,11 +119,11 @@ void UnitTestService::WriteTestCaseDisabled(const TestCase& test_case) {
TestCaseDescriptor::StreamEncoder descriptor =
event.GetTestCaseDisabledEncoder();
descriptor.WriteSuiteName(test_case.suite_name)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
descriptor.WriteTestName(test_case.test_name)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
descriptor.WriteFileName(test_case.file_name)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
});
}
@@ -147,14 +137,14 @@ void UnitTestService::WriteTestCaseExpectation(
TestCaseExpectation::StreamEncoder test_case_expectation =
event.GetTestCaseExpectationEncoder();
test_case_expectation.WriteExpression(expectation.expression)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
test_case_expectation
.WriteEvaluatedExpression(expectation.evaluated_expression)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
test_case_expectation.WriteLineNumber(expectation.line_number)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
test_case_expectation.WriteSuccess(expectation.success)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
});
}
diff --git a/pw_varint/Android.bp b/pw_varint/Android.bp
new file mode 100644
index 000000000..aabeb216c
--- /dev/null
+++ b/pw_varint/Android.bp
@@ -0,0 +1,31 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_static {
+ name: "pw_varint",
+ cpp_std: "c++2a",
+ vendor_available: true,
+ export_include_dirs: ["public"],
+ header_libs: [
+ "pw_preprocessor_headers",
+ "pw_polyfill_headers",
+ "pw_span_headers",
+ ],
+ srcs: ["varint.cc"],
+ host_supported: true,
+}
diff --git a/pw_varint/BUILD.gn b/pw_varint/BUILD.gn
index 7edbe7e0a..ae545d651 100644
--- a/pw_varint/BUILD.gn
+++ b/pw_varint/BUILD.gn
@@ -24,7 +24,11 @@ config("default_config") {
pw_source_set("pw_varint") {
public_configs = [ ":default_config" ]
- public_deps = [ dir_pw_preprocessor ]
+ public_deps = [
+ dir_pw_polyfill,
+ dir_pw_preprocessor,
+ dir_pw_span,
+ ]
sources = [ "varint.cc" ]
public = [ "public/pw_varint/varint.h" ]
}
@@ -37,7 +41,10 @@ pw_source_set("stream") {
]
public = [ "public/pw_varint/stream.h" ]
sources = [ "stream.cc" ]
- deps = [ ":pw_varint" ]
+ deps = [
+ ":pw_varint",
+ dir_pw_span,
+ ]
}
pw_test_group("tests") {
@@ -57,6 +64,7 @@ pw_test("varint_test") {
pw_test("stream_test") {
deps = [
+ ":pw_varint",
":stream",
dir_pw_stream,
]
diff --git a/pw_varint/CMakeLists.txt b/pw_varint/CMakeLists.txt
index 2a88f6261..74b495d12 100644
--- a/pw_varint/CMakeLists.txt
+++ b/pw_varint/CMakeLists.txt
@@ -14,15 +14,15 @@
include($ENV{PW_ROOT}/pw_build/pigweed.cmake)
-pw_add_module_library(pw_varint
+pw_add_library(pw_varint STATIC
HEADERS
public/pw_varint/varint.h
PUBLIC_INCLUDES
public
PUBLIC_DEPS
- pw_polyfill.cstddef
- pw_polyfill.span
+ pw_polyfill
pw_preprocessor
+ pw_span
SOURCES
varint.cc
)
@@ -30,7 +30,7 @@ if(Zephyr_FOUND AND CONFIG_PIGWEED_VARINT)
zephyr_link_libraries(pw_varint)
endif()
-pw_add_module_library(pw_varint.stream
+pw_add_library(pw_varint.stream STATIC
HEADERS
public/pw_varint/stream.h
PUBLIC_INCLUDES
@@ -48,7 +48,7 @@ pw_add_test(pw_varint.varint_test
SOURCES
varint_test.cc
varint_test_c.c
- DEPS
+ PRIVATE_DEPS
pw_varint
GROUPS
modules
@@ -58,7 +58,7 @@ pw_add_test(pw_varint.varint_test
pw_add_test(pw_varint.stream_test
SOURCES
stream_test.cc
- DEPS
+ PRIVATE_DEPS
pw_stream
pw_varint.stream
GROUPS
diff --git a/pw_varint/public/pw_varint/stream.h b/pw_varint/public/pw_varint/stream.h
index 7ae356413..c055bc52b 100644
--- a/pw_varint/public/pw_varint/stream.h
+++ b/pw_varint/public/pw_varint/stream.h
@@ -14,6 +14,7 @@
#pragma once
#include <cstdint>
+#include <limits>
#include "pw_status/status_with_size.h"
#include "pw_stream/stream.h"
@@ -26,9 +27,14 @@ namespace varint {
//
// Returns the number of bytes read from the stream if successful, OutOfRange
// if the varint does not fit in a int64_t / uint64_t or if the input is
-// exhausted before the number terminates. Reads a maximum of 10 bytes.
-StatusWithSize Read(stream::Reader& reader, int64_t* output);
-StatusWithSize Read(stream::Reader& reader, uint64_t* output);
+// exhausted before the number terminates. Reads a maximum of 10 bytes or
+// max_size, whichever is smaller.
+StatusWithSize Read(stream::Reader& reader,
+ int64_t* output,
+ size_t max_size = std::numeric_limits<size_t>::max());
+StatusWithSize Read(stream::Reader& reader,
+ uint64_t* output,
+ size_t max_size = std::numeric_limits<size_t>::max());
} // namespace varint
} // namespace pw
diff --git a/pw_varint/public/pw_varint/varint.h b/pw_varint/public/pw_varint/varint.h
index 9d0e0ce49..d53705be0 100644
--- a/pw_varint/public/pw_varint/varint.h
+++ b/pw_varint/public/pw_varint/varint.h
@@ -25,10 +25,10 @@ extern "C" {
// Expose a subset of the varint API for use in C code.
typedef enum {
- PW_VARINT_ZERO_TERMINATED_LEAST_SIGNIFICANT = 0b00,
- PW_VARINT_ZERO_TERMINATED_MOST_SIGNIFICANT = 0b01,
- PW_VARINT_ONE_TERMINATED_LEAST_SIGNIFICANT = 0b10,
- PW_VARINT_ONE_TERMINATED_MOST_SIGNIFICANT = 0b11,
+ PW_VARINT_ZERO_TERMINATED_LEAST_SIGNIFICANT = 0,
+ PW_VARINT_ZERO_TERMINATED_MOST_SIGNIFICANT = 1,
+ PW_VARINT_ONE_TERMINATED_LEAST_SIGNIFICANT = 2,
+ PW_VARINT_ONE_TERMINATED_MOST_SIGNIFICANT = 3,
} pw_varint_Format;
size_t pw_varint_EncodeCustom(uint64_t integer,
@@ -70,10 +70,11 @@ size_t pw_varint_ZigZagEncodedSize(int64_t integer);
} // extern "C"
-#include <span>
+#include <limits>
#include <type_traits>
#include "pw_polyfill/language_feature_macros.h"
+#include "pw_span/span.h"
namespace pw {
namespace varint {
@@ -112,7 +113,7 @@ constexpr std::make_signed_t<T> ZigZagDecode(T n)
// Encodes a uint64_t with Little-Endian Base 128 (LEB128) encoding.
inline size_t EncodeLittleEndianBase128(uint64_t integer,
- const std::span<std::byte>& output) {
+ const span<std::byte>& output) {
return pw_varint_Encode(integer, output.data(), output.size());
}
@@ -127,7 +128,7 @@ inline size_t EncodeLittleEndianBase128(uint64_t integer,
// Returns the number of bytes written or 0 if the result didn't fit in the
// encoding buffer.
template <typename T>
-size_t Encode(T integer, const std::span<std::byte>& output) {
+size_t Encode(T integer, const span<std::byte>& output) {
if (std::is_signed<T>()) {
return pw_varint_ZigZagEncode(integer, output.data(), output.size());
} else {
@@ -155,11 +156,11 @@ size_t Encode(T integer, const std::span<std::byte>& output) {
// data = data.subspan(bytes)
// }
//
-inline size_t Decode(const std::span<const std::byte>& input, int64_t* value) {
+inline size_t Decode(const span<const std::byte>& input, int64_t* value) {
return pw_varint_ZigZagDecode(input.data(), input.size(), value);
}
-inline size_t Decode(const std::span<const std::byte>& input, uint64_t* value) {
+inline size_t Decode(const span<const std::byte>& input, uint64_t* value) {
return pw_varint_Decode(input.data(), input.size(), value);
}
@@ -171,9 +172,7 @@ enum class Format {
};
// Encodes a varint in a custom format.
-inline size_t Encode(uint64_t value,
- std::span<std::byte> output,
- Format format) {
+inline size_t Encode(uint64_t value, span<std::byte> output, Format format) {
return pw_varint_EncodeCustom(value,
output.data(),
output.size(),
@@ -181,7 +180,7 @@ inline size_t Encode(uint64_t value,
}
// Decodes a varint from a custom format.
-inline size_t Decode(std::span<const std::byte> input,
+inline size_t Decode(span<const std::byte> input,
uint64_t* value,
Format format) {
return pw_varint_DecodeCustom(
diff --git a/pw_varint/stream.cc b/pw_varint/stream.cc
index 00fe1fb37..83a80de08 100644
--- a/pw_varint/stream.cc
+++ b/pw_varint/stream.cc
@@ -16,8 +16,8 @@
#include <cstddef>
#include <cstdint>
-#include <span>
+#include "pw_span/span.h"
#include "pw_status/status_with_size.h"
#include "pw_stream/stream.h"
#include "pw_varint/varint.h"
@@ -25,9 +25,9 @@
namespace pw {
namespace varint {
-StatusWithSize Read(stream::Reader& reader, int64_t* output) {
+StatusWithSize Read(stream::Reader& reader, int64_t* output, size_t max_size) {
uint64_t value = 0;
- StatusWithSize count = Read(reader, &value);
+ StatusWithSize count = Read(reader, &value, max_size);
if (!count.ok()) {
return count;
}
@@ -36,18 +36,34 @@ StatusWithSize Read(stream::Reader& reader, int64_t* output) {
return count;
}
-StatusWithSize Read(stream::Reader& reader, uint64_t* output) {
+StatusWithSize Read(stream::Reader& reader, uint64_t* output, size_t max_size) {
uint64_t value = 0;
size_t count = 0;
while (true) {
if (count >= varint::kMaxVarint64SizeBytes) {
- // Varint can't fit a uint64_t.
- return StatusWithSize::OutOfRange();
+ // Varint can't fit a uint64_t, this likely means we're reading binary
+ // data that is not actually a varint.
+ return StatusWithSize::DataLoss();
+ }
+
+ if (count >= max_size) {
+ // Varint didn't fit within the range given; return OutOfRange() if
+ // max_size was 0, but DataLoss if we were reading something we thought
+ // was going to be a varint.
+ return count > 0 ? StatusWithSize::DataLoss()
+ : StatusWithSize::OutOfRange();
}
std::byte b;
- if (auto result = reader.Read(std::span(&b, 1)); !result.ok()) {
+ if (auto result = reader.Read(span(&b, 1)); !result.ok()) {
+ if (count > 0 && result.status().IsOutOfRange()) {
+ // Status::OutOfRange on the first byte means we tried to read a varint
+ // when we reached the end of file. But after the first byte it means we
+ // failed to decode a varint we were in the middle of, and that's not
+ // a normal error condition.
+ return StatusWithSize(Status::DataLoss(), 0);
+ }
return StatusWithSize(result.status(), 0);
}
diff --git a/pw_varint/stream_test.cc b/pw_varint/stream_test.cc
index 6a42f584b..bd7069400 100644
--- a/pw_varint/stream_test.cc
+++ b/pw_varint/stream_test.cc
@@ -22,6 +22,7 @@
#include "gtest/gtest.h"
#include "pw_stream/memory_stream.h"
+#include "pw_varint/varint.h"
namespace pw::varint {
namespace {
@@ -241,4 +242,62 @@ TEST(VarintRead, Unsigned64_MultiByte) {
}
}
+TEST(VarintRead, Errors) {
+ uint64_t value = -1234;
+
+ {
+ std::array<std::byte, 0> buffer{};
+ stream::MemoryReader reader(buffer);
+ const auto sws = Read(reader, &value);
+ EXPECT_FALSE(sws.ok());
+ EXPECT_EQ(sws.status(), Status::OutOfRange());
+ }
+
+ {
+ const auto buffer = MakeBuffer("\xff\xff");
+ stream::MemoryReader reader(buffer);
+ const auto sws = Read(reader, &value);
+ EXPECT_FALSE(sws.ok());
+ EXPECT_EQ(sws.status(), Status::DataLoss());
+ }
+
+ {
+ std::array<std::byte, varint::kMaxVarint64SizeBytes + 1> buffer{};
+ for (auto& b : buffer) {
+ b = std::byte{0xff};
+ }
+
+ stream::MemoryReader reader(buffer);
+ const auto sws = Read(reader, &value);
+ EXPECT_FALSE(sws.ok());
+ EXPECT_EQ(sws.status(), Status::DataLoss());
+ }
+}
+
+TEST(VarintRead, SizeLimit) {
+ uint64_t value = -1234;
+
+ {
+ // buffer contains a valid varint, but we limit the read length to ensure
+ // that the final byte is not read, turning it into an error.
+ const auto buffer = MakeBuffer("\xff\xff\xff\xff\x0f");
+ stream::MemoryReader reader(buffer);
+ const auto sws = Read(reader, &value, 4);
+ EXPECT_FALSE(sws.ok());
+ EXPECT_EQ(sws.status(), Status::DataLoss());
+ EXPECT_EQ(reader.Tell(), 4u);
+ }
+
+ {
+ // If we tell varint::Read() to read zero bytes, it should always return
+ // OutOfRange() without moving the reader.
+ const auto buffer = MakeBuffer("\xff\xff\xff\xff\x0f");
+ stream::MemoryReader reader(buffer);
+ const auto sws = Read(reader, &value, 0);
+ EXPECT_FALSE(sws.ok());
+ EXPECT_EQ(sws.status(), Status::OutOfRange());
+ EXPECT_EQ(reader.Tell(), 0u);
+ }
+}
+
} // namespace pw::varint
diff --git a/pw_varint/varint.cc b/pw_varint/varint.cc
index 0eadab3c2..4ae65cdcc 100644
--- a/pw_varint/varint.cc
+++ b/pw_varint/varint.cc
@@ -15,6 +15,7 @@
#include "pw_varint/varint.h"
#include <algorithm>
+#include <cstddef>
namespace pw {
namespace varint {
diff --git a/pw_varint/varint_test.cc b/pw_varint/varint_test.cc
index a359f849a..dfae0f7a6 100644
--- a/pw_varint/varint_test.cc
+++ b/pw_varint/varint_test.cc
@@ -38,20 +38,23 @@ size_t pw_varint_CallZigZagDecode(void* input,
} // extern "C"
-class Varint : public ::testing::Test {
+class VarintWithBuffer : public ::testing::Test {
protected:
- Varint() : buffer_ {
- std::byte{'a'}, std::byte{'b'}, std::byte{'c'}, std::byte{'d'},
- std::byte{'e'}, std::byte{'f'}, std::byte{'g'}, std::byte{'h'},
- std::byte{'i'}, std::byte {
- 'j'
- }
- }
- {}
+ VarintWithBuffer()
+ : buffer_{std::byte{'a'},
+ std::byte{'b'},
+ std::byte{'c'},
+ std::byte{'d'},
+ std::byte{'e'},
+ std::byte{'f'},
+ std::byte{'g'},
+ std::byte{'h'},
+ std::byte{'i'},
+ std::byte{'j'}} {}
std::byte buffer_[10];
};
-TEST_F(Varint, EncodeSizeUnsigned32_SmallSingleByte) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_SmallSingleByte) {
ASSERT_EQ(1u, Encode(UINT32_C(0), buffer_));
EXPECT_EQ(std::byte{0}, buffer_[0]);
ASSERT_EQ(1u, Encode(UINT32_C(1), buffer_));
@@ -60,7 +63,7 @@ TEST_F(Varint, EncodeSizeUnsigned32_SmallSingleByte) {
EXPECT_EQ(std::byte{2}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeUnsigned32_SmallSingleByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_SmallSingleByte_C) {
ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(0), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{0}, buffer_[0]);
ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(1), buffer_, sizeof(buffer_)));
@@ -69,7 +72,7 @@ TEST_F(Varint, EncodeSizeUnsigned32_SmallSingleByte_C) {
EXPECT_EQ(std::byte{2}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeUnsigned32_LargeSingleByte) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_LargeSingleByte) {
ASSERT_EQ(1u, Encode(UINT32_C(63), buffer_));
EXPECT_EQ(std::byte{63}, buffer_[0]);
ASSERT_EQ(1u, Encode(UINT32_C(64), buffer_));
@@ -80,7 +83,7 @@ TEST_F(Varint, EncodeSizeUnsigned32_LargeSingleByte) {
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeUnsigned32_LargeSingleByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_LargeSingleByte_C) {
ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{63}, buffer_[0]);
ASSERT_EQ(1u, pw_varint_CallEncode(UINT32_C(64), buffer_, sizeof(buffer_)));
@@ -91,7 +94,7 @@ TEST_F(Varint, EncodeSizeUnsigned32_LargeSingleByte_C) {
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeUnsigned32_MultiByte) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_MultiByte) {
ASSERT_EQ(2u, Encode(UINT32_C(128), buffer_));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
ASSERT_EQ(2u, Encode(UINT32_C(129), buffer_));
@@ -104,7 +107,7 @@ TEST_F(Varint, EncodeSizeUnsigned32_MultiByte) {
EXPECT_EQ(std::memcmp("\xff\xff\xff\xff\x0f", buffer_, 5), 0);
}
-TEST_F(Varint, EncodeSizeUnsigned32_MultiByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned32_MultiByte_C) {
ASSERT_EQ(2u, pw_varint_CallEncode(UINT32_C(128), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
ASSERT_EQ(2u, pw_varint_CallEncode(UINT32_C(129), buffer_, sizeof(buffer_)));
@@ -123,7 +126,7 @@ TEST_F(Varint, EncodeSizeUnsigned32_MultiByte_C) {
EXPECT_EQ(std::memcmp("\xff\xff\xff\xff\x0f", buffer_, 5), 0);
}
-TEST_F(Varint, EncodeSizeSigned32_SmallSingleByte) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned32_SmallSingleByte) {
ASSERT_EQ(1u, Encode(INT32_C(0), buffer_));
EXPECT_EQ(std::byte{0}, buffer_[0]);
ASSERT_EQ(1u, Encode(INT32_C(-1), buffer_));
@@ -136,7 +139,7 @@ TEST_F(Varint, EncodeSizeSigned32_SmallSingleByte) {
EXPECT_EQ(std::byte{4}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeSigned32_SmallSingleByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned32_SmallSingleByte_C) {
ASSERT_EQ(1u,
pw_varint_CallZigZagEncode(INT32_C(0), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{0}, buffer_[0]);
@@ -154,7 +157,7 @@ TEST_F(Varint, EncodeSizeSigned32_SmallSingleByte_C) {
EXPECT_EQ(std::byte{4}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeSigned32_LargeSingleByte) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned32_LargeSingleByte) {
ASSERT_EQ(1u, Encode(INT32_C(-63), buffer_));
EXPECT_EQ(std::byte{125}, buffer_[0]);
ASSERT_EQ(1u, Encode(INT32_C(63), buffer_));
@@ -163,7 +166,7 @@ TEST_F(Varint, EncodeSizeSigned32_LargeSingleByte) {
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeSigned32_LargeSingleByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned32_LargeSingleByte_C) {
ASSERT_EQ(1u,
pw_varint_CallZigZagEncode(INT32_C(-63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{125}, buffer_[0]);
@@ -175,7 +178,7 @@ TEST_F(Varint, EncodeSizeSigned32_LargeSingleByte_C) {
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeSigned32_MultiByte) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned32_MultiByte) {
ASSERT_EQ(2u, Encode(INT32_C(64), buffer_));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
ASSERT_EQ(2u, Encode(INT32_C(-65), buffer_));
@@ -190,7 +193,7 @@ TEST_F(Varint, EncodeSizeSigned32_MultiByte) {
EXPECT_EQ(std::memcmp("\xfe\xff\xff\xff\x0f", buffer_, 5), 0);
}
-TEST_F(Varint, EncodeSizeSigned32_MultiByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned32_MultiByte_C) {
ASSERT_EQ(2u,
pw_varint_CallZigZagEncode(INT32_C(64), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
@@ -212,7 +215,7 @@ TEST_F(Varint, EncodeSizeSigned32_MultiByte_C) {
EXPECT_EQ(std::memcmp("\xfe\xff\xff\xff\x0f", buffer_, 5), 0);
}
-TEST_F(Varint, EncodeSizeUnsigned64_SmallSingleByte) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_SmallSingleByte) {
ASSERT_EQ(1u, Encode(UINT64_C(0), buffer_));
EXPECT_EQ(std::byte{0}, buffer_[0]);
ASSERT_EQ(1u, Encode(UINT64_C(1), buffer_));
@@ -221,7 +224,7 @@ TEST_F(Varint, EncodeSizeUnsigned64_SmallSingleByte) {
EXPECT_EQ(std::byte{2}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeUnsigned64_SmallSingleByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_SmallSingleByte_C) {
ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(0), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{0}, buffer_[0]);
ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(1), buffer_, sizeof(buffer_)));
@@ -230,7 +233,7 @@ TEST_F(Varint, EncodeSizeUnsigned64_SmallSingleByte_C) {
EXPECT_EQ(std::byte{2}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeUnsigned64_LargeSingleByte) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_LargeSingleByte) {
ASSERT_EQ(1u, Encode(UINT64_C(63), buffer_));
EXPECT_EQ(std::byte{63}, buffer_[0]);
ASSERT_EQ(1u, Encode(UINT64_C(64), buffer_));
@@ -241,7 +244,7 @@ TEST_F(Varint, EncodeSizeUnsigned64_LargeSingleByte) {
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeUnsigned64_LargeSingleByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_LargeSingleByte_C) {
ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{63}, buffer_[0]);
ASSERT_EQ(1u, pw_varint_CallEncode(UINT64_C(64), buffer_, sizeof(buffer_)));
@@ -252,7 +255,7 @@ TEST_F(Varint, EncodeSizeUnsigned64_LargeSingleByte_C) {
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeUnsigned64_MultiByte) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_MultiByte) {
ASSERT_EQ(2u, Encode(UINT64_C(128), buffer_));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
ASSERT_EQ(2u, Encode(UINT64_C(129), buffer_));
@@ -273,7 +276,7 @@ TEST_F(Varint, EncodeSizeUnsigned64_MultiByte) {
std::memcmp("\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", buffer_, 10), 0);
}
-TEST_F(Varint, EncodeSizeUnsigned64_MultiByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeUnsigned64_MultiByte_C) {
ASSERT_EQ(2u, pw_varint_CallEncode(UINT64_C(128), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
ASSERT_EQ(2u, pw_varint_CallEncode(UINT64_C(129), buffer_, sizeof(buffer_)));
@@ -306,7 +309,7 @@ TEST_F(Varint, EncodeSizeUnsigned64_MultiByte_C) {
std::memcmp("\xff\xff\xff\xff\xff\xff\xff\xff\xff\x01", buffer_, 10), 0);
}
-TEST_F(Varint, EncodeSizeSigned64_SmallSingleByte) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned64_SmallSingleByte) {
ASSERT_EQ(1u, Encode(INT64_C(0), buffer_));
EXPECT_EQ(std::byte{0}, buffer_[0]);
ASSERT_EQ(1u, Encode(INT64_C(-1), buffer_));
@@ -319,7 +322,7 @@ TEST_F(Varint, EncodeSizeSigned64_SmallSingleByte) {
EXPECT_EQ(std::byte{4}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeSigned64_SmallSingleByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned64_SmallSingleByte_C) {
ASSERT_EQ(1u,
pw_varint_CallZigZagEncode(INT64_C(0), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{0}, buffer_[0]);
@@ -337,7 +340,7 @@ TEST_F(Varint, EncodeSizeSigned64_SmallSingleByte_C) {
EXPECT_EQ(std::byte{4}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeSigned64_LargeSingleByte) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned64_LargeSingleByte) {
ASSERT_EQ(1u, Encode(INT64_C(-63), buffer_));
EXPECT_EQ(std::byte{125}, buffer_[0]);
ASSERT_EQ(1u, Encode(INT64_C(63), buffer_));
@@ -346,7 +349,7 @@ TEST_F(Varint, EncodeSizeSigned64_LargeSingleByte) {
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeSigned64_LargeSingleByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned64_LargeSingleByte_C) {
ASSERT_EQ(1u,
pw_varint_CallZigZagEncode(INT64_C(-63), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::byte{125}, buffer_[0]);
@@ -358,7 +361,7 @@ TEST_F(Varint, EncodeSizeSigned64_LargeSingleByte_C) {
EXPECT_EQ(std::byte{127}, buffer_[0]);
}
-TEST_F(Varint, EncodeSizeSigned64_MultiByte) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned64_MultiByte) {
ASSERT_EQ(2u, Encode(INT64_C(64), buffer_));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
ASSERT_EQ(2u, Encode(INT64_C(-65), buffer_));
@@ -385,7 +388,7 @@ TEST_F(Varint, EncodeSizeSigned64_MultiByte) {
std::memcmp("\xfe\xff\xff\xff\xff\xff\xff\xff\xff\x01", buffer_, 10), 0);
}
-TEST_F(Varint, EncodeSizeSigned64_MultiByte_C) {
+TEST_F(VarintWithBuffer, EncodeSizeSigned64_MultiByte_C) {
ASSERT_EQ(2u,
pw_varint_CallZigZagEncode(INT64_C(64), buffer_, sizeof(buffer_)));
EXPECT_EQ(std::memcmp("\x80\x01", buffer_, 2), 0);
@@ -427,7 +430,7 @@ TEST_F(Varint, EncodeSizeSigned64_MultiByte_C) {
// tests. Set the increment to 1 to test every number (this is slow).
constexpr int kIncrement = 100'000'009;
-TEST_F(Varint, EncodeDecodeSigned32) {
+TEST_F(VarintWithBuffer, EncodeDecodeSigned32) {
int32_t i = std::numeric_limits<int32_t>::min();
while (true) {
size_t encoded = Encode(i, buffer_);
@@ -446,7 +449,7 @@ TEST_F(Varint, EncodeDecodeSigned32) {
}
}
-TEST_F(Varint, EncodeDecodeSigned32_C) {
+TEST_F(VarintWithBuffer, EncodeDecodeSigned32_C) {
int32_t i = std::numeric_limits<int32_t>::min();
while (true) {
size_t encoded = pw_varint_CallZigZagEncode(i, buffer_, sizeof(buffer_));
@@ -466,7 +469,7 @@ TEST_F(Varint, EncodeDecodeSigned32_C) {
}
}
-TEST_F(Varint, EncodeDecodeUnsigned32) {
+TEST_F(VarintWithBuffer, EncodeDecodeUnsigned32) {
uint32_t i = 0;
while (true) {
size_t encoded = Encode(i, buffer_);
@@ -485,7 +488,7 @@ TEST_F(Varint, EncodeDecodeUnsigned32) {
}
}
-TEST_F(Varint, EncodeDecodeUnsigned32_C) {
+TEST_F(VarintWithBuffer, EncodeDecodeUnsigned32_C) {
uint32_t i = 0;
while (true) {
size_t encoded = pw_varint_CallEncode(i, buffer_, sizeof(buffer_));
@@ -777,7 +780,7 @@ TEST(Varint, ZigZagEncodeDecode) {
std::numeric_limits<int64_t>::max());
}
-TEST_F(Varint, EncodeWithOptions_SingleByte) {
+TEST_F(VarintWithBuffer, EncodeWithOptions_SingleByte) {
ASSERT_EQ(Encode(0u, buffer_, Format::kZeroTerminatedLeastSignificant), 1u);
EXPECT_EQ(buffer_[0], std::byte{0x00});
@@ -815,7 +818,7 @@ TEST_F(Varint, EncodeWithOptions_SingleByte) {
EXPECT_EQ(buffer_[0], std::byte{0xff});
}
-TEST_F(Varint, EncodeWithOptions_MultiByte) {
+TEST_F(VarintWithBuffer, EncodeWithOptions_MultiByte) {
ASSERT_EQ(Encode(128u, buffer_, Format::kZeroTerminatedLeastSignificant), 2u);
EXPECT_EQ(std::memcmp("\x01\x02", buffer_, 2), 0);
diff --git a/pw_watch/BUILD.gn b/pw_watch/BUILD.gn
index c0c998808..2405d1556 100644
--- a/pw_watch/BUILD.gn
+++ b/pw_watch/BUILD.gn
@@ -15,8 +15,15 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
- inputs = [ "doc_resources/pw_watch_on_device_demo.gif" ]
+ inputs = [
+ "doc_resources/pw_watch_on_device_demo.gif",
+ "doc_resources/pw_watch_test_demo2.gif",
+ ]
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_watch/doc_resources/pw_watch_test_demo2.gif b/pw_watch/doc_resources/pw_watch_test_demo2.gif
new file mode 120000
index 000000000..a3c62134e
--- /dev/null
+++ b/pw_watch/doc_resources/pw_watch_test_demo2.gif
@@ -0,0 +1 @@
+../../docs/images/pw_watch_test_demo2.gif \ No newline at end of file
diff --git a/pw_watch/docs.rst b/pw_watch/docs.rst
index 306bec2b9..8998b3f2b 100644
--- a/pw_watch/docs.rst
+++ b/pw_watch/docs.rst
@@ -1,76 +1,139 @@
.. _module-pw_watch:
---------
+========
pw_watch
+========
+``pw_watch`` is similar to file system watchers found in web development
+tooling. These watchers trigger a web server reload on source file change,
+increasing iteration. In the embedded space, file system watchers are less
+prevalent but no less useful! The Pigweed watcher module makes it easy to
+instantly compile, flash, and run tests upon save.
+
+.. figure:: doc_resources/pw_watch_test_demo2.gif
+ :width: 1420
+ :alt: pw watch running in fullscreen mode and displaying errors
+
+ ``pw watch`` running in fullscreen mode and displaying errors.
+
+--------------------------
+``pw watch`` Command Usage
+--------------------------
+The simplest way to get started with ``pw_watch`` is to launch it from a shell
+using the Pigweed environment as ``pw watch``. By default, ``pw_watch`` watches
+for repository changes and triggers the default Ninja build target at out/. To
+override this behavior, provide the ``-C`` argument to ``pw watch``.
+
+.. argparse::
+ :module: pw_watch.watch
+ :func: get_parser
+ :prog: pw watch
+ :nodefaultconst:
+ :nodescription:
+ :noepilog:
+
+--------
+Examples
--------
-``pw_watch`` is similar to file system watchers found in the web development
-space. These watchers trigger a web server reload on source change, increasing
-iteration. In the embedded space, file system watchers are less prevalent but no
-less useful! The Pigweed watcher module makes it easy to instantly compile,
-flash, and run tests upon save.
-.. image:: doc_resources/pw_watch_on_device_demo.gif
+Ninja
+=====
+Build the default target in ``out/`` using ``ninja``.
-.. note::
+.. code-block:: sh
- ``pw_watch`` currently only works with Pigweed's GN and CMake builds.
+ pw watch -C out
-Module Usage
-============
-The simplest way to get started with ``pw_watch`` is to launch it from a shell
-using the Pigweed environment as ``pw watch``. By default, ``pw_watch`` watches
-for repository changes and triggers the default Ninja build target at out/. To
-override this behavior, provide the ``-C`` argument to ``pw watch``.
+Build ``python.lint`` and ``stm32f429i`` targets in ``out/`` using ``ninja``.
+
+.. code-block:: sh
+
+ pw watch python.lint stm32f429i
+
+Build the ``pw_run_tests.modules`` target in the ``out/cmake/`` directory
+
+.. code-block:: sh
+
+ pw watch -C out/cmake pw_run_tests.modules
-.. code:: sh
+Build the default target in ``out/`` and ``pw_apps`` in ``out/cmake/``
- # Use ./out/ as the build directory and build the default target
- pw watch
+.. code-block:: sh
- # Use ./out/ as the build directory and build the stm32f429i target
- pw watch python.lint stm32f429i
+ pw watch -C out -C out/cmake pw_apps
- # Build pw_run_tests.modules in the out/cmake directory
- pw watch -C out/cmake pw_run_tests.modules
+Build ``python.tests`` in ``out/`` and ``pw_apps`` in ``out/cmake/``
- # Build the default target in out/ and pw_apps in out/cmake
- pw watch -C out -C out/cmake pw_apps
+.. code-block:: sh
- # Build python.tests in out/ and build pw_apps in out/cmake
- pw watch python.tests -C out/cmake pw_apps
+ pw watch python.tests -C out/cmake pw_apps
- # Build the default target, but only run up to 8 jobs in parallel.
- pw watch -j8
+Bazel
+=====
+Run ``bazel build`` followed by ``bazel test`` on the target ``//...`` using the
+``out-bazel/`` build directory.
- # Build the default target and start a docs server on http://127.0.0.1:8000
- pw watch --serve-docs
+.. code-block:: sh
- # Build the default target and start a docs server on http://127.0.0.1:5555
- pw watch --serve-docs --serve-docs-port=5555
+ pw watch --run-command 'mkdir -p out-bazel' \
+ -C out-bazel '//...' \
+ --build-system-command out-bazel 'bazel build' \
+ --build-system-command out-bazel 'bazel test'
- # Build with a full screen terminal user interface similar to pw_console.
- pw watch --fullscreen
+Log Files
+=========
+Run two separate builds simultaneously and stream the output to separate build
+log files. These log files are created:
+- ``out/build.txt``: This will contain overall status messages and any sub build
+ errors.
+- ``out/build_out.txt``: Sub-build log only output for the ``out`` build
+ directory:
+- ``out/build_outbazel.txt``: Sub-build log only output for the ``outbazel``
+ build directory.
+
+.. code-block:: sh
+
+ pw watch \
+ --parallel \
+ --logfile out/build.txt \
+ --separate-logfiles \
+ -C out default \
+ -C outbazel '//...:all' \
+ --build-system-command outbazel 'bazel build' \
+ --build-system-command outbazel 'bazel test'
+
+Including and Ignoring Files
+============================
``pw watch`` only rebuilds when a file that is not ignored by Git changes.
Adding exclusions to a ``.gitignore`` causes watch to ignore them, even if the
files were forcibly added to a repo. By default, only files matching certain
extensions are applied, even if they're tracked by Git. The ``--patterns`` and
-``--ignore_patterns`` arguments can be used to include or exclude specific
+``--ignore-patterns`` arguments can be used to include or exclude specific
patterns. These patterns do not override Git's ignoring logic.
-The ``--exclude_list`` argument can be used to exclude directories from being
+The ``--exclude-list`` argument can be used to exclude directories from being
watched. This decreases the number of files monitored with inotify in Linux.
-By default, ``pw watch`` automatically restarts an ongoing build when files
+Documentation Output
+====================
+When using ``--serve-docs``, by default the docs will be rebuilt when changed,
+just like code files. However, you will need to manually reload the page in
+your browser to see changes. If you install the ``httpwatcher`` Python package
+into your Pigweed environment (``pip install httpwatcher``), docs pages will
+automatically reload when changed.
+
+Disable Automatic Rebuilds
+==========================
+``pw watch`` automatically restarts an ongoing build when files
change. This can be disabled with the ``--no-restart`` option. While running
``pw watch``, you may also press enter to immediately restart a build.
-See ``pw watch -h`` for the full list of command line arguments.
-
+---------------------
Unit Test Integration
-=====================
+---------------------
Thanks to GN's understanding of the full dependency tree, only the tests
affected by a file change are run when ``pw_watch`` triggers a build. By
default, host builds using ``pw_watch`` will run unit tests. To run unit tests
on a device as part of ``pw_watch``, refer to your device's
:ref:`target documentation<docs-targets>`.
+
diff --git a/pw_watch/py/BUILD.gn b/pw_watch/py/BUILD.gn
index 2197983ab..31576d090 100644
--- a/pw_watch/py/BUILD.gn
+++ b/pw_watch/py/BUILD.gn
@@ -24,11 +24,17 @@ pw_python_package("py") {
]
sources = [
"pw_watch/__init__.py",
+ "pw_watch/argparser.py",
"pw_watch/debounce.py",
"pw_watch/watch.py",
"pw_watch/watch_app.py",
]
tests = [ "watch_test.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
- python_deps = [ "$dir_pw_cli/py" ]
+ mypy_ini = "$dir_pigweed/.mypy.ini"
+ python_deps = [
+ "$dir_pw_build/py",
+ "$dir_pw_cli/py",
+ "$dir_pw_console/py",
+ ]
}
diff --git a/pw_watch/py/pw_watch/argparser.py b/pw_watch/py/pw_watch/argparser.py
new file mode 100644
index 000000000..77974ece4
--- /dev/null
+++ b/pw_watch/py/pw_watch/argparser.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Pigweed Watch argparser."""
+
+import argparse
+from pathlib import Path
+
+from pw_build.project_builder_argparse import add_project_builder_arguments
+
+WATCH_PATTERN_DELIMITER = ','
+
+WATCH_PATTERNS = (
+ '*.bloaty',
+ '*.c',
+ '*.cc',
+ '*.css',
+ '*.cpp',
+ '*.cmake',
+ 'CMakeLists.txt',
+ '*.dts',
+ '*.dtsi',
+ '*.gn',
+ '*.gni',
+ '*.go',
+ '*.h',
+ '*.hpp',
+ '*.ld',
+ '*.md',
+ '*.options',
+ '*.proto',
+ '*.py',
+ '*.rs',
+ '*.rst',
+ '*.s',
+ '*.S',
+ '*.toml',
+)
+
+
+def add_parser_arguments(
+ parser: argparse.ArgumentParser,
+) -> argparse.ArgumentParser:
+ """Sets up an argument parser for pw watch."""
+ parser = add_project_builder_arguments(parser)
+
+ watch_group = parser.add_argument_group(title='Watch Options')
+
+ watch_group.add_argument(
+ '--patterns',
+ help=('Comma delimited list of globs to watch to trigger recompile.'),
+ default=WATCH_PATTERN_DELIMITER.join(WATCH_PATTERNS),
+ )
+
+ watch_group.add_argument(
+ '--ignore-patterns',
+ dest='ignore_patterns_string',
+ help=('Comma delimited list of globs to ignore events from.'),
+ )
+
+ watch_group.add_argument(
+ '--exclude-list',
+ nargs='+',
+ type=Path,
+ help=(
+ 'Directories to ignore during pw watch. This option may be '
+ 'repeated. Directories are passed as separate arguments.'
+ ),
+ default=[],
+ )
+
+ watch_group.add_argument(
+ '--no-restart',
+ dest='restart',
+ action='store_false',
+ help='do not restart ongoing builds if files change',
+ )
+
+ watch_group.add_argument(
+ '--serve-docs',
+ dest='serve_docs',
+ action='store_true',
+ default=False,
+ help='Start a webserver for docs on localhost. The port for this '
+ ' webserver can be set with the --serve-docs-port option. '
+ ' Defaults to http://127.0.0.1:8000',
+ )
+
+ watch_group.add_argument(
+ '--serve-docs-port',
+ dest='serve_docs_port',
+ type=int,
+ default=8000,
+ help='Set the port for the docs webserver. Default: 8000.',
+ )
+
+ watch_group.add_argument(
+ '--serve-docs-path',
+ dest='serve_docs_path',
+ type=Path,
+ default='docs/gen/docs',
+ help='Set the path for the docs to serve. Default: docs/gen/docs'
+ ' in the build directory.',
+ )
+
+ watch_group.add_argument(
+ '-f',
+ '--fullscreen',
+ action='store_true',
+ help='Use a fullscreen interface.',
+ )
+
+ return parser
diff --git a/pw_watch/py/pw_watch/debounce.py b/pw_watch/py/pw_watch/debounce.py
index 3417b1e67..ce09063b0 100644
--- a/pw_watch/py/pw_watch/debounce.py
+++ b/pw_watch/py/pw_watch/debounce.py
@@ -18,11 +18,16 @@ import logging
import threading
from abc import ABC, abstractmethod
-_LOG = logging.getLogger('pw_watch')
+from pw_build.project_builder_context import get_project_builder_context
+
+BUILDER_CONTEXT = get_project_builder_context()
+
+_LOG = logging.getLogger('pw_build.watch')
class DebouncedFunction(ABC):
"""Function to be run by Debouncer"""
+
@abstractmethod
def run(self) -> None:
"""Run the function"""
@@ -50,13 +55,14 @@ class State(enum.Enum):
IDLE = 1 # ------- Transistions to: DEBOUNCING
DEBOUNCING = 2 # - Transistions to: RUNNING
RUNNING = 3 # ---- Transistions to: INTERRUPTED or COOLDOWN
- INTERRUPTED = 4 #- Transistions to: RERUN
- COOLDOWN = 5 #---- Transistions to: IDLE
- RERUN = 6 #------- Transistions to: IDLE (but triggers a press)
+ INTERRUPTED = 4 # - Transistions to: RERUN
+ COOLDOWN = 5 # ---- Transistions to: IDLE
+ RERUN = 6 # ------- Transistions to: IDLE (but triggers a press)
class Debouncer:
"""Run an interruptable, cancellable function with debouncing"""
+
def __init__(self, function: DebouncedFunction) -> None:
super().__init__()
self.function = function
@@ -95,14 +101,17 @@ class Debouncer:
# When the function is already running but we get an incoming
# event, go into the INTERRUPTED state to signal that we should
# re-try running afterwards.
-
- # Push an empty line to flush ongoing I/O in subprocess.
- _LOG.error('')
-
- # Surround the error message with newlines to make it stand out.
- _LOG.error('')
- _LOG.error('Event while running: %s', event_description)
- _LOG.error('')
+ error_message = ['Event while running: %s', event_description]
+ if BUILDER_CONTEXT.using_progress_bars():
+ _LOG.warning(*error_message)
+ else:
+ # Push an empty line to flush ongoing I/O in subprocess.
+ print('')
+
+ # Surround the error message with newlines to make it stand out.
+ print('')
+ _LOG.warning(*error_message)
+ print('')
self.function.cancel()
self._transition(State.INTERRUPTED)
@@ -126,8 +135,9 @@ class Debouncer:
assert self.lock.locked()
if self.state == State.DEBOUNCING:
self.debounce_timer.cancel()
- self.debounce_timer = threading.Timer(self.debounce_seconds,
- self._run_function)
+ self.debounce_timer = threading.Timer(
+ self.debounce_seconds, self._run_function
+ )
self.debounce_timer.start()
# Called from debounce_timer thread.
@@ -159,8 +169,9 @@ class Debouncer:
def _start_cooldown_timer(self):
assert self.lock.locked()
- self.cooldown_timer = threading.Timer(self.cooldown_seconds,
- self._exit_cooldown)
+ self.cooldown_timer = threading.Timer(
+ self.cooldown_seconds, self._exit_cooldown
+ )
self.cooldown_timer.start()
# Called from cooldown_timer thread.
@@ -168,13 +179,14 @@ class Debouncer:
try:
with self.lock:
self.cooldown_timer = None
- rerun = (self.state == State.RERUN)
+ rerun = self.state == State.RERUN
self._transition(State.IDLE)
# If we were in the RERUN state, then re-trigger the event.
if rerun:
- self._press_unlocked('Rerunning: ' +
- self.rerun_event_description)
+ self._press_unlocked(
+ 'Rerunning: ' + self.rerun_event_description
+ )
# Ctrl-C on Unix generates KeyboardInterrupt
# Ctrl-Z on Windows generates EOFError
diff --git a/pw_watch/py/pw_watch/watch.py b/pw_watch/py/pw_watch/watch.py
index fe495efba..d956df3ec 100755
--- a/pw_watch/py/pw_watch/watch.py
+++ b/pw_watch/py/pw_watch/watch.py
@@ -14,15 +14,15 @@
# the License.
"""Watch files for changes and rebuild.
-pw watch runs Ninja in a build directory when source files change. It works with
-any Ninja project (GN or CMake).
+Run arbitrary commands or invoke build systems (Ninja, Bazel and make) on one or
+more build directories whenever source files change.
-Usage examples:
+Examples:
- # Find a build directory and build the default target
- pw watch
+ # Build the default target in out/ using ninja.
+ pw watch -C out
- # Find a build directory and build the stm32f429i target
+ # Build python.lint and stm32f429i targets in out/ using ninja.
pw watch python.lint stm32f429i
# Build pw_run_tests.modules in the out/cmake directory
@@ -31,41 +31,60 @@ Usage examples:
# Build the default target in out/ and pw_apps in out/cmake
pw watch -C out -C out/cmake pw_apps
- # Find a directory and build python.tests, and build pw_apps in out/cmake
+ # Build python.tests in out/ and pw_apps in out/cmake/
pw watch python.tests -C out/cmake pw_apps
+
+ # Run 'bazel build' and 'bazel test' on the target '//...' in outbazel/
+ pw watch --run-command 'mkdir -p outbazel'
+ -C outbazel '//...'
+ --build-system-command outbazel 'bazel build'
+ --build-system-command outbazel 'bazel test'
"""
import argparse
-from dataclasses import dataclass
+import concurrent.futures
import errno
-from itertools import zip_longest
+import http.server
import logging
import os
from pathlib import Path
import re
-import shlex
import subprocess
+import socketserver
import sys
import threading
from threading import Thread
from typing import (
+ Callable,
Iterable,
List,
- NamedTuple,
NoReturn,
Optional,
Sequence,
Tuple,
)
-import httpwatcher # type: ignore
+try:
+ import httpwatcher # type: ignore[import]
+except ImportError:
+ httpwatcher = None
from watchdog.events import FileSystemEventHandler # type: ignore[import]
from watchdog.observers import Observer # type: ignore[import]
-from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
-from prompt_toolkit.formatted_text import StyleAndTextTuples
-
+from prompt_toolkit import prompt
+
+from pw_build.build_recipe import BuildRecipe, create_build_recipes
+from pw_build.project_builder import (
+ ProjectBuilder,
+ execute_command_no_logging,
+ execute_command_with_logging,
+ log_build_recipe_start,
+ log_build_recipe_finish,
+ ASCII_CHARSET,
+ EMOJI_CHARSET,
+)
+from pw_build.project_builder_context import get_project_builder_context
import pw_cli.branding
import pw_cli.color
import pw_cli.env
@@ -73,12 +92,16 @@ import pw_cli.log
import pw_cli.plugins
import pw_console.python_logging
-from pw_watch.watch_app import WatchApp
+from pw_watch.argparser import (
+ WATCH_PATTERN_DELIMITER,
+ WATCH_PATTERNS,
+ add_parser_arguments,
+)
from pw_watch.debounce import DebouncedFunction, Debouncer
+from pw_watch.watch_app import WatchAppPrefs, WatchApp
_COLOR = pw_cli.color.colors()
-_LOG = logging.getLogger('pw_watch')
-_NINJA_LOG = logging.getLogger('pw_watch_ninja_output')
+_LOG = logging.getLogger('pw_build.watch')
_ERRNO_INOTIFY_LIMIT_REACHED = 28
# Suppress events under 'fsevents', generated by watchdog on every file
@@ -87,59 +110,9 @@ _ERRNO_INOTIFY_LIMIT_REACHED = 28
_FSEVENTS_LOG = logging.getLogger('fsevents')
_FSEVENTS_LOG.setLevel(logging.WARNING)
-_PASS_MESSAGE = """
- ██████╗ █████╗ ███████╗███████╗██╗
- ██╔══██╗██╔══██╗██╔════╝██╔════╝██║
- ██████╔╝███████║███████╗███████╗██║
- ██╔═══╝ ██╔══██║╚════██║╚════██║╚═╝
- ██║ ██║ ██║███████║███████║██╗
- ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝
-"""
-
-# Pick a visually-distinct font from "PASS" to ensure that readers can't
-# possibly mistake the difference between the two states.
-_FAIL_MESSAGE = """
- ▄██████▒░▄▄▄ ██▓ ░██▓
- ▓█▓ ░▒████▄ ▓██▒ ░▓██▒
- ▒████▒ ░▒█▀ ▀█▄ ▒██▒ ▒██░
- ░▓█▒ ░░██▄▄▄▄██ ░██░ ▒██░
- ░▒█░ ▓█ ▓██▒░██░░ ████████▒
- ▒█░ ▒▒ ▓▒█░░▓ ░ ▒░▓ ░
- ░▒ ▒ ▒▒ ░ ▒ ░░ ░ ▒ ░
- ░ ░ ░ ▒ ▒ ░ ░ ░
- ░ ░ ░ ░ ░
-"""
-
_FULLSCREEN_STATUS_COLUMN_WIDTH = 10
-
-# TODO(keir): Figure out a better strategy for exiting. The problem with the
-# watcher is that doing a "clean exit" is slow. However, by directly exiting,
-# we remove the possibility of the wrapper script doing anything on exit.
-def _die(*args) -> NoReturn:
- _LOG.critical(*args)
- sys.exit(1)
-
-
-class WatchCharset(NamedTuple):
- slug_ok: str
- slug_fail: str
-
-
-_ASCII_CHARSET = WatchCharset(_COLOR.green('OK '), _COLOR.red('FAIL'))
-_EMOJI_CHARSET = WatchCharset('✔️ ', '💥')
-
-
-@dataclass(frozen=True)
-class BuildCommand:
- build_dir: Path
- targets: Tuple[str, ...] = ()
-
- def args(self) -> Tuple[str, ...]:
- return (str(self.build_dir), *self.targets)
-
- def __str__(self) -> str:
- return ' '.join(shlex.quote(arg) for arg in self.args())
+BUILDER_CONTEXT = get_project_builder_context()
def git_ignored(file: Path) -> bool:
@@ -157,7 +130,8 @@ def git_ignored(file: Path) -> bool:
['git', 'check-ignore', '--quiet', '--no-index', file],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
- cwd=directory).returncode
+ cwd=directory,
+ ).returncode
return returncode in (0, 128)
except FileNotFoundError:
# If the directory no longer exists, try parent directories until
@@ -172,77 +146,82 @@ def git_ignored(file: Path) -> bool:
class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction):
"""Process filesystem events and launch builds if necessary."""
+
# pylint: disable=too-many-instance-attributes
NINJA_BUILD_STEP = re.compile(
- r'^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$')
+ r'^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$'
+ )
- def __init__(
+ def __init__( # pylint: disable=too-many-arguments
self,
- build_commands: Sequence[BuildCommand],
+ project_builder: ProjectBuilder,
patterns: Sequence[str] = (),
ignore_patterns: Sequence[str] = (),
- charset: WatchCharset = _ASCII_CHARSET,
restart: bool = True,
- jobs: int = None,
fullscreen: bool = False,
banners: bool = True,
+ use_logfile: bool = False,
+ separate_logfiles: bool = False,
+ parallel_workers: int = 1,
):
super().__init__()
self.banners = banners
- self.status_message: Optional[OneStyleAndTextTuple] = None
- self.result_message: Optional[StyleAndTextTuples] = None
- self.current_stdout = ''
self.current_build_step = ''
self.current_build_percent = 0.0
self.current_build_errors = 0
self.patterns = patterns
self.ignore_patterns = ignore_patterns
- self.build_commands = build_commands
- self.charset: WatchCharset = charset
+ self.project_builder = project_builder
+ self.parallel_workers = parallel_workers
self.restart_on_changes = restart
self.fullscreen_enabled = fullscreen
self.watch_app: Optional[WatchApp] = None
- self._current_build: subprocess.Popen
- self._extra_ninja_args = [] if jobs is None else [f'-j{jobs}']
+ self.use_logfile = use_logfile
+ self.separate_logfiles = separate_logfiles
+ if self.parallel_workers > 1:
+ self.separate_logfiles = True
self.debouncer = Debouncer(self)
# Track state of a build. These need to be members instead of locals
# due to the split between dispatch(), run(), and on_complete().
self.matching_path: Optional[Path] = None
- self.builds_succeeded: List[bool] = []
- if not self.fullscreen_enabled:
+ if (
+ not self.fullscreen_enabled
+ and not self.project_builder.should_use_progress_bars()
+ ):
self.wait_for_keypress_thread = threading.Thread(
- None, self._wait_for_enter)
+ None, self._wait_for_enter
+ )
self.wait_for_keypress_thread.start()
+ if self.fullscreen_enabled:
+ BUILDER_CONTEXT.using_fullscreen = True
+
def rebuild(self):
- """ Rebuild command triggered from watch app."""
- self._current_build.terminate()
- self._current_build.wait()
+ """Rebuild command triggered from watch app."""
self.debouncer.press('Manual build requested')
- def _wait_for_enter(self) -> NoReturn:
+ def _wait_for_enter(self) -> None:
try:
while True:
- _ = input()
- self._current_build.terminate()
- self._current_build.wait()
-
- self.debouncer.press('Manual build requested...')
+ _ = prompt('')
+ self.rebuild()
# Ctrl-C on Unix generates KeyboardInterrupt
# Ctrl-Z on Windows generates EOFError
except (KeyboardInterrupt, EOFError):
+ # Force stop any running ninja builds.
_exit_due_to_interrupt()
def _path_matches(self, path: Path) -> bool:
"""Returns true if path matches according to the watcher patterns"""
- return (not any(path.match(x) for x in self.ignore_patterns)
- and any(path.match(x) for x in self.patterns))
+ return not any(path.match(x) for x in self.ignore_patterns) and any(
+ path.match(x) for x in self.patterns
+ )
def dispatch(self, event) -> None:
# There isn't any point in triggering builds on new directory creation.
@@ -273,15 +252,19 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction):
log_message = f'File change detected: {os.path.relpath(matching_path)}'
if self.restart_on_changes:
if self.fullscreen_enabled and self.watch_app:
- self.watch_app.rebuild_on_filechange()
+ self.watch_app.clear_log_panes()
self.debouncer.press(f'{log_message} Triggering build...')
else:
_LOG.info('%s ; not rebuilding', log_message)
def _clear_screen(self) -> None:
- if not self.fullscreen_enabled:
- print('\033c', end='') # TODO(pwbug/38): Not Windows compatible.
- sys.stdout.flush()
+ if self.fullscreen_enabled:
+ return
+ if self.project_builder.should_use_progress_bars():
+ BUILDER_CONTEXT.clear_progress_scrollback()
+ return
+ print('\033c', end='') # TODO(pwbug/38): Not Windows compatible.
+ sys.stdout.flush()
# Implementation of DebouncedFunction.run()
#
@@ -289,353 +272,211 @@ class PigweedBuildWatcher(FileSystemEventHandler, DebouncedFunction):
# than on the main thread that's watching file events. This enables the
# watcher to continue receiving file change events during a build.
def run(self) -> None:
- """Run all the builds in serial and capture pass/fail for each."""
+ """Run all the builds and capture pass/fail for each."""
# Clear the screen and show a banner indicating the build is starting.
self._clear_screen()
+ if self.banners:
+ for line in pw_cli.branding.banner().splitlines():
+ _LOG.info(line)
if self.fullscreen_enabled:
- self.create_result_message()
_LOG.info(
- _COLOR.green(
- 'Watching for changes. Ctrl-d to exit; enter to rebuild'))
+ self.project_builder.color.green(
+ 'Watching for changes. Ctrl-d to exit; enter to rebuild'
+ )
+ )
else:
- for line in pw_cli.branding.banner().splitlines():
- _LOG.info(line)
_LOG.info(
- _COLOR.green(
- ' Watching for changes. Ctrl-C to exit; enter to rebuild')
+ self.project_builder.color.green(
+ 'Watching for changes. Ctrl-C to exit; enter to rebuild'
+ )
)
- _LOG.info('')
- _LOG.info('Change detected: %s', self.matching_path)
+ if self.matching_path:
+ _LOG.info('')
+ _LOG.info('Change detected: %s', self.matching_path)
- self._clear_screen()
-
- self.builds_succeeded = []
- num_builds = len(self.build_commands)
+ num_builds = len(self.project_builder)
_LOG.info('Starting build with %d directories', num_builds)
- env = os.environ.copy()
- # Force colors in Pigweed subcommands run through the watcher.
- env['PW_USE_COLOR'] = '1'
- # Force Ninja to output ANSI colors
- env['CLICOLOR_FORCE'] = '1'
-
- for i, cmd in enumerate(self.build_commands, 1):
- index = f'[{i}/{num_builds}]'
- self.builds_succeeded.append(self._run_build(index, cmd, env))
- if self.builds_succeeded[-1]:
- level = logging.INFO
- tag = '(OK)'
- else:
- level = logging.ERROR
- tag = '(FAIL)'
-
- _LOG.log(level, '%s Finished build: %s %s', index, cmd, tag)
- self.create_result_message()
+ if self.project_builder.default_logfile:
+ _LOG.info(
+ '%s %s',
+ self.project_builder.color.blue('Root logfile:'),
+ self.project_builder.default_logfile.resolve(),
+ )
- def create_result_message(self):
- if not self.fullscreen_enabled:
+ env = os.environ.copy()
+ if self.project_builder.colors:
+ # Force colors in Pigweed subcommands run through the watcher.
+ env['PW_USE_COLOR'] = '1'
+ # Force Ninja to output ANSI colors
+ env['CLICOLOR_FORCE'] = '1'
+
+ # Reset status
+ BUILDER_CONTEXT.set_project_builder(self.project_builder)
+ BUILDER_CONTEXT.set_enter_callback(self.rebuild)
+ BUILDER_CONTEXT.set_building()
+
+ for cfg in self.project_builder:
+ cfg.reset_status()
+
+ with concurrent.futures.ThreadPoolExecutor(
+ max_workers=self.parallel_workers
+ ) as executor:
+ futures = []
+ if (
+ not self.fullscreen_enabled
+ and self.project_builder.should_use_progress_bars()
+ ):
+ BUILDER_CONTEXT.add_progress_bars()
+
+ for i, cfg in enumerate(self.project_builder, start=1):
+ futures.append(executor.submit(self.run_recipe, i, cfg, env))
+
+ for future in concurrent.futures.as_completed(futures):
+ future.result()
+
+ BUILDER_CONTEXT.set_idle()
+
+ def run_recipe(self, index: int, cfg: BuildRecipe, env) -> None:
+ if BUILDER_CONTEXT.interrupted():
+ return
+ if not cfg.enabled:
return
- self.result_message = []
- first_building_target_found = False
- for (succeeded, command) in zip_longest(self.builds_succeeded,
- self.build_commands):
- if succeeded:
- self.result_message.append(
- ('class:theme-fg-green',
- 'OK'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH)))
- elif succeeded is None and not first_building_target_found:
- first_building_target_found = True
- self.result_message.append(
- ('class:theme-fg-yellow',
- 'Building'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH)))
- elif first_building_target_found:
- self.result_message.append(
- ('', ''.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH)))
- else:
- self.result_message.append(
- ('class:theme-fg-red',
- 'Failed'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH)))
- self.result_message.append(('', f' {command}\n'))
-
- def _run_build(self, index: str, cmd: BuildCommand, env: dict) -> bool:
- # Make sure there is a build.ninja file for Ninja to use.
- build_ninja = cmd.build_dir / 'build.ninja'
- if not build_ninja.exists():
- # If this is a CMake directory, prompt the user to re-run CMake.
- if cmd.build_dir.joinpath('CMakeCache.txt').exists():
- _LOG.error('%s %s does not exist; re-run CMake to generate it',
- index, build_ninja)
- return False
+ num_builds = len(self.project_builder)
+ index_message = f'[{index}/{num_builds}]'
- _LOG.warning('%s %s does not exist; running gn gen %s', index,
- build_ninja, cmd.build_dir)
- if not self._execute_command(['gn', 'gen', cmd.build_dir], env):
- return False
+ log_build_recipe_start(
+ index_message, self.project_builder, cfg, logger=_LOG
+ )
- command = ['ninja', *self._extra_ninja_args, '-C', *cmd.args()]
- _LOG.info('%s Starting build: %s', index,
- ' '.join(shlex.quote(arg) for arg in command))
+ self.project_builder.run_build(
+ cfg,
+ env,
+ index_message=index_message,
+ )
- return self._execute_command(command, env)
+ log_build_recipe_finish(
+ index_message,
+ self.project_builder,
+ cfg,
+ logger=_LOG,
+ )
- def _execute_command(self, command: list, env: dict) -> bool:
+ def execute_command(
+ self,
+ command: list,
+ env: dict,
+ recipe: BuildRecipe,
+ # pylint: disable=unused-argument
+ *args,
+ **kwargs,
+ # pylint: enable=unused-argument
+ ) -> bool:
"""Runs a command with a blank before/after for visual separation."""
- self.current_build_errors = 0
- self.status_message = (
- 'class:theme-fg-yellow',
- 'Building'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH))
if self.fullscreen_enabled:
- return self._execute_command_watch_app(command, env)
- print()
- self._current_build = subprocess.Popen(command, env=env)
- returncode = self._current_build.wait()
- print()
- return returncode == 0
-
- def _execute_command_watch_app(self, command: list, env: dict) -> bool:
+ return self._execute_command_watch_app(command, env, recipe)
+
+ if self.separate_logfiles:
+ return execute_command_with_logging(
+ command, env, recipe, logger=recipe.log
+ )
+
+ if self.use_logfile:
+ return execute_command_with_logging(
+ command, env, recipe, logger=_LOG
+ )
+
+ return execute_command_no_logging(command, env, recipe)
+
+ def _execute_command_watch_app(
+ self,
+ command: list,
+ env: dict,
+ recipe: BuildRecipe,
+ ) -> bool:
"""Runs a command with and outputs the logs."""
if not self.watch_app:
return False
- self.current_stdout = ''
- returncode = None
- with subprocess.Popen(command,
- env=env,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- errors='replace') as proc:
- self._current_build = proc
-
- # Empty line at the start.
- _NINJA_LOG.info('')
- while returncode is None:
- if not proc.stdout:
- continue
-
- output = proc.stdout.readline()
- self.current_stdout += output
-
- line_match_result = self.NINJA_BUILD_STEP.match(output)
- if line_match_result:
- matches = line_match_result.groupdict()
- self.current_build_step = line_match_result.group(0)
- self.current_build_percent = float(
- int(matches.get('step', 0)) /
- int(matches.get('total_steps', 1)))
-
- elif output.startswith(WatchApp.NINJA_FAILURE_TEXT):
- _NINJA_LOG.critical(output.strip())
- self.current_build_errors += 1
-
- else:
- # Mypy output mixes character encoding in its colored output
- # due to it's use of the curses module retrieving the 'sgr0'
- # (or exit_attribute_mode) capability from the host
- # machine's terminfo database.
- #
- # This can result in this sequence ending up in STDOUT as
- # b'\x1b(B\x1b[m'. (B tells terminals to interpret text as
- # USASCII encoding but will appear in prompt_toolkit as a B
- # character.
- #
- # The following replace calls will strip out those
- # instances.
- _NINJA_LOG.info(
- output.replace('\x1b(B\x1b[m',
- '').replace('\x1b[1m', '').strip())
+
+ self.watch_app.redraw_ui()
+
+ def new_line_callback(recipe: BuildRecipe) -> None:
+ self.current_build_step = recipe.status.current_step
+ self.current_build_percent = recipe.status.percent
+ self.current_build_errors = recipe.status.error_count
+
+ if self.watch_app:
self.watch_app.redraw_ui()
- returncode = proc.poll()
- # Empty line at the end.
- _NINJA_LOG.info('')
+ desired_logger = _LOG
+ if self.separate_logfiles:
+ desired_logger = recipe.log
+
+ result = execute_command_with_logging(
+ command,
+ env,
+ recipe,
+ logger=desired_logger,
+ line_processed_callback=new_line_callback,
+ )
+
+ self.watch_app.redraw_ui()
- return returncode == 0
+ return result
# Implementation of DebouncedFunction.cancel()
def cancel(self) -> bool:
if self.restart_on_changes:
- self._current_build.terminate()
- self._current_build.wait()
+ BUILDER_CONTEXT.restart_flag = True
+ BUILDER_CONTEXT.terminate_and_wait()
return True
return False
- # Implementation of DebouncedFunction.run()
+ # Implementation of DebouncedFunction.on_complete()
def on_complete(self, cancelled: bool = False) -> None:
# First, use the standard logging facilities to report build status.
if cancelled:
- self.status_message = (
- '', 'Cancelled'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH))
- _LOG.error('Finished; build was interrupted')
- elif all(self.builds_succeeded):
- self.status_message = (
- 'class:theme-fg-green',
- 'Succeeded'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH))
+ _LOG.info('Build stopped.')
+ elif BUILDER_CONTEXT.interrupted():
+ pass # Don't print anything.
+ elif all(
+ recipe.status.passed()
+ for recipe in self.project_builder
+ if recipe.enabled
+ ):
_LOG.info('Finished; all successful')
else:
- self.status_message = (
- 'class:theme-fg-red',
- 'Failed'.rjust(_FULLSCREEN_STATUS_COLUMN_WIDTH))
_LOG.info('Finished; some builds failed')
- # Show individual build results for fullscreen app
- if self.fullscreen_enabled:
- self.create_result_message()
# For non-fullscreen pw watch
- else:
+ if (
+ not self.fullscreen_enabled
+ and not self.project_builder.should_use_progress_bars()
+ ):
# Show a more distinct colored banner.
- if not cancelled:
- # Write out build summary table so you can tell which builds
- # passed and which builds failed.
- _LOG.info('')
- _LOG.info(' .------------------------------------')
- _LOG.info(' |')
- for (succeeded, cmd) in zip(self.builds_succeeded,
- self.build_commands):
- slug = (self.charset.slug_ok
- if succeeded else self.charset.slug_fail)
- _LOG.info(' | %s %s', slug, cmd)
- _LOG.info(' |')
- _LOG.info(" '------------------------------------")
- else:
- # Build was interrupted.
- _LOG.info('')
- _LOG.info(' .------------------------------------')
- _LOG.info(' |')
- _LOG.info(' | %s- interrupted', self.charset.slug_fail)
- _LOG.info(' |')
- _LOG.info(" '------------------------------------")
-
- # Show a large color banner for the overall result.
- if self.banners:
- if all(self.builds_succeeded) and not cancelled:
- for line in _PASS_MESSAGE.splitlines():
- _LOG.info(_COLOR.green(line))
- else:
- for line in _FAIL_MESSAGE.splitlines():
- _LOG.info(_COLOR.red(line))
+ self.project_builder.print_build_summary(
+ cancelled=cancelled, logger=_LOG
+ )
+ self.project_builder.print_pass_fail_banner(
+ cancelled=cancelled, logger=_LOG
+ )
if self.watch_app:
self.watch_app.redraw_ui()
self.matching_path = None
# Implementation of DebouncedFunction.on_keyboard_interrupt()
- def on_keyboard_interrupt(self) -> NoReturn:
+ def on_keyboard_interrupt(self) -> None:
_exit_due_to_interrupt()
-_WATCH_PATTERN_DELIMITER = ','
-_WATCH_PATTERNS = (
- '*.bloaty',
- '*.c',
- '*.cc',
- '*.css',
- '*.cpp',
- '*.cmake',
- 'CMakeLists.txt',
- '*.gn',
- '*.gni',
- '*.go',
- '*.h',
- '*.hpp',
- '*.ld',
- '*.md',
- '*.options',
- '*.proto',
- '*.py',
- '*.rst',
- '*.s',
- '*.S',
-)
-
-
-def add_parser_arguments(parser: argparse.ArgumentParser) -> None:
- """Sets up an argument parser for pw watch."""
- parser.add_argument('--patterns',
- help=(_WATCH_PATTERN_DELIMITER +
- '-delimited list of globs to '
- 'watch to trigger recompile'),
- default=_WATCH_PATTERN_DELIMITER.join(_WATCH_PATTERNS))
- parser.add_argument('--ignore_patterns',
- dest='ignore_patterns_string',
- help=(_WATCH_PATTERN_DELIMITER +
- '-delimited list of globs to '
- 'ignore events from'))
-
- parser.add_argument('--exclude_list',
- nargs='+',
- type=Path,
- help='directories to ignore during pw watch',
- default=[])
- parser.add_argument('--no-restart',
- dest='restart',
- action='store_false',
- help='do not restart ongoing builds if files change')
- parser.add_argument(
- 'default_build_targets',
- nargs='*',
- metavar='target',
- default=[],
- help=('Automatically locate a build directory and build these '
- 'targets. For example, `host docs` searches for a Ninja '
- 'build directory at out/ and builds the `host` and `docs` '
- 'targets. To specify one or more directories, ust the '
- '-C / --build_directory option.'))
- parser.add_argument(
- '-C',
- '--build_directory',
- dest='build_directories',
- nargs='+',
- action='append',
- default=[],
- metavar=('directory', 'target'),
- help=('Specify a build directory and optionally targets to '
- 'build. `pw watch -C out tgt` is equivalent to `ninja '
- '-C out tgt`'))
- parser.add_argument(
- '--serve-docs',
- dest='serve_docs',
- action='store_true',
- default=False,
- help='Start a webserver for docs on localhost. The port for this '
- ' webserver can be set with the --serve-docs-port option. '
- ' Defaults to http://127.0.0.1:8000')
- parser.add_argument(
- '--serve-docs-port',
- dest='serve_docs_port',
- type=int,
- default=8000,
- help='Set the port for the docs webserver. Default to 8000.')
-
- parser.add_argument(
- '--serve-docs-path',
- dest='serve_docs_path',
- type=Path,
- default="docs/gen/docs",
- help='Set the path for the docs to serve. Default to docs/gen/docs'
- ' in the build directory.')
- parser.add_argument(
- '-j',
- '--jobs',
- type=int,
- help="Number of cores to use; defaults to Ninja's default")
- parser.add_argument('-f',
- '--fullscreen',
- action='store_true',
- default=False,
- help='Use a fullscreen interface.')
- parser.add_argument('--debug-logging',
- action='store_true',
- help='Enable debug logging.')
- parser.add_argument('--no-banners',
- dest='banners',
- action='store_false',
- help='Hide pass/fail banners.')
-
-
def _exit(code: int) -> NoReturn:
+ # Flush all log handlers
+ logging.shutdown()
# Note: The "proper" way to exit is via observer.stop(), then
# running a join. However it's slower, so just exit immediately.
#
@@ -645,46 +486,77 @@ def _exit(code: int) -> NoReturn:
os._exit(code) # pylint: disable=protected-access
-def _exit_due_to_interrupt() -> NoReturn:
+def _exit_due_to_interrupt() -> None:
# To keep the log lines aligned with each other in the presence of
# a '^C' from the keyboard interrupt, add a newline before the log.
+ print('')
_LOG.info('Got Ctrl-C; exiting...')
- _exit(0)
+ BUILDER_CONTEXT.ctrl_c_interrupt()
-def _exit_due_to_inotify_watch_limit():
+def _log_inotify_watch_limit_reached():
# Show information and suggested commands in OSError: inotify limit reached.
- _LOG.error('Inotify watch limit reached: run this in your terminal if '
- 'you are in Linux to temporarily increase inotify limit. \n')
+ _LOG.error(
+ 'Inotify watch limit reached: run this in your terminal if '
+ 'you are in Linux to temporarily increase inotify limit.'
+ )
+ _LOG.info('')
_LOG.info(
- _COLOR.green(' sudo sysctl fs.inotify.max_user_watches='
- '$NEW_LIMIT$\n'))
- _LOG.info(' Change $NEW_LIMIT$ with an integer number, '
- 'e.g., 20000 should be enough.')
- _exit(0)
+ _COLOR.green(
+ ' sudo sysctl fs.inotify.max_user_watches=' '$NEW_LIMIT$'
+ )
+ )
+ _LOG.info('')
+ _LOG.info(
+ ' Change $NEW_LIMIT$ with an integer number, '
+ 'e.g., 20000 should be enough.'
+ )
-def _exit_due_to_inotify_instance_limit():
+def _exit_due_to_inotify_watch_limit():
+ _log_inotify_watch_limit_reached()
+ _exit(1)
+
+
+def _log_inotify_instance_limit_reached():
# Show information and suggested commands in OSError: inotify limit reached.
- _LOG.error('Inotify instance limit reached: run this in your terminal if '
- 'you are in Linux to temporarily increase inotify limit. \n')
+ _LOG.error(
+ 'Inotify instance limit reached: run this in your terminal if '
+ 'you are in Linux to temporarily increase inotify limit.'
+ )
+ _LOG.info('')
+ _LOG.info(
+ _COLOR.green(
+ ' sudo sysctl fs.inotify.max_user_instances=' '$NEW_LIMIT$'
+ )
+ )
+ _LOG.info('')
_LOG.info(
- _COLOR.green(' sudo sysctl fs.inotify.max_user_instances='
- '$NEW_LIMIT$\n'))
- _LOG.info(' Change $NEW_LIMIT$ with an integer number, '
- 'e.g., 20000 should be enough.')
- _exit(0)
+ ' Change $NEW_LIMIT$ with an integer number, '
+ 'e.g., 20000 should be enough.'
+ )
+
+
+def _exit_due_to_inotify_instance_limit():
+ _log_inotify_instance_limit_reached()
+ _exit(1)
def _exit_due_to_pigweed_not_installed():
# Show information and suggested commands when pigweed environment variable
# not found.
- _LOG.error('Environment variable $PW_ROOT not defined or is defined '
- 'outside the current directory.')
- _LOG.error('Did you forget to activate the Pigweed environment? '
- 'Try source ./activate.sh')
- _LOG.error('Did you forget to install the Pigweed environment? '
- 'Try source ./bootstrap.sh')
+ _LOG.error(
+ 'Environment variable $PW_ROOT not defined or is defined '
+ 'outside the current directory.'
+ )
+ _LOG.error(
+ 'Did you forget to activate the Pigweed environment? '
+ 'Try source ./activate.sh'
+ )
+ _LOG.error(
+ 'Did you forget to install the Pigweed environment? '
+ 'Try source ./bootstrap.sh'
+ )
_exit(1)
@@ -712,8 +584,9 @@ def minimal_watch_directories(to_watch: Path, to_exclude: Iterable[Path]):
# and generate all parent paths needed to be watched without recursion.
exclude_dir_parents = {to_watch}
for directory_to_exclude in directories_to_exclude:
- parts = list(
- Path(directory_to_exclude).relative_to(to_watch).parts)[:-1]
+ parts = list(Path(directory_to_exclude).relative_to(to_watch).parts)[
+ :-1
+ ]
dir_tmp = to_watch
for part in parts:
dir_tmp = Path(dir_tmp, part)
@@ -726,8 +599,11 @@ def minimal_watch_directories(to_watch: Path, to_exclude: Iterable[Path]):
dir_path = Path(directory)
yield dir_path, False
for item in Path(directory).iterdir():
- if (item.is_dir() and item not in exclude_dir_parents
- and item not in directories_to_exclude):
+ if (
+ item.is_dir()
+ and item not in exclude_dir_parents
+ and item not in directories_to_exclude
+ ):
yield item, True
@@ -747,8 +623,10 @@ def get_common_excludes() -> List[Path]:
# Preset exclude list for Pigweed's upstream directories.
pw_root_dir = Path(os.environ['PW_ROOT'])
- exclude_list.extend(pw_root_dir / ignored_directory
- for ignored_directory in typical_ignored_directories)
+ exclude_list.extend(
+ pw_root_dir / ignored_directory
+ for ignored_directory in typical_ignored_directories
+ )
# Preset exclude for common downstream project structures.
#
@@ -758,7 +636,8 @@ def get_common_excludes() -> List[Path]:
if pw_project_root_dir != pw_root_dir:
exclude_list.extend(
pw_project_root_dir / ignored_directory
- for ignored_directory in typical_ignored_directories)
+ for ignored_directory in typical_ignored_directories
+ )
# Check for and warn about legacy directories.
legacy_directories = [
@@ -769,33 +648,141 @@ def get_common_excludes() -> List[Path]:
for legacy_directory in legacy_directories:
full_legacy_directory = pw_root_dir / legacy_directory
if full_legacy_directory.is_dir():
- _LOG.warning('Legacy environment directory found: %s',
- str(full_legacy_directory))
+ _LOG.warning(
+ 'Legacy environment directory found: %s',
+ str(full_legacy_directory),
+ )
exclude_list.append(full_legacy_directory)
found_legacy = True
if found_legacy:
- _LOG.warning('Found legacy environment directory(s); these '
- 'should be deleted')
+ _LOG.warning(
+ 'Found legacy environment directory(s); these ' 'should be deleted'
+ )
return exclude_list
-def watch_setup(
- default_build_targets: List[str],
- build_directories: List[str],
- patterns: str,
- ignore_patterns_string: str,
- exclude_list: List[Path],
- restart: bool,
- jobs: Optional[int],
- serve_docs: bool,
- serve_docs_port: int,
- serve_docs_path: Path,
- fullscreen: bool,
- banners: bool,
+def _simple_docs_server(
+ address: str, port: int, path: Path
+) -> Callable[[], None]:
+ class Handler(http.server.SimpleHTTPRequestHandler):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, directory=path, **kwargs)
+
+ # Disable logs to stdout
+ def log_message(
+ self, format: str, *args # pylint: disable=redefined-builtin
+ ) -> None:
+ return
+
+ def simple_http_server_thread():
+ with socketserver.TCPServer((address, port), Handler) as httpd:
+ httpd.serve_forever()
+
+ return simple_http_server_thread
+
+
+def _httpwatcher_docs_server(
+ address: str, port: int, path: Path
+) -> Callable[[], None]:
+ def httpwatcher_thread():
+ # Disable logs from httpwatcher and deps
+ logging.getLogger('httpwatcher').setLevel(logging.CRITICAL)
+ logging.getLogger('tornado').setLevel(logging.CRITICAL)
+
+ httpwatcher.watch(path, host=address, port=port)
+
+ return httpwatcher_thread
+
+
+def _serve_docs(
+ build_dir: Path,
+ docs_path: Path,
+ address: str = '127.0.0.1',
+ port: int = 8000,
+) -> None:
+ address = '127.0.0.1'
+ docs_path = build_dir.joinpath(docs_path.joinpath('html'))
+
+ if httpwatcher is not None:
+ _LOG.info('Using httpwatcher. Docs will reload when changed.')
+ server_thread = _httpwatcher_docs_server(address, port, docs_path)
+ else:
+ _LOG.info(
+ 'Using simple HTTP server. Docs will not reload when changed.'
+ )
+ _LOG.info('Install httpwatcher and restart for automatic docs reload.')
+ server_thread = _simple_docs_server(address, port, docs_path)
+
+ # Spin up server in a new thread since it blocks
+ threading.Thread(None, server_thread, 'pw_docs_server').start()
+
+
+def watch_logging_init(log_level: int, fullscreen: bool, colors: bool) -> None:
+ # Logging setup
+ if not fullscreen:
+ pw_cli.log.install(
+ level=log_level,
+ use_color=colors,
+ hide_timestamp=False,
+ )
+ return
+
+ watch_logfile = pw_console.python_logging.create_temp_log_file(
+ prefix=__package__
+ )
+
+ pw_cli.log.install(
+ level=logging.DEBUG,
+ use_color=colors,
+ hide_timestamp=False,
+ log_file=watch_logfile,
+ )
+
+
+def watch_setup( # pylint: disable=too-many-locals
+ project_builder: ProjectBuilder,
+ # NOTE: The following args should have defaults matching argparse. This
+ # allows use of watch_setup by other project build scripts.
+ patterns: str = WATCH_PATTERN_DELIMITER.join(WATCH_PATTERNS),
+ ignore_patterns_string: str = '',
+ exclude_list: Optional[List[Path]] = None,
+ restart: bool = True,
+ serve_docs: bool = False,
+ serve_docs_port: int = 8000,
+ serve_docs_path: Path = Path('docs/gen/docs'),
+ fullscreen: bool = False,
+ banners: bool = True,
+ logfile: Optional[Path] = None,
+ separate_logfiles: bool = False,
+ parallel: bool = False,
+ parallel_workers: int = 0,
+ # pylint: disable=unused-argument
+ default_build_targets: Optional[List[str]] = None,
+ build_directories: Optional[List[str]] = None,
+ build_system_commands: Optional[List[str]] = None,
+ run_command: Optional[List[str]] = None,
+ jobs: Optional[int] = None,
+ keep_going: bool = False,
+ colors: bool = True,
+ debug_logging: bool = False,
+ # pylint: enable=unused-argument
# pylint: disable=too-many-arguments
-) -> Tuple[str, PigweedBuildWatcher, List[Path]]:
+) -> Tuple[PigweedBuildWatcher, List[Path]]:
"""Watches files and runs Ninja commands when they change."""
+ watch_logging_init(
+ log_level=project_builder.default_log_level,
+ fullscreen=fullscreen,
+ colors=colors,
+ )
+
+ # Update the project_builder log formatters since pw_cli.log.install may
+ # have changed it.
+ project_builder.apply_root_log_formatting()
+
+ if project_builder.should_use_progress_bars():
+ project_builder.use_stdout_proxy()
+
_LOG.info('Starting Pigweed build watcher')
# Get pigweed directory information from environment variable PW_ROOT.
@@ -805,83 +792,81 @@ def watch_setup(
if Path.cwd().resolve() not in [pw_root, *pw_root.parents]:
_exit_due_to_pigweed_not_installed()
+ build_recipes = project_builder.build_recipes
+
# Preset exclude list for pigweed directory.
+ if not exclude_list:
+ exclude_list = []
exclude_list += get_common_excludes()
# Add build directories to the exclude list.
exclude_list.extend(
- Path(build_dir[0]).resolve() for build_dir in build_directories)
-
- build_commands = [
- BuildCommand(Path(build_dir[0]), tuple(build_dir[1:]))
- for build_dir in build_directories
- ]
-
- # If no build directory was specified, check for out/build.ninja.
- if default_build_targets or not build_directories:
- # Make sure we found something; if not, bail.
- if not Path('out').exists():
- _die("No build dirs found. Did you forget to run 'gn gen out'?")
-
- build_commands.append(
- BuildCommand(Path('out'), tuple(default_build_targets)))
+ cfg.build_dir.resolve()
+ for cfg in build_recipes
+ if isinstance(cfg.build_dir, Path)
+ )
- # Verify that the build output directories exist.
- for i, build_target in enumerate(build_commands, 1):
- if not build_target.build_dir.is_dir():
- _die("Build directory doesn't exist: %s", build_target)
- else:
- _LOG.info('Will build [%d/%d]: %s', i, len(build_commands),
- build_target)
+ for i, build_recipe in enumerate(build_recipes, start=1):
+ _LOG.info('Will build [%d/%d]: %s', i, len(build_recipes), build_recipe)
_LOG.debug('Patterns: %s', patterns)
if serve_docs:
-
- def _serve_docs():
- # Disable logs from httpwatcher and deps
- logging.getLogger('httpwatcher').setLevel(logging.CRITICAL)
- logging.getLogger('tornado').setLevel(logging.CRITICAL)
-
- docs_path = build_commands[0].build_dir.joinpath(
- serve_docs_path.joinpath('html'))
- httpwatcher.watch(docs_path,
- host="127.0.0.1",
- port=serve_docs_port)
-
- # Spin up an httpwatcher in a new thread since it blocks
- threading.Thread(None, _serve_docs, "httpwatcher").start()
-
- # Try to make a short display path for the watched directory that has
- # "$HOME" instead of the full home directory. This is nice for users
- # who have deeply nested home directories.
- path_to_log = str(Path().resolve()).replace(str(Path.home()), '$HOME')
+ _serve_docs(
+ build_recipes[0].build_dir, serve_docs_path, port=serve_docs_port
+ )
# Ignore the user-specified patterns.
- ignore_patterns = (ignore_patterns_string.split(_WATCH_PATTERN_DELIMITER)
- if ignore_patterns_string else [])
+ ignore_patterns = (
+ ignore_patterns_string.split(WATCH_PATTERN_DELIMITER)
+ if ignore_patterns_string
+ else []
+ )
- env = pw_cli.env.pigweed_environment()
- if env.PW_EMOJI:
- charset = _EMOJI_CHARSET
- else:
- charset = _ASCII_CHARSET
+ # Add project_builder logfiles to ignore_patterns
+ if project_builder.default_logfile:
+ ignore_patterns.append(str(project_builder.default_logfile))
+ if project_builder.separate_build_file_logging:
+ for recipe in project_builder:
+ if recipe.logfile:
+ ignore_patterns.append(str(recipe.logfile))
+
+ workers = 1
+ if parallel:
+ # If parallel is requested and parallel_workers is set to 0 run all
+ # recipes in parallel. That is, use the number of recipes as the worker
+ # count.
+ if parallel_workers == 0:
+ workers = len(project_builder)
+ else:
+ workers = parallel_workers
event_handler = PigweedBuildWatcher(
- build_commands=build_commands,
- patterns=patterns.split(_WATCH_PATTERN_DELIMITER),
+ project_builder=project_builder,
+ patterns=patterns.split(WATCH_PATTERN_DELIMITER),
ignore_patterns=ignore_patterns,
- charset=charset,
restart=restart,
- jobs=jobs,
fullscreen=fullscreen,
banners=banners,
+ use_logfile=bool(logfile),
+ separate_logfiles=separate_logfiles,
+ parallel_workers=workers,
)
- return path_to_log, event_handler, exclude_list
+ project_builder.execute_command = event_handler.execute_command
+
+ return event_handler, exclude_list
-def watch(path_to_log: Path, event_handler: PigweedBuildWatcher,
- exclude_list: List[Path]):
+
+def watch(
+ event_handler: PigweedBuildWatcher,
+ exclude_list: List[Path],
+):
"""Watches files and runs Ninja commands when they change."""
+ # Try to make a short display path for the watched directory that has
+ # "$HOME" instead of the full home directory. This is nice for users
+ # who have deeply nested home directories.
+ path_to_log = str(Path().resolve()).replace(str(Path.home()), '$HOME')
+
try:
# It can take awhile to configure the filesystem watcher, so have the
# message reflect that with the "...". Run inside the try: to
@@ -907,6 +892,7 @@ def watch(path_to_log: Path, event_handler: PigweedBuildWatcher,
for observer in observers:
while observer.is_alive():
observer.join(1)
+ _LOG.error('observers joined')
# Ctrl-C on Unix generates KeyboardInterrupt
# Ctrl-Z on Windows generates EOFError
@@ -914,67 +900,113 @@ def watch(path_to_log: Path, event_handler: PigweedBuildWatcher,
_exit_due_to_interrupt()
except OSError as err:
if err.args[0] == _ERRNO_INOTIFY_LIMIT_REACHED:
- _exit_due_to_inotify_watch_limit()
+ if event_handler.watch_app:
+ event_handler.watch_app.exit(
+ log_after_shutdown=_log_inotify_watch_limit_reached
+ )
+ elif event_handler.project_builder.should_use_progress_bars():
+ BUILDER_CONTEXT.exit(
+ log_after_shutdown=_log_inotify_watch_limit_reached,
+ )
+ else:
+ _exit_due_to_inotify_watch_limit()
if err.errno == errno.EMFILE:
- _exit_due_to_inotify_instance_limit()
+ if event_handler.watch_app:
+ event_handler.watch_app.exit(
+ log_after_shutdown=_log_inotify_instance_limit_reached
+ )
+ elif event_handler.project_builder.should_use_progress_bars():
+ BUILDER_CONTEXT.exit(
+ log_after_shutdown=_log_inotify_instance_limit_reached
+ )
+ else:
+ _exit_due_to_inotify_instance_limit()
raise err
- _LOG.critical('Should never get here')
- observer.join()
+def run_watch(
+ event_handler: PigweedBuildWatcher,
+ exclude_list: List[Path],
+ prefs: Optional[WatchAppPrefs] = None,
+ fullscreen: bool = False,
+) -> None:
+ """Start pw_watch."""
+ if not prefs:
+ prefs = WatchAppPrefs(load_argparse_arguments=add_parser_arguments)
+
+ if fullscreen:
+ watch_thread = Thread(
+ target=watch,
+ args=(event_handler, exclude_list),
+ daemon=True,
+ )
+ watch_thread.start()
+ watch_app = WatchApp(
+ event_handler=event_handler,
+ prefs=prefs,
+ )
-def main() -> None:
- """Watch files for changes and rebuild."""
+ event_handler.watch_app = watch_app
+ watch_app.run()
+
+ else:
+ watch(event_handler, exclude_list)
+
+
+def get_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=__doc__,
- formatter_class=argparse.RawDescriptionHelpFormatter)
- add_parser_arguments(parser)
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser = add_parser_arguments(parser)
+ return parser
+
+
+def main() -> None:
+ """Watch files for changes and rebuild."""
+ parser = get_parser()
args = parser.parse_args()
- path_to_log, event_handler, exclude_list = watch_setup(
- default_build_targets=args.default_build_targets,
- build_directories=args.build_directories,
- patterns=args.patterns,
- ignore_patterns_string=args.ignore_patterns_string,
- exclude_list=args.exclude_list,
- restart=args.restart,
+ prefs = WatchAppPrefs(load_argparse_arguments=add_parser_arguments)
+ prefs.apply_command_line_args(args)
+ build_recipes = create_build_recipes(prefs)
+
+ env = pw_cli.env.pigweed_environment()
+ if env.PW_EMOJI:
+ charset = EMOJI_CHARSET
+ else:
+ charset = ASCII_CHARSET
+
+ # Force separate-logfiles for split window panes if running in parallel.
+ separate_logfiles = args.separate_logfiles
+ if args.parallel:
+ separate_logfiles = True
+
+ def _recipe_abort(*args) -> None:
+ _LOG.critical(*args)
+
+ project_builder = ProjectBuilder(
+ build_recipes=build_recipes,
jobs=args.jobs,
- serve_docs=args.serve_docs,
- serve_docs_port=args.serve_docs_port,
- serve_docs_path=args.serve_docs_path,
- fullscreen=args.fullscreen,
banners=args.banners,
+ keep_going=args.keep_going,
+ colors=args.colors,
+ charset=charset,
+ separate_build_file_logging=separate_logfiles,
+ root_logfile=args.logfile,
+ root_logger=_LOG,
+ log_level=logging.DEBUG if args.debug_logging else logging.INFO,
+ abort_callback=_recipe_abort,
)
- if args.fullscreen:
- watch_logfile = (pw_console.python_logging.create_temp_log_file(
- prefix=__package__))
- pw_cli.log.install(
- level=logging.DEBUG,
- use_color=True,
- hide_timestamp=False,
- log_file=watch_logfile,
- )
- pw_console.python_logging.setup_python_logging(
- last_resort_filename=watch_logfile)
-
- watch_thread = Thread(target=watch,
- args=(path_to_log, event_handler, exclude_list),
- daemon=True)
- watch_thread.start()
- watch_app = WatchApp(event_handler=event_handler,
- debug_logging=args.debug_logging,
- log_file_name=watch_logfile)
+ event_handler, exclude_list = watch_setup(project_builder, **vars(args))
- event_handler.watch_app = watch_app
- watch_app.run()
- else:
- pw_cli.log.install(
- level=logging.DEBUG if args.debug_logging else logging.INFO,
- use_color=True,
- hide_timestamp=False,
- )
- watch(Path(path_to_log), event_handler, exclude_list)
+ run_watch(
+ event_handler,
+ exclude_list,
+ prefs=prefs,
+ fullscreen=args.fullscreen,
+ )
if __name__ == '__main__':
diff --git a/pw_watch/py/pw_watch/watch_app.py b/pw_watch/py/pw_watch/watch_app.py
index 61f7fd99c..38514533d 100644
--- a/pw_watch/py/pw_watch/watch_app.py
+++ b/pw_watch/py/pw_watch/watch_app.py
@@ -15,17 +15,18 @@
""" Prompt toolkit application for pw watch. """
import asyncio
+import functools
import logging
-from pathlib import Path
+import os
import re
-import sys
-from typing import List, NoReturn, Optional
+import time
+from typing import Callable, Dict, Iterable, List, NoReturn, Optional
from prompt_toolkit.application import Application
from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
from prompt_toolkit.filters import Condition
from prompt_toolkit.history import (
- FileHistory,
+ InMemoryHistory,
History,
ThreadedHistory,
)
@@ -35,7 +36,6 @@ from prompt_toolkit.key_binding import (
merge_key_bindings,
)
from prompt_toolkit.layout import (
- Dimension,
DynamicContainer,
Float,
FloatContainer,
@@ -45,142 +45,419 @@ from prompt_toolkit.layout import (
Window,
)
from prompt_toolkit.layout.controls import BufferControl
-from prompt_toolkit.styles import DynamicStyle, merge_styles, Style
+from prompt_toolkit.styles import (
+ ConditionalStyleTransformation,
+ DynamicStyle,
+ SwapLightAndDarkStyleTransformation,
+ merge_style_transformations,
+ merge_styles,
+ style_from_pygments_cls,
+)
from prompt_toolkit.formatted_text import StyleAndTextTuples
+from prompt_toolkit.lexers import PygmentsLexer
+from pygments.lexers.markup import MarkdownLexer # type: ignore
from pw_console.console_app import get_default_colordepth
-from pw_console.console_prefs import ConsolePrefs
from pw_console.get_pw_console_app import PW_CONSOLE_APP_CONTEXTVAR
+from pw_console.help_window import HelpWindow
+from pw_console.key_bindings import DEFAULT_KEY_BINDINGS
from pw_console.log_pane import LogPane
from pw_console.plugin_mixin import PluginMixin
+import pw_console.python_logging
from pw_console.quit_dialog import QuitDialog
-import pw_console.style
-import pw_console.widgets.border
+from pw_console.style import generate_styles, get_theme_colors
+from pw_console.pigweed_code_style import PigweedCodeStyle
+from pw_console.widgets import (
+ FloatingWindowPane,
+ ToolbarButton,
+ WindowPaneToolbar,
+ create_border,
+ mouse_handlers,
+ to_checkbox,
+)
+from pw_console.window_list import DisplayMode
from pw_console.window_manager import WindowManager
-_NINJA_LOG = logging.getLogger('pw_watch_ninja_output')
-_LOG = logging.getLogger('pw_watch')
+from pw_build.project_builder_prefs import ProjectBuilderPrefs
+from pw_build.project_builder_context import get_project_builder_context
+
+
+_LOG = logging.getLogger('pw_build.watch')
+
+BUILDER_CONTEXT = get_project_builder_context()
+
+_HELP_TEXT = """
+Mouse Keys
+==========
+
+- Click on a line in the bottom progress bar to switch to that tab.
+- Click on any tab, or button to activate.
+- Scroll wheel in the the log windows moves back through the history.
+
+
+Global Keys
+===========
+
+Quit with confirmation dialog. -------------------- Ctrl-D
+Quit without confirmation. ------------------------ Ctrl-X Ctrl-C
+Toggle user guide window. ------------------------- F1
+Trigger a rebuild. -------------------------------- Enter
+
+
+Window Management Keys
+======================
+
+Switch focus to the next window pane or tab. ------ Ctrl-Alt-N
+Switch focus to the previous window pane or tab. -- Ctrl-Alt-P
+Move window pane left. ---------------------------- Ctrl-Alt-Left
+Move window pane right. --------------------------- Ctrl-Alt-Right
+Move window pane down. ---------------------------- Ctrl-Alt-Down
+Move window pane up. ------------------------------ Ctrl-Alt-Up
+Balance all window sizes. ------------------------- Ctrl-U
+
+
+Bottom Toolbar Controls
+=======================
+
+Rebuild Enter --------------- Click or press Enter to trigger a rebuild.
+[x] Auto Rebuild ------------ Click to globaly enable or disable automatic
+ rebuilding when files change.
+Help F1 --------------------- Click or press F1 to open this help window.
+Quit Ctrl-d ----------------- Click or press Ctrl-d to quit pw_watch.
+Next Tab Ctrl-Alt-n --------- Switch to the next log tab.
+Previous Tab Ctrl-Alt-p ----- Switch to the previous log tab.
+
+
+Build Status Bar
+================
+
+The build status bar shows the current status of all build directories outlined
+in a colored frame.
+
+ ┏━━ BUILDING ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+ ┃ [✓] out_directory Building Last line of standard out. ┃
+ ┃ [✓] out_dir2 Waiting Last line of standard out. ┃
+ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+
+Each checkbox on the far left controls whether that directory is built when
+files change and manual builds are run.
+
+
+Copying Text
+============
+
+- Click drag will select whole lines in the log windows.
+- `Ctrl-c` will copy selected lines to your system clipboard.
+
+If running over SSH you will need to use your terminal's built in text
+selection.
+
+Linux
+-----
+
+- Holding `Shift` and dragging the mouse in most terminals.
+
+Mac
+---
+
+- Apple Terminal:
+
+ Hold `Fn` and drag the mouse
+
+- iTerm2:
+
+ Hold `Cmd+Option` and drag the mouse
+
+Windows
+-------
+
+- Git CMD (included in `Git for Windows)
+
+ 1. Click on the Git window icon in the upper left of the title bar
+ 2. Click `Edit` then `Mark`
+ 3. Drag the mouse to select text and press Enter to copy.
+
+- Windows Terminal
+
+ 1. Hold `Shift` and drag the mouse to select text
+ 2. Press `Ctrl-Shift-C` to copy.
+
+"""
+
+
+class WatchAppPrefs(ProjectBuilderPrefs):
+ """Add pw_console specific prefs standard ProjectBuilderPrefs."""
+
+ def __init__(self, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+
+ self.registered_commands = DEFAULT_KEY_BINDINGS
+ self.registered_commands.update(self.user_key_bindings)
+
+ new_config_settings = {
+ 'key_bindings': DEFAULT_KEY_BINDINGS,
+ 'show_python_logger': True,
+ }
+ self.default_config.update(new_config_settings)
+ self._update_config(new_config_settings)
+
+ # Required pw_console preferences for key bindings and themes
+ @property
+ def user_key_bindings(self) -> Dict[str, List[str]]:
+ return self._config.get('key_bindings', {})
+
+ @property
+ def ui_theme(self) -> str:
+ return self._config.get('ui_theme', '')
+
+ @ui_theme.setter
+ def ui_theme(self, new_ui_theme: str) -> None:
+ self._config['ui_theme'] = new_ui_theme
+
+ @property
+ def theme_colors(self):
+ return get_theme_colors(self.ui_theme)
+
+ @property
+ def swap_light_and_dark(self) -> bool:
+ return self._config.get('swap_light_and_dark', False)
+
+ def get_function_keys(self, name: str) -> List:
+ """Return the keys for the named function."""
+ try:
+ return self.registered_commands[name]
+ except KeyError as error:
+ raise KeyError('Unbound key function: {}'.format(name)) from error
+
+ def register_named_key_function(
+ self, name: str, default_bindings: List[str]
+ ) -> None:
+ self.registered_commands[name] = default_bindings
+
+ def register_keybinding(
+ self, name: str, key_bindings: KeyBindings, **kwargs
+ ) -> Callable:
+ """Apply registered keys for the given named function."""
+
+ def decorator(handler: Callable) -> Callable:
+ "`handler` is a callable or Binding."
+ for keys in self.get_function_keys(name):
+ key_bindings.add(*keys.split(' '), **kwargs)(handler)
+ return handler
+
+ return decorator
+
+ # Required pw_console preferences for using a log window pane.
+ @property
+ def spaces_between_columns(self) -> int:
+ return 2
+
+ @property
+ def window_column_split_method(self) -> str:
+ return 'vertical'
+
+ @property
+ def hide_date_from_log_time(self) -> bool:
+ return True
+
+ @property
+ def column_order(self) -> list:
+ return []
+
+ def column_style( # pylint: disable=no-self-use
+ self,
+ _column_name: str,
+ _column_value: str,
+ default='',
+ ) -> str:
+ return default
+
+ @property
+ def show_python_file(self) -> bool:
+ return self._config.get('show_python_file', False)
+
+ @property
+ def show_source_file(self) -> bool:
+ return self._config.get('show_source_file', False)
+
+ @property
+ def show_python_logger(self) -> bool:
+ return self._config.get('show_python_logger', False)
class WatchWindowManager(WindowManager):
def update_root_container_body(self):
- self.application.window_manager_container = (
- self.create_root_container())
+ self.application.window_manager_container = self.create_root_container()
class WatchApp(PluginMixin):
"""Pigweed Watch main window application."""
+
# pylint: disable=too-many-instance-attributes
NINJA_FAILURE_TEXT = '\033[31mFAILED: '
NINJA_BUILD_STEP = re.compile(
- r"^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$")
-
- def __init__(self,
- event_handler,
- debug_logging: bool = False,
- log_file_name: Optional[str] = None):
-
+ r"^\[(?P<step>[0-9]+)/(?P<total_steps>[0-9]+)\] (?P<action>.*)$"
+ )
+
+ def __init__(
+ self,
+ event_handler,
+ prefs: WatchAppPrefs,
+ ):
self.event_handler = event_handler
- self.external_logfile: Optional[Path] = (Path(log_file_name)
- if log_file_name else None)
self.color_depth = get_default_colordepth()
# Necessary for some of pw_console's window manager features to work
# such as mouse drag resizing.
PW_CONSOLE_APP_CONTEXTVAR.set(self) # type: ignore
- self.prefs = ConsolePrefs()
+ self.prefs = prefs
self.quit_dialog = QuitDialog(self, self.exit) # type: ignore
- self.search_history_filename = self.prefs.search_history
- # History instance for search toolbars.
- self.search_history: History = ThreadedHistory(
- FileHistory(str(self.search_history_filename)))
+ self.search_history: History = ThreadedHistory(InMemoryHistory())
self.window_manager = WatchWindowManager(self)
- pw_console.python_logging.setup_python_logging()
-
self._build_error_count = 0
self._errors_in_output = False
- self.ninja_log_pane = LogPane(application=self,
- pane_title='Pigweed Watch')
- self.ninja_log_pane.add_log_handler(_NINJA_LOG, level_name='INFO')
- self.ninja_log_pane.add_log_handler(
- _LOG, level_name=('DEBUG' if debug_logging else 'INFO'))
- # Set python log format to just the message itself.
- self.ninja_log_pane.log_view.log_store.formatter = logging.Formatter(
- '%(message)s')
- self.ninja_log_pane.table_view = False
- # Enable line wrapping
- self.ninja_log_pane.toggle_wrap_lines()
- # Blank right side toolbar text
- self.ninja_log_pane._pane_subtitle = ' '
- self.ninja_log_view = self.ninja_log_pane.log_view
+ self.log_ui_update_frequency = 0.1 # 10 FPS
+ self._last_ui_update_time = time.time()
- # Make tab and shift-tab search for next and previous error
- next_error_bindings = KeyBindings()
-
- @next_error_bindings.add('s-tab')
- def _previous_error(_event):
- self.jump_to_error(backwards=True)
-
- @next_error_bindings.add('tab')
- def _next_error(_event):
- self.jump_to_error()
+ self.recipe_name_to_log_pane: Dict[str, LogPane] = {}
+ self.recipe_index_to_log_pane: Dict[int, LogPane] = {}
- existing_log_bindings: Optional[KeyBindingsBase] = (
- self.ninja_log_pane.log_content_control.key_bindings)
+ debug_logging = (
+ event_handler.project_builder.default_log_level == logging.DEBUG
+ )
+ level_name = 'DEBUG' if debug_logging else 'INFO'
+
+ no_propagation_loggers = []
+
+ if event_handler.separate_logfiles:
+ pane_index = len(event_handler.project_builder.build_recipes) - 1
+ for recipe in reversed(event_handler.project_builder.build_recipes):
+ log_pane = self.add_build_log_pane(
+ recipe.display_name,
+ loggers=[recipe.log],
+ level_name=level_name,
+ )
+ if recipe.log.propagate is False:
+ no_propagation_loggers.append(recipe.log)
+
+ self.recipe_name_to_log_pane[recipe.display_name] = log_pane
+ self.recipe_index_to_log_pane[pane_index] = log_pane
+ pane_index -= 1
+
+ pw_console.python_logging.setup_python_logging(
+ loggers_with_no_propagation=no_propagation_loggers
+ )
- key_binding_list: List[KeyBindingsBase] = []
- if existing_log_bindings:
- key_binding_list.append(existing_log_bindings)
- key_binding_list.append(next_error_bindings)
- self.ninja_log_pane.log_content_control.key_bindings = (
- merge_key_bindings(key_binding_list))
+ self.root_log_pane = self.add_build_log_pane(
+ 'Root Log',
+ loggers=[
+ logging.getLogger('pw_build'),
+ ],
+ level_name=level_name,
+ )
- self.window_manager.add_pane(self.ninja_log_pane)
+ self.window_manager.window_lists[0].display_mode = DisplayMode.TABBED
self.window_manager_container = (
- self.window_manager.create_root_container())
+ self.window_manager.create_root_container()
+ )
self.status_bar_border_style = 'class:command-runner-border'
+ self.status_bar_control = FormattedTextControl(self.get_status_bar_text)
+
+ self.status_bar_container = create_border(
+ HSplit(
+ [
+ # Result Toolbar.
+ Window(
+ content=self.status_bar_control,
+ height=len(self.event_handler.project_builder),
+ wrap_lines=False,
+ style='class:pane_active',
+ ),
+ ]
+ ),
+ content_height=len(self.event_handler.project_builder),
+ title=BUILDER_CONTEXT.get_title_bar_text,
+ border_style=(BUILDER_CONTEXT.get_title_style),
+ base_style='class:pane_active',
+ left_margin_columns=1,
+ right_margin_columns=1,
+ )
+
+ self.floating_window_plugins: List[FloatingWindowPane] = []
+
+ self.user_guide_window = HelpWindow(
+ self, # type: ignore
+ title='Pigweed Watch',
+ disable_ctrl_c=True,
+ )
+ self.user_guide_window.set_help_text(
+ _HELP_TEXT, lexer=PygmentsLexer(MarkdownLexer)
+ )
+
+ self.help_toolbar = WindowPaneToolbar(
+ title='Pigweed Watch',
+ include_resize_handle=False,
+ focus_action_callable=self.switch_to_root_log,
+ click_to_focus_text='',
+ )
+ self.help_toolbar.add_button(
+ ToolbarButton('Enter', 'Rebuild', self.run_build)
+ )
+ self.help_toolbar.add_button(
+ ToolbarButton(
+ description='Auto Rebuild',
+ mouse_handler=self.toggle_restart_on_filechange,
+ is_checkbox=True,
+ checked=lambda: self.restart_on_changes,
+ )
+ )
+ self.help_toolbar.add_button(
+ ToolbarButton('F1', 'Help', self.user_guide_window.toggle_display)
+ )
+ self.help_toolbar.add_button(ToolbarButton('Ctrl-d', 'Quit', self.exit))
+ self.help_toolbar.add_button(
+ ToolbarButton(
+ 'Ctrl-Alt-n', 'Next Tab', self.window_manager.focus_next_pane
+ )
+ )
+ self.help_toolbar.add_button(
+ ToolbarButton(
+ 'Ctrl-Alt-p',
+ 'Previous Tab',
+ self.window_manager.focus_previous_pane,
+ )
+ )
+
self.root_container = FloatContainer(
- HSplit([
- pw_console.widgets.border.create_border(
- HSplit([
- # The top toolbar.
- Window(
- content=FormattedTextControl(
- self.get_statusbar_text),
- height=Dimension.exact(1),
- style='class:toolbar_inactive',
- ),
- # Result Toolbar.
- Window(
- content=FormattedTextControl(
- self.get_resultbar_text),
- height=lambda: len(self.event_handler.
- build_commands),
- style='class:toolbar_inactive',
- ),
- ]),
- border_style=lambda: self.status_bar_border_style,
- base_style='class:toolbar_inactive',
- left_margin_columns=1,
- right_margin_columns=1,
- ),
- # The main content.
- DynamicContainer(lambda: self.window_manager_container),
- ]),
+ HSplit(
+ [
+ # Window pane content:
+ DynamicContainer(lambda: self.window_manager_container),
+ self.status_bar_container,
+ self.help_toolbar,
+ ]
+ ),
floats=[
Float(
+ content=self.user_guide_window,
+ top=2,
+ left=4,
+ bottom=4,
+ width=self.user_guide_window.content_width,
+ ),
+ Float(
content=self.quit_dialog,
top=2,
left=2,
@@ -208,20 +485,39 @@ class WatchApp(PluginMixin):
"""Quit with confirmation dialog."""
self.quit_dialog.open_dialog()
- self.key_bindings = merge_key_bindings([
- self.window_manager.key_bindings,
+ @register(
+ 'global.open-user-guide',
key_bindings,
- ])
+ filter=Condition(lambda: not self.modal_window_is_open()),
+ )
+ def _show_help(_event):
+ """Toggle user guide window."""
+ self.user_guide_window.toggle_display()
+
+ self.key_bindings = merge_key_bindings(
+ [
+ self.window_manager.key_bindings,
+ key_bindings,
+ ]
+ )
- self.current_theme = pw_console.style.generate_styles(
- self.prefs.ui_theme)
- self.current_theme = merge_styles([
- self.current_theme,
- Style.from_dict({'search': 'bg:ansired ansiblack'}),
- ])
+ self.current_theme = generate_styles(self.prefs.ui_theme)
- self.layout = Layout(self.root_container,
- focused_element=self.ninja_log_pane)
+ self.style_transformation = merge_style_transformations(
+ [
+ ConditionalStyleTransformation(
+ SwapLightAndDarkStyleTransformation(),
+ filter=Condition(lambda: self.prefs.swap_light_and_dark),
+ ),
+ ]
+ )
+
+ self.code_theme = style_from_pygments_cls(PigweedCodeStyle)
+
+ self.layout = Layout(
+ self.root_container,
+ focused_element=self.root_log_pane,
+ )
self.application: Application = Application(
layout=self.layout,
@@ -229,9 +525,15 @@ class WatchApp(PluginMixin):
mouse_support=True,
color_depth=self.color_depth,
clipboard=PyperclipClipboard(),
- style=DynamicStyle(lambda: merge_styles([
- self.current_theme,
- ])),
+ style=DynamicStyle(
+ lambda: merge_styles(
+ [
+ self.current_theme,
+ self.code_theme,
+ ]
+ )
+ ),
+ style_transformation=self.style_transformation,
full_screen=True,
)
@@ -241,18 +543,94 @@ class WatchApp(PluginMixin):
plugin_logger_name='pw_watch_stdout_checker',
)
+ def add_build_log_pane(
+ self, title: str, loggers: List[logging.Logger], level_name: str
+ ) -> LogPane:
+ """Setup a new build log pane."""
+ new_log_pane = LogPane(application=self, pane_title=title)
+ for logger in loggers:
+ new_log_pane.add_log_handler(logger, level_name=level_name)
+
+ # Set python log format to just the message itself.
+ new_log_pane.log_view.log_store.formatter = logging.Formatter(
+ '%(message)s'
+ )
+
+ new_log_pane.table_view = False
+
+ # Disable line wrapping for improved error visibility.
+ if new_log_pane.wrap_lines:
+ new_log_pane.toggle_wrap_lines()
+
+ # Blank right side toolbar text
+ new_log_pane._pane_subtitle = ' ' # pylint: disable=protected-access
+
+ # Make tab and shift-tab search for next and previous error
+ next_error_bindings = KeyBindings()
+
+ @next_error_bindings.add('s-tab')
+ def _previous_error(_event):
+ self.jump_to_error(backwards=True)
+
+ @next_error_bindings.add('tab')
+ def _next_error(_event):
+ self.jump_to_error()
+
+ existing_log_bindings: Optional[
+ KeyBindingsBase
+ ] = new_log_pane.log_content_control.key_bindings
+
+ key_binding_list: List[KeyBindingsBase] = []
+ if existing_log_bindings:
+ key_binding_list.append(existing_log_bindings)
+ key_binding_list.append(next_error_bindings)
+ new_log_pane.log_content_control.key_bindings = merge_key_bindings(
+ key_binding_list
+ )
+
+ # Only show a few buttons in the log pane toolbars.
+ new_buttons = []
+ for button in new_log_pane.bottom_toolbar.buttons:
+ if button.description in [
+ 'Search',
+ 'Save',
+ 'Follow',
+ 'Wrap',
+ 'Clear',
+ ]:
+ new_buttons.append(button)
+ new_log_pane.bottom_toolbar.buttons = new_buttons
+
+ self.window_manager.add_pane(new_log_pane)
+ return new_log_pane
+
+ def logs_redraw(self):
+ emit_time = time.time()
+ # Has enough time passed since last UI redraw due to new logs?
+ if emit_time > self._last_ui_update_time + self.log_ui_update_frequency:
+ # Update last log time
+ self._last_ui_update_time = emit_time
+
+ # Trigger Prompt Toolkit UI redraw.
+ self.redraw_ui()
+
def jump_to_error(self, backwards: bool = False) -> None:
- if not self.ninja_log_pane.log_view.search_text:
- self.ninja_log_pane.log_view.set_search_regex(
- '^FAILED: ', False, None)
+ if not self.root_log_pane.log_view.search_text:
+ self.root_log_pane.log_view.set_search_regex(
+ '^FAILED: ', False, None
+ )
if backwards:
- self.ninja_log_pane.log_view.search_backwards()
+ self.root_log_pane.log_view.search_backwards()
else:
- self.ninja_log_pane.log_view.search_forwards()
- self.ninja_log_pane.log_view.log_screen.reset_logs(
- log_index=self.ninja_log_pane.log_view.log_index)
+ self.root_log_pane.log_view.search_forwards()
+ self.root_log_pane.log_view.log_screen.reset_logs(
+ log_index=self.root_log_pane.log_view.log_index
+ )
- self.ninja_log_pane.log_view.move_selected_line_to_top()
+ self.root_log_pane.log_view.move_selected_line_to_top()
+
+ def refresh_layout(self) -> None:
+ self.window_manager.update_root_container_body()
def update_menu_items(self):
"""Required by the Window Manager Class."""
@@ -264,74 +642,174 @@ class WatchApp(PluginMixin):
def focus_on_container(self, pane):
"""Set application focus to a specific container."""
- self.application.layout.focus(pane)
+ # Try to focus on the given pane
+ try:
+ self.application.layout.focus(pane)
+ except ValueError:
+ # If the container can't be focused, focus on the first visible
+ # window pane.
+ self.window_manager.focus_first_visible_pane()
def focused_window(self):
"""Return the currently focused window."""
return self.application.layout.current_window
+ def focus_main_menu(self):
+ """Focus on the main menu.
+
+ Currently pw_watch has no main menu so focus on the first visible pane
+ instead."""
+ self.window_manager.focus_first_visible_pane()
+
+ def switch_to_root_log(self) -> None:
+ (
+ window_list,
+ pane_index,
+ ) = self.window_manager.find_window_list_and_pane_index(
+ self.root_log_pane
+ )
+ window_list.switch_to_tab(pane_index)
+
+ def switch_to_build_log(self, log_index: int) -> None:
+ pane = self.recipe_index_to_log_pane.get(log_index, None)
+ if not pane:
+ return
+
+ (
+ window_list,
+ pane_index,
+ ) = self.window_manager.find_window_list_and_pane_index(pane)
+ window_list.switch_to_tab(pane_index)
+
def command_runner_is_open(self) -> bool:
# pylint: disable=no-self-use
return False
- def clear_ninja_log(self) -> None:
- self.ninja_log_view.log_store.clear_logs()
- self.ninja_log_view._restart_filtering() # pylint: disable=protected-access
- self.ninja_log_view.view_mode_changed()
-
- def run_build(self):
- """Manually trigger a rebuild."""
- self.clear_ninja_log()
+ def all_log_panes(self) -> Iterable[LogPane]:
+ for pane in self.window_manager.active_panes():
+ if isinstance(pane, LogPane):
+ yield pane
+
+ def clear_log_panes(self) -> None:
+ """Erase all log pane content and turn on follow.
+
+ This is called whenever rebuilds occur. Either a manual build from
+ self.run_build or on file changes called from
+ pw_watch._handle_matched_event."""
+ for pane in self.all_log_panes():
+ pane.log_view.clear_visual_selection()
+ pane.log_view.clear_filters()
+ pane.log_view.log_store.clear_logs()
+ pane.log_view.view_mode_changed()
+ # Re-enable follow if needed
+ if not pane.log_view.follow:
+ pane.log_view.toggle_follow()
+
+ def run_build(self) -> None:
+ """Manually trigger a rebuild from the UI."""
+ self.clear_log_panes()
self.event_handler.rebuild()
- def rebuild_on_filechange(self):
- self.ninja_log_view.log_store.clear_logs()
- self.ninja_log_view.view_mode_changed()
-
- def get_statusbar_text(self):
- status = self.event_handler.status_message
- fragments = [('class:logo', 'Pigweed Watch')]
- is_building = False
- if status:
- fragments = [status]
- is_building = status[1].endswith('Building')
- separator = ('', ' ')
- self.status_bar_border_style = 'class:theme-fg-green'
-
- if is_building:
- percent = self.event_handler.current_build_percent
- percent *= 100
- fragments.append(separator)
- fragments.append(('ansicyan', '{:.0f}%'.format(percent)))
- self.status_bar_border_style = 'class:theme-fg-yellow'
-
- if self.event_handler.current_build_errors > 0:
- fragments.append(separator)
- fragments.append(('', 'Errors:'))
- fragments.append(
- ('ansired', str(self.event_handler.current_build_errors)))
- self.status_bar_border_style = 'class:theme-fg-red'
-
- if is_building:
- fragments.append(separator)
- fragments.append(('', self.event_handler.current_build_step))
-
- return fragments
-
- def get_resultbar_text(self) -> StyleAndTextTuples:
- result = self.event_handler.result_message
- if not result:
- result = [('', 'Loading...')]
- return result
-
- def exit(self, exit_code: int = 0) -> None:
- log_file = self.external_logfile
+ @property
+ def restart_on_changes(self) -> bool:
+ return self.event_handler.restart_on_changes
+
+ def toggle_restart_on_filechange(self) -> None:
+ self.event_handler.restart_on_changes = (
+ not self.event_handler.restart_on_changes
+ )
+ def get_status_bar_text(self) -> StyleAndTextTuples:
+ """Return formatted text for build status bar."""
+ formatted_text: StyleAndTextTuples = []
+
+ separator = ('', ' ')
+ name_width = self.event_handler.project_builder.max_name_width
+
+ # pylint: disable=protected-access
+ (
+ _window_list,
+ pane,
+ ) = self.window_manager._get_active_window_list_and_pane()
+ # pylint: enable=protected-access
+ restarting = BUILDER_CONTEXT.restart_flag
+
+ for i, cfg in enumerate(self.event_handler.project_builder):
+ # The build directory
+ name_style = ''
+ if not pane:
+ formatted_text.append(('', '\n'))
+ continue
+
+ # Dim the build name if disabled
+ if not cfg.enabled:
+ name_style = 'class:theme-fg-inactive'
+
+ # If this build tab is selected, highlight with cyan.
+ if pane.pane_title() == cfg.display_name:
+ name_style = 'class:theme-fg-cyan'
+
+ formatted_text.append(
+ to_checkbox(
+ cfg.enabled,
+ functools.partial(
+ mouse_handlers.on_click,
+ cfg.toggle_enabled,
+ ),
+ end=' ',
+ unchecked_style='class:checkbox',
+ checked_style='class:checkbox-checked',
+ )
+ )
+ formatted_text.append(
+ (
+ name_style,
+ f'{cfg.display_name}'.ljust(name_width),
+ functools.partial(
+ mouse_handlers.on_click,
+ functools.partial(self.switch_to_build_log, i),
+ ),
+ )
+ )
+ formatted_text.append(separator)
+ # Status
+ formatted_text.append(cfg.status.status_slug(restarting=restarting))
+ formatted_text.append(separator)
+ # Current stdout line
+ formatted_text.extend(cfg.status.current_step_formatted())
+ formatted_text.append(('', '\n'))
+
+ if not formatted_text:
+ formatted_text = [('', 'Loading...')]
+
+ self.set_tab_bar_colors()
+
+ return formatted_text
+
+ def set_tab_bar_colors(self) -> None:
+ restarting = BUILDER_CONTEXT.restart_flag
+
+ for cfg in BUILDER_CONTEXT.recipes:
+ pane = self.recipe_name_to_log_pane.get(cfg.display_name, None)
+ if not pane:
+ continue
+
+ pane.extra_tab_style = None
+ if not restarting and cfg.status.failed():
+ pane.extra_tab_style = 'class:theme-fg-red'
+
+ def exit(
+ self,
+ exit_code: int = 1,
+ log_after_shutdown: Optional[Callable[[], None]] = None,
+ ) -> None:
+ _LOG.info('Exiting...')
+ BUILDER_CONTEXT.ctrl_c_pressed = True
+
+ # Shut everything down after the prompt_toolkit app exits.
def _really_exit(future: asyncio.Future) -> NoReturn:
- if log_file:
- # Print a message showing where logs were saved to.
- print('Logs saved to: {}'.format(log_file.resolve()))
- sys.exit(future.result())
+ BUILDER_CONTEXT.restore_logging_and_shutdown(log_after_shutdown)
+ os._exit(future.result()) # pylint: disable=protected-access
if self.application.future:
self.application.future.add_done_callback(_really_exit)
@@ -357,6 +835,7 @@ class WatchApp(PluginMixin):
def input_box_not_focused(self) -> Condition:
"""Condition checking the focused control is not a text input field."""
+
@Condition
def _test() -> bool:
"""Check if the currently focused control is an input buffer.
@@ -366,7 +845,20 @@ class WatchApp(PluginMixin):
box. For example if the user presses enter when typing in
the search box, return False.
"""
- return not isinstance(self.application.layout.current_control,
- BufferControl)
+ return not isinstance(
+ self.application.layout.current_control, BufferControl
+ )
return _test
+
+ def modal_window_is_open(self):
+ """Return true if any modal window or dialog is open."""
+ floating_window_is_open = (
+ self.user_guide_window.show_window or self.quit_dialog.show_dialog
+ )
+
+ floating_plugin_is_open = any(
+ plugin.show_pane for plugin in self.floating_window_plugins
+ )
+
+ return floating_window_is_open or floating_plugin_is_open
diff --git a/pw_watch/py/setup.cfg b/pw_watch/py/setup.cfg
index 504489c0b..12853f7da 100644
--- a/pw_watch/py/setup.cfg
+++ b/pw_watch/py/setup.cfg
@@ -21,7 +21,8 @@ description = Pigweed automatic builder
[options]
packages = find:
zip_safe = False
-install_requires = httpwatcher; pw_cli; watchdog>=2.1.0
+install_requires =
+ watchdog>=2.1.0
[options.package_data]
pw_watch = py.typed
diff --git a/pw_watch/py/watch_test.py b/pw_watch/py/watch_test.py
index 6c9323af7..4e5fde7a8 100755
--- a/pw_watch/py/watch_test.py
+++ b/pw_watch/py/watch_test.py
@@ -23,6 +23,7 @@ from pw_watch import watch
class TestMinimalWatchDirectories(unittest.TestCase):
"""Tests for pw_watch.watch.minimal_watch_directories."""
+
def setUp(self):
self._tempdir = tempfile.TemporaryDirectory()
self._root = Path(self._tempdir.name)
@@ -38,10 +39,12 @@ class TestMinimalWatchDirectories(unittest.TestCase):
subdirectories_to_watch = []
ans_subdirectories_to_watch = [(self._root, False)]
subdirectories_to_watch = watch.minimal_watch_directories(
- self._root, 'f1')
+ self._root, 'f1'
+ )
- self.assertEqual(set(subdirectories_to_watch),
- set(ans_subdirectories_to_watch))
+ self.assertEqual(
+ set(subdirectories_to_watch), set(ans_subdirectories_to_watch)
+ )
def test_non_exist_directories_to_exclude(self):
subdirectories_to_watch = []
@@ -53,10 +56,12 @@ class TestMinimalWatchDirectories(unittest.TestCase):
(self._root, False),
]
subdirectories_to_watch = watch.minimal_watch_directories(
- self._root, exclude_list)
+ self._root, exclude_list
+ )
- self.assertEqual(set(subdirectories_to_watch),
- set(ans_subdirectories_to_watch))
+ self.assertEqual(
+ set(subdirectories_to_watch), set(ans_subdirectories_to_watch)
+ )
def test_one_layer_directories(self):
subdirectories_to_watch = []
@@ -71,10 +76,12 @@ class TestMinimalWatchDirectories(unittest.TestCase):
(self._root, False),
]
subdirectories_to_watch = watch.minimal_watch_directories(
- self._root, exclude_list)
+ self._root, exclude_list
+ )
- self.assertEqual(set(subdirectories_to_watch),
- set(ans_subdirectories_to_watch))
+ self.assertEqual(
+ set(subdirectories_to_watch), set(ans_subdirectories_to_watch)
+ )
def test_two_layers_direcories(self):
subdirectories_to_watch = []
@@ -91,10 +98,12 @@ class TestMinimalWatchDirectories(unittest.TestCase):
(self._root / 'f1', False),
]
subdirectories_to_watch = watch.minimal_watch_directories(
- self._root, exclude_list)
+ self._root, exclude_list
+ )
- self.assertEqual(set(subdirectories_to_watch),
- set(ans_subdirectories_to_watch))
+ self.assertEqual(
+ set(subdirectories_to_watch), set(ans_subdirectories_to_watch)
+ )
def test_empty_exclude_list(self):
subdirectories_to_watch = []
@@ -110,10 +119,12 @@ class TestMinimalWatchDirectories(unittest.TestCase):
(self._root, False),
]
subdirectories_to_watch = watch.minimal_watch_directories(
- self._root, exclude_list)
+ self._root, exclude_list
+ )
- self.assertEqual(set(subdirectories_to_watch),
- set(ans_subdirectories_to_watch))
+ self.assertEqual(
+ set(subdirectories_to_watch), set(ans_subdirectories_to_watch)
+ )
def test_multiple_directories_in_exclude_list(self):
"""test case for multiple directories to exclude"""
@@ -140,10 +151,12 @@ class TestMinimalWatchDirectories(unittest.TestCase):
(self._root / 'f3', False),
]
subdirectories_to_watch = watch.minimal_watch_directories(
- self._root, exclude_list)
+ self._root, exclude_list
+ )
- self.assertEqual(set(subdirectories_to_watch),
- set(ans_subdirectories_to_watch))
+ self.assertEqual(
+ set(subdirectories_to_watch), set(ans_subdirectories_to_watch)
+ )
def test_nested_sibling_exclusion(self):
subdirectories_to_watch = []
@@ -167,10 +180,12 @@ class TestMinimalWatchDirectories(unittest.TestCase):
(self._root / 'f1/f1/f1/f1', False),
]
subdirectories_to_watch = watch.minimal_watch_directories(
- self._root, exclude_list)
+ self._root, exclude_list
+ )
- self.assertEqual(set(subdirectories_to_watch),
- set(ans_subdirectories_to_watch))
+ self.assertEqual(
+ set(subdirectories_to_watch), set(ans_subdirectories_to_watch)
+ )
if __name__ == '__main__':
diff --git a/pw_web_ui/BUILD.gn b/pw_web/BUILD.gn
index dd021e82f..a4d5ebe10 100644
--- a/pw_web_ui/BUILD.gn
+++ b/pw_web/BUILD.gn
@@ -15,7 +15,11 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
pw_doc_group("docs") {
sources = [ "docs.rst" ]
}
+
+pw_test_group("tests") {
+}
diff --git a/pw_web/README.md b/pw_web/README.md
new file mode 100644
index 000000000..64c95e43a
--- /dev/null
+++ b/pw_web/README.md
@@ -0,0 +1 @@
+# pw\_web: Tools for building web UIs
diff --git a/pw_web/docs.rst b/pw_web/docs.rst
new file mode 100644
index 000000000..a6eddf98e
--- /dev/null
+++ b/pw_web/docs.rst
@@ -0,0 +1,199 @@
+.. _module-pw_web:
+
+---------
+pw_web
+---------
+
+Pigweed provides an NPM package with modules to build web apps for Pigweed
+devices.
+
+Also included is a basic React app that demonstrates using the npm package.
+
+Getting Started
+===============
+
+Installation
+-------------
+If you have a bundler set up, you can install ``pigweedjs`` in your web application by:
+
+.. code:: bash
+
+ $ npm install --save pigweedjs
+
+
+After installing, you can import modules from ``pigweedjs`` in this way:
+
+.. code:: javascript
+
+ import { pw_rpc, pw_tokenizer, Device, WebSerial } from 'pigweedjs';
+
+Import Directly in HTML
+^^^^^^^^^^^^^^^^^^^^^^^
+
+If you don't want to set up a bundler, you can also load Pigweed directly in
+your HTML page by:
+
+.. code:: html
+
+ <script src="https://unpkg.com/pigweedjs@0.0.5/dist/index.umd.js"></script>
+ <script>
+ const { pw_rpc, pw_hdlc, Device, WebSerial } from Pigweed;
+ </script>
+
+Getting Started
+---------------
+Easiest way to get started is to build pw_system demo and run it on a STM32F429I
+Discovery board. Discovery board is Pigweed's primary target for development.
+Refer to :ref:`target documentation<target-stm32f429i-disc1-stm32cube>` for
+instructions on how to build the demo and try things out.
+
+``pigweedjs`` provides a ``Device`` API which simplifies common tasks. Here is
+an example to connect to device and call ``EchoService.Echo`` RPC service.
+
+.. code:: html
+
+ <h1>Hello Pigweed</h1>
+ <button onclick="connect()">Connect</button>
+ <button onclick="echo()">Echo RPC</button>
+ <br /><br />
+ <code></code>
+ <script src="https://unpkg.com/pigweedjs@0.0.5/dist/index.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs@0.0.5/dist/protos/collection.umd.js"></script>
+ <script>
+ const { Device } = Pigweed;
+ const { ProtoCollection } = PigweedProtoCollection;
+
+ const device = new Device(new ProtoCollection());
+
+ async function connect(){
+ await device.connect();
+ }
+
+ async function echo(){
+ const [status, response] = await device.rpcs.pw.rpc.EchoService.Echo("Hello");
+ document.querySelector('code').innerText = "Response: " + response;
+ }
+ </script>
+
+pw_system demo uses ``pw_log_rpc``; an RPC-based logging solution. pw_system
+also uses pw_tokenizer to tokenize strings and save device space. Below is an
+example that streams logs using the ``Device`` API.
+
+.. code:: html
+
+ <h1>Hello Pigweed</h1>
+ <button onclick="connect()">Connect</button>
+ <br /><br />
+ <code></code>
+ <script src="https://unpkg.com/pigweedjs@0.0.5/dist/index.umd.js"></script>
+ <script src="https://unpkg.com/pigweedjs@0.0.5/dist/protos/collection.umd.js"></script>
+ <script>
+ const { Device, pw_tokenizer } = Pigweed;
+ const { ProtoCollection } = PigweedProtoCollection;
+ const tokenDBCsv = `...` // Load token database here
+
+ const device = new Device(new ProtoCollection());
+ const detokenizer = new pw_tokenizer.Detokenizer(tokenDBCsv);
+
+ async function connect(){
+ await device.connect();
+ const call = device.rpcs.pw.log.Logs.Listen((msg) => {
+ msg.getEntriesList().forEach((entry) => {
+ const frame = entry.getMessage();
+ const detokenized = detokenizer.detokenizeUint8Array(frame);
+ document.querySelector('code').innerHTML += detokenized + "<br/>";
+ });
+ })
+ }
+ </script>
+
+The above example requires a token database in CSV format. You can generate one
+from the pw_system's ``.elf`` file by running:
+
+.. code:: bash
+
+ $ pw_tokenizer/py/pw_tokenizer/database.py create \
+ --database db.csv out/stm32f429i_disc1_stm32cube.size_optimized/obj/pw_system/bin/system_example.elf
+
+You can then load this CSV in JavaScript using ``fetch()`` or by just copying
+the contents into the ``tokenDBCsv`` variable in the above example.
+
+Modules
+=======
+
+Device
+------
+Device class is a helper API to connect to a device over serial and call RPCs
+easily.
+
+To initialize device, it needs a ``ProtoCollection`` instance. ``pigweedjs``
+includes a default one which you can use to get started, you can also generate
+one from your own ``.proto`` files using ``pw_proto_compiler``.
+
+``Device`` goes through all RPC methods in the provided ProtoCollection. For
+each RPC, it reads all the fields in ``Request`` proto and generates a
+JavaScript function that accepts all the fields as it's arguments. It then makes
+this function available under ``rpcs.*`` namespaced by its package name.
+
+Device has following public API:
+
+- ``constructor(ProtoCollection, WebSerialTransport <optional>, rpcAddress <optional>)``
+- ``connect()`` - Shows browser's WebSerial connection dialog and let's user
+ make device selection
+- ``rpcs.*`` - Device API enumerates all RPC services and methods present in the
+ provided proto collection and makes them available as callable functions under
+ ``rpcs``. Example: If provided proto collection includes Pigweed's Echo
+ service ie. ``pw.rpc.EchoService.Echo``, it can be triggered by calling
+ ``device.rpcs.pw.rpc.EchoService.Echo("some message")``. The functions return
+ a ``Promise`` that resolves an array with status and response.
+
+WebSerialTransport
+------------------
+
+To help with connecting to WebSerial and listening for serial data, a helper
+class is also included under ``WebSerial.WebSerialTransport``. Here is an
+example usage:
+
+.. code:: javascript
+
+ import { WebSerial, pw_hdlc } from 'pigweedjs';
+
+ const transport = new WebSerial.WebSerialTransport();
+ const decoder = new pw_hdlc.Decoder();
+
+ // Present device selection prompt to user
+ await transport.connect();
+
+ // Listen and decode HDLC frames
+ transport.chunks.subscribe((item) => {
+ const decoded = decoder.process(item);
+ for (const frame of decoded) {
+ if (frame.address === 1) {
+ const decodedLine = new TextDecoder().decode(frame.data);
+ console.log(decodedLine);
+ }
+ }
+ });
+
+
+Individual Modules
+==================
+Following Pigweed modules are included in the NPM package:
+
+- `pw_hdlc <https://pigweed.dev/pw_hdlc/#typescript>`_
+- `pw_rpc <https://pigweed.dev/pw_rpc/ts/>`_
+- `pw_tokenizer <https://pigweed.dev/pw_tokenizer/#typescript>`_
+- `pw_transfer <https://pigweed.dev/pw_transfer/#typescript>`_
+
+Web Console
+===========
+
+Pigweed includes a web console that demonstrates `pigweedjs` usage in a
+React-based web app. Web console includes a log viewer and a REPL that supports
+autocomplete. Here's how to run the web console locally:
+
+.. code:: bash
+
+ $ cd pw_web/webconsole
+ $ npm install
+ $ npm run dev
diff --git a/pw_web/webconsole/.eslintrc.json b/pw_web/webconsole/.eslintrc.json
new file mode 100644
index 000000000..bffb357a7
--- /dev/null
+++ b/pw_web/webconsole/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "next/core-web-vitals"
+}
diff --git a/pw_web/webconsole/.gitignore b/pw_web/webconsole/.gitignore
new file mode 100644
index 000000000..81a5e1e63
--- /dev/null
+++ b/pw_web/webconsole/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+next-env.d.ts
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
diff --git a/pw_web/webconsole/common/device.ts b/pw_web/webconsole/common/device.ts
new file mode 100644
index 000000000..5ba735b90
--- /dev/null
+++ b/pw_web/webconsole/common/device.ts
@@ -0,0 +1,30 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {Device} from "pigweedjs";
+import {createDefaultProtoCollection} from "./protos";
+
+/**
+ * Returns an instance of Device, ensures there is only one Device in
+ * current session.
+ *
+ * We do this to avoid multiple clients listening on single serial port.
+ */
+export default async function SingletonDevice(): Promise<Device> {
+ if ((window as any).device === undefined) {
+ const protoCollection = await createDefaultProtoCollection();
+ (window as any).device = new Device(protoCollection);
+ }
+ return (window as any).device;
+}
diff --git a/pw_web/webconsole/common/logService.ts b/pw_web/webconsole/common/logService.ts
new file mode 100644
index 000000000..106520c90
--- /dev/null
+++ b/pw_web/webconsole/common/logService.ts
@@ -0,0 +1,26 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {Device} from "pigweedjs";
+
+export async function listenToDefaultLogService(
+ device: Device,
+ onFrame: (frame: Uint8Array) => void) {
+ const call = device.rpcs.pw.log.Logs.Listen((msg: any) => {
+ msg.getEntriesList().forEach((entry: any) => onFrame(entry.getMessage()));
+ })
+ return () => {
+ call.cancel();
+ };
+}
diff --git a/pw_web/webconsole/common/protos.ts b/pw_web/webconsole/common/protos.ts
new file mode 100644
index 000000000..3524ad5ec
--- /dev/null
+++ b/pw_web/webconsole/common/protos.ts
@@ -0,0 +1,20 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+
+export async function createDefaultProtoCollection() {
+ // @ts-ignore
+ const ProtoCollection = await import("pigweedjs/protos/collection.umd");
+ return new ProtoCollection.ProtoCollection();
+}
diff --git a/pw_web/webconsole/components/connect.tsx b/pw_web/webconsole/components/connect.tsx
new file mode 100644
index 000000000..17961c36c
--- /dev/null
+++ b/pw_web/webconsole/components/connect.tsx
@@ -0,0 +1,35 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import Button from '@mui/material/Button';
+import {Alert} from '@mui/material';
+import {Device, WebSerial} from "pigweedjs";
+import {useState} from 'react';
+import DeviceFactory from "../common/device";
+type WebSerialTransport = WebSerial.WebSerialTransport
+
+interface LogProps {
+ onConnection: (device: Device) => void
+}
+
+export default function BtnConnect({onConnection}: LogProps) {
+ const [connected, setConnected] = useState(false);
+ if (connected) return (<Alert severity="success">Connected!</Alert>)
+ return (<Button onClick={async () => {
+ const device = await DeviceFactory();
+ await device.connect();
+ setConnected(true);
+ onConnection(device);
+ }} variant="contained">Connect</Button>)
+}
diff --git a/pw_web/webconsole/components/log.tsx b/pw_web/webconsole/components/log.tsx
new file mode 100644
index 000000000..4ad1efe8b
--- /dev/null
+++ b/pw_web/webconsole/components/log.tsx
@@ -0,0 +1,161 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {useEffect, useRef, useState} from "react";
+import {pw_tokenizer, Device} from "pigweedjs";
+import {AutoSizer, Table, Column} from 'react-virtualized';
+import {listenToDefaultLogService} from "../common/logService";
+import 'react-virtualized/styles.css';
+import styles from "../styles/log.module.css";
+
+type Detokenizer = pw_tokenizer.Detokenizer;
+
+interface LogProps {
+ device: Device | undefined,
+ tokenDB: string | undefined
+}
+
+interface LogEntry {
+ msg: string,
+ timestamp: number,
+ humanTime: string,
+ module: string,
+ file: string
+}
+
+function parseLogMsg(msg: string): LogEntry {
+ const pairs = msg.split("■").slice(1).map(pair => pair.split("♦"));
+
+ // Not a valid message, print as-is.
+ if (pairs.length === 0) {
+ return {
+ msg,
+ module: "",
+ file: "",
+ timestamp: Date.now(),
+ humanTime: new Date(Date.now()).toLocaleTimeString("en-US")
+ }
+ }
+
+ let map: any = {};
+ pairs.forEach(pair => map[pair[0]] = pair[1])
+ return {
+ msg: map.msg,
+ module: map.module,
+ file: map.file,
+ timestamp: Date.now(),
+ humanTime: new Date(Date.now()).toLocaleTimeString("en-US")
+ }
+}
+
+const keyToDisplayName: {[key: string]: string} = {
+ "msg": "Message",
+ "humanTime": "Time",
+ "module": "Module",
+ "file": "File"
+}
+
+export default function Log({device, tokenDB}: LogProps) {
+ const [logs, setLogs] = useState<LogEntry[]>([]);
+ const [detokenizer, setDetokenizer] = useState<Detokenizer | null>(null);
+ const logTable = useRef<Table | null>(null);
+ const _headerRenderer = ({dataKey, sortBy, sortDirection}: any) => {
+ return (
+ <div>
+ {keyToDisplayName[dataKey]}
+ </div>
+ );
+ }
+
+ const processFrame = (frame: Uint8Array) => {
+ if (detokenizer) {
+ const detokenized = detokenizer.detokenizeUint8Array(frame);
+ setLogs(oldLogs => [...oldLogs, parseLogMsg(detokenized)]);
+ }
+ else {
+ const decoded = new TextDecoder().decode(frame);
+ setLogs(oldLogs => [...oldLogs, parseLogMsg(decoded)]);
+ }
+ setTimeout(() => {
+ logTable.current!.scrollToRow(logs.length - 1);
+ }, 100);
+ }
+
+ useEffect(() => {
+ if (device) {
+ let cleanupFn: () => void;
+ listenToDefaultLogService(device, processFrame).then((unsub) => cleanupFn = unsub);
+ return () => {
+ if (cleanupFn) cleanupFn();
+ }
+ }
+ }, [device, detokenizer]);
+
+ useEffect(() => {
+ if (tokenDB && tokenDB.length > 0) {
+ const detokenizer = new pw_tokenizer.Detokenizer(tokenDB);
+ setDetokenizer(detokenizer);
+ }
+ }, [tokenDB])
+
+ return (
+ <>
+ {/* @ts-ignore */}
+ <AutoSizer>
+ {({height, width}) => (
+ <>
+ {/* @ts-ignore */}
+ <Table
+ className={styles.logsContainer}
+ headerHeight={40}
+ height={height}
+ rowCount={logs.length}
+ rowGetter={({index}) => logs[index]}
+ rowHeight={30}
+ ref={logTable}
+ width={width}
+ >
+ {/* @ts-ignore */}
+ <Column
+ dataKey="humanTime"
+ width={190}
+ headerRenderer={_headerRenderer}
+ />
+ {/* @ts-ignore */}
+ <Column
+ dataKey="msg"
+ flexGrow={1}
+ width={290}
+ headerRenderer={_headerRenderer}
+ />
+ {/* @ts-ignore */}
+ <Column
+ dataKey="module"
+ width={190}
+ headerRenderer={_headerRenderer}
+ />
+ {/* @ts-ignore */}
+ <Column
+ dataKey="file"
+ flexGrow={1}
+ width={190}
+ headerRenderer={_headerRenderer}
+ />
+ </Table>
+ </>
+ )}
+ </AutoSizer>
+ </>
+ )
+}
diff --git a/pw_web/webconsole/components/repl/autocomplete.ts b/pw_web/webconsole/components/repl/autocomplete.ts
new file mode 100644
index 000000000..24d7efb3a
--- /dev/null
+++ b/pw_web/webconsole/components/repl/autocomplete.ts
@@ -0,0 +1,107 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {CompletionContext} from '@codemirror/autocomplete'
+import {syntaxTree} from '@codemirror/language'
+import {Device} from "pigweedjs";
+
+const completePropertyAfter = ['PropertyName', '.', '?.']
+const dontCompleteIn = [
+ 'TemplateString',
+ 'LineComment',
+ 'BlockComment',
+ 'VariableDefinition',
+ 'PropertyDefinition'
+]
+var objectPath = require("object-path");
+
+export function completeFromGlobalScope(context: CompletionContext) {
+ let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
+
+ if (
+ completePropertyAfter.includes(nodeBefore.name) &&
+ nodeBefore.parent?.name == 'MemberExpression'
+ ) {
+ let object = nodeBefore.parent.getChild('Expression')
+ if (object?.name == 'VariableName') {
+ let from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from
+ let variableName = context.state.sliceDoc(object.from, object.to)
+ // @ts-ignore
+ if (typeof window[variableName] == 'object') {
+ // @ts-ignore
+ return completeProperties(from, window[variableName])
+ }
+ }
+ else if (object?.name == 'MemberExpression') {
+ let from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from
+ let variableName = context.state.sliceDoc(object.from, object.to)
+ let variable = resolveWindowVariable(variableName);
+ // @ts-ignore
+ if (typeof variable == 'object') {
+ // @ts-ignore
+ return completeProperties(from, variable, variableName)
+ }
+ }
+ } else if (nodeBefore.name == 'VariableName') {
+ return completeProperties(nodeBefore.from, window)
+ } else if (context.explicit && !dontCompleteIn.includes(nodeBefore.name)) {
+ return completeProperties(context.pos, window)
+ }
+ return null
+}
+
+function completeProperties(from: number, object: Object, variableName?: string) {
+ let options = []
+ for (let name in object) {
+ // @ts-ignore
+ if (object[name] instanceof Function && variableName) {
+ debugger;
+ options.push({
+ label: name,
+ // @ts-ignore
+ detail: getFunctionDetailText(`${variableName}.${name}`),
+ type: 'function'
+ })
+ }
+ else {
+ options.push({
+ label: name,
+ type: 'variable'
+ })
+ }
+
+ }
+ return {
+ from,
+ options,
+ validFor: /^[\w$]*$/
+ }
+}
+
+function resolveWindowVariable(variableName: string) {
+ if (objectPath.has(window, variableName)) {
+ return objectPath.get(window, variableName);
+ }
+}
+
+function getFunctionDetailText(fullExpression: string): string {
+ if (fullExpression.startsWith("device.rpcs.")) {
+ fullExpression = fullExpression.replace("device.rpcs.", "");
+ }
+ const args = ((window as any).device as Device).getMethodArguments(fullExpression);
+ if (args) {
+ return `(${args.join(", ")})`;
+ }
+ return "";
+}
diff --git a/pw_web/webconsole/components/repl/basicSetup.ts b/pw_web/webconsole/components/repl/basicSetup.ts
new file mode 100644
index 000000000..e3f0640ce
--- /dev/null
+++ b/pw_web/webconsole/components/repl/basicSetup.ts
@@ -0,0 +1,69 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {
+ keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor,
+ rectangularSelection, crosshairCursor,
+ highlightActiveLineGutter
+} from "@codemirror/view"
+import {Extension, EditorState} from "@codemirror/state"
+import {
+ defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching,
+ foldGutter, foldKeymap
+} from "@codemirror/language"
+import {defaultKeymap, history, historyKeymap} from "@codemirror/commands"
+import {searchKeymap, highlightSelectionMatches} from "@codemirror/search"
+import {autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap} from "@codemirror/autocomplete"
+import {lintKeymap} from "@codemirror/lint"
+
+const defaultKeymapMinusEnterAndArrowUpDown = defaultKeymap.map((keymap) => {
+ if (keymap.key === "Enter") {
+ return {...keymap, key: "Shift-Enter"};
+ }
+ if (keymap.key === "ArrowUp") {
+ return {...keymap, key: "Shift-ArrowUp"}
+ }
+ if (keymap.key === "ArrowDown") {
+ return {...keymap, key: "Shift-ArrowDown"}
+ }
+ return keymap;
+});
+
+export const basicSetup: Extension = (() => [
+ highlightActiveLineGutter(),
+ highlightSpecialChars(),
+ history(),
+ foldGutter(),
+ drawSelection(),
+ dropCursor(),
+ EditorState.allowMultipleSelections.of(true),
+ indentOnInput(),
+ syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
+ bracketMatching(),
+ closeBrackets(),
+ autocompletion(),
+ rectangularSelection(),
+ crosshairCursor(),
+ highlightActiveLine(),
+ highlightSelectionMatches(),
+ keymap.of([
+ ...closeBracketsKeymap,
+ ...defaultKeymapMinusEnterAndArrowUpDown,
+ ...searchKeymap,
+ ...historyKeymap,
+ ...foldKeymap,
+ ...completionKeymap,
+ ...lintKeymap
+ ])
+])()
diff --git a/pw_web/webconsole/components/repl/index.tsx b/pw_web/webconsole/components/repl/index.tsx
new file mode 100644
index 000000000..a48e8cd57
--- /dev/null
+++ b/pw_web/webconsole/components/repl/index.tsx
@@ -0,0 +1,202 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {useEffect, useState} from "react";
+import {Device} from "pigweedjs";
+import {EditorView} from "codemirror"
+import {basicSetup} from "./basicSetup";
+import {javascript, javascriptLanguage} from "@codemirror/lang-javascript"
+import {placeholder} from "@codemirror/view";
+import {oneDark} from "@codemirror/theme-one-dark";
+import {keymap} from "@codemirror/view"
+import {Extension} from "@codemirror/state"
+import {completeFromGlobalScope} from "./autocomplete";
+import LocalStorageArray from "./localStorageArray";
+import "xterm/css/xterm.css";
+import styles from "../../styles/repl.module.css";
+
+const isSSR = () => typeof window === 'undefined';
+
+interface ReplProps {
+ device: Device | undefined
+}
+
+const globalJavaScriptCompletions = javascriptLanguage.data.of({
+ autocomplete: completeFromGlobalScope
+})
+
+const createTerminal = async (container: HTMLElement) => {
+ const {Terminal} = await import('xterm');
+ const {FitAddon} = await import('xterm-addon-fit');
+ const terminal = new Terminal({
+ // cursorBlink: true,
+ theme: {
+ background: '#2c313a'
+ }
+ });
+ terminal.open(container);
+
+ const fitAddon = new FitAddon();
+ terminal.loadAddon(fitAddon);
+ fitAddon.fit();
+ return terminal;
+};
+
+const createPlaceholderText = () => {
+ var div = document.createElement('div');
+ div.innerHTML = `Type code and hit Enter to run. See <b>[?]</b> for more info.`
+ return div;
+}
+
+const createEditor = (container: HTMLElement, enterKeyMap: Extension) => {
+ let view = new EditorView({
+ extensions: [basicSetup, javascript(), placeholder(createPlaceholderText()), oneDark, globalJavaScriptCompletions, enterKeyMap],
+ parent: container,
+ });
+ return view;
+}
+
+let currentCommandHistoryIndex = -1;
+let historyStorage: LocalStorageArray;
+if (typeof window !== 'undefined') {
+ historyStorage = new LocalStorageArray();
+}
+
+export default function Repl({device}: ReplProps) {
+ const [terminal, setTerminal] = useState<any>(null);
+ const [codeEditor, setCodeEditor] = useState<EditorView | null>(null);
+
+ useEffect(() => {
+ let cleanupFns: {(): void; (): void;}[] = [];
+ if (!terminal && !isSSR() && device) {
+ const futureTerm = createTerminal(document.querySelector('#repl-log-container')!);
+ futureTerm.then(async (term) => {
+ cleanupFns.push(() => {
+ term.dispose();
+ setTerminal(null);
+ });
+ setTerminal(term);
+ });
+
+ return () => {
+ cleanupFns.forEach(fn => fn());
+ }
+ }
+ else if (terminal && !device) {
+ terminal.dispose();
+ setTerminal(null);
+ }
+ }, [device]);
+
+ useEffect(() => {
+ if (!terminal) return;
+ const enterKeyMap = {
+ key: "Enter",
+ run(view: EditorView) {
+ if (view.state.doc.toString().trim().length === 0) return true;
+ try {
+ // To run eval() in global scope, we do (1, eval) here.
+ const cmdOutput = (1, eval)(view.state.doc.toString());
+ // Check if eval returned a promise
+ if (typeof cmdOutput === "object" && cmdOutput.then !== undefined) {
+ cmdOutput
+ .then((result: any) => {
+ terminal.write(`Promise { ${result} }\r\n`);
+ })
+ .catch((e: any) => {
+ if (e instanceof Error) {
+ terminal.write(`\x1b[31;1mUncaught (in promise) Error: ${e.message}\x1b[0m\r\n`)
+ }
+ else {
+ terminal.write(`\x1b[31;1mUncaught (in promise) ${e}\x1b[0m\r\n`)
+ }
+ });
+ }
+ else {
+ terminal.write(cmdOutput + "\r\n");
+ }
+ }
+ catch (e) {
+ if (e instanceof Error) terminal.write(`\x1b[31;1m${e.message}\x1b[0m\r\n`)
+ }
+
+ currentCommandHistoryIndex = -1;
+ historyStorage.unshift(view.state.doc.toString());
+
+ // Clear text editor
+ const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: ""}});
+ view.dispatch(transaction);
+ return true;
+ }
+ };
+
+ const upKeyMap = {
+ key: "ArrowUp",
+ run(view: EditorView) {
+ currentCommandHistoryIndex++;
+ if (historyStorage.data[currentCommandHistoryIndex]) {
+ // set text editor
+ const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: historyStorage.data[currentCommandHistoryIndex]}});
+ view.dispatch(transaction);
+ }
+ else {
+ currentCommandHistoryIndex = historyStorage.data.length - 1;
+ }
+ return true;
+ }
+ };
+
+ const downKeyMap = {
+ key: "ArrowDown",
+ run(view: EditorView) {
+ currentCommandHistoryIndex--;
+ if (currentCommandHistoryIndex <= -1) {
+ currentCommandHistoryIndex = -1;
+ const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: ""}});
+ view.dispatch(transaction);
+ }
+ else if (historyStorage.data[currentCommandHistoryIndex]) {
+ // set text editor
+ const transaction = view.state.update({changes: {from: 0, to: view.state.doc.length, insert: historyStorage.data[currentCommandHistoryIndex]}});
+ view.dispatch(transaction);
+ }
+ return true;
+ }
+ };
+
+ const keymaps = keymap.of([enterKeyMap, upKeyMap, downKeyMap]);
+ let view = createEditor(document.querySelector('#repl-editor-container')!, keymaps);
+ return () => view.destroy();
+ }, [terminal]);
+
+ return (
+ <div className={styles.container}>
+ <div id="repl-log-container" className={styles.logs}></div>
+ <div className={styles.replWithCaret}>
+ <div>
+ <div className={styles.tooltip}>?
+ <span className={styles.tooltiptext}>
+ This REPL runs JavaScript.
+ You can navigate previous commands using <span>Up</span> and <span>Down</span> arrow keys.
+ <br /><br />
+ Call device RPCs using <span>device.rpcs.*</span> API.
+ </span>
+ </div>
+ <span className={styles.caret}>{`> `}</span>
+ </div>
+ <div id="repl-editor-container" className={styles.editor}></div>
+ </div>
+ </div>
+ )
+}
diff --git a/pw_web/webconsole/components/repl/localStorageArray.ts b/pw_web/webconsole/components/repl/localStorageArray.ts
new file mode 100644
index 000000000..318d8ff77
--- /dev/null
+++ b/pw_web/webconsole/components/repl/localStorageArray.ts
@@ -0,0 +1,40 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+export default class LocalStorageArray {
+ data: string[] = [];
+ maxSize: number;
+ key: string = "__pw_repl_history";
+
+ constructor(maxSize: number = 4) {
+ this.maxSize = maxSize;
+ const curHistory = localStorage.getItem(this.key);
+ if (curHistory) {
+ this.data = JSON.parse(localStorage.getItem(this.key)!)
+ }
+ }
+
+ unshift(data: string) {
+ this.data.unshift(data);
+ if (this.data.length > this.maxSize) {
+ this.data = this.data.slice(0, this.maxSize);
+ }
+ localStorage.setItem(this.key, JSON.stringify(this.data));
+ }
+
+ clear() {
+ this.data = [];
+ localStorage.removeItem(this.key);
+ }
+}
diff --git a/pw_web/webconsole/components/uploadDb.tsx b/pw_web/webconsole/components/uploadDb.tsx
new file mode 100644
index 000000000..11a6ea9a5
--- /dev/null
+++ b/pw_web/webconsole/components/uploadDb.tsx
@@ -0,0 +1,79 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import Button from '@mui/material/Button';
+import {Alert} from '@mui/material';
+import {useState, useRef} from 'react';
+
+interface Props {
+ onUpload: (db: string) => void
+}
+
+function testTokenDB(tokenCsv: string) {
+ const lines = tokenCsv.trim().split(/\r?\n/).map(line => line.split(/,/));
+ lines.forEach((line) => {
+ // CSV has no columns or has malformed number.
+ if (line.length < 2 || !/^[a-fA-F0-9]+$/.test(line[0])) {
+ throw new Error("Not a valid token database.")
+ }
+ });
+}
+
+export default function BtnUploadDB({onUpload}: Props) {
+ const [uploaded, setUploaded] = useState(false);
+ const [error, setError] = useState("");
+ const uploadInputRef = useRef<HTMLInputElement>(null);
+
+ if (uploaded) return (<Alert severity="success">DB Loaded</Alert>)
+ return (
+ <>
+ <input
+ ref={uploadInputRef}
+ type="file"
+ accept="text/*"
+ style={{display: "none"}}
+ onChange={async e => {
+ const tokenCsv = await readFile(e.target.files![0]);
+ try {
+ testTokenDB(tokenCsv);
+ onUpload(tokenCsv);
+ setUploaded(true);
+ setError("");
+ }
+ catch (e: any) {
+ if (e instanceof Error) setError(e.message);
+ else setError("Error loading token database.");
+ }
+ }}
+ />
+ <Button
+ onClick={() => uploadInputRef.current && uploadInputRef.current.click()}
+ variant="contained">
+ Upload token database
+ </Button>
+ {error && <Alert severity="error">{error}</Alert>}
+ </>
+ )
+}
+
+function readFile(file: Blob): Promise<string> {
+ return new Promise((resolve, reject) => {
+ if (!file) return resolve('');
+ const reader = new FileReader();
+ reader.onload = function (e) {
+ resolve(String(e.target!.result));
+ };
+ reader.readAsText(file);
+ });
+}
diff --git a/pw_web/webconsole/next.config.js b/pw_web/webconsole/next.config.js
new file mode 100644
index 000000000..6ac9a17f5
--- /dev/null
+++ b/pw_web/webconsole/next.config.js
@@ -0,0 +1,21 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ reactStrictMode: true,
+ swcMinify: true,
+}
+
+ module.exports = nextConfig
diff --git a/pw_web/webconsole/package.json b/pw_web/webconsole/package.json
new file mode 100644
index 000000000..91b7e1f2a
--- /dev/null
+++ b/pw_web/webconsole/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "webconsole",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.1.0",
+ "@codemirror/lang-javascript": "^6.0.2",
+ "@codemirror/language": "^6.2.1",
+ "@codemirror/theme-one-dark": "^6.0.0",
+ "@emotion/react": "^11.10.0",
+ "@emotion/styled": "^11.10.0",
+ "@mui/material": "^5.9.3",
+ "codemirror": "^6.0.1",
+ "next": "12.2.3",
+ "object-path": "^0.11.8",
+ "pigweedjs": "latest",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "react-virtualized": "^9.22.3",
+ "react-virtualized-auto-sizer": "^1.0.6",
+ "react-window": "^1.8.7",
+ "xterm": "^4.19.0",
+ "xterm-addon-fit": "^0.5.0"
+ },
+ "devDependencies": {
+ "@types/node": "18.6.3",
+ "@types/react": "18.0.15",
+ "@types/react-dom": "18.0.6",
+ "@types/react-virtualized": "^9.21.21",
+ "@types/react-window": "^1.8.5",
+ "eslint": "8.21.0",
+ "eslint-config-next": "12.2.3",
+ "sass": "^1.54.0",
+ "typescript": "4.7.4"
+ }
+}
diff --git a/pw_web/webconsole/pages/_app.tsx b/pw_web/webconsole/pages/_app.tsx
new file mode 100644
index 000000000..904ae9796
--- /dev/null
+++ b/pw_web/webconsole/pages/_app.tsx
@@ -0,0 +1,35 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import '../styles/globals.css'
+import type {AppProps} from 'next/app'
+import {ThemeProvider, createTheme} from '@mui/material/styles';
+import CssBaseline from '@mui/material/CssBaseline';
+
+const darkTheme = createTheme({
+ palette: {
+ mode: 'dark',
+ },
+});
+
+function MyApp({Component, pageProps}: AppProps) {
+ return (
+ <ThemeProvider theme={darkTheme}>
+ <CssBaseline />
+ <Component {...pageProps} />
+ </ThemeProvider>
+ )
+}
+
+export default MyApp
diff --git a/pw_web/webconsole/pages/_document.tsx b/pw_web/webconsole/pages/_document.tsx
new file mode 100644
index 000000000..94e93d7c9
--- /dev/null
+++ b/pw_web/webconsole/pages/_document.tsx
@@ -0,0 +1,39 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import Document, {Html, Head, Main, NextScript} from 'next/document'
+
+class MyDocument extends Document {
+ render() {
+ return (
+ <Html>
+ <Head>
+ <link
+ href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
+ rel="stylesheet"
+ />
+ <link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro&display=swap"
+ rel="stylesheet"
+ />
+ </Head>
+ <body>
+ <Main />
+ <NextScript />
+ </body>
+ </Html>
+ )
+ }
+}
+
+export default MyDocument
diff --git a/pw_web/webconsole/pages/index.tsx b/pw_web/webconsole/pages/index.tsx
new file mode 100644
index 000000000..20866870b
--- /dev/null
+++ b/pw_web/webconsole/pages/index.tsx
@@ -0,0 +1,61 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import type {NextPage} from 'next'
+import Head from 'next/head'
+import styles from '../styles/Home.module.scss';
+import Log from "../components/log";
+import Repl from "../components/repl";
+import Connect from "../components/connect";
+import BtnUploadDB from '../components/uploadDb';
+import {WebSerial, Device} from "pigweedjs";
+import {useState} from 'react';
+type WebSerialTransport = WebSerial.WebSerialTransport
+
+const Home: NextPage = () => {
+ const [device, setDevice] = useState<Device | undefined>(undefined);
+ const [tokenDB, setTokenDB] = useState("");
+ return (
+ <div className={styles.container}>
+ <Head>
+ <title>Pigweed Console</title>
+ <meta name="description" content="Generated by create next app" />
+ <link rel="icon" href="/favicon.png" />
+ </Head>
+
+ <main className={styles.main}>
+ <div className={styles.toolbar}>
+ <span className={styles.logo}><span>Pigweed</span> Web Console</span>
+ <Connect onConnection={(device) => {
+ setDevice(device);
+ }} />
+ <BtnUploadDB onUpload={(db) => {
+ setTokenDB(db);
+ }} />
+ </div>
+
+ <div className={styles.grid}>
+ <div>
+ <Log device={device} tokenDB={tokenDB}></Log>
+ </div>
+ <div>
+ <Repl device={device}></Repl>
+ </div>
+ </div>
+ </main>
+ </div>
+ )
+}
+
+export default Home
diff --git a/pw_web/webconsole/public/favicon.png b/pw_web/webconsole/public/favicon.png
new file mode 100644
index 000000000..f8001a34e
--- /dev/null
+++ b/pw_web/webconsole/public/favicon.png
Binary files differ
diff --git a/pw_web/webconsole/styles/Home.module.scss b/pw_web/webconsole/styles/Home.module.scss
new file mode 100644
index 000000000..5f671c86a
--- /dev/null
+++ b/pw_web/webconsole/styles/Home.module.scss
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+.container {
+ padding: 1rem;
+ overflow: hidden;
+ height: 100vh;
+}
+
+.main {
+ min-height: 100vh;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.toolbar {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+
+ .logo {
+ margin: 0;
+ line-height: 1.15;
+ font-size: 1rem;
+
+ &>span {
+ color: #D475D4;
+ font-size: 1.5rem;
+ }
+ }
+}
+
+.description {
+ margin: 4rem 0;
+ line-height: 1.5;
+ font-size: 1.5rem;
+}
+
+.grid {
+ display: flex;
+ flex-grow: 1;
+ background-color: #2c313a;
+ border-radius: 10px;
+
+ &>div {
+ flex-grow: 1;
+ padding: 1rem;
+ max-width: 50vw;
+ }
+
+ &>div:first-child {
+ border-right: 1px solid #414141;
+ }
+}
+
+@media (max-width: 600px) {
+ .grid {
+ width: 100%;
+ flex-direction: column;
+ }
+}
diff --git a/pw_web/webconsole/styles/globals.css b/pw_web/webconsole/styles/globals.css
new file mode 100644
index 000000000..ac741785b
--- /dev/null
+++ b/pw_web/webconsole/styles/globals.css
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2022 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+html,
+body {
+ padding: 0;
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
+ Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+ body {
+ color: white;
+ background: black;
+ }
+}
diff --git a/pw_web/webconsole/styles/log.module.css b/pw_web/webconsole/styles/log.module.css
new file mode 100644
index 000000000..7fe2a9cf4
--- /dev/null
+++ b/pw_web/webconsole/styles/log.module.css
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2022 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+.logsContainer {
+ font-family: monospace;
+}
diff --git a/pw_web/webconsole/styles/repl.module.css b/pw_web/webconsole/styles/repl.module.css
new file mode 100644
index 000000000..c8c94af40
--- /dev/null
+++ b/pw_web/webconsole/styles/repl.module.css
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2022 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+.container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.editor {
+ border-top: 1px solid #414141;
+ min-height: 150px;
+ flex: 1;
+ max-height: 15vh;
+ overflow: auto;
+ }
+
+ .editor .cm-content,
+ .editor .cm-gutter {
+ min-height: 150px;
+ }
+
+ .editor .cm-gutters {
+ margin: 1px;
+ }
+
+ .editor .cm-scroller {
+ overflow: auto;
+}
+
+.logs {
+ flex: 1;
+}
+
+.replWithCaret {
+ display: flex;
+}
+
+.caret {
+ margin: 3px;
+ font-weight: bold;
+}
+
+.tooltip {
+ position: relative;
+ display: inline-block;
+ font-weight: bold;
+ border-radius: 50%;
+ line-height: 1;
+ background: #131518;
+ cursor: help;
+ padding: 7px 11px;
+ margin-right: 5px;
+}
+
+.tooltip .tooltiptext {
+ visibility: hidden;
+ width: 320px;
+ background-color: black;
+ color: #fff;
+ padding: 10px;
+ line-height: 1.6;
+ border-radius: 6px;
+ font-weight: normal;
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform: translate(-50%, -105%);
+ z-index: 1;
+}
+
+.tooltip:hover .tooltiptext {
+ visibility: visible;
+}
+
+.tooltip .tooltiptext span {
+ background-color: #414141;
+ padding: 3px 8px;
+ border-radius: 6px;
+}
diff --git a/pw_web/webconsole/tsconfig.json b/pw_web/webconsole/tsconfig.json
new file mode 100644
index 000000000..99710e857
--- /dev/null
+++ b/pw_web/webconsole/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "node",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+ "exclude": ["node_modules"]
+}
diff --git a/pw_web_ui/BUILD.bazel b/pw_web_ui/BUILD.bazel
deleted file mode 100644
index 1fe99a2ec..000000000
--- a/pw_web_ui/BUILD.bazel
+++ /dev/null
@@ -1,120 +0,0 @@
-# Copyright 2020 The Pigweed Authors
-#
-# 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
-#
-# https://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.
-
-load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
-load("@npm//@bazel/concatjs:index.bzl", "karma_web_test")
-load("@npm//@bazel/esbuild:index.bzl", "esbuild")
-load("@npm//@bazel/typescript:index.bzl", "ts_library", "ts_project")
-load("@npm//http-server:index.bzl", "http_server")
-load("//pw_protobuf_compiler/ts:ts_proto_collection.bzl", "ts_proto_collection")
-load("@rules_proto_grpc//js:defs.bzl", "js_proto_library")
-
-package(default_visibility = ["//visibility:public"])
-
-js_proto_library(
- name = "rpc_protos_tspb",
- protos = [
- "//pw_rpc:echo_proto",
- ],
-)
-
-ts_proto_collection(
- name = "web_proto_collection",
- js_proto_library = "@//pw_web_ui:rpc_protos_tspb",
- proto_library = "@//pw_rpc:echo_proto",
-)
-
-ts_project(
- name = "lib",
- srcs = [
- "index.ts",
- "src/frontend/app.tsx",
- "src/frontend/index.tsx",
- "src/frontend/log.tsx",
- "src/frontend/serial_log.tsx",
- "src/transport/device_transport.ts",
- "src/transport/serial_mock.ts",
- "src/transport/web_serial_transport.ts",
- "types/serial.d.ts",
- ],
- declaration = True,
- source_map = True,
- deps = [
- ":web_proto_collection",
- "//pw_hdlc/ts:pw_hdlc",
- "//pw_rpc/ts:pw_rpc",
- "//pw_status/ts:pw_status",
- "@npm//:node_modules",
- ], # can't use fine-grained deps
-)
-
-js_library(
- name = "pw_web_ui",
- package_name = "@pigweed/pw_web_ui",
- srcs = ["package.json"],
- deps = [":lib"],
-)
-
-ts_library(
- name = "web_ui_test_lib",
- srcs = [
- "src/transport/web_serial_transport_test.ts",
- ],
- deps = [
- ":lib",
- "@npm//rxjs",
- ],
-)
-
-esbuild(
- name = "web_ui_test_bundle",
- entry_point = "src/transport/web_serial_transport_test.ts",
- deps = [":web_ui_test_lib"],
-)
-
-esbuild(
- name = "app_bundle",
- args = {
- "resolveExtensions": [
- ".mjs",
- ".js",
- ],
- },
- entry_point = "src/frontend/index.tsx",
- target = "es2021",
- deps = [":lib"],
-)
-
-karma_web_test(
- name = "web_ui_test",
- srcs = [
- ":web_ui_test_bundle",
- ],
-)
-
-http_server(
- name = "devserver",
- args = ["pw_web_ui/"],
- data = [
- "index.html",
- ":app_bundle",
- ],
-)
-
-# needed for embedding into downstream projects
-filegroup(name = "pw_web_ui__contents")
-
-filegroup(name = "pw_web_ui__files")
-
-filegroup(name = "pw_web_ui__nested_node_modules")
diff --git a/pw_web_ui/README.md b/pw_web_ui/README.md
deleted file mode 100644
index d11d2e37d..000000000
--- a/pw_web_ui/README.md
+++ /dev/null
@@ -1 +0,0 @@
-# pw\_web\_ui: Tools for building web UIs
diff --git a/pw_web_ui/docs.rst b/pw_web_ui/docs.rst
deleted file mode 100644
index 18d5f214c..000000000
--- a/pw_web_ui/docs.rst
+++ /dev/null
@@ -1,11 +0,0 @@
-.. _module-pw_web_ui:
-
----------
-pw_web_ui
----------
-
-This module is a set of npm libraries for building web UIs
-for pigweed devices.
-
-Note that this module and its documentation are currently incomplete and
-experimental.
diff --git a/pw_web_ui/index.html b/pw_web_ui/index.html
deleted file mode 100644
index e46873bfe..000000000
--- a/pw_web_ui/index.html
+++ /dev/null
@@ -1,24 +0,0 @@
-<!--
-Copyright 2020 The Pigweed Authors
-
-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
-
- https://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.
--->
-<html>
- <head>
- <title>Pigweed Web UI</title>
- </head>
- <body style="background-color:#1a1c1e"></body>
- <div id="react-root"></div>
- <script src="app_bundle.js"></script>
- </body>
-</html>
diff --git a/pw_web_ui/index.ts b/pw_web_ui/index.ts
deleted file mode 100644
index 8a34d2a2c..000000000
--- a/pw_web_ui/index.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-export {default as DeviceTransport} from './src/transport/device_transport';
-export * from './src/transport/web_serial_transport';
diff --git a/pw_web_ui/package.json b/pw_web_ui/package.json
deleted file mode 100644
index 75105578b..000000000
--- a/pw_web_ui/package.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "name": "@pigweed/pw_web_ui",
- "version": "1.0.0",
- "main": "index.js",
- "license": "MIT",
- "dependencies": {
- "@types/jasmine": "^3.9.0",
- "@types/react-dom": "^17.0.9",
- "http-server": "^13.0.2",
- "jasmine": "^3.9.0",
- "react-dom": "^17.0.2",
- "rxjs": "^7.3.0"
- }
-}
diff --git a/pw_web_ui/rollup.config.js b/pw_web_ui/rollup.config.js
deleted file mode 100644
index f862c7b6d..000000000
--- a/pw_web_ui/rollup.config.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-import commonjs from '@rollup/plugin-commonjs';
-import resolve from '@rollup/plugin-node-resolve';
-import builtins from 'rollup-plugin-node-builtins';
-import nodeGlobals from 'rollup-plugin-node-globals';
-import sourcemaps from 'rollup-plugin-sourcemaps';
-
-export default {
- plugins:
- [
- resolve({
- browser: true,
- }),
- commonjs(),
- builtins(),
- nodeGlobals(),
- sourcemaps(),
- ],
-};
diff --git a/pw_web_ui/src/frontend/app.tsx b/pw_web_ui/src/frontend/app.tsx
deleted file mode 100644
index 0c7ebc67f..000000000
--- a/pw_web_ui/src/frontend/app.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-/* eslint-env browser */
-import {
- Button,
- TextField,
- makeStyles,
- Typography,
- createTheme,
- ThemeProvider,
-} from '@material-ui/core';
-import {ToggleButtonGroup, ToggleButton} from '@material-ui/lab';
-import {WebSerialTransport} from '../transport/web_serial_transport';
-import {Decoder, Frame, Encoder} from '@pigweed/pw_hdlc';
-import {SerialLog} from './serial_log';
-import {Log} from './log';
-import * as React from 'react';
-import {useState, useRef} from 'react';
-import {Channel, Client, UnaryMethodStub} from '@pigweed/pw_rpc';
-import {Status} from '@pigweed/pw_status';
-import {ProtoCollection} from 'web_proto_collection/generated/ts_proto_collection';
-
-const RPC_ADDRESS = 82;
-
-const darkTheme = createTheme({
- palette: {
- type: 'dark',
- primary: {
- main: '#42a5f5',
- },
- secondary: {
- main: 'rgb(232, 21, 165)',
- },
- },
-});
-
-const useStyles = makeStyles(() => ({
- root: {
- display: 'flex',
- 'flex-direction': 'column',
- 'align-items': 'flex-start',
- color: 'rgba(255, 255, 255, 0.8);',
- overflow: 'hidden',
- width: '1000px',
- },
- connect: {
- 'margin-top': '16px',
- 'margin-bottom': '20px',
- },
- rpc: {
- margin: '10px',
- },
-}));
-
-export function App() {
- const [connected, setConnected] = useState<boolean>(false);
- const [frames, setFrames] = useState<Frame[]>([]);
- const [logLines, setLogLines] = useState<string[]>([]);
- const [echoText, setEchoText] = useState<string>('Hello World');
- const [logViewer, setLogViewer] = useState<string>('log');
-
- const classes = useStyles();
-
- const transportRef = useRef(new WebSerialTransport());
- const decoderRef = useRef(new Decoder());
- const encoderRef = useRef(new Encoder());
- const protoCollectionRef = useRef(new ProtoCollection());
- const channelsRef = useRef([
- new Channel(1, (bytes: Uint8Array) => {
- sendPacket(transportRef.current!, bytes);
- }),
- ]);
- const clientRef = useRef<Client>(
- Client.fromProtoSet(channelsRef.current!, protoCollectionRef.current!)
- );
- const echoService = clientRef
- .current!.channel()!
- .methodStub('pw.rpc.EchoService.Echo') as UnaryMethodStub;
-
- function onConnected() {
- setConnected(true);
- transportRef.current!.chunks.subscribe((item: Uint8Array) => {
- const decoded = decoderRef.current!.process(item);
- for (const frame of decoded) {
- setFrames(old => [...old, frame]);
- if (frame.address === RPC_ADDRESS) {
- const status = clientRef.current!.processPacket(frame.data);
- }
- if (frame.address === 1) {
- const decodedLine = new TextDecoder().decode(frame.data);
- const date = new Date();
- const logLine = `[${date.toLocaleTimeString()}] ${decodedLine}`;
- setLogLines(old => [...old, logLine]);
- }
- }
- });
- }
-
- function sendPacket(
- transport: WebSerialTransport,
- packetBytes: Uint8Array
- ): void {
- const hdlcBytes = encoderRef.current.uiFrame(RPC_ADDRESS, packetBytes);
- transport.sendChunk(hdlcBytes);
- }
-
- function echo(text: string) {
- const request = new echoService.method.responseType();
- request.setMsg(text);
- echoService
- .call(request)
- .then(([status, response]) => {
- console.log(response.toObject());
- })
- .catch(() => {});
- }
-
- return (
- <div className={classes.root}>
- <ThemeProvider theme={darkTheme}>
- <Typography variant="h3">Pigweb Demo</Typography>
- <Button
- className={classes.connect}
- disabled={connected}
- variant="contained"
- color="primary"
- onClick={() => {
- transportRef.current
- .connect()
- .then(onConnected)
- .catch(error => {
- setConnected(false);
- console.log(error);
- });
- }}
- >
- {connected ? 'Connected' : 'Connect'}
- </Button>
- <ToggleButtonGroup
- value={logViewer}
- onChange={(event, selected) => {
- setLogViewer(selected);
- }}
- exclusive
- >
- <ToggleButton value="log">Log Viewer</ToggleButton>
- <ToggleButton value="serial">Serial Debug</ToggleButton>
- </ToggleButtonGroup>
- {logViewer === 'log' ? (
- <Log lines={logLines} />
- ) : (
- <SerialLog frames={frames} />
- )}
- <span className={classes.rpc}>
- <TextField
- id="echo-text"
- label="Echo Text"
- disabled={!connected}
- value={echoText}
- onChange={event => {
- setEchoText(event.target.value);
- }}
- ></TextField>
- <Button
- disabled={!connected}
- variant="contained"
- color="primary"
- onClick={() => {
- echo(echoText);
- }}
- >
- Send Echo RPC
- </Button>
- </span>
- </ThemeProvider>
- </div>
- );
-}
diff --git a/pw_web_ui/src/frontend/log.tsx b/pw_web_ui/src/frontend/log.tsx
deleted file mode 100644
index 669bc2b80..000000000
--- a/pw_web_ui/src/frontend/log.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2021 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-/* eslint-env browser */
-
-import {makeStyles, Paper, Box} from '@material-ui/core';
-import * as React from 'react';
-import {default as AnsiUp} from 'ansi_up';
-import * as Parser from 'html-react-parser';
-
-type Props = {
- lines: string[];
-};
-
-const useStyles = makeStyles(() => ({
- root: {
- padding: '8px',
- 'background-color': '#131416',
- height: '500px',
- 'overflow-y': 'auto',
- width: '100%',
- color: 'white',
- },
-}));
-
-export function Log(props: Props) {
- const classes = useStyles();
- const xtermRef = React.useRef(null);
- const ansiUp = new AnsiUp();
-
- function row(text: string, index: number) {
- const textHtml = ansiUp.ansi_to_html(text);
- return (
- <Box key={index} sx={{fontFamily: 'Monospace'}}>
- {Parser.default(textHtml)}
- </Box>
- );
- }
-
- return <div className={classes.root}>{props.lines.map(row)}</div>;
-}
diff --git a/pw_web_ui/src/frontend/rpc.tsx b/pw_web_ui/src/frontend/rpc.tsx
deleted file mode 100644
index f7defc2dc..000000000
--- a/pw_web_ui/src/frontend/rpc.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-/* eslint-env browser */
-import {makeStyles, Box} from '@material-ui/core';
-import * as React from 'react';
-import {Status} from '@pigweed/pw_status';
-import {Message} from 'google-protobuf';
-import {Call} from '@pigweed/pw_rpc';
-
-type Props = {
- calls: Call[];
-};
-
-const useStyles = makeStyles(() => ({
- root: {
- padding: '8px',
- 'background-color': '131416',
- height: '300px',
- 'overflow-y': 'auto',
- width: '100%',
- color: 'white',
- },
-}));
-
-export function RpcPane(props: Props) {
- const classes = useStyles();
-
- function row(call: Call, index: number) {
- return (
- <Box key={index} sx={{fontFamily: 'Monospace'}}>
- {call.rpc.service.name}.{call.rpc.method.name}
- \n---
- {call.completed ? Status[call.status!] : 'In progress...'}
- </Box>
- );
- }
-
- return <div className={classes.root}>{props.calls.map(row)}</div>;
-}
diff --git a/pw_web_ui/src/frontend/serial_log.tsx b/pw_web_ui/src/frontend/serial_log.tsx
deleted file mode 100644
index 9a7192775..000000000
--- a/pw_web_ui/src/frontend/serial_log.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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.
-
-/* eslint-env browser */
-import {makeStyles, Paper, Box} from '@material-ui/core';
-import * as React from 'react';
-import {FrameStatus, Frame} from '@pigweed/pw_hdlc';
-
-type Props = {
- frames: Frame[];
-};
-
-const useStyles = makeStyles(() => ({
- root: {
- padding: '8px',
- 'background-color': '#131416',
- height: '500px',
- 'overflow-y': 'auto',
- width: '100%',
- color: 'white',
- },
-}));
-
-export function SerialLog(props: Props) {
- const classes = useStyles();
- const decoder = new TextDecoder();
-
- // TODO(b/199515206): Display HDLC packets in user friendly manner.
- //
- // See the python console serial debug window for reference.
- function row(frame: Frame, index: number) {
- let rowText = '';
- if (frame.status === FrameStatus.OK) {
- rowText = decoder.decode(frame.data);
- } else {
- rowText = `[${frame.rawDecoded}]`;
- }
-
- return (
- <div key={index}>
- {frame.status}: {rowText}
- </div>
- );
- }
-
- return (
- <Paper className={classes.root}>
- <Box sx={{fontFamily: 'Monospace'}}>
- {props.frames.map((frame: Frame, index: number) => row(frame, index))}
- </Box>
- </Paper>
- );
-}
diff --git a/pw_web_ui/tsconfig.json b/pw_web_ui/tsconfig.json
deleted file mode 100644
index eafd51cad..000000000
--- a/pw_web_ui/tsconfig.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "compilerOptions": {
- "allowUnreachableCode": false,
- "allowUnusedLabels": false,
- "declaration": true,
- "forceConsistentCasingInFileNames": true,
- "lib": [
- "es2021",
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "module": "commonjs",
- "noEmitOnError": true,
- "noFallthroughCasesInSwitch": true,
- "noImplicitReturns": true,
- "pretty": true,
- "sourceMap": true,
- "strict": true,
- "target": "es2018",
- "jsx": "react",
- "plugins": [
- {
- "name": "@bazel/tsetse",
- "disabledRules": [
- "must-use-promises"
- ]
- }
- ]
- },
- "exclude": [
- "node_modules"
- ]
-}
diff --git a/pw_web_ui/yarn.lock b/pw_web_ui/yarn.lock
deleted file mode 100644
index 680739f1c..000000000
--- a/pw_web_ui/yarn.lock
+++ /dev/null
@@ -1,359 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@types/jasmine@^3.9.0":
- version "3.9.1"
- resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.9.1.tgz#94c65ee8bf9d24d9e1d84abaed57b6e0da8b49de"
- integrity sha512-PVpjh8S8lqKFKurWSKdFATlfBHGPzgy0PoDdzQ+rr78jTQ0aacyh9YndzZcAUPxhk4kRujItFFGQdUJ7flHumw==
-
-"@types/prop-types@*":
- version "15.7.4"
- resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
- integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
-
-"@types/react-dom@^17.0.9":
- version "17.0.9"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.9.tgz#441a981da9d7be117042e1a6fd3dac4b30f55add"
- integrity sha512-wIvGxLfgpVDSAMH5utdL9Ngm5Owu0VsGmldro3ORLXV8CShrL8awVj06NuEXFQ5xyaYfdca7Sgbk/50Ri1GdPg==
- dependencies:
- "@types/react" "*"
-
-"@types/react@*":
- version "17.0.27"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.27.tgz#6498ed9b3ad117e818deb5525fa1946c09f2e0e6"
- integrity sha512-zgiJwtsggVGtr53MndV7jfiUESTqrbxOcBvwfe6KS/9bzaVPCTDieTWnFNecVNx6EAaapg5xsLLWFfHHR437AA==
- dependencies:
- "@types/prop-types" "*"
- "@types/scheduler" "*"
- csstype "^3.0.2"
-
-"@types/scheduler@*":
- version "0.16.2"
- resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
- integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
-
-async@^2.6.2:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
- integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
- dependencies:
- lodash "^4.17.14"
-
-balanced-match@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
- integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-basic-auth@^1.0.3:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884"
- integrity sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=
-
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-call-bind@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
- integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
- dependencies:
- function-bind "^1.1.1"
- get-intrinsic "^1.0.2"
-
-colors@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
- integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-
-corser@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
- integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=
-
-csstype@^3.0.2:
- version "3.0.9"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b"
- integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==
-
-debug@^3.1.1:
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
- integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
- dependencies:
- ms "^2.1.1"
-
-eventemitter3@^4.0.0:
- version "4.0.7"
- resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
- integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
-
-follow-redirects@^1.0.0:
- version "1.14.4"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
- integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==
-
-fs.realpath@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
- integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-intrinsic@^1.0.2:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
- integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
- dependencies:
- function-bind "^1.1.1"
- has "^1.0.3"
- has-symbols "^1.0.1"
-
-glob@^7.1.6:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
- integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-has-symbols@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
- integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
-
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
- dependencies:
- function-bind "^1.1.1"
-
-he@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
- integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
-
-http-proxy@^1.18.0:
- version "1.18.1"
- resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
- integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
- dependencies:
- eventemitter3 "^4.0.0"
- follow-redirects "^1.0.0"
- requires-port "^1.0.0"
-
-http-server@^13.0.2:
- version "13.0.2"
- resolved "https://registry.yarnpkg.com/http-server/-/http-server-13.0.2.tgz#36f8a8ae0e1b78e7bf30a4dfb01ae89b904904ef"
- integrity sha512-R8kvPT7qp11AMJWLZsRShvm6heIXdlR/+tL5oAWNG/86A/X+IAFX6q0F9SA2G+dR5aH/759+9PLH0V34Q6j4rg==
- dependencies:
- basic-auth "^1.0.3"
- colors "^1.4.0"
- corser "^2.0.1"
- he "^1.1.0"
- http-proxy "^1.18.0"
- mime "^1.6.0"
- minimist "^1.2.5"
- opener "^1.5.1"
- portfinder "^1.0.25"
- secure-compare "3.0.1"
- union "~0.5.0"
- url-join "^2.0.5"
-
-inflight@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
- integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
- dependencies:
- once "^1.3.0"
- wrappy "1"
-
-inherits@2:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-jasmine-core@~3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.9.0.tgz#09a3c8169fe98ec69440476d04a0e4cb4d59e452"
- integrity sha512-Tv3kVbPCGVrjsnHBZ38NsPU3sDOtNa0XmbG2baiyJqdb5/SPpDO6GVwJYtUryl6KB4q1Ssckwg612ES9Z0dreQ==
-
-jasmine@^3.9.0:
- version "3.9.0"
- resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.9.0.tgz#286c4f9f88b69defc24acf3989af5533d5c6a0e6"
- integrity sha512-JgtzteG7xnqZZ51fg7N2/wiQmXon09szkALcRMTgCMX4u/m17gVJFjObnvw5FXkZOWuweHPaPRVB6DI2uN0wVA==
- dependencies:
- glob "^7.1.6"
- jasmine-core "~3.9.0"
-
-"js-tokens@^3.0.0 || ^4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
- integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-
-lodash@^4.17.14:
- version "4.17.21"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
- integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
-loose-envify@^1.1.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
- integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
- dependencies:
- js-tokens "^3.0.0 || ^4.0.0"
-
-mime@^1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
- integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-
-minimatch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimist@^1.2.5:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
- integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
-
-mkdirp@^0.5.5:
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
- integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
- dependencies:
- minimist "^1.2.5"
-
-ms@^2.1.1:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
- integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
-object-assign@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
- integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
-
-object-inspect@^1.9.0:
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
- integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
-
-once@^1.3.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
- integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
- dependencies:
- wrappy "1"
-
-opener@^1.5.1:
- version "1.5.2"
- resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
- integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
-
-path-is-absolute@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
- integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-
-portfinder@^1.0.25:
- version "1.0.28"
- resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
- integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==
- dependencies:
- async "^2.6.2"
- debug "^3.1.1"
- mkdirp "^0.5.5"
-
-qs@^6.4.0:
- version "6.10.1"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
- integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
- dependencies:
- side-channel "^1.0.4"
-
-react-dom@^17.0.2:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
- integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
- dependencies:
- loose-envify "^1.1.0"
- object-assign "^4.1.1"
- scheduler "^0.20.2"
-
-requires-port@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
- integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
-
-rxjs@^7.3.0:
- version "7.3.1"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.3.1.tgz#cc375521f9e238b474fe552b0b9fd1be33d08099"
- integrity sha512-vNenx7gqjPyeKpRnM6S5Ksm/oFTRijWWzYlRON04KaehZ3YjDwEmVjGUGo0TKWVjeNXOujVRlh0K1drUbcdPkw==
- dependencies:
- tslib "~2.1.0"
-
-scheduler@^0.20.2:
- version "0.20.2"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
- integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
- dependencies:
- loose-envify "^1.1.0"
- object-assign "^4.1.1"
-
-secure-compare@3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3"
- integrity sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=
-
-side-channel@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
- integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
- dependencies:
- call-bind "^1.0.0"
- get-intrinsic "^1.0.2"
- object-inspect "^1.9.0"
-
-tslib@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
- integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
-
-union@~0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075"
- integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==
- dependencies:
- qs "^6.4.0"
-
-url-join@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
- integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=
-
-wrappy@1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
- integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
diff --git a/pw_work_queue/BUILD.bazel b/pw_work_queue/BUILD.bazel
index 3f2932775..ff8855c1d 100644
--- a/pw_work_queue/BUILD.bazel
+++ b/pw_work_queue/BUILD.bazel
@@ -58,7 +58,7 @@ pw_cc_library(
],
deps = [
":pw_work_queue",
- ":test_thread",
+ ":stl_test_thread",
"//pw_log",
"//pw_unit_test",
],
@@ -71,7 +71,7 @@ pw_cc_library(
],
target_compatible_with = select(TARGET_COMPATIBLE_WITH_HOST_SELECT),
deps = [
- "//pw_thread:test_thread_header",
+ ":test_thread_header",
"//pw_thread:thread",
"//pw_thread_stl:thread",
],
diff --git a/pw_work_queue/BUILD.gn b/pw_work_queue/BUILD.gn
index 08fd679d8..a84e30a08 100644
--- a/pw_work_queue/BUILD.gn
+++ b/pw_work_queue/BUILD.gn
@@ -38,6 +38,7 @@ pw_source_set("pw_work_queue") {
"$dir_pw_thread:thread",
dir_pw_function,
dir_pw_metric,
+ dir_pw_span,
dir_pw_status,
]
sources = [ "work_queue.cc" ]
diff --git a/pw_work_queue/CMakeLists.txt b/pw_work_queue/CMakeLists.txt
new file mode 100644
index 000000000..32100a474
--- /dev/null
+++ b/pw_work_queue/CMakeLists.txt
@@ -0,0 +1,77 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_work_queue STATIC
+ HEADERS
+ public/pw_work_queue/internal/circular_buffer.h
+ public/pw_work_queue/work_queue.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_sync.interrupt_spin_lock
+ pw_sync.lock_annotations
+ pw_sync.thread_notification
+ pw_thread.thread
+ pw_function
+ pw_metric
+ pw_span
+ pw_status
+ SOURCES
+ work_queue.cc
+)
+
+pw_add_library(pw_work_queue.test_thread INTERFACE
+ HEADERS
+ public/pw_work_queue/test_thread.h
+ PUBLIC_INCLUDES
+ public
+ PUBLIC_DEPS
+ pw_thread.thread
+)
+
+# To instantiate this test based on a selected thread backend to provide
+# test_thread you can create a pw_add_test which depends on this
+# pw_add_library and a pw_add_library which provides the implementation of
+# test_thread. See pw_work_queue.stl_work_queue_test as an example.
+pw_add_library(pw_work_queue.work_queue_test STATIC
+ SOURCES
+ work_queue_test.cc
+ PRIVATE_DEPS
+ pw_work_queue
+ pw_work_queue.test_thread
+ pw_log
+ pw_unit_test
+)
+
+pw_add_library(pw_work_queue.stl_test_thread STATIC
+ SOURCES
+ stl_test_thread.cc
+ PRIVATE_DEPS
+ pw_work_queue.test_thread
+ pw_thread.thread
+ pw_thread_stl.thread
+)
+
+if("${pw_thread.thread_BACKEND}" STREQUAL "pw_thread_stl.thread")
+ pw_add_test(pw_work_queue.stl_work_queue_test
+ PRIVATE_DEPS
+ pw_work_queue.stl_test_thread
+ pw_work_queue.work_queue_test
+ GROUPS
+ modules
+ pw_work_queue
+ )
+endif()
diff --git a/pw_work_queue/public/pw_work_queue/internal/circular_buffer.h b/pw_work_queue/public/pw_work_queue/internal/circular_buffer.h
index 55295d0b3..c2f4dc5dd 100644
--- a/pw_work_queue/public/pw_work_queue/internal/circular_buffer.h
+++ b/pw_work_queue/public/pw_work_queue/internal/circular_buffer.h
@@ -16,7 +16,9 @@
#include <cstdint>
#include <optional>
-#include <span>
+
+#include "pw_assert/assert.h"
+#include "pw_span/span.h"
namespace pw::work_queue::internal {
@@ -24,7 +26,7 @@ namespace pw::work_queue::internal {
template <typename T>
class CircularBuffer {
public:
- explicit constexpr CircularBuffer(std::span<T> buffer)
+ explicit constexpr CircularBuffer(span<T> buffer)
: buffer_(buffer), head_(0), tail_(0), count_(0) {}
bool empty() const { return count_ == 0; }
@@ -68,7 +70,7 @@ class CircularBuffer {
}
}
- std::span<T> buffer_;
+ span<T> buffer_;
size_t head_;
size_t tail_;
diff --git a/pw_work_queue/public/pw_work_queue/work_queue.h b/pw_work_queue/public/pw_work_queue/work_queue.h
index 9b592b820..7afb74e02 100644
--- a/pw_work_queue/public/pw_work_queue/work_queue.h
+++ b/pw_work_queue/public/pw_work_queue/work_queue.h
@@ -16,10 +16,10 @@
#include <array>
#include <cstdint>
-#include <span>
#include "pw_function/function.h"
#include "pw_metric/metric.h"
+#include "pw_span/span.h"
#include "pw_status/status.h"
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/lock_annotations.h"
@@ -38,7 +38,7 @@ using WorkItem = Function<void()>;
class WorkQueue : public thread::ThreadCore {
public:
// Note: the ThreadNotification prevents this from being constexpr.
- explicit WorkQueue(std::span<WorkItem> queue_storage)
+ explicit WorkQueue(span<WorkItem> queue_storage)
: stop_requested_(false), circular_buffer_(queue_storage) {}
// Enqueues a work_item for execution by the work queue thread.
diff --git a/pw_work_queue/work_queue_test.cc b/pw_work_queue/work_queue_test.cc
index 6b793e0eb..ce349d705 100644
--- a/pw_work_queue/work_queue_test.cc
+++ b/pw_work_queue/work_queue_test.cc
@@ -40,21 +40,16 @@ TEST(WorkQueue, PingPongOneRequestType) {
for (int i = 0; i < kPingPongs; ++i) {
// Ping: throw work at the queue that will increment our counter.
- work_queue
- .PushWork([&context] {
- context.counter++;
- PW_LOG_INFO("Send pong...");
- context.worker_ping.release();
- })
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), work_queue.PushWork([&context] {
+ context.counter++;
+ PW_LOG_INFO("Send pong...");
+ context.worker_ping.release();
+ }));
// Throw a distraction in the queue.
- work_queue
- .PushWork([] {
- PW_LOG_INFO(
- "I'm a random task in the work queue; nothing to see here!");
- })
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), work_queue.PushWork([] {
+ PW_LOG_INFO("I'm a random task in the work queue; nothing to see here!");
+ }));
// Pong: wait for the callback to notify us from the worker thread.
context.worker_ping.acquire();
@@ -84,34 +79,30 @@ TEST(WorkQueue, PingPongTwoRequestTypesWithExtraRequests) {
// Run a bunch of work items in the queue.
for (int i = 0; i < kPingPongs; ++i) {
// Other requests...
- work_queue.PushWork([] { PW_LOG_INFO("Chopping onions"); })
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(),
+ work_queue.PushWork([] { PW_LOG_INFO("Chopping onions"); }));
// Ping A: throw work at the queue that will increment our counter.
- work_queue
- .PushWork([&context_a] {
- context_a.counter++;
- context_a.worker_ping.release();
- })
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), work_queue.PushWork([&context_a] {
+ context_a.counter++;
+ context_a.worker_ping.release();
+ }));
// Other requests...
- work_queue.PushWork([] { PW_LOG_INFO("Dicing carrots"); })
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- work_queue.PushWork([] { PW_LOG_INFO("Blanching spinach"); })
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(),
+ work_queue.PushWork([] { PW_LOG_INFO("Dicing carrots"); }));
+ EXPECT_EQ(OkStatus(),
+ work_queue.PushWork([] { PW_LOG_INFO("Blanching spinach"); }));
// Ping B: throw work at the queue that will increment our counter.
- work_queue
- .PushWork([&context_b] {
- context_b.counter++;
- context_b.worker_ping.release();
- })
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(), work_queue.PushWork([&context_b] {
+ context_b.counter++;
+ context_b.worker_ping.release();
+ }));
// Other requests...
- work_queue.PushWork([] { PW_LOG_INFO("Peeling potatoes"); })
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ EXPECT_EQ(OkStatus(),
+ work_queue.PushWork([] { PW_LOG_INFO("Peeling potatoes"); }));
// Pong A & B: wait for the callbacks to notify us from the worker thread.
context_a.worker_ping.acquire();
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 000000000..fbe041be2
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,116 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import commonjs from '@rollup/plugin-commonjs';
+import resolve from '@rollup/plugin-node-resolve';
+import pluginTypescript from '@rollup/plugin-typescript';
+import path from 'path';
+import dts from 'rollup-plugin-dts';
+import nodePolyfills from 'rollup-plugin-node-polyfills';
+import sourceMaps from 'rollup-plugin-sourcemaps';
+
+import tsConfig from './tsconfig.json';
+
+export default [
+ // Bundle proto collection script
+ {
+ input: path.join('pw_protobuf_compiler', 'ts', 'build.ts'),
+ output: [{
+ file: path.join('dist', 'bin', 'pw_protobuf_compiler.js'),
+ format: 'cjs',
+ banner: '#!/usr/bin/env node\n\nconst window = null;'
+ }],
+ plugins: [
+ pluginTypescript(
+ {tsconfig: './tsconfig.json', exclude: ['**/*_test.ts']}),
+ resolve(),
+ commonjs(),
+
+ // Resolve source maps to the original source
+ sourceMaps()
+ ]
+ },
+ // bundle proto collection template used by the above script
+ {
+ input: path.join(
+ 'pw_protobuf_compiler', 'ts', 'ts_proto_collection.template.ts'),
+ output: [{
+ file: path.join('dist', 'bin', 'ts_proto_collection.template.js'),
+ format: 'esm',
+ banner: '/* eslint-disable */'
+ }],
+ plugins: [
+ pluginTypescript(
+ {tsconfig: './tsconfig.json', exclude: ['**/*_test.ts']}),
+ resolve(),
+ commonjs(),
+
+ // Resolve source maps to the original source
+ sourceMaps()
+ ]
+ },
+ // Bundle proto collection into one UMD file for consumption from browser
+ {
+ input: path.join('dist', 'protos', 'collection.ts'),
+ output: [{
+ file: path.join('dist', 'protos', 'collection.umd.js'),
+ format: 'umd',
+ sourcemap: true,
+ name: 'PigweedProtoCollection',
+ }],
+ plugins: [
+ pluginTypescript({tsconfig: './tsconfig.json'}),
+ commonjs(),
+ resolve(),
+
+ // Resolve source maps to the original source
+ sourceMaps()
+ ]
+ },
+ // Types for proto collection
+ {
+ input: path.join(
+ 'dist', 'protos', 'types', 'dist', 'protos', 'collection.d.ts'),
+ output:
+ [{file: path.join('dist', 'protos', 'collection.d.ts'), format: 'es'}],
+ plugins: [dts({compilerOptions: tsConfig.compilerOptions})]
+ },
+ // Bundle Pigweed modules
+ {
+ input: path.join('ts', 'index.ts'),
+ output: [
+ {
+ file: path.join('dist', 'index.umd.js'),
+ format: 'umd',
+ sourcemap: true,
+ name: 'Pigweed',
+ },
+ {
+ file: path.join('dist', 'index.mjs'),
+ format: 'esm',
+ sourcemap: true,
+ }
+ ],
+ plugins: [
+ pluginTypescript(
+ {tsconfig: './tsconfig.json', exclude: ['**/*_test.ts']}),
+ nodePolyfills(),
+ resolve(),
+ commonjs(),
+
+ // Resolve source maps to the original source
+ sourceMaps()
+ ]
+ }
+];
diff --git a/seed/0000-index.rst b/seed/0000-index.rst
new file mode 100644
index 000000000..35f1e753f
--- /dev/null
+++ b/seed/0000-index.rst
@@ -0,0 +1,14 @@
+.. _seed-0000:
+
+==========
+SEED Index
+==========
+All pending, active, and resolved SEEDs are listed below.
+
+.. toctree::
+ :maxdepth: 1
+
+ 0001-the-seed-process
+ 0002-template
+ 0101-pigweed.json
+ 0102-module-docs
diff --git a/seed/0001-the-seed-process.rst b/seed/0001-the-seed-process.rst
new file mode 100644
index 000000000..3b7958bd6
--- /dev/null
+++ b/seed/0001-the-seed-process.rst
@@ -0,0 +1,412 @@
+.. _seed-0001:
+
+======================
+0001: The SEED Process
+======================
+
+.. card::
+ :fas:`seedling` SEED-0001: :ref:`The SEED Process<seed-0001>`
+
+ :octicon:`comment-discussion` Status:
+ :bdg-secondary-line:`Open for Comments`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Last Call`
+ :octicon:`chevron-right`
+ :bdg-primary:`Accepted`
+ :octicon:`kebab-horizontal`
+ :bdg-secondary-line:`Rejected`
+
+ :octicon:`calendar` Proposal Date: 2022-10-31
+
+ :octicon:`code-review` CL: `pwrev/116577 <https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/116577>`_
+
+-------
+Summary
+-------
+SEEDs are the process through which substantial changes to Pigweed are proposed,
+reviewed, and decided by community stakeholders to collaboratively drive the
+project in a favorable direction.
+
+This document outlines the SEED process at a high level. Details about how SEEDs
+should be written and structured are described in :ref:`seed-0002`.
+
+----------
+Motivation
+----------
+As Pigweed and its community grow, it becomes important to ensure that the
+Pigweed team, Pigweed users, and other community stakeholders align on the
+future of the project. To date, development of Pigweed has primarily been
+driven by the core team, and community feedback has been mostly informal and
+undocumented.
+
+SEEDs are a formalized process for authoring, reviewing, and ratifying proposed
+changes to Pigweed.
+
+The SEED process has several goals.
+
+- Increase the visibility of proposed changes to Pigweed early on and allow
+ interested parties to engage in their design.
+
+- Maintain a public record of design decisions rendered during Pigweed's
+ development and the rationales behind them.
+
+- Ensure consistent API design across Pigweed modules through a formal team-wide
+ review process.
+
+Active SEEDs are discussed by the community through Gerrit code review comments
+as well as a SEEDs chatroom in Pigweed's Discord server. Decisions are
+ultimately rendered by a review committee of Pigweed team members.
+
+------------------------
+When is a SEED required?
+------------------------
+SEEDs should be written by any developer wishing to make a "substantial" change
+to Pigweed. Whether or not a change is considered "substantial" varies depending
+on what parts of Pigweed it touches and may change over time as the project
+evolves. Some general guidelines are established below.
+
+Examples of changes considered "substantial" include, but are not limited to:
+
+- Adding a new top-level module.
+
+- Modifying a widely-used public API.
+
+- A breaking update to typical Pigweed user workflows (bootstrap, build setup,
+ ``pw`` commands, etc.).
+
+- Changing any data interchange or storage protocol or format (e.g. transport
+ protocols, flash layout), unless the change is small and backwards compatible
+ (e.g. adding a field to an exchanged Protobuf message).
+
+- Changes to Pigweed's code policy, style guides, or other project-level
+ guidelines.
+
+- Whenever a Pigweed team member asks you to write a SEED.
+
+Conversely, the following changes would likely not require a SEED:
+
+- Fixing typos.
+
+- Refactoring internal module code without public API changes.
+
+- Adding minor parallel operations to existing APIs (e.g.
+ ``Read(ConstByteSpan)`` vs ``Read(const byte*, size_t)``).
+
+If you're unsure whether a change you wish to make requires a SEED, it's worth
+asking the Pigweed team in our Discord server prior to writing any code.
+
+-------
+Process
+-------
+Suppose you'd like to propose a new Pigweed RPC Over Smoke Signals protocol.
+
+#. If you haven't already, clone the Pigweed repository and set it up locally,
+ following the :ref:`docs-getting-started` guide.
+
+#. Copy the `SEED template <0002-template>`_ to create the RST file for your
+ SEED. As you don't yet have a SEED number, use XXXX as a placeholder,
+ followed by the lowercase title of the proposal, with words separated by
+ hyphens.
+
+ .. code-block:: sh
+
+ cp seed/0002-template.rst seed/XXXX-pw_rpc-over-smoke-signals.rst
+
+#. Push up the template to Gerrit, marking it as a Work-In-Progress change.
+ From here, you may fill the template out with the content of your proposal
+ at your convenience.
+
+#. At any point, you may claim a SEED number by opening the
+ `SEED index`_ and taking the next available number by inserting
+ a row into the ``toctree`` table. Link the entry to the WIP change for your
+ SEED.
+
+ .. _SEED index: https://cs.opensource.google/pigweed/pigweed/+/main:seed/0000-index.rst
+
+ .. code-block:: rst
+
+ .. toctree::
+
+ 0001-the-seed-process
+ ...
+ 5308-some-other-seed
+ 5309: pw_rpc Over Smoke Signals<https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/116577>
+
+#. Commit your change to the index (and nothing else) with the commit message
+ ``SEED-xxxx: Claim SEED number``.
+
+ .. code-block:: sh
+
+ git add seed/0000-index.rst
+ git commit -m "SEED-5309: Claim SEED number"
+
+#. Push up a changelist (CL) to Gerrit following the :ref:`docs-contributing`
+ guide and add GWSQ as a reviewer. Set ``Pigweed-Auto-Submit`` to +1.
+
+ .. image:: 0001-the-seed-process/seed-index-gerrit.png
+
+#. Once your CL has been reviewed and submitted, the SEED number belongs to you.
+ Update your document's template and filename with this number.
+
+#. When you feel you have enough substantive content in your proposal to be
+ reviewed, push it up to Gerrit and switch the change from WIP to Active.
+ This will begin the open comments period.
+
+#. Engage with reviewers to iterate on your proposal through its comment period.
+
+#. When a tentative decision has been reached, a Pigweed team member will
+ comment on your proposal with a summary of the discussion and reasoning,
+ moving it into its Last Call phase (as described in the :ref:`Lifecycle
+ <seed-0001-lifecycle>` section).
+
+#. Following the conclusion of the Last Call period, a Pigweed team member will
+ sign off on the CL with a +2 vote, allowing it to be submitted. Update the
+ reference in the SEED index with the link to your document and submit the CL.
+
+ .. code-block:: rst
+
+ .. toctree::
+
+ 0001-the-seed-process
+ ...
+ 5308-some-other-seed
+ 5309-pw_rpc-over-smoke-signals
+
+--------------
+SEED documents
+--------------
+SEEDs are written as ReST documents integrated with the rest of Pigweed's
+documentation. They live directly within the core Pigweed repository, under a
+top-level ``seed/`` subdirectory.
+
+The structure of SEED documents themselves, their format, required sections, and
+other considerations are outlined in :ref:`seed-0002`.
+
+The first 100 SEEDs (0000-0100) are *Meta-SEEDs*. These are reserved for
+internal Pigweed usage and generally detail SEED-related processes. Unlike
+regular SEEDs, Meta-SEEDs are living documents which may be revised over time.
+
+.. _seed-0001-lifecycle:
+
+-----------------------
+The lifecycle of a SEED
+-----------------------
+A SEED proposal undergoes several phases between first being published and a
+final decision.
+
+:bdg-primary-line:`Draft` **The SEED is a work-in-progress and not yet ready
+for comments.**
+
+- The SEED exists in Gerrit as a Work-In-Progress (WIP) change.
+- Has an assigned SEED number and exists in the index.
+- Not yet ready to receive feedback.
+
+:bdg-primary:`Open for Comments` **The SEED is soliciting feedback.**
+
+- The SEED has sufficient substance to be reviewed, as determined by its
+ author.
+- A thread for the SEED is created in Discord to promote the proposal and open
+ discussion.
+- Interested parties comment on the SEED to evaluate the proposal, raise
+ questions and concerns, and express support or opposition.
+- Back and forth discussion between the author and reviewers, resulting in
+ modifications to the document.
+- The SEED remains open for as long as necessary. Internally, Pigweed's review
+ committee will regularly meet to consider active SEEDs and determine when to
+ advance to them the next stage.
+
+:bdg-warning:`Last Call` **A tentative decision has been reached, but
+commenters may raise final objections.**
+
+- A tentative decision on the SEED has been made. The decision is issued at the
+ best judgement of Pigweed's review committee when they feel there has been
+ sufficient discussion on the tradeoffs of the proposal to do so.
+- Transition is triggered manually by a member of the Pigweed team, with a
+ comment on the likely outcome of the SEED (acceptance / rejection).
+- On entering Last Call, the visibility of the SEED is widely boosted through
+ Pigweed's communication channels (Discord, mailing list, Pigweed Live, etc.)
+ to solicit any strong objections from stakeholders.
+- Typically, Last Call lasts for a set period of 7 calendar days, after which
+ the final decision is formalized.
+- If any substantial new arguments are raised during Last Call, the review
+ committee may decide to re-open the discussion, returning the SEED to a
+ commenting phase.
+
+:bdg-success:`Accepted` **The proposal is ratified and ready for
+implementation.**
+
+- The SEED is submitted into the Pigweed repository.
+- A tracking bug is created for the implementation, if applicable.
+- The SEED may no longer be modified (except minor changes such as typos).
+ Follow-up discussions on the same topic require a new SEED.
+
+:bdg-danger:`Rejected` **The proposal has been turned down.**
+
+- The SEED is submitted into the Pigweed repository to provide a permanent
+ record of the considerations made for future reference.
+- The SEED may no longer be modified.
+
+:bdg-secondary:`Deprecated` **The proposal was originally accepted and
+implemented but later removed.**
+
+- The proposal was once implemented but later undone.
+- The SEED's changelog contains justification for the deprecation.
+
+:bdg-info:`Superseded` **The proposal was originally accepted and implemented
+but significant portions were later overruled by a different SEED.**
+
+- A newer SEED proposal revisits the same topic and proposal and redesigns
+ significant parts of the original.
+- The SEED is marked as superseded with a reference to the newer proposal.
+
+---------
+Rationale
+---------
+
+Document format
+---------------
+Three different documentation formats are considered for SEEDs:
+
+- **ReST:** Used for Pigweed's existing documentation, making it a natural
+ option.
+- **Google Docs:** The traditional way of writing SEED-like investigation and
+ design documents.
+- **Markdown:** Ubiquitous across open-source projects, with extensive tooling
+ available.
+
+Summary
+^^^^^^^
+Based on the evaluated criteria, ReST documents provide the best overall SEED
+experience. The primary issues with ReST exist around contributor tooling, which
+may be mitigated with additional investment from the Pigweed team.
+
+The table below details the main criteria evaluated for each format, with more
+detailed explanations following.
+
+.. list-table::
+ :widths: 55 15 15 15
+ :header-rows: 1
+
+ * - Criterion
+ - ReST
+ - Markdown
+ - Google Docs
+ * - Straightforward integration with existing docs
+ - ✅
+ - ❌
+ - ❌
+ * - Indexable on `pigweed.dev <https://pigweed.dev>`_
+ - ✅
+ - ✅
+ - ❌
+ * - Auditable through source control
+ - ✅
+ - ✅
+ - ❌
+ * - Archive of review comments and changes
+ - ✅
+ - ✅
+ - ❌
+ * - Accessible to contributors
+ - ❌
+ - ✅
+ - ✅
+ * - Extensive styling and formatting options
+ - ✅
+ - ❌
+ - ✅
+ * - Easy sharing between Google and external contributors
+ - ✅
+ - ✅
+ - ❌
+
+Integration
+^^^^^^^^^^^
+.. admonition:: Goal
+
+ SEED documents should seamlessly integrate with the rest of Pigweed's docs.
+
+As all of Pigweed's documentation is written using ReST, it becomes a natural
+choice for SEEDs. The use of other formats requires additional scaffolding and
+may not provide as seamless of an experience.
+
+Indexability
+^^^^^^^^^^^^
+.. admonition:: Goal
+
+ Design decisions in SEEDs should be readily available for Pigweed users.
+
+`pigweed.dev <https://pigweed.dev>`_ has a search function allowing users to
+search the site for Pigweed-related keywords. As SEEDs contain design discussion
+and rationales, having them appear in these searches offers useful information
+to users.
+
+The search function is provided by Pigweed's Sphinx build, so only documents
+which exist as part of that (ReST / Markdown) are indexed.
+
+Auditability
+^^^^^^^^^^^^
+.. admonition:: Goal
+
+ Changes to SEED documents should be reviewed and recorded.
+
+ReST and Markdown documents exist directly within Pigweed's source repository
+after being submitted, requiring any further changes to go through a code
+review process.
+
+Conversely, Google Docs may be edited by anyone with access, making them prone
+to unintentional modification.
+
+Archive of discussions
+^^^^^^^^^^^^^^^^^^^^^^
+.. admonition:: Goal
+
+ Discussions during the review of a SEED should be well-archived for
+ future reference.
+
+ReST and Markdown documentation are submitted through Gerrit and follow the
+standard code review process. Review comments on the changes are saved in
+Gerrit and are easily revisited. Incremental updates to the SEED during the
+review process are saved as patch sets.
+
+Comments in Google Docs are more difficult to find once they are resolved, and
+document changes do not exist as clearly-defined snapshots, making the history
+of a SEED harder to follow.
+
+Accessibility
+^^^^^^^^^^^^^
+.. admonition:: Goal
+
+ SEEDs should be easy for contributors to write.
+
+Both Markdown and Google Docs are easy to write, familiar to many, and have
+extensive tooling available. SEED documents can be written outside of the
+Pigweed ecosystem using authors' preferred tools.
+
+ReST, on the other hand, is an unfamiliar and occasionally strange format, and
+its usage for SEEDs is heavily tied to Pigweed's documentation build. Authors
+are required to set up and constantly re-run this build, slowing iteration.
+
+Format and styling
+^^^^^^^^^^^^^^^^^^
+.. admonition:: Goal
+
+ SEED authors should have options for formatting various kinds of information
+ and data in their proposals.
+
+Markdown intentionally only offers limited control over document formatting,
+whereas ReST has a wide selection of directives and Google Docs functions as a
+traditional WYSIWYG editor, making them far more flexible.
+
+Sharing between Google and non-Google
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+.. admonition:: Goal
+
+ Both Google and non-Google contributors should easily be able to write and
+ review SEEDs.
+
+Due to security and legal concerns, managing ownership of Google Docs between
+internal and external contributors is nontrivial.
+
+Text documentation formats like Markdown and ReST live within the Pigweed
+repository, and as such follow the standard code contribution process.
diff --git a/seed/0001-the-seed-process/seed-index-gerrit.png b/seed/0001-the-seed-process/seed-index-gerrit.png
new file mode 100644
index 000000000..f4b70a2ff
--- /dev/null
+++ b/seed/0001-the-seed-process/seed-index-gerrit.png
Binary files differ
diff --git a/seed/0002-template.rst b/seed/0002-template.rst
new file mode 100644
index 000000000..d6700a361
--- /dev/null
+++ b/seed/0002-template.rst
@@ -0,0 +1,117 @@
+.. _seed-0002:
+
+===================
+0002: SEED Template
+===================
+
+.. card::
+ :fas:`seedling` SEED-0002: :ref:`SEED Template<seed-0002>`
+
+ :octicon:`comment-discussion` Status:
+ :bdg-primary:`Open for Comments`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Last Call`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Accepted`
+ :octicon:`kebab-horizontal`
+ :bdg-secondary-line:`Rejected`
+
+ :octicon:`calendar` Proposal Date: 2022-11-30
+
+ :octicon:`code-review` CL: `pwrev/123090 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/123090>`_
+
+-------
+Summary
+-------
+Write a brief paragraph outlining your proposal. If the SEED supersedes another,
+mention it here.
+
+----------
+Motivation
+----------
+Describe the purpose of this proposal. What problem is it trying to solve?
+Include any relevant background information.
+
+---------------
+Guide reference
+---------------
+In this section, detail the user-facing impacts if the proposal were to be
+accepted. Treat it as a reference for the new feature, describing the changes to
+a user without background context on the proposal, with explanations of key
+concepts and examples.
+
+For example, if the proposal adds a new library, this section should describe
+its public API, and be written in the style of API documentation.
+
+---------------------
+Problem investigation
+---------------------
+This section contains detailed research into the problem at hand. Crucially, the
+focus here is on the problem itself --- *not* the proposed solution. The purpose
+is to convince stakeholders that the proposal is addressing the right issues:
+there needs to be agreement on the *why* before they can think about the *how*.
+
+Generally, this section should be the primary focus of the proposal.
+
+Considerations which should be made in the investigation include:
+
+- How does this problem currently impact stakeholders?
+
+- What kinds of use cases, both specific and general, are being addressed? What
+ are their priorities? What use cases are *not* being considered, and why?
+
+- Examination of any "prior art". Are there existing solutions to this problem
+ elsewhere? What attempts have previously been made, and how do they compare to
+ this proposal? What are the benefits and drawbacks of each?
+
+- Is there a workaround for this problem currently in use? If so, how does it
+ work, and what are its drawbacks?
+
+- Are there existing Pigweed decisions or policies which are relevant to this
+ issue, and can anything be learned from them?
+
+- If the problem involves data not under Pigweed's control, consider the
+ security and privacy implications.
+
+The depth of the problem investigation should increase proportionally to the
+scope and impact of the problem.
+
+---------------
+Detailed design
+---------------
+If the proposal includes code changes, the detailed design section should
+explain the implementation at a technical level. It should be sufficiently
+detailed for a developer to understand how to implement the proposal.
+
+Any of the following should be discussed in the design, if applicable:
+
+- API design.
+- Internal data storage / layout.
+- Interactions with existing systems.
+- Similar existing Pigweed APIs: consider framework-wide consistency.
+- Testing strategies.
+- Performance.
+- Code / memory size requirements.
+- If an existing implementation is being changed, will there be backwards
+ compatibility?
+
+This section may also choose to link to a prototype implementation if one is
+written.
+
+------------
+Alternatives
+------------
+Describe what alternative solutions to the problem were considered, referencing
+the problem investigation as necessary.
+
+Additionally, consider the outcome if the proposal were not to be accepted.
+
+--------------
+Open questions
+--------------
+Are there any areas of the problem that are not addressed by this proposal, and
+is there a plan to resolve them? Does this proposal have implications beyond its
+described scope?
+
+If there are anticipated to be additional SEEDs to address outstanding issues,
+reference them here, linking to them if they already exist.
diff --git a/seed/0101-pigweed.json.rst b/seed/0101-pigweed.json.rst
new file mode 100644
index 000000000..b8def7f82
--- /dev/null
+++ b/seed/0101-pigweed.json.rst
@@ -0,0 +1,210 @@
+.. _seed-0101:
+
+==================
+0101: pigweed.json
+==================
+
+.. card::
+ :fas:`seedling` SEED-0101: :ref:`pigweed.json<seed-0101>`
+
+ :octicon:`comment-discussion` Status:
+ :bdg-secondary-line:`Open for Comments`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Last Call`
+ :octicon:`chevron-right`
+ :bdg-primary:`Accepted`
+ :octicon:`kebab-horizontal`
+ :bdg-secondary-line:`Rejected`
+
+ :octicon:`calendar` Proposal Date: 2023-02-06
+
+ :octicon:`code-review` CL: `pwrev/128010 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/128010>`_
+
+-------
+Summary
+-------
+Combine several of the configuration options downstream projects use to
+configure parts of Pigweed in one place, and use this place for further
+configuration options.
+
+----------
+Motivation
+----------
+Pigweed-based projects configure Pigweed and themselves in a variety of ways.
+The environment setup is controlled by a JSON file that's referenced in
+``bootstrap.sh`` files and in internal infrastructure repos that looks
+something like this:
+
+.. code-block::
+
+ {
+ "root_variable": "<PROJNAME>_ROOT",
+ "cipd_package_files": ["tools/default.json"],
+ "virtualenv": {
+ "gn_args": ["dir_pw_third_party_stm32cube=\"\""],
+ "gn_root": ".",
+ "gn_targets": [":python.install"]
+ },
+ "optional_submodules": ["vendor/shhh-secret"],
+ "gni_file": "build_overrides/pigweed_environment.gni"
+ }
+
+The plugins to the ``pw`` command-line utility are configured in ``PW_PLUGINS``,
+which looks like this:
+
+.. code-block::
+
+ # <name> <Python module> <function>
+ console pw_console.__main__ main
+ format pw_presubmit.format_code _pigweed_upstream_main
+
+In addition, changes have been proposed to configure some of the behavior of
+``pw format`` and the formatting steps of ``pw presubmit`` from config files,
+but there's no standard place to put these configuration options.
+
+---------------
+Guide reference
+---------------
+This proposal affects two sets of people: developers looking to use Pigweed,
+and developers looking to add configurable features to Pigweed.
+
+Developers looking to use Pigweed will have one config file that contains all
+the options they need to set. Documentation for individual Pigweed modules will
+show only the configuration options relevant for that module, and multiple of
+these examples can simply be concatenated to form a valid config file.
+
+Developers looking to add configurable features to Pigweed no longer need to
+define a new file format, figure out where to find it in the tree (or how to
+have Pigweed-projects specify a location), or parse this format.
+
+---------------------
+Problem investigation
+---------------------
+There are multiple issues with the current system that need to be addressed.
+
+* ``PW_PLUGINS`` works, but is a narrow custom format with exactly one purpose.
+* The environment config file is somewhat extensible, but is still specific to
+ environment setup.
+* There's no accepted place for other modules to retrieve configuration options.
+
+These should be combined into a single file. There are several formats that
+could be selected, and many more arguments for and against each. Only a subset
+of these arguments are reproduced here.
+
+* JSON does not support comments
+* JSON5 is not supported in the Python standard library
+* XML is too verbose
+* YAML is acceptable, but implicit type conversion could be a problem, and it's
+ not supported in the Python standard library
+* TOML is acceptable, and `was selected for a similar purpose by Python
+ <https://snarky.ca/what-the-heck-is-pyproject-toml/>`_, but it's
+ not supported in the Python standard library before Python v3.11
+* Protobuf Text Format is acceptable and widely used within Google, but is not
+ supported in the Python standard library
+
+The location of the file is also an issue. Environment config files can be found
+in a variety of locations depending on the project—all of the following paths
+are used by at least one internal Pigweed-based project.
+
+* ``build/environment.json``
+* ``build/pigweed/env_setup.json``
+* ``environment.json``
+* ``env_setup.json``
+* ``pw_env_setup.json``
+* ``scripts/environment.json``
+* ``tools/environment.json``
+* ``tools/env_setup.json``
+
+``PW_PLUGINS`` files can in theory be in any directory and ``pw`` will search up
+for them from the current directory, but in practice they only exist at the root
+of checkouts. Having this file in a fixed location with a fixed name makes it
+significantly easier to find as a user, and the fixed name (if not path) makes
+it easy to find programmatically too.
+
+---------------
+Detailed design
+---------------
+The ``pw_env_setup`` Python module will provide an API to retrieve a parsed
+``pigweed.json`` file from the root of the checkout. ``pw_env_setup`` is the
+correct location because it can't depend on anything else, but other modules can
+depend on it. Code in other languages does not yet depend on configuration
+files.
+
+A ``pigweed.json`` file might look like the following. Individual option names
+and structures are not final but will evolve as those options are
+implemented—this is merely an example of what an actual file could look like.
+The ``pw`` namespace is reserved for Pigweed, but other projects can use other
+namespaces for their own needs. Within the ``pw`` namespace all options are
+first grouped by their module name, which simplifies searching for the code and
+documentation related to the option in question.
+
+.. code-block::
+
+ {
+ "pw": {
+ "pw_cli": {
+ "plugins": {
+ "console": {
+ "module": "pw_console.__main__",
+ "function": "main"
+ },
+ "format": {
+ "module": "pw_presubmit.format_code",
+ "function": "_pigweed_upstream_main"
+ }
+ }
+ },
+ "pw_env_setup": {
+ "root_variable": "<PROJNAME>_ROOT",
+ "rosetta": "allow",
+ "gni_file": "build_overrides/pigweed_environment.gni",
+ "cipd": {
+ "package_files": [
+ "tools/default.json"
+ ]
+ },
+ "virtualenv": {
+ "gn_args": [
+ "dir_pw_third_party_stm32cube=\"\""
+ ],
+ "gn_targets": [
+ "python.install"
+ ],
+ "gn_root": "."
+ },
+ "submodules": {
+ "optional": [
+ "vendor/shhh-secret"
+ ]
+ }
+ },
+ "pw_presubmit": {
+ "format": {
+ "python": {
+ "formatter": "black",
+ "black_path": "pyink"
+ }
+ }
+ }
+ }
+ }
+
+Some teams will resist a new file at the root of their checkout, but this seed
+won't be adding any files, it'll be combining at least one top-level file, maybe
+two, into a new top-level file, so there won't be any additional files in the
+checkout root.
+
+------------
+Alternatives
+------------
+``pw format`` and the formatting steps of ``pw presubmit`` could read from yet
+another config file, further fracturing Pigweed's configuration.
+
+A different file format could be chosen over JSON. Since JSON is parsed into
+only Python lists, dicts, and primitives, switching to another format that can
+be parsed into the same internal structure should be trivial.
+
+--------------
+Open questions
+--------------
+None?
diff --git a/seed/0102-module-docs.rst b/seed/0102-module-docs.rst
new file mode 100644
index 000000000..5ca9ab5bf
--- /dev/null
+++ b/seed/0102-module-docs.rst
@@ -0,0 +1,203 @@
+.. _seed-0102:
+
+=====================================
+0102: Consistent Module Documentation
+=====================================
+
+.. card::
+ :fas:`seedling` SEED-0102: :ref:`Consistent Module Documentation<seed-0102>`
+
+ :octicon:`comment-discussion` Status:
+ :bdg-secondary-line:`Open for Comments`
+ :octicon:`chevron-right`
+ :bdg-secondary-line:`Last Call`
+ :octicon:`chevron-right`
+ :bdg-primary:`Accepted`
+ :octicon:`kebab-horizontal`
+ :bdg-secondary-line:`Rejected`
+
+ :octicon:`calendar` Proposal Date: 2023-02-10
+
+ :octicon:`code-review` CL: `pwrev/128811 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/128811>`_, `pwrev/130410 <https://pigweed-review.git.corp.google.com/c/pigweed/pigweed/+/130410>`_
+
+-------
+Summary
+-------
+Pigweed modules ought to have documentation that is reasonably comprehensive,
+that has a consistent and predictable format, and that provides the reader with
+sufficient information to judge whether using the module is the right choice for
+them. This SEED proposes a documentation philosophy applicable to all Pigweed
+modules and a flexible yet consistent structure to which all module docs should
+conform.
+
+Definitions
+-----------
+In this SEED, we define *users* as developers using Pigweed in downstream
+projects, and *maintainers* as developers working on upstream Pigweed. The
+primary focus of this SEED is to improve the documentation experience for users.
+
+----------
+Motivation
+----------
+Currently, each Pigweed module is required to have, at minimum, a single
+``docs.rst`` file that contains the module's documentation. This gives the
+module maintainer considerable discretion to provide as much or as little
+documentation as they would like. However, this approach fails for Pigweed
+maintainers by providing no guidance or structure to help them write effective
+documentation, and certainly fails Pigweed users who struggle to find the
+information they're looking for. So a solution needs to make it easier for
+Pigweed maintainers to write good documentation, thereby making Pigweed much
+more accessible to its users.
+
+Pigweed's design is inherently and intentionally modular. So documentation at
+the level of the *module* is the most natural place to make impactful
+improvements, while avoiding a fundamental restructuring of the Pigweed
+documentation. Module docs are also what the majority of Pigweed users rely on
+most. As a result, this SEED is focused exclusively on improving module
+documentation.
+
+`Diátaxis <https://diataxis.fr/>`_ proposes a four-mode framework for technical
+documentation, illustrated below with terminology altered to better match
+Pigweed's needs:
+
+.. csv-table::
+ :widths: 10, 20, 20
+
+ , "**Serve our study**", "**Serve our work**"
+ "**Practical steps**", "Tutorials (`learning-oriented <https://diataxis.fr/tutorials/>`_)", "Guides (`task-oriented <https://diataxis.fr/how-to-guides/>`_)"
+ "**Theoretical knowledge**", "Concept & design docs (`understanding-oriented <https://diataxis.fr/explanation/>`_)", "Interface reference (`information-oriented <https://diataxis.fr/reference/>`_)"
+
+Pigweed needs a framework that ensures modules have coverage across these four
+quadrants. That framework should provide a structure that makes it easier for
+maintainers to write effective documentation, and a single page that provides
+the most basic information a user needs to understand the module.
+
+Alternatives
+------------
+There are risks to focusing on module docs:
+
+* The most useful docs are those that focus on tasks rather than system
+ features. The module-focused approach risks producing feature-focused docs
+ rather than task-focused docs, since the tasks users need to complete may not
+ fit within the boundaries of a module.
+
+* Likewise, focusing on module documentation reduces focus on content that
+ integrates across multiple modules.
+
+The justification for focusing on module documentation doesn't imply that module
+docs are the *only* docs that matter. Higher level introductory and guidance
+material that integrates Pigweed as a system and covers cross cutting concerns
+is also important, and would arguably be more effective at bringing new
+developers into the Pigweed ecosystem. However, this SEED proposes focusing on
+module docs for two primary reasons:
+
+1. Improving module docs and providing them with a consistent structure will
+ have the largest impact with the least amount of investment.
+
+2. It will be easier to construct higher-level and cross-cutting documentation
+ from well-developed module docs compared to going the other direction.
+
+While not a primary consideration, a bonus of a module-focused approach is that
+modules already have owners, and those owners are natural candidates to be the
+maintainers of their modules' docs.
+
+--------
+Proposal
+--------
+This change would require each module directory to match this structure::
+
+ module root directory/
+ ├── docs.rst
+ ├── concepts.rst [or concepts/...] [when needed]
+ ├── design.rst [or design/...] [when needed]
+ ├── guides.rst [or guides/...] [when needed]
+ │
+ ├── tutorials/ [aspirational]
+ │ ├── index.rst
+ │ └── ...
+ │
+ ├── api.rst [or api/...] [if applicable]
+ ├── cli.rst [if applicable]
+ └── gui.rst [if applicable]
+
+Fundamental module docs
+-----------------------
+These three documents are the minimum required of every Pigweed module.
+
+The basics: ``docs.rst``
+^^^^^^^^^^^^^^^^^^^^^^^^
+Basic, structured information about the module, including what it does, what
+problems it's designed solve, and information that lets a user quickly evaluate
+if the module is useful to them.
+
+How it works and why: ``design.rst`` & ``concepts.rst`` (understanding-oriented)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Background on the design goals, assumptions, limitations, and implementation
+details of a module, and may contrast the design of the module with alternative
+solutions.
+
+This content can start in the "Design considerations" section of the index, and
+grow into this separate document as the module matures. If that document becomes
+too large, the single ``design.rst`` file can be replaced by a ``design``
+subdirectory containing more than one nested doc.
+
+Some modules may need documentation on fundamental concepts that are independent
+of the module's solution. For example, a module that provides a reliable
+transport layer may include a conceptual description of reliable transport in
+general in a ``concepts.rst`` file or ``concepts`` subdirectory.
+
+How to get stuff done: ``guides.rst`` (task-oriented)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+These are focused on specific outcomes and should be produced as soon as we see
+a question being answered multiple times. Each module should have at least one
+guide on integrating the module into a project, and one guide on the most common
+use case.
+
+This content can start in the "Getting started" section of the index, and grow
+into this separate document as the module matures. If that document becomes too
+large, it can be replaced with a ``guides`` subdirectory containing more than
+one doc.
+
+Interface docs (information-oriented)
+-------------------------------------
+These docs describe the module's interfaces. Each of these docs may be omitted
+if the module doesn't include an applicable interface.
+
+``api.rst``: External API reference
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Modules should have reference documentation for their user-facing APIs. Modules
+that have APIs for multiple languages should replace the single ``api.rst`` with
+an ``api`` subdirectory with docs for each supported language.
+
+How API docs should be structured, generated, and maintained is a complex topic
+that this SEED will not determine.
+
+``cli.rst`` & ``gui.rst``: Developer tools reference
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+A user-facing command line interface (CLI) should be documented in ``cli.rst``
+if the module provides one. It's ideal if this documentation closely matches the
+output of the CLI tool's "help" command.
+
+If the module provides a graphical user interface (GUI) (including text mode
+interfaces and web front-ends), its documentation should be included in
+``gui.rst``.
+
+Tutorials (learning-oriented)
+-----------------------------
+We keep these as separate files in ``tutorials``. These take considerable effort
+to develop, so they aren't *required*, but we aspire to develop them for all but
+the most trivial modules.
+
+When one size does not fit all
+------------------------------
+Pigweed modules span a spectrum of complexity, from relatively simple embedded
+libraries to sophisticated communication protocols and host-side developer
+tooling. The structure described above should be the starting point for each
+module's documentation and should be appropriate to the vast majority of
+modules. But this proposal is not strictly prescriptive; modules with
+documentation needs that are not met by this structure are free to deviate from
+it by *adding* docs that are not mentioned here.
+
+Examples
+--------
+A template for implementing this structure can be found ``docs/templates/docs``.
diff --git a/seed/BUILD.gn b/seed/BUILD.gn
new file mode 100644
index 000000000..6a5158fc6
--- /dev/null
+++ b/seed/BUILD.gn
@@ -0,0 +1,44 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_docgen/docs.gni")
+
+pw_doc_group("docs") {
+ sources = [ "0000-index.rst" ]
+ group_deps = [
+ ":0001",
+ ":0002",
+ ":0101",
+ ":0102",
+ ]
+}
+
+pw_doc_group("0001") {
+ sources = [ "0001-the-seed-process.rst" ]
+ inputs = [ "0001-the-seed-process/seed-index-gerrit.png" ]
+}
+
+pw_doc_group("0002") {
+ sources = [ "0002-template.rst" ]
+}
+
+pw_doc_group("0101") {
+ sources = [ "0101-pigweed.json.rst" ]
+}
+
+pw_doc_group("0102") {
+ sources = [ "0102-module-docs.rst" ]
+}
diff --git a/targets/arduino/BUILD.bazel b/targets/arduino/BUILD.bazel
index fcfb66065..07e2150c0 100644
--- a/targets/arduino/BUILD.bazel
+++ b/targets/arduino/BUILD.bazel
@@ -26,17 +26,14 @@ pw_cc_library(
srcs = [
"init.cc",
],
+ hdrs = [
+ ],
+ # TODO(b/259149817): pw_sys_io_arduino cannot find "Arduino.h"
+ tags = ["manual"],
deps = [
+ "//pw_arduino_build:pw_arduino_build_header",
"//pw_preprocessor",
"//pw_sys_io_arduino",
- ],
-)
-
-pw_cc_library(
- name = "system_rpc_server",
- srcs = ["system_rpc_server.cc"],
- deps = [
- "//pw_hdlc:pw_rpc",
- "//pw_rpc/system_server:facade",
+ "//pw_sys_io_arduino:pw_sys_io_arduino_header",
],
)
diff --git a/targets/arduino/BUILD.gn b/targets/arduino/BUILD.gn
index 0032273c1..2f62200c2 100644
--- a/targets/arduino/BUILD.gn
+++ b/targets/arduino/BUILD.gn
@@ -98,17 +98,6 @@ if (pw_arduino_build_CORE_PATH != "") {
"$dir_pw_preprocessor",
]
}
-
- pw_source_set("system_rpc_server") {
- deps = [
- "$dir_pw_hdlc:pw_rpc",
- "$dir_pw_hdlc:rpc_channel_output",
- "$dir_pw_rpc/system_server:facade",
- "$dir_pw_stream:sys_io_stream",
- dir_pw_log,
- ]
- sources = [ "system_rpc_server.cc" ]
- }
}
} else {
config("arduino_build") {
diff --git a/targets/arduino/system_rpc_server.cc b/targets/arduino/system_rpc_server.cc
deleted file mode 100644
index 52be51435..000000000
--- a/targets/arduino/system_rpc_server.cc
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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 <cstddef>
-
-#include "pw_hdlc/rpc_channel.h"
-#include "pw_hdlc/rpc_packets.h"
-#include "pw_log/log.h"
-#include "pw_rpc_system_server/rpc_server.h"
-#include "pw_stream/sys_io_stream.h"
-
-namespace pw::rpc::system_server {
-namespace {
-
-constexpr size_t kMaxTransmissionUnit = 256;
-
-// Used to write HDLC data to pw::sys_io.
-stream::SysIoWriter writer;
-stream::SysIoReader reader;
-
-// Set up the output channel for the pw_rpc server to use.
-hdlc::RpcChannelOutput hdlc_channel_output(writer,
- pw::hdlc::kDefaultRpcAddress,
- "HDLC channel");
-Channel channels[] = {pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
-rpc::Server server(channels);
-
-} // namespace
-
-void Init() {
- // Send log messages to HDLC address 1. This prevents logs from interfering
- // with pw_rpc communications.
- pw::log_basic::SetOutput([](std::string_view log) {
- pw::hdlc::WriteUIFrame(1, std::as_bytes(std::span(log)), writer);
- });
-}
-
-rpc::Server& Server() { return server; }
-
-Status Start() {
- // Declare a buffer for decoding incoming HDLC frames.
- std::array<std::byte, kMaxTransmissionUnit> input_buffer;
- hdlc::Decoder decoder(input_buffer);
-
- while (true) {
- std::byte byte;
- Status ret_val = pw::sys_io::ReadByte(&byte);
- if (!ret_val.ok()) {
- return ret_val;
- }
- if (auto result = decoder.Process(byte); result.ok()) {
- hdlc::Frame& frame = result.value();
- if (frame.address() == hdlc::kDefaultRpcAddress) {
- server.ProcessPacket(frame.data(), hdlc_channel_output);
- }
- }
- }
-}
-
-} // namespace pw::rpc::system_server
diff --git a/targets/arduino/target_toolchains.gni b/targets/arduino/target_toolchains.gni
index dfc9b8350..3b74b8b96 100644
--- a/targets/arduino/target_toolchains.gni
+++ b/targets/arduino/target_toolchains.gni
@@ -47,9 +47,9 @@ _target_config = {
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
"$dir_pw_sync_baremetal:interrupt_spin_lock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+ pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
pw_sys_io_BACKEND = dir_pw_sys_io_arduino
- pw_rpc_system_server_BACKEND =
- "$dir_pigweed/targets/arduino:system_rpc_server"
+ pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
pw_arduino_build_INIT_BACKEND = "$dir_pigweed/targets/arduino:pre_init"
pw_build_LINK_DEPS = [
diff --git a/targets/default_config.BUILD b/targets/default_config.BUILD
index 2ed1d01f1..b2d33d5a5 100644
--- a/targets/default_config.BUILD
+++ b/targets/default_config.BUILD
@@ -14,6 +14,18 @@
package(default_visibility = ["//visibility:public"])
+# TODO(b/236321905): Support backends other than boringSSL.
+label_flag(
+ name = "pw_crypto_sha256_backend",
+ build_setting_default = "@pigweed//pw_crypto:sha256_boringssl",
+)
+
+# TODO(b/236321905): Support backends other than boringSSL.
+label_flag(
+ name = "pw_crypto_ecdsa_backend",
+ build_setting_default = "@pigweed//pw_crypto:ecdsa_boringssl",
+)
+
label_flag(
name = "pw_log_backend",
build_setting_default = "@pigweed//pw_log:backend_multiplexer",
@@ -25,6 +37,11 @@ label_flag(
)
label_flag(
+ name = "pw_log_tokenized_handler_backend",
+ build_setting_default = "@pigweed//pw_log_tokenized:base64_over_hdlc",
+)
+
+label_flag(
name = "pw_assert_backend",
build_setting_default = "@pigweed//pw_assert:backend_multiplexer",
)
@@ -70,6 +87,11 @@ label_flag(
)
label_flag(
+ name = "pw_sync_recursive_mutex_backend",
+ build_setting_default = "@pigweed//pw_sync:recursive_mutex_backend_multiplexer",
+)
+
+label_flag(
name = "pw_sync_interrupt_spin_lock_backend",
build_setting_default = "@pigweed//pw_sync:interrupt_spin_lock_backend_multiplexer",
)
@@ -100,6 +122,11 @@ label_flag(
)
label_flag(
+ name = "pw_thread_iteration_backend",
+ build_setting_default = "@pigweed//pw_thread:iteration_backend_multiplexer",
+)
+
+label_flag(
name = "pw_thread_sleep_backend",
build_setting_default = "@pigweed//pw_thread:sleep_backend_multiplexer",
)
@@ -115,21 +142,36 @@ label_flag(
)
label_flag(
- name = "pw_tokenizer_global_handler_backend",
- build_setting_default = "@pigweed//pw_tokenizer:test_backend",
+ name = "pw_sys_io_backend",
+ build_setting_default = "@pigweed//pw_sys_io:backend_multiplexer",
)
label_flag(
- name = "pw_tokenizer_global_handler_with_payload_backend",
- build_setting_default = "@pigweed//pw_tokenizer:test_backend",
+ name = "pw_system_target_hooks_backend",
+ build_setting_default = "@pigweed//pw_system:target_hooks_multiplexer",
)
label_flag(
- name = "pw_sys_io_backend",
- build_setting_default = "@pigweed//pw_sys_io:backend_multiplexer",
+ name = "pw_unit_test_googletest_backend",
+ build_setting_default = "@pigweed//pw_unit_test:light",
)
label_flag(
name = "target_rtos",
build_setting_default = "@pigweed//pw_build/constraints/rtos:none",
)
+
+label_flag(
+ name = "pw_perf_test_timer_backend",
+ build_setting_default = "@pigweed//pw_perf_test:timer_multiplexer",
+)
+
+label_flag(
+ name = "pw_trace_backend",
+ build_setting_default = "@pigweed//pw_trace:backend_multiplexer",
+)
+
+label_flag(
+ name = "freertos_config",
+ build_setting_default = "@pigweed//third_party/freertos:freertos_config",
+)
diff --git a/targets/docs/BUILD.bazel b/targets/docs/BUILD.bazel
new file mode 100644
index 000000000..e342830b0
--- /dev/null
+++ b/targets/docs/BUILD.bazel
@@ -0,0 +1,24 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+filegroup(
+ name = "tokenized_log_handler",
+ srcs = [
+ "tokenized_log_handler.cc",
+ ],
+)
diff --git a/targets/docs/BUILD.gn b/targets/docs/BUILD.gn
index 8f4124736..e6c723a56 100644
--- a/targets/docs/BUILD.gn
+++ b/targets/docs/BUILD.gn
@@ -16,14 +16,27 @@ import("//build_overrides/pigweed.gni")
import("$dir_pigweed/targets/stm32f429i_disc1/target_toolchains.gni")
import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_log_tokenized/backend.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_third_party/nanopb/nanopb.gni")
import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
import("$dir_pw_toolchain/generate_toolchain.gni")
+import("$dir_pw_toolchain/traits.gni")
+
+if (current_toolchain != default_toolchain) {
+ pw_source_set("tokenized_log_handler") {
+ deps = [
+ "$dir_pw_bytes",
+ "$dir_pw_log_tokenized:handler.facade",
+ "$dir_pw_sys_io",
+ ]
+ sources = [ "tokenized_log_handler.cc" ]
+ }
+}
# Toolchain for generating upstream Pigweed documentation.
generate_toolchain("docs") {
- # Use the stm32f429i-disc1 toolchain for pw_size_report targets.
+ # Use the stm32f429i-disc1 toolchain for pw_size_diff targets.
_base_toolchain = pw_target_toolchain_stm32f429i_disc1.size_optimized
forward_variables_from(_base_toolchain,
"*",
@@ -36,6 +49,16 @@ generate_toolchain("docs") {
# This is the docs target.
pw_docgen_BUILD_DOCS = true
+
+ # Disable NC tests in case the base toolchain has them enabled.
+ pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = false
+
+ pw_assert_BACKEND = dir_pw_assert_log
+ pw_log_BACKEND = dir_pw_log_tokenized
+ pw_log_tokenized_HANDLER_BACKEND =
+ get_path_info(":tokenized_log_handler", "abspath")
+
+ pw_toolchain_CXX_STANDARD = pw_toolchain_STANDARD.CXX20
}
}
diff --git a/targets/docs/target_docs.rst b/targets/docs/target_docs.rst
index 37db6c248..e130e8a17 100644
--- a/targets/docs/target_docs.rst
+++ b/targets/docs/target_docs.rst
@@ -6,6 +6,9 @@ docs
The Pigweed docs target assembles Pigweed's reStructuredText and markdown
documentation into a collection of HTML pages.
+The docs target uses the :ref:`target-stm32f429i-disc1` toolchain with C++20 for
+size reports.
+
Building
========
To build for this target, invoke ninja with the top-level "docs" group as the
diff --git a/targets/docs/tokenized_log_handler.cc b/targets/docs/tokenized_log_handler.cc
new file mode 100644
index 000000000..410c73645
--- /dev/null
+++ b/targets/docs/tokenized_log_handler.cc
@@ -0,0 +1,24 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 "pw_bytes/span.h"
+#include "pw_log_tokenized/handler.h"
+#include "pw_sys_io/sys_io.h"
+
+extern "C" void pw_log_tokenized_HandleLog(uint32_t,
+ const uint8_t encoded_message[],
+ size_t size_bytes) {
+ pw::sys_io::WriteBytes(pw::ConstByteSpan(
+ reinterpret_cast<const std::byte*>(encoded_message), size_bytes));
+}
diff --git a/targets/emcraft_sf2_som/BUILD.bazel b/targets/emcraft_sf2_som/BUILD.bazel
index 26d04260a..92ee012b4 100644
--- a/targets/emcraft_sf2_som/BUILD.bazel
+++ b/targets/emcraft_sf2_som/BUILD.bazel
@@ -32,6 +32,11 @@ pw_cc_library(
"config/FreeRTOSConfig.h",
"config/sf2_mss_hal_conf.h",
],
+ # TODO(b/260642311): This target doesn't build
+ tags = ["manual"],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
"//pw_boot",
"//pw_boot_cortex_m",
@@ -49,6 +54,10 @@ pw_cc_binary(
srcs = [
"main.cc",
],
+ tags = ["manual"],
+ target_compatible_with = [
+ "//pw_build/constraints/rtos:freertos",
+ ],
deps = [
"//pw_thread:thread",
"//pw_thread:thread_core",
diff --git a/targets/emcraft_sf2_som/BUILD.gn b/targets/emcraft_sf2_som/BUILD.gn
index 9d778698e..36bd46b70 100644
--- a/targets/emcraft_sf2_som/BUILD.gn
+++ b/targets/emcraft_sf2_som/BUILD.gn
@@ -14,6 +14,7 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/linker_script.gni")
import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_malloc/backend.gni")
@@ -28,7 +29,73 @@ config("pw_malloc_active") {
}
}
+config("emcraft_ddr_init") {
+ # Emcraft's DDR config must be manually set by a custom init function. This
+ # conflicts with the built-in MSS init function. I have not looked into this
+ # myself to see if it's absoutely needed or not.
+ defines = [ "MSS_SYS_MDDR_CONFIG_BY_CORTEX=0" ]
+}
+
if (current_toolchain != default_toolchain) {
+ pw_linker_script("mddr_debug_linker_script") {
+ defines = [
+ "PW_BOOT_CODE_BEGIN=0x00000200", # After vector table.
+
+ # TODO(skeys) Bootloader is capable of loading 16M of uncompressed code
+ # from SPI flash to external RAM. For now use the allocated eNVM flash
+ # (256K - Bootloader - InSystemProgrammer = 192K)
+ "PW_BOOT_CODE_SIZE=0x30000",
+
+ # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
+ # least 6K bytes in heap when using pw_malloc_freelist. The heap size
+ # required for tests should be investigated.
+ "PW_BOOT_HEAP_SIZE=4M",
+
+ # With external RAM remapped, we use the entire internal ram for the
+ # stack (64K).
+ "PW_BOOT_MIN_STACK_SIZE=64K",
+
+ # Using external DDR RAM, we just need to make sure we go past our ROM
+ # sections.
+ "PW_BOOT_RAM_BEGIN=0xA1000000",
+
+ # We assume that the bootloader loaded all 16M of text.
+ "PW_BOOT_RAM_SIZE=48M",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x00000000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ ]
+ linker_script = "emcraft_sf2_som_mddr_debug.ld"
+ }
+ pw_linker_script("mddr_production_linker_script") {
+ defines = [
+ "PW_BOOT_FLASH_BEGIN=0x00000200", # After vector table.
+
+ # TODO(skeys) Bootloader is capable of loading 16M of uncompressed code
+ # from SPI flash to external RAM. For now use the allocated eNVM flash
+ # (256K - Bootloader - InSystemProgrammer = 192K)
+ "PW_BOOT_FLASH_SIZE=0x30000",
+
+ # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
+ # least 6K bytes in heap when using pw_malloc_freelist. The heap size
+ # required for tests should be investigated.
+ "PW_BOOT_HEAP_SIZE=4M",
+
+ # With external RAM remapped, we use the entire internal ram for the
+ # stack (64K).
+ "PW_BOOT_MIN_STACK_SIZE=1024K",
+
+ # Using external DDR RAM, we just need to make sure we go past our ROM
+ # sections.
+ "PW_BOOT_RAM_BEGIN=0xA1000000",
+
+ # We assume that the bootloader loaded all 16M of text.
+ "PW_BOOT_RAM_SIZE=48M",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x00000000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ ]
+ linker_script = "$dir_pw_boot_cortex_m/basic_cortex_m.ld"
+ }
+
pw_source_set("pre_init") {
configs = [ ":pw_malloc_active" ]
deps = [
@@ -49,6 +116,7 @@ if (current_toolchain != default_toolchain) {
config("config_includes") {
include_dirs = [ "config" ]
+ configs = [ ":emcraft_ddr_init" ]
}
pw_source_set("sf2_mss_hal_config") {
@@ -74,40 +142,14 @@ pw_system_target("emcraft_sf2_som") {
build_args = {
pw_log_BACKEND = dir_pw_log_tokenized
- pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND =
- "$dir_pw_system:log_backend.impl"
+ pw_log_tokenized_HANDLER_BACKEND = "$dir_pw_system:log_backend.impl"
pw_third_party_freertos_CONFIG =
"$dir_pigweed/targets/emcraft_sf2_som:sf2_freertos_config"
pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm3"
pw_sys_io_BACKEND = dir_pw_sys_io_emcraft_sf2
- # Non-debug build for use with the boot loader.
- pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
- "PW_BOOT_FLASH_BEGIN=0x00000200", # After vector table.
-
- # TODO(skeys) Bootloader is capable of loading 16M of uncompressed code
- # from SPI flash to external RAM. For now use the allocated eNVM flash
- # (256K - Bootloader - InSystemProgrammer = 192K)
- "PW_BOOT_FLASH_SIZE=0x30000",
-
- # TODO(pwbug/219): Currently "pw_tokenizer/detokenize_test" requires at
- # least 6K bytes in heap when using pw_malloc_freelist. The heap size
- # required for tests should be investigated.
- "PW_BOOT_HEAP_SIZE=4M",
-
- # With external RAM remapped, we use the entire internal ram for the
- # stack (64K).
- "PW_BOOT_MIN_STACK_SIZE=1024K",
-
- # Using external DDR RAM, we just need to make sure we go past our ROM
- # sections.
- "PW_BOOT_RAM_BEGIN=0xA1000000",
-
- # We assume that the bootloader loaded all 16M of text.
- "PW_BOOT_RAM_SIZE=48M",
- "PW_BOOT_VECTOR_TABLE_BEGIN=0x00000000",
- "PW_BOOT_VECTOR_TABLE_SIZE=512",
- ]
+ pw_boot_cortex_m_LINKER_SCRIPT =
+ "//targets/emcraft_sf2_som:mddr_production_linker_script"
}
}
@@ -120,27 +162,17 @@ pw_system_target("emcraft_sf2_som_debug") {
build_args = {
pw_log_BACKEND = dir_pw_log_tokenized
- pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND =
- "$dir_pw_system:log_backend.impl"
+ pw_log_tokenized_HANDLER_BACKEND = "$dir_pw_system:log_backend.impl"
pw_third_party_freertos_CONFIG =
"$dir_pigweed/targets/emcraft_sf2_som:sf2_freertos_config"
pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm3"
pw_sys_io_BACKEND = dir_pw_sys_io_emcraft_sf2
- pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
- "PW_BOOT_FLASH_BEGIN=0x00000200",
- "PW_BOOT_FLASH_SIZE=200K",
-
- # TODO(pwbug/219): Currently "pw_tokenizer/detokenize_test" requires at
- # least 6K bytes in heap when using pw_malloc_freelist. The heap size
- # required for tests should be investigated.
- "PW_BOOT_HEAP_SIZE=7K",
- "PW_BOOT_MIN_STACK_SIZE=1K",
- "PW_BOOT_RAM_BEGIN=0x20000000",
- "PW_BOOT_RAM_SIZE=64K",
- "PW_BOOT_VECTOR_TABLE_BEGIN=0x00000000",
- "PW_BOOT_VECTOR_TABLE_SIZE=512",
- ]
+ # Override the default pw_boot_cortex_m linker script and set the memory
+ # regions for the target.
+ pw_boot_cortex_m_LINKER_SCRIPT =
+ "//targets/emcraft_sf2_som:mddr_debug_linker_script"
+ pw_third_party_smartfusion_mss_CONFIG = "debug"
}
}
diff --git a/targets/emcraft_sf2_som/OWNERS b/targets/emcraft_sf2_som/OWNERS
new file mode 100644
index 000000000..8c230af99
--- /dev/null
+++ b/targets/emcraft_sf2_som/OWNERS
@@ -0,0 +1 @@
+skeys@google.com
diff --git a/targets/emcraft_sf2_som/boot.cc b/targets/emcraft_sf2_som/boot.cc
index bb786c850..ecb2eaef8 100644
--- a/targets/emcraft_sf2_som/boot.cc
+++ b/targets/emcraft_sf2_som/boot.cc
@@ -42,6 +42,8 @@ std::array<char, configMAX_TASK_NAME_LEN> temp_thread_name_buffer;
} // namespace
+extern "C" void Reset_Handler(void);
+
// Functions needed when configGENERATE_RUN_TIME_STATS is on.
extern "C" void configureTimerForRunTimeStats(void) {}
extern "C" unsigned long getRunTimeCounterValue(void) { return 10 /* FIXME */; }
@@ -183,6 +185,13 @@ extern "C" void pw_boot_PreMainInit() {
PW_UNREACHABLE;
}
+extern "C" void sf2_SocInit() {
+#if SF2_MSS_NO_BOOTLOADER
+ Reset_Handler();
+#endif
+ pw_boot_Entry();
+}
+
// This `main()` stub prevents another main function from being linked since
// this target deliberately doesn't run `main()`.
extern "C" int main() {}
diff --git a/targets/emcraft_sf2_som/config/sf2_mss_hal_conf.h b/targets/emcraft_sf2_som/config/sf2_mss_hal_conf.h
index 0e82d6856..4deb476c7 100644
--- a/targets/emcraft_sf2_som/config/sf2_mss_hal_conf.h
+++ b/targets/emcraft_sf2_som/config/sf2_mss_hal_conf.h
@@ -14,8 +14,10 @@
#pragma once
+#include "../drivers_config/sys_config/sys_config.h"
+
#if (MSS_SYS_MDDR_CONFIG_BY_CORTEX == 1)
-#error "Please turn off DDR initialization! See the comment in this file above."
+#error "Please turn off DDR initialization! See the comment in BUILD.gn file."
#endif
#define HAL_GPIO_MODULE_ENABLED
diff --git a/targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld b/targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld
new file mode 100644
index 000000000..dd3f2c6b2
--- /dev/null
+++ b/targets/emcraft_sf2_som/emcraft_sf2_som_mddr_debug.ld
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2022 The Pigweed Authors
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/* This relatively simplified linker script will work with many ARMv7-M and
+ * ARMv8-M cores that have on-board memory-mapped RAM and FLASH. For more
+ * complex projects and devices, it's possible this linker script will not be
+ * sufficient as-is.
+ *
+ * This linker script is likely not suitable for a project with a bootloader.
+ */
+
+/* Provide useful error messages when required configurations are not set. */
+#ifndef PW_BOOT_VECTOR_TABLE_BEGIN
+#error "PW_BOOT_VECTOR_TABLE_BEGIN is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_VECTOR_TABLE_BEGIN
+
+#ifndef PW_BOOT_VECTOR_TABLE_SIZE
+#error "PW_BOOT_VECTOR_TABLE_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_VECTOR_TABLE_SIZE
+
+#ifndef PW_BOOT_CODE_BEGIN
+#error "PW_BOOT_CODE_BEGIN is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_CODE_BEGIN
+
+#ifndef PW_BOOT_CODE_SIZE
+#error "PW_BOOT_CODE_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_CODE_SIZE
+
+#ifndef PW_BOOT_RAM_BEGIN
+#error "PW_BOOT_RAM_BEGIN is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_RAM_BEGIN
+
+#ifndef PW_BOOT_RAM_SIZE
+#error "PW_BOOT_RAM_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_RAM_SIZE
+
+#ifndef PW_BOOT_HEAP_SIZE
+#error "PW_BOOT_HEAP_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_HEAP_SIZE
+
+#ifndef PW_BOOT_MIN_STACK_SIZE
+#error "PW_BOOT_MIN_STACK_SIZE is not defined, and is required to use pw_boot_cortex_m"
+#endif // PW_BOOT_MIN_STACK_SIZE
+
+
+/* Note: This technically doesn't set the firmware's entry point. Setting the
+ * firmware entry point is done by setting vector_table[1]
+ * (Reset_Handler). However, this DOES tell the compiler how to optimize
+ * when --gc-sections is enabled.
+ */
+ENTRY(pw_boot_Entry)
+
+MEMORY
+{
+ /* TODO(b/234892223): Make it possible for projects to freely customize
+ * memory regions.
+ */
+
+ /* Vector Table */
+ VECTOR_TABLE(rx) : \
+ ORIGIN = PW_BOOT_VECTOR_TABLE_BEGIN, \
+ LENGTH = PW_BOOT_VECTOR_TABLE_SIZE
+ /* External RAM being used for code. */
+ TEXT_EXTERNAL_RAM(rx) : \
+ ORIGIN = PW_BOOT_CODE_BEGIN, \
+ LENGTH = PW_BOOT_CODE_SIZE
+ /* External DDR RAM */
+ EXTERNAL_RAM(rwx) : \
+ ORIGIN = PW_BOOT_RAM_BEGIN, \
+ LENGTH = PW_BOOT_RAM_SIZE
+
+ /* Each memory region above has an associated .*.unused_space section that
+ * overlays the unused space at the end of the memory segment. These segments
+ * are used by pw_bloat.bloaty_config to create the utilization data source
+ * for bloaty size reports.
+ *
+ * These sections MUST be located immediately after the last section that is
+ * placed in the respective memory region or lld will issue a warning like:
+ *
+ * warning: ignoring memory region assignment for non-allocatable section
+ * '.VECTOR_TABLE.unused_space'
+ *
+ * If this warning occurs, it's also likely that LLD will have created quite
+ * large padded regions in the ELF file due to bad cursor operations. This
+ * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
+ * megabytes.
+ *
+ * Attempting to add sections to the memory region AFTER the unused_space
+ * section will cause the region to overflow.
+ */
+}
+
+SECTIONS
+{
+ /* This is the link-time vector table. If used, the VTOR (Vector Table Offset
+ * Register) MUST point to this memory location in order to be used. This can
+ * be done by ensuring this section exists at the default location of the VTOR
+ * so it's used on reset, or by explicitly setting the VTOR in a bootloader
+ * manually to point to &pw_boot_vector_table_addr before interrupts are
+ * enabled.
+ *
+ * The ARMv7-M architecture requires this is at least aligned to 128 bytes,
+ * and aligned to a power of two that is greater than 4 times the number of
+ * supported exceptions. 512 has been selected as it accommodates this
+ * device's vector table.
+ */
+ .vector_table : ALIGN(512)
+ {
+ pw_boot_vector_table_addr = .;
+ KEEP(*(.vector_table))
+ } >VECTOR_TABLE
+
+ /* Represents unused space in the VECTOR_TABLE segment. This MUST be the last
+ * section assigned to the VECTOR_TABLE region.
+ */
+ .VECTOR_TABLE.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE));
+ } >VECTOR_TABLE
+
+ /* Main executable code. */
+ .code : ALIGN(4)
+ {
+ CREATE_OBJECT_SYMBOLS
+ __text_load = LOADADDR(.code);
+ /* Application code. */
+ *(.text)
+ *(.text*)
+ KEEP(*(.init))
+ KEEP(*(.fini))
+
+ . = ALIGN(4);
+ /* Constants.*/
+ *(.rodata)
+ *(.rodata*)
+
+ /* .preinit_array, .init_array, .fini_array are used by libc.
+ * Each section is a list of function pointers that are called pre-main and
+ * post-exit for object initialization and tear-down.
+ * Since the region isn't explicitly referenced, specify KEEP to prevent
+ * link-time garbage collection. SORT is used for sections that have strict
+ * init/de-init ordering requirements. */
+ . = ALIGN(4);
+ PROVIDE_HIDDEN(__preinit_array_start = .);
+ KEEP(*(.preinit_array*))
+ PROVIDE_HIDDEN(__preinit_array_end = .);
+
+ PROVIDE_HIDDEN(__init_array_start = .);
+ KEEP(*(SORT(.init_array.*)))
+ KEEP(*(.init_array*))
+ PROVIDE_HIDDEN(__init_array_end = .);
+
+ PROVIDE_HIDDEN(__fini_array_start = .);
+ KEEP(*(SORT(.fini_array.*)))
+ KEEP(*(.fini_array*))
+ PROVIDE_HIDDEN(__fini_array_end = .);
+ } >TEXT_EXTERNAL_RAM
+
+ /* Used by unwind-arm/ */
+ .ARM : ALIGN(4) {
+ __exidx_start = .;
+ *(.ARM.exidx*)
+ __exidx_end = .;
+ } >TEXT_EXTERNAL_RAM
+
+ /* Explicitly initialized global and static data. (.data)*/
+ .static_init_ram : ALIGN(4)
+ {
+ *(.data)
+ *(.data*)
+ . = ALIGN(4);
+ } >EXTERNAL_RAM AT> TEXT_EXTERNAL_RAM
+
+ /* Represents unused space in the TEXT_EXTERNAL_RAM segment. This MUST be the
+ * last section assigned to the TEXT_EXTERNAL_RAM region.
+ */
+ .TEXT_EXTERNAL_RAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(TEXT_EXTERNAL_RAM) + LENGTH(TEXT_EXTERNAL_RAM));
+ } >TEXT_EXTERNAL_RAM
+
+ /* The .zero_init_ram, .heap, and .stack sections below require (NOLOAD)
+ * annotations for LLVM lld, but not GNU ld, because LLVM's lld intentionally
+ * interprets the linker file differently from ld:
+ *
+ * https://discourse.llvm.org/t/lld-vs-ld-section-type-progbits-vs-nobits/5999/3
+ *
+ * Zero initialized global/static data (.bss) is initialized in
+ * pw_boot_Entry() via memset(), so the section doesn't need to be loaded from
+ * flash. The .heap and .stack sections don't require any initialization,
+ * as they only represent allocated memory regions, so they also do not need
+ * to be loaded.
+ */
+ .zero_init_ram (NOLOAD) : ALIGN(4)
+ {
+ *(.bss)
+ *(.bss*)
+ *(COMMON)
+ . = ALIGN(4);
+ } >EXTERNAL_RAM
+
+ .heap (NOLOAD) : ALIGN(4)
+ {
+ pw_boot_heap_low_addr = .;
+ . = . + PW_BOOT_HEAP_SIZE;
+ . = ALIGN(4);
+ pw_boot_heap_high_addr = .;
+ } >EXTERNAL_RAM
+
+ /* Link-time check for stack overlaps.
+ *
+ * The ARMv7-M architecture may require 8-byte alignment of the stack pointer
+ * rather than 4 in some contexts and implementations, so this region is
+ * 8-byte aligned (see ARMv7-M Architecture Reference Manual DDI0403E
+ * section B1.5.7).
+ */
+ .stack (NOLOAD) : ALIGN(8)
+ {
+ /* Set the address that the main stack pointer should be initialized to. */
+ pw_boot_stack_low_addr = .;
+ HIDDEN(_stack_size = ORIGIN(EXTERNAL_RAM) + LENGTH(EXTERNAL_RAM) - .);
+ /* Align the stack to a lower address to ensure it isn't out of range. */
+ HIDDEN(_stack_high = (. + _stack_size) & ~0x7);
+ ASSERT(_stack_high - . >= PW_BOOT_MIN_STACK_SIZE,
+ "Error: Not enough RAM for desired minimum stack size.");
+ . = _stack_high;
+ pw_boot_stack_high_addr = .;
+ } >EXTERNAL_RAM
+
+ /* Represents unused space in the EXTERNAL_RAM segment. This MUST be the last
+ * section assigned to the EXTERNAL_RAM region.
+ */
+ .EXTERNAL_RAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(EXTERNAL_RAM) + LENGTH(EXTERNAL_RAM));
+ } >EXTERNAL_RAM
+
+ /* Discard unwind info. */
+ .ARM.extab 0x0 (INFO) :
+ {
+ KEEP(*(.ARM.extab*))
+ }
+}
+
+/* Symbols used by core_init.c: */
+/* Start of .static_init_ram in TEXT_EXTERNAL_RAM. */
+_pw_static_init_flash_start = LOADADDR(.static_init_ram);
+
+/* Region of .static_init_ram in RAM. */
+_pw_static_init_ram_start = ADDR(.static_init_ram);
+_pw_static_init_ram_end = _pw_static_init_ram_start + SIZEOF(.static_init_ram);
+
+/* Region of .zero_init_ram. */
+_pw_zero_init_ram_start = ADDR(.zero_init_ram);
+_pw_zero_init_ram_end = _pw_zero_init_ram_start + SIZEOF(.zero_init_ram);
+
+/* arm-none-eabi expects `end` symbol to point to start of heap for sbrk. */
+PROVIDE(end = _pw_zero_init_ram_end);
+
+/* These symbols are used by pw_bloat.bloaty_config to create the memoryregions
+ * data source for bloaty in this format (where the optional _N defaults to 0):
+ * pw_bloat_config_memory_region_NAME_{start,end}{_N,} */
+pw_bloat_config_memory_region_VECTOR_TABLE_start = ORIGIN(VECTOR_TABLE);
+pw_bloat_config_memory_region_VECTOR_TABLE_end =
+ ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE);
+pw_bloat_config_memory_region_FLASH_start = ORIGIN(TEXT_EXTERNAL_RAM);
+pw_bloat_config_memory_region_FLASH_end = ORIGIN(TEXT_EXTERNAL_RAM) + LENGTH(TEXT_EXTERNAL_RAM);
+pw_bloat_config_memory_region_RAM_start = ORIGIN(EXTERNAL_RAM);
+pw_bloat_config_memory_region_RAM_end = ORIGIN(EXTERNAL_RAM) + LENGTH(EXTERNAL_RAM);
+
+/* Symbol mapping used by MSS Startup and Debugger. */
+PROVIDE (__smartfusion2_memory_remap = 2); /* Remap according to LMA (this script) */
+PROVIDE (__mirrored_nvm = 1); /* GDB will load to LMA directly no need to load again. */
+PROVIDE (_estack = pw_boot_stack_high_addr);
+PROVIDE (__stack_start__ = pw_boot_stack_low_addr);
+PROVIDE (__vector_table_load = LOADADDR(.vector_table));
+PROVIDE (__vector_table_start = pw_boot_vector_table_addr);
+PROVIDE (__vector_table_vma_base_address = __vector_table_start); /* required by debugger for start address */
+PROVIDE (__text_end = __exidx_end);
+PROVIDE (__heap_start__ = pw_boot_heap_low_addr);
+PROVIDE (_eheap = pw_boot_heap_high_addr);
+PROVIDE (_etext = __exidx_end);
+PROVIDE (_evector_table = pw_boot_vector_table_addr);
+PROVIDE (__data_start = _pw_static_init_ram_start);
+PROVIDE (_edata = _pw_static_init_ram_end);
+PROVIDE (__text_start = __text_load);
+PROVIDE (__bss_start__ = _pw_zero_init_ram_start);
+PROVIDE (__bss_end__ = _pw_static_init_ram_end);
+PROVIDE (__data_load = _pw_static_init_flash_start);
diff --git a/targets/emcraft_sf2_som/target_docs.rst b/targets/emcraft_sf2_som/target_docs.rst
index adcc077e8..2389f67a0 100644
--- a/targets/emcraft_sf2_som/target_docs.rst
+++ b/targets/emcraft_sf2_som/target_docs.rst
@@ -1,15 +1,15 @@
.. _target-emcraft-sf2-som:
--------------------------------------
-_target-emcraft-sf2-som: SmartFusion2
--------------------------------------
+--------------------
+Emcraft SmartFusion2
+--------------------
The Emcraft SmartFusion2 system-on-module target configuration
uses FreeRTOS and the Microchip MSS HAL rather than a from-the-ground-up
baremetal approach.
------
+
Setup
------
+=====
To use this target, pigweed must be set up to use FreeRTOS and the Microchip
MSS HAL for the SmartFusion series. The supported repositories can be
downloaded via ``pw package``, and then the build must be manually configured
@@ -22,12 +22,11 @@ to point to the locations the repositories were downloaded to.
pw package install nanopb
gn args out
- # Add these lines, replacing ${PW_ROOT} with the path to the location that
- # Pigweed is checked out at.
- dir_pw_third_party_freertos = "${PW_ROOT}/.environment/packages/freertos"
+ # Add these lines.
+ dir_pw_third_party_freertos = pw_env_setup_PACKAGE_ROOT + "/freertos"
dir_pw_third_party_smartfusion_mss =
- "${PW_ROOT}/.environment/packages/smartfusion_mss"
- dir_pw_third_party_nanopb = "${PW_ROOT}/.environment/packages/nanopb"
+ pw_env_setup_PACKAGE_ROOT + "/smartfusion_mss"
+ dir_pw_third_party_nanopb = pw_env_setup_PACKAGE_ROOT + "/nanopb"
Building and running the demo
=============================
diff --git a/targets/emcraft_sf2_som/vector_table.c b/targets/emcraft_sf2_som/vector_table.c
index a4f5f7fc4..5205404f4 100644
--- a/targets/emcraft_sf2_som/vector_table.c
+++ b/targets/emcraft_sf2_som/vector_table.c
@@ -44,6 +44,7 @@ typedef void (*InterruptHandler)(void);
void SVC_Handler(void);
void PendSV_Handler(void);
void SysTick_Handler(void);
+void sf2_SocInit(void);
PW_KEEP_IN_SECTION(".vector_table")
const InterruptHandler vector_table[] = {
@@ -55,7 +56,7 @@ const InterruptHandler vector_table[] = {
// Reset handler, dictates how to handle reset interrupt. This is the
// address that the Program Counter (PC) is initialized to at boot.
- [1] = pw_boot_Entry,
+ [1] = sf2_SocInit,
// NMI handler.
[2] = DefaultFaultHandler,
diff --git a/targets/host/CMakeLists.txt b/targets/host/CMakeLists.txt
index dc7d487fb..9cba1b9b8 100644
--- a/targets/host/CMakeLists.txt
+++ b/targets/host/CMakeLists.txt
@@ -14,13 +14,14 @@
include("$ENV{PW_ROOT}/pw_build/pigweed.cmake")
-pw_add_module_library(targets.host.system_rpc_server
- IMPLEMENTS_FACADES
- pw_rpc.system_server
+pw_add_library(targets.host.system_rpc_server STATIC
SOURCES
system_rpc_server.cc
PRIVATE_DEPS
- pw_hdlc
- pw_rpc.server
+ pw_assert.check
+ pw_hdlc.pw_rpc
+ pw_hdlc.rpc_channel_output
+ pw_log
+ pw_rpc.system_server.facade
pw_stream.socket_stream
)
diff --git a/targets/host/pw_add_test_executable.cmake b/targets/host/pw_add_test_executable.cmake
new file mode 100644
index 000000000..53b7e8acc
--- /dev/null
+++ b/targets/host/pw_add_test_executable.cmake
@@ -0,0 +1,54 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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_guard(GLOBAL)
+
+include("$ENV{PW_ROOT}/pw_build/pigweed.cmake")
+include("$ENV{PW_ROOT}/pw_unit_test/test.cmake")
+
+# Used by pw_add_test to instantiate unit test executables for the host.
+#
+# Required Args:
+#
+# NAME: name for the desired executable
+# TEST_DEP: the target which provides the tests for this executable
+#
+function(pw_add_test_executable NAME TEST_DEP)
+ set(num_positional_args 2)
+ set(option_args)
+ set(one_value_args)
+ set(multi_value_args)
+ pw_parse_arguments_strict(
+ pw_add_test_executable "${num_positional_args}" "${option_args}"
+ "${one_value_args}" "${multi_value_args}")
+
+ # CMake requires a source file to determine the LINKER_LANGUAGE.
+ add_executable("${NAME}" EXCLUDE_FROM_ALL
+ $<TARGET_PROPERTY:pw_build.empty,SOURCES>)
+
+ set(test_backend "${pw_unit_test_GOOGLETEST_BACKEND}")
+ if("${test_backend}" STREQUAL "pw_unit_test.light")
+ set(main pw_unit_test.logging_main)
+ elseif("${test_backend}" STREQUAL "pw_third_party.googletest")
+ set(main pw_third_party.googletest.gmock_main)
+ else()
+ message(FATAL_ERROR
+ "Unsupported test backend selected for host test executables")
+ endif()
+
+ pw_target_link_targets("${NAME}"
+ PRIVATE
+ "${main}"
+ "${TEST_DEP}"
+ )
+endfunction(pw_add_test_executable)
diff --git a/targets/host/system_rpc_server.cc b/targets/host/system_rpc_server.cc
index 79e00caff..8ff75751a 100644
--- a/targets/host/system_rpc_server.cc
+++ b/targets/host/system_rpc_server.cc
@@ -17,6 +17,7 @@
#include <cstdio>
#include "pw_assert/check.h"
+#include "pw_hdlc/encoded_size.h"
#include "pw_hdlc/rpc_channel.h"
#include "pw_hdlc/rpc_packets.h"
#include "pw_log/log.h"
@@ -26,14 +27,19 @@
namespace pw::rpc::system_server {
namespace {
-constexpr size_t kMaxTransmissionUnit = 512;
+// Hard-coded to 1055 bytes, which is enough to fit 512-byte payloads when using
+// HDLC framing.
+constexpr size_t kMaxTransmissionUnit = 1055;
uint16_t socket_port = 33000;
+static_assert(kMaxTransmissionUnit ==
+ hdlc::MaxEncodedFrameSize(rpc::cfg::kEncodingBufferSizeBytes));
+
+stream::ServerSocket server_socket;
stream::SocketStream socket_stream;
-hdlc::RpcChannelOutput hdlc_channel_output(socket_stream,
- hdlc::kDefaultRpcAddress,
- "HDLC channel");
+hdlc::FixedMtuChannelOutput<kMaxTransmissionUnit> hdlc_channel_output(
+ socket_stream, hdlc::kDefaultRpcAddress, "HDLC channel");
Channel channels[] = {rpc::Channel::Create<1>(&hdlc_channel_output)};
rpc::Server server(channels);
@@ -43,37 +49,57 @@ void set_socket_port(uint16_t new_socket_port) {
socket_port = new_socket_port;
}
+int GetServerSocketFd() { return socket_stream.connection_fd(); }
+
void Init() {
log_basic::SetOutput([](std::string_view log) {
std::fprintf(stderr, "%.*s\n", static_cast<int>(log.size()), log.data());
- hdlc::WriteUIFrame(1, std::as_bytes(std::span(log)), socket_stream)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
+ hdlc::WriteUIFrame(1, as_bytes(span<const char>(log)), socket_stream)
+ .IgnoreError(); // TODO(b/242598609): Handle Status properly
});
PW_LOG_INFO("Starting pw_rpc server on port %d", socket_port);
- PW_CHECK_OK(socket_stream.Serve(socket_port));
+ PW_CHECK_OK(server_socket.Listen(socket_port));
+ auto accept_result = server_socket.Accept();
+ PW_CHECK_OK(accept_result.status());
+ socket_stream = *std::move(accept_result);
}
rpc::Server& Server() { return server; }
Status Start() {
+ constexpr size_t kDecoderBufferSize =
+ hdlc::Decoder::RequiredBufferSizeForFrameSize(kMaxTransmissionUnit);
// Declare a buffer for decoding incoming HDLC frames.
- std::array<std::byte, kMaxTransmissionUnit> input_buffer;
+ std::array<std::byte, kDecoderBufferSize> input_buffer;
hdlc::Decoder decoder(input_buffer);
while (true) {
std::array<std::byte, kMaxTransmissionUnit> data;
auto ret_val = socket_stream.Read(data);
- if (ret_val.ok()) {
- for (std::byte byte : ret_val.value()) {
- if (auto result = decoder.Process(byte); result.ok()) {
- hdlc::Frame& frame = result.value();
- if (frame.address() == hdlc::kDefaultRpcAddress) {
- server.ProcessPacket(frame.data(), hdlc_channel_output)
- .IgnoreError(); // TODO(pwbug/387): Handle Status properly
- }
- }
+ if (!ret_val.ok()) {
+ if (ret_val.status() == Status::OutOfRange()) {
+ // An out of range status indicates the remote end has disconnected.
+ return OkStatus();
}
+ continue;
+ }
+
+ for (std::byte byte : ret_val.value()) {
+ auto result = decoder.Process(byte);
+ if (!result.ok()) {
+ // Non-OK means there isn't a complete packet yet, or there was some
+ // other issue. Wait for more bytes that form a complete packet.
+ continue;
+ }
+ hdlc::Frame& frame = result.value();
+ if (frame.address() != hdlc::kDefaultRpcAddress) {
+ // Wrong address; ignore the packet for now. In the future, this branch
+ // could expand to add packet routing or metrics.
+ continue;
+ }
+
+ server.ProcessPacket(frame.data()).IgnoreError();
}
}
}
diff --git a/targets/host/target_docs.rst b/targets/host/target_docs.rst
index cadbb9b03..266ce71d1 100644
--- a/targets/host/target_docs.rst
+++ b/targets/host/target_docs.rst
@@ -47,6 +47,18 @@ upstream toolchains are defined in ``//targets/host/pigweed_internal`` and are
prefixed with ``pw_strict_``. The upstream toolchains may not be used by
downstream projects.
+Toolchains for other C++ standards
+==================================
+Most Pigweed code requires C++17, but a few modules, such as ``pw_tokenizer``,
+work with C++14. All Pigweed code is compatible with C++20. Pigweed defines
+toolchains for testing with C++14 and C++20.
+
+* ``pw_strict_host_clang_debug_cpp14`` -- Builds with ``-std=c++14``.
+* ``pw_strict_host_clang_size_optimized_cpp20`` -- Builds with ``-std=c++20``.
+
+These toolchains are only permitted for use in upstream pigweed, but downstream
+users may create similar toolchains as needed.
+
--------
Building
--------
@@ -58,7 +70,7 @@ top-level ``host`` group as the target to build.
$ ninja -C out host
-``host`` may be replaced with with ``host_clang``, ``host_gcc``,
+``host`` may be replaced with ``host_clang``, ``host_gcc``,
``host_clang_debug``, etc. to build with a more specific host toolchain. Not all
toolchains are supported on all platforms. Unless working specifically on one
toolchain, it is recommended to use the default.
diff --git a/targets/host/target_toolchains.gni b/targets/host/target_toolchains.gni
index 15c08acb4..5c325aa68 100644
--- a/targets/host/target_toolchains.gni
+++ b/targets/host/target_toolchains.gni
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_chrono/backend.gni")
+import("$dir_pw_perf_test/perf_test.gni")
import("$dir_pw_protobuf_compiler/proto.gni")
import("$dir_pw_rpc/system_server/backend.gni")
import("$dir_pw_sync/backend.gni")
@@ -24,6 +25,7 @@ import("$dir_pw_third_party/nanopb/nanopb.gni")
import("$dir_pw_thread/backend.gni")
import("$dir_pw_toolchain/host_clang/toolchains.gni")
import("$dir_pw_toolchain/host_gcc/toolchains.gni")
+import("$dir_pw_toolchain/traits.gni")
import("$dir_pw_trace/backend.gni")
import("$dir_pw_trace_tokenized/config.gni")
@@ -31,19 +33,28 @@ _host_common = {
# Use logging-based test output on host.
pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
+ # Used to configure logging-based perftest on host
+ pw_perf_test_MAIN_FUNCTION = "$dir_pw_perf_test:log_perf_handler_main"
+
# Configure backend for assert facade.
pw_assert_BACKEND = "$dir_pw_assert_basic"
- pw_assert_LITE_BACKEND = "$dir_pw_assert:print_and_abort"
+ pw_assert_LITE_BACKEND = "$dir_pw_assert:print_and_abort_assert_backend"
# Configure backend for logging facade.
pw_log_BACKEND = "$dir_pw_log_basic"
+ # Enable decimal expansion when converting floats to string.
+ pw_string_CONFIG = "$dir_pw_string:enable_decimal_float_expansion"
+
# Configure backends for pw_sync's facades.
- pw_sync_INTERRUPT_SPIN_LOCK_BACKEND = "$dir_pw_sync_stl:interrupt_spin_lock"
pw_sync_BINARY_SEMAPHORE_BACKEND = "$dir_pw_sync_stl:binary_semaphore_backend"
+ pw_sync_CONDITION_VARIABLE_BACKEND =
+ "$dir_pw_sync_stl:condition_variable_backend"
pw_sync_COUNTING_SEMAPHORE_BACKEND =
"$dir_pw_sync_stl:counting_semaphore_backend"
+ pw_sync_INTERRUPT_SPIN_LOCK_BACKEND = "$dir_pw_sync_stl:interrupt_spin_lock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_stl:mutex_backend"
+ pw_sync_RECURSIVE_MUTEX_BACKEND = "$dir_pw_sync_stl:recursive_mutex_backend"
pw_sync_TIMED_MUTEX_BACKEND = "$dir_pw_sync_stl:timed_mutex_backend"
pw_sync_THREAD_NOTIFICATION_BACKEND =
"$dir_pw_sync:binary_semaphore_thread_notification_backend"
@@ -55,7 +66,6 @@ _host_common = {
# Configure backend for pw_rpc_system_server.
pw_rpc_system_server_BACKEND = "$dir_pigweed/targets/host:system_rpc_server"
- pw_rpc_CONFIG = "$dir_pw_rpc:use_global_mutex"
# Configure backend for trace facade.
pw_trace_BACKEND = "$dir_pw_trace_tokenized"
@@ -67,6 +77,9 @@ _host_common = {
pw_chrono_SYSTEM_CLOCK_BACKEND = "$dir_pw_chrono_stl:system_clock"
pw_chrono_SYSTEM_TIMER_BACKEND = "$dir_pw_chrono_stl:system_timer"
+ # Configure backend for pw_perf_test timing facade.
+ pw_perf_test_TIMER_INTERFACE_BACKEND = "$dir_pw_perf_test:chrono_timer"
+
# Configure backends for pw_thread's facades.
pw_thread_ID_BACKEND = "$dir_pw_thread_stl:id"
pw_thread_YIELD_BACKEND = "$dir_pw_thread_stl:yield"
@@ -118,10 +131,15 @@ _clang_default_configs = [
"$dir_pw_build:extra_strict_warnings",
"$dir_pw_build:clang_thread_safety_warnings",
]
+_internal_clang_default_configs =
+ _clang_default_configs + [ "$dir_pw_build:internal_strict_warnings" ]
+
_gcc_default_configs = [
"$dir_pw_build:extra_strict_warnings",
"$dir_pw_toolchain/host_gcc:threading_support",
]
+_internal_gcc_default_configs =
+ _gcc_default_configs + [ "$dir_pw_build:internal_strict_warnings" ]
_excluded_members = [
"defaults",
@@ -237,6 +255,18 @@ pw_target_toolchain_host = {
}
}
+ clang_coverage = {
+ name = "host_clang_coverage"
+ _toolchain_base = pw_toolchain_host_clang.coverage
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_host_common, "*")
+ forward_variables_from(_os_specific_config, "*")
+ default_configs += _clang_default_configs
+ }
+ }
+
gcc_debug = {
name = "host_gcc_debug"
_toolchain_base = pw_toolchain_host_gcc.debug
@@ -287,6 +317,7 @@ pw_target_toolchain_host_list = [
pw_target_toolchain_host.clang_ubsan_heuristic,
pw_target_toolchain_host.clang_msan,
pw_target_toolchain_host.clang_tsan,
+ pw_target_toolchain_host.clang_coverage,
pw_target_toolchain_host.gcc_debug,
pw_target_toolchain_host.gcc_speed_optimized,
pw_target_toolchain_host.gcc_size_optimized,
@@ -295,6 +326,9 @@ pw_target_toolchain_host_list = [
# Additional configuration intended only for upstream Pigweed use.
_pigweed_internal = {
pw_status_CONFIG = "$dir_pw_status:check_if_used"
+
+ # TODO(b/241565082): Enable NC testing in GN Windows when it is fixed.
+ pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = host_os != "win"
}
# Host toolchains exclusively for upstream Pigweed use. To give upstream Pigweed
@@ -310,7 +344,7 @@ pw_internal_host_toolchains = [
forward_variables_from(_host_common, "*")
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
- default_configs += _clang_default_configs
+ default_configs += _internal_clang_default_configs
}
},
{
@@ -322,7 +356,7 @@ pw_internal_host_toolchains = [
forward_variables_from(_host_common, "*")
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
- default_configs += _clang_default_configs
+ default_configs += _internal_clang_default_configs
}
},
{
@@ -334,7 +368,7 @@ pw_internal_host_toolchains = [
forward_variables_from(_host_common, "*")
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
- default_configs += _clang_default_configs
+ default_configs += _internal_clang_default_configs
}
},
{
@@ -346,7 +380,7 @@ pw_internal_host_toolchains = [
forward_variables_from(_host_common, "*")
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
- default_configs += _gcc_default_configs
+ default_configs += _internal_gcc_default_configs
}
},
{
@@ -358,7 +392,7 @@ pw_internal_host_toolchains = [
forward_variables_from(_host_common, "*")
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
- default_configs += _gcc_default_configs
+ default_configs += _internal_gcc_default_configs
}
},
{
@@ -370,7 +404,64 @@ pw_internal_host_toolchains = [
forward_variables_from(_host_common, "*")
forward_variables_from(_pigweed_internal, "*")
forward_variables_from(_os_specific_config, "*")
- default_configs += _gcc_default_configs
+ default_configs += _internal_gcc_default_configs
+ }
+ },
+ {
+ name = "pw_strict_host_clang_debug_cpp14"
+ _toolchain_base = pw_toolchain_host_clang.debug
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_host_common, "*")
+ forward_variables_from(_pigweed_internal, "*")
+ forward_variables_from(_os_specific_config, "*")
+ default_configs += _internal_clang_default_configs
+
+ # Set the C++ standard to C++14 instead of the default (C++17).
+ pw_toolchain_CXX_STANDARD = pw_toolchain_STANDARD.CXX14
+
+ # Do not do negative compilation testing with C++14 since the code may
+ # fail to compile for different reasons than in C++17 or newer.
+ pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = false
+
+ # Select C++14-compatible backends.
+ pw_assert_BACKEND = "$dir_pw_assert:print_and_abort_check_backend"
+ pw_log_BACKEND = "$dir_pw_log_null"
+ pw_unit_test_MAIN = "$dir_pw_unit_test:printf_main"
+ }
+ },
+ {
+ name = "pw_strict_host_clang_size_optimized_cpp20"
+ _toolchain_base = pw_toolchain_host_clang.size_optimized
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_host_common, "*")
+ forward_variables_from(_pigweed_internal, "*")
+ forward_variables_from(_os_specific_config, "*")
+ default_configs += _internal_clang_default_configs
+
+ # Don't enable span asserts since C++20 provides the implementation for
+ # pw::span, and there's no way to ensure asserts are enabled for the C++
+ # standard library's std::span implementation.
+ pw_span_ENABLE_ASSERTS = false
+
+ # Set the C++ standard to C++20 instead of the default.
+ pw_toolchain_CXX_STANDARD = pw_toolchain_STANDARD.CXX20
+ }
+ },
+ {
+ name = "pw_strict_host_clang_size_optimized_minimal_cpp_stdlib"
+ _toolchain_base = pw_toolchain_host_clang.size_optimized
+ forward_variables_from(_toolchain_base, "*", _excluded_members)
+ defaults = {
+ forward_variables_from(_toolchain_base.defaults, "*")
+ forward_variables_from(_host_common, "*")
+ forward_variables_from(_pigweed_internal, "*")
+ forward_variables_from(_os_specific_config, "*")
+ default_configs += _internal_clang_default_configs
+ default_configs += [ "$dir_pw_minimal_cpp_stdlib:use_minimal_cpp_stdlib" ]
}
},
]
diff --git a/targets/host_device_simulator/BUILD.bazel b/targets/host_device_simulator/BUILD.bazel
new file mode 100644
index 000000000..2c758fb01
--- /dev/null
+++ b/targets/host_device_simulator/BUILD.bazel
@@ -0,0 +1,34 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "boot",
+ srcs = [
+ "boot.cc",
+ ],
+ deps = [
+ "//pw_log",
+ "//pw_system:init",
+ "//pw_thread:sleep",
+ ],
+)
diff --git a/targets/host_device_simulator/BUILD.gn b/targets/host_device_simulator/BUILD.gn
index 68b58fc10..403f8f24d 100644
--- a/targets/host_device_simulator/BUILD.gn
+++ b/targets/host_device_simulator/BUILD.gn
@@ -18,9 +18,21 @@ import("$dir_pw_build/target_types.gni")
import("$dir_pw_docgen/docs.gni")
import("$dir_pw_system/system_target.gni")
+if (current_toolchain != default_toolchain) {
+ pw_source_set("boot") {
+ deps = [
+ "$dir_pw_log",
+ "$dir_pw_system",
+ "$dir_pw_thread:sleep",
+ ]
+ sources = [ "boot.cc" ]
+ }
+}
+
pw_system_target("host_device_simulator") {
cpu = PW_SYSTEM_CPU.NATIVE
scheduler = PW_SYSTEM_SCHEDULER.NATIVE
+ link_deps = [ "$dir_pigweed/targets/host_device_simulator:boot" ]
}
pw_doc_group("target_docs") {
diff --git a/targets/host_device_simulator/OWNERS b/targets/host_device_simulator/OWNERS
new file mode 100644
index 000000000..307b1deb5
--- /dev/null
+++ b/targets/host_device_simulator/OWNERS
@@ -0,0 +1 @@
+amontanez@google.com
diff --git a/targets/host_device_simulator/boot.cc b/targets/host_device_simulator/boot.cc
new file mode 100644
index 000000000..1e49aaadb
--- /dev/null
+++ b/targets/host_device_simulator/boot.cc
@@ -0,0 +1,35 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+#define PW_LOG_MODULE_NAME "SYS"
+
+#include <chrono>
+
+#include "pw_log/log.h"
+#include "pw_system/init.h"
+#include "pw_thread/sleep.h"
+
+// Longer term, should this move into a pw_boot_stl module?
+extern "C" int main() {
+ pw::system::Init();
+ // Sleep loop rather than return on this thread so the process isn't closed.
+ while (true) {
+ pw::this_thread::sleep_for(std::chrono::seconds(10));
+ // It's hard to tell that simulator is alive and working since nothing is
+ // logging after initial "boot," so for now log a line occasionally so
+ // users can see that the simulator is alive and well.
+ PW_LOG_INFO("Simulated device is still alive");
+ // TODO(amontanez): This thread should probably have a way to exit.
+ }
+}
diff --git a/targets/host_device_simulator/target_docs.rst b/targets/host_device_simulator/target_docs.rst
index 494a4b9aa..c2b437d97 100644
--- a/targets/host_device_simulator/target_docs.rst
+++ b/targets/host_device_simulator/target_docs.rst
@@ -4,7 +4,7 @@
Host Device Simulator
=====================
This Pigweed target simulates the behavior of an embedded device, spawning
-threads for facilities like RPC and logging. Executables build by this target
+threads for facilities like RPC and logging. Executables built by this target
will perpetually run until they crash or are explicitly terminated. All
communications with the process are over the RPC server hosted on a local
socket rather than by directly interacting with the terminal via standard I/O.
@@ -12,30 +12,104 @@ socket rather than by directly interacting with the terminal via standard I/O.
-----
Setup
-----
-To use this target, Pigweed must be set up to use nanopb. The required source
-repository can be downloaded via ``pw package``, and then the build must be
-manually configured to point to the location the repository was downloaded to.
+To use this target, Pigweed must be set up to use nanopb and FreeRTOS. The
+required source repositories can be downloaded via ``pw package``, and then the
+build must be manually configured to point to the location the repository was
+downloaded to using gn args. Optionally you can include the ``stm32cube_f4``
+package to build for the
+:bdg-ref-primary-line:`target-stm32f429i-disc1-stm32cube` target at the same
+time.
.. code:: sh
- pw package install nanopb
+ pw package install nanopb
+ pw package install freertos
+ pw package install stm32cube_f4
- gn args out
- # Add this line, replacing ${PW_ROOT} with the path to the location that
- # Pigweed is checked out at.
- dir_pw_third_party_nanopb = "${PW_ROOT}/.environment/packages/nanopb"
+ gn gen out --export-compile-commands --args="
+ dir_pw_third_party_nanopb=\"$PW_PROJECT_ROOT/environment/packages/nanopb\"
+ dir_pw_third_party_freertos=\"$PW_PROJECT_ROOT/environment/packages/freertos\"
+ dir_pw_third_party_stm32cube_f4=\"$PW_PROJECT_ROOT/environment/packages/stm32cube_f4\"
+ "
+
+.. tip::
+
+ Instead of the ``gn gen out`` with args set on the command line above you can
+ run:
+
+ .. code:: sh
+
+ gn args out
+
+ Then add the following lines to that text file:
+
+ .. code::
+
+ dir_pw_third_party_nanopb = pw_env_setup_PACKAGE_ROOT + "/nanopb"
+ dir_pw_third_party_freertos = pw_env_setup_PACKAGE_ROOT + "/freertos"
+ dir_pw_third_party_stm32cube_f4 = pw_env_setup_PACKAGE_ROOT + "/stm32cube_f4"
-----------------------------
-Building and running the demo
+Building and Running the Demo
-----------------------------
This target has an associated demo application that can be built and then
run with the following commands:
.. code:: sh
- ninja -C out pw_system_demo
+ ninja -C out pw_system_demo
+
+.. code:: sh
+
+ ./out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
+
+To communicate with the launched process run this in a separate shell:
+
+.. code:: sh
+
+ pw-system-console -s default --proto-globs pw_rpc/echo.proto \
+ --token-databases out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
+
+.. tip::
+
+ Alternatively you can run the system_example app in the background, then
+ launch the console on the same line with:
+
+ .. code:: sh
+
+ ./out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
+ & \
+ pw-system-console -s default --proto-globs pw_rpc/echo.proto \
+ --token-databases \
+ out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
+
+ Exit the console via the menu or pressing :kbd:`Ctrl-d` twice. Then stop the
+ system_example app with:
+
+ .. code:: sh
+
+ killall system_example
+
+In the bottom-most pane labeled ``Python Repl`` you should be able to send RPC
+commands to the simulated device process. For example, you can send an RPC
+message that will be echoed back:
+
+.. code:: pycon
+
+ >>> device.rpcs.pw.rpc.EchoService.Echo(msg='Hello, world!')
+ (Status.OK, pw.rpc.EchoMessage(msg='Hello, world!'))
+
+Or run unit tests included on the simulated device:
+
+.. code:: pycon
+
+ >>> device.run_tests()
+ True
+
+You are now up and running!
- ./out/host_device_simulator.speed_optimized/obj/pw_system/bin/system_example
+.. seealso::
-To communicate with the launched process, use
-``pw-system-console -s localhost:33000 --proto-globs pw_rpc/echo.proto``.
+ The :ref:`module-pw_console`
+ :bdg-ref-primary-line:`module-pw_console-user_guide` for more info on using
+ the the pw_console UI.
diff --git a/targets/lm3s6965evb_qemu/BUILD.gn b/targets/lm3s6965evb_qemu/BUILD.gn
index 230221c86..56481dd5e 100644
--- a/targets/lm3s6965evb_qemu/BUILD.gn
+++ b/targets/lm3s6965evb_qemu/BUILD.gn
@@ -23,6 +23,12 @@ generate_toolchains("target_toolchains") {
toolchains = pw_target_toolchain_lm3s6965evb_qemu_list
}
+# TODO(b/232587313): This config is only used in the clang build for this
+# target, see target_toolchains.gni or the associated bug for more details.
+config("disable_lock_annotations") {
+ cflags = [ "-Wno-thread-safety-analysis" ]
+}
+
if (current_toolchain != default_toolchain) {
pw_source_set("pre_init") {
public_deps = [
diff --git a/targets/lm3s6965evb_qemu/py/BUILD.gn b/targets/lm3s6965evb_qemu/py/BUILD.gn
index 38d0add8c..53d6e0583 100644
--- a/targets/lm3s6965evb_qemu/py/BUILD.gn
+++ b/targets/lm3s6965evb_qemu/py/BUILD.gn
@@ -27,4 +27,5 @@ pw_python_package("py") {
"lm3s6965evb_qemu_utils/unit_test_runner.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
}
diff --git a/targets/lm3s6965evb_qemu/py/lm3s6965evb_qemu_utils/unit_test_runner.py b/targets/lm3s6965evb_qemu/py/lm3s6965evb_qemu_utils/unit_test_runner.py
index 134df906d..ef35c62f4 100755
--- a/targets/lm3s6965evb_qemu/py/lm3s6965evb_qemu_utils/unit_test_runner.py
+++ b/targets/lm3s6965evb_qemu/py/lm3s6965evb_qemu_utils/unit_test_runner.py
@@ -45,8 +45,15 @@ def parse_args():
def launch_tests(binary: str) -> int:
"""Start a process that runs test on binary."""
cmd = [
- _TARGET_QEMU_COMMAND, '-cpu', 'cortex-m3', '-machine', 'lm3s6965evb',
- '-nographic', '-no-reboot', '-kernel', binary
+ _TARGET_QEMU_COMMAND,
+ '-cpu',
+ 'cortex-m3',
+ '-machine',
+ 'lm3s6965evb',
+ '-nographic',
+ '-no-reboot',
+ '-kernel',
+ binary,
]
test_process = subprocess.run(cmd, stdout=subprocess.PIPE)
print(test_process.stdout.decode('utf-8'))
diff --git a/targets/lm3s6965evb_qemu/py/setup.cfg b/targets/lm3s6965evb_qemu/py/setup.cfg
index f38027501..0d325189e 100644
--- a/targets/lm3s6965evb_qemu/py/setup.cfg
+++ b/targets/lm3s6965evb_qemu/py/setup.cfg
@@ -21,7 +21,8 @@ description = Target-specific python scripts for the lm3s6965evb-qemu target
[options]
packages = find:
zip_safe = False
-install_requires = coloredlogs
+install_requires =
+ coloredlogs
[options.entry_points]
console_scripts =
diff --git a/targets/lm3s6965evb_qemu/target_toolchains.gni b/targets/lm3s6965evb_qemu/target_toolchains.gni
index 3a271b7c6..25779b893 100644
--- a/targets/lm3s6965evb_qemu/target_toolchains.gni
+++ b/targets/lm3s6965evb_qemu/target_toolchains.gni
@@ -40,9 +40,11 @@ _target_config = {
pw_boot_BACKEND = "$dir_pw_boot_cortex_m"
pw_log_BACKEND = dir_pw_log_basic
pw_sys_io_BACKEND = dir_pw_sys_io_baremetal_lm3s6965evb
+ pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
"$dir_pw_sync_baremetal:interrupt_spin_lock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+ pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
# pw_cpu_exception_armv7m tests do not work as expected in QEMU. It does not
# appear the divide-by-zero traps as expected when enabled, which prevents the
@@ -80,6 +82,14 @@ _clang_target_default_configs = [
"$dir_pw_build:clang_thread_safety_warnings",
"$dir_pw_build:extra_strict_warnings",
"$dir_pw_toolchain/arm_clang:enable_float_printf",
+
+ # TODO(b/232587313): Disable lock annotations for this target because the
+ # clang toolchain currently relies on the standard library headers provided by
+ # arm-none-eabi-gcc, and thus do not have thread safety lock annotations on
+ # things like std::lock_guard. Thread safety checks will not work until
+ # the clang-based ARM toolchain uses a C++ standard library that has these
+ # annotations set up.
+ "$dir_pigweed/targets/lm3s6965evb_qemu:disable_lock_annotations",
]
pw_target_toolchain_lm3s6965evb_qemu = {
diff --git a/targets/mimxrt595_evk/BUILD.bazel b/targets/mimxrt595_evk/BUILD.bazel
index 6f70dddae..64e72a2c1 100644
--- a/targets/mimxrt595_evk/BUILD.bazel
+++ b/targets/mimxrt595_evk/BUILD.bazel
@@ -36,12 +36,3 @@ pw_cc_library(
"//pw_sys_io_mcuxpresso",
],
)
-
-pw_cc_library(
- name = "system_rpc_server",
- srcs = ["system_rpc_server.cc"],
- deps = [
- "//pw_hdlc:pw_rpc",
- "//pw_rpc/system_server:facade",
- ],
-)
diff --git a/targets/mimxrt595_evk/BUILD.gn b/targets/mimxrt595_evk/BUILD.gn
index 9d60034d0..08c4060d4 100644
--- a/targets/mimxrt595_evk/BUILD.gn
+++ b/targets/mimxrt595_evk/BUILD.gn
@@ -80,17 +80,6 @@ if (current_toolchain != default_toolchain) {
defines = pw_target_mimxrt595_evk_LINK_CONFIG_DEFINES
linker_script = "mimxrt595_flash.ld"
}
-
- pw_source_set("system_rpc_server") {
- deps = [
- "$dir_pw_hdlc:pw_rpc",
- "$dir_pw_hdlc:rpc_channel_output",
- "$dir_pw_rpc/system_server:facade",
- "$dir_pw_stream:sys_io_stream",
- dir_pw_log,
- ]
- sources = [ "system_rpc_server.cc" ]
- }
}
if (pw_third_party_mcuxpresso_SDK != "") {
diff --git a/targets/mimxrt595_evk/OWNERS b/targets/mimxrt595_evk/OWNERS
new file mode 100644
index 000000000..c224618cd
--- /dev/null
+++ b/targets/mimxrt595_evk/OWNERS
@@ -0,0 +1 @@
+keybuk@google.com
diff --git a/targets/mimxrt595_evk/mimxrt595_flash.ld b/targets/mimxrt595_evk/mimxrt595_flash.ld
index 46e79ffdd..8c540997e 100644
--- a/targets/mimxrt595_evk/mimxrt595_flash.ld
+++ b/targets/mimxrt595_evk/mimxrt595_flash.ld
@@ -81,6 +81,26 @@ MEMORY
USB_SRAM(rw) : \
ORIGIN = 0x40140000, \
LENGTH = 0x00004000
+
+ /* Each memory region above has an associated .*.unused_space section that
+ * overlays the unused space at the end of the memory segment. These segments
+ * are used by pw_bloat.bloaty_config to create the utilization data source
+ * for bloaty size reports.
+ *
+ * These sections MUST be located immediately after the last section that is
+ * placed in the respective memory region or lld will issue a warning like:
+ *
+ * warning: ignoring memory region assignment for non-allocatable section
+ * '.VECTOR_TABLE.unused_space'
+ *
+ * If this warning occurs, it's also likely that LLD will have created quite
+ * large padded regions in the ELF file due to bad cursor operations. This
+ * can cause ELF files to balloon from hundreds of kilobytes to hundreds of
+ * megabytes.
+ *
+ * Attempting to add sections to the memory region AFTER the unused_space
+ * section will cause the region to overflow.
+ */
}
SECTIONS
@@ -95,7 +115,13 @@ SECTIONS
* Register) MUST point to this memory location in order to be used. This can
* be done by ensuring this section exists at the default location of the VTOR
* so it's used on reset, or by explicitly setting the VTOR in a bootloader
- * manually to point to &pw_boot_vector_table_addr before interrupts are enabled.
+ * manually to point to &pw_boot_vector_table_addr before interrupts are
+ * enabled.
+ *
+ * The ARMv8-M architecture requires this is at least aligned to 128 bytes,
+ * and aligned to a power of two that is greater than 4 times the number of
+ * supported exceptions. 512 has been selected as it accommodates this
+ * device's vector table.
*/
.vector_table : ALIGN(512)
{
@@ -103,17 +129,25 @@ SECTIONS
KEEP(*(.vector_table))
} >VECTOR_TABLE
+ /* Represents unused space in the VECTOR_TABLE segment. This MUST be the last
+ * section assigned to the VECTOR_TABLE region.
+ */
+ .VECTOR_TABLE.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(VECTOR_TABLE) + LENGTH(VECTOR_TABLE));
+ } >VECTOR_TABLE
+
/* Main executable code. */
- .code : ALIGN(8)
+ .code : ALIGN(4)
{
- . = ALIGN(8);
+ . = ALIGN(4);
/* Application code. */
*(.text)
*(.text*)
KEEP(*(.init))
KEEP(*(.fini))
- . = ALIGN(8);
+ . = ALIGN(4);
/* Constants.*/
*(.rodata)
*(.rodata*)
@@ -131,7 +165,7 @@ SECTIONS
* Since the region isn't explicitly referenced, specify KEEP to prevent
* link-time garbage collection. SORT is used for sections that have strict
* init/de-init ordering requirements. */
- . = ALIGN(8);
+ . = ALIGN(4);
PROVIDE_HIDDEN(__preinit_array_start = .);
KEEP(*(.preinit_array*))
PROVIDE_HIDDEN(__preinit_array_end = .);
@@ -148,41 +182,64 @@ SECTIONS
} >FLASH
/* Used by unwind-arm/ */
- .ARM : ALIGN(8) {
+ .ARM : ALIGN(4) {
__exidx_start = .;
*(.ARM.exidx*)
__exidx_end = .;
} >FLASH
/* Explicitly initialized global and static data. (.data)*/
- .static_init_ram : ALIGN(8)
+ .static_init_ram : ALIGN(4)
{
*(CodeQuickAccess)
*(DataQuickAccess)
*(.data)
*(.data*)
- . = ALIGN(8);
+ . = ALIGN(4);
} >RAM AT> FLASH
- /* Zero initialized global/static data. (.bss)
- * This section is zero initialized in pw_boot_Entry(). */
- .zero_init_ram : ALIGN(8)
+ /* Represents unused space in the FLASH segment. This MUST be the last section
+ * assigned to the FLASH region.
+ */
+ .FLASH.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(FLASH) + LENGTH(FLASH));
+ } >FLASH
+
+ /* The .zero_init_ram, .heap, and .stack sections below require (NOLOAD)
+ * annotations for LLVM lld, but not GNU ld, because LLVM's lld intentionally
+ * interprets the linker file differently from ld:
+ *
+ * https://discourse.llvm.org/t/lld-vs-ld-section-type-progbits-vs-nobits/5999/3
+ *
+ * Zero initialized global/static data (.bss) is initialized in
+ * pw_boot_Entry() via memset(), so the section doesn't need to be loaded from
+ * flash. The .heap and .stack sections don't require any initialization,
+ * as they only represent allocated memory regions, so they also do not need
+ * to be loaded.
+ */
+ .zero_init_ram (NOLOAD) : ALIGN(4)
{
*(.bss)
*(.bss*)
*(COMMON)
- . = ALIGN(8);
+ . = ALIGN(4);
} >RAM
- .heap : ALIGN(8)
+ .heap (NOLOAD) : ALIGN(4)
{
pw_boot_heap_low_addr = .;
. = . + PW_BOOT_HEAP_SIZE;
- . = ALIGN(8);
+ . = ALIGN(4);
pw_boot_heap_high_addr = .;
} >RAM
- /* Link-time check for stack overlaps. */
+ /* Link-time check for stack overlaps.
+ *
+ * The ARMv8-M architecture requires 8-byte alignment of the stack pointer
+ * rather than 4 in some contexts, so this region is 8-byte aligned (see
+ * ARMv8-M Architecture Reference Manual DDI0553 section B3.8).
+ */
.stack (NOLOAD) : ALIGN(8)
{
/* Set the address that the main stack pointer should be initialized to. */
@@ -196,6 +253,14 @@ SECTIONS
pw_boot_stack_high_addr = .;
} >RAM
+ /* Represents unused space in the RAM segment. This MUST be the last section
+ * assigned to the RAM region.
+ */
+ .RAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(RAM) + LENGTH(RAM));
+ } >RAM
+
m_usb_bdt (NOLOAD) :
{
. = ALIGN(512);
@@ -207,6 +272,14 @@ SECTIONS
*(m_usb_global)
} >USB_SRAM
+ /* Represents unused space in the USB_SRAM segment. This MUST be the last
+ * section assigned to the USB_SRAM region.
+ */
+ .USB_SRAM.unused_space (NOLOAD) : ALIGN(4)
+ {
+ . = ABSOLUTE(ORIGIN(USB_SRAM) + LENGTH(USB_SRAM));
+ } >USB_SRAM
+
/* Discard unwind info. */
.ARM.extab 0x0 (INFO) :
{
diff --git a/targets/mimxrt595_evk/target_toolchains.gni b/targets/mimxrt595_evk/target_toolchains.gni
index ff344022b..d4d851855 100644
--- a/targets/mimxrt595_evk/target_toolchains.gni
+++ b/targets/mimxrt595_evk/target_toolchains.gni
@@ -37,10 +37,10 @@ _target_config = {
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
"$dir_pw_sync_baremetal:interrupt_spin_lock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+ pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
pw_log_BACKEND = dir_pw_log_basic
pw_sys_io_BACKEND = "$dir_pw_sys_io_mcuxpresso"
- pw_rpc_system_server_BACKEND =
- "$dir_pigweed/targets/mimxrt595_evk:system_rpc_server"
+ pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
# Override the default pw_boot_cortex_m linker script and set the memory
# regions for the target.
diff --git a/targets/rp2040/BUILD.bazel b/targets/rp2040/BUILD.bazel
index 76bd682b4..ddd50ed85 100644
--- a/targets/rp2040/BUILD.bazel
+++ b/targets/rp2040/BUILD.bazel
@@ -24,11 +24,14 @@ licenses(["notice"])
# This is just a stub to silence warnings saying that pico_logging_test_main.cc
# is missing from the bazel build. There's no plans yet to do a Bazel build for
# the Pi Pico.
+#
+# TODO(b/260639642): Support Pico in the Bazel build.
pw_cc_library(
name = "pico_logging_test_main",
srcs = [
"pico_logging_test_main.cc",
],
+ tags = ["manual"],
deps = [
"//pw_unit_test",
"//pw_unit_test:logging_event_handler",
diff --git a/targets/rp2040/BUILD.gn b/targets/rp2040/BUILD.gn
index 9f0bbdb10..329d95ea6 100644
--- a/targets/rp2040/BUILD.gn
+++ b/targets/rp2040/BUILD.gn
@@ -22,13 +22,9 @@ import("$dir_pw_toolchain/generate_toolchain.gni")
if (current_toolchain != default_toolchain) {
pw_source_set("pico_logging_test_main") {
- # Required because the pico SDK can't properly propagate -Wno-undef and
- # -Wno-unused-function because of Pigweed's very unusual default_configs
- # behavior.
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
deps = [
- "$PICO_ROOT/src/common/pico_base",
- "$PICO_ROOT/src/common/pico_stdlib",
+ "$dir_pw_log",
+ "$dir_pw_sys_io",
"$dir_pw_unit_test:logging_event_handler",
"$dir_pw_unit_test:pw_unit_test",
]
@@ -43,7 +39,6 @@ generate_toolchain("rp2040") {
]
_toolchain_base = pw_toolchain_arm_gcc.cortex_m0plus_size_optimized
forward_variables_from(_toolchain_base, "*", _excluded_members)
- final_binary_extension = ".elf"
# For now, no Pigweed configurations set up.
defaults = {
@@ -55,11 +50,12 @@ generate_toolchain("rp2040") {
pw_unit_test_MAIN = "$dir_pigweed/targets/rp2040:pico_logging_test_main"
pw_assert_BACKEND = dir_pw_assert_basic
pw_log_BACKEND = dir_pw_log_basic
- pw_sys_io_BACKEND = "$dir_pw_sys_io_stdio"
+ pw_sys_io_BACKEND = dir_pw_sys_io_pico
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
"$dir_pw_sync_baremetal:interrupt_spin_lock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+ pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
# Silence GN variable overwrite warning.
pw_build_LINK_DEPS = []
diff --git a/targets/rp2040/OWNERS b/targets/rp2040/OWNERS
new file mode 100644
index 000000000..55936259d
--- /dev/null
+++ b/targets/rp2040/OWNERS
@@ -0,0 +1,2 @@
+amontanez@google.com
+tonymd@google.com
diff --git a/targets/rp2040/pico_executable.gni b/targets/rp2040/pico_executable.gni
index f8afcf16e..baba6f3c0 100644
--- a/targets/rp2040/pico_executable.gni
+++ b/targets/rp2040/pico_executable.gni
@@ -14,12 +14,41 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/exec.gni")
+
# Executable wrapper that allows the 2nd stage bootloader to strip link deps.
template("pico_executable") {
- target("executable", target_name) {
- forward_variables_from(invoker, "*")
- if (defined(no_link_deps) && no_link_deps) {
- public_deps -= [ "$dir_pw_build:link_deps" ]
+ if (defined(invoker.is_boot_stage2) && invoker.is_boot_stage2) {
+ executable(target_name) {
+ forward_variables_from(invoker, "*")
+
+ # Link deps pulls in Pigweed things that don't fit in the 2nd stage
+ # bootloader.
+ deps -= [ "$dir_pw_build:link_deps" ]
+ }
+ } else {
+ _uf2_name = "${target_name}.uf2"
+ _elf_name = "${target_name}.elf"
+ executable(_elf_name) {
+ forward_variables_from(invoker, "*")
+ }
+
+ pw_exec(_uf2_name) {
+ _elf2uf2_target = "$dir_pw_third_party/pico_sdk/src:elf2uf2($dir_pigweed/targets/host:host_clang_debug)"
+ _uf2_out_path = "${target_out_dir}/${_uf2_name}"
+ deps = [
+ ":${_elf_name}",
+ _elf2uf2_target,
+ ]
+ program = "<TARGET_FILE(${_elf2uf2_target})>"
+ args = [
+ "<TARGET_FILE(:${_elf_name})>",
+ rebase_path(_uf2_out_path, root_build_dir),
+ ]
+ outputs = [ _uf2_out_path ]
+ }
+ group(target_name) {
+ deps = [ ":${_uf2_name}" ]
}
}
}
diff --git a/targets/rp2040/pico_logging_test_main.cc b/targets/rp2040/pico_logging_test_main.cc
index 1c5c92c59..0ae40bf4f 100644
--- a/targets/rp2040/pico_logging_test_main.cc
+++ b/targets/rp2040/pico_logging_test_main.cc
@@ -12,13 +12,21 @@
// License for the specific language governing permissions and limitations under
// the License.
-#include "pico/stdlib.h"
-#include "pw_unit_test/framework.h"
+#include "gtest/gtest.h"
+#include "pw_log/log.h"
+#include "pw_sys_io/sys_io.h"
#include "pw_unit_test/logging_event_handler.h"
int main() {
- setup_default_uart();
pw::unit_test::LoggingEventHandler handler;
pw::unit_test::RegisterEventHandler(&handler);
- return RUN_ALL_TESTS();
+ while (true) {
+ std::byte b;
+ if (pw::sys_io::ReadByte(&b).ok() && b == std::byte(' ')) {
+ RUN_ALL_TESTS();
+ } else {
+ PW_LOG_INFO("Press space to start test");
+ }
+ }
+ return 0;
}
diff --git a/targets/rp2040/target_docs.rst b/targets/rp2040/target_docs.rst
index 0546f724b..201c7366c 100644
--- a/targets/rp2040/target_docs.rst
+++ b/targets/rp2040/target_docs.rst
@@ -8,9 +8,8 @@ Raspberry Pi Pico
is not very polished, and many features/configuration options that work in
upstream Pi Pico CMake build have not yet been ported to the GN build.
------
Setup
------
+=====
To use this target, Pigweed must be set up to build against the Raspberry Pi
Pico SDK. This can be downloaded via ``pw package``, and then the build must be
manually configured to point to the location of the downloaded SDK.
@@ -20,16 +19,27 @@ manually configured to point to the location of the downloaded SDK.
pw package install pico_sdk
gn args out
- # Add these lines, replacing ${PW_ROOT} with the path to the location that
- # Pigweed is checked out at.
- PICO_SRC_DIR = "${PW_ROOT}/.environment/packages/pico_sdk"
+ # Add this line.
+ PICO_SRC_DIR = getenv("PW_PACKAGE_ROOT") + "/pico_sdk"
+Linux
-----
+On linux, you may need to update your udev rules at
+``/etc/udev/rules.d/49-pico.rules`` to include the following:
+
+.. code:: none
+
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0004", MODE:="0666"
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="0003", MODE:="0666"
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000a", MODE:="0666"
+
Usage
------
-The Pi Pico is currently configured to output logs and test results over UART
-via GPIO 1 and 2 (TX and RX, respectively) at a baud rate of 115200. Because
-of this, you'll need a USB TTL adapter to communicate with the Pi Pico.
+=====
+The Pi Pico is configured to output logs and test results over USB serial at a
+baud rate of 115200.
Once the pico SDK is configured, the Pi Pico will build as part of the default
GN build:
@@ -38,26 +48,66 @@ GN build:
ninja -C out
-Pigweed's build will produce ELF files for each unit test built for the Pi Pico.
-While ELF files can be flashed to a Pi Pico via SWD, it's slightly easier to
-use the Pi Pico's bootloader to flash the firmware as a UF2 file.
-
-Pigweed currently does not yet build/provide the elf2uf2 utility used to convert
-ELF files to UF2 files. This tool can be built from within the Pi Pico SDK with
-the following command:
-
-.. code:: sh
-
- mkdir build && cd build && cmake -G Ninja ../ && ninja
- # Copy the tool so it's visible in your PATH.
- cp elf2uf2/elf2uf2 $HOME/bin/elf2uf2
+Pigweed's build will produce ELF and UF2 files for each unit test built for the
+Pi Pico.
Flashing
========
-Flashing the Pi Pico is as easy as 1-2-3:
+Flashing the Pi Pico is two easy steps:
-#. Create a UF2 file from an ELF file using ``elf2uf2``.
#. While holding the button on the Pi Pico, connect the Pico to your computer
via the micro USB port.
-#. Copy the UF2 to the RPI-RP2 volume that enumerated when you connected the
- Pico.
+#. Copy the desired UF2 firmware image to the RPI-RP2 volume that enumerated
+ when you connected the Pico.
+
+Testing
+=======
+Unlike some other targets, the RP2040 does not automatically run tests on boot.
+To run a test, flash it to the RP2040 and connect to the serial port and then
+press the spacebar to start the test:
+
+.. code:: none
+
+ $ python -m serial.tools.miniterm --raw /dev/ttyACM0 115200
+ --- Miniterm on /dev/cu.usbmodem142401 115200,8,N,1 ---
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---INF [==========] Running all tests.
+ INF [ RUN ] Status.Default
+ INF [ OK ] Status.Default
+ INF [ RUN ] Status.ConstructWithStatusCode
+ INF [ OK ] Status.ConstructWithStatusCode
+ INF [ RUN ] Status.AssignFromStatusCode
+ INF [ OK ] Status.AssignFromStatusCode
+ INF [ RUN ] Status.Ok_OkIsTrue
+ INF [ OK ] Status.Ok_OkIsTrue
+ INF [ RUN ] Status.NotOk_OkIsFalse
+ INF [ OK ] Status.NotOk_OkIsFalse
+ INF [ RUN ] Status.Code
+ INF [ OK ] Status.Code
+ INF [ RUN ] Status.EqualCodes
+ INF [ OK ] Status.EqualCodes
+ INF [ RUN ] Status.IsError
+ INF [ OK ] Status.IsError
+ INF [ RUN ] Status.IsNotError
+ INF [ OK ] Status.IsNotError
+ INF [ RUN ] Status.Strings
+ INF [ OK ] Status.Strings
+ INF [ RUN ] Status.UnknownString
+ INF [ OK ] Status.UnknownString
+ INF [ RUN ] Status.Update
+ INF [ OK ] Status.Update
+ INF [ RUN ] StatusCLinkage.CallCFunctionWithStatus
+ INF [ OK ] StatusCLinkage.CallCFunctionWithStatus
+ INF [ RUN ] StatusCLinkage.TestStatusFromC
+ INF [ OK ] StatusCLinkage.TestStatusFromC
+ INF [ RUN ] StatusCLinkage.TestStatusStringsFromC
+ INF [ OK ] StatusCLinkage.TestStatusStringsFromC
+ INF [==========] Done running all tests.
+ INF [ PASSED ] 15 test(s).
+
+This is done because the serial port enumerated by the Pi Pico goes away on
+reboot, so it's not safe to run tests until the port has fully enumerated and
+a terminal has connected. To avoid races, the Pico will just wait until it
+receives the space character (0x20) as a signal to start running the tests.
+
+The RP2040 does not yet provide an automated test runner with build system
+integration.
diff --git a/targets/rp2040_pw_system/BUILD.bazel b/targets/rp2040_pw_system/BUILD.bazel
new file mode 100644
index 000000000..a4611aecc
--- /dev/null
+++ b/targets/rp2040_pw_system/BUILD.bazel
@@ -0,0 +1,29 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+# This is just a stub to silence warnings saying that boot.cc and *.h files are
+# missing from the bazel build. There's no plans yet to do a Bazel build for
+# the Pi Pico.
+filegroup(
+ name = "rp2040_pw_system_files",
+ srcs = [
+ "boot.cc",
+ "config/FreeRTOSConfig.h",
+ "config/rp2040_hal_config.h",
+ ],
+)
diff --git a/targets/rp2040_pw_system/BUILD.gn b/targets/rp2040_pw_system/BUILD.gn
new file mode 100644
index 000000000..286164e60
--- /dev/null
+++ b/targets/rp2040_pw_system/BUILD.gn
@@ -0,0 +1,87 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_system/system_target.gni")
+import("$dir_pw_tokenizer/backend.gni")
+import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
+import("$dir_pw_toolchain/generate_toolchain.gni")
+
+if (current_toolchain != default_toolchain) {
+ pw_source_set("pre_init") {
+ remove_configs = [ "$dir_pw_build:strict_warnings" ]
+
+ deps = [
+ "$PICO_ROOT/src/common/pico_base",
+ "$PICO_ROOT/src/common/pico_stdlib",
+ "$PICO_ROOT/src/rp2_common/pico_malloc",
+ "$dir_pw_string",
+ "$dir_pw_system",
+ "$dir_pw_third_party/freertos",
+ ]
+ sources = [ "boot.cc" ]
+ }
+
+ config("config_includes") {
+ include_dirs = [ "config" ]
+ }
+
+ config("rp2040_hal_config") {
+ inputs = [ "config/rp2040_hal_config.h" ]
+ cflags = [ "-include" +
+ rebase_path("config/rp2040_hal_config.h", root_build_dir) ]
+ }
+
+ pw_source_set("rp2040_freertos_config") {
+ public_configs = [ ":config_includes" ]
+ public_deps = [ "$dir_pw_third_party/freertos:config_assert" ]
+ public = [ "config/FreeRTOSConfig.h" ]
+ }
+}
+
+pw_system_target("rp2040_pw_system") {
+ cpu = PW_SYSTEM_CPU.CORTEX_M0PLUS
+ scheduler = PW_SYSTEM_SCHEDULER.FREERTOS
+ use_pw_malloc = false
+ global_configs = [ ":rp2040_hal_config" ]
+
+ build_args = {
+ pw_build_EXECUTABLE_TARGET_TYPE = "pico_executable"
+ pw_build_EXECUTABLE_TARGET_TYPE_FILE =
+ get_path_info("$dir_pigweed/targets/rp2040/pico_executable.gni",
+ "abspath")
+
+ pw_log_BACKEND = dir_pw_log_tokenized
+ pw_log_tokenized_HANDLER_BACKEND = "$dir_pw_system:log_backend.impl"
+
+ pw_third_party_freertos_CONFIG =
+ "$dir_pigweed/targets/rp2040_pw_system:rp2040_freertos_config"
+ pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm0"
+
+ pw_sys_io_BACKEND = dir_pw_sys_io_stdio
+ pw_build_LINK_DEPS += [
+ "$dir_pigweed/targets/rp2040_pw_system:pre_init",
+ "$dir_pw_assert:impl",
+ "$dir_pw_log:impl",
+ ]
+ }
+}
+
+pw_doc_group("target_docs") {
+ sources = [ "target_docs.rst" ]
+}
diff --git a/targets/rp2040_pw_system/OWNERS b/targets/rp2040_pw_system/OWNERS
new file mode 100644
index 000000000..55936259d
--- /dev/null
+++ b/targets/rp2040_pw_system/OWNERS
@@ -0,0 +1,2 @@
+amontanez@google.com
+tonymd@google.com
diff --git a/targets/rp2040_pw_system/boot.cc b/targets/rp2040_pw_system/boot.cc
new file mode 100644
index 000000000..685709ed7
--- /dev/null
+++ b/targets/rp2040_pw_system/boot.cc
@@ -0,0 +1,77 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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 <array>
+
+#define PW_LOG_MODULE_NAME "pw_system"
+
+#include "FreeRTOS.h"
+#include "pico/stdlib.h"
+#include "pw_log/log.h"
+#include "pw_string/util.h"
+#include "pw_system/init.h"
+#include "task.h"
+
+namespace {
+
+std::array<StackType_t, configMINIMAL_STACK_SIZE> freertos_idle_stack;
+StaticTask_t freertos_idle_tcb;
+
+std::array<StackType_t, configTIMER_TASK_STACK_DEPTH> freertos_timer_stack;
+StaticTask_t freertos_timer_tcb;
+
+std::array<char, configMAX_TASK_NAME_LEN> temp_thread_name_buffer;
+
+} // namespace
+
+extern "C" {
+// Functions needed when configGENERATE_RUN_TIME_STATS is on.
+// void configureTimerForRunTimeStats(void) {}
+// unsigned long getRunTimeCounterValue(void) { return time_us_64(); }
+
+// Required for configCHECK_FOR_STACK_OVERFLOW.
+void vApplicationStackOverflowHook(TaskHandle_t, char* pcTaskName) {
+ pw::string::Copy(pcTaskName, temp_thread_name_buffer);
+ PW_CRASH("Stack OVF for task %s", temp_thread_name_buffer.data());
+}
+
+// Required for configUSE_TIMERS.
+void vApplicationGetTimerTaskMemory(StaticTask_t** ppxTimerTaskTCBBuffer,
+ StackType_t** ppxTimerTaskStackBuffer,
+ uint32_t* pulTimerTaskStackSize) {
+ *ppxTimerTaskTCBBuffer = &freertos_timer_tcb;
+ *ppxTimerTaskStackBuffer = freertos_timer_stack.data();
+ *pulTimerTaskStackSize = freertos_timer_stack.size();
+}
+
+void vApplicationGetIdleTaskMemory(StaticTask_t** ppxIdleTaskTCBBuffer,
+ StackType_t** ppxIdleTaskStackBuffer,
+ uint32_t* pulIdleTaskStackSize) {
+ *ppxIdleTaskTCBBuffer = &freertos_idle_tcb;
+ *ppxIdleTaskStackBuffer = freertos_idle_stack.data();
+ *pulIdleTaskStackSize = freertos_idle_stack.size();
+}
+}
+
+int main() {
+ // PICO_SDK Inits
+ stdio_init_all();
+ setup_default_uart();
+ // stdio_usb_init();
+ PW_LOG_INFO("pw_system main");
+
+ pw::system::Init();
+ vTaskStartScheduler();
+ PW_UNREACHABLE;
+}
diff --git a/targets/rp2040_pw_system/config/FreeRTOSConfig.h b/targets/rp2040_pw_system/config/FreeRTOSConfig.h
new file mode 100644
index 000000000..728408487
--- /dev/null
+++ b/targets/rp2040_pw_system/config/FreeRTOSConfig.h
@@ -0,0 +1,93 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#include <stdint.h>
+
+// Disable formatting to make it easier to compare with other config files.
+// clang-format off
+
+#define vPortSVCHandler isr_svcall
+#define xPortPendSVHandler isr_pendsv
+#define xPortSysTickHandler isr_systick
+
+#define configUSE_PREEMPTION 1
+#define configUSE_PORT_OPTIMISED_TASK_SELECTION 0
+#define configUSE_TICKLESS_IDLE 0
+#define configCPU_CLOCK_HZ 133000000
+#define configTICK_RATE_HZ 100
+#define configMAX_PRIORITIES 5
+#define configMINIMAL_STACK_SIZE ((uint16_t)(128))
+#define configMAX_TASK_NAME_LEN 16
+#define configUSE_16_BIT_TICKS 0
+#define configIDLE_SHOULD_YIELD 1
+#define configUSE_TASK_NOTIFICATIONS 1
+#define configTASK_NOTIFICATION_ARRAY_ENTRIES 3
+#define configUSE_MUTEXES 1
+#define configUSE_RECURSIVE_MUTEXES 0
+#define configUSE_COUNTING_SEMAPHORES 0
+#define configQUEUE_REGISTRY_SIZE 10
+#define configUSE_QUEUE_SETS 0
+#define configUSE_TIME_SLICING 0
+#define configUSE_NEWLIB_REENTRANT 0
+#define configENABLE_BACKWARD_COMPATIBILITY 0
+#define configNUM_THREAD_LOCAL_STORAGE_POINTERS 5
+#define configSTACK_DEPTH_TYPE uint16_t
+#define configMESSAGE_BUFFER_LENGTH_TYPE size_t
+
+#define configSUPPORT_STATIC_ALLOCATION 1
+#define configSUPPORT_DYNAMIC_ALLOCATION 0
+#define configTOTAL_HEAP_SIZE ((size_t)(1 * 1024))
+#define configAPPLICATION_ALLOCATED_HEAP 1
+
+#define configUSE_IDLE_HOOK 0
+#define configUSE_TICK_HOOK 0
+#define configCHECK_FOR_STACK_OVERFLOW 0
+#define configUSE_MALLOC_FAILED_HOOK 0
+#define configUSE_DAEMON_TASK_STARTUP_HOOK 0
+
+#define configGENERATE_RUN_TIME_STATS 0
+// #define portGET_RUN_TIME_COUNTER_VALUE getRunTimeCounterValue
+#define configUSE_TRACE_FACILITY 0
+#define configUSE_STATS_FORMATTING_FUNCTIONS 0
+
+#define configUSE_CO_ROUTINES 0
+#define configMAX_CO_ROUTINE_PRIORITIES 1
+
+#define configUSE_TIMERS 1
+#define configTIMER_TASK_PRIORITY 3
+#define configTIMER_QUEUE_LENGTH 10
+#define configTIMER_TASK_STACK_DEPTH configMINIMAL_STACK_SIZE
+
+// Instead of defining configASSERT(), include a header that provides a
+// definition that redirects to pw_assert.
+#include "pw_third_party/freertos/config_assert.h"
+
+#define INCLUDE_vTaskPrioritySet 1
+#define INCLUDE_uxTaskPriorityGet 1
+#define INCLUDE_vTaskDelete 1
+#define INCLUDE_vTaskSuspend 1
+#define INCLUDE_xResumeFromISR 1
+#define INCLUDE_vTaskDelayUntil 1
+#define INCLUDE_vTaskDelay 1
+#define INCLUDE_xTaskGetSchedulerState 1
+#define INCLUDE_xTaskGetCurrentTaskHandle 1
+#define INCLUDE_uxTaskGetStackHighWaterMark 0
+#define INCLUDE_xTaskGetIdleTaskHandle 0
+#define INCLUDE_eTaskGetState 0
+#define INCLUDE_xEventGroupSetBitFromISR 1
+#define INCLUDE_xTimerPendFunctionCall 0
+#define INCLUDE_xTaskAbortDelay 0
+#define INCLUDE_xTaskGetHandle 0
+#define INCLUDE_xTaskResumeFromISR 1
diff --git a/targets/rp2040_pw_system/config/rp2040_hal_config.h b/targets/rp2040_pw_system/config/rp2040_hal_config.h
new file mode 100644
index 000000000..61ed10465
--- /dev/null
+++ b/targets/rp2040_pw_system/config/rp2040_hal_config.h
@@ -0,0 +1,16 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+#pragma once
+
+#define PICO_STDIO_ENABLE_CRLF_SUPPORT 0
diff --git a/targets/rp2040_pw_system/target_docs.rst b/targets/rp2040_pw_system/target_docs.rst
new file mode 100644
index 000000000..035c9b209
--- /dev/null
+++ b/targets/rp2040_pw_system/target_docs.rst
@@ -0,0 +1,170 @@
+.. _target-raspberry-pi-pico-pw-system:
+
+================================
+Raspberry Pi Pico with pw_system
+================================
+.. warning::
+
+ This target is in a very preliminary state and is under active development.
+ This demo gives a preview of the direction we are heading with
+ :ref:`pw_system<module-pw_system>`, but it is not yet ready for production
+ use.
+
+This target configuration uses :ref:`pw_system<module-pw_system>` on top of
+FreeRTOS and the `Raspberry Pi Pico SDK
+<https://github.com/raspberrypi/pico-sdk>`_ HAL rather than a from-the-ground-up
+baremetal approach.
+
+-----
+Setup
+-----
+To use this target, Pigweed must be set up to use FreeRTOS and the STM32Cube HAL
+for the STM32F4 series. The supported repositories can be downloaded via
+``pw package``, and then the build must be manually configured to point to the
+locations the repositories were downloaded to.
+
+.. code:: sh
+
+ pw package install nanopb
+ pw package install freertos
+ pw package install pico_sdk
+
+ gn gen out --export-compile-commands --args="
+ dir_pw_third_party_nanopb=\"//environment/packages/nanopb\"
+ dir_pw_third_party_freertos=\"//environment/packages/freertos\"
+ PICO_SRC_DIR=\"//environment/packages/pico_sdk\"
+ "
+
+.. tip::
+
+ Instead of the ``gn gen out`` with args set on the command line above you can
+ run:
+
+ .. code:: sh
+
+ gn args out
+
+ Then add the following lines to that text file:
+
+ .. code::
+
+ dir_pw_third_party_nanopb = pw_env_setup_PACKAGE_ROOT + "/nanopb"
+ dir_pw_third_party_freertos = pw_env_setup_PACKAGE_ROOT + "/freertos"
+ PICO_SRC_DIR = pw_env_setup_PACKAGE_ROOT + "/pico_sdk"
+
+-----------------------------
+Building and Running the Demo
+-----------------------------
+This target has an associated demo application that can be built and then
+flashed to a device with the following commands:
+
+**Build**
+
+.. code:: sh
+
+ ninja -C out pw_system_demo
+
+**Flash**
+
+- Using a uf2 file:
+
+ Copy to ``out/rp2040_pw_system.size_optimized/obj/pw_system/system_example.uf2``
+ your Pico when it is in USB bootloader mode. Hold down the BOOTSEL button when
+ plugging in the pico and it will appear as a mass storage device.
+
+- Using a Pico Probe and openocd:
+
+ This requires installing the Raspberry Pi foundation's OpenOCD fork for the
+ Pico probe. More details including how to connect the two Pico boards is
+ available in ``Appendix A: Using Picoprobe`` of the `Getting started with
+ Raspberry Pi Pico
+ <https://datasheets.raspberrypi.com/pico/getting-started-with-pico.pdf>`_ guide.
+
+ **Install RaspberryPi's OpenOCD Fork:**
+
+ .. code:: sh
+
+ git clone https://github.com/raspberrypi/openocd.git \
+ --branch picoprobe \
+ --depth=1 \
+ --no-single-branch \
+ openocd-picoprobe
+
+ cd openocd-picoprobe
+
+ ./bootstrap
+ ./configure --enable-picoprobe --prefix=$HOME/apps/openocd --disable-werror
+ make -j2
+ make install
+
+ **Setup udev rules (Linux only):**
+
+ .. code:: sh
+
+ cat <<EOF > 49-picoprobe.rules
+ SUBSYSTEMS=="usb", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000[43a]", MODE:="0666"
+ KERNEL=="ttyACM*", ATTRS{idVendor}=="2e8a", ATTRS{idProduct}=="000[43a]", MODE:="0666"
+ EOF
+ sudo cp 49-picoprobe.rules /usr/lib/udev/rules.d/49-picoprobe.rules
+ sudo udevadm control --reload-rules
+
+ **Flash the Pico:**
+
+ .. code:: sh
+
+ ~/apps/openocd/bin/openocd -f ~/apps/openocd/share/openocd/scripts/interface/picoprobe.cfg -f ~/apps/openocd/share/openocd/scripts/target/rp2040.cfg -c 'program out/rp2040_pw_system.size_optimized/obj/pw_system/bin/system_example.elf verify reset exit'
+
+**Connect with pw_console**
+
+Once the board has been flashed, you can connect to it and send RPC commands
+via the Pigweed console:
+
+.. code:: sh
+
+ pw-system-console -d /dev/{ttyX} -b 115200 \
+ --proto-globs pw_rpc/echo.proto \
+ --token-databases \
+ out/rp2040_pw_system.size_optimized/obj/pw_system/bin/system_example.elf
+
+Replace ``{ttyX}`` with the appropriate device on your machine. On Linux this
+may look like ``ttyACM0``, and on a Mac it may look like ``cu.usbmodem***``.
+
+When the console opens, try sending an Echo RPC request. You should get back
+the same message you sent to the device.
+
+.. code:: pycon
+
+ >>> device.rpcs.pw.rpc.EchoService.Echo(msg="Hello, Pigweed!")
+ (Status.OK, pw.rpc.EchoMessage(msg='Hello, Pigweed!'))
+
+You can also try out our thread snapshot RPC service, which should return a
+stack usage overview of all running threads on the device in Host Logs.
+
+.. code:: pycon
+
+ >>> device.snapshot_peak_stack_usage()
+
+Example output:
+
+.. code::
+
+ 20220826 09:47:22 INF PendingRpc(channel=1, method=pw.thread.ThreadSnapshotService.GetPeakStackUsage) completed: Status.OK
+ 20220826 09:47:22 INF Thread State
+ 20220826 09:47:22 INF 5 threads running.
+ 20220826 09:47:22 INF
+ 20220826 09:47:22 INF Thread (UNKNOWN): IDLE
+ 20220826 09:47:22 INF Est CPU usage: unknown
+ 20220826 09:47:22 INF Stack info
+ 20220826 09:47:22 INF Current usage: 0x20002da0 - 0x???????? (size unknown)
+ 20220826 09:47:22 INF Est peak usage: 390 bytes, 76.77%
+ 20220826 09:47:22 INF Stack limits: 0x20002da0 - 0x20002ba4 (508 bytes)
+ 20220826 09:47:22 INF
+ 20220826 09:47:22 INF ...
+
+You are now up and running!
+
+.. seealso::
+
+ The :ref:`module-pw_console`
+ :bdg-ref-primary-line:`module-pw_console-user_guide` for more info on using
+ the the pw_console UI.
diff --git a/targets/stm32f429i_disc1/BUILD.bazel b/targets/stm32f429i_disc1/BUILD.bazel
index f43dd19d0..988b58801 100644
--- a/targets/stm32f429i_disc1/BUILD.bazel
+++ b/targets/stm32f429i_disc1/BUILD.bazel
@@ -16,31 +16,49 @@ load(
"//pw_build:pigweed.bzl",
"pw_cc_library",
)
+load(
+ "//pw_build/bazel_internal:pigweed_internal.bzl",
+ "pw_linker_script",
+)
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
+pw_linker_script(
+ name = "basic_linker_script",
+ # These come from
+ # https://cs.opensource.google/pigweed/pigweed/+/main:targets/stm32f429i_disc1/target_toolchains.gni
+ # TODO(tpudlik): Figure out how to share them between bazel and GN.
+ defines = [
+ "PW_BOOT_FLASH_BEGIN=0x08000200",
+ "PW_BOOT_FLASH_SIZE=1024K",
+ "PW_BOOT_HEAP_SIZE=112K",
+ "PW_BOOT_MIN_STACK_SIZE=1K",
+ "PW_BOOT_RAM_BEGIN=0x20000000",
+ "PW_BOOT_RAM_SIZE=192K",
+ "PW_BOOT_VECTOR_TABLE_BEGIN=0x08000000",
+ "PW_BOOT_VECTOR_TABLE_SIZE=512",
+ ],
+ linker_script = "//pw_boot_cortex_m:basic_cortex_m.ld",
+)
+
pw_cc_library(
name = "pre_init",
srcs = [
"boot.cc",
"vector_table.c",
],
+ defines = [
+ "PW_MALLOC_ACTIVE=1",
+ ],
deps = [
"//pw_boot",
- "//pw_boot_cortex_m",
"//pw_malloc",
"//pw_preprocessor",
"//pw_sys_io_baremetal_stm32f429",
],
-)
-
-pw_cc_library(
- name = "system_rpc_server",
- srcs = ["system_rpc_server.cc"],
- deps = [
- "//pw_hdlc:pw_rpc",
- "//pw_rpc/system_server:facade",
- ],
+ # TODO(b/251939135): Remove the need for alwayslink by rethinking how
+ # pw_boot_cortex_m is structured in the build system.
+ alwayslink = 1,
)
diff --git a/targets/stm32f429i_disc1/BUILD.gn b/targets/stm32f429i_disc1/BUILD.gn
index 8bfec7014..25e8bd0f7 100644
--- a/targets/stm32f429i_disc1/BUILD.gn
+++ b/targets/stm32f429i_disc1/BUILD.gn
@@ -47,17 +47,6 @@ if (current_toolchain != default_toolchain) {
"vector_table.c",
]
}
-
- pw_source_set("system_rpc_server") {
- deps = [
- "$dir_pw_hdlc:pw_rpc",
- "$dir_pw_hdlc:rpc_channel_output",
- "$dir_pw_rpc/system_server:facade",
- "$dir_pw_stream:sys_io_stream",
- dir_pw_log,
- ]
- sources = [ "system_rpc_server.cc" ]
- }
}
pw_doc_group("target_docs") {
diff --git a/targets/stm32f429i_disc1/boot.cc b/targets/stm32f429i_disc1/boot.cc
index aec48dfe7..586159b13 100644
--- a/targets/stm32f429i_disc1/boot.cc
+++ b/targets/stm32f429i_disc1/boot.cc
@@ -25,8 +25,7 @@
// compile time but does NOT require it to be evaluated at compile time and we
// have to be incredibly careful that this does not end up in the .data section.
void pw_boot_PreStaticMemoryInit() {
- // TODO(pwbug/17): Optionally enable Replace when Pigweed config system is
- // added.
+// TODO(b/264897542): Whether the FPU is enabled should be an Arm target trait.
#if PW_ARMV7M_ENABLE_FPU
// Enable FPU if built using hardware FPU instructions.
// CPCAR mask that enables FPU. (ARMv7-M Section B3.2.20)
@@ -35,7 +34,7 @@ void pw_boot_PreStaticMemoryInit() {
// Memory mapped register to enable FPU. (ARMv7-M Section B3.2.2, Table B3-4)
volatile uint32_t& arm_v7m_cpacr =
*reinterpret_cast<volatile uint32_t*>(0xE000ED88u);
- arm_v7m_cpacr |= kFpuEnableMask;
+ arm_v7m_cpacr = arm_v7m_cpacr | kFpuEnableMask;
// Ensure the FPU configuration is committed and enabled before continuing and
// potentially executing any FPU instructions, however rare that may be during
diff --git a/targets/stm32f429i_disc1/py/BUILD.gn b/targets/stm32f429i_disc1/py/BUILD.gn
index ea35c608f..a0f356d83 100644
--- a/targets/stm32f429i_disc1/py/BUILD.gn
+++ b/targets/stm32f429i_disc1/py/BUILD.gn
@@ -30,5 +30,6 @@ pw_python_package("py") {
"stm32f429i_disc1_utils/unit_test_server.py",
]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
python_deps = [ "$dir_pw_cli/py" ]
}
diff --git a/targets/stm32f429i_disc1/py/setup.cfg b/targets/stm32f429i_disc1/py/setup.cfg
index 42a74b831..a3f377296 100644
--- a/targets/stm32f429i_disc1/py/setup.cfg
+++ b/targets/stm32f429i_disc1/py/setup.cfg
@@ -21,7 +21,10 @@ description = Target-specific python scripts for the stm32f429i-disc1 target
[options]
packages = find:
zip_safe = False
-install_requires = pyserial>=3.5,<4.0; coloredlogs; pw_cli
+install_requires =
+ pyserial>=3.5,<4.0
+ types-pyserial>=3.5,<4.0
+ coloredlogs
[options.entry_points]
console_scripts =
diff --git a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/BUILD.bazel b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/BUILD.bazel
new file mode 100644
index 000000000..1e5dbf19a
--- /dev/null
+++ b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/BUILD.bazel
@@ -0,0 +1,18 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+package(default_visibility = ["//visibility:public"])
+
+# Allow other packages to use this configuration file.
+exports_files(["openocd_stm32f4xx.cfg"])
diff --git a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py
index 8b7a0e4cd..bc09d282b 100644
--- a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py
+++ b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/stm32f429i_detector.py
@@ -16,21 +16,23 @@
import logging
import typing
+from typing import Optional
import coloredlogs # type: ignore
-import serial.tools.list_ports # type: ignore
+import serial.tools.list_ports
# Vendor/device ID to search for in USB devices.
_ST_VENDOR_ID = 0x0483
-_DISCOVERY_MODEL_ID = 0x374b
+_DISCOVERY_MODEL_ID = 0x374B
_LOG = logging.getLogger('stm32f429i_detector')
class BoardInfo(typing.NamedTuple):
"""Information about a connected dev board."""
+
dev_name: str
- serial_number: str
+ serial_number: Optional[str]
def detect_boards() -> list:
@@ -40,8 +42,8 @@ def detect_boards() -> list:
for dev in all_devs:
if dev.vid == _ST_VENDOR_ID and dev.pid == _DISCOVERY_MODEL_ID:
boards.append(
- BoardInfo(dev_name=dev.device,
- serial_number=dev.serial_number))
+ BoardInfo(dev_name=dev.device, serial_number=dev.serial_number)
+ )
return boards
@@ -51,18 +53,14 @@ def main():
# Try to use pw_cli logs, else default to something reasonable.
try:
import pw_cli.log # pylint: disable=import-outside-toplevel
+
pw_cli.log.install()
except ImportError:
- coloredlogs.install(level='INFO',
- level_styles={
- 'debug': {
- 'color': 244
- },
- 'error': {
- 'color': 'red'
- }
- },
- fmt='%(asctime)s %(levelname)s | %(message)s')
+ coloredlogs.install(
+ level='INFO',
+ level_styles={'debug': {'color': 244}, 'error': {'color': 'red'}},
+ fmt='%(asctime)s %(levelname)s | %(message)s',
+ )
boards = detect_boards()
if not boards:
@@ -70,7 +68,7 @@ def main():
for idx, board in enumerate(boards):
_LOG.info('Board %d:', idx)
_LOG.info(' - Port: %s', board.dev_name)
- _LOG.info(' - Serial #: %s', board.serial_number)
+ _LOG.info(' - Serial #: %s', board.serial_number or '<not set>')
if __name__ == '__main__':
diff --git a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_client.py b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_client.py
index 02fdb41c0..cb96f9fef 100755
--- a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_client.py
+++ b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_client.py
@@ -27,9 +27,9 @@ def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('binary', help='The target test binary to run')
- parser.add_argument('--server-port',
- type=int,
- help='Port the test server is located on')
+ parser.add_argument(
+ '--server-port', type=int, help='Port the test server is located on'
+ )
return parser.parse_args()
diff --git a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py
index 328def717..b17d62790 100755
--- a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py
+++ b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_runner.py
@@ -23,7 +23,7 @@ import threading
from typing import List
import coloredlogs # type: ignore
-import serial # type: ignore
+import serial
from stm32f429i_disc1_utils import stm32f429i_detector
# Path used to access non-python resources in this python module.
@@ -34,8 +34,8 @@ _OPENOCD_CONFIG = os.path.join(_DIR, 'openocd_stm32f4xx.cfg')
# Path to scripts provided by openocd.
_OPENOCD_SCRIPTS_DIR = os.path.join(
- os.getenv('PW_PIGWEED_CIPD_INSTALL_DIR', ''), 'share', 'openocd',
- 'scripts')
+ os.getenv('PW_PIGWEED_CIPD_INSTALL_DIR', ''), 'share', 'openocd', 'scripts'
+)
_LOG = logging.getLogger('unit_test_runner')
@@ -65,32 +65,43 @@ def parse_args():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('binary', help='The target test binary to run')
- parser.add_argument('--openocd-config',
- default=_OPENOCD_CONFIG,
- help='Path to openocd configuration file')
- parser.add_argument('--stlink-serial',
- default=None,
- help='The serial number of the stlink to use when '
- 'flashing the target device')
- parser.add_argument('--port',
- default=None,
- help='The name of the serial port to connect to when '
- 'running tests')
- parser.add_argument('--baud',
- type=int,
- default=115200,
- help='Target baud rate to use for serial communication'
- ' with target device')
- parser.add_argument('--test-timeout',
- type=float,
- default=5.0,
- help='Maximum communication delay in seconds before a '
- 'test is considered unresponsive and aborted')
- parser.add_argument('--verbose',
- '-v',
- dest='verbose',
- action="store_true",
- help='Output additional logs as the script runs')
+ parser.add_argument(
+ '--openocd-config',
+ default=_OPENOCD_CONFIG,
+ help='Path to openocd configuration file',
+ )
+ parser.add_argument(
+ '--stlink-serial',
+ default=None,
+ help='The serial number of the stlink to use when '
+ 'flashing the target device',
+ )
+ parser.add_argument(
+ '--port',
+ default=None,
+ help='The name of the serial port to connect to when ' 'running tests',
+ )
+ parser.add_argument(
+ '--baud',
+ type=int,
+ default=115200,
+ help='Target baud rate to use for serial communication'
+ ' with target device',
+ )
+ parser.add_argument(
+ '--test-timeout',
+ type=float,
+ default=5.0,
+ help='Maximum communication delay in seconds before a '
+ 'test is considered unresponsive and aborted',
+ )
+ parser.add_argument(
+ '--verbose',
+ '-v',
+ dest='verbose',
+ action="store_true",
+ help='Output additional logs as the script runs',
+ )
return parser.parse_args()
@@ -111,8 +122,17 @@ def reset_device(openocd_config, stlink_serial):
flash_tool = os.getenv('OPENOCD_PATH', default_flasher)
cmd = [
- flash_tool, '-s', _OPENOCD_SCRIPTS_DIR, '-f', openocd_config, '-c',
- 'init', '-c', 'reset run', '-c', 'exit'
+ flash_tool,
+ '-s',
+ _OPENOCD_SCRIPTS_DIR,
+ '-f',
+ openocd_config,
+ '-c',
+ 'init',
+ '-c',
+ 'reset run',
+ '-c',
+ 'exit',
]
_LOG.debug('Resetting device')
@@ -122,10 +142,9 @@ def reset_device(openocd_config, stlink_serial):
# Disable GDB port to support multi-device testing.
env['PW_GDB_PORT'] = 'disabled'
- process = subprocess.run(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- env=env)
+ process = subprocess.run(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
+ )
if process.returncode:
log_subprocess_output(logging.ERROR, process.stdout)
raise TestingFailure('Failed to reset target device')
@@ -142,9 +161,9 @@ def read_serial(port, baud_rate, test_timeout) -> bytes:
"""
serial_data = bytearray()
- device = serial.Serial(baudrate=baud_rate,
- port=port,
- timeout=_FLASH_TIMEOUT)
+ device = serial.Serial(
+ baudrate=baud_rate, port=port, timeout=_FLASH_TIMEOUT
+ )
if not device.is_open:
raise TestingFailure('Failed to open device')
@@ -175,8 +194,11 @@ def read_serial(port, baud_rate, test_timeout) -> bytes:
# Try to trim captured results to only contain most recent test run.
test_start_index = serial_data.rfind(_TESTS_STARTING_STRING)
- return serial_data if test_start_index == -1 else serial_data[
- test_start_index:]
+ return (
+ serial_data
+ if test_start_index == -1
+ else serial_data[test_start_index:]
+ )
def flash_device(binary, openocd_config, stlink_serial):
@@ -188,8 +210,13 @@ def flash_device(binary, openocd_config, stlink_serial):
openocd_command = ' '.join(['program', binary, 'reset', 'exit'])
cmd = [
- flash_tool, '-s', _OPENOCD_SCRIPTS_DIR, '-f', openocd_config, '-c',
- openocd_command
+ flash_tool,
+ '-s',
+ _OPENOCD_SCRIPTS_DIR,
+ '-f',
+ openocd_config,
+ '-c',
+ openocd_command,
]
_LOG.info('Flashing firmware to device')
@@ -199,10 +226,9 @@ def flash_device(binary, openocd_config, stlink_serial):
# Disable GDB port to support multi-device testing.
env['PW_GDB_PORT'] = 'disabled'
- process = subprocess.run(cmd,
- stdout=subprocess.PIPE,
- stderr=subprocess.STDOUT,
- env=env)
+ process = subprocess.run(
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
+ )
if process.returncode:
log_subprocess_output(logging.ERROR, process.stdout)
raise TestingFailure('Failed to flash target device')
@@ -236,12 +262,9 @@ def _threaded_test_reader(dest, port, baud_rate, test_timeout):
dest.append(read_serial(port, baud_rate, test_timeout))
-def run_device_test(binary,
- test_timeout,
- openocd_config,
- baud,
- stlink_serial=None,
- port=None) -> bool:
+def run_device_test(
+ binary, test_timeout, openocd_config, baud, stlink_serial=None, port=None
+) -> bool:
"""Flashes, runs, and checks an on-device test binary.
Returns true on test pass.
@@ -265,8 +288,9 @@ def run_device_test(binary,
# correctly relative to the start of capturing device output.
result: List[bytes] = []
threaded_reader_args = (result, port, baud, test_timeout)
- read_thread = threading.Thread(target=_threaded_test_reader,
- args=threaded_reader_args)
+ read_thread = threading.Thread(
+ target=_threaded_test_reader, args=threaded_reader_args
+ )
read_thread.start()
_LOG.info('Running test')
flash_device(binary, openocd_config, stlink_serial)
@@ -287,22 +311,24 @@ def main():
# Try to use pw_cli logs, else default to something reasonable.
try:
import pw_cli.log # pylint: disable=import-outside-toplevel
+
log_level = logging.DEBUG if args.verbose else logging.INFO
pw_cli.log.install(level=log_level)
except ImportError:
- coloredlogs.install(level='DEBUG' if args.verbose else 'INFO',
- level_styles={
- 'debug': {
- 'color': 244
- },
- 'error': {
- 'color': 'red'
- }
- },
- fmt='%(asctime)s %(levelname)s | %(message)s')
-
- if run_device_test(args.binary, args.test_timeout, args.openocd_config,
- args.baud, args.stlink_serial, args.port):
+ coloredlogs.install(
+ level='DEBUG' if args.verbose else 'INFO',
+ level_styles={'debug': {'color': 244}, 'error': {'color': 'red'}},
+ fmt='%(asctime)s %(levelname)s | %(message)s',
+ )
+
+ if run_device_test(
+ args.binary,
+ args.test_timeout,
+ args.openocd_config,
+ args.baud,
+ args.stlink_serial,
+ args.port,
+ ):
sys.exit(0)
else:
sys.exit(1)
diff --git a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_server.py b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_server.py
index 302be222d..6d07c4bde 100644
--- a/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_server.py
+++ b/targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/unit_test_server.py
@@ -36,18 +36,24 @@ def parse_args():
"""Parses command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument('--server-port',
- type=int,
- default=8080,
- help='Port to launch the pw_target_runner_server on')
- parser.add_argument('--server-config',
- type=argparse.FileType('r'),
- help='Path to server config file')
- parser.add_argument('--verbose',
- '-v',
- dest='verbose',
- action="store_true",
- help='Output additional logs as the script runs')
+ parser.add_argument(
+ '--server-port',
+ type=int,
+ default=8080,
+ help='Port to launch the pw_target_runner_server on',
+ )
+ parser.add_argument(
+ '--server-config',
+ type=argparse.FileType('r'),
+ help='Path to server config file',
+ )
+ parser.add_argument(
+ '--verbose',
+ '-v',
+ dest='verbose',
+ action="store_true",
+ help='Output additional logs as the script runs',
+ )
return parser.parse_args()
@@ -75,17 +81,23 @@ def generate_server_config() -> IO[bytes]:
_LOG.debug('Found %d attached devices', len(boards))
for board in boards:
test_runner_args = [
- '--stlink-serial', board.serial_number, '--port', board.dev_name
+ '--stlink-serial',
+ board.serial_number,
+ '--port',
+ board.dev_name,
]
config_file.write(
- generate_runner(_TEST_RUNNER_COMMAND,
- test_runner_args).encode('utf-8'))
+ generate_runner(_TEST_RUNNER_COMMAND, test_runner_args).encode(
+ 'utf-8'
+ )
+ )
config_file.flush()
return config_file
-def launch_server(server_config: Optional[IO[bytes]],
- server_port: Optional[int]) -> int:
+def launch_server(
+ server_config: Optional[IO[bytes]], server_port: Optional[int]
+) -> int:
"""Launch a device test server with the provided arguments."""
if server_config is None:
# Auto-detect attached boards if no config is provided.
diff --git a/targets/stm32f429i_disc1/system_rpc_server.cc b/targets/stm32f429i_disc1/system_rpc_server.cc
deleted file mode 100644
index 52be51435..000000000
--- a/targets/stm32f429i_disc1/system_rpc_server.cc
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2020 The Pigweed Authors
-//
-// 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
-//
-// https://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 <cstddef>
-
-#include "pw_hdlc/rpc_channel.h"
-#include "pw_hdlc/rpc_packets.h"
-#include "pw_log/log.h"
-#include "pw_rpc_system_server/rpc_server.h"
-#include "pw_stream/sys_io_stream.h"
-
-namespace pw::rpc::system_server {
-namespace {
-
-constexpr size_t kMaxTransmissionUnit = 256;
-
-// Used to write HDLC data to pw::sys_io.
-stream::SysIoWriter writer;
-stream::SysIoReader reader;
-
-// Set up the output channel for the pw_rpc server to use.
-hdlc::RpcChannelOutput hdlc_channel_output(writer,
- pw::hdlc::kDefaultRpcAddress,
- "HDLC channel");
-Channel channels[] = {pw::rpc::Channel::Create<1>(&hdlc_channel_output)};
-rpc::Server server(channels);
-
-} // namespace
-
-void Init() {
- // Send log messages to HDLC address 1. This prevents logs from interfering
- // with pw_rpc communications.
- pw::log_basic::SetOutput([](std::string_view log) {
- pw::hdlc::WriteUIFrame(1, std::as_bytes(std::span(log)), writer);
- });
-}
-
-rpc::Server& Server() { return server; }
-
-Status Start() {
- // Declare a buffer for decoding incoming HDLC frames.
- std::array<std::byte, kMaxTransmissionUnit> input_buffer;
- hdlc::Decoder decoder(input_buffer);
-
- while (true) {
- std::byte byte;
- Status ret_val = pw::sys_io::ReadByte(&byte);
- if (!ret_val.ok()) {
- return ret_val;
- }
- if (auto result = decoder.Process(byte); result.ok()) {
- hdlc::Frame& frame = result.value();
- if (frame.address() == hdlc::kDefaultRpcAddress) {
- server.ProcessPacket(frame.data(), hdlc_channel_output);
- }
- }
- }
-}
-
-} // namespace pw::rpc::system_server
diff --git a/targets/stm32f429i_disc1/target_toolchains.gni b/targets/stm32f429i_disc1/target_toolchains.gni
index 61012a741..d7c0fdf4b 100644
--- a/targets/stm32f429i_disc1/target_toolchains.gni
+++ b/targets/stm32f429i_disc1/target_toolchains.gni
@@ -14,6 +14,8 @@
import("//build_overrides/pigweed.gni")
+import("$dir_pw_compilation_testing/negative_compilation_test.gni")
+import("$dir_pw_perf_test/perf_test.gni")
import("$dir_pw_rpc/system_server/backend.gni")
import("$dir_pw_sys_io/backend.gni")
import("$dir_pw_toolchain/arm_gcc/toolchains.gni")
@@ -24,9 +26,15 @@ declare_args() {
}
_target_config = {
+ # TODO(b/241565082): Enable NC testing in GN Windows when it is fixed.
+ pw_compilation_testing_NEGATIVE_COMPILATION_ENABLED = host_os != "win"
+
# Use the logging main.
pw_unit_test_MAIN = "$dir_pw_unit_test:logging_main"
+ # Use ARM Cycle Counts
+ pw_perf_test_TIMER_INTERFACE_BACKEND = "$dir_pw_perf_test:arm_cortex_timer"
+
# Configuration options for Pigweed executable targets.
pw_build_EXECUTABLE_TARGET_TYPE = "stm32f429i_executable"
@@ -52,17 +60,17 @@ _target_config = {
pw_sync_INTERRUPT_SPIN_LOCK_BACKEND =
"$dir_pw_sync_baremetal:interrupt_spin_lock"
pw_sync_MUTEX_BACKEND = "$dir_pw_sync_baremetal:mutex"
+ pw_rpc_CONFIG = "$dir_pw_rpc:disable_global_mutex"
pw_log_BACKEND = dir_pw_log_basic
pw_sys_io_BACKEND = dir_pw_sys_io_baremetal_stm32f429
- pw_rpc_system_server_BACKEND =
- "$dir_pigweed/targets/stm32f429i_disc1:system_rpc_server"
+ pw_rpc_system_server_BACKEND = "$dir_pw_hdlc:hdlc_sys_io_system_server"
pw_malloc_BACKEND = dir_pw_malloc_freelist
pw_boot_cortex_m_LINK_CONFIG_DEFINES = [
"PW_BOOT_FLASH_BEGIN=0x08000200",
"PW_BOOT_FLASH_SIZE=1024K",
- # TODO(pwbug/219): Currently "pw_tokenizer/detokenize_test" requires at
+ # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
# least 6K bytes in heap when using pw_malloc_freelist. The heap size
# required for tests should be investigated.
#
@@ -78,10 +86,12 @@ _target_config = {
"PW_BOOT_VECTOR_TABLE_SIZE=512",
]
+ pw_build_LINK_DEPS = []
pw_build_LINK_DEPS = [
"$dir_pw_assert:impl",
"$dir_pw_cpu_exception:entry_impl",
"$dir_pw_log:impl",
+ "$dir_pw_toolchain/arm_gcc:arm_none_eabi_gcc_support",
]
current_cpu = "arm"
diff --git a/targets/stm32f429i_disc1_stm32cube/BUILD.bazel b/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
index 891d89016..7922f2968 100644
--- a/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
+++ b/targets/stm32f429i_disc1_stm32cube/BUILD.bazel
@@ -23,24 +23,59 @@ package(default_visibility = ["//visibility:public"])
licenses(["notice"])
pw_cc_library(
+ name = "freertos_config",
+ hdrs = [
+ "config/FreeRTOSConfig.h",
+ ],
+ includes = ["config/"],
+ target_compatible_with = [":freertos_config_cv"],
+ deps = ["//third_party/freertos:config_assert"],
+)
+
+# Constraint value corresponding to :freertos_config.
+#
+# If you include this in your platform definition, you will tell Bazel to use
+# the :freertos_config defined above when compiling FreeRTOS. (See
+# //third_party/freertos/BUILD.bazel.) If you include it in a target's
+# `target_compatible_with`, you will tell Bazel the target can only be built
+# for platforms that specify this FreeRTOS config.
+constraint_value(
+ name = "freertos_config_cv",
+ constraint_setting = "//third_party/freertos:freertos_config_setting",
+)
+
+# TODO(b/261506064): Additional constraint values for configuring stm32cube
+# need to be added here, once constraint settings for stm32cube are defined.
+platform(
+ name = "platform",
+ constraint_values = [
+ ":freertos_config_cv",
+ "//pw_build/constraints/rtos:freertos",
+ "@freertos//:port_ARM_CM4F",
+ ],
+ parents = ["@bazel_embedded//platforms:cortex_m4_fpu"],
+)
+
+pw_cc_library(
name = "pre_init",
srcs = [
"boot.cc",
"vector_table.c",
],
hdrs = [
- "config/FreeRTOSConfig.h",
"config/stm32f4xx_hal_conf.h",
],
+ target_compatible_with = [":freertos_config_cv"],
deps = [
+ ":freertos_config",
"//pw_boot",
"//pw_boot_cortex_m",
"//pw_malloc",
"//pw_preprocessor",
"//pw_string",
"//pw_sys_io_stm32cube",
- "//third_party/freertos",
"//third_party/stm32cube",
+ "@freertos",
],
)
@@ -49,10 +84,11 @@ pw_cc_binary(
srcs = [
"main.cc",
],
+ target_compatible_with = [":freertos_config_cv"],
deps = [
"//pw_thread:thread",
"//pw_thread:thread_core",
"//pw_thread_freertos:thread",
- "//third_party/freertos",
+ "@freertos",
],
)
diff --git a/targets/stm32f429i_disc1_stm32cube/BUILD.gn b/targets/stm32f429i_disc1_stm32cube/BUILD.gn
index 818692857..da7d9cab7 100644
--- a/targets/stm32f429i_disc1_stm32cube/BUILD.gn
+++ b/targets/stm32f429i_disc1_stm32cube/BUILD.gn
@@ -71,8 +71,7 @@ pw_system_target("stm32f429i_disc1_stm32cube") {
link_deps = [ "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:pre_init" ]
build_args = {
pw_log_BACKEND = dir_pw_log_tokenized
- pw_tokenizer_GLOBAL_HANDLER_WITH_PAYLOAD_BACKEND =
- "$dir_pw_system:log_backend.impl"
+ pw_log_tokenized_HANDLER_BACKEND = "$dir_pw_system:log_backend.impl"
pw_third_party_freertos_CONFIG = "$dir_pigweed/targets/stm32f429i_disc1_stm32cube:stm32f4xx_freertos_config"
pw_third_party_freertos_PORT = "$dir_pw_third_party/freertos:arm_cm4f"
pw_sys_io_BACKEND = dir_pw_sys_io_stm32cube
@@ -85,7 +84,7 @@ pw_system_target("stm32f429i_disc1_stm32cube") {
"PW_BOOT_FLASH_BEGIN=0x08000200",
"PW_BOOT_FLASH_SIZE=2048K",
- # TODO(pwbug/219): Currently "pw_tokenizer/detokenize_test" requires at
+ # TODO(b/235348465): Currently "pw_tokenizer/detokenize_test" requires at
# least 6K bytes in heap when using pw_malloc_freelist. The heap size
# required for tests should be investigated.
"PW_BOOT_HEAP_SIZE=7K",
diff --git a/targets/stm32f429i_disc1_stm32cube/OWNERS b/targets/stm32f429i_disc1_stm32cube/OWNERS
new file mode 100644
index 000000000..307b1deb5
--- /dev/null
+++ b/targets/stm32f429i_disc1_stm32cube/OWNERS
@@ -0,0 +1 @@
+amontanez@google.com
diff --git a/targets/stm32f429i_disc1_stm32cube/config/FreeRTOSConfig.h b/targets/stm32f429i_disc1_stm32cube/config/FreeRTOSConfig.h
index e32e24424..7b855bc7c 100644
--- a/targets/stm32f429i_disc1_stm32cube/config/FreeRTOSConfig.h
+++ b/targets/stm32f429i_disc1_stm32cube/config/FreeRTOSConfig.h
@@ -71,6 +71,7 @@ extern unsigned long getRunTimeCounterValue(void);
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_xTaskGetSchedulerState 1
+#define INCLUDE_uxTaskGetStackHighWaterMark 1
// Instead of defining configASSERT(), include a header that provides a
// definition that redirects to pw_assert.
diff --git a/targets/stm32f429i_disc1_stm32cube/target_docs.rst b/targets/stm32f429i_disc1_stm32cube/target_docs.rst
index e7e46225b..3eaedc89b 100644
--- a/targets/stm32f429i_disc1_stm32cube/target_docs.rst
+++ b/targets/stm32f429i_disc1_stm32cube/target_docs.rst
@@ -1,15 +1,16 @@
.. _target-stm32f429i-disc1-stm32cube:
----------------------------
+===========================
stm32f429i-disc1: STM32Cube
----------------------------
+===========================
+
.. warning::
+
This target is in a very preliminary state and is under active development.
This demo gives a preview of the direction we are heading with
:ref:`pw_system<module-pw_system>`, but it is not yet ready for production
use.
-
The STMicroelectronics STM32F429I-DISC1 development board is currently Pigweed's
primary target for on-device testing and development. This target configuration
uses :ref:`pw_system<module-pw_system>` on top of FreeRTOS and the STM32Cube HAL
@@ -25,34 +26,57 @@ locations the repositories were downloaded to.
.. code:: sh
- pw package install freertos
- pw package install stm32cube_f4
- pw package install nanopb
+ pw package install nanopb
+ pw package install freertos
+ pw package install stm32cube_f4
+
+ gn gen out --export-compile-commands --args="
+ dir_pw_third_party_nanopb=\"$PW_PROJECT_ROOT/environment/packages/nanopb\"
+ dir_pw_third_party_freertos=\"$PW_PROJECT_ROOT/environment/packages/freertos\"
+ dir_pw_third_party_stm32cube_f4=\"$PW_PROJECT_ROOT/environment/packages/stm32cube_f4\"
+ "
+
+.. tip::
+
+ Instead of the ``gn gen out`` with args set on the command line above you can
+ run:
- gn args out
- # Add these lines, replacing ${PW_ROOT} with the path to the location that
- # Pigweed is checked out at.
- dir_pw_third_party_freertos = "${PW_ROOT}/.environment/packages/freertos"
- dir_pw_third_party_stm32cube_f4 = "${PW_ROOT}/.environment/packages/stm32cube_f4"
- dir_pw_third_party_nanopb = "${PW_ROOT}/.environment/packages/nanopb"
+ .. code:: sh
-Building and running the demo
-=============================
+ gn args out
+
+ Then add the following lines to that text file:
+
+ .. code::
+
+ dir_pw_third_party_nanopb = pw_env_setup_PACKAGE_ROOT + "/nanopb"
+ dir_pw_third_party_freertos = pw_env_setup_PACKAGE_ROOT + "/freertos"
+ dir_pw_third_party_stm32cube_f4 = pw_env_setup_PACKAGE_ROOT + "/stm32cube_f4"
+
+-----------------------------
+Building and Running the Demo
+-----------------------------
This target has an associated demo application that can be built and then
flashed to a device with the following commands:
.. code:: sh
- ninja -C out pw_system_demo
+ ninja -C out pw_system_demo
- openocd -f targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg -c "program out/stm32f429i_disc1_stm32cube.size_optimized/obj/pw_system/bin/system_example.elf reset exit"
+.. code:: sh
+
+ openocd -f targets/stm32f429i_disc1/py/stm32f429i_disc1_utils/openocd_stm32f4xx.cfg \
+ -c "program out/stm32f429i_disc1_stm32cube.size_optimized/obj/pw_system/bin/system_example.elf reset exit"
Once the board has been flashed, you can connect to it and send RPC commands
via the Pigweed console:
.. code:: sh
- pw-system-console -d /dev/{ttyX} -b 115200 --proto-globs pw_rpc/echo.proto --token-databases out/stm32f429i_disc1_stm32cube.size_optimized/obj/pw_system/bin/system_example.elf
+ pw-system-console -d /dev/{ttyX} -b 115200 \
+ --proto-globs pw_rpc/echo.proto \
+ --token-databases \
+ out/stm32f429i_disc1_stm32cube.size_optimized/obj/pw_system/bin/system_example.elf
Replace ``{ttyX}`` with the appropriate device on your machine. On Linux this
may look like ``ttyACM0``, and on a Mac it may look like ``cu.usbmodem***``.
@@ -60,9 +84,39 @@ may look like ``ttyACM0``, and on a Mac it may look like ``cu.usbmodem***``.
When the console opens, try sending an Echo RPC request. You should get back
the same message you sent to the device.
-.. code:: sh
+.. code:: pycon
+
+ >>> device.rpcs.pw.rpc.EchoService.Echo(msg="Hello, Pigweed!")
+ (Status.OK, pw.rpc.EchoMessage(msg='Hello, Pigweed!'))
+
+You can also try out our thread snapshot RPC service, which should return a
+stack usage overview of all running threads on the device in Host Logs.
+
+.. code:: pycon
- >>> device.rpcs.pw.rpc.EchoService.Echo(msg="Hello, Pigweed!")
- (Status.OK, pw.rpc.EchoMessage(msg='Hello, Pigweed!'))
+ >>> device.snapshot_peak_stack_usage()
+
+Example output:
+
+.. code::
+
+ 20220826 09:47:22 INF PendingRpc(channel=1, method=pw.thread.ThreadSnapshotService.GetPeakStackUsage) completed: Status.OK
+ 20220826 09:47:22 INF Thread State
+ 20220826 09:47:22 INF 5 threads running.
+ 20220826 09:47:22 INF
+ 20220826 09:47:22 INF Thread (UNKNOWN): IDLE
+ 20220826 09:47:22 INF Est CPU usage: unknown
+ 20220826 09:47:22 INF Stack info
+ 20220826 09:47:22 INF Current usage: 0x20002da0 - 0x???????? (size unknown)
+ 20220826 09:47:22 INF Est peak usage: 390 bytes, 76.77%
+ 20220826 09:47:22 INF Stack limits: 0x20002da0 - 0x20002ba4 (508 bytes)
+ 20220826 09:47:22 INF
+ 20220826 09:47:22 INF ...
You are now up and running!
+
+.. seealso::
+
+ The :ref:`module-pw_console`
+ :bdg-ref-primary-line:`module-pw_console-user_guide` for more info on using
+ the the pw_console UI.
diff --git a/third_party/Android.bp b/third_party/Android.bp
new file mode 100644
index 000000000..5d66645d4
--- /dev/null
+++ b/third_party/Android.bp
@@ -0,0 +1,38 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+package {
+ default_applicable_licenses: ["external_pigweed_license"],
+}
+
+cc_library_headers {
+ name: "fuschia_sdk_lib_fit",
+ vendor_available: true,
+ export_include_dirs: [
+ "fuchsia/repo/sdk/lib/fit/include",
+ ],
+ header_libs: [
+ "fuschia_sdk_lib_stdcompat",
+ ],
+ host_supported: true,
+}
+
+cc_library_headers {
+ name: "fuschia_sdk_lib_stdcompat",
+ vendor_available: true,
+ export_include_dirs: [
+ "fuchsia/repo/sdk/lib/stdcompat/include",
+ ],
+ host_supported: true,
+}
diff --git a/third_party/boringssl/BUILD.bazel b/third_party/boringssl/BUILD.bazel
index e0051e3a3..292b3f575 100644
--- a/third_party/boringssl/BUILD.bazel
+++ b/third_party/boringssl/BUILD.bazel
@@ -22,7 +22,7 @@ pw_cc_library(
name = "sysdeps",
srcs = ["crypto_sysrand.cc"],
hdrs = ["sysdeps/sys/socket.h"],
+ copts = ["-Wno-unused-parameter"],
includes = ["sysdeps"],
+ deps = ["@boringssl//:bssl"],
)
-
-# TODO(zyecheng): Add build recipes for BoringSSL
diff --git a/third_party/boringssl/BUILD.gn b/third_party/boringssl/BUILD.gn
index af41f2822..b0174f332 100644
--- a/third_party/boringssl/BUILD.gn
+++ b/third_party/boringssl/BUILD.gn
@@ -14,22 +14,24 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
import("$dir_pw_third_party/boringssl/boringssl.gni")
import("$dir_pw_unit_test/test.gni")
-if (dir_pw_third_party_boringssl != "") {
+if (pw_third_party_boringssl_ALIAS != "") {
+ assert(dir_pw_third_party_boringssl == "")
+
+ pw_source_set("boringssl") {
+ public_deps = [ pw_third_party_boringssl_ALIAS ]
+ }
+} else if (dir_pw_third_party_boringssl != "") {
import("$dir_pw_third_party_boringssl/BUILD.generated.gni")
- config("boringssl_public_config") {
+ config("public_config") {
include_dirs = [
"$dir_pw_third_party_boringssl/src/include",
"public",
]
- cflags = [
- "-Wno-cast-qual",
- "-Wno-ignored-qualifiers",
- "-w",
- ]
# This can be removed once boringssl threading primitives are implemented,
# i.e. using pw_sync, and when we have a posix style socket layer.
@@ -37,7 +39,7 @@ if (dir_pw_third_party_boringssl != "") {
[ "OPENSSL_NO_THREADS_CORRUPT_MEMORY_AND_LEAK_SECRETS_IF_THREADED" ]
}
- config("boringssl_internal_config") {
+ config("internal_config") {
defines = [
# Enable virtual desctructor and compile-time check of pure virtual base class
"BORINGSSL_ALLOW_CXX_RUNTIME",
@@ -57,6 +59,7 @@ if (dir_pw_third_party_boringssl != "") {
"-Wno-conversion",
"-Wno-unused-parameter",
"-Wno-char-subscripts",
+ "-w",
]
cflags_cc = [
"-fpermissive",
@@ -78,8 +81,8 @@ if (dir_pw_third_party_boringssl != "") {
foreach(source, crypto_sources - excluded_sources + ssl_sources) {
sources += [ "$dir_pw_third_party_boringssl/$source" ]
}
- public_configs = [ ":boringssl_public_config" ]
- configs = [ ":boringssl_internal_config" ]
+ public_configs = [ ":public_config" ]
+ configs = [ ":internal_config" ]
# Contains a faked "sysdeps/sys/socket.h"
# Can be removed once posix socket layer in Pigweed is supported.
@@ -91,3 +94,7 @@ if (dir_pw_third_party_boringssl != "") {
group("boringssl") {
}
}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/third_party/boringssl/README.md b/third_party/boringssl/README.md
deleted file mode 100644
index 0d3e06d0b..000000000
--- a/third_party/boringssl/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# BoringSSL Library
-
-The folder provides build scripts for building the BoringSSL library. The
-source code needs to be downloaded by the user. It is recommended to download
-via "pw package install boringssl". This ensures that necessary build files
-are generated. It als downloads the chromium verifier library, which will be
-used as the default certificate verifier for boringssl in pw_tls_client.
-For gn build, set `dir_pw_third_party_boringssl` to point to the
-path of the source code. For applications using BoringSSL, add
-`$dir_pw_third_party/boringssl` to the dependency list.
diff --git a/third_party/boringssl/boringssl.gni b/third_party/boringssl/boringssl.gni
index 12f7f6bb2..b7275d4db 100644
--- a/third_party/boringssl/boringssl.gni
+++ b/third_party/boringssl/boringssl.gni
@@ -17,4 +17,10 @@ declare_args() {
# boringssl source code. When set, a pw_source_set for the boringssl library is
# created at "$dir_pw_third_party/boringssl".
dir_pw_third_party_boringssl = ""
+
+ # Create a "$dir_pw_third_party/boringssl" target that aliases an existing
+ # target. This can be used to fix a diamond dependency conflict if a
+ # downstream project uses its own boringssl target and cannot be changed to
+ # use Pigweed's boringssl exclusively.
+ pw_third_party_boringssl_ALIAS = ""
}
diff --git a/third_party/boringssl/docs.rst b/third_party/boringssl/docs.rst
new file mode 100644
index 000000000..f2442f5e8
--- /dev/null
+++ b/third_party/boringssl/docs.rst
@@ -0,0 +1,78 @@
+.. _module-pw_third_party_boringssl:
+
+=========
+BoringSSL
+=========
+
+The ``$dir_pw_third_party/boringssl`` module provides the build files to
+compile and use BoringSSL. The source code of BoringSSL needs to be provided by
+the user. It is recommended to download it via Git submodules.
+
+-------------
+Build support
+-------------
+
+This module provides support to compile BoringSSL with GN. This is required when
+compiling backends modules that use BoringSSL, such as some facades in
+:ref:`module-pw_crypto`
+
+Submodule
+=========
+
+The recommended way to include BoringSSL source code is to add it as a
+submodule:
+
+.. code-block:: sh
+
+ git submodule add https://boringssl.googlesource.com/boringssl/ \
+ third_party/boringssl/src
+
+GN
+==
+The GN build file depends on a generated file called ``BUILD.generated.gni``
+with the list of the different types of source files for the selected BoringSSL
+version.
+
+.. code-block:: sh
+
+ cd third_party/boringssl
+ python src/util/generate_build_files.py gn
+
+The GN variables needed are defined in
+``$dir_pw_third_party/boringssl/boringssl.gni``:
+
+#. Set the GN ``dir_pw_third_party_boringssl`` to the path of the BoringSSL
+ installation.
+
+ - If using the submodule path from above, add the following to the
+ ``default_args`` in the project's ``.gn``:
+
+ .. code-block::
+
+ dir_pw_third_party_boringssl = "//third_party/boringssl/src"
+
+#. Having a non-empty ``dir_pw_third_party_boringssl`` variable causes GN to
+ attempt to include the ``BUILD.generated.gni`` file from the sources even
+ during the bootstrap process before the source package is installed by the
+ bootstrap process. To avoid this problem, set this variable to the empty
+ string during bootstrap by adding it to the ``virtualenv.gn_args`` setting in
+ the ``env_setup.json`` file:
+
+ .. code-block:: json
+
+ {
+ "virtualenv": {
+ "gn_args": [
+ "dir_pw_third_party_boringssl=\"\""
+ ]
+ }
+ }
+
+#. Alternatively, set the GN ``pw_third_party_boringssl_ALIAS`` to your
+ boringssl build target if you would like to use your own build target instead
+ of the one provided by Pigweed. This should be used instead of
+ ``dir_pw_third_party_boringssl``. This fixes diamond dependency conflicts
+ caused by two build targets using the same source files.
+
+After this is done a ``pw_source_set`` for the BoringSSL library is created at
+``$dir_pw_third_party/boringssl``.
diff --git a/third_party/emboss/BUILD.gn b/third_party/emboss/BUILD.gn
new file mode 100644
index 000000000..a0ef87dda
--- /dev/null
+++ b/third_party/emboss/BUILD.gn
@@ -0,0 +1,71 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/python.gni")
+
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_third_party/emboss/emboss.gni")
+
+config("default_config") {
+ # EMBOSS_DCHECK is used as an assert() for logic embedded in Emboss, where
+ # EMBOSS_CHECK is used to check preconditions on application logic (e.g.
+ # Write() checks the [requires: ...] attribute).
+ defines = [
+ "EMBOSS_CHECK=PW_DCHECK",
+ "EMBOSS_CHECK_ABORTS",
+ "EMBOSS_DCHECK=PW_DCHECK",
+ "EMBOSS_DCHECK_ABORTS",
+ ]
+}
+
+source_set("cpp_utils") {
+ public_configs = [ pw_third_party_emboss_CONFIG ]
+ sources = [
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_arithmetic.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_arithmetic_all_known_generated.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_arithmetic_maximum_operation_generated.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_array_view.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_bit_util.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_constant_view.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_cpp_types.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_cpp_util.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_defines.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_enum_view.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_maybe.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_memory_util.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_prelude.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_text_util.h",
+ "$dir_pw_third_party_emboss/runtime/cpp/emboss_view_parameters.h",
+ ]
+}
+
+# Exists solely to satisfy presubmit. embossc_runner.py is used for real in
+# build_defs.gni.
+action("embossc_runner") {
+ script = "embossc_runner.py"
+ visibility = []
+ outputs = [ "$target_gen_dir/foo" ]
+}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+# Flags that are needed to compile targets that depend on Emboss.
+# TODO(benlawson): Fix Emboss upstream so this can be removed
+# (https://github.com/google/emboss/issues/69)
+config("flags") {
+ cflags = [ "-Wno-unused-parameter" ]
+}
diff --git a/third_party/emboss/OWNERS b/third_party/emboss/OWNERS
new file mode 100644
index 000000000..dc0a3fc4a
--- /dev/null
+++ b/third_party/emboss/OWNERS
@@ -0,0 +1,2 @@
+benlawson@google.com
+saeedali@google.com
diff --git a/third_party/emboss/build_defs.gni b/third_party/emboss/build_defs.gni
new file mode 100644
index 000000000..74bd061ae
--- /dev/null
+++ b/third_party/emboss/build_defs.gni
@@ -0,0 +1,167 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_third_party/emboss/emboss.gni")
+
+# Compiles a .emb file into a .h file.
+# $dir_pw_third_party_emboss must be set to the path of your Emboss
+# installation.
+#
+# Parameters
+#
+# source (required)
+# [path] The path to the .emb file that is to be compiled.
+#
+# import_dirs (optional)
+# [list of paths] The list of directories to search for imported .emb files,
+# in the order they should be searched. The directory that `source` is in is
+# always searched first.
+#
+# imports (optional)
+# [list of paths] Paths to any imported .emb files, including those imported
+# resursively.
+#
+# deps (optional)
+# [list of targets] Paths to other emboss_cc_library targets that are
+# imported by this target.
+template("emboss_cc_library") {
+ assert(defined(invoker.source), "Need source arg for emboss_cc_library")
+ assert(
+ "$dir_pw_third_party_emboss" != "",
+ "\$dir_pw_third_party_emboss must be set to the path of your Emboss installation")
+
+ # The --output-path arg to the embossc script only specifies the
+ # prefix of the path at which the generated header is placed. To this
+ # prefix, the script appends the entire path of the input Emboss source,
+ # which is provided as rebase_path(invoker.source, root_build_dir).
+
+ # rebase_path(invoker.source, root_build_dir) will always start with a number
+ # of updirs (e.g. "../../../").
+
+ # In order for the compiled header path in the embossc script to resolve to
+ # $target_gen_dir as we desire, we must provide an --output-path arg that
+ # resolves to $target_gen_dir after rebase_path(invoker.source,
+ # root_build_dir) is appended to it. To achieve this, we specify output-path
+ # to be $root_gen_dir followed by a number of fake directories needed to
+ # cancel out these starting updirs.
+ compiled_header_path = "$target_gen_dir/" + invoker.source + ".h"
+ path_sep = "/"
+ elements = string_split(rebase_path(invoker.source, root_build_dir), path_sep)
+ updirs = filter_include(elements, [ ".." ])
+
+ fakedirs = []
+ foreach(element, updirs) {
+ fakedirs += [ "fake" ]
+ }
+ output_path = root_gen_dir + path_sep + string_join(path_sep, fakedirs)
+
+ # Look for imports in the same directory as invoker.source by default.
+ default_import_dir = get_path_info(invoker.source, "abspath")
+ default_import_dir = get_path_info(default_import_dir, "dir")
+ default_import_dir = rebase_path(default_import_dir, root_build_dir)
+
+ action(target_name + "_header") {
+ script = "$dir_pw_third_party/emboss/embossc_runner.py"
+
+ args = [
+ rebase_path("$dir_pw_third_party_emboss/embossc", root_build_dir),
+ "--generate",
+ "cc",
+ "--import-dir",
+ default_import_dir,
+ "--output-path",
+ rebase_path(output_path, root_build_dir),
+ rebase_path(invoker.source, root_build_dir),
+ ]
+ if (defined(invoker.import_dirs)) {
+ foreach(dir, invoker.import_dirs) {
+ args += [
+ "--import-dir",
+ dir,
+ ]
+ }
+ }
+
+ sources = [
+ "$dir_pw_third_party_emboss/compiler/back_end/__init__.py",
+ "$dir_pw_third_party_emboss/compiler/back_end/cpp/__init__.py",
+ "$dir_pw_third_party_emboss/compiler/back_end/cpp/emboss_codegen_cpp.py",
+ "$dir_pw_third_party_emboss/compiler/back_end/cpp/generated_code_templates",
+ "$dir_pw_third_party_emboss/compiler/back_end/cpp/header_generator.py",
+ "$dir_pw_third_party_emboss/compiler/back_end/util/__init__.py",
+ "$dir_pw_third_party_emboss/compiler/back_end/util/code_template.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/__init__.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/attribute_checker.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/attributes.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/constraints.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/dependency_checker.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/emboss_front_end.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/error_examples",
+ "$dir_pw_third_party_emboss/compiler/front_end/expression_bounds.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/glue.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/lr1.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/module_ir.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/parser.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/prelude.emb",
+ "$dir_pw_third_party_emboss/compiler/front_end/reserved_words",
+ "$dir_pw_third_party_emboss/compiler/front_end/symbol_resolver.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/synthetics.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/tokenizer.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/type_check.py",
+ "$dir_pw_third_party_emboss/compiler/front_end/write_inference.py",
+ "$dir_pw_third_party_emboss/compiler/util/error.py",
+ "$dir_pw_third_party_emboss/compiler/util/expression_parser.py",
+ "$dir_pw_third_party_emboss/compiler/util/ir_pb2.py",
+ "$dir_pw_third_party_emboss/compiler/util/ir_util.py",
+ "$dir_pw_third_party_emboss/compiler/util/name_conversion.py",
+ "$dir_pw_third_party_emboss/compiler/util/parser_types.py",
+ "$dir_pw_third_party_emboss/compiler/util/simple_memoizer.py",
+ "$dir_pw_third_party_emboss/compiler/util/traverse_ir.py",
+ "$dir_pw_third_party_emboss/embossc",
+ invoker.source,
+ ]
+
+ # TODO(benlawson): Use a depfile for adding imports to inputs (https://github.com/google/emboss/issues/68).
+ if (defined(invoker.imports)) {
+ inputs = invoker.imports
+ }
+
+ outputs = [ compiled_header_path ]
+ }
+
+ config(target_name + "_emboss_config") {
+ include_dirs = [
+ "$dir_pw_third_party_emboss",
+ root_gen_dir,
+ ]
+ }
+
+ source_set(target_name) {
+ forward_variables_from(invoker, "*")
+
+ sources = [ compiled_header_path ]
+
+ if (!defined(invoker.deps)) {
+ deps = []
+ }
+ deps += [ "$dir_pw_third_party/emboss:cpp_utils" ]
+ public_deps = [ ":" + target_name + "_header" ]
+
+ if (!defined(invoker.public_configs)) {
+ public_configs = []
+ }
+ public_configs += [ ":" + target_name + "_emboss_config" ]
+ }
+}
diff --git a/third_party/emboss/docs.rst b/third_party/emboss/docs.rst
new file mode 100644
index 000000000..1cabc8aae
--- /dev/null
+++ b/third_party/emboss/docs.rst
@@ -0,0 +1,63 @@
+.. _module-pw_third_party_emboss:
+
+======
+Emboss
+======
+`Emboss <https://github.com/google/emboss>`_ is a tool for generating code to
+safely read and write binary data structures.
+
+The ``$dir_pw_third_party/emboss`` module provides an ``emboss_cc_library`` GN
+template, defined in build_defs.gni, which generates C++ bindings for the given
+Emboss source file. The Emboss source code needs to be provided by the user.
+
+------------------
+Configuring Emboss
+------------------
+The recommended way to include the Emboss source code is to add it as a
+Git submodule:
+
+.. code-block:: sh
+
+ git submodule add https://github.com/google/emboss.git third_party/emboss/src
+
+Next, set the GN variable ``dir_pw_third_party_emboss`` to the path of your Emboss
+installation. If using the submodule path from above, add the following to the
+``default_args`` of your project's ``.gn`` file:
+
+.. code-block::
+
+ dir_pw_third_party_emboss = "//third_party/emboss/src"
+
+..
+ inclusive-language: disable
+
+Optionally, configure the Emboss defines documented at
+`dir_pw_third_party_emboss/runtime/cpp/emboss_defines.h
+<https://github.com/google/emboss/blob/master/runtime/cpp/emboss_defines.h>`_
+by setting the ``pw_third_party_emboss_CONFIG`` variable to a config that
+overrides the defines. By default, checks will use PW_DCHECK.
+
+..
+ inclusive-language: enable
+
+------------
+Using Emboss
+------------
+Let's say you've authored an Emboss source file at ``//some/path/to/my-protocol.emb``.
+To generate its bindings, you can add the following to ``//some/path/to/BUILD.gn``:
+
+.. code-block::
+
+ import("$dir_pw_third_party/emboss/build_defs.gni")
+
+ emboss_cc_library("protocol") {
+ source = "my-protocol.emb"
+ }
+
+This generates a source set of the same name as the target, in this case "protocol".
+To use the bindings, list this target as a dependency in GN and include the generated
+header by adding ``.h`` to the path of your Emboss source file:
+
+.. code-block::
+
+ #include <some/path/to/protocol.emb.h>
diff --git a/third_party/emboss/emboss.gni b/third_party/emboss/emboss.gni
new file mode 100644
index 000000000..93d8396d7
--- /dev/null
+++ b/third_party/emboss/emboss.gni
@@ -0,0 +1,25 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+declare_args() {
+ # If compiling with Emboss, this variable is set to the path to the Emboss
+ # source code.
+ dir_pw_third_party_emboss = ""
+
+ # config target for overriding Emboss defines (e.g. EMBOSS_CHECK).
+ pw_third_party_emboss_CONFIG =
+ "$dir_pigweed/third_party/emboss:default_config"
+}
diff --git a/third_party/emboss/embossc_runner.py b/third_party/emboss/embossc_runner.py
new file mode 100644
index 000000000..7943c210a
--- /dev/null
+++ b/third_party/emboss/embossc_runner.py
@@ -0,0 +1,23 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import sys
+import importlib.util
+
+loader = importlib.machinery.SourceFileLoader("embossc", sys.argv[1])
+spec = importlib.util.spec_from_loader(loader.name, loader)
+main_module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(main_module)
+
+sys.exit(main_module.main(sys.argv[1:]))
diff --git a/third_party/freertos/BUILD.bazel b/third_party/freertos/BUILD.bazel
index f635a0f6d..01256d41b 100644
--- a/third_party/freertos/BUILD.bazel
+++ b/third_party/freertos/BUILD.bazel
@@ -13,7 +13,7 @@
# the License.
load(
- "//pw_build:pigweed.bzl",
+ "@pigweed//pw_build:pigweed.bzl",
"pw_cc_library",
)
@@ -28,6 +28,154 @@ pw_cc_library(
],
includes = ["public"],
deps = [
- "//pw_assert",
+ "@pigweed//pw_assert",
],
)
+
+constraint_setting(
+ name = "port",
+)
+
+constraint_value(
+ name = "port_ARM_CM7",
+ constraint_setting = ":port",
+)
+
+constraint_value(
+ name = "port_ARM_CM4F",
+ constraint_setting = ":port",
+)
+
+pw_cc_library(
+ name = "freertos",
+ srcs = [
+ "croutine.c",
+ "event_groups.c",
+ "list.c",
+ "queue.c",
+ "stream_buffer.c",
+ "timers.c",
+ ] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/port.c"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/port.c"],
+ "//conditions:default": [],
+ }),
+ includes = ["include/"] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1"],
+ "//conditions:default": [],
+ }),
+ textual_hdrs = [
+ "include/FreeRTOS.h",
+ "include/StackMacros.h",
+ "include/croutine.h",
+ "include/deprecated_definitions.h",
+ "include/event_groups.h",
+ "include/list.h",
+ "include/message_buffer.h",
+ "include/mpu_wrappers.h",
+ "include/portable.h",
+ "include/projdefs.h",
+ "include/queue.h",
+ "include/semphr.h",
+ "include/stack_macros.h",
+ "include/stream_buffer.h",
+ "include/task.h",
+ "include/timers.h",
+ ] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/portmacro.h"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/portmacro.h"],
+ "//conditions:default": [],
+ }),
+ deps = [
+ ":pigweed_tasks_c",
+ "@pigweed_config//:freertos_config",
+ ],
+ # Required because breaking out tasks_c results in the dependencies between
+ # the libraries not being quite correct: to link pigweed_tasks_c you
+ # actually need a bunch of the source files from here (e.g., list.c).
+ alwayslink = 1,
+)
+
+# Constraint setting used to determine if task statics should be disabled.
+constraint_setting(
+ name = "disable_tasks_statics_setting",
+ default_constraint_value = ":no_disable_task_statics",
+)
+
+constraint_value(
+ name = "disable_task_statics",
+ constraint_setting = ":disable_tasks_statics_setting",
+)
+
+constraint_value(
+ name = "no_disable_task_statics",
+ constraint_setting = ":disable_tasks_statics_setting",
+)
+
+pw_cc_library(
+ name = "pigweed_tasks_c",
+ srcs = ["tasks.c"],
+ defines = select({
+ ":disable_task_statics": [
+ "PW_THIRD_PARTY_FREERTOS_NO_STATICS=1",
+ ],
+ "//conditions:default": [],
+ }),
+ includes = [
+ "include/",
+ ] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/"],
+ "//conditions:default": [],
+ }),
+ local_defines = select({
+ ":disable_task_statics": [
+ "static=",
+ ],
+ "//conditions:default": [],
+ }),
+ # tasks.c transitively includes all these headers :/
+ textual_hdrs = [
+ "include/FreeRTOS.h",
+ "include/portable.h",
+ "include/projdefs.h",
+ "include/list.h",
+ "include/deprecated_definitions.h",
+ "include/mpu_wrappers.h",
+ "include/stack_macros.h",
+ "include/task.h",
+ "include/timers.h",
+ ] + select({
+ ":port_ARM_CM4F": ["portable/GCC/ARM_CM4F/portmacro.h"],
+ ":port_ARM_CM7": ["portable/GCC/ARM_CM7/r0p1/portmacro.h"],
+ "//conditions:default": [],
+ }),
+ deps = ["@pigweed_config//:freertos_config"],
+)
+
+# Constraint setting used to select the FreeRTOSConfig version.
+constraint_setting(
+ name = "freertos_config_setting",
+)
+
+alias(
+ name = "freertos_config",
+ actual = select({
+ "@pigweed//targets/stm32f429i_disc1_stm32cube:freertos_config_cv": "@pigweed//targets/stm32f429i_disc1_stm32cube:freertos_config",
+ "//conditions:default": "default_freertos_config",
+ }),
+)
+
+pw_cc_library(
+ name = "default_freertos_config",
+ # The "default" config is not compatible with any configuration: you can't
+ # build FreeRTOS without choosing a config.
+ target_compatible_with = ["@platforms//:incompatible"],
+)
+
+# Exported for
+# pw_thread_freertos/py/pw_thread_freertos/generate_freertos_tsktcb.py
+exports_files(
+ ["tasks.c"],
+)
diff --git a/third_party/freertos/BUILD.gn b/third_party/freertos/BUILD.gn
index 48749ccc9..f4c12bb07 100644
--- a/third_party/freertos/BUILD.gn
+++ b/third_party/freertos/BUILD.gn
@@ -45,6 +45,7 @@ if (dir_pw_third_party_freertos == "") {
cflags = [
"-Wno-unused-parameter",
"-Wno-cast-qual",
+ "-Wno-int-in-bool-context",
]
visibility = [ ":*" ]
}
@@ -172,6 +173,23 @@ if (dir_pw_third_party_freertos == "") {
sources = [ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM3/port.c" ]
configs = [ ":disable_warnings" ]
}
+
+ # ARM CM0 port of FreeRTOS.
+ config("arm_cm0_includes") {
+ include_dirs = [ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM0" ]
+ visibility = [ ":arm_cm0" ]
+ }
+
+ pw_source_set("arm_cm0") {
+ public_configs = [
+ ":arm_cm0_includes",
+ ":public_includes",
+ ]
+ public_deps = [ pw_third_party_freertos_CONFIG ]
+ public = [ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM0/portmacro.h" ]
+ sources = [ "$dir_pw_third_party_freertos/portable/GCC/ARM_CM0/port.c" ]
+ configs = [ ":disable_warnings" ]
+ }
}
config("public_include_path") {
diff --git a/third_party/freertos/CMakeLists.txt b/third_party/freertos/CMakeLists.txt
index 3208003dd..0efa9780f 100644
--- a/third_party/freertos/CMakeLists.txt
+++ b/third_party/freertos/CMakeLists.txt
@@ -24,7 +24,7 @@ set(pw_third_party_freertos_PORT "" CACHE STRING
option(pw_third_party_freertos_DISABLE_TASKS_STATICS
"Whether to disable statics inside of tasks.c")
-pw_add_module_library(pw_third_party.freertos.disable_warnings
+pw_add_library(pw_third_party.freertos.disable_warnings INTERFACE
PUBLIC_COMPILE_OPTIONS
-Wno-unused-parameter
-Wno-cast-qual
@@ -33,18 +33,12 @@ pw_add_module_library(pw_third_party.freertos.disable_warnings
# If FreeRTOS is not configured, a script that displays an error message is used
# instead. If the build rule is used in the build it fails with this error.
if(NOT dir_pw_third_party_freertos)
- add_custom_target(pw_third_party.freertos._not_configured
- COMMAND
- "${CMAKE_COMMAND}" -E echo
- "ERROR: Attempted to build the pw_third_party.freertos without"
- "configuring it via dir_pw_third_party_freertos."
- "See https://pigweed.dev/third_party/freertos."
- COMMAND
- "${CMAKE_COMMAND}" -E false
+ pw_add_error_target(pw_third_party.freertos
+ MESSAGE
+ "Attempted to build the pw_third_party.freertos without configuring it "
+ "via dir_pw_third_party_freertos. "
+ "See https://pigweed.dev/third_party/freertos."
)
- add_library(pw_third_party.freertos INTERFACE)
- add_dependencies(pw_third_party.freertos
- pw_third_party.freertos._not_configured)
return()
else(dir_pw_third_party_freertos)
if(NOT pw_third_party_freertos_PORT)
@@ -56,7 +50,7 @@ else(dir_pw_third_party_freertos)
"pw_third_party_freertos_CONFIG is not set.")
endif()
- pw_add_module_library(pw_third_party.freertos
+ pw_add_library(pw_third_party.freertos STATIC
HEADERS
${dir_pw_third_party_freertos}/include/FreeRTOS.h
${dir_pw_third_party_freertos}/include/StackMacros.h
@@ -96,7 +90,7 @@ endif()
if(pw_third_party_freertos_DISABLE_TASKS_STATICS)
set(disable_tasks_statics "static=" "PW_THIRD_PARTY_FREERTOS_NO_STATICS=1")
endif()
-pw_add_module_library(pw_third_party.freertos.freertos_tasks
+pw_add_library(pw_third_party.freertos.freertos_tasks STATIC
SOURCES
${dir_pw_third_party_freertos}/tasks.c
PRIVATE_DEPS
@@ -110,7 +104,7 @@ pw_add_module_library(pw_third_party.freertos.freertos_tasks
)
# ARM CM7 port of FreeRTOS.
-pw_add_module_library(pw_third_party.freertos.arm_cm7
+pw_add_library(pw_third_party.freertos.arm_cm7 STATIC
HEADERS
${dir_pw_third_party_freertos}/portable/GCC/ARM_CM7/r0p1/portmacro.h
PUBLIC_DEPS
@@ -125,7 +119,7 @@ pw_add_module_library(pw_third_party.freertos.arm_cm7
)
# ARM CM4F port of FreeRTOS.
-pw_add_module_library(pw_third_party.freertos.arm_cm4f
+pw_add_library(pw_third_party.freertos.arm_cm4f STATIC
HEADERS
${dir_pw_third_party_freertos}/portable/GCC/ARM_CM4F/portmacro.h
PUBLIC_DEPS
@@ -140,7 +134,7 @@ pw_add_module_library(pw_third_party.freertos.arm_cm4f
)
# ARM CM33F port of FreeRTOS.
-pw_add_module_library(pw_third_party.freertos.arm_cm33f
+pw_add_library(pw_third_party.freertos.arm_cm33f STATIC
HEADERS
${dir_pw_third_party_freertos}/portable/GCC/ARM_CM33F/portmacro.h
PUBLIC_DEPS
@@ -154,7 +148,7 @@ pw_add_module_library(pw_third_party.freertos.arm_cm33f
pw_third_party.freertos.disable_warnings
)
-pw_add_module_library(pw_third_party.freertos.config_assert
+pw_add_library(pw_third_party.freertos.config_assert INTERFACE
HEADERS
public/pw_third_party/freertos/config_assert.h
PUBLIC_INCLUDES
diff --git a/third_party/freertos/docs.rst b/third_party/freertos/docs.rst
index e7d905084..695dd7d32 100644
--- a/third_party/freertos/docs.rst
+++ b/third_party/freertos/docs.rst
@@ -10,8 +10,8 @@ FreeRTOS, including Pigweed backend modules which depend on FreeRTOS.
-------------
Build Support
-------------
-This module provides support to compile FreeRTOS with GN and CMake. This is
-required when compiling backends modules for FreeRTOS.
+This module provides support to compile FreeRTOS with GN, CMake, and Bazel.
+This is required when compiling backends modules for FreeRTOS.
GN
==
@@ -33,13 +33,31 @@ CMake
In order to use this you are expected to set the following variables from
``third_party/freertos/CMakeLists.txt``:
-#. Set the GN ``dir_pw_third_party_freertos`` to the path of the FreeRTOS
- installation.
+#. Set ``dir_pw_third_party_freertos`` to the path of the FreeRTOS installation.
#. Set ``pw_third_party_freertos_CONFIG`` to a library target which provides
the FreeRTOS config header.
#. Set ``pw_third_party_freertos_PORT`` to a library target which provides
the FreeRTOS port specific includes and sources.
+Bazel
+=====
+In Bazel, the FreeRTOS build is configured through `constraint_settings
+<https://bazel.build/reference/be/platform#constraint_setting>`_. The `platform
+<https://bazel.build/extending/platforms>`_ you are building for must specify
+values for the following settings:
+
+* ``//third_party/freertos:port``, to set which FreeRTOS port to use. You can
+ select a value from those defined in ``third_party/freertos/BUILD.bazel``.
+* ``//third_party/freertos:disable_task_statics_setting``, to determine
+ whether statics should be disabled during compilation of the tasks.c source
+ file (see next section). This setting has only two possible values, also
+ defined in ``third_party/freertos/BUILD.bazel``.
+
+In addition, you need to set the ``@pigweed_config//:freertos_config`` label
+flag to point to the library target providing the FreeRTOS config header. See
+:ref:`docs-build_system-bazel_configuration` for a discussion of how to work
+with ``@pigweed_config``.
+
.. _third_party-freertos_disable_task_statics:
@@ -49,10 +67,12 @@ In order to link against internal kernel data structures through the use of
extern "C", statics can be optionally disabled for the tasks.c source file
to enable use of things like pw_thread_freertos/util.h's ``ForEachThread``.
-To facilitate this, Pigweed offers an opt-in option which can be enabled by
-configuring GN through
-``pw_third_party_freertos_DISABLE_TASKS_STATICS = true`` or CMake through
-``set(pw_third_party_freertos_DISABLE_TASKS_STATICS ON CACHE BOOL "" FORCE)``.
+To facilitate this, Pigweed offers an opt-in option which can be enabled,
+
+* in GN through ``pw_third_party_freertos_DISABLE_TASKS_STATICS = true``,
+* in CMake through ``set(pw_third_party_freertos_DISABLE_TASKS_STATICS ON CACHE BOOL "" FORCE)``,
+* in Bazel through ``//third_party/freertos:disable_task_statics``.
+
This redefines ``static`` to nothing for the ``Source/tasks.c`` FreeRTOS source
file when building through ``$dir_pw_third_party/freertos`` in GN and through
``pw_third_party.freertos`` in CMake.
diff --git a/third_party/fuchsia/BUILD.bazel b/third_party/fuchsia/BUILD.bazel
new file mode 100644
index 000000000..d3d90a74f
--- /dev/null
+++ b/third_party/fuchsia/BUILD.bazel
@@ -0,0 +1,71 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_library",
+)
+
+package(default_visibility = ["//visibility:public"])
+
+licenses(["notice"])
+
+pw_cc_library(
+ name = "config",
+ hdrs = ["public/pw_function/config.h"],
+ includes = ["public"],
+)
+
+pw_cc_library(
+ name = "fit",
+ srcs = [
+ "repo/sdk/lib/fit/include/lib/fit/internal/compiler.h",
+ "repo/sdk/lib/fit/include/lib/fit/internal/function.h",
+ "repo/sdk/lib/fit/include/lib/fit/internal/result.h",
+ "repo/sdk/lib/fit/include/lib/fit/internal/utility.h",
+ ],
+ hdrs = [
+ "repo/sdk/lib/fit/include/lib/fit/function.h",
+ "repo/sdk/lib/fit/include/lib/fit/nullable.h",
+ "repo/sdk/lib/fit/include/lib/fit/result.h",
+ "repo/sdk/lib/fit/include/lib/fit/traits.h",
+ ],
+ includes = ["repo/sdk/lib/fit/include"],
+ deps = [
+ ":stdcompat",
+ "//pw_assert",
+ ],
+)
+
+pw_cc_library(
+ name = "stdcompat",
+ srcs = [
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h",
+ ],
+ hdrs = [
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h",
+ ],
+ includes = ["repo/sdk/lib/stdcompat/include"],
+)
diff --git a/third_party/fuchsia/BUILD.gn b/third_party/fuchsia/BUILD.gn
new file mode 100644
index 000000000..2773a80d1
--- /dev/null
+++ b/third_party/fuchsia/BUILD.gn
@@ -0,0 +1,90 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/python.gni")
+import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
+import("$dir_pw_unit_test/test.gni")
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
+
+config("fit_public_include_path") {
+ include_dirs = [ "repo/sdk/lib/fit/include" ]
+ visibility = [ ":*" ]
+}
+
+config("stdcompat_public_include_path") {
+ include_dirs = [ "repo/sdk/lib/stdcompat/include" ]
+ visibility = [ ":*" ]
+}
+
+pw_source_set("fit") {
+ public_configs = [ ":fit_public_include_path" ]
+ public_deps = [
+ ":stdcompat",
+ dir_pw_assert,
+ ]
+ public = [
+ "repo/sdk/lib/fit/include/lib/fit/function.h",
+ "repo/sdk/lib/fit/include/lib/fit/nullable.h",
+ "repo/sdk/lib/fit/include/lib/fit/result.h",
+ "repo/sdk/lib/fit/include/lib/fit/traits.h",
+ ]
+ sources = [
+ "repo/sdk/lib/fit/include/lib/fit/internal/compiler.h",
+ "repo/sdk/lib/fit/include/lib/fit/internal/function.h",
+ "repo/sdk/lib/fit/include/lib/fit/internal/result.h",
+ "repo/sdk/lib/fit/include/lib/fit/internal/utility.h",
+ ]
+}
+
+pw_test("function_tests") {
+ sources = [ "repo/sdk/lib/fit/test/function_tests.cc" ]
+ deps = [ ":fit" ]
+
+ # Define EXPECT_NULL(), which Pigweed's test framework does not have
+ defines = [ "EXPECT_NULL(arg)=EXPECT_EQ((arg), nullptr)" ]
+
+ # This test does not build with strict warnings, so disable them.
+ remove_configs = [ "$dir_pw_build:strict_warnings" ]
+}
+
+pw_source_set("stdcompat") {
+ public_configs = [ ":stdcompat_public_include_path" ]
+ public = [
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h",
+ ]
+ sources = [
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h",
+ "repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h",
+ ]
+}
+
+pw_python_script("generate_fuchsia_patch") {
+ sources = [ "generate_fuchsia_patch.py" ]
+}
diff --git a/third_party/fuchsia/CMakeLists.txt b/third_party/fuchsia/CMakeLists.txt
new file mode 100644
index 000000000..1efc5d0db
--- /dev/null
+++ b/third_party/fuchsia/CMakeLists.txt
@@ -0,0 +1,51 @@
+# Copyright 2021 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+pw_add_library(pw_third_party.fuchsia.fit INTERFACE
+ HEADERS
+ repo/sdk/lib/fit/include/lib/fit/function.h
+ repo/sdk/lib/fit/include/lib/fit/internal/compiler.h
+ repo/sdk/lib/fit/include/lib/fit/internal/result.h
+ repo/sdk/lib/fit/include/lib/fit/internal/function.h
+ repo/sdk/lib/fit/include/lib/fit/internal/utility.h
+ repo/sdk/lib/fit/include/lib/fit/nullable.h
+ repo/sdk/lib/fit/include/lib/fit/result.h
+ repo/sdk/lib/fit/include/lib/fit/traits.h
+ PUBLIC_INCLUDES
+ repo/sdk/lib/fit/include
+ PUBLIC_DEPS
+ pw_third_party.fuchsia.stdcompat
+ pw_assert.assert
+)
+
+pw_add_library(pw_third_party.fuchsia.stdcompat INTERFACE
+ HEADERS
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h
+ repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h
+ PUBLIC_INCLUDES
+ repo/sdk/lib/stdcompat/include
+)
diff --git a/third_party/fuchsia/OWNERS b/third_party/fuchsia/OWNERS
new file mode 100644
index 000000000..73d84b4af
--- /dev/null
+++ b/third_party/fuchsia/OWNERS
@@ -0,0 +1,2 @@
+hepler@google.com
+mohrr@google.com
diff --git a/third_party/fuchsia/copy.bara.sky b/third_party/fuchsia/copy.bara.sky
new file mode 100644
index 000000000..7d51519e8
--- /dev/null
+++ b/third_party/fuchsia/copy.bara.sky
@@ -0,0 +1,72 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+fuchsia_repo_files = [
+ ".clang-format",
+ # fit
+ "sdk/lib/fit/include/lib/fit/function.h",
+ "sdk/lib/fit/include/lib/fit/internal/compiler.h",
+ "sdk/lib/fit/include/lib/fit/internal/function.h",
+ "sdk/lib/fit/include/lib/fit/internal/result.h",
+ "sdk/lib/fit/include/lib/fit/internal/utility.h",
+ "sdk/lib/fit/include/lib/fit/nullable.h",
+ "sdk/lib/fit/include/lib/fit/result.h",
+ "sdk/lib/fit/include/lib/fit/traits.h",
+ "sdk/lib/fit/test/function_tests.cc",
+ # stdcompat
+ "sdk/lib/stdcompat/include/lib/stdcompat/bit.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/functional.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/memory.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/optional.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/utility.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/version.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h",
+ "sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h",
+]
+
+core.workflow(
+ name = "default",
+ description = "Imports files from Fuchsia's fit library",
+ origin = git.origin(
+ url = "https://fuchsia.googlesource.com/fuchsia",
+ ref = "main",
+ ),
+ destination = git.gerrit_destination(
+ url = "https://pigweed.googlesource.com/pigweed/pigweed",
+ fetch = "main",
+ push_to_refs_for = "main%message=No%2dDocs%2dUpdate%2dReason%3a_copybara_import",
+ labels = ["Commit-Queue+1"],
+ checker = leakr.disable_check("Syncing between OSS projects"),
+ ),
+ origin_files = glob(fuchsia_repo_files),
+ destination_files = glob(["third_party/fuchsia/repo/**"]),
+ authoring = authoring.pass_thru("Fuchsia Authors <noreply@google.com>"),
+ transformations = [
+ core.move("", "third_party/fuchsia/repo"),
+ # Apply the patch file created by generate_fuchsia_patch.py.
+ patch.apply(["pigweed_adaptations.patch"]),
+ # Replace test #includes with gtest.
+ core.replace("#include <zxtest/zxtest.h>", "#include \"gtest/gtest.h\""),
+ # Show all commits but exclude the author to reduce line length.
+ metadata.squash_notes(
+ "third_party/fuchsia: Copybara import of the fit library\n\n",
+ show_author = False,
+ ),
+ ],
+)
diff --git a/third_party/fuchsia/docs.rst b/third_party/fuchsia/docs.rst
new file mode 100644
index 000000000..5c59fbc7e
--- /dev/null
+++ b/third_party/fuchsia/docs.rst
@@ -0,0 +1,65 @@
+.. _module-pw_third_party_fuchsia:
+
+=================
+Fuchsia libraries
+=================
+`Fuchsia <https://fuchsia.dev/>`_ is a modern open source operating system
+developed by Google.
+
+Pigweed does not use the Fuchsia operating system itself, but uses some
+low-level libraries developed for it.
+
+--------
+Features
+--------
+Parts of two Fuchsia libraries are used in Pigweed:
+
+- `FIT <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/fit/>`_ --
+ Portable library of low-level C++ features.
+- `stdcompat <https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/lib/stdcompat/>`_ --
+ Implements newer C++ features for older standards.
+
+--------------------
+Code synchronization
+--------------------
+Unlike other third party libraries used by Pigweed, some Fuchsia source code is
+included in tree. A few factors drove this decision:
+
+- Core Pigweed features like :cpp:type:`pw::Function` depend on these Fuchsia
+ libraries. Including the source in-tree avoids having Pigweed require an
+ an external repository.
+- The Fuchsia repository is too large to require downstream projects to clone.
+
+If Fuchsia moves ``stdcompat`` and ``fit`` to separate repositories, the
+decision to include Fuchsia code in tree may be reconsidered.
+
+Files are synced from Fuchsia repository to the ``third_party/fuchsia/repo``
+directory in Pigweed. The files maintain their original paths under that
+directory. The Copybara script applies patches to adapt the sources for use in
+Pigweed. For example, ``__builtin_abort`` is replaced with ``PW_ASSERT``.
+
+Process
+=======
+Code is synchronized between the `Fuchsia repository
+<https://fuchsia.googlesource.com/fuchsia>`_ and the `Pigweed repository
+<https://pigweed.googlesource.com/pigweed/pigweed>`_ using the
+`third_party/fuchsia/copy.bara.sky
+<https://cs.opensource.google/pigweed/pigweed/+/main:third_party/fuchsia/copy.bara.sky>`_
+`Copybara <https://github.com/google/copybara>`_ script.
+
+To synchronize with the Fuchsia repository, run the ``copybara`` tool with the
+script:
+
+.. code-block:: bash
+
+ copybara third_party/fuchsia/copy.bara.sky
+
+That creates a Gerrit change with updates from the Fuchsia repo, if any.
+
+If the ``copybara`` command fails, the Copybara script or patch file may need to
+be updated. Try the following:
+
+- Ensure that the source files in ``copy.bara.sky`` are up-to-date. Fix the list
+ if any files were renamed in Fuchsia.
+- Update the patch file Copybara applies by running ``python
+ third_party/fuchsia/generate_fuchsia_patch.py``.
diff --git a/third_party/fuchsia/generate_fuchsia_patch.py b/third_party/fuchsia/generate_fuchsia_patch.py
new file mode 100755
index 000000000..9025a139e
--- /dev/null
+++ b/third_party/fuchsia/generate_fuchsia_patch.py
@@ -0,0 +1,173 @@
+#!/usr/bin/env python
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Generates a patch file for sources from Fuchsia's fit and stdcompat.
+
+Run this script to update third_party/fuchsia/function.patch.
+"""
+
+from pathlib import Path
+import re
+import subprocess
+import tempfile
+from typing import Iterable, List, TextIO, Optional, Union
+from datetime import datetime
+
+PathOrStr = Union[Path, str]
+
+HEADER = f'''# Copyright {datetime.today().year} The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Patch the fit::function implementation for use in Pigweed:
+#
+# - Use PW_ASSERT instead of __builtin_abort.
+# - Temporarily disable sanitizers when invoking a function for b/241567321.
+#
+'''.encode()
+
+
+def _read_files_list(file: TextIO) -> Iterable[str]:
+ """Reads the files list from the copy.bara.sky file."""
+ found_list = False
+
+ for line in file:
+ if found_list:
+ yield line
+ if line == ']\n':
+ break
+ else:
+ if line == 'fuchsia_repo_files = [\n':
+ found_list = True
+ yield '['
+
+
+def _clone_fuchsia(temp_path: Path) -> Path:
+ subprocess.run(
+ [
+ 'git',
+ '-C',
+ temp_path,
+ 'clone',
+ '--depth',
+ '1',
+ 'https://fuchsia.googlesource.com/fuchsia',
+ ],
+ check=True,
+ )
+
+ return temp_path / 'fuchsia'
+
+
+# TODO(b/248257406): Replace typing.List with list. # pylint: disable=fixme
+def _read_files(script: Path) -> List[Path]:
+ with script.open() as file:
+ paths_list: List[str] = eval( # pylint: disable=eval-used
+ ''.join(_read_files_list(file))
+ )
+ return list(Path(p) for p in paths_list if not 'lib/stdcompat/' in p)
+
+
+def _add_include_before_namespace(text: str, include: str) -> str:
+ return text.replace(
+ '\nnamespace ', f'\n#include "{include}"\n\nnamespace ', 1
+ )
+
+
+_ASSERT = re.compile(r'\bassert\(')
+
+
+def _patch_assert(text: str) -> str:
+ replaced = text.replace('__builtin_abort()', 'PW_ASSERT(false)')
+ replaced = _ASSERT.sub('PW_ASSERT(', replaced)
+
+ if replaced == text:
+ return replaced
+
+ return _add_include_before_namespace(replaced, 'pw_assert/assert.h')
+
+
+_INVOKE_PATCH = (
+ '\n'
+ ' // TODO(b/241567321): Remove "no sanitize" after pw_protobuf is fixed.\n'
+ ' Result invoke(Args... args) const PW_NO_SANITIZE("function") {'
+)
+
+
+def _patch_invoke(file: Path, text: str) -> str:
+ # Update internal/function.h only.
+ if file.name != 'function.h' or file.parent.name != 'internal':
+ return text
+
+ text = _add_include_before_namespace(text, 'pw_preprocessor/compiler.h')
+ return text.replace(
+ '\n Result invoke(Args... args) const {', _INVOKE_PATCH
+ )
+
+
+def _patch(file: Path) -> Optional[str]:
+ text = file.read_text()
+ updated = _patch_assert(text)
+ updated = _patch_invoke(file, updated)
+ return None if text == updated else updated
+
+
+def _main() -> None:
+ output_path = Path(__file__).parent
+
+ # Clone Fuchsia to a temp directory
+ with tempfile.TemporaryDirectory() as directory:
+ repo = _clone_fuchsia(Path(directory))
+
+ # Read the files list from copy.bara.sky and patch those files.
+ paths = _read_files(output_path / 'copy.bara.sky')
+ for file in (repo / path for path in paths):
+ if (text := _patch(file)) is not None:
+ print('Patching', file)
+ file.write_text(text)
+ subprocess.run(['clang-format', '-i', file], check=True)
+
+ # Create a diff for the changes.
+ diff = subprocess.run(
+ ['git', '-C', repo, 'diff'], stdout=subprocess.PIPE, check=True
+ ).stdout
+ for path in paths:
+ diff = diff.replace(
+ path.as_posix().encode(),
+ Path('third_party/fuchsia/repo', path).as_posix().encode(),
+ )
+
+ # Write the diff to function.patch.
+ with output_path.joinpath('pigweed_adaptations.patch').open('wb') as output:
+ output.write(HEADER)
+
+ for line in diff.splitlines(keepends=True):
+ if line == b' \n':
+ output.write(b'\n')
+ elif not line.startswith(b'index '): # drop line with Git hashes
+ output.write(line)
+
+
+if __name__ == '__main__':
+ _main()
diff --git a/third_party/fuchsia/pigweed_adaptations.patch b/third_party/fuchsia/pigweed_adaptations.patch
new file mode 100644
index 000000000..4f7ea5b61
--- /dev/null
+++ b/third_party/fuchsia/pigweed_adaptations.patch
@@ -0,0 +1,276 @@
+# Copyright 2023 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+# Patch the fit::function implementation for use in Pigweed:
+#
+# - Use PW_ASSERT instead of __builtin_abort.
+# - Temporarily disable sanitizers when invoking a function for b/241567321.
+#
+diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h
+--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h
++++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h
+@@ -18,6 +18,8 @@
+ #include <utility>
+
+ #include "../nullable.h"
++#include "pw_assert/assert.h"
++#include "pw_preprocessor/compiler.h"
+
+ namespace fit {
+ namespace internal {
+@@ -87,7 +89,7 @@ inline const void* unshared_target_type_id(void* /*bits*/, const void* impl_ops)
+ // elsewhere in the header as an inline variable.
+ template <typename Unused = void>
+ struct null_target {
+- static void invoke(void* /*bits*/) { __builtin_abort(); }
++ static void invoke(void* /*bits*/) { PW_ASSERT(false); }
+
+ static const target_ops<void> ops;
+
+@@ -493,7 +495,8 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
+ // Note that fit::callback will release the target immediately after
+ // invoke() (also affecting any share()d copies).
+ // Aborts if the function's target is empty.
+- Result invoke(Args... args) const {
++ // TODO(b/241567321): Remove "no sanitize" after pw_protobuf is fixed.
++ Result invoke(Args... args) const PW_NO_SANITIZE("function") {
+ // Down cast the ops to the derived type that this function was instantiated
+ // with, which includes the invoke function.
+ //
+@@ -523,7 +526,7 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
+ template <typename SharedFunction>
+ void copy_shared_target_to(SharedFunction& copy) {
+ copy.destroy_target();
+- assert(base::ops() == &shared_target_type<SharedFunction>::ops);
++ PW_ASSERT(base::ops() == &shared_target_type<SharedFunction>::ops);
+ shared_target_type<SharedFunction>::copy_shared_ptr(base::bits(), copy.bits());
+ copy.set_ops(base::ops());
+ }
+@@ -553,7 +556,7 @@ class function_base<inline_target_size, require_inline, Result(Args...)>
+ void check_target_type() const {
+ if (target_type<Callable>::ops.target_type_id(nullptr, &target_type<Callable>::ops) !=
+ base::target_type_id()) {
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ }
+ };
+diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h
+--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h
++++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h
+@@ -11,6 +11,8 @@
+ #include <type_traits>
+ #include <utility>
+
++#include "pw_assert/assert.h"
++
+ namespace fit {
+
+ // Determines whether a type can be compared with nullptr.
+@@ -130,28 +132,28 @@ class nullable<T, true> final {
+ if (has_value()) {
+ return value_;
+ } else {
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ }
+ constexpr const T& value() const& {
+ if (has_value()) {
+ return value_;
+ } else {
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ }
+ constexpr T&& value() && {
+ if (has_value()) {
+ return std::move(value_);
+ } else {
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ }
+ constexpr const T&& value() const&& {
+ if (has_value()) {
+ return std::move(value_);
+ } else {
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ }
+
+diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
+--- a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
++++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
+@@ -55,6 +55,8 @@
+ // // fit::result with a different "success" vluae type (or
+ // // fit::result<E>).
+
++#include "pw_assert/assert.h"
++
+ namespace fit {
+
+ // Convenience type to indicate failure without elaboration.
+@@ -280,25 +282,25 @@ class LIB_FIT_NODISCARD result<E, T> {
+ if (is_error()) {
+ return storage_.error_or_value.error;
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr const E& error_value() const& {
+ if (is_error()) {
+ return storage_.error_or_value.error;
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr E&& error_value() && {
+ if (is_error()) {
+ return std::move(storage_.error_or_value.error);
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr const E&& error_value() const&& {
+ if (is_error()) {
+ return std::move(storage_.error_or_value.error);
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Moves the underlying error and returns it as an instance of fit::error, simplifying
+@@ -309,7 +311,7 @@ class LIB_FIT_NODISCARD result<E, T> {
+ if (is_error()) {
+ return error<E>(std::move(storage_.error_or_value.error));
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Accessors for the underlying value.
+@@ -319,25 +321,25 @@ class LIB_FIT_NODISCARD result<E, T> {
+ if (is_ok()) {
+ return storage_.error_or_value.value;
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr const T& value() const& {
+ if (is_ok()) {
+ return storage_.error_or_value.value;
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr T&& value() && {
+ if (is_ok()) {
+ return std::move(storage_.error_or_value.value);
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr const T&& value() const&& {
+ if (is_ok()) {
+ return std::move(storage_.error_or_value.value);
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Moves the underlying value and returns it as an instance of fit::success, simplifying
+@@ -348,7 +350,7 @@ class LIB_FIT_NODISCARD result<E, T> {
+ if (is_ok()) {
+ return success<T>(std::move(storage_.error_or_value.value));
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Contingent accessors for the underlying value.
+@@ -377,13 +379,13 @@ class LIB_FIT_NODISCARD result<E, T> {
+ if (is_ok()) {
+ return ::fit::internal::arrow_operator<T>::forward(storage_.error_or_value.value);
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr decltype(auto) operator->() const {
+ if (is_ok()) {
+ return ::fit::internal::arrow_operator<T>::forward(storage_.error_or_value.value);
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Accessors for the underlying value. This is a syntax sugar for value().
+@@ -406,7 +408,7 @@ class LIB_FIT_NODISCARD result<E, T> {
+ storage_.error_or_value.error += std::move(error.value_);
+ return *this;
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Maps a result<E, T> to a result<E2, T> by transforming the error through
+@@ -517,25 +519,25 @@ class LIB_FIT_NODISCARD result<E> {
+ if (is_error()) {
+ return storage_.error_or_value.error;
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr const E& error_value() const& {
+ if (is_error()) {
+ return storage_.error_or_value.error;
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr E&& error_value() && {
+ if (is_error()) {
+ return std::move(storage_.error_or_value.error);
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+ constexpr const E&& error_value() const&& {
+ if (is_error()) {
+ return std::move(storage_.error_or_value.error);
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Moves the underlying error and returns it as an instance of fit::error, simplifying
+@@ -546,7 +548,7 @@ class LIB_FIT_NODISCARD result<E> {
+ if (is_error()) {
+ return error<E>(std::move(storage_.error_or_value.error));
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Augments the error value of the result with the given value. The operator E::operator+=(F) must
+@@ -560,7 +562,7 @@ class LIB_FIT_NODISCARD result<E> {
+ storage_.error_or_value.error += std::move(error.value_);
+ return *this;
+ }
+- __builtin_abort();
++ PW_ASSERT(false);
+ }
+
+ // Maps a result<E, T> to a result<E2, T> by transforming the error through
diff --git a/third_party/fuchsia/repo/.clang-format b/third_party/fuchsia/repo/.clang-format
new file mode 100644
index 000000000..58381b3fd
--- /dev/null
+++ b/third_party/fuchsia/repo/.clang-format
@@ -0,0 +1,33 @@
+# http://clang.llvm.org/docs/ClangFormatStyleOptions.html
+BasedOnStyle: Google
+# This defaults to 'Auto'. Explicitly set it for a while, so that
+# 'vector<vector<int> >' in existing files gets formatted to
+# 'vector<vector<int>>'. ('Auto' means that clang-format will only use
+# 'int>>' if the file already contains at least one such instance.)
+Standard: Cpp11
+SortIncludes: true
+AllowShortIfStatementsOnASingleLine: false
+AllowShortLoopsOnASingleLine: false
+BreakStringLiterals: false
+DerivePointerAlignment: true
+PointerAlignment: Left
+ColumnLimit: 100
+ForEachMacros: ['list_for_every_entry','list_for_every_entry_safe']
+IncludeBlocks: Regroup
+IncludeCategories:
+ # This specific header must come last in kernel source files. Its
+ # matching rule must come first so the lower-priority rules don't match.
+ - Regex: '^<ktl/enforce\.h>'
+ Priority: 1000
+ # C Header: <foo.h>, <net/foo.h>, etc
+ - Regex: '^(<((zircon/|lib/|fuchsia/|arpa/|net/|netinet/|sys/|fidl/)[a-zA-Z0-9_/\.-]+\.h|[a-zA-Z0-9_-]+\.h)>)'
+ Priority: 1
+ # Cpp Header: <foo> and <experimental/foo>
+ - Regex: '^(<(experimental/)*[a-zA-Z0-9_-]+>)'
+ Priority: 2
+ # Libraries: <foo/bar.h>
+ - Regex: '^(<[a-zA-Z0-9_/-]+\.h>)'
+ Priority: 3
+ # Local headers: "foo/bar.h"
+ - Regex: '^("[.a-zA-Z0-9_/-]+\.h")'
+ Priority: 4
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h
new file mode 100644
index 000000000..fba3e95c2
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/function.h
@@ -0,0 +1,540 @@
+// Copyright 2017 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_INCLUDE_LIB_FIT_FUNCTION_H_
+#define LIB_FIT_INCLUDE_LIB_FIT_FUNCTION_H_
+
+#include <type_traits>
+
+#include "internal/function.h"
+#include "internal/utility.h"
+#include "traits.h"
+
+namespace fit {
+
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+class function_impl {
+ static_assert(std::is_function<FunctionType>::value,
+ "fit::function must be instantiated with a function type, such as void() or "
+ "int(char*, bool)");
+};
+
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+class callback_impl {
+ static_assert(std::is_function<FunctionType>::value,
+ "fit::callback must be instantiated with a function type, such as void() or "
+ "int(char*, bool)");
+};
+
+// The default size allowance for storing a target inline within a function
+// object, in bytes. This default allows for inline storage of targets
+// as big as two pointers, such as an object pointer and a pointer to a member
+// function.
+constexpr size_t default_inline_target_size = sizeof(void*) * 2;
+
+// A |fit::function| is a move-only polymorphic function wrapper.
+//
+// If you need a class with similar characteristics that also ensures
+// "run-once" semantics (such as callbacks shared with timeouts, or for
+// service requests with redundant, failover, or fallback service providers),
+// see |fit::callback|.
+//
+// |fit::function<T>| behaves like |std::function<T>| except that it is
+// move-only instead of copyable, so it can hold targets that cannot be copied,
+// such as mutable lambdas, and immutable lambdas that capture move-only
+// objects.
+//
+// Targets of up to |inline_target_size| bytes in size are stored inline within
+// the function object without incurring any heap allocation. Larger callable
+// objects will be moved to the heap as required. |inline_target_size| is
+// rounded up to a multiple of sizeof(void*).
+//
+// See also |fit::inline_function<T, size>| for more control over allocation
+// behavior.
+//
+// SYNOPSIS
+//
+// |T| is the function's signature. e.g. void(int, std::string).
+//
+// |inline_target_size| is the minimum size of target that is guaranteed to
+// fit within a function without requiring heap allocation.
+// Defaults to |default_inline_target_size|.
+//
+// Class members are documented in |fit::function_impl|, below.
+//
+// EXAMPLES
+//
+// -
+// https://fuchsia.googlesource.com/fuchsia/+/HEAD/sdk/lib/fit/test/examples/function_example1.cc
+// -
+// https://fuchsia.googlesource.com/fuchsia/+/HEAD/sdk/lib/fit/test/examples/function_example2.cc
+//
+template <typename T, size_t inline_target_size = default_inline_target_size>
+using function = function_impl<internal::RoundUpToWord(inline_target_size),
+ /*require_inline=*/false, T>;
+
+// A move-only callable object wrapper that forces callables to be stored inline
+// and never performs heap allocation.
+//
+// Behaves just like |fit::function<T, inline_target_size>| except that
+// attempting to store a target larger than |inline_target_size| will fail to
+// compile.
+template <typename T, size_t inline_target_size = default_inline_target_size>
+using inline_function = function_impl<internal::RoundUpToWord(inline_target_size),
+ /*require_inline=*/true, T>;
+
+// Synonym for a function which takes no arguments and produces no result.
+using closure = function<void()>;
+
+// A |fit::callback| is a move-only polymorphic function wrapper that also
+// ensures "run-once" semantics (such as callbacks shared with timeouts, or for
+// service requests with redundant, failover, or fallback service providers).
+// A |fit::callback| releases it's resources after the first call, and can be
+// inspected before calling, so a potential caller can know if it should call
+// the function, or skip the call because the target was already called.
+//
+// If you need a move-only function class with typical function characteristics,
+// that permits multiple invocations of the same function, see |fit::function|.
+//
+// |fit::callback<T>| behaves like |std::function<T>| except:
+//
+// 1. It is move-only instead of copyable, so it can hold targets that cannot
+// be copied, such as mutable lambdas, and immutable lambdas that capture
+// move-only objects.
+// 2. On the first call to invoke a |fit::callback|, the target function held
+// by the |fit::callback| cannot be called again.
+//
+// When a |fit::callback| is invoked for the first time, the target function is
+// released and destructed, along with any resources owned by that function
+// (typically the objects captured by a lambda).
+//
+// A |fit::callback| in the "already called" state has the same state as a
+// |fit::callback| that has been assigned to |nullptr|. It can be compared to
+// |nullptr| (via "==" or "!=", and its "operator bool()" returns false, which
+// provides a convenient way to gate whether or not the |fit::callback| should
+// be called. (Note that invoking an empty |fit::callback| or |fit::function|
+// will cause a program abort!)
+//
+// As an example, sharing |fit::callback| between both a service and a timeout
+// might look something like this:
+//
+// void service_with_timeout(fit::callback<void(bool)> cb, uint timeout_ms) {
+// service_request([cb = cb.share()]() mutable { if (cb) cb(false); });
+// timeout(timeout_ms, [cb = std::move(cb)]() mutable { if (cb) cb(true); });
+// }
+//
+// Since |fit::callback| objects are move-only, and not copyable, duplicate
+// references to the same |fit::callback| can be obtained via share(), as shown
+// in the example above. This method converts the |fit::callback| into a
+// reference-counted version of the |fit::callback| and returns a copy of the
+// reference as another |fit::callback| with the same target function.
+//
+// What is notable about |fit::callback<T>.share()| is that invoking any shared
+// copy will "nullify" all shared copies, as shown in the example.
+//
+// Note that |fit::callback| is NOT thread-safe by default. If multi-threaded
+// support is required, you would need to implement your own mutex, or similar
+// guard, before checking and calling a |fit::callback|.
+//
+// Targets of up to |inline_target_size| bytes in size are stored inline within
+// the callback object without incurring any heap allocation. Larger callable
+// objects will be moved to the heap as required. |inline_target_size| is
+// rounded up to a multiple of sizeof(void*).
+//
+// See also |fit::inline_callback<T, size>| for more control over allocation
+// behavior.
+//
+// SYNOPSIS
+//
+// |T| is the callback's signature. e.g. void(int, std::string).
+//
+// |inline_target_size| is the minimum size of target that is guaranteed to
+// fit within a callback without requiring heap allocation.
+// Defaults to |default_inline_target_size|.
+//
+// Class members are documented in |fit::callback_impl|, below.
+//
+template <typename T, size_t inline_target_size = default_inline_target_size>
+using callback =
+ callback_impl<internal::RoundUpToWord(inline_target_size), /*require_inline=*/false, T>;
+
+// A move-only, run-once, callable object wrapper that forces callables to be
+// stored inline and never performs heap allocation.
+//
+// Behaves just like |fit::callback<T, inline_target_size>| except that
+// attempting to store a target larger than |inline_target_size| will fail to
+// compile.
+template <typename T, size_t inline_target_size = default_inline_target_size>
+using inline_callback = callback_impl<internal::RoundUpToWord(inline_target_size),
+ /*require_inline=*/true, T>;
+
+template <size_t inline_target_size, bool require_inline, typename Result, typename... Args>
+class function_impl<inline_target_size, require_inline, Result(Args...)> final
+ : private ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)> {
+ using base = ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)>;
+
+ // function_base requires private access during share()
+ friend class ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)>;
+
+ // supports target() for shared functions
+ friend const void* ::fit::internal::get_target_type_id<>(
+ const function_impl<inline_target_size, require_inline, Result(Args...)>&);
+
+ template <typename U>
+ using not_self_type = ::fit::internal::not_same_type<function_impl, U>;
+
+ template <typename... Conditions>
+ using requires_conditions = ::fit::internal::requires_conditions<Conditions...>;
+
+ template <typename... Conditions>
+ using assignment_requires_conditions =
+ ::fit::internal::assignment_requires_conditions<function_impl&, Conditions...>;
+
+ public:
+ // The function's result type.
+ using typename base::result_type;
+
+ // Initializes an empty (null) function. Attempting to call an empty
+ // function will abort the program.
+ constexpr function_impl() = default;
+
+ // Creates a function with an empty target (same outcome as the default
+ // constructor).
+ constexpr function_impl(decltype(nullptr)) : base(nullptr) {}
+
+ // Creates a function bound to the specified function pointer.
+ // If target == nullptr, assigns an empty target.
+ function_impl(Result (*function_target)(Args...)) : base(function_target) {}
+
+ // Creates a function bound to the specified callable object.
+ // If target == nullptr, assigns an empty target.
+ //
+ // For functors, we need to capture the raw type but also restrict on the
+ // existence of an appropriate operator () to resolve overloads and implicit
+ // casts properly.
+ //
+ // Note that specializations of this template method that take fit::callback
+ // objects as the target Callable are deleted (see below).
+ template <typename Callable,
+ requires_conditions<
+ std::is_convertible<decltype(std::declval<Callable&>()(std::declval<Args>()...)),
+ result_type>,
+ not_self_type<Callable>> = true>
+ function_impl(Callable&& function_target) : base(std::forward<Callable>(function_target)) {}
+
+ // Deletes the specializations of function_impl(Callable) that would allow
+ // a |fit::function| to be constructed from a |fit::callback|. This prevents
+ // unexpected behavior of a |fit::function| that would otherwise fail after
+ // one call. To explicitly allow this, simply wrap the |fit::callback| in a
+ // pass-through lambda before passing it to the |fit::function|.
+ template <size_t other_inline_target_size, bool other_require_inline>
+ function_impl(
+ ::fit::callback_impl<other_inline_target_size, other_require_inline, Result(Args...)>) =
+ delete;
+
+ // Creates a function with a target moved from another function,
+ // leaving the other function with an empty target.
+ function_impl(function_impl&& other) noexcept : base(static_cast<base&&>(other)) {}
+
+ // Destroys the function, releasing its target.
+ ~function_impl() = default;
+
+ // Assigns the function to an empty target. Attempting to invoke the
+ // function will abort the program.
+ function_impl& operator=(decltype(nullptr)) {
+ base::assign_null();
+ return *this;
+ }
+
+ // Assigns the function to the specified callable object. If target ==
+ // nullptr, assigns an empty target.
+ //
+ // For functors, we need to capture the raw type but also restrict on the
+ // existence of an appropriate operator () to resolve overloads and implicit
+ // casts properly.
+ //
+ // Note that specializations of this template method that take fit::callback
+ // objects as the target Callable are deleted (see below).
+ template <typename Callable>
+ // NOLINTNEXTLINE(misc-unconventional-assign-operator)
+ assignment_requires_conditions<
+ std::is_convertible<decltype(std::declval<Callable&>()(std::declval<Args>()...)),
+ result_type>,
+ not_self_type<Callable>>
+ operator=(Callable&& function_target) {
+ base::assign_callable(std::forward<Callable>(function_target));
+ return *this;
+ }
+
+ // Deletes the specializations of operator=(Callable) that would allow
+ // a |fit::function| to be assigned from a |fit::callback|. This
+ // prevents unexpected behavior of a |fit::function| that would otherwise
+ // fail after one call. To explicitly allow this, simply wrap the
+ // |fit::callback| in a pass-through lambda before assigning it to the
+ // |fit::function|.
+ template <size_t other_inline_target_size, bool other_require_inline>
+ function_impl& operator=(
+ ::fit::callback_impl<other_inline_target_size, other_require_inline, Result(Args...)>) =
+ delete;
+
+ // Move assignment
+ function_impl& operator=(function_impl&& other) noexcept {
+ if (&other == this)
+ return *this;
+ base::assign_function(static_cast<base&&>(other));
+ return *this;
+ }
+
+ // Swaps the functions' targets.
+ void swap(function_impl& other) { base::swap(other); }
+
+ // Returns a pointer to the function's target.
+ using base::target;
+
+ // Returns true if the function has a non-empty target.
+ using base::operator bool;
+
+ // Invokes the function's target.
+ // Aborts if the function's target is empty.
+ Result operator()(Args... args) const { return base::invoke(std::forward<Args>(args)...); }
+
+ // Returns a new function object that invokes the same target.
+ // The target itself is not copied; it is moved to the heap and its
+ // lifetime is extended until all references have been released.
+ //
+ // Note: This method is not supported on |fit::inline_function<>|
+ // because it may incur a heap allocation which is contrary to
+ // the stated purpose of |fit::inline_function<>|.
+ function_impl share() {
+ function_impl copy;
+ base::template share_with<function_impl>(copy);
+ return copy;
+ }
+};
+
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+void swap(function_impl<inline_target_size, require_inline, FunctionType>& a,
+ function_impl<inline_target_size, require_inline, FunctionType>& b) {
+ a.swap(b);
+}
+
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+bool operator==(const function_impl<inline_target_size, require_inline, FunctionType>& f,
+ decltype(nullptr)) {
+ return !f;
+}
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+bool operator==(decltype(nullptr),
+ const function_impl<inline_target_size, require_inline, FunctionType>& f) {
+ return !f;
+}
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+bool operator!=(const function_impl<inline_target_size, require_inline, FunctionType>& f,
+ decltype(nullptr)) {
+ return !!f;
+}
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+bool operator!=(decltype(nullptr),
+ const function_impl<inline_target_size, require_inline, FunctionType>& f) {
+ return !!f;
+}
+
+template <size_t inline_target_size, bool require_inline, typename Result, typename... Args>
+class callback_impl<inline_target_size, require_inline, Result(Args...)> final
+ : private ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)> {
+ using base = ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)>;
+
+ // function_base requires private access during share()
+ friend class ::fit::internal::function_base<inline_target_size, require_inline, Result(Args...)>;
+
+ // supports target() for shared functions
+ friend const void* ::fit::internal::get_target_type_id<>(
+ const callback_impl<inline_target_size, require_inline, Result(Args...)>&);
+
+ template <typename U>
+ using not_self_type = ::fit::internal::not_same_type<callback_impl, U>;
+
+ template <typename... Conditions>
+ using requires_conditions = ::fit::internal::requires_conditions<Conditions...>;
+
+ template <typename... Conditions>
+ using assignment_requires_conditions =
+ ::fit::internal::assignment_requires_conditions<callback_impl&, Conditions...>;
+
+ public:
+ // The callback function's result type.
+ using typename base::result_type;
+
+ // Initializes an empty (null) callback. Attempting to call an empty
+ // callback will abort the program.
+ constexpr callback_impl() = default;
+
+ // Creates a callback with an empty target (same outcome as the default
+ // constructor).
+ constexpr callback_impl(decltype(nullptr)) : base(nullptr) {}
+
+ // Creates a callback bound to the specified function pointer.
+ // If target == nullptr, assigns an empty target.
+ callback_impl(Result (*callback_target)(Args...)) : base(callback_target) {}
+
+ // Creates a callback bound to the specified callable object.
+ // If target == nullptr, assigns an empty target.
+ //
+ // For functors, we need to capture the raw type but also restrict on the
+ // existence of an appropriate operator () to resolve overloads and implicit
+ // casts properly.
+ template <typename Callable,
+ requires_conditions<
+ std::is_convertible<decltype(std::declval<Callable&>()(std::declval<Args>()...)),
+ result_type>,
+ not_self_type<Callable>> = true>
+ callback_impl(Callable&& callback_target) : base(std::forward<Callable>(callback_target)) {}
+
+ // Creates a callback with a target moved from another callback,
+ // leaving the other callback with an empty target.
+ callback_impl(callback_impl&& other) noexcept : base(static_cast<base&&>(other)) {}
+
+ // Destroys the callback, releasing its target.
+ ~callback_impl() = default;
+
+ // Assigns the callback to an empty target. Attempting to invoke the
+ // callback will abort the program.
+ callback_impl& operator=(decltype(nullptr)) {
+ base::assign_null();
+ return *this;
+ }
+
+ // Assigns the callback to the specified callable object. If target ==
+ // nullptr, assigns an empty target.
+ //
+ // For functors, we need to capture the raw type but also restrict on the
+ // existence of an appropriate operator () to resolve overloads and implicit
+ // casts properly.
+ template <typename Callable>
+ // NOLINTNEXTLINE(misc-unconventional-assign-operator)
+ assignment_requires_conditions<
+ std::is_convertible<decltype(std::declval<Callable&>()(std::declval<Args>()...)),
+ result_type>,
+ not_self_type<Callable>>
+ operator=(Callable&& callback_target) {
+ base::assign_callable(std::forward<Callable>(callback_target));
+ return *this;
+ }
+
+ // Move assignment
+ callback_impl& operator=(callback_impl&& other) noexcept {
+ if (&other == this)
+ return *this;
+ base::assign_function(static_cast<base&&>(other));
+ return *this;
+ }
+
+ // Swaps the callbacks' targets.
+ void swap(callback_impl& other) { base::swap(other); }
+
+ // Returns a pointer to the callback's target.
+ using base::target;
+
+ // Returns true if the callback has a non-empty target.
+ using base::operator bool;
+
+ // Invokes the callback's target.
+ // Aborts if the callback's target is empty.
+ // |fit::callback| must be non-const to invoke. Before the target function
+ // is actually called, the fit::callback will be set to the default empty
+ // state (== nullptr, and operator bool() will subsequently return |false|).
+ // The target function will then be released after the function is called.
+ // If the callback was shared, any remaining copies will also be cleared.
+ Result operator()(Args... args) {
+ auto temp = std::move(*this);
+ return temp.invoke(std::forward<Args>(args)...);
+ }
+
+ // Returns a new callback object that invokes the same target.
+ // The target itself is not copied; it is moved to the heap and its
+ // lifetime is extended until all references have been released.
+ // For |fit::callback| (unlike fit::function), the first invocation of the
+ // callback will release all references to the target. All callbacks
+ // derived from the same original callback (via share()) will be cleared,
+ // as if set to |nullptr|, and "operator bool()" will return false.
+ //
+ // Note: This method is not supported on |fit::inline_function<>|
+ // because it may incur a heap allocation which is contrary to
+ // the stated purpose of |fit::inline_function<>|.
+ callback_impl share() {
+ callback_impl copy;
+ base::template share_with<callback_impl>(copy);
+ return copy;
+ }
+};
+
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+void swap(callback_impl<inline_target_size, require_inline, FunctionType>& a,
+ callback_impl<inline_target_size, require_inline, FunctionType>& b) {
+ a.swap(b);
+}
+
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+bool operator==(const callback_impl<inline_target_size, require_inline, FunctionType>& f,
+ decltype(nullptr)) {
+ return !f;
+}
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+bool operator==(decltype(nullptr),
+ const callback_impl<inline_target_size, require_inline, FunctionType>& f) {
+ return !f;
+}
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+bool operator!=(const callback_impl<inline_target_size, require_inline, FunctionType>& f,
+ decltype(nullptr)) {
+ return !!f;
+}
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+bool operator!=(decltype(nullptr),
+ const callback_impl<inline_target_size, require_inline, FunctionType>& f) {
+ return !!f;
+}
+
+// Returns a Callable object that invokes a member function of an object.
+// When used in a fit::function, this heap allocates (the returned lambda is
+// 3*sizeof(void*)).
+//
+// Deprecated in favor of the bind_member definition below that will inline into a
+// fit::function without heap allocating. The new bind_member definition is only
+// supported on C++17 and up. On C++14, a plain lambda should be used instead.
+template <typename R, typename T, typename... Args>
+auto bind_member(T* instance, R (T::*fn)(Args...)) {
+ // Use explicit type on the return to ensure perfect forwarding of references.
+ return [instance, fn](Args... args) -> R { return (instance->*fn)(std::forward<Args>(args)...); };
+}
+
+// C++17 due to use of 'auto' template parameters and lambda parameters.
+#if __cplusplus >= 201703L
+namespace internal {
+// Performs the call for bind_member but captures the arguments of the method.
+// This ensure that the correct overload of |method| is called.
+template <auto method, typename T, typename... Args>
+auto make_the_call(T* instance, parameter_pack<Args...>) {
+ // Use decltype(auto) on the return to ensure perfect forwarding of references.
+ return [instance](Args... args) -> decltype(auto) {
+ return (instance->*method)(std::forward<decltype(args)>(args)...);
+ };
+}
+} // namespace internal
+
+// Returns a Callable object that invokes a member function of an object.
+// In other words, returns a closure 'f' for which calling f(args) is equivalent to
+// calling obj.method(args).
+//
+// Usage: fit::bind_member<&ObjType::MethodName>(&obj)
+template <auto method, typename T>
+auto bind_member(T* instance) {
+ return internal::make_the_call<method>(instance,
+ typename callable_traits<decltype(method)>::args{});
+}
+#endif // __cplusplus >= 201703L
+
+} // namespace fit
+
+#endif // LIB_FIT_INCLUDE_LIB_FIT_FUNCTION_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h
new file mode 100644
index 000000000..1bd806414
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/compiler.h
@@ -0,0 +1,21 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_COMPILER_H_
+#define LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_COMPILER_H_
+
+// Annotate a class or function with C++17's [[nodiscard]] or similar where supported by the
+// compiler.
+//
+// C++14 doesn't support [[nodiscard]], but Clang allows __attribute__((warn_unused_result))
+// to be placed on class declarations. GCC only allows the attribute to be used on methods.
+#if __cplusplus >= 201703L
+#define LIB_FIT_NODISCARD [[nodiscard]]
+#elif defined(__clang__)
+#define LIB_FIT_NODISCARD __attribute__((__warn_unused_result__))
+#else
+#define LIB_FIT_NODISCARD /* nothing */
+#endif
+
+#endif // LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_COMPILER_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h
new file mode 100644
index 000000000..dd655f3d0
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/function.h
@@ -0,0 +1,567 @@
+// Copyright 2017 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_FUNCTION_H_
+#define LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_FUNCTION_H_
+
+#include <lib/stdcompat/bit.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#include <algorithm>
+#include <cstring>
+#include <functional>
+#include <memory>
+#include <new>
+#include <type_traits>
+#include <utility>
+
+#include "../nullable.h"
+#include "pw_assert/assert.h"
+#include "pw_preprocessor/compiler.h"
+
+namespace fit {
+namespace internal {
+
+// Rounds the first argument up to a non-zero multiple of the second argument.
+constexpr size_t RoundUpToMultiple(size_t value, size_t multiple) {
+ return value == 0 ? multiple : (value + multiple - 1) / multiple * multiple;
+}
+
+// Rounds up to the nearest word. To avoid unnecessary instantiations, function_base can only be
+// instantiated with an inline size that is a non-zero multiple of the word size.
+constexpr size_t RoundUpToWord(size_t value) { return RoundUpToMultiple(value, sizeof(void*)); }
+
+// target_ops is the vtable for the function_base class. The base_target_ops struct holds functions
+// that are common to all function_base instantiations, regardless of the function's signature.
+// The derived target_ops template that adds the signature-specific invoke method.
+//
+// Splitting the common functions into base_target_ops allows all function_base instantiations to
+// share the same vtable for their null function instantiation, reducing code size.
+struct base_target_ops {
+ const void* (*target_type_id)(void* bits, const void* impl_ops);
+ void* (*get)(void* bits);
+ void (*move)(void* from_bits, void* to_bits);
+ void (*destroy)(void* bits);
+
+ protected:
+ // Aggregate initialization isn't supported with inheritance until C++17, so define a constructor.
+ constexpr base_target_ops(decltype(target_type_id) target_type_id_func, decltype(get) get_func,
+ decltype(move) move_func, decltype(destroy) destroy_func)
+ : target_type_id(target_type_id_func),
+ get(get_func),
+ move(move_func),
+ destroy(destroy_func) {}
+};
+
+template <typename Result, typename... Args>
+struct target_ops final : public base_target_ops {
+ Result (*invoke)(void* bits, Args... args);
+
+ constexpr target_ops(decltype(target_type_id) target_type_id_func, decltype(get) get_func,
+ decltype(move) move_func, decltype(destroy) destroy_func,
+ decltype(invoke) invoke_func)
+ : base_target_ops(target_type_id_func, get_func, move_func, destroy_func),
+ invoke(invoke_func) {}
+};
+
+static_assert(sizeof(target_ops<void>) == sizeof(void (*)()) * 5, "Unexpected target_ops padding");
+
+template <typename Callable, bool is_inline, bool is_shared, typename Result, typename... Args>
+struct target;
+
+inline void trivial_target_destroy(void* /*bits*/) {}
+
+inline const void* unshared_target_type_id(void* /*bits*/, const void* impl_ops) {
+ return impl_ops;
+}
+
+// vtable for nullptr (empty target function)
+
+// All function_base instantiations, regardless of callable type, use the same
+// vtable for nullptr functions. This avoids generating unnecessary identical
+// vtables, which reduces code size.
+//
+// The null_target class does not need to be a template. However, if it was not
+// a template, the ops variable would need to be defined in a .cc file for C++14
+// compatibility. In C++17, null_target::ops could be defined in the class or
+// elsewhere in the header as an inline variable.
+template <typename Unused = void>
+struct null_target {
+ static void invoke(void* /*bits*/) { PW_ASSERT(false); }
+
+ static const target_ops<void> ops;
+
+ static_assert(std::is_same<Unused, void>::value, "Only instantiate null_target with void");
+};
+
+template <typename Result, typename... Args>
+struct target<decltype(nullptr), /*is_inline=*/true, /*is_shared=*/false, Result, Args...> final
+ : public null_target<> {};
+
+inline void* null_target_get(void* /*bits*/) { return nullptr; }
+inline void null_target_move(void* /*from_bits*/, void* /*to_bits*/) {}
+
+template <typename Unused>
+constexpr target_ops<void> null_target<Unused>::ops = {&unshared_target_type_id, &null_target_get,
+ &null_target_move, &trivial_target_destroy,
+ &null_target::invoke};
+
+// vtable for inline target function
+
+// Trivially movable and destructible types can be moved with a simple memcpy. Use the same function
+// for all callable types of a particular size to reduce code size.
+template <size_t size_bytes>
+inline void inline_trivial_target_move(void* from_bits, void* to_bits) {
+ std::memcpy(to_bits, from_bits, size_bytes);
+}
+
+template <typename Callable, typename Result, typename... Args>
+struct target<Callable,
+ /*is_inline=*/true, /*is_shared=*/false, Result, Args...>
+ final {
+ template <typename Callable_>
+ static void initialize(void* bits, Callable_&& target) {
+ new (bits) Callable(std::forward<Callable_>(target));
+ }
+ static Result invoke(void* bits, Args... args) {
+ auto& target = *static_cast<Callable*>(bits);
+ return target(std::forward<Args>(args)...);
+ }
+ // Selects which move function to use. Trivially movable and destructible types of a particular
+ // size share a single move function.
+ static constexpr auto get_move_function() {
+ if (std::is_trivially_move_constructible<Callable>::value &&
+ std::is_trivially_destructible<Callable>::value) {
+ return &inline_trivial_target_move<sizeof(Callable)>;
+ }
+ return &move;
+ }
+ // Selects which destroy function to use. Trivially destructible types share a single, empty
+ // destroy function.
+ static constexpr auto get_destroy_function() {
+ return std::is_trivially_destructible<Callable>::value ? &trivial_target_destroy : &destroy;
+ }
+
+ static const target_ops<Result, Args...> ops;
+
+ private:
+ static void move(void* from_bits, void* to_bits) {
+ auto& from_target = *static_cast<Callable*>(from_bits);
+ new (to_bits) Callable(std::move(from_target));
+ from_target.~Callable(); // NOLINT(bugprone-use-after-move)
+ }
+ static void destroy(void* bits) {
+ auto& target = *static_cast<Callable*>(bits);
+ target.~Callable();
+ }
+};
+
+inline void* inline_target_get(void* bits) { return bits; }
+
+template <typename Callable, typename Result, typename... Args>
+constexpr target_ops<Result, Args...> target<Callable,
+ /*is_inline=*/true,
+ /*is_shared=*/false, Result, Args...>::ops = {
+ &unshared_target_type_id, &inline_target_get, target::get_move_function(),
+ target::get_destroy_function(), &target::invoke};
+
+// vtable for pointer to target function
+
+template <typename Callable, typename Result, typename... Args>
+struct target<Callable,
+ /*is_inline=*/false, /*is_shared=*/false, Result, Args...>
+ final {
+ template <typename Callable_>
+ static void initialize(void* bits, Callable_&& target) {
+ auto ptr = static_cast<Callable**>(bits);
+ *ptr = new Callable(std::forward<Callable_>(target));
+ }
+ static Result invoke(void* bits, Args... args) {
+ auto& target = **static_cast<Callable**>(bits);
+ return target(std::forward<Args>(args)...);
+ }
+ static void move(void* from_bits, void* to_bits) {
+ auto from_ptr = static_cast<Callable**>(from_bits);
+ auto to_ptr = static_cast<Callable**>(to_bits);
+ *to_ptr = *from_ptr;
+ }
+ static void destroy(void* bits) {
+ auto ptr = static_cast<Callable**>(bits);
+ delete *ptr;
+ }
+
+ static const target_ops<Result, Args...> ops;
+};
+
+inline void* heap_target_get(void* bits) { return *static_cast<void**>(bits); }
+
+template <typename Callable, typename Result, typename... Args>
+constexpr target_ops<Result, Args...> target<Callable,
+ /*is_inline=*/false,
+ /*is_shared=*/false, Result, Args...>::ops = {
+ &unshared_target_type_id, &heap_target_get, &target::move, &target::destroy, &target::invoke};
+
+// vtable for fit::function std::shared_ptr to target function
+
+template <typename SharedFunction>
+const void* get_target_type_id(const SharedFunction& function_or_callback) {
+ return function_or_callback.target_type_id();
+}
+
+// For this vtable,
+// Callable by definition will be either a fit::function or fit::callback
+template <typename SharedFunction, typename Result, typename... Args>
+struct target<SharedFunction,
+ /*is_inline=*/false, /*is_shared=*/true, Result, Args...>
+ final {
+ static void initialize(void* bits, SharedFunction target) {
+ new (bits) std::shared_ptr<SharedFunction>(
+ std::move(std::make_shared<SharedFunction>(std::move(target))));
+ }
+ static void copy_shared_ptr(void* from_bits, void* to_bits) {
+ auto& from_shared_ptr = *static_cast<std::shared_ptr<SharedFunction>*>(from_bits);
+ new (to_bits) std::shared_ptr<SharedFunction>(from_shared_ptr);
+ }
+ static const void* target_type_id(void* bits, const void* /*impl_ops*/) {
+ auto& function_or_callback = **static_cast<std::shared_ptr<SharedFunction>*>(bits);
+ return ::fit::internal::get_target_type_id(function_or_callback);
+ }
+ static void* get(void* bits) {
+ auto& function_or_callback = **static_cast<std::shared_ptr<SharedFunction>*>(bits);
+ return function_or_callback.template target<SharedFunction>(
+ /*check=*/false); // void* will fail the check
+ }
+ static Result invoke(void* bits, Args... args) {
+ auto& function_or_callback = **static_cast<std::shared_ptr<SharedFunction>*>(bits);
+ return function_or_callback(std::forward<Args>(args)...);
+ }
+ static void move(void* from_bits, void* to_bits) {
+ auto from_shared_ptr = std::move(*static_cast<std::shared_ptr<SharedFunction>*>(from_bits));
+ new (to_bits) std::shared_ptr<SharedFunction>(std::move(from_shared_ptr));
+ }
+ static void destroy(void* bits) { static_cast<std::shared_ptr<SharedFunction>*>(bits)->reset(); }
+
+ static const target_ops<Result, Args...> ops;
+};
+
+template <typename SharedFunction, typename Result, typename... Args>
+constexpr target_ops<Result, Args...> target<SharedFunction,
+ /*is_inline=*/false,
+ /*is_shared=*/true, Result, Args...>::ops = {
+ &target::target_type_id, &target::get, &target::move, &target::destroy, &target::invoke};
+
+// Calculates the alignment to use for a function of the provided
+// inline_target_size. Some platforms use a large alignment for max_align_t, so
+// use the minimum of max_align_t and the largest alignment for the inline
+// target size.
+//
+// Alignments must be powers of 2, and alignof(T) <= sizeof(T), so find the
+// largest power of 2 <= inline_target_size.
+constexpr size_t FunctionAlignment(size_t inline_target_size) {
+ return std::min(cpp20::bit_floor(inline_target_size), alignof(max_align_t));
+}
+
+// Function implementation details shared by all functions, regardless of
+// signature. This class is aligned based on inline_target_size and max_align_t
+// so that the target storage (bits_, the first class member) has correct
+// alignment.
+//
+// See |fit::function| and |fit::callback| documentation for more information.
+template <size_t inline_target_size>
+class alignas(FunctionAlignment(inline_target_size)) generic_function_base {
+ public:
+ // The inline target size must be a non-zero multiple of sizeof(void*). Uses
+ // of |fit::function_impl| and |fit::callback_impl| may call
+ // fit::internal::RoundUpToWord to round to a valid inline size.
+ //
+ // A multiple of sizeof(void*) is required because it:
+ //
+ // - Avoids unnecessary duplicate instantiations of the function classes when
+ // working with different inline sizes. This reduces code size.
+ // - Prevents creating unnecessarily restrictive functions. Without rounding, a
+ // function with a non-word size would be padded to at least the next word,
+ // but that space would be unusable.
+ // - Ensures that the true inline size matches the template parameter, which
+ // could cause confusion in error messages.
+ //
+ static_assert(inline_target_size >= sizeof(void*),
+ "The inline target size must be at least one word");
+ static_assert(inline_target_size % sizeof(void*) == 0,
+ "The inline target size must be a multiple of the word size");
+
+ // Deleted copy constructor and assign. |generic_function_base|
+ // implementations are move-only.
+ generic_function_base(const generic_function_base& other) = delete;
+ generic_function_base& operator=(const generic_function_base& other) = delete;
+
+ // Move assignment must be provided by subclasses.
+ generic_function_base& operator=(generic_function_base&& other) = delete;
+
+ protected:
+ constexpr generic_function_base() : null_bits_(), ops_(&null_target<>::ops) {}
+
+ generic_function_base(generic_function_base&& other) noexcept { move_target_from(other); }
+
+ ~generic_function_base() { destroy_target(); }
+
+ // Returns true if the function has a non-empty target.
+ explicit operator bool() const { return ops_->get(bits_) != nullptr; }
+
+ // Used by derived "impl" classes to implement operator=().
+ // Assigns an empty target.
+ void assign_null() {
+ destroy_target();
+ initialize_null_target();
+ }
+
+ // Used by derived "impl" classes to implement operator=().
+ // Assigns the function with a target moved from another function,
+ // leaving the other function with an empty target.
+ void assign_function(generic_function_base&& other) {
+ destroy_target();
+ move_target_from(other);
+ }
+
+ void swap(generic_function_base& other) {
+ if (&other == this)
+ return;
+
+ const base_target_ops* temp_ops = ops_;
+ // temp_bits, which stores the target, must maintain the expected alignment.
+ alignas(generic_function_base) uint8_t temp_bits[inline_target_size];
+ ops_->move(bits_, temp_bits);
+
+ ops_ = other.ops_;
+ other.ops_->move(other.bits_, bits_);
+
+ other.ops_ = temp_ops;
+ temp_ops->move(temp_bits, other.bits_);
+ }
+
+ // returns an opaque ID unique to the |Callable| type of the target.
+ // Used by check_target_type.
+ const void* target_type_id() const { return ops_->target_type_id(bits_, ops_); }
+
+ // leaves target uninitialized
+ void destroy_target() { ops_->destroy(bits_); }
+
+ // assumes target is uninitialized
+ void initialize_null_target() { ops_ = &null_target<>::ops; }
+
+ // Gets a pointer to the function context.
+ void* get() const { return ops_->get(bits_); }
+
+ // Allow function_base to directly access bits_ and ops_ when needed.
+ void* bits() const { return bits_; }
+ const base_target_ops* ops() const { return ops_; }
+ void set_ops(const base_target_ops* new_ops) { ops_ = new_ops; }
+
+ private:
+ // Implements the move operation, used by move construction and move
+ // assignment. Leaves other target initialized to null.
+ void move_target_from(generic_function_base& other) {
+ ops_ = other.ops_;
+ other.ops_->move(other.bits_, bits_);
+ other.initialize_null_target();
+ }
+
+ struct empty {};
+
+ union {
+ // Function context data. The bits_ field requires special alignment, but
+ // adding the alignas() at the field declaration increases the padding.
+ // Instead, generic_function_base is aligned according to max_align_t and
+ // inline_target_size, and bits_ is placed first in the class. Thus, bits_
+ // MUST remain first in the class to ensure proper alignment.
+ mutable uint8_t bits_[inline_target_size];
+
+ // Empty struct used when initializing the storage in the constexpr
+ // constructor.
+ empty null_bits_;
+ };
+
+ // The target_ops pointer for this function. This field has lower alignment
+ // requirement than bits, so placing ops after bits allows for better
+ // packing reducing the padding needed in some cases.
+ const base_target_ops* ops_;
+};
+
+template <size_t inline_target_size, bool require_inline, typename FunctionType>
+class function_base;
+
+// Function implementation details that require the function signature.
+// See |fit::function| and |fit::callback| documentation for more information.
+template <size_t inline_target_size, bool require_inline, typename Result, typename... Args>
+class function_base<inline_target_size, require_inline, Result(Args...)>
+ : public generic_function_base<inline_target_size> {
+ using base = generic_function_base<inline_target_size>;
+
+ // Check alignment and size of the base, which holds the bits_ and ops_ members.
+ static_assert(alignof(base) == FunctionAlignment(inline_target_size),
+ "Must be aligned as min(alignof(max_align_t), inline_target_size)");
+ static_assert(sizeof(base) == RoundUpToMultiple(inline_target_size + sizeof(base_target_ops*),
+ FunctionAlignment(inline_target_size)),
+ "generic_function_base has unexpected padding and is not minimal in size");
+
+ template <typename Callable>
+ using target_type = target<Callable, (sizeof(Callable) <= inline_target_size),
+ /*is_shared=*/false, Result, Args...>;
+ template <typename SharedFunction>
+ using shared_target_type = target<SharedFunction,
+ /*is_inline=*/false,
+ /*is_shared=*/true, Result, Args...>;
+
+ using ops_type = const target_ops<Result, Args...>*;
+
+ protected:
+ using result_type = Result;
+
+ constexpr function_base() = default;
+
+ constexpr function_base(decltype(nullptr)) : function_base() {}
+
+ function_base(Result (*function_target)(Args...)) { initialize_target(function_target); }
+
+ template <typename Callable,
+ typename = std::enable_if_t<std::is_convertible<
+ decltype(std::declval<Callable&>()(std::declval<Args>()...)), result_type>::value>>
+ function_base(Callable&& target) {
+ initialize_target(std::forward<Callable>(target));
+ }
+
+ function_base(function_base&&) noexcept = default;
+
+ // Returns a pointer to the function's target.
+ // If |check| is true (the default), the function _may_ abort if the
+ // caller tries to assign the target to a varible of the wrong type. (This
+ // check is currently skipped for share()d objects.)
+ // Note the shared pointer vtable must set |check| to false to assign the
+ // target to |void*|.
+ template <typename Callable>
+ Callable* target(bool check = true) {
+ if (check)
+ check_target_type<Callable>();
+ return static_cast<Callable*>(base::get());
+ }
+
+ // Returns a pointer to the function's target (const version).
+ // If |check| is true (the default), the function _may_ abort if the
+ // caller tries to assign the target to a varible of the wrong type. (This
+ // check is currently skipped for share()d objects.)
+ // Note the shared pointer vtable must set |check| to false to assign the
+ // target to |void*|.
+ template <typename Callable>
+ const Callable* target(bool check = true) const {
+ if (check)
+ check_target_type<Callable>();
+ return static_cast<Callable*>(base::get());
+ }
+
+ // Used by the derived "impl" classes to implement share().
+ //
+ // The caller creates a new object of the same type as itself, and passes in
+ // the empty object. This function first checks if |this| is already shared,
+ // and if not, creates a new version of itself containing a |std::shared_ptr|
+ // to its original self, and updates |ops_| to the vtable for the shared
+ // version.
+ //
+ // Then it copies its |shared_ptr| to the |bits_| of the given |copy|, and
+ // assigns the same shared pointer vtable to the copy's |ops_|.
+ //
+ // The target itself is not copied; it is moved to the heap and its lifetime
+ // is extended until all references have been released.
+ //
+ // Note: This method is not supported on |fit::inline_function<>|
+ // because it may incur a heap allocation which is contrary to
+ // the stated purpose of |fit::inline_function<>|.
+ template <typename SharedFunction>
+ void share_with(SharedFunction& copy) {
+ static_assert(!require_inline, "Inline functions cannot be shared.");
+ if (base::get() != nullptr) {
+ // Convert to a shared function if it isn't already.
+ if (base::ops() != &shared_target_type<SharedFunction>::ops) {
+ shared_target_type<SharedFunction>::initialize(
+ base::bits(), std::move(*static_cast<SharedFunction*>(this)));
+ base::set_ops(&shared_target_type<SharedFunction>::ops);
+ }
+ copy_shared_target_to(copy);
+ }
+ }
+
+ // Used by derived "impl" classes to implement operator()().
+ // Invokes the function's target.
+ // Note that fit::callback will release the target immediately after
+ // invoke() (also affecting any share()d copies).
+ // Aborts if the function's target is empty.
+ // TODO(b/241567321): Remove "no sanitize" after pw_protobuf is fixed.
+ Result invoke(Args... args) const PW_NO_SANITIZE("function") {
+ // Down cast the ops to the derived type that this function was instantiated
+ // with, which includes the invoke function.
+ //
+ // NOTE: This abuses the calling convention when invoking a null function
+ // that takes arguments! Null functions share a single vtable with a void()
+ // invoke function. This is permitted only because invoking a null function
+ // is an error that immediately aborts execution. Also, the null invoke
+ // function never attempts to access any passed arguments.
+ return static_cast<ops_type>(base::ops())->invoke(base::bits(), std::forward<Args>(args)...);
+ }
+
+ // Used by derived "impl" classes to implement operator=().
+ // Assigns the function's target.
+ // If target == nullptr, assigns an empty target.
+ template <typename Callable,
+ typename = std::enable_if_t<std::is_convertible<
+ decltype(std::declval<Callable&>()(std::declval<Args>()...)), result_type>::value>>
+ void assign_callable(Callable&& target) {
+ base::destroy_target();
+ initialize_target(std::forward<Callable>(target));
+ }
+
+ private:
+ // fit::function and fit::callback are not directly copyable, but share()
+ // will create shared references to the original object. This method
+ // implements the copy operation for the |std::shared_ptr| wrapper.
+ template <typename SharedFunction>
+ void copy_shared_target_to(SharedFunction& copy) {
+ copy.destroy_target();
+ PW_ASSERT(base::ops() == &shared_target_type<SharedFunction>::ops);
+ shared_target_type<SharedFunction>::copy_shared_ptr(base::bits(), copy.bits());
+ copy.set_ops(base::ops());
+ }
+
+ // target may or may not be initialized.
+ template <typename Callable>
+ void initialize_target(Callable&& target) {
+ // Convert function or function references to function pointer.
+ using DecayedCallable = std::decay_t<Callable>;
+ static_assert(!require_inline || alignof(DecayedCallable) <= alignof(base),
+ "Alignment of Callable must be <= alignment of the function class.");
+ static_assert(!require_inline || sizeof(DecayedCallable) <= inline_target_size,
+ "Callable too large to store inline as requested.");
+ if (is_null(target)) {
+ base::initialize_null_target();
+ } else {
+ base::set_ops(&target_type<DecayedCallable>::ops);
+ target_type<DecayedCallable>::initialize(base::bits(), std::forward<Callable>(target));
+ }
+ }
+
+ // Called by target() if |check| is true.
+ // Checks the template parameter, usually inferred from the context of
+ // the call to target(), and aborts the program if it can determine that
+ // the Callable type is not compatible with the function's Result and Args.
+ template <typename Callable>
+ void check_target_type() const {
+ if (target_type<Callable>::ops.target_type_id(nullptr, &target_type<Callable>::ops) !=
+ base::target_type_id()) {
+ PW_ASSERT(false);
+ }
+ }
+};
+
+} // namespace internal
+} // namespace fit
+
+#endif // LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_FUNCTION_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h
new file mode 100644
index 000000000..18a0c8751
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/result.h
@@ -0,0 +1,445 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_RESULT_H_
+#define LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_RESULT_H_
+
+#include <lib/fit/internal/compiler.h>
+#include <lib/stdcompat/type_traits.h>
+
+#include <cstddef>
+#include <new>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+namespace fit {
+
+// Forward declarations.
+template <typename E>
+class error;
+
+template <typename... Ts>
+class success;
+
+template <typename E, typename... Ts>
+class result;
+
+namespace internal {
+
+// Determines whether T has an operator-> overload and provides a method that
+// forwards its argument by reference when T has the overload, or by pointer
+// otherwise.
+template <typename T, typename = void>
+struct arrow_operator {
+ static constexpr T* forward(T& value) { return &value; }
+ static constexpr const T* forward(const T& value) { return &value; }
+};
+template <typename T>
+struct arrow_operator<T, std::enable_if_t<cpp17::is_pointer_v<T>>> {
+ static constexpr T& forward(T& value) { return value; }
+ static constexpr const T& forward(const T& value) { return value; }
+};
+template <typename T>
+struct arrow_operator<T, cpp17::void_t<decltype(std::declval<T>().operator->())>> {
+ static constexpr T& forward(T& value) { return value; }
+ static constexpr const T& forward(const T& value) { return value; }
+};
+
+// Concept helper for constructor, method, and operator overloads.
+template <typename... Conditions>
+using requires_conditions = std::enable_if_t<cpp17::conjunction_v<Conditions...>, bool>;
+
+// Detects whether the given expression evaluates to an instance of the template T.
+template <template <typename...> class T>
+struct template_matcher {
+ template <typename... Args>
+ static constexpr std::true_type match(const T<Args...>&);
+ static constexpr std::false_type match(...);
+};
+
+template <typename T, template <typename...> class U, typename = bool>
+struct is_match : decltype(template_matcher<U>::match(std::declval<T>())) {};
+
+template <typename T, template <typename...> class U>
+struct is_match<T, U, requires_conditions<std::is_void<T>>> : std::false_type {};
+
+template <typename T, template <typename...> class U>
+static constexpr bool is_match_v = is_match<T, U>::value;
+
+// Predicate indicating whether type T is an instantiation of fit::error.
+template <typename T>
+struct is_error : is_match<T, ::fit::error>::type {};
+
+template <typename T>
+static constexpr bool is_error_v = is_error<T>::value;
+
+// Predicate indicating whether type T is not an instantiation of fit::error.
+template <typename T>
+struct not_error_type : cpp17::negation<is_error<T>>::type {};
+
+// Predicate indicating whether type T is an instantiation of fit::success.
+template <typename T>
+struct is_success : is_match<T, ::fit::success>::type {};
+
+template <typename T>
+static constexpr bool is_success_v = is_success<T>::value;
+
+// Predicate indicating whether type T is an instantiation of fit::result.
+template <typename T>
+struct is_result : is_match<T, ::fit::result>::type {};
+
+template <typename T>
+static constexpr bool is_result_v = is_result<T>::value;
+
+// Predicate indicating whether type T is not an instantiation of fit::result.
+template <typename T>
+struct not_result_type : cpp17::negation<is_result<T>>::type {};
+
+// Determines whether T += U is well defined.
+template <typename T, typename U, typename = void>
+struct has_plus_equals : std::false_type {};
+template <typename T, typename U>
+struct has_plus_equals<T, U, cpp17::void_t<decltype(std::declval<T>() += std::declval<U>())>>
+ : std::true_type {};
+
+// Enable if relational operator is convertible to bool and the optional
+// conditions are true.
+template <typename Op, typename... Conditions>
+using enable_rel_op =
+ std::enable_if_t<(cpp17::is_convertible_v<Op, bool> && cpp17::conjunction_v<Conditions...>),
+ bool>;
+
+// Specifies whether a type is trivially or non-trivially destructible.
+enum class storage_class_e {
+ trivial,
+ non_trivial,
+};
+
+// Evaluates to storage_class_e::trivial if all of the types in Ts are trivially
+// destructible, storage_class_e::non_trivial otherwise.
+template <typename... Ts>
+static constexpr storage_class_e storage_class_trait =
+ cpp17::conjunction_v<std::is_trivially_destructible<Ts>...> ? storage_class_e::trivial
+ : storage_class_e::non_trivial;
+
+// Trivial type for the default variant of the union below.
+struct empty_type {};
+
+// Type tags to discriminate between empty, error, and value constructors,
+// avoiding ambiguity with copy/move constructors.
+enum empty_t { empty_v };
+enum error_t { error_v };
+enum value_t { value_v };
+
+// Union that stores either nothing, an error of type E, or a value of type T.
+// This type is specialized for trivially and non-trivially destructible types
+// to support multi-register return values for trivial types.
+template <typename E, typename T, storage_class_e = storage_class_trait<E, T>>
+union error_or_value_type {
+ constexpr error_or_value_type() : empty{} {}
+
+ constexpr error_or_value_type(const error_or_value_type&) = default;
+ constexpr error_or_value_type& operator=(const error_or_value_type&) = default;
+ constexpr error_or_value_type(error_or_value_type&&) = default;
+ constexpr error_or_value_type& operator=(error_or_value_type&&) = default;
+
+ template <typename F>
+ constexpr error_or_value_type(error_t, F&& error) : error(std::forward<F>(error)) {}
+
+ template <typename U>
+ constexpr error_or_value_type(value_t, U&& value) : value(std::forward<U>(value)) {}
+
+ ~error_or_value_type() = default;
+
+ constexpr void destroy(error_t) {}
+ constexpr void destroy(value_t) {}
+
+ empty_type empty;
+ E error;
+ T value;
+};
+template <typename E, typename T>
+union error_or_value_type<E, T, storage_class_e::non_trivial> {
+ constexpr error_or_value_type() : empty{} {}
+
+ constexpr error_or_value_type(const error_or_value_type&) = default;
+ constexpr error_or_value_type& operator=(const error_or_value_type&) = default;
+ constexpr error_or_value_type(error_or_value_type&&) = default;
+ constexpr error_or_value_type& operator=(error_or_value_type&&) = default;
+
+ template <typename F>
+ constexpr error_or_value_type(error_t, F&& error) : error(std::forward<F>(error)) {}
+
+ template <typename U>
+ constexpr error_or_value_type(value_t, U&& value) : value(std::forward<U>(value)) {}
+
+ ~error_or_value_type() {}
+
+ // The caller must manually destroy() if overwriting an existing value.
+ constexpr void copy_from(error_t, const E& e) { new (&error) E(e); }
+ constexpr void copy_from(value_t, const T& t) { new (&value) T(t); }
+ constexpr void move_from(error_t, E&& e) { new (&error) E(std::move(e)); }
+ constexpr void move_from(value_t, T&& t) { new (&value) T(std::move(t)); }
+
+ constexpr void destroy(error_t) { error.E::~E(); }
+ constexpr void destroy(value_t) { value.T::~T(); }
+
+ empty_type empty;
+ E error;
+ T value;
+};
+
+// Specifies whether the storage is empty, contains an error, or contains a
+// a value.
+enum class state_e {
+ empty,
+ has_error,
+ has_value,
+};
+
+// Storage type is either empty, holds an error, or holds a set of values. This
+// type is specialized for trivially and non-trivially destructible types. When
+// E and all of the elements of Ts are trivially destructible, this type
+// provides a trivial destructor, which is necessary for multi-register return
+// value optimization.
+template <storage_class_e storage_class, typename E, typename... Ts>
+struct storage_type;
+
+template <storage_class_e storage_class, typename E, typename T>
+struct storage_type<storage_class, E, T> {
+ using value_type = error_or_value_type<E, T>;
+
+ constexpr storage_type() = default;
+
+ constexpr storage_type(const storage_type&) = default;
+ constexpr storage_type& operator=(const storage_type&) = default;
+ constexpr storage_type(storage_type&&) = default;
+ constexpr storage_type& operator=(storage_type&&) = default;
+
+ constexpr void destroy() {}
+
+ constexpr void reset() { state = state_e::empty; }
+
+ ~storage_type() = default;
+
+ explicit constexpr storage_type(empty_t) {}
+
+ template <typename F>
+ constexpr storage_type(error_t, F&& error)
+ : state{state_e::has_error}, error_or_value{error_v, std::forward<F>(error)} {}
+
+ template <typename U>
+ explicit constexpr storage_type(value_t, U&& value)
+ : state{state_e::has_value}, error_or_value{value_v, std::forward<U>(value)} {}
+
+ template <storage_class_e other_storage_class, typename F, typename U>
+ explicit constexpr storage_type(storage_type<other_storage_class, F, U>&& other)
+ : state{other.state},
+ error_or_value{other.state == state_e::empty ? value_type{}
+ : other.state == state_e::has_error
+ ? value_type{error_v, std::move(other.error_or_value.error)}
+ : value_type{value_v, std::move(other.error_or_value.value)}} {}
+
+ state_e state{state_e::empty};
+ value_type error_or_value;
+};
+template <typename E, typename T>
+struct storage_type<storage_class_e::non_trivial, E, T> {
+ using value_type = error_or_value_type<E, T>;
+
+ constexpr storage_type() = default;
+
+ constexpr storage_type(const storage_type& other) { copy_from(other); }
+ constexpr storage_type& operator=(const storage_type& other) {
+ destroy();
+ copy_from(other);
+ return *this;
+ }
+
+ constexpr storage_type(storage_type&& other) noexcept(
+ std::is_nothrow_move_constructible<E>::value&& std::is_nothrow_move_constructible<T>::value) {
+ move_from(std::move(other));
+ }
+ constexpr storage_type& operator=(storage_type&& other) noexcept(
+ std::is_nothrow_move_assignable<E>::value&& std::is_nothrow_move_assignable<T>::value) {
+ destroy();
+ move_from(std::move(other));
+ return *this;
+ }
+
+ // Copy/move-constructs over this object's value. If there could be a previous value, callers must
+ // call destroy() first.
+ constexpr void copy_from(const storage_type& other) {
+ state = other.state;
+ if (state == state_e::has_value) {
+ error_or_value.copy_from(value_v, other.error_or_value.value);
+ } else if (state == state_e::has_error) {
+ error_or_value.copy_from(error_v, other.error_or_value.error);
+ }
+ }
+ constexpr void move_from(storage_type&& other) {
+ state = other.state;
+ if (state == state_e::has_value) {
+ error_or_value.move_from(value_v, std::move(other.error_or_value.value));
+ } else if (state == state_e::has_error) {
+ error_or_value.move_from(error_v, std::move(other.error_or_value.error));
+ }
+ }
+
+ constexpr void destroy() {
+ if (state == state_e::has_value) {
+ error_or_value.destroy(value_v);
+ } else if (state == state_e::has_error) {
+ error_or_value.destroy(error_v);
+ }
+ }
+
+ constexpr void reset() {
+ destroy();
+ state = state_e::empty;
+ }
+
+ ~storage_type() { destroy(); }
+
+ explicit constexpr storage_type(empty_t) {}
+
+ template <typename F>
+ constexpr storage_type(error_t, F&& error)
+ : state{state_e::has_error}, error_or_value{error_v, std::forward<F>(error)} {}
+
+ template <typename U>
+ explicit constexpr storage_type(value_t, U&& value)
+ : state{state_e::has_value}, error_or_value{value_v, std::forward<U>(value)} {}
+
+ template <storage_class_e other_storage_class, typename F, typename U>
+ explicit constexpr storage_type(storage_type<other_storage_class, F, U>&& other)
+ : state{other.state},
+ error_or_value{other.state == state_e::empty ? value_type{}
+ : other.state == state_e::has_error
+ ? value_type{error_v, std::move(other.error_or_value.error)}
+ : value_type{value_v, std::move(other.error_or_value.value)}} {}
+
+ state_e state{state_e::empty};
+ value_type error_or_value;
+};
+
+template <storage_class_e storage_class, typename E>
+struct storage_type<storage_class, E> {
+ using value_type = error_or_value_type<E, empty_type>;
+
+ constexpr storage_type() = default;
+
+ constexpr storage_type(const storage_type&) = default;
+ constexpr storage_type& operator=(const storage_type&) = default;
+ constexpr storage_type(storage_type&&) = default;
+ constexpr storage_type& operator=(storage_type&&) = default;
+
+ constexpr void destroy() {}
+
+ constexpr void reset() { state = state_e::empty; }
+
+ ~storage_type() = default;
+
+ explicit constexpr storage_type(empty_t) {}
+
+ explicit constexpr storage_type(value_t)
+ : state{state_e::has_value}, error_or_value{value_v, empty_type{}} {}
+
+ template <typename F>
+ constexpr storage_type(error_t, F&& error)
+ : state{state_e::has_error}, error_or_value{error_v, std::forward<F>(error)} {}
+
+ template <storage_class_e other_storage_class, typename F>
+ explicit constexpr storage_type(storage_type<other_storage_class, F>&& other)
+ : state{other.state},
+ error_or_value{other.state == state_e::empty ? value_type{}
+ : other.state == state_e::has_error
+ ? value_type{error_v, std::move(other.error_or_value.error)}
+ : value_type{value_v, std::move(other.error_or_value.value)}} {}
+
+ state_e state{state_e::empty};
+ value_type error_or_value;
+};
+template <typename E>
+struct storage_type<storage_class_e::non_trivial, E> {
+ using value_type = error_or_value_type<E, empty_type>;
+
+ constexpr storage_type() = default;
+
+ constexpr storage_type(const storage_type& other) { copy_from(other); }
+ constexpr storage_type& operator=(const storage_type& other) {
+ destroy();
+ copy_from(other);
+ return *this;
+ }
+
+ constexpr storage_type(storage_type&& other) noexcept(
+ std::is_nothrow_move_constructible<E>::value) {
+ move_from(std::move(other));
+ }
+ constexpr storage_type& operator=(storage_type&& other) noexcept(
+ std::is_nothrow_move_assignable<E>::value) {
+ destroy();
+ move_from(std::move(other));
+ return *this;
+ }
+
+ // Copy/move-constructs over this object's value. If there could be a previous value, callers must
+ // call destroy() first.
+ constexpr void copy_from(const storage_type& other) {
+ state = other.state;
+ if (state == state_e::has_error) {
+ error_or_value.copy_from(error_v, other.error_or_value.error);
+ }
+ }
+ constexpr void move_from(storage_type&& other) {
+ state = other.state;
+ if (state == state_e::has_error) {
+ error_or_value.move_from(error_v, std::move(other.error_or_value.error));
+ }
+ }
+
+ constexpr void destroy() {
+ if (state == state_e::has_error) {
+ error_or_value.destroy(error_v);
+ }
+ }
+
+ constexpr void reset() {
+ destroy();
+ state = state_e::empty;
+ }
+
+ ~storage_type() { destroy(); }
+
+ explicit constexpr storage_type(empty_t) {}
+
+ explicit constexpr storage_type(value_t)
+ : state{state_e::has_value}, error_or_value{value_v, empty_type{}} {}
+
+ template <typename F>
+ constexpr storage_type(error_t, F&& error)
+ : state{state_e::has_error}, error_or_value{error_v, std::forward<F>(error)} {}
+
+ template <storage_class_e other_storage_class, typename F>
+ explicit constexpr storage_type(storage_type<other_storage_class, F>&& other)
+ : state{other.state},
+ error_or_value{other.state == state_e::empty ? value_type{}
+ : other.state == state_e::has_error
+ ? value_type{error_v, std::move(other.error_or_value.error)}
+ : value_type{value_v, std::move(other.error_or_value.value)}} {}
+
+ state_e state{state_e::empty};
+ value_type error_or_value;
+};
+
+// Simplified alias of storage_type.
+template <typename E, typename... Ts>
+using storage = storage_type<storage_class_trait<E, Ts...>, E, Ts...>;
+
+} // namespace internal
+} // namespace fit
+
+#endif // LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_RESULT_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h
new file mode 100644
index 000000000..2d1f8a03a
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/internal/utility.h
@@ -0,0 +1,138 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_UTILITY_H_
+#define LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_UTILITY_H_
+
+#include <lib/stdcompat/type_traits.h>
+
+#include <type_traits>
+#include <utility>
+
+namespace fit {
+namespace internal {
+
+// Utility to return the first type in a parameter pack.
+template <typename... Ts>
+struct first;
+template <typename First, typename... Rest>
+struct first<First, Rest...> {
+ using type = First;
+};
+
+template <typename... Ts>
+using first_t = typename first<Ts...>::type;
+
+// Utility to count the occurences of type T in the parameter pack Ts.
+template <typename T, typename... Ts>
+struct occurences_of : std::integral_constant<size_t, 0> {};
+template <typename T, typename U>
+struct occurences_of<T, U> : std::integral_constant<size_t, std::is_same<T, U>::value> {};
+template <typename T, typename First, typename... Rest>
+struct occurences_of<T, First, Rest...>
+ : std::integral_constant<size_t,
+ occurences_of<T, First>::value + occurences_of<T, Rest...>::value> {};
+
+template <typename T, typename... Ts>
+constexpr size_t occurences_of_v = occurences_of<T, Ts...>::value;
+
+// Utility to remove const, volatile, and reference qualifiers.
+template <typename T>
+using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;
+
+// Evaluates to truth-like when type T matches type U with cv-reference removed.
+template <typename T, typename U>
+using not_same_type = cpp17::negation<std::is_same<T, remove_cvref_t<U>>>;
+
+// Concept helper for constructors.
+template <typename... Conditions>
+using requires_conditions = std::enable_if_t<cpp17::conjunction_v<Conditions...>, bool>;
+
+// Concept helper for assignment operators.
+template <typename Return, typename... Conditions>
+using assignment_requires_conditions =
+ std::enable_if_t<cpp17::conjunction_v<Conditions...>, std::add_lvalue_reference_t<Return>>;
+
+// Evaluates to true when every element type of Ts is trivially destructible.
+template <typename... Ts>
+constexpr bool is_trivially_destructible_v =
+ cpp17::conjunction_v<std::is_trivially_destructible<Ts>...>;
+
+// Evaluates to true when every element type of Ts is trivially copyable.
+template <typename... Ts>
+constexpr bool is_trivially_copyable_v =
+ (cpp17::conjunction_v<std::is_trivially_copy_assignable<Ts>...> &&
+ cpp17::conjunction_v<std::is_trivially_copy_constructible<Ts>...>);
+
+// Evaluates to true when every element type of Ts is trivially movable.
+template <typename... Ts>
+constexpr bool is_trivially_movable_v =
+ (cpp17::conjunction_v<std::is_trivially_move_assignable<Ts>...> &&
+ cpp17::conjunction_v<std::is_trivially_move_constructible<Ts>...>);
+
+// Enable if relational operator is convertible to bool and the optional
+// conditions are true.
+template <typename Op, typename... Conditions>
+using enable_relop_t =
+ std::enable_if_t<(std::is_convertible<Op, bool>::value && cpp17::conjunction_v<Conditions...>),
+ bool>;
+
+template <typename T>
+struct identity {
+ using type = T;
+};
+
+// Evaluates to true when T is an unbounded array.
+template <typename T>
+struct is_unbounded_array : cpp17::conjunction<std::is_array<T>, cpp17::negation<std::extent<T>>> {
+};
+
+// Returns true when T is a complete type or an unbounded array.
+template <typename T, size_t = sizeof(T)>
+constexpr bool is_complete_or_unbounded_array(identity<T>) {
+ return true;
+}
+template <typename Identity, typename T = typename Identity::type>
+constexpr bool is_complete_or_unbounded_array(Identity) {
+ return cpp17::disjunction<std::is_reference<T>, std::is_function<T>, std::is_void<T>,
+ is_unbounded_array<T>>::value;
+}
+
+// Using swap for ADL. This directive is contained within the fit::internal
+// namespace, which prevents leaking std::swap into user namespaces. Doing this
+// at namespace scope is necessary to lookup swap via ADL while preserving the
+// noexcept() specification of the resulting lookup.
+using std::swap;
+
+// Evaluates to true when T is swappable.
+template <typename T, typename = void>
+struct is_swappable : std::false_type {
+ static_assert(is_complete_or_unbounded_array(identity<T>{}),
+ "T must be a complete type or an unbounded array!");
+};
+template <typename T>
+struct is_swappable<T, cpp17::void_t<decltype(swap(std::declval<T&>(), std::declval<T&>()))>>
+ : std::true_type {
+ static_assert(is_complete_or_unbounded_array(identity<T>{}),
+ "T must be a complete type or an unbounded array!");
+};
+
+// Evaluates to true when T is nothrow swappable.
+template <typename T, typename = void>
+struct is_nothrow_swappable : std::false_type {
+ static_assert(is_complete_or_unbounded_array(identity<T>{}),
+ "T must be a complete type or an unbounded array!");
+};
+template <typename T>
+struct is_nothrow_swappable<T,
+ cpp17::void_t<decltype(swap(std::declval<T&>(), std::declval<T&>()))>>
+ : std::integral_constant<bool, noexcept(swap(std::declval<T&>(), std::declval<T&>()))> {
+ static_assert(is_complete_or_unbounded_array(identity<T>{}),
+ "T must be a complete type or an unbounded array!");
+};
+
+} // namespace internal
+} // namespace fit
+
+#endif // LIB_FIT_INCLUDE_LIB_FIT_INTERNAL_UTILITY_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h
new file mode 100644
index 000000000..d6ba9b7ff
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/nullable.h
@@ -0,0 +1,252 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_NULLABLE_H_
+#define LIB_FIT_NULLABLE_H_
+
+#include <assert.h>
+#include <lib/stdcompat/optional.h>
+
+#include <type_traits>
+#include <utility>
+
+#include "pw_assert/assert.h"
+
+namespace fit {
+
+// Determines whether a type can be compared with nullptr.
+template <typename T, typename Comparable = bool>
+struct is_comparable_with_null : public std::false_type {};
+template <typename T>
+struct is_comparable_with_null<T, decltype(std::declval<const T&>() == nullptr)>
+ : public std::true_type {};
+
+// Suppress the warning when the compiler can see that a nullable value is
+// never equal to nullptr.
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Waddress"
+template <typename T, std::enable_if_t<is_comparable_with_null<T>::value, bool> = true>
+constexpr inline bool is_null(T&& value) {
+ return std::forward<T>(value) == nullptr;
+}
+#pragma GCC diagnostic pop
+
+template <typename T, std::enable_if_t<!is_comparable_with_null<T>::value, bool> = false>
+constexpr inline bool is_null(T&&) {
+ return false;
+}
+
+// Determines whether a type can be initialized, assigned, and compared
+// with nullptr.
+template <typename T>
+struct is_nullable
+ : public std::integral_constant<bool, std::is_constructible<T, decltype(nullptr)>::value &&
+ std::is_assignable<T&, decltype(nullptr)>::value &&
+ is_comparable_with_null<T>::value> {};
+template <>
+struct is_nullable<void> : public std::false_type {};
+
+// Holds a value or nullptr.
+//
+// This class is similar to |std::optional<T>| except that it uses less
+// storage when the value type can be initialized, assigned, and compared
+// with nullptr.
+//
+// For example:
+// - sizeof(fit::nullable<void*>) == sizeof(void*)
+// - sizeof(std::optional<void*>) == sizeof(struct { bool; void*; })
+// - sizeof(fit::nullable<int>) == sizeof(struct { bool; int; })
+// - sizeof(std::optional<int>) == sizeof(struct { bool; int; })
+//
+// TODO(fxbug.dev/4681): fit::nullable does not precisely mirror
+// cpp17::optional. This should be corrected to avoid surprises when switching
+// between the types.
+template <typename T, bool = (is_nullable<T>::value && std::is_constructible<T, T&&>::value &&
+ std::is_assignable<T&, T&&>::value)>
+class nullable final {
+ public:
+ using value_type = T;
+
+ ~nullable() = default;
+ constexpr nullable() = default;
+
+ explicit constexpr nullable(decltype(nullptr)) {}
+ explicit constexpr nullable(T value) : opt_(std::move(value)) {}
+
+ constexpr nullable(const nullable& other) = default;
+ constexpr nullable& operator=(const nullable& other) = default;
+
+ constexpr nullable(nullable&& other) = default;
+ constexpr nullable& operator=(nullable&& other) = default;
+
+ constexpr T& value() & { return opt_.value(); }
+ constexpr const T& value() const& { return opt_.value(); }
+ constexpr T&& value() && { return std::move(opt_.value()); }
+ constexpr const T&& value() const&& { return std::move(opt_.value()); }
+
+ template <typename U = T>
+ constexpr T value_or(U&& default_value) const {
+ return opt_.value_or(std::forward<U>(default_value));
+ }
+
+ constexpr T* operator->() { return &*opt_; }
+ constexpr const T* operator->() const { return &*opt_; }
+ constexpr T& operator*() { return *opt_; }
+ constexpr const T& operator*() const { return *opt_; }
+
+ constexpr bool has_value() const { return opt_.has_value(); }
+ explicit constexpr operator bool() const { return has_value(); }
+
+ constexpr nullable& operator=(decltype(nullptr)) {
+ reset();
+ return *this;
+ }
+
+ constexpr nullable& operator=(T value) {
+ opt_ = std::move(value);
+ return *this;
+ }
+
+ constexpr void reset() { opt_.reset(); }
+
+ constexpr void swap(nullable& other) { opt_.swap(other.opt_); }
+
+ private:
+ cpp17::optional<T> opt_;
+};
+
+template <typename T>
+class nullable<T, true> final {
+ public:
+ using value_type = T;
+
+ constexpr nullable() : value_(nullptr) {}
+ explicit constexpr nullable(decltype(nullptr)) : value_(nullptr) {}
+ explicit constexpr nullable(T value) : value_(std::move(value)) {}
+ constexpr nullable(const nullable& other) = default;
+ constexpr nullable(nullable&& other) : value_(std::move(other.value_)) {}
+ ~nullable() = default;
+
+ constexpr T& value() & {
+ if (has_value()) {
+ return value_;
+ } else {
+ PW_ASSERT(false);
+ }
+ }
+ constexpr const T& value() const& {
+ if (has_value()) {
+ return value_;
+ } else {
+ PW_ASSERT(false);
+ }
+ }
+ constexpr T&& value() && {
+ if (has_value()) {
+ return std::move(value_);
+ } else {
+ PW_ASSERT(false);
+ }
+ }
+ constexpr const T&& value() const&& {
+ if (has_value()) {
+ return std::move(value_);
+ } else {
+ PW_ASSERT(false);
+ }
+ }
+
+ template <typename U = T>
+ constexpr T value_or(U&& default_value) const {
+ return has_value() ? value_ : static_cast<T>(std::forward<U>(default_value));
+ }
+
+ constexpr T* operator->() { return &value_; }
+ constexpr const T* operator->() const { return &value_; }
+ constexpr T& operator*() { return value_; }
+ constexpr const T& operator*() const { return value_; }
+
+ constexpr bool has_value() const { return !(value_ == nullptr); }
+ explicit constexpr operator bool() const { return has_value(); }
+
+ constexpr nullable& operator=(const nullable& other) = default;
+ constexpr nullable& operator=(nullable&& other) {
+ value_ = std::move(other.value_);
+ return *this;
+ }
+
+ constexpr nullable& operator=(decltype(nullptr)) {
+ reset();
+ return *this;
+ }
+
+ constexpr nullable& operator=(T value) {
+ value_ = std::move(value);
+ return *this;
+ }
+
+ constexpr void reset() { value_ = nullptr; }
+
+ constexpr void swap(nullable& other) {
+ using std::swap;
+ swap(value_, other.value_);
+ }
+
+ private:
+ T value_;
+};
+
+template <typename T>
+void swap(nullable<T>& a, nullable<T>& b) {
+ a.swap(b);
+}
+
+template <typename T>
+constexpr bool operator==(const nullable<T>& lhs, decltype(nullptr)) {
+ return !lhs.has_value();
+}
+template <typename T>
+constexpr bool operator!=(const nullable<T>& lhs, decltype(nullptr)) {
+ return lhs.has_value();
+}
+
+template <typename T>
+constexpr bool operator==(decltype(nullptr), const nullable<T>& rhs) {
+ return !rhs.has_value();
+}
+template <typename T>
+constexpr bool operator!=(decltype(nullptr), const nullable<T>& rhs) {
+ return rhs.has_value();
+}
+
+template <typename T, typename U>
+constexpr bool operator==(const nullable<T>& lhs, const nullable<U>& rhs) {
+ return (lhs.has_value() == rhs.has_value()) && (!lhs.has_value() || *lhs == *rhs);
+}
+template <typename T, typename U>
+constexpr bool operator!=(const nullable<T>& lhs, const nullable<U>& rhs) {
+ return (lhs.has_value() != rhs.has_value()) || (lhs.has_value() && *lhs != *rhs);
+}
+
+template <typename T, typename U>
+constexpr bool operator==(const nullable<T>& lhs, const U& rhs) {
+ return (lhs.has_value() != is_null(rhs)) && (!lhs.has_value() || *lhs == rhs);
+}
+template <typename T, typename U>
+constexpr bool operator!=(const nullable<T>& lhs, const U& rhs) {
+ return (lhs.has_value() == is_null(rhs)) || (lhs.has_value() && *lhs != rhs);
+}
+
+template <typename T, typename U>
+constexpr bool operator==(const T& lhs, const nullable<U>& rhs) {
+ return (is_null(lhs) != rhs.has_value()) && (!rhs.has_value() || lhs == *rhs);
+}
+template <typename T, typename U>
+constexpr bool operator!=(const T& lhs, const nullable<U>& rhs) {
+ return (is_null(lhs) == rhs.has_value()) || (rhs.has_value() && lhs != *rhs);
+}
+
+} // namespace fit
+
+#endif // LIB_FIT_NULLABLE_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
new file mode 100644
index 000000000..a7f77cac9
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/result.h
@@ -0,0 +1,800 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_INCLUDE_LIB_FIT_RESULT_H_
+#define LIB_FIT_INCLUDE_LIB_FIT_RESULT_H_
+
+#include <lib/fit/internal/compiler.h>
+#include <lib/fit/internal/result.h>
+
+#include <cstddef>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+// General purpose fit::result type for Zircon kernel, system, and above.
+//
+// fit::result is an efficient C++ implementation of the result pattern found in many languages and
+// vocabulary libraries. This implementation supports returning either an error value or zero/one
+// non-error values from a function or method.
+//
+// To make a fit::result:
+//
+// fit::success(success_value) // Success for fit::result<E, V>.
+// fit::success() // Success for fit::result<E> (no success value).
+// fit::ok(success_value) // Success for fit::result<E, V>.
+// fit::ok() // Success for fit::result<E> (no success value).
+//
+// fit::error(error_value) // Failure.
+// fit::as_error(error_value) // Failure.
+// fit::failed() // Failure for fit::result<>.
+//
+// General functions that can always be called:
+//
+// bool is_ok()
+// bool is_error()
+// T value_or(default_value) // Returns value on success, or default on failure.
+// result<E[, V]> map_error(fn) // Transforms the error value of the result using fn.
+//
+// Available only when is_ok() (will assert otherwise).
+//
+// T& value() // Accesses the value.
+// T&& value() // Moves the value.
+// T& operator*() // Accesses the value.
+// T&& operator*() // Moves the value.
+// T* operator->() // Accesses the value.
+// success<T> take_value() // Generates a fit::success() which can be implicitly converted to
+// // another fit::result with the same "success" type.
+//
+// Available only when is_error() (will assert otherwise):
+//
+// E& error_value() // Error value.
+// E&& error_value() // Error value.
+// error<E> take_error() // Generates a fit::error() which can be implicitly converted to a
+// // fit::result with a different "success" vluae type (or
+// // fit::result<E>).
+
+#include "pw_assert/assert.h"
+
+namespace fit {
+
+// Convenience type to indicate failure without elaboration.
+//
+// Example:
+//
+// fit::result<fit::failed> Contains(const char* string, const char* find) {
+// if (string == nullptr || find == nullptr ||
+// strstr(string, find) == nullptr) {
+// return fit::failed();
+// }
+// return fit::ok();
+// }
+//
+struct failed {};
+
+// Type representing an error value of type E to return as a result. Returning an error through
+// fit::result always requires using fit::error to disambiguate errors from values.
+//
+// fit::result<E, Ts...> is implicitly constructible from any fit::error<F>, where E is
+// constructible from F. This simplifies returning errors when the E has converting constructors.
+//
+// Example usage:
+//
+// fit::result<std::string, size_t> StringLength(const char* string) {
+// if (string == nullptr) {
+// return fit::error("Argument to StringLength is nullptr!");
+// }
+// return fit::success(strlen(string));
+// }
+//
+template <typename E>
+class error {
+ public:
+ using error_type = E;
+
+ // Constructs an error with the given arguments.
+ template <typename... Args,
+ ::fit::internal::requires_conditions<std::is_constructible<E, Args...>> = true>
+ explicit constexpr error(Args&&... args) : value_(std::forward<Args>(args)...) {}
+
+ ~error() = default;
+
+ // Error has the same copyability and moveability as the underlying type E.
+ constexpr error(const error&) = default;
+ constexpr error& operator=(const error&) = default;
+ constexpr error(error&&) = default;
+ constexpr error& operator=(error&&) = default;
+
+ private:
+ template <typename F, typename... Ts>
+ friend class result;
+
+ E value_;
+};
+
+#if __cplusplus >= 201703L
+
+// Deduction guide to simplify single argument error expressions in C++17.
+template <typename T>
+error(T) -> error<T>;
+
+#endif
+
+// Returns fit::error<E> for the given value, where E is deduced from the argument type. This
+// utility is a C++14 compatible alternative to the C++17 deduction guide above.
+//
+// Example:
+//
+// fit::result<std::string, std::string> MakeString(const char* string) {
+// if (string == nullptr) {
+// return fit::as_error("String is nullptr!");
+// } else if (strlen(string) == 0) {
+// return fit::as_error("String is empty!");
+// }
+// return fit::ok(string);
+// }
+//
+template <typename E>
+constexpr error<std::decay_t<E>> as_error(E&& error_value) {
+ return error<std::decay_t<E>>(std::forward<E>(error_value));
+}
+
+// Type representing success with zero or one value.
+//
+// Base type.
+template <typename... Ts>
+class success;
+
+// Type representing a success value of type T to return as a result. Returning a value through
+// fit::result always requires using fit::success to disambiguate errors from values.
+//
+// fit::result<E, T> is implicitly constructible from any fit::success<U>, where T is
+// constructible from U. This simplifies returning values when T has converting constructors.
+template <typename T>
+class success<T> {
+ public:
+ using value_type = T;
+
+ // Constructs a success value with the given arguments.
+ template <typename... Args,
+ ::fit::internal::requires_conditions<std::is_constructible<T, Args...>> = true>
+ explicit constexpr success(Args&&... args) : value_(std::forward<Args>(args)...) {}
+
+ ~success() = default;
+
+ // Error has the same copyability and moveability as the underlying type E.
+ constexpr success(const success&) = default;
+ constexpr success& operator=(const success&) = default;
+ constexpr success(success&&) = default;
+ constexpr success& operator=(success&&) = default;
+
+ private:
+ template <typename E, typename... Ts>
+ friend class result;
+
+ T value_;
+};
+
+// Specialization of success for empty values.
+template <>
+class success<> {
+ public:
+ constexpr success() = default;
+ ~success() = default;
+
+ constexpr success(const success&) = default;
+ constexpr success& operator=(const success&) = default;
+ constexpr success(success&&) = default;
+ constexpr success& operator=(success&&) = default;
+};
+
+#if __cplusplus >= 201703L
+
+// Deduction guides to simplify zero and single argument success expressions in C++17.
+success()->success<>;
+
+template <typename T>
+success(T) -> success<T>;
+
+#endif
+
+// Returns fit::success<T> for the given value, where T is deduced from the argument type. This
+// utility is a C++14 compatible alternative to the C++17 deduction guide above.
+//
+// Example:
+//
+// fit::result<std::string, std::string> MakeString(const char* string) {
+// if (string == nullptr) {
+// return fit::as_error("String is nullptr!");
+// } else if (strlen(string) == 0) {
+// return fit::as_error("String is empty!");
+// }
+// return fit::ok(string);
+// }
+template <typename T>
+constexpr success<std::decay_t<T>> ok(T&& value) {
+ return success<std::decay_t<T>>(std::forward<T>(value));
+}
+
+// Overload for empty value success.
+constexpr success<> ok() { return success<>{}; }
+
+// Result type representing either an error or zero/one return values.
+//
+// Base type.
+template <typename E, typename... Ts>
+class result;
+
+// Specialization of result for one value type.
+template <typename E, typename T>
+class LIB_FIT_NODISCARD result<E, T> {
+ static_assert(!::fit::internal::is_success_v<E>,
+ "fit::success may not be used as the error type of fit::result!");
+ static_assert(!cpp17::is_same_v<failed, std::decay_t<T>>,
+ "fit::failed may not be used as a value type of fit::result!");
+
+ template <typename U>
+ using not_same = cpp17::negation<std::is_same<result, U>>;
+
+ struct none {};
+ using failed_or_none = std::conditional_t<cpp17::is_same_v<failed, E>, failed, none>;
+
+ public:
+ using error_type = E;
+ using value_type = T;
+
+ constexpr result(const result&) = default;
+ constexpr result& operator=(const result&) = default;
+ constexpr result(result&&) = default;
+ constexpr result& operator=(result&&) = default;
+
+ // Implicit conversion from fit::failed. This overload is only enabled when the error type E is
+ // fit::failed.
+ constexpr result(failed_or_none) : storage_{::fit::internal::error_v, failed{}} {}
+
+ // Implicit conversion from success<U>, where T is constructible from U.
+ template <typename U, ::fit::internal::requires_conditions<std::is_constructible<T, U>> = true>
+ constexpr result(success<U> success)
+ : storage_{::fit::internal::value_v, std::move(success.value_)} {}
+
+ // Implicit conversion from error<F>, where E is constructible from F.
+ template <typename F, ::fit::internal::requires_conditions<std::is_constructible<E, F>> = true>
+ constexpr result(error<F> error) : storage_{::fit::internal::error_v, std::move(error.value_)} {}
+
+ // Implicitly constructs a result from another result with compatible types.
+ template <
+ typename F, typename U,
+ ::fit::internal::requires_conditions<not_same<result<F, U>>, std::is_constructible<E, F>,
+ std::is_constructible<T, U>> = true>
+ constexpr result(result<F, U> other) : storage_{std::move(other.storage_)} {}
+
+ // Predicates indicating whether the result contains a value or an error. The positive values are
+ // mutually exclusive, however, both predicates are negative when the result is default
+ // constructed to the empty state.
+ constexpr bool is_ok() const { return storage_.state == ::fit::internal::state_e::has_value; }
+ constexpr bool is_error() const { return storage_.state == ::fit::internal::state_e::has_error; }
+
+ // Accessors for the underlying error.
+ //
+ // May only be called when the result contains an error.
+ constexpr E& error_value() & {
+ if (is_error()) {
+ return storage_.error_or_value.error;
+ }
+ PW_ASSERT(false);
+ }
+ constexpr const E& error_value() const& {
+ if (is_error()) {
+ return storage_.error_or_value.error;
+ }
+ PW_ASSERT(false);
+ }
+ constexpr E&& error_value() && {
+ if (is_error()) {
+ return std::move(storage_.error_or_value.error);
+ }
+ PW_ASSERT(false);
+ }
+ constexpr const E&& error_value() const&& {
+ if (is_error()) {
+ return std::move(storage_.error_or_value.error);
+ }
+ PW_ASSERT(false);
+ }
+
+ // Moves the underlying error and returns it as an instance of fit::error, simplifying
+ // propagating the error to another fit::result.
+ //
+ // May only be called when the result contains an error.
+ constexpr error<E> take_error() {
+ if (is_error()) {
+ return error<E>(std::move(storage_.error_or_value.error));
+ }
+ PW_ASSERT(false);
+ }
+
+ // Accessors for the underlying value.
+ //
+ // May only be called when the result contains a value.
+ constexpr T& value() & {
+ if (is_ok()) {
+ return storage_.error_or_value.value;
+ }
+ PW_ASSERT(false);
+ }
+ constexpr const T& value() const& {
+ if (is_ok()) {
+ return storage_.error_or_value.value;
+ }
+ PW_ASSERT(false);
+ }
+ constexpr T&& value() && {
+ if (is_ok()) {
+ return std::move(storage_.error_or_value.value);
+ }
+ PW_ASSERT(false);
+ }
+ constexpr const T&& value() const&& {
+ if (is_ok()) {
+ return std::move(storage_.error_or_value.value);
+ }
+ PW_ASSERT(false);
+ }
+
+ // Moves the underlying value and returns it as an instance of fit::success, simplifying
+ // propagating the value to another fit::result.
+ //
+ // May only be called when the result contains a value.
+ constexpr success<T> take_value() {
+ if (is_ok()) {
+ return success<T>(std::move(storage_.error_or_value.value));
+ }
+ PW_ASSERT(false);
+ }
+
+ // Contingent accessors for the underlying value.
+ //
+ // Returns the value when the result has a value, otherwise returns the given default value.
+ template <typename U, ::fit::internal::requires_conditions<std::is_constructible<T, U>> = true>
+ constexpr T value_or(U&& default_value) const& {
+ if (is_ok()) {
+ return storage_.error_or_value.value;
+ }
+ return static_cast<T>(std::forward<U>(default_value));
+ }
+ template <typename U, ::fit::internal::requires_conditions<std::is_constructible<T, U>> = true>
+ constexpr T value_or(U&& default_value) && {
+ if (is_ok()) {
+ return std::move(storage_.error_or_value.value);
+ }
+ return static_cast<T>(std::forward<U>(default_value));
+ }
+
+ // Accessors for the members of the underlying value. These operators forward to T::operator->()
+ // when defined, otherwise they provide direct access to T*.
+ //
+ // May only be called when the result contains a value.
+ constexpr decltype(auto) operator->() {
+ if (is_ok()) {
+ return ::fit::internal::arrow_operator<T>::forward(storage_.error_or_value.value);
+ }
+ PW_ASSERT(false);
+ }
+ constexpr decltype(auto) operator->() const {
+ if (is_ok()) {
+ return ::fit::internal::arrow_operator<T>::forward(storage_.error_or_value.value);
+ }
+ PW_ASSERT(false);
+ }
+
+ // Accessors for the underlying value. This is a syntax sugar for value().
+ //
+ // May only be called when the result contains a value.
+ constexpr T& operator*() & { return value(); }
+ constexpr const T& operator*() const& { return value(); }
+ constexpr T&& operator*() && { return std::move(value()); }
+ constexpr const T&& operator*() const&& { return std::move(value()); }
+
+ // Augments the error value of the result with the given error value. The operator
+ // E::operator+=(F) must be defined. Additionally, E may not be a pointer, primitive, or enum
+ // type.
+ //
+ // May only be called when the result contains an error.
+ template <typename F, ::fit::internal::requires_conditions<
+ std::is_class<E>, ::fit::internal::has_plus_equals<E, F>> = true>
+ constexpr result& operator+=(error<F> error) {
+ if (is_error()) {
+ storage_.error_or_value.error += std::move(error.value_);
+ return *this;
+ }
+ PW_ASSERT(false);
+ }
+
+ // Maps a result<E, T> to a result<E2, T> by transforming the error through
+ // fn, where E2 is the result of invoking fn on E. Success values will be
+ // passed through untouched.
+ //
+ // Note that map_error is not necessary if E2 is constructible from E.
+ // In that case, result<E2, T> will be constructible from result<E, T>.
+ //
+ // If the current object is an r-value, errors and successes in the current
+ // result object will be moved.
+ template <typename Fn>
+ constexpr result<std::invoke_result_t<Fn, E>, T> map_error(Fn&& fn) & {
+ if (is_error()) {
+ return error<std::invoke_result_t<Fn, E>>(std::forward<Fn>(fn)(error_value()));
+ }
+ return success<T>(value());
+ }
+ template <typename Fn>
+ constexpr result<std::invoke_result_t<Fn, E>, T> map_error(Fn&& fn) const& {
+ if (is_error()) {
+ return error<std::invoke_result_t<Fn, E>>(std::forward<Fn>(fn)(error_value()));
+ }
+ return success<T>(value());
+ }
+ template <typename Fn>
+ constexpr result<std::invoke_result_t<Fn, E>, T> map_error(Fn&& fn) && {
+ if (is_error()) {
+ return error<std::invoke_result_t<Fn, E>>(std::forward<Fn>(fn)(std::move(error_value())));
+ }
+ return take_value();
+ }
+ template <typename Fn>
+ constexpr result<std::invoke_result_t<Fn, E>, T> map_error(Fn&& fn) const&& {
+ if (is_error()) {
+ return error<std::invoke_result_t<Fn, E>>(std::forward<Fn>(fn)(std::move(error_value())));
+ }
+ return success<T>(value());
+ }
+
+ constexpr void swap(result& other) {
+ if (&other != this) {
+ using std::swap;
+ swap(storage_, other.storage_);
+ }
+ }
+
+ protected:
+ // Default constructs a result in empty state.
+ constexpr result() = default;
+
+ // Reset is not a recommended operation for the general result pattern. This method is provided
+ // for derived types that need it for specific use cases.
+ constexpr void reset() { storage_.reset(); }
+
+ private:
+ template <typename, typename...>
+ friend class result;
+
+ ::fit::internal::storage<E, T> storage_;
+};
+
+// Specialization of the result type for zero values.
+template <typename E>
+class LIB_FIT_NODISCARD result<E> {
+ static_assert(!::fit::internal::is_success_v<E>,
+ "fit::success may not be used as the error type of fit::result!");
+
+ template <typename U>
+ using not_same = cpp17::negation<std::is_same<result, U>>;
+
+ template <size_t>
+ struct none {};
+ using failure_or_none = std::conditional_t<cpp17::is_same_v<failed, E>, failed, none<1>>;
+
+ public:
+ using error_type = E;
+
+ constexpr result(const result&) = default;
+ constexpr result& operator=(const result&) = default;
+ constexpr result(result&&) = default;
+ constexpr result& operator=(result&&) = default;
+
+ // Implicit conversion from fit::failure. This overload is only enabled when the error type E is
+ // fit::failed.
+ constexpr result(failure_or_none) : storage_{::fit::internal::error_v, failed{}} {}
+
+ // Implicit conversion from fit::success<>.
+ constexpr result(success<>) : storage_{::fit::internal::value_v} {}
+
+ // Implicit conversion from error<F>, where E is constructible from F.
+ template <typename F, ::fit::internal::requires_conditions<std::is_constructible<E, F>> = true>
+ constexpr result(error<F> error) : storage_{::fit::internal::error_v, std::move(error.value_)} {}
+
+ // Implicitly constructs a result from another result with compatible types.
+ template <typename F, ::fit::internal::requires_conditions<not_same<result<F>>,
+ std::is_constructible<E, F>> = true>
+ constexpr result(result<F> other) : storage_{std::move(other.storage_)} {}
+
+ // Predicates indicating whether the result contains a value or an error.
+ constexpr bool is_ok() const { return storage_.state == ::fit::internal::state_e::has_value; }
+ constexpr bool is_error() const { return storage_.state == ::fit::internal::state_e::has_error; }
+
+ // Accessors for the underlying error.
+ //
+ // May only be called when the result contains an error.
+ constexpr E& error_value() & {
+ if (is_error()) {
+ return storage_.error_or_value.error;
+ }
+ PW_ASSERT(false);
+ }
+ constexpr const E& error_value() const& {
+ if (is_error()) {
+ return storage_.error_or_value.error;
+ }
+ PW_ASSERT(false);
+ }
+ constexpr E&& error_value() && {
+ if (is_error()) {
+ return std::move(storage_.error_or_value.error);
+ }
+ PW_ASSERT(false);
+ }
+ constexpr const E&& error_value() const&& {
+ if (is_error()) {
+ return std::move(storage_.error_or_value.error);
+ }
+ PW_ASSERT(false);
+ }
+
+ // Moves the underlying error and returns it as an instance of fit::error, simplifying
+ // propagating the error to another fit::result.
+ //
+ // May only be called when the result contains an error.
+ constexpr error<E> take_error() {
+ if (is_error()) {
+ return error<E>(std::move(storage_.error_or_value.error));
+ }
+ PW_ASSERT(false);
+ }
+
+ // Augments the error value of the result with the given value. The operator E::operator+=(F) must
+ // be defined. Additionally, E may not be a pointer, primitive, or enum type.
+ //
+ // May only be called when the result contains an error.
+ template <typename F, ::fit::internal::requires_conditions<
+ std::is_class<E>, ::fit::internal::has_plus_equals<E, F>> = true>
+ constexpr result& operator+=(error<F> error) {
+ if (is_error()) {
+ storage_.error_or_value.error += std::move(error.value_);
+ return *this;
+ }
+ PW_ASSERT(false);
+ }
+
+ // Maps a result<E, T> to a result<E2, T> by transforming the error through
+ // fn, where E2 is the result of invoking fn on E. Success values will be
+ // passed through untouched.
+ //
+ // Note that map_error is not necessary if E2 is constructible from E.
+ // In that case, result<E2, T> will be constructible from result<E, T>.
+ //
+ // If the current object is an r-value, errors in the current result object
+ // will be moved.
+ template <typename Fn>
+ constexpr result<std::invoke_result_t<Fn, E>> map_error(Fn&& fn) & {
+ if (is_error()) {
+ return error<std::invoke_result_t<Fn, E>>(std::forward<Fn>(fn)(error_value()));
+ }
+ return success<>();
+ }
+ template <typename Fn>
+ constexpr result<std::invoke_result_t<Fn, E>> map_error(Fn&& fn) const& {
+ if (is_error()) {
+ return error<std::invoke_result_t<Fn, E>>(std::forward<Fn>(fn)(error_value()));
+ }
+ return success<>();
+ }
+ template <typename Fn>
+ constexpr result<std::invoke_result_t<Fn, E>> map_error(Fn&& fn) && {
+ if (is_error()) {
+ return error<std::invoke_result_t<Fn, E>>(std::forward<Fn>(fn)(std::move(error_value())));
+ }
+ return success<>();
+ }
+ template <typename Fn>
+ constexpr result<std::invoke_result_t<Fn, E>> map_error(Fn&& fn) const&& {
+ if (is_error()) {
+ return error<std::invoke_result_t<Fn, E>>(std::forward<Fn>(fn)(std::move(error_value())));
+ }
+ return success<>();
+ }
+
+ constexpr void swap(result& other) {
+ if (&other != this) {
+ using std::swap;
+ swap(storage_, other.storage_);
+ }
+ }
+
+ protected:
+ // Default constructs a result in empty state.
+ constexpr result() = default;
+
+ // Reset is not a recommended operation for the general result pattern. This method is provided
+ // for derived types that need it for specific use cases.
+ constexpr void reset() { storage_.reset(); }
+
+ private:
+ template <typename, typename...>
+ friend class result;
+
+ ::fit::internal::storage<E> storage_;
+};
+
+template <typename E, typename... Ts>
+constexpr void swap(result<E, Ts...>& r, result<E, Ts...>& s) {
+ return r.swap(s);
+}
+
+// Relational Operators.
+//
+// Results are comparable to the follownig types:
+// * Other results with the same arity when the value types are comparable.
+// * Any type that is comparable to the value type when the arity is 1.
+// * Any instance of fit::success<> (i.e. fit::ok()).
+// * Any instance of fit::failed.
+//
+// Result comparisons behave similarly to std::optional<T>, having the same empty and non-empty
+// lexicographic ordering. A non-value result behaves like an empty std::optional, regardless of the
+// value of the actual error. Error values are never compared, only the is_ok() predicate and result
+// values are considered in comparisons.
+
+// Equal/not equal to fit::success.
+template <typename E, typename... Ts>
+constexpr bool operator==(const result<E, Ts...>& lhs, const success<>&) {
+ return lhs.is_ok();
+}
+template <typename E, typename... Ts>
+constexpr bool operator!=(const result<E, Ts...>& lhs, const success<>&) {
+ return !lhs.is_ok();
+}
+
+template <typename E, typename... Ts>
+constexpr bool operator==(const success<>&, const result<E, Ts...>& rhs) {
+ return rhs.is_ok();
+}
+template <typename E, typename... Ts>
+constexpr bool operator!=(const success<>&, const result<E, Ts...>& rhs) {
+ return !rhs.is_ok();
+}
+
+// Equal/not equal to fit::failed.
+template <typename E, typename... Ts>
+constexpr bool operator==(const result<E, Ts...>& lhs, failed) {
+ return lhs.is_error();
+}
+template <typename E, typename... Ts>
+constexpr bool operator!=(const result<E, Ts...>& lhs, failed) {
+ return !lhs.is_error();
+}
+
+template <typename E, typename... Ts>
+constexpr bool operator==(failed, const result<E, Ts...>& rhs) {
+ return rhs.is_error();
+}
+template <typename E, typename... Ts>
+constexpr bool operator!=(failed, const result<E, Ts...>& rhs) {
+ return !rhs.is_error();
+}
+
+// Equal/not equal.
+template <typename E, typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() == std::declval<U>())> = true>
+constexpr bool operator==(const result<E, T>& lhs, const result<F, U>& rhs) {
+ return (lhs.is_ok() == rhs.is_ok()) && (!lhs.is_ok() || lhs.value() == rhs.value());
+}
+template <typename E, typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() != std::declval<U>())> = true>
+constexpr bool operator!=(const result<E, T>& lhs, const result<F, U>& rhs) {
+ return (lhs.is_ok() != rhs.is_ok()) || (lhs.is_ok() && lhs.value() != rhs.value());
+}
+
+template <typename E, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() == std::declval<U>()),
+ ::fit::internal::not_result_type<U>> = true>
+constexpr bool operator==(const result<E, T>& lhs, const U& rhs) {
+ return lhs.is_ok() && lhs.value() == rhs;
+}
+template <typename E, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() != std::declval<U>()),
+ ::fit::internal::not_result_type<U>> = true>
+constexpr bool operator!=(const result<E, T>& lhs, const U& rhs) {
+ return !lhs.is_ok() || lhs.value() != rhs;
+}
+
+template <typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() == std::declval<U>()),
+ ::fit::internal::not_result_type<T>> = true>
+constexpr bool operator==(const T& lhs, const result<F, U>& rhs) {
+ return rhs.is_ok() && lhs == rhs.value();
+}
+template <typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() != std::declval<U>()),
+ ::fit::internal::not_result_type<T>> = true>
+constexpr bool operator!=(const T& lhs, const result<F, U>& rhs) {
+ return !rhs.is_ok() || lhs != rhs.value();
+}
+
+// Less than/greater than.
+template <typename E, typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() < std::declval<U>())> = true>
+constexpr bool operator<(const result<E, T>& lhs, const result<F, U>& rhs) {
+ return rhs.is_ok() && (!lhs.is_ok() || lhs.value() < rhs.value());
+}
+template <typename E, typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() > std::declval<U>())> = true>
+constexpr bool operator>(const result<E, T>& lhs, const result<F, U>& rhs) {
+ return lhs.is_ok() && (!rhs.is_ok() || lhs.value() > rhs.value());
+}
+
+template <typename E, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() < std::declval<U>()),
+ ::fit::internal::not_result_type<U>> = true>
+constexpr bool operator<(const result<E, T>& lhs, const U& rhs) {
+ return !lhs.is_ok() || lhs.value() < rhs;
+}
+template <typename E, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() > std::declval<U>()),
+ ::fit::internal::not_result_type<U>> = true>
+constexpr bool operator>(const result<E, T>& lhs, const U& rhs) {
+ return lhs.is_ok() && lhs.value() > rhs;
+}
+
+template <typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() < std::declval<U>()),
+ ::fit::internal::not_result_type<T>> = true>
+constexpr bool operator<(const T& lhs, const result<F, U>& rhs) {
+ return rhs.is_ok() && lhs < rhs.value();
+}
+template <typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() > std::declval<U>()),
+ ::fit::internal::not_result_type<T>> = true>
+constexpr bool operator>(const T& lhs, const result<F, U>& rhs) {
+ return !rhs.is_ok() || lhs > rhs.value();
+}
+
+// Less than or equal/greater than or equal.
+template <typename E, typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() <= std::declval<U>())> = true>
+constexpr bool operator<=(const result<E, T>& lhs, const result<F, U>& rhs) {
+ return !lhs.is_ok() || (rhs.is_ok() && lhs.value() <= rhs.value());
+}
+template <typename E, typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() >= std::declval<U>())> = true>
+constexpr bool operator>=(const result<E, T>& lhs, const result<F, U>& rhs) {
+ return !rhs.is_ok() || (lhs.is_ok() && lhs.value() >= rhs.value());
+}
+
+template <typename E, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() <= std::declval<U>()),
+ ::fit::internal::not_result_type<U>> = true>
+constexpr bool operator<=(const result<E, T>& lhs, const U& rhs) {
+ return !lhs.is_ok() || lhs.value() <= rhs;
+}
+template <typename E, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() >= std::declval<U>()),
+ ::fit::internal::not_result_type<U>> = true>
+constexpr bool operator>=(const result<E, T>& lhs, const U& rhs) {
+ return lhs.is_ok() && lhs.value() >= rhs;
+}
+
+template <typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() <= std::declval<U>()),
+ ::fit::internal::not_result_type<T>> = true>
+constexpr bool operator<=(const T& lhs, const result<F, U>& rhs) {
+ return rhs.is_ok() && lhs <= rhs.value();
+}
+template <typename F, typename T, typename U,
+ ::fit::internal::enable_rel_op<decltype(std::declval<T>() >= std::declval<U>()),
+ ::fit::internal::not_result_type<T>> = true>
+constexpr bool operator>=(const T& lhs, const result<F, U>& rhs) {
+ return !rhs.is_ok() || lhs >= rhs.value();
+}
+
+} // namespace fit
+
+#endif // LIB_FIT_INCLUDE_LIB_FIT_RESULT_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h
new file mode 100644
index 000000000..916a47507
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/include/lib/fit/traits.h
@@ -0,0 +1,181 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_FIT_INCLUDE_LIB_FIT_TRAITS_H_
+#define LIB_FIT_INCLUDE_LIB_FIT_TRAITS_H_
+
+#include <lib/stdcompat/type_traits.h>
+
+#include <tuple>
+#include <type_traits>
+
+namespace fit {
+
+// Encapsulates capture of a parameter pack. Typical use is to use instances of this empty struct
+// for type dispatch in function template deduction/overload resolution.
+//
+// Example:
+// template <typename Callable, typename... Args>
+// auto inspect_args(Callable c, parameter_pack<Args...>) {
+// // do something with Args...
+// }
+//
+// template <typename Callable>
+// auto inspect_args(Callable c) {
+// return inspect_args(std::move(c), typename callable_traits<Callable>::args{});
+// }
+template <typename... T>
+struct parameter_pack {
+ static constexpr size_t size = sizeof...(T);
+
+ template <size_t i>
+ using at = typename std::tuple_element_t<i, std::tuple<T...>>;
+};
+
+// |callable_traits| captures elements of interest from function-like types (functions, function
+// pointers, and functors, including lambdas). Due to common usage patterns, const and non-const
+// functors are treated identically.
+//
+// Member types:
+// |args| - a |parameter_pack| that captures the parameter types of the function. See
+// |parameter_pack| for usage and details.
+// |return_type| - the return type of the function.
+// |type| - the underlying functor or function pointer type. This member is absent if
+// |callable_traits| are requested for a raw function signature (as opposed to a
+// function pointer or functor; e.g. |callable_traits<void()>|).
+// |signature| - the type of the equivalent function.
+
+template <typename T>
+struct callable_traits : public callable_traits<decltype(&T::operator())> {};
+
+// Treat mutable call operators the same as const call operators.
+//
+// It would be equivalent to erase the const instead, but the common case is lambdas, which are
+// const, so prefer to nest less deeply for the common const case.
+template <typename FunctorType, typename ReturnType, typename... ArgTypes>
+struct callable_traits<ReturnType (FunctorType::*)(ArgTypes...)>
+ : public callable_traits<ReturnType (FunctorType::*)(ArgTypes...) const> {};
+
+// Common functor specialization.
+template <typename FunctorType, typename ReturnType, typename... ArgTypes>
+struct callable_traits<ReturnType (FunctorType::*)(ArgTypes...) const>
+ : public callable_traits<ReturnType (*)(ArgTypes...)> {
+ using type = FunctorType;
+};
+
+// Function pointer specialization.
+template <typename ReturnType, typename... ArgTypes>
+struct callable_traits<ReturnType (*)(ArgTypes...)>
+ : public callable_traits<ReturnType(ArgTypes...)> {
+ using type = ReturnType (*)(ArgTypes...);
+};
+
+// Base specialization.
+template <typename ReturnType, typename... ArgTypes>
+struct callable_traits<ReturnType(ArgTypes...)> {
+ using signature = ReturnType(ArgTypes...);
+ using return_type = ReturnType;
+ using args = parameter_pack<ArgTypes...>;
+
+ callable_traits() = delete;
+};
+
+// Determines whether a type has an operator() that can be invoked.
+template <typename T, typename = cpp17::void_t<>>
+struct is_callable : public std::false_type {};
+template <typename ReturnType, typename... ArgTypes>
+struct is_callable<ReturnType (*)(ArgTypes...)> : public std::true_type {};
+template <typename FunctorType, typename ReturnType, typename... ArgTypes>
+struct is_callable<ReturnType (FunctorType::*)(ArgTypes...)> : public std::true_type {};
+template <typename T>
+struct is_callable<T, cpp17::void_t<decltype(&T::operator())>> : public std::true_type {};
+
+namespace internal {
+
+template <typename Default, typename AlwaysVoid, template <typename...> class Op, typename... Args>
+struct detector {
+ using value_t = std::false_type;
+ using type = Default;
+};
+
+template <typename Default, template <typename...> class Op, typename... Args>
+struct detector<Default, cpp17::void_t<Op<Args...>>, Op, Args...> {
+ using value_t = std::true_type;
+ using type = Op<Args...>;
+};
+
+} // namespace internal
+
+// Default type when detection fails; |void| could be a legitimate result so we need something else.
+struct nonesuch {
+ constexpr nonesuch() = delete;
+ constexpr nonesuch(const nonesuch&) = delete;
+ constexpr nonesuch& operator=(const nonesuch&) = delete;
+ constexpr nonesuch(nonesuch&&) = delete;
+ constexpr nonesuch& operator=(nonesuch&&) = delete;
+
+ // This ensures that no one can actually make a value of this type.
+ ~nonesuch() = delete;
+};
+
+// Trait for detecting if |Op<Args...>| resolves to a legitimate type. Without this trait,
+// metaprogrammers often resort to making their own detectors with |void_t<>|.
+//
+// With this trait, they can simply make the core part of the detector, like this to detect an
+// |operator==|:
+//
+// template <typename T>
+// using equality_t = decltype(std::declval<const T&>() == std::declval<const T&>());
+//
+// And then make their detector like so:
+//
+// template <typename T>
+// using has_equality = fit::is_detected<equality_t, T>;
+template <template <typename...> class Op, typename... Args>
+using is_detected = typename ::fit::internal::detector<nonesuch, void, Op, Args...>::value_t;
+
+template <template <typename...> class Op, typename... Args>
+constexpr bool is_detected_v = is_detected<Op, Args...>::value;
+
+// Trait for accessing the result of |Op<Args...>| if it exists, or |fit::nonesuch| otherwise.
+//
+// This is advantageous because the same "core" of the detector can be used both for finding if the
+// prospective type exists at all (with |fit::is_detected|) and what it actually is.
+template <template <typename...> class Op, typename... Args>
+using detected_t = typename ::fit::internal::detector<nonesuch, void, Op, Args...>::type;
+
+// Trait for detecting whether |Op<Args...>| exists (via |::value_t|, which is either
+// |std::true_type| or |std::false_type|) and accessing its type via |::type| if so, which will
+// otherwise be |Default|.
+template <typename Default, template <typename...> class Op, typename... Args>
+using detected_or = typename ::fit::internal::detector<Default, void, Op, Args...>;
+
+// Essentially the same as |fit::detected_t| but with a user-specified default instead of
+// |fit::nonesuch|.
+template <typename Default, template <typename...> class Op, typename... Args>
+using detected_or_t = typename detected_or<Default, Op, Args...>::type;
+
+// Trait for detecting whether |Op<Args...>| exists and is exactly the type |Expected|.
+//
+// To reuse the previous example of |operator==| and |equality_t|:
+//
+// template <typename T>
+// using has_equality_exact = fit::is_detected_exact<bool, equality_t, T>;
+template <typename Expected, template <typename...> class Op, typename... Args>
+using is_detected_exact = std::is_same<Expected, detected_t<Op, Args...>>;
+
+template <typename Expected, template <typename...> class Op, typename... Args>
+constexpr bool is_detected_exact_v = is_detected_exact<Expected, Op, Args...>::value;
+
+// Essentially the same as |fit::is_detected_exact| but tests for convertibility instead of exact
+// sameness.
+template <typename To, template <typename...> class Op, typename... Args>
+using is_detected_convertible = std::is_convertible<detected_t<Op, Args...>, To>;
+
+template <typename To, template <typename...> class Op, typename... Args>
+constexpr bool is_detected_convertible_v = is_detected_convertible<To, Op, Args...>::value;
+
+} // namespace fit
+
+#endif // LIB_FIT_INCLUDE_LIB_FIT_TRAITS_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc b/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc
new file mode 100644
index 000000000..c2a220002
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/fit/test/function_tests.cc
@@ -0,0 +1,1092 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <lib/fit/function.h>
+#include <lib/stdcompat/bit.h>
+
+#include <algorithm>
+
+#include "gtest/gtest.h"
+
+namespace {
+
+using Closure = void();
+using ClosureWrongReturnType = int();
+using BinaryOp = int(int a, int b);
+using BinaryOpWrongReturnType = void(int a, int b);
+using MoveOp = std::unique_ptr<int>(std::unique_ptr<int> value);
+using BooleanGenerator = bool();
+using IntGenerator = int();
+
+class BuildableFromInt {
+ public:
+ BuildableFromInt(int);
+ BuildableFromInt& operator=(int);
+};
+
+using BuildableFromIntGenerator = BuildableFromInt();
+
+// A big object which causes a function target to be heap allocated.
+struct Big {
+ int data[64]{};
+};
+// An object with a very large alignment requirement that cannot be placed in a
+// fit::inline_function.
+struct alignas(64) BigAlignment {
+ int data[64]{};
+};
+constexpr size_t HugeCallableSize = sizeof(Big) + sizeof(void*) * 4;
+
+// An object that looks like an "empty" std::function.
+template <typename>
+struct EmptyFunction;
+template <typename R, typename... Args>
+struct EmptyFunction<R(Args...)> {
+ R operator()(Args... args) const { return fptr(args...); }
+ bool operator==(decltype(nullptr)) const { return true; }
+
+ R(*fptr)
+ (Args...) = nullptr;
+};
+
+// An object whose state we can examine from the outside.
+struct SlotMachine {
+ void operator()() { value++; }
+ int operator()(int a, int b) {
+ value += a * b;
+ return value;
+ }
+
+ int value = 0;
+};
+
+// A move-only object which increments a counter when uniquely destroyed.
+class DestructionObserver {
+ public:
+ DestructionObserver(int* counter) : counter_(counter) {}
+ DestructionObserver(DestructionObserver&& other) : counter_(other.counter_) {
+ other.counter_ = nullptr;
+ }
+ DestructionObserver(const DestructionObserver& other) = delete;
+
+ ~DestructionObserver() {
+ if (counter_)
+ *counter_ += 1;
+ }
+
+ DestructionObserver& operator=(const DestructionObserver& other) = delete;
+ DestructionObserver& operator=(DestructionObserver&& other) {
+ if (counter_)
+ *counter_ += 1;
+ counter_ = other.counter_;
+ other.counter_ = nullptr;
+ return *this;
+ }
+
+ private:
+ int* counter_;
+};
+
+template <typename ClosureFunction>
+void closure() {
+ static_assert(fit::is_nullable<ClosureFunction>::value, "");
+
+ // default initialization
+ ClosureFunction fdefault;
+ EXPECT_FALSE(!!fdefault);
+
+ // nullptr initialization
+ ClosureFunction fnull(nullptr);
+ EXPECT_FALSE(!!fnull);
+
+ // null function pointer initialization
+ Closure* fptr = nullptr;
+ ClosureFunction ffunc(fptr);
+ EXPECT_FALSE(!!ffunc);
+
+ // "empty std::function" initialization
+ EmptyFunction<Closure> empty;
+ ClosureFunction fwrapper(empty);
+ EXPECT_FALSE(!!fwrapper);
+
+ // inline callable initialization
+ int finline_value = 0;
+ ClosureFunction finline([&finline_value] { finline_value++; });
+ EXPECT_TRUE(!!finline);
+ finline();
+ EXPECT_EQ(1, finline_value);
+ finline();
+ EXPECT_EQ(2, finline_value);
+
+ // heap callable initialization
+ int fheap_value = 0;
+ ClosureFunction fheap([&fheap_value, big = Big()] { fheap_value++; });
+ EXPECT_TRUE(!!fheap);
+ fheap();
+ EXPECT_EQ(1, fheap_value);
+ fheap();
+ EXPECT_EQ(2, fheap_value);
+
+ // move initialization of a nullptr
+ ClosureFunction fnull2(std::move(fnull));
+ EXPECT_FALSE(!!fnull2);
+
+ // move initialization of an inline callable
+ ClosureFunction finline2(std::move(finline));
+ EXPECT_TRUE(!!finline2);
+ EXPECT_FALSE(!!finline);
+ finline2();
+ EXPECT_EQ(3, finline_value);
+ finline2();
+ EXPECT_EQ(4, finline_value);
+
+ // move initialization of a heap callable
+ ClosureFunction fheap2(std::move(fheap));
+ EXPECT_TRUE(!!fheap2);
+ EXPECT_FALSE(!!fheap);
+ fheap2();
+ EXPECT_EQ(3, fheap_value);
+ fheap2();
+ EXPECT_EQ(4, fheap_value);
+
+ // inline mutable lambda
+ int fmutinline_value = 0;
+ ClosureFunction fmutinline([&fmutinline_value, x = 1]() mutable {
+ x *= 2;
+ fmutinline_value = x;
+ });
+ EXPECT_TRUE(!!fmutinline);
+ fmutinline();
+ EXPECT_EQ(2, fmutinline_value);
+ fmutinline();
+ EXPECT_EQ(4, fmutinline_value);
+
+ // heap-allocated mutable lambda
+ int fmutheap_value = 0;
+ ClosureFunction fmutheap([&fmutheap_value, big = Big(), x = 1]() mutable {
+ x *= 2;
+ fmutheap_value = x;
+ });
+ EXPECT_TRUE(!!fmutheap);
+ fmutheap();
+ EXPECT_EQ(2, fmutheap_value);
+ fmutheap();
+ EXPECT_EQ(4, fmutheap_value);
+
+ // move assignment of non-null
+ ClosureFunction fnew([] {});
+ fnew = std::move(finline2);
+ EXPECT_TRUE(!!fnew);
+ fnew();
+ EXPECT_EQ(5, finline_value);
+ fnew();
+ EXPECT_EQ(6, finline_value);
+
+ // move assignment of self
+ fnew = std::move(fnew);
+ EXPECT_TRUE(!!fnew);
+ fnew();
+ EXPECT_EQ(7, finline_value);
+
+ // move assignment of null
+ fnew = std::move(fnull);
+ EXPECT_FALSE(!!fnew);
+
+ // callable assignment with operator=
+ int fnew_value = 0;
+ fnew = [&fnew_value] { fnew_value++; };
+ EXPECT_TRUE(!!fnew);
+ fnew();
+ EXPECT_EQ(1, fnew_value);
+ fnew();
+ EXPECT_EQ(2, fnew_value);
+
+ // nullptr assignment
+ fnew = nullptr;
+ EXPECT_FALSE(!!fnew);
+
+ // swap (currently null)
+ swap(fnew, fheap2);
+ EXPECT_TRUE(!!fnew);
+ EXPECT_FALSE(!!fheap);
+ fnew();
+ EXPECT_EQ(5, fheap_value);
+ fnew();
+ EXPECT_EQ(6, fheap_value);
+
+ // swap with self
+ swap(fnew, fnew);
+ EXPECT_TRUE(!!fnew);
+ fnew();
+ EXPECT_EQ(7, fheap_value);
+ fnew();
+ EXPECT_EQ(8, fheap_value);
+
+ // swap with non-null
+ swap(fnew, fmutinline);
+ EXPECT_TRUE(!!fmutinline);
+ EXPECT_TRUE(!!fnew);
+ fmutinline();
+ EXPECT_EQ(9, fheap_value);
+ fmutinline();
+ EXPECT_EQ(10, fheap_value);
+ fnew();
+ EXPECT_EQ(8, fmutinline_value);
+ fnew();
+ EXPECT_EQ(16, fmutinline_value);
+
+ // nullptr comparison operators
+ EXPECT_TRUE(fnull == nullptr);
+ EXPECT_FALSE(fnull != nullptr);
+ EXPECT_TRUE(nullptr == fnull);
+ EXPECT_FALSE(nullptr != fnull);
+ EXPECT_FALSE(fnew == nullptr);
+ EXPECT_TRUE(fnew != nullptr);
+ EXPECT_FALSE(nullptr == fnew);
+ EXPECT_TRUE(nullptr != fnew);
+
+ // null function pointer assignment
+ fnew = fptr;
+ EXPECT_FALSE(!!fnew);
+
+ // "empty std::function" assignment
+ fmutinline = empty;
+ EXPECT_FALSE(!!fmutinline);
+
+ // target access
+ ClosureFunction fslot;
+ EXPECT_NULL(fslot.template target<decltype(nullptr)>());
+ fslot = SlotMachine{42};
+ fslot();
+ SlotMachine* fslottarget = fslot.template target<SlotMachine>();
+ EXPECT_EQ(43, fslottarget->value);
+ const SlotMachine* fslottargetconst =
+ const_cast<const ClosureFunction&>(fslot).template target<SlotMachine>();
+ EXPECT_EQ(fslottarget, fslottargetconst);
+ fslot = nullptr;
+ EXPECT_NULL(fslot.template target<decltype(nullptr)>());
+}
+
+template <typename BinaryOpFunction>
+void binary_op() {
+ static_assert(fit::is_nullable<BinaryOpFunction>::value, "");
+
+ // default initialization
+ BinaryOpFunction fdefault;
+ EXPECT_FALSE(!!fdefault);
+
+ // nullptr initialization
+ BinaryOpFunction fnull(nullptr);
+ EXPECT_FALSE(!!fnull);
+
+ // null function pointer initialization
+ BinaryOp* fptr = nullptr;
+ BinaryOpFunction ffunc(fptr);
+ EXPECT_FALSE(!!ffunc);
+
+ // "empty std::function" initialization
+ EmptyFunction<BinaryOp> empty;
+ BinaryOpFunction fwrapper(empty);
+ EXPECT_FALSE(!!fwrapper);
+
+ // inline callable initialization
+ int finline_value = 0;
+ BinaryOpFunction finline([&finline_value](int a, int b) {
+ finline_value++;
+ return a + b;
+ });
+ EXPECT_TRUE(!!finline);
+ EXPECT_EQ(10, finline(3, 7));
+ EXPECT_EQ(1, finline_value);
+ EXPECT_EQ(10, finline(3, 7));
+ EXPECT_EQ(2, finline_value);
+
+ // heap callable initialization
+ int fheap_value = 0;
+ BinaryOpFunction fheap([&fheap_value, big = Big()](int a, int b) {
+ fheap_value++;
+ return a + b;
+ });
+ EXPECT_TRUE(!!fheap);
+ EXPECT_EQ(10, fheap(3, 7));
+ EXPECT_EQ(1, fheap_value);
+ EXPECT_EQ(10, fheap(3, 7));
+ EXPECT_EQ(2, fheap_value);
+
+ // move initialization of a nullptr
+ BinaryOpFunction fnull2(std::move(fnull));
+ EXPECT_FALSE(!!fnull2);
+
+ // move initialization of an inline callable
+ BinaryOpFunction finline2(std::move(finline));
+ EXPECT_TRUE(!!finline2);
+ EXPECT_FALSE(!!finline);
+ EXPECT_EQ(10, finline2(3, 7));
+ EXPECT_EQ(3, finline_value);
+ EXPECT_EQ(10, finline2(3, 7));
+ EXPECT_EQ(4, finline_value);
+
+ // move initialization of a heap callable
+ BinaryOpFunction fheap2(std::move(fheap));
+ EXPECT_TRUE(!!fheap2);
+ EXPECT_FALSE(!!fheap);
+ EXPECT_EQ(10, fheap2(3, 7));
+ EXPECT_EQ(3, fheap_value);
+ EXPECT_EQ(10, fheap2(3, 7));
+ EXPECT_EQ(4, fheap_value);
+
+ // inline mutable lambda
+ int fmutinline_value = 0;
+ BinaryOpFunction fmutinline([&fmutinline_value, x = 1](int a, int b) mutable {
+ x *= 2;
+ fmutinline_value = x;
+ return a + b;
+ });
+ EXPECT_TRUE(!!fmutinline);
+ EXPECT_EQ(10, fmutinline(3, 7));
+ EXPECT_EQ(2, fmutinline_value);
+ EXPECT_EQ(10, fmutinline(3, 7));
+ EXPECT_EQ(4, fmutinline_value);
+
+ // heap-allocated mutable lambda
+ int fmutheap_value = 0;
+ BinaryOpFunction fmutheap([&fmutheap_value, big = Big(), x = 1](int a, int b) mutable {
+ x *= 2;
+ fmutheap_value = x;
+ return a + b;
+ });
+ EXPECT_TRUE(!!fmutheap);
+ EXPECT_EQ(10, fmutheap(3, 7));
+ EXPECT_EQ(2, fmutheap_value);
+ EXPECT_EQ(10, fmutheap(3, 7));
+ EXPECT_EQ(4, fmutheap_value);
+
+ // move assignment of non-null
+ BinaryOpFunction fnew([](int a, int b) { return 0; });
+ fnew = std::move(finline2);
+ EXPECT_TRUE(!!fnew);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(5, finline_value);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(6, finline_value);
+
+ // self-assignment of non-null
+ fnew = std::move(fnew);
+ EXPECT_TRUE(!!fnew);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(7, finline_value);
+
+ // move assignment of null
+ fnew = std::move(fnull);
+ EXPECT_FALSE(!!fnew);
+
+ // self-assignment of non-null
+ fnew = std::move(fnew);
+ EXPECT_FALSE(!!fnew);
+
+ // callable assignment with operator=
+ int fnew_value = 0;
+ fnew = [&fnew_value](int a, int b) {
+ fnew_value++;
+ return a + b;
+ };
+ EXPECT_TRUE(!!fnew);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(1, fnew_value);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(2, fnew_value);
+
+ // nullptr assignment
+ fnew = nullptr;
+ EXPECT_FALSE(!!fnew);
+
+ // swap (currently null)
+ swap(fnew, fheap2);
+ EXPECT_TRUE(!!fnew);
+ EXPECT_FALSE(!!fheap);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(5, fheap_value);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(6, fheap_value);
+
+ // swap with self
+ swap(fnew, fnew);
+ EXPECT_TRUE(!!fnew);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(7, fheap_value);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(8, fheap_value);
+
+ // swap with non-null
+ swap(fnew, fmutinline);
+ EXPECT_TRUE(!!fmutinline);
+ EXPECT_TRUE(!!fnew);
+ EXPECT_EQ(10, fmutinline(3, 7));
+ EXPECT_EQ(9, fheap_value);
+ EXPECT_EQ(10, fmutinline(3, 7));
+ EXPECT_EQ(10, fheap_value);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(8, fmutinline_value);
+ EXPECT_EQ(10, fnew(3, 7));
+ EXPECT_EQ(16, fmutinline_value);
+
+ // nullptr comparison operators
+ EXPECT_TRUE(fnull == nullptr);
+ EXPECT_FALSE(fnull != nullptr);
+ EXPECT_TRUE(nullptr == fnull);
+ EXPECT_FALSE(nullptr != fnull);
+ EXPECT_FALSE(fnew == nullptr);
+ EXPECT_TRUE(fnew != nullptr);
+ EXPECT_FALSE(nullptr == fnew);
+ EXPECT_TRUE(nullptr != fnew);
+
+ // null function pointer assignment
+ fnew = fptr;
+ EXPECT_FALSE(!!fnew);
+
+ // "empty std::function" assignment
+ fmutinline = empty;
+ EXPECT_FALSE(!!fmutinline);
+
+ // target access
+ BinaryOpFunction fslot;
+ EXPECT_NULL(fslot.template target<decltype(nullptr)>());
+ fslot = SlotMachine{42};
+ EXPECT_EQ(54, fslot(3, 4));
+ SlotMachine* fslottarget = fslot.template target<SlotMachine>();
+ EXPECT_EQ(54, fslottarget->value);
+ const SlotMachine* fslottargetconst =
+ const_cast<const BinaryOpFunction&>(fslot).template target<SlotMachine>();
+ EXPECT_EQ(fslottarget, fslottargetconst);
+ fslot = nullptr;
+ EXPECT_NULL(fslot.template target<decltype(nullptr)>());
+}
+
+TEST(FunctionTests, sized_function_size_bounds) {
+ auto empty = [] {};
+ fit::function<Closure, sizeof(empty)> fempty(std::move(empty));
+ static_assert(sizeof(fempty) >= sizeof(empty), "size bounds");
+
+ auto small = [x = 1, y = 2] {
+ (void)x; // suppress unused lambda capture warning
+ (void)y;
+ };
+ fit::function<Closure, sizeof(small)> fsmall(std::move(small));
+ static_assert(sizeof(fsmall) >= sizeof(small), "size bounds");
+ fsmall = [] {};
+
+ auto big = [big = Big(), x = 1] { (void)x; };
+ fit::function<Closure, sizeof(big)> fbig(std::move(big));
+ static_assert(sizeof(fbig) >= sizeof(big), "size bounds");
+ fbig = [x = 1, y = 2] {
+ (void)x;
+ (void)y;
+ };
+ fbig = [] {};
+
+ // These statements do compile though the lambda will be copied to the heap
+ // when they exceed the inline size.
+ fempty = [x = 1, y = 2] {
+ (void)x;
+ (void)y;
+ };
+ fsmall = [big = Big(), x = 1] { (void)x; };
+ fbig = [big = Big(), x = 1, y = 2] {
+ (void)x;
+ (void)y;
+ };
+}
+
+TEST(FunctionTests, inline_function_size_bounds) {
+ auto empty = [] {};
+ fit::inline_function<Closure, sizeof(empty)> fempty(std::move(empty));
+ static_assert(sizeof(fempty) >= sizeof(empty), "size bounds");
+
+ auto small = [x = 1, y = 2] {
+ (void)x; // suppress unused lambda capture warning
+ (void)y;
+ };
+ fit::inline_function<Closure, sizeof(small)> fsmall(std::move(small));
+ static_assert(sizeof(fsmall) >= sizeof(small), "size bounds");
+ fsmall = [] {};
+
+ auto big = [big = Big(), x = 1] { (void)x; };
+ fit::inline_function<Closure, sizeof(big)> fbig(std::move(big));
+ static_assert(sizeof(fbig) >= sizeof(big), "size bounds");
+ fbig = [x = 1, y = 2] {
+ (void)x;
+ (void)y;
+ };
+ fbig = [] {};
+
+// These statements do not compile because the lambdas are too big to fit.
+#if 0
+ fempty = [ x = 1, y = 2 ] {
+ (void)x;
+ (void)y;
+ };
+ fsmall = [ big = Big(), x = 1 ] { (void)x; };
+ fbig = [ big = Big(), x = 1, y = 2 ] {
+ (void)x;
+ (void)y;
+ };
+#endif
+}
+
+TEST(FunctionTests, inline_function_alignment_check) {
+// These statements do not compile because the alignment is too large.
+#if 0
+ auto big = [big = BigAlignment()] { };
+ fit::inline_function<Closure, sizeof(big)> fbig(std::move(big));
+#endif
+}
+
+TEST(FunctionTests, move_only_argument_and_result) {
+ std::unique_ptr<int> arg(new int());
+ fit::function<MoveOp> f([](std::unique_ptr<int> value) {
+ *value += 1;
+ return value;
+ });
+ arg = f(std::move(arg));
+ EXPECT_EQ(1, *arg);
+ arg = f(std::move(arg));
+ EXPECT_EQ(2, *arg);
+}
+
+void implicit_construction_helper(fit::closure closure) {}
+
+TEST(FunctionTests, implicit_construction) {
+ // ensure we can implicitly construct from nullptr
+ implicit_construction_helper(nullptr);
+
+ // ensure we can implicitly construct from a lambda
+ implicit_construction_helper([] {});
+}
+
+int arg_count(fit::closure) { return 0; }
+int arg_count(fit::function<void(int)>) { return 1; }
+
+TEST(FunctionTests, overload_resolution) {
+ EXPECT_EQ(0, arg_count([] {}));
+ EXPECT_EQ(1, arg_count([](int) {}));
+}
+
+TEST(FunctionTests, sharing) {
+ fit::function<Closure> fnull;
+ fit::function<Closure> fnullshare1 = fnull.share();
+ fit::function<Closure> fnullshare2 = fnull.share();
+ fit::function<Closure> fnullshare3 = fnullshare1.share();
+ EXPECT_FALSE(!!fnull);
+ EXPECT_FALSE(!!fnullshare1);
+ EXPECT_FALSE(!!fnullshare2);
+ EXPECT_FALSE(!!fnullshare3);
+
+ int finlinevalue = 1;
+ int finlinedestroy = 0;
+ fit::function<Closure> finline = [&finlinevalue, d = DestructionObserver(&finlinedestroy)] {
+ finlinevalue++;
+ };
+ fit::function<Closure> finlineshare1 = finline.share();
+ fit::function<Closure> finlineshare2 = finline.share();
+ fit::function<Closure> finlineshare3 = finlineshare1.share();
+ EXPECT_TRUE(!!finline);
+ EXPECT_TRUE(!!finlineshare1);
+ EXPECT_TRUE(!!finlineshare2);
+ EXPECT_TRUE(!!finlineshare3);
+ finline();
+ EXPECT_EQ(2, finlinevalue);
+ finlineshare1();
+ EXPECT_EQ(3, finlinevalue);
+ finlineshare2();
+ EXPECT_EQ(4, finlinevalue);
+ finlineshare3();
+ EXPECT_EQ(5, finlinevalue);
+ finlineshare2();
+ EXPECT_EQ(6, finlinevalue);
+ finline();
+ EXPECT_EQ(7, finlinevalue);
+ EXPECT_EQ(0, finlinedestroy);
+ finline = nullptr;
+ EXPECT_EQ(0, finlinedestroy);
+ finlineshare3 = nullptr;
+ EXPECT_EQ(0, finlinedestroy);
+ finlineshare2 = nullptr;
+ EXPECT_EQ(0, finlinedestroy);
+ finlineshare1 = nullptr;
+ EXPECT_EQ(1, finlinedestroy);
+
+ int fheapvalue = 1;
+ int fheapdestroy = 0;
+ fit::function<Closure> fheap = [&fheapvalue, big = Big(),
+ d = DestructionObserver(&fheapdestroy)] { fheapvalue++; };
+ fit::function<Closure> fheapshare1 = fheap.share();
+ fit::function<Closure> fheapshare2 = fheap.share();
+ fit::function<Closure> fheapshare3 = fheapshare1.share();
+ EXPECT_TRUE(!!fheap);
+ EXPECT_TRUE(!!fheapshare1);
+ EXPECT_TRUE(!!fheapshare2);
+ EXPECT_TRUE(!!fheapshare3);
+ fheap();
+ EXPECT_EQ(2, fheapvalue);
+ fheapshare1();
+ EXPECT_EQ(3, fheapvalue);
+ fheapshare2();
+ EXPECT_EQ(4, fheapvalue);
+ fheapshare3();
+ EXPECT_EQ(5, fheapvalue);
+ fheapshare2();
+ EXPECT_EQ(6, fheapvalue);
+ fheap();
+ EXPECT_EQ(7, fheapvalue);
+ EXPECT_EQ(0, fheapdestroy);
+ fheap = nullptr;
+ EXPECT_EQ(0, fheapdestroy);
+ fheapshare3 = nullptr;
+ EXPECT_EQ(0, fheapdestroy);
+ fheapshare2 = nullptr;
+ EXPECT_EQ(0, fheapdestroy);
+ fheapshare1 = nullptr;
+ EXPECT_EQ(1, fheapdestroy);
+
+ // target access now available after share()
+ using ClosureFunction = fit::function<Closure, HugeCallableSize>;
+ ClosureFunction fslot = SlotMachine{42};
+ fslot();
+ SlotMachine* fslottarget = fslot.template target<SlotMachine>();
+ EXPECT_EQ(43, fslottarget->value);
+
+ auto shared_fslot = fslot.share();
+ shared_fslot();
+ fslottarget = shared_fslot.template target<SlotMachine>();
+ EXPECT_EQ(44, fslottarget->value);
+ fslot();
+ EXPECT_EQ(45, fslottarget->value);
+ fslot = nullptr;
+ EXPECT_NULL(fslot.template target<decltype(nullptr)>());
+ shared_fslot();
+ EXPECT_EQ(46, fslottarget->value);
+ shared_fslot = nullptr;
+ EXPECT_NULL(shared_fslot.template target<decltype(nullptr)>());
+
+// These statements do not compile because inline functions cannot be shared
+#if 0
+ fit::inline_function<Closure> fbad;
+ fbad.share();
+#endif
+}
+
+struct Obj {
+ void Call() { calls++; }
+
+ int AddOne(int x) {
+ calls++;
+ return x + 1;
+ }
+
+ int Sum(int a, int b, int c) {
+ calls++;
+ return a + b + c;
+ }
+
+ std::unique_ptr<int> AddAndReturn(std::unique_ptr<int> value) {
+ (*value)++;
+ return value;
+ }
+
+ uint32_t calls = 0;
+};
+
+TEST(FunctionTests, deprecated_bind_member) {
+ Obj obj;
+ auto move_only_value = std::make_unique<int>(4);
+
+ static_assert(sizeof(fit::bind_member(&obj, &Obj::AddOne)) == 3 * sizeof(void*));
+ fit::bind_member(&obj, &Obj::Call)();
+ EXPECT_EQ(23, fit::bind_member(&obj, &Obj::AddOne)(22));
+ EXPECT_EQ(6, fit::bind_member(&obj, &Obj::Sum)(1, 2, 3));
+ move_only_value = fit::bind_member(&obj, &Obj::AddAndReturn)(std::move(move_only_value));
+ EXPECT_EQ(5, *move_only_value);
+ EXPECT_EQ(3, obj.calls);
+}
+
+TEST(FunctionTests, bind_member) {
+ Obj obj;
+ auto move_only_value = std::make_unique<int>(4);
+
+ static_assert(sizeof(fit::bind_member<&Obj::AddOne>(&obj)) == sizeof(void*));
+ fit::bind_member<&Obj::Call> (&obj)();
+ EXPECT_EQ(23, fit::bind_member<&Obj::AddOne>(&obj)(22));
+ EXPECT_EQ(6, fit::bind_member<&Obj::Sum>(&obj)(1, 2, 3));
+ move_only_value = fit::bind_member<&Obj::AddAndReturn>(&obj)(std::move(move_only_value));
+ fit::function<int(int, int, int)> f(fit::bind_member<&Obj::Sum>(&obj));
+ EXPECT_EQ(6, f(1, 2, 3));
+ EXPECT_EQ(5, *move_only_value);
+ EXPECT_EQ(4, obj.calls);
+}
+
+TEST(FunctionTests, callback_once) {
+ fit::callback<Closure> cbnull;
+ fit::callback<Closure> cbnullshare1 = cbnull.share();
+ fit::callback<Closure> cbnullshare2 = cbnull.share();
+ fit::callback<Closure> cbnullshare3 = cbnullshare1.share();
+ EXPECT_FALSE(!!cbnull);
+ EXPECT_FALSE(!!cbnullshare1);
+ EXPECT_FALSE(!!cbnullshare2);
+ EXPECT_FALSE(!!cbnullshare3);
+
+ int cbinlinevalue = 1;
+ int cbinlinedestroy = 0;
+ fit::callback<Closure> cbinline = [&cbinlinevalue, d = DestructionObserver(&cbinlinedestroy)] {
+ cbinlinevalue++;
+ };
+ EXPECT_TRUE(!!cbinline);
+ EXPECT_FALSE(cbinline == nullptr);
+ EXPECT_EQ(1, cbinlinevalue);
+ EXPECT_EQ(0, cbinlinedestroy);
+ cbinline(); // releases resources even if never shared
+ EXPECT_FALSE(!!cbinline);
+ EXPECT_TRUE(cbinline == nullptr);
+ EXPECT_EQ(2, cbinlinevalue);
+ EXPECT_EQ(1, cbinlinedestroy);
+
+ cbinlinevalue = 1;
+ cbinlinedestroy = 0;
+ cbinline = [&cbinlinevalue, d = DestructionObserver(&cbinlinedestroy)] { cbinlinevalue++; };
+ fit::callback<Closure> cbinlineshare1 = cbinline.share();
+ fit::callback<Closure> cbinlineshare2 = cbinline.share();
+ fit::callback<Closure> cbinlineshare3 = cbinlineshare1.share();
+ EXPECT_TRUE(!!cbinline);
+ EXPECT_TRUE(!!cbinlineshare1);
+ EXPECT_TRUE(!!cbinlineshare2);
+ EXPECT_TRUE(!!cbinlineshare3);
+ EXPECT_EQ(1, cbinlinevalue);
+ EXPECT_EQ(0, cbinlinedestroy);
+ cbinline();
+ EXPECT_EQ(2, cbinlinevalue);
+ EXPECT_EQ(1, cbinlinedestroy);
+ EXPECT_FALSE(!!cbinline);
+ EXPECT_TRUE(cbinline == nullptr);
+ // cbinline(); // should abort
+ EXPECT_FALSE(!!cbinlineshare1);
+ EXPECT_TRUE(cbinlineshare1 == nullptr);
+ // cbinlineshare1(); // should abort
+ EXPECT_FALSE(!!cbinlineshare2);
+ // cbinlineshare2(); // should abort
+ EXPECT_FALSE(!!cbinlineshare3);
+ // cbinlineshare3(); // should abort
+ EXPECT_EQ(1, cbinlinedestroy);
+ cbinlineshare3 = nullptr;
+ EXPECT_EQ(1, cbinlinedestroy);
+ cbinline = nullptr;
+ EXPECT_EQ(1, cbinlinedestroy);
+
+ int cbheapvalue = 1;
+ int cbheapdestroy = 0;
+ fit::callback<Closure> cbheap = [&cbheapvalue, big = Big(),
+ d = DestructionObserver(&cbheapdestroy)] { cbheapvalue++; };
+ EXPECT_TRUE(!!cbheap);
+ EXPECT_FALSE(cbheap == nullptr);
+ EXPECT_EQ(1, cbheapvalue);
+ EXPECT_EQ(0, cbheapdestroy);
+ cbheap(); // releases resources even if never shared
+ EXPECT_FALSE(!!cbheap);
+ EXPECT_TRUE(cbheap == nullptr);
+ EXPECT_EQ(2, cbheapvalue);
+ EXPECT_EQ(1, cbheapdestroy);
+
+ cbheapvalue = 1;
+ cbheapdestroy = 0;
+ cbheap = [&cbheapvalue, big = Big(), d = DestructionObserver(&cbheapdestroy)] { cbheapvalue++; };
+ fit::callback<Closure> cbheapshare1 = cbheap.share();
+ fit::callback<Closure> cbheapshare2 = cbheap.share();
+ fit::callback<Closure> cbheapshare3 = cbheapshare1.share();
+ EXPECT_TRUE(!!cbheap);
+ EXPECT_TRUE(!!cbheapshare1);
+ EXPECT_TRUE(!!cbheapshare2);
+ EXPECT_TRUE(!!cbheapshare3);
+ EXPECT_EQ(1, cbheapvalue);
+ EXPECT_EQ(0, cbheapdestroy);
+ cbheap();
+ EXPECT_EQ(2, cbheapvalue);
+ EXPECT_EQ(1, cbheapdestroy);
+ EXPECT_FALSE(!!cbheap);
+ EXPECT_TRUE(cbheap == nullptr);
+ // cbheap(); // should abort
+ EXPECT_FALSE(!!cbheapshare1);
+ EXPECT_TRUE(cbheapshare1 == nullptr);
+ // cbheapshare1(); // should abort
+ EXPECT_FALSE(!!cbheapshare2);
+ // cbheapshare2(); // should abort
+ EXPECT_FALSE(!!cbheapshare3);
+ // cbheapshare3(); // should abort
+ EXPECT_EQ(1, cbheapdestroy);
+ cbheapshare3 = nullptr;
+ EXPECT_EQ(1, cbheapdestroy);
+ cbheap = nullptr;
+ EXPECT_EQ(1, cbheapdestroy);
+
+ // Verify new design, splitting out fit::callback, still supports
+ // assignment of move-only "Callables" (that is, lambdas made move-only
+ // because they capture a move-only object, like a fit::function, for
+ // example!)
+ fit::function<void()> fn_to_wrap = []() {};
+ fit::function<void()> fn_from_lambda;
+ fn_from_lambda = [fn = fn_to_wrap.share()]() mutable { fn(); };
+
+ // Same test for fit::callback
+ fit::callback<void()> cb_to_wrap = []() {};
+ fit::callback<void()> cb_from_lambda;
+ cb_from_lambda = [cb = std::move(cb_to_wrap)]() mutable { cb(); };
+
+ // |fit::function| objects can be constructed from or assigned from
+ // a |fit::callback|, if the result and arguments are compatible.
+ fit::function<Closure> fn = []() {};
+ fit::callback<Closure> cb = []() {};
+ fit::callback<Closure> cb_assign;
+ cb_assign = std::move(fn);
+ fit::callback<Closure> cb_construct = std::move(fn);
+ fit::callback<Closure> cb_share = fn.share();
+
+ static_assert(!std::is_convertible<fit::function<void()>*, fit::callback<void()>*>::value, "");
+ static_assert(!std::is_constructible<fit::function<void()>, fit::callback<void()>>::value, "");
+ static_assert(!std::is_assignable<fit::function<void()>, fit::callback<void()>>::value, "");
+ static_assert(!std::is_constructible<fit::function<void()>, decltype(cb.share())>::value, "");
+#if 0
+ // These statements do not compile because inline callbacks cannot be shared
+ fit::inline_callback<Closure> cbbad;
+ cbbad.share();
+
+ {
+ // Attempts to copy, move, or share a callback into a fit::function<>
+ // should not compile. This is verified by static_assert above, and
+ // was verified interactively using the compiler.
+ fit::callback<Closure> cb = []() {};
+ fit::function<Closure> fn = []() {};
+ fit::function<Closure> fn_assign;
+ fn_assign = cb; // BAD
+ fn_assign = std::move(cb); // BAD
+ fit::function<Closure> fn_construct = cb; // BAD
+ fit::function<Closure> fn_construct2 = std::move(cb); // BAD
+ fit::function<Closure> fn_share = cb.share(); // BAD
+ }
+
+#endif
+}
+
+#if defined(__cpp_constinit)
+#define CONSTINIT constinit
+#elif defined(__clang__)
+#define CONSTINIT [[clang::require_constant_initialization]]
+#else
+#define CONSTINIT
+#endif // __cpp_constinit
+
+CONSTINIT const fit::function<void()> kDefaultConstructed;
+CONSTINIT const fit::function<void()> kNullptrConstructed(nullptr);
+
+#undef CONSTINIT
+
+TEST(FunctionTests, null_constructors_are_constexpr) {
+ EXPECT_EQ(kDefaultConstructed, nullptr);
+ EXPECT_EQ(kNullptrConstructed, nullptr);
+}
+
+TEST(FunctionTests, function_with_callable_aligned_larger_than_inline_size) {
+ struct alignas(4 * alignof(void*)) LargeAlignedCallable {
+ void operator()() { calls += 1; }
+
+ char calls = 0;
+ };
+
+ static_assert(sizeof(LargeAlignedCallable) > sizeof(void*), "Should not fit inline in function");
+
+ fit::function<void(), sizeof(void*)> function = LargeAlignedCallable();
+
+ static_assert(alignof(LargeAlignedCallable) > alignof(decltype(function)), "");
+
+ // Verify that the allocated target is aligned correctly.
+ LargeAlignedCallable* callable_ptr = function.target<LargeAlignedCallable>();
+ EXPECT_EQ(cpp20::bit_cast<uintptr_t>(callable_ptr) % alignof(LargeAlignedCallable), 0u);
+
+ function();
+ EXPECT_EQ(callable_ptr->calls, 1);
+}
+
+// Test that function inline sizes round up to the nearest word.
+template <size_t bytes>
+using Function = fit::function<void(), bytes>; // Use an alias for brevity
+
+static_assert(std::is_same<Function<0>, Function<sizeof(void*)>>::value, "");
+static_assert(std::is_same<Function<1>, Function<sizeof(void*)>>::value, "");
+static_assert(std::is_same<Function<sizeof(void*) - 1>, Function<sizeof(void*)>>::value, "");
+static_assert(std::is_same<Function<sizeof(void*)>, Function<sizeof(void*)>>::value, "");
+static_assert(std::is_same<Function<sizeof(void*) + 1>, Function<2 * sizeof(void*)>>::value, "");
+static_assert(std::is_same<Function<2 * sizeof(void*)>, Function<2 * sizeof(void*)>>::value, "");
+
+// Also test the inline_function, callback, and inline_callback aliases.
+static_assert(
+ std::is_same_v<fit::inline_function<void(), 0>, fit::inline_function<void(), sizeof(void*)>>,
+ "");
+static_assert(
+ std::is_same_v<fit::inline_function<void(), 1>, fit::inline_function<void(), sizeof(void*)>>,
+ "");
+static_assert(std::is_same_v<fit::callback<void(), 0>, fit::callback<void(), sizeof(void*)>>, "");
+static_assert(std::is_same_v<fit::callback<void(), 1>, fit::callback<void(), sizeof(void*)>>, "");
+static_assert(
+ std::is_same_v<fit::inline_callback<void(), 0>, fit::inline_callback<void(), sizeof(void*)>>,
+ "");
+static_assert(
+ std::is_same_v<fit::inline_callback<void(), 1>, fit::inline_callback<void(), sizeof(void*)>>,
+ "");
+
+TEST(FunctionTests, rounding_function) {
+ EXPECT_EQ(5, fit::internal::RoundUpToMultiple(0, 5));
+ EXPECT_EQ(5, fit::internal::RoundUpToMultiple(1, 5));
+ EXPECT_EQ(5, fit::internal::RoundUpToMultiple(4, 5));
+ EXPECT_EQ(5, fit::internal::RoundUpToMultiple(5, 5));
+ EXPECT_EQ(10, fit::internal::RoundUpToMultiple(6, 5));
+ EXPECT_EQ(10, fit::internal::RoundUpToMultiple(9, 5));
+ EXPECT_EQ(10, fit::internal::RoundUpToMultiple(10, 5));
+}
+
+// Test that the alignment of function and callback is always the minimum of alignof(max_align_t)
+// and largest possible alignment for the specified inline target size.
+constexpr size_t ExpectedAlignment(size_t alignment_32, size_t alignment_64) {
+ if (sizeof(void*) == 4) {
+ return std::min(alignment_32, alignof(max_align_t));
+ }
+ if (sizeof(void*) == 8) {
+ return std::min(alignment_64, alignof(max_align_t));
+ }
+ return 0; // Word sizes other than 32/64 are not supported, will need to update test.
+}
+
+static_assert(alignof(fit::function<void(), 0>) == ExpectedAlignment(4, 8), "");
+static_assert(alignof(fit::function<void(), 1>) == ExpectedAlignment(4, 8), "");
+static_assert(alignof(fit::function<int(), 4>) == ExpectedAlignment(4, 8), "");
+static_assert(alignof(fit::function<bool(), 8>) == ExpectedAlignment(8, 8), "");
+static_assert(alignof(fit::function<float(int), 9>) == ExpectedAlignment(8, 16), "");
+static_assert(alignof(fit::function<float(int), 25>) == ExpectedAlignment(16, 16), "");
+static_assert(alignof(fit::inline_function<void(), 1>) == ExpectedAlignment(4, 8), "");
+static_assert(alignof(fit::inline_function<void(), 9>) == ExpectedAlignment(8, 16), "");
+static_assert(alignof(fit::callback<void(), 0>) == ExpectedAlignment(4, 8), "");
+static_assert(alignof(fit::callback<void(), 1>) == ExpectedAlignment(4, 8), "");
+static_assert(alignof(fit::callback<int(), 4>) == ExpectedAlignment(4, 8), "");
+static_assert(alignof(fit::callback<bool(), 8>) == ExpectedAlignment(8, 8), "");
+static_assert(alignof(fit::callback<float(int), 9>) == ExpectedAlignment(8, 16), "");
+static_assert(alignof(fit::callback<float(int), 25>) == ExpectedAlignment(16, 16), "");
+static_assert(alignof(fit::inline_callback<void(), 1>) == ExpectedAlignment(4, 8), "");
+static_assert(alignof(fit::inline_callback<void(), 9>) == ExpectedAlignment(8, 16), "");
+static_assert(alignof(fit::inline_callback<void(), 25>) == ExpectedAlignment(16, 16), "");
+
+namespace test_copy_move_constructions {
+
+template <typename F>
+class assert_move_only {
+ static_assert(!std::is_copy_assignable<F>::value);
+ static_assert(!std::is_copy_constructible<F>::value);
+ static_assert(std::is_move_assignable<F>::value);
+ static_assert(std::is_move_constructible<F>::value);
+
+ // It seems that just testing `!std::is_copy_assignable<F>` is not enough,
+ // as the `fit::function` class could use a perfect-forwarding mechanism
+ // that still allows expressions of the form `fit::function func1 = func2`
+ // to compile, even though `std::is_copy_assignable<F>` is false.
+ template <typename T, typename = void>
+ struct test : std::false_type {};
+ template <typename T>
+ struct test<T, std::void_t<decltype(T::v1 = T::v2)>> : std::true_type {};
+
+ struct NoAssign {
+ static F v1;
+ static F v2;
+ };
+ static_assert(!test<NoAssign>::value);
+
+ struct CanAssign {
+ static int v1;
+ static int v2;
+ };
+ static_assert(test<CanAssign>::value);
+
+ template <typename T, typename = void>
+ struct test_construct : std::false_type {};
+ template <typename T>
+ struct test_construct<T, std::void_t<decltype(T{std::declval<const T&>()})>> : std::true_type {};
+
+ static_assert(!test_construct<F>::value);
+ static_assert(test_construct<int>::value);
+};
+
+template class assert_move_only<fit::function<void()>>;
+template class assert_move_only<fit::callback<void()>>;
+
+} // namespace test_copy_move_constructions
+
+} // namespace
+
+namespace test_conversions {
+static_assert(std::is_convertible<Closure, fit::function<Closure>>::value, "");
+static_assert(std::is_convertible<BinaryOp, fit::function<BinaryOp>>::value, "");
+static_assert(std::is_assignable<fit::function<Closure>, Closure>::value, "");
+static_assert(std::is_assignable<fit::function<BinaryOp>, BinaryOp>::value, "");
+
+static_assert(std::is_assignable<fit::function<BooleanGenerator>, IntGenerator>::value, "");
+static_assert(std::is_assignable<fit::function<BuildableFromIntGenerator>, IntGenerator>::value,
+ "");
+static_assert(!std::is_assignable<fit::function<IntGenerator>, BuildableFromIntGenerator>::value,
+ "");
+
+static_assert(!std::is_convertible<BinaryOp, fit::function<Closure>>::value, "");
+static_assert(!std::is_convertible<Closure, fit::function<BinaryOp>>::value, "");
+static_assert(!std::is_assignable<fit::function<Closure>, BinaryOp>::value, "");
+static_assert(!std::is_assignable<fit::function<BinaryOp>, Closure>::value, "");
+
+static_assert(!std::is_convertible<ClosureWrongReturnType, fit::function<Closure>>::value, "");
+static_assert(!std::is_convertible<BinaryOpWrongReturnType, fit::function<BinaryOp>>::value, "");
+static_assert(!std::is_assignable<fit::function<Closure>, ClosureWrongReturnType>::value, "");
+static_assert(!std::is_assignable<fit::function<BinaryOp>, BinaryOpWrongReturnType>::value, "");
+
+static_assert(!std::is_convertible<void, fit::function<Closure>>::value, "");
+static_assert(!std::is_convertible<void, fit::function<BinaryOp>>::value, "");
+static_assert(!std::is_assignable<void, fit::function<Closure>>::value, "");
+static_assert(!std::is_assignable<void, fit::function<BinaryOp>>::value, "");
+
+static_assert(std::is_same<fit::function<BinaryOp>::result_type, int>::value, "");
+static_assert(std::is_same<fit::callback<BinaryOp>::result_type, int>::value, "");
+} // namespace test_conversions
+
+TEST(FunctionTests, closure_fit_function_Closure) { closure<fit::function<Closure>>(); }
+TEST(FunctionTests, binary_op_fit_function_BinaryOp) { binary_op<fit::function<BinaryOp>>(); }
+TEST(FunctionTests, closure_fit_function_Closure_0u) { closure<fit::function<Closure, 0u>>(); }
+TEST(FunctionTests, binary_op_fit_function_BinaryOp_0u) {
+ binary_op<fit::function<BinaryOp, 0u>>();
+}
+TEST(FunctionTests, closure_fit_function_Closure_HugeCallableSize) {
+ closure<fit::function<Closure, HugeCallableSize>>();
+}
+TEST(FunctionTests, binary_op_fit_function_BinaryOp_HugeCallableSize) {
+ binary_op<fit::function<BinaryOp, HugeCallableSize>>();
+}
+TEST(FunctionTests, closure_fit_inline_function_Closure_HugeCallableSize) {
+ closure<fit::inline_function<Closure, HugeCallableSize>>();
+}
+TEST(FunctionTests, binary_op_fit_inline_function_BinaryOp_HugeCallableSize) {
+ binary_op<fit::inline_function<BinaryOp, HugeCallableSize>>();
+}
+
+TEST(FunctionTests, bind_return_reference) {
+ struct TestClass {
+ int& member() { return member_; }
+ int member_ = 0;
+ };
+
+ TestClass instance;
+ // Ensure that references to the original values are returned, not copies.
+ fit::function<int&()> func = fit::bind_member<&TestClass::member>(&instance);
+ EXPECT_EQ(&func(), &instance.member_);
+
+ fit::function<int&()> func_deprecated = fit::bind_member(&instance, &TestClass::member);
+ EXPECT_EQ(&func_deprecated(), &instance.member_);
+}
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h
new file mode 100644
index 000000000..1e9309417
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/bit.h
@@ -0,0 +1,185 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_BIT_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_BIT_H_
+
+#include <cstring>
+#include <limits>
+
+#include "internal/bit.h"
+#include "memory.h"
+#include "version.h"
+
+#if __has_include(<bit>) && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#include <bit>
+
+#endif //__has_include(<bit>) && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+namespace cpp20 {
+
+#if defined(__cpp_lib_bit_cast) && __cpp_lib_bit_cast >= 201806L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::bit_cast;
+
+#else // Use builtin to provide constexpr bit_cast.
+
+#if defined(__has_builtin) && !defined(LIB_STDCOMPAT_NO_BUILTIN_BITCAST)
+#if __has_builtin(__builtin_bit_cast)
+
+template <typename To, typename From>
+constexpr std::enable_if_t<sizeof(To) == sizeof(From) && std::is_trivially_copyable<To>::value &&
+ std::is_trivially_copyable<From>::value,
+ To>
+bit_cast(const From& from) {
+ return __builtin_bit_cast(To, from);
+}
+
+// Since there are two #if checks, using #else would require code duplication.
+// Define a temporary internal macro to indicate that bit_cast was defined.
+#define LIB_STDCOMPAT_BIT_CAST_DEFINED_
+
+#endif // __has_builtin(__builtin_bit_cast)
+#endif // defined(__has_builtin) && !defined(LIB_STDCOMPAT_NO_BUILTIN_BITCAST)
+
+// Use memcpy instead, not constexpr though.
+#ifndef LIB_STDCOMPAT_BIT_CAST_DEFINED_
+
+#define LIB_STDCOMPAT_NONCONSTEXPR_BITCAST 1
+
+template <typename To, typename From>
+std::enable_if_t<sizeof(To) == sizeof(From) && std::is_trivially_copyable<To>::value &&
+ std::is_trivially_copyable<From>::value,
+ To>
+bit_cast(const From& from) {
+ std::aligned_storage_t<sizeof(To)> uninitialized_to;
+ std::memcpy(static_cast<void*>(&uninitialized_to),
+ static_cast<const void*>(cpp17::addressof(from)), sizeof(To));
+ return *reinterpret_cast<const To*>(&uninitialized_to);
+}
+
+#endif // LIB_STDCOMPAT_BIT_CAST_DEFINED_
+
+#undef LIB_STDCOMPAT_BIT_CAST_DEFINED_
+
+#endif // __cpp_lib_bit_cast >= 201806L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_bitops) && __cpp_lib_bitops >= 201907L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::countl_one;
+using std::countl_zero;
+using std::countr_one;
+using std::countr_zero;
+using std::popcount;
+using std::rotl;
+using std::rotr;
+
+#else
+
+template <class T>
+constexpr std::enable_if_t<std::is_unsigned<T>::value, int> countr_zero(T x) noexcept {
+ if (x == 0) {
+ return std::numeric_limits<T>::digits;
+ }
+
+ return internal::count_zeros_from_right(x);
+}
+
+template <class T>
+constexpr std::enable_if_t<std::is_unsigned<T>::value, int> countl_zero(T x) noexcept {
+ if (x == 0) {
+ return std::numeric_limits<T>::digits;
+ }
+
+ return internal::count_zeros_from_left(x);
+}
+
+template <class T>
+constexpr std::enable_if_t<std::is_unsigned<T>::value, int> countl_one(T x) noexcept {
+ return countl_zero(static_cast<T>(~x));
+}
+
+template <class T>
+constexpr std::enable_if_t<std::is_unsigned<T>::value, int> countr_one(T x) noexcept {
+ return countr_zero(static_cast<T>(~x));
+}
+
+template <class T>
+[[gnu::warn_unused_result]] constexpr std::enable_if_t<std::is_unsigned<T>::value, T> rotl(
+ T x, int s) noexcept {
+ return internal::rotl(x, s);
+}
+
+template <class T>
+[[gnu::warn_unused_result]] constexpr std::enable_if_t<std::is_unsigned<T>::value, T> rotr(
+ T x, int s) noexcept {
+ return internal::rotr(x, s);
+}
+
+template <class T>
+constexpr int popcount(T x) noexcept {
+ return internal::popcount(x);
+}
+
+#endif
+
+#if defined(__cpp_lib_int_pow2) && __cpp_lib_int_pow2 >= 202002L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::bit_ceil;
+using std::bit_floor;
+using std::bit_width;
+using std::has_single_bit;
+
+#else // Provide polyfills for power of two bit functions.
+
+template <typename T>
+constexpr std::enable_if_t<std::is_unsigned<T>::value, bool> has_single_bit(T value) {
+ return popcount(value) == static_cast<T>(1);
+}
+
+template <typename T>
+constexpr std::enable_if_t<std::is_unsigned<T>::value, T> bit_width(T value) {
+ return internal::bit_width(value);
+}
+
+template <typename T>
+constexpr std::enable_if_t<std::is_unsigned<T>::value, T> bit_ceil(T value) {
+ if (value <= 1) {
+ return T(1);
+ }
+ return internal::bit_ceil<T>(value);
+}
+
+template <typename T>
+constexpr std::enable_if_t<std::is_unsigned<T>::value, T> bit_floor(T value) {
+ if (value == 0) {
+ return 0;
+ }
+ return internal::bit_floor(value);
+}
+
+#endif // __cpp_lib_int_pow2 >= 202002L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_endian) && __cpp_lib_endian >= 201907L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::endian;
+
+#else // Provide polyfill for endian enum.
+
+enum class endian {
+ little = 0x11771E,
+ big = 0xB16,
+ native = internal::native_endianess<little, big>(),
+};
+
+#endif
+
+} // namespace cpp20
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_BIT_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h
new file mode 100644
index 000000000..34d44e0d5
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/functional.h
@@ -0,0 +1,50 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_FUNCTIONAL_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_FUNCTIONAL_H_
+
+#include "internal/functional.h"
+
+namespace cpp20 {
+
+// This version is always constexpr-qualified, with no other changes from C++17.
+
+#if defined(__cpp_lib_invoke) && defined(__cpp_lib_constexpr_functional) && \
+ __cpp_lib_invoke >= 201411L && __cpp_lib_constexpr_functional >= 201907L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::invoke;
+
+#else // Provide invoke() polyfill
+
+template <typename F, typename... Args>
+constexpr cpp17::invoke_result_t<F, Args...> invoke(F&& f, Args&&... args) noexcept(
+ cpp17::is_nothrow_invocable_v<F, Args...>) {
+ return ::cpp17::internal::invoke(std::forward<F>(f), std::forward<Args>(args)...);
+}
+
+#endif // __cpp_lib_invoke >= 201411L && __cpp_lib_constexpr_functional >= 201907L &&
+ // !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_bind_front) && __cpp_lib_bind_front >= 201907L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::bind_front;
+
+#else // Provide bind_front() polyfill
+
+template <typename F, typename... Args>
+constexpr ::cpp20::internal::front_binder_t<F, Args...> bind_front(F&& f, Args&&... args) noexcept(
+ cpp17::is_nothrow_constructible_v<::cpp20::internal::front_binder_t<F, Args...>,
+ cpp17::in_place_t, F, Args...>) {
+ return ::cpp20::internal::front_binder_t<F, Args...>(cpp17::in_place, std::forward<F>(f),
+ std::forward<Args>(args)...);
+}
+
+#endif // __cpp_lib_bind_front >= 201907L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+} // namespace cpp20
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_FUNCTIONAL_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h
new file mode 100644
index 000000000..92b33a633
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/bit.h
@@ -0,0 +1,230 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_BIT_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_BIT_H_
+
+#include <limits>
+#include <type_traits>
+
+#include "../type_traits.h"
+
+namespace cpp20 {
+namespace internal {
+
+// Helper for exclusing specific char types when char is considered unsigned by the compiler.
+template <typename T>
+struct is_bit_type
+ : std::integral_constant<
+ bool, std::is_unsigned<T>::value && !std::is_same<bool, T>::value &&
+ !std::is_same<T, char>::value && !std::is_same<T, char16_t>::value &&
+ !std::is_same<T, char32_t>::value && !std::is_same<T, wchar_t>::value> {};
+
+// Rotation implementation.
+// Only internal for usage in implementation of certain methods.
+template <class T>
+[[gnu::warn_unused_result]] constexpr std::enable_if_t<is_bit_type<T>::value, T> rotl(
+ T x, int s) noexcept {
+ const auto digits = std::numeric_limits<T>::digits;
+ const auto rotate_by = s % digits;
+
+ if (rotate_by > 0) {
+ return static_cast<T>((x << rotate_by) | (x >> (digits - rotate_by)));
+ }
+
+ if (rotate_by < 0) {
+ return static_cast<T>((x >> -rotate_by) | (x << (digits + rotate_by)));
+ }
+
+ return x;
+}
+
+template <class T>
+[[gnu::warn_unused_result]] constexpr std::enable_if_t<is_bit_type<T>::value, T> rotr(
+ T x, int s) noexcept {
+ int digits = std::numeric_limits<T>::digits;
+ int rotate_by = s % digits;
+
+ if (rotate_by > 0) {
+ return static_cast<T>((x >> rotate_by) | (x << (digits - rotate_by)));
+ }
+
+ if (rotate_by < 0) {
+ return rotl(x, -rotate_by);
+ }
+
+ return x;
+}
+
+// Overloads for intrinsics.
+// Precondition: |value| != 0.
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(T) <= sizeof(unsigned), int>
+count_zeros_from_right(T value) noexcept {
+ return __builtin_ctz(static_cast<unsigned>(value));
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned) < sizeof(T) &&
+ sizeof(T) <= sizeof(unsigned long),
+ int>
+count_zeros_from_right(T value) noexcept {
+ return __builtin_ctzl(static_cast<unsigned long>(value));
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned long) < sizeof(T) &&
+ sizeof(T) <= sizeof(unsigned long long),
+ int>
+count_zeros_from_right(T value) noexcept {
+ return __builtin_ctzll(static_cast<unsigned long long>(value));
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned long long) < sizeof(T), int>
+count_zeros_from_right(T value) noexcept {
+ int count = 0;
+ int iter_count = 0;
+ const unsigned long long max_digits = std::numeric_limits<unsigned long long>::digits;
+
+ for (int slot = 0; slot * max_digits < std::numeric_limits<T>::digits; ++slot) {
+ const unsigned long long chunk =
+ static_cast<unsigned long long>(internal::rotr(value, (slot)*max_digits));
+ iter_count = (chunk == 0) ? static_cast<int>(max_digits) : count_zeros_from_right(chunk);
+ count += iter_count;
+ if (iter_count != max_digits) {
+ break;
+ }
+ }
+ return count - static_cast<int>(std::numeric_limits<T>::digits % max_digits);
+}
+
+template <typename T, typename U,
+ typename std::enable_if<sizeof(T) >= sizeof(U), bool>::type = true>
+struct digit_diff
+ : std::integral_constant<T, std::numeric_limits<T>::digits - std::numeric_limits<U>::digits> {};
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(T) <= sizeof(unsigned), int>
+count_zeros_from_left(T value) noexcept {
+ return __builtin_clz(static_cast<unsigned>(value)) -
+ static_cast<int>(digit_diff<unsigned, T>::value);
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned) < sizeof(T) &&
+ sizeof(T) <= sizeof(unsigned long),
+ int>
+count_zeros_from_left(T value) noexcept {
+ return __builtin_clzl(static_cast<unsigned long>(value)) -
+ static_cast<int>(digit_diff<unsigned long, T>::value);
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned long) < sizeof(T) &&
+ sizeof(T) <= sizeof(unsigned long long),
+ int>
+count_zeros_from_left(T value) noexcept {
+ return __builtin_clzll(static_cast<unsigned long long>(value)) -
+ static_cast<int>(digit_diff<unsigned long long, T>::value);
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned long long) < sizeof(T), int>
+count_zeros_from_left(T value) noexcept {
+ int count = 0;
+ int iter_count = 0;
+ const unsigned int max_digits = std::numeric_limits<unsigned long long>::digits;
+
+ for (int slot = 0; slot * max_digits < std::numeric_limits<T>::digits; ++slot) {
+ const unsigned long long chunk =
+ static_cast<unsigned long long>(internal::rotl(value, (slot + 1) * max_digits));
+ iter_count = (chunk == 0) ? static_cast<int>(max_digits) : count_zeros_from_left(chunk);
+ count += iter_count;
+ if (iter_count != max_digits) {
+ break;
+ }
+ }
+ return count - static_cast<int>(std::numeric_limits<T>::digits % max_digits);
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(T) <= sizeof(unsigned), int> popcount(
+ T value) noexcept {
+ return __builtin_popcount(static_cast<unsigned>(value));
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned) < sizeof(T) &&
+ sizeof(T) <= sizeof(unsigned long),
+ int>
+popcount(T value) noexcept {
+ return __builtin_popcountl(static_cast<unsigned long>(value));
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned long) < sizeof(T) &&
+ sizeof(T) <= sizeof(unsigned long long),
+ int>
+popcount(T value) noexcept {
+ return __builtin_popcountll(static_cast<unsigned long long>(value));
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && sizeof(unsigned long long) < sizeof(T), int>
+popcount(T value) noexcept {
+ int accumulated_count = 0;
+ while (value != 0) {
+ accumulated_count += popcount(static_cast<unsigned long long>(value));
+ value >>= std::numeric_limits<unsigned long long>::digits;
+ }
+ return accumulated_count;
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value, T> bit_width(T value) {
+ const T zeros_left =
+ (value == 0) ? std::numeric_limits<T>::digits : static_cast<T>(count_zeros_from_left(value));
+ return std::numeric_limits<T>::digits - zeros_left;
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && !std::is_same<T, decltype(+T())>::value, T>
+bit_ceil(T value) {
+ unsigned ub_offset = std::numeric_limits<unsigned>::digits - std::numeric_limits<T>::digits;
+ return static_cast<T>(1 << (bit_width(static_cast<T>(value - 1)) + ub_offset) >> ub_offset);
+}
+
+// When there is no integer promotion.
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value && std::is_same<T, decltype(+T())>::value, T>
+bit_ceil(T value) {
+ auto width = bit_width(value - 1);
+ if (!cpp20::is_constant_evaluated() && width == std::numeric_limits<T>::digits) {
+ __builtin_abort();
+ }
+ return static_cast<T>(1) << width;
+}
+
+template <typename T>
+constexpr std::enable_if_t<is_bit_type<T>::value, T> bit_floor(T value) {
+ return static_cast<T>(T(1) << (bit_width(value) - T(1)));
+}
+
+template <unsigned little, unsigned big>
+constexpr unsigned native_endianess() {
+ auto curr = __BYTE_ORDER__;
+ if (curr == __ORDER_LITTLE_ENDIAN__) {
+ return little;
+ }
+ if (curr == __ORDER_BIG_ENDIAN__) {
+ return big;
+ }
+ return 0x404;
+}
+
+} // namespace internal
+} // namespace cpp20
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_BIT_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h
new file mode 100644
index 000000000..e86f2f108
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/constructors.h
@@ -0,0 +1,100 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
+
+#include <cstddef>
+#include <type_traits>
+#include <utility>
+
+namespace cpp17 {
+namespace internal {
+
+// Mixin that implicitly deletes the subclass default constructor when type T
+// is not default constructible.
+template <typename T, bool = std::is_default_constructible<T>::value>
+struct modulate_default_constructor {};
+template <typename T>
+struct modulate_default_constructor<T, false> {
+ constexpr modulate_default_constructor() = delete;
+};
+
+// Mixin that implicitly deletes the subclass copy constructor when type T is
+// not copy constructible.
+template <std::size_t Index, typename T, bool = std::is_copy_constructible<T>::value>
+struct modulate_copy_constructor {};
+template <std::size_t Index, typename T>
+struct modulate_copy_constructor<Index, T, false> {
+ constexpr modulate_copy_constructor() = default;
+ constexpr modulate_copy_constructor(const modulate_copy_constructor&) = delete;
+ constexpr modulate_copy_constructor& operator=(const modulate_copy_constructor&) = default;
+ constexpr modulate_copy_constructor(modulate_copy_constructor&&) = default;
+ constexpr modulate_copy_constructor& operator=(modulate_copy_constructor&&) = default;
+};
+
+// Mixin that implicitly deletes the subclass copy assignment operator when type
+// T is not copy assignable.
+template <std::size_t Index, typename T, bool = std::is_copy_assignable<T>::value>
+struct modulate_copy_assignment {};
+template <std::size_t Index, typename T>
+struct modulate_copy_assignment<Index, T, false> {
+ constexpr modulate_copy_assignment() = default;
+ constexpr modulate_copy_assignment(const modulate_copy_assignment&) = default;
+ constexpr modulate_copy_assignment& operator=(const modulate_copy_assignment&) = delete;
+ constexpr modulate_copy_assignment(modulate_copy_assignment&&) = default;
+ constexpr modulate_copy_assignment& operator=(modulate_copy_assignment&&) = default;
+};
+
+// Mixin that implicitly deletes the subclass move constructor when type T is
+// not move constructible.
+template <std::size_t Index, typename T, bool = std::is_move_constructible<T>::value>
+struct modulate_move_constructor {};
+template <std::size_t Index, typename T>
+struct modulate_move_constructor<Index, T, false> {
+ constexpr modulate_move_constructor() = default;
+ constexpr modulate_move_constructor(const modulate_move_constructor&) = default;
+ constexpr modulate_move_constructor& operator=(const modulate_move_constructor&) = default;
+ constexpr modulate_move_constructor(modulate_move_constructor&&) = delete;
+ constexpr modulate_move_constructor& operator=(modulate_move_constructor&&) = default;
+};
+
+// Mixin that implicitly deletes the subclass move assignment operator when type
+// T is not move assignable.
+template <std::size_t Index, typename T, bool = std::is_move_assignable<T>::value>
+struct modulate_move_assignment {};
+template <std::size_t Index, typename T>
+struct modulate_move_assignment<Index, T, false> {
+ constexpr modulate_move_assignment() = default;
+ constexpr modulate_move_assignment(const modulate_move_assignment&) = default;
+ constexpr modulate_move_assignment& operator=(const modulate_move_assignment&) = default;
+ constexpr modulate_move_assignment(modulate_move_assignment&&) = default;
+ constexpr modulate_move_assignment& operator=(modulate_move_assignment&&) = delete;
+};
+
+// Utility that takes an index sequence and an equally sized parameter pack and
+// mixes in each of the above copy/move construction/assignment modulators for
+// each type in Ts. The indices are used to avoid duplicate direct base errors
+// by ensuring that each mixin type is unique, even when there are duplicate
+// types within the parameter pack Ts.
+template <typename IndexSequence, typename... Ts>
+struct modulate_copy_and_move_index;
+
+template <std::size_t... Is, typename... Ts>
+struct modulate_copy_and_move_index<std::index_sequence<Is...>, Ts...>
+ : modulate_copy_constructor<Is, Ts>...,
+ modulate_copy_assignment<Is, Ts>...,
+ modulate_move_constructor<Is, Ts>...,
+ modulate_move_assignment<Is, Ts>... {};
+
+// Mixin that modulates the subclass copy/move constructors and assignment
+// operators based on the copy/move characteristics of each type in Ts.
+template <typename... Ts>
+struct modulate_copy_and_move
+ : modulate_copy_and_move_index<std::index_sequence_for<Ts...>, Ts...> {};
+
+} // namespace internal
+} // namespace cpp17
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_CONSTRUCTORS_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h
new file mode 100644
index 000000000..74f4e8c08
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/exception.h
@@ -0,0 +1,56 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
+
+#include <exception>
+
+#include "../type_traits.h"
+
+namespace cpp17 {
+namespace internal {
+
+// When exceptions are enabled, will generate an exception of the right type, when disabled will
+// simply abort execution.
+//
+// Note: both clang and gcc support gnu::unused, which makes it a portable alternative for
+// [[maybe_unused]].
+template <typename T,
+ typename std::enable_if<std::is_base_of<std::exception, T>::value, bool>::type = true>
+[[noreturn]] inline constexpr void throw_or_abort([[gnu::unused]] const char* reason) {
+#if defined(__cpp_exceptions) && __cpp_exceptions >= 199711L
+ throw T(reason);
+#else
+ __builtin_abort();
+#endif
+}
+
+template <typename T>
+inline constexpr void throw_or_abort_if_any_impl(const char* reason, bool should_abort) {
+ if (should_abort) {
+ throw_or_abort<T>(reason);
+ }
+}
+
+template <typename T, typename... AbortIf>
+inline constexpr void throw_or_abort_if_any_impl(const char* reason, bool head, AbortIf... tail) {
+ if (head) {
+ throw_or_abort<T>(reason);
+ }
+ throw_or_abort_if_any_impl<T>(reason, tail...);
+}
+
+template <typename T, typename... AbortIf>
+inline constexpr void throw_or_abort_if_any(const char* reason, AbortIf... abort_if) {
+ static_assert(sizeof...(AbortIf) > 0, "Must provide an |abort_if| clause.");
+ static_assert(cpp17::conjunction_v<std::is_same<bool, AbortIf>...>,
+ "|abort_if| arguments must be boolean.");
+ throw_or_abort_if_any_impl<T>(reason, abort_if...);
+}
+
+} // namespace internal
+} // namespace cpp17
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_EXCEPTION_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h
new file mode 100644
index 000000000..16c64d559
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/storage.h
@@ -0,0 +1,847 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_STORAGE_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_STORAGE_H_
+
+#include <cstddef>
+#include <cstdint>
+#include <limits>
+#include <type_traits>
+
+#include "utility.h"
+
+namespace cpp17 {
+namespace internal {
+
+// Type tag to select overloads based on type T.
+template <typename T>
+struct type_tag {
+ using type = T;
+};
+
+// Type tag to select overloads based on index Index.
+template <std::size_t Index>
+struct index_tag {
+ static constexpr std::size_t index = Index;
+};
+
+// Type tag to select trivial initialization.
+enum trivial_init_t { trivial_init_v };
+
+// Type tag to select default initialization.
+enum default_init_t { default_init_v };
+
+// Type tag to select conditional initialization.
+enum maybe_init_t { maybe_init_v };
+
+// Represents the pair (T, Index) in the type system.
+template <typename T, std::size_t Index>
+struct type_index {};
+
+// Represents whether a type is trivially/non-trivially destructible.
+enum class destructor_class {
+ trivial,
+ non_trivial,
+};
+
+// Represents whether a type is trivially/non-trivially copyable.
+enum class copy_class {
+ trivial,
+ non_trivial,
+};
+
+// Represents whether a type is trivially/non-trivially movable.
+enum class move_class {
+ trivial,
+ non_trivial,
+};
+
+// Represents the full complement of move/copy/destruct classes for a type.
+template <destructor_class DestructorClass, copy_class CopyClass, move_class MoveClass>
+struct storage_class {};
+
+template <typename... Ts>
+using make_storage_class =
+ storage_class<is_trivially_destructible_v<Ts...> ? destructor_class::trivial
+ : destructor_class::non_trivial,
+ is_trivially_copyable_v<Ts...> ? copy_class::trivial : copy_class::non_trivial,
+ is_trivially_movable_v<Ts...> ? move_class::trivial : move_class::non_trivial>;
+
+// A trivial type for the empty alternative of union-based storage.
+struct empty_type {};
+
+// Index type used to track the active variant. Tracking uses zero-based
+// indices. Empty is denoted by the maximum representable value.
+using index_type = std::size_t;
+
+// Index denoting that no user-specified variant is active. Take care not to
+// ODR-use this value.
+constexpr index_type empty_index = std::numeric_limits<index_type>::max();
+
+#ifdef NDEBUG
+#define LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT __builtin_unreachable
+#else
+#define LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT __builtin_abort
+#endif
+
+// Base type for lazy-initialized union storage types. This type implements a
+// recursive union of the element types in Ts. Specializations handle the
+// recursive and terminal cases, and the different storage requirements for
+// trivially/non-trivially destructible types.
+template <destructor_class, typename...>
+union storage_base;
+
+// Non-trivial terminal case.
+template <>
+union storage_base<destructor_class::non_trivial, type_index<empty_type, empty_index>> {
+ storage_base() : empty{} {}
+
+ template <typename... Args>
+ storage_base(type_tag<empty_type>, Args&&...) : empty{} {}
+ template <typename... Args>
+ storage_base(index_tag<empty_index>, Args&&...) : empty{} {}
+
+ // Non-trivial destructor.
+ ~storage_base() {}
+
+ storage_base(const storage_base&) = default;
+ storage_base(storage_base&&) = default;
+ storage_base& operator=(const storage_base&) = default;
+ storage_base& operator=(storage_base&&) = default;
+
+ void construct_at(std::size_t index, const storage_base&) {
+ if (index == empty_index) {
+ new (&empty) empty_type{};
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+ void construct_at(std::size_t index, storage_base&&) {
+ if (index == empty_index) {
+ new (&empty) empty_type{};
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+
+ void assign_at(std::size_t index, const storage_base& other) {
+ if (index == empty_index) {
+ empty = other.empty;
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+ void assign_at(std::size_t index, storage_base&& other) {
+ if (index == empty_index) {
+ empty = std::move(other.empty);
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+
+ void swap_at(std::size_t index, storage_base& other) {
+ if (index == empty_index) {
+ using std::swap;
+ swap(empty, other.empty);
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+
+ template <typename... Args>
+ std::size_t construct(type_tag<empty_type>, Args&&...) {
+ new (&empty) empty_type{};
+ return empty_index;
+ }
+ template <typename... Args>
+ std::size_t construct(index_tag<empty_index>, Args&&...) {
+ new (&empty) empty_type{};
+ return empty_index;
+ }
+
+ void reset(std::size_t index) {
+ if (index == empty_index) {
+ empty.empty_type::~empty_type();
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+
+ empty_type& get(type_tag<empty_type>) { return empty; }
+ const empty_type& get(type_tag<empty_type>) const { return empty; }
+ empty_type& get(index_tag<empty_index>) { return empty; }
+ const empty_type& get(index_tag<empty_index>) const { return empty; }
+
+ std::size_t index(type_tag<empty_type>) const { return empty_index; }
+
+ template <typename V>
+ bool visit(std::size_t, V&&) {
+ return false;
+ }
+ template <typename V>
+ bool visit(std::size_t, V&&) const {
+ return false;
+ }
+
+ empty_type empty;
+};
+
+// Trivial terminal case.
+template <>
+union storage_base<destructor_class::trivial, type_index<empty_type, empty_index>> {
+ constexpr storage_base() : empty{} {}
+
+ template <typename... Args>
+ constexpr storage_base(type_tag<empty_type>, Args&&...) : empty{} {}
+ template <typename... Args>
+ constexpr storage_base(index_tag<empty_index>, Args&&...) : empty{} {}
+
+ // Trivial destructor.
+ ~storage_base() = default;
+
+ constexpr storage_base(const storage_base&) = default;
+ constexpr storage_base(storage_base&&) = default;
+ constexpr storage_base& operator=(const storage_base&) = default;
+ constexpr storage_base& operator=(storage_base&&) = default;
+
+ constexpr void construct_at(std::size_t index, const storage_base&) {
+ if (index == empty_index) {
+ new (&empty) empty_type{};
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+ constexpr void construct_at(std::size_t index, storage_base&&) {
+ if (index == empty_index) {
+ new (&empty) empty_type{};
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+
+ constexpr void assign_at(std::size_t index, const storage_base& other) {
+ if (index == empty_index) {
+ empty = other.empty;
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+ constexpr void assign_at(std::size_t index, storage_base&& other) {
+ if (index == empty_index) {
+ empty = std::move(other.empty);
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+
+ constexpr void swap_at(std::size_t index, storage_base& other) {
+ if (index == empty_index) {
+ using std::swap;
+ swap(empty, other.empty);
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+
+ template <typename... Args>
+ constexpr std::size_t construct(type_tag<empty_type>, Args&&...) {
+ new (&empty) empty_type{};
+ return empty_index;
+ }
+ template <typename... Args>
+ constexpr std::size_t construct(index_tag<empty_index>, Args&&...) {
+ new (&empty) empty_type{};
+ return empty_index;
+ }
+
+ constexpr void reset(std::size_t index) {
+ if (index == empty_index) {
+ empty.empty_type::~empty_type();
+ } else {
+ LIB_STDCOMPAT_INTERNAL_UNREACHABLE_OR_ABORT();
+ }
+ }
+
+ constexpr empty_type& get(type_tag<empty_type>) { return empty; }
+ constexpr const empty_type& get(type_tag<empty_type>) const { return empty; }
+ constexpr empty_type& get(index_tag<empty_index>) { return empty; }
+ constexpr const empty_type& get(index_tag<empty_index>) const { return empty; }
+
+ constexpr std::size_t index(type_tag<empty_type>) const { return empty_index; }
+
+ template <typename V>
+ constexpr bool visit(std::size_t, V&&) {
+ return false;
+ }
+ template <typename V>
+ constexpr bool visit(std::size_t, V&&) const {
+ return false;
+ }
+
+ empty_type empty;
+};
+
+template <typename T, std::size_t Index, typename... Ts, std::size_t... Is>
+union storage_base<destructor_class::non_trivial, type_index<T, Index>, type_index<Ts, Is>...> {
+ storage_base() : empty{} {}
+
+ template <typename... Args>
+ storage_base(type_tag<T>, Args&&... args) : value(std::forward<Args>(args)...) {}
+ template <typename... Args>
+ storage_base(index_tag<Index>, Args&&... args) : value(std::forward<Args>(args)...) {}
+
+ template <typename U, typename... Args>
+ storage_base(type_tag<U>, Args&&... args) : rest(type_tag<U>{}, std::forward<Args>(args)...) {}
+ template <std::size_t OtherIndex, typename... Args>
+ storage_base(index_tag<OtherIndex>, Args&&... args)
+ : rest(index_tag<OtherIndex>{}, std::forward<Args>(args)...) {}
+
+ // Non-trivial destructor.
+ ~storage_base() {}
+
+ // Trival copy/move construction and assignment.
+ storage_base(const storage_base&) = default;
+ storage_base(storage_base&&) = default;
+ storage_base& operator=(const storage_base&) = default;
+ storage_base& operator=(storage_base&&) = default;
+
+ void construct_at(std::size_t index, const storage_base& other) {
+ if (index == Index) {
+ new (&value) T{other.value};
+ } else {
+ rest.construct_at(index, other.rest);
+ }
+ }
+ void construct_at(std::size_t index, storage_base&& other) {
+ if (index == Index) {
+ new (&value) T{std::move(other.value)};
+ } else {
+ rest.construct_at(index, std::move(other.rest));
+ }
+ }
+
+ void assign_at(std::size_t index, const storage_base& other) {
+ if (index == Index) {
+ value = other.value;
+ } else {
+ rest.assign_at(index, other.rest);
+ }
+ }
+ void assign_at(std::size_t index, storage_base&& other) {
+ if (index == Index) {
+ value = std::move(other.value);
+ } else {
+ rest.assign_at(index, std::move(other.rest));
+ }
+ }
+
+ void swap_at(std::size_t index, storage_base& other) {
+ if (index == Index) {
+ using std::swap;
+ swap(value, other.value);
+ } else {
+ rest.swap_at(index, other.rest);
+ }
+ }
+
+ template <typename... Args>
+ std::size_t construct(type_tag<T>, Args&&... args) {
+ new (&value) T(std::forward<Args>(args)...);
+ return Index;
+ }
+ template <typename U, typename... Args>
+ std::size_t construct(type_tag<U>, Args&&... args) {
+ return rest.construct(type_tag<U>{}, std::forward<Args>(args)...);
+ }
+ template <typename... Args>
+ std::size_t construct(index_tag<Index>, Args&&... args) {
+ new (&value) T(std::forward<Args>(args)...);
+ return Index;
+ }
+ template <std::size_t OtherIndex, typename... Args>
+ std::size_t construct(index_tag<OtherIndex>, Args&&... args) {
+ return rest.construct(index_tag<OtherIndex>{}, std::forward<Args>(args)...);
+ }
+
+ void reset(std::size_t index) {
+ if (index == Index) {
+ value.~T();
+ } else {
+ rest.reset(index);
+ }
+ }
+
+ T& get(type_tag<T>) { return value; }
+ const T& get(type_tag<T>) const { return value; }
+ template <typename U>
+ U& get(type_tag<U>) {
+ return rest.get(type_tag<U>{});
+ }
+ template <typename U>
+ const U& get(type_tag<U>) const {
+ return rest.get(type_tag<U>{});
+ }
+ T& get(index_tag<Index>) { return value; }
+ const T& get(index_tag<Index>) const { return value; }
+ template <std::size_t OtherIndex>
+ auto& get(index_tag<OtherIndex>) {
+ return rest.get(index_tag<OtherIndex>{});
+ }
+ template <std::size_t OtherIndex>
+ const auto& get(index_tag<OtherIndex>) const {
+ return rest.get(index_tag<OtherIndex>{});
+ }
+
+ std::size_t index(type_tag<T>) const { return Index; }
+ template <typename U>
+ std::size_t index(type_tag<U>) const {
+ return rest.index(type_tag<U>{});
+ }
+
+ template <typename V>
+ bool visit(std::size_t index, V&& visitor) {
+ if (index == Index) {
+ std::forward<V>(visitor)(type_tag<T>{}, index_tag<Index>{}, this);
+ return true;
+ } else {
+ return rest.visit(index, std::forward<V>(visitor));
+ }
+ }
+ template <typename V>
+ bool visit(std::size_t index, V&& visitor) const {
+ if (index == Index) {
+ std::forward<V>(visitor)(type_tag<T>{}, index_tag<Index>{}, this);
+ return true;
+ } else {
+ return rest.visit(index, std::forward<V>(visitor));
+ }
+ }
+
+ empty_type empty;
+ T value;
+ storage_base<destructor_class::non_trivial, type_index<Ts, Is>...> rest;
+};
+
+template <typename T, std::size_t Index, typename... Ts, std::size_t... Is>
+union storage_base<destructor_class::trivial, type_index<T, Index>, type_index<Ts, Is>...> {
+ constexpr storage_base() : empty{} {}
+
+ template <typename... Args>
+ constexpr storage_base(type_tag<T>, Args&&... args) : value(std::forward<Args>(args)...) {}
+ template <typename... Args>
+ constexpr storage_base(index_tag<Index>, Args&&... args) : value(std::forward<Args>(args)...) {}
+
+ template <typename U, typename... Args>
+ constexpr storage_base(type_tag<U>, Args&&... args)
+ : rest(type_tag<U>{}, std::forward<Args>(args)...) {}
+ template <std::size_t OtherIndex, typename... Args>
+ constexpr storage_base(index_tag<OtherIndex>, Args&&... args)
+ : rest(index_tag<OtherIndex>{}, std::forward<Args>(args)...) {}
+
+ // Trivial destructor.
+ ~storage_base() = default;
+
+ // Trival copy/move construction and assignment.
+ constexpr storage_base(const storage_base&) = default;
+ constexpr storage_base(storage_base&&) = default;
+ constexpr storage_base& operator=(const storage_base&) = default;
+ constexpr storage_base& operator=(storage_base&&) = default;
+
+ constexpr void construct_at(std::size_t index, const storage_base& other) {
+ if (index == Index) {
+ new (&value) T{other.value};
+ } else {
+ rest.construct_at(index, other.rest);
+ }
+ }
+ constexpr void construct_at(std::size_t index, storage_base&& other) {
+ if (index == Index) {
+ new (&value) T{std::move(other.value)};
+ } else {
+ rest.construct_at(index, std::move(other.rest));
+ }
+ }
+
+ constexpr void assign_at(std::size_t index, const storage_base& other) {
+ if (index == Index) {
+ value = other.value;
+ } else {
+ rest.assign_at(index, other.rest);
+ }
+ }
+ constexpr void assign_at(std::size_t index, storage_base&& other) {
+ if (index == Index) {
+ value = std::move(other.value);
+ } else {
+ rest.assign_at(index, std::move(other.rest));
+ }
+ }
+
+ constexpr void swap_at(std::size_t index, storage_base& other) {
+ if (index == Index) {
+ using std::swap;
+ swap(value, other.value);
+ } else {
+ rest.swap_at(index, other.rest);
+ }
+ }
+
+ template <typename... Args>
+ constexpr std::size_t construct(type_tag<T>, Args&&... args) {
+ new (&value) T(std::forward<Args>(args)...);
+ return Index;
+ }
+ template <typename U, typename... Args>
+ constexpr std::size_t construct(type_tag<U>, Args&&... args) {
+ return rest.construct(type_tag<U>{}, std::forward<Args>(args)...);
+ }
+ template <typename... Args>
+ constexpr std::size_t construct(index_tag<Index>, Args&&... args) {
+ new (&value) T(std::forward<Args>(args)...);
+ return Index;
+ }
+ template <std::size_t OtherIndex, typename... Args>
+ constexpr std::size_t construct(index_tag<OtherIndex>, Args&&... args) {
+ return rest.construct(index_tag<OtherIndex>{}, std::forward<Args>(args)...);
+ }
+
+ constexpr void reset(std::size_t) {}
+
+ constexpr T& get(type_tag<T>) { return value; }
+ constexpr const T& get(type_tag<T>) const { return value; }
+ template <typename U>
+ constexpr U& get(type_tag<U>) {
+ return rest.get(type_tag<U>{});
+ }
+ template <typename U>
+ constexpr const U& get(type_tag<U>) const {
+ return rest.get(type_tag<U>{});
+ }
+ constexpr T& get(index_tag<Index>) { return value; }
+ constexpr const T& get(index_tag<Index>) const { return value; }
+ template <std::size_t OtherIndex>
+ constexpr auto& get(index_tag<OtherIndex>) {
+ return rest.get(index_tag<OtherIndex>{});
+ }
+ template <std::size_t OtherIndex>
+ constexpr const auto& get(index_tag<OtherIndex>) const {
+ return rest.get(index_tag<OtherIndex>{});
+ }
+
+ constexpr std::size_t index(type_tag<T>) const { return Index; }
+ template <typename U>
+ constexpr std::size_t index(type_tag<U>) const {
+ return rest.index(type_tag<U>{});
+ }
+
+ template <typename V>
+ constexpr bool visit(std::size_t index, V&& visitor) {
+ if (index == Index) {
+ std::forward<V>(visitor)(type_tag<T>{}, index_tag<Index>{}, this);
+ return true;
+ } else {
+ return rest.visit(index, std::forward<V>(visitor));
+ }
+ }
+ template <typename V>
+ constexpr bool visit(std::size_t index, V&& visitor) const {
+ if (index == Index) {
+ std::forward<V>(visitor)(type_tag<T>{}, index_tag<Index>{}, this);
+ return true;
+ } else {
+ return rest.visit(index, std::forward<V>(visitor));
+ }
+ }
+
+ empty_type empty;
+ T value;
+ storage_base<destructor_class::trivial, type_index<Ts, Is>...> rest;
+};
+
+// Lazy-initialized union storage type that tracks the index of the active
+// variant.
+template <destructor_class, typename...>
+class indexed_storage;
+
+template <destructor_class DestructorClass, typename... Ts, std::size_t... Is>
+class indexed_storage<DestructorClass, type_index<Ts, Is>...> {
+ private:
+ using base_type =
+ storage_base<DestructorClass, type_index<Ts, Is>..., type_index<empty_type, empty_index>>;
+
+ public:
+ static constexpr bool nothrow_default_constructible =
+ std::is_nothrow_default_constructible<first_t<Ts...>>::value;
+ static constexpr bool nothrow_move_constructible =
+ conjunction_v<std::is_nothrow_move_constructible<Ts>...>;
+ static constexpr bool nothrow_move_assignable =
+ conjunction_v<std::is_nothrow_move_assignable<Ts>...>;
+
+ constexpr indexed_storage() = default;
+
+ constexpr indexed_storage(trivial_init_t) : indexed_storage{} {}
+
+ constexpr indexed_storage(default_init_t) : index_{0}, base_{index_tag<0>{}} {}
+
+ // Only used by trivial copy/move types.
+ constexpr indexed_storage(const indexed_storage& other) = default;
+ constexpr indexed_storage& operator=(const indexed_storage& other) = default;
+ constexpr indexed_storage(indexed_storage&& other) = default;
+ constexpr indexed_storage& operator=(indexed_storage&& other) = default;
+
+ template <typename T, typename... Args>
+ constexpr indexed_storage(type_tag<T>, Args&&... args)
+ : base_(type_tag<T>{}, std::forward<Args>(args)...) {
+ index_ = base_.index(type_tag<T>{});
+ }
+ template <std::size_t Index, typename... Args>
+ constexpr indexed_storage(index_tag<Index>, Args&&... args)
+ : index_{Index}, base_(index_tag<Index>{}, std::forward<Args>(args)...) {}
+
+ constexpr indexed_storage(maybe_init_t, const indexed_storage& other)
+ : index_{other.index()}, base_{} {
+ base_.construct_at(other.index(), other.base_);
+ }
+ constexpr indexed_storage(maybe_init_t, indexed_storage&& other)
+ : index_{other.index()}, base_{} {
+ base_.construct_at(other.index(), std::move(other.base_));
+ }
+
+ ~indexed_storage() = default;
+
+ constexpr index_type index() const { return index_; }
+ constexpr bool is_empty() const { return index() == empty_index; }
+ template <typename T>
+ constexpr bool has_value(type_tag<T>) const {
+ return index() == base_.index(type_tag<T>{});
+ }
+ template <std::size_t Index>
+ constexpr bool has_value(index_tag<Index>) const {
+ return index() == Index;
+ }
+
+ template <typename T>
+ constexpr auto& get(type_tag<T>) {
+ return base_.get(type_tag<T>{});
+ }
+ template <typename T>
+ constexpr const auto& get(type_tag<T>) const {
+ return base_.get(type_tag<T>{});
+ }
+ template <std::size_t Index>
+ constexpr auto& get(index_tag<Index>) {
+ return base_.get(index_tag<Index>{});
+ }
+ template <std::size_t Index>
+ constexpr const auto& get(index_tag<Index>) const {
+ return base_.get(index_tag<Index>{});
+ }
+
+ template <typename T, typename... Args>
+ constexpr void construct(type_tag<T>, Args&&... args) {
+ index_ = base_.construct(type_tag<T>{}, std::forward<Args>(args)...);
+ }
+ template <std::size_t Index, typename... Args>
+ constexpr void construct(index_tag<Index>, Args&&... args) {
+ index_ = base_.construct(index_tag<Index>{}, std::forward<Args>(args)...);
+ }
+
+ constexpr void assign(const indexed_storage& other) {
+ if (index() == other.index()) {
+ base_.assign_at(index_, other.base_);
+ } else {
+ reset();
+ base_.construct_at(other.index_, other.base_);
+ index_ = other.index_;
+ }
+ }
+ constexpr void assign(indexed_storage&& other) {
+ if (index() == other.index()) {
+ base_.assign_at(index_, std::move(other.base_));
+ } else {
+ reset();
+ base_.construct_at(other.index_, std::move(other.base_));
+ index_ = other.index_;
+ }
+ }
+
+ template <typename V>
+ constexpr bool visit(V&& visitor) {
+ return base_.visit(index_, std::forward<V>(visitor));
+ }
+ template <typename V>
+ constexpr bool visit(V&& visitor) const {
+ return base_.visit(index_, std::forward<V>(visitor));
+ }
+
+ constexpr void swap(indexed_storage& other) {
+ if (index() == other.index()) {
+ // Swap directly when the variants are the same, including empty.
+ base_.swap_at(index_, other.base_);
+ } else {
+ // Swap when the variants are different, including one being empty.
+ // This approach avoids GCC -Wmaybe-uninitialized warnings by
+ // initializing and accessing |temp| unconditionally within a
+ // conditional scope. The alternative, using the maybe_init_t
+ // constructor confuses GCC because it doesn't understand that the
+ // index checks prevent uninitialized access.
+ auto do_swap = [](indexed_storage& a, indexed_storage& b) {
+ return a.base_.visit(a.index_, [&a, &b](auto, auto index_tag_v, auto* element) {
+ indexed_storage temp{index_tag_v, std::move(element->value)};
+ a.reset();
+
+ a.base_.construct_at(b.index_, std::move(b.base_));
+ a.index_ = b.index_;
+ b.reset();
+
+ b.base_.construct_at(temp.index_, std::move(temp.base_));
+ b.index_ = temp.index_;
+ temp.reset();
+ });
+ };
+
+ // The visitor above returns false when the first argument is empty
+ // and no action is taken. In that case, the other order is tried to
+ // complete the half-empty swap.
+ do_swap(*this, other) || do_swap(other, *this);
+ }
+ }
+
+ // Destroys the active variant. Does nothing when already empty.
+ constexpr void reset() {
+ base_.reset(index_);
+ index_ = empty_index;
+ }
+
+ private:
+ index_type index_{empty_index};
+ base_type base_;
+};
+
+// Internal variant storage type used by cpp17::optional and cpp17::variant.
+// Specializations of this type select trivial vs. non-trivial copy/move
+// construction, assignment operators, and destructor based on the storage class
+// of the types in Ts.
+template <typename StorageClass, typename... Ts>
+struct storage;
+
+template <typename... Ts, std::size_t... Is>
+struct storage<storage_class<destructor_class::trivial, copy_class::trivial, move_class::trivial>,
+ type_index<Ts, Is>...>
+ : indexed_storage<destructor_class::trivial, type_index<Ts, Is>...> {
+ using base_type = indexed_storage<destructor_class::trivial, type_index<Ts, Is>...>;
+ using base_type::base_type;
+ constexpr storage() = default;
+};
+
+template <typename... Ts, std::size_t... Is>
+struct storage<
+ storage_class<destructor_class::trivial, copy_class::non_trivial, move_class::trivial>,
+ type_index<Ts, Is>...> : indexed_storage<destructor_class::trivial, type_index<Ts, Is>...> {
+ using base_type = indexed_storage<destructor_class::trivial, type_index<Ts, Is>...>;
+ using base_type::base_type;
+
+ ~storage() = default;
+ constexpr storage() = default;
+
+ constexpr storage(const storage& other) : base_type{maybe_init_v, other} {}
+
+ constexpr storage& operator=(const storage& other) {
+ this->assign(other);
+ return *this;
+ }
+
+ constexpr storage(storage&&) = default;
+ constexpr storage& operator=(storage&&) = default;
+};
+
+template <typename... Ts, std::size_t... Is>
+struct storage<
+ storage_class<destructor_class::trivial, copy_class::trivial, move_class::non_trivial>,
+ type_index<Ts, Is>...> : indexed_storage<destructor_class::trivial, type_index<Ts, Is>...> {
+ using base_type = indexed_storage<destructor_class::trivial, type_index<Ts, Is>...>;
+ using base_type::base_type;
+
+ ~storage() = default;
+ constexpr storage() = default;
+ constexpr storage(const storage&) = default;
+ constexpr storage& operator=(const storage&) = default;
+
+ constexpr storage(storage&& other) noexcept(base_type::nothrow_move_constructible)
+ : base_type{maybe_init_v, std::move(other)} {}
+
+ constexpr storage& operator=(storage&& other) noexcept(base_type::nothrow_move_assignable) {
+ this->assign(std::move(other));
+ return *this;
+ }
+};
+
+template <typename... Ts, std::size_t... Is>
+struct storage<
+ storage_class<destructor_class::trivial, copy_class::non_trivial, move_class::non_trivial>,
+ type_index<Ts, Is>...> : indexed_storage<destructor_class::trivial, type_index<Ts, Is>...> {
+ using base_type = indexed_storage<destructor_class::trivial, type_index<Ts, Is>...>;
+ using base_type::base_type;
+
+ ~storage() = default;
+ constexpr storage() = default;
+
+ constexpr storage(const storage& other) : base_type{maybe_init_v, other} {}
+
+ constexpr storage& operator=(const storage& other) {
+ this->assign(other);
+ return *this;
+ }
+
+ constexpr storage(storage&& other) noexcept(base_type::nothrow_move_constructible)
+ : base_type{maybe_init_v, std::move(other)} {}
+
+ constexpr storage& operator=(storage&& other) noexcept(base_type::nothrow_move_assignable) {
+ this->assign(std::move(other));
+ return *this;
+ }
+};
+
+// Specialization for non-trivially movable/copyable types. Types with a non-
+// trivial destructor are always non-trivially movable/copyable.
+template <copy_class CopyClass, move_class MoveClass, typename... Ts, std::size_t... Is>
+struct storage<storage_class<destructor_class::non_trivial, CopyClass, MoveClass>,
+ type_index<Ts, Is>...>
+ : indexed_storage<destructor_class::non_trivial, type_index<Ts, Is>...> {
+ using base_type = indexed_storage<destructor_class::non_trivial, type_index<Ts, Is>...>;
+ using base_type::base_type;
+
+ ~storage() { this->reset(); }
+
+ constexpr storage() = default;
+
+ constexpr storage(const storage& other) : base_type{maybe_init_v, other} {}
+
+ constexpr storage& operator=(const storage& other) {
+ this->assign(other);
+ return *this;
+ }
+
+ constexpr storage(storage&& other) noexcept(base_type::nothrow_move_constructible)
+ : base_type{maybe_init_v, std::move(other)} {}
+
+ constexpr storage& operator=(storage&& other) noexcept(base_type::nothrow_move_assignable) {
+ this->assign(std::move(other));
+ return *this;
+ }
+};
+
+template <typename... Ts, std::size_t... Is>
+constexpr auto make_storage(std::index_sequence<Is...>) {
+ return storage<make_storage_class<Ts...>, type_index<Ts, Is>...>{};
+}
+
+template <typename... Ts>
+using storage_type = decltype(make_storage<Ts...>(std::index_sequence_for<Ts...>{}));
+
+} // namespace internal
+} // namespace cpp17
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_STORAGE_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h
new file mode 100644
index 000000000..dced28d9f
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/type_traits.h
@@ -0,0 +1,112 @@
+// Copyright 2021 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
+
+#include <type_traits>
+
+namespace cpp17 {
+namespace internal {
+
+template <typename T>
+static constexpr bool is_reference_wrapper = false;
+template <typename T>
+static constexpr bool is_reference_wrapper<std::reference_wrapper<T>> = true;
+
+// These are from [func.require] ¶ 1.1-7
+template <typename MemFn, typename Class, typename T>
+static constexpr bool invoke_pmf_base = std::is_member_function_pointer<MemFn Class::*>::value&&
+ std::is_base_of<Class, std::remove_reference_t<T>>::value;
+
+template <typename MemFn, typename Class, typename T>
+static constexpr bool invoke_pmf_refwrap = std::is_member_function_pointer<MemFn Class::*>::value&&
+ is_reference_wrapper<std::remove_cv_t<std::remove_reference_t<T>>>;
+
+template <typename MemFn, typename Class, typename T>
+static constexpr bool invoke_pmf_other =
+ std::is_member_function_pointer<MemFn Class::*>::value && !invoke_pmf_base<MemFn, Class, T> &&
+ !invoke_pmf_refwrap<MemFn, Class, T>;
+
+template <typename MemObj, typename Class, typename T>
+static constexpr bool invoke_pmd_base = std::is_member_object_pointer<MemObj Class::*>::value&&
+ std::is_base_of<Class, std::remove_reference_t<T>>::value;
+
+template <typename MemObj, typename Class, typename T>
+static constexpr bool invoke_pmd_refwrap = std::is_member_object_pointer<MemObj Class::*>::value&&
+ is_reference_wrapper<std::remove_cv_t<std::remove_reference_t<T>>>;
+
+template <typename MemObj, typename Class, typename T>
+static constexpr bool invoke_pmd_other =
+ std::is_member_object_pointer<MemObj Class::*>::value && !invoke_pmd_base<MemObj, Class, T> &&
+ !invoke_pmd_refwrap<MemObj, Class, T>;
+
+// ¶ 1.7 says to just return f(t1, t2, ..., tn) in all other cases
+
+// Just internal forward declarations for SFINAE; cpp20::invoke is defined in
+// lib/stdcompat/functional.h
+template <typename MemFn, typename Class, typename T, typename... Args>
+constexpr auto invoke(MemFn Class::*f, T&& obj, Args&&... args)
+ -> std::enable_if_t<invoke_pmf_base<MemFn, Class, T>,
+ decltype((std::forward<T>(obj).*f)(std::forward<Args>(args)...))>;
+
+template <typename MemFn, typename Class, typename T, typename... Args>
+constexpr auto invoke(MemFn Class::*f, T&& obj, Args&&... args)
+ -> std::enable_if_t<invoke_pmf_refwrap<MemFn, Class, T>,
+ decltype((obj.get().*f)(std::forward<Args>(args)...))>;
+
+template <typename MemFn, typename Class, typename T, typename... Args>
+constexpr auto invoke(MemFn Class::*f, T&& obj, Args&&... args)
+ -> std::enable_if_t<invoke_pmf_other<MemFn, Class, T>,
+ decltype(((*std::forward<T>(obj)).*f)(std::forward<Args>(args)...))>;
+
+template <typename MemObj, typename Class, typename T>
+constexpr auto invoke(MemObj Class::*f, T&& obj)
+ -> std::enable_if_t<invoke_pmd_base<MemObj, Class, T>, decltype(std::forward<T>(obj).*f)>;
+
+template <typename MemObj, typename Class, typename T>
+constexpr auto invoke(MemObj Class::*f, T&& obj)
+ -> std::enable_if_t<invoke_pmd_refwrap<MemObj, Class, T>, decltype(obj.get().*f)>;
+
+template <typename MemObj, typename Class, typename T>
+constexpr auto invoke(MemObj Class::*f, T&& obj)
+ -> std::enable_if_t<invoke_pmd_other<MemObj, Class, T>, decltype((*std::forward<T>(obj)).*f)>;
+
+template <typename F, typename... Args>
+constexpr auto invoke(F&& f, Args&&... args)
+ -> decltype(std::forward<F>(f)(std::forward<Args>(args)...));
+
+template <typename R, typename F, typename... Args,
+ typename = std::enable_if_t<std::is_void<R>::value>>
+constexpr auto invoke_r(F&& f, Args&&... args)
+ -> decltype(static_cast<void>(::cpp17::internal::invoke(std::forward<F>(f),
+ std::forward<Args>(args)...)));
+
+template <typename R, typename F, typename... Args,
+ typename = std::enable_if_t<!std::is_void<R>::value>>
+constexpr auto invoke_r(F&& f, Args&&... args)
+ -> std::enable_if_t<std::is_convertible<decltype(::cpp17::internal::invoke(
+ std::forward<F>(f), std::forward<Args>(args)...)),
+ R>::value,
+ R>;
+
+template <typename R, typename F, typename... Args>
+constexpr auto is_valid_invoke(std::nullptr_t)
+ -> decltype(invoke_r<R>(std::declval<F>(), std::declval<Args>()...), std::true_type());
+
+template <typename R, typename F, typename... Args>
+constexpr std::false_type is_valid_invoke(...);
+
+template <bool Enable, typename F, typename... Args>
+struct invoke_result {};
+
+template <typename F, typename... Args>
+struct invoke_result<true, F, Args...> {
+ using type = decltype(::cpp17::internal::invoke(std::declval<F>(), std::declval<Args>()...));
+};
+
+} // namespace internal
+} // namespace cpp17
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_TYPE_TRAITS_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h
new file mode 100644
index 000000000..a5da381ae
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/internal/utility.h
@@ -0,0 +1,137 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_UTILITY_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_UTILITY_H_
+
+#include <cstddef>
+#include <utility>
+
+#include "../type_traits.h"
+
+namespace cpp17 {
+namespace internal {
+
+template <typename Tag>
+struct instantiate_templated_tag {
+ static constexpr const Tag storage{};
+};
+
+template <typename Tag>
+constexpr const Tag instantiate_templated_tag<Tag>::storage;
+
+template <typename T, typename ValueType, ValueType Value>
+struct inline_storage {
+ static constexpr const ValueType storage{Value};
+};
+
+template <typename T, typename ValueType, ValueType Value>
+constexpr const ValueType inline_storage<T, ValueType, Value>::storage;
+
+// Utility to return the first type in a parameter pack.
+template <typename... Ts>
+struct first;
+template <typename First, typename... Rest>
+struct first<First, Rest...> {
+ using type = First;
+};
+
+template <typename... Ts>
+using first_t = typename first<Ts...>::type;
+
+// Utility to count the occurences of type T in the parameter pack Ts.
+template <typename T, typename... Ts>
+struct occurences_of : std::integral_constant<std::size_t, 0> {};
+template <typename T, typename U>
+struct occurences_of<T, U> : std::integral_constant<std::size_t, std::is_same<T, U>::value> {};
+template <typename T, typename First, typename... Rest>
+struct occurences_of<T, First, Rest...>
+ : std::integral_constant<std::size_t,
+ occurences_of<T, First>::value + occurences_of<T, Rest...>::value> {};
+
+template <typename T, typename... Ts>
+constexpr std::size_t occurences_of_v = occurences_of<T, Ts...>::value;
+
+// Evaluates to truth-like when type T matches type U with cv-reference removed.
+template <typename T, typename U>
+using not_same_type = negation<std::is_same<T, ::cpp20::remove_cvref_t<U>>>;
+
+// Concept helper for constructors.
+template <typename... Conditions>
+using requires_conditions = std::enable_if_t<conjunction_v<Conditions...>, bool>;
+
+// Concept helper for assignment operators.
+template <typename Return, typename... Conditions>
+using assignment_requires_conditions =
+ std::enable_if_t<conjunction_v<Conditions...>, std::add_lvalue_reference_t<Return>>;
+
+// Evaluates to true when every element type of Ts is trivially destructible.
+template <typename... Ts>
+constexpr bool is_trivially_destructible_v = conjunction_v<std::is_trivially_destructible<Ts>...>;
+
+// Evaluates to true when every element type of Ts is trivially copyable.
+template <typename... Ts>
+constexpr bool is_trivially_copyable_v =
+ (conjunction_v<std::is_trivially_copy_assignable<Ts>...> &&
+ conjunction_v<std::is_trivially_copy_constructible<Ts>...>);
+
+// Evaluates to true when every element type of Ts is trivially movable.
+template <typename... Ts>
+constexpr bool is_trivially_movable_v =
+ (conjunction_v<std::is_trivially_move_assignable<Ts>...> &&
+ conjunction_v<std::is_trivially_move_constructible<Ts>...>);
+
+// Enable if relational operator is convertible to bool and the optional
+// conditions are true.
+template <typename Op, typename... Conditions>
+using enable_relop_t =
+ std::enable_if_t<(std::is_convertible<Op, bool>::value && conjunction_v<Conditions...>), bool>;
+
+// Returns true when T is a complete type or an unbounded array.
+template <typename T, std::size_t = sizeof(T)>
+constexpr bool is_complete_or_unbounded_array(::cpp20::type_identity<T>) {
+ return true;
+}
+template <typename Identity, typename T = typename Identity::type>
+constexpr bool is_complete_or_unbounded_array(Identity) {
+ return disjunction<std::is_reference<T>, std::is_function<T>, std::is_void<T>,
+ ::cpp20::is_unbounded_array<T>>::value;
+}
+
+// Using swap for ADL. This directive is contained within the cpp17::internal
+// namespace, which prevents leaking std::swap into user namespaces. Doing this
+// at namespace scope is necessary to lookup swap via ADL while preserving the
+// noexcept() specification of the resulting lookup.
+using std::swap;
+
+// Evaluates to true when T is swappable.
+template <typename T, typename = void>
+struct is_swappable : std::false_type {
+ static_assert(is_complete_or_unbounded_array(::cpp20::type_identity<T>{}),
+ "T must be a complete type or an unbounded array!");
+};
+template <typename T>
+struct is_swappable<T, void_t<decltype(swap(std::declval<T&>(), std::declval<T&>()))>>
+ : std::true_type {
+ static_assert(is_complete_or_unbounded_array(::cpp20::type_identity<T>{}),
+ "T must be a complete type or an unbounded array!");
+};
+
+// Evaluates to true when T is nothrow swappable.
+template <typename T, typename = void>
+struct is_nothrow_swappable : std::false_type {
+ static_assert(is_complete_or_unbounded_array(::cpp20::type_identity<T>{}),
+ "T must be a complete type or an unbounded array!");
+};
+template <typename T>
+struct is_nothrow_swappable<T, void_t<decltype(swap(std::declval<T&>(), std::declval<T&>()))>>
+ : std::integral_constant<bool, noexcept(swap(std::declval<T&>(), std::declval<T&>()))> {
+ static_assert(is_complete_or_unbounded_array(::cpp20::type_identity<T>{}),
+ "T must be a complete type or an unbounded array!");
+};
+
+} // namespace internal
+} // namespace cpp17
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_INTERNAL_UTILITY_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h
new file mode 100644
index 000000000..2dc3198a4
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/memory.h
@@ -0,0 +1,66 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_MEMORY_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_MEMORY_H_
+
+#include <memory>
+
+#include "version.h"
+
+namespace cpp17 {
+
+#if defined(__cpp_lib_addressof_constexpr) && __cpp_lib_addressof_constexpr >= 201603L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::addressof;
+
+#else // Provide constexpr polyfill for addressof.
+
+template <typename T>
+constexpr T* addressof(T& arg) noexcept {
+ return __builtin_addressof(arg);
+}
+
+template <typename T>
+const T* addressof(const T&&) = delete;
+
+#endif // __cpp_lib_addressof_constexpr >= 201603L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+} // namespace cpp17
+
+namespace cpp20 {
+
+#if defined(__cpp_lib_to_address) && __cpp_lib_to_address >= 201711L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::to_address;
+
+#else // Provide to_address polyfill.
+
+template <typename T>
+constexpr T* to_address(T* pointer) noexcept {
+ static_assert(!std::is_function<T>::value, "Cannot pass function pointers to std::to_address()");
+ return pointer;
+}
+
+// TODO(fxbug.dev/70523): This std::pointer_traits stuff is only to be bug-compatible with the
+// standard library implementations; switch back to auto when the linked bug is resolved.
+template <typename T>
+constexpr typename std::pointer_traits<T>::element_type* to_address(const T& pointer) noexcept {
+ static_assert(
+ std::is_same<decltype(pointer.operator->()),
+ typename std::pointer_traits<T>::element_type*>::value,
+ "For compatibility with libc++ and libstdc++, operator->() must return "
+ "typename std::pointer_traits<T>::element_type*. 'Chaining' operator->() in "
+ "cpp20::to_address() will not be permitted until https://fxbug.dev/70523 is resolved.");
+
+ return to_address(pointer.operator->());
+}
+
+#endif // __cpp_lib_to_address >= 201711L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+} // namespace cpp20
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_MEMORY_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h
new file mode 100644
index 000000000..bc6a421a0
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/optional.h
@@ -0,0 +1,475 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_OPTIONAL_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_OPTIONAL_H_
+
+#include "utility.h"
+#include "version.h"
+
+#if defined(__cpp_lib_optional) && __cpp_lib_optional >= 201606L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#include <optional>
+
+namespace cpp17 {
+
+using std::bad_optional_access;
+using std::make_optional;
+using std::nullopt;
+using std::nullopt_t;
+using std::optional;
+
+} // namespace cpp17
+
+#else // Provide std::optional and std::nullopt_t polyfill.
+
+#include <cstdlib>
+#include <exception>
+#include <new>
+
+#include "internal/constructors.h"
+#include "internal/exception.h"
+#include "internal/storage.h"
+#include "internal/utility.h"
+#include "type_traits.h"
+
+namespace cpp17 {
+
+// A sentinel value for indicating that it contains no value.
+struct nullopt_t {
+ explicit constexpr nullopt_t(int) {}
+};
+static constexpr nullopt_t nullopt{0};
+
+// Exception type to report bad accesses to optional.
+class bad_optional_access : public std::exception {
+ public:
+ bad_optional_access() noexcept {}
+
+ const char* what() const noexcept override { return reason_; }
+
+ private:
+ template <typename T>
+ friend class optional;
+
+ bad_optional_access(const char* reason) noexcept : reason_{reason} {}
+
+ // String describing the reason for the bad access. Must point to a string
+ // with static storage duration.
+ const char* reason_;
+
+ template <typename T,
+ typename std::enable_if<std::is_base_of<std::exception, T>::value, bool>::type>
+ friend constexpr void cpp17::internal::throw_or_abort(const char*);
+};
+
+// A reasonably complete implementation of std::optional compatible with C++14.
+template <typename T>
+class optional : private ::cpp17::internal::modulate_copy_and_move<T> {
+ private:
+ // Helper types and values for SFINAE and noexcept rules.
+ static constexpr bool nothrow_move_constructible = std::is_nothrow_move_constructible<T>::value;
+
+ static constexpr bool nothrow_swappable = std::is_nothrow_move_constructible<T>::value &&
+ ::cpp17::internal::is_nothrow_swappable<T>::value;
+
+ static constexpr auto trivial_init_v = ::cpp17::internal::trivial_init_v;
+ static constexpr auto maybe_init_v = ::cpp17::internal::maybe_init_v;
+ using type_tag = ::cpp17::internal::type_tag<T>;
+
+ template <typename U, typename V>
+ using converts_from_optional = disjunction<
+ std::is_constructible<U, const optional<V>&>, std::is_constructible<U, optional<V>&>,
+ std::is_constructible<U, const optional<V>&&>, std::is_constructible<U, optional<V>&&>,
+ std::is_convertible<const optional<V>&, U>, std::is_convertible<optional<V>&, U>,
+ std::is_convertible<const optional<V>&&, U>, std::is_convertible<optional<V>&&, U>>;
+
+ template <typename U, typename V>
+ using assigns_from_optional =
+ disjunction<std::is_assignable<U&, const optional<V>&>, std::is_assignable<U&, optional<V>&>,
+ std::is_assignable<U&, const optional<V>&&>,
+ std::is_assignable<U&, optional<V>&&>>;
+
+ template <typename U>
+ using not_self_type = ::cpp17::internal::not_same_type<optional, U>;
+
+ template <typename U>
+ using not_in_place = ::cpp17::internal::not_same_type<in_place_t, U>;
+
+ template <typename... Conditions>
+ using requires_conditions = ::cpp17::internal::requires_conditions<Conditions...>;
+
+ template <typename... Conditions>
+ using assignment_requires_conditions =
+ ::cpp17::internal::assignment_requires_conditions<optional&, Conditions...>;
+
+ template <typename... Args>
+ using emplace_constructible = std::enable_if_t<std::is_constructible<T, Args...>::value, T&>;
+
+ public:
+ using value_type = T;
+
+ // Default constructors.
+
+ constexpr optional() = default;
+
+ constexpr optional(nullopt_t) noexcept {}
+
+ // Copy/move constructors and assignment operators.
+
+ constexpr optional(const optional&) = default;
+ constexpr optional& operator=(const optional&) = default;
+
+ constexpr optional(optional&&) = default;
+ constexpr optional& operator=(optional&&) = default;
+
+ // Converting constructors.
+
+ template <typename U = T,
+ requires_conditions<not_self_type<U>, not_in_place<U>, std::is_constructible<T, U&&>,
+ std::is_convertible<U&&, T>> = true>
+ constexpr optional(U&& value) : storage_(type_tag{}, std::forward<U>(value)) {}
+
+ template <typename U = T,
+ requires_conditions<not_self_type<U>, not_in_place<U>, std::is_constructible<T, U&&>,
+ negation<std::is_convertible<U&&, T>>> = false>
+ explicit constexpr optional(U&& value) : storage_{type_tag{}, std::forward<U>(value)} {}
+
+ template <typename U,
+ requires_conditions<negation<std::is_same<T, U>>, std::is_constructible<T, const U&>,
+ std::is_convertible<const U&, T>,
+ negation<converts_from_optional<T, U>>> = true>
+ constexpr optional(const optional<U>& other) : storage_{maybe_init_v, other.storage_} {}
+
+ template <typename U,
+ requires_conditions<negation<std::is_same<T, U>>, std::is_constructible<T, const U&>,
+ negation<std::is_convertible<const U&, T>>,
+ negation<converts_from_optional<T, U>>> = false>
+ explicit constexpr optional(const optional<U>& other) : storage_{maybe_init_v, other.storage_} {}
+
+ template <typename U,
+ requires_conditions<negation<std::is_same<T, U>>, std::is_constructible<T, U&&>,
+ std::is_convertible<U&&, T>,
+ negation<converts_from_optional<T, U>>> = true>
+ constexpr optional(optional<U>&& other) : storage_{maybe_init_v, std::move(other.storage_)} {}
+
+ template <typename U,
+ requires_conditions<negation<std::is_same<T, U>>, std::is_constructible<T, U&&>,
+ negation<std::is_convertible<U&&, T>>,
+ negation<converts_from_optional<T, U>>> = false>
+ explicit constexpr optional(optional<U>&& other)
+ : storage_{maybe_init_v, std::move(other.storage_)} {}
+
+ template <typename... Args, requires_conditions<std::is_constructible<T, Args&&...>> = false>
+ explicit constexpr optional(in_place_t, Args&&... args)
+ : storage_(type_tag{}, std::forward<Args>(args)...) {}
+
+ template <
+ typename U, typename... Args,
+ requires_conditions<std::is_constructible<T, std::initializer_list<U>&, Args&&...>> = false>
+ explicit constexpr optional(in_place_t, std::initializer_list<U> init_list, Args&&... args)
+ : storage_(type_tag{}, init_list, std::forward<Args>(args)...) {}
+
+ // Destructor.
+
+ ~optional() = default;
+
+ // Checked accessors.
+
+ constexpr T& value() & {
+ if (has_value()) {
+ return storage_.get(type_tag{});
+ }
+ internal::throw_or_abort<bad_optional_access>("Accessed value of empty optional!");
+ }
+ constexpr const T& value() const& {
+ if (has_value()) {
+ return storage_.get(type_tag{});
+ }
+ internal::throw_or_abort<bad_optional_access>("Accessed value of empty optional!");
+ }
+ constexpr T&& value() && {
+ if (has_value()) {
+ return std::move(storage_.get(type_tag{}));
+ }
+ internal::throw_or_abort<bad_optional_access>("Accessed value of empty optional!");
+ }
+ constexpr const T&& value() const&& {
+ if (has_value()) {
+ return std::move(storage_.get(type_tag{}));
+ }
+ internal::throw_or_abort<bad_optional_access>("Accessed value of empty optional!");
+ }
+
+ template <typename U>
+ constexpr T value_or(U&& default_value) const& {
+ static_assert(std::is_copy_constructible<T>::value,
+ "value_or() requires copy-constructible value_type!");
+ static_assert(std::is_convertible<U&&, T>::value,
+ "Default value must be convertible to value_type!");
+
+ return has_value() ? storage_.get(type_tag{}) : static_cast<T>(std::forward<U>(default_value));
+ }
+ template <typename U>
+ constexpr T value_or(U&& default_value) && {
+ static_assert(std::is_move_constructible<T>::value,
+ "value_or() requires move-constructible value_type!");
+ static_assert(std::is_convertible<U&&, T>::value,
+ "Default value must be convertible to value_type!");
+
+ return has_value() ? std::move(storage_.get(type_tag{}))
+ : static_cast<T>(std::forward<U>(default_value));
+ }
+
+ // Unchecked accessors.
+
+ constexpr T* operator->() { return std::addressof(storage_.get(type_tag{})); }
+ constexpr const T* operator->() const { return std::addressof(storage_.get(type_tag{})); }
+
+ constexpr T& operator*() { return storage_.get(type_tag{}); }
+ constexpr const T& operator*() const { return storage_.get(type_tag{}); }
+
+ // Availability accessors/operators.
+
+ constexpr bool has_value() const { return !storage_.is_empty(); }
+ constexpr explicit operator bool() const { return has_value(); }
+
+ // Assignment operators.
+
+ template <typename U>
+ constexpr assignment_requires_conditions<
+ not_self_type<U>, negation<conjunction<std::is_scalar<T>, std::is_same<T, std::decay_t<U>>>>,
+ std::is_constructible<T, U>, std::is_assignable<T&, U>>
+ operator=(U&& value) {
+ if (has_value()) {
+ storage_.get(type_tag{}) = std::forward<U>(value);
+ } else {
+ storage_.construct(type_tag{}, std::forward<U>(value));
+ }
+ return *this;
+ }
+
+ template <typename U>
+ constexpr assignment_requires_conditions<
+ negation<std::is_same<T, U>>, std::is_constructible<T, const U&>, std::is_assignable<T&, U>,
+ negation<converts_from_optional<T, U>>, negation<assigns_from_optional<T, U>>>
+ operator=(const optional<U>& other) {
+ storage_.assign(other.storage_);
+ return *this;
+ }
+
+ template <typename U>
+ constexpr assignment_requires_conditions<
+ negation<std::is_same<T, U>>, std::is_constructible<T, U>, std::is_assignable<T&, U>,
+ negation<converts_from_optional<T, U>>, negation<assigns_from_optional<T, U>>>
+ operator=(optional<U>&& other) {
+ storage_.assign(std::move(other.storage_));
+ return *this;
+ }
+
+ constexpr optional& operator=(nullopt_t) {
+ storage_.reset();
+ return *this;
+ }
+
+ // Swap.
+
+ constexpr void swap(optional& other) noexcept(nothrow_swappable) {
+ storage_.swap(other.storage_);
+ }
+
+ // Emplacement.
+
+ template <typename... Args>
+ constexpr emplace_constructible<Args&&...> emplace(Args&&... args) {
+ storage_.reset();
+ storage_.construct(type_tag{}, std::forward<Args>(args)...);
+ return storage_.get(type_tag{});
+ }
+
+ template <typename U, typename... Args>
+ constexpr emplace_constructible<std::initializer_list<U>&, Args&&...> emplace(
+ std::initializer_list<U> init_list, Args&&... args) {
+ storage_.reset();
+ storage_.construct(type_tag{}, init_list, std::forward<Args>(args)...);
+ return storage_.get(type_tag{});
+ }
+
+ // Reset.
+
+ void reset() noexcept { storage_.reset(); }
+
+ private:
+ ::cpp17::internal::storage_type<T> storage_;
+};
+
+// Swap.
+template <typename T>
+inline std::enable_if_t<(std::is_move_constructible<T>::value &&
+ ::cpp17::internal::is_swappable<T>::value)>
+swap(optional<T>& a, optional<T>& b) noexcept(noexcept(a.swap(b))) {
+ a.swap(b);
+}
+template <typename T>
+inline std::enable_if_t<(!std::is_move_constructible<T>::value &&
+ ::cpp17::internal::is_swappable<T>::value)>
+swap(optional<T>& a, optional<T>& b) = delete;
+
+// Make optional.
+template <typename T>
+constexpr optional<std::decay_t<T>> make_optional(T&& value) {
+ return optional<std::decay_t<T>>{std::forward<T>(value)};
+}
+template <typename T, typename... Args>
+constexpr optional<T> make_optional(Args&&... args) {
+ return optional<T>{in_place, std::forward<Args>(args)...};
+}
+template <typename T, typename U, typename... Args>
+constexpr optional<T> make_optional(std::initializer_list<U> init_list, Args&&... args) {
+ return optional<T>{in_place, init_list, std::forward<Args>(args)...};
+}
+
+// Empty.
+template <typename T>
+constexpr bool operator==(const optional<T>& lhs, nullopt_t) {
+ return !lhs.has_value();
+}
+template <typename T>
+constexpr bool operator!=(const optional<T>& lhs, nullopt_t) {
+ return lhs.has_value();
+}
+
+template <typename T>
+constexpr bool operator==(nullopt_t, const optional<T>& rhs) {
+ return !rhs.has_value();
+}
+template <typename T>
+constexpr bool operator!=(nullopt_t, const optional<T>& rhs) {
+ return rhs.has_value();
+}
+
+// Equal/not equal.
+template <
+ typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() == std::declval<U>())> = true>
+constexpr bool operator==(const optional<T>& lhs, const optional<U>& rhs) {
+ return (lhs.has_value() == rhs.has_value()) && (!lhs.has_value() || *lhs == *rhs);
+}
+template <
+ typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() != std::declval<U>())> = true>
+constexpr bool operator!=(const optional<T>& lhs, const optional<U>& rhs) {
+ return (lhs.has_value() != rhs.has_value()) || (lhs.has_value() && *lhs != *rhs);
+}
+
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() == std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, U>> = true>
+constexpr bool operator==(const optional<T>& lhs, const U& rhs) {
+ return lhs.has_value() && *lhs == rhs;
+}
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() != std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, U>> = true>
+constexpr bool operator!=(const optional<T>& lhs, const U& rhs) {
+ return !lhs.has_value() || *lhs != rhs;
+}
+
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() == std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, T>> = true>
+constexpr bool operator==(const T& lhs, const optional<U>& rhs) {
+ return rhs.has_value() && lhs == *rhs;
+}
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() != std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, T>> = true>
+constexpr bool operator!=(const T& lhs, const optional<U>& rhs) {
+ return !rhs.has_value() || lhs != *rhs;
+}
+
+// Less than/greater than.
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() < std::declval<U>())> = true>
+constexpr bool operator<(const optional<T>& lhs, const optional<U>& rhs) {
+ return rhs.has_value() && (!lhs.has_value() || *lhs < *rhs);
+}
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() > std::declval<U>())> = true>
+constexpr bool operator>(const optional<T>& lhs, const optional<U>& rhs) {
+ return lhs.has_value() && (!rhs.has_value() || *lhs > *rhs);
+}
+
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() < std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, U>> = true>
+constexpr bool operator<(const optional<T>& lhs, const U& rhs) {
+ return !lhs.has_value() || *lhs < rhs;
+}
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() > std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, U>> = true>
+constexpr bool operator>(const optional<T>& lhs, const U& rhs) {
+ return lhs.has_value() && *lhs > rhs;
+}
+
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() < std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, T>> = true>
+constexpr bool operator<(const T& lhs, const optional<U>& rhs) {
+ return rhs.has_value() && lhs < *rhs;
+}
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() > std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, T>> = true>
+constexpr bool operator>(const T& lhs, const optional<U>& rhs) {
+ return !rhs.has_value() || lhs > *rhs;
+}
+
+// Less than or equal/greater than or equal.
+template <
+ typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() <= std::declval<U>())> = true>
+constexpr bool operator<=(const optional<T>& lhs, const optional<U>& rhs) {
+ return !lhs.has_value() || (rhs.has_value() && *lhs <= *rhs);
+}
+template <
+ typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() >= std::declval<U>())> = true>
+constexpr bool operator>=(const optional<T>& lhs, const optional<U>& rhs) {
+ return !rhs.has_value() || (lhs.has_value() && *lhs >= *rhs);
+}
+
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() <= std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, U>> = true>
+constexpr bool operator<=(const optional<T>& lhs, const U& rhs) {
+ return !lhs.has_value() || *lhs <= rhs;
+}
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() >= std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, U>> = true>
+constexpr bool operator>=(const optional<T>& lhs, const U& rhs) {
+ return lhs.has_value() && *lhs >= rhs;
+}
+
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() <= std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, T>> = true>
+constexpr bool operator<=(const T& lhs, const optional<U>& rhs) {
+ return rhs.has_value() && lhs <= *rhs;
+}
+template <typename T, typename U,
+ ::cpp17::internal::enable_relop_t<decltype(std::declval<T>() >= std::declval<U>()),
+ ::cpp17::internal::not_same_type<nullopt_t, T>> = true>
+constexpr bool operator>=(const T& lhs, const optional<U>& rhs) {
+ return !rhs.has_value() || lhs >= *rhs;
+}
+
+} // namespace cpp17
+
+#endif
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_OPTIONAL_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h
new file mode 100644
index 000000000..8f0d80ede
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/type_traits.h
@@ -0,0 +1,509 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_TYPE_TRAITS_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_TYPE_TRAITS_H_
+
+#include <cstddef>
+#include <tuple>
+#include <type_traits>
+
+#include "internal/type_traits.h"
+#include "version.h"
+
+namespace cpp17 {
+
+#if defined(__cpp_lib_void_t) && __cpp_lib_void_t >= 201411L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+using std::void_t;
+#else // Provide std::void_t polyfill.
+template <typename... T>
+using void_t = void;
+#endif // __cpp_lib_void_t >= 201411L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_logical_traits) && __cpp_lib_logical_traits >= 201510L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::conjunction;
+using std::conjunction_v;
+using std::disjunction;
+using std::disjunction_v;
+using std::negation;
+using std::negation_v;
+
+#else // Provide polyfills for std::{negation, conjunction, disjunction} and the *_v helpers.
+
+template <typename... Ts>
+struct conjunction : std::true_type {};
+template <typename T>
+struct conjunction<T> : T {};
+template <typename First, typename... Rest>
+struct conjunction<First, Rest...>
+ : std::conditional_t<bool(First::value), conjunction<Rest...>, First> {};
+
+template <typename... Ts>
+static constexpr bool conjunction_v = conjunction<Ts...>::value;
+
+template <typename... Ts>
+struct disjunction : std::false_type {};
+template <typename T>
+struct disjunction<T> : T {};
+template <typename First, typename... Rest>
+struct disjunction<First, Rest...>
+ : std::conditional_t<bool(First::value), First, disjunction<Rest...>> {};
+
+template <typename... Ts>
+static constexpr bool disjunction_v = disjunction<Ts...>::value;
+
+// Utility type that negates its truth-like parameter type.
+template <typename T>
+struct negation : std::integral_constant<bool, !bool(T::value)> {};
+
+template <typename T>
+static constexpr bool negation_v = negation<T>::value;
+
+#endif // __cpp_lib_logical_traits >= 201510L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_bool_constant) && __cpp_lib_bool_constant >= 201505L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::bool_constant;
+
+#else // Provide polyfill for std::bool_constant
+
+template <bool B>
+using bool_constant = std::integral_constant<bool, B>;
+
+#endif // __cpp_lib_bool_constant >= 201505L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_type_trait_variable_templates) && \
+ __cpp_lib_type_trait_variable_templates >= 201510L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::is_array_v;
+using std::is_class_v;
+using std::is_enum_v;
+using std::is_floating_point_v;
+using std::is_function_v;
+using std::is_integral_v;
+using std::is_lvalue_reference_v;
+using std::is_member_function_pointer_v;
+using std::is_member_object_pointer_v;
+using std::is_null_pointer_v;
+using std::is_pointer_v;
+using std::is_rvalue_reference_v;
+using std::is_union_v;
+using std::is_void_v;
+
+using std::is_arithmetic_v;
+using std::is_compound_v;
+using std::is_fundamental_v;
+using std::is_member_pointer_v;
+using std::is_object_v;
+using std::is_reference_v;
+using std::is_scalar_v;
+
+using std::is_abstract_v;
+using std::is_const_v;
+using std::is_empty_v;
+using std::is_final_v;
+using std::is_pod_v;
+using std::is_polymorphic_v;
+using std::is_signed_v;
+using std::is_standard_layout_v;
+using std::is_trivial_v;
+using std::is_trivially_copyable_v;
+using std::is_unsigned_v;
+using std::is_volatile_v;
+
+using std::is_constructible_v;
+using std::is_nothrow_constructible_v;
+using std::is_trivially_constructible_v;
+
+using std::is_default_constructible_v;
+using std::is_nothrow_default_constructible_v;
+using std::is_trivially_default_constructible_v;
+
+using std::is_copy_constructible_v;
+using std::is_nothrow_copy_constructible_v;
+using std::is_trivially_copy_constructible_v;
+
+using std::is_move_constructible_v;
+using std::is_nothrow_move_constructible_v;
+using std::is_trivially_move_constructible_v;
+
+using std::is_assignable_v;
+using std::is_nothrow_assignable_v;
+using std::is_trivially_assignable_v;
+
+using std::is_copy_assignable_v;
+using std::is_nothrow_copy_assignable_v;
+using std::is_trivially_copy_assignable_v;
+
+using std::is_move_assignable_v;
+using std::is_nothrow_move_assignable_v;
+using std::is_trivially_move_assignable_v;
+
+using std::is_destructible_v;
+using std::is_nothrow_destructible_v;
+using std::is_trivially_destructible_v;
+
+using std::has_virtual_destructor_v;
+
+using std::alignment_of_v;
+using std::extent_v;
+using std::rank_v;
+
+using std::is_base_of_v;
+using std::is_convertible_v;
+using std::is_same_v;
+
+#else // Provide polyfills for the bulk of the *_v helpers
+
+template <typename T>
+static constexpr bool is_void_v = std::is_void<T>::value;
+template <typename T>
+static constexpr bool is_null_pointer_v = std::is_null_pointer<T>::value;
+template <typename T>
+static constexpr bool is_integral_v = std::is_integral<T>::value;
+template <typename T>
+static constexpr bool is_floating_point_v = std::is_floating_point<T>::value;
+template <typename T>
+static constexpr bool is_array_v = std::is_array<T>::value;
+template <typename T>
+static constexpr bool is_enum_v = std::is_enum<T>::value;
+template <typename T>
+static constexpr bool is_union_v = std::is_union<T>::value;
+template <typename T>
+static constexpr bool is_class_v = std::is_class<T>::value;
+template <typename T>
+static constexpr bool is_function_v = std::is_function<T>::value;
+template <typename T>
+static constexpr bool is_pointer_v = std::is_pointer<T>::value;
+template <typename T>
+static constexpr bool is_lvalue_reference_v = std::is_lvalue_reference<T>::value;
+template <typename T>
+static constexpr bool is_rvalue_reference_v = std::is_rvalue_reference<T>::value;
+template <typename T>
+static constexpr bool is_member_object_pointer_v = std::is_member_object_pointer<T>::value;
+template <typename T>
+static constexpr bool is_member_function_pointer_v = std::is_member_function_pointer<T>::value;
+
+template <typename T>
+static constexpr bool is_fundamental_v = std::is_fundamental<T>::value;
+template <typename T>
+static constexpr bool is_arithmetic_v = std::is_arithmetic<T>::value;
+template <typename T>
+static constexpr bool is_scalar_v = std::is_scalar<T>::value;
+template <typename T>
+static constexpr bool is_object_v = std::is_object<T>::value;
+template <typename T>
+static constexpr bool is_compound_v = std::is_compound<T>::value;
+template <typename T>
+static constexpr bool is_reference_v = std::is_reference<T>::value;
+template <typename T>
+static constexpr bool is_member_pointer_v = std::is_member_pointer<T>::value;
+
+template <typename T>
+static constexpr bool is_const_v = std::is_const<T>::value;
+template <typename T>
+static constexpr bool is_volatile_v = std::is_volatile<T>::value;
+template <typename T>
+static constexpr bool is_trivial_v = std::is_trivial<T>::value;
+template <typename T>
+static constexpr bool is_trivially_copyable_v = std::is_trivially_copyable<T>::value;
+template <typename T>
+static constexpr bool is_standard_layout_v = std::is_standard_layout<T>::value;
+template <typename T>
+[[deprecated]] static constexpr bool is_pod_v = std::is_pod<T>::value;
+template <typename T>
+static constexpr bool is_empty_v = std::is_empty<T>::value;
+template <typename T>
+static constexpr bool is_polymorphic_v = std::is_polymorphic<T>::value;
+template <typename T>
+static constexpr bool is_abstract_v = std::is_abstract<T>::value;
+template <typename T>
+static constexpr bool is_final_v = std::is_final<T>::value;
+template <typename T>
+static constexpr bool is_signed_v = std::is_signed<T>::value;
+template <typename T>
+static constexpr bool is_unsigned_v = std::is_unsigned<T>::value;
+
+template <typename T, typename... Args>
+static constexpr bool is_constructible_v = std::is_constructible<T, Args...>::value;
+template <typename T, typename... Args>
+static constexpr bool is_trivially_constructible_v =
+ std::is_trivially_constructible<T, Args...>::value;
+template <typename T, typename... Args>
+static constexpr bool is_nothrow_constructible_v = std::is_nothrow_constructible<T, Args...>::value;
+
+template <typename T>
+static constexpr bool is_default_constructible_v = std::is_default_constructible<T>::value;
+template <typename T>
+static constexpr bool is_trivially_default_constructible_v =
+ std::is_trivially_default_constructible<T>::value;
+template <typename T>
+static constexpr bool is_nothrow_default_constructible_v =
+ std::is_nothrow_default_constructible<T>::value;
+
+template <typename T>
+static constexpr bool is_copy_constructible_v = std::is_copy_constructible<T>::value;
+template <typename T>
+static constexpr bool is_trivially_copy_constructible_v =
+ std::is_trivially_copy_constructible<T>::value;
+template <typename T>
+static constexpr bool is_nothrow_copy_constructible_v =
+ std::is_nothrow_copy_constructible<T>::value;
+
+template <typename T>
+static constexpr bool is_move_constructible_v = std::is_move_constructible<T>::value;
+template <typename T>
+static constexpr bool is_trivially_move_constructible_v =
+ std::is_trivially_move_constructible<T>::value;
+template <typename T>
+static constexpr bool is_nothrow_move_constructible_v =
+ std::is_nothrow_move_constructible<T>::value;
+
+template <typename T, typename U>
+static constexpr bool is_assignable_v = std::is_assignable<T, U>::value;
+template <typename T, typename U>
+static constexpr bool is_trivially_assignable_v = std::is_trivially_assignable<T, U>::value;
+template <typename T, typename U>
+static constexpr bool is_nothrow_assignable_v = std::is_nothrow_assignable<T, U>::value;
+
+template <typename T>
+static constexpr bool is_copy_assignable_v = std::is_copy_assignable<T>::value;
+template <typename T>
+static constexpr bool is_trivially_copy_assignable_v = std::is_trivially_copy_assignable<T>::value;
+template <typename T>
+static constexpr bool is_nothrow_copy_assignable_v = std::is_nothrow_copy_assignable<T>::value;
+
+template <typename T>
+static constexpr bool is_move_assignable_v = std::is_move_assignable<T>::value;
+template <typename T>
+static constexpr bool is_trivially_move_assignable_v = std::is_trivially_move_assignable<T>::value;
+template <typename T>
+static constexpr bool is_nothrow_move_assignable_v = std::is_nothrow_move_assignable<T>::value;
+
+template <typename T>
+static constexpr bool is_destructible_v = std::is_destructible<T>::value;
+template <typename T>
+static constexpr bool is_trivially_destructible_v = std::is_trivially_destructible<T>::value;
+template <typename T>
+static constexpr bool is_nothrow_destructible_v = std::is_nothrow_destructible<T>::value;
+
+template <typename T>
+static constexpr bool has_virtual_destructor_v = std::has_virtual_destructor<T>::value;
+
+template <typename T>
+static constexpr bool alignment_of_v = std::alignment_of<T>::value;
+template <typename T>
+static constexpr bool rank_v = std::rank<T>::value;
+template <typename T, unsigned N = 0>
+static constexpr bool extent_v = std::extent<T, N>::value;
+
+template <typename T, typename U>
+static constexpr bool is_same_v = std::is_same<T, U>::value;
+template <typename T, typename U>
+static constexpr bool is_base_of_v = std::is_base_of<T, U>::value;
+template <typename T, typename U>
+static constexpr bool is_convertible_v = std::is_convertible<T, U>::value;
+
+#endif // __cpp_lib_type_trait_variable_templates >= 201510L &&
+ // !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_is_aggregate) && __cpp_lib_is_aggregate >= 201703L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::is_aggregate;
+using std::is_aggregate_v;
+
+#else // Provide std::is_aggregate polyfill
+
+template <typename T>
+struct is_aggregate : bool_constant<__is_aggregate(T)> {};
+
+template <typename T>
+static constexpr bool is_aggregate_v = is_aggregate<T>::value;
+
+#endif // __cpp_lib_is_aggregate >= 201703L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_is_invocable) && __cpp_lib_is_invocable >= 201703L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::is_invocable;
+using std::is_invocable_r;
+using std::is_nothrow_invocable;
+using std::is_nothrow_invocable_r;
+
+using std::is_invocable_r_v;
+using std::is_invocable_v;
+using std::is_nothrow_invocable_r_v;
+using std::is_nothrow_invocable_v;
+
+using std::invoke_result;
+using std::invoke_result_t;
+
+#else
+
+template <typename R, typename F, typename... Args>
+struct is_invocable_r : decltype(::cpp17::internal::is_valid_invoke<R, F, Args...>(nullptr)) {};
+
+template <typename R, typename F, typename... Args>
+static constexpr bool is_invocable_r_v = is_invocable_r<R, F, Args...>::value;
+
+// INVOKE() is a subexpression of INVOKE<R>()
+// INVOKE<void>(f, t1, t2, ..., tn) results in a call to
+// static_cast<void>(INVOKE(f, t1, t2, ..., tn)) per [func.require] ¶ 2
+template <typename F, typename... Args>
+struct is_invocable : is_invocable_r<void, F, Args...> {};
+
+template <typename F, typename... Args>
+static constexpr bool is_invocable_v = is_invocable<F, Args...>::value;
+
+template <typename F, typename... Args>
+struct is_nothrow_invocable : bool_constant<is_invocable_v<F, Args...> &&
+ noexcept(::cpp17::internal::invoke(
+ std::declval<F>(), std::declval<Args>()...))> {};
+
+template <typename F, typename... Args>
+static constexpr bool is_nothrow_invocable_v = is_nothrow_invocable<F, Args...>::value;
+
+template <typename R, typename F, typename... Args>
+struct is_nothrow_invocable_r : bool_constant<is_invocable_r_v<R, F, Args...> &&
+ noexcept(::cpp17::internal::invoke_r<R>(
+ std::declval<F>(), std::declval<Args>()...))> {};
+
+template <typename R, typename F, typename... Args>
+static constexpr bool is_nothrow_invocable_r_v = is_nothrow_invocable_r<R, F, Args...>::value;
+
+template <typename F, typename... Args>
+struct invoke_result : ::cpp17::internal::invoke_result<is_invocable_v<F, Args...>, F, Args...> {};
+
+template <typename F, typename... Args>
+using invoke_result_t = typename invoke_result<F, Args...>::type;
+
+#endif // __cpp_lib_is_invocable >= 201703L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+} // namespace cpp17
+
+namespace cpp20 {
+
+#if defined(__cpp_lib_bounded_array_traits) && __cpp_lib_bounded_array_traits >= 201902L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::is_bounded_array;
+using std::is_bounded_array_v;
+
+using std::is_unbounded_array;
+using std::is_unbounded_array_v;
+
+#else // Provide polyfills for std::is_{,un}bounded_array{,_v}
+
+template <typename T>
+struct is_bounded_array : std::false_type {};
+template <typename T, std::size_t N>
+struct is_bounded_array<T[N]> : std::true_type {};
+
+template <typename T>
+static constexpr bool is_bounded_array_v = is_bounded_array<T>::value;
+
+template <typename T>
+struct is_unbounded_array : std::false_type {};
+template <typename T>
+struct is_unbounded_array<T[]> : std::true_type {};
+
+template <typename T>
+static constexpr bool is_unbounded_array_v = is_unbounded_array<T>::value;
+
+#endif // __cpp_lib_bounded_array_traits >= 201902L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_remove_cvref) && __cpp_lib_remove_cvref >= 201711L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::remove_cvref;
+using std::remove_cvref_t;
+
+#else // Provide polyfill for std::remove_cvref{,_t}
+
+template <typename T>
+struct remove_cvref {
+ using type = std::remove_cv_t<std::remove_reference_t<T>>;
+};
+
+template <typename T>
+using remove_cvref_t = typename remove_cvref<T>::type;
+
+#endif // __cpp_lib_remove_cvref >= 201711L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_type_identity) && __cpp_lib_type_identity >= 201806L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::type_identity;
+using std::type_identity_t;
+
+#else // Provide polyfill for std::type_identity{,_t}
+
+template <typename T>
+struct type_identity {
+ using type = T;
+};
+
+template <typename T>
+using type_identity_t = typename type_identity<T>::type;
+
+#endif // __cpp_lib_type_identity >= 201806L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_is_constant_evaluated) && __cpp_lib_is_constant_evaluated >= 201811L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#define LIB_STDCOMPAT_CONSTEVAL_SUPPORT 1
+using std::is_constant_evaluated;
+
+#else // Provide polyfill for std::is_constant_evaluated
+
+#ifdef __has_builtin
+#if __has_builtin(__builtin_is_constant_evaluated)
+
+#define LIB_STDCOMPAT_CONSTEVAL_SUPPORT 1
+inline constexpr bool is_constant_evaluated() noexcept { return __builtin_is_constant_evaluated(); }
+
+#endif // __has_builtin(__builtin_is_constant_evaluated)
+#endif // __has_builtin
+
+#ifndef LIB_STDCOMPAT_CONSTEVAL_SUPPORT
+
+#define LIB_STDCOMPAT_CONSTEVAL_SUPPORT 0
+inline constexpr bool is_constant_evaluated() noexcept { return false; }
+
+#endif // LIB_STDCOMPAT_CONSTEVAL_SUPPORT
+
+#endif // __cpp_lib_is_constant_evaluated >= 201811L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+} // namespace cpp20
+
+namespace cpp23 {
+
+#if defined(__cpp_lib_is_scoped_enum) && __cpp_lib_is_scoped_enum >= 202011L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::is_scoped_enum;
+using std::is_scoped_enum_v;
+
+#else // Provide polyfill for std::is_scoped_enum{,_v}
+
+template <typename T, typename = void>
+struct is_scoped_enum : std::false_type {};
+
+template <typename T>
+struct is_scoped_enum<T, std::enable_if_t<cpp17::is_enum_v<T>>>
+ : cpp17::bool_constant<!cpp17::is_convertible_v<T, std::underlying_type_t<T>>> {};
+
+template <typename T>
+static constexpr bool is_scoped_enum_v = is_scoped_enum<T>::value;
+
+#endif // __cpp_lib_is_scoped_enum >= 202011L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+} // namespace cpp23
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_TYPE_TRAITS_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h
new file mode 100644
index 000000000..fe0993d47
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/utility.h
@@ -0,0 +1,118 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_UTILITY_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_UTILITY_H_
+
+#include <cstddef>
+#include <type_traits>
+#include <utility>
+
+#include "internal/utility.h"
+#include "version.h"
+
+namespace cpp17 {
+// Use alias for cpp17 and above.
+#if __cplusplus >= 201411L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::in_place;
+using std::in_place_t;
+
+using std::in_place_index;
+using std::in_place_index_t;
+
+using std::in_place_type;
+using std::in_place_type_t;
+
+#else // Provide provide polyfills for |in_place*| types and variables.
+
+// Tag for requesting in-place initialization.
+struct in_place_t {
+ explicit constexpr in_place_t() = default;
+};
+
+// Tag for requesting in-place initialization by type.
+template <typename T>
+struct in_place_type_t {
+ explicit constexpr in_place_type_t() = default;
+};
+
+// Tag for requesting in-place initialization by index.
+template <std::size_t Index>
+struct in_place_index_t final {
+ explicit constexpr in_place_index_t() = default;
+};
+
+// Use inline variables if available.
+#if defined(__cpp_inline_variables) && __cpp_inline_variables >= 201606L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+constexpr in_place_t in_place{};
+
+template <typename T>
+constexpr in_place_type_t<T> in_place_type{};
+
+template <std::size_t Index>
+constexpr in_place_index_t<Index> in_place_index{};
+
+#else // Provide polyfill reference to provided variable storage.
+
+static constexpr const in_place_t& in_place =
+ internal::instantiate_templated_tag<in_place_t>::storage;
+
+template <typename T>
+static constexpr const in_place_type_t<T>& in_place_type =
+ internal::instantiate_templated_tag<in_place_type_t<T>>::storage;
+
+template <std::size_t Index>
+static constexpr const in_place_index_t<Index>& in_place_index =
+ internal::instantiate_templated_tag<in_place_index_t<Index>>::storage;
+
+#endif // __cpp_inline_variables >= 201606L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#endif // __cplusplus >= 201411L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#if defined(__cpp_lib_as_const) && __cpp_lib_as_const >= 201510L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::as_const;
+
+#else // Provide as_const polyfill.
+
+template <typename T>
+constexpr std::add_const_t<T>& as_const(T& t) noexcept {
+ return t;
+}
+
+template <typename T>
+void as_const(T&&) = delete;
+
+#endif // __cpp_lib_as_const >= 201510L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+} // namespace cpp17
+
+namespace cpp20 {
+
+#if defined(__cpp_lib_constexpr_algorithms) && __cpp_lib_constexpr_algorithms >= 201806L && \
+ !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+using std::exchange;
+
+#else // Add swap constexpr polyfill.
+
+template <
+ typename T, typename U = T,
+ typename std::enable_if<std::is_move_assignable<T>::value && cpp17::is_assignable_v<T&, U>,
+ bool>::type = true>
+constexpr T exchange(T& obj, U&& new_value) {
+ T old = std::move(obj);
+ obj = std::forward<U>(new_value);
+ return old;
+}
+
+#endif // __cpp_lib_constexpr_algorithms >= 201806L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+} // namespace cpp20
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_UTILITY_H_
diff --git a/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h
new file mode 100644
index 000000000..c022e4b30
--- /dev/null
+++ b/third_party/fuchsia/repo/sdk/lib/stdcompat/include/lib/stdcompat/version.h
@@ -0,0 +1,79 @@
+// Copyright 2020 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_VERSION_H_
+#define LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_VERSION_H_
+
+// This <version> polyfills is meant to provide the feature testing macros for the rest of
+// the stdcompat library. It is not meant to be a full polyfill of <version>.
+
+#if __has_include(<version>) && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+#include <version>
+#elif __cplusplus > 201703L && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+#error "cpp=std20 must provide a '<version>' header."
+#else
+
+#if __has_include(<optional>) && !defined(__cpp_lib_optional) && __cplusplus >= 201606L
+#define __cpp_lib_optional 201606L
+#endif
+
+#if __has_include(<string_view>) && !defined(__cpp_lib_string_view) && __cplusplus >= 201606L
+#define __cpp_lib_string_view 201606L
+#endif
+
+#if __has_include(<variant>) && !defined(__cpp_lib_variant) && __cplusplus >= 201606L
+#define __cpp_lib_variant 201606L
+#endif
+
+#if !defined(__cpp_lib_void_t) && __cplusplus >= 201411L
+#define __cpp_lib_void_t 201411L
+#endif
+
+#if !defined(__cpp_lib_logical_traits) && __cplusplus >= 201510L
+#define __cpp_lib_logical_traits 201510L
+#endif
+
+#if !defined(__cpp_lib_addressof_constexpr) && __cplusplus >= 201603L
+#define __cpp_lib_addressof_constexpr 201603L
+#endif
+
+#if !defined(__cpp_lib_nonmember_container_access) && __cplusplus >= 201411L
+#define __cpp_lib_nonmember_container_access 201411L
+#endif
+
+#if !defined(__cpp_lib_byte) && __cplusplus >= 201603L
+#define __cpp_lib_byte 201603L
+#endif
+
+#if !defined(__cpp_lib_bool_constant) && __cplusplus >= 201505L
+#define __cpp_lib_bool_constant 201505L
+#endif
+
+#if !defined(__cpp_lib_type_trait_variable_templates) && __cplusplus >= 201510L
+#define __cpp_lib_type_trait_variable_templates 201510L
+#endif
+
+#if !defined(__cpp_lib_is_aggregate) && __cplusplus >= 201703L
+#define __cpp_lib_is_aggregate 201703L
+#endif
+
+#if !defined(__cpp_lib_is_invocable) && __cplusplus >= 201703L
+#define __cpp_lib_is_invocable 201703L
+#endif
+
+#if !defined(__cpp_lib_invoke) && __cplusplus >= 201411L
+#define __cpp_lib_invoke 201411L
+#endif
+
+#if !defined(__cpp_lib_apply) && __cplusplus >= 201603L
+#define __cpp_lib_apply 201603L
+#endif
+
+#if !defined(__cpp_lib_as_const) && __cplusplus >= 201510L
+#define __cpp_lib_as_const 201510L
+#endif
+
+#endif // __has_include(<version>) && !defined(LIB_STDCOMPAT_USE_POLYFILLS)
+
+#endif // LIB_STDCOMPAT_INCLUDE_LIB_STDCOMPAT_VERSION_H_
diff --git a/third_party/google_auto/OWNERS b/third_party/google_auto/OWNERS
new file mode 100644
index 000000000..d96cbc68d
--- /dev/null
+++ b/third_party/google_auto/OWNERS
@@ -0,0 +1 @@
+hepler@google.com
diff --git a/third_party/googletest/BUILD.gn b/third_party/googletest/BUILD.gn
index f50a339d3..50f7be353 100644
--- a/third_party/googletest/BUILD.gn
+++ b/third_party/googletest/BUILD.gn
@@ -15,6 +15,7 @@
import("//build_overrides/pigweed.gni")
import("$dir_pw_build/target_types.gni")
+import("$dir_pw_docgen/docs.gni")
import("googletest.gni")
# This file defines a GN source_set for an external installation of googletest.
@@ -31,65 +32,26 @@ if (dir_pw_third_party_googletest != "") {
]
# Fix some compiler warnings.
- cflags = [ "-Wno-undef" ]
+ cflags = [
+ "-Wno-undef",
+ "-Wno-conversion",
+ ]
}
pw_source_set("googletest") {
public_configs = [ ":includes" ]
public = [
"$dir_pw_third_party_googletest/googlemock/include/gmock/gmock.h",
+ "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-spi.h",
"$dir_pw_third_party_googletest/googletest/include/gtest/gtest.h",
]
+
+ # Only add the "*-all.cc" files (and no headers) to improve maintainability
+ # from upstream refactoring. The "*-all.cc" files include the respective
+ # source files.
sources = [
- "$dir_pw_third_party_googletest/googlemock/include/gmock/gmock-actions.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/gmock-cardinalities.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/gmock-function-mocker.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/gmock-matchers.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/gmock-more-actions.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/gmock-more-matchers.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/gmock-nice-strict.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/gmock-spec-builders.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/internal/custom/gmock-generated-actions.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/internal/custom/gmock-matchers.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/internal/custom/gmock-port.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/internal/gmock-internal-utils.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/internal/gmock-port.h",
- "$dir_pw_third_party_googletest/googlemock/include/gmock/internal/gmock-pp.h",
- "$dir_pw_third_party_googletest/googlemock/src/gmock-cardinalities.cc",
- "$dir_pw_third_party_googletest/googlemock/src/gmock-internal-utils.cc",
- "$dir_pw_third_party_googletest/googlemock/src/gmock-matchers.cc",
- "$dir_pw_third_party_googletest/googlemock/src/gmock-spec-builders.cc",
- "$dir_pw_third_party_googletest/googlemock/src/gmock.cc",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-death-test.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-matchers.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-message.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-param-test.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-printers.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-spi.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-test-part.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest-typed-test.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest_pred_impl.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/gtest_prod.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/custom/gtest-port.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/custom/gtest-printers.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/custom/gtest.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/gtest-death-test-internal.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/gtest-filepath.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/gtest-internal.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/gtest-param-util.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/gtest-port-arch.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/gtest-port.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/gtest-string.h",
- "$dir_pw_third_party_googletest/googletest/include/gtest/internal/gtest-type-util.h",
- "$dir_pw_third_party_googletest/googletest/src/gtest-death-test.cc",
- "$dir_pw_third_party_googletest/googletest/src/gtest-filepath.cc",
- "$dir_pw_third_party_googletest/googletest/src/gtest-internal-inl.h",
- "$dir_pw_third_party_googletest/googletest/src/gtest-matchers.cc",
- "$dir_pw_third_party_googletest/googletest/src/gtest-port.cc",
- "$dir_pw_third_party_googletest/googletest/src/gtest-printers.cc",
- "$dir_pw_third_party_googletest/googletest/src/gtest-test-part.cc",
- "$dir_pw_third_party_googletest/googletest/src/gtest-typed-test.cc",
- "$dir_pw_third_party_googletest/googletest/src/gtest.cc",
+ "$dir_pw_third_party_googletest/googlemock/src/gmock-all.cc",
+ "$dir_pw_third_party_googletest/googletest/src/gtest-all.cc",
]
}
@@ -106,3 +68,7 @@ if (dir_pw_third_party_googletest != "") {
group("googletest") {
}
}
+
+pw_doc_group("docs") {
+ sources = [ "docs.rst" ]
+}
diff --git a/third_party/googletest/CMakeLists.txt b/third_party/googletest/CMakeLists.txt
new file mode 100644
index 000000000..64f3842f8
--- /dev/null
+++ b/third_party/googletest/CMakeLists.txt
@@ -0,0 +1,76 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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($ENV{PW_ROOT}/pw_build/pigweed.cmake)
+
+set(dir_pw_third_party_googletest "" CACHE PATH
+ "Path to the googletest installation. When set, pw_third_party.googletest \
+ is provided")
+
+# If googletest is not configured, a script that displays an error message is
+# used instead. If the build rule is used in the build it fails with this
+# error.
+if(NOT dir_pw_third_party_googletest)
+ pw_add_error_target(pw_third_party.googletest
+ MESSAGE
+ "Attempted to build the pw_third_party.googletest without "
+ "configuring it via dir_pw_third_party_googletest. "
+ "See https://pigweed.dev/third_party/googletest."
+ )
+
+ pw_add_library(pw_third_party.googletest.gtest_main INTERFACE
+ PUBLIC_DEPS
+ pw_third_party.googletest
+ )
+
+ pw_add_library(pw_third_party.googletest.gmock_main INTERFACE
+ PUBLIC_DEPS
+ pw_third_party.googletest
+ )
+
+else() # dir_pw_thid_parrty_googletest is set
+ pw_add_library(pw_third_party.googletest STATIC
+ SOURCES
+ # Only add the "*-all.cc" files (and no headers) to improve maintainability
+ # from upstream refactoring. The "*-all.cc" files include the respective
+ # source files.
+ ${dir_pw_third_party_googletest}/googlemock/src/gmock-all.cc
+ ${dir_pw_third_party_googletest}/googletest/src/gtest-all.cc
+ HEADERS
+ ${dir_pw_third_party_googletest}/googlemock/include/gmock/gmock.h
+ ${dir_pw_third_party_googletest}/googletest/include/gtest/gtest-spi.h
+ ${dir_pw_third_party_googletest}/googletest/include/gtest/gtest.h
+ PUBLIC_INCLUDES
+ ${dir_pw_third_party_googletest}/googletest
+ ${dir_pw_third_party_googletest}/googletest/include
+ ${dir_pw_third_party_googletest}/googlemock
+ ${dir_pw_third_party_googletest}/googlemock/include
+ PUBLIC_COMPILE_OPTIONS
+ -Wno-undef
+ )
+
+ pw_add_library(pw_third_party.googletest.gtest_main STATIC
+ SOURCES
+ ${dir_pw_third_party_googletest}/googletest/src/gtest_main.cc
+ PUBLIC_DEPS
+ pw_third_party.googletest
+ )
+
+ pw_add_library(pw_third_party.googletest.gmock_main STATIC
+ SOURCES
+ ${dir_pw_third_party_googletest}/googlemock/src/gmock_main.cc
+ PUBLIC_DEPS
+ pw_third_party.googletest
+ )
+endif()
diff --git a/third_party/googletest/docs.rst b/third_party/googletest/docs.rst
new file mode 100644
index 000000000..e9cf3b158
--- /dev/null
+++ b/third_party/googletest/docs.rst
@@ -0,0 +1,44 @@
+.. _module-pw_third_party_googletest:
+
+==========
+GoogleTest
+==========
+The ``$dir_pw_third_party/googletest/`` module provides various helpers to
+optionally use full upstream GoogleTest/GoogleMock with
+:ref:`module-pw_unit_test`.
+
+----------------------------------------
+Using upstream GoogleTest and GoogleMock
+----------------------------------------
+If you want to use the full upstream GoogleTest/GoogleMock, you must do the
+following:
+
+Submodule
+=========
+Add GoogleTest to your workspace with the following command.
+
+.. code-block:: sh
+
+ git submodule add https://github.com/google/googletest third_party/googletest
+
+GN
+==
+* Set the GN var ``dir_pw_third_party_googletest`` to the location of the
+ GoogleTest source. If you used the command above this will be
+ ``//third_party/googletest``.
+* Set the GN var ``pw_unit_test_MAIN = dir_pigweed + "/third_party/googletest:gmock_main"``.
+* Set the GN var
+ ``pw_unit_test_GOOGLETEST_BACKEND = "//third_party/googletest"``.
+
+CMake
+=====
+* Set the ``dir_pw_third_party_googletest`` to the location of the
+ GoogleTest source.
+* Set the var ``pw_unit_test_MAIN`` to ``pw_third_party.googletest.gmock_main``.
+* Set the var ``pw_unit_test_GOOGLETEST_BACKEND`` to
+ ``pw_third_party.googletest``.
+
+.. note::
+
+ Not all unit tests build properly with upstream GoogleTest yet. This is a
+ work in progress.
diff --git a/third_party/mbedtls/BUILD.gn b/third_party/mbedtls/BUILD.gn
index 27bc89df3..6d91da071 100644
--- a/third_party/mbedtls/BUILD.gn
+++ b/third_party/mbedtls/BUILD.gn
@@ -107,7 +107,11 @@ if (dir_pw_third_party_mbedtls != "") {
"library/xtea.c",
]
- config("mbedtls_config") {
+ config("public_config") {
+ include_dirs = [ "$dir_pw_third_party_mbedtls/include" ]
+ }
+
+ config("internal_config") {
include_dirs = [
"$dir_pw_third_party_mbedtls",
"$dir_pw_third_party_mbedtls/include",
@@ -118,8 +122,8 @@ if (dir_pw_third_party_mbedtls != "") {
"-Wno-error=redundant-decls",
"-w",
]
-
- config_header_file = rebase_path(pw_third_party_mbedtls_CONFIG_HEADER)
+ config_header_file = rebase_path(pw_third_party_mbedtls_CONFIG_HEADER,
+ get_label_info(":mbedtls", "dir"))
defines = [ "MBEDTLS_CONFIG_FILE=\"$config_header_file\"" ]
}
@@ -135,7 +139,8 @@ if (dir_pw_third_party_mbedtls != "") {
]
public_deps = [ "$dir_pw_tls_client:time" ]
- public_configs = [ ":mbedtls_config" ]
+ public_configs = [ ":public_config" ]
+ configs = [ ":internal_config" ]
}
} else {
group("mbedtls") {
diff --git a/third_party/mbedtls/README.md b/third_party/mbedtls/README.md
index 4d11f6621..39c3e62f5 100644
--- a/third_party/mbedtls/README.md
+++ b/third_party/mbedtls/README.md
@@ -1,8 +1,8 @@
# MbedTLS Library
The folder provides build scripts and configuration recipes for building
-the MbedTLS library. The source code needs to be downloaded by the user, or
-via the support in pw_package "pw package install mbedtls". For gn build,
+the MbedTLS library. The source code needs to be downloaded by the user,
+preferably using Git submodules. For gn build,
set `dir_pw_third_party_mbedtls` to point to the path of the source code.
For applications using MbedTLS, add `$dir_pw_third_party/mbedtls` to the
dependency list. The config header can be set using gn variable
diff --git a/third_party/micro_ecc/BUILD.gn b/third_party/micro_ecc/BUILD.gn
index e0de9fa22..39cfd09ea 100644
--- a/third_party/micro_ecc/BUILD.gn
+++ b/third_party/micro_ecc/BUILD.gn
@@ -17,18 +17,50 @@ import("$dir_pw_build/target_types.gni")
import("micro_ecc.gni")
if (dir_pw_third_party_micro_ecc != "") {
- config("default") {
+ config("public_config") {
+ include_dirs = [ "$dir_pw_third_party_micro_ecc/" ]
+ }
+
+ config("internal_config") {
# Suppress all upstream introduced warnings.
cflags = [ "-w" ]
- include_dirs = [ "$dir_pw_third_party_micro_ecc/" ]
-
# Disabling point compression saves 200 bytes.
defines = [ "uECC_SUPPORT_COMPRESSED_POINT=0" ]
}
+ # Endianess is a public configuration for uECC as it determines how large
+ # integers are interpreted in uECC public APIs.
+ #
+ # Big endian is a lot more common and thus is recommended unless you are
+ # really resource-constrained or another uECC client expects little
+ # endian.
+ config("big_endian_config") {
+ defines = [ "uECC_VLI_NATIVE_LITTLE_ENDIAN=0" ]
+ }
+
+ # Little endian can reduce call stack usage in native little endian
+ # execution environments (as determined by processor state, memory
+ # access config etc.)
+ config("little_endian_config") {
+ defines = [ "uECC_VLI_NATIVE_LITTLE_ENDIAN=1" ]
+ }
+
pw_source_set("micro_ecc") {
- public_configs = [ ":default" ]
+ public_configs = [
+ ":big_endian_config",
+ ":public_config",
+ ]
+ configs = [ ":internal_config" ]
+ sources = [ "$dir_pw_third_party_micro_ecc/uECC.c" ]
+ }
+
+ pw_source_set("micro_ecc_little_endian") {
+ public_configs = [
+ ":little_endian_config",
+ ":public_config",
+ ]
+ configs = [ ":internal_config" ]
sources = [ "$dir_pw_third_party_micro_ecc/uECC.c" ]
}
} else {
diff --git a/third_party/micro_ecc/BUILD.micro_ecc b/third_party/micro_ecc/BUILD.micro_ecc
new file mode 100644
index 000000000..1ed01ae0f
--- /dev/null
+++ b/third_party/micro_ecc/BUILD.micro_ecc
@@ -0,0 +1,28 @@
+# Copyright 2022 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+
+cc_library(
+ name = "uecc",
+ srcs = [
+ "curve-specific.inc",
+ "platform-specific.inc",
+ "uECC.c",
+ ],
+ hdrs = [
+ "types.h",
+ "uECC.h",
+ "uECC_vli.h",
+ ],
+ visibility = ["//visibility:public"],
+)
diff --git a/third_party/nanopb/BUILD.gn b/third_party/nanopb/BUILD.gn
index 8e43b73f7..a79221a69 100644
--- a/third_party/nanopb/BUILD.gn
+++ b/third_party/nanopb/BUILD.gn
@@ -59,6 +59,7 @@ if (dir_pw_third_party_nanopb != "") {
pw_python_script("generate_nanopb_proto") {
sources = [ "generate_nanopb_proto.py" ]
pylintrc = "$dir_pigweed/.pylintrc"
+ mypy_ini = "$dir_pigweed/.mypy.ini"
action = {
args = [ rebase_path(dir_pw_third_party_nanopb, root_build_dir) ]
stamp = true
diff --git a/third_party/nanopb/CMakeLists.txt b/third_party/nanopb/CMakeLists.txt
index 11cde7fb9..c8461a1d0 100644
--- a/third_party/nanopb/CMakeLists.txt
+++ b/third_party/nanopb/CMakeLists.txt
@@ -20,6 +20,11 @@ option(pw_third_party_nanopb_ADD_SUBDIRECTORY
"Whether to add the dir_pw_third_party_nanopb subdirectory" OFF)
if("${dir_pw_third_party_nanopb}" STREQUAL "")
+ pw_add_error_target(pw_third_party.nanopb
+ MESSAGE
+ "Attempted to use nanopb without configuring it, see "
+ "pigweed.dev/third_party/nanopb/"
+ )
return()
elseif(pw_third_party_nanopb_ADD_SUBDIRECTORY)
add_subdirectory("${dir_pw_third_party_nanopb}" third_party/nanopb)
diff --git a/third_party/nanopb/generate_nanopb_proto.py b/third_party/nanopb/generate_nanopb_proto.py
index 166db858d..2934d0635 100644
--- a/third_party/nanopb/generate_nanopb_proto.py
+++ b/third_party/nanopb/generate_nanopb_proto.py
@@ -34,7 +34,8 @@ def generate_nanopb_proto(root: Path) -> None:
sys.path.append(str(root / 'generator'))
spec = importlib.util.spec_from_file_location(
- 'proto', root / 'generator' / 'proto' / '__init__.py')
+ 'proto', root / 'generator' / 'proto' / '__init__.py'
+ )
assert spec is not None
proto_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(proto_module) # type: ignore[union-attr]
diff --git a/third_party/pico_sdk/OWNERS b/third_party/pico_sdk/OWNERS
new file mode 100644
index 000000000..55936259d
--- /dev/null
+++ b/third_party/pico_sdk/OWNERS
@@ -0,0 +1,2 @@
+amontanez@google.com
+tonymd@google.com
diff --git a/third_party/pico_sdk/gn/BUILD.gn b/third_party/pico_sdk/gn/BUILD.gn
index f411b8033..8234f01c1 100644
--- a/third_party/pico_sdk/gn/BUILD.gn
+++ b/third_party/pico_sdk/gn/BUILD.gn
@@ -13,15 +13,11 @@
# the License.
# These warnings need to be disabled when using strict warnings.
-#
-# TODO(amontanez): Just applying these flags to Pi Pico source sets does not
-# work because of Pigweed's default_configs notion and how it orders flags.
-# Removing Pigweed's strict warnings config is the only working solution for
-# now.
config("disable_warnings") {
cflags = [
"-Wno-undef",
"-Wno-unused-function",
+ "-Wno-ignored-qualifiers",
]
asmflags = cflags
}
diff --git a/third_party/pico_sdk/gn/generate_config_header.gni b/third_party/pico_sdk/gn/generate_config_header.gni
index 0cbc4e0e1..ee2a5b7c9 100644
--- a/third_party/pico_sdk/gn/generate_config_header.gni
+++ b/third_party/pico_sdk/gn/generate_config_header.gni
@@ -57,8 +57,10 @@ template("generate_config_header") {
# This source set bundles up the generated header such that depending on
# this template will allow targets to include "pico/config_autogen.h".
pw_source_set("${target_name}") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":${target_name}.public_include_dirs" ]
+ public_configs = [
+ "${PICO_ROOT}/gn:disable_warnings",
+ ":${target_name}.public_include_dirs",
+ ]
deps = [ ":${target_name}.generated_header" ]
public = [ "${_generated_header_path}" ]
forward_variables_from(invoker, "*", [ "config_header_files" ])
diff --git a/third_party/pico_sdk/src/BUILD.gn b/third_party/pico_sdk/src/BUILD.gn
index 62b395275..1b3b302dd 100644
--- a/third_party/pico_sdk/src/BUILD.gn
+++ b/third_party/pico_sdk/src/BUILD.gn
@@ -12,6 +12,11 @@
# License for the specific language governing permissions and limitations under
# the License.
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
# TODO(amontanez): If a successor to the RP2040 comes out, this might need to
# be a little smarter about what code is pulled in.
group("pico_sdk") {
@@ -21,3 +26,20 @@ group("pico_sdk") {
"rp2_common",
]
}
+
+config("elf2uf2_configs") {
+ include_dirs = [ "$PICO_SRC_DIR/src/common/boot_uf2/include" ]
+ cflags_cc = [ "-std=gnu++14" ]
+ cflags = [
+ "-Wno-reorder-ctor",
+ "-Wno-unused-variable",
+ ]
+}
+
+pw_executable("elf2uf2") {
+ configs = [
+ ":elf2uf2_configs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
+ sources = [ "$PICO_SRC_DIR/tools/elf2uf2/main.cpp" ]
+}
diff --git a/third_party/pico_sdk/src/boards/BUILD.gn b/third_party/pico_sdk/src/boards/BUILD.gn
index d92764e2b..c6b6fdf21 100644
--- a/third_party/pico_sdk/src/boards/BUILD.gn
+++ b/third_party/pico_sdk/src/boards/BUILD.gn
@@ -26,26 +26,41 @@ config("public_include_dirs") {
}
pw_source_set("boards") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public = [
"${_CWD}/include/boards/adafruit_feather_rp2040.h",
"${_CWD}/include/boards/adafruit_itsybitsy_rp2040.h",
+ "${_CWD}/include/boards/adafruit_kb2040.h",
+ "${_CWD}/include/boards/adafruit_macropad_rp2040.h",
"${_CWD}/include/boards/adafruit_qtpy_rp2040.h",
"${_CWD}/include/boards/adafruit_trinkey_qt2040.h",
"${_CWD}/include/boards/arduino_nano_rp2040_connect.h",
+ "${_CWD}/include/boards/datanoisetv_rp2040_dsp.h",
+ "${_CWD}/include/boards/eetree_gamekit_rp2040.h",
+ "${_CWD}/include/boards/garatronic_pybstick26_rp2040.h",
"${_CWD}/include/boards/melopero_shake_rp2040.h",
"${_CWD}/include/boards/none.h",
"${_CWD}/include/boards/pico.h",
+ "${_CWD}/include/boards/pico_w.h",
+ "${_CWD}/include/boards/pimoroni_badger2040.h",
"${_CWD}/include/boards/pimoroni_interstate75.h",
"${_CWD}/include/boards/pimoroni_keybow2040.h",
+ "${_CWD}/include/boards/pimoroni_motor2040.h",
"${_CWD}/include/boards/pimoroni_pga2040.h",
"${_CWD}/include/boards/pimoroni_picolipo_16mb.h",
"${_CWD}/include/boards/pimoroni_picolipo_4mb.h",
"${_CWD}/include/boards/pimoroni_picosystem.h",
"${_CWD}/include/boards/pimoroni_plasma2040.h",
+ "${_CWD}/include/boards/pimoroni_servo2040.h",
"${_CWD}/include/boards/pimoroni_tiny2040.h",
- "${_CWD}/include/boards/pybstick26_rp2040.h",
+ "${_CWD}/include/boards/pimoroni_tiny2040_2mb.h",
+ "${_CWD}/include/boards/seeed_xiao_rp2040.h",
+ "${_CWD}/include/boards/solderparty_rp2040_stamp.h",
+ "${_CWD}/include/boards/solderparty_rp2040_stamp_carrier.h",
+ "${_CWD}/include/boards/solderparty_rp2040_stamp_round_carrier.h",
"${_CWD}/include/boards/sparkfun_micromod.h",
"${_CWD}/include/boards/sparkfun_promicro.h",
"${_CWD}/include/boards/sparkfun_thingplus.h",
@@ -54,5 +69,6 @@ pw_source_set("boards") {
"${_CWD}/include/boards/waveshare_rp2040_plus_16mb.h",
"${_CWD}/include/boards/waveshare_rp2040_plus_4mb.h",
"${_CWD}/include/boards/waveshare_rp2040_zero.h",
+ "${_CWD}/include/boards/wiznet_w5100s_evb_pico.h",
]
}
diff --git a/third_party/pico_sdk/src/common/BUILD.gn b/third_party/pico_sdk/src/common/BUILD.gn
index 3dda4de96..924868816 100644
--- a/third_party/pico_sdk/src/common/BUILD.gn
+++ b/third_party/pico_sdk/src/common/BUILD.gn
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations under
# the License.
+import("//build_overrides/pi_pico.gni")
+
group("common") {
public_deps = [
"boot_picoboot",
diff --git a/third_party/pico_sdk/src/common/boot_picoboot/BUILD.gn b/third_party/pico_sdk/src/common/boot_picoboot/BUILD.gn
index 2e141d4f2..8110df48d 100644
--- a/third_party/pico_sdk/src/common/boot_picoboot/BUILD.gn
+++ b/third_party/pico_sdk/src/common/boot_picoboot/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("boot_picoboot") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
# Optionally requires a dep on "pico/platform.h"
diff --git a/third_party/pico_sdk/src/common/boot_uf2/BUILD.gn b/third_party/pico_sdk/src/common/boot_uf2/BUILD.gn
index 2e6468b90..f1dd2a853 100644
--- a/third_party/pico_sdk/src/common/boot_uf2/BUILD.gn
+++ b/third_party/pico_sdk/src/common/boot_uf2/BUILD.gn
@@ -26,7 +26,9 @@ config("public_include_dirs") {
}
pw_source_set("boot_uf2") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public = [ "${_CWD}/include/boot/uf2.h" ]
}
diff --git a/third_party/pico_sdk/src/common/pico_base/BUILD.gn b/third_party/pico_sdk/src/common/pico_base/BUILD.gn
index 73e176dc0..179750a6b 100644
--- a/third_party/pico_sdk/src/common/pico_base/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_base/BUILD.gn
@@ -44,10 +44,10 @@ generate_version_header("version") {
}
pw_source_set("pico_base") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
public_configs = [
":board_define",
":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
]
public = [
"${_CWD}/include/pico.h",
diff --git a/third_party/pico_sdk/src/common/pico_base/generate_version_header.gni b/third_party/pico_sdk/src/common/pico_base/generate_version_header.gni
index 189bf7e8f..5200cf5e0 100644
--- a/third_party/pico_sdk/src/common/pico_base/generate_version_header.gni
+++ b/third_party/pico_sdk/src/common/pico_base/generate_version_header.gni
@@ -64,8 +64,10 @@ template("generate_version_header") {
}
pw_source_set("${target_name}") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":${target_name}.public_include_dirs" ]
+ public_configs = [
+ "${PICO_ROOT}/gn:disable_warnings",
+ ":${target_name}.public_include_dirs",
+ ]
deps = [ ":${target_name}.generated_header" ]
public = [ "${_generated_header_path}" ]
forward_variables_from(invoker,
diff --git a/third_party/pico_sdk/src/common/pico_binary_info/BUILD.gn b/third_party/pico_sdk/src/common/pico_binary_info/BUILD.gn
index 86fbbb510..6bed6bbdc 100644
--- a/third_party/pico_sdk/src/common/pico_binary_info/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_binary_info/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_binary_info") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/common/pico_base" ]
public = [
"${_CWD}/include/pico/binary_info.h",
diff --git a/third_party/pico_sdk/src/common/pico_bit_ops/BUILD.gn b/third_party/pico_sdk/src/common/pico_bit_ops/BUILD.gn
index 7a55fb15b..e7ca0149b 100644
--- a/third_party/pico_sdk/src/common/pico_bit_ops/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_bit_ops/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_bit_ops") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/common/pico_base" ]
public = [ "${_CWD}/include/pico/bit_ops.h" ]
}
diff --git a/third_party/pico_sdk/src/common/pico_divider/BUILD.gn b/third_party/pico_sdk/src/common/pico_divider/BUILD.gn
index dea76d4a4..fb5a81ed0 100644
--- a/third_party/pico_sdk/src/common/pico_divider/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_divider/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_divider") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2_common/hardware_divider",
diff --git a/third_party/pico_sdk/src/common/pico_stdlib/BUILD.gn b/third_party/pico_sdk/src/common/pico_stdlib/BUILD.gn
index 721254d83..06d8bd3c7 100644
--- a/third_party/pico_sdk/src/common/pico_stdlib/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_stdlib/BUILD.gn
@@ -29,8 +29,10 @@ config("public_include_dirs") {
}
pw_source_set("headers") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ "${PICO_ROOT}/gn:disable_warnings",
+ ":public_include_dirs",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_time",
@@ -43,16 +45,24 @@ pw_source_set("headers") {
public_deps += [ "${PICO_ROOT}/src/rp2_common/pico_stdio_uart" ]
} else if (PICO_STDIO == ENUM_LIB_PICO_STDIO.USB) {
public_deps += [ "${PICO_ROOT}/src/rp2_common/pico_stdio_usb" ]
+
+ # Required to allow circular dependencies.
+ deps = [ "${PICO_ROOT}/src/rp2_common/tinyusb:bsp" ]
+ allow_circular_includes_from = [
+ "${PICO_ROOT}/src/rp2_common/pico_stdio_usb",
+ "${PICO_ROOT}/src/rp2_common/tinyusb:bsp",
+ ]
} else if (PICO_STDIO == ENUM_LIB_PICO_STDIO.SEMIHOSTING) {
public_deps += [ "${PICO_ROOT}/src/rp2_common/pico_stdio_semihosting" ]
}
- public = [ "include/pico/stdlib.h" ]
+ public = [ "${_CWD}/include/pico/stdlib.h" ]
}
pw_source_set("pico_stdlib") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
+ public_configs = [ "${PICO_ROOT}/gn:disable_warnings" ]
public_deps = [ ":headers" ]
+ public = [ "${_CWD}/include/pico/stdlib.h" ]
# Ensure the pico stdlib implementation is linked in.
deps = [ "${PICO_ROOT}/src/rp2_common/pico_stdlib" ]
diff --git a/third_party/pico_sdk/src/common/pico_stdlib/pico_stdio.gni b/third_party/pico_sdk/src/common/pico_stdlib/pico_stdio.gni
index 8d1f27b03..7ed7a9a1b 100644
--- a/third_party/pico_sdk/src/common/pico_stdlib/pico_stdio.gni
+++ b/third_party/pico_sdk/src/common/pico_stdlib/pico_stdio.gni
@@ -20,5 +20,5 @@ ENUM_LIB_PICO_STDIO = {
# TODO(amontanez): This looks like a facade. Rethink?
declare_args() {
- PICO_STDIO = ENUM_LIB_PICO_STDIO.UART
+ PICO_STDIO = ENUM_LIB_PICO_STDIO.USB
}
diff --git a/third_party/pico_sdk/src/common/pico_sync/BUILD.gn b/third_party/pico_sdk/src/common/pico_sync/BUILD.gn
index 4fe1f46fa..fccc423c4 100644
--- a/third_party/pico_sdk/src/common/pico_sync/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_sync/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_sync") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_time:headers",
diff --git a/third_party/pico_sdk/src/common/pico_time/BUILD.gn b/third_party/pico_sdk/src/common/pico_time/BUILD.gn
index ca0dff6b9..25a08d495 100644
--- a/third_party/pico_sdk/src/common/pico_time/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_time/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("headers") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2_common/hardware_timer",
@@ -39,7 +41,7 @@ pw_source_set("headers") {
}
pw_source_set("pico_time") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
+ public_configs = [ "${PICO_ROOT}/gn:disable_warnings" ]
public_deps = [ ":headers" ]
deps = [ "${PICO_ROOT}/src/common/pico_util" ]
sources = [
diff --git a/third_party/pico_sdk/src/common/pico_usb_reset_interface/BUILD.gn b/third_party/pico_sdk/src/common/pico_usb_reset_interface/BUILD.gn
index 2c3abd89b..ce201b4e4 100644
--- a/third_party/pico_sdk/src/common/pico_usb_reset_interface/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_usb_reset_interface/BUILD.gn
@@ -26,7 +26,9 @@ config("public_include_dirs") {
}
pw_source_set("pico_usb_reset_interface") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public = [ "${_CWD}/include/pico/usb_reset_interface.h" ]
}
diff --git a/third_party/pico_sdk/src/common/pico_util/BUILD.gn b/third_party/pico_sdk/src/common/pico_util/BUILD.gn
index eed47bde9..a283d013f 100644
--- a/third_party/pico_sdk/src/common/pico_util/BUILD.gn
+++ b/third_party/pico_sdk/src/common/pico_util/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_util") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_sync",
diff --git a/third_party/pico_sdk/src/rp2040/hardware_regs/BUILD.gn b/third_party/pico_sdk/src/rp2040/hardware_regs/BUILD.gn
index 739d4bb38..946eaa1c0 100644
--- a/third_party/pico_sdk/src/rp2040/hardware_regs/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2040/hardware_regs/BUILD.gn
@@ -26,14 +26,22 @@ config("public_include_dirs") {
}
pw_source_set("platform_defs") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
- public = [ "${_CWD}/include/hardware/platform_defs.h" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
+ public = [
+ "${_CWD}/include/hardware/platform_defs.h",
+ "${_CWD}/include/hardware/regs/addressmap.h",
+ "${_CWD}/include/hardware/regs/sio.h",
+ ]
}
pw_source_set("hardware_regs") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
":platform_defs",
"${PICO_ROOT}/src/rp2_common/pico_platform:headers",
diff --git a/third_party/pico_sdk/src/rp2040/hardware_structs/BUILD.gn b/third_party/pico_sdk/src/rp2040/hardware_structs/BUILD.gn
index b472f5e2c..1a9edfe7c 100644
--- a/third_party/pico_sdk/src/rp2040/hardware_structs/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2040/hardware_structs/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_structs") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/rp2040/hardware_regs",
"${PICO_ROOT}/src/rp2040/hardware_regs:platform_defs",
diff --git a/third_party/pico_sdk/src/rp2_common/BUILD.gn b/third_party/pico_sdk/src/rp2_common/BUILD.gn
index 494af4a4e..f3d4f7f10 100644
--- a/third_party/pico_sdk/src/rp2_common/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/BUILD.gn
@@ -12,6 +12,8 @@
# License for the specific language governing permissions and limitations under
# the License.
+import("//build_overrides/pi_pico.gni")
+
group("rp2_common") {
public_deps = [
"hardware_adc",
diff --git a/third_party/pico_sdk/src/rp2_common/boot_stage2/BUILD.gn b/third_party/pico_sdk/src/rp2_common/boot_stage2/BUILD.gn
index d23802a89..2a6851ed3 100644
--- a/third_party/pico_sdk/src/rp2_common/boot_stage2/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/boot_stage2/BUILD.gn
@@ -30,6 +30,17 @@ config("public_include_dirs") {
]
}
+# The upstream boot_stage2.ld doesn't specify the binary entry point or
+# mark the required sections as KEEP(), so they're optimized out with
+# Pigweed's aggressive default optimizations.
+#
+# Because of Pigweed's pw_build_DEFAULT_CONFIGS behavior, this flag
+# needs to be a config rather than just an ldflag of boot_stage2_elf to ensure
+# the flag is ordered properly.
+config("no_gc_sections") {
+ ldflags = [ "-Wl,--no-gc-sections" ]
+}
+
pw_executable("boot_stage2_elf") {
_linker_script_path = rebase_path("${_CWD}/boot_stage2.ld", root_build_dir)
@@ -41,22 +52,12 @@ pw_executable("boot_stage2_elf") {
ldflags += [
"-T${_linker_script_path}",
"-nostartfiles",
-
- # Unfortunately, this is not properly applied to compiler flags thanks to
- # `default_configs`.
- "-Wl,--no-gc-sections",
]
public_configs = [ ":public_include_dirs" ]
+ configs = [ ":no_gc_sections" ]
- # The upstream boot_stage2.ld doesn't specify the binary entry point or
- # mark the required sections as KEEP(), so they're optimized out with
- # Pigweed's aggressive default optimizations.
- remove_configs = [
- "$dir_pw_build:reduced_size",
- "$dir_pw_build:strict_warnings",
- ]
- no_link_deps = true
+ is_boot_stage2 = true
public = [ "${_CWD}/include/boot_stage2/config.h" ]
diff --git a/third_party/pico_sdk/src/rp2_common/cmsis/BUILD.gn b/third_party/pico_sdk/src/rp2_common/cmsis/BUILD.gn
index 94c8f41fa..1e997c54d 100644
--- a/third_party/pico_sdk/src/rp2_common/cmsis/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/cmsis/BUILD.gn
@@ -30,16 +30,20 @@ config("public_include_dirs") {
}
pw_source_set("rename_exceptions") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
- public = [ "include/cmsis/rename_exceptions.h" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
+ public = [ "${_CWD}/include/cmsis/rename_exceptions.h" ]
}
# TODO(amontanez): The CMSIS stub should probably be more configurable to match
# CMake.
pw_source_set("cmsis") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public = [
"${_CWD}/stub/CMSIS/Core/Include/cmsis_armcc.h",
"${_CWD}/stub/CMSIS/Core/Include/cmsis_armclang.h",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_adc/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_adc/BUILD.gn
index 7a9364651..244be5baa 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_adc/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_adc/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_adc") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_base/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_base/BUILD.gn
index 00909d44c..7f78ed7e8 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_base/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_base/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_base") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_claim/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_claim/BUILD.gn
index 9ced82c0d..605d3429b 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_claim/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_claim/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_claim") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2_common/hardware_sync",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_clocks/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_clocks/BUILD.gn
index dd4eb4791..6f2322816 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_clocks/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_clocks/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_clocks") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public = [ "${_CWD}/include/hardware/clocks.h" ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_divider/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_divider/BUILD.gn
index 25e1a5fca..a277b2e22 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_divider/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_divider/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_divider") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_dma/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_dma/BUILD.gn
index 33ca25c33..060461d1a 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_dma/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_dma/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_dma") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_exception/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_exception/BUILD.gn
index cb8e2dee1..4b8e38520 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_exception/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_exception/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_exception") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_flash/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_flash/BUILD.gn
index 6e3e19014..57ec6c8cc 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_flash/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_flash/BUILD.gn
@@ -25,9 +25,19 @@ config("public_include_dirs") {
include_dirs = [ "${_CWD}/include" ]
}
+config("disable_warnings") {
+ cflags = [
+ "-Wno-pointer-arith",
+ "-Wno-shadow",
+ ]
+}
+
pw_source_set("hardware_flash") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
+ configs = [ ":disable_warnings" ]
public_deps = [ "${PICO_ROOT}/src/common/pico_base" ]
deps = [
"${PICO_ROOT}/src/common/pico_sync",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_gpio/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_gpio/BUILD.gn
index 193f3236a..b2a7459cc 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_gpio/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_gpio/BUILD.gn
@@ -26,18 +26,20 @@ config("public_include_dirs") {
}
pw_source_set("hardware_gpio") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
+ "${PICO_ROOT}/src/rp2_common/hardware_irq",
]
deps = [
# TODO(amontanez): This is off by default, properly control with
# configuration.
# "${PICO_ROOT}/src/common/pico_binary_info",
- "${PICO_ROOT}/src/rp2_common/hardware_irq",
"${PICO_ROOT}/src/rp2_common/hardware_sync",
]
public = [ "${_CWD}/include/hardware/gpio.h" ]
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_i2c/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_i2c/BUILD.gn
index 1c3f717a5..9db318062 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_i2c/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_i2c/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_i2c") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_time",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_interp/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_interp/BUILD.gn
index 6a2dec8c6..f48373386 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_interp/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_interp/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_interp") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_irq/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_irq/BUILD.gn
index a21900ee0..83f6f17d1 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_irq/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_irq/BUILD.gn
@@ -26,12 +26,15 @@ config("public_include_dirs") {
}
pw_source_set("hardware_irq") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
"${PICO_ROOT}/src/rp2_common/hardware_base",
+ "${PICO_ROOT}/src/rp2_common/hardware_claim",
]
deps = [
"${PICO_ROOT}/src/common/pico_sync",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_pio/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_pio/BUILD.gn
index f1fc257df..9f5177a2b 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_pio/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_pio/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_pio") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_pll/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_pll/BUILD.gn
index 837611dff..2952e66d0 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_pll/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_pll/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_pll") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_pwm/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_pwm/BUILD.gn
index 968ef3e8f..b67cecb82 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_pwm/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_pwm/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_pwm") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_resets/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_resets/BUILD.gn
index 92beff00e..99cba5517 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_resets/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_resets/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_resets") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_rtc/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_rtc/BUILD.gn
index b59a0f2bf..eca3cc0f6 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_rtc/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_rtc/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_rtc") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_spi/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_spi/BUILD.gn
index 42f559747..b32518a32 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_spi/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_spi/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_spi") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_time",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_sync/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_sync/BUILD.gn
index 442aab30b..a85301a77 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_sync/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_sync/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_sync") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_timer/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_timer/BUILD.gn
index 6abf5acbc..97f6da194 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_timer/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_timer/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_timer") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_uart/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_uart/BUILD.gn
index 0c71aae95..04654d1a9 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_uart/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_uart/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_uart") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_regs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_vreg/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_vreg/BUILD.gn
index 1815fc078..b36293db4 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_vreg/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_vreg/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_vreg") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_watchdog/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_watchdog/BUILD.gn
index 644f72291..3acc5fd3d 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_watchdog/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_watchdog/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_watchdog") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/hardware_xosc/BUILD.gn b/third_party/pico_sdk/src/rp2_common/hardware_xosc/BUILD.gn
index a2d77d862..6e5d69302 100644
--- a/third_party/pico_sdk/src/rp2_common/hardware_xosc/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/hardware_xosc/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("hardware_xosc") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2040/hardware_structs",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_bootrom/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_bootrom/BUILD.gn
index d1d28a2f3..2f401acdc 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_bootrom/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_bootrom/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_bootrom") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/common/pico_base" ]
public = [
"${_CWD}/include/pico/bootrom.h",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_bootsel_via_double_reset/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_bootsel_via_double_reset/BUILD.gn
index 5f615278d..89dd81e53 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_bootsel_via_double_reset/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_bootsel_via_double_reset/BUILD.gn
@@ -25,8 +25,8 @@ pw_source_set("pico_bootsel_via_double_reset") {
deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_binary_info",
+ "${PICO_ROOT}/src/common/pico_time",
"${PICO_ROOT}/src/rp2_common/pico_bootrom",
- "${PICO_ROOT}/src/rp2_common/pico_time",
]
sources = [ "${_CWD}/pico_bootsel_via_double_reset.c" ]
}
diff --git a/third_party/pico_sdk/src/rp2_common/pico_double/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_double/BUILD.gn
index ef7feb76d..c2fe44f51 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_double/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_double/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_double") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2_common/pico_bootrom",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.gn
index 99d8cd466..bf80323f9 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_fix/rp2040_usb_device_enumeration/BUILD.gn
@@ -26,14 +26,16 @@ config("public_include_dirs") {
}
pw_source_set("rp2040_usb_device_enumeration") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
deps = [
"${PICO_ROOT}/src/common/pico_base",
+ "${PICO_ROOT}/src/common/pico_time",
"${PICO_ROOT}/src/rp2040/hardware_structs",
"${PICO_ROOT}/src/rp2_common/hardware_gpio",
- "${PICO_ROOT}/src/rp2_common/pico_time",
]
public = [ "${_CWD}/include/pico/fix/rp2040_usb_device_enumeration.h" ]
- sources = [ "${_CWD}/rp2040_usb_device_enumberation.c" ]
+ sources = [ "${_CWD}/rp2040_usb_device_enumeration.c" ]
}
diff --git a/third_party/pico_sdk/src/rp2_common/pico_float/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_float/BUILD.gn
index 04c0fdf00..967b6c085 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_float/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_float/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_float") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2_common/pico_bootrom",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_int64_ops/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_int64_ops/BUILD.gn
index 6458280d9..def0ace27 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_int64_ops/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_int64_ops/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_int64_ops") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/common/pico_base" ]
deps = [ "${PICO_ROOT}/src/rp2_common/pico_platform" ]
public = [ "${_CWD}/include/pico/int64_ops.h" ]
diff --git a/third_party/pico_sdk/src/rp2_common/pico_malloc/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_malloc/BUILD.gn
index ad1afbed9..4d080b2b3 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_malloc/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_malloc/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_malloc") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_sync",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_mem_ops/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_mem_ops/BUILD.gn
index df40c7edd..775f2703a 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_mem_ops/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_mem_ops/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_mem_ops") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/common/pico_base" ]
deps = [
"${PICO_ROOT}/src/rp2_common/pico_bootrom",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn
index d154954f5..1e7833b3d 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_multicore/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_multicore") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/common/pico_sync",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_platform/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_platform/BUILD.gn
index 593196dc4..f97238ef7 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_platform/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_platform/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("headers") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/rp2040/hardware_regs:platform_defs" ]
configs = [ "${PICO_ROOT}/src/rp2_common/pico_platform:public_include_dirs" ]
public = [
@@ -37,7 +39,7 @@ pw_source_set("headers") {
}
pw_source_set("pico_platform") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
+ public_configs = [ "${PICO_ROOT}/gn:disable_warnings" ]
public_deps = [ ":headers" ]
deps = [ "${PICO_ROOT}/src/rp2_common/hardware_base" ]
sources = [ "${_CWD}/platform.c" ]
diff --git a/third_party/pico_sdk/src/rp2_common/pico_printf/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_printf/BUILD.gn
index 00453c48b..a159a2fd6 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_printf/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_printf/BUILD.gn
@@ -34,8 +34,10 @@ pw_source_set("pico_printf_none") {
}
pw_source_set("pico_printf") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/common/pico_base" ]
deps = [ "${PICO_ROOT}/src/rp2_common/pico_platform" ]
public = [ "${_CWD}/include/pico/printf.h" ]
diff --git a/third_party/pico_sdk/src/rp2_common/pico_runtime/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_runtime/BUILD.gn
index f0c42c69e..134c1de23 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_runtime/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_runtime/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_runtime") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
deps = [
"${PICO_ROOT}/src/common/pico_base",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_standard_link/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_standard_link/BUILD.gn
index 457f93d8e..654c34baa 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_standard_link/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_standard_link/BUILD.gn
@@ -29,7 +29,7 @@ config("linker_script") {
}
pw_source_set("pico_standard_link") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
+ public_configs = [ "${PICO_ROOT}/gn:disable_warnings" ]
all_dependent_configs = [ ":linker_script" ]
inputs = [ "${_CWD}/memmap_default.ld" ]
deps = [
diff --git a/third_party/pico_sdk/src/rp2_common/pico_stdio/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_stdio/BUILD.gn
index 38e2c69ca..f58d518ed 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_stdio/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_stdio/BUILD.gn
@@ -25,6 +25,7 @@ import("${PICO_ROOT}/src/common/pico_stdlib/pico_stdio.gni")
config("public_include_dirs") {
include_dirs = [ "${_CWD}/include" ]
+ defines = [ "${PICO_STDIO}=1" ]
}
config("printf_wrappers") {
@@ -40,8 +41,10 @@ config("printf_wrappers") {
# TODO(amontanez): This is definitely a facade. For now, just have header and
# header+impl build targets to simulate.
pw_source_set("headers") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_base",
"${PICO_ROOT}/src/rp2_common/pico_platform",
@@ -54,7 +57,7 @@ pw_source_set("headers") {
}
pw_source_set("pico_stdio") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
+ public_configs = [ "${PICO_ROOT}/gn:disable_warnings" ]
all_dependent_configs = [ ":printf_wrappers" ]
public_deps = [ ":headers" ]
deps = [
@@ -70,4 +73,7 @@ pw_source_set("pico_stdio") {
deps += [ "${PICO_ROOT}/src/rp2_common/pico_stdio_semihosting" ]
}
sources = [ "${_CWD}/stdio.c" ]
+
+ # Due to disabling CRLF support.
+ cflags = [ "-Wno-unused-parameter" ]
}
diff --git a/third_party/pico_sdk/src/rp2_common/pico_stdio_semihosting/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_stdio_semihosting/BUILD.gn
index 5590ad224..a41941b74 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_stdio_semihosting/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_stdio_semihosting/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_stdio_semihosting") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/rp2_common/pico_stdio:headers" ]
deps = [ "${PICO_ROOT}/src/common/pico_binary_info" ]
public = [ "${_CWD}/include/pico/stdio_semihosting.h" ]
diff --git a/third_party/pico_sdk/src/rp2_common/pico_stdio_uart/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_stdio_uart/BUILD.gn
index 788a0de14..c4ca7a24a 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_stdio_uart/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_stdio_uart/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_stdio_uart") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/rp2_common/hardware_uart",
"${PICO_ROOT}/src/rp2_common/pico_stdio:headers",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn
index 30a3434d7..4bb51c5c3 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_stdio_usb/BUILD.gn
@@ -25,28 +25,47 @@ config("public_include_dirs") {
include_dirs = [ "${_CWD}/include" ]
}
-pw_source_set("pico_stdio_usb") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
+pw_source_set("tusb_config") {
public_configs = [ ":public_include_dirs" ]
+ public = [ "${_CWD}/include/tusb_config.h" ]
+ visibility = [
+ ":pico_stdio_usb",
+ "${PICO_ROOT}/src/rp2_common/tinyusb",
+ ]
+}
+
+pw_source_set("pico_stdio_usb") {
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [
"${PICO_ROOT}/src/common/pico_usb_reset_interface",
"${PICO_ROOT}/src/rp2_common/pico_stdio:headers",
]
deps = [
+ ":tusb_config",
+ "${PICO_ROOT}/src/common/pico_binary_info",
"${PICO_ROOT}/src/common/pico_sync",
"${PICO_ROOT}/src/common/pico_time",
"${PICO_ROOT}/src/rp2_common/hardware_irq",
+ "${PICO_ROOT}/src/rp2_common/hardware_watchdog",
+ "${PICO_ROOT}/src/rp2_common/pico_bootrom",
+ "${PICO_ROOT}/src/rp2_common/pico_unique_id",
+ "${PICO_ROOT}/src/rp2_common/tinyusb",
]
- # TODO(amontanez): Still needs a dependency on tinyusb.
public = [
"${_CWD}/include/pico/stdio_usb.h",
"${_CWD}/include/pico/stdio_usb/reset_interface.h",
- "${_CWD}/include/tusb_config.h",
]
sources = [
"${_CWD}/reset_interface.c",
"${_CWD}/stdio_usb.c",
"${_CWD}/stdio_usb_descriptors.c",
]
+ allow_circular_includes_from = [
+ "${PICO_ROOT}/src/rp2_common/tinyusb",
+ ":tusb_config",
+ ]
}
diff --git a/third_party/pico_sdk/src/rp2_common/pico_stdlib/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_stdlib/BUILD.gn
index 448d1d758..aca0c9eb7 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_stdlib/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_stdlib/BUILD.gn
@@ -24,7 +24,7 @@ _CWD = "${PICO_SRC_DIR}/src/rp2_common/pico_stdlib"
import("${PICO_ROOT}/src/common/pico_stdlib/pico_stdio.gni")
pw_source_set("pico_stdlib") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
+ public_configs = [ "${PICO_ROOT}/gn:disable_warnings" ]
deps = [
"${PICO_ROOT}/src/common/pico_stdlib:headers",
"${PICO_ROOT}/src/rp2_common/hardware_clocks",
diff --git a/third_party/pico_sdk/src/rp2_common/pico_unique_id/BUILD.gn b/third_party/pico_sdk/src/rp2_common/pico_unique_id/BUILD.gn
index a3f7b5750..c74f108e0 100644
--- a/third_party/pico_sdk/src/rp2_common/pico_unique_id/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/pico_unique_id/BUILD.gn
@@ -26,8 +26,10 @@ config("public_include_dirs") {
}
pw_source_set("pico_unique_id") {
- remove_configs = [ "$dir_pw_build:strict_warnings" ]
- public_configs = [ ":public_include_dirs" ]
+ public_configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/gn:disable_warnings",
+ ]
public_deps = [ "${PICO_ROOT}/src/common/pico_base" ]
deps = [ "${PICO_ROOT}/src/rp2_common/hardware_flash" ]
public = [ "${_CWD}/include/pico/unique_id.h" ]
diff --git a/third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn b/third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn
index 32680d51a..fa1ee3360 100644
--- a/third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn
+++ b/third_party/pico_sdk/src/rp2_common/tinyusb/BUILD.gn
@@ -12,4 +12,98 @@
# License for the specific language governing permissions and limitations under
# the License.
-# TODO(amontanez): Build shim for TinyUSB
+import("//build_overrides/pi_pico.gni")
+import("//build_overrides/pigweed.gni")
+
+import("$dir_pw_build/target_types.gni")
+
+config("public_include_dirs") {
+ include_dirs = [ "${PICO_SRC_DIR}/lib/tinyusb/src" ]
+}
+
+config("tinyusb_defines") {
+ defines = [
+ "CFG_TUSB_DEBUG=0",
+ "CFG_TUSB_MCU=OPT_MCU_RP2040",
+ "CFG_TUSB_OS=OPT_OS_PICO",
+ ]
+}
+
+config("silence_errors") {
+ cflags = [
+ "-Wno-cast-qual",
+ "-Wno-strict-prototypes",
+ ]
+}
+
+# This creates a circular dependency on pico_stdlib, so we have to be messy
+# about grabbing the right dependencies.
+pw_source_set("bsp") {
+ remove_configs = [ "$dir_pw_build:strict_warnings" ]
+ configs = [
+ ":public_include_dirs",
+ "${PICO_ROOT}/src/common/pico_stdlib:public_include_dirs",
+ "${PICO_ROOT}/src/rp2_common/pico_stdio_usb:public_include_dirs",
+ ]
+ include_dirs = [ "${PICO_SRC_DIR}/lib/tinyusb/hw" ]
+ deps = [
+ "${PICO_ROOT}/src/common/pico_base",
+ "${PICO_ROOT}/src/common/pico_binary_info",
+ "${PICO_ROOT}/src/common/pico_time",
+ "${PICO_ROOT}/src/rp2_common/hardware_gpio",
+ "${PICO_ROOT}/src/rp2_common/hardware_sync",
+ "${PICO_ROOT}/src/rp2_common/hardware_uart",
+ "${PICO_ROOT}/src/rp2_common/pico_stdio:headers",
+ ]
+ sources = [
+ "${PICO_SRC_DIR}/lib/tinyusb/hw/bsp/board.h",
+ "${PICO_SRC_DIR}/lib/tinyusb/hw/bsp/rp2040/family.c",
+ ]
+ visibility = [
+ ":tinyusb",
+ "${PICO_ROOT}/src/common/pico_stdlib:headers",
+ ]
+}
+
+pw_source_set("tinyusb") {
+ configs = [
+ "${PICO_ROOT}/gn:disable_warnings",
+ ":silence_errors",
+ ]
+ public_configs = [
+ ":public_include_dirs",
+ ":tinyusb_defines",
+ ]
+ public_deps = [
+ "${PICO_ROOT}/src/common/pico_sync",
+ "${PICO_ROOT}/src/common/pico_time:headers",
+ "${PICO_ROOT}/src/rp2_common/hardware_irq",
+ "${PICO_ROOT}/src/rp2_common/hardware_resets",
+ "${PICO_ROOT}/src/rp2_common/pico_fix/rp2040_usb_device_enumeration",
+ "${PICO_ROOT}/src/rp2_common/pico_stdio:headers",
+ "${PICO_ROOT}/src/rp2_common/pico_stdio_usb:tusb_config",
+ ]
+ deps = [ ":bsp" ]
+ public = [ "${PICO_SRC_DIR}/lib/tinyusb/src/tusb.h" ]
+ sources = [
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/audio/audio_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/cdc/cdc_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/dfu/dfu_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/dfu/dfu_rt_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/hid/hid_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/midi/midi_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/msc/msc_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/net/ecm_rndis_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/net/ncm_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/usbtmc/usbtmc_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/vendor/vendor_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/class/video/video_device.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/common/tusb_fifo.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/device/usbd.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/device/usbd_control.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/portable/raspberrypi/rp2040/dcd_rp2040.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/portable/raspberrypi/rp2040/rp2040_usb.c",
+ "${PICO_SRC_DIR}/lib/tinyusb/src/tusb.c",
+ ]
+ allow_circular_includes_from = [ ":bsp" ]
+}
diff --git a/third_party/rules_proto_grpc/OWNERS b/third_party/rules_proto_grpc/OWNERS
new file mode 100644
index 000000000..68efc833c
--- /dev/null
+++ b/third_party/rules_proto_grpc/OWNERS
@@ -0,0 +1 @@
+tpudlik@google.com
diff --git a/third_party/rules_proto_grpc/internal_proto.bzl b/third_party/rules_proto_grpc/internal_proto.bzl
index cab19d405..cf997aa7a 100644
--- a/third_party/rules_proto_grpc/internal_proto.bzl
+++ b/third_party/rules_proto_grpc/internal_proto.bzl
@@ -34,6 +34,109 @@ load(
)
load("@rules_proto_grpc//internal:filter_files.bzl", "filter_files")
+def pwpb_proto_library(name, deps, **kwargs):
+ """A C++ proto library generated using pw_protobuf.
+
+ Attributes:
+ deps: proto_library targets for which to generate this library.
+ """
+ _pw_proto_library(
+ name = name,
+ compiler = pwpb_compile,
+ deps = deps,
+ cc_deps = ["@pigweed//pw_protobuf"],
+ has_srcs = False,
+ extra_tags = [],
+ **kwargs
+ )
+
+def pwpb_rpc_proto_library(name, deps, pwpb_proto_library_deps, **kwargs):
+ """A pwpb_rpc proto library target.
+
+ Attributes:
+ deps: proto_library targets for which to generate this library.
+ pwpb_proto_library_deps: A pwpb_proto_library generated
+ from the same proto_library. Required.
+ """
+ _pw_proto_library(
+ name = name,
+ compiler = pwpb_rpc_compile,
+ deps = deps,
+ cc_deps = [
+ "@pigweed//pw_protobuf",
+ "@pigweed//pw_rpc/pwpb:server_api",
+ "@pigweed//pw_rpc/pwpb:client_api",
+ "@pigweed//pw_rpc",
+ ] + pwpb_proto_library_deps,
+ has_srcs = False,
+ extra_tags = [],
+ **kwargs
+ )
+
+def raw_rpc_proto_library(name, deps, **kwargs):
+ """A raw C++ RPC proto library."""
+ _pw_proto_library(
+ name = name,
+ compiler = raw_rpc_compile,
+ deps = deps,
+ cc_deps = [
+ "@pigweed//pw_rpc",
+ "@pigweed//pw_rpc/raw:server_api",
+ "@pigweed//pw_rpc/raw:client_api",
+ ],
+ has_srcs = False,
+ extra_tags = [],
+ **kwargs
+ )
+
+def nanopb_proto_library(name, deps, **kwargs):
+ """A C++ proto library generated using nanopb."""
+ _pw_proto_library(
+ name = name,
+ compiler = nanopb_compile,
+ deps = deps,
+ cc_deps = [
+ "@com_github_nanopb_nanopb//:nanopb",
+ ],
+ has_srcs = True,
+ # TODO(tpudlik): Find a way to get Nanopb to generate nested structs.
+ # Otherwise add the manual tag to the resulting library, preventing it
+ # from being built unless directly depended on. e.g. The 'Pigweed'
+ # message in
+ # pw_protobuf/pw_protobuf_test_protos/full_test.proto will fail to
+ # compile as it has a self referring nested message. According to
+ # the docs
+ # https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
+ # and https://github.com/nanopb/nanopb/issues/433 it seams like it
+ # should be possible to configure nanopb to generate nested structs.
+ extra_tags = ["manual"],
+ **kwargs
+ )
+
+def nanopb_rpc_proto_library(name, deps, nanopb_proto_library_deps, **kwargs):
+ """A C++ RPC proto library using nanopb.
+
+ Attributes:
+ deps: proto_library targets for which to generate this library.
+ nanopb_proto_library_deps: A pw_nanopb_cc_library generated
+ from the same proto_library. Required.
+ """
+ _pw_proto_library(
+ name = name,
+ compiler = nanopb_rpc_compile,
+ deps = deps,
+ cc_deps = [
+ "@com_github_nanopb_nanopb//:nanopb",
+ "@pigweed//pw_rpc/nanopb:server_api",
+ "@pigweed//pw_rpc/nanopb:client_api",
+ "@pigweed//pw_rpc",
+ ] + nanopb_proto_library_deps,
+ has_srcs = True,
+ # TODO(tpudlik): See nanopb_proto_library.
+ extra_tags = ["manual"],
+ **kwargs
+ )
+
# Create compile rule
def _proto_compiler_aspect(plugin_group, prefix):
return aspect(
@@ -96,6 +199,21 @@ pwpb_compile = _proto_compiler_rule(
pwpb_compile_aspect,
)
+pwpb_rpc_compile_aspect = _proto_compiler_aspect(
+ [
+ Label("@pigweed//pw_rpc:pw_cc_plugin_pwpb_rpc"),
+ Label("@pigweed//pw_protobuf:pw_cc_plugin"),
+ ],
+ "pwpb_rpc_proto_compile_aspect",
+)
+pwpb_rpc_compile = _proto_compiler_rule(
+ [
+ Label("@pigweed//pw_rpc:pw_cc_plugin_pwpb_rpc"),
+ Label("@pigweed//pw_protobuf:pw_cc_plugin"),
+ ],
+ pwpb_rpc_compile_aspect,
+)
+
raw_rpc_compile_aspect = _proto_compiler_aspect(
[Label("@pigweed//pw_rpc:pw_cc_plugin_raw")],
"raw_rpc_proto_compile_aspect",
@@ -120,105 +238,94 @@ nanopb_rpc_compile = _proto_compiler_rule(
nanopb_rpc_compile_aspect,
)
-PLUGIN_INFO = {
- "nanopb": {
- "compiler": nanopb_compile,
- "deps": ["@com_github_nanopb_nanopb//:nanopb"],
- "has_srcs": True,
- # TODO: Find a way to get Nanopb to generate nested structs.
- # Otherwise add the manual tag to the resulting library,
- # preventing it from being built unless directly depended on.
- # e.g. The 'Pigweed' message in
- # pw_protobuf/pw_protobuf_test_protos/full_test.proto will fail to
- # compile as it has a self referring nested message. According to
- # the docs
- # https://jpa.kapsi.fi/nanopb/docs/reference.html#proto-file-options
- # and https://github.com/nanopb/nanopb/issues/433 it seams like it
- # should be possible to configure nanopb to generate nested structs.
- "additional_tags": ["manual"],
- },
- "nanopb_rpc": {
- "compiler": nanopb_rpc_compile,
- "deps": [
- "@com_github_nanopb_nanopb//:nanopb",
- "@pigweed//pw_rpc/nanopb:server_api",
- "@pigweed//pw_rpc/nanopb:client_api",
- "@pigweed//pw_rpc",
- ],
- "has_srcs": True,
- # See above todo.
- "additional_tags": ["manual"],
- },
- "pwpb": {
- "compiler": pwpb_compile,
- "deps": ["@pigweed//pw_protobuf"],
- "has_srcs": False,
- "additional_tags": [],
- },
- "raw_rpc": {
- "compiler": raw_rpc_compile,
- "deps": [
- "@pigweed//pw_rpc",
- "@pigweed//pw_rpc/raw:server_api",
- "@pigweed//pw_rpc/raw:client_api",
- ],
- "has_srcs": False,
- "additional_tags": [],
- },
-}
+def _pw_proto_library(name, compiler, deps, cc_deps, has_srcs, extra_tags, **kwargs):
+ name_pb = name + ".pb"
+ additional_tags = [
+ tag
+ for tag in extra_tags
+ if tag not in kwargs.get("tags", [])
+ ]
+ compiler(
+ name = name_pb,
+ tags = additional_tags,
+ # Forward deps and verbose tags to implementation
+ verbose = kwargs.get("verbose", 0),
+ deps = deps,
+ protos = kwargs.get("protos", []),
+ )
+
+ # Filter files to sources and headers
+ filter_files(
+ name = name_pb + "_srcs",
+ target = name_pb,
+ extensions = ["c", "cc", "cpp", "cxx"],
+ tags = additional_tags,
+ )
+
+ filter_files(
+ name = name_pb + "_hdrs",
+ target = name_pb,
+ extensions = ["h"],
+ tags = additional_tags,
+ )
+
+ # Cannot use pw_cc_library here as it will add cxxopts.
+ # Note that the srcs attribute here is passed in as a DefaultInfo
+ # object, which is not supported by pw_cc_library.
+ native.cc_library(
+ name = name,
+ hdrs = [name_pb + "_hdrs"],
+ includes = [name_pb],
+ alwayslink = kwargs.get("alwayslink"),
+ copts = kwargs.get("copts", []),
+ defines = kwargs.get("defines", []),
+ srcs = [name_pb + "_srcs"] if has_srcs else [],
+ deps = cc_deps,
+ linkopts = kwargs.get("linkopts", []),
+ linkstatic = kwargs.get("linkstatic", True),
+ local_defines = kwargs.get("local_defines", []),
+ nocopts = kwargs.get("nocopts", ""),
+ visibility = kwargs.get("visibility"),
+ tags = kwargs.get("tags", []) + additional_tags,
+ )
def pw_proto_library(name, **kwargs): # buildifier: disable=function-docstring
- for plugin_name, info in PLUGIN_INFO.items():
- name_pb = name + "_pb" + "." + plugin_name
- additional_tags = [
- tag
- for tag in info["additional_tags"]
- if tag not in kwargs.get("tags", [])
- ]
- info["compiler"](
- name = name_pb,
- tags = additional_tags,
- # Forward deps and verbose tags to implementation
- verbose = kwargs.get("verbose", 0),
- deps = kwargs.get("deps", []),
- protos = kwargs.get("protos", []),
- )
-
- # Filter files to sources and headers
- filter_files(
- name = name_pb + "_srcs",
- target = name_pb,
- extensions = ["c", "cc", "cpp", "cxx"],
- tags = additional_tags,
- )
-
- filter_files(
- name = name_pb + "_hdrs",
- target = name_pb,
- extensions = ["h"],
- tags = additional_tags,
- )
-
- # Cannot use pw_cc_library here as it will add cxxopts.
- # Note that the srcs attribute here is passed in as a DefaultInfo
- # object, which is not supported by pw_cc_library.
- native.cc_library(
- name = name + "." + plugin_name,
- hdrs = [name_pb + "_hdrs"],
- includes = [name_pb],
- alwayslink = kwargs.get("alwayslink"),
- copts = kwargs.get("copts", []),
- defines = kwargs.get("defines", []),
- srcs = [name_pb + "_srcs"] if info["has_srcs"] else [],
- deps = info["deps"],
- include_prefix = kwargs.get("include_prefix", ""),
- linkopts = kwargs.get("linkopts", []),
- linkstatic = kwargs.get("linkstatic", True),
- local_defines = kwargs.get("local_defines", []),
- nocopts = kwargs.get("nocopts", ""),
- visibility = kwargs.get("visibility"),
- tags = kwargs.get("tags", []) + additional_tags,
- )
+ """Generate all the Pigweed proto library targets.
+
+ Args:
+ name: Name of this proto library.
+ **kwargs: Forwarded to wrapped native.cc_library.
+
+ Deprecated: This macro is deprecated and will be removed in a future
+ Pigweed version. Please use one of the single-target macros above.
+ """
+
+ pwpb_proto_library(
+ name = name + ".pwpb",
+ **kwargs
+ )
+
+ pwpb_rpc_proto_library(
+ name = name + ".pwpb_rpc",
+ pwpb_proto_library_deps = [name + ".pwpb"],
+ **kwargs
+ )
+
+ raw_rpc_proto_library(
+ name = name + ".raw_rpc",
+ **kwargs
+ )
+
+ nanopb_proto_library(
+ name = name + ".nanopb",
+ **kwargs
+ )
+
+ nanopb_rpc_proto_library(
+ name = name + ".nanopb_rpc",
+ nanopb_proto_library_deps = [name + ".nanopb"],
+ **kwargs
+ )
if "manual" in kwargs.get("tags", []):
additional_tags = []
@@ -230,8 +337,12 @@ def pw_proto_library(name, **kwargs): # buildifier: disable=function-docstring
name = name,
deps = [
name + "." + plugin_name
- for plugin_name in PLUGIN_INFO.keys()
+ for plugin_name in ["pwpb", "pwpb_rpc", "raw_rpc", "nanopb", "nanopb_rpc"]
],
tags = kwargs.get("tags", []) + additional_tags,
- **{k: v for k, v in kwargs.items() if k not in ["deps", "protos"]}
+ **{
+ k: v
+ for k, v in kwargs.items()
+ if k not in ["deps", "protos", "tags"]
+ }
)
diff --git a/third_party/smartfusion_mss/OWNERS b/third_party/smartfusion_mss/OWNERS
new file mode 100644
index 000000000..8c230af99
--- /dev/null
+++ b/third_party/smartfusion_mss/OWNERS
@@ -0,0 +1 @@
+skeys@google.com
diff --git a/third_party/smartfusion_mss/README.md b/third_party/smartfusion_mss/README.md
index 7532545e2..60de6f0ab 100644
--- a/third_party/smartfusion_mss/README.md
+++ b/third_party/smartfusion_mss/README.md
@@ -1,6 +1,5 @@
# LiberoSoC Library
The folder provides build scripts and configuration recipes for building
-the SmartFusion2 Microcontroller Subsystem library. The source code needs to be downloaded by the user, or
-via the support in pw_package "pw package install sf2mss". For gn build,
+the SmartFusion2 Microcontroller Subsystem library. The source code needs to be downloaded by the user, preferably via Git submodules. For gn build,
set `dir_pw_third_party_smartfusion_mss` to point to the path of the source code.
diff --git a/third_party/stm32cube/BUILD.gn b/third_party/stm32cube/BUILD.gn
index c541fc859..da8c1373e 100644
--- a/third_party/stm32cube/BUILD.gn
+++ b/third_party/stm32cube/BUILD.gn
@@ -122,11 +122,21 @@ if (dir_pw_third_party_stm32cube == "") {
cflags_c = [
"-Wno-redundant-decls",
"-Wno-sign-compare",
- "-Wno-old-style-declaration",
- "-Wno-maybe-uninitialized",
"-Wno-undef",
"-Wno-implicit-function-declaration",
+ "-Wno-switch-enum",
]
+ if (get_path_info(pw_toolchain_SCOPE.cc, "file") == "clang") {
+ cflags += [ "-Wno-deprecated-volatile" ]
+ cflags_c += [ "-Wno-parentheses-equality" ]
+ } else {
+ cflags_c += [
+ "-Wno-old-style-declaration",
+ "-Wno-maybe-uninitialized",
+ ]
+ cflags_cc = [ "-Wno-volatile" ]
+ }
+
defines = [
"USE_HAL_DRIVER",
files.product_define,
diff --git a/third_party/stm32cube/stm32cube.gni b/third_party/stm32cube/stm32cube.gni
index f1848df71..98ccce506 100644
--- a/third_party/stm32cube/stm32cube.gni
+++ b/third_party/stm32cube/stm32cube.gni
@@ -15,7 +15,7 @@
import("//build_overrides/pigweed.gni")
declare_args() {
- # pw_package/stm32cube_xx install directories
+ # stm32cube_xx install directories
dir_pw_third_party_stm32cube_f0 = ""
dir_pw_third_party_stm32cube_f1 = ""
dir_pw_third_party_stm32cube_f2 = ""
diff --git a/ts/buildprotos.ts b/ts/buildprotos.ts
new file mode 100644
index 000000000..5f76e996d
--- /dev/null
+++ b/ts/buildprotos.ts
@@ -0,0 +1,81 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {exec, ExecException} from 'child_process';
+import fs from 'fs';
+
+const run = function (executable: string, args: string[]) {
+ console.log(args)
+ return new Promise<void>(resolve => {
+ exec(`${executable} ${args.join(" ")}`, {cwd: process.cwd()}, (error: ExecException | null, stdout: string | Buffer) => {
+ if (error) {
+ throw error;
+ }
+
+ console.log(stdout);
+ resolve();
+ });
+ });
+};
+
+const protos = [
+ 'pw_transfer/transfer.proto',
+ 'pw_rpc/ts/test.proto',
+ 'pw_rpc/echo.proto',
+ 'pw_protobuf/pw_protobuf_protos/status.proto',
+ 'pw_protobuf/pw_protobuf_protos/common.proto',
+ 'pw_tokenizer/options.proto',
+ 'pw_log/log.proto',
+ 'pw_rpc/ts/test2.proto',
+ 'pw_rpc/internal/packet.proto',
+ 'pw_protobuf_compiler/pw_protobuf_compiler_protos/nested/more_nesting/test.proto',
+ 'pw_protobuf_compiler/pw_protobuf_compiler_protos/test.proto'
+];
+
+// Replace these import statements so they are actual paths to proto files.
+const remapImports = {
+ 'pw_protobuf_protos/common.proto': 'pw_protobuf/pw_protobuf_protos/common.proto',
+ 'pw_tokenizer/proto/options.proto': 'pw_tokenizer/options.proto'
+}
+
+// Only modify the .proto files when running this builder and then restore any
+// modified .proto files to their original states after the builder has finished
+// running.
+let restoreProtoList = [];
+protos.forEach((protoPath) => {
+ const protoData = fs.readFileSync(protoPath, 'utf-8');
+ let newProtoData = protoData;
+ Object.keys(remapImports).forEach((remapImportFrom) => {
+ if (protoData.indexOf(`import "${remapImportFrom}"`) !== -1) {
+ newProtoData = newProtoData
+ .replaceAll(remapImportFrom, remapImports[remapImportFrom]);
+ }
+ });
+ if (protoData !== newProtoData) {
+ restoreProtoList.push([protoPath, protoData]);
+ fs.writeFileSync(protoPath, newProtoData);
+ }
+});
+
+run('ts-node', [
+ `./pw_protobuf_compiler/ts/build.ts`,
+ `--out dist/protos`
+].concat(
+ protos.map(proto => `-p ${proto}`)
+))
+ .then(() => {
+ restoreProtoList.forEach((restoreProtoData) => {
+ fs.writeFileSync(restoreProtoData[0], restoreProtoData[1]);
+ });
+ });
diff --git a/ts/device/index.ts b/ts/device/index.ts
new file mode 100644
index 000000000..325007039
--- /dev/null
+++ b/ts/device/index.ts
@@ -0,0 +1,197 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import objectPath from 'object-path';
+import {Decoder, Encoder} from 'pigweedjs/pw_hdlc';
+import {
+ Client,
+ Channel,
+ ServiceClient,
+ UnaryMethodStub,
+ MethodStub,
+ ServerStreamingMethodStub
+} from 'pigweedjs/pw_rpc';
+import {WebSerialTransport} from '../transport/web_serial_transport';
+import {ProtoCollection} from 'pigweedjs/pw_protobuf_compiler';
+
+function protoFieldToMethodName(string) {
+ return string.split("_").map(titleCase).join("");
+}
+function titleCase(string) {
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+export class Device {
+ private protoCollection: ProtoCollection;
+ private transport: WebSerialTransport;
+ private decoder: Decoder;
+ private encoder: Encoder;
+ private rpcAddress: number;
+ private nameToMethodArgumentsMap: any;
+ client: Client;
+ rpcs: any
+
+ constructor(
+ protoCollection: ProtoCollection,
+ transport: WebSerialTransport = new WebSerialTransport(),
+ rpcAddress: number = 82) {
+ this.transport = transport;
+ this.rpcAddress = rpcAddress;
+ this.protoCollection = protoCollection;
+ this.decoder = new Decoder();
+ this.encoder = new Encoder();
+ this.nameToMethodArgumentsMap = {};
+ const channels = [
+ new Channel(1, (bytes) => {
+ const hdlcBytes = this.encoder.uiFrame(this.rpcAddress, bytes);
+ this.transport.sendChunk(hdlcBytes);
+ })];
+ this.client =
+ Client.fromProtoSet(channels, this.protoCollection);
+
+ this.setupRpcs();
+ }
+
+ async connect() {
+ await this.transport.connect();
+ this.transport.chunks.subscribe((item) => {
+ const decoded = this.decoder.process(item);
+ for (const frame of decoded) {
+ if (frame.address === this.rpcAddress) {
+ this.client.processPacket(frame.data);
+ }
+ }
+ });
+ }
+
+ getMethodArguments(fullPath) {
+ return this.nameToMethodArgumentsMap[fullPath];
+ }
+
+ private setupRpcs() {
+ let rpcMap = {};
+ let channel = this.client.channel();
+ let servicesKeys = Array.from(channel.services.keys());
+ servicesKeys.forEach((serviceKey) => {
+ objectPath.set(rpcMap, serviceKey,
+ this.mapServiceMethods(channel.services.get(serviceKey))
+ );
+ });
+ this.rpcs = rpcMap;
+ }
+
+ private mapServiceMethods(service: ServiceClient) {
+ let methodMap = {};
+ let methodKeys = Array.from(service.methodsByName.keys());
+ methodKeys
+ .filter((method: any) =>
+ service.methodsByName.get(method) instanceof UnaryMethodStub
+ || service.methodsByName.get(method) instanceof ServerStreamingMethodStub)
+ .forEach(key => {
+ let fn = this.createMethodWrapper(
+ service.methodsByName.get(key),
+ key,
+ `${service.name}.${key}`
+ );
+ methodMap[key] = fn;
+ });
+ return methodMap;
+ }
+
+ private createMethodWrapper(
+ realMethod: MethodStub,
+ methodName: string,
+ fullMethodPath: string) {
+ if (realMethod instanceof UnaryMethodStub) {
+ return this.createUnaryMethodWrapper(
+ realMethod,
+ methodName,
+ fullMethodPath);
+ }
+ else if (realMethod instanceof ServerStreamingMethodStub) {
+ return this.createServerStreamingMethodWrapper(
+ realMethod,
+ methodName,
+ fullMethodPath);
+ }
+ }
+
+ private createUnaryMethodWrapper(
+ realMethod: UnaryMethodStub,
+ methodName: string,
+ fullMethodPath: string) {
+ const requestType =
+ realMethod.method.descriptor.getInputType().replace(/^\./, '');
+ const requestProtoDescriptor =
+ this.protoCollection.getDescriptorProto(requestType);
+ const requestFields = requestProtoDescriptor.getFieldList();
+ const functionArguments = requestFields
+ .map(field => field.getName())
+ .concat(
+ 'return this(arguments);'
+ );
+
+ // We store field names so REPL can show hints in autocomplete using these.
+ this.nameToMethodArgumentsMap[fullMethodPath] = requestFields
+ .map(field => field.getName());
+
+ // We create a new JS function dynamically here that takes
+ // proto message fields as arguments and calls the actual RPC method.
+ let fn = new Function(...functionArguments).bind((args) => {
+ const request = new realMethod.method.requestType();
+ requestFields.forEach((field, index) => {
+ request[`set${titleCase(field.getName())}`](args[index]);
+ })
+ return realMethod.call(request);
+ });
+ return fn;
+ }
+
+ private createServerStreamingMethodWrapper(
+ realMethod: ServerStreamingMethodStub,
+ methodName: string,
+ fullMethodPath: string) {
+ const requestType = realMethod.method.descriptor.getInputType().replace(/^\./, '');
+ const requestProtoDescriptor =
+ this.protoCollection.getDescriptorProto(requestType);
+ const requestFields = requestProtoDescriptor.getFieldList();
+ const functionArguments = requestFields
+ .map(field => field.getName())
+ .concat(
+ [
+ 'onNext',
+ 'onComplete',
+ 'onError',
+ 'return this(arguments);'
+ ]
+ );
+
+ // We store field names so REPL can show hints in autocomplete using these.
+ this.nameToMethodArgumentsMap[fullMethodPath] = requestFields
+ .map(field => field.getName());
+
+ // We create a new JS function dynamically here that takes
+ // proto message fields as arguments and calls the actual RPC method.
+ let fn = new Function(...functionArguments).bind((args) => {
+ const request = new realMethod.method.requestType();
+ requestFields.forEach((field, index) => {
+ request[`set${protoFieldToMethodName(field.getName())}`](args[index]);
+ })
+ const callbacks = Array.from(args).slice(requestFields.length);
+ // @ts-ignore
+ return realMethod.invoke(request, callbacks[0], callbacks[1], callbacks[2]);
+ });
+ return fn;
+ }
+}
diff --git a/ts/device/index_test.ts b/ts/device/index_test.ts
new file mode 100644
index 000000000..3a383bfc3
--- /dev/null
+++ b/ts/device/index_test.ts
@@ -0,0 +1,126 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+/* eslint-env browser */
+import {SerialMock} from '../transport/serial_mock';
+import {Device} from "./"
+import {ProtoCollection} from 'pigweedjs/protos/collection';
+import {WebSerialTransport} from '../transport/web_serial_transport';
+import {Serial} from 'pigweedjs/types/serial';
+import {Message} from 'google-protobuf';
+import {RpcPacket, PacketType} from 'pigweedjs/protos/pw_rpc/internal/packet_pb';
+import {Method, ServerStreamingMethodStub} from 'pigweedjs/pw_rpc';
+import {Status} from 'pigweedjs/pw_status';
+import {
+ Response,
+} from 'pigweedjs/protos/pw_rpc/ts/test_pb';
+
+describe('WebSerialTransport', () => {
+ let device: Device;
+ let serialMock: SerialMock;
+
+ function newResponse(payload = '._.'): Message {
+ const response = new Response();
+ response.setPayload(payload);
+ return response;
+ }
+
+ function generateResponsePacket(
+ channelId: number,
+ method: Method,
+ status: Status,
+ response?: Message
+ ) {
+ const packet = new RpcPacket();
+ packet.setType(PacketType.RESPONSE);
+ packet.setChannelId(channelId);
+ packet.setServiceId(method.service.id);
+ packet.setMethodId(method.id);
+ packet.setStatus(status);
+ if (response === undefined) {
+ packet.setPayload(new Uint8Array());
+ } else {
+ packet.setPayload(response.serializeBinary());
+ }
+ return packet.serializeBinary();
+ }
+
+ function generateStreamingPacket(
+ channelId: number,
+ method: Method,
+ response: Message,
+ status: Status = Status.OK
+ ) {
+ const packet = new RpcPacket();
+ packet.setType(PacketType.SERVER_STREAM);
+ packet.setChannelId(channelId);
+ packet.setServiceId(method.service.id);
+ packet.setMethodId(method.id);
+ packet.setPayload(response.serializeBinary());
+ packet.setStatus(status);
+ return packet.serializeBinary();
+ }
+
+ beforeEach(() => {
+ serialMock = new SerialMock();
+ device = new Device(new ProtoCollection(), new WebSerialTransport(serialMock as Serial));
+ });
+
+ it('has rpcs defined', () => {
+ expect(device.rpcs).toBeDefined();
+ expect(device.rpcs.pw.rpc.EchoService.Echo).toBeDefined();
+ });
+
+ it('has method arguments data', () => {
+ expect(device.getMethodArguments("pw.rpc.EchoService.Echo")).toStrictEqual(["msg"]);
+ expect(device.getMethodArguments("pw.test2.Alpha.Unary")).toStrictEqual(['magic_number']);
+ });
+
+ it('unary rpc sends request to serial', async () => {
+ const helloResponse = new Uint8Array([
+ 126, 165, 3, 42, 7, 10, 5, 104,
+ 101, 108, 108, 111, 8, 1, 16, 1,
+ 29, 82, 208, 251, 20, 37, 233, 14,
+ 71, 139, 109, 127, 108, 165, 126]);
+
+ await device.connect();
+ serialMock.dataFromDevice(helloResponse);
+ const [status, response] = await device.rpcs.pw.rpc.EchoService.Echo("hello");
+ expect(response.getMsg()).toBe("hello");
+ expect(status).toBe(0);
+ });
+
+ it('server streaming rpc sends response', async () => {
+ await device.connect();
+ const response1 = newResponse('!!!');
+ const response2 = newResponse('?');
+ const serverStreaming = device.client
+ .channel()
+ ?.methodStub(
+ 'pw.rpc.test1.TheTestService.SomeServerStreaming'
+ )! as ServerStreamingMethodStub;
+ const onNext = jest.fn();
+ const onCompleted = jest.fn();
+ const onError = jest.fn();
+
+ device.rpcs.pw.rpc.test1.TheTestService.SomeServerStreaming(4, onNext, onCompleted, onError);
+ device.client.processPacket(generateStreamingPacket(1, serverStreaming.method, response1));
+ device.client.processPacket(generateStreamingPacket(1, serverStreaming.method, response2));
+ device.client.processPacket(generateResponsePacket(1, serverStreaming.method, Status.ABORTED));
+
+ expect(onNext).toBeCalledWith(response1);
+ expect(onNext).toBeCalledWith(response2);
+ expect(onCompleted).toBeCalledWith(Status.ABORTED);
+ });
+});
diff --git a/ts/index.ts b/ts/index.ts
new file mode 100644
index 000000000..9b0b02dc7
--- /dev/null
+++ b/ts/index.ts
@@ -0,0 +1,22 @@
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+export * as pw_hdlc from "../pw_hdlc/ts";
+export * as pw_rpc from "../pw_rpc/ts";
+export * as pw_status from "../pw_status/ts";
+export * as pw_tokenizer from "../pw_tokenizer/ts";
+export * as pw_protobuf_compiler from "../pw_protobuf_compiler/ts";
+export * as pw_transfer from "../pw_transfer/ts";
+export * as WebSerial from "./transport/web_serial_transport";
+export {Device} from "./device";
diff --git a/ts/index_test.ts b/ts/index_test.ts
new file mode 100644
index 000000000..9019842b2
--- /dev/null
+++ b/ts/index_test.ts
@@ -0,0 +1,76 @@
+/**
+ * @jest-environment jsdom
+ */
+
+// Copyright 2022 The Pigweed Authors
+//
+// 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
+//
+// https://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.
+
+import {
+ pw_status,
+ pw_hdlc,
+ pw_rpc,
+ pw_tokenizer,
+ pw_transfer,
+ WebSerial
+} from "../dist/index.umd";
+
+import {ProtoCollection} from "../dist/protos/collection.umd";
+import * as fs from "fs";
+
+describe('Pigweed Bundle', () => {
+
+ it('proto collection has file list', () => {
+ const protoCollection = new ProtoCollection();
+ const fd = protoCollection.fileDescriptorSet.getFileList();
+ expect(fd.length).toBeGreaterThan(0);
+ });
+
+ it('has pw_status enum defined', () => {
+ const Status = pw_status.Status;
+ expect(Status[Status.OUT_OF_RANGE]).toBeDefined();
+ });
+
+ it('has pw_hdlc frame, frame status, decoder and encoder defined', () => {
+ expect(pw_hdlc.Frame).toBeDefined();
+ expect(pw_hdlc.FrameStatus).toBeDefined();
+ expect(pw_hdlc.Decoder).toBeDefined();
+ expect(pw_hdlc.Encoder).toBeDefined();
+ });
+
+ it('has pw_rpc defined', () => {
+ expect(pw_rpc.Client).toBeDefined();
+ expect(pw_rpc.Rpc).toBeDefined();
+ expect(pw_rpc.Channel).toBeDefined();
+ });
+
+ it('has pw_tokenizer defined', () => {
+ expect(pw_tokenizer.Detokenizer).toBeDefined();
+ expect(pw_tokenizer.PrintfDecoder).toBeDefined();
+ });
+
+ it('has pw_transfer defined', () => {
+ expect(pw_transfer.Manager).toBeDefined();
+ });
+
+ it('has WebSerialTransport defined', () => {
+ expect(WebSerial.WebSerialTransport).toBeDefined();
+ });
+
+ it('is not referring to any outside Pigweed modules', () => {
+ const requireString = "require('pigweedjs";
+ const file = fs.readFileSync(require.resolve("../dist/index.umd"));
+ expect(file.indexOf(requireString)).toBe(-1)
+ });
+
+});
diff --git a/pw_web_ui/src/transport/device_transport.ts b/ts/transport/device_transport.ts
index ee2df8720..ee2df8720 100644
--- a/pw_web_ui/src/transport/device_transport.ts
+++ b/ts/transport/device_transport.ts
diff --git a/pw_web_ui/src/transport/serial_mock.ts b/ts/transport/serial_mock.ts
index fe2a9bcdc..8fb48c4da 100644
--- a/pw_web_ui/src/transport/serial_mock.ts
+++ b/ts/transport/serial_mock.ts
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,9 +12,9 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
+/* eslint-env browser */
import {Subject} from 'rxjs';
-
+import type {SerialConnectionEvent, SerialPort, Serial, SerialPortRequestOptions, SerialOptions} from "pigweedjs/types/serial"
/**
* AsyncQueue is a queue that allows values to be dequeued
* before they are enqueued, returning a promise that resolves
@@ -56,7 +56,7 @@ class AsyncQueue<T> {
/**
* SerialPortMock is a mock for Chrome's upcoming SerialPort interface.
- * Since pw_web_ui only depends on a subset of the interface, this mock
+ * Since pw_web only depends on a subset of the interface, this mock
* only implements that subset.
*/
class SerialPortMock implements SerialPort {
@@ -125,12 +125,12 @@ class SerialPortMock implements SerialPort {
/**
* A spy for opening the serial port.
*/
- open = jasmine.createSpy('openSpy', async (options?: SerialOptions) => {});
+ open = jest.fn(async (options?: SerialOptions) => { });
/**
* A spy for closing the serial port.
*/
- close = jasmine.createSpy('closeSpy', () => {});
+ close = jest.fn(() => { });
}
export class SerialMock implements Serial {
diff --git a/pw_web_ui/src/transport/web_serial_transport.ts b/ts/transport/web_serial_transport.ts
index 70dd9fe14..17edc1d3a 100644
--- a/pw_web_ui/src/transport/web_serial_transport.ts
+++ b/ts/transport/web_serial_transport.ts
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -14,8 +14,8 @@
/* eslint-env browser */
import {BehaviorSubject, Observable, Subject, Subscription} from 'rxjs';
-
import DeviceTransport from './device_transport';
+import type {SerialPort, Serial, SerialOptions, Navigator, SerialPortFilter} from "pigweedjs/types/serial"
const DEFAULT_SERIAL_OPTIONS: SerialOptions & {baudRate: number} = {
// Some versions of chrome use `baudrate` (linux)
@@ -59,10 +59,10 @@ export class WebSerialTransport implements DeviceTransport {
private rxSubscriptions: Subscription[] = [];
constructor(
- private serial: Serial = navigator.serial,
+ private serial: Serial = (navigator as unknown as Navigator).serial,
private filters: SerialPortFilter[] = [],
private serialOptions = DEFAULT_SERIAL_OPTIONS
- ) {}
+ ) { }
/**
* Send a UInt8Array chunk of data to the connected device.
diff --git a/pw_web_ui/src/transport/web_serial_transport_test.ts b/ts/transport/web_serial_transport_test.ts
index 689e8fe8f..2ccbd10d6 100644
--- a/pw_web_ui/src/transport/web_serial_transport_test.ts
+++ b/ts/transport/web_serial_transport_test.ts
@@ -1,4 +1,4 @@
-// Copyright 2020 The Pigweed Authors
+// Copyright 2022 The Pigweed Authors
//
// 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
@@ -12,11 +12,12 @@
// License for the specific language governing permissions and limitations under
// the License.
-/* eslint-env browser, jasmine */
+/* eslint-env browser */
import {last, take} from 'rxjs/operators';
import {SerialMock} from './serial_mock';
-import {DeviceLockedError, WebSerialTransport} from './web_serial_transport';
+import {WebSerialTransport, DeviceLockedError} from './web_serial_transport';
+import type {Serial} from "pigweedjs/types/serial"
describe('WebSerialTransport', () => {
let serialMock: SerialMock;
@@ -26,14 +27,14 @@ describe('WebSerialTransport', () => {
it('is disconnected before connecting', () => {
const transport = new WebSerialTransport(serialMock as Serial);
- expect(transport.connected.getValue()).toBeFalse();
+ expect(transport.connected.getValue()).toBe(false);
});
it('reports that it has connected', async () => {
const transport = new WebSerialTransport(serialMock as Serial);
await transport.connect();
expect(serialMock.serialPort.open).toHaveBeenCalled();
- expect(transport.connected.getValue()).toBeTrue();
+ expect(transport.connected.getValue()).toBe(true);
});
it('emits chunks as they arrive from the device', async () => {
@@ -44,9 +45,9 @@ describe('WebSerialTransport', () => {
serialMock.dataFromDevice(data);
expect(await emitted).toEqual(data);
- expect(transport.connected.getValue()).toBeTrue();
- expect(serialMock.serialPort.readable.locked).toBeTrue();
- expect(serialMock.serialPort.writable.locked).toBeTrue();
+ expect(transport.connected.getValue()).toBe(true);
+ expect(serialMock.serialPort.readable.locked).toBe(true);
+ expect(serialMock.serialPort.writable.locked).toBe(true);
});
it('is disconnected when it reaches the final chunk', async () => {
@@ -57,7 +58,7 @@ describe('WebSerialTransport', () => {
.toPromise();
serialMock.closeFromDevice();
- expect(await disconnectPromise).toBeFalse();
+ expect(await disconnectPromise).toBe(false);
});
it('waits for the writer to be ready', async () => {
@@ -91,9 +92,11 @@ describe('WebSerialTransport', () => {
it('throws an error on failing to connect', async () => {
const connectError = new Error('Example connection error');
- spyOn(serialMock, 'requestPort').and.throwError(connectError);
+ const spy = jest.spyOn(serialMock, 'requestPort').mockImplementation(() => {
+ throw connectError;
+ });
const transport = new WebSerialTransport(serialMock as Serial);
- await expectAsync(transport.connect()).toBeRejectedWith(connectError);
+ await expect(transport.connect()).rejects.toThrow(connectError.message);
});
it("emits connection errors in the 'errors' observable", async () => {
diff --git a/pw_web_ui/types/serial.d.ts b/ts/types/serial.d.ts
index 46bbcc777..0e4142c6e 100644
--- a/pw_web_ui/types/serial.d.ts
+++ b/ts/types/serial.d.ts
@@ -1,5 +1,5 @@
/**
- * Copyright 2020 The Pigweed Authors
+ * Copyright 2022 The Pigweed Authors
*
* 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
@@ -102,3 +102,5 @@ interface Navigator {
interface WorkerNavigator {
readonly serial: Serial;
}
+
+export type {Navigator, SerialPortFilter, Serial, SerialOptions, SerialConnectionEvent, SerialPortRequestOptions, SerialPort}
diff --git a/tsconfig.json b/tsconfig.json
index a398e19c0..c73ca8deb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,19 +1,38 @@
{
- "extends": "./node_modules/gts/tsconfig-google.json",
"compilerOptions": {
"lib": [
"dom",
"dom.iterable",
"esnext"
],
- "jsx": "react",
- "plugins": [
- {
- "name": "@bazel/tsetse",
- "disabledRules": [
- "must-use-promises"
- ]
- }
- ]
- }
+ "outDir": "dist",
+ "baseUrl": ".",
+ "esModuleInterop": true,
+ "target": "es6",
+ "declarationDir": "./types",
+ "moduleResolution": "node",
+ "declaration": true,
+ "allowJs": true,
+ "paths": {
+ "pigweedjs/pw_*": [
+ "./pw_*/ts"
+ ],
+ "pigweedjs/protos/*": [
+ "./dist/protos/*"
+ ],
+ "pigweedjs/types/*": [
+ "./ts/types/*"
+ ]
+ }
+ },
+ "include": [
+ "ts",
+ "pw_status/ts",
+ "pw_hdlc/ts",
+ "pw_tokenizer/ts",
+ "pw_protobuf_compiler/ts",
+ "pw_rpc/ts",
+ "pw_rpc/internal",
+ "dist/protos/**/*"
+ ]
}
diff --git a/yarn.lock b/yarn.lock
deleted file mode 100644
index bafa4ded9..000000000
--- a/yarn.lock
+++ /dev/null
@@ -1,5136 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@babel/code-frame@7.12.11":
- version "7.12.11"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
- integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
- dependencies:
- "@babel/highlight" "^7.10.4"
-
-"@babel/code-frame@^7.0.0":
- version "7.15.8"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503"
- integrity sha512-2IAnmn8zbvC/jKYhq5Ki9I+DwjlrtMPUCH/CpHvqI4dNnlwHwsxoIhlc8WcYY5LSYknXQtAlFYuHfqAFCvQ4Wg==
- dependencies:
- "@babel/highlight" "^7.14.5"
-
-"@babel/helper-validator-identifier@^7.14.5":
- version "7.15.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
- integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
-
-"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5":
- version "7.14.5"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9"
- integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==
- dependencies:
- "@babel/helper-validator-identifier" "^7.14.5"
- chalk "^2.0.0"
- js-tokens "^4.0.0"
-
-"@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7":
- version "7.15.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.4.tgz#fd17d16bfdf878e6dd02d19753a39fa8a8d9c84a"
- integrity sha512-99catp6bHCaxr4sJ/DbTGgHS4+Rs2RVd2g7iOap6SLGPDknRK9ztKNsE/Fg6QhSeh1FGE5f6gHGQmvvn3I3xhw==
- dependencies:
- regenerator-runtime "^0.13.4"
-
-"@bazel/concatjs@4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@bazel/concatjs/-/concatjs-4.1.0.tgz#73be7f4a6ab94ba3531a39d3a31e15c50a139800"
- integrity sha512-r9ZM3TxOHPM1OUAxa5NAfAe/bHg9jn7Q0J92HJBv4m+/MGnAJ1ZZPTzxu7w79aUxha/Cr/oqF2DxKAdDbA7Lkg==
- dependencies:
- protobufjs "6.8.8"
- source-map-support "0.5.9"
- tsutils "3.21.0"
-
-"@bazel/esbuild@4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@bazel/esbuild/-/esbuild-4.1.0.tgz#419def12e7d56e095b9d3eb40a66574d7f9b8114"
- integrity sha512-mN1PfDMJlkgxcpMMTKbD6cpalL8SkRo9om7m6bg1olYSwsrAW6QHNa7ohK4xVtnxgQaWDu4KxioJVMlEmebEqw==
-
-"@bazel/ibazel@^0.15.10":
- version "0.15.10"
- resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.15.10.tgz#cf0cff1aec6d8e7bb23e1fc618d09fbd39b7a13f"
- integrity sha512-0v+OwCQ6fsGFa50r6MXWbUkSGuWOoZ22K4pMSdtWiL5LKFIE4kfmMmtQS+M7/ICNwk2EIYob+NRreyi/DGUz5A==
-
-"@bazel/jasmine@4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-4.1.0.tgz#ec9fc5179af265de47aba8bb40a094e9b062aab2"
- integrity sha512-AUKzBZ12qKkcI5apXzL/2VKfsF4tHkdLPNsF/p6gEnIW4/aYb6M9wZOFsUh1MLYds+kqx1zN90EGfiZKa6wbOw==
- dependencies:
- c8 "~7.5.0"
- jasmine-reporters "~2.4.0"
-
-"@bazel/rollup@4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-4.1.0.tgz#63c1ae1043a0245e8f21cf9d59030093db3e9721"
- integrity sha512-EYCbhH8gxGnGT0b/sgjcL5/9MQ2rVkUA6mg9sRJsJPuyyQ9mP0Cbvm0Xy9pqoeu5bFb8aTudQR973Xpcr4LaqQ==
- dependencies:
- "@bazel/worker" "4.1.0"
-
-"@bazel/typescript@4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-4.1.0.tgz#883e793c37df70c4ebc44bc4139f6008b3eb92fa"
- integrity sha512-EfjGIa70IGwkBqOeirrbCRgvC/jue91L5r13c6NErb6JiSAzbKuxyKvZp4isGNPqv5W/LqpLGim5/yUQAmKXww==
- dependencies:
- "@bazel/worker" "4.1.0"
- protobufjs "6.8.8"
- semver "5.6.0"
- source-map-support "0.5.9"
- tsutils "3.21.0"
-
-"@bazel/worker@4.1.0":
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-4.1.0.tgz#f63a5c821ce01731a4bc1b38f7dff16332fe331c"
- integrity sha512-xO8dMC2GR+MwZrRa+FTG9yr5yzUCFvs7MiVYEJ25L708XhOqs34at6BFH1Xa3/ZYQt7iDBFpwY6QzbhhIIQrXA==
- dependencies:
- google-protobuf "^3.6.1"
-
-"@bcoe/v8-coverage@^0.2.3":
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
- integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
-
-"@emotion/hash@^0.8.0":
- version "0.8.0"
- resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
- integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
-
-"@eslint/eslintrc@^0.4.3":
- version "0.4.3"
- resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
- integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==
- dependencies:
- ajv "^6.12.4"
- debug "^4.1.1"
- espree "^7.3.0"
- globals "^13.9.0"
- ignore "^4.0.6"
- import-fresh "^3.2.1"
- js-yaml "^3.13.1"
- minimatch "^3.0.4"
- strip-json-comments "^3.1.1"
-
-"@grpc/grpc-js@^1.3.7":
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.4.1.tgz#799063a4ff7395987d4fceb2aab133629b003840"
- integrity sha512-/chkA48TdAvATHA7RXJPeHQLdfFhpu51974s8htjO/XTDHA41j5+SkR5Io+lr9XsLmkZD6HxLyRAFGmA9wjO2w==
- dependencies:
- "@grpc/proto-loader" "^0.6.4"
- "@types/node" ">=12.12.47"
-
-"@grpc/proto-loader@^0.6.4":
- version "0.6.6"
- resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.6.tgz#d8e51ea808ec5fa54d9defbecbf859336fb2da71"
- integrity sha512-cdMaPZ8AiFz6ua6PUbP+LKbhwJbFXnrQ/mlnKGUyzDUZ3wp7vPLksnmLCBX6SHgSmjX7CbNVNLFYD5GmmjO4GQ==
- dependencies:
- "@types/long" "^4.0.1"
- lodash.camelcase "^4.3.0"
- long "^4.0.0"
- protobufjs "^6.10.0"
- yargs "^16.1.1"
-
-"@humanwhocodes/config-array@^0.5.0":
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
- integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
- dependencies:
- "@humanwhocodes/object-schema" "^1.2.0"
- debug "^4.1.1"
- minimatch "^3.0.4"
-
-"@humanwhocodes/object-schema@^1.2.0":
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
- integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
-
-"@istanbuljs/schema@^0.1.2":
- version "0.1.3"
- resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
- integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
-
-"@mapbox/node-pre-gyp@^1.0.5":
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.6.tgz#f859d601a210537e27530f363028cde56e0cf962"
- integrity sha512-qK1ECws8UxuPqOA8F5LFD90vyVU33W7N3hGfgsOVfrJaRVc8McC3JClTDHpeSbL9CBrOHly/4GsNPAvIgNZE+g==
- dependencies:
- detect-libc "^1.0.3"
- https-proxy-agent "^5.0.0"
- make-dir "^3.1.0"
- node-fetch "^2.6.5"
- nopt "^5.0.0"
- npmlog "^5.0.1"
- rimraf "^3.0.2"
- semver "^7.3.5"
- tar "^6.1.11"
-
-"@material-ui/core@^4.12.1":
- version "4.12.3"
- resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca"
- integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw==
- dependencies:
- "@babel/runtime" "^7.4.4"
- "@material-ui/styles" "^4.11.4"
- "@material-ui/system" "^4.12.1"
- "@material-ui/types" "5.1.0"
- "@material-ui/utils" "^4.11.2"
- "@types/react-transition-group" "^4.2.0"
- clsx "^1.0.4"
- hoist-non-react-statics "^3.3.2"
- popper.js "1.16.1-lts"
- prop-types "^15.7.2"
- react-is "^16.8.0 || ^17.0.0"
- react-transition-group "^4.4.0"
-
-"@material-ui/lab@^4.0.0-alpha.60":
- version "4.0.0-alpha.60"
- resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz#5ad203aed5a8569b0f1753945a21a05efa2234d2"
- integrity sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA==
- dependencies:
- "@babel/runtime" "^7.4.4"
- "@material-ui/utils" "^4.11.2"
- clsx "^1.0.4"
- prop-types "^15.7.2"
- react-is "^16.8.0 || ^17.0.0"
-
-"@material-ui/styles@^4.11.4":
- version "4.11.4"
- resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d"
- integrity sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew==
- dependencies:
- "@babel/runtime" "^7.4.4"
- "@emotion/hash" "^0.8.0"
- "@material-ui/types" "5.1.0"
- "@material-ui/utils" "^4.11.2"
- clsx "^1.0.4"
- csstype "^2.5.2"
- hoist-non-react-statics "^3.3.2"
- jss "^10.5.1"
- jss-plugin-camel-case "^10.5.1"
- jss-plugin-default-unit "^10.5.1"
- jss-plugin-global "^10.5.1"
- jss-plugin-nested "^10.5.1"
- jss-plugin-props-sort "^10.5.1"
- jss-plugin-rule-value-function "^10.5.1"
- jss-plugin-vendor-prefixer "^10.5.1"
- prop-types "^15.7.2"
-
-"@material-ui/system@^4.12.1":
- version "4.12.1"
- resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.1.tgz#2dd96c243f8c0a331b2bb6d46efd7771a399707c"
- integrity sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw==
- dependencies:
- "@babel/runtime" "^7.4.4"
- "@material-ui/utils" "^4.11.2"
- csstype "^2.5.2"
- prop-types "^15.7.2"
-
-"@material-ui/types@5.1.0":
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/@material-ui/types/-/types-5.1.0.tgz#efa1c7a0b0eaa4c7c87ac0390445f0f88b0d88f2"
- integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==
-
-"@material-ui/utils@^4.11.2":
- version "4.11.2"
- resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-4.11.2.tgz#f1aefa7e7dff2ebcb97d31de51aecab1bb57540a"
- integrity sha512-Uul8w38u+PICe2Fg2pDKCaIG7kOyhowZ9vjiC1FsVwPABTW8vPPKfF6OvxRq3IiBaI1faOJmgdvMG7rMJARBhA==
- dependencies:
- "@babel/runtime" "^7.4.4"
- prop-types "^15.7.2"
- react-is "^16.8.0 || ^17.0.0"
-
-"@nodelib/fs.scandir@2.1.5":
- version "2.1.5"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
- integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
- dependencies:
- "@nodelib/fs.stat" "2.0.5"
- run-parallel "^1.1.9"
-
-"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
- integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
-
-"@nodelib/fs.walk@^1.2.3":
- version "1.2.8"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
- integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
- dependencies:
- "@nodelib/fs.scandir" "2.1.5"
- fastq "^1.6.0"
-
-"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
- integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
-
-"@protobufjs/base64@^1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
- integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
-
-"@protobufjs/codegen@^2.0.4":
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
- integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
-
-"@protobufjs/eventemitter@^1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
- integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
-
-"@protobufjs/fetch@^1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
- integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
- dependencies:
- "@protobufjs/aspromise" "^1.1.1"
- "@protobufjs/inquire" "^1.1.0"
-
-"@protobufjs/float@^1.0.2":
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
- integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
-
-"@protobufjs/inquire@^1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
- integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
-
-"@protobufjs/path@^1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
- integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
-
-"@protobufjs/pool@^1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
- integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
-
-"@protobufjs/utf8@^1.1.0":
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
- integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
-
-"@rollup/plugin-commonjs@^19.0.0":
- version "19.0.2"
- resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-19.0.2.tgz#1ccc3d63878d1bc9846f8969f09dd3b3e4ecc244"
- integrity sha512-gBjarfqlC7qs0AutpRW/hrFNm+cd2/QKxhwyFa+srbg1oX7rDsEU3l+W7LAUhsAp9mPJMAkXDhLbQaVwEaE8bA==
- dependencies:
- "@rollup/pluginutils" "^3.1.0"
- commondir "^1.0.1"
- estree-walker "^2.0.1"
- glob "^7.1.6"
- is-reference "^1.2.1"
- magic-string "^0.25.7"
- resolve "^1.17.0"
-
-"@rollup/plugin-node-resolve@^13.0.0":
- version "13.0.6"
- resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.6.tgz#29629070bb767567be8157f575cfa8f2b8e9ef77"
- integrity sha512-sFsPDMPd4gMqnh2gS0uIxELnoRUp5kBl5knxD2EO0778G1oOJv4G1vyT2cpWz75OU2jDVcXhjVUuTAczGyFNKA==
- dependencies:
- "@rollup/pluginutils" "^3.1.0"
- "@types/resolve" "1.17.1"
- builtin-modules "^3.1.0"
- deepmerge "^4.2.2"
- is-module "^1.0.0"
- resolve "^1.19.0"
-
-"@rollup/pluginutils@^3.0.9", "@rollup/pluginutils@^3.1.0":
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
- integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
- dependencies:
- "@types/estree" "0.0.39"
- estree-walker "^1.0.1"
- picomatch "^2.2.2"
-
-"@sindresorhus/is@^0.14.0":
- version "0.14.0"
- resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
- integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
-
-"@szmarczak/http-timer@^1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
- integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
- dependencies:
- defer-to-connect "^1.0.1"
-
-"@types/argparse@^2.0.10":
- version "2.0.10"
- resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-2.0.10.tgz#664e84808accd1987548d888b9d21b3e9c996a6c"
- integrity sha512-C4wahC3gz3vQtvPazrJ5ONwmK1zSDllQboiWvpMM/iOswCYfBREFnjFbq/iWKIVOCl8+m5Pk6eva6/ZSsDuIGA==
-
-"@types/component-emitter@^1.2.10":
- version "1.2.11"
- resolved "https://registry.yarnpkg.com/@types/component-emitter/-/component-emitter-1.2.11.tgz#50d47d42b347253817a39709fef03ce66a108506"
- integrity sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==
-
-"@types/cookie@^0.4.0":
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
- integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
-
-"@types/cors@^2.8.8":
- version "2.8.12"
- resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
- integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
-
-"@types/crc@^3.4.0":
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/@types/crc/-/crc-3.4.0.tgz#2366beb4399cd734b33e42c7ac809576e617d48a"
- integrity sha1-I2a+tDmc1zSzPkLHrICVduYX1Io=
- dependencies:
- "@types/node" "*"
-
-"@types/estree@*":
- version "0.0.50"
- resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.50.tgz#1e0caa9364d3fccd2931c3ed96fdbeaa5d4cca83"
- integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw==
-
-"@types/estree@0.0.39":
- version "0.0.39"
- resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
- integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
-
-"@types/google-protobuf@^3.15.5":
- version "3.15.5"
- resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.15.5.tgz#644b2be0f5613b1f822c70c73c6b0e0b5b5fa2ad"
- integrity sha512-6bgv24B+A2bo9AfzReeg5StdiijKzwwnRflA8RLd1V4Yv995LeTmo0z69/MPbBDFSiZWdZHQygLo/ccXhMEDgw==
-
-"@types/is-windows@^1.0.0":
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/@types/is-windows/-/is-windows-1.0.0.tgz#1011fa129d87091e2f6faf9042d6704cdf2e7be0"
- integrity sha512-tJ1rq04tGKuIJoWIH0Gyuwv4RQ3+tIu7wQrC0MV47raQ44kIzXSSFKfrxFUOWVRvesoF7mrTqigXmqoZJsXwTg==
-
-"@types/istanbul-lib-coverage@^2.0.1":
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
- integrity sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==
-
-"@types/jasmine@^3.7.8":
- version "3.10.1"
- resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.10.1.tgz#9a7ceab2e539503f70a7e6a7028c70171aca2754"
- integrity sha512-So26woGjM6F9b2julbJlXdcPdyhwteZzEX2EbFmreuJBamPVVdp6w4djywUG9TmcwjiC+ECAe+RSSBgYEOgEqQ==
-
-"@types/json-schema@^7.0.7":
- version "7.0.9"
- resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
- integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
-
-"@types/long@^4.0.0", "@types/long@^4.0.1":
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
- integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
-
-"@types/minimist@^1.2.0":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
- integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
-
-"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^16.0.1":
- version "16.11.4"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.4.tgz#90771124822d6663814f7c1c9b45a6654d8fd964"
- integrity sha512-TMgXmy0v2xWyuCSCJM6NCna2snndD8yvQF67J29ipdzMcsPa9u+o0tjF5+EQNdhcuZplYuouYqpc4zcd5I6amQ==
-
-"@types/node@^10.1.0":
- version "10.17.60"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
- integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
-
-"@types/normalize-package-data@^2.4.0":
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
- integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==
-
-"@types/prop-types@*":
- version "15.7.4"
- resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
- integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
-
-"@types/react-dom@^17.0.9":
- version "17.0.10"
- resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.10.tgz#d6972ec018d23cf22b99597f1289343d99ea9d9d"
- integrity sha512-8oz3NAUId2z/zQdFI09IMhQPNgIbiP8Lslhv39DIDamr846/0spjZK0vnrMak0iB8EKb9QFTTIdg2Wj2zH5a3g==
- dependencies:
- "@types/react" "*"
-
-"@types/react-transition-group@^4.2.0":
- version "4.4.4"
- resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.4.tgz#acd4cceaa2be6b757db61ed7b432e103242d163e"
- integrity sha512-7gAPz7anVK5xzbeQW9wFBDg7G++aPLAFY0QaSMOou9rJZpbuI58WAuJrgu+qR92l61grlnCUe7AFX8KGahAgug==
- dependencies:
- "@types/react" "*"
-
-"@types/react@*", "@types/react@^17.0.14":
- version "17.0.31"
- resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.31.tgz#fe05ebf91ff3ae35bb6b13f6c1b461db8089dff8"
- integrity sha512-MQSR5EL4JZtdWRvqDgz9kXhSDDoy2zMTYyg7UhP+FZ5ttUOocWyxiqFJiI57sUG0BtaEX7WDXYQlkCYkb3X9vQ==
- dependencies:
- "@types/prop-types" "*"
- "@types/scheduler" "*"
- csstype "^3.0.2"
-
-"@types/resolve@1.17.1":
- version "1.17.1"
- resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
- integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
- dependencies:
- "@types/node" "*"
-
-"@types/scheduler@*":
- version "0.16.2"
- resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
- integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
-
-"@typescript-eslint/eslint-plugin@^4.2.0":
- version "4.33.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276"
- integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==
- dependencies:
- "@typescript-eslint/experimental-utils" "4.33.0"
- "@typescript-eslint/scope-manager" "4.33.0"
- debug "^4.3.1"
- functional-red-black-tree "^1.0.1"
- ignore "^5.1.8"
- regexpp "^3.1.0"
- semver "^7.3.5"
- tsutils "^3.21.0"
-
-"@typescript-eslint/experimental-utils@4.33.0":
- version "4.33.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd"
- integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==
- dependencies:
- "@types/json-schema" "^7.0.7"
- "@typescript-eslint/scope-manager" "4.33.0"
- "@typescript-eslint/types" "4.33.0"
- "@typescript-eslint/typescript-estree" "4.33.0"
- eslint-scope "^5.1.1"
- eslint-utils "^3.0.0"
-
-"@typescript-eslint/parser@^4.2.0":
- version "4.33.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899"
- integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==
- dependencies:
- "@typescript-eslint/scope-manager" "4.33.0"
- "@typescript-eslint/types" "4.33.0"
- "@typescript-eslint/typescript-estree" "4.33.0"
- debug "^4.3.1"
-
-"@typescript-eslint/scope-manager@4.33.0":
- version "4.33.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3"
- integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==
- dependencies:
- "@typescript-eslint/types" "4.33.0"
- "@typescript-eslint/visitor-keys" "4.33.0"
-
-"@typescript-eslint/types@4.33.0":
- version "4.33.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72"
- integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==
-
-"@typescript-eslint/typescript-estree@4.33.0":
- version "4.33.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609"
- integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==
- dependencies:
- "@typescript-eslint/types" "4.33.0"
- "@typescript-eslint/visitor-keys" "4.33.0"
- debug "^4.3.1"
- globby "^11.0.3"
- is-glob "^4.0.1"
- semver "^7.3.5"
- tsutils "^3.21.0"
-
-"@typescript-eslint/visitor-keys@4.33.0":
- version "4.33.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd"
- integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==
- dependencies:
- "@typescript-eslint/types" "4.33.0"
- eslint-visitor-keys "^2.0.0"
-
-abbrev@1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
- integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
-
-abstract-leveldown@~0.12.0, abstract-leveldown@~0.12.1:
- version "0.12.4"
- resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-0.12.4.tgz#29e18e632e60e4e221d5810247852a63d7b2e410"
- integrity sha1-KeGOYy5g5OIh1YECR4UqY9ey5BA=
- dependencies:
- xtend "~3.0.0"
-
-accepts@~1.3.4:
- version "1.3.7"
- resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
- integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
- dependencies:
- mime-types "~2.1.24"
- negotiator "0.6.2"
-
-acorn-jsx@^5.3.1:
- version "5.3.2"
- resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
- integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
-
-acorn@^5.7.3:
- version "5.7.4"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
- integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
-
-acorn@^7.4.0:
- version "7.4.1"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
- integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
-
-agent-base@6:
- version "6.0.2"
- resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
- integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
- dependencies:
- debug "4"
-
-ajv@^6.10.0, ajv@^6.12.4:
- version "6.12.6"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
- integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
- dependencies:
- fast-deep-equal "^3.1.1"
- fast-json-stable-stringify "^2.0.0"
- json-schema-traverse "^0.4.1"
- uri-js "^4.2.2"
-
-ajv@^8.0.1:
- version "8.6.3"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764"
- integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==
- dependencies:
- fast-deep-equal "^3.1.1"
- json-schema-traverse "^1.0.0"
- require-from-string "^2.0.2"
- uri-js "^4.2.2"
-
-ansi-align@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.1.tgz#0cdf12e111ace773a86e9a1fad1225c43cb19a59"
- integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
- dependencies:
- string-width "^4.1.0"
-
-ansi-colors@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
- integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
-
-ansi-escapes@^4.2.1:
- version "4.3.2"
- resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
- integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==
- dependencies:
- type-fest "^0.21.3"
-
-ansi-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
- integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
-
-ansi-regex@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
- integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
-
-ansi-styles@^3.2.1:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
- integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
- dependencies:
- color-convert "^1.9.0"
-
-ansi-styles@^4.0.0, ansi-styles@^4.1.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
- integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
- dependencies:
- color-convert "^2.0.1"
-
-ansi_up@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/ansi_up/-/ansi_up-5.1.0.tgz#9cf10e6d359bb434bdcfab5ae4c3abfe1617b6db"
- integrity sha512-3wwu+nJCKBVBwOCurm0uv91lMoVkhFB+3qZQz3U11AmAdDJ4tkw1sNPWJQcVxMVYwe0pGEALOjSBOxdxNc+pNQ==
-
-anymatch@~3.1.2:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
- integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
- dependencies:
- normalize-path "^3.0.0"
- picomatch "^2.0.4"
-
-"aproba@^1.0.3 || ^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
- integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
-
-are-we-there-yet@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c"
- integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==
- dependencies:
- delegates "^1.0.0"
- readable-stream "^3.6.0"
-
-argparse@^1.0.7:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
- integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
- dependencies:
- sprintf-js "~1.0.2"
-
-argparse@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
- integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
-
-array-includes@^3.1.3:
- version "3.1.4"
- resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9"
- integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
- get-intrinsic "^1.1.1"
- is-string "^1.0.7"
-
-array-union@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
- integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
-
-array.prototype.flatmap@^1.2.4:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz#908dc82d8a406930fdf38598d51e7411d18d4446"
- integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA==
- dependencies:
- call-bind "^1.0.0"
- define-properties "^1.1.3"
- es-abstract "^1.19.0"
-
-arrify@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
- integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
-
-asn1.js@^5.2.0:
- version "5.4.1"
- resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
- integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
- dependencies:
- bn.js "^4.0.0"
- inherits "^2.0.1"
- minimalistic-assert "^1.0.0"
- safer-buffer "^2.1.0"
-
-astral-regex@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
- integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
-
-async@^2.6.2:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
- integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
- dependencies:
- lodash "^4.17.14"
-
-atob@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
- integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
-
-balanced-match@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
- integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-
-base64-arraybuffer@0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
- integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
-
-base64-js@^1.3.1, base64-js@^1.5.1:
- version "1.5.1"
- resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
- integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-
-base64id@2.0.0, base64id@~2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
- integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
-
-basic-auth@^1.0.3:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-1.1.0.tgz#45221ee429f7ee1e5035be3f51533f1cdfd29884"
- integrity sha1-RSIe5Cn37h5QNb4/UVM/HN/SmIQ=
-
-binary-extensions@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
- integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
-
-bl@~0.8.1:
- version "0.8.2"
- resolved "https://registry.yarnpkg.com/bl/-/bl-0.8.2.tgz#c9b6bca08d1bc2ea00fc8afb4f1a5fd1e1c66e4e"
- integrity sha1-yba8oI0bwuoA/Ir7Txpf0eHGbk4=
- dependencies:
- readable-stream "~1.0.26"
-
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
- version "4.12.0"
- resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
- integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
-
-bn.js@^5.0.0, bn.js@^5.1.1:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002"
- integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==
-
-body-parser@^1.19.0:
- version "1.19.0"
- resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
- integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
- dependencies:
- bytes "3.1.0"
- content-type "~1.0.4"
- debug "2.6.9"
- depd "~1.1.2"
- http-errors "1.7.2"
- iconv-lite "0.4.24"
- on-finished "~2.3.0"
- qs "6.7.0"
- raw-body "2.4.0"
- type-is "~1.6.17"
-
-boxen@^5.0.0:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/boxen/-/boxen-5.1.2.tgz#788cb686fc83c1f486dfa8a40c68fc2b831d2b50"
- integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==
- dependencies:
- ansi-align "^3.0.0"
- camelcase "^6.2.0"
- chalk "^4.1.0"
- cli-boxes "^2.2.1"
- string-width "^4.2.2"
- type-fest "^0.20.2"
- widest-line "^3.1.0"
- wrap-ansi "^7.0.0"
-
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-braces@^3.0.1, braces@^3.0.2, braces@~3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
- integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
- dependencies:
- fill-range "^7.0.1"
-
-brorand@^1.0.1, brorand@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
- integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
-
-browserify-aes@^1.0.0, browserify-aes@^1.0.4:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
- integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
- dependencies:
- buffer-xor "^1.0.3"
- cipher-base "^1.0.0"
- create-hash "^1.1.0"
- evp_bytestokey "^1.0.3"
- inherits "^2.0.1"
- safe-buffer "^5.0.1"
-
-browserify-cipher@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0"
- integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==
- dependencies:
- browserify-aes "^1.0.4"
- browserify-des "^1.0.0"
- evp_bytestokey "^1.0.0"
-
-browserify-des@^1.0.0:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c"
- integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==
- dependencies:
- cipher-base "^1.0.1"
- des.js "^1.0.0"
- inherits "^2.0.1"
- safe-buffer "^5.1.2"
-
-browserify-fs@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/browserify-fs/-/browserify-fs-1.0.0.tgz#f075aa8a729d4d1716d066620e386fcc1311a96f"
- integrity sha1-8HWqinKdTRcW0GZiDjhvzBMRqW8=
- dependencies:
- level-filesystem "^1.0.1"
- level-js "^2.1.3"
- levelup "^0.18.2"
-
-browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
- integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
- dependencies:
- bn.js "^5.0.0"
- randombytes "^2.0.1"
-
-browserify-sign@^4.0.0:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3"
- integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==
- dependencies:
- bn.js "^5.1.1"
- browserify-rsa "^4.0.1"
- create-hash "^1.2.0"
- create-hmac "^1.1.7"
- elliptic "^6.5.3"
- inherits "^2.0.4"
- parse-asn1 "^5.1.5"
- readable-stream "^3.6.0"
- safe-buffer "^5.2.0"
-
-buffer-es6@^4.9.2, buffer-es6@^4.9.3:
- version "4.9.3"
- resolved "https://registry.yarnpkg.com/buffer-es6/-/buffer-es6-4.9.3.tgz#f26347b82df76fd37e18bcb5288c4970cfd5c404"
- integrity sha1-8mNHuC33b9N+GLy1KIxJcM/VxAQ=
-
-buffer-from@^1.0.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
- integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
-
-buffer-xor@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9"
- integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=
-
-buffer@^5.1.0:
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
- integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
- dependencies:
- base64-js "^1.3.1"
- ieee754 "^1.1.13"
-
-buffer@^6.0.3:
- version "6.0.3"
- resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
- integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
- dependencies:
- base64-js "^1.3.1"
- ieee754 "^1.2.1"
-
-builtin-modules@^3.1.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887"
- integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==
-
-bytes@3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6"
- integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
-
-c8@~7.5.0:
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/c8/-/c8-7.5.0.tgz#a69439ab82848f344a74bb25dc5dd4e867764481"
- integrity sha512-GSkLsbvDr+FIwjNSJ8OwzWAyuznEYGTAd1pzb/Kr0FMLuV4vqYJTyjboDTwmlUNAG6jAU3PFWzqIdKrOt1D8tw==
- dependencies:
- "@bcoe/v8-coverage" "^0.2.3"
- "@istanbuljs/schema" "^0.1.2"
- find-up "^5.0.0"
- foreground-child "^2.0.0"
- furi "^2.0.0"
- istanbul-lib-coverage "^3.0.0"
- istanbul-lib-report "^3.0.0"
- istanbul-reports "^3.0.2"
- rimraf "^3.0.0"
- test-exclude "^6.0.0"
- v8-to-istanbul "^7.1.0"
- yargs "^16.0.0"
- yargs-parser "^20.0.0"
-
-cacheable-request@^6.0.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
- integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
- dependencies:
- clone-response "^1.0.2"
- get-stream "^5.1.0"
- http-cache-semantics "^4.0.0"
- keyv "^3.0.0"
- lowercase-keys "^2.0.0"
- normalize-url "^4.1.0"
- responselike "^1.0.2"
-
-call-bind@^1.0.0, call-bind@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
- integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==
- dependencies:
- function-bind "^1.1.1"
- get-intrinsic "^1.0.2"
-
-callsites@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
- integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
-
-camelcase-keys@^6.2.2:
- version "6.2.2"
- resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0"
- integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==
- dependencies:
- camelcase "^5.3.1"
- map-obj "^4.0.0"
- quick-lru "^4.0.1"
-
-camelcase@^5.3.1:
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
- integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-
-camelcase@^6.2.0:
- version "6.2.0"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809"
- integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==
-
-chalk@^2.0.0:
- version "2.4.2"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
- integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
- dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
-
-chalk@^4.0.0, chalk@^4.1.0:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
- integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
- dependencies:
- ansi-styles "^4.1.0"
- supports-color "^7.1.0"
-
-chardet@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
- integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
-
-chokidar@^3.5.1:
- version "3.5.2"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
- integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
- dependencies:
- anymatch "~3.1.2"
- braces "~3.0.2"
- glob-parent "~5.1.2"
- is-binary-path "~2.1.0"
- is-glob "~4.0.1"
- normalize-path "~3.0.0"
- readdirp "~3.6.0"
- optionalDependencies:
- fsevents "~2.3.2"
-
-chownr@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece"
- integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==
-
-ci-info@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
- integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
-
-cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
- integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==
- dependencies:
- inherits "^2.0.1"
- safe-buffer "^5.0.1"
-
-cli-boxes@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.1.tgz#ddd5035d25094fce220e9cab40a45840a440318f"
- integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==
-
-cli-cursor@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
- integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
- dependencies:
- restore-cursor "^3.1.0"
-
-cli-width@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6"
- integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==
-
-cliui@^7.0.2:
- version "7.0.4"
- resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
- integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
- dependencies:
- string-width "^4.2.0"
- strip-ansi "^6.0.0"
- wrap-ansi "^7.0.0"
-
-clone-response@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
- integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
- dependencies:
- mimic-response "^1.0.0"
-
-clone@~0.1.9:
- version "0.1.19"
- resolved "https://registry.yarnpkg.com/clone/-/clone-0.1.19.tgz#613fb68639b26a494ac53253e15b1a6bd88ada85"
- integrity sha1-YT+2hjmyaklKxTJT4Vsaa9iK2oU=
-
-clsx@^1.0.4:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
- integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
-
-color-convert@^1.9.0:
- version "1.9.3"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
- integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
- dependencies:
- color-name "1.1.3"
-
-color-convert@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
- integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
- dependencies:
- color-name "~1.1.4"
-
-color-name@1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
- integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
-
-color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-color-support@^1.1.2:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
- integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
-
-colors@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
- integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
-
-commondir@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
- integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
-
-component-emitter@~1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
- integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-
-concat-stream@^1.4.4:
- version "1.6.2"
- resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
- integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
- dependencies:
- buffer-from "^1.0.0"
- inherits "^2.0.3"
- readable-stream "^2.2.2"
- typedarray "^0.0.6"
-
-configstore@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96"
- integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==
- dependencies:
- dot-prop "^5.2.0"
- graceful-fs "^4.1.2"
- make-dir "^3.0.0"
- unique-string "^2.0.0"
- write-file-atomic "^3.0.0"
- xdg-basedir "^4.0.0"
-
-connect@^3.7.0:
- version "3.7.0"
- resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8"
- integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==
- dependencies:
- debug "2.6.9"
- finalhandler "1.1.2"
- parseurl "~1.3.3"
- utils-merge "1.0.1"
-
-console-control-strings@^1.0.0, console-control-strings@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
- integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
-
-content-type@~1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
- integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
-
-convert-source-map@^1.6.0:
- version "1.8.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
- integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
- dependencies:
- safe-buffer "~5.1.1"
-
-cookie@~0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
- integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
-
-core-util-is@~1.0.0:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
- integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
-
-cors@~2.8.5:
- version "2.8.5"
- resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
- integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
- dependencies:
- object-assign "^4"
- vary "^1"
-
-corser@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/corser/-/corser-2.0.1.tgz#8eda252ecaab5840dcd975ceb90d9370c819ff87"
- integrity sha1-jtolLsqrWEDc2XXOuQ2TcMgZ/4c=
-
-crc@^3.8.0:
- version "3.8.0"
- resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
- integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
- dependencies:
- buffer "^5.1.0"
-
-create-ecdh@^4.0.0:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
- integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==
- dependencies:
- bn.js "^4.1.0"
- elliptic "^6.5.3"
-
-create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196"
- integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==
- dependencies:
- cipher-base "^1.0.1"
- inherits "^2.0.1"
- md5.js "^1.3.4"
- ripemd160 "^2.0.1"
- sha.js "^2.4.0"
-
-create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff"
- integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==
- dependencies:
- cipher-base "^1.0.3"
- create-hash "^1.1.0"
- inherits "^2.0.1"
- ripemd160 "^2.0.0"
- safe-buffer "^5.0.1"
- sha.js "^2.4.8"
-
-cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
- integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
- dependencies:
- path-key "^3.1.0"
- shebang-command "^2.0.0"
- which "^2.0.1"
-
-crypto-browserify@^3.11.0:
- version "3.12.0"
- resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec"
- integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==
- dependencies:
- browserify-cipher "^1.0.0"
- browserify-sign "^4.0.0"
- create-ecdh "^4.0.0"
- create-hash "^1.1.0"
- create-hmac "^1.1.0"
- diffie-hellman "^5.0.0"
- inherits "^2.0.1"
- pbkdf2 "^3.0.3"
- public-encrypt "^4.0.0"
- randombytes "^2.0.0"
- randomfill "^1.0.3"
-
-crypto-random-string@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
- integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==
-
-css-vendor@^2.0.8:
- version "2.0.8"
- resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d"
- integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==
- dependencies:
- "@babel/runtime" "^7.8.3"
- is-in-browser "^1.0.2"
-
-csstype@^2.5.2:
- version "2.6.18"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.18.tgz#980a8b53085f34af313410af064f2bd241784218"
- integrity sha512-RSU6Hyeg14am3Ah4VZEmeX8H7kLwEEirXe6aU2IPfKNvhXwTflK5HQRDNI0ypQXoqmm+QPyG2IaPuQE5zMwSIQ==
-
-csstype@^3.0.2:
- version "3.0.9"
- resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.9.tgz#6410af31b26bd0520933d02cbc64fce9ce3fbf0b"
- integrity sha512-rpw6JPxK6Rfg1zLOYCSwle2GFOOsnjmDYDaBwEcwoOg4qlsIVCN789VkBZDJAGi4T07gI4YSutR43t9Zz4Lzuw==
-
-custom-event@~1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
- integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=
-
-date-format@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf"
- integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==
-
-date-format@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/date-format/-/date-format-3.0.0.tgz#eb8780365c7d2b1511078fb491e6479780f3ad95"
- integrity sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==
-
-debug@2.6.9:
- version "2.6.9"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
- integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
- dependencies:
- ms "2.0.0"
-
-debug@4, debug@^4.0.1, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@~4.3.1:
- version "4.3.2"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
- integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
- dependencies:
- ms "2.1.2"
-
-debug@^3.1.1:
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
- integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==
- dependencies:
- ms "^2.1.1"
-
-decamelize-keys@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
- integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
- dependencies:
- decamelize "^1.1.0"
- map-obj "^1.0.0"
-
-decamelize@^1.1.0, decamelize@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
- integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
-
-decode-uri-component@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
- integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
-
-decompress-response@^3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
- integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
- dependencies:
- mimic-response "^1.0.0"
-
-deep-extend@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
- integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
-
-deep-is@^0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
- integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
-
-deepmerge@^4.2.2:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
- integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
-
-defer-to-connect@^1.0.1:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591"
- integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==
-
-deferred-leveldown@~0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-0.2.0.tgz#2cef1f111e1c57870d8bbb8af2650e587cd2f5b4"
- integrity sha1-LO8fER4cV4cNi7uK8mUOWHzS9bQ=
- dependencies:
- abstract-leveldown "~0.12.1"
-
-define-properties@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
- integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
- dependencies:
- object-keys "^1.0.12"
-
-delegates@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
- integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
-
-depd@~1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
- integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
-
-des.js@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
- integrity sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==
- dependencies:
- inherits "^2.0.1"
- minimalistic-assert "^1.0.0"
-
-detect-libc@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
- integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
-
-di@^0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
- integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=
-
-diffie-hellman@^5.0.0:
- version "5.0.3"
- resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875"
- integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==
- dependencies:
- bn.js "^4.1.0"
- miller-rabin "^4.0.0"
- randombytes "^2.0.0"
-
-dir-glob@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
- integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
- dependencies:
- path-type "^4.0.0"
-
-doctrine@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
- integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
- dependencies:
- esutils "^2.0.2"
-
-doctrine@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
- integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
- dependencies:
- esutils "^2.0.2"
-
-dom-helpers@^5.0.1:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
- integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
- dependencies:
- "@babel/runtime" "^7.8.7"
- csstype "^3.0.2"
-
-dom-serialize@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
- integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=
- dependencies:
- custom-event "~1.0.0"
- ent "~2.2.0"
- extend "^3.0.0"
- void-elements "^2.0.0"
-
-dom-serializer@^1.0.1:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
- integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==
- dependencies:
- domelementtype "^2.0.1"
- domhandler "^4.2.0"
- entities "^2.0.0"
-
-domelementtype@^2.0.1, domelementtype@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
- integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
-
-domhandler@4.2.2, domhandler@^4.0.0, domhandler@^4.2.0:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
- integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==
- dependencies:
- domelementtype "^2.2.0"
-
-domutils@^2.5.2:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
- integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
- dependencies:
- dom-serializer "^1.0.1"
- domelementtype "^2.2.0"
- domhandler "^4.2.0"
-
-dot-prop@^5.2.0:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88"
- integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==
- dependencies:
- is-obj "^2.0.0"
-
-duplexer3@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
- integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
-
-ee-first@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
- integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-
-elliptic@^6.5.3:
- version "6.5.4"
- resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
- integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
- dependencies:
- bn.js "^4.11.9"
- brorand "^1.1.0"
- hash.js "^1.0.0"
- hmac-drbg "^1.0.1"
- inherits "^2.0.4"
- minimalistic-assert "^1.0.1"
- minimalistic-crypto-utils "^1.0.1"
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-encodeurl@~1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
- integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
-
-end-of-stream@^1.1.0:
- version "1.4.4"
- resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
- integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
- dependencies:
- once "^1.4.0"
-
-engine.io-parser@~4.0.0:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-4.0.3.tgz#83d3a17acfd4226f19e721bb22a1ee8f7662d2f6"
- integrity sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==
- dependencies:
- base64-arraybuffer "0.1.4"
-
-engine.io@~4.1.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-4.1.1.tgz#9a8f8a5ac5a5ea316183c489bf7f5b6cf91ace5b"
- integrity sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==
- dependencies:
- accepts "~1.3.4"
- base64id "2.0.0"
- cookie "~0.4.1"
- cors "~2.8.5"
- debug "~4.3.1"
- engine.io-parser "~4.0.0"
- ws "~7.4.2"
-
-enquirer@^2.3.5:
- version "2.3.6"
- resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
- integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
- dependencies:
- ansi-colors "^4.1.1"
-
-ent@~2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
- integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0=
-
-entities@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
- integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==
-
-errno@^0.1.1, errno@~0.1.1:
- version "0.1.8"
- resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
- integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==
- dependencies:
- prr "~1.0.1"
-
-error-ex@^1.3.1:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
- integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
- dependencies:
- is-arrayish "^0.2.1"
-
-es-abstract@^1.19.0, es-abstract@^1.19.1:
- version "1.19.1"
- resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3"
- integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==
- dependencies:
- call-bind "^1.0.2"
- es-to-primitive "^1.2.1"
- function-bind "^1.1.1"
- get-intrinsic "^1.1.1"
- get-symbol-description "^1.0.0"
- has "^1.0.3"
- has-symbols "^1.0.2"
- internal-slot "^1.0.3"
- is-callable "^1.2.4"
- is-negative-zero "^2.0.1"
- is-regex "^1.1.4"
- is-shared-array-buffer "^1.0.1"
- is-string "^1.0.7"
- is-weakref "^1.0.1"
- object-inspect "^1.11.0"
- object-keys "^1.1.1"
- object.assign "^4.1.2"
- string.prototype.trimend "^1.0.4"
- string.prototype.trimstart "^1.0.4"
- unbox-primitive "^1.0.1"
-
-es-to-primitive@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a"
- integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==
- dependencies:
- is-callable "^1.1.4"
- is-date-object "^1.0.1"
- is-symbol "^1.0.2"
-
-escalade@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
- integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
-
-escape-goat@^2.0.0:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
- integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
-
-escape-html@~1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
- integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
-
-escape-string-regexp@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
- integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
-
-escape-string-regexp@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
- integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
-
-eslint-config-prettier@^7.0.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9"
- integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==
-
-eslint-plugin-es@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893"
- integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==
- dependencies:
- eslint-utils "^2.0.0"
- regexpp "^3.0.0"
-
-eslint-plugin-node@^11.1.0:
- version "11.1.0"
- resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d"
- integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==
- dependencies:
- eslint-plugin-es "^3.0.0"
- eslint-utils "^2.0.0"
- ignore "^5.1.1"
- minimatch "^3.0.4"
- resolve "^1.10.1"
- semver "^6.1.0"
-
-eslint-plugin-prettier@^3.1.4:
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz#e9ddb200efb6f3d05ffe83b1665a716af4a387e5"
- integrity sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==
- dependencies:
- prettier-linter-helpers "^1.0.0"
-
-eslint-plugin-react@^7.24.0:
- version "7.26.1"
- resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.26.1.tgz#41bcfe3e39e6a5ac040971c1af94437c80daa40e"
- integrity sha512-Lug0+NOFXeOE+ORZ5pbsh6mSKjBKXDXItUD2sQoT+5Yl0eoT82DqnXeTMfUare4QVCn9QwXbfzO/dBLjLXwVjQ==
- dependencies:
- array-includes "^3.1.3"
- array.prototype.flatmap "^1.2.4"
- doctrine "^2.1.0"
- estraverse "^5.2.0"
- jsx-ast-utils "^2.4.1 || ^3.0.0"
- minimatch "^3.0.4"
- object.entries "^1.1.4"
- object.fromentries "^2.0.4"
- object.hasown "^1.0.0"
- object.values "^1.1.4"
- prop-types "^15.7.2"
- resolve "^2.0.0-next.3"
- semver "^6.3.0"
- string.prototype.matchall "^4.0.5"
-
-eslint-scope@^5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
- integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
- dependencies:
- esrecurse "^4.3.0"
- estraverse "^4.1.1"
-
-eslint-utils@^2.0.0, eslint-utils@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
- integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
- dependencies:
- eslint-visitor-keys "^1.1.0"
-
-eslint-utils@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
- integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
- dependencies:
- eslint-visitor-keys "^2.0.0"
-
-eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
- integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
-
-eslint-visitor-keys@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
- integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
-
-eslint@^7.10.0, eslint@^7.30.0:
- version "7.32.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
- integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
- dependencies:
- "@babel/code-frame" "7.12.11"
- "@eslint/eslintrc" "^0.4.3"
- "@humanwhocodes/config-array" "^0.5.0"
- ajv "^6.10.0"
- chalk "^4.0.0"
- cross-spawn "^7.0.2"
- debug "^4.0.1"
- doctrine "^3.0.0"
- enquirer "^2.3.5"
- escape-string-regexp "^4.0.0"
- eslint-scope "^5.1.1"
- eslint-utils "^2.1.0"
- eslint-visitor-keys "^2.0.0"
- espree "^7.3.1"
- esquery "^1.4.0"
- esutils "^2.0.2"
- fast-deep-equal "^3.1.3"
- file-entry-cache "^6.0.1"
- functional-red-black-tree "^1.0.1"
- glob-parent "^5.1.2"
- globals "^13.6.0"
- ignore "^4.0.6"
- import-fresh "^3.0.0"
- imurmurhash "^0.1.4"
- is-glob "^4.0.0"
- js-yaml "^3.13.1"
- json-stable-stringify-without-jsonify "^1.0.1"
- levn "^0.4.1"
- lodash.merge "^4.6.2"
- minimatch "^3.0.4"
- natural-compare "^1.4.0"
- optionator "^0.9.1"
- progress "^2.0.0"
- regexpp "^3.1.0"
- semver "^7.2.1"
- strip-ansi "^6.0.0"
- strip-json-comments "^3.1.0"
- table "^6.0.9"
- text-table "^0.2.0"
- v8-compile-cache "^2.0.3"
-
-espree@^7.3.0, espree@^7.3.1:
- version "7.3.1"
- resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
- integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==
- dependencies:
- acorn "^7.4.0"
- acorn-jsx "^5.3.1"
- eslint-visitor-keys "^1.3.0"
-
-esprima@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
- integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-
-esquery@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
- integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
- dependencies:
- estraverse "^5.1.0"
-
-esrecurse@^4.3.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
- integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
- dependencies:
- estraverse "^5.2.0"
-
-estraverse@^4.1.1:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
- integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
-
-estraverse@^5.1.0, estraverse@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
- integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
-
-estree-walker@^0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.5.2.tgz#d3850be7529c9580d815600b53126515e146dd39"
- integrity sha512-XpCnW/AE10ws/kDAs37cngSkvgIR8aN3G0MS85m7dUpuK2EREo9VJ00uvw6Dg/hXEpfsE1I1TvJOJr+Z+TL+ig==
-
-estree-walker@^0.6.1:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362"
- integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==
-
-estree-walker@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
- integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
-
-estree-walker@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
- integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
-
-esutils@^2.0.2:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
- integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
-
-eventemitter3@^4.0.0:
- version "4.0.7"
- resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
- integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
-
-evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
- integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==
- dependencies:
- md5.js "^1.3.4"
- safe-buffer "^5.1.1"
-
-execa@^5.0.0:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
- integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==
- dependencies:
- cross-spawn "^7.0.3"
- get-stream "^6.0.0"
- human-signals "^2.1.0"
- is-stream "^2.0.0"
- merge-stream "^2.0.0"
- npm-run-path "^4.0.1"
- onetime "^5.1.2"
- signal-exit "^3.0.3"
- strip-final-newline "^2.0.0"
-
-executioner@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/executioner/-/executioner-2.0.1.tgz#add328e03bc45dd598f358fbb529fc0be0ec6fcd"
- integrity sha1-rdMo4DvEXdWY81j7tSn8C+Dsb80=
- dependencies:
- mixly "^1.0.0"
-
-extend@^3.0.0:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
- integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
-
-external-editor@^3.0.3:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495"
- integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==
- dependencies:
- chardet "^0.7.0"
- iconv-lite "^0.4.24"
- tmp "^0.0.33"
-
-fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
- integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
-
-fast-diff@^1.1.2:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
- integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
-
-fast-glob@^3.1.1:
- version "3.2.7"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
- integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
- dependencies:
- "@nodelib/fs.stat" "^2.0.2"
- "@nodelib/fs.walk" "^1.2.3"
- glob-parent "^5.1.2"
- merge2 "^1.3.0"
- micromatch "^4.0.4"
-
-fast-json-stable-stringify@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
- integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
-
-fast-levenshtein@^2.0.6:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
- integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
-
-fastq@^1.6.0:
- version "1.13.0"
- resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
- integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
- dependencies:
- reusify "^1.0.4"
-
-figures@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
- integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==
- dependencies:
- escape-string-regexp "^1.0.5"
-
-file-entry-cache@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
- integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
- dependencies:
- flat-cache "^3.0.4"
-
-fill-range@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
- integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
- dependencies:
- to-regex-range "^5.0.1"
-
-finalhandler@1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d"
- integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
- dependencies:
- debug "2.6.9"
- encodeurl "~1.0.2"
- escape-html "~1.0.3"
- on-finished "~2.3.0"
- parseurl "~1.3.3"
- statuses "~1.5.0"
- unpipe "~1.0.0"
-
-find-up@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
- integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
- dependencies:
- locate-path "^5.0.0"
- path-exists "^4.0.0"
-
-find-up@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
- integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
- dependencies:
- locate-path "^6.0.0"
- path-exists "^4.0.0"
-
-flat-cache@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
- integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
- dependencies:
- flatted "^3.1.0"
- rimraf "^3.0.2"
-
-flatted@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
- integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
-
-flatted@^3.1.0:
- version "3.2.2"
- resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
- integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
-
-follow-redirects@^1.0.0:
- version "1.14.4"
- resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
- integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==
-
-foreach@~2.0.1:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
- integrity sha1-C+4AUBiusmDQo6865ljdATbsG5k=
-
-foreground-child@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-2.0.0.tgz#71b32800c9f15aa8f2f83f4a6bd9bff35d861a53"
- integrity sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==
- dependencies:
- cross-spawn "^7.0.0"
- signal-exit "^3.0.2"
-
-fs-extra@^8.1.0:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
- integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==
- dependencies:
- graceful-fs "^4.2.0"
- jsonfile "^4.0.0"
- universalify "^0.1.0"
-
-fs-minipass@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
- integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
- dependencies:
- minipass "^3.0.0"
-
-fs.realpath@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
- integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-
-fsevents@~2.3.2:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
- integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
-
-fulcon@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/fulcon/-/fulcon-1.0.2.tgz#8a4dfda4c73fcd9cc62a79d5045c392b45547320"
- integrity sha1-ik39pMc/zZzGKnnVBFw5K0VUcyA=
-
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-functional-red-black-tree@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
- integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
-
-furi@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/furi/-/furi-2.0.0.tgz#13d85826a1af21acc691da6254b3888fc39f0b4a"
- integrity sha512-uKuNsaU0WVaK/vmvj23wW1bicOFfyqSsAIH71bRZx8kA4Xj+YCHin7CJKJJjkIsmxYaPFLk9ljmjEyB7xF7WvQ==
- dependencies:
- "@types/is-windows" "^1.0.0"
- is-windows "^1.0.2"
-
-fwd-stream@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/fwd-stream/-/fwd-stream-1.0.4.tgz#ed281cabed46feecf921ee32dc4c50b372ac7cfa"
- integrity sha1-7Sgcq+1G/uz5Ie4y3ExQs3KsfPo=
- dependencies:
- readable-stream "~1.0.26-4"
-
-gauge@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.1.tgz#4bea07bcde3782f06dced8950e51307aa0f4a346"
- integrity sha512-6STz6KdQgxO4S/ko+AbjlFGGdGcknluoqU+79GOFCDqqyYj5OanQf9AjxwN0jCidtT+ziPMmPSt9E4hfQ0CwIQ==
- dependencies:
- aproba "^1.0.3 || ^2.0.0"
- color-support "^1.1.2"
- console-control-strings "^1.0.0"
- has-unicode "^2.0.1"
- object-assign "^4.1.1"
- signal-exit "^3.0.0"
- string-width "^1.0.1 || ^2.0.0"
- strip-ansi "^3.0.1 || ^4.0.0"
- wide-align "^1.1.2"
-
-get-caller-file@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
- integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-
-get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
- integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
- dependencies:
- function-bind "^1.1.1"
- has "^1.0.3"
- has-symbols "^1.0.1"
-
-get-stream@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
- integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
- dependencies:
- pump "^3.0.0"
-
-get-stream@^5.1.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3"
- integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==
- dependencies:
- pump "^3.0.0"
-
-get-stream@^6.0.0:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
- integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
-
-get-symbol-description@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6"
- integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==
- dependencies:
- call-bind "^1.0.2"
- get-intrinsic "^1.1.1"
-
-glob-parent@^5.1.2, glob-parent@~5.1.2:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
- integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
- dependencies:
- is-glob "^4.0.1"
-
-glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
- integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-global-dirs@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686"
- integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==
- dependencies:
- ini "2.0.0"
-
-globals@^13.6.0, globals@^13.9.0:
- version "13.11.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7"
- integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==
- dependencies:
- type-fest "^0.20.2"
-
-globby@^11.0.3:
- version "11.0.4"
- resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
- integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
- dependencies:
- array-union "^2.1.0"
- dir-glob "^3.0.1"
- fast-glob "^3.1.1"
- ignore "^5.1.4"
- merge2 "^1.3.0"
- slash "^3.0.0"
-
-google-protobuf@^3.15.5, google-protobuf@^3.17.3, google-protobuf@^3.6.1:
- version "3.19.0"
- resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.19.0.tgz#97f474323c92f19fd6737af1bb792e396991e0b8"
- integrity sha512-qXGAiv3OOlaJXJNeKOBKxbBAwjsxzhx+12ZdKOkZTsqsRkyiQRmr/nBkAkqnuQ8cmA9X5NVXvObQTpHVnXE2DQ==
-
-got@^9.6.0:
- version "9.6.0"
- resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
- integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
- dependencies:
- "@sindresorhus/is" "^0.14.0"
- "@szmarczak/http-timer" "^1.1.2"
- cacheable-request "^6.0.0"
- decompress-response "^3.3.0"
- duplexer3 "^0.1.4"
- get-stream "^4.1.0"
- lowercase-keys "^1.0.1"
- mimic-response "^1.0.1"
- p-cancelable "^1.0.0"
- to-readable-stream "^1.0.0"
- url-parse-lax "^3.0.0"
-
-graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6:
- version "4.2.8"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
- integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
-
-grpc-tools@^1.11.2:
- version "1.11.2"
- resolved "https://registry.yarnpkg.com/grpc-tools/-/grpc-tools-1.11.2.tgz#22d802d40012510ccc6591d11f9c94109ac07aab"
- integrity sha512-4+EgpnnkJraamY++oyBCw5Hp9huRYfgakjNVKbiE3PgO9Tv5ydVlRo7ZyGJ0C0SEiA7HhbVc1sNNtIyK7FiEtg==
- dependencies:
- "@mapbox/node-pre-gyp" "^1.0.5"
-
-grpc-web@^1.2.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/grpc-web/-/grpc-web-1.3.0.tgz#4c36d97e7a7b6102a7df463e7822cd86d4f65ed8"
- integrity sha512-nePImtnrnkZLErFq00Sr1H6AqaRrRptOJEhjUnlTB6RiJgs8ULYvRI9cX2hDwMvyYgakmO3H/wThYvS+Ibdreg==
-
-gts@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/gts/-/gts-3.1.0.tgz#b27ce914191ed6ad34781968d0c77e0ed3042388"
- integrity sha512-Pbj3ob1VR1IRlEVEBNtKoQ1wHOa8cZz62KEojK8Fn/qeS2ClWI4gLNfhek3lD68aZSmUEg8TFb6AHXIwUMgyqQ==
- dependencies:
- "@typescript-eslint/eslint-plugin" "^4.2.0"
- "@typescript-eslint/parser" "^4.2.0"
- chalk "^4.1.0"
- eslint "^7.10.0"
- eslint-config-prettier "^7.0.0"
- eslint-plugin-node "^11.1.0"
- eslint-plugin-prettier "^3.1.4"
- execa "^5.0.0"
- inquirer "^7.3.3"
- json5 "^2.1.3"
- meow "^9.0.0"
- ncp "^2.0.0"
- prettier "^2.1.2"
- rimraf "^3.0.2"
- update-notifier "^5.0.0"
- write-file-atomic "^3.0.3"
-
-hard-rejection@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883"
- integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==
-
-has-bigints@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
- integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
-
-has-flag@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
- integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
-
-has-flag@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
- integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-has-symbols@^1.0.1, has-symbols@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423"
- integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==
-
-has-tostringtag@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
- integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==
- dependencies:
- has-symbols "^1.0.2"
-
-has-unicode@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
- integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
-
-has-yarn@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77"
- integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==
-
-has@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
- integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
- dependencies:
- function-bind "^1.1.1"
-
-hash-base@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33"
- integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==
- dependencies:
- inherits "^2.0.4"
- readable-stream "^3.6.0"
- safe-buffer "^5.2.0"
-
-hash.js@^1.0.0, hash.js@^1.0.3:
- version "1.1.7"
- resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
- integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
- dependencies:
- inherits "^2.0.3"
- minimalistic-assert "^1.0.1"
-
-he@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
- integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
-
-hmac-drbg@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
- integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
- dependencies:
- hash.js "^1.0.3"
- minimalistic-assert "^1.0.0"
- minimalistic-crypto-utils "^1.0.1"
-
-hoist-non-react-statics@^3.3.2:
- version "3.3.2"
- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
- integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
- dependencies:
- react-is "^16.7.0"
-
-hosted-git-info@^2.1.4:
- version "2.8.9"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
- integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
-
-hosted-git-info@^4.0.1:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.0.2.tgz#5e425507eede4fea846b7262f0838456c4209961"
- integrity sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==
- dependencies:
- lru-cache "^6.0.0"
-
-html-dom-parser@1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-1.0.2.tgz#bb5ff844f214657d899aa4fb7b0a9e7d15607e96"
- integrity sha512-Jq4oVkVSn+10ut3fyc2P/Fs1jqTo0l45cP6Q8d2ef/9jfkYwulO0QXmyLI0VUiZrXF4czpGgMEJRa52CQ6Fk8Q==
- dependencies:
- domhandler "4.2.2"
- htmlparser2 "6.1.0"
-
-html-escaper@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
- integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
-
-html-react-parser@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-1.4.0.tgz#bf264f38b9fdf4d94e2120f6a39586c15cb81bd0"
- integrity sha512-v8Kxy+7L90ZFSM690oJWBNRzZWZOQquYPpQt6kDQPzQyZptXgOJ69kHSi7xdqNdm1mOfsDPwF4K9Bo/dS5gRTQ==
- dependencies:
- domhandler "4.2.2"
- html-dom-parser "1.0.2"
- react-property "2.0.0"
- style-to-js "1.1.0"
-
-htmlparser2@6.1.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
- integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
- dependencies:
- domelementtype "^2.0.1"
- domhandler "^4.0.0"
- domutils "^2.5.2"
- entities "^2.0.0"
-
-http-cache-semantics@^4.0.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
- integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
-
-http-errors@1.7.2:
- version "1.7.2"
- resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f"
- integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
- dependencies:
- depd "~1.1.2"
- inherits "2.0.3"
- setprototypeof "1.1.1"
- statuses ">= 1.5.0 < 2"
- toidentifier "1.0.0"
-
-http-proxy@^1.18.0, http-proxy@^1.18.1:
- version "1.18.1"
- resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
- integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==
- dependencies:
- eventemitter3 "^4.0.0"
- follow-redirects "^1.0.0"
- requires-port "^1.0.0"
-
-http-server@^13.0.2:
- version "13.0.2"
- resolved "https://registry.yarnpkg.com/http-server/-/http-server-13.0.2.tgz#36f8a8ae0e1b78e7bf30a4dfb01ae89b904904ef"
- integrity sha512-R8kvPT7qp11AMJWLZsRShvm6heIXdlR/+tL5oAWNG/86A/X+IAFX6q0F9SA2G+dR5aH/759+9PLH0V34Q6j4rg==
- dependencies:
- basic-auth "^1.0.3"
- colors "^1.4.0"
- corser "^2.0.1"
- he "^1.1.0"
- http-proxy "^1.18.0"
- mime "^1.6.0"
- minimist "^1.2.5"
- opener "^1.5.1"
- portfinder "^1.0.25"
- secure-compare "3.0.1"
- union "~0.5.0"
- url-join "^2.0.5"
-
-https-proxy-agent@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
- integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
- dependencies:
- agent-base "6"
- debug "4"
-
-human-signals@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
- integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
-
-hyphenate-style-name@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
- integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
-
-iconv-lite@0.4.24, iconv-lite@^0.4.24:
- version "0.4.24"
- resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
- integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
- dependencies:
- safer-buffer ">= 2.1.2 < 3"
-
-idb-wrapper@^1.5.0:
- version "1.7.2"
- resolved "https://registry.yarnpkg.com/idb-wrapper/-/idb-wrapper-1.7.2.tgz#8251afd5e77fe95568b1c16152eb44b396767ea2"
- integrity sha512-zfNREywMuf0NzDo9mVsL0yegjsirJxHpKHvWcyRozIqQy89g0a3U+oBPOCN4cc0oCiOuYgZHimzaW/R46G1Mpg==
-
-ieee754@^1.1.13, ieee754@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
- integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
-
-ignore@^4.0.6:
- version "4.0.6"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
- integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
-
-ignore@^5.1.1, ignore@^5.1.4, ignore@^5.1.8:
- version "5.1.8"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
- integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
-
-import-fresh@^3.0.0, import-fresh@^3.2.1:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
- integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
- dependencies:
- parent-module "^1.0.0"
- resolve-from "^4.0.0"
-
-import-lazy@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
- integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
-
-imurmurhash@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
- integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
-
-indent-string@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
- integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-
-indexof@~0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
- integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
-
-inflight@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
- integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
- dependencies:
- once "^1.3.0"
- wrappy "1"
-
-inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-inherits@2.0.3:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
- integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
-
-ini@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5"
- integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
-
-ini@~1.3.0:
- version "1.3.8"
- resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c"
- integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
-
-inline-style-parser@0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
- integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
-
-inquirer@^7.3.3:
- version "7.3.3"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003"
- integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==
- dependencies:
- ansi-escapes "^4.2.1"
- chalk "^4.1.0"
- cli-cursor "^3.1.0"
- cli-width "^3.0.0"
- external-editor "^3.0.3"
- figures "^3.0.0"
- lodash "^4.17.19"
- mute-stream "0.0.8"
- run-async "^2.4.0"
- rxjs "^6.6.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
- through "^2.3.6"
-
-install-peers@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/install-peers/-/install-peers-1.0.3.tgz#6348f8f67e6bc23c19ee78adb819c43f8d1dd7d7"
- integrity sha512-MAlSHlrn4p+g3fhx8ZVxQZXX+MkeinKLu/ThfAmrVnN5c2L8Vof7myb0UsgowJEiGcFNHYnTvo37r3uap5asYA==
- dependencies:
- executioner "^2.0.1"
-
-internal-slot@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
- integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==
- dependencies:
- get-intrinsic "^1.1.0"
- has "^1.0.3"
- side-channel "^1.0.4"
-
-is-arrayish@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
- integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
-
-is-bigint@^1.0.1:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
- integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==
- dependencies:
- has-bigints "^1.0.1"
-
-is-binary-path@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
- integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
- dependencies:
- binary-extensions "^2.0.0"
-
-is-boolean-object@^1.1.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719"
- integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==
- dependencies:
- call-bind "^1.0.2"
- has-tostringtag "^1.0.0"
-
-is-callable@^1.1.4, is-callable@^1.2.4:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945"
- integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==
-
-is-ci@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
- integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
- dependencies:
- ci-info "^2.0.0"
-
-is-core-module@^2.2.0, is-core-module@^2.5.0:
- version "2.8.0"
- resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548"
- integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==
- dependencies:
- has "^1.0.3"
-
-is-date-object@^1.0.1:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f"
- integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==
- dependencies:
- has-tostringtag "^1.0.0"
-
-is-docker@^2.0.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
- integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
-
-is-extglob@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
- integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
-
-is-fullwidth-code-point@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
- integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
- integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
- dependencies:
- is-extglob "^2.1.1"
-
-is-in-browser@^1.0.2, is-in-browser@^1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
- integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=
-
-is-installed-globally@^0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520"
- integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==
- dependencies:
- global-dirs "^3.0.0"
- is-path-inside "^3.0.2"
-
-is-module@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
- integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=
-
-is-negative-zero@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24"
- integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==
-
-is-npm@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-5.0.0.tgz#43e8d65cc56e1b67f8d47262cf667099193f45a8"
- integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA==
-
-is-number-object@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0"
- integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==
- dependencies:
- has-tostringtag "^1.0.0"
-
-is-number@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
- integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-
-is-obj@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
- integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
-
-is-object@~0.1.2:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/is-object/-/is-object-0.1.2.tgz#00efbc08816c33cfc4ac8251d132e10dc65098d7"
- integrity sha1-AO+8CIFsM8/ErIJR0TLhDcZQmNc=
-
-is-path-inside@^3.0.2:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
- integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
-
-is-plain-obj@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
- integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
-
-is-reference@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
- integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
- dependencies:
- "@types/estree" "*"
-
-is-regex@^1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958"
- integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==
- dependencies:
- call-bind "^1.0.2"
- has-tostringtag "^1.0.0"
-
-is-shared-array-buffer@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6"
- integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==
-
-is-stream@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077"
- integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==
-
-is-string@^1.0.5, is-string@^1.0.7:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd"
- integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==
- dependencies:
- has-tostringtag "^1.0.0"
-
-is-symbol@^1.0.2, is-symbol@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c"
- integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==
- dependencies:
- has-symbols "^1.0.2"
-
-is-typedarray@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
- integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
-
-is-weakref@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.1.tgz#842dba4ec17fa9ac9850df2d6efbc1737274f2a2"
- integrity sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==
- dependencies:
- call-bind "^1.0.0"
-
-is-windows@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
- integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
-
-is-wsl@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271"
- integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==
- dependencies:
- is-docker "^2.0.0"
-
-is-yarn-global@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
- integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
-
-is@~0.2.6:
- version "0.2.7"
- resolved "https://registry.yarnpkg.com/is/-/is-0.2.7.tgz#3b34a2c48f359972f35042849193ae7264b63562"
- integrity sha1-OzSixI81mXLzUEKEkZOucmS2NWI=
-
-isarray@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
- integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
-
-isarray@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
- integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
-
-isbinaryfile@^4.0.8:
- version "4.0.8"
- resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.8.tgz#5d34b94865bd4946633ecc78a026fc76c5b11fcf"
- integrity sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==
-
-isbuffer@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/isbuffer/-/isbuffer-0.0.0.tgz#38c146d9df528b8bf9b0701c3d43cf12df3fc39b"
- integrity sha1-OMFG2d9Si4v5sHAcPUPPEt8/w5s=
-
-isexe@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
- integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
-
-istanbul-lib-coverage@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3"
- integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==
-
-istanbul-lib-report@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6"
- integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==
- dependencies:
- istanbul-lib-coverage "^3.0.0"
- make-dir "^3.0.0"
- supports-color "^7.1.0"
-
-istanbul-reports@^3.0.2:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.0.5.tgz#a2580107e71279ea6d661ddede929ffc6d693384"
- integrity sha512-5+19PlhnGabNWB7kOFnuxT8H3T/iIyQzIbQMxXsURmmvKg86P2sbkrGOT77VnHw0Qr0gc2XzRaRfMZYYbSQCJQ==
- dependencies:
- html-escaper "^2.0.0"
- istanbul-lib-report "^3.0.0"
-
-jasmine-core@^3.6.0, jasmine-core@^3.8.0, jasmine-core@~3.10.0:
- version "3.10.1"
- resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.10.1.tgz#7aa6fa2b834a522315c651a128d940eca553989a"
- integrity sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==
-
-jasmine-reporters@~2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/jasmine-reporters/-/jasmine-reporters-2.4.0.tgz#708c17ae70ba6671e3a930bb1b202aab80a31409"
- integrity sha512-jxONSrBLN1vz/8zCx5YNWQSS8iyDAlXQ5yk1LuqITe4C6iXCDx5u6Q0jfNtkKhL4qLZPe69fL+AWvXFt9/x38w==
- dependencies:
- mkdirp "^0.5.1"
- xmldom "^0.5.0"
-
-jasmine@^3.8.0:
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.10.0.tgz#acd3cd560a9d20d8fdad6bd2dd05867d188503f3"
- integrity sha512-2Y42VsC+3CQCTzTwJezOvji4qLORmKIE0kwowWC+934Krn6ZXNQYljiwK5st9V3PVx96BSiDYXSB60VVah3IlQ==
- dependencies:
- glob "^7.1.6"
- jasmine-core "~3.10.0"
-
-"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
- integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-
-js-yaml@^3.13.1:
- version "3.14.1"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
- integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
- dependencies:
- argparse "^1.0.7"
- esprima "^4.0.0"
-
-json-buffer@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
- integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
-
-json-parse-even-better-errors@^2.3.0:
- version "2.3.1"
- resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
- integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
-
-json-schema-traverse@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
- integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
-
-json-schema-traverse@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
- integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
-
-json-stable-stringify-without-jsonify@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
- integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
-
-json5@^2.1.3:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.0.tgz#2dfefe720c6ba525d9ebd909950f0515316c89a3"
- integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==
- dependencies:
- minimist "^1.2.5"
-
-jsonfile@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
- integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=
- optionalDependencies:
- graceful-fs "^4.1.6"
-
-jss-plugin-camel-case@^10.5.1:
- version "10.8.1"
- resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.8.1.tgz#342fd406c2e8781ecdc4ac298a78b892f75581ab"
- integrity sha512-nOYKsvX9qh/AcUWSSRZHKyUj4RwqnhUSq4EKNFA1nHsNw0VJYwtF1yqtOPvztWEP3LTlNhcwoPINsb/eKVmYqA==
- dependencies:
- "@babel/runtime" "^7.3.1"
- hyphenate-style-name "^1.0.3"
- jss "10.8.1"
-
-jss-plugin-default-unit@^10.5.1:
- version "10.8.1"
- resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.8.1.tgz#1c35b89cd70ca5b0e01c21d89d908e75c1ea80ad"
- integrity sha512-W/uwVJNrFtUrVyAPfH/3ZngFYUVilMxgNbuWHYslqv3c5gnBKM6iXeoDzOnB+wtQJoSCTLzD3q77H7OeNK3oxg==
- dependencies:
- "@babel/runtime" "^7.3.1"
- jss "10.8.1"
-
-jss-plugin-global@^10.5.1:
- version "10.8.1"
- resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.8.1.tgz#992a14210c567178eb4cd385edcd267ae8cc6f28"
- integrity sha512-ERYLzD+L/v3yQL2mM5/PE+3xU/GCXcfXuGIL1kVkiEIpXnWtND/Mphf2iHQaqedx59uhiVHFZaMtv6qv+iNsDw==
- dependencies:
- "@babel/runtime" "^7.3.1"
- jss "10.8.1"
-
-jss-plugin-nested@^10.5.1:
- version "10.8.1"
- resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.8.1.tgz#ac9750f8185725a0fd6ea484860767c53ec3d3dc"
- integrity sha512-Z15G23Fb8/br23EclH9CAq2UGdi29XgpSWXFTBusMJbWjitFdDCdYMzk7bSUJ6P7L5+WpaIDNxIJ9WrdMRqdXw==
- dependencies:
- "@babel/runtime" "^7.3.1"
- jss "10.8.1"
- tiny-warning "^1.0.2"
-
-jss-plugin-props-sort@^10.5.1:
- version "10.8.1"
- resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.8.1.tgz#ee07bebf8ebeab01f8d9369973c64891cca53af9"
- integrity sha512-BNbKYuh4IawWr7cticlnbI+kBx01o39DNHkjAkc2CGKWVboUb2EpktDqonqVN/BjyzDgZXKOmwz36ZFkLQB45g==
- dependencies:
- "@babel/runtime" "^7.3.1"
- jss "10.8.1"
-
-jss-plugin-rule-value-function@^10.5.1:
- version "10.8.1"
- resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.8.1.tgz#40b19406f3cc027d9001de9026750e726c322993"
- integrity sha512-XrvM4bokyU1xPXr+gVEIlT9WylLQZcdC+1JDxriXDEWmKEjJgtH+w6ZicchTydLqq1qtA4fEevhdMvm4QvgIKw==
- dependencies:
- "@babel/runtime" "^7.3.1"
- jss "10.8.1"
- tiny-warning "^1.0.2"
-
-jss-plugin-vendor-prefixer@^10.5.1:
- version "10.8.1"
- resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.8.1.tgz#362146c2b641aae1d29f279307aec0e2167c7ee2"
- integrity sha512-77b/iEFmA669s+USru2Y5eg9Hs1C1N0zE/4EaJm/fqKScCTNawHXZv5l5w6j81A9CNa63Ar7jekAIfBkoKFmLw==
- dependencies:
- "@babel/runtime" "^7.3.1"
- css-vendor "^2.0.8"
- jss "10.8.1"
-
-jss@10.8.1, jss@^10.5.1:
- version "10.8.1"
- resolved "https://registry.yarnpkg.com/jss/-/jss-10.8.1.tgz#375797c259ffce417e56ae1a7fe703acde8de9ee"
- integrity sha512-P4wKxU+2m5ReGl0Mmbf9XYgVjFIVZJOZ9ylXBxdpanX+HHgj5XVaAIgYzYpKbBLPCdkAUsI/Iq1fhQPsMNu0YA==
- dependencies:
- "@babel/runtime" "^7.3.1"
- csstype "^3.0.2"
- is-in-browser "^1.1.3"
- tiny-warning "^1.0.2"
-
-"jsx-ast-utils@^2.4.1 || ^3.0.0":
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz#720b97bfe7d901b927d87c3773637ae8ea48781b"
- integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA==
- dependencies:
- array-includes "^3.1.3"
- object.assign "^4.1.2"
-
-karma-chrome-launcher@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738"
- integrity sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==
- dependencies:
- which "^1.2.1"
-
-karma-firefox-launcher@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-2.1.1.tgz#6457226f8e4f091b664cef79bb5d39bf1e008765"
- integrity sha512-VzDMgPseXak9DtfyE1O5bB2BwsMy1zzO1kUxVW1rP0yhC4tDNJ0p3JoFdzvrK4QqVzdqUMa9Rx9YzkdFp8hz3Q==
- dependencies:
- is-wsl "^2.2.0"
- which "^2.0.1"
-
-karma-jasmine@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-4.0.1.tgz#b99e073b6d99a5196fc4bffc121b89313b0abd82"
- integrity sha512-h8XDAhTiZjJKzfkoO1laMH+zfNlra+dEQHUAjpn5JV1zCPtOIVWGQjLBrqhnzQa/hrU2XrZwSyBa6XjEBzfXzw==
- dependencies:
- jasmine-core "^3.6.0"
-
-karma-junit-reporter@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/karma-junit-reporter/-/karma-junit-reporter-2.0.1.tgz#d34eef7f0b2fd064e0896954e8851a90cf14c8f3"
- integrity sha512-VtcGfE0JE4OE1wn0LK8xxDKaTP7slN8DO3I+4xg6gAi1IoAHAXOJ1V9G/y45Xg6sxdxPOR3THCFtDlAfBo9Afw==
- dependencies:
- path-is-absolute "^1.0.0"
- xmlbuilder "12.0.0"
-
-karma-requirejs@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/karma-requirejs/-/karma-requirejs-1.1.0.tgz#fddae2cb87d7ebc16fb0222893564d7fee578798"
- integrity sha1-/driy4fX68FvsCIok1ZNf+5Xh5g=
-
-karma-sourcemap-loader@^0.3.8:
- version "0.3.8"
- resolved "https://registry.yarnpkg.com/karma-sourcemap-loader/-/karma-sourcemap-loader-0.3.8.tgz#d4bae72fb7a8397328a62b75013d2df937bdcf9c"
- integrity sha512-zorxyAakYZuBcHRJE+vbrK2o2JXLFWK8VVjiT/6P+ltLBUGUvqTEkUiQ119MGdOrK7mrmxXHZF1/pfT6GgIZ6g==
- dependencies:
- graceful-fs "^4.1.2"
-
-karma@6.3.4:
- version "6.3.4"
- resolved "https://registry.yarnpkg.com/karma/-/karma-6.3.4.tgz#359899d3aab3d6b918ea0f57046fd2a6b68565e6"
- integrity sha512-hbhRogUYIulfkBTZT7xoPrCYhRBnBoqbbL4fszWD0ReFGUxU+LYBr3dwKdAluaDQ/ynT9/7C+Lf7pPNW4gSx4Q==
- dependencies:
- body-parser "^1.19.0"
- braces "^3.0.2"
- chokidar "^3.5.1"
- colors "^1.4.0"
- connect "^3.7.0"
- di "^0.0.1"
- dom-serialize "^2.2.1"
- glob "^7.1.7"
- graceful-fs "^4.2.6"
- http-proxy "^1.18.1"
- isbinaryfile "^4.0.8"
- lodash "^4.17.21"
- log4js "^6.3.0"
- mime "^2.5.2"
- minimatch "^3.0.4"
- qjobs "^1.2.0"
- range-parser "^1.2.1"
- rimraf "^3.0.2"
- socket.io "^3.1.0"
- source-map "^0.6.1"
- tmp "^0.2.1"
- ua-parser-js "^0.7.28"
- yargs "^16.1.1"
-
-keyv@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
- integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
- dependencies:
- json-buffer "3.0.0"
-
-kind-of@^6.0.3:
- version "6.0.3"
- resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
- integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
-
-latest-version@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"
- integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==
- dependencies:
- package-json "^6.3.0"
-
-level-blobs@^0.1.7:
- version "0.1.7"
- resolved "https://registry.yarnpkg.com/level-blobs/-/level-blobs-0.1.7.tgz#9ab9b97bb99f1edbf9f78a3433e21ed56386bdaf"
- integrity sha1-mrm5e7mfHtv594o0M+Ie1WOGva8=
- dependencies:
- level-peek "1.0.6"
- once "^1.3.0"
- readable-stream "^1.0.26-4"
-
-level-filesystem@^1.0.1:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/level-filesystem/-/level-filesystem-1.2.0.tgz#a00aca9919c4a4dfafdca6a8108d225aadff63b3"
- integrity sha1-oArKmRnEpN+v3KaoEI0iWq3/Y7M=
- dependencies:
- concat-stream "^1.4.4"
- errno "^0.1.1"
- fwd-stream "^1.0.4"
- level-blobs "^0.1.7"
- level-peek "^1.0.6"
- level-sublevel "^5.2.0"
- octal "^1.0.0"
- once "^1.3.0"
- xtend "^2.2.0"
-
-level-fix-range@2.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/level-fix-range/-/level-fix-range-2.0.0.tgz#c417d62159442151a19d9a2367868f1724c2d548"
- integrity sha1-xBfWIVlEIVGhnZojZ4aPFyTC1Ug=
- dependencies:
- clone "~0.1.9"
-
-level-fix-range@~1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/level-fix-range/-/level-fix-range-1.0.2.tgz#bf15b915ae36d8470c821e883ddf79cd16420828"
- integrity sha1-vxW5Fa422EcMgh6IPd95zRZCCCg=
-
-"level-hooks@>=4.4.0 <5":
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/level-hooks/-/level-hooks-4.5.0.tgz#1b9ae61922930f3305d1a61fc4d83c8102c0dd93"
- integrity sha1-G5rmGSKTDzMF0aYfxNg8gQLA3ZM=
- dependencies:
- string-range "~1.2"
-
-level-js@^2.1.3:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/level-js/-/level-js-2.2.4.tgz#bc055f4180635d4489b561c9486fa370e8c11697"
- integrity sha1-vAVfQYBjXUSJtWHJSG+jcOjBFpc=
- dependencies:
- abstract-leveldown "~0.12.0"
- idb-wrapper "^1.5.0"
- isbuffer "~0.0.0"
- ltgt "^2.1.2"
- typedarray-to-buffer "~1.0.0"
- xtend "~2.1.2"
-
-level-peek@1.0.6, level-peek@^1.0.6:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/level-peek/-/level-peek-1.0.6.tgz#bec51c72a82ee464d336434c7c876c3fcbcce77f"
- integrity sha1-vsUccqgu5GTTNkNMfIdsP8vM538=
- dependencies:
- level-fix-range "~1.0.2"
-
-level-sublevel@^5.2.0:
- version "5.2.3"
- resolved "https://registry.yarnpkg.com/level-sublevel/-/level-sublevel-5.2.3.tgz#744c12c72d2e72be78dde3b9b5cd84d62191413a"
- integrity sha1-dEwSxy0ucr543eO5tc2E1iGRQTo=
- dependencies:
- level-fix-range "2.0"
- level-hooks ">=4.4.0 <5"
- string-range "~1.2.1"
- xtend "~2.0.4"
-
-levelup@^0.18.2:
- version "0.18.6"
- resolved "https://registry.yarnpkg.com/levelup/-/levelup-0.18.6.tgz#e6a01cb089616c8ecc0291c2a9bd3f0c44e3e5eb"
- integrity sha1-5qAcsIlhbI7MApHCqb0/DETj5es=
- dependencies:
- bl "~0.8.1"
- deferred-leveldown "~0.2.0"
- errno "~0.1.1"
- prr "~0.0.0"
- readable-stream "~1.0.26"
- semver "~2.3.1"
- xtend "~3.0.0"
-
-levn@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
- integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
- dependencies:
- prelude-ls "^1.2.1"
- type-check "~0.4.0"
-
-lines-and-columns@^1.1.6:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
- integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
-
-locate-path@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
- integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
- dependencies:
- p-locate "^4.1.0"
-
-locate-path@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
- integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
- dependencies:
- p-locate "^5.0.0"
-
-lodash.camelcase@^4.3.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
- integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
-
-lodash.clonedeep@^4.5.0:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
- integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
-
-lodash.merge@^4.6.2:
- version "4.6.2"
- resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
- integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
-
-lodash.truncate@^4.4.2:
- version "4.4.2"
- resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
- integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
-
-lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.21:
- version "4.17.21"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
- integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
-
-log4js@^6.3.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/log4js/-/log4js-6.3.0.tgz#10dfafbb434351a3e30277a00b9879446f715bcb"
- integrity sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==
- dependencies:
- date-format "^3.0.0"
- debug "^4.1.1"
- flatted "^2.0.1"
- rfdc "^1.1.4"
- streamroller "^2.2.4"
-
-long@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
- integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
-
-loose-envify@^1.1.0, loose-envify@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
- integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
- dependencies:
- js-tokens "^3.0.0 || ^4.0.0"
-
-lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
- integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
-
-lowercase-keys@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
- integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
-
-lru-cache@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
- integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
- dependencies:
- yallist "^4.0.0"
-
-ltgt@^2.1.2:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5"
- integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=
-
-magic-string@^0.22.5:
- version "0.22.5"
- resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.22.5.tgz#8e9cf5afddf44385c1da5bc2a6a0dbd10b03657e"
- integrity sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==
- dependencies:
- vlq "^0.2.2"
-
-magic-string@^0.25.7:
- version "0.25.7"
- resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
- integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
- dependencies:
- sourcemap-codec "^1.4.4"
-
-make-dir@^3.0.0, make-dir@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
- integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==
- dependencies:
- semver "^6.0.0"
-
-map-obj@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
- integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
-
-map-obj@^4.0.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.3.0.tgz#9304f906e93faae70880da102a9f1df0ea8bb05a"
- integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==
-
-md5.js@^1.3.4:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
- integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==
- dependencies:
- hash-base "^3.0.0"
- inherits "^2.0.1"
- safe-buffer "^5.1.2"
-
-media-typer@0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
- integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
-
-meow@^9.0.0:
- version "9.0.0"
- resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364"
- integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==
- dependencies:
- "@types/minimist" "^1.2.0"
- camelcase-keys "^6.2.2"
- decamelize "^1.2.0"
- decamelize-keys "^1.1.0"
- hard-rejection "^2.1.0"
- minimist-options "4.1.0"
- normalize-package-data "^3.0.0"
- read-pkg-up "^7.0.1"
- redent "^3.0.0"
- trim-newlines "^3.0.0"
- type-fest "^0.18.0"
- yargs-parser "^20.2.3"
-
-merge-stream@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
- integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
-
-merge2@^1.3.0:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
- integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
-
-micromatch@^4.0.4:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9"
- integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==
- dependencies:
- braces "^3.0.1"
- picomatch "^2.2.3"
-
-miller-rabin@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
- integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==
- dependencies:
- bn.js "^4.0.0"
- brorand "^1.0.1"
-
-mime-db@1.50.0:
- version "1.50.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.50.0.tgz#abd4ac94e98d3c0e185016c67ab45d5fde40c11f"
- integrity sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==
-
-mime-types@~2.1.24:
- version "2.1.33"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.33.tgz#1fa12a904472fafd068e48d9e8401f74d3f70edb"
- integrity sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==
- dependencies:
- mime-db "1.50.0"
-
-mime@^1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
- integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
-
-mime@^2.5.2:
- version "2.5.2"
- resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe"
- integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg==
-
-mimic-fn@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
- integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
-
-mimic-response@^1.0.0, mimic-response@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
- integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
-
-min-indent@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869"
- integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==
-
-minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
- integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
-
-minimalistic-crypto-utils@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
- integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
-
-minimatch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimist-options@4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"
- integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==
- dependencies:
- arrify "^1.0.1"
- is-plain-obj "^1.1.0"
- kind-of "^6.0.3"
-
-minimist@^1.2.0, minimist@^1.2.5:
- version "1.2.5"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
- integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
-
-minipass@^3.0.0:
- version "3.1.5"
- resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.5.tgz#71f6251b0a33a49c01b3cf97ff77eda030dff732"
- integrity sha512-+8NzxD82XQoNKNrl1d/FSi+X8wAEWR+sbYAfIvub4Nz0d22plFG72CEVVaufV8PNf4qSslFTD8VMOxNVhHCjTw==
- dependencies:
- yallist "^4.0.0"
-
-minizlib@^2.1.1:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
- integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
- dependencies:
- minipass "^3.0.0"
- yallist "^4.0.0"
-
-mixly@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/mixly/-/mixly-1.0.0.tgz#9b5a2e1f63e6dfba0d30e6797ffae62ab1dc24ef"
- integrity sha1-m1ouH2Pm37oNMOZ5f/rmKrHcJO8=
- dependencies:
- fulcon "^1.0.1"
-
-mkdirp@^0.5.1, mkdirp@^0.5.5:
- version "0.5.5"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
- integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
- dependencies:
- minimist "^1.2.5"
-
-mkdirp@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
- integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
-
-ms@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
- integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
-
-ms@2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
- integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
-ms@^2.1.1:
- version "2.1.3"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
- integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-
-mute-stream@0.0.8:
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d"
- integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==
-
-natural-compare@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
- integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
-
-ncp@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
- integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
-
-negotiator@0.6.2:
- version "0.6.2"
- resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
- integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
-
-node-fetch@^2.6.5:
- version "2.6.5"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.5.tgz#42735537d7f080a7e5f78b6c549b7146be1742fd"
- integrity sha512-mmlIVHJEu5rnIxgEgez6b9GgWXbkZj5YZ7fx+2r94a2E+Uirsp6HsPTPlomfdHtpt/B0cdKviwkoaM6pyvUOpQ==
- dependencies:
- whatwg-url "^5.0.0"
-
-nopt@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
- integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
- dependencies:
- abbrev "1"
-
-normalize-package-data@^2.5.0:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
- integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
- dependencies:
- hosted-git-info "^2.1.4"
- resolve "^1.10.0"
- semver "2 || 3 || 4 || 5"
- validate-npm-package-license "^3.0.1"
-
-normalize-package-data@^3.0.0:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e"
- integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==
- dependencies:
- hosted-git-info "^4.0.1"
- is-core-module "^2.5.0"
- semver "^7.3.4"
- validate-npm-package-license "^3.0.1"
-
-normalize-path@^3.0.0, normalize-path@~3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
- integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
-normalize-url@^4.1.0:
- version "4.5.1"
- resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.1.tgz#0dd90cf1288ee1d1313b87081c9a5932ee48518a"
- integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==
-
-npm-run-path@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
- integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==
- dependencies:
- path-key "^3.0.0"
-
-npmlog@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0"
- integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==
- dependencies:
- are-we-there-yet "^2.0.0"
- console-control-strings "^1.1.0"
- gauge "^3.0.0"
- set-blocking "^2.0.0"
-
-object-assign@^4, object-assign@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
- integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
-
-object-inspect@^1.11.0, object-inspect@^1.9.0:
- version "1.11.0"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
- integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
-
-object-keys@^1.0.12, object-keys@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
- integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
-
-object-keys@~0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.2.0.tgz#cddec02998b091be42bf1035ae32e49f1cb6ea67"
- integrity sha1-zd7AKZiwkb5CvxA1rjLknxy26mc=
- dependencies:
- foreach "~2.0.1"
- indexof "~0.0.1"
- is "~0.2.6"
-
-object-keys@~0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
- integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=
-
-object.assign@^4.1.2:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940"
- integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==
- dependencies:
- call-bind "^1.0.0"
- define-properties "^1.1.3"
- has-symbols "^1.0.1"
- object-keys "^1.1.1"
-
-object.entries@^1.1.4:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.5.tgz#e1acdd17c4de2cd96d5a08487cfb9db84d881861"
- integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
-
-object.fromentries@^2.0.4:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.5.tgz#7b37b205109c21e741e605727fe8b0ad5fa08251"
- integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
-
-object.hasown@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.0.tgz#7232ed266f34d197d15cac5880232f7a4790afe5"
- integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg==
- dependencies:
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
-
-object.values@^1.1.4:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac"
- integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
-
-octal@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/octal/-/octal-1.0.0.tgz#63e7162a68efbeb9e213588d58e989d1e5c4530b"
- integrity sha1-Y+cWKmjvvrniE1iNWOmJ0eXEUws=
-
-on-finished@~2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
- integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
- dependencies:
- ee-first "1.1.1"
-
-once@^1.3.0, once@^1.3.1, once@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
- integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
- dependencies:
- wrappy "1"
-
-onetime@^5.1.0, onetime@^5.1.2:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
- integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==
- dependencies:
- mimic-fn "^2.1.0"
-
-opener@^1.5.1:
- version "1.5.2"
- resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
- integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
-
-optionator@^0.9.1:
- version "0.9.1"
- resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
- integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
- dependencies:
- deep-is "^0.1.3"
- fast-levenshtein "^2.0.6"
- levn "^0.4.1"
- prelude-ls "^1.2.1"
- type-check "^0.4.0"
- word-wrap "^1.2.3"
-
-os-tmpdir@~1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
- integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
-
-p-cancelable@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
- integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
-
-p-limit@^2.2.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
- integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
- dependencies:
- p-try "^2.0.0"
-
-p-limit@^3.0.2:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
- integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
- dependencies:
- yocto-queue "^0.1.0"
-
-p-locate@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
- integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
- dependencies:
- p-limit "^2.2.0"
-
-p-locate@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
- integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
- dependencies:
- p-limit "^3.0.2"
-
-p-try@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
- integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-
-package-json@^6.3.0:
- version "6.5.0"
- resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
- integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
- dependencies:
- got "^9.6.0"
- registry-auth-token "^4.0.0"
- registry-url "^5.0.0"
- semver "^6.2.0"
-
-parent-module@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
- integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
- dependencies:
- callsites "^3.0.0"
-
-parse-asn1@^5.0.0, parse-asn1@^5.1.5:
- version "5.1.6"
- resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
- integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
- dependencies:
- asn1.js "^5.2.0"
- browserify-aes "^1.0.0"
- evp_bytestokey "^1.0.0"
- pbkdf2 "^3.0.3"
- safe-buffer "^5.1.1"
-
-parse-json@^5.0.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd"
- integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==
- dependencies:
- "@babel/code-frame" "^7.0.0"
- error-ex "^1.3.1"
- json-parse-even-better-errors "^2.3.0"
- lines-and-columns "^1.1.6"
-
-parseurl@~1.3.3:
- version "1.3.3"
- resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
- integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
-
-path-exists@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
- integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-is-absolute@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
- integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-
-path-key@^3.0.0, path-key@^3.1.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
- integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
-
-path-parse@^1.0.6:
- version "1.0.7"
- resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
- integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
-
-path-type@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
- integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-
-pbkdf2@^3.0.3:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075"
- integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==
- dependencies:
- create-hash "^1.1.2"
- create-hmac "^1.1.4"
- ripemd160 "^2.0.1"
- safe-buffer "^5.0.1"
- sha.js "^2.4.8"
-
-picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
- integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==
-
-popper.js@1.16.1-lts:
- version "1.16.1-lts"
- resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05"
- integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==
-
-portfinder@^1.0.25:
- version "1.0.28"
- resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778"
- integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==
- dependencies:
- async "^2.6.2"
- debug "^3.1.1"
- mkdirp "^0.5.5"
-
-prelude-ls@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
- integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
-
-prepend-http@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
- integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
-
-prettier-linter-helpers@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
- integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
- dependencies:
- fast-diff "^1.1.2"
-
-prettier@^2.1.2:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
- integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
-
-process-es6@^0.11.2, process-es6@^0.11.6:
- version "0.11.6"
- resolved "https://registry.yarnpkg.com/process-es6/-/process-es6-0.11.6.tgz#c6bb389f9a951f82bd4eb169600105bd2ff9c778"
- integrity sha1-xrs4n5qVH4K9TrFpYAEFvS/5x3g=
-
-process-nextick-args@~2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
- integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
-
-progress@^2.0.0:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
- integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
-
-prop-types@^15.6.2, prop-types@^15.7.2:
- version "15.7.2"
- resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
- integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
- dependencies:
- loose-envify "^1.4.0"
- object-assign "^4.1.1"
- react-is "^16.8.1"
-
-protobufjs@6.8.8:
- version "6.8.8"
- resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c"
- integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==
- dependencies:
- "@protobufjs/aspromise" "^1.1.2"
- "@protobufjs/base64" "^1.1.2"
- "@protobufjs/codegen" "^2.0.4"
- "@protobufjs/eventemitter" "^1.1.0"
- "@protobufjs/fetch" "^1.1.0"
- "@protobufjs/float" "^1.0.2"
- "@protobufjs/inquire" "^1.1.0"
- "@protobufjs/path" "^1.1.2"
- "@protobufjs/pool" "^1.1.0"
- "@protobufjs/utf8" "^1.1.0"
- "@types/long" "^4.0.0"
- "@types/node" "^10.1.0"
- long "^4.0.0"
-
-protobufjs@^6.10.0:
- version "6.11.2"
- resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b"
- integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==
- dependencies:
- "@protobufjs/aspromise" "^1.1.2"
- "@protobufjs/base64" "^1.1.2"
- "@protobufjs/codegen" "^2.0.4"
- "@protobufjs/eventemitter" "^1.1.0"
- "@protobufjs/fetch" "^1.1.0"
- "@protobufjs/float" "^1.0.2"
- "@protobufjs/inquire" "^1.1.0"
- "@protobufjs/path" "^1.1.2"
- "@protobufjs/pool" "^1.1.0"
- "@protobufjs/utf8" "^1.1.0"
- "@types/long" "^4.0.1"
- "@types/node" ">=13.7.0"
- long "^4.0.0"
-
-prr@~0.0.0:
- version "0.0.0"
- resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
- integrity sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=
-
-prr@~1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
- integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY=
-
-public-encrypt@^4.0.0:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0"
- integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==
- dependencies:
- bn.js "^4.1.0"
- browserify-rsa "^4.0.0"
- create-hash "^1.1.0"
- parse-asn1 "^5.0.0"
- randombytes "^2.0.1"
- safe-buffer "^5.1.2"
-
-pump@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
- integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
- dependencies:
- end-of-stream "^1.1.0"
- once "^1.3.1"
-
-punycode@^2.1.0:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
- integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
-
-pupa@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.1.1.tgz#f5e8fd4afc2c5d97828faa523549ed8744a20d62"
- integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==
- dependencies:
- escape-goat "^2.0.0"
-
-qjobs@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071"
- integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==
-
-qs@6.7.0:
- version "6.7.0"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
- integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
-
-qs@^6.4.0:
- version "6.10.1"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
- integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
- dependencies:
- side-channel "^1.0.4"
-
-queue-microtask@^1.2.2:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
- integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
-
-quick-lru@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
- integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
-
-randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
- integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==
- dependencies:
- safe-buffer "^5.1.0"
-
-randomfill@^1.0.3:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458"
- integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==
- dependencies:
- randombytes "^2.0.5"
- safe-buffer "^5.1.0"
-
-range-parser@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
- integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
-
-raw-body@2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"
- integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
- dependencies:
- bytes "3.1.0"
- http-errors "1.7.2"
- iconv-lite "0.4.24"
- unpipe "1.0.0"
-
-rc@^1.2.8:
- version "1.2.8"
- resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
- integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
- dependencies:
- deep-extend "^0.6.0"
- ini "~1.3.0"
- minimist "^1.2.0"
- strip-json-comments "~2.0.1"
-
-react-dom@^17.0.2:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
- integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
- dependencies:
- loose-envify "^1.1.0"
- object-assign "^4.1.1"
- scheduler "^0.20.2"
-
-react-is@^16.7.0, react-is@^16.8.1:
- version "16.13.1"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
- integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
-
-"react-is@^16.8.0 || ^17.0.0":
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
- integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
-
-react-property@2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.0.tgz#2156ba9d85fa4741faf1918b38efc1eae3c6a136"
- integrity sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==
-
-react-transition-group@^4.4.0:
- version "4.4.2"
- resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
- integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
- dependencies:
- "@babel/runtime" "^7.5.5"
- dom-helpers "^5.0.1"
- loose-envify "^1.4.0"
- prop-types "^15.6.2"
-
-react@^17.0.2:
- version "17.0.2"
- resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
- integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
- dependencies:
- loose-envify "^1.1.0"
- object-assign "^4.1.1"
-
-read-pkg-up@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507"
- integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==
- dependencies:
- find-up "^4.1.0"
- read-pkg "^5.2.0"
- type-fest "^0.8.1"
-
-read-pkg@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc"
- integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==
- dependencies:
- "@types/normalize-package-data" "^2.4.0"
- normalize-package-data "^2.5.0"
- parse-json "^5.0.0"
- type-fest "^0.6.0"
-
-readable-stream@^1.0.26-4:
- version "1.1.14"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
- integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk=
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.1"
- isarray "0.0.1"
- string_decoder "~0.10.x"
-
-readable-stream@^2.2.2:
- version "2.3.7"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
- integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.3"
- isarray "~1.0.0"
- process-nextick-args "~2.0.0"
- safe-buffer "~5.1.1"
- string_decoder "~1.1.1"
- util-deprecate "~1.0.1"
-
-readable-stream@^3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
- integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
- dependencies:
- inherits "^2.0.3"
- string_decoder "^1.1.1"
- util-deprecate "^1.0.1"
-
-readable-stream@~1.0.26, readable-stream@~1.0.26-4:
- version "1.0.34"
- resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
- integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
- dependencies:
- core-util-is "~1.0.0"
- inherits "~2.0.1"
- isarray "0.0.1"
- string_decoder "~0.10.x"
-
-readdirp@~3.6.0:
- version "3.6.0"
- resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
- integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
- dependencies:
- picomatch "^2.2.1"
-
-redent@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f"
- integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==
- dependencies:
- indent-string "^4.0.0"
- strip-indent "^3.0.0"
-
-regenerator-runtime@^0.13.4:
- version "0.13.9"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
- integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
-
-regexp.prototype.flags@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz#7ef352ae8d159e758c0eadca6f8fcb4eef07be26"
- integrity sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.3"
-
-regexpp@^3.0.0, regexpp@^3.1.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
- integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
-
-registry-auth-token@^4.0.0:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250"
- integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==
- dependencies:
- rc "^1.2.8"
-
-registry-url@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009"
- integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
- dependencies:
- rc "^1.2.8"
-
-require-directory@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
- integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
-
-require-from-string@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
- integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
-
-requirejs@^2.3.6:
- version "2.3.6"
- resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.6.tgz#e5093d9601c2829251258c0b9445d4d19fa9e7c9"
- integrity sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==
-
-requires-port@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
- integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
-
-resolve-from@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
- integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
-
-resolve@^1.10.0, resolve@^1.10.1, resolve@^1.17.0, resolve@^1.19.0:
- version "1.20.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
- integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
- dependencies:
- is-core-module "^2.2.0"
- path-parse "^1.0.6"
-
-resolve@^2.0.0-next.3:
- version "2.0.0-next.3"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.3.tgz#d41016293d4a8586a39ca5d9b5f15cbea1f55e46"
- integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==
- dependencies:
- is-core-module "^2.2.0"
- path-parse "^1.0.6"
-
-responselike@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
- integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
- dependencies:
- lowercase-keys "^1.0.0"
-
-restore-cursor@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
- integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
- dependencies:
- onetime "^5.1.0"
- signal-exit "^3.0.2"
-
-reusify@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
- integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-
-rfdc@^1.1.4:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
- integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
-
-rimraf@^3.0.0, rimraf@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
- integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
- dependencies:
- glob "^7.1.3"
-
-ripemd160@^2.0.0, ripemd160@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
- integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==
- dependencies:
- hash-base "^3.0.0"
- inherits "^2.0.1"
-
-rollup-plugin-node-builtins@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/rollup-plugin-node-builtins/-/rollup-plugin-node-builtins-2.1.2.tgz#24a1fed4a43257b6b64371d8abc6ce1ab14597e9"
- integrity sha1-JKH+1KQyV7a2Q3HYq8bOGrFFl+k=
- dependencies:
- browserify-fs "^1.0.0"
- buffer-es6 "^4.9.2"
- crypto-browserify "^3.11.0"
- process-es6 "^0.11.2"
-
-rollup-plugin-node-globals@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/rollup-plugin-node-globals/-/rollup-plugin-node-globals-1.4.0.tgz#5e1f24a9bb97c0ef51249f625e16c7e61b7c020b"
- integrity sha512-xRkB+W/m1KLIzPUmG0ofvR+CPNcvuCuNdjVBVS7ALKSxr3EDhnzNceGkGi1m8MToSli13AzKFYH4ie9w3I5L3g==
- dependencies:
- acorn "^5.7.3"
- buffer-es6 "^4.9.3"
- estree-walker "^0.5.2"
- magic-string "^0.22.5"
- process-es6 "^0.11.6"
- rollup-pluginutils "^2.3.1"
-
-rollup-plugin-sourcemaps@^0.6.3:
- version "0.6.3"
- resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.6.3.tgz#bf93913ffe056e414419607f1d02780d7ece84ed"
- integrity sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==
- dependencies:
- "@rollup/pluginutils" "^3.0.9"
- source-map-resolve "^0.6.0"
-
-rollup-pluginutils@^2.3.1:
- version "2.8.2"
- resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e"
- integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==
- dependencies:
- estree-walker "^0.6.1"
-
-rollup@^2.52.8:
- version "2.58.0"
- resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.58.0.tgz#a643983365e7bf7f5b7c62a8331b983b7c4c67fb"
- integrity sha512-NOXpusKnaRpbS7ZVSzcEXqxcLDOagN6iFS8p45RkoiMqPHDLwJm758UF05KlMoCRbLBTZsPOIa887gZJ1AiXvw==
- optionalDependencies:
- fsevents "~2.3.2"
-
-run-async@^2.4.0:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
- integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==
-
-run-parallel@^1.1.9:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
- integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
- dependencies:
- queue-microtask "^1.2.2"
-
-rxjs@^6.6.0:
- version "6.6.7"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
- integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
- dependencies:
- tslib "^1.9.0"
-
-rxjs@^7.2.0:
- version "7.4.0"
- resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68"
- integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w==
- dependencies:
- tslib "~2.1.0"
-
-safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0:
- version "5.2.1"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
- integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
-
-safe-buffer@~5.1.0, safe-buffer@~5.1.1:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
- integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
-"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
- integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
-
-scheduler@^0.20.2:
- version "0.20.2"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
- integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
- dependencies:
- loose-envify "^1.1.0"
- object-assign "^4.1.1"
-
-secure-compare@3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/secure-compare/-/secure-compare-3.0.1.tgz#f1a0329b308b221fae37b9974f3d578d0ca999e3"
- integrity sha1-8aAymzCLIh+uN7mXTz1XjQypmeM=
-
-semver-diff@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b"
- integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==
- dependencies:
- semver "^6.3.0"
-
-"semver@2 || 3 || 4 || 5":
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
- integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
-
-semver@5.6.0:
- version "5.6.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
- integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==
-
-semver@^6.0.0, semver@^6.1.0, semver@^6.2.0, semver@^6.3.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
- integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-semver@^7.2.1, semver@^7.3.4, semver@^7.3.5:
- version "7.3.5"
- resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
- integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
- dependencies:
- lru-cache "^6.0.0"
-
-semver@~2.3.1:
- version "2.3.2"
- resolved "https://registry.yarnpkg.com/semver/-/semver-2.3.2.tgz#b9848f25d6cf36333073ec9ef8856d42f1233e52"
- integrity sha1-uYSPJdbPNjMwc+ye+IVtQvEjPlI=
-
-set-blocking@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
- integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
-
-setprototypeof@1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683"
- integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
-
-sha.js@^2.4.0, sha.js@^2.4.8:
- version "2.4.11"
- resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7"
- integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==
- dependencies:
- inherits "^2.0.1"
- safe-buffer "^5.0.1"
-
-shebang-command@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
- integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
- dependencies:
- shebang-regex "^3.0.0"
-
-shebang-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
- integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
-
-side-channel@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
- integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==
- dependencies:
- call-bind "^1.0.0"
- get-intrinsic "^1.0.2"
- object-inspect "^1.9.0"
-
-signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
- integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
-
-slash@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
- integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
-
-slice-ansi@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
- integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==
- dependencies:
- ansi-styles "^4.0.0"
- astral-regex "^2.0.0"
- is-fullwidth-code-point "^3.0.0"
-
-socket.io-adapter@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz#edc5dc36602f2985918d631c1399215e97a1b527"
- integrity sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==
-
-socket.io-parser@~4.0.3:
- version "4.0.4"
- resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0"
- integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==
- dependencies:
- "@types/component-emitter" "^1.2.10"
- component-emitter "~1.3.0"
- debug "~4.3.1"
-
-socket.io@^3.1.0:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-3.1.2.tgz#06e27caa1c4fc9617547acfbb5da9bc1747da39a"
- integrity sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==
- dependencies:
- "@types/cookie" "^0.4.0"
- "@types/cors" "^2.8.8"
- "@types/node" ">=10.0.0"
- accepts "~1.3.4"
- base64id "~2.0.0"
- debug "~4.3.1"
- engine.io "~4.1.0"
- socket.io-adapter "~2.1.0"
- socket.io-parser "~4.0.3"
-
-source-map-resolve@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2"
- integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==
- dependencies:
- atob "^2.1.2"
- decode-uri-component "^0.2.0"
-
-source-map-support@0.5.9:
- version "0.5.9"
- resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f"
- integrity sha512-gR6Rw4MvUlYy83vP0vxoVNzM6t8MUXqNuRsuBmBHQDu1Fh6X015FrLdgoDKcNdkwGubozq0P4N0Q37UyFVr1EA==
- dependencies:
- buffer-from "^1.0.0"
- source-map "^0.6.0"
-
-source-map@^0.6.0, source-map@^0.6.1:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
- integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-
-source-map@^0.7.3:
- version "0.7.3"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
- integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
-
-sourcemap-codec@^1.4.4:
- version "1.4.8"
- resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
- integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
-
-spdx-correct@^3.0.0:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9"
- integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==
- dependencies:
- spdx-expression-parse "^3.0.0"
- spdx-license-ids "^3.0.0"
-
-spdx-exceptions@^2.1.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d"
- integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==
-
-spdx-expression-parse@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679"
- integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==
- dependencies:
- spdx-exceptions "^2.1.0"
- spdx-license-ids "^3.0.0"
-
-spdx-license-ids@^3.0.0:
- version "3.0.10"
- resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz#0d9becccde7003d6c658d487dd48a32f0bf3014b"
- integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
-
-sprintf-js@~1.0.2:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
- integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
-
-"statuses@>= 1.5.0 < 2", statuses@~1.5.0:
- version "1.5.0"
- resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
- integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
-
-streamroller@^2.2.4:
- version "2.2.4"
- resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-2.2.4.tgz#c198ced42db94086a6193608187ce80a5f2b0e53"
- integrity sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==
- dependencies:
- date-format "^2.1.0"
- debug "^4.1.1"
- fs-extra "^8.1.0"
-
-string-range@~1.2, string-range@~1.2.1:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/string-range/-/string-range-1.2.2.tgz#a893ed347e72299bc83befbbf2a692a8d239d5dd"
- integrity sha1-qJPtNH5yKZvIO++78qaSqNI51d0=
-
-"string-width@^1.0.1 || ^2.0.0":
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
- integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
- dependencies:
- is-fullwidth-code-point "^2.0.0"
- strip-ansi "^4.0.0"
-
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-string.prototype.matchall@^4.0.5:
- version "4.0.6"
- resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz#5abb5dabc94c7b0ea2380f65ba610b3a544b15fa"
- integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.3"
- es-abstract "^1.19.1"
- get-intrinsic "^1.1.1"
- has-symbols "^1.0.2"
- internal-slot "^1.0.3"
- regexp.prototype.flags "^1.3.1"
- side-channel "^1.0.4"
-
-string.prototype.trimend@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
- integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.3"
-
-string.prototype.trimstart@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed"
- integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==
- dependencies:
- call-bind "^1.0.2"
- define-properties "^1.1.3"
-
-string_decoder@^1.1.1:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
- integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
- dependencies:
- safe-buffer "~5.2.0"
-
-string_decoder@~0.10.x:
- version "0.10.31"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
- integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=
-
-string_decoder@~1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
- integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
- dependencies:
- safe-buffer "~5.1.0"
-
-"strip-ansi@^3.0.1 || ^4.0.0", strip-ansi@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
- integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
- dependencies:
- ansi-regex "^3.0.0"
-
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
-strip-final-newline@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
- integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
-
-strip-indent@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001"
- integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==
- dependencies:
- min-indent "^1.0.0"
-
-strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
- integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
-
-strip-json-comments@~2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
- integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
-
-style-to-js@1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.0.tgz#631cbb20fce204019b3aa1fcb5b69d951ceac4ac"
- integrity sha512-1OqefPDxGrlMwcbfpsTVRyzwdhr4W0uxYQzeA2F1CBc8WG04udg2+ybRnvh3XYL4TdHQrCahLtax2jc8xaE6rA==
- dependencies:
- style-to-object "0.3.0"
-
-style-to-object@0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46"
- integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==
- dependencies:
- inline-style-parser "0.1.1"
-
-supports-color@^5.3.0:
- version "5.5.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
- integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
- dependencies:
- has-flag "^3.0.0"
-
-supports-color@^7.1.0:
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da"
- integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
- dependencies:
- has-flag "^4.0.0"
-
-table@^6.0.9:
- version "6.7.2"
- resolved "https://registry.yarnpkg.com/table/-/table-6.7.2.tgz#a8d39b9f5966693ca8b0feba270a78722cbaf3b0"
- integrity sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==
- dependencies:
- ajv "^8.0.1"
- lodash.clonedeep "^4.5.0"
- lodash.truncate "^4.4.2"
- slice-ansi "^4.0.0"
- string-width "^4.2.3"
- strip-ansi "^6.0.1"
-
-tar@^6.1.11:
- version "6.1.11"
- resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
- integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
- dependencies:
- chownr "^2.0.0"
- fs-minipass "^2.0.0"
- minipass "^3.0.0"
- minizlib "^2.1.1"
- mkdirp "^1.0.3"
- yallist "^4.0.0"
-
-test-exclude@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e"
- integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==
- dependencies:
- "@istanbuljs/schema" "^0.1.2"
- glob "^7.1.4"
- minimatch "^3.0.4"
-
-text-table@^0.2.0:
- version "0.2.0"
- resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
- integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
-
-through@^2.3.6:
- version "2.3.8"
- resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
- integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
-
-tiny-warning@^1.0.2:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
- integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
-
-tmp@0.2.1, tmp@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
- integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==
- dependencies:
- rimraf "^3.0.0"
-
-tmp@^0.0.33:
- version "0.0.33"
- resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
- integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==
- dependencies:
- os-tmpdir "~1.0.2"
-
-to-readable-stream@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
- integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
-
-to-regex-range@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
- integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
- dependencies:
- is-number "^7.0.0"
-
-toidentifier@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
- integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
-
-tr46@~0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
- integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
-
-trim-newlines@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.1.tgz#260a5d962d8b752425b32f3a7db0dcacd176c144"
- integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==
-
-ts-protoc-gen@^0.15.0:
- version "0.15.0"
- resolved "https://registry.yarnpkg.com/ts-protoc-gen/-/ts-protoc-gen-0.15.0.tgz#2fec5930b46def7dcc9fa73c060d770b7b076b7b"
- integrity sha512-TycnzEyrdVDlATJ3bWFTtra3SCiEP0W0vySXReAuEygXCUr1j2uaVyL0DhzjwuUdQoW5oXPwk6oZWeA0955V+g==
- dependencies:
- google-protobuf "^3.15.5"
-
-tslib@^1.8.1, tslib@^1.9.0:
- version "1.14.1"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
- integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
-
-tslib@~2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
- integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
-
-tsutils@3.21.0, tsutils@^3.21.0:
- version "3.21.0"
- resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
- integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
- dependencies:
- tslib "^1.8.1"
-
-type-check@^0.4.0, type-check@~0.4.0:
- version "0.4.0"
- resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
- integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
- dependencies:
- prelude-ls "^1.2.1"
-
-type-fest@^0.18.0:
- version "0.18.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f"
- integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==
-
-type-fest@^0.20.2:
- version "0.20.2"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
- integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
-
-type-fest@^0.21.3:
- version "0.21.3"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
- integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
-
-type-fest@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
- integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==
-
-type-fest@^0.8.1:
- version "0.8.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
- integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
-
-type-is@~1.6.17:
- version "1.6.18"
- resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
- integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
- dependencies:
- media-typer "0.3.0"
- mime-types "~2.1.24"
-
-typedarray-to-buffer@^3.1.5:
- version "3.1.5"
- resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
- integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
- dependencies:
- is-typedarray "^1.0.0"
-
-typedarray-to-buffer@~1.0.0:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-1.0.4.tgz#9bb8ba0e841fb3f4cf1fe7c245e9f3fa8a5fe99c"
- integrity sha1-m7i6DoQfs/TPH+fCRenz+opf6Zw=
-
-typedarray@^0.0.6:
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
- integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
-
-typescript@^4.3.5:
- version "4.4.4"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.4.tgz#2cd01a1a1f160704d3101fd5a58ff0f9fcb8030c"
- integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
-
-ua-parser-js@^0.7.28:
- version "0.7.30"
- resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.30.tgz#4cf5170e8b55ac553fe8b38df3a82f0669671f0b"
- integrity sha512-uXEtSresNUlXQ1QL4/3dQORcGv7+J2ookOG2ybA/ga9+HYEXueT2o+8dUJQkpedsyTyCJ6jCCirRcKtdtx1kbg==
-
-unbox-primitive@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
- integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==
- dependencies:
- function-bind "^1.1.1"
- has-bigints "^1.0.1"
- has-symbols "^1.0.2"
- which-boxed-primitive "^1.0.2"
-
-union@~0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/union/-/union-0.5.0.tgz#b2c11be84f60538537b846edb9ba266ba0090075"
- integrity sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==
- dependencies:
- qs "^6.4.0"
-
-unique-string@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d"
- integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==
- dependencies:
- crypto-random-string "^2.0.0"
-
-universalify@^0.1.0:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
- integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
-
-unpipe@1.0.0, unpipe@~1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
- integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
-
-update-notifier@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-5.1.0.tgz#4ab0d7c7f36a231dd7316cf7729313f0214d9ad9"
- integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw==
- dependencies:
- boxen "^5.0.0"
- chalk "^4.1.0"
- configstore "^5.0.1"
- has-yarn "^2.1.0"
- import-lazy "^2.1.0"
- is-ci "^2.0.0"
- is-installed-globally "^0.4.0"
- is-npm "^5.0.0"
- is-yarn-global "^0.3.0"
- latest-version "^5.1.0"
- pupa "^2.1.1"
- semver "^7.3.4"
- semver-diff "^3.1.1"
- xdg-basedir "^4.0.0"
-
-uri-js@^4.2.2:
- version "4.4.1"
- resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
- integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
- dependencies:
- punycode "^2.1.0"
-
-url-join@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/url-join/-/url-join-2.0.5.tgz#5af22f18c052a000a48d7b82c5e9c2e2feeda728"
- integrity sha1-WvIvGMBSoACkjXuCxenC4v7tpyg=
-
-url-parse-lax@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
- integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
- dependencies:
- prepend-http "^2.0.0"
-
-util-deprecate@^1.0.1, util-deprecate@~1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
- integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
-
-utils-merge@1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
- integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
-
-v8-compile-cache@^2.0.3:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
- integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
-
-v8-to-istanbul@^7.1.0:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz#30898d1a7fa0c84d225a2c1434fb958f290883c1"
- integrity sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==
- dependencies:
- "@types/istanbul-lib-coverage" "^2.0.1"
- convert-source-map "^1.6.0"
- source-map "^0.7.3"
-
-validate-npm-package-license@^3.0.1:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
- integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
- dependencies:
- spdx-correct "^3.0.0"
- spdx-expression-parse "^3.0.0"
-
-vary@^1:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
- integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
-
-vlq@^0.2.2:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26"
- integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==
-
-void-elements@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
- integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
-
-wait-queue@^1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/wait-queue/-/wait-queue-1.1.4.tgz#344f9bdd6e011ddc0bb1e3252eeb41234f7a8a85"
- integrity sha512-/VdMghiBDG/Ch43ZRp3d8OSd8A0dx8hfkBO7AfWCDzMn2blHquMf+3gqHHhYcggSBpKf7VTzA939bb0DevYKBA==
-
-webidl-conversions@^3.0.0:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
- integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
-
-whatwg-url@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
- integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
- dependencies:
- tr46 "~0.0.3"
- webidl-conversions "^3.0.0"
-
-which-boxed-primitive@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6"
- integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==
- dependencies:
- is-bigint "^1.0.1"
- is-boolean-object "^1.1.0"
- is-number-object "^1.0.4"
- is-string "^1.0.5"
- is-symbol "^1.0.3"
-
-which@^1.2.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
- integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
- dependencies:
- isexe "^2.0.0"
-
-which@^2.0.1:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
- integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
- dependencies:
- isexe "^2.0.0"
-
-wide-align@^1.1.2:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
- integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
- dependencies:
- string-width "^1.0.2 || 2 || 3 || 4"
-
-widest-line@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca"
- integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==
- dependencies:
- string-width "^4.0.0"
-
-word-wrap@^1.2.3:
- version "1.2.3"
- resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
- integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
-
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
-wrappy@1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
- integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-
-write-file-atomic@^3.0.0, write-file-atomic@^3.0.3:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8"
- integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==
- dependencies:
- imurmurhash "^0.1.4"
- is-typedarray "^1.0.0"
- signal-exit "^3.0.2"
- typedarray-to-buffer "^3.1.5"
-
-ws@~7.4.2:
- version "7.4.6"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
- integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
-
-xdg-basedir@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
- integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==
-
-xmlbuilder@12.0.0:
- version "12.0.0"
- resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-12.0.0.tgz#e2ed675e06834a089ddfb84db96e2c2b03f78c1a"
- integrity sha512-lMo8DJ8u6JRWp0/Y4XLa/atVDr75H9litKlb2E5j3V3MesoL50EBgZDWoLT3F/LztVnG67GjPXLZpqcky/UMnQ==
-
-xmldom@^0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.5.0.tgz#193cb96b84aa3486127ea6272c4596354cb4962e"
- integrity sha512-Foaj5FXVzgn7xFzsKeNIde9g6aFBxTPi37iwsno8QvApmtg7KYrr+OPyRHcJF7dud2a5nGRBXK3n0dL62Gf7PA==
-
-xtend@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.2.0.tgz#eef6b1f198c1c8deafad8b1765a04dad4a01c5a9"
- integrity sha1-7vax8ZjByN6vrYsXZaBNrUoBxak=
-
-xtend@~2.0.4:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.0.6.tgz#5ea657a6dba447069c2e59c58a1138cb0c5e6cee"
- integrity sha1-XqZXptukRwacLlnFihE4ywxebO4=
- dependencies:
- is-object "~0.1.2"
- object-keys "~0.2.0"
-
-xtend@~2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"
- integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os=
- dependencies:
- object-keys "~0.4.0"
-
-xtend@~3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-3.0.0.tgz#5cce7407baf642cba7becda568111c493f59665a"
- integrity sha1-XM50B7r2Qsunvs2laBEcST9ZZlo=
-
-y18n@^5.0.5:
- version "5.0.8"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
- integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
-
-yallist@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
- integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
-
-yargs-parser@^20.0.0, yargs-parser@^20.2.2, yargs-parser@^20.2.3:
- version "20.2.9"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
- integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
-
-yargs@^16.0.0, yargs@^16.1.1:
- version "16.2.0"
- resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
- integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
- dependencies:
- cliui "^7.0.2"
- escalade "^3.1.1"
- get-caller-file "^2.0.5"
- require-directory "^2.1.1"
- string-width "^4.2.0"
- y18n "^5.0.5"
- yargs-parser "^20.2.2"
-
-yocto-queue@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
- integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==